ogl
Version:
WebGL Library
1,059 lines (934 loc) • 42.6 kB
JavaScript
import { Geometry } from '../core/Geometry.js';
import { Transform } from '../core/Transform.js';
import { Texture } from '../core/Texture.js';
import { Mesh } from '../core/Mesh.js';
import { Camera } from '../core/Camera.js';
import { GLTFAnimation } from './GLTFAnimation.js';
import { GLTFSkin } from './GLTFSkin.js';
import { Mat4 } from '../math/Mat4.js';
import { Vec3 } from '../math/Vec3.js';
import { NormalProgram } from './NormalProgram.js';
import { InstancedMesh } from './InstancedMesh.js';
// TODO
// [ ] Morph targets
// [ ] Materials
// [ ] Sparse accessor packing? For morph targets basically
// [ ] Option to turn off GPU instancing?
// [ ] Spot lights
const TYPE_ARRAY = {
5120: Int8Array,
5121: Uint8Array,
5122: Int16Array,
5123: Uint16Array,
5125: Uint32Array,
5126: Float32Array,
'image/jpeg': Uint8Array,
'image/png': Uint8Array,
'image/webp': Uint8Array,
};
const TYPE_SIZE = {
SCALAR: 1,
VEC2: 2,
VEC3: 3,
VEC4: 4,
MAT2: 4,
MAT3: 9,
MAT4: 16,
};
const ATTRIBUTES = {
POSITION: 'position',
NORMAL: 'normal',
TANGENT: 'tangent',
TEXCOORD_0: 'uv',
TEXCOORD_1: 'uv2',
COLOR_0: 'color',
WEIGHTS_0: 'skinWeight',
JOINTS_0: 'skinIndex',
};
const TRANSFORMS = {
translation: 'position',
rotation: 'quaternion',
scale: 'scale',
};
export class GLTFLoader {
static setDracoManager(manager) {
this.dracoManager = manager;
}
static setBasisManager(manager) {
this.basisManager = manager;
}
static async load(gl, src) {
const dir = src.split('/').slice(0, -1).join('/') + '/';
// Load main description json
const desc = await this.parseDesc(src);
return this.parse(gl, desc, dir);
}
static async parse(gl, desc, dir) {
if (desc.asset === undefined || desc.asset.version[0] < 2)
console.warn('Only GLTF >=2.0 supported. Attempting to parse.');
if (desc.extensionsRequired?.includes('KHR_draco_mesh_compression') && !this.dracoManager)
console.warn('KHR_draco_mesh_compression extension required but no manager supplied. Use .setDracoManager()');
if (desc.extensionsRequired?.includes('KHR_texture_basisu') && !this.basisManager)
console.warn('KHR_texture_basisu extension required but no manager supplied. Use .setBasisManager()');
// Load buffers async
const buffers = await this.loadBuffers(desc, dir);
// Unbind current VAO so that new buffers don't get added to active mesh
gl.renderer.bindVertexArray(null);
// Create gl buffers from bufferViews
const bufferViews = this.parseBufferViews(gl, desc, buffers);
// Create images from either bufferViews or separate image files
const images = await this.parseImages(gl, desc, dir, bufferViews);
const textures = this.parseTextures(gl, desc, images);
// Just pass through material data for now
const materials = this.parseMaterials(gl, desc, textures);
// Fetch the inverse bind matrices for skeleton joints
const skins = this.parseSkins(gl, desc, bufferViews);
// Create geometries for each mesh primitive
const meshes = await this.parseMeshes(gl, desc, bufferViews, materials, skins);
// Create transforms, meshes and hierarchy
const [nodes, cameras] = this.parseNodes(gl, desc, meshes, skins, images);
// Place nodes in skeletons
this.populateSkins(skins, nodes);
// Create animation handlers
const animations = this.parseAnimations(gl, desc, nodes, bufferViews);
// Get top level nodes for each scene
const scenes = this.parseScenes(desc, nodes);
const scene = scenes[desc.scene];
// Create uniforms for scene lights (TODO: light linking?)
const lights = this.parseLights(gl, desc, nodes, scenes);
// Remove null nodes (instanced transforms)
for (let i = nodes.length; i >= 0; i--) if (!nodes[i]) nodes.splice(i, 1);
return {
json: desc,
buffers,
bufferViews,
cameras,
images,
textures,
materials,
meshes,
nodes,
lights,
animations,
scenes,
scene,
};
}
static parseDesc(src) {
return fetch(src, { mode: 'cors' })
.then((res) => res.arrayBuffer())
.then((data) => {
const textDecoder = new TextDecoder();
if (textDecoder.decode(new Uint8Array(data, 0, 4)) === 'glTF') {
return this.unpackGLB(data);
} else {
return JSON.parse(textDecoder.decode(data));
}
});
}
// From https://github.com/donmccurdy/glTF-Transform/blob/e4108cc/packages/core/src/io/io.ts#L32
static unpackGLB(glb) {
// Decode and verify GLB header
const header = new Uint32Array(glb, 0, 3);
if (header[0] !== 0x46546c67) {
throw new Error('Invalid glTF asset.');
} else if (header[1] !== 2) {
throw new Error(`Unsupported glTF binary version, "${header[1]}".`);
}
// Decode and verify chunk headers
const jsonChunkHeader = new Uint32Array(glb, 12, 2);
const jsonByteOffset = 20;
const jsonByteLength = jsonChunkHeader[0];
if (jsonChunkHeader[1] !== 0x4e4f534a) {
throw new Error('Unexpected GLB layout.');
}
// Decode JSON
const jsonText = new TextDecoder().decode(glb.slice(jsonByteOffset, jsonByteOffset + jsonByteLength));
const json = JSON.parse(jsonText);
// JSON only
if (jsonByteOffset + jsonByteLength === glb.byteLength) return json;
const binaryChunkHeader = new Uint32Array(glb, jsonByteOffset + jsonByteLength, 2);
if (binaryChunkHeader[1] !== 0x004e4942) {
throw new Error('Unexpected GLB layout.');
}
// Decode content
const binaryByteOffset = jsonByteOffset + jsonByteLength + 8;
const binaryByteLength = binaryChunkHeader[0];
const binary = glb.slice(binaryByteOffset, binaryByteOffset + binaryByteLength);
// Attach binary to buffer
json.buffers[0].binary = binary;
return json;
}
// ThreeJS GLTF Loader https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/GLTFLoader.js#L1085
static resolveURI(uri, dir) {
// Invalid URI
if (typeof uri !== 'string' || uri === '') return '';
// Host Relative URI
if (/^https?:\/\//i.test(dir) && /^\//.test(uri)) {
dir = dir.replace(/(^https?:\/\/[^\/]+).*/i, '$1');
}
// Absolute URI http://, https://, //
if (/^(https?:)?\/\//i.test(uri)) return uri;
// Data URI
if (/^data:.*,.*$/i.test(uri)) return uri;
// Blob URI
if (/^blob:.*$/i.test(uri)) return uri;
// Relative URI
return dir + uri;
}
static loadBuffers(desc, dir) {
if (!desc.buffers) return null;
return Promise.all(
desc.buffers.map((buffer) => {
// For GLB, binary buffer ready to go
if (buffer.binary) return buffer.binary;
const uri = this.resolveURI(buffer.uri, dir);
return fetch(uri, { mode: 'cors' }).then((res) => res.arrayBuffer());
})
);
}
static parseBufferViews(gl, desc, buffers) {
if (!desc.bufferViews) return null;
const bufferViews = desc.bufferViews;
desc.meshes &&
desc.meshes.forEach(({ primitives }) => {
primitives.forEach(({ attributes, indices, extensions }) => {
// Flag bufferView as an attribute, so it knows to create a gl buffer
for (const attr in attributes) {
const accessor = desc.accessors[attributes[attr]];
if (accessor.bufferView === undefined && !!extensions) {
// Draco extension buffer view
if (extensions.KHR_draco_mesh_compression) {
accessor.bufferView = extensions.KHR_draco_mesh_compression.bufferView;
bufferViews[accessor.bufferView].isDraco = true;
}
}
bufferViews[accessor.bufferView].isAttribute = true;
}
if (indices !== undefined) {
const accessor = desc.accessors[indices];
if (accessor.bufferView === undefined && !!extensions) {
// Draco extension buffer view
if (extensions.KHR_draco_mesh_compression) {
accessor.bufferView = extensions.KHR_draco_mesh_compression.bufferView;
bufferViews[accessor.bufferView].isDraco = true;
}
}
bufferViews[accessor.bufferView].isAttribute = true;
// Make sure indices bufferView have a target property for gl buffer binding
bufferViews[accessor.bufferView].target = gl.ELEMENT_ARRAY_BUFFER;
}
});
});
// Get componentType of each bufferView from the accessors
desc.accessors.forEach(({ bufferView: bufferViewIndex, componentType }) => {
if (bufferViewIndex === undefined) return;
bufferViews[bufferViewIndex].componentType = componentType;
});
// Get mimetype of bufferView from images
desc.images &&
desc.images.forEach(({ uri, bufferView: bufferViewIndex, mimeType }) => {
if (bufferViewIndex === undefined) return;
bufferViews[bufferViewIndex].mimeType = mimeType;
});
// Push each bufferView to the GPU as a separate buffer
bufferViews.forEach(
(
{
buffer: bufferIndex, // required
byteOffset = 0, // optional
byteLength, // required
byteStride, // optional
target = gl.ARRAY_BUFFER, // optional, added above for elements
name, // optional
extensions, // optional
extras, // optional
componentType, // optional, added from accessor above
mimeType, // optional, added from images above
isAttribute,
isDraco,
},
i
) => {
bufferViews[i].data = buffers[bufferIndex].slice(byteOffset, byteOffset + byteLength);
if (!isAttribute || isDraco) return;
// Create gl buffers for the bufferView, pushing it to the GPU
const buffer = gl.createBuffer();
gl.bindBuffer(target, buffer);
gl.renderer.state.boundBuffer = buffer;
gl.bufferData(target, bufferViews[i].data, gl.STATIC_DRAW);
bufferViews[i].buffer = buffer;
}
);
return bufferViews;
}
static parseImages(gl, desc, dir, bufferViews) {
if (!desc.images) return null;
return Promise.all(
desc.images.map(async ({ uri, bufferView: bufferViewIndex, mimeType, name }) => {
if (mimeType === 'image/ktx2') {
const { data } = bufferViews[bufferViewIndex];
const image = await this.basisManager.parseTexture(data);
return image;
}
// jpg / png / webp
const image = new Image();
image.name = name;
if (uri) {
image.src = this.resolveURI(uri, dir);
} else if (bufferViewIndex !== undefined) {
const { data } = bufferViews[bufferViewIndex];
const blob = new Blob([data], { type: mimeType });
image.src = URL.createObjectURL(blob);
}
image.ready = new Promise((res) => {
image.onload = () => res();
});
return image;
})
);
}
static parseTextures(gl, desc, images) {
if (!desc.textures) return null;
return desc.textures.map((textureInfo) => this.createTexture(gl, desc, images, textureInfo));
}
static createTexture(gl, desc, images, { sampler: samplerIndex, source: sourceIndex, name, extensions, extras }) {
if (sourceIndex === undefined && !!extensions) {
// WebP extension source index
if (extensions.EXT_texture_webp) sourceIndex = extensions.EXT_texture_webp.source;
// Basis extension source index
if (extensions.KHR_texture_basisu) sourceIndex = extensions.KHR_texture_basisu.source;
}
const image = images[sourceIndex];
if (image.texture) return image.texture;
const options = {
flipY: false,
wrapS: gl.REPEAT, // Repeat by default, opposed to OGL's clamp by default
wrapT: gl.REPEAT,
};
const sampler = samplerIndex !== undefined ? desc.samplers[samplerIndex] : null;
if (sampler) {
['magFilter', 'minFilter', 'wrapS', 'wrapT'].forEach((prop) => {
if (sampler[prop]) options[prop] = sampler[prop];
});
}
// For compressed textures
if (image.isBasis) {
options.image = image;
options.internalFormat = image.internalFormat;
if (image.isCompressedTexture) {
options.generateMipmaps = false;
if (image.length > 1) this.minFilter = gl.NEAREST_MIPMAP_LINEAR;
}
const texture = new Texture(gl, options);
texture.name = name;
image.texture = texture;
return texture;
}
const texture = new Texture(gl, options);
texture.name = name;
image.texture = texture;
image.ready.then(() => {
texture.image = image;
});
return texture;
}
static parseMaterials(gl, desc, textures) {
if (!desc.materials) return null;
return desc.materials.map(
({
name,
extensions,
extras,
pbrMetallicRoughness = {},
normalTexture,
occlusionTexture,
emissiveTexture,
emissiveFactor = [0, 0, 0],
alphaMode = 'OPAQUE',
alphaCutoff = 0.5,
doubleSided = false,
}) => {
const {
baseColorFactor = [1, 1, 1, 1],
baseColorTexture,
metallicFactor = 1,
roughnessFactor = 1,
metallicRoughnessTexture,
// extensions,
// extras,
} = pbrMetallicRoughness;
if (baseColorTexture) {
baseColorTexture.texture = textures[baseColorTexture.index];
// texCoord
}
if (normalTexture) {
normalTexture.texture = textures[normalTexture.index];
// scale: 1
// texCoord
}
if (metallicRoughnessTexture) {
metallicRoughnessTexture.texture = textures[metallicRoughnessTexture.index];
// texCoord
}
if (occlusionTexture) {
occlusionTexture.texture = textures[occlusionTexture.index];
// strength 1
// texCoord
}
if (emissiveTexture) {
emissiveTexture.texture = textures[emissiveTexture.index];
// texCoord
}
return {
name,
extensions,
extras,
baseColorFactor,
baseColorTexture,
metallicFactor,
roughnessFactor,
metallicRoughnessTexture,
normalTexture,
occlusionTexture,
emissiveTexture,
emissiveFactor,
alphaMode,
alphaCutoff,
doubleSided,
};
}
);
}
static parseSkins(gl, desc, bufferViews) {
if (!desc.skins) return null;
return desc.skins.map(
({
inverseBindMatrices, // optional
skeleton, // optional
joints, // required
// name,
// extensions,
// extras,
}) => {
return {
inverseBindMatrices: this.parseAccessor(inverseBindMatrices, desc, bufferViews),
skeleton,
joints,
};
}
);
}
static parseMeshes(gl, desc, bufferViews, materials, skins) {
if (!desc.meshes) return null;
return Promise.all(
desc.meshes.map(
async (
{
primitives, // required
weights, // optional
name, // optional
extensions, // optional
extras = {}, // optional - will get merged with node extras
},
meshIndex
) => {
// TODO: weights stuff?
// Parse through nodes to see how many instances there are and if there is a skin attached
// If multiple instances of a skin, need to create each
let numInstances = 0;
let skinIndices = [];
let isLightmap = false;
desc.nodes &&
desc.nodes.forEach(({ mesh, skin, extras }) => {
if (mesh === meshIndex) {
numInstances++;
if (skin !== undefined) skinIndices.push(skin);
if (extras && extras.lightmap_scale_offset) isLightmap = true;
}
});
let isSkin = !!skinIndices.length;
// For skins, return array of skin meshes to account for multiple instances
if (isSkin) {
primitives = await Promise.all(
skinIndices.map(async (skinIndex) => {
return (await this.parsePrimitives(gl, primitives, desc, bufferViews, materials, 1, isLightmap)).map(({ geometry, program, mode }) => {
const mesh = new GLTFSkin(gl, { skeleton: skins[skinIndex], geometry, program, mode });
mesh.name = name;
mesh.extras = extras;
if (extensions) mesh.extensions = extensions;
// TODO: support skin frustum culling
mesh.frustumCulled = false;
return mesh;
});
})
);
// For retrieval to add to node
primitives.instanceCount = 0;
primitives.numInstances = numInstances;
} else {
primitives = (await this.parsePrimitives(gl, primitives, desc, bufferViews, materials, numInstances, isLightmap)).map(({ geometry, program, mode }) => {
// InstancedMesh class has custom frustum culling for instances
const meshConstructor = geometry.attributes.instanceMatrix ? InstancedMesh : Mesh;
const mesh = new meshConstructor(gl, { geometry, program, mode });
mesh.name = name;
mesh.extras = extras;
if (extensions) mesh.extensions = extensions;
// Tag mesh so that nodes can add their transforms to the instance attribute
mesh.numInstances = numInstances;
return mesh;
});
}
return {
primitives,
weights,
name,
};
}
)
);
}
static parsePrimitives(gl, primitives, desc, bufferViews, materials, numInstances, isLightmap) {
return Promise.all(
primitives.map(
async ({
attributes, // required
indices, // optional
material: materialIndex, // optional
mode = 4, // optional
targets, // optional
extensions, // optional
extras, // optional
}) => {
// TODO: materials
const program = new NormalProgram(gl);
if (materialIndex !== undefined) {
program.gltfMaterial = materials[materialIndex];
}
const geometry = new Geometry(gl);
if (extras) geometry.extras = extras;
if (extensions) geometry.extensions = extensions;
// For compressed geometry data
if (extensions && extensions.KHR_draco_mesh_compression) {
const bufferViewIndex = extensions.KHR_draco_mesh_compression.bufferView;
const gltfAttributeMap = extensions.KHR_draco_mesh_compression.attributes;
const attributeMap = {};
const attributeTypeMap = {};
const attributeTypeNameMap = {};
const attributeNormalizedMap = {};
for (const attr in attributes) {
const accessor = desc.accessors[attributes[attr]];
const attributeName = ATTRIBUTES[attr];
attributeMap[attributeName] = gltfAttributeMap[attr];
attributeTypeMap[attributeName] = accessor.componentType;
attributeTypeNameMap[attributeName] = TYPE_ARRAY[accessor.componentType].name;
attributeNormalizedMap[attributeName] = accessor.normalized === true;
}
const { data } = bufferViews[bufferViewIndex];
const geometryData = await this.dracoManager.decodeGeometry(data, {
attributeIds: attributeMap,
attributeTypes: attributeTypeNameMap,
});
// Add each attribute result
for (let i = 0; i < geometryData.attributes.length; i++) {
const result = geometryData.attributes[i];
const name = result.name;
const data = result.array;
const size = result.itemSize;
const type = attributeTypeMap[name];
const normalized = attributeNormalizedMap[name];
// Create gl buffers for the attribute data, pushing it to the GPU
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.renderer.state.boundBuffer = buffer;
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
geometry.addAttribute(name, {
data,
size,
type,
normalized,
buffer,
});
}
// Add index attribute if found
if (geometryData.index) {
const data = geometryData.index.array;
const size = geometryData.index.itemSize;
// Create gl buffers for the index attribute data, pushing it to the GPU
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.renderer.state.boundBuffer = buffer;
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
geometry.addAttribute('index', {
data,
size,
type: 5125, // Uint32Array
normalized: false,
buffer,
});
}
} else {
// Add each attribute found in primitive
for (const attr in attributes) {
geometry.addAttribute(ATTRIBUTES[attr], this.parseAccessor(attributes[attr], desc, bufferViews));
}
// Add index attribute if found
if (indices !== undefined) {
geometry.addAttribute('index', this.parseAccessor(indices, desc, bufferViews));
}
}
// Add instanced transform attribute if multiple instances
// Ignore if skin as we don't support instanced skins out of the box
if (numInstances > 1) {
geometry.addAttribute('instanceMatrix', {
instanced: 1,
size: 16,
data: new Float32Array(numInstances * 16),
});
}
// Always supply lightmapScaleOffset as an instanced attribute
// Instanced skin lightmaps not supported
if (isLightmap) {
geometry.addAttribute('lightmapScaleOffset', {
instanced: 1,
size: 4,
data: new Float32Array(numInstances * 4),
});
}
return {
geometry,
program,
mode,
};
}
)
);
}
static parseAccessor(index, desc, bufferViews) {
// TODO: init missing bufferView with 0s
// TODO: support sparse
const {
bufferView: bufferViewIndex, // optional
byteOffset = 0, // optional
componentType, // required
normalized = false, // optional
count, // required
type, // required
min, // optional
max, // optional
sparse, // optional
// name, // optional
// extensions, // optional
// extras, // optional
} = desc.accessors[index];
const {
data, // attached in parseBufferViews
buffer, // replaced to be the actual GL buffer
byteOffset: bufferByteOffset = 0,
// byteLength, // applied in parseBufferViews
byteStride = 0,
target,
// name,
// extensions,
// extras,
} = bufferViews[bufferViewIndex];
const size = TYPE_SIZE[type];
// Parse data from joined buffers
const TypeArray = TYPE_ARRAY[componentType];
const elementBytes = TypeArray.BYTES_PER_ELEMENT;
const componentStride = byteStride / elementBytes;
const isInterleaved = !!byteStride && componentStride !== size;
let filteredData;
// Convert data to typed array for various uses (bounding boxes, raycasting, animation, merging etc)
if (isInterleaved) {
// First convert entire buffer to type
const typedData = new TypeArray(data, byteOffset);
// TODO: add length to not copy entire buffer if can help it
// const typedData = new TypeArray(data, byteOffset, (count - 1) * componentStride)
// Create output with length
filteredData = new TypeArray(count * size);
// Add element by element
for (let i = 0; i < count; i++) {
const start = componentStride * i;
const end = start + size;
filteredData.set(typedData.slice(start, end), i * size);
}
} else {
// Simply a slice
filteredData = new TypeArray(data, byteOffset, count * size);
}
// Return attribute data
return {
data: filteredData,
size,
type: componentType,
normalized,
buffer,
stride: byteStride,
offset: byteOffset,
count,
min,
max,
};
}
static parseNodes(gl, desc, meshes, skins, images) {
if (!desc.nodes) return null;
const cameras = [];
const nodes = desc.nodes.map(
({
camera, // optional
children, // optional
skin: skinIndex, // optional
matrix, // optional
mesh: meshIndex, // optional
rotation, // optional
scale, // optional
translation, // optional
weights, // optional
name, // optional
extensions, // optional
extras, // optional
}) => {
const isCamera = camera !== undefined;
const node = isCamera ? new Camera(gl) : new Transform();
if (isCamera) {
// NOTE: chose to use node's name and extras/extensions over camera
const cameraOpts = desc.cameras[camera];
if (cameraOpts.type === 'perspective') {
const { yfov: fov, znear: near, zfar: far } = cameraOpts.perspective;
node.perspective({ fov: fov * (180 / Math.PI), near, far });
} else {
const { xmag, ymag, znear: near, zfar: far } = cameraOpts.orthographic;
node.orthographic({ near, far, left: -xmag, right: xmag, top: -ymag, bottom: ymag });
}
cameras.push(node);
}
if (name) node.name = name;
if (extras) node.extras = extras;
if (extensions) node.extensions = extensions;
// Need to attach to node as may have same material but different lightmap
if (extras && extras.lightmapTexture !== undefined) {
extras.lightmapTexture.texture = this.createTexture(gl, desc, images, { source: extras.lightmapTexture.index });
}
// Apply transformations
if (matrix) {
node.matrix.copy(matrix);
node.decompose();
} else {
if (rotation) node.quaternion.copy(rotation);
if (scale) node.scale.copy(scale);
if (translation) node.position.copy(translation);
node.updateMatrix();
}
// Flags for avoiding duplicate transforms and removing unused instance nodes
let isInstanced = false;
let isFirstInstance = true;
let isInstancedMatrix = false;
let isSkin = skinIndex !== undefined;
// Add mesh if included
if (meshIndex !== undefined) {
if (isSkin) {
meshes[meshIndex].primitives[meshes[meshIndex].primitives.instanceCount].forEach((mesh) => {
if (extras) Object.assign(mesh.extras, extras);
mesh.setParent(node);
});
meshes[meshIndex].primitives.instanceCount++;
// Remove properties once all instances added
if (meshes[meshIndex].primitives.instanceCount === meshes[meshIndex].primitives.numInstances) {
delete meshes[meshIndex].primitives.numInstances;
delete meshes[meshIndex].primitives.instanceCount;
}
} else {
meshes[meshIndex].primitives.forEach((mesh) => {
if (extras) Object.assign(mesh.extras, extras);
// Instanced mesh might only have 1
if (mesh.geometry.isInstanced) {
isInstanced = true;
if (!mesh.instanceCount) {
mesh.instanceCount = 0;
} else {
isFirstInstance = false;
}
if (mesh.geometry.attributes.instanceMatrix) {
isInstancedMatrix = true;
node.matrix.toArray(mesh.geometry.attributes.instanceMatrix.data, mesh.instanceCount * 16);
}
if (mesh.geometry.attributes.lightmapScaleOffset) {
mesh.geometry.attributes.lightmapScaleOffset.data.set(extras.lightmap_scale_offset, mesh.instanceCount * 4);
}
mesh.instanceCount++;
if (mesh.instanceCount === mesh.numInstances) {
// Remove properties once all instances added
delete mesh.numInstances;
delete mesh.instanceCount;
// Flag attribute as dirty
if (mesh.geometry.attributes.instanceMatrix) {
mesh.geometry.attributes.instanceMatrix.needsUpdate = true;
}
if (mesh.geometry.attributes.lightmapScaleOffset) {
mesh.geometry.attributes.lightmapScaleOffset.needsUpdate = true;
}
}
}
// For instances, only the first node will actually have the mesh
if (isInstanced) {
if (isFirstInstance) mesh.setParent(node);
} else {
mesh.setParent(node);
}
});
}
}
// Reset node if instanced to not duplicate transforms
if (isInstancedMatrix) {
// Remove unused nodes just providing an instance transform
if (!isFirstInstance) return null;
// Avoid duplicate transform for node containing the instanced mesh
node.matrix.identity();
node.decompose();
}
return node;
}
);
desc.nodes.forEach(({ children = [] }, i) => {
// Set hierarchy now all nodes created
children.forEach((childIndex) => {
if (!nodes[childIndex]) return;
nodes[childIndex].setParent(nodes[i]);
});
});
// Add frustum culling for instances now that instanceMatrix attribute is populated
meshes.forEach(({ primitives }, i) => {
primitives.forEach((primitive, i) => {
if (primitive.isInstancedMesh) primitive.addFrustumCull();
});
});
return [nodes, cameras];
}
static populateSkins(skins, nodes) {
if (!skins) return;
skins.forEach((skin) => {
skin.joints = skin.joints.map((i, index) => {
const joint = nodes[i];
joint.skin = skin;
joint.bindInverse = new Mat4(...skin.inverseBindMatrices.data.slice(index * 16, (index + 1) * 16));
return joint;
});
if (skin.skeleton) skin.skeleton = nodes[skin.skeleton];
});
}
static parseAnimations(gl, desc, nodes, bufferViews) {
if (!desc.animations) return null;
return desc.animations.map(
(
{
channels, // required
samplers, // required
name, // optional
// extensions, // optional
// extras, // optional
},
animationIndex
) => {
const data = channels.map(
({
sampler: samplerIndex, // required
target, // required
// extensions, // optional
// extras, // optional
}) => {
const {
input: inputIndex, // required
interpolation = 'LINEAR',
output: outputIndex, // required
// extensions, // optional
// extras, // optional
} = samplers[samplerIndex];
const {
node: nodeIndex, // optional - TODO: when is it not included?
path, // required
// extensions, // optional
// extras, // optional
} = target;
const node = nodes[nodeIndex];
const transform = TRANSFORMS[path];
const times = this.parseAccessor(inputIndex, desc, bufferViews).data;
const values = this.parseAccessor(outputIndex, desc, bufferViews).data;
// Store reference on node for cyclical retrieval
if (!node.animations) node.animations = [];
if (!node.animations.includes(animationIndex)) node.animations.push(animationIndex);
return {
node,
transform,
interpolation,
times,
values,
};
}
);
return {
name,
animation: new GLTFAnimation(data),
};
}
);
}
static parseScenes(desc, nodes) {
if (!desc.scenes) return null;
return desc.scenes.map(
({
nodes: nodesIndices = [],
name, // optional
extensions,
extras,
}) => {
const scene = nodesIndices.reduce((map, i) => {
// Don't add null nodes (instanced transforms)
if (nodes[i]) map.push(nodes[i]);
return map;
}, []);
scene.extras = extras;
return scene;
}
);
}
static parseLights(gl, desc, nodes, scenes) {
const lights = {
directional: [],
point: [],
spot: [],
};
// Update matrices on root nodes
scenes.forEach((scene) => scene.forEach((node) => node.updateMatrixWorld()));
// Uses KHR_lights_punctual extension
const lightsDescArray = desc.extensions?.KHR_lights_punctual?.lights || [];
// Need nodes for transforms
nodes.forEach((node) => {
if (!node?.extensions?.KHR_lights_punctual) return;
const lightIndex = node.extensions.KHR_lights_punctual.light;
const lightDesc = lightsDescArray[lightIndex];
const light = {
name: lightDesc.name || '',
color: { value: new Vec3().set(lightDesc.color || 1) },
};
// Apply intensity directly to color
if (lightDesc.intensity !== undefined) light.color.value.multiply(lightDesc.intensity);
switch (lightDesc.type) {
case 'directional':
light.direction = { value: new Vec3(0, 0, 1).transformDirection(node.worldMatrix) };
break;
case 'point':
light.position = { value: new Vec3().applyMatrix4(node.worldMatrix) };
light.distance = { value: lightDesc.range };
light.decay = { value: 2 };
break;
case 'spot':
// TODO: support spot uniforms
Object.assign(light, lightDesc);
break;
}
lights[lightDesc.type].push(light);
});
return lights;
}
}