Three.js教程
Shader
threejs-raymarching-nano-chip
threejs-raymarching-nano-chip

Three.js 怎么做芯片效果?一个矩形 + 一段 Shader 就够了

第一次看到这个效果,以为是 3D 建模加材质贴图做出来的。

拖进代码一看:场景里就一个铺满屏幕的矩形,所有电路板、霓虹辉光、流动数据、闪烁粒子,全部在一段 Fragment Shader 里算出来的

这篇文章拆解它的完整实现原理。


架构:全屏 Quad,CPU 什么都不做

大多数 Three.js 项目是这样工作的:CPU 建模 → 上传 GPU → 逐帧渲染。

这个效果反过来:CPU 只放一个铺满屏幕的矩形,GPU 的 Fragment Shader 负责计算每个像素应该是什么颜色

// 正交相机恰好覆盖整个裁剪空间
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
camera.position.z = 1;
 
// 2x2 的矩形,刚好铺满 NDC 空间
const geometry = new THREE.PlaneGeometry(2, 2);
const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms });

Vertex Shader 只做一件事:把 UV 传给 Fragment Shader,连矩阵变换都不需要。

varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = vec4(position, 1.0); // 直接用,不乘 MVP 矩阵
}

这是 ShaderToy 类效果的标准姿势,也叫 Fullscreen Quad。你在 ShaderToy 上看到的所有效果,底层都是这一个矩形。


核心:光线步进(Raymarching)

场景里没有真实的 3D 网格,那"摄像机飞越芯片"的立体感从哪来?

答案是 Raymarching:从每个像素向场景发出一条射线,沿射线一步步推进,直到射线"碰到"某个表面。

#define MAX_STEPS 120   // 最多走 120 步
#define MAX_DIST 40.0   // 走超过这个距离就算没碰到
#define SURF_DIST 0.002 // 距离小于这个就算碰到了
 
for(int i = 0; i < MAX_STEPS; i++) {
    p = ro + rd * distTotal;      // 当前位置
    float distStep = getDist(p);  // 距离场函数:离最近表面多远?
    distTotal += distStep;
    if(distTotal > MAX_DIST || abs(distStep) < SURF_DIST) break;
}

关键在于 getDist() 怎么描述场景形状。这里没有三角面,而是用一个数学函数来表达"整个芯片电路板"。


电路图案:随机二分,不是贴图

芯片电路的图案是程序化生成的,每次运行都一样,但没有任何图片文件。

getPatternClean()随机二分法生成它:

float getPatternClean(vec2 p) {
    vec2 g = floor(p);    // 当前格子坐标
    vec2 lp = fract(p);   // 格子内的局部坐标
    vec2 e = g;
    float lines = 0.0;
 
    for(int i = 0; i < 10; i++) {
        if (i >= uIterations) break;
 
        // 随机切一刀(X 方向)
        float fX = rand2(e + vec2(0.0, float(i)));
        if(lp.x < fX) e.x += w; else e.x -= w;
 
        // 用 smoothstep 把切割线画出来
        float lineX = smoothstep(thick + bevel, thick, abs(lp.x - fX));
        lines = max(lines, lineX);
    }
    return 1.0 - lines; // 1 = 芯片板块,0 = 沟槽
}

每次迭代随机切一刀,切多了就形成类似芯片电路的分形分割图案。uIterations 控制迭代次数,次数越高细节越丰富,GPU 消耗也越高。

[配图:不同 uIterations 值下的电路图案对比]


把 2D 图案"挤出"成 3D 地形

getPatternClean() 返回的是 0 到 1 的平面图案值。getDist() 把它变成 3D 高度场:

float getDist(vec3 p) {
    float chip = getPatternClean(p.xz * uPatternScale); // 采样 XZ 平面
    float h = chip * uExtrusion;  // 图案值 × 挤出高度
    return (p.y - h) * 0.45;     // 当前点离地面的距离
}

逻辑很简单:电路板块的地方高,沟槽的地方低,一个高度场就构成了整个芯片的 3D 形状。

Raymarching 沿射线步进时就用这个函数判断有没有碰到表面。


法线、光照、软阴影

碰到表面之后,需要计算光照。法线这里用四点差分估算(比六点差分省 1/3 采样):

vec3 getNormal(vec3 p) {
    vec2 e = vec2(0.003, -0.003);
    return normalize(
        e.xyy * getDist(p + e.xyy) +
        e.yyx * getDist(p + e.yyx) +
        e.yxy * getDist(p + e.yxy) +
        e.xxx * getDist(p + e.xxx)
    );
}

软阴影用 Inigo Quilez 的经典公式,min(res, 8.0 * h / t) 这一行:距离场值越小,阴影越硬,产生柔和的接触阴影效果。

[配图:有软阴影 vs 无软阴影的电路板对比]


三层视觉效果叠加

基础光照之后,再叠加三层效果,把"普通 3D 场景"变成"赛博朋克芯片"。

第一层:霓虹辉光

deepGap 检测射线是否打到沟槽深处,是就叠加霓虹蓝:

float gap = 1.0 - chip;            // 沟槽区域
float deepGap = smoothstep(0.3, 0.0, p.y / uExtrusion); // 越深越亮
col += uGlowColor * gap * deepGap * uGlowIntensity;

第二层:数据流拖尾

pow(pulse, trailLength) 生成带衰减的流光。trailLength 越大,指数衰减越快,拖尾越短:

float pulseX = fract(p.x * tFreq * dirX - speedX + rX * 10.0);
float trailX = pow(pulseX, uTrailLength) * step(0.6, rX); // 只有部分通道有数据流
col += uTrailColor * max(trailX, trailZ) * gap * deepGap * 3.0;

第三层:霓虹粒子

1D Cellular Noise 在沟槽线上生成随机间距的小点。这里有个细节:粒子大小随 distTotal(射线长度)缩放,保证远处粒子和近处粒子在屏幕上看起来差不多大

float dotScale = uParticleSize * max(0.5, distTotal * 0.5);

另外还有第四层:芯片顶面的硅晶点阵,只在 isTop > 0.5 的板块顶面渲染,用 sin(uTime) 做闪烁。

[配图:三层效果分别叠加的过程图]


后处理:雾、渐晕、软输出

三件事,几行代码:

// 指数雾:距离越远越浓
float fog = 1.0 - exp(-distTotal * distTotal * uFogDensity);
col = mix(col, fogColor, fog);
 
// 渐晕:边缘压暗
float vig = length(uv) * 0.4;
col -= vig * vig * 0.5;
 
// 软输出:轻微提亮,避免全黑区域太死
col = smoothstep(0.0, 1.1, col);

雾用二次方(distTotal * distTotal)而不是线性,让雾在远处快速加浓,近处几乎透明,视觉层次感更强。


SSAA 抗锯齿:2 次采样取平均

Shader Art 类效果用默认抗锯齿效果差,这里手动实现了 SSAA:

if (uAA) {
    vec2 offsets[2];
    offsets[0] = vec2(-0.25, -0.25); // 亚像素偏移
    offsets[1] = vec2( 0.25,  0.25);
 
    for(int i = 0; i < 2; i++) {
        vec2 uv = (vUv - 0.5 + offsets[i] / uResolution.xy) * 2.0;
        totalCol += renderScene(uv);
    }
    totalCol *= 0.5; // 取平均
}

只多一倍计算量,锯齿明显减少。不追求极致质量的话,2 次采样够用了。


参数控制

整个效果用 lil-gui 做了完整的实时调参面板:

  • Camera & Zoom:飞行速度、相机距离、镜头焦距
  • Architecture:电路细节层级、网格缩放、芯片高度
  • Data Trails / Neon Particles:数据流和粒子的速度、密度
  • Colors:所有颜色独立控制
  • Quality:开关 SSAA、调整渲染分辨率

[配图:调参面板截图]

调低 uIterations(细节层级)+ 关掉 SSAA + 降低渲染缩放,可以在低端设备上跑。


关键代码汇总

函数作用
getPatternClean()随机二分法生成电路图案
getDist()把 2D 图案挤出成 3D 距离场
getNormal()四点差分估算法线
getShadow()IQ 软阴影公式
renderScene()Raymarching 主循环 + 全部效果叠加

整个效果的本质:一个数学函数描述场景形状,射线步进找到交点,再叠加多层程序化效果。没有模型,没有贴图,纯数学。

源码:https://codepen.io/sabosugi/pen/wBzxKJa (opens in a new tab)