第三课threejs全景预览房间案例

> 背景: 如何在网页中预览房间每个角度? 如全景看房

## 功能实现思路

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)

    THE END
    喜欢就支持一下吧
    点赞12 分享
    评论 抢沙发
    头像
    欢迎您留下宝贵的见解!
    提交
    头像

    昵称

    取消
    昵称表情代码图片

      暂无评论内容