/** * 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} - 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} - 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} - 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 };