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;
  }
}
相关推荐
鱼樱前端5 分钟前
重度Cursor用户 最强 Cursor Rules 和 Cursor 配置 mcp 以及最佳实践配置方式
前端
曼陀罗6 分钟前
Path<T> 、 keyof T 什么情况下用合适
前端
锈儿海老师12 分钟前
AST 工具大PK!Biome 的 GritQL 插件 vs. ast-grep,谁是你的菜?
前端·javascript·eslint
飞龙AI14 分钟前
鸿蒙Next实现瀑布流布局
前端
快起来别睡了15 分钟前
代理模式:送花风波
前端·javascript·架构
海底火旺17 分钟前
电影应用开发:从代码细节到用户体验优化
前端·css·html
陈随易26 分钟前
Gitea v1.24.0发布,自建github神器
前端·后端·程序员
前端付豪29 分钟前
汇丰银行技术架构揭秘:全球交易稳定背后的“微服务+容灾+零信任安全体系”
前端·后端·架构
邹荣乐31 分钟前
uni-app开发微信小程序的报错[渲染层错误]排查及解决
前端·微信小程序·uni-app
今天出摊吗32 分钟前
表单元素的默认提交行为
前端