blob: d2683c1c1bc16721f08e62623f6d310eeda5a828 [file] [edit]
// Copyright 2018 The Immersive Web Community Group
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import {PbrMaterial} from '../materials/pbr.js';
import {Node} from '../core/node.js';
import {Primitive, PrimitiveAttribute} from '../core/primitive.js';
import {ImageTexture, ColorTexture} from '../core/texture.js';
const GL = WebGLRenderingContext; // For enums
const GLB_MAGIC = 0x46546C67;
const CHUNK_TYPE = {
JSON: 0x4E4F534A,
BIN: 0x004E4942,
};
function isAbsoluteUri(uri) {
let absRegEx = new RegExp('^'+window.location.protocol, 'i');
return !!uri.match(absRegEx);
}
function isDataUri(uri) {
let dataRegEx = /^data:/;
return !!uri.match(dataRegEx);
}
function resolveUri(uri, baseUrl) {
if (isAbsoluteUri(uri) || isDataUri(uri)) {
return uri;
}
return baseUrl + uri;
}
function getComponentCount(type) {
switch (type) {
case 'SCALAR': return 1;
case 'VEC2': return 2;
case 'VEC3': return 3;
case 'VEC4': return 4;
default: return 0;
}
}
/**
* Gltf2SceneLoader
* Loads glTF 2.0 scenes into a renderable node tree.
*/
export class Gltf2Loader {
constructor(renderer) {
this.renderer = renderer;
this._gl = renderer._gl;
}
loadFromUrl(url) {
return fetch(url)
.then((response) => {
let i = url.lastIndexOf('/');
let baseUrl = (i !== 0) ? url.substring(0, i + 1) : '';
if (url.endsWith('.gltf')) {
return response.json().then((json) => {
return this.loadFromJson(json, baseUrl);
});
} else if (url.endsWith('.glb')) {
return response.arrayBuffer().then((arrayBuffer) => {
return this.loadFromBinary(arrayBuffer, baseUrl);
});
} else {
throw new Error('Unrecognized file extension');
}
});
}
loadFromBinary(arrayBuffer, baseUrl) {
let headerView = new DataView(arrayBuffer, 0, 12);
let magic = headerView.getUint32(0, true);
let version = headerView.getUint32(4, true);
let length = headerView.getUint32(8, true);
if (magic != GLB_MAGIC) {
throw new Error('Invalid magic string in binary header.');
}
if (version != 2) {
throw new Error('Incompatible version in binary header.');
}
let chunks = {};
let chunkOffset = 12;
while (chunkOffset < length) {
let chunkHeaderView = new DataView(arrayBuffer, chunkOffset, 8);
let chunkLength = chunkHeaderView.getUint32(0, true);
let chunkType = chunkHeaderView.getUint32(4, true);
chunks[chunkType] = arrayBuffer.slice(chunkOffset + 8, chunkOffset + 8 + chunkLength);
chunkOffset += chunkLength + 8;
}
if (!chunks[CHUNK_TYPE.JSON]) {
throw new Error('File contained no json chunk.');
}
let decoder = new TextDecoder('utf-8');
let jsonString = decoder.decode(chunks[CHUNK_TYPE.JSON]);
let json = JSON.parse(jsonString);
return this.loadFromJson(json, baseUrl, chunks[CHUNK_TYPE.BIN]);
}
loadFromJson(json, baseUrl, binaryChunk) {
if (!json.asset) {
throw new Error('Missing asset description.');
}
if (json.asset.minVersion != '2.0' && json.asset.version != '2.0') {
throw new Error('Incompatible asset version.');
}
let buffers = [];
if (binaryChunk) {
buffers[0] = new Gltf2Resource({}, baseUrl, binaryChunk);
} else {
for (let buffer of json.buffers) {
buffers.push(new Gltf2Resource(buffer, baseUrl));
}
}
let bufferViews = [];
for (let bufferView of json.bufferViews) {
bufferViews.push(new Gltf2BufferView(bufferView, buffers));
}
let images = [];
if (json.images) {
for (let image of json.images) {
images.push(new Gltf2Resource(image, baseUrl));
}
}
let textures = [];
if (json.textures) {
for (let texture of json.textures) {
let image = images[texture.source];
let glTexture = image.texture(bufferViews);
if (texture.sampler) {
let sampler = sampler[texture.sampler];
glTexture.sampler.minFilter = sampler.minFilter;
glTexture.sampler.magFilter = sampler.magFilter;
glTexture.sampler.wrapS = sampler.wrapS;
glTexture.sampler.wrapT = sampler.wrapT;
}
textures.push(glTexture);
}
}
function getTexture(textureInfo) {
if (!textureInfo) {
return null;
}
return textures[textureInfo.index];
}
let materials = [];
if (json.materials) {
for (let material of json.materials) {
let glMaterial = new PbrMaterial();
let pbr = material.pbrMetallicRoughness || {};
glMaterial.baseColorFactor.value = pbr.baseColorFactor || [1, 1, 1, 1];
glMaterial.baseColor.texture = getTexture(pbr.baseColorTexture);
glMaterial.metallicRoughnessFactor.value = [
pbr.metallicFactor || 1.0,
pbr.roughnessFactor || 1.0,
];
glMaterial.metallicRoughness.texture = getTexture(pbr.metallicRoughnessTexture);
glMaterial.normal.texture = getTexture(material.normalTexture);
glMaterial.occlusion.texture = getTexture(material.occlusionTexture);
glMaterial.occlusionStrength.value = (material.occlusionTexture && material.occlusionTexture.strength) ?
material.occlusionTexture.strength : 1.0;
glMaterial.emissiveFactor.value = material.emissiveFactor || [0, 0, 0];
glMaterial.emissive.texture = getTexture(material.emissiveTexture);
if (!glMaterial.emissive.texture && material.emissiveFactor) {
glMaterial.emissive.texture = new ColorTexture(1.0, 1.0, 1.0, 1.0);
}
switch (material.alphaMode) {
case 'BLEND':
glMaterial.state.blend = true;
break;
case 'MASK':
// Not really supported.
glMaterial.state.blend = true;
break;
default: // Includes 'OPAQUE'
glMaterial.state.blend = false;
}
// glMaterial.alpha_mode = material.alphaMode;
// glMaterial.alpha_cutoff = material.alphaCutoff;
glMaterial.state.cullFace = !(material.doubleSided);
materials.push(glMaterial);
}
}
let accessors = json.accessors;
let meshes = [];
for (let mesh of json.meshes) {
let glMesh = new Gltf2Mesh();
meshes.push(glMesh);
for (let primitive of mesh.primitives) {
let material = null;
if ('material' in primitive) {
material = materials[primitive.material];
} else {
// Create a "default" material if the primitive has none.
material = new PbrMaterial();
}
let attributes = [];
let elementCount = 0;
/* let glPrimitive = new Gltf2Primitive(primitive, material);
glMesh.primitives.push(glPrimitive); */
let min = null;
let max = null;
for (let name in primitive.attributes) {
let accessor = accessors[primitive.attributes[name]];
let bufferView = bufferViews[accessor.bufferView];
elementCount = accessor.count;
let glAttribute = new PrimitiveAttribute(
name,
bufferView.renderBuffer(this.renderer, GL.ARRAY_BUFFER),
getComponentCount(accessor.type),
accessor.componentType,
bufferView.byteStride || 0,
accessor.byteOffset || 0
);
glAttribute.normalized = accessor.normalized || false;
if (name == 'POSITION') {
min = accessor.min;
max = accessor.max;
}
attributes.push(glAttribute);
}
let glPrimitive = new Primitive(attributes, elementCount, primitive.mode);
if ('indices' in primitive) {
let accessor = accessors[primitive.indices];
let bufferView = bufferViews[accessor.bufferView];
glPrimitive.setIndexBuffer(
bufferView.renderBuffer(this.renderer, GL.ELEMENT_ARRAY_BUFFER),
accessor.byteOffset || 0,
accessor.componentType
);
glPrimitive.indexType = accessor.componentType;
glPrimitive.indexByteOffset = accessor.byteOffset || 0;
glPrimitive.elementCount = accessor.count;
}
if (min && max) {
glPrimitive.setBounds(min, max);
}
// After all the attributes have been processed, get a program that is
// appropriate for both the material and the primitive attributes.
glMesh.primitives.push(
this.renderer.createRenderPrimitive(glPrimitive, material));
}
}
let sceneNode = new Node();
let scene = json.scenes[json.scene];
for (let nodeId of scene.nodes) {
let node = json.nodes[nodeId];
sceneNode.addNode(
this.processNodes(node, json.nodes, meshes));
}
return sceneNode;
}
processNodes(node, nodes, meshes) {
let glNode = new Node();
glNode.name = node.name;
if ('mesh' in node) {
let mesh = meshes[node.mesh];
for (let primitive of mesh.primitives) {
glNode.addRenderPrimitive(primitive);
}
}
if (node.matrix) {
glNode.matrix = new Float32Array(node.matrix);
} else if (node.translation || node.rotation || node.scale) {
if (node.translation) {
glNode.translation = new Float32Array(node.translation);
}
if (node.rotation) {
glNode.rotation = new Float32Array(node.rotation);
}
if (node.scale) {
glNode.scale = new Float32Array(node.scale);
}
}
if (node.children) {
for (let nodeId of node.children) {
let node = nodes[nodeId];
glNode.addNode(this.processNodes(node, nodes, meshes));
}
}
return glNode;
}
}
class Gltf2Mesh {
constructor() {
this.primitives = [];
}
}
class Gltf2BufferView {
constructor(json, buffers) {
this.buffer = buffers[json.buffer];
this.byteOffset = json.byteOffset || 0;
this.byteLength = json.byteLength || null;
this.byteStride = json.byteStride;
this._viewPromise = null;
this._renderBuffer = null;
}
dataView() {
if (!this._viewPromise) {
this._viewPromise = this.buffer.arrayBuffer().then((arrayBuffer) => {
return new DataView(arrayBuffer, this.byteOffset, this.byteLength);
});
}
return this._viewPromise;
}
renderBuffer(renderer, target) {
if (!this._renderBuffer) {
this._renderBuffer = renderer.createRenderBuffer(target, this.dataView());
}
return this._renderBuffer;
}
}
class Gltf2Resource {
constructor(json, baseUrl, arrayBuffer) {
this.json = json;
this.baseUrl = baseUrl;
this._dataPromise = null;
this._texture = null;
if (arrayBuffer) {
this._dataPromise = Promise.resolve(arrayBuffer);
}
}
arrayBuffer() {
if (!this._dataPromise) {
if (isDataUri(this.json.uri)) {
let base64String = this.json.uri.replace('data:application/octet-stream;base64,', '');
let binaryArray = Uint8Array.from(atob(base64String), (c) => c.charCodeAt(0));
this._dataPromise = Promise.resolve(binaryArray.buffer);
return this._dataPromise;
}
this._dataPromise = fetch(resolveUri(this.json.uri, this.baseUrl))
.then((response) => response.arrayBuffer());
}
return this._dataPromise;
}
texture(bufferViews) {
if (!this._texture) {
let img = new Image();
this._texture = new ImageTexture(img);
if (this.json.uri) {
if (isDataUri(this.json.uri)) {
img.src = this.json.uri;
} else {
img.src = `${this.baseUrl}${this.json.uri}`;
}
} else {
this._texture.genDataKey();
let view = bufferViews[this.json.bufferView];
view.dataView().then((dataView) => {
let blob = new Blob([dataView], {type: this.json.mimeType});
img.src = window.URL.createObjectURL(blob);
});
}
}
return this._texture;
}
}