草庐IT

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

林恒 2023-03-28 原文

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

最近几天在学习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地址

<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版本的浏览器中这样导入控制器是无法使用的

<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) 上面写的很详细 ,有问题的同学可以去看看 ?

<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 主要作用就是 主要用于检测动画运行时的帧数

<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 就好比刚刚物色的那块风水宝地 ,我什么都准备好了总要找个合适的地方进行拍摄嘛 ,这里就是通过渲染器来创建一个自定义大小的拍摄地点 渲染器中文文档
new THREE.WebGLRenderer(); //创建渲染器
<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 就是你拍摄地点找好了,但里面什么都没有一片漆黑伸手不见五指 ,作为导演的我们是不是应该把光源装上去在把演员请进来呢 (这里光源就代表-环境光 , 演员-就代表创建好的模型) 不然我们这个导演就当的不合格,那还怎么走向人生巅峰啊
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 相机顾名思义就是拍摄用的道具 , 相机的视角也就是我们最终画面呈现的视角 ,这里我们使用透视相机因为透视相机的视角更贴近真实人眼看的视角,透视相机具体参数可以看 透视相机中文文档
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();
 },
         

全景图完整代码

<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

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

 

有关记录--vue+three.js 构建 简易全景图的更多相关文章

  1. Ruby 元类 : why three when defined singleton methods? - 2

    让我们计算MRI范围内的类别:defcount_classesObjectSpace.count_objects[:T_CLASS]endk=count_classes用类方法定义类:classAdefself.foonilendend然后运行:putscount_classes-k#=>3请解释一下,为什么是三个? 最佳答案 查看MRI代码,每次你创建一个Class时,在Ruby中它是Class类型的对象,ruby会自动为这个新类创建“元类”类,这是另一个单例类型的Class对象。C函数调用(class.c)是:rb_define

  2. ruby - Sinatra:运行 rspec 测试时记录噪音 - 2

    Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/

  3. ruby-on-rails - Rails 5 Active Record 记录无效错误 - 2

    我有两个Rails模型,即Invoice和Invoice_details。一个Invoice_details属于Invoice,一个Invoice有多个Invoice_details。我无法使用accepts_nested_attributes_forinInvoice通过Invoice模型保存Invoice_details。我收到以下错误:(0.2ms)BEGIN(0.2ms)ROLLBACKCompleted422UnprocessableEntityin25ms(ActiveRecord:4.0ms)ActiveRecord::RecordInvalid(Validationfa

  4. ruby - 在 Ruby 中构建长字符串的简洁方法 - 2

    在编写Ruby(客户端脚本)时,我看到了三种构建更长字符串的方法,包括行尾,所有这些对我来说“闻起来”有点难看。有没有更干净、更好的方法?变量递增。ifrender_quote?quote="NowthatthereistheTec-9,acrappyspraygunfromSouthMiami."quote+="ThisgunisadvertisedasthemostpopularguninAmericancrime.Doyoubelievethatshit?"quote+="Itactuallysaysthatinthelittlebookthatcomeswithit:themo

  5. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  6. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  7. ruby-on-rails - 事件记录 : Select max of limit - 2

    我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).

  8. ruby - 使用 rbenv 和 ruby​​-build 构建 ruby​​ 失败,出现 undefined symbol : SSLv2_method - 2

    我正在尝试在配备ARMv7处理器的SynologyDS215j上安装ruby​​2.2.4或2.3.0。我用了optware-ng安装gcc、make、openssl、openssl-dev和zlib。我根据README中的说明安装了rbenv(版本1.0.0-19-g29b4da7)和ruby​​-build插件。.这些是随optware-ng安装的软件包及其版本binutils-2.25.1-1gcc-5.3.0-6gconv-modules-2.21-3glibc-opt-2.21-4libc-dev-2.21-1libgmp-6.0.0a-1libmpc-1.0.2-1libm

  9. Ruby 守护进程导致 ActiveRecord 记录器 IOError - 2

    我目前正在用Ruby编写一个项目,它使用ActiveRecordgem进行数据库交互,我正在尝试使用ActiveRecord::Base.logger记录所有数据库事件具有以下代码的属性ActiveRecord::Base.logger=Logger.new(File.open('logs/database.log','a'))这适用于迁移等(出于某种原因似乎需要启用日志记录,因为它在禁用时会出现NilClass错误)但是当我尝试运行包含调用ActiveRecord对象的线程守护程序的项目时脚本失败并出现以下错误/System/Library/Frameworks/Ruby.frame

  10. ruby-on-rails - 在 Rails 中更高效地查找或创建多条记录 - 2

    我有一个应用需要发送用户事件邀请。当用户邀请friend(用户)参加事件时,如果尚不存在将用户连接到该事件的新记录,则会创建该记录。我的模型由用户、事件和events_user组成。classEventdefinvite(user_id,*args)user_id.eachdo|u|e=EventsUser.find_or_create_by_event_id_and_user_id(self.id,u)e.save!endendend用法Event.first.invite([1,2,3])我不认为以上是完成我的任务的最有效方法。我设想了一种方法,例如Model.find_or_cr

随机推荐