├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── build └── webpack.config.js ├── example ├── app.js ├── app.json ├── app.wxss ├── index │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── loaders │ └── gltf-loader.js ├── miniprogram_npm │ └── threejs-miniprogram │ │ └── index.js ├── package.json ├── project.config.json ├── sitemap.json └── test-cases │ ├── cube.js │ ├── cubes.js │ ├── model.js │ ├── orbit.js │ └── sphere.js ├── package-lock.json ├── package.json └── src ├── EventTarget.js ├── Node.js ├── XMLHttpRequest.js ├── copyProperties.js └── index.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "babel-eslint", 4 | parserOptions: { 5 | ecmaVersion: 2018, 6 | sourceType: 'module' 7 | }, 8 | env: { 9 | es6: true, 10 | node: true 11 | }, 12 | extends: [ 13 | "eslint:recommended" 14 | ], 15 | globals: { 16 | 'XMLHttpRequest': true, 17 | 'window': true, 18 | 'document': true, 19 | 'navigator': true, 20 | 'wx': true 21 | }, 22 | rules: { 23 | 'no-console': process.env.NODE_ENV !== 'production' ? 0 : 2, 24 | 'no-useless-escape': 0, 25 | 'no-empty': 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | node_modules 10 | coverage 11 | dist 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build 2 | example 3 | src 4 | .eslintrc.js 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wechat-miniprogram 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 | # threejs-miniprogram 2 | Three.js 小程序 WebGL 的适配版本。 3 | 4 | ## 使用 5 | 6 | 可参考 example 目录下的示例项目或参照以下流程: 7 | 8 | 1. 通过 npm 安装 9 | 10 | ``` 11 | npm install --save threejs-miniprogram 12 | ``` 13 | 安装完成之后在微信开发者工具中点击构建 npm。 14 | 15 | 2. 导入小程序适配版本的 Three.js 16 | 17 | ```javascript 18 | import {createScopedThreejs} from 'threejs-miniprogram' 19 | 20 | Page({ 21 | onReady() { 22 | wx.createSelectorQuery() 23 | .select('#webgl') 24 | .node() 25 | .exec((res) => { 26 | const canvas = res[0].node 27 | // 创建一个与 canvas 绑定的 three.js 28 | const THREE = createScopedThreejs(canvas) 29 | // 传递并使用 THREE 变量 30 | }) 31 | } 32 | }) 33 | ``` 34 | 35 | ## 说明 36 | 37 | - 本项目当前使用的 Three.js 版本号为 0.108.0,如要更新 threejs 版本可发 PR 修改或 fork 后自行修改。 38 | - 该适配版本的 THREE 不在全局环境中,如使用 Three.js 的其他配套类库,需要自行传入 THREE 到类库中。 39 | - 如在使用过程中发现有适配问题,可通过 issue 反馈或发 PR 修复。 40 | -------------------------------------------------------------------------------- /build/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const StringReplacePlugin = require("string-replace-webpack-plugin"); 4 | 5 | function resolveThreeModule() { 6 | const threePath = require.resolve('three') 7 | const code = fs.readFileSync(path.resolve(threePath), 'utf8') 8 | return code 9 | } 10 | 11 | module.exports = { 12 | entry: path.join(__dirname, '../src/index'), 13 | target: 'web', 14 | output: { 15 | path: path.join(__dirname, '../dist'), 16 | filename: 'index.js', 17 | libraryTarget: 'commonjs', 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | loader: 'babel-loader', 25 | options: { 26 | presets: [ 27 | "@babel/preset-env", 28 | ], 29 | plugins: ["@babel/plugin-proposal-class-properties"] 30 | } 31 | }, 32 | { 33 | test: /\index.js$/, 34 | loader: StringReplacePlugin.replace({ 35 | replacements: [ 36 | { 37 | pattern: /__INJECT_THREE__/ig, 38 | replacement: () => { 39 | return resolveThreeModule() 40 | } 41 | } 42 | ] 43 | }) 44 | } 45 | ] 46 | }, 47 | plugins: [ 48 | // an instance of the plugin must be present 49 | new StringReplacePlugin() 50 | ], 51 | optimization:{ 52 | minimize: true, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | App({ 2 | onLaunch: function () { 3 | 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "index/index" 4 | ], 5 | "window": { 6 | "backgroundTextStyle": "light", 7 | "navigationBarBackgroundColor": "#fff", 8 | "navigationBarTitleText": "WeChat", 9 | "navigationBarTextStyle": "black" 10 | }, 11 | "sitemapLocation": "sitemap.json" 12 | } -------------------------------------------------------------------------------- /example/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | page { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | .container { 10 | height: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | justify-content: space-between; 15 | padding: 200rpx 0; 16 | box-sizing: border-box; 17 | } 18 | -------------------------------------------------------------------------------- /example/index/index.js: -------------------------------------------------------------------------------- 1 | // const { createScopedThreejs } = require('threejs-miniprogram') 2 | import { createScopedThreejs } from 'threejs-miniprogram' 3 | 4 | const { renderCube } = require('../test-cases/cube') 5 | const { renderCubes } = require('../test-cases/cubes') 6 | const { renderSphere } = require('../test-cases/sphere') 7 | const { renderModel } = require('../test-cases/model') 8 | 9 | const app = getApp() 10 | 11 | Page({ 12 | data: {}, 13 | onLoad: function () { 14 | wx.createSelectorQuery() 15 | .select('#webgl') 16 | .node() 17 | .exec((res) => { 18 | const canvas = res[0].node 19 | this.canvas = canvas 20 | const THREE = createScopedThreejs(canvas) 21 | 22 | // renderSphere(canvas, THREE) 23 | // renderCube(canvas, THREE) 24 | // renderCubes(canvas, THREE) 25 | renderModel(canvas, THREE) 26 | }) 27 | }, 28 | touchStart(e) { 29 | this.canvas.dispatchTouchEvent({...e, type:'touchstart'}) 30 | }, 31 | touchMove(e) { 32 | this.canvas.dispatchTouchEvent({...e, type:'touchmove'}) 33 | }, 34 | touchEnd(e) { 35 | this.canvas.dispatchTouchEvent({...e, type:'touchend'}) 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /example/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /example/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 15 | -------------------------------------------------------------------------------- /example/index/index.wxss: -------------------------------------------------------------------------------- 1 | .intro { 2 | margin: 30px; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /example/loaders/gltf-loader.js: -------------------------------------------------------------------------------- 1 | export function registerGLTFLoader(THREE) { 2 | 3 | /** 4 | * @author Rich Tibbett / https://github.com/richtr 5 | * @author mrdoob / http://mrdoob.com/ 6 | * @author Tony Parisi / http://www.tonyparisi.com/ 7 | * @author Takahiro / https://github.com/takahirox 8 | * @author Don McCurdy / https://www.donmccurdy.com 9 | */ 10 | 11 | THREE.GLTFLoader = (function () { 12 | 13 | function GLTFLoader(manager) { 14 | 15 | this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager; 16 | this.dracoLoader = null; 17 | this.ddsLoader = null; 18 | 19 | } 20 | 21 | GLTFLoader.prototype = { 22 | 23 | constructor: GLTFLoader, 24 | 25 | crossOrigin: 'anonymous', 26 | 27 | load: function (url, onLoad, onProgress, onError) { 28 | 29 | var scope = this; 30 | 31 | var resourcePath; 32 | 33 | if (this.resourcePath !== undefined) { 34 | 35 | resourcePath = this.resourcePath; 36 | 37 | } else if (this.path !== undefined) { 38 | 39 | resourcePath = this.path; 40 | 41 | } else { 42 | 43 | resourcePath = THREE.LoaderUtils.extractUrlBase(url); 44 | 45 | } 46 | 47 | // Tells the LoadingManager to track an extra item, which resolves after 48 | // the model is fully loaded. This means the count of items loaded will 49 | // be incorrect, but ensures manager.onLoad() does not fire early. 50 | scope.manager.itemStart(url); 51 | 52 | var _onError = function (e) { 53 | 54 | if (onError) { 55 | 56 | onError(e); 57 | 58 | } else { 59 | 60 | console.error(e); 61 | 62 | } 63 | 64 | scope.manager.itemError(url); 65 | scope.manager.itemEnd(url); 66 | 67 | }; 68 | 69 | var loader = new THREE.FileLoader(scope.manager); 70 | 71 | loader.setPath(this.path); 72 | loader.setResponseType('arraybuffer'); 73 | 74 | if (scope.crossOrigin === 'use-credentials') { 75 | 76 | loader.setWithCredentials(true); 77 | 78 | } 79 | 80 | loader.load(url, function (data) { 81 | 82 | try { 83 | 84 | scope.parse(data, resourcePath, function (gltf) { 85 | 86 | onLoad(gltf); 87 | 88 | scope.manager.itemEnd(url); 89 | 90 | }, _onError); 91 | 92 | } catch (e) { 93 | 94 | _onError(e); 95 | 96 | } 97 | 98 | }, onProgress, _onError); 99 | 100 | }, 101 | 102 | setCrossOrigin: function (value) { 103 | 104 | this.crossOrigin = value; 105 | return this; 106 | 107 | }, 108 | 109 | setPath: function (value) { 110 | 111 | this.path = value; 112 | return this; 113 | 114 | }, 115 | 116 | setResourcePath: function (value) { 117 | 118 | this.resourcePath = value; 119 | return this; 120 | 121 | }, 122 | 123 | setDRACOLoader: function (dracoLoader) { 124 | 125 | this.dracoLoader = dracoLoader; 126 | return this; 127 | 128 | }, 129 | 130 | setDDSLoader: function (ddsLoader) { 131 | 132 | this.ddsLoader = ddsLoader; 133 | return this; 134 | 135 | }, 136 | 137 | parse: function (data, path, onLoad, onError) { 138 | 139 | var content; 140 | var extensions = {}; 141 | 142 | if (typeof data === 'string') { 143 | 144 | content = data; 145 | 146 | } else { 147 | 148 | var magic = THREE.LoaderUtils.decodeText(new Uint8Array(data, 0, 4)); 149 | 150 | if (magic === BINARY_EXTENSION_HEADER_MAGIC) { 151 | 152 | try { 153 | 154 | extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data); 155 | 156 | } catch (error) { 157 | 158 | if (onError) onError(error); 159 | return; 160 | 161 | } 162 | 163 | content = extensions[EXTENSIONS.KHR_BINARY_GLTF].content; 164 | 165 | } else { 166 | 167 | content = THREE.LoaderUtils.decodeText(new Uint8Array(data)); 168 | 169 | } 170 | 171 | } 172 | 173 | var json = JSON.parse(content); 174 | 175 | if (json.asset === undefined || json.asset.version[0] < 2) { 176 | 177 | if (onError) onError(new Error('THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.')); 178 | return; 179 | 180 | } 181 | 182 | if (json.extensionsUsed) { 183 | 184 | for (var i = 0; i < json.extensionsUsed.length; ++i) { 185 | 186 | var extensionName = json.extensionsUsed[i]; 187 | var extensionsRequired = json.extensionsRequired || []; 188 | 189 | switch (extensionName) { 190 | 191 | case EXTENSIONS.KHR_LIGHTS_PUNCTUAL: 192 | extensions[extensionName] = new GLTFLightsExtension(json); 193 | break; 194 | 195 | case EXTENSIONS.KHR_MATERIALS_UNLIT: 196 | extensions[extensionName] = new GLTFMaterialsUnlitExtension(); 197 | break; 198 | 199 | case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 200 | extensions[extensionName] = new GLTFMaterialsPbrSpecularGlossinessExtension(); 201 | break; 202 | 203 | case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: 204 | extensions[extensionName] = new GLTFDracoMeshCompressionExtension(json, this.dracoLoader); 205 | break; 206 | 207 | case EXTENSIONS.MSFT_TEXTURE_DDS: 208 | extensions[EXTENSIONS.MSFT_TEXTURE_DDS] = new GLTFTextureDDSExtension(this.ddsLoader); 209 | break; 210 | 211 | case EXTENSIONS.KHR_TEXTURE_TRANSFORM: 212 | extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM] = new GLTFTextureTransformExtension(); 213 | break; 214 | 215 | default: 216 | 217 | if (extensionsRequired.indexOf(extensionName) >= 0) { 218 | 219 | console.warn('THREE.GLTFLoader: Unknown extension "' + extensionName + '".'); 220 | 221 | } 222 | 223 | } 224 | 225 | } 226 | 227 | } 228 | 229 | var parser = new GLTFParser(json, extensions, { 230 | 231 | path: path || this.resourcePath || '', 232 | crossOrigin: this.crossOrigin, 233 | manager: this.manager 234 | 235 | }); 236 | 237 | parser.parse(onLoad, onError); 238 | 239 | } 240 | 241 | }; 242 | 243 | /* GLTFREGISTRY */ 244 | 245 | function GLTFRegistry() { 246 | 247 | var objects = {}; 248 | 249 | return { 250 | 251 | get: function (key) { 252 | 253 | return objects[key]; 254 | 255 | }, 256 | 257 | add: function (key, object) { 258 | 259 | objects[key] = object; 260 | 261 | }, 262 | 263 | remove: function (key) { 264 | 265 | delete objects[key]; 266 | 267 | }, 268 | 269 | removeAll: function () { 270 | 271 | objects = {}; 272 | 273 | } 274 | 275 | }; 276 | 277 | } 278 | 279 | /*********************************/ 280 | /********** EXTENSIONS ***********/ 281 | /*********************************/ 282 | 283 | var EXTENSIONS = { 284 | KHR_BINARY_GLTF: 'KHR_binary_glTF', 285 | KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', 286 | KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', 287 | KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', 288 | KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', 289 | KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', 290 | MSFT_TEXTURE_DDS: 'MSFT_texture_dds' 291 | }; 292 | 293 | /** 294 | * DDS Texture Extension 295 | * 296 | * Specification: 297 | * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds 298 | * 299 | */ 300 | function GLTFTextureDDSExtension(ddsLoader) { 301 | 302 | if (!ddsLoader) { 303 | 304 | throw new Error('THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader'); 305 | 306 | } 307 | 308 | this.name = EXTENSIONS.MSFT_TEXTURE_DDS; 309 | this.ddsLoader = ddsLoader; 310 | 311 | } 312 | 313 | /** 314 | * Lights Extension 315 | * 316 | * Specification: PENDING 317 | */ 318 | function GLTFLightsExtension(json) { 319 | 320 | this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; 321 | 322 | var extension = (json.extensions && json.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL]) || {}; 323 | this.lightDefs = extension.lights || []; 324 | 325 | } 326 | 327 | GLTFLightsExtension.prototype.loadLight = function (lightIndex) { 328 | 329 | var lightDef = this.lightDefs[lightIndex]; 330 | var lightNode; 331 | 332 | var color = new THREE.Color(0xffffff); 333 | if (lightDef.color !== undefined) color.fromArray(lightDef.color); 334 | 335 | var range = lightDef.range !== undefined ? lightDef.range : 0; 336 | 337 | switch (lightDef.type) { 338 | 339 | case 'directional': 340 | lightNode = new THREE.DirectionalLight(color); 341 | lightNode.target.position.set(0, 0, - 1); 342 | lightNode.add(lightNode.target); 343 | break; 344 | 345 | case 'point': 346 | lightNode = new THREE.PointLight(color); 347 | lightNode.distance = range; 348 | break; 349 | 350 | case 'spot': 351 | lightNode = new THREE.SpotLight(color); 352 | lightNode.distance = range; 353 | // Handle spotlight properties. 354 | lightDef.spot = lightDef.spot || {}; 355 | lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; 356 | lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; 357 | lightNode.angle = lightDef.spot.outerConeAngle; 358 | lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; 359 | lightNode.target.position.set(0, 0, - 1); 360 | lightNode.add(lightNode.target); 361 | break; 362 | 363 | default: 364 | throw new Error('THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".'); 365 | 366 | } 367 | 368 | // Some lights (e.g. spot) default to a position other than the origin. Reset the position 369 | // here, because node-level parsing will only override position if explicitly specified. 370 | lightNode.position.set(0, 0, 0); 371 | 372 | lightNode.decay = 2; 373 | 374 | if (lightDef.intensity !== undefined) lightNode.intensity = lightDef.intensity; 375 | 376 | lightNode.name = lightDef.name || ('light_' + lightIndex); 377 | 378 | return Promise.resolve(lightNode); 379 | 380 | }; 381 | 382 | /** 383 | * Unlit Materials Extension (pending) 384 | * 385 | * PR: https://github.com/KhronosGroup/glTF/pull/1163 386 | */ 387 | function GLTFMaterialsUnlitExtension() { 388 | 389 | this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; 390 | 391 | } 392 | 393 | GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { 394 | 395 | return THREE.MeshBasicMaterial; 396 | 397 | }; 398 | 399 | GLTFMaterialsUnlitExtension.prototype.extendParams = function (materialParams, materialDef, parser) { 400 | 401 | var pending = []; 402 | 403 | materialParams.color = new THREE.Color(1.0, 1.0, 1.0); 404 | materialParams.opacity = 1.0; 405 | 406 | var metallicRoughness = materialDef.pbrMetallicRoughness; 407 | 408 | if (metallicRoughness) { 409 | 410 | if (Array.isArray(metallicRoughness.baseColorFactor)) { 411 | 412 | var array = metallicRoughness.baseColorFactor; 413 | 414 | materialParams.color.fromArray(array); 415 | materialParams.opacity = array[3]; 416 | 417 | } 418 | 419 | if (metallicRoughness.baseColorTexture !== undefined) { 420 | 421 | pending.push(parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture)); 422 | 423 | } 424 | 425 | } 426 | 427 | return Promise.all(pending); 428 | 429 | }; 430 | 431 | /* BINARY EXTENSION */ 432 | var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; 433 | var BINARY_EXTENSION_HEADER_LENGTH = 12; 434 | var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 }; 435 | 436 | function GLTFBinaryExtension(data) { 437 | 438 | this.name = EXTENSIONS.KHR_BINARY_GLTF; 439 | this.content = null; 440 | this.body = null; 441 | 442 | var headerView = new DataView(data, 0, BINARY_EXTENSION_HEADER_LENGTH); 443 | 444 | this.header = { 445 | magic: THREE.LoaderUtils.decodeText(new Uint8Array(data.slice(0, 4))), 446 | version: headerView.getUint32(4, true), 447 | length: headerView.getUint32(8, true) 448 | }; 449 | 450 | if (this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC) { 451 | 452 | throw new Error('THREE.GLTFLoader: Unsupported glTF-Binary header.'); 453 | 454 | } else if (this.header.version < 2.0) { 455 | 456 | throw new Error('THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.'); 457 | 458 | } 459 | 460 | var chunkView = new DataView(data, BINARY_EXTENSION_HEADER_LENGTH); 461 | var chunkIndex = 0; 462 | 463 | while (chunkIndex < chunkView.byteLength) { 464 | 465 | var chunkLength = chunkView.getUint32(chunkIndex, true); 466 | chunkIndex += 4; 467 | 468 | var chunkType = chunkView.getUint32(chunkIndex, true); 469 | chunkIndex += 4; 470 | 471 | if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON) { 472 | 473 | var contentArray = new Uint8Array(data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength); 474 | this.content = THREE.LoaderUtils.decodeText(contentArray); 475 | 476 | } else if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN) { 477 | 478 | var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; 479 | this.body = data.slice(byteOffset, byteOffset + chunkLength); 480 | 481 | } 482 | 483 | // Clients must ignore chunks with unknown types. 484 | 485 | chunkIndex += chunkLength; 486 | 487 | } 488 | 489 | if (this.content === null) { 490 | 491 | throw new Error('THREE.GLTFLoader: JSON content not found.'); 492 | 493 | } 494 | 495 | } 496 | 497 | /** 498 | * DRACO Mesh Compression Extension 499 | * 500 | * Specification: https://github.com/KhronosGroup/glTF/pull/874 501 | */ 502 | function GLTFDracoMeshCompressionExtension(json, dracoLoader) { 503 | 504 | if (!dracoLoader) { 505 | 506 | throw new Error('THREE.GLTFLoader: No DRACOLoader instance provided.'); 507 | 508 | } 509 | 510 | this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; 511 | this.json = json; 512 | this.dracoLoader = dracoLoader; 513 | 514 | } 515 | 516 | GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function (primitive, parser) { 517 | 518 | var json = this.json; 519 | var dracoLoader = this.dracoLoader; 520 | var bufferViewIndex = primitive.extensions[this.name].bufferView; 521 | var gltfAttributeMap = primitive.extensions[this.name].attributes; 522 | var threeAttributeMap = {}; 523 | var attributeNormalizedMap = {}; 524 | var attributeTypeMap = {}; 525 | 526 | for (var attributeName in gltfAttributeMap) { 527 | 528 | var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase(); 529 | 530 | threeAttributeMap[threeAttributeName] = gltfAttributeMap[attributeName]; 531 | 532 | } 533 | 534 | for (attributeName in primitive.attributes) { 535 | 536 | var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase(); 537 | 538 | if (gltfAttributeMap[attributeName] !== undefined) { 539 | 540 | var accessorDef = json.accessors[primitive.attributes[attributeName]]; 541 | var componentType = WEBGL_COMPONENT_TYPES[accessorDef.componentType]; 542 | 543 | attributeTypeMap[threeAttributeName] = componentType; 544 | attributeNormalizedMap[threeAttributeName] = accessorDef.normalized === true; 545 | 546 | } 547 | 548 | } 549 | 550 | return parser.getDependency('bufferView', bufferViewIndex).then(function (bufferView) { 551 | 552 | return new Promise(function (resolve) { 553 | 554 | dracoLoader.decodeDracoFile(bufferView, function (geometry) { 555 | 556 | for (var attributeName in geometry.attributes) { 557 | 558 | var attribute = geometry.attributes[attributeName]; 559 | var normalized = attributeNormalizedMap[attributeName]; 560 | 561 | if (normalized !== undefined) attribute.normalized = normalized; 562 | 563 | } 564 | 565 | resolve(geometry); 566 | 567 | }, threeAttributeMap, attributeTypeMap); 568 | 569 | }); 570 | 571 | }); 572 | 573 | }; 574 | 575 | /** 576 | * Texture Transform Extension 577 | * 578 | * Specification: 579 | */ 580 | function GLTFTextureTransformExtension() { 581 | 582 | this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; 583 | 584 | } 585 | 586 | GLTFTextureTransformExtension.prototype.extendTexture = function (texture, transform) { 587 | 588 | texture = texture.clone(); 589 | 590 | if (transform.offset !== undefined) { 591 | 592 | texture.offset.fromArray(transform.offset); 593 | 594 | } 595 | 596 | if (transform.rotation !== undefined) { 597 | 598 | texture.rotation = transform.rotation; 599 | 600 | } 601 | 602 | if (transform.scale !== undefined) { 603 | 604 | texture.repeat.fromArray(transform.scale); 605 | 606 | } 607 | 608 | if (transform.texCoord !== undefined) { 609 | 610 | console.warn('THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.'); 611 | 612 | } 613 | 614 | texture.needsUpdate = true; 615 | 616 | return texture; 617 | 618 | }; 619 | 620 | /** 621 | * Specular-Glossiness Extension 622 | * 623 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness 624 | */ 625 | function GLTFMaterialsPbrSpecularGlossinessExtension() { 626 | 627 | return { 628 | 629 | name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, 630 | 631 | specularGlossinessParams: [ 632 | 'color', 633 | 'map', 634 | 'lightMap', 635 | 'lightMapIntensity', 636 | 'aoMap', 637 | 'aoMapIntensity', 638 | 'emissive', 639 | 'emissiveIntensity', 640 | 'emissiveMap', 641 | 'bumpMap', 642 | 'bumpScale', 643 | 'normalMap', 644 | 'displacementMap', 645 | 'displacementScale', 646 | 'displacementBias', 647 | 'specularMap', 648 | 'specular', 649 | 'glossinessMap', 650 | 'glossiness', 651 | 'alphaMap', 652 | 'envMap', 653 | 'envMapIntensity', 654 | 'refractionRatio', 655 | ], 656 | 657 | getMaterialType: function () { 658 | 659 | return THREE.ShaderMaterial; 660 | 661 | }, 662 | 663 | extendParams: function (materialParams, materialDef, parser) { 664 | 665 | var pbrSpecularGlossiness = materialDef.extensions[this.name]; 666 | 667 | var shader = THREE.ShaderLib['standard']; 668 | 669 | var uniforms = THREE.UniformsUtils.clone(shader.uniforms); 670 | 671 | var specularMapParsFragmentChunk = [ 672 | '#ifdef USE_SPECULARMAP', 673 | ' uniform sampler2D specularMap;', 674 | '#endif' 675 | ].join('\n'); 676 | 677 | var glossinessMapParsFragmentChunk = [ 678 | '#ifdef USE_GLOSSINESSMAP', 679 | ' uniform sampler2D glossinessMap;', 680 | '#endif' 681 | ].join('\n'); 682 | 683 | var specularMapFragmentChunk = [ 684 | 'vec3 specularFactor = specular;', 685 | '#ifdef USE_SPECULARMAP', 686 | ' vec4 texelSpecular = texture2D( specularMap, vUv );', 687 | ' texelSpecular = sRGBToLinear( texelSpecular );', 688 | ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', 689 | ' specularFactor *= texelSpecular.rgb;', 690 | '#endif' 691 | ].join('\n'); 692 | 693 | var glossinessMapFragmentChunk = [ 694 | 'float glossinessFactor = glossiness;', 695 | '#ifdef USE_GLOSSINESSMAP', 696 | ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', 697 | ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', 698 | ' glossinessFactor *= texelGlossiness.a;', 699 | '#endif' 700 | ].join('\n'); 701 | 702 | var lightPhysicalFragmentChunk = [ 703 | 'PhysicalMaterial material;', 704 | 'material.diffuseColor = diffuseColor.rgb;', 705 | 'material.specularRoughness = clamp( 1.0 - glossinessFactor, 0.04, 1.0 );', 706 | 'material.specularColor = specularFactor.rgb;', 707 | ].join('\n'); 708 | 709 | var fragmentShader = shader.fragmentShader 710 | .replace('uniform float roughness;', 'uniform vec3 specular;') 711 | .replace('uniform float metalness;', 'uniform float glossiness;') 712 | .replace('#include ', specularMapParsFragmentChunk) 713 | .replace('#include ', glossinessMapParsFragmentChunk) 714 | .replace('#include ', specularMapFragmentChunk) 715 | .replace('#include ', glossinessMapFragmentChunk) 716 | .replace('#include ', lightPhysicalFragmentChunk); 717 | 718 | delete uniforms.roughness; 719 | delete uniforms.metalness; 720 | delete uniforms.roughnessMap; 721 | delete uniforms.metalnessMap; 722 | 723 | uniforms.specular = { value: new THREE.Color().setHex(0x111111) }; 724 | uniforms.glossiness = { value: 0.5 }; 725 | uniforms.specularMap = { value: null }; 726 | uniforms.glossinessMap = { value: null }; 727 | 728 | materialParams.vertexShader = shader.vertexShader; 729 | materialParams.fragmentShader = fragmentShader; 730 | materialParams.uniforms = uniforms; 731 | materialParams.defines = { 'STANDARD': '' } 732 | 733 | materialParams.color = new THREE.Color(1.0, 1.0, 1.0); 734 | materialParams.opacity = 1.0; 735 | 736 | var pending = []; 737 | 738 | if (Array.isArray(pbrSpecularGlossiness.diffuseFactor)) { 739 | 740 | var array = pbrSpecularGlossiness.diffuseFactor; 741 | 742 | materialParams.color.fromArray(array); 743 | materialParams.opacity = array[3]; 744 | 745 | } 746 | 747 | if (pbrSpecularGlossiness.diffuseTexture !== undefined) { 748 | 749 | pending.push(parser.assignTexture(materialParams, 'map', pbrSpecularGlossiness.diffuseTexture)); 750 | 751 | } 752 | 753 | materialParams.emissive = new THREE.Color(0.0, 0.0, 0.0); 754 | materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0; 755 | materialParams.specular = new THREE.Color(1.0, 1.0, 1.0); 756 | 757 | if (Array.isArray(pbrSpecularGlossiness.specularFactor)) { 758 | 759 | materialParams.specular.fromArray(pbrSpecularGlossiness.specularFactor); 760 | 761 | } 762 | 763 | if (pbrSpecularGlossiness.specularGlossinessTexture !== undefined) { 764 | 765 | var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; 766 | pending.push(parser.assignTexture(materialParams, 'glossinessMap', specGlossMapDef)); 767 | pending.push(parser.assignTexture(materialParams, 'specularMap', specGlossMapDef)); 768 | 769 | } 770 | 771 | return Promise.all(pending); 772 | 773 | }, 774 | 775 | createMaterial: function (params) { 776 | 777 | // setup material properties based on MeshStandardMaterial for Specular-Glossiness 778 | 779 | var material = new THREE.ShaderMaterial({ 780 | defines: params.defines, 781 | vertexShader: params.vertexShader, 782 | fragmentShader: params.fragmentShader, 783 | uniforms: params.uniforms, 784 | fog: true, 785 | lights: true, 786 | opacity: params.opacity, 787 | transparent: params.transparent 788 | }); 789 | 790 | material.isGLTFSpecularGlossinessMaterial = true; 791 | 792 | material.color = params.color; 793 | 794 | material.map = params.map === undefined ? null : params.map; 795 | 796 | material.lightMap = null; 797 | material.lightMapIntensity = 1.0; 798 | 799 | material.aoMap = params.aoMap === undefined ? null : params.aoMap; 800 | material.aoMapIntensity = 1.0; 801 | 802 | material.emissive = params.emissive; 803 | material.emissiveIntensity = 1.0; 804 | material.emissiveMap = params.emissiveMap === undefined ? null : params.emissiveMap; 805 | 806 | material.bumpMap = params.bumpMap === undefined ? null : params.bumpMap; 807 | material.bumpScale = 1; 808 | 809 | material.normalMap = params.normalMap === undefined ? null : params.normalMap; 810 | 811 | if (params.normalScale) material.normalScale = params.normalScale; 812 | 813 | material.displacementMap = null; 814 | material.displacementScale = 1; 815 | material.displacementBias = 0; 816 | 817 | material.specularMap = params.specularMap === undefined ? null : params.specularMap; 818 | material.specular = params.specular; 819 | 820 | material.glossinessMap = params.glossinessMap === undefined ? null : params.glossinessMap; 821 | material.glossiness = params.glossiness; 822 | 823 | material.alphaMap = null; 824 | 825 | material.envMap = params.envMap === undefined ? null : params.envMap; 826 | material.envMapIntensity = 1.0; 827 | 828 | material.refractionRatio = 0.98; 829 | 830 | material.extensions.derivatives = true; 831 | 832 | return material; 833 | 834 | }, 835 | 836 | /** 837 | * Clones a GLTFSpecularGlossinessMaterial instance. The ShaderMaterial.copy() method can 838 | * copy only properties it knows about or inherits, and misses many properties that would 839 | * normally be defined by MeshStandardMaterial. 840 | * 841 | * This method allows GLTFSpecularGlossinessMaterials to be cloned in the process of 842 | * loading a glTF model, but cloning later (e.g. by the user) would require these changes 843 | * AND also updating `.onBeforeRender` on the parent mesh. 844 | * 845 | * @param {THREE.ShaderMaterial} source 846 | * @return {THREE.ShaderMaterial} 847 | */ 848 | cloneMaterial: function (source) { 849 | 850 | var target = source.clone(); 851 | 852 | target.isGLTFSpecularGlossinessMaterial = true; 853 | 854 | var params = this.specularGlossinessParams; 855 | 856 | for (var i = 0, il = params.length; i < il; i++) { 857 | 858 | var value = source[params[i]]; 859 | target[params[i]] = (value && value.isColor) ? value.clone() : value; 860 | 861 | } 862 | 863 | return target; 864 | 865 | }, 866 | 867 | // Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer. 868 | refreshUniforms: function (renderer, scene, camera, geometry, material) { 869 | 870 | if (material.isGLTFSpecularGlossinessMaterial !== true) { 871 | 872 | return; 873 | 874 | } 875 | 876 | var uniforms = material.uniforms; 877 | var defines = material.defines; 878 | 879 | uniforms.opacity.value = material.opacity; 880 | 881 | uniforms.diffuse.value.copy(material.color); 882 | uniforms.emissive.value.copy(material.emissive).multiplyScalar(material.emissiveIntensity); 883 | 884 | uniforms.map.value = material.map; 885 | uniforms.specularMap.value = material.specularMap; 886 | uniforms.alphaMap.value = material.alphaMap; 887 | 888 | uniforms.lightMap.value = material.lightMap; 889 | uniforms.lightMapIntensity.value = material.lightMapIntensity; 890 | 891 | uniforms.aoMap.value = material.aoMap; 892 | uniforms.aoMapIntensity.value = material.aoMapIntensity; 893 | 894 | // uv repeat and offset setting priorities 895 | // 1. color map 896 | // 2. specular map 897 | // 3. normal map 898 | // 4. bump map 899 | // 5. alpha map 900 | // 6. emissive map 901 | 902 | var uvScaleMap; 903 | 904 | if (material.map) { 905 | 906 | uvScaleMap = material.map; 907 | 908 | } else if (material.specularMap) { 909 | 910 | uvScaleMap = material.specularMap; 911 | 912 | } else if (material.displacementMap) { 913 | 914 | uvScaleMap = material.displacementMap; 915 | 916 | } else if (material.normalMap) { 917 | 918 | uvScaleMap = material.normalMap; 919 | 920 | } else if (material.bumpMap) { 921 | 922 | uvScaleMap = material.bumpMap; 923 | 924 | } else if (material.glossinessMap) { 925 | 926 | uvScaleMap = material.glossinessMap; 927 | 928 | } else if (material.alphaMap) { 929 | 930 | uvScaleMap = material.alphaMap; 931 | 932 | } else if (material.emissiveMap) { 933 | 934 | uvScaleMap = material.emissiveMap; 935 | 936 | } 937 | 938 | if (uvScaleMap !== undefined) { 939 | 940 | // backwards compatibility 941 | if (uvScaleMap.isWebGLRenderTarget) { 942 | 943 | uvScaleMap = uvScaleMap.texture; 944 | 945 | } 946 | 947 | if (uvScaleMap.matrixAutoUpdate === true) { 948 | 949 | uvScaleMap.updateMatrix(); 950 | 951 | } 952 | 953 | uniforms.uvTransform.value.copy(uvScaleMap.matrix); 954 | 955 | } 956 | 957 | if (material.envMap) { 958 | 959 | uniforms.envMap.value = material.envMap; 960 | uniforms.envMapIntensity.value = material.envMapIntensity; 961 | 962 | // don't flip CubeTexture envMaps, flip everything else: 963 | // WebGLRenderTargetCube will be flipped for backwards compatibility 964 | // WebGLRenderTargetCube.texture will be flipped because it's a Texture and NOT a CubeTexture 965 | // this check must be handled differently, or removed entirely, if WebGLRenderTargetCube uses a CubeTexture in the future 966 | uniforms.flipEnvMap.value = material.envMap.isCubeTexture ? - 1 : 1; 967 | 968 | uniforms.reflectivity.value = material.reflectivity; 969 | uniforms.refractionRatio.value = material.refractionRatio; 970 | 971 | uniforms.maxMipLevel.value = renderer.properties.get(material.envMap).__maxMipLevel; 972 | 973 | } 974 | 975 | uniforms.specular.value.copy(material.specular); 976 | uniforms.glossiness.value = material.glossiness; 977 | 978 | uniforms.glossinessMap.value = material.glossinessMap; 979 | 980 | uniforms.emissiveMap.value = material.emissiveMap; 981 | uniforms.bumpMap.value = material.bumpMap; 982 | uniforms.normalMap.value = material.normalMap; 983 | 984 | uniforms.displacementMap.value = material.displacementMap; 985 | uniforms.displacementScale.value = material.displacementScale; 986 | uniforms.displacementBias.value = material.displacementBias; 987 | 988 | if (uniforms.glossinessMap.value !== null && defines.USE_GLOSSINESSMAP === undefined) { 989 | 990 | defines.USE_GLOSSINESSMAP = ''; 991 | // set USE_ROUGHNESSMAP to enable vUv 992 | defines.USE_ROUGHNESSMAP = ''; 993 | 994 | } 995 | 996 | if (uniforms.glossinessMap.value === null && defines.USE_GLOSSINESSMAP !== undefined) { 997 | 998 | delete defines.USE_GLOSSINESSMAP; 999 | delete defines.USE_ROUGHNESSMAP; 1000 | 1001 | } 1002 | 1003 | } 1004 | 1005 | }; 1006 | 1007 | } 1008 | 1009 | /*********************************/ 1010 | /********** INTERPOLATION ********/ 1011 | /*********************************/ 1012 | 1013 | // Spline Interpolation 1014 | // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation 1015 | function GLTFCubicSplineInterpolant(parameterPositions, sampleValues, sampleSize, resultBuffer) { 1016 | 1017 | THREE.Interpolant.call(this, parameterPositions, sampleValues, sampleSize, resultBuffer); 1018 | 1019 | } 1020 | 1021 | GLTFCubicSplineInterpolant.prototype = Object.create(THREE.Interpolant.prototype); 1022 | GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; 1023 | 1024 | GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function (index) { 1025 | 1026 | // Copies a sample value to the result buffer. See description of glTF 1027 | // CUBICSPLINE values layout in interpolate_() function below. 1028 | 1029 | var result = this.resultBuffer, 1030 | values = this.sampleValues, 1031 | valueSize = this.valueSize, 1032 | offset = index * valueSize * 3 + valueSize; 1033 | 1034 | for (var i = 0; i !== valueSize; i++) { 1035 | 1036 | result[i] = values[offset + i]; 1037 | 1038 | } 1039 | 1040 | return result; 1041 | 1042 | }; 1043 | 1044 | GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1045 | 1046 | GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_; 1047 | 1048 | GLTFCubicSplineInterpolant.prototype.interpolate_ = function (i1, t0, t, t1) { 1049 | 1050 | var result = this.resultBuffer; 1051 | var values = this.sampleValues; 1052 | var stride = this.valueSize; 1053 | 1054 | var stride2 = stride * 2; 1055 | var stride3 = stride * 3; 1056 | 1057 | var td = t1 - t0; 1058 | 1059 | var p = (t - t0) / td; 1060 | var pp = p * p; 1061 | var ppp = pp * p; 1062 | 1063 | var offset1 = i1 * stride3; 1064 | var offset0 = offset1 - stride3; 1065 | 1066 | var s2 = - 2 * ppp + 3 * pp; 1067 | var s3 = ppp - pp; 1068 | var s0 = 1 - s2; 1069 | var s1 = s3 - pp + p; 1070 | 1071 | // Layout of keyframe output values for CUBICSPLINE animations: 1072 | // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] 1073 | for (var i = 0; i !== stride; i++) { 1074 | 1075 | var p0 = values[offset0 + i + stride]; // splineVertex_k 1076 | var m0 = values[offset0 + i + stride2] * td; // outTangent_k * (t_k+1 - t_k) 1077 | var p1 = values[offset1 + i + stride]; // splineVertex_k+1 1078 | var m1 = values[offset1 + i] * td; // inTangent_k+1 * (t_k+1 - t_k) 1079 | 1080 | result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; 1081 | 1082 | } 1083 | 1084 | return result; 1085 | 1086 | }; 1087 | 1088 | /*********************************/ 1089 | /********** INTERNALS ************/ 1090 | /*********************************/ 1091 | 1092 | /* CONSTANTS */ 1093 | 1094 | var WEBGL_CONSTANTS = { 1095 | FLOAT: 5126, 1096 | //FLOAT_MAT2: 35674, 1097 | FLOAT_MAT3: 35675, 1098 | FLOAT_MAT4: 35676, 1099 | FLOAT_VEC2: 35664, 1100 | FLOAT_VEC3: 35665, 1101 | FLOAT_VEC4: 35666, 1102 | LINEAR: 9729, 1103 | REPEAT: 10497, 1104 | SAMPLER_2D: 35678, 1105 | POINTS: 0, 1106 | LINES: 1, 1107 | LINE_LOOP: 2, 1108 | LINE_STRIP: 3, 1109 | TRIANGLES: 4, 1110 | TRIANGLE_STRIP: 5, 1111 | TRIANGLE_FAN: 6, 1112 | UNSIGNED_BYTE: 5121, 1113 | UNSIGNED_SHORT: 5123 1114 | }; 1115 | 1116 | var WEBGL_COMPONENT_TYPES = { 1117 | 5120: Int8Array, 1118 | 5121: Uint8Array, 1119 | 5122: Int16Array, 1120 | 5123: Uint16Array, 1121 | 5125: Uint32Array, 1122 | 5126: Float32Array 1123 | }; 1124 | 1125 | var WEBGL_FILTERS = { 1126 | 9728: THREE.NearestFilter, 1127 | 9729: THREE.LinearFilter, 1128 | 9984: THREE.NearestMipmapNearestFilter, 1129 | 9985: THREE.LinearMipmapNearestFilter, 1130 | 9986: THREE.NearestMipmapLinearFilter, 1131 | 9987: THREE.LinearMipmapLinearFilter 1132 | }; 1133 | 1134 | var WEBGL_WRAPPINGS = { 1135 | 33071: THREE.ClampToEdgeWrapping, 1136 | 33648: THREE.MirroredRepeatWrapping, 1137 | 10497: THREE.RepeatWrapping 1138 | }; 1139 | 1140 | var WEBGL_TYPE_SIZES = { 1141 | 'SCALAR': 1, 1142 | 'VEC2': 2, 1143 | 'VEC3': 3, 1144 | 'VEC4': 4, 1145 | 'MAT2': 4, 1146 | 'MAT3': 9, 1147 | 'MAT4': 16 1148 | }; 1149 | 1150 | var ATTRIBUTES = { 1151 | POSITION: 'position', 1152 | NORMAL: 'normal', 1153 | TANGENT: 'tangent', 1154 | TEXCOORD_0: 'uv', 1155 | TEXCOORD_1: 'uv2', 1156 | COLOR_0: 'color', 1157 | WEIGHTS_0: 'skinWeight', 1158 | JOINTS_0: 'skinIndex', 1159 | }; 1160 | 1161 | var PATH_PROPERTIES = { 1162 | scale: 'scale', 1163 | translation: 'position', 1164 | rotation: 'quaternion', 1165 | weights: 'morphTargetInfluences' 1166 | }; 1167 | 1168 | var INTERPOLATION = { 1169 | CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each 1170 | // keyframe track will be initialized with a default interpolation type, then modified. 1171 | LINEAR: THREE.InterpolateLinear, 1172 | STEP: THREE.InterpolateDiscrete 1173 | }; 1174 | 1175 | var ALPHA_MODES = { 1176 | OPAQUE: 'OPAQUE', 1177 | MASK: 'MASK', 1178 | BLEND: 'BLEND' 1179 | }; 1180 | 1181 | var MIME_TYPE_FORMATS = { 1182 | 'image/png': THREE.RGBAFormat, 1183 | 'image/jpeg': THREE.RGBFormat 1184 | }; 1185 | 1186 | /* UTILITY FUNCTIONS */ 1187 | 1188 | function resolveURL(url, path) { 1189 | 1190 | // Invalid URL 1191 | if (typeof url !== 'string' || url === '') return ''; 1192 | 1193 | // Host Relative URL 1194 | if (/^https?:\/\//i.test(path) && /^\//.test(url)) { 1195 | 1196 | path = path.replace(/(^https?:\/\/[^\/]+).*/i, '$1'); 1197 | 1198 | } 1199 | 1200 | // Absolute URL http://,https://,// 1201 | if (/^(https?:)?\/\//i.test(url)) return url; 1202 | 1203 | // Data URI 1204 | if (/^data:.*,.*$/i.test(url)) return url; 1205 | 1206 | // Blob URL 1207 | if (/^blob:.*$/i.test(url)) return url; 1208 | 1209 | // Relative URL 1210 | return path + url; 1211 | 1212 | } 1213 | 1214 | var defaultMaterial; 1215 | 1216 | /** 1217 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material 1218 | */ 1219 | function createDefaultMaterial() { 1220 | 1221 | defaultMaterial = defaultMaterial || new THREE.MeshStandardMaterial({ 1222 | color: 0xFFFFFF, 1223 | emissive: 0x000000, 1224 | metalness: 1, 1225 | roughness: 1, 1226 | transparent: false, 1227 | depthTest: true, 1228 | side: THREE.FrontSide 1229 | }); 1230 | 1231 | return defaultMaterial; 1232 | 1233 | } 1234 | 1235 | function addUnknownExtensionsToUserData(knownExtensions, object, objectDef) { 1236 | 1237 | // Add unknown glTF extensions to an object's userData. 1238 | 1239 | for (var name in objectDef.extensions) { 1240 | 1241 | if (knownExtensions[name] === undefined) { 1242 | 1243 | object.userData.gltfExtensions = object.userData.gltfExtensions || {}; 1244 | object.userData.gltfExtensions[name] = objectDef.extensions[name]; 1245 | 1246 | } 1247 | 1248 | } 1249 | 1250 | } 1251 | 1252 | /** 1253 | * @param {THREE.Object3D|THREE.Material|THREE.BufferGeometry} object 1254 | * @param {GLTF.definition} gltfDef 1255 | */ 1256 | function assignExtrasToUserData(object, gltfDef) { 1257 | 1258 | if (gltfDef.extras !== undefined) { 1259 | 1260 | if (typeof gltfDef.extras === 'object') { 1261 | 1262 | Object.assign(object.userData, gltfDef.extras); 1263 | 1264 | } else { 1265 | 1266 | console.warn('THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras); 1267 | 1268 | } 1269 | 1270 | } 1271 | 1272 | } 1273 | 1274 | /** 1275 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets 1276 | * 1277 | * @param {THREE.BufferGeometry} geometry 1278 | * @param {Array} targets 1279 | * @param {GLTFParser} parser 1280 | * @return {Promise} 1281 | */ 1282 | function addMorphTargets(geometry, targets, parser) { 1283 | 1284 | var hasMorphPosition = false; 1285 | var hasMorphNormal = false; 1286 | 1287 | for (var i = 0, il = targets.length; i < il; i++) { 1288 | 1289 | var target = targets[i]; 1290 | 1291 | if (target.POSITION !== undefined) hasMorphPosition = true; 1292 | if (target.NORMAL !== undefined) hasMorphNormal = true; 1293 | 1294 | if (hasMorphPosition && hasMorphNormal) break; 1295 | 1296 | } 1297 | 1298 | if (!hasMorphPosition && !hasMorphNormal) return Promise.resolve(geometry); 1299 | 1300 | var pendingPositionAccessors = []; 1301 | var pendingNormalAccessors = []; 1302 | 1303 | for (var i = 0, il = targets.length; i < il; i++) { 1304 | 1305 | var target = targets[i]; 1306 | 1307 | if (hasMorphPosition) { 1308 | 1309 | var pendingAccessor = target.POSITION !== undefined 1310 | ? parser.getDependency('accessor', target.POSITION) 1311 | : geometry.attributes.position; 1312 | 1313 | pendingPositionAccessors.push(pendingAccessor); 1314 | 1315 | } 1316 | 1317 | if (hasMorphNormal) { 1318 | 1319 | var pendingAccessor = target.NORMAL !== undefined 1320 | ? parser.getDependency('accessor', target.NORMAL) 1321 | : geometry.attributes.normal; 1322 | 1323 | pendingNormalAccessors.push(pendingAccessor); 1324 | 1325 | } 1326 | 1327 | } 1328 | 1329 | return Promise.all([ 1330 | Promise.all(pendingPositionAccessors), 1331 | Promise.all(pendingNormalAccessors) 1332 | ]).then(function (accessors) { 1333 | 1334 | var morphPositions = accessors[0]; 1335 | var morphNormals = accessors[1]; 1336 | 1337 | // Clone morph target accessors before modifying them. 1338 | 1339 | for (var i = 0, il = morphPositions.length; i < il; i++) { 1340 | 1341 | if (geometry.attributes.position === morphPositions[i]) continue; 1342 | 1343 | morphPositions[i] = cloneBufferAttribute(morphPositions[i]); 1344 | 1345 | } 1346 | 1347 | for (var i = 0, il = morphNormals.length; i < il; i++) { 1348 | 1349 | if (geometry.attributes.normal === morphNormals[i]) continue; 1350 | 1351 | morphNormals[i] = cloneBufferAttribute(morphNormals[i]); 1352 | 1353 | } 1354 | 1355 | for (var i = 0, il = targets.length; i < il; i++) { 1356 | 1357 | var target = targets[i]; 1358 | var attributeName = 'morphTarget' + i; 1359 | 1360 | if (hasMorphPosition) { 1361 | 1362 | // Three.js morph position is absolute value. The formula is 1363 | // basePosition 1364 | // + weight0 * ( morphPosition0 - basePosition ) 1365 | // + weight1 * ( morphPosition1 - basePosition ) 1366 | // ... 1367 | // while the glTF one is relative 1368 | // basePosition 1369 | // + weight0 * glTFmorphPosition0 1370 | // + weight1 * glTFmorphPosition1 1371 | // ... 1372 | // then we need to convert from relative to absolute here. 1373 | 1374 | if (target.POSITION !== undefined) { 1375 | 1376 | var positionAttribute = morphPositions[i]; 1377 | positionAttribute.name = attributeName; 1378 | 1379 | var position = geometry.attributes.position; 1380 | 1381 | for (var j = 0, jl = positionAttribute.count; j < jl; j++) { 1382 | 1383 | positionAttribute.setXYZ( 1384 | j, 1385 | positionAttribute.getX(j) + position.getX(j), 1386 | positionAttribute.getY(j) + position.getY(j), 1387 | positionAttribute.getZ(j) + position.getZ(j) 1388 | ); 1389 | 1390 | } 1391 | 1392 | } 1393 | 1394 | } 1395 | 1396 | if (hasMorphNormal) { 1397 | 1398 | // see target.POSITION's comment 1399 | 1400 | if (target.NORMAL !== undefined) { 1401 | 1402 | var normalAttribute = morphNormals[i]; 1403 | normalAttribute.name = attributeName; 1404 | 1405 | var normal = geometry.attributes.normal; 1406 | 1407 | for (var j = 0, jl = normalAttribute.count; j < jl; j++) { 1408 | 1409 | normalAttribute.setXYZ( 1410 | j, 1411 | normalAttribute.getX(j) + normal.getX(j), 1412 | normalAttribute.getY(j) + normal.getY(j), 1413 | normalAttribute.getZ(j) + normal.getZ(j) 1414 | ); 1415 | 1416 | } 1417 | 1418 | } 1419 | 1420 | } 1421 | 1422 | } 1423 | 1424 | if (hasMorphPosition) geometry.morphAttributes.position = morphPositions; 1425 | if (hasMorphNormal) geometry.morphAttributes.normal = morphNormals; 1426 | 1427 | return geometry; 1428 | 1429 | }); 1430 | 1431 | } 1432 | 1433 | /** 1434 | * @param {THREE.Mesh} mesh 1435 | * @param {GLTF.Mesh} meshDef 1436 | */ 1437 | function updateMorphTargets(mesh, meshDef) { 1438 | 1439 | mesh.updateMorphTargets(); 1440 | 1441 | if (meshDef.weights !== undefined) { 1442 | 1443 | for (var i = 0, il = meshDef.weights.length; i < il; i++) { 1444 | 1445 | mesh.morphTargetInfluences[i] = meshDef.weights[i]; 1446 | 1447 | } 1448 | 1449 | } 1450 | 1451 | // .extras has user-defined data, so check that .extras.targetNames is an array. 1452 | if (meshDef.extras && Array.isArray(meshDef.extras.targetNames)) { 1453 | 1454 | var targetNames = meshDef.extras.targetNames; 1455 | 1456 | if (mesh.morphTargetInfluences.length === targetNames.length) { 1457 | 1458 | mesh.morphTargetDictionary = {}; 1459 | 1460 | for (var i = 0, il = targetNames.length; i < il; i++) { 1461 | 1462 | mesh.morphTargetDictionary[targetNames[i]] = i; 1463 | 1464 | } 1465 | 1466 | } else { 1467 | 1468 | console.warn('THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.'); 1469 | 1470 | } 1471 | 1472 | } 1473 | 1474 | } 1475 | 1476 | function createPrimitiveKey(primitiveDef) { 1477 | 1478 | var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]; 1479 | var geometryKey; 1480 | 1481 | if (dracoExtension) { 1482 | 1483 | geometryKey = 'draco:' + dracoExtension.bufferView 1484 | + ':' + dracoExtension.indices 1485 | + ':' + createAttributesKey(dracoExtension.attributes); 1486 | 1487 | } else { 1488 | 1489 | geometryKey = primitiveDef.indices + ':' + createAttributesKey(primitiveDef.attributes) + ':' + primitiveDef.mode; 1490 | 1491 | } 1492 | 1493 | return geometryKey; 1494 | 1495 | } 1496 | 1497 | function createAttributesKey(attributes) { 1498 | 1499 | var attributesKey = ''; 1500 | 1501 | var keys = Object.keys(attributes).sort(); 1502 | 1503 | for (var i = 0, il = keys.length; i < il; i++) { 1504 | 1505 | attributesKey += keys[i] + ':' + attributes[keys[i]] + ';'; 1506 | 1507 | } 1508 | 1509 | return attributesKey; 1510 | 1511 | } 1512 | 1513 | function cloneBufferAttribute(attribute) { 1514 | 1515 | if (attribute.isInterleavedBufferAttribute) { 1516 | 1517 | var count = attribute.count; 1518 | var itemSize = attribute.itemSize; 1519 | var array = attribute.array.slice(0, count * itemSize); 1520 | 1521 | for (var i = 0, j = 0; i < count; ++i) { 1522 | 1523 | array[j++] = attribute.getX(i); 1524 | if (itemSize >= 2) array[j++] = attribute.getY(i); 1525 | if (itemSize >= 3) array[j++] = attribute.getZ(i); 1526 | if (itemSize >= 4) array[j++] = attribute.getW(i); 1527 | 1528 | } 1529 | 1530 | return new THREE.BufferAttribute(array, itemSize, attribute.normalized); 1531 | 1532 | } 1533 | 1534 | return attribute.clone(); 1535 | 1536 | } 1537 | 1538 | /* GLTF PARSER */ 1539 | 1540 | function GLTFParser(json, extensions, options) { 1541 | 1542 | this.json = json || {}; 1543 | this.extensions = extensions || {}; 1544 | this.options = options || {}; 1545 | 1546 | // loader object cache 1547 | this.cache = new GLTFRegistry(); 1548 | 1549 | // BufferGeometry caching 1550 | this.primitiveCache = {}; 1551 | 1552 | this.textureLoader = new THREE.TextureLoader(this.options.manager); 1553 | this.textureLoader.setCrossOrigin(this.options.crossOrigin); 1554 | 1555 | this.fileLoader = new THREE.FileLoader(this.options.manager); 1556 | this.fileLoader.setResponseType('arraybuffer'); 1557 | 1558 | if (this.options.crossOrigin === 'use-credentials') { 1559 | 1560 | this.fileLoader.setWithCredentials(true); 1561 | 1562 | } 1563 | 1564 | } 1565 | 1566 | GLTFParser.prototype.parse = function (onLoad, onError) { 1567 | 1568 | var parser = this; 1569 | var json = this.json; 1570 | var extensions = this.extensions; 1571 | 1572 | // Clear the loader cache 1573 | this.cache.removeAll(); 1574 | 1575 | // Mark the special nodes/meshes in json for efficient parse 1576 | this.markDefs(); 1577 | 1578 | Promise.all([ 1579 | 1580 | this.getDependencies('scene'), 1581 | this.getDependencies('animation'), 1582 | this.getDependencies('camera'), 1583 | 1584 | ]).then(function (dependencies) { 1585 | 1586 | var result = { 1587 | scene: dependencies[0][json.scene || 0], 1588 | scenes: dependencies[0], 1589 | animations: dependencies[1], 1590 | cameras: dependencies[2], 1591 | asset: json.asset, 1592 | parser: parser, 1593 | userData: {} 1594 | }; 1595 | 1596 | addUnknownExtensionsToUserData(extensions, result, json); 1597 | 1598 | assignExtrasToUserData(result, json); 1599 | 1600 | onLoad(result); 1601 | 1602 | }).catch(onError); 1603 | 1604 | }; 1605 | 1606 | /** 1607 | * Marks the special nodes/meshes in json for efficient parse. 1608 | */ 1609 | GLTFParser.prototype.markDefs = function () { 1610 | 1611 | var nodeDefs = this.json.nodes || []; 1612 | var skinDefs = this.json.skins || []; 1613 | var meshDefs = this.json.meshes || []; 1614 | 1615 | var meshReferences = {}; 1616 | var meshUses = {}; 1617 | 1618 | // Nothing in the node definition indicates whether it is a Bone or an 1619 | // Object3D. Use the skins' joint references to mark bones. 1620 | for (var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex++) { 1621 | 1622 | var joints = skinDefs[skinIndex].joints; 1623 | 1624 | for (var i = 0, il = joints.length; i < il; i++) { 1625 | 1626 | nodeDefs[joints[i]].isBone = true; 1627 | 1628 | } 1629 | 1630 | } 1631 | 1632 | // Meshes can (and should) be reused by multiple nodes in a glTF asset. To 1633 | // avoid having more than one THREE.Mesh with the same name, count 1634 | // references and rename instances below. 1635 | // 1636 | // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. 1637 | for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) { 1638 | 1639 | var nodeDef = nodeDefs[nodeIndex]; 1640 | 1641 | if (nodeDef.mesh !== undefined) { 1642 | 1643 | if (meshReferences[nodeDef.mesh] === undefined) { 1644 | 1645 | meshReferences[nodeDef.mesh] = meshUses[nodeDef.mesh] = 0; 1646 | 1647 | } 1648 | 1649 | meshReferences[nodeDef.mesh]++; 1650 | 1651 | // Nothing in the mesh definition indicates whether it is 1652 | // a SkinnedMesh or Mesh. Use the node's mesh reference 1653 | // to mark SkinnedMesh if node has skin. 1654 | if (nodeDef.skin !== undefined) { 1655 | 1656 | meshDefs[nodeDef.mesh].isSkinnedMesh = true; 1657 | 1658 | } 1659 | 1660 | } 1661 | 1662 | } 1663 | 1664 | this.json.meshReferences = meshReferences; 1665 | this.json.meshUses = meshUses; 1666 | 1667 | }; 1668 | 1669 | /** 1670 | * Requests the specified dependency asynchronously, with caching. 1671 | * @param {string} type 1672 | * @param {number} index 1673 | * @return {Promise} 1674 | */ 1675 | GLTFParser.prototype.getDependency = function (type, index) { 1676 | 1677 | var cacheKey = type + ':' + index; 1678 | var dependency = this.cache.get(cacheKey); 1679 | 1680 | if (!dependency) { 1681 | 1682 | switch (type) { 1683 | 1684 | case 'scene': 1685 | dependency = this.loadScene(index); 1686 | break; 1687 | 1688 | case 'node': 1689 | dependency = this.loadNode(index); 1690 | break; 1691 | 1692 | case 'mesh': 1693 | dependency = this.loadMesh(index); 1694 | break; 1695 | 1696 | case 'accessor': 1697 | dependency = this.loadAccessor(index); 1698 | break; 1699 | 1700 | case 'bufferView': 1701 | dependency = this.loadBufferView(index); 1702 | break; 1703 | 1704 | case 'buffer': 1705 | dependency = this.loadBuffer(index); 1706 | break; 1707 | 1708 | case 'material': 1709 | dependency = this.loadMaterial(index); 1710 | break; 1711 | 1712 | case 'texture': 1713 | dependency = this.loadTexture(index); 1714 | break; 1715 | 1716 | case 'skin': 1717 | dependency = this.loadSkin(index); 1718 | break; 1719 | 1720 | case 'animation': 1721 | dependency = this.loadAnimation(index); 1722 | break; 1723 | 1724 | case 'camera': 1725 | dependency = this.loadCamera(index); 1726 | break; 1727 | 1728 | case 'light': 1729 | dependency = this.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].loadLight(index); 1730 | break; 1731 | 1732 | default: 1733 | throw new Error('Unknown type: ' + type); 1734 | 1735 | } 1736 | 1737 | this.cache.add(cacheKey, dependency); 1738 | 1739 | } 1740 | 1741 | return dependency; 1742 | 1743 | }; 1744 | 1745 | /** 1746 | * Requests all dependencies of the specified type asynchronously, with caching. 1747 | * @param {string} type 1748 | * @return {Promise>} 1749 | */ 1750 | GLTFParser.prototype.getDependencies = function (type) { 1751 | 1752 | var dependencies = this.cache.get(type); 1753 | 1754 | if (!dependencies) { 1755 | 1756 | var parser = this; 1757 | var defs = this.json[type + (type === 'mesh' ? 'es' : 's')] || []; 1758 | 1759 | dependencies = Promise.all(defs.map(function (def, index) { 1760 | 1761 | return parser.getDependency(type, index); 1762 | 1763 | })); 1764 | 1765 | this.cache.add(type, dependencies); 1766 | 1767 | } 1768 | 1769 | return dependencies; 1770 | 1771 | }; 1772 | 1773 | /** 1774 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1775 | * @param {number} bufferIndex 1776 | * @return {Promise} 1777 | */ 1778 | GLTFParser.prototype.loadBuffer = function (bufferIndex) { 1779 | 1780 | var bufferDef = this.json.buffers[bufferIndex]; 1781 | var loader = this.fileLoader; 1782 | 1783 | if (bufferDef.type && bufferDef.type !== 'arraybuffer') { 1784 | 1785 | throw new Error('THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.'); 1786 | 1787 | } 1788 | 1789 | // If present, GLB container is required to be the first buffer. 1790 | if (bufferDef.uri === undefined && bufferIndex === 0) { 1791 | 1792 | return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body); 1793 | 1794 | } 1795 | 1796 | var options = this.options; 1797 | 1798 | return new Promise(function (resolve, reject) { 1799 | 1800 | loader.load(resolveURL(bufferDef.uri, options.path), resolve, undefined, function () { 1801 | 1802 | reject(new Error('THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".')); 1803 | 1804 | }); 1805 | 1806 | }); 1807 | 1808 | }; 1809 | 1810 | /** 1811 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views 1812 | * @param {number} bufferViewIndex 1813 | * @return {Promise} 1814 | */ 1815 | GLTFParser.prototype.loadBufferView = function (bufferViewIndex) { 1816 | 1817 | var bufferViewDef = this.json.bufferViews[bufferViewIndex]; 1818 | 1819 | return this.getDependency('buffer', bufferViewDef.buffer).then(function (buffer) { 1820 | 1821 | var byteLength = bufferViewDef.byteLength || 0; 1822 | var byteOffset = bufferViewDef.byteOffset || 0; 1823 | return buffer.slice(byteOffset, byteOffset + byteLength); 1824 | 1825 | }); 1826 | 1827 | }; 1828 | 1829 | /** 1830 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors 1831 | * @param {number} accessorIndex 1832 | * @return {Promise} 1833 | */ 1834 | GLTFParser.prototype.loadAccessor = function (accessorIndex) { 1835 | 1836 | var parser = this; 1837 | var json = this.json; 1838 | 1839 | var accessorDef = this.json.accessors[accessorIndex]; 1840 | 1841 | if (accessorDef.bufferView === undefined && accessorDef.sparse === undefined) { 1842 | 1843 | // Ignore empty accessors, which may be used to declare runtime 1844 | // information about attributes coming from another source (e.g. Draco 1845 | // compression extension). 1846 | return Promise.resolve(null); 1847 | 1848 | } 1849 | 1850 | var pendingBufferViews = []; 1851 | 1852 | if (accessorDef.bufferView !== undefined) { 1853 | 1854 | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.bufferView)); 1855 | 1856 | } else { 1857 | 1858 | pendingBufferViews.push(null); 1859 | 1860 | } 1861 | 1862 | if (accessorDef.sparse !== undefined) { 1863 | 1864 | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.indices.bufferView)); 1865 | pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.values.bufferView)); 1866 | 1867 | } 1868 | 1869 | return Promise.all(pendingBufferViews).then(function (bufferViews) { 1870 | 1871 | var bufferView = bufferViews[0]; 1872 | 1873 | var itemSize = WEBGL_TYPE_SIZES[accessorDef.type]; 1874 | var TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType]; 1875 | 1876 | // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. 1877 | var elementBytes = TypedArray.BYTES_PER_ELEMENT; 1878 | var itemBytes = elementBytes * itemSize; 1879 | var byteOffset = accessorDef.byteOffset || 0; 1880 | var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[accessorDef.bufferView].byteStride : undefined; 1881 | var normalized = accessorDef.normalized === true; 1882 | var array, bufferAttribute; 1883 | 1884 | // The buffer is not interleaved if the stride is the item size in bytes. 1885 | if (byteStride && byteStride !== itemBytes) { 1886 | 1887 | // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer 1888 | // This makes sure that IBA.count reflects accessor.count properly 1889 | var ibSlice = Math.floor(byteOffset / byteStride); 1890 | var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count; 1891 | var ib = parser.cache.get(ibCacheKey); 1892 | 1893 | if (!ib) { 1894 | 1895 | array = new TypedArray(bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes); 1896 | 1897 | // Integer parameters to IB/IBA are in array elements, not bytes. 1898 | ib = new THREE.InterleavedBuffer(array, byteStride / elementBytes); 1899 | 1900 | parser.cache.add(ibCacheKey, ib); 1901 | 1902 | } 1903 | 1904 | bufferAttribute = new THREE.InterleavedBufferAttribute(ib, itemSize, (byteOffset % byteStride) / elementBytes, normalized); 1905 | 1906 | } else { 1907 | 1908 | if (bufferView === null) { 1909 | 1910 | array = new TypedArray(accessorDef.count * itemSize); 1911 | 1912 | } else { 1913 | 1914 | array = new TypedArray(bufferView, byteOffset, accessorDef.count * itemSize); 1915 | 1916 | } 1917 | 1918 | bufferAttribute = new THREE.BufferAttribute(array, itemSize, normalized); 1919 | 1920 | } 1921 | 1922 | // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors 1923 | if (accessorDef.sparse !== undefined) { 1924 | 1925 | var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; 1926 | var TypedArrayIndices = WEBGL_COMPONENT_TYPES[accessorDef.sparse.indices.componentType]; 1927 | 1928 | var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; 1929 | var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; 1930 | 1931 | var sparseIndices = new TypedArrayIndices(bufferViews[1], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices); 1932 | var sparseValues = new TypedArray(bufferViews[2], byteOffsetValues, accessorDef.sparse.count * itemSize); 1933 | 1934 | if (bufferView !== null) { 1935 | 1936 | // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. 1937 | bufferAttribute.setArray(bufferAttribute.array.slice()); 1938 | 1939 | } 1940 | 1941 | for (var i = 0, il = sparseIndices.length; i < il; i++) { 1942 | 1943 | var index = sparseIndices[i]; 1944 | 1945 | bufferAttribute.setX(index, sparseValues[i * itemSize]); 1946 | if (itemSize >= 2) bufferAttribute.setY(index, sparseValues[i * itemSize + 1]); 1947 | if (itemSize >= 3) bufferAttribute.setZ(index, sparseValues[i * itemSize + 2]); 1948 | if (itemSize >= 4) bufferAttribute.setW(index, sparseValues[i * itemSize + 3]); 1949 | if (itemSize >= 5) throw new Error('THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.'); 1950 | 1951 | } 1952 | 1953 | } 1954 | 1955 | return bufferAttribute; 1956 | 1957 | }); 1958 | 1959 | }; 1960 | 1961 | /** 1962 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures 1963 | * @param {number} textureIndex 1964 | * @return {Promise} 1965 | */ 1966 | GLTFParser.prototype.loadTexture = function (textureIndex) { 1967 | 1968 | var parser = this; 1969 | var json = this.json; 1970 | var options = this.options; 1971 | var textureLoader = this.textureLoader; 1972 | 1973 | var URL = window.URL || window.webkitURL; 1974 | 1975 | var textureDef = json.textures[textureIndex]; 1976 | 1977 | var textureExtensions = textureDef.extensions || {}; 1978 | 1979 | var source; 1980 | 1981 | if (textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS]) { 1982 | 1983 | source = json.images[textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS].source]; 1984 | 1985 | } else { 1986 | 1987 | source = json.images[textureDef.source]; 1988 | 1989 | } 1990 | 1991 | var sourceURI = source.uri; 1992 | var isObjectURL = false; 1993 | 1994 | if (source.bufferView !== undefined) { 1995 | 1996 | // Load binary image data from bufferView, if provided. 1997 | 1998 | sourceURI = parser.getDependency('bufferView', source.bufferView).then(function (bufferView) { 1999 | 2000 | isObjectURL = true; 2001 | var blob = new Blob([bufferView], { type: source.mimeType }); 2002 | sourceURI = URL.createObjectURL(blob); 2003 | return sourceURI; 2004 | 2005 | }); 2006 | 2007 | } 2008 | 2009 | return Promise.resolve(sourceURI).then(function (sourceURI) { 2010 | 2011 | // Load Texture resource. 2012 | 2013 | var loader = THREE.Loader.Handlers.get(sourceURI); 2014 | 2015 | if (!loader) { 2016 | 2017 | loader = textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS] 2018 | ? parser.extensions[EXTENSIONS.MSFT_TEXTURE_DDS].ddsLoader 2019 | : textureLoader; 2020 | 2021 | } 2022 | 2023 | return new Promise(function (resolve, reject) { 2024 | 2025 | loader.load(resolveURL(sourceURI, options.path), resolve, undefined, reject); 2026 | 2027 | }); 2028 | 2029 | }).then(function (texture) { 2030 | 2031 | // Clean up resources and configure Texture. 2032 | 2033 | if (isObjectURL === true) { 2034 | 2035 | URL.revokeObjectURL(sourceURI); 2036 | 2037 | } 2038 | 2039 | texture.flipY = false; 2040 | 2041 | if (textureDef.name !== undefined) texture.name = textureDef.name; 2042 | 2043 | // Ignore unknown mime types, like DDS files. 2044 | if (source.mimeType in MIME_TYPE_FORMATS) { 2045 | 2046 | texture.format = MIME_TYPE_FORMATS[source.mimeType]; 2047 | 2048 | } 2049 | 2050 | var samplers = json.samplers || {}; 2051 | var sampler = samplers[textureDef.sampler] || {}; 2052 | 2053 | texture.magFilter = WEBGL_FILTERS[sampler.magFilter] || THREE.LinearFilter; 2054 | texture.minFilter = WEBGL_FILTERS[sampler.minFilter] || THREE.LinearMipmapLinearFilter; 2055 | texture.wrapS = WEBGL_WRAPPINGS[sampler.wrapS] || THREE.RepeatWrapping; 2056 | texture.wrapT = WEBGL_WRAPPINGS[sampler.wrapT] || THREE.RepeatWrapping; 2057 | 2058 | return texture; 2059 | 2060 | }); 2061 | 2062 | }; 2063 | 2064 | /** 2065 | * Asynchronously assigns a texture to the given material parameters. 2066 | * @param {Object} materialParams 2067 | * @param {string} mapName 2068 | * @param {Object} mapDef 2069 | * @return {Promise} 2070 | */ 2071 | GLTFParser.prototype.assignTexture = function (materialParams, mapName, mapDef) { 2072 | 2073 | var parser = this; 2074 | 2075 | return this.getDependency('texture', mapDef.index).then(function (texture) { 2076 | 2077 | if (!texture.isCompressedTexture) { 2078 | 2079 | switch (mapName) { 2080 | 2081 | case 'aoMap': 2082 | case 'emissiveMap': 2083 | case 'metalnessMap': 2084 | case 'normalMap': 2085 | case 'roughnessMap': 2086 | texture.format = THREE.RGBFormat; 2087 | break; 2088 | 2089 | } 2090 | 2091 | } 2092 | 2093 | if (parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM]) { 2094 | 2095 | var transform = mapDef.extensions !== undefined ? mapDef.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM] : undefined; 2096 | 2097 | if (transform) { 2098 | 2099 | texture = parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM].extendTexture(texture, transform); 2100 | 2101 | } 2102 | 2103 | } 2104 | 2105 | materialParams[mapName] = texture; 2106 | 2107 | }); 2108 | 2109 | }; 2110 | 2111 | /** 2112 | * Assigns final material to a Mesh, Line, or Points instance. The instance 2113 | * already has a material (generated from the glTF material options alone) 2114 | * but reuse of the same glTF material may require multiple threejs materials 2115 | * to accomodate different primitive types, defines, etc. New materials will 2116 | * be created if necessary, and reused from a cache. 2117 | * @param {THREE.Object3D} mesh Mesh, Line, or Points instance. 2118 | */ 2119 | GLTFParser.prototype.assignFinalMaterial = function (mesh) { 2120 | 2121 | var geometry = mesh.geometry; 2122 | var material = mesh.material; 2123 | var extensions = this.extensions; 2124 | 2125 | var useVertexTangents = geometry.attributes.tangent !== undefined; 2126 | var useVertexColors = geometry.attributes.color !== undefined; 2127 | var useFlatShading = geometry.attributes.normal === undefined; 2128 | var useSkinning = mesh.isSkinnedMesh === true; 2129 | var useMorphTargets = Object.keys(geometry.morphAttributes).length > 0; 2130 | var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; 2131 | 2132 | if (mesh.isPoints) { 2133 | 2134 | var cacheKey = 'PointsMaterial:' + material.uuid; 2135 | 2136 | var pointsMaterial = this.cache.get(cacheKey); 2137 | 2138 | if (!pointsMaterial) { 2139 | 2140 | pointsMaterial = new THREE.PointsMaterial(); 2141 | THREE.Material.prototype.copy.call(pointsMaterial, material); 2142 | pointsMaterial.color.copy(material.color); 2143 | pointsMaterial.map = material.map; 2144 | pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet 2145 | pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px 2146 | 2147 | this.cache.add(cacheKey, pointsMaterial); 2148 | 2149 | } 2150 | 2151 | material = pointsMaterial; 2152 | 2153 | } else if (mesh.isLine) { 2154 | 2155 | var cacheKey = 'LineBasicMaterial:' + material.uuid; 2156 | 2157 | var lineMaterial = this.cache.get(cacheKey); 2158 | 2159 | if (!lineMaterial) { 2160 | 2161 | lineMaterial = new THREE.LineBasicMaterial(); 2162 | THREE.Material.prototype.copy.call(lineMaterial, material); 2163 | lineMaterial.color.copy(material.color); 2164 | lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet 2165 | 2166 | this.cache.add(cacheKey, lineMaterial); 2167 | 2168 | } 2169 | 2170 | material = lineMaterial; 2171 | 2172 | } 2173 | 2174 | // Clone the material if it will be modified 2175 | if (useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets) { 2176 | 2177 | var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; 2178 | 2179 | if (material.isGLTFSpecularGlossinessMaterial) cacheKey += 'specular-glossiness:'; 2180 | if (useSkinning) cacheKey += 'skinning:'; 2181 | if (useVertexTangents) cacheKey += 'vertex-tangents:'; 2182 | if (useVertexColors) cacheKey += 'vertex-colors:'; 2183 | if (useFlatShading) cacheKey += 'flat-shading:'; 2184 | if (useMorphTargets) cacheKey += 'morph-targets:'; 2185 | if (useMorphNormals) cacheKey += 'morph-normals:'; 2186 | 2187 | var cachedMaterial = this.cache.get(cacheKey); 2188 | 2189 | if (!cachedMaterial) { 2190 | 2191 | cachedMaterial = material.isGLTFSpecularGlossinessMaterial 2192 | ? extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].cloneMaterial(material) 2193 | : material.clone(); 2194 | 2195 | if (useSkinning) cachedMaterial.skinning = true; 2196 | if (useVertexTangents) cachedMaterial.vertexTangents = true; 2197 | if (useVertexColors) cachedMaterial.vertexColors = THREE.VertexColors; 2198 | if (useFlatShading) cachedMaterial.flatShading = true; 2199 | if (useMorphTargets) cachedMaterial.morphTargets = true; 2200 | if (useMorphNormals) cachedMaterial.morphNormals = true; 2201 | 2202 | this.cache.add(cacheKey, cachedMaterial); 2203 | 2204 | } 2205 | 2206 | material = cachedMaterial; 2207 | 2208 | } 2209 | 2210 | // workarounds for mesh and geometry 2211 | 2212 | if (material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined) { 2213 | 2214 | console.log('THREE.GLTFLoader: Duplicating UVs to support aoMap.'); 2215 | geometry.addAttribute('uv2', new THREE.BufferAttribute(geometry.attributes.uv.array, 2)); 2216 | 2217 | } 2218 | 2219 | if (material.isGLTFSpecularGlossinessMaterial) { 2220 | 2221 | // for GLTFSpecularGlossinessMaterial(ShaderMaterial) uniforms runtime update 2222 | mesh.onBeforeRender = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].refreshUniforms; 2223 | 2224 | } 2225 | 2226 | mesh.material = material; 2227 | 2228 | }; 2229 | 2230 | /** 2231 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials 2232 | * @param {number} materialIndex 2233 | * @return {Promise} 2234 | */ 2235 | GLTFParser.prototype.loadMaterial = function (materialIndex) { 2236 | 2237 | var parser = this; 2238 | var json = this.json; 2239 | var extensions = this.extensions; 2240 | var materialDef = json.materials[materialIndex]; 2241 | 2242 | var materialType; 2243 | var materialParams = {}; 2244 | var materialExtensions = materialDef.extensions || {}; 2245 | 2246 | var pending = []; 2247 | 2248 | if (materialExtensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]) { 2249 | 2250 | var sgExtension = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]; 2251 | materialType = sgExtension.getMaterialType(); 2252 | pending.push(sgExtension.extendParams(materialParams, materialDef, parser)); 2253 | 2254 | } else if (materialExtensions[EXTENSIONS.KHR_MATERIALS_UNLIT]) { 2255 | 2256 | var kmuExtension = extensions[EXTENSIONS.KHR_MATERIALS_UNLIT]; 2257 | materialType = kmuExtension.getMaterialType(); 2258 | pending.push(kmuExtension.extendParams(materialParams, materialDef, parser)); 2259 | 2260 | } else { 2261 | 2262 | // Specification: 2263 | // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material 2264 | 2265 | materialType = THREE.MeshStandardMaterial; 2266 | 2267 | var metallicRoughness = materialDef.pbrMetallicRoughness || {}; 2268 | 2269 | materialParams.color = new THREE.Color(1.0, 1.0, 1.0); 2270 | materialParams.opacity = 1.0; 2271 | 2272 | if (Array.isArray(metallicRoughness.baseColorFactor)) { 2273 | 2274 | var array = metallicRoughness.baseColorFactor; 2275 | 2276 | materialParams.color.fromArray(array); 2277 | materialParams.opacity = array[3]; 2278 | 2279 | } 2280 | 2281 | if (metallicRoughness.baseColorTexture !== undefined) { 2282 | 2283 | pending.push(parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture)); 2284 | 2285 | } 2286 | 2287 | materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; 2288 | materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; 2289 | 2290 | if (metallicRoughness.metallicRoughnessTexture !== undefined) { 2291 | 2292 | pending.push(parser.assignTexture(materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture)); 2293 | pending.push(parser.assignTexture(materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture)); 2294 | 2295 | } 2296 | 2297 | } 2298 | 2299 | if (materialDef.doubleSided === true) { 2300 | 2301 | materialParams.side = THREE.DoubleSide; 2302 | 2303 | } 2304 | 2305 | var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; 2306 | 2307 | if (alphaMode === ALPHA_MODES.BLEND) { 2308 | 2309 | materialParams.transparent = true; 2310 | 2311 | } else { 2312 | 2313 | materialParams.transparent = false; 2314 | 2315 | if (alphaMode === ALPHA_MODES.MASK) { 2316 | 2317 | materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; 2318 | 2319 | } 2320 | 2321 | } 2322 | 2323 | if (materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { 2324 | 2325 | pending.push(parser.assignTexture(materialParams, 'normalMap', materialDef.normalTexture)); 2326 | 2327 | materialParams.normalScale = new THREE.Vector2(1, 1); 2328 | 2329 | if (materialDef.normalTexture.scale !== undefined) { 2330 | 2331 | materialParams.normalScale.set(materialDef.normalTexture.scale, materialDef.normalTexture.scale); 2332 | 2333 | } 2334 | 2335 | } 2336 | 2337 | if (materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { 2338 | 2339 | pending.push(parser.assignTexture(materialParams, 'aoMap', materialDef.occlusionTexture)); 2340 | 2341 | if (materialDef.occlusionTexture.strength !== undefined) { 2342 | 2343 | materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; 2344 | 2345 | } 2346 | 2347 | } 2348 | 2349 | if (materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial) { 2350 | 2351 | materialParams.emissive = new THREE.Color().fromArray(materialDef.emissiveFactor); 2352 | 2353 | } 2354 | 2355 | if (materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial) { 2356 | 2357 | pending.push(parser.assignTexture(materialParams, 'emissiveMap', materialDef.emissiveTexture)); 2358 | 2359 | } 2360 | 2361 | return Promise.all(pending).then(function () { 2362 | 2363 | var material; 2364 | 2365 | if (materialType === THREE.ShaderMaterial) { 2366 | 2367 | material = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].createMaterial(materialParams); 2368 | 2369 | } else { 2370 | 2371 | material = new materialType(materialParams); 2372 | 2373 | } 2374 | 2375 | if (materialDef.name !== undefined) material.name = materialDef.name; 2376 | 2377 | // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. 2378 | if (material.map) material.map.encoding = THREE.sRGBEncoding; 2379 | if (material.emissiveMap) material.emissiveMap.encoding = THREE.sRGBEncoding; 2380 | if (material.specularMap) material.specularMap.encoding = THREE.sRGBEncoding; 2381 | 2382 | assignExtrasToUserData(material, materialDef); 2383 | 2384 | if (materialDef.extensions) addUnknownExtensionsToUserData(extensions, material, materialDef); 2385 | 2386 | return material; 2387 | 2388 | }); 2389 | 2390 | }; 2391 | 2392 | /** 2393 | * @param {THREE.BufferGeometry} geometry 2394 | * @param {GLTF.Primitive} primitiveDef 2395 | * @param {GLTFParser} parser 2396 | * @return {Promise} 2397 | */ 2398 | function addPrimitiveAttributes(geometry, primitiveDef, parser) { 2399 | 2400 | var attributes = primitiveDef.attributes; 2401 | 2402 | var pending = []; 2403 | 2404 | function assignAttributeAccessor(accessorIndex, attributeName) { 2405 | 2406 | return parser.getDependency('accessor', accessorIndex) 2407 | .then(function (accessor) { 2408 | 2409 | geometry.addAttribute(attributeName, accessor); 2410 | 2411 | }); 2412 | 2413 | } 2414 | 2415 | for (var gltfAttributeName in attributes) { 2416 | 2417 | var threeAttributeName = ATTRIBUTES[gltfAttributeName] || gltfAttributeName.toLowerCase(); 2418 | 2419 | // Skip attributes already provided by e.g. Draco extension. 2420 | if (threeAttributeName in geometry.attributes) continue; 2421 | 2422 | pending.push(assignAttributeAccessor(attributes[gltfAttributeName], threeAttributeName)); 2423 | 2424 | } 2425 | 2426 | if (primitiveDef.indices !== undefined && !geometry.index) { 2427 | 2428 | var accessor = parser.getDependency('accessor', primitiveDef.indices).then(function (accessor) { 2429 | 2430 | geometry.setIndex(accessor); 2431 | 2432 | }); 2433 | 2434 | pending.push(accessor); 2435 | 2436 | } 2437 | 2438 | assignExtrasToUserData(geometry, primitiveDef); 2439 | 2440 | return Promise.all(pending).then(function () { 2441 | 2442 | return primitiveDef.targets !== undefined 2443 | ? addMorphTargets(geometry, primitiveDef.targets, parser) 2444 | : geometry; 2445 | 2446 | }); 2447 | 2448 | } 2449 | 2450 | /** 2451 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry 2452 | * 2453 | * Creates BufferGeometries from primitives. 2454 | * 2455 | * @param {Array} primitives 2456 | * @return {Promise>} 2457 | */ 2458 | GLTFParser.prototype.loadGeometries = function (primitives) { 2459 | 2460 | var parser = this; 2461 | var extensions = this.extensions; 2462 | var cache = this.primitiveCache; 2463 | 2464 | function createDracoPrimitive(primitive) { 2465 | 2466 | return extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION] 2467 | .decodePrimitive(primitive, parser) 2468 | .then(function (geometry) { 2469 | 2470 | return addPrimitiveAttributes(geometry, primitive, parser); 2471 | 2472 | }); 2473 | 2474 | } 2475 | 2476 | var pending = []; 2477 | 2478 | for (var i = 0, il = primitives.length; i < il; i++) { 2479 | 2480 | var primitive = primitives[i]; 2481 | var cacheKey = createPrimitiveKey(primitive); 2482 | 2483 | // See if we've already created this geometry 2484 | var cached = cache[cacheKey]; 2485 | 2486 | if (cached) { 2487 | 2488 | // Use the cached geometry if it exists 2489 | pending.push(cached.promise); 2490 | 2491 | } else { 2492 | 2493 | var geometryPromise; 2494 | 2495 | if (primitive.extensions && primitive.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]) { 2496 | 2497 | // Use DRACO geometry if available 2498 | geometryPromise = createDracoPrimitive(primitive); 2499 | 2500 | } else { 2501 | 2502 | // Otherwise create a new geometry 2503 | geometryPromise = addPrimitiveAttributes(new THREE.BufferGeometry(), primitive, parser); 2504 | 2505 | } 2506 | 2507 | // Cache this geometry 2508 | cache[cacheKey] = { primitive: primitive, promise: geometryPromise }; 2509 | 2510 | pending.push(geometryPromise); 2511 | 2512 | } 2513 | 2514 | } 2515 | 2516 | return Promise.all(pending); 2517 | 2518 | }; 2519 | 2520 | /** 2521 | * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes 2522 | * @param {number} meshIndex 2523 | * @return {Promise} 2524 | */ 2525 | GLTFParser.prototype.loadMesh = function (meshIndex) { 2526 | 2527 | var parser = this; 2528 | var json = this.json; 2529 | 2530 | var meshDef = json.meshes[meshIndex]; 2531 | var primitives = meshDef.primitives; 2532 | 2533 | var pending = []; 2534 | 2535 | for (var i = 0, il = primitives.length; i < il; i++) { 2536 | 2537 | var material = primitives[i].material === undefined 2538 | ? createDefaultMaterial() 2539 | : this.getDependency('material', primitives[i].material); 2540 | 2541 | pending.push(material); 2542 | 2543 | } 2544 | 2545 | return Promise.all(pending).then(function (originalMaterials) { 2546 | 2547 | return parser.loadGeometries(primitives).then(function (geometries) { 2548 | 2549 | var meshes = []; 2550 | 2551 | for (var i = 0, il = geometries.length; i < il; i++) { 2552 | 2553 | var geometry = geometries[i]; 2554 | var primitive = primitives[i]; 2555 | 2556 | // 1. create Mesh 2557 | 2558 | var mesh; 2559 | 2560 | var material = originalMaterials[i]; 2561 | 2562 | if (primitive.mode === WEBGL_CONSTANTS.TRIANGLES || 2563 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || 2564 | primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || 2565 | primitive.mode === undefined) { 2566 | 2567 | // .isSkinnedMesh isn't in glTF spec. See .markDefs() 2568 | mesh = meshDef.isSkinnedMesh === true 2569 | ? new THREE.SkinnedMesh(geometry, material) 2570 | : new THREE.Mesh(geometry, material); 2571 | 2572 | if (mesh.isSkinnedMesh === true && !mesh.geometry.attributes.skinWeight.normalized) { 2573 | 2574 | // we normalize floating point skin weight array to fix malformed assets (see #15319) 2575 | // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs 2576 | mesh.normalizeSkinWeights(); 2577 | 2578 | } 2579 | 2580 | if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP) { 2581 | 2582 | mesh.drawMode = THREE.TriangleStripDrawMode; 2583 | 2584 | } else if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN) { 2585 | 2586 | mesh.drawMode = THREE.TriangleFanDrawMode; 2587 | 2588 | } 2589 | 2590 | } else if (primitive.mode === WEBGL_CONSTANTS.LINES) { 2591 | 2592 | mesh = new THREE.LineSegments(geometry, material); 2593 | 2594 | } else if (primitive.mode === WEBGL_CONSTANTS.LINE_STRIP) { 2595 | 2596 | mesh = new THREE.Line(geometry, material); 2597 | 2598 | } else if (primitive.mode === WEBGL_CONSTANTS.LINE_LOOP) { 2599 | 2600 | mesh = new THREE.LineLoop(geometry, material); 2601 | 2602 | } else if (primitive.mode === WEBGL_CONSTANTS.POINTS) { 2603 | 2604 | mesh = new THREE.Points(geometry, material); 2605 | 2606 | } else { 2607 | 2608 | throw new Error('THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode); 2609 | 2610 | } 2611 | 2612 | if (Object.keys(mesh.geometry.morphAttributes).length > 0) { 2613 | 2614 | updateMorphTargets(mesh, meshDef); 2615 | 2616 | } 2617 | 2618 | mesh.name = meshDef.name || ('mesh_' + meshIndex); 2619 | 2620 | if (geometries.length > 1) mesh.name += '_' + i; 2621 | 2622 | assignExtrasToUserData(mesh, meshDef); 2623 | 2624 | parser.assignFinalMaterial(mesh); 2625 | 2626 | meshes.push(mesh); 2627 | 2628 | } 2629 | 2630 | if (meshes.length === 1) { 2631 | 2632 | return meshes[0]; 2633 | 2634 | } 2635 | 2636 | var group = new THREE.Group(); 2637 | 2638 | for (var i = 0, il = meshes.length; i < il; i++) { 2639 | 2640 | group.add(meshes[i]); 2641 | 2642 | } 2643 | 2644 | return group; 2645 | 2646 | }); 2647 | 2648 | }); 2649 | 2650 | }; 2651 | 2652 | /** 2653 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras 2654 | * @param {number} cameraIndex 2655 | * @return {Promise} 2656 | */ 2657 | GLTFParser.prototype.loadCamera = function (cameraIndex) { 2658 | 2659 | var camera; 2660 | var cameraDef = this.json.cameras[cameraIndex]; 2661 | var params = cameraDef[cameraDef.type]; 2662 | 2663 | if (!params) { 2664 | 2665 | console.warn('THREE.GLTFLoader: Missing camera parameters.'); 2666 | return; 2667 | 2668 | } 2669 | 2670 | if (cameraDef.type === 'perspective') { 2671 | 2672 | camera = new THREE.PerspectiveCamera(THREE.Math.radToDeg(params.yfov), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6); 2673 | 2674 | } else if (cameraDef.type === 'orthographic') { 2675 | 2676 | camera = new THREE.OrthographicCamera(params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar); 2677 | 2678 | } 2679 | 2680 | if (cameraDef.name !== undefined) camera.name = cameraDef.name; 2681 | 2682 | assignExtrasToUserData(camera, cameraDef); 2683 | 2684 | return Promise.resolve(camera); 2685 | 2686 | }; 2687 | 2688 | /** 2689 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins 2690 | * @param {number} skinIndex 2691 | * @return {Promise} 2692 | */ 2693 | GLTFParser.prototype.loadSkin = function (skinIndex) { 2694 | 2695 | var skinDef = this.json.skins[skinIndex]; 2696 | 2697 | var skinEntry = { joints: skinDef.joints }; 2698 | 2699 | if (skinDef.inverseBindMatrices === undefined) { 2700 | 2701 | return Promise.resolve(skinEntry); 2702 | 2703 | } 2704 | 2705 | return this.getDependency('accessor', skinDef.inverseBindMatrices).then(function (accessor) { 2706 | 2707 | skinEntry.inverseBindMatrices = accessor; 2708 | 2709 | return skinEntry; 2710 | 2711 | }); 2712 | 2713 | }; 2714 | 2715 | /** 2716 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations 2717 | * @param {number} animationIndex 2718 | * @return {Promise} 2719 | */ 2720 | GLTFParser.prototype.loadAnimation = function (animationIndex) { 2721 | 2722 | var json = this.json; 2723 | 2724 | var animationDef = json.animations[animationIndex]; 2725 | 2726 | var pendingNodes = []; 2727 | var pendingInputAccessors = []; 2728 | var pendingOutputAccessors = []; 2729 | var pendingSamplers = []; 2730 | var pendingTargets = []; 2731 | 2732 | for (var i = 0, il = animationDef.channels.length; i < il; i++) { 2733 | 2734 | var channel = animationDef.channels[i]; 2735 | var sampler = animationDef.samplers[channel.sampler]; 2736 | var target = channel.target; 2737 | var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. 2738 | var input = animationDef.parameters !== undefined ? animationDef.parameters[sampler.input] : sampler.input; 2739 | var output = animationDef.parameters !== undefined ? animationDef.parameters[sampler.output] : sampler.output; 2740 | 2741 | pendingNodes.push(this.getDependency('node', name)); 2742 | pendingInputAccessors.push(this.getDependency('accessor', input)); 2743 | pendingOutputAccessors.push(this.getDependency('accessor', output)); 2744 | pendingSamplers.push(sampler); 2745 | pendingTargets.push(target); 2746 | 2747 | } 2748 | 2749 | return Promise.all([ 2750 | 2751 | Promise.all(pendingNodes), 2752 | Promise.all(pendingInputAccessors), 2753 | Promise.all(pendingOutputAccessors), 2754 | Promise.all(pendingSamplers), 2755 | Promise.all(pendingTargets) 2756 | 2757 | ]).then(function (dependencies) { 2758 | 2759 | var nodes = dependencies[0]; 2760 | var inputAccessors = dependencies[1]; 2761 | var outputAccessors = dependencies[2]; 2762 | var samplers = dependencies[3]; 2763 | var targets = dependencies[4]; 2764 | 2765 | var tracks = []; 2766 | 2767 | for (var i = 0, il = nodes.length; i < il; i++) { 2768 | 2769 | var node = nodes[i]; 2770 | var inputAccessor = inputAccessors[i]; 2771 | var outputAccessor = outputAccessors[i]; 2772 | var sampler = samplers[i]; 2773 | var target = targets[i]; 2774 | 2775 | if (node === undefined) continue; 2776 | 2777 | node.updateMatrix(); 2778 | node.matrixAutoUpdate = true; 2779 | 2780 | var TypedKeyframeTrack; 2781 | 2782 | switch (PATH_PROPERTIES[target.path]) { 2783 | 2784 | case PATH_PROPERTIES.weights: 2785 | 2786 | TypedKeyframeTrack = THREE.NumberKeyframeTrack; 2787 | break; 2788 | 2789 | case PATH_PROPERTIES.rotation: 2790 | 2791 | TypedKeyframeTrack = THREE.QuaternionKeyframeTrack; 2792 | break; 2793 | 2794 | case PATH_PROPERTIES.position: 2795 | case PATH_PROPERTIES.scale: 2796 | default: 2797 | 2798 | TypedKeyframeTrack = THREE.VectorKeyframeTrack; 2799 | break; 2800 | 2801 | } 2802 | 2803 | var targetName = node.name ? node.name : node.uuid; 2804 | 2805 | var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[sampler.interpolation] : THREE.InterpolateLinear; 2806 | 2807 | var targetNames = []; 2808 | 2809 | if (PATH_PROPERTIES[target.path] === PATH_PROPERTIES.weights) { 2810 | 2811 | // Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh. 2812 | node.traverse(function (object) { 2813 | 2814 | if (object.isMesh === true && object.morphTargetInfluences) { 2815 | 2816 | targetNames.push(object.name ? object.name : object.uuid); 2817 | 2818 | } 2819 | 2820 | }); 2821 | 2822 | } else { 2823 | 2824 | targetNames.push(targetName); 2825 | 2826 | } 2827 | 2828 | var outputArray = outputAccessor.array; 2829 | 2830 | if (outputAccessor.normalized) { 2831 | 2832 | var scale; 2833 | 2834 | if (outputArray.constructor === Int8Array) { 2835 | 2836 | scale = 1 / 127; 2837 | 2838 | } else if (outputArray.constructor === Uint8Array) { 2839 | 2840 | scale = 1 / 255; 2841 | 2842 | } else if (outputArray.constructor == Int16Array) { 2843 | 2844 | scale = 1 / 32767; 2845 | 2846 | } else if (outputArray.constructor === Uint16Array) { 2847 | 2848 | scale = 1 / 65535; 2849 | 2850 | } else { 2851 | 2852 | throw new Error('THREE.GLTFLoader: Unsupported output accessor component type.'); 2853 | 2854 | } 2855 | 2856 | var scaled = new Float32Array(outputArray.length); 2857 | 2858 | for (var j = 0, jl = outputArray.length; j < jl; j++) { 2859 | 2860 | scaled[j] = outputArray[j] * scale; 2861 | 2862 | } 2863 | 2864 | outputArray = scaled; 2865 | 2866 | } 2867 | 2868 | for (var j = 0, jl = targetNames.length; j < jl; j++) { 2869 | 2870 | var track = new TypedKeyframeTrack( 2871 | targetNames[j] + '.' + PATH_PROPERTIES[target.path], 2872 | inputAccessor.array, 2873 | outputArray, 2874 | interpolation 2875 | ); 2876 | 2877 | // Override interpolation with custom factory method. 2878 | if (sampler.interpolation === 'CUBICSPLINE') { 2879 | 2880 | track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline(result) { 2881 | 2882 | // A CUBICSPLINE keyframe in glTF has three output values for each input value, 2883 | // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() 2884 | // must be divided by three to get the interpolant's sampleSize argument. 2885 | 2886 | return new GLTFCubicSplineInterpolant(this.times, this.values, this.getValueSize() / 3, result); 2887 | 2888 | }; 2889 | 2890 | // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. 2891 | track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; 2892 | 2893 | } 2894 | 2895 | tracks.push(track); 2896 | 2897 | } 2898 | 2899 | } 2900 | 2901 | var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex; 2902 | 2903 | return new THREE.AnimationClip(name, undefined, tracks); 2904 | 2905 | }); 2906 | 2907 | }; 2908 | 2909 | /** 2910 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy 2911 | * @param {number} nodeIndex 2912 | * @return {Promise} 2913 | */ 2914 | GLTFParser.prototype.loadNode = function (nodeIndex) { 2915 | 2916 | var json = this.json; 2917 | var extensions = this.extensions; 2918 | var parser = this; 2919 | 2920 | var meshReferences = json.meshReferences; 2921 | var meshUses = json.meshUses; 2922 | 2923 | var nodeDef = json.nodes[nodeIndex]; 2924 | 2925 | return (function () { 2926 | 2927 | var pending = []; 2928 | 2929 | if (nodeDef.mesh !== undefined) { 2930 | 2931 | pending.push(parser.getDependency('mesh', nodeDef.mesh).then(function (mesh) { 2932 | 2933 | var node; 2934 | 2935 | if (meshReferences[nodeDef.mesh] > 1) { 2936 | 2937 | var instanceNum = meshUses[nodeDef.mesh]++; 2938 | 2939 | node = mesh.clone(); 2940 | node.name += '_instance_' + instanceNum; 2941 | 2942 | // onBeforeRender copy for Specular-Glossiness 2943 | node.onBeforeRender = mesh.onBeforeRender; 2944 | 2945 | for (var i = 0, il = node.children.length; i < il; i++) { 2946 | 2947 | node.children[i].name += '_instance_' + instanceNum; 2948 | node.children[i].onBeforeRender = mesh.children[i].onBeforeRender; 2949 | 2950 | } 2951 | 2952 | } else { 2953 | 2954 | node = mesh; 2955 | 2956 | } 2957 | 2958 | // if weights are provided on the node, override weights on the mesh. 2959 | if (nodeDef.weights !== undefined) { 2960 | 2961 | node.traverse(function (o) { 2962 | 2963 | if (!o.isMesh) return; 2964 | 2965 | for (var i = 0, il = nodeDef.weights.length; i < il; i++) { 2966 | 2967 | o.morphTargetInfluences[i] = nodeDef.weights[i]; 2968 | 2969 | } 2970 | 2971 | }); 2972 | 2973 | } 2974 | 2975 | return node; 2976 | 2977 | })); 2978 | 2979 | } 2980 | 2981 | if (nodeDef.camera !== undefined) { 2982 | 2983 | pending.push(parser.getDependency('camera', nodeDef.camera)); 2984 | 2985 | } 2986 | 2987 | if (nodeDef.extensions 2988 | && nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL] 2989 | && nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light !== undefined) { 2990 | 2991 | pending.push(parser.getDependency('light', nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light)); 2992 | 2993 | } 2994 | 2995 | return Promise.all(pending); 2996 | 2997 | }()).then(function (objects) { 2998 | 2999 | var node; 3000 | 3001 | // .isBone isn't in glTF spec. See .markDefs 3002 | if (nodeDef.isBone === true) { 3003 | 3004 | node = new THREE.Bone(); 3005 | 3006 | } else if (objects.length > 1) { 3007 | 3008 | node = new THREE.Group(); 3009 | 3010 | } else if (objects.length === 1) { 3011 | 3012 | node = objects[0]; 3013 | 3014 | } else { 3015 | 3016 | node = new THREE.Object3D(); 3017 | 3018 | } 3019 | 3020 | if (node !== objects[0]) { 3021 | 3022 | for (var i = 0, il = objects.length; i < il; i++) { 3023 | 3024 | node.add(objects[i]); 3025 | 3026 | } 3027 | 3028 | } 3029 | 3030 | if (nodeDef.name !== undefined) { 3031 | 3032 | node.userData.name = nodeDef.name; 3033 | node.name = THREE.PropertyBinding.sanitizeNodeName(nodeDef.name); 3034 | 3035 | } 3036 | 3037 | assignExtrasToUserData(node, nodeDef); 3038 | 3039 | if (nodeDef.extensions) addUnknownExtensionsToUserData(extensions, node, nodeDef); 3040 | 3041 | if (nodeDef.matrix !== undefined) { 3042 | 3043 | var matrix = new THREE.Matrix4(); 3044 | matrix.fromArray(nodeDef.matrix); 3045 | node.applyMatrix(matrix); 3046 | 3047 | } else { 3048 | 3049 | if (nodeDef.translation !== undefined) { 3050 | 3051 | node.position.fromArray(nodeDef.translation); 3052 | 3053 | } 3054 | 3055 | if (nodeDef.rotation !== undefined) { 3056 | 3057 | node.quaternion.fromArray(nodeDef.rotation); 3058 | 3059 | } 3060 | 3061 | if (nodeDef.scale !== undefined) { 3062 | 3063 | node.scale.fromArray(nodeDef.scale); 3064 | 3065 | } 3066 | 3067 | } 3068 | 3069 | return node; 3070 | 3071 | }); 3072 | 3073 | }; 3074 | 3075 | /** 3076 | * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes 3077 | * @param {number} sceneIndex 3078 | * @return {Promise} 3079 | */ 3080 | GLTFParser.prototype.loadScene = function () { 3081 | 3082 | // scene node hierachy builder 3083 | 3084 | function buildNodeHierachy(nodeId, parentObject, json, parser) { 3085 | 3086 | var nodeDef = json.nodes[nodeId]; 3087 | 3088 | return parser.getDependency('node', nodeId).then(function (node) { 3089 | 3090 | if (nodeDef.skin === undefined) return node; 3091 | 3092 | // build skeleton here as well 3093 | 3094 | var skinEntry; 3095 | 3096 | return parser.getDependency('skin', nodeDef.skin).then(function (skin) { 3097 | 3098 | skinEntry = skin; 3099 | 3100 | var pendingJoints = []; 3101 | 3102 | for (var i = 0, il = skinEntry.joints.length; i < il; i++) { 3103 | 3104 | pendingJoints.push(parser.getDependency('node', skinEntry.joints[i])); 3105 | 3106 | } 3107 | 3108 | return Promise.all(pendingJoints); 3109 | 3110 | }).then(function (jointNodes) { 3111 | 3112 | node.traverse(function (mesh) { 3113 | 3114 | if (!mesh.isMesh) return; 3115 | 3116 | var bones = []; 3117 | var boneInverses = []; 3118 | 3119 | for (var j = 0, jl = jointNodes.length; j < jl; j++) { 3120 | 3121 | var jointNode = jointNodes[j]; 3122 | 3123 | if (jointNode) { 3124 | 3125 | bones.push(jointNode); 3126 | 3127 | var mat = new THREE.Matrix4(); 3128 | 3129 | if (skinEntry.inverseBindMatrices !== undefined) { 3130 | 3131 | mat.fromArray(skinEntry.inverseBindMatrices.array, j * 16); 3132 | 3133 | } 3134 | 3135 | boneInverses.push(mat); 3136 | 3137 | } else { 3138 | 3139 | console.warn('THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[j]); 3140 | 3141 | } 3142 | 3143 | } 3144 | 3145 | mesh.bind(new THREE.Skeleton(bones, boneInverses), mesh.matrixWorld); 3146 | 3147 | }); 3148 | 3149 | return node; 3150 | 3151 | }); 3152 | 3153 | }).then(function (node) { 3154 | 3155 | // build node hierachy 3156 | 3157 | parentObject.add(node); 3158 | 3159 | var pending = []; 3160 | 3161 | if (nodeDef.children) { 3162 | 3163 | var children = nodeDef.children; 3164 | 3165 | for (var i = 0, il = children.length; i < il; i++) { 3166 | 3167 | var child = children[i]; 3168 | pending.push(buildNodeHierachy(child, node, json, parser)); 3169 | 3170 | } 3171 | 3172 | } 3173 | 3174 | return Promise.all(pending); 3175 | 3176 | }); 3177 | 3178 | } 3179 | 3180 | return function loadScene(sceneIndex) { 3181 | 3182 | var json = this.json; 3183 | var extensions = this.extensions; 3184 | var sceneDef = this.json.scenes[sceneIndex]; 3185 | var parser = this; 3186 | 3187 | var scene = new THREE.Scene(); 3188 | if (sceneDef.name !== undefined) scene.name = sceneDef.name; 3189 | 3190 | assignExtrasToUserData(scene, sceneDef); 3191 | 3192 | if (sceneDef.extensions) addUnknownExtensionsToUserData(extensions, scene, sceneDef); 3193 | 3194 | var nodeIds = sceneDef.nodes || []; 3195 | 3196 | var pending = []; 3197 | 3198 | for (var i = 0, il = nodeIds.length; i < il; i++) { 3199 | 3200 | pending.push(buildNodeHierachy(nodeIds[i], scene, json, parser)); 3201 | 3202 | } 3203 | 3204 | return Promise.all(pending).then(function () { 3205 | 3206 | return scene; 3207 | 3208 | }); 3209 | 3210 | }; 3211 | 3212 | }(); 3213 | 3214 | return GLTFLoader; 3215 | 3216 | })(); 3217 | } -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minicode-26", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "threejs-miniprogram": "0.0.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Project configuration file", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true, 12 | "coverView": true, 13 | "autoAudits": false, 14 | "checkInvalidKey": true, 15 | "checkSiteMap": true, 16 | "uploadWithSourceMap": true, 17 | "babelSetting": { 18 | "ignore": [], 19 | "disablePlugins": [], 20 | "outputPath": "" 21 | } 22 | }, 23 | "compileType": "miniprogram", 24 | "libVersion": "2.7.7", 25 | "appid": "wxda2a1cb48a786901", 26 | "projectname": "three.js-demo", 27 | "debugOptions": { 28 | "hidedInDevtools": [] 29 | }, 30 | "isGameTourist": false, 31 | "simulatorType": "wechat", 32 | "simulatorPluginLibVersion": {}, 33 | "condition": { 34 | "search": { 35 | "current": -1, 36 | "list": [] 37 | }, 38 | "conversation": { 39 | "current": -1, 40 | "list": [] 41 | }, 42 | "game": { 43 | "currentL": -1, 44 | "list": [] 45 | }, 46 | "miniprogram": { 47 | "current": -1, 48 | "list": [] 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /example/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /example/test-cases/cube.js: -------------------------------------------------------------------------------- 1 | export function renderCube(canvas, THREE) { 2 | var camera, scene, renderer; 3 | var mesh; 4 | init(); 5 | animate(); 6 | function init() { 7 | camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000); 8 | camera.position.z = 400; 9 | scene = new THREE.Scene(); 10 | var texture = new THREE.TextureLoader().load('/assets/crate.png'); 11 | var geometry = new THREE.BoxBufferGeometry(200, 200, 200); 12 | var material = new THREE.MeshBasicMaterial({ map: texture }); 13 | mesh = new THREE.Mesh(geometry, material); 14 | scene.add(mesh); 15 | renderer = new THREE.WebGLRenderer({ antialias: true }); 16 | renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); 17 | renderer.setSize(canvas.width, canvas.height); 18 | } 19 | function onWindowResize() { 20 | camera.aspect = window.innerWidth / window.innerHeight; 21 | camera.updateProjectionMatrix(); 22 | renderer.setSize(canvas.width, canvas.height); 23 | } 24 | function animate() { 25 | canvas.requestAnimationFrame(animate); 26 | mesh.rotation.x += 0.005; 27 | mesh.rotation.y += 0.01; 28 | renderer.render(scene, camera); 29 | } 30 | } -------------------------------------------------------------------------------- /example/test-cases/cubes.js: -------------------------------------------------------------------------------- 1 | export function renderCubes(canvas, THREE) { 2 | var container, stats; 3 | var camera, scene, raycaster, renderer; 4 | var mouse = new THREE.Vector2(), INTERSECTED; 5 | var radius = 100, theta = 0; 6 | init(); 7 | animate(); 8 | function init() { 9 | camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 10000); 10 | scene = new THREE.Scene(); 11 | scene.background = new THREE.Color(0xf0f0f0); 12 | var light = new THREE.DirectionalLight(0xffffff, 1); 13 | light.position.set(1, 1, 1).normalize(); 14 | scene.add(light); 15 | var geometry = new THREE.BoxBufferGeometry(20, 20, 20); 16 | for (var i = 0; i < 2000; i++) { 17 | var object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); 18 | object.position.x = Math.random() * 800 - 400; 19 | object.position.y = Math.random() * 800 - 400; 20 | object.position.z = Math.random() * 800 - 400; 21 | object.rotation.x = Math.random() * 2 * Math.PI; 22 | object.rotation.y = Math.random() * 2 * Math.PI; 23 | object.rotation.z = Math.random() * 2 * Math.PI; 24 | object.scale.x = Math.random() + 0.5; 25 | object.scale.y = Math.random() + 0.5; 26 | object.scale.z = Math.random() + 0.5; 27 | scene.add(object); 28 | } 29 | raycaster = new THREE.Raycaster(); 30 | renderer = new THREE.WebGLRenderer(); 31 | renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); 32 | renderer.setSize(canvas.width, canvas.height); 33 | } 34 | function animate() { 35 | canvas.requestAnimationFrame(animate); 36 | render(); 37 | } 38 | function render() { 39 | theta += 0.5; 40 | camera.position.x = radius * Math.sin(THREE.Math.degToRad(theta)); 41 | camera.position.y = radius * Math.sin(THREE.Math.degToRad(theta)); 42 | camera.position.z = radius * Math.cos(THREE.Math.degToRad(theta)); 43 | camera.lookAt(scene.position); 44 | camera.updateMatrixWorld(); 45 | // find intersections 46 | raycaster.setFromCamera(mouse, camera); 47 | var intersects = raycaster.intersectObjects(scene.children); 48 | if (intersects.length > 0) { 49 | if (INTERSECTED != intersects[0].object) { 50 | if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex); 51 | INTERSECTED = intersects[0].object; 52 | INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex(); 53 | INTERSECTED.material.emissive.setHex(0xff0000); 54 | } 55 | } else { 56 | if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex); 57 | INTERSECTED = null; 58 | } 59 | renderer.render(scene, camera); 60 | } 61 | } -------------------------------------------------------------------------------- /example/test-cases/model.js: -------------------------------------------------------------------------------- 1 | import { registerGLTFLoader } from '../loaders/gltf-loader' 2 | import registerOrbit from "./orbit" 3 | 4 | export function renderModel(canvas, THREE) { 5 | registerGLTFLoader(THREE) 6 | 7 | var container, stats, clock, gui, mixer, actions, activeAction, previousAction; 8 | var camera, scene, renderer, model, face, controls; 9 | var api = { state: 'Walking' }; 10 | init(); 11 | animate(); 12 | function init() { 13 | camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.25, 100); 14 | camera.position.set(- 5, 3, 10); 15 | camera.lookAt(new THREE.Vector3(0, 2, 0)); 16 | scene = new THREE.Scene(); 17 | scene.background = new THREE.Color(0xe0e0e0); 18 | scene.fog = new THREE.Fog(0xe0e0e0, 20, 100); 19 | clock = new THREE.Clock(); 20 | // lights 21 | var light = new THREE.HemisphereLight(0xffffff, 0x444444); 22 | light.position.set(0, 20, 0); 23 | scene.add(light); 24 | light = new THREE.DirectionalLight(0xffffff); 25 | light.position.set(0, 20, 10); 26 | scene.add(light); 27 | // ground 28 | var mesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(2000, 2000), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false })); 29 | mesh.rotation.x = - Math.PI / 2; 30 | scene.add(mesh); 31 | var grid = new THREE.GridHelper(200, 40, 0x000000, 0x000000); 32 | grid.material.opacity = 0.2; 33 | grid.material.transparent = true; 34 | scene.add(grid); 35 | // model 36 | var loader = new THREE.GLTFLoader(); 37 | loader.load('https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb', function (gltf) { 38 | model = gltf.scene; 39 | scene.add(model); 40 | createGUI(model, gltf.animations) 41 | }, undefined, function (e) { 42 | console.error(e); 43 | }); 44 | renderer = new THREE.WebGLRenderer({ antialias: true }); 45 | renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); 46 | renderer.setSize(canvas.width, canvas.height); 47 | renderer.gammaOutput = true; 48 | renderer.gammaFactor = 2.2; 49 | 50 | const { OrbitControls } = registerOrbit(THREE) 51 | controls = new OrbitControls( camera, renderer.domElement ); 52 | 53 | camera.position.set( 5, 5, 10 ); 54 | controls.update(); 55 | } 56 | 57 | function createGUI(model, animations) { 58 | var states = ['Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing']; 59 | var emotes = ['Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp']; 60 | mixer = new THREE.AnimationMixer(model); 61 | actions = {}; 62 | for (var i = 0; i < animations.length; i++) { 63 | var clip = animations[i]; 64 | var action = mixer.clipAction(clip); 65 | actions[clip.name] = action; 66 | if (emotes.indexOf(clip.name) >= 0 || states.indexOf(clip.name) >= 4) { 67 | action.clampWhenFinished = true; 68 | action.loop = THREE.LoopOnce; 69 | } 70 | } 71 | 72 | // expressions 73 | face = model.getObjectByName('Head_2'); 74 | activeAction = actions['Walking']; 75 | activeAction.play(); 76 | } 77 | 78 | function fadeToAction(name, duration) { 79 | previousAction = activeAction; 80 | activeAction = actions[name]; 81 | if (previousAction !== activeAction) { 82 | previousAction.fadeOut(duration); 83 | } 84 | activeAction 85 | .reset() 86 | .setEffectiveTimeScale(1) 87 | .setEffectiveWeight(1) 88 | .fadeIn(duration) 89 | .play(); 90 | } 91 | function animate() { 92 | var dt = clock.getDelta(); 93 | if (mixer) mixer.update(dt); 94 | canvas.requestAnimationFrame(animate); 95 | controls.update() 96 | renderer.render(scene, camera); 97 | } 98 | } -------------------------------------------------------------------------------- /example/test-cases/orbit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | * @author ScieCode / http://github.com/sciecode 8 | */ 9 | 10 | const registerOrbit = (THREE) => { 11 | const { 12 | EventDispatcher, 13 | MOUSE, 14 | Quaternion, 15 | Spherical, 16 | TOUCH, 17 | Vector2, 18 | Vector3 19 | } = THREE 20 | 21 | // This set of controls performs orbiting, dollying (zooming), and panning. 22 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 23 | // 24 | // Orbit - left mouse / touch: one-finger move 25 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 26 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 27 | 28 | var OrbitControls = function (object, domElement) { 29 | 30 | if (domElement === undefined) console.warn('THREE.OrbitControls: The second parameter "domElement" is now mandatory.'); 31 | if (domElement === document) console.error('THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'); 32 | 33 | this.object = object; 34 | this.domElement = domElement; 35 | 36 | // Set to false to disable this control 37 | this.enabled = true; 38 | 39 | // "target" sets the location of focus, where the object orbits around 40 | this.target = new Vector3(); 41 | 42 | // How far you can dolly in and out ( PerspectiveCamera only ) 43 | this.minDistance = 0; 44 | this.maxDistance = Infinity; 45 | 46 | // How far you can zoom in and out ( OrthographicCamera only ) 47 | this.minZoom = 0; 48 | this.maxZoom = Infinity; 49 | 50 | // How far you can orbit vertically, upper and lower limits. 51 | // Range is 0 to Math.PI radians. 52 | this.minPolarAngle = 0; // radians 53 | this.maxPolarAngle = Math.PI; // radians 54 | 55 | // How far you can orbit horizontally, upper and lower limits. 56 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 57 | this.minAzimuthAngle = -Infinity; // radians 58 | this.maxAzimuthAngle = Infinity; // radians 59 | 60 | // Set to true to enable damping (inertia) 61 | // If damping is enabled, you must call controls.update() in your animation loop 62 | this.enableDamping = false; 63 | this.dampingFactor = 0.05; 64 | 65 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 66 | // Set to false to disable zooming 67 | this.enableZoom = true; 68 | this.zoomSpeed = 1.0; 69 | 70 | // Set to false to disable rotating 71 | this.enableRotate = true; 72 | this.rotateSpeed = 1.0; 73 | 74 | // Set to false to disable panning 75 | this.enablePan = true; 76 | this.panSpeed = 1.0; 77 | this.screenSpacePanning = false; // if true, pan in screen-space 78 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 79 | 80 | // Set to true to automatically rotate around the target 81 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 82 | this.autoRotate = false; 83 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 84 | 85 | // Set to false to disable use of the keys 86 | this.enableKeys = true; 87 | 88 | // The four arrow keys 89 | this.keys = { 90 | LEFT: 37, 91 | UP: 38, 92 | RIGHT: 39, 93 | BOTTOM: 40 94 | }; 95 | 96 | // Mouse buttons 97 | this.mouseButtons = { 98 | LEFT: MOUSE.ROTATE, 99 | MIDDLE: MOUSE.DOLLY, 100 | RIGHT: MOUSE.PAN 101 | }; 102 | 103 | // Touch fingers 104 | this.touches = { 105 | ONE: TOUCH.ROTATE, 106 | TWO: TOUCH.DOLLY_PAN 107 | }; 108 | 109 | // for reset 110 | this.target0 = this.target.clone(); 111 | this.position0 = this.object.position.clone(); 112 | this.zoom0 = this.object.zoom; 113 | 114 | // 115 | // public methods 116 | // 117 | 118 | this.getPolarAngle = function () { 119 | 120 | return spherical.phi; 121 | 122 | }; 123 | 124 | this.getAzimuthalAngle = function () { 125 | 126 | return spherical.theta; 127 | 128 | }; 129 | 130 | this.saveState = function () { 131 | 132 | scope.target0.copy(scope.target); 133 | scope.position0.copy(scope.object.position); 134 | scope.zoom0 = scope.object.zoom; 135 | 136 | }; 137 | 138 | this.reset = function () { 139 | 140 | scope.target.copy(scope.target0); 141 | scope.object.position.copy(scope.position0); 142 | scope.object.zoom = scope.zoom0; 143 | 144 | scope.object.updateProjectionMatrix(); 145 | scope.dispatchEvent(changeEvent); 146 | 147 | scope.update(); 148 | 149 | state = STATE.NONE; 150 | 151 | }; 152 | 153 | // this method is exposed, but perhaps it would be better if we can make it private... 154 | this.update = function () { 155 | 156 | var offset = new Vector3(); 157 | 158 | // so camera.up is the orbit axis 159 | var quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)); 160 | var quatInverse = quat.clone().inverse(); 161 | 162 | var lastPosition = new Vector3(); 163 | var lastQuaternion = new Quaternion(); 164 | 165 | return function update() { 166 | 167 | var position = scope.object.position; 168 | 169 | offset.copy(position).sub(scope.target); 170 | 171 | // rotate offset to "y-axis-is-up" space 172 | offset.applyQuaternion(quat); 173 | 174 | // angle from z-axis around y-axis 175 | spherical.setFromVector3(offset); 176 | 177 | if (scope.autoRotate && state === STATE.NONE) { 178 | 179 | rotateLeft(getAutoRotationAngle()); 180 | 181 | } 182 | 183 | if (scope.enableDamping) { 184 | 185 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 186 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 187 | 188 | } else { 189 | 190 | spherical.theta += sphericalDelta.theta; 191 | spherical.phi += sphericalDelta.phi; 192 | 193 | } 194 | 195 | // restrict theta to be between desired limits 196 | spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)); 197 | 198 | // restrict phi to be between desired limits 199 | spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); 200 | 201 | spherical.makeSafe(); 202 | 203 | 204 | spherical.radius *= scale; 205 | 206 | // restrict radius to be between desired limits 207 | spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)); 208 | 209 | // move target to panned location 210 | 211 | if (scope.enableDamping === true) { 212 | 213 | scope.target.addScaledVector(panOffset, scope.dampingFactor); 214 | 215 | } else { 216 | 217 | scope.target.add(panOffset); 218 | 219 | } 220 | 221 | offset.setFromSpherical(spherical); 222 | 223 | // rotate offset back to "camera-up-vector-is-up" space 224 | offset.applyQuaternion(quatInverse); 225 | 226 | position.copy(scope.target).add(offset); 227 | 228 | scope.object.lookAt(scope.target); 229 | 230 | if (scope.enableDamping === true) { 231 | 232 | sphericalDelta.theta *= (1 - scope.dampingFactor); 233 | sphericalDelta.phi *= (1 - scope.dampingFactor); 234 | 235 | panOffset.multiplyScalar(1 - scope.dampingFactor); 236 | 237 | } else { 238 | 239 | sphericalDelta.set(0, 0, 0); 240 | 241 | panOffset.set(0, 0, 0); 242 | 243 | } 244 | 245 | scale = 1; 246 | 247 | // update condition is: 248 | // min(camera displacement, camera rotation in radians)^2 > EPS 249 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 250 | 251 | if (zoomChanged || 252 | lastPosition.distanceToSquared(scope.object.position) > EPS || 253 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { 254 | 255 | scope.dispatchEvent(changeEvent); 256 | 257 | lastPosition.copy(scope.object.position); 258 | lastQuaternion.copy(scope.object.quaternion); 259 | zoomChanged = false; 260 | 261 | return true; 262 | 263 | } 264 | 265 | return false; 266 | 267 | }; 268 | 269 | }(); 270 | 271 | this.dispose = function () { 272 | 273 | scope.domElement.removeEventListener('contextmenu', onContextMenu, false); 274 | scope.domElement.removeEventListener('mousedown', onMouseDown, false); 275 | scope.domElement.removeEventListener('wheel', onMouseWheel, false); 276 | 277 | scope.domElement.removeEventListener('touchstart', onTouchStart, false); 278 | scope.domElement.removeEventListener('touchend', onTouchEnd, false); 279 | scope.domElement.removeEventListener('touchmove', onTouchMove, false); 280 | 281 | document.removeEventListener('mousemove', onMouseMove, false); 282 | document.removeEventListener('mouseup', onMouseUp, false); 283 | 284 | scope.domElement.removeEventListener('keydown', onKeyDown, false); 285 | 286 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 287 | 288 | }; 289 | 290 | // 291 | // internals 292 | // 293 | 294 | var scope = this; 295 | 296 | var changeEvent = { 297 | type: 'change' 298 | }; 299 | var startEvent = { 300 | type: 'start' 301 | }; 302 | var endEvent = { 303 | type: 'end' 304 | }; 305 | 306 | var STATE = { 307 | NONE: -1, 308 | ROTATE: 0, 309 | DOLLY: 1, 310 | PAN: 2, 311 | TOUCH_ROTATE: 3, 312 | TOUCH_PAN: 4, 313 | TOUCH_DOLLY_PAN: 5, 314 | TOUCH_DOLLY_ROTATE: 6 315 | }; 316 | 317 | var state = STATE.NONE; 318 | 319 | var EPS = 0.000001; 320 | 321 | // current position in spherical coordinates 322 | var spherical = new Spherical(); 323 | var sphericalDelta = new Spherical(); 324 | 325 | var scale = 1; 326 | var panOffset = new Vector3(); 327 | var zoomChanged = false; 328 | 329 | var rotateStart = new Vector2(); 330 | var rotateEnd = new Vector2(); 331 | var rotateDelta = new Vector2(); 332 | 333 | var panStart = new Vector2(); 334 | var panEnd = new Vector2(); 335 | var panDelta = new Vector2(); 336 | 337 | var dollyStart = new Vector2(); 338 | var dollyEnd = new Vector2(); 339 | var dollyDelta = new Vector2(); 340 | 341 | function getAutoRotationAngle() { 342 | 343 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 344 | 345 | } 346 | 347 | function getZoomScale() { 348 | 349 | return Math.pow(0.95, scope.zoomSpeed); 350 | 351 | } 352 | 353 | function rotateLeft(angle) { 354 | 355 | sphericalDelta.theta -= angle; 356 | 357 | } 358 | 359 | function rotateUp(angle) { 360 | 361 | sphericalDelta.phi -= angle; 362 | 363 | } 364 | 365 | var panLeft = function () { 366 | 367 | var v = new Vector3(); 368 | 369 | return function panLeft(distance, objectMatrix) { 370 | 371 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix 372 | v.multiplyScalar(-distance); 373 | 374 | panOffset.add(v); 375 | 376 | }; 377 | 378 | }(); 379 | 380 | var panUp = function () { 381 | 382 | var v = new Vector3(); 383 | 384 | return function panUp(distance, objectMatrix) { 385 | 386 | if (scope.screenSpacePanning === true) { 387 | 388 | v.setFromMatrixColumn(objectMatrix, 1); 389 | 390 | } else { 391 | 392 | v.setFromMatrixColumn(objectMatrix, 0); 393 | v.crossVectors(scope.object.up, v); 394 | 395 | } 396 | 397 | v.multiplyScalar(distance); 398 | 399 | panOffset.add(v); 400 | 401 | }; 402 | 403 | }(); 404 | 405 | // deltaX and deltaY are in pixels; right and down are positive 406 | var pan = function () { 407 | 408 | var offset = new Vector3(); 409 | 410 | return function pan(deltaX, deltaY) { 411 | 412 | var element = scope.domElement; 413 | 414 | if (scope.object.isPerspectiveCamera) { 415 | 416 | // perspective 417 | var position = scope.object.position; 418 | offset.copy(position).sub(scope.target); 419 | var targetDistance = offset.length(); 420 | 421 | // half of the fov is center to top of screen 422 | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0); 423 | 424 | // we use only clientHeight here so aspect ratio does not distort speed 425 | panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); 426 | panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); 427 | 428 | } else if (scope.object.isOrthographicCamera) { 429 | 430 | // orthographic 431 | panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); 432 | panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); 433 | 434 | } else { 435 | 436 | // camera neither orthographic nor perspective 437 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.'); 438 | scope.enablePan = false; 439 | 440 | } 441 | 442 | }; 443 | 444 | }(); 445 | 446 | function dollyOut(dollyScale) { 447 | if (scope.object.isPerspectiveCamera) { 448 | 449 | scale /= dollyScale; 450 | 451 | } else if (scope.object.isOrthographicCamera) { 452 | 453 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)); 454 | scope.object.updateProjectionMatrix(); 455 | zoomChanged = true; 456 | 457 | } else { 458 | 459 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 460 | scope.enableZoom = false; 461 | 462 | } 463 | 464 | } 465 | 466 | function dollyIn(dollyScale) { 467 | 468 | if (scope.object.isPerspectiveCamera) { 469 | 470 | scale *= dollyScale; 471 | 472 | } else if (scope.object.isOrthographicCamera) { 473 | 474 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)); 475 | scope.object.updateProjectionMatrix(); 476 | zoomChanged = true; 477 | 478 | } else { 479 | 480 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.'); 481 | scope.enableZoom = false; 482 | 483 | } 484 | 485 | } 486 | 487 | // 488 | // event callbacks - update the object state 489 | // 490 | 491 | function handleMouseDownRotate(event) { 492 | 493 | rotateStart.set(event.clientX, event.clientY); 494 | 495 | } 496 | 497 | function handleMouseDownDolly(event) { 498 | 499 | dollyStart.set(event.clientX, event.clientY); 500 | 501 | } 502 | 503 | function handleMouseDownPan(event) { 504 | 505 | panStart.set(event.clientX, event.clientY); 506 | 507 | } 508 | 509 | function handleMouseMoveRotate(event) { 510 | 511 | rotateEnd.set(event.clientX, event.clientY); 512 | 513 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 514 | 515 | var element = scope.domElement; 516 | 517 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height 518 | 519 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); 520 | 521 | rotateStart.copy(rotateEnd); 522 | 523 | scope.update(); 524 | 525 | } 526 | 527 | function handleMouseMoveDolly(event) { 528 | 529 | dollyEnd.set(event.clientX, event.clientY); 530 | 531 | dollyDelta.subVectors(dollyEnd, dollyStart); 532 | 533 | if (dollyDelta.y > 0) { 534 | 535 | dollyOut(getZoomScale()); 536 | 537 | } else if (dollyDelta.y < 0) { 538 | 539 | dollyIn(getZoomScale()); 540 | 541 | } 542 | 543 | dollyStart.copy(dollyEnd); 544 | 545 | scope.update(); 546 | 547 | } 548 | 549 | function handleMouseMovePan(event) { 550 | 551 | panEnd.set(event.clientX, event.clientY); 552 | 553 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 554 | 555 | pan(panDelta.x, panDelta.y); 556 | 557 | panStart.copy(panEnd); 558 | 559 | scope.update(); 560 | 561 | } 562 | 563 | function handleMouseUp( /*event*/ ) { 564 | 565 | // no-op 566 | 567 | } 568 | 569 | function handleMouseWheel(event) { 570 | 571 | if (event.deltaY < 0) { 572 | 573 | dollyIn(getZoomScale()); 574 | 575 | } else if (event.deltaY > 0) { 576 | 577 | dollyOut(getZoomScale()); 578 | 579 | } 580 | 581 | scope.update(); 582 | 583 | } 584 | 585 | function handleKeyDown(event) { 586 | 587 | var needsUpdate = false; 588 | 589 | switch (event.keyCode) { 590 | 591 | case scope.keys.UP: 592 | pan(0, scope.keyPanSpeed); 593 | needsUpdate = true; 594 | break; 595 | 596 | case scope.keys.BOTTOM: 597 | pan(0, -scope.keyPanSpeed); 598 | needsUpdate = true; 599 | break; 600 | 601 | case scope.keys.LEFT: 602 | pan(scope.keyPanSpeed, 0); 603 | needsUpdate = true; 604 | break; 605 | 606 | case scope.keys.RIGHT: 607 | pan(-scope.keyPanSpeed, 0); 608 | needsUpdate = true; 609 | break; 610 | 611 | } 612 | 613 | if (needsUpdate) { 614 | 615 | // prevent the browser from scrolling on cursor keys 616 | event.preventDefault(); 617 | 618 | scope.update(); 619 | 620 | } 621 | 622 | 623 | } 624 | 625 | function handleTouchStartRotate(event) { 626 | 627 | if (event.touches.length == 1) { 628 | 629 | rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); 630 | 631 | } else { 632 | 633 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 634 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 635 | 636 | rotateStart.set(x, y); 637 | 638 | } 639 | 640 | } 641 | 642 | function handleTouchStartPan(event) { 643 | 644 | if (event.touches.length == 1) { 645 | 646 | panStart.set(event.touches[0].pageX, event.touches[0].pageY); 647 | 648 | } else { 649 | 650 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 651 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 652 | 653 | panStart.set(x, y); 654 | 655 | } 656 | 657 | } 658 | 659 | function handleTouchStartDolly(event) { 660 | 661 | var dx = event.touches[0].pageX - event.touches[1].pageX; 662 | var dy = event.touches[0].pageY - event.touches[1].pageY; 663 | 664 | var distance = Math.sqrt(dx * dx + dy * dy); 665 | 666 | dollyStart.set(0, distance); 667 | 668 | } 669 | 670 | function handleTouchStartDollyPan(event) { 671 | 672 | if (scope.enableZoom) handleTouchStartDolly(event); 673 | 674 | if (scope.enablePan) handleTouchStartPan(event); 675 | 676 | } 677 | 678 | function handleTouchStartDollyRotate(event) { 679 | 680 | if (scope.enableZoom) handleTouchStartDolly(event); 681 | 682 | if (scope.enableRotate) handleTouchStartRotate(event); 683 | 684 | } 685 | 686 | function handleTouchMoveRotate(event) { 687 | if (event.touches.length == 1) { 688 | 689 | rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); 690 | 691 | } else { 692 | 693 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 694 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 695 | 696 | rotateEnd.set(x, y); 697 | 698 | } 699 | 700 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); 701 | 702 | var element = scope.domElement; 703 | 704 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height 705 | 706 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); 707 | 708 | rotateStart.copy(rotateEnd); 709 | 710 | } 711 | 712 | function handleTouchMovePan(event) { 713 | 714 | if (event.touches.length == 1) { 715 | 716 | panEnd.set(event.touches[0].pageX, event.touches[0].pageY); 717 | 718 | } else { 719 | 720 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX); 721 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY); 722 | 723 | panEnd.set(x, y); 724 | 725 | } 726 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); 727 | 728 | pan(panDelta.x, panDelta.y); 729 | 730 | panStart.copy(panEnd); 731 | 732 | } 733 | 734 | function handleTouchMoveDolly(event) { 735 | 736 | var dx = event.touches[0].pageX - event.touches[1].pageX; 737 | var dy = event.touches[0].pageY - event.touches[1].pageY; 738 | 739 | var distance = Math.sqrt(dx * dx + dy * dy); 740 | 741 | dollyEnd.set(0, distance); 742 | 743 | dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); 744 | 745 | dollyOut(dollyDelta.y); 746 | 747 | dollyStart.copy(dollyEnd); 748 | 749 | } 750 | 751 | function handleTouchMoveDollyPan(event) { 752 | 753 | if (scope.enableZoom) handleTouchMoveDolly(event); 754 | 755 | if (scope.enablePan) handleTouchMovePan(event); 756 | 757 | } 758 | 759 | function handleTouchMoveDollyRotate(event) { 760 | 761 | if (scope.enableZoom) handleTouchMoveDolly(event); 762 | 763 | if (scope.enableRotate) handleTouchMoveRotate(event); 764 | 765 | } 766 | 767 | function handleTouchEnd( /*event*/ ) { 768 | 769 | // no-op 770 | 771 | } 772 | 773 | // 774 | // event handlers - FSM: listen for events and reset state 775 | // 776 | 777 | function onMouseDown(event) { 778 | 779 | if (scope.enabled === false) return; 780 | 781 | // Prevent the browser from scrolling. 782 | event.preventDefault(); 783 | 784 | // Manually set the focus since calling preventDefault above 785 | // prevents the browser from setting it automatically. 786 | 787 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 788 | 789 | var mouseAction; 790 | 791 | switch (event.button) { 792 | 793 | case 0: 794 | 795 | mouseAction = scope.mouseButtons.LEFT; 796 | break; 797 | 798 | case 1: 799 | 800 | mouseAction = scope.mouseButtons.MIDDLE; 801 | break; 802 | 803 | case 2: 804 | 805 | mouseAction = scope.mouseButtons.RIGHT; 806 | break; 807 | 808 | default: 809 | 810 | mouseAction = -1; 811 | 812 | } 813 | 814 | switch (mouseAction) { 815 | 816 | case MOUSE.DOLLY: 817 | 818 | if (scope.enableZoom === false) return; 819 | 820 | handleMouseDownDolly(event); 821 | 822 | state = STATE.DOLLY; 823 | 824 | break; 825 | 826 | case MOUSE.ROTATE: 827 | 828 | if (event.ctrlKey || event.metaKey || event.shiftKey) { 829 | 830 | if (scope.enablePan === false) return; 831 | 832 | handleMouseDownPan(event); 833 | 834 | state = STATE.PAN; 835 | 836 | } else { 837 | 838 | if (scope.enableRotate === false) return; 839 | 840 | handleMouseDownRotate(event); 841 | 842 | state = STATE.ROTATE; 843 | 844 | } 845 | 846 | break; 847 | 848 | case MOUSE.PAN: 849 | 850 | if (event.ctrlKey || event.metaKey || event.shiftKey) { 851 | 852 | if (scope.enableRotate === false) return; 853 | 854 | handleMouseDownRotate(event); 855 | 856 | state = STATE.ROTATE; 857 | 858 | } else { 859 | 860 | if (scope.enablePan === false) return; 861 | 862 | handleMouseDownPan(event); 863 | 864 | state = STATE.PAN; 865 | 866 | } 867 | 868 | break; 869 | 870 | default: 871 | 872 | state = STATE.NONE; 873 | 874 | } 875 | 876 | if (state !== STATE.NONE) { 877 | 878 | document.addEventListener('mousemove', onMouseMove, false); 879 | document.addEventListener('mouseup', onMouseUp, false); 880 | 881 | scope.dispatchEvent(startEvent); 882 | 883 | } 884 | 885 | } 886 | 887 | function onMouseMove(event) { 888 | 889 | if (scope.enabled === false) return; 890 | 891 | event.preventDefault(); 892 | 893 | switch (state) { 894 | 895 | case STATE.ROTATE: 896 | 897 | if (scope.enableRotate === false) return; 898 | 899 | handleMouseMoveRotate(event); 900 | 901 | break; 902 | 903 | case STATE.DOLLY: 904 | 905 | if (scope.enableZoom === false) return; 906 | 907 | handleMouseMoveDolly(event); 908 | 909 | break; 910 | 911 | case STATE.PAN: 912 | 913 | if (scope.enablePan === false) return; 914 | 915 | handleMouseMovePan(event); 916 | 917 | break; 918 | 919 | } 920 | 921 | } 922 | 923 | function onMouseUp(event) { 924 | 925 | if (scope.enabled === false) return; 926 | 927 | handleMouseUp(event); 928 | 929 | document.removeEventListener('mousemove', onMouseMove, false); 930 | document.removeEventListener('mouseup', onMouseUp, false); 931 | 932 | scope.dispatchEvent(endEvent); 933 | 934 | state = STATE.NONE; 935 | 936 | } 937 | 938 | function onMouseWheel(event) { 939 | 940 | if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) return; 941 | 942 | event.preventDefault(); 943 | event.stopPropagation(); 944 | 945 | scope.dispatchEvent(startEvent); 946 | 947 | handleMouseWheel(event); 948 | 949 | scope.dispatchEvent(endEvent); 950 | 951 | } 952 | 953 | function onKeyDown(event) { 954 | 955 | if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return; 956 | 957 | handleKeyDown(event); 958 | 959 | } 960 | 961 | function onTouchStart(event) { 962 | 963 | if (scope.enabled === false) return; 964 | 965 | event.preventDefault(); // prevent scrolling 966 | 967 | switch (event.touches.length) { 968 | 969 | case 1: 970 | 971 | switch (scope.touches.ONE) { 972 | 973 | case TOUCH.ROTATE: 974 | 975 | if (scope.enableRotate === false) return; 976 | 977 | handleTouchStartRotate(event); 978 | 979 | state = STATE.TOUCH_ROTATE; 980 | 981 | break; 982 | 983 | case TOUCH.PAN: 984 | 985 | if (scope.enablePan === false) return; 986 | 987 | handleTouchStartPan(event); 988 | 989 | state = STATE.TOUCH_PAN; 990 | 991 | break; 992 | 993 | default: 994 | 995 | state = STATE.NONE; 996 | 997 | } 998 | 999 | break; 1000 | 1001 | case 2: 1002 | 1003 | switch (scope.touches.TWO) { 1004 | 1005 | case TOUCH.DOLLY_PAN: 1006 | 1007 | if (scope.enableZoom === false && scope.enablePan === false) return; 1008 | 1009 | handleTouchStartDollyPan(event); 1010 | 1011 | state = STATE.TOUCH_DOLLY_PAN; 1012 | 1013 | break; 1014 | 1015 | case TOUCH.DOLLY_ROTATE: 1016 | 1017 | if (scope.enableZoom === false && scope.enableRotate === false) return; 1018 | 1019 | handleTouchStartDollyRotate(event); 1020 | 1021 | state = STATE.TOUCH_DOLLY_ROTATE; 1022 | 1023 | break; 1024 | 1025 | default: 1026 | 1027 | state = STATE.NONE; 1028 | 1029 | } 1030 | 1031 | break; 1032 | 1033 | default: 1034 | 1035 | state = STATE.NONE; 1036 | 1037 | } 1038 | 1039 | if (state !== STATE.NONE) { 1040 | 1041 | scope.dispatchEvent(startEvent); 1042 | 1043 | } 1044 | 1045 | } 1046 | 1047 | function onTouchMove(event) { 1048 | if (scope.enabled === false) return; 1049 | 1050 | event.preventDefault(); // prevent scrolling 1051 | event.stopPropagation(); 1052 | 1053 | switch (state) { 1054 | 1055 | case STATE.TOUCH_ROTATE: 1056 | 1057 | if (scope.enableRotate === false) return; 1058 | 1059 | handleTouchMoveRotate(event); 1060 | scope.update(); 1061 | 1062 | break; 1063 | 1064 | case STATE.TOUCH_PAN: 1065 | 1066 | if (scope.enablePan === false) return; 1067 | 1068 | handleTouchMovePan(event); 1069 | scope.update(); 1070 | 1071 | break; 1072 | 1073 | case STATE.TOUCH_DOLLY_PAN: 1074 | 1075 | if (scope.enableZoom === false && scope.enablePan === false) return; 1076 | 1077 | handleTouchMoveDollyPan(event); 1078 | scope.update(); 1079 | 1080 | break; 1081 | 1082 | case STATE.TOUCH_DOLLY_ROTATE: 1083 | 1084 | if (scope.enableZoom === false && scope.enableRotate === false) return; 1085 | 1086 | handleTouchMoveDollyRotate(event); 1087 | scope.update(); 1088 | 1089 | break; 1090 | 1091 | default: 1092 | 1093 | state = STATE.NONE; 1094 | 1095 | } 1096 | 1097 | } 1098 | 1099 | function onTouchEnd(event) { 1100 | 1101 | if (scope.enabled === false) return; 1102 | 1103 | handleTouchEnd(event); 1104 | 1105 | scope.dispatchEvent(endEvent); 1106 | 1107 | state = STATE.NONE; 1108 | 1109 | } 1110 | 1111 | function onContextMenu(event) { 1112 | 1113 | if (scope.enabled === false) return; 1114 | 1115 | event.preventDefault(); 1116 | 1117 | } 1118 | 1119 | // 1120 | 1121 | scope.domElement.addEventListener('contextmenu', onContextMenu, false); 1122 | 1123 | scope.domElement.addEventListener('mousedown', onMouseDown, false); 1124 | scope.domElement.addEventListener('wheel', onMouseWheel, false); 1125 | 1126 | scope.domElement.addEventListener('touchstart', onTouchStart, false); 1127 | scope.domElement.addEventListener('touchend', onTouchEnd, false); 1128 | scope.domElement.addEventListener('touchmove', onTouchMove, false); 1129 | 1130 | scope.domElement.addEventListener('keydown', onKeyDown, false); 1131 | 1132 | // make sure element can receive keys. 1133 | 1134 | if (scope.domElement.tabIndex === -1) { 1135 | 1136 | scope.domElement.tabIndex = 0; 1137 | 1138 | } 1139 | 1140 | // force an update at start 1141 | 1142 | this.update(); 1143 | 1144 | }; 1145 | 1146 | OrbitControls.prototype = Object.create(EventDispatcher.prototype); 1147 | OrbitControls.prototype.constructor = OrbitControls; 1148 | 1149 | 1150 | // This set of controls performs orbiting, dollying (zooming), and panning. 1151 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1152 | // This is very similar to OrbitControls, another set of touch behavior 1153 | // 1154 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1155 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1156 | // Pan - left mouse, or arrow keys / touch: one-finger move 1157 | 1158 | var MapControls = function (object, domElement) { 1159 | 1160 | OrbitControls.call(this, object, domElement); 1161 | 1162 | this.mouseButtons.LEFT = MOUSE.PAN; 1163 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1164 | 1165 | this.touches.ONE = TOUCH.PAN; 1166 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1167 | 1168 | }; 1169 | 1170 | MapControls.prototype = Object.create(EventDispatcher.prototype); 1171 | MapControls.prototype.constructor = MapControls; 1172 | 1173 | return { 1174 | OrbitControls, 1175 | MapControls 1176 | } 1177 | } 1178 | 1179 | export default registerOrbit -------------------------------------------------------------------------------- /example/test-cases/sphere.js: -------------------------------------------------------------------------------- 1 | export function renderSphere(canvas, THREE) { 2 | var container, stats; 3 | var camera, scene, renderer; 4 | var pointLight; 5 | var objects = [], materials = []; 6 | init(); 7 | animate(); 8 | function init() { 9 | camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 1, 2000); 10 | camera.position.set(0, 200, 800); 11 | scene = new THREE.Scene(); 12 | // Grid 13 | var helper = new THREE.GridHelper(1000, 40, 0x303030, 0x303030); 14 | helper.position.y = - 75; 15 | scene.add(helper); 16 | // Materials 17 | // var texture = new THREE.Texture(generateTexture()); 18 | // texture.needsUpdate = true; 19 | // materials.push(new THREE.MeshLambertMaterial({ map: texture, transparent: true })); 20 | materials.push(new THREE.MeshLambertMaterial({ color: 0xdddddd })); 21 | materials.push(new THREE.MeshPhongMaterial({ color: 0xdddddd, specular: 0x009900, shininess: 30, flatShading: true })); 22 | materials.push(new THREE.MeshNormalMaterial()); 23 | materials.push(new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, blending: THREE.AdditiveBlending })); 24 | materials.push(new THREE.MeshLambertMaterial({ color: 0xdddddd })); 25 | materials.push(new THREE.MeshBasicMaterial({ color: 0xffaa00, wireframe: true })); 26 | // materials.push(new THREE.MeshPhongMaterial({ color: 0xdddddd, specular: 0x009900, shininess: 30, map: texture, transparent: true })); 27 | materials.push(new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, blending: THREE.AdditiveBlending })); 28 | materials.push(new THREE.MeshNormalMaterial({ flatShading: true })); 29 | materials.push(new THREE.MeshBasicMaterial({ color: 0xffaa00, wireframe: true })); 30 | materials.push(new THREE.MeshDepthMaterial()); 31 | materials.push(new THREE.MeshLambertMaterial({ color: 0x666666, emissive: 0xff0000 })); 32 | materials.push(new THREE.MeshPhongMaterial({ color: 0x000000, specular: 0x666666, emissive: 0xff0000, shininess: 10, opacity: 0.9, transparent: true })); 33 | materials.push(new THREE.MeshPhongMaterial({ color: 0x000000, specular: 0x666666, emissive: 0xff0000, shininess: 10, opacity: 0.9, transparent: true })); 34 | // materials.push(new THREE.MeshBasicMaterial({ map: texture, transparent: true })); 35 | // Spheres geometry 36 | var geometry = new THREE.SphereBufferGeometry(70, 32, 16); 37 | for (var i = 0, l = materials.length; i < l; i++) { 38 | addMesh(geometry, materials[i]); 39 | } 40 | // Lights 41 | scene.add(new THREE.AmbientLight(0x111111)); 42 | var directionalLight = new THREE.DirectionalLight(0xffffff, 0.125); 43 | directionalLight.position.x = Math.random() - 0.5; 44 | directionalLight.position.y = Math.random() - 0.5; 45 | directionalLight.position.z = Math.random() - 0.5; 46 | directionalLight.position.normalize(); 47 | scene.add(directionalLight); 48 | pointLight = new THREE.PointLight(0xffffff, 1); 49 | scene.add(pointLight); 50 | pointLight.add(new THREE.Mesh(new THREE.SphereBufferGeometry(4, 8, 8), new THREE.MeshBasicMaterial({ color: 0xffffff }))); 51 | // 52 | renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 53 | renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); 54 | renderer.setSize(canvas.width ,canvas.height); 55 | } 56 | function addMesh(geometry, material) { 57 | var mesh = new THREE.Mesh(geometry, material); 58 | mesh.position.x = (objects.length % 4) * 200 - 400; 59 | mesh.position.z = Math.floor(objects.length / 4) * 200 - 200; 60 | mesh.rotation.x = Math.random() * 200 - 100; 61 | mesh.rotation.y = Math.random() * 200 - 100; 62 | mesh.rotation.z = Math.random() * 200 - 100; 63 | objects.push(mesh); 64 | scene.add(mesh); 65 | } 66 | function onWindowResize() { 67 | camera.aspect = window.innerWidth / window.innerHeight; 68 | camera.updateProjectionMatrix(); 69 | renderer.setSize(window.innerWidth, window.innerHeight); 70 | } 71 | function generateTexture() { 72 | canvas.width = 256; 73 | canvas.height = 256; 74 | var context = canvas.getContext('2d'); 75 | var image = context.getImageData(0, 0, 256, 256); 76 | var x = 0, y = 0; 77 | for (var i = 0, j = 0, l = image.data.length; i < l; i += 4, j++) { 78 | x = j % 256; 79 | y = x == 0 ? y + 1 : y; 80 | image.data[i] = 255; 81 | image.data[i + 1] = 255; 82 | image.data[i + 2] = 255; 83 | image.data[i + 3] = Math.floor(x ^ y); 84 | } 85 | context.putImageData(image, 0, 0); 86 | return canvas; 87 | } 88 | // 89 | function animate() { 90 | render(); 91 | canvas.requestAnimationFrame(animate); 92 | } 93 | function render() { 94 | var timer = 0.0005 * Date.now(); 95 | camera.position.x = Math.cos(timer) * 1000; 96 | camera.position.z = Math.sin(timer) * 1000; 97 | camera.lookAt(scene.position); 98 | for (var i = 0, l = objects.length; i < l; i++) { 99 | var object = objects[i]; 100 | object.rotation.x += 0.01; 101 | object.rotation.y += 0.005; 102 | } 103 | // materials[materials.length - 2].emissive.setHSL(0.54, 1, 0.35 * (0.5 + 0.5 * Math.sin(35 * timer))); 104 | // materials[materials.length - 3].emissive.setHSL(0.04, 1, 0.35 * (0.5 + 0.5 * Math.cos(35 * timer))); 105 | pointLight.position.x = Math.sin(timer * 7) * 300; 106 | pointLight.position.y = Math.cos(timer * 5) * 400; 107 | pointLight.position.z = Math.cos(timer * 3) * 300; 108 | renderer.render(scene, camera); 109 | } 110 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threejs-miniprogram", 3 | "version": "0.0.8", 4 | "description": "ThreeJS adapter for WeChat MiniProgram", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "dev": "webpack --config build/webpack.config.js --watch", 8 | "build": "webpack --hide-modules --config build/webpack.config.js", 9 | "lint": "eslint \"src/**/*.js\"", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "miniprogram": "dist/", 13 | "author": "wechat-miniprogram", 14 | "license": "MIT", 15 | "homepage": "https://github.com/wechat-miniprogram/threejs-miniprogram", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/wechat-miniprogram/threejs-miniprogram" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.5.5", 25 | "@babel/plugin-proposal-class-properties": "^7.5.5", 26 | "@babel/preset-env": "^7.5.5", 27 | "abab": "^2.0.3", 28 | "babel-loader": "^8.0.6", 29 | "eslint": "^4.18.2", 30 | "eslint-friendly-formatter": "^2.0.6", 31 | "eslint-loader": "^1.5.0", 32 | "eslint-plugin-jest": "^20.0.3", 33 | "eventemitter2": "^4.1.2", 34 | "jest": "^20.0.4", 35 | "path": "^0.12.7", 36 | "string-replace-loader": "^2.2.0", 37 | "string-replace-webpack-plugin": "^0.1.3", 38 | "three": "0.108.0", 39 | "webpack": "^4.39.1", 40 | "webpack-cli": "^3.3.6", 41 | "babel-eslint": "^10.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/EventTarget.js: -------------------------------------------------------------------------------- 1 | const _events = new WeakMap() 2 | 3 | class Touch { 4 | constructor(touch) { 5 | // CanvasTouch{identifier, x, y} 6 | // Touch{identifier, pageX, pageY, clientX, clientY, force} 7 | this.identifier = touch.identifier 8 | 9 | this.force = touch.force === undefined ? 1 : touch.force 10 | this.pageX = touch.pageX || touch.x 11 | this.pageY = touch.pageY || touch.y 12 | this.clientX = touch.clientX || touch.x 13 | this.clientY = touch.clientY || touch.y 14 | 15 | this.screenX = this.pageX 16 | this.screenY = this.pageY 17 | } 18 | } 19 | 20 | export default class EventTarget { 21 | constructor() { 22 | _events.set(this, {}) 23 | } 24 | 25 | addEventListener(type, listener, options = {}) { 26 | let events = _events.get(this) 27 | 28 | if (!events) { 29 | events = {} 30 | _events.set(this, events) 31 | } 32 | if (!events[type]) { 33 | events[type] = [] 34 | } 35 | events[type].push(listener) 36 | 37 | if (options.capture) { 38 | // console.warn('EventTarget.addEventListener: options.capture is not implemented.') 39 | } 40 | if (options.once) { 41 | // console.warn('EventTarget.addEventListener: options.once is not implemented.') 42 | } 43 | if (options.passive) { 44 | // console.warn('EventTarget.addEventListener: options.passive is not implemented.') 45 | } 46 | } 47 | 48 | removeEventListener(type, listener) { 49 | const events = _events.get(this) 50 | 51 | if (events) { 52 | const listeners = events[type] 53 | 54 | if (listeners && listeners.length > 0) { 55 | for (let i = listeners.length; i--; i > 0) { 56 | if (listeners[i] === listener) { 57 | listeners.splice(i, 1) 58 | break 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | dispatchEvent(event = {}) { 66 | if (typeof event.preventDefault !== 'function') { 67 | event.preventDefault = () => {} 68 | } 69 | if (typeof event.stopPropagation !== 'function') { 70 | event.stopPropagation = () => {} 71 | } 72 | const listeners = _events.get(this)[event.type] 73 | 74 | if (listeners) { 75 | for (let i = 0; i < listeners.length; i++) { 76 | listeners[i](event) 77 | } 78 | } 79 | } 80 | 81 | dispatchTouchEvent(e = {}) { 82 | const target = { 83 | ...this 84 | } 85 | 86 | const event = { 87 | changedTouches: e.changedTouches.map(touch => new Touch(touch)), 88 | touches: e.touches.map(touch => new Touch(touch)), 89 | targetTouches: Array.prototype.slice.call(e.touches.map(touch => new Touch(touch))), 90 | timeStamp: e.timeStamp, 91 | target: target, 92 | currentTarget: target, 93 | type: e.type, 94 | cancelBubble: false, 95 | cancelable: false 96 | } 97 | 98 | this.dispatchEvent(event) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Node.js: -------------------------------------------------------------------------------- 1 | import EventTarget from './EventTarget.js' 2 | 3 | export default class Node extends EventTarget { 4 | constructor() { 5 | super() 6 | this.childNodes = [] 7 | } 8 | 9 | appendChild(node) { 10 | this.childNodes.push(node) 11 | // if (node instanceof Node) { 12 | // this.childNodes.push(node) 13 | // } else { 14 | // throw new TypeError('Failed to executed \'appendChild\' on \'Node\': parameter 1 is not of type \'Node\'.') 15 | // } 16 | } 17 | 18 | cloneNode() { 19 | const copyNode = Object.create(this) 20 | 21 | Object.assign(copyNode, this) 22 | return copyNode 23 | } 24 | 25 | removeChild(node) { 26 | const index = this.childNodes.findIndex((child) => child === node) 27 | 28 | if (index > -1) { 29 | return this.childNodes.splice(index, 1) 30 | } 31 | return null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/XMLHttpRequest.js: -------------------------------------------------------------------------------- 1 | import EventTarget from './EventTarget.js' 2 | 3 | const _requestHeader = new WeakMap() 4 | const _responseHeader = new WeakMap() 5 | const _requestTask = new WeakMap() 6 | 7 | function _triggerEvent(type, event = {}) { 8 | event.target = event.target || this 9 | 10 | if (typeof this[`on${type}`] === 'function') { 11 | this[`on${type}`].call(this, event) 12 | } 13 | } 14 | 15 | function _changeReadyState(readyState, event = {}) { 16 | this.readyState = readyState 17 | 18 | event.readyState = readyState; 19 | 20 | _triggerEvent.call(this, 'readystatechange', event) 21 | } 22 | 23 | function _isRelativePath(url) { 24 | return !(/^(http|https|ftp|wxfile):\/\/.*/i.test(url)); 25 | } 26 | 27 | export default class XMLHttpRequest extends EventTarget { 28 | 29 | constructor() { 30 | super(); 31 | 32 | /* 33 | * TODO 这一批事件应该是在 XMLHttpRequestEventTarget.prototype 上面的 34 | */ 35 | this.onabort = null 36 | this.onerror = null 37 | this.onload = null 38 | this.onloadstart = null 39 | this.onprogress = null 40 | this.ontimeout = null 41 | this.onloadend = null 42 | 43 | this.onreadystatechange = null 44 | this.readyState = 0 45 | this.response = null 46 | this.responseText = null 47 | this.responseType = 'text' 48 | this.dataType = 'string' 49 | this.responseXML = null 50 | this.status = 0 51 | this.statusText = '' 52 | this.upload = {} 53 | this.withCredentials = false 54 | 55 | _requestHeader.set(this, { 56 | 'content-type': 'application/x-www-form-urlencoded' 57 | }) 58 | _responseHeader.set(this, {}) 59 | } 60 | 61 | abort() { 62 | const myRequestTask = _requestTask.get(this) 63 | 64 | if (myRequestTask) { 65 | myRequestTask.abort() 66 | } 67 | } 68 | 69 | getAllResponseHeaders() { 70 | const responseHeader = _responseHeader.get(this) 71 | 72 | return Object.keys(responseHeader).map((header) => { 73 | return `${header}: ${responseHeader[header]}` 74 | }).join('\n') 75 | } 76 | 77 | getResponseHeader(header) { 78 | return _responseHeader.get(this)[header] 79 | } 80 | 81 | open(method, url /* async, user, password 这几个参数在小程序内不支持*/ ) { 82 | this._method = method 83 | this._url = url 84 | _changeReadyState.call(this, XMLHttpRequest.OPENED) 85 | } 86 | 87 | overrideMimeType() {} 88 | 89 | send(data = '') { 90 | if (this.readyState !== XMLHttpRequest.OPENED) { 91 | throw new Error("Failed to execute 'send' on 'XMLHttpRequest': The object's state must be OPENED.") 92 | } else { 93 | const url = this._url 94 | const header = _requestHeader.get(this) 95 | const responseType = this.responseType 96 | const dataType = this.dataType 97 | 98 | const relative = _isRelativePath(url) 99 | let encoding; 100 | 101 | if (responseType === 'arraybuffer') { 102 | // encoding = 'binary' 103 | } else { 104 | encoding = 'utf8' 105 | } 106 | 107 | delete this.response; 108 | this.response = null; 109 | 110 | const onSuccess = ({ data, statusCode, header }) => { 111 | statusCode = statusCode === undefined ? 200 : statusCode; 112 | if (typeof data !== 'string' && !(data instanceof ArrayBuffer)) { 113 | try { 114 | data = JSON.stringify(data) 115 | } catch (e) {} 116 | } 117 | 118 | this.status = statusCode 119 | if (header) { 120 | _responseHeader.set(this, header) 121 | } 122 | _triggerEvent.call(this, 'loadstart') 123 | _changeReadyState.call(this, XMLHttpRequest.HEADERS_RECEIVED) 124 | _changeReadyState.call(this, XMLHttpRequest.LOADING) 125 | 126 | this.response = data 127 | 128 | if (data instanceof ArrayBuffer) { 129 | Object.defineProperty(this, 'responseText', { 130 | enumerable: true, 131 | configurable: true, 132 | get: function() { 133 | throw "InvalidStateError : responseType is " + this.responseType; 134 | } 135 | }); 136 | } else { 137 | this.responseText = data 138 | } 139 | _changeReadyState.call(this, XMLHttpRequest.DONE) 140 | _triggerEvent.call(this, 'load') 141 | _triggerEvent.call(this, 'loadend') 142 | } 143 | 144 | const onFail = ({ errMsg }) => { 145 | // TODO 规范错误 146 | 147 | if (errMsg.indexOf('abort') !== -1) { 148 | _triggerEvent.call(this, 'abort') 149 | } else { 150 | _triggerEvent.call(this, 'error', { 151 | message: errMsg 152 | }) 153 | } 154 | _triggerEvent.call(this, 'loadend') 155 | 156 | if (relative) { 157 | // 用户即使没监听error事件, 也给出相应的警告 158 | console.warn(errMsg) 159 | } 160 | } 161 | 162 | if (relative) { 163 | const fs = wx.getFileSystemManager(); 164 | 165 | var options = { 166 | 'filePath': url, 167 | 'success': onSuccess, 168 | 'fail': onFail 169 | } 170 | if (encoding) { 171 | options['encoding'] = encoding; 172 | } 173 | fs.readFile(options) 174 | return 175 | } 176 | 177 | wx.request({ 178 | data, 179 | url: url, 180 | method: this._method, 181 | header: header, 182 | dataType: dataType, 183 | responseType: responseType, 184 | success: onSuccess, 185 | fail: onFail 186 | }) 187 | } 188 | } 189 | 190 | setRequestHeader(header, value) { 191 | const myHeader = _requestHeader.get(this) 192 | 193 | myHeader[header] = value 194 | _requestHeader.set(this, myHeader) 195 | } 196 | 197 | addEventListener(type, listener) { 198 | if (typeof listener !== 'function') { 199 | return; 200 | } 201 | 202 | this['on' + type] = (event = {}) => { 203 | event.target = event.target || this 204 | listener.call(this, event) 205 | } 206 | } 207 | 208 | removeEventListener(type, listener) { 209 | if (this['on' + type] === listener) { 210 | this['on' + type] = null; 211 | } 212 | } 213 | } 214 | 215 | // TODO 没法模拟 HEADERS_RECEIVED 和 LOADING 两个状态 216 | XMLHttpRequest.UNSEND = 0 217 | XMLHttpRequest.OPENED = 1 218 | XMLHttpRequest.HEADERS_RECEIVED = 2 219 | XMLHttpRequest.LOADING = 3 220 | XMLHttpRequest.DONE = 4 221 | -------------------------------------------------------------------------------- /src/copyProperties.js: -------------------------------------------------------------------------------- 1 | export default function copyProperties(target, source) { 2 | for (let key of Object.getOwnPropertyNames(source)) { 3 | if (key !== 'constructor' 4 | && key !== 'prototype' 5 | && key !== 'name' 6 | ) { 7 | let desc = Object.getOwnPropertyDescriptor(source, key); 8 | Object.defineProperty(target, key, desc); 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {atob as _atob} from 'abab'; 2 | import _XMLHttpRequest from './XMLHttpRequest' 3 | import copyProperties from './copyProperties' 4 | import EventTarget from "./EventTarget" 5 | 6 | export function createScopedThreejs(canvas) { 7 | // adapt canvas 8 | // canvas.style = {width: canvas.width + 'px', height: canvas.height + 'px'} 9 | // canvas.clientHeight = canvas.height 10 | // canvas.clientWidth = canvas.width 11 | 12 | Object.defineProperty(canvas, 'style', { 13 | get() { 14 | return { 15 | width: this.width + 'px', 16 | height: this.height + 'px' 17 | } 18 | } 19 | }) 20 | 21 | Object.defineProperty(canvas, 'clientHeight', { 22 | get() { 23 | return this.height 24 | } 25 | }) 26 | 27 | Object.defineProperty(canvas, 'clientWidth', { 28 | get() { 29 | return this.width 30 | } 31 | }) 32 | 33 | copyProperties(canvas.constructor.prototype, EventTarget.prototype) 34 | 35 | // eslint-disable-next-line 36 | const document = { 37 | createElementNS(_, type) { 38 | if (type === 'canvas') return canvas 39 | if (type === 'img') return canvas.createImage() 40 | } 41 | } 42 | copyProperties(document.constructor.prototype, EventTarget.prototype) 43 | 44 | // eslint-disable-next-line 45 | const window = { 46 | AudioContext: function() {}, 47 | URL: {}, 48 | } 49 | copyProperties(window.constructor.prototype, EventTarget.prototype) 50 | 51 | // eslint-disable-next-line 52 | const atob = (a) => { 53 | return _atob(a) 54 | } 55 | 56 | // eslint-disable-next-line 57 | const XMLHttpRequest = _XMLHttpRequest 58 | 59 | const exports = {}; 60 | 61 | // eslint-disable-next-line 62 | const HTMLCanvasElement = undefined; 63 | 64 | // three.js source code will be injected here 65 | // eslint-disable-next-line 66 | __INJECT_THREE__ 67 | 68 | return exports 69 | } 70 | 71 | --------------------------------------------------------------------------------