/** * glTF Loader * Specification https://github.com/KhronosGroup/glTF/blob/master/specification/README.md * * TODO Morph targets */ import Base from '../core/Base'; import util from '../core/util'; import vendor from '../core/vendor'; import Scene from '../Scene'; import Material from '../Material'; import StandardMaterial from '../StandardMaterial'; import Mesh from '../Mesh'; import Node from '../Node'; import Texture from '../Texture'; import Texture2D from '../Texture2D'; import shaderLibrary from '../shader/library'; import Skeleton from '../Skeleton'; import Joint from '../Joint'; import PerspectiveCamera from '../camera/Perspective'; import OrthographicCamera from '../camera/Orthographic'; import glenum from '../core/glenum'; import BoundingBox from '../math/BoundingBox'; import TrackClip from '../animation/TrackClip'; import SamplerTrack from '../animation/SamplerTrack'; import Geometry from '../Geometry'; // Import builtin shader import '../shader/builtin'; import Shader from '../Shader'; var semanticAttributeMap = { 'NORMAL': 'normal', 'POSITION': 'position', 'TEXCOORD_0': 'texcoord0', 'TEXCOORD_1': 'texcoord1', 'WEIGHTS_0': 'weight', 'JOINTS_0': 'joint', 'COLOR_0': 'color' }; var ARRAY_CTOR_MAP = { 5120: vendor.Int8Array, 5121: vendor.Uint8Array, 5122: vendor.Int16Array, 5123: vendor.Uint16Array, 5125: vendor.Uint32Array, 5126: vendor.Float32Array }; var SIZE_MAP = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 }; function getAccessorData(json, lib, accessorIdx, isIndices) { var accessorInfo = json.accessors[accessorIdx]; var buffer = lib.bufferViews[accessorInfo.bufferView]; var byteOffset = accessorInfo.byteOffset || 0; var ArrayCtor = ARRAY_CTOR_MAP[accessorInfo.componentType] || vendor.Float32Array; var size = SIZE_MAP[accessorInfo.type]; if (size == null && isIndices) { size = 1; } var arr = new ArrayCtor(buffer, byteOffset, size * accessorInfo.count); var quantizeExtension = accessorInfo.extensions && accessorInfo.extensions['WEB3D_quantized_attributes']; if (quantizeExtension) { var decodedArr = new vendor.Float32Array(size * accessorInfo.count); var decodeMatrix = quantizeExtension.decodeMatrix; var decodeOffset; var decodeScale; var decodeOffset = new Array(size); var decodeScale = new Array(size); for (var k = 0; k < size; k++) { decodeOffset[k] = decodeMatrix[size * (size + 1) + k]; decodeScale[k] = decodeMatrix[k * (size + 1) + k]; } for (var i = 0; i < accessorInfo.count; i++) { for (var k = 0; k < size; k++) { decodedArr[i * size + k] = arr[i * size + k] * decodeScale[k] + decodeOffset[k]; } } arr = decodedArr; } return arr; } function base64ToBinary(input, charStart) { var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var lookup = new Uint8Array(130); for (var i = 0; i < chars.length; i++) { lookup[chars.charCodeAt(i)] = i; } // Ignore var len = input.length - charStart; if (input.charAt(len - 1) === '=') { len--; } if (input.charAt(len - 1) === '=') { len--; } var uarray = new Uint8Array((len / 4) * 3); for (var i = 0, j = charStart; i < uarray.length;) { var c1 = lookup[input.charCodeAt(j++)]; var c2 = lookup[input.charCodeAt(j++)]; var c3 = lookup[input.charCodeAt(j++)]; var c4 = lookup[input.charCodeAt(j++)]; uarray[i++] = (c1 << 2) | (c2 >> 4); uarray[i++] = ((c2 & 15) << 4) | (c3 >> 2); uarray[i++] = ((c3 & 3) << 6) | c4; } return uarray.buffer; } /** * @typedef {Object} clay.loader.GLTF.Result * @property {Object} json * @property {clay.Scene} scene * @property {clay.Node} rootNode * @property {clay.Camera[]} cameras * @property {clay.Texture[]} textures * @property {clay.Material[]} materials * @property {clay.Skeleton[]} skeletons * @property {clay.Mesh[]} meshes * @property {clay.animation.TrackClip[]} clips * @property {clay.Node[]} nodes */ /** * @constructor clay.loader.GLTF * @extends clay.core.Base */ var GLTFLoader = Base.extend(/** @lends clay.loader.GLTF# */ { /** * * @type {clay.Node} */ rootNode: null, /** * Root path for uri parsing. * @type {string} */ rootPath: null, /** * Root path for texture uri parsing. Defaultly use the rootPath * @type {string} */ textureRootPath: null, /** * Root path for buffer uri parsing. Defaultly use the rootPath * @type {string} */ bufferRootPath: null, /** * Shader used when creating the materials. * @type {string|clay.Shader} * @default 'clay.standard' */ shader: 'clay.standard', /** * If use {@link clay.StandardMaterial} * @type {string} */ useStandardMaterial: false, /** * If loading the cameras. * @type {boolean} */ includeCamera: true, /** * If loading the animations. * @type {boolean} */ includeAnimation: true, /** * If loading the meshes * @type {boolean} */ includeMesh: true, /** * If loading the textures. * @type {boolean} */ includeTexture: true, /** * @type {string} */ crossOrigin: '', /** * @type {boolean} * @see https://github.com/KhronosGroup/glTF/issues/674 */ textureFlipY: false, /** * If convert texture to power-of-two * @type {boolean} */ textureConvertToPOT: false, shaderLibrary: null }, function () { if (!this.shaderLibrary) { this.shaderLibrary = shaderLibrary.createLibrary(); } }, /** @lends clay.loader.GLTF.prototype */ { /** * @param {string} url */ load: function (url) { var self = this; var isBinary = url.endsWith('.glb'); if (this.rootPath == null) { this.rootPath = url.slice(0, url.lastIndexOf('/')); } vendor.request.get({ url: url, onprogress: function (percent, loaded, total) { self.trigger('progress', percent, loaded, total); }, onerror: function (e) { self.trigger('error', e); }, responseType: isBinary ? 'arraybuffer' : 'text', onload: function (data) { if (isBinary) { self.parseBinary(data); } else { if (typeof data === 'string') { data = JSON.parse(data); } self.parse(data); } } }); }, /** * Parse glTF binary * @param {ArrayBuffer} buffer * @return {clay.loader.GLTF.Result} */ parseBinary: function (buffer) { var header = new Uint32Array(buffer, 0, 4); if (header[0] !== 0x46546C67) { this.trigger('error', 'Invalid glTF binary format: Invalid header'); return; } if (header[0] < 2) { this.trigger('error', 'Only glTF2.0 is supported.'); return; } var dataView = new DataView(buffer, 12); var json; var buffers = []; // Read chunks for (var i = 0; i < dataView.byteLength;) { var chunkLength = dataView.getUint32(i, true); i += 4; var chunkType = dataView.getUint32(i, true); i += 4; // json if (chunkType === 0x4E4F534A) { var arr = new Uint8Array(buffer, i + 12, chunkLength); // TODO, for the browser not support TextDecoder. var decoder = new TextDecoder(); var str = decoder.decode(arr); try { json = JSON.parse(str); } catch (e) { this.trigger('error', 'JSON Parse error:' + e.toString()); return; } } else if (chunkType === 0x004E4942) { buffers.push(buffer.slice(i + 12, i + 12 + chunkLength)); } i += chunkLength; } if (!json) { this.trigger('error', 'Invalid glTF binary format: Can\'t find JSON.'); return; } return this.parse(json, buffers); }, /** * @param {Object} json * @param {ArrayBuffer[]} [buffer] * @return {clay.loader.GLTF.Result} */ parse: function (json, buffers) { var self = this; var lib = { json: json, buffers: [], bufferViews: [], materials: [], textures: [], meshes: [], joints: [], skeletons: [], cameras: [], nodes: [], clips: [] }; // Mount on the root node if given var rootNode = this.rootNode || new Scene(); var loading = 0; function checkLoad() { loading--; if (loading === 0) { afterLoadBuffer(); } } // If already load buffers if (buffers) { lib.buffers = buffers.slice(); afterLoadBuffer(true); } else { // Load buffers util.each(json.buffers, function (bufferInfo, idx) { loading++; var path = bufferInfo.uri; self._loadBuffers(path, function (buffer) { lib.buffers[idx] = buffer; checkLoad(); }, checkLoad); }); } function getResult() { return { json: json, scene: self.rootNode ? null : rootNode, rootNode: self.rootNode ? rootNode : null, cameras: lib.cameras, textures: lib.textures, materials: lib.materials, skeletons: lib.skeletons, meshes: lib.instancedMeshes, clips: lib.clips, nodes: lib.nodes }; } function afterLoadBuffer(immediately) { // Buffer not load complete. if (lib.buffers.length !== json.buffers.length) { setTimeout(function () { self.trigger('error', 'Buffer not load complete.'); }); return; } json.bufferViews.forEach(function (bufferViewInfo, idx) { // PENDING Performance lib.bufferViews[idx] = lib.buffers[bufferViewInfo.buffer] .slice(bufferViewInfo.byteOffset || 0, (bufferViewInfo.byteOffset || 0) + (bufferViewInfo.byteLength || 0)); }); lib.buffers = null; if (self.includeMesh) { if (self.includeTexture) { self._parseTextures(json, lib); } self._parseMaterials(json, lib); self._parseMeshes(json, lib); } self._parseNodes(json, lib); // Only support one scene. if (json.scenes) { var sceneInfo = json.scenes[json.scene || 0]; // Default use the first scene. if (sceneInfo) { for (var i = 0; i < sceneInfo.nodes.length; i++) { var node = lib.nodes[sceneInfo.nodes[i]]; node.update(); rootNode.add(node); } } } if (self.includeMesh) { self._parseSkins(json, lib); } if (self.includeAnimation) { self._parseAnimations(json, lib); } if (immediately) { setTimeout(function () { self.trigger('success', getResult()); }); } else { self.trigger('success', getResult()); } } return getResult(); }, /** * Binary file path resolver. User can override it * @param {string} path */ resolveBufferPath: function (path) { if (path && path.match(/^data:(.*?)base64,/)) { return path; } var rootPath = this.bufferRootPath; if (rootPath == null) { rootPath = this.rootPath; } return util.relative2absolute(path, rootPath); }, /** * Texture file path resolver. User can override it * @param {string} path */ resolveTexturePath: function (path) { if (path && path.match(/^data:(.*?)base64,/)) { return path; } var rootPath = this.textureRootPath; if (rootPath == null) { rootPath = this.rootPath; } return util.relative2absolute(path, rootPath); }, /** * Buffer loader * @param {string} * @param {Function} onsuccess * @param {Function} onerror */ loadBuffer: function (path, onsuccess, onerror) { vendor.request.get({ url: path, responseType: 'arraybuffer', onload: function (buffer) { onsuccess && onsuccess(buffer); }, onerror: function (buffer) { onerror && onerror(buffer); } }); }, _getShader: function () { if (typeof this.shader === 'string') { return this.shaderLibrary.get(this.shader); } else if (this.shader instanceof Shader) { return this.shader; } }, _loadBuffers: function (path, onsuccess, onerror) { var base64Prefix = 'data:application/octet-stream;base64,'; var strStart = path.substr(0, base64Prefix.length); if (strStart === base64Prefix) { onsuccess( base64ToBinary(path, base64Prefix.length) ); } else { this.loadBuffer( this.resolveBufferPath(path), onsuccess, onerror ); } }, // https://github.com/KhronosGroup/glTF/issues/100 // https://github.com/KhronosGroup/glTF/issues/193 _parseSkins: function (json, lib) { // Create skeletons and joints var haveInvBindMatrices = false; util.each(json.skins, function (skinInfo, idx) { var skeleton = new Skeleton({ name: skinInfo.name }); for (var i = 0; i < skinInfo.joints.length; i++) { var nodeIdx = skinInfo.joints[i]; var node = lib.nodes[nodeIdx]; var joint = new Joint({ name: node.name, node: node, index: skeleton.joints.length }); skeleton.joints.push(joint); } skeleton.relativeRootNode = lib.nodes[skinInfo.skeleton] || this.rootNode; if (skinInfo.inverseBindMatrices) { haveInvBindMatrices = true; var IBMInfo = json.accessors[skinInfo.inverseBindMatrices]; var buffer = lib.bufferViews[IBMInfo.bufferView]; var offset = IBMInfo.byteOffset || 0; var size = IBMInfo.count * 16; var array = new vendor.Float32Array(buffer, offset, size); skeleton.setJointMatricesArray(array); } else { skeleton.updateJointMatrices(); } lib.skeletons[idx] = skeleton; }, this); function enableSkinningForMesh(mesh, skeleton, jointIndices) { mesh.skeleton = skeleton; mesh.joints = jointIndices; if (!skeleton.boundingBox) { skeleton.updateJointsBoundingBoxes(mesh.geometry); } } function getJointIndex(joint) { return joint.index; } util.each(json.nodes, function (nodeInfo, nodeIdx) { if (nodeInfo.skin != null) { var skinIdx = nodeInfo.skin; var skeleton = lib.skeletons[skinIdx]; var node = lib.nodes[nodeIdx]; var jointIndices = skeleton.joints.map(getJointIndex); if (node instanceof Mesh) { enableSkinningForMesh(node, skeleton, jointIndices); } else { // Mesh have multiple primitives var children = node.children(); for (var i = 0; i < children.length; i++) { enableSkinningForMesh(children[i], skeleton, jointIndices); } } } }, this); }, _parseTextures: function (json, lib) { util.each(json.textures, function (textureInfo, idx){ // samplers is optional var samplerInfo = (json.samplers && json.samplers[textureInfo.sampler]) || {}; var parameters = {}; ['wrapS', 'wrapT', 'magFilter', 'minFilter'].forEach(function (name) { var value = samplerInfo[name]; if (value != null) { parameters[name] = value; } }); util.defaults(parameters, { wrapS: Texture.REPEAT, wrapT: Texture.REPEAT, flipY: this.textureFlipY, convertToPOT: this.textureConvertToPOT }); var target = textureInfo.target || glenum.TEXTURE_2D; var format = textureInfo.format; if (format != null) { parameters.format = format; } if (target === glenum.TEXTURE_2D) { var texture = new Texture2D(parameters); var imageInfo = json.images[textureInfo.source]; var uri; if (imageInfo.uri) { uri = this.resolveTexturePath(imageInfo.uri); } else if (imageInfo.bufferView != null) { uri = URL.createObjectURL(new Blob([lib.bufferViews[imageInfo.bufferView]], { type: imageInfo.mimeType })); } if (uri) { texture.load(uri, this.crossOrigin); lib.textures[idx] = texture; } } }, this); }, _KHRCommonMaterialToStandard: function (materialInfo, lib) { var uniforms = {}; var commonMaterialInfo = materialInfo.extensions['KHR_materials_common']; uniforms = commonMaterialInfo.values || {}; if (typeof uniforms.diffuse === 'number') { uniforms.diffuse = lib.textures[uniforms.diffuse] || null; } if (typeof uniforms.emission === 'number') { uniforms.emission = lib.textures[uniforms.emission] || null; } var enabledTextures = []; if (uniforms['diffuse'] instanceof Texture2D) { enabledTextures.push('diffuseMap'); } if (materialInfo.normalTexture) { enabledTextures.push('normalMap'); } if (uniforms['emission'] instanceof Texture2D) { enabledTextures.push('emissiveMap'); } var material; var isStandardMaterial = this.useStandardMaterial; if (isStandardMaterial) { material = new StandardMaterial({ name: materialInfo.name, doubleSided: materialInfo.doubleSided }); } else { material = new Material({ name: materialInfo.name, shader: this._getShader() }); material.define('fragment', 'USE_ROUGHNESS'); material.define('fragment', 'USE_METALNESS'); if (materialInfo.doubleSided) { material.define('fragment', 'DOUBLE_SIDED'); } } if (uniforms.transparent) { material.depthMask = false; material.depthTest = true; material.transparent = true; } var diffuseProp = uniforms['diffuse']; if (diffuseProp) { // Color if (Array.isArray(diffuseProp)) { diffuseProp = diffuseProp.slice(0, 3); isStandardMaterial ? (material.color = diffuseProp) : material.set('color', diffuseProp); } else { // Texture isStandardMaterial ? (material.diffuseMap = diffuseProp) : material.set('diffuseMap', diffuseProp); } } var emissionProp = uniforms['emission']; if (emissionProp != null) { // Color if (Array.isArray(emissionProp)) { emissionProp = emissionProp.slice(0, 3); isStandardMaterial ? (material.emission = emissionProp) : material.set('emission', emissionProp); } else { // Texture isStandardMaterial ? (material.emissiveMap = emissionProp) : material.set('emissiveMap', emissionProp); } } if (materialInfo.normalTexture != null) { // TODO texCoord var normalTextureIndex = materialInfo.normalTexture.index; if (isStandardMaterial) { material.normalMap = lib.textures[normalTextureIndex] || null; } else { material.set('normalMap', lib.textures[normalTextureIndex] || null); } } if (uniforms['shininess'] != null) { var glossiness = Math.log(uniforms['shininess']) / Math.log(8192); // Uniform glossiness material.set('glossiness', glossiness); material.set('roughness', 1 - glossiness); } else { material.set('glossiness', 0.3); material.set('roughness', 0.3); } if (uniforms['specular'] != null) { material.set('specularColor', uniforms['specular'].slice(0, 3)); } if (uniforms['transparency'] != null) { material.set('alpha', uniforms['transparency']); } return material; }, _pbrMetallicRoughnessToStandard: function (materialInfo, metallicRoughnessMatInfo, lib) { var alphaTest = materialInfo.alphaMode === 'MASK'; var isStandardMaterial = this.useStandardMaterial; var material; var diffuseMap, roughnessMap, metalnessMap, normalMap, emissiveMap, occlusionMap; var enabledTextures = []; /** * The scalar multiplier applied to each normal vector of the normal texture. * * @type {number} * * XXX This value is ignored if `materialInfo.normalTexture` is not specified. */ var normalScale = 1.0; // TODO texCoord if (metallicRoughnessMatInfo.baseColorTexture) { diffuseMap = lib.textures[metallicRoughnessMatInfo.baseColorTexture.index] || null; diffuseMap && enabledTextures.push('diffuseMap'); } if (metallicRoughnessMatInfo.metallicRoughnessTexture) { roughnessMap = metalnessMap = lib.textures[metallicRoughnessMatInfo.metallicRoughnessTexture.index] || null; roughnessMap && enabledTextures.push('metalnessMap', 'roughnessMap'); } if (materialInfo.normalTexture) { normalMap = lib.textures[materialInfo.normalTexture.index] || null; normalMap && enabledTextures.push('normalMap'); if (typeof materialInfo.normalTexture.scale === 'number') { normalScale = materialInfo.normalTexture.scale; } } if (materialInfo.emissiveTexture) { emissiveMap = lib.textures[materialInfo.emissiveTexture.index] || null; emissiveMap && enabledTextures.push('emissiveMap'); } if (materialInfo.occlusionTexture) { occlusionMap = lib.textures[materialInfo.occlusionTexture.index] || null; occlusionMap && enabledTextures.push('occlusionMap'); } var baseColor = metallicRoughnessMatInfo.baseColorFactor || [1, 1, 1, 1]; var commonProperties = { diffuseMap: diffuseMap || null, roughnessMap: roughnessMap || null, metalnessMap: metalnessMap || null, normalMap: normalMap || null, occlusionMap: occlusionMap || null, emissiveMap: emissiveMap || null, color: baseColor.slice(0, 3), alpha: baseColor[3], metalness: metallicRoughnessMatInfo.metallicFactor || 0, roughness: metallicRoughnessMatInfo.roughnessFactor || 0, emission: materialInfo.emissiveFactor || [0, 0, 0], emissionIntensity: 1, alphaCutoff: materialInfo.alphaCutoff || 0, normalScale: normalScale }; if (commonProperties.roughnessMap) { // In glTF metallicFactor will do multiply, which is different from StandardMaterial. // So simply ignore it commonProperties.metalness = 0.5; commonProperties.roughness = 0.5; } if (isStandardMaterial) { material = new StandardMaterial(util.extend({ name: materialInfo.name, alphaTest: alphaTest, doubleSided: materialInfo.doubleSided, // G channel roughnessChannel: 1, // B Channel metalnessChannel: 2 }, commonProperties)); } else { material = new Material({ name: materialInfo.name, shader: this._getShader() }); material.define('fragment', 'USE_ROUGHNESS'); material.define('fragment', 'USE_METALNESS'); material.define('fragment', 'ROUGHNESS_CHANNEL', 1); material.define('fragment', 'METALNESS_CHANNEL', 2); material.define('fragment', 'DIFFUSEMAP_ALPHA_ALPHA'); if (alphaTest) { material.define('fragment', 'ALPHA_TEST'); } if (materialInfo.doubleSided) { material.define('fragment', 'DOUBLE_SIDED'); } material.set(commonProperties); } if (materialInfo.alphaMode === 'BLEND') { material.depthMask = false; material.depthTest = true; material.transparent = true; } return material; }, _pbrSpecularGlossinessToStandard: function (materialInfo, specularGlossinessMatInfo, lib) { var alphaTest = materialInfo.alphaMode === 'MASK'; if (this.useStandardMaterial) { console.error('StandardMaterial doesn\'t support specular glossiness workflow yet'); } var material; var diffuseMap, glossinessMap, specularMap, normalMap, emissiveMap, occlusionMap; var enabledTextures = []; // TODO texCoord if (specularGlossinessMatInfo.diffuseTexture) { diffuseMap = lib.textures[specularGlossinessMatInfo.diffuseTexture.index] || null; diffuseMap && enabledTextures.push('diffuseMap'); } if (specularGlossinessMatInfo.specularGlossinessTexture) { glossinessMap = specularMap = lib.textures[specularGlossinessMatInfo.specularGlossinessTexture.index] || null; glossinessMap && enabledTextures.push('specularMap', 'glossinessMap'); } if (materialInfo.normalTexture) { normalMap = lib.textures[materialInfo.normalTexture.index] || null; normalMap && enabledTextures.push('normalMap'); } if (materialInfo.emissiveTexture) { emissiveMap = lib.textures[materialInfo.emissiveTexture.index] || null; emissiveMap && enabledTextures.push('emissiveMap'); } if (materialInfo.occlusionTexture) { occlusionMap = lib.textures[materialInfo.occlusionTexture.index] || null; occlusionMap && enabledTextures.push('occlusionMap'); } var diffuseColor = specularGlossinessMatInfo.diffuseFactor || [1, 1, 1, 1]; var commonProperties = { diffuseMap: diffuseMap || null, glossinessMap: glossinessMap || null, specularMap: specularMap || null, normalMap: normalMap || null, emissiveMap: emissiveMap || null, occlusionMap: occlusionMap || null, color: diffuseColor.slice(0, 3), alpha: diffuseColor[3], specularColor: specularGlossinessMatInfo.specularFactor || [1, 1, 1], glossiness: specularGlossinessMatInfo.glossinessFactor || 0, emission: materialInfo.emissiveFactor || [0, 0, 0], emissionIntensity: 1, alphaCutoff: materialInfo.alphaCutoff == null ? 0.9 : materialInfo.alphaCutoff }; if (commonProperties.glossinessMap) { // Ignore specularFactor commonProperties.glossiness = 0.5; } if (commonProperties.specularMap) { // Ignore specularFactor commonProperties.specularColor = [1, 1, 1]; } material = new Material({ name: materialInfo.name, shader: this._getShader() }); material.define('fragment', 'GLOSSINESS_CHANNEL', 3); material.define('fragment', 'DIFFUSEMAP_ALPHA_ALPHA'); if (alphaTest) { material.define('fragment', 'ALPHA_TEST'); } if (materialInfo.doubleSided) { material.define('fragment', 'DOUBLE_SIDED'); } material.set(commonProperties); if (materialInfo.alphaMode === 'BLEND') { material.depthMask = false; material.depthTest = true; material.transparent = true; } return material; }, _parseMaterials: function (json, lib) { util.each(json.materials, function (materialInfo, idx) { if (materialInfo.extensions && materialInfo.extensions['KHR_materials_common']) { lib.materials[idx] = this._KHRCommonMaterialToStandard(materialInfo, lib); } else if (materialInfo.extensions && materialInfo.extensions['KHR_materials_pbrSpecularGlossiness']) { lib.materials[idx] = this._pbrSpecularGlossinessToStandard(materialInfo, materialInfo.extensions['KHR_materials_pbrSpecularGlossiness'], lib); } else { lib.materials[idx] = this._pbrMetallicRoughnessToStandard(materialInfo, materialInfo.pbrMetallicRoughness || {}, lib); } }, this); }, _parseMeshes: function (json, lib) { var self = this; util.each(json.meshes, function (meshInfo, idx) { lib.meshes[idx] = []; // Geometry for (var pp = 0; pp < meshInfo.primitives.length; pp++) { var primitiveInfo = meshInfo.primitives[pp]; var geometry = new Geometry({ dynamic: false, // PENDIGN name: meshInfo.name, boundingBox: new BoundingBox() }); // Parse attributes var semantics = Object.keys(primitiveInfo.attributes); for (var ss = 0; ss < semantics.length; ss++) { var semantic = semantics[ss]; var accessorIdx = primitiveInfo.attributes[semantic]; var attributeInfo = json.accessors[accessorIdx]; var attributeName = semanticAttributeMap[semantic]; if (!attributeName) { continue; } var size = SIZE_MAP[attributeInfo.type]; var attributeArray = getAccessorData(json, lib, accessorIdx); // WebGL attribute buffer not support uint32. // Direct use Float32Array may also have issue. if (attributeArray instanceof vendor.Uint32Array) { attributeArray = new Float32Array(attributeArray); } if (semantic === 'WEIGHTS_0' && size === 4) { // Weight data in QTEK has only 3 component, the last component can be evaluated since it is normalized var weightArray = new attributeArray.constructor(attributeInfo.count * 3); for (var i = 0; i < attributeInfo.count; i++) { var i4 = i * 4, i3 = i * 3; var w1 = attributeArray[i4], w2 = attributeArray[i4 + 1], w3 = attributeArray[i4 + 2], w4 = attributeArray[i4 + 3]; var wSum = w1 + w2 + w3 + w4; weightArray[i3] = w1 / wSum; weightArray[i3 + 1] = w2 / wSum; weightArray[i3 + 2] = w3 / wSum; } geometry.attributes[attributeName].value = weightArray; } else if (semantic === 'COLOR_0' && size === 3) { var colorArray = new attributeArray.constructor(attributeInfo.count * 4); for (var i = 0; i < attributeInfo.count; i++) { var i4 = i * 4, i3 = i * 3; colorArray[i4] = attributeArray[i3]; colorArray[i4 + 1] = attributeArray[i3 + 1]; colorArray[i4 + 2] = attributeArray[i3 + 2]; colorArray[i4 + 3] = 1; } geometry.attributes[attributeName].value = colorArray; } else { geometry.attributes[attributeName].value = attributeArray; } var attributeType = 'float'; if (attributeArray instanceof vendor.Uint16Array) { attributeType = 'ushort'; } else if (attributeArray instanceof vendor.Int16Array) { attributeType = 'short'; } else if (attributeArray instanceof vendor.Uint8Array) { attributeType = 'ubyte'; } else if (attributeArray instanceof vendor.Int8Array) { attributeType = 'byte'; } geometry.attributes[attributeName].type = attributeType; if (semantic === 'POSITION') { // Bounding Box var min = attributeInfo.min; var max = attributeInfo.max; if (min) { geometry.boundingBox.min.set(min[0], min[1], min[2]); } if (max) { geometry.boundingBox.max.set(max[0], max[1], max[2]); } } } // Parse indices if (primitiveInfo.indices != null) { geometry.indices = getAccessorData(json, lib, primitiveInfo.indices, true); if (geometry.vertexCount <= 0xffff && geometry.indices instanceof vendor.Uint32Array) { geometry.indices = new vendor.Uint16Array(geometry.indices); } if(geometry.indices instanceof vendor.Uint8Array) { geometry.indices = new vendor.Uint16Array(geometry.indices); } } var material = lib.materials[primitiveInfo.material]; var materialInfo = (json.materials || [])[primitiveInfo.material]; // Use default material if (!material) { material = new Material({ shader: self._getShader() }); } var mesh = new Mesh({ geometry: geometry, material: material, mode: [Mesh.POINTS, Mesh.LINES, Mesh.LINE_LOOP, Mesh.LINE_STRIP, Mesh.TRIANGLES, Mesh.TRIANGLE_STRIP, Mesh.TRIANGLE_FAN][primitiveInfo.mode] || Mesh.TRIANGLES, ignoreGBuffer: material.transparent }); if (materialInfo != null) { mesh.culling = !materialInfo.doubleSided; } if (!mesh.geometry.attributes.normal.value) { mesh.geometry.generateVertexNormals(); } if (((material instanceof StandardMaterial) && material.normalMap) || (material.isTextureEnabled('normalMap')) ) { if (!mesh.geometry.attributes.tangent.value) { mesh.geometry.generateTangents(); } } if (mesh.geometry.attributes.color.value) { mesh.material.define('VERTEX_COLOR'); } mesh.name = GLTFLoader.generateMeshName(json.meshes, idx, pp); lib.meshes[idx].push(mesh); } }, this); }, _instanceCamera: function (json, nodeInfo) { var cameraInfo = json.cameras[nodeInfo.camera]; if (cameraInfo.type === 'perspective') { var perspectiveInfo = cameraInfo.perspective || {}; return new PerspectiveCamera({ name: nodeInfo.name, aspect: perspectiveInfo.aspectRatio, fov: perspectiveInfo.yfov / Math.PI * 180, far: perspectiveInfo.zfar, near: perspectiveInfo.znear }); } else { var orthographicInfo = cameraInfo.orthographic || {}; return new OrthographicCamera({ name: nodeInfo.name, top: orthographicInfo.ymag, right: orthographicInfo.xmag, left: -orthographicInfo.xmag, bottom: -orthographicInfo.ymag, near: orthographicInfo.znear, far: orthographicInfo.zfar }); } }, _parseNodes: function (json, lib) { function instanceMesh(mesh) { return new Mesh({ name: mesh.name, geometry: mesh.geometry, material: mesh.material, culling: mesh.culling, mode: mesh.mode }); } lib.instancedMeshes = []; util.each(json.nodes, function (nodeInfo, idx) { var node; if (nodeInfo.camera != null && this.includeCamera) { node = this._instanceCamera(json, nodeInfo); lib.cameras.push(node); } else if (nodeInfo.mesh != null && this.includeMesh) { var primitives = lib.meshes[nodeInfo.mesh]; if (primitives) { if (primitives.length === 1) { // Replace the node with mesh directly node = instanceMesh(primitives[0]); node.setName(nodeInfo.name); lib.instancedMeshes.push(node); } else { node = new Node(); node.setName(nodeInfo.name); for (var j = 0; j < primitives.length; j++) { var newMesh = instanceMesh(primitives[j]); node.add(newMesh); lib.instancedMeshes.push(newMesh); } } } } else { node = new Node(); // PENDING Dulplicate name. node.setName(nodeInfo.name); } if (nodeInfo.matrix) { node.localTransform.setArray(nodeInfo.matrix); node.decomposeLocalTransform(); } else { if (nodeInfo.translation) { node.position.setArray(nodeInfo.translation); } if (nodeInfo.rotation) { node.rotation.setArray(nodeInfo.rotation); } if (nodeInfo.scale) { node.scale.setArray(nodeInfo.scale); } } lib.nodes[idx] = node; }, this); // Build hierarchy util.each(json.nodes, function (nodeInfo, idx) { var node = lib.nodes[idx]; if (nodeInfo.children) { for (var i = 0; i < nodeInfo.children.length; i++) { var childIdx = nodeInfo.children[i]; var child = lib.nodes[childIdx]; node.add(child); } } }); }, _parseAnimations: function (json, lib) { function checkChannelPath(channelInfo) { if (channelInfo.path === 'weights') { console.warn('GLTFLoader not support morph targets yet.'); return false; } return true; } function getChannelHash(channelInfo, animationInfo) { return channelInfo.target.node + '_' + animationInfo.samplers[channelInfo.sampler].input; } var timeAccessorMultiplied = {}; util.each(json.animations, function (animationInfo, idx) { var channels = animationInfo.channels.filter(checkChannelPath); if (!channels.length) { return; } var tracks = {}; for (var i = 0; i < channels.length; i++) { var channelInfo = channels[i]; var channelHash = getChannelHash(channelInfo, animationInfo); var targetNode = lib.nodes[channelInfo.target.node]; var track = tracks[channelHash]; var samplerInfo = animationInfo.samplers[channelInfo.sampler]; if (!track) { track = tracks[channelHash] = new SamplerTrack({ name: targetNode ? targetNode.name : '', target: targetNode }); track.targetNodeIndex = channelInfo.target.node; track.channels.time = getAccessorData(json, lib, samplerInfo.input); var frameLen = track.channels.time.length; if (!timeAccessorMultiplied[samplerInfo.input]) { for (var k = 0; k < frameLen; k++) { track.channels.time[k] *= 1000; } timeAccessorMultiplied[samplerInfo.input] = true; } } var interpolation = samplerInfo.interpolation || 'LINEAR'; if (interpolation !== 'LINEAR') { console.warn('GLTFLoader only support LINEAR interpolation.'); } var path = channelInfo.target.path; if (path === 'translation') { path = 'position'; } track.channels[path] = getAccessorData(json, lib, samplerInfo.output); } var tracksList = []; for (var hash in tracks) { tracksList.push(tracks[hash]); } var clip = new TrackClip({ name: animationInfo.name, loop: true, tracks: tracksList }); lib.clips.push(clip); }, this); // PENDING var maxLife = lib.clips.reduce(function (maxTime, clip) { return Math.max(maxTime, clip.life); }, 0); lib.clips.forEach(function (clip) { clip.life = maxLife; }); return lib.clips; } }); GLTFLoader.generateMeshName = function (meshes, idx, primitiveIdx) { var meshInfo = meshes[idx]; var meshName = meshInfo.name || ('mesh_' + idx); return primitiveIdx === 0 ? meshName : (meshName + '$' + primitiveIdx); }; export default GLTFLoader;