最终的效果

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

这个蜂窝的纹理会不断扩散, 直到最后消失。
灵感来源
其实做这个特效,参考了很多网站,例如 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;
}
}