粒子可以被用来创建星星、烟雾、雨滴、灰尘、火焰等等。我们可以使用合理的帧速率来创建数千个粒子。每个粒子都是由始终面向摄影机的平面(俩个三角形)组成的。
创建粒子
创建粒子和创建网格很像,不同的是粒子使用的材质是点材质PointsMaterial和点实例Points。
下面代码为例,当实例化球缓冲几何体时,该几何体的每个顶点都将成为粒子。
/**
* 粒子
*/
const particlesGeometry = new THREE.SphereBufferGeometry(1,32,32)
const particlesMaterial = new THREE.PointsMaterial({
//点的大小
size:0.02,
//开启尺寸衰减,当相机靠近时粒子变大,当相机远离时粒子变小
sizeAttenuation:true})
const particles = new THREE.Points(particlesGeometry,particlesMaterial)
scene.add(particles)
创建500个粒子
需要用到自定义几何体
const particlesGeometry = new THREE.BufferGeometry()
const count = 500
const positions = new Float32Array(count*3)
for(let i = 0;i<count*3;i++){
positions[i] = (Math.random()-0.5)*10
}
particlesGeometry.setAttribute('position',new THREE.BufferAttribute(positions,3))
可以得到类似星空的效果
颜色
可以通过点材质PointsMaterial
的color
属性改变粒子颜色
particles.material.color = new THREE.Color('#bbffaa')
材质
想要粒子纹理贴图可以去这个网站,当然也可以试着自己做。
const textureLoader = new THREE.TextureLoader()
const particleTexture = textureLoader.load('/textures/particles/2.png')
particlesMaterial.map = particleTexture
观察上图可以发现粒子贴图挡住了后面的粒子,因此我们可以设置为将map
属性改为alphaMap
属性,记得开启transparent
particlesMaterial.transparent = true
particlesMaterial.alphaMap = particleTexture
观察上图可以发现设置为alphaMap后,粒子的边缘还是存在遮挡,这是因为粒子的绘制顺序与它们的创建顺序相同,而WebGL并不知道哪一个在另一个之前。当然有好几种方法可以修复该现象。
1.设置alphaTest
alphaTest
是一个介于0和1之间的值,它使WebGL能够知道根据像素的透明度什么时候不去渲染该像素。默认情况下,该值为0表示无论如何都会渲染该像素。
// 将alphaTest设为0.001
particlesMaterial.alphaTest = 0.001
2.设置depthTest
当WebGL绘制粒子时,WebGL会测试正在绘制的粒子哪个更靠前,在其后面的粒子不会被绘制,在其前面的粒子会被绘制,这就造成了混乱。这被称为深度测试,可以通过alphaTest
停用depth testing
// particlesMaterial.alphaTest = 0.001
particlesMaterial.depthTest = false
关闭了深度测试后,GPU就不用再去猜哪一个在前面哪一个在后面,只管无脑绘制就对了,观察下图看出效果很不错。
但是,如果场景中有其他对象或具有不同颜色的粒子,禁用深度测试可能会产生错误,我们可以往场景添加一个简单的立方体来测试。
观察上图可以发现,原本在立方体后面的粒子我们不应该看到的,但却被一同渲染绘制出来能被观察到。所以除非粒子和物体颜色相同,并且场景只有粒子,否则不推荐轻易停用深度测试。
3.设置depthWrite
正在绘制的深度被存储在一个称为深度缓冲区的位置,作为不测试粒子是否比深度缓冲区中的粒子更近的替代方案,我们可以告诉WebGL不要使用深度测试depthTest去往深度缓冲区中写入粒子。
// particlesMaterial.alphaTest = 0.001
// particlesMaterial.depthTest = false
particlesMaterial.depthWrite = false
观察下图可以发现得到满意的效果
4.blending属性
WebGL当前是在一个像素顶部绘制另一个像素,然后我们通过设置blending
混合属性为THREE.AdditiveBlending
,可以告诉WebGL将像素的颜色添加到已经绘制完的像素的颜色上去,这样不同像素的颜色将会混合到一块而不是说处于前面的像素颜色会覆盖掉后面的像素颜色。
// particlesMaterial.alphaTest = 0.001
// particlesMaterial.depthTest = false
particlesMaterial.depthWrite = false
particlesMaterial.blending = THREE.AdditiveBlending
可以看到圆环相交处非常高亮,这是多个像素颜色混合的结果,或者对比下图立方体与上图立方体区域的区别,下图位于立方体前面的圆环因为颜色混合以至于我们看到的是整块白色区域,而上图由于我们没有设置blending
,所以我们可以看清楚看到立方体前面的圆环。
注意,此设置会影响运行性能
为每个粒子设置不同颜色
与设置随机位置一样道理,不够这次数组里放的不是位置xyz的值,而是颜色rgb的值,然后几何体通过setAttribute
方法设置color
属性
const colors = new Float32Array(count*3)
for(let i = 0;i<count*3;i++){
......
colors[i]=Math.random()
}
particlesGeometry.setAttribute('color',new THREE.BufferAttribute(colors,3))
之后设置材质的vertexColors
属性为true开启顶点着色
particlesMaterial.vertexColors = true
可以观察到还是有个主色调,因为我们前面设置了圆环的颜色,导致颜色混合了,将圆环颜色去掉后再观察便正常了
// particlesMaterial.color = new THREE.Color('#ff88cc')
动画
Points
类继承于Object3D
类,因此我们可以把这些点当作一个整体去移动、旋转或缩放
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update controls
controls.update()
// 更新粒子
particles.rotation.y = elapsedTime * 0.2
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
观察到这些点全都缓慢绕着y轴旋转
但是我们想要的是其中每一个粒子做动画而不是把它们当作一个整体去做动画。
因此,我们可以在particlesGeometry.attributes.position.array
中分别更新每个顶点,因为这个数组包含了所有粒子的位置信息。
// 更新粒子
// particles.rotation.y = elapsedTime * 0.2
for (let i = 0; i < count; i++) {
// 顶点索引
const i3 = i * 3
//i3+0便是每个顶点的x值,i3+1便是每个顶点的y值,i3+2便是每个顶点的z值
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime)
}
会发现所有粒子平面扁平化了一动不动,没有任何移动现象,这是因为当几何体的属性有所变化时,我们需要告诉Three.js几何体属性更新了。
因此我们需要将几何体position
属性的needsUpdate
设为true。
//在循环体外面
particlesGeometry.attributes.position.needsUpdate = true
然后我们会看到整个粒子构成的平面整体在上下移动,然而我们需要的是波浪效果。
因此可以对sin设置偏移量,这样便可以得到波形,这里我们使用x坐标作为其偏移量
for (let i = 0; i < count; i++) {
const i3 = i * 3
const x = particlesGeometry.attributes.position.array[i3 + 0]
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime + x)
}
总结
然而我们应该避免这种做法,因为在每个帧上去更新整个属性对于性能损耗非常巨大。设置粒子动画的最佳方法是创建我们自己的自定义着色器custom shader
,我们将在后面的课程中这样做。