参考课程是这位老哥的 three.js 教程 非常厉害 感兴趣的可以买来看看
如果本身对 shader 还不够了解的话, the book of shaders 是非常好的入门读物
说到做“萤火虫”,肯定会想到用“粒子”(particles)。
用 buffer geometry 创建粒子
首先定义一个 buffer geometry, 然后创建一个 BufferAttribute 用于存储每一个萤火虫的位置信息;
const firefliesGeometry = new THREE.BufferGeometry()
const firefliesCount = 30 //创建萤火虫的个数
const positionArray = new Float32Array(firefliesCount * 3)
for(let i = 0; i < firefliesCount; i++)
{
positionArray[i * 3 + 0] = Math.random() * 4
positionArray[i * 3 + 1] = Math.random() * 4
positionArray[i * 3 + 2] = Math.random() * 4
}
firefliesGeometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3))
我们先给它一个 pointsMaterial 看看这些点都在哪里;
const firefliesMaterial = new THREE.PointsMaterial({ size: 0.1, sizeAttenuation: true })
const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial)
scene.add(fireflies)
看到了空中的小点点,发现完全的随机位置有点奇怪,所以改改生成位置的代码:
for(let i = 0; i < firefliesCount; i++)
{
positionArray[i * 3 + 0] = (Math.random() - 0.5) * 4
positionArray[i * 3 + 1] = Math.random() * 1.5
positionArray[i * 3 + 2] = (Math.random() - 0.5) * 4
}
基础的小点点有了,接下来就是 shader 的魔法时刻。
自定义 shader material: vertex shader
首先是 vertex shader,用于决定物体的节点的位置属性。
The vertex shader's purpose is to position the vertices of the geometry. The idea is to send the vertices positions, the mesh transformations (like its position, rotation, and scale), the camera information (like its position, rotation, and field of view). Then, the GPU will follow the instructions in the vertex shader to process all of this information in order to project the vertices on a 2D space that will become our render —in other words, our canvas.
这段代码几乎没有什么含义,重头戏在 fragment shader 上。
但是有一个地方需要注意一下,即 gl_PointSize
需要根据屏幕分辨率而变化,所以需要从 js 文件中传进去一个 uniform: window.devicePixelRatio
。
const firefliesMaterial = new THREE.ShaderMaterial({
uniforms:
{
uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) },
uSize: { value: 100 },
uTime: { value: 0 },
},
blending: THREE.AdditiveBlending, //叠加模式也很重要
vertexShader: firefliesVertexShader,
fragmentShader: firefliesFragmentShader
})
同时,要监听 resize 事件,实时更新分辨率:
window.addEventListener('resize', () =>
{
// ...
// Update fireflies
firefliesMaterial.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2)
})
vertex.glsl
uniform float uPixelRatio;
uniform float uSize; // 粒子大小
uniform float uTime;
void main()
{
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectionPosition = projectionMatrix * viewPosition;
gl_Position = projectionPosition;
gl_PointSize = uSize * uPixelRatio; //每一个点的渲染尺寸
gl_PointSize *= (1.0 / - viewPosition.z); //近大远小
// 萤火虫的浮动
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.y += sin(uTime);
}
想让萤火虫浮动起来,还需要在外面更新 uTime
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update materials
firefliesMaterial.uniforms.uTime.value = elapsedTime
// ...
}
自定义 shader material: fragment shader
我们的目标是创造一个中心渐变的图案,听起来很简单(确实
void main()
{
float distanceToCenter = distance(gl_PointCoord, vec2(0.5)); // 计算每一个像素点到中心的距离
float strength = 0.05 / distanceToCenter - 0.1; // 中心大,周围小
gl_FragColor = vec4(1.0, 1.0, 1.0, strength); // 将 strength 作为 alpha
}
voila.