Spaces:
Sleeping
Sleeping
| /** | |
| * GPU-Accelerated Draco Mesh Decompression | |
| * Uses WebGL compute shaders to efficiently decompress Draco-encoded 3D meshes | |
| */ | |
| class DracoGPUDecompressor { | |
| constructor() { | |
| this.gl = null; | |
| this.extensions = null; | |
| this.programs = {}; | |
| this.buffers = {}; | |
| this.textures = {}; | |
| this.dracoDecoder = null; | |
| this.isInitialized = false; | |
| this.maxVertices = 100000; // Maximum vertices to process in one batch | |
| this.maxIndices = 300000; // Maximum indices to process in one batch | |
| // Initialize WebGL and shaders | |
| this.initialize(); | |
| } | |
| /** | |
| * Initialize WebGL context and required extensions | |
| */ | |
| async initialize() { | |
| try { | |
| // Create WebGL 2.0 context | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 1; | |
| canvas.height = 1; | |
| this.gl = canvas.getContext('webgl2', { | |
| premultipliedAlpha: false, | |
| antialias: false, | |
| depth: false, | |
| stencil: false | |
| }); | |
| if (!this.gl) { | |
| throw new Error('WebGL 2.0 not supported'); | |
| } | |
| // Check for required extensions | |
| this.extensions = { | |
| computeShader: this.gl.getExtension('OES_texture_float_linear'), | |
| floatTextures: this.gl.getExtension('OES_texture_float'), | |
| drawBuffers: this.gl.getExtension('WEBGL_draw_buffers'), | |
| instancedArrays: this.gl.getExtension('ANGLE_instanced_arrays') | |
| }; | |
| // Make sure we have the required extensions | |
| if (!this.extensions.floatTextures) { | |
| throw new Error('OES_texture_float extension not supported'); | |
| } | |
| // Initialize Draco decoder (web worker) | |
| await this.initDracoDecoder(); | |
| // Compile shaders | |
| await this.initShaders(); | |
| console.log('GPU Draco decompressor initialized successfully'); | |
| this.isInitialized = true; | |
| } catch (error) { | |
| console.error('Failed to initialize GPU Draco decompressor:', error); | |
| // Fall back to CPU-based decompression | |
| this.useFallback = true; | |
| } | |
| } | |
| /** | |
| * Initialize Draco decoder (CPU-side preprocessing) | |
| */ | |
| async initDracoDecoder() { | |
| return new Promise((resolve, reject) => { | |
| // Create a web worker for initial Draco decoding steps | |
| this.dracoWorker = new Worker('/static/js/workers/draco_worker.js'); | |
| // Handle worker messages | |
| this.dracoWorker.onmessage = (e) => { | |
| const { type, result } = e.data; | |
| if (type === 'initialized') { | |
| resolve(); | |
| } else if (type === 'error') { | |
| reject(new Error(result.message)); | |
| } | |
| }; | |
| // Initialize worker | |
| this.dracoWorker.postMessage({ type: 'initialize' }); | |
| }); | |
| } | |
| /** | |
| * Initialize WebGL shader programs for decompression steps | |
| */ | |
| async initShaders() { | |
| // Shader to decompress vertex positions | |
| const positionVertexShader = ` | |
| #version 300 es | |
| in vec2 position; | |
| in vec2 texCoord; | |
| out vec2 vTexCoord; | |
| void main() { | |
| vTexCoord = texCoord; | |
| gl_Position = vec4(position, 0.0, 1.0); | |
| } | |
| `; | |
| // Shader to decompress vertex positions | |
| const positionFragmentShader = ` | |
| #version 300 es | |
| precision highp float; | |
| precision highp sampler2D; | |
| in vec2 vTexCoord; | |
| out vec4 fragColor; | |
| uniform sampler2D uOctCoordsTex; | |
| uniform sampler2D uQuantizedPosTex; | |
| uniform vec3 uDequantizationFactors; | |
| uniform vec3 uPositionOffset; | |
| void main() { | |
| vec4 quantized = texture(uQuantizedPosTex, vTexCoord); | |
| vec3 position = quantized.xyz * uDequantizationFactors + uPositionOffset; | |
| fragColor = vec4(position, 1.0); | |
| } | |
| `; | |
| // Shader to decompress normals | |
| const normalFragmentShader = ` | |
| #version 300 es | |
| precision highp float; | |
| precision highp sampler2D; | |
| in vec2 vTexCoord; | |
| out vec4 fragColor; | |
| uniform sampler2D uOctCoordsTex; | |
| // Octahedral normal unpacking | |
| vec3 decodeOctNormal(vec2 oct) { | |
| oct = oct * 2.0 - 1.0; // from [0,1] to [-1,1] | |
| // Decode the octahedral normal encoding | |
| vec3 normal; | |
| normal.z = 1.0 - abs(oct.x) - abs(oct.y); | |
| if (normal.z < 0.0) { | |
| normal.xy = (1.0 - abs(oct.yx)) * vec2( | |
| oct.x >= 0.0 ? 1.0 : -1.0, | |
| oct.y >= 0.0 ? 1.0 : -1.0 | |
| ); | |
| } else { | |
| normal.xy = oct.xy; | |
| } | |
| return normalize(normal); | |
| } | |
| void main() { | |
| vec2 octCoords = texture(uOctCoordsTex, vTexCoord).xy; | |
| vec3 normal = decodeOctNormal(octCoords); | |
| fragColor = vec4(normal * 0.5 + 0.5, 1.0); | |
| } | |
| `; | |
| // Shader to decompress texture coordinates | |
| const uvFragmentShader = ` | |
| #version 300 es | |
| precision highp float; | |
| precision highp sampler2D; | |
| in vec2 vTexCoord; | |
| out vec4 fragColor; | |
| uniform sampler2D uQuantizedUVsTex; | |
| uniform vec2 uUVFactors; | |
| void main() { | |
| vec2 quantizedUV = texture(uQuantizedUVsTex, vTexCoord).xy; | |
| vec2 uv = quantizedUV * uUVFactors; | |
| fragColor = vec4(uv, 0.0, 1.0); | |
| } | |
| `; | |
| // Create shader programs | |
| this.programs.position = this.createProgram(positionVertexShader, positionFragmentShader); | |
| this.programs.normal = this.createProgram(positionVertexShader, normalFragmentShader); | |
| this.programs.uv = this.createProgram(positionVertexShader, uvFragmentShader); | |
| // Create a full-screen quad for rendering to textures | |
| this.createFullScreenQuad(); | |
| } | |
| /** | |
| * Create a WebGL shader program from vertex and fragment shader sources | |
| */ | |
| createProgram(vertexShaderSource, fragmentShaderSource) { | |
| const gl = this.gl; | |
| // Create and compile vertex shader | |
| const vertexShader = gl.createShader(gl.VERTEX_SHADER); | |
| gl.shaderSource(vertexShader, vertexShaderSource); | |
| gl.compileShader(vertexShader); | |
| if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { | |
| const error = gl.getShaderInfoLog(vertexShader); | |
| gl.deleteShader(vertexShader); | |
| throw new Error(`Failed to compile vertex shader: ${error}`); | |
| } | |
| // Create and compile fragment shader | |
| const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); | |
| gl.shaderSource(fragmentShader, fragmentShaderSource); | |
| gl.compileShader(fragmentShader); | |
| if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { | |
| const error = gl.getShaderInfoLog(fragmentShader); | |
| gl.deleteShader(vertexShader); | |
| gl.deleteShader(fragmentShader); | |
| throw new Error(`Failed to compile fragment shader: ${error}`); | |
| } | |
| // Create and link program | |
| const program = gl.createProgram(); | |
| gl.attachShader(program, vertexShader); | |
| gl.attachShader(program, fragmentShader); | |
| gl.linkProgram(program); | |
| if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
| const error = gl.getProgramInfoLog(program); | |
| gl.deleteProgram(program); | |
| gl.deleteShader(vertexShader); | |
| gl.deleteShader(fragmentShader); | |
| throw new Error(`Failed to link shader program: ${error}`); | |
| } | |
| // Clean up shaders, they're linked into the program now | |
| gl.deleteShader(vertexShader); | |
| gl.deleteShader(fragmentShader); | |
| // Get attribute and uniform locations | |
| const attributes = {}; | |
| const uniforms = {}; | |
| // Get attribute locations | |
| const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); | |
| for (let i = 0; i < numAttributes; i++) { | |
| const info = gl.getActiveAttrib(program, i); | |
| attributes[info.name] = gl.getAttribLocation(program, info.name); | |
| } | |
| // Get uniform locations | |
| const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); | |
| for (let i = 0; i < numUniforms; i++) { | |
| const info = gl.getActiveUniform(program, i); | |
| uniforms[info.name] = gl.getUniformLocation(program, info.name); | |
| } | |
| return { program, attributes, uniforms }; | |
| } | |
| /** | |
| * Create a full-screen quad for rendering to textures | |
| */ | |
| createFullScreenQuad() { | |
| const gl = this.gl; | |
| // Vertex positions for a full-screen quad (2 triangles) | |
| const positions = new Float32Array([ | |
| -1, -1, | |
| 1, -1, | |
| -1, 1, | |
| 1, 1 | |
| ]); | |
| // Texture coordinates for the quad | |
| const texCoords = new Float32Array([ | |
| 0, 0, | |
| 1, 0, | |
| 0, 1, | |
| 1, 1 | |
| ]); | |
| // Create and bind vertex position buffer | |
| const positionBuffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW); | |
| this.buffers.quadPosition = positionBuffer; | |
| // Create and bind texture coordinate buffer | |
| const texCoordBuffer = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); | |
| gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW); | |
| this.buffers.quadTexCoord = texCoordBuffer; | |
| // Vertex Array Object for the quad | |
| if (this.gl instanceof WebGL2RenderingContext) { | |
| const vao = gl.createVertexArray(); | |
| gl.bindVertexArray(vao); | |
| // Set up position attribute | |
| gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
| gl.enableVertexAttribArray(0); | |
| gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); | |
| // Set up texture coordinate attribute | |
| gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); | |
| gl.enableVertexAttribArray(1); | |
| gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0); | |
| gl.bindVertexArray(null); | |
| this.buffers.quadVAO = vao; | |
| } | |
| } | |
| /** | |
| * Create a framebuffer with texture attachments for output | |
| * @param {number} width - Texture width | |
| * @param {number} height - Texture height | |
| * @returns {Object} - Framebuffer and attached textures | |
| */ | |
| createOutputFramebuffer(width, height) { | |
| const gl = this.gl; | |
| // Create framebuffer | |
| const framebuffer = gl.createFramebuffer(); | |
| gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); | |
| // Create output texture | |
| const texture = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, width, height, 0, gl.RGBA, gl.FLOAT, null); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| // Attach texture to framebuffer | |
| gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); | |
| // Check framebuffer status | |
| const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); | |
| if (status !== gl.FRAMEBUFFER_COMPLETE) { | |
| throw new Error(`Framebuffer not complete: ${status}`); | |
| } | |
| // Unbind framebuffer | |
| gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
| return { framebuffer, texture }; | |
| } | |
| /** | |
| * Create a texture from data | |
| * @param {TypedArray} data - Data to upload to the texture | |
| * @param {number} width - Texture width | |
| * @param {number} height - Texture height | |
| * @param {number} format - Texture format (e.g., gl.RGBA) | |
| * @param {number} type - Texture data type (e.g., gl.FLOAT) | |
| * @param {number} internalFormat - Texture internal format (e.g., gl.RGBA32F) | |
| * @returns {WebGLTexture} - Created texture | |
| */ | |
| createDataTexture(data, width, height, format, type, internalFormat) { | |
| const gl = this.gl; | |
| const texture = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, data); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| return texture; | |
| } | |
| /** | |
| * Run a decompression shader | |
| * @param {string} programName - Name of the shader program to use | |
| * @param {Object} textures - Input textures | |
| * @param {Object} uniforms - Uniform values | |
| * @param {number} width - Output width | |
| * @param {number} height - Output height | |
| * @returns {Float32Array} - Decompressed data | |
| */ | |
| runDecompressionShader(programName, textures, uniforms, width, height) { | |
| const gl = this.gl; | |
| const program = this.programs[programName]; | |
| // Create output framebuffer | |
| const output = this.createOutputFramebuffer(width, height); | |
| // Bind framebuffer | |
| gl.bindFramebuffer(gl.FRAMEBUFFER, output.framebuffer); | |
| gl.viewport(0, 0, width, height); | |
| // Use shader program | |
| gl.useProgram(program.program); | |
| // Bind quad VAO | |
| if (this.gl instanceof WebGL2RenderingContext) { | |
| gl.bindVertexArray(this.buffers.quadVAO); | |
| } else { | |
| // For WebGL 1, set up attributes manually | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.quadPosition); | |
| gl.enableVertexAttribArray(program.attributes.position); | |
| gl.vertexAttribPointer(program.attributes.position, 2, gl.FLOAT, false, 0, 0); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.quadTexCoord); | |
| gl.enableVertexAttribArray(program.attributes.texCoord); | |
| gl.vertexAttribPointer(program.attributes.texCoord, 2, gl.FLOAT, false, 0, 0); | |
| } | |
| // Bind input textures | |
| let textureUnit = 0; | |
| for (const [name, texture] of Object.entries(textures)) { | |
| gl.activeTexture(gl.TEXTURE0 + textureUnit); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.uniform1i(program.uniforms[name], textureUnit); | |
| textureUnit++; | |
| } | |
| // Set uniforms | |
| for (const [name, value] of Object.entries(uniforms)) { | |
| const location = program.uniforms[name]; | |
| if (location) { | |
| if (Array.isArray(value) && value.length === 2) { | |
| gl.uniform2fv(location, value); | |
| } else if (Array.isArray(value) && value.length === 3) { | |
| gl.uniform3fv(location, value); | |
| } else if (Array.isArray(value) && value.length === 4) { | |
| gl.uniform4fv(location, value); | |
| } else if (Array.isArray(value) && value.length === 9) { | |
| gl.uniformMatrix3fv(location, false, value); | |
| } else if (Array.isArray(value) && value.length === 16) { | |
| gl.uniformMatrix4fv(location, false, value); | |
| } else { | |
| gl.uniform1f(location, value); | |
| } | |
| } | |
| } | |
| // Draw quad | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| // Read back data | |
| const data = new Float32Array(width * height * 4); | |
| gl.readPixels(0, 0, width, height, gl.RGBA, gl.FLOAT, data); | |
| // Clean up | |
| gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
| gl.deleteFramebuffer(output.framebuffer); | |
| gl.deleteTexture(output.texture); | |
| return data; | |
| } | |
| /** | |
| * Decompress a Draco-encoded mesh using GPU acceleration | |
| * @param {ArrayBuffer} dracoData - Draco-encoded mesh data | |
| * @returns {Promise<Object>} - Decompressed mesh data | |
| */ | |
| async decompressMesh(dracoData) { | |
| if (!this.isInitialized) { | |
| await new Promise(resolve => { | |
| const checkInitialized = () => { | |
| if (this.isInitialized) resolve(); | |
| else setTimeout(checkInitialized, 100); | |
| }; | |
| checkInitialized(); | |
| }); | |
| } | |
| // If we had to fall back to CPU decompression | |
| if (this.useFallback) { | |
| return this.decompressMeshCPU(dracoData); | |
| } | |
| try { | |
| // First, use Draco decoder to extract the compressed data into a format | |
| // suitable for GPU processing | |
| const preparedData = await this.prepareDracoData(dracoData); | |
| // Extract geometry metadata | |
| const { | |
| vertexCount, | |
| indexCount, | |
| positionData, | |
| normalData, | |
| uvData, | |
| indices, | |
| positionDequantizationFactors, | |
| positionOffset, | |
| uvFactors | |
| } = preparedData; | |
| // Calculate texture dimensions | |
| const texWidth = Math.ceil(Math.sqrt(vertexCount)); | |
| const texHeight = Math.ceil(vertexCount / texWidth); | |
| // Create input textures | |
| const textures = {}; | |
| // Position data texture | |
| if (positionData) { | |
| textures.uQuantizedPosTex = this.createDataTexture( | |
| positionData, | |
| texWidth, | |
| texHeight, | |
| this.gl.RGBA, | |
| this.gl.FLOAT, | |
| this.gl.RGBA32F | |
| ); | |
| } | |
| // Normal data texture (octahedral encoded) | |
| if (normalData) { | |
| textures.uOctCoordsTex = this.createDataTexture( | |
| normalData, | |
| texWidth, | |
| texHeight, | |
| this.gl.RGBA, | |
| this.gl.FLOAT, | |
| this.gl.RGBA32F | |
| ); | |
| } | |
| // UV data texture | |
| if (uvData) { | |
| textures.uQuantizedUVsTex = this.createDataTexture( | |
| uvData, | |
| texWidth, | |
| texHeight, | |
| this.gl.RGBA, | |
| this.gl.FLOAT, | |
| this.gl.RGBA32F | |
| ); | |
| } | |
| // Decompress positions | |
| const positions = positionData ? this.runDecompressionShader( | |
| 'position', | |
| { | |
| uQuantizedPosTex: textures.uQuantizedPosTex | |
| }, | |
| { | |
| uDequantizationFactors: positionDequantizationFactors, | |
| uPositionOffset: positionOffset | |
| }, | |
| texWidth, | |
| texHeight | |
| ) : null; | |
| // Decompress normals | |
| const normals = normalData ? this.runDecompressionShader( | |
| 'normal', | |
| { | |
| uOctCoordsTex: textures.uOctCoordsTex | |
| }, | |
| {}, | |
| texWidth, | |
| texHeight | |
| ) : null; | |
| // Decompress UVs | |
| const uvs = uvData ? this.runDecompressionShader( | |
| 'uv', | |
| { | |
| uQuantizedUVsTex: textures.uQuantizedUVsTex | |
| }, | |
| { | |
| uUVFactors: uvFactors | |
| }, | |
| texWidth, | |
| texHeight | |
| ) : null; | |
| // Clean up textures | |
| for (const texture of Object.values(textures)) { | |
| this.gl.deleteTexture(texture); | |
| } | |
| // Prepare result | |
| const result = { | |
| vertexCount, | |
| indexCount, | |
| indices | |
| }; | |
| if (positions) { | |
| // Extract positions from RGBA texture data | |
| result.positions = new Float32Array(vertexCount * 3); | |
| for (let i = 0; i < vertexCount; i++) { | |
| result.positions[i * 3] = positions[i * 4]; | |
| result.positions[i * 3 + 1] = positions[i * 4 + 1]; | |
| result.positions[i * 3 + 2] = positions[i * 4 + 2]; | |
| } | |
| } | |
| if (normals) { | |
| // Extract normals from RGBA texture data | |
| result.normals = new Float32Array(vertexCount * 3); | |
| for (let i = 0; i < vertexCount; i++) { | |
| // Convert from [0,1] to [-1,1] range | |
| result.normals[i * 3] = normals[i * 4] * 2 - 1; | |
| result.normals[i * 3 + 1] = normals[i * 4 + 1] * 2 - 1; | |
| result.normals[i * 3 + 2] = normals[i * 4 + 2] * 2 - 1; | |
| } | |
| } | |
| if (uvs) { | |
| // Extract UVs from RGBA texture data | |
| result.uvs = new Float32Array(vertexCount * 2); | |
| for (let i = 0; i < vertexCount; i++) { | |
| result.uvs[i * 2] = uvs[i * 4]; | |
| result.uvs[i * 2 + 1] = uvs[i * 4 + 1]; | |
| } | |
| } | |
| return result; | |
| } catch (error) { | |
| console.error('GPU decompression failed, falling back to CPU:', error); | |
| return this.decompressMeshCPU(dracoData); | |
| } | |
| } | |
| /** | |
| * Prepare Draco data for GPU processing | |
| * @param {ArrayBuffer} dracoData - Draco-encoded mesh data | |
| * @returns {Promise<Object>} - Prepared data for GPU processing | |
| */ | |
| async prepareDracoData(dracoData) { | |
| return new Promise((resolve, reject) => { | |
| // Send data to worker for preparation | |
| this.dracoWorker.onmessage = (e) => { | |
| const { type, result } = e.data; | |
| if (type === 'prepared') { | |
| resolve(result); | |
| } else if (type === 'error') { | |
| reject(new Error(result.message)); | |
| } | |
| }; | |
| // Send Draco data to worker | |
| this.dracoWorker.postMessage( | |
| { | |
| type: 'prepare', | |
| buffer: dracoData | |
| }, | |
| [dracoData] | |
| ); | |
| }); | |
| } | |
| /** | |
| * Fallback CPU-based Draco decompression | |
| * @param {ArrayBuffer} dracoData - Draco-encoded mesh data | |
| * @returns {Promise<Object>} - Decompressed mesh data | |
| */ | |
| async decompressMeshCPU(dracoData) { | |
| return new Promise((resolve, reject) => { | |
| // Send data to worker for CPU decompression | |
| this.dracoWorker.onmessage = (e) => { | |
| const { type, result } = e.data; | |
| if (type === 'decompressed') { | |
| resolve(result); | |
| } else if (type === 'error') { | |
| reject(new Error(result.message)); | |
| } | |
| }; | |
| // Send Draco data to worker | |
| this.dracoWorker.postMessage( | |
| { | |
| type: 'decompress', | |
| buffer: dracoData | |
| }, | |
| [dracoData] | |
| ); | |
| }); | |
| } | |
| /** | |
| * Dispose of resources | |
| */ | |
| dispose() { | |
| const gl = this.gl; | |
| // Delete programs | |
| for (const { program } of Object.values(this.programs)) { | |
| gl.deleteProgram(program); | |
| } | |
| // Delete buffers | |
| for (const buffer of Object.values(this.buffers)) { | |
| gl.deleteBuffer(buffer); | |
| } | |
| // Delete textures | |
| for (const texture of Object.values(this.textures)) { | |
| gl.deleteTexture(texture); | |
| } | |
| // Terminate worker | |
| if (this.dracoWorker) { | |
| this.dracoWorker.terminate(); | |
| } | |
| this.isInitialized = false; | |
| } | |
| } | |
| export { DracoGPUDecompressor }; |