Three.js使用gif动图作为纹理
迪丽瓦拉
2025-05-30 04:35:04
0

请添加图片描述
以上三张图片第一张使用计时器更新 第二张在onAfterRender中更新 第三张逐帧更新

参考 three-gif-loader
但是我在使用three-gif-loader时第一次播放动画产生问题 并且是计时器更新
以下是修改后的代码 可以编程式更新
代码仓库

使用方法
自动更新

const gifTexture = new GIFTexture("/textures/bomb.gif", "autoDraw");const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1),new THREE.MeshStandardMaterial({ map: gifTexture.image })
);
mesh.position.x -= 1;
helper.add(mesh);

编程更新(按需更新)

const gifTexture = new GIFTexture("/textures/bomb.gif");const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1),new THREE.MeshStandardMaterial({ map: gifTexture.image })
);mesh.onAfterRender = () => {gifTexture.draw();
};helper.add(mesh);

代码

/** @Author: hongbin* @Date: 2023-03-17 22:17:04* @LastEditors: hongbin* @LastEditTime: 2023-03-18 16:09:24* @Description:*/
import {NearestFilter,LinearMipMapLinearFilter,sRGBEncoding,EquirectangularReflectionMapping,
} from "three";import GifLoader from "./gif-loader";export class GIFTexture extends GifLoader {image;constructor(path: string,/*** 自动播放gif 定时器循环执行*/autoDraw?: "autoDraw") {super();let image = this.load(path);autoDraw && image.autoDraw();// optionsimage.mapping = EquirectangularReflectionMapping;image.encoding = sRGBEncoding;image.magFilter = NearestFilter;image.minFilter = LinearMipMapLinearFilter;this.image = image;}draw() {this.image.draw();}
}
/** @Author: hongbin* @Date: 2023-03-17 21:48:36* @LastEditors: hongbin* @LastEditTime: 2023-03-18 15:46:54* @Description:*/
import { FileLoader } from 'three/src/loaders/FileLoader';
import { DefaultLoadingManager } from 'three/src/loaders/LoadingManager';
import GifTexture from './gif-texture';
// import { GifReader } from 'omggif';
import { GifReader } from './omggif';export default class GifLoader {constructor(manager, autoDraw) {this.manager = manager || DefaultLoadingManager;this.crossOrigin = 'anonymous';this.autoDraw = autoDraw;}load(url, onLoad, onProgress, onError) {const texture = new GifTexture();const loader = new FileLoader(this.manager);loader.setResponseType('arraybuffer');loader.load(url, (response) => {const gifData = new Uint8Array(response);const reader = new GifReader(gifData);texture.setReader(reader);if (onLoad) onLoad(reader);}, onProgress, onError);return texture;}
}
/** @Author: hongbin* @Date: 2023-03-17 21:46:30* @LastEditors: hongbin* @LastEditTime: 2023-03-18 17:48:08* @Description:*/
import { Clock } from 'three';
import { CanvasTexture } from 'three/src/textures/CanvasTexture';export default class GifTexture extends CanvasTexture {constructor(image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy) {super(image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy);this.needsUpdate = false;}setReader(reader) {this.reader = reader;this.numFrames = reader.numFrames();this.image = document.createElement('canvas');this.image.width = reader.width;this.image.height = reader.height;this.context = this.image.getContext('2d');this.frameNumber = 0;this.previousFrameInfo = null;this.clock = new Clock()if (this._autoDraw) {this.autoDraw()}}draw() {if (!this.reader) {return;}const { reader, image, context, clock } = this;const { width, height } = image;const frameNum = Math.floor(this.frameNumber % this.numFrames);const frameInfo = reader.frameInfo(frameNum);if (frameNum === 0) {context.clearRect(0, 0, width, height);} else if (this.previousFrameInfo && this.previousFrameInfo.disposal === 2) {context.clearRect(this.previousFrameInfo.x,this.previousFrameInfo.y,this.previousFrameInfo.width,this.previousFrameInfo.height);}// this.frameNumber += 0.01 * frameInfo.delay;// 在onAfterRender中更新会比正常的逐帧更新多一次运行(当相机在轨道控制器中移动时 所以根据时间长度播放进度)this.frameNumber += clock.getDelta() * 0.5 * frameInfo.delay;const imageData = context.getImageData(0, 0, width, height);reader.decodeAndBlitFrameRGBA(frameNum, imageData.data);context.putImageData(imageData, 0, 0);this.needsUpdate = true;this.previousFrameInfo = frameInfo;}/*** 开启定时器自动更新*/autoDraw() {this._autoDraw = true;if (!this.reader) {return;}const { reader, image, context } = this;const { width, height } = image;const frameNum = this.frameNumber % this.numFrames;const frameInfo = reader.frameInfo(frameNum);if (frameNum === 0) {context.clearRect(0, 0, width, height);// always clear canvas to start} else if (this.previousFrameInfo && this.previousFrameInfo.disposal === 2) {// disposal was "restore to background" which is essentially "restore to transparent"context.clearRect(this.previousFrameInfo.x,this.previousFrameInfo.y,this.previousFrameInfo.width,this.previousFrameInfo.height);}this.frameNumber++;const imageData = context.getImageData(0, 0, width, height);reader.decodeAndBlitFrameRGBA(frameNum, imageData.data);context.putImageData(imageData, 0, 0);this.needsUpdate = true;this.previousFrameInfo = frameInfo;setTimeout(this.autoDraw.bind(this), frameInfo.delay * 10);}};
/** @Author: hongbin* @Date: 2023-03-18 15:46:30* @LastEditors: hongbin* @LastEditTime: 2023-03-18 15:51:05* @Description: */
// (c) Dean McNamee , 2013.
//
// https://github.com/deanm/omggif
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,
// including animation and compression.  It does not rely on any specific
// underlying system, so should run in the browser, Node, or Plask."use strict";function GifReader(buf) {var p = 0;// - Header (GIF87a or GIF89a).if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||buf[p++] !== 0x38 || (buf[p++] + 1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {throw new Error("Invalid GIF 87a/89a header.");}// - Logical Screen Descriptor.var width = buf[p++] | buf[p++] << 8;var height = buf[p++] | buf[p++] << 8;var pf0 = buf[p++];  // .var global_palette_flag = pf0 >> 7;var num_global_colors_pow2 = pf0 & 0x7;var num_global_colors = 1 << (num_global_colors_pow2 + 1);var background = buf[p++];buf[p++];  // Pixel aspect ratio (unused?).var global_palette_offset = null;var global_palette_size = null;if (global_palette_flag) {global_palette_offset = p;global_palette_size = num_global_colors;p += num_global_colors * 3;  // Seek past palette.}var no_eof = true;var frames = [];var delay = 0;var transparent_index = null;var disposal = 0;  // 0 - No disposal specified.var loop_count = null;this.width = width;this.height = height;while (no_eof && p < buf.length) {switch (buf[p++]) {case 0x21:  // Graphics Control Extension Blockswitch (buf[p++]) {case 0xff:  // Application specific block// Try if it's a Netscape block (with animation loop counter).if (buf[p] !== 0x0b ||  // 21 FF already read, check block size.// NETSCAPE2.0buf[p + 1] == 0x4e && buf[p + 2] == 0x45 && buf[p + 3] == 0x54 &&buf[p + 4] == 0x53 && buf[p + 5] == 0x43 && buf[p + 6] == 0x41 &&buf[p + 7] == 0x50 && buf[p + 8] == 0x45 && buf[p + 9] == 0x32 &&buf[p + 10] == 0x2e && buf[p + 11] == 0x30 &&// Sub-blockbuf[p + 12] == 0x03 && buf[p + 13] == 0x01 && buf[p + 16] == 0) {p += 14;loop_count = buf[p++] | buf[p++] << 8;p++;  // Skip terminator.} else {  // We don't know what it is, just try to get past it.p += 12;while (true) {  // Seek through subblocks.var block_size = buf[p++];// Bad block size (ex: undefined from an out of bounds read).if (!(block_size >= 0)) throw Error("Invalid block size");if (block_size === 0) break;  // 0 size is terminatorp += block_size;}}break;case 0xf9:  // Graphics Control Extensionif (buf[p++] !== 0x4 || buf[p + 4] !== 0)throw new Error("Invalid graphics extension block.");var pf1 = buf[p++];delay = buf[p++] | buf[p++] << 8;transparent_index = buf[p++];if ((pf1 & 1) === 0) transparent_index = null;disposal = pf1 >> 2 & 0x7;p++;  // Skip terminator.break;case 0xfe:  // Comment Extension.while (true) {  // Seek through subblocks.var block_size = buf[p++];// Bad block size (ex: undefined from an out of bounds read).if (!(block_size >= 0)) throw Error("Invalid block size");if (block_size === 0) break;  // 0 size is terminator// console.log(buf.slice(p, p+block_size).toString('ascii'));p += block_size;}break;default:throw new Error("Unknown graphic control label: 0x" + buf[p - 1].toString(16));}break;case 0x2c:  // Image Descriptor.var x = buf[p++] | buf[p++] << 8;var y = buf[p++] | buf[p++] << 8;var w = buf[p++] | buf[p++] << 8;var h = buf[p++] | buf[p++] << 8;var pf2 = buf[p++];var local_palette_flag = pf2 >> 7;var interlace_flag = pf2 >> 6 & 1;var num_local_colors_pow2 = pf2 & 0x7;var num_local_colors = 1 << (num_local_colors_pow2 + 1);var palette_offset = global_palette_offset;var palette_size = global_palette_size;var has_local_palette = false;if (local_palette_flag) {var has_local_palette = true;palette_offset = p;  // Override with local palette.palette_size = num_local_colors;p += num_local_colors * 3;  // Seek past palette.}var data_offset = p;p++;  // codesizewhile (true) {var block_size = buf[p++];// Bad block size (ex: undefined from an out of bounds read).if (!(block_size >= 0)) throw Error("Invalid block size");if (block_size === 0) break;  // 0 size is terminatorp += block_size;}frames.push({x: x, y: y, width: w, height: h,has_local_palette: has_local_palette,palette_offset: palette_offset,palette_size: palette_size,data_offset: data_offset,data_length: p - data_offset,transparent_index: transparent_index,interlaced: !!interlace_flag,delay: delay,disposal: disposal});break;case 0x3b:  // Trailer Marker (end of file).no_eof = false;break;default:throw new Error("Unknown gif block: 0x" + buf[p - 1].toString(16));break;}}this.numFrames = function () {return frames.length;};this.loopCount = function () {return loop_count;};this.frameInfo = function (frame_num) {if (frame_num < 0 || frame_num >= frames.length)throw new Error("Frame index out of range.");return frames[frame_num];}// I will go to copy and paste hell one day...this.decodeAndBlitFrameRGBA = function (frame_num, pixels) {var frame = this.frameInfo(frame_num);var num_pixels = frame.width * frame.height;var index_stream = new Uint8Array(num_pixels);  // At most 8-bit indices.GifReaderLZWOutputIndexStream(buf, frame.data_offset, index_stream, num_pixels);var palette_offset = frame.palette_offset;// NOTE(deanm): It seems to be much faster to compare index to 256 than// to === null.  Not sure why, but CompareStub_EQ_STRICT shows up high in// the profile, not sure if it's related to using a Uint8Array.var trans = frame.transparent_index;if (trans === null) trans = 256;// We are possibly just blitting to a portion of the entire frame.// That is a subrect within the framerect, so the additional pixels// must be skipped over after we finished a scanline.var framewidth = frame.width;var framestride = width - framewidth;var xleft = framewidth;  // Number of subrect pixels left in scanline.// Output indicies of the top left and bottom right corners of the subrect.var opbeg = ((frame.y * width) + frame.x) * 4;var opend = ((frame.y + frame.height) * width + frame.x) * 4;var op = opbeg;var scanstride = framestride * 4;// Use scanstride to skip past the rows when interlacing.  This is skipping// 7 rows for the first two passes, then 3 then 1.if (frame.interlaced === true) {scanstride += width * 4 * 7;  // Pass 1.}var interlaceskip = 8;  // Tracking the row interval in the current pass.for (var i = 0, il = index_stream.length; i < il; ++i) {var index = index_stream[i];if (xleft === 0) {  // Beginning of new scan lineop += scanstride;xleft = framewidth;if (op >= opend) { // Catch the wrap to switch passes when interlacing.scanstride = framestride * 4 + width * 4 * (interlaceskip - 1);// interlaceskip / 2 * 4 is interlaceskip << 1.op = opbeg + (framewidth + framestride) * (interlaceskip << 1);interlaceskip >>= 1;}}if (index === trans) {op += 4;} else {var r = buf[palette_offset + index * 3];var g = buf[palette_offset + index * 3 + 1];var b = buf[palette_offset + index * 3 + 2];pixels[op++] = r;pixels[op++] = g;pixels[op++] = b;pixels[op++] = 255;}--xleft;}};
}function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {var min_code_size = code_stream[p++];var clear_code = 1 << min_code_size;var eoi_code = clear_code + 1;var next_code = eoi_code + 1;var cur_code_size = min_code_size + 1;  // Number of bits per code.// NOTE: This shares the same name as the encoder, but has a different// meaning here.  Here this masks each code coming from the code stream.var code_mask = (1 << cur_code_size) - 1;var cur_shift = 0;var cur = 0;var op = 0;  // Output pointer.var subblock_size = code_stream[p++];// TODO(deanm): Would using a TypedArray be any faster?  At least it would// solve the fast mode / backing store uncertainty.// var code_table = Array(4096);var code_table = new Int32Array(4096);  // Can be signed, we only use 20 bits.var prev_code = null;  // Track code-1.while (true) {// Read up to two bytes, making sure we always 12-bits for max sized code.while (cur_shift < 16) {if (subblock_size === 0) break;  // No more data to be read.cur |= code_stream[p++] << cur_shift;cur_shift += 8;if (subblock_size === 1) {  // Never let it get to 0 to hold logic above.subblock_size = code_stream[p++];  // Next subblock.} else {--subblock_size;}}// TODO(deanm): We should never really get here, we should have received// and EOI.if (cur_shift < cur_code_size)break;var code = cur & code_mask;cur >>= cur_code_size;cur_shift -= cur_code_size;// TODO(deanm): Maybe should check that the first code was a clear code,// at least this is what you're supposed to do.  But actually our encoder// now doesn't emit a clear code first anyway.if (code === clear_code) {// We don't actually have to clear the table.  This could be a good idea// for greater error checking, but we don't really do any anyway.  We// will just track it with next_code and overwrite old entries.next_code = eoi_code + 1;cur_code_size = min_code_size + 1;code_mask = (1 << cur_code_size) - 1;// Don't update prev_code ?prev_code = null;continue;} else if (code === eoi_code) {break;}// We have a similar situation as the decoder, where we want to store// variable length entries (code table entries), but we want to do in a// faster manner than an array of arrays.  The code below stores sort of a// linked list within the code table, and then "chases" through it to// construct the dictionary entries.  When a new entry is created, just the// last byte is stored, and the rest (prefix) of the entry is only// referenced by its table entry.  Then the code chases through the// prefixes until it reaches a single byte code.  We have to chase twice,// first to compute the length, and then to actually copy the data to the// output (backwards, since we know the length).  The alternative would be// storing something in an intermediate stack, but that doesn't make any// more sense.  I implemented an approach where it also stored the length// in the code table, although it's a bit tricky because you run out of// bits (12 + 12 + 8), but I didn't measure much improvements (the table// entries are generally not the long).  Even when I created benchmarks for// very long table entries the complexity did not seem worth it.// The code table stores the prefix entry in 12 bits and then the suffix// byte in 8 bits, so each entry is 20 bits.var chase_code = code < next_code ? code : prev_code;// Chase what we will output, either {CODE} or {CODE-1}.var chase_length = 0;var chase = chase_code;while (chase > clear_code) {chase = code_table[chase] >> 8;++chase_length;}var k = chase;var op_end = op + chase_length + (chase_code !== code ? 1 : 0);if (op_end > output_length) {console.log("Warning, gif stream longer than expected.");return;}// Already have the first byte from the chase, might as well write it fast.output[op++] = k;op += chase_length;var b = op;  // Track pointer, writing backwards.if (chase_code !== code)  // The case of emitting {CODE-1} + k.output[op++] = k;chase = chase_code;while (chase_length--) {chase = code_table[chase];output[--b] = chase & 0xff;  // Write backwards.chase >>= 8;  // Pull down to the prefix code.}if (prev_code !== null && next_code < 4096) {code_table[next_code++] = prev_code << 8 | k;// TODO(deanm): Figure out this clearing vs code growth logic better.  I// have an feeling that it should just happen somewhere else, for now it// is awkward between when we grow past the max and then hit a clear code.// For now just check if we hit the max 12-bits (then a clear code should// follow, also of course encoded in 12-bits).if (next_code >= code_mask + 1 && cur_code_size < 12) {++cur_code_size;code_mask = code_mask << 1 | 1;}}prev_code = code;}if (op !== output_length) {console.log("Warning, gif stream shorter than expected.");}return output;
}// CommonJS.
try { exports.GifReader = GifReader } catch (e) { }

相关内容