| Ascii or Binary | |
The glTF file format is one format - but can be stored in different ways:
• '.gltf' extension which uses 'ASCII' (both with seperate binary files and inline version)
• '.glb' binary version - all the data is stored in a single binary file.
The first thing you need to do is get at the data - once you've got the data you can decode it!
The initial functions determine if it's a `glb` or `gltf` by checking the digital signature (start of the file). If it's a `glb` we treat the file as binary - and extract the important information. It it's a `gltf` then it's a text file - and use the parseGLTF(..).
The code is written so the processing of the data (both glb and gltf) is done in the parseGLTF(..) function (same place).
const gltfFileUrl = 'https://webgpulab.xbdev.net/var/resources/gltf/Bee/Bee.glb'; const gltfPath = 'https://webgpulab.xbdev.net/var/resources/gltf/';
class glTFLoader { async load(url, gltfPath) { try { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer();
let gltf; if (this.isBinaryGLTF(arrayBuffer)) { gltf = this.parseGLB(arrayBuffer); // Parse GLB as a binary format gltf.gltfPath = gltfPath || ''; // No external path needed for `.glb` } else { const textDecoder = new TextDecoder(); const jsonString = textDecoder.decode(arrayBuffer); gltf = JSON.parse(jsonString); gltf.gltfPath = gltfPath || this.getBasePath(url); // Infer base path for `.gltf` }
const parsedData = await this.parseGLTF(gltf); // Parse the content (same for both) return parsedData; } catch (error) { console.error('Error loading GLTF/GLB file:', error); throw error; } }
isBinaryGLTF(arrayBuffer) { const magic = new TextDecoder().decode(arrayBuffer.slice(0, 4)); return magic === 'glTF'; }
parseGLB(arrayBuffer) { console.log('parseGLB'); const dataView = new DataView(arrayBuffer);
// Header: Magic (4 bytes) + Version (4 bytes) + Length (4 bytes) const magic = dataView.getUint32(0, true); const version = dataView.getUint32(4, true); const length = dataView.getUint32(8, true);
if (magic !== 0x46546C67) { // "glTF" in ASCII throw new Error('Invalid GLB file magic.'); } if (version !== 2) { throw new Error('Unsupported GLB version.'); }
let offset = 12; let jsonChunk, binChunk;
// Iterate through the chunks in the GLB file while (offset < length) { const chunkLength = dataView.getUint32(offset, true); const chunkType = dataView.getUint32(offset + 4, true); const chunkData = arrayBuffer.slice(offset + 8, offset + 8 + chunkLength);
if (chunkType === 0x4E4F534A) { // "JSON" in ASCII const textDecoder = new TextDecoder(); jsonChunk = JSON.parse(textDecoder.decode(chunkData)); } else if (chunkType === 0x004E4942) { // "BIN" in ASCII binChunk = chunkData; }
offset += 8 + chunkLength; }
if (!jsonChunk) { throw new Error('Missing JSON chunk in GLB file.'); }
if (binChunk) { jsonChunk.buffers[0].binary = binChunk; // Attach binary data directly to buffers }
return jsonChunk; // Return parsed JSON, now with binary buffers attached }
async parseGLTF(gltf) { console.log('parseGLTF'); const parsedData = { buffers: await this.loadBuffers(gltf), bufferViews: gltf.bufferViews, accessors: gltf.accessors, meshes: gltf.meshes, nodes: gltf.nodes, materials: gltf.materials, textures: gltf.textures, images: gltf.images, animations: gltf.animations, };
console.log('gltf.meshes:', gltf.meshes ); this.extractMeshData(parsedData); this.extractAnimations(parsedData); this.extractTextures(parsedData);
return parsedData; }
async loadBuffers(gltf) { const bufferPromises = gltf.buffers.map((buffer, index) => { if (buffer.binary) { // Directly use binary data for GLB return Promise.resolve(buffer.binary); } else if (buffer.uri) { console.log('loading bin:', gltf.gltfPath + buffer.uri); return fetch(gltf.gltfPath + buffer.uri).then(res => res.arrayBuffer()); } else { throw new Error(`Buffer ${index} is missing both "binary" and "uri".`); } }); return await Promise.all(bufferPromises); }
extractMeshData(parsedData) { parsedData.meshes.forEach(mesh => {
mesh.primitives.forEach(primitive => { const positionAccessorIndex = primitive.attributes.POSITION; const positionAccessor = parsedData.accessors[positionAccessorIndex]; const positionBufferView = parsedData.bufferViews[positionAccessor.bufferView]; console.log('here'); const positionBuffer = parsedData.buffers[positionBufferView.buffer];
console.log('here2'); console.log('positionBufferView.byteOffset:', positionBufferView.byteOffset ); console.log('positionAccessor.byteOffset:', positionAccessor.byteOffset ); console.log('positionAccessor.count:', positionAccessor.count ); console.log('positionAccessor.type:', positionAccessor.type ); console.log('positionBuffer:', positionBuffer ); const positions = new Float32Array( positionBuffer, positionBufferView.byteOffset + (positionAccessor.byteOffset || 0), positionAccessor.count * (positionAccessor.type === 'VEC3' ? 3 : 1) ); console.log('here3'); primitive.positions = positions;
if (primitive.indices !== undefined) { const indexAccessor = parsedData.accessors[primitive.indices]; const indexBufferView = parsedData.bufferViews[indexAccessor.bufferView]; const indexBuffer = parsedData.buffers[indexBufferView.buffer];
const indices = new Uint16Array( indexBuffer, indexBufferView.byteOffset + (indexAccessor.byteOffset || 0), indexAccessor.count ); primitive.indices = indices; }
if (primitive.attributes.NORMAL !== undefined) { const normalAccessorIndex = primitive.attributes.NORMAL; const normalAccessor = parsedData.accessors[normalAccessorIndex]; const normalBufferView = parsedData.bufferViews[normalAccessor.bufferView]; const normalBuffer = parsedData.buffers[normalBufferView.buffer];
const normals = new Float32Array( normalBuffer, normalBufferView.byteOffset + (normalAccessor.byteOffset || 0), normalAccessor.count * (normalAccessor.type === 'VEC3' ? 3 : 1) ); primitive.normals = normals; }
if (primitive.attributes.TEXCOORD_0 !== undefined) { const texCoordAccessorIndex = primitive.attributes.TEXCOORD_0; const texCoordAccessor = parsedData.accessors[texCoordAccessorIndex]; const texCoordBufferView = parsedData.bufferViews[texCoordAccessor.bufferView]; const texCoordBuffer = parsedData.buffers[texCoordBufferView.buffer];
const texCoords = new Float32Array( texCoordBuffer, texCoordBufferView.byteOffset + (texCoordAccessor.byteOffset || 0), texCoordAccessor.count * (texCoordAccessor.type === 'VEC2' ? 2 : 1) ); primitive.texCoords = texCoords; } }); }); }
extractAnimations(parsedData) { if (parsedData.animations) { parsedData.animations.forEach(animation => { animation.samplers.forEach(sampler => { const inputAccessor = parsedData.accessors[sampler.input]; const inputBufferView = parsedData.bufferViews[inputAccessor.bufferView]; const inputBuffer = parsedData.buffers[inputBufferView.buffer];
sampler.inputData = new Float32Array( inputBuffer, inputBufferView.byteOffset + (inputAccessor.byteOffset || 0), inputAccessor.count );
const outputAccessor = parsedData.accessors[sampler.output]; const outputBufferView = parsedData.bufferViews[outputAccessor.bufferView]; const outputBuffer = parsedData.buffers[outputBufferView.buffer];
sampler.outputData = new Float32Array( outputBuffer, outputBufferView.byteOffset + (outputAccessor.byteOffset || 0), outputAccessor.count * (outputAccessor.type === 'VEC3' ? 3 : 1) ); }); }); } }
extractTextures(parsedData) { if (parsedData.textures) { parsedData.textures.forEach(texture => { if (texture.source !== undefined) { const image = parsedData.images[texture.source]; texture.imageUri = image.uri; } }); } } }
// Function to display statistics function displayStats(parsedData) { const stats = { numberOfMeshes: parsedData.meshes ? parsedData.meshes.length : 0, numberOfNodes: parsedData.nodes ? parsedData.nodes.length : 0, numberOfMaterials: parsedData.materials ? parsedData.materials.length : 0, numberOfTextures: parsedData.textures ? parsedData.textures.length : 0, numberOfAnimations: parsedData.animations ? parsedData.animations.length : 0, };
let details = ` Number of Meshes: ${stats.numberOfMeshes}<br> Number of Nodes: ${stats.numberOfNodes}<br> Number of Materials: ${stats.numberOfMaterials}<br> Number of Textures: ${stats.numberOfTextures}<br> Number of Animations: ${stats.numberOfAnimations}<br> <h2>Mesh Details:</h2> `;
parsedData.meshes.forEach((mesh, meshIndex) => { details += `Mesh ${meshIndex} - Name: ${mesh.name || 'Unnamed'}<br>`; mesh.primitives.forEach((primitive, primitiveIndex) => { const verticesCount = primitive.positions ? primitive.positions.length / 3 : 0; const indicesCount = primitive.indices ? primitive.indices.length : 0; details += ` ${' '.repeat(8)}Primitive ${primitiveIndex}:<br> ${' '.repeat(16)}Number of Vertices: ${verticesCount}<br> ${' '.repeat(16)}Number of Indices: ${indicesCount}<br> `; }); }); const statsDiv = document.createElement('div'); document.body.appendChild( statsDiv ); statsDiv.innerHTML = details; }
let div = document.createElement('div'); document.body.appendChild( div ); div.innerHTML = ` Vanilla glTF loader/parser in native JavaScript<br><br>
File: ${gltfFileUrl}<br><br> `;
const loader = new glTFLoader();
try { const parsedData = await loader.load(gltfFileUrl, gltfPath); displayStats(parsedData);
} catch (error) { console.error('An error occurred:', error); }
| Resources and Links | |
• glTF ascii and binary file dumper (LINK)
|