webGPU渲染闪电带你渡劫飞升
这次我们介绍如何在three.js中用webGPU渲染这种闪电渡劫飞升效果,实现这种效果的方法也有很多,这里只是抛砖引玉介绍行歌这两天实践出的一种方法,如果你有更好的实现方式,欢迎在留言区分享。
实现闪电效果可以分为以下几步:创建闪电几何体,创建闪电发光效果,闪电动画。
创建闪电几何体
首先我们沿Y轴方向创建一些列的点来构成闪电的基本形状,我们后面可以在TSL中将这些点转换成面。
const points = [];
const pointsCount = 15;
const height = 15;
const interY = height / (pointsCount - 1);
for (let i = 0; i < pointsCount; i++) {
const tx = (Math.random() - 0.5) * 1;
const ty = i * interY;
const tz = (Math.random() - 0.5) * 1;
const point = new Vector3(tx, ty, tz);
points.push(point);
}
const geometry = new LineGeometry(points);
这里使用了自定义的LineGeometry类,方便我们后面通过点来实现面。
export default class LineGeometry extends BufferGeometry {
constructor(points: Vector3[]) {
super();
const count = points.length;
const positions = new Float32Array(count * 3 * 2);
const indices = new Uint16Array((count - 1) * 2 * 3);
const ratios = new Float32Array(count);
for (let i = 0; i < count; i++) {
const i1 = i * 1;
const i2 = i * 2;
const i6 = i * 6;
const point = points[i];
//position
positions[i6 + 0] = point.x;
positions[i6 + 1] = point.y;
positions[i6 + 2] = point.z;
positions[i6 + 3] = point.x;
positions[i6 + 4] = point.y;
positions[i6 + 5] = point.z;
indices[i6 + 0] = i2 + 2;
indices[i6 + 1] = i2;
indices[i6 + 2] = i2 + 1;
indices[i6 + 3] = i2 + 1;
indices[i6 + 4] = i2 + 3;
indices[i6 + 5] = i2 + 2;
ratios[i1] = i / count;
}
this.setAttribute("position", new Float32BufferAttribute(positions, 3));
this.setAttribute("ratio", new Float32BufferAttribute(ratios, 1));
this.setIndex(new BufferAttribute(indices, 1));
}
}
借助一些TSL技巧我们用一系列的分段平面几何体来代表闪电,借助一些shader技巧我们可以让这些平面几何体始终朝向摄像机的方向。
material.vertexNode = Fn(() => {
const worldPosition = modelWorldMatrix.mul(vec4(positionGeometry, 1));
const toCamera = worldPosition.xyz.sub(cameraPosition).normalize();
const nextPosition = positionGeometry.add(vec3(0, 1, 0));
const nextWorldPosition = modelWorldMatrix.mul(vec4(nextPosition, 1);
const nextDelta = nextWorldPosition.xyz.sub(worldPosition.xyz).normalize();
const tagent = cross(nextDelta, toCamera).normalize();
const sideStep = floor(float(vertexIndex).mul(3).sub(2).div(3).mod(2)).sub(
0.5
);
const sideOffset = tagent.mul(sideStep.mul(0.3));
worldPosition.addAssign(vec4(sideOffset, 0));
const viewPosition = cameraViewMatrix.mul(worldPosition);
return cameraProjectionMatrix.mul(viewPosition);
})();
借助我们的老朋友vertexIndex我们可以在TSL中让我们的几何体有粗细的变化,看起来更像闪电了。

const ratio = attribute("ratio");
const baseThickness = ratio
.sub(0.5)
.abs()
.mul(2)
.oneMinus()
.smoothstep(0, 1);
const remapProgress = progress.mul(3).sub(1);
const progressThickness = ratio.sub(
remapProgress.abs().oneMinus().smoothstep(0, 1)
);
const finalThickness = mul(thickness, baseThickness, progressThickness);
const sideStep = floor(float(vertexIndex).mul(3).sub(2).div(3).mod(2)).sub(
0.5
);
const sideOffset = tagent.mul(sideStep.mul(finalThickness));
闪电发光效果
我们给渲染加入后期bloom发光效果,在three.js中实现这样的效果很简单,只需要引入postprocessing库。

const postProcessing = new PostProcessing(renderer);
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode("output");
bloomPass = bloom(scenePassColor);
bloomPass.threshold.value = 0;
bloomPass.strength.value = 2;
bloomPass.radius.value = 0.7;
postProcessing.outputNode = scenePassColor.add(bloomPass);
闪电动画
闪电动画我是使用两组顶点之间做过渡来实现,第一组顶点构成了闪电开始时的形状,第二组顶点构成结束时的形状,让闪电在两种形状间做过渡就形成了闪电出现时的效果。
_update() {
const position = this.geometry.getAttribute("position") as BufferAttribute;
for (let i = 0; i < this.pointsCount; i++) {
const point = this.startPoints[i].lerp(
this.endPoints[i],
this.shapeProgress
);
position.setXYZ(i * 2, point.x, point.y, point.z);
position.setXYZ(i * 2 + 1, point.x, point.y, point.z);
}
position.needsUpdate = true;
}

用gsap库很容易就实现了我们的动画效果。
gsap.to(this, {
shapeProgress: 1,
duration: 0.3,
ease: "power2.Out",
delay: 0.1,
onUpdate: () => {
this._update();
},
});
我们可以用gsap改变闪电的thickness参数来实现闪电的消失动画。
gsap.to(this.thickness, {
value: 0,
duration: 0.5,
ease: "power2.inOut",
delay: 0.5,
onComplete: () => {
this.destroy();
},
});