├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .yarn └── install-state.gz ├── .yarnrc.yml ├── LICENSE ├── cli.js ├── logo.svg ├── package.json ├── readme.md ├── rollup.config.js ├── src ├── bin │ ├── DRACOLoader.js │ └── GLTFLoader.js ├── gltfjsx.js ├── test.js └── utils │ ├── exports.js │ ├── isVarName.js │ ├── parser.js │ └── transform.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: drcmda 4 | open_collective: react-three-fiber 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: corepack enable 21 | - run: yarn install --immutable 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | types/ 5 | Thumbs.db 6 | ehthumbs.db 7 | Desktop.ini 8 | $RECYCLE.BIN/ 9 | .DS_Store 10 | .vscode 11 | .docz/ 12 | package-lock.json 13 | coverage/ 14 | .idea 15 | yarn-error.log 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/gltfjsx/f4fbfa2a19ffa7c05b1a93745da7b4a7abd497f4/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paul Henschel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | import meow from 'meow' 4 | import { fileURLToPath } from 'url' 5 | import { dirname } from 'path' 6 | import gltfjsx from './src/gltfjsx.js' 7 | import { readPackageUpSync } from 'read-pkg-up' 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = dirname(__filename) 11 | 12 | const cli = meow( 13 | ` 14 | Usage 15 | $ npx gltfjsx [Model.glb] [options] 16 | 17 | Options 18 | --output, -o Output file name/path 19 | --types, -t Add Typescript definitions 20 | --keepnames, -k Keep original names 21 | --keepgroups, -K Keep (empty) groups, disable pruning 22 | --bones, -b Lay out bones declaratively (default: false) 23 | --meta, -m Include metadata (as userData) 24 | --shadows, s Let meshes cast and receive shadows 25 | --printwidth, w Prettier printWidth (default: 120) 26 | --precision, -p Number of fractional digits (default: 3) 27 | --draco, -d Draco binary path 28 | --root, -r Sets directory from which .gltf file is served 29 | --instance, -i Instance re-occuring geometry 30 | --instanceall, -I Instance every geometry (for cheaper re-use) 31 | --exportdefault, -E Use default export 32 | --transform, -T Transform the asset for the web (draco, prune, resize) 33 | --resolution, -R Resolution for texture resizing (default: 1024) 34 | --keepmeshes, -j Do not join compatible meshes 35 | --keepmaterials, -M Do not palette join materials 36 | --format, -f Texture format (default: "webp") 37 | --simplify, -S Mesh simplification (default: false) 38 | --ratio Simplifier ratio (default: 0) 39 | --error Simplifier error threshold (default: 0.0001) 40 | --console, -c Log JSX to console, won't produce a file 41 | --debug, -D Debug output 42 | `, 43 | { 44 | importMeta: import.meta, 45 | flags: { 46 | output: { type: 'string', shortFlag: 'o' }, 47 | types: { type: 'boolean', shortFlag: 't' }, 48 | keepnames: { type: 'boolean', shortFlag: 'k' }, 49 | keepgroups: { type: 'boolean', shortFlag: 'K' }, 50 | bones: { type: 'boolean', shortFlag: 'b', default: false }, 51 | shadows: { type: 'boolean', shortFlag: 's' }, 52 | printwidth: { type: 'number', shortFlag: 'p', default: 1000 }, 53 | meta: { type: 'boolean', shortFlag: 'm' }, 54 | precision: { type: 'number', shortFlag: 'p', default: 3 }, 55 | draco: { type: 'string', shortFlag: 'd' }, 56 | root: { type: 'string', shortFlag: 'r' }, 57 | instance: { type: 'boolean', shortFlag: 'i' }, 58 | instanceall: { type: 'boolean', shortFlag: 'I' }, 59 | transform: { type: 'boolean', shortFlag: 'T' }, 60 | resolution: { type: 'number', shortFlag: 'R', default: 1024 }, 61 | degrade: { type: 'string', shortFlag: 'q', default: '' }, 62 | degraderesolution: { type: 'number', shortFlag: 'Q', default: 512 }, 63 | simplify: { type: 'boolean', shortFlag: 'S', default: false }, 64 | keepmeshes: { type: 'boolean', shortFlag: 'j', default: false }, 65 | keepmaterials: { type: 'boolean', shortFlag: 'M', default: false }, 66 | format: { type: 'string', shortFlag: 'f', default: 'webp' }, 67 | exportdefault: { type: 'boolean', shortFlag: 'E' }, 68 | ratio: { type: 'number', default: 0.75 }, 69 | error: { type: 'number', default: 0.001 }, 70 | console: { type: 'boolean', shortFlag: 'c' }, 71 | debug: { type: 'boolean', shortFlag: 'D' }, 72 | }, 73 | } 74 | ) 75 | 76 | const { packageJson } = readPackageUpSync({ cwd: __dirname, normalize: false }) 77 | 78 | if (cli.input.length === 0) { 79 | console.log(cli.help) 80 | } else { 81 | const config = { 82 | ...cli.flags, 83 | header: `Auto-generated by: https://github.com/pmndrs/gltfjsx 84 | Command: npx gltfjsx@${packageJson.version} ${process.argv.slice(2).join(' ')}`, 85 | } 86 | const file = cli.input[0] 87 | let nameExt = file.match(/[-_\w\d\s]+[.][\w]+$/i)[0] 88 | let name = nameExt.split('.').slice(0, -1).join('.') 89 | const output = config.output ?? name.charAt(0).toUpperCase() + name.slice(1) + (config.types ? '.tsx' : '.jsx') 90 | const showLog = (log) => { 91 | console.info('log:', log) 92 | } 93 | try { 94 | const response = await gltfjsx(file, output, { ...config, showLog, timeout: 0, delay: 1 }) 95 | } catch (e) { 96 | console.error(e) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 |
145 |
146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gltfjsx", 3 | "version": "6.5.2", 4 | "description": "GLTF to JSX converter", 5 | "scripts": { 6 | "build": "rollup -c", 7 | "test": "node src/test" 8 | }, 9 | "type": "module", 10 | "keywords": [ 11 | "gltf", 12 | "jsx", 13 | "react", 14 | "fiber", 15 | "three", 16 | "threejs", 17 | "webp" 18 | ], 19 | "author": "Paul Henschel", 20 | "maintainers": [ 21 | "Max Rusan" 22 | ], 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/pmndrs/gltfjsx.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/pmndrs/gltfjsx/issues" 30 | }, 31 | "homepage": "https://github.com/pmndrs/gltfjsx#readme", 32 | "bin": "./cli.js", 33 | "main": "dist/index.cjs", 34 | "module": "dist/index.js", 35 | "engines": { 36 | "node": ">=16" 37 | }, 38 | "dependencies": { 39 | "@gltf-transform/core": "4.1.0", 40 | "@gltf-transform/extensions": "4.1.0", 41 | "@gltf-transform/functions": "4.1.0", 42 | "@node-loader/babel": "^2.0.1", 43 | "draco3dgltf": "^1.5.7", 44 | "is-var-name": "^2.0.0", 45 | "keyframe-resample": "^0.1.0", 46 | "meow": "^12.1.1", 47 | "meshoptimizer": "^0.22.0", 48 | "prettier": "3.1.1", 49 | "read-pkg-up": "^10.1.0", 50 | "three": "0.159.0", 51 | "three-stdlib": "^2.28.7" 52 | }, 53 | "optionalDependencies": { 54 | "jsdom": "^24.1.0", 55 | "jsdom-global": "^3.0.2", 56 | "libvips": "0.0.2", 57 | "sharp": "^0.33.5" 58 | }, 59 | "resolutions": { 60 | "sharp": "<0.33.0", 61 | "@gltf-transform/core": "4.0.8", 62 | "@gltf-transform/extensions": "4.0.8", 63 | "@gltf-transform/functions": "4.0.8" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "7.23.6", 67 | "@babel/plugin-proposal-class-properties": "^7.16.0", 68 | "@babel/plugin-transform-modules-commonjs": "7.23.3", 69 | "@babel/plugin-transform-parameters": "7.23.3", 70 | "@babel/plugin-transform-runtime": "7.23.6", 71 | "@babel/plugin-transform-template-literals": "7.23.3", 72 | "@babel/preset-env": "7.23.6", 73 | "@babel/preset-react": "7.23.3", 74 | "@babel/preset-typescript": "^7.23.3", 75 | "@rollup/plugin-babel": "^6.0.4", 76 | "@rollup/plugin-node-resolve": "^15.2.3", 77 | "chalk": "^5.3.0", 78 | "fast-glob": "^3.3.2", 79 | "fs-extra": "^11.2.0", 80 | "lint-staged": "^13.2.0", 81 | "rollup": "^4.9.1", 82 | "rollup-plugin-size-snapshot": "^0.12.0", 83 | "rollup-plugin-terser": "^7.0.2" 84 | }, 85 | "prettier": { 86 | "semi": false, 87 | "trailingComma": "es5", 88 | "singleQuote": true, 89 | "jsxBracketSameLine": true, 90 | "tabWidth": 2, 91 | "printWidth": 120 92 | }, 93 | "lint-staged": { 94 | "*.{js,jsx,ts,tsx}": [ 95 | "prettier --write" 96 | ] 97 | }, 98 | "collective": { 99 | "type": "opencollective", 100 | "url": "https://opencollective.com/react-three-fiber" 101 | }, 102 | "packageManager": "yarn@4.4.0+sha256.5f228cb28f2edb97d8c3b667fb7b2fdcf06c46798e25ea889dad9e0b4bc2e2c1" 103 | } 104 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | https://user-images.githubusercontent.com/2223602/126318148-99da7ed6-a578-48dd-bdd2-21056dbad003.mp4 2 | 3 |
4 |
5 | 6 | [![Version](https://img.shields.io/npm/v/gltfjsx?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/gltfjsx) [![Discord Shield](https://img.shields.io/discord/740090768164651008?style=flat&colorA=000000&colorB=000000&label=discord&logo=discord&logoColor=ffffff)](https://discord.gg/ZZjjNvJ) 7 | 8 | A small command-line tool that turns GLTF assets into declarative and re-usable [react-three-fiber](https://github.com/pmndrs/react-three-fiber) JSX components. 9 | 10 | ### The GLTF workflow on the web is not ideal ... 11 | 12 | - GLTF is thrown whole into the scene which prevents re-use, in threejs objects can only be mounted once 13 | - Contents can only be found by traversal which is cumbersome and slow 14 | - Changes to queried nodes are made by mutation, which alters the source data and prevents re-use 15 | - Re-structuring content, making nodes conditional or adding/removing is cumbersome 16 | - Model compression is complex and not easily achieved 17 | - Models often have unnecessary nodes that cause extra work and matrix updates 18 | 19 | ### GLTFJSX fixes that 20 | 21 | - 🧑‍💻 It creates a virtual graph of all objects and materials. Now you can easily alter contents and re-use. 22 | - 🏎️ The graph gets pruned (empty groups, unnecessary transforms, ...) and will perform better. 23 | - ⚡️ It will optionally compress your model with up to 70%-90% size reduction. 24 | 25 | ## Usage 26 | 27 | ```text 28 | Usage 29 | $ npx gltfjsx [Model.glb] [options] 30 | 31 | Options 32 | --output, -o Output file name/path 33 | --types, -t Add Typescript definitions 34 | --keepnames, -k Keep original names 35 | --keepgroups, -K Keep (empty) groups, disable pruning 36 | --bones, -b Lay out bones declaratively (default: false) 37 | --meta, -m Include metadata (as userData) 38 | --shadows, s Let meshes cast and receive shadows 39 | --printwidth, w Prettier printWidth (default: 120) 40 | --precision, -p Number of fractional digits (default: 3) 41 | --draco, -d Draco binary path 42 | --root, -r Sets directory from which .gltf file is served 43 | --instance, -i Instance re-occuring geometry 44 | --instanceall, -I Instance every geometry (for cheaper re-use) 45 | --exportdefault, -E Use default export 46 | --transform, -T Transform the asset for the web (draco, prune, resize) 47 | --resolution, -R Resolution for texture resizing (default: 1024) 48 | --keepmeshes, -j Do not join compatible meshes 49 | --keepmaterials, -M Do not palette join materials 50 | --format, -f Texture format (default: "webp") 51 | --simplify, -S Mesh simplification (default: false) 52 | --ratio Simplifier ratio (default: 0) 53 | --error Simplifier error threshold (default: 0.0001) 54 | --console, -c Log JSX to console, won't produce a file 55 | --debug, -D Debug output 56 | ``` 57 | 58 | ### A typical use-case 59 | 60 | First you run your model through gltfjsx. `npx` allows you to use npm packages without installing them. 61 | 62 | ```bash 63 | npx gltfjsx model.gltf --transform 64 | ``` 65 | 66 | This will create a `Model.jsx` file that plots out all of the assets contents. 67 | 68 | ```jsx 69 | /* 70 | auto-generated by: https://github.com/pmdrs/gltfjsx 71 | author: abcdef (https://sketchfab.com/abcdef) 72 | license: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/) 73 | source: https://sketchfab.com/models/... 74 | title: Model 75 | */ 76 | 77 | import { useGLTF, PerspectiveCamera } from '@react-three/drei' 78 | 79 | export function Model(props) { 80 | const { nodes, materials } = useGLTF('/model-transformed.glb') 81 | return ( 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ) 91 | } 92 | 93 | useGLTF.preload('/model.gltf') 94 | ``` 95 | 96 | Add your model to your `/public` folder as you would normally do. With the `--transform` flag it has created a compressed copy of it (in the above case `model-transformed.glb`). Without the flag just copy the original model. 97 | 98 | ```text 99 | /public 100 | model-transformed.glb 101 | ``` 102 | 103 | The component can now be dropped into your scene. 104 | 105 | ```jsx 106 | import { Canvas } from '@react-three/fiber' 107 | import { Model } from './Model' 108 | 109 | function App() { 110 | return ( 111 | 112 | 113 | ``` 114 | 115 | You can re-use it, it will re-use geometries and materials out of the box: 116 | 117 | ```jsx 118 | 119 | 120 | ``` 121 | 122 | Or make the model dynamic. Change its colors for example: 123 | 124 | ```jsx 125 | 126 | ``` 127 | 128 | Or exchange materials: 129 | 130 | ```jsx 131 | 132 | 133 | 134 | ``` 135 | 136 | Make contents conditional: 137 | 138 | ```jsx 139 | {condition && } 140 | ``` 141 | 142 | Add events: 143 | 144 | ```jsx 145 | 146 | ``` 147 | 148 | ## Features 149 | 150 | #### ⚡️ Draco and meshopt compression ootb 151 | 152 | You don't need to do anything if your models are draco compressed, since `useGLTF` defaults to a [draco CDN](https://www.gstatic.com/draco/v1/decoders/). By adding the `--draco` flag you can refer to [local binaries](https://github.com/mrdoob/three.js/tree/dev/examples/js/libs/draco/gltf) which must reside in your /public folder. 153 | 154 | #### ⚡️ Preload your assets for faster response 155 | 156 | The asset will be preloaded by default, this makes it quicker to load and reduces time-to-paint. Remove the preloader if you don't need it. 157 | 158 | ```jsx 159 | useGLTF.preload('/model.gltf') 160 | ``` 161 | 162 | #### ⚡️ Auto-transform (compression, resize) 163 | 164 | With the `--transform` flag it creates a binary-packed, draco-compressed, texture-resized (1024x1024), webp compressed, deduped, instanced and pruned *.glb ready to be consumed on a web site. It uses [glTF-Transform](https://github.com/donmccurdy/glTF-Transform). This can reduce the size of an asset by 70%-90%. 165 | 166 | It will not alter the original but create a copy and append `[modelname]-transformed.glb`. 167 | 168 | #### ⚡️ Type-safety 169 | 170 | Add the `--types` flag and your GLTF will be typesafe. 171 | 172 | ```tsx 173 | type GLTFResult = GLTF & { 174 | nodes: { robot: THREE.Mesh; rocket: THREE.Mesh } 175 | materials: { metal: THREE.MeshStandardMaterial; wood: THREE.MeshStandardMaterial } 176 | } 177 | 178 | export default function Model(props: JSX.IntrinsicElements['group']) { 179 | const { nodes, materials } = useGLTF('/model.gltf') 180 | ``` 181 | 182 | #### ⚡️ Easier access to animations 183 | 184 | If your GLTF contains animations it will add [drei's](https://github.com/pmndrs/drei) `useAnimations` hook, which extracts all clips and prepares them as actions: 185 | 186 | ```jsx 187 | const { nodes, materials, animations } = useGLTF('/model.gltf') 188 | const { actions } = useAnimations(animations, group) 189 | ``` 190 | 191 | If you want to play an animation you can do so at any time: 192 | 193 | ```jsx 194 | actions.jump.play()} /> 195 | ``` 196 | 197 | If you want to blend animations: 198 | 199 | ```jsx 200 | const [name, setName] = useState("jump") 201 | ... 202 | useEffect(() => { 203 | actions[name].reset().fadeIn(0.5).play() 204 | return () => actions[name].fadeOut(0.5) 205 | }, [name]) 206 | ``` 207 | 208 | #### ⚡️ Auto-instancing 209 | 210 | Use the `--instance` flag and it will look for similar geometry and create instances of them. Look into [drei/Merged](https://github.com/pmndrs/drei#instances) to understand how it works. It does not matter if you instanced the model previously in Blender, it creates instances for each mesh that has a specific geometry and/or material. 211 | 212 | `--instanceall` will create instances of all the geometry. This allows you to re-use the model with the smallest amount of drawcalls. 213 | 214 | Your export will look like something like this: 215 | 216 | ```jsx 217 | const context = createContext() 218 | export function Instances({ children, ...props }) { 219 | const { nodes } = useGLTF('/model-transformed.glb') 220 | const instances = useMemo(() => ({ Screw1: nodes['Screw1'], Screw2: nodes['Screw2'] }), [nodes]) 221 | return ( 222 | 223 | {(instances) => } 224 | 225 | ) 226 | } 227 | 228 | export function Model(props) { 229 | const instances = useContext(context) 230 | return ( 231 | 232 | 233 | 234 | 235 | ) 236 | } 237 | ``` 238 | 239 | Note that similar to `--transform` it also has to transform the model. In order to use and re-use the model import both `Instances` and `Model`. Put all your models into the `Instances` component (you can nest). 240 | 241 | The following will show the model three times, but you will only have 2 drawcalls tops. 242 | 243 | ```jsx 244 | import { Instances, Model } from './Model' 245 | 246 | 247 | 248 | 249 | 250 | 251 | ``` 252 | 253 | ## Using the parser stand-alone 254 | 255 | ```jsx 256 | import { parse } from 'gltfjsx' 257 | import { GLTFLoader, DRACOLoader } from 'three-stdlib' 258 | 259 | const gltfLoader = new GLTFLoader() 260 | const dracoloader = new DRACOLoader() 261 | dracoloader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/') 262 | gltfLoader.setDRACOLoader(dracoloader) 263 | 264 | gltfLoader.load(url, (gltf) => { 265 | const jsx = parse(gltf, optionalConfig) 266 | }) 267 | ``` 268 | 269 | ## Using the parser stand-alone for scenes (object3d's) 270 | 271 | ```jsx 272 | const jsx = parse(scene, optionalConfig) 273 | ``` 274 | 275 | ## Using GLTFStructureLoader stand-alone 276 | 277 | The GLTFStructureLoader can come in handy while testing gltf assets. It allows you to extract the structure without the actual binaries and textures making it possible to run in a testing environment. 278 | 279 | ```jsx 280 | import { GLTFStructureLoader } from 'gltfjsx' 281 | import fs from 'fs/promises' 282 | 283 | it('should have a scene with a blue mesh', async () => { 284 | const loader = new GLTFStructureLoader() 285 | const data = await fs.readFile('./model.glb') 286 | const { scene } = await new Promise((res) => loader.parse(data, '', res)) 287 | expect(() => scene.children.length).toEqual(1) 288 | expect(() => scene.children[0].type).toEqual('mesh') 289 | expect(() => scene.children[0].material.color).toEqual('blue') 290 | }) 291 | ``` 292 | 293 | ## Requirements 294 | 295 | - Nodejs must be installed 296 | - The GLTF file has to be present in your projects `/public` folder 297 | - [three](https://github.com/mrdoob/three.js/) (>= 122.x) 298 | - [@react-three/fiber](https://github.com/pmndrs/react-three-fiber) (>= 5.x) 299 | - [@react-three/drei](https://github.com/pmndrs/drei) (>= 2.x) 300 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import babel from '@rollup/plugin-babel' 3 | import resolve from '@rollup/plugin-node-resolve' 4 | 5 | const root = process.platform === 'win32' ? path.resolve('/') : '/' 6 | const external = (id) => !id.startsWith('.') && !id.startsWith(root) 7 | const extensions = ['.js', '.jsx', '.ts', '.tsx', '.json'] 8 | 9 | const getBabelOptions = ({ useESModules }) => ({ 10 | babelrc: false, 11 | extensions, 12 | exclude: '**/node_modules/**', 13 | babelHelpers: 'runtime', 14 | presets: [ 15 | [ 16 | '@babel/preset-env', 17 | { 18 | include: [ 19 | '@babel/plugin-proposal-optional-chaining', 20 | '@babel/plugin-proposal-nullish-coalescing-operator', 21 | '@babel/plugin-proposal-numeric-separator', 22 | '@babel/plugin-proposal-logical-assignment-operators', 23 | ], 24 | bugfixes: true, 25 | loose: true, 26 | modules: false, 27 | targets: '> 1%, not dead, not ie 11, not op_mini all', 28 | }, 29 | ], 30 | ], 31 | plugins: [['@babel/transform-runtime', { regenerator: false, useESModules }]], 32 | }) 33 | 34 | export default [ 35 | { 36 | input: `./src/utils/exports.js`, 37 | output: { file: `dist/index.js`, format: 'esm' }, 38 | external, 39 | plugins: [babel(getBabelOptions({ useESModules: true })), resolve({ extensions })], 40 | }, 41 | { 42 | input: `./src/utils/exports.js`, 43 | output: { file: `dist/index.cjs.js`, format: 'cjs' }, 44 | external, 45 | plugins: [babel(getBabelOptions({ useESModules: false })), resolve({ extensions })], 46 | }, 47 | ] 48 | -------------------------------------------------------------------------------- /src/bin/DRACOLoader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import draco from 'draco3dgltf' 3 | 4 | const decoder = draco.createDecoderModule() 5 | const DRACOLoader = function (t) { 6 | ;(this.timeLoaded = 0), 7 | (this.manager = t || THREE.DefaultLoadingManager), 8 | (this.materials = null), 9 | (this.verbosity = 0), 10 | (this.attributeOptions = {}), 11 | (this.drawMode = THREE.TrianglesDrawMode), 12 | (this.nativeAttributeMap = { position: 'POSITION', normal: 'NORMAL', color: 'COLOR', uv: 'TEX_COORD' }) 13 | } 14 | 15 | DRACOLoader.prototype = { 16 | constructor: DRACOLoader, 17 | load: function (t, e, r, o) { 18 | var i = this, 19 | n = new THREE.FileLoader(i.manager) 20 | n.setPath(this.path), 21 | n.setResponseType('arraybuffer'), 22 | n.load( 23 | t, 24 | function (t) { 25 | i.decodeDracoFile(t, e) 26 | }, 27 | r, 28 | o 29 | ) 30 | }, 31 | setPath: function (t) { 32 | return (this.path = t), this 33 | }, 34 | setVerbosity: function (t) { 35 | return (this.verbosity = t), this 36 | }, 37 | setDrawMode: function (t) { 38 | return (this.drawMode = t), this 39 | }, 40 | setSkipDequantization: function (t, e) { 41 | var r = !0 42 | return void 0 !== e && (r = e), (this.getAttributeOptions(t).skipDequantization = r), this 43 | }, 44 | decodeDracoFile: function (t, e, r, o) { 45 | decoder.then((decoder) => this.decodeDracoFileInternal(t, decoder, e, r, o)) 46 | }, 47 | decodeDracoFileInternal: function (t, e, r, o, i) { 48 | var n = new e.DecoderBuffer() 49 | n.Init(new Int8Array(t), t.byteLength) 50 | var a = new e.Decoder(), 51 | s = a.GetEncodedGeometryType(n) 52 | if (s == e.TRIANGULAR_MESH) this.verbosity > 0 && console.log('Loaded a mesh.') 53 | else { 54 | if (s != e.POINT_CLOUD) { 55 | var u = 'THREE.DRACOLoader: Unknown geometry type.' 56 | //throw (console.error(u), new Error(u)) 57 | } 58 | this.verbosity > 0 && console.log('Loaded a point cloud.') 59 | } 60 | r(this.convertDracoGeometryTo3JS(e, a, s, n, o, i)) 61 | }, 62 | addAttributeToGeometry: function (t, e, r, o, i, n, a, s) { 63 | if (0 === n.ptr) { 64 | var u = 'THREE.DRACOLoader: No attribute ' + o 65 | throw (console.error(u), new Error(u)) 66 | } 67 | var d, 68 | A, 69 | c = n.num_components(), 70 | l = r.num_points() * c 71 | switch (i) { 72 | case Float32Array: 73 | ;(d = new t.DracoFloat32Array()), 74 | e.GetAttributeFloatForAllPoints(r, n, d), 75 | (s[o] = new Float32Array(l)), 76 | (A = THREE.Float32BufferAttribute) 77 | break 78 | case Int8Array: 79 | ;(d = new t.DracoInt8Array()), 80 | e.GetAttributeInt8ForAllPoints(r, n, d), 81 | (s[o] = new Int8Array(l)), 82 | (A = THREE.Int8BufferAttribute) 83 | break 84 | case Int16Array: 85 | ;(d = new t.DracoInt16Array()), 86 | e.GetAttributeInt16ForAllPoints(r, n, d), 87 | (s[o] = new Int16Array(l)), 88 | (A = THREE.Int16BufferAttribute) 89 | break 90 | case Int32Array: 91 | ;(d = new t.DracoInt32Array()), 92 | e.GetAttributeInt32ForAllPoints(r, n, d), 93 | (s[o] = new Int32Array(l)), 94 | (A = THREE.Int32BufferAttribute) 95 | break 96 | case Uint8Array: 97 | ;(d = new t.DracoUInt8Array()), 98 | e.GetAttributeUInt8ForAllPoints(r, n, d), 99 | (s[o] = new Uint8Array(l)), 100 | (A = THREE.Uint8BufferAttribute) 101 | break 102 | case Uint16Array: 103 | ;(d = new t.DracoUInt16Array()), 104 | e.GetAttributeUInt16ForAllPoints(r, n, d), 105 | (s[o] = new Uint16Array(l)), 106 | (A = THREE.Uint16BufferAttribute) 107 | break 108 | case Uint32Array: 109 | ;(d = new t.DracoUInt32Array()), 110 | e.GetAttributeUInt32ForAllPoints(r, n, d), 111 | (s[o] = new Uint32Array(l)), 112 | (A = THREE.Uint32BufferAttribute) 113 | break 114 | default: 115 | u = 'THREE.DRACOLoader: Unexpected attribute type.' 116 | throw (console.error(u), new Error(u)) 117 | } 118 | for (var b = 0; b < l; b++) s[o][b] = d.GetValue(b) 119 | a.setAttribute(o, new A(s[o], c)), t.destroy(d) 120 | }, 121 | convertDracoGeometryTo3JS: function (t, e, r, o, i, n) { 122 | var a, s, u 123 | if ( 124 | (!0 === this.getAttributeOptions('position').skipDequantization && e.SkipAttributeTransform(t.POSITION), 125 | r === t.TRIANGULAR_MESH 126 | ? ((a = new t.Mesh()), (s = e.DecodeBufferToMesh(o, a))) 127 | : ((a = new t.PointCloud()), (s = e.DecodeBufferToPointCloud(o, a))), 128 | !s.ok() || 0 == a.ptr) 129 | ) { 130 | return new THREE.BufferGeometry() 131 | var d = 'THREE.DRACOLoader: Decoding failed: ' 132 | throw ((d += s.error_msg()), console.error(d), t.destroy(e), t.destroy(a), new Error(d)) 133 | } 134 | t.destroy(o), 135 | r == t.TRIANGULAR_MESH 136 | ? ((u = a.num_faces()), this.verbosity > 0 && console.log('Number of faces loaded: ' + u.toString())) 137 | : (u = 0) 138 | var A = a.num_points(), 139 | c = a.num_attributes() 140 | this.verbosity > 0 && 141 | (console.log('Number of points loaded: ' + A.toString()), 142 | console.log('Number of attributes loaded: ' + c.toString())) 143 | var l = e.GetAttributeId(a, t.POSITION) 144 | if (-1 == l) { 145 | d = 'THREE.DRACOLoader: No position attribute found.' 146 | throw (console.error(d), t.destroy(e), t.destroy(a), new Error(d)) 147 | } 148 | var b = e.GetAttribute(a, l), 149 | f = {}, 150 | y = new THREE.BufferGeometry() 151 | if (i) 152 | for (var E in i) { 153 | var h = n[E], 154 | w = i[E], 155 | p = e.GetAttributeByUniqueId(a, w) 156 | //this.addAttributeToGeometry(t, e, a, E, h, p, y, f) 157 | } 158 | else 159 | for (var E in this.nativeAttributeMap) { 160 | var T = e.GetAttributeId(a, t[this.nativeAttributeMap[E]]) 161 | if (-1 !== T) { 162 | this.verbosity > 0 && console.log('Loaded ' + E + ' attribute.') 163 | p = e.GetAttribute(a, T) 164 | //this.addAttributeToGeometry(t, e, a, E, Float32Array, p, y, f) 165 | } 166 | } 167 | if (r == t.TRIANGULAR_MESH) 168 | if (this.drawMode === THREE.TriangleStripDrawMode) { 169 | var R = new t.DracoInt32Array() 170 | e.GetTriangleStripsFromMesh(a, R) 171 | f.indices = new Uint32Array(R.size()) 172 | for (var I = 0; I < R.size(); ++I) f.indices[I] = R.GetValue(I) 173 | t.destroy(R) 174 | } else { 175 | var v = 3 * u 176 | f.indices = new Uint32Array(v) 177 | var D = new t.DracoInt32Array() 178 | for (I = 0; I < u; ++I) { 179 | e.GetFaceFromMesh(a, I, D) 180 | var m = 3 * I 181 | ;(f.indices[m] = D.GetValue(0)), (f.indices[m + 1] = D.GetValue(1)), (f.indices[m + 2] = D.GetValue(2)) 182 | } 183 | t.destroy(D) 184 | } 185 | ;(y.drawMode = this.drawMode), 186 | r == t.TRIANGULAR_MESH && 187 | y.setIndex( 188 | new (f.indices.length > 65535 ? THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute)(f.indices, 1) 189 | ) 190 | var G = new t.AttributeQuantizationTransform() 191 | if (G.InitFromAttribute(b)) { 192 | ;(y.attributes.position.isQuantized = !0), 193 | (y.attributes.position.maxRange = G.range()), 194 | (y.attributes.position.numQuantizationBits = G.quantization_bits()), 195 | (y.attributes.position.minValues = new Float32Array(3)) 196 | for (I = 0; I < 3; ++I) y.attributes.position.minValues[I] = G.min_value(I) 197 | } 198 | return t.destroy(G), t.destroy(e), t.destroy(a), y 199 | }, 200 | isVersionSupported: function (t, e) { 201 | e(decoder.isVersionSupported(t)) 202 | }, 203 | getAttributeOptions: function (t) { 204 | return void 0 === this.attributeOptions[t] && (this.attributeOptions[t] = {}), this.attributeOptions[t] 205 | }, 206 | } 207 | 208 | export { DRACOLoader } 209 | -------------------------------------------------------------------------------- /src/bin/GLTFLoader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | export class GLTFLoader extends THREE.Loader { 4 | constructor() { 5 | super() 6 | this.dracoLoader = null 7 | this.ddsLoader = null 8 | this.ktx2Loader = null 9 | 10 | this.pluginCallbacks = [] 11 | 12 | this.register(function (parser) { 13 | return new GLTFMaterialsClearcoatExtension(parser) 14 | }) 15 | 16 | this.register(function (parser) { 17 | return new GLTFTextureBasisUExtension(parser) 18 | }) 19 | 20 | this.register(function (parser) { 21 | return new GLTFMaterialsTransmissionExtension(parser) 22 | }) 23 | 24 | this.register(function (parser) { 25 | return new GLTFLightsExtension(parser) 26 | }) 27 | this.register(function (parser) { 28 | return new GLTFMeshGpuInstancing(parser) 29 | }) 30 | } 31 | 32 | load(url, onLoad, onProgress, onError) { 33 | var scope = this 34 | 35 | var resourcePath 36 | 37 | if (this.resourcePath !== '') { 38 | resourcePath = this.resourcePath 39 | } else if (this.path !== '') { 40 | resourcePath = this.path 41 | } else { 42 | resourcePath = THREE.LoaderUtils.extractUrlBase(url) 43 | } 44 | 45 | // Tells the LoadingManager to track an extra item, which resolves after 46 | // the model is fully loaded. This means the count of items loaded will 47 | // be incorrect, but ensures manager.onLoad() does not fire early. 48 | this.manager.itemStart(url) 49 | 50 | var _onError = function (e) { 51 | if (onError) { 52 | onError(e) 53 | } else { 54 | console.error(e) 55 | } 56 | 57 | scope.manager.itemError(url) 58 | scope.manager.itemEnd(url) 59 | } 60 | 61 | var loader = new THREE.FileLoader(this.manager) 62 | 63 | loader.setPath(this.path) 64 | loader.setResponseType('arraybuffer') 65 | loader.setRequestHeader(this.requestHeader) 66 | loader.setWithCredentials(this.withCredentials) 67 | 68 | loader.load( 69 | url, 70 | function (data) { 71 | try { 72 | scope.parse( 73 | data, 74 | resourcePath, 75 | function (gltf) { 76 | onLoad(gltf) 77 | 78 | scope.manager.itemEnd(url) 79 | }, 80 | _onError 81 | ) 82 | } catch (e) { 83 | _onError(e) 84 | } 85 | }, 86 | onProgress, 87 | _onError 88 | ) 89 | } 90 | 91 | setDRACOLoader(dracoLoader) { 92 | this.dracoLoader = dracoLoader 93 | return this 94 | } 95 | 96 | setDDSLoader(ddsLoader) { 97 | this.ddsLoader = ddsLoader 98 | return this 99 | } 100 | 101 | setKTX2Loader(ktx2Loader) { 102 | this.ktx2Loader = ktx2Loader 103 | return this 104 | } 105 | 106 | register(callback) { 107 | if (this.pluginCallbacks.indexOf(callback) === -1) { 108 | this.pluginCallbacks.push(callback) 109 | } 110 | 111 | return this 112 | } 113 | 114 | unregister(callback) { 115 | if (this.pluginCallbacks.indexOf(callback) !== -1) { 116 | this.pluginCallbacks.splice(this.pluginCallbacks.indexOf(callback), 1) 117 | } 118 | 119 | return this 120 | } 121 | 122 | parse(data, path, onLoad, onError) { 123 | var content 124 | var extensions = {} 125 | var plugins = {} 126 | 127 | if (typeof data === 'string') { 128 | content = data 129 | } else { 130 | var magic = THREE.LoaderUtils.decodeText(new Uint8Array(data, 0, 4)) 131 | 132 | if (magic === BINARY_EXTENSION_HEADER_MAGIC) { 133 | try { 134 | extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data) 135 | } catch (error) { 136 | if (onError) onError(error) 137 | return 138 | } 139 | 140 | content = extensions[EXTENSIONS.KHR_BINARY_GLTF].content 141 | } else { 142 | content = THREE.LoaderUtils.decodeText(new Uint8Array(data)) 143 | } 144 | } 145 | 146 | var json = JSON.parse(content) 147 | 148 | if (json.asset === undefined || json.asset.version[0] < 2) { 149 | if (onError) onError(new Error('THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.')) 150 | return 151 | } 152 | 153 | var parser = new GLTFParser(json, { 154 | path: path || this.resourcePath || '', 155 | crossOrigin: this.crossOrigin, 156 | manager: this.manager, 157 | ktx2Loader: this.ktx2Loader, 158 | }) 159 | 160 | parser.fileLoader.setRequestHeader(this.requestHeader) 161 | 162 | for (var i = 0; i < this.pluginCallbacks.length; i++) { 163 | var plugin = this.pluginCallbacks[i](parser) 164 | plugins[plugin.name] = plugin 165 | 166 | // Workaround to avoid determining as unknown extension 167 | // in addUnknownExtensionsToUserData(). 168 | // Remove this workaround if we move all the existing 169 | // extension handlers to plugin system 170 | extensions[plugin.name] = true 171 | } 172 | 173 | if (json.extensionsUsed) { 174 | for (var i = 0; i < json.extensionsUsed.length; ++i) { 175 | var extensionName = json.extensionsUsed[i] 176 | var extensionsRequired = json.extensionsRequired || [] 177 | 178 | switch (extensionName) { 179 | case EXTENSIONS.KHR_MATERIALS_UNLIT: 180 | extensions[extensionName] = new GLTFMaterialsUnlitExtension() 181 | break 182 | 183 | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 184 | extensions[extensionName] = new GLTFMaterialsPbrSpecularGlossinessExtension() 185 | break 186 | 187 | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: 188 | extensions[extensionName] = new GLTFDracoMeshCompressionExtension(json, this.dracoLoader) 189 | break 190 | 191 | case EXTENSIONS.MSFT_TEXTURE_DDS: 192 | extensions[extensionName] = new GLTFTextureDDSExtension(this.ddsLoader) 193 | break 194 | 195 | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: 196 | extensions[extensionName] = new GLTFTextureTransformExtension() 197 | break 198 | 199 | case EXTENSIONS.KHR_MESH_QUANTIZATION: 200 | extensions[extensionName] = new GLTFMeshQuantizationExtension() 201 | break 202 | 203 | default: 204 | if (extensionsRequired.indexOf(extensionName) >= 0 && plugins[extensionName] === undefined) { 205 | // console.warn('THREE.GLTFLoader: Unknown extension "' + extensionName + '".') 206 | } 207 | } 208 | } 209 | } 210 | 211 | parser.setExtensions(extensions) 212 | parser.setPlugins(plugins) 213 | parser.parse(onLoad, onError) 214 | } 215 | } 216 | 217 | /* GLTFREGISTRY */ 218 | 219 | function GLTFRegistry() { 220 | var objects = {} 221 | 222 | return { 223 | get: function (key) { 224 | return objects[key] 225 | }, 226 | 227 | add: function (key, object) { 228 | objects[key] = object 229 | }, 230 | 231 | remove: function (key) { 232 | delete objects[key] 233 | }, 234 | 235 | removeAll: function () { 236 | objects = {} 237 | }, 238 | } 239 | } 240 | 241 | /*********************************/ 242 | /********** EXTENSIONS ***********/ 243 | /*********************************/ 244 | 245 | var EXTENSIONS = { 246 | KHR_BINARY_GLTF: 'KHR_binary_glTF', 247 | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', 248 | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', 249 | KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', 250 | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', 251 | KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission', 252 | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', 253 | KHR_TEXTURE_BASISU: 'KHR_texture_basisu', 254 | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', 255 | KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', 256 | MSFT_TEXTURE_DDS: 'MSFT_texture_dds', 257 | EXT_MESH_GPU_INSTANCING: 'EXT_mesh_gpu_instancing', 258 | } 259 | 260 | /** 261 | * DDS Texture Extension 262 | * 263 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds 264 | * 265 | */ 266 | function GLTFTextureDDSExtension(ddsLoader) { 267 | if (!ddsLoader) { 268 | throw new Error('THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader') 269 | } 270 | 271 | this.name = EXTENSIONS.MSFT_TEXTURE_DDS 272 | this.ddsLoader = ddsLoader 273 | } 274 | 275 | /** 276 | * Punctual Lights Extension 277 | * 278 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual 279 | */ 280 | function GLTFLightsExtension(parser) { 281 | this.parser = parser 282 | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL 283 | 284 | // Object3D instance caches 285 | this.cache = { refs: {}, uses: {} } 286 | } 287 | 288 | GLTFLightsExtension.prototype._markDefs = function () { 289 | var parser = this.parser 290 | var nodeDefs = this.parser.json.nodes || [] 291 | 292 | for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) { 293 | var nodeDef = nodeDefs[nodeIndex] 294 | 295 | if (nodeDef.extensions && nodeDef.extensions[this.name] && nodeDef.extensions[this.name].light !== undefined) { 296 | parser._addNodeRef(this.cache, nodeDef.extensions[this.name].light) 297 | } 298 | } 299 | } 300 | 301 | GLTFLightsExtension.prototype._loadLight = function (lightIndex) { 302 | var parser = this.parser 303 | var cacheKey = 'light:' + lightIndex 304 | var dependency = parser.cache.get(cacheKey) 305 | 306 | if (dependency) return dependency 307 | 308 | var json = parser.json 309 | var extensions = (json.extensions && json.extensions[this.name]) || {} 310 | var lightDefs = extensions.lights || [] 311 | var lightDef = lightDefs[lightIndex] 312 | var lightNode 313 | 314 | var color = new THREE.Color(0xffffff) 315 | 316 | if (lightDef.color !== undefined) color.fromArray(lightDef.color) 317 | 318 | var range = lightDef.range !== undefined ? lightDef.range : 0 319 | 320 | switch (lightDef.type) { 321 | case 'directional': 322 | lightNode = new THREE.DirectionalLight(color) 323 | lightNode.target.position.set(0, 0, -1) 324 | lightNode.add(lightNode.target) 325 | break 326 | 327 | case 'point': 328 | lightNode = new THREE.PointLight(color) 329 | lightNode.distance = range 330 | break 331 | 332 | case 'spot': 333 | lightNode = new THREE.SpotLight(color) 334 | lightNode.distance = range 335 | // Handle spotlight properties. 336 | lightDef.spot = lightDef.spot || {} 337 | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0 338 | lightDef.spot.outerConeAngle = 339 | lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0 340 | lightNode.angle = lightDef.spot.outerConeAngle 341 | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle 342 | lightNode.target.position.set(0, 0, -1) 343 | lightNode.add(lightNode.target) 344 | break 345 | 346 | default: 347 | throw new Error('THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".') 348 | } 349 | 350 | // Some lights (e.g. spot) default to a position other than the origin. Reset the position 351 | // here, because node-level parsing will only override position if explicitly specified. 352 | lightNode.position.set(0, 0, 0) 353 | 354 | lightNode.decay = 2 355 | 356 | if (lightDef.intensity !== undefined) lightNode.intensity = lightDef.intensity 357 | 358 | lightNode.name = parser.createUniqueName(lightDef.name || 'light_' + lightIndex) 359 | 360 | dependency = Promise.resolve(lightNode) 361 | 362 | parser.cache.add(cacheKey, dependency) 363 | 364 | return dependency 365 | } 366 | 367 | GLTFLightsExtension.prototype.createNodeAttachment = function (nodeIndex) { 368 | var self = this 369 | var parser = this.parser 370 | var json = parser.json 371 | var nodeDef = json.nodes[nodeIndex] 372 | var lightDef = (nodeDef.extensions && nodeDef.extensions[this.name]) || {} 373 | var lightIndex = lightDef.light 374 | 375 | if (lightIndex === undefined) return null 376 | 377 | return this._loadLight(lightIndex).then(function (light) { 378 | return parser._getNodeRef(self.cache, lightIndex, light) 379 | }) 380 | } 381 | 382 | /** 383 | * Unlit Materials Extension 384 | * 385 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit 386 | */ 387 | function GLTFMaterialsUnlitExtension() { 388 | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT 389 | } 390 | 391 | GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { 392 | return THREE.MeshBasicMaterial 393 | } 394 | 395 | GLTFMaterialsUnlitExtension.prototype.extendParams = function (materialParams, materialDef, parser) { 396 | var pending = [] 397 | 398 | materialParams.color = new THREE.Color(1.0, 1.0, 1.0) 399 | materialParams.opacity = 1.0 400 | 401 | var metallicRoughness = materialDef.pbrMetallicRoughness 402 | 403 | if (metallicRoughness) { 404 | if (Array.isArray(metallicRoughness.baseColorFactor)) { 405 | var array = metallicRoughness.baseColorFactor 406 | 407 | materialParams.color.fromArray(array) 408 | materialParams.opacity = array[3] 409 | } 410 | } 411 | 412 | return Promise.all(pending) 413 | } 414 | 415 | /** 416 | * Clearcoat Materials Extension 417 | * 418 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat 419 | */ 420 | function GLTFMaterialsClearcoatExtension(parser) { 421 | this.parser = parser 422 | this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT 423 | } 424 | 425 | GLTFMaterialsClearcoatExtension.prototype.getMaterialType = function (materialIndex) { 426 | var parser = this.parser 427 | var materialDef = parser.json.materials[materialIndex] 428 | 429 | if (!materialDef.extensions || !materialDef.extensions[this.name]) return null 430 | 431 | return THREE.MeshPhysicalMaterial 432 | } 433 | 434 | GLTFMaterialsClearcoatExtension.prototype.extendMaterialParams = function (materialIndex, materialParams) { 435 | var parser = this.parser 436 | var materialDef = parser.json.materials[materialIndex] 437 | 438 | if (!materialDef.extensions || !materialDef.extensions[this.name]) { 439 | return Promise.resolve() 440 | } 441 | 442 | var pending = [] 443 | 444 | var extension = materialDef.extensions[this.name] 445 | 446 | if (extension.clearcoatFactor !== undefined) { 447 | materialParams.clearcoat = extension.clearcoatFactor 448 | } 449 | 450 | if (extension.clearcoatRoughnessFactor !== undefined) { 451 | materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor 452 | } 453 | 454 | if (extension.clearcoatNormalTexture !== undefined) { 455 | if (extension.clearcoatNormalTexture.scale !== undefined) { 456 | var scale = extension.clearcoatNormalTexture.scale 457 | materialParams.clearcoatNormalScale = new THREE.Vector2(scale, scale) 458 | } 459 | } 460 | 461 | return Promise.all(pending) 462 | } 463 | 464 | /** 465 | * Transmission Materials Extension 466 | * 467 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission 468 | * Draft: https://github.com/KhronosGroup/glTF/pull/1698 469 | */ 470 | function GLTFMaterialsTransmissionExtension(parser) { 471 | this.parser = parser 472 | this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION 473 | } 474 | 475 | GLTFMaterialsTransmissionExtension.prototype.getMaterialType = function (materialIndex) { 476 | var parser = this.parser 477 | var materialDef = parser.json.materials[materialIndex] 478 | 479 | if (!materialDef.extensions || !materialDef.extensions[this.name]) return null 480 | 481 | return THREE.MeshPhysicalMaterial 482 | } 483 | 484 | GLTFMaterialsTransmissionExtension.prototype.extendMaterialParams = function (materialIndex, materialParams) { 485 | var parser = this.parser 486 | var materialDef = parser.json.materials[materialIndex] 487 | 488 | if (!materialDef.extensions || !materialDef.extensions[this.name]) { 489 | return Promise.resolve() 490 | } 491 | 492 | var pending = [] 493 | 494 | var extension = materialDef.extensions[this.name] 495 | 496 | if (extension.transmissionFactor !== undefined) { 497 | materialParams.transmission = extension.transmissionFactor 498 | } 499 | 500 | return Promise.all(pending) 501 | } 502 | 503 | /** 504 | * BasisU Texture Extension 505 | * 506 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu 507 | * (draft PR https://github.com/KhronosGroup/glTF/pull/1751) 508 | */ 509 | function GLTFTextureBasisUExtension(parser) { 510 | this.parser = parser 511 | this.name = EXTENSIONS.KHR_TEXTURE_BASISU 512 | } 513 | 514 | GLTFTextureBasisUExtension.prototype.loadTexture = function (textureIndex) { 515 | return Promise.resolve(new THREE.Texture()) 516 | } 517 | 518 | /** 519 | * GPU Instancing Extension 520 | * 521 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing 522 | * 523 | */ 524 | class GLTFMeshGpuInstancing { 525 | constructor(parser) { 526 | this.name = EXTENSIONS.EXT_MESH_GPU_INSTANCING 527 | this.parser = parser 528 | } 529 | 530 | createNodeMesh(nodeIndex) { 531 | const json = this.parser.json 532 | const nodeDef = json.nodes[nodeIndex] 533 | 534 | if (!nodeDef.extensions || !nodeDef.extensions[this.name] || nodeDef.mesh === undefined) { 535 | return null 536 | } 537 | 538 | const meshDef = json.meshes[nodeDef.mesh] 539 | 540 | // No Points or Lines + Instancing support yet 541 | for (const primitive of meshDef.primitives) { 542 | if ( 543 | primitive.mode !== WEBGL_CONSTANTS.TRIANGLES && 544 | primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_STRIP && 545 | primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_FAN && 546 | primitive.mode !== undefined 547 | ) { 548 | return null 549 | } 550 | } 551 | 552 | const extensionDef = nodeDef.extensions[this.name] 553 | const attributesDef = extensionDef.attributes 554 | // @TODO: Can we support InstancedMesh + SkinnedMesh? 555 | const pending = [] 556 | const attributes = {} 557 | 558 | for (const key in attributesDef) { 559 | pending.push( 560 | this.parser.getDependency('accessor', attributesDef[key]).then((accessor) => { 561 | attributes[key] = accessor 562 | return attributes[key] 563 | }) 564 | ) 565 | } 566 | if (pending.length < 1) { 567 | return null 568 | } 569 | pending.push(this.parser.createNodeMesh(nodeIndex)) 570 | 571 | return Promise.all(pending).then((results) => { 572 | const nodeObject = results.pop() 573 | const meshes = nodeObject.isGroup ? nodeObject.children : [nodeObject] 574 | const count = results[0].count // All attribute counts should be same 575 | 576 | const instancedMeshes = [] 577 | for (const mesh of meshes) { 578 | // Temporal variables 579 | const m = new THREE.Matrix4() 580 | const p = new THREE.Vector3() 581 | const q = new THREE.Quaternion() 582 | const s = new THREE.Vector3(1, 1, 1) 583 | const instancedMesh = new THREE.InstancedMesh(mesh.geometry, mesh.material, count) 584 | 585 | for (let i = 0; i < count; i++) { 586 | if (attributes.TRANSLATION) { 587 | p.fromBufferAttribute(attributes.TRANSLATION, i) 588 | } 589 | if (attributes.ROTATION) { 590 | q.fromBufferAttribute(attributes.ROTATION, i) 591 | } 592 | if (attributes.SCALE) { 593 | s.fromBufferAttribute(attributes.SCALE, i) 594 | } 595 | instancedMesh.setMatrixAt(i, m.compose(p, q, s)) 596 | } 597 | 598 | // Add instance attributes to the geometry, excluding TRS. 599 | for (const attributeName in attributes) { 600 | if (attributeName !== 'TRANSLATION' && attributeName !== 'ROTATION' && attributeName !== 'SCALE') { 601 | mesh.geometry.setAttribute(attributeName, attributes[attributeName]) 602 | } 603 | } 604 | 605 | // Just in case 606 | THREE.Object3D.prototype.copy.call(instancedMesh, mesh) 607 | this.parser.assignFinalMaterial(instancedMesh) 608 | instancedMeshes.push(instancedMesh) 609 | } 610 | if (nodeObject.isGroup) { 611 | nodeObject.clear() 612 | nodeObject.add(...instancedMeshes) 613 | return nodeObject 614 | } 615 | return instancedMeshes[0] 616 | }) 617 | } 618 | } 619 | 620 | /* BINARY EXTENSION */ 621 | var BINARY_EXTENSION_HEADER_MAGIC = 'glTF' 622 | var BINARY_EXTENSION_HEADER_LENGTH = 12 623 | var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4e4f534a, BIN: 0x004e4942 } 624 | 625 | function GLTFBinaryExtension(data) { 626 | this.name = EXTENSIONS.KHR_BINARY_GLTF 627 | this.content = null 628 | this.body = null 629 | 630 | var headerView = new DataView(data, 0, BINARY_EXTENSION_HEADER_LENGTH) 631 | 632 | this.header = { 633 | magic: THREE.LoaderUtils.decodeText(new Uint8Array(data.slice(0, 4))), 634 | version: headerView.getUint32(4, true), 635 | length: headerView.getUint32(8, true), 636 | } 637 | 638 | if (this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC) { 639 | throw new Error('THREE.GLTFLoader: Unsupported glTF-Binary header.') 640 | } else if (this.header.version < 2.0) { 641 | throw new Error('THREE.GLTFLoader: Legacy binary file detected.') 642 | } 643 | 644 | var chunkView = new DataView(data, BINARY_EXTENSION_HEADER_LENGTH) 645 | var chunkIndex = 0 646 | 647 | while (chunkIndex < chunkView.byteLength) { 648 | var chunkLength = chunkView.getUint32(chunkIndex, true) 649 | chunkIndex += 4 650 | 651 | var chunkType = chunkView.getUint32(chunkIndex, true) 652 | chunkIndex += 4 653 | 654 | if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON) { 655 | var contentArray = new Uint8Array(data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength) 656 | this.content = THREE.LoaderUtils.decodeText(contentArray) 657 | } else if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN) { 658 | var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex 659 | this.body = data.slice(byteOffset, byteOffset + chunkLength) 660 | } 661 | 662 | // Clients must ignore chunks with unknown types. 663 | 664 | chunkIndex += chunkLength 665 | } 666 | 667 | if (this.content === null) { 668 | throw new Error('THREE.GLTFLoader: JSON content not found.') 669 | } 670 | } 671 | 672 | /** 673 | * DRACO Mesh Compression Extension 674 | * 675 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression 676 | */ 677 | function GLTFDracoMeshCompressionExtension(json, dracoLoader) { 678 | if (!dracoLoader) { 679 | throw new Error('THREE.GLTFLoader: No DRACOLoader instance provided.') 680 | } 681 | 682 | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION 683 | this.json = json 684 | this.dracoLoader = dracoLoader 685 | } 686 | 687 | GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function (primitive, parser) { 688 | var json = this.json 689 | var dracoLoader = this.dracoLoader 690 | var bufferViewIndex = primitive.extensions[this.name].bufferView 691 | var gltfAttributeMap = primitive.extensions[this.name].attributes 692 | var threeAttributeMap = {} 693 | var attributeNormalizedMap = {} 694 | var attributeTypeMap = {} 695 | 696 | for (var attributeName in gltfAttributeMap) { 697 | var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase() 698 | 699 | threeAttributeMap[threeAttributeName] = gltfAttributeMap[attributeName] 700 | } 701 | 702 | for (attributeName in primitive.attributes) { 703 | var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase() 704 | 705 | if (gltfAttributeMap[attributeName] !== undefined) { 706 | var accessorDef = json.accessors[primitive.attributes[attributeName]] 707 | var componentType = WEBGL_COMPONENT_TYPES[accessorDef.componentType] 708 | 709 | attributeTypeMap[threeAttributeName] = componentType 710 | attributeNormalizedMap[threeAttributeName] = accessorDef.normalized === true 711 | } 712 | } 713 | 714 | return parser.getDependency('bufferView', bufferViewIndex).then(function (bufferView) { 715 | return new Promise(function (resolve) { 716 | dracoLoader.decodeDracoFile( 717 | bufferView, 718 | function (geometry) { 719 | for (var attributeName in geometry.attributes) { 720 | var attribute = geometry.attributes[attributeName] 721 | var normalized = attributeNormalizedMap[attributeName] 722 | 723 | if (normalized !== undefined) attribute.normalized = normalized 724 | } 725 | 726 | resolve(geometry) 727 | }, 728 | threeAttributeMap, 729 | attributeTypeMap 730 | ) 731 | }) 732 | }) 733 | } 734 | 735 | /** 736 | * Texture Transform Extension 737 | * 738 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform 739 | */ 740 | function GLTFTextureTransformExtension() { 741 | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM 742 | } 743 | 744 | GLTFTextureTransformExtension.prototype.extendTexture = function (texture, transform) { 745 | texture = texture.clone() 746 | 747 | if (transform.offset !== undefined) { 748 | texture.offset.fromArray(transform.offset) 749 | } 750 | 751 | if (transform.rotation !== undefined) { 752 | texture.rotation = transform.rotation 753 | } 754 | 755 | if (transform.scale !== undefined) { 756 | texture.repeat.fromArray(transform.scale) 757 | } 758 | 759 | if (transform.texCoord !== undefined) { 760 | console.warn('THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.') 761 | } 762 | 763 | texture.needsUpdate = true 764 | 765 | return texture 766 | } 767 | 768 | /** 769 | * Specular-Glossiness Extension 770 | * 771 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness 772 | */ 773 | 774 | function GLTFMaterialsPbrSpecularGlossinessExtension() { 775 | return { 776 | name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, 777 | 778 | specularGlossinessParams: [ 779 | 'color', 780 | 'map', 781 | 'lightMap', 782 | 'lightMapIntensity', 783 | 'aoMap', 784 | 'aoMapIntensity', 785 | 'emissive', 786 | 'emissiveIntensity', 787 | 'emissiveMap', 788 | 'bumpMap', 789 | 'bumpScale', 790 | 'normalMap', 791 | 'normalMapType', 792 | 'displacementMap', 793 | 'displacementScale', 794 | 'displacementBias', 795 | 'specularMap', 796 | 'specular', 797 | 'glossinessMap', 798 | 'glossiness', 799 | 'alphaMap', 800 | 'envMap', 801 | 'envMapIntensity', 802 | 'refractionRatio', 803 | ], 804 | 805 | getMaterialType: function () { 806 | return THREE.MeshStandardMaterial 807 | }, 808 | 809 | extendParams: function (materialParams, materialDef, parser) { 810 | var pbrSpecularGlossiness = materialDef.extensions[this.name] 811 | 812 | materialParams.color = new THREE.Color(1.0, 1.0, 1.0) 813 | materialParams.opacity = 1.0 814 | 815 | var pending = [] 816 | 817 | if (Array.isArray(pbrSpecularGlossiness.diffuseFactor)) { 818 | var array = pbrSpecularGlossiness.diffuseFactor 819 | 820 | materialParams.color.fromArray(array) 821 | materialParams.opacity = array[3] 822 | } 823 | 824 | materialParams.emissive = new THREE.Color(0.0, 0.0, 0.0) 825 | materialParams.glossiness = 826 | pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0 827 | materialParams.specular = new THREE.Color(1.0, 1.0, 1.0) 828 | 829 | if (Array.isArray(pbrSpecularGlossiness.specularFactor)) { 830 | materialParams.specular.fromArray(pbrSpecularGlossiness.specularFactor) 831 | } 832 | 833 | return Promise.all(pending) 834 | }, 835 | 836 | createMaterial: function (materialParams) { 837 | var material = new THREE.MeshStandardMaterial(materialParams) 838 | material.fog = true 839 | 840 | material.color = materialParams.color 841 | 842 | material.map = materialParams.map === undefined ? null : materialParams.map 843 | 844 | material.lightMap = null 845 | material.lightMapIntensity = 1.0 846 | 847 | material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap 848 | material.aoMapIntensity = 1.0 849 | 850 | material.emissive = materialParams.emissive 851 | material.emissiveIntensity = 1.0 852 | material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap 853 | 854 | material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap 855 | material.bumpScale = 1 856 | 857 | material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap 858 | material.normalMapType = THREE.TangentSpaceNormalMap 859 | 860 | if (materialParams.normalScale) material.normalScale = materialParams.normalScale 861 | 862 | material.displacementMap = null 863 | material.displacementScale = 1 864 | material.displacementBias = 0 865 | 866 | material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap 867 | material.specular = materialParams.specular 868 | 869 | material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap 870 | material.glossiness = materialParams.glossiness 871 | 872 | material.alphaMap = null 873 | 874 | material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap 875 | material.envMapIntensity = 1.0 876 | 877 | material.refractionRatio = 0.98 878 | 879 | return material 880 | }, 881 | } 882 | } 883 | 884 | /** 885 | * Mesh Quantization Extension 886 | * 887 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization 888 | */ 889 | function GLTFMeshQuantizationExtension() { 890 | this.name = EXTENSIONS.KHR_MESH_QUANTIZATION 891 | } 892 | 893 | /*********************************/ 894 | /********** INTERPOLATION ********/ 895 | /*********************************/ 896 | 897 | // Spline Interpolation 898 | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation 899 | function GLTFCubicSplineInterpolant(parameterPositions, sampleValues, sampleSize, resultBuffer) { 900 | THREE.Interpolant.call(this, parameterPositions, sampleValues, sampleSize, resultBuffer) 901 | } 902 | 903 | GLTFCubicSplineInterpolant.prototype = Object.create(THREE.Interpolant.prototype) 904 | GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant 905 | 906 | GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function (index) { 907 | // Copies a sample value to the result buffer. See description of glTF 908 | // CUBICSPLINE values layout in interpolate_() function below. 909 | 910 | var result = this.resultBuffer, 911 | values = this.sampleValues, 912 | valueSize = this.valueSize, 913 | offset = index * valueSize * 3 + valueSize 914 | 915 | for (var i = 0; i !== valueSize; i++) { 916 | result[i] = values[offset + i] 917 | } 918 | 919 | return result 920 | } 921 | 922 | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_ 923 | 924 | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_ 925 | 926 | GLTFCubicSplineInterpolant.prototype.interpolate_ = function (i1, t0, t, t1) { 927 | var result = this.resultBuffer 928 | var values = this.sampleValues 929 | var stride = this.valueSize 930 | 931 | var stride2 = stride * 2 932 | var stride3 = stride * 3 933 | 934 | var td = t1 - t0 935 | 936 | var p = (t - t0) / td 937 | var pp = p * p 938 | var ppp = pp * p 939 | 940 | var offset1 = i1 * stride3 941 | var offset0 = offset1 - stride3 942 | 943 | var s2 = -2 * ppp + 3 * pp 944 | var s3 = ppp - pp 945 | var s0 = 1 - s2 946 | var s1 = s3 - pp + p 947 | 948 | // Layout of keyframe output values for CUBICSPLINE animations: 949 | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] 950 | for (var i = 0; i !== stride; i++) { 951 | var p0 = values[offset0 + i + stride] // splineVertex_k 952 | var m0 = values[offset0 + i + stride2] * td // outTangent_k * (t_k+1 - t_k) 953 | var p1 = values[offset1 + i + stride] // splineVertex_k+1 954 | var m1 = values[offset1 + i] * td // inTangent_k+1 * (t_k+1 - t_k) 955 | 956 | result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1 957 | } 958 | 959 | return result 960 | } 961 | 962 | /*********************************/ 963 | /********** INTERNALS ************/ 964 | /*********************************/ 965 | 966 | /* CONSTANTS */ 967 | 968 | var WEBGL_CONSTANTS = { 969 | FLOAT: 5126, 970 | //FLOAT_MAT2: 35674, 971 | FLOAT_MAT3: 35675, 972 | FLOAT_MAT4: 35676, 973 | FLOAT_VEC2: 35664, 974 | FLOAT_VEC3: 35665, 975 | FLOAT_VEC4: 35666, 976 | LINEAR: 9729, 977 | REPEAT: 10497, 978 | SAMPLER_2D: 35678, 979 | POINTS: 0, 980 | LINES: 1, 981 | LINE_LOOP: 2, 982 | LINE_STRIP: 3, 983 | TRIANGLES: 4, 984 | TRIANGLE_STRIP: 5, 985 | TRIANGLE_FAN: 6, 986 | UNSIGNED_BYTE: 5121, 987 | UNSIGNED_SHORT: 5123, 988 | } 989 | 990 | var WEBGL_COMPONENT_TYPES = { 991 | 5120: Int8Array, 992 | 5121: Uint8Array, 993 | 5122: Int16Array, 994 | 5123: Uint16Array, 995 | 5125: Uint32Array, 996 | 5126: Float32Array, 997 | } 998 | 999 | var WEBGL_FILTERS = { 1000 | 9728: THREE.NearestFilter, 1001 | 9729: THREE.LinearFilter, 1002 | 9984: THREE.NearestMipmapNearestFilter, 1003 | 9985: THREE.LinearMipmapNearestFilter, 1004 | 9986: THREE.NearestMipmapLinearFilter, 1005 | 9987: THREE.LinearMipmapLinearFilter, 1006 | } 1007 | 1008 | var WEBGL_WRAPPINGS = { 1009 | 33071: THREE.ClampToEdgeWrapping, 1010 | 33648: THREE.MirroredRepeatWrapping, 1011 | 10497: THREE.RepeatWrapping, 1012 | } 1013 | 1014 | var WEBGL_TYPE_SIZES = { 1015 | SCALAR: 1, 1016 | VEC2: 2, 1017 | VEC3: 3, 1018 | VEC4: 4, 1019 | MAT2: 4, 1020 | MAT3: 9, 1021 | MAT4: 16, 1022 | } 1023 | 1024 | var ATTRIBUTES = { 1025 | POSITION: 'position', 1026 | NORMAL: 'normal', 1027 | TANGENT: 'tangent', 1028 | TEXCOORD_0: 'uv', 1029 | TEXCOORD_1: 'uv2', 1030 | COLOR_0: 'color', 1031 | WEIGHTS_0: 'skinWeight', 1032 | JOINTS_0: 'skinIndex', 1033 | } 1034 | 1035 | var PATH_PROPERTIES = { 1036 | scale: 'scale', 1037 | translation: 'position', 1038 | rotation: 'quaternion', 1039 | weights: 'morphTargetInfluences', 1040 | } 1041 | 1042 | var INTERPOLATION = { 1043 | CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each 1044 | // keyframe track will be initialized with a default interpolation type, then modified. 1045 | LINEAR: THREE.InterpolateLinear, 1046 | STEP: THREE.InterpolateDiscrete, 1047 | } 1048 | 1049 | var ALPHA_MODES = { 1050 | OPAQUE: 'OPAQUE', 1051 | MASK: 'MASK', 1052 | BLEND: 'BLEND', 1053 | } 1054 | 1055 | /* UTILITY FUNCTIONS */ 1056 | 1057 | function resolveURL(url, path) { 1058 | // Invalid URL 1059 | if (typeof url !== 'string' || url === '') return '' 1060 | 1061 | // Host Relative URL 1062 | if (/^https?:\/\//i.test(path) && /^\//.test(url)) { 1063 | path = path.replace(/(^https?:\/\/[^\/]+).*/i, '$1') 1064 | } 1065 | 1066 | // Absolute URL http://,https://,// 1067 | if (/^(https?:)?\/\//i.test(url)) return url 1068 | 1069 | // Data URI 1070 | if (/^data:.*,.*$/i.test(url)) return url 1071 | 1072 | // Blob URL 1073 | if (/^blob:.*$/i.test(url)) return url 1074 | 1075 | // Relative URL 1076 | return path + url 1077 | } 1078 | 1079 | /** 1080 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 1081 | */ 1082 | function createDefaultMaterial(cache) { 1083 | if (cache['DefaultMaterial'] === undefined) { 1084 | cache['DefaultMaterial'] = new THREE.MeshStandardMaterial({ 1085 | color: 0xffffff, 1086 | emissive: 0x000000, 1087 | metalness: 1, 1088 | roughness: 1, 1089 | transparent: false, 1090 | depthTest: true, 1091 | side: THREE.FrontSide, 1092 | }) 1093 | } 1094 | 1095 | return cache['DefaultMaterial'] 1096 | } 1097 | 1098 | function addUnknownExtensionsToUserData(knownExtensions, object, objectDef) { 1099 | // Add unknown glTF extensions to an object's userData. 1100 | 1101 | for (var name in objectDef.extensions) { 1102 | if (knownExtensions[name] === undefined) { 1103 | object.userData.gltfExtensions = object.userData.gltfExtensions || {} 1104 | object.userData.gltfExtensions[name] = objectDef.extensions[name] 1105 | } 1106 | } 1107 | } 1108 | 1109 | /** 1110 | * @param {THREE.Object3D|THREE.Material|THREE.BufferGeometry} object 1111 | * @param {GLTF.definition} gltfDef 1112 | */ 1113 | function assignExtrasToUserData(object, gltfDef) { 1114 | if (gltfDef.extras !== undefined) { 1115 | if (typeof gltfDef.extras === 'object') { 1116 | Object.assign(object.userData, gltfDef.extras) 1117 | } else { 1118 | console.warn('THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras) 1119 | } 1120 | } 1121 | } 1122 | 1123 | /** 1124 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets 1125 | * 1126 | * @param {THREE.BufferGeometry} geometry 1127 | * @param {Array} targets 1128 | * @param {GLTFParser} parser 1129 | * @return {Promise} 1130 | */ 1131 | function addMorphTargets(geometry, targets, parser) { 1132 | var hasMorphPosition = false 1133 | var hasMorphNormal = false 1134 | 1135 | for (var i = 0, il = targets.length; i < il; i++) { 1136 | var target = targets[i] 1137 | 1138 | if (target.POSITION !== undefined) hasMorphPosition = true 1139 | if (target.NORMAL !== undefined) hasMorphNormal = true 1140 | 1141 | if (hasMorphPosition && hasMorphNormal) break 1142 | } 1143 | 1144 | if (!hasMorphPosition && !hasMorphNormal) return Promise.resolve(geometry) 1145 | 1146 | var pendingPositionAccessors = [] 1147 | var pendingNormalAccessors = [] 1148 | 1149 | for (var i = 0, il = targets.length; i < il; i++) { 1150 | var target = targets[i] 1151 | 1152 | if (hasMorphPosition) { 1153 | var pendingAccessor = 1154 | target.POSITION !== undefined ? parser.getDependency('accessor', target.POSITION) : geometry.attributes.position 1155 | 1156 | pendingPositionAccessors.push(pendingAccessor) 1157 | } 1158 | 1159 | if (hasMorphNormal) { 1160 | var pendingAccessor = 1161 | target.NORMAL !== undefined ? parser.getDependency('accessor', target.NORMAL) : geometry.attributes.normal 1162 | 1163 | pendingNormalAccessors.push(pendingAccessor) 1164 | } 1165 | } 1166 | 1167 | return Promise.all([Promise.all(pendingPositionAccessors), Promise.all(pendingNormalAccessors)]).then(function ( 1168 | accessors 1169 | ) { 1170 | var morphPositions = accessors[0] 1171 | var morphNormals = accessors[1] 1172 | 1173 | if (hasMorphPosition) geometry.morphAttributes.position = morphPositions 1174 | if (hasMorphNormal) geometry.morphAttributes.normal = morphNormals 1175 | geometry.morphTargetsRelative = true 1176 | 1177 | return geometry 1178 | }) 1179 | } 1180 | 1181 | function createPrimitiveKey(primitiveDef) { 1182 | var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION] 1183 | var geometryKey 1184 | 1185 | if (dracoExtension) { 1186 | geometryKey = 1187 | 'draco:' + 1188 | dracoExtension.bufferView + 1189 | ':' + 1190 | dracoExtension.indices + 1191 | ':' + 1192 | createAttributesKey(dracoExtension.attributes) 1193 | } else { 1194 | geometryKey = primitiveDef.indices + ':' + createAttributesKey(primitiveDef.attributes) + ':' + primitiveDef.mode 1195 | } 1196 | 1197 | return geometryKey 1198 | } 1199 | 1200 | function createAttributesKey(attributes) { 1201 | var attributesKey = '' 1202 | 1203 | var keys = Object.keys(attributes).sort() 1204 | 1205 | for (var i = 0, il = keys.length; i < il; i++) { 1206 | attributesKey += keys[i] + ':' + attributes[keys[i]] + ';' 1207 | } 1208 | 1209 | return attributesKey 1210 | } 1211 | 1212 | /* GLTF PARSER */ 1213 | 1214 | function GLTFParser(json, options) { 1215 | this.json = json || {} 1216 | this.extensions = {} 1217 | this.plugins = {} 1218 | this.options = options || {} 1219 | 1220 | // loader object cache 1221 | this.cache = new GLTFRegistry() 1222 | 1223 | // associations between Three.js objects and glTF elements 1224 | this.associations = new Map() 1225 | 1226 | // BufferGeometry caching 1227 | this.primitiveCache = {} 1228 | 1229 | // Object3D instance caches 1230 | this.meshCache = { refs: {}, uses: {} } 1231 | this.cameraCache = { refs: {}, uses: {} } 1232 | this.lightCache = { refs: {}, uses: {} } 1233 | 1234 | // Track node names, to ensure no duplicates 1235 | this.nodeNamesUsed = {} 1236 | 1237 | // Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the 1238 | // expensive work of uploading a texture to the GPU off the main thread. 1239 | if (typeof createImageBitmap !== 'undefined' && /Firefox/.test(navigator.userAgent) === false) { 1240 | this.textureLoader = new THREE.ImageBitmapLoader(this.options.manager) 1241 | } else { 1242 | this.textureLoader = new THREE.TextureLoader(this.options.manager) 1243 | } 1244 | 1245 | this.textureLoader.setCrossOrigin(this.options.crossOrigin) 1246 | 1247 | this.fileLoader = new THREE.FileLoader(this.options.manager) 1248 | this.fileLoader.setResponseType('arraybuffer') 1249 | 1250 | if (this.options.crossOrigin === 'use-credentials') { 1251 | this.fileLoader.setWithCredentials(true) 1252 | } 1253 | } 1254 | 1255 | GLTFParser.prototype.setExtensions = function (extensions) { 1256 | this.extensions = extensions 1257 | } 1258 | 1259 | GLTFParser.prototype.setPlugins = function (plugins) { 1260 | this.plugins = plugins 1261 | } 1262 | 1263 | GLTFParser.prototype.parse = function (onLoad, onError) { 1264 | var parser = this 1265 | var json = this.json 1266 | var extensions = this.extensions 1267 | 1268 | // Clear the loader cache 1269 | this.cache.removeAll() 1270 | 1271 | // Mark the special nodes/meshes in json for efficient parse 1272 | this._invokeAll(function (ext) { 1273 | return ext._markDefs && ext._markDefs() 1274 | }) 1275 | 1276 | Promise.all([this.getDependencies('scene'), this.getDependencies('animation'), this.getDependencies('camera')]) 1277 | .then(function (dependencies) { 1278 | var result = { 1279 | scene: dependencies[0][json.scene || 0], 1280 | scenes: dependencies[0], 1281 | animations: dependencies[1], 1282 | cameras: dependencies[2], 1283 | asset: json.asset, 1284 | parser: parser, 1285 | userData: {}, 1286 | } 1287 | 1288 | addUnknownExtensionsToUserData(extensions, result, json) 1289 | 1290 | assignExtrasToUserData(result, json) 1291 | 1292 | onLoad(result) 1293 | }) 1294 | .catch(onError) 1295 | } 1296 | 1297 | /** 1298 | * Marks the special nodes/meshes in json for efficient parse. 1299 | */ 1300 | GLTFParser.prototype._markDefs = function () { 1301 | var nodeDefs = this.json.nodes || [] 1302 | var skinDefs = this.json.skins || [] 1303 | var meshDefs = this.json.meshes || [] 1304 | 1305 | // Nothing in the node definition indicates whether it is a Bone or an 1306 | // Object3D. Use the skins' joint references to mark bones. 1307 | for (var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex++) { 1308 | var joints = skinDefs[skinIndex].joints 1309 | 1310 | for (var i = 0, il = joints.length; i < il; i++) { 1311 | nodeDefs[joints[i]].isBone = true 1312 | } 1313 | } 1314 | 1315 | // Iterate over all nodes, marking references to shared resources, 1316 | // as well as skeleton joints. 1317 | for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) { 1318 | var nodeDef = nodeDefs[nodeIndex] 1319 | 1320 | if (nodeDef.mesh !== undefined) { 1321 | this._addNodeRef(this.meshCache, nodeDef.mesh) 1322 | 1323 | // Nothing in the mesh definition indicates whether it is 1324 | // a SkinnedMesh or Mesh. Use the node's mesh reference 1325 | // to mark SkinnedMesh if node has skin. 1326 | if (nodeDef.skin !== undefined) { 1327 | meshDefs[nodeDef.mesh].isSkinnedMesh = true 1328 | } 1329 | } 1330 | 1331 | if (nodeDef.camera !== undefined) { 1332 | this._addNodeRef(this.cameraCache, nodeDef.camera) 1333 | } 1334 | } 1335 | } 1336 | 1337 | /** 1338 | * Counts references to shared node / Object3D resources. These resources 1339 | * can be reused, or "instantiated", at multiple nodes in the scene 1340 | * hierarchy. Mesh, Camera, and Light instances are instantiated and must 1341 | * be marked. Non-scenegraph resources (like Materials, Geometries, and 1342 | * Textures) can be reused directly and are not marked here. 1343 | * 1344 | * Example: CesiumMilkTruck sample model reuses "Wheel" meshes. 1345 | */ 1346 | GLTFParser.prototype._addNodeRef = function (cache, index) { 1347 | if (index === undefined) return 1348 | 1349 | if (cache.refs[index] === undefined) { 1350 | cache.refs[index] = cache.uses[index] = 0 1351 | } 1352 | 1353 | cache.refs[index]++ 1354 | } 1355 | 1356 | /** Returns a reference to a shared resource, cloning it if necessary. */ 1357 | GLTFParser.prototype._getNodeRef = function (cache, index, object) { 1358 | if (cache.refs[index] <= 1) return object 1359 | 1360 | var ref = object.clone() 1361 | 1362 | ref.name += '_instance_' + cache.uses[index]++ 1363 | 1364 | return ref 1365 | } 1366 | 1367 | GLTFParser.prototype._invokeOne = function (func) { 1368 | var extensions = Object.values(this.plugins) 1369 | extensions.push(this) 1370 | 1371 | for (var i = 0; i < extensions.length; i++) { 1372 | var result = func(extensions[i]) 1373 | 1374 | if (result) return result 1375 | } 1376 | } 1377 | 1378 | GLTFParser.prototype._invokeAll = function (func) { 1379 | var extensions = Object.values(this.plugins) 1380 | extensions.unshift(this) 1381 | 1382 | var pending = [] 1383 | 1384 | for (var i = 0; i < extensions.length; i++) { 1385 | var result = func(extensions[i]) 1386 | 1387 | if (result) pending.push(result) 1388 | } 1389 | 1390 | return pending 1391 | } 1392 | 1393 | /** 1394 | * Requests the specified dependency asynchronously, with caching. 1395 | * @param {string} type 1396 | * @param {number} index 1397 | * @return {Promise} 1398 | */ 1399 | GLTFParser.prototype.getDependency = function (type, index) { 1400 | var cacheKey = type + ':' + index 1401 | var dependency = this.cache.get(cacheKey) 1402 | 1403 | if (!dependency) { 1404 | switch (type) { 1405 | case 'scene': 1406 | dependency = this.loadScene(index) 1407 | break 1408 | 1409 | case 'node': 1410 | dependency = this.loadNode(index) 1411 | break 1412 | 1413 | case 'mesh': 1414 | dependency = this._invokeOne(function (ext) { 1415 | return ext.loadMesh && ext.loadMesh(index) 1416 | }) 1417 | break 1418 | 1419 | case 'accessor': 1420 | dependency = this.loadAccessor(index) 1421 | break 1422 | 1423 | case 'bufferView': 1424 | dependency = Promise.resolve(new Float32Array(0)) 1425 | break 1426 | 1427 | case 'buffer': 1428 | dependency = Promise.resolve(new Float32Array(0)) 1429 | break 1430 | 1431 | case 'material': 1432 | dependency = this._invokeOne(function (ext) { 1433 | return ext.loadMaterial && ext.loadMaterial(index) 1434 | }) 1435 | break 1436 | 1437 | case 'skin': 1438 | dependency = this.loadSkin(index) 1439 | break 1440 | 1441 | case 'animation': 1442 | dependency = this.loadAnimation(index) 1443 | break 1444 | 1445 | case 'camera': 1446 | dependency = this.loadCamera(index) 1447 | break 1448 | 1449 | default: 1450 | throw new Error('Unknown type: ' + type) 1451 | } 1452 | 1453 | this.cache.add(cacheKey, dependency) 1454 | } 1455 | 1456 | return dependency 1457 | } 1458 | 1459 | /** 1460 | * Requests all dependencies of the specified type asynchronously, with caching. 1461 | * @param {string} type 1462 | * @return {Promise>} 1463 | */ 1464 | GLTFParser.prototype.getDependencies = function (type) { 1465 | var dependencies = this.cache.get(type) 1466 | 1467 | if (!dependencies) { 1468 | var parser = this 1469 | var defs = this.json[type + (type === 'mesh' ? 'es' : 's')] || [] 1470 | 1471 | dependencies = Promise.all( 1472 | defs.map(function (def, index) { 1473 | return parser.getDependency(type, index) 1474 | }) 1475 | ) 1476 | 1477 | this.cache.add(type, dependencies) 1478 | } 1479 | 1480 | return dependencies 1481 | } 1482 | 1483 | /** 1484 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1485 | * @param {number} bufferIndex 1486 | * @return {Promise} 1487 | */ 1488 | GLTFParser.prototype.loadBuffer = function (bufferIndex) { 1489 | var bufferDef = this.json.buffers[bufferIndex] 1490 | var loader = this.fileLoader 1491 | 1492 | if (bufferDef.type && bufferDef.type !== 'arraybuffer') { 1493 | throw new Error('THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.') 1494 | } 1495 | 1496 | // If present, GLB container is required to be the first buffer. 1497 | if (bufferDef.uri === undefined && bufferIndex === 0) { 1498 | return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body) 1499 | } 1500 | 1501 | var options = this.options 1502 | 1503 | return new Promise(function (resolve, reject) { 1504 | loader.load(resolveURL(bufferDef.uri, options.path), resolve, undefined, function () { 1505 | reject(new Error('THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".')) 1506 | }) 1507 | }) 1508 | } 1509 | 1510 | /** 1511 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1512 | * @param {number} bufferViewIndex 1513 | * @return {Promise} 1514 | */ 1515 | GLTFParser.prototype.loadBufferView = function (bufferViewIndex) { 1516 | var bufferViewDef = this.json.bufferViews[bufferViewIndex] 1517 | 1518 | return this.getDependency('buffer', bufferViewDef.buffer).then(function (buffer) { 1519 | var byteLength = bufferViewDef.byteLength || 0 1520 | var byteOffset = bufferViewDef.byteOffset || 0 1521 | return buffer.slice(byteOffset, byteOffset + byteLength) 1522 | }) 1523 | } 1524 | 1525 | /** 1526 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors 1527 | * @param {number} accessorIndex 1528 | * @return {Promise} 1529 | */ 1530 | GLTFParser.prototype.loadAccessor = function (accessorIndex) { 1531 | var parser = this 1532 | var json = this.json 1533 | 1534 | var accessorDef = this.json.accessors[accessorIndex] 1535 | 1536 | if (accessorDef.bufferView === undefined && accessorDef.sparse === undefined) { 1537 | // Ignore empty accessors, which may be used to declare runtime 1538 | // information about attributes coming from another source (e.g. Draco 1539 | // compression extension). 1540 | return Promise.resolve(null) 1541 | } 1542 | 1543 | var pendingBufferViews = [] 1544 | 1545 | if (accessorDef.bufferView !== undefined) { 1546 | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.bufferView)) 1547 | } else { 1548 | pendingBufferViews.push(null) 1549 | } 1550 | 1551 | if (accessorDef.sparse !== undefined) { 1552 | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.indices.bufferView)) 1553 | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.values.bufferView)) 1554 | } 1555 | 1556 | return Promise.all(pendingBufferViews).then(function (bufferViews) { 1557 | var bufferView = bufferViews[0] 1558 | 1559 | var itemSize = WEBGL_TYPE_SIZES[accessorDef.type] 1560 | var TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType] 1561 | 1562 | // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. 1563 | var elementBytes = TypedArray.BYTES_PER_ELEMENT 1564 | var itemBytes = elementBytes * itemSize 1565 | var byteOffset = accessorDef.byteOffset || 0 1566 | var byteStride = 1567 | accessorDef.bufferView !== undefined ? json.bufferViews[accessorDef.bufferView].byteStride : undefined 1568 | var normalized = accessorDef.normalized === true 1569 | var array, bufferAttribute 1570 | 1571 | // The buffer is not interleaved if the stride is the item size in bytes. 1572 | if (byteStride && byteStride !== itemBytes) { 1573 | // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer 1574 | // This makes sure that IBA.count reflects accessor.count properly 1575 | var ibSlice = Math.floor(byteOffset / byteStride) 1576 | var ibCacheKey = 1577 | 'InterleavedBuffer:' + 1578 | accessorDef.bufferView + 1579 | ':' + 1580 | accessorDef.componentType + 1581 | ':' + 1582 | ibSlice + 1583 | ':' + 1584 | accessorDef.count 1585 | var ib = parser.cache.get(ibCacheKey) 1586 | 1587 | if (!ib) { 1588 | array = new TypedArray(bufferView, ibSlice * byteStride, (accessorDef.count * byteStride) / elementBytes) 1589 | 1590 | // Integer parameters to IB/IBA are in array elements, not bytes. 1591 | ib = new THREE.InterleavedBuffer(array, byteStride / elementBytes) 1592 | 1593 | parser.cache.add(ibCacheKey, ib) 1594 | } 1595 | 1596 | bufferAttribute = new THREE.InterleavedBufferAttribute( 1597 | ib, 1598 | itemSize, 1599 | (byteOffset % byteStride) / elementBytes, 1600 | normalized 1601 | ) 1602 | } else { 1603 | if (bufferView === null) { 1604 | array = new TypedArray(accessorDef.count * itemSize) 1605 | } else { 1606 | array = new TypedArray(bufferView, byteOffset, accessorDef.count * itemSize) 1607 | } 1608 | bufferAttribute = new THREE.BufferAttribute(array, itemSize, normalized) 1609 | } 1610 | 1611 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors 1612 | if (accessorDef.sparse !== undefined) { 1613 | var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR 1614 | var TypedArrayIndices = WEBGL_COMPONENT_TYPES[accessorDef.sparse.indices.componentType] 1615 | 1616 | var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0 1617 | var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0 1618 | 1619 | var sparseIndices = new TypedArrayIndices( 1620 | bufferViews[1], 1621 | byteOffsetIndices, 1622 | accessorDef.sparse.count * itemSizeIndices 1623 | ) 1624 | var sparseValues = new TypedArray(bufferViews[2], byteOffsetValues, accessorDef.sparse.count * itemSize) 1625 | 1626 | if (bufferView !== null) { 1627 | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. 1628 | bufferAttribute = new THREE.BufferAttribute( 1629 | bufferAttribute.array.slice(), 1630 | bufferAttribute.itemSize, 1631 | bufferAttribute.normalized 1632 | ) 1633 | } 1634 | 1635 | for (var i = 0, il = sparseIndices.length; i < il; i++) { 1636 | var index = sparseIndices[i] 1637 | 1638 | bufferAttribute.setX(index, sparseValues[i * itemSize]) 1639 | if (itemSize >= 2) bufferAttribute.setY(index, sparseValues[i * itemSize + 1]) 1640 | if (itemSize >= 3) bufferAttribute.setZ(index, sparseValues[i * itemSize + 2]) 1641 | if (itemSize >= 4) bufferAttribute.setW(index, sparseValues[i * itemSize + 3]) 1642 | if (itemSize >= 5) throw new Error('THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.') 1643 | } 1644 | } 1645 | 1646 | if (bufferAttribute.isInterleavedBufferAttribute) { 1647 | bufferAttribute.data.count = accessorDef.count 1648 | } else { 1649 | bufferAttribute.count = accessorDef.count 1650 | } 1651 | return bufferAttribute 1652 | }) 1653 | } 1654 | 1655 | /** 1656 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures 1657 | * @param {number} textureIndex 1658 | * @return {Promise} 1659 | */ 1660 | GLTFParser.prototype.loadTexture = function (textureIndex) { 1661 | return Promise.resolve(new THREE.Texture()) 1662 | } 1663 | 1664 | /** 1665 | * Assigns final material to a Mesh, Line, or Points instance. The instance 1666 | * already has a material (generated from the glTF material options alone) 1667 | * but reuse of the same glTF material may require multiple threejs materials 1668 | * to accomodate different primitive types, defines, etc. New materials will 1669 | * be created if necessary, and reused from a cache. 1670 | * @param {THREE.Object3D} mesh Mesh, Line, or Points instance. 1671 | */ 1672 | GLTFParser.prototype.assignFinalMaterial = function (mesh) { 1673 | var geometry = mesh.geometry 1674 | var material = mesh.material 1675 | 1676 | var useVertexTangents = geometry.attributes.tangent !== undefined 1677 | var useVertexColors = geometry.attributes.color !== undefined 1678 | var useFlatShading = geometry.attributes.normal === undefined 1679 | var useSkinning = mesh.isSkinnedMesh === true 1680 | var useMorphTargets = Object.keys(geometry.morphAttributes).length > 0 1681 | var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined 1682 | 1683 | if (mesh.isPoints) { 1684 | var cacheKey = 'PointsMaterial:' + material.uuid 1685 | 1686 | var pointsMaterial = this.cache.get(cacheKey) 1687 | 1688 | if (!pointsMaterial) { 1689 | pointsMaterial = new THREE.PointsMaterial() 1690 | THREE.Material.prototype.copy.call(pointsMaterial, material) 1691 | pointsMaterial.color.copy(material.color) 1692 | pointsMaterial.map = material.map 1693 | pointsMaterial.sizeAttenuation = false // glTF spec says points should be 1px 1694 | 1695 | this.cache.add(cacheKey, pointsMaterial) 1696 | } 1697 | 1698 | material = pointsMaterial 1699 | } else if (mesh.isLine) { 1700 | var cacheKey = 'LineBasicMaterial:' + material.uuid 1701 | 1702 | var lineMaterial = this.cache.get(cacheKey) 1703 | 1704 | if (!lineMaterial) { 1705 | lineMaterial = new THREE.LineBasicMaterial() 1706 | THREE.Material.prototype.copy.call(lineMaterial, material) 1707 | lineMaterial.color.copy(material.color) 1708 | 1709 | this.cache.add(cacheKey, lineMaterial) 1710 | } 1711 | 1712 | material = lineMaterial 1713 | } 1714 | 1715 | // Clone the material if it will be modified 1716 | if (useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets) { 1717 | var cacheKey = 'ClonedMaterial:' + material.uuid + ':' 1718 | 1719 | if (material.isGLTFSpecularGlossinessMaterial) cacheKey += 'specular-glossiness:' 1720 | if (useSkinning) cacheKey += 'skinning:' 1721 | if (useVertexTangents) cacheKey += 'vertex-tangents:' 1722 | if (useVertexColors) cacheKey += 'vertex-colors:' 1723 | if (useFlatShading) cacheKey += 'flat-shading:' 1724 | if (useMorphTargets) cacheKey += 'morph-targets:' 1725 | if (useMorphNormals) cacheKey += 'morph-normals:' 1726 | 1727 | var cachedMaterial = this.cache.get(cacheKey) 1728 | 1729 | if (!cachedMaterial) { 1730 | cachedMaterial = material.clone() 1731 | 1732 | if (useSkinning) cachedMaterial.skinning = true 1733 | if (useVertexTangents) cachedMaterial.vertexTangents = true 1734 | if (useVertexColors) cachedMaterial.vertexColors = true 1735 | if (useFlatShading) cachedMaterial.flatShading = true 1736 | if (useMorphTargets) cachedMaterial.morphTargets = true 1737 | if (useMorphNormals) cachedMaterial.morphNormals = true 1738 | 1739 | this.cache.add(cacheKey, cachedMaterial) 1740 | 1741 | this.associations.set(cachedMaterial, this.associations.get(material)) 1742 | } 1743 | 1744 | material = cachedMaterial 1745 | } 1746 | 1747 | // workarounds for mesh and geometry 1748 | 1749 | if (material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined) { 1750 | geometry.setAttribute('uv2', geometry.attributes.uv) 1751 | } 1752 | 1753 | // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 1754 | if (material.normalScale && !useVertexTangents) { 1755 | material.normalScale.y = -material.normalScale.y 1756 | } 1757 | 1758 | if (material.clearcoatNormalScale && !useVertexTangents) { 1759 | material.clearcoatNormalScale.y = -material.clearcoatNormalScale.y 1760 | } 1761 | 1762 | mesh.material = material 1763 | } 1764 | 1765 | GLTFParser.prototype.getMaterialType = function (/* materialIndex */) { 1766 | return THREE.MeshStandardMaterial 1767 | } 1768 | 1769 | /** 1770 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials 1771 | * @param {number} materialIndex 1772 | * @return {Promise} 1773 | */ 1774 | GLTFParser.prototype.loadMaterial = function (materialIndex) { 1775 | var parser = this 1776 | var json = this.json 1777 | var extensions = this.extensions 1778 | var materialDef = json.materials[materialIndex] 1779 | 1780 | var materialType 1781 | var materialParams = {} 1782 | var materialExtensions = materialDef.extensions || {} 1783 | 1784 | var pending = [] 1785 | 1786 | if (materialExtensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]) { 1787 | var sgExtension = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS] 1788 | materialType = sgExtension.getMaterialType() 1789 | pending.push(sgExtension.extendParams(materialParams, materialDef, parser)) 1790 | } else if (materialExtensions[EXTENSIONS.KHR_MATERIALS_UNLIT]) { 1791 | var kmuExtension = extensions[EXTENSIONS.KHR_MATERIALS_UNLIT] 1792 | materialType = kmuExtension.getMaterialType() 1793 | pending.push(kmuExtension.extendParams(materialParams, materialDef, parser)) 1794 | } else { 1795 | // Specification: 1796 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 1797 | 1798 | var metallicRoughness = materialDef.pbrMetallicRoughness || {} 1799 | 1800 | materialParams.color = new THREE.Color(1.0, 1.0, 1.0) 1801 | materialParams.opacity = 1.0 1802 | 1803 | if (Array.isArray(metallicRoughness.baseColorFactor)) { 1804 | var array = metallicRoughness.baseColorFactor 1805 | 1806 | materialParams.color.fromArray(array) 1807 | materialParams.opacity = array[3] 1808 | } 1809 | 1810 | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0 1811 | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0 1812 | 1813 | materialType = this._invokeOne(function (ext) { 1814 | return ext.getMaterialType && ext.getMaterialType(materialIndex) 1815 | }) 1816 | 1817 | pending.push( 1818 | Promise.all( 1819 | this._invokeAll(function (ext) { 1820 | return ext.extendMaterialParams && ext.extendMaterialParams(materialIndex, materialParams) 1821 | }) 1822 | ) 1823 | ) 1824 | } 1825 | 1826 | if (materialDef.doubleSided === true) { 1827 | materialParams.side = THREE.DoubleSide 1828 | } 1829 | 1830 | var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE 1831 | 1832 | if (alphaMode === ALPHA_MODES.BLEND) { 1833 | materialParams.transparent = true 1834 | 1835 | // See: https://github.com/mrdoob/three.js/issues/17706 1836 | materialParams.depthWrite = false 1837 | } else { 1838 | materialParams.transparent = false 1839 | 1840 | if (alphaMode === ALPHA_MODES.MASK) { 1841 | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5 1842 | } 1843 | } 1844 | 1845 | if (materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { 1846 | materialParams.normalScale = new THREE.Vector2(1, 1) 1847 | 1848 | if (materialDef.normalTexture.scale !== undefined) { 1849 | materialParams.normalScale.set(materialDef.normalTexture.scale, materialDef.normalTexture.scale) 1850 | } 1851 | } 1852 | 1853 | if (materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { 1854 | if (materialDef.occlusionTexture.strength !== undefined) { 1855 | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength 1856 | } 1857 | } 1858 | 1859 | if (materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial) { 1860 | materialParams.emissive = new THREE.Color().fromArray(materialDef.emissiveFactor) 1861 | } 1862 | 1863 | return Promise.all(pending).then(function () { 1864 | var material 1865 | 1866 | material = new materialType(materialParams) 1867 | 1868 | if (materialDef.name) material.name = materialDef.name 1869 | 1870 | // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. 1871 | if (material.map) material.map.encoding = THREE.sRGBEncoding 1872 | if (material.emissiveMap) material.emissiveMap.encoding = THREE.sRGBEncoding 1873 | 1874 | assignExtrasToUserData(material, materialDef) 1875 | 1876 | parser.associations.set(material, { type: 'materials', index: materialIndex }) 1877 | 1878 | if (materialDef.extensions) addUnknownExtensionsToUserData(extensions, material, materialDef) 1879 | 1880 | return material 1881 | }) 1882 | } 1883 | 1884 | /** When Object3D instances are targeted by animation, they need unique names. */ 1885 | GLTFParser.prototype.createUniqueName = function (originalName) { 1886 | var sanitizedName = THREE.PropertyBinding.sanitizeNodeName(originalName || '') 1887 | 1888 | var name = sanitizedName 1889 | 1890 | for (var i = 1; this.nodeNamesUsed[name]; ++i) { 1891 | name = sanitizedName + '_' + i 1892 | } 1893 | 1894 | this.nodeNamesUsed[name] = true 1895 | 1896 | return name 1897 | } 1898 | 1899 | /** 1900 | * @param {THREE.BufferGeometry} geometry 1901 | * @param {GLTF.Primitive} primitiveDef 1902 | * @param {GLTFParser} parser 1903 | */ 1904 | function computeBounds(geometry, primitiveDef, parser) { 1905 | var attributes = primitiveDef.attributes 1906 | 1907 | var box = new THREE.Box3() 1908 | 1909 | if (attributes.POSITION !== undefined) { 1910 | var accessor = parser.json.accessors[attributes.POSITION] 1911 | 1912 | var min = accessor.min 1913 | var max = accessor.max 1914 | 1915 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 1916 | 1917 | if (min !== undefined && max !== undefined) { 1918 | box.set(new THREE.Vector3(min[0], min[1], min[2]), new THREE.Vector3(max[0], max[1], max[2])) 1919 | } else { 1920 | console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.') 1921 | 1922 | return 1923 | } 1924 | } else { 1925 | return 1926 | } 1927 | 1928 | var targets = primitiveDef.targets 1929 | 1930 | if (targets !== undefined) { 1931 | var maxDisplacement = new THREE.Vector3() 1932 | var vector = new THREE.Vector3() 1933 | 1934 | for (var i = 0, il = targets.length; i < il; i++) { 1935 | var target = targets[i] 1936 | 1937 | if (target.POSITION !== undefined) { 1938 | var accessor = parser.json.accessors[target.POSITION] 1939 | var min = accessor.min 1940 | var max = accessor.max 1941 | 1942 | // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. 1943 | 1944 | if (min !== undefined && max !== undefined) { 1945 | // we need to get max of absolute components because target weight is [-1,1] 1946 | vector.setX(Math.max(Math.abs(min[0]), Math.abs(max[0]))) 1947 | vector.setY(Math.max(Math.abs(min[1]), Math.abs(max[1]))) 1948 | vector.setZ(Math.max(Math.abs(min[2]), Math.abs(max[2]))) 1949 | 1950 | // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative 1951 | // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets 1952 | // are used to implement key-frame animations and as such only two are active at a time - this results in very large 1953 | // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. 1954 | maxDisplacement.max(vector) 1955 | } else { 1956 | console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.') 1957 | } 1958 | } 1959 | } 1960 | 1961 | // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. 1962 | box.expandByVector(maxDisplacement) 1963 | } 1964 | 1965 | geometry.boundingBox = box 1966 | 1967 | var sphere = new THREE.Sphere() 1968 | 1969 | box.getCenter(sphere.center) 1970 | sphere.radius = box.min.distanceTo(box.max) / 2 1971 | 1972 | geometry.boundingSphere = sphere 1973 | } 1974 | 1975 | /** 1976 | * @param {THREE.BufferGeometry} geometry 1977 | * @param {GLTF.Primitive} primitiveDef 1978 | * @param {GLTFParser} parser 1979 | * @return {Promise} 1980 | */ 1981 | function addPrimitiveAttributes(geometry, primitiveDef, parser) { 1982 | var attributes = primitiveDef.attributes 1983 | 1984 | var pending = [] 1985 | 1986 | function assignAttributeAccessor(accessorIndex, attributeName) { 1987 | return parser.getDependency('accessor', accessorIndex).then(function (accessor) { 1988 | geometry.setAttribute(attributeName, accessor) 1989 | }) 1990 | } 1991 | 1992 | for (var gltfAttributeName in attributes) { 1993 | var threeAttributeName = ATTRIBUTES[gltfAttributeName] || gltfAttributeName.toLowerCase() 1994 | 1995 | // Skip attributes already provided by e.g. Draco extension. 1996 | if (threeAttributeName in geometry.attributes) continue 1997 | 1998 | pending.push(assignAttributeAccessor(attributes[gltfAttributeName], threeAttributeName)) 1999 | } 2000 | 2001 | if (primitiveDef.indices !== undefined && !geometry.index) { 2002 | var accessor = parser.getDependency('accessor', primitiveDef.indices).then(function (accessor) { 2003 | geometry.setIndex(accessor) 2004 | }) 2005 | 2006 | pending.push(accessor) 2007 | } 2008 | 2009 | assignExtrasToUserData(geometry, primitiveDef) 2010 | 2011 | computeBounds(geometry, primitiveDef, parser) 2012 | 2013 | return Promise.all(pending).then(function () { 2014 | return primitiveDef.targets !== undefined ? addMorphTargets(geometry, primitiveDef.targets, parser) : geometry 2015 | }) 2016 | } 2017 | 2018 | /** 2019 | * @param {THREE.BufferGeometry} geometry 2020 | * @param {Number} drawMode 2021 | * @return {THREE.BufferGeometry} 2022 | */ 2023 | function toTrianglesDrawMode(geometry, drawMode) { 2024 | var index = geometry.getIndex() 2025 | 2026 | // generate index if not present 2027 | 2028 | if (index === null) { 2029 | var indices = [] 2030 | 2031 | var position = geometry.getAttribute('position') 2032 | 2033 | if (position !== undefined) { 2034 | for (var i = 0; i < position.count; i++) { 2035 | indices.push(i) 2036 | } 2037 | 2038 | geometry.setIndex(indices) 2039 | index = geometry.getIndex() 2040 | } else { 2041 | console.error('THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.') 2042 | return geometry 2043 | } 2044 | } 2045 | 2046 | // 2047 | 2048 | var numberOfTriangles = index.count - 2 2049 | var newIndices = [] 2050 | 2051 | if (drawMode === THREE.TriangleFanDrawMode) { 2052 | // gl.TRIANGLE_FAN 2053 | 2054 | for (var i = 1; i <= numberOfTriangles; i++) { 2055 | newIndices.push(index.getX(0)) 2056 | newIndices.push(index.getX(i)) 2057 | newIndices.push(index.getX(i + 1)) 2058 | } 2059 | } else { 2060 | // gl.TRIANGLE_STRIP 2061 | 2062 | for (var i = 0; i < numberOfTriangles; i++) { 2063 | if (i % 2 === 0) { 2064 | newIndices.push(index.getX(i)) 2065 | newIndices.push(index.getX(i + 1)) 2066 | newIndices.push(index.getX(i + 2)) 2067 | } else { 2068 | newIndices.push(index.getX(i + 2)) 2069 | newIndices.push(index.getX(i + 1)) 2070 | newIndices.push(index.getX(i)) 2071 | } 2072 | } 2073 | } 2074 | 2075 | if (newIndices.length / 3 !== numberOfTriangles) { 2076 | console.error('THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.') 2077 | } 2078 | 2079 | // build final geometry 2080 | 2081 | var newGeometry = geometry.clone() 2082 | newGeometry.setIndex(newIndices) 2083 | 2084 | return newGeometry 2085 | } 2086 | 2087 | /** 2088 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry 2089 | * 2090 | * Creates BufferGeometries from primitives. 2091 | * 2092 | * @param {Array} primitives 2093 | * @return {Promise>} 2094 | */ 2095 | GLTFParser.prototype.loadGeometries = function (primitives) { 2096 | var parser = this 2097 | var extensions = this.extensions 2098 | var cache = this.primitiveCache 2099 | 2100 | function createDracoPrimitive(primitive) { 2101 | return extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION] 2102 | .decodePrimitive(primitive, parser) 2103 | .then(function (geometry) { 2104 | return addPrimitiveAttributes(geometry, primitive, parser) 2105 | }) 2106 | } 2107 | 2108 | var pending = [] 2109 | 2110 | for (var i = 0, il = primitives.length; i < il; i++) { 2111 | var primitive = primitives[i] 2112 | var cacheKey = createPrimitiveKey(primitive) 2113 | 2114 | // See if we've already created this geometry 2115 | var cached = cache[cacheKey] 2116 | 2117 | if (cached) { 2118 | // Use the cached geometry if it exists 2119 | pending.push(cached.promise) 2120 | } else { 2121 | var geometryPromise 2122 | 2123 | if (primitive.extensions && primitive.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]) { 2124 | // Use DRACO geometry if available 2125 | geometryPromise = createDracoPrimitive(primitive) 2126 | } else { 2127 | // Otherwise create a new geometry 2128 | geometryPromise = addPrimitiveAttributes(new THREE.BufferGeometry(), primitive, parser) 2129 | } 2130 | 2131 | // Cache this geometry 2132 | cache[cacheKey] = { primitive: primitive, promise: geometryPromise } 2133 | 2134 | pending.push(geometryPromise) 2135 | } 2136 | } 2137 | 2138 | return Promise.all(pending) 2139 | } 2140 | 2141 | /** 2142 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes 2143 | * @param {number} meshIndex 2144 | * @return {Promise} 2145 | */ 2146 | GLTFParser.prototype.loadMesh = function (meshIndex) { 2147 | var parser = this 2148 | var json = this.json 2149 | var extensions = this.extensions 2150 | 2151 | var meshDef = json.meshes[meshIndex] 2152 | var primitives = meshDef.primitives 2153 | 2154 | var pending = [] 2155 | 2156 | for (var i = 0, il = primitives.length; i < il; i++) { 2157 | var material = 2158 | primitives[i].material === undefined 2159 | ? createDefaultMaterial(this.cache) 2160 | : this.getDependency('material', primitives[i].material) 2161 | 2162 | pending.push(material) 2163 | } 2164 | 2165 | pending.push(parser.loadGeometries(primitives)) 2166 | 2167 | return Promise.all(pending).then(function (results) { 2168 | var materials = results.slice(0, results.length - 1) 2169 | var geometries = results[results.length - 1] 2170 | 2171 | var meshes = [] 2172 | 2173 | for (var i = 0, il = geometries.length; i < il; i++) { 2174 | var geometry = geometries[i] 2175 | var primitive = primitives[i] 2176 | 2177 | // 1. create Mesh 2178 | var mesh 2179 | var material = materials[i] 2180 | 2181 | if ( 2182 | primitive.mode === WEBGL_CONSTANTS.TRIANGLES || 2183 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || 2184 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || 2185 | primitive.mode === undefined 2186 | ) { 2187 | // .isSkinnedMesh isn't in glTF spec. See ._markDefs() 2188 | if (geometry.morphAttributes && Object.keys(geometry.morphAttributes).length > 0) { 2189 | meshDef.hasMorphAttributes = true 2190 | } 2191 | geometry.morphAttributes = {} 2192 | mesh = 2193 | meshDef.isSkinnedMesh === true 2194 | ? new THREE.SkinnedMesh(geometry, material) 2195 | : new THREE.Mesh(geometry, material) 2196 | 2197 | if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP) { 2198 | mesh.geometry = toTrianglesDrawMode(mesh.geometry, THREE.TriangleStripDrawMode) 2199 | } else if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN) { 2200 | mesh.geometry = toTrianglesDrawMode(mesh.geometry, THREE.TriangleFanDrawMode) 2201 | } 2202 | } else if (primitive.mode === WEBGL_CONSTANTS.LINES) { 2203 | mesh = new THREE.LineSegments(geometry, material) 2204 | } else if (primitive.mode === WEBGL_CONSTANTS.LINE_STRIP) { 2205 | mesh = new THREE.Line(geometry, material) 2206 | } else if (primitive.mode === WEBGL_CONSTANTS.LINE_LOOP) { 2207 | mesh = new THREE.LineLoop(geometry, material) 2208 | } else if (primitive.mode === WEBGL_CONSTANTS.POINTS) { 2209 | mesh = new THREE.Points(geometry, material) 2210 | } else { 2211 | throw new Error('THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode) 2212 | } 2213 | 2214 | if (meshDef.hasMorphAttributes) { 2215 | // Just flag the mesh, so that parser.js can link morphTarget dictionaries and influences 2216 | // This prevented a crash relating to morphTarget definitions 2217 | mesh.morphTargetDictionary = true 2218 | mesh.morphTargetInfluences = true 2219 | } 2220 | 2221 | mesh.name = parser.createUniqueName(meshDef.name || 'mesh_' + meshIndex) 2222 | 2223 | assignExtrasToUserData(mesh, meshDef) 2224 | if (primitive.extensions) addUnknownExtensionsToUserData(extensions, mesh, primitive) 2225 | 2226 | parser.assignFinalMaterial(mesh) 2227 | 2228 | meshes.push(mesh) 2229 | } 2230 | 2231 | if (meshes.length === 1) { 2232 | return meshes[0] 2233 | } 2234 | 2235 | var group = new THREE.Group() 2236 | 2237 | for (var i = 0, il = meshes.length; i < il; i++) { 2238 | group.add(meshes[i]) 2239 | } 2240 | 2241 | return group 2242 | }) 2243 | } 2244 | 2245 | /** 2246 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras 2247 | * @param {number} cameraIndex 2248 | * @return {Promise} 2249 | */ 2250 | GLTFParser.prototype.loadCamera = function (cameraIndex) { 2251 | var camera 2252 | var cameraDef = this.json.cameras[cameraIndex] 2253 | var params = cameraDef[cameraDef.type] 2254 | 2255 | if (!params) { 2256 | console.warn('THREE.GLTFLoader: Missing camera parameters.') 2257 | return 2258 | } 2259 | 2260 | if (cameraDef.type === 'perspective') { 2261 | camera = new THREE.PerspectiveCamera( 2262 | THREE.MathUtils.radToDeg(params.yfov), 2263 | params.aspectRatio || 1, 2264 | params.znear || 1, 2265 | params.zfar || 2e6 2266 | ) 2267 | } else if (cameraDef.type === 'orthographic') { 2268 | camera = new THREE.OrthographicCamera( 2269 | -params.xmag, 2270 | params.xmag, 2271 | params.ymag, 2272 | -params.ymag, 2273 | params.znear, 2274 | params.zfar 2275 | ) 2276 | } 2277 | 2278 | if (cameraDef.name) camera.name = this.createUniqueName(cameraDef.name) 2279 | 2280 | assignExtrasToUserData(camera, cameraDef) 2281 | 2282 | return Promise.resolve(camera) 2283 | } 2284 | 2285 | /** 2286 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins 2287 | * @param {number} skinIndex 2288 | * @return {Promise} 2289 | */ 2290 | GLTFParser.prototype.loadSkin = function (skinIndex) { 2291 | var skinDef = this.json.skins[skinIndex] 2292 | 2293 | var skinEntry = { joints: skinDef.joints } 2294 | 2295 | if (skinDef.inverseBindMatrices === undefined) { 2296 | return Promise.resolve(skinEntry) 2297 | } 2298 | 2299 | return this.getDependency('accessor', skinDef.inverseBindMatrices).then(function (accessor) { 2300 | skinEntry.inverseBindMatrices = accessor 2301 | 2302 | return skinEntry 2303 | }) 2304 | } 2305 | 2306 | /** 2307 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations 2308 | * @param {number} animationIndex 2309 | * @return {Promise} 2310 | */ 2311 | GLTFParser.prototype.loadAnimation = function (animationIndex) { 2312 | var json = this.json 2313 | 2314 | var animationDef = json.animations[animationIndex] 2315 | 2316 | var pendingNodes = [] 2317 | var pendingInputAccessors = [] 2318 | var pendingOutputAccessors = [] 2319 | var pendingSamplers = [] 2320 | var pendingTargets = [] 2321 | 2322 | for (var i = 0, il = animationDef.channels.length; i < il; i++) { 2323 | var channel = animationDef.channels[i] 2324 | var sampler = animationDef.samplers[channel.sampler] 2325 | var target = channel.target 2326 | var name = target.node !== undefined ? target.node : target.id // NOTE: target.id is deprecated. 2327 | var input = animationDef.parameters !== undefined ? animationDef.parameters[sampler.input] : sampler.input 2328 | var output = animationDef.parameters !== undefined ? animationDef.parameters[sampler.output] : sampler.output 2329 | 2330 | pendingNodes.push(this.getDependency('node', name)) 2331 | pendingInputAccessors.push(this.getDependency('accessor', input)) 2332 | pendingOutputAccessors.push(this.getDependency('accessor', output)) 2333 | pendingSamplers.push(sampler) 2334 | pendingTargets.push(target) 2335 | } 2336 | 2337 | return Promise.all([ 2338 | Promise.all(pendingNodes), 2339 | Promise.all(pendingInputAccessors), 2340 | Promise.all(pendingOutputAccessors), 2341 | Promise.all(pendingSamplers), 2342 | Promise.all(pendingTargets), 2343 | ]).then(function (dependencies) { 2344 | var nodes = dependencies[0] 2345 | var inputAccessors = dependencies[1] 2346 | var outputAccessors = dependencies[2] 2347 | var samplers = dependencies[3] 2348 | var targets = dependencies[4] 2349 | 2350 | var tracks = [] 2351 | 2352 | for (var i = 0, il = nodes.length; i < il; i++) { 2353 | var node = nodes[i] 2354 | var inputAccessor = inputAccessors[i] 2355 | var outputAccessor = outputAccessors[i] 2356 | var sampler = samplers[i] 2357 | var target = targets[i] 2358 | 2359 | if (node === undefined) continue 2360 | 2361 | node.updateMatrix() 2362 | node.matrixAutoUpdate = true 2363 | 2364 | var TypedKeyframeTrack 2365 | 2366 | switch (PATH_PROPERTIES[target.path]) { 2367 | case PATH_PROPERTIES.weights: 2368 | TypedKeyframeTrack = THREE.NumberKeyframeTrack 2369 | break 2370 | 2371 | case PATH_PROPERTIES.rotation: 2372 | TypedKeyframeTrack = THREE.QuaternionKeyframeTrack 2373 | break 2374 | 2375 | case PATH_PROPERTIES.position: 2376 | case PATH_PROPERTIES.scale: 2377 | default: 2378 | TypedKeyframeTrack = THREE.VectorKeyframeTrack 2379 | break 2380 | } 2381 | 2382 | var targetName = node.name ? node.name : node.uuid 2383 | 2384 | var interpolation = 2385 | sampler.interpolation !== undefined ? INTERPOLATION[sampler.interpolation] : THREE.InterpolateLinear 2386 | 2387 | var targetNames = [] 2388 | 2389 | if (PATH_PROPERTIES[target.path] === PATH_PROPERTIES.weights) { 2390 | // Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh. 2391 | node.traverse(function (object) { 2392 | if (object.isMesh === true && object.morphTargetInfluences) { 2393 | targetNames.push(object.name ? object.name : object.uuid) 2394 | } 2395 | }) 2396 | } else { 2397 | targetNames.push(targetName) 2398 | } 2399 | 2400 | if (outputAccessor === null) continue 2401 | var outputArray = outputAccessor.array 2402 | 2403 | if (outputAccessor.normalized) { 2404 | var scale 2405 | 2406 | if (outputArray.constructor === Int8Array) { 2407 | scale = 1 / 127 2408 | } else if (outputArray.constructor === Uint8Array) { 2409 | scale = 1 / 255 2410 | } else if (outputArray.constructor == Int16Array) { 2411 | scale = 1 / 32767 2412 | } else if (outputArray.constructor === Uint16Array) { 2413 | scale = 1 / 65535 2414 | } else { 2415 | throw new Error('THREE.GLTFLoader: Unsupported output accessor component type.') 2416 | } 2417 | 2418 | var scaled = new Float32Array(outputArray.length) 2419 | 2420 | for (var j = 0, jl = outputArray.length; j < jl; j++) { 2421 | scaled[j] = outputArray[j] * scale 2422 | } 2423 | 2424 | outputArray = scaled 2425 | } 2426 | } 2427 | 2428 | var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex 2429 | var clip = new THREE.AnimationClip(name, undefined, tracks) 2430 | clip.targetNames = targetNames 2431 | return clip 2432 | }) 2433 | } 2434 | 2435 | GLTFParser.prototype.createNodeMesh = function (nodeIndex) { 2436 | const json = this.json 2437 | const parser = this 2438 | const nodeDef = json.nodes[nodeIndex] 2439 | 2440 | if (nodeDef.mesh === undefined) return null 2441 | 2442 | return parser.getDependency('mesh', nodeDef.mesh).then(function (mesh) { 2443 | const node = parser._getNodeRef(parser.meshCache, nodeDef.mesh, mesh) 2444 | 2445 | // if weights are provided on the node, override weights on the mesh. 2446 | if (nodeDef.weights !== undefined) { 2447 | node.traverse(function (o) { 2448 | if (!o.isMesh) return 2449 | 2450 | for (let i = 0, il = nodeDef.weights.length; i < il; i++) { 2451 | o.morphTargetInfluences[i] = nodeDef.weights[i] 2452 | } 2453 | }) 2454 | } 2455 | 2456 | return node 2457 | }) 2458 | } 2459 | 2460 | /** 2461 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy 2462 | * @param {number} nodeIndex 2463 | * @return {Promise} 2464 | */ 2465 | GLTFParser.prototype.loadNode = function (nodeIndex) { 2466 | var json = this.json 2467 | var extensions = this.extensions 2468 | var parser = this 2469 | 2470 | var nodeDef = json.nodes[nodeIndex] 2471 | 2472 | // reserve node's name before its dependencies, so the root has the intended name. 2473 | var nodeName = nodeDef.name ? parser.createUniqueName(nodeDef.name) : '' 2474 | 2475 | return (function () { 2476 | var pending = [] 2477 | 2478 | const meshPromise = parser._invokeOne(function (ext) { 2479 | return ext.createNodeMesh && ext.createNodeMesh(nodeIndex) 2480 | }) 2481 | 2482 | if (meshPromise) { 2483 | pending.push(meshPromise) 2484 | } 2485 | 2486 | if (nodeDef.camera !== undefined) { 2487 | pending.push( 2488 | parser.getDependency('camera', nodeDef.camera).then(function (camera) { 2489 | return parser._getNodeRef(parser.cameraCache, nodeDef.camera, camera) 2490 | }) 2491 | ) 2492 | } 2493 | 2494 | parser 2495 | ._invokeAll(function (ext) { 2496 | return ext.createNodeAttachment && ext.createNodeAttachment(nodeIndex) 2497 | }) 2498 | .forEach(function (promise) { 2499 | pending.push(promise) 2500 | }) 2501 | 2502 | return Promise.all(pending) 2503 | })().then(function (objects) { 2504 | var node 2505 | 2506 | // .isBone isn't in glTF spec. See ._markDefs 2507 | if (nodeDef.isBone === true) { 2508 | node = new THREE.Bone() 2509 | } else if (objects.length > 1) { 2510 | node = new THREE.Group() 2511 | } else if (objects.length === 1) { 2512 | node = objects[0] 2513 | } else { 2514 | node = new THREE.Object3D() 2515 | } 2516 | 2517 | if (node !== objects[0]) { 2518 | for (var i = 0, il = objects.length; i < il; i++) { 2519 | node.add(objects[i]) 2520 | } 2521 | } 2522 | 2523 | if (nodeDef.name) { 2524 | node.userData.name = nodeDef.name 2525 | node.name = nodeName 2526 | } 2527 | 2528 | assignExtrasToUserData(node, nodeDef) 2529 | 2530 | if (nodeDef.extensions) addUnknownExtensionsToUserData(extensions, node, nodeDef) 2531 | 2532 | if (nodeDef.matrix !== undefined) { 2533 | var matrix = new THREE.Matrix4() 2534 | matrix.fromArray(nodeDef.matrix) 2535 | node.applyMatrix4(matrix) 2536 | } else { 2537 | if (nodeDef.translation !== undefined) { 2538 | node.position.fromArray(nodeDef.translation) 2539 | } 2540 | 2541 | if (nodeDef.rotation !== undefined) { 2542 | node.quaternion.fromArray(nodeDef.rotation) 2543 | } 2544 | 2545 | if (nodeDef.scale !== undefined) { 2546 | node.scale.fromArray(nodeDef.scale) 2547 | } 2548 | } 2549 | 2550 | parser.associations.set(node, { type: 'nodes', index: nodeIndex }) 2551 | 2552 | return node 2553 | }) 2554 | } 2555 | 2556 | /** 2557 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes 2558 | * @param {number} sceneIndex 2559 | * @return {Promise} 2560 | */ 2561 | GLTFParser.prototype.loadScene = (function () { 2562 | // scene node hierachy builder 2563 | 2564 | function buildNodeHierachy(nodeId, parentObject, json, parser) { 2565 | var nodeDef = json.nodes[nodeId] 2566 | 2567 | return parser 2568 | .getDependency('node', nodeId) 2569 | .then(function (node) { 2570 | if (nodeDef.skin === undefined) return node 2571 | 2572 | // build skeleton here as well 2573 | 2574 | var skinEntry 2575 | 2576 | return parser 2577 | .getDependency('skin', nodeDef.skin) 2578 | .then(function (skin) { 2579 | skinEntry = skin 2580 | 2581 | var pendingJoints = [] 2582 | 2583 | for (var i = 0, il = skinEntry.joints.length; i < il; i++) { 2584 | pendingJoints.push(parser.getDependency('node', skinEntry.joints[i])) 2585 | } 2586 | 2587 | return Promise.all(pendingJoints) 2588 | }) 2589 | .then(function (jointNodes) { 2590 | node.traverse(function (mesh) { 2591 | if (!mesh.isMesh) return 2592 | 2593 | var bones = [] 2594 | var boneInverses = [] 2595 | 2596 | for (var j = 0, jl = jointNodes.length; j < jl; j++) { 2597 | var jointNode = jointNodes[j] 2598 | 2599 | if (jointNode) { 2600 | bones.push(jointNode) 2601 | 2602 | var mat = new THREE.Matrix4() 2603 | 2604 | if (skinEntry.inverseBindMatrices !== undefined) { 2605 | mat.fromArray(skinEntry.inverseBindMatrices.array, j * 16) 2606 | } 2607 | 2608 | boneInverses.push(mat) 2609 | } else { 2610 | console.warn('THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[j]) 2611 | } 2612 | } 2613 | 2614 | mesh.bind(new THREE.Skeleton(bones, boneInverses), mesh.matrixWorld) 2615 | }) 2616 | 2617 | return node 2618 | }) 2619 | }) 2620 | .then(function (node) { 2621 | // build node hierachy 2622 | 2623 | parentObject.add(node) 2624 | 2625 | var pending = [] 2626 | 2627 | if (nodeDef.children) { 2628 | var children = nodeDef.children 2629 | 2630 | for (var i = 0, il = children.length; i < il; i++) { 2631 | var child = children[i] 2632 | pending.push(buildNodeHierachy(child, node, json, parser)) 2633 | } 2634 | } 2635 | 2636 | return Promise.all(pending) 2637 | }) 2638 | } 2639 | 2640 | return function loadScene(sceneIndex) { 2641 | var json = this.json 2642 | var extensions = this.extensions 2643 | var sceneDef = this.json.scenes[sceneIndex] 2644 | var parser = this 2645 | 2646 | // Loader returns Group, not Scene. 2647 | // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 2648 | var scene = new THREE.Group() 2649 | if (sceneDef.name) scene.name = parser.createUniqueName(sceneDef.name) 2650 | 2651 | assignExtrasToUserData(scene, sceneDef) 2652 | 2653 | if (sceneDef.extensions) addUnknownExtensionsToUserData(extensions, scene, sceneDef) 2654 | 2655 | var nodeIds = sceneDef.nodes || [] 2656 | 2657 | var pending = [] 2658 | 2659 | for (var i = 0, il = nodeIds.length; i < il; i++) { 2660 | pending.push(buildNodeHierachy(nodeIds[i], scene, json, parser)) 2661 | } 2662 | 2663 | return Promise.all(pending).then(function () { 2664 | return scene 2665 | }) 2666 | } 2667 | })() 2668 | -------------------------------------------------------------------------------- /src/gltfjsx.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import transform from './utils/transform.js' 5 | 6 | import { GLTFLoader } from './bin/GLTFLoader.js' 7 | import { DRACOLoader } from './bin/DRACOLoader.js' 8 | DRACOLoader.getDecoderModule = () => {} 9 | import parse from './utils/parser.js' 10 | 11 | const gltfLoader = new GLTFLoader() 12 | gltfLoader.setDRACOLoader(new DRACOLoader()) 13 | 14 | function toArrayBuffer(buf) { 15 | var ab = new ArrayBuffer(buf.length) 16 | var view = new Uint8Array(ab) 17 | for (var i = 0; i < buf.length; ++i) view[i] = buf[i] 18 | return ab 19 | } 20 | 21 | function roundOff(value) { 22 | return Math.round(value * 100) / 100 23 | } 24 | 25 | function getFileSize(file) { 26 | const stats = fs.statSync(file) 27 | let fileSize = stats.size 28 | let fileSizeKB = roundOff(fileSize * 0.001) 29 | let fileSizeMB = roundOff(fileSizeKB * 0.001) 30 | return { 31 | size: fileSizeKB > 1000 ? `${fileSizeMB}MB` : `${fileSizeKB}KB`, 32 | sizeKB: fileSizeKB, 33 | } 34 | } 35 | 36 | export default function (file, output, options) { 37 | function getRelativeFilePath(file) { 38 | const filePath = path.resolve(file) 39 | const rootPath = options.root ? path.resolve(options.root) : path.dirname(file) 40 | const relativePath = path.relative(rootPath, filePath) || '' 41 | if (process.platform === 'win32') return relativePath.replace(/\\/g, '/') 42 | return relativePath 43 | } 44 | 45 | return new Promise((resolve, reject) => { 46 | async function run(stream) { 47 | let size = '' 48 | // Process GLTF 49 | if (output && path.parse(output).ext === '.tsx') options.types = true 50 | if (options.transform || options.instance || options.instanceall) { 51 | const { name } = path.parse(file) 52 | const outputDir = path.parse(path.resolve(output ?? file)).dir 53 | const transformOut = path.join(outputDir, name + '-transformed.glb') 54 | await transform(file, transformOut, options) 55 | const { size: sizeOriginal, sizeKB: sizeKBOriginal } = getFileSize(file) 56 | const { size: sizeTransformed, sizeKB: sizeKBTransformed } = getFileSize(transformOut) 57 | size = `${file} [${sizeOriginal}] > ${transformOut} [${sizeTransformed}] (${Math.round( 58 | 100 - (sizeKBTransformed / sizeKBOriginal) * 100 59 | )}%)` 60 | file = transformOut 61 | } 62 | const filePath = getRelativeFilePath(file) 63 | const data = fs.readFileSync(file) 64 | const arrayBuffer = toArrayBuffer(data) 65 | gltfLoader.parse( 66 | arrayBuffer, 67 | '', 68 | async (gltf) => { 69 | const output = await parse(gltf, { fileName: filePath, size, ...options }) 70 | if (options.console) console.log(output) 71 | else stream?.write(output) 72 | stream?.end() 73 | resolve() 74 | }, 75 | (reason) => console.log(reason) 76 | ) 77 | } 78 | 79 | if (options.console) { 80 | run() 81 | } else { 82 | const stream = fs.createWriteStream(path.resolve(output)) 83 | stream.once('open', async () => { 84 | if (!fs.existsSync(file)) reject(file + ' does not exist.') 85 | else run(stream) 86 | }) 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | const cli = await import('../cli.js') 3 | 4 | assert(cli) 5 | -------------------------------------------------------------------------------- /src/utils/exports.js: -------------------------------------------------------------------------------- 1 | import parse from './parser.js' 2 | import { GLTFLoader as GLTFStructureLoader } from '../bin/GLTFLoader.js' 3 | 4 | export { parse, GLTFStructureLoader } 5 | -------------------------------------------------------------------------------- /src/utils/isVarName.js: -------------------------------------------------------------------------------- 1 | const isVarName = (str) => { 2 | // eslint-disable-next-line no-misleading-character-class 3 | const regex = new RegExp( 4 | // eslint-disable-next-line no-misleading-character-class 5 | /^(?!(?:do|if|in|for|let|new|try|var|case|else|enum|eval|null|this|true|void|with|await|break|catch|class|const|false|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$)(?:[$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7B9\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD23\uDF00-\uDF1C\uDF27\uDF30-\uDF45]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD44\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF1A]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDEE0-\uDEF2]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFF1]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D])(?:[\\$0-9A-Z_a-z\xAA\xB5\xB7\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05EF-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u07FD\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D3-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u09FE\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1878\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7B9\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDD00-\uDD27\uDD30-\uDD39\uDF00-\uDF1C\uDF27\uDF30-\uDF50]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD44-\uDD46\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDC9-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3B-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC5E\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF1A\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDC00-\uDC3A\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD8E\uDD90\uDD91\uDD93-\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF6]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE7F\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFF1]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF])*$/ 6 | ) 7 | return regex.test(str) 8 | } 9 | 10 | export default isVarName 11 | -------------------------------------------------------------------------------- /src/utils/parser.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import * as prettier from 'prettier' 3 | import babelParser from 'prettier/parser-babel.js' 4 | import isVarName from './isVarName.js' 5 | 6 | function parse(gltf, { fileName = 'model', ...options } = {}) { 7 | if (gltf.isObject3D) { 8 | // Wrap scene in a GLTF Structure 9 | gltf = { scene: gltf, animations: [], parser: { json: {} } } 10 | } 11 | 12 | const url = (fileName.toLowerCase().startsWith('http') ? '' : '/') + fileName 13 | const animations = gltf.animations 14 | const hasAnimations = animations.length > 0 15 | 16 | // Collect all objects 17 | const objects = [] 18 | gltf.scene.traverse((child) => objects.push(child)) 19 | 20 | // Browse for duplicates 21 | const duplicates = { 22 | names: {}, 23 | materials: {}, 24 | geometries: {}, 25 | } 26 | 27 | function uniqueName(attempt, index = 0) { 28 | const newAttempt = index > 0 ? attempt + index : attempt 29 | if (Object.values(duplicates.geometries).find(({ name }) => name === newAttempt) === undefined) return newAttempt 30 | else return uniqueName(attempt, index + 1) 31 | } 32 | 33 | gltf.scene.traverse((child) => { 34 | if (child.isMesh) { 35 | if (child.material) { 36 | if (!duplicates.materials[child.material.name]) { 37 | duplicates.materials[child.material.name] = 1 38 | } else { 39 | duplicates.materials[child.material.name]++ 40 | } 41 | } 42 | } 43 | }) 44 | 45 | gltf.scene.traverse((child) => { 46 | if (child.isMesh) { 47 | if (child.geometry) { 48 | const key = child.geometry.uuid + child.material?.name ?? '' 49 | if (!duplicates.geometries[key]) { 50 | let name = (child.name || 'Part').replace(/[^a-zA-Z]/g, '') 51 | name = name.charAt(0).toUpperCase() + name.slice(1) 52 | duplicates.geometries[key] = { 53 | count: 1, 54 | name: uniqueName(name), 55 | node: 'nodes' + sanitizeName(child.name), 56 | } 57 | } else { 58 | duplicates.geometries[key].count++ 59 | } 60 | } 61 | } 62 | }) 63 | 64 | // Prune duplicate geometries 65 | if (!options.instanceall) { 66 | for (let key of Object.keys(duplicates.geometries)) { 67 | const duplicate = duplicates.geometries[key] 68 | if (duplicate.count === 1) delete duplicates.geometries[key] 69 | } 70 | } 71 | 72 | const hasInstances = (options.instance || options.instanceall) && Object.keys(duplicates.geometries).length > 0 73 | 74 | function sanitizeName(name) { 75 | return isVarName(name) ? `.${name}` : `['${name}']` 76 | } 77 | 78 | const rNbr = (number) => { 79 | return parseFloat(number.toFixed(Math.round(options.precision || 2))) 80 | } 81 | 82 | const rDeg = (number) => { 83 | const abs = Math.abs(Math.round(parseFloat(number) * 100000)) 84 | for (let i = 1; i <= 10; i++) { 85 | if (abs === Math.round(parseFloat(Math.PI / i) * 100000)) 86 | return `${number < 0 ? '-' : ''}Math.PI${i > 1 ? ' / ' + i : ''}` 87 | } 88 | for (let i = 1; i <= 10; i++) { 89 | if (abs === Math.round(parseFloat(Math.PI * i) * 100000)) 90 | return `${number < 0 ? '-' : ''}Math.PI${i > 1 ? ' * ' + i : ''}` 91 | } 92 | return rNbr(number) 93 | } 94 | 95 | function printTypes(objects, animations) { 96 | let meshes = objects.filter((o) => o.isMesh && o.__removed === undefined) 97 | let bones = objects.filter((o) => o.isBone && !(o.parent && o.parent.isBone) && o.__removed === undefined) 98 | let materials = [...new Set(objects.filter((o) => o.material && o.material.name).map((o) => o.material))] 99 | 100 | let animationTypes = '' 101 | if (animations.length) { 102 | animationTypes = `\n 103 | type ActionName = ${animations.map((clip, i) => `"${clip.name}"`).join(' | ')}; 104 | 105 | interface GLTFAction extends THREE.AnimationClip { name: ActionName }\n` 106 | } 107 | 108 | const types = [...new Set([...meshes, ...bones].map((o) => getType(o)))] 109 | const contextType = hasInstances 110 | ? `\ntype ContextType = Record `JSX.IntrinsicElements['${type}']`).join(' | ')} 112 | >>\n` 113 | : '' 114 | 115 | return `\n${animationTypes}\ntype GLTFResult = GLTF & { 116 | nodes: { 117 | ${meshes.map(({ name, type }) => (isVarName(name) ? name : `['${name}']`) + ': THREE.' + type).join(',')} 118 | ${bones.map(({ name, type }) => (isVarName(name) ? name : `['${name}']`) + ': THREE.' + type).join(',')} 119 | } 120 | materials: { 121 | ${materials.map(({ name, type }) => (isVarName(name) ? name : `['${name}']`) + ': THREE.' + type).join(',')} 122 | } 123 | animations: GLTFAction[] 124 | }\n${contextType}` 125 | } 126 | 127 | function getType(obj) { 128 | let type = obj.type.charAt(0).toLowerCase() + obj.type.slice(1) 129 | // Turn object3d's into groups, it should be faster according to the threejs docs 130 | if (type === 'object3D') type = 'group' 131 | if (type === 'perspectiveCamera') type = 'PerspectiveCamera' 132 | if (type === 'orthographicCamera') type = 'OrthographicCamera' 133 | return type 134 | } 135 | 136 | function handleProps(obj) { 137 | let { type, node, instanced } = getInfo(obj) 138 | 139 | let result = '' 140 | let isCamera = type === 'PerspectiveCamera' || type === 'OrthographicCamera' 141 | // Handle cameras 142 | if (isCamera) { 143 | result += `makeDefault={false} ` 144 | if (obj.zoom !== 1) result += `zoom={${rNbr(obj.zoom)}} ` 145 | if (obj.far !== 2000) result += `far={${rNbr(obj.far)}} ` 146 | if (obj.near !== 0.1) result += `near={${rNbr(obj.near)}} ` 147 | } 148 | if (type === 'PerspectiveCamera') { 149 | if (obj.fov !== 50) result += `fov={${rNbr(obj.fov)}} ` 150 | } 151 | 152 | if (!instanced) { 153 | // Shadows 154 | if (type === 'mesh' && options.shadows) result += `castShadow receiveShadow ` 155 | 156 | // Write out geometry first 157 | if (obj.geometry && !obj.isInstancedMesh) { 158 | result += `geometry={${node}.geometry} ` 159 | } 160 | 161 | // Write out materials 162 | if (obj.material && !obj.isInstancedMesh) { 163 | if (obj.material.name) result += `material={materials${sanitizeName(obj.material.name)}} ` 164 | else result += `material={${node}.material} ` 165 | } 166 | 167 | if (obj.instanceMatrix) result += `instanceMatrix={${node}.instanceMatrix} ` 168 | if (obj.instanceColor) result += `instanceColor={${node}.instanceColor} ` 169 | if (obj.skeleton) result += `skeleton={${node}.skeleton} ` 170 | if (obj.visible === false) result += `visible={false} ` 171 | if (obj.castShadow === true) result += `castShadow ` 172 | if (obj.receiveShadow === true) result += `receiveShadow ` 173 | if (obj.morphTargetDictionary) result += `morphTargetDictionary={${node}.morphTargetDictionary} ` 174 | if (obj.morphTargetInfluences) result += `morphTargetInfluences={${node}.morphTargetInfluences} ` 175 | if (obj.intensity && rNbr(obj.intensity)) result += `intensity={${rNbr(obj.intensity)}} ` 176 | //if (obj.power && obj.power !== 4 * Math.PI) result += `power={${rNbr(obj.power)}} ` 177 | if (obj.angle && obj.angle !== Math.PI / 3) result += `angle={${rDeg(obj.angle)}} ` 178 | if (obj.penumbra && rNbr(obj.penumbra) !== 0) result += `penumbra={${rNbr(obj.penumbra)}} ` 179 | if (obj.decay && rNbr(obj.decay) !== 1) result += `decay={${rNbr(obj.decay)}} ` 180 | if (obj.distance && rNbr(obj.distance) !== 0) result += `distance={${rNbr(obj.distance)}} ` 181 | if (obj.up && obj.up.isVector3 && !obj.up.equals(new THREE.Vector3(0, 1, 0))) 182 | result += `up={[${rNbr(obj.up.x)}, ${rNbr(obj.up.y)}, ${rNbr(obj.up.z)},]} ` 183 | } 184 | 185 | if (obj.color && obj.color.getHexString() !== 'ffffff') result += `color="#${obj.color.getHexString()}" ` 186 | if (obj.position && obj.position.isVector3 && rNbr(obj.position.length())) 187 | result += `position={[${rNbr(obj.position.x)}, ${rNbr(obj.position.y)}, ${rNbr(obj.position.z)},]} ` 188 | if ( 189 | obj.rotation && 190 | obj.rotation.isEuler && 191 | rNbr(new THREE.Vector3(obj.rotation.x, obj.rotation.y, obj.rotation.z).length()) 192 | ) 193 | result += `rotation={[${rDeg(obj.rotation.x)}, ${rDeg(obj.rotation.y)}, ${rDeg(obj.rotation.z)},]} ` 194 | if ( 195 | obj.scale && 196 | obj.scale.isVector3 && 197 | !(rNbr(obj.scale.x) === 1 && rNbr(obj.scale.y) === 1 && rNbr(obj.scale.z) === 1) 198 | ) { 199 | const rX = rNbr(obj.scale.x) 200 | const rY = rNbr(obj.scale.y) 201 | const rZ = rNbr(obj.scale.z) 202 | if (rX === rY && rX === rZ) { 203 | result += `scale={${rX}} ` 204 | } else { 205 | result += `scale={[${rX}, ${rY}, ${rZ},]} ` 206 | } 207 | } 208 | if (options.meta && obj.userData && Object.keys(obj.userData).length) 209 | result += `userData={${JSON.stringify(obj.userData)}} ` 210 | 211 | return result 212 | } 213 | 214 | function getInfo(obj) { 215 | let type = getType(obj) 216 | let node = 'nodes' + sanitizeName(obj.name) 217 | let instanced = 218 | (options.instance || options.instanceall) && 219 | obj.geometry && 220 | obj.material && 221 | duplicates.geometries[obj.geometry.uuid + obj.material.name] && 222 | duplicates.geometries[obj.geometry.uuid + obj.material.name].count > (options.instanceall ? 0 : 1) 223 | let animated = gltf.animations && gltf.animations.length > 0 224 | return { type, node, instanced, animated } 225 | } 226 | 227 | function equalOrNegated(a, b) { 228 | return (a.x === b.x || a.x === -b.x) && (a.y === b.y || a.y === -b.y) && (a.z === b.z || a.z === -b.z) 229 | } 230 | 231 | function prune(obj, children, result, oldResult, silent) { 232 | let { type, animated } = getInfo(obj) 233 | // Prune ... 234 | if (!obj.__removed && !options.keepgroups && !animated && (type === 'group' || type === 'scene')) { 235 | /** Empty or no-property groups 236 | * 237 | * 238 | * Solution: 239 | * 240 | */ 241 | if (result === oldResult || obj.children.length === 0) { 242 | if (options.debug && !silent) console.log(`group ${obj.name} removed (empty)`) 243 | obj.__removed = true 244 | return children 245 | } 246 | 247 | // More aggressive removal strategies ... 248 | const first = obj.children[0] 249 | const firstProps = handleProps(first) 250 | const regex = /([a-z-A-Z]*)={([a-zA-Z0-9\.\[\]\-\,\ \/]*)}/g 251 | const keys1 = [...result.matchAll(regex)].map(([, match]) => match) 252 | const values1 = [...result.matchAll(regex)].map(([, , match]) => match) 253 | const keys2 = [...firstProps.matchAll(regex)].map(([, match]) => match) 254 | 255 | /** Double negative rotations 256 | * 257 | * 258 | * 259 | * Solution: 260 | * 261 | */ 262 | if (obj.children.length === 1 && getType(first) === type && equalOrNegated(obj.rotation, first.rotation)) { 263 | if (keys1.length === 1 && keys2.length === 1 && keys1[0] === 'rotation' && keys2[0] === 'rotation') { 264 | if (options.debug && !silent) console.log(`group ${obj.name} removed (aggressive: double negative rotation)`) 265 | obj.__removed = first.__removed = true 266 | children = '' 267 | if (first.children) first.children.forEach((child) => (children += print(child, true))) 268 | return children 269 | } 270 | } 271 | 272 | /** Double negative rotations w/ props 273 | * 274 | * 275 | * 276 | * Solution: 277 | * 278 | * 279 | */ 280 | if (obj.children.length === 1 && getType(first) === type && equalOrNegated(obj.rotation, first.rotation)) { 281 | if (keys1.length === 1 && keys2.length > 1 && keys1[0] === 'rotation' && keys2.includes('rotation')) { 282 | if (options.debug && !silent) 283 | console.log(`group ${obj.name} removed (aggressive: double negative rotation w/ props)`) 284 | obj.__removed = true 285 | // Remove rotation from first child 286 | first.rotation.set(0, 0, 0) 287 | children = print(first, true) 288 | return children 289 | } 290 | } 291 | 292 | /** Transform overlap 293 | * 294 | * 295 | * Solution: 296 | * 297 | */ 298 | const isChildTransformed = keys2.includes('position') || keys2.includes('rotation') || keys2.includes('scale') 299 | const hasOtherProps = keys1.some((key) => !['position', 'scale', 'rotation'].includes(key)) 300 | if (obj.children.length === 1 && !first.__removed && !isChildTransformed && !hasOtherProps) { 301 | if (options.debug && !silent) console.log(`group ${obj.name} removed (aggressive: ${keys1.join(' ')} overlap)`) 302 | // Move props over from the to-be-deleted object to the child 303 | // This ensures that the child will have the correct transform when pruning is being repeated 304 | keys1.forEach((key) => obj.children[0][key].copy(obj[key])) 305 | // Insert the props into the result string 306 | children = print(first, true) 307 | obj.__removed = true 308 | return children 309 | } 310 | 311 | /** Lack of content 312 | * 313 | * 314 | * 315 | * Solution: 316 | * (delete the whole sub graph) 317 | */ 318 | const empty = [] 319 | obj.traverse((o) => { 320 | const type = getType(o) 321 | if (type !== 'group' && type !== 'object3D') empty.push(o) 322 | }) 323 | if (!empty.length) { 324 | if (options.debug && !silent) console.log(`group ${obj.name} removed (aggressive: lack of content)`) 325 | empty.forEach((child) => (child.__removed = true)) 326 | return '' 327 | } 328 | } 329 | } 330 | 331 | function print(obj, silent = false) { 332 | let result = '' 333 | let children = '' 334 | let { type, node, instanced, animated } = getInfo(obj) 335 | 336 | // Check if the root node is useless 337 | if (obj.__removed && obj.children.length) { 338 | obj.children.forEach((child) => (result += print(child))) 339 | return result 340 | } 341 | 342 | // Bail out on bones 343 | if (!options.bones && type === 'bone') { 344 | return `` 345 | } 346 | 347 | // Take care of lights with targets 348 | if (type.endsWith('Light') && obj.target && obj.children[0] === obj.target) { 349 | return `<${type} ${handleProps(obj)} target={${node}.target}> 350 | 351 | ` 352 | } 353 | 354 | // Collect children 355 | if (obj.children) obj.children.forEach((child) => (children += print(child))) 356 | 357 | if (instanced) { 358 | result = `' : '/>'}\n` 385 | 386 | // Add children and return 387 | if (children.length) { 388 | if (type === 'bone') result += children + `` 389 | else result += children + `` 390 | } 391 | return result 392 | } 393 | 394 | function printAnimations(animations) { 395 | return animations.length ? `\nconst { actions } = useAnimations(animations, group)` : '' 396 | } 397 | 398 | function parseExtras(extras) { 399 | if (extras) { 400 | return ( 401 | Object.keys(extras) 402 | .map((key) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${extras[key]}`) 403 | .join('\n') + '\n' 404 | ) 405 | } else return '' 406 | } 407 | 408 | function p(obj, line) { 409 | console.log( 410 | [...new Array(line * 2)].map(() => ' ').join(''), 411 | obj.type, 412 | obj.name, 413 | 'pos:', 414 | obj.position.toArray().map(rNbr), 415 | 'scale:', 416 | obj.scale.toArray().map(rNbr), 417 | 'rot:', 418 | [obj.rotation.x, obj.rotation.y, obj.rotation.z].map(rNbr), 419 | 'mat:', 420 | obj.material ? `${obj.material.name}-${obj.material.uuid.substring(0, 8)}` : '' 421 | ) 422 | obj.children.forEach((o) => p(o, line + 1)) 423 | } 424 | 425 | if (options.debug) p(gltf.scene, 0) 426 | 427 | let scene 428 | try { 429 | if (!options.keepgroups) { 430 | // Dry run to prune graph 431 | print(gltf.scene) 432 | // Move children of deleted objects to their new parents 433 | objects.forEach((o) => { 434 | if (o.__removed) { 435 | let parent = o.parent 436 | // Making sure we don't add to a removed parent 437 | while (parent && parent.__removed) parent = parent.parent 438 | // If no parent was found it must be the root node 439 | if (!parent) parent = gltf.scene 440 | o.children.slice().forEach((child) => parent.add(child)) 441 | } 442 | }) 443 | // Remove deleted objects 444 | objects.forEach((o) => { 445 | if (o.__removed && o.parent) o.parent.remove(o) 446 | }) 447 | } 448 | // 2nd pass to eliminate hard to swat left-overs 449 | scene = print(gltf.scene) 450 | } catch (e) { 451 | console.log('Error while parsing glTF', e) 452 | } 453 | const header = `/* 454 | ${options.header ? options.header : 'Auto-generated by: https://github.com/pmndrs/gltfjsx'} ${ 455 | options.size ? `\nFiles: ${options.size}` : '' 456 | } 457 | ${parseExtras(gltf.parser.json.asset && gltf.parser.json.asset.extras)}*/` 458 | const hasPrimitives = scene.includes(' ({ 483 | ${Object.values(duplicates.geometries) 484 | .map((v) => `${v.name}: ${v.node}`) 485 | .join(', ')} 486 | }), [nodes]) 487 | return ( 488 | 489 | {(instances${ 490 | options.types ? ': ContextType' : '' 491 | }) => } 492 | 493 | ) 494 | } 495 | ` 496 | : '' 497 | } 498 | 499 | export ${options.exportdefault ? 'default' : ''} function Model(props${ 500 | options.types ? ": JSX.IntrinsicElements['group']" : '' 501 | }) { 502 | ${hasInstances ? 'const instances = React.useContext(context);' : ''} ${ 503 | hasAnimations ? `const group = ${options.types ? 'React.useRef()' : 'React.useRef()'};` : '' 504 | } ${ 505 | !options.instanceall 506 | ? `const { ${!hasPrimitives ? `nodes, materials` : 'scene'} ${hasAnimations ? ', animations' : ''}} = useGLTF('${url}'${ 507 | options.draco ? `, ${JSON.stringify(options.draco)}` : '' 508 | })${!hasPrimitives && options.types ? ' as GLTFResult' : ''}${ 509 | hasPrimitives 510 | ? `\nconst clone = React.useMemo(() => SkeletonUtils.clone(scene), [scene]) 511 | const { nodes, materials } = useGraph(clone) ${options.types ? ' as GLTFResult' : ''}` 512 | : '' 513 | }` 514 | : '' 515 | } ${printAnimations(animations)} 516 | return ( 517 | 518 | ${scene} 519 | 520 | ) 521 | } 522 | 523 | useGLTF.preload('${url}')` 524 | 525 | if (!options.console) console.log(header) 526 | const output = header + '\n' + result 527 | const formatted = prettier.format(output, { 528 | semi: false, 529 | printWidth: options.printwidth || 1000, 530 | singleQuote: true, 531 | jsxBracketSameLine: true, 532 | parser: 'babel-ts', 533 | plugins: [babelParser], 534 | }) 535 | return formatted 536 | } 537 | 538 | export default parse 539 | -------------------------------------------------------------------------------- /src/utils/transform.js: -------------------------------------------------------------------------------- 1 | import { Logger, NodeIO } from '@gltf-transform/core' 2 | import { 3 | simplify, 4 | instance, 5 | flatten, 6 | dequantize, 7 | reorder, 8 | join, 9 | weld, 10 | sparse, 11 | dedup, 12 | resample, 13 | prune, 14 | textureCompress, 15 | draco, 16 | palette, 17 | unpartition, 18 | } from '@gltf-transform/functions' 19 | import { ALL_EXTENSIONS } from '@gltf-transform/extensions' 20 | import { MeshoptDecoder, MeshoptEncoder, MeshoptSimplifier } from 'meshoptimizer' 21 | import { ready as resampleReady, resample as resampleWASM } from 'keyframe-resample' 22 | import draco3d from 'draco3dgltf' 23 | import sharp from 'sharp' 24 | 25 | async function transform(file, output, config = {}) { 26 | await MeshoptDecoder.ready 27 | await MeshoptEncoder.ready 28 | const io = new NodeIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({ 29 | 'draco3d.decoder': await draco3d.createDecoderModule(), 30 | 'draco3d.encoder': await draco3d.createEncoderModule(), 31 | 'meshopt.decoder': MeshoptDecoder, 32 | 'meshopt.encoder': MeshoptEncoder, 33 | }) 34 | if (config.console) io.setLogger(new Logger(Logger.Verbosity.ERROR)) 35 | else io.setLogger(new Logger(Logger.Verbosity.WARN)) 36 | 37 | const document = await io.read(file) 38 | const resolution = config.resolution ?? 1024 39 | const normalResolution = Math.max(resolution, 2048) 40 | const degradeResolution = config.degraderesolution ?? 512 41 | const functions = [unpartition()] 42 | 43 | if (!config.keepmaterials) functions.push(palette({ min: 5 })) 44 | 45 | functions.push( 46 | reorder({ encoder: MeshoptEncoder }), 47 | dedup(), 48 | // This seems problematic ... 49 | // instance({ min: 5 }), 50 | flatten(), 51 | dequantize() // ... 52 | ) 53 | 54 | if (!config.keepmeshes) { 55 | functions.push( 56 | join() // ... 57 | ) 58 | } 59 | 60 | functions.push( 61 | // Weld vertices 62 | weld(), 63 | ) 64 | 65 | if (config.simplify) { 66 | functions.push( 67 | // Simplify meshes 68 | simplify({ simplifier: MeshoptSimplifier, ratio: config.ratio ?? 0, error: config.error ?? 0.0001 }) 69 | ) 70 | } 71 | 72 | functions.push( 73 | resample({ ready: resampleReady, resample: resampleWASM }), 74 | prune({ keepAttributes: false, keepLeaves: false }), 75 | sparse() 76 | ) 77 | 78 | if (config.degrade) { 79 | // Custom per-file resolution 80 | functions.push( 81 | textureCompress({ 82 | encoder: sharp, 83 | pattern: new RegExp(`^(?=${config.degrade}).*$`), 84 | targetFormat: config.format, 85 | resize: [degradeResolution, degradeResolution], 86 | }), 87 | textureCompress({ 88 | encoder: sharp, 89 | pattern: new RegExp(`^(?!${config.degrade}).*$`), 90 | targetFormat: config.format, 91 | resize: [resolution, resolution], 92 | }) 93 | ) 94 | } else { 95 | // Keep normal maps near lossless 96 | functions.push( 97 | textureCompress({ 98 | slots: /^(?!normalTexture).*$/, // exclude normal maps 99 | encoder: sharp, 100 | targetFormat: config.format, 101 | resize: [resolution, resolution], 102 | }), 103 | textureCompress({ 104 | slots: /^(?=normalTexture).*$/, // include normal maps 105 | encoder: sharp, 106 | targetFormat: 'jpeg', 107 | resize: [normalResolution, normalResolution], 108 | }) 109 | ) 110 | } 111 | 112 | functions.push(draco()) 113 | 114 | await document.transform(...functions) 115 | await io.write(output, document) 116 | } 117 | 118 | export default transform 119 | --------------------------------------------------------------------------------