首页 / Three.js 入门教程 / GLSL 语言入门:着色器世界的 JavaScript

Three.js 入门教程

GLSL 语言入门:着色器世界的 JavaScript

本教程共 40 篇 · 第 34 篇 · 更新于 2026-08-14 · 约 9 分钟阅读

Three.jsGLSL着色器数据类型内置函数texture2D

本节目标:掌握 GLSL 的数据类型、精度与存储限定符、常用内置函数,理解 gl_Position、gl_FragColor 和 texture2D,能读懂并修改第 33 章那种着色器代码。

第 33 章的例子能跑,但代码里的 vec3、varying、sin(uTime) 都是什么意思?这一章把 GLSL 这门语言讲透。GLSL 全称 OpenGL Shading Language,是写顶点着色器和片元着色器的语言。它长得像 C 语言:强类型、分号结尾、有 main 函数。如果你写过 C,上手很快;只写过 JavaScript,下面这些差异要留意。

标量:float、int、bool

GLSL 只有三种基础标量:

float a = 1.0;   // 浮点数,必须带小数点
int   b = 2;     // 整数
bool  c = true;  // 布尔值

最重要的规则:浮点数必须写成 1.0,写成 1 就是整数。GLSL 是强类型,float 和 int 不能直接运算:

float x = 1.0 * float(2); // 用 float() 显式转换

忘记转换是新手报错的第一来源。

向量:vec2、vec3、vec4

图形编程里,坐标、颜色、方向全是向量。GLSL 提供 2 到 4 维浮点向量:

vec2 a = vec2(1.0, 2.0);
vec3 b = vec3(1.0);         // 所有分量都是 1.0
vec4 c = vec4(a, 3.0, 4.0); // 从 vec2 扩展成 vec4

向量分量可以用三套名字访问:xyz(坐标)、rgb(颜色)、stpq(纹理坐标)。同一个向量,写 .x.r.s 拿到的都是第一个分量,按语义选顺眼的:

vec3 color = vec3(0.2, 0.4, 0.6);
float r = color.r; // 0.2
float x = color.x; // 0.2,同一个分量

还可以重排、复制分量,这叫 swizzle:

vec2 uv2 = color.rg;    // 取前两个分量
vec3 flipped = color.zyx; // 反转顺序

向量之间可以整体运算,这是 GLSL 高效的关键:

vec3 a = vec3(1.0);
vec3 b = a * 2.0; // 每个分量都乘 2,等价于 vec3(2.0)

不用写循环,一个运算符处理所有分量。

矩阵:mat2、mat3、mat4

矩阵用来做变换,3D 里最常用 mat4。第 33 章的 projectionMatrix * modelViewMatrix * vec4(position, 1.0) 就是矩阵乘向量,把顶点从局部坐标一路变换到裁剪坐标。新手阶段不需要手写矩阵,three.js 会把现成的矩阵作为 uniform 传进来,照用即可。 矩阵之后还有两个常用类型。定长数组和结构体:

float values[4];   // 定长数组
vec3 points[10];

struct Light {     // 结构体
  vec3 position;
  vec3 color;
};
Light sun = Light(vec3(0.0, 5.0, 0.0), vec3(1.0, 1.0, 0.9));

vec4 的第四个分量 w 也别忽略。3D 坐标写成 vec4(x, y, z, 1.0) 才能和 4×4 矩阵相乘,1.0 是”这是个点”的标记。变换之后 GPU 还会做透视除法:xyz 同时除以 w,远处的点自然变小。这正是 gl_Position 赋值的完整含义。

采样器:sampler2D

sampler2D 是纹理句柄,配合 texture2D 函数从纹理里取颜色:

uniform sampler2D uTexture;
varying vec2 vUv;

void main() {
  vec4 texColor = texture2D(uTexture, vUv);
  gl_FragColor = texColor;
}

texture2D 返回 vec4:前三个分量是 RGB,第四个是透明度。vUv 范围 0~1,超出范围后的采样行为由纹理的 wrap 设置决定(第 15 章讲过)。

精度限定符

GPU 的浮点精度可以调节,精度越低算得越快、越省电,但误差越大:

  • highp:高精度,顶点着色器 float 的默认值;
  • mediump:中精度。注意 GLSL ES 1.00 规范里片元着色器的 float 没有默认精度,three.js 会自动注入 highp;
  • lowp:低精度,颜色这类宽容数据够用。

片元着色器里做复杂计算(比如距离场)时,mediump 可能不够,需要声明:

precision highp float;
Note

three.js 的 ShaderMaterial 会自动注入合适的精度声明,大多数时候不用自己写。但如果你把着色器代码搬到别处用,缺失精度声明会直接编译失败。

存储限定符

三个存储限定符对应第 33 章的数据流:

限定符谁写谁读典型例子
attribute几何体顶点着色器position、uv、normal
uniformJavaScript两个着色器uTime、uColor
varying顶点着色器片元着色器vUv(自动插值)

自定义 attribute 需要几何体配合。比如给每个粒子单独的大小:

geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
attribute float scale; // 顶点着色器里声明同名 attribute

attribute 这个名字在 GLSL3 里改叫 in,three.js 默认按 GLSL1 语法,attribute 照写。 uniform 的传递方向相反:JS 侧往 ShaderMaterial 的 uniforms 里塞值,着色器侧同名声明。名字必须一致,类型要匹配——JS 里 { value: 1.0 } 对应 GLSL 的 uniform float,{ value: texture } 对应 uniform sampler2D。对不上不会报错,但你会拿到默认值。这是排查”uniform 没生效”的第一站。

内置函数:着色器的武器库

GLSL 内置了几十个数学函数,全部是逐分量运算。最高频的八个:

mix(a, b, t)          // 线性插值:t=0 取 a,t=1 取 b
smoothstep(e0, e1, x) // 平滑过渡,0~1 之间是缓和的 S 曲线
fract(x)              // 取小数部分,做重复图案的利器
clamp(x, 0.0, 1.0)    // 限制范围
step(edge, x)         // x < edge 返回 0,否则返回 1
length(v)             // 向量长度
dot(a, b)             // 点积,判断方向、算光照
normalize(v)          // 归一化,方向不变、长度变 1

配合使用就能画出各种图案。棋盘格:

vec2 grid = fract(vUv * 8.0);   // 每格 0~1
float cell = step(0.5, grid.x) * step(0.5, grid.y); // 0 或 1

圆形:

float d = distance(vUv, vec2(0.5));     // 到中心的距离
float circle = smoothstep(0.4, 0.2, d); // 边缘柔和过渡的圆

sincos 做周期动画,0.5 + 0.5 * sin(uTime) 把 -11 的波形映射到 01,这是最常用的”呼吸”写法。

再补几个常用函数:

pow(x, 2.0)    // x 的平方
abs(x)         // 绝对值
mod(x, y)      // 取余
distance(a, b) // 两点距离
min(a, b)      // 取较小值
max(a, b)      // 取较大值

dot 值得单独说。两个向量点积等于夹角余弦乘长度乘积:方向相同接近 1,垂直接近 0。光照里”法线朝不朝光源”就用它判断,这是所有光照模型的基石。

gl_Position 与 gl_FragColor

两个特殊输出变量,是着色器的”出口”:

  • 顶点着色器必须写 gl_Position:顶点的裁剪空间坐标,GPU 靠它决定画在哪;
  • 片元着色器写 gl_FragColor:像素最终颜色,第四分量是透明度。

不写 gl_Position,顶点无处安放;不写 gl_FragColor,像素默认黑色。这两个名字来自 GLSL1:GLSL ES 3.00 里 gl_FragColor 改为用户声明的 out 变量,gl_Position 仍是内置输出变量。three.js 默认兼容老写法。

一个完整的 uv + texture2D 示例

把本章的知识串起来:用 canvas 生成一张渐变纹理,着色器里采样它,再叠加正弦波纹。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>34 GLSL 语言入门</title>
  <style>body { margin: 0; overflow: hidden; }</style>
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
    }
  }
  </script>
</head>
<body>
  <script type="x-shader/x-vertex" id="vertexShader">
    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  </script>

  <script type="x-shader/x-fragment" id="fragmentShader">
    uniform float uTime;
    uniform sampler2D uTexture;
    varying vec2 vUv;

    void main() {
      // 亮度波纹:沿 y 方向的正弦波,范围 0~1
      float wave = 0.5 + 0.5 * sin(vUv.y * 12.0 + uTime * 3.0);

      // 采样纹理
      vec4 texColor = texture2D(uTexture, vUv);

      // mix 在蓝色和橙色之间按波纹混合
      vec3 color = mix(vec3(0.1, 0.3, 0.8), vec3(0.95, 0.5, 0.1), wave);
      gl_FragColor = vec4(color * texColor.rgb, 1.0);
    }
  </script>

  <script type="module">
    import * as THREE from 'three';

    // canvas 渐变纹理
    const canvas = document.createElement('canvas');
    canvas.width = 256;
    canvas.height = 256;
    const ctx = canvas.getContext('2d');
    const gradient = ctx.createLinearGradient(0, 0, 256, 256);
    gradient.addColorStop(0, '#ffffff');
    gradient.addColorStop(1, '#ffcc00');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 256, 256);
    const texture = new THREE.CanvasTexture(canvas);

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
    camera.position.z = 2;

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(innerWidth, innerHeight);
    document.body.appendChild(renderer.domElement);

    const material = new THREE.ShaderMaterial({
      uniforms: {
        uTime: { value: 0 },
        uTexture: { value: texture }
      },
      vertexShader: document.getElementById('vertexShader').textContent,
      fragmentShader: document.getElementById('fragmentShader').textContent
    });

    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 1.6), material);
    scene.add(mesh);

    renderer.setAnimationLoop(() => {
      material.uniforms.uTime.value += 0.02;
      renderer.render(scene, camera);
    });
  </script>
</body>
</html>

读这个片元着色器的顺序:

  1. wave:用 vUv.y 和 uTime 造一条上下滚动的波纹,范围 0~1;
  2. texColor:texture2D 从纹理里取色;
  3. mix:按 wave 在蓝、橙之间插值;
  4. 最后乘上纹理色,透明度固定 1。
Note

关于 GLSL 版本:three.js 的 ShaderMaterial 默认按 GLSL ES 1.00 语法编译(texture2D、gl_FragColor、attribute 都是 1.00 的写法),底层 WebGL2 由 three.js 自动兼容。想用 GLSL3 的新语法(texture()、out 变量),把材质设置成 glslVersion: THREE.GLSL3,同时全部代码要换成新写法。入门阶段用默认的 1.00 即可,网上大多数着色器教程也是这个写法。

到这里,手写 GLSL 的基础就齐了。第 35 章换个思路:用 JavaScript 对象代替字符串,这就是 three.js 的新着色语言 TSL。