├── 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