├── .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 | [![npm version](https://badge.fury.io/js/three-gltf-plugins.svg)](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 |
12 | three.js - GLTFLoader/Exporter + KHR_materials_variants extension
13 | Materials Variants Shoe by 14 | Shopify, Inc
15 | Royal Esplanade by HDRI Haven 16 |
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 |
12 | three.js - GLTFLoader + MSFT_texture_dds extension
13 | BoomBox with DDS texture by MicroSoft
14 | Royal Esplanade by HDRI Haven 15 |
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 | ![screenshot](screenshot/screenshot.jpg) 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 |
11 | 14 |
15 |
16 |
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 | --------------------------------------------------------------------------------