> 背景: 如何在网页中预览房间每个角度? 如全景看房
## 功能实现思路
1. 创建threejs场景
2. 创建球体
3. 创建鱼眼全景图片
4. 翻转球体将鱼眼图贴在球体内部
5. 相机设置在球体中心,循环更好相机拍摄目标位置
## 第一步
> 创建盛放场景盒子div
“`js
“`
## 第二步
> 引入vue, threejs
“`js
import Vue from vue;
import * as THREE from three;
“`
## 第三步
> 创建基础Data数据
“`js
data (): DataType {
return {
id: null,
domW: 0,
domH: 0,
scene: THREE.Scene,
camera: THREE.PerspectiveCamera,
renderer: THREE.WebGLRenderer,
mesh: THREE.Mesh,
material: new THREE.MeshBasicMaterial(),
// controls: OrbitControls,
onMouseDownMouseX: 0,
onMouseDownMouseY: 0,
lon: 0,
lat: 0,
onMouseDownLon: 0,
onMouseDownLat: 0,
phi: 0,
theta: 0,
isUserInteracting: false
}
},
“`
## 第四步
> 初始运行创建场景,获取场景宽度高度
“`js
mounted () {
this.id = document.getElementById(three)
this.domW = this.id.offsetWidth
this.domH = this.id.offsetHeight
this.init()
},
“`
## 第五步
> init 函数讲解
“`js
init () {
// 创建场景
this.scene = new THREE.Scene()
// 创建近大远小(透视投影)相机
this.camera = new THREE.PerspectiveCamera(75, this.domW / this.domH, 0.01, 1100)
// 返回一个能够表示当前摄像机所正视(拍摄)的世界空间方向的Vector3对象
this.camera.target = new THREE.Vector3(0, 0, 0)
// 创建渲染函数
this.renderer = new THREE.WebGLRenderer({
antialias: true, // 模型抗锯齿
alpha: true // 开启背景透明
})
// 设置渲染场景大小
this.renderer.setSize(this.domW, this.domH)
// 将场景添加到div标签
this.id.appendChild(this.renderer.domElement)
// 添加灯光
this.addLight()
// 添加场景辅助线
this.axisHelper()
// 添加球体设置材质
this.initSphereGeometry()
// 添加事件监听器,配合鼠标做不同的位置变换
this.addEventListenFn()
// 刷帧渲染动画
this.animate()
// 响应屏幕改变大小函数
this.onWindowResize()
},
“`
## 第五步
> 添加灯光
“`js
addLight () {
// 设置环境光
const ambientLight = new THREE.AmbientLight(#ffffff)
this.scene.add(ambientLight)
// 设置平行光
const light = new THREE.DirectionalLight(#ffffff)
this.scene.add(light)
// 设置点光源
const pointLight = new THREE.PointLight(#ffffff, 0.1, 1000)
pointLight.position.set(300, 300, 300)
this.scene.add(pointLight)
},
“`
## 第六步
> 添加空间辅助线
“`js
axisHelper () {
const axes:THREE.AxesHelper = new THREE.AxesHelper(800)
this.scene.add(axes)
},
“`
## 第七步
> 创建圆球盛放鱼眼材质图
> 知识点:
`SphereBufferGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float)`
* radius — 球体半径,默认为1。
* widthSegments — 水平分段数(沿着经线分段),最小值为3,默认值为8。
* heightSegments — 垂直分段数(沿着纬线分段),最小值为2,默认值为6。
* phiStart — 指定水平(经线)起始角度,默认值为0。。
* phiLength — 指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2。
* thetaStart — 指定垂直(纬线)起始角度,默认值为0。
* thetaLength — 指定垂直(纬线)扫描角度大小,默认值为 Math.PI。
* 该几何体是通过扫描并计算围绕着Y轴(水平扫描)和X轴(垂直扫描)的顶点来创建的。 因此,不完整的球体(类似球形切片)可以通过为phiStart,phiLength,thetaStart和thetaLength设置不同的值来创建, 以定义我们开始(或结束)计算这些顶点的起点(或终点)。
*
*
“`js
initSphereGeometry () {
// 创建半径500的球体 (球缓冲几何体)
const geometry = new THREE.SphereBufferGeometry(500, 60, 40)
geometry.scale(-1, 1, 1)
// 创建材质获取材质图片鱼眼图
this.material = new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load(https://www.douchuanwei.com/api/files/img/a.jpg)
})
this.mesh = new THREE.Mesh(geometry, this.material)
// 将球体添加到场景
this.scene.add(this.mesh)
},
“`
## 第八步
> 添加document 事件监听
“`js
addEventListenFn () {
const _this:any = this
// 鼠标按下获取鼠标xy坐标转换成球体经纬度
document.addEventListener(mousedown, this.onPointerStart, false)
// 鼠标移动计算变化后的球体经纬度
document.addEventListener(mousemove, this.onPointerMove, false)
// 鼠标抬起停止跟随
document.addEventListener(mouseup, this.onPointerUp, false)
// 鼠标滚轮放大缩小摄像机目标距离
document.addEventListener(wheel, this.onDocumentMouseWheel, false)
// 移动端手指移上获取鼠标xy坐标转换成球体经纬度
document.addEventListener(touchstart, this.onPointerStart, false)
// 移动端手指移动计算变化后的球体经纬度
document.addEventListener(touchmove, this.onPointerMove, false)
// 移动端手指抬起停止跟随
document.addEventListener(touchend, this.onPointerUp, false)
// 拖拽
document.addEventListener(dragover, (e:any) => {
e.preventDefault()
e.dataTransfer.dropEffect = copy
}, false)
// 拓拽停止设置body 半透明
document.addEventListener(dragenter, () => {
document.body.style.opacity = 0.5
}, false)
// 拖拽离开回归透明度
document.addEventListener(dragleave, () => {
document.body.style.opacity = 1
}, false)
document.addEventListener(drop, (e:any) => {
e.preventDefault()
// 读取图片为二进制码
const reader = new FileReader()
reader.addEventListener(load, (es:any) => {
// 更新材质
_this.material.map.image.src = es.target.result
_this.material.needsUpdate = true
}, false)
reader.readAsDataURL(e.dataTransfer.files[0])
document.body.style.opacity = 1
}, false)
},
“`
## 第九步
> 获取鼠标 x y 坐标,设置经纬度
“`js
onPointerStart (e) {
this.isUserInteracting = true
// 获取鼠标x y 坐标
const clientX = e.clientX || e.touches[0].clientX
const clientY = e.clientY || e.touches[0].clientY
this.onMouseDownMouseX = clientX
this.onMouseDownMouseY = clientY
// 设置经纬度
this.onMouseDownLon = this.lon
this.onMouseDownLat = this.lat
},
“`
## 第十步
> 鼠标移动转换移动坐标到经纬度
“`js
onPointerMove (e) {
if (this.isUserInteracting) {
const clientX = e.clientX || e.touches[0].clientX
const clientY = e.clientY || e.touches[0].clientY
this.lon = (this.onMouseDownMouseX – clientX) * 0.1 + this.onMouseDownLon
this.lat = (clientY – this.onMouseDownMouseY) * 0.1 + this.onMouseDownLat
}
},
“`
## 第十一步
> 鼠标离开停止经纬度转换
“`js
onPointerUp () {
this.isUserInteracting = false
},
“`
## 第十二步
> 鼠标滚轮放大缩小场景,设置相机拍摄目标位置
“`js
onDocumentMouseWheel (e) {
const fov = this.camera.fov + e.deltaY * 0.05
this.camera.fov = THREE.MathUtils.clamp(fov, 10, 75)
this.camera.updateProjectionMatrix()
},
“`
## 第十二步
> 刷帧函数讲解
“`js
animate () {
window.requestAnimationFrame(this.animate)
// 更新相机 旋转场景空间
this.updateFn()
}
“`
## 第十三步
> 更新场景函数
“`js
updateFn () {
if (!this.isUserInteracting) {
// 经度每帧更新0.1 场景自动旋转起来
this.lon += 0.1
}
this.lat = Math.max(-85, Math.min(85, this.lat))
this.phi = THREE.MathUtils.degToRad(90 – this.lat)
this.theta = THREE.MathUtils.degToRad(this.lon)
// 更新相机 目标 x y z 位置
this.camera.target.x = 500 * Math.sin(this.phi) * Math.cos(this.theta)
this.camera.target.y = 500 * Math.cos(this.phi)
this.camera.target.z = 500 * Math.sin(this.phi) * Math.sin(this.theta)
// 相机拍摄目标(始终拍摄这里)
this.camera.lookAt(this.camera.target)
this.renderer.render(this.scene, this.camera)
},
“`
## 第十四步
> 防止浏览器窗口变化随时响应场景大小
“`js
onWindowResize () {
window.onresize = () => {
this.domH = this.id.offsetHeight
this.domW = this.id.offsetWidth
this.camera.aspect = this.domW / this.domH
this.camera.updateProjectionMatrix()
this.renderer.setSize(this.domW, this.domH)
}
},
“`
## 完整示例:
“`js
import Vue from vue;
import * as THREE from three;
// import { OrbitControls } from three/examples/jsm/controls/OrbitControls;
interface DataType {
id: HTMLElement | any;
domW: Number | any;
domH: Number | any;
scene: THREE.Scene | any;
camera: THREE.PerspectiveCamera | any;
renderer: THREE.WebGLRenderer | any;
mesh: THREE.Mesh | any;
material: THREE.MeshBasicMaterial;
// controls?: OrbitControls | any;
onMouseDownMouseX: number;
onMouseDownMouseY: number;
lon: number;
lat: number;
onMouseDownLon: number;
onMouseDownLat: number;
phi: number;
theta: number;
isUserInteracting: Boolean;
}
export default Vue.extend({
data (): DataType {
return {
id: null,
domW: 0,
domH: 0,
scene: THREE.Scene,
camera: THREE.PerspectiveCamera,
renderer: THREE.WebGLRenderer,
mesh: THREE.Mesh,
material: new THREE.MeshBasicMaterial(),
// controls: OrbitControls,
onMouseDownMouseX: 0,
onMouseDownMouseY: 0,
lon: 0,
lat: 0,
onMouseDownLon: 0,
onMouseDownLat: 0,
phi: 0,
theta: 0,
isUserInteracting: false
}
},
mounted () {
this.id = document.getElementById(three)
this.domW = this.id.offsetWidth
this.domH = this.id.offsetHeight
this.init()
},
methods: {
init () {
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(75, this.domW / this.domH, 0.01, 1100)
this.camera.target = new THREE.Vector3(0, 0, 0)
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
})
this.renderer.setSize(this.domW, this.domH)
this.id.appendChild(this.renderer.domElement)
this.addLight()
this.axisHelper()
this.initSphereGeometry()
this.addEventListenFn()
this.animate()
this.onWindowResize()
},
controlsFn () {
// this.controls = new OrbitControls(this.camera, this.renderer.domElement)
},
initSphereGeometry () {
const geometry = new THREE.SphereBufferGeometry(500, 60, 40)
geometry.scale(-1, 1, 1)
this.material = new THREE.MeshBasicMaterial({
map: new THREE.TextureLoader().load(https://www.douchuanwei.com/api/files/img/a.jpg)
})
this.mesh = new THREE.Mesh(geometry, this.material)
this.scene.add(this.mesh)
},
addEventListenFn () {
const _this:any = this
document.addEventListener(mousedown, this.onPointerStart, false)
document.addEventListener(mousemove, this.onPointerMove, false)
document.addEventListener(mouseup, this.onPointerUp, false)
document.addEventListener(wheel, this.onDocumentMouseWheel, false)
document.addEventListener(touchstart, this.onPointerStart, false)
document.addEventListener(touchmove, this.onPointerMove, false)
document.addEventListener(touchend, this.onPointerUp, false)
document.addEventListener(dragover, (e:any) => {
e.preventDefault()
e.dataTransfer.dropEffect = copy
}, false)
document.addEventListener(dragenter, () => {
document.body.style.opacity = 0.5
}, false)
document.addEventListener(dragleave, () => {
document.body.style.opacity = 1
}, false)
document.addEventListener(drop, (e:any) => {
e.preventDefault()
const reader = new FileReader()
reader.addEventListener(load, (es:any) => {
_this.material.map.image.src = es.target.result
_this.material.needsUpdate = true
}, false)
reader.readAsDataURL(e.dataTransfer.files[0])
document.body.style.opacity = 1
}, false)
},
onPointerStart (e) {
this.isUserInteracting = true
const clientX = e.clientX || e.touches[0].clientX
const clientY = e.clientY || e.touches[0].clientY
this.onMouseDownMouseX = clientX
this.onMouseDownMouseY = clientY
this.onMouseDownLon = this.lon
this.onMouseDownLat = this.lat
},
onPointerMove (e) {
if (this.isUserInteracting) {
const clientX = e.clientX || e.touches[0].clientX
const clientY = e.clientY || e.touches[0].clientY
this.lon = (this.onMouseDownMouseX – clientX) * 0.1 + this.onMouseDownLon
this.lat = (clientY – this.onMouseDownMouseY) * 0.1 + this.onMouseDownLat
}
},
onPointerUp () {
this.isUserInteracting = false
},
onDocumentMouseWheel (e) {
const fov = this.camera.fov + e.deltaY * 0.05
this.camera.fov = THREE.MathUtils.clamp(fov, 10, 75)
this.camera.updateProjectionMatrix()
},
addLight () {
const ambientLight = new THREE.AmbientLight(#ffffff)
this.scene.add(ambientLight)
const light = new THREE.DirectionalLight(#ffffff)
this.scene.add(light)
const pointLight = new THREE.PointLight(#ffffff, 0.1, 1000)
pointLight.position.set(300, 300, 300)
this.scene.add(pointLight)
},
axisHelper () {
const axes:THREE.AxesHelper = new THREE.AxesHelper(800)
this.scene.add(axes)
},
onWindowResize () {
window.onresize = () => {
this.domH = this.id.offsetHeight
this.domW = this.id.offsetWidth
this.camera.aspect = this.domW / this.domH
this.camera.updateProjectionMatrix()
this.renderer.setSize(this.domW, this.domH)
}
},
updateFn () {
if (!this.isUserInteracting) {
this.lon += 0.1
}
this.lat = Math.max(-85, Math.min(85, this.lat))
this.phi = THREE.MathUtils.degToRad(90 – this.lat)
this.theta = THREE.MathUtils.degToRad(this.lon)
this.camera.target.x = 500 * Math.sin(this.phi) * Math.cos(this.theta)
this.camera.target.y = 500 * Math.cos(this.phi)
this.camera.target.z = 500 * Math.sin(this.phi) * Math.sin(this.theta)
this.camera.lookAt(this.camera.target)
this.renderer.render(this.scene, this.camera)
},
animate () {
window.requestAnimationFrame(this.animate)
this.updateFn()
// this.mesh.rotation.y += 0.02
// this.mesh.rotation.x += 0.01
// this.renderer.render(this.scene, this.camera)
}
},
});
“`
[更多内容请到小豆包》](https://www.douchuanwei.com/)
> 扫码访问小豆包
>
![23_34de43370a343405db1b00359bb895cd.png](https://www.douchuanwei.com/api/files/2022-03-05/png/23_34de43370a343405db1b00359bb895cd.png)
### 扫码关注小豆包公众号
![小豆包公众号.jpg](https://www.douchuanwei.com/api/files/2022-03-05/jpeg/小豆包公众号.jpg)
暂无评论内容