Repository: react-spring/gltfjsx Branch: master Commit: f4fbfa2a19ff Files: 18 Total size: 149.1 KB Directory structure: gitextract_1319rtdu/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── .npmignore ├── .yarnrc.yml ├── LICENSE ├── cli.js ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: drcmda open_collective: react-three-fiber ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: - push - pull_request jobs: test: name: Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: node-version: - 20 - 18 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: corepack enable - run: yarn install --immutable - run: npm test ================================================ FILE: .gitignore ================================================ node_modules/ coverage/ dist/ types/ Thumbs.db ehthumbs.db Desktop.ini $RECYCLE.BIN/ .DS_Store .vscode .docz/ package-lock.json coverage/ .idea yarn-error.log ================================================ FILE: .npmignore ================================================ example/ ================================================ FILE: .yarnrc.yml ================================================ nodeLinker: node-modules ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Paul Henschel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: cli.js ================================================ #!/usr/bin/env node 'use strict' import meow from 'meow' import { fileURLToPath } from 'url' import { dirname } from 'path' import gltfjsx from './src/gltfjsx.js' import { readPackageUpSync } from 'read-pkg-up' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const cli = meow( ` Usage $ npx gltfjsx [Model.glb] [options] Options --output, -o Output file name/path --types, -t Add Typescript definitions --keepnames, -k Keep original names --keepgroups, -K Keep (empty) groups, disable pruning --bones, -b Lay out bones declaratively (default: false) --meta, -m Include metadata (as userData) --shadows, s Let meshes cast and receive shadows --printwidth, w Prettier printWidth (default: 120) --precision, -p Number of fractional digits (default: 3) --draco, -d Draco binary path --root, -r Sets directory from which .gltf file is served --instance, -i Instance re-occuring geometry --instanceall, -I Instance every geometry (for cheaper re-use) --exportdefault, -E Use default export --transform, -T Transform the asset for the web (draco, prune, resize) --resolution, -R Resolution for texture resizing (default: 1024) --keepmeshes, -j Do not join compatible meshes --keepmaterials, -M Do not palette join materials --format, -f Texture format (default: "webp") --simplify, -S Mesh simplification (default: false) --ratio Simplifier ratio (default: 0) --error Simplifier error threshold (default: 0.0001) --console, -c Log JSX to console, won't produce a file --debug, -D Debug output `, { importMeta: import.meta, flags: { output: { type: 'string', shortFlag: 'o' }, types: { type: 'boolean', shortFlag: 't' }, keepnames: { type: 'boolean', shortFlag: 'k' }, keepgroups: { type: 'boolean', shortFlag: 'K' }, bones: { type: 'boolean', shortFlag: 'b', default: false }, shadows: { type: 'boolean', shortFlag: 's' }, printwidth: { type: 'number', shortFlag: 'p', default: 1000 }, meta: { type: 'boolean', shortFlag: 'm' }, precision: { type: 'number', shortFlag: 'p', default: 3 }, draco: { type: 'string', shortFlag: 'd' }, root: { type: 'string', shortFlag: 'r' }, instance: { type: 'boolean', shortFlag: 'i' }, instanceall: { type: 'boolean', shortFlag: 'I' }, transform: { type: 'boolean', shortFlag: 'T' }, resolution: { type: 'number', shortFlag: 'R', default: 1024 }, degrade: { type: 'string', shortFlag: 'q', default: '' }, degraderesolution: { type: 'number', shortFlag: 'Q', default: 512 }, simplify: { type: 'boolean', shortFlag: 'S', default: false }, keepmeshes: { type: 'boolean', shortFlag: 'j', default: false }, keepmaterials: { type: 'boolean', shortFlag: 'M', default: false }, format: { type: 'string', shortFlag: 'f', default: 'webp' }, exportdefault: { type: 'boolean', shortFlag: 'E' }, ratio: { type: 'number', default: 0.75 }, error: { type: 'number', default: 0.001 }, console: { type: 'boolean', shortFlag: 'c' }, debug: { type: 'boolean', shortFlag: 'D' }, }, } ) const { packageJson } = readPackageUpSync({ cwd: __dirname, normalize: false }) if (cli.input.length === 0) { console.log(cli.help) } else { const config = { ...cli.flags, header: `Auto-generated by: https://github.com/pmndrs/gltfjsx Command: npx gltfjsx@${packageJson.version} ${process.argv.slice(2).join(' ')}`, } const file = cli.input[0] let nameExt = file.match(/[-_\w\d\s]+[.][\w]+$/i)[0] let name = nameExt.split('.').slice(0, -1).join('.') const output = config.output ?? name.charAt(0).toUpperCase() + name.slice(1) + (config.types ? '.tsx' : '.jsx') const showLog = (log) => { console.info('log:', log) } try { const response = await gltfjsx(file, output, { ...config, showLog, timeout: 0, delay: 1 }) } catch (e) { console.error(e) } } ================================================ FILE: package.json ================================================ { "name": "gltfjsx", "version": "6.5.2", "description": "GLTF to JSX converter", "scripts": { "build": "rollup -c", "test": "node src/test" }, "type": "module", "keywords": [ "gltf", "jsx", "react", "fiber", "three", "threejs", "webp" ], "author": "Paul Henschel", "maintainers": [ "Max Rusan" ], "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/pmndrs/gltfjsx.git" }, "bugs": { "url": "https://github.com/pmndrs/gltfjsx/issues" }, "homepage": "https://github.com/pmndrs/gltfjsx#readme", "bin": "./cli.js", "main": "dist/index.cjs", "module": "dist/index.js", "engines": { "node": ">=16" }, "dependencies": { "@gltf-transform/core": "4.1.0", "@gltf-transform/extensions": "4.1.0", "@gltf-transform/functions": "4.1.0", "@node-loader/babel": "^2.0.1", "draco3dgltf": "^1.5.7", "is-var-name": "^2.0.0", "keyframe-resample": "^0.1.0", "meow": "^12.1.1", "meshoptimizer": "^0.22.0", "prettier": "3.1.1", "read-pkg-up": "^10.1.0", "three": "0.159.0", "three-stdlib": "^2.28.7" }, "optionalDependencies": { "jsdom": "^24.1.0", "jsdom-global": "^3.0.2", "libvips": "0.0.2", "sharp": "^0.33.5" }, "resolutions": { "sharp": "<0.33.0", "@gltf-transform/core": "4.0.8", "@gltf-transform/extensions": "4.0.8", "@gltf-transform/functions": "4.0.8" }, "devDependencies": { "@babel/core": "7.23.6", "@babel/plugin-proposal-class-properties": "^7.16.0", "@babel/plugin-transform-modules-commonjs": "7.23.3", "@babel/plugin-transform-parameters": "7.23.3", "@babel/plugin-transform-runtime": "7.23.6", "@babel/plugin-transform-template-literals": "7.23.3", "@babel/preset-env": "7.23.6", "@babel/preset-react": "7.23.3", "@babel/preset-typescript": "^7.23.3", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-node-resolve": "^15.2.3", "chalk": "^5.3.0", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "lint-staged": "^13.2.0", "rollup": "^4.9.1", "rollup-plugin-size-snapshot": "^0.12.0", "rollup-plugin-terser": "^7.0.2" }, "prettier": { "semi": false, "trailingComma": "es5", "singleQuote": true, "jsxBracketSameLine": true, "tabWidth": 2, "printWidth": 120 }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ "prettier --write" ] }, "collective": { "type": "opencollective", "url": "https://opencollective.com/react-three-fiber" }, "packageManager": "yarn@4.4.0+sha256.5f228cb28f2edb97d8c3b667fb7b2fdcf06c46798e25ea889dad9e0b4bc2e2c1" } ================================================ FILE: readme.md ================================================ https://user-images.githubusercontent.com/2223602/126318148-99da7ed6-a578-48dd-bdd2-21056dbad003.mp4

[![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) 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. ### The GLTF workflow on the web is not ideal ... - GLTF is thrown whole into the scene which prevents re-use, in threejs objects can only be mounted once - Contents can only be found by traversal which is cumbersome and slow - Changes to queried nodes are made by mutation, which alters the source data and prevents re-use - Re-structuring content, making nodes conditional or adding/removing is cumbersome - Model compression is complex and not easily achieved - Models often have unnecessary nodes that cause extra work and matrix updates ### GLTFJSX fixes that - 🧑‍💻 It creates a virtual graph of all objects and materials. Now you can easily alter contents and re-use. - 🏎️ The graph gets pruned (empty groups, unnecessary transforms, ...) and will perform better. - ⚡️ It will optionally compress your model with up to 70%-90% size reduction. ## Usage ```text Usage $ npx gltfjsx [Model.glb] [options] Options --output, -o Output file name/path --types, -t Add Typescript definitions --keepnames, -k Keep original names --keepgroups, -K Keep (empty) groups, disable pruning --bones, -b Lay out bones declaratively (default: false) --meta, -m Include metadata (as userData) --shadows, s Let meshes cast and receive shadows --printwidth, w Prettier printWidth (default: 120) --precision, -p Number of fractional digits (default: 3) --draco, -d Draco binary path --root, -r Sets directory from which .gltf file is served --instance, -i Instance re-occuring geometry --instanceall, -I Instance every geometry (for cheaper re-use) --exportdefault, -E Use default export --transform, -T Transform the asset for the web (draco, prune, resize) --resolution, -R Resolution for texture resizing (default: 1024) --keepmeshes, -j Do not join compatible meshes --keepmaterials, -M Do not palette join materials --format, -f Texture format (default: "webp") --simplify, -S Mesh simplification (default: false) --ratio Simplifier ratio (default: 0) --error Simplifier error threshold (default: 0.0001) --console, -c Log JSX to console, won't produce a file --debug, -D Debug output ``` ### A typical use-case First you run your model through gltfjsx. `npx` allows you to use npm packages without installing them. ```bash npx gltfjsx model.gltf --transform ``` This will create a `Model.jsx` file that plots out all of the assets contents. ```jsx /* auto-generated by: https://github.com/pmdrs/gltfjsx author: abcdef (https://sketchfab.com/abcdef) license: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/) source: https://sketchfab.com/models/... title: Model */ import { useGLTF, PerspectiveCamera } from '@react-three/drei' export function Model(props) { const { nodes, materials } = useGLTF('/model-transformed.glb') return ( ) } useGLTF.preload('/model.gltf') ``` 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. ```text /public model-transformed.glb ``` The component can now be dropped into your scene. ```jsx import { Canvas } from '@react-three/fiber' import { Model } from './Model' function App() { return ( ``` You can re-use it, it will re-use geometries and materials out of the box: ```jsx ``` Or make the model dynamic. Change its colors for example: ```jsx ``` Or exchange materials: ```jsx ``` Make contents conditional: ```jsx {condition && } ``` Add events: ```jsx ``` ## Features #### ⚡️ Draco and meshopt compression ootb 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. #### ⚡️ Preload your assets for faster response 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. ```jsx useGLTF.preload('/model.gltf') ``` #### ⚡️ Auto-transform (compression, resize) 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%. It will not alter the original but create a copy and append `[modelname]-transformed.glb`. #### ⚡️ Type-safety Add the `--types` flag and your GLTF will be typesafe. ```tsx type GLTFResult = GLTF & { nodes: { robot: THREE.Mesh; rocket: THREE.Mesh } materials: { metal: THREE.MeshStandardMaterial; wood: THREE.MeshStandardMaterial } } export default function Model(props: JSX.IntrinsicElements['group']) { const { nodes, materials } = useGLTF('/model.gltf') ``` #### ⚡️ Easier access to animations 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: ```jsx const { nodes, materials, animations } = useGLTF('/model.gltf') const { actions } = useAnimations(animations, group) ``` If you want to play an animation you can do so at any time: ```jsx actions.jump.play()} /> ``` If you want to blend animations: ```jsx const [name, setName] = useState("jump") ... useEffect(() => { actions[name].reset().fadeIn(0.5).play() return () => actions[name].fadeOut(0.5) }, [name]) ``` #### ⚡️ Auto-instancing 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. `--instanceall` will create instances of all the geometry. This allows you to re-use the model with the smallest amount of drawcalls. Your export will look like something like this: ```jsx const context = createContext() export function Instances({ children, ...props }) { const { nodes } = useGLTF('/model-transformed.glb') const instances = useMemo(() => ({ Screw1: nodes['Screw1'], Screw2: nodes['Screw2'] }), [nodes]) return ( {(instances) => } ) } export function Model(props) { const instances = useContext(context) return ( ) } ``` 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). The following will show the model three times, but you will only have 2 drawcalls tops. ```jsx import { Instances, Model } from './Model' ``` ## Using the parser stand-alone ```jsx import { parse } from 'gltfjsx' import { GLTFLoader, DRACOLoader } from 'three-stdlib' const gltfLoader = new GLTFLoader() const dracoloader = new DRACOLoader() dracoloader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/') gltfLoader.setDRACOLoader(dracoloader) gltfLoader.load(url, (gltf) => { const jsx = parse(gltf, optionalConfig) }) ``` ## Using the parser stand-alone for scenes (object3d's) ```jsx const jsx = parse(scene, optionalConfig) ``` ## Using GLTFStructureLoader stand-alone 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. ```jsx import { GLTFStructureLoader } from 'gltfjsx' import fs from 'fs/promises' it('should have a scene with a blue mesh', async () => { const loader = new GLTFStructureLoader() const data = await fs.readFile('./model.glb') const { scene } = await new Promise((res) => loader.parse(data, '', res)) expect(() => scene.children.length).toEqual(1) expect(() => scene.children[0].type).toEqual('mesh') expect(() => scene.children[0].material.color).toEqual('blue') }) ``` ## Requirements - Nodejs must be installed - The GLTF file has to be present in your projects `/public` folder - [three](https://github.com/mrdoob/three.js/) (>= 122.x) - [@react-three/fiber](https://github.com/pmndrs/react-three-fiber) (>= 5.x) - [@react-three/drei](https://github.com/pmndrs/drei) (>= 2.x) ================================================ FILE: rollup.config.js ================================================ import path from 'path' import babel from '@rollup/plugin-babel' import resolve from '@rollup/plugin-node-resolve' const root = process.platform === 'win32' ? path.resolve('/') : '/' const external = (id) => !id.startsWith('.') && !id.startsWith(root) const extensions = ['.js', '.jsx', '.ts', '.tsx', '.json'] const getBabelOptions = ({ useESModules }) => ({ babelrc: false, extensions, exclude: '**/node_modules/**', babelHelpers: 'runtime', presets: [ [ '@babel/preset-env', { include: [ '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-numeric-separator', '@babel/plugin-proposal-logical-assignment-operators', ], bugfixes: true, loose: true, modules: false, targets: '> 1%, not dead, not ie 11, not op_mini all', }, ], ], plugins: [['@babel/transform-runtime', { regenerator: false, useESModules }]], }) export default [ { input: `./src/utils/exports.js`, output: { file: `dist/index.js`, format: 'esm' }, external, plugins: [babel(getBabelOptions({ useESModules: true })), resolve({ extensions })], }, { input: `./src/utils/exports.js`, output: { file: `dist/index.cjs.js`, format: 'cjs' }, external, plugins: [babel(getBabelOptions({ useESModules: false })), resolve({ extensions })], }, ] ================================================ FILE: src/bin/DRACOLoader.js ================================================ import * as THREE from 'three' import draco from 'draco3dgltf' const decoder = draco.createDecoderModule() const DRACOLoader = function (t) { ;(this.timeLoaded = 0), (this.manager = t || THREE.DefaultLoadingManager), (this.materials = null), (this.verbosity = 0), (this.attributeOptions = {}), (this.drawMode = THREE.TrianglesDrawMode), (this.nativeAttributeMap = { position: 'POSITION', normal: 'NORMAL', color: 'COLOR', uv: 'TEX_COORD' }) } DRACOLoader.prototype = { constructor: DRACOLoader, load: function (t, e, r, o) { var i = this, n = new THREE.FileLoader(i.manager) n.setPath(this.path), n.setResponseType('arraybuffer'), n.load( t, function (t) { i.decodeDracoFile(t, e) }, r, o ) }, setPath: function (t) { return (this.path = t), this }, setVerbosity: function (t) { return (this.verbosity = t), this }, setDrawMode: function (t) { return (this.drawMode = t), this }, setSkipDequantization: function (t, e) { var r = !0 return void 0 !== e && (r = e), (this.getAttributeOptions(t).skipDequantization = r), this }, decodeDracoFile: function (t, e, r, o) { decoder.then((decoder) => this.decodeDracoFileInternal(t, decoder, e, r, o)) }, decodeDracoFileInternal: function (t, e, r, o, i) { var n = new e.DecoderBuffer() n.Init(new Int8Array(t), t.byteLength) var a = new e.Decoder(), s = a.GetEncodedGeometryType(n) if (s == e.TRIANGULAR_MESH) this.verbosity > 0 && console.log('Loaded a mesh.') else { if (s != e.POINT_CLOUD) { var u = 'THREE.DRACOLoader: Unknown geometry type.' //throw (console.error(u), new Error(u)) } this.verbosity > 0 && console.log('Loaded a point cloud.') } r(this.convertDracoGeometryTo3JS(e, a, s, n, o, i)) }, addAttributeToGeometry: function (t, e, r, o, i, n, a, s) { if (0 === n.ptr) { var u = 'THREE.DRACOLoader: No attribute ' + o throw (console.error(u), new Error(u)) } var d, A, c = n.num_components(), l = r.num_points() * c switch (i) { case Float32Array: ;(d = new t.DracoFloat32Array()), e.GetAttributeFloatForAllPoints(r, n, d), (s[o] = new Float32Array(l)), (A = THREE.Float32BufferAttribute) break case Int8Array: ;(d = new t.DracoInt8Array()), e.GetAttributeInt8ForAllPoints(r, n, d), (s[o] = new Int8Array(l)), (A = THREE.Int8BufferAttribute) break case Int16Array: ;(d = new t.DracoInt16Array()), e.GetAttributeInt16ForAllPoints(r, n, d), (s[o] = new Int16Array(l)), (A = THREE.Int16BufferAttribute) break case Int32Array: ;(d = new t.DracoInt32Array()), e.GetAttributeInt32ForAllPoints(r, n, d), (s[o] = new Int32Array(l)), (A = THREE.Int32BufferAttribute) break case Uint8Array: ;(d = new t.DracoUInt8Array()), e.GetAttributeUInt8ForAllPoints(r, n, d), (s[o] = new Uint8Array(l)), (A = THREE.Uint8BufferAttribute) break case Uint16Array: ;(d = new t.DracoUInt16Array()), e.GetAttributeUInt16ForAllPoints(r, n, d), (s[o] = new Uint16Array(l)), (A = THREE.Uint16BufferAttribute) break case Uint32Array: ;(d = new t.DracoUInt32Array()), e.GetAttributeUInt32ForAllPoints(r, n, d), (s[o] = new Uint32Array(l)), (A = THREE.Uint32BufferAttribute) break default: u = 'THREE.DRACOLoader: Unexpected attribute type.' throw (console.error(u), new Error(u)) } for (var b = 0; b < l; b++) s[o][b] = d.GetValue(b) a.setAttribute(o, new A(s[o], c)), t.destroy(d) }, convertDracoGeometryTo3JS: function (t, e, r, o, i, n) { var a, s, u if ( (!0 === this.getAttributeOptions('position').skipDequantization && e.SkipAttributeTransform(t.POSITION), r === t.TRIANGULAR_MESH ? ((a = new t.Mesh()), (s = e.DecodeBufferToMesh(o, a))) : ((a = new t.PointCloud()), (s = e.DecodeBufferToPointCloud(o, a))), !s.ok() || 0 == a.ptr) ) { return new THREE.BufferGeometry() var d = 'THREE.DRACOLoader: Decoding failed: ' throw ((d += s.error_msg()), console.error(d), t.destroy(e), t.destroy(a), new Error(d)) } t.destroy(o), r == t.TRIANGULAR_MESH ? ((u = a.num_faces()), this.verbosity > 0 && console.log('Number of faces loaded: ' + u.toString())) : (u = 0) var A = a.num_points(), c = a.num_attributes() this.verbosity > 0 && (console.log('Number of points loaded: ' + A.toString()), console.log('Number of attributes loaded: ' + c.toString())) var l = e.GetAttributeId(a, t.POSITION) if (-1 == l) { d = 'THREE.DRACOLoader: No position attribute found.' throw (console.error(d), t.destroy(e), t.destroy(a), new Error(d)) } var b = e.GetAttribute(a, l), f = {}, y = new THREE.BufferGeometry() if (i) for (var E in i) { var h = n[E], w = i[E], p = e.GetAttributeByUniqueId(a, w) //this.addAttributeToGeometry(t, e, a, E, h, p, y, f) } else for (var E in this.nativeAttributeMap) { var T = e.GetAttributeId(a, t[this.nativeAttributeMap[E]]) if (-1 !== T) { this.verbosity > 0 && console.log('Loaded ' + E + ' attribute.') p = e.GetAttribute(a, T) //this.addAttributeToGeometry(t, e, a, E, Float32Array, p, y, f) } } if (r == t.TRIANGULAR_MESH) if (this.drawMode === THREE.TriangleStripDrawMode) { var R = new t.DracoInt32Array() e.GetTriangleStripsFromMesh(a, R) f.indices = new Uint32Array(R.size()) for (var I = 0; I < R.size(); ++I) f.indices[I] = R.GetValue(I) t.destroy(R) } else { var v = 3 * u f.indices = new Uint32Array(v) var D = new t.DracoInt32Array() for (I = 0; I < u; ++I) { e.GetFaceFromMesh(a, I, D) var m = 3 * I ;(f.indices[m] = D.GetValue(0)), (f.indices[m + 1] = D.GetValue(1)), (f.indices[m + 2] = D.GetValue(2)) } t.destroy(D) } ;(y.drawMode = this.drawMode), r == t.TRIANGULAR_MESH && y.setIndex( new (f.indices.length > 65535 ? THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute)(f.indices, 1) ) var G = new t.AttributeQuantizationTransform() if (G.InitFromAttribute(b)) { ;(y.attributes.position.isQuantized = !0), (y.attributes.position.maxRange = G.range()), (y.attributes.position.numQuantizationBits = G.quantization_bits()), (y.attributes.position.minValues = new Float32Array(3)) for (I = 0; I < 3; ++I) y.attributes.position.minValues[I] = G.min_value(I) } return t.destroy(G), t.destroy(e), t.destroy(a), y }, isVersionSupported: function (t, e) { e(decoder.isVersionSupported(t)) }, getAttributeOptions: function (t) { return void 0 === this.attributeOptions[t] && (this.attributeOptions[t] = {}), this.attributeOptions[t] }, } export { DRACOLoader } ================================================ FILE: src/bin/GLTFLoader.js ================================================ import * as THREE from 'three' export class GLTFLoader extends THREE.Loader { constructor() { super() this.dracoLoader = null this.ddsLoader = null this.ktx2Loader = null this.pluginCallbacks = [] this.register(function (parser) { return new GLTFMaterialsClearcoatExtension(parser) }) this.register(function (parser) { return new GLTFTextureBasisUExtension(parser) }) this.register(function (parser) { return new GLTFMaterialsTransmissionExtension(parser) }) this.register(function (parser) { return new GLTFLightsExtension(parser) }) this.register(function (parser) { return new GLTFMeshGpuInstancing(parser) }) } load(url, onLoad, onProgress, onError) { var scope = this var resourcePath if (this.resourcePath !== '') { resourcePath = this.resourcePath } else if (this.path !== '') { resourcePath = this.path } else { resourcePath = THREE.LoaderUtils.extractUrlBase(url) } // Tells the LoadingManager to track an extra item, which resolves after // the model is fully loaded. This means the count of items loaded will // be incorrect, but ensures manager.onLoad() does not fire early. this.manager.itemStart(url) var _onError = function (e) { if (onError) { onError(e) } else { console.error(e) } scope.manager.itemError(url) scope.manager.itemEnd(url) } var loader = new THREE.FileLoader(this.manager) loader.setPath(this.path) loader.setResponseType('arraybuffer') loader.setRequestHeader(this.requestHeader) loader.setWithCredentials(this.withCredentials) loader.load( url, function (data) { try { scope.parse( data, resourcePath, function (gltf) { onLoad(gltf) scope.manager.itemEnd(url) }, _onError ) } catch (e) { _onError(e) } }, onProgress, _onError ) } setDRACOLoader(dracoLoader) { this.dracoLoader = dracoLoader return this } setDDSLoader(ddsLoader) { this.ddsLoader = ddsLoader return this } setKTX2Loader(ktx2Loader) { this.ktx2Loader = ktx2Loader return this } register(callback) { if (this.pluginCallbacks.indexOf(callback) === -1) { this.pluginCallbacks.push(callback) } return this } unregister(callback) { if (this.pluginCallbacks.indexOf(callback) !== -1) { this.pluginCallbacks.splice(this.pluginCallbacks.indexOf(callback), 1) } return this } parse(data, path, onLoad, onError) { var content var extensions = {} var plugins = {} if (typeof data === 'string') { content = data } else { var magic = THREE.LoaderUtils.decodeText(new Uint8Array(data, 0, 4)) if (magic === BINARY_EXTENSION_HEADER_MAGIC) { try { extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data) } catch (error) { if (onError) onError(error) return } content = extensions[EXTENSIONS.KHR_BINARY_GLTF].content } else { content = THREE.LoaderUtils.decodeText(new Uint8Array(data)) } } var json = JSON.parse(content) if (json.asset === undefined || json.asset.version[0] < 2) { if (onError) onError(new Error('THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.')) return } var parser = new GLTFParser(json, { path: path || this.resourcePath || '', crossOrigin: this.crossOrigin, manager: this.manager, ktx2Loader: this.ktx2Loader, }) parser.fileLoader.setRequestHeader(this.requestHeader) for (var i = 0; i < this.pluginCallbacks.length; i++) { var plugin = this.pluginCallbacks[i](parser) plugins[plugin.name] = plugin // Workaround to avoid determining as unknown extension // in addUnknownExtensionsToUserData(). // Remove this workaround if we move all the existing // extension handlers to plugin system extensions[plugin.name] = true } if (json.extensionsUsed) { for (var i = 0; i < json.extensionsUsed.length; ++i) { var extensionName = json.extensionsUsed[i] var extensionsRequired = json.extensionsRequired || [] switch (extensionName) { case EXTENSIONS.KHR_MATERIALS_UNLIT: extensions[extensionName] = new GLTFMaterialsUnlitExtension() break case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: extensions[extensionName] = new GLTFMaterialsPbrSpecularGlossinessExtension() break case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: extensions[extensionName] = new GLTFDracoMeshCompressionExtension(json, this.dracoLoader) break case EXTENSIONS.MSFT_TEXTURE_DDS: extensions[extensionName] = new GLTFTextureDDSExtension(this.ddsLoader) break case EXTENSIONS.KHR_TEXTURE_TRANSFORM: extensions[extensionName] = new GLTFTextureTransformExtension() break case EXTENSIONS.KHR_MESH_QUANTIZATION: extensions[extensionName] = new GLTFMeshQuantizationExtension() break default: if (extensionsRequired.indexOf(extensionName) >= 0 && plugins[extensionName] === undefined) { // console.warn('THREE.GLTFLoader: Unknown extension "' + extensionName + '".') } } } } parser.setExtensions(extensions) parser.setPlugins(plugins) parser.parse(onLoad, onError) } } /* GLTFREGISTRY */ function GLTFRegistry() { var objects = {} return { get: function (key) { return objects[key] }, add: function (key, object) { objects[key] = object }, remove: function (key) { delete objects[key] }, removeAll: function () { objects = {} }, } } /*********************************/ /********** EXTENSIONS ***********/ /*********************************/ var EXTENSIONS = { KHR_BINARY_GLTF: 'KHR_binary_glTF', KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission', KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', KHR_TEXTURE_BASISU: 'KHR_texture_basisu', KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', MSFT_TEXTURE_DDS: 'MSFT_texture_dds', EXT_MESH_GPU_INSTANCING: 'EXT_mesh_gpu_instancing', } /** * DDS Texture Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds * */ function GLTFTextureDDSExtension(ddsLoader) { if (!ddsLoader) { throw new Error('THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader') } this.name = EXTENSIONS.MSFT_TEXTURE_DDS this.ddsLoader = ddsLoader } /** * Punctual Lights Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual */ function GLTFLightsExtension(parser) { this.parser = parser this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL // Object3D instance caches this.cache = { refs: {}, uses: {} } } GLTFLightsExtension.prototype._markDefs = function () { var parser = this.parser var nodeDefs = this.parser.json.nodes || [] for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) { var nodeDef = nodeDefs[nodeIndex] if (nodeDef.extensions && nodeDef.extensions[this.name] && nodeDef.extensions[this.name].light !== undefined) { parser._addNodeRef(this.cache, nodeDef.extensions[this.name].light) } } } GLTFLightsExtension.prototype._loadLight = function (lightIndex) { var parser = this.parser var cacheKey = 'light:' + lightIndex var dependency = parser.cache.get(cacheKey) if (dependency) return dependency var json = parser.json var extensions = (json.extensions && json.extensions[this.name]) || {} var lightDefs = extensions.lights || [] var lightDef = lightDefs[lightIndex] var lightNode var color = new THREE.Color(0xffffff) if (lightDef.color !== undefined) color.fromArray(lightDef.color) var range = lightDef.range !== undefined ? lightDef.range : 0 switch (lightDef.type) { case 'directional': lightNode = new THREE.DirectionalLight(color) lightNode.target.position.set(0, 0, -1) lightNode.add(lightNode.target) break case 'point': lightNode = new THREE.PointLight(color) lightNode.distance = range break case 'spot': lightNode = new THREE.SpotLight(color) lightNode.distance = range // Handle spotlight properties. lightDef.spot = lightDef.spot || {} lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0 lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0 lightNode.angle = lightDef.spot.outerConeAngle lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle lightNode.target.position.set(0, 0, -1) lightNode.add(lightNode.target) break default: throw new Error('THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".') } // Some lights (e.g. spot) default to a position other than the origin. Reset the position // here, because node-level parsing will only override position if explicitly specified. lightNode.position.set(0, 0, 0) lightNode.decay = 2 if (lightDef.intensity !== undefined) lightNode.intensity = lightDef.intensity lightNode.name = parser.createUniqueName(lightDef.name || 'light_' + lightIndex) dependency = Promise.resolve(lightNode) parser.cache.add(cacheKey, dependency) return dependency } GLTFLightsExtension.prototype.createNodeAttachment = function (nodeIndex) { var self = this var parser = this.parser var json = parser.json var nodeDef = json.nodes[nodeIndex] var lightDef = (nodeDef.extensions && nodeDef.extensions[this.name]) || {} var lightIndex = lightDef.light if (lightIndex === undefined) return null return this._loadLight(lightIndex).then(function (light) { return parser._getNodeRef(self.cache, lightIndex, light) }) } /** * Unlit Materials Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit */ function GLTFMaterialsUnlitExtension() { this.name = EXTENSIONS.KHR_MATERIALS_UNLIT } GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { return THREE.MeshBasicMaterial } GLTFMaterialsUnlitExtension.prototype.extendParams = function (materialParams, materialDef, parser) { var pending = [] materialParams.color = new THREE.Color(1.0, 1.0, 1.0) materialParams.opacity = 1.0 var metallicRoughness = materialDef.pbrMetallicRoughness if (metallicRoughness) { if (Array.isArray(metallicRoughness.baseColorFactor)) { var array = metallicRoughness.baseColorFactor materialParams.color.fromArray(array) materialParams.opacity = array[3] } } return Promise.all(pending) } /** * Clearcoat Materials Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat */ function GLTFMaterialsClearcoatExtension(parser) { this.parser = parser this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT } GLTFMaterialsClearcoatExtension.prototype.getMaterialType = function (materialIndex) { var parser = this.parser var materialDef = parser.json.materials[materialIndex] if (!materialDef.extensions || !materialDef.extensions[this.name]) return null return THREE.MeshPhysicalMaterial } GLTFMaterialsClearcoatExtension.prototype.extendMaterialParams = function (materialIndex, materialParams) { var parser = this.parser var materialDef = parser.json.materials[materialIndex] if (!materialDef.extensions || !materialDef.extensions[this.name]) { return Promise.resolve() } var pending = [] var extension = materialDef.extensions[this.name] if (extension.clearcoatFactor !== undefined) { materialParams.clearcoat = extension.clearcoatFactor } if (extension.clearcoatRoughnessFactor !== undefined) { materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor } if (extension.clearcoatNormalTexture !== undefined) { if (extension.clearcoatNormalTexture.scale !== undefined) { var scale = extension.clearcoatNormalTexture.scale materialParams.clearcoatNormalScale = new THREE.Vector2(scale, scale) } } return Promise.all(pending) } /** * Transmission Materials Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission * Draft: https://github.com/KhronosGroup/glTF/pull/1698 */ function GLTFMaterialsTransmissionExtension(parser) { this.parser = parser this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION } GLTFMaterialsTransmissionExtension.prototype.getMaterialType = function (materialIndex) { var parser = this.parser var materialDef = parser.json.materials[materialIndex] if (!materialDef.extensions || !materialDef.extensions[this.name]) return null return THREE.MeshPhysicalMaterial } GLTFMaterialsTransmissionExtension.prototype.extendMaterialParams = function (materialIndex, materialParams) { var parser = this.parser var materialDef = parser.json.materials[materialIndex] if (!materialDef.extensions || !materialDef.extensions[this.name]) { return Promise.resolve() } var pending = [] var extension = materialDef.extensions[this.name] if (extension.transmissionFactor !== undefined) { materialParams.transmission = extension.transmissionFactor } return Promise.all(pending) } /** * BasisU Texture Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu * (draft PR https://github.com/KhronosGroup/glTF/pull/1751) */ function GLTFTextureBasisUExtension(parser) { this.parser = parser this.name = EXTENSIONS.KHR_TEXTURE_BASISU } GLTFTextureBasisUExtension.prototype.loadTexture = function (textureIndex) { return Promise.resolve(new THREE.Texture()) } /** * GPU Instancing Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_mesh_gpu_instancing * */ class GLTFMeshGpuInstancing { constructor(parser) { this.name = EXTENSIONS.EXT_MESH_GPU_INSTANCING this.parser = parser } createNodeMesh(nodeIndex) { const json = this.parser.json const nodeDef = json.nodes[nodeIndex] if (!nodeDef.extensions || !nodeDef.extensions[this.name] || nodeDef.mesh === undefined) { return null } const meshDef = json.meshes[nodeDef.mesh] // No Points or Lines + Instancing support yet for (const primitive of meshDef.primitives) { if ( primitive.mode !== WEBGL_CONSTANTS.TRIANGLES && primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_STRIP && primitive.mode !== WEBGL_CONSTANTS.TRIANGLE_FAN && primitive.mode !== undefined ) { return null } } const extensionDef = nodeDef.extensions[this.name] const attributesDef = extensionDef.attributes // @TODO: Can we support InstancedMesh + SkinnedMesh? const pending = [] const attributes = {} for (const key in attributesDef) { pending.push( this.parser.getDependency('accessor', attributesDef[key]).then((accessor) => { attributes[key] = accessor return attributes[key] }) ) } if (pending.length < 1) { return null } pending.push(this.parser.createNodeMesh(nodeIndex)) return Promise.all(pending).then((results) => { const nodeObject = results.pop() const meshes = nodeObject.isGroup ? nodeObject.children : [nodeObject] const count = results[0].count // All attribute counts should be same const instancedMeshes = [] for (const mesh of meshes) { // Temporal variables const m = new THREE.Matrix4() const p = new THREE.Vector3() const q = new THREE.Quaternion() const s = new THREE.Vector3(1, 1, 1) const instancedMesh = new THREE.InstancedMesh(mesh.geometry, mesh.material, count) for (let i = 0; i < count; i++) { if (attributes.TRANSLATION) { p.fromBufferAttribute(attributes.TRANSLATION, i) } if (attributes.ROTATION) { q.fromBufferAttribute(attributes.ROTATION, i) } if (attributes.SCALE) { s.fromBufferAttribute(attributes.SCALE, i) } instancedMesh.setMatrixAt(i, m.compose(p, q, s)) } // Add instance attributes to the geometry, excluding TRS. for (const attributeName in attributes) { if (attributeName !== 'TRANSLATION' && attributeName !== 'ROTATION' && attributeName !== 'SCALE') { mesh.geometry.setAttribute(attributeName, attributes[attributeName]) } } // Just in case THREE.Object3D.prototype.copy.call(instancedMesh, mesh) this.parser.assignFinalMaterial(instancedMesh) instancedMeshes.push(instancedMesh) } if (nodeObject.isGroup) { nodeObject.clear() nodeObject.add(...instancedMeshes) return nodeObject } return instancedMeshes[0] }) } } /* BINARY EXTENSION */ var BINARY_EXTENSION_HEADER_MAGIC = 'glTF' var BINARY_EXTENSION_HEADER_LENGTH = 12 var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4e4f534a, BIN: 0x004e4942 } function GLTFBinaryExtension(data) { this.name = EXTENSIONS.KHR_BINARY_GLTF this.content = null this.body = null var headerView = new DataView(data, 0, BINARY_EXTENSION_HEADER_LENGTH) this.header = { magic: THREE.LoaderUtils.decodeText(new Uint8Array(data.slice(0, 4))), version: headerView.getUint32(4, true), length: headerView.getUint32(8, true), } if (this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC) { throw new Error('THREE.GLTFLoader: Unsupported glTF-Binary header.') } else if (this.header.version < 2.0) { throw new Error('THREE.GLTFLoader: Legacy binary file detected.') } var chunkView = new DataView(data, BINARY_EXTENSION_HEADER_LENGTH) var chunkIndex = 0 while (chunkIndex < chunkView.byteLength) { var chunkLength = chunkView.getUint32(chunkIndex, true) chunkIndex += 4 var chunkType = chunkView.getUint32(chunkIndex, true) chunkIndex += 4 if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON) { var contentArray = new Uint8Array(data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength) this.content = THREE.LoaderUtils.decodeText(contentArray) } else if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN) { var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex this.body = data.slice(byteOffset, byteOffset + chunkLength) } // Clients must ignore chunks with unknown types. chunkIndex += chunkLength } if (this.content === null) { throw new Error('THREE.GLTFLoader: JSON content not found.') } } /** * DRACO Mesh Compression Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression */ function GLTFDracoMeshCompressionExtension(json, dracoLoader) { if (!dracoLoader) { throw new Error('THREE.GLTFLoader: No DRACOLoader instance provided.') } this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION this.json = json this.dracoLoader = dracoLoader } GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function (primitive, parser) { var json = this.json var dracoLoader = this.dracoLoader var bufferViewIndex = primitive.extensions[this.name].bufferView var gltfAttributeMap = primitive.extensions[this.name].attributes var threeAttributeMap = {} var attributeNormalizedMap = {} var attributeTypeMap = {} for (var attributeName in gltfAttributeMap) { var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase() threeAttributeMap[threeAttributeName] = gltfAttributeMap[attributeName] } for (attributeName in primitive.attributes) { var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase() if (gltfAttributeMap[attributeName] !== undefined) { var accessorDef = json.accessors[primitive.attributes[attributeName]] var componentType = WEBGL_COMPONENT_TYPES[accessorDef.componentType] attributeTypeMap[threeAttributeName] = componentType attributeNormalizedMap[threeAttributeName] = accessorDef.normalized === true } } return parser.getDependency('bufferView', bufferViewIndex).then(function (bufferView) { return new Promise(function (resolve) { dracoLoader.decodeDracoFile( bufferView, function (geometry) { for (var attributeName in geometry.attributes) { var attribute = geometry.attributes[attributeName] var normalized = attributeNormalizedMap[attributeName] if (normalized !== undefined) attribute.normalized = normalized } resolve(geometry) }, threeAttributeMap, attributeTypeMap ) }) }) } /** * Texture Transform Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform */ function GLTFTextureTransformExtension() { this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM } GLTFTextureTransformExtension.prototype.extendTexture = function (texture, transform) { texture = texture.clone() if (transform.offset !== undefined) { texture.offset.fromArray(transform.offset) } if (transform.rotation !== undefined) { texture.rotation = transform.rotation } if (transform.scale !== undefined) { texture.repeat.fromArray(transform.scale) } if (transform.texCoord !== undefined) { console.warn('THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.') } texture.needsUpdate = true return texture } /** * Specular-Glossiness Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness */ function GLTFMaterialsPbrSpecularGlossinessExtension() { return { name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, specularGlossinessParams: [ 'color', 'map', 'lightMap', 'lightMapIntensity', 'aoMap', 'aoMapIntensity', 'emissive', 'emissiveIntensity', 'emissiveMap', 'bumpMap', 'bumpScale', 'normalMap', 'normalMapType', 'displacementMap', 'displacementScale', 'displacementBias', 'specularMap', 'specular', 'glossinessMap', 'glossiness', 'alphaMap', 'envMap', 'envMapIntensity', 'refractionRatio', ], getMaterialType: function () { return THREE.MeshStandardMaterial }, extendParams: function (materialParams, materialDef, parser) { var pbrSpecularGlossiness = materialDef.extensions[this.name] materialParams.color = new THREE.Color(1.0, 1.0, 1.0) materialParams.opacity = 1.0 var pending = [] if (Array.isArray(pbrSpecularGlossiness.diffuseFactor)) { var array = pbrSpecularGlossiness.diffuseFactor materialParams.color.fromArray(array) materialParams.opacity = array[3] } materialParams.emissive = new THREE.Color(0.0, 0.0, 0.0) materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0 materialParams.specular = new THREE.Color(1.0, 1.0, 1.0) if (Array.isArray(pbrSpecularGlossiness.specularFactor)) { materialParams.specular.fromArray(pbrSpecularGlossiness.specularFactor) } return Promise.all(pending) }, createMaterial: function (materialParams) { var material = new THREE.MeshStandardMaterial(materialParams) material.fog = true material.color = materialParams.color material.map = materialParams.map === undefined ? null : materialParams.map material.lightMap = null material.lightMapIntensity = 1.0 material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap material.aoMapIntensity = 1.0 material.emissive = materialParams.emissive material.emissiveIntensity = 1.0 material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap material.bumpScale = 1 material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap material.normalMapType = THREE.TangentSpaceNormalMap if (materialParams.normalScale) material.normalScale = materialParams.normalScale material.displacementMap = null material.displacementScale = 1 material.displacementBias = 0 material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap material.specular = materialParams.specular material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap material.glossiness = materialParams.glossiness material.alphaMap = null material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap material.envMapIntensity = 1.0 material.refractionRatio = 0.98 return material }, } } /** * Mesh Quantization Extension * * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization */ function GLTFMeshQuantizationExtension() { this.name = EXTENSIONS.KHR_MESH_QUANTIZATION } /*********************************/ /********** INTERPOLATION ********/ /*********************************/ // Spline Interpolation // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation function GLTFCubicSplineInterpolant(parameterPositions, sampleValues, sampleSize, resultBuffer) { THREE.Interpolant.call(this, parameterPositions, sampleValues, sampleSize, resultBuffer) } GLTFCubicSplineInterpolant.prototype = Object.create(THREE.Interpolant.prototype) GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function (index) { // Copies a sample value to the result buffer. See description of glTF // CUBICSPLINE values layout in interpolate_() function below. var result = this.resultBuffer, values = this.sampleValues, valueSize = this.valueSize, offset = index * valueSize * 3 + valueSize for (var i = 0; i !== valueSize; i++) { result[i] = values[offset + i] } return result } GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_ GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_ GLTFCubicSplineInterpolant.prototype.interpolate_ = function (i1, t0, t, t1) { var result = this.resultBuffer var values = this.sampleValues var stride = this.valueSize var stride2 = stride * 2 var stride3 = stride * 3 var td = t1 - t0 var p = (t - t0) / td var pp = p * p var ppp = pp * p var offset1 = i1 * stride3 var offset0 = offset1 - stride3 var s2 = -2 * ppp + 3 * pp var s3 = ppp - pp var s0 = 1 - s2 var s1 = s3 - pp + p // Layout of keyframe output values for CUBICSPLINE animations: // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] for (var i = 0; i !== stride; i++) { var p0 = values[offset0 + i + stride] // splineVertex_k var m0 = values[offset0 + i + stride2] * td // outTangent_k * (t_k+1 - t_k) var p1 = values[offset1 + i + stride] // splineVertex_k+1 var m1 = values[offset1 + i] * td // inTangent_k+1 * (t_k+1 - t_k) result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1 } return result } /*********************************/ /********** INTERNALS ************/ /*********************************/ /* CONSTANTS */ var WEBGL_CONSTANTS = { FLOAT: 5126, //FLOAT_MAT2: 35674, FLOAT_MAT3: 35675, FLOAT_MAT4: 35676, FLOAT_VEC2: 35664, FLOAT_VEC3: 35665, FLOAT_VEC4: 35666, LINEAR: 9729, REPEAT: 10497, SAMPLER_2D: 35678, POINTS: 0, LINES: 1, LINE_LOOP: 2, LINE_STRIP: 3, TRIANGLES: 4, TRIANGLE_STRIP: 5, TRIANGLE_FAN: 6, UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, } var WEBGL_COMPONENT_TYPES = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array, } var WEBGL_FILTERS = { 9728: THREE.NearestFilter, 9729: THREE.LinearFilter, 9984: THREE.NearestMipmapNearestFilter, 9985: THREE.LinearMipmapNearestFilter, 9986: THREE.NearestMipmapLinearFilter, 9987: THREE.LinearMipmapLinearFilter, } var WEBGL_WRAPPINGS = { 33071: THREE.ClampToEdgeWrapping, 33648: THREE.MirroredRepeatWrapping, 10497: THREE.RepeatWrapping, } var WEBGL_TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16, } var ATTRIBUTES = { POSITION: 'position', NORMAL: 'normal', TANGENT: 'tangent', TEXCOORD_0: 'uv', TEXCOORD_1: 'uv2', COLOR_0: 'color', WEIGHTS_0: 'skinWeight', JOINTS_0: 'skinIndex', } var PATH_PROPERTIES = { scale: 'scale', translation: 'position', rotation: 'quaternion', weights: 'morphTargetInfluences', } var INTERPOLATION = { CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each // keyframe track will be initialized with a default interpolation type, then modified. LINEAR: THREE.InterpolateLinear, STEP: THREE.InterpolateDiscrete, } var ALPHA_MODES = { OPAQUE: 'OPAQUE', MASK: 'MASK', BLEND: 'BLEND', } /* UTILITY FUNCTIONS */ function resolveURL(url, path) { // Invalid URL if (typeof url !== 'string' || url === '') return '' // Host Relative URL if (/^https?:\/\//i.test(path) && /^\//.test(url)) { path = path.replace(/(^https?:\/\/[^\/]+).*/i, '$1') } // Absolute URL http://,https://,// if (/^(https?:)?\/\//i.test(url)) return url // Data URI if (/^data:.*,.*$/i.test(url)) return url // Blob URL if (/^blob:.*$/i.test(url)) return url // Relative URL return path + url } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material */ function createDefaultMaterial(cache) { if (cache['DefaultMaterial'] === undefined) { cache['DefaultMaterial'] = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x000000, metalness: 1, roughness: 1, transparent: false, depthTest: true, side: THREE.FrontSide, }) } return cache['DefaultMaterial'] } function addUnknownExtensionsToUserData(knownExtensions, object, objectDef) { // Add unknown glTF extensions to an object's userData. for (var name in objectDef.extensions) { if (knownExtensions[name] === undefined) { object.userData.gltfExtensions = object.userData.gltfExtensions || {} object.userData.gltfExtensions[name] = objectDef.extensions[name] } } } /** * @param {THREE.Object3D|THREE.Material|THREE.BufferGeometry} object * @param {GLTF.definition} gltfDef */ function assignExtrasToUserData(object, gltfDef) { if (gltfDef.extras !== undefined) { if (typeof gltfDef.extras === 'object') { Object.assign(object.userData, gltfDef.extras) } else { console.warn('THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras) } } } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets * * @param {THREE.BufferGeometry} geometry * @param {Array} targets * @param {GLTFParser} parser * @return {Promise} */ function addMorphTargets(geometry, targets, parser) { var hasMorphPosition = false var hasMorphNormal = false for (var i = 0, il = targets.length; i < il; i++) { var target = targets[i] if (target.POSITION !== undefined) hasMorphPosition = true if (target.NORMAL !== undefined) hasMorphNormal = true if (hasMorphPosition && hasMorphNormal) break } if (!hasMorphPosition && !hasMorphNormal) return Promise.resolve(geometry) var pendingPositionAccessors = [] var pendingNormalAccessors = [] for (var i = 0, il = targets.length; i < il; i++) { var target = targets[i] if (hasMorphPosition) { var pendingAccessor = target.POSITION !== undefined ? parser.getDependency('accessor', target.POSITION) : geometry.attributes.position pendingPositionAccessors.push(pendingAccessor) } if (hasMorphNormal) { var pendingAccessor = target.NORMAL !== undefined ? parser.getDependency('accessor', target.NORMAL) : geometry.attributes.normal pendingNormalAccessors.push(pendingAccessor) } } return Promise.all([Promise.all(pendingPositionAccessors), Promise.all(pendingNormalAccessors)]).then(function ( accessors ) { var morphPositions = accessors[0] var morphNormals = accessors[1] if (hasMorphPosition) geometry.morphAttributes.position = morphPositions if (hasMorphNormal) geometry.morphAttributes.normal = morphNormals geometry.morphTargetsRelative = true return geometry }) } function createPrimitiveKey(primitiveDef) { var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION] var geometryKey if (dracoExtension) { geometryKey = 'draco:' + dracoExtension.bufferView + ':' + dracoExtension.indices + ':' + createAttributesKey(dracoExtension.attributes) } else { geometryKey = primitiveDef.indices + ':' + createAttributesKey(primitiveDef.attributes) + ':' + primitiveDef.mode } return geometryKey } function createAttributesKey(attributes) { var attributesKey = '' var keys = Object.keys(attributes).sort() for (var i = 0, il = keys.length; i < il; i++) { attributesKey += keys[i] + ':' + attributes[keys[i]] + ';' } return attributesKey } /* GLTF PARSER */ function GLTFParser(json, options) { this.json = json || {} this.extensions = {} this.plugins = {} this.options = options || {} // loader object cache this.cache = new GLTFRegistry() // associations between Three.js objects and glTF elements this.associations = new Map() // BufferGeometry caching this.primitiveCache = {} // Object3D instance caches this.meshCache = { refs: {}, uses: {} } this.cameraCache = { refs: {}, uses: {} } this.lightCache = { refs: {}, uses: {} } // Track node names, to ensure no duplicates this.nodeNamesUsed = {} // Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the // expensive work of uploading a texture to the GPU off the main thread. if (typeof createImageBitmap !== 'undefined' && /Firefox/.test(navigator.userAgent) === false) { this.textureLoader = new THREE.ImageBitmapLoader(this.options.manager) } else { this.textureLoader = new THREE.TextureLoader(this.options.manager) } this.textureLoader.setCrossOrigin(this.options.crossOrigin) this.fileLoader = new THREE.FileLoader(this.options.manager) this.fileLoader.setResponseType('arraybuffer') if (this.options.crossOrigin === 'use-credentials') { this.fileLoader.setWithCredentials(true) } } GLTFParser.prototype.setExtensions = function (extensions) { this.extensions = extensions } GLTFParser.prototype.setPlugins = function (plugins) { this.plugins = plugins } GLTFParser.prototype.parse = function (onLoad, onError) { var parser = this var json = this.json var extensions = this.extensions // Clear the loader cache this.cache.removeAll() // Mark the special nodes/meshes in json for efficient parse this._invokeAll(function (ext) { return ext._markDefs && ext._markDefs() }) Promise.all([this.getDependencies('scene'), this.getDependencies('animation'), this.getDependencies('camera')]) .then(function (dependencies) { var result = { scene: dependencies[0][json.scene || 0], scenes: dependencies[0], animations: dependencies[1], cameras: dependencies[2], asset: json.asset, parser: parser, userData: {}, } addUnknownExtensionsToUserData(extensions, result, json) assignExtrasToUserData(result, json) onLoad(result) }) .catch(onError) } /** * Marks the special nodes/meshes in json for efficient parse. */ GLTFParser.prototype._markDefs = function () { var nodeDefs = this.json.nodes || [] var skinDefs = this.json.skins || [] var meshDefs = this.json.meshes || [] // Nothing in the node definition indicates whether it is a Bone or an // Object3D. Use the skins' joint references to mark bones. for (var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex++) { var joints = skinDefs[skinIndex].joints for (var i = 0, il = joints.length; i < il; i++) { nodeDefs[joints[i]].isBone = true } } // Iterate over all nodes, marking references to shared resources, // as well as skeleton joints. for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) { var nodeDef = nodeDefs[nodeIndex] if (nodeDef.mesh !== undefined) { this._addNodeRef(this.meshCache, nodeDef.mesh) // Nothing in the mesh definition indicates whether it is // a SkinnedMesh or Mesh. Use the node's mesh reference // to mark SkinnedMesh if node has skin. if (nodeDef.skin !== undefined) { meshDefs[nodeDef.mesh].isSkinnedMesh = true } } if (nodeDef.camera !== undefined) { this._addNodeRef(this.cameraCache, nodeDef.camera) } } } /** * Counts references to shared node / Object3D resources. These resources * can be reused, or "instantiated", at multiple nodes in the scene * hierarchy. Mesh, Camera, and Light instances are instantiated and must * be marked. Non-scenegraph resources (like Materials, Geometries, and * Textures) can be reused directly and are not marked here. * * Example: CesiumMilkTruck sample model reuses "Wheel" meshes. */ GLTFParser.prototype._addNodeRef = function (cache, index) { if (index === undefined) return if (cache.refs[index] === undefined) { cache.refs[index] = cache.uses[index] = 0 } cache.refs[index]++ } /** Returns a reference to a shared resource, cloning it if necessary. */ GLTFParser.prototype._getNodeRef = function (cache, index, object) { if (cache.refs[index] <= 1) return object var ref = object.clone() ref.name += '_instance_' + cache.uses[index]++ return ref } GLTFParser.prototype._invokeOne = function (func) { var extensions = Object.values(this.plugins) extensions.push(this) for (var i = 0; i < extensions.length; i++) { var result = func(extensions[i]) if (result) return result } } GLTFParser.prototype._invokeAll = function (func) { var extensions = Object.values(this.plugins) extensions.unshift(this) var pending = [] for (var i = 0; i < extensions.length; i++) { var result = func(extensions[i]) if (result) pending.push(result) } return pending } /** * Requests the specified dependency asynchronously, with caching. * @param {string} type * @param {number} index * @return {Promise} */ GLTFParser.prototype.getDependency = function (type, index) { var cacheKey = type + ':' + index var dependency = this.cache.get(cacheKey) if (!dependency) { switch (type) { case 'scene': dependency = this.loadScene(index) break case 'node': dependency = this.loadNode(index) break case 'mesh': dependency = this._invokeOne(function (ext) { return ext.loadMesh && ext.loadMesh(index) }) break case 'accessor': dependency = this.loadAccessor(index) break case 'bufferView': dependency = Promise.resolve(new Float32Array(0)) break case 'buffer': dependency = Promise.resolve(new Float32Array(0)) break case 'material': dependency = this._invokeOne(function (ext) { return ext.loadMaterial && ext.loadMaterial(index) }) break case 'skin': dependency = this.loadSkin(index) break case 'animation': dependency = this.loadAnimation(index) break case 'camera': dependency = this.loadCamera(index) break default: throw new Error('Unknown type: ' + type) } this.cache.add(cacheKey, dependency) } return dependency } /** * Requests all dependencies of the specified type asynchronously, with caching. * @param {string} type * @return {Promise>} */ GLTFParser.prototype.getDependencies = function (type) { var dependencies = this.cache.get(type) if (!dependencies) { var parser = this var defs = this.json[type + (type === 'mesh' ? 'es' : 's')] || [] dependencies = Promise.all( defs.map(function (def, index) { return parser.getDependency(type, index) }) ) this.cache.add(type, dependencies) } return dependencies } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views * @param {number} bufferIndex * @return {Promise} */ GLTFParser.prototype.loadBuffer = function (bufferIndex) { var bufferDef = this.json.buffers[bufferIndex] var loader = this.fileLoader if (bufferDef.type && bufferDef.type !== 'arraybuffer') { throw new Error('THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.') } // If present, GLB container is required to be the first buffer. if (bufferDef.uri === undefined && bufferIndex === 0) { return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body) } var options = this.options return new Promise(function (resolve, reject) { loader.load(resolveURL(bufferDef.uri, options.path), resolve, undefined, function () { reject(new Error('THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".')) }) }) } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views * @param {number} bufferViewIndex * @return {Promise} */ GLTFParser.prototype.loadBufferView = function (bufferViewIndex) { var bufferViewDef = this.json.bufferViews[bufferViewIndex] return this.getDependency('buffer', bufferViewDef.buffer).then(function (buffer) { var byteLength = bufferViewDef.byteLength || 0 var byteOffset = bufferViewDef.byteOffset || 0 return buffer.slice(byteOffset, byteOffset + byteLength) }) } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors * @param {number} accessorIndex * @return {Promise} */ GLTFParser.prototype.loadAccessor = function (accessorIndex) { var parser = this var json = this.json var accessorDef = this.json.accessors[accessorIndex] if (accessorDef.bufferView === undefined && accessorDef.sparse === undefined) { // Ignore empty accessors, which may be used to declare runtime // information about attributes coming from another source (e.g. Draco // compression extension). return Promise.resolve(null) } var pendingBufferViews = [] if (accessorDef.bufferView !== undefined) { pendingBufferViews.push(this.getDependency('bufferView', accessorDef.bufferView)) } else { pendingBufferViews.push(null) } if (accessorDef.sparse !== undefined) { pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.indices.bufferView)) pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.values.bufferView)) } return Promise.all(pendingBufferViews).then(function (bufferViews) { var bufferView = bufferViews[0] var itemSize = WEBGL_TYPE_SIZES[accessorDef.type] var TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType] // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. var elementBytes = TypedArray.BYTES_PER_ELEMENT var itemBytes = elementBytes * itemSize var byteOffset = accessorDef.byteOffset || 0 var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[accessorDef.bufferView].byteStride : undefined var normalized = accessorDef.normalized === true var array, bufferAttribute // The buffer is not interleaved if the stride is the item size in bytes. if (byteStride && byteStride !== itemBytes) { // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer // This makes sure that IBA.count reflects accessor.count properly var ibSlice = Math.floor(byteOffset / byteStride) var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count var ib = parser.cache.get(ibCacheKey) if (!ib) { array = new TypedArray(bufferView, ibSlice * byteStride, (accessorDef.count * byteStride) / elementBytes) // Integer parameters to IB/IBA are in array elements, not bytes. ib = new THREE.InterleavedBuffer(array, byteStride / elementBytes) parser.cache.add(ibCacheKey, ib) } bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, (byteOffset % byteStride) / elementBytes, normalized ) } else { if (bufferView === null) { array = new TypedArray(accessorDef.count * itemSize) } else { array = new TypedArray(bufferView, byteOffset, accessorDef.count * itemSize) } bufferAttribute = new THREE.BufferAttribute(array, itemSize, normalized) } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors if (accessorDef.sparse !== undefined) { var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR var TypedArrayIndices = WEBGL_COMPONENT_TYPES[accessorDef.sparse.indices.componentType] var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0 var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0 var sparseIndices = new TypedArrayIndices( bufferViews[1], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices ) var sparseValues = new TypedArray(bufferViews[2], byteOffsetValues, accessorDef.sparse.count * itemSize) if (bufferView !== null) { // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. bufferAttribute = new THREE.BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized ) } for (var i = 0, il = sparseIndices.length; i < il; i++) { var index = sparseIndices[i] bufferAttribute.setX(index, sparseValues[i * itemSize]) if (itemSize >= 2) bufferAttribute.setY(index, sparseValues[i * itemSize + 1]) if (itemSize >= 3) bufferAttribute.setZ(index, sparseValues[i * itemSize + 2]) if (itemSize >= 4) bufferAttribute.setW(index, sparseValues[i * itemSize + 3]) if (itemSize >= 5) throw new Error('THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.') } } if (bufferAttribute.isInterleavedBufferAttribute) { bufferAttribute.data.count = accessorDef.count } else { bufferAttribute.count = accessorDef.count } return bufferAttribute }) } /** * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures * @param {number} textureIndex * @return {Promise} */ GLTFParser.prototype.loadTexture = function (textureIndex) { return Promise.resolve(new THREE.Texture()) } /** * Assigns final material to a Mesh, Line, or Points instance. The instance * already has a material (generated from the glTF material options alone) * but reuse of the same glTF material may require multiple threejs materials * to accomodate different primitive types, defines, etc. New materials will * be created if necessary, and reused from a cache. * @param {THREE.Object3D} mesh Mesh, Line, or Points instance. */ GLTFParser.prototype.assignFinalMaterial = function (mesh) { var geometry = mesh.geometry var material = mesh.material var useVertexTangents = geometry.attributes.tangent !== undefined var useVertexColors = geometry.attributes.color !== undefined var useFlatShading = geometry.attributes.normal === undefined var useSkinning = mesh.isSkinnedMesh === true var useMorphTargets = Object.keys(geometry.morphAttributes).length > 0 var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined if (mesh.isPoints) { var cacheKey = 'PointsMaterial:' + material.uuid var pointsMaterial = this.cache.get(cacheKey) if (!pointsMaterial) { pointsMaterial = new THREE.PointsMaterial() THREE.Material.prototype.copy.call(pointsMaterial, material) pointsMaterial.color.copy(material.color) pointsMaterial.map = material.map pointsMaterial.sizeAttenuation = false // glTF spec says points should be 1px this.cache.add(cacheKey, pointsMaterial) } material = pointsMaterial } else if (mesh.isLine) { var cacheKey = 'LineBasicMaterial:' + material.uuid var lineMaterial = this.cache.get(cacheKey) if (!lineMaterial) { lineMaterial = new THREE.LineBasicMaterial() THREE.Material.prototype.copy.call(lineMaterial, material) lineMaterial.color.copy(material.color) this.cache.add(cacheKey, lineMaterial) } material = lineMaterial } // Clone the material if it will be modified if (useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets) { var cacheKey = 'ClonedMaterial:' + material.uuid + ':' if (material.isGLTFSpecularGlossinessMaterial) cacheKey += 'specular-glossiness:' if (useSkinning) cacheKey += 'skinning:' if (useVertexTangents) cacheKey += 'vertex-tangents:' if (useVertexColors) cacheKey += 'vertex-colors:' if (useFlatShading) cacheKey += 'flat-shading:' if (useMorphTargets) cacheKey += 'morph-targets:' if (useMorphNormals) cacheKey += 'morph-normals:' var cachedMaterial = this.cache.get(cacheKey) if (!cachedMaterial) { cachedMaterial = material.clone() if (useSkinning) cachedMaterial.skinning = true if (useVertexTangents) cachedMaterial.vertexTangents = true if (useVertexColors) cachedMaterial.vertexColors = true if (useFlatShading) cachedMaterial.flatShading = true if (useMorphTargets) cachedMaterial.morphTargets = true if (useMorphNormals) cachedMaterial.morphNormals = true this.cache.add(cacheKey, cachedMaterial) this.associations.set(cachedMaterial, this.associations.get(material)) } material = cachedMaterial } // workarounds for mesh and geometry if (material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined) { geometry.setAttribute('uv2', geometry.attributes.uv) } // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 if (material.normalScale && !useVertexTangents) { material.normalScale.y = -material.normalScale.y } if (material.clearcoatNormalScale && !useVertexTangents) { material.clearcoatNormalScale.y = -material.clearcoatNormalScale.y } mesh.material = material } GLTFParser.prototype.getMaterialType = function (/* materialIndex */) { return THREE.MeshStandardMaterial } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials * @param {number} materialIndex * @return {Promise} */ GLTFParser.prototype.loadMaterial = function (materialIndex) { var parser = this var json = this.json var extensions = this.extensions var materialDef = json.materials[materialIndex] var materialType var materialParams = {} var materialExtensions = materialDef.extensions || {} var pending = [] if (materialExtensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]) { var sgExtension = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS] materialType = sgExtension.getMaterialType() pending.push(sgExtension.extendParams(materialParams, materialDef, parser)) } else if (materialExtensions[EXTENSIONS.KHR_MATERIALS_UNLIT]) { var kmuExtension = extensions[EXTENSIONS.KHR_MATERIALS_UNLIT] materialType = kmuExtension.getMaterialType() pending.push(kmuExtension.extendParams(materialParams, materialDef, parser)) } else { // Specification: // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material var metallicRoughness = materialDef.pbrMetallicRoughness || {} materialParams.color = new THREE.Color(1.0, 1.0, 1.0) materialParams.opacity = 1.0 if (Array.isArray(metallicRoughness.baseColorFactor)) { var array = metallicRoughness.baseColorFactor materialParams.color.fromArray(array) materialParams.opacity = array[3] } materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0 materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0 materialType = this._invokeOne(function (ext) { return ext.getMaterialType && ext.getMaterialType(materialIndex) }) pending.push( Promise.all( this._invokeAll(function (ext) { return ext.extendMaterialParams && ext.extendMaterialParams(materialIndex, materialParams) }) ) ) } if (materialDef.doubleSided === true) { materialParams.side = THREE.DoubleSide } var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE if (alphaMode === ALPHA_MODES.BLEND) { materialParams.transparent = true // See: https://github.com/mrdoob/three.js/issues/17706 materialParams.depthWrite = false } else { materialParams.transparent = false if (alphaMode === ALPHA_MODES.MASK) { materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5 } } if (materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { materialParams.normalScale = new THREE.Vector2(1, 1) if (materialDef.normalTexture.scale !== undefined) { materialParams.normalScale.set(materialDef.normalTexture.scale, materialDef.normalTexture.scale) } } if (materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { if (materialDef.occlusionTexture.strength !== undefined) { materialParams.aoMapIntensity = materialDef.occlusionTexture.strength } } if (materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial) { materialParams.emissive = new THREE.Color().fromArray(materialDef.emissiveFactor) } return Promise.all(pending).then(function () { var material material = new materialType(materialParams) if (materialDef.name) material.name = materialDef.name // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. if (material.map) material.map.encoding = THREE.sRGBEncoding if (material.emissiveMap) material.emissiveMap.encoding = THREE.sRGBEncoding assignExtrasToUserData(material, materialDef) parser.associations.set(material, { type: 'materials', index: materialIndex }) if (materialDef.extensions) addUnknownExtensionsToUserData(extensions, material, materialDef) return material }) } /** When Object3D instances are targeted by animation, they need unique names. */ GLTFParser.prototype.createUniqueName = function (originalName) { var sanitizedName = THREE.PropertyBinding.sanitizeNodeName(originalName || '') var name = sanitizedName for (var i = 1; this.nodeNamesUsed[name]; ++i) { name = sanitizedName + '_' + i } this.nodeNamesUsed[name] = true return name } /** * @param {THREE.BufferGeometry} geometry * @param {GLTF.Primitive} primitiveDef * @param {GLTFParser} parser */ function computeBounds(geometry, primitiveDef, parser) { var attributes = primitiveDef.attributes var box = new THREE.Box3() if (attributes.POSITION !== undefined) { var accessor = parser.json.accessors[attributes.POSITION] var min = accessor.min var max = accessor.max // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. if (min !== undefined && max !== undefined) { box.set(new THREE.Vector3(min[0], min[1], min[2]), new THREE.Vector3(max[0], max[1], max[2])) } else { console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.') return } } else { return } var targets = primitiveDef.targets if (targets !== undefined) { var maxDisplacement = new THREE.Vector3() var vector = new THREE.Vector3() for (var i = 0, il = targets.length; i < il; i++) { var target = targets[i] if (target.POSITION !== undefined) { var accessor = parser.json.accessors[target.POSITION] var min = accessor.min var max = accessor.max // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. if (min !== undefined && max !== undefined) { // we need to get max of absolute components because target weight is [-1,1] vector.setX(Math.max(Math.abs(min[0]), Math.abs(max[0]))) vector.setY(Math.max(Math.abs(min[1]), Math.abs(max[1]))) vector.setZ(Math.max(Math.abs(min[2]), Math.abs(max[2]))) // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets // are used to implement key-frame animations and as such only two are active at a time - this results in very large // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. maxDisplacement.max(vector) } else { console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.') } } } // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. box.expandByVector(maxDisplacement) } geometry.boundingBox = box var sphere = new THREE.Sphere() box.getCenter(sphere.center) sphere.radius = box.min.distanceTo(box.max) / 2 geometry.boundingSphere = sphere } /** * @param {THREE.BufferGeometry} geometry * @param {GLTF.Primitive} primitiveDef * @param {GLTFParser} parser * @return {Promise} */ function addPrimitiveAttributes(geometry, primitiveDef, parser) { var attributes = primitiveDef.attributes var pending = [] function assignAttributeAccessor(accessorIndex, attributeName) { return parser.getDependency('accessor', accessorIndex).then(function (accessor) { geometry.setAttribute(attributeName, accessor) }) } for (var gltfAttributeName in attributes) { var threeAttributeName = ATTRIBUTES[gltfAttributeName] || gltfAttributeName.toLowerCase() // Skip attributes already provided by e.g. Draco extension. if (threeAttributeName in geometry.attributes) continue pending.push(assignAttributeAccessor(attributes[gltfAttributeName], threeAttributeName)) } if (primitiveDef.indices !== undefined && !geometry.index) { var accessor = parser.getDependency('accessor', primitiveDef.indices).then(function (accessor) { geometry.setIndex(accessor) }) pending.push(accessor) } assignExtrasToUserData(geometry, primitiveDef) computeBounds(geometry, primitiveDef, parser) return Promise.all(pending).then(function () { return primitiveDef.targets !== undefined ? addMorphTargets(geometry, primitiveDef.targets, parser) : geometry }) } /** * @param {THREE.BufferGeometry} geometry * @param {Number} drawMode * @return {THREE.BufferGeometry} */ function toTrianglesDrawMode(geometry, drawMode) { var index = geometry.getIndex() // generate index if not present if (index === null) { var indices = [] var position = geometry.getAttribute('position') if (position !== undefined) { for (var i = 0; i < position.count; i++) { indices.push(i) } geometry.setIndex(indices) index = geometry.getIndex() } else { console.error('THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.') return geometry } } // var numberOfTriangles = index.count - 2 var newIndices = [] if (drawMode === THREE.TriangleFanDrawMode) { // gl.TRIANGLE_FAN for (var i = 1; i <= numberOfTriangles; i++) { newIndices.push(index.getX(0)) newIndices.push(index.getX(i)) newIndices.push(index.getX(i + 1)) } } else { // gl.TRIANGLE_STRIP for (var i = 0; i < numberOfTriangles; i++) { if (i % 2 === 0) { newIndices.push(index.getX(i)) newIndices.push(index.getX(i + 1)) newIndices.push(index.getX(i + 2)) } else { newIndices.push(index.getX(i + 2)) newIndices.push(index.getX(i + 1)) newIndices.push(index.getX(i)) } } } if (newIndices.length / 3 !== numberOfTriangles) { console.error('THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.') } // build final geometry var newGeometry = geometry.clone() newGeometry.setIndex(newIndices) return newGeometry } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry * * Creates BufferGeometries from primitives. * * @param {Array} primitives * @return {Promise>} */ GLTFParser.prototype.loadGeometries = function (primitives) { var parser = this var extensions = this.extensions var cache = this.primitiveCache function createDracoPrimitive(primitive) { return extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION] .decodePrimitive(primitive, parser) .then(function (geometry) { return addPrimitiveAttributes(geometry, primitive, parser) }) } var pending = [] for (var i = 0, il = primitives.length; i < il; i++) { var primitive = primitives[i] var cacheKey = createPrimitiveKey(primitive) // See if we've already created this geometry var cached = cache[cacheKey] if (cached) { // Use the cached geometry if it exists pending.push(cached.promise) } else { var geometryPromise if (primitive.extensions && primitive.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]) { // Use DRACO geometry if available geometryPromise = createDracoPrimitive(primitive) } else { // Otherwise create a new geometry geometryPromise = addPrimitiveAttributes(new THREE.BufferGeometry(), primitive, parser) } // Cache this geometry cache[cacheKey] = { primitive: primitive, promise: geometryPromise } pending.push(geometryPromise) } } return Promise.all(pending) } /** * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes * @param {number} meshIndex * @return {Promise} */ GLTFParser.prototype.loadMesh = function (meshIndex) { var parser = this var json = this.json var extensions = this.extensions var meshDef = json.meshes[meshIndex] var primitives = meshDef.primitives var pending = [] for (var i = 0, il = primitives.length; i < il; i++) { var material = primitives[i].material === undefined ? createDefaultMaterial(this.cache) : this.getDependency('material', primitives[i].material) pending.push(material) } pending.push(parser.loadGeometries(primitives)) return Promise.all(pending).then(function (results) { var materials = results.slice(0, results.length - 1) var geometries = results[results.length - 1] var meshes = [] for (var i = 0, il = geometries.length; i < il; i++) { var geometry = geometries[i] var primitive = primitives[i] // 1. create Mesh var mesh var material = materials[i] if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || primitive.mode === undefined ) { // .isSkinnedMesh isn't in glTF spec. See ._markDefs() if (geometry.morphAttributes && Object.keys(geometry.morphAttributes).length > 0) { meshDef.hasMorphAttributes = true } geometry.morphAttributes = {} mesh = meshDef.isSkinnedMesh === true ? new THREE.SkinnedMesh(geometry, material) : new THREE.Mesh(geometry, material) if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP) { mesh.geometry = toTrianglesDrawMode(mesh.geometry, THREE.TriangleStripDrawMode) } else if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN) { mesh.geometry = toTrianglesDrawMode(mesh.geometry, THREE.TriangleFanDrawMode) } } else if (primitive.mode === WEBGL_CONSTANTS.LINES) { mesh = new THREE.LineSegments(geometry, material) } else if (primitive.mode === WEBGL_CONSTANTS.LINE_STRIP) { mesh = new THREE.Line(geometry, material) } else if (primitive.mode === WEBGL_CONSTANTS.LINE_LOOP) { mesh = new THREE.LineLoop(geometry, material) } else if (primitive.mode === WEBGL_CONSTANTS.POINTS) { mesh = new THREE.Points(geometry, material) } else { throw new Error('THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode) } if (meshDef.hasMorphAttributes) { // Just flag the mesh, so that parser.js can link morphTarget dictionaries and influences // This prevented a crash relating to morphTarget definitions mesh.morphTargetDictionary = true mesh.morphTargetInfluences = true } mesh.name = parser.createUniqueName(meshDef.name || 'mesh_' + meshIndex) assignExtrasToUserData(mesh, meshDef) if (primitive.extensions) addUnknownExtensionsToUserData(extensions, mesh, primitive) parser.assignFinalMaterial(mesh) meshes.push(mesh) } if (meshes.length === 1) { return meshes[0] } var group = new THREE.Group() for (var i = 0, il = meshes.length; i < il; i++) { group.add(meshes[i]) } return group }) } /** * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras * @param {number} cameraIndex * @return {Promise} */ GLTFParser.prototype.loadCamera = function (cameraIndex) { var camera var cameraDef = this.json.cameras[cameraIndex] var params = cameraDef[cameraDef.type] if (!params) { console.warn('THREE.GLTFLoader: Missing camera parameters.') return } if (cameraDef.type === 'perspective') { camera = new THREE.PerspectiveCamera( THREE.MathUtils.radToDeg(params.yfov), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ) } else if (cameraDef.type === 'orthographic') { camera = new THREE.OrthographicCamera( -params.xmag, params.xmag, params.ymag, -params.ymag, params.znear, params.zfar ) } if (cameraDef.name) camera.name = this.createUniqueName(cameraDef.name) assignExtrasToUserData(camera, cameraDef) return Promise.resolve(camera) } /** * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins * @param {number} skinIndex * @return {Promise} */ GLTFParser.prototype.loadSkin = function (skinIndex) { var skinDef = this.json.skins[skinIndex] var skinEntry = { joints: skinDef.joints } if (skinDef.inverseBindMatrices === undefined) { return Promise.resolve(skinEntry) } return this.getDependency('accessor', skinDef.inverseBindMatrices).then(function (accessor) { skinEntry.inverseBindMatrices = accessor return skinEntry }) } /** * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations * @param {number} animationIndex * @return {Promise} */ GLTFParser.prototype.loadAnimation = function (animationIndex) { var json = this.json var animationDef = json.animations[animationIndex] var pendingNodes = [] var pendingInputAccessors = [] var pendingOutputAccessors = [] var pendingSamplers = [] var pendingTargets = [] for (var i = 0, il = animationDef.channels.length; i < il; i++) { var channel = animationDef.channels[i] var sampler = animationDef.samplers[channel.sampler] var target = channel.target var name = target.node !== undefined ? target.node : target.id // NOTE: target.id is deprecated. var input = animationDef.parameters !== undefined ? animationDef.parameters[sampler.input] : sampler.input var output = animationDef.parameters !== undefined ? animationDef.parameters[sampler.output] : sampler.output pendingNodes.push(this.getDependency('node', name)) pendingInputAccessors.push(this.getDependency('accessor', input)) pendingOutputAccessors.push(this.getDependency('accessor', output)) pendingSamplers.push(sampler) pendingTargets.push(target) } return Promise.all([ Promise.all(pendingNodes), Promise.all(pendingInputAccessors), Promise.all(pendingOutputAccessors), Promise.all(pendingSamplers), Promise.all(pendingTargets), ]).then(function (dependencies) { var nodes = dependencies[0] var inputAccessors = dependencies[1] var outputAccessors = dependencies[2] var samplers = dependencies[3] var targets = dependencies[4] var tracks = [] for (var i = 0, il = nodes.length; i < il; i++) { var node = nodes[i] var inputAccessor = inputAccessors[i] var outputAccessor = outputAccessors[i] var sampler = samplers[i] var target = targets[i] if (node === undefined) continue node.updateMatrix() node.matrixAutoUpdate = true var TypedKeyframeTrack switch (PATH_PROPERTIES[target.path]) { case PATH_PROPERTIES.weights: TypedKeyframeTrack = THREE.NumberKeyframeTrack break case PATH_PROPERTIES.rotation: TypedKeyframeTrack = THREE.QuaternionKeyframeTrack break case PATH_PROPERTIES.position: case PATH_PROPERTIES.scale: default: TypedKeyframeTrack = THREE.VectorKeyframeTrack break } var targetName = node.name ? node.name : node.uuid var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[sampler.interpolation] : THREE.InterpolateLinear var targetNames = [] if (PATH_PROPERTIES[target.path] === PATH_PROPERTIES.weights) { // Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh. node.traverse(function (object) { if (object.isMesh === true && object.morphTargetInfluences) { targetNames.push(object.name ? object.name : object.uuid) } }) } else { targetNames.push(targetName) } if (outputAccessor === null) continue var outputArray = outputAccessor.array if (outputAccessor.normalized) { var scale if (outputArray.constructor === Int8Array) { scale = 1 / 127 } else if (outputArray.constructor === Uint8Array) { scale = 1 / 255 } else if (outputArray.constructor == Int16Array) { scale = 1 / 32767 } else if (outputArray.constructor === Uint16Array) { scale = 1 / 65535 } else { throw new Error('THREE.GLTFLoader: Unsupported output accessor component type.') } var scaled = new Float32Array(outputArray.length) for (var j = 0, jl = outputArray.length; j < jl; j++) { scaled[j] = outputArray[j] * scale } outputArray = scaled } } var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex var clip = new THREE.AnimationClip(name, undefined, tracks) clip.targetNames = targetNames return clip }) } GLTFParser.prototype.createNodeMesh = function (nodeIndex) { const json = this.json const parser = this const nodeDef = json.nodes[nodeIndex] if (nodeDef.mesh === undefined) return null return parser.getDependency('mesh', nodeDef.mesh).then(function (mesh) { const node = parser._getNodeRef(parser.meshCache, nodeDef.mesh, mesh) // if weights are provided on the node, override weights on the mesh. if (nodeDef.weights !== undefined) { node.traverse(function (o) { if (!o.isMesh) return for (let i = 0, il = nodeDef.weights.length; i < il; i++) { o.morphTargetInfluences[i] = nodeDef.weights[i] } }) } return node }) } /** * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy * @param {number} nodeIndex * @return {Promise} */ GLTFParser.prototype.loadNode = function (nodeIndex) { var json = this.json var extensions = this.extensions var parser = this var nodeDef = json.nodes[nodeIndex] // reserve node's name before its dependencies, so the root has the intended name. var nodeName = nodeDef.name ? parser.createUniqueName(nodeDef.name) : '' return (function () { var pending = [] const meshPromise = parser._invokeOne(function (ext) { return ext.createNodeMesh && ext.createNodeMesh(nodeIndex) }) if (meshPromise) { pending.push(meshPromise) } if (nodeDef.camera !== undefined) { pending.push( parser.getDependency('camera', nodeDef.camera).then(function (camera) { return parser._getNodeRef(parser.cameraCache, nodeDef.camera, camera) }) ) } parser ._invokeAll(function (ext) { return ext.createNodeAttachment && ext.createNodeAttachment(nodeIndex) }) .forEach(function (promise) { pending.push(promise) }) return Promise.all(pending) })().then(function (objects) { var node // .isBone isn't in glTF spec. See ._markDefs if (nodeDef.isBone === true) { node = new THREE.Bone() } else if (objects.length > 1) { node = new THREE.Group() } else if (objects.length === 1) { node = objects[0] } else { node = new THREE.Object3D() } if (node !== objects[0]) { for (var i = 0, il = objects.length; i < il; i++) { node.add(objects[i]) } } if (nodeDef.name) { node.userData.name = nodeDef.name node.name = nodeName } assignExtrasToUserData(node, nodeDef) if (nodeDef.extensions) addUnknownExtensionsToUserData(extensions, node, nodeDef) if (nodeDef.matrix !== undefined) { var matrix = new THREE.Matrix4() matrix.fromArray(nodeDef.matrix) node.applyMatrix4(matrix) } else { if (nodeDef.translation !== undefined) { node.position.fromArray(nodeDef.translation) } if (nodeDef.rotation !== undefined) { node.quaternion.fromArray(nodeDef.rotation) } if (nodeDef.scale !== undefined) { node.scale.fromArray(nodeDef.scale) } } parser.associations.set(node, { type: 'nodes', index: nodeIndex }) return node }) } /** * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes * @param {number} sceneIndex * @return {Promise} */ GLTFParser.prototype.loadScene = (function () { // scene node hierachy builder function buildNodeHierachy(nodeId, parentObject, json, parser) { var nodeDef = json.nodes[nodeId] return parser .getDependency('node', nodeId) .then(function (node) { if (nodeDef.skin === undefined) return node // build skeleton here as well var skinEntry return parser .getDependency('skin', nodeDef.skin) .then(function (skin) { skinEntry = skin var pendingJoints = [] for (var i = 0, il = skinEntry.joints.length; i < il; i++) { pendingJoints.push(parser.getDependency('node', skinEntry.joints[i])) } return Promise.all(pendingJoints) }) .then(function (jointNodes) { node.traverse(function (mesh) { if (!mesh.isMesh) return var bones = [] var boneInverses = [] for (var j = 0, jl = jointNodes.length; j < jl; j++) { var jointNode = jointNodes[j] if (jointNode) { bones.push(jointNode) var mat = new THREE.Matrix4() if (skinEntry.inverseBindMatrices !== undefined) { mat.fromArray(skinEntry.inverseBindMatrices.array, j * 16) } boneInverses.push(mat) } else { console.warn('THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[j]) } } mesh.bind(new THREE.Skeleton(bones, boneInverses), mesh.matrixWorld) }) return node }) }) .then(function (node) { // build node hierachy parentObject.add(node) var pending = [] if (nodeDef.children) { var children = nodeDef.children for (var i = 0, il = children.length; i < il; i++) { var child = children[i] pending.push(buildNodeHierachy(child, node, json, parser)) } } return Promise.all(pending) }) } return function loadScene(sceneIndex) { var json = this.json var extensions = this.extensions var sceneDef = this.json.scenes[sceneIndex] var parser = this // Loader returns Group, not Scene. // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 var scene = new THREE.Group() if (sceneDef.name) scene.name = parser.createUniqueName(sceneDef.name) assignExtrasToUserData(scene, sceneDef) if (sceneDef.extensions) addUnknownExtensionsToUserData(extensions, scene, sceneDef) var nodeIds = sceneDef.nodes || [] var pending = [] for (var i = 0, il = nodeIds.length; i < il; i++) { pending.push(buildNodeHierachy(nodeIds[i], scene, json, parser)) } return Promise.all(pending).then(function () { return scene }) } })() ================================================ FILE: src/gltfjsx.js ================================================ import 'jsdom-global' import fs from 'fs' import path from 'path' import transform from './utils/transform.js' import { GLTFLoader } from './bin/GLTFLoader.js' import { DRACOLoader } from './bin/DRACOLoader.js' DRACOLoader.getDecoderModule = () => {} import parse from './utils/parser.js' const gltfLoader = new GLTFLoader() gltfLoader.setDRACOLoader(new DRACOLoader()) function toArrayBuffer(buf) { var ab = new ArrayBuffer(buf.length) var view = new Uint8Array(ab) for (var i = 0; i < buf.length; ++i) view[i] = buf[i] return ab } function roundOff(value) { return Math.round(value * 100) / 100 } function getFileSize(file) { const stats = fs.statSync(file) let fileSize = stats.size let fileSizeKB = roundOff(fileSize * 0.001) let fileSizeMB = roundOff(fileSizeKB * 0.001) return { size: fileSizeKB > 1000 ? `${fileSizeMB}MB` : `${fileSizeKB}KB`, sizeKB: fileSizeKB, } } export default function (file, output, options) { function getRelativeFilePath(file) { const filePath = path.resolve(file) const rootPath = options.root ? path.resolve(options.root) : path.dirname(file) const relativePath = path.relative(rootPath, filePath) || '' if (process.platform === 'win32') return relativePath.replace(/\\/g, '/') return relativePath } return new Promise((resolve, reject) => { async function run(stream) { let size = '' // Process GLTF if (output && path.parse(output).ext === '.tsx') options.types = true if (options.transform || options.instance || options.instanceall) { const { name } = path.parse(file) const outputDir = path.parse(path.resolve(output ?? file)).dir const transformOut = path.join(outputDir, name + '-transformed.glb') await transform(file, transformOut, options) const { size: sizeOriginal, sizeKB: sizeKBOriginal } = getFileSize(file) const { size: sizeTransformed, sizeKB: sizeKBTransformed } = getFileSize(transformOut) size = `${file} [${sizeOriginal}] > ${transformOut} [${sizeTransformed}] (${Math.round( 100 - (sizeKBTransformed / sizeKBOriginal) * 100 )}%)` file = transformOut } const filePath = getRelativeFilePath(file) const data = fs.readFileSync(file) const arrayBuffer = toArrayBuffer(data) gltfLoader.parse( arrayBuffer, '', async (gltf) => { const output = await parse(gltf, { fileName: filePath, size, ...options }) if (options.console) console.log(output) else stream?.write(output) stream?.end() resolve() }, (reason) => console.log(reason) ) } if (options.console) { run() } else { const stream = fs.createWriteStream(path.resolve(output)) stream.once('open', async () => { if (!fs.existsSync(file)) reject(file + ' does not exist.') else run(stream) }) } }) } ================================================ FILE: src/test.js ================================================ import assert from 'node:assert' const cli = await import('../cli.js') assert(cli) ================================================ FILE: src/utils/exports.js ================================================ import parse from './parser.js' import { GLTFLoader as GLTFStructureLoader } from '../bin/GLTFLoader.js' export { parse, GLTFStructureLoader } ================================================ FILE: src/utils/isVarName.js ================================================ const isVarName = (str) => { // eslint-disable-next-line no-misleading-character-class const regex = new RegExp( // eslint-disable-next-line no-misleading-character-class /^(?!(?: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])*$/ ) return regex.test(str) } export default isVarName ================================================ FILE: src/utils/parser.js ================================================ import * as THREE from 'three' import * as prettier from 'prettier' import babelParser from 'prettier/parser-babel.js' import isVarName from './isVarName.js' function parse(gltf, { fileName = 'model', ...options } = {}) { if (gltf.isObject3D) { // Wrap scene in a GLTF Structure gltf = { scene: gltf, animations: [], parser: { json: {} } } } const url = (fileName.toLowerCase().startsWith('http') ? '' : '/') + fileName const animations = gltf.animations const hasAnimations = animations.length > 0 // Collect all objects const objects = [] gltf.scene.traverse((child) => objects.push(child)) // Browse for duplicates const duplicates = { names: {}, materials: {}, geometries: {}, } function uniqueName(attempt, index = 0) { const newAttempt = index > 0 ? attempt + index : attempt if (Object.values(duplicates.geometries).find(({ name }) => name === newAttempt) === undefined) return newAttempt else return uniqueName(attempt, index + 1) } gltf.scene.traverse((child) => { if (child.isMesh) { if (child.material) { if (!duplicates.materials[child.material.name]) { duplicates.materials[child.material.name] = 1 } else { duplicates.materials[child.material.name]++ } } } }) gltf.scene.traverse((child) => { if (child.isMesh) { if (child.geometry) { const key = child.geometry.uuid + child.material?.name ?? '' if (!duplicates.geometries[key]) { let name = (child.name || 'Part').replace(/[^a-zA-Z]/g, '') name = name.charAt(0).toUpperCase() + name.slice(1) duplicates.geometries[key] = { count: 1, name: uniqueName(name), node: 'nodes' + sanitizeName(child.name), } } else { duplicates.geometries[key].count++ } } } }) // Prune duplicate geometries if (!options.instanceall) { for (let key of Object.keys(duplicates.geometries)) { const duplicate = duplicates.geometries[key] if (duplicate.count === 1) delete duplicates.geometries[key] } } const hasInstances = (options.instance || options.instanceall) && Object.keys(duplicates.geometries).length > 0 function sanitizeName(name) { return isVarName(name) ? `.${name}` : `['${name}']` } const rNbr = (number) => { return parseFloat(number.toFixed(Math.round(options.precision || 2))) } const rDeg = (number) => { const abs = Math.abs(Math.round(parseFloat(number) * 100000)) for (let i = 1; i <= 10; i++) { if (abs === Math.round(parseFloat(Math.PI / i) * 100000)) return `${number < 0 ? '-' : ''}Math.PI${i > 1 ? ' / ' + i : ''}` } for (let i = 1; i <= 10; i++) { if (abs === Math.round(parseFloat(Math.PI * i) * 100000)) return `${number < 0 ? '-' : ''}Math.PI${i > 1 ? ' * ' + i : ''}` } return rNbr(number) } function printTypes(objects, animations) { let meshes = objects.filter((o) => o.isMesh && o.__removed === undefined) let bones = objects.filter((o) => o.isBone && !(o.parent && o.parent.isBone) && o.__removed === undefined) let materials = [...new Set(objects.filter((o) => o.material && o.material.name).map((o) => o.material))] let animationTypes = '' if (animations.length) { animationTypes = `\n type ActionName = ${animations.map((clip, i) => `"${clip.name}"`).join(' | ')}; interface GLTFAction extends THREE.AnimationClip { name: ActionName }\n` } const types = [...new Set([...meshes, ...bones].map((o) => getType(o)))] const contextType = hasInstances ? `\ntype ContextType = Record `JSX.IntrinsicElements['${type}']`).join(' | ')} >>\n` : '' return `\n${animationTypes}\ntype GLTFResult = GLTF & { nodes: { ${meshes.map(({ name, type }) => (isVarName(name) ? name : `['${name}']`) + ': THREE.' + type).join(',')} ${bones.map(({ name, type }) => (isVarName(name) ? name : `['${name}']`) + ': THREE.' + type).join(',')} } materials: { ${materials.map(({ name, type }) => (isVarName(name) ? name : `['${name}']`) + ': THREE.' + type).join(',')} } animations: GLTFAction[] }\n${contextType}` } function getType(obj) { let type = obj.type.charAt(0).toLowerCase() + obj.type.slice(1) // Turn object3d's into groups, it should be faster according to the threejs docs if (type === 'object3D') type = 'group' if (type === 'perspectiveCamera') type = 'PerspectiveCamera' if (type === 'orthographicCamera') type = 'OrthographicCamera' return type } function handleProps(obj) { let { type, node, instanced } = getInfo(obj) let result = '' let isCamera = type === 'PerspectiveCamera' || type === 'OrthographicCamera' // Handle cameras if (isCamera) { result += `makeDefault={false} ` if (obj.zoom !== 1) result += `zoom={${rNbr(obj.zoom)}} ` if (obj.far !== 2000) result += `far={${rNbr(obj.far)}} ` if (obj.near !== 0.1) result += `near={${rNbr(obj.near)}} ` } if (type === 'PerspectiveCamera') { if (obj.fov !== 50) result += `fov={${rNbr(obj.fov)}} ` } if (!instanced) { // Shadows if (type === 'mesh' && options.shadows) result += `castShadow receiveShadow ` // Write out geometry first if (obj.geometry && !obj.isInstancedMesh) { result += `geometry={${node}.geometry} ` } // Write out materials if (obj.material && !obj.isInstancedMesh) { if (obj.material.name) result += `material={materials${sanitizeName(obj.material.name)}} ` else result += `material={${node}.material} ` } if (obj.instanceMatrix) result += `instanceMatrix={${node}.instanceMatrix} ` if (obj.instanceColor) result += `instanceColor={${node}.instanceColor} ` if (obj.skeleton) result += `skeleton={${node}.skeleton} ` if (obj.visible === false) result += `visible={false} ` if (obj.castShadow === true) result += `castShadow ` if (obj.receiveShadow === true) result += `receiveShadow ` if (obj.morphTargetDictionary) result += `morphTargetDictionary={${node}.morphTargetDictionary} ` if (obj.morphTargetInfluences) result += `morphTargetInfluences={${node}.morphTargetInfluences} ` if (obj.intensity && rNbr(obj.intensity)) result += `intensity={${rNbr(obj.intensity)}} ` //if (obj.power && obj.power !== 4 * Math.PI) result += `power={${rNbr(obj.power)}} ` if (obj.angle && obj.angle !== Math.PI / 3) result += `angle={${rDeg(obj.angle)}} ` if (obj.penumbra && rNbr(obj.penumbra) !== 0) result += `penumbra={${rNbr(obj.penumbra)}} ` if (obj.decay && rNbr(obj.decay) !== 1) result += `decay={${rNbr(obj.decay)}} ` if (obj.distance && rNbr(obj.distance) !== 0) result += `distance={${rNbr(obj.distance)}} ` if (obj.up && obj.up.isVector3 && !obj.up.equals(new THREE.Vector3(0, 1, 0))) result += `up={[${rNbr(obj.up.x)}, ${rNbr(obj.up.y)}, ${rNbr(obj.up.z)},]} ` } if (obj.color && obj.color.getHexString() !== 'ffffff') result += `color="#${obj.color.getHexString()}" ` if (obj.position && obj.position.isVector3 && rNbr(obj.position.length())) result += `position={[${rNbr(obj.position.x)}, ${rNbr(obj.position.y)}, ${rNbr(obj.position.z)},]} ` if ( obj.rotation && obj.rotation.isEuler && rNbr(new THREE.Vector3(obj.rotation.x, obj.rotation.y, obj.rotation.z).length()) ) result += `rotation={[${rDeg(obj.rotation.x)}, ${rDeg(obj.rotation.y)}, ${rDeg(obj.rotation.z)},]} ` if ( obj.scale && obj.scale.isVector3 && !(rNbr(obj.scale.x) === 1 && rNbr(obj.scale.y) === 1 && rNbr(obj.scale.z) === 1) ) { const rX = rNbr(obj.scale.x) const rY = rNbr(obj.scale.y) const rZ = rNbr(obj.scale.z) if (rX === rY && rX === rZ) { result += `scale={${rX}} ` } else { result += `scale={[${rX}, ${rY}, ${rZ},]} ` } } if (options.meta && obj.userData && Object.keys(obj.userData).length) result += `userData={${JSON.stringify(obj.userData)}} ` return result } function getInfo(obj) { let type = getType(obj) let node = 'nodes' + sanitizeName(obj.name) let instanced = (options.instance || options.instanceall) && obj.geometry && obj.material && duplicates.geometries[obj.geometry.uuid + obj.material.name] && duplicates.geometries[obj.geometry.uuid + obj.material.name].count > (options.instanceall ? 0 : 1) let animated = gltf.animations && gltf.animations.length > 0 return { type, node, instanced, animated } } function equalOrNegated(a, b) { 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) } function prune(obj, children, result, oldResult, silent) { let { type, animated } = getInfo(obj) // Prune ... if (!obj.__removed && !options.keepgroups && !animated && (type === 'group' || type === 'scene')) { /** Empty or no-property groups * * * Solution: * */ if (result === oldResult || obj.children.length === 0) { if (options.debug && !silent) console.log(`group ${obj.name} removed (empty)`) obj.__removed = true return children } // More aggressive removal strategies ... const first = obj.children[0] const firstProps = handleProps(first) const regex = /([a-z-A-Z]*)={([a-zA-Z0-9\.\[\]\-\,\ \/]*)}/g const keys1 = [...result.matchAll(regex)].map(([, match]) => match) const values1 = [...result.matchAll(regex)].map(([, , match]) => match) const keys2 = [...firstProps.matchAll(regex)].map(([, match]) => match) /** Double negative rotations * * * * Solution: * */ if (obj.children.length === 1 && getType(first) === type && equalOrNegated(obj.rotation, first.rotation)) { if (keys1.length === 1 && keys2.length === 1 && keys1[0] === 'rotation' && keys2[0] === 'rotation') { if (options.debug && !silent) console.log(`group ${obj.name} removed (aggressive: double negative rotation)`) obj.__removed = first.__removed = true children = '' if (first.children) first.children.forEach((child) => (children += print(child, true))) return children } } /** Double negative rotations w/ props * * * * Solution: * * */ if (obj.children.length === 1 && getType(first) === type && equalOrNegated(obj.rotation, first.rotation)) { if (keys1.length === 1 && keys2.length > 1 && keys1[0] === 'rotation' && keys2.includes('rotation')) { if (options.debug && !silent) console.log(`group ${obj.name} removed (aggressive: double negative rotation w/ props)`) obj.__removed = true // Remove rotation from first child first.rotation.set(0, 0, 0) children = print(first, true) return children } } /** Transform overlap * * * Solution: * */ const isChildTransformed = keys2.includes('position') || keys2.includes('rotation') || keys2.includes('scale') const hasOtherProps = keys1.some((key) => !['position', 'scale', 'rotation'].includes(key)) if (obj.children.length === 1 && !first.__removed && !isChildTransformed && !hasOtherProps) { if (options.debug && !silent) console.log(`group ${obj.name} removed (aggressive: ${keys1.join(' ')} overlap)`) // Move props over from the to-be-deleted object to the child // This ensures that the child will have the correct transform when pruning is being repeated keys1.forEach((key) => obj.children[0][key].copy(obj[key])) // Insert the props into the result string children = print(first, true) obj.__removed = true return children } /** Lack of content * * * * Solution: * (delete the whole sub graph) */ const empty = [] obj.traverse((o) => { const type = getType(o) if (type !== 'group' && type !== 'object3D') empty.push(o) }) if (!empty.length) { if (options.debug && !silent) console.log(`group ${obj.name} removed (aggressive: lack of content)`) empty.forEach((child) => (child.__removed = true)) return '' } } } function print(obj, silent = false) { let result = '' let children = '' let { type, node, instanced, animated } = getInfo(obj) // Check if the root node is useless if (obj.__removed && obj.children.length) { obj.children.forEach((child) => (result += print(child))) return result } // Bail out on bones if (!options.bones && type === 'bone') { return `` } // Take care of lights with targets if (type.endsWith('Light') && obj.target && obj.children[0] === obj.target) { return `<${type} ${handleProps(obj)} target={${node}.target}> ` } // Collect children if (obj.children) obj.children.forEach((child) => (children += print(child))) if (instanced) { result = `' : '/>'}\n` // Add children and return if (children.length) { if (type === 'bone') result += children + `` else result += children + `` } return result } function printAnimations(animations) { return animations.length ? `\nconst { actions } = useAnimations(animations, group)` : '' } function parseExtras(extras) { if (extras) { return ( Object.keys(extras) .map((key) => `${key.charAt(0).toUpperCase() + key.slice(1)}: ${extras[key]}`) .join('\n') + '\n' ) } else return '' } function p(obj, line) { console.log( [...new Array(line * 2)].map(() => ' ').join(''), obj.type, obj.name, 'pos:', obj.position.toArray().map(rNbr), 'scale:', obj.scale.toArray().map(rNbr), 'rot:', [obj.rotation.x, obj.rotation.y, obj.rotation.z].map(rNbr), 'mat:', obj.material ? `${obj.material.name}-${obj.material.uuid.substring(0, 8)}` : '' ) obj.children.forEach((o) => p(o, line + 1)) } if (options.debug) p(gltf.scene, 0) let scene try { if (!options.keepgroups) { // Dry run to prune graph print(gltf.scene) // Move children of deleted objects to their new parents objects.forEach((o) => { if (o.__removed) { let parent = o.parent // Making sure we don't add to a removed parent while (parent && parent.__removed) parent = parent.parent // If no parent was found it must be the root node if (!parent) parent = gltf.scene o.children.slice().forEach((child) => parent.add(child)) } }) // Remove deleted objects objects.forEach((o) => { if (o.__removed && o.parent) o.parent.remove(o) }) } // 2nd pass to eliminate hard to swat left-overs scene = print(gltf.scene) } catch (e) { console.log('Error while parsing glTF', e) } const header = `/* ${options.header ? options.header : 'Auto-generated by: https://github.com/pmndrs/gltfjsx'} ${ options.size ? `\nFiles: ${options.size}` : '' } ${parseExtras(gltf.parser.json.asset && gltf.parser.json.asset.extras)}*/` const hasPrimitives = scene.includes(' ({ ${Object.values(duplicates.geometries) .map((v) => `${v.name}: ${v.node}`) .join(', ')} }), [nodes]) return ( {(instances${ options.types ? ': ContextType' : '' }) => } ) } ` : '' } export ${options.exportdefault ? 'default' : ''} function Model(props${ options.types ? ": JSX.IntrinsicElements['group']" : '' }) { ${hasInstances ? 'const instances = React.useContext(context);' : ''} ${ hasAnimations ? `const group = ${options.types ? 'React.useRef()' : 'React.useRef()'};` : '' } ${ !options.instanceall ? `const { ${!hasPrimitives ? `nodes, materials` : 'scene'} ${hasAnimations ? ', animations' : ''}} = useGLTF('${url}'${ options.draco ? `, ${JSON.stringify(options.draco)}` : '' })${!hasPrimitives && options.types ? ' as GLTFResult' : ''}${ hasPrimitives ? `\nconst clone = React.useMemo(() => SkeletonUtils.clone(scene), [scene]) const { nodes, materials } = useGraph(clone) ${options.types ? ' as GLTFResult' : ''}` : '' }` : '' } ${printAnimations(animations)} return ( ${scene} ) } useGLTF.preload('${url}')` if (!options.console) console.log(header) const output = header + '\n' + result const formatted = prettier.format(output, { semi: false, printWidth: options.printwidth || 1000, singleQuote: true, jsxBracketSameLine: true, parser: 'babel-ts', plugins: [babelParser], }) return formatted } export default parse ================================================ FILE: src/utils/transform.js ================================================ import { Logger, NodeIO } from '@gltf-transform/core' import { simplify, instance, flatten, dequantize, reorder, join, weld, sparse, dedup, resample, prune, textureCompress, draco, palette, unpartition, } from '@gltf-transform/functions' import { ALL_EXTENSIONS } from '@gltf-transform/extensions' import { MeshoptDecoder, MeshoptEncoder, MeshoptSimplifier } from 'meshoptimizer' import { ready as resampleReady, resample as resampleWASM } from 'keyframe-resample' import draco3d from 'draco3dgltf' import sharp from 'sharp' async function transform(file, output, config = {}) { await MeshoptDecoder.ready await MeshoptEncoder.ready const io = new NodeIO().registerExtensions(ALL_EXTENSIONS).registerDependencies({ 'draco3d.decoder': await draco3d.createDecoderModule(), 'draco3d.encoder': await draco3d.createEncoderModule(), 'meshopt.decoder': MeshoptDecoder, 'meshopt.encoder': MeshoptEncoder, }) if (config.console) io.setLogger(new Logger(Logger.Verbosity.ERROR)) else io.setLogger(new Logger(Logger.Verbosity.WARN)) const document = await io.read(file) const resolution = config.resolution ?? 1024 const normalResolution = Math.max(resolution, 2048) const degradeResolution = config.degraderesolution ?? 512 const functions = [unpartition()] if (!config.keepmaterials) functions.push(palette({ min: 5 })) functions.push( reorder({ encoder: MeshoptEncoder }), dedup(), // This seems problematic ... // instance({ min: 5 }), flatten(), dequantize() // ... ) if (!config.keepmeshes) { functions.push( join() // ... ) } functions.push( // Weld vertices weld(), ) if (config.simplify) { functions.push( // Simplify meshes simplify({ simplifier: MeshoptSimplifier, ratio: config.ratio ?? 0, error: config.error ?? 0.0001 }) ) } functions.push( resample({ ready: resampleReady, resample: resampleWASM }), prune({ keepAttributes: false, keepLeaves: false }), sparse() ) if (config.degrade) { // Custom per-file resolution functions.push( textureCompress({ encoder: sharp, pattern: new RegExp(`^(?=${config.degrade}).*$`), targetFormat: config.format, resize: [degradeResolution, degradeResolution], }), textureCompress({ encoder: sharp, pattern: new RegExp(`^(?!${config.degrade}).*$`), targetFormat: config.format, resize: [resolution, resolution], }) ) } else { // Keep normal maps near lossless functions.push( textureCompress({ slots: /^(?!normalTexture).*$/, // exclude normal maps encoder: sharp, targetFormat: config.format, resize: [resolution, resolution], }), textureCompress({ slots: /^(?=normalTexture).*$/, // include normal maps encoder: sharp, targetFormat: 'jpeg', resize: [normalResolution, normalResolution], }) ) } functions.push(draco()) await document.transform(...functions) await io.write(output, document) } export default transform