diff --git a/package.json b/package.json index 215cee49a9b31300f977579f29c697e42658e83f..b952d3a7ee378bc854b2e2adf13a28d9f3906727 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "jsonschema": "^1.2.2", "material-design-lite": "^1.3.0", "minijanus": "^0.4.0", - "naf-janus-adapter": "^0.3.0", + "naf-janus-adapter": "^0.4.0", "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master", "nipplejs": "^0.6.7", "query-string": "^5.0.1", diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000000000000000000000000000000000000..c542f439f903e0a4426d087cb33d69852b25944d --- /dev/null +++ b/src/App.js @@ -0,0 +1,21 @@ +export class App { + constructor() { + this.scene = null; + this.quality = "low"; + } + + setQuality(quality) { + if (this.quality === quality) { + return false; + } + + this.quality = quality; + + if (this.scene) { + console.log("quality-changed", quality); + this.scene.dispatchEvent(new CustomEvent("quality-changed", { detail: quality })); + } + + return true; + } +} diff --git a/src/assets/avatars/BotDefault_Avatar.glb b/src/assets/avatars/BotDefault_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..ccb77bf21362f3a5b2058f59eafd56fa17a55ec4 Binary files /dev/null and b/src/assets/avatars/BotDefault_Avatar.glb differ diff --git a/src/assets/avatars/BotDefault_Avatar_Unlit.glb b/src/assets/avatars/BotDefault_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..ba33589d4c42fe6bf9756d5164f331e9830dab91 Binary files /dev/null and b/src/assets/avatars/BotDefault_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/Bot_SkinnedWithAnim.glb b/src/assets/avatars/Bot_SkinnedWithAnim.glb deleted file mode 100644 index 192bbed19788cb4c5cb71a3f23da428173766948..0000000000000000000000000000000000000000 Binary files a/src/assets/avatars/Bot_SkinnedWithAnim.glb and /dev/null differ diff --git a/src/assets/environments/CliffVista_mesh.glb b/src/assets/environments/CliffVista_mesh.glb index 0bfecdc65832c99b903d4f3bc40bf207bf3a867f..fbd377865a799bb52240155dbe34bf49d699bd97 100644 Binary files a/src/assets/environments/CliffVista_mesh.glb and b/src/assets/environments/CliffVista_mesh.glb differ diff --git a/src/assets/environments/MeetingSpace1_mesh.glb b/src/assets/environments/MeetingSpace1_mesh.glb index 431a21b5a3e034e3866fc6a6e480580375e35c46..e184d5800b35daa15754ba88db845144bcd53309 100644 Binary files a/src/assets/environments/MeetingSpace1_mesh.glb and b/src/assets/environments/MeetingSpace1_mesh.glb differ diff --git a/src/assets/environments/OutdoorFacade_mesh.glb b/src/assets/environments/OutdoorFacade_mesh.glb index 4a24a0667ab9f13a57b752d8b5ab1b1bbcd6599d..05ceb7b09786450daf59833ca10c20fa214ac362 100644 Binary files a/src/assets/environments/OutdoorFacade_mesh.glb and b/src/assets/environments/OutdoorFacade_mesh.glb differ diff --git a/src/assets/hud/watch.bin b/src/assets/hud/watch.bin deleted file mode 100644 index c90e4ecbe70f2623bfab3bcbf75dbee0e96a4144..0000000000000000000000000000000000000000 Binary files a/src/assets/hud/watch.bin and /dev/null differ diff --git a/src/assets/hud/watch.glb b/src/assets/hud/watch.glb index 4d6d9ed1673bc650558091c521edf13b086073a9..306fcfd7414d24584ecbf59aa3942318b2f33211 100644 Binary files a/src/assets/hud/watch.glb and b/src/assets/hud/watch.glb differ diff --git a/src/assets/hud/watch.gltf b/src/assets/hud/watch.gltf deleted file mode 100644 index d7fb78d83fa60dab9c23749699f814f344bbb232..0000000000000000000000000000000000000000 --- a/src/assets/hud/watch.gltf +++ /dev/null @@ -1,139 +0,0 @@ -{ - "accessors" : [ - { - "bufferView" : 0, - "componentType" : 5121, - "count" : 204, - "max" : [ - 119 - ], - "min" : [ - 0 - ], - "type" : "SCALAR" - }, - { - "bufferView" : 1, - "componentType" : 5126, - "count" : 120, - "max" : [ - 0.10501318424940109, - 0.04724307730793953, - 0.10020249336957932 - ], - "min" : [ - -0.10501328855752945, - 0.01861731894314289, - -0.10663323104381561 - ], - "type" : "VEC3" - }, - { - "bufferView" : 2, - "componentType" : 5126, - "count" : 120, - "max" : [ - 0.9848077297210693, - 1.0, - 1.0 - ], - "min" : [ - -0.9848077297210693, - -1.0, - -0.9396926164627075 - ], - "type" : "VEC3" - } - ], - "asset" : { - "generator" : "Khronos Blender glTF 2.0 exporter", - "version" : "2.0" - }, - "bufferViews" : [ - { - "buffer" : 0, - "byteLength" : 204, - "byteOffset" : 0, - "target" : 34963 - }, - { - "buffer" : 0, - "byteLength" : 1440, - "byteOffset" : 204, - "target" : 34962 - }, - { - "buffer" : 0, - "byteLength" : 1440, - "byteOffset" : 1644, - "target" : 34962 - } - ], - "buffers" : [ - { - "byteLength" : 3084, - "uri" : "watch.bin" - } - ], - "materials" : [ - { - "name" : "Material.001", - "emissiveFactor": [ - 0.6400000190734865, - 0.6400000190734865, - 0.6400000190734865 - ], - "pbrMetallicRoughness" : { - "baseColorFactor" : [ - 0.6400000190734865, - 0.6400000190734865, - 0.6400000190734865, - 1.0 - ], - "metallicFactor" : 0.0, - "roughnessFactor": 0 - }, - "extensions": { - "KHR_materials_cmnConstant": {} - } - } - ], - "meshes" : [ - { - "name" : "Cylinder.001", - "primitives" : [ - { - "attributes" : { - "NORMAL" : 2, - "POSITION" : 1 - }, - "indices" : 0, - "material" : 0 - } - ] - } - ], - "nodes" : [ - { - "mesh" : 0, - "name" : "Watch", - "scale" : [ - 0.6932165622711182, - 0.6932165622711182, - 0.6932165622711182 - ] - } - ], - "scene" : 0, - "scenes" : [ - { - "name" : "Scene", - "nodes" : [ - 0 - ] - } - ], - "extensionsUsed": [ - "KHR_materials_cmnConstant" - ] -} diff --git a/src/components/hide-when-quality.js b/src/components/hide-when-quality.js new file mode 100644 index 0000000000000000000000000000000000000000..e09d3ae90db3ce48e7afb815b5e0fcf22aa79fc3 --- /dev/null +++ b/src/components/hide-when-quality.js @@ -0,0 +1,23 @@ +AFRAME.registerComponent("hide-when-quality", { + schema: { type: "string", default: "low" }, + + init() { + this.onQualityChanged = this.onQualityChanged.bind(this); + this.el.sceneEl.addEventListener("quality-changed", this.onQualityChanged); + }, + + onQualityChanged(event) { + this.updateComponentState(event.detail); + }, + + update(oldData) { + if (this.data !== oldData) { + this.updateComponentState(window.APP.quality); + } + }, + + updateComponentState(quality) { + console.log(quality); + this.el.setAttribute("visible", quality !== this.data); + } +}); diff --git a/src/components/networked-video-player.css b/src/components/networked-video-player.css index e6859395801b79bddc37c85c53a92e6a1780c8ec..00dd3a89730b18ba210d2b981c0929399c3f1902 100644 --- a/src/components/networked-video-player.css +++ b/src/components/networked-video-player.css @@ -1,12 +1,11 @@ :local(.video) { - height: 100px; + /* 1x1px so that Safari on iOS allows us to autoplay the video */ + width: 1px; height: 1px; /* toggle to show debug video elements */ background: black; - margin: 5px; } :local(.container) { position: absolute; bottom: 0; - display: flex; - visibility: hidden; /* toggle to show debug video elements */ + width: 10px; height: 10px; /* toggle to show debug video elements */ } diff --git a/src/components/networked-video-player.js b/src/components/networked-video-player.js index 6e0218a9f0ace62a506be85d56036b17c00bfdb6..03cfba4f6d7a835ac825804724a4109d16690cdb 100644 --- a/src/components/networked-video-player.js +++ b/src/components/networked-video-player.js @@ -2,9 +2,7 @@ import styles from "./networked-video-player.css"; const nafConnected = function() { return new Promise(resolve => { - NAF.clientId - ? resolve() - : document.body.addEventListener("connected", resolve); + NAF.clientId ? resolve() : document.body.addEventListener("connected", resolve); }); }; @@ -21,21 +19,23 @@ AFRAME.registerComponent("networked-video-player", { await nafConnected(); - const networkedEl = NAF.utils.getNetworkedEntity(this.el); + const networkedEl = await NAF.utils.getNetworkedEntity(this.el); if (!networkedEl) { - throw new Error( - "Video player must be added on a node, or a child of a node, with the `networked` component." - ); + throw new Error("Video player must be added on a node, or a child of a node, with the `networked` component."); } const ownerId = networkedEl.components.networked.data.owner; - const stream = await NAF.connection.adapter.getMediaStream(ownerId); + const stream = await NAF.connection.adapter.getMediaStream(ownerId, "video"); if (!stream) { return; } const v = document.createElement("video"); v.id = `nvp-video-${ownerId}`; + // muted and autoplay so that more restrictive browsers (e.g. Safari on iOS) will actually play the video. + v.muted = true; + v.autoplay = true; + v.playsInline = true; v.classList.add(styles.video); v.srcObject = new MediaStream(stream.getVideoTracks()); // We only want the video track so make a new MediaStream container.appendChild(v); diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js index ee59e3a24ead224a4095c69c70d2ea0da1c9cbd5..7caec327629a2d4059f0254a5a0438afe712b46b 100644 --- a/src/elements/a-gltf-entity.js +++ b/src/elements/a-gltf-entity.js @@ -1,5 +1,24 @@ const GLTFCache = {}; +AFRAME.AGLTFEntity = { + defaultInflator(el, componentName, componentData) { + if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) { + for (let i = 0; i < componentData.length; i++) { + el.setAttribute(componentName + "__" + i, componentData[i]); + } + } else { + el.setAttribute(componentName, componentData); + } + }, + registerComponent(componentKey, componentName, inflator) { + AFRAME.AGLTFEntity.components[componentKey] = { + inflator: inflator || AFRAME.AGLTFEntity.defaultInflator, + componentName + }; + }, + components: {} +}; + // From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87 // Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573 function cloneGltf(gltf) { @@ -83,6 +102,20 @@ const inflateEntities = function(classPrefix, parentEl, node) { el.setObject3D(node.type.toLowerCase(), node); + const entityComponents = node.userData.components; + + if (entityComponents) { + for (const prop in entityComponents) { + if (entityComponents.hasOwnProperty(prop)) { + const { inflator, componentName } = AFRAME.AGLTFEntity.components[prop]; + + if (inflator) { + inflator(el, componentName, entityComponents[prop]); + } + } + } + } + children.forEach(childNode => { inflateEntities(classPrefix, el, childNode); }); @@ -121,7 +154,18 @@ AFRAME.registerElement("a-gltf-entity", { // If the src attribute is a selector, get the url from the asset item. if (src.charAt(0) === "#") { const assetEl = document.getElementById(src.substring(1)); - src = assetEl.getAttribute("src"); + + const fallbackSrc = assetEl.getAttribute("src"); + const highSrc = assetEl.getAttribute("high-src"); + const lowSrc = assetEl.getAttribute("low-src"); + + if (highSrc && window.APP.quality === "high") { + src = highSrc; + } else if (lowSrc && window.APP.quality === "low") { + src = lowSrc; + } else { + src = fallbackSrc; + } } const onLoad = gltfModel => { diff --git a/src/elements/a-progressive-asset.js b/src/elements/a-progressive-asset.js new file mode 100644 index 0000000000000000000000000000000000000000..a367e374aece30163bc73802ab8cf11950f28c1a --- /dev/null +++ b/src/elements/a-progressive-asset.js @@ -0,0 +1,67 @@ +/** + * Modified version of a-asset-item that adds high-src and low-src options + * Extracted from https://github.com/aframevr/aframe/blob/master/src/core/a-assets.js + */ + +AFRAME.registerElement("a-progressive-asset", { + prototype: Object.create(AFRAME.ANode.prototype, { + createdCallback: { + value() { + this.data = null; + this.isAssetItem = true; + + if (!this.parentNode.fileLoader) { + throw new Error("a-progressive-asset must be the child of an a-assets element."); + } + + this.fileLoader = this.parentNode.fileLoader; + } + }, + + attachedCallback: { + value() { + const self = this; + const fallbackSrc = this.getAttribute("src"); + const highSrc = this.getAttribute("high-src"); + const lowSrc = this.getAttribute("low-src"); + + let src = fallbackSrc; + + if (window.APP.quality === "high") { + src = highSrc; + } else if (window.APP.quality === "low") { + src = lowSrc; + } + + this.fileLoader.setResponseType(this.getAttribute("response-type") || inferResponseType(src)); + this.fileLoader.load( + src, + function handleOnLoad(response) { + self.data = response; + /* + Workaround for a Chrome bug. If another XHR is sent to the same url before the + previous one closes, the second request never finishes. + setTimeout finishes the first request and lets the logic triggered by load open + subsequent requests. + setTimeout can be removed once the fix for the bug below ships: + https://bugs.chromium.org/p/chromium/issues/detail?id=633696&q=component%3ABlink%3ENetwork%3EXHR%20&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified + */ + setTimeout(function load() { + AFRAME.ANode.prototype.load.call(self); + }); + }, + function handleOnProgress(xhr) { + self.emit("progress", { + loadedBytes: xhr.loaded, + totalBytes: xhr.total, + xhr: xhr + }); + }, + function handleOnError(xhr) { + self.emit("error", { xhr: xhr }); + } + ); + } + } + }) +}); diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js new file mode 100644 index 0000000000000000000000000000000000000000..e73692a8ff9270985f3b533ff8bd1f8fdc0cb835 --- /dev/null +++ b/src/gltf-component-mappings.js @@ -0,0 +1,3 @@ +import "./elements/a-gltf-entity"; + +AFRAME.AGLTFEntity.registerComponent("scale-audio-feedback", "scale-audio-feedback"); diff --git a/src/room.html b/src/room.html index 85f370c9764e09124e9e7eab16e87799ea5ebed9..8ef44dfb116b36ee8de5c6a0f320774f5faab60c 100644 --- a/src/room.html +++ b/src/room.html @@ -7,9 +7,9 @@ <meta http-equiv="origin-trial" data-feature="WebVR (For Chrome M62+)" data-expires="<%= ORIGIN_TRIAL_EXPIRES %>" content="<%= ORIGIN_TRIAL_TOKEN %>"> <% if(NODE_ENV === "production") { %> - <script src="https://cdn.rawgit.com/brianpeiris/aframe/bba200440e3279753df85a1f52ba4c77a3b16e47/dist/aframe-master.min.js"></script> + <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script> <% } else { %> - <script src="https://cdn.rawgit.com/brianpeiris/aframe/bba200440e3279753df85a1f52ba4c77a3b16e47/dist/aframe-master.js"></script> + <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.js"></script> <% } %> </head> @@ -28,7 +28,14 @@ light="defaultLightsEnabled: false"> <a-assets> - <a-asset-item id="bot-skinned-mesh" response-type="arraybuffer" src="./assets/avatars/Bot_SkinnedWithAnim.glb"></a-asset-item> + <a-progressive-asset + id="bot-skinned-mesh" + response-type="arraybuffer" + src="./assets/avatars/BotDefault_Avatar_Unlit.glb" + high-src="./assets/avatars/BotDefault_Avatar.glb" + low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item> <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="./assets/environments/MeetingSpace1_mesh.glb"></a-asset-item> @@ -44,10 +51,10 @@ <a-entity class="video" geometry="primitive: plane;" material="side: double" networked-video-player></a-entity> </template> - <template id="remote-avatar-template"> + <template id="remote-avatar-template"> <a-entity ik-root> <a-entity class="camera"></a-entity> - + <a-entity class="left-controller"></a-entity> <a-entity class="right-controller"></a-entity> @@ -64,14 +71,12 @@ ></a-entity> </a-entity> </template> - + <template data-selector=".Head"> <a-entity networked-audio-source networked-audio-analyser - scale-audio-feedback personal-space-invader - animation-mixer > </a-entity> </template> @@ -106,7 +111,7 @@ personal-space-bubble look-controls ></a-entity> - + <a-entity id="player-left-controller" class="left-controller" @@ -151,6 +156,13 @@ </a-gltf-entity> </a-entity> + <!-- Lights --> + <a-entity + hide-when-quality="low" + light="type: directional; color: #F9FFCE; intensity: 0.6" + position="0.002 5.231 -15.3" + ></a-entity> + <!-- Environment --> <a-gltf-entity id="meeting-space" diff --git a/src/room.js b/src/room.js index 218783a060662b9b4f9c99b784f8fcdca6995e4e..afe163d273ff9825a2f596188cfa5dbf6b8f9008 100644 --- a/src/room.js +++ b/src/room.js @@ -38,6 +38,7 @@ import "./components/water"; import "./components/skybox"; import "./components/layers"; import "./components/spawn-controller"; +import "./components/hide-when-quality"; import ReactDOM from "react-dom"; import React from "react"; @@ -45,7 +46,22 @@ import UIRoot from "./react-components/ui-root"; import "./systems/personal-space-bubble"; -import "./elements/a-gltf-entity"; +import "./gltf-component-mappings"; + +import { App } from "./App"; + +window.APP = new App(); + +const qs = queryString.parse(location.search); +const isMobile = AFRAME.utils.device.isMobile(); + +if (qs.quality) { + window.APP.quality = qs.quality; +} else { + window.APP.quality = isMobile ? "low" : "high"; +} + +import "./elements/a-progressive-asset"; import registerNetworkSchemas from "./network-schemas"; import { inGameActions, config } from "./input-mappings"; @@ -70,6 +86,33 @@ const store = new Store(); // Always layer in any new default profile bits store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) }}) +async function shareMedia(audio, video) { + const constraints = { + audio: !!audio, + video: video ? { mediaSource: "screen", height: 720, frameRate: 30 } : false + }; + const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + NAF.connection.adapter.setLocalMediaStream(mediaStream); + + const id = `${NAF.clientId}-screen`; + let entity = document.getElementById(id); + if (entity) { + entity.setAttribute("visible", !!video); + } else if (video) { + const sceneEl = document.querySelector("a-scene"); + entity = document.createElement("a-entity"); + entity.id = id; + entity.setAttribute("offset-relative-to", { + target: "#player-camera", + offset: "0 0 -2", + on: "action_share_screen" + }); + entity.setAttribute("networked", { template: "#video-template" }); + sceneEl.appendChild(entity); + } +} + + async function enterScene(mediaStream) { const qs = queryString.parse(location.search); const scene = document.querySelector("a-scene"); @@ -83,7 +126,7 @@ async function enterScene(mediaStream) { scene.setAttribute("stats", true); } - if (AFRAME.utils.device.isMobile() || qs.gamepad) { + if (isMobile || qs.mobile) { const playerRig = document.querySelector("#player-rig"); playerRig.setAttribute("virtual-gamepad-controls", {}); } @@ -148,4 +191,9 @@ function mountUI() { }); } -document.addEventListener("DOMContentLoaded", () => mountUI()); +document.addEventListener("DOMContentLoaded", () => { + const scene = document.querySelector("a-scene"); + window.APP.scene = scene; + + mountUI(); +}); diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js index 90afbfd648c059f98aaa547c31fee06dbe100735..0992a18dba1a474bed8999035e6c5e1376b568b3 100644 --- a/src/vendor/GLTFLoader.js +++ b/src/vendor/GLTFLoader.js @@ -1,3 +1,5 @@ +// https://github.com/mrdoob/three.js/blob/1e943ba79196737bc8505522e928595687c09425/examples/js/loaders/GLTFLoader.js + /** * @author Rich Tibbett / https://github.com/richtr * @author mrdoob / http://mrdoob.com/ @@ -11,6 +13,7 @@ THREE.GLTFLoader = ( function () { function GLTFLoader( manager ) { this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; + this.dracoLoader = null; } @@ -24,7 +27,7 @@ THREE.GLTFLoader = ( function () { var scope = this; - var path = this.path !== undefined ? this.path : THREE.Loader.prototype.extractUrlBase( url ); + var path = this.path !== undefined ? this.path : THREE.LoaderUtils.extractUrlBase( url ); var loader = new THREE.FileLoader( scope.manager ); @@ -57,12 +60,21 @@ THREE.GLTFLoader = ( function () { setCrossOrigin: function ( value ) { this.crossOrigin = value; + return this; }, setPath: function ( value ) { this.path = value; + return this; + + }, + + setDRACOLoader: function ( dracoLoader ) { + + this.dracoLoader = dracoLoader; + return this; }, @@ -77,7 +89,7 @@ THREE.GLTFLoader = ( function () { } else { - var magic = convertUint8ArrayToString( new Uint8Array( data, 0, 4 ) ); + var magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) ); if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { @@ -96,7 +108,7 @@ THREE.GLTFLoader = ( function () { } else { - content = convertUint8ArrayToString( new Uint8Array( data ) ); + content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) ); } @@ -106,7 +118,7 @@ THREE.GLTFLoader = ( function () { if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { - if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) ); + if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.' ) ); return; } @@ -119,9 +131,9 @@ THREE.GLTFLoader = ( function () { } - if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_MATERIALS_COMMON ) >= 0 ) { + if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_MATERIALS_UNLIT ) >= 0 ) { - extensions[ EXTENSIONS.KHR_MATERIALS_COMMON ] = new GLTFMaterialsCommonExtension( json ); + extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] = new GLTFMaterialsUnlitExtension( json ); } @@ -131,6 +143,12 @@ THREE.GLTFLoader = ( function () { } + if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ) >= 0 ) { + + extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] = new GLTFDracoMeshCompressionExtension( this.dracoLoader ); + + } + } console.time( 'GLTFLoader' ); @@ -143,7 +161,7 @@ THREE.GLTFLoader = ( function () { } ); - parser.parse( function ( scene, scenes, cameras, animations ) { + parser.parse( function ( scene, scenes, cameras, animations, asset ) { console.timeEnd( 'GLTFLoader' ); @@ -151,7 +169,8 @@ THREE.GLTFLoader = ( function () { scene: scene, scenes: scenes, cameras: cameras, - animations: animations + animations: animations, + asset: asset }; onLoad( glTF ); @@ -204,10 +223,10 @@ THREE.GLTFLoader = ( function () { var EXTENSIONS = { KHR_BINARY_GLTF: 'KHR_binary_glTF', + KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', KHR_LIGHTS: 'KHR_lights', - KHR_MATERIALS_COMMON: 'KHR_materials_common', KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', - KHR_MATERIALS_CMN_CONSTANT: 'KHR_materials_cmnConstant' + KHR_MATERIALS_UNLIT: 'KHR_materials_unlit' }; /** @@ -295,99 +314,47 @@ THREE.GLTFLoader = ( function () { } /** - * Common Materials Extension + * Unlit Materials Extension (pending) * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_common + * PR: https://github.com/KhronosGroup/glTF/pull/1163 */ - function GLTFMaterialsCommonExtension( json ) { + function GLTFMaterialsUnlitExtension( json ) { - this.name = EXTENSIONS.KHR_MATERIALS_COMMON; + this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; } - GLTFMaterialsCommonExtension.prototype.getMaterialType = function ( material ) { - - var khrMaterial = material.extensions[ this.name ]; - - switch ( khrMaterial.type ) { - - case 'commonBlinn' : - case 'commonPhong' : - return THREE.MeshPhongMaterial; + GLTFMaterialsUnlitExtension.prototype.getMaterialType = function ( material ) { - case 'commonLambert' : - return THREE.MeshLambertMaterial; - - case 'commonConstant' : - default : - return THREE.MeshBasicMaterial; - - } + return THREE.MeshBasicMaterial; }; - GLTFMaterialsCommonExtension.prototype.extendParams = function ( materialParams, material, parser ) { - - var khrMaterial = material.extensions[ this.name ]; + GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, material, parser ) { var pending = []; - var keys = []; - - // TODO: Currently ignored: 'ambientFactor', 'ambientTexture' - switch ( khrMaterial.type ) { - - case 'commonBlinn' : - case 'commonPhong' : - keys.push( 'diffuseFactor', 'diffuseTexture', 'specularFactor', 'specularTexture', 'shininessFactor' ); - break; - - case 'commonLambert' : - keys.push( 'diffuseFactor', 'diffuseTexture' ); - break; - - case 'commonConstant' : - default : - break; - - } - - var materialValues = {}; - - keys.forEach( function ( v ) { - - if ( khrMaterial[ v ] !== undefined ) materialValues[ v ] = khrMaterial[ v ]; - - } ); - - if ( materialValues.diffuseFactor !== undefined ) { - - materialParams.color = new THREE.Color().fromArray( materialValues.diffuseFactor ); - materialParams.opacity = materialValues.diffuseFactor[ 3 ]; - - } - - if ( materialValues.diffuseTexture !== undefined ) { + materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; - pending.push( parser.assignTexture( materialParams, 'map', materialValues.diffuseTexture.index ) ); + var metallicRoughness = material.pbrMetallicRoughness; - } - - if ( materialValues.specularFactor !== undefined ) { + if ( metallicRoughness ) { - materialParams.specular = new THREE.Color().fromArray( materialValues.specularFactor ); + if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { - } + var array = metallicRoughness.baseColorFactor; - if ( materialValues.specularTexture !== undefined ) { + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; - pending.push( parser.assignTexture( materialParams, 'specularMap', materialValues.specularTexture.index ) ); + } - } + if ( metallicRoughness.baseColorTexture !== undefined ) { - if ( materialValues.shininessFactor !== undefined ) { + pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) ); - materialParams.shininess = materialValues.shininessFactor; + } } @@ -411,7 +378,7 @@ THREE.GLTFLoader = ( function () { var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); this.header = { - magic: convertUint8ArrayToString( new Uint8Array( data.slice( 0, 4 ) ) ), + magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), version: headerView.getUint32( 4, true ), length: headerView.getUint32( 8, true ) }; @@ -422,7 +389,7 @@ THREE.GLTFLoader = ( function () { } else if ( this.header.version < 2.0 ) { - throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use GLTFLoader instead.' ); + throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.' ); } @@ -440,7 +407,7 @@ THREE.GLTFLoader = ( function () { if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); - this.content = convertUint8ArrayToString( contentArray ); + this.content = THREE.LoaderUtils.decodeText( contentArray ); } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { @@ -463,10 +430,55 @@ THREE.GLTFLoader = ( function () { } + /** + * DRACO Mesh Compression Extension + * + * Specification: https://github.com/KhronosGroup/glTF/pull/874 + */ + function GLTFDracoMeshCompressionExtension ( dracoLoader ) { + + if ( ! dracoLoader ) { + + throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); + + } + + this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; + this.dracoLoader = dracoLoader; + + } + + GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) { + + var dracoLoader = this.dracoLoader; + var bufferViewIndex = primitive.extensions[ this.name ].bufferView; + var gltfAttributeMap = primitive.extensions[ this.name ].attributes; + var threeAttributeMap = {}; + + for ( var attributeName in gltfAttributeMap ) { + + if ( !( attributeName in ATTRIBUTES ) ) continue; + + threeAttributeMap[ ATTRIBUTES[ attributeName ] ] = gltfAttributeMap[ attributeName ]; + + } + + return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { + + return new Promise( function ( resolve ) { + + dracoLoader.decodeDracoFile( bufferView, resolve, threeAttributeMap ); + + } ); + + } ); + + }; + /** * Specular-Glossiness Extension * - * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_pbrSpecularGlossiness + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness */ function GLTFMaterialsPbrSpecularGlossinessExtension() { @@ -530,6 +542,7 @@ THREE.GLTFLoader = ( function () { 'vec3 specularFactor = specular;', '#ifdef USE_SPECULARMAP', ' vec4 texelSpecular = texture2D( specularMap, vUv );', + ' texelSpecular = sRGBToLinear( texelSpecular );', ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', ' specularFactor *= texelSpecular.rgb;', '#endif' @@ -698,7 +711,7 @@ THREE.GLTFLoader = ( function () { var params = this.specularGlossinessParams; - for ( var i = 0; i < params.length; i ++ ) { + for ( var i = 0, il = params.length; i < il; i ++ ) { target[ params[ i ] ] = source[ params[ i ] ]; @@ -711,6 +724,12 @@ THREE.GLTFLoader = ( function () { // Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer. refreshUniforms: function ( renderer, scene, camera, geometry, material, group ) { + if ( material.isGLTFSpecularGlossinessMaterial !== true ) { + + return; + + } + var uniforms = material.uniforms; var defines = material.defines; @@ -855,6 +874,61 @@ THREE.GLTFLoader = ( function () { } + /*********************************/ + /********** INTERPOLATION ********/ + /*********************************/ + + // Spline Interpolation + // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation + function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) { + + THREE.Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer ); + + }; + + GLTFCubicSplineInterpolant.prototype = Object.create( THREE.Interpolant.prototype ); + GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; + + GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { + + var result = this.resultBuffer; + var values = this.sampleValues; + var stride = this.valueSize; + + var stride2 = stride * 2; + var stride3 = stride * 3; + + var td = t1 - t0; + + var p = ( t - t0 ) / td; + var pp = p * p; + var ppp = pp * p; + + var offset1 = i1 * stride3; + var offset0 = offset1 - stride3; + + var s0 = 2 * ppp - 3 * pp + 1; + var s1 = ppp - 2 * pp + p; + var s2 = - 2 * ppp + 3 * pp; + var s3 = ppp - pp; + + // Layout of keyframe output values for CUBICSPLINE animations: + // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] + for ( var i = 0; i !== stride; i ++ ) { + + var p0 = values[ offset0 + i + stride ]; // splineVertex_k + var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) + var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 + var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) + + result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; + + } + + return result; + + }; + /*********************************/ /********** INTERNALS ************/ /*********************************/ @@ -985,6 +1059,22 @@ THREE.GLTFLoader = ( function () { 'MAT4': 16 }; + var ATTRIBUTES = { + POSITION: 'position', + NORMAL: 'normal', + TEXCOORD_0: 'uv', + TEXCOORD0: 'uv', // deprecated + TEXCOORD: 'uv', // deprecated + TEXCOORD_1: 'uv2', + COLOR_0: 'color', + COLOR0: 'color', // deprecated + COLOR: 'color', // deprecated + WEIGHTS_0: 'skinWeight', + WEIGHT: 'skinWeight', // deprecated + JOINTS_0: 'skinIndex', + JOINT: 'skinIndex' // deprecated + } + var PATH_PROPERTIES = { scale: 'scale', translation: 'position', @@ -993,8 +1083,10 @@ THREE.GLTFLoader = ( function () { }; var INTERPOLATION = { - CATMULLROMSPLINE: THREE.InterpolateSmooth, - CUBICSPLINE: THREE.InterpolateSmooth, + CUBICSPLINE: THREE.InterpolateSmooth, // We use custom interpolation GLTFCubicSplineInterpolation for CUBICSPLINE. + // KeyframeTrack.optimize() can't handle glTF Cubic Spline output values layout, + // using THREE.InterpolateSmooth for KeyframeTrack instantiation to prevent optimization. + // See KeyframeTrack.optimize() for the detail. LINEAR: THREE.InterpolateLinear, STEP: THREE.InterpolateDiscrete }; @@ -1016,148 +1108,25 @@ THREE.GLTFLoader = ( function () { /* UTILITY FUNCTIONS */ - function _each( object, callback, thisObj ) { - - if ( ! object ) { - - return Promise.resolve(); - - } - - var results; - var fns = []; - - if ( Object.prototype.toString.call( object ) === '[object Array]' ) { - - results = []; - - var length = object.length; - - for ( var idx = 0; idx < length; idx ++ ) { - - var value = callback.call( thisObj || this, object[ idx ], idx ); - - if ( value ) { - - if ( value instanceof Promise ) { - - value = value.then( function ( key, value ) { - - results[ key ] = value; - - }.bind( this, idx ) ); - - } else { - - results[ idx ] = value; - - } - - fns.push( value ); - - } - - } - - } else { - - results = {}; - - for ( var key in object ) { - - if ( object.hasOwnProperty( key ) ) { - - var value = callback.call( thisObj || this, object[ key ], key ); - - if ( value ) { - - if ( value instanceof Promise ) { - - value = value.then( function ( key, value ) { - - results[ key ] = value; - - }.bind( this, key ) ); - - } else { - - results[ key ] = value; - - } - - fns.push( value ); - - } - - } - - } - - } - - return Promise.all( fns ).then( function () { - - return results; - - } ); - - } - function resolveURL( url, path ) { // Invalid URL - if ( typeof url !== 'string' || url === '' ) - return ''; + if ( typeof url !== 'string' || url === '' ) return ''; // Absolute URL http://,https://,// - if ( /^(https?:)?\/\//i.test( url ) ) { - - return url; - - } + if ( /^(https?:)?\/\//i.test( url ) ) return url; // Data URI - if ( /^data:.*,.*$/i.test( url ) ) { - - return url; - - } + if ( /^data:.*,.*$/i.test( url ) ) return url; // Blob URL - if ( /^blob:.*$/i.test( url ) ) { - - return url; - - } + if ( /^blob:.*$/i.test( url ) ) return url; // Relative URL return path + url; } - function convertUint8ArrayToString( array ) { - - if ( window.TextDecoder !== undefined ) { - - return new TextDecoder().decode( array ); - - } - - // Avoid the String.fromCharCode.apply(null, array) shortcut, which - // throws a "maximum call stack size exceeded" error for large arrays. - - var s = ''; - - for ( var i = 0, il = array.length; i < il; i ++ ) { - - s += String.fromCharCode( array[ i ] ); - - } - - return s; - - } - /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material */ @@ -1178,14 +1147,12 @@ THREE.GLTFLoader = ( function () { /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets * - * TODO: Implement support for morph targets on TANGENT attribute. - * * @param {THREE.Mesh} mesh * @param {GLTF.Mesh} meshDef * @param {GLTF.Primitive} primitiveDef - * @param {Object} dependencies + * @param {Array<THREE.BufferAttribute>} accessors */ - function addMorphTargets( mesh, meshDef, primitiveDef, dependencies ) { + function addMorphTargets( mesh, meshDef, primitiveDef, accessors ) { var geometry = mesh.geometry; var material = mesh.material; @@ -1221,7 +1188,7 @@ THREE.GLTFLoader = ( function () { // So morphTarget value will depend on mesh's position, then cloning attribute // for the case if attribute is shared among two or more meshes. - positionAttribute = dependencies.accessors[ target.POSITION ].clone(); + positionAttribute = cloneBufferAttribute( accessors[ target.POSITION ] ); var position = geometry.attributes.position; for ( var j = 0, jl = positionAttribute.count; j < jl; j ++ ) { @@ -1239,7 +1206,7 @@ THREE.GLTFLoader = ( function () { // Copying the original position not to affect the final position. // See the formula above. - positionAttribute = geometry.attributes.position.clone(); + positionAttribute = cloneBufferAttribute( geometry.attributes.position ); } @@ -1256,7 +1223,7 @@ THREE.GLTFLoader = ( function () { // see target.POSITION's comment - normalAttribute = dependencies.accessors[ target.NORMAL ].clone(); + normalAttribute = cloneBufferAttribute( accessors[ target.NORMAL ] ); var normal = geometry.attributes.normal; for ( var j = 0, jl = normalAttribute.count; j < jl; j ++ ) { @@ -1272,7 +1239,7 @@ THREE.GLTFLoader = ( function () { } else if ( geometry.attributes.normal !== undefined ) { - normalAttribute = geometry.attributes.normal.clone(); + normalAttribute = cloneBufferAttribute( geometry.attributes.normal ); } @@ -1297,109 +1264,231 @@ THREE.GLTFLoader = ( function () { } + // .extras has user-defined data, so check that .extras.targetNames is an array. + if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { + + for ( var i = 0, il = meshDef.extras.targetNames.length; i < il; i ++ ) { + + mesh.morphTargetDictionary[ meshDef.extras.targetNames[ i ] ] = i; + + } + + } + } - /* GLTF PARSER */ + function isPrimitiveEqual( a, b ) { - function GLTFParser( json, extensions, options ) { + if ( a.indices !== b.indices ) { - this.json = json || {}; - this.extensions = extensions || {}; - this.options = options || {}; + return false; - // loader object cache - this.cache = new GLTFRegistry(); + } - this.textureLoader = new THREE.TextureLoader( this.options.manager ); - this.textureLoader.setCrossOrigin( this.options.crossOrigin ); + var attribA = a.attributes || {}; + var attribB = b.attributes || {}; + var keysA = Object.keys( attribA ); + var keysB = Object.keys( attribB ); - this.fileLoader = new THREE.FileLoader( this.options.manager ); - this.fileLoader.setResponseType( 'arraybuffer' ); + if ( keysA.length !== keysB.length ) { + + return false; + + } + + for ( var i = 0, il = keysA.length; i < il; i ++ ) { + + var key = keysA[ i ]; + + if ( attribA[ key ] !== attribB[ key ] ) { + + return false; + + } + + } + + return true; } - GLTFParser.prototype._withDependencies = function ( dependencies ) { + function getCachedGeometry( cache, newPrimitive ) { + + for ( var i = 0, il = cache.length; i < il; i ++ ) { - var _dependencies = {}; + var cached = cache[ i ]; - for ( var i = 0; i < dependencies.length; i ++ ) { + if ( isPrimitiveEqual( cached.primitive, newPrimitive ) ) { - var dependency = dependencies[ i ]; - var fnName = 'load' + dependency.charAt( 0 ).toUpperCase() + dependency.slice( 1 ); + return cached.promise; - var cached = this.cache.get( dependency ); + } + + } + + return null; - if ( cached !== undefined ) { + } + + function cloneBufferAttribute( attribute ) { - _dependencies[ dependency ] = cached; + if ( attribute.isInterleavedBufferAttribute ) { - } else if ( this[ fnName ] ) { + var count = attribute.count; + var itemSize = attribute.itemSize; + var array = attribute.array.slice( 0, count * itemSize ); - var fn = this[ fnName ](); - this.cache.add( dependency, fn ); + for ( var i = 0; i < count; ++ i ) { - _dependencies[ dependency ] = fn; + array[ i ] = attribute.getX( i ); + if ( itemSize >= 2 ) array[ i + 1 ] = attribute.getY( i ); + if ( itemSize >= 3 ) array[ i + 2 ] = attribute.getZ( i ); + if ( itemSize >= 4 ) array[ i + 3 ] = attribute.getW( i ); } + return new THREE.BufferAttribute( array, itemSize, attribute.normalized ); + } - return _each( _dependencies, function ( dependency ) { + return attribute.clone(); + + } - return dependency; + /* GLTF PARSER */ - } ); + function GLTFParser( json, extensions, options ) { - }; + this.json = json || {}; + this.extensions = extensions || {}; + this.options = options || {}; + + // loader object cache + this.cache = new GLTFRegistry(); + + // BufferGeometry caching + this.primitiveCache = []; + + this.textureLoader = new THREE.TextureLoader( this.options.manager ); + this.textureLoader.setCrossOrigin( this.options.crossOrigin ); + + this.fileLoader = new THREE.FileLoader( this.options.manager ); + this.fileLoader.setResponseType( 'arraybuffer' ); + + } GLTFParser.prototype.parse = function ( onLoad, onError ) { var json = this.json; - var parser = this; // Clear the loader cache this.cache.removeAll(); + // Mark the special nodes/meshes in json for efficient parse + this.markDefs(); + // Fire the callback on complete - this._withDependencies( [ + this.getMultiDependencies( [ - 'scenes', - 'animations' + 'scene', + 'animation', + 'camera' ] ).then( function ( dependencies ) { var scenes = dependencies.scenes || []; var scene = scenes[ json.scene || 0 ]; var animations = dependencies.animations || []; + var asset = json.asset; + var cameras = dependencies.cameras || []; - parser.getDependencies( 'camera' ).then( function ( cameras ) { - - onLoad( scene, scenes, cameras, animations ); - - } ).catch( onError ); + onLoad( scene, scenes, cameras, animations, asset ); } ).catch( onError ); }; /** - * Requests the specified dependency asynchronously, with caching. - * @param {string} type - * @param {number} index - * @return {Promise<Object>} + * Marks the special nodes/meshes in json for efficient parse. */ - GLTFParser.prototype.getDependency = function ( type, index ) { + GLTFParser.prototype.markDefs = function () { - var cacheKey = type + ':' + index; - var dependency = this.cache.get( cacheKey ); + var nodeDefs = this.json.nodes || []; + var skinDefs = this.json.skins || []; + var meshDefs = this.json.meshes || []; - if ( ! dependency ) { + var meshReferences = {}; + var meshUses = {}; - var fnName = 'load' + type.charAt( 0 ).toUpperCase() + type.slice( 1 ); - dependency = this[ fnName ]( index ); - this.cache.add( cacheKey, dependency ); + // Nothing in the node definition indicates whether it is a Bone or an + // Object3D. Use the skins' joint references to mark bones. + for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { - } + var joints = skinDefs[ skinIndex ].joints; + + for ( var i = 0, il = joints.length; i < il; i ++ ) { + + nodeDefs[ joints[ i ] ].isBone = true; + + } + + } + + // Meshes can (and should) be reused by multiple nodes in a glTF asset. To + // avoid having more than one THREE.Mesh with the same name, count + // references and rename instances below. + // + // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. + for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { + + var nodeDef = nodeDefs[ nodeIndex ]; + + if ( nodeDef.mesh !== undefined ) { + + if ( meshReferences[ nodeDef.mesh ] === undefined ) { + + meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0; + + } + + meshReferences[ nodeDef.mesh ] ++; + + // Nothing in the mesh definition indicates whether it is + // a SkinnedMesh or Mesh. Use the node's mesh reference + // to mark SkinnedMesh if node has skin. + if ( nodeDef.skin !== undefined ) { + + meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; + + } + + } + + } + + this.json.meshReferences = meshReferences; + this.json.meshUses = meshUses; + + }; + + /** + * Requests the specified dependency asynchronously, with caching. + * @param {string} type + * @param {number} index + * @return {Promise<Object>} + */ + GLTFParser.prototype.getDependency = function ( type, index ) { + + var cacheKey = type + ':' + index; + var dependency = this.cache.get( cacheKey ); + + if ( ! dependency ) { + + var fnName = 'load' + type.charAt( 0 ).toUpperCase() + type.slice( 1 ); + dependency = this[ fnName ]( index ); + this.cache.add( cacheKey, dependency ); + + } return dependency; @@ -1412,14 +1501,57 @@ THREE.GLTFLoader = ( function () { */ GLTFParser.prototype.getDependencies = function ( type ) { - var parser = this; - var defs = this.json[ type + 's' ] || []; + var dependencies = this.cache.get( type ); + + if ( ! dependencies ) { + + var parser = this; + var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; + + dependencies = Promise.all( defs.map( function ( def, index ) { - return Promise.all( defs.map( function ( def, index ) { + return parser.getDependency( type, index ); - return parser.getDependency( type, index ); + } ) ); - } ) ); + this.cache.add( type, dependencies ); + + } + + return dependencies; + + }; + + /** + * Requests all multiple dependencies of the specified types asynchronously, with caching. + * @param {Array<string>} types + * @return {Promise<Object<Array<Object>>>} + */ + GLTFParser.prototype.getMultiDependencies = function ( types ) { + + var results = {}; + var pendings = []; + + for ( var i = 0, il = types.length; i < il; i ++ ) { + + var type = types[ i ]; + var value = this.getDependencies( type ); + + value = value.then( function ( key, value ) { + + results[ key ] = value; + + }.bind( this, type + ( type === 'mesh' ? 'es' : 's' ) ) ); + + pendings.push( value ); + + } + + return Promise.all( pendings ).then( function () { + + return results; + + } ); }; @@ -1479,45 +1611,131 @@ THREE.GLTFLoader = ( function () { }; - GLTFParser.prototype.loadAccessors = function () { + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors + * @param {number} accessorIndex + * @return {Promise<THREE.BufferAttribute|THREE.InterleavedBufferAttribute>} + */ + GLTFParser.prototype.loadAccessor = function ( accessorIndex ) { var parser = this; var json = this.json; - return _each( json.accessors, function ( accessor ) { + var accessorDef = this.json.accessors[ accessorIndex ]; + + if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { + + // Ignore empty accessors, which may be used to declare runtime + // information about attributes coming from another source (e.g. Draco + // compression extension). + return null; + + } - return parser.getDependency( 'bufferView', accessor.bufferView ).then( function ( bufferView ) { + var pendingBufferViews = []; - var itemSize = WEBGL_TYPE_SIZES[ accessor.type ]; - var TypedArray = WEBGL_COMPONENT_TYPES[ accessor.componentType ]; + if ( accessorDef.bufferView !== undefined ) { - // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. - var elementBytes = TypedArray.BYTES_PER_ELEMENT; - var itemBytes = elementBytes * itemSize; - var byteStride = json.bufferViews[ accessor.bufferView ].byteStride; - var normalized = accessor.normalized === true; - var array; + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); + + } else { + + pendingBufferViews.push( null ); + + } - // The buffer is not interleaved if the stride is the item size in bytes. - if ( byteStride && byteStride !== itemBytes ) { + if ( accessorDef.sparse !== undefined ) { + + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); + pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); + + } + + return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { + + var bufferView = bufferViews[ 0 ]; + + var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; + var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; + + // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. + var elementBytes = TypedArray.BYTES_PER_ELEMENT; + var itemBytes = elementBytes * itemSize; + var byteOffset = accessorDef.byteOffset || 0; + var byteStride = json.bufferViews[ accessorDef.bufferView ].byteStride; + var normalized = accessorDef.normalized === true; + var array, bufferAttribute; + + // The buffer is not interleaved if the stride is the item size in bytes. + if ( byteStride && byteStride !== itemBytes ) { + + var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType; + var ib = parser.cache.get( ibCacheKey ); + + if ( ! ib ) { // Use the full buffer if it's interleaved. array = new TypedArray( bufferView ); // Integer parameters to IB/IBA are in array elements, not bytes. - var ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes ); + ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes ); + + parser.cache.add( ibCacheKey, ib ); - return new THREE.InterleavedBufferAttribute( ib, itemSize, accessor.byteOffset / elementBytes, normalized ); + } + + bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, byteOffset / elementBytes, normalized ); + + } else { + + if ( bufferView === null ) { + + array = new TypedArray( accessorDef.count * itemSize ); } else { - array = new TypedArray( bufferView, accessor.byteOffset, accessor.count * itemSize ); + array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); + + } + + bufferAttribute = new THREE.BufferAttribute( array, itemSize, normalized ); + + } + + // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors + if ( accessorDef.sparse !== undefined ) { - return new THREE.BufferAttribute( array, itemSize, normalized ); + var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; + var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; + + var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; + var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; + + var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); + var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); + + if ( bufferView !== null ) { + + // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. + bufferAttribute.setArray( bufferAttribute.array.slice() ); } - } ); + for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) { + + var index = sparseIndices[ i ]; + + bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); + if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); + if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); + if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); + if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' ); + + } + + } + + return bufferAttribute; } ); @@ -1546,15 +1764,14 @@ THREE.GLTFLoader = ( function () { // Load binary image data from bufferView, if provided. - sourceURI = parser.getDependency( 'bufferView', source.bufferView ) - .then( function ( bufferView ) { + sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { - isObjectURL = true; - var blob = new Blob( [ bufferView ], { type: source.mimeType } ); - sourceURI = URL.createObjectURL( blob ); - return sourceURI; + isObjectURL = true; + var blob = new Blob( [ bufferView ], { type: source.mimeType } ); + sourceURI = URL.createObjectURL( blob ); + return sourceURI; - } ); + } ); } @@ -1628,275 +1845,283 @@ THREE.GLTFLoader = ( function () { /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials - * @return {Promise<Array<THREE.Material>>} + * @param {number} materialIndex + * @return {Promise<THREE.Material>} */ - GLTFParser.prototype.loadMaterials = function () { + GLTFParser.prototype.loadMaterial = function ( materialIndex ) { var parser = this; var json = this.json; var extensions = this.extensions; + var materialDef = this.json.materials[ materialIndex ]; - return _each( json.materials, function ( material ) { - - var materialType; - var materialParams = {}; - var materialExtensions = material.extensions || {}; - - var pending = []; + var materialType; + var materialParams = {}; + var materialExtensions = materialDef.extensions || {}; - if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_COMMON ] ) { - - var khcExtension = extensions[ EXTENSIONS.KHR_MATERIALS_COMMON ]; - materialType = khcExtension.getMaterialType( material ); - pending.push( khcExtension.extendParams( materialParams, material, parser ) ); + var pending = []; - } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_CMN_CONSTANT ] ) { + if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { - materialType = THREE.MeshBasicMaterial; + var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; + materialType = sgExtension.getMaterialType( materialDef ); + pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); - } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { + } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { - var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; - materialType = sgExtension.getMaterialType( material ); - pending.push( sgExtension.extendParams( materialParams, material, parser ) ); + var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; + materialType = kmuExtension.getMaterialType( materialDef ); + pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); - } else if ( material.pbrMetallicRoughness !== undefined ) { + } else if ( materialDef.pbrMetallicRoughness !== undefined ) { - // Specification: - // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material + // Specification: + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material - materialType = THREE.MeshStandardMaterial; + materialType = THREE.MeshStandardMaterial; - var metallicRoughness = material.pbrMetallicRoughness; + var metallicRoughness = materialDef.pbrMetallicRoughness; - materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); - materialParams.opacity = 1.0; + materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); + materialParams.opacity = 1.0; - if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { + if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { - var array = metallicRoughness.baseColorFactor; + var array = metallicRoughness.baseColorFactor; - materialParams.color.fromArray( array ); - materialParams.opacity = array[ 3 ]; + materialParams.color.fromArray( array ); + materialParams.opacity = array[ 3 ]; - } + } - if ( metallicRoughness.baseColorTexture !== undefined ) { + if ( metallicRoughness.baseColorTexture !== undefined ) { - pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) ); + pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) ); - } + } - materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; - materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; + materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; + materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; - if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { + if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { - var textureIndex = metallicRoughness.metallicRoughnessTexture.index; - pending.push( parser.assignTexture( materialParams, 'metalnessMap', textureIndex ) ); - pending.push( parser.assignTexture( materialParams, 'roughnessMap', textureIndex ) ); + var textureIndex = metallicRoughness.metallicRoughnessTexture.index; + pending.push( parser.assignTexture( materialParams, 'metalnessMap', textureIndex ) ); + pending.push( parser.assignTexture( materialParams, 'roughnessMap', textureIndex ) ); - } + } - } else { + } else { - materialType = THREE.MeshPhongMaterial; + materialType = THREE.MeshPhongMaterial; - } - - if ( material.doubleSided === true ) { + } - materialParams.side = THREE.DoubleSide; + if ( materialDef.doubleSided === true ) { - } + materialParams.side = THREE.DoubleSide; - var alphaMode = material.alphaMode || ALPHA_MODES.OPAQUE; + } - if ( alphaMode !== ALPHA_MODES.OPAQUE ) { + var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; - materialParams.transparent = true; + if ( alphaMode === ALPHA_MODES.BLEND ) { - if ( alphaMode === ALPHA_MODES.MASK ) { + materialParams.transparent = true; - materialParams.alphaTest = material.alphaCutoff !== undefined ? material.alphaCutoff : 0.5; + } else { - } + materialParams.transparent = false; - } else { + if ( alphaMode === ALPHA_MODES.MASK ) { - materialParams.transparent = false; + materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; } - if ( material.normalTexture !== undefined ) { + } - pending.push( parser.assignTexture( materialParams, 'normalMap', material.normalTexture.index ) ); + if ( materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { - materialParams.normalScale = new THREE.Vector2( 1, 1 ); + pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture.index ) ); - if ( material.normalTexture.scale !== undefined ) { + materialParams.normalScale = new THREE.Vector2( 1, 1 ); - materialParams.normalScale.set( material.normalTexture.scale, material.normalTexture.scale ); + if ( materialDef.normalTexture.scale !== undefined ) { - } + materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale ); } - if ( material.occlusionTexture !== undefined ) { + } - pending.push( parser.assignTexture( materialParams, 'aoMap', material.occlusionTexture.index ) ); + if ( materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { - if ( material.occlusionTexture.strength !== undefined ) { + pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture.index ) ); - materialParams.aoMapIntensity = material.occlusionTexture.strength; + if ( materialDef.occlusionTexture.strength !== undefined ) { - } + materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; } - if ( material.emissiveFactor !== undefined ) { + } - if ( materialType === THREE.MeshBasicMaterial ) { + if ( materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial) { - materialParams.color = new THREE.Color().fromArray( material.emissiveFactor ); + materialParams.emissive = new THREE.Color().fromArray( materialDef.emissiveFactor ); - } else { + } - materialParams.emissive = new THREE.Color().fromArray( material.emissiveFactor ); + if ( materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { - } + pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture.index ) ); - } + } - if ( material.emissiveTexture !== undefined ) { + return Promise.all( pending ).then( function () { - if ( materialType === THREE.MeshBasicMaterial ) { + var material; - pending.push( parser.assignTexture( materialParams, 'map', material.emissiveTexture.index ) ); + if ( materialType === THREE.ShaderMaterial ) { - } else { + material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); - pending.push( parser.assignTexture( materialParams, 'emissiveMap', material.emissiveTexture.index ) ); + } else { - } + material = new materialType( materialParams ); } - return Promise.all( pending ).then( function () { + if ( materialDef.name !== undefined ) material.name = materialDef.name; - var _material; + // Normal map textures use OpenGL conventions: + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materialnormaltexture + if ( material.normalScale ) { - if ( materialType === THREE.ShaderMaterial ) { + material.normalScale.x = - material.normalScale.x; - _material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); + } - } else { + // emissiveTexture and baseColorTexture use sRGB encoding. + // if ( material.map ) material.map.encoding = THREE.sRGBEncoding; + // if ( material.emissiveMap ) material.emissiveMap.encoding = THREE.sRGBEncoding; - _material = new materialType( materialParams ); + if ( materialDef.extras ) material.userData = materialDef.extras; - } + return material; - if ( material.name !== undefined ) _material.name = material.name; + } ); - // Normal map textures use OpenGL conventions: - // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materialnormaltexture - if ( _material.normalScale ) { + }; - _material.normalScale.x = - _material.normalScale.x; + /** + * @param {THREE.BufferGeometry} geometry + * @param {GLTF.Primitive} primitiveDef + * @param {Array<THREE.BufferAttribute>} accessors + */ + function addPrimitiveAttributes ( geometry, primitiveDef, accessors ) { - } + var attributes = primitiveDef.attributes; - // emissiveTexture and baseColorTexture use sRGB encoding. - // TODO: Figure out why we need to comment this out. Textures that are exported as sRGB appear darker. - //if ( _material.map ) _material.map.encoding = THREE.sRGBEncoding; - //if ( _material.emissiveMap ) _material.emissiveMap.encoding = THREE.sRGBEncoding; + for ( var gltfAttributeName in attributes ) { - if ( material.extras ) _material.userData = material.extras; + var threeAttributeName = ATTRIBUTES[ gltfAttributeName ]; + var bufferAttribute = accessors[ attributes[ gltfAttributeName ] ]; - return _material; + // Skip attributes already provided by e.g. Draco extension. + if ( !threeAttributeName ) continue; + if ( threeAttributeName in geometry.attributes ) continue; - } ); + geometry.addAttribute( threeAttributeName, bufferAttribute ); - } ); + } - }; + if ( primitiveDef.indices !== undefined && !geometry.index ) { - GLTFParser.prototype.loadGeometries = function ( primitives ) { + geometry.setIndex( accessors[ primitiveDef.indices ] ); - return this._withDependencies( [ + } - 'accessors', + } - ] ).then( function ( dependencies ) { + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry + * @param {Array<Object>} primitives + * @return {Promise<Array<THREE.BufferGeometry>>} + */ + GLTFParser.prototype.loadGeometries = function ( primitives ) { - return _each( primitives, function ( primitive ) { + var parser = this; + var extensions = this.extensions; + var cache = this.primitiveCache; - var geometry = new THREE.BufferGeometry(); + return this.getDependencies( 'accessor' ).then( function ( accessors ) { - var attributes = primitive.attributes; + var geometries = []; + var pending = []; - for ( var attributeId in attributes ) { + for ( var i = 0, il = primitives.length; i < il; i ++ ) { - var attributeEntry = attributes[ attributeId ]; + var primitive = primitives[ i ]; - if ( attributeEntry === undefined ) return; + // See if we've already created this geometry + var cached = getCachedGeometry( cache, primitive ); - var bufferAttribute = dependencies.accessors[ attributeEntry ]; + var geometry; - switch ( attributeId ) { + if ( cached ) { - case 'POSITION': + // Use the cached geometry if it exists + pending.push( cached.then( function ( geometry ) { - geometry.addAttribute( 'position', bufferAttribute ); - break; + geometries.push( geometry ); - case 'NORMAL': + } ) ); - geometry.addAttribute( 'normal', bufferAttribute ); - break; + } else if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { - case 'TEXCOORD_0': - case 'TEXCOORD0': - case 'TEXCOORD': + // Use DRACO geometry if available + var geometryPromise = extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] + .decodePrimitive( primitive, parser ) + .then( function ( geometry ) { - geometry.addAttribute( 'uv', bufferAttribute ); - break; + addPrimitiveAttributes( geometry, primitive, accessors ); - case 'TEXCOORD_1': + geometries.push( geometry ); - geometry.addAttribute( 'uv2', bufferAttribute ); - break; + return geometry; - case 'COLOR_0': - case 'COLOR0': - case 'COLOR': + } ); - geometry.addAttribute( 'color', bufferAttribute ); - break; + cache.push( { primitive: primitive, promise: geometryPromise } ); - case 'WEIGHTS_0': - case 'WEIGHT': // WEIGHT semantic deprecated. + pending.push( geometryPromise ); - geometry.addAttribute( 'skinWeight', bufferAttribute ); - break; + } else { - case 'JOINTS_0': - case 'JOINT': // JOINT semantic deprecated. + // Otherwise create a new geometry + geometry = new THREE.BufferGeometry(); - geometry.addAttribute( 'skinIndex', bufferAttribute ); - break; + addPrimitiveAttributes( geometry, primitive, accessors ); - } + // Cache this geometry + cache.push( { - } + primitive: primitive, + promise: Promise.resolve( geometry ) - if ( primitive.indices !== undefined ) { + } ); - geometry.setIndex( dependencies.accessors[ primitive.indices ] ); + geometries.push( geometry ); } - return geometry; + } + + return Promise.all( pending ).then( function () { + + return geometries; } ); @@ -1906,94 +2131,132 @@ THREE.GLTFLoader = ( function () { /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes + * @param {number} meshIndex + * @return {Promise<THREE.Group|THREE.Mesh|THREE.SkinnedMesh>} */ - GLTFParser.prototype.loadMeshes = function () { + GLTFParser.prototype.loadMesh = function ( meshIndex ) { var scope = this; var json = this.json; var extensions = this.extensions; - return this._withDependencies( [ + var meshDef = this.json.meshes[ meshIndex ]; + + return this.getMultiDependencies( [ - 'accessors', - 'materials' + 'accessor', + 'material' ] ).then( function ( dependencies ) { - return _each( json.meshes, function ( meshDef, meshIndex ) { + var group = new THREE.Group(); - var group = new THREE.Group(); + var primitives = meshDef.primitives; - var primitives = meshDef.primitives || []; + return scope.loadGeometries( primitives ).then( function ( geometries ) { - return scope.loadGeometries( primitives ).then( function ( geometries ) { + for ( var i = 0, il = primitives.length; i < il; i ++ ) { - for ( var i = 0; i < primitives.length; i ++ ) { + var primitive = primitives[ i ]; + var geometry = geometries[ i ]; - var primitive = primitives[ i ]; - var geometry = geometries[ i ]; + var material = primitive.material === undefined + ? createDefaultMaterial() + : dependencies.materials[ primitive.material ]; - var material = primitive.material === undefined - ? createDefaultMaterial() - : dependencies.materials[ primitive.material ]; + if ( material.aoMap + && geometry.attributes.uv2 === undefined + && geometry.attributes.uv !== undefined ) { - if ( material.aoMap - && geometry.attributes.uv2 === undefined - && geometry.attributes.uv !== undefined ) { + console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' ); + geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) ); - console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' ); - geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) ); + } - } + // If the material will be modified later on, clone it now. + var useVertexColors = geometry.attributes.color !== undefined; + var useFlatShading = geometry.attributes.normal === undefined; + var useSkinning = meshDef.isSkinnedMesh === true; + var useMorphTargets = primitive.targets !== undefined; - var useVertexColors = geometry.attributes.color !== undefined; - var useFlatShading = geometry.attributes.normal === undefined; + if ( useVertexColors || useFlatShading || useSkinning || useMorphTargets ) { - if ( useVertexColors || useFlatShading ) { + if ( material.isGLTFSpecularGlossinessMaterial ) { - if ( material.isGLTFSpecularGlossinessMaterial ) { + var specGlossExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; + material = specGlossExtension.cloneMaterial( material ); - var specGlossExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; - material = specGlossExtension.cloneMaterial( material ); + } else { - } else { + material = material.clone(); - material = material.clone(); + } - } + } - } + if ( useVertexColors ) { - if ( useVertexColors ) { + material.vertexColors = THREE.VertexColors; + material.needsUpdate = true; - material.vertexColors = THREE.VertexColors; - material.needsUpdate = true; + } - } + if ( useFlatShading ) { - if ( useFlatShading ) { + material.flatShading = true; - material.flatShading = true; + } - } + var mesh; - var mesh; + if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || + primitive.mode === undefined ) { - if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || primitive.mode === undefined ) { + if ( useSkinning ) { - mesh = new THREE.Mesh( geometry, material ); + mesh = new THREE.SkinnedMesh( geometry, material ); + material.skinning = true; - } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { + } else { mesh = new THREE.Mesh( geometry, material ); + + } + + if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { + mesh.drawMode = THREE.TriangleStripDrawMode; } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { - mesh = new THREE.Mesh( geometry, material ); mesh.drawMode = THREE.TriangleFanDrawMode; - } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { + } + + } else if ( primitive.mode === WEBGL_CONSTANTS.LINES || + primitive.mode === WEBGL_CONSTANTS.LINE_STRIP || + primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { + + var cacheKey = 'LineBasicMaterial:' + material.uuid; + + var lineMaterial = scope.cache.get( cacheKey ); + + if ( ! lineMaterial ) { + + lineMaterial = new THREE.LineBasicMaterial(); + THREE.Material.prototype.copy.call( lineMaterial, material ); + lineMaterial.color.copy( material.color ); + lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet + + scope.cache.add( cacheKey, lineMaterial ); + + } + + material = lineMaterial; + + if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { mesh = new THREE.LineSegments( geometry, material ); @@ -2001,47 +2264,73 @@ THREE.GLTFLoader = ( function () { mesh = new THREE.Line( geometry, material ); - } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { + } else { mesh = new THREE.LineLoop( geometry, material ); - } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { + } - mesh = new THREE.Points( geometry, material ); + } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { - } else { + var cacheKey = 'PointsMaterial:' + material.uuid; - throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ', primitive.mode ); + var pointsMaterial = scope.cache.get( cacheKey ); + + if ( ! pointsMaterial ) { + + pointsMaterial = new THREE.PointsMaterial(); + THREE.Material.prototype.copy.call( pointsMaterial, material ); + pointsMaterial.color.copy( material.color ); + pointsMaterial.map = material.map; + pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet + + scope.cache.add( cacheKey, pointsMaterial ); } - mesh.name = meshDef.name || ( 'mesh_' + meshIndex ); + material = pointsMaterial; - if ( primitive.targets !== undefined ) { + mesh = new THREE.Points( geometry, material ); - addMorphTargets( mesh, meshDef, primitive, dependencies ); + } else { - } + throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); - if ( primitive.extras ) mesh.userData = primitive.extras; + } - if ( primitives.length > 1 ) { + mesh.name = meshDef.name || ( 'mesh_' + meshIndex ); - mesh.name += '_' + i; + if ( useMorphTargets ) { - group.add( mesh ); + addMorphTargets( mesh, meshDef, primitive, dependencies.accessors ); - } else { + } - return mesh; + if ( meshDef.extras !== undefined ) mesh.userData = meshDef.extras; + if ( primitive.extras !== undefined ) mesh.geometry.userData = primitive.extras; - } + // for Specular-Glossiness. + if ( material.isGLTFSpecularGlossinessMaterial === true ) { + + mesh.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms; } - return group; + if ( primitives.length > 1 ) { - } ); + mesh.name += '_' + i; + + group.add( mesh ); + + } else { + + return mesh; + + } + + } + + return group; } ); @@ -2087,386 +2376,375 @@ THREE.GLTFLoader = ( function () { }; - GLTFParser.prototype.loadSkins = function () { + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins + * @param {number} skinIndex + * @return {Promise<Object>} + */ + GLTFParser.prototype.loadSkin = function ( skinIndex ) { - var json = this.json; + var skinDef = this.json.skins[ skinIndex ]; - return this._withDependencies( [ + var skinEntry = { joints: skinDef.joints }; - 'accessors' + if ( skinDef.inverseBindMatrices === undefined ) { - ] ).then( function ( dependencies ) { + return Promise.resolve( skinEntry ); - return _each( json.skins, function ( skin ) { + } - var _skin = { - joints: skin.joints, - inverseBindMatrices: dependencies.accessors[ skin.inverseBindMatrices ] - }; + return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { - return _skin; + skinEntry.inverseBindMatrices = accessor; - } ); + return skinEntry; } ); }; - GLTFParser.prototype.loadAnimations = function () { + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations + * @param {number} animationIndex + * @return {Promise<THREE.AnimationClip>} + */ + GLTFParser.prototype.loadAnimation = function ( animationIndex ) { var json = this.json; - return this._withDependencies( [ + var animationDef = this.json.animations[ animationIndex ]; - 'accessors', - 'nodes' + return this.getMultiDependencies( [ + + 'accessor', + 'node' ] ).then( function ( dependencies ) { - return _each( json.animations, function ( animation, animationId ) { + var tracks = []; + + for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) { - var tracks = []; + var channel = animationDef.channels[ i ]; + var sampler = animationDef.samplers[ channel.sampler ]; - for ( var i = 0; i < animation.channels.length; i ++ ) { + if ( sampler ) { - var channel = animation.channels[ i ]; - var sampler = animation.samplers[ channel.sampler ]; + var target = channel.target; + var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. + var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; + var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; - if ( sampler ) { + var inputAccessor = dependencies.accessors[ input ]; + var outputAccessor = dependencies.accessors[ output ]; - var target = channel.target; - var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. - var input = animation.parameters !== undefined ? animation.parameters[ sampler.input ] : sampler.input; - var output = animation.parameters !== undefined ? animation.parameters[ sampler.output ] : sampler.output; + var node = dependencies.nodes[ name ]; - var inputAccessor = dependencies.accessors[ input ]; - var outputAccessor = dependencies.accessors[ output ]; + if ( node ) { - var node = dependencies.nodes[ name ]; + node.updateMatrix(); + node.matrixAutoUpdate = true; - if ( node ) { + var TypedKeyframeTrack; - node.updateMatrix(); - node.matrixAutoUpdate = true; + switch ( PATH_PROPERTIES[ target.path ] ) { - var TypedKeyframeTrack; + case PATH_PROPERTIES.weights: - switch ( PATH_PROPERTIES[ target.path ] ) { + TypedKeyframeTrack = THREE.NumberKeyframeTrack; + break; - case PATH_PROPERTIES.weights: + case PATH_PROPERTIES.rotation: - TypedKeyframeTrack = THREE.NumberKeyframeTrack; - break; + TypedKeyframeTrack = THREE.QuaternionKeyframeTrack; + break; - case PATH_PROPERTIES.rotation: + case PATH_PROPERTIES.position: + case PATH_PROPERTIES.scale: + default: - TypedKeyframeTrack = THREE.QuaternionKeyframeTrack; - break; + TypedKeyframeTrack = THREE.VectorKeyframeTrack; + break; - case PATH_PROPERTIES.position: - case PATH_PROPERTIES.scale: - default: + } - TypedKeyframeTrack = THREE.VectorKeyframeTrack; - break; + var targetName = node.name ? node.name : node.uuid; - } + var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear; - var targetName = node.name ? node.name : node.uuid; + var targetNames = []; - if ( sampler.interpolation === 'CATMULLROMSPLINE' ) { + if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { - console.warn( 'THREE.GLTFLoader: CATMULLROMSPLINE interpolation is not supported. Using CUBICSPLINE instead.' ); + // node should be THREE.Group here but + // PATH_PROPERTIES.weights(morphTargetInfluences) should be + // the property of a mesh object under node. + // So finding targets here. - } + node.traverse( function ( object ) { - var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear; + if ( object.isMesh === true && object.material.morphTargets === true ) { - var targetNames = []; + targetNames.push( object.name ? object.name : object.uuid ); - if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { + } - // node should be THREE.Group here but - // PATH_PROPERTIES.weights(morphTargetInfluences) should be - // the property of a mesh object under node. - // So finding targets here. + } ); - node.traverse( function ( object ) { + } else { - if ( object.isMesh === true && object.material.morphTargets === true ) { + targetNames.push( targetName ); - targetNames.push( object.name ? object.name : object.uuid ); + } - } + // KeyframeTrack.optimize() will modify given 'times' and 'values' + // buffers before creating a truncated copy to keep. Because buffers may + // be reused by other tracks, make copies here. + for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) { - } ); + var track = new TypedKeyframeTrack( + targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], + THREE.AnimationUtils.arraySlice( inputAccessor.array, 0 ), + THREE.AnimationUtils.arraySlice( outputAccessor.array, 0 ), + interpolation + ); - } else { + // Here is the trick to enable custom interpolation. + // Overrides .createInterpolant in a factory method which creates custom interpolation. + if ( sampler.interpolation === 'CUBICSPLINE' ) { - targetNames.push( targetName ); + track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { - } + // A CUBICSPLINE keyframe in glTF has three output values for each input value, + // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() + // must be divided by three to get the interpolant's sampleSize argument. + + return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result ); - // KeyframeTrack.optimize() will modify given 'times' and 'values' - // buffers before creating a truncated copy to keep. Because buffers may - // be reused by other tracks, make copies here. - for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) { + }; - tracks.push( new TypedKeyframeTrack( - targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], - THREE.AnimationUtils.arraySlice( inputAccessor.array, 0 ), - THREE.AnimationUtils.arraySlice( outputAccessor.array, 0 ), - interpolation - ) ); + // Workaround, provide an alternate way to know if the interpolant type is cubis spline to track. + // track.getInterpolation() doesn't return valid value for custom interpolant. + track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; } + tracks.push( track ); + } } } - var name = animation.name !== undefined ? animation.name : 'animation_' + animationId; + } - return new THREE.AnimationClip( name, undefined, tracks ); + var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex; - } ); + return new THREE.AnimationClip( name, undefined, tracks ); } ); }; - GLTFParser.prototype.loadNodes = function () { + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy + * @param {number} nodeIndex + * @return {Promise<THREE.Object3D>} + */ + GLTFParser.prototype.loadNode = function ( nodeIndex ) { var json = this.json; var extensions = this.extensions; - var scope = this; - var nodes = json.nodes || []; - var skins = json.skins || []; + var meshReferences = this.json.meshReferences; + var meshUses = this.json.meshUses; - var meshReferences = {}; - var meshUses = {}; + var nodeDef = this.json.nodes[ nodeIndex ]; - // Nothing in the node definition indicates whether it is a Bone or an - // Object3D. Use the skins' joint references to mark bones. - for ( var skinIndex = 0; skinIndex < skins.length; skinIndex ++ ) { + return this.getMultiDependencies( [ - var joints = skins[ skinIndex ].joints; + 'mesh', + 'skin', + 'camera' - for ( var i = 0; i < joints.length; ++ i ) { + ] ).then( function ( dependencies ) { - nodes[ joints[ i ] ].isBone = true; + var node; - } + if ( nodeDef.isBone === true ) { - } + node = new THREE.Bone(); - // Meshes can (and should) be reused by multiple nodes in a glTF asset. To - // avoid having more than one THREE.Mesh with the same name, count - // references and rename instances below. - // - // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. - for ( var nodeIndex = 0; nodeIndex < nodes.length; nodeIndex ++ ) { + } else if ( nodeDef.mesh !== undefined ) { - var nodeDef = nodes[ nodeIndex ]; + var mesh = dependencies.meshes[ nodeDef.mesh ]; - if ( nodeDef.mesh !== undefined ) { + node = mesh.clone(); - if ( meshReferences[ nodeDef.mesh ] === undefined ) { + // for Specular-Glossiness + if ( mesh.isGroup === true ) { - meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0; + for ( var i = 0, il = mesh.children.length; i < il; i ++ ) { - } + var child = mesh.children[ i ]; - meshReferences[ nodeDef.mesh ] ++; + if ( child.material && child.material.isGLTFSpecularGlossinessMaterial === true ) { - } - - } + node.children[ i ].onBeforeRender = child.onBeforeRender; - return scope._withDependencies( [ - - 'meshes', - 'skins', - 'cameras' - - ] ).then( function ( dependencies ) { - - return _each( json.nodes, function ( nodeDef ) { - - if ( nodeDef.isBone === true ) { - - return new THREE.Bone(); + } - } else if ( nodeDef.mesh !== undefined ) { + } - var mesh = dependencies.meshes[ nodeDef.mesh ].clone(); + } else { - if ( meshReferences[ nodeDef.mesh ] > 1 ) { + if ( mesh.material && mesh.material.isGLTFSpecularGlossinessMaterial === true ) { - mesh.name += '_instance_' + meshUses[ nodeDef.mesh ] ++; + node.onBeforeRender = mesh.onBeforeRender; } - return mesh; + } - } else if ( nodeDef.camera !== undefined ) { + if ( meshReferences[ nodeDef.mesh ] > 1 ) { - return scope.getDependency( 'camera', nodeDef.camera ); + node.name += '_instance_' + meshUses[ nodeDef.mesh ] ++; - } else if ( nodeDef.extensions - && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ] - && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) { + } - var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights; - return lights[ nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ]; + } else if ( nodeDef.camera !== undefined ) { - } else { + node = dependencies.cameras[ nodeDef.camera ]; - return new THREE.Object3D(); + } else if ( nodeDef.extensions + && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ] + && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) { - } + var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights; + node = lights[ nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ]; - } ).then( function ( __nodes ) { + } else { - return _each( __nodes, function ( node, nodeIndex ) { + node = new THREE.Object3D(); - var nodeDef = json.nodes[ nodeIndex ]; + } - if ( nodeDef.name !== undefined ) { + if ( nodeDef.name !== undefined ) { - node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name ); + node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name ); - } + } - if ( nodeDef.extras ) node.userData = nodeDef.extras; + if ( nodeDef.extras ) node.userData = nodeDef.extras; - if ( nodeDef.matrix !== undefined ) { + if ( nodeDef.matrix !== undefined ) { - var matrix = new THREE.Matrix4(); - matrix.fromArray( nodeDef.matrix ); - node.applyMatrix( matrix ); + var matrix = new THREE.Matrix4(); + matrix.fromArray( nodeDef.matrix ); + node.applyMatrix( matrix ); - } else { + } else { - if ( nodeDef.translation !== undefined ) { + if ( nodeDef.translation !== undefined ) { - node.position.fromArray( nodeDef.translation ); + node.position.fromArray( nodeDef.translation ); - } + } - if ( nodeDef.rotation !== undefined ) { + if ( nodeDef.rotation !== undefined ) { - node.quaternion.fromArray( nodeDef.rotation ); + node.quaternion.fromArray( nodeDef.rotation ); - } + } - if ( nodeDef.scale !== undefined ) { + if ( nodeDef.scale !== undefined ) { - node.scale.fromArray( nodeDef.scale ); + node.scale.fromArray( nodeDef.scale ); - } + } - } + } - if ( nodeDef.skin !== undefined ) { + return node; - var skinnedMeshes = []; + } ); - var meshes = node.children.length > 0 ? node.children : [ node ]; + }; - for ( var i = 0; i < meshes.length; i ++ ) { + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes + * @param {number} sceneIndex + * @return {Promise<THREE.Scene>} + */ + GLTFParser.prototype.loadScene = function () { - var mesh = meshes[ i ]; - var skinEntry = dependencies.skins[ nodeDef.skin ]; + // scene node hierachy builder - // Replace Mesh with SkinnedMesh. - var geometry = mesh.geometry; - var material = mesh.material; - material.skinning = true; + function buildNodeHierachy( nodeId, parentObject, json, allNodes, skins ) { - var skinnedMesh = new THREE.SkinnedMesh( geometry, material ); - skinnedMesh.morphTargetInfluences = mesh.morphTargetInfluences; - skinnedMesh.userData = mesh.userData; - skinnedMesh.name = mesh.name; + var node = allNodes[ nodeId ]; + var nodeDef = json.nodes[ nodeId ]; - var bones = []; - var boneInverses = []; + // build skeleton here as well - for ( var j = 0, l = skinEntry.joints.length; j < l; j ++ ) { + if ( nodeDef.skin !== undefined ) { - var jointId = skinEntry.joints[ j ]; - var jointNode = __nodes[ jointId ]; + var meshes = node.isGroup === true ? node.children : [ node ]; - if ( jointNode ) { + for ( var i = 0, il = meshes.length; i < il; i ++ ) { - bones.push( jointNode ); + var mesh = meshes[ i ]; + var skinEntry = skins[ nodeDef.skin ]; - var m = skinEntry.inverseBindMatrices.array; - var mat = new THREE.Matrix4().fromArray( m, j * 16 ); - boneInverses.push( mat ); + var bones = []; + var boneInverses = []; - } else { + for ( var j = 0, jl = skinEntry.joints.length; j < jl; j ++ ) { - console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', jointId ); + var jointId = skinEntry.joints[ j ]; + var jointNode = allNodes[ jointId ]; - } + if ( jointNode ) { - } + bones.push( jointNode ); - skinnedMesh.bind( new THREE.Skeleton( bones, boneInverses ), skinnedMesh.matrixWorld ); + var mat = new THREE.Matrix4(); - skinnedMeshes.push( skinnedMesh ); + if ( skinEntry.inverseBindMatrices !== undefined ) { - } + mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); - if ( node.children.length > 0 ) { + } - node.remove.apply( node, node.children ); - node.add.apply( node, skinnedMeshes ); + boneInverses.push( mat ); } else { - node = skinnedMeshes[ 0 ]; + console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', jointId ); } } - return node; - - } ); - - } ); - - } ); - - }; - - GLTFParser.prototype.loadScenes = function () { - - var json = this.json; - var extensions = this.extensions; + mesh.bind( new THREE.Skeleton( bones, boneInverses ), mesh.matrixWorld ); - // scene node hierachy builder + } - function buildNodeHierachy( nodeId, parentObject, allNodes ) { + } - var _node = allNodes[ nodeId ]; - parentObject.add( _node ); + // build node hierachy - var node = json.nodes[ nodeId ]; + parentObject.add( node ); - if ( node.children ) { + if ( nodeDef.children ) { - var children = node.children; + var children = nodeDef.children; - for ( var i = 0, l = children.length; i < l; i ++ ) { + for ( var i = 0, il = children.length; i < il; i ++ ) { var child = children[ i ]; - buildNodeHierachy( child, _node, allNodes ); + buildNodeHierachy( child, node, json, allNodes, skins ); } @@ -2474,56 +2752,49 @@ THREE.GLTFLoader = ( function () { } - return this._withDependencies( [ - - 'nodes' - - ] ).then( function ( dependencies ) { - - return _each( json.scenes, function ( scene ) { - - var _scene = new THREE.Scene(); - if ( scene.name !== undefined ) _scene.name = scene.name; + return function loadScene( sceneIndex ) { - if ( scene.extras ) _scene.userData = scene.extras; + var json = this.json; + var extensions = this.extensions; + var sceneDef = this.json.scenes[ sceneIndex ]; - var nodes = scene.nodes || []; + return this.getMultiDependencies( [ - for ( var i = 0, l = nodes.length; i < l; i ++ ) { + 'node', + 'skin' - var nodeId = nodes[ i ]; - buildNodeHierachy( nodeId, _scene, dependencies.nodes ); + ] ).then( function ( dependencies ) { - } + var scene = new THREE.Scene(); + if ( sceneDef.name !== undefined ) scene.name = sceneDef.name; - _scene.traverse( function ( child ) { + if ( sceneDef.extras ) scene.userData = sceneDef.extras; - // for Specular-Glossiness. - if ( child.material && child.material.isGLTFSpecularGlossinessMaterial ) { + var nodeIds = sceneDef.nodes || []; - child.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms; + for ( var i = 0, il = nodeIds.length; i < il; i ++ ) { - } + buildNodeHierachy( nodeIds[ i ], scene, json, dependencies.nodes, dependencies.skins ); - } ); + } // Ambient lighting, if present, is always attached to the scene root. - if ( scene.extensions - && scene.extensions[ EXTENSIONS.KHR_LIGHTS ] - && scene.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) { + if ( sceneDef.extensions + && sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ] + && sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) { var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights; - _scene.add( lights[ scene.extensions[ EXTENSIONS.KHR_LIGHTS ].light ] ); + scene.add( lights[ sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ] ); } - return _scene; + return scene; } ); - } ); + }; - }; + }(); return GLTFLoader; diff --git a/webpack.config.js b/webpack.config.js index 9b21aae0841e10351ec7bfcdc401b845a3cea869..a9b388b51809208c840e933970169a3eda4f241f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -52,7 +52,7 @@ function createHTTPSConfig() { fs.writeFileSync(path.join(__dirname, "certs", "key.pem"), pems.private); return { - key: pems.public, + key: pems.private, cert: pems.cert }; } @@ -73,7 +73,7 @@ class LodashTemplatePlugin { } } -module.exports = { +const config = { entry: { lobby: path.join(__dirname, "src", "lobby.js"), room: path.join(__dirname, "src", "room.js"), @@ -81,7 +81,8 @@ module.exports = { }, output: { path: path.join(__dirname, "public"), - filename: "[name]-[chunkhash].js" + filename: "[name]-[chunkhash].js", + publicPath: process.env.BASE_ASSETS_PATH || "" }, mode: "development", devtool: process.env.NODE_ENV === "production" ? "source-map" : "inline-source-map", @@ -89,6 +90,7 @@ module.exports = { open: true, https: createHTTPSConfig(), host: "0.0.0.0", + useLocalIp: true, port: 8080, before: function(app) { // networked-aframe makes HEAD requests to the server for time syncing. Respond with an empty body. @@ -114,7 +116,14 @@ module.exports = { loader: "html-loader", options: { // <a-asset-item>'s src property is overwritten with the correct transformed asset url. - attrs: ["img:src", "a-asset-item:src", "audio:src"], + attrs: [ + "img:src", + "a-asset-item:src", + "a-progressive-asset:src", + "a-progressive-asset:high-src", + "a-progressive-asset:low-src", + "audio:src" + ], // You can get transformed asset urls in an html template using ${require("pathToFile.ext")} interpolate: "require" } @@ -198,3 +207,30 @@ module.exports = { }) ] }; + +module.exports = () => { + if (process.env.GENERATE_SMOKE_TESTS && process.env.BASE_ASSETS_PATH) { + const smokeConfig = Object.assign({}, config, { + // Set the public path for to point to the correct assets on the smoke-test build. + output: Object.assign({}, config.output, { + publicPath: process.env.BASE_ASSETS_PATH.replace("://", "://smoke-") + }), + // For this config + plugins: config.plugins.map(plugin => { + if (plugin instanceof HTMLWebpackPlugin) { + return new HTMLWebpackPlugin( + Object.assign({}, plugin.options, { + filename: "smoke-" + plugin.options.filename + }) + ); + } + + return plugin; + }) + }); + + return [config, smokeConfig]; + } else { + return config; + } +}; diff --git a/yarn.lock b/yarn.lock index 2503a504c466982b1187c7e9e220a57082bee5f7..ca49471c81719770f30b8ce7a5765d9bb0490685 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4505,9 +4505,9 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -naf-janus-adapter@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.3.0.tgz#fee55fe0f4724238da5f87fbb0e7f75cd522905e" +naf-janus-adapter@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.4.0.tgz#22f14212a14d9e3d30c8d9441978704ff58392f4" dependencies: debug "^3.1.0" minijanus "^0.4.0"