Three.js 玻璃雨景效果详解教程

460 阅读6分钟

概述

这个项目实现了一个令人惊艳的玻璃雨景效果,通过Three.js和GLSL着色器技术,模拟了雨滴在玻璃表面流淌的真实物理现象。效果包含动态雨滴、光线折射、景深模糊以及可选的心形图案等功能。

项目地址 github.com/xiaxiangfen…

核心技术架构

1. 基础框架设置

项目使用Three.js的着色器材质系统:

// 使用正交相机创建全屏四边形
camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);

// 着色器材质配置
material = new THREE.ShaderMaterial({
    uniforms: {
        iTime: { value: 0 },
        iResolution: { value: new THREE.Vector3(width, height, 1.0) },
        rainAmount: { value: 0.7 },
        hasHeart: { value: 1.0 },
        iChannel0: { value: texture }
    },
    vertexShader: vertexShader,
    fragmentShader: fragmentShader
});

2. 顶点着色器

顶点着色器负责将几何体顶点从模型空间变换到屏幕空间:

varying vec2 vUv;
void main() {
    vUv = uv;  // 传递UV坐标到片元着色器
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

片元着色器核心算法解析

1. 噪声函数系统

N13函数 - 三维哈希噪声

vec3 N13(float p) {
    vec3 p3 = fract(vec3(p) * vec3(.1031,.11369,.13787));
    p3 += dot(p3, p3.yzx + 19.19);
    return fract(vec3((p3.x + p3.y)*p3.z, (p3.x+p3.z)*p3.y, (p3.y+p3.z)*p3.x));
}

数学原理:

  • 使用分数函数fract()确保输出在[0,1)范围内
  • 通过质数乘法和点积操作增加随机性
  • 三个分量间的交叉计算确保输出的伪随机性

用途: 为每个雨滴网格生成唯一的随机属性(位置、时间偏移、大小等)

N14函数 - 四维向量噪声

vec4 N14(float t) {
    return fract(sin(t*vec4(123., 1024., 1456., 264.))*vec4(6547., 345., 8799., 1564.));
}

特点: 一次性生成四个相关但独立的随机值,提高计算效率

2. 平滑插值函数

Saw函数 - 锯齿波平滑

float Saw(float b, float t) {
    return S(0., b, t)*S(1., b, t);
}

数学含义:

  • S(0., b, t) = smoothstep(0, b, t):从0到b的平滑上升
  • S(1., b, t) = smoothstep(1, b, t):从1到b的平滑下降
  • 两者相乘产生一个在中间达到峰值的钟形曲线

应用: 控制雨滴的生命周期,实现自然的出现和消失效果

3. 雨滴层渲染算法

DropLayer2函数 - 主雨滴层

这是整个效果的核心函数,模拟雨滴在玻璃上的物理行为:

vec2 DropLayer2(vec2 uv, float t) {
    vec2 UV = uv;
    
    // 时间驱动的垂直流动
    uv.y += t*0.75;
    vec2 a = vec2(6., 1.);  // 长宽比控制
    vec2 grid = a*2.;       // 网格密度
    vec2 id = floor(uv*grid); // 网格ID
    
    // 列偏移实现随机起始时间
    float colShift = N(id.x); 
    uv.y += colShift;
    
    id = floor(uv*grid);
    vec3 n = N13(id.x*35.2+id.y*2376.1); // 每个网格的随机属性
    vec2 st = fract(uv*grid)-vec2(.5, 0); // 网格内局部坐标
    
    // 水平摆动模拟
    float x = n.x-.5;  // 基础水平位置
    float y = UV.y*20.;
    float wiggle = sin(y+sin(y)); // 复合正弦波产生自然摆动
    x += wiggle*(.5-abs(x))*(n.z-.5); // 边界约束的摆动
    x *= .7; // 幅度控制
    
    // 垂直位置动画
    float ti = fract(t+n.z); // 时间偏移
    y = (Saw(.85, ti)-.5)*.9+.5; // 锯齿波控制雨滴生命周期
    
    vec2 p = vec2(x, y); // 雨滴中心位置
    
    // 主雨滴渲染
    float d = length((st-p)*a.yx); // 椭圆距离场
    float mainDrop = S(.4, .0, d); // 平滑阶梯函数创建圆形
    
    // 雨痕轨迹计算
    float r = sqrt(S(1., y, st.y)); // 垂直衰减因子
    float cd = abs(st.x-x); // 到中心线的水平距离
    float trail = S(.23*r, .15*r*r, cd); // 轨迹宽度随高度变化
    float trailFront = S(-.02, .02, st.y-y); // 轨迹前端淡出
    trail *= trailFront*r*r;
    
    // 小水滴效果
    y = UV.y;
    float trail2 = S(.2*r, .0, cd);
    float droplets = max(0., (sin(y*(1.-y)*120.)-st.y))*trail2*trailFront*n.z;
    y = fract(y*10.)+(st.y-.5);
    float dd = length(st-vec2(x, y));
    droplets = S(.3, 0., dd);
    
    float m = mainDrop+droplets*r*trailFront;
    
    return vec2(m, trail); // 返回雨滴遮罩和轨迹强度
}

关键数学概念:

  1. 距离场(Distance Field)

    • length((st-p)*a.yx)计算椭圆形距离场
    • 通过长宽比a控制雨滴形状
  2. 平滑阶梯函数

    • smoothstep(edge0, edge1, x)实现平滑边界
    • 比硬边界更真实,避免锯齿效应
  3. 复合波形

    • sin(y+sin(y))创建非规律摆动
    • 模拟雨滴受风力和重力的复合影响

4. 静态水滴系统

float StaticDrops(vec2 uv, float t) {
    uv *= 40.; // 增加密度
    
    vec2 id = floor(uv);
    uv = fract(uv)-.5;
    vec3 n = N13(id.x*107.45+id.y*3543.654);
    vec2 p = (n.xy-.5)*.7;
    float d = length(uv-p);
    
    float fade = Saw(.025, fract(t+n.z)); // 闪烁效果
    float c = S(.3, 0., d)*fract(n.z*10.)*fade;
    return c;
}

作用: 模拟玻璃表面的静态小水珠,增加细节层次

5. 光线折射与模糊效果

法线计算

vec2 e = vec2(.001, 0.);
float cx = Drops(uv+e, t, staticDrops, layer1, layer2).x;
float cy = Drops(uv+e.yx, t, staticDrops, layer1, layer2).x;
vec2 n = vec2(cx-c.x, cy-c.x); // 数值微分计算法线

数学原理:

  • 使用有限差分法计算表面法线
  • 法线用于计算光线折射偏移

景深模糊

float focus = mix(maxBlur-c.y, minBlur, S(.1, .2, c.x));
vec3 col = textureLod(iChannel0, UV+n, focus).rgb;

物理模拟:

  • 根据雨滴密度调整模糊程度
  • 使用textureLod实现多级纹理采样的景深效果

6. 心形特效算法

vec2 hv = uv-vec2(.0, -.1);
hv.x *= .5;
float s = S(110., 70., T);
hv.y-=sqrt(abs(hv.x))*.5*s; // 心形数学方程
heart = length(hv);
heart = S(.4*s, .2*s, heart)*s;

心形方程解析:

  • 基于参数方程 y - sqrt(|x|) * scale
  • 通过时间函数控制心形的出现和消失

渲染管线流程

1. 初始化阶段

// 纹理加载与配置
texture = loader.load('./rain-heart.jpg');
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipmapLinearFilter;

2. 实时渲染循环

function animate() {
    const time = clock.getElapsedTime() * timeSpeed;
    material.uniforms.iTime.value = time;
    renderer.render(scene, camera);
}

3. 交互控制系统

  • 雨量控制:调整不同雨滴层的强度
  • 时间速度:控制动画播放速度
  • 心形模式:切换普通/心形特效模式

性能优化策略

1. 纹理优化

  • 使用mipmap实现高效的多级纹理采样
  • 线性过滤减少采样噪声

2. 计算优化

  • 噪声函数使用整数运算避免浮点精度问题
  • 分层渲染减少重复计算

3. 内存管理

  • 统一缓冲区对象管理GPU资源
  • 事件监听器正确绑定和解绑

相关技术:

  • 流体模拟:Navier-Stokes方程
  • 光线追踪:更精确的折射计算
  • 粒子系统:GPU粒子渲染

结语

这个雨景效果展示了现代GPU着色器编程的强大能力,通过数学函数和物理模拟的巧妙结合,在实时渲染中创造出逼真的自然现象。掌握这些技术对于理解现代图形编程和游戏开发具有重要意义。

关键学习要点:

  • 噪声生成:程序化内容生成的基础
  • 距离场:高效的形状表示方法
  • 数值微分:计算图形学中的导数
  • 纹理采样:GPU纹理系统的高级应用
  • 着色器优化:实时渲染的性能考量