├── pages ├── cube │ ├── index.wxss │ ├── index.json │ ├── pikachu.png │ ├── index.wxml │ └── index.js ├── glb │ ├── index.wxss │ ├── index.json │ ├── index.wxml │ └── index.js ├── gltf │ ├── index.wxss │ ├── index.json │ ├── index.wxml │ ├── index.js │ └── loadgLTF.js ├── ngltf │ ├── index.wxss │ ├── index.json │ ├── index.wxml │ ├── index.js │ └── render.js ├── trackcontrol │ ├── index.wxss │ ├── index.json │ ├── pikachu.png │ ├── index.wxml │ └── index.js ├── index │ ├── index.json │ ├── index.js │ ├── index.wxss │ └── index.wxml ├── obj │ ├── index.wxss │ ├── index.json │ ├── index.wxml │ ├── index.js │ └── loadObj.js ├── primitives │ ├── index.wxss │ ├── index.json │ ├── index.wxml │ ├── index.js │ └── primitives.js ├── raycaster │ ├── index.wxss │ ├── index.json │ ├── index.wxml │ └── index.js ├── animation │ ├── index.wxss │ ├── index.json │ ├── index.wxml │ ├── index.js │ └── animate.js └── logs │ ├── logs.json │ ├── logs.wxss │ ├── logs.wxml │ └── logs.js ├── app.js ├── demo ├── qrcode.jpg ├── Screenshot_2019-10-22-11-01-18-748_com.tencent.mm.png ├── Screenshot_2019-10-22-11-03-37-095_com.tencent.mm.png ├── Screenshot_2019-10-22-11-03-47-236_com.tencent.mm.png ├── Screenshot_2019-10-22-11-04-02-587_com.tencent.mm.png ├── Screenshot_2019-10-22-11-04-32-720_com.tencent.mm.png ├── Screenshot_2019-10-24-10-43-43-123_com.tencent.mm.png ├── Screenshot_2019-10-28-19-13-24-007_com.tencent.mm.png ├── Screenshot_2019-10-28-20-01-20-469_com.tencent.mm.png └── Screenshot_2019-10-30-00-14-41-017_com.tencent.mm.png ├── sitemap.json ├── app.wxss ├── utils ├── util.js └── ResourceTracker.js ├── app.json ├── LICENSE ├── project.config.json ├── README.md └── jsm ├── loaders ├── EquirectangularToCubeGenerator.js ├── DDSLoader.js ├── MTLLoader.js ├── STLLoader.js ├── RGBELoader.js └── OBJLoader.js ├── pmrem ├── PMREMCubeUVPacker.js └── PMREMGenerator.js ├── utils └── SkeletonUtils.js └── controls └── TrackballControls.js /pages/cube/index.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/glb/index.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/gltf/index.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/ngltf/index.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/trackcontrol/index.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /pages/obj/index.wxss: -------------------------------------------------------------------------------- 1 | 2 | #c { 3 | width: 100%; 4 | height: 100%; 5 | } -------------------------------------------------------------------------------- /pages/primitives/index.wxss: -------------------------------------------------------------------------------- 1 | 2 | #c { 3 | width: 100%; 4 | height: 100%; 5 | } -------------------------------------------------------------------------------- /pages/raycaster/index.wxss: -------------------------------------------------------------------------------- 1 | 2 | #c { 3 | width: 100%; 4 | height: 100%; 5 | } -------------------------------------------------------------------------------- /pages/animation/index.wxss: -------------------------------------------------------------------------------- 1 | 2 | #c { 3 | width: 100%; 4 | height: 100%; 5 | } -------------------------------------------------------------------------------- /pages/cube/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/glb/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/gltf/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/ngltf/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/obj/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | App({ 2 | onLaunch: function () { 3 | // wx.THREE = THREE 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /pages/animation/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/primitives/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/raycaster/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/trackcontrol/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志", 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /demo/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/qrcode.jpg -------------------------------------------------------------------------------- /pages/cube/pikachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/pages/cube/pikachu.png -------------------------------------------------------------------------------- /pages/primitives/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/trackcontrol/pikachu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/pages/trackcontrol/pikachu.png -------------------------------------------------------------------------------- /pages/glb/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-22-11-01-18-748_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-22-11-01-18-748_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-22-11-03-37-095_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-22-11-03-37-095_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-22-11-03-47-236_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-22-11-03-47-236_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-22-11-04-02-587_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-22-11-04-02-587_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-22-11-04-32-720_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-22-11-04-32-720_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-24-10-43-43-123_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-24-10-43-43-123_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-28-19-13-24-007_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-28-19-13-24-007_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-28-20-01-20-469_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-28-20-01-20-469_com.tencent.mm.png -------------------------------------------------------------------------------- /demo/Screenshot_2019-10-30-00-14-41-017_com.tencent.mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yannliao/threejs-example-for-miniprogram/HEAD/demo/Screenshot_2019-10-30-00-14-41-017_com.tencent.mm.png -------------------------------------------------------------------------------- /sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: {}, 3 | onLoad: function () { 4 | 5 | }, 6 | tap: function(e) { 7 | let path = e.target.dataset.id 8 | wx.navigateTo({ 9 | url: `/pages/${path}/index` 10 | }) 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | page { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | /* .list-container { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: flex-start; 9 | align-items: center; 10 | } */ 11 | button {margin: 10px;} -------------------------------------------------------------------------------- /pages/obj/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/animation/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/raycaster/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/gltf/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/ngltf/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js') 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)) 12 | }) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /pages/cube/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/trackcontrol/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /pages/primitives/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import renderPrimitives from './primitives' 3 | 4 | Page({ 5 | data: {}, 6 | onLoad: function () { 7 | wx.createSelectorQuery() 8 | .select('#c') 9 | .node() 10 | .exec((res) => { 11 | const canvas = new THREE.global.registerCanvas(res[0].node) 12 | renderPrimitives(canvas, THREE) 13 | }) 14 | }, 15 | onUnload: function () { 16 | THREE.global.clearCanvas() 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | module.exports = { 18 | formatTime: formatTime 19 | } 20 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/primitives/index", 5 | "pages/cube/index", 6 | "pages/glb/index", 7 | "pages/gltf/index", 8 | "pages/ngltf/index", 9 | "pages/obj/index", 10 | "pages/animation/index", 11 | "pages/trackcontrol/index", 12 | "pages/raycaster/index", 13 | "pages/logs/logs" 14 | ], 15 | "window": { 16 | "backgroundTextStyle": "light", 17 | "navigationBarBackgroundColor": "#fff", 18 | "navigationBarTitleText": "WeChat", 19 | "navigationBarTextStyle": "black" 20 | }, 21 | "sitemapLocation": "sitemap.json" 22 | } -------------------------------------------------------------------------------- /pages/obj/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import loadObj from './loadObj' 3 | 4 | Page({ 5 | data: {}, 6 | onLoad: function () { 7 | wx.createSelectorQuery() 8 | .select('#c') 9 | .node() 10 | .exec((res) => { 11 | const canvas = new THREE.global.registerCanvas(res[0].node) 12 | loadObj(canvas, THREE) 13 | }) 14 | }, 15 | onUnload: function () { 16 | THREE.global.clearCanvas() 17 | }, 18 | touchStart(e) { 19 | console.log('canvas', e) 20 | THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) 21 | }, 22 | touchMove(e) { 23 | console.log('canvas', e) 24 | THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) 25 | }, 26 | touchEnd(e) { 27 | console.log('canvas', e) 28 | THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) 29 | }, 30 | touchCancel(e) { 31 | // console.log('canvas', e) 32 | }, 33 | longTap(e) { 34 | // console.log('canvas', e) 35 | }, 36 | tap(e) { 37 | // console.log('canvas', e) 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /pages/animation/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import animate from './animate' 3 | 4 | Page({ 5 | data: {}, 6 | onLoad: function () { 7 | wx.createSelectorQuery() 8 | .select('#c') 9 | .node() 10 | .exec((res) => { 11 | const canvas = new THREE.global.registerCanvas(res[0].node) 12 | animate(canvas, THREE) 13 | }) 14 | }, 15 | onUnload: function () { 16 | THREE.global.clearCanvas() 17 | }, 18 | touchStart(e) { 19 | console.log('canvas', e) 20 | THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) 21 | }, 22 | touchMove(e) { 23 | console.log('canvas', e) 24 | THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) 25 | }, 26 | touchEnd(e) { 27 | console.log('canvas', e) 28 | THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) 29 | }, 30 | touchCancel(e) { 31 | // console.log('canvas', e) 32 | }, 33 | longTap(e) { 34 | // console.log('canvas', e) 35 | }, 36 | tap(e) { 37 | // console.log('canvas', e) 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /pages/ngltf/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import render from './render' 3 | 4 | Page({ 5 | data: {}, 6 | onLoad: function () { 7 | wx.createSelectorQuery() 8 | .select('#c') 9 | .node() 10 | .exec((res) => { 11 | const canvas = new THREE.global.registerCanvas(res[0].node) 12 | render(canvas, THREE) 13 | }) 14 | }, 15 | onUnload: function () { 16 | //注意清理global中的canvas对象 17 | THREE.global.clearCanvas() 18 | }, 19 | touchStart(e) { 20 | console.log('canvas', e) 21 | THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) 22 | }, 23 | touchMove(e) { 24 | console.log('canvas', e) 25 | THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) 26 | }, 27 | touchEnd(e) { 28 | console.log('canvas', e) 29 | THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) 30 | }, 31 | touchCancel(e) { 32 | // console.log('canvas', e) 33 | }, 34 | longTap(e) { 35 | // console.log('canvas', e) 36 | }, 37 | tap(e) { 38 | // console.log('canvas', e) 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /pages/gltf/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import loadgLTF from './loadgLTF' 3 | 4 | Page({ 5 | data: {}, 6 | onLoad: function () { 7 | wx.createSelectorQuery() 8 | .select('#c') 9 | .node() 10 | .exec((res) => { 11 | const canvas = new THREE.global.registerCanvas(res[0].node) 12 | loadgLTF(canvas, THREE) 13 | }) 14 | }, 15 | onUnload: function () { 16 | //注意清理global中的canvas对象 17 | THREE.global.clearCanvas() 18 | }, 19 | touchStart(e) { 20 | console.log('canvas', e) 21 | THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) 22 | }, 23 | touchMove(e) { 24 | console.log('canvas', e) 25 | THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) 26 | }, 27 | touchEnd(e) { 28 | console.log('canvas', e) 29 | THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) 30 | }, 31 | touchCancel(e) { 32 | // console.log('canvas', e) 33 | }, 34 | longTap(e) { 35 | // console.log('canvas', e) 36 | }, 37 | tap(e) { 38 | // console.log('canvas', e) 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yann Liao 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 | -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [ 5 | { 6 | "value": "demo", 7 | "type": "folder" 8 | } 9 | ] 10 | }, 11 | "setting": { 12 | "urlCheck": false, 13 | "es6": true, 14 | "postcss": true, 15 | "minified": true, 16 | "newFeature": true, 17 | "coverView": true, 18 | "nodeModules": true, 19 | "autoAudits": false, 20 | "uglifyFileName": true, 21 | "checkInvalidKey": true, 22 | "checkSiteMap": true, 23 | "uploadWithSourceMap": true, 24 | "babelSetting": { 25 | "ignore": [], 26 | "disablePlugins": [], 27 | "outputPath": "" 28 | } 29 | }, 30 | "compileType": "miniprogram", 31 | "libVersion": "2.8.3", 32 | "appid": "wx6d94fd5dfb164f75", 33 | "projectname": "threejs-example", 34 | "debugOptions": { 35 | "hidedInDevtools": [] 36 | }, 37 | "isGameTourist": false, 38 | "simulatorType": "wechat", 39 | "simulatorPluginLibVersion": {}, 40 | "condition": { 41 | "search": { 42 | "current": -1, 43 | "list": [] 44 | }, 45 | "conversation": { 46 | "current": -1, 47 | "list": [] 48 | }, 49 | "game": { 50 | "currentL": -1, 51 | "list": [] 52 | }, 53 | "miniprogram": { 54 | "current": -1, 55 | "list": [] 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /pages/glb/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import gLTF from '../../jsm/loaders/GLTFLoader' 3 | let window = THREE.global 4 | let GLTFLoader = gLTF(THREE) 5 | 6 | Page({ 7 | data: {}, 8 | onLoad: function () { 9 | var that = this 10 | wx.createSelectorQuery() 11 | .select('#c') 12 | .node() 13 | .exec((res) => { 14 | const canvas = new THREE.global.registerCanvas(res[0].node) 15 | that.render(canvas, THREE) 16 | }) 17 | }, 18 | onUnload: function () { 19 | //注意清理global中的canvas对象 20 | THREE.global.clearCanvas() 21 | }, 22 | render(canvas) { 23 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 24 | renderer.setPixelRatio(window.devicePixelRatio); 25 | // renderer.setSize(canvas.width, canvas.height); 26 | renderer.gammaOutput = true; 27 | 28 | const fov = 45; 29 | const aspect = 2; // the canvas default 30 | const near = 0.1; 31 | const far = 100; 32 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 33 | camera.position.set(0, 10, 20); 34 | 35 | 36 | const scene = new THREE.Scene(); 37 | scene.background = new THREE.Color('black'); 38 | 39 | 40 | { 41 | const skyColor = 0xB1E1FF; // light blue 42 | const groundColor = 0xB97A20; // brownish orange 43 | const intensity = 1; 44 | const light = new THREE.HemisphereLight(skyColor, groundColor, intensity); 45 | scene.add(light); 46 | } 47 | 48 | { 49 | const color = 0xFFFFFF; 50 | const intensity = 1; 51 | const light = new THREE.DirectionalLight(color, intensity); 52 | light.position.set(5, 10, 2); 53 | scene.add(light); 54 | scene.add(light.target); 55 | } 56 | 57 | 58 | 59 | { 60 | const gltfLoader = new GLTFLoader(); 61 | gltfLoader.load('https://cdn.liubaiwenhua.com/201812/ctree/resource/model/scene.glb', (gltf) => { 62 | const root = gltf.scene; 63 | scene.add(root); 64 | }); 65 | } 66 | 67 | 68 | 69 | function render() { 70 | 71 | camera.aspect = canvas.clientWidth / canvas.clientHeight; 72 | camera.updateProjectionMatrix(); 73 | 74 | renderer.render(scene, camera); 75 | canvas.requestAnimationFrame(render); 76 | } 77 | 78 | canvas.requestAnimationFrame(render); 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /utils/ResourceTracker.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../libs/three.weapp.js' 2 | export default class ResourceTracker { 3 | constructor() { 4 | this.resources = new Set(); 5 | } 6 | track(resource) { 7 | if (!resource) { 8 | return resource; 9 | } 10 | 11 | // handle children and when material is an array of materials or 12 | // uniform is array of textures 13 | if (Array.isArray(resource)) { 14 | resource.forEach(resource => this.track(resource)); 15 | return resource; 16 | } 17 | 18 | if (resource.dispose || resource instanceof THREE.Object3D) { 19 | this.resources.add(resource); 20 | } 21 | if (resource instanceof THREE.Object3D) { 22 | this.track(resource.geometry); 23 | this.track(resource.material); 24 | this.track(resource.children); 25 | } else if (resource instanceof THREE.Material) { 26 | // We have to check if there are any textures on the material 27 | for (const value of Object.values(resource)) { 28 | if (value instanceof THREE.Texture) { 29 | this.track(value); 30 | } 31 | } 32 | // We also have to check if any uniforms reference textures or arrays of textures 33 | if (resource.uniforms) { 34 | for (const value of Object.values(resource.uniforms)) { 35 | if (value) { 36 | const uniformValue = value.value; 37 | if (uniformValue instanceof THREE.Texture || 38 | Array.isArray(uniformValue)) { 39 | this.track(uniformValue); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | return resource; 46 | } 47 | untrack(resource) { 48 | this.resources.delete(resource); 49 | } 50 | dispose() { 51 | for (const resource of this.resources) { 52 | if (resource instanceof THREE.Object3D) { 53 | if (resource.parent) { 54 | resource.parent.remove(resource); 55 | } 56 | } 57 | if (resource.dispose) { 58 | resource.dispose(); 59 | } 60 | } 61 | this.resources.clear(); 62 | } 63 | } -------------------------------------------------------------------------------- /pages/ngltf/render.js: -------------------------------------------------------------------------------- 1 | import gLTF from '../../jsm/loaders/GLTFLoader' 2 | import { OrbitControls } from '../../jsm/controls/OrbitControls' 3 | 4 | import getRGBLoader from '../../jsm/loaders/RGBELoader.js'; 5 | import getGenerator from '../../jsm/loaders/EquirectangularToCubeGenerator.js'; 6 | import getPMREMGenerator from '../../jsm/pmrem/PMREMGenerator.js'; 7 | import getPMREMCubeUVPacker from '../../jsm/pmrem/PMREMCubeUVPacker.js'; 8 | 9 | 10 | export default function (canvas, THREE) { 11 | let GLTFLoader = gLTF(THREE); 12 | let { RGBELoader } = getRGBLoader(THREE) 13 | let { EquirectangularToCubeGenerator } = getGenerator(THREE) 14 | let { PMREMGenerator } = getPMREMGenerator(THREE) 15 | let { PMREMCubeUVPacker } = getPMREMCubeUVPacker(THREE) 16 | let window = THREE.global 17 | 18 | var controls; 19 | var camera, scene, renderer; 20 | 21 | init(); 22 | animate(); 23 | 24 | function init() { 25 | 26 | renderer = new THREE.WebGLRenderer({ antialias: true }); 27 | renderer.setPixelRatio(window.devicePixelRatio); 28 | // renderer.setSize(canvas.width, canvas.height); 29 | renderer.gammaOutput = true; 30 | 31 | 32 | camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.25, 20); 33 | camera.position.set(- 1.8, 0.9, 2.7); 34 | 35 | scene = new THREE.Scene(); 36 | 37 | new RGBELoader() 38 | .setDataType(THREE.UnsignedByteType) 39 | .setPath('https://threejs.org/examples/textures/equirectangular/') 40 | .load('pedestrian_overpass_2k.hdr', function (texture) { 41 | 42 | var cubeGenerator = new EquirectangularToCubeGenerator(texture, { resolution: 1024 }); 43 | cubeGenerator.update(renderer); 44 | 45 | var pmremGenerator = new PMREMGenerator(cubeGenerator.renderTarget.texture); 46 | pmremGenerator.update(renderer); 47 | 48 | var pmremCubeUVPacker = new PMREMCubeUVPacker(pmremGenerator.cubeLods); 49 | pmremCubeUVPacker.update(renderer); 50 | 51 | var envMap = pmremCubeUVPacker.CubeUVRenderTarget.texture; 52 | 53 | // model 54 | 55 | var loader = new GLTFLoader().setPath('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/'); 56 | loader.load('DamagedHelmet.gltf', function (gltf) { 57 | 58 | gltf.scene.traverse(function (child) { 59 | 60 | if (child.isMesh) { 61 | 62 | child.material.envMap = envMap; 63 | 64 | } 65 | 66 | }); 67 | 68 | scene.add(gltf.scene); 69 | 70 | }); 71 | 72 | pmremGenerator.dispose(); 73 | pmremCubeUVPacker.dispose(); 74 | 75 | scene.background = cubeGenerator.renderTarget; 76 | 77 | }); 78 | 79 | 80 | 81 | controls = new OrbitControls(camera, renderer.domElement); 82 | controls.target.set(0, - 0.2, - 0.2); 83 | controls.update(); 84 | } 85 | 86 | // 87 | 88 | function animate() { 89 | 90 | canvas.requestAnimationFrame(animate); 91 | 92 | renderer.render(scene, camera); 93 | 94 | } 95 | 96 | 97 | } -------------------------------------------------------------------------------- /pages/cube/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import { OrbitControls } from '../../jsm/controls/OrbitControls' 3 | 4 | Page({ 5 | data: { 6 | canvasId: null 7 | }, 8 | onLoad: function () { 9 | wx.createSelectorQuery() 10 | .select('#c') 11 | .node() 12 | .exec((res) => { 13 | let canvasId = res[0].node._canvasId 14 | const canvas = THREE.global.registerCanvas(canvasId, res[0].node) 15 | 16 | this.setData({ canvasId }) 17 | 18 | const camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 1, 1000); 19 | camera.position.z = 500; 20 | const scene = new THREE.Scene(); 21 | scene.background = new THREE.Color(0xAAAAAA); 22 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 23 | 24 | const controls = new OrbitControls(camera, renderer.domElement); 25 | // controls.enableDamping = true; 26 | // controls.dampingFactor = 0.25; 27 | // controls.enableZoom = false; 28 | camera.position.set(200, 200, 500); 29 | controls.update(); 30 | const geometry = new THREE.BoxBufferGeometry(200, 200, 200); 31 | 32 | const texture = new THREE.TextureLoader().load('./pikachu.png'); 33 | const material = new THREE.MeshBasicMaterial({ map: texture }); 34 | 35 | // const material = new THREE.MeshBasicMaterial({ color: 0x44aa88 }); 36 | const mesh = new THREE.Mesh(geometry, material); 37 | scene.add(mesh); 38 | 39 | // renderer.setPixelRatio(wx.getSystemInfoSync().pixelRatio); 40 | // renderer.setSize(canvas.width, canvas.height); 41 | 42 | function onWindowResize() { 43 | camera.aspect = window.innerWidth / window.innerHeight; 44 | camera.updateProjectionMatrix(); 45 | renderer.setSize(canvas.width, canvas.height); 46 | } 47 | function render() { 48 | canvas.requestAnimationFrame(render); 49 | // mesh.rotation.x += 0.005; 50 | // mesh.rotation.y += 0.01; 51 | controls.update(); 52 | renderer.render(scene, camera); 53 | } 54 | 55 | render() 56 | 57 | }) 58 | }, 59 | onUnload: function () { 60 | THREE.global.unregisterCanvas(this.data.canvasId) 61 | }, 62 | touchStart(e) { 63 | console.log('canvas', e) 64 | THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) 65 | }, 66 | touchMove(e) { 67 | console.log('canvas', e) 68 | THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) 69 | }, 70 | touchEnd(e) { 71 | console.log('canvas', e) 72 | THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) 73 | }, 74 | touchCancel(e) { 75 | // console.log('canvas', e) 76 | }, 77 | longTap(e) { 78 | // console.log('canvas', e) 79 | }, 80 | tap(e) { 81 | // console.log('canvas', e) 82 | }, 83 | documentTouchStart(e) { 84 | // console.log('document',e) 85 | }, 86 | documentTouchMove(e) { 87 | // console.log('document',e) 88 | }, 89 | documentTouchEnd(e) { 90 | // console.log('document',e) 91 | }, 92 | }) 93 | -------------------------------------------------------------------------------- /pages/obj/loadObj.js: -------------------------------------------------------------------------------- 1 | 2 | import getDDSLoader from '../../jsm/loaders/DDSLoader.js'; 3 | import getMTLLoader from '../../jsm/loaders/MTLLoader.js'; 4 | import getOBJLoader from '../../jsm/loaders/OBJLoader.js'; 5 | import { OrbitControls } from '../../jsm/controls/OrbitControls'; 6 | 7 | export default function (canvas, THREE) { 8 | let { DDSLoader } = getDDSLoader(THREE); 9 | let { MTLLoader } = getMTLLoader(THREE); 10 | let OBJLoader = getOBJLoader(THREE); 11 | let window = THREE.global; 12 | 13 | let camera, scene, renderer, controls; 14 | 15 | let object; 16 | 17 | init(); 18 | animate(); 19 | 20 | 21 | function init() { 22 | //renderer 23 | { 24 | renderer = new THREE.WebGLRenderer({ canvas }); 25 | renderer.setPixelRatio(window.devicePixelRatio); 26 | renderer.setSize(window.innerWidth, window.innerHeight); 27 | } 28 | //camera 29 | { 30 | camera = new THREE.PerspectiveCamera(45, canvas.clientWidth / canvas.clientHeight, 1, 2000); 31 | camera.position.z = 250; 32 | } 33 | //controls 34 | { 35 | controls = new OrbitControls(camera, canvas); 36 | controls.target.set(0, 5, 0); 37 | controls.update(); 38 | } 39 | 40 | // scene & light 41 | { 42 | scene = new THREE.Scene(); 43 | 44 | let ambientLight = new THREE.AmbientLight(0xcccccc, 0.4); 45 | scene.add(ambientLight); 46 | 47 | let pointLight = new THREE.PointLight(0xffffff, 0.8); 48 | camera.add(pointLight); 49 | scene.add(camera); 50 | } 51 | 52 | // manager 53 | 54 | function loadModel() { 55 | 56 | object.traverse(function (child) { 57 | 58 | if (child.isMesh) child.material.map = texture; 59 | 60 | }); 61 | 62 | object.position.y = - 95; 63 | scene.add(object); 64 | 65 | } 66 | 67 | let manager = new THREE.LoadingManager(loadModel); 68 | 69 | manager.onProgress = function (item, loaded, total) { 70 | 71 | console.log(item, loaded, total); 72 | 73 | }; 74 | 75 | // texture 76 | 77 | let textureLoader = new THREE.TextureLoader(manager); 78 | 79 | let texture = textureLoader.load('https://threejs.org/examples/textures/uv_grid_opengl.jpg'); 80 | 81 | // model 82 | 83 | function onProgress(xhr) { 84 | 85 | if (xhr.lengthComputable) { 86 | 87 | let percentComplete = xhr.loaded / xhr.total * 100; 88 | console.log('model ' + Math.round(percentComplete, 2) + '% downloaded'); 89 | 90 | } 91 | 92 | } 93 | 94 | function onError() { } 95 | 96 | let loader = new OBJLoader(manager); 97 | 98 | loader.load('https://threejs.org/examples/models/obj/male02/male02.obj', function (obj) { 99 | 100 | object = obj; 101 | 102 | }, onProgress, onError); 103 | 104 | } 105 | 106 | function animate() { 107 | 108 | canvas.requestAnimationFrame(animate); 109 | render(); 110 | 111 | } 112 | 113 | function render() { 114 | 115 | camera.lookAt(scene.position); 116 | controls.update(); 117 | renderer.render(scene, camera); 118 | 119 | } 120 | 121 | 122 | } -------------------------------------------------------------------------------- /pages/raycaster/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | 3 | let window = THREE.global; 4 | let { document } = window; 5 | 6 | Page({ 7 | data: { 8 | }, 9 | onLoad: function () { 10 | this.theta = 0; 11 | this.mouse = new THREE.Vector2(); 12 | this.radius = 100; 13 | 14 | wx.createSelectorQuery() 15 | .select('#c') 16 | .node() 17 | .exec((res) => { 18 | const canvas = THREE.global.registerCanvas(res[0].node) 19 | this.canvas = canvas; 20 | 21 | this.init(canvas) 22 | this.animate(canvas); 23 | }) 24 | 25 | }, 26 | 27 | render() { 28 | let { scene, mouse, renderer, radius, raycaster } = this; 29 | 30 | this.theta += 0.1; 31 | 32 | this.camera.position.x = radius * Math.sin(THREE.Math.degToRad(this.theta)); 33 | this.camera.position.y = radius * Math.sin(THREE.Math.degToRad(this.theta)); 34 | this.camera.position.z = radius * Math.cos(THREE.Math.degToRad(this.theta)); 35 | this.camera.lookAt(scene.position); 36 | 37 | this.camera.updateMatrixWorld(); 38 | 39 | // find intersections 40 | 41 | raycaster.setFromCamera(mouse, this.camera); 42 | 43 | var intersects = raycaster.intersectObjects(scene.children); 44 | 45 | if (intersects.length > 0) { 46 | 47 | if (this.INTERSECTED != intersects[0].object) { 48 | 49 | if (this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex); 50 | 51 | this.INTERSECTED = intersects[0].object; 52 | this.INTERSECTED.currentHex = this.INTERSECTED.material.emissive.getHex(); 53 | this.INTERSECTED.material.emissive.setHex(0xff0000); 54 | 55 | } 56 | 57 | } else { 58 | 59 | if (this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex); 60 | 61 | this.INTERSECTED = null; 62 | 63 | } 64 | 65 | renderer.render(scene, this.camera); 66 | 67 | }, 68 | animate() { 69 | let canvas = this.canvas; 70 | canvas.requestAnimationFrame(this.animate); 71 | 72 | this.render(); 73 | 74 | }, 75 | init(canvas) { 76 | 77 | this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000); 78 | 79 | this.scene = new THREE.Scene(); 80 | this.scene.background = new THREE.Color(0xf0f0f0); 81 | 82 | var light = new THREE.DirectionalLight(0xffffff, 1); 83 | light.position.set(1, 1, 1).normalize(); 84 | this.scene.add(light); 85 | 86 | var geometry = new THREE.BoxBufferGeometry(20, 20, 20); 87 | 88 | for (var i = 0; i < 2000; i++) { 89 | 90 | var object = new THREE.Mesh(geometry, new THREE.MeshLambertMaterial({ color: Math.random() * 0xffffff })); 91 | 92 | object.position.x = Math.random() * 800 - 400; 93 | object.position.y = Math.random() * 800 - 400; 94 | object.position.z = Math.random() * 800 - 400; 95 | 96 | object.rotation.x = Math.random() * 2 * Math.PI; 97 | object.rotation.y = Math.random() * 2 * Math.PI; 98 | object.rotation.z = Math.random() * 2 * Math.PI; 99 | 100 | object.scale.x = Math.random() + 0.5; 101 | object.scale.y = Math.random() + 0.5; 102 | object.scale.z = Math.random() + 0.5; 103 | 104 | this.scene.add(object); 105 | 106 | } 107 | 108 | this.raycaster = new THREE.Raycaster(); 109 | 110 | this.renderer = new THREE.WebGLRenderer({ canvas }); 111 | // this.renderer.setPixelRatio(window.devicePixelRatio); 112 | this.renderer.setSize(window.innerWidth, window.innerHeight); 113 | 114 | 115 | // 116 | window.addEventListener('resize', onWindowResize, false); 117 | 118 | function onWindowResize() { 119 | 120 | this.camera.aspect = window.innerWidth / window.innerHeight; 121 | this.camera.updateProjectionMatrix(); 122 | 123 | this.renderer.setSize(window.innerWidth, window.innerHeight); 124 | 125 | } 126 | 127 | 128 | 129 | }, 130 | onUnload() { 131 | THREE.global.clearCanvas() 132 | }, 133 | touchStart(e) { 134 | // console.log('canvas', e) 135 | // THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) 136 | }, 137 | touchMove(event) { 138 | console.log('canvas', event) 139 | 140 | let touch = event.touches[0]; 141 | this.mouse.x = (touch.x / window.innerWidth) * 2 - 1; 142 | this.mouse.y = - (touch.y / window.innerHeight) * 2 + 1; 143 | 144 | }, 145 | touchEnd(e) { 146 | // console.log('canvas', e) 147 | // THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) 148 | }, 149 | touchCancel(e) { 150 | // console.log('canvas', e) 151 | }, 152 | longTap(e) { 153 | // console.log('canvas', e) 154 | }, 155 | tap(e) { 156 | 157 | // console.log('canvas', e) 158 | }, 159 | }) 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three.js example 2 | 3 | three.js example in wechat miniprogram 4 | 5 | ---- 6 | 7 | three.js 在小程序里的使用示例,其中 [three.js](https://github.com/yannliao/three.js) 使用的是小程序移植版,详情请到 [https://github.com/yannliao/three.js](https://github.com/yannliao/three.js)。 8 | 9 | ## 注意 10 | 1. 由于示例中有较多的模型是使用官网链接,加载比较慢。如果可以请查找替换资源路径,并替换。小程序只可以跟指定的域名与进行网络通信。使用前请[配置信任域名](https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html) 11 | 12 | 2. 内存泄漏。为了防止内存泄漏,请在页面unload时,用 THREE.global.unregisterCanvas 或 THREE.global.clearCanvas 清除THREE.global中的canvas引用。同时利用canvas.cancelAnimationFrame 清除动画, 并且释放 Geometries Materials 等 Three.js 对象, 参考[How to dispose of objects](https://threejs.org/docs/index.html#manual/en/introduction/How-to-dispose-of-objects), [Three.js Cleanup](https://threejsfundamentals.org/threejs/lessons/threejs-cleanup.html) 13 | 14 | ## 交流 15 | 大家可以加入QQ群进行交流,群号: 858741591 16 | 17 | 18 | 19 | ## 示例&兼容性 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 44 | 49 | 50 | 51 | 52 | 55 | 60 | 61 | 62 | 63 | 66 | 71 | 72 | 73 | 74 | 77 | 82 | 83 | 84 | 85 | 89 | 94 | 95 | 96 | 97 | 100 | 105 | 106 | 107 | 108 | 111 | 115 | 116 | 117 |
名称性能机型
基本 34 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
35 | vivo X21A Android 9 Wechat Version 7.0.5
36 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
37 |
动画 42 | 43 | 45 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
46 | vivo X21A Android 9 Wechat Version 7.0.5
47 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
48 |
OrbitControl 立方 53 | 54 | 56 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
57 | vivo X21A Android 9 Wechat Version 7.0.5
58 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
59 |
gLTF 文件加载 64 | 65 | 67 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
68 | vivo X21A Android 9 Wechat Version 7.0.5
69 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
70 |
glb 文件 75 | 76 | 78 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
79 | vivo X21A Android 9 Wechat Version 7.0.5
80 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
81 |
gLTF 文件加贴图 86 | 87 | 88 | 90 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
91 | vivo X21A Android 9 Wechat Version 7.0.5
92 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
93 |
obj 文件 (WEBGL_compressed_texture_s3tc 扩展不支持) 98 | 99 | 101 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
102 | vivo X21A Android 9 Wechat Version 7.0.5
103 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
104 |
raycaster 选中立方体 109 | 110 | 112 | xiaomi MIX2 Android 8.0.0 Wechat Version 7.0.5
113 | iphone 8Plus ios13.1.2 Wechat Version 7.0.8
114 |
118 | -------------------------------------------------------------------------------- /pages/trackcontrol/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../libs/three.weapp.js' 2 | import getControl from '../../jsm/controls/TrackballControls.js' 3 | let { TrackballControls } = getControl(THREE); 4 | let window = THREE.global; 5 | import ResourceTracker from '../../utils/ResourceTracker'; 6 | 7 | const resMgr = new ResourceTracker(); 8 | const track = resMgr.track.bind(resMgr); 9 | 10 | Page({ 11 | data: { 12 | canvas: null 13 | }, 14 | onLoad: function () { 15 | let that = this; 16 | wx.createSelectorQuery() 17 | .select('#c') 18 | .node() 19 | .exec((res) => { 20 | const canvas = THREE.global.registerCanvas(res[0].node); 21 | that.setData({ 22 | canvas: canvas 23 | }) 24 | const params = { 25 | orthographicCamera: false 26 | }; 27 | 28 | const frustumSize = 400; 29 | 30 | 31 | const aspect = window.innerWidth / window.innerHeight; 32 | 33 | const perspectiveCamera = new THREE.PerspectiveCamera(60, aspect, 1, 1000); 34 | perspectiveCamera.position.z = 500; 35 | 36 | const orthographicCamera = new THREE.OrthographicCamera(frustumSize * aspect / - 2, frustumSize * aspect / 2, frustumSize / 2, frustumSize / - 2, 1, 1000); 37 | orthographicCamera.position.z = 500; 38 | 39 | // world 40 | 41 | const scene = new THREE.Scene(); 42 | scene.background = new THREE.Color(0xcccccc); 43 | scene.fog = new THREE.FogExp2(0xcccccc, 0.002); 44 | track(scene); 45 | 46 | const geometry = new THREE.CylinderBufferGeometry(0, 10, 30, 4, 1); 47 | const material = new THREE.MeshPhongMaterial({ color: 0xffffff, flatShading: true }); 48 | 49 | for (let i = 0; i < 500; i++) { 50 | 51 | let mesh = new THREE.Mesh(geometry, material); 52 | mesh.position.x = (Math.random() - 0.5) * 1000; 53 | mesh.position.y = (Math.random() - 0.5) * 1000; 54 | mesh.position.z = (Math.random() - 0.5) * 1000; 55 | mesh.updateMatrix(); 56 | mesh.matrixAutoUpdate = false; 57 | scene.add(mesh); 58 | 59 | } 60 | 61 | // lights 62 | 63 | { 64 | let dlight = new THREE.DirectionalLight(0xffffff); 65 | dlight.position.set(1, 1, 1); 66 | scene.add(dlight); 67 | 68 | let Dlight = new THREE.DirectionalLight(0x002288); 69 | Dlight.position.set(- 1, - 1, - 1); 70 | scene.add(Dlight); 71 | 72 | let Alight = new THREE.AmbientLight(0x222222); 73 | scene.add(Alight); 74 | } 75 | // renderer 76 | 77 | const renderer = new THREE.WebGLRenderer({ antialias: true }); 78 | renderer.setPixelRatio(window.devicePixelRatio); 79 | renderer.setSize(window.innerWidth, window.innerHeight); 80 | 81 | 82 | const controls = new TrackballControls(perspectiveCamera, renderer.domElement); 83 | 84 | controls.rotateSpeed = 1.0; 85 | controls.zoomSpeed = 1.2; 86 | controls.panSpeed = 0.8; 87 | 88 | controls.staticMoving = true; 89 | controls.dynamicDampingFactor = 0.3; 90 | 91 | controls.keys = [65, 83, 68]; 92 | 93 | controls.addEventListener('change', render); 94 | 95 | 96 | 97 | window.addEventListener('resize', onWindowResize, false); 98 | 99 | function onWindowResize() { 100 | 101 | var aspect = window.innerWidth / window.innerHeight; 102 | 103 | perspectiveCamera.aspect = aspect; 104 | perspectiveCamera.updateProjectionMatrix(); 105 | 106 | orthographicCamera.left = - frustumSize * aspect / 2; 107 | orthographicCamera.right = frustumSize * aspect / 2; 108 | orthographicCamera.top = frustumSize / 2; 109 | orthographicCamera.bottom = - frustumSize / 2; 110 | orthographicCamera.updateProjectionMatrix(); 111 | 112 | renderer.setSize(window.innerWidth, window.innerHeight); 113 | 114 | // controls.handleResize(); 115 | 116 | render(); 117 | 118 | } 119 | 120 | function animate() { 121 | 122 | that.animateId = canvas.requestAnimationFrame(animate); 123 | 124 | controls.update(); 125 | 126 | } 127 | 128 | function render() { 129 | 130 | let camera = (params.orthographicCamera) ? orthographicCamera : perspectiveCamera; 131 | 132 | renderer.render(scene, camera); 133 | 134 | } 135 | 136 | render(); 137 | animate(); 138 | }) 139 | }, 140 | onUnload: function () { 141 | let that = this; 142 | that.data.canvas.cancelAnimationFrame(that.animateId); 143 | that.animateId = null; 144 | that.setData({ 145 | canvas: null 146 | }); 147 | THREE.global.clearCanvas(); 148 | resMgr.dispose(); 149 | }, 150 | touchStart(e) { 151 | console.log('canvas', e) 152 | THREE.global.touchEventHandlerFactory('canvas', 'touchstart')(e) 153 | }, 154 | touchMove(e) { 155 | console.log('canvas', e) 156 | THREE.global.touchEventHandlerFactory('canvas', 'touchmove')(e) 157 | }, 158 | touchEnd(e) { 159 | console.log('canvas', e) 160 | THREE.global.touchEventHandlerFactory('canvas', 'touchend')(e) 161 | }, 162 | touchCancel(e) { 163 | // console.log('canvas', e) 164 | }, 165 | longTap(e) { 166 | // console.log('canvas', e) 167 | }, 168 | tap(e) { 169 | // console.log('canvas', e) 170 | }, 171 | documentTouchStart(e) { 172 | // console.log('document',e) 173 | }, 174 | documentTouchMove(e) { 175 | // console.log('document',e) 176 | }, 177 | documentTouchEnd(e) { 178 | // console.log('document',e) 179 | }, 180 | }) 181 | -------------------------------------------------------------------------------- /jsm/loaders/EquirectangularToCubeGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Richard M. / https://github.com/richardmonette 3 | * @author WestLangley / http://github.com/WestLangley 4 | */ 5 | export default function (THREE) { 6 | let { 7 | BackSide, 8 | BoxBufferGeometry, 9 | CubeCamera, 10 | Mesh, 11 | NoBlending, 12 | PerspectiveCamera, 13 | Scene, 14 | ShaderMaterial, 15 | UniformsUtils, 16 | WebGLRenderTargetCube 17 | } = THREE; 18 | 19 | var CubemapGenerator = function ( renderer ) { 20 | 21 | this.renderer = renderer; 22 | 23 | }; 24 | 25 | CubemapGenerator.prototype.fromEquirectangular = function ( texture, options ) { 26 | 27 | options = options || {}; 28 | 29 | var scene = new Scene(); 30 | 31 | var shader = { 32 | 33 | uniforms: { 34 | tEquirect: { value: null }, 35 | }, 36 | 37 | vertexShader: 38 | 39 | ` 40 | varying vec3 vWorldDirection; 41 | 42 | //include 43 | vec3 transformDirection( in vec3 dir, in mat4 matrix ) { 44 | 45 | return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); 46 | 47 | } 48 | 49 | void main() { 50 | 51 | vWorldDirection = transformDirection( position, modelMatrix ); 52 | 53 | #include 54 | #include 55 | 56 | } 57 | `, 58 | 59 | fragmentShader: 60 | 61 | ` 62 | uniform sampler2D tEquirect; 63 | 64 | varying vec3 vWorldDirection; 65 | 66 | //include 67 | #define RECIPROCAL_PI 0.31830988618 68 | #define RECIPROCAL_PI2 0.15915494 69 | 70 | void main() { 71 | 72 | vec3 direction = normalize( vWorldDirection ); 73 | 74 | vec2 sampleUV; 75 | 76 | sampleUV.y = asin( clamp( direction.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; 77 | 78 | sampleUV.x = atan( direction.z, direction.x ) * RECIPROCAL_PI2 + 0.5; 79 | 80 | gl_FragColor = texture2D( tEquirect, sampleUV ); 81 | 82 | } 83 | ` 84 | }; 85 | 86 | var material = new ShaderMaterial( { 87 | 88 | type: 'CubemapFromEquirect', 89 | 90 | uniforms: UniformsUtils.clone( shader.uniforms ), 91 | vertexShader: shader.vertexShader, 92 | fragmentShader: shader.fragmentShader, 93 | side: BackSide, 94 | blending: NoBlending 95 | 96 | } ); 97 | 98 | material.uniforms.tEquirect.value = texture; 99 | 100 | var mesh = new Mesh( new BoxBufferGeometry( 5, 5, 5 ), material ); 101 | 102 | scene.add( mesh ); 103 | 104 | var resolution = options.resolution || 512; 105 | 106 | var params = { 107 | type: texture.type, 108 | format: texture.format, 109 | encoding: texture.encoding, 110 | generateMipmaps: ( options.generateMipmaps !== undefined ) ? options.generateMipmaps : texture.generateMipmaps, 111 | minFilter: ( options.minFilter !== undefined ) ? options.minFilter : texture.minFilter, 112 | magFilter: ( options.magFilter !== undefined ) ? options.magFilter : texture.magFilter 113 | }; 114 | 115 | var camera = new CubeCamera( 1, 10, resolution, params ); 116 | 117 | camera.update( this.renderer, scene ); 118 | 119 | mesh.geometry.dispose(); 120 | mesh.material.dispose(); 121 | 122 | return camera.renderTarget; 123 | 124 | }; 125 | 126 | // 127 | 128 | var EquirectangularToCubeGenerator = ( function () { 129 | 130 | var camera = new PerspectiveCamera( 90, 1, 0.1, 10 ); 131 | var scene = new Scene(); 132 | var boxMesh = new Mesh( new BoxBufferGeometry( 1, 1, 1 ), getShader() ); 133 | boxMesh.material.side = BackSide; 134 | scene.add( boxMesh ); 135 | 136 | var EquirectangularToCubeGenerator = function ( sourceTexture, options ) { 137 | 138 | options = options || {}; 139 | 140 | this.sourceTexture = sourceTexture; 141 | this.resolution = options.resolution || 512; 142 | 143 | this.views = [ 144 | { t: [ 1, 0, 0 ], u: [ 0, - 1, 0 ] }, 145 | { t: [ - 1, 0, 0 ], u: [ 0, - 1, 0 ] }, 146 | { t: [ 0, 1, 0 ], u: [ 0, 0, 1 ] }, 147 | { t: [ 0, - 1, 0 ], u: [ 0, 0, - 1 ] }, 148 | { t: [ 0, 0, 1 ], u: [ 0, - 1, 0 ] }, 149 | { t: [ 0, 0, - 1 ], u: [ 0, - 1, 0 ] }, 150 | ]; 151 | 152 | var params = { 153 | format: options.format || this.sourceTexture.format, 154 | magFilter: this.sourceTexture.magFilter, 155 | minFilter: this.sourceTexture.minFilter, 156 | type: options.type || this.sourceTexture.type, 157 | generateMipmaps: this.sourceTexture.generateMipmaps, 158 | anisotropy: this.sourceTexture.anisotropy, 159 | encoding: this.sourceTexture.encoding 160 | }; 161 | 162 | this.renderTarget = new WebGLRenderTargetCube( this.resolution, this.resolution, params ); 163 | 164 | }; 165 | 166 | EquirectangularToCubeGenerator.prototype = { 167 | 168 | constructor: EquirectangularToCubeGenerator, 169 | 170 | update: function ( renderer ) { 171 | 172 | var currentRenderTarget = renderer.getRenderTarget(); 173 | 174 | boxMesh.material.uniforms.equirectangularMap.value = this.sourceTexture; 175 | 176 | for ( var i = 0; i < 6; i ++ ) { 177 | 178 | var v = this.views[ i ]; 179 | 180 | camera.position.set( 0, 0, 0 ); 181 | camera.up.set( v.u[ 0 ], v.u[ 1 ], v.u[ 2 ] ); 182 | camera.lookAt( v.t[ 0 ], v.t[ 1 ], v.t[ 2 ] ); 183 | 184 | renderer.setRenderTarget( this.renderTarget, i ); 185 | renderer.clear(); 186 | renderer.render( scene, camera ); 187 | 188 | } 189 | 190 | renderer.setRenderTarget( currentRenderTarget ); 191 | 192 | return this.renderTarget.texture; 193 | 194 | }, 195 | 196 | dispose: function () { 197 | 198 | this.renderTarget.dispose(); 199 | 200 | } 201 | 202 | }; 203 | 204 | function getShader() { 205 | 206 | var shaderMaterial = new ShaderMaterial( { 207 | 208 | uniforms: { 209 | "equirectangularMap": { value: null }, 210 | }, 211 | 212 | vertexShader: 213 | "varying vec3 localPosition;\n\ 214 | \n\ 215 | void main() {\n\ 216 | localPosition = position;\n\ 217 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\ 218 | }", 219 | 220 | fragmentShader: 221 | "#include \n\ 222 | varying vec3 localPosition;\n\ 223 | uniform sampler2D equirectangularMap;\n\ 224 | \n\ 225 | vec2 EquirectangularSampleUV(vec3 v) {\n\ 226 | vec2 uv = vec2(atan(v.z, v.x), asin(v.y));\n\ 227 | uv *= vec2(0.1591, 0.3183); // inverse atan\n\ 228 | uv += 0.5;\n\ 229 | return uv;\n\ 230 | }\n\ 231 | \n\ 232 | void main() {\n\ 233 | vec2 uv = EquirectangularSampleUV(normalize(localPosition));\n\ 234 | gl_FragColor = texture2D(equirectangularMap, uv);\n\ 235 | }", 236 | 237 | blending: NoBlending 238 | 239 | } ); 240 | 241 | shaderMaterial.type = 'EquirectangularToCubeGenerator'; 242 | 243 | return shaderMaterial; 244 | 245 | } 246 | 247 | return EquirectangularToCubeGenerator; 248 | 249 | } )(); 250 | 251 | return { CubemapGenerator, EquirectangularToCubeGenerator }; 252 | } 253 | -------------------------------------------------------------------------------- /jsm/loaders/DDSLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | export default function (THREE) { 5 | let { 6 | CompressedTextureLoader, 7 | RGBAFormat, 8 | RGBA_S3TC_DXT3_Format, 9 | RGBA_S3TC_DXT5_Format, 10 | RGB_ETC1_Format, 11 | RGB_S3TC_DXT1_Format 12 | } = THREE; 13 | 14 | var DDSLoader = function (manager) { 15 | 16 | CompressedTextureLoader.call(this, manager); 17 | 18 | }; 19 | 20 | DDSLoader.prototype = Object.assign(Object.create(CompressedTextureLoader.prototype), { 21 | 22 | constructor: DDSLoader, 23 | 24 | parse: function (buffer, loadMipmaps) { 25 | 26 | var dds = { mipmaps: [], width: 0, height: 0, format: null, mipmapCount: 1 }; 27 | 28 | // Adapted from @toji's DDS utils 29 | // https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js 30 | 31 | // All values and structures referenced from: 32 | // http://msdn.microsoft.com/en-us/library/bb943991.aspx/ 33 | 34 | var DDS_MAGIC = 0x20534444; 35 | 36 | var DDSD_CAPS = 0x1, 37 | DDSD_HEIGHT = 0x2, 38 | DDSD_WIDTH = 0x4, 39 | DDSD_PITCH = 0x8, 40 | DDSD_PIXELFORMAT = 0x1000, 41 | DDSD_MIPMAPCOUNT = 0x20000, 42 | DDSD_LINEARSIZE = 0x80000, 43 | DDSD_DEPTH = 0x800000; 44 | 45 | var DDSCAPS_COMPLEX = 0x8, 46 | DDSCAPS_MIPMAP = 0x400000, 47 | DDSCAPS_TEXTURE = 0x1000; 48 | 49 | var DDSCAPS2_CUBEMAP = 0x200, 50 | DDSCAPS2_CUBEMAP_POSITIVEX = 0x400, 51 | DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800, 52 | DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000, 53 | DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000, 54 | DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000, 55 | DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000, 56 | DDSCAPS2_VOLUME = 0x200000; 57 | 58 | var DDPF_ALPHAPIXELS = 0x1, 59 | DDPF_ALPHA = 0x2, 60 | DDPF_FOURCC = 0x4, 61 | DDPF_RGB = 0x40, 62 | DDPF_YUV = 0x200, 63 | DDPF_LUMINANCE = 0x20000; 64 | 65 | function fourCCToInt32(value) { 66 | 67 | return value.charCodeAt(0) + 68 | (value.charCodeAt(1) << 8) + 69 | (value.charCodeAt(2) << 16) + 70 | (value.charCodeAt(3) << 24); 71 | 72 | } 73 | 74 | function int32ToFourCC(value) { 75 | 76 | return String.fromCharCode( 77 | value & 0xff, 78 | (value >> 8) & 0xff, 79 | (value >> 16) & 0xff, 80 | (value >> 24) & 0xff 81 | ); 82 | 83 | } 84 | 85 | function loadARGBMip(buffer, dataOffset, width, height) { 86 | 87 | var dataLength = width * height * 4; 88 | var srcBuffer = new Uint8Array(buffer, dataOffset, dataLength); 89 | var byteArray = new Uint8Array(dataLength); 90 | var dst = 0; 91 | var src = 0; 92 | for (var y = 0; y < height; y++) { 93 | 94 | for (var x = 0; x < width; x++) { 95 | 96 | var b = srcBuffer[src]; src++; 97 | var g = srcBuffer[src]; src++; 98 | var r = srcBuffer[src]; src++; 99 | var a = srcBuffer[src]; src++; 100 | byteArray[dst] = r; dst++; //r 101 | byteArray[dst] = g; dst++; //g 102 | byteArray[dst] = b; dst++; //b 103 | byteArray[dst] = a; dst++; //a 104 | 105 | } 106 | 107 | } 108 | return byteArray; 109 | 110 | } 111 | 112 | var FOURCC_DXT1 = fourCCToInt32("DXT1"); 113 | var FOURCC_DXT3 = fourCCToInt32("DXT3"); 114 | var FOURCC_DXT5 = fourCCToInt32("DXT5"); 115 | var FOURCC_ETC1 = fourCCToInt32("ETC1"); 116 | 117 | var headerLengthInt = 31; // The header length in 32 bit ints 118 | 119 | // Offsets into the header array 120 | 121 | var off_magic = 0; 122 | 123 | var off_size = 1; 124 | var off_flags = 2; 125 | var off_height = 3; 126 | var off_width = 4; 127 | 128 | var off_mipmapCount = 7; 129 | 130 | var off_pfFlags = 20; 131 | var off_pfFourCC = 21; 132 | var off_RGBBitCount = 22; 133 | var off_RBitMask = 23; 134 | var off_GBitMask = 24; 135 | var off_BBitMask = 25; 136 | var off_ABitMask = 26; 137 | 138 | var off_caps = 27; 139 | var off_caps2 = 28; 140 | var off_caps3 = 29; 141 | var off_caps4 = 30; 142 | 143 | // Parse header 144 | 145 | var header = new Int32Array(buffer, 0, headerLengthInt); 146 | 147 | if (header[off_magic] !== DDS_MAGIC) { 148 | 149 | console.error('THREE.DDSLoader.parse: Invalid magic number in DDS header.'); 150 | return dds; 151 | 152 | } 153 | 154 | if (!header[off_pfFlags] & DDPF_FOURCC) { 155 | 156 | console.error('THREE.DDSLoader.parse: Unsupported format, must contain a FourCC code.'); 157 | return dds; 158 | 159 | } 160 | 161 | var blockBytes; 162 | 163 | var fourCC = header[off_pfFourCC]; 164 | 165 | var isRGBAUncompressed = false; 166 | 167 | switch (fourCC) { 168 | 169 | case FOURCC_DXT1: 170 | 171 | blockBytes = 8; 172 | dds.format = RGB_S3TC_DXT1_Format; 173 | break; 174 | 175 | case FOURCC_DXT3: 176 | 177 | blockBytes = 16; 178 | dds.format = RGBA_S3TC_DXT3_Format; 179 | break; 180 | 181 | case FOURCC_DXT5: 182 | 183 | blockBytes = 16; 184 | dds.format = RGBA_S3TC_DXT5_Format; 185 | break; 186 | 187 | case FOURCC_ETC1: 188 | 189 | blockBytes = 8; 190 | dds.format = RGB_ETC1_Format; 191 | break; 192 | 193 | default: 194 | 195 | if (header[off_RGBBitCount] === 32 196 | && header[off_RBitMask] & 0xff0000 197 | && header[off_GBitMask] & 0xff00 198 | && header[off_BBitMask] & 0xff 199 | && header[off_ABitMask] & 0xff000000) { 200 | 201 | isRGBAUncompressed = true; 202 | blockBytes = 64; 203 | dds.format = RGBAFormat; 204 | 205 | } else { 206 | 207 | console.error('THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC(fourCC)); 208 | return dds; 209 | 210 | } 211 | 212 | } 213 | 214 | dds.mipmapCount = 1; 215 | 216 | if (header[off_flags] & DDSD_MIPMAPCOUNT && loadMipmaps !== false) { 217 | 218 | dds.mipmapCount = Math.max(1, header[off_mipmapCount]); 219 | 220 | } 221 | 222 | var caps2 = header[off_caps2]; 223 | dds.isCubemap = caps2 & DDSCAPS2_CUBEMAP ? true : false; 224 | if (dds.isCubemap && ( 225 | !(caps2 & DDSCAPS2_CUBEMAP_POSITIVEX) || 226 | !(caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX) || 227 | !(caps2 & DDSCAPS2_CUBEMAP_POSITIVEY) || 228 | !(caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY) || 229 | !(caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ) || 230 | !(caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ) 231 | )) { 232 | 233 | console.error('THREE.DDSLoader.parse: Incomplete cubemap faces'); 234 | return dds; 235 | 236 | } 237 | 238 | dds.width = header[off_width]; 239 | dds.height = header[off_height]; 240 | 241 | var dataOffset = header[off_size] + 4; 242 | 243 | // Extract mipmaps buffers 244 | 245 | var faces = dds.isCubemap ? 6 : 1; 246 | 247 | for (var face = 0; face < faces; face++) { 248 | 249 | var width = dds.width; 250 | var height = dds.height; 251 | 252 | for (var i = 0; i < dds.mipmapCount; i++) { 253 | 254 | if (isRGBAUncompressed) { 255 | 256 | var byteArray = loadARGBMip(buffer, dataOffset, width, height); 257 | var dataLength = byteArray.length; 258 | 259 | } else { 260 | 261 | var dataLength = Math.max(4, width) / 4 * Math.max(4, height) / 4 * blockBytes; 262 | var byteArray = new Uint8Array(buffer, dataOffset, dataLength); 263 | 264 | } 265 | 266 | var mipmap = { "data": byteArray, "width": width, "height": height }; 267 | dds.mipmaps.push(mipmap); 268 | 269 | dataOffset += dataLength; 270 | 271 | width = Math.max(width >> 1, 1); 272 | height = Math.max(height >> 1, 1); 273 | 274 | } 275 | 276 | } 277 | 278 | return dds; 279 | 280 | } 281 | 282 | }); 283 | 284 | return { DDSLoader }; 285 | } -------------------------------------------------------------------------------- /jsm/pmrem/PMREMCubeUVPacker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Prashant Sharma / spidersharma03 3 | * @author Ben Houston / bhouston, https://clara.io 4 | * 5 | * This class takes the cube lods(corresponding to different roughness values), and creates a single cubeUV 6 | * Texture. The format for a given roughness set of faces is simply:: 7 | * +X+Y+Z 8 | * -X-Y-Z 9 | * For every roughness a mip map chain is also saved, which is essential to remove the texture artifacts due to 10 | * minification. 11 | * Right now for every face a PlaneMesh is drawn, which leads to a lot of geometry draw calls, but can be replaced 12 | * later by drawing a single buffer and by sending the appropriate faceIndex via vertex attributes. 13 | * The arrangement of the faces is fixed, as assuming this arrangement, the sampling function has been written. 14 | */ 15 | export default function (THREE) { 16 | let { 17 | BackSide, 18 | CubeUVReflectionMapping, 19 | LinearFilter, 20 | LinearToneMapping, 21 | Mesh, 22 | NoBlending, 23 | OrthographicCamera, 24 | PlaneBufferGeometry, 25 | RGBEEncoding, 26 | RGBM16Encoding, 27 | Scene, 28 | ShaderMaterial, 29 | Vector2, 30 | Vector3, 31 | WebGLRenderTarget 32 | } = THREE; 33 | 34 | var PMREMCubeUVPacker = ( function () { 35 | 36 | var camera = new OrthographicCamera(); 37 | var scene = new Scene(); 38 | var shader = getShader(); 39 | 40 | var PMREMCubeUVPacker = function ( cubeTextureLods ) { 41 | 42 | this.cubeLods = cubeTextureLods; 43 | var size = cubeTextureLods[ 0 ].width * 4; 44 | 45 | var sourceTexture = cubeTextureLods[ 0 ].texture; 46 | var params = { 47 | format: sourceTexture.format, 48 | magFilter: sourceTexture.magFilter, 49 | minFilter: sourceTexture.minFilter, 50 | type: sourceTexture.type, 51 | generateMipmaps: sourceTexture.generateMipmaps, 52 | anisotropy: sourceTexture.anisotropy, 53 | encoding: ( sourceTexture.encoding === RGBEEncoding ) ? RGBM16Encoding : sourceTexture.encoding 54 | }; 55 | 56 | if ( params.encoding === RGBM16Encoding ) { 57 | 58 | params.magFilter = LinearFilter; 59 | params.minFilter = LinearFilter; 60 | 61 | } 62 | 63 | this.CubeUVRenderTarget = new WebGLRenderTarget( size, size, params ); 64 | this.CubeUVRenderTarget.texture.name = "PMREMCubeUVPacker.cubeUv"; 65 | this.CubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping; 66 | 67 | this.objects = []; 68 | 69 | var geometry = new PlaneBufferGeometry( 1, 1 ); 70 | 71 | var faceOffsets = []; 72 | faceOffsets.push( new Vector2( 0, 0 ) ); 73 | faceOffsets.push( new Vector2( 1, 0 ) ); 74 | faceOffsets.push( new Vector2( 2, 0 ) ); 75 | faceOffsets.push( new Vector2( 0, 1 ) ); 76 | faceOffsets.push( new Vector2( 1, 1 ) ); 77 | faceOffsets.push( new Vector2( 2, 1 ) ); 78 | 79 | var textureResolution = size; 80 | size = cubeTextureLods[ 0 ].width; 81 | 82 | var offset2 = 0; 83 | var c = 4.0; 84 | this.numLods = Math.log( cubeTextureLods[ 0 ].width ) / Math.log( 2 ) - 2; // IE11 doesn't support Math.log2 85 | for ( var i = 0; i < this.numLods; i ++ ) { 86 | 87 | var offset1 = ( textureResolution - textureResolution / c ) * 0.5; 88 | if ( size > 16 ) c *= 2; 89 | var nMips = size > 16 ? 6 : 1; 90 | var mipOffsetX = 0; 91 | var mipOffsetY = 0; 92 | var mipSize = size; 93 | 94 | for ( var j = 0; j < nMips; j ++ ) { 95 | 96 | // Mip Maps 97 | for ( var k = 0; k < 6; k ++ ) { 98 | 99 | // 6 Cube Faces 100 | var material = shader.clone(); 101 | material.uniforms[ 'envMap' ].value = this.cubeLods[ i ].texture; 102 | material.envMap = this.cubeLods[ i ].texture; 103 | material.uniforms[ 'faceIndex' ].value = k; 104 | material.uniforms[ 'mapSize' ].value = mipSize; 105 | 106 | var planeMesh = new Mesh( geometry, material ); 107 | planeMesh.position.x = faceOffsets[ k ].x * mipSize - offset1 + mipOffsetX; 108 | planeMesh.position.y = faceOffsets[ k ].y * mipSize - offset1 + offset2 + mipOffsetY; 109 | planeMesh.material.side = BackSide; 110 | planeMesh.scale.setScalar( mipSize ); 111 | this.objects.push( planeMesh ); 112 | 113 | } 114 | mipOffsetY += 1.75 * mipSize; 115 | mipOffsetX += 1.25 * mipSize; 116 | mipSize /= 2; 117 | 118 | } 119 | offset2 += 2 * size; 120 | if ( size > 16 ) size /= 2; 121 | 122 | } 123 | 124 | }; 125 | 126 | PMREMCubeUVPacker.prototype = { 127 | 128 | constructor: PMREMCubeUVPacker, 129 | 130 | update: function ( renderer ) { 131 | 132 | var size = this.cubeLods[ 0 ].width * 4; 133 | // top and bottom are swapped for some reason? 134 | camera.left = - size * 0.5; 135 | camera.right = size * 0.5; 136 | camera.top = - size * 0.5; 137 | camera.bottom = size * 0.5; 138 | camera.near = 0; 139 | camera.far = 1; 140 | camera.updateProjectionMatrix(); 141 | 142 | for ( var i = 0; i < this.objects.length; i ++ ) { 143 | 144 | scene.add( this.objects[ i ] ); 145 | 146 | } 147 | 148 | var gammaInput = renderer.gammaInput; 149 | var gammaOutput = renderer.gammaOutput; 150 | var toneMapping = renderer.toneMapping; 151 | var toneMappingExposure = renderer.toneMappingExposure; 152 | var currentRenderTarget = renderer.getRenderTarget(); 153 | 154 | renderer.gammaInput = false; 155 | renderer.gammaOutput = false; 156 | renderer.toneMapping = LinearToneMapping; 157 | renderer.toneMappingExposure = 1.0; 158 | renderer.setRenderTarget( this.CubeUVRenderTarget ); 159 | renderer.render( scene, camera ); 160 | 161 | renderer.setRenderTarget( currentRenderTarget ); 162 | renderer.toneMapping = toneMapping; 163 | renderer.toneMappingExposure = toneMappingExposure; 164 | renderer.gammaInput = gammaInput; 165 | renderer.gammaOutput = gammaOutput; 166 | 167 | for ( var i = 0; i < this.objects.length; i ++ ) { 168 | 169 | scene.remove( this.objects[ i ] ); 170 | 171 | } 172 | 173 | }, 174 | 175 | dispose: function () { 176 | 177 | for ( var i = 0, l = this.objects.length; i < l; i ++ ) { 178 | 179 | this.objects[ i ].material.dispose(); 180 | 181 | } 182 | 183 | this.objects[ 0 ].geometry.dispose(); 184 | 185 | } 186 | 187 | }; 188 | 189 | function getShader() { 190 | 191 | var shaderMaterial = new ShaderMaterial( { 192 | 193 | uniforms: { 194 | "faceIndex": { value: 0 }, 195 | "mapSize": { value: 0 }, 196 | "envMap": { value: null }, 197 | "testColor": { value: new Vector3( 1, 1, 1 ) } 198 | }, 199 | 200 | vertexShader: 201 | "precision highp float;\ 202 | varying vec2 vUv;\ 203 | void main() {\ 204 | vUv = uv;\ 205 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\ 206 | }", 207 | 208 | fragmentShader: 209 | "precision highp float;\ 210 | varying vec2 vUv;\ 211 | uniform samplerCube envMap;\ 212 | uniform float mapSize;\ 213 | uniform vec3 testColor;\ 214 | uniform int faceIndex;\ 215 | \ 216 | void main() {\ 217 | vec3 sampleDirection;\ 218 | vec2 uv = vUv;\ 219 | uv = uv * 2.0 - 1.0;\ 220 | uv.y *= -1.0;\ 221 | if(faceIndex == 0) {\ 222 | sampleDirection = normalize(vec3(1.0, uv.y, -uv.x));\ 223 | } else if(faceIndex == 1) {\ 224 | sampleDirection = normalize(vec3(uv.x, 1.0, uv.y));\ 225 | } else if(faceIndex == 2) {\ 226 | sampleDirection = normalize(vec3(uv.x, uv.y, 1.0));\ 227 | } else if(faceIndex == 3) {\ 228 | sampleDirection = normalize(vec3(-1.0, uv.y, uv.x));\ 229 | } else if(faceIndex == 4) {\ 230 | sampleDirection = normalize(vec3(uv.x, -1.0, -uv.y));\ 231 | } else {\ 232 | sampleDirection = normalize(vec3(-uv.x, uv.y, -1.0));\ 233 | }\ 234 | vec4 color = envMapTexelToLinear( textureCube( envMap, sampleDirection ) );\ 235 | gl_FragColor = linearToOutputTexel( color );\ 236 | }", 237 | 238 | blending: NoBlending 239 | 240 | } ); 241 | 242 | shaderMaterial.type = 'PMREMCubeUVPacker'; 243 | 244 | return shaderMaterial; 245 | 246 | } 247 | 248 | 249 | return PMREMCubeUVPacker; 250 | 251 | } )(); 252 | 253 | return { PMREMCubeUVPacker }; 254 | } -------------------------------------------------------------------------------- /jsm/pmrem/PMREMGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Prashant Sharma / spidersharma03 3 | * @author Ben Houston / bhouston, https://clara.io 4 | * 5 | * To avoid cube map seams, I create an extra pixel around each face. This way when the cube map is 6 | * sampled by an application later(with a little care by sampling the centre of the texel), the extra 1 border 7 | * of pixels makes sure that there is no seams artifacts present. This works perfectly for cubeUV format as 8 | * well where the 6 faces can be arranged in any manner whatsoever. 9 | * Code in the beginning of fragment shader's main function does this job for a given resolution. 10 | * Run Scene_PMREM_Test.html in the examples directory to see the sampling from the cube lods generated 11 | * by this class. 12 | */ 13 | export default function (THREE) { 14 | let { 15 | DoubleSide, 16 | GammaEncoding, 17 | LinearEncoding, 18 | LinearFilter, 19 | LinearToneMapping, 20 | Mesh, 21 | NearestFilter, 22 | NoBlending, 23 | OrthographicCamera, 24 | PlaneBufferGeometry, 25 | Scene, 26 | ShaderMaterial, 27 | WebGLRenderTargetCube, 28 | sRGBEncoding 29 | } = THREE; 30 | 31 | var PMREMGenerator = ( function () { 32 | 33 | var shader = getShader(); 34 | var camera = new OrthographicCamera( - 1, 1, 1, - 1, 0.0, 1000 ); 35 | var scene = new Scene(); 36 | var planeMesh = new Mesh( new PlaneBufferGeometry( 2, 2, 0 ), shader ); 37 | planeMesh.material.side = DoubleSide; 38 | scene.add( planeMesh ); 39 | scene.add( camera ); 40 | 41 | var PMREMGenerator = function ( sourceTexture, samplesPerLevel, resolution ) { 42 | 43 | this.sourceTexture = sourceTexture; 44 | this.resolution = ( resolution !== undefined ) ? resolution : 256; // NODE: 256 is currently hard coded in the glsl code for performance reasons 45 | this.samplesPerLevel = ( samplesPerLevel !== undefined ) ? samplesPerLevel : 32; 46 | 47 | var monotonicEncoding = ( this.sourceTexture.encoding === LinearEncoding ) || 48 | ( this.sourceTexture.encoding === GammaEncoding ) || ( this.sourceTexture.encoding === sRGBEncoding ); 49 | 50 | this.sourceTexture.minFilter = ( monotonicEncoding ) ? LinearFilter : NearestFilter; 51 | this.sourceTexture.magFilter = ( monotonicEncoding ) ? LinearFilter : NearestFilter; 52 | this.sourceTexture.generateMipmaps = this.sourceTexture.generateMipmaps && monotonicEncoding; 53 | 54 | this.cubeLods = []; 55 | 56 | var size = this.resolution; 57 | var params = { 58 | format: this.sourceTexture.format, 59 | magFilter: this.sourceTexture.magFilter, 60 | minFilter: this.sourceTexture.minFilter, 61 | type: this.sourceTexture.type, 62 | generateMipmaps: this.sourceTexture.generateMipmaps, 63 | anisotropy: this.sourceTexture.anisotropy, 64 | encoding: this.sourceTexture.encoding 65 | }; 66 | 67 | // how many LODs fit in the given CubeUV Texture. 68 | this.numLods = Math.log( size ) / Math.log( 2 ) - 2; // IE11 doesn't support Math.log2 69 | 70 | for ( var i = 0; i < this.numLods; i ++ ) { 71 | 72 | var renderTarget = new WebGLRenderTargetCube( size, size, params ); 73 | renderTarget.texture.name = "PMREMGenerator.cube" + i; 74 | this.cubeLods.push( renderTarget ); 75 | size = Math.max( 16, size / 2 ); 76 | 77 | } 78 | 79 | }; 80 | 81 | PMREMGenerator.prototype = { 82 | 83 | constructor: PMREMGenerator, 84 | 85 | /* 86 | * Prashant Sharma / spidersharma03: More thought and work is needed here. 87 | * Right now it's a kind of a hack to use the previously convolved map to convolve the current one. 88 | * I tried to use the original map to convolve all the lods, but for many textures(specially the high frequency) 89 | * even a high number of samples(1024) dosen't lead to satisfactory results. 90 | * By using the previous convolved maps, a lower number of samples are generally sufficient(right now 32, which 91 | * gives okay results unless we see the reflection very carefully, or zoom in too much), however the math 92 | * goes wrong as the distribution function tries to sample a larger area than what it should be. So I simply scaled 93 | * the roughness by 0.9(totally empirical) to try to visually match the original result. 94 | * The condition "if(i <5)" is also an attemt to make the result match the original result. 95 | * This method requires the most amount of thinking I guess. Here is a paper which we could try to implement in future:: 96 | * https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch20.html 97 | */ 98 | update: function ( renderer ) { 99 | 100 | // Texture should only be flipped for CubeTexture, not for 101 | // a Texture created via WebGLRenderTargetCube. 102 | var tFlip = ( this.sourceTexture.isCubeTexture ) ? - 1 : 1; 103 | 104 | shader.defines[ 'SAMPLES_PER_LEVEL' ] = this.samplesPerLevel; 105 | shader.uniforms[ 'faceIndex' ].value = 0; 106 | shader.uniforms[ 'envMap' ].value = this.sourceTexture; 107 | shader.envMap = this.sourceTexture; 108 | shader.needsUpdate = true; 109 | 110 | var gammaInput = renderer.gammaInput; 111 | var gammaOutput = renderer.gammaOutput; 112 | var toneMapping = renderer.toneMapping; 113 | var toneMappingExposure = renderer.toneMappingExposure; 114 | var currentRenderTarget = renderer.getRenderTarget(); 115 | 116 | renderer.toneMapping = LinearToneMapping; 117 | renderer.toneMappingExposure = 1.0; 118 | renderer.gammaInput = false; 119 | renderer.gammaOutput = false; 120 | 121 | for ( var i = 0; i < this.numLods; i ++ ) { 122 | 123 | var r = i / ( this.numLods - 1 ); 124 | shader.uniforms[ 'roughness' ].value = r * 0.9; // see comment above, pragmatic choice 125 | // Only apply the tFlip for the first LOD 126 | shader.uniforms[ 'tFlip' ].value = ( i == 0 ) ? tFlip : 1; 127 | var size = this.cubeLods[ i ].width; 128 | shader.uniforms[ 'mapSize' ].value = size; 129 | this.renderToCubeMapTarget( renderer, this.cubeLods[ i ] ); 130 | 131 | if ( i < 5 ) shader.uniforms[ 'envMap' ].value = this.cubeLods[ i ].texture; 132 | 133 | } 134 | 135 | renderer.setRenderTarget( currentRenderTarget ); 136 | renderer.toneMapping = toneMapping; 137 | renderer.toneMappingExposure = toneMappingExposure; 138 | renderer.gammaInput = gammaInput; 139 | renderer.gammaOutput = gammaOutput; 140 | 141 | }, 142 | 143 | renderToCubeMapTarget: function ( renderer, renderTarget ) { 144 | 145 | for ( var i = 0; i < 6; i ++ ) { 146 | 147 | this.renderToCubeMapTargetFace( renderer, renderTarget, i ); 148 | 149 | } 150 | 151 | }, 152 | 153 | renderToCubeMapTargetFace: function ( renderer, renderTarget, faceIndex ) { 154 | 155 | shader.uniforms[ 'faceIndex' ].value = faceIndex; 156 | renderer.setRenderTarget( renderTarget, faceIndex ); 157 | renderer.clear(); 158 | renderer.render( scene, camera ); 159 | 160 | }, 161 | 162 | dispose: function () { 163 | 164 | for ( var i = 0, l = this.cubeLods.length; i < l; i ++ ) { 165 | 166 | this.cubeLods[ i ].dispose(); 167 | 168 | } 169 | 170 | }, 171 | 172 | }; 173 | 174 | function getShader() { 175 | 176 | var shaderMaterial = new ShaderMaterial( { 177 | 178 | defines: { 179 | "SAMPLES_PER_LEVEL": 20, 180 | }, 181 | 182 | uniforms: { 183 | "faceIndex": { value: 0 }, 184 | "roughness": { value: 0.5 }, 185 | "mapSize": { value: 0.5 }, 186 | "envMap": { value: null }, 187 | "tFlip": { value: - 1 }, 188 | }, 189 | 190 | vertexShader: 191 | "varying vec2 vUv;\n\ 192 | void main() {\n\ 193 | vUv = uv;\n\ 194 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\ 195 | }", 196 | 197 | fragmentShader: 198 | "#include \n\ 199 | varying vec2 vUv;\n\ 200 | uniform int faceIndex;\n\ 201 | uniform float roughness;\n\ 202 | uniform samplerCube envMap;\n\ 203 | uniform float mapSize;\n\ 204 | uniform float tFlip;\n\ 205 | \n\ 206 | float GGXRoughnessToBlinnExponent( const in float ggxRoughness ) {\n\ 207 | float a = ggxRoughness + 0.0001;\n\ 208 | a *= a;\n\ 209 | return ( 2.0 / a - 2.0 );\n\ 210 | }\n\ 211 | vec3 ImportanceSamplePhong(vec2 uv, mat3 vecSpace, float specPow) {\n\ 212 | float phi = uv.y * 2.0 * PI;\n\ 213 | float cosTheta = pow(1.0 - uv.x, 1.0 / (specPow + 1.0));\n\ 214 | float sinTheta = sqrt(1.0 - cosTheta * cosTheta);\n\ 215 | vec3 sampleDir = vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);\n\ 216 | return vecSpace * sampleDir;\n\ 217 | }\n\ 218 | vec3 ImportanceSampleGGX( vec2 uv, mat3 vecSpace, float Roughness )\n\ 219 | {\n\ 220 | float a = Roughness * Roughness;\n\ 221 | float Phi = 2.0 * PI * uv.x;\n\ 222 | float CosTheta = sqrt( (1.0 - uv.y) / ( 1.0 + (a*a - 1.0) * uv.y ) );\n\ 223 | float SinTheta = sqrt( 1.0 - CosTheta * CosTheta );\n\ 224 | return vecSpace * vec3(SinTheta * cos( Phi ), SinTheta * sin( Phi ), CosTheta);\n\ 225 | }\n\ 226 | mat3 matrixFromVector(vec3 n) {\n\ 227 | float a = 1.0 / (1.0 + n.z);\n\ 228 | float b = -n.x * n.y * a;\n\ 229 | vec3 b1 = vec3(1.0 - n.x * n.x * a, b, -n.x);\n\ 230 | vec3 b2 = vec3(b, 1.0 - n.y * n.y * a, -n.y);\n\ 231 | return mat3(b1, b2, n);\n\ 232 | }\n\ 233 | \n\ 234 | vec4 testColorMap(float Roughness) {\n\ 235 | vec4 color;\n\ 236 | if(faceIndex == 0)\n\ 237 | color = vec4(1.0,0.0,0.0,1.0);\n\ 238 | else if(faceIndex == 1)\n\ 239 | color = vec4(0.0,1.0,0.0,1.0);\n\ 240 | else if(faceIndex == 2)\n\ 241 | color = vec4(0.0,0.0,1.0,1.0);\n\ 242 | else if(faceIndex == 3)\n\ 243 | color = vec4(1.0,1.0,0.0,1.0);\n\ 244 | else if(faceIndex == 4)\n\ 245 | color = vec4(0.0,1.0,1.0,1.0);\n\ 246 | else\n\ 247 | color = vec4(1.0,0.0,1.0,1.0);\n\ 248 | color *= ( 1.0 - Roughness );\n\ 249 | return color;\n\ 250 | }\n\ 251 | void main() {\n\ 252 | vec3 sampleDirection;\n\ 253 | vec2 uv = vUv*2.0 - 1.0;\n\ 254 | float offset = -1.0/mapSize;\n\ 255 | const float a = -1.0;\n\ 256 | const float b = 1.0;\n\ 257 | float c = -1.0 + offset;\n\ 258 | float d = 1.0 - offset;\n\ 259 | float bminusa = b - a;\n\ 260 | uv.x = (uv.x - a)/bminusa * d - (uv.x - b)/bminusa * c;\n\ 261 | uv.y = (uv.y - a)/bminusa * d - (uv.y - b)/bminusa * c;\n\ 262 | if (faceIndex==0) {\n\ 263 | sampleDirection = vec3(1.0, -uv.y, -uv.x);\n\ 264 | } else if (faceIndex==1) {\n\ 265 | sampleDirection = vec3(-1.0, -uv.y, uv.x);\n\ 266 | } else if (faceIndex==2) {\n\ 267 | sampleDirection = vec3(uv.x, 1.0, uv.y);\n\ 268 | } else if (faceIndex==3) {\n\ 269 | sampleDirection = vec3(uv.x, -1.0, -uv.y);\n\ 270 | } else if (faceIndex==4) {\n\ 271 | sampleDirection = vec3(uv.x, -uv.y, 1.0);\n\ 272 | } else {\n\ 273 | sampleDirection = vec3(-uv.x, -uv.y, -1.0);\n\ 274 | }\n\ 275 | vec3 correctedDirection = vec3( tFlip * sampleDirection.x, sampleDirection.yz );\n\ 276 | mat3 vecSpace = matrixFromVector( normalize( correctedDirection ) );\n\ 277 | vec3 rgbColor = vec3(0.0);\n\ 278 | const int NumSamples = SAMPLES_PER_LEVEL;\n\ 279 | vec3 vect;\n\ 280 | float weight = 0.0;\n\ 281 | for( int i = 0; i < NumSamples; i ++ ) {\n\ 282 | float sini = sin(float(i));\n\ 283 | float cosi = cos(float(i));\n\ 284 | float r = rand(vec2(sini, cosi));\n\ 285 | vect = ImportanceSampleGGX(vec2(float(i) / float(NumSamples), r), vecSpace, roughness);\n\ 286 | float dotProd = dot(vect, normalize(sampleDirection));\n\ 287 | weight += dotProd;\n\ 288 | vec3 color = envMapTexelToLinear(textureCube(envMap, vect)).rgb;\n\ 289 | rgbColor.rgb += color;\n\ 290 | }\n\ 291 | rgbColor /= float(NumSamples);\n\ 292 | //rgbColor = testColorMap( roughness ).rgb;\n\ 293 | gl_FragColor = linearToOutputTexel( vec4( rgbColor, 1.0 ) );\n\ 294 | }", 295 | 296 | blending: NoBlending 297 | 298 | } ); 299 | 300 | shaderMaterial.type = 'PMREMGenerator'; 301 | 302 | return shaderMaterial; 303 | 304 | } 305 | 306 | return PMREMGenerator; 307 | 308 | } )(); 309 | 310 | return { PMREMGenerator }; 311 | } -------------------------------------------------------------------------------- /pages/animation/animate.js: -------------------------------------------------------------------------------- 1 | import getSkeletonUtils from '../../jsm/utils/SkeletonUtils.js'; 2 | 3 | import getGLTFLoader from '../../jsm/loaders/GLTFLoader' 4 | import { OrbitControls } from '../../jsm/controls/OrbitControls' 5 | 6 | 7 | export default function (canvas, THREE) { 8 | let GLTFLoader = getGLTFLoader(THREE); 9 | let { SkeletonUtils } = getSkeletonUtils(THREE); 10 | let window = THREE.global; 11 | 12 | 13 | ////////////////////////////// 14 | // Global objects 15 | ////////////////////////////// 16 | var worldScene = null; // THREE.Scene where it all will be rendered 17 | var renderer = null; 18 | var camera = null; 19 | var clock = null; 20 | var mixers = []; // All the THREE.AnimationMixer objects for all the animations in the scene 21 | ////////////////////////////// 22 | 23 | 24 | ////////////////////////////// 25 | // Information about our 3D models and units 26 | ////////////////////////////// 27 | 28 | // The names of the 3D models to load. One-per file. 29 | // A model may have multiple SkinnedMesh objects as well as several rigs (armatures). Units will define which 30 | // meshes, armatures and animations to use. We will load the whole scene for each object and clone it for each unit. 31 | // Models are from https://www.mixamo.com/ 32 | var MODELS = [ 33 | { name: "Soldier" }, 34 | { name: "Parrot" }, 35 | // { name: "RiflePunch" }, 36 | ]; 37 | 38 | // Here we define instances of the models that we want to place in the scene, their position, scale and the animations 39 | // that must be played. 40 | var UNITS = [ 41 | { 42 | modelName: "Soldier", // Will use the 3D model from file models/gltf/Soldier.glb 43 | meshName: "vanguard_Mesh", // Name of the main mesh to animate 44 | position: { x: 0, y: 2, z: 0 }, // Where to put the unit in the scene 45 | scale: 1, // Scaling of the unit. 1.0 means: use original size, 0.1 means "10 times smaller", etc. 46 | animationName: "Idle" // Name of animation to run 47 | }, 48 | { 49 | modelName: "Soldier", 50 | meshName: "vanguard_Mesh", 51 | position: { x: 1, y: -2, z: 0 }, 52 | scale: 2, 53 | animationName: "Walk" 54 | }, 55 | { 56 | modelName: "Soldier", 57 | meshName: "vanguard_Mesh", 58 | position: { x: 1, y: 2, z: 0 }, 59 | scale: 1, 60 | animationName: "Run" 61 | }, 62 | { 63 | modelName: "Parrot", 64 | meshName: "mesh_0", 65 | position: { x: - 2, y: 0, z: 0 }, 66 | rotation: { x: 0, y: Math.PI, z: 0 }, 67 | scale: 0.01, 68 | animationName: "parrot_A_" 69 | }, 70 | { 71 | modelName: "Parrot", 72 | meshName: "mesh_0", 73 | position: { x: - 1, y: 3, z: 0 }, 74 | rotation: { x: 0, y: Math.PI / 2, z: 0 }, 75 | scale: 0.02, 76 | animationName: null 77 | }, 78 | ]; 79 | 80 | ////////////////////////////// 81 | // The main setup happens here 82 | ////////////////////////////// 83 | var numLoadedModels = 0; 84 | initScene(); 85 | initRenderer(); 86 | loadModels(); 87 | animate(); 88 | ////////////////////////////// 89 | 90 | 91 | ////////////////////////////// 92 | // Function implementations 93 | ////////////////////////////// 94 | /** 95 | * Function that starts loading process for the next model in the queue. The loading process is 96 | * asynchronous: it happens "in the background". Therefore we don't load all the models at once. We load one, 97 | * wait until it is done, then load the next one. When all models are loaded, we call loadUnits(). 98 | */ 99 | function loadModels() { 100 | 101 | for (var i = 0; i < MODELS.length; ++i) { 102 | 103 | var m = MODELS[i]; 104 | 105 | loadGltfModel(m, function () { 106 | 107 | ++numLoadedModels; 108 | 109 | if (numLoadedModels === MODELS.length) { 110 | 111 | console.log("All models loaded, time to instantiate units..."); 112 | instantiateUnits(); 113 | 114 | } 115 | 116 | }); 117 | 118 | } 119 | 120 | } 121 | 122 | /** 123 | * Look at UNITS configuration, clone necessary 3D model scenes, place the armatures and meshes in the scene and 124 | * launch necessary animations 125 | */ 126 | function instantiateUnits() { 127 | 128 | var numSuccess = 0; 129 | 130 | for (var i = 0; i < UNITS.length; ++i) { 131 | 132 | var u = UNITS[i]; 133 | var model = getModelByName(u.modelName); 134 | 135 | if (model) { 136 | 137 | var clonedScene = SkeletonUtils.clone(model.scene); 138 | 139 | if (clonedScene) { 140 | 141 | // THREE.Scene is cloned properly, let's find one mesh and launch animation for it 142 | var clonedMesh = clonedScene.getObjectByName(u.meshName); 143 | 144 | if (clonedMesh) { 145 | 146 | var mixer = startAnimation(clonedMesh, model.animations, u.animationName); 147 | 148 | // Save the animation mixer in the list, will need it in the animation loop 149 | mixers.push(mixer); 150 | numSuccess++; 151 | 152 | } 153 | 154 | // Different models can have different configurations of armatures and meshes. Therefore, 155 | // We can't set position, scale or rotation to individual mesh objects. Instead we set 156 | // it to the whole cloned scene and then add the whole scene to the game world 157 | // Note: this may have weird effects if you have lights or other items in the GLTF file's scene! 158 | worldScene.add(clonedScene); 159 | 160 | if (u.position) { 161 | 162 | clonedScene.position.set(u.position.x, u.position.y, u.position.z); 163 | 164 | } 165 | 166 | if (u.scale) { 167 | 168 | clonedScene.scale.set(u.scale, u.scale, u.scale); 169 | 170 | } 171 | 172 | if (u.rotation) { 173 | 174 | clonedScene.rotation.x = u.rotation.x; 175 | clonedScene.rotation.y = u.rotation.y; 176 | clonedScene.rotation.z = u.rotation.z; 177 | 178 | } 179 | 180 | } 181 | 182 | } else { 183 | 184 | console.error("Can not find model", u.modelName); 185 | 186 | } 187 | 188 | } 189 | 190 | console.log(`Successfully instantiated ${numSuccess} units`); 191 | 192 | } 193 | 194 | /** 195 | * Start animation for a specific mesh object. Find the animation by name in the 3D model's animation array 196 | * @param skinnedMesh {THREE.SkinnedMesh} The mesh to animate 197 | * @param animations {Array} Array containing all the animations for this model 198 | * @param animationName {string} Name of the animation to launch 199 | * @return {THREE.AnimationMixer} Mixer to be used in the render loop 200 | */ 201 | function startAnimation(skinnedMesh, animations, animationName) { 202 | 203 | var mixer = new THREE.AnimationMixer(skinnedMesh); 204 | var clip = THREE.AnimationClip.findByName(animations, animationName); 205 | 206 | if (clip) { 207 | 208 | var action = mixer.clipAction(clip); 209 | action.play(); 210 | 211 | } 212 | 213 | return mixer; 214 | 215 | } 216 | 217 | /** 218 | * Find a model object by name 219 | * @param name 220 | * @returns {object|null} 221 | */ 222 | function getModelByName(name) { 223 | 224 | for (var i = 0; i < MODELS.length; ++i) { 225 | 226 | if (MODELS[i].name === name) { 227 | 228 | return MODELS[i]; 229 | 230 | } 231 | 232 | } 233 | 234 | return null; 235 | 236 | } 237 | 238 | /** 239 | * Load a 3D model from a GLTF file. Use the GLTFLoader. 240 | * @param model {object} Model config, one item from the MODELS array. It will be updated inside the function! 241 | * @param onLoaded {function} A callback function that will be called when the model is loaded 242 | */ 243 | function loadGltfModel(model, onLoaded) { 244 | 245 | var loader = new GLTFLoader(); 246 | var modelName = "models/gltf/" + model.name + ".glb"; 247 | 248 | loader.load("https://threejs.org/examples/" + modelName, function (gltf) { 249 | 250 | var scene = gltf.scene; 251 | 252 | model.animations = gltf.animations; 253 | model.scene = scene; 254 | 255 | // Enable Shadows 256 | 257 | gltf.scene.traverse(function (object) { 258 | 259 | if (object.isMesh) { 260 | 261 | object.castShadow = true; 262 | 263 | } 264 | 265 | }); 266 | 267 | console.log("Done loading model", model.name); 268 | 269 | onLoaded(); 270 | 271 | }); 272 | 273 | } 274 | 275 | /** 276 | * Render loop. Renders the next frame of all animations 277 | */ 278 | function animate() { 279 | 280 | canvas.requestAnimationFrame(animate); 281 | 282 | // Get the time elapsed since the last frame 283 | 284 | var mixerUpdateDelta = clock.getDelta(); 285 | 286 | // Update all the animation frames 287 | 288 | for (var i = 0; i < mixers.length; ++i) { 289 | 290 | mixers[i].update(mixerUpdateDelta); 291 | 292 | } 293 | 294 | renderer.render(worldScene, camera); 295 | 296 | } 297 | 298 | ////////////////////////////// 299 | // General Three.JS stuff 300 | ////////////////////////////// 301 | // This part is not anyhow related to the cloning of models, it's just setting up the scene. 302 | 303 | /** 304 | * Initialize ThreeJS scene renderer 305 | */ 306 | function initRenderer() { 307 | 308 | renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); 309 | renderer.setPixelRatio(window.devicePixelRatio); 310 | renderer.setSize(window.innerWidth, window.innerHeight); 311 | renderer.gammaOutput = true; 312 | renderer.gammaFactor = 2.2; 313 | renderer.shadowMap.enabled = true; 314 | renderer.shadowMap.type = THREE.PCFSoftShadowMap; 315 | 316 | } 317 | 318 | /** 319 | * Initialize ThreeJS THREE.Scene 320 | */ 321 | function initScene() { 322 | 323 | camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000); 324 | camera.position.set(3, 6, - 10); 325 | camera.lookAt(0, 1, 0); 326 | 327 | clock = new THREE.Clock(); 328 | 329 | worldScene = new THREE.Scene(); 330 | worldScene.background = new THREE.Color(0xa0a0a0); 331 | worldScene.fog = new THREE.Fog(0xa0a0a0, 10, 22); 332 | 333 | var hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444); 334 | hemiLight.position.set(0, 20, 0); 335 | worldScene.add(hemiLight); 336 | 337 | var dirLight = new THREE.DirectionalLight(0xffffff); 338 | dirLight.position.set(- 3, 10, - 10); 339 | dirLight.castShadow = true; 340 | dirLight.shadow.camera.top = 10; 341 | dirLight.shadow.camera.bottom = - 10; 342 | dirLight.shadow.camera.left = - 10; 343 | dirLight.shadow.camera.right = 10; 344 | dirLight.shadow.camera.near = 0.1; 345 | dirLight.shadow.camera.far = 40; 346 | worldScene.add(dirLight); 347 | 348 | // ground 349 | var groundMesh = new THREE.Mesh( 350 | new THREE.PlaneBufferGeometry(40, 40), 351 | new THREE.MeshPhongMaterial({ 352 | color: 0x999999, 353 | depthWrite: false 354 | }) 355 | ); 356 | 357 | groundMesh.rotation.x = - Math.PI / 2; 358 | groundMesh.receiveShadow = true; 359 | worldScene.add(groundMesh); 360 | window.addEventListener('resize', onWindowResize, false); 361 | 362 | } 363 | 364 | /** 365 | * A callback that will be called whenever the browser window is resized. 366 | */ 367 | function onWindowResize() { 368 | 369 | camera.aspect = window.innerWidth / window.innerHeight; 370 | camera.updateProjectionMatrix(); 371 | renderer.setSize(window.innerWidth, window.innerHeight); 372 | 373 | } 374 | } -------------------------------------------------------------------------------- /jsm/loaders/MTLLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads a Wavefront .mtl file specifying materials 3 | * 4 | * @author angelxuanchang 5 | */ 6 | export default function (THREE) { 7 | let { 8 | Color, 9 | DefaultLoadingManager, 10 | FileLoader, 11 | FrontSide, 12 | Loader, 13 | LoaderUtils, 14 | MeshPhongMaterial, 15 | RepeatWrapping, 16 | TextureLoader, 17 | Vector2 18 | } = THREE; 19 | 20 | var MTLLoader = function (manager) { 21 | 22 | Loader.call(this, manager); 23 | 24 | }; 25 | 26 | MTLLoader.prototype = Object.assign(Object.create(Loader.prototype), { 27 | 28 | constructor: MTLLoader, 29 | 30 | /** 31 | * Loads and parses a MTL asset from a URL. 32 | * 33 | * @param {String} url - URL to the MTL file. 34 | * @param {Function} [onLoad] - Callback invoked with the loaded object. 35 | * @param {Function} [onProgress] - Callback for download progress. 36 | * @param {Function} [onError] - Callback for download errors. 37 | * 38 | * @see setPath setResourcePath 39 | * 40 | * @note In order for relative texture references to resolve correctly 41 | * you must call setResourcePath() explicitly prior to load. 42 | */ 43 | load: function (url, onLoad, onProgress, onError) { 44 | 45 | var scope = this; 46 | 47 | var path = (this.path === '') ? LoaderUtils.extractUrlBase(url) : this.path; 48 | 49 | var loader = new FileLoader(this.manager); 50 | loader.setPath(this.path); 51 | loader.load(url, function (text) { 52 | 53 | onLoad(scope.parse(text, path)); 54 | 55 | }, onProgress, onError); 56 | 57 | }, 58 | 59 | setMaterialOptions: function (value) { 60 | 61 | this.materialOptions = value; 62 | return this; 63 | 64 | }, 65 | 66 | /** 67 | * Parses a MTL file. 68 | * 69 | * @param {String} text - Content of MTL file 70 | * @return {MTLLoader.MaterialCreator} 71 | * 72 | * @see setPath setResourcePath 73 | * 74 | * @note In order for relative texture references to resolve correctly 75 | * you must call setResourcePath() explicitly prior to parse. 76 | */ 77 | parse: function (text, path) { 78 | 79 | var lines = text.split('\n'); 80 | var info = {}; 81 | var delimiter_pattern = /\s+/; 82 | var materialsInfo = {}; 83 | 84 | for (var i = 0; i < lines.length; i++) { 85 | 86 | var line = lines[i]; 87 | line = line.trim(); 88 | 89 | if (line.length === 0 || line.charAt(0) === '#') { 90 | 91 | // Blank line or comment ignore 92 | continue; 93 | 94 | } 95 | 96 | var pos = line.indexOf(' '); 97 | 98 | var key = (pos >= 0) ? line.substring(0, pos) : line; 99 | key = key.toLowerCase(); 100 | 101 | var value = (pos >= 0) ? line.substring(pos + 1) : ''; 102 | value = value.trim(); 103 | 104 | if (key === 'newmtl') { 105 | 106 | // New material 107 | 108 | info = { name: value }; 109 | materialsInfo[value] = info; 110 | 111 | } else { 112 | 113 | if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') { 114 | 115 | var ss = value.split(delimiter_pattern, 3); 116 | info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])]; 117 | 118 | } else { 119 | 120 | info[key] = value; 121 | 122 | } 123 | 124 | } 125 | 126 | } 127 | 128 | var materialCreator = new MTLLoader.MaterialCreator(this.resourcePath || path, this.materialOptions); 129 | materialCreator.setCrossOrigin(this.crossOrigin); 130 | materialCreator.setManager(this.manager); 131 | materialCreator.setMaterials(materialsInfo); 132 | return materialCreator; 133 | 134 | } 135 | 136 | }); 137 | 138 | /** 139 | * Create a new THREE-MTLLoader.MaterialCreator 140 | * @param baseUrl - Url relative to which textures are loaded 141 | * @param options - Set of options on how to construct the materials 142 | * side: Which side to apply the material 143 | * FrontSide (default), THREE.BackSide, THREE.DoubleSide 144 | * wrap: What type of wrapping to apply for textures 145 | * RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping 146 | * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 147 | * Default: false, assumed to be already normalized 148 | * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's 149 | * Default: false 150 | * @constructor 151 | */ 152 | 153 | MTLLoader.MaterialCreator = function (baseUrl, options) { 154 | 155 | this.baseUrl = baseUrl || ''; 156 | this.options = options; 157 | this.materialsInfo = {}; 158 | this.materials = {}; 159 | this.materialsArray = []; 160 | this.nameLookup = {}; 161 | 162 | this.side = (this.options && this.options.side) ? this.options.side : FrontSide; 163 | this.wrap = (this.options && this.options.wrap) ? this.options.wrap : RepeatWrapping; 164 | 165 | }; 166 | 167 | MTLLoader.MaterialCreator.prototype = { 168 | 169 | constructor: MTLLoader.MaterialCreator, 170 | 171 | crossOrigin: 'anonymous', 172 | 173 | setCrossOrigin: function (value) { 174 | 175 | this.crossOrigin = value; 176 | return this; 177 | 178 | }, 179 | 180 | setManager: function (value) { 181 | 182 | this.manager = value; 183 | 184 | }, 185 | 186 | setMaterials: function (materialsInfo) { 187 | 188 | this.materialsInfo = this.convert(materialsInfo); 189 | this.materials = {}; 190 | this.materialsArray = []; 191 | this.nameLookup = {}; 192 | 193 | }, 194 | 195 | convert: function (materialsInfo) { 196 | 197 | if (!this.options) return materialsInfo; 198 | 199 | var converted = {}; 200 | 201 | for (var mn in materialsInfo) { 202 | 203 | // Convert materials info into normalized form based on options 204 | 205 | var mat = materialsInfo[mn]; 206 | 207 | var covmat = {}; 208 | 209 | converted[mn] = covmat; 210 | 211 | for (var prop in mat) { 212 | 213 | var save = true; 214 | var value = mat[prop]; 215 | var lprop = prop.toLowerCase(); 216 | 217 | switch (lprop) { 218 | 219 | case 'kd': 220 | case 'ka': 221 | case 'ks': 222 | 223 | // Diffuse color (color under white light) using RGB values 224 | 225 | if (this.options && this.options.normalizeRGB) { 226 | 227 | value = [value[0] / 255, value[1] / 255, value[2] / 255]; 228 | 229 | } 230 | 231 | if (this.options && this.options.ignoreZeroRGBs) { 232 | 233 | if (value[0] === 0 && value[1] === 0 && value[2] === 0) { 234 | 235 | // ignore 236 | 237 | save = false; 238 | 239 | } 240 | 241 | } 242 | 243 | break; 244 | 245 | default: 246 | 247 | break; 248 | 249 | } 250 | 251 | if (save) { 252 | 253 | covmat[lprop] = value; 254 | 255 | } 256 | 257 | } 258 | 259 | } 260 | 261 | return converted; 262 | 263 | }, 264 | 265 | preload: function () { 266 | 267 | for (var mn in this.materialsInfo) { 268 | 269 | this.create(mn); 270 | 271 | } 272 | 273 | }, 274 | 275 | getIndex: function (materialName) { 276 | 277 | return this.nameLookup[materialName]; 278 | 279 | }, 280 | 281 | getAsArray: function () { 282 | 283 | var index = 0; 284 | 285 | for (var mn in this.materialsInfo) { 286 | 287 | this.materialsArray[index] = this.create(mn); 288 | this.nameLookup[mn] = index; 289 | index++; 290 | 291 | } 292 | 293 | return this.materialsArray; 294 | 295 | }, 296 | 297 | create: function (materialName) { 298 | 299 | if (this.materials[materialName] === undefined) { 300 | 301 | this.createMaterial_(materialName); 302 | 303 | } 304 | 305 | return this.materials[materialName]; 306 | 307 | }, 308 | 309 | createMaterial_: function (materialName) { 310 | 311 | // Create material 312 | 313 | var scope = this; 314 | var mat = this.materialsInfo[materialName]; 315 | var params = { 316 | 317 | name: materialName, 318 | side: this.side 319 | 320 | }; 321 | 322 | function resolveURL(baseUrl, url) { 323 | 324 | if (typeof url !== 'string' || url === '') 325 | return ''; 326 | 327 | // Absolute URL 328 | if (/^https?:\/\//i.test(url)) return url; 329 | 330 | return baseUrl + url; 331 | 332 | } 333 | 334 | function setMapForType(mapType, value) { 335 | 336 | if (params[mapType]) return; // Keep the first encountered texture 337 | 338 | var texParams = scope.getTextureParams(value, params); 339 | var map = scope.loadTexture(resolveURL(scope.baseUrl, texParams.url)); 340 | 341 | map.repeat.copy(texParams.scale); 342 | map.offset.copy(texParams.offset); 343 | 344 | map.wrapS = scope.wrap; 345 | map.wrapT = scope.wrap; 346 | 347 | params[mapType] = map; 348 | 349 | } 350 | 351 | for (var prop in mat) { 352 | 353 | var value = mat[prop]; 354 | var n; 355 | 356 | if (value === '') continue; 357 | 358 | switch (prop.toLowerCase()) { 359 | 360 | // Ns is material specular exponent 361 | 362 | case 'kd': 363 | 364 | // Diffuse color (color under white light) using RGB values 365 | 366 | params.color = new Color().fromArray(value); 367 | 368 | break; 369 | 370 | case 'ks': 371 | 372 | // Specular color (color when light is reflected from shiny surface) using RGB values 373 | params.specular = new Color().fromArray(value); 374 | 375 | break; 376 | 377 | case 'ke': 378 | 379 | // Emissive using RGB values 380 | params.emissive = new Color().fromArray(value); 381 | 382 | break; 383 | 384 | case 'map_kd': 385 | 386 | // Diffuse texture map 387 | 388 | setMapForType("map", value); 389 | 390 | break; 391 | 392 | case 'map_ks': 393 | 394 | // Specular map 395 | 396 | setMapForType("specularMap", value); 397 | 398 | break; 399 | 400 | case 'map_ke': 401 | 402 | // Emissive map 403 | 404 | setMapForType("emissiveMap", value); 405 | 406 | break; 407 | 408 | case 'norm': 409 | 410 | setMapForType("normalMap", value); 411 | 412 | break; 413 | 414 | case 'map_bump': 415 | case 'bump': 416 | 417 | // Bump texture map 418 | 419 | setMapForType("bumpMap", value); 420 | 421 | break; 422 | 423 | case 'map_d': 424 | 425 | // Alpha map 426 | 427 | setMapForType("alphaMap", value); 428 | params.transparent = true; 429 | 430 | break; 431 | 432 | case 'ns': 433 | 434 | // The specular exponent (defines the focus of the specular highlight) 435 | // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. 436 | 437 | params.shininess = parseFloat(value); 438 | 439 | break; 440 | 441 | case 'd': 442 | n = parseFloat(value); 443 | 444 | if (n < 1) { 445 | 446 | params.opacity = n; 447 | params.transparent = true; 448 | 449 | } 450 | 451 | break; 452 | 453 | case 'tr': 454 | n = parseFloat(value); 455 | 456 | if (this.options && this.options.invertTrProperty) n = 1 - n; 457 | 458 | if (n > 0) { 459 | 460 | params.opacity = 1 - n; 461 | params.transparent = true; 462 | 463 | } 464 | 465 | break; 466 | 467 | default: 468 | break; 469 | 470 | } 471 | 472 | } 473 | 474 | this.materials[materialName] = new MeshPhongMaterial(params); 475 | return this.materials[materialName]; 476 | 477 | }, 478 | 479 | getTextureParams: function (value, matParams) { 480 | 481 | var texParams = { 482 | 483 | scale: new Vector2(1, 1), 484 | offset: new Vector2(0, 0) 485 | 486 | }; 487 | 488 | var items = value.split(/\s+/); 489 | var pos; 490 | 491 | pos = items.indexOf('-bm'); 492 | 493 | if (pos >= 0) { 494 | 495 | matParams.bumpScale = parseFloat(items[pos + 1]); 496 | items.splice(pos, 2); 497 | 498 | } 499 | 500 | pos = items.indexOf('-s'); 501 | 502 | if (pos >= 0) { 503 | 504 | texParams.scale.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); 505 | items.splice(pos, 4); // we expect 3 parameters here! 506 | 507 | } 508 | 509 | pos = items.indexOf('-o'); 510 | 511 | if (pos >= 0) { 512 | 513 | texParams.offset.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); 514 | items.splice(pos, 4); // we expect 3 parameters here! 515 | 516 | } 517 | 518 | texParams.url = items.join(' ').trim(); 519 | return texParams; 520 | 521 | }, 522 | 523 | loadTexture: function (url, mapping, onLoad, onProgress, onError) { 524 | 525 | var texture; 526 | var manager = (this.manager !== undefined) ? this.manager : DefaultLoadingManager; 527 | var loader = manager.getHandler(url); 528 | 529 | if (loader === null) { 530 | 531 | loader = new TextureLoader(manager); 532 | 533 | } 534 | 535 | if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin); 536 | texture = loader.load(url, onLoad, onProgress, onError); 537 | 538 | if (mapping !== undefined) texture.mapping = mapping; 539 | 540 | return texture; 541 | 542 | } 543 | 544 | }; 545 | 546 | return { MTLLoader }; 547 | } -------------------------------------------------------------------------------- /pages/gltf/loadgLTF.js: -------------------------------------------------------------------------------- 1 | import gLTF from '../../jsm/loaders/GLTFLoader' 2 | import { OrbitControls } from '../../jsm/controls/OrbitControls' 3 | 4 | export default function (canvas, THREE) { 5 | let GLTFLoader = gLTF(THREE); 6 | 7 | const renderer = new THREE.WebGLRenderer({ canvas }); 8 | renderer.shadowMap.enabled = true; 9 | 10 | const fov = 45; 11 | const aspect = 2; // the canvas default 12 | const near = 0.1; 13 | const far = 100; 14 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 15 | camera.position.set(0, 10, 20); 16 | 17 | const controls = new OrbitControls(camera, canvas); 18 | controls.target.set(0, 5, 0); 19 | controls.update(); 20 | 21 | const scene = new THREE.Scene(); 22 | scene.background = new THREE.Color('#DEFEFF'); 23 | 24 | { 25 | const planeSize = 40; 26 | 27 | const loader = new THREE.TextureLoader(); 28 | const texture = loader.load('https://threejsfundamentals.org/threejs/resources/images/checker.png'); 29 | texture.wrapS = THREE.RepeatWrapping; 30 | texture.wrapT = THREE.RepeatWrapping; 31 | texture.magFilter = THREE.NearestFilter; 32 | const repeats = planeSize / 2; 33 | texture.repeat.set(repeats, repeats); 34 | 35 | const planeGeo = new THREE.PlaneBufferGeometry(planeSize, planeSize); 36 | const planeMat = new THREE.MeshPhongMaterial({ 37 | map: texture, 38 | side: THREE.DoubleSide, 39 | }); 40 | const mesh = new THREE.Mesh(planeGeo, planeMat); 41 | mesh.rotation.x = Math.PI * -.5; 42 | scene.add(mesh); 43 | } 44 | 45 | { 46 | const skyColor = 0xB1E1FF; // light blue 47 | const groundColor = 0xB97A20; // brownish orange 48 | const intensity = 1; 49 | const light = new THREE.HemisphereLight(skyColor, groundColor, intensity); 50 | scene.add(light); 51 | } 52 | 53 | { 54 | const color = 0xFFFFFF; 55 | const intensity = 1; 56 | const light = new THREE.DirectionalLight(color, intensity); 57 | light.castShadow = true; 58 | light.position.set(-250, 800, -850); 59 | light.target.position.set(-550, 40, -450); 60 | 61 | light.shadow.bias = -0.004; 62 | light.shadow.mapSize.width = 2048; 63 | light.shadow.mapSize.height = 2048; 64 | 65 | scene.add(light); 66 | scene.add(light.target); 67 | const cam = light.shadow.camera; 68 | cam.near = 1; 69 | cam.far = 2000; 70 | cam.left = -1500; 71 | cam.right = 1500; 72 | cam.top = 1500; 73 | cam.bottom = -1500; 74 | 75 | const cameraHelper = new THREE.CameraHelper(cam); 76 | scene.add(cameraHelper); 77 | cameraHelper.visible = false; 78 | const helper = new THREE.DirectionalLightHelper(light, 100); 79 | scene.add(helper); 80 | helper.visible = false; 81 | 82 | // function makeXYZGUI(gui, vector3, name, onChangeFn) { 83 | // const folder = gui.addFolder(name); 84 | // folder.add(vector3, 'x', vector3.x - 500, vector3.x + 500).onChange(onChangeFn); 85 | // folder.add(vector3, 'y', vector3.y - 500, vector3.y + 500).onChange(onChangeFn); 86 | // folder.add(vector3, 'z', vector3.z - 500, vector3.z + 500).onChange(onChangeFn); 87 | // folder.open(); 88 | // } 89 | 90 | function updateCamera() { 91 | // update the light target's matrixWorld because it's needed by the helper 92 | light.updateMatrixWorld(); 93 | light.target.updateMatrixWorld(); 94 | helper.update(); 95 | // update the light's shadow camera's projection matrix 96 | light.shadow.camera.updateProjectionMatrix(); 97 | // and now update the camera helper we're using to show the light's shadow camera 98 | cameraHelper.update(); 99 | } 100 | updateCamera(); 101 | 102 | // class DimensionGUIHelper { 103 | // constructor(obj, minProp, maxProp) { 104 | // this.obj = obj; 105 | // this.minProp = minProp; 106 | // this.maxProp = maxProp; 107 | // } 108 | // get value() { 109 | // return this.obj[this.maxProp] * 2; 110 | // } 111 | // set value(v) { 112 | // this.obj[this.maxProp] = v / 2; 113 | // this.obj[this.minProp] = v / -2; 114 | // } 115 | // } 116 | 117 | // class MinMaxGUIHelper { 118 | // constructor(obj, minProp, maxProp, minDif) { 119 | // this.obj = obj; 120 | // this.minProp = minProp; 121 | // this.maxProp = maxProp; 122 | // this.minDif = minDif; 123 | // } 124 | // get min() { 125 | // return this.obj[this.minProp]; 126 | // } 127 | // set min(v) { 128 | // this.obj[this.minProp] = v; 129 | // this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif); 130 | // } 131 | // get max() { 132 | // return this.obj[this.maxProp]; 133 | // } 134 | // set max(v) { 135 | // this.obj[this.maxProp] = v; 136 | // this.min = this.min; // this will call the min setter 137 | // } 138 | // } 139 | 140 | // class VisibleGUIHelper { 141 | // constructor(...objects) { 142 | // this.objects = [...objects]; 143 | // } 144 | // get value() { 145 | // return this.objects[0].visible; 146 | // } 147 | // set value(v) { 148 | // this.objects.forEach((obj) => { 149 | // obj.visible = v; 150 | // }); 151 | // } 152 | // } 153 | 154 | // const gui = new GUI(); 155 | // gui.close(); 156 | // gui.add(new VisibleGUIHelper(helper, cameraHelper), 'value').name('show helpers'); 157 | // gui.add(light.shadow, 'bias', -0.1, 0.1, 0.001); 158 | // { 159 | // const folder = gui.addFolder('Shadow Camera'); 160 | // folder.open(); 161 | // folder.add(new DimensionGUIHelper(light.shadow.camera, 'left', 'right'), 'value', 1, 4000) 162 | // .name('width') 163 | // .onChange(updateCamera); 164 | // folder.add(new DimensionGUIHelper(light.shadow.camera, 'bottom', 'top'), 'value', 1, 4000) 165 | // .name('height') 166 | // .onChange(updateCamera); 167 | // const minMaxGUIHelper = new MinMaxGUIHelper(light.shadow.camera, 'near', 'far', 0.1); 168 | // folder.add(minMaxGUIHelper, 'min', 1, 1000, 1).name('near').onChange(updateCamera); 169 | // folder.add(minMaxGUIHelper, 'max', 1, 4000, 1).name('far').onChange(updateCamera); 170 | // folder.add(light.shadow.camera, 'zoom', 0.01, 1.5, 0.01).onChange(updateCamera); 171 | // } 172 | 173 | // makeXYZGUI(gui, light.position, 'position', updateCamera); 174 | // makeXYZGUI(gui, light.target.position, 'target', updateCamera); 175 | } 176 | 177 | function frameArea(sizeToFitOnScreen, boxSize, boxCenter, camera) { 178 | const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5; 179 | const halfFovY = THREE.Math.degToRad(camera.fov * .5); 180 | const distance = halfSizeToFitOnScreen / Math.tan(halfFovY); 181 | // compute a unit vector that points in the direction the camera is now 182 | // in the xz plane from the center of the box 183 | const direction = (new THREE.Vector3()) 184 | .subVectors(camera.position, boxCenter) 185 | .multiply(new THREE.Vector3(1, 0, 1)) 186 | .normalize(); 187 | 188 | // move the camera to a position distance units way from the center 189 | // in whatever direction the camera was from the center already 190 | camera.position.copy(direction.multiplyScalar(distance).add(boxCenter)); 191 | 192 | // pick some near and far values for the frustum that 193 | // will contain the box. 194 | camera.near = boxSize / 100; 195 | camera.far = boxSize * 100; 196 | 197 | camera.updateProjectionMatrix(); 198 | 199 | // point the camera to look at the center of the box 200 | camera.lookAt(boxCenter.x, boxCenter.y, boxCenter.z); 201 | } 202 | 203 | let curve; 204 | let curveObject; 205 | { 206 | const controlPoints = [ 207 | [1.118281, 5.115846, -3.681386], 208 | [3.948875, 5.115846, -3.641834], 209 | [3.960072, 5.115846, -0.240352], 210 | [3.985447, 5.115846, 4.585005], 211 | [-3.793631, 5.115846, 4.585006], 212 | [-3.826839, 5.115846, -14.736200], 213 | [-14.542292, 5.115846, -14.765865], 214 | [-14.520929, 5.115846, -3.627002], 215 | [-5.452815, 5.115846, -3.634418], 216 | [-5.467251, 5.115846, 4.549161], 217 | [-13.266233, 5.115846, 4.567083], 218 | [-13.250067, 5.115846, -13.499271], 219 | [4.081842, 5.115846, -13.435463], 220 | [4.125436, 5.115846, -5.334928], 221 | [-14.521364, 5.115846, -5.239871], 222 | [-14.510466, 5.115846, 5.486727], 223 | [5.745666, 5.115846, 5.510492], 224 | [5.787942, 5.115846, -14.728308], 225 | [-5.423720, 5.115846, -14.761919], 226 | [-5.373599, 5.115846, -3.704133], 227 | [1.004861, 5.115846, -3.641834], 228 | ]; 229 | const p0 = new THREE.Vector3(); 230 | const p1 = new THREE.Vector3(); 231 | let con = controlPoints.map((p, ndx) => { 232 | p0.set(...p); 233 | p1.set(...controlPoints[(ndx + 1) % controlPoints.length]); 234 | return [ 235 | (new THREE.Vector3()).copy(p0), 236 | (new THREE.Vector3()).lerpVectors(p0, p1, 0.1), 237 | (new THREE.Vector3()).lerpVectors(p0, p1, 0.9), 238 | ]; 239 | }); 240 | curve = new THREE.CatmullRomCurve3( 241 | [].concat(...con), 242 | true, 243 | ); 244 | { 245 | const points = curve.getPoints(250); 246 | const geometry = new THREE.BufferGeometry().setFromPoints(points); 247 | const material = new THREE.LineBasicMaterial({ color: 0xff0000 }); 248 | curveObject = new THREE.Line(geometry, material); 249 | curveObject.scale.set(100, 100, 100); 250 | curveObject.position.y = -621; 251 | curveObject.visible = false; 252 | scene.add(curveObject); 253 | } 254 | } 255 | 256 | const cars = []; 257 | { 258 | const gltfLoader = new GLTFLoader(); 259 | gltfLoader.load('https://threejsfundamentals.org/threejs/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf', (gltf) => { 260 | const root = gltf.scene; 261 | scene.add(root); 262 | 263 | root.traverse((obj) => { 264 | if (obj.castShadow !== undefined) { 265 | obj.castShadow = true; 266 | obj.receiveShadow = true; 267 | } 268 | }); 269 | 270 | const loadedCars = root.getObjectByName('Cars'); 271 | const fixes = [ 272 | { prefix: 'Car_08', y: 0, rot: [Math.PI * .5, 0, Math.PI * .5], }, 273 | { prefix: 'CAR_03', y: 33, rot: [0, Math.PI, 0], }, 274 | { prefix: 'Car_04', y: 40, rot: [0, Math.PI, 0], }, 275 | ]; 276 | 277 | root.updateMatrixWorld(); 278 | for (const car of loadedCars.children.slice()) { 279 | const fix = fixes.find(fix => car.name.startsWith(fix.prefix)); 280 | const obj = new THREE.Object3D(); 281 | car.position.set(0, fix.y, 0); 282 | car.rotation.set(...fix.rot); 283 | obj.add(car); 284 | scene.add(obj); 285 | cars.push(obj); 286 | } 287 | 288 | // compute the box that contains all the stuff 289 | // from root and below 290 | const box = new THREE.Box3().setFromObject(root); 291 | 292 | const boxSize = box.getSize(new THREE.Vector3()).length(); 293 | const boxCenter = box.getCenter(new THREE.Vector3()); 294 | 295 | // set the camera to frame the box 296 | frameArea(boxSize * 0.5, boxSize, boxCenter, camera); 297 | 298 | // update the Trackball controls to handle the new size 299 | controls.maxDistance = boxSize * 10; 300 | controls.target.copy(boxCenter); 301 | controls.update(); 302 | }, (e) => { 303 | console.log('loading') 304 | }); 305 | } 306 | 307 | function resizeRendererToDisplaySize(renderer) { 308 | const canvas = renderer.domElement; 309 | const width = canvas.clientWidth; 310 | const height = canvas.clientHeight; 311 | const needResize = canvas.width !== width || canvas.height !== height; 312 | if (needResize) { 313 | renderer.setSize(width, height, false); 314 | } 315 | return needResize; 316 | } 317 | 318 | // create 2 Vector3s we can use for path calculations 319 | const carPosition = new THREE.Vector3(); 320 | const carTarget = new THREE.Vector3(); 321 | 322 | function render(time) { 323 | time *= 0.001; // convert to seconds 324 | 325 | if (resizeRendererToDisplaySize(renderer)) { 326 | const canvas = renderer.domElement; 327 | camera.aspect = canvas.clientWidth / canvas.clientHeight; 328 | camera.updateProjectionMatrix(); 329 | } 330 | 331 | { 332 | const pathTime = time * .01; 333 | const targetOffset = 0.01; 334 | cars.forEach((car, ndx) => { 335 | // a number between 0 and 1 to evenly space the cars 336 | const u = pathTime + ndx / cars.length; 337 | 338 | // get the first point 339 | curve.getPointAt(u % 1, carPosition); 340 | carPosition.applyMatrix4(curveObject.matrixWorld); 341 | 342 | // get a second point slightly further down the curve 343 | curve.getPointAt((u + targetOffset) % 1, carTarget); 344 | carTarget.applyMatrix4(curveObject.matrixWorld); 345 | 346 | // put the car at the first point (temporarily) 347 | car.position.copy(carPosition); 348 | // point the car the second point 349 | car.lookAt(carTarget); 350 | 351 | // put the car between the 2 points 352 | car.position.lerpVectors(carPosition, carTarget, 0.5); 353 | }); 354 | } 355 | 356 | renderer.render(scene, camera); 357 | 358 | canvas.requestAnimationFrame(render); 359 | } 360 | 361 | canvas.requestAnimationFrame(render); 362 | } -------------------------------------------------------------------------------- /pages/primitives/primitives.js: -------------------------------------------------------------------------------- 1 | const regeneratorRuntime = require('../../utils/runtime') 2 | export default function renderPrimitives(canvas, THREE) { 3 | const renderer = new THREE.WebGLRenderer({ canvas }); 4 | 5 | const fov = 40; 6 | const aspect = 0.5; // the canvas default 7 | const near = 0.1; 8 | const far = 1000; 9 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); 10 | camera.position.z = 150; 11 | 12 | const scene = new THREE.Scene(); 13 | scene.background = new THREE.Color(0xAAAAAA); 14 | 15 | { 16 | const color = 0xFFFFFF; 17 | const intensity = 1; 18 | const light = new THREE.DirectionalLight(color, intensity); 19 | light.position.set(-1, 2, 4); 20 | scene.add(light); 21 | } 22 | { 23 | const color = 0xFFFFFF; 24 | const intensity = 1; 25 | const light = new THREE.DirectionalLight(color, intensity); 26 | light.position.set(1, -2, -4); 27 | scene.add(light); 28 | } 29 | 30 | const objects = []; 31 | const spread = 15; 32 | 33 | function addObject(x, y, obj) { 34 | obj.position.x = x * spread; 35 | obj.position.y = y * spread; 36 | 37 | scene.add(obj); 38 | objects.push(obj); 39 | } 40 | 41 | function createMaterial() { 42 | const material = new THREE.MeshPhongMaterial({ 43 | side: THREE.DoubleSide, 44 | }); 45 | 46 | const hue = Math.random(); 47 | const saturation = 1; 48 | const luminance = .5; 49 | material.color.setHSL(hue, saturation, luminance); 50 | 51 | return material; 52 | } 53 | 54 | function addSolidGeometry(x, y, geometry) { 55 | const mesh = new THREE.Mesh(geometry, createMaterial()); 56 | addObject(x, y, mesh); 57 | } 58 | 59 | function addLineGeometry(x, y, geometry) { 60 | const material = new THREE.LineBasicMaterial({ color: 0x000000 }); 61 | const mesh = new THREE.LineSegments(geometry, material); 62 | addObject(x, y, mesh); 63 | } 64 | 65 | { 66 | const width = 8; 67 | const height = 8; 68 | const depth = 8; 69 | addSolidGeometry(-1, 2, new THREE.BoxBufferGeometry(width, height, depth)); 70 | } 71 | { 72 | const radius = 7; 73 | const segments = 24; 74 | addSolidGeometry(0, 2, new THREE.CircleBufferGeometry(radius, segments)); 75 | } 76 | { 77 | const radius = 6; 78 | const height = 8; 79 | const segments = 16; 80 | addSolidGeometry(1, 2, new THREE.ConeBufferGeometry(radius, height, segments)); 81 | } 82 | { 83 | const radiusTop = 4; 84 | const radiusBottom = 4; 85 | const height = 8; 86 | const radialSegments = 12; 87 | addSolidGeometry(-1, 1, new THREE.CylinderBufferGeometry(radiusTop, radiusBottom, height, radialSegments)); 88 | } 89 | { 90 | const radius = 7; 91 | addSolidGeometry(0, 1, new THREE.DodecahedronBufferGeometry(radius)); 92 | } 93 | { 94 | const shape = new THREE.Shape(); 95 | const x = -2.5; 96 | const y = -5; 97 | shape.moveTo(x + 2.5, y + 2.5); 98 | shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y); 99 | shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5); 100 | shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5); 101 | shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5); 102 | shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y); 103 | shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5); 104 | 105 | const extrudeSettings = { 106 | steps: 2, 107 | depth: 2, 108 | bevelEnabled: true, 109 | bevelThickness: 1, 110 | bevelSize: 1, 111 | bevelSegments: 2, 112 | }; 113 | 114 | addSolidGeometry(1, 1, new THREE.ExtrudeBufferGeometry(shape, extrudeSettings)); 115 | } 116 | { 117 | const radius = 7; 118 | addSolidGeometry(-1, 0, new THREE.IcosahedronBufferGeometry(radius)); 119 | } 120 | { 121 | const points = []; 122 | for (let i = 0; i < 10; ++i) { 123 | points.push(new THREE.Vector2(Math.sin(i * 0.2) * 3 + 3, (i - 5) * .8)); 124 | } 125 | addSolidGeometry(0, 0, new THREE.LatheBufferGeometry(points)); 126 | } 127 | { 128 | const radius = 7; 129 | addSolidGeometry(1, 0, new THREE.OctahedronBufferGeometry(radius)); 130 | } 131 | { 132 | /* 133 | from: https://github.com/mrdoob/three.js/blob/b8d8a8625465bd634aa68e5846354d69f34d2ff5/examples/js/ParametricGeometries.js 134 | 135 | The MIT License 136 | 137 | Copyright © 2010-2018 three.js authors 138 | 139 | Permission is hereby granted, free of charge, to any person obtaining a copy 140 | of this software and associated documentation files (the "Software"), to deal 141 | in the Software without restriction, including without limitation the rights 142 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 143 | copies of the Software, and to permit persons to whom the Software is 144 | furnished to do so, subject to the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be included in 147 | all copies or substantial portions of the Software. 148 | 149 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 150 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 151 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 152 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 153 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 154 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 155 | THE SOFTWARE. 156 | 157 | */ 158 | function klein(v, u, target) { 159 | u *= Math.PI; 160 | v *= 2 * Math.PI; 161 | u = u * 2; 162 | 163 | let x; 164 | let z; 165 | 166 | if (u < Math.PI) { 167 | x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(u) * Math.cos(v); 168 | z = -8 * Math.sin(u) - 2 * (1 - Math.cos(u) / 2) * Math.sin(u) * Math.cos(v); 169 | } else { 170 | x = 3 * Math.cos(u) * (1 + Math.sin(u)) + (2 * (1 - Math.cos(u) / 2)) * Math.cos(v + Math.PI); 171 | z = -8 * Math.sin(u); 172 | } 173 | 174 | const y = -2 * (1 - Math.cos(u) / 2) * Math.sin(v); 175 | 176 | target.set(x, y, z).multiplyScalar(0.75); 177 | } 178 | 179 | const slices = 25; 180 | const stacks = 25; 181 | addSolidGeometry(-1, -1, new THREE.ParametricBufferGeometry(klein, slices, stacks)); 182 | } 183 | { 184 | const width = 9; 185 | const height = 9; 186 | const widthSegments = 2; 187 | const heightSegments = 2; 188 | addSolidGeometry(0, -1, new THREE.PlaneBufferGeometry(width, height, widthSegments, heightSegments)); 189 | } 190 | { 191 | const verticesOfCube = [ 192 | -1, -1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, 193 | -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, 194 | ]; 195 | const indicesOfFaces = [ 196 | 2, 1, 0, 0, 3, 2, 197 | 0, 4, 7, 7, 3, 0, 198 | 0, 1, 5, 5, 4, 0, 199 | 1, 2, 6, 6, 5, 1, 200 | 2, 3, 7, 7, 6, 2, 201 | 4, 5, 6, 6, 7, 4, 202 | ]; 203 | const radius = 7; 204 | const detail = 2; 205 | addSolidGeometry(1, -1, new THREE.PolyhedronBufferGeometry(verticesOfCube, indicesOfFaces, radius, detail)); 206 | } 207 | { 208 | const innerRadius = 2; 209 | const outerRadius = 7; 210 | const segments = 18; 211 | addSolidGeometry(-1, -2, new THREE.RingBufferGeometry(innerRadius, outerRadius, segments)); 212 | } 213 | { 214 | const shape = new THREE.Shape(); 215 | const x = -2.5; 216 | const y = -5; 217 | shape.moveTo(x + 2.5, y + 2.5); 218 | shape.bezierCurveTo(x + 2.5, y + 2.5, x + 2, y, x, y); 219 | shape.bezierCurveTo(x - 3, y, x - 3, y + 3.5, x - 3, y + 3.5); 220 | shape.bezierCurveTo(x - 3, y + 5.5, x - 1.5, y + 7.7, x + 2.5, y + 9.5); 221 | shape.bezierCurveTo(x + 6, y + 7.7, x + 8, y + 4.5, x + 8, y + 3.5); 222 | shape.bezierCurveTo(x + 8, y + 3.5, x + 8, y, x + 5, y); 223 | shape.bezierCurveTo(x + 3.5, y, x + 2.5, y + 2.5, x + 2.5, y + 2.5); 224 | addSolidGeometry(0, -2, new THREE.ShapeBufferGeometry(shape)); 225 | } 226 | { 227 | const radius = 7; 228 | const widthSegments = 12; 229 | const heightSegments = 8; 230 | addSolidGeometry(1, -2, new THREE.SphereBufferGeometry(radius, widthSegments, heightSegments)); 231 | } 232 | { 233 | const radius = 7; 234 | addSolidGeometry(-1, -3, new THREE.TetrahedronBufferGeometry(radius)); 235 | } 236 | { 237 | const loader = new THREE.FontLoader(); 238 | // promisify font loading 239 | function loadFont(url) { 240 | return new Promise((resolve, reject) => { 241 | loader.load(url, resolve, undefined, reject); 242 | }); 243 | } 244 | 245 | async function doit() { 246 | const font = await loadFont('https://threejsfundamentals.org/threejs/resources/threejs/fonts/helvetiker_regular.typeface.json'); 247 | const geometry = new THREE.TextBufferGeometry('three.js', { 248 | font: font, 249 | size: 3.0, 250 | height: .2, 251 | curveSegments: 12, 252 | bevelEnabled: true, 253 | bevelThickness: 0.15, 254 | bevelSize: .3, 255 | bevelSegments: 5, 256 | }); 257 | const mesh = new THREE.Mesh(geometry, createMaterial()); 258 | geometry.computeBoundingBox(); 259 | geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1); 260 | 261 | const parent = new THREE.Object3D(); 262 | parent.add(mesh); 263 | 264 | addObject(0, -3, parent); 265 | } 266 | doit(); 267 | } 268 | { 269 | const radius = 5; 270 | const tubeRadius = 2; 271 | const radialSegments = 8; 272 | const tubularSegments = 24; 273 | addSolidGeometry(1, -3, new THREE.TorusBufferGeometry(radius, tubeRadius, radialSegments, tubularSegments)); 274 | } 275 | //top four 276 | { 277 | const radius = 3.5; 278 | const tube = 1.5; 279 | const radialSegments = 8; 280 | const tubularSegments = 64; 281 | const p = 2; 282 | const q = 3; 283 | addSolidGeometry(-1.5, 3, new THREE.TorusKnotBufferGeometry(radius, tube, tubularSegments, radialSegments, p, q)); 284 | } 285 | { 286 | class CustomSinCurve extends THREE.Curve { 287 | constructor(scale) { 288 | super(); 289 | this.scale = scale; 290 | } 291 | getPoint(t) { 292 | const tx = t * 3 - 1.5; 293 | const ty = Math.sin(2 * Math.PI * t); 294 | const tz = 0; 295 | return new THREE.Vector3(tx, ty, tz).multiplyScalar(this.scale); 296 | } 297 | } 298 | 299 | const path = new CustomSinCurve(4); 300 | const tubularSegments = 20; 301 | const radius = 1; 302 | const radialSegments = 8; 303 | const closed = false; 304 | addSolidGeometry(-0.5, 3, new THREE.TubeBufferGeometry(path, tubularSegments, radius, radialSegments, closed)); 305 | } 306 | { 307 | const width = 8; 308 | const height = 8; 309 | const depth = 8; 310 | const thresholdAngle = 15; 311 | addLineGeometry(0.5, 3, new THREE.EdgesGeometry( 312 | new THREE.BoxBufferGeometry(width, height, depth), 313 | thresholdAngle)); 314 | } 315 | { 316 | const width = 8; 317 | const height = 8; 318 | const depth = 8; 319 | addLineGeometry(1.5, 3, new THREE.WireframeGeometry(new THREE.BoxBufferGeometry(width, height, depth))); 320 | } 321 | 322 | function resizeRendererToDisplaySize(renderer) { 323 | const canvas = renderer.domElement; 324 | // const width = canvas.clientWidth; 325 | // const height = canvas.clientHeight; 326 | let { width, height } = canvas.getBoundingClientRect() 327 | const needResize = canvas.getBoundingClientRect().width !== width || canvas.getBoundingClientRect().height !== height; 328 | if (needResize) { 329 | renderer.setSize(width, height, false); 330 | } 331 | return needResize; 332 | } 333 | 334 | function render(time) { 335 | time *= 0.001; 336 | 337 | if (resizeRendererToDisplaySize(renderer)) { 338 | const canvas = renderer.domElement; 339 | let { width, height } = canvas.getBoundingClientRect() 340 | // camera.aspect = canvas.clientWidth / canvas.clientHeight; 341 | camera.aspect = width / height; 342 | camera.updateProjectionMatrix(); 343 | } 344 | 345 | objects.forEach((obj, ndx) => { 346 | const speed = .1 + ndx * .05; 347 | const rot = time * speed; 348 | obj.rotation.x = rot; 349 | obj.rotation.y = rot; 350 | }); 351 | 352 | renderer.render(scene, camera); 353 | 354 | canvas.requestAnimationFrame(render); 355 | } 356 | 357 | canvas.requestAnimationFrame(render); 358 | } -------------------------------------------------------------------------------- /jsm/utils/SkeletonUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author sunag / http://www.sunag.com.br 3 | */ 4 | export default function (THREE) { 5 | let { 6 | AnimationClip, 7 | AnimationMixer, 8 | Euler, 9 | Matrix4, 10 | Quaternion, 11 | QuaternionKeyframeTrack, 12 | SkeletonHelper, 13 | Vector2, 14 | Vector3, 15 | VectorKeyframeTrack 16 | } = THREE; 17 | 18 | var SkeletonUtils = { 19 | 20 | retarget: function () { 21 | 22 | var pos = new Vector3(), 23 | quat = new Quaternion(), 24 | scale = new Vector3(), 25 | bindBoneMatrix = new Matrix4(), 26 | relativeMatrix = new Matrix4(), 27 | globalMatrix = new Matrix4(); 28 | 29 | return function ( target, source, options ) { 30 | 31 | options = options || {}; 32 | options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true; 33 | options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true; 34 | options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false; 35 | options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false; 36 | options.hip = options.hip !== undefined ? options.hip : "hip"; 37 | options.names = options.names || {}; 38 | 39 | var sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), 40 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), 41 | bindBones, 42 | bone, name, boneTo, 43 | bonesPosition, i; 44 | 45 | // reset bones 46 | 47 | if ( target.isObject3D ) { 48 | 49 | target.skeleton.pose(); 50 | 51 | } else { 52 | 53 | options.useTargetMatrix = true; 54 | options.preserveMatrix = false; 55 | 56 | } 57 | 58 | if ( options.preservePosition ) { 59 | 60 | bonesPosition = []; 61 | 62 | for ( i = 0; i < bones.length; i ++ ) { 63 | 64 | bonesPosition.push( bones[ i ].position.clone() ); 65 | 66 | } 67 | 68 | } 69 | 70 | if ( options.preserveMatrix ) { 71 | 72 | // reset matrix 73 | 74 | target.updateMatrixWorld(); 75 | 76 | target.matrixWorld.identity(); 77 | 78 | // reset children matrix 79 | 80 | for ( i = 0; i < target.children.length; ++ i ) { 81 | 82 | target.children[ i ].updateMatrixWorld( true ); 83 | 84 | } 85 | 86 | } 87 | 88 | if ( options.offsets ) { 89 | 90 | bindBones = []; 91 | 92 | for ( i = 0; i < bones.length; ++ i ) { 93 | 94 | bone = bones[ i ]; 95 | name = options.names[ bone.name ] || bone.name; 96 | 97 | if ( options.offsets && options.offsets[ name ] ) { 98 | 99 | bone.matrix.multiply( options.offsets[ name ] ); 100 | 101 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 102 | 103 | bone.updateMatrixWorld(); 104 | 105 | } 106 | 107 | bindBones.push( bone.matrixWorld.clone() ); 108 | 109 | } 110 | 111 | } 112 | 113 | for ( i = 0; i < bones.length; ++ i ) { 114 | 115 | bone = bones[ i ]; 116 | name = options.names[ bone.name ] || bone.name; 117 | 118 | boneTo = this.getBoneByName( name, sourceBones ); 119 | 120 | globalMatrix.copy( bone.matrixWorld ); 121 | 122 | if ( boneTo ) { 123 | 124 | boneTo.updateMatrixWorld(); 125 | 126 | if ( options.useTargetMatrix ) { 127 | 128 | relativeMatrix.copy( boneTo.matrixWorld ); 129 | 130 | } else { 131 | 132 | relativeMatrix.getInverse( target.matrixWorld ); 133 | relativeMatrix.multiply( boneTo.matrixWorld ); 134 | 135 | } 136 | 137 | // ignore scale to extract rotation 138 | 139 | scale.setFromMatrixScale( relativeMatrix ); 140 | relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) ); 141 | 142 | // apply to global matrix 143 | 144 | globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) ); 145 | 146 | if ( target.isObject3D ) { 147 | 148 | var boneIndex = bones.indexOf( bone ), 149 | wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.getInverse( target.skeleton.boneInverses[ boneIndex ] ); 150 | 151 | globalMatrix.multiply( wBindMatrix ); 152 | 153 | } 154 | 155 | globalMatrix.copyPosition( relativeMatrix ); 156 | 157 | } 158 | 159 | if ( bone.parent && bone.parent.isBone ) { 160 | 161 | bone.matrix.getInverse( bone.parent.matrixWorld ); 162 | bone.matrix.multiply( globalMatrix ); 163 | 164 | } else { 165 | 166 | bone.matrix.copy( globalMatrix ); 167 | 168 | } 169 | 170 | if ( options.preserveHipPosition && name === options.hip ) { 171 | 172 | bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) ); 173 | 174 | } 175 | 176 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 177 | 178 | bone.updateMatrixWorld(); 179 | 180 | } 181 | 182 | if ( options.preservePosition ) { 183 | 184 | for ( i = 0; i < bones.length; ++ i ) { 185 | 186 | bone = bones[ i ]; 187 | name = options.names[ bone.name ] || bone.name; 188 | 189 | if ( name !== options.hip ) { 190 | 191 | bone.position.copy( bonesPosition[ i ] ); 192 | 193 | } 194 | 195 | } 196 | 197 | } 198 | 199 | if ( options.preserveMatrix ) { 200 | 201 | // restore matrix 202 | 203 | target.updateMatrixWorld( true ); 204 | 205 | } 206 | 207 | }; 208 | 209 | }(), 210 | 211 | retargetClip: function ( target, source, clip, options ) { 212 | 213 | options = options || {}; 214 | options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false; 215 | options.fps = options.fps !== undefined ? options.fps : 30; 216 | options.names = options.names || []; 217 | 218 | if ( ! source.isObject3D ) { 219 | 220 | source = this.getHelperFromSkeleton( source ); 221 | 222 | } 223 | 224 | var numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ), 225 | delta = 1 / options.fps, 226 | convertedTracks = [], 227 | mixer = new AnimationMixer( source ), 228 | bones = this.getBones( target.skeleton ), 229 | boneDatas = [], 230 | positionOffset, 231 | bone, boneTo, boneData, 232 | name, i, j; 233 | 234 | mixer.clipAction( clip ).play(); 235 | mixer.update( 0 ); 236 | 237 | source.updateMatrixWorld(); 238 | 239 | for ( i = 0; i < numFrames; ++ i ) { 240 | 241 | var time = i * delta; 242 | 243 | this.retarget( target, source, options ); 244 | 245 | for ( j = 0; j < bones.length; ++ j ) { 246 | 247 | name = options.names[ bones[ j ].name ] || bones[ j ].name; 248 | 249 | boneTo = this.getBoneByName( name, source.skeleton ); 250 | 251 | if ( boneTo ) { 252 | 253 | bone = bones[ j ]; 254 | boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone }; 255 | 256 | if ( options.hip === name ) { 257 | 258 | if ( ! boneData.pos ) { 259 | 260 | boneData.pos = { 261 | times: new Float32Array( numFrames ), 262 | values: new Float32Array( numFrames * 3 ) 263 | }; 264 | 265 | } 266 | 267 | if ( options.useFirstFramePosition ) { 268 | 269 | if ( i === 0 ) { 270 | 271 | positionOffset = bone.position.clone(); 272 | 273 | } 274 | 275 | bone.position.sub( positionOffset ); 276 | 277 | } 278 | 279 | boneData.pos.times[ i ] = time; 280 | 281 | bone.position.toArray( boneData.pos.values, i * 3 ); 282 | 283 | } 284 | 285 | if ( ! boneData.quat ) { 286 | 287 | boneData.quat = { 288 | times: new Float32Array( numFrames ), 289 | values: new Float32Array( numFrames * 4 ) 290 | }; 291 | 292 | } 293 | 294 | boneData.quat.times[ i ] = time; 295 | 296 | bone.quaternion.toArray( boneData.quat.values, i * 4 ); 297 | 298 | } 299 | 300 | } 301 | 302 | mixer.update( delta ); 303 | 304 | source.updateMatrixWorld(); 305 | 306 | } 307 | 308 | for ( i = 0; i < boneDatas.length; ++ i ) { 309 | 310 | boneData = boneDatas[ i ]; 311 | 312 | if ( boneData ) { 313 | 314 | if ( boneData.pos ) { 315 | 316 | convertedTracks.push( new VectorKeyframeTrack( 317 | ".bones[" + boneData.bone.name + "].position", 318 | boneData.pos.times, 319 | boneData.pos.values 320 | ) ); 321 | 322 | } 323 | 324 | convertedTracks.push( new QuaternionKeyframeTrack( 325 | ".bones[" + boneData.bone.name + "].quaternion", 326 | boneData.quat.times, 327 | boneData.quat.values 328 | ) ); 329 | 330 | } 331 | 332 | } 333 | 334 | mixer.uncacheAction( clip ); 335 | 336 | return new AnimationClip( clip.name, - 1, convertedTracks ); 337 | 338 | }, 339 | 340 | getHelperFromSkeleton: function ( skeleton ) { 341 | 342 | var source = new SkeletonHelper( skeleton.bones[ 0 ] ); 343 | source.skeleton = skeleton; 344 | 345 | return source; 346 | 347 | }, 348 | 349 | getSkeletonOffsets: function () { 350 | 351 | var targetParentPos = new Vector3(), 352 | targetPos = new Vector3(), 353 | sourceParentPos = new Vector3(), 354 | sourcePos = new Vector3(), 355 | targetDir = new Vector2(), 356 | sourceDir = new Vector2(); 357 | 358 | return function ( target, source, options ) { 359 | 360 | options = options || {}; 361 | options.hip = options.hip !== undefined ? options.hip : "hip"; 362 | options.names = options.names || {}; 363 | 364 | if ( ! source.isObject3D ) { 365 | 366 | source = this.getHelperFromSkeleton( source ); 367 | 368 | } 369 | 370 | var nameKeys = Object.keys( options.names ), 371 | nameValues = Object.values( options.names ), 372 | sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ), 373 | bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ), 374 | offsets = [], 375 | bone, boneTo, 376 | name, i; 377 | 378 | target.skeleton.pose(); 379 | 380 | for ( i = 0; i < bones.length; ++ i ) { 381 | 382 | bone = bones[ i ]; 383 | name = options.names[ bone.name ] || bone.name; 384 | 385 | boneTo = this.getBoneByName( name, sourceBones ); 386 | 387 | if ( boneTo && name !== options.hip ) { 388 | 389 | var boneParent = this.getNearestBone( bone.parent, nameKeys ), 390 | boneToParent = this.getNearestBone( boneTo.parent, nameValues ); 391 | 392 | boneParent.updateMatrixWorld(); 393 | boneToParent.updateMatrixWorld(); 394 | 395 | targetParentPos.setFromMatrixPosition( boneParent.matrixWorld ); 396 | targetPos.setFromMatrixPosition( bone.matrixWorld ); 397 | 398 | sourceParentPos.setFromMatrixPosition( boneToParent.matrixWorld ); 399 | sourcePos.setFromMatrixPosition( boneTo.matrixWorld ); 400 | 401 | targetDir.subVectors( 402 | new Vector2( targetPos.x, targetPos.y ), 403 | new Vector2( targetParentPos.x, targetParentPos.y ) 404 | ).normalize(); 405 | 406 | sourceDir.subVectors( 407 | new Vector2( sourcePos.x, sourcePos.y ), 408 | new Vector2( sourceParentPos.x, sourceParentPos.y ) 409 | ).normalize(); 410 | 411 | var laterialAngle = targetDir.angle() - sourceDir.angle(); 412 | 413 | var offset = new Matrix4().makeRotationFromEuler( 414 | new Euler( 415 | 0, 416 | 0, 417 | laterialAngle 418 | ) 419 | ); 420 | 421 | bone.matrix.multiply( offset ); 422 | 423 | bone.matrix.decompose( bone.position, bone.quaternion, bone.scale ); 424 | 425 | bone.updateMatrixWorld(); 426 | 427 | offsets[ name ] = offset; 428 | 429 | } 430 | 431 | } 432 | 433 | return offsets; 434 | 435 | }; 436 | 437 | }(), 438 | 439 | renameBones: function ( skeleton, names ) { 440 | 441 | var bones = this.getBones( skeleton ); 442 | 443 | for ( var i = 0; i < bones.length; ++ i ) { 444 | 445 | var bone = bones[ i ]; 446 | 447 | if ( names[ bone.name ] ) { 448 | 449 | bone.name = names[ bone.name ]; 450 | 451 | } 452 | 453 | } 454 | 455 | return this; 456 | 457 | }, 458 | 459 | getBones: function ( skeleton ) { 460 | 461 | return Array.isArray( skeleton ) ? skeleton : skeleton.bones; 462 | 463 | }, 464 | 465 | getBoneByName: function ( name, skeleton ) { 466 | 467 | for ( var i = 0, bones = this.getBones( skeleton ); i < bones.length; i ++ ) { 468 | 469 | if ( name === bones[ i ].name ) 470 | 471 | return bones[ i ]; 472 | 473 | } 474 | 475 | }, 476 | 477 | getNearestBone: function ( bone, names ) { 478 | 479 | while ( bone.isBone ) { 480 | 481 | if ( names.indexOf( bone.name ) !== - 1 ) { 482 | 483 | return bone; 484 | 485 | } 486 | 487 | bone = bone.parent; 488 | 489 | } 490 | 491 | }, 492 | 493 | findBoneTrackData: function ( name, tracks ) { 494 | 495 | var regexp = /\[(.*)\]\.(.*)/, 496 | result = { name: name }; 497 | 498 | for ( var i = 0; i < tracks.length; ++ i ) { 499 | 500 | // 1 is track name 501 | // 2 is track type 502 | var trackData = regexp.exec( tracks[ i ].name ); 503 | 504 | if ( trackData && name === trackData[ 1 ] ) { 505 | 506 | result[ trackData[ 2 ] ] = i; 507 | 508 | } 509 | 510 | } 511 | 512 | return result; 513 | 514 | }, 515 | 516 | getEqualsBonesNames: function ( skeleton, targetSkeleton ) { 517 | 518 | var sourceBones = this.getBones( skeleton ), 519 | targetBones = this.getBones( targetSkeleton ), 520 | bones = []; 521 | 522 | search : for ( var i = 0; i < sourceBones.length; i ++ ) { 523 | 524 | var boneName = sourceBones[ i ].name; 525 | 526 | for ( var j = 0; j < targetBones.length; j ++ ) { 527 | 528 | if ( boneName === targetBones[ j ].name ) { 529 | 530 | bones.push( boneName ); 531 | 532 | continue search; 533 | 534 | } 535 | 536 | } 537 | 538 | } 539 | 540 | return bones; 541 | 542 | }, 543 | 544 | clone: function ( source ) { 545 | 546 | var sourceLookup = new Map(); 547 | var cloneLookup = new Map(); 548 | 549 | var clone = source.clone(); 550 | 551 | parallelTraverse( source, clone, function ( sourceNode, clonedNode ) { 552 | 553 | sourceLookup.set( clonedNode, sourceNode ); 554 | cloneLookup.set( sourceNode, clonedNode ); 555 | 556 | } ); 557 | 558 | clone.traverse( function ( node ) { 559 | 560 | if ( ! node.isSkinnedMesh ) return; 561 | 562 | var clonedMesh = node; 563 | var sourceMesh = sourceLookup.get( node ); 564 | var sourceBones = sourceMesh.skeleton.bones; 565 | 566 | clonedMesh.skeleton = sourceMesh.skeleton.clone(); 567 | clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix ); 568 | 569 | clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) { 570 | 571 | return cloneLookup.get( bone ); 572 | 573 | } ); 574 | 575 | clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix ); 576 | 577 | } ); 578 | 579 | return clone; 580 | 581 | } 582 | 583 | }; 584 | 585 | 586 | function parallelTraverse( a, b, callback ) { 587 | 588 | callback( a, b ); 589 | 590 | for ( var i = 0; i < a.children.length; i ++ ) { 591 | 592 | parallelTraverse( a.children[ i ], b.children[ i ], callback ); 593 | 594 | } 595 | 596 | } 597 | 598 | return { SkeletonUtils }; 599 | } -------------------------------------------------------------------------------- /jsm/loaders/STLLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author aleeper / http://adamleeper.com/ 3 | * @author mrdoob / http://mrdoob.com/ 4 | * @author gero3 / https://github.com/gero3 5 | * @author Mugen87 / https://github.com/Mugen87 6 | * @author neverhood311 / https://github.com/neverhood311 7 | * 8 | * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs. 9 | * 10 | * Supports both binary and ASCII encoded files, with automatic detection of type. 11 | * 12 | * The loader returns a non-indexed buffer geometry. 13 | * 14 | * Limitations: 15 | * Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL). 16 | * There is perhaps some question as to how valid it is to always assume little-endian-ness. 17 | * ASCII decoding assumes file is UTF-8. 18 | * 19 | * Usage: 20 | * var loader = new STLLoader(); 21 | * loader.load( './models/stl/slotted_disk.stl', function ( geometry ) { 22 | * scene.add( new THREE.Mesh( geometry ) ); 23 | * }); 24 | * 25 | * For binary STLs geometry might contain colors for vertices. To use it: 26 | * // use the same code to load STL as above 27 | * if (geometry.hasColors) { 28 | * material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: THREE.VertexColors }); 29 | * } else { .... } 30 | * var mesh = new THREE.Mesh( geometry, material ); 31 | * 32 | * For ASCII STLs containing multiple solids, each solid is assigned to a different group. 33 | * Groups can be used to assign a different color by defining an array of materials with the same length of 34 | * geometry.groups and passing it to the Mesh constructor: 35 | * 36 | * var mesh = new THREE.Mesh( geometry, material ); 37 | * 38 | * For example: 39 | * 40 | * var materials = []; 41 | * var nGeometryGroups = geometry.groups.length; 42 | * 43 | * var colorMap = ...; // Some logic to index colors. 44 | * 45 | * for (var i = 0; i < nGeometryGroups; i++) { 46 | * 47 | * var material = new THREE.MeshPhongMaterial({ 48 | * color: colorMap[i], 49 | * wireframe: false 50 | * }); 51 | * 52 | * } 53 | * 54 | * materials.push(material); 55 | * var mesh = new THREE.Mesh(geometry, materials); 56 | */ 57 | 58 | export default function (THREE) { 59 | let { 60 | BufferAttribute, 61 | BufferGeometry, 62 | FileLoader, 63 | Float32BufferAttribute, 64 | Loader, 65 | LoaderUtils, 66 | Vector3 67 | } = THREE; 68 | 69 | 70 | var STLLoader = function ( manager ) { 71 | 72 | Loader.call( this, manager ); 73 | 74 | }; 75 | 76 | STLLoader.prototype = Object.assign( Object.create( Loader.prototype ), { 77 | 78 | constructor: STLLoader, 79 | 80 | load: function ( url, onLoad, onProgress, onError ) { 81 | 82 | var scope = this; 83 | 84 | var loader = new FileLoader( scope.manager ); 85 | loader.setPath( scope.path ); 86 | loader.setResponseType( 'arraybuffer' ); 87 | loader.load( url, function ( text ) { 88 | 89 | try { 90 | 91 | onLoad( scope.parse( text ) ); 92 | 93 | } catch ( exception ) { 94 | 95 | if ( onError ) { 96 | 97 | onError( exception ); 98 | 99 | } 100 | 101 | } 102 | 103 | }, onProgress, onError ); 104 | 105 | }, 106 | 107 | parse: function ( data ) { 108 | 109 | function isBinary( data ) { 110 | 111 | var expect, face_size, n_faces, reader; 112 | reader = new DataView( data ); 113 | face_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 ); 114 | n_faces = reader.getUint32( 80, true ); 115 | expect = 80 + ( 32 / 8 ) + ( n_faces * face_size ); 116 | 117 | if ( expect === reader.byteLength ) { 118 | 119 | return true; 120 | 121 | } 122 | 123 | // An ASCII STL data must begin with 'solid ' as the first six bytes. 124 | // However, ASCII STLs lacking the SPACE after the 'd' are known to be 125 | // plentiful. So, check the first 5 bytes for 'solid'. 126 | 127 | // Several encodings, such as UTF-8, precede the text with up to 5 bytes: 128 | // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding 129 | // Search for "solid" to start anywhere after those prefixes. 130 | 131 | // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd' 132 | 133 | var solid = [ 115, 111, 108, 105, 100 ]; 134 | 135 | for ( var off = 0; off < 5; off ++ ) { 136 | 137 | // If "solid" text is matched to the current offset, declare it to be an ASCII STL. 138 | 139 | if ( matchDataViewAt( solid, reader, off ) ) return false; 140 | 141 | } 142 | 143 | // Couldn't find "solid" text at the beginning; it is binary STL. 144 | 145 | return true; 146 | 147 | } 148 | 149 | function matchDataViewAt( query, reader, offset ) { 150 | 151 | // Check if each byte in query matches the corresponding byte from the current offset 152 | 153 | for ( var i = 0, il = query.length; i < il; i ++ ) { 154 | 155 | if ( query[ i ] !== reader.getUint8( offset + i, false ) ) return false; 156 | 157 | } 158 | 159 | return true; 160 | 161 | } 162 | 163 | function parseBinary( data ) { 164 | 165 | var reader = new DataView( data ); 166 | var faces = reader.getUint32( 80, true ); 167 | 168 | var r, g, b, hasColors = false, colors; 169 | var defaultR, defaultG, defaultB, alpha; 170 | 171 | // process STL header 172 | // check for default color in header ("COLOR=rgba" sequence). 173 | 174 | for ( var index = 0; index < 80 - 10; index ++ ) { 175 | 176 | if ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) && 177 | ( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) && 178 | ( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) { 179 | 180 | hasColors = true; 181 | colors = new Float32Array( faces * 3 * 3 ); 182 | 183 | defaultR = reader.getUint8( index + 6 ) / 255; 184 | defaultG = reader.getUint8( index + 7 ) / 255; 185 | defaultB = reader.getUint8( index + 8 ) / 255; 186 | alpha = reader.getUint8( index + 9 ) / 255; 187 | 188 | } 189 | 190 | } 191 | 192 | var dataOffset = 84; 193 | var faceLength = 12 * 4 + 2; 194 | 195 | var geometry = new BufferGeometry(); 196 | 197 | var vertices = new Float32Array( faces * 3 * 3 ); 198 | var normals = new Float32Array( faces * 3 * 3 ); 199 | 200 | for ( var face = 0; face < faces; face ++ ) { 201 | 202 | var start = dataOffset + face * faceLength; 203 | var normalX = reader.getFloat32( start, true ); 204 | var normalY = reader.getFloat32( start + 4, true ); 205 | var normalZ = reader.getFloat32( start + 8, true ); 206 | 207 | if ( hasColors ) { 208 | 209 | var packedColor = reader.getUint16( start + 48, true ); 210 | 211 | if ( ( packedColor & 0x8000 ) === 0 ) { 212 | 213 | // facet has its own unique color 214 | 215 | r = ( packedColor & 0x1F ) / 31; 216 | g = ( ( packedColor >> 5 ) & 0x1F ) / 31; 217 | b = ( ( packedColor >> 10 ) & 0x1F ) / 31; 218 | 219 | } else { 220 | 221 | r = defaultR; 222 | g = defaultG; 223 | b = defaultB; 224 | 225 | } 226 | 227 | } 228 | 229 | for ( var i = 1; i <= 3; i ++ ) { 230 | 231 | var vertexstart = start + i * 12; 232 | var componentIdx = ( face * 3 * 3 ) + ( ( i - 1 ) * 3 ); 233 | 234 | vertices[ componentIdx ] = reader.getFloat32( vertexstart, true ); 235 | vertices[ componentIdx + 1 ] = reader.getFloat32( vertexstart + 4, true ); 236 | vertices[ componentIdx + 2 ] = reader.getFloat32( vertexstart + 8, true ); 237 | 238 | normals[ componentIdx ] = normalX; 239 | normals[ componentIdx + 1 ] = normalY; 240 | normals[ componentIdx + 2 ] = normalZ; 241 | 242 | if ( hasColors ) { 243 | 244 | colors[ componentIdx ] = r; 245 | colors[ componentIdx + 1 ] = g; 246 | colors[ componentIdx + 2 ] = b; 247 | 248 | } 249 | 250 | } 251 | 252 | } 253 | 254 | geometry.addAttribute( 'position', new BufferAttribute( vertices, 3 ) ); 255 | geometry.addAttribute( 'normal', new BufferAttribute( normals, 3 ) ); 256 | 257 | if ( hasColors ) { 258 | 259 | geometry.addAttribute( 'color', new BufferAttribute( colors, 3 ) ); 260 | geometry.hasColors = true; 261 | geometry.alpha = alpha; 262 | 263 | } 264 | 265 | return geometry; 266 | 267 | } 268 | 269 | function parseASCII( data ) { 270 | 271 | var geometry = new BufferGeometry(); 272 | var patternSolid = /solid([\s\S]*?)endsolid/g; 273 | var patternFace = /facet([\s\S]*?)endfacet/g; 274 | var faceCounter = 0; 275 | 276 | var patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source; 277 | var patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' ); 278 | var patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' ); 279 | 280 | var vertices = []; 281 | var normals = []; 282 | 283 | var normal = new Vector3(); 284 | 285 | var result; 286 | 287 | var groupVertexes = []; 288 | var groupCount = 0; 289 | var startVertex = 0; 290 | var endVertex = 0; 291 | 292 | while ( ( result = patternSolid.exec( data ) ) !== null ) { 293 | 294 | startVertex = endVertex; 295 | 296 | var solid = result[ 0 ]; 297 | 298 | while ( ( result = patternFace.exec( solid ) ) !== null ) { 299 | 300 | var vertexCountPerFace = 0; 301 | var normalCountPerFace = 0; 302 | 303 | var text = result[ 0 ]; 304 | 305 | while ( ( result = patternNormal.exec( text ) ) !== null ) { 306 | 307 | normal.x = parseFloat( result[ 1 ] ); 308 | normal.y = parseFloat( result[ 2 ] ); 309 | normal.z = parseFloat( result[ 3 ] ); 310 | normalCountPerFace ++; 311 | 312 | } 313 | 314 | while ( ( result = patternVertex.exec( text ) ) !== null ) { 315 | 316 | vertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) ); 317 | normals.push( normal.x, normal.y, normal.z ); 318 | vertexCountPerFace ++; 319 | endVertex ++; 320 | 321 | } 322 | 323 | // every face have to own ONE valid normal 324 | 325 | if ( normalCountPerFace !== 1 ) { 326 | 327 | console.error( 'THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter ); 328 | 329 | } 330 | 331 | // each face have to own THREE valid vertices 332 | 333 | if ( vertexCountPerFace !== 3 ) { 334 | 335 | console.error( 'THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter ); 336 | 337 | } 338 | 339 | faceCounter ++; 340 | 341 | } 342 | 343 | groupVertexes.push( { startVertex: startVertex, endVertex: endVertex } ); 344 | groupCount ++; 345 | 346 | } 347 | 348 | geometry.addAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); 349 | geometry.addAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); 350 | 351 | if ( groupCount > 0 ) { 352 | 353 | for ( var i = 0; i < groupVertexes.length; i ++ ) { 354 | 355 | geometry.addGroup( groupVertexes[ i ].startVertex, groupVertexes[ i ].endVertex, i ); 356 | 357 | } 358 | 359 | } 360 | 361 | return geometry; 362 | 363 | } 364 | 365 | function ensureString( buffer ) { 366 | 367 | if ( typeof buffer !== 'string' ) { 368 | 369 | return LoaderUtils.decodeText( new Uint8Array( buffer ) ); 370 | 371 | } 372 | 373 | return buffer; 374 | 375 | } 376 | 377 | function ensureBinary( buffer ) { 378 | 379 | if ( typeof buffer === 'string' ) { 380 | 381 | var array_buffer = new Uint8Array( buffer.length ); 382 | for ( var i = 0; i < buffer.length; i ++ ) { 383 | 384 | array_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian 385 | 386 | } 387 | 388 | return array_buffer.buffer || array_buffer; 389 | 390 | } else { 391 | 392 | return buffer; 393 | 394 | } 395 | 396 | } 397 | 398 | // start 399 | 400 | var binData = ensureBinary( data ); 401 | 402 | return isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) ); 403 | 404 | } 405 | 406 | } ); 407 | 408 | return { STLLoader }; 409 | } -------------------------------------------------------------------------------- /jsm/loaders/RGBELoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Nikos M. / https://github.com/foo123/ 3 | */ 4 | export default function (THREE) { 5 | let { 6 | DataTextureLoader, 7 | FloatType, 8 | HalfFloatType, 9 | LinearEncoding, 10 | LinearFilter, 11 | NearestFilter, 12 | RGBEEncoding, 13 | RGBEFormat, 14 | RGBFormat, 15 | UnsignedByteType 16 | } = THREE; 17 | 18 | // https://github.com/mrdoob/three.js/issues/5552 19 | // http://en.wikipedia.org/wiki/RGBE_image_format 20 | 21 | var RGBELoader = function ( manager ) { 22 | 23 | DataTextureLoader.call( this, manager ); 24 | 25 | this.type = UnsignedByteType; 26 | 27 | }; 28 | 29 | RGBELoader.prototype = Object.assign( Object.create( DataTextureLoader.prototype ), { 30 | 31 | constructor: RGBELoader, 32 | 33 | // adapted from http://www.graphics.cornell.edu/~bjw/rgbe.html 34 | 35 | parse: function ( buffer ) { 36 | 37 | var 38 | /* return codes for rgbe routines */ 39 | //RGBE_RETURN_SUCCESS = 0, 40 | RGBE_RETURN_FAILURE = - 1, 41 | 42 | /* default error routine. change this to change error handling */ 43 | rgbe_read_error = 1, 44 | rgbe_write_error = 2, 45 | rgbe_format_error = 3, 46 | rgbe_memory_error = 4, 47 | rgbe_error = function ( rgbe_error_code, msg ) { 48 | 49 | switch ( rgbe_error_code ) { 50 | 51 | case rgbe_read_error: console.error( "RGBELoader Read Error: " + ( msg || '' ) ); 52 | break; 53 | case rgbe_write_error: console.error( "RGBELoader Write Error: " + ( msg || '' ) ); 54 | break; 55 | case rgbe_format_error: console.error( "RGBELoader Bad File Format: " + ( msg || '' ) ); 56 | break; 57 | default: 58 | case rgbe_memory_error: console.error( "RGBELoader: Error: " + ( msg || '' ) ); 59 | 60 | } 61 | return RGBE_RETURN_FAILURE; 62 | 63 | }, 64 | 65 | /* offsets to red, green, and blue components in a data (float) pixel */ 66 | //RGBE_DATA_RED = 0, 67 | //RGBE_DATA_GREEN = 1, 68 | //RGBE_DATA_BLUE = 2, 69 | 70 | /* number of floats per pixel, use 4 since stored in rgba image format */ 71 | //RGBE_DATA_SIZE = 4, 72 | 73 | /* flags indicating which fields in an rgbe_header_info are valid */ 74 | RGBE_VALID_PROGRAMTYPE = 1, 75 | RGBE_VALID_FORMAT = 2, 76 | RGBE_VALID_DIMENSIONS = 4, 77 | 78 | NEWLINE = "\n", 79 | 80 | fgets = function ( buffer, lineLimit, consume ) { 81 | 82 | lineLimit = ! lineLimit ? 1024 : lineLimit; 83 | var p = buffer.pos, 84 | i = - 1, len = 0, s = '', chunkSize = 128, 85 | chunk = String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) ) 86 | ; 87 | while ( ( 0 > ( i = chunk.indexOf( NEWLINE ) ) ) && ( len < lineLimit ) && ( p < buffer.byteLength ) ) { 88 | 89 | s += chunk; len += chunk.length; 90 | p += chunkSize; 91 | chunk += String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) ); 92 | 93 | } 94 | 95 | if ( - 1 < i ) { 96 | 97 | /*for (i=l-1; i>=0; i--) { 98 | byteCode = m.charCodeAt(i); 99 | if (byteCode > 0x7f && byteCode <= 0x7ff) byteLen++; 100 | else if (byteCode > 0x7ff && byteCode <= 0xffff) byteLen += 2; 101 | if (byteCode >= 0xDC00 && byteCode <= 0xDFFF) i--; //trail surrogate 102 | }*/ 103 | if ( false !== consume ) buffer.pos += len + i + 1; 104 | return s + chunk.slice( 0, i ); 105 | 106 | } 107 | return false; 108 | 109 | }, 110 | 111 | /* minimal header reading. modify if you want to parse more information */ 112 | RGBE_ReadHeader = function ( buffer ) { 113 | 114 | var line, match, 115 | 116 | // regexes to parse header info fields 117 | magic_token_re = /^#\?(\S+)$/, 118 | gamma_re = /^\s*GAMMA\s*=\s*(\d+(\.\d+)?)\s*$/, 119 | exposure_re = /^\s*EXPOSURE\s*=\s*(\d+(\.\d+)?)\s*$/, 120 | format_re = /^\s*FORMAT=(\S+)\s*$/, 121 | dimensions_re = /^\s*\-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/, 122 | 123 | // RGBE format header struct 124 | header = { 125 | 126 | valid: 0, /* indicate which fields are valid */ 127 | 128 | string: '', /* the actual header string */ 129 | 130 | comments: '', /* comments found in header */ 131 | 132 | programtype: 'RGBE', /* listed at beginning of file to identify it after "#?". defaults to "RGBE" */ 133 | 134 | format: '', /* RGBE format, default 32-bit_rle_rgbe */ 135 | 136 | gamma: 1.0, /* image has already been gamma corrected with given gamma. defaults to 1.0 (no correction) */ 137 | 138 | exposure: 1.0, /* a value of 1.0 in an image corresponds to watts/steradian/m^2. defaults to 1.0 */ 139 | 140 | width: 0, height: 0 /* image dimensions, width/height */ 141 | 142 | }; 143 | 144 | if ( buffer.pos >= buffer.byteLength || ! ( line = fgets( buffer ) ) ) { 145 | 146 | return rgbe_error( rgbe_read_error, "no header found" ); 147 | 148 | } 149 | /* if you want to require the magic token then uncomment the next line */ 150 | if ( ! ( match = line.match( magic_token_re ) ) ) { 151 | 152 | return rgbe_error( rgbe_format_error, "bad initial token" ); 153 | 154 | } 155 | header.valid |= RGBE_VALID_PROGRAMTYPE; 156 | header.programtype = match[ 1 ]; 157 | header.string += line + "\n"; 158 | 159 | while ( true ) { 160 | 161 | line = fgets( buffer ); 162 | if ( false === line ) break; 163 | header.string += line + "\n"; 164 | 165 | if ( '#' === line.charAt( 0 ) ) { 166 | 167 | header.comments += line + "\n"; 168 | continue; // comment line 169 | 170 | } 171 | 172 | if ( match = line.match( gamma_re ) ) { 173 | 174 | header.gamma = parseFloat( match[ 1 ], 10 ); 175 | 176 | } 177 | if ( match = line.match( exposure_re ) ) { 178 | 179 | header.exposure = parseFloat( match[ 1 ], 10 ); 180 | 181 | } 182 | if ( match = line.match( format_re ) ) { 183 | 184 | header.valid |= RGBE_VALID_FORMAT; 185 | header.format = match[ 1 ];//'32-bit_rle_rgbe'; 186 | 187 | } 188 | if ( match = line.match( dimensions_re ) ) { 189 | 190 | header.valid |= RGBE_VALID_DIMENSIONS; 191 | header.height = parseInt( match[ 1 ], 10 ); 192 | header.width = parseInt( match[ 2 ], 10 ); 193 | 194 | } 195 | 196 | if ( ( header.valid & RGBE_VALID_FORMAT ) && ( header.valid & RGBE_VALID_DIMENSIONS ) ) break; 197 | 198 | } 199 | 200 | if ( ! ( header.valid & RGBE_VALID_FORMAT ) ) { 201 | 202 | return rgbe_error( rgbe_format_error, "missing format specifier" ); 203 | 204 | } 205 | if ( ! ( header.valid & RGBE_VALID_DIMENSIONS ) ) { 206 | 207 | return rgbe_error( rgbe_format_error, "missing image size specifier" ); 208 | 209 | } 210 | 211 | return header; 212 | 213 | }, 214 | 215 | RGBE_ReadPixels_RLE = function ( buffer, w, h ) { 216 | 217 | var data_rgba, offset, pos, count, byteValue, 218 | scanline_buffer, ptr, ptr_end, i, l, off, isEncodedRun, 219 | scanline_width = w, num_scanlines = h, rgbeStart 220 | ; 221 | 222 | if ( 223 | // run length encoding is not allowed so read flat 224 | ( ( scanline_width < 8 ) || ( scanline_width > 0x7fff ) ) || 225 | // this file is not run length encoded 226 | ( ( 2 !== buffer[ 0 ] ) || ( 2 !== buffer[ 1 ] ) || ( buffer[ 2 ] & 0x80 ) ) 227 | ) { 228 | 229 | // return the flat buffer 230 | return new Uint8Array( buffer ); 231 | 232 | } 233 | 234 | if ( scanline_width !== ( ( buffer[ 2 ] << 8 ) | buffer[ 3 ] ) ) { 235 | 236 | return rgbe_error( rgbe_format_error, "wrong scanline width" ); 237 | 238 | } 239 | 240 | data_rgba = new Uint8Array( 4 * w * h ); 241 | 242 | if ( ! data_rgba || ! data_rgba.length ) { 243 | 244 | return rgbe_error( rgbe_memory_error, "unable to allocate buffer space" ); 245 | 246 | } 247 | 248 | offset = 0; pos = 0; ptr_end = 4 * scanline_width; 249 | rgbeStart = new Uint8Array( 4 ); 250 | scanline_buffer = new Uint8Array( ptr_end ); 251 | 252 | // read in each successive scanline 253 | while ( ( num_scanlines > 0 ) && ( pos < buffer.byteLength ) ) { 254 | 255 | if ( pos + 4 > buffer.byteLength ) { 256 | 257 | return rgbe_error( rgbe_read_error ); 258 | 259 | } 260 | 261 | rgbeStart[ 0 ] = buffer[ pos ++ ]; 262 | rgbeStart[ 1 ] = buffer[ pos ++ ]; 263 | rgbeStart[ 2 ] = buffer[ pos ++ ]; 264 | rgbeStart[ 3 ] = buffer[ pos ++ ]; 265 | 266 | if ( ( 2 != rgbeStart[ 0 ] ) || ( 2 != rgbeStart[ 1 ] ) || ( ( ( rgbeStart[ 2 ] << 8 ) | rgbeStart[ 3 ] ) != scanline_width ) ) { 267 | 268 | return rgbe_error( rgbe_format_error, "bad rgbe scanline format" ); 269 | 270 | } 271 | 272 | // read each of the four channels for the scanline into the buffer 273 | // first red, then green, then blue, then exponent 274 | ptr = 0; 275 | while ( ( ptr < ptr_end ) && ( pos < buffer.byteLength ) ) { 276 | 277 | count = buffer[ pos ++ ]; 278 | isEncodedRun = count > 128; 279 | if ( isEncodedRun ) count -= 128; 280 | 281 | if ( ( 0 === count ) || ( ptr + count > ptr_end ) ) { 282 | 283 | return rgbe_error( rgbe_format_error, "bad scanline data" ); 284 | 285 | } 286 | 287 | if ( isEncodedRun ) { 288 | 289 | // a (encoded) run of the same value 290 | byteValue = buffer[ pos ++ ]; 291 | for ( i = 0; i < count; i ++ ) { 292 | 293 | scanline_buffer[ ptr ++ ] = byteValue; 294 | 295 | } 296 | //ptr += count; 297 | 298 | } else { 299 | 300 | // a literal-run 301 | scanline_buffer.set( buffer.subarray( pos, pos + count ), ptr ); 302 | ptr += count; pos += count; 303 | 304 | } 305 | 306 | } 307 | 308 | 309 | // now convert data from buffer into rgba 310 | // first red, then green, then blue, then exponent (alpha) 311 | l = scanline_width; //scanline_buffer.byteLength; 312 | for ( i = 0; i < l; i ++ ) { 313 | 314 | off = 0; 315 | data_rgba[ offset ] = scanline_buffer[ i + off ]; 316 | off += scanline_width; //1; 317 | data_rgba[ offset + 1 ] = scanline_buffer[ i + off ]; 318 | off += scanline_width; //1; 319 | data_rgba[ offset + 2 ] = scanline_buffer[ i + off ]; 320 | off += scanline_width; //1; 321 | data_rgba[ offset + 3 ] = scanline_buffer[ i + off ]; 322 | offset += 4; 323 | 324 | } 325 | 326 | num_scanlines --; 327 | 328 | } 329 | 330 | return data_rgba; 331 | 332 | }; 333 | 334 | var RGBEByteToRGBFloat = function ( sourceArray, sourceOffset, destArray, destOffset ) { 335 | 336 | var e = sourceArray[ sourceOffset + 3 ]; 337 | var scale = Math.pow( 2.0, e - 128.0 ) / 255.0; 338 | 339 | destArray[ destOffset + 0 ] = sourceArray[ sourceOffset + 0 ] * scale; 340 | destArray[ destOffset + 1 ] = sourceArray[ sourceOffset + 1 ] * scale; 341 | destArray[ destOffset + 2 ] = sourceArray[ sourceOffset + 2 ] * scale; 342 | 343 | }; 344 | 345 | var RGBEByteToRGBHalf = ( function () { 346 | 347 | // Source: http://gamedev.stackexchange.com/questions/17326/conversion-of-a-number-from-single-precision-floating-point-representation-to-a/17410#17410 348 | 349 | var floatView = new Float32Array( 1 ); 350 | var int32View = new Int32Array( floatView.buffer ); 351 | 352 | /* This method is faster than the OpenEXR implementation (very often 353 | * used, eg. in Ogre), with the additional benefit of rounding, inspired 354 | * by James Tursa?s half-precision code. */ 355 | function toHalf( val ) { 356 | 357 | floatView[ 0 ] = val; 358 | var x = int32View[ 0 ]; 359 | 360 | var bits = ( x >> 16 ) & 0x8000; /* Get the sign */ 361 | var m = ( x >> 12 ) & 0x07ff; /* Keep one extra bit for rounding */ 362 | var e = ( x >> 23 ) & 0xff; /* Using int is faster here */ 363 | 364 | /* If zero, or denormal, or exponent underflows too much for a denormal 365 | * half, return signed zero. */ 366 | if ( e < 103 ) return bits; 367 | 368 | /* If NaN, return NaN. If Inf or exponent overflow, return Inf. */ 369 | if ( e > 142 ) { 370 | 371 | bits |= 0x7c00; 372 | /* If exponent was 0xff and one mantissa bit was set, it means NaN, 373 | * not Inf, so make sure we set one mantissa bit too. */ 374 | bits |= ( ( e == 255 ) ? 0 : 1 ) && ( x & 0x007fffff ); 375 | return bits; 376 | 377 | } 378 | 379 | /* If exponent underflows but not too much, return a denormal */ 380 | if ( e < 113 ) { 381 | 382 | m |= 0x0800; 383 | /* Extra rounding may overflow and set mantissa to 0 and exponent 384 | * to 1, which is OK. */ 385 | bits |= ( m >> ( 114 - e ) ) + ( ( m >> ( 113 - e ) ) & 1 ); 386 | return bits; 387 | 388 | } 389 | 390 | bits |= ( ( e - 112 ) << 10 ) | ( m >> 1 ); 391 | /* Extra rounding. An overflow will set mantissa to 0 and increment 392 | * the exponent, which is OK. */ 393 | bits += m & 1; 394 | return bits; 395 | 396 | } 397 | 398 | return function ( sourceArray, sourceOffset, destArray, destOffset ) { 399 | 400 | var e = sourceArray[ sourceOffset + 3 ]; 401 | var scale = Math.pow( 2.0, e - 128.0 ) / 255.0; 402 | 403 | destArray[ destOffset + 0 ] = toHalf( sourceArray[ sourceOffset + 0 ] * scale ); 404 | destArray[ destOffset + 1 ] = toHalf( sourceArray[ sourceOffset + 1 ] * scale ); 405 | destArray[ destOffset + 2 ] = toHalf( sourceArray[ sourceOffset + 2 ] * scale ); 406 | 407 | }; 408 | 409 | } )(); 410 | 411 | var byteArray = new Uint8Array( buffer ); 412 | byteArray.pos = 0; 413 | var rgbe_header_info = RGBE_ReadHeader( byteArray ); 414 | 415 | if ( RGBE_RETURN_FAILURE !== rgbe_header_info ) { 416 | 417 | var w = rgbe_header_info.width, 418 | h = rgbe_header_info.height, 419 | image_rgba_data = RGBE_ReadPixels_RLE( byteArray.subarray( byteArray.pos ), w, h ); 420 | 421 | if ( RGBE_RETURN_FAILURE !== image_rgba_data ) { 422 | 423 | switch ( this.type ) { 424 | 425 | case UnsignedByteType: 426 | 427 | var data = image_rgba_data; 428 | var format = RGBEFormat; // handled as THREE.RGBAFormat in shaders 429 | var type = UnsignedByteType; 430 | break; 431 | 432 | case FloatType: 433 | 434 | var numElements = ( image_rgba_data.length / 4 ) * 3; 435 | var floatArray = new Float32Array( numElements ); 436 | 437 | for ( var j = 0; j < numElements; j ++ ) { 438 | 439 | RGBEByteToRGBFloat( image_rgba_data, j * 4, floatArray, j * 3 ); 440 | 441 | } 442 | 443 | var data = floatArray; 444 | var format = RGBFormat; 445 | var type = FloatType; 446 | break; 447 | 448 | case HalfFloatType: 449 | 450 | var numElements = ( image_rgba_data.length / 4 ) * 3; 451 | var halfArray = new Uint16Array( numElements ); 452 | 453 | for ( var j = 0; j < numElements; j ++ ) { 454 | 455 | RGBEByteToRGBHalf( image_rgba_data, j * 4, halfArray, j * 3 ); 456 | 457 | } 458 | 459 | var data = halfArray; 460 | var format = RGBFormat; 461 | var type = HalfFloatType; 462 | break; 463 | 464 | default: 465 | 466 | console.error( 'THREE.RGBELoader: unsupported type: ', this.type ); 467 | break; 468 | 469 | } 470 | 471 | return { 472 | width: w, height: h, 473 | data: data, 474 | header: rgbe_header_info.string, 475 | gamma: rgbe_header_info.gamma, 476 | exposure: rgbe_header_info.exposure, 477 | format: format, 478 | type: type 479 | }; 480 | 481 | } 482 | 483 | } 484 | 485 | return null; 486 | 487 | }, 488 | 489 | setDataType: function ( value ) { 490 | 491 | this.type = value; 492 | return this; 493 | 494 | }, 495 | 496 | load: function ( url, onLoad, onProgress, onError ) { 497 | 498 | function onLoadCallback( texture, texData ) { 499 | 500 | switch ( texture.type ) { 501 | 502 | case UnsignedByteType: 503 | 504 | texture.encoding = RGBEEncoding; 505 | texture.minFilter = NearestFilter; 506 | texture.magFilter = NearestFilter; 507 | texture.generateMipmaps = false; 508 | texture.flipY = true; 509 | break; 510 | 511 | case FloatType: 512 | 513 | texture.encoding = LinearEncoding; 514 | texture.minFilter = LinearFilter; 515 | texture.magFilter = LinearFilter; 516 | texture.generateMipmaps = false; 517 | texture.flipY = true; 518 | break; 519 | 520 | case HalfFloatType: 521 | 522 | texture.encoding = LinearEncoding; 523 | texture.minFilter = LinearFilter; 524 | texture.magFilter = LinearFilter; 525 | texture.generateMipmaps = false; 526 | texture.flipY = true; 527 | break; 528 | 529 | } 530 | 531 | if ( onLoad ) onLoad( texture, texData ); 532 | 533 | } 534 | 535 | return DataTextureLoader.prototype.load.call( this, url, onLoadCallback, onProgress, onError ); 536 | 537 | } 538 | 539 | } ); 540 | 541 | return { RGBELoader }; 542 | } -------------------------------------------------------------------------------- /jsm/controls/TrackballControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Eberhard Graether / http://egraether.com/ 3 | * @author Mark Lundin / http://mark-lundin.com 4 | * @author Simone Manini / http://daron1337.github.io 5 | * @author Luca Antiga / http://lantiga.github.io 6 | */ 7 | export default function (THREE) { 8 | 9 | let { 10 | EventDispatcher, 11 | MOUSE, 12 | Quaternion, 13 | Vector2, 14 | Vector3, 15 | global: window, 16 | } = THREE; 17 | let document = window.document; 18 | 19 | 20 | var TrackballControls = function (object, domElement) { 21 | 22 | if (domElement === undefined) console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.'); 23 | if (domElement === document) console.error('THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.'); 24 | 25 | var _this = this; 26 | var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 27 | 28 | this.object = object; 29 | this.domElement = domElement; 30 | 31 | // API 32 | 33 | this.enabled = true; 34 | 35 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 36 | 37 | this.rotateSpeed = 1.0; 38 | this.zoomSpeed = 1.2; 39 | this.panSpeed = 0.3; 40 | 41 | this.noRotate = false; 42 | this.noZoom = false; 43 | this.noPan = false; 44 | 45 | this.staticMoving = false; 46 | this.dynamicDampingFactor = 0.2; 47 | 48 | this.minDistance = 0; 49 | this.maxDistance = Infinity; 50 | 51 | this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/]; 52 | 53 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.ZOOM, RIGHT: MOUSE.PAN }; 54 | 55 | // internals 56 | 57 | this.target = new Vector3(); 58 | 59 | var EPS = 0.000001; 60 | 61 | var lastPosition = new Vector3(); 62 | var lastZoom = 1; 63 | 64 | var _state = STATE.NONE, 65 | _keyState = STATE.NONE, 66 | 67 | _eye = new Vector3(), 68 | 69 | _movePrev = new Vector2(), 70 | _moveCurr = new Vector2(), 71 | 72 | _lastAxis = new Vector3(), 73 | _lastAngle = 0, 74 | 75 | _zoomStart = new Vector2(), 76 | _zoomEnd = new Vector2(), 77 | 78 | _touchZoomDistanceStart = 0, 79 | _touchZoomDistanceEnd = 0, 80 | 81 | _panStart = new Vector2(), 82 | _panEnd = new Vector2(); 83 | 84 | // for reset 85 | 86 | this.target0 = this.target.clone(); 87 | this.position0 = this.object.position.clone(); 88 | this.up0 = this.object.up.clone(); 89 | this.zoom0 = this.object.zoom; 90 | 91 | // events 92 | 93 | var changeEvent = { type: 'change' }; 94 | var startEvent = { type: 'start' }; 95 | var endEvent = { type: 'end' }; 96 | 97 | 98 | // methods 99 | 100 | this.handleResize = function () { 101 | 102 | var box = this.domElement.getBoundingClientRect(); 103 | // adjustments come from similar code in the jquery offset() function 104 | // var d = this.domElement.ownerDocument.documentElement; 105 | var d = document.documentElement; 106 | this.screen.left = box.left + window.scrollX - d.clientLeft; 107 | this.screen.top = box.top + window.scrollY - d.clientTop; 108 | this.screen.width = box.width; 109 | this.screen.height = box.height; 110 | 111 | }; 112 | 113 | var getMouseOnScreen = (function () { 114 | 115 | var vector = new Vector2(); 116 | 117 | return function getMouseOnScreen(pageX, pageY) { 118 | 119 | vector.set( 120 | (pageX - _this.screen.left) / _this.screen.width, 121 | (pageY - _this.screen.top) / _this.screen.height 122 | ); 123 | 124 | return vector; 125 | 126 | }; 127 | 128 | }()); 129 | 130 | var getMouseOnCircle = (function () { 131 | 132 | var vector = new Vector2(); 133 | 134 | return function getMouseOnCircle(pageX, pageY) { 135 | 136 | vector.set( 137 | ((pageX - _this.screen.width * 0.5 - _this.screen.left) / (_this.screen.width * 0.5)), 138 | ((_this.screen.height + 2 * (_this.screen.top - pageY)) / _this.screen.width) // screen.width intentional 139 | ); 140 | 141 | return vector; 142 | 143 | }; 144 | 145 | }()); 146 | 147 | this.rotateCamera = (function () { 148 | 149 | var axis = new Vector3(), 150 | quaternion = new Quaternion(), 151 | eyeDirection = new Vector3(), 152 | objectUpDirection = new Vector3(), 153 | objectSidewaysDirection = new Vector3(), 154 | moveDirection = new Vector3(), 155 | angle; 156 | 157 | return function rotateCamera() { 158 | 159 | moveDirection.set(_moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0); 160 | angle = moveDirection.length(); 161 | 162 | if (angle) { 163 | 164 | _eye.copy(_this.object.position).sub(_this.target); 165 | 166 | eyeDirection.copy(_eye).normalize(); 167 | objectUpDirection.copy(_this.object.up).normalize(); 168 | objectSidewaysDirection.crossVectors(objectUpDirection, eyeDirection).normalize(); 169 | 170 | objectUpDirection.setLength(_moveCurr.y - _movePrev.y); 171 | objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x); 172 | 173 | moveDirection.copy(objectUpDirection.add(objectSidewaysDirection)); 174 | 175 | axis.crossVectors(moveDirection, _eye).normalize(); 176 | 177 | angle *= _this.rotateSpeed; 178 | quaternion.setFromAxisAngle(axis, angle); 179 | 180 | _eye.applyQuaternion(quaternion); 181 | _this.object.up.applyQuaternion(quaternion); 182 | 183 | _lastAxis.copy(axis); 184 | _lastAngle = angle; 185 | 186 | } else if (!_this.staticMoving && _lastAngle) { 187 | 188 | _lastAngle *= Math.sqrt(1.0 - _this.dynamicDampingFactor); 189 | _eye.copy(_this.object.position).sub(_this.target); 190 | quaternion.setFromAxisAngle(_lastAxis, _lastAngle); 191 | _eye.applyQuaternion(quaternion); 192 | _this.object.up.applyQuaternion(quaternion); 193 | 194 | } 195 | 196 | _movePrev.copy(_moveCurr); 197 | 198 | }; 199 | 200 | }()); 201 | 202 | 203 | this.zoomCamera = function () { 204 | 205 | var factor; 206 | 207 | if (_state === STATE.TOUCH_ZOOM_PAN) { 208 | 209 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 210 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 211 | 212 | if (_this.object.isPerspectiveCamera) { 213 | 214 | _eye.multiplyScalar(factor); 215 | 216 | } else if (_this.object.isOrthographicCamera) { 217 | 218 | _this.object.zoom *= factor; 219 | _this.object.updateProjectionMatrix(); 220 | 221 | } else { 222 | 223 | console.warn('THREE.TrackballControls: Unsupported camera type'); 224 | 225 | } 226 | 227 | } else { 228 | 229 | factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed; 230 | 231 | if (factor !== 1.0 && factor > 0.0) { 232 | 233 | if (_this.object.isPerspectiveCamera) { 234 | 235 | _eye.multiplyScalar(factor); 236 | 237 | } else if (_this.object.isOrthographicCamera) { 238 | 239 | _this.object.zoom /= factor; 240 | _this.object.updateProjectionMatrix(); 241 | 242 | } else { 243 | 244 | console.warn('THREE.TrackballControls: Unsupported camera type'); 245 | 246 | } 247 | 248 | } 249 | 250 | if (_this.staticMoving) { 251 | 252 | _zoomStart.copy(_zoomEnd); 253 | 254 | } else { 255 | 256 | _zoomStart.y += (_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor; 257 | 258 | } 259 | 260 | } 261 | 262 | }; 263 | 264 | this.panCamera = (function () { 265 | 266 | var mouseChange = new Vector2(), 267 | objectUp = new Vector3(), 268 | pan = new Vector3(); 269 | 270 | return function panCamera() { 271 | 272 | mouseChange.copy(_panEnd).sub(_panStart); 273 | 274 | if (mouseChange.lengthSq()) { 275 | 276 | if (_this.object.isOrthographicCamera) { 277 | 278 | var scale_x = (_this.object.right - _this.object.left) / _this.object.zoom / _this.domElement.clientWidth; 279 | var scale_y = (_this.object.top - _this.object.bottom) / _this.object.zoom / _this.domElement.clientWidth; 280 | 281 | mouseChange.x *= scale_x; 282 | mouseChange.y *= scale_y; 283 | 284 | } 285 | 286 | mouseChange.multiplyScalar(_eye.length() * _this.panSpeed); 287 | 288 | pan.copy(_eye).cross(_this.object.up).setLength(mouseChange.x); 289 | pan.add(objectUp.copy(_this.object.up).setLength(mouseChange.y)); 290 | 291 | _this.object.position.add(pan); 292 | _this.target.add(pan); 293 | 294 | if (_this.staticMoving) { 295 | 296 | _panStart.copy(_panEnd); 297 | 298 | } else { 299 | 300 | _panStart.add(mouseChange.subVectors(_panEnd, _panStart).multiplyScalar(_this.dynamicDampingFactor)); 301 | 302 | } 303 | 304 | } 305 | 306 | }; 307 | 308 | }()); 309 | 310 | this.checkDistances = function () { 311 | 312 | if (!_this.noZoom || !_this.noPan) { 313 | 314 | if (_eye.lengthSq() > _this.maxDistance * _this.maxDistance) { 315 | 316 | _this.object.position.addVectors(_this.target, _eye.setLength(_this.maxDistance)); 317 | _zoomStart.copy(_zoomEnd); 318 | 319 | } 320 | 321 | if (_eye.lengthSq() < _this.minDistance * _this.minDistance) { 322 | 323 | _this.object.position.addVectors(_this.target, _eye.setLength(_this.minDistance)); 324 | _zoomStart.copy(_zoomEnd); 325 | 326 | } 327 | 328 | } 329 | 330 | }; 331 | 332 | this.update = function () { 333 | 334 | _eye.subVectors(_this.object.position, _this.target); 335 | 336 | if (!_this.noRotate) { 337 | 338 | _this.rotateCamera(); 339 | 340 | } 341 | 342 | if (!_this.noZoom) { 343 | 344 | _this.zoomCamera(); 345 | 346 | } 347 | 348 | if (!_this.noPan) { 349 | 350 | _this.panCamera(); 351 | 352 | } 353 | 354 | _this.object.position.addVectors(_this.target, _eye); 355 | 356 | if (_this.object.isPerspectiveCamera) { 357 | 358 | _this.checkDistances(); 359 | 360 | _this.object.lookAt(_this.target); 361 | 362 | if (lastPosition.distanceToSquared(_this.object.position) > EPS) { 363 | 364 | _this.dispatchEvent(changeEvent); 365 | 366 | lastPosition.copy(_this.object.position); 367 | 368 | } 369 | 370 | } else if (_this.object.isOrthographicCamera) { 371 | 372 | _this.object.lookAt(_this.target); 373 | 374 | if (lastPosition.distanceToSquared(_this.object.position) > EPS || lastZoom !== _this.object.zoom) { 375 | 376 | _this.dispatchEvent(changeEvent); 377 | 378 | lastPosition.copy(_this.object.position); 379 | lastZoom = _this.object.zoom; 380 | 381 | } 382 | 383 | } else { 384 | 385 | console.warn('THREE.TrackballControls: Unsupported camera type'); 386 | 387 | } 388 | 389 | }; 390 | 391 | this.reset = function () { 392 | 393 | _state = STATE.NONE; 394 | _keyState = STATE.NONE; 395 | 396 | _this.target.copy(_this.target0); 397 | _this.object.position.copy(_this.position0); 398 | _this.object.up.copy(_this.up0); 399 | _this.object.zoom = _this.zoom0; 400 | 401 | _this.object.updateProjectionMatrix(); 402 | 403 | _eye.subVectors(_this.object.position, _this.target); 404 | 405 | _this.object.lookAt(_this.target); 406 | 407 | _this.dispatchEvent(changeEvent); 408 | 409 | lastPosition.copy(_this.object.position); 410 | lastZoom = _this.object.zoom; 411 | 412 | }; 413 | 414 | // listeners 415 | 416 | function keydown(event) { 417 | 418 | if (_this.enabled === false) return; 419 | 420 | window.removeEventListener('keydown', keydown); 421 | 422 | if (_keyState !== STATE.NONE) { 423 | 424 | return; 425 | 426 | } else if (event.keyCode === _this.keys[STATE.ROTATE] && !_this.noRotate) { 427 | 428 | _keyState = STATE.ROTATE; 429 | 430 | } else if (event.keyCode === _this.keys[STATE.ZOOM] && !_this.noZoom) { 431 | 432 | _keyState = STATE.ZOOM; 433 | 434 | } else if (event.keyCode === _this.keys[STATE.PAN] && !_this.noPan) { 435 | 436 | _keyState = STATE.PAN; 437 | 438 | } 439 | 440 | } 441 | 442 | function keyup() { 443 | 444 | if (_this.enabled === false) return; 445 | 446 | _keyState = STATE.NONE; 447 | 448 | window.addEventListener('keydown', keydown, false); 449 | 450 | } 451 | 452 | function mousedown(event) { 453 | 454 | if (_this.enabled === false) return; 455 | 456 | event.preventDefault(); 457 | event.stopPropagation(); 458 | 459 | if (_state === STATE.NONE) { 460 | 461 | switch (event.button) { 462 | 463 | case _this.mouseButtons.LEFT: 464 | _state = STATE.ROTATE; 465 | break; 466 | 467 | case _this.mouseButtons.MIDDLE: 468 | _state = STATE.ZOOM; 469 | break; 470 | 471 | case _this.mouseButtons.RIGHT: 472 | _state = STATE.PAN; 473 | break; 474 | 475 | default: 476 | _state = STATE.NONE; 477 | 478 | } 479 | 480 | } 481 | 482 | var state = (_keyState !== STATE.NONE) ? _keyState : _state; 483 | 484 | if (state === STATE.ROTATE && !_this.noRotate) { 485 | 486 | _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); 487 | _movePrev.copy(_moveCurr); 488 | 489 | } else if (state === STATE.ZOOM && !_this.noZoom) { 490 | 491 | _zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY)); 492 | _zoomEnd.copy(_zoomStart); 493 | 494 | } else if (state === STATE.PAN && !_this.noPan) { 495 | 496 | _panStart.copy(getMouseOnScreen(event.pageX, event.pageY)); 497 | _panEnd.copy(_panStart); 498 | 499 | } 500 | 501 | document.addEventListener('mousemove', mousemove, false); 502 | document.addEventListener('mouseup', mouseup, false); 503 | 504 | _this.dispatchEvent(startEvent); 505 | 506 | } 507 | 508 | function mousemove(event) { 509 | 510 | if (_this.enabled === false) return; 511 | 512 | event.preventDefault(); 513 | event.stopPropagation(); 514 | 515 | var state = (_keyState !== STATE.NONE) ? _keyState : _state; 516 | 517 | if (state === STATE.ROTATE && !_this.noRotate) { 518 | 519 | _movePrev.copy(_moveCurr); 520 | _moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); 521 | 522 | } else if (state === STATE.ZOOM && !_this.noZoom) { 523 | 524 | _zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); 525 | 526 | } else if (state === STATE.PAN && !_this.noPan) { 527 | 528 | _panEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); 529 | 530 | } 531 | 532 | } 533 | 534 | function mouseup(event) { 535 | 536 | if (_this.enabled === false) return; 537 | 538 | event.preventDefault(); 539 | event.stopPropagation(); 540 | 541 | _state = STATE.NONE; 542 | 543 | document.removeEventListener('mousemove', mousemove); 544 | document.removeEventListener('mouseup', mouseup); 545 | _this.dispatchEvent(endEvent); 546 | 547 | } 548 | 549 | function mousewheel(event) { 550 | 551 | if (_this.enabled === false) return; 552 | 553 | if (_this.noZoom === true) return; 554 | 555 | event.preventDefault(); 556 | event.stopPropagation(); 557 | 558 | switch (event.deltaMode) { 559 | 560 | case 2: 561 | // Zoom in pages 562 | _zoomStart.y -= event.deltaY * 0.025; 563 | break; 564 | 565 | case 1: 566 | // Zoom in lines 567 | _zoomStart.y -= event.deltaY * 0.01; 568 | break; 569 | 570 | default: 571 | // undefined, 0, assume pixels 572 | _zoomStart.y -= event.deltaY * 0.00025; 573 | break; 574 | 575 | } 576 | 577 | _this.dispatchEvent(startEvent); 578 | _this.dispatchEvent(endEvent); 579 | 580 | } 581 | 582 | function touchstart(event) { 583 | 584 | if (_this.enabled === false) return; 585 | 586 | event.preventDefault(); 587 | 588 | switch (event.touches.length) { 589 | 590 | case 1: 591 | _state = STATE.TOUCH_ROTATE; 592 | _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); 593 | _movePrev.copy(_moveCurr); 594 | break; 595 | 596 | default: // 2 or more 597 | _state = STATE.TOUCH_ZOOM_PAN; 598 | var dx = event.touches[0].pageX - event.touches[1].pageX; 599 | var dy = event.touches[0].pageY - event.touches[1].pageY; 600 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy); 601 | 602 | var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; 603 | var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; 604 | _panStart.copy(getMouseOnScreen(x, y)); 605 | _panEnd.copy(_panStart); 606 | break; 607 | 608 | } 609 | 610 | _this.dispatchEvent(startEvent); 611 | 612 | } 613 | 614 | function touchmove(event) { 615 | 616 | if (_this.enabled === false) return; 617 | 618 | event.preventDefault(); 619 | event.stopPropagation(); 620 | 621 | switch (event.touches.length) { 622 | 623 | case 1: 624 | _movePrev.copy(_moveCurr); 625 | _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); 626 | break; 627 | 628 | default: // 2 or more 629 | var dx = event.touches[0].pageX - event.touches[1].pageX; 630 | var dy = event.touches[0].pageY - event.touches[1].pageY; 631 | _touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy); 632 | 633 | var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; 634 | var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; 635 | _panEnd.copy(getMouseOnScreen(x, y)); 636 | break; 637 | 638 | } 639 | 640 | } 641 | 642 | function touchend(event) { 643 | 644 | if (_this.enabled === false) return; 645 | 646 | switch (event.touches.length) { 647 | 648 | case 0: 649 | _state = STATE.NONE; 650 | break; 651 | 652 | case 1: 653 | _state = STATE.TOUCH_ROTATE; 654 | _moveCurr.copy(getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY)); 655 | _movePrev.copy(_moveCurr); 656 | break; 657 | 658 | } 659 | 660 | _this.dispatchEvent(endEvent); 661 | 662 | } 663 | 664 | function contextmenu(event) { 665 | 666 | if (_this.enabled === false) return; 667 | 668 | event.preventDefault(); 669 | 670 | } 671 | 672 | this.dispose = function () { 673 | 674 | this.domElement.removeEventListener('contextmenu', contextmenu, false); 675 | this.domElement.removeEventListener('mousedown', mousedown, false); 676 | this.domElement.removeEventListener('wheel', mousewheel, false); 677 | 678 | this.domElement.removeEventListener('touchstart', touchstart, false); 679 | this.domElement.removeEventListener('touchend', touchend, false); 680 | this.domElement.removeEventListener('touchmove', touchmove, false); 681 | 682 | document.removeEventListener('mousemove', mousemove, false); 683 | document.removeEventListener('mouseup', mouseup, false); 684 | 685 | window.removeEventListener('keydown', keydown, false); 686 | window.removeEventListener('keyup', keyup, false); 687 | 688 | }; 689 | 690 | this.domElement.addEventListener('contextmenu', contextmenu, false); 691 | this.domElement.addEventListener('mousedown', mousedown, false); 692 | this.domElement.addEventListener('wheel', mousewheel, false); 693 | 694 | this.domElement.addEventListener('touchstart', touchstart, false); 695 | this.domElement.addEventListener('touchend', touchend, false); 696 | this.domElement.addEventListener('touchmove', touchmove, false); 697 | 698 | window.addEventListener('keydown', keydown, false); 699 | window.addEventListener('keyup', keyup, false); 700 | 701 | this.handleResize(); 702 | 703 | // force an update at start 704 | this.update(); 705 | 706 | }; 707 | 708 | TrackballControls.prototype = Object.create(EventDispatcher.prototype); 709 | TrackballControls.prototype.constructor = TrackballControls; 710 | 711 | return { TrackballControls }; 712 | } -------------------------------------------------------------------------------- /jsm/loaders/OBJLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | export default function (THREE) { 6 | let { 7 | BufferGeometry, 8 | FileLoader, 9 | Float32BufferAttribute, 10 | Group, 11 | LineBasicMaterial, 12 | LineSegments, 13 | Loader, 14 | Material, 15 | Mesh, 16 | MeshPhongMaterial, 17 | NoColors, 18 | Points, 19 | PointsMaterial, 20 | VertexColors 21 | } = THREE; 22 | 23 | 24 | 25 | // o object_name | g group_name 26 | var object_pattern = /^[og]\s*(.+)?/; 27 | // mtllib file_reference 28 | var material_library_pattern = /^mtllib /; 29 | // usemtl material_name 30 | var material_use_pattern = /^usemtl /; 31 | 32 | function ParserState() { 33 | 34 | var state = { 35 | objects: [], 36 | object: {}, 37 | 38 | vertices: [], 39 | normals: [], 40 | colors: [], 41 | uvs: [], 42 | 43 | materialLibraries: [], 44 | 45 | startObject: function (name, fromDeclaration) { 46 | 47 | // If the current object (initial from reset) is not from a g/o declaration in the parsed 48 | // file. We need to use it for the first parsed g/o to keep things in sync. 49 | if (this.object && this.object.fromDeclaration === false) { 50 | 51 | this.object.name = name; 52 | this.object.fromDeclaration = (fromDeclaration !== false); 53 | return; 54 | 55 | } 56 | 57 | var previousMaterial = (this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined); 58 | 59 | if (this.object && typeof this.object._finalize === 'function') { 60 | 61 | this.object._finalize(true); 62 | 63 | } 64 | 65 | this.object = { 66 | name: name || '', 67 | fromDeclaration: (fromDeclaration !== false), 68 | 69 | geometry: { 70 | vertices: [], 71 | normals: [], 72 | colors: [], 73 | uvs: [] 74 | }, 75 | materials: [], 76 | smooth: true, 77 | 78 | startMaterial: function (name, libraries) { 79 | 80 | var previous = this._finalize(false); 81 | 82 | // New usemtl declaration overwrites an inherited material, except if faces were declared 83 | // after the material, then it must be preserved for proper MultiMaterial continuation. 84 | if (previous && (previous.inherited || previous.groupCount <= 0)) { 85 | 86 | this.materials.splice(previous.index, 1); 87 | 88 | } 89 | 90 | var material = { 91 | index: this.materials.length, 92 | name: name || '', 93 | mtllib: (Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : ''), 94 | smooth: (previous !== undefined ? previous.smooth : this.smooth), 95 | groupStart: (previous !== undefined ? previous.groupEnd : 0), 96 | groupEnd: - 1, 97 | groupCount: - 1, 98 | inherited: false, 99 | 100 | clone: function (index) { 101 | 102 | var cloned = { 103 | index: (typeof index === 'number' ? index : this.index), 104 | name: this.name, 105 | mtllib: this.mtllib, 106 | smooth: this.smooth, 107 | groupStart: 0, 108 | groupEnd: - 1, 109 | groupCount: - 1, 110 | inherited: false 111 | }; 112 | cloned.clone = this.clone.bind(cloned); 113 | return cloned; 114 | 115 | } 116 | }; 117 | 118 | this.materials.push(material); 119 | 120 | return material; 121 | 122 | }, 123 | 124 | currentMaterial: function () { 125 | 126 | if (this.materials.length > 0) { 127 | 128 | return this.materials[this.materials.length - 1]; 129 | 130 | } 131 | 132 | return undefined; 133 | 134 | }, 135 | 136 | _finalize: function (end) { 137 | 138 | var lastMultiMaterial = this.currentMaterial(); 139 | if (lastMultiMaterial && lastMultiMaterial.groupEnd === - 1) { 140 | 141 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; 142 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; 143 | lastMultiMaterial.inherited = false; 144 | 145 | } 146 | 147 | // Ignore objects tail materials if no face declarations followed them before a new o/g started. 148 | if (end && this.materials.length > 1) { 149 | 150 | for (var mi = this.materials.length - 1; mi >= 0; mi--) { 151 | 152 | if (this.materials[mi].groupCount <= 0) { 153 | 154 | this.materials.splice(mi, 1); 155 | 156 | } 157 | 158 | } 159 | 160 | } 161 | 162 | // Guarantee at least one empty material, this makes the creation later more straight forward. 163 | if (end && this.materials.length === 0) { 164 | 165 | this.materials.push({ 166 | name: '', 167 | smooth: this.smooth 168 | }); 169 | 170 | } 171 | 172 | return lastMultiMaterial; 173 | 174 | } 175 | }; 176 | 177 | // Inherit previous objects material. 178 | // Spec tells us that a declared material must be set to all objects until a new material is declared. 179 | // If a usemtl declaration is encountered while this new object is being parsed, it will 180 | // overwrite the inherited material. Exception being that there was already face declarations 181 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation. 182 | 183 | if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function') { 184 | 185 | var declared = previousMaterial.clone(0); 186 | declared.inherited = true; 187 | this.object.materials.push(declared); 188 | 189 | } 190 | 191 | this.objects.push(this.object); 192 | 193 | }, 194 | 195 | finalize: function () { 196 | 197 | if (this.object && typeof this.object._finalize === 'function') { 198 | 199 | this.object._finalize(true); 200 | 201 | } 202 | 203 | }, 204 | 205 | parseVertexIndex: function (value, len) { 206 | 207 | var index = parseInt(value, 10); 208 | return (index >= 0 ? index - 1 : index + len / 3) * 3; 209 | 210 | }, 211 | 212 | parseNormalIndex: function (value, len) { 213 | 214 | var index = parseInt(value, 10); 215 | return (index >= 0 ? index - 1 : index + len / 3) * 3; 216 | 217 | }, 218 | 219 | parseUVIndex: function (value, len) { 220 | 221 | var index = parseInt(value, 10); 222 | return (index >= 0 ? index - 1 : index + len / 2) * 2; 223 | 224 | }, 225 | 226 | addVertex: function (a, b, c) { 227 | 228 | var src = this.vertices; 229 | var dst = this.object.geometry.vertices; 230 | 231 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 232 | dst.push(src[b + 0], src[b + 1], src[b + 2]); 233 | dst.push(src[c + 0], src[c + 1], src[c + 2]); 234 | 235 | }, 236 | 237 | addVertexPoint: function (a) { 238 | 239 | var src = this.vertices; 240 | var dst = this.object.geometry.vertices; 241 | 242 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 243 | 244 | }, 245 | 246 | addVertexLine: function (a) { 247 | 248 | var src = this.vertices; 249 | var dst = this.object.geometry.vertices; 250 | 251 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 252 | 253 | }, 254 | 255 | addNormal: function (a, b, c) { 256 | 257 | var src = this.normals; 258 | var dst = this.object.geometry.normals; 259 | 260 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 261 | dst.push(src[b + 0], src[b + 1], src[b + 2]); 262 | dst.push(src[c + 0], src[c + 1], src[c + 2]); 263 | 264 | }, 265 | 266 | addColor: function (a, b, c) { 267 | 268 | var src = this.colors; 269 | var dst = this.object.geometry.colors; 270 | 271 | dst.push(src[a + 0], src[a + 1], src[a + 2]); 272 | dst.push(src[b + 0], src[b + 1], src[b + 2]); 273 | dst.push(src[c + 0], src[c + 1], src[c + 2]); 274 | 275 | }, 276 | 277 | addUV: function (a, b, c) { 278 | 279 | var src = this.uvs; 280 | var dst = this.object.geometry.uvs; 281 | 282 | dst.push(src[a + 0], src[a + 1]); 283 | dst.push(src[b + 0], src[b + 1]); 284 | dst.push(src[c + 0], src[c + 1]); 285 | 286 | }, 287 | 288 | addUVLine: function (a) { 289 | 290 | var src = this.uvs; 291 | var dst = this.object.geometry.uvs; 292 | 293 | dst.push(src[a + 0], src[a + 1]); 294 | 295 | }, 296 | 297 | addFace: function (a, b, c, ua, ub, uc, na, nb, nc) { 298 | 299 | var vLen = this.vertices.length; 300 | 301 | var ia = this.parseVertexIndex(a, vLen); 302 | var ib = this.parseVertexIndex(b, vLen); 303 | var ic = this.parseVertexIndex(c, vLen); 304 | 305 | this.addVertex(ia, ib, ic); 306 | 307 | if (this.colors.length > 0) { 308 | 309 | this.addColor(ia, ib, ic); 310 | 311 | } 312 | 313 | if (ua !== undefined && ua !== '') { 314 | 315 | var uvLen = this.uvs.length; 316 | ia = this.parseUVIndex(ua, uvLen); 317 | ib = this.parseUVIndex(ub, uvLen); 318 | ic = this.parseUVIndex(uc, uvLen); 319 | this.addUV(ia, ib, ic); 320 | 321 | } 322 | 323 | if (na !== undefined && na !== '') { 324 | 325 | // Normals are many times the same. If so, skip function call and parseInt. 326 | var nLen = this.normals.length; 327 | ia = this.parseNormalIndex(na, nLen); 328 | 329 | ib = na === nb ? ia : this.parseNormalIndex(nb, nLen); 330 | ic = na === nc ? ia : this.parseNormalIndex(nc, nLen); 331 | 332 | this.addNormal(ia, ib, ic); 333 | 334 | } 335 | 336 | }, 337 | 338 | addPointGeometry: function (vertices) { 339 | 340 | this.object.geometry.type = 'Points'; 341 | 342 | var vLen = this.vertices.length; 343 | 344 | for (var vi = 0, l = vertices.length; vi < l; vi++) { 345 | 346 | this.addVertexPoint(this.parseVertexIndex(vertices[vi], vLen)); 347 | 348 | } 349 | 350 | }, 351 | 352 | addLineGeometry: function (vertices, uvs) { 353 | 354 | this.object.geometry.type = 'Line'; 355 | 356 | var vLen = this.vertices.length; 357 | var uvLen = this.uvs.length; 358 | 359 | for (var vi = 0, l = vertices.length; vi < l; vi++) { 360 | 361 | this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen)); 362 | 363 | } 364 | 365 | for (var uvi = 0, l = uvs.length; uvi < l; uvi++) { 366 | 367 | this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen)); 368 | 369 | } 370 | 371 | } 372 | 373 | }; 374 | 375 | state.startObject('', false); 376 | 377 | return state; 378 | 379 | } 380 | 381 | // 382 | 383 | function OBJLoader(manager) { 384 | 385 | Loader.call(this, manager); 386 | 387 | this.materials = null; 388 | 389 | } 390 | 391 | OBJLoader.prototype = Object.assign(Object.create(Loader.prototype), { 392 | 393 | constructor: OBJLoader, 394 | 395 | load: function (url, onLoad, onProgress, onError) { 396 | 397 | var scope = this; 398 | 399 | var loader = new FileLoader(scope.manager); 400 | loader.setPath(this.path); 401 | loader.load(url, function (text) { 402 | 403 | onLoad(scope.parse(text)); 404 | 405 | }, onProgress, onError); 406 | 407 | }, 408 | 409 | setMaterials: function (materials) { 410 | 411 | this.materials = materials; 412 | 413 | return this; 414 | 415 | }, 416 | 417 | parse: function (text) { 418 | 419 | console.time('OBJLoader'); 420 | 421 | var state = new ParserState(); 422 | 423 | if (text.indexOf('\r\n') !== - 1) { 424 | 425 | // This is faster than String.split with regex that splits on both 426 | text = text.replace(/\r\n/g, '\n'); 427 | 428 | } 429 | 430 | if (text.indexOf('\\\n') !== - 1) { 431 | 432 | // join lines separated by a line continuation character (\) 433 | text = text.replace(/\\\n/g, ''); 434 | 435 | } 436 | 437 | var lines = text.split('\n'); 438 | var line = '', lineFirstChar = ''; 439 | var lineLength = 0; 440 | var result = []; 441 | 442 | // Faster to just trim left side of the line. Use if available. 443 | var trimLeft = (typeof ''.trimLeft === 'function'); 444 | 445 | for (var i = 0, l = lines.length; i < l; i++) { 446 | 447 | line = lines[i]; 448 | 449 | line = trimLeft ? line.trimLeft() : line.trim(); 450 | 451 | lineLength = line.length; 452 | 453 | if (lineLength === 0) continue; 454 | 455 | lineFirstChar = line.charAt(0); 456 | 457 | // @todo invoke passed in handler if any 458 | if (lineFirstChar === '#') continue; 459 | 460 | if (lineFirstChar === 'v') { 461 | // \f -> 匹配一个换页 462 | // \n -> 匹配一个换行符 463 | // \r -> 匹配一个回车符 464 | // \t -> 匹配一个制表符 465 | // \v -> 匹配一个垂直制表符 466 | // \s+ 表示匹配任意多个上面的字符 467 | var data = line.split(/\s+/); 468 | // var data = line.split(" "); 469 | 470 | switch (data[0]) { 471 | 472 | case 'v': 473 | state.vertices.push( 474 | parseFloat(data[1]), 475 | parseFloat(data[2]), 476 | parseFloat(data[3]) 477 | ); 478 | if (data.length >= 7) { 479 | 480 | state.colors.push( 481 | parseFloat(data[4]), 482 | parseFloat(data[5]), 483 | parseFloat(data[6]) 484 | 485 | ); 486 | 487 | } 488 | break; 489 | case 'vn': 490 | state.normals.push( 491 | parseFloat(data[1]), 492 | parseFloat(data[2]), 493 | parseFloat(data[3]) 494 | ); 495 | break; 496 | case 'vt': 497 | state.uvs.push( 498 | parseFloat(data[1]), 499 | parseFloat(data[2]) 500 | ); 501 | break; 502 | 503 | } 504 | 505 | } else if (lineFirstChar === 'f') { 506 | 507 | var lineData = line.substr(1).trim(); 508 | var vertexData = lineData.split(/\s+/); 509 | // var vertexData = lineData.split(" "); 510 | var faceVertices = []; 511 | 512 | // Parse the face vertex data into an easy to work with format 513 | 514 | for (var j = 0, jl = vertexData.length; j < jl; j++) { 515 | 516 | var vertex = vertexData[j]; 517 | 518 | if (vertex.length > 0) { 519 | 520 | var vertexParts = vertex.split('/'); 521 | faceVertices.push(vertexParts); 522 | 523 | } 524 | 525 | } 526 | 527 | // Draw an edge between the first vertex and all subsequent vertices to form an n-gon 528 | 529 | var v1 = faceVertices[0]; 530 | 531 | for (var j = 1, jl = faceVertices.length - 1; j < jl; j++) { 532 | 533 | var v2 = faceVertices[j]; 534 | var v3 = faceVertices[j + 1]; 535 | 536 | state.addFace( 537 | v1[0], v2[0], v3[0], 538 | v1[1], v2[1], v3[1], 539 | v1[2], v2[2], v3[2] 540 | ); 541 | 542 | } 543 | 544 | } else if (lineFirstChar === 'l') { 545 | 546 | var lineParts = line.substring(1).trim().split(" "); 547 | var lineVertices = [], lineUVs = []; 548 | 549 | if (line.indexOf("/") === - 1) { 550 | 551 | lineVertices = lineParts; 552 | 553 | } else { 554 | 555 | for (var li = 0, llen = lineParts.length; li < llen; li++) { 556 | 557 | var parts = lineParts[li].split("/"); 558 | 559 | if (parts[0] !== "") lineVertices.push(parts[0]); 560 | if (parts[1] !== "") lineUVs.push(parts[1]); 561 | 562 | } 563 | 564 | } 565 | state.addLineGeometry(lineVertices, lineUVs); 566 | 567 | } else if (lineFirstChar === 'p') { 568 | 569 | var lineData = line.substr(1).trim(); 570 | var pointData = lineData.split(" "); 571 | 572 | state.addPointGeometry(pointData); 573 | 574 | } else if ((result = object_pattern.exec(line)) !== null) { 575 | 576 | // o object_name 577 | // or 578 | // g group_name 579 | 580 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 581 | // var name = result[ 0 ].substr( 1 ).trim(); 582 | var name = (" " + result[0].substr(1).trim()).substr(1); 583 | 584 | state.startObject(name); 585 | 586 | } else if (material_use_pattern.test(line)) { 587 | 588 | // material 589 | 590 | state.object.startMaterial(line.substring(7).trim(), state.materialLibraries); 591 | 592 | } else if (material_library_pattern.test(line)) { 593 | 594 | // mtl file 595 | 596 | state.materialLibraries.push(line.substring(7).trim()); 597 | 598 | } else if (lineFirstChar === 's') { 599 | 600 | result = line.split(' '); 601 | 602 | // smooth shading 603 | 604 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry, 605 | // but does not define a usemtl for each face set. 606 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups). 607 | // This requires some care to not create extra material on each smooth value for "normal" obj files. 608 | // where explicit usemtl defines geometry groups. 609 | // Example asset: examples/models/obj/cerberus/Cerberus.obj 610 | 611 | /* 612 | * http://paulbourke.net/dataformats/obj/ 613 | * or 614 | * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf 615 | * 616 | * From chapter "Grouping" Syntax explanation "s group_number": 617 | * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. 618 | * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form 619 | * surfaces, smoothing groups are either turned on or off; there is no difference between values greater 620 | * than 0." 621 | */ 622 | if (result.length > 1) { 623 | 624 | var value = result[1].trim().toLowerCase(); 625 | state.object.smooth = (value !== '0' && value !== 'off'); 626 | 627 | } else { 628 | 629 | // ZBrush can produce "s" lines #11707 630 | state.object.smooth = true; 631 | 632 | } 633 | var material = state.object.currentMaterial(); 634 | if (material) material.smooth = state.object.smooth; 635 | 636 | } else { 637 | 638 | // Handle null terminated files without exception 639 | if (line === '\0') continue; 640 | 641 | throw new Error('THREE.OBJLoader: Unexpected line: "' + line + '"'); 642 | 643 | } 644 | 645 | } 646 | 647 | state.finalize(); 648 | 649 | var container = new Group(); 650 | container.materialLibraries = [].concat(state.materialLibraries); 651 | 652 | for (var i = 0, l = state.objects.length; i < l; i++) { 653 | 654 | var object = state.objects[i]; 655 | var geometry = object.geometry; 656 | var materials = object.materials; 657 | var isLine = (geometry.type === 'Line'); 658 | var isPoints = (geometry.type === 'Points'); 659 | var hasVertexColors = false; 660 | 661 | // Skip o/g line declarations that did not follow with any faces 662 | if (geometry.vertices.length === 0) continue; 663 | 664 | var buffergeometry = new BufferGeometry(); 665 | 666 | buffergeometry.setAttribute('position', new Float32BufferAttribute(geometry.vertices, 3)); 667 | 668 | if (geometry.normals.length > 0) { 669 | 670 | buffergeometry.setAttribute('normal', new Float32BufferAttribute(geometry.normals, 3)); 671 | 672 | } else { 673 | 674 | buffergeometry.computeVertexNormals(); 675 | 676 | } 677 | 678 | if (geometry.colors.length > 0) { 679 | 680 | hasVertexColors = true; 681 | buffergeometry.setAttribute('color', new Float32BufferAttribute(geometry.colors, 3)); 682 | 683 | } 684 | 685 | if (geometry.uvs.length > 0) { 686 | 687 | buffergeometry.setAttribute('uv', new Float32BufferAttribute(geometry.uvs, 2)); 688 | 689 | } 690 | 691 | // Create materials 692 | 693 | var createdMaterials = []; 694 | 695 | for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { 696 | 697 | var sourceMaterial = materials[mi]; 698 | var material = undefined; 699 | 700 | if (this.materials !== null) { 701 | 702 | material = this.materials.create(sourceMaterial.name); 703 | 704 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. 705 | if (isLine && material && !(material instanceof LineBasicMaterial)) { 706 | 707 | var materialLine = new LineBasicMaterial(); 708 | Material.prototype.copy.call(materialLine, material); 709 | materialLine.color.copy(material.color); 710 | material = materialLine; 711 | 712 | } else if (isPoints && material && !(material instanceof PointsMaterial)) { 713 | 714 | var materialPoints = new PointsMaterial({ size: 10, sizeAttenuation: false }); 715 | Material.prototype.copy.call(materialPoints, material); 716 | materialPoints.color.copy(material.color); 717 | materialPoints.map = material.map; 718 | material = materialPoints; 719 | 720 | } 721 | 722 | } 723 | 724 | if (!material) { 725 | 726 | if (isLine) { 727 | 728 | material = new LineBasicMaterial(); 729 | 730 | } else if (isPoints) { 731 | 732 | material = new PointsMaterial({ size: 1, sizeAttenuation: false }); 733 | 734 | } else { 735 | 736 | material = new MeshPhongMaterial(); 737 | 738 | } 739 | 740 | material.name = sourceMaterial.name; 741 | 742 | } 743 | 744 | material.flatShading = sourceMaterial.smooth ? false : true; 745 | material.vertexColors = hasVertexColors ? VertexColors : NoColors; 746 | 747 | createdMaterials.push(material); 748 | 749 | } 750 | 751 | // Create mesh 752 | 753 | var mesh; 754 | 755 | if (createdMaterials.length > 1) { 756 | 757 | for (var mi = 0, miLen = materials.length; mi < miLen; mi++) { 758 | 759 | var sourceMaterial = materials[mi]; 760 | buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi); 761 | 762 | } 763 | 764 | if (isLine) { 765 | 766 | mesh = new LineSegments(buffergeometry, createdMaterials); 767 | 768 | } else if (isPoints) { 769 | 770 | mesh = new Points(buffergeometry, createdMaterials); 771 | 772 | } else { 773 | 774 | mesh = new Mesh(buffergeometry, createdMaterials); 775 | 776 | } 777 | 778 | } else { 779 | 780 | if (isLine) { 781 | 782 | mesh = new LineSegments(buffergeometry, createdMaterials[0]); 783 | 784 | } else if (isPoints) { 785 | 786 | mesh = new Points(buffergeometry, createdMaterials[0]); 787 | 788 | } else { 789 | 790 | mesh = new Mesh(buffergeometry, createdMaterials[0]); 791 | 792 | } 793 | 794 | } 795 | 796 | mesh.name = object.name; 797 | 798 | container.add(mesh); 799 | 800 | } 801 | 802 | console.timeEnd('OBJLoader'); 803 | 804 | return container; 805 | 806 | } 807 | 808 | }); 809 | 810 | return OBJLoader; 811 | 812 | } 813 | --------------------------------------------------------------------------------