├── img ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg ├── 9.jpg ├── 04.jpg ├── 10.jpg ├── 11.jpg ├── 12.jpg └── 13.jpg ├── gallery.blend ├── gallery2.blend ├── model ├── collider.glb └── gallery.glb ├── css └── style.css ├── index.html └── js ├── joystick.js ├── PointerLockControls.js ├── _app.js ├── FirstPersonControls.js ├── app.js ├── OrbitControls.js └── GLTFLoader.js /img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/1.jpg -------------------------------------------------------------------------------- /img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/2.jpg -------------------------------------------------------------------------------- /img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/3.jpg -------------------------------------------------------------------------------- /img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/4.jpg -------------------------------------------------------------------------------- /img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/5.jpg -------------------------------------------------------------------------------- /img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/6.jpg -------------------------------------------------------------------------------- /img/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/7.jpg -------------------------------------------------------------------------------- /img/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/8.jpg -------------------------------------------------------------------------------- /img/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/9.jpg -------------------------------------------------------------------------------- /img/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/04.jpg -------------------------------------------------------------------------------- /img/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/10.jpg -------------------------------------------------------------------------------- /img/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/11.jpg -------------------------------------------------------------------------------- /img/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/12.jpg -------------------------------------------------------------------------------- /img/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/img/13.jpg -------------------------------------------------------------------------------- /gallery.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/gallery.blend -------------------------------------------------------------------------------- /gallery2.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/gallery2.blend -------------------------------------------------------------------------------- /model/collider.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/model/collider.glb -------------------------------------------------------------------------------- /model/gallery.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mayupi/3dvr-gallery/HEAD/model/gallery.glb -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, body { 7 | height: 100%; 8 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 3D画廊 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /js/joystick.js: -------------------------------------------------------------------------------- 1 | const touchEnabled = !!('ontouchstart' in window) 2 | 3 | class JoyStick { 4 | 5 | constructor(options) { 6 | this.createDom() 7 | this.maxRadius = options.maxRadius || 40 8 | this.maxRadiusSquared = this.maxRadius * this.maxRadius 9 | this.onMove = options.onMove 10 | this.game = options.game 11 | this.origin = { 12 | left: this.domElement.offsetLeft, 13 | top: this.domElement.offsetTop 14 | } 15 | console.log(this.origin) 16 | this.rotationDamping = options.rotationDamping || 0.06 17 | this.moveDamping = options.moveDamping || 0.01 18 | this.createEvent() 19 | } 20 | 21 | createEvent() { 22 | const joystick = this 23 | if(touchEnabled) { 24 | this.domElement.addEventListener('touchstart', function(e) { 25 | e.preventDefault() 26 | joystick.tap(e) 27 | e.stopPropagation() 28 | }) 29 | } else { 30 | this.domElement.addEventListener('mousedown', function(e) { 31 | e.preventDefault() 32 | joystick.tap(e) 33 | e.stopPropagation() 34 | }) 35 | } 36 | } 37 | 38 | getMousePosition(e) { 39 | let clientX = e.targetTouches ? e.targetTouches[0].pageX : e.clientX 40 | let clientY = e.targetTouches ? e.targetTouches[0].pageY : e.clientY 41 | return { 42 | x:clientX, 43 | y:clientY 44 | } 45 | } 46 | 47 | tap(e) { 48 | this.offset = this.getMousePosition(e) 49 | const joystick = this 50 | this.onTouchMoved = function(e) { 51 | e.preventDefault() 52 | joystick.move(e) 53 | } 54 | this.onTouchEnded = function(e) { 55 | e.preventDefault() 56 | joystick.up(e) 57 | } 58 | if(touchEnabled) { 59 | document.addEventListener('touchmove', this.onTouchMoved) 60 | document.addEventListener('touchend', this.onTouchEnded) 61 | } else { 62 | document.addEventListener('mousemove', this.onTouchMoved) 63 | document.addEventListener('mouseup', this.onTouchEnded) 64 | } 65 | } 66 | 67 | move(e) { 68 | const mouse = this.getMousePosition(e) 69 | 70 | let left = mouse.x - this.offset.x 71 | let top = mouse.y - this.offset.y 72 | 73 | const sqMag = left * left + top * top 74 | 75 | if (sqMag > this.maxRadiusSquared){ 76 | const magnitude = Math.sqrt(sqMag) 77 | left /= magnitude 78 | top /= magnitude 79 | left *= this.maxRadius 80 | top *= this.maxRadius 81 | } 82 | 83 | this.domElement.style.top = `${ top + this.domElement.clientHeight / 2 }px` 84 | this.domElement.style.left = `${ left + this.domElement.clientWidth / 2 }px` 85 | 86 | const forward = -(top - this.origin.top + this.domElement.clientHeight / 2) / this.maxRadius 87 | const turn = (left - this.origin.left + this.domElement.clientWidth / 2) / this.maxRadius 88 | 89 | if(this.onMove) { 90 | this.onMove(forward, turn) 91 | } 92 | 93 | } 94 | 95 | up(e) { 96 | if (touchEnabled){ 97 | document.removeEventListener('touchmove', this.onTouchMoved) 98 | document.removeEventListener('touchend', this.onTouchEned) 99 | }else{ 100 | document.removeEventListener('mousemove', this.onTouchMoved) 101 | document.removeEventListener('mouseup', this.onTouchEned) 102 | } 103 | this.domElement.style.top = `${this.origin.top}px` 104 | this.domElement.style.left = `${this.origin.left}px` 105 | if(this.onMove) { 106 | this.onMove(0, 0) 107 | } 108 | } 109 | 110 | createDom() { 111 | const circle = document.createElement('div') 112 | circle.style.cssText = ` 113 | position: absolute; 114 | bottom: 35px; 115 | width: 80px; 116 | height: 80px; 117 | background: rgba(126, 126, 126, 0.2); 118 | border: #444 solid medium; 119 | border-radius: 50%; 120 | left: 50%; 121 | transform: translateX(-50%); 122 | ` 123 | const thumb = document.createElement('div') 124 | thumb.style.cssText = ` 125 | position: absolute; 126 | left: 20px; 127 | top: 20px; 128 | width: 40px; 129 | height: 40px; 130 | border-radius: 50%; 131 | background: #fff; 132 | ` 133 | circle.appendChild(thumb) 134 | document.body.appendChild(circle) 135 | this.domElement = thumb 136 | } 137 | } -------------------------------------------------------------------------------- /js/PointerLockControls.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | const _euler = new THREE.Euler( 0, 0, 0, 'YXZ' ); 4 | 5 | const _vector = new THREE.Vector3(); 6 | 7 | const _changeEvent = { 8 | type: 'change' 9 | }; 10 | const _lockEvent = { 11 | type: 'lock' 12 | }; 13 | const _unlockEvent = { 14 | type: 'unlock' 15 | }; 16 | 17 | const _PI_2 = Math.PI / 2; 18 | 19 | class PointerLockControls extends THREE.EventDispatcher { 20 | 21 | constructor( camera, domElement ) { 22 | 23 | super(); 24 | 25 | if ( domElement === undefined ) { 26 | 27 | console.warn( 'THREE.PointerLockControls: The second parameter "domElement" is now mandatory.' ); 28 | domElement = document.body; 29 | 30 | } 31 | 32 | this.domElement = domElement; 33 | this.isLocked = true; // Set to constrain the pitch of the camera 34 | // Range is 0 to Math.PI radians 35 | 36 | this.minPolarAngle = 0; // radians 37 | 38 | this.maxPolarAngle = Math.PI; // radians 39 | 40 | const scope = this; 41 | 42 | function onMouseMove( event ) { 43 | 44 | //dwif ( scope.isLocked === false ) return; 45 | const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; 46 | const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; 47 | 48 | _euler.setFromQuaternion( camera.quaternion ); 49 | 50 | _euler.y -= movementX * 0.002; 51 | _euler.x -= movementY * 0.002; 52 | _euler.x = Math.max( _PI_2 - scope.maxPolarAngle, Math.min( _PI_2 - scope.minPolarAngle, _euler.x ) ); 53 | camera.quaternion.setFromEuler( _euler ); 54 | scope.dispatchEvent( _changeEvent ); 55 | 56 | } 57 | 58 | function onPointerlockChange() { 59 | 60 | if ( scope.domElement.ownerDocument.pointerLockElement === scope.domElement ) { 61 | 62 | scope.dispatchEvent( _lockEvent ); 63 | scope.isLocked = true; 64 | 65 | } else { 66 | 67 | scope.dispatchEvent( _unlockEvent ); 68 | //scope.isLocked = false; 69 | 70 | } 71 | 72 | } 73 | 74 | function onPointerlockError() { 75 | 76 | console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' ); 77 | 78 | } 79 | 80 | this.connect = function () { 81 | 82 | scope.domElement.ownerDocument.addEventListener( 'mousemove', onMouseMove ); 83 | //scope.domElement.ownerDocument.addEventListener( 'pointerlockchange', onPointerlockChange ); 84 | //ddwdscope.domElement.ownerDocument.addEventListener( 'pointerlockerror', onPointerlockError ); 85 | 86 | }; 87 | 88 | this.disconnect = function () { 89 | 90 | scope.domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove ); 91 | //scope.domElement.ownerDocument.removeEventListener( 'pointerlockchange', onPointerlockChange ); 92 | //scope.domElement.ownerDocument.removeEventListener( 'pointerlockerror', onPointerlockError ); 93 | 94 | }; 95 | 96 | this.dispose = function () { 97 | 98 | this.disconnect(); 99 | 100 | }; 101 | 102 | this.getObject = function () { 103 | 104 | // retaining this method for backward compatibility 105 | return camera; 106 | 107 | }; 108 | 109 | this.getDirection = function () { 110 | 111 | const direction = new THREE.Vector3( 0, 0, - 1 ); 112 | return function ( v ) { 113 | 114 | return v.copy( direction ).applyQuaternion( camera.quaternion ); 115 | 116 | }; 117 | 118 | }(); 119 | 120 | this.moveForward = function ( distance ) { 121 | 122 | // move forward parallel to the xz-plane 123 | // assumes camera.up is y-up 124 | _vector.setFromMatrixColumn( camera.matrix, 0 ); 125 | 126 | _vector.crossVectors( camera.up, _vector ); 127 | 128 | camera.position.addScaledVector( _vector, distance ); 129 | 130 | }; 131 | 132 | this.moveRight = function ( distance ) { 133 | 134 | _vector.setFromMatrixColumn( camera.matrix, 0 ); 135 | 136 | camera.position.addScaledVector( _vector, distance ); 137 | 138 | }; 139 | 140 | this.lock = function () { 141 | 142 | //this.domElement.requestPointerLock(); 143 | 144 | }; 145 | 146 | this.unlock = function () { 147 | 148 | //scope.domElement.ownerDocument.exitPointerLock(); 149 | 150 | }; 151 | 152 | this.connect(); 153 | 154 | } 155 | 156 | } 157 | 158 | THREE.PointerLockControls = PointerLockControls; 159 | 160 | } )(); 161 | -------------------------------------------------------------------------------- /js/_app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 作者: 行歌 3 | * 微信公众号: 码语派 4 | */ 5 | 6 | let camera, renderer, scene 7 | let controls 8 | let pointLight1, pointLight2, pointLight3 9 | let pointLight4, pointLight5, pointLight6 10 | let pointLight7 11 | let ambientLight 12 | let clock = new THREE.Clock() 13 | 14 | 15 | let player, activeCamera 16 | let speed = 6 //移动速度 17 | let turnSpeed = 2 18 | let move = { 19 | forward: 0, 20 | turn: 0 21 | } 22 | 23 | function init() { 24 | createScene() 25 | createObjects() 26 | createColliders() 27 | createPlayer() 28 | createCamera() 29 | createLights() 30 | //createLightHelpers() 31 | createControls() 32 | createEvents() 33 | render() 34 | } 35 | 36 | function createEvents() { 37 | document.addEventListener('keydown', onKeyDown) 38 | document.addEventListener('keyup', onKeyUp) 39 | } 40 | 41 | function createColliders() { 42 | const loader = new THREE.GLTFLoader() 43 | loader.load( 44 | 'model/collider.glb', 45 | gltf => { 46 | gltf.scene.traverse(child => { 47 | console.log(child) 48 | }) 49 | } 50 | ) 51 | } 52 | 53 | function onKeyDown(event) { 54 | switch ( event.code ) { 55 | case 'ArrowUp': 56 | case 'KeyW': 57 | move.forward = 1 58 | break 59 | 60 | case 'ArrowLeft': 61 | case 'KeyA': 62 | move.turn = turnSpeed 63 | break 64 | 65 | case 'ArrowDown': 66 | case 'KeyS': 67 | move.forward = -1 68 | break 69 | 70 | case 'ArrowRight': 71 | case 'KeyD': 72 | move.turn = -turnSpeed 73 | break 74 | case 'Space': 75 | break 76 | } 77 | } 78 | 79 | function onKeyUp(event) { 80 | switch ( event.code ) { 81 | 82 | case 'ArrowUp': 83 | case 'KeyW': 84 | move.forward = 0 85 | break 86 | 87 | case 'ArrowLeft': 88 | case 'KeyA': 89 | move.turn = 0 90 | break 91 | 92 | case 'ArrowDown': 93 | case 'KeyS': 94 | move.forward = 0 95 | break 96 | 97 | case 'ArrowRight': 98 | case 'KeyD': 99 | move.turn = 0 100 | break 101 | 102 | } 103 | } 104 | 105 | function createPlayer() { 106 | const geometry = new THREE.BoxGeometry(1, 2, 1) 107 | const material = new THREE.MeshBasicMaterial({ 108 | color: 0xff0000, 109 | wireframe: true 110 | }) 111 | player = new THREE.Mesh(geometry, material) 112 | geometry.translate(0, 1, 0) 113 | player.position.set(-5, 0, 5) 114 | //scene.add(player) 115 | } 116 | 117 | function createCamera() { 118 | const back = new THREE.Object3D() 119 | back.position.set(0, 2, 1) 120 | back.parent = player 121 | //player.add(back) 122 | 123 | activeCamera = back 124 | 125 | } 126 | 127 | function createScene() { 128 | renderer = new THREE.WebGLRenderer({ 129 | antialias: true 130 | }) 131 | renderer.outputEncoding = THREE.sRGBEncoding 132 | renderer.setSize(window.innerWidth, window.innerHeight) 133 | renderer.setPixelRatio(window.devicePixelRatio) 134 | // renderer.shadowMap.enabled = true 135 | // renderer.shadowMap.type = THREE.PCFSoftShadowMap 136 | 137 | camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000) 138 | camera.position.set(-10, 2, 10) 139 | 140 | scene = new THREE.Scene() 141 | 142 | const container = document.querySelector('#container') 143 | container.appendChild(renderer.domElement) 144 | 145 | window.addEventListener('resize', onResize) 146 | } 147 | 148 | function createLights() { 149 | ambientLight = new THREE.AmbientLight(0xe0ffff, 0.6) 150 | scene.add(ambientLight) 151 | 152 | pointLight1 = new THREE.PointLight(0xe0ffff, 0.1, 20) 153 | pointLight1.position.set(-2, 3, 2) 154 | 155 | scene.add(pointLight1) 156 | 157 | pointLight2 = new THREE.PointLight(0xe0ffff, 0.1, 20) 158 | pointLight2.position.set(0, 3, -6) 159 | scene.add(pointLight2) 160 | 161 | pointLight3 = new THREE.PointLight(0xe0ffff, 0.1, 20) 162 | pointLight3.position.set(-12, 3, 6) 163 | scene.add(pointLight3) 164 | 165 | pointLight4 = new THREE.PointLight(0xe0ffff, 0.1, 20) 166 | pointLight4.position.set(-12, 4, -4) 167 | scene.add(pointLight4) 168 | 169 | pointLight5 = new THREE.PointLight(0xe0ffff, 0.1, 20) 170 | pointLight5.position.set(12, 4, -8) 171 | scene.add(pointLight5) 172 | 173 | pointLight6 = new THREE.PointLight(0xe0ffff, 0.1, 20) 174 | pointLight6.position.set(12, 4, 0) 175 | scene.add(pointLight6) 176 | 177 | pointLight7 = new THREE.PointLight(0xe0ffff, 0.1, 20) 178 | pointLight7.position.set(12, 4, 8) 179 | scene.add(pointLight7) 180 | } 181 | 182 | function createLightHelpers() { 183 | 184 | const pointLightHelper1 = new THREE.PointLightHelper(pointLight1, 1) 185 | scene.add(pointLightHelper1) 186 | 187 | const pointLightHelper2 = new THREE.PointLightHelper(pointLight2, 1) 188 | scene.add(pointLightHelper2) 189 | 190 | const pointLightHelper3 = new THREE.PointLightHelper(pointLight3, 1) 191 | scene.add(pointLightHelper3) 192 | 193 | const pointLightHelper4 = new THREE.PointLightHelper(pointLight4, 1) 194 | scene.add(pointLightHelper4) 195 | 196 | const pointLightHelper5 = new THREE.PointLightHelper(pointLight5, 1) 197 | scene.add(pointLightHelper5) 198 | 199 | const pointLightHelper6 = new THREE.PointLightHelper(pointLight6, 1) 200 | scene.add(pointLightHelper6) 201 | 202 | const pointLightHelper7 = new THREE.PointLightHelper(pointLight7, 1) 203 | scene.add(pointLightHelper7) 204 | } 205 | 206 | function createControls() { 207 | //controls = new THREE.OrbitControls(camera, renderer.domElement) 208 | } 209 | 210 | 211 | function createObjects() { 212 | const loader = new THREE.GLTFLoader() 213 | loader.load( 214 | 'model/gallery.glb', 215 | gltf => { 216 | gltf.scene.traverse(child => { 217 | switch(child.name) { 218 | case 'walls': 219 | initWalls(child) 220 | break 221 | case 'stairs': 222 | initStairs(child) 223 | break 224 | } 225 | //设置展画边框贴图 226 | if(child.name.includes('paint')) { 227 | initFrames(child) 228 | } 229 | //设置展画图片贴图 230 | if(child.name.includes('draw')) { 231 | initDraws(child) 232 | } 233 | }) 234 | scene.add(gltf.scene) 235 | } 236 | ) 237 | } 238 | 239 | function initDraws(child) { 240 | const index = child.name.split('draw')[1] 241 | const texture = new THREE.TextureLoader().load(`img/${index}.jpg`) 242 | texture.encoding = THREE.sRGBEncoding 243 | texture.flipY = false 244 | const material = new THREE.MeshPhongMaterial({ 245 | map: texture 246 | }) 247 | child.material = material 248 | } 249 | 250 | function initFrames(child) { 251 | child.material = new THREE.MeshBasicMaterial({ 252 | color: 0x7f5816 253 | }) 254 | } 255 | 256 | function initStairs(child) { 257 | child.castShadow = true 258 | child.material = new THREE.MeshStandardMaterial({ 259 | color: 0xd1cdb7 260 | }) 261 | child.material.roughness = 0.5 262 | child.material.metalness = 0.6 263 | } 264 | 265 | function initWalls(child) { 266 | child.receiveShadow = true 267 | child.material = new THREE.MeshStandardMaterial({ 268 | color: 0xffffff 269 | }) 270 | child.material.roughness = 0.5 271 | child.material.metalness = 0.6 272 | } 273 | 274 | function onResize() { 275 | const w = window.innerWidth 276 | const h = window.innerHeight 277 | camera.aspect = w / h 278 | camera.updateProjectionMatrix() 279 | renderer.setSize(w, h) 280 | } 281 | 282 | 283 | function render() { 284 | const dt = clock.getDelta() 285 | update(dt) 286 | renderer.render(scene, camera) 287 | window.requestAnimationFrame(render) 288 | } 289 | 290 | function update(dt) { 291 | updatePlayer(dt) 292 | updateCamera(dt) 293 | } 294 | 295 | function updatePlayer(dt) { 296 | if(move.forward !== 0) { 297 | if (move.forward > 0) { 298 | console.log('dd') 299 | player.translateZ(-dt * speed) 300 | } else { 301 | player.translateZ(dt * speed * 0.5) 302 | } 303 | } 304 | if(move.turn !== 0) { 305 | player.rotateY(move.turn * dt) 306 | } 307 | } 308 | 309 | function updateCamera(dt) { 310 | //更新摄像机 311 | camera.position.lerp( 312 | activeCamera.getWorldPosition( 313 | new THREE.Vector3() 314 | ), 315 | 0.05 316 | ) 317 | const pos = player.position.clone() 318 | pos.y += 2 319 | camera.lookAt(pos) 320 | } 321 | 322 | init() -------------------------------------------------------------------------------- /js/FirstPersonControls.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | const _lookDirection = new THREE.Vector3(); 4 | 5 | const _spherical = new THREE.Spherical(); 6 | 7 | const _target = new THREE.Vector3(); 8 | 9 | class FirstPersonControls { 10 | 11 | constructor( object, domElement ) { 12 | 13 | if ( domElement === undefined ) { 14 | 15 | console.warn( 'THREE.FirstPersonControls: The second parameter "domElement" is now mandatory.' ); 16 | domElement = document; 17 | 18 | } 19 | 20 | this.object = object; 21 | this.domElement = domElement; // API 22 | 23 | this.enabled = true; 24 | this.movementSpeed = 1.0; 25 | this.lookSpeed = 0.005; 26 | this.lookVertical = true; 27 | this.autoForward = false; 28 | this.activeLook = true; 29 | this.heightSpeed = false; 30 | this.heightCoef = 1.0; 31 | this.heightMin = 0.0; 32 | this.heightMax = 1.0; 33 | this.constrainVertical = false; 34 | this.verticalMin = 0; 35 | this.verticalMax = Math.PI; 36 | this.mouseDragOn = false; // internals 37 | 38 | this.autoSpeedFactor = 0.0; 39 | this.mouseX = 0; 40 | this.mouseY = 0; 41 | this.moveForward = false; 42 | this.moveBackward = false; 43 | this.moveLeft = false; 44 | this.moveRight = false; 45 | this.viewHalfX = 0; 46 | this.viewHalfY = 0; // private variables 47 | 48 | let lat = 0; 49 | let lon = 0; // 50 | 51 | this.handleResize = function () { 52 | 53 | if ( this.domElement === document ) { 54 | 55 | this.viewHalfX = window.innerWidth / 2; 56 | this.viewHalfY = window.innerHeight / 2; 57 | 58 | } else { 59 | 60 | this.viewHalfX = this.domElement.offsetWidth / 2; 61 | this.viewHalfY = this.domElement.offsetHeight / 2; 62 | 63 | } 64 | 65 | }; 66 | 67 | this.onMouseDown = function ( event ) { 68 | 69 | if ( this.domElement !== document ) { 70 | 71 | this.domElement.focus(); 72 | 73 | } 74 | 75 | if ( this.activeLook ) { 76 | 77 | switch ( event.button ) { 78 | 79 | case 0: 80 | this.moveForward = true; 81 | break; 82 | 83 | case 2: 84 | this.moveBackward = true; 85 | break; 86 | 87 | } 88 | 89 | } 90 | 91 | this.mouseDragOn = true; 92 | 93 | }; 94 | 95 | this.onMouseUp = function ( event ) { 96 | 97 | if ( this.activeLook ) { 98 | 99 | switch ( event.button ) { 100 | 101 | case 0: 102 | this.moveForward = false; 103 | break; 104 | 105 | case 2: 106 | this.moveBackward = false; 107 | break; 108 | 109 | } 110 | 111 | } 112 | 113 | this.mouseDragOn = false; 114 | 115 | }; 116 | 117 | this.onMouseMove = function ( event ) { 118 | 119 | if ( this.domElement === document ) { 120 | 121 | this.mouseX = event.pageX - this.viewHalfX; 122 | this.mouseY = event.pageY - this.viewHalfY; 123 | 124 | } else { 125 | 126 | this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX; 127 | this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY; 128 | 129 | } 130 | 131 | }; 132 | 133 | this.onKeyDown = function ( event ) { 134 | 135 | switch ( event.code ) { 136 | 137 | case 'ArrowUp': 138 | case 'KeyW': 139 | this.moveForward = true; 140 | break; 141 | 142 | case 'ArrowLeft': 143 | case 'KeyA': 144 | this.moveLeft = true; 145 | break; 146 | 147 | case 'ArrowDown': 148 | case 'KeyS': 149 | this.moveBackward = true; 150 | break; 151 | 152 | case 'ArrowRight': 153 | case 'KeyD': 154 | this.moveRight = true; 155 | break; 156 | 157 | case 'KeyR': 158 | this.moveUp = true; 159 | break; 160 | 161 | case 'KeyF': 162 | this.moveDown = true; 163 | break; 164 | 165 | } 166 | 167 | }; 168 | 169 | this.onKeyUp = function ( event ) { 170 | 171 | switch ( event.code ) { 172 | 173 | case 'ArrowUp': 174 | case 'KeyW': 175 | this.moveForward = false; 176 | break; 177 | 178 | case 'ArrowLeft': 179 | case 'KeyA': 180 | this.moveLeft = false; 181 | break; 182 | 183 | case 'ArrowDown': 184 | case 'KeyS': 185 | this.moveBackward = false; 186 | break; 187 | 188 | case 'ArrowRight': 189 | case 'KeyD': 190 | this.moveRight = false; 191 | break; 192 | 193 | case 'KeyR': 194 | this.moveUp = false; 195 | break; 196 | 197 | case 'KeyF': 198 | this.moveDown = false; 199 | break; 200 | 201 | } 202 | 203 | }; 204 | 205 | this.lookAt = function ( x, y, z ) { 206 | 207 | if ( x.isVector3 ) { 208 | 209 | _target.copy( x ); 210 | 211 | } else { 212 | 213 | _target.set( x, y, z ); 214 | 215 | } 216 | 217 | this.object.lookAt( _target ); 218 | setOrientation( this ); 219 | return this; 220 | 221 | }; 222 | 223 | this.update = function () { 224 | 225 | const targetPosition = new THREE.Vector3(); 226 | return function update( delta ) { 227 | 228 | if ( this.enabled === false ) return; 229 | 230 | if ( this.heightSpeed ) { 231 | 232 | const y = THREE.MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax ); 233 | const heightDelta = y - this.heightMin; 234 | this.autoSpeedFactor = delta * ( heightDelta * this.heightCoef ); 235 | 236 | } else { 237 | 238 | this.autoSpeedFactor = 0.0; 239 | 240 | } 241 | 242 | const actualMoveSpeed = delta * this.movementSpeed; 243 | if ( this.moveForward || this.autoForward && ! this.moveBackward ) this.object.translateZ( - ( actualMoveSpeed + this.autoSpeedFactor ) ); 244 | if ( this.moveBackward ) this.object.translateZ( actualMoveSpeed ); 245 | if ( this.moveLeft ) this.object.translateX( - actualMoveSpeed ); 246 | if ( this.moveRight ) this.object.translateX( actualMoveSpeed ); 247 | if ( this.moveUp ) this.object.translateY( actualMoveSpeed ); 248 | if ( this.moveDown ) this.object.translateY( - actualMoveSpeed ); 249 | let actualLookSpeed = delta * this.lookSpeed; 250 | 251 | if ( ! this.activeLook ) { 252 | 253 | actualLookSpeed = 0; 254 | 255 | } 256 | 257 | let verticalLookRatio = 1; 258 | 259 | if ( this.constrainVertical ) { 260 | 261 | verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin ); 262 | 263 | } 264 | 265 | lon -= this.mouseX * actualLookSpeed; 266 | if ( this.lookVertical ) lat -= this.mouseY * actualLookSpeed * verticalLookRatio; 267 | lat = Math.max( - 85, Math.min( 85, lat ) ); 268 | let phi = THREE.MathUtils.degToRad( 90 - lat ); 269 | const theta = THREE.MathUtils.degToRad( lon ); 270 | 271 | if ( this.constrainVertical ) { 272 | 273 | phi = THREE.MathUtils.mapLinear( phi, 0, Math.PI, this.verticalMin, this.verticalMax ); 274 | 275 | } 276 | 277 | const position = this.object.position; 278 | targetPosition.setFromSphericalCoords( 1, phi, theta ).add( position ); 279 | this.object.lookAt( targetPosition ); 280 | 281 | }; 282 | 283 | }(); 284 | 285 | this.dispose = function () { 286 | 287 | this.domElement.removeEventListener( 'contextmenu', contextmenu ); 288 | this.domElement.removeEventListener( 'mousedown', _onMouseDown ); 289 | this.domElement.removeEventListener( 'mousemove', _onMouseMove ); 290 | this.domElement.removeEventListener( 'mouseup', _onMouseUp ); 291 | window.removeEventListener( 'keydown', _onKeyDown ); 292 | window.removeEventListener( 'keyup', _onKeyUp ); 293 | 294 | }; 295 | 296 | const _onMouseMove = this.onMouseMove.bind( this ); 297 | 298 | const _onMouseDown = this.onMouseDown.bind( this ); 299 | 300 | const _onMouseUp = this.onMouseUp.bind( this ); 301 | 302 | const _onKeyDown = this.onKeyDown.bind( this ); 303 | 304 | const _onKeyUp = this.onKeyUp.bind( this ); 305 | 306 | this.domElement.addEventListener( 'contextmenu', contextmenu ); 307 | this.domElement.addEventListener( 'mousemove', _onMouseMove ); 308 | this.domElement.addEventListener( 'mousedown', _onMouseDown ); 309 | this.domElement.addEventListener( 'mouseup', _onMouseUp ); 310 | window.addEventListener( 'keydown', _onKeyDown ); 311 | window.addEventListener( 'keyup', _onKeyUp ); 312 | 313 | function setOrientation( controls ) { 314 | 315 | const quaternion = controls.object.quaternion; 316 | 317 | _lookDirection.set( 0, 0, - 1 ).applyQuaternion( quaternion ); 318 | 319 | _spherical.setFromVector3( _lookDirection ); 320 | 321 | lat = 90 - THREE.MathUtils.radToDeg( _spherical.phi ); 322 | lon = THREE.MathUtils.radToDeg( _spherical.theta ); 323 | 324 | } 325 | 326 | this.handleResize(); 327 | setOrientation( this ); 328 | 329 | } 330 | 331 | } 332 | 333 | function contextmenu( event ) { 334 | 335 | event.preventDefault(); 336 | 337 | } 338 | 339 | THREE.FirstPersonControls = FirstPersonControls; 340 | 341 | } )(); 342 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 作者: 行歌 3 | * 微信公众号: 码语派 4 | */ 5 | 6 | let camera, renderer, scene 7 | let controls 8 | let pointLight1, pointLight2, pointLight3 9 | let pointLight4, pointLight5, pointLight6 10 | let pointLight7 11 | let ambientLight 12 | let clock = new THREE.Clock() 13 | 14 | 15 | let player, activeCamera 16 | let speed = 6 //移动速度 17 | let turnSpeed = 2 18 | let move = { 19 | forward: 0, 20 | turn: 0 21 | } 22 | 23 | let colliders = [] //碰撞物 24 | let debugMaterial = new THREE.MeshBasicMaterial({ 25 | color:0xff0000, 26 | wireframe: true 27 | }) 28 | 29 | let arrowHelper1, arrowHelper2 30 | let joystick //移动设备控制器 31 | 32 | function init() { 33 | createScene() 34 | createObjects() 35 | createColliders() 36 | createPlayer() 37 | createCamera() 38 | createLights() 39 | //createLightHelpers() 40 | //createControls() 41 | createEvents() 42 | createJoyStick() 43 | render() 44 | } 45 | 46 | function createJoyStick() { 47 | 48 | joystick = new JoyStick({ 49 | onMove: function(forward, turn) { 50 | turn = -turn 51 | if(Math.abs(forward) < 0.3) forward = 0 52 | if(Math.abs(turn) < 0.1) turn = 0 53 | move.forward = forward 54 | move.turn = turn 55 | } 56 | }) 57 | } 58 | 59 | function createEvents() { 60 | document.addEventListener('keydown', onKeyDown) 61 | document.addEventListener('keyup', onKeyUp) 62 | } 63 | 64 | function createColliders() { 65 | const loader = new THREE.GLTFLoader() 66 | loader.load( 67 | 'model/collider.glb', 68 | gltf => { 69 | gltf.scene.traverse(child => { 70 | if(child.name.includes('collider')) { 71 | colliders.push(child) 72 | } 73 | }) 74 | colliders.forEach(item=> { 75 | item.visible = false 76 | scene.add(item) 77 | }) 78 | } 79 | ) 80 | 81 | } 82 | 83 | function onKeyDown(event) { 84 | switch ( event.code ) { 85 | case 'ArrowUp': 86 | case 'KeyW': 87 | move.forward = 1 88 | break 89 | 90 | case 'ArrowLeft': 91 | case 'KeyA': 92 | move.turn = turnSpeed 93 | break 94 | 95 | case 'ArrowDown': 96 | case 'KeyS': 97 | move.forward = -1 98 | break 99 | 100 | case 'ArrowRight': 101 | case 'KeyD': 102 | move.turn = -turnSpeed 103 | break 104 | case 'Space': 105 | break 106 | } 107 | } 108 | 109 | function onKeyUp(event) { 110 | switch ( event.code ) { 111 | 112 | case 'ArrowUp': 113 | case 'KeyW': 114 | move.forward = 0 115 | break 116 | 117 | case 'ArrowLeft': 118 | case 'KeyA': 119 | move.turn = 0 120 | break 121 | 122 | case 'ArrowDown': 123 | case 'KeyS': 124 | move.forward = 0 125 | break 126 | 127 | case 'ArrowRight': 128 | case 'KeyD': 129 | move.turn = 0 130 | break 131 | 132 | } 133 | } 134 | 135 | function createPlayer() { 136 | const geometry = new THREE.BoxGeometry(1, 2, 1) 137 | const material = new THREE.MeshBasicMaterial({ 138 | color: 0xff0000, 139 | wireframe: true 140 | }) 141 | player = new THREE.Mesh(geometry, material) 142 | player.name = 'player' 143 | geometry.translate(0, 1, 0) 144 | player.position.set(-5, 0, 5) 145 | //scene.add(player) 146 | } 147 | 148 | function createCamera() { 149 | const back = new THREE.Object3D() 150 | back.position.set(0, 2, 1) 151 | back.parent = player 152 | //player.add(back) 153 | activeCamera = back 154 | } 155 | 156 | function createScene() { 157 | renderer = new THREE.WebGLRenderer({ 158 | antialias: false 159 | }) 160 | renderer.outputEncoding = THREE.sRGBEncoding 161 | renderer.setSize(window.innerWidth, window.innerHeight) 162 | renderer.setPixelRatio(window.devicePixelRatio) 163 | // renderer.shadowMap.enabled = true 164 | // renderer.shadowMap.type = THREE.PCFSoftShadowMap 165 | 166 | camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 5000) 167 | camera.position.set(-10, 2, 10) 168 | 169 | scene = new THREE.Scene() 170 | 171 | const container = document.querySelector('#container') 172 | container.appendChild(renderer.domElement) 173 | 174 | window.addEventListener('resize', onResize) 175 | } 176 | 177 | function createLights() { 178 | ambientLight = new THREE.AmbientLight(0xe0ffff, 0.6) 179 | scene.add(ambientLight) 180 | 181 | pointLight1 = new THREE.PointLight(0xe0ffff, 0.1, 20) 182 | pointLight1.position.set(-2, 3, 2) 183 | 184 | scene.add(pointLight1) 185 | 186 | pointLight2 = new THREE.PointLight(0xe0ffff, 0.1, 20) 187 | pointLight2.position.set(0, 3, -6) 188 | scene.add(pointLight2) 189 | 190 | pointLight3 = new THREE.PointLight(0xe0ffff, 0.1, 20) 191 | pointLight3.position.set(-12, 3, 6) 192 | scene.add(pointLight3) 193 | 194 | pointLight4 = new THREE.PointLight(0xe0ffff, 0.1, 20) 195 | pointLight4.position.set(-12, 4, -4) 196 | scene.add(pointLight4) 197 | 198 | pointLight5 = new THREE.PointLight(0xe0ffff, 0.1, 20) 199 | pointLight5.position.set(12, 4, -8) 200 | scene.add(pointLight5) 201 | 202 | pointLight6 = new THREE.PointLight(0xe0ffff, 0.1, 20) 203 | pointLight6.position.set(12, 4, 0) 204 | scene.add(pointLight6) 205 | 206 | pointLight7 = new THREE.PointLight(0xe0ffff, 0.1, 20) 207 | pointLight7.position.set(12, 4, 8) 208 | scene.add(pointLight7) 209 | } 210 | 211 | function createLightHelpers() { 212 | 213 | const pointLightHelper1 = new THREE.PointLightHelper(pointLight1, 1) 214 | scene.add(pointLightHelper1) 215 | 216 | const pointLightHelper2 = new THREE.PointLightHelper(pointLight2, 1) 217 | scene.add(pointLightHelper2) 218 | 219 | const pointLightHelper3 = new THREE.PointLightHelper(pointLight3, 1) 220 | scene.add(pointLightHelper3) 221 | 222 | const pointLightHelper4 = new THREE.PointLightHelper(pointLight4, 1) 223 | scene.add(pointLightHelper4) 224 | 225 | const pointLightHelper5 = new THREE.PointLightHelper(pointLight5, 1) 226 | scene.add(pointLightHelper5) 227 | 228 | const pointLightHelper6 = new THREE.PointLightHelper(pointLight6, 1) 229 | scene.add(pointLightHelper6) 230 | 231 | const pointLightHelper7 = new THREE.PointLightHelper(pointLight7, 1) 232 | scene.add(pointLightHelper7) 233 | } 234 | 235 | function createControls() { 236 | controls = new THREE.OrbitControls(camera, renderer.domElement) 237 | } 238 | 239 | 240 | function createObjects() { 241 | const loader = new THREE.GLTFLoader() 242 | loader.load( 243 | 'model/gallery.glb', 244 | gltf => { 245 | gltf.scene.traverse(child => { 246 | switch(child.name) { 247 | case 'walls': 248 | initWalls(child) 249 | break 250 | case 'stairs': 251 | initStairs(child) 252 | break 253 | } 254 | //设置展画边框贴图 255 | if(child.name.includes('paint')) { 256 | initFrames(child) 257 | } 258 | //设置展画图片贴图 259 | if(child.name.includes('draw')) { 260 | initDraws(child) 261 | } 262 | }) 263 | scene.add(gltf.scene) 264 | } 265 | ) 266 | } 267 | 268 | function initDraws(child) { 269 | const index = child.name.split('draw')[1] 270 | const texture = new THREE.TextureLoader().load(`img/${index}.jpg`) 271 | texture.encoding = THREE.sRGBEncoding 272 | texture.flipY = false 273 | const material = new THREE.MeshPhongMaterial({ 274 | map: texture 275 | }) 276 | child.material = material 277 | } 278 | 279 | function initFrames(child) { 280 | child.material = new THREE.MeshBasicMaterial({ 281 | color: 0x7f5816 282 | }) 283 | } 284 | 285 | function initStairs(child) { 286 | child.material = new THREE.MeshStandardMaterial({ 287 | color: 0xd1cdb7 288 | }) 289 | child.material.roughness = 0.5 290 | child.material.metalness = 0.6 291 | } 292 | 293 | function initWalls(child) { 294 | child.material = new THREE.MeshStandardMaterial({ 295 | color: 0xffffff 296 | }) 297 | child.material.roughness = 0.5 298 | child.material.metalness = 0.6 299 | } 300 | 301 | function onResize() { 302 | const w = window.innerWidth 303 | const h = window.innerHeight 304 | camera.aspect = w / h 305 | camera.updateProjectionMatrix() 306 | renderer.setSize(w, h) 307 | } 308 | 309 | function render() { 310 | const dt = clock.getDelta() 311 | update(dt) 312 | renderer.render(scene, camera) 313 | window.requestAnimationFrame(render) 314 | } 315 | 316 | function update(dt) { 317 | updatePlayer(dt) 318 | updateCamera(dt) 319 | } 320 | 321 | function updatePlayer(dt) { 322 | 323 | const pos = player.position.clone() 324 | pos.y += 2 325 | let dir = new THREE.Vector3() 326 | 327 | player.getWorldDirection(dir) 328 | dir.negate() 329 | 330 | if (move.forward < 0) dir.negate() 331 | let raycaster = new THREE.Raycaster(pos, dir) 332 | let blocked = false 333 | 334 | if(colliders.length > 0) { 335 | const intersect = raycaster.intersectObjects(colliders) 336 | if (intersect.length > 0) { 337 | if (intersect[0].distance < 1) { 338 | blocked = true 339 | } 340 | } 341 | } 342 | 343 | // if(colliders.length > 0) { 344 | // //左方向碰撞监测 345 | // dir.set(-1, 0, 0) 346 | // dir.applyMatrix4(player.matrix) 347 | // dir.normalize() 348 | // raycaster = new THREE.Raycaster(pos, dir) 349 | 350 | // let intersect = raycaster.intersectObjects(colliders) 351 | // if(intersect.length > 0) { 352 | // if(intersect[0].distance < 2) { 353 | // player.translateX(2 - intersect[0].distance) 354 | // } 355 | // } 356 | 357 | // //右方向碰撞监测 358 | // dir.set(1, 0, 0) 359 | // dir.applyMatrix4(player.matrix) 360 | // dir.normalize() 361 | // raycaster = new THREE.Raycaster(pos, dir) 362 | 363 | // intersect = raycaster.intersectObjects(colliders) 364 | // if(intersect.length > 0) { 365 | // if(intersect[0].distance < 2) { 366 | // player.translateX(intersect[0].distance - 2) 367 | // } 368 | // } 369 | // } 370 | 371 | if(!blocked) { 372 | if(move.forward !== 0) { 373 | if (move.forward > 0) { 374 | player.translateZ(-dt * speed) 375 | } else { 376 | player.translateZ(dt * speed * 0.5) 377 | } 378 | } 379 | } 380 | 381 | if(move.turn !== 0) { 382 | player.rotateY(move.turn * dt) 383 | } 384 | } 385 | 386 | function updateCamera(dt) { 387 | //更新摄像机 388 | camera.position.lerp( 389 | activeCamera.getWorldPosition( 390 | new THREE.Vector3() 391 | ), 392 | 0.08 393 | ) 394 | const pos = player.position.clone() 395 | pos.y += 2 396 | camera.lookAt(pos) 397 | } 398 | 399 | init() -------------------------------------------------------------------------------- /js/OrbitControls.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 4 | // 5 | // Orbit - left mouse / touch: one-finger move 6 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 7 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 8 | 9 | const _changeEvent = { 10 | type: 'change' 11 | }; 12 | const _startEvent = { 13 | type: 'start' 14 | }; 15 | const _endEvent = { 16 | type: 'end' 17 | }; 18 | 19 | class OrbitControls extends THREE.EventDispatcher { 20 | 21 | constructor( object, domElement ) { 22 | 23 | super(); 24 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 25 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 26 | this.object = object; 27 | this.domElement = domElement; 28 | this.domElement.style.touchAction = 'none'; // disable touch scroll 29 | // Set to false to disable this control 30 | 31 | this.enabled = true; // "target" sets the location of focus, where the object orbits around 32 | 33 | this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) 34 | 35 | this.minDistance = 0; 36 | this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) 37 | 38 | this.minZoom = 0; 39 | this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits. 40 | // Range is 0 to Math.PI radians. 41 | 42 | this.minPolarAngle = 0; // radians 43 | 44 | this.maxPolarAngle = Math.PI; // radians 45 | // How far you can orbit horizontally, upper and lower limits. 46 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 47 | 48 | this.minAzimuthAngle = - Infinity; // radians 49 | 50 | this.maxAzimuthAngle = Infinity; // radians 51 | // Set to true to enable damping (inertia) 52 | // If damping is enabled, you must call controls.update() in your animation loop 53 | 54 | this.enableDamping = false; 55 | this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 56 | // Set to false to disable zooming 57 | 58 | this.enableZoom = true; 59 | this.zoomSpeed = 1.0; // Set to false to disable rotating 60 | 61 | this.enableRotate = true; 62 | this.rotateSpeed = 1.0; // Set to false to disable panning 63 | 64 | this.enablePan = true; 65 | this.panSpeed = 1.0; 66 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 67 | 68 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 69 | // Set to true to automatically rotate around the target 70 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 71 | 72 | this.autoRotate = false; 73 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 74 | // The four arrow keys 75 | 76 | this.keys = { 77 | LEFT: 'ArrowLeft', 78 | UP: 'ArrowUp', 79 | RIGHT: 'ArrowRight', 80 | BOTTOM: 'ArrowDown' 81 | }; // Mouse buttons 82 | 83 | this.mouseButtons = { 84 | LEFT: THREE.MOUSE.ROTATE, 85 | MIDDLE: THREE.MOUSE.DOLLY, 86 | RIGHT: THREE.MOUSE.PAN 87 | }; // Touch fingers 88 | 89 | this.touches = { 90 | ONE: THREE.TOUCH.ROTATE, 91 | TWO: THREE.TOUCH.DOLLY_PAN 92 | }; // for reset 93 | 94 | this.target0 = this.target.clone(); 95 | this.position0 = this.object.position.clone(); 96 | this.zoom0 = this.object.zoom; // the target DOM element for key events 97 | 98 | this._domElementKeyEvents = null; // 99 | // public methods 100 | // 101 | 102 | this.getPolarAngle = function () { 103 | 104 | return spherical.phi; 105 | 106 | }; 107 | 108 | this.getAzimuthalAngle = function () { 109 | 110 | return spherical.theta; 111 | 112 | }; 113 | 114 | this.getDistance = function () { 115 | 116 | return this.object.position.distanceTo( this.target ); 117 | 118 | }; 119 | 120 | this.listenToKeyEvents = function ( domElement ) { 121 | 122 | domElement.addEventListener( 'keydown', onKeyDown ); 123 | this._domElementKeyEvents = domElement; 124 | 125 | }; 126 | 127 | this.saveState = function () { 128 | 129 | scope.target0.copy( scope.target ); 130 | scope.position0.copy( scope.object.position ); 131 | scope.zoom0 = scope.object.zoom; 132 | 133 | }; 134 | 135 | this.reset = function () { 136 | 137 | scope.target.copy( scope.target0 ); 138 | scope.object.position.copy( scope.position0 ); 139 | scope.object.zoom = scope.zoom0; 140 | scope.object.updateProjectionMatrix(); 141 | scope.dispatchEvent( _changeEvent ); 142 | scope.update(); 143 | state = STATE.NONE; 144 | 145 | }; // this method is exposed, but perhaps it would be better if we can make it private... 146 | 147 | 148 | this.update = function () { 149 | 150 | const offset = new THREE.Vector3(); // so camera.up is the orbit axis 151 | 152 | const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 153 | const quatInverse = quat.clone().invert(); 154 | const lastPosition = new THREE.Vector3(); 155 | const lastQuaternion = new THREE.Quaternion(); 156 | const twoPI = 2 * Math.PI; 157 | return function update() { 158 | 159 | const position = scope.object.position; 160 | offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space 161 | 162 | offset.applyQuaternion( quat ); // angle from z-axis around y-axis 163 | 164 | spherical.setFromVector3( offset ); 165 | 166 | if ( scope.autoRotate && state === STATE.NONE ) { 167 | 168 | rotateLeft( getAutoRotationAngle() ); 169 | 170 | } 171 | 172 | if ( scope.enableDamping ) { 173 | 174 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 175 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 176 | 177 | } else { 178 | 179 | spherical.theta += sphericalDelta.theta; 180 | spherical.phi += sphericalDelta.phi; 181 | 182 | } // restrict theta to be between desired limits 183 | 184 | 185 | let min = scope.minAzimuthAngle; 186 | let max = scope.maxAzimuthAngle; 187 | 188 | if ( isFinite( min ) && isFinite( max ) ) { 189 | 190 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 191 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 192 | 193 | if ( min <= max ) { 194 | 195 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 196 | 197 | } else { 198 | 199 | spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta ); 200 | 201 | } 202 | 203 | } // restrict phi to be between desired limits 204 | 205 | 206 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 207 | spherical.makeSafe(); 208 | spherical.radius *= scale; // restrict radius to be between desired limits 209 | 210 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); // move target to panned location 211 | 212 | if ( scope.enableDamping === true ) { 213 | 214 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 215 | 216 | } else { 217 | 218 | scope.target.add( panOffset ); 219 | 220 | } 221 | 222 | offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space 223 | 224 | offset.applyQuaternion( quatInverse ); 225 | position.copy( scope.target ).add( offset ); 226 | scope.object.lookAt( scope.target ); 227 | 228 | if ( scope.enableDamping === true ) { 229 | 230 | sphericalDelta.theta *= 1 - scope.dampingFactor; 231 | sphericalDelta.phi *= 1 - scope.dampingFactor; 232 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 233 | 234 | } else { 235 | 236 | sphericalDelta.set( 0, 0, 0 ); 237 | panOffset.set( 0, 0, 0 ); 238 | 239 | } 240 | 241 | scale = 1; // update condition is: 242 | // min(camera displacement, camera rotation in radians)^2 > EPS 243 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 244 | 245 | if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 246 | 247 | scope.dispatchEvent( _changeEvent ); 248 | lastPosition.copy( scope.object.position ); 249 | lastQuaternion.copy( scope.object.quaternion ); 250 | zoomChanged = false; 251 | return true; 252 | 253 | } 254 | 255 | return false; 256 | 257 | }; 258 | 259 | }(); 260 | 261 | this.dispose = function () { 262 | 263 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 264 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 265 | scope.domElement.removeEventListener( 'pointercancel', onPointerCancel ); 266 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 267 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 268 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 269 | 270 | if ( scope._domElementKeyEvents !== null ) { 271 | 272 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 273 | 274 | } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 275 | 276 | }; // 277 | // internals 278 | // 279 | 280 | 281 | const scope = this; 282 | const STATE = { 283 | NONE: - 1, 284 | ROTATE: 0, 285 | DOLLY: 1, 286 | PAN: 2, 287 | TOUCH_ROTATE: 3, 288 | TOUCH_PAN: 4, 289 | TOUCH_DOLLY_PAN: 5, 290 | TOUCH_DOLLY_ROTATE: 6 291 | }; 292 | let state = STATE.NONE; 293 | const EPS = 0.000001; // current position in spherical coordinates 294 | 295 | const spherical = new THREE.Spherical(); 296 | const sphericalDelta = new THREE.Spherical(); 297 | let scale = 1; 298 | const panOffset = new THREE.Vector3(); 299 | let zoomChanged = false; 300 | const rotateStart = new THREE.Vector2(); 301 | const rotateEnd = new THREE.Vector2(); 302 | const rotateDelta = new THREE.Vector2(); 303 | const panStart = new THREE.Vector2(); 304 | const panEnd = new THREE.Vector2(); 305 | const panDelta = new THREE.Vector2(); 306 | const dollyStart = new THREE.Vector2(); 307 | const dollyEnd = new THREE.Vector2(); 308 | const dollyDelta = new THREE.Vector2(); 309 | const pointers = []; 310 | const pointerPositions = {}; 311 | 312 | function getAutoRotationAngle() { 313 | 314 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 315 | 316 | } 317 | 318 | function getZoomScale() { 319 | 320 | return Math.pow( 0.95, scope.zoomSpeed ); 321 | 322 | } 323 | 324 | function rotateLeft( angle ) { 325 | 326 | sphericalDelta.theta -= angle; 327 | 328 | } 329 | 330 | function rotateUp( angle ) { 331 | 332 | sphericalDelta.phi -= angle; 333 | 334 | } 335 | 336 | const panLeft = function () { 337 | 338 | const v = new THREE.Vector3(); 339 | return function panLeft( distance, objectMatrix ) { 340 | 341 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 342 | 343 | v.multiplyScalar( - distance ); 344 | panOffset.add( v ); 345 | 346 | }; 347 | 348 | }(); 349 | 350 | const panUp = function () { 351 | 352 | const v = new THREE.Vector3(); 353 | return function panUp( distance, objectMatrix ) { 354 | 355 | if ( scope.screenSpacePanning === true ) { 356 | 357 | v.setFromMatrixColumn( objectMatrix, 1 ); 358 | 359 | } else { 360 | 361 | v.setFromMatrixColumn( objectMatrix, 0 ); 362 | v.crossVectors( scope.object.up, v ); 363 | 364 | } 365 | 366 | v.multiplyScalar( distance ); 367 | panOffset.add( v ); 368 | 369 | }; 370 | 371 | }(); // deltaX and deltaY are in pixels; right and down are positive 372 | 373 | 374 | const pan = function () { 375 | 376 | const offset = new THREE.Vector3(); 377 | return function pan( deltaX, deltaY ) { 378 | 379 | const element = scope.domElement; 380 | 381 | if ( scope.object.isPerspectiveCamera ) { 382 | 383 | // perspective 384 | const position = scope.object.position; 385 | offset.copy( position ).sub( scope.target ); 386 | let targetDistance = offset.length(); // half of the fov is center to top of screen 387 | 388 | targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed 389 | 390 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 391 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 392 | 393 | } else if ( scope.object.isOrthographicCamera ) { 394 | 395 | // orthographic 396 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 397 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 398 | 399 | } else { 400 | 401 | // camera neither orthographic nor perspective 402 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 403 | scope.enablePan = false; 404 | 405 | } 406 | 407 | }; 408 | 409 | }(); 410 | 411 | function dollyOut( dollyScale ) { 412 | 413 | if ( scope.object.isPerspectiveCamera ) { 414 | 415 | scale /= dollyScale; 416 | 417 | } else if ( scope.object.isOrthographicCamera ) { 418 | 419 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 420 | scope.object.updateProjectionMatrix(); 421 | zoomChanged = true; 422 | 423 | } else { 424 | 425 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 426 | scope.enableZoom = false; 427 | 428 | } 429 | 430 | } 431 | 432 | function dollyIn( dollyScale ) { 433 | 434 | if ( scope.object.isPerspectiveCamera ) { 435 | 436 | scale *= dollyScale; 437 | 438 | } else if ( scope.object.isOrthographicCamera ) { 439 | 440 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 441 | scope.object.updateProjectionMatrix(); 442 | zoomChanged = true; 443 | 444 | } else { 445 | 446 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 447 | scope.enableZoom = false; 448 | 449 | } 450 | 451 | } // 452 | // event callbacks - update the object state 453 | // 454 | 455 | 456 | function handleMouseDownRotate( event ) { 457 | 458 | rotateStart.set( event.clientX, event.clientY ); 459 | 460 | } 461 | 462 | function handleMouseDownDolly( event ) { 463 | 464 | dollyStart.set( event.clientX, event.clientY ); 465 | 466 | } 467 | 468 | function handleMouseDownPan( event ) { 469 | 470 | panStart.set( event.clientX, event.clientY ); 471 | 472 | } 473 | 474 | function handleMouseMoveRotate( event ) { 475 | 476 | rotateEnd.set( event.clientX, event.clientY ); 477 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 478 | const element = scope.domElement; 479 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 480 | 481 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 482 | rotateStart.copy( rotateEnd ); 483 | scope.update(); 484 | 485 | } 486 | 487 | function handleMouseMoveDolly( event ) { 488 | 489 | dollyEnd.set( event.clientX, event.clientY ); 490 | dollyDelta.subVectors( dollyEnd, dollyStart ); 491 | 492 | if ( dollyDelta.y > 0 ) { 493 | 494 | dollyOut( getZoomScale() ); 495 | 496 | } else if ( dollyDelta.y < 0 ) { 497 | 498 | dollyIn( getZoomScale() ); 499 | 500 | } 501 | 502 | dollyStart.copy( dollyEnd ); 503 | scope.update(); 504 | 505 | } 506 | 507 | function handleMouseMovePan( event ) { 508 | 509 | panEnd.set( event.clientX, event.clientY ); 510 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 511 | pan( panDelta.x, panDelta.y ); 512 | panStart.copy( panEnd ); 513 | scope.update(); 514 | 515 | } 516 | 517 | function handleMouseUp( ) { // no-op 518 | } 519 | 520 | function handleMouseWheel( event ) { 521 | 522 | if ( event.deltaY < 0 ) { 523 | 524 | dollyIn( getZoomScale() ); 525 | 526 | } else if ( event.deltaY > 0 ) { 527 | 528 | dollyOut( getZoomScale() ); 529 | 530 | } 531 | 532 | scope.update(); 533 | 534 | } 535 | 536 | function handleKeyDown( event ) { 537 | 538 | let needsUpdate = false; 539 | 540 | switch ( event.code ) { 541 | 542 | case scope.keys.UP: 543 | pan( 0, scope.keyPanSpeed ); 544 | needsUpdate = true; 545 | break; 546 | 547 | case scope.keys.BOTTOM: 548 | pan( 0, - scope.keyPanSpeed ); 549 | needsUpdate = true; 550 | break; 551 | 552 | case scope.keys.LEFT: 553 | pan( scope.keyPanSpeed, 0 ); 554 | needsUpdate = true; 555 | break; 556 | 557 | case scope.keys.RIGHT: 558 | pan( - scope.keyPanSpeed, 0 ); 559 | needsUpdate = true; 560 | break; 561 | 562 | } 563 | 564 | if ( needsUpdate ) { 565 | 566 | // prevent the browser from scrolling on cursor keys 567 | event.preventDefault(); 568 | scope.update(); 569 | 570 | } 571 | 572 | } 573 | 574 | function handleTouchStartRotate() { 575 | 576 | if ( pointers.length === 1 ) { 577 | 578 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 579 | 580 | } else { 581 | 582 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 583 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 584 | rotateStart.set( x, y ); 585 | 586 | } 587 | 588 | } 589 | 590 | function handleTouchStartPan() { 591 | 592 | if ( pointers.length === 1 ) { 593 | 594 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 595 | 596 | } else { 597 | 598 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 599 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 600 | panStart.set( x, y ); 601 | 602 | } 603 | 604 | } 605 | 606 | function handleTouchStartDolly() { 607 | 608 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 609 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 610 | const distance = Math.sqrt( dx * dx + dy * dy ); 611 | dollyStart.set( 0, distance ); 612 | 613 | } 614 | 615 | function handleTouchStartDollyPan() { 616 | 617 | if ( scope.enableZoom ) handleTouchStartDolly(); 618 | if ( scope.enablePan ) handleTouchStartPan(); 619 | 620 | } 621 | 622 | function handleTouchStartDollyRotate() { 623 | 624 | if ( scope.enableZoom ) handleTouchStartDolly(); 625 | if ( scope.enableRotate ) handleTouchStartRotate(); 626 | 627 | } 628 | 629 | function handleTouchMoveRotate( event ) { 630 | 631 | if ( pointers.length == 1 ) { 632 | 633 | rotateEnd.set( event.pageX, event.pageY ); 634 | 635 | } else { 636 | 637 | const position = getSecondPointerPosition( event ); 638 | const x = 0.5 * ( event.pageX + position.x ); 639 | const y = 0.5 * ( event.pageY + position.y ); 640 | rotateEnd.set( x, y ); 641 | 642 | } 643 | 644 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 645 | const element = scope.domElement; 646 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 647 | 648 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 649 | rotateStart.copy( rotateEnd ); 650 | 651 | } 652 | 653 | function handleTouchMovePan( event ) { 654 | 655 | if ( pointers.length === 1 ) { 656 | 657 | panEnd.set( event.pageX, event.pageY ); 658 | 659 | } else { 660 | 661 | const position = getSecondPointerPosition( event ); 662 | const x = 0.5 * ( event.pageX + position.x ); 663 | const y = 0.5 * ( event.pageY + position.y ); 664 | panEnd.set( x, y ); 665 | 666 | } 667 | 668 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 669 | pan( panDelta.x, panDelta.y ); 670 | panStart.copy( panEnd ); 671 | 672 | } 673 | 674 | function handleTouchMoveDolly( event ) { 675 | 676 | const position = getSecondPointerPosition( event ); 677 | const dx = event.pageX - position.x; 678 | const dy = event.pageY - position.y; 679 | const distance = Math.sqrt( dx * dx + dy * dy ); 680 | dollyEnd.set( 0, distance ); 681 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 682 | dollyOut( dollyDelta.y ); 683 | dollyStart.copy( dollyEnd ); 684 | 685 | } 686 | 687 | function handleTouchMoveDollyPan( event ) { 688 | 689 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 690 | if ( scope.enablePan ) handleTouchMovePan( event ); 691 | 692 | } 693 | 694 | function handleTouchMoveDollyRotate( event ) { 695 | 696 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 697 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 698 | 699 | } 700 | 701 | function handleTouchEnd( ) { // no-op 702 | } // 703 | // event handlers - FSM: listen for events and reset state 704 | // 705 | 706 | 707 | function onPointerDown( event ) { 708 | 709 | if ( scope.enabled === false ) return; 710 | 711 | if ( pointers.length === 0 ) { 712 | 713 | scope.domElement.setPointerCapture( event.pointerId ); 714 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 715 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 716 | 717 | } // 718 | 719 | 720 | addPointer( event ); 721 | 722 | if ( event.pointerType === 'touch' ) { 723 | 724 | onTouchStart( event ); 725 | 726 | } else { 727 | 728 | onMouseDown( event ); 729 | 730 | } 731 | 732 | } 733 | 734 | function onPointerMove( event ) { 735 | 736 | if ( scope.enabled === false ) return; 737 | 738 | if ( event.pointerType === 'touch' ) { 739 | 740 | onTouchMove( event ); 741 | 742 | } else { 743 | 744 | onMouseMove( event ); 745 | 746 | } 747 | 748 | } 749 | 750 | function onPointerUp( event ) { 751 | 752 | if ( scope.enabled === false ) return; 753 | 754 | if ( event.pointerType === 'touch' ) { 755 | 756 | onTouchEnd(); 757 | 758 | } else { 759 | 760 | onMouseUp( event ); 761 | 762 | } 763 | 764 | removePointer( event ); // 765 | 766 | if ( pointers.length === 0 ) { 767 | 768 | scope.domElement.releasePointerCapture( event.pointerId ); 769 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 770 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 771 | 772 | } 773 | 774 | } 775 | 776 | function onPointerCancel( event ) { 777 | 778 | removePointer( event ); 779 | 780 | } 781 | 782 | function onMouseDown( event ) { 783 | 784 | let mouseAction; 785 | 786 | switch ( event.button ) { 787 | 788 | case 0: 789 | mouseAction = scope.mouseButtons.LEFT; 790 | break; 791 | 792 | case 1: 793 | mouseAction = scope.mouseButtons.MIDDLE; 794 | break; 795 | 796 | case 2: 797 | mouseAction = scope.mouseButtons.RIGHT; 798 | break; 799 | 800 | default: 801 | mouseAction = - 1; 802 | 803 | } 804 | 805 | switch ( mouseAction ) { 806 | 807 | case THREE.MOUSE.DOLLY: 808 | if ( scope.enableZoom === false ) return; 809 | handleMouseDownDolly( event ); 810 | state = STATE.DOLLY; 811 | break; 812 | 813 | case THREE.MOUSE.ROTATE: 814 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 815 | 816 | if ( scope.enablePan === false ) return; 817 | handleMouseDownPan( event ); 818 | state = STATE.PAN; 819 | 820 | } else { 821 | 822 | if ( scope.enableRotate === false ) return; 823 | handleMouseDownRotate( event ); 824 | state = STATE.ROTATE; 825 | 826 | } 827 | 828 | break; 829 | 830 | case THREE.MOUSE.PAN: 831 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 832 | 833 | if ( scope.enableRotate === false ) return; 834 | handleMouseDownRotate( event ); 835 | state = STATE.ROTATE; 836 | 837 | } else { 838 | 839 | if ( scope.enablePan === false ) return; 840 | handleMouseDownPan( event ); 841 | state = STATE.PAN; 842 | 843 | } 844 | 845 | break; 846 | 847 | default: 848 | state = STATE.NONE; 849 | 850 | } 851 | 852 | if ( state !== STATE.NONE ) { 853 | 854 | scope.dispatchEvent( _startEvent ); 855 | 856 | } 857 | 858 | } 859 | 860 | function onMouseMove( event ) { 861 | 862 | if ( scope.enabled === false ) return; 863 | 864 | switch ( state ) { 865 | 866 | case STATE.ROTATE: 867 | if ( scope.enableRotate === false ) return; 868 | handleMouseMoveRotate( event ); 869 | break; 870 | 871 | case STATE.DOLLY: 872 | if ( scope.enableZoom === false ) return; 873 | handleMouseMoveDolly( event ); 874 | break; 875 | 876 | case STATE.PAN: 877 | if ( scope.enablePan === false ) return; 878 | handleMouseMovePan( event ); 879 | break; 880 | 881 | } 882 | 883 | } 884 | 885 | function onMouseUp( event ) { 886 | 887 | handleMouseUp( event ); 888 | scope.dispatchEvent( _endEvent ); 889 | state = STATE.NONE; 890 | 891 | } 892 | 893 | function onMouseWheel( event ) { 894 | 895 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE && state !== STATE.ROTATE ) return; 896 | event.preventDefault(); 897 | scope.dispatchEvent( _startEvent ); 898 | handleMouseWheel( event ); 899 | scope.dispatchEvent( _endEvent ); 900 | 901 | } 902 | 903 | function onKeyDown( event ) { 904 | 905 | if ( scope.enabled === false || scope.enablePan === false ) return; 906 | handleKeyDown( event ); 907 | 908 | } 909 | 910 | function onTouchStart( event ) { 911 | 912 | trackPointer( event ); 913 | 914 | switch ( pointers.length ) { 915 | 916 | case 1: 917 | switch ( scope.touches.ONE ) { 918 | 919 | case THREE.TOUCH.ROTATE: 920 | if ( scope.enableRotate === false ) return; 921 | handleTouchStartRotate(); 922 | state = STATE.TOUCH_ROTATE; 923 | break; 924 | 925 | case THREE.TOUCH.PAN: 926 | if ( scope.enablePan === false ) return; 927 | handleTouchStartPan(); 928 | state = STATE.TOUCH_PAN; 929 | break; 930 | 931 | default: 932 | state = STATE.NONE; 933 | 934 | } 935 | 936 | break; 937 | 938 | case 2: 939 | switch ( scope.touches.TWO ) { 940 | 941 | case THREE.TOUCH.DOLLY_PAN: 942 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 943 | handleTouchStartDollyPan(); 944 | state = STATE.TOUCH_DOLLY_PAN; 945 | break; 946 | 947 | case THREE.TOUCH.DOLLY_ROTATE: 948 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 949 | handleTouchStartDollyRotate(); 950 | state = STATE.TOUCH_DOLLY_ROTATE; 951 | break; 952 | 953 | default: 954 | state = STATE.NONE; 955 | 956 | } 957 | 958 | break; 959 | 960 | default: 961 | state = STATE.NONE; 962 | 963 | } 964 | 965 | if ( state !== STATE.NONE ) { 966 | 967 | scope.dispatchEvent( _startEvent ); 968 | 969 | } 970 | 971 | } 972 | 973 | function onTouchMove( event ) { 974 | 975 | trackPointer( event ); 976 | 977 | switch ( state ) { 978 | 979 | case STATE.TOUCH_ROTATE: 980 | if ( scope.enableRotate === false ) return; 981 | handleTouchMoveRotate( event ); 982 | scope.update(); 983 | break; 984 | 985 | case STATE.TOUCH_PAN: 986 | if ( scope.enablePan === false ) return; 987 | handleTouchMovePan( event ); 988 | scope.update(); 989 | break; 990 | 991 | case STATE.TOUCH_DOLLY_PAN: 992 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 993 | handleTouchMoveDollyPan( event ); 994 | scope.update(); 995 | break; 996 | 997 | case STATE.TOUCH_DOLLY_ROTATE: 998 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 999 | handleTouchMoveDollyRotate( event ); 1000 | scope.update(); 1001 | break; 1002 | 1003 | default: 1004 | state = STATE.NONE; 1005 | 1006 | } 1007 | 1008 | } 1009 | 1010 | function onTouchEnd( event ) { 1011 | 1012 | handleTouchEnd( event ); 1013 | scope.dispatchEvent( _endEvent ); 1014 | state = STATE.NONE; 1015 | 1016 | } 1017 | 1018 | function onContextMenu( event ) { 1019 | 1020 | if ( scope.enabled === false ) return; 1021 | event.preventDefault(); 1022 | 1023 | } 1024 | 1025 | function addPointer( event ) { 1026 | 1027 | pointers.push( event ); 1028 | 1029 | } 1030 | 1031 | function removePointer( event ) { 1032 | 1033 | delete pointerPositions[ event.pointerId ]; 1034 | 1035 | for ( let i = 0; i < pointers.length; i ++ ) { 1036 | 1037 | if ( pointers[ i ].pointerId == event.pointerId ) { 1038 | 1039 | pointers.splice( i, 1 ); 1040 | return; 1041 | 1042 | } 1043 | 1044 | } 1045 | 1046 | } 1047 | 1048 | function trackPointer( event ) { 1049 | 1050 | let position = pointerPositions[ event.pointerId ]; 1051 | 1052 | if ( position === undefined ) { 1053 | 1054 | position = new THREE.Vector2(); 1055 | pointerPositions[ event.pointerId ] = position; 1056 | 1057 | } 1058 | 1059 | position.set( event.pageX, event.pageY ); 1060 | 1061 | } 1062 | 1063 | function getSecondPointerPosition( event ) { 1064 | 1065 | const pointer = event.pointerId === pointers[ 0 ].pointerId ? pointers[ 1 ] : pointers[ 0 ]; 1066 | return pointerPositions[ pointer.pointerId ]; 1067 | 1068 | } // 1069 | 1070 | 1071 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1072 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1073 | scope.domElement.addEventListener( 'pointercancel', onPointerCancel ); 1074 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { 1075 | passive: false 1076 | } ); // force an update at start 1077 | 1078 | this.update(); 1079 | 1080 | } 1081 | 1082 | } // This set of controls performs orbiting, dollying (zooming), and panning. 1083 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1084 | // This is very similar to OrbitControls, another set of touch behavior 1085 | // 1086 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1087 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1088 | // Pan - left mouse, or arrow keys / touch: one-finger move 1089 | 1090 | 1091 | class MapControls extends OrbitControls { 1092 | 1093 | constructor( object, domElement ) { 1094 | 1095 | super( object, domElement ); 1096 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1097 | 1098 | this.mouseButtons.LEFT = THREE.MOUSE.PAN; 1099 | this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; 1100 | this.touches.ONE = THREE.TOUCH.PAN; 1101 | this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; 1102 | 1103 | } 1104 | 1105 | } 1106 | 1107 | THREE.MapControls = MapControls; 1108 | THREE.OrbitControls = OrbitControls; 1109 | 1110 | } )(); 1111 | -------------------------------------------------------------------------------- /js/GLTFLoader.js: -------------------------------------------------------------------------------- 1 | ( function () { 2 | 3 | class GLTFLoader extends THREE.Loader { 4 | 5 | constructor( manager ) { 6 | 7 | super( manager ); 8 | this.dracoLoader = null; 9 | this.ktx2Loader = null; 10 | this.meshoptDecoder = null; 11 | this.pluginCallbacks = []; 12 | this.register( function ( parser ) { 13 | 14 | return new GLTFMaterialsClearcoatExtension( parser ); 15 | 16 | } ); 17 | this.register( function ( parser ) { 18 | 19 | return new GLTFTextureBasisUExtension( parser ); 20 | 21 | } ); 22 | this.register( function ( parser ) { 23 | 24 | return new GLTFTextureWebPExtension( parser ); 25 | 26 | } ); 27 | this.register( function ( parser ) { 28 | 29 | return new GLTFMaterialsTransmissionExtension( parser ); 30 | 31 | } ); 32 | this.register( function ( parser ) { 33 | 34 | return new GLTFMaterialsVolumeExtension( parser ); 35 | 36 | } ); 37 | this.register( function ( parser ) { 38 | 39 | return new GLTFMaterialsIorExtension( parser ); 40 | 41 | } ); 42 | this.register( function ( parser ) { 43 | 44 | return new GLTFMaterialsSpecularExtension( parser ); 45 | 46 | } ); 47 | this.register( function ( parser ) { 48 | 49 | return new GLTFLightsExtension( parser ); 50 | 51 | } ); 52 | this.register( function ( parser ) { 53 | 54 | return new GLTFMeshoptCompression( parser ); 55 | 56 | } ); 57 | 58 | } 59 | 60 | load( url, onLoad, onProgress, onError ) { 61 | 62 | const scope = this; 63 | let resourcePath; 64 | 65 | if ( this.resourcePath !== '' ) { 66 | 67 | resourcePath = this.resourcePath; 68 | 69 | } else if ( this.path !== '' ) { 70 | 71 | resourcePath = this.path; 72 | 73 | } else { 74 | 75 | resourcePath = THREE.LoaderUtils.extractUrlBase( url ); 76 | 77 | } // Tells the LoadingManager to track an extra item, which resolves after 78 | // the model is fully loaded. This means the count of items loaded will 79 | // be incorrect, but ensures manager.onLoad() does not fire early. 80 | 81 | 82 | this.manager.itemStart( url ); 83 | 84 | const _onError = function ( e ) { 85 | 86 | if ( onError ) { 87 | 88 | onError( e ); 89 | 90 | } else { 91 | 92 | console.error( e ); 93 | 94 | } 95 | 96 | scope.manager.itemError( url ); 97 | scope.manager.itemEnd( url ); 98 | 99 | }; 100 | 101 | const loader = new THREE.FileLoader( this.manager ); 102 | loader.setPath( this.path ); 103 | loader.setResponseType( 'arraybuffer' ); 104 | loader.setRequestHeader( this.requestHeader ); 105 | loader.setWithCredentials( this.withCredentials ); 106 | loader.load( url, function ( data ) { 107 | 108 | try { 109 | 110 | scope.parse( data, resourcePath, function ( gltf ) { 111 | 112 | onLoad( gltf ); 113 | scope.manager.itemEnd( url ); 114 | 115 | }, _onError ); 116 | 117 | } catch ( e ) { 118 | 119 | _onError( e ); 120 | 121 | } 122 | 123 | }, onProgress, _onError ); 124 | 125 | } 126 | 127 | setDRACOLoader( dracoLoader ) { 128 | 129 | this.dracoLoader = dracoLoader; 130 | return this; 131 | 132 | } 133 | 134 | setDDSLoader() { 135 | 136 | throw new Error( 'THREE.GLTFLoader: "MSFT_texture_dds" no longer supported. Please update to "KHR_texture_basisu".' ); 137 | 138 | } 139 | 140 | setKTX2Loader( ktx2Loader ) { 141 | 142 | this.ktx2Loader = ktx2Loader; 143 | return this; 144 | 145 | } 146 | 147 | setMeshoptDecoder( meshoptDecoder ) { 148 | 149 | this.meshoptDecoder = meshoptDecoder; 150 | return this; 151 | 152 | } 153 | 154 | register( callback ) { 155 | 156 | if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) { 157 | 158 | this.pluginCallbacks.push( callback ); 159 | 160 | } 161 | 162 | return this; 163 | 164 | } 165 | 166 | unregister( callback ) { 167 | 168 | if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) { 169 | 170 | this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 ); 171 | 172 | } 173 | 174 | return this; 175 | 176 | } 177 | 178 | parse( data, path, onLoad, onError ) { 179 | 180 | let content; 181 | const extensions = {}; 182 | const plugins = {}; 183 | 184 | if ( typeof data === 'string' ) { 185 | 186 | content = data; 187 | 188 | } else { 189 | 190 | const magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) ); 191 | 192 | if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) { 193 | 194 | try { 195 | 196 | extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data ); 197 | 198 | } catch ( error ) { 199 | 200 | if ( onError ) onError( error ); 201 | return; 202 | 203 | } 204 | 205 | content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content; 206 | 207 | } else { 208 | 209 | content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) ); 210 | 211 | } 212 | 213 | } 214 | 215 | const json = JSON.parse( content ); 216 | 217 | if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) { 218 | 219 | if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) ); 220 | return; 221 | 222 | } 223 | 224 | const parser = new GLTFParser( json, { 225 | path: path || this.resourcePath || '', 226 | crossOrigin: this.crossOrigin, 227 | requestHeader: this.requestHeader, 228 | manager: this.manager, 229 | ktx2Loader: this.ktx2Loader, 230 | meshoptDecoder: this.meshoptDecoder 231 | } ); 232 | parser.fileLoader.setRequestHeader( this.requestHeader ); 233 | 234 | for ( let i = 0; i < this.pluginCallbacks.length; i ++ ) { 235 | 236 | const plugin = this.pluginCallbacks[ i ]( parser ); 237 | plugins[ plugin.name ] = plugin; // Workaround to avoid determining as unknown extension 238 | // in addUnknownExtensionsToUserData(). 239 | // Remove this workaround if we move all the existing 240 | // extension handlers to plugin system 241 | 242 | extensions[ plugin.name ] = true; 243 | 244 | } 245 | 246 | if ( json.extensionsUsed ) { 247 | 248 | for ( let i = 0; i < json.extensionsUsed.length; ++ i ) { 249 | 250 | const extensionName = json.extensionsUsed[ i ]; 251 | const extensionsRequired = json.extensionsRequired || []; 252 | 253 | switch ( extensionName ) { 254 | 255 | case EXTENSIONS.KHR_MATERIALS_UNLIT: 256 | extensions[ extensionName ] = new GLTFMaterialsUnlitExtension(); 257 | break; 258 | 259 | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 260 | extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension(); 261 | break; 262 | 263 | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: 264 | extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader ); 265 | break; 266 | 267 | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: 268 | extensions[ extensionName ] = new GLTFTextureTransformExtension(); 269 | break; 270 | 271 | case EXTENSIONS.KHR_MESH_QUANTIZATION: 272 | extensions[ extensionName ] = new GLTFMeshQuantizationExtension(); 273 | break; 274 | 275 | default: 276 | if ( extensionsRequired.indexOf( extensionName ) >= 0 && plugins[ extensionName ] === undefined ) { 277 | 278 | console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' ); 279 | 280 | } 281 | 282 | } 283 | 284 | } 285 | 286 | } 287 | 288 | parser.setExtensions( extensions ); 289 | parser.setPlugins( plugins ); 290 | parser.parse( onLoad, onError ); 291 | 292 | } 293 | 294 | } 295 | /* GLTFREGISTRY */ 296 | 297 | 298 | function GLTFRegistry() { 299 | 300 | let objects = {}; 301 | return { 302 | get: function ( key ) { 303 | 304 | return objects[ key ]; 305 | 306 | }, 307 | add: function ( key, object ) { 308 | 309 | objects[ key ] = object; 310 | 311 | }, 312 | remove: function ( key ) { 313 | 314 | delete objects[ key ]; 315 | 316 | }, 317 | removeAll: function () { 318 | 319 | objects = {}; 320 | 321 | } 322 | }; 323 | 324 | } 325 | /*********************************/ 326 | 327 | /********** EXTENSIONS ***********/ 328 | 329 | /*********************************/ 330 | 331 | 332 | const EXTENSIONS = { 333 | KHR_BINARY_GLTF: 'KHR_binary_glTF', 334 | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', 335 | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', 336 | KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', 337 | KHR_MATERIALS_IOR: 'KHR_materials_ior', 338 | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', 339 | KHR_MATERIALS_SPECULAR: 'KHR_materials_specular', 340 | KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission', 341 | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', 342 | KHR_MATERIALS_VOLUME: 'KHR_materials_volume', 343 | KHR_TEXTURE_BASISU: 'KHR_texture_basisu', 344 | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', 345 | KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', 346 | EXT_TEXTURE_WEBP: 'EXT_texture_webp', 347 | EXT_MESHOPT_COMPRESSION: 'EXT_meshopt_compression' 348 | }; 349 | /** 350 | * Punctual Lights Extension 351 | * 352 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual 353 | */ 354 | 355 | class GLTFLightsExtension { 356 | 357 | constructor( parser ) { 358 | 359 | this.parser = parser; 360 | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; // THREE.Object3D instance caches 361 | 362 | this.cache = { 363 | refs: {}, 364 | uses: {} 365 | }; 366 | 367 | } 368 | 369 | _markDefs() { 370 | 371 | const parser = this.parser; 372 | const nodeDefs = this.parser.json.nodes || []; 373 | 374 | for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { 375 | 376 | const nodeDef = nodeDefs[ nodeIndex ]; 377 | 378 | if ( nodeDef.extensions && nodeDef.extensions[ this.name ] && nodeDef.extensions[ this.name ].light !== undefined ) { 379 | 380 | parser._addNodeRef( this.cache, nodeDef.extensions[ this.name ].light ); 381 | 382 | } 383 | 384 | } 385 | 386 | } 387 | 388 | _loadLight( lightIndex ) { 389 | 390 | const parser = this.parser; 391 | const cacheKey = 'light:' + lightIndex; 392 | let dependency = parser.cache.get( cacheKey ); 393 | if ( dependency ) return dependency; 394 | const json = parser.json; 395 | const extensions = json.extensions && json.extensions[ this.name ] || {}; 396 | const lightDefs = extensions.lights || []; 397 | const lightDef = lightDefs[ lightIndex ]; 398 | let lightNode; 399 | const color = new THREE.Color( 0xffffff ); 400 | if ( lightDef.color !== undefined ) color.fromArray( lightDef.color ); 401 | const range = lightDef.range !== undefined ? lightDef.range : 0; 402 | 403 | switch ( lightDef.type ) { 404 | 405 | case 'directional': 406 | lightNode = new THREE.DirectionalLight( color ); 407 | lightNode.target.position.set( 0, 0, - 1 ); 408 | lightNode.add( lightNode.target ); 409 | break; 410 | 411 | case 'point': 412 | lightNode = new THREE.PointLight( color ); 413 | lightNode.distance = range; 414 | break; 415 | 416 | case 'spot': 417 | lightNode = new THREE.SpotLight( color ); 418 | lightNode.distance = range; // Handle spotlight properties. 419 | 420 | lightDef.spot = lightDef.spot || {}; 421 | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; 422 | lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; 423 | lightNode.angle = lightDef.spot.outerConeAngle; 424 | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; 425 | lightNode.target.position.set( 0, 0, - 1 ); 426 | lightNode.add( lightNode.target ); 427 | break; 428 | 429 | default: 430 | throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type ); 431 | 432 | } // Some lights (e.g. spot) default to a position other than the origin. Reset the position 433 | // here, because node-level parsing will only override position if explicitly specified. 434 | 435 | 436 | lightNode.position.set( 0, 0, 0 ); 437 | lightNode.decay = 2; 438 | if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity; 439 | lightNode.name = parser.createUniqueName( lightDef.name || 'light_' + lightIndex ); 440 | dependency = Promise.resolve( lightNode ); 441 | parser.cache.add( cacheKey, dependency ); 442 | return dependency; 443 | 444 | } 445 | 446 | createNodeAttachment( nodeIndex ) { 447 | 448 | const self = this; 449 | const parser = this.parser; 450 | const json = parser.json; 451 | const nodeDef = json.nodes[ nodeIndex ]; 452 | const lightDef = nodeDef.extensions && nodeDef.extensions[ this.name ] || {}; 453 | const lightIndex = lightDef.light; 454 | if ( lightIndex === undefined ) return null; 455 | return this._loadLight( lightIndex ).then( function ( light ) { 456 | 457 | return parser._getNodeRef( self.cache, lightIndex, light ); 458 | 459 | } ); 460 | 461 | } 462 | 463 | } 464 | /** 465 | * Unlit Materials Extension 466 | * 467 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit 468 | */ 469 | 470 | 471 | class GLTFMaterialsUnlitExtension { 472 | 473 | constructor() { 474 | 475 | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; 476 | 477 | } 478 | 479 | getMaterialType() { 480 | 481 | return THREE.MeshBasicMaterial; 482 | 483 | } 484 | 485 | extendParams( materialParams, materialDef, parser ) { 486 | 487 | const pending = []; 488 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 489 | materialParams.opacity = 1.0; 490 | const metallicRoughness = materialDef.pbrMetallicRoughness; 491 | 492 | if ( metallicRoughness ) { 493 | 494 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 495 | 496 | const array = metallicRoughness.baseColorFactor; 497 | materialParams.color.fromArray( array ); 498 | materialParams.opacity = array[ 3 ]; 499 | 500 | } 501 | 502 | if ( metallicRoughness.baseColorTexture !== undefined ) { 503 | 504 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 505 | 506 | } 507 | 508 | } 509 | 510 | return Promise.all( pending ); 511 | 512 | } 513 | 514 | } 515 | /** 516 | * Clearcoat Materials Extension 517 | * 518 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat 519 | */ 520 | 521 | 522 | class GLTFMaterialsClearcoatExtension { 523 | 524 | constructor( parser ) { 525 | 526 | this.parser = parser; 527 | this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; 528 | 529 | } 530 | 531 | getMaterialType( materialIndex ) { 532 | 533 | const parser = this.parser; 534 | const materialDef = parser.json.materials[ materialIndex ]; 535 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 536 | return THREE.MeshPhysicalMaterial; 537 | 538 | } 539 | 540 | extendMaterialParams( materialIndex, materialParams ) { 541 | 542 | const parser = this.parser; 543 | const materialDef = parser.json.materials[ materialIndex ]; 544 | 545 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 546 | 547 | return Promise.resolve(); 548 | 549 | } 550 | 551 | const pending = []; 552 | const extension = materialDef.extensions[ this.name ]; 553 | 554 | if ( extension.clearcoatFactor !== undefined ) { 555 | 556 | materialParams.clearcoat = extension.clearcoatFactor; 557 | 558 | } 559 | 560 | if ( extension.clearcoatTexture !== undefined ) { 561 | 562 | pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) ); 563 | 564 | } 565 | 566 | if ( extension.clearcoatRoughnessFactor !== undefined ) { 567 | 568 | materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; 569 | 570 | } 571 | 572 | if ( extension.clearcoatRoughnessTexture !== undefined ) { 573 | 574 | pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) ); 575 | 576 | } 577 | 578 | if ( extension.clearcoatNormalTexture !== undefined ) { 579 | 580 | pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) ); 581 | 582 | if ( extension.clearcoatNormalTexture.scale !== undefined ) { 583 | 584 | const scale = extension.clearcoatNormalTexture.scale; // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 585 | 586 | materialParams.clearcoatNormalScale = new THREE.Vector2( scale, - scale ); 587 | 588 | } 589 | 590 | } 591 | 592 | return Promise.all( pending ); 593 | 594 | } 595 | 596 | } 597 | /** 598 | * Transmission Materials Extension 599 | * 600 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission 601 | * Draft: https://github.com/KhronosGroup/glTF/pull/1698 602 | */ 603 | 604 | 605 | class GLTFMaterialsTransmissionExtension { 606 | 607 | constructor( parser ) { 608 | 609 | this.parser = parser; 610 | this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION; 611 | 612 | } 613 | 614 | getMaterialType( materialIndex ) { 615 | 616 | const parser = this.parser; 617 | const materialDef = parser.json.materials[ materialIndex ]; 618 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 619 | return THREE.MeshPhysicalMaterial; 620 | 621 | } 622 | 623 | extendMaterialParams( materialIndex, materialParams ) { 624 | 625 | const parser = this.parser; 626 | const materialDef = parser.json.materials[ materialIndex ]; 627 | 628 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 629 | 630 | return Promise.resolve(); 631 | 632 | } 633 | 634 | const pending = []; 635 | const extension = materialDef.extensions[ this.name ]; 636 | 637 | if ( extension.transmissionFactor !== undefined ) { 638 | 639 | materialParams.transmission = extension.transmissionFactor; 640 | 641 | } 642 | 643 | if ( extension.transmissionTexture !== undefined ) { 644 | 645 | pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) ); 646 | 647 | } 648 | 649 | return Promise.all( pending ); 650 | 651 | } 652 | 653 | } 654 | /** 655 | * Materials Volume Extension 656 | * 657 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume 658 | */ 659 | 660 | 661 | class GLTFMaterialsVolumeExtension { 662 | 663 | constructor( parser ) { 664 | 665 | this.parser = parser; 666 | this.name = EXTENSIONS.KHR_MATERIALS_VOLUME; 667 | 668 | } 669 | 670 | getMaterialType( materialIndex ) { 671 | 672 | const parser = this.parser; 673 | const materialDef = parser.json.materials[ materialIndex ]; 674 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 675 | return THREE.MeshPhysicalMaterial; 676 | 677 | } 678 | 679 | extendMaterialParams( materialIndex, materialParams ) { 680 | 681 | const parser = this.parser; 682 | const materialDef = parser.json.materials[ materialIndex ]; 683 | 684 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 685 | 686 | return Promise.resolve(); 687 | 688 | } 689 | 690 | const pending = []; 691 | const extension = materialDef.extensions[ this.name ]; 692 | materialParams.thickness = extension.thicknessFactor !== undefined ? extension.thicknessFactor : 0; 693 | 694 | if ( extension.thicknessTexture !== undefined ) { 695 | 696 | pending.push( parser.assignTexture( materialParams, 'thicknessMap', extension.thicknessTexture ) ); 697 | 698 | } 699 | 700 | materialParams.attenuationDistance = extension.attenuationDistance || 0; 701 | const colorArray = extension.attenuationColor || [ 1, 1, 1 ]; 702 | materialParams.attenuationTint = new THREE.Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] ); 703 | return Promise.all( pending ); 704 | 705 | } 706 | 707 | } 708 | /** 709 | * Materials ior Extension 710 | * 711 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_ior 712 | */ 713 | 714 | 715 | class GLTFMaterialsIorExtension { 716 | 717 | constructor( parser ) { 718 | 719 | this.parser = parser; 720 | this.name = EXTENSIONS.KHR_MATERIALS_IOR; 721 | 722 | } 723 | 724 | getMaterialType( materialIndex ) { 725 | 726 | const parser = this.parser; 727 | const materialDef = parser.json.materials[ materialIndex ]; 728 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 729 | return THREE.MeshPhysicalMaterial; 730 | 731 | } 732 | 733 | extendMaterialParams( materialIndex, materialParams ) { 734 | 735 | const parser = this.parser; 736 | const materialDef = parser.json.materials[ materialIndex ]; 737 | 738 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 739 | 740 | return Promise.resolve(); 741 | 742 | } 743 | 744 | const extension = materialDef.extensions[ this.name ]; 745 | materialParams.ior = extension.ior !== undefined ? extension.ior : 1.5; 746 | return Promise.resolve(); 747 | 748 | } 749 | 750 | } 751 | /** 752 | * Materials specular Extension 753 | * 754 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_specular 755 | */ 756 | 757 | 758 | class GLTFMaterialsSpecularExtension { 759 | 760 | constructor( parser ) { 761 | 762 | this.parser = parser; 763 | this.name = EXTENSIONS.KHR_MATERIALS_SPECULAR; 764 | 765 | } 766 | 767 | getMaterialType( materialIndex ) { 768 | 769 | const parser = this.parser; 770 | const materialDef = parser.json.materials[ materialIndex ]; 771 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null; 772 | return THREE.MeshPhysicalMaterial; 773 | 774 | } 775 | 776 | extendMaterialParams( materialIndex, materialParams ) { 777 | 778 | const parser = this.parser; 779 | const materialDef = parser.json.materials[ materialIndex ]; 780 | 781 | if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) { 782 | 783 | return Promise.resolve(); 784 | 785 | } 786 | 787 | const pending = []; 788 | const extension = materialDef.extensions[ this.name ]; 789 | materialParams.specularIntensity = extension.specularFactor !== undefined ? extension.specularFactor : 1.0; 790 | 791 | if ( extension.specularTexture !== undefined ) { 792 | 793 | pending.push( parser.assignTexture( materialParams, 'specularIntensityMap', extension.specularTexture ) ); 794 | 795 | } 796 | 797 | const colorArray = extension.specularColorFactor || [ 1, 1, 1 ]; 798 | materialParams.specularTint = new THREE.Color( colorArray[ 0 ], colorArray[ 1 ], colorArray[ 2 ] ); 799 | 800 | if ( extension.specularColorTexture !== undefined ) { 801 | 802 | pending.push( parser.assignTexture( materialParams, 'specularTintMap', extension.specularColorTexture ).then( function ( texture ) { 803 | 804 | texture.encoding = THREE.sRGBEncoding; 805 | 806 | } ) ); 807 | 808 | } 809 | 810 | return Promise.all( pending ); 811 | 812 | } 813 | 814 | } 815 | /** 816 | * BasisU THREE.Texture Extension 817 | * 818 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu 819 | */ 820 | 821 | 822 | class GLTFTextureBasisUExtension { 823 | 824 | constructor( parser ) { 825 | 826 | this.parser = parser; 827 | this.name = EXTENSIONS.KHR_TEXTURE_BASISU; 828 | 829 | } 830 | 831 | loadTexture( textureIndex ) { 832 | 833 | const parser = this.parser; 834 | const json = parser.json; 835 | const textureDef = json.textures[ textureIndex ]; 836 | 837 | if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) { 838 | 839 | return null; 840 | 841 | } 842 | 843 | const extension = textureDef.extensions[ this.name ]; 844 | const source = json.images[ extension.source ]; 845 | const loader = parser.options.ktx2Loader; 846 | 847 | if ( ! loader ) { 848 | 849 | if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { 850 | 851 | throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' ); 852 | 853 | } else { 854 | 855 | // Assumes that the extension is optional and that a fallback texture is present 856 | return null; 857 | 858 | } 859 | 860 | } 861 | 862 | return parser.loadTextureImage( textureIndex, source, loader ); 863 | 864 | } 865 | 866 | } 867 | /** 868 | * WebP THREE.Texture Extension 869 | * 870 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp 871 | */ 872 | 873 | 874 | class GLTFTextureWebPExtension { 875 | 876 | constructor( parser ) { 877 | 878 | this.parser = parser; 879 | this.name = EXTENSIONS.EXT_TEXTURE_WEBP; 880 | this.isSupported = null; 881 | 882 | } 883 | 884 | loadTexture( textureIndex ) { 885 | 886 | const name = this.name; 887 | const parser = this.parser; 888 | const json = parser.json; 889 | const textureDef = json.textures[ textureIndex ]; 890 | 891 | if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) { 892 | 893 | return null; 894 | 895 | } 896 | 897 | const extension = textureDef.extensions[ name ]; 898 | const source = json.images[ extension.source ]; 899 | let loader = parser.textureLoader; 900 | 901 | if ( source.uri ) { 902 | 903 | const handler = parser.options.manager.getHandler( source.uri ); 904 | if ( handler !== null ) loader = handler; 905 | 906 | } 907 | 908 | return this.detectSupport().then( function ( isSupported ) { 909 | 910 | if ( isSupported ) return parser.loadTextureImage( textureIndex, source, loader ); 911 | 912 | if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) { 913 | 914 | throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' ); 915 | 916 | } // Fall back to PNG or JPEG. 917 | 918 | 919 | return parser.loadTexture( textureIndex ); 920 | 921 | } ); 922 | 923 | } 924 | 925 | detectSupport() { 926 | 927 | if ( ! this.isSupported ) { 928 | 929 | this.isSupported = new Promise( function ( resolve ) { 930 | 931 | const image = new Image(); // Lossy test image. Support for lossy images doesn't guarantee support for all 932 | // WebP images, unfortunately. 933 | 934 | image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA'; 935 | 936 | image.onload = image.onerror = function () { 937 | 938 | resolve( image.height === 1 ); 939 | 940 | }; 941 | 942 | } ); 943 | 944 | } 945 | 946 | return this.isSupported; 947 | 948 | } 949 | 950 | } 951 | /** 952 | * meshopt BufferView Compression Extension 953 | * 954 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression 955 | */ 956 | 957 | 958 | class GLTFMeshoptCompression { 959 | 960 | constructor( parser ) { 961 | 962 | this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION; 963 | this.parser = parser; 964 | 965 | } 966 | 967 | loadBufferView( index ) { 968 | 969 | const json = this.parser.json; 970 | const bufferView = json.bufferViews[ index ]; 971 | 972 | if ( bufferView.extensions && bufferView.extensions[ this.name ] ) { 973 | 974 | const extensionDef = bufferView.extensions[ this.name ]; 975 | const buffer = this.parser.getDependency( 'buffer', extensionDef.buffer ); 976 | const decoder = this.parser.options.meshoptDecoder; 977 | 978 | if ( ! decoder || ! decoder.supported ) { 979 | 980 | if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) { 981 | 982 | throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' ); 983 | 984 | } else { 985 | 986 | // Assumes that the extension is optional and that fallback buffer data is present 987 | return null; 988 | 989 | } 990 | 991 | } 992 | 993 | return Promise.all( [ buffer, decoder.ready ] ).then( function ( res ) { 994 | 995 | const byteOffset = extensionDef.byteOffset || 0; 996 | const byteLength = extensionDef.byteLength || 0; 997 | const count = extensionDef.count; 998 | const stride = extensionDef.byteStride; 999 | const result = new ArrayBuffer( count * stride ); 1000 | const source = new Uint8Array( res[ 0 ], byteOffset, byteLength ); 1001 | decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter ); 1002 | return result; 1003 | 1004 | } ); 1005 | 1006 | } else { 1007 | 1008 | return null; 1009 | 1010 | } 1011 | 1012 | } 1013 | 1014 | } 1015 | /* BINARY EXTENSION */ 1016 | 1017 | 1018 | const BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; 1019 | const BINARY_EXTENSION_HEADER_LENGTH = 12; 1020 | const BINARY_EXTENSION_CHUNK_TYPES = { 1021 | JSON: 0x4E4F534A, 1022 | BIN: 0x004E4942 1023 | }; 1024 | 1025 | class GLTFBinaryExtension { 1026 | 1027 | constructor( data ) { 1028 | 1029 | this.name = EXTENSIONS.KHR_BINARY_GLTF; 1030 | this.content = null; 1031 | this.body = null; 1032 | const headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH ); 1033 | this.header = { 1034 | magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ), 1035 | version: headerView.getUint32( 4, true ), 1036 | length: headerView.getUint32( 8, true ) 1037 | }; 1038 | 1039 | if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) { 1040 | 1041 | throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' ); 1042 | 1043 | } else if ( this.header.version < 2.0 ) { 1044 | 1045 | throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' ); 1046 | 1047 | } 1048 | 1049 | const chunkContentsLength = this.header.length - BINARY_EXTENSION_HEADER_LENGTH; 1050 | const chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH ); 1051 | let chunkIndex = 0; 1052 | 1053 | while ( chunkIndex < chunkContentsLength ) { 1054 | 1055 | const chunkLength = chunkView.getUint32( chunkIndex, true ); 1056 | chunkIndex += 4; 1057 | const chunkType = chunkView.getUint32( chunkIndex, true ); 1058 | chunkIndex += 4; 1059 | 1060 | if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) { 1061 | 1062 | const contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength ); 1063 | this.content = THREE.LoaderUtils.decodeText( contentArray ); 1064 | 1065 | } else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) { 1066 | 1067 | const byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; 1068 | this.body = data.slice( byteOffset, byteOffset + chunkLength ); 1069 | 1070 | } // Clients must ignore chunks with unknown types. 1071 | 1072 | 1073 | chunkIndex += chunkLength; 1074 | 1075 | } 1076 | 1077 | if ( this.content === null ) { 1078 | 1079 | throw new Error( 'THREE.GLTFLoader: JSON content not found.' ); 1080 | 1081 | } 1082 | 1083 | } 1084 | 1085 | } 1086 | /** 1087 | * DRACO THREE.Mesh Compression Extension 1088 | * 1089 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression 1090 | */ 1091 | 1092 | 1093 | class GLTFDracoMeshCompressionExtension { 1094 | 1095 | constructor( json, dracoLoader ) { 1096 | 1097 | if ( ! dracoLoader ) { 1098 | 1099 | throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' ); 1100 | 1101 | } 1102 | 1103 | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; 1104 | this.json = json; 1105 | this.dracoLoader = dracoLoader; 1106 | this.dracoLoader.preload(); 1107 | 1108 | } 1109 | 1110 | decodePrimitive( primitive, parser ) { 1111 | 1112 | const json = this.json; 1113 | const dracoLoader = this.dracoLoader; 1114 | const bufferViewIndex = primitive.extensions[ this.name ].bufferView; 1115 | const gltfAttributeMap = primitive.extensions[ this.name ].attributes; 1116 | const threeAttributeMap = {}; 1117 | const attributeNormalizedMap = {}; 1118 | const attributeTypeMap = {}; 1119 | 1120 | for ( const attributeName in gltfAttributeMap ) { 1121 | 1122 | const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 1123 | threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ]; 1124 | 1125 | } 1126 | 1127 | for ( const attributeName in primitive.attributes ) { 1128 | 1129 | const threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase(); 1130 | 1131 | if ( gltfAttributeMap[ attributeName ] !== undefined ) { 1132 | 1133 | const accessorDef = json.accessors[ primitive.attributes[ attributeName ] ]; 1134 | const componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; 1135 | attributeTypeMap[ threeAttributeName ] = componentType; 1136 | attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true; 1137 | 1138 | } 1139 | 1140 | } 1141 | 1142 | return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) { 1143 | 1144 | return new Promise( function ( resolve ) { 1145 | 1146 | dracoLoader.decodeDracoFile( bufferView, function ( geometry ) { 1147 | 1148 | for ( const attributeName in geometry.attributes ) { 1149 | 1150 | const attribute = geometry.attributes[ attributeName ]; 1151 | const normalized = attributeNormalizedMap[ attributeName ]; 1152 | if ( normalized !== undefined ) attribute.normalized = normalized; 1153 | 1154 | } 1155 | 1156 | resolve( geometry ); 1157 | 1158 | }, threeAttributeMap, attributeTypeMap ); 1159 | 1160 | } ); 1161 | 1162 | } ); 1163 | 1164 | } 1165 | 1166 | } 1167 | /** 1168 | * THREE.Texture Transform Extension 1169 | * 1170 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform 1171 | */ 1172 | 1173 | 1174 | class GLTFTextureTransformExtension { 1175 | 1176 | constructor() { 1177 | 1178 | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; 1179 | 1180 | } 1181 | 1182 | extendTexture( texture, transform ) { 1183 | 1184 | if ( transform.texCoord !== undefined ) { 1185 | 1186 | console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' ); 1187 | 1188 | } 1189 | 1190 | if ( transform.offset === undefined && transform.rotation === undefined && transform.scale === undefined ) { 1191 | 1192 | // See https://github.com/mrdoob/three.js/issues/21819. 1193 | return texture; 1194 | 1195 | } 1196 | 1197 | texture = texture.clone(); 1198 | 1199 | if ( transform.offset !== undefined ) { 1200 | 1201 | texture.offset.fromArray( transform.offset ); 1202 | 1203 | } 1204 | 1205 | if ( transform.rotation !== undefined ) { 1206 | 1207 | texture.rotation = transform.rotation; 1208 | 1209 | } 1210 | 1211 | if ( transform.scale !== undefined ) { 1212 | 1213 | texture.repeat.fromArray( transform.scale ); 1214 | 1215 | } 1216 | 1217 | texture.needsUpdate = true; 1218 | return texture; 1219 | 1220 | } 1221 | 1222 | } 1223 | /** 1224 | * Specular-Glossiness Extension 1225 | * 1226 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness 1227 | */ 1228 | 1229 | /** 1230 | * A sub class of StandardMaterial with some of the functionality 1231 | * changed via the `onBeforeCompile` callback 1232 | * @pailhead 1233 | */ 1234 | 1235 | 1236 | class GLTFMeshStandardSGMaterial extends THREE.MeshStandardMaterial { 1237 | 1238 | constructor( params ) { 1239 | 1240 | super(); 1241 | this.isGLTFSpecularGlossinessMaterial = true; //various chunks that need replacing 1242 | 1243 | const specularMapParsFragmentChunk = [ '#ifdef USE_SPECULARMAP', ' uniform sampler2D specularMap;', '#endif' ].join( '\n' ); 1244 | const glossinessMapParsFragmentChunk = [ '#ifdef USE_GLOSSINESSMAP', ' uniform sampler2D glossinessMap;', '#endif' ].join( '\n' ); 1245 | const specularMapFragmentChunk = [ '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' ].join( '\n' ); 1246 | const glossinessMapFragmentChunk = [ 'float glossinessFactor = glossiness;', '#ifdef USE_GLOSSINESSMAP', ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', ' glossinessFactor *= texelGlossiness.a;', '#endif' ].join( '\n' ); 1247 | const lightPhysicalFragmentChunk = [ 'PhysicalMaterial material;', 'material.diffuseColor = diffuseColor.rgb * ( 1. - max( specularFactor.r, max( specularFactor.g, specularFactor.b ) ) );', 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', 'material.specularRoughness = max( 1.0 - glossinessFactor, 0.0525 ); // 0.0525 corresponds to the base mip of a 256 cubemap.', 'material.specularRoughness += geometryRoughness;', 'material.specularRoughness = min( material.specularRoughness, 1.0 );', 'material.specularColor = specularFactor;' ].join( '\n' ); 1248 | const uniforms = { 1249 | specular: { 1250 | value: new THREE.Color().setHex( 0xffffff ) 1251 | }, 1252 | glossiness: { 1253 | value: 1 1254 | }, 1255 | specularMap: { 1256 | value: null 1257 | }, 1258 | glossinessMap: { 1259 | value: null 1260 | } 1261 | }; 1262 | this._extraUniforms = uniforms; 1263 | 1264 | this.onBeforeCompile = function ( shader ) { 1265 | 1266 | for ( const uniformName in uniforms ) { 1267 | 1268 | shader.uniforms[ uniformName ] = uniforms[ uniformName ]; 1269 | 1270 | } 1271 | 1272 | shader.fragmentShader = shader.fragmentShader.replace( 'uniform float roughness;', 'uniform vec3 specular;' ).replace( 'uniform float metalness;', 'uniform float glossiness;' ).replace( '#include ', specularMapParsFragmentChunk ).replace( '#include ', glossinessMapParsFragmentChunk ).replace( '#include ', specularMapFragmentChunk ).replace( '#include ', glossinessMapFragmentChunk ).replace( '#include ', lightPhysicalFragmentChunk ); 1273 | 1274 | }; 1275 | 1276 | Object.defineProperties( this, { 1277 | specular: { 1278 | get: function () { 1279 | 1280 | return uniforms.specular.value; 1281 | 1282 | }, 1283 | set: function ( v ) { 1284 | 1285 | uniforms.specular.value = v; 1286 | 1287 | } 1288 | }, 1289 | specularMap: { 1290 | get: function () { 1291 | 1292 | return uniforms.specularMap.value; 1293 | 1294 | }, 1295 | set: function ( v ) { 1296 | 1297 | uniforms.specularMap.value = v; 1298 | 1299 | if ( v ) { 1300 | 1301 | this.defines.USE_SPECULARMAP = ''; // USE_UV is set by the renderer for specular maps 1302 | 1303 | } else { 1304 | 1305 | delete this.defines.USE_SPECULARMAP; 1306 | 1307 | } 1308 | 1309 | } 1310 | }, 1311 | glossiness: { 1312 | get: function () { 1313 | 1314 | return uniforms.glossiness.value; 1315 | 1316 | }, 1317 | set: function ( v ) { 1318 | 1319 | uniforms.glossiness.value = v; 1320 | 1321 | } 1322 | }, 1323 | glossinessMap: { 1324 | get: function () { 1325 | 1326 | return uniforms.glossinessMap.value; 1327 | 1328 | }, 1329 | set: function ( v ) { 1330 | 1331 | uniforms.glossinessMap.value = v; 1332 | 1333 | if ( v ) { 1334 | 1335 | this.defines.USE_GLOSSINESSMAP = ''; 1336 | this.defines.USE_UV = ''; 1337 | 1338 | } else { 1339 | 1340 | delete this.defines.USE_GLOSSINESSMAP; 1341 | delete this.defines.USE_UV; 1342 | 1343 | } 1344 | 1345 | } 1346 | } 1347 | } ); 1348 | delete this.metalness; 1349 | delete this.roughness; 1350 | delete this.metalnessMap; 1351 | delete this.roughnessMap; 1352 | this.setValues( params ); 1353 | 1354 | } 1355 | 1356 | copy( source ) { 1357 | 1358 | super.copy( source ); 1359 | this.specularMap = source.specularMap; 1360 | this.specular.copy( source.specular ); 1361 | this.glossinessMap = source.glossinessMap; 1362 | this.glossiness = source.glossiness; 1363 | delete this.metalness; 1364 | delete this.roughness; 1365 | delete this.metalnessMap; 1366 | delete this.roughnessMap; 1367 | return this; 1368 | 1369 | } 1370 | 1371 | } 1372 | 1373 | class GLTFMaterialsPbrSpecularGlossinessExtension { 1374 | 1375 | constructor() { 1376 | 1377 | this.name = EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS; 1378 | this.specularGlossinessParams = [ 'color', 'map', 'lightMap', 'lightMapIntensity', 'aoMap', 'aoMapIntensity', 'emissive', 'emissiveIntensity', 'emissiveMap', 'bumpMap', 'bumpScale', 'normalMap', 'normalMapType', 'displacementMap', 'displacementScale', 'displacementBias', 'specularMap', 'specular', 'glossinessMap', 'glossiness', 'alphaMap', 'envMap', 'envMapIntensity', 'refractionRatio' ]; 1379 | 1380 | } 1381 | 1382 | getMaterialType() { 1383 | 1384 | return GLTFMeshStandardSGMaterial; 1385 | 1386 | } 1387 | 1388 | extendParams( materialParams, materialDef, parser ) { 1389 | 1390 | const pbrSpecularGlossiness = materialDef.extensions[ this.name ]; 1391 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 1392 | materialParams.opacity = 1.0; 1393 | const pending = []; 1394 | 1395 | if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) { 1396 | 1397 | const array = pbrSpecularGlossiness.diffuseFactor; 1398 | materialParams.color.fromArray( array ); 1399 | materialParams.opacity = array[ 3 ]; 1400 | 1401 | } 1402 | 1403 | if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) { 1404 | 1405 | pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) ); 1406 | 1407 | } 1408 | 1409 | materialParams.emissive = new THREE.Color( 0.0, 0.0, 0.0 ); 1410 | materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; 1411 | materialParams.specular = new THREE.Color( 1.0, 1.0, 1.0 ); 1412 | 1413 | if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) { 1414 | 1415 | materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor ); 1416 | 1417 | } 1418 | 1419 | if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) { 1420 | 1421 | const specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; 1422 | pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) ); 1423 | pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) ); 1424 | 1425 | } 1426 | 1427 | return Promise.all( pending ); 1428 | 1429 | } 1430 | 1431 | createMaterial( materialParams ) { 1432 | 1433 | const material = new GLTFMeshStandardSGMaterial( materialParams ); 1434 | material.fog = true; 1435 | material.color = materialParams.color; 1436 | material.map = materialParams.map === undefined ? null : materialParams.map; 1437 | material.lightMap = null; 1438 | material.lightMapIntensity = 1.0; 1439 | material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; 1440 | material.aoMapIntensity = 1.0; 1441 | material.emissive = materialParams.emissive; 1442 | material.emissiveIntensity = 1.0; 1443 | material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; 1444 | material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; 1445 | material.bumpScale = 1; 1446 | material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap; 1447 | material.normalMapType = THREE.TangentSpaceNormalMap; 1448 | if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale; 1449 | material.displacementMap = null; 1450 | material.displacementScale = 1; 1451 | material.displacementBias = 0; 1452 | material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap; 1453 | material.specular = materialParams.specular; 1454 | material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; 1455 | material.glossiness = materialParams.glossiness; 1456 | material.alphaMap = null; 1457 | material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; 1458 | material.envMapIntensity = 1.0; 1459 | material.refractionRatio = 0.98; 1460 | return material; 1461 | 1462 | } 1463 | 1464 | } 1465 | /** 1466 | * THREE.Mesh Quantization Extension 1467 | * 1468 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization 1469 | */ 1470 | 1471 | 1472 | class GLTFMeshQuantizationExtension { 1473 | 1474 | constructor() { 1475 | 1476 | this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; 1477 | 1478 | } 1479 | 1480 | } 1481 | /*********************************/ 1482 | 1483 | /********** INTERPOLATION ********/ 1484 | 1485 | /*********************************/ 1486 | // Spline Interpolation 1487 | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation 1488 | 1489 | 1490 | class GLTFCubicSplineInterpolant extends THREE.Interpolant { 1491 | 1492 | constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) { 1493 | 1494 | super( parameterPositions, sampleValues, sampleSize, resultBuffer ); 1495 | 1496 | } 1497 | 1498 | copySampleValue_( index ) { 1499 | 1500 | // Copies a sample value to the result buffer. See description of glTF 1501 | // CUBICSPLINE values layout in interpolate_() function below. 1502 | const result = this.resultBuffer, 1503 | values = this.sampleValues, 1504 | valueSize = this.valueSize, 1505 | offset = index * valueSize * 3 + valueSize; 1506 | 1507 | for ( let i = 0; i !== valueSize; i ++ ) { 1508 | 1509 | result[ i ] = values[ offset + i ]; 1510 | 1511 | } 1512 | 1513 | return result; 1514 | 1515 | } 1516 | 1517 | } 1518 | 1519 | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1520 | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1521 | 1522 | GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) { 1523 | 1524 | const result = this.resultBuffer; 1525 | const values = this.sampleValues; 1526 | const stride = this.valueSize; 1527 | const stride2 = stride * 2; 1528 | const stride3 = stride * 3; 1529 | const td = t1 - t0; 1530 | const p = ( t - t0 ) / td; 1531 | const pp = p * p; 1532 | const ppp = pp * p; 1533 | const offset1 = i1 * stride3; 1534 | const offset0 = offset1 - stride3; 1535 | const s2 = - 2 * ppp + 3 * pp; 1536 | const s3 = ppp - pp; 1537 | const s0 = 1 - s2; 1538 | const s1 = s3 - pp + p; // Layout of keyframe output values for CUBICSPLINE animations: 1539 | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] 1540 | 1541 | for ( let i = 0; i !== stride; i ++ ) { 1542 | 1543 | const p0 = values[ offset0 + i + stride ]; // splineVertex_k 1544 | 1545 | const m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k) 1546 | 1547 | const p1 = values[ offset1 + i + stride ]; // splineVertex_k+1 1548 | 1549 | const m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k) 1550 | 1551 | result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; 1552 | 1553 | } 1554 | 1555 | return result; 1556 | 1557 | }; 1558 | /*********************************/ 1559 | 1560 | /********** INTERNALS ************/ 1561 | 1562 | /*********************************/ 1563 | 1564 | /* CONSTANTS */ 1565 | 1566 | 1567 | const WEBGL_CONSTANTS = { 1568 | FLOAT: 5126, 1569 | //FLOAT_MAT2: 35674, 1570 | FLOAT_MAT3: 35675, 1571 | FLOAT_MAT4: 35676, 1572 | FLOAT_VEC2: 35664, 1573 | FLOAT_VEC3: 35665, 1574 | FLOAT_VEC4: 35666, 1575 | LINEAR: 9729, 1576 | REPEAT: 10497, 1577 | SAMPLER_2D: 35678, 1578 | POINTS: 0, 1579 | LINES: 1, 1580 | LINE_LOOP: 2, 1581 | LINE_STRIP: 3, 1582 | TRIANGLES: 4, 1583 | TRIANGLE_STRIP: 5, 1584 | TRIANGLE_FAN: 6, 1585 | UNSIGNED_BYTE: 5121, 1586 | UNSIGNED_SHORT: 5123 1587 | }; 1588 | const WEBGL_COMPONENT_TYPES = { 1589 | 5120: Int8Array, 1590 | 5121: Uint8Array, 1591 | 5122: Int16Array, 1592 | 5123: Uint16Array, 1593 | 5125: Uint32Array, 1594 | 5126: Float32Array 1595 | }; 1596 | const WEBGL_FILTERS = { 1597 | 9728: THREE.NearestFilter, 1598 | 9729: THREE.LinearFilter, 1599 | 9984: THREE.NearestMipmapNearestFilter, 1600 | 9985: THREE.LinearMipmapNearestFilter, 1601 | 9986: THREE.NearestMipmapLinearFilter, 1602 | 9987: THREE.LinearMipmapLinearFilter 1603 | }; 1604 | const WEBGL_WRAPPINGS = { 1605 | 33071: THREE.ClampToEdgeWrapping, 1606 | 33648: THREE.MirroredRepeatWrapping, 1607 | 10497: THREE.RepeatWrapping 1608 | }; 1609 | const WEBGL_TYPE_SIZES = { 1610 | 'SCALAR': 1, 1611 | 'VEC2': 2, 1612 | 'VEC3': 3, 1613 | 'VEC4': 4, 1614 | 'MAT2': 4, 1615 | 'MAT3': 9, 1616 | 'MAT4': 16 1617 | }; 1618 | const ATTRIBUTES = { 1619 | POSITION: 'position', 1620 | NORMAL: 'normal', 1621 | TANGENT: 'tangent', 1622 | TEXCOORD_0: 'uv', 1623 | TEXCOORD_1: 'uv2', 1624 | COLOR_0: 'color', 1625 | WEIGHTS_0: 'skinWeight', 1626 | JOINTS_0: 'skinIndex' 1627 | }; 1628 | const PATH_PROPERTIES = { 1629 | scale: 'scale', 1630 | translation: 'position', 1631 | rotation: 'quaternion', 1632 | weights: 'morphTargetInfluences' 1633 | }; 1634 | const INTERPOLATION = { 1635 | CUBICSPLINE: undefined, 1636 | // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each 1637 | // keyframe track will be initialized with a default interpolation type, then modified. 1638 | LINEAR: THREE.InterpolateLinear, 1639 | STEP: THREE.InterpolateDiscrete 1640 | }; 1641 | const ALPHA_MODES = { 1642 | OPAQUE: 'OPAQUE', 1643 | MASK: 'MASK', 1644 | BLEND: 'BLEND' 1645 | }; 1646 | /* UTILITY FUNCTIONS */ 1647 | 1648 | function resolveURL( url, path ) { 1649 | 1650 | // Invalid URL 1651 | if ( typeof url !== 'string' || url === '' ) return ''; // Host Relative URL 1652 | 1653 | if ( /^https?:\/\//i.test( path ) && /^\//.test( url ) ) { 1654 | 1655 | path = path.replace( /(^https?:\/\/[^\/]+).*/i, '$1' ); 1656 | 1657 | } // Absolute URL http://,https://,// 1658 | 1659 | 1660 | if ( /^(https?:)?\/\//i.test( url ) ) return url; // Data URI 1661 | 1662 | if ( /^data:.*,.*$/i.test( url ) ) return url; // Blob URL 1663 | 1664 | if ( /^blob:.*$/i.test( url ) ) return url; // Relative URL 1665 | 1666 | return path + url; 1667 | 1668 | } 1669 | /** 1670 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 1671 | */ 1672 | 1673 | 1674 | function createDefaultMaterial( cache ) { 1675 | 1676 | if ( cache[ 'DefaultMaterial' ] === undefined ) { 1677 | 1678 | cache[ 'DefaultMaterial' ] = new THREE.MeshStandardMaterial( { 1679 | color: 0xFFFFFF, 1680 | emissive: 0x000000, 1681 | metalness: 1, 1682 | roughness: 1, 1683 | transparent: false, 1684 | depthTest: true, 1685 | side: THREE.FrontSide 1686 | } ); 1687 | 1688 | } 1689 | 1690 | return cache[ 'DefaultMaterial' ]; 1691 | 1692 | } 1693 | 1694 | function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) { 1695 | 1696 | // Add unknown glTF extensions to an object's userData. 1697 | for ( const name in objectDef.extensions ) { 1698 | 1699 | if ( knownExtensions[ name ] === undefined ) { 1700 | 1701 | object.userData.gltfExtensions = object.userData.gltfExtensions || {}; 1702 | object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ]; 1703 | 1704 | } 1705 | 1706 | } 1707 | 1708 | } 1709 | /** 1710 | * @param {Object3D|Material|BufferGeometry} object 1711 | * @param {GLTF.definition} gltfDef 1712 | */ 1713 | 1714 | 1715 | function assignExtrasToUserData( object, gltfDef ) { 1716 | 1717 | if ( gltfDef.extras !== undefined ) { 1718 | 1719 | if ( typeof gltfDef.extras === 'object' ) { 1720 | 1721 | Object.assign( object.userData, gltfDef.extras ); 1722 | 1723 | } else { 1724 | 1725 | console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras ); 1726 | 1727 | } 1728 | 1729 | } 1730 | 1731 | } 1732 | /** 1733 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets 1734 | * 1735 | * @param {BufferGeometry} geometry 1736 | * @param {Array} targets 1737 | * @param {GLTFParser} parser 1738 | * @return {Promise} 1739 | */ 1740 | 1741 | 1742 | function addMorphTargets( geometry, targets, parser ) { 1743 | 1744 | let hasMorphPosition = false; 1745 | let hasMorphNormal = false; 1746 | 1747 | for ( let i = 0, il = targets.length; i < il; i ++ ) { 1748 | 1749 | const target = targets[ i ]; 1750 | if ( target.POSITION !== undefined ) hasMorphPosition = true; 1751 | if ( target.NORMAL !== undefined ) hasMorphNormal = true; 1752 | if ( hasMorphPosition && hasMorphNormal ) break; 1753 | 1754 | } 1755 | 1756 | if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry ); 1757 | const pendingPositionAccessors = []; 1758 | const pendingNormalAccessors = []; 1759 | 1760 | for ( let i = 0, il = targets.length; i < il; i ++ ) { 1761 | 1762 | const target = targets[ i ]; 1763 | 1764 | if ( hasMorphPosition ) { 1765 | 1766 | const pendingAccessor = target.POSITION !== undefined ? parser.getDependency( 'accessor', target.POSITION ) : geometry.attributes.position; 1767 | pendingPositionAccessors.push( pendingAccessor ); 1768 | 1769 | } 1770 | 1771 | if ( hasMorphNormal ) { 1772 | 1773 | const pendingAccessor = target.NORMAL !== undefined ? parser.getDependency( 'accessor', target.NORMAL ) : geometry.attributes.normal; 1774 | pendingNormalAccessors.push( pendingAccessor ); 1775 | 1776 | } 1777 | 1778 | } 1779 | 1780 | return Promise.all( [ Promise.all( pendingPositionAccessors ), Promise.all( pendingNormalAccessors ) ] ).then( function ( accessors ) { 1781 | 1782 | const morphPositions = accessors[ 0 ]; 1783 | const morphNormals = accessors[ 1 ]; 1784 | if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions; 1785 | if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals; 1786 | geometry.morphTargetsRelative = true; 1787 | return geometry; 1788 | 1789 | } ); 1790 | 1791 | } 1792 | /** 1793 | * @param {Mesh} mesh 1794 | * @param {GLTF.Mesh} meshDef 1795 | */ 1796 | 1797 | 1798 | function updateMorphTargets( mesh, meshDef ) { 1799 | 1800 | mesh.updateMorphTargets(); 1801 | 1802 | if ( meshDef.weights !== undefined ) { 1803 | 1804 | for ( let i = 0, il = meshDef.weights.length; i < il; i ++ ) { 1805 | 1806 | mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ]; 1807 | 1808 | } 1809 | 1810 | } // .extras has user-defined data, so check that .extras.targetNames is an array. 1811 | 1812 | 1813 | if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) { 1814 | 1815 | const targetNames = meshDef.extras.targetNames; 1816 | 1817 | if ( mesh.morphTargetInfluences.length === targetNames.length ) { 1818 | 1819 | mesh.morphTargetDictionary = {}; 1820 | 1821 | for ( let i = 0, il = targetNames.length; i < il; i ++ ) { 1822 | 1823 | mesh.morphTargetDictionary[ targetNames[ i ] ] = i; 1824 | 1825 | } 1826 | 1827 | } else { 1828 | 1829 | console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' ); 1830 | 1831 | } 1832 | 1833 | } 1834 | 1835 | } 1836 | 1837 | function createPrimitiveKey( primitiveDef ) { 1838 | 1839 | const dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]; 1840 | let geometryKey; 1841 | 1842 | if ( dracoExtension ) { 1843 | 1844 | geometryKey = 'draco:' + dracoExtension.bufferView + ':' + dracoExtension.indices + ':' + createAttributesKey( dracoExtension.attributes ); 1845 | 1846 | } else { 1847 | 1848 | geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode; 1849 | 1850 | } 1851 | 1852 | return geometryKey; 1853 | 1854 | } 1855 | 1856 | function createAttributesKey( attributes ) { 1857 | 1858 | let attributesKey = ''; 1859 | const keys = Object.keys( attributes ).sort(); 1860 | 1861 | for ( let i = 0, il = keys.length; i < il; i ++ ) { 1862 | 1863 | attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';'; 1864 | 1865 | } 1866 | 1867 | return attributesKey; 1868 | 1869 | } 1870 | 1871 | function getNormalizedComponentScale( constructor ) { 1872 | 1873 | // Reference: 1874 | // https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#encoding-quantized-data 1875 | switch ( constructor ) { 1876 | 1877 | case Int8Array: 1878 | return 1 / 127; 1879 | 1880 | case Uint8Array: 1881 | return 1 / 255; 1882 | 1883 | case Int16Array: 1884 | return 1 / 32767; 1885 | 1886 | case Uint16Array: 1887 | return 1 / 65535; 1888 | 1889 | default: 1890 | throw new Error( 'THREE.GLTFLoader: Unsupported normalized accessor component type.' ); 1891 | 1892 | } 1893 | 1894 | } 1895 | /* GLTF PARSER */ 1896 | 1897 | 1898 | class GLTFParser { 1899 | 1900 | constructor( json = {}, options = {} ) { 1901 | 1902 | this.json = json; 1903 | this.extensions = {}; 1904 | this.plugins = {}; 1905 | this.options = options; // loader object cache 1906 | 1907 | this.cache = new GLTFRegistry(); // associations between Three.js objects and glTF elements 1908 | 1909 | this.associations = new Map(); // THREE.BufferGeometry caching 1910 | 1911 | this.primitiveCache = {}; // THREE.Object3D instance caches 1912 | 1913 | this.meshCache = { 1914 | refs: {}, 1915 | uses: {} 1916 | }; 1917 | this.cameraCache = { 1918 | refs: {}, 1919 | uses: {} 1920 | }; 1921 | this.lightCache = { 1922 | refs: {}, 1923 | uses: {} 1924 | }; 1925 | this.textureCache = {}; // Track node names, to ensure no duplicates 1926 | 1927 | this.nodeNamesUsed = {}; // Use an THREE.ImageBitmapLoader if imageBitmaps are supported. Moves much of the 1928 | // expensive work of uploading a texture to the GPU off the main thread. 1929 | 1930 | if ( typeof createImageBitmap !== 'undefined' && /Firefox/.test( navigator.userAgent ) === false ) { 1931 | 1932 | this.textureLoader = new THREE.ImageBitmapLoader( this.options.manager ); 1933 | 1934 | } else { 1935 | 1936 | this.textureLoader = new THREE.TextureLoader( this.options.manager ); 1937 | 1938 | } 1939 | 1940 | this.textureLoader.setCrossOrigin( this.options.crossOrigin ); 1941 | this.textureLoader.setRequestHeader( this.options.requestHeader ); 1942 | this.fileLoader = new THREE.FileLoader( this.options.manager ); 1943 | this.fileLoader.setResponseType( 'arraybuffer' ); 1944 | 1945 | if ( this.options.crossOrigin === 'use-credentials' ) { 1946 | 1947 | this.fileLoader.setWithCredentials( true ); 1948 | 1949 | } 1950 | 1951 | } 1952 | 1953 | setExtensions( extensions ) { 1954 | 1955 | this.extensions = extensions; 1956 | 1957 | } 1958 | 1959 | setPlugins( plugins ) { 1960 | 1961 | this.plugins = plugins; 1962 | 1963 | } 1964 | 1965 | parse( onLoad, onError ) { 1966 | 1967 | const parser = this; 1968 | const json = this.json; 1969 | const extensions = this.extensions; // Clear the loader cache 1970 | 1971 | this.cache.removeAll(); // Mark the special nodes/meshes in json for efficient parse 1972 | 1973 | this._invokeAll( function ( ext ) { 1974 | 1975 | return ext._markDefs && ext._markDefs(); 1976 | 1977 | } ); 1978 | 1979 | Promise.all( this._invokeAll( function ( ext ) { 1980 | 1981 | return ext.beforeRoot && ext.beforeRoot(); 1982 | 1983 | } ) ).then( function () { 1984 | 1985 | return Promise.all( [ parser.getDependencies( 'scene' ), parser.getDependencies( 'animation' ), parser.getDependencies( 'camera' ) ] ); 1986 | 1987 | } ).then( function ( dependencies ) { 1988 | 1989 | const result = { 1990 | scene: dependencies[ 0 ][ json.scene || 0 ], 1991 | scenes: dependencies[ 0 ], 1992 | animations: dependencies[ 1 ], 1993 | cameras: dependencies[ 2 ], 1994 | asset: json.asset, 1995 | parser: parser, 1996 | userData: {} 1997 | }; 1998 | addUnknownExtensionsToUserData( extensions, result, json ); 1999 | assignExtrasToUserData( result, json ); 2000 | Promise.all( parser._invokeAll( function ( ext ) { 2001 | 2002 | return ext.afterRoot && ext.afterRoot( result ); 2003 | 2004 | } ) ).then( function () { 2005 | 2006 | onLoad( result ); 2007 | 2008 | } ); 2009 | 2010 | } ).catch( onError ); 2011 | 2012 | } 2013 | /** 2014 | * Marks the special nodes/meshes in json for efficient parse. 2015 | */ 2016 | 2017 | 2018 | _markDefs() { 2019 | 2020 | const nodeDefs = this.json.nodes || []; 2021 | const skinDefs = this.json.skins || []; 2022 | const meshDefs = this.json.meshes || []; // Nothing in the node definition indicates whether it is a THREE.Bone or an 2023 | // THREE.Object3D. Use the skins' joint references to mark bones. 2024 | 2025 | for ( let skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) { 2026 | 2027 | const joints = skinDefs[ skinIndex ].joints; 2028 | 2029 | for ( let i = 0, il = joints.length; i < il; i ++ ) { 2030 | 2031 | nodeDefs[ joints[ i ] ].isBone = true; 2032 | 2033 | } 2034 | 2035 | } // Iterate over all nodes, marking references to shared resources, 2036 | // as well as skeleton joints. 2037 | 2038 | 2039 | for ( let nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) { 2040 | 2041 | const nodeDef = nodeDefs[ nodeIndex ]; 2042 | 2043 | if ( nodeDef.mesh !== undefined ) { 2044 | 2045 | this._addNodeRef( this.meshCache, nodeDef.mesh ); // Nothing in the mesh definition indicates whether it is 2046 | // a THREE.SkinnedMesh or THREE.Mesh. Use the node's mesh reference 2047 | // to mark THREE.SkinnedMesh if node has skin. 2048 | 2049 | 2050 | if ( nodeDef.skin !== undefined ) { 2051 | 2052 | meshDefs[ nodeDef.mesh ].isSkinnedMesh = true; 2053 | 2054 | } 2055 | 2056 | } 2057 | 2058 | if ( nodeDef.camera !== undefined ) { 2059 | 2060 | this._addNodeRef( this.cameraCache, nodeDef.camera ); 2061 | 2062 | } 2063 | 2064 | } 2065 | 2066 | } 2067 | /** 2068 | * Counts references to shared node / THREE.Object3D resources. These resources 2069 | * can be reused, or "instantiated", at multiple nodes in the scene 2070 | * hierarchy. THREE.Mesh, Camera, and Light instances are instantiated and must 2071 | * be marked. Non-scenegraph resources (like Materials, Geometries, and 2072 | * Textures) can be reused directly and are not marked here. 2073 | * 2074 | * Example: CesiumMilkTruck sample model reuses "Wheel" meshes. 2075 | */ 2076 | 2077 | 2078 | _addNodeRef( cache, index ) { 2079 | 2080 | if ( index === undefined ) return; 2081 | 2082 | if ( cache.refs[ index ] === undefined ) { 2083 | 2084 | cache.refs[ index ] = cache.uses[ index ] = 0; 2085 | 2086 | } 2087 | 2088 | cache.refs[ index ] ++; 2089 | 2090 | } 2091 | /** Returns a reference to a shared resource, cloning it if necessary. */ 2092 | 2093 | 2094 | _getNodeRef( cache, index, object ) { 2095 | 2096 | if ( cache.refs[ index ] <= 1 ) return object; 2097 | const ref = object.clone(); 2098 | ref.name += '_instance_' + cache.uses[ index ] ++; 2099 | return ref; 2100 | 2101 | } 2102 | 2103 | _invokeOne( func ) { 2104 | 2105 | const extensions = Object.values( this.plugins ); 2106 | extensions.push( this ); 2107 | 2108 | for ( let i = 0; i < extensions.length; i ++ ) { 2109 | 2110 | const result = func( extensions[ i ] ); 2111 | if ( result ) return result; 2112 | 2113 | } 2114 | 2115 | return null; 2116 | 2117 | } 2118 | 2119 | _invokeAll( func ) { 2120 | 2121 | const extensions = Object.values( this.plugins ); 2122 | extensions.unshift( this ); 2123 | const pending = []; 2124 | 2125 | for ( let i = 0; i < extensions.length; i ++ ) { 2126 | 2127 | const result = func( extensions[ i ] ); 2128 | if ( result ) pending.push( result ); 2129 | 2130 | } 2131 | 2132 | return pending; 2133 | 2134 | } 2135 | /** 2136 | * Requests the specified dependency asynchronously, with caching. 2137 | * @param {string} type 2138 | * @param {number} index 2139 | * @return {Promise} 2140 | */ 2141 | 2142 | 2143 | getDependency( type, index ) { 2144 | 2145 | const cacheKey = type + ':' + index; 2146 | let dependency = this.cache.get( cacheKey ); 2147 | 2148 | if ( ! dependency ) { 2149 | 2150 | switch ( type ) { 2151 | 2152 | case 'scene': 2153 | dependency = this.loadScene( index ); 2154 | break; 2155 | 2156 | case 'node': 2157 | dependency = this.loadNode( index ); 2158 | break; 2159 | 2160 | case 'mesh': 2161 | dependency = this._invokeOne( function ( ext ) { 2162 | 2163 | return ext.loadMesh && ext.loadMesh( index ); 2164 | 2165 | } ); 2166 | break; 2167 | 2168 | case 'accessor': 2169 | dependency = this.loadAccessor( index ); 2170 | break; 2171 | 2172 | case 'bufferView': 2173 | dependency = this._invokeOne( function ( ext ) { 2174 | 2175 | return ext.loadBufferView && ext.loadBufferView( index ); 2176 | 2177 | } ); 2178 | break; 2179 | 2180 | case 'buffer': 2181 | dependency = this.loadBuffer( index ); 2182 | break; 2183 | 2184 | case 'material': 2185 | dependency = this._invokeOne( function ( ext ) { 2186 | 2187 | return ext.loadMaterial && ext.loadMaterial( index ); 2188 | 2189 | } ); 2190 | break; 2191 | 2192 | case 'texture': 2193 | dependency = this._invokeOne( function ( ext ) { 2194 | 2195 | return ext.loadTexture && ext.loadTexture( index ); 2196 | 2197 | } ); 2198 | break; 2199 | 2200 | case 'skin': 2201 | dependency = this.loadSkin( index ); 2202 | break; 2203 | 2204 | case 'animation': 2205 | dependency = this.loadAnimation( index ); 2206 | break; 2207 | 2208 | case 'camera': 2209 | dependency = this.loadCamera( index ); 2210 | break; 2211 | 2212 | default: 2213 | throw new Error( 'Unknown type: ' + type ); 2214 | 2215 | } 2216 | 2217 | this.cache.add( cacheKey, dependency ); 2218 | 2219 | } 2220 | 2221 | return dependency; 2222 | 2223 | } 2224 | /** 2225 | * Requests all dependencies of the specified type asynchronously, with caching. 2226 | * @param {string} type 2227 | * @return {Promise>} 2228 | */ 2229 | 2230 | 2231 | getDependencies( type ) { 2232 | 2233 | let dependencies = this.cache.get( type ); 2234 | 2235 | if ( ! dependencies ) { 2236 | 2237 | const parser = this; 2238 | const defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || []; 2239 | dependencies = Promise.all( defs.map( function ( def, index ) { 2240 | 2241 | return parser.getDependency( type, index ); 2242 | 2243 | } ) ); 2244 | this.cache.add( type, dependencies ); 2245 | 2246 | } 2247 | 2248 | return dependencies; 2249 | 2250 | } 2251 | /** 2252 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 2253 | * @param {number} bufferIndex 2254 | * @return {Promise} 2255 | */ 2256 | 2257 | 2258 | loadBuffer( bufferIndex ) { 2259 | 2260 | const bufferDef = this.json.buffers[ bufferIndex ]; 2261 | const loader = this.fileLoader; 2262 | 2263 | if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) { 2264 | 2265 | throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' ); 2266 | 2267 | } // If present, GLB container is required to be the first buffer. 2268 | 2269 | 2270 | if ( bufferDef.uri === undefined && bufferIndex === 0 ) { 2271 | 2272 | return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body ); 2273 | 2274 | } 2275 | 2276 | const options = this.options; 2277 | return new Promise( function ( resolve, reject ) { 2278 | 2279 | loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () { 2280 | 2281 | reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) ); 2282 | 2283 | } ); 2284 | 2285 | } ); 2286 | 2287 | } 2288 | /** 2289 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 2290 | * @param {number} bufferViewIndex 2291 | * @return {Promise} 2292 | */ 2293 | 2294 | 2295 | loadBufferView( bufferViewIndex ) { 2296 | 2297 | const bufferViewDef = this.json.bufferViews[ bufferViewIndex ]; 2298 | return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) { 2299 | 2300 | const byteLength = bufferViewDef.byteLength || 0; 2301 | const byteOffset = bufferViewDef.byteOffset || 0; 2302 | return buffer.slice( byteOffset, byteOffset + byteLength ); 2303 | 2304 | } ); 2305 | 2306 | } 2307 | /** 2308 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors 2309 | * @param {number} accessorIndex 2310 | * @return {Promise} 2311 | */ 2312 | 2313 | 2314 | loadAccessor( accessorIndex ) { 2315 | 2316 | const parser = this; 2317 | const json = this.json; 2318 | const accessorDef = this.json.accessors[ accessorIndex ]; 2319 | 2320 | if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) { 2321 | 2322 | // Ignore empty accessors, which may be used to declare runtime 2323 | // information about attributes coming from another source (e.g. Draco 2324 | // compression extension). 2325 | return Promise.resolve( null ); 2326 | 2327 | } 2328 | 2329 | const pendingBufferViews = []; 2330 | 2331 | if ( accessorDef.bufferView !== undefined ) { 2332 | 2333 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) ); 2334 | 2335 | } else { 2336 | 2337 | pendingBufferViews.push( null ); 2338 | 2339 | } 2340 | 2341 | if ( accessorDef.sparse !== undefined ) { 2342 | 2343 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) ); 2344 | pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) ); 2345 | 2346 | } 2347 | 2348 | return Promise.all( pendingBufferViews ).then( function ( bufferViews ) { 2349 | 2350 | const bufferView = bufferViews[ 0 ]; 2351 | const itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ]; 2352 | const TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ]; // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. 2353 | 2354 | const elementBytes = TypedArray.BYTES_PER_ELEMENT; 2355 | const itemBytes = elementBytes * itemSize; 2356 | const byteOffset = accessorDef.byteOffset || 0; 2357 | const byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined; 2358 | const normalized = accessorDef.normalized === true; 2359 | let array, bufferAttribute; // The buffer is not interleaved if the stride is the item size in bytes. 2360 | 2361 | if ( byteStride && byteStride !== itemBytes ) { 2362 | 2363 | // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own THREE.InterleavedBuffer 2364 | // This makes sure that IBA.count reflects accessor.count properly 2365 | const ibSlice = Math.floor( byteOffset / byteStride ); 2366 | const ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; 2367 | let ib = parser.cache.get( ibCacheKey ); 2368 | 2369 | if ( ! ib ) { 2370 | 2371 | array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes ); // Integer parameters to IB/IBA are in array elements, not bytes. 2372 | 2373 | ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes ); 2374 | parser.cache.add( ibCacheKey, ib ); 2375 | 2376 | } 2377 | 2378 | bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, byteOffset % byteStride / elementBytes, normalized ); 2379 | 2380 | } else { 2381 | 2382 | if ( bufferView === null ) { 2383 | 2384 | array = new TypedArray( accessorDef.count * itemSize ); 2385 | 2386 | } else { 2387 | 2388 | array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize ); 2389 | 2390 | } 2391 | 2392 | bufferAttribute = new THREE.BufferAttribute( array, itemSize, normalized ); 2393 | 2394 | } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors 2395 | 2396 | 2397 | if ( accessorDef.sparse !== undefined ) { 2398 | 2399 | const itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; 2400 | const TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ]; 2401 | const byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; 2402 | const byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; 2403 | const sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ); 2404 | const sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize ); 2405 | 2406 | if ( bufferView !== null ) { 2407 | 2408 | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. 2409 | bufferAttribute = new THREE.BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ); 2410 | 2411 | } 2412 | 2413 | for ( let i = 0, il = sparseIndices.length; i < il; i ++ ) { 2414 | 2415 | const index = sparseIndices[ i ]; 2416 | bufferAttribute.setX( index, sparseValues[ i * itemSize ] ); 2417 | if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] ); 2418 | if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] ); 2419 | if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] ); 2420 | if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse THREE.BufferAttribute.' ); 2421 | 2422 | } 2423 | 2424 | } 2425 | 2426 | return bufferAttribute; 2427 | 2428 | } ); 2429 | 2430 | } 2431 | /** 2432 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures 2433 | * @param {number} textureIndex 2434 | * @return {Promise} 2435 | */ 2436 | 2437 | 2438 | loadTexture( textureIndex ) { 2439 | 2440 | const json = this.json; 2441 | const options = this.options; 2442 | const textureDef = json.textures[ textureIndex ]; 2443 | const source = json.images[ textureDef.source ]; 2444 | let loader = this.textureLoader; 2445 | 2446 | if ( source.uri ) { 2447 | 2448 | const handler = options.manager.getHandler( source.uri ); 2449 | if ( handler !== null ) loader = handler; 2450 | 2451 | } 2452 | 2453 | return this.loadTextureImage( textureIndex, source, loader ); 2454 | 2455 | } 2456 | 2457 | loadTextureImage( textureIndex, source, loader ) { 2458 | 2459 | const parser = this; 2460 | const json = this.json; 2461 | const options = this.options; 2462 | const textureDef = json.textures[ textureIndex ]; 2463 | const cacheKey = ( source.uri || source.bufferView ) + ':' + textureDef.sampler; 2464 | 2465 | if ( this.textureCache[ cacheKey ] ) { 2466 | 2467 | // See https://github.com/mrdoob/three.js/issues/21559. 2468 | return this.textureCache[ cacheKey ]; 2469 | 2470 | } 2471 | 2472 | const URL = self.URL || self.webkitURL; 2473 | let sourceURI = source.uri || ''; 2474 | let isObjectURL = false; 2475 | let hasAlpha = true; 2476 | const isJPEG = sourceURI.search( /\.jpe?g($|\?)/i ) > 0 || sourceURI.search( /^data\:image\/jpeg/ ) === 0; 2477 | if ( source.mimeType === 'image/jpeg' || isJPEG ) hasAlpha = false; 2478 | 2479 | if ( source.bufferView !== undefined ) { 2480 | 2481 | // Load binary image data from bufferView, if provided. 2482 | sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) { 2483 | 2484 | if ( source.mimeType === 'image/png' ) { 2485 | 2486 | // Inspect the PNG 'IHDR' chunk to determine whether the image could have an 2487 | // alpha channel. This check is conservative — the image could have an alpha 2488 | // channel with all values == 1, and the indexed type (colorType == 3) only 2489 | // sometimes contains alpha. 2490 | // 2491 | // https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header 2492 | const colorType = new DataView( bufferView, 25, 1 ).getUint8( 0, false ); 2493 | hasAlpha = colorType === 6 || colorType === 4 || colorType === 3; 2494 | 2495 | } 2496 | 2497 | isObjectURL = true; 2498 | const blob = new Blob( [ bufferView ], { 2499 | type: source.mimeType 2500 | } ); 2501 | sourceURI = URL.createObjectURL( blob ); 2502 | return sourceURI; 2503 | 2504 | } ); 2505 | 2506 | } else if ( source.uri === undefined ) { 2507 | 2508 | throw new Error( 'THREE.GLTFLoader: Image ' + textureIndex + ' is missing URI and bufferView' ); 2509 | 2510 | } 2511 | 2512 | const promise = Promise.resolve( sourceURI ).then( function ( sourceURI ) { 2513 | 2514 | return new Promise( function ( resolve, reject ) { 2515 | 2516 | let onLoad = resolve; 2517 | 2518 | if ( loader.isImageBitmapLoader === true ) { 2519 | 2520 | onLoad = function ( imageBitmap ) { 2521 | 2522 | const texture = new THREE.Texture( imageBitmap ); 2523 | texture.needsUpdate = true; 2524 | resolve( texture ); 2525 | 2526 | }; 2527 | 2528 | } 2529 | 2530 | loader.load( resolveURL( sourceURI, options.path ), onLoad, undefined, reject ); 2531 | 2532 | } ); 2533 | 2534 | } ).then( function ( texture ) { 2535 | 2536 | // Clean up resources and configure THREE.Texture. 2537 | if ( isObjectURL === true ) { 2538 | 2539 | URL.revokeObjectURL( sourceURI ); 2540 | 2541 | } 2542 | 2543 | texture.flipY = false; 2544 | if ( textureDef.name ) texture.name = textureDef.name; // When there is definitely no alpha channel in the texture, set THREE.RGBFormat to save space. 2545 | 2546 | if ( ! hasAlpha ) texture.format = THREE.RGBFormat; 2547 | const samplers = json.samplers || {}; 2548 | const sampler = samplers[ textureDef.sampler ] || {}; 2549 | texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || THREE.LinearFilter; 2550 | texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || THREE.LinearMipmapLinearFilter; 2551 | texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || THREE.RepeatWrapping; 2552 | texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || THREE.RepeatWrapping; 2553 | parser.associations.set( texture, { 2554 | type: 'textures', 2555 | index: textureIndex 2556 | } ); 2557 | return texture; 2558 | 2559 | } ).catch( function () { 2560 | 2561 | console.error( 'THREE.GLTFLoader: Couldn\'t load texture', sourceURI ); 2562 | return null; 2563 | 2564 | } ); 2565 | this.textureCache[ cacheKey ] = promise; 2566 | return promise; 2567 | 2568 | } 2569 | /** 2570 | * Asynchronously assigns a texture to the given material parameters. 2571 | * @param {Object} materialParams 2572 | * @param {string} mapName 2573 | * @param {Object} mapDef 2574 | * @return {Promise} 2575 | */ 2576 | 2577 | 2578 | assignTexture( materialParams, mapName, mapDef ) { 2579 | 2580 | const parser = this; 2581 | return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) { 2582 | 2583 | // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured 2584 | // However, we will copy UV set 0 to UV set 1 on demand for aoMap 2585 | if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) { 2586 | 2587 | console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' ); 2588 | 2589 | } 2590 | 2591 | if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) { 2592 | 2593 | const transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined; 2594 | 2595 | if ( transform ) { 2596 | 2597 | const gltfReference = parser.associations.get( texture ); 2598 | texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform ); 2599 | parser.associations.set( texture, gltfReference ); 2600 | 2601 | } 2602 | 2603 | } 2604 | 2605 | materialParams[ mapName ] = texture; 2606 | return texture; 2607 | 2608 | } ); 2609 | 2610 | } 2611 | /** 2612 | * Assigns final material to a THREE.Mesh, THREE.Line, or THREE.Points instance. The instance 2613 | * already has a material (generated from the glTF material options alone) 2614 | * but reuse of the same glTF material may require multiple threejs materials 2615 | * to accommodate different primitive types, defines, etc. New materials will 2616 | * be created if necessary, and reused from a cache. 2617 | * @param {Object3D} mesh THREE.Mesh, THREE.Line, or THREE.Points instance. 2618 | */ 2619 | 2620 | 2621 | assignFinalMaterial( mesh ) { 2622 | 2623 | const geometry = mesh.geometry; 2624 | let material = mesh.material; 2625 | const useVertexTangents = geometry.attributes.tangent !== undefined; 2626 | const useVertexColors = geometry.attributes.color !== undefined; 2627 | const useFlatShading = geometry.attributes.normal === undefined; 2628 | 2629 | if ( mesh.isPoints ) { 2630 | 2631 | const cacheKey = 'PointsMaterial:' + material.uuid; 2632 | let pointsMaterial = this.cache.get( cacheKey ); 2633 | 2634 | if ( ! pointsMaterial ) { 2635 | 2636 | pointsMaterial = new THREE.PointsMaterial(); 2637 | THREE.Material.prototype.copy.call( pointsMaterial, material ); 2638 | pointsMaterial.color.copy( material.color ); 2639 | pointsMaterial.map = material.map; 2640 | pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px 2641 | 2642 | this.cache.add( cacheKey, pointsMaterial ); 2643 | 2644 | } 2645 | 2646 | material = pointsMaterial; 2647 | 2648 | } else if ( mesh.isLine ) { 2649 | 2650 | const cacheKey = 'LineBasicMaterial:' + material.uuid; 2651 | let lineMaterial = this.cache.get( cacheKey ); 2652 | 2653 | if ( ! lineMaterial ) { 2654 | 2655 | lineMaterial = new THREE.LineBasicMaterial(); 2656 | THREE.Material.prototype.copy.call( lineMaterial, material ); 2657 | lineMaterial.color.copy( material.color ); 2658 | this.cache.add( cacheKey, lineMaterial ); 2659 | 2660 | } 2661 | 2662 | material = lineMaterial; 2663 | 2664 | } // Clone the material if it will be modified 2665 | 2666 | 2667 | if ( useVertexTangents || useVertexColors || useFlatShading ) { 2668 | 2669 | let cacheKey = 'ClonedMaterial:' + material.uuid + ':'; 2670 | if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:'; 2671 | if ( useVertexTangents ) cacheKey += 'vertex-tangents:'; 2672 | if ( useVertexColors ) cacheKey += 'vertex-colors:'; 2673 | if ( useFlatShading ) cacheKey += 'flat-shading:'; 2674 | let cachedMaterial = this.cache.get( cacheKey ); 2675 | 2676 | if ( ! cachedMaterial ) { 2677 | 2678 | cachedMaterial = material.clone(); 2679 | if ( useVertexColors ) cachedMaterial.vertexColors = true; 2680 | if ( useFlatShading ) cachedMaterial.flatShading = true; 2681 | 2682 | if ( useVertexTangents ) { 2683 | 2684 | // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 2685 | if ( cachedMaterial.normalScale ) cachedMaterial.normalScale.y *= - 1; 2686 | if ( cachedMaterial.clearcoatNormalScale ) cachedMaterial.clearcoatNormalScale.y *= - 1; 2687 | 2688 | } 2689 | 2690 | this.cache.add( cacheKey, cachedMaterial ); 2691 | this.associations.set( cachedMaterial, this.associations.get( material ) ); 2692 | 2693 | } 2694 | 2695 | material = cachedMaterial; 2696 | 2697 | } // workarounds for mesh and geometry 2698 | 2699 | 2700 | if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) { 2701 | 2702 | geometry.setAttribute( 'uv2', geometry.attributes.uv ); 2703 | 2704 | } 2705 | 2706 | mesh.material = material; 2707 | 2708 | } 2709 | 2710 | getMaterialType( ) { 2711 | 2712 | return THREE.MeshStandardMaterial; 2713 | 2714 | } 2715 | /** 2716 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials 2717 | * @param {number} materialIndex 2718 | * @return {Promise} 2719 | */ 2720 | 2721 | 2722 | loadMaterial( materialIndex ) { 2723 | 2724 | const parser = this; 2725 | const json = this.json; 2726 | const extensions = this.extensions; 2727 | const materialDef = json.materials[ materialIndex ]; 2728 | let materialType; 2729 | const materialParams = {}; 2730 | const materialExtensions = materialDef.extensions || {}; 2731 | const pending = []; 2732 | 2733 | if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) { 2734 | 2735 | const sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ]; 2736 | materialType = sgExtension.getMaterialType(); 2737 | pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) ); 2738 | 2739 | } else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) { 2740 | 2741 | const kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ]; 2742 | materialType = kmuExtension.getMaterialType(); 2743 | pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) ); 2744 | 2745 | } else { 2746 | 2747 | // Specification: 2748 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 2749 | const metallicRoughness = materialDef.pbrMetallicRoughness || {}; 2750 | materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 ); 2751 | materialParams.opacity = 1.0; 2752 | 2753 | if ( Array.isArray( metallicRoughness.baseColorFactor ) ) { 2754 | 2755 | const array = metallicRoughness.baseColorFactor; 2756 | materialParams.color.fromArray( array ); 2757 | materialParams.opacity = array[ 3 ]; 2758 | 2759 | } 2760 | 2761 | if ( metallicRoughness.baseColorTexture !== undefined ) { 2762 | 2763 | pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) ); 2764 | 2765 | } 2766 | 2767 | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; 2768 | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; 2769 | 2770 | if ( metallicRoughness.metallicRoughnessTexture !== undefined ) { 2771 | 2772 | pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2773 | pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) ); 2774 | 2775 | } 2776 | 2777 | materialType = this._invokeOne( function ( ext ) { 2778 | 2779 | return ext.getMaterialType && ext.getMaterialType( materialIndex ); 2780 | 2781 | } ); 2782 | pending.push( Promise.all( this._invokeAll( function ( ext ) { 2783 | 2784 | return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams ); 2785 | 2786 | } ) ) ); 2787 | 2788 | } 2789 | 2790 | if ( materialDef.doubleSided === true ) { 2791 | 2792 | materialParams.side = THREE.DoubleSide; 2793 | 2794 | } 2795 | 2796 | const alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; 2797 | 2798 | if ( alphaMode === ALPHA_MODES.BLEND ) { 2799 | 2800 | materialParams.transparent = true; // See: https://github.com/mrdoob/three.js/issues/17706 2801 | 2802 | materialParams.depthWrite = false; 2803 | 2804 | } else { 2805 | 2806 | materialParams.transparent = false; 2807 | 2808 | if ( alphaMode === ALPHA_MODES.MASK ) { 2809 | 2810 | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; 2811 | 2812 | } 2813 | 2814 | } 2815 | 2816 | if ( materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2817 | 2818 | pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) ); // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 2819 | 2820 | materialParams.normalScale = new THREE.Vector2( 1, - 1 ); 2821 | 2822 | if ( materialDef.normalTexture.scale !== undefined ) { 2823 | 2824 | materialParams.normalScale.set( materialDef.normalTexture.scale, - materialDef.normalTexture.scale ); 2825 | 2826 | } 2827 | 2828 | } 2829 | 2830 | if ( materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2831 | 2832 | pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) ); 2833 | 2834 | if ( materialDef.occlusionTexture.strength !== undefined ) { 2835 | 2836 | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; 2837 | 2838 | } 2839 | 2840 | } 2841 | 2842 | if ( materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2843 | 2844 | materialParams.emissive = new THREE.Color().fromArray( materialDef.emissiveFactor ); 2845 | 2846 | } 2847 | 2848 | if ( materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) { 2849 | 2850 | pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) ); 2851 | 2852 | } 2853 | 2854 | return Promise.all( pending ).then( function () { 2855 | 2856 | let material; 2857 | 2858 | if ( materialType === GLTFMeshStandardSGMaterial ) { 2859 | 2860 | material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams ); 2861 | 2862 | } else { 2863 | 2864 | material = new materialType( materialParams ); 2865 | 2866 | } 2867 | 2868 | if ( materialDef.name ) material.name = materialDef.name; // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. 2869 | 2870 | if ( material.map ) material.map.encoding = THREE.sRGBEncoding; 2871 | if ( material.emissiveMap ) material.emissiveMap.encoding = THREE.sRGBEncoding; 2872 | assignExtrasToUserData( material, materialDef ); 2873 | parser.associations.set( material, { 2874 | type: 'materials', 2875 | index: materialIndex 2876 | } ); 2877 | if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); 2878 | return material; 2879 | 2880 | } ); 2881 | 2882 | } 2883 | /** When THREE.Object3D instances are targeted by animation, they need unique names. */ 2884 | 2885 | 2886 | createUniqueName( originalName ) { 2887 | 2888 | const sanitizedName = THREE.PropertyBinding.sanitizeNodeName( originalName || '' ); 2889 | let name = sanitizedName; 2890 | 2891 | for ( let i = 1; this.nodeNamesUsed[ name ]; ++ i ) { 2892 | 2893 | name = sanitizedName + '_' + i; 2894 | 2895 | } 2896 | 2897 | this.nodeNamesUsed[ name ] = true; 2898 | return name; 2899 | 2900 | } 2901 | /** 2902 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry 2903 | * 2904 | * Creates BufferGeometries from primitives. 2905 | * 2906 | * @param {Array} primitives 2907 | * @return {Promise>} 2908 | */ 2909 | 2910 | 2911 | loadGeometries( primitives ) { 2912 | 2913 | const parser = this; 2914 | const extensions = this.extensions; 2915 | const cache = this.primitiveCache; 2916 | 2917 | function createDracoPrimitive( primitive ) { 2918 | 2919 | return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ].decodePrimitive( primitive, parser ).then( function ( geometry ) { 2920 | 2921 | return addPrimitiveAttributes( geometry, primitive, parser ); 2922 | 2923 | } ); 2924 | 2925 | } 2926 | 2927 | const pending = []; 2928 | 2929 | for ( let i = 0, il = primitives.length; i < il; i ++ ) { 2930 | 2931 | const primitive = primitives[ i ]; 2932 | const cacheKey = createPrimitiveKey( primitive ); // See if we've already created this geometry 2933 | 2934 | const cached = cache[ cacheKey ]; 2935 | 2936 | if ( cached ) { 2937 | 2938 | // Use the cached geometry if it exists 2939 | pending.push( cached.promise ); 2940 | 2941 | } else { 2942 | 2943 | let geometryPromise; 2944 | 2945 | if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) { 2946 | 2947 | // Use DRACO geometry if available 2948 | geometryPromise = createDracoPrimitive( primitive ); 2949 | 2950 | } else { 2951 | 2952 | // Otherwise create a new geometry 2953 | geometryPromise = addPrimitiveAttributes( new THREE.BufferGeometry(), primitive, parser ); 2954 | 2955 | } // Cache this geometry 2956 | 2957 | 2958 | cache[ cacheKey ] = { 2959 | primitive: primitive, 2960 | promise: geometryPromise 2961 | }; 2962 | pending.push( geometryPromise ); 2963 | 2964 | } 2965 | 2966 | } 2967 | 2968 | return Promise.all( pending ); 2969 | 2970 | } 2971 | /** 2972 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes 2973 | * @param {number} meshIndex 2974 | * @return {Promise} 2975 | */ 2976 | 2977 | 2978 | loadMesh( meshIndex ) { 2979 | 2980 | const parser = this; 2981 | const json = this.json; 2982 | const extensions = this.extensions; 2983 | const meshDef = json.meshes[ meshIndex ]; 2984 | const primitives = meshDef.primitives; 2985 | const pending = []; 2986 | 2987 | for ( let i = 0, il = primitives.length; i < il; i ++ ) { 2988 | 2989 | const material = primitives[ i ].material === undefined ? createDefaultMaterial( this.cache ) : this.getDependency( 'material', primitives[ i ].material ); 2990 | pending.push( material ); 2991 | 2992 | } 2993 | 2994 | pending.push( parser.loadGeometries( primitives ) ); 2995 | return Promise.all( pending ).then( function ( results ) { 2996 | 2997 | const materials = results.slice( 0, results.length - 1 ); 2998 | const geometries = results[ results.length - 1 ]; 2999 | const meshes = []; 3000 | 3001 | for ( let i = 0, il = geometries.length; i < il; i ++ ) { 3002 | 3003 | const geometry = geometries[ i ]; 3004 | const primitive = primitives[ i ]; // 1. create THREE.Mesh 3005 | 3006 | let mesh; 3007 | const material = materials[ i ]; 3008 | 3009 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || primitive.mode === undefined ) { 3010 | 3011 | // .isSkinnedMesh isn't in glTF spec. See ._markDefs() 3012 | mesh = meshDef.isSkinnedMesh === true ? new THREE.SkinnedMesh( geometry, material ) : new THREE.Mesh( geometry, material ); 3013 | 3014 | if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) { 3015 | 3016 | // we normalize floating point skin weight array to fix malformed assets (see #15319) 3017 | // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs 3018 | mesh.normalizeSkinWeights(); 3019 | 3020 | } 3021 | 3022 | if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) { 3023 | 3024 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, THREE.TriangleStripDrawMode ); 3025 | 3026 | } else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) { 3027 | 3028 | mesh.geometry = toTrianglesDrawMode( mesh.geometry, THREE.TriangleFanDrawMode ); 3029 | 3030 | } 3031 | 3032 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) { 3033 | 3034 | mesh = new THREE.LineSegments( geometry, material ); 3035 | 3036 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) { 3037 | 3038 | mesh = new THREE.Line( geometry, material ); 3039 | 3040 | } else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) { 3041 | 3042 | mesh = new THREE.LineLoop( geometry, material ); 3043 | 3044 | } else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) { 3045 | 3046 | mesh = new THREE.Points( geometry, material ); 3047 | 3048 | } else { 3049 | 3050 | throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode ); 3051 | 3052 | } 3053 | 3054 | if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) { 3055 | 3056 | updateMorphTargets( mesh, meshDef ); 3057 | 3058 | } 3059 | 3060 | mesh.name = parser.createUniqueName( meshDef.name || 'mesh_' + meshIndex ); 3061 | assignExtrasToUserData( mesh, meshDef ); 3062 | if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive ); 3063 | parser.assignFinalMaterial( mesh ); 3064 | meshes.push( mesh ); 3065 | 3066 | } 3067 | 3068 | if ( meshes.length === 1 ) { 3069 | 3070 | return meshes[ 0 ]; 3071 | 3072 | } 3073 | 3074 | const group = new THREE.Group(); 3075 | 3076 | for ( let i = 0, il = meshes.length; i < il; i ++ ) { 3077 | 3078 | group.add( meshes[ i ] ); 3079 | 3080 | } 3081 | 3082 | return group; 3083 | 3084 | } ); 3085 | 3086 | } 3087 | /** 3088 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras 3089 | * @param {number} cameraIndex 3090 | * @return {Promise} 3091 | */ 3092 | 3093 | 3094 | loadCamera( cameraIndex ) { 3095 | 3096 | let camera; 3097 | const cameraDef = this.json.cameras[ cameraIndex ]; 3098 | const params = cameraDef[ cameraDef.type ]; 3099 | 3100 | if ( ! params ) { 3101 | 3102 | console.warn( 'THREE.GLTFLoader: Missing camera parameters.' ); 3103 | return; 3104 | 3105 | } 3106 | 3107 | if ( cameraDef.type === 'perspective' ) { 3108 | 3109 | camera = new THREE.PerspectiveCamera( THREE.MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); 3110 | 3111 | } else if ( cameraDef.type === 'orthographic' ) { 3112 | 3113 | camera = new THREE.OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar ); 3114 | 3115 | } 3116 | 3117 | if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name ); 3118 | assignExtrasToUserData( camera, cameraDef ); 3119 | return Promise.resolve( camera ); 3120 | 3121 | } 3122 | /** 3123 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins 3124 | * @param {number} skinIndex 3125 | * @return {Promise} 3126 | */ 3127 | 3128 | 3129 | loadSkin( skinIndex ) { 3130 | 3131 | const skinDef = this.json.skins[ skinIndex ]; 3132 | const skinEntry = { 3133 | joints: skinDef.joints 3134 | }; 3135 | 3136 | if ( skinDef.inverseBindMatrices === undefined ) { 3137 | 3138 | return Promise.resolve( skinEntry ); 3139 | 3140 | } 3141 | 3142 | return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) { 3143 | 3144 | skinEntry.inverseBindMatrices = accessor; 3145 | return skinEntry; 3146 | 3147 | } ); 3148 | 3149 | } 3150 | /** 3151 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations 3152 | * @param {number} animationIndex 3153 | * @return {Promise} 3154 | */ 3155 | 3156 | 3157 | loadAnimation( animationIndex ) { 3158 | 3159 | const json = this.json; 3160 | const animationDef = json.animations[ animationIndex ]; 3161 | const pendingNodes = []; 3162 | const pendingInputAccessors = []; 3163 | const pendingOutputAccessors = []; 3164 | const pendingSamplers = []; 3165 | const pendingTargets = []; 3166 | 3167 | for ( let i = 0, il = animationDef.channels.length; i < il; i ++ ) { 3168 | 3169 | const channel = animationDef.channels[ i ]; 3170 | const sampler = animationDef.samplers[ channel.sampler ]; 3171 | const target = channel.target; 3172 | const name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. 3173 | 3174 | const input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input; 3175 | const output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output; 3176 | pendingNodes.push( this.getDependency( 'node', name ) ); 3177 | pendingInputAccessors.push( this.getDependency( 'accessor', input ) ); 3178 | pendingOutputAccessors.push( this.getDependency( 'accessor', output ) ); 3179 | pendingSamplers.push( sampler ); 3180 | pendingTargets.push( target ); 3181 | 3182 | } 3183 | 3184 | return Promise.all( [ Promise.all( pendingNodes ), Promise.all( pendingInputAccessors ), Promise.all( pendingOutputAccessors ), Promise.all( pendingSamplers ), Promise.all( pendingTargets ) ] ).then( function ( dependencies ) { 3185 | 3186 | const nodes = dependencies[ 0 ]; 3187 | const inputAccessors = dependencies[ 1 ]; 3188 | const outputAccessors = dependencies[ 2 ]; 3189 | const samplers = dependencies[ 3 ]; 3190 | const targets = dependencies[ 4 ]; 3191 | const tracks = []; 3192 | 3193 | for ( let i = 0, il = nodes.length; i < il; i ++ ) { 3194 | 3195 | const node = nodes[ i ]; 3196 | const inputAccessor = inputAccessors[ i ]; 3197 | const outputAccessor = outputAccessors[ i ]; 3198 | const sampler = samplers[ i ]; 3199 | const target = targets[ i ]; 3200 | if ( node === undefined ) continue; 3201 | node.updateMatrix(); 3202 | node.matrixAutoUpdate = true; 3203 | let TypedKeyframeTrack; 3204 | 3205 | switch ( PATH_PROPERTIES[ target.path ] ) { 3206 | 3207 | case PATH_PROPERTIES.weights: 3208 | TypedKeyframeTrack = THREE.NumberKeyframeTrack; 3209 | break; 3210 | 3211 | case PATH_PROPERTIES.rotation: 3212 | TypedKeyframeTrack = THREE.QuaternionKeyframeTrack; 3213 | break; 3214 | 3215 | case PATH_PROPERTIES.position: 3216 | case PATH_PROPERTIES.scale: 3217 | default: 3218 | TypedKeyframeTrack = THREE.VectorKeyframeTrack; 3219 | break; 3220 | 3221 | } 3222 | 3223 | const targetName = node.name ? node.name : node.uuid; 3224 | const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear; 3225 | const targetNames = []; 3226 | 3227 | if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) { 3228 | 3229 | // Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh. 3230 | node.traverse( function ( object ) { 3231 | 3232 | if ( object.isMesh === true && object.morphTargetInfluences ) { 3233 | 3234 | targetNames.push( object.name ? object.name : object.uuid ); 3235 | 3236 | } 3237 | 3238 | } ); 3239 | 3240 | } else { 3241 | 3242 | targetNames.push( targetName ); 3243 | 3244 | } 3245 | 3246 | let outputArray = outputAccessor.array; 3247 | 3248 | if ( outputAccessor.normalized ) { 3249 | 3250 | const scale = getNormalizedComponentScale( outputArray.constructor ); 3251 | const scaled = new Float32Array( outputArray.length ); 3252 | 3253 | for ( let j = 0, jl = outputArray.length; j < jl; j ++ ) { 3254 | 3255 | scaled[ j ] = outputArray[ j ] * scale; 3256 | 3257 | } 3258 | 3259 | outputArray = scaled; 3260 | 3261 | } 3262 | 3263 | for ( let j = 0, jl = targetNames.length; j < jl; j ++ ) { 3264 | 3265 | const track = new TypedKeyframeTrack( targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ], inputAccessor.array, outputArray, interpolation ); // Override interpolation with custom factory method. 3266 | 3267 | if ( sampler.interpolation === 'CUBICSPLINE' ) { 3268 | 3269 | track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) { 3270 | 3271 | // A CUBICSPLINE keyframe in glTF has three output values for each input value, 3272 | // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() 3273 | // must be divided by three to get the interpolant's sampleSize argument. 3274 | return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result ); 3275 | 3276 | }; // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. 3277 | 3278 | 3279 | track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; 3280 | 3281 | } 3282 | 3283 | tracks.push( track ); 3284 | 3285 | } 3286 | 3287 | } 3288 | 3289 | const name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; 3290 | return new THREE.AnimationClip( name, undefined, tracks ); 3291 | 3292 | } ); 3293 | 3294 | } 3295 | 3296 | createNodeMesh( nodeIndex ) { 3297 | 3298 | const json = this.json; 3299 | const parser = this; 3300 | const nodeDef = json.nodes[ nodeIndex ]; 3301 | if ( nodeDef.mesh === undefined ) return null; 3302 | return parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) { 3303 | 3304 | const node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh ); // if weights are provided on the node, override weights on the mesh. 3305 | 3306 | 3307 | if ( nodeDef.weights !== undefined ) { 3308 | 3309 | node.traverse( function ( o ) { 3310 | 3311 | if ( ! o.isMesh ) return; 3312 | 3313 | for ( let i = 0, il = nodeDef.weights.length; i < il; i ++ ) { 3314 | 3315 | o.morphTargetInfluences[ i ] = nodeDef.weights[ i ]; 3316 | 3317 | } 3318 | 3319 | } ); 3320 | 3321 | } 3322 | 3323 | return node; 3324 | 3325 | } ); 3326 | 3327 | } 3328 | /** 3329 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy 3330 | * @param {number} nodeIndex 3331 | * @return {Promise} 3332 | */ 3333 | 3334 | 3335 | loadNode( nodeIndex ) { 3336 | 3337 | const json = this.json; 3338 | const extensions = this.extensions; 3339 | const parser = this; 3340 | const nodeDef = json.nodes[ nodeIndex ]; // reserve node's name before its dependencies, so the root has the intended name. 3341 | 3342 | const nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : ''; 3343 | return function () { 3344 | 3345 | const pending = []; 3346 | 3347 | const meshPromise = parser._invokeOne( function ( ext ) { 3348 | 3349 | return ext.createNodeMesh && ext.createNodeMesh( nodeIndex ); 3350 | 3351 | } ); 3352 | 3353 | if ( meshPromise ) { 3354 | 3355 | pending.push( meshPromise ); 3356 | 3357 | } 3358 | 3359 | if ( nodeDef.camera !== undefined ) { 3360 | 3361 | pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) { 3362 | 3363 | return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera ); 3364 | 3365 | } ) ); 3366 | 3367 | } 3368 | 3369 | parser._invokeAll( function ( ext ) { 3370 | 3371 | return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex ); 3372 | 3373 | } ).forEach( function ( promise ) { 3374 | 3375 | pending.push( promise ); 3376 | 3377 | } ); 3378 | 3379 | return Promise.all( pending ); 3380 | 3381 | }().then( function ( objects ) { 3382 | 3383 | let node; // .isBone isn't in glTF spec. See ._markDefs 3384 | 3385 | if ( nodeDef.isBone === true ) { 3386 | 3387 | node = new THREE.Bone(); 3388 | 3389 | } else if ( objects.length > 1 ) { 3390 | 3391 | node = new THREE.Group(); 3392 | 3393 | } else if ( objects.length === 1 ) { 3394 | 3395 | node = objects[ 0 ]; 3396 | 3397 | } else { 3398 | 3399 | node = new THREE.Object3D(); 3400 | 3401 | } 3402 | 3403 | if ( node !== objects[ 0 ] ) { 3404 | 3405 | for ( let i = 0, il = objects.length; i < il; i ++ ) { 3406 | 3407 | node.add( objects[ i ] ); 3408 | 3409 | } 3410 | 3411 | } 3412 | 3413 | if ( nodeDef.name ) { 3414 | 3415 | node.userData.name = nodeDef.name; 3416 | node.name = nodeName; 3417 | 3418 | } 3419 | 3420 | assignExtrasToUserData( node, nodeDef ); 3421 | if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef ); 3422 | 3423 | if ( nodeDef.matrix !== undefined ) { 3424 | 3425 | const matrix = new THREE.Matrix4(); 3426 | matrix.fromArray( nodeDef.matrix ); 3427 | node.applyMatrix4( matrix ); 3428 | 3429 | } else { 3430 | 3431 | if ( nodeDef.translation !== undefined ) { 3432 | 3433 | node.position.fromArray( nodeDef.translation ); 3434 | 3435 | } 3436 | 3437 | if ( nodeDef.rotation !== undefined ) { 3438 | 3439 | node.quaternion.fromArray( nodeDef.rotation ); 3440 | 3441 | } 3442 | 3443 | if ( nodeDef.scale !== undefined ) { 3444 | 3445 | node.scale.fromArray( nodeDef.scale ); 3446 | 3447 | } 3448 | 3449 | } 3450 | 3451 | parser.associations.set( node, { 3452 | type: 'nodes', 3453 | index: nodeIndex 3454 | } ); 3455 | return node; 3456 | 3457 | } ); 3458 | 3459 | } 3460 | /** 3461 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes 3462 | * @param {number} sceneIndex 3463 | * @return {Promise} 3464 | */ 3465 | 3466 | 3467 | loadScene( sceneIndex ) { 3468 | 3469 | const json = this.json; 3470 | const extensions = this.extensions; 3471 | const sceneDef = this.json.scenes[ sceneIndex ]; 3472 | const parser = this; // THREE.Loader returns THREE.Group, not Scene. 3473 | // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 3474 | 3475 | const scene = new THREE.Group(); 3476 | if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name ); 3477 | assignExtrasToUserData( scene, sceneDef ); 3478 | if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef ); 3479 | const nodeIds = sceneDef.nodes || []; 3480 | const pending = []; 3481 | 3482 | for ( let i = 0, il = nodeIds.length; i < il; i ++ ) { 3483 | 3484 | pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) ); 3485 | 3486 | } 3487 | 3488 | return Promise.all( pending ).then( function () { 3489 | 3490 | return scene; 3491 | 3492 | } ); 3493 | 3494 | } 3495 | 3496 | } 3497 | 3498 | function buildNodeHierachy( nodeId, parentObject, json, parser ) { 3499 | 3500 | const nodeDef = json.nodes[ nodeId ]; 3501 | return parser.getDependency( 'node', nodeId ).then( function ( node ) { 3502 | 3503 | if ( nodeDef.skin === undefined ) return node; // build skeleton here as well 3504 | 3505 | let skinEntry; 3506 | return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) { 3507 | 3508 | skinEntry = skin; 3509 | const pendingJoints = []; 3510 | 3511 | for ( let i = 0, il = skinEntry.joints.length; i < il; i ++ ) { 3512 | 3513 | pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) ); 3514 | 3515 | } 3516 | 3517 | return Promise.all( pendingJoints ); 3518 | 3519 | } ).then( function ( jointNodes ) { 3520 | 3521 | node.traverse( function ( mesh ) { 3522 | 3523 | if ( ! mesh.isMesh ) return; 3524 | const bones = []; 3525 | const boneInverses = []; 3526 | 3527 | for ( let j = 0, jl = jointNodes.length; j < jl; j ++ ) { 3528 | 3529 | const jointNode = jointNodes[ j ]; 3530 | 3531 | if ( jointNode ) { 3532 | 3533 | bones.push( jointNode ); 3534 | const mat = new THREE.Matrix4(); 3535 | 3536 | if ( skinEntry.inverseBindMatrices !== undefined ) { 3537 | 3538 | mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 ); 3539 | 3540 | } 3541 | 3542 | boneInverses.push( mat ); 3543 | 3544 | } else { 3545 | 3546 | console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] ); 3547 | 3548 | } 3549 | 3550 | } 3551 | 3552 | mesh.bind( new THREE.Skeleton( bones, boneInverses ), mesh.matrixWorld ); 3553 | 3554 | } ); 3555 | return node; 3556 | 3557 | } ); 3558 | 3559 | } ).then( function ( node ) { 3560 | 3561 | // build node hierachy 3562 | parentObject.add( node ); 3563 | const pending = []; 3564 | 3565 | if ( nodeDef.children ) { 3566 | 3567 | const children = nodeDef.children; 3568 | 3569 | for ( let i = 0, il = children.length; i < il; i ++ ) { 3570 | 3571 | const child = children[ i ]; 3572 | pending.push( buildNodeHierachy( child, node, json, parser ) ); 3573 | 3574 | } 3575 | 3576 | } 3577 | 3578 | return Promise.all( pending ); 3579 | 3580 | } ); 3581 | 3582 | } 3583 | /** 3584 | * @param {BufferGeometry} geometry 3585 | * @param {GLTF.Primitive} primitiveDef 3586 | * @param {GLTFParser} parser 3587 | */ 3588 | 3589 | 3590 | function computeBounds( geometry, primitiveDef, parser ) { 3591 | 3592 | const attributes = primitiveDef.attributes; 3593 | const box = new THREE.Box3(); 3594 | 3595 | if ( attributes.POSITION !== undefined ) { 3596 | 3597 | const accessor = parser.json.accessors[ attributes.POSITION ]; 3598 | const min = accessor.min; 3599 | const max = accessor.max; // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 3600 | 3601 | if ( min !== undefined && max !== undefined ) { 3602 | 3603 | box.set( new THREE.Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ), new THREE.Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) ); 3604 | 3605 | if ( accessor.normalized ) { 3606 | 3607 | const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); 3608 | box.min.multiplyScalar( boxScale ); 3609 | box.max.multiplyScalar( boxScale ); 3610 | 3611 | } 3612 | 3613 | } else { 3614 | 3615 | console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); 3616 | return; 3617 | 3618 | } 3619 | 3620 | } else { 3621 | 3622 | return; 3623 | 3624 | } 3625 | 3626 | const targets = primitiveDef.targets; 3627 | 3628 | if ( targets !== undefined ) { 3629 | 3630 | const maxDisplacement = new THREE.Vector3(); 3631 | const vector = new THREE.Vector3(); 3632 | 3633 | for ( let i = 0, il = targets.length; i < il; i ++ ) { 3634 | 3635 | const target = targets[ i ]; 3636 | 3637 | if ( target.POSITION !== undefined ) { 3638 | 3639 | const accessor = parser.json.accessors[ target.POSITION ]; 3640 | const min = accessor.min; 3641 | const max = accessor.max; // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 3642 | 3643 | if ( min !== undefined && max !== undefined ) { 3644 | 3645 | // we need to get max of absolute components because target weight is [-1,1] 3646 | vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) ); 3647 | vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) ); 3648 | vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) ); 3649 | 3650 | if ( accessor.normalized ) { 3651 | 3652 | const boxScale = getNormalizedComponentScale( WEBGL_COMPONENT_TYPES[ accessor.componentType ] ); 3653 | vector.multiplyScalar( boxScale ); 3654 | 3655 | } // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative 3656 | // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets 3657 | // are used to implement key-frame animations and as such only two are active at a time - this results in very large 3658 | // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. 3659 | 3660 | 3661 | maxDisplacement.max( vector ); 3662 | 3663 | } else { 3664 | 3665 | console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' ); 3666 | 3667 | } 3668 | 3669 | } 3670 | 3671 | } // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. 3672 | 3673 | 3674 | box.expandByVector( maxDisplacement ); 3675 | 3676 | } 3677 | 3678 | geometry.boundingBox = box; 3679 | const sphere = new THREE.Sphere(); 3680 | box.getCenter( sphere.center ); 3681 | sphere.radius = box.min.distanceTo( box.max ) / 2; 3682 | geometry.boundingSphere = sphere; 3683 | 3684 | } 3685 | /** 3686 | * @param {BufferGeometry} geometry 3687 | * @param {GLTF.Primitive} primitiveDef 3688 | * @param {GLTFParser} parser 3689 | * @return {Promise} 3690 | */ 3691 | 3692 | 3693 | function addPrimitiveAttributes( geometry, primitiveDef, parser ) { 3694 | 3695 | const attributes = primitiveDef.attributes; 3696 | const pending = []; 3697 | 3698 | function assignAttributeAccessor( accessorIndex, attributeName ) { 3699 | 3700 | return parser.getDependency( 'accessor', accessorIndex ).then( function ( accessor ) { 3701 | 3702 | geometry.setAttribute( attributeName, accessor ); 3703 | 3704 | } ); 3705 | 3706 | } 3707 | 3708 | for ( const gltfAttributeName in attributes ) { 3709 | 3710 | const threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase(); // Skip attributes already provided by e.g. Draco extension. 3711 | 3712 | if ( threeAttributeName in geometry.attributes ) continue; 3713 | pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) ); 3714 | 3715 | } 3716 | 3717 | if ( primitiveDef.indices !== undefined && ! geometry.index ) { 3718 | 3719 | const accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) { 3720 | 3721 | geometry.setIndex( accessor ); 3722 | 3723 | } ); 3724 | pending.push( accessor ); 3725 | 3726 | } 3727 | 3728 | assignExtrasToUserData( geometry, primitiveDef ); 3729 | computeBounds( geometry, primitiveDef, parser ); 3730 | return Promise.all( pending ).then( function () { 3731 | 3732 | return primitiveDef.targets !== undefined ? addMorphTargets( geometry, primitiveDef.targets, parser ) : geometry; 3733 | 3734 | } ); 3735 | 3736 | } 3737 | /** 3738 | * @param {BufferGeometry} geometry 3739 | * @param {Number} drawMode 3740 | * @return {BufferGeometry} 3741 | */ 3742 | 3743 | 3744 | function toTrianglesDrawMode( geometry, drawMode ) { 3745 | 3746 | let index = geometry.getIndex(); // generate index if not present 3747 | 3748 | if ( index === null ) { 3749 | 3750 | const indices = []; 3751 | const position = geometry.getAttribute( 'position' ); 3752 | 3753 | if ( position !== undefined ) { 3754 | 3755 | for ( let i = 0; i < position.count; i ++ ) { 3756 | 3757 | indices.push( i ); 3758 | 3759 | } 3760 | 3761 | geometry.setIndex( indices ); 3762 | index = geometry.getIndex(); 3763 | 3764 | } else { 3765 | 3766 | console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' ); 3767 | return geometry; 3768 | 3769 | } 3770 | 3771 | } // 3772 | 3773 | 3774 | const numberOfTriangles = index.count - 2; 3775 | const newIndices = []; 3776 | 3777 | if ( drawMode === THREE.TriangleFanDrawMode ) { 3778 | 3779 | // gl.TRIANGLE_FAN 3780 | for ( let i = 1; i <= numberOfTriangles; i ++ ) { 3781 | 3782 | newIndices.push( index.getX( 0 ) ); 3783 | newIndices.push( index.getX( i ) ); 3784 | newIndices.push( index.getX( i + 1 ) ); 3785 | 3786 | } 3787 | 3788 | } else { 3789 | 3790 | // gl.TRIANGLE_STRIP 3791 | for ( let i = 0; i < numberOfTriangles; i ++ ) { 3792 | 3793 | if ( i % 2 === 0 ) { 3794 | 3795 | newIndices.push( index.getX( i ) ); 3796 | newIndices.push( index.getX( i + 1 ) ); 3797 | newIndices.push( index.getX( i + 2 ) ); 3798 | 3799 | } else { 3800 | 3801 | newIndices.push( index.getX( i + 2 ) ); 3802 | newIndices.push( index.getX( i + 1 ) ); 3803 | newIndices.push( index.getX( i ) ); 3804 | 3805 | } 3806 | 3807 | } 3808 | 3809 | } 3810 | 3811 | if ( newIndices.length / 3 !== numberOfTriangles ) { 3812 | 3813 | console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' ); 3814 | 3815 | } // build final geometry 3816 | 3817 | 3818 | const newGeometry = geometry.clone(); 3819 | newGeometry.setIndex( newIndices ); 3820 | return newGeometry; 3821 | 3822 | } 3823 | 3824 | THREE.GLTFLoader = GLTFLoader; 3825 | 3826 | } )(); 3827 | --------------------------------------------------------------------------------