www.xbdev.net
xbdev - software development
Friday June 12, 2026
Home | Contact | Support | glTF File Format The JPG of 3D Formats ...
     
 

glTF File Format Tutorials

Unlocking the power ...

 


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)



















Ray-Tracing with WebGPU kenwright WebGPU Development Cookbook - coding recipes for all your webgpu needs! WebGPU by Example: Fractals, Image Effects, Ray-Tracing, Procedural Geometry, 2D/3D, Particles, Simulations WebGPU Games WGSL 2d 3d interactive web-based fun learning WebGPU Compute WebGPU API - Owners WebGPU & WGSL Essentials: A Hands-On Approach to Interactive Graphics, Games, 2D Interfaces, 3D Meshes, Animation, Security and Production Kenwright graphics and animations using the webgpu api 12 week course kenwright learn webgpu api kenwright programming compute and graphics applications with html5 and webgpu api kenwright real-time 3d graphics with webgpu kenwright webgpu for dummies kenwright webgpu api develompent a quick start guide kenwright webgpu by example 2022 kenwright webgpu gems kenwright webgpu interactive compute and graphics visualization cookbook kenwright wgsl webgpu shading language cookbook kenwright WebGPU Shader Language Development: Vertex, Fragment, Compute Shaders for Programmers Kenwright wgsl webgpugems shading language cookbook kenwright WGSL Fundamentals book kenwright WebGPU Data Visualization Cookbook kenwright Special Effects Programming with WebGPU kenwright WebGPU Programming Guide: Interactive Graphics and Compute Programming with WebGPU & WGSL kenwright



 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2026 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.