02 GLSL 数据类型与基本语法

前置知识:第 1 章的顶点属性、uniform 与 varying。
本章示例:数据类型可视化,完整源码。

学完本章能做什么

你将能读懂基础 GLSL 声明,使用标量和向量组合颜色,判断哪些运算需要浮点数,并能在 Three.js 中传入自定义 uniform。

先看现象

打开本章示例。画面的红色随横向 UV 变化,绿色随纵向 UV 变化。拖动滑块只改变蓝色通道;勾选复选框交换红绿通道;选择“平均值”后,三个通道被计算成同一个灰度值。这三个控件分别对应 float、bool 和 int uniform。

1. 标量:一个值

类型 示例 用途
float float strength = 0.35; 位置、时间、颜色、比例等连续数值。
int int mode = 1; 模式编号、循环计数等整数。
bool bool enabled = true; 条件开关;值为 true 或 false。

GLSL 对类型比 JavaScript 更严格。下面写法清楚地执行浮点除法:

float halfValue = 1.0 / 2.0;  // 0.5

1 / 2 是整数除法,结果为整数 0;再把它放到需要 float 的位置,还可能引起类型错误。浮点常量明确写小数点,是最容易养成的习惯之一。需要转换时显式写 float(mode) 或 int(value),不要期待自动替你完成所有转换。

2. 向量:成组的值

vec2、vec3、vec4 分别装 2、3、4 个浮点数。

vec2 point = vec2( 0.2, 0.8 );
vec3 color = vec3( 1.0, 0.5, 0.0 );
vec4 rgba = vec4( color, 1.0 );

构造函数可以拼接已有的分量:vec3(point, 0.35) 把一个 vec2 和一个 float 合为 vec3;vec3(0.5) 则让三个分量都等于 0.5。构造时需要提供足够的分量,例如 vec3(0.2, 0.8) 不完整。

每个向量分量有几组常用名字:x/y/z/w 常用于位置,r/g/b/a 常用于颜色,s/t/p/q 常用于纹理坐标。它们只是同一组分量的不同叫法。

float red = color.r;     // 等同于 color.x
vec2 xy = color.xy;      // 取前两个分量
vec3 swapped = color.grb; // 交换红绿,保留蓝色

取多个分量叫 swizzle。读 color.grb 会产生一个新的 vec3;它不会自己改变原来的 color。本章示例通过 color = color.grb; 把新向量赋回去。

向量可以与同维度向量逐分量运算,也可以乘标量。例如 color * 0.5 会把三个通道同时减半。矩阵与向量相乘则是另一种运算,不等于逐分量相乘。

3. 矩阵与纹理采样器

mat3、mat4 是矩阵类型。第 1 章的 projectionMatrix 是 mat4,因此常与 vec4 相乘;normalMatrix 是 mat3,常与 vec3 法线相乘。乘法顺序有意义:

vec4 clip = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

sampler2D 表示二维纹理的采样器,稍后在纹理章节详细使用。它不是一张可以用普通加法直接处理的图片;要先用纹理坐标采样,得到颜色值。

4. attribute、uniform、varying 各在哪里变化

声明 谁提供或写入 变化频率 可以在哪里读
attribute 几何体的顶点数据 不同顶点可以不同 顶点着色器
uniform Three.js 或应用代码 一次绘制中的所有顶点、片元共享 声明它的 shader 阶段
varying 顶点着色器写入,光栅化器插值 三角形内部随位置变化 顶点和片元着色器均需声明

本章示例在 JavaScript 中定义:

const material = new THREE.ShaderMaterial( {
  uniforms: {
    uBlue: { value: 0.35 },
    uSwap: { value: false },
    uMode: { value: 0 }
  },
  vertexShader: vertexShaderSource,
  fragmentShader: fragmentShaderSource
} );

上面只展示 uniform 的结构;完整源码给出了真实的两段 shader 字符串。片元着色器内的声明必须与传入值的用途对应:

uniform float uBlue;
uniform bool uSwap;
uniform int uMode;
varying vec2 vUv;

控件改变时,JavaScript 更新的是 material.uniforms.uBlue.value 等值。无需每次重新创建材质。

5. 从 UV 到颜色:逐行读示例

顶点着色器先将几何体自带的 uv 赋给 vUv,片元着色器再执行:

vec2 coords = vUv;
vec3 color = vec3( coords, uBlue );

if ( uSwap ) {
  color = color.grb;
}

if ( uMode == 1 ) {
  float average = ( color.r + color.g + color.b ) / 3.0;
  color = vec3( average );
}

gl_FragColor = vec4( color, 1.0 );
#include <colorspace_fragment>

coords 是二维向量,color 是三维向量,输出颜色是四维向量。第四个分量 1.0 是完全不透明。#include <colorspace_fragment> 是 Three.js 的 shader 片段,用于把线性颜色转换到渲染器输出的色彩空间;它不是 GLSL 自带函数。

6. 函数、作用域与精度

每段 shader 都从 void main() 进入。变量写在 { ... } 块内时,只在该块及其子块内可用。可以自定义函数并复用,例如:

float average3( vec3 value ) {
  return ( value.r + value.g + value.b ) / 3.0;
}

GLSL 有 highp、mediump、lowp 等精度限定。Three.js 的 ShaderMaterial 会注入精度声明;本教程的基础例子先使用默认配置。到 noise 章节再讨论精度为何可能影响不同设备上的结果。

常见问题

本章小结

GLSL 的类型和向量维度必须明确;attribute、uniform、varying 说明数据由谁提供、在哪个阶段变化。下一章会把这些数值放进不同坐标空间,解释为什么同一个顶点可以有几组不同的位置。

本章练习