介绍

该实例是threejs制作的下雨场景,使用Points创建一个粒子集合,PointsMaterial加贴图方式美化粒子,项目是一个练习demo,若有错误请指正,感谢大佬。

演示地址(在github上有点卡,请见谅)
在这里插入图片描述

实现

场景创建

1
let scene: THREE.Scene = new THREE.Scene();

相机

透视相机:PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )

  • fov — 摄像机视锥体垂直视野角度
  • aspect — 摄像机视锥体长宽比
  • near — 摄像机视锥体近端面
  • far — 摄像机视锥体远端面

若想了解正交相机与透视相机的区别请移步到官网demo

1
2
3
4
5
6
7
const initCamera = (width: number, height: number): void => {
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 40);
// 设置相机位置
camera.position.set(0, 0, 40);
// 将相机添加到场景中
scene.add(camera);
};

坐标辅助器

1
2
3
4
const initAxesHelper = (): void => {
const axesHelper: THREE.AxesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
};

状态检测器

1
2
3
4
const initStats = (): void => {
stats = new Stats();
canvas.value.appendChild(stats.dom);
};

渲染器

这里说一下,有些同学在网上看的教程会有使用 WebGL1Renderer 来创建渲染器,那到底是使用WebGLRenderer还是WebGL1Renderer,接下来我们来了解下这两的区别

如果看过WebGL的同学应该知道WebGL有1和2两个版本,从r118起,WebGLRenderer会自动使用WebGL2来做渲染

那么这两个版本有何区别,简单说就是多了更多纹理格式、内置函数、3D 纹理贴图,同时还支持了非2的整数次方大小的图片。同时,WebGL2WebGL1 在对浏览器的兼容性上有很大的差异,这两者对浏览器兼容产生的巨大差异会导致陈旧的WebGL1的系统崩溃,所以threejs给我们提供了WebGL1Renderer来进行适配兼容

1
2
3
4
5
6
const initRenderer = (width: number, height: number): void => {
renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
canvas.value.appendChild(renderer.domElement);
renderer.render(scene, camera);
};

轨道控制器

1
2
3
4
5
6
7
8
9
10
11
const initControls = (): void => {
controls = new OrbitControls(camera, renderer.domElement);
// 使动画循环使用时阻尼或自转 意思是否有惯性
controls.enableDamping = true;
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
//是否开启右键拖拽
controls.enablePan = true;
};

创建雨水粒子

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
const createRaining = (
size: number,
transparent: boolean,
opacity: number,
vertexColors: boolean,
sizeAttenuation: boolean,
color: number,
): void => {
let geometry: THREE.BufferGeometry = new THREE.BufferGeometry();

// pointCount * 3 共pointCount个点,每个点有x,y,z三个坐标,所以需要*3
let positions: Float32Array = new Float32Array(pointCount * 3);
for (let i = 0; i < pointCount * 3; i++) {
positions[i] = (Math.random() - 0.5) * 100;
}
// 设置顶点
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));

// 加载雨滴贴图
let textureLoader: THREE.TextureLoader = new THREE.TextureLoader();
let rainTexture: THREE.Texture = textureLoader.load(rainingPng);
// 点材质
let material: THREE.PointsMaterial = new THREE.PointsMaterial({
size: size, // 大小
transparent: transparent, // 材质是否透明,配合opacity设置透明度
opacity: opacity, // 透明度
vertexColors: vertexColors, // 顶点着色
sizeAttenuation: sizeAttenuation, // 指定点的大小是否因相机深度而衰减
color: color, // 颜色
depthTest: true, // 渲染此材质时启用深度测试
depthWrite: false, // 渲染此材质是否对深度缓冲区有任何影响
map: rainTexture, // 贴图
alphaMap: rainTexture, // 贴图灰度
blending: THREE.AdditiveBlending, // 材质混合模式
});

// 生成点
point = new THREE.Points(geometry, material);
// 添加到场景
scene.add(point);
};

调试器

创建调试器控制粒子数据

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
const initGui = (): void => {
const datGui = new dat.GUI();
const guiConfig = {
size: 1,
transparent: true,
opacity: 0.5,
vertexColors: false,
sizeAttenuation: true,
color: 0xededed,
rotateSystem: false,
reDraw: () => {
// 点存在时移除掉,避免创建相同的point导致性能变差
if (point) {
scene.remove(point);
}
createRaining(
guiConfig.size,
guiConfig.transparent,
guiConfig.opacity,
guiConfig.vertexColors,
guiConfig.sizeAttenuation,
guiConfig.color,
);
controls.autoRotate = guiConfig.rotateSystem;
},
};
datGui.add(guiConfig, "size", 0.1, 3).onChange(guiConfig.reDraw);
datGui.add(guiConfig, "transparent").onChange(guiConfig.reDraw);
datGui.add(guiConfig, "opacity", 0.1, 1).onChange(guiConfig.reDraw);
datGui.add(guiConfig, "vertexColors").onChange(guiConfig.reDraw);
datGui.add(guiConfig, "sizeAttenuation").onChange(guiConfig.reDraw);
datGui.addColor(guiConfig, "color").onChange(guiConfig.reDraw);
datGui.add(guiConfig, "rotateSystem").onChange(guiConfig.reDraw);

guiConfig.reDraw();
};

渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const render = (): void => {
controls.update();
renderer.render(scene, camera);
if (stats) {
stats.update();
}
if (point) {
// 雨水动画,修改顶点位置来模拟雨水降落效果
const positions = (point.geometry.getAttribute("position") as any).array;
for (let i = 0; i < pointCount * 3; i += 3) {
positions[i + 1] -= Math.random() * 1;
if (positions[i + 1] < -40) {
positions[i + 1] = 40;
}
}
// 更新顶点
point.geometry.getAttribute("position").needsUpdate = true;
}
// 使用requestAnimationFrame函数进行帧渲染
requestAnimationFrame(render);
};

完整代码 raining.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<template>
<div id="canvas" ref="canvas"></div>
</template>

<script lang="ts" setup>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import dat from "dat.gui";
import Stats from "stats.js";
import { nextTick, ref } from "vue";
import rainingPng from "../assets/rain.png";

const canvas = ref<any>(null);
let scene: THREE.Scene = new THREE.Scene();
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let controls: any;
let stats: any;
let point: THREE.Points;
const pointCount: number = 5000;

nextTick(() => {
initCamera(canvas.value.clientWidth, canvas.value.clientHeight);
initRenderer(canvas.value.clientWidth, canvas.value.clientHeight);
// initAxesHelper();
initControls();
render();
initStats();
initGui();
});

const initCamera = (width: number, height: number): void => {
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 40);
camera.position.set(0, 0, 40);
scene.add(camera);
};

const initRenderer = (width: number, height: number): void => {
renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
canvas.value.appendChild(renderer.domElement);
renderer.render(scene, camera);
};

const initStats = (): void => {
stats = new Stats();
canvas.value.appendChild(stats.dom);
};

const initAxesHelper = (): void => {
const axesHelper: THREE.AxesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);
};

const initControls = (): void => {
controls = new OrbitControls(camera, renderer.domElement);
// 使动画循环使用时阻尼或自转 意思是否有惯性
controls.enableDamping = true;
//是否可以缩放
controls.enableZoom = true;
//是否自动旋转
controls.autoRotate = false;
//是否开启右键拖拽
controls.enablePan = true;
};

const render = (): void => {
controls.update();
renderer.render(scene, camera);
if (stats) {
stats.update();
}
if (point) {
const positions = (point.geometry.getAttribute("position") as any).array;
for (let i = 0; i < pointCount * 3; i += 3) {
positions[i + 1] -= Math.random() * 1;
if (positions[i + 1] < -40) {
positions[i + 1] = 40;
}
}
point.geometry.getAttribute("position").needsUpdate = true;
}
requestAnimationFrame(render);
};

const initGui = (): void => {
const datGui = new dat.GUI();
const guiConfig = {
size: 1,
transparent: true,
opacity: 0.5,
vertexColors: false,
sizeAttenuation: true,
color: 0xededed,
rotateSystem: false,
reDraw: () => {
if (point) {
scene.remove(point);
}
createRaining(
guiConfig.size,
guiConfig.transparent,
guiConfig.opacity,
guiConfig.vertexColors,
guiConfig.sizeAttenuation,
guiConfig.color,
);
controls.autoRotate = guiConfig.rotateSystem;
},
};
datGui.add(guiConfig, "size", 0.1, 3).onChange(guiConfig.reDraw);
datGui.add(guiConfig, "transparent").onChange(guiConfig.reDraw);
datGui.add(guiConfig, "opacity", 0.1, 1).onChange(guiConfig.reDraw);
datGui.add(guiConfig, "vertexColors").onChange(guiConfig.reDraw);
datGui.add(guiConfig, "sizeAttenuation").onChange(guiConfig.reDraw);
datGui.addColor(guiConfig, "color").onChange(guiConfig.reDraw);
datGui.add(guiConfig, "rotateSystem").onChange(guiConfig.reDraw);

guiConfig.reDraw();
};

const createRaining = (
size: number,
transparent: boolean,
opacity: number,
vertexColors: boolean,
sizeAttenuation: boolean,
color: number,
): void => {
let geometry: THREE.BufferGeometry = new THREE.BufferGeometry();

let positions: Float32Array = new Float32Array(pointCount * 3);
for (let i = 0; i < pointCount * 3; i++) {
positions[i] = (Math.random() - 0.5) * 100;
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));

let textureLoader: THREE.TextureLoader = new THREE.TextureLoader();
let rainTexture: THREE.Texture = textureLoader.load(rainingPng);
let material: THREE.PointsMaterial = new THREE.PointsMaterial({
size: size,
transparent: transparent,
opacity: opacity,
vertexColors: vertexColors,
sizeAttenuation: sizeAttenuation,
color: color,
depthTest: true,
depthWrite: false,
map: rainTexture,
alphaMap: rainTexture,
blending: THREE.AdditiveBlending,
});

point = new THREE.Points(geometry, material);
scene.add(point);
};

// 监听画面变化更新渲染画面
window.addEventListener("resize", () => {
// 更新摄像机
camera.aspect = canvas.value.clientWidth / canvas.value.clientHeight;
// 更新摄像机投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(canvas.value.clientWidth, canvas.value.clientHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
</script>