这次我们介绍如何在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();
  },
});