Three.js开发案例:VR全景看房

项目初始化

1. 创建并配置项目

使用可视化面板创建项目:

  • 功能选择:BableRouterCSS Pre-processorsLinter / FormatterUse config files
  • 配置选择:2.xLessESLint + Standard config

创建代码格式化配置文件.prettierrc.js

1
2
3
4
5
6
7
8
9
module.exports = {
semi: false, //代码末尾的分号
singleQuote: true, //使用单引号
bracketSpacing: true, //括号内部不要出现空格
useTabs: false, //使用 tab 缩进
tabWidth: 2, //缩进空格数
trailingComma: 'none', //末尾逗号
printWidth: 100, //行宽,超过这个数值才换行。否则默认每个标签属性单独占一行
}

在代码审查配置文件.eslintrc.js中添加一行规则:

1
2
3
4
5
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'space-before-function-paren': 0 // 函数括号前的空格
}

创建全局css样式文件/src/assets/css/global.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 全局样式表 */

* {
margin: 0;
padding: 0;
}

html,
body{
height: 100%;
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
display: none;
}

/src/main.js中导入全局样式文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// 导入全局样式表
import './assets/css/global.css'

Vue.config.productionTip = false

new Vue({
router,
render: h => h(App)
}).$mount('#app')

2. 初始化页面

在可视化面板中安装运行依赖three,初始化App.vue

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
<template>
<div class="container" ref="container"></div>
</template>

<script>
import * as THREE from 'three'
// 导入轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

export default {
data() {
return {}
},
mounted() {
this.scene = null // 场景
this.camera = null // 摄像机
this.renderer = null // 渲染器
this.controls = null // 轨道控制器

this.init()
this.render()
},
methods: {
init() {
// 初始化场景
this.scene = new THREE.Scene()

// 初始化摄像机(透视相机)
this.camera = new THREE.PerspectiveCamera(
75, // 摄像机视锥体垂直视野角度
window.innerWidth / window.innerHeight, // 摄像机视锥体长宽比
0.1, // 摄像机视锥体近端面
1000 // 摄像机视锥体远端面
)
this.camera.position.z = 5

// 初始化渲染器
this.renderer = new THREE.WebGLRenderer()
this.renderer.setSize(window.innerWidth, window.innerHeight) // 设置渲染的宽高尺寸大小

// 挂载完毕之后获取Dom
const container = this.$refs.container
container.appendChild(this.renderer.domElement)

// 实例化轨道控制器
this.controls = new OrbitControls(this.camera, container)
this.controls.enableDamping = true // 设置控制器阻尼
},
// 重绘渲染函数
render() {
this.controls.update() // 控制器阻尼的调用函数
this.renderer.render(this.scene, this.camera) // 使用渲染器,通过相机将场景渲染进来
requestAnimationFrame(this.render) // 请求动画帧,渲染下一帧的时候就会调用render函数,也就是回调自身
}
},
beforeDestroy() {
this.scene = null // 场景
this.camera = null // 摄像机
this.renderer = null // 渲染器
this.controls = null // 轨道控制器
}
}
</script>

<style lang="less" scoped>
.container {
height: 100vh;
width: 100vw;
background-color: #f0f0f0;
}
</style>


一、立方体场景(天空盒)

1. 添加立方体

1
2
3
4
5
// 添加立方体
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const cube = new THREE.Mesh(geometry, material)
this.scene.add(cube)

2. 添加图片纹理贴图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 添加立方体
const geometry = new THREE.BoxGeometry(1, 1, 1)
// const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
// const cube = new THREE.Mesh(geometry, material)
// this.scene.add(cube)

// 贴图(客厅living)
const arr = ['4_l', '4_r', '4_u', '4_d', '4_b', '4_f']
const boxMaterials = []
arr.forEach((item) => {
// 纹理加载(图片)
const texture = new THREE.TextureLoader().load(`./imgs/living/${item}.jpg`)
// 创建材质
boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
})
const cube = new THREE.Mesh(geometry, boxMaterials)
this.scene.add(cube)

3. 翻转贴图到内部

放大盒子,并设置z周缩放为负值,从而将贴图由外翻转到盒子内部:

1
2
3
4
5
6
7
8
9
10
11
12
// 贴图(客厅living)
const arr = ['4_l', '4_r', '4_u', '4_d', '4_b', '4_f']
const boxMaterials = []
arr.forEach((item) => {
// 纹理加载(图片)
const texture = new THREE.TextureLoader().load(`./imgs/living/${item}.jpg`)
// 创建材质
boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
})
const cube = new THREE.Mesh(geometry, boxMaterials)
cube.geometry.scale(1, 1, -1) // 让z轴反过来,使贴图内外翻转,让场景在盒子内部
this.scene.add(cube)

4. 调整部分贴图旋转

天花板和地面的贴图角度不对,给它们旋转过来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 贴图(客厅living)
const arr = ['4_l', '4_r', '4_u', '4_d', '4_b', '4_f']
const boxMaterials = []
arr.forEach((item) => {
// 纹理加载(图片)
const texture = new THREE.TextureLoader().load(`./imgs/living/${item}.jpg`)
// 创建材质
if (item === '4_u' || item === '4_d') {
texture.rotation = Math.PI // 旋转180度
texture.center = new THREE.Vector2(0.5, 0.5) // 设置纹理中心(二维向量)
boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
} else {
boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
}
})
const cube = new THREE.Mesh(geometry, boxMaterials)
cube.geometry.scale(1, 1, -1) // 让z轴反过来,使贴图内外翻转,让场景在盒子内部
this.scene.add(cube)

完整Vue代码

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
<template>
<div class="container" ref="container"></div>
</template>

<script>
import * as THREE from 'three'
// 导入轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

export default {
data() {
return {}
},
mounted() {
this.scene = null // 场景
this.camera = null // 摄像机
this.renderer = null // 渲染器
this.controls = null // 轨道控制器

this.init()
this.render()
},
methods: {
init() {
// 初始化场景
this.scene = new THREE.Scene()

// 初始化摄像机(透视相机)
this.camera = new THREE.PerspectiveCamera(
75, // 摄像机视锥体垂直视野角度
window.innerWidth / window.innerHeight, // 摄像机视锥体长宽比
0.1, // 摄像机视锥体近端面
1000 // 摄像机视锥体远端面
)
this.camera.position.z = 5
// this.camera.position.set(0, 0, 10) // 设置摄像机位置坐标
// this.camera.aspect = window.innerWidth / window.innerHeight // 更新摄像头宽高比例
// this.camera.updateProjectionMatrix() // 更新摄像机的投影矩阵
// this.scene.add(this.camera) // 将摄像机添加到场景中

// 添加立方体
const geometry = new THREE.BoxGeometry(10, 10, 10)
// const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
// const cube = new THREE.Mesh(geometry, material)
// this.scene.add(cube)

// 贴图(客厅living)
const arr = ['4_l', '4_r', '4_u', '4_d', '4_b', '4_f']
const boxMaterials = []
arr.forEach((item) => {
// 纹理加载(图片)
const texture = new THREE.TextureLoader().load(`./imgs/living/${item}.jpg`)
// 创建材质
if (item === '4_u' || item === '4_d') {
texture.rotation = Math.PI // 旋转180度
texture.center = new THREE.Vector2(0.5, 0.5) // 设置纹理中心(二维向量)
boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
} else {
boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
}
})
const cube = new THREE.Mesh(geometry, boxMaterials)
cube.geometry.scale(1, 1, -1) // 让z轴反过来,使贴图内外翻转,让场景在盒子内部
this.scene.add(cube)

// 初始化渲染器
this.renderer = new THREE.WebGLRenderer()
this.renderer.setSize(window.innerWidth, window.innerHeight) // 设置渲染的宽高尺寸大小

// // 监听屏幕画面大小变化,修改渲染器宽高和相机的比例,更新渲染画面
// window.addEventListener('resize', () => {
// this.camera.aspect = window.innerWidth / window.innerHeight // 更新摄像头宽高比例
// this.camera.updateProjectionMatrix() // 更新摄像机的投影矩阵
// this.renderer.setSize(window.innerWidth, window.innerHeight) // 更新渲染器
// this.renderer.setPixelRatio(window.deviceRixelRatio) // 设置渲染器的像素比(让其等于设备的像素比)
// })

// 挂载完毕之后获取Dom
const container = this.$refs.container
container.appendChild(this.renderer.domElement)

// 实例化轨道控制器
this.controls = new OrbitControls(this.camera, container)
this.controls.enableDamping = true // 设置控制器阻尼

// // 添加坐标轴辅助器
// const axesHelper = new THREE.AxesHelper(5)
// this.scene.add(axesHelper) // 添加到场景中
},
// 重绘渲染函数
render() {
this.controls.update() // 控制器阻尼的调用函数
this.renderer.render(this.scene, this.camera) // 使用渲染器,通过相机将场景渲染进来
requestAnimationFrame(this.render) // 请求动画帧,渲染下一帧的时候就会调用render函数,也就是回调自身
}
},
beforeDestroy() {
this.scene = null // 场景
this.camera = null // 摄像机
this.renderer = null // 渲染器
this.controls = null // 轨道控制器
}
}
</script>

<style lang="less" scoped>
.container {
height: 100vh;
width: 100vw;
background-color: #f0f0f0;
}
</style>


二、球体场景(全景图片)

1. 添加球体

1
const geometry = new THREE.SphereGeometry(5, 32, 32) // 半径、细分程度

2. 加载hdr纹理

1
2
3
4
5
6
7
8
9
10
11
12
// 导入RGBELoader,准备加载hdr纹理
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'

// 添加球体
const geometry = new THREE.SphereGeometry(5, 32, 32) // 半径、细分程度
const loader = new RGBELoader()
loader.load('./imgs/hdr/Living.hdr', (texture) => {
const material = new THREE.MeshBasicMaterial({ map: texture })
const sphere = new THREE.Mesh(geometry, material)
sphere.geometry.scale(1, 1, -1) // 让z轴反过来,使贴图内外翻转,让场景在盒子内部
this.scene.add(sphere)
})

完整Vue代码

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
<template>
<div class="container" ref="container"></div>
</template>

<script>
import * as THREE from 'three'
// 导入轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 导入RGBELoader,准备加载hdr纹理
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'

export default {
data() {
return {}
},
mounted() {
this.scene = null // 场景
this.camera = null // 摄像机
this.renderer = null // 渲染器
this.controls = null // 轨道控制器

this.init()
this.render()
},
methods: {
init() {
// 初始化场景
this.scene = new THREE.Scene()

// 初始化摄像机(透视相机)
this.camera = new THREE.PerspectiveCamera(
75, // 摄像机视锥体垂直视野角度
window.innerWidth / window.innerHeight, // 摄像机视锥体长宽比
0.1, // 摄像机视锥体近端面
1000 // 摄像机视锥体远端面
)
this.camera.position.z = 5

// // 添加立方体
// const geometry = new THREE.BoxGeometry(10, 10, 10)
// // const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
// // const cube = new THREE.Mesh(geometry, material)
// // this.scene.add(cube)

// // 贴图(客厅living)
// const arr = ['4_l', '4_r', '4_u', '4_d', '4_b', '4_f']
// const boxMaterials = []
// arr.forEach((item) => {
// // 纹理加载(图片)
// const texture = new THREE.TextureLoader().load(`./imgs/living/${item}.jpg`)
// // 创建材质
// if (item === '4_u' || item === '4_d') {
// texture.rotation = Math.PI // 旋转180度
// texture.center = new THREE.Vector2(0.5, 0.5) // 设置纹理中心(二维向量)
// boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
// } else {
// boxMaterials.push(new THREE.MeshBasicMaterial({ map: texture }))
// }
// })
// const cube = new THREE.Mesh(geometry, boxMaterials)
// cube.geometry.scale(1, 1, -1) // 让z轴反过来,使贴图内外翻转,让场景在盒子内部
// this.scene.add(cube)

// 添加球体
const geometry = new THREE.SphereGeometry(5, 32, 32) // 半径、细分程度
const loader = new RGBELoader()
loader.load('./imgs/hdr/Living.hdr', (texture) => {
const material = new THREE.MeshBasicMaterial({ map: texture })
const sphere = new THREE.Mesh(geometry, material)
sphere.geometry.scale(1, 1, -1) // 让z轴反过来,使贴图内外翻转,让场景在盒子内部
this.scene.add(sphere)
})

// 初始化渲染器
this.renderer = new THREE.WebGLRenderer()
this.renderer.setSize(window.innerWidth, window.innerHeight) // 设置渲染的宽高尺寸大小

// // 监听屏幕画面大小变化,修改渲染器宽高和相机的比例,更新渲染画面
// window.addEventListener('resize', () => {
// this.camera.aspect = window.innerWidth / window.innerHeight // 更新摄像头宽高比例
// this.camera.updateProjectionMatrix() // 更新摄像机的投影矩阵
// this.renderer.setSize(window.innerWidth, window.innerHeight) // 更新渲染器
// this.renderer.setPixelRatio(window.deviceRixelRatio) // 设置渲染器的像素比(让其等于设备的像素比)
// })

// 挂载完毕之后获取Dom
const container = this.$refs.container
container.appendChild(this.renderer.domElement)

// 实例化轨道控制器
this.controls = new OrbitControls(this.camera, container)
this.controls.enableDamping = true // 设置控制器阻尼

// // 添加坐标轴辅助器
// const axesHelper = new THREE.AxesHelper(5)
// this.scene.add(axesHelper) // 添加到场景中
},
// 重绘渲染函数
render() {
this.controls.update() // 控制器阻尼的调用函数
this.renderer.render(this.scene, this.camera) // 使用渲染器,通过相机将场景渲染进来
requestAnimationFrame(this.render) // 请求动画帧,渲染下一帧的时候就会调用render函数,也就是回调自身
}
},
beforeDestroy() {
this.scene = null // 场景
this.camera = null // 摄像机
this.renderer = null // 渲染器
this.controls = null // 轨道控制器
}
}
</script>

<style lang="less" scoped>
.container {
height: 100vh;
width: 100vw;
background-color: #f0f0f0;
}
</style>


三、小思考

全景效果是进入物体内部实现的,但是鼠标拖动屏幕时,画面的旋转方向是按照外面的情况旋转的。

因此,造成了观看全景场景的时候鼠标拖动方向与画面旋转方向“相反”。如何解决?


【参考内容】: