开发案例:水天一色小岛

项目初始化

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
18
/* 全局样式表 */

* {
margin: 0;
padding: 0;
}

html,
body{
height: 100%;
margin: 0;
padding: 0;
background-color: skyblue;
}
::-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
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
<template>
<div></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, // 摄像机视锥体近端面
2000 // 摄像机视锥体远端面
)
this.camera.position.set(-50, -50, 130) // 设置摄像机位置坐标
this.camera.aspect = window.innerWidth / window.innerHeight // 更新摄像头宽高比例
this.camera.updateProjectionMatrix() // 更新摄像机的投影矩阵
this.scene.add(this.camera) // 将摄像机添加到场景中

// // 创建物体
// // 1.几何体(球缓冲几何体)
// const sphereGeometry = new THREE.SphereGeometry(1, 20, 20)
// // 2.材质
// const material = new THREE.MeshStandardMaterial()
// // 3.根据集合体和材质创建物体
// const sphere = new THREE.Mesh(sphereGeometry, material)
// this.scene.add(sphere)

// // 创建平面
// const planeGeometry = new THREE.PlaneBufferGeometry(10, 10) // 面片、线或点几何体的有效表述。包括顶点位置,面片索引、法相量、颜色值、UV 坐标和自定义缓存属性值
// const plane = new THREE.Mesh(planeGeometry, material)
// plane.position.set(0, -1, 0)
// plane.rotation.x = -Math.PI / 2
// // 设置物体(平面)接收阴影
// plane.receiveShadow = true
// this.scene.add(plane)

// // 灯光
// // 环境光
// const light = new THREE.AmbientLight(0xffffff, 0.5) // soft white light
// this.scene.add(light)
// // 直线光源
// const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
// directionalLight.position.set(10, 10, 10) // 平行光位置
// this.scene.add(directionalLight)

// 初始化渲染器
this.renderer = new THREE.WebGLRenderer({
antialias: true // 设置抗锯齿
})
this.renderer.outputEncoding = THREE.sRGBEncoding // 设置渲染输出环境的编码,使画面更好看
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) // 设置渲染器的像素比(让其等于设备的像素比)
})

// 将WebGL渲染的内容canvas添加到页面body
document.body.appendChild(this.renderer.domElement)

// 实例化轨道控制器
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
// 设置控制器阻尼,让其更有真实效果
// 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>
</style>

本次项目虽然也是用的Vue2.x,但我这次尝试舍弃data(),将three.js的场景/摄像机/渲染器/控制器等都放在mounted()中。一个原因是我看到好几篇文章都提到放在data中会导致项目运行使用卡顿,另一个原因是提早适应Vue3.x的书写逻辑。

运行并启动项目,开始开发。


场景搭建

天空球

  • 创建天空球

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 创建一个巨大的天空球
    const texture = new THREE.TextureLoader().load('./textures/sky.jpg') // 天空纹理
    const skyGeometry = new THREE.SphereGeometry(1000, 60, 60) // 半径、细分程度
    const skyMaterial = new THREE.MeshBasicMaterial({
    map: texture
    })
    skyGeometry.scale(1, 1, -1) // 将几何体内外翻转颠倒过来(否则球内是黑的,只有球外是亮的)
    const sky = new THREE.Mesh(skyGeometry, skyMaterial)
    this.scene.add(sky)
  • 创建视频纹理(如果监听的事件是鼠标移动mousemove,需要设置自动播放video.muted = true,否则浏览器可能因为鼠标移动不算用户与网页交互,从而无法执行音视频播放)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 创建视频纹理
    const video = document.createElement('video')
    video.src = './textures/sky.mp4'
    video.loop = true // 循环播放
    // video.muted = true // 自动播放
    // 监听鼠标事件播放视频
    window.addEventListener('click', (e) => {
    // window.addEventListener('mousemove', (e) => {
    // 判断当前是否处于播放状态
    if (video.paused) {
    video.play()
    skyMaterial.map = new THREE.VideoTexture(video)
    skyMaterial.map.needsUpdate = true
    }
    })

水面

  • 创建水面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 导入水面
    import { Water } from 'three/examples/jsm/objects/Water2'

    // 创建水面(平面圆形几何体)
    const waterGeometry = new THREE.CircleGeometry(300, 64) // 半径、细分程度
    const water = new Water(waterGeometry, {
    textureWidth: 1024, // 纹理宽度(细分程度)
    textureHeight: 1024, // 纹理高度(细分程度)
    color: 0xeeeeff, // 水面颜色
    flowDirection: new THREE.Vector2(1, 1), // 水面流动方向
    scale: 1 // 水面波纹大小
    })
    water.rotation.x = -Math.PI / 2 // 将水面从竖直旋转至水平
    this.scene.add(water)

小岛

  • 添加小岛模型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 导入glTF载入库
    import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
    // 导入Draco载入库(用于解压模型)
    import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'

    // 添加小岛模型
    const loader = new GLTFLoader() // 实例化glTF载入库
    const dracoLoader = new DRACOLoader() // 实例化Draco载入库
    dracoLoader.setDecoderPath('./draco/') // 添加Draco载入库
    loader.setDRACOLoader(dracoLoader) // 将loader和解压的码放在一起

    // 加载模型
    loader.load('./model/island2.glb', (gltf) => {
    this.scene.add(gltf.scene)
    })

场景调整

导入场景HDR纹理

  • 现在小岛是纯黑色的,需要载入hdr环境纹理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 导入RGBELoader,用于导入hdr图
    import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader'

    // 载入环境纹理hdr
    const hdrLoader = new RGBELoader()
    // 异步导入
    hdrLoader.loadAsync('./assets/050.hdr').then((texture) => {
    texture.mapping = THREE.EquirectangularReflectionMapping // 球面映射
    this.scene.background = texture // 场景背景
    this.scene.environment = texture // 场景环境
    })

添加平行光

  • 如果认为场景不够亮,可以添加一个平行光,相当于太阳光
    1
    2
    3
    4
    // 导入平行光
    const light = new THREE.DirectionalLight(0xffffff, 1)
    light.position.set(-100, 100, 10)
    this.scene.add(light)

提高水平面

  • 将水平面提高3米,没过小岛模型沙滩边缘
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 创建水面(平面圆形几何体)
    const waterGeometry = new THREE.CircleGeometry(300, 64) // 半径、细分程度
    const water = new Water(waterGeometry, {
    textureWidth: 1024, // 纹理宽度(细分程度)
    textureHeight: 1024, // 纹理高度(细分程度)
    color: 0xeeeeff, // 水面颜色
    flowDirection: new THREE.Vector2(1, 1), // 水面流动方向
    scale: 1 // 水面波纹大小
    })
    water.rotation.x = -Math.PI / 2 // 将水面从竖直旋转至水平
    water.position.y = 3 // 将水平面提高3米,没过小岛模型沙滩边缘
    this.scene.add(water)

对数深度缓冲区

  • 模型在旋转的时候部分面会闪烁,是由于模型有太多的面,可能靠得很近,渲染的时候不知道渲染哪一个。可以对渲染器设置对数深度缓冲区logarithmicDepthBuffer
    1
    2
    3
    4
    5
    6
    7
    // 初始化渲染器
    this.renderer = new THREE.WebGLRenderer({
    antialias: true, // 设置抗锯齿
    logarithmicDepthBuffer: true // 对数深度缓冲区
    })
    this.renderer.outputEncoding = THREE.sRGBEncoding // 设置渲染输出环境的编码,使画面更好看
    this.renderer.setSize(window.innerWidth, window.innerHeight) // 设置渲染的宽高尺寸大小

完整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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<template>
<div></div>
</template>

<script>
import * as THREE from 'three'
// 导入轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 导入水面
import { Water } from 'three/examples/jsm/objects/Water2'
// 导入glTF载入库
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// 导入Draco载入库(用于解压模型)
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
// 导入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, // 摄像机视锥体近端面
2000 // 摄像机视锥体远端面
)
this.camera.position.set(-50, -50, 130) // 设置摄像机位置坐标
this.camera.aspect = window.innerWidth / window.innerHeight // 更新摄像头宽高比例
this.camera.updateProjectionMatrix() // 更新摄像机的投影矩阵
this.scene.add(this.camera) // 将摄像机添加到场景中

// 创建一个巨大的天空球
const texture = new THREE.TextureLoader().load('./textures/sky.jpg') // 天空纹理
const skyGeometry = new THREE.SphereGeometry(1000, 60, 60) // 半径、细分程度
const skyMaterial = new THREE.MeshBasicMaterial({
map: texture
})
skyGeometry.scale(1, 1, -1) // 将几何体内外翻转颠倒过来(否则球内是黑的,只有球外是亮的)
const sky = new THREE.Mesh(skyGeometry, skyMaterial)
this.scene.add(sky)

// 创建视频纹理
const video = document.createElement('video')
video.src = './textures/sky.mp4'
video.loop = true // 循环播放
// 监听鼠标事件播放视频
window.addEventListener('click', (e) => {
// 判断当前是否处于播放状态
if (video.paused) {
video.play()
skyMaterial.map = new THREE.VideoTexture(video)
skyMaterial.map.needsUpdate = true
}
})

// 载入环境纹理hdr
const hdrLoader = new RGBELoader()
// 异步导入
hdrLoader.loadAsync('./assets/050.hdr').then((texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping // 球面映射
this.scene.background = texture // 场景背景
this.scene.environment = texture // 场景环境
})

// 导入平行光
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(-100, 100, 10)
this.scene.add(light)

// 创建水面(平面圆形几何体)
const waterGeometry = new THREE.CircleGeometry(300, 64) // 半径、细分程度
const water = new Water(waterGeometry, {
textureWidth: 1024, // 纹理宽度(细分程度)
textureHeight: 1024, // 纹理高度(细分程度)
color: 0xeeeeff, // 水面颜色
flowDirection: new THREE.Vector2(1, 1), // 水面流动方向
scale: 1 // 水面波纹大小
})
water.rotation.x = -Math.PI / 2 // 将水面从竖直旋转至水平
water.position.y = 3 // 将水平面提高3米,没过小岛模型沙滩边缘
this.scene.add(water)

// 添加小岛模型
const loader = new GLTFLoader() // 实例化glTF载入库
const dracoLoader = new DRACOLoader() // 实例化Draco载入库
dracoLoader.setDecoderPath('./draco/') // 添加Draco载入库
loader.setDRACOLoader(dracoLoader) // 将loader和解压的码放在一起

// 加载模型
loader.load('./model/island2.glb', (gltf) => {
this.scene.add(gltf.scene)
})

// 初始化渲染器
this.renderer = new THREE.WebGLRenderer({
antialias: true, // 设置抗锯齿
logarithmicDepthBuffer: true // 对数深度缓冲区
})
this.renderer.outputEncoding = THREE.sRGBEncoding // 设置渲染输出环境的编码,使画面更好看
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) // 设置渲染器的像素比(让其等于设备的像素比)
})

// 将WebGL渲染的内容canvas添加到页面body
document.body.appendChild(this.renderer.domElement)

// 实例化轨道控制器
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
// 设置控制器阻尼,让其更有真实效果
// 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>
</style>


【参考内容】: