├── .gitignore
├── LICENSE
├── README.md
├── examples
├── KHR_materials_variants.html
├── MSFT_texture_dds.html
├── assets
│ ├── gltf
│ │ ├── BoomBox
│ │ │ └── glTF-dds
│ │ │ │ ├── BoomBox.bin
│ │ │ │ ├── BoomBox.gltf
│ │ │ │ ├── BoomBox_baseColor.dds
│ │ │ │ ├── BoomBox_baseColor.png
│ │ │ │ ├── BoomBox_emissive.dds
│ │ │ │ ├── BoomBox_emissive.png
│ │ │ │ ├── BoomBox_normal.dds
│ │ │ │ ├── BoomBox_normal.png
│ │ │ │ ├── BoomBox_occlusionRoughnessMetallic.dds
│ │ │ │ └── BoomBox_occlusionRoughnessMetallic.png
│ │ └── MaterialsVariantsShoe
│ │ │ ├── README.md
│ │ │ ├── glTF
│ │ │ ├── MaterialsVariantsShoe.bin
│ │ │ ├── MaterialsVariantsShoe.gltf
│ │ │ ├── diffuseBeach.jpg
│ │ │ ├── diffuseMidnight.jpg
│ │ │ ├── diffuseStreet.jpg
│ │ │ ├── normal.jpg
│ │ │ └── occlusionRougnessMetalness.jpg
│ │ │ └── screenshot
│ │ │ └── screenshot.jpg
│ └── textures
│ │ └── equirectangular
│ │ └── royal_esplanade_1k.hdr
├── index.css
├── index.html
├── libs
│ └── dat.gui.module.js
├── main.css
├── three
│ ├── controls
│ │ └── OrbitControls.js
│ ├── exporters
│ │ └── GLTFExporter.js
│ ├── loaders
│ │ ├── DDSLoader.js
│ │ ├── GLTFLoader.js
│ │ └── RGBELoader.js
│ └── three.module.js
└── utils
│ ├── copy_js.sh
│ └── save.js
├── exporters
├── KHR_materials_variants
│ ├── KHR_materials_variants_exporter.js
│ └── README.md
└── README.md
├── loaders
├── KHR_materials_variants
│ ├── KHR_materials_variants.js
│ └── README.md
├── MSFT_texture_dds
│ ├── MSFT_texture_dds.js
│ └── README.md
└── README.md
├── package-lock.json
├── package.json
└── test
├── .gitignore
├── KHR_materials_variants.js
├── MSFT_texture_dds.js
├── build
└── unit.js
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── rollup.unit.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Takahiro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # three-gltf-plugins
2 |
3 | [](https://badge.fury.io/js/three-gltf-plugins)
4 |
5 | [Three.js](https://threejs.org) glTF [loader](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) and [exporter](https://threejs.org/docs/#examples/en/exporters/GLTFExporter) have plugin system to provide extensibility mechanism to users. glTF extensions can be handled with the plugin system.
6 |
7 | Some plugins for major and stable extensions are built-in in the loader and exporter. But other extensions are not supported as built-in by them (yet) because for example the specification is not great fit to Three.js API or structure, or the specification is not finalized.
8 |
9 | If you want to use such extensions you need to write plugins by yourself but it requires the knowledge of glTF specification, extensions specification, Three.js core API, or Three.js glTF loader/exporter API. It can be difficult for some users.
10 |
11 | This project provides you Three.js glTF loader/extension plugins even for such extensions. You no longer need to write the plugin on your own.
12 |
13 | ## Goals
14 |
15 | * Provide reusablity and easiness to use even for the the extensions the spec of which isn't great fit to Three.js API or structure
16 | * Allow early trial of glTF extensions the spec of which is not finalized yet
17 | * Send feedback to Three.js glTF loader/exporter plugin system APIs
18 |
19 | ## Online demo
20 |
21 | * [Online demo](https://rawcdn.githack.com/takahirox/three-gltf-plugins/1f9092a4f14eb6a74790733570334b5b5f81bbd1/examples/index.html)
22 |
23 | ## Supported glTF extensions
24 |
25 | * [KHR_materials_variants](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants)
26 | * [MSFT_texture_dds](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds) (Loader only)
27 |
28 | ## Compatible Three.js revision
29 |
30 | >= r126dev
31 |
32 | ## How to use
33 |
34 | **GLTFLoader plugins**
35 |
36 | ```javascript
37 | // Import Three.js
38 | import * as THREE from 'path_to_three.module.js';
39 | import {GLTFLoader} from 'path_to_GLTFLoader.js';
40 |
41 | // Import three-gltf-plugins loader plugins
42 | import GLTFFooExtension from 'path_to_three-gltf-plugins/loaders/Foo_extension/Foo_extension.js';
43 |
44 | // Register the plugin to the loader and then load glTF
45 | const loader = new GLTFLoader();
46 | loader.register(parser => new GLTFFooExtension(parser));
47 | loader.load(path_to_gltf_asset, gltf => {
48 | ...
49 | });
50 | ```
51 |
52 |
53 | **GLTFExporter plugins**
54 |
55 | ```javascript
56 | // Import Three.js
57 | import * as THREE from 'path_to_three.module.js';
58 | import {GLTFExporter} from 'path_to_GLTFExporter.js';
59 |
60 | // Import three-gltf-plugins exporter plugins
61 | import GLTFExporterFooExtension from 'path_to_three-gltf-plugins/exporters/Foo_extension/Foo_extension_exporter.js';
62 |
63 | // Register the plugin to the exporter and then export Three.js objects
64 | const exporter = new GLTFExporter();
65 | exporter.register(writer => new GLTFExporterFooExtension(writer));
66 | exporter.parse(scene, result => {
67 | ...
68 | });
69 | ```
70 |
71 | Refer to each plugin's README for more detail.
72 |
73 |
74 | ## Locally run examples
75 |
76 | ```sh
77 | $ npm install
78 | $ npm start
79 | # Access http://localhost:8080/examples/index.html
80 | ```
81 |
82 | ## Unit Test
83 |
84 | ### Unit Test on Web browser
85 |
86 | ```sh
87 | $ npm install
88 | $ npm run test-install
89 | $ npm run test-build
90 | $ npm start
91 | # Access http://localhost:8080/test/index.html
92 | ```
93 |
94 | ### Unit Test on Node.js
95 |
96 |
97 | ```sh
98 | $ npm run test-install
99 | $ npm run test
100 | ```
101 |
102 | Note that the unit tests which rely on Web don't run. I recommend to run the unit tests on Web browser.
103 |
104 | ## Customize the plugins in your side
105 |
106 | As written above, some extensions are not great fit to Three.js API or structure. So the plugins for them may have some limitations. If they don't cover your use case, please fork the repository and customize in your side.
107 |
--------------------------------------------------------------------------------
/examples/KHR_materials_variants.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Three.js glTF loader/exporter KHR_materials_variants extension
5 |
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
151 |
152 |
153 |
--------------------------------------------------------------------------------
/examples/MSFT_texture_dds.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Three.js glTF loader/exporter MSFT_texture_dds extension
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox.bin
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox.gltf:
--------------------------------------------------------------------------------
1 | {
2 | "accessors": [
3 | {
4 | "bufferView": 0,
5 | "componentType": 5126,
6 | "count": 3575,
7 | "type": "VEC2",
8 | "max": [
9 | 0.9999003,
10 | -0.0221377648
11 | ],
12 | "min": [
13 | 0.0006585993,
14 | -0.996773958
15 | ]
16 | },
17 | {
18 | "bufferView": 1,
19 | "componentType": 5126,
20 | "count": 3575,
21 | "type": "VEC3",
22 | "max": [
23 | 1.0,
24 | 1.0,
25 | 0.9999782
26 | ],
27 | "min": [
28 | -1.0,
29 | -1.0,
30 | -0.9980823
31 | ]
32 | },
33 | {
34 | "bufferView": 2,
35 | "componentType": 5126,
36 | "count": 3575,
37 | "type": "VEC4",
38 | "max": [
39 | 1.0,
40 | 0.9999976,
41 | 1.0,
42 | 1.0
43 | ],
44 | "min": [
45 | -0.9991289,
46 | -0.999907851,
47 | -1.0,
48 | 1.0
49 | ]
50 | },
51 | {
52 | "bufferView": 3,
53 | "componentType": 5126,
54 | "count": 3575,
55 | "type": "VEC3",
56 | "max": [
57 | 0.009921154,
58 | 0.00977163,
59 | 0.0100762453
60 | ],
61 | "min": [
62 | -0.009921154,
63 | -0.00977163,
64 | -0.0100762453
65 | ]
66 | },
67 | {
68 | "bufferView": 4,
69 | "componentType": 5123,
70 | "count": 18108,
71 | "type": "SCALAR",
72 | "max": [
73 | 3574
74 | ],
75 | "min": [
76 | 0
77 | ]
78 | }
79 | ],
80 | "asset": {
81 | "generator": "glTF Tools for Unity",
82 | "version": "2.0"
83 | },
84 | "bufferViews": [
85 | {
86 | "buffer": 0,
87 | "byteLength": 28600
88 | },
89 | {
90 | "buffer": 0,
91 | "byteOffset": 28600,
92 | "byteLength": 42900
93 | },
94 | {
95 | "buffer": 0,
96 | "byteOffset": 71500,
97 | "byteLength": 57200
98 | },
99 | {
100 | "buffer": 0,
101 | "byteOffset": 128700,
102 | "byteLength": 42900
103 | },
104 | {
105 | "buffer": 0,
106 | "byteOffset": 171600,
107 | "byteLength": 36216
108 | }
109 | ],
110 | "buffers": [
111 | {
112 | "uri": "BoomBox.bin",
113 | "byteLength": 207816
114 | }
115 | ],
116 | "images": [
117 | {
118 | "uri": "BoomBox_baseColor.png"
119 | },
120 | {
121 | "uri": "BoomBox_occlusionRoughnessMetallic.png"
122 | },
123 | {
124 | "uri": "BoomBox_normal.png"
125 | },
126 | {
127 | "uri": "BoomBox_emissive.png"
128 | },
129 | {
130 | "uri": "BoomBox_baseColor.dds"
131 | },
132 | {
133 | "uri": "BoomBox_occlusionRoughnessMetallic.dds"
134 | },
135 | {
136 | "uri": "BoomBox_normal.dds"
137 | },
138 | {
139 | "uri": "BoomBox_emissive.dds"
140 | }
141 | ],
142 | "meshes": [
143 | {
144 | "primitives": [
145 | {
146 | "attributes": {
147 | "TEXCOORD_0": 0,
148 | "NORMAL": 1,
149 | "TANGENT": 2,
150 | "POSITION": 3
151 | },
152 | "indices": 4,
153 | "material": 0
154 | }
155 | ],
156 | "name": "BoomBox"
157 | }
158 | ],
159 | "materials": [
160 | {
161 | "pbrMetallicRoughness": {
162 | "baseColorTexture": {
163 | "index": 0
164 | },
165 | "metallicRoughnessTexture": {
166 | "index": 1
167 | }
168 | },
169 | "normalTexture": {
170 | "index": 2
171 | },
172 | "occlusionTexture": {
173 | "index": 1
174 | },
175 | "emissiveFactor": [
176 | 1.0,
177 | 1.0,
178 | 1.0
179 | ],
180 | "emissiveTexture": {
181 | "index": 3
182 | },
183 | "name": "BoomBox_Mat"
184 | }
185 | ],
186 | "nodes": [
187 | {
188 | "mesh": 0,
189 | "name": "BoomBox"
190 | }
191 | ],
192 | "scene": 0,
193 | "scenes": [
194 | {
195 | "nodes": [
196 | 0
197 | ]
198 | }
199 | ],
200 | "textures": [
201 | {
202 | "source": 0,
203 | "extensions": {
204 | "MSFT_texture_dds": {
205 | "source": 4
206 | }
207 | }
208 | },
209 | {
210 | "source": 1,
211 | "extensions": {
212 | "MSFT_texture_dds": {
213 | "source": 5
214 | }
215 | }
216 | },
217 | {
218 | "source": 2,
219 | "extensions": {
220 | "MSFT_texture_dds": {
221 | "source": 6
222 | }
223 | }
224 | },
225 | {
226 | "source": 3,
227 | "extensions": {
228 | "MSFT_texture_dds": {
229 | "source": 7
230 | }
231 | }
232 | }
233 | ],
234 | "extensionsUsed": [
235 | "MSFT_texture_dds"
236 | ]
237 | }
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_baseColor.dds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_baseColor.dds
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_baseColor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_baseColor.png
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_emissive.dds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_emissive.dds
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_emissive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_emissive.png
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_normal.dds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_normal.dds
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_normal.png
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_occlusionRoughnessMetallic.dds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_occlusionRoughnessMetallic.dds
--------------------------------------------------------------------------------
/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_occlusionRoughnessMetallic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/BoomBox/glTF-dds/BoomBox_occlusionRoughnessMetallic.png
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/README.md:
--------------------------------------------------------------------------------
1 | # Materials Variants Shoe
2 |
3 | ## Screenshot
4 |
5 | 
6 |
7 | ## Description
8 |
9 | This model uses the KHR_materials_variants extension. It is a shoe with 3 color variants in it: "Beach", "Midnight", and "Street".
10 |
11 | If each variant was a separate model, they would be 5.4 MB each. Combined they make up a single model that is 7.8MB since they share geometry and all textures except the base color texture.
12 |
13 | note: The textures in this repository have been resized to save space.
14 | See https://github.com/pushmatrix/glTF-Sample-Models/tree/master/2.0/MaterialsVariantsShoe for the original.
15 |
16 | ## License Information
17 | Copyright 2020 Shopify, Inc.
18 | CC BY 4.0 https://creativecommons.org/licenses/by/4.0/
19 |
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.bin
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.gltf:
--------------------------------------------------------------------------------
1 | {
2 | "accessors": [
3 | {
4 | "bufferView": 0,
5 | "byteOffset": 0,
6 | "count": 13540,
7 | "componentType": 5126,
8 | "type": "VEC3",
9 | "min": [
10 | -0.999970018863678,
11 | -1,
12 | -0.9999949932098388
13 | ],
14 | "max": [
15 | 0.999809980392456,
16 | 0.9999020099639891,
17 | 0.9999240040779114
18 | ]
19 | },
20 | {
21 | "bufferView": 1,
22 | "byteOffset": 0,
23 | "count": 13540,
24 | "componentType": 5126,
25 | "type": "VEC3",
26 | "min": [
27 | -1,
28 | -0.5139704942703247,
29 | -0.38874176144599915
30 | ],
31 | "max": [
32 | 1.0000001192092896,
33 | 0.5139704942703247,
34 | 0.38874176144599915
35 | ]
36 | },
37 | {
38 | "bufferView": 2,
39 | "byteOffset": 0,
40 | "count": 13540,
41 | "componentType": 5126,
42 | "type": "VEC2",
43 | "min": [
44 | 0.001952999969944358,
45 | 0.03520399332046509
46 | ],
47 | "max": [
48 | 0.9980469942092896,
49 | 0.9980469942092896
50 | ]
51 | },
52 | {
53 | "bufferView": 3,
54 | "byteOffset": 0,
55 | "count": 68100,
56 | "componentType": 5125,
57 | "type": "SCALAR",
58 | "min": [
59 | 0
60 | ],
61 | "max": [
62 | 13539
63 | ]
64 | }
65 | ],
66 | "asset": {
67 | "generator": "THREE.GLTFExporter",
68 | "version": "2.0"
69 | },
70 | "buffers": [
71 | {
72 | "name": "shoes-processed",
73 | "byteLength": 705680,
74 | "uri": "MaterialsVariantsShoe.bin"
75 | }
76 | ],
77 | "bufferViews": [
78 | {
79 | "buffer": 0,
80 | "byteLength": 162480,
81 | "byteOffset": 0,
82 | "byteStride": 12,
83 | "target": 34962
84 | },
85 | {
86 | "buffer": 0,
87 | "byteLength": 162480,
88 | "byteOffset": 162480,
89 | "byteStride": 12,
90 | "target": 34962
91 | },
92 | {
93 | "buffer": 0,
94 | "byteLength": 108320,
95 | "byteOffset": 324960,
96 | "byteStride": 8,
97 | "target": 34962
98 | },
99 | {
100 | "buffer": 0,
101 | "byteLength": 272400,
102 | "byteOffset": 433280,
103 | "target": 34963
104 | }
105 | ],
106 | "scene": 0,
107 | "extensions": {
108 | "KHR_materials_variants": {
109 | "variants": [
110 | {"name": "midnight"},
111 | {"name": "beach" },
112 | {"name": "street" }
113 | ]
114 | }
115 | },
116 | "extensionsUsed": [
117 | "KHR_materials_variants"
118 | ],
119 | "images": [
120 | {
121 | "mimeType": "image/jpeg",
122 | "uri": "occlusionRougnessMetalness.jpg"
123 | },
124 | {
125 | "mimeType": "image/jpeg",
126 | "uri": "diffuseMidnight.jpg"
127 | },
128 | {
129 | "mimeType": "image/jpeg",
130 | "uri": "normal.jpg"
131 | },
132 | {
133 | "mimeType": "image/jpeg",
134 | "uri": "diffuseBeach.jpg"
135 | },
136 | {
137 | "mimeType": "image/jpeg",
138 | "uri": "diffuseStreet.jpg"
139 | }
140 | ],
141 | "materials": [
142 | {
143 | "alphaMode": "OPAQUE",
144 | "doubleSided": false,
145 | "name": "phong1SG",
146 | "pbrMetallicRoughness": {
147 | "baseColorFactor": [
148 | 1,
149 | 1,
150 | 1,
151 | 1
152 | ],
153 | "baseColorTexture": {
154 | "index": 1,
155 | "texCoord": 0
156 | },
157 | "metallicFactor": 1,
158 | "roughnessFactor": 1,
159 | "metallicRoughnessTexture": {
160 | "index": 0,
161 | "texCoord": 0
162 | }
163 | },
164 | "normalTexture": {
165 | "index": 2,
166 | "scale": 1,
167 | "texCoord": 0
168 | },
169 | "occlusionTexture": {
170 | "index": 0,
171 | "strength": 1,
172 | "texCoord": 0
173 | },
174 | "emissiveFactor": [
175 | 0,
176 | 0,
177 | 0
178 | ]
179 | },
180 | {
181 | "alphaMode": "OPAQUE",
182 | "doubleSided": false,
183 | "name": "phong1SG",
184 | "pbrMetallicRoughness": {
185 | "baseColorFactor": [
186 | 1,
187 | 1,
188 | 1,
189 | 1
190 | ],
191 | "baseColorTexture": {
192 | "index": 3,
193 | "texCoord": 0
194 | },
195 | "metallicFactor": 1,
196 | "roughnessFactor": 1,
197 | "metallicRoughnessTexture": {
198 | "index": 0,
199 | "texCoord": 0
200 | }
201 | },
202 | "normalTexture": {
203 | "index": 2,
204 | "scale": 1,
205 | "texCoord": 0
206 | },
207 | "occlusionTexture": {
208 | "index": 0,
209 | "strength": 1,
210 | "texCoord": 0
211 | },
212 | "emissiveFactor": [
213 | 0,
214 | 0,
215 | 0
216 | ]
217 | },
218 | {
219 | "alphaMode": "OPAQUE",
220 | "doubleSided": false,
221 | "name": "phong1SG",
222 | "pbrMetallicRoughness": {
223 | "baseColorFactor": [
224 | 1,
225 | 1,
226 | 1,
227 | 1
228 | ],
229 | "baseColorTexture": {
230 | "index": 4,
231 | "texCoord": 0
232 | },
233 | "metallicFactor": 1,
234 | "roughnessFactor": 1,
235 | "metallicRoughnessTexture": {
236 | "index": 0,
237 | "texCoord": 0
238 | }
239 | },
240 | "normalTexture": {
241 | "index": 2,
242 | "scale": 1,
243 | "texCoord": 0
244 | },
245 | "occlusionTexture": {
246 | "index": 0,
247 | "strength": 1,
248 | "texCoord": 0
249 | },
250 | "emissiveFactor": [
251 | 0,
252 | 0,
253 | 0
254 | ]
255 | }
256 | ],
257 | "meshes": [
258 | {
259 | "name": "shoe",
260 | "primitives": [
261 | {
262 | "attributes": {
263 | "NORMAL": 0,
264 | "POSITION": 1,
265 | "TEXCOORD_0": 2
266 | },
267 | "extensions": {
268 | "KHR_materials_variants": {
269 | "mappings": [
270 | {
271 | "material": 0,
272 | "variants": [
273 | 0
274 | ]
275 | },
276 | {
277 | "material": 1,
278 | "variants": [
279 | 1
280 | ]
281 | },
282 | {
283 | "material": 2,
284 | "variants": [
285 | 2
286 | ]
287 | }
288 | ]
289 | }
290 | },
291 | "indices": 3,
292 | "material": 0,
293 | "mode": 4
294 | }
295 | ]
296 | }
297 | ],
298 | "nodes": [
299 | {
300 | "extras": {
301 | "name": "Shoe"
302 | },
303 | "mesh": 0,
304 | "name": "Shoe"
305 | },
306 | {
307 | "children": [
308 | 0
309 | ],
310 | "extras": {
311 | "name": "g Shoe"
312 | },
313 | "name": "g_Shoe"
314 | },
315 | {
316 | "children": [
317 | 1
318 | ],
319 | "extras": {
320 | "name": "Shoe.obj"
321 | },
322 | "matrix": [
323 | 0.14893949,
324 | 0,
325 | 0,
326 | 0,
327 | 0,
328 | 0.14893949,
329 | 0,
330 | 0,
331 | 0,
332 | 0,
333 | 0.14893949,
334 | 0,
335 | 0.0016104877,
336 | 0.07590251,
337 | 0.0048509985,
338 | 1
339 | ],
340 | "name": "Shoeobj"
341 | }
342 | ],
343 | "samplers": [
344 | {
345 | "magFilter": 9729,
346 | "minFilter": 9985,
347 | "wrapS": 10497,
348 | "wrapT": 10497
349 | },
350 | {
351 | "magFilter": 9729,
352 | "minFilter": 9985,
353 | "wrapS": 10497,
354 | "wrapT": 10497
355 | },
356 | {
357 | "magFilter": 9729,
358 | "minFilter": 9985,
359 | "wrapS": 10497,
360 | "wrapT": 10497
361 | }
362 | ],
363 | "scenes": [
364 | {
365 | "name": "Scene",
366 | "nodes": [
367 | 2
368 | ]
369 | }
370 | ],
371 | "textures": [
372 | {
373 | "sampler": 0,
374 | "source": 0
375 | },
376 | {
377 | "sampler": 1,
378 | "source": 1
379 | },
380 | {
381 | "sampler": 2,
382 | "source": 2
383 | },
384 | {
385 | "sampler": 0,
386 | "source": 3
387 | },
388 | {
389 | "sampler": 0,
390 | "source": 4
391 | }
392 | ]
393 | }
394 |
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/glTF/diffuseBeach.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/MaterialsVariantsShoe/glTF/diffuseBeach.jpg
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/glTF/diffuseMidnight.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/MaterialsVariantsShoe/glTF/diffuseMidnight.jpg
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/glTF/diffuseStreet.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/MaterialsVariantsShoe/glTF/diffuseStreet.jpg
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/glTF/normal.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/MaterialsVariantsShoe/glTF/normal.jpg
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/glTF/occlusionRougnessMetalness.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/MaterialsVariantsShoe/glTF/occlusionRougnessMetalness.jpg
--------------------------------------------------------------------------------
/examples/assets/gltf/MaterialsVariantsShoe/screenshot/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/gltf/MaterialsVariantsShoe/screenshot/screenshot.jpg
--------------------------------------------------------------------------------
/examples/assets/textures/equirectangular/royal_esplanade_1k.hdr:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/examples/assets/textures/equirectangular/royal_esplanade_1k.hdr
--------------------------------------------------------------------------------
/examples/index.css:
--------------------------------------------------------------------------------
1 | /* Copied from Three.js */
2 |
3 | :root {
4 | color-scheme: light dark;
5 |
6 | --background-color: #fff;
7 | --secondary-background-color: #f7f7f7;
8 |
9 | --color-blue: #049EF4;
10 | --text-color: #444;
11 | --secondary-text-color: #9e9e9e;
12 |
13 | --font-size: 16px;
14 | --line-height: 26px;
15 |
16 | --border-style: 1px solid #E8E8E8;
17 | --header-height: 48px;
18 | --panel-width: 300px;
19 | --panel-padding: 16px;
20 | --icon-size: 20px;
21 | }
22 |
23 | @media (prefers-color-scheme: dark) {
24 |
25 | :root {
26 | --background-color: #222;
27 | --secondary-background-color: #2e2e2e;
28 |
29 | --text-color: #bbb;
30 | --secondary-text-color: #666;
31 |
32 | --border-style: 1px solid #444;
33 | }
34 |
35 | #previewsToggler {
36 | filter: invert(100%);
37 | }
38 |
39 | }
40 |
41 | * {
42 | box-sizing: border-box;
43 | -webkit-font-smoothing: antialiased;
44 | -moz-osx-font-smoothing: grayscale;
45 | }
46 |
47 | html, body {
48 | height: 100%;
49 | }
50 |
51 | html {
52 | font-size: calc(var(--font-size) - 1px);
53 | line-height: calc(var(--line-height) - 1px);
54 | }
55 |
56 | body {
57 | font-family: 'Roboto Mono', monospace;
58 | margin: 0px;
59 | color: var(--text-color);
60 | background-color: var(--background-color);
61 | }
62 |
63 | a {
64 | text-decoration: none;
65 | }
66 |
67 | h1 {
68 | font-size: 18px;
69 | line-height: 24px;
70 | font-weight: 500;
71 | }
72 |
73 | h2 {
74 | padding: 0;
75 | margin: 16px 0;
76 | font-size: calc(var(--font-size) - 1px);
77 | line-height: var(--line-height);
78 | font-weight: 500;
79 | color: var(--color-blue);
80 | }
81 |
82 | h3 {
83 | margin: 0;
84 | font-weight: 500;
85 | font-size: calc(var(--font-size) - 1px);
86 | line-height: var(--line-height);
87 | color: var(--secondary-text-color);
88 | }
89 |
90 | h1 a {
91 | color: var(--color-blue);
92 | }
93 |
94 | #header {
95 | display: flex;
96 | height: var(--header-height);
97 | border-bottom: var(--border-style);
98 | align-items: center;
99 | }
100 | #header h1 {
101 | padding-left: var(--panel-padding);
102 | flex: 1;
103 | display: flex;
104 | align-items: center;
105 | color: var(--color-blue);
106 | }
107 | #header #version {
108 | border: 1px solid var(--color-blue);
109 | color: var(--color-blue);
110 | border-radius: 4px;
111 | line-height: 16px;
112 | padding: 0px 2px;
113 | margin-left: 6px;
114 | font-size: .9rem;
115 | }
116 | #panel {
117 | position: fixed;
118 | z-index: 100;
119 | left: 0px;
120 | width: var(--panel-width);
121 | height: 100%;
122 | overflow: auto;
123 | border-right: var(--border-style);
124 | display: flex;
125 | flex-direction: column;
126 | transition: 0s 0s height;
127 | }
128 |
129 | #panel #exitSearchButton {
130 | width: 48px;
131 | height: 48px;
132 | display: none;
133 | background-color: var(--text-color);
134 | background-size: var(--icon-size);
135 | -webkit-mask-image: url(../files/ic_close_black_24dp.svg);
136 | -webkit-mask-position: 50% 50%;
137 | -webkit-mask-repeat: no-repeat;
138 | mask-image: url(../files/ic_close_black_24dp.svg);
139 | mask-position: 50% 50%;
140 | mask-repeat: no-repeat;
141 | cursor: pointer;
142 | margin-right: 0px;
143 | }
144 |
145 | #panel.searchFocused #exitSearchButton {
146 | display: block;
147 | }
148 |
149 | #panel.searchFocused #language {
150 | display: none;
151 | }
152 |
153 | #panel.searchFocused #filterInput {
154 | -webkit-mask-image: none;
155 | mask-image: none;
156 | background-color: inherit;
157 | padding-left: 0;
158 | }
159 |
160 | #panel #expandButton {
161 | width: 48px;
162 | height: 48px;
163 | margin-right: 4px;
164 | margin-left: 4px;
165 | display: none;
166 | cursor: pointer;
167 | background-color: var(--text-color);
168 | background-size: var(--icon-size);
169 | -webkit-mask-image: url(../files/ic_menu_black_24dp.svg);
170 | -webkit-mask-position: 50% 50%;
171 | -webkit-mask-repeat: no-repeat;
172 | mask-image: url(../files/ic_menu_black_24dp.svg);
173 | mask-position: 50% 50%;
174 | mask-repeat: no-repeat;
175 | }
176 |
177 | #panel #sections {
178 | display: flex;
179 | justify-content: center;
180 | z-index: 1000;
181 | position: relative;
182 | height: 100%;
183 | align-items: center;
184 | font-weight: 500;
185 | }
186 |
187 | #panel #sections * {
188 | padding: 0 var(--panel-padding);
189 | height: 100%;
190 | position: relative;
191 | display: flex;
192 | justify-content: center;
193 | align-items: center;
194 | }
195 | #panel #sections .selected:after {
196 | content: "";
197 | position: absolute;
198 | left: 0;
199 | right: 0;
200 | bottom: -1px;
201 | border-bottom: 1px solid var(--text-color);
202 | }
203 | #panel #sections a {
204 | color: var(--secondary-text-color);
205 | }
206 |
207 | body.home #panel #sections {
208 | display: none;
209 | }
210 |
211 |
212 | #panel #inputWrapper {
213 | display: flex;
214 | align-items: center;
215 | height: var(--header-height);
216 | padding: 0 0 0 var(--panel-padding);
217 | position: relative;
218 | background: var(--background-color);
219 | }
220 | #panel #inputWrapper:after {
221 | position: absolute;
222 | left: 0;
223 | right: 0;
224 | bottom: 0;
225 | border-bottom: var(--border-style);
226 | content: "";
227 | }
228 |
229 | #panel #filterInput {
230 | flex: 1;
231 | width: 100%;
232 | font-size: 1rem;
233 | font-weight: 500;
234 | color: var(--text-color);
235 | outline: none;
236 | border: 0px;
237 | background-color: var(--text-color);
238 | background-size: var(--icon-size);
239 | -webkit-mask-image: url(../files/ic_search_black_24dp.svg);
240 | -webkit-mask-position: 0 50%;
241 | -webkit-mask-repeat: no-repeat;
242 | mask-image: url(../files/ic_search_black_24dp.svg);
243 | mask-position: 0 50%;
244 | mask-repeat: no-repeat;
245 | font-family: 'Roboto Mono', monospace;
246 | }
247 |
248 | #panel #language {
249 | font-family: 'Roboto Mono', monospace;
250 | font-size: 1rem;
251 | font-weight: 500;
252 | color: var(--text-color);
253 | border: 0px;
254 | background-image: url(ic_arrow_drop_down_black_24dp.svg);
255 | background-size: var(--icon-size);
256 | background-repeat: no-repeat;
257 | background-position: right center;
258 | background-color: var(--background-color);
259 | padding: 2px 24px 4px 24px;
260 | -webkit-appearance: none;
261 | -moz-appearance: none;
262 | appearance: none;
263 | margin-right: 10px;
264 | text-align-last: right;
265 | }
266 |
267 | #panel #language:focus {
268 | outline: none;
269 | }
270 |
271 | #contentWrapper {
272 | flex: 1;
273 | overflow: hidden;
274 | display: flex;
275 | flex-direction: column;
276 | }
277 |
278 | #panel #content {
279 | flex: 1;
280 | overflow-y: auto;
281 | overflow-x: hidden;
282 | -webkit-overflow-scrolling: touch;
283 | padding: 0 var(--panel-padding) var(--panel-padding) var(--panel-padding);
284 | }
285 |
286 | #panel #content ul {
287 | list-style-type: none;
288 | padding: 0px;
289 | margin: 0px 0 20px 0;
290 | }
291 | #panel #content ul li {
292 | margin: 1px 0;
293 | }
294 |
295 | #panel #content h2:not(.hidden) {
296 | margin-top: 16px;
297 | border-top: none;
298 | padding-top: 0;
299 | }
300 |
301 | #panel #content h2:not(.hidden) ~ h2 {
302 | margin-top: 32px;
303 | border-top: var(--border-style);
304 | padding-top: 12px;
305 | }
306 |
307 | #panel #content a {
308 | position: relative;
309 | color: var(--text-color);
310 | }
311 |
312 | #panel #content a:hover,
313 | #panel #content a:hover .spacer,
314 | #panel #content .selected {
315 | color: var(--color-blue);
316 | }
317 |
318 | #panel #content .selected {
319 | text-decoration: underline;
320 | }
321 |
322 | #panel #content .hidden {
323 | display: none !important;
324 | }
325 |
326 | #panel #content #previewsToggler {
327 | cursor: pointer;
328 | float: right;
329 | margin-top: 18px;
330 | margin-bottom: -18px;
331 | opacity: 0.25;
332 | }
333 |
334 | #panel #content.minimal .card {
335 | background-color: transparent;
336 | margin-bottom: 4px;
337 | }
338 |
339 | #panel #content.minimal .cover {
340 | display: none;
341 | }
342 |
343 | #panel #content.minimal .title {
344 | padding: 0;
345 | }
346 |
347 | #panel #content.minimal #previewsToggler {
348 | opacity: 1;
349 | }
350 |
351 | body.home #panel #content h2 {
352 | margin-bottom: 2px;
353 | padding-bottom: 0px;
354 | margin-top: 18px;
355 | border-top: none;
356 | padding-top: 6px;
357 | }
358 |
359 | .spacer {
360 | color: var(--secondary-text-color);
361 | margin-left: 2px;
362 | padding-right: 2px;
363 | }
364 |
365 | #viewer,
366 | iframe {
367 | position: absolute;
368 | border: 0px;
369 | left: 0;
370 | right: 0;
371 | width: 100%;
372 | height: 100%;
373 | overflow: auto;
374 | }
375 |
376 | #viewer {
377 | padding-left: var(--panel-width);
378 | }
379 |
380 | #button {
381 | position: fixed;
382 | bottom: 16px;
383 | right: 16px;
384 |
385 | padding: 12px;
386 | border-radius: 50%;
387 | margin-bottom: 0px;
388 |
389 | background-color: #FFF;
390 | opacity: .9;
391 | z-index: 999;
392 |
393 | box-shadow: 0 0 4px rgba(0,0,0,.15);
394 | }
395 | #button:hover {
396 | cursor: pointer;
397 | opacity: 1;
398 | }
399 | #button img {
400 | display: block;
401 | width: var(--icon-size);
402 | }
403 |
404 | #button.text {
405 | border-radius: 25px;
406 | padding-right: 20px;
407 | padding-left: 20px;
408 | color: var(--color-blue);
409 | opacity: 1;
410 | font-weight: 500;
411 | }
412 |
413 |
414 | #projects {
415 | display: grid;
416 | grid-template-columns: repeat(6, 1fr);
417 | line-height: 0;
418 | }
419 | #projects a {
420 | overflow: hidden;
421 | }
422 | #projects a img {
423 | width: 100%;
424 | transform: scale(1.0);
425 | transition: 0.15s transform;
426 | }
427 | #projects a:hover img {
428 | transform: scale(1.08);
429 | }
430 |
431 |
432 |
433 | @media all and ( min-width: 1500px ) {
434 | #projects {
435 | grid-template-columns: repeat(7, 1fr);
436 | }
437 | }
438 |
439 | @media all and ( min-width: 1700px ) {
440 | :root {
441 | --panel-width: 360px;
442 | --font-size: 18px;
443 | --line-height: 28px;
444 | --header-height: 56px;
445 | --icon-size: 24px;
446 | }
447 | #projects {
448 | grid-template-columns: repeat(8, 1fr);
449 | }
450 | }
451 |
452 | @media all and ( min-width: 1900px ) {
453 |
454 | #projects {
455 | grid-template-columns: repeat(9, 1fr);
456 | }
457 |
458 | }
459 |
460 | @media all and ( max-width: 1300px ) {
461 | #projects {
462 | grid-template-columns: repeat(6, 1fr);
463 | }
464 | }
465 |
466 | @media all and ( max-width: 1100px ) {
467 | #projects {
468 | grid-template-columns: repeat(5, 1fr);
469 | }
470 | }
471 |
472 | @media all and ( max-width: 900px ) {
473 | #projects {
474 | grid-template-columns: repeat(4, 1fr);
475 | }
476 | }
477 |
478 | @media all and ( max-width: 700px ) {
479 | #projects {
480 | grid-template-columns: repeat(3, 1fr);
481 | }
482 | }
483 |
484 |
485 | .card {
486 | border-radius: 3px;
487 | overflow: hidden;
488 | background-color: var(--secondary-background-color);
489 | padding-bottom: 6px;
490 | margin-bottom: 16px;
491 | }
492 |
493 | .card.selected {
494 | box-shadow: 0 0 0 3px var(--color-blue);
495 | text-decoration: none !important;
496 | }
497 |
498 | .card .cover {
499 | padding-bottom: 56.25%; /* 16:9 aspect ratio */
500 | position: relative;
501 | overflow: hidden;
502 | }
503 |
504 | .card .cover img {
505 | position: absolute;
506 | width: 100%;
507 | top: 50%;
508 | left: 50%;
509 | transform: translate(-50%, -50%);
510 | }
511 |
512 | .card .title {
513 | padding: 8px 12px 4px;
514 | font-size: calc(var(--font-size) - 1px);
515 | font-weight: 500;
516 | line-height: calc(var(--line-height) - 6px);
517 | }
518 |
519 | /* mobile */
520 |
521 | @media all and ( max-width: 640px ) {
522 |
523 | :root {
524 | --header-height: 56px;
525 | --icon-size: 24px;
526 | }
527 |
528 | #projects {
529 | grid-template-columns: repeat(2, 1fr);
530 | }
531 |
532 | #panel #expandButton {
533 | display: block;
534 | }
535 | #panel {
536 | position: absolute;
537 | left: 0;
538 | top: 0;
539 | width: 100%;
540 | right: 0;
541 | z-index: 1000;
542 | overflow-x: hidden;
543 | transition: 0s 0s height;
544 | border: none;
545 | height: var(--header-height);
546 | transition: 0s 0.2s height;
547 | }
548 | #panel.open {
549 | height: 100%;
550 | transition: 0s 0s height;
551 | }
552 |
553 | #panelScrim {
554 | pointer-events: none;
555 | background-color: rgba(0,0,0,0);
556 | position: absolute;
557 | left: 0;
558 | right: 0;
559 | top: 0;
560 | bottom: 0;
561 | z-index: 1000;
562 | pointer-events: none;
563 | transition: .2s background-color;
564 | }
565 | #panel.open #panelScrim {
566 | pointer-events: auto;
567 | background-color: rgba(0,0,0,0.4);
568 | }
569 |
570 | #contentWrapper {
571 | position: absolute;
572 | right: 0;
573 | top: 0;
574 | bottom: 0;
575 | background: var(--background-color);
576 | box-shadow: 0 0 8px rgba(0,0,0,.1);
577 | width: calc(100vw - 60px);
578 | max-width: 360px;
579 | z-index: 10000;
580 | transition: .25s transform;
581 | overflow-x: hidden;
582 | margin-right: -380px;
583 | line-height: 2rem;
584 | }
585 | #panel.open #contentWrapper {
586 | transform: translate3d(-380px, 0 ,0);
587 | }
588 | #viewer,
589 | iframe {
590 | left: 0;
591 | top: var(--header-height);
592 | width: 100%;
593 | height: calc(100% - var(--header-height));
594 | }
595 | #viewer {
596 | padding-left: 0;
597 | }
598 | }
599 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | three-gltf-plugins examples
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/examples/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | background-color: #000;
4 | color: #fff;
5 | font-family: Monospace;
6 | font-size: 13px;
7 | line-height: 24px;
8 | overscroll-behavior: none;
9 | }
10 |
11 | a {
12 | color: #ff0;
13 | text-decoration: none;
14 | }
15 |
16 | a:hover {
17 | text-decoration: underline;
18 | }
19 |
20 | button {
21 | cursor: pointer;
22 | text-transform: uppercase;
23 | }
24 |
25 | #info {
26 | position: absolute;
27 | top: 0px;
28 | width: 100%;
29 | padding: 10px;
30 | box-sizing: border-box;
31 | text-align: center;
32 | -moz-user-select: none;
33 | -webkit-user-select: none;
34 | -ms-user-select: none;
35 | user-select: none;
36 | pointer-events: none;
37 | z-index: 1; /* TODO Solve this in HTML */
38 | }
39 |
40 | a, button, input, select {
41 | pointer-events: auto;
42 | }
43 |
44 | .dg.ac {
45 | -moz-user-select: none;
46 | -webkit-user-select: none;
47 | -ms-user-select: none;
48 | user-select: none;
49 | z-index: 2 !important; /* TODO Solve this in HTML */
50 | }
51 |
52 | #overlay {
53 | position: absolute;
54 | font-size: 16px;
55 | z-index: 2;
56 | top: 0;
57 | left: 0;
58 | width: 100%;
59 | height: 100%;
60 | display: flex;
61 | align-items: center;
62 | justify-content: center;
63 | flex-direction: column;
64 | background: rgba(0,0,0,0.7);
65 | }
66 |
67 | #overlay button {
68 | background: transparent;
69 | border: 0;
70 | border: 1px solid rgb(255, 255, 255);
71 | border-radius: 4px;
72 | color: #ffffff;
73 | padding: 12px 18px;
74 | text-transform: uppercase;
75 | cursor: pointer;
76 | }
77 |
78 | #notSupported {
79 | width: 50%;
80 | margin: auto;
81 | background-color: #f00;
82 | margin-top: 20px;
83 | padding: 10px;
84 | }
85 |
--------------------------------------------------------------------------------
/examples/three/controls/OrbitControls.js:
--------------------------------------------------------------------------------
1 | import {
2 | EventDispatcher,
3 | MOUSE,
4 | Quaternion,
5 | Spherical,
6 | TOUCH,
7 | Vector2,
8 | Vector3
9 | } from '../three.module.js';
10 |
11 | // This set of controls performs orbiting, dollying (zooming), and panning.
12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
13 | //
14 | // Orbit - left mouse / touch: one-finger move
15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
17 |
18 | var OrbitControls = function ( object, domElement ) {
19 |
20 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' );
21 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
22 |
23 | this.object = object;
24 | this.domElement = domElement;
25 |
26 | // Set to false to disable this control
27 | this.enabled = true;
28 |
29 | // "target" sets the location of focus, where the object orbits around
30 | this.target = new Vector3();
31 |
32 | // How far you can dolly in and out ( PerspectiveCamera only )
33 | this.minDistance = 0;
34 | this.maxDistance = Infinity;
35 |
36 | // How far you can zoom in and out ( OrthographicCamera only )
37 | this.minZoom = 0;
38 | this.maxZoom = Infinity;
39 |
40 | // How far you can orbit vertically, upper and lower limits.
41 | // Range is 0 to Math.PI radians.
42 | this.minPolarAngle = 0; // radians
43 | this.maxPolarAngle = Math.PI; // radians
44 |
45 | // How far you can orbit horizontally, upper and lower limits.
46 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
47 | this.minAzimuthAngle = - Infinity; // radians
48 | this.maxAzimuthAngle = Infinity; // radians
49 |
50 | // Set to true to enable damping (inertia)
51 | // If damping is enabled, you must call controls.update() in your animation loop
52 | this.enableDamping = false;
53 | this.dampingFactor = 0.05;
54 |
55 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
56 | // Set to false to disable zooming
57 | this.enableZoom = true;
58 | this.zoomSpeed = 1.0;
59 |
60 | // Set to false to disable rotating
61 | this.enableRotate = true;
62 | this.rotateSpeed = 1.0;
63 |
64 | // Set to false to disable panning
65 | this.enablePan = true;
66 | this.panSpeed = 1.0;
67 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
68 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push
69 |
70 | // Set to true to automatically rotate around the target
71 | // If auto-rotate is enabled, you must call controls.update() in your animation loop
72 | this.autoRotate = false;
73 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60
74 |
75 | // The four arrow keys
76 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
77 |
78 | // Mouse buttons
79 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
80 |
81 | // Touch fingers
82 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
83 |
84 | // for reset
85 | this.target0 = this.target.clone();
86 | this.position0 = this.object.position.clone();
87 | this.zoom0 = this.object.zoom;
88 |
89 | // the target DOM element for key events
90 | this._domElementKeyEvents = null;
91 |
92 | //
93 | // public methods
94 | //
95 |
96 | this.getPolarAngle = function () {
97 |
98 | return spherical.phi;
99 |
100 | };
101 |
102 | this.getAzimuthalAngle = function () {
103 |
104 | return spherical.theta;
105 |
106 | };
107 |
108 | this.listenToKeyEvents = function ( domElement ) {
109 |
110 | domElement.addEventListener( 'keydown', onKeyDown );
111 | this._domElementKeyEvents = domElement;
112 |
113 | };
114 |
115 | this.saveState = function () {
116 |
117 | scope.target0.copy( scope.target );
118 | scope.position0.copy( scope.object.position );
119 | scope.zoom0 = scope.object.zoom;
120 |
121 | };
122 |
123 | this.reset = function () {
124 |
125 | scope.target.copy( scope.target0 );
126 | scope.object.position.copy( scope.position0 );
127 | scope.object.zoom = scope.zoom0;
128 |
129 | scope.object.updateProjectionMatrix();
130 | scope.dispatchEvent( changeEvent );
131 |
132 | scope.update();
133 |
134 | state = STATE.NONE;
135 |
136 | };
137 |
138 | // this method is exposed, but perhaps it would be better if we can make it private...
139 | this.update = function () {
140 |
141 | var offset = new Vector3();
142 |
143 | // so camera.up is the orbit axis
144 | var quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) );
145 | var quatInverse = quat.clone().invert();
146 |
147 | var lastPosition = new Vector3();
148 | var lastQuaternion = new Quaternion();
149 |
150 | var twoPI = 2 * Math.PI;
151 |
152 | return function update() {
153 |
154 | var position = scope.object.position;
155 |
156 | offset.copy( position ).sub( scope.target );
157 |
158 | // rotate offset to "y-axis-is-up" space
159 | offset.applyQuaternion( quat );
160 |
161 | // angle from z-axis around y-axis
162 | spherical.setFromVector3( offset );
163 |
164 | if ( scope.autoRotate && state === STATE.NONE ) {
165 |
166 | rotateLeft( getAutoRotationAngle() );
167 |
168 | }
169 |
170 | if ( scope.enableDamping ) {
171 |
172 | spherical.theta += sphericalDelta.theta * scope.dampingFactor;
173 | spherical.phi += sphericalDelta.phi * scope.dampingFactor;
174 |
175 | } else {
176 |
177 | spherical.theta += sphericalDelta.theta;
178 | spherical.phi += sphericalDelta.phi;
179 |
180 | }
181 |
182 | // restrict theta to be between desired limits
183 |
184 | var min = scope.minAzimuthAngle;
185 | var max = scope.maxAzimuthAngle;
186 |
187 | if ( isFinite( min ) && isFinite( max ) ) {
188 |
189 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
190 |
191 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
192 |
193 | if ( min <= max ) {
194 |
195 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
196 |
197 | } else {
198 |
199 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ?
200 | Math.max( min, spherical.theta ) :
201 | Math.min( max, spherical.theta );
202 |
203 | }
204 |
205 | }
206 |
207 | // restrict phi to be between desired limits
208 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
209 |
210 | spherical.makeSafe();
211 |
212 |
213 | spherical.radius *= scale;
214 |
215 | // restrict radius to be between desired limits
216 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) );
217 |
218 | // move target to panned location
219 |
220 | if ( scope.enableDamping === true ) {
221 |
222 | scope.target.addScaledVector( panOffset, scope.dampingFactor );
223 |
224 | } else {
225 |
226 | scope.target.add( panOffset );
227 |
228 | }
229 |
230 | offset.setFromSpherical( spherical );
231 |
232 | // rotate offset back to "camera-up-vector-is-up" space
233 | offset.applyQuaternion( quatInverse );
234 |
235 | position.copy( scope.target ).add( offset );
236 |
237 | scope.object.lookAt( scope.target );
238 |
239 | if ( scope.enableDamping === true ) {
240 |
241 | sphericalDelta.theta *= ( 1 - scope.dampingFactor );
242 | sphericalDelta.phi *= ( 1 - scope.dampingFactor );
243 |
244 | panOffset.multiplyScalar( 1 - scope.dampingFactor );
245 |
246 | } else {
247 |
248 | sphericalDelta.set( 0, 0, 0 );
249 |
250 | panOffset.set( 0, 0, 0 );
251 |
252 | }
253 |
254 | scale = 1;
255 |
256 | // update condition is:
257 | // min(camera displacement, camera rotation in radians)^2 > EPS
258 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8
259 |
260 | if ( zoomChanged ||
261 | lastPosition.distanceToSquared( scope.object.position ) > EPS ||
262 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
263 |
264 | scope.dispatchEvent( changeEvent );
265 |
266 | lastPosition.copy( scope.object.position );
267 | lastQuaternion.copy( scope.object.quaternion );
268 | zoomChanged = false;
269 |
270 | return true;
271 |
272 | }
273 |
274 | return false;
275 |
276 | };
277 |
278 | }();
279 |
280 | this.dispose = function () {
281 |
282 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu );
283 |
284 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
285 | scope.domElement.removeEventListener( 'wheel', onMouseWheel );
286 |
287 | scope.domElement.removeEventListener( 'touchstart', onTouchStart );
288 | scope.domElement.removeEventListener( 'touchend', onTouchEnd );
289 | scope.domElement.removeEventListener( 'touchmove', onTouchMove );
290 |
291 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
292 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
293 |
294 |
295 | if ( scope._domElementKeyEvents !== null ) {
296 |
297 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
298 |
299 | }
300 |
301 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
302 |
303 | };
304 |
305 | //
306 | // internals
307 | //
308 |
309 | var scope = this;
310 |
311 | var changeEvent = { type: 'change' };
312 | var startEvent = { type: 'start' };
313 | var endEvent = { type: 'end' };
314 |
315 | var STATE = {
316 | NONE: - 1,
317 | ROTATE: 0,
318 | DOLLY: 1,
319 | PAN: 2,
320 | TOUCH_ROTATE: 3,
321 | TOUCH_PAN: 4,
322 | TOUCH_DOLLY_PAN: 5,
323 | TOUCH_DOLLY_ROTATE: 6
324 | };
325 |
326 | var state = STATE.NONE;
327 |
328 | var EPS = 0.000001;
329 |
330 | // current position in spherical coordinates
331 | var spherical = new Spherical();
332 | var sphericalDelta = new Spherical();
333 |
334 | var scale = 1;
335 | var panOffset = new Vector3();
336 | var zoomChanged = false;
337 |
338 | var rotateStart = new Vector2();
339 | var rotateEnd = new Vector2();
340 | var rotateDelta = new Vector2();
341 |
342 | var panStart = new Vector2();
343 | var panEnd = new Vector2();
344 | var panDelta = new Vector2();
345 |
346 | var dollyStart = new Vector2();
347 | var dollyEnd = new Vector2();
348 | var dollyDelta = new Vector2();
349 |
350 | function getAutoRotationAngle() {
351 |
352 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
353 |
354 | }
355 |
356 | function getZoomScale() {
357 |
358 | return Math.pow( 0.95, scope.zoomSpeed );
359 |
360 | }
361 |
362 | function rotateLeft( angle ) {
363 |
364 | sphericalDelta.theta -= angle;
365 |
366 | }
367 |
368 | function rotateUp( angle ) {
369 |
370 | sphericalDelta.phi -= angle;
371 |
372 | }
373 |
374 | var panLeft = function () {
375 |
376 | var v = new Vector3();
377 |
378 | return function panLeft( distance, objectMatrix ) {
379 |
380 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
381 | v.multiplyScalar( - distance );
382 |
383 | panOffset.add( v );
384 |
385 | };
386 |
387 | }();
388 |
389 | var panUp = function () {
390 |
391 | var v = new Vector3();
392 |
393 | return function panUp( distance, objectMatrix ) {
394 |
395 | if ( scope.screenSpacePanning === true ) {
396 |
397 | v.setFromMatrixColumn( objectMatrix, 1 );
398 |
399 | } else {
400 |
401 | v.setFromMatrixColumn( objectMatrix, 0 );
402 | v.crossVectors( scope.object.up, v );
403 |
404 | }
405 |
406 | v.multiplyScalar( distance );
407 |
408 | panOffset.add( v );
409 |
410 | };
411 |
412 | }();
413 |
414 | // deltaX and deltaY are in pixels; right and down are positive
415 | var pan = function () {
416 |
417 | var offset = new Vector3();
418 |
419 | return function pan( deltaX, deltaY ) {
420 |
421 | var element = scope.domElement;
422 |
423 | if ( scope.object.isPerspectiveCamera ) {
424 |
425 | // perspective
426 | var position = scope.object.position;
427 | offset.copy( position ).sub( scope.target );
428 | var targetDistance = offset.length();
429 |
430 | // half of the fov is center to top of screen
431 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
432 |
433 | // we use only clientHeight here so aspect ratio does not distort speed
434 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
435 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
436 |
437 | } else if ( scope.object.isOrthographicCamera ) {
438 |
439 | // orthographic
440 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
441 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
442 |
443 | } else {
444 |
445 | // camera neither orthographic nor perspective
446 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
447 | scope.enablePan = false;
448 |
449 | }
450 |
451 | };
452 |
453 | }();
454 |
455 | function dollyOut( dollyScale ) {
456 |
457 | if ( scope.object.isPerspectiveCamera ) {
458 |
459 | scale /= dollyScale;
460 |
461 | } else if ( scope.object.isOrthographicCamera ) {
462 |
463 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
464 | scope.object.updateProjectionMatrix();
465 | zoomChanged = true;
466 |
467 | } else {
468 |
469 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
470 | scope.enableZoom = false;
471 |
472 | }
473 |
474 | }
475 |
476 | function dollyIn( dollyScale ) {
477 |
478 | if ( scope.object.isPerspectiveCamera ) {
479 |
480 | scale *= dollyScale;
481 |
482 | } else if ( scope.object.isOrthographicCamera ) {
483 |
484 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
485 | scope.object.updateProjectionMatrix();
486 | zoomChanged = true;
487 |
488 | } else {
489 |
490 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
491 | scope.enableZoom = false;
492 |
493 | }
494 |
495 | }
496 |
497 | //
498 | // event callbacks - update the object state
499 | //
500 |
501 | function handleMouseDownRotate( event ) {
502 |
503 | rotateStart.set( event.clientX, event.clientY );
504 |
505 | }
506 |
507 | function handleMouseDownDolly( event ) {
508 |
509 | dollyStart.set( event.clientX, event.clientY );
510 |
511 | }
512 |
513 | function handleMouseDownPan( event ) {
514 |
515 | panStart.set( event.clientX, event.clientY );
516 |
517 | }
518 |
519 | function handleMouseMoveRotate( event ) {
520 |
521 | rotateEnd.set( event.clientX, event.clientY );
522 |
523 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
524 |
525 | var element = scope.domElement;
526 |
527 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
528 |
529 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
530 |
531 | rotateStart.copy( rotateEnd );
532 |
533 | scope.update();
534 |
535 | }
536 |
537 | function handleMouseMoveDolly( event ) {
538 |
539 | dollyEnd.set( event.clientX, event.clientY );
540 |
541 | dollyDelta.subVectors( dollyEnd, dollyStart );
542 |
543 | if ( dollyDelta.y > 0 ) {
544 |
545 | dollyOut( getZoomScale() );
546 |
547 | } else if ( dollyDelta.y < 0 ) {
548 |
549 | dollyIn( getZoomScale() );
550 |
551 | }
552 |
553 | dollyStart.copy( dollyEnd );
554 |
555 | scope.update();
556 |
557 | }
558 |
559 | function handleMouseMovePan( event ) {
560 |
561 | panEnd.set( event.clientX, event.clientY );
562 |
563 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
564 |
565 | pan( panDelta.x, panDelta.y );
566 |
567 | panStart.copy( panEnd );
568 |
569 | scope.update();
570 |
571 | }
572 |
573 | function handleMouseUp( /*event*/ ) {
574 |
575 | // no-op
576 |
577 | }
578 |
579 | function handleMouseWheel( event ) {
580 |
581 | if ( event.deltaY < 0 ) {
582 |
583 | dollyIn( getZoomScale() );
584 |
585 | } else if ( event.deltaY > 0 ) {
586 |
587 | dollyOut( getZoomScale() );
588 |
589 | }
590 |
591 | scope.update();
592 |
593 | }
594 |
595 | function handleKeyDown( event ) {
596 |
597 | var needsUpdate = false;
598 |
599 | switch ( event.keyCode ) {
600 |
601 | case scope.keys.UP:
602 | pan( 0, scope.keyPanSpeed );
603 | needsUpdate = true;
604 | break;
605 |
606 | case scope.keys.BOTTOM:
607 | pan( 0, - scope.keyPanSpeed );
608 | needsUpdate = true;
609 | break;
610 |
611 | case scope.keys.LEFT:
612 | pan( scope.keyPanSpeed, 0 );
613 | needsUpdate = true;
614 | break;
615 |
616 | case scope.keys.RIGHT:
617 | pan( - scope.keyPanSpeed, 0 );
618 | needsUpdate = true;
619 | break;
620 |
621 | }
622 |
623 | if ( needsUpdate ) {
624 |
625 | // prevent the browser from scrolling on cursor keys
626 | event.preventDefault();
627 |
628 | scope.update();
629 |
630 | }
631 |
632 |
633 | }
634 |
635 | function handleTouchStartRotate( event ) {
636 |
637 | if ( event.touches.length == 1 ) {
638 |
639 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
640 |
641 | } else {
642 |
643 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
644 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
645 |
646 | rotateStart.set( x, y );
647 |
648 | }
649 |
650 | }
651 |
652 | function handleTouchStartPan( event ) {
653 |
654 | if ( event.touches.length == 1 ) {
655 |
656 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
657 |
658 | } else {
659 |
660 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
661 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
662 |
663 | panStart.set( x, y );
664 |
665 | }
666 |
667 | }
668 |
669 | function handleTouchStartDolly( event ) {
670 |
671 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
672 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
673 |
674 | var distance = Math.sqrt( dx * dx + dy * dy );
675 |
676 | dollyStart.set( 0, distance );
677 |
678 | }
679 |
680 | function handleTouchStartDollyPan( event ) {
681 |
682 | if ( scope.enableZoom ) handleTouchStartDolly( event );
683 |
684 | if ( scope.enablePan ) handleTouchStartPan( event );
685 |
686 | }
687 |
688 | function handleTouchStartDollyRotate( event ) {
689 |
690 | if ( scope.enableZoom ) handleTouchStartDolly( event );
691 |
692 | if ( scope.enableRotate ) handleTouchStartRotate( event );
693 |
694 | }
695 |
696 | function handleTouchMoveRotate( event ) {
697 |
698 | if ( event.touches.length == 1 ) {
699 |
700 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
701 |
702 | } else {
703 |
704 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
705 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
706 |
707 | rotateEnd.set( x, y );
708 |
709 | }
710 |
711 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
712 |
713 | var element = scope.domElement;
714 |
715 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
716 |
717 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
718 |
719 | rotateStart.copy( rotateEnd );
720 |
721 | }
722 |
723 | function handleTouchMovePan( event ) {
724 |
725 | if ( event.touches.length == 1 ) {
726 |
727 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
728 |
729 | } else {
730 |
731 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
732 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
733 |
734 | panEnd.set( x, y );
735 |
736 | }
737 |
738 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
739 |
740 | pan( panDelta.x, panDelta.y );
741 |
742 | panStart.copy( panEnd );
743 |
744 | }
745 |
746 | function handleTouchMoveDolly( event ) {
747 |
748 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
749 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
750 |
751 | var distance = Math.sqrt( dx * dx + dy * dy );
752 |
753 | dollyEnd.set( 0, distance );
754 |
755 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
756 |
757 | dollyOut( dollyDelta.y );
758 |
759 | dollyStart.copy( dollyEnd );
760 |
761 | }
762 |
763 | function handleTouchMoveDollyPan( event ) {
764 |
765 | if ( scope.enableZoom ) handleTouchMoveDolly( event );
766 |
767 | if ( scope.enablePan ) handleTouchMovePan( event );
768 |
769 | }
770 |
771 | function handleTouchMoveDollyRotate( event ) {
772 |
773 | if ( scope.enableZoom ) handleTouchMoveDolly( event );
774 |
775 | if ( scope.enableRotate ) handleTouchMoveRotate( event );
776 |
777 | }
778 |
779 | function handleTouchEnd( /*event*/ ) {
780 |
781 | // no-op
782 |
783 | }
784 |
785 | //
786 | // event handlers - FSM: listen for events and reset state
787 | //
788 |
789 | function onPointerDown( event ) {
790 |
791 | if ( scope.enabled === false ) return;
792 |
793 | switch ( event.pointerType ) {
794 |
795 | case 'mouse':
796 | case 'pen':
797 | onMouseDown( event );
798 | break;
799 |
800 | // TODO touch
801 |
802 | }
803 |
804 | }
805 |
806 | function onPointerMove( event ) {
807 |
808 | if ( scope.enabled === false ) return;
809 |
810 | switch ( event.pointerType ) {
811 |
812 | case 'mouse':
813 | case 'pen':
814 | onMouseMove( event );
815 | break;
816 |
817 | // TODO touch
818 |
819 | }
820 |
821 | }
822 |
823 | function onPointerUp( event ) {
824 |
825 | switch ( event.pointerType ) {
826 |
827 | case 'mouse':
828 | case 'pen':
829 | onMouseUp( event );
830 | break;
831 |
832 | // TODO touch
833 |
834 | }
835 |
836 | }
837 |
838 | function onMouseDown( event ) {
839 |
840 | // Prevent the browser from scrolling.
841 | event.preventDefault();
842 |
843 | // Manually set the focus since calling preventDefault above
844 | // prevents the browser from setting it automatically.
845 |
846 | scope.domElement.focus ? scope.domElement.focus() : window.focus();
847 |
848 | var mouseAction;
849 |
850 | switch ( event.button ) {
851 |
852 | case 0:
853 |
854 | mouseAction = scope.mouseButtons.LEFT;
855 | break;
856 |
857 | case 1:
858 |
859 | mouseAction = scope.mouseButtons.MIDDLE;
860 | break;
861 |
862 | case 2:
863 |
864 | mouseAction = scope.mouseButtons.RIGHT;
865 | break;
866 |
867 | default:
868 |
869 | mouseAction = - 1;
870 |
871 | }
872 |
873 | switch ( mouseAction ) {
874 |
875 | case MOUSE.DOLLY:
876 |
877 | if ( scope.enableZoom === false ) return;
878 |
879 | handleMouseDownDolly( event );
880 |
881 | state = STATE.DOLLY;
882 |
883 | break;
884 |
885 | case MOUSE.ROTATE:
886 |
887 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
888 |
889 | if ( scope.enablePan === false ) return;
890 |
891 | handleMouseDownPan( event );
892 |
893 | state = STATE.PAN;
894 |
895 | } else {
896 |
897 | if ( scope.enableRotate === false ) return;
898 |
899 | handleMouseDownRotate( event );
900 |
901 | state = STATE.ROTATE;
902 |
903 | }
904 |
905 | break;
906 |
907 | case MOUSE.PAN:
908 |
909 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
910 |
911 | if ( scope.enableRotate === false ) return;
912 |
913 | handleMouseDownRotate( event );
914 |
915 | state = STATE.ROTATE;
916 |
917 | } else {
918 |
919 | if ( scope.enablePan === false ) return;
920 |
921 | handleMouseDownPan( event );
922 |
923 | state = STATE.PAN;
924 |
925 | }
926 |
927 | break;
928 |
929 | default:
930 |
931 | state = STATE.NONE;
932 |
933 | }
934 |
935 | if ( state !== STATE.NONE ) {
936 |
937 | scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
938 | scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
939 |
940 | scope.dispatchEvent( startEvent );
941 |
942 | }
943 |
944 | }
945 |
946 | function onMouseMove( event ) {
947 |
948 | if ( scope.enabled === false ) return;
949 |
950 | event.preventDefault();
951 |
952 | switch ( state ) {
953 |
954 | case STATE.ROTATE:
955 |
956 | if ( scope.enableRotate === false ) return;
957 |
958 | handleMouseMoveRotate( event );
959 |
960 | break;
961 |
962 | case STATE.DOLLY:
963 |
964 | if ( scope.enableZoom === false ) return;
965 |
966 | handleMouseMoveDolly( event );
967 |
968 | break;
969 |
970 | case STATE.PAN:
971 |
972 | if ( scope.enablePan === false ) return;
973 |
974 | handleMouseMovePan( event );
975 |
976 | break;
977 |
978 | }
979 |
980 | }
981 |
982 | function onMouseUp( event ) {
983 |
984 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
985 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
986 |
987 | if ( scope.enabled === false ) return;
988 |
989 | handleMouseUp( event );
990 |
991 | scope.dispatchEvent( endEvent );
992 |
993 | state = STATE.NONE;
994 |
995 | }
996 |
997 | function onMouseWheel( event ) {
998 |
999 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return;
1000 |
1001 | event.preventDefault();
1002 | event.stopPropagation();
1003 |
1004 | scope.dispatchEvent( startEvent );
1005 |
1006 | handleMouseWheel( event );
1007 |
1008 | scope.dispatchEvent( endEvent );
1009 |
1010 | }
1011 |
1012 | function onKeyDown( event ) {
1013 |
1014 | if ( scope.enabled === false || scope.enablePan === false ) return;
1015 |
1016 | handleKeyDown( event );
1017 |
1018 | }
1019 |
1020 | function onTouchStart( event ) {
1021 |
1022 | if ( scope.enabled === false ) return;
1023 |
1024 | event.preventDefault(); // prevent scrolling
1025 |
1026 | switch ( event.touches.length ) {
1027 |
1028 | case 1:
1029 |
1030 | switch ( scope.touches.ONE ) {
1031 |
1032 | case TOUCH.ROTATE:
1033 |
1034 | if ( scope.enableRotate === false ) return;
1035 |
1036 | handleTouchStartRotate( event );
1037 |
1038 | state = STATE.TOUCH_ROTATE;
1039 |
1040 | break;
1041 |
1042 | case TOUCH.PAN:
1043 |
1044 | if ( scope.enablePan === false ) return;
1045 |
1046 | handleTouchStartPan( event );
1047 |
1048 | state = STATE.TOUCH_PAN;
1049 |
1050 | break;
1051 |
1052 | default:
1053 |
1054 | state = STATE.NONE;
1055 |
1056 | }
1057 |
1058 | break;
1059 |
1060 | case 2:
1061 |
1062 | switch ( scope.touches.TWO ) {
1063 |
1064 | case TOUCH.DOLLY_PAN:
1065 |
1066 | if ( scope.enableZoom === false && scope.enablePan === false ) return;
1067 |
1068 | handleTouchStartDollyPan( event );
1069 |
1070 | state = STATE.TOUCH_DOLLY_PAN;
1071 |
1072 | break;
1073 |
1074 | case TOUCH.DOLLY_ROTATE:
1075 |
1076 | if ( scope.enableZoom === false && scope.enableRotate === false ) return;
1077 |
1078 | handleTouchStartDollyRotate( event );
1079 |
1080 | state = STATE.TOUCH_DOLLY_ROTATE;
1081 |
1082 | break;
1083 |
1084 | default:
1085 |
1086 | state = STATE.NONE;
1087 |
1088 | }
1089 |
1090 | break;
1091 |
1092 | default:
1093 |
1094 | state = STATE.NONE;
1095 |
1096 | }
1097 |
1098 | if ( state !== STATE.NONE ) {
1099 |
1100 | scope.dispatchEvent( startEvent );
1101 |
1102 | }
1103 |
1104 | }
1105 |
1106 | function onTouchMove( event ) {
1107 |
1108 | if ( scope.enabled === false ) return;
1109 |
1110 | event.preventDefault(); // prevent scrolling
1111 | event.stopPropagation();
1112 |
1113 | switch ( state ) {
1114 |
1115 | case STATE.TOUCH_ROTATE:
1116 |
1117 | if ( scope.enableRotate === false ) return;
1118 |
1119 | handleTouchMoveRotate( event );
1120 |
1121 | scope.update();
1122 |
1123 | break;
1124 |
1125 | case STATE.TOUCH_PAN:
1126 |
1127 | if ( scope.enablePan === false ) return;
1128 |
1129 | handleTouchMovePan( event );
1130 |
1131 | scope.update();
1132 |
1133 | break;
1134 |
1135 | case STATE.TOUCH_DOLLY_PAN:
1136 |
1137 | if ( scope.enableZoom === false && scope.enablePan === false ) return;
1138 |
1139 | handleTouchMoveDollyPan( event );
1140 |
1141 | scope.update();
1142 |
1143 | break;
1144 |
1145 | case STATE.TOUCH_DOLLY_ROTATE:
1146 |
1147 | if ( scope.enableZoom === false && scope.enableRotate === false ) return;
1148 |
1149 | handleTouchMoveDollyRotate( event );
1150 |
1151 | scope.update();
1152 |
1153 | break;
1154 |
1155 | default:
1156 |
1157 | state = STATE.NONE;
1158 |
1159 | }
1160 |
1161 | }
1162 |
1163 | function onTouchEnd( event ) {
1164 |
1165 | if ( scope.enabled === false ) return;
1166 |
1167 | handleTouchEnd( event );
1168 |
1169 | scope.dispatchEvent( endEvent );
1170 |
1171 | state = STATE.NONE;
1172 |
1173 | }
1174 |
1175 | function onContextMenu( event ) {
1176 |
1177 | if ( scope.enabled === false ) return;
1178 |
1179 | event.preventDefault();
1180 |
1181 | }
1182 |
1183 | //
1184 |
1185 | scope.domElement.addEventListener( 'contextmenu', onContextMenu );
1186 |
1187 | scope.domElement.addEventListener( 'pointerdown', onPointerDown );
1188 | scope.domElement.addEventListener( 'wheel', onMouseWheel );
1189 |
1190 | scope.domElement.addEventListener( 'touchstart', onTouchStart );
1191 | scope.domElement.addEventListener( 'touchend', onTouchEnd );
1192 | scope.domElement.addEventListener( 'touchmove', onTouchMove );
1193 |
1194 | // force an update at start
1195 |
1196 | this.update();
1197 |
1198 | };
1199 |
1200 | OrbitControls.prototype = Object.create( EventDispatcher.prototype );
1201 | OrbitControls.prototype.constructor = OrbitControls;
1202 |
1203 |
1204 | // This set of controls performs orbiting, dollying (zooming), and panning.
1205 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
1206 | // This is very similar to OrbitControls, another set of touch behavior
1207 | //
1208 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
1209 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
1210 | // Pan - left mouse, or arrow keys / touch: one-finger move
1211 |
1212 | var MapControls = function ( object, domElement ) {
1213 |
1214 | OrbitControls.call( this, object, domElement );
1215 |
1216 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
1217 |
1218 | this.mouseButtons.LEFT = MOUSE.PAN;
1219 | this.mouseButtons.RIGHT = MOUSE.ROTATE;
1220 |
1221 | this.touches.ONE = TOUCH.PAN;
1222 | this.touches.TWO = TOUCH.DOLLY_ROTATE;
1223 |
1224 | };
1225 |
1226 | MapControls.prototype = Object.create( EventDispatcher.prototype );
1227 | MapControls.prototype.constructor = MapControls;
1228 |
1229 | export { OrbitControls, MapControls };
1230 |
--------------------------------------------------------------------------------
/examples/three/exporters/GLTFExporter.js:
--------------------------------------------------------------------------------
1 | import {
2 | BufferAttribute,
3 | ClampToEdgeWrapping,
4 | DoubleSide,
5 | InterpolateDiscrete,
6 | InterpolateLinear,
7 | LinearFilter,
8 | LinearMipmapLinearFilter,
9 | LinearMipmapNearestFilter,
10 | MathUtils,
11 | Matrix4,
12 | MirroredRepeatWrapping,
13 | NearestFilter,
14 | NearestMipmapLinearFilter,
15 | NearestMipmapNearestFilter,
16 | PropertyBinding,
17 | RGBAFormat,
18 | RGBFormat,
19 | RepeatWrapping,
20 | Scene,
21 | Vector3
22 | } from '../three.module.js';
23 |
24 | var GLTFExporter = ( function () {
25 |
26 | function GLTFExporter() {
27 |
28 | this.pluginCallbacks = [];
29 |
30 | this.register( function ( writer ) {
31 |
32 | return new GLTFLightExtension( writer );
33 |
34 | } );
35 |
36 | this.register( function ( writer ) {
37 |
38 | return new GLTFMaterialsUnlitExtension( writer );
39 |
40 | } );
41 |
42 | this.register( function ( writer ) {
43 |
44 | return new GLTFMaterialsPBRSpecularGlossiness( writer );
45 |
46 | } );
47 |
48 | }
49 |
50 | GLTFExporter.prototype = {
51 |
52 | constructor: GLTFExporter,
53 |
54 | register: function ( callback ) {
55 |
56 | if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) {
57 |
58 | this.pluginCallbacks.push( callback );
59 |
60 | }
61 |
62 | return this;
63 |
64 | },
65 |
66 | unregister: function ( callback ) {
67 |
68 | if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) {
69 |
70 | this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 );
71 |
72 | }
73 |
74 | return this;
75 |
76 | },
77 |
78 | /**
79 | * Parse scenes and generate GLTF output
80 | * @param {Scene or [THREE.Scenes]} input Scene or Array of THREE.Scenes
81 | * @param {Function} onDone Callback on completed
82 | * @param {Object} options options
83 | */
84 | parse: function ( input, onDone, options ) {
85 |
86 | var writer = new GLTFWriter();
87 | var plugins = [];
88 |
89 | for ( var i = 0, il = this.pluginCallbacks.length; i < il; i ++ ) {
90 |
91 | plugins.push( this.pluginCallbacks[ i ]( writer ) );
92 |
93 | }
94 |
95 | writer.setPlugins( plugins );
96 | writer.write( input, onDone, options );
97 |
98 | }
99 |
100 | };
101 |
102 | //------------------------------------------------------------------------------
103 | // Constants
104 | //------------------------------------------------------------------------------
105 |
106 | var WEBGL_CONSTANTS = {
107 | POINTS: 0x0000,
108 | LINES: 0x0001,
109 | LINE_LOOP: 0x0002,
110 | LINE_STRIP: 0x0003,
111 | TRIANGLES: 0x0004,
112 | TRIANGLE_STRIP: 0x0005,
113 | TRIANGLE_FAN: 0x0006,
114 |
115 | UNSIGNED_BYTE: 0x1401,
116 | UNSIGNED_SHORT: 0x1403,
117 | FLOAT: 0x1406,
118 | UNSIGNED_INT: 0x1405,
119 | ARRAY_BUFFER: 0x8892,
120 | ELEMENT_ARRAY_BUFFER: 0x8893,
121 |
122 | NEAREST: 0x2600,
123 | LINEAR: 0x2601,
124 | NEAREST_MIPMAP_NEAREST: 0x2700,
125 | LINEAR_MIPMAP_NEAREST: 0x2701,
126 | NEAREST_MIPMAP_LINEAR: 0x2702,
127 | LINEAR_MIPMAP_LINEAR: 0x2703,
128 |
129 | CLAMP_TO_EDGE: 33071,
130 | MIRRORED_REPEAT: 33648,
131 | REPEAT: 10497
132 | };
133 |
134 | var THREE_TO_WEBGL = {};
135 |
136 | THREE_TO_WEBGL[ NearestFilter ] = WEBGL_CONSTANTS.NEAREST;
137 | THREE_TO_WEBGL[ NearestMipmapNearestFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST;
138 | THREE_TO_WEBGL[ NearestMipmapLinearFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR;
139 | THREE_TO_WEBGL[ LinearFilter ] = WEBGL_CONSTANTS.LINEAR;
140 | THREE_TO_WEBGL[ LinearMipmapNearestFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST;
141 | THREE_TO_WEBGL[ LinearMipmapLinearFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR;
142 |
143 | THREE_TO_WEBGL[ ClampToEdgeWrapping ] = WEBGL_CONSTANTS.CLAMP_TO_EDGE;
144 | THREE_TO_WEBGL[ RepeatWrapping ] = WEBGL_CONSTANTS.REPEAT;
145 | THREE_TO_WEBGL[ MirroredRepeatWrapping ] = WEBGL_CONSTANTS.MIRRORED_REPEAT;
146 |
147 | var PATH_PROPERTIES = {
148 | scale: 'scale',
149 | position: 'translation',
150 | quaternion: 'rotation',
151 | morphTargetInfluences: 'weights'
152 | };
153 |
154 | // GLB constants
155 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
156 |
157 | var GLB_HEADER_BYTES = 12;
158 | var GLB_HEADER_MAGIC = 0x46546C67;
159 | var GLB_VERSION = 2;
160 |
161 | var GLB_CHUNK_PREFIX_BYTES = 8;
162 | var GLB_CHUNK_TYPE_JSON = 0x4E4F534A;
163 | var GLB_CHUNK_TYPE_BIN = 0x004E4942;
164 |
165 | //------------------------------------------------------------------------------
166 | // Utility functions
167 | //------------------------------------------------------------------------------
168 |
169 | /**
170 | * Compare two arrays
171 | * @param {Array} array1 Array 1 to compare
172 | * @param {Array} array2 Array 2 to compare
173 | * @return {Boolean} Returns true if both arrays are equal
174 | */
175 | function equalArray( array1, array2 ) {
176 |
177 | return ( array1.length === array2.length ) && array1.every( function ( element, index ) {
178 |
179 | return element === array2[ index ];
180 |
181 | } );
182 |
183 | }
184 |
185 | /**
186 | * Converts a string to an ArrayBuffer.
187 | * @param {string} text
188 | * @return {ArrayBuffer}
189 | */
190 | function stringToArrayBuffer( text ) {
191 |
192 | if ( window.TextEncoder !== undefined ) {
193 |
194 | return new TextEncoder().encode( text ).buffer;
195 |
196 | }
197 |
198 | var array = new Uint8Array( new ArrayBuffer( text.length ) );
199 |
200 | for ( var i = 0, il = text.length; i < il; i ++ ) {
201 |
202 | var value = text.charCodeAt( i );
203 |
204 | // Replacing multi-byte character with space(0x20).
205 | array[ i ] = value > 0xFF ? 0x20 : value;
206 |
207 | }
208 |
209 | return array.buffer;
210 |
211 | }
212 |
213 | /**
214 | * Is identity matrix
215 | *
216 | * @param {Matrix4} matrix
217 | * @returns {Boolean} Returns true, if parameter is identity matrix
218 | */
219 | function isIdentityMatrix( matrix ) {
220 |
221 | return equalArray( matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
222 |
223 | }
224 |
225 | /**
226 | * Get the min and max vectors from the given attribute
227 | * @param {BufferAttribute} attribute Attribute to find the min/max in range from start to start + count
228 | * @param {Integer} start
229 | * @param {Integer} count
230 | * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components)
231 | */
232 | function getMinMax( attribute, start, count ) {
233 |
234 | var output = {
235 |
236 | min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
237 | max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )
238 |
239 | };
240 |
241 | for ( var i = start; i < start + count; i ++ ) {
242 |
243 | for ( var a = 0; a < attribute.itemSize; a ++ ) {
244 |
245 | var value;
246 |
247 | if ( attribute.itemSize > 4 ) {
248 |
249 | // no support for interleaved data for itemSize > 4
250 |
251 | value = attribute.array[ i * attribute.itemSize + a ];
252 |
253 | } else {
254 |
255 | if ( a === 0 ) value = attribute.getX( i );
256 | else if ( a === 1 ) value = attribute.getY( i );
257 | else if ( a === 2 ) value = attribute.getZ( i );
258 | else if ( a === 3 ) value = attribute.getW( i );
259 |
260 | }
261 |
262 | output.min[ a ] = Math.min( output.min[ a ], value );
263 | output.max[ a ] = Math.max( output.max[ a ], value );
264 |
265 | }
266 |
267 | }
268 |
269 | return output;
270 |
271 | }
272 |
273 | /**
274 | * Get the required size + padding for a buffer, rounded to the next 4-byte boundary.
275 | * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment
276 | *
277 | * @param {Integer} bufferSize The size the original buffer.
278 | * @returns {Integer} new buffer size with required padding.
279 | *
280 | */
281 | function getPaddedBufferSize( bufferSize ) {
282 |
283 | return Math.ceil( bufferSize / 4 ) * 4;
284 |
285 | }
286 |
287 | /**
288 | * Returns a buffer aligned to 4-byte boundary.
289 | *
290 | * @param {ArrayBuffer} arrayBuffer Buffer to pad
291 | * @param {Integer} paddingByte (Optional)
292 | * @returns {ArrayBuffer} The same buffer if it's already aligned to 4-byte boundary or a new buffer
293 | */
294 | function getPaddedArrayBuffer( arrayBuffer, paddingByte ) {
295 |
296 | paddingByte = paddingByte || 0;
297 |
298 | var paddedLength = getPaddedBufferSize( arrayBuffer.byteLength );
299 |
300 | if ( paddedLength !== arrayBuffer.byteLength ) {
301 |
302 | var array = new Uint8Array( paddedLength );
303 | array.set( new Uint8Array( arrayBuffer ) );
304 |
305 | if ( paddingByte !== 0 ) {
306 |
307 | for ( var i = arrayBuffer.byteLength; i < paddedLength; i ++ ) {
308 |
309 | array[ i ] = paddingByte;
310 |
311 | }
312 |
313 | }
314 |
315 | return array.buffer;
316 |
317 | }
318 |
319 | return arrayBuffer;
320 |
321 | }
322 |
323 | var cachedCanvas = null;
324 |
325 | /**
326 | * Writer
327 | */
328 | function GLTFWriter() {
329 |
330 | this.plugins = [];
331 |
332 | this.options = {};
333 | this.pending = [];
334 | this.buffers = [];
335 |
336 | this.byteOffset = 0;
337 | this.buffers = [];
338 | this.nodeMap = new Map();
339 | this.skins = [];
340 | this.extensionsUsed = {};
341 |
342 | this.uids = new Map();
343 | this.uid = 0;
344 |
345 | this.json = {
346 | asset: {
347 | version: '2.0',
348 | generator: 'THREE.GLTFExporter'
349 | }
350 | };
351 |
352 | this.cache = {
353 | meshes: new Map(),
354 | attributes: new Map(),
355 | attributesNormalized: new Map(),
356 | materials: new Map(),
357 | textures: new Map(),
358 | images: new Map()
359 | };
360 |
361 | }
362 |
363 | GLTFWriter.prototype = {
364 |
365 | constructor: GLTFWriter,
366 |
367 | setPlugins: function ( plugins ) {
368 |
369 | this.plugins = plugins;
370 |
371 | },
372 |
373 | /**
374 | * Parse scenes and generate GLTF output
375 | * @param {Scene or [THREE.Scenes]} input Scene or Array of THREE.Scenes
376 | * @param {Function} onDone Callback on completed
377 | * @param {Object} options options
378 | */
379 | write: function ( input, onDone, options ) {
380 |
381 | this.options = Object.assign( {}, {
382 | // default options
383 | binary: false,
384 | trs: false,
385 | onlyVisible: true,
386 | truncateDrawRange: true,
387 | embedImages: true,
388 | maxTextureSize: Infinity,
389 | animations: [],
390 | includeCustomExtensions: false
391 | }, options );
392 |
393 | if ( this.options.animations.length > 0 ) {
394 |
395 | // Only TRS properties, and not matrices, may be targeted by animation.
396 | this.options.trs = true;
397 |
398 | }
399 |
400 | this.processInput( input );
401 |
402 | var writer = this;
403 |
404 | Promise.all( this.pending ).then( function () {
405 |
406 | var buffers = writer.buffers;
407 | var json = writer.json;
408 | var options = writer.options;
409 | var extensionsUsed = writer.extensionsUsed;
410 |
411 | // Merge buffers.
412 | var blob = new Blob( buffers, { type: 'application/octet-stream' } );
413 |
414 | // Declare extensions.
415 | var extensionsUsedList = Object.keys( extensionsUsed );
416 |
417 | if ( extensionsUsedList.length > 0 ) json.extensionsUsed = extensionsUsedList;
418 |
419 | // Update bytelength of the single buffer.
420 | if ( json.buffers && json.buffers.length > 0 ) json.buffers[ 0 ].byteLength = blob.size;
421 |
422 | if ( options.binary === true ) {
423 |
424 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
425 |
426 | var reader = new window.FileReader();
427 | reader.readAsArrayBuffer( blob );
428 | reader.onloadend = function () {
429 |
430 | // Binary chunk.
431 | var binaryChunk = getPaddedArrayBuffer( reader.result );
432 | var binaryChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
433 | binaryChunkPrefix.setUint32( 0, binaryChunk.byteLength, true );
434 | binaryChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_BIN, true );
435 |
436 | // JSON chunk.
437 | var jsonChunk = getPaddedArrayBuffer( stringToArrayBuffer( JSON.stringify( json ) ), 0x20 );
438 | var jsonChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
439 | jsonChunkPrefix.setUint32( 0, jsonChunk.byteLength, true );
440 | jsonChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_JSON, true );
441 |
442 | // GLB header.
443 | var header = new ArrayBuffer( GLB_HEADER_BYTES );
444 | var headerView = new DataView( header );
445 | headerView.setUint32( 0, GLB_HEADER_MAGIC, true );
446 | headerView.setUint32( 4, GLB_VERSION, true );
447 | var totalByteLength = GLB_HEADER_BYTES
448 | + jsonChunkPrefix.byteLength + jsonChunk.byteLength
449 | + binaryChunkPrefix.byteLength + binaryChunk.byteLength;
450 | headerView.setUint32( 8, totalByteLength, true );
451 |
452 | var glbBlob = new Blob( [
453 | header,
454 | jsonChunkPrefix,
455 | jsonChunk,
456 | binaryChunkPrefix,
457 | binaryChunk
458 | ], { type: 'application/octet-stream' } );
459 |
460 | var glbReader = new window.FileReader();
461 | glbReader.readAsArrayBuffer( glbBlob );
462 | glbReader.onloadend = function () {
463 |
464 | onDone( glbReader.result );
465 |
466 | };
467 |
468 | };
469 |
470 | } else {
471 |
472 | if ( json.buffers && json.buffers.length > 0 ) {
473 |
474 | var reader = new window.FileReader();
475 | reader.readAsDataURL( blob );
476 | reader.onloadend = function () {
477 |
478 | var base64data = reader.result;
479 | json.buffers[ 0 ].uri = base64data;
480 | onDone( json );
481 |
482 | };
483 |
484 | } else {
485 |
486 | onDone( json );
487 |
488 | }
489 |
490 | }
491 |
492 | } );
493 |
494 | },
495 |
496 | /**
497 | * Serializes a userData.
498 | *
499 | * @param {THREE.Object3D|THREE.Material} object
500 | * @param {Object} objectDef
501 | */
502 | serializeUserData: function ( object, objectDef ) {
503 |
504 | if ( Object.keys( object.userData ).length === 0 ) return;
505 |
506 | var options = this.options;
507 | var extensionsUsed = this.extensionsUsed;
508 |
509 | try {
510 |
511 | var json = JSON.parse( JSON.stringify( object.userData ) );
512 |
513 | if ( options.includeCustomExtensions && json.gltfExtensions ) {
514 |
515 | if ( objectDef.extensions === undefined ) objectDef.extensions = {};
516 |
517 | for ( var extensionName in json.gltfExtensions ) {
518 |
519 | objectDef.extensions[ extensionName ] = json.gltfExtensions[ extensionName ];
520 | extensionsUsed[ extensionName ] = true;
521 |
522 | }
523 |
524 | delete json.gltfExtensions;
525 |
526 | }
527 |
528 | if ( Object.keys( json ).length > 0 ) objectDef.extras = json;
529 |
530 | } catch ( error ) {
531 |
532 | console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' +
533 | 'won\'t be serialized because of JSON.stringify error - ' + error.message );
534 |
535 | }
536 |
537 | },
538 |
539 | /**
540 | * Assign and return a temporal unique id for an object
541 | * especially which doesn't have .uuid
542 | * @param {Object} object
543 | * @return {Integer}
544 | */
545 | getUID: function ( object ) {
546 |
547 | if ( ! this.uids.has( object ) ) this.uids.set( object, this.uid ++ );
548 |
549 | return this.uids.get( object );
550 |
551 | },
552 |
553 | /**
554 | * Checks if normal attribute values are normalized.
555 | *
556 | * @param {BufferAttribute} normal
557 | * @returns {Boolean}
558 | */
559 | isNormalizedNormalAttribute: function ( normal ) {
560 |
561 | var cache = this.cache;
562 |
563 | if ( cache.attributesNormalized.has( normal ) ) return false;
564 |
565 | var v = new Vector3();
566 |
567 | for ( var i = 0, il = normal.count; i < il; i ++ ) {
568 |
569 | // 0.0005 is from glTF-validator
570 | if ( Math.abs( v.fromBufferAttribute( normal, i ).length() - 1.0 ) > 0.0005 ) return false;
571 |
572 | }
573 |
574 | return true;
575 |
576 | },
577 |
578 | /**
579 | * Creates normalized normal buffer attribute.
580 | *
581 | * @param {BufferAttribute} normal
582 | * @returns {BufferAttribute}
583 | *
584 | */
585 | createNormalizedNormalAttribute: function ( normal ) {
586 |
587 | var cache = this.cache;
588 |
589 | if ( cache.attributesNormalized.has( normal ) ) return cache.attributesNormalized.get( normal );
590 |
591 | var attribute = normal.clone();
592 | var v = new Vector3();
593 |
594 | for ( var i = 0, il = attribute.count; i < il; i ++ ) {
595 |
596 | v.fromBufferAttribute( attribute, i );
597 |
598 | if ( v.x === 0 && v.y === 0 && v.z === 0 ) {
599 |
600 | // if values can't be normalized set (1, 0, 0)
601 | v.setX( 1.0 );
602 |
603 | } else {
604 |
605 | v.normalize();
606 |
607 | }
608 |
609 | attribute.setXYZ( i, v.x, v.y, v.z );
610 |
611 | }
612 |
613 | cache.attributesNormalized.set( normal, attribute );
614 |
615 | return attribute;
616 |
617 | },
618 |
619 | /**
620 | * Applies a texture transform, if present, to the map definition. Requires
621 | * the KHR_texture_transform extension.
622 | *
623 | * @param {Object} mapDef
624 | * @param {THREE.Texture} texture
625 | */
626 | applyTextureTransform: function ( mapDef, texture ) {
627 |
628 | var didTransform = false;
629 | var transformDef = {};
630 |
631 | if ( texture.offset.x !== 0 || texture.offset.y !== 0 ) {
632 |
633 | transformDef.offset = texture.offset.toArray();
634 | didTransform = true;
635 |
636 | }
637 |
638 | if ( texture.rotation !== 0 ) {
639 |
640 | transformDef.rotation = texture.rotation;
641 | didTransform = true;
642 |
643 | }
644 |
645 | if ( texture.repeat.x !== 1 || texture.repeat.y !== 1 ) {
646 |
647 | transformDef.scale = texture.repeat.toArray();
648 | didTransform = true;
649 |
650 | }
651 |
652 | if ( didTransform ) {
653 |
654 | mapDef.extensions = mapDef.extensions || {};
655 | mapDef.extensions[ 'KHR_texture_transform' ] = transformDef;
656 | this.extensionsUsed[ 'KHR_texture_transform' ] = true;
657 |
658 | }
659 |
660 | },
661 |
662 | /**
663 | * Process a buffer to append to the default one.
664 | * @param {ArrayBuffer} buffer
665 | * @return {Integer}
666 | */
667 | processBuffer: function ( buffer ) {
668 |
669 | var json = this.json;
670 | var buffers = this.buffers;
671 |
672 | if ( ! json.buffers ) json.buffers = [ { byteLength: 0 } ];
673 |
674 | // All buffers are merged before export.
675 | buffers.push( buffer );
676 |
677 | return 0;
678 |
679 | },
680 |
681 | /**
682 | * Process and generate a BufferView
683 | * @param {BufferAttribute} attribute
684 | * @param {number} componentType
685 | * @param {number} start
686 | * @param {number} count
687 | * @param {number} target (Optional) Target usage of the BufferView
688 | * @return {Object}
689 | */
690 | processBufferView: function ( attribute, componentType, start, count, target ) {
691 |
692 | var json = this.json;
693 |
694 | if ( ! json.bufferViews ) json.bufferViews = [];
695 |
696 | // Create a new dataview and dump the attribute's array into it
697 |
698 | var componentSize;
699 |
700 | if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {
701 |
702 | componentSize = 1;
703 |
704 | } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
705 |
706 | componentSize = 2;
707 |
708 | } else {
709 |
710 | componentSize = 4;
711 |
712 | }
713 |
714 | var byteLength = getPaddedBufferSize( count * attribute.itemSize * componentSize );
715 | var dataView = new DataView( new ArrayBuffer( byteLength ) );
716 | var offset = 0;
717 |
718 | for ( var i = start; i < start + count; i ++ ) {
719 |
720 | for ( var a = 0; a < attribute.itemSize; a ++ ) {
721 |
722 | var value;
723 |
724 | if ( attribute.itemSize > 4 ) {
725 |
726 | // no support for interleaved data for itemSize > 4
727 |
728 | value = attribute.array[ i * attribute.itemSize + a ];
729 |
730 | } else {
731 |
732 | if ( a === 0 ) value = attribute.getX( i );
733 | else if ( a === 1 ) value = attribute.getY( i );
734 | else if ( a === 2 ) value = attribute.getZ( i );
735 | else if ( a === 3 ) value = attribute.getW( i );
736 |
737 | }
738 |
739 | if ( componentType === WEBGL_CONSTANTS.FLOAT ) {
740 |
741 | dataView.setFloat32( offset, value, true );
742 |
743 | } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_INT ) {
744 |
745 | dataView.setUint32( offset, value, true );
746 |
747 | } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
748 |
749 | dataView.setUint16( offset, value, true );
750 |
751 | } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {
752 |
753 | dataView.setUint8( offset, value );
754 |
755 | }
756 |
757 | offset += componentSize;
758 |
759 | }
760 |
761 | }
762 |
763 | var bufferViewDef = {
764 |
765 | buffer: this.processBuffer( dataView.buffer ),
766 | byteOffset: this.byteOffset,
767 | byteLength: byteLength
768 |
769 | };
770 |
771 | if ( target !== undefined ) bufferViewDef.target = target;
772 |
773 | if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) {
774 |
775 | // Only define byteStride for vertex attributes.
776 | bufferViewDef.byteStride = attribute.itemSize * componentSize;
777 |
778 | }
779 |
780 | this.byteOffset += byteLength;
781 |
782 | json.bufferViews.push( bufferViewDef );
783 |
784 | // @TODO Merge bufferViews where possible.
785 | var output = {
786 |
787 | id: json.bufferViews.length - 1,
788 | byteLength: 0
789 |
790 | };
791 |
792 | return output;
793 |
794 | },
795 |
796 | /**
797 | * Process and generate a BufferView from an image Blob.
798 | * @param {Blob} blob
799 | * @return {Promise}
800 | */
801 | processBufferViewImage: function ( blob ) {
802 |
803 | var writer = this;
804 | var json = writer.json;
805 |
806 | if ( ! json.bufferViews ) json.bufferViews = [];
807 |
808 | return new Promise( function ( resolve ) {
809 |
810 | var reader = new window.FileReader();
811 | reader.readAsArrayBuffer( blob );
812 | reader.onloadend = function () {
813 |
814 | var buffer = getPaddedArrayBuffer( reader.result );
815 |
816 | var bufferViewDef = {
817 | buffer: writer.processBuffer( buffer ),
818 | byteOffset: writer.byteOffset,
819 | byteLength: buffer.byteLength
820 | };
821 |
822 | writer.byteOffset += buffer.byteLength;
823 | resolve( json.bufferViews.push( bufferViewDef ) - 1 );
824 |
825 | };
826 |
827 | } );
828 |
829 | },
830 |
831 | /**
832 | * Process attribute to generate an accessor
833 | * @param {BufferAttribute} attribute Attribute to process
834 | * @param {THREE.BufferGeometry} geometry (Optional) Geometry used for truncated draw range
835 | * @param {Integer} start (Optional)
836 | * @param {Integer} count (Optional)
837 | * @return {Integer|null} Index of the processed accessor on the "accessors" array
838 | */
839 | processAccessor: function ( attribute, geometry, start, count ) {
840 |
841 | var options = this.options;
842 | var json = this.json;
843 |
844 | var types = {
845 |
846 | 1: 'SCALAR',
847 | 2: 'VEC2',
848 | 3: 'VEC3',
849 | 4: 'VEC4',
850 | 16: 'MAT4'
851 |
852 | };
853 |
854 | var componentType;
855 |
856 | // Detect the component type of the attribute array (float, uint or ushort)
857 | if ( attribute.array.constructor === Float32Array ) {
858 |
859 | componentType = WEBGL_CONSTANTS.FLOAT;
860 |
861 | } else if ( attribute.array.constructor === Uint32Array ) {
862 |
863 | componentType = WEBGL_CONSTANTS.UNSIGNED_INT;
864 |
865 | } else if ( attribute.array.constructor === Uint16Array ) {
866 |
867 | componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT;
868 |
869 | } else if ( attribute.array.constructor === Uint8Array ) {
870 |
871 | componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE;
872 |
873 | } else {
874 |
875 | throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type.' );
876 |
877 | }
878 |
879 | if ( start === undefined ) start = 0;
880 | if ( count === undefined ) count = attribute.count;
881 |
882 | // @TODO Indexed buffer geometry with drawRange not supported yet
883 | if ( options.truncateDrawRange && geometry !== undefined && geometry.index === null ) {
884 |
885 | var end = start + count;
886 | var end2 = geometry.drawRange.count === Infinity
887 | ? attribute.count
888 | : geometry.drawRange.start + geometry.drawRange.count;
889 |
890 | start = Math.max( start, geometry.drawRange.start );
891 | count = Math.min( end, end2 ) - start;
892 |
893 | if ( count < 0 ) count = 0;
894 |
895 | }
896 |
897 | // Skip creating an accessor if the attribute doesn't have data to export
898 | if ( count === 0 ) return null;
899 |
900 | var minMax = getMinMax( attribute, start, count );
901 | var bufferViewTarget;
902 |
903 | // If geometry isn't provided, don't infer the target usage of the bufferView. For
904 | // animation samplers, target must not be set.
905 | if ( geometry !== undefined ) {
906 |
907 | bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER;
908 |
909 | }
910 |
911 | var bufferView = this.processBufferView( attribute, componentType, start, count, bufferViewTarget );
912 |
913 | var accessorDef = {
914 |
915 | bufferView: bufferView.id,
916 | byteOffset: bufferView.byteOffset,
917 | componentType: componentType,
918 | count: count,
919 | max: minMax.max,
920 | min: minMax.min,
921 | type: types[ attribute.itemSize ]
922 |
923 | };
924 |
925 | if ( attribute.normalized === true ) accessorDef.normalized = true;
926 | if ( ! json.accessors ) json.accessors = [];
927 |
928 | return json.accessors.push( accessorDef ) - 1;
929 |
930 | },
931 |
932 | /**
933 | * Process image
934 | * @param {Image} image to process
935 | * @param {Integer} format of the image (e.g. RGBFormat, RGBAFormat etc)
936 | * @param {Boolean} flipY before writing out the image
937 | * @return {Integer} Index of the processed texture in the "images" array
938 | */
939 | processImage: function ( image, format, flipY ) {
940 |
941 | var writer = this;
942 | var cache = writer.cache;
943 | var json = writer.json;
944 | var options = writer.options;
945 | var pending = writer.pending;
946 |
947 | if ( ! cache.images.has( image ) ) cache.images.set( image, {} );
948 |
949 | var cachedImages = cache.images.get( image );
950 | var mimeType = format === RGBAFormat ? 'image/png' : 'image/jpeg';
951 | var key = mimeType + ':flipY/' + flipY.toString();
952 |
953 | if ( cachedImages[ key ] !== undefined ) return cachedImages[ key ];
954 |
955 | if ( ! json.images ) json.images = [];
956 |
957 | var imageDef = { mimeType: mimeType };
958 |
959 | if ( options.embedImages ) {
960 |
961 | var canvas = cachedCanvas = cachedCanvas || document.createElement( 'canvas' );
962 |
963 | canvas.width = Math.min( image.width, options.maxTextureSize );
964 | canvas.height = Math.min( image.height, options.maxTextureSize );
965 |
966 | var ctx = canvas.getContext( '2d' );
967 |
968 | if ( flipY === true ) {
969 |
970 | ctx.translate( 0, canvas.height );
971 | ctx.scale( 1, - 1 );
972 |
973 | }
974 |
975 | if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
976 | ( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
977 | ( typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas ) ||
978 | ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {
979 |
980 | ctx.drawImage( image, 0, 0, canvas.width, canvas.height );
981 |
982 | } else {
983 |
984 | if ( format !== RGBAFormat && format !== RGBFormat ) {
985 |
986 | console.error( 'GLTFExporter: Only RGB and RGBA formats are supported.' );
987 |
988 | }
989 |
990 | if ( image.width > options.maxTextureSize || image.height > options.maxTextureSize ) {
991 |
992 | console.warn( 'GLTFExporter: Image size is bigger than maxTextureSize', image );
993 |
994 | }
995 |
996 | var data = image.data;
997 |
998 | if ( format === RGBFormat ) {
999 |
1000 | data = new Uint8ClampedArray( image.height * image.width * 4 );
1001 |
1002 | for ( var i = 0, j = 0; i < data.length; i += 4, j += 3 ) {
1003 |
1004 | data[ i + 0 ] = image.data[ j + 0 ];
1005 | data[ i + 1 ] = image.data[ j + 1 ];
1006 | data[ i + 2 ] = image.data[ j + 2 ];
1007 | data[ i + 3 ] = 255;
1008 |
1009 | }
1010 |
1011 | }
1012 |
1013 | ctx.putImageData( new ImageData( data, image.width, image.height ), 0, 0 );
1014 |
1015 | }
1016 |
1017 | if ( options.binary === true ) {
1018 |
1019 | pending.push( new Promise( function ( resolve ) {
1020 |
1021 | canvas.toBlob( function ( blob ) {
1022 |
1023 | writer.processBufferViewImage( blob ).then( function ( bufferViewIndex ) {
1024 |
1025 | imageDef.bufferView = bufferViewIndex;
1026 | resolve();
1027 |
1028 | } );
1029 |
1030 | }, mimeType );
1031 |
1032 | } ) );
1033 |
1034 | } else {
1035 |
1036 | imageDef.uri = canvas.toDataURL( mimeType );
1037 |
1038 | }
1039 |
1040 | } else {
1041 |
1042 | imageDef.uri = image.src;
1043 |
1044 | }
1045 |
1046 | var index = json.images.push( imageDef ) - 1;
1047 | cachedImages[ key ] = index;
1048 | return index;
1049 |
1050 | },
1051 |
1052 | /**
1053 | * Process sampler
1054 | * @param {Texture} map Texture to process
1055 | * @return {Integer} Index of the processed texture in the "samplers" array
1056 | */
1057 | processSampler: function ( map ) {
1058 |
1059 | var json = this.json;
1060 |
1061 | if ( ! json.samplers ) json.samplers = [];
1062 |
1063 | var samplerDef = {
1064 | magFilter: THREE_TO_WEBGL[ map.magFilter ],
1065 | minFilter: THREE_TO_WEBGL[ map.minFilter ],
1066 | wrapS: THREE_TO_WEBGL[ map.wrapS ],
1067 | wrapT: THREE_TO_WEBGL[ map.wrapT ]
1068 | };
1069 |
1070 | return json.samplers.push( samplerDef ) - 1;
1071 |
1072 | },
1073 |
1074 | /**
1075 | * Process texture
1076 | * @param {Texture} map Map to process
1077 | * @return {Integer} Index of the processed texture in the "textures" array
1078 | */
1079 | processTexture: function ( map ) {
1080 |
1081 | var cache = this.cache;
1082 | var json = this.json;
1083 |
1084 | if ( cache.textures.has( map ) ) return cache.textures.get( map );
1085 |
1086 | if ( ! json.textures ) json.textures = [];
1087 |
1088 | var textureDef = {
1089 | sampler: this.processSampler( map ),
1090 | source: this.processImage( map.image, map.format, map.flipY )
1091 | };
1092 |
1093 | if ( map.name ) textureDef.name = map.name;
1094 |
1095 | this._invokeAll( function ( ext ) {
1096 |
1097 | ext.writeTexture && ext.writeTexture( map, textureDef );
1098 |
1099 | } );
1100 |
1101 | var index = json.textures.push( textureDef ) - 1;
1102 | cache.textures.set( map, index );
1103 | return index;
1104 |
1105 | },
1106 |
1107 | /**
1108 | * Process material
1109 | * @param {THREE.Material} material Material to process
1110 | * @return {Integer|null} Index of the processed material in the "materials" array
1111 | */
1112 | processMaterial: function ( material ) {
1113 |
1114 | var cache = this.cache;
1115 | var json = this.json;
1116 |
1117 | if ( cache.materials.has( material ) ) return cache.materials.get( material );
1118 |
1119 | if ( material.isShaderMaterial ) {
1120 |
1121 | console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' );
1122 | return null;
1123 |
1124 | }
1125 |
1126 | if ( ! json.materials ) json.materials = [];
1127 |
1128 | // @QUESTION Should we avoid including any attribute that has the default value?
1129 | var materialDef = { pbrMetallicRoughness: {} };
1130 |
1131 | if ( ! material.isMeshStandardMaterial || ! material.isMeshBasicMaterial ) {
1132 |
1133 | console.warn( 'GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.' );
1134 |
1135 | }
1136 |
1137 | // pbrMetallicRoughness.baseColorFactor
1138 | var color = material.color.toArray().concat( [ material.opacity ] );
1139 |
1140 | if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) {
1141 |
1142 | materialDef.pbrMetallicRoughness.baseColorFactor = color;
1143 |
1144 | }
1145 |
1146 | if ( material.isMeshStandardMaterial ) {
1147 |
1148 | materialDef.pbrMetallicRoughness.metallicFactor = material.metalness;
1149 | materialDef.pbrMetallicRoughness.roughnessFactor = material.roughness;
1150 |
1151 | } else {
1152 |
1153 | materialDef.pbrMetallicRoughness.metallicFactor = 0.5;
1154 | materialDef.pbrMetallicRoughness.roughnessFactor = 0.5;
1155 |
1156 | }
1157 |
1158 | // pbrMetallicRoughness.metallicRoughnessTexture
1159 | if ( material.metalnessMap || material.roughnessMap ) {
1160 |
1161 | if ( material.metalnessMap === material.roughnessMap ) {
1162 |
1163 | var metalRoughMapDef = { index: this.processTexture( material.metalnessMap ) };
1164 | this.applyTextureTransform( metalRoughMapDef, material.metalnessMap );
1165 | materialDef.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef;
1166 |
1167 | } else {
1168 |
1169 | console.warn( 'THREE.GLTFExporter: Ignoring metalnessMap and roughnessMap because they are not the same Texture.' );
1170 |
1171 | }
1172 |
1173 | }
1174 |
1175 | // pbrMetallicRoughness.baseColorTexture or pbrSpecularGlossiness diffuseTexture
1176 | if ( material.map ) {
1177 |
1178 | var baseColorMapDef = { index: this.processTexture( material.map ) };
1179 | this.applyTextureTransform( baseColorMapDef, material.map );
1180 | materialDef.pbrMetallicRoughness.baseColorTexture = baseColorMapDef;
1181 |
1182 | }
1183 |
1184 | if ( material.emissive ) {
1185 |
1186 | // emissiveFactor
1187 | var emissive = material.emissive.clone().multiplyScalar( material.emissiveIntensity ).toArray();
1188 |
1189 | if ( ! equalArray( emissive, [ 0, 0, 0 ] ) ) {
1190 |
1191 | materialDef.emissiveFactor = emissive;
1192 |
1193 | }
1194 |
1195 | // emissiveTexture
1196 | if ( material.emissiveMap ) {
1197 |
1198 | var emissiveMapDef = { index: this.processTexture( material.emissiveMap ) };
1199 | this.applyTextureTransform( emissiveMapDef, material.emissiveMap );
1200 | materialDef.emissiveTexture = emissiveMapDef;
1201 |
1202 | }
1203 |
1204 | }
1205 |
1206 | // normalTexture
1207 | if ( material.normalMap ) {
1208 |
1209 | var normalMapDef = { index: this.processTexture( material.normalMap ) };
1210 |
1211 | if ( material.normalScale && material.normalScale.x !== - 1 ) {
1212 |
1213 | if ( material.normalScale.x !== material.normalScale.y ) {
1214 |
1215 | console.warn( 'THREE.GLTFExporter: Normal scale components are different, ignoring Y and exporting X.' );
1216 |
1217 | }
1218 |
1219 | normalMapDef.scale = material.normalScale.x;
1220 |
1221 | }
1222 |
1223 | this.applyTextureTransform( normalMapDef, material.normalMap );
1224 | materialDef.normalTexture = normalMapDef;
1225 |
1226 | }
1227 |
1228 | // occlusionTexture
1229 | if ( material.aoMap ) {
1230 |
1231 | var occlusionMapDef = {
1232 | index: this.processTexture( material.aoMap ),
1233 | texCoord: 1
1234 | };
1235 |
1236 | if ( material.aoMapIntensity !== 1.0 ) {
1237 |
1238 | occlusionMapDef.strength = material.aoMapIntensity;
1239 |
1240 | }
1241 |
1242 | this.applyTextureTransform( occlusionMapDef, material.aoMap );
1243 | materialDef.occlusionTexture = occlusionMapDef;
1244 |
1245 | }
1246 |
1247 | // alphaMode
1248 | if ( material.transparent ) {
1249 |
1250 | materialDef.alphaMode = 'BLEND';
1251 |
1252 | } else {
1253 |
1254 | if ( material.alphaTest > 0.0 ) {
1255 |
1256 | materialDef.alphaMode = 'MASK';
1257 | materialDef.alphaCutoff = material.alphaTest;
1258 |
1259 | }
1260 |
1261 | }
1262 |
1263 | // doubleSided
1264 | if ( material.side === DoubleSide ) materialDef.doubleSided = true;
1265 | if ( material.name !== '' ) materialDef.name = material.name;
1266 |
1267 | this.serializeUserData( material, materialDef );
1268 |
1269 | this._invokeAll( function ( ext ) {
1270 |
1271 | ext.writeMaterial && ext.writeMaterial( material, materialDef );
1272 |
1273 | } );
1274 |
1275 | var index = json.materials.push( materialDef ) - 1;
1276 | cache.materials.set( material, index );
1277 | return index;
1278 |
1279 | },
1280 |
1281 | /**
1282 | * Process mesh
1283 | * @param {THREE.Mesh} mesh Mesh to process
1284 | * @return {Integer|null} Index of the processed mesh in the "meshes" array
1285 | */
1286 | processMesh: function ( mesh ) {
1287 |
1288 | var cache = this.cache;
1289 | var json = this.json;
1290 |
1291 | var meshCacheKeyParts = [ mesh.geometry.uuid ];
1292 |
1293 | if ( Array.isArray( mesh.material ) ) {
1294 |
1295 | for ( var i = 0, l = mesh.material.length; i < l; i ++ ) {
1296 |
1297 | meshCacheKeyParts.push( mesh.material[ i ].uuid );
1298 |
1299 | }
1300 |
1301 | } else {
1302 |
1303 | meshCacheKeyParts.push( mesh.material.uuid );
1304 |
1305 | }
1306 |
1307 | var meshCacheKey = meshCacheKeyParts.join( ':' );
1308 |
1309 | if ( cache.meshes.has( meshCacheKey ) ) return cache.meshes.get( meshCacheKey );
1310 |
1311 | var geometry = mesh.geometry;
1312 | var mode;
1313 |
1314 | // Use the correct mode
1315 | if ( mesh.isLineSegments ) {
1316 |
1317 | mode = WEBGL_CONSTANTS.LINES;
1318 |
1319 | } else if ( mesh.isLineLoop ) {
1320 |
1321 | mode = WEBGL_CONSTANTS.LINE_LOOP;
1322 |
1323 | } else if ( mesh.isLine ) {
1324 |
1325 | mode = WEBGL_CONSTANTS.LINE_STRIP;
1326 |
1327 | } else if ( mesh.isPoints ) {
1328 |
1329 | mode = WEBGL_CONSTANTS.POINTS;
1330 |
1331 | } else {
1332 |
1333 | mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;
1334 |
1335 | }
1336 |
1337 | if ( geometry.isBufferGeometry !== true ) {
1338 |
1339 | throw new Error( 'THREE.GLTFExporter: Geometry is not of type THREE.BufferGeometry.' );
1340 |
1341 | }
1342 |
1343 | var meshDef = {};
1344 | var attributes = {};
1345 | var primitives = [];
1346 | var targets = [];
1347 |
1348 | // Conversion between attributes names in threejs and gltf spec
1349 | var nameConversion = {
1350 | uv: 'TEXCOORD_0',
1351 | uv2: 'TEXCOORD_1',
1352 | color: 'COLOR_0',
1353 | skinWeight: 'WEIGHTS_0',
1354 | skinIndex: 'JOINTS_0'
1355 | };
1356 |
1357 | var originalNormal = geometry.getAttribute( 'normal' );
1358 |
1359 | if ( originalNormal !== undefined && ! this.isNormalizedNormalAttribute( originalNormal ) ) {
1360 |
1361 | console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' );
1362 |
1363 | geometry.setAttribute( 'normal', this.createNormalizedNormalAttribute( originalNormal ) );
1364 |
1365 | }
1366 |
1367 | // @QUESTION Detect if .vertexColors = true?
1368 | // For every attribute create an accessor
1369 | var modifiedAttribute = null;
1370 |
1371 | for ( var attributeName in geometry.attributes ) {
1372 |
1373 | // Ignore morph target attributes, which are exported later.
1374 | if ( attributeName.substr( 0, 5 ) === 'morph' ) continue;
1375 |
1376 | var attribute = geometry.attributes[ attributeName ];
1377 | attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
1378 |
1379 | // Prefix all geometry attributes except the ones specifically
1380 | // listed in the spec; non-spec attributes are considered custom.
1381 | var validVertexAttributes =
1382 | /^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/;
1383 |
1384 | if ( ! validVertexAttributes.test( attributeName ) ) attributeName = '_' + attributeName;
1385 |
1386 | if ( cache.attributes.has( this.getUID( attribute ) ) ) {
1387 |
1388 | attributes[ attributeName ] = cache.attributes.get( this.getUID( attribute ) );
1389 | continue;
1390 |
1391 | }
1392 |
1393 | // JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT.
1394 | modifiedAttribute = null;
1395 | var array = attribute.array;
1396 |
1397 | if ( attributeName === 'JOINTS_0' &&
1398 | ! ( array instanceof Uint16Array ) &&
1399 | ! ( array instanceof Uint8Array ) ) {
1400 |
1401 | console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' );
1402 | modifiedAttribute = new BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized );
1403 |
1404 | }
1405 |
1406 | var accessor = this.processAccessor( modifiedAttribute || attribute, geometry );
1407 |
1408 | if ( accessor !== null ) {
1409 |
1410 | attributes[ attributeName ] = accessor;
1411 | cache.attributes.set( this.getUID( attribute ), accessor );
1412 |
1413 | }
1414 |
1415 | }
1416 |
1417 | if ( originalNormal !== undefined ) geometry.setAttribute( 'normal', originalNormal );
1418 |
1419 | // Skip if no exportable attributes found
1420 | if ( Object.keys( attributes ).length === 0 ) return null;
1421 |
1422 | // Morph targets
1423 | if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {
1424 |
1425 | var weights = [];
1426 | var targetNames = [];
1427 | var reverseDictionary = {};
1428 |
1429 | if ( mesh.morphTargetDictionary !== undefined ) {
1430 |
1431 | for ( var key in mesh.morphTargetDictionary ) {
1432 |
1433 | reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;
1434 |
1435 | }
1436 |
1437 | }
1438 |
1439 | for ( var i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {
1440 |
1441 | var target = {};
1442 | var warned = false;
1443 |
1444 | for ( var attributeName in geometry.morphAttributes ) {
1445 |
1446 | // glTF 2.0 morph supports only POSITION/NORMAL/TANGENT.
1447 | // Three.js doesn't support TANGENT yet.
1448 |
1449 | if ( attributeName !== 'position' && attributeName !== 'normal' ) {
1450 |
1451 | if ( ! warned ) {
1452 |
1453 | console.warn( 'GLTFExporter: Only POSITION and NORMAL morph are supported.' );
1454 | warned = true;
1455 |
1456 | }
1457 |
1458 | continue;
1459 |
1460 | }
1461 |
1462 | var attribute = geometry.morphAttributes[ attributeName ][ i ];
1463 | var gltfAttributeName = attributeName.toUpperCase();
1464 |
1465 | // Three.js morph attribute has absolute values while the one of glTF has relative values.
1466 | //
1467 | // glTF 2.0 Specification:
1468 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets
1469 |
1470 | var baseAttribute = geometry.attributes[ attributeName ];
1471 |
1472 | if ( cache.attributes.has( this.getUID( attribute ) ) ) {
1473 |
1474 | target[ gltfAttributeName ] = cache.attributes.get( this.getUID( attribute ) );
1475 | continue;
1476 |
1477 | }
1478 |
1479 | // Clones attribute not to override
1480 | var relativeAttribute = attribute.clone();
1481 |
1482 | if ( ! geometry.morphTargetsRelative ) {
1483 |
1484 | for ( var j = 0, jl = attribute.count; j < jl; j ++ ) {
1485 |
1486 | relativeAttribute.setXYZ(
1487 | j,
1488 | attribute.getX( j ) - baseAttribute.getX( j ),
1489 | attribute.getY( j ) - baseAttribute.getY( j ),
1490 | attribute.getZ( j ) - baseAttribute.getZ( j )
1491 | );
1492 |
1493 | }
1494 |
1495 | }
1496 |
1497 | target[ gltfAttributeName ] = this.processAccessor( relativeAttribute, geometry );
1498 | cache.attributes.set( this.getUID( baseAttribute ), target[ gltfAttributeName ] );
1499 |
1500 | }
1501 |
1502 | targets.push( target );
1503 |
1504 | weights.push( mesh.morphTargetInfluences[ i ] );
1505 |
1506 | if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );
1507 |
1508 | }
1509 |
1510 | meshDef.weights = weights;
1511 |
1512 | if ( targetNames.length > 0 ) {
1513 |
1514 | meshDef.extras = {};
1515 | meshDef.extras.targetNames = targetNames;
1516 |
1517 | }
1518 |
1519 | }
1520 |
1521 | var isMultiMaterial = Array.isArray( mesh.material );
1522 |
1523 | if ( isMultiMaterial && geometry.groups.length === 0 ) return null;
1524 |
1525 | var materials = isMultiMaterial ? mesh.material : [ mesh.material ];
1526 | var groups = isMultiMaterial ? geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ];
1527 |
1528 | for ( var i = 0, il = groups.length; i < il; i ++ ) {
1529 |
1530 | var primitive = {
1531 | mode: mode,
1532 | attributes: attributes,
1533 | };
1534 |
1535 | this.serializeUserData( geometry, primitive );
1536 |
1537 | if ( targets.length > 0 ) primitive.targets = targets;
1538 |
1539 | if ( geometry.index !== null ) {
1540 |
1541 | var cacheKey = this.getUID( geometry.index );
1542 |
1543 | if ( groups[ i ].start !== undefined || groups[ i ].count !== undefined ) {
1544 |
1545 | cacheKey += ':' + groups[ i ].start + ':' + groups[ i ].count;
1546 |
1547 | }
1548 |
1549 | if ( cache.attributes.has( cacheKey ) ) {
1550 |
1551 | primitive.indices = cache.attributes.get( cacheKey );
1552 |
1553 | } else {
1554 |
1555 | primitive.indices = this.processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count );
1556 | cache.attributes.set( cacheKey, primitive.indices );
1557 |
1558 | }
1559 |
1560 | if ( primitive.indices === null ) delete primitive.indices;
1561 |
1562 | }
1563 |
1564 | var material = this.processMaterial( materials[ groups[ i ].materialIndex ] );
1565 |
1566 | if ( material !== null ) primitive.material = material;
1567 |
1568 | primitives.push( primitive );
1569 |
1570 | }
1571 |
1572 | meshDef.primitives = primitives;
1573 |
1574 | if ( ! json.meshes ) json.meshes = [];
1575 |
1576 | this._invokeAll( function ( ext ) {
1577 |
1578 | ext.writeMesh && ext.writeMesh( mesh, meshDef );
1579 |
1580 | } );
1581 |
1582 | var index = json.meshes.push( meshDef ) - 1;
1583 | cache.meshes.set( meshCacheKey, index );
1584 | return index;
1585 |
1586 | },
1587 |
1588 | /**
1589 | * Process camera
1590 | * @param {THREE.Camera} camera Camera to process
1591 | * @return {Integer} Index of the processed mesh in the "camera" array
1592 | */
1593 | processCamera: function ( camera ) {
1594 |
1595 | var json = this.json;
1596 |
1597 | if ( ! json.cameras ) json.cameras = [];
1598 |
1599 | var isOrtho = camera.isOrthographicCamera;
1600 |
1601 | var cameraDef = {
1602 | type: isOrtho ? 'orthographic' : 'perspective'
1603 | };
1604 |
1605 | if ( isOrtho ) {
1606 |
1607 | cameraDef.orthographic = {
1608 | xmag: camera.right * 2,
1609 | ymag: camera.top * 2,
1610 | zfar: camera.far <= 0 ? 0.001 : camera.far,
1611 | znear: camera.near < 0 ? 0 : camera.near
1612 | };
1613 |
1614 | } else {
1615 |
1616 | cameraDef.perspective = {
1617 | aspectRatio: camera.aspect,
1618 | yfov: MathUtils.degToRad( camera.fov ),
1619 | zfar: camera.far <= 0 ? 0.001 : camera.far,
1620 | znear: camera.near < 0 ? 0 : camera.near
1621 | };
1622 |
1623 | }
1624 |
1625 | // Question: Is saving "type" as name intentional?
1626 | if ( camera.name !== '' ) cameraDef.name = camera.type;
1627 |
1628 | return json.cameras.push( cameraDef ) - 1;
1629 |
1630 | },
1631 |
1632 | /**
1633 | * Creates glTF animation entry from AnimationClip object.
1634 | *
1635 | * Status:
1636 | * - Only properties listed in PATH_PROPERTIES may be animated.
1637 | *
1638 | * @param {THREE.AnimationClip} clip
1639 | * @param {THREE.Object3D} root
1640 | * @return {number|null}
1641 | */
1642 | processAnimation: function ( clip, root ) {
1643 |
1644 | var json = this.json;
1645 | var nodeMap = this.nodeMap;
1646 |
1647 | if ( ! json.animations ) json.animations = [];
1648 |
1649 | clip = GLTFExporter.Utils.mergeMorphTargetTracks( clip.clone(), root );
1650 |
1651 | var tracks = clip.tracks;
1652 | var channels = [];
1653 | var samplers = [];
1654 |
1655 | for ( var i = 0; i < tracks.length; ++ i ) {
1656 |
1657 | var track = tracks[ i ];
1658 | var trackBinding = PropertyBinding.parseTrackName( track.name );
1659 | var trackNode = PropertyBinding.findNode( root, trackBinding.nodeName );
1660 | var trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ];
1661 |
1662 | if ( trackBinding.objectName === 'bones' ) {
1663 |
1664 | if ( trackNode.isSkinnedMesh === true ) {
1665 |
1666 | trackNode = trackNode.skeleton.getBoneByName( trackBinding.objectIndex );
1667 |
1668 | } else {
1669 |
1670 | trackNode = undefined;
1671 |
1672 | }
1673 |
1674 | }
1675 |
1676 | if ( ! trackNode || ! trackProperty ) {
1677 |
1678 | console.warn( 'THREE.GLTFExporter: Could not export animation track "%s".', track.name );
1679 | return null;
1680 |
1681 | }
1682 |
1683 | var inputItemSize = 1;
1684 | var outputItemSize = track.values.length / track.times.length;
1685 |
1686 | if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {
1687 |
1688 | outputItemSize /= trackNode.morphTargetInfluences.length;
1689 |
1690 | }
1691 |
1692 | var interpolation;
1693 |
1694 | // @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE
1695 |
1696 | // Detecting glTF cubic spline interpolant by checking factory method's special property
1697 | // GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return
1698 | // valid value from .getInterpolation().
1699 | if ( track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true ) {
1700 |
1701 | interpolation = 'CUBICSPLINE';
1702 |
1703 | // itemSize of CUBICSPLINE keyframe is 9
1704 | // (VEC3 * 3: inTangent, splineVertex, and outTangent)
1705 | // but needs to be stored as VEC3 so dividing by 3 here.
1706 | outputItemSize /= 3;
1707 |
1708 | } else if ( track.getInterpolation() === InterpolateDiscrete ) {
1709 |
1710 | interpolation = 'STEP';
1711 |
1712 | } else {
1713 |
1714 | interpolation = 'LINEAR';
1715 |
1716 | }
1717 |
1718 | samplers.push( {
1719 | input: this.processAccessor( new BufferAttribute( track.times, inputItemSize ) ),
1720 | output: this.processAccessor( new BufferAttribute( track.values, outputItemSize ) ),
1721 | interpolation: interpolation
1722 | } );
1723 |
1724 | channels.push( {
1725 | sampler: samplers.length - 1,
1726 | target: {
1727 | node: nodeMap.get( trackNode ),
1728 | path: trackProperty
1729 | }
1730 | } );
1731 |
1732 | }
1733 |
1734 | json.animations.push( {
1735 | name: clip.name || 'clip_' + json.animations.length,
1736 | samplers: samplers,
1737 | channels: channels
1738 | } );
1739 |
1740 | return json.animations.length - 1;
1741 |
1742 | },
1743 |
1744 | /**
1745 | * @param {THREE.Object3D} object
1746 | * @return {number|null}
1747 | */
1748 | processSkin: function ( object ) {
1749 |
1750 | var json = this.json;
1751 | var nodeMap = this.nodeMap;
1752 |
1753 | var node = json.nodes[ nodeMap.get( object ) ];
1754 |
1755 | var skeleton = object.skeleton;
1756 |
1757 | if ( skeleton === undefined ) return null;
1758 |
1759 | var rootJoint = object.skeleton.bones[ 0 ];
1760 |
1761 | if ( rootJoint === undefined ) return null;
1762 |
1763 | var joints = [];
1764 | var inverseBindMatrices = new Float32Array( skeleton.bones.length * 16 );
1765 | var temporaryBoneInverse = new Matrix4();
1766 |
1767 | for ( var i = 0; i < skeleton.bones.length; ++ i ) {
1768 |
1769 | joints.push( nodeMap.get( skeleton.bones[ i ] ) );
1770 | temporaryBoneInverse.copy( skeleton.boneInverses[ i ] );
1771 | temporaryBoneInverse.multiply( object.bindMatrix ).toArray( inverseBindMatrices, i * 16 );
1772 |
1773 | }
1774 |
1775 | if ( json.skins === undefined ) json.skins = [];
1776 |
1777 | json.skins.push( {
1778 | inverseBindMatrices: this.processAccessor( new BufferAttribute( inverseBindMatrices, 16 ) ),
1779 | joints: joints,
1780 | skeleton: nodeMap.get( rootJoint )
1781 | } );
1782 |
1783 | var skinIndex = node.skin = json.skins.length - 1;
1784 |
1785 | return skinIndex;
1786 |
1787 | },
1788 |
1789 | /**
1790 | * Process Object3D node
1791 | * @param {THREE.Object3D} node Object3D to processNode
1792 | * @return {Integer} Index of the node in the nodes list
1793 | */
1794 | processNode: function ( object ) {
1795 |
1796 | var json = this.json;
1797 | var options = this.options;
1798 | var nodeMap = this.nodeMap;
1799 |
1800 | if ( ! json.nodes ) json.nodes = [];
1801 |
1802 | var nodeDef = {};
1803 |
1804 | if ( options.trs ) {
1805 |
1806 | var rotation = object.quaternion.toArray();
1807 | var position = object.position.toArray();
1808 | var scale = object.scale.toArray();
1809 |
1810 | if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) {
1811 |
1812 | nodeDef.rotation = rotation;
1813 |
1814 | }
1815 |
1816 | if ( ! equalArray( position, [ 0, 0, 0 ] ) ) {
1817 |
1818 | nodeDef.translation = position;
1819 |
1820 | }
1821 |
1822 | if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) {
1823 |
1824 | nodeDef.scale = scale;
1825 |
1826 | }
1827 |
1828 | } else {
1829 |
1830 | if ( object.matrixAutoUpdate ) {
1831 |
1832 | object.updateMatrix();
1833 |
1834 | }
1835 |
1836 | if ( isIdentityMatrix( object.matrix ) === false ) {
1837 |
1838 | nodeDef.matrix = object.matrix.elements;
1839 |
1840 | }
1841 |
1842 | }
1843 |
1844 | // We don't export empty strings name because it represents no-name in Three.js.
1845 | if ( object.name !== '' ) nodeDef.name = String( object.name );
1846 |
1847 | this.serializeUserData( object, nodeDef );
1848 |
1849 | if ( object.isMesh || object.isLine || object.isPoints ) {
1850 |
1851 | var meshIndex = this.processMesh( object );
1852 |
1853 | if ( meshIndex !== null ) nodeDef.mesh = meshIndex;
1854 |
1855 | } else if ( object.isCamera ) {
1856 |
1857 | nodeDef.camera = this.processCamera( object );
1858 |
1859 | }
1860 |
1861 | if ( object.isSkinnedMesh ) this.skins.push( object );
1862 |
1863 | if ( object.children.length > 0 ) {
1864 |
1865 | var children = [];
1866 |
1867 | for ( var i = 0, l = object.children.length; i < l; i ++ ) {
1868 |
1869 | var child = object.children[ i ];
1870 |
1871 | if ( child.visible || options.onlyVisible === false ) {
1872 |
1873 | var nodeIndex = this.processNode( child );
1874 |
1875 | if ( nodeIndex !== null ) children.push( nodeIndex );
1876 |
1877 | }
1878 |
1879 | }
1880 |
1881 | if ( children.length > 0 ) nodeDef.children = children;
1882 |
1883 | }
1884 |
1885 | this._invokeAll( function ( ext ) {
1886 |
1887 | ext.writeNode && ext.writeNode( object, nodeDef );
1888 |
1889 | } );
1890 |
1891 | var nodeIndex = json.nodes.push( nodeDef ) - 1;
1892 | nodeMap.set( object, nodeIndex );
1893 | return nodeIndex;
1894 |
1895 | },
1896 |
1897 | /**
1898 | * Process Scene
1899 | * @param {Scene} node Scene to process
1900 | */
1901 | processScene: function ( scene ) {
1902 |
1903 | var json = this.json;
1904 | var options = this.options;
1905 |
1906 | if ( ! json.scenes ) {
1907 |
1908 | json.scenes = [];
1909 | json.scene = 0;
1910 |
1911 | }
1912 |
1913 | var sceneDef = {};
1914 |
1915 | if ( scene.name !== '' ) sceneDef.name = scene.name;
1916 |
1917 | json.scenes.push( sceneDef );
1918 |
1919 | var nodes = [];
1920 |
1921 | for ( var i = 0, l = scene.children.length; i < l; i ++ ) {
1922 |
1923 | var child = scene.children[ i ];
1924 |
1925 | if ( child.visible || options.onlyVisible === false ) {
1926 |
1927 | var nodeIndex = this.processNode( child );
1928 |
1929 | if ( nodeIndex !== null ) nodes.push( nodeIndex );
1930 |
1931 | }
1932 |
1933 | }
1934 |
1935 | if ( nodes.length > 0 ) sceneDef.nodes = nodes;
1936 |
1937 | this.serializeUserData( scene, sceneDef );
1938 |
1939 | },
1940 |
1941 | /**
1942 | * Creates a Scene to hold a list of objects and parse it
1943 | * @param {Array} objects List of objects to process
1944 | */
1945 | processObjects: function ( objects ) {
1946 |
1947 | var scene = new Scene();
1948 | scene.name = 'AuxScene';
1949 |
1950 | for ( var i = 0; i < objects.length; i ++ ) {
1951 |
1952 | // We push directly to children instead of calling `add` to prevent
1953 | // modify the .parent and break its original scene and hierarchy
1954 | scene.children.push( objects[ i ] );
1955 |
1956 | }
1957 |
1958 | this.processScene( scene );
1959 |
1960 | },
1961 |
1962 | /**
1963 | * @param {THREE.Object3D|Array} input
1964 | */
1965 | processInput: function ( input ) {
1966 |
1967 | var options = this.options;
1968 |
1969 | input = input instanceof Array ? input : [ input ];
1970 |
1971 | this._invokeAll( function ( ext ) {
1972 |
1973 | ext.beforeParse && ext.beforeParse( input );
1974 |
1975 | } );
1976 |
1977 | var objectsWithoutScene = [];
1978 |
1979 | for ( var i = 0; i < input.length; i ++ ) {
1980 |
1981 | if ( input[ i ] instanceof Scene ) {
1982 |
1983 | this.processScene( input[ i ] );
1984 |
1985 | } else {
1986 |
1987 | objectsWithoutScene.push( input[ i ] );
1988 |
1989 | }
1990 |
1991 | }
1992 |
1993 | if ( objectsWithoutScene.length > 0 ) this.processObjects( objectsWithoutScene );
1994 |
1995 | for ( var i = 0; i < this.skins.length; ++ i ) {
1996 |
1997 | this.processSkin( this.skins[ i ] );
1998 |
1999 | }
2000 |
2001 | for ( var i = 0; i < options.animations.length; ++ i ) {
2002 |
2003 | this.processAnimation( options.animations[ i ], input[ 0 ] );
2004 |
2005 | }
2006 |
2007 | this._invokeAll( function ( ext ) {
2008 |
2009 | ext.afterParse && ext.afterParse( input );
2010 |
2011 | } );
2012 |
2013 | },
2014 |
2015 | _invokeAll: function ( func ) {
2016 |
2017 | for ( var i = 0, il = this.plugins.length; i < il; i ++ ) {
2018 |
2019 | func( this.plugins[ i ] );
2020 |
2021 | }
2022 |
2023 | }
2024 |
2025 | };
2026 |
2027 | /**
2028 | * Punctual Lights Extension
2029 | *
2030 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
2031 | */
2032 | function GLTFLightExtension( writer ) {
2033 |
2034 | this.writer = writer;
2035 | this.name = 'KHR_lights_punctual';
2036 |
2037 | }
2038 |
2039 | GLTFLightExtension.prototype = {
2040 |
2041 | constructor: GLTFLightExtension,
2042 |
2043 | writeNode: function ( light, nodeDef ) {
2044 |
2045 | if ( ! light.isLight ) return;
2046 |
2047 | if ( ! light.isDirectionalLight && ! light.isPointLight && ! light.isSpotLight ) {
2048 |
2049 | console.warn( 'THREE.GLTFExporter: Only directional, point, and spot lights are supported.', light );
2050 | return;
2051 |
2052 | }
2053 |
2054 | var writer = this.writer;
2055 | var json = writer.json;
2056 | var extensionsUsed = writer.extensionsUsed;
2057 |
2058 | var lightDef = {};
2059 |
2060 | if ( light.name ) lightDef.name = light.name;
2061 |
2062 | lightDef.color = light.color.toArray();
2063 |
2064 | lightDef.intensity = light.intensity;
2065 |
2066 | if ( light.isDirectionalLight ) {
2067 |
2068 | lightDef.type = 'directional';
2069 |
2070 | } else if ( light.isPointLight ) {
2071 |
2072 | lightDef.type = 'point';
2073 |
2074 | if ( light.distance > 0 ) lightDef.range = light.distance;
2075 |
2076 | } else if ( light.isSpotLight ) {
2077 |
2078 | lightDef.type = 'spot';
2079 |
2080 | if ( light.distance > 0 ) lightDef.range = light.distance;
2081 |
2082 | lightDef.spot = {};
2083 | lightDef.spot.innerConeAngle = ( light.penumbra - 1.0 ) * light.angle * - 1.0;
2084 | lightDef.spot.outerConeAngle = light.angle;
2085 |
2086 | }
2087 |
2088 | if ( light.decay !== undefined && light.decay !== 2 ) {
2089 |
2090 | console.warn( 'THREE.GLTFExporter: Light decay may be lost. glTF is physically-based, '
2091 | + 'and expects light.decay=2.' );
2092 |
2093 | }
2094 |
2095 | if ( light.target
2096 | && ( light.target.parent !== light
2097 | || light.target.position.x !== 0
2098 | || light.target.position.y !== 0
2099 | || light.target.position.z !== - 1 ) ) {
2100 |
2101 | console.warn( 'THREE.GLTFExporter: Light direction may be lost. For best results, '
2102 | + 'make light.target a child of the light with position 0,0,-1.' );
2103 |
2104 | }
2105 |
2106 | if ( ! extensionsUsed[ this.name ] ) {
2107 |
2108 | json.extensions = json.extensions || {};
2109 | json.extensions[ this.name ] = { lights: [] };
2110 | extensionsUsed[ this.name ] = true;
2111 |
2112 | }
2113 |
2114 | var lights = json.extensions[ this.name ].lights;
2115 | lights.push( lightDef );
2116 |
2117 | nodeDef.extensions = nodeDef.extensions || {};
2118 | nodeDef.extensions[ this.name ] = { light: lights.length - 1 };
2119 |
2120 | }
2121 |
2122 | };
2123 |
2124 | /**
2125 | * Unlit Materials Extension
2126 | *
2127 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit
2128 | */
2129 | function GLTFMaterialsUnlitExtension( writer ) {
2130 |
2131 | this.writer = writer;
2132 | this.name = 'KHR_materials_unlit';
2133 |
2134 | }
2135 |
2136 | GLTFMaterialsUnlitExtension.prototype = {
2137 |
2138 | constructor: GLTFMaterialsUnlitExtension,
2139 |
2140 | writeMaterial: function ( material, materialDef ) {
2141 |
2142 | if ( ! material.isMeshBasicMaterial ) return;
2143 |
2144 | var writer = this.writer;
2145 | var extensionsUsed = writer.extensionsUsed;
2146 |
2147 | materialDef.extensions = materialDef.extensions || {};
2148 | materialDef.extensions[ this.name ] = {};
2149 |
2150 | extensionsUsed[ this.name ] = true;
2151 |
2152 | materialDef.pbrMetallicRoughness.metallicFactor = 0.0;
2153 | materialDef.pbrMetallicRoughness.roughnessFactor = 0.9;
2154 |
2155 | }
2156 |
2157 | };
2158 |
2159 | /**
2160 | * Specular-Glossiness Extension
2161 | *
2162 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness
2163 | */
2164 | function GLTFMaterialsPBRSpecularGlossiness( writer ) {
2165 |
2166 | this.writer = writer;
2167 | this.name = 'KHR_materials_pbrSpecularGlossiness';
2168 |
2169 | }
2170 |
2171 | GLTFMaterialsPBRSpecularGlossiness.prototype = {
2172 |
2173 | constructor: GLTFMaterialsPBRSpecularGlossiness,
2174 |
2175 | writeMaterial: function ( material, materialDef ) {
2176 |
2177 | if ( ! material.isGLTFSpecularGlossinessMaterial ) return;
2178 |
2179 | var writer = this.writer;
2180 | var extensionsUsed = writer.extensionsUsed;
2181 |
2182 | var extensionDef = {};
2183 |
2184 | if ( materialDef.pbrMetallicRoughness.baseColorFactor ) {
2185 |
2186 | extensionDef.diffuseFactor = materialDef.pbrMetallicRoughness.baseColorFactor;
2187 |
2188 | }
2189 |
2190 | var specularFactor = [ 1, 1, 1 ];
2191 | material.specular.toArray( specularFactor, 0 );
2192 | extensionDef.specularFactor = specularFactor;
2193 | extensionDef.glossinessFactor = material.glossiness;
2194 |
2195 | if ( materialDef.pbrMetallicRoughness.baseColorTexture ) {
2196 |
2197 | extensionDef.diffuseTexture = materialDef.pbrMetallicRoughness.baseColorTexture;
2198 |
2199 | }
2200 |
2201 | if ( material.specularMap ) {
2202 |
2203 | var specularMapDef = { index: writer.processTexture( material.specularMap ) };
2204 | writer.applyTextureTransform( specularMapDef, material.specularMap );
2205 | extensionDef.specularGlossinessTexture = specularMapDef;
2206 |
2207 | }
2208 |
2209 | materialDef.extensions = materialDef.extensions || {};
2210 | materialDef.extensions[ this.name ] = extensionDef;
2211 | extensionsUsed[ this.name ] = true;
2212 |
2213 | }
2214 |
2215 | };
2216 |
2217 | /**
2218 | * Static utility functions
2219 | */
2220 | GLTFExporter.Utils = {
2221 |
2222 | insertKeyframe: function ( track, time ) {
2223 |
2224 | var tolerance = 0.001; // 1ms
2225 | var valueSize = track.getValueSize();
2226 |
2227 | var times = new track.TimeBufferType( track.times.length + 1 );
2228 | var values = new track.ValueBufferType( track.values.length + valueSize );
2229 | var interpolant = track.createInterpolant( new track.ValueBufferType( valueSize ) );
2230 |
2231 | var index;
2232 |
2233 | if ( track.times.length === 0 ) {
2234 |
2235 | times[ 0 ] = time;
2236 |
2237 | for ( var i = 0; i < valueSize; i ++ ) {
2238 |
2239 | values[ i ] = 0;
2240 |
2241 | }
2242 |
2243 | index = 0;
2244 |
2245 | } else if ( time < track.times[ 0 ] ) {
2246 |
2247 | if ( Math.abs( track.times[ 0 ] - time ) < tolerance ) return 0;
2248 |
2249 | times[ 0 ] = time;
2250 | times.set( track.times, 1 );
2251 |
2252 | values.set( interpolant.evaluate( time ), 0 );
2253 | values.set( track.values, valueSize );
2254 |
2255 | index = 0;
2256 |
2257 | } else if ( time > track.times[ track.times.length - 1 ] ) {
2258 |
2259 | if ( Math.abs( track.times[ track.times.length - 1 ] - time ) < tolerance ) {
2260 |
2261 | return track.times.length - 1;
2262 |
2263 | }
2264 |
2265 | times[ times.length - 1 ] = time;
2266 | times.set( track.times, 0 );
2267 |
2268 | values.set( track.values, 0 );
2269 | values.set( interpolant.evaluate( time ), track.values.length );
2270 |
2271 | index = times.length - 1;
2272 |
2273 | } else {
2274 |
2275 | for ( var i = 0; i < track.times.length; i ++ ) {
2276 |
2277 | if ( Math.abs( track.times[ i ] - time ) < tolerance ) return i;
2278 |
2279 | if ( track.times[ i ] < time && track.times[ i + 1 ] > time ) {
2280 |
2281 | times.set( track.times.slice( 0, i + 1 ), 0 );
2282 | times[ i + 1 ] = time;
2283 | times.set( track.times.slice( i + 1 ), i + 2 );
2284 |
2285 | values.set( track.values.slice( 0, ( i + 1 ) * valueSize ), 0 );
2286 | values.set( interpolant.evaluate( time ), ( i + 1 ) * valueSize );
2287 | values.set( track.values.slice( ( i + 1 ) * valueSize ), ( i + 2 ) * valueSize );
2288 |
2289 | index = i + 1;
2290 |
2291 | break;
2292 |
2293 | }
2294 |
2295 | }
2296 |
2297 | }
2298 |
2299 | track.times = times;
2300 | track.values = values;
2301 |
2302 | return index;
2303 |
2304 | },
2305 |
2306 | mergeMorphTargetTracks: function ( clip, root ) {
2307 |
2308 | var tracks = [];
2309 | var mergedTracks = {};
2310 | var sourceTracks = clip.tracks;
2311 |
2312 | for ( var i = 0; i < sourceTracks.length; ++ i ) {
2313 |
2314 | var sourceTrack = sourceTracks[ i ];
2315 | var sourceTrackBinding = PropertyBinding.parseTrackName( sourceTrack.name );
2316 | var sourceTrackNode = PropertyBinding.findNode( root, sourceTrackBinding.nodeName );
2317 |
2318 | if ( sourceTrackBinding.propertyName !== 'morphTargetInfluences' || sourceTrackBinding.propertyIndex === undefined ) {
2319 |
2320 | // Tracks that don't affect morph targets, or that affect all morph targets together, can be left as-is.
2321 | tracks.push( sourceTrack );
2322 | continue;
2323 |
2324 | }
2325 |
2326 | if ( sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete
2327 | && sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear ) {
2328 |
2329 | if ( sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
2330 |
2331 | // This should never happen, because glTF morph target animations
2332 | // affect all targets already.
2333 | throw new Error( 'THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation.' );
2334 |
2335 | }
2336 |
2337 | console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' );
2338 |
2339 | sourceTrack = sourceTrack.clone();
2340 | sourceTrack.setInterpolation( InterpolateLinear );
2341 |
2342 | }
2343 |
2344 | var targetCount = sourceTrackNode.morphTargetInfluences.length;
2345 | var targetIndex = sourceTrackNode.morphTargetDictionary[ sourceTrackBinding.propertyIndex ];
2346 |
2347 | if ( targetIndex === undefined ) {
2348 |
2349 | throw new Error( 'THREE.GLTFExporter: Morph target name not found: ' + sourceTrackBinding.propertyIndex );
2350 |
2351 | }
2352 |
2353 | var mergedTrack;
2354 |
2355 | // If this is the first time we've seen this object, create a new
2356 | // track to store merged keyframe data for each morph target.
2357 | if ( mergedTracks[ sourceTrackNode.uuid ] === undefined ) {
2358 |
2359 | mergedTrack = sourceTrack.clone();
2360 |
2361 | var values = new mergedTrack.ValueBufferType( targetCount * mergedTrack.times.length );
2362 |
2363 | for ( var j = 0; j < mergedTrack.times.length; j ++ ) {
2364 |
2365 | values[ j * targetCount + targetIndex ] = mergedTrack.values[ j ];
2366 |
2367 | }
2368 |
2369 | // We need to take into consideration the intended target node
2370 | // of our original un-merged morphTarget animation.
2371 | mergedTrack.name = ( sourceTrackBinding.nodeName || '' ) + '.morphTargetInfluences';
2372 | mergedTrack.values = values;
2373 |
2374 | mergedTracks[ sourceTrackNode.uuid ] = mergedTrack;
2375 | tracks.push( mergedTrack );
2376 |
2377 | continue;
2378 |
2379 | }
2380 |
2381 | var sourceInterpolant = sourceTrack.createInterpolant( new sourceTrack.ValueBufferType( 1 ) );
2382 |
2383 | mergedTrack = mergedTracks[ sourceTrackNode.uuid ];
2384 |
2385 | // For every existing keyframe of the merged track, write a (possibly
2386 | // interpolated) value from the source track.
2387 | for ( var j = 0; j < mergedTrack.times.length; j ++ ) {
2388 |
2389 | mergedTrack.values[ j * targetCount + targetIndex ] = sourceInterpolant.evaluate( mergedTrack.times[ j ] );
2390 |
2391 | }
2392 |
2393 | // For every existing keyframe of the source track, write a (possibly
2394 | // new) keyframe to the merged track. Values from the previous loop may
2395 | // be written again, but keyframes are de-duplicated.
2396 | for ( var j = 0; j < sourceTrack.times.length; j ++ ) {
2397 |
2398 | var keyframeIndex = this.insertKeyframe( mergedTrack, sourceTrack.times[ j ] );
2399 | mergedTrack.values[ keyframeIndex * targetCount + targetIndex ] = sourceTrack.values[ j ];
2400 |
2401 | }
2402 |
2403 | }
2404 |
2405 | clip.tracks = tracks;
2406 |
2407 | return clip;
2408 |
2409 | }
2410 |
2411 | };
2412 |
2413 | return GLTFExporter;
2414 |
2415 | } )();
2416 |
2417 | export { GLTFExporter };
2418 |
--------------------------------------------------------------------------------
/examples/three/loaders/DDSLoader.js:
--------------------------------------------------------------------------------
1 | import {
2 | CompressedTextureLoader,
3 | RGBAFormat,
4 | RGBA_S3TC_DXT3_Format,
5 | RGBA_S3TC_DXT5_Format,
6 | RGB_ETC1_Format,
7 | RGB_S3TC_DXT1_Format
8 | } from '../three.module.js';
9 |
10 | var DDSLoader = function ( manager ) {
11 |
12 | CompressedTextureLoader.call( this, manager );
13 |
14 | };
15 |
16 | DDSLoader.prototype = Object.assign( Object.create( CompressedTextureLoader.prototype ), {
17 |
18 | constructor: DDSLoader,
19 |
20 | parse: function ( buffer, loadMipmaps ) {
21 |
22 | var dds = { mipmaps: [], width: 0, height: 0, format: null, mipmapCount: 1 };
23 |
24 | // Adapted from @toji's DDS utils
25 | // https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js
26 |
27 | // All values and structures referenced from:
28 | // http://msdn.microsoft.com/en-us/library/bb943991.aspx/
29 |
30 | var DDS_MAGIC = 0x20534444;
31 |
32 | // var DDSD_CAPS = 0x1;
33 | // var DDSD_HEIGHT = 0x2;
34 | // var DDSD_WIDTH = 0x4;
35 | // var DDSD_PITCH = 0x8;
36 | // var DDSD_PIXELFORMAT = 0x1000;
37 | var DDSD_MIPMAPCOUNT = 0x20000;
38 | // var DDSD_LINEARSIZE = 0x80000;
39 | // var DDSD_DEPTH = 0x800000;
40 |
41 | // var DDSCAPS_COMPLEX = 0x8;
42 | // var DDSCAPS_MIPMAP = 0x400000;
43 | // var DDSCAPS_TEXTURE = 0x1000;
44 |
45 | var DDSCAPS2_CUBEMAP = 0x200;
46 | var DDSCAPS2_CUBEMAP_POSITIVEX = 0x400;
47 | var DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800;
48 | var DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000;
49 | var DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000;
50 | var DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000;
51 | var DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000;
52 | // var DDSCAPS2_VOLUME = 0x200000;
53 |
54 | // var DDPF_ALPHAPIXELS = 0x1;
55 | // var DDPF_ALPHA = 0x2;
56 | var DDPF_FOURCC = 0x4;
57 | // var DDPF_RGB = 0x40;
58 | // var DDPF_YUV = 0x200;
59 | // var DDPF_LUMINANCE = 0x20000;
60 |
61 | function fourCCToInt32( value ) {
62 |
63 | return value.charCodeAt( 0 ) +
64 | ( value.charCodeAt( 1 ) << 8 ) +
65 | ( value.charCodeAt( 2 ) << 16 ) +
66 | ( value.charCodeAt( 3 ) << 24 );
67 |
68 | }
69 |
70 | function int32ToFourCC( value ) {
71 |
72 | return String.fromCharCode(
73 | value & 0xff,
74 | ( value >> 8 ) & 0xff,
75 | ( value >> 16 ) & 0xff,
76 | ( value >> 24 ) & 0xff
77 | );
78 |
79 | }
80 |
81 | function loadARGBMip( buffer, dataOffset, width, height ) {
82 |
83 | var dataLength = width * height * 4;
84 | var srcBuffer = new Uint8Array( buffer, dataOffset, dataLength );
85 | var byteArray = new Uint8Array( dataLength );
86 | var dst = 0;
87 | var src = 0;
88 | for ( var y = 0; y < height; y ++ ) {
89 |
90 | for ( var x = 0; x < width; x ++ ) {
91 |
92 | var b = srcBuffer[ src ]; src ++;
93 | var g = srcBuffer[ src ]; src ++;
94 | var r = srcBuffer[ src ]; src ++;
95 | var a = srcBuffer[ src ]; src ++;
96 | byteArray[ dst ] = r; dst ++; //r
97 | byteArray[ dst ] = g; dst ++; //g
98 | byteArray[ dst ] = b; dst ++; //b
99 | byteArray[ dst ] = a; dst ++; //a
100 |
101 | }
102 |
103 | }
104 |
105 | return byteArray;
106 |
107 | }
108 |
109 | var FOURCC_DXT1 = fourCCToInt32( 'DXT1' );
110 | var FOURCC_DXT3 = fourCCToInt32( 'DXT3' );
111 | var FOURCC_DXT5 = fourCCToInt32( 'DXT5' );
112 | var FOURCC_ETC1 = fourCCToInt32( 'ETC1' );
113 |
114 | var headerLengthInt = 31; // The header length in 32 bit ints
115 |
116 | // Offsets into the header array
117 |
118 | var off_magic = 0;
119 |
120 | var off_size = 1;
121 | var off_flags = 2;
122 | var off_height = 3;
123 | var off_width = 4;
124 |
125 | var off_mipmapCount = 7;
126 |
127 | var off_pfFlags = 20;
128 | var off_pfFourCC = 21;
129 | var off_RGBBitCount = 22;
130 | var off_RBitMask = 23;
131 | var off_GBitMask = 24;
132 | var off_BBitMask = 25;
133 | var off_ABitMask = 26;
134 |
135 | // var off_caps = 27;
136 | var off_caps2 = 28;
137 | // var off_caps3 = 29;
138 | // var off_caps4 = 30;
139 |
140 | // Parse header
141 |
142 | var header = new Int32Array( buffer, 0, headerLengthInt );
143 |
144 | if ( header[ off_magic ] !== DDS_MAGIC ) {
145 |
146 | console.error( 'THREE.DDSLoader.parse: Invalid magic number in DDS header.' );
147 | return dds;
148 |
149 | }
150 |
151 | if ( ! header[ off_pfFlags ] & DDPF_FOURCC ) {
152 |
153 | console.error( 'THREE.DDSLoader.parse: Unsupported format, must contain a FourCC code.' );
154 | return dds;
155 |
156 | }
157 |
158 | var blockBytes;
159 |
160 | var fourCC = header[ off_pfFourCC ];
161 |
162 | var isRGBAUncompressed = false;
163 |
164 | switch ( fourCC ) {
165 |
166 | case FOURCC_DXT1:
167 |
168 | blockBytes = 8;
169 | dds.format = RGB_S3TC_DXT1_Format;
170 | break;
171 |
172 | case FOURCC_DXT3:
173 |
174 | blockBytes = 16;
175 | dds.format = RGBA_S3TC_DXT3_Format;
176 | break;
177 |
178 | case FOURCC_DXT5:
179 |
180 | blockBytes = 16;
181 | dds.format = RGBA_S3TC_DXT5_Format;
182 | break;
183 |
184 | case FOURCC_ETC1:
185 |
186 | blockBytes = 8;
187 | dds.format = RGB_ETC1_Format;
188 | break;
189 |
190 | default:
191 |
192 | if ( header[ off_RGBBitCount ] === 32
193 | && header[ off_RBitMask ] & 0xff0000
194 | && header[ off_GBitMask ] & 0xff00
195 | && header[ off_BBitMask ] & 0xff
196 | && header[ off_ABitMask ] & 0xff000000 ) {
197 |
198 | isRGBAUncompressed = true;
199 | blockBytes = 64;
200 | dds.format = RGBAFormat;
201 |
202 | } else {
203 |
204 | console.error( 'THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC( fourCC ) );
205 | return dds;
206 |
207 | }
208 |
209 | }
210 |
211 | dds.mipmapCount = 1;
212 |
213 | if ( header[ off_flags ] & DDSD_MIPMAPCOUNT && loadMipmaps !== false ) {
214 |
215 | dds.mipmapCount = Math.max( 1, header[ off_mipmapCount ] );
216 |
217 | }
218 |
219 | var caps2 = header[ off_caps2 ];
220 | dds.isCubemap = caps2 & DDSCAPS2_CUBEMAP ? true : false;
221 | if ( dds.isCubemap && (
222 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEX ) ||
223 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX ) ||
224 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEY ) ||
225 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY ) ||
226 | ! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ ) ||
227 | ! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ )
228 | ) ) {
229 |
230 | console.error( 'THREE.DDSLoader.parse: Incomplete cubemap faces' );
231 | return dds;
232 |
233 | }
234 |
235 | dds.width = header[ off_width ];
236 | dds.height = header[ off_height ];
237 |
238 | var dataOffset = header[ off_size ] + 4;
239 |
240 | // Extract mipmaps buffers
241 |
242 | var faces = dds.isCubemap ? 6 : 1;
243 |
244 | for ( var face = 0; face < faces; face ++ ) {
245 |
246 | var width = dds.width;
247 | var height = dds.height;
248 |
249 | for ( var i = 0; i < dds.mipmapCount; i ++ ) {
250 |
251 | if ( isRGBAUncompressed ) {
252 |
253 | var byteArray = loadARGBMip( buffer, dataOffset, width, height );
254 | var dataLength = byteArray.length;
255 |
256 | } else {
257 |
258 | var dataLength = Math.max( 4, width ) / 4 * Math.max( 4, height ) / 4 * blockBytes;
259 | var byteArray = new Uint8Array( buffer, dataOffset, dataLength );
260 |
261 | }
262 |
263 | var mipmap = { 'data': byteArray, 'width': width, 'height': height };
264 | dds.mipmaps.push( mipmap );
265 |
266 | dataOffset += dataLength;
267 |
268 | width = Math.max( width >> 1, 1 );
269 | height = Math.max( height >> 1, 1 );
270 |
271 | }
272 |
273 | }
274 |
275 | return dds;
276 |
277 | }
278 |
279 | } );
280 |
281 | export { DDSLoader };
282 |
--------------------------------------------------------------------------------
/examples/three/loaders/RGBELoader.js:
--------------------------------------------------------------------------------
1 | import {
2 | DataTextureLoader,
3 | DataUtils,
4 | FloatType,
5 | HalfFloatType,
6 | LinearEncoding,
7 | LinearFilter,
8 | NearestFilter,
9 | RGBEEncoding,
10 | RGBEFormat,
11 | RGBFormat,
12 | UnsignedByteType
13 | } from '../three.module.js';
14 |
15 | // https://github.com/mrdoob/three.js/issues/5552
16 | // http://en.wikipedia.org/wiki/RGBE_image_format
17 |
18 | var RGBELoader = function ( manager ) {
19 |
20 | DataTextureLoader.call( this, manager );
21 |
22 | this.type = UnsignedByteType;
23 |
24 | };
25 |
26 | RGBELoader.prototype = Object.assign( Object.create( DataTextureLoader.prototype ), {
27 |
28 | constructor: RGBELoader,
29 |
30 | // adapted from http://www.graphics.cornell.edu/~bjw/rgbe.html
31 |
32 | parse: function ( buffer ) {
33 |
34 | var
35 | /* return codes for rgbe routines */
36 | //RGBE_RETURN_SUCCESS = 0,
37 | RGBE_RETURN_FAILURE = - 1,
38 |
39 | /* default error routine. change this to change error handling */
40 | rgbe_read_error = 1,
41 | rgbe_write_error = 2,
42 | rgbe_format_error = 3,
43 | rgbe_memory_error = 4,
44 | rgbe_error = function ( rgbe_error_code, msg ) {
45 |
46 | switch ( rgbe_error_code ) {
47 |
48 | case rgbe_read_error: console.error( 'THREE.RGBELoader Read Error: ' + ( msg || '' ) );
49 | break;
50 | case rgbe_write_error: console.error( 'THREE.RGBELoader Write Error: ' + ( msg || '' ) );
51 | break;
52 | case rgbe_format_error: console.error( 'THREE.RGBELoader Bad File Format: ' + ( msg || '' ) );
53 | break;
54 | default:
55 | case rgbe_memory_error: console.error( 'THREE.RGBELoader: Error: ' + ( msg || '' ) );
56 |
57 | }
58 |
59 | return RGBE_RETURN_FAILURE;
60 |
61 | },
62 |
63 | /* offsets to red, green, and blue components in a data (float) pixel */
64 | //RGBE_DATA_RED = 0,
65 | //RGBE_DATA_GREEN = 1,
66 | //RGBE_DATA_BLUE = 2,
67 |
68 | /* number of floats per pixel, use 4 since stored in rgba image format */
69 | //RGBE_DATA_SIZE = 4,
70 |
71 | /* flags indicating which fields in an rgbe_header_info are valid */
72 | RGBE_VALID_PROGRAMTYPE = 1,
73 | RGBE_VALID_FORMAT = 2,
74 | RGBE_VALID_DIMENSIONS = 4,
75 |
76 | NEWLINE = '\n',
77 |
78 | fgets = function ( buffer, lineLimit, consume ) {
79 |
80 | lineLimit = ! lineLimit ? 1024 : lineLimit;
81 | var p = buffer.pos,
82 | i = - 1, len = 0, s = '', chunkSize = 128,
83 | chunk = String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) )
84 | ;
85 | while ( ( 0 > ( i = chunk.indexOf( NEWLINE ) ) ) && ( len < lineLimit ) && ( p < buffer.byteLength ) ) {
86 |
87 | s += chunk; len += chunk.length;
88 | p += chunkSize;
89 | chunk += String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) );
90 |
91 | }
92 |
93 | if ( - 1 < i ) {
94 |
95 | /*for (i=l-1; i>=0; i--) {
96 | byteCode = m.charCodeAt(i);
97 | if (byteCode > 0x7f && byteCode <= 0x7ff) byteLen++;
98 | else if (byteCode > 0x7ff && byteCode <= 0xffff) byteLen += 2;
99 | if (byteCode >= 0xDC00 && byteCode <= 0xDFFF) i--; //trail surrogate
100 | }*/
101 | if ( false !== consume ) buffer.pos += len + i + 1;
102 | return s + chunk.slice( 0, i );
103 |
104 | }
105 |
106 | return false;
107 |
108 | },
109 |
110 | /* minimal header reading. modify if you want to parse more information */
111 | RGBE_ReadHeader = function ( buffer ) {
112 |
113 | var line, match,
114 |
115 | // regexes to parse header info fields
116 | magic_token_re = /^#\?(\S+)/,
117 | gamma_re = /^\s*GAMMA\s*=\s*(\d+(\.\d+)?)\s*$/,
118 | exposure_re = /^\s*EXPOSURE\s*=\s*(\d+(\.\d+)?)\s*$/,
119 | format_re = /^\s*FORMAT=(\S+)\s*$/,
120 | dimensions_re = /^\s*\-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/,
121 |
122 | // RGBE format header struct
123 | header = {
124 |
125 | valid: 0, /* indicate which fields are valid */
126 |
127 | string: '', /* the actual header string */
128 |
129 | comments: '', /* comments found in header */
130 |
131 | programtype: 'RGBE', /* listed at beginning of file to identify it after "#?". defaults to "RGBE" */
132 |
133 | format: '', /* RGBE format, default 32-bit_rle_rgbe */
134 |
135 | gamma: 1.0, /* image has already been gamma corrected with given gamma. defaults to 1.0 (no correction) */
136 |
137 | exposure: 1.0, /* a value of 1.0 in an image corresponds to watts/steradian/m^2. defaults to 1.0 */
138 |
139 | width: 0, height: 0 /* image dimensions, width/height */
140 |
141 | };
142 |
143 | if ( buffer.pos >= buffer.byteLength || ! ( line = fgets( buffer ) ) ) {
144 |
145 | return rgbe_error( rgbe_read_error, 'no header found' );
146 |
147 | }
148 |
149 | /* if you want to require the magic token then uncomment the next line */
150 | if ( ! ( match = line.match( magic_token_re ) ) ) {
151 |
152 | return rgbe_error( rgbe_format_error, 'bad initial token' );
153 |
154 | }
155 |
156 | header.valid |= RGBE_VALID_PROGRAMTYPE;
157 | header.programtype = match[ 1 ];
158 | header.string += line + '\n';
159 |
160 | while ( true ) {
161 |
162 | line = fgets( buffer );
163 | if ( false === line ) break;
164 | header.string += line + '\n';
165 |
166 | if ( '#' === line.charAt( 0 ) ) {
167 |
168 | header.comments += line + '\n';
169 | continue; // comment line
170 |
171 | }
172 |
173 | if ( match = line.match( gamma_re ) ) {
174 |
175 | header.gamma = parseFloat( match[ 1 ], 10 );
176 |
177 | }
178 |
179 | if ( match = line.match( exposure_re ) ) {
180 |
181 | header.exposure = parseFloat( match[ 1 ], 10 );
182 |
183 | }
184 |
185 | if ( match = line.match( format_re ) ) {
186 |
187 | header.valid |= RGBE_VALID_FORMAT;
188 | header.format = match[ 1 ];//'32-bit_rle_rgbe';
189 |
190 | }
191 |
192 | if ( match = line.match( dimensions_re ) ) {
193 |
194 | header.valid |= RGBE_VALID_DIMENSIONS;
195 | header.height = parseInt( match[ 1 ], 10 );
196 | header.width = parseInt( match[ 2 ], 10 );
197 |
198 | }
199 |
200 | if ( ( header.valid & RGBE_VALID_FORMAT ) && ( header.valid & RGBE_VALID_DIMENSIONS ) ) break;
201 |
202 | }
203 |
204 | if ( ! ( header.valid & RGBE_VALID_FORMAT ) ) {
205 |
206 | return rgbe_error( rgbe_format_error, 'missing format specifier' );
207 |
208 | }
209 |
210 | if ( ! ( header.valid & RGBE_VALID_DIMENSIONS ) ) {
211 |
212 | return rgbe_error( rgbe_format_error, 'missing image size specifier' );
213 |
214 | }
215 |
216 | return header;
217 |
218 | },
219 |
220 | RGBE_ReadPixels_RLE = function ( buffer, w, h ) {
221 |
222 | var data_rgba, offset, pos, count, byteValue,
223 | scanline_buffer, ptr, ptr_end, i, l, off, isEncodedRun,
224 | scanline_width = w, num_scanlines = h, rgbeStart
225 | ;
226 |
227 | if (
228 | // run length encoding is not allowed so read flat
229 | ( ( scanline_width < 8 ) || ( scanline_width > 0x7fff ) ) ||
230 | // this file is not run length encoded
231 | ( ( 2 !== buffer[ 0 ] ) || ( 2 !== buffer[ 1 ] ) || ( buffer[ 2 ] & 0x80 ) )
232 | ) {
233 |
234 | // return the flat buffer
235 | return new Uint8Array( buffer );
236 |
237 | }
238 |
239 | if ( scanline_width !== ( ( buffer[ 2 ] << 8 ) | buffer[ 3 ] ) ) {
240 |
241 | return rgbe_error( rgbe_format_error, 'wrong scanline width' );
242 |
243 | }
244 |
245 | data_rgba = new Uint8Array( 4 * w * h );
246 |
247 | if ( ! data_rgba.length ) {
248 |
249 | return rgbe_error( rgbe_memory_error, 'unable to allocate buffer space' );
250 |
251 | }
252 |
253 | offset = 0; pos = 0; ptr_end = 4 * scanline_width;
254 | rgbeStart = new Uint8Array( 4 );
255 | scanline_buffer = new Uint8Array( ptr_end );
256 |
257 | // read in each successive scanline
258 | while ( ( num_scanlines > 0 ) && ( pos < buffer.byteLength ) ) {
259 |
260 | if ( pos + 4 > buffer.byteLength ) {
261 |
262 | return rgbe_error( rgbe_read_error );
263 |
264 | }
265 |
266 | rgbeStart[ 0 ] = buffer[ pos ++ ];
267 | rgbeStart[ 1 ] = buffer[ pos ++ ];
268 | rgbeStart[ 2 ] = buffer[ pos ++ ];
269 | rgbeStart[ 3 ] = buffer[ pos ++ ];
270 |
271 | if ( ( 2 != rgbeStart[ 0 ] ) || ( 2 != rgbeStart[ 1 ] ) || ( ( ( rgbeStart[ 2 ] << 8 ) | rgbeStart[ 3 ] ) != scanline_width ) ) {
272 |
273 | return rgbe_error( rgbe_format_error, 'bad rgbe scanline format' );
274 |
275 | }
276 |
277 | // read each of the four channels for the scanline into the buffer
278 | // first red, then green, then blue, then exponent
279 | ptr = 0;
280 | while ( ( ptr < ptr_end ) && ( pos < buffer.byteLength ) ) {
281 |
282 | count = buffer[ pos ++ ];
283 | isEncodedRun = count > 128;
284 | if ( isEncodedRun ) count -= 128;
285 |
286 | if ( ( 0 === count ) || ( ptr + count > ptr_end ) ) {
287 |
288 | return rgbe_error( rgbe_format_error, 'bad scanline data' );
289 |
290 | }
291 |
292 | if ( isEncodedRun ) {
293 |
294 | // a (encoded) run of the same value
295 | byteValue = buffer[ pos ++ ];
296 | for ( i = 0; i < count; i ++ ) {
297 |
298 | scanline_buffer[ ptr ++ ] = byteValue;
299 |
300 | }
301 | //ptr += count;
302 |
303 | } else {
304 |
305 | // a literal-run
306 | scanline_buffer.set( buffer.subarray( pos, pos + count ), ptr );
307 | ptr += count; pos += count;
308 |
309 | }
310 |
311 | }
312 |
313 |
314 | // now convert data from buffer into rgba
315 | // first red, then green, then blue, then exponent (alpha)
316 | l = scanline_width; //scanline_buffer.byteLength;
317 | for ( i = 0; i < l; i ++ ) {
318 |
319 | off = 0;
320 | data_rgba[ offset ] = scanline_buffer[ i + off ];
321 | off += scanline_width; //1;
322 | data_rgba[ offset + 1 ] = scanline_buffer[ i + off ];
323 | off += scanline_width; //1;
324 | data_rgba[ offset + 2 ] = scanline_buffer[ i + off ];
325 | off += scanline_width; //1;
326 | data_rgba[ offset + 3 ] = scanline_buffer[ i + off ];
327 | offset += 4;
328 |
329 | }
330 |
331 | num_scanlines --;
332 |
333 | }
334 |
335 | return data_rgba;
336 |
337 | };
338 |
339 | var RGBEByteToRGBFloat = function ( sourceArray, sourceOffset, destArray, destOffset ) {
340 |
341 | var e = sourceArray[ sourceOffset + 3 ];
342 | var scale = Math.pow( 2.0, e - 128.0 ) / 255.0;
343 |
344 | destArray[ destOffset + 0 ] = sourceArray[ sourceOffset + 0 ] * scale;
345 | destArray[ destOffset + 1 ] = sourceArray[ sourceOffset + 1 ] * scale;
346 | destArray[ destOffset + 2 ] = sourceArray[ sourceOffset + 2 ] * scale;
347 |
348 | };
349 |
350 | var RGBEByteToRGBHalf = function ( sourceArray, sourceOffset, destArray, destOffset ) {
351 |
352 | var e = sourceArray[ sourceOffset + 3 ];
353 | var scale = Math.pow( 2.0, e - 128.0 ) / 255.0;
354 |
355 | destArray[ destOffset + 0 ] = DataUtils.toHalfFloat( sourceArray[ sourceOffset + 0 ] * scale );
356 | destArray[ destOffset + 1 ] = DataUtils.toHalfFloat( sourceArray[ sourceOffset + 1 ] * scale );
357 | destArray[ destOffset + 2 ] = DataUtils.toHalfFloat( sourceArray[ sourceOffset + 2 ] * scale );
358 |
359 | };
360 |
361 | var byteArray = new Uint8Array( buffer );
362 | byteArray.pos = 0;
363 | var rgbe_header_info = RGBE_ReadHeader( byteArray );
364 |
365 | if ( RGBE_RETURN_FAILURE !== rgbe_header_info ) {
366 |
367 | var w = rgbe_header_info.width,
368 | h = rgbe_header_info.height,
369 | image_rgba_data = RGBE_ReadPixels_RLE( byteArray.subarray( byteArray.pos ), w, h );
370 |
371 | if ( RGBE_RETURN_FAILURE !== image_rgba_data ) {
372 |
373 | switch ( this.type ) {
374 |
375 | case UnsignedByteType:
376 |
377 | var data = image_rgba_data;
378 | var format = RGBEFormat; // handled as THREE.RGBAFormat in shaders
379 | var type = UnsignedByteType;
380 | break;
381 |
382 | case FloatType:
383 |
384 | var numElements = ( image_rgba_data.length / 4 ) * 3;
385 | var floatArray = new Float32Array( numElements );
386 |
387 | for ( var j = 0; j < numElements; j ++ ) {
388 |
389 | RGBEByteToRGBFloat( image_rgba_data, j * 4, floatArray, j * 3 );
390 |
391 | }
392 |
393 | var data = floatArray;
394 | var format = RGBFormat;
395 | var type = FloatType;
396 | break;
397 |
398 | case HalfFloatType:
399 |
400 | var numElements = ( image_rgba_data.length / 4 ) * 3;
401 | var halfArray = new Uint16Array( numElements );
402 |
403 | for ( var j = 0; j < numElements; j ++ ) {
404 |
405 | RGBEByteToRGBHalf( image_rgba_data, j * 4, halfArray, j * 3 );
406 |
407 | }
408 |
409 | var data = halfArray;
410 | var format = RGBFormat;
411 | var type = HalfFloatType;
412 | break;
413 |
414 | default:
415 |
416 | console.error( 'THREE.RGBELoader: unsupported type: ', this.type );
417 | break;
418 |
419 | }
420 |
421 | return {
422 | width: w, height: h,
423 | data: data,
424 | header: rgbe_header_info.string,
425 | gamma: rgbe_header_info.gamma,
426 | exposure: rgbe_header_info.exposure,
427 | format: format,
428 | type: type
429 | };
430 |
431 | }
432 |
433 | }
434 |
435 | return null;
436 |
437 | },
438 |
439 | setDataType: function ( value ) {
440 |
441 | this.type = value;
442 | return this;
443 |
444 | },
445 |
446 | load: function ( url, onLoad, onProgress, onError ) {
447 |
448 | function onLoadCallback( texture, texData ) {
449 |
450 | switch ( texture.type ) {
451 |
452 | case UnsignedByteType:
453 |
454 | texture.encoding = RGBEEncoding;
455 | texture.minFilter = NearestFilter;
456 | texture.magFilter = NearestFilter;
457 | texture.generateMipmaps = false;
458 | texture.flipY = true;
459 | break;
460 |
461 | case FloatType:
462 |
463 | texture.encoding = LinearEncoding;
464 | texture.minFilter = LinearFilter;
465 | texture.magFilter = LinearFilter;
466 | texture.generateMipmaps = false;
467 | texture.flipY = true;
468 | break;
469 |
470 | case HalfFloatType:
471 |
472 | texture.encoding = LinearEncoding;
473 | texture.minFilter = LinearFilter;
474 | texture.magFilter = LinearFilter;
475 | texture.generateMipmaps = false;
476 | texture.flipY = true;
477 | break;
478 |
479 | }
480 |
481 | if ( onLoad ) onLoad( texture, texData );
482 |
483 | }
484 |
485 | return DataTextureLoader.prototype.load.call( this, url, onLoadCallback, onProgress, onError );
486 |
487 | }
488 |
489 | } );
490 |
491 | export { RGBELoader };
492 |
--------------------------------------------------------------------------------
/examples/utils/copy_js.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | # Run this script at three-gltf-plugins/examples
4 |
5 | # Path from three-gltf-plugins/examples to
6 | # Three.js cloned repository directory
7 | three_dir="../../three.js"
8 |
9 | echo [log] copy ${three_dir}/build/three.module.js
10 | cp ${three_dir}/build/three.module.js ./three/
11 |
12 | for file in \
13 | 'loaders/GLTFLoader.js' \
14 | 'loaders/RGBELoader.js' \
15 | 'loaders/DDSLoader.js' \
16 | 'exporters/GLTFExporter.js' \
17 | 'controls/OrbitControls.js'
18 | do
19 | echo [log] copy ${three_dir}/examples/jsm/${file}
20 | cat ${three_dir}/examples/jsm/${file} |\
21 | sed -e 's/..\/..\/..\/build\/three/..\/three/' > ./three/${file}
22 | done
23 |
24 |
--------------------------------------------------------------------------------
/examples/utils/save.js:
--------------------------------------------------------------------------------
1 | const link = document.createElement( 'a' );
2 | link.style.display = 'none';
3 | document.body.appendChild(link); // Firefox workaround
4 |
5 | const saveBlob = (blob, filename) => {
6 | link.href = URL.createObjectURL( blob );
7 | link.download = filename;
8 | link.click();
9 | // URL.revokeObjectURL(url); breaks Firefox...
10 | };
11 |
12 | const saveArrayBuffer = (buffer, filename) => {
13 | saveBlob(new Blob([buffer], {type: 'application/octet-stream'}), filename);
14 | };
15 |
16 | export {
17 | saveBlob,
18 | saveArrayBuffer
19 | };
20 |
--------------------------------------------------------------------------------
/exporters/KHR_materials_variants/KHR_materials_variants_exporter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Materials variants extension
3 | *
4 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants
5 | */
6 |
7 | /**
8 | * @param object {THREE.Object3D}
9 | * @return {boolean}
10 | */
11 | const compatibleObject = object => {
12 | // @TODO: Need properer variantMaterials format validation?
13 | return object.material !== undefined && // easier than (!object.isMesh && !object.isLine && !object.isPoints)
14 | object.userData && // just in case
15 | object.userData.variantMaterials &&
16 | // Is this line costly?
17 | !!Object.values(object.userData.variantMaterials).filter(m => compatibleMaterial(m.material));
18 | };
19 |
20 | /**
21 | * @param material {THREE.Material}
22 | * @return {boolean}
23 | */
24 | const compatibleMaterial = material => {
25 | // @TODO: support multi materials?
26 | return material && material.isMaterial && !Array.isArray(material);
27 | };
28 |
29 | export default class GLTFExporterMaterialsVariantsExtension {
30 | constructor(writer) {
31 | this.writer = writer;
32 | this.name = 'KHR_materials_variants';
33 | this.variantNames = [];
34 | }
35 |
36 | beforeParse(objects) {
37 | // Find all variant names and store them to the table
38 | const variantNameTable = {};
39 | for (const object of objects) {
40 | object.traverse(o => {
41 | if (!compatibleObject(o)) {
42 | return;
43 | }
44 | const variantMaterials = o.userData.variantMaterials;
45 | for (const variantName in variantMaterials) {
46 | const variantMaterial = variantMaterials[variantName];
47 | // Ignore unloaded variant materials
48 | if (compatibleMaterial(variantMaterial.material)) {
49 | variantNameTable[variantName] = true;
50 | }
51 | }
52 | });
53 | }
54 | // We may want to sort?
55 | Object.keys(variantNameTable).forEach(name => this.variantNames.push(name));
56 | }
57 |
58 | writeMesh(mesh, meshDef) {
59 | if (!compatibleObject(mesh)) {
60 | return;
61 | }
62 |
63 | const userData = mesh.userData;
64 | const variantMaterials = userData.variantMaterials;
65 | const mappingTable = {};
66 | for (const variantName in variantMaterials) {
67 | const variantMaterialInstance = variantMaterials[variantName].material;
68 | if (!compatibleMaterial(variantMaterialInstance)) {
69 | continue;
70 | }
71 | const variantIndex = this.variantNames.indexOf(variantName); // Shouldn't be -1
72 | const materialIndex = this.writer.processMaterial(variantMaterialInstance);
73 | if (!mappingTable[materialIndex]) {
74 | mappingTable[materialIndex] = {
75 | material: materialIndex,
76 | variants: []
77 | };
78 | }
79 | mappingTable[materialIndex].variants.push(variantIndex);
80 | }
81 |
82 | const mappingsDef = Object.values(mappingTable)
83 | .map(m => {return m.variants.sort((a, b) => a - b) && m})
84 | .sort((a, b) => a.material - b.material);
85 |
86 | const originalMaterialIndex = compatibleMaterial(userData.originalMaterial)
87 | ? this.writer.processMaterial(userData.originalMaterial) : -1;
88 |
89 | for (const primitiveDef of meshDef.primitives) {
90 | // Override primitiveDef.material with original material.
91 | if (originalMaterialIndex >= 0) {
92 | primitiveDef.material = originalMaterialIndex;
93 | }
94 | primitiveDef.extensions = primitiveDef.extensions || {};
95 | primitiveDef.extensions[this.name] = {mappings: mappingsDef};
96 | }
97 | }
98 |
99 | afterParse(input) {
100 | if (this.variantNames.length === 0) {
101 | return;
102 | }
103 |
104 | const root = this.writer.json;
105 | root.extensions = root.extensions || {};
106 |
107 | const variantsDef = this.variantNames.map(n => {return {name: n};});
108 | root.extensions[this.name] = {variants: variantsDef};
109 | this.writer.extensionsUsed[this.name] = true;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/exporters/KHR_materials_variants/README.md:
--------------------------------------------------------------------------------
1 | # Three.js GLTFExporter [KHR_materials_variants](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants) extension
2 |
3 | ## How to use
4 |
5 | ```javascript
6 | import * as THREE from 'path_to_three.module.js';
7 | import {GLTFExporter} from 'path_to_GLTFExporter.js';
8 | import GLTFExporterMaterialsVariantsExtension from 'path_to_three-gltf-plugins/exporters/KHR_materials_variants/KHR_materials_variants_exporter.js';
9 |
10 | const exporter = new GLTFExporter();
11 | exporter.register(writer => new GLTFExporterMaterialsVariantsExtension(writer));
12 |
13 | gltf.functions.ensureLoadVariantMaterials(scene); // Refer to "ensureLoadVariants()" in the KHR_materials_variants GLTFLoader plugin README
14 | exporter.parse(scene, result => {
15 | ...
16 | });
17 | ```
18 |
19 | ## Compatible Three.js revision
20 |
21 | >= r126
22 |
23 | ## API
24 |
25 | The plugin traverses a scene graph and find valid `mesh.userData.variantMaterials` as the following layout
26 |
27 | ```javascript
28 | {
29 | variantName0: {
30 | material: Three.js material instance
31 | },
32 | variantName1: {
33 | material: Three.js material instance
34 | },
35 | ...
36 | }
37 | ```
38 |
39 | and embeds the variant names and materials as `KHR_materials_variants` extension into exported glTF content.
40 |
41 | And if `mesh.userData.originalMaterial` is defined as Three.js Material, the plugin overrides `gltf.meshes[n].primitive.material` with it.
42 |
43 | ## Limitations
44 |
45 | * Three.js multi material is not supported (yet)
46 | * The plugin doesn't verify `.userData` format. it's a user side responsibility.
47 | * If you want to export the models including variants materials loaded by the `GLTFLoader` KHR_materials_variants plugin, be careful that unloaded materials are not exported. If you want to ensure that all the variant materials will be exported, use `ensureLoadVariantMaterials()` of the loader plugin.
48 |
--------------------------------------------------------------------------------
/exporters/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/exporters/README.md
--------------------------------------------------------------------------------
/loaders/KHR_materials_variants/KHR_materials_variants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Materials variants extension
3 | *
4 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants
5 | */
6 |
7 | /**
8 | * KHR_materials_variants specification allows duplicated variant names
9 | * but it makes handling the extension complex.
10 | * We ensure tha names and make it easier.
11 | * If you want to export the extension with the original names
12 | * you are recommended to write GLTFExporter plugin to restore the names.
13 | *
14 | * @param variantNames {Array}
15 | * @return {Array}
16 | */
17 | const ensureUniqueNames = (variantNames) => {
18 | const uniqueNames = [];
19 | const knownNames = {};
20 |
21 | for (const name of variantNames) {
22 | let uniqueName = name;
23 | let suffix = 0;
24 | // @TODO: An easy solution.
25 | // O(N^2) in the worst scenario where N is variantNames.length.
26 | // Fix me if needed.
27 | while (knownNames[uniqueName] !== undefined) {
28 | uniqueName = name + '.' + (++suffix);
29 | }
30 | knownNames[uniqueName] = true;
31 | uniqueNames.push(uniqueName);
32 | }
33 |
34 | return uniqueNames;
35 | };
36 |
37 | /**
38 | * Convert mappings array to table object to make handling the extension easier.
39 | *
40 | * @param extensionDef {glTF.meshes[n].primitive.extensions.KHR_materials_variants}
41 | * @param variantNames {Array} Required to be unique names
42 | * @return {Object}
43 | */
44 | const mappingsArrayToTable = (extensionDef, variantNames) => {
45 | const table = {};
46 | for (const mapping of extensionDef.mappings) {
47 | for (const variant of mapping.variants) {
48 | table[variantNames[variant]] = {
49 | material: null,
50 | gltfMaterialIndex: mapping.material
51 | };
52 | }
53 | }
54 | return table;
55 | };
56 |
57 | /**
58 | * @param object {THREE.Object3D}
59 | * @return {boolean}
60 | */
61 | const compatibleObject = object => {
62 | return object.material !== undefined && // easier than (!object.isMesh && !object.isLine && !object.isPoints)
63 | object.userData && // just in case
64 | object.userData.variantMaterials;
65 | };
66 |
67 | export default class GLTFMaterialsVariantsExtension {
68 | constructor(parser) {
69 | this.parser = parser;
70 | this.name = 'KHR_materials_variants';
71 | }
72 |
73 | // Note that the following properties will be overridden even if they are pre-defined
74 | // - gltf.userData.variants
75 | // - mesh.userData.variantMaterials
76 | afterRoot(gltf) {
77 | const parser = this.parser;
78 | const json = parser.json;
79 |
80 | if (!json.extensions || !json.extensions[this.name]) {
81 | return null;
82 | }
83 |
84 | const extensionDef = json.extensions[this.name];
85 | const variantsDef = extensionDef.variants || [];
86 | const variants = ensureUniqueNames(variantsDef.map(v => v.name));
87 |
88 | for (const scene of gltf.scenes) {
89 | // Save the variants data under associated mesh.userData
90 | scene.traverse(object => {
91 | // The following code can be simplified if parser.associations directly supports meshes.
92 | const association = parser.associations.get(object);
93 |
94 | if (!association || association.type !== 'nodes') {
95 | return;
96 | }
97 |
98 | const nodeDef = json.nodes[association.index];
99 | const meshIndex = nodeDef.mesh;
100 |
101 | if (meshIndex === undefined) {
102 | return;
103 | }
104 |
105 | // Two limitations:
106 | // 1. The nodeDef shouldn't have any objects (camera, light, or nodeDef.extensions object)
107 | // other than nodeDef.mesh
108 | // 2. Other plugins shouldn't change any scene graph hierarchy
109 | // The following code can cause error if hitting the either or both limitations
110 | // If parser.associations will directly supports meshes these limitations can be removed
111 |
112 | const meshDef = json.meshes[meshIndex];
113 | const primitivesDef = meshDef.primitives;
114 | const meshes = object.isMesh ? [object] : object.children;
115 |
116 | for (let i = 0; i < primitivesDef.length; i++) {
117 | const primitiveDef = primitivesDef[i];
118 | const extensionsDef = primitiveDef.extensions;
119 | if (!extensionsDef || !extensionsDef[this.name]) {
120 | continue;
121 | }
122 | meshes[i].userData.variantMaterials = mappingsArrayToTable(extensionsDef[this.name], variants);
123 | }
124 | });
125 | }
126 |
127 | gltf.userData.variants = variants;
128 |
129 | // @TODO: Adding new unofficial property .functions.
130 | // It can be problematic especially with TypeScript?
131 | gltf.functions = gltf.functions || {};
132 |
133 | /**
134 | * @param object {THREE.Object3D}
135 | * @param variantName {string}
136 | * @return {Promise}
137 | */
138 | const switchMaterial = async (object, variantName) => {
139 | if (!object.userData.originalMaterial) {
140 | object.userData.originalMaterial = object.material;
141 | }
142 |
143 | if (!object.userData.variantMaterials[variantName]) {
144 | object.material = object.userData.originalMaterial;
145 | return;
146 | }
147 |
148 | if (object.userData.variantMaterials[variantName].material) {
149 | object.material = object.userData.variantMaterials[variantName].material;
150 | return;
151 | }
152 |
153 | const materialIndex = object.userData.variantMaterials[variantName].gltfMaterialIndex;
154 | object.material = await parser.getDependency('material', materialIndex);
155 | parser.assignFinalMaterial(object);
156 | object.userData.variantMaterials[variantName].material = object.material;
157 | };
158 |
159 | /**
160 | * @param object {THREE.Object3D}
161 | * @return {Promise}
162 | */
163 | const ensureLoadVariantMaterials = object => {
164 | const currentMaterial = object.material;
165 | const variantMaterials = object.userData.variantMaterials;
166 | const pending = [];
167 | for (const variantName in variantMaterials) {
168 | const variantMaterial = variantMaterials[variantName];
169 | if (variantMaterial.material) {
170 | continue;
171 | }
172 | const materialIndex = variantMaterial.gltfMaterialIndex;
173 | pending.push(parser.getDependency('material', materialIndex).then(material => {
174 | object.material = material;
175 | parser.assignFinalMaterial(object);
176 | variantMaterials[variantName].material = object.material;
177 | }));
178 | }
179 | return Promise.all(pending).then(() => {
180 | object.material = currentMaterial;
181 | });
182 | };
183 |
184 | /**
185 | * @param object {THREE.Object3D}
186 | * @param variantName {string}
187 | * @param doTraverse {boolean} Default is true
188 | * @return {Promise}
189 | */
190 | gltf.functions.selectVariant = (object, variantName, doTraverse = true) => {
191 | const pending = [];
192 | if (doTraverse) {
193 | object.traverse(o => compatibleObject(o) && pending.push(switchMaterial(o, variantName)));
194 | } else {
195 | compatibleObject(object) && pending.push(switchMaterial(object, variantName));
196 | }
197 | return Promise.all(pending);
198 | };
199 |
200 | /**
201 | * @param object {THREE.Object3D}
202 | * @param doTraverse {boolean} Default is true
203 | * @return {Promise}
204 | */
205 | gltf.functions.ensureLoadVariantMaterials = (object, doTraverse = true) => {
206 | const pending = [];
207 | if (doTraverse) {
208 | object.traverse(o => compatibleObject(o) && pending.push(ensureLoadVariantMaterials(o)));
209 | } else {
210 | compatibleObject(object) && pending.push(ensureLoadVariantMaterials(object));
211 | }
212 | return Promise.all(pending);
213 | };
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/loaders/KHR_materials_variants/README.md:
--------------------------------------------------------------------------------
1 | # [Three.js](https://threejs.org) [GLTFLoader](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) [KHR_materials_variants](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants) extension
2 |
3 | ## How to use
4 |
5 | ```javascript
6 | import * as THREE from 'path_to_three.module.js';
7 | import {GLTFLoader} from 'path_to_GLTFLoader.js';
8 | import GLTFMaterialsVariantsExtension from 'path_to_three-gltf-plugins/loaders/KHR_materials_variants/KHR_materials_variants.js';
9 |
10 | const loader = new GLTFLoader();
11 | loader.register(parser => new GLTFMaterialsVariantsExtension(parser));
12 | loader.load(path_to_gltf_asset, async gltf => {
13 | scene.add(gltf.scene);
14 | const variants = gltf.userData.variants; // [variantName0, variantName1, ...]
15 | const variantName = variants[theIndexYouWantToUse];
16 | await gltf.functions.selectVariant(gltf.scene, variantName);
17 | render();
18 | });
19 | ```
20 |
21 | ## Compatible Three.js revision
22 |
23 | >= r126dev
24 |
25 | ## API
26 |
27 | If glTF file includes `KHR_materials_variants` extension, the result returned by the loader will have the following data and function.
28 |
29 | **gltf.userData.variants**
30 |
31 | `variants` is an array of variant names like `[variantName0, variantName1, ...]`.
32 |
33 | **mesh.userData.variantMaterials**
34 |
35 | ```javascript
36 | {
37 | variantName0: {
38 | material: null,
39 | gltfMaterialIndex: number
40 | },
41 | variantName1: {
42 | material: null,
43 | gltfMaterialIndex: number
44 | },
45 | ...
46 | }
47 | ```
48 |
49 | `variantMaterials` is an object whose key is variant name and whose value has an index to glTF material index and variant material instance cache. Variant material cache is saved in `selectVariant()` or `ensureLoadVariants()`. The cache is also used when exporting variant materials.
50 |
51 | **gltf.functions.selectVariant(object: THREE.Object3D, variantName: string, doTraverse = true: boolean): Promise**
52 |
53 | `selectVariant()` is a function to switch materials to the ones associated with `variantName`. Unless `doTraverse` is set to `false` the function traverses the children and applys the change to all the child objects, too. The returned Promise will be resolved when all the selected materials are ready.
54 |
55 | **gltf.functions.ensureLoadVariants(object: THREE.Object3D, doTraverse = true: boolean): Promise**
56 |
57 | `ensureLoadVariants()` is a function to ensure all the variant materials are loaded and saved under `mesh.userData.variantMaterials` (See [Side effects](#Side-effects) below). Unless `doTraverse` is set to `false`, the function traverses the children and applys it to all the child objects, too. You are recommended to call this function before exporting objects with [GLTFExporter KHR_materials_variants plugin](../../exporters/KHR_materials_variants/#README.md)
58 |
59 | ## Side effects
60 |
61 | * `gltf.functions.selectVariant()` saves an original (core-spec) material as `mesh.userData.originalMaterial` for each Mesh
62 | * `gltf.functions.selectVariant()` saves a selected variant material as `mesh.userData.variantMaterials[variantName].material` for each Mesh. It is expected to be used when exporting with [GLTFExporter KHR_materials_variants plugin](../../exporters/KHR_materials_variants/#README.md).
63 | * The plugin ensures unique name in variant names so if duplicated names exist the plugin renames them. If you want to export the extension with the original names you are required to write an exporter plugin restoring the names.
64 |
65 | ## Replace with or add your custom variant materials
66 |
67 | You can replace with or add your custom variant materials in your application by setting your custom material instance to `mesh.userData.variantMaterials[variantName].material` like
68 |
69 | ```
70 | mesh.userData.variantMaterials[customVariantName] = {material: customVariantMaterial};
71 | ```
72 |
73 | You can omit `.gltfMaterialIndex` property and `customVariantName` doesn't have to be in `gltf.userData.variants`.
74 |
75 | ## Limitations
76 |
77 | * `selectVariant()` doesn't have selective option. All the meshes under a scene switch their materials.
78 | * `selectVariant()` doesn't have effect to meshes which are already removed from a scene
79 | * This plugin may not work if a glTF node has camera, light, or other extension objects in addition to mesh. This limitation may be removed if [this suggestion](https://github.com/mrdoob/three.js/pull/19359#issuecomment-774487100) is accepted.
80 | * `mesh.userData.variantMaterials` are not serialized by Three.js `toJSON()` method correctly because it doesn't support the serialization of Three.js objects under `.userData`.
81 |
--------------------------------------------------------------------------------
/loaders/MSFT_texture_dds/MSFT_texture_dds.js:
--------------------------------------------------------------------------------
1 | /**
2 | * DDS Texture Extension
3 | *
4 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds
5 | *
6 | */
7 | export default class GLTFTextureDDSExtension {
8 | constructor(parser, ddsLoader) {
9 | this.name = 'MSFT_texture_dds';
10 | this.parser = parser;
11 | this.ddsLoader = ddsLoader;
12 | }
13 |
14 | loadTexture(textureIndex) {
15 | const json = this.parser.json;
16 | const textureDef = json.textures[textureIndex];
17 | if (!textureDef.extensions || !textureDef.extensions[this.name]) {
18 | return null;
19 | }
20 | const extensionDef = textureDef.extensions[this.name];
21 | const source = json.images[extensionDef.source];
22 | return this.parser.loadTextureImage(textureIndex, source, this.ddsLoader);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/loaders/MSFT_texture_dds/README.md:
--------------------------------------------------------------------------------
1 | # [Three.js](https://threejs.org) [GLTFLoader](https://threejs.org/docs/#examples/en/loaders/GLTFLoader) [MSFT_texture_dds](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds) extension
2 |
3 | ## How to use
4 |
5 | ```javascript
6 | import * as THREE from 'path_to_three.module.js';
7 | import {GLTFLoader} from 'path_to_GLTFLoader.js';
8 | import {DDSLoader} from 'path_to_DDSLoader.js';
9 | import GLTFTextureDDSExtension from 'path_to_three-gltf-plugins/loaders/MSFT_texture_dds/MSFT_texture_dds.js';
10 |
11 | const loader = new GLTFLoader();
12 | loader.register(parser => new GLTFTextureDDS(parser, new DDSLoader()));
13 | loader.load(path_to_gltf_asset, gltf => {
14 | scene.add(gltf.scene);
15 | render();
16 | });
17 | ```
18 |
19 | ## Compatible Three.js revision
20 |
21 | >= r126dev
22 |
23 | ## Dependencies
24 |
25 | - [Three.js DDSLoader](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/loaders/DDSLoader.js)
26 |
27 | Pass `DDSLoader` instance to `GLTFTextureDDSExtension` constructor as the second argument.
28 |
--------------------------------------------------------------------------------
/loaders/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrdoob/three-gltf-plugins/4504f49468a7b81f1a28efb2a54d7535fc1ebadd/loaders/README.md
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "three-gltf-plugins",
3 | "version": "0.0.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "async": {
8 | "version": "2.6.3",
9 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
10 | "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
11 | "dev": true,
12 | "requires": {
13 | "lodash": "^4.17.14"
14 | }
15 | },
16 | "basic-auth": {
17 | "version": "1.1.0",
18 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz",
19 | "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=",
20 | "dev": true
21 | },
22 | "colors": {
23 | "version": "1.4.0",
24 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
25 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
26 | "dev": true
27 | },
28 | "corser": {
29 | "version": "2.0.1",
30 | "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
31 | "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
32 | "dev": true
33 | },
34 | "debug": {
35 | "version": "3.2.7",
36 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
37 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
38 | "dev": true,
39 | "requires": {
40 | "ms": "^2.1.1"
41 | }
42 | },
43 | "ecstatic": {
44 | "version": "3.3.2",
45 | "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
46 | "integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
47 | "dev": true,
48 | "requires": {
49 | "he": "^1.1.1",
50 | "mime": "^1.6.0",
51 | "minimist": "^1.1.0",
52 | "url-join": "^2.0.5"
53 | }
54 | },
55 | "eventemitter3": {
56 | "version": "4.0.7",
57 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
58 | "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
59 | "dev": true
60 | },
61 | "follow-redirects": {
62 | "version": "1.13.2",
63 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz",
64 | "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==",
65 | "dev": true
66 | },
67 | "he": {
68 | "version": "1.2.0",
69 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
70 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
71 | "dev": true
72 | },
73 | "http-proxy": {
74 | "version": "1.18.1",
75 | "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
76 | "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
77 | "dev": true,
78 | "requires": {
79 | "eventemitter3": "^4.0.0",
80 | "follow-redirects": "^1.0.0",
81 | "requires-port": "^1.0.0"
82 | }
83 | },
84 | "http-server": {
85 | "version": "0.12.3",
86 | "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.3.tgz",
87 | "integrity": "sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA==",
88 | "dev": true,
89 | "requires": {
90 | "basic-auth": "^1.0.3",
91 | "colors": "^1.4.0",
92 | "corser": "^2.0.1",
93 | "ecstatic": "^3.3.2",
94 | "http-proxy": "^1.18.0",
95 | "minimist": "^1.2.5",
96 | "opener": "^1.5.1",
97 | "portfinder": "^1.0.25",
98 | "secure-compare": "3.0.1",
99 | "union": "~0.5.0"
100 | }
101 | },
102 | "lodash": {
103 | "version": "4.17.20",
104 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
105 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
106 | "dev": true
107 | },
108 | "mime": {
109 | "version": "1.6.0",
110 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
111 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
112 | "dev": true
113 | },
114 | "minimist": {
115 | "version": "1.2.5",
116 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
117 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
118 | "dev": true
119 | },
120 | "mkdirp": {
121 | "version": "0.5.5",
122 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
123 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
124 | "dev": true,
125 | "requires": {
126 | "minimist": "^1.2.5"
127 | }
128 | },
129 | "ms": {
130 | "version": "2.1.3",
131 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
132 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
133 | "dev": true
134 | },
135 | "opener": {
136 | "version": "1.5.2",
137 | "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
138 | "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
139 | "dev": true
140 | },
141 | "portfinder": {
142 | "version": "1.0.28",
143 | "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
144 | "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
145 | "dev": true,
146 | "requires": {
147 | "async": "^2.6.2",
148 | "debug": "^3.1.1",
149 | "mkdirp": "^0.5.5"
150 | }
151 | },
152 | "qs": {
153 | "version": "6.9.6",
154 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
155 | "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==",
156 | "dev": true
157 | },
158 | "requires-port": {
159 | "version": "1.0.0",
160 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
161 | "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
162 | "dev": true
163 | },
164 | "secure-compare": {
165 | "version": "3.0.1",
166 | "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
167 | "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=",
168 | "dev": true
169 | },
170 | "union": {
171 | "version": "0.5.0",
172 | "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
173 | "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
174 | "dev": true,
175 | "requires": {
176 | "qs": "^6.4.0"
177 | }
178 | },
179 | "url-join": {
180 | "version": "2.0.5",
181 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
182 | "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
183 | "dev": true
184 | }
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "three-gltf-plugins",
3 | "version": "0.0.2",
4 | "description": "Three.js glTF loader/exporter unofficial plugins",
5 | "directories": {
6 | "example": "examples",
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "start": "http-server . -p 8080 -c-1",
11 | "test": "npm run run --prefix test",
12 | "test-build": "npm run build --prefix test",
13 | "test-install": "npm install --prefix test"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/takahirox/three-gltf-extensions.git"
18 | },
19 | "files": [
20 | "loaders",
21 | "exporters"
22 | ],
23 | "author": "Takahiro ",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/takahirox/three-gltf-extensions/issues"
27 | },
28 | "homepage": "https://github.com/takahirox/three-gltf-extensions#readme",
29 | "keywords": [
30 | "three",
31 | "three.js",
32 | "javascript",
33 | "3d",
34 | "gltf",
35 | "webgl",
36 | "webgl2"
37 | ],
38 | "devDependencies": {
39 | "http-server": "^0.12.3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/.gitignore:
--------------------------------------------------------------------------------
1 | qunit
2 | qunit.cmd
3 | qunit.ps1
4 | rollup
5 | rollup.cmd
6 | rollup.ps1
7 |
--------------------------------------------------------------------------------
/test/KHR_materials_variants.js:
--------------------------------------------------------------------------------
1 | /* global QUnit */
2 |
3 | import {GLTFLoader} from '../examples/three/loaders/GLTFLoader.js';
4 | import GLTFMaterialsVariantsExtension from '../loaders/KHR_materials_variants/KHR_materials_variants.js';
5 |
6 | const assetPath = '../examples/assets/gltf/MaterialsVariantsShoe/glTF/MaterialsVariantsShoe.gltf';
7 |
8 | export default QUnit.module('KHR_materials_variants', () => {
9 | QUnit.module('GLTFMaterialsVariantsExtension', () => {
10 | QUnit.test('register', assert => {
11 | const done = assert.async();
12 | new GLTFLoader()
13 | .register(parser => new GLTFMaterialsVariantsExtension(parser))
14 | .parse('{"asset": {"version": "2.0"}}', null, result => {
15 | assert.ok(true, 'can register');
16 | done();
17 | }, error => {
18 | assert.ok(false, 'can register');
19 | done();
20 | });
21 | });
22 | });
23 |
24 | QUnit.module('GLTFMaterialsVariantsExtension-webonly', () => {
25 | QUnit.test('parse', assert => {
26 | const done = assert.async();
27 | new GLTFLoader()
28 | .register(parser => new GLTFMaterialsVariantsExtension(parser))
29 | .load(assetPath, gltf => {
30 | assert.ok(true, 'can load');
31 |
32 | const variants = gltf.userData.variants;
33 | assert.ok(Array.isArray(variants) &&
34 | variants.length === 3 &&
35 | variants[0] === 'midnight' &&
36 | variants[1] === 'beach' &&
37 | variants[2] === 'street',
38 | 'expected variant names are saved under gltf.userData');
39 |
40 | const objects = [];
41 | gltf.scene.traverse(object => {
42 | if (object.isMesh && object.userData.variantMaterials) {
43 | objects.push(object);
44 | }
45 | });
46 |
47 | let validKey = true;
48 | let validInitialMaterial = true;
49 | let validGltfMaterialIndex = true;
50 |
51 | objects.forEach(object => {
52 | const variantMaterials = object.userData.variantMaterials;
53 | for (const key in variantMaterials) {
54 | const variant = variantMaterials[key];
55 | if (!variants.includes(key)) {
56 | validKey = false;
57 | }
58 | if (variant.material !== null) {
59 | validInitialMaterial = false;
60 | }
61 | // @TODO: Check index is in the length of gltfDef.materials
62 | if (typeof variant.gltfMaterialIndex !== 'number') {
63 | validGltfMaterialIndex = false;
64 | }
65 | }
66 | });
67 |
68 | assert.ok(objects.length > 0, 'variant materials info are saved under mesh.userData.variantMaterials');
69 | assert.ok(validKey, 'variantMaterials\' keys are included in variants');
70 | assert.ok(validInitialMaterial, 'initial variantMaterial.material is null');
71 | assert.ok(validGltfMaterialIndex, 'variant.gltfMaterialIndex is number');
72 |
73 | done();
74 | }, undefined, error => {
75 | assert.ok(false, 'can load');
76 | done();
77 | });
78 | });
79 |
80 | QUnit.todo('selectVariant', assert => {
81 | assert.ok(false);
82 | });
83 |
84 | QUnit.todo('ensureLoadVariants', assert => {
85 | assert.ok(false);
86 | });
87 | });
88 |
89 | QUnit.module('GLTFExporterMaterialsVariantsExtension-webonly', () => {
90 | QUnit.todo('export', assert => {
91 | assert.ok(false);
92 | });
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/test/MSFT_texture_dds.js:
--------------------------------------------------------------------------------
1 | /* global QUnit */
2 |
3 | import {GLTFLoader} from '../examples/three/loaders/GLTFLoader.js';
4 | import {DDSLoader} from '../examples/three/loaders/DDSLoader.js';
5 | import GLTFTextureDDSExtension from '../loaders/MSFT_texture_dds/MSFT_texture_dds.js';
6 |
7 | const assetPath = '../examples/assets/gltf/BoomBox/glTF-dds/BoomBox.gltf';
8 |
9 | export default QUnit.module('MSFT_texture_dds', () => {
10 | QUnit.module('GLTFMaterialsVariantsExtension', () => {
11 | QUnit.test('register', assert => {
12 | const done = assert.async();
13 | new GLTFLoader()
14 | .register(parser => new GLTFTextureDDSExtension(parser, new DDSLoader()))
15 | .parse('{"asset": {"version": "2.0"}}', null, result => {
16 | assert.ok(true, 'can register');
17 | done();
18 | }, error => {
19 | assert.ok(false, 'can register');
20 | done();
21 | });
22 | });
23 | });
24 |
25 | QUnit.module('GLTFMaterialsVariantsExtension-webonly', () => {
26 | QUnit.todo('parse', assert => {
27 | assert.ok(false);
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | three-gltf-plugins Unit Tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import './KHR_materials_variants.js'
2 | import './MSFT_texture_dds.js'
3 |
--------------------------------------------------------------------------------
/test/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-deps-installer",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "commander": {
8 | "version": "6.2.0",
9 | "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
10 | "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
11 | "dev": true
12 | },
13 | "fsevents": {
14 | "version": "2.3.2",
15 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
16 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
17 | "dev": true,
18 | "optional": true
19 | },
20 | "globalyzer": {
21 | "version": "0.1.0",
22 | "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
23 | "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
24 | "dev": true
25 | },
26 | "globrex": {
27 | "version": "0.1.2",
28 | "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
29 | "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
30 | "dev": true
31 | },
32 | "js-reporters": {
33 | "version": "1.2.3",
34 | "resolved": "https://registry.npmjs.org/js-reporters/-/js-reporters-1.2.3.tgz",
35 | "integrity": "sha512-2YzWkHbbRu6LueEs5ZP3P1LqbECvAeUJYrjw3H4y1ofW06hqCS0AbzBtLwbr+Hke51bt9CUepJ/Fj1hlCRIF6A==",
36 | "dev": true
37 | },
38 | "node-watch": {
39 | "version": "0.7.1",
40 | "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.1.tgz",
41 | "integrity": "sha512-UWblPYuZYrkCQCW5PxAwYSxaELNBLUckrTBBk8xr1/bUgyOkYYTsUcV4e3ytcazFEOyiRyiUrsG37pu6I0I05g==",
42 | "dev": true
43 | },
44 | "qunit": {
45 | "version": "2.14.0",
46 | "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.14.0.tgz",
47 | "integrity": "sha512-CYfenbgdpmhl2Ql2rDrrj0felY4h8k6lYhtWwGBCLL4qQC33YOj0psV8MWo85L1i0SIOmEDRXkFopWnGCLmf7g==",
48 | "dev": true,
49 | "requires": {
50 | "commander": "6.2.0",
51 | "js-reporters": "1.2.3",
52 | "node-watch": "0.7.1",
53 | "tiny-glob": "0.2.8"
54 | }
55 | },
56 | "rollup": {
57 | "version": "2.39.0",
58 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.39.0.tgz",
59 | "integrity": "sha512-+WR3bttcq7zE+BntH09UxaW3bQo3vItuYeLsyk4dL2tuwbeSKJuvwiawyhEnvRdRgrII0Uzk00FpctHO/zB1kw==",
60 | "dev": true,
61 | "requires": {
62 | "fsevents": "~2.3.1"
63 | }
64 | },
65 | "tiny-glob": {
66 | "version": "0.2.8",
67 | "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.8.tgz",
68 | "integrity": "sha512-vkQP7qOslq63XRX9kMswlby99kyO5OvKptw7AMwBVMjXEI7Tb61eoI5DydyEMOseyGS5anDN1VPoVxEvH01q8w==",
69 | "dev": true,
70 | "requires": {
71 | "globalyzer": "0.1.0",
72 | "globrex": "^0.1.2"
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-deps-installer",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "build": "rollup -c rollup.unit.config.js",
6 | "run": "npm run build && qunit -f !-webonly build/unit.js "
7 | },
8 | "devDependencies": {
9 | "qunit": "^2.14.0",
10 | "rollup": "^2.39.0"
11 | },
12 | "license": "MIT"
13 | }
14 |
--------------------------------------------------------------------------------
/test/rollup.unit.config.js:
--------------------------------------------------------------------------------
1 | require('qunit');
2 |
3 | export default [{
4 | input: 'index.js',
5 | output: [{
6 | format: 'umd',
7 | file: 'build/unit.js'
8 | }]
9 | }];
10 |
--------------------------------------------------------------------------------