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