├── .gitignore
├── README.md
├── app.js
├── app.json
├── app.wxss
├── package.json
├── pages
├── index
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── lists
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
└── scene
│ ├── camera.js
│ └── scene.js
├── project.config.json
├── sitemap.json
└── utils
├── loadingManager.js
├── mtlLoader.js
├── objLoader.js
├── orbitControls.js
├── pointPick.js
├── requestAnimationFrame.js
├── threeutils.js
└── util.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.swp
3 | .project
4 | node_modules
5 | .idea/
6 | .vscode/
7 | npm-debug.log
8 | .jshintrc
9 | .vs/
10 | test/unit/three.*.unit.js
11 | miniprogram_npm
12 | package-lock.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wx_threeJs_project
2 | 微信小程序,使用three.js引擎,渲染场景
3 |
4 | 基于官方适配的threejs-miniprogram 兼容three.j框架
5 |
6 | 修改兼容Three.js 官方提供的ObjLoader.js
7 |
8 | 完成功能
9 | 1.导入显示obj文件
10 | 2.选种图中实体
11 | 3.给选中的实体更换材质
12 | 4.场景切换
13 |
14 |
15 |
16 | 
17 | 
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | //app.js
2 | App({
3 | onLaunch: function () {
4 | // 展示本地存储能力
5 | var logs = wx.getStorageSync('logs') || []
6 | logs.unshift(Date.now())
7 | wx.setStorageSync('logs', logs)
8 |
9 | // 登录
10 | wx.login({
11 | success: res => {
12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId
13 | }
14 | })
15 | wx.getSystemInfo({
16 | success:(res)=> {
17 | // 获取可使用窗口宽度
18 | this.globalData.height= res.windowHeight;
19 | // 获取可使用窗口高度
20 | this.globalData.width = res.windowWidth;
21 | }
22 | })
23 | },
24 | globalData: {
25 | height:0,width:0
26 | },
27 | THREE:null,
28 | Viewer:null,
29 | })
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": [
3 | "pages/lists/index",
4 | "pages/index/index"
5 | ],
6 | "window": {
7 | "backgroundTextStyle": "light",
8 | "navigationBarBackgroundColor": "#fff",
9 | "navigationBarTitleText": "WeChat",
10 | "navigationBarTextStyle": "black"
11 | },
12 | "style": "v2",
13 | "sitemapLocation": "sitemap.json"
14 | }
--------------------------------------------------------------------------------
/app.wxss:
--------------------------------------------------------------------------------
1 | page{
2 | height: 100%;
3 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lookcad",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "three": "^0.113.2",
13 | "threejs-miniprogram": "0.0.4"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/pages/index/index.js:
--------------------------------------------------------------------------------
1 | //index.js
2 | import {
3 | createScopedThreejs
4 | } from 'threejs-miniprogram'
5 |
6 | import Viewer from '../scene/scene.js';
7 | import { pointPick } from '../../utils/pointPick.js';
8 |
9 | //获取应用实例
10 | const app = getApp()
11 |
12 | Page({
13 | data: { isMove: false,showMtls:"none" },
14 | onLoad: function (option) {
15 | const eventChannel = this.getOpenerEventChannel();
16 | eventChannel.on('acceptData', function (data) {
17 | wx.createSelectorQuery().select('#canvas').node().exec((res) => {
18 | const canvas = res[0].node;
19 | let [width, height] = [app.globalData.width, app.globalData.height];
20 | canvas.width=width;
21 | canvas.height=height;
22 | const THREE = createScopedThreejs(canvas);
23 | const viewer = new Viewer();
24 | viewer.init(canvas, THREE);
25 | app.Viewer = viewer;
26 | app.THREE = THREE;
27 | if (data.url.endsWith(".mtl"))
28 | viewer.loadObjAndMtl(data.url);
29 | else
30 | viewer.loaderObj(data.url);
31 | });
32 | })
33 | },
34 | touchstart(e) {
35 | app.Viewer.controls.onTouchStart(e);
36 | },
37 | touchMove(e) {
38 | this.setData({ isMove: true });
39 | app.Viewer.controls.onTouchMove(e);
40 | },
41 | touchEnd(e) {
42 | if (!this.data.isMove) {
43 | let selectObjects = app.Viewer.selectObjects;
44 | let o = pointPick({ x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY });
45 | console.log(o);
46 | if (selectObjects.has(o)) {
47 | o.material.emissive = new app.THREE.Color(0x000000);
48 | selectObjects.delete(o)
49 | }
50 | else {
51 | if (o && o.material && o.material.type === "MeshPhongMaterial") {
52 | o.material.emissive = new app.THREE.Color(0x33C541);
53 | selectObjects.add(o);
54 | }
55 | }
56 | }
57 | app.Viewer.controls.onTouchEnd(e);
58 | this.setData({ isMove: false });
59 | },
60 | selectMtl(e){
61 | let viewer=app.Viewer;
62 | let url=e.target.dataset.url;
63 | if(!url) return;
64 | viewer.changeObjectsMaterial(url)
65 | },
66 | clearSelct(){
67 | app.Viewer.cancelSelect();
68 | },
69 | toggleShowMtls(){
70 | this.setData({
71 | showMtls:this.data.showMtls==="none"?"flex":"none"
72 | })
73 | },
74 | toggleSceneBackground(){
75 | let viewer=app.Viewer;
76 | viewer.loadSceneBg();
77 | },
78 | onHide() {
79 | app.Viewer.clear();
80 | }
81 | })
--------------------------------------------------------------------------------
/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {}
3 | }
--------------------------------------------------------------------------------
/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/pages/index/index.wxss:
--------------------------------------------------------------------------------
1 | #canvas{
2 | width:100%;
3 | height:100%;
4 | }
5 | .container{
6 | height: 100%;
7 | }
8 |
9 | .mtl-list{
10 | position: fixed;
11 | bottom: 10%;
12 | display: flex;
13 | overflow-x: scroll ;
14 | }
15 | .mtl-list>cover-imgage{
16 | width: 25%;
17 | height: 25%;
18 | }
19 |
20 | .clear-btn{
21 | position: fixed;
22 | left: 20rpx;
23 | bottom: 20rpx;
24 | display: flex;
25 | width: 100%;
26 | justify-content: space-between;
27 | }
--------------------------------------------------------------------------------
/pages/lists/index.js:
--------------------------------------------------------------------------------
1 | // pages/lists/index.js
2 | Page({
3 | data: {
4 | url:""
5 | },
6 | look(e){
7 | let url=e.target.dataset.url;
8 | if(!url)
9 | url=this.data.url;
10 | wx.navigateTo({
11 | url: "/pages/index/index",
12 | success:(res)=> {
13 | this.setData({url:""});
14 | res.eventChannel.emit('acceptData', { url })
15 | }
16 | });
17 | },
18 | change(e){
19 | this.setData({url:e.detail.value});
20 | },
21 | scanCode(){
22 | wx.scanCode({
23 | success: (res) => {
24 | let url=res.result;
25 | if(url&&url.startsWith("http")&&url.endsWith(".obj")){
26 | wx.navigateTo({
27 | url: "/pages/index/index",
28 | success:(res)=> {
29 | this.setData({url:""});
30 | res.eventChannel.emit('acceptData', { url})
31 | }
32 | });
33 | }
34 | },
35 | })
36 | },
37 | })
--------------------------------------------------------------------------------
/pages/lists/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {}
3 | }
--------------------------------------------------------------------------------
/pages/lists/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | 柜子
10 | 人
11 | haha
12 | male02mtl
13 | male02_dds
14 | wenli
15 | m2
16 | m22
17 |
18 |
19 |
--------------------------------------------------------------------------------
/pages/lists/index.wxss:
--------------------------------------------------------------------------------
1 | /* pages/lists/index.wxss */
2 | .container{
3 | padding: 30rpx;
4 | }
5 |
6 | .input{
7 | display: flex;
8 | align-content: center;
9 | align-items: center;
10 | align-self: center;
11 | height: 60rpx
12 | }
13 | .input>input{
14 | outline: 1px solid #ccc;
15 | height: 60rpx;
16 | width: 60%;
17 | }
18 | .input>button{
19 | flex: 1;
20 | height: 60rpx;
21 | line-height: 60rpx;
22 | font-size: 26rpx;
23 | padding: 0;
24 | }
--------------------------------------------------------------------------------
/pages/scene/camera.js:
--------------------------------------------------------------------------------
1 | const app=getApp();
2 | class Camera {
3 | constructor(THREE) {
4 | this.THREE=THREE;
5 | this.intance=null;
6 | this._size = 1.5;
7 | this.orthCamera=null;
8 | this.persCamera=null;
9 | this.init();
10 | }
11 | initOrthCamera() {
12 | const THREE=this.THREE;
13 | let [width, height] = [app.globalData.width, app.globalData.height];
14 | const aspect = height / width;
15 | const size = this._size;
16 | const camera = new THREE.OrthographicCamera(-size, size, size * aspect, -size * aspect, -1000, 1000);
17 | camera.position.set(-1, 1, 1);
18 | const target = new THREE.Vector3(0, 0, 0);
19 | camera.lookAt(target);
20 | this.orthCamera = camera;
21 | this.target=target;
22 | }
23 | initPeCamera(){
24 | const THREE=this.THREE;
25 | const fov = 75;
26 | let [width, height] = [app.globalData.width, app.globalData.height];
27 | const aspect = width / height;
28 | const near = 0.1;
29 | const far = 100;
30 | const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
31 | camera.position.z = 5;
32 | this.persCamera = camera;
33 | }
34 | init(){
35 | this.initOrthCamera();
36 | this.initPeCamera();
37 | this.intance=this.orthCamera;
38 | }
39 | }
40 | export default Camera;
--------------------------------------------------------------------------------
/pages/scene/scene.js:
--------------------------------------------------------------------------------
1 | import { getLoader } from '../../utils/objLoader.js';
2 | import Camera from './camera';
3 | import { GetOrbitControls } from '../../utils/orbitControls';
4 | import { getMtlLoader } from '../../utils/mtlLoader.js';
5 | import { LoadingManager } from '../../utils/loadingManager.js';
6 |
7 | const uv = "http://cdn.dodream.top/uv_grid_opengl.jpg?key=joelee";
8 | const app=getApp();
9 | class Viewer {
10 | constructor() {
11 | this.camera = null;
12 | this.scene = null;
13 | this.renderer = null;
14 | this.objLoader = null;
15 | this.textureLoader = null;
16 | this.mtlLoader=null;
17 | this.plane=null;
18 | this.selectObjects=new Set();
19 | }
20 | init(canvas, THREE) {
21 | this.THREE = THREE;
22 | this.canvas = canvas;
23 | this.initScene(THREE);
24 | this.initRenderer(canvas, THREE);
25 |
26 | let camera = new Camera(THREE);
27 |
28 | this.scene.add(camera.intance)
29 |
30 | this.camera = camera;
31 |
32 | this.initHelper(THREE);
33 |
34 | this.initPlane(THREE);
35 | this.initLight(THREE);
36 | this.initControl(THREE);
37 | this.initLoader(THREE);
38 |
39 | const ObjLoader = getLoader(THREE);
40 | let objLoader = new ObjLoader(THREE.DefaultLoadingManager)
41 | this.objLoader = objLoader;
42 |
43 | this.textureLoader = new THREE.TextureLoader();
44 |
45 | const MTLLoader =getMtlLoader(THREE);
46 |
47 | this.mtlLoader=new MTLLoader(new THREE.LoadingManager());
48 |
49 | // this.testScene(THREE);
50 | // this.loadSceneBg();
51 | this.animate();
52 | }
53 | initLoader(THREE){
54 | console.log(THREE);
55 | THREE.LoadingManager=LoadingManager;
56 | }
57 | initScene(THREE) {
58 | const scene = new THREE.Scene();
59 | scene.background = new THREE.Color(0xf0f0f0);
60 | this.scene = scene;
61 |
62 | }
63 | initRenderer(canvas, THREE) {
64 | let renderer = new THREE.WebGLRenderer({
65 | canvas,
66 | antialias: true
67 | });
68 | this.renderer = renderer;
69 | }
70 | initHelper(THREE) {
71 | const axesHelper = new THREE.AxesHelper(100);
72 | this.scene.add(axesHelper);
73 |
74 | var helper = new THREE.GridHelper(10, 20);
75 | helper.position.y = - 0.1;
76 | helper.material.opacity = 0.25;
77 | helper.material.transparent = true;
78 | this.scene.add(helper);
79 | }
80 | initPlane(THREE) {
81 | var planeGeometry = new THREE.PlaneBufferGeometry(10, 10);
82 | planeGeometry.rotateX(- Math.PI / 2);
83 | var planeMaterial = new THREE.MeshBasicMaterial({ transparent: true, opacity: 0.25 });
84 | var plane = new THREE.Mesh(planeGeometry, planeMaterial);
85 | plane.name="plane";
86 | this.plane=plane;
87 | this.scene.add(plane);
88 | }
89 | initLight(THREE) {
90 | this.scene.add(new THREE.AmbientLight(0xf0f0f0));
91 | }
92 | initControl(THREE) {
93 | const OrbitControls = GetOrbitControls(this.camera.intance, this.renderer.domElement, THREE);
94 | var controls = new OrbitControls(this.camera.intance, this.renderer.domElement);
95 | controls.target.set(0, 0, 0);
96 | controls.update();
97 | this.controls = controls;
98 | }
99 | loadSceneBg(){
100 |
101 | if(this.camera.intance===this.camera.orthCamera){
102 | const THREE=this.THREE;
103 | let texture=new THREE.CubeTextureLoader()
104 | .setPath( 'http://cdn.dodream.top/' )
105 | .load( [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ] );
106 | this.scene.background = texture;
107 | this.plane.visiable=false;
108 | }
109 | else{
110 | this.scene.background =new this.THREE.Color(0xf0f0f0);
111 | this.plane.visiable=true;
112 | }
113 | this.switchCamera();
114 |
115 | }
116 | switchCamera(){
117 | if(this.camera.intance===this.camera.orthCamera)
118 | this.camera.intance=this.camera.persCamera;
119 | else
120 | this.camera.intance=this.camera.orthCamera;
121 |
122 | this.controls.object=this.camera.intance;
123 | this.controls.update();
124 | }
125 | testScene(THREE) {
126 | let objLoader = this.objLoader;
127 | //http://cdn.dodream.top/haha.obj
128 |
129 | objLoader.load(
130 | // resource URL
131 | 'http://cdn.dodream.top/haha.obj',
132 | // called when resource is loaded
133 | (object) => {
134 | object.children.forEach(obj => {
135 | if (obj.material)
136 | obj.material.color = new THREE.Color("#000");
137 | });
138 | this.scene.add(object);
139 | },
140 | // called when loading is in progresses
141 | function (xhr) {
142 |
143 | console.log((xhr.loaded / xhr.total * 100) + '% loaded');
144 |
145 | },
146 | // called when loading has errors
147 | function (error) {
148 |
149 | console.log('An error happened');
150 |
151 | }
152 | );
153 | }
154 | loaderObj(url) {
155 | console.log(url);
156 | let objLoader = this.objLoader;
157 | const THREE = this.THREE;
158 |
159 | objLoader.load(
160 | // resource URL
161 | url,
162 | // called when resource is loaded
163 | (object) => {
164 | var texture = this.textureLoader.load(uv);
165 | texture.wrapS=THREE.RepeatWrapping;
166 | texture.wrapT=THREE.RepeatWrapping;
167 | texture.repeat.set(1,1);
168 | let hasMesh = false;
169 | object.traverse(function (child) {
170 | if (child.isMesh) {
171 | child.material.map = texture;
172 | hasMesh = true;
173 | }
174 |
175 | if (!hasMesh && child.isLineSegments)
176 | child.material.color = new THREE.Color("#000");
177 | });
178 | this.scene.add(object);
179 | },
180 | // called when loading is in progresses
181 | function (xhr) {
182 |
183 | console.log((xhr.loaded / xhr.total * 100) + '% loaded');
184 |
185 | },
186 | // called when loading has errors
187 | function (error) {
188 |
189 | console.log('An error happened');
190 |
191 | }
192 | );
193 | }
194 | loadObjAndMtl(mtlUrl){
195 | let objLoader = this.objLoader;
196 | let url=mtlUrl.replace(".mtl",".obj");
197 | let mtlLoader=this.mtlLoader;
198 | let self=this;
199 | mtlLoader
200 | .load( mtlUrl, function ( materials ) {
201 | materials.preload();
202 | objLoader
203 | .setMaterials( materials )
204 | .load( url, ( object )=> {
205 | self.scene.add( object );
206 | });
207 | } );
208 | }
209 | changeObjectsMaterial(url){
210 | var texture = this.textureLoader.load(url);
211 | texture.wrapS=this.THREE.RepeatWrapping;
212 | texture.wrapT=this.THREE.RepeatWrapping;
213 | texture.repeat.set(1,1);
214 |
215 | this.selectObjects.forEach(o=>{
216 | o.material.map=texture;
217 | });
218 |
219 | }
220 | cancelSelect(){
221 | this.selectObjects.forEach(o=>{
222 | o.material.emissive = new this.THREE.Color(0x000000);
223 | });
224 | this.selectObjects.clear();
225 | }
226 | clear(obj) {
227 | if (!obj)
228 | obj = this.scene;
229 |
230 | for (let o of obj.children) {
231 | if (o.geometry)
232 | o.geometry.dispose();
233 | this.clear(o);
234 | o.parent = null;
235 | o.dispatchEvent({ type: "removed" });
236 | }
237 | obj.children.length = 0;
238 | }
239 | render() {
240 | this.renderer.render(this.scene, this.camera.intance);
241 | }
242 | animate() {
243 | this.canvas.requestAnimationFrame(this.animate.bind(this));
244 | this.controls.update();
245 | this.render();
246 | }
247 | }
248 |
249 | export default Viewer;
--------------------------------------------------------------------------------
/project.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "项目配置文件",
3 | "packOptions": {
4 | "ignore": []
5 | },
6 | "setting": {
7 | "urlCheck": false,
8 | "es6": true,
9 | "postcss": true,
10 | "minified": true,
11 | "newFeature": true,
12 | "coverView": true,
13 | "nodeModules": true,
14 | "autoAudits": false,
15 | "showShadowRootInWxmlPanel": true,
16 | "scopeDataCheck": false,
17 | "checkInvalidKey": true,
18 | "checkSiteMap": true,
19 | "uploadWithSourceMap": true,
20 | "babelSetting": {
21 | "ignore": [],
22 | "disablePlugins": [],
23 | "outputPath": ""
24 | }
25 | },
26 | "compileType": "miniprogram",
27 | "libVersion": "2.10.3",
28 | "appid": "wx4dd9d1cf3942532d",
29 | "projectname": "lookcad",
30 | "debugOptions": {
31 | "hidedInDevtools": []
32 | },
33 | "isGameTourist": false,
34 | "simulatorType": "wechat",
35 | "simulatorPluginLibVersion": {},
36 | "condition": {
37 | "search": {
38 | "current": -1,
39 | "list": []
40 | },
41 | "conversation": {
42 | "current": -1,
43 | "list": []
44 | },
45 | "game": {
46 | "currentL": -1,
47 | "list": []
48 | },
49 | "miniprogram": {
50 | "current": -1,
51 | "list": []
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/sitemap.json:
--------------------------------------------------------------------------------
1 | {
2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
3 | "rules": [{
4 | "action": "allow",
5 | "page": "*"
6 | }]
7 | }
--------------------------------------------------------------------------------
/utils/loadingManager.js:
--------------------------------------------------------------------------------
1 | export function LoadingManager( onLoad, onProgress, onError ) {
2 |
3 | var scope = this;
4 |
5 | var isLoading = false;
6 | var itemsLoaded = 0;
7 | var itemsTotal = 0;
8 | var urlModifier = undefined;
9 | var handlers = [];
10 |
11 | // Refer to #5689 for the reason why we don't set .onStart
12 | // in the constructor
13 |
14 | this.onStart = undefined;
15 | this.onLoad = onLoad;
16 | this.onProgress = onProgress;
17 | this.onError = onError;
18 |
19 | this.itemStart = function ( url ) {
20 |
21 | itemsTotal ++;
22 |
23 | if ( isLoading === false ) {
24 |
25 | if ( scope.onStart !== undefined ) {
26 |
27 | scope.onStart( url, itemsLoaded, itemsTotal );
28 |
29 | }
30 |
31 | }
32 |
33 | isLoading = true;
34 |
35 | };
36 |
37 | this.itemEnd = function ( url ) {
38 |
39 | itemsLoaded ++;
40 |
41 | if ( scope.onProgress !== undefined ) {
42 |
43 | scope.onProgress( url, itemsLoaded, itemsTotal );
44 |
45 | }
46 |
47 | if ( itemsLoaded === itemsTotal ) {
48 |
49 | isLoading = false;
50 |
51 | if ( scope.onLoad !== undefined ) {
52 |
53 | scope.onLoad();
54 |
55 | }
56 |
57 | }
58 |
59 | };
60 |
61 | this.itemError = function ( url ) {
62 |
63 | if ( scope.onError !== undefined ) {
64 |
65 | scope.onError( url );
66 |
67 | }
68 |
69 | };
70 |
71 | this.resolveURL = function ( url ) {
72 |
73 | if ( urlModifier ) {
74 |
75 | return urlModifier( url );
76 |
77 | }
78 |
79 | return url;
80 |
81 | };
82 |
83 | this.setURLModifier = function ( transform ) {
84 |
85 | urlModifier = transform;
86 |
87 | return this;
88 |
89 | };
90 |
91 | this.addHandler = function ( regex, loader ) {
92 |
93 | handlers.push( regex, loader );
94 |
95 | return this;
96 |
97 | };
98 |
99 | this.removeHandler = function ( regex ) {
100 |
101 | var index = handlers.indexOf( regex );
102 |
103 | if ( index !== - 1 ) {
104 |
105 | handlers.splice( index, 2 );
106 |
107 | }
108 |
109 | return this;
110 |
111 | };
112 |
113 | this.getHandler = function ( file ) {
114 |
115 | for ( var i = 0, l = handlers.length; i < l; i += 2 ) {
116 |
117 | var regex = handlers[ i ];
118 | var loader = handlers[ i + 1 ];
119 |
120 | if ( regex.global ) regex.lastIndex = 0; // see #17920
121 |
122 | if ( regex.test( file ) ) {
123 |
124 | return loader;
125 |
126 | }
127 |
128 | }
129 |
130 | return null;
131 |
132 | };
133 |
134 | }
--------------------------------------------------------------------------------
/utils/mtlLoader.js:
--------------------------------------------------------------------------------
1 | export function getMtlLoader(THREE) {
2 | /**
3 | * Loads a Wavefront .mtl file specifying materials
4 | *
5 | * @author angelxuanchang
6 | */
7 | function MTLLoader(manager) {
8 |
9 | THREE.Loader.call(this, manager);
10 |
11 | };
12 |
13 | MTLLoader.prototype = Object.assign(Object.create(THREE.Loader.prototype), {
14 |
15 | constructor: MTLLoader,
16 |
17 | /**
18 | * Loads and parses a MTL asset from a URL.
19 | *
20 | * @param {String} url - URL to the MTL file.
21 | * @param {Function} [onLoad] - Callback invoked with the loaded object.
22 | * @param {Function} [onProgress] - Callback for download progress.
23 | * @param {Function} [onError] - Callback for download errors.
24 | *
25 | * @see setPath setResourcePath
26 | *
27 | * @note In order for relative texture references to resolve correctly
28 | * you must call setResourcePath() explicitly prior to load.
29 | */
30 | load: function (url, onLoad, onProgress, onError) {
31 |
32 | var scope = this;
33 |
34 | var path = (this.path === '') ? THREE.LoaderUtils.extractUrlBase(url) : this.path;
35 |
36 | var loader = new THREE.FileLoader(this.manager);
37 | loader.setPath(this.path);
38 | loader.load(url, function (text) {
39 |
40 | onLoad(scope.parse(text, path));
41 |
42 | }, onProgress, onError);
43 |
44 | },
45 |
46 | setMaterialOptions: function (value) {
47 |
48 | this.materialOptions = value;
49 | return this;
50 |
51 | },
52 |
53 | /**
54 | * Parses a MTL file.
55 | *
56 | * @param {String} text - Content of MTL file
57 | * @return {THREE.MTLLoader.MaterialCreator}
58 | *
59 | * @see setPath setResourcePath
60 | *
61 | * @note In order for relative texture references to resolve correctly
62 | * you must call setResourcePath() explicitly prior to parse.
63 | */
64 | parse: function (text, path) {
65 |
66 | var lines = text.split('\n');
67 | var info = {};
68 | var delimiter_pattern = /\s+/;
69 | var materialsInfo = {};
70 |
71 | for (var i = 0; i < lines.length; i++) {
72 |
73 | var line = lines[i];
74 | line = line.trim();
75 |
76 | if (line.length === 0 || line.charAt(0) === '#') {
77 |
78 | // Blank line or comment ignore
79 | continue;
80 |
81 | }
82 |
83 | var pos = line.indexOf(' ');
84 |
85 | var key = (pos >= 0) ? line.substring(0, pos) : line;
86 | key = key.toLowerCase();
87 |
88 | var value = (pos >= 0) ? line.substring(pos + 1) : '';
89 | value = value.trim();
90 |
91 | if (key === 'newmtl') {
92 |
93 | // New material
94 |
95 | info = { name: value };
96 | materialsInfo[value] = info;
97 |
98 | } else {
99 |
100 | if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') {
101 |
102 | var ss = value.split(delimiter_pattern, 3);
103 | info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])];
104 |
105 | } else {
106 |
107 | info[key] = value;
108 |
109 | }
110 |
111 | }
112 |
113 | }
114 |
115 | var materialCreator = new MTLLoader.MaterialCreator(this.resourcePath || path, this.materialOptions);
116 | materialCreator.setCrossOrigin(this.crossOrigin);
117 | materialCreator.setManager(this.manager);
118 | materialCreator.setMaterials(materialsInfo);
119 | return materialCreator;
120 |
121 | }
122 |
123 | });
124 |
125 | /**
126 | * Create a new THREE.MTLLoader.MaterialCreator
127 | * @param baseUrl - Url relative to which textures are loaded
128 | * @param options - Set of options on how to construct the materials
129 | * side: Which side to apply the material
130 | * THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide
131 | * wrap: What type of wrapping to apply for textures
132 | * THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping
133 | * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255
134 | * Default: false, assumed to be already normalized
135 | * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's
136 | * Default: false
137 | * @constructor
138 | */
139 |
140 | MTLLoader.MaterialCreator = function (baseUrl, options) {
141 |
142 | this.baseUrl = baseUrl || '';
143 | this.options = options;
144 | this.materialsInfo = {};
145 | this.materials = {};
146 | this.materialsArray = [];
147 | this.nameLookup = {};
148 |
149 | this.side = (this.options && this.options.side) ? this.options.side : THREE.FrontSide;
150 | this.wrap = (this.options && this.options.wrap) ? this.options.wrap : THREE.RepeatWrapping;
151 |
152 | };
153 |
154 | MTLLoader.MaterialCreator.prototype = {
155 |
156 | constructor: MTLLoader.MaterialCreator,
157 |
158 | crossOrigin: 'anonymous',
159 |
160 | setCrossOrigin: function (value) {
161 |
162 | this.crossOrigin = value;
163 | return this;
164 |
165 | },
166 |
167 | setManager: function (value) {
168 |
169 | this.manager = value;
170 |
171 | },
172 |
173 | setMaterials: function (materialsInfo) {
174 |
175 | this.materialsInfo = this.convert(materialsInfo);
176 | this.materials = {};
177 | this.materialsArray = [];
178 | this.nameLookup = {};
179 |
180 | },
181 |
182 | convert: function (materialsInfo) {
183 |
184 | if (!this.options) return materialsInfo;
185 |
186 | var converted = {};
187 |
188 | for (var mn in materialsInfo) {
189 |
190 | // Convert materials info into normalized form based on options
191 |
192 | var mat = materialsInfo[mn];
193 |
194 | var covmat = {};
195 |
196 | converted[mn] = covmat;
197 |
198 | for (var prop in mat) {
199 |
200 | var save = true;
201 | var value = mat[prop];
202 | var lprop = prop.toLowerCase();
203 |
204 | switch (lprop) {
205 |
206 | case 'kd':
207 | case 'ka':
208 | case 'ks':
209 |
210 | // Diffuse color (color under white light) using RGB values
211 |
212 | if (this.options && this.options.normalizeRGB) {
213 |
214 | value = [value[0] / 255, value[1] / 255, value[2] / 255];
215 |
216 | }
217 |
218 | if (this.options && this.options.ignoreZeroRGBs) {
219 |
220 | if (value[0] === 0 && value[1] === 0 && value[2] === 0) {
221 |
222 | // ignore
223 |
224 | save = false;
225 |
226 | }
227 |
228 | }
229 |
230 | break;
231 |
232 | default:
233 |
234 | break;
235 |
236 | }
237 |
238 | if (save) {
239 |
240 | covmat[lprop] = value;
241 |
242 | }
243 |
244 | }
245 |
246 | }
247 |
248 | return converted;
249 |
250 | },
251 |
252 | preload: function () {
253 |
254 | for (var mn in this.materialsInfo) {
255 |
256 | this.create(mn);
257 |
258 | }
259 |
260 | },
261 |
262 | getIndex: function (materialName) {
263 |
264 | return this.nameLookup[materialName];
265 |
266 | },
267 |
268 | getAsArray: function () {
269 |
270 | var index = 0;
271 |
272 | for (var mn in this.materialsInfo) {
273 |
274 | this.materialsArray[index] = this.create(mn);
275 | this.nameLookup[mn] = index;
276 | index++;
277 |
278 | }
279 |
280 | return this.materialsArray;
281 |
282 | },
283 |
284 | create: function (materialName) {
285 |
286 | if (this.materials[materialName] === undefined) {
287 |
288 | this.createMaterial_(materialName);
289 |
290 | }
291 |
292 | return this.materials[materialName];
293 |
294 | },
295 |
296 | createMaterial_: function (materialName) {
297 |
298 | // Create material
299 |
300 | var scope = this;
301 | var mat = this.materialsInfo[materialName];
302 | var params = {
303 |
304 | name: materialName,
305 | side: this.side
306 |
307 | };
308 |
309 | function resolveURL(baseUrl, url) {
310 |
311 | if (typeof url !== 'string' || url === '')
312 | return '';
313 |
314 | // Absolute URL
315 | if (/^https?:\/\//i.test(url)) return url;
316 |
317 | return baseUrl + url;
318 |
319 | }
320 |
321 | function setMapForType(mapType, value) {
322 |
323 | if (params[mapType]) return; // Keep the first encountered texture
324 |
325 | var texParams = scope.getTextureParams(value, params);
326 | var map = scope.loadTexture(resolveURL(scope.baseUrl, texParams.url));
327 |
328 | map.repeat.copy(texParams.scale);
329 | map.offset.copy(texParams.offset);
330 |
331 | map.wrapS = scope.wrap;
332 | map.wrapT = scope.wrap;
333 |
334 | params[mapType] = map;
335 |
336 | }
337 |
338 | for (var prop in mat) {
339 |
340 | var value = mat[prop];
341 | var n;
342 |
343 | if (value === '') continue;
344 |
345 | switch (prop.toLowerCase()) {
346 |
347 | // Ns is material specular exponent
348 |
349 | case 'kd':
350 |
351 | // Diffuse color (color under white light) using RGB values
352 |
353 | params.color = new THREE.Color().fromArray(value);
354 |
355 | break;
356 |
357 | case 'ks':
358 |
359 | // Specular color (color when light is reflected from shiny surface) using RGB values
360 | params.specular = new THREE.Color().fromArray(value);
361 |
362 | break;
363 |
364 | case 'ke':
365 |
366 | // Emissive using RGB values
367 | params.emissive = new THREE.Color().fromArray(value);
368 |
369 | break;
370 |
371 | case 'map_kd':
372 |
373 | // Diffuse texture map
374 |
375 | setMapForType("map", value);
376 |
377 | break;
378 |
379 | case 'map_ks':
380 |
381 | // Specular map
382 |
383 | setMapForType("specularMap", value);
384 |
385 | break;
386 |
387 | case 'map_ke':
388 |
389 | // Emissive map
390 |
391 | setMapForType("emissiveMap", value);
392 |
393 | break;
394 |
395 | case 'norm':
396 |
397 | setMapForType("normalMap", value);
398 |
399 | break;
400 |
401 | case 'map_bump':
402 | case 'bump':
403 |
404 | // Bump texture map
405 |
406 | setMapForType("bumpMap", value);
407 |
408 | break;
409 |
410 | case 'map_d':
411 |
412 | // Alpha map
413 |
414 | setMapForType("alphaMap", value);
415 | params.transparent = true;
416 |
417 | break;
418 |
419 | case 'ns':
420 |
421 | // The specular exponent (defines the focus of the specular highlight)
422 | // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
423 |
424 | params.shininess = parseFloat(value);
425 |
426 | break;
427 |
428 | case 'd':
429 | n = parseFloat(value);
430 |
431 | if (n < 1) {
432 |
433 | params.opacity = n;
434 | params.transparent = true;
435 |
436 | }
437 |
438 | break;
439 |
440 | case 'tr':
441 | n = parseFloat(value);
442 |
443 | if (this.options && this.options.invertTrProperty) n = 1 - n;
444 |
445 | if (n > 0) {
446 |
447 | params.opacity = 1 - n;
448 | params.transparent = true;
449 |
450 | }
451 |
452 | break;
453 |
454 | default:
455 | break;
456 |
457 | }
458 |
459 | }
460 |
461 | this.materials[materialName] = new THREE.MeshPhongMaterial(params);
462 | return this.materials[materialName];
463 |
464 | },
465 |
466 | getTextureParams: function (value, matParams) {
467 |
468 | var texParams = {
469 |
470 | scale: new THREE.Vector2(1, 1),
471 | offset: new THREE.Vector2(0, 0)
472 |
473 | };
474 |
475 | var items = value.split(/\s+/);
476 | var pos;
477 |
478 | pos = items.indexOf('-bm');
479 |
480 | if (pos >= 0) {
481 |
482 | matParams.bumpScale = parseFloat(items[pos + 1]);
483 | items.splice(pos, 2);
484 |
485 | }
486 |
487 | pos = items.indexOf('-s');
488 |
489 | if (pos >= 0) {
490 |
491 | texParams.scale.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2]));
492 | items.splice(pos, 4); // we expect 3 parameters here!
493 |
494 | }
495 |
496 | pos = items.indexOf('-o');
497 |
498 | if (pos >= 0) {
499 |
500 | texParams.offset.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2]));
501 | items.splice(pos, 4); // we expect 3 parameters here!
502 |
503 | }
504 |
505 | texParams.url = items.join(' ').trim();
506 | return texParams;
507 |
508 | },
509 |
510 | loadTexture: function (url, mapping, onLoad, onProgress, onError) {
511 |
512 | var texture;
513 | var manager = (this.manager !== undefined) ? this.manager : THREE.DefaultLoadingManager;
514 | var loader = manager.getHandler(url);
515 |
516 | if (loader === null) {
517 |
518 | loader = new THREE.TextureLoader(manager);
519 |
520 | }
521 |
522 | if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin);
523 | texture = loader.load(url, onLoad, onProgress, onError);
524 |
525 | if (mapping !== undefined) texture.mapping = mapping;
526 |
527 | return texture;
528 |
529 | }
530 |
531 | };
532 | return MTLLoader;
533 |
534 | }
--------------------------------------------------------------------------------
/utils/objLoader.js:
--------------------------------------------------------------------------------
1 | export function getLoader(THREE) {
2 | const {
3 | BufferGeometry,
4 | FileLoader,
5 | Float32BufferAttribute,
6 | Group,
7 | LineBasicMaterial,
8 | LineSegments,
9 | Loader,
10 | Material,
11 | Mesh,
12 | MeshPhongMaterial,
13 | Points,
14 | PointsMaterial
15 | } = THREE;
16 |
17 | BufferGeometry.prototype.setAttribute = function (name, attribute) {
18 | this.attributes[name] = attribute;
19 | return this;
20 | }
21 | BufferGeometry.prototype.getAttribute = function (namee) {
22 | return this.attributes[name];
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 | // usemap map_name
32 | var map_use_pattern = /^usemap /;
33 |
34 | function ParserState() {
35 |
36 | var state = {
37 | objects: [],
38 | object: {},
39 |
40 | vertices: [],
41 | normals: [],
42 | colors: [],
43 | uvs: [],
44 |
45 | materials: {},
46 | materialLibraries: [],
47 |
48 | startObject: function (name, fromDeclaration) {
49 |
50 | // If the current object (initial from reset) is not from a g/o declaration in the parsed
51 | // file. We need to use it for the first parsed g/o to keep things in sync.
52 | if (this.object && this.object.fromDeclaration === false) {
53 |
54 | this.object.name = name;
55 | this.object.fromDeclaration = (fromDeclaration !== false);
56 | return;
57 |
58 | }
59 |
60 | var previousMaterial = (this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined);
61 |
62 | if (this.object && typeof this.object._finalize === 'function') {
63 |
64 | this.object._finalize(true);
65 |
66 | }
67 |
68 | this.object = {
69 | name: name || '',
70 | fromDeclaration: (fromDeclaration !== false),
71 |
72 | geometry: {
73 | vertices: [],
74 | normals: [],
75 | colors: [],
76 | uvs: []
77 | },
78 | materials: [],
79 | smooth: true,
80 |
81 | startMaterial: function (name, libraries) {
82 |
83 | var previous = this._finalize(false);
84 |
85 | // New usemtl declaration overwrites an inherited material, except if faces were declared
86 | // after the material, then it must be preserved for proper MultiMaterial continuation.
87 | if (previous && (previous.inherited || previous.groupCount <= 0)) {
88 |
89 | this.materials.splice(previous.index, 1);
90 |
91 | }
92 |
93 | var material = {
94 | index: this.materials.length,
95 | name: name || '',
96 | mtllib: (Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : ''),
97 | smooth: (previous !== undefined ? previous.smooth : this.smooth),
98 | groupStart: (previous !== undefined ? previous.groupEnd : 0),
99 | groupEnd: - 1,
100 | groupCount: - 1,
101 | inherited: false,
102 |
103 | clone: function (index) {
104 |
105 | var cloned = {
106 | index: (typeof index === 'number' ? index : this.index),
107 | name: this.name,
108 | mtllib: this.mtllib,
109 | smooth: this.smooth,
110 | groupStart: 0,
111 | groupEnd: - 1,
112 | groupCount: - 1,
113 | inherited: false
114 | };
115 | cloned.clone = this.clone.bind(cloned);
116 | return cloned;
117 |
118 | }
119 | };
120 |
121 | this.materials.push(material);
122 |
123 | return material;
124 |
125 | },
126 |
127 | currentMaterial: function () {
128 |
129 | if (this.materials.length > 0) {
130 |
131 | return this.materials[this.materials.length - 1];
132 |
133 | }
134 |
135 | return undefined;
136 |
137 | },
138 |
139 | _finalize: function (end) {
140 |
141 | var lastMultiMaterial = this.currentMaterial();
142 | if (lastMultiMaterial && lastMultiMaterial.groupEnd === - 1) {
143 |
144 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
145 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
146 | lastMultiMaterial.inherited = false;
147 |
148 | }
149 |
150 | // Ignore objects tail materials if no face declarations followed them before a new o/g started.
151 | if (end && this.materials.length > 1) {
152 |
153 | for (var mi = this.materials.length - 1; mi >= 0; mi--) {
154 |
155 | if (this.materials[mi].groupCount <= 0) {
156 |
157 | this.materials.splice(mi, 1);
158 |
159 | }
160 |
161 | }
162 |
163 | }
164 |
165 | // Guarantee at least one empty material, this makes the creation later more straight forward.
166 | if (end && this.materials.length === 0) {
167 |
168 | this.materials.push({
169 | name: '',
170 | smooth: this.smooth
171 | });
172 |
173 | }
174 |
175 | return lastMultiMaterial;
176 |
177 | }
178 | };
179 |
180 | // Inherit previous objects material.
181 | // Spec tells us that a declared material must be set to all objects until a new material is declared.
182 | // If a usemtl declaration is encountered while this new object is being parsed, it will
183 | // overwrite the inherited material. Exception being that there was already face declarations
184 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation.
185 |
186 | if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function') {
187 |
188 | var declared = previousMaterial.clone(0);
189 | declared.inherited = true;
190 | this.object.materials.push(declared);
191 |
192 | }
193 |
194 | this.objects.push(this.object);
195 |
196 | },
197 |
198 | finalize: function () {
199 |
200 | if (this.object && typeof this.object._finalize === 'function') {
201 |
202 | this.object._finalize(true);
203 |
204 | }
205 |
206 | },
207 |
208 | parseVertexIndex: function (value, len) {
209 |
210 | var index = parseInt(value, 10);
211 | return (index >= 0 ? index - 1 : index + len / 3) * 3;
212 |
213 | },
214 |
215 | parseNormalIndex: function (value, len) {
216 |
217 | var index = parseInt(value, 10);
218 | return (index >= 0 ? index - 1 : index + len / 3) * 3;
219 |
220 | },
221 |
222 | parseUVIndex: function (value, len) {
223 |
224 | var index = parseInt(value, 10);
225 | return (index >= 0 ? index - 1 : index + len / 2) * 2;
226 |
227 | },
228 |
229 | addVertex: function (a, b, c) {
230 |
231 | var src = this.vertices;
232 | var dst = this.object.geometry.vertices;
233 |
234 | dst.push(src[a + 0], src[a + 1], src[a + 2]);
235 | dst.push(src[b + 0], src[b + 1], src[b + 2]);
236 | dst.push(src[c + 0], src[c + 1], src[c + 2]);
237 |
238 | },
239 |
240 | addVertexPoint: function (a) {
241 |
242 | var src = this.vertices;
243 | var dst = this.object.geometry.vertices;
244 |
245 | dst.push(src[a + 0], src[a + 1], src[a + 2]);
246 |
247 | },
248 |
249 | addVertexLine: function (a) {
250 |
251 | var src = this.vertices;
252 | var dst = this.object.geometry.vertices;
253 |
254 | dst.push(src[a + 0], src[a + 1], src[a + 2]);
255 |
256 | },
257 |
258 | addNormal: function (a, b, c) {
259 |
260 | var src = this.normals;
261 | var dst = this.object.geometry.normals;
262 |
263 | dst.push(src[a + 0], src[a + 1], src[a + 2]);
264 | dst.push(src[b + 0], src[b + 1], src[b + 2]);
265 | dst.push(src[c + 0], src[c + 1], src[c + 2]);
266 |
267 | },
268 |
269 | addColor: function (a, b, c) {
270 |
271 | var src = this.colors;
272 | var dst = this.object.geometry.colors;
273 |
274 | dst.push(src[a + 0], src[a + 1], src[a + 2]);
275 | dst.push(src[b + 0], src[b + 1], src[b + 2]);
276 | dst.push(src[c + 0], src[c + 1], src[c + 2]);
277 |
278 | },
279 |
280 | addUV: function (a, b, c) {
281 |
282 | var src = this.uvs;
283 | var dst = this.object.geometry.uvs;
284 |
285 | dst.push(src[a + 0], src[a + 1]);
286 | dst.push(src[b + 0], src[b + 1]);
287 | dst.push(src[c + 0], src[c + 1]);
288 |
289 | },
290 |
291 | addUVLine: function (a) {
292 |
293 | var src = this.uvs;
294 | var dst = this.object.geometry.uvs;
295 |
296 | dst.push(src[a + 0], src[a + 1]);
297 |
298 | },
299 |
300 | addFace: function (a, b, c, ua, ub, uc, na, nb, nc) {
301 |
302 | var vLen = this.vertices.length;
303 |
304 | var ia = this.parseVertexIndex(a, vLen);
305 | var ib = this.parseVertexIndex(b, vLen);
306 | var ic = this.parseVertexIndex(c, vLen);
307 |
308 | this.addVertex(ia, ib, ic);
309 |
310 | if (this.colors.length > 0) {
311 |
312 | this.addColor(ia, ib, ic);
313 |
314 | }
315 |
316 | if (ua !== undefined && ua !== '') {
317 |
318 | var uvLen = this.uvs.length;
319 | ia = this.parseUVIndex(ua, uvLen);
320 | ib = this.parseUVIndex(ub, uvLen);
321 | ic = this.parseUVIndex(uc, uvLen);
322 | this.addUV(ia, ib, ic);
323 |
324 | }
325 |
326 | if (na !== undefined && na !== '') {
327 |
328 | // Normals are many times the same. If so, skip function call and parseInt.
329 | var nLen = this.normals.length;
330 | ia = this.parseNormalIndex(na, nLen);
331 |
332 | ib = na === nb ? ia : this.parseNormalIndex(nb, nLen);
333 | ic = na === nc ? ia : this.parseNormalIndex(nc, nLen);
334 |
335 | this.addNormal(ia, ib, ic);
336 |
337 | }
338 |
339 | },
340 |
341 | addPointGeometry: function (vertices) {
342 |
343 | this.object.geometry.type = 'Points';
344 |
345 | var vLen = this.vertices.length;
346 |
347 | for (var vi = 0, l = vertices.length; vi < l; vi++) {
348 |
349 | this.addVertexPoint(this.parseVertexIndex(vertices[vi], vLen));
350 |
351 | }
352 |
353 | },
354 |
355 | addLineGeometry: function (vertices, uvs) {
356 |
357 | this.object.geometry.type = 'Line';
358 |
359 | var vLen = this.vertices.length;
360 | var uvLen = this.uvs.length;
361 |
362 | for (var vi = 0, l = vertices.length; vi < l; vi++) {
363 |
364 | this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen));
365 |
366 | }
367 |
368 | for (var uvi = 0, l = uvs.length; uvi < l; uvi++) {
369 |
370 | this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen));
371 |
372 | }
373 |
374 | }
375 |
376 | };
377 |
378 | state.startObject('', false);
379 |
380 | return state;
381 |
382 | }
383 |
384 | //
385 |
386 | function OBJLoader(manager) {
387 |
388 | Loader.call(this, manager);
389 |
390 | this.materials = null;
391 |
392 | }
393 |
394 | OBJLoader.prototype = Object.assign(Object.create(Loader.prototype), {
395 |
396 | constructor: OBJLoader,
397 |
398 | load: function (url, onLoad, onProgress, onError) {
399 |
400 | var scope = this;
401 |
402 | var loader = new FileLoader(scope.manager);
403 | loader.setPath(this.path);
404 | loader.load(url, function (text) {
405 |
406 | onLoad(scope.parse(text));
407 |
408 | }, onProgress, onError);
409 |
410 | },
411 |
412 | setMaterials: function (materials) {
413 |
414 | this.materials = materials;
415 |
416 | return this;
417 |
418 | },
419 |
420 | parse: function (text) {
421 |
422 | var state = new ParserState();
423 |
424 | if (text.indexOf('\r\n') !== - 1) {
425 |
426 | // This is faster than String.split with regex that splits on both
427 | text = text.replace(/\r\n/g, '\n');
428 |
429 | }
430 |
431 | if (text.indexOf('\\\n') !== - 1) {
432 |
433 | // join lines separated by a line continuation character (\)
434 | text = text.replace(/\\\n/g, '');
435 |
436 | }
437 |
438 | var lines = text.split('\n');
439 | var line = '', lineFirstChar = '';
440 | var lineLength = 0;
441 | var result = [];
442 |
443 | // Faster to just trim left side of the line. Use if available.
444 | var trimLeft = (typeof ''.trimLeft === 'function');
445 |
446 | for (var i = 0, l = lines.length; i < l; i++) {
447 |
448 | line = lines[i];
449 |
450 | line = trimLeft ? line.trimLeft() : line.trim();
451 |
452 | lineLength = line.length;
453 |
454 | if (lineLength === 0) continue;
455 |
456 | lineFirstChar = line.charAt(0);
457 |
458 | // @todo invoke passed in handler if any
459 | if (lineFirstChar === '#') continue;
460 |
461 | if (lineFirstChar === 'v') {
462 |
463 | var data = line.split(/\s+/);
464 |
465 | switch (data[0]) {
466 |
467 | case 'v':
468 | state.vertices.push(
469 | parseFloat(data[1]),
470 | parseFloat(data[2]),
471 | parseFloat(data[3])
472 | );
473 | if (data.length >= 7) {
474 |
475 | state.colors.push(
476 | parseFloat(data[4]),
477 | parseFloat(data[5]),
478 | parseFloat(data[6])
479 |
480 | );
481 |
482 | }
483 | break;
484 | case 'vn':
485 | state.normals.push(
486 | parseFloat(data[1]),
487 | parseFloat(data[2]),
488 | parseFloat(data[3])
489 | );
490 | break;
491 | case 'vt':
492 | state.uvs.push(
493 | parseFloat(data[1]),
494 | parseFloat(data[2])
495 | );
496 | break;
497 |
498 | }
499 |
500 | } else if (lineFirstChar === 'f') {
501 |
502 | var lineData = line.substr(1).trim();
503 | var vertexData = lineData.split(/\s+/);
504 | var faceVertices = [];
505 |
506 | // Parse the face vertex data into an easy to work with format
507 |
508 | for (var j = 0, jl = vertexData.length; j < jl; j++) {
509 |
510 | var vertex = vertexData[j];
511 |
512 | if (vertex.length > 0) {
513 |
514 | var vertexParts = vertex.split('/');
515 | faceVertices.push(vertexParts);
516 |
517 | }
518 |
519 | }
520 |
521 | // Draw an edge between the first vertex and all subsequent vertices to form an n-gon
522 |
523 | var v1 = faceVertices[0];
524 |
525 | for (var j = 1, jl = faceVertices.length - 1; j < jl; j++) {
526 |
527 | var v2 = faceVertices[j];
528 | var v3 = faceVertices[j + 1];
529 |
530 | state.addFace(
531 | v1[0], v2[0], v3[0],
532 | v1[1], v2[1], v3[1],
533 | v1[2], v2[2], v3[2]
534 | );
535 |
536 | }
537 |
538 | } else if (lineFirstChar === 'l') {
539 |
540 | var lineParts = line.substring(1).trim().split(" ");
541 | var lineVertices = [], lineUVs = [];
542 |
543 | if (line.indexOf("/") === - 1) {
544 |
545 | lineVertices = lineParts;
546 |
547 | } else {
548 |
549 | for (var li = 0, llen = lineParts.length; li < llen; li++) {
550 |
551 | var parts = lineParts[li].split("/");
552 |
553 | if (parts[0] !== "") lineVertices.push(parts[0]);
554 | if (parts[1] !== "") lineUVs.push(parts[1]);
555 |
556 | }
557 |
558 | }
559 | state.addLineGeometry(lineVertices, lineUVs);
560 |
561 | } else if (lineFirstChar === 'p') {
562 |
563 | var lineData = line.substr(1).trim();
564 | var pointData = lineData.split(" ");
565 |
566 | state.addPointGeometry(pointData);
567 |
568 | } else if ((result = object_pattern.exec(line)) !== null) {
569 |
570 | // o object_name
571 | // or
572 | // g group_name
573 |
574 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
575 | // var name = result[ 0 ].substr( 1 ).trim();
576 | var name = (" " + result[0].substr(1).trim()).substr(1);
577 |
578 | state.startObject(name);
579 |
580 | } else if (material_use_pattern.test(line)) {
581 |
582 | // material
583 |
584 | state.object.startMaterial(line.substring(7).trim(), state.materialLibraries);
585 |
586 | } else if (material_library_pattern.test(line)) {
587 |
588 | // mtl file
589 |
590 | state.materialLibraries.push(line.substring(7).trim());
591 |
592 | } else if (map_use_pattern.test(line)) {
593 |
594 | // the line is parsed but ignored since the loader assumes textures are defined MTL files
595 | // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method)
596 |
597 | console.warn('THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.');
598 |
599 | } else if (lineFirstChar === 's') {
600 |
601 | result = line.split(' ');
602 |
603 | // smooth shading
604 |
605 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry,
606 | // but does not define a usemtl for each face set.
607 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups).
608 | // This requires some care to not create extra material on each smooth value for "normal" obj files.
609 | // where explicit usemtl defines geometry groups.
610 | // Example asset: examples/models/obj/cerberus/Cerberus.obj
611 |
612 | /*
613 | * http://paulbourke.net/dataformats/obj/
614 | * or
615 | * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf
616 | *
617 | * From chapter "Grouping" Syntax explanation "s group_number":
618 | * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
619 | * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
620 | * surfaces, smoothing groups are either turned on or off; there is no difference between values greater
621 | * than 0."
622 | */
623 | if (result.length > 1) {
624 |
625 | var value = result[1].trim().toLowerCase();
626 | state.object.smooth = (value !== '0' && value !== 'off');
627 |
628 | } else {
629 |
630 | // ZBrush can produce "s" lines #11707
631 | state.object.smooth = true;
632 |
633 | }
634 | var material = state.object.currentMaterial();
635 | if (material) material.smooth = state.object.smooth;
636 |
637 | } else {
638 |
639 | // Handle null terminated files without exception
640 | if (line === '\0') continue;
641 |
642 | console.warn('THREE.OBJLoader: Unexpected line: "' + line + '"');
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 materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors;
699 | var material = state.materials[materialHash];
700 |
701 | if (this.materials !== null) {
702 |
703 | material = this.materials.create(sourceMaterial.name);
704 |
705 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
706 | if (isLine && material && !(material instanceof LineBasicMaterial)) {
707 |
708 | var materialLine = new LineBasicMaterial();
709 | Material.prototype.copy.call(materialLine, material);
710 | materialLine.color.copy(material.color);
711 | material = materialLine;
712 |
713 | } else if (isPoints && material && !(material instanceof PointsMaterial)) {
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 === undefined) {
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 | material.flatShading = sourceMaterial.smooth ? false : true;
742 | material.vertexColors = hasVertexColors;
743 |
744 | state.materials[materialHash] = material;
745 |
746 | }
747 |
748 | createdMaterials.push(material);
749 |
750 | }
751 |
752 | // Create mesh
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 | let mtl=createdMaterials[0].clone();
791 |
792 | if(mtl.map){
793 | mtl.map.wrapS=THREE.MirroredRepeatWrapping;
794 | mtl.map.wrapT=THREE.MirroredRepeatWrapping;
795 | }
796 | mesh = new Mesh(buffergeometry,mtl);
797 |
798 | }
799 |
800 | }
801 |
802 | mesh.name = object.name;
803 |
804 | container.add(mesh);
805 |
806 | }
807 |
808 | return container;
809 |
810 | }
811 |
812 | });
813 |
814 | return OBJLoader;
815 |
816 | }
--------------------------------------------------------------------------------
/utils/orbitControls.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export function GetOrbitControls(object, domElement, THREE){
4 | const {
5 | EventDispatcher,
6 | MOUSE,
7 | Quaternion,
8 | Spherical,
9 | TOUCH,
10 | Vector2,
11 | Vector3
12 | } = THREE;
13 | function OrbitControls(object, domElement) {
14 | if (domElement === undefined) console.warn('THREE.OrbitControls: The second parameter "domElement" is now mandatory.');
15 | if (domElement === document) console.error('THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.');
16 |
17 | this.object = object;
18 | this.domElement = domElement;
19 |
20 | // Set to false to disable this control
21 | this.enabled = true;
22 |
23 | // "target" sets the location of focus, where the object orbits around
24 | this.target = new Vector3();
25 |
26 | // How far you can dolly in and out ( PerspectiveCamera only )
27 | this.minDistance = 0;
28 | this.maxDistance = Infinity;
29 |
30 | // How far you can zoom in and out ( OrthographicCamera only )
31 | this.minZoom = 0;
32 | this.maxZoom = Infinity;
33 |
34 | // How far you can orbit vertically, upper and lower limits.
35 | // Range is 0 to Math.PI radians.
36 | this.minPolarAngle = 0; // radians
37 | this.maxPolarAngle = Math.PI; // radians
38 |
39 | // How far you can orbit horizontally, upper and lower limits.
40 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
41 | this.minAzimuthAngle = - Infinity; // radians
42 | this.maxAzimuthAngle = Infinity; // radians
43 |
44 | // Set to true to enable damping (inertia)
45 | // If damping is enabled, you must call controls.update() in your animation loop
46 | this.enableDamping = false;
47 | this.dampingFactor = 0.05;
48 |
49 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
50 | // Set to false to disable zooming
51 | this.enableZoom = true;
52 | this.zoomSpeed = 1.0;
53 |
54 | // Set to false to disable rotating
55 | this.enableRotate = true;
56 | this.rotateSpeed = 1.0;
57 |
58 | // Set to false to disable panning
59 | this.enablePan = true;
60 | this.panSpeed = 1.0;
61 | this.screenSpacePanning = false; // if true, pan in screen-space
62 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push
63 |
64 | // Set to true to automatically rotate around the target
65 | // If auto-rotate is enabled, you must call controls.update() in your animation loop
66 | this.autoRotate = false;
67 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
68 |
69 | // Set to false to disable use of the keys
70 | this.enableKeys = true;
71 |
72 | // The four arrow keys
73 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
74 |
75 | // Mouse buttons
76 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
77 |
78 | // Touch fingers
79 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
80 |
81 | // for reset
82 | this.target0 = this.target.clone();
83 | this.position0 = this.object.position.clone();
84 | this.zoom0 = this.object.zoom;
85 |
86 | //
87 | // public methods
88 | //
89 |
90 | this.getPolarAngle = function () {
91 |
92 | return spherical.phi;
93 |
94 | };
95 |
96 | this.getAzimuthalAngle = function () {
97 |
98 | return spherical.theta;
99 |
100 | };
101 |
102 | this.saveState = function () {
103 |
104 | scope.target0.copy(scope.target);
105 | scope.position0.copy(scope.object.position);
106 | scope.zoom0 = scope.object.zoom;
107 |
108 | };
109 |
110 | this.reset = function () {
111 |
112 | scope.target.copy(scope.target0);
113 | scope.object.position.copy(scope.position0);
114 | scope.object.zoom = scope.zoom0;
115 |
116 | scope.object.updateProjectionMatrix();
117 | scope.dispatchEvent(changeEvent);
118 |
119 | scope.update();
120 |
121 | state = STATE.NONE;
122 |
123 | };
124 |
125 | // this method is exposed, but perhaps it would be better if we can make it private...
126 | this.update = function () {
127 |
128 | var offset = new Vector3();
129 |
130 | // so camera.up is the orbit axis
131 | var quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0));
132 | var quatInverse = quat.clone().inverse();
133 |
134 | var lastPosition = new Vector3();
135 | var lastQuaternion = new Quaternion();
136 |
137 | return function update() {
138 |
139 | var position = scope.object.position;
140 |
141 | offset.copy(position).sub(scope.target);
142 |
143 | // rotate offset to "y-axis-is-up" space
144 | offset.applyQuaternion(quat);
145 |
146 | // angle from z-axis around y-axis
147 | spherical.setFromVector3(offset);
148 |
149 | if (scope.autoRotate && state === STATE.NONE) {
150 |
151 | rotateLeft(getAutoRotationAngle());
152 |
153 | }
154 |
155 | if (scope.enableDamping) {
156 |
157 | spherical.theta += sphericalDelta.theta * scope.dampingFactor;
158 | spherical.phi += sphericalDelta.phi * scope.dampingFactor;
159 |
160 | } else {
161 |
162 | spherical.theta += sphericalDelta.theta;
163 | spherical.phi += sphericalDelta.phi;
164 |
165 | }
166 |
167 | // restrict theta to be between desired limits
168 | spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta));
169 |
170 | // restrict phi to be between desired limits
171 | spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi));
172 |
173 | spherical.makeSafe();
174 |
175 |
176 | spherical.radius *= scale;
177 |
178 | // restrict radius to be between desired limits
179 | spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius));
180 |
181 | // move target to panned location
182 |
183 | if (scope.enableDamping === true) {
184 |
185 | scope.target.addScaledVector(panOffset, scope.dampingFactor);
186 |
187 | } else {
188 |
189 | scope.target.add(panOffset);
190 |
191 | }
192 |
193 | offset.setFromSpherical(spherical);
194 |
195 | // rotate offset back to "camera-up-vector-is-up" space
196 | offset.applyQuaternion(quatInverse);
197 |
198 | position.copy(scope.target).add(offset);
199 |
200 | scope.object.lookAt(scope.target);
201 |
202 | if (scope.enableDamping === true) {
203 |
204 | sphericalDelta.theta *= (1 - scope.dampingFactor);
205 | sphericalDelta.phi *= (1 - scope.dampingFactor);
206 |
207 | panOffset.multiplyScalar(1 - scope.dampingFactor);
208 |
209 | } else {
210 |
211 | sphericalDelta.set(0, 0, 0);
212 |
213 | panOffset.set(0, 0, 0);
214 |
215 | }
216 |
217 | scale = 1;
218 |
219 | // update condition is:
220 | // min(camera displacement, camera rotation in radians)^2 > EPS
221 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8
222 |
223 | if (zoomChanged ||
224 | lastPosition.distanceToSquared(scope.object.position) > EPS ||
225 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) {
226 |
227 | scope.dispatchEvent(changeEvent);
228 |
229 | lastPosition.copy(scope.object.position);
230 | lastQuaternion.copy(scope.object.quaternion);
231 | zoomChanged = false;
232 |
233 | return true;
234 |
235 | }
236 |
237 | return false;
238 |
239 | };
240 |
241 | }();
242 |
243 | this.dispose = function () {
244 |
245 | // scope.domElement.removeEventListener('contextmenu', onContextMenu, false);
246 | // scope.domElement.removeEventListener('mousedown', onMouseDown, false);
247 | // scope.domElement.removeEventListener('wheel', onMouseWheel, false);
248 |
249 | // scope.domElement.removeEventListener('touchstart', onTouchStart, false);
250 | // scope.domElement.removeEventListener('touchend', onTouchEnd, false);
251 | // scope.domElement.removeEventListener('touchmove', onTouchMove, false);
252 |
253 | // document.removeEventListener('mousemove', onMouseMove, false);
254 | // document.removeEventListener('mouseup', onMouseUp, false);
255 |
256 | // scope.domElement.removeEventListener('keydown', onKeyDown, false);
257 |
258 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
259 |
260 | };
261 |
262 | //
263 | // internals
264 | //
265 |
266 | var scope = this;
267 |
268 | var changeEvent = { type: 'change' };
269 | var startEvent = { type: 'start' };
270 | var endEvent = { type: 'end' };
271 |
272 | var STATE = {
273 | NONE: - 1,
274 | ROTATE: 0,
275 | DOLLY: 1,
276 | PAN: 2,
277 | TOUCH_ROTATE: 3,
278 | TOUCH_PAN: 4,
279 | TOUCH_DOLLY_PAN: 5,
280 | TOUCH_DOLLY_ROTATE: 6
281 | };
282 |
283 | var state = STATE.NONE;
284 |
285 | var EPS = 0.000001;
286 |
287 | // current position in spherical coordinates
288 | var spherical = new Spherical();
289 | var sphericalDelta = new Spherical();
290 |
291 | var scale = 1;
292 | var panOffset = new Vector3();
293 | var zoomChanged = false;
294 |
295 | var rotateStart = new Vector2();
296 | var rotateEnd = new Vector2();
297 | var rotateDelta = new Vector2();
298 |
299 | var panStart = new Vector2();
300 | var panEnd = new Vector2();
301 | var panDelta = new Vector2();
302 |
303 | var dollyStart = new Vector2();
304 | var dollyEnd = new Vector2();
305 | var dollyDelta = new Vector2();
306 |
307 | function getAutoRotationAngle() {
308 |
309 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
310 |
311 | }
312 |
313 | function getZoomScale() {
314 |
315 | return Math.pow(0.95, scope.zoomSpeed);
316 |
317 | }
318 |
319 | function rotateLeft(angle) {
320 |
321 | sphericalDelta.theta -= angle;
322 |
323 | }
324 |
325 | function rotateUp(angle) {
326 |
327 | sphericalDelta.phi -= angle;
328 |
329 | }
330 |
331 | var panLeft = function () {
332 |
333 | var v = new Vector3();
334 |
335 | return function panLeft(distance, objectMatrix) {
336 |
337 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix
338 | v.multiplyScalar(- distance);
339 |
340 | panOffset.add(v);
341 |
342 | };
343 |
344 | }();
345 |
346 | var panUp = function () {
347 |
348 | var v = new Vector3();
349 |
350 | return function panUp(distance, objectMatrix) {
351 |
352 | if (scope.screenSpacePanning === true) {
353 |
354 | v.setFromMatrixColumn(objectMatrix, 1);
355 |
356 | } else {
357 |
358 | v.setFromMatrixColumn(objectMatrix, 0);
359 | v.crossVectors(scope.object.up, v);
360 |
361 | }
362 |
363 | v.multiplyScalar(distance);
364 |
365 | panOffset.add(v);
366 |
367 | };
368 |
369 | }();
370 |
371 | // deltaX and deltaY are in pixels; right and down are positive
372 | var pan = function () {
373 |
374 | var offset = new Vector3();
375 |
376 | return function pan(deltaX, deltaY) {
377 |
378 | var element = scope.domElement;
379 |
380 | if (scope.object.isPerspectiveCamera) {
381 |
382 | // perspective
383 | var position = scope.object.position;
384 | offset.copy(position).sub(scope.target);
385 | var targetDistance = offset.length();
386 |
387 | // half of the fov is center to top of screen
388 | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0);
389 |
390 | // we use only height here so aspect ratio does not distort speed
391 | panLeft(2 * deltaX * targetDistance / element.height, scope.object.matrix);
392 | panUp(2 * deltaY * targetDistance / element.height, scope.object.matrix);
393 |
394 | } else if (scope.object.isOrthographicCamera) {
395 |
396 | // orthographic
397 | panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.width, scope.object.matrix);
398 | panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.height, scope.object.matrix);
399 |
400 | } else {
401 |
402 | // camera neither orthographic nor perspective
403 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.');
404 | scope.enablePan = false;
405 |
406 | }
407 |
408 | };
409 |
410 | }();
411 |
412 | function dollyOut(dollyScale) {
413 |
414 | if (scope.object.isPerspectiveCamera) {
415 |
416 | scale /= dollyScale;
417 |
418 | } else if (scope.object.isOrthographicCamera) {
419 |
420 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale));
421 | scope.object.updateProjectionMatrix();
422 | zoomChanged = true;
423 |
424 | } else {
425 |
426 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.');
427 | scope.enableZoom = false;
428 |
429 | }
430 |
431 | }
432 |
433 | function dollyIn(dollyScale) {
434 |
435 | if (scope.object.isPerspectiveCamera) {
436 |
437 | scale *= dollyScale;
438 |
439 | } else if (scope.object.isOrthographicCamera) {
440 |
441 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale));
442 | scope.object.updateProjectionMatrix();
443 | zoomChanged = true;
444 |
445 | } else {
446 |
447 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.');
448 | scope.enableZoom = false;
449 |
450 | }
451 |
452 | }
453 |
454 | //
455 | // event callbacks - update the object state
456 | //
457 |
458 | function handleMouseDownRotate(event) {
459 |
460 | rotateStart.set(event.clientX, event.clientY);
461 |
462 | }
463 |
464 | function handleMouseDownDolly(event) {
465 |
466 | dollyStart.set(event.clientX, event.clientY);
467 |
468 | }
469 |
470 | function handleMouseDownPan(event) {
471 |
472 | panStart.set(event.clientX, event.clientY);
473 |
474 | }
475 |
476 | function handleMouseMoveRotate(event) {
477 |
478 | rotateEnd.set(event.clientX, event.clientY);
479 |
480 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed);
481 |
482 | var element = scope.domElement;
483 |
484 | rotateLeft(2 * Math.PI * rotateDelta.x / element.height); // yes, height
485 |
486 | rotateUp(2 * Math.PI * rotateDelta.y / element.height);
487 |
488 | rotateStart.copy(rotateEnd);
489 |
490 | scope.update();
491 |
492 | }
493 |
494 | function handleMouseMoveDolly(event) {
495 |
496 | dollyEnd.set(event.clientX, event.clientY);
497 |
498 | dollyDelta.subVectors(dollyEnd, dollyStart);
499 |
500 | if (dollyDelta.y > 0) {
501 |
502 | dollyOut(getZoomScale());
503 |
504 | } else if (dollyDelta.y < 0) {
505 |
506 | dollyIn(getZoomScale());
507 |
508 | }
509 |
510 | dollyStart.copy(dollyEnd);
511 |
512 | scope.update();
513 |
514 | }
515 |
516 | function handleMouseMovePan(event) {
517 |
518 | panEnd.set(event.clientX, event.clientY);
519 |
520 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed);
521 |
522 | pan(panDelta.x, panDelta.y);
523 |
524 | panStart.copy(panEnd);
525 |
526 | scope.update();
527 |
528 | }
529 |
530 | function handleMouseUp( /*event*/) {
531 |
532 | // no-op
533 |
534 | }
535 |
536 | function handleMouseWheel(event) {
537 |
538 | if (event.deltaY < 0) {
539 |
540 | dollyIn(getZoomScale());
541 |
542 | } else if (event.deltaY > 0) {
543 |
544 | dollyOut(getZoomScale());
545 |
546 | }
547 |
548 | scope.update();
549 |
550 | }
551 |
552 | function handleKeyDown(event) {
553 |
554 | var needsUpdate = false;
555 |
556 | switch (event.keyCode) {
557 |
558 | case scope.keys.UP:
559 | pan(0, scope.keyPanSpeed);
560 | needsUpdate = true;
561 | break;
562 |
563 | case scope.keys.BOTTOM:
564 | pan(0, - scope.keyPanSpeed);
565 | needsUpdate = true;
566 | break;
567 |
568 | case scope.keys.LEFT:
569 | pan(scope.keyPanSpeed, 0);
570 | needsUpdate = true;
571 | break;
572 |
573 | case scope.keys.RIGHT:
574 | pan(- scope.keyPanSpeed, 0);
575 | needsUpdate = true;
576 | break;
577 |
578 | }
579 |
580 | if (needsUpdate) {
581 |
582 | // prevent the browser from scrolling on cursor keys
583 | // event.preventDefault();
584 |
585 | scope.update();
586 |
587 | }
588 |
589 |
590 | }
591 |
592 | function handleTouchStartRotate(event) {
593 |
594 | if (event.touches.length == 1) {
595 |
596 | rotateStart.set(event.touches[0].pageX, event.touches[0].pageY);
597 |
598 | } else {
599 |
600 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
601 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
602 |
603 | rotateStart.set(x, y);
604 |
605 | }
606 |
607 | }
608 |
609 | function handleTouchStartPan(event) {
610 |
611 | if (event.touches.length == 1) {
612 |
613 | panStart.set(event.touches[0].pageX, event.touches[0].pageY);
614 |
615 | } else {
616 |
617 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
618 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
619 |
620 | panStart.set(x, y);
621 |
622 | }
623 |
624 | }
625 |
626 | function handleTouchStartDolly(event) {
627 |
628 | var dx = event.touches[0].pageX - event.touches[1].pageX;
629 | var dy = event.touches[0].pageY - event.touches[1].pageY;
630 |
631 | var distance = Math.sqrt(dx * dx + dy * dy);
632 |
633 | dollyStart.set(0, distance);
634 |
635 | }
636 |
637 | function handleTouchStartDollyPan(event) {
638 |
639 | if (scope.enableZoom) handleTouchStartDolly(event);
640 |
641 | if (scope.enablePan) handleTouchStartPan(event);
642 |
643 | }
644 |
645 | function handleTouchStartDollyRotate(event) {
646 |
647 | if (scope.enableZoom) handleTouchStartDolly(event);
648 |
649 | if (scope.enableRotate) handleTouchStartRotate(event);
650 |
651 | }
652 |
653 | function handleTouchMoveRotate(event) {
654 |
655 | if (event.touches.length == 1) {
656 |
657 | rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
658 |
659 | } else {
660 |
661 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
662 | var y = 0.5 * (event.touches[0].pageY + event.touches[1].pageY);
663 |
664 | rotateEnd.set(x, y);
665 |
666 | }
667 |
668 | rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed);
669 |
670 | var element = scope.domElement;
671 |
672 | rotateLeft(2 * Math.PI * rotateDelta.x / element.height); // yes, height
673 |
674 | rotateUp(2 * Math.PI * rotateDelta.y / element.height);
675 |
676 | rotateStart.copy(rotateEnd);
677 |
678 | }
679 |
680 | function handleTouchMovePan(event) {
681 |
682 | if (event.touches.length == 1) {
683 |
684 | panEnd.set(event.touches[0].pageX, event.touches[0].pageY);
685 |
686 | } else {
687 |
688 | var x = 0.5 * (event.touches[0].pageX + event.touches[1].pageX);
689 | var y = 0.5 * (event.touches[0].pageY+ event.touches[1].pageY);
690 |
691 | panEnd.set(x, y);
692 |
693 | }
694 |
695 | panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed);
696 |
697 | pan(panDelta.x, panDelta.y);
698 |
699 | panStart.copy(panEnd);
700 |
701 | }
702 |
703 | function handleTouchMoveDolly(event) {
704 |
705 | var dx = event.touches[0].pageX - event.touches[1].pageX;
706 | var dy = event.touches[0].pageY - event.touches[1].pageY;
707 |
708 | var distance = Math.sqrt(dx * dx + dy * dy);
709 |
710 | dollyEnd.set(0, distance);
711 |
712 | dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed));
713 |
714 | dollyOut(dollyDelta.y);
715 |
716 | dollyStart.copy(dollyEnd);
717 |
718 | }
719 |
720 | function handleTouchMoveDollyPan(event) {
721 |
722 | if (scope.enableZoom) handleTouchMoveDolly(event);
723 |
724 | if (scope.enablePan) handleTouchMovePan(event);
725 |
726 | }
727 |
728 | function handleTouchMoveDollyRotate(event) {
729 |
730 | if (scope.enableZoom) handleTouchMoveDolly(event);
731 |
732 | if (scope.enableRotate) handleTouchMoveRotate(event);
733 |
734 | }
735 |
736 | function handleTouchEnd( /*event*/) {
737 |
738 | // no-op
739 |
740 | }
741 |
742 | //
743 | // event handlers - FSM: listen for events and reset state
744 | //
745 |
746 | function onMouseDown(event) {
747 |
748 | if (scope.enabled === false) return;
749 |
750 | // Prevent the browser from scrolling.
751 | // event.preventDefault();
752 |
753 | // Manually set the focus since calling preventDefault above
754 | // prevents the browser from setting it automatically.
755 |
756 | scope.domElement.focus ? scope.domElement.focus() : window.focus();
757 |
758 | var mouseAction;
759 |
760 | switch (event.button) {
761 |
762 | case 0:
763 |
764 | mouseAction = scope.mouseButtons.LEFT;
765 | break;
766 |
767 | case 1:
768 |
769 | mouseAction = scope.mouseButtons.MIDDLE;
770 | break;
771 |
772 | case 2:
773 |
774 | mouseAction = scope.mouseButtons.RIGHT;
775 | break;
776 |
777 | default:
778 |
779 | mouseAction = - 1;
780 |
781 | }
782 |
783 | switch (mouseAction) {
784 |
785 | case MOUSE.DOLLY:
786 |
787 | if (scope.enableZoom === false) return;
788 |
789 | handleMouseDownDolly(event);
790 |
791 | state = STATE.DOLLY;
792 |
793 | break;
794 |
795 | case MOUSE.ROTATE:
796 |
797 | if (event.ctrlKey || event.metaKey || event.shiftKey) {
798 |
799 | if (scope.enablePan === false) return;
800 |
801 | handleMouseDownPan(event);
802 |
803 | state = STATE.PAN;
804 |
805 | } else {
806 |
807 | if (scope.enableRotate === false) return;
808 |
809 | handleMouseDownRotate(event);
810 |
811 | state = STATE.ROTATE;
812 |
813 | }
814 |
815 | break;
816 |
817 | case MOUSE.PAN:
818 |
819 | if (event.ctrlKey || event.metaKey || event.shiftKey) {
820 |
821 | if (scope.enableRotate === false) return;
822 |
823 | handleMouseDownRotate(event);
824 |
825 | state = STATE.ROTATE;
826 |
827 | } else {
828 |
829 | if (scope.enablePan === false) return;
830 |
831 | handleMouseDownPan(event);
832 |
833 | state = STATE.PAN;
834 |
835 | }
836 |
837 | break;
838 |
839 | default:
840 |
841 | state = STATE.NONE;
842 |
843 | }
844 |
845 | if (state !== STATE.NONE) {
846 |
847 | document.addEventListener('mousemove', onMouseMove, false);
848 | document.addEventListener('mouseup', onMouseUp, false);
849 |
850 | scope.dispatchEvent(startEvent);
851 |
852 | }
853 |
854 | }
855 |
856 | function onMouseMove(event) {
857 |
858 | if (scope.enabled === false) return;
859 |
860 | // event.preventDefault();
861 |
862 | switch (state) {
863 |
864 | case STATE.ROTATE:
865 |
866 | if (scope.enableRotate === false) return;
867 |
868 | handleMouseMoveRotate(event);
869 |
870 | break;
871 |
872 | case STATE.DOLLY:
873 |
874 | if (scope.enableZoom === false) return;
875 |
876 | handleMouseMoveDolly(event);
877 |
878 | break;
879 |
880 | case STATE.PAN:
881 |
882 | if (scope.enablePan === false) return;
883 |
884 | handleMouseMovePan(event);
885 |
886 | break;
887 |
888 | }
889 |
890 | }
891 |
892 | function onMouseUp(event) {
893 |
894 | if (scope.enabled === false) return;
895 |
896 | handleMouseUp(event);
897 |
898 | document.removeEventListener('mousemove', onMouseMove, false);
899 | document.removeEventListener('mouseup', onMouseUp, false);
900 |
901 | scope.dispatchEvent(endEvent);
902 |
903 | state = STATE.NONE;
904 |
905 | }
906 |
907 | function onMouseWheel(event) {
908 |
909 | if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) return;
910 |
911 | // event.preventDefault();
912 | // event.stopPropagation();
913 |
914 | scope.dispatchEvent(startEvent);
915 |
916 | handleMouseWheel(event);
917 |
918 | scope.dispatchEvent(endEvent);
919 |
920 | }
921 |
922 | function onKeyDown(event) {
923 |
924 | if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return;
925 |
926 | handleKeyDown(event);
927 |
928 | }
929 |
930 | function onTouchStart(event) {
931 | if (scope.enabled === false) return;
932 | // event.preventDefault(); // prevent scrolling
933 |
934 | switch (event.touches.length) {
935 |
936 | case 1:
937 |
938 | switch (scope.touches.ONE) {
939 |
940 | case TOUCH.ROTATE:
941 |
942 | if (scope.enableRotate === false) return;
943 |
944 | handleTouchStartRotate(event);
945 |
946 | state = STATE.TOUCH_ROTATE;
947 |
948 | break;
949 |
950 | case TOUCH.PAN:
951 |
952 | if (scope.enablePan === false) return;
953 |
954 | handleTouchStartPan(event);
955 |
956 | state = STATE.TOUCH_PAN;
957 |
958 | break;
959 |
960 | default:
961 |
962 | state = STATE.NONE;
963 |
964 | }
965 |
966 | break;
967 |
968 | case 2:
969 |
970 | switch (scope.touches.TWO) {
971 |
972 | case TOUCH.DOLLY_PAN:
973 |
974 | if (scope.enableZoom === false && scope.enablePan === false) return;
975 |
976 | handleTouchStartDollyPan(event);
977 |
978 | state = STATE.TOUCH_DOLLY_PAN;
979 |
980 | break;
981 |
982 | case TOUCH.DOLLY_ROTATE:
983 |
984 | if (scope.enableZoom === false && scope.enableRotate === false) return;
985 |
986 | handleTouchStartDollyRotate(event);
987 |
988 | state = STATE.TOUCH_DOLLY_ROTATE;
989 |
990 | break;
991 |
992 | default:
993 |
994 | state = STATE.NONE;
995 |
996 | }
997 |
998 | break;
999 |
1000 | default:
1001 |
1002 | state = STATE.NONE;
1003 |
1004 | }
1005 |
1006 | if (state !== STATE.NONE) {
1007 |
1008 | scope.dispatchEvent(startEvent);
1009 |
1010 | }
1011 |
1012 | }
1013 |
1014 | function onTouchMove(event) {
1015 |
1016 | if (scope.enabled === false) return;
1017 |
1018 | // event.preventDefault(); // prevent scrolling
1019 | // event.stopPropagation();
1020 |
1021 | switch (state) {
1022 |
1023 | case STATE.TOUCH_ROTATE:
1024 |
1025 | if (scope.enableRotate === false) return;
1026 |
1027 | handleTouchMoveRotate(event);
1028 |
1029 | scope.update();
1030 |
1031 | break;
1032 |
1033 | case STATE.TOUCH_PAN:
1034 |
1035 | if (scope.enablePan === false) return;
1036 |
1037 | handleTouchMovePan(event);
1038 |
1039 | scope.update();
1040 |
1041 | break;
1042 |
1043 | case STATE.TOUCH_DOLLY_PAN:
1044 |
1045 | if (scope.enableZoom === false && scope.enablePan === false) return;
1046 |
1047 | handleTouchMoveDollyPan(event);
1048 |
1049 | scope.update();
1050 |
1051 | break;
1052 |
1053 | case STATE.TOUCH_DOLLY_ROTATE:
1054 |
1055 | if (scope.enableZoom === false && scope.enableRotate === false) return;
1056 |
1057 | handleTouchMoveDollyRotate(event);
1058 |
1059 | scope.update();
1060 |
1061 | break;
1062 |
1063 | default:
1064 |
1065 | state = STATE.NONE;
1066 |
1067 | }
1068 |
1069 | }
1070 |
1071 | function onTouchEnd(event) {
1072 |
1073 | if (scope.enabled === false) return;
1074 |
1075 | handleTouchEnd(event);
1076 |
1077 | scope.dispatchEvent(endEvent);
1078 |
1079 | state = STATE.NONE;
1080 |
1081 | }
1082 |
1083 | function onContextMenu(event) {
1084 |
1085 | if (scope.enabled === false) return;
1086 |
1087 | event.preventDefault();
1088 |
1089 | }
1090 |
1091 | //
1092 |
1093 | // scope.domElement.addEventListener('contextmenu', onContextMenu, false);
1094 |
1095 | // scope.domElement.addEventListener('mousedown', onMouseDown, false);
1096 | // scope.domElement.addEventListener('wheel', onMouseWheel, false);
1097 | // scope.domElement.addEventListener('touchstart', onTouchStart, false);
1098 | // scope.domElement.addEventListener('touchend', onTouchEnd, false);
1099 | // scope.domElement.addEventListener('touchmove', onTouchMove, false);
1100 |
1101 | // scope.domElement.addEventListener('keydown', onKeyDown, false);
1102 |
1103 | scope.onMouseDown=onMouseDown;
1104 | scope.onContextMenu=onContextMenu;
1105 | scope.onMouseWheel=onMouseWheel;
1106 |
1107 | scope.onTouchStart=onTouchStart;
1108 | scope.onTouchEnd=onTouchEnd;
1109 | scope.onTouchMove=onTouchMove;
1110 |
1111 | scope.onKeyDown=onKeyDown;
1112 |
1113 | // make sure element can receive keys.
1114 |
1115 | if (scope.domElement.tabIndex === - 1) {
1116 |
1117 | scope.domElement.tabIndex = 0;
1118 |
1119 | }
1120 |
1121 | // force an update at start
1122 |
1123 | this.update();
1124 |
1125 | };
1126 |
1127 | OrbitControls.prototype = Object.create(EventDispatcher.prototype);
1128 | OrbitControls.prototype.constructor = OrbitControls;
1129 | return OrbitControls;
1130 | }
--------------------------------------------------------------------------------
/utils/pointPick.js:
--------------------------------------------------------------------------------
1 | const app = getApp();
2 |
3 | export function onMouseMove(pt, mouse) {
4 | let { width, height } = app.globalData;
5 | // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
6 | mouse.x = (pt.x / width) * 2 - 1;
7 | mouse.y = - (pt.y / height) * 2 + 1;
8 | }
9 |
10 | export function pointPick(event, object) {
11 | let THREE = app.THREE;
12 |
13 | if (!object)
14 | object = app.Viewer.scene;
15 |
16 | let selectObjs = [];
17 | object.traverse(o => {
18 | if (o.isMesh&&o.name!=="plane")
19 | selectObjs.push(o);
20 | })
21 | var raycaster = new THREE.Raycaster();
22 | var mouse = new THREE.Vector2();
23 | onMouseMove(event, mouse);
24 | // 通过摄像机和鼠标位置更新射线
25 | raycaster.setFromCamera(mouse, app.Viewer.camera.intance);
26 |
27 | // 计算物体和射线的焦点
28 | var intersects = raycaster.intersectObjects(selectObjs);
29 | for(let inter of intersects){
30 | if(inter.object)
31 | return inter.object;
32 | }
33 | }
--------------------------------------------------------------------------------
/utils/requestAnimationFrame.js:
--------------------------------------------------------------------------------
1 | export const requestAnimationFrame = function (callback, lastTime) {
2 | var lastTime;
3 | if (typeof lastTime === 'undefined') {
4 | lastTime = 0
5 | }
6 | var currTime = new Date().getTime();
7 | var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
8 | lastTime = currTime + timeToCall;
9 | var id = setTimeout(function () {
10 | callback(currTime + timeToCall, lastTime);
11 | },
12 | timeToCall);
13 | return id;
14 | };
15 |
16 | export const cancelAnimationFrame = function (id) {
17 | clearTimeout(id);
18 | };
19 |
--------------------------------------------------------------------------------
/utils/threeutils.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZoeLeee/wx_threeJs_project/b792adc4b4968bd58ab3142e20f210b5854c4276/utils/threeutils.js
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------