484 lines
14 KiB
JavaScript
484 lines
14 KiB
JavaScript
import {
|
|
Accessor,
|
|
Camera,
|
|
Material,
|
|
Mesh,
|
|
Model,
|
|
Node,
|
|
Primitive,
|
|
Sampler,
|
|
Texture,
|
|
Transform,
|
|
Vertex,
|
|
} from '../core.js';
|
|
|
|
// TODO: GLB support
|
|
// TODO: accessors with no buffer views (zero-initialized)
|
|
// TODO: image from buffer view
|
|
// TODO: mipmaps
|
|
// TODO: material texcoord sets
|
|
// TODO: material alpha, doubleSided
|
|
|
|
export class GLTFLoader {
|
|
|
|
// Loads the GLTF JSON file and all buffers and images that it references.
|
|
// It also creates a cache for all future resource loading.
|
|
async load(url) {
|
|
this.gltfUrl = new URL(url, window.location);
|
|
this.gltf = await this.fetchJson(this.gltfUrl);
|
|
this.defaultScene = this.gltf.scene ?? 0;
|
|
this.cache = new Map();
|
|
|
|
await Promise.all(this.gltf.buffers?.map(buffer => this.preloadBuffer(buffer)) ?? []);
|
|
await Promise.all(this.gltf.images?.map(image => this.preloadImage(image)) ?? []);
|
|
}
|
|
|
|
// Finds an object in list at the given index, or if the 'name'
|
|
// property matches the given name.
|
|
findByNameOrIndex(list, nameOrIndex) {
|
|
if (typeof nameOrIndex === 'number') {
|
|
return list[nameOrIndex];
|
|
} else {
|
|
return list.find(element => element.name === nameOrIndex);
|
|
}
|
|
}
|
|
|
|
fetchJson(url) {
|
|
return fetch(url)
|
|
.then(response => response.json());
|
|
}
|
|
|
|
fetchBuffer(url) {
|
|
return fetch(url)
|
|
.then(response => response.arrayBuffer());
|
|
}
|
|
|
|
fetchImage(url) {
|
|
return fetch(url)
|
|
.then(response => response.blob())
|
|
.then(blob => createImageBitmap(blob));
|
|
}
|
|
|
|
async preloadImage(gltfSpec) {
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
if (gltfSpec.uri) {
|
|
const url = new URL(gltfSpec.uri, this.gltfUrl);
|
|
const image = await this.fetchImage(url);
|
|
this.cache.set(gltfSpec, image);
|
|
return image;
|
|
} else {
|
|
const bufferView = this.gltf.bufferViews[gltfSpec.bufferView];
|
|
const buffer = this.loadBuffer(bufferView.buffer);
|
|
const dataView = new DataView(buffer, bufferView.byteOffset ?? 0, bufferView.byteLength);
|
|
const blob = new Blob([dataView], { type: gltfSpec.mimeType });
|
|
const url = URL.createObjectURL(blob);
|
|
const image = await this.fetchImage(url);
|
|
URL.revokeObjectURL(url);
|
|
this.cache.set(gltfSpec, image);
|
|
return image;
|
|
}
|
|
}
|
|
|
|
async preloadBuffer(gltfSpec) {
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const url = new URL(gltfSpec.uri, this.gltfUrl);
|
|
const buffer = await this.fetchBuffer(url);
|
|
this.cache.set(gltfSpec, buffer);
|
|
return buffer;
|
|
}
|
|
|
|
loadImage(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.images, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
loadBuffer(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.buffers, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
loadSampler(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.samplers, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const minFilter = {
|
|
9728: 'nearest',
|
|
9729: 'linear',
|
|
9984: 'nearest',
|
|
9985: 'linear',
|
|
9986: 'nearest',
|
|
9987: 'linear',
|
|
};
|
|
|
|
const magFilter = {
|
|
9728: 'nearest',
|
|
9729: 'linear',
|
|
};
|
|
|
|
const mipmapFilter = {
|
|
9728: 'nearest',
|
|
9729: 'linear',
|
|
9984: 'nearest',
|
|
9985: 'nearest',
|
|
9986: 'linear',
|
|
9987: 'linear',
|
|
};
|
|
|
|
const addressMode = {
|
|
33071: 'clamp-to-edge',
|
|
33648: 'mirror-repeat',
|
|
10497: 'repeat',
|
|
};
|
|
|
|
const sampler = new Sampler({
|
|
minFilter: minFilter[gltfSpec.minFilter ?? 9729],
|
|
magFilter: magFilter[gltfSpec.magFilter ?? 9729],
|
|
mipmapFilter: mipmapFilter[gltfSpec.minFilter ?? 9729],
|
|
addressModeU: addressMode[gltfSpec.wrapS ?? 10497],
|
|
addressModeV: addressMode[gltfSpec.wrapT ?? 10497],
|
|
});
|
|
|
|
this.cache.set(gltfSpec, sampler);
|
|
return sampler;
|
|
}
|
|
|
|
loadTexture(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.textures, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const options = {};
|
|
if (gltfSpec.source !== undefined) {
|
|
options.image = this.loadImage(gltfSpec.source);
|
|
}
|
|
if (gltfSpec.sampler !== undefined) {
|
|
options.sampler = this.loadSampler(gltfSpec.sampler);
|
|
} else {
|
|
options.sampler = new Sampler();
|
|
}
|
|
|
|
const texture = new Texture(options);
|
|
|
|
this.cache.set(gltfSpec, texture);
|
|
return texture;
|
|
}
|
|
|
|
loadMaterial(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.materials, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const options = {};
|
|
const pbr = gltfSpec.pbrMetallicRoughness;
|
|
if (pbr) {
|
|
if (pbr.baseColorTexture) {
|
|
options.baseTexture = this.loadTexture(pbr.baseColorTexture.index);
|
|
options.baseTexture.isSRGB = true;
|
|
}
|
|
if (pbr.metallicRoughnessTexture) {
|
|
options.metalnessTexture = this.loadTexture(pbr.metallicRoughnessTexture.index);
|
|
options.roughnessTexture = this.loadTexture(pbr.metallicRoughnessTexture.index);
|
|
}
|
|
options.baseFactor = pbr.baseColorFactor;
|
|
options.metalnessFactor = pbr.metallicFactor;
|
|
options.roughnessFactor = pbr.roughnessFactor;
|
|
}
|
|
|
|
if (gltfSpec.normalTexture) {
|
|
options.normalTexture = this.loadTexture(gltfSpec.normalTexture.index);
|
|
options.normalFactor = gltfSpec.normalTexture.scale;
|
|
}
|
|
|
|
if (gltfSpec.emissiveTexture) {
|
|
options.emissionTexture = this.loadTexture(gltfSpec.emissiveTexture.index);
|
|
options.emissionTexture.isSRGB = true;
|
|
options.emissionFactor = gltfSpec.emissiveFactor;
|
|
}
|
|
|
|
if (gltfSpec.occlusionTexture) {
|
|
options.occlusionTexture = this.loadTexture(gltfSpec.occlusionTexture.index);
|
|
options.occlusionFactor = gltfSpec.occlusionTexture.strength;
|
|
}
|
|
|
|
const material = new Material(options);
|
|
|
|
this.cache.set(gltfSpec, material);
|
|
return material;
|
|
}
|
|
|
|
loadAccessor(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.accessors, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
if (gltfSpec.bufferView === undefined) {
|
|
console.warn('Accessor does not reference a buffer view');
|
|
return null;
|
|
}
|
|
|
|
const bufferView = this.gltf.bufferViews[gltfSpec.bufferView];
|
|
const buffer = this.loadBuffer(bufferView.buffer);
|
|
|
|
const componentType = {
|
|
5120: 'int',
|
|
5121: 'int',
|
|
5122: 'int',
|
|
5123: 'int',
|
|
5124: 'int',
|
|
5125: 'int',
|
|
5126: 'float',
|
|
}[gltfSpec.componentType];
|
|
|
|
const componentSize = {
|
|
5120: 1,
|
|
5121: 1,
|
|
5122: 2,
|
|
5123: 2,
|
|
5124: 4,
|
|
5125: 4,
|
|
5126: 4,
|
|
}[gltfSpec.componentType];
|
|
|
|
const componentSigned = {
|
|
5120: true,
|
|
5121: false,
|
|
5122: true,
|
|
5123: false,
|
|
5124: true,
|
|
5125: false,
|
|
5126: false,
|
|
}[gltfSpec.componentType];
|
|
|
|
const componentCount = {
|
|
SCALAR: 1,
|
|
VEC2: 2,
|
|
VEC3: 3,
|
|
VEC4: 4,
|
|
MAT2: 4,
|
|
MAT3: 9,
|
|
MAT4: 16,
|
|
}[gltfSpec.type];
|
|
|
|
const componentNormalized = gltfSpec.normalized ?? false;
|
|
|
|
const stride = bufferView.byteStride ?? (componentSize * componentCount);
|
|
const offset = gltfSpec.byteOffset ?? 0;
|
|
const viewOffset = bufferView.byteOffset ?? 0;
|
|
const viewLength = bufferView.byteLength;
|
|
|
|
const accessor = new Accessor({
|
|
buffer,
|
|
viewLength,
|
|
viewOffset,
|
|
offset,
|
|
stride,
|
|
|
|
componentType,
|
|
componentCount,
|
|
componentSize,
|
|
componentSigned,
|
|
componentNormalized,
|
|
});
|
|
|
|
this.cache.set(gltfSpec, accessor);
|
|
return accessor;
|
|
}
|
|
|
|
createMeshFromPrimitive(spec) {
|
|
if (spec.attributes.POSITION === undefined) {
|
|
console.warn('No position in mesh');
|
|
return new Mesh();
|
|
}
|
|
|
|
if (spec.indices === undefined) {
|
|
console.warn('No indices in mesh');
|
|
return new Mesh();
|
|
}
|
|
|
|
const accessors = {};
|
|
for (const attribute in spec.attributes) {
|
|
accessors[attribute] = this.loadAccessor(spec.attributes[attribute]);
|
|
}
|
|
|
|
const position = accessors.POSITION;
|
|
const texcoords = accessors.TEXCOORD_0;
|
|
const normal = accessors.NORMAL;
|
|
const tangent = accessors.TANGENT;
|
|
|
|
const vertexCount = position.count;
|
|
const vertices = [];
|
|
|
|
for (let i = 0; i < vertexCount; i++) {
|
|
const options = {};
|
|
|
|
if (position) { options.position = position.get(i); }
|
|
if (texcoords) { options.texcoords = texcoords.get(i); }
|
|
if (normal) { options.normal = normal.get(i); }
|
|
if (tangent) { options.tangent = tangent.get(i); }
|
|
|
|
vertices.push(new Vertex(options));
|
|
}
|
|
|
|
const indices = [];
|
|
const indicesAccessor = this.loadAccessor(spec.indices);
|
|
const indexCount = indicesAccessor.count;
|
|
|
|
for (let i = 0; i < indexCount; i++) {
|
|
indices.push(indicesAccessor.get(i));
|
|
}
|
|
|
|
return new Mesh({ vertices, indices });
|
|
}
|
|
|
|
loadMesh(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.meshes, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const primitives = [];
|
|
for (const primitiveSpec of gltfSpec.primitives) {
|
|
if (primitiveSpec.mode !== 4 && primitiveSpec.mode !== undefined) {
|
|
console.warn(`GLTFLoader: skipping primitive with mode ${primitiveSpec.mode}`);
|
|
continue;
|
|
}
|
|
|
|
const options = {};
|
|
options.mesh = this.createMeshFromPrimitive(primitiveSpec);
|
|
|
|
if (primitiveSpec.material !== undefined) {
|
|
options.material = this.loadMaterial(primitiveSpec.material);
|
|
}
|
|
|
|
primitives.push(new Primitive(options));
|
|
}
|
|
|
|
const model = new Model({ primitives });
|
|
|
|
this.cache.set(gltfSpec, model);
|
|
return model;
|
|
}
|
|
|
|
loadCamera(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.cameras, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const options = {};
|
|
if (gltfSpec.type === 'perspective') {
|
|
const { aspectRatio, yfov, znear, zfar } = gltfSpec.perspective;
|
|
Object.assign(options, {
|
|
orthographic: 0,
|
|
aspect: aspectRatio,
|
|
fovy: yfov,
|
|
near: znear,
|
|
far: zfar,
|
|
});
|
|
} else if (gltfSpec.type === 'orthographic') {
|
|
const { xmag, ymag, znear, zfar } = gltfSpec.orthographic;
|
|
Object.assign(options, {
|
|
orthographic: 1,
|
|
aspect: xmag / ymag,
|
|
halfy: ymag,
|
|
near: znear,
|
|
far: zfar,
|
|
});
|
|
}
|
|
|
|
const camera = new Camera(options);
|
|
|
|
this.cache.set(gltfSpec, camera);
|
|
return camera;
|
|
}
|
|
|
|
loadNode(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.nodes, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const node = new Node();
|
|
|
|
node.addComponent(new Transform(gltfSpec));
|
|
|
|
if (gltfSpec.children) {
|
|
for (const childIndex of gltfSpec.children) {
|
|
node.addChild(this.loadNode(childIndex));
|
|
}
|
|
}
|
|
|
|
if (gltfSpec.camera !== undefined) {
|
|
node.addComponent(this.loadCamera(gltfSpec.camera));
|
|
}
|
|
|
|
if (gltfSpec.mesh !== undefined) {
|
|
node.addComponent(this.loadMesh(gltfSpec.mesh));
|
|
}
|
|
|
|
this.cache.set(gltfSpec, node);
|
|
return node;
|
|
}
|
|
|
|
loadScene(nameOrIndex) {
|
|
const gltfSpec = this.findByNameOrIndex(this.gltf.scenes, nameOrIndex);
|
|
if (!gltfSpec) {
|
|
return null;
|
|
}
|
|
if (this.cache.has(gltfSpec)) {
|
|
return this.cache.get(gltfSpec);
|
|
}
|
|
|
|
const scene = new Node();
|
|
if (gltfSpec.nodes) {
|
|
for (const nodeIndex of gltfSpec.nodes) {
|
|
scene.addChild(this.loadNode(nodeIndex));
|
|
}
|
|
}
|
|
|
|
this.cache.set(gltfSpec, scene);
|
|
return scene;
|
|
}
|
|
|
|
}
|