前言
今天带大家做一个烟花效果,这个效果是基于之前的孔明灯,水波纹学习制作的,如果你还没有看过,可以先看一下,这里就不再赘述了。
1.复制之前孔明灯的案例代码
main.js
1 | import * as THREE from "three"; |
顶点着色器
1 | precision lowp float; |
片元着色器
1 | precision lowp float; |
ok,接下来我们要考虑下,怎么实现这样的一个烟花效果,首先发射出去是一个点,之后到一定距离就炸开,然后消失。
那么我们就可以简单理解为这是两个步骤,第一步是创建一个点,直行上天,第二步是炸开,然后消失。
随机创建一个点
新建文件夹,放置shader,切记,记得能拆出去的代码就不要在一个文件写
在main文件夹下新建firework.js
暴露出一个烟火的类,需要创建的时候去调用这个类,产生烟花效果(color是颜色,to是目标点,from是起始点)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
51import * as Three from "three";
import startPointFragment from "../shaders/startpoint/fragment.glsl";
import startPointVertex from "../shaders/startpoint/vertex.glsl";
export default class Fireworks {
constructor(color, to, from = { x: 0, y: 0, z: 0 }) {
this.color = new Three.Color(color);
// 创建烟花发射的球点
this.startGeometry = new Three.BufferGeometry();
const startPositionArray = new Float32Array(3);
startPositionArray[0] = from.x;
startPositionArray[1] = from.y;
startPositionArray[2] = from.z;
this.startGeometry.setAttribute(
"position",
new Three.BufferAttribute(startPositionArray, 3)
);
// 得到向量,用来的到轨迹
const astepArray = new Float32Array(3);
astepArray[0] = to.x - from.x;
astepArray[1] = to.y - from.y;
astepArray[2] = to.z - from.x;
this.startGeometry.setAttribute(
"aStep",
new Three.BufferAttribute(astepArray, 3)
);
// 设置着色器材质
this.startMaterial = new Three.ShaderMaterial({
vertexShader: startPointVertex,
fragmentShader: startPointFragment,
transparent: true,
blending: Three.AdditiveBlending,
depthWrite: false,
uniforms: {
uSize: {
value: 20,
},
uColor: { value: this.color },
},
});
// 创建烟花点球
this.startPoint = new Three.Points(this.startGeometry, this.startMaterial);
}
// 创建一个点,添加到场景中
addScene(scene, camera) {
scene.add(this.startPoint);
this.scene = scene;
}
}
在main.js中引入这个类
1 | import Fireworks from "./firework"; |
添加shader
顶点着色器1
2
3
4
5
6
7
8
9
10attribute vec3 aStep;
uniform float uSize;
void main(){
vec4 modelPosition = modelMatrix * vec4( position, 1.0 );
modelPosition.xyz += aStep;
vec4 viewPosition = viewMatrix * modelPosition;
gl_Position = projectionMatrix * viewPosition;
// 设置顶点大小
gl_PointSize =uSize;
}
片元着色器
这里我使用了之前做过的小圆的片元着色器1
2
3
4
5
6
7
8uniform vec3 uColor;
void main(){
float distanceToCenter = distance(gl_PointCoord,vec2(0.5));
float strength = distanceToCenter*2.0;
strength = 1.0-strength;
strength = pow(strength,1.5);
gl_FragColor = vec4(uColor,strength);
}
添加起始点到目标点的动画
既然我们已经有了起始点和目标点,那么我们就可以根据起始点和目标点的向量,做一个轨迹动画
在main.js中添加一个update函数,用来每一帧调用firework的update方法,这样就可以在update中定义一个时间,传给起始点的顶点着色器进行轨迹变化
1 | let fireworks = []; |
在firework.js中添加update方法更新时间
定义变量
1
2
3
4
5
6
7
8
9uniforms: {
uTime: {
value: 0,
},
uSize: {
value: 20,
},
uColor: { value: this.color },
},开始计时
1
2// 开始计时
this.clock = new Three.Clock();更新时间
1
2
3
4
5update() {
const elapsedTime = this.clock.getElapsedTime();
this.startMaterial.uniforms.uTime.value = elapsedTime;
this.startMaterial.uniforms.uSize.value = 20;
}
在顶点着色器接受时间变化
1 | uniform float uTime; |
添加烟花爆炸效果
上面我们说过,我们需要将这一个点变成很多点,然后散开,这样就可以形成爆炸效果
一样的,先去新建一个着色器文件夹命名为fireworks
在fireworks文件夹中新建一个vertex.glsl文件
在这里我们添加一个随机方向的参数aRandom(向量),让爆炸随机产生方向,增加一个时间参数,一个scale参数来让爆炸之后的多个小球变小1
2
3
4
5
6
7
8
9
10
11
12
13attribute float aScale;
attribute vec3 aRandom;
uniform float uTime;
uniform float uSize;
void main(){
vec4 modelPosition = modelMatrix * vec4( position, 1.0 );
modelPosition.xyz+=aRandom*uTime*10.0;
vec4 viewPosition = viewMatrix * modelPosition;
gl_Position = projectionMatrix * viewPosition;
// 设置顶点大小
gl_PointSize =uSize*aScale-(uTime*20.0);
}
片元着色器fragment.glsl不用变化1
2
3
4
5
6
7
8uniform vec3 uColor;
void main(){
float distanceToCenter = distance(gl_PointCoord,vec2(0.5));
float strength = distanceToCenter*2.0;
strength = 1.0-strength;
strength = pow(strength,1.5);
gl_FragColor = vec4(uColor,strength);
}
在firework.js中添加一个新的材质
导入1
2import fireworksFragment from "../shaders/fireworks/fragment.glsl";
import fireworksVertex from "../shaders/fireworks/vertex.glsl";
编写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// 创建爆炸的烟花
this.fireworkGeometry = new Three.BufferGeometry();
this.FireworksCount = 180 + Math.floor(Math.random() * 180);
const positionFireworksArray = new Float32Array(this.FireworksCount * 3);
const scaleFireArray = new Float32Array(this.FireworksCount);
const directionArray = new Float32Array(this.FireworksCount * 3);
for (let i = 0; i < this.FireworksCount; i++) {
// 一开始烟花位置
positionFireworksArray[i * 3 + 0] = to.x;
positionFireworksArray[i * 3 + 1] = to.y;
positionFireworksArray[i * 3 + 2] = to.z;
// 设置烟花所有粒子初始化大小
scaleFireArray[i] = Math.random();
// 设置四周发射的角度
let theta = Math.random() * 2 * Math.PI;
let beta = Math.random() * 2 * Math.PI;
let r = Math.random();
// 这里运用了一定的数学知识,在一个立体里面,如何计算x,y,z轴上的投影
directionArray[i * 3 + 0] = r * Math.sin(theta) + r * Math.sin(beta);
directionArray[i * 3 + 1] = r * Math.cos(theta) + r * Math.cos(beta);
directionArray[i * 3 + 2] = r * Math.sin(theta) + r * Math.cos(beta);
}
this.fireworkGeometry.setAttribute(
"position",
new Three.BufferAttribute(positionFireworksArray, 3)
);
this.fireworkGeometry.setAttribute(
"aScale",
new Three.BufferAttribute(scaleFireArray, 1)
);
this.fireworkGeometry.setAttribute(
"aRandom",
new Three.BufferAttribute(directionArray, 3)
);
this.fireworksMaterial = new Three.ShaderMaterial({
uniforms: {
uTime: {
value: 0,
},
uSize: {
value: 0,
},
uColor: { value: this.color },
},
transparent: true,
blending: Three.AdditiveBlending,
depthWrite: false,
vertexShader: fireworksVertex,
fragmentShader: fireworksFragment,
});
this.fireworks = new Three.Points(
this.fireworkGeometry,
this.fireworksMaterial
);
添加时加上1
2
3
4
5
6// 添加到场景
addScene(scene, camera) {
scene.add(this.startPoint);
scene.add(this.fireworks);
this.scene = scene;
}
在更新时的时候设置效果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
28update() {
const elapsedTime = this.clock.getElapsedTime();
if (elapsedTime > 0.2 && elapsedTime < 1) {
this.startMaterial.uniforms.uTime.value = elapsedTime;
this.startMaterial.uniforms.uSize.value = 20;
} else if (elapsedTime > 0.2) {
const time = elapsedTime - 1;
// 让点元素消失
this.startMaterial.uniforms.uSize.value = 0;
this.startPoint.clear();
this.startGeometry.dispose();
this.startMaterial.dispose();
//设置烟花显示
this.fireworksMaterial.uniforms.uSize.value = 20;
// console.log(time);
this.fireworksMaterial.uniforms.uTime.value = time;
if (time > 5) {
this.fireworksMaterial.uniforms.uSize.value = 0;
this.fireworks.clear();
this.fireworkGeometry.dispose();
this.fireworksMaterial.dispose();
this.scene.remove(this.fireworks);
this.scene.remove(this.startPoint);
return "remove";
}
}
}
在main.js删除数组中的值,避免渲染错误
1 | let fireworks = []; |
添加xiu的发射声音和bool爆炸的音效
资源可以通过爱给网去找
在firework.js中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
171
172
173
174
175
176
177
178
179import * as Three from "three";
import startPointFragment from "../shaders/startpoint/fragment.glsl";
import startPointVertex from "../shaders/startpoint/vertex.glsl";
import fireworksFragment from "../shaders/fireworks/fragment.glsl";
import fireworksVertex from "../shaders/fireworks/vertex.glsl";
export default class Fireworks {
constructor(color, to, from = { x: 0, y: 0, z: 0 }) {
this.color = new Three.Color(color);
// 创建烟花发射的球点
this.startGeometry = new Three.BufferGeometry();
const startPositionArray = new Float32Array(3);
startPositionArray[0] = from.x;
startPositionArray[1] = from.y;
startPositionArray[2] = from.z;
this.startGeometry.setAttribute(
"position",
new Three.BufferAttribute(startPositionArray, 3)
);
const astepArray = new Float32Array(3);
astepArray[0] = to.x - from.x;
astepArray[1] = to.y - from.y;
astepArray[2] = to.z - from.x;
this.startGeometry.setAttribute(
"aStep",
new Three.BufferAttribute(astepArray, 3)
);
// 设置着色器材质
this.startMaterial = new Three.ShaderMaterial({
vertexShader: startPointVertex,
fragmentShader: startPointFragment,
transparent: true,
blending: Three.AdditiveBlending,
depthWrite: false,
uniforms: {
uTime: {
value: 0,
},
uSize: {
value: 20,
},
uColor: { value: this.color },
},
});
// 创建烟花点球
this.startPoint = new Three.Points(this.startGeometry, this.startMaterial);
// 开始计时
this.clock = new Three.Clock();
// 创建爆炸的烟花
this.fireworkGeometry = new Three.BufferGeometry();
this.FireworksCount = 180 + Math.floor(Math.random() * 180);
const positionFireworksArray = new Float32Array(this.FireworksCount * 3);
const scaleFireArray = new Float32Array(this.FireworksCount);
const directionArray = new Float32Array(this.FireworksCount * 3);
for (let i = 0; i < this.FireworksCount; i++) {
// 一开始烟花位置
positionFireworksArray[i * 3 + 0] = to.x;
positionFireworksArray[i * 3 + 1] = to.y;
positionFireworksArray[i * 3 + 2] = to.z;
// 设置烟花所有粒子初始化大小
scaleFireArray[i] = Math.random();
// 设置四周发射的角度
let theta = Math.random() * 2 * Math.PI;
let beta = Math.random() * 2 * Math.PI;
let r = Math.random();
directionArray[i * 3 + 0] = r * Math.sin(theta) + r * Math.sin(beta);
directionArray[i * 3 + 1] = r * Math.cos(theta) + r * Math.cos(beta);
directionArray[i * 3 + 2] = r * Math.sin(theta) + r * Math.cos(beta);
}
this.fireworkGeometry.setAttribute(
"position",
new Three.BufferAttribute(positionFireworksArray, 3)
);
this.fireworkGeometry.setAttribute(
"aScale",
new Three.BufferAttribute(scaleFireArray, 1)
);
this.fireworkGeometry.setAttribute(
"aRandom",
new Three.BufferAttribute(directionArray, 3)
);
this.fireworksMaterial = new Three.ShaderMaterial({
uniforms: {
uTime: {
value: 0,
},
uSize: {
value: 0,
},
uColor: { value: this.color },
},
transparent: true,
blending: Three.AdditiveBlending,
depthWrite: false,
vertexShader: fireworksVertex,
fragmentShader: fireworksFragment,
});
this.fireworks = new Three.Points(
this.fireworkGeometry,
this.fireworksMaterial
);
// 创建音频
this.linstener = new Three.AudioListener();
this.linstener1 = new Three.AudioListener();
this.sound = new Three.Audio(this.linstener);
this.sendSound = new Three.Audio(this.linstener1);
// 创建音频加载器
const audioLoader = new Three.AudioLoader();
audioLoader.load(
`./assets/audio/pow${Math.floor(Math.random() * 4) + 1}.ogg`,
(buffer) => {
this.sound.setBuffer(buffer);
this.sound.setLoop(false);
this.sound.setVolume(1);
}
);
audioLoader.load(`./assets/audio/send.mp3`, (buffer) => {
this.sendSound.setBuffer(buffer);
this.sendSound.setLoop(false);
this.sendSound.setVolume(1);
});
}
// 添加到场景
addScene(scene, camera) {
scene.add(this.startPoint);
scene.add(this.fireworks);
this.scene = scene;
}
// update变量
update() {
const elapsedTime = this.clock.getElapsedTime();
if (elapsedTime > 0.2 && elapsedTime < 1) {
if (!this.sendSound.isPlaying && !this.sendSoundplay) {
this.sendSound.play();
this.sendSoundplay = true;
}
this.startMaterial.uniforms.uTime.value = elapsedTime;
this.startMaterial.uniforms.uSize.value = 20;
} else if (elapsedTime > 0.2) {
const time = elapsedTime - 1;
// 让点元素消失
this.startMaterial.uniforms.uSize.value = 0;
this.startPoint.clear();
this.startGeometry.dispose();
this.startMaterial.dispose();
if (!this.sound.isPlaying && !this.play) {
this.sound.play();
this.play = true;
}
//设置烟花显示
this.fireworksMaterial.uniforms.uSize.value = 20;
this.fireworksMaterial.uniforms.uTime.value = time;
if (time > 5) {
this.fireworksMaterial.uniforms.uSize.value = 0;
this.fireworks.clear();
this.fireworkGeometry.dispose();
this.fireworksMaterial.dispose();
this.scene.remove(this.fireworks);
this.scene.remove(this.startPoint);
return "remove";
}
}
}
}
导入模型
在main.js中导入,模型我通过溜溜网找到的,这里为了给水面加上波纹效果,使用了three自带的水波纹,代码可以参考之前写过的文章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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import gsap from "gsap";
import * as dat from "dat.gui";
import vertexShader from "../shaders/flylight/vertex.glsl";
import fragmentShader from "../shaders/flylight/fragment.glsl";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import Fireworks from "./firework";
// 导入水模块
import { Water } from "three/examples/jsm/objects/Water2";
// 目标:认识shader
//创建gui对象
const gui = new dat.GUI();
// console.log(THREE);
// 初始化场景
const scene = new THREE.Scene();
// 创建透视相机
const camera = new THREE.PerspectiveCamera(
90,
window.innerHeight / window.innerHeight,
0.1,
1000
);
// 设置相机位置
// object3d具有position,属性是1个3维的向量
camera.position.set(0, 0, 30);
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
scene.add(camera);
// 加入辅助轴,帮助我们查看3维坐标轴
// const axesHelper = new THREE.AxesHelper(5);
// scene.add(axesHelper);
// 加载纹理
// 创建纹理加载器对象
const rgbeLoader = new RGBELoader();
rgbeLoader.loadAsync("./assets/2k.hdr").then((texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
scene.environment = texture;
});
// 创建着色器材质;
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {},
side: THREE.DoubleSide,
// transparent: true,
});
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({ alpha: true });
// renderer.shadowMap.enabled = true;
// renderer.shadowMap.type = THREE.BasicShadowMap;
// renderer.shadowMap.type = THREE.VSMShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// renderer.toneMapping = THREE.LinearToneMapping;
// renderer.toneMapping = THREE.ReinhardToneMapping;
// renderer.toneMapping = THREE.CineonToneMapping;
renderer.toneMappingExposure = 0.1;
const gltfLoader = new GLTFLoader();
let LightBox = null;
gltfLoader.load("./assets/model/newyears_min.glb", (gltf) => {
scene.add(gltf.scene);
// 创建水面
const waterGeometry = new THREE.PlaneBufferGeometry(100, 100);
let water = new Water(waterGeometry, {
scale: 4,
textureHeight: 1024,
textureWidth: 1024,
});
water.position.y = 1;
water.rotation.x = -Math.PI / 2;
scene.add(water);
});
gltfLoader.load("./assets/model/flyLight.glb", (gltf) => {
console.log(gltf);
LightBox = gltf.scene.children[0];
LightBox.material = shaderMaterial;
for (let i = 0; i < 150; i++) {
let flyLight = gltf.scene.clone(true);
let x = (Math.random() - 0.5) * 300;
let z = (Math.random() - 0.5) * 300;
let y = Math.random() * 60 + 5;
flyLight.position.set(x, y, z);
gsap.to(flyLight.rotation, {
y: 2 * Math.PI,
duration: 10 + Math.random() * 30,
repeat: -1,
});
gsap.to(flyLight.position, {
x: "+=" + Math.random() * 5,
y: "+=" + Math.random() * 20,
yoyo: true,
duration: 5 + Math.random() * 10,
repeat: -1,
});
scene.add(flyLight);
}
});
// 设置渲染尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
// console.log("resize");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});
// 将渲染器添加到body
document.body.appendChild(renderer.domElement);
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼
controls.enableDamping = true;
// 设置自动旋转
controls.autoRotate = true;
controls.autoRotateSpeed = 0.1;
// controls.maxPolarAngle = (Math.PI / 3) * 2;
// controls.minPolarAngle = (Math.PI / 3) * 2;
const clock = new THREE.Clock();
// 管理烟花
let fireworks = [];
function animate(t) {
controls.update();
const elapsedTime = clock.getElapsedTime();
// console.log(fireworks);
fireworks.forEach((item, i) => {
const type = item.update();
if (type == "remove") {
fireworks.splice(i, 1);
}
});
requestAnimationFrame(animate);
// 使用渲染器渲染相机看这个场景的内容渲染出来
renderer.render(scene, camera);
}
animate();
// 设置创建烟花函数
let createFireworks = () => {
let color = `hsl(${Math.floor(Math.random() * 360)},100%,80%)`;
let position = {
x: (Math.random() - 0.5) * 40,
z: -(Math.random() - 0.5) * 40,
y: 3 + Math.random() * 15,
};
// 随机生成颜色和烟花放的位置
let firework = new Fireworks(color, position);
firework.addScene(scene, camera);
fireworks.push(firework);
};
// 监听点击事件
window.addEventListener("click", createFireworks);