【Three.js】day116-shader-basics

作者:mario 发布时间: 2026-09-07 阅读量:4 评论数:0

Day 116 · 自定义着色器入门 — 打开 3D 特效的最后一道门

前三周你一直在 Three.js 的"预设材质"里工作:MeshStandardMaterial 调参数、贴图、光照——效果好,但有天花板。今天打开最后一道门:自定义着色器(ShaderMaterial)。着色器是"直接给 GPU 写代码"——你想让物体怎么显示,就怎么算颜色。这是本周的灵魂课,也是"手写 WebGL vs Three.js"对照表的最终章:Three.js 帮你搭好了管线,但着色器里的一切是你自己的。今天只学"够用"的 GLSL 基础,写第一个会发光的自定义平面。


目录


一、为什么需要自定义着色器

1.1 内置材质的天花板

MeshStandardMaterial:调参数能到"真实"
但你要的效果(工业专属)内置材质做不了:
  ① 扫描脉冲(光带沿设备表面扫过)
  ② 能量流(流动的光线沿管道走)
  ③ 数据可视化专属形态(半透明能量柱、波面)

着色器 = 自己定义"每个像素的颜色怎么算"。从"调参"到"写算法",能力跃升。

1.2 定位:不是图形学专家,但要"够用"

红线第 3 条:会写"够用"的着色器即可——扫描线、发光、流动光效这类工业特效,用 GLSL 基础就能写。深入(PBR 公式、阴影算法)是另一个学习曲线,不在本周。


二、GPU 的流水线:顶点着色器 vs 片元着色器

2.1 两个阶段的职责

顶点着色器(Vertex Shader):
  每个顶点跑一次
  职责:决定顶点在哪(处理 position/矩阵变换)
  输出:varying(传给片元着色器的中间量)

片元着色器(Fragment Shader):
  每个像素跑一次(几十万次/帧!)
  职责:决定这个像素什么颜色(最终输出的 gl_FragColor)

2.2 一个通俗比喻

顶点着色器 = 布置画布上的"锚点"(每个顶点一个位置)
片元着色器 = 给锚点之间的"每一小块颜色"上色(每像素一次)

记忆钩子顶点管"形状",片元管"颜色"。想做形状动画 → 顶点着色器;想做颜色/光效 → 片元着色器。今天的主角是片元(工业特效大多是"给表面做颜色")。


三、GLSL 基础:够用的语法

3.1 核心语法(足够写工业特效)

// 1. 数据类型
float a = 1.0;        // 浮点(注意:必须有小数点!)
vec2 p = vec2(0.0, 1.0);    // 2 维向量
vec3 color = vec3(0.0, 0.8, 1.0);  // 3 维向量(RGB)
vec4 rgba = vec4(1.0, 0.0, 0.0, 0.5);  // 4 维(RGBA)

// 2. 分量访问(xyz / rgb 两套名字)
color.rgb   // 相当于 color.xyz
color.x     // 第一个分量

// 3. 内置数学函数
sin(uTime)              // 正弦(周期动画的万能工具)
mix(a, b, t)            // 线性插值:a 与 b 之间按 t 混合
smoothstep(a, b, x)     // 平滑阶跃:0~1 平滑过渡(做光带)
clamp(x, 0.0, 1.0)      // 夹取范围

3.2 必会的三个函数(工业特效三件套)

函数

作用

工业用途

sin(uTime)

周期往复

脉冲、闪烁、波动

mix(a,b,t)

两色渐变

能量柱渐变、状态色过渡

smoothstep(a,b,x)

平滑过渡

扫描光带、边缘柔化


四、ShaderMaterial 的结构

import * as THREE from "three";

// ShaderMaterial:用字符串写 GLSL,替代内置材质
const material = new THREE.ShaderMaterial({
  uniforms: {
    uTime: { value: 0 },        // 时间(循环里更新)
    uColor: { value: new THREE.Color(0x00c6ff) },  // 主色
  },
  vertexShader: `...`,    // 顶点着色器(GLSL 字符串)
  fragmentShader: `...`,  // 片元着色器(GLSL 字符串)
  transparent: true,      // 按需
});

注意:ShaderMaterial 不参与光照、没有 PBR——它完全由你写的着色器决定。这是"自由"也是"责任"(忘了自己算颜色,物体就是白的)。


五、uniforms:把数据喂给 GPU

5.1 三种传参方式

方式

方向

用在哪

uniforms

CPU → GPU(每帧可更新)

时间、颜色、状态

varying

顶点着色器 → 片元着色器

把顶点的位置/法线传给片元

attribute

几何数据(position 等)

顶点数据(内置)

5.2 更新 uniform(循环里的标准姿势)

// 循环里:每帧更新 uniform(Day 100 时间纪律的着色器版)
material.uniforms.uTime.value = clock.getElapsedTime();   // 时间推进动画

uniform 是"CPU 和 GPU 的通信口":想让动画动,就把时间/状态写进 uniform。今天起你有了"给 GPU 发指令"的能力。


六、第一个自定义材质:发光平面

写一个"会呼吸的发光平面"——把 GLSL 基础全部串起来:

// src/core/three/create-glow-plane.ts
import * as THREE from "three";

/**
 * 自定义着色器材质:工业发光平面(可做地面/墙面光带)
 * - 用 sin(uTime) 做"呼吸"强度变化
 * - 用 mix 混合主色与暗色
 * 这是 ShaderMaterial 的最小可运行示例
 */
export function createGlowPlane(): THREE.Mesh {
  const material = new THREE.ShaderMaterial({
    uniforms: {
      uTime: { value: 0 },
      uColor: { value: new THREE.Color(0x00c6ff) },
    },
    transparent: true,
    depthWrite: false,

    // —— 顶点着色器:形状不动,原样输出 ——
    vertexShader: `
      void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,

    // —— 片元着色器:每个像素算颜色(今天是主角)——
    fragmentShader: `
      uniform float uTime;
      uniform vec3 uColor;
      void main() {
        // 呼吸:0.3 ~ 1.0 之间往复(sin 周期)
        float pulse = 0.35 + 0.65 * (0.5 + 0.5 * sin(uTime * 2.0));
        // 输出:主色 × 呼吸强度,半透明
        gl_FragColor = vec4(uColor * pulse, 0.4);
      }
    `,
  });

  const mesh = new THREE.Mesh(new THREE.PlaneGeometry(6, 6), material);
  mesh.rotation.x = -Math.PI / 2;   // 躺平当"发光地面"
  mesh.position.y = 0.01;
  return mesh;
}

// 循环里更新:
// const glow = createGlowPlane(); scene.add(glow);
// 每帧:glow.material.uniforms.uTime.value = clock.getElapsedTime();

读这个片元着色器pulsesin(uTime) 在 0.35~1.0 间呼吸,uColor * pulse 让发光平面"一亮一暗"——这就是工业"呼吸灯/脉冲"的着色器版。Day 117 会在此基础上做扫描光带等。


七、对照表第 7-9 行:着色器

环节

手写 WebGL

Three.js

渲染循环

rAF + 手动矩阵 + draw

renderer.render(scene, camera)

光照

手写着色器

new DirectionalLight(...)

几何体

手动顶点数组

new BoxGeometry(...)

几何数据

手动 attribute

构造函数内置

纹理

手动纹理单元 + 采样

TextureLoader + map

模型加载

手写 glTF 解析器

GLTFLoader.loadAsync()

粒子

手写粒子缓冲 + 着色器

Points + PointsMaterial

后处理

手写全屏 quad + 多 pass 管线

EffectComposer + Pass

着色器

手写全套(矩阵/光照/纹理/混合全自己管)

ShaderMaterial 替你接好管线,GLSL 仍是你写

结论(最终章):Three.js 把"管线接线"全包了(矩阵、缓冲区、纹理绑定),但着色器里的代码永远是自己的——这就是"手写 WebGL vs Three.js"对照表的终点:你不是零基础手写,但你要会写那个"最核心的一段"


八、常见坑点

坑 1:写着色器没反应(全白/全黑)

排查:① 顶点着色器的 gl_Position 那行在吗(没它物体不显示)② fragmentShader 里 gl_FragColor 赋值了吗 ③ uniform 名与 JS 端一致吗(拼写!)。

坑 2:忘记 projectionMatrix * modelViewMatrix

顶点着色器漏了矩阵变换 → 物体不在正确位置/看不见。修法:这行是顶点着色器的"标准模板",先抄对再改(第六节)。

坑 3:uniform 不更新(动画不动)

原因:改了 JS 端 uniform 但循环里没赋值,或名字不一致。修法:每帧 material.uniforms.uTime.value = ...(第五节)。

坑 4:GLSL 语法错(变量类型/分号)

GLSL 严格:float 必须有小数点(1.0 不是 1)、语句有分号、函数有返回。修法:报错信息里找第几行,逐行查语法

坑 5:ShaderMaterial 物体"不发光/无光照"

原因:ShaderMaterial 不吃光——它全由你的片元着色器决定。修法:记得在片元里自己算颜色(含"假光照")


九、自测挑战

T1 · 发光平面(50 分钟)

完成第六节:发光平面呼吸效果。调 sin(uTime * 2.0) 的系数看快慢——亲手理解"周期=动画节奏"

T2 · GLSL 三件套演练(40 分钟)

在片元里分别用 sin/mix/smoothstep 做三个小效果:闪烁 / 双色渐变 / 平滑光带。每个都是 Day 117 特效的积木

T3 · 逐行读着色器(20 分钟)

把第六节的 vertex/fragment 逐行加注释,讲给自己听(每行干什么)。讲不清楚的地方就是没懂的地方——回去查 GLSL 基础。

T4 · 对照表收尾(20 分钟)

把第 7-9 行补进"手写 WebGL vs Three.js"对照表,通读整张表。这张表是你 3D 能力的"知识地图"——面试前翻它。


十、总结

环节

要点

为什么学

内置材质有天花板,工业特效要自己写

双阶段

顶点管形状 / 片元管颜色——片元是本周主角

GLSL 基础

float/vec3/gl_FragColor + sin/mix/smoothstep 三件套

ShaderMaterial

uniforms + vertexShader + fragmentShader 三部分

uniforms

CPU→GPU 通信口,循环里更新 uTime 驱动动画

对照表

最终章:Three 包管线,GLSL 是你的责任

你写下了第一段 GPU 代码——发光平面在呼吸。明天把这些积木拼成工业特效。


明日预告:Day 117 着色器实战——扫描脉冲(smoothstep 光带)、设备能量发光(uTime 驱动)、顶点动画(形状变形)——用 Day 116 的 GLSL 基础写出真正的工业特效。

评论