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;
  }
}
相关推荐
木子雨廷2 分钟前
Flutter 使用 flutter_flavorizr 多渠道打包
前端·flutter
环境工程笔记4 分钟前
浏览器自动化跑成功了,为什么结果还是不对?
前端
东风破_6 分钟前
一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?
前端·javascript
问心无愧05139 分钟前
ctf show web入门261
android·前端·笔记
触底反弹11 分钟前
你真的理解 JavaScript 变量提升(Hoisting)吗?从 V8 引擎编译原理深入剖析
前端·面试
蜡台23 分钟前
Vue2 使用 typescript 教程
前端·vue.js·typescript
光影少年36 分钟前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
云水一下43 分钟前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript
ZC跨境爬虫1 小时前
跟着 MDN 学CSS day_32:(Web字体深度解析与实践指南)
前端·javascript·css·ui·html