Shader 蜂窝网格扩散动画

最终的效果

将以上的蜂窝网格作为大屏背景,效果如下:

这个蜂窝的纹理会不断扩散, 直到最后消失。

灵感来源

其实做这个特效,参考了很多网站,例如 DataV、 EasyV , 这些专门做大屏的公司,给出了很多大屏模板, 不少模板的背景,就是蜂窝网格,如下面2张图。

所需贴图

three.js 中, 用着色器编程, 需要的贴图如下。

  • 首先构造一个平面, 将贴图水平方向复制 N 份, 垂直方向也复制 N 份,形成了一个蜂窝网格平面
  • 其次,平面的材质,使用 ShaderMaterial 创建着色器材质, 通过传入的半径参数, 给平面着色
  • 最后,通过关键帧动画,不断地调整扩散半径, 渐变透明度,周而复始

着色原理

如下图所示, 蓝色圆环表示扩散圆环, 其中 OA 表示扩散半径, O为圆心, A为环形内部的中间位置, 假设扩散半径 OA 的长度为 r, 圆环的染色部位是 CD, CD 的长度是 2 * halfR, 而且 CD 的渐变是 0 ------> 1 ------> 0 那么,我们只需要计算平面上的点是否落在圆环区域: 是,则需要考虑染色; 不是,即返回透明色。

如果平面上的点, 不在圆环区域内,那么直接返回 gl_FragColor = vec4(0,0,0,0) 表示透明。

如果平面的点, 在圆环区域内, 则需要考虑, 是否在蜂窝贴图的白色线上面: 是,则需要染色; 不是,则直接用透明色。

例如,上图的 A 点,需要染色, B 点则不需要要染色。

以A点为例, 通过读取贴图的纹理,得到颜色

vec4 colors = texture2D(gridTexture, vUv * repeat);

这时候, colors 结构是 (r, g, b, a), 其中, r, g, b, a 均是 0 至 1 的数字,

由于贴图是白色透明的, A点在白色线上, 所以 r、g、b 这三个数,基本上都是接近或等于 1,

而 a 这个分量,则可能不是 1, 因为白色线可以是渐变的, 白色线边缘越接近透明, a 越接近 0。

如果这时,需要将 A点 染成某个颜色, 则需要下面这句:

gl_FragColor = vec4(diffuseColor, colos.a);

这样, diffuseColor 这个颜色,就取代了白色。

但是,这样一来, 整个扩散的圆环部位,基本上都是 diffuseColor 这个颜色, 渐变效果不明显。

这时, 可以计算点在圆环中,靠近中心的程度:

1、越靠近中心, 不透明度越接近 1

2、越靠近圆环边沿, 不透明度越接近0

这样就计算出新的不透明度 a2;

为了让圆环呈现出渐变效果, 将 colos.a 乘以 a2 并作为新的不透明度,这样渐变效果开始明显。

也就是 gl_FragColor = vec4(diffuseColor, colos.a * a2);

复杂化

以上分析,已经将基本形态做好了,但是如果希望半径越大, 扩散圆环的颜色越淡,

则可以再传入一个不透明度的系数 opacity, 也就是最终的着色计算是:

gl_FragColor = vec4(diffuseColor, colos.a * a2 * opacity);

同时, 扩散半径越大, 圆环越细小, 都可以通过参数传入,来改变形态, 整体代码如下。

完整代码

js 复制代码
import {
  AdditiveBlending,
  Color,
  Group,
  Mesh,
  MeshBasicMaterial,
  PlaneGeometry,
  RepeatWrapping,
  ShaderMaterial,
  Texture,
} from 'three';

type floorParams = {
  group: Group;
  grid: Texture;
  gridBlack: Texture;
};

type floorUniform = {
  diffuseColor: { value: Color };
  r: { value: number };
  halfR: { value: number };
  gridTexture: { value: Texture };
  repeat: { value: number };
  opacity: { value: number };
};

export class FloorBg {
  private config: floorParams;
  private uniforms: floorUniform;
  private needToAnimateFloor = false;
  private readonly planeSize = 350;
  private readonly repeatNum: number = 40;
  private readonly translateZ: number = -5;
  private readonly firstDefaultR: number = -50;
  private nextDefaultR: number = -150;
  private readonly maxR: number = 200;
  private readonly halfRBase: number = 5;
  private readonly reduceR: number = 100;

  constructor(params: floorParams) {
    this.config = params;
    this.uniforms = {
      diffuseColor: { value: new Color(0x30dcff) },
      r: { value: this.firstDefaultR },
      halfR: { value: this.halfRBase },
      gridTexture: { value: this.config.grid },
      repeat: { value: this.repeatNum },
      opacity: { value: 1.0 },
    };
  }

  public tick(quickly: boolean) {
    if (this.uniforms && this.needToAnimateFloor) {
      let newR = this.uniforms.r.value;
      let newHalfR = this.halfRBase;
      let opacity = 1.0;

      const step = quickly ? 0.3 : 0.5;
      this.nextDefaultR = quickly ? -200 : -150;

      if (newR >= this.maxR) {
        newR = this.nextDefaultR;
        newHalfR = this.halfRBase;
        opacity = 1.0;
      } else {
        newR += step;
        if (newR <= this.reduceR) {
          newHalfR = this.halfRBase + (10 / this.reduceR) * newR;
          opacity = 1.0;
        } else {
          newHalfR =
            this.halfRBase +
            10 -
            ((this.halfRBase + 10) / (this.maxR - this.reduceR)) * (newR - this.reduceR);
          opacity = 1.0 - (newR - this.reduceR) / (this.maxR - this.reduceR);
        }
      }

      this.uniforms.r.value = newR;
      this.uniforms.halfR.value = newHalfR;
      this.uniforms.opacity.value = opacity;
    }
  }

  public create() {
    const texture = this.config.grid;
    const alphaMap = this.config.gridBlack;
    texture.wrapS = texture.wrapT = alphaMap.wrapS = alphaMap.wrapT = RepeatWrapping;
    texture.repeat.set(this.repeatNum, this.repeatNum);
    alphaMap.repeat.set(this.repeatNum, this.repeatNum);

    const planeBg = new Mesh(
      new PlaneGeometry(this.planeSize, this.planeSize),
      new MeshBasicMaterial({
        map: texture,
        color: 0x00ffff,
        transparent: true,
        opacity: 0.05,
        alphaMap: alphaMap,
        blending: AdditiveBlending,
      }),
    );
    planeBg.translateZ(this.translateZ);
    this.config.group.add(planeBg);

    const shaderMaterial = new ShaderMaterial({
      uniforms: this.uniforms,
      vertexShader: `
        //纹理坐标uv
        varying vec2 vUv;
        //顶点坐标
        varying vec3 vPosition;
        void main(){
          vPosition = position;
          vUv = uv;
          gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
      `,
      fragmentShader: `
        //纹理坐标uv
        varying vec2 vUv;
        //顶点坐标
        varying vec3 vPosition;
        //扩散光圈的颜色
        uniform vec3 diffuseColor;
        //扩散半径
        uniform float r;
        //扩散半径加减一定的范围用来染色
        uniform float halfR;
        //网格的纹理贴图
        uniform sampler2D gridTexture;
        //纹理重复次数
        uniform float repeat;
        //整体的衰减透明度
        uniform float opacity;
        void main(){
          if(r < 0.0){
            //扩散半径太小,一律变成透明
            gl_FragColor = vec4(0,0,0,0);
          }else{
            //圆心原点
            vec2 center = vec2(0.0, 0.0); 
            //距离圆心的距离
            float rDistance = distance(vPosition.xy, center);
            if(rDistance < r - halfR || rDistance > r + halfR){
              //不在光圈范围内,一律变成透明
              gl_FragColor = vec4(0,0,0,0);
            }else{
              float a;
              if(rDistance < r){
                a = (rDistance - (r - halfR)) / halfR;
              }else{
                a = 1.0 - ((rDistance - r) / halfR);
              }
              //因为水平方向、垂直方向都重复了N次,所以乘以N
              vec4 colors = texture2D(gridTexture, vUv * repeat);
              a = a * colors.a * opacity;
              gl_FragColor = vec4(diffuseColor, a);
            }
          }
        }
      `,
      transparent: true,
      depthTest: false,
    });
    const animatedPlaneBg = new Mesh(
      new PlaneGeometry(this.planeSize, this.planeSize),
      shaderMaterial,
    );
    animatedPlaneBg.translateZ(this.translateZ);
    this.config.group.add(animatedPlaneBg);

    this.needToAnimateFloor = true;
  }
}
相关推荐
Momo__41 分钟前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富1 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇1 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇1 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端
weedsfly1 小时前
语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌
前端·javascript
JustHappy1 小时前
「软件设计思想杂谈🤔」“切图仔”也能懂编译原理?框架源码也许没那么难。聊聊 Vue 的编译(上)
前端·javascript·vue.js