记录--vue+three.js 构建 简易全景图

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

最近几天在学习three.js ,因为我相信只有实践才能出真理,捣鼓捣鼓做了一个简易的全景图,这里主要是分享做这个vue版全景图中遇到的问题,有些代码可能与其他做过全景图的大佬有些相似毕竟原因都差不多 😀

本文属于技术总结类的文章

将介绍在 vue中如何安装并使用 three.js 以及一些配套插件 , 使用three.js 实现全景图的原理 , vue打包后图片显示的问题 ,及在32位谷歌49版本的浏览器无法使用three.js等问题,至于如何安装 Node服务这里就不再赘述了

在 vue 中安装 three.js 以及配套插件

npm 安装 three.js npm install three 然后在对应页面上将three的功能模块全部导入进来
three.js - npm地址

1
2
3
4
<script>
import * as THREE from "three";
...
</script>

npm 安装 OrbitControls.js 操作三维场景插件 npm install three-orbit-controls 在引入插件时必须保证three被成功引入否则页面会报错,如果你不想通过npm下载 , 其实在 npm three 的时候已经下载对应的插件, 在 node_modules 文件夹下找到 three/examples/jsm/controls/OrbitControls 这个路径里面也能找到对应的插件, 通过下面注释里面的形式也能导入,但是不推荐这样导入因为在谷歌32位49版本的浏览器中这样导入控制器是无法使用的

1
2
3
4
5
6
<script>
import * as THREE from "three";
const OrbitControls = require('three-orbit-controls')(THREE);
//import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'
...
</script>

npm 安装.obj 和.mtl 文件的插件 npm i --save three-obj-mtl-loader 加载 .obj 模型文件 , .mtl 材质信息文件这里我就不过多赘述了想要试试的小伙伴可以看看 郭隆邦老师的电子书指南-第14小节 , 还有fbx模型文件 ,也就是除了包含几何、材质信息,还可以存储骨骼动画等数据的模型文件 ,可以通过 npm i three-fbx-loader进行安装

说到这些材质文件的导入我要忍不住吐槽两句 .stl格式 , obj文件 大多数按照官方的导入方法来做是没有问题的 , 但.fbx格式就很特殊 , 之前我在网上下载的比较多的fbx格式的模型和对应的材质但大多数都用不了 ,我真的是裂开了。

后面到处查原因,总结了一下就是插件的兼容性不好 网上下载的fbx动画大多数都是用不了的,有版本问题、也有文件本身的问题 后面找到一篇大佬的开荒文章 THREE.js中加载不同格式的模型及动画(fbx、json和obj) 上面写的很详细 ,有问题的同学可以去看看 😀

1
2
3
4
5
6
<script>
import * as THREE from "three";
import {OBJLoader,MTLLoader} from 'three-obj-mtl-loader';
const OrbitControls = require('three-orbit-controls')(THREE);
...
</script>

npm 安装性能检测插件 , npm i three-stats 主要作用就是 主要用于检测动画运行时的帧数

1
2
3
4
5
6
7
<script>
import * as THREE from "three";
import {OBJLoader,MTLLoader} from 'three-obj-mtl-loader';
const OrbitControls = require('three-orbit-controls')(THREE);
import * as ThreeStats from 'three-stats'
...
</script>

文章到这里当前项目的配置文件就已经介绍完毕了,后面我就会开始介绍一些three.js的最基本的原理以及全景图的实现方式 ,完整的代码我会贴到文章的最下方

three.js 的基本原理 (渲染器-renderer, 场景-scene,相机-camera)

这里只对原理进行简单的讲述,想要详细了解的同学请进 郭隆邦老师的电子书指南

举个栗子 ,假如我是一名导演, 我已经准备了最好了演员 ,还请了岛国一流的拍摄团队 , 最后物色一块风水宝地 ,准备拍一部让人热血沸腾的青春偶像动作片 ,一战成名 , 然后走向人生巅峰 😋

  1. 渲染器-renderer 就好比刚刚物色的那块风水宝地 ,我什么都准备好了总要找个合适的地方进行拍摄嘛 ,这里就是通过渲染器来创建一个自定义大小的拍摄地点 渲染器中文文档
1
new THREE.WebGLRenderer(); //创建渲染器
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div ref='threeDom'></div>
</template>
<script>
   rendererInit(){ //初始化渲染器
    var width = 1000; //窗口宽度 window.innerWidth 浏览器窗口可视区宽度(不包括浏览器控制台、菜单栏、工具栏)包含滚条
    var height = 800; //窗口高度 window.innerHeight
    this.renderer = new THREE.WebGLRenderer(); //创建渲染器
    this.renderer.setClearColor(0xffffff); //添加背景颜色
    this.renderer.setSize(width, height); // 设定渲染器尺寸
    this.$refs.threeDom.appendChild(this.renderer.domElement); //通过 this.$refs获取页面的dom将场景初始化上去
   },
</script>
  1. 场景-scene 就是你拍摄地点找好了,但里面什么都没有一片漆黑伸手不见五指 ,作为导演的我们是不是应该把光源装上去在把演员请进来呢 (这里光源就代表-环境光 , 演员-就代表创建好的模型) 不然我们这个导演就当的不合格,那还怎么走向人生巅峰啊
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sceneInit(){ //初始化场景 并向场景添加光源和辅助坐标系
    this.scene = new THREE.Scene(); //初始化场景
    var ambient = new THREE.AmbientLight(0x444444, 3); //添加光源  颜色和光照强度
    var axisHelper = new THREE.AxesHelper(600); //添加辅助坐标系 参数位辅助坐标系的长度
    this.scene.add(ambient, axisHelper); //向场景中添加光源 和 辅助坐标系
  },
   
modelling(){ //开始建立模型
this.mygroup = new THREE.Group(); //建立一个分组
    var textureLoader = new THREE.TextureLoader(); //创建纹理贴图        
    var img = textureLoader.load(require('../../public/img/qjt.jpeg'));
    var geometry = new THREE.SphereGeometry(130, 256, 256); // 球体网格模型
    var material = new THREE.MeshLambertMaterial({
        map: img, //设置颜色贴图属性值
        side: THREE.DoubleSide, //双面渲染
    });
    var meshSphere = new THREE.Mesh(geometry, material); //网格模型对象Mesh  
    meshSphere.name = '球体容器';
    this.mygroup.add(meshSphere);
      this.scene.add(this.mygroup);
      ...
}
  1. 相机-camera 相机顾名思义就是拍摄用的道具 , 相机的视角也就是我们最终画面呈现的视角 ,这里我们使用透视相机因为透视相机的视角更贴近真实人眼看的视角,透视相机具体参数可以看 透视相机中文文档
1
2
3
4
5
6
7
cameraInit() { //初始化相机
    var width = 800; //窗口宽度
    var height = 800; //窗口高度
    this.camera = new THREE.PerspectiveCamera(90, width / height, 1, 1000); //使用透视相机
    this.camera.position.set(0, 0, 10); //设置相机位置
    this.camera.lookAt(new THREE.Vector3(0, 0, 0)); // 相机看向
},
  • 开始实现简易的全景图

终于到这里了现在正式开搞 😤 , 先通过上面介绍的基础原理把 渲染器-renderer, 场景-scene,相机-camera , 弄出来 , 然后全景图实现原理是,首先在坐标轴的中心创建一个,带图片纹理的小球 当前这里不一定要用球体 ,其他形状也是可以实现的 , 具体根据使用场景来定义

首先创建一个球体网格模型和对应的纹理贴图

建立球体模型以及使用 TextureLoader 生成纹理贴图 - 纹理贴图中文文档 纹理贴图默认渲染模式为 THREE.FrontSide 前面渲染 , 设置配置的时候需要注意一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
modelling(){ //开始建立模型
   this.mygroup = new THREE.Group();
   var textureLoader = new THREE.TextureLoader(); //创建纹理贴图         
   var img = textureLoader.load(require('../../public/img/home3.jpeg'));       
   var geometry = new THREE.SphereGeometry(130, 256, 256); // 球体网格模型
   var material = new THREE.MeshLambertMaterial({
    map: img, //设置颜色贴图属性值
    side: THREE.DoubleSide, //双面渲染
   });
   var meshSphere = new THREE.Mesh(geometry, material); //网格模型对象Mesh   
   meshSphere.name = '球体容器';
   this.mygroup.add(meshSphere);
   this.scene.add(this.mygroup);
},
                     
},

建立矩形平面自定义文字 three.js中自定义文字的方式大概分为以下几种

形成文字的方式实现方案优点缺点
DOM + CSS 一般的实现方式使用绝对定位和足够大的z-index让组件或者文字在3D图形的上方 实现简单效果强大 3d效果和物体联动性差
THREE.CanvasTexture 在canvas中绘制文字,然后使用CanvasTexture作为纹理进行贴图 文字效果较为丰富 一旦生成,分辨率固定,放大会产生失真
THREE.TextGeometry 使用原生的TextGeometry进行渲染生成 效果好,可与场景进行同步 字体的颜色和动画制作较为复杂,特别耗费资源
3d字体模型 使用3d制作的字体模型,使用threejs进行加载控制 效果好,可定制效果 加载模型耗费资源,字体内容无法自定义
位图字体 通过BmpFont生成文字模板,然后进行加载显示 可自定义字体和效果 加载模型耗费资源,字体内容无法自定义
Three.Sprite精灵材质 Sprite加载图像纹理 永远面向相机的平面,适合作为标签显示 一旦生成,分辨率固定,放大会产生失真

 这里我选择的是canvas绘制文字 , 至于为什么,就是因为不用导入图片,并且自定义文字比较方便 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
modelling(){ //开始建立模型
 this.mygroup = new THREE.Group();
 var canvasText = this.getcanvers('进门'); //生成一个canvers 文字图案对象
 var texture = new THREE.CanvasTexture(canvasText);
 var geometryText = new THREE.PlaneGeometry(16, 10, 60, 60); //生成一个平面模型
 var materialText = new THREE.MeshPhongMaterial({
    map: texture, // 设置纹理贴图
    side: THREE.DoubleSide, //双面渲染
 });
 var meshText = new THREE.Mesh(geometryText, materialText);
 meshText.name = '进门';
 meshText.position.set(40, 20, -90)
 this.mygroup.add(meshText);
 this.scene.add(this.mygroup);
},
 
getcanvers(text) { //生成一个canvers图案
 var canvasText = document.createElement("canvas");
 var c = canvasText.getContext('2d');
 // 矩形区域填充背景
 c.fillStyle = "#FFFFFF"; //canver背景
 c.fillRect(0, 0, 300, 200); //生成一个矩形
 c.translate(160, 80);
 c.fillStyle = "#000000"; //文本填充颜色
 c.font = "bold 100px 宋体"; //字体样式设置
 c.textBaseline = "middle"; //文本与
 c.textAlign = "center"; //文本居中
 c.fillText(text, 0, 0);
 var texture = new THREE.CanvasTexture(canvasText); //Canvas纹理
 var geometryText = new THREE.PlaneGeometry(16, 10, 60, 60); //生成一个矩形平面
 var materialText = new THREE.MeshPhongMaterial({
    map: texture, // 设置纹理贴图
    side: THREE.DoubleSide, //双面渲染
 });
 var meshText = new THREE.Mesh(geometryText, materialText);
 meshText.name = text;
 meshText.position.set(40, 20, -90);
 return canvasText;
},                   
},

通过点击矩形平面切换场景

在一般的 HTML 中触发点击事件只需要给对应的dom绑定事件即可 , 但是在three.js 里面就行不通 , 因为three生成的图形页面其实就是一张canvas画布无法直接取到对应的dom , 更不用说了给dom绑定事件了 ,不过好在three.js 提供了一个 new THREE.Raycaster() 光线投射 (用于拾取鼠标的位置以及在三维空间中计算出鼠标移过了什么物体)

 射线会记录与之相交几何体,并以数组的形式从近到远返回对应模型的mesh ,只需要向射线中传入鼠标的位置和当前相机即可,这样我们就可以根据模型的名称获取当前点击的那个模型并触发对应的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
init(){
    this.$refs.threeDom.addEventListener('dblclick', this.onMouseDblclick); //监听双击事件
   },
 onMouseDblclick(event){ //触发双击事件
    // 获取 raycaster 和所有模型相交的数组,其中的元素按照距离排序,越近的越靠前
    var intersects = this.getIntersects(event);
    ...
   }, 
   getIntersects(event) { // 获取与射线相交的对象数组
    event.preventDefault();
    // 声明 raycaster 和 mouse 变量
    var raycaster = new THREE.Raycaster(); //生成射线
    var mouse = new THREE.Vector2();
    var container = this.$refs.threeDom;
    let getBoundingClientRect = container.getBoundingClientRect();
    // 通过鼠标点击位置,计算出 raycaster 所需点的位置 分量,以屏幕为中心点,范围 -1 到 1
    mouse.x = ((event.clientX - getBoundingClientRect.left) / container.offsetWidth) * 2 - 1;
    mouse.y = -((event.clientY - getBoundingClientRect.top) / container.offsetHeight) * 2 + 1;
    //通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
    raycaster.setFromCamera(mouse, this.camera);
    // 获取与射线相交的对象数组,其中的元素按照距离排序,越近的越靠前
    var intersects = raycaster.intersectObjects(this.scene.children[2].children);
    //返回选中的对象
    return intersects;
   },

定义相机的位置

我们需要将透视投影相机放在球体的中心模拟人在在房间里面的位置 ,调整相机位置和相机看向即可 

1
2
3
4
5
6
7
  cameraInit() { //初始化相机
        var width = 800; //窗口宽度
        var height = 800; //窗口高度
        this.camera = new THREE.PerspectiveCamera(90, width / height, 1, 1000); //使用透视相机
        this.camera.position.set(0, 0, 10); //设置相机位置
        this.camera.lookAt(new THREE.Vector3(0, 0, 0)); // 相机看向
},

初始化控制器

控制器也就是我们最开始引入的 OrbitControls.js 操作三维场景插件 , OrbitControls 的刷新机制是当控制器监听到页面改变时不停的高频率执行重新渲染的操作动态改变页面

1
2
3
4
5
6
7
8
9
10
11
12
13
    controlInit(){ //初始化控制器
        this.controls = new OrbitControls(this.camera, this.$refs.threeDom); // 初始化控制器
        this.controls.target.set(0, 0, 0); // 设置控制器的焦点,使控制器围绕这个焦点进行旋转
        this.controls.minDistance = 10; // 设置移动的最短距离(默认为零)
        this.controls.maxPolarAngle = Math.PI; //绕垂直轨道的距离(范围是0-Math.PI,默认为Math.PI)
        this.controls.maxDistance = 30; // 设置移动的最长距离(默认为无穷)
        this.controls.enablePan = false; //禁用右键功能
        this.controls.addEventListener('change', this.refresh); //监听鼠标、键盘事件 让整个控件可以拖动
},
    refresh(){ //刷新页面
        this.renderer.render(this.scene, this.camera); //执行渲染操作
        this.stats.update(); //更新性能监控的值        
},

定义可控制的自动旋转动画

上面几个步骤做完后,全景图功能差不多都实现了 , 但是页面不会自动旋转总感觉少了点意思 ,现在就给这个项目加上自动旋转的功能同时能根据按钮来停止和开启自动旋转 , 实现方案时通过three.js 准备好的 new THREE.KeyframeTrack() 定义关键帧 , new THREE.AnimationClip() 剪辑keyframe对象 , new THREE.AnimationMixer() 动画混合实例

想要详细了解一下动画基本原理的小伙伴可以看下大佬写的这篇文章 Three.js - KeyframeTrack 帧动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
addAnimation(){ //添加并开启动画
    this.clock = new THREE.Clock(); // three.js 时钟对象
    var times = [0, 3600]; //   创建帧动画序列
    var position_x = [0, 360]; //离散属性值
    var keyframe = new THREE.KeyframeTrack('meshSphere.rotation[y]', times, position_x);
    var duration = 100; //持续时间
    var cilp = new THREE.AnimationClip('sphereRotate', duration, [keyframe]); //剪辑 keyframe对象
    this.mixer = new THREE.AnimationMixer(this.mygroup); //动画混合实例
    this.action = this.mixer.clipAction(cilp);
    this.action.timeScale = 1; //播放速度
    this.action.setLoop(THREE.LoopPingPong).play(); //开始播放 像乒乓球一样在起始点与结束点之间来回循环
    this.animate(); //开启动画
 },
  
 animate() { //循环渲染
    this.rotateAnimate = requestAnimationFrame(this.animate);
    this.renderer.render(this.scene, this.camera);
    this.update();
 },

全景图完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
<template>
    <div class="homePage">
        <el-card class="card">
            <div slot="header">
                <div class="card-title">
                    <span>简易版全景图</span>
 
                    <div class="card-property">
                        <span ref='property'></span>
                    </div>
 
                </div>
            </div>
 
            <div class="card-content">
                <div ref='threeDom' class="model"></div>
                <div class="control">
                    <span class="control-title">控制台</span>
                    <div class="control-block">
                        <span class="control-block-title">是否自动旋转</span>
                        <el-radio-group v-model="isRotate" @change="isSpin">
                            <el-radio :label="1">开启</el-radio>
                            <el-radio :label="0">关闭</el-radio>
                        </el-radio-group>
                    </div>
                </div>
            </div>
        </el-card>
    </div>
</template>
 
<script>
    import axios from 'axios';
    import * as THREE from "three";
    import * as TrackballControls from 'three-trackballcontrols'
    import * as ThreeStats from 'three-stats'
    import { OBJLoader, MTLLoader } from 'three-obj-mtl-loader';
    const OrbitControls = require('three-orbit-controls')(THREE);
 
    export default {
        props: {
            msg: String
        },
        data() {
            return {
                renderer: '', //渲染器
                scene: '', //场景
                light: '', //光源
                camera: '', //相机
                controls: '', //控制器
                stats: '', //性能监控器
                mygroup: '', //模型组
 
                action: '', //控制动画的值
                clock: '', //时钟
                mixer: '', //混合实例
                rotateAnimate: '', //旋转动画
                isRotate: 1, //是否开启旋转
 
            }
        },
 
        mounted() {
            this.init(); //初始化
        },
 
        methods: {
            init() {
                this.$refs.threeDom.addEventListener('dblclick', this.onMouseDblclick); //监听双击事件
                this.rendererInit(); //创建渲染器
                this.sceneInit(); //创建场景    包含光源和辅助坐标系
                this.cameraInit(); //创建相机
                this.controlInit(); //初始化控制器
                this.propertyInit(); //性能监控
                this.modelling(); //建立模型
            },
 
            modelling(){ //开始建立模型
                this.mygroup = new THREE.Group();
                var textureLoader = new THREE.TextureLoader(); //创建纹理贴图    
                var img = textureLoader.load(require('../../public/img/home3.jpeg'));
 
                var geometry = new THREE.SphereGeometry(130, 256, 256); // 球体网格模型
                var material = new THREE.MeshLambertMaterial({
                    map: img, //设置颜色贴图属性值
                    side: THREE.DoubleSide, //双面渲染
                });
                var meshSphere = new THREE.Mesh(geometry, material); //网格模型对象Mesh  
                meshSphere.name = '球体容器';
                this.mygroup.add(meshSphere);
 
                var canvasText = this.getcanvers('进门'); //生成一个canvers 文字图案对象
                var texture = new THREE.CanvasTexture(canvasText);
                var geometryText = new THREE.PlaneGeometry(16, 10, 60, 60);
                var materialText = new THREE.MeshPhongMaterial({
                    map: texture, // 设置纹理贴图
                    side: THREE.DoubleSide, //双面渲染
                });
                var meshText = new THREE.Mesh(geometryText, materialText);
                meshText.name = '进门';
                meshText.position.set(40, 20, -90)
                this.mygroup.add(meshText);
 
                this.scene.add(this.mygroup);
                this.addAnimation(); //添加并开启动画
                this.refresh();
            },
 
            isSpin(val) { //开启和关闭旋转
                if (val == 0) { //关闭控制台    
                    this.action.paused = true;
                } else {
                    this.action.paused = false;
                }
            },
 
            addAnimation() { //添加并开启动画
                this.clock = new THREE.Clock(); // three.js 时钟对象
                var times = [0, 3600]; //   创建帧动画序列
                var position_x = [0, 360]; //离散属性值
                var keyframe = new THREE.KeyframeTrack('meshSphere.rotation[y]', times, position_x);
                var duration = 100; //持续时间
                var cilp = new THREE.AnimationClip('sphereRotate', duration, [keyframe]); //剪辑 keyframe对象
                this.mixer = new THREE.AnimationMixer(this.mygroup); //动画混合实例
                this.action = this.mixer.clipAction(cilp);
                this.action.timeScale = 1; //播放速度
                this.action.setLoop(THREE.LoopPingPong).play(); //开始播放 像乒乓球一样在起始点与结束点之间来回循环
                this.animate(); //开启动画
            },
 
            animate() { //循环渲染
                this.rotateAnimate = requestAnimationFrame(this.animate);
                this.renderer.render(this.scene, this.camera);
                this.update();
            },
 
            update() { //数据更新
                this.stats.update();
                this.mixer.update(this.clock.getDelta());
            },
 
            rendererInit() { //初始化渲染器
                var width = 1000; //窗口宽度
                var height = 800; //窗口高度
                this.renderer = new THREE.WebGLRenderer(); //创建渲染器
                this.renderer.setClearColor(0xffffff); //添加背景颜色
                this.renderer.setSize(width, height); // 设定渲染器尺寸
                this.$refs.threeDom.appendChild(this.renderer.domElement);
            },
 
            sceneInit() { //初始化场景 并向场景添加光源和辅助坐标系
                this.scene = new THREE.Scene();
                var ambient = new THREE.AmbientLight(0x444444, 3); //添加光源  颜色和光照强度
                var axisHelper = new THREE.AxesHelper(600); //添加辅助坐标系
                this.scene.add(ambient, axisHelper);
            },
 
            cameraInit() { //初始化相机
                var width = 800; //窗口宽度
                var height = 800; //窗口高度
                this.camera = new THREE.PerspectiveCamera(90, width / height, 1, 1000); //使用透视相机
                this.camera.position.set(0, 0, 10); //设置相机位置
                this.camera.lookAt(new THREE.Vector3(0, 0, 0)); // 相机看向
            },
 
            controlInit() { //初始化控制器
                this.controls = new OrbitControls(this.camera, this.$refs.threeDom); // 初始化控制器
                this.controls.target.set(0, 0, 0); // 设置控制器的焦点,使控制器围绕这个焦点进行旋转
                this.controls.minDistance = 10; // 设置移动的最短距离(默认为零)
                this.controls.maxPolarAngle = Math.PI; //绕垂直轨道的距离(范围是0-Math.PI,默认为Math.PI)
                this.controls.maxDistance = 30; // 设置移动的最长距离(默认为无穷)
                this.controls.enablePan = false; //禁用右键功能
                this.controls.addEventListener('change', this.refresh); //监听鼠标、键盘事件 让整个控件可以拖动
            },
 
            propertyInit() { //初始化性能监控
                this.stats = new ThreeStats.Stats(); // 创建一个性能监视器  
                this.stats.dom.style.position = 'absolute';
                this.stats.dom.style.top = '-4px';
                this.$refs.property.appendChild(this.stats.dom);
                this.stats.update();
            },
 
            getcanvers(text) { //生成一个canvers图案
                var canvasText = document.createElement("canvas");
                var c = canvasText.getContext('2d');
                // 矩形区域填充背景
                c.fillStyle = "#FFFFFF"; //canver背景
                c.fillRect(0, 0, 300, 200); //生成一个矩形
                c.translate(160, 80);
                c.fillStyle = "#000000"; //文本填充颜色
                c.font = "bold 100px 宋体"; //字体样式设置
                c.textBaseline = "middle"; //文本与
                c.textAlign = "center"; //文本居中
                c.fillText(text, 0, 0);
 
                var texture = new THREE.CanvasTexture(canvasText); //Canvas纹理
                var geometryText = new THREE.PlaneGeometry(16, 10, 60, 60); //生成一个矩形平面
                var materialText = new THREE.MeshPhongMaterial({
                    map: texture, // 设置纹理贴图
                    side: THREE.DoubleSide, //双面渲染
                });
                var meshText = new THREE.Mesh(geometryText, materialText);
                meshText.name = text;
                meshText.position.set(40, 20, -90);
                return canvasText;
            },
 
            refresh(){ //刷新页面
                this.renderer.render(this.scene, this.camera); //执行渲染操作
                this.stats.update(); //更新性能监控的值        
            },
 
            onMouseDblclick(event) { //触发双击事件
                // 获取 raycaster 和所有模型相交的数组,其中的元素按照距离排序,越近的越靠前
                var intersects = this.getIntersects(event);
                if (intersects.length != 0) {
                    for (var item of intersects) {
                        if (item.object.name != '') { //找到第一个不等于空的模型 就是自定义最近的模型
                            this.action.paused = true; //停止旋转          
                            this.$confirm('是否切换场景?', '提示', {
                                confirmButtonText: '切换',
                                cancelButtonText: '取消',
                                type: 'warning'
                            }).then(() => {
                                this.action.paused = false; //开启旋转
                                if (item.object.name == '进门') {
                                    this.changeScene('enter'); //改变页面场景
                                } else if (item.object.name == '返回') {
                                    this.changeScene('backtrack'); //改变页面场景
                                }
                            }).catch(() => {
                                this.action.paused = false; //开启旋转
                            });
                            break;
                        }
                    }
                } else { //这里是未选中状态
                }
            },
 
            changeScene(type) {
                var img = '';
                var names = '';
                var canvasText = '';
                var textureLoader = new THREE.TextureLoader(); //创建纹理贴图    
                if (type == 'enter') {
                    img = textureLoader.load(require('../../public/img/home1.jpg')); //vue加载图表需要用 require形式
                    canvasText = this.getcanvers('返回'); //生成一个canvers 文字图案对象   
                    names = '返回';
                } else if (type == 'backtrack') { //返回房间
                    img = textureLoader.load(require('../../public/img/home3.jpeg')); //vue加载图表需要用 require形式   
                    canvasText = this.getcanvers('进门'); //生成一个canvers 文字图案对象   
                    names = '进门';
                }
 
                for (var item of this.scene.children[2].children) {
                    if (item.name == '球体容器') { //切换贴图 进入下一张贴图                  
                        var material = new THREE.MeshLambertMaterial({
                            map: img, //设置颜色贴图属性值
                            side: THREE.DoubleSide, //双面渲染
                        });
                        item.material = material;
                    } else if (item.name == '进门' || item.name == '返回') {
                        var texture = new THREE.CanvasTexture(canvasText);
                        var materialText = new THREE.MeshPhongMaterial({
                            map: texture, // 设置纹理贴图
                            side: THREE.DoubleSide, //双面渲染
                        });
 
                        item.name = names; //改名模型的名字
                        item.material = materialText;
                    }
                }
 
                setTimeout(() => { //延迟刷新
                    this.refresh();
                }, 100)
 
            },
 
            getIntersects(event) { // 获取与射线相交的对象数组
                event.preventDefault();
                // 声明 raycaster 和 mouse 变量
                var raycaster = new THREE.Raycaster(); //生成射线
                var mouse = new THREE.Vector2();
                var container = this.$refs.threeDom;
                let getBoundingClientRect = container.getBoundingClientRect();
                // 通过鼠标点击位置,计算出 raycaster 所需点的位置 分量,以屏幕为中心点,范围 -1 到 1
                mouse.x = ((event.clientX - getBoundingClientRect.left) / container.offsetWidth) * 2 - 1;
                mouse.y = -((event.clientY - getBoundingClientRect.top) / container.offsetHeight) * 2 + 1;
                //通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
                raycaster.setFromCamera(mouse, this.camera);
                // 获取与射线相交的对象数组,其中的元素按照距离排序,越近的越靠前
                var intersects = raycaster.intersectObjects(this.scene.children[2].children);
                //返回选中的对象
                return intersects;
            },
        }
    }
</script>
 
<style>
    .homePage {
        position: absolute;
        height: 100%;
        width: 100%;
        font-size: 14px;
        color: #303133;
        display: flex;
        align-items: center;
        justify-content: center;
    }
 
    .card {
        width: 1300px;
        height: 900px;
    }
 
    .card-title {
        display: flex;
        align-items: center;
        justify-content: space-between;
    }
 
    .card-title span {
        font-weight: 600;
        font-size: 18px;
    }
 
    .card-property {
        position: relative;
        width: 70px;
        height: 40px;
    }
 
    .card-content {
        display: flex;
        flex-direction: row;
    }
 
    .model {
        border: 1px solid #DCDFE6;
    }
 
    .control {
        display: flex;
        flex-direction: column;
        width: 300px;
        height: 800px;
        border: 1px solid #DCDFE6;
        border-left: none;
    }
 
    .control-title {
        font-size: 18px;
        font-weight: 600;
        text-align: center;
        color: #409EFF;
        padding: 10px;
        border-bottom: 1px solid #DCDFE6;
    }
 
    .control-block {
        padding: 10px;
        border-bottom: 1px solid #DCDFE6;
    }
 
    .control-block-title {
        display: block;
        margin-bottom: 5px;
    }
 
    /* 自定义element样式 */
    .el-card__header {
        padding: 10px 20px;
    }
</style>

构建这个全景图时遇到的问题

  1. vue中直接放入图片失败

    因为我们使用的是node.js启动的前端服务所以引入本地图片需要使用 require('../../public/img/home3.jpeg') 形式进行引入 ,直接使用地址图片是不会显示的

  2. vue 打包后图片不显示

使用 require('') 打包后node把文件协议改为 file:// 形式的协议用于访问本地打包后的图片 , 然而 textureLoader.load(); 只接收 http:// 形式的文件所以打包后图片无法显示 , 这里只需要把自己的图片放在tomcat服务器上 , 在取自己tomcat服务器上的图片就可以了

  1. vue构建的项目放在谷歌32位49版本的浏览器中无法打开的问题

    为什么我老是会提起谷歌低版本毕竟现在一般人都不会使用低版本了 , 但我这边的客户还是有一小部分群体在使用 XP系统 我们虽然不能给他做到兼容 IE , 但至少谷歌低版本要给别人弄好 !

 具体是什么原因我也不是特别清楚 , 大概就是低版本浏览器获取值的方式和高版本不怎么一样,解决方案就是在 node_modules\three\build 找到 three.module.js

注释掉 this.setSession 这个获取方法

 还没完还得找到 node_modules\three-trackballcontrols 中的 index.js 文件并注释掉 const getMouseOnScreen 以及 const getMouseOnCircle 这两个方法

 至于为什么要注释这几个方法我也没有特地去研究 ,对于低版本我的理念就是能解决问题就OK了! 如果有知道具体解决方案的大佬可以在留言里面告诉我一下, 阿里嘎多

 

本文转载于:

https://juejin.cn/post/6927193628724953096

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

posted @   林恒  阅读(761)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
历史上的今天:
2021-08-29 工作记录:8个有用的JS技巧
欢迎阅读『记录--vue+three.js 构建 简易全景图』
点击右上角即可分享
微信分享提示