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;
 | |
|     }
 | |
| 
 | |
| }
 |