|
|
|
|
 |
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)
|
|
|
|
|
|
|
|