手把手带你使用Three.js Shader一步步实现炫酷Flower特效(下)

145 阅读8分钟

前言

你好啊,这里是西伯利亚小火龙

书接上回,我们完成了花朵特效、落叶特效、还有后处理特效。

image.png

但目前我们还缺少背景。边框还有鼠标轨迹特效,接下来就让我们一起完成它吧!

添加背景

initWrold(){
    ...
    this.foregroundLeavesCreate = new ForegroundLeavesCreate(this);
    this.bckgroundCreate = new BackgroundCreate(this);
    ...
}

BackgroundCreate 类是用于生成一个全屏背景平面网格,使用自定义 ShaderMaterial 渲染出带有纹理扰动和颜色混合效果的背景,可用于 WebGL 场景中作为视觉底图

  1. 创建几何体
const geometry = new THREE.PlaneGeometry(20, 20, 2, 2)
  1. Shader 材质定义

const material = new THREE.ShaderMaterial({
    uniformsGroups: [this.base.UBO],
    uniforms: {
        uColor1: {value: new THREE.Color("#ffec95")},
        uColor2: {value: new THREE.Color("#ecc168")},
        tNoise: {value: this.base.noiseSimplexLayeredTexture}
    },
    vertexShader: ...,
    fragmentShader: ...,
    depthTest: !1,
})
  • uColor1/uColor2: 背景颜色区间

  • tNoise: 用于扰动的噪声贴图

顶点着色器(vertexShader)

uniform Global { vec2 resolution; float time; float dtRatio; };
varying vec2 vUv;

void main() {
    vUv = uv;
    gl_Position = vec4(position, 1.0);
}

  • 传递 uv 到片元着色器

  • 顶点不做任何变形(全屏背景)

片元着色器(fragmentShader)

layout(location = 1) out highp vec4 gInfo; // 用于 G-buffer 输出,留空

uniform Global { vec2 resolution; float time; float dtRatio; };
uniform sampler2D tNoise;
uniform vec3 uColor1, uColor2;
varying vec2 vUv;

mat2 rotateAngle(float a) {
    ...
}

动画噪声背景逻辑:

vec2 screenUv = gl_FragCoord.xy / resolution.xy;
float aspect = resolution.x / resolution.y;
screenUv.x *= aspect;

// 动画时间步长
float steppedTime = floor(time * 3.0) * 3.14159 * 1.53;
screenUv = rotateAngle(steppedTime) * screenUv;

// 三重采样 + 扰动
float n1 = texture2D(tNoise, screenUv * 4.31).r;
float n2 = texture2D(tNoise, -screenUv * 1.814).r;
float n3 = texture2D(tNoise, screenUv * 5.714).r;
float noise = n1 * n2 * n3;

使用三个方向上的 noise 幅度乘积来产生变化丰富的动态扰动

抖动边缘采样 + 颜色混合:

noise = aastep(0.00015, noise);
vec3 color = mix(uColor2, uColor1, noise);
gl_FragColor = vec4(color, 0.0);
gInfo = vec4(0.0);
  • aastep() 用于抗锯齿

  • uColor1uColor2 之间做颜色插值

  • 不输出 alpha,背景是透明的

  • gInfo设置为 0,表明这个材质不影响 G-buffer 数据写入(可能配合 deferred shading 使用)

其他:

this.mesh.renderOrder = 0
this.mesh.matrixAutoUpdate = false
this.base.scene.add(this.mesh)
  • 禁用矩阵自动更新,提高性能

  • 设定 renderOrder 为 0,确保它在最底层渲染(在其他对象前)

  • 添加mesh到场景

阶段展示

添加边框

async initLoader() {
     ...
    for (let i = 0; i < geometryArray.length; i++) {
        ...
        if (geometryArray[i].key === 'border') {
            this[geometryArray[i].key].setAttribute('inset', new THREE.BufferAttribute(new Float32Array([
                0.0002442598342895508,
                0.0002442598342895508,
                0,
                0.0002442598342895508,
                0.0002442598342895508,
                0,
                0.0002442598342895508,
                0.0002442598342895508,
                0,
                0.0002442598342895508,
                0.0002442598342895508,
                0,
                -1,
                -1,
                0,
                -1,
                1,
                0,
                -1,
                1,
                0,
                1,
                1,
                0,
                1,
                -1,
                0,
                -1,
                1,
                0,
                0.0002442598342895508,
                0.0002442598342895508,
                0,
                0.0002442598342895508,
                0.0002442598342895508,
                0,
                0.0002442598342895508,
                0.0002442598342895508,
                0,
                1,
                -1,
                0,
                0.0002442598342895508,
                0.0002442598342895508,
                0
            ]), 3))
            this[geometryArray[i].key].setAttribute('notch', new THREE.BufferAttribute(new Float32Array([
                0,
                0,
                0,
                0,
                1,
                0,
                -1,
                0,
                0,
                -1,
                1,
                0,
                0,
                0,
                0,
                0,
                1,
                0,
                -1,
                1,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                -1,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0,
                0
            ]), 3))
        }
            
        total += 1;
        。。。
    }
}

border.drc 本身是带有 insetnotch 数据, 但是dracoLoader加载后会丢失所以需要我们手动配置, 后面会在着色器中使用。

initWrold(){ 
    ... 
    this.bckgroundCreate = new BackgroundCreate(this); 
    this.borderCreate = new BorderCreate(this);
    ... 
}
class BorderCreate {
    constructor(base, t = {}) {
        this.base = base;               // 引用全局系统(含 renderer, scene, screen 等)
        this.options = {...t};         // 接收配置项(目前未使用,但为扩展保留)
        this.init();                   // 初始化边框
    }

    init() {
        // 从 base 中获取边框几何体
        const geometry = this.base.border;

        // 将几何体中的 position 顶点坐标进行四舍五入(对齐像素?)
        const t = this.base.border.attributes.position.array;
        for (let s = 0; s < t.length; s++) {
            t[s] = Math.round(t[s]);
        }

        // 创建 ShaderMaterial,用于渲染边框
        const material = new THREE.ShaderMaterial({
            uniformsGroups: [this.base.UBO], // 使用共享的 Uniform Buffer Object(含 time, resolution 等)
            uniforms: {
                uBorderSizePixels: { value: 64 },                             // 边框宽度(像素)
                uNotchSizePixels: { value: new THREE.Vector2(384, 103) },     // 缺口尺寸(像素)
                uColor1: { value: new THREE.Color("#ecc168") },               // 主颜色
                uColor2: { value: new THREE.Color("#ffec95") },               // 次颜色(暂未使用)
                tNoise: { value: this.base.noiseSimplexLayeredTexture },      // 噪声贴图
                uRes: { value: new THREE.Vector2(this.base.screen.width, this.base.screen.height) } // 屏幕尺寸
            },

            vertexShader: `
// 顶点着色器:用于计算每个顶点的偏移位置

uniform Global{ vec2 resolution; float time; float dtRatio; };

attribute vec3 inset;     // 顶点属性,表示边框方向(单位向量)
attribute vec3 notch;     // 顶点属性,表示缺口方向(单位向量)

uniform float uBorderSizePixels;
uniform vec2 uNotchSizePixels;
uniform vec2 uRes;

varying vec2 vUv;

void main() {
    vUv = uv;             // 将 UV 传递给片元着色器
    vec3 pos = position;

    // 将方向向量从像素坐标系转换为 NDC 坐标系([-1, 1])
    vec2 borderDir = inset.xy / uRes;
    vec2 notchDir = notch.xy / uRes;

    // 应用边框宽度(以像素为单位)
    pos.xy += borderDir * uBorderSizePixels;

    // 应用 notch 缺口尺寸(以像素为单位)
    pos.xy += notchDir * uNotchSizePixels;

    // 输出最终顶点位置(NDC 空间)
    gl_Position = vec4(pos, 1.0);
}
            `,

            fragmentShader: `
// 片元着色器:负责绘制边框外观

layout(location = 1) out highp vec4 gInfo; // 输出 G-buffer,用于延迟渲染管线(伪 ID)

uniform Global{ vec2 resolution; float time; float dtRatio; };
float aastep(float threshold, float value) {
    // AA 平滑函数,避免锯齿
    float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.7071;
    return smoothstep(threshold - afwidth, threshold + afwidth, value);
}

uniform sampler2D tNoise;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform float uThickness;

varying vec2 vUv;

mat2 rotateAngle(float a) {
    // 旋转矩阵
    float s = sin(a);
    float c = cos(a);
    return mat2(c, s, -s, c);
}

void main() {
    // 将屏幕坐标转换为归一化的 UV(带宽高比)
    vec2 screenUv = gl_FragCoord.xy / resolution.xy;
    float aspect = resolution.x / resolution.y;
    screenUv.x *= aspect;

    vec2 uv = screenUv;
    vec2 noiseUv = screenUv;

    // 时间步长控制旋转动画扰动(每约 0.33 秒更新一次角度)
    float steppedTime = floor(time * 3.0) * 3.14159 * 0.2;
    noiseUv = rotateAngle(steppedTime) * noiseUv;

    // 使用噪声扰动边框
    float n0 = texture2D(tNoise, noiseUv).r;

    // 计算一个从左到右的渐变(+ 噪声扰动)
    float gradient = smoothstep(0.0, 0.2, vUv.x + n0 * 0.1);

    // 丢弃部分片元,使边框有断裂/不规则边缘效果
    if (gradient < 0.5) {
        discard;
    }

    // 颜色设置
    vec3 color = uColor1;
    gl_FragColor = vec4(color, 0.87946); // 带透明度

    // 写入 G-buffer:蓝色编码 1.0(可能用于识别 border)
    gInfo = vec4(1.0, vec3(0.0, 0.0, 1.0));
}
            `,

            depthTest: false
        });

        // 创建并配置 Mesh
        this.mesh = new THREE.Mesh(geometry, material);
        this.mesh.name = "border";
        this.mesh.renderOrder = 2; // 渲染顺序早于其他元素(可用于 UI 层)
        this.mesh.updateMatrixWorld();
        this.mesh.matrixAutoUpdate = false; // 禁用自动更新,提高性能

        // 初次调整分辨率
        this.resize({ w: this.base.screen.width, h: this.base.screen.height });

        // 监听窗口大小变化
        this.base.eventManage.on("resize", this.resize.bind(this));

        // 添加到场景中
        this.base.scene.add(this.mesh);
    }

    // 处理窗口尺寸变化时更新 uniform 分辨率
    resize({ w, h }) {
        this.mesh.material.uniforms.uRes.value.set(w, h);
    }
}

阶段展示

添加缺口图标

initWrold(){
    ... 
    this.borderCreate = new BorderCreate(this); 
    this.notchCreate = new NotchCreate(this);
    ... 
}
class NotchCreate {
    constructor(base, t = {}) {
        this.base = base;                   // 全局控制器,包含 scene、camera、uniforms、纹理等
        this.imageAspect = 1;              // 记录贴图宽高比(用于正确缩放 UI)
        this.options = {...t};             // 可选项,暂未使用
        this.init();                       // 初始化 Notch
    }

    init() {
        // 创建一个单位大小的平面几何体,并将其原点移至左上角(便于屏幕对齐)
        const geometry = new THREE.PlaneGeometry();
        geometry.translate(-0.5, 0.5, 0);

        // 创建 Shader 材质,包含颜色、贴图、噪声扰动等
        const material = new THREE.ShaderMaterial({
            uniformsGroups: [this.base.UBO], // 引用统一全局 uniform buffer(resolution、time等)
            uniforms: {
                uColor1: {value: new THREE.Color("#ecc168")}, // 渐变起始色
                uColor2: {value: new THREE.Color("#9f4a16")}, // 渐变目标色
                tNoise:  {value: this.base.noiseSimplexLayeredTexture}, // 噪声扰动贴图
                tMap:    {value: this.base.emailTexture} // 显示的文字或图案贴图
            },
            depthTest: false, // UI 元素一般不需要深度测试
            vertexShader: `
uniform Global { vec2 resolution; float time; float dtRatio; };
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
            `,
            fragmentShader: `
layout(location = 1) out highp vec4 gInfo;

uniform Global{ vec2 resolution; float time; float dtRatio; };

// 自定义抗锯齿函数
float aastep(float threshold, float value){
    float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.7071;
    return smoothstep(threshold - afwidth, threshold + afwidth, value);
}

uniform sampler2D tNoise;   // 噪声贴图
uniform sampler2D tMap;     // 显示内容贴图
uniform vec3 uColor1;       // 起始颜色
uniform vec3 uColor2;       // 目标颜色

varying vec2 vUv;

// 旋转矩阵函数,用于扭曲 UV
mat2 rotateAngle(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c, s, -s, c);
}

void main() {
    vec2 uv = vUv;

    // 使用噪声扰动 uv,实现动态抖动视觉
    vec2 noiseUv = uv * 0.5;
    float steppedTime = floor(time * 3.0) * 3.14159 * 0.2;
    noiseUv = rotateAngle(steppedTime) * noiseUv;
    float n0 = texture2D(tNoise, noiseUv).r;
    uv += n0 * 0.006;

    // 缩放 UV 坐标以放大中心内容
    uv -= 0.5;
    uv *= 1.2;
    uv += 0.5;

    // 采样图案贴图,获取灰度值
    float text = texture2D(tMap, uv).r;

    // 使用 aastep 对文字边缘做平滑处理
    text = aastep(0.7, text);

    // 根据文字灰度混合颜色
    vec3 color = mix(uColor1, uColor2, text);

    // 输出颜色
    gl_FragColor = vec4(color, 0.87946);

    // 输出 G-buffer 信息(用于后处理/点击判定)
    gInfo = vec4(1.0, vec3(0.0, 0.0, 1.0));
}
            `
        });

        // 创建并添加 UI Mesh 到场景
        this.mesh = new THREE.Mesh(geometry, material);
        this.mesh.renderOrder = 3; // 提高渲染优先级,保证 UI 在最上层
        this.base.scene.add(this.mesh);

        // 创建交互器,启用点击 + 悬停监听
        this.interaction = new MeshInteractor(this, {
            camera: this.base.scene.camera,
            meshes: [this.mesh],
            hoverCursor: true, // 悬停时变鼠标样式
            onHover: () => {},
            onClick: () => {
                // 点击跳转邮箱
                window.location.href = "mailto:hi@abeto.co";
            }
        });

        // 计算贴图的宽高比
        this.imageAspect = material.uniforms.tMap.value.image.width /
                           material.uniforms.tMap.value.image.height;

        // 每帧调整位置(响应相机或屏幕变化)
        this.base.scene.beforeRenderCbs.push(this.positionMesh.bind(this));

        // 启用交互
        this.interaction.enable();
    }

    /**
     * 将一个平面 UI 精确放置到相机视野中指定位置
     */
    positionUI({
        camera = null,
        mesh = null,
        x = 0,
        y = 0,
        width = 1,
        height = 1,
        distance = null,
        billboardCamera = true
    } = {}) {
        const Bm = new THREE.Vector3(); // 缓存向量
        const $r = new THREE.Vector2(); // 缓存视野大小

        // 计算 UI 距离相机的距离
        const l = distance || Bm.subVectors(camera.position, camera.target).length();

        // 获取当前距离下相机视野范围(世界单位)
        camera.getViewSize(l, $r);

        // 缩放 UI 到屏幕等比例大小
        const scaleFactor = $r.y / this.base.screen.height;
        mesh.scale.set(width * scaleFactor, height * scaleFactor, 1);

        // 将屏幕像素坐标 (x, y) 映射到相机空间位置
        const nx = x / this.base.screen.width;
        const ny = y / this.base.screen.height;
        mesh.position.copy(camera.position).add(
            Bm.set(
                $r.x * -0.5 + $r.x * nx,
                $r.y * 0.5 - $r.y * ny,
                -l
            ).applyQuaternion(camera.quaternion)
        );

        // 面向摄像机(billboard UI)
        if (billboardCamera) {
            mesh.quaternion.copy(camera.quaternion);
        }

        mesh.updateMatrixWorld();
    }

    /**
     * 每帧调用,将 Notch 放置在屏幕右上角
     */
    positionMesh() {
        const height = 200 / this.imageAspect; // 根据图片比例自动算高
        const paddingX = 11; // 距离右侧边距
        const paddingY = 15; // 距离顶部边距

        this.positionUI({
            camera: this.base.scene.camera,
            mesh: this.mesh,
            x: this.base.screen.width - paddingX,
            y: this.base.screen.height - paddingY,
            width: 200,
            height: height
        });
    }
}
功能实现方式
✅ UI 创建使用 PlaneGeometry 创建纯 GLSL 驱动的 2D 元素
🎨 动态扰动使用噪声贴图动态扰动 UV
✍️ 内容显示tMap 贴图可以显示 logo/文字
🖱️ 点击交互MeshInteractor 启用点击事件
🎯 精准定位使用 getViewSize + 自定义坐标实现屏幕定位
📐 自动缩放UI 会随贴图宽高比自动适配大小

阶段展示

到了这一步,我们基本上完成了目标网站的复刻,现在只差鼠标轨迹了,让我们继续吧!

添加鼠标轨迹

initWrold(){
    ...
    this.notchCreate = new NotchCreate(this);
    this.lineCreate = new LineCreate(this);
    ... 
}

LineCreate

1. 构造函数部分

constructor(base, t = {}) {
    this.base = base;
    this.options = { length: 0.5, ...t };
    this.count = 1;       // 支持多个实例(目前只用 1 条线)
    this.points = 16;     // 一条线由 16 个顶点构成(16 个 position 点)
    this.init();          // 初始化系统
}

2. 初始化核心 init()

构造 Polyline 几何体

this.geometry = this.createPolylineGeometry({
    count: this.count,
    points: this.points,
    closed: false
});
  • 创建支持宽度的“线段面片”几何体

  • 每段线有两个三角形构成,可以在 shader 中偏移实现厚度

创建 Shader 材质(用于渲染线)

this.material = new THREE.ShaderMaterial({...});

顶点着色器:

变量功能
position储存的是 [currIndex, prevIndex, nextIndex]
uvy表示该顶点使用的数据纹理 Y 坐标(其实恒为 0)
tTexture1存储点位数据的纹理,每个像素一个 vec3
lineWidth动态线条宽度,实时变化

关键步骤:

  1. 采样当前位置 + 前后点
  2. 投影到屏幕空间,计算方向
  3. 求朝向 → 得到法线 → 位移当前点实现宽度
  4. 支持多种宽度变化曲线(如 parabola, pcurve

片元着色器:

功能:

  • tNoise 噪声贴图扰动 → 使线条“流动”
  • 使用 (1 - vUv.x) 让头部更不透明、尾部淡出
  • 如果噪声太低就 discard(制造断裂效果)

创建计算材质(用于更新点位)

this.textureData = {
  textures: 1,
  material: new THREE.ShaderMaterial({...})
}

这是一个在 GPU 中运行的模拟器,逻辑:

  1. 从上一帧 tTexture1 中读取点位
  2. 第一个点直接设为 uMousePos
  3. 其余点使用 mix() 向前一个点靠近
  4. 可通过 uSnap 强制所有点同步为当前位置

结果写入一个新的纹理,实现“尾巴”拖动效果。

生成 Mesh 并添加到场景中

this.mesh = new CustomLineMesh(this);
this.mesh.name = "line"
...
this.base.scene.add(this.mesh)
  • CustomLineMesh 是对材质 + 几何的包装,支持带宽度 polyline

  • 关闭 matrixAutoUpdate 是为了提升性能(你手动更新位置)

初始化用户交互 & 动态控制

const e = new THREE.Vector3();
const t = new THREE.Vector3();
let n = 0;
let s = false;
this.base.eventManage.once("touch_move", () => { s = true });

每帧更新逻辑(beforeRender)

this.base.scene.beforeRenderCbs.push(() => { ... })

每帧执行:

  1. 获取当前触点位置(投影到世界)
  2. 插值更新 uMousePos
  3. 计算速度 n,用于控制线宽变化
  4. 设置 lineWidth
lineWidth = 9 / screenHeight * fit(n, .01, .001, 1, 0)

移动越快越粗,越慢越细

  1. 判断是否需要 snap 所有点

3.createPolylineGeometry方法

createPolylineGeometry({ points, count, closed })

核心输出:

  • position: 每个顶点存储 [当前索引, 前索引, 后索引]
  • uv: 标准 uv 坐标
  • uvy: 存储实例编号(这里只为 0
  • index: 构建三角 strip 面片

顶点重复两次是为了什么?

每个点上下边界都需要,才能做“宽度”。

4.顶点着色器(vertexShader)

attribute float uvy;

uniform Global { vec2 resolution; float time; float dtRatio; };

uniform sampler2D tTexture1;
uniform float lineWidth;

varying vec2 vUv;
varying vec2 vHighPrecisionZW;

float parabola(float x, float k) {
    return pow(4.0 * x * (1.0 - x), k);
}

float pcurve(float x, float a, float b) {
    float k = pow(a + b, a + b) / (pow(a, a) * pow(b, b));
    return k * pow(x, a) * pow(1.0 - x, b);
}

float when_eq(float x, float y) {
    return 1.0 - abs(sign(x - y));
}

void main() {
    vUv = uv;

    // 从纹理中读取当前/前一个/后一个控制点位置
    vec3 current = texelFetch(tTexture1, ivec2(position.x, uvy), 0).xyz;
    vec3 previous = texelFetch(tTexture1, ivec2(position.y, uvy), 0).xyz;
    vec3 next = texelFetch(tTexture1, ivec2(position.z, uvy), 0).xyz;

    // 投影到裁剪空间
    mat4 projViewModel = projectionMatrix * modelViewMatrix;
    vec4 currentProjected = projViewModel * vec4(current, 1.0);
    vec4 previousProjected = projViewModel * vec4(previous, 1.0);
    vec4 nextProjected = projViewModel * vec4(next, 1.0);

    // 考虑屏幕比例变形
    vec2 aspectVec = vec2(resolution.x / resolution.y, 1.0);

    // 转换为 NDC 坐标(考虑 aspect 比)
    vec2 currentScreen = currentProjected.xy / currentProjected.w * aspectVec;
    vec2 previousScreen = previousProjected.xy / previousProjected.w * aspectVec;
    vec2 nextScreen = nextProjected.xy / nextProjected.w * aspectVec;

    // 求方向向量(朝向),双向平均
    vec2 dir1 = normalize(currentScreen - previousScreen);
    vec2 dir2 = normalize(nextScreen - currentScreen);
    vec2 dir = normalize(dir1 + dir2);

    // 边缘兼容处理(若当前点等于 next 或 prev)
    dir = mix(dir, dir1, when_eq(position.x, position.z));
    dir = mix(dir, dir2, when_eq(position.x, position.y));

    // 求法线方向(旋转 90°)
    vec2 normal = vec2(-dir.y, dir.x);
    normal.x /= aspectVec.x; // 纠正横向拉伸

    float w = lineWidth;

    // 不同线条形状(可通过 #define SHAPE 控制)
    #if SHAPE == 1
        w *= uv.x;
    #elif SHAPE == 2
        w *= 1.0 - uv.x;
    #elif SHAPE == 3
        w *= parabola(uv.x, 1.0);
    #endif

    // 偏移位置:上边界 +w,下边界 -w(由 uv.y 控制)
    normal *= w;
    currentProjected.xy += normal * mix(1.0, -1.0, step(0.5, uv.y));

    gl_Position = currentProjected;

    // 用于后处理中的高精度深度计算
    vHighPrecisionZW = gl_Position.zw;
}

核心逻辑:

  • 每个顶点是 [当前, 上一个, 下一个] 的 index
  • 每帧从数据纹理读取控制点位置
  • 计算线的方向 → 得到法线 → 加偏移 → 实现线宽
  • 支持不同形状(平头、锥形、抛物线收尾等)

4.片元着色器(fragmentShader)

layout(location = 1) out highp vec4 gInfo;

uniform Global { vec2 resolution; float time; float dtRatio; };
uniform vec3 uColor;
uniform sampler2D tNoise;

varying vec2 vUv;
varying vec2 vHighPrecisionZW;

void main() {
    vec3 color = uColor;

    // 使用时间控制动画扰动(整数时间步)
    float steppedTime = floor(time * 2.0);

    // 屏幕坐标 → UV
    vec2 screenUV = gl_FragCoord.xy / resolution.xy;

    // 采样噪声贴图(扰动加动画)
    float noise = texture2D(tNoise, screenUV * 2.0 + steppedTime * 0.02).r;

    // 尾部位置更不受干扰,线头更容易被遮断
    noise *= (1.0 - vUv.x);

    // 噪声太低则丢弃片元,形成“破损、闪烁”效果
    if (noise < 0.125) discard;

    gl_FragColor = vec4(color, 0.87946); // 最终颜色带透明度

    gInfo = vec4(1.0, vec2(0.0), 0.0); // 输出给 gBuffer 的附加数据(非必需)
}

核心逻辑:

  • 使用 tNoise 纹理加动画扰动
  • 尾部更完整,头部有噪声“撕裂感”
  • discard 实现断裂的视觉美术效果

CustomLineMesh

是一个自定义的三维网格类,专门用于处理具有特殊渲染目标和计算的线段(MeshLines)。这个类的设计目的是为了解决线段渲染中常见的一些问题,如使用多个渲染目标(Multiple Render Targets, MRT)和通过计算材质处理渲染操作。

1. 构造函数

constructor(base) {
    super(base.geometry, base.material);
    this.base = base;
    this.isMeshLine = !0;
    this.linesCount = this.base.count;
    this.name = this.linesCount > 1 ? "Meshlines" : "Meshline";
    this.frustumCulled = !1;

  • super(base.geometry, base.material):调用父类 THREE.Mesh 的构造函数,传入几何体和材质。

  • this.base = base:保存传入的 base 对象。

  • this.isMeshLine = !0:表示当前对象是一个 "MeshLine"(线段网格)。

  • this.linesCount = this.base.count:获取线段的数量。

  • this.name = this.linesCount > 1 ? "Meshlines" : "Meshline":根据线段数量设置名称,单一线段时为 "Meshline",多条线段时为 "Meshlines"。

  • this.frustumCulled = !1:禁用视锥剔除,确保对象始终渲染。

2. WebGL 渲染目标设置

const renderTargetType = this.base.base.renderer.webgl.capabilities.floatRenderTarget ? THREE.FloatType : THREE.HalfFloatType;
const size = this.base.base.utils.ceilPowerOfTwo(Math.max(2, this.base.points));
this.rt1 = new THREE.WebGLMultipleRenderTargets(size, size, this.base.textureData.textures || 1, {
    wrapS: THREE.ClampToEdgeWrapping,
    wrapT: THREE.ClampToEdgeWrapping,
    minFilter: THREE.NearestFilter,
    magFilter: THREE.NearestFilter,
    format: THREE.RGBAFormat,
    type: renderTargetType,
    depthBuffer: !1
});
this.rt2 = this.rt1.clone();
this.rtCurrent = 0;
  • 判断 WebGL 是否支持浮点渲染目标(float render target),并选择合适的类型(THREE.FloatTypeTHREE.HalfFloatType)。

  • 计算渲染目标的大小,保证它是 2 的幂。

  • 使用 THREE.WebGLMultipleRenderTargets 创建渲染目标 rt1rt2,这两个渲染目标用于交替存储渲染数据。

3. FullScreenQuad 渲染设置

this.fsQuad = new FullScreenQuad(null);
this.computationMaterial = this.base.textureData.material;
this.fsQuad.material = this.computationMaterial;
  • 创建一个全屏四边形 (FullScreenQuad) 用于渲染计算操作。

  • computationMaterial 是用于执行计算的材质,这个材质通过 this.base.textureData.material 获取。

4. compute 方法

compute(renderer, scene, camera) {
    const t = this.base.base.renderer.webgl;
    const r = this.computationMaterial.uniforms.uModelMatrix;
    const a = this.computationMaterial.uniforms.uViewMatrix;
    const o = this.computationMaterial.uniforms.uProjMatrix;
    r && r.value.copy(this.matrixWorld);
    a && a.value.copy(camera.matrixWorldInverse);
    o && o.value.copy(camera.projectionMatrix);
  • compute 方法执行渲染计算,将当前对象的模型矩阵、视图矩阵和投影矩阵传递给计算材质。

  • r, a, o 分别是模型矩阵、视图矩阵和投影矩阵的 uniform。

const l = this.rtCurrent === 0 ? this.rt1 : this.rt2;
const c = this.rtCurrent === 0 ? this.rt2 : this.rt1;
this.rtCurrent = (this.rtCurrent + 1) % 2;
  • 判断当前使用哪个渲染目标 rt1rt2,并在每次渲染后交替使用它们。
for (let f = 0; f < c.texture.length; f++) {
    const A = this.computationMaterial.uniforms[`tTexture${f + 1}`];
    A && (A.value = c.texture[f]);
}
  • 将前一个渲染目标的纹理传递给计算材质的 uniforms。
const h = t.autoClear;
t.autoClear = !1;
const u = t.getRenderTarget();
t.setRenderTarget(l);
t.getClearColor(new THREE.Color());
const d = t.getClearAlpha();
t.setClearColor(new THREE.Color("#000000"), 0);
t.clear(!0, !1, !1);
this.fsQuad.render(t);
t.autoClear = h;
t.setRenderTarget(u);
t.setClearColor(new THREE.Color(), d);
  • 禁用自动清除,保存当前的渲染目标,设置新的渲染目标并清空它,最后通过 fsQuad.render 渲染计算。
for (let f = 0; f < l.texture.length; f++) {
    const A = this.material.uniforms[`tTexture${f + 1}`];
    A && (A.value = l.texture[f]);
    const g = this.material.uniforms[`tTexture${f + 1}Prev`];
    g && (g.value = c.texture[f]);
}
  • 将计算后的纹理传递给材质的 uniforms,用于后续渲染。
this.afterCompute && this.afterCompute(renderer, scene, camera);
  • 如果定义了 afterCompute 方法,则在计算完成后执行它。

5. dispose 方法

dispose() {
    var t;
    this.fsQuad.dispose();
    this.computationMaterial.dispose();
    this.rt1.dispose();
    this.rt2.dispose();
    (t = super.dispose) == null || t.call(this);
}

释放资源,清理 FullScreenQuad、计算材质、渲染目标 (rt1, rt2) 以及调用父类的 dispose 方法进行其他清理。

PlaneProjector

PlaneProjector类是一个实现了将点投影到平面上的系统,并且支持多种不同的平面定义和交互方式。

最终展示

总结

源代码:code.juejin.cn/pen/7494477…

好了,到这里我们就结束了,希望你能获得些什么,这也是我所快乐的。

感谢你的阅读!

我是乌鲁木齐大海龟,也是西伯利亚小火龙,我们下次再见!