这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
最近几天在学习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地址
npm 安装 OrbitControls.js 操作三维场景插件 npm install three-orbit-controls
在引入插件时必须保证three被成功引入否则页面会报错,如果你不想通过npm下载 , 其实在 npm three 的时候已经下载对应的插件, 在 node_modules 文件夹下找到 three/examples/jsm/controls/OrbitControls 这个路径里面也能找到对应的插件, 通过下面注释里面的形式也能导入,但是不推荐这样导入因为在谷歌32位49版本的浏览器中这样导入控制器是无法使用的
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) 上面写的很详细 ,有问题的同学可以去看看
npm 安装性能检测插件 , npm i three-stats
主要作用就是 主要用于检测动画运行时的帧数
文章到这里当前项目的配置文件就已经介绍完毕了,后面我就会开始介绍一些three.js的最基本的原理以及全景图的实现方式 ,完整的代码我会贴到文章的最下方
three.js 的基本原理 (渲染器-renderer, 场景-scene,相机-camera)
这里只对原理进行简单的讲述,想要详细了解的同学请进 郭隆邦老师的电子书指南
举个栗子 ,假如我是一名导演, 我已经准备了最好了演员 ,还请了岛国一流的拍摄团队 , 最后物色一块风水宝地 ,准备拍一部让人热血沸腾的青春偶像动作片 ,一战成名 , 然后走向人生巅峰
- 渲染器-renderer 就好比刚刚物色的那块风水宝地 ,我什么都准备好了总要找个合适的地方进行拍摄嘛 ,这里就是通过渲染器来创建一个自定义大小的拍摄地点 渲染器中文文档
new THREE.WebGLRenderer(); //创建渲染器
- 场景-scene 就是你拍摄地点找好了,但里面什么都没有一片漆黑伸手不见五指 ,作为导演的我们是不是应该把光源装上去在把演员请进来呢 (这里光源就代表-环境光 , 演员-就代表创建好的模型) 不然我们这个导演就当的不合格,那还怎么走向人生巅峰啊
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);
...
}
- 相机-camera 相机顾名思义就是拍摄用的道具 , 相机的视角也就是我们最终画面呈现的视角 ,这里我们使用透视相机因为透视相机的视角更贴近真实人眼看的视角,透视相机具体参数可以看 透视相机中文文档
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 前面渲染 , 设置配置的时候需要注意一下
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绘制文字 , 至于为什么,就是因为不用导入图片,并且自定义文字比较方便
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 ,只需要向射线中传入鼠标的位置和当前相机即可,这样我们就可以根据模型的名称获取当前点击的那个模型并触发对应的事件
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;
},
定义相机的位置
我们需要将透视投影相机放在球体的中心模拟人在在房间里面的位置 ,调整相机位置和相机看向即可
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 的刷新机制是当控制器监听到页面改变时不停的高频率执行重新渲染的操作动态改变页面
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 帧动画
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();
},
全景图完整代码
简易版全景图
控制台
是否自动旋转
开启
关闭
构建这个全景图时遇到的问题
-
vue中直接放入图片失败
因为我们使用的是node.js启动的前端服务所以引入本地图片需要使用 require('../../public/img/home3.jpeg') 形式进行引入 ,直接使用地址图片是不会显示的
-
vue 打包后图片不显示
使用 require('') 打包后node把文件协议改为 file:// 形式的协议用于访问本地打包后的图片 , 然而 textureLoader.load(); 只接收 http:// 形式的文件所以打包后图片无法显示 , 这里只需要把自己的图片放在tomcat服务器上 , 在取自己tomcat服务器上的图片就可以了
-
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
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。