Skip to content

Shader 从入门到精通 - GPU 的艺术语言 ​

1. 什么是 Shader?为什么要学它? ​

在传统的 Web 开发中,我们习惯了用 JavaScript 控制 CPU 来完成计算任务。但当涉及到图形渲染时,CPU 就像一个聪明但孤军奋战的将军,而 GPU 则是拥有成千上万士兵的军团。

Shader 本质上是运行在 GPU 上的"微型程序"。想象一下:

  • CPU 一次处理一个任务,像单线程流水线
  • GPU 可以同时处理成千上万个像素,每个像素都在并行计算

这就是为什么 Shader 能够创造出那些令人惊叹的视觉效果:实时的粒子系统、流体模拟、光线追踪、后期特效等等。

1.1 Shader 的运行机制 ​

用 GLSL(OpenGL Shading Language)编写的 Shader 代码会经历这样的生命周期:

GLSL源代码 → 编译器 → GPU汇编指令 → GPU执行

每一帧(通常是 60fps),GPU 都会执行这些指令:

  • 顶点着色器(Vertex Shader):处理每个顶点的位置、变换
  • 片元着色器(Fragment Shader):为每个像素计算颜色

举个例子:一个 1920×1080 的屏幕有 207万个像素。传统 JS 逐个处理需要几秒,而 GPU 能在 16.6ms 内(60fps)全部计算完成!这就是并行计算的威力。

2. Shader 如何获取数据?深入理解数据流 ​

在 Shader 的世界里,数据的流向 是理解整个渲染管线的关键。就像水管系统,不同类型的"水管"承载着不同的数据。

2.1 四种数据来源详解 ​

数据类型 💡声明方式作用范围数据特性典型应用
uniform 🌐uniform float u_time;全局共享✅ CPU→GPU 单向传递
✅ 所有顶点/像素共用同一个值
❌ 在单次绘制中不可变
时间、鼠标位置、贴图、变换矩阵、光源参数
attribute / in 📍attribute vec3 position; (GLSL 1.0)
in vec3 position; (GLSL 3.0)
仅顶点着色器✅ 来自 VBO(顶点缓冲)
✅ 每个顶点值不同
⚡ GPU 逐顶点读取
顶点位置、法线、UV坐标、顶点颜色、骨骼权重
varying / out 🌈varying vec2 vUv; (旧版)
out vec2 vUv; (新版)
顶点→片元传递✅ 顶点着色器输出
✅ GPU 自动插值
✅ 片元着色器接收
UV插值、法线插值、颜色渐变、深度传递
内置变量 ⚙️无需声明GPU 自动提供✅ 系统级变量
✅ 只读(大部分)
✅ 自动更新
gl_Position, gl_FragCoord, gl_FragColor

2.2 深度解析:uniform - 全局的"广播电台" ​

uniform 就像一个广播电台,CPU 发出一个信号,所有 GPU 单元都接收到相同的值。

使用场景举例:

glsl
// ⏰ 时间动画
uniform float u_time;
void main() {
    float wave = sin(u_time);  // 所有像素同步使用这个时间值
}

// 🖱️ 鼠标交互
uniform vec2 u_mouse;
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    float dist = distance(st, u_mouse);  // 计算到鼠标的距离
}

// 🎨 纹理采样
uniform sampler2D u_texture;
void main() {
    vec4 texColor = texture2D(u_texture, vUv);
}

为什么叫 uniform? 因为它对所有顶点/像素是 统一(uniform) 的!

2.3 深度解析:attribute - 每个顶点的"身份证" ​

attribute 为每个顶点提供独特的数据,就像每个人的身份证号都不同。

数据流动过程:

JavaScript 创建 VBO → 上传顶点数据 → GPU 逐顶点读取 attribute

实际例子:

javascript
// JavaScript 端
const positions = new Float32Array([
    -1.0, -1.0, 0.0,  // 顶点1
     1.0, -1.0, 0.0,  // 顶点2
     0.0,  1.0, 0.0   // 顶点3
]);
glsl
// Shader 端
attribute vec3 position;  // GPU 会为每个顶点自动填充不同的 position 值

void main() {
    gl_Position = vec4(position, 1.0);
    // 顶点1 接收到: (-1.0, -1.0, 0.0)
    // 顶点2 接收到: ( 1.0, -1.0, 0.0)
    // 顶点3 接收到: ( 0.0,  1.0, 0.0)
}

2.4 深度解析:varying - 自动的"魔法插值器" ​

varying 是 GPU 最神奇的功能之一!它能在三角形内部为每个像素自动插值。

工作原理图解:

顶点着色器                 光栅化(插值)              片元着色器
┌─────────┐                                    ┌─────────┐
│ 顶点A: 红色│────┐                            │ 像素1: ?  │
└─────────┘    │                            └─────────┘
               ├──→ GPU自动插值 ────→        ┌─────────┐
┌─────────┐    │                            │ 像素2: ?  │
│ 顶点B: 蓝色│────┘                            └─────────┘
└─────────┘

插值实例:

glsl
// ===== 顶点着色器 =====
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;        // 输出 UV

void main() {
    vUv = uv;            // 传递给片元着色器
    gl_Position = vec4(position, 1.0);
}

// ===== 片元着色器 =====
varying vec2 vUv;        // 接收插值后的 UV

void main() {
    // 假设三角形三个顶点的 UV 是:
    // 顶点A: (0, 0)  顶点B: (1, 0)  顶点C: (0.5, 1)
    // GPU 会自动为三角形内每个像素插值出平滑的 UV 值
    gl_FragColor = vec4(vUv, 0.0, 1.0);  // 显示 UV 渐变
}

为什么需要插值?

  • ✅ 平滑的颜色渐变
  • ✅ 正确的纹理映射
  • ✅ 逐像素光照计算
  • ✅ 深度信息传递

3. 内置变量 - GPU 的"系统级API" ​

GPU 为我们提供了一些开箱即用的内置变量,它们就像操作系统提供的系统调用,让我们能够获取关键的渲染信息。

3.1 gl_FragCoord - 像素的"GPS定位" ​

gl_FragCoord 是片元着色器中最常用的内置变量,它告诉当前像素在屏幕上的精确位置。

结构解析: vec4(x, y, z, w)

分量含义取值范围用途
.x屏幕横坐标(像素)0 ~ 屏幕宽度水平位置计算
.y屏幕纵坐标(像素)0 ~ 屏幕高度垂直位置计算
.z深度值0.0 ~ 1.0深度测试、后期特效
.w齐次坐标 w 分量透视除法后的值透视校正(少用)

🔍 坐标系陷阱警告!

这是很多初学者困惑的点:

Canvas 2D 坐标系 (DOM)          WebGL/Shader 坐标系
┌─────────────────→ x          y
│ (0,0)                        ↑
↓                              │
y                              ───────────────→ x
                               
原点: 左上角                     原点: 左下角 (0,0)
                               (跟数学坐标系一致!)

这是为什么?因为 OpenGL 遵循数学惯例(y 轴向上),而 DOM 遵循屏幕扫描惯例(y 轴向下)。

3.2 场景1:归一化坐标 - 让代码适配所有屏幕 ​

问题: 不同设备屏幕尺寸不同(手机 375px,桌面 1920px),如何让 Shader 效果在所有设备上看起来一致?

解决方案:归一化!

glsl
uniform vec2 u_resolution;  // 从 JS 传入,例如 vec2(1920.0, 1080.0)

void main() {
    // 📐 归一化:将像素坐标转换为 0.0~1.0 范围
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 现在 st 的范围是:
    // 左下角: (0.0, 0.0)
    // 右上角: (1.0, 1.0)
    // 屏幕中心: (0.5, 0.5)
    
    // ⚠️ 如果你想要 (0,0) 在左上角(习惯 DOM 坐标):
    st.y = 1.0 - st.y;
    
    // 🎨 现在可以用归一化坐标做各种效果了
    float circle = distance(st, vec2(0.5));  // 到中心的距离
}

为什么归一化这么重要?

  • ✅ 与屏幕分辨率无关
  • ✅ 便于数学计算(0~1 比 0~1920 更直观)
  • ✅ 方便复用算法和效果

3.3 场景2:基于位置的渐变效果 ​

glsl
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 🌈 方案1:水平渐变(从左到右)
    vec3 color = vec3(st.x);  // 左侧黑(0.0),右侧白(1.0)
    
    // 🌈 方案2:垂直渐变
    color = vec3(st.y);
    
    // 🌈 方案3:对角渐变
    color = vec3(st.x * st.y);
    
    // 🌈 方案4:双色混合
    vec3 colorA = vec3(0.149, 0.141, 0.912);  // 蓝色
    vec3 colorB = vec3(1.000, 0.833, 0.224);  // 黄色
    color = mix(colorA, colorB, st.x);  // 从左到右,蓝→黄
    
    gl_FragColor = vec4(color, 1.0);
}

实际效果:

  • st.x:左边全黑,右边全白,中间平滑过渡
  • st.y:下边全黑,上边全白
  • st.x * st.y:左下角最黑,右上角最白

3.4 场景3:后期处理 - 屏幕空间特效 ​

在游戏和视频特效中,后期处理(Post-processing)是必不可少的。

实例:简单的模糊效果

glsl
uniform sampler2D u_texture;   // 原始渲染结果
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 📏 计算像素大小(用于偏移采样)
    vec2 pixelSize = 1.0 / u_resolution;
    
    // 🔍 9点采样模糊(Box Blur)
    vec4 color = vec4(0.0);
    for(float y = -1.0; y <= 1.0; y += 1.0) {
        for(float x = -1.0; x <= 1.0; x += 1.0) {
            vec2 offset = vec2(x, y) * pixelSize;
            color += texture2D(u_texture, st + offset);
        }
    }
    color /= 9.0;  // 平均
    
    gl_FragColor = color;
}

3.5 场景4:像素化效果(Pixelation) ​

glsl
uniform vec2 u_resolution;
uniform float u_pixelSize;  // 控制像素块大小,例如 8.0

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 🎮 像素化算法:向下取整 → 再还原
    vec2 pixelated = floor(st * u_resolution / u_pixelSize) * u_pixelSize / u_resolution;
    
    // 现在 pixelated 是"阶梯状"的坐标
    vec3 color = somePattern(pixelated);  // 用像素化坐标绘图
    
    gl_FragColor = vec4(color, 1.0);
}

3.6 其他重要内置变量 ​

变量所在着色器类型作用
gl_Position顶点着色器(输出)vec4必须赋值! 顶点的裁剪空间坐标
gl_FragColor片元着色器(输出)vec4必须赋值! 像素的最终颜色(RGBA)
gl_PointSize顶点着色器(输出)float点精灵(GL_POINTS)的大小
gl_FrontFacing片元着色器(输入)bool是否是正面(用于双面材质)

示例:双面材质

glsl
void main() {
    vec3 color;
    if(gl_FrontFacing) {
        color = vec3(1.0, 0.0, 0.0);  // 正面:红色
    } else {
        color = vec3(0.0, 0.0, 1.0);  // 背面:蓝色
    }
    gl_FragColor = vec4(color, 1.0);
}

4. 实战:搭建你的第一个 Shader 开发环境 ​

学习 Shader 最好的方式就是立即动手写代码!这里我推荐使用 glslCanvas —— 一个轻量级的 Shader 练习工具。

4.1 方案一:在线快速体验(推荐新手) ​

你可以直接使用这些在线编辑器,无需安装:

4.2 方案二:本地开发环境(推荐进阶) ​

4.2.1 步骤1:安装 glslCanvas ​

bash
# NPM 安装
npm install glslCanvas

# 或者使用 CDN(更简单)
# 直接在 HTML 中引入即可

4.2.2 步骤2:创建 HTML 文件 ​

创建 index.html:

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>🎨 我的第一个 Shader</title>
    <!-- 引入 glslCanvas 库 -->
  <script type="text/javascript"
            src="https://rawgit.com/patriciogonzalezvivo/glslCanvas/master/dist/GlslCanvas.js">
    </script>
    <style>
        body {
            margin: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: #1a1a2e;
        }
        canvas {
            border: 2px solid #16213e;
            box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
        }
    </style>
</head>
<body>
    <!-- 
        glslCanvas 的魔法配置:
        - class="glslCanvas" 必须保留
        - data-fragment-url 指定 shader 文件路径
        - data-textures 可选,用于加载纹理图片
    -->
    <canvas class="glslCanvas" 
            data-fragment-url="shader.frag" 
            width="800" 
            height="600">
    </canvas>
</body>
</html>

4.2.3 步骤3:创建你的第一个 Shader ​

创建 shader.frag 文件:

glsl
// ========================================
// 🎯 Shader 基础设置
// ========================================

#ifdef GL_ES
precision mediump float;  // 设置浮点精度(移动设备需要)
#endif

// ========================================
// 📥 Uniform 输入(glslCanvas 自动提供)
// ========================================

uniform float u_time;        // 程序运行时间(秒)
uniform vec2 u_resolution;   // 画布分辨率(像素)
uniform vec2 u_mouse;        // 鼠标位置(归一化 0~1)

// ========================================
// 🎨 主函数 - 为每个像素计算颜色
// ========================================

void main() {
    // 1️⃣ 归一化坐标 (0.0 ~ 1.0)
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 2️⃣ 定义两个颜色
    vec3 coolBlue = vec3(0.2, 0.5, 0.9);   // RGB: 冷蓝色
    vec3 hotPink  = vec3(0.9, 0.2, 0.5);   // RGB: 热粉色
    
    // 3️⃣ 创建时间动画:sin 波在 -1~1 之间振荡
    //    abs() 将其映射到 0~1 范围
    float wave = abs(sin(u_time * 0.5));
    
    // 4️⃣ 在两个颜色之间插值
    //    wave=0 时是 coolBlue,wave=1 时是 hotPink
    vec3 animatedColor = mix(coolBlue, hotPink, wave);
    
    // 5️⃣ 添加位置渐变效果
    //    左下角会更暗,右上角会更亮
    animatedColor *= vec3(st.x, st.y, 1.0);
    
    // 6️⃣ 输出最终颜色(RGBA)
    gl_FragColor = vec4(animatedColor, 1.0);
}

4.2.4 步骤4:运行! ​

bash
# 使用任意静态服务器
npx http-server .

# 或者 Python
python -m http.server 8000

# 或者 VSCode 的 Live Server 插件

打开浏览器访问 http://localhost:8000,你会看到:

  • ✨ 颜色在蓝色和粉色之间呼吸式渐变
  • 🎨 左下角暗,右上角亮的空间渐变
  • 🔄 实时动画,永不停歇

4.3 进阶:添加鼠标交互 ​

在 shader.frag 中加入鼠标效果:

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 🖱️ 计算当前像素到鼠标的距离
    float distToMouse = distance(st, u_mouse);
    
    // 🎯 距离越近,颜色越亮
    float brightness = 1.0 - smoothstep(0.0, 0.5, distToMouse);
    
    // 🌈 创建鼠标周围的光晕效果
    vec3 color = vec3(brightness) * vec3(0.5, 0.8, 1.0);
    
    gl_FragColor = vec4(color, 1.0);
}

现在移动鼠标,你会看到一个跟随鼠标的蓝色光晕!

4.4 glslCanvas 自动提供的 Uniform ​

Uniform类型说明示例用途
u_timefloat程序运行秒数动画、波动效果
u_resolutionvec2画布尺寸(宽,高)归一化坐标
u_mousevec2鼠标位置(归一化)交互效果
u_datevec4当前日期时间时钟、日历效果
u_deltafloat帧间隔时间物理模拟

4.5 小练习 ​

尝试修改代码实现这些效果:

  1. 让颜色变化速度加快(提示:修改 u_time * 0.5 的系数)
  2. 改变颜色方案(试试不同的 RGB 值)
  3. 创建一个从中心向外的径向渐变(提示:使用 distance(st, vec2(0.5)))
  4. 让动画反向播放(提示:1.0 - wave)

5. Swizzling - GLSL 的优雅"魔法语法" ​

在传统编程语言中,访问向量分量通常需要用索引:v[0], v[1], v[2]。但 GLSL 提供了一种极其优雅的语法糖 —— Swizzling(混洗)。

5.1 什么是 Swizzling? ​

Swizzling 允许你用 点号 + 字母组合 来:

  • ✅ 访问向量的分量
  • ✅ 重新排列分量顺序
  • ✅ 复制分量
  • ✅ 创建不同维度的向量

5.2 三套等价的命名规则 ​

GLSL 为向量分量提供了三套命名,它们完全等价,只是语义不同:

命名规则用途分量示例
xyzw 🎯位置/方向(空间坐标)x, y, z, wposition.xyz
rgba 🎨颜色r, g, b, acolor.rgb
stpq 🗺️纹理坐标s, t, p, quv.st

它们指向同一块数据! 只是为了代码可读性而存在。

glsl
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);

// 这三个完全等价!
float a = v.x;  // 1.0
float b = v.r;  // 1.0(同一个值)
float c = v.s;  // 1.0(同一个值)

5.3 基础用法:单分量访问 ​

glsl
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);

// 访问单个分量
float x = v.x;  // 1.0
float y = v.y;  // 2.0
float z = v.z;  // 3.0
float w = v.w;  // 4.0

// 颜色语义
vec4 color = vec4(0.5, 0.8, 1.0, 1.0);
float red   = color.r;  // 0.5
float green = color.g;  // 0.8
float blue  = color.b;  // 1.0
float alpha = color.a;  // 1.0

5.4 进阶用法:提取多个分量 ​

glsl
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);

// 提取 2D 向量
vec2 xy = v.xy;      // (1.0, 2.0)
vec2 zw = v.zw;      // (3.0, 4.0)

// 提取 3D 向量
vec3 xyz = v.xyz;    // (1.0, 2.0, 3.0)
vec3 yzw = v.yzw;    // (2.0, 3.0, 4.0)

// 混合命名(不推荐!但语法允许)
vec3 rgb = color.rgb;  // ✅ 清晰的语义
// vec3 bad = color.xyz;  // ❌ 语法正确但语义混乱

5.5 黑魔法:任意重排和复制 ​

这才是 Swizzling 的真正威力!

glsl
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);

// 🔄 反转顺序
vec3 reversed = v.zyx;     // (3.0, 2.0, 1.0)
vec4 fullReverse = v.wzyx; // (4.0, 3.0, 2.0, 1.0)

// 🔁 重新排列
vec4 shuffled = v.yxzw;    // (2.0, 1.0, 3.0, 4.0)
vec2 swapped = v.yx;       // (2.0, 1.0)

// 📋 复制分量
vec2 xx = v.xx;            // (1.0, 1.0) - 复制 x
vec3 xxx = v.xxx;          // (1.0, 1.0, 1.0)
vec4 yyyy = v.yyyy;        // (2.0, 2.0, 2.0, 2.0)

// 🎲 任意组合
vec3 crazy = v.xzx;        // (1.0, 3.0, 1.0)
vec4 wild = v.xyxy;        // (1.0, 2.0, 1.0, 2.0)

5.6 实际应用场景 ​

5.6.1 场景1:颜色通道操作 ​

glsl
vec4 color = vec4(0.8, 0.3, 0.5, 1.0);

// 🔴 提取 RGB,丢弃 Alpha
vec3 rgb = color.rgb;

// 🔄 交换红蓝通道(BGR 效果)
vec3 bgr = color.bgr;

// 🎭 只保留红色通道
vec3 redOnly = vec3(color.r, 0.0, 0.0);  // 传统写法
vec3 redOnly = color.rrr * vec3(1.0, 0.0, 0.0);  // Swizzle 写法

// 💡 灰度化(简单版)
float gray = (color.r + color.g + color.b) / 3.0;
vec3 grayscale = vec3(gray);  // 或者 vec3(gray, gray, gray)

5.6.2 场景2:坐标变换 ​

glsl
vec3 position = vec3(1.0, 2.0, 3.0);

// 📏 只要 XY 平面(丢弃 Z)
vec2 flatPosition = position.xy;

// 🔄 交换 XY(旋转 90° 效果)
vec2 rotated = position.yx;

// 📐 从 2D 升级到 3D(添加 Z=0)
vec2 pos2D = vec2(5.0, 10.0);
vec3 pos3D = vec3(pos2D, 0.0);     // 传统写法
vec3 pos3D = vec3(pos2D.xy, 0.0);  // Swizzle 写法

5.6.3 场景3:UV 操作 ​

glsl
vec2 uv = vec2(0.3, 0.7);

// 🔄 翻转 UV(镜像效果)
vec2 flippedU = vec2(1.0 - uv.x, uv.y);  // 水平翻转
vec2 flippedV = vec2(uv.x, 1.0 - uv.y);  // 垂直翻转

// 或者用 Swizzle 简化
vec2 flipped = vec2(1.0) - uv;  // 完全翻转
vec2 flipped = vec2(1.0) - uv.yx;  // 翻转 + 旋转

5.6.4 场景4:高效的数据重组 ​

glsl
// 🎨 从纹理采样后重组颜色
vec4 texColor = texture2D(u_texture, uv);

// 传统写法:交换红蓝通道
vec4 swapped;
swapped.r = texColor.b;
swapped.g = texColor.g;
swapped.b = texColor.r;
swapped.a = texColor.a;

// Swizzle 写法:一行搞定!
vec4 swapped = texColor.bgra;

// 🎭 创建 Alpha 遮罩
float mask = texColor.a;
vec3 masked = texColor.rgb * mask;

5.7 注意事项 ​

glsl
// ❌ 不能混用不同的命名规则
vec2 bad = v.xr;  // 错误!不能混用 xyzw 和 rgba

// ❌ 不能用于赋值的左侧(某些情况)
v.xy = vec2(1.0);  // ✅ 可以
v.xx = vec2(1.0);  // ❌ 错误!不能赋值给重复分量

// ✅ 正确的赋值
v.x = 1.0;
v.xy = vec2(1.0, 2.0);
v.xyz = vec3(1.0, 2.0, 3.0);

5.8 性能优势 ​

Swizzling 不会产生运行时开销!它是编译时的语法糖,编译器会直接优化成寄存器操作。

glsl
// 这两种写法性能完全一样!
vec3 a = vec3(v.x, v.y, v.z);  // 传统
vec3 b = v.xyz;                // Swizzle(更简洁)

6. GLSL 核心函数库 - 你的数学工具箱 ​

Shader 编程的本质是数学运算。GLSL 为我们提供了丰富的内置函数,掌握它们是写出高质量 Shader 的关键。

6.1 Step 与 Smoothstep - 阶跃与平滑过渡 ​

这两个函数是 Shader 中最常用的函数,用于创建硬边缘和软边缘效果。

6.1.1 step(edge, x) - 硬边缘阶跃函数 ​

功能: 创建一个突变的"开关"效果。

数学定义:

glsl
step(edge, x) = {
    0.0  if x < edge
    1.0  if x >= edge
}

可视化效果:

1.0 ┤         ┌─────────
    │         │
0.5 ┤         │
    │         │
0.0 ┤─────────┘
    └─────────┬─────────> x
           edge

实例1:创建一个圆形

glsl
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    st -= 0.5;  // 移到中心
    
    // 计算到中心的距离
    float dist = length(st);
    
    // 🎯 使用 step 创建硬边缘圆
    float circle = step(dist, 0.3);  
    // dist < 0.3 → 1.0 (白色,圆内)
    // dist >= 0.3 → 0.0 (黑色,圆外)
    
    vec3 color = vec3(circle);
    gl_FragColor = vec4(color, 1.0);
}

实例2:创建条纹图案

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 🎨 使用 mod 创建循环,step 创建阶跃
    float pattern = mod(st.x * 10.0, 1.0);  // 0~1 循环10次
    float stripes = step(0.5, pattern);     // 0.5 处切换
    
    vec3 color = vec3(stripes);  // 黑白条纹
    gl_FragColor = vec4(color, 1.0);
}

向量版本的 step:

glsl
// step 可以对向量的每个分量单独操作!
vec2 edge = vec2(0.1, 0.2);
vec2 val = vec2(0.05, 0.3);
vec2 result = step(edge, val);  // (0.0, 1.0)

// 等价于:
result.x = step(0.1, 0.05);  // 0.0
result.y = step(0.2, 0.3);   // 1.0

6.1.2 smoothstep(edge0, edge1, x) - 平滑过渡(抗锯齿神器) ​

功能: 创建平滑的 S 曲线过渡,天生具有抗锯齿效果。

数学定义: 使用 Hermite 插值(三次多项式)

glsl
t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0)
smoothstep = t * t * (3.0 - 2.0 * t)

可视化效果:

1.0 ┤           ╭─────
    │          ╱
0.5 ┤        ╱
    │      ╱
0.0 ┤─────╯
    └─────┬──┬────────> x
       edge0 edge1

对比 step 和 smoothstep:

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    st -= 0.5;
    float dist = length(st);
    
    // ❌ 硬边缘 - 有明显锯齿
    float hardCircle = step(dist, 0.3);
    
    // ✅ 软边缘 - 平滑抗锯齿
    float softCircle = smoothstep(0.31, 0.29, dist);
    //                             ↑    ↑
    //                          外边界 内边界
    // 注意:edge0 > edge1 会反转效果(外黑内白)
    
    vec3 color = vec3(softCircle);
    gl_FragColor = vec4(color, 1.0);
}

实例3:平滑矩形(带圆角效果)

glsl
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    vec3 color = vec3(0.0);
    
    // 📐 定义矩形的边界范围
    float smoothness = 0.02;  // 控制边缘柔和程度(越小越锐利)
    
    // 左下角边界(0.1 ~ 0.2 范围内平滑过渡)
  vec2 bl = vec2(
        smoothstep(0.1, 0.1 + smoothness, st.x),     
        smoothstep(0.1, 0.1 + smoothness, st.y)
  );
  
    // 右上角边界(0.9 ~ 0.8 范围内平滑过渡)
  vec2 tr = vec2(
        smoothstep(0.1, 0.1 + smoothness, 1.0 - st.x), 
        smoothstep(0.1, 0.1 + smoothness, 1.0 - st.y)
    );
    
    // 🎨 四个边界相乘 = 矩形内部
    float rect = (bl.x * bl.y) * (tr.x * tr.y);
    color = vec3(rect);
    
    gl_FragColor = vec4(color, 1.0);
}

实例4:渐变进度条

glsl
uniform float u_time;
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    // 📊 进度条:随时间从 0 到 1
    float progress = abs(sin(u_time * 0.5));
    
    // 🎨 平滑的进度条效果
    float bar = smoothstep(progress - 0.01, progress + 0.01, st.x);
    
    // 双色效果
    vec3 colorA = vec3(0.2, 0.8, 0.3);  // 绿色(已完成)
    vec3 colorB = vec3(0.1, 0.1, 0.1);  // 灰色(未完成)
    vec3 color = mix(colorA, colorB, bar);
    
    gl_FragColor = vec4(color, 1.0);
}

6.2 何时用 step,何时用 smoothstep? ​

特性stepsmoothstep
边缘硬边缘(像素级锐利)软边缘(抗锯齿)
性能⚡ 极快(1条指令)🐢 稍慢(多条指令)
用途像素艺术、硬边图案、遮罩真实感图形、UI、抗锯齿
锯齿❌ 有明显锯齿✅ 无锯齿
控制单一阈值过渡范围可调

💡 经验法则:

  • 追求性能或像素风格 → 用 step
  • 追求视觉质量 → 用 smoothstep
  • 大部分情况下,smoothstep 的视觉效果远超其微小的性能代价

6.3 常用数学函数速查表 ​

6.3.1 基础数学运算 ​

函数说明公式/用途实例
pow(a, b)幂运算a^bpow(2.0, 3.0) = 8.0
常用于衰减曲线、光照计算
sqrt(a)平方根√asqrt(16.0) = 4.0
计算距离时常用
abs(x)绝对值|x|abs(-5.0) = 5.0
创建对称图案
sign(x)符号函数-1/0/+1sign(-3.5) = -1.0
判断方向

6.3.2 取整与取模 ​

函数说明行为实例
floor(x)向下取整⌊x⌋floor(3.7) = 3.0
floor(-1.2) = -2.0
ceil(x)向上取整⌈x⌉ceil(3.2) = 4.0
ceil(-1.8) = -1.0
fract(x)小数部分x - floor(x)fract(3.7) = 0.7
常用于循环重复图案
mod(x, y)取模(余数)x % ymod(5.5, 2.0) = 1.5
创建周期性效果

💡 fract 和 mod 的黄金组合:

glsl
// 🎨 创建重复的棋盘格图案
void main() {
    // 这意味着整个画布被分成 10×10 的格子;st.x 的整数部分表示该像素在哪一列(0 到 9),st.y 的整数部分表示在哪一行(0 到 9)。
    vec2 st = gl_FragCoord.xy / u_resolution * 10.0;  // 放大10倍
    
    // fract 让坐标在 0~1 范围内循环, 相当于"每个格子里的局部坐标", 通常会用 grid 来做子元素绘制。
    vec2 grid = fract(st);
    
    // 关键:相邻格子在行或列上相差 1,因此它们的行列和会相差 1(奇偶性翻转),所以相邻格子的和在偶/奇之间交替。
    float pattern = step(0.5, mod(floor(st.x) + floor(st.y), 2.0));
    
    gl_FragColor = vec4(vec3(pattern), 1.0);
}

6.3.3 范围控制 ​

函数说明用途实例
clamp(x, min, max)限制范围把 x 约束在 [min, max]clamp(5.0, 0.0, 1.0) = 1.0
防止数值溢出
min(x, y)取最小值x < y ? x : ymin(3.0, 7.0) = 3.0
SDF 合并操作
max(x, y)取最大值x > y ? x : ymax(3.0, 7.0) = 7.0
SDF 交集操作

6.3.4 插值与混合 ​

函数说明公式用途
mix(x, y, a)线性插值x×(1-a) + y×a颜色混合、动画过渡

mix() 深度解析:

glsl
// 🌈 颜色混合
vec3 red = vec3(1.0, 0.0, 0.0);
vec3 blue = vec3(0.0, 0.0, 1.0);

mix(red, blue, 0.0);   // 纯红色
mix(red, blue, 0.5);   // 紫色(中间色)
mix(red, blue, 1.0);   // 纯蓝色

// ⏰ 动画示例
vec3 color = mix(red, blue, abs(sin(u_time)));  // 红蓝呼吸

6.3.5 向量运算(空间几何) ​

函数说明公式典型应用
length(v)向量长度√(x²+y²+z²)计算距离、创建圆形
distance(a, b)两点距离length(b - a)光晕效果、粒子系统
normalize(v)单位化向量v / length(v)方向计算、法线归一化
dot(a, b)点积a·b = |a||b|cosθ角度计算、投影、光照
cross(a, b)叉积(3D)a × b ⊥ (a, b)计算法线、判断方向

📐 dot 点积的5大应用:

glsl
// 1️⃣ 计算角度(通过余弦值)
vec3 a = normalize(vec3(1.0, 0.0, 0.0));
vec3 b = normalize(vec3(0.707, 0.707, 0.0));
float cosTheta = dot(a, b);  // ≈ 0.707(45度)
float angle = acos(cosTheta);  // ≈ π/4

// 2️⃣ 投影(把向量 b 投影到 a 上)
float projLength = dot(b, normalize(a));
vec3 projection = normalize(a) * projLength;

// 3️⃣ 判断前后(用于光照)
vec3 normal = vec3(0.0, 0.0, 1.0);
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float NdotL = dot(normal, lightDir);  // > 0 朝向光源

// 4️⃣ 快速计算距离平方(避免 sqrt)
vec2 v = vec2(3.0, 4.0);
float distSq = dot(v, v);  // 25.0(比 length(v)^2 更快)

// 5️⃣ 反射光线方向(镜面反射基础)
vec3 reflected = reflect(lightDir, normal);
// 内部实现:lightDir - 2.0 * dot(lightDir, normal) * normal

🧭 cross 叉积的3大应用:

glsl
// 1️⃣ 计算三角形法线(最常用!)
vec3 edge1 = B - A;
vec3 edge2 = C - A;
vec3 normal = normalize(cross(edge1, edge2));

// 2️⃣ 判断左右方向(2D判断):在三维空间中,两个向量的叉积是一个新的向量,这个向量垂直于由原两个向量构成的平面。 这个新向量的方向可以用右手定则判断
vec2 a = vec2(1.0, 0.0);
vec2 b = vec2(0.0, 1.0);
float z = cross(vec3(a, 0.0), vec3(b, 0.0)).z;
// z > 0: b 在 a 左侧
// z < 0: b 在 a 右侧

// 3️⃣ 构建正交基(切线空间)
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 tangent = normalize(cross(normal, up));
vec3 bitangent = cross(normal, tangent);
// 4️⃣ 计算平行四边形(三角形)面积
float area = length(cross(edge1, edge2));

6.3.6 光学函数(光照计算专用) ​

函数说明公式应用场景
reflect(I, N)反射I - 2.0×dot(N,I)×N镜面反射、环境映射
refract(I, N, eta)折射斯涅尔定律实现玻璃、水面效果
faceforward(N, I, Nref)法线翻转确保法线朝向视线双面材质

🌟 反射实例:镜面球

glsl
// 计算环境反射
vec3 viewDir = normalize(cameraPos - worldPos);
vec3 normal = normalize(surfaceNormal);
vec3 reflectDir = reflect(-viewDir, normal);

// 采样环境贴图
vec3 envColor = textureCube(u_envMap, reflectDir).rgb;

GLSL 函数可视化参考图

7. 实战案例:在 Shader 中绘制三角形 ​

在 Shader 中画三角形是一个思维转变的过程。传统编程是"画"图形(调用 drawTriangle 函数),而 Shader 是逐像素判断:"这个像素在三角形内吗?"

7.1 核心思路拆解 ​

传统编程思维                Shader 思维
─────────────                ──────────────
1. 定义三角形顶点            1. 建立坐标系
2. 调用绘图API               2. 定义三角形顶点
3. GPU自动填充               3. 对每个像素判断:在三角形内?
                            4. 在内→白色;在外→黑色

7.2 步骤1:建立坐标系 ​

不同的坐标系适合不同的场景。我们需要先把像素坐标转换成数学坐标。

glsl
// ====== 方案 A:标准化坐标 (0~1) ======
vec2 st = gl_FragCoord.xy / u_resolution.xy;
// 范围:(0,0) 左下角 → (1,1) 右上角
// 优点:简单直观
// 缺点:宽屏会拉伸(非正方形屏幕变形)

// ====== 方案 B:中心对称坐标 (-1~1) ======
vec2 st = gl_FragCoord.xy / u_resolution.xy;  // 先归一化
st = st * 2.0 - 1.0;  // 再映射到 -1~1
// 范围:(-1,-1) 左下角 → (1,1) 右上角,中心点 (0,0)
// 优点:以原点为中心,方便旋转和缩放
// 缺点:仍然有拉伸问题

// ====== 方案 C:宽高比校正(推荐) ======
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st = st * 2.0 - 1.0;
float aspect = u_resolution.x / u_resolution.y;  // 例如 1920/1080 ≈ 1.78
st.x *= aspect;  // X 轴拉伸,保持正方形不变形
// 范围:x ∈ [-aspect, aspect],y ∈ [-1, 1]
// 优点:无变形,圆是圆,正方形是正方形
// 这是专业 Shader 的标准做法!

// ====== 方案 D:自定义缩放 ======
vec2 pos = st * 0.5;  // 整体缩小到原来的 0.5 倍
// 灵活调整可视范围

可视化对比:

方案A (0~1)          方案B (-1~1)        方案C (宽高比校正)
┌─────────┐         ┌─────────┐         ┌──────────────┐
│(0,1) (1,1)│         │(-1,1) (1,1)│         │(-1.78,1)  (1.78,1)│
│    ●     │   拉伸   │    ●     │   拉伸   │       ●       │  正常
│(0,0) (1,0)│    →    │(-1,-1)(1,-1)│    →    │(-1.78,-1)(1.78,-1)│
└─────────┘         └─────────┘         └──────────────┘
  宽屏变形              宽屏变形              无变形 ✅

7.3 步骤2:定义三角形 - 如何量化大小? ​

在传统绘图中,你可能直接指定像素坐标。但在 Shader 中,我们需要用数学方法定义三角形。

7.3.1 方法 A:直接指定顶点坐标(自由三角形) ​

适用场景: 不规则三角形、艺术图案

glsl
// 💡 直接定义三个顶点
vec2 v1 = vec2(-0.5, -0.3);  // 左下
vec2 v2 = vec2( 0.5, -0.3);  // 右下
vec2 v3 = vec2( 0.0,  0.5);  // 顶部

// ✅ 优点:灵活,可以画任意形状
// ❌ 缺点:不方便缩放、旋转、平移

改进:参数化定义

glsl
// 🎯 通过底边中心点和尺寸参数化
vec2 center = vec2(0.0, 0.0);
float width = 0.8;   // 底边宽度
float height = 0.6;  // 高度

vec2 v1 = center + vec2(-width/2.0, -height/2.0);
vec2 v2 = center + vec2( width/2.0, -height/2.0);
vec2 v3 = center + vec2( 0.0, height/2.0);

// ✅ 现在可以轻松调整大小和位置了!

7.3.2 方法 B:外接圆半径(等边三角形标准方法) ​

适用场景: 等边三角形、规则图案、极坐标设计

数学原理:

等边三角形的三个顶点在圆上均匀分布
角度间隔 = 360° / 3 = 120° = 2π/3

顶点1:角度 90°   (指向上)
顶点2:角度 210°  (左下)
顶点3:角度 330°  (右下)
glsl
#define PI 3.14159265359
#define TWO_PI 6.28318530718

uniform float u_time;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
  st = st * 2.0 - 1.0;
    float aspect = u_resolution.x / u_resolution.y;
    st.x *= aspect;
    
    // 📐 定义外接圆半径(控制三角形大小)
    float radius = 0.4;
    
    // 🔄 可选:添加旋转动画
    float rotation = u_time * 0.5;  // 旋转角度
    
    // 🎯 计算三个顶点(极坐标转直角坐标)
    vec2 v1 = radius * vec2(
        cos(rotation + PI * 0.5),      // 90°
        sin(rotation + PI * 0.5)
    );
    vec2 v2 = radius * vec2(
        cos(rotation + PI * 0.5 + TWO_PI/3.0),  // 210°
        sin(rotation + PI * 0.5 + TWO_PI/3.0)
    );
    vec2 v3 = radius * vec2(
        cos(rotation + PI * 0.5 + TWO_PI*2.0/3.0), // 330°
        sin(rotation + PI * 0.5 + TWO_PI*2.0/3.0)
    );
    
    // ... 后续判断逻辑
}

优化版:用循环生成 N 边形

glsl
// 🎨 通用正多边形生成器
vec2 getPolygonVertex(int n, int index, float radius, float rotation) {
    float angle = rotation + TWO_PI * float(index) / float(n);
    return radius * vec2(cos(angle), sin(angle));
}

// 使用示例
vec2 v1 = getPolygonVertex(3, 0, 0.4, u_time);  // 三角形
vec2 v2 = getPolygonVertex(3, 1, 0.4, u_time);
vec2 v3 = getPolygonVertex(3, 2, 0.4, u_time);

// 改成 getPolygonVertex(6, ...) 就是六边形!

7.3.3 方法 C:内接圆半径(精确控制间距) ​

适用场景: 平铺(tiling)、蜂巢图案

数学关系:

等边三角形:
内接圆半径 r = 外接圆半径 R / 2
边长 a = r * √3
glsl
// 通过内接圆半径定义
float inRadius = 0.2;  // 内接圆半径
float outRadius = inRadius * 2.0;  // 换算成外接圆半径
// 然后用方法 B 的代码即可

7.3.4 三种方法对比 ​

方法优点缺点适用场景
直接坐标灵活自由难以变换不规则三角形、固定图案
外接圆半径易旋转缩放
代码规整
只适合等边规则图案、动画、N边形
内接圆半径精确控制密度需换算平铺图案、蜂巢结构

7.4 步骤3:判断点是否在三角形内 - 核心算法 ​

现在有了三角形的三个顶点,如何判断当前像素是否在三角形内部?这里有多种数学方法。

7.4.1 方法 A:叉积法(最直观)⭐ ​

核心原理: 如果点在三角形内,则它必定在三条边的同一侧。

数学背景:

2D 叉积的 Z 分量表示:
- 正值:点在边的左侧
- 负值:点在边的右侧
- 零:点在边上

如果点在所有三条边的同一侧 → 点在三角形内

可视化图解:

       v3
       /\
      /  \
     / ●P \    ← P在三角形内
    /______\
   v1      v2

边1(v1→v2): P在左侧 ✓
边2(v2→v3): P在左侧 ✓
边3(v3→v1): P在左侧 ✓
结论:P在三角形内!

完整实现:

glsl
// 🧮 2D叉积辅助函数
float cross2D(vec2 a, vec2 b) {
    return a.x * b.y - a.y * b.x;
}

// 🔺 判断点是否在三角形内(叉积法)
bool isInsideTriangle_Cross(vec2 p, vec2 v1, vec2 v2, vec2 v3) {
    // 计算三条边的叉积
    float c1 = cross2D(v2 - v1, p - v1);  // 边 v1→v2
    float c2 = cross2D(v3 - v2, p - v2);  // 边 v2→v3
    float c3 = cross2D(v1 - v3, p - v3);  // 边 v3→v1
    
    // 检查符号是否一致(都>0 或都<0)
    return (c1 >= 0.0 && c2 >= 0.0 && c3 >= 0.0) ||
           (c1 <= 0.0 && c2 <= 0.0 && c3 <= 0.0);
}

// 🎨 在 main 中使用
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    vec2 v1 = vec2(-0.5, -0.3);
    vec2 v2 = vec2( 0.5, -0.3);
    vec2 v3 = vec2( 0.0,  0.5);
    
    // 判断当前像素是否在三角形内
    bool inside = isInsideTriangle_Cross(st, v1, v2, v3);
    
    // 输出颜色
    vec3 color = inside ? vec3(1.0, 0.5, 0.2) : vec3(0.1);
    gl_FragColor = vec4(color, 1.0);
}

优化版:返回浮点值(便于插值)

glsl
float triangleMask(vec2 p, vec2 v1, vec2 v2, vec2 v3) {
    float c1 = cross2D(v2 - v1, p - v1);
    float c2 = cross2D(v3 - v2, p - v2);
    float c3 = cross2D(v1 - v3, p - v3);
    
    // 所有叉积同号 → 1.0,否则 → 0.0
    float sameSign = step(0.0, c1) * step(0.0, c2) * step(0.0, c3) +
                     step(c1, 0.0) * step(c2, 0.0) * step(c3, 0.0);
    return clamp(sameSign, 0.0, 1.0);
}

7.4.2 方法 B:面积法(精确但慢) ​

核心原理: 如果点 P 在三角形 ABC 内,则:

面积(PAB) + 面积(PBC) + 面积(PCA) = 面积(ABC)
glsl
float triangleArea(vec2 a, vec2 b, vec2 c) {
    return abs(cross2D(b - a, c - a)) * 0.5;
}

bool isInsideTriangle_Area(vec2 p, vec2 v1, vec2 v2, vec2 v3) {
    float areaTotal = triangleArea(v1, v2, v3);
    float area1 = triangleArea(p, v1, v2);
    float area2 = triangleArea(p, v2, v3);
    float area3 = triangleArea(p, v3, v1);
    
    float epsilon = 0.0001;  // 处理浮点误差
    return abs((area1 + area2 + area3) - areaTotal) < epsilon;
}

性能对比:

  • ⚡ 叉积法:3次叉积 + 简单比较
  • 🐢 面积法:4次叉积 + 面积求和 + 浮点误差处理

7.4.3 方法 C:重心坐标法(高级,用于插值) ​

核心原理: 将点 P 表示为三角形顶点的加权和:

P = α·v1 + β·v2 + γ·v3
其中 α + β + γ = 1

若 α,β,γ 都 ≥ 0 → P在三角形内

应用场景: 不仅判断内外,还能得到插值权重(用于纹理映射、颜色渐变)

glsl
vec3 barycentric(vec2 p, vec2 v1, vec2 v2, vec2 v3) {
    vec2 v0 = v2 - v1;
    vec2 v1_offset = v3 - v1;
    vec2 v2_offset = p - v1;
    
    float d00 = dot(v0, v0);
    float d01 = dot(v0, v1_offset);
    float d11 = dot(v1_offset, v1_offset);
    float d20 = dot(v2_offset, v0);
    float d21 = dot(v2_offset, v1_offset);
    
    float denom = d00 * d11 - d01 * d01;
    float beta = (d11 * d20 - d01 * d21) / denom;
    float gamma = (d00 * d21 - d01 * d20) / denom;
    float alpha = 1.0 - beta - gamma;
    
    return vec3(alpha, beta, gamma);
}

// 使用
vec3 weights = barycentric(st, v1, v2, v3);
bool inside = all(greaterThanEqual(weights, vec3(0.0)));

// 🎨 高级用法:基于重心坐标的颜色插值
vec3 color1 = vec3(1.0, 0.0, 0.0);  // 顶点1:红色
vec3 color2 = vec3(0.0, 1.0, 0.0);  // 顶点2:绿色
vec3 color3 = vec3(0.0, 0.0, 1.0);  // 顶点3:蓝色

vec3 interpolatedColor = weights.x * color1 + 
                         weights.y * color2 + 
                         weights.z * color3;

7.4.4 方法 D:极坐标法(高级,用于正多边形) ​

适用场景: 绘制任意正 N 边形(三角形、五角星、六边形等)

核心技巧: 利用极坐标的角度对称性

glsl
#define TWO_PI 6.28318530718

uniform float u_time;
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    st.x *= u_resolution.x / u_resolution.y;
    st = st * 2.0 - 1.0;

    int N = 3;  // 边数:3=三角形, 5=五边形, 6=六边形
    float rotation = u_time * 0.3;  // 旋转动画

    // 🧭 转换到极坐标
    float angle = atan(st.y, st.x);  // 角度 [-π, π]
    if (angle < 0.0) angle += TWO_PI;  // 映射到 [0, 2π]
    angle += rotation;  // 添加旋转
    
    float dist = length(st);  // 距离
    
    // 🔄 角度折叠:将360°分成N份,每份内部对称
    float sectorAngle = TWO_PI / float(N);
    float foldedAngle = mod(angle + sectorAngle * 0.5, sectorAngle) - sectorAngle * 0.5;
    
    // 📏 修正距离(投影到边上)
    float correctedDist = dist * cos(foldedAngle);
    
    // 🎨 生成图形(smoothstep 创建平滑边缘)
    float size = 0.5;  // 控制大小
    float edge = 0.02;  // 边缘宽度
    float shape = 1.0 - smoothstep(size - edge, size + edge, correctedDist);
    
    gl_FragColor = vec4(vec3(shape), 1.0);
}

优势:

  • ✅ 一套代码支持任意正多边形
  • ✅ 天然支持旋转动画
  • ✅ 代码简洁

7.5 三种判断方法对比总结 ​

方法性能精度额外功能推荐场景
叉积法⚡⚡⚡ 快✅ 高无日常使用首选
面积法⚡⚡ 中✅ 高无理论学习
重心坐标⚡ 慢✅ 高插值权重纹理映射、颜色渐变
极坐标法⚡⚡ 中⚠️ 近似正多边形通用规则图案、艺术效果

💡 实际开发建议:

  • 🎯 简单判断 → 叉积法
  • 🎨 需要插值 → 重心坐标法
  • 🔺 正多边形 → 极坐标法
  • 🎮 复杂形状 → SDF 距离场(下一节)

8. SDF (Signed Distance Field) - 现代 Shader 绘图的革命 ​

如果说前面的方法是"判断点是否在图形内",那么 SDF 则是计算点到图形的距离。这个思维转变带来了惊人的能力。

8.1 什么是 SDF? ​

Signed Distance Field (有符号距离场) 是一个函数 d(p),它返回空间中任意点 p 到形状表面的带符号距离:

d(p) < 0  →  点在形状内部(距离为负)
d(p) = 0  →  点在形状表面
d(p) > 0  →  点在形状外部(距离为正)

|d(p)| = 到表面的最短距离

可视化示例:

圆形 SDF 的距离值分布:

       -0.3  -0.2  -0.1  0.0  +0.1  +0.2
       ┌────────────────────────────┐
       │    ●●●●●●●●●●●●●●●●●●     │
       │  ●●               ●●       │
       │ ●      -0.2         ●      │
       │●     -0.1  0.0  +0.1  ●    │
       │●          圆心         ●    │
       │ ●                    ●     │
       │  ●●               ●●       │
       │    ●●●●●●●●●●●●●●●●●      │
       └────────────────────────────┘

8.2 SDF 的三大优势 ​

8.2.1 天然的抗锯齿边缘 ​

glsl
// 传统方法:硬边缘
float circle = step(distance(p, center), radius);  // ❌ 锯齿明显

// SDF 方法:平滑边缘
float d = length(p - center) - radius;  // 计算距离
float circle = smoothstep(0.01, -0.01, d);  // ✅ 完美抗锯齿!

8.2.2 轻松创建轮廓/发光效果 ​

glsl
float sdCircle(vec2 p, float r) {
    return length(p) - r;
}

void main() {
    vec2 st = (gl_FragCoord.xy / u_resolution * 2.0 - 1.0);
    st.x *= u_resolution.x / u_resolution.y;
    
    float d = sdCircle(st, 0.3);
    
    // 🎨 多种视觉效果:
    
    // 1️⃣ 填充(内部白色,外部黑色)
    float fill = step(0.0, -d);
    
    // 2️⃣ 轮廓线(只显示边缘)
    float outline = smoothstep(0.01, 0.0, abs(d) - 0.02);
    
    // 3️⃣ 光晕效果(距离越近越亮)
    float glow = 0.02 / abs(d);
    
    // 4️⃣ 渐变填充(基于距离)
    float gradient = 1.0 - clamp(abs(d) * 2.0, 0.0, 1.0);
    
    vec3 color = vec3(gradient);  // 选择一种效果
    gl_FragColor = vec4(color, 1.0);
}

8.2.3 布尔运算(组合多个形状) ​

这是 SDF 最强大的功能!

glsl
// 🔧 基础 SDF 函数库
float sdCircle(vec2 p, float r) {
    return length(p) - r;
}

float sdBox(vec2 p, vec2 b) {
    vec2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

float sdTriangle(vec2 p, vec2 a, vec2 b, vec2 c) {
    vec2 e0 = b - a, e1 = c - b, e2 = a - c;
    vec2 v0 = p - a, v1 = p - b, v2 = p - c;
    vec2 pq0 = v0 - e0*clamp(dot(v0,e0)/dot(e0,e0), 0.0, 1.0);
    vec2 pq1 = v1 - e1*clamp(dot(v1,e1)/dot(e1,e1), 0.0, 1.0);
    vec2 pq2 = v2 - e2*clamp(dot(v2,e2)/dot(e2,e2), 0.0, 1.0);
    float s = sign(e0.x*e2.y - e0.y*e2.x);
    vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
                     vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
                     vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
    return -sqrt(d.x)*sign(d.y);
}

// 🎭 布尔运算操作符

// 并集(Union):两个形状合并
float opUnion(float d1, float d2) {
    return min(d1, d2);
}

// 交集(Intersection):两个形状重叠部分
float opIntersection(float d1, float d2) {
    return max(d1, d2);
}

// 差集(Subtraction):从 d1 中减去 d2
float opSubtraction(float d1, float d2) {
    return max(d1, -d2);
}

// 🌟 平滑并集(Smooth Union)
float opSmoothUnion(float d1, float d2, float k) {
    float h = clamp(0.5 + 0.5*(d2-d1)/k, 0.0, 1.0);
    return mix(d2, d1, h) - k*h*(1.0-h);
}

// 📐 实际应用示例
void main() {
    vec2 st = (gl_FragCoord.xy / u_resolution * 2.0 - 1.0);
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🔵 圆形
    float circle1 = sdCircle(st - vec2(-0.3, 0.0), 0.2);
    float circle2 = sdCircle(st - vec2( 0.3, 0.0), 0.2);
    
    // 🟦 矩形
    float box = sdBox(st, vec2(0.3, 0.15));
    
    // 🎨 组合操作
    float shape;
    
    // 示例1:两个圆的并集
    shape = opUnion(circle1, circle2);
    
    // 示例2:矩形减去两个圆(挖洞效果)
    // shape = opSubtraction(box, opUnion(circle1, circle2));
    
    // 示例3:平滑融合(液体效果)
    // shape = opSmoothUnion(circle1, circle2, 0.1);
    
    // 渲染
    float mask = smoothstep(0.01, -0.01, shape);
    vec3 color = vec3(mask);
    gl_FragColor = vec4(color, 1.0);
}

8.3 常用 2D SDF 形状库 ​

形状SDF 函数参数说明
圆形length(p) - rr = 半径
矩形见上方 sdBoxb = 半尺寸 (宽/2, 高/2)
线段点到线段的距离a, b = 端点
五角星极坐标 + 角度折叠n = 边数, r = 半径

完整资源: Inigo Quilez 的 SDF 函数库 (业界标准)

8.4 SDF 的高级应用 ​

8.4.1 Ray Marching(光线追踪) ​

SDF 是 Ray Marching 的核心!通过距离信息,可以高效地在 3D 场景中追踪光线。

8.4.2 文字渲染 ​

使用 SDF 纹理可以实现无损缩放的文字(如 Valve 的 SDF 字体技术)。

8.4.3 粒子系统 ​

每个粒子用 SDF 表示,可以实现流体般的融合效果(Metaball)。

8.5 SDF vs 传统方法对比 ​

特性传统判断法SDF 方法
边缘质量❌ 锯齿✅ 天然抗锯齿
轮廓/描边❌ 难实现✅ 轻松
形状组合❌ 复杂✅ 简单(min/max)
性能⚡⚡⚡ 极快⚡⚡ 稍慢(涉及distance计算)
应用场景简单图形复杂场景、艺术效果、3D

结论: SDF 是现代 Shader 艺术和程序化建模的基石!

9. 2D 矩阵变换 - 操控空间的艺术 ​

在传统编程中,我们移动物体(object.x += 10)。但在 Shader 中,思维需要完全逆转:

传统思维:移动物体
Shader思维:移动整个坐标系

9.1 核心概念:坐标空间变换 ​

想象一张写满数字的透明纸:

  • 传统方法:移动纸上的数字
  • Shader方法:数字不动,移动整张纸!
glsl
vec2 st = gl_FragCoord.xy / u_resolution.xy;  // 每个像素知道自己的位置

// 我们的所有变换都是在"扭曲空间"
// 图形本身的定义不变,变的是坐标系

为什么这样做?

  • GPU 是并行的,每个像素独立计算
  • 无法"通知"其他像素"我们要一起移动"
  • 只能让每个像素重新计算自己在新坐标系下的位置

9.2 平移(Translate)- 最简单的变换 ​

平移就是让坐标系整体移动。

9.2.1 数学原理 ​

新坐标 = 旧坐标 + 位移向量
st' = st + offset

⚠️ 注意逆向思维:

坐标系向右移 → 图形看起来向左移
坐标系向上移 → 图形看起来向下移

9.2.2 基础示例:固定位移 ​

glsl
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    st = st * 2.0 - 1.0;
    
    // 🎯 平移:把原点从 (0,0) 移到 (0.3, 0.2)
    st -= vec2(0.3, 0.2);  // 注意是 -= (减法)
    
    // 现在在新坐标系下画圆(圆心在原点)
    float circle = length(st);
    circle = step(circle, 0.2);
    
    // 结果:圆显示在屏幕的 (0.3, 0.2) 位置!
    gl_FragColor = vec4(vec3(circle), 1.0);
}

为什么是 -= 而不是 +=?

目标:让圆向右移 0.3
逻辑:圆的定义是"在原点"
解决:让坐标系向左移 -0.3(st -= 0.3)
结果:圆相对屏幕向右了!

类比:你站在传送带上向左走,相对地面你在向右移动

9.2.3 动画示例:圆周运动 ​

glsl
uniform float u_time;
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🎡 计算圆周运动的位移
    float radius = 0.5;  // 轨道半径
    vec2 offset = vec2(
        cos(u_time) * radius,  // X: -0.5 ~ +0.5
        sin(u_time) * radius   // Y: -0.5 ~ +0.5
    );
    
    // 让坐标系跟着圆周运动
    st -= offset;
    
    // 画一个固定在原点的圆
    float circle = smoothstep(0.11, 0.10, length(st));
    
    // 🎨 添加拖尾效果(可选)
    float trail = 0.02 / length(st);
    vec3 color = vec3(circle) + vec3(0.1, 0.3, 0.5) * trail;
    
    gl_FragColor = vec4(color, 1.0);
}

9.2.4 多物体平移 ​

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🟢 圆1:左侧
    vec2 st1 = st - vec2(-0.4, 0.0);
    float circle1 = smoothstep(0.11, 0.10, length(st1));
    
    // 🔵 圆2:右侧
    vec2 st2 = st - vec2(0.4, 0.0);
    float circle2 = smoothstep(0.11, 0.10, length(st2));
    
    // 🟣 圆3:上方(动画)
    vec2 st3 = st - vec2(0.0, 0.4 * abs(sin(u_time)));
    float circle3 = smoothstep(0.11, 0.10, length(st3));
    
    vec3 color = vec3(0.0);
    color += vec3(0.0, 1.0, 0.0) * circle1;  // 绿色
    color += vec3(0.0, 0.5, 1.0) * circle2;  // 蓝色
    color += vec3(1.0, 0.0, 1.0) * circle3;  // 紫色
    
    gl_FragColor = vec4(color, 1.0);
}

9.3 旋转(Rotation)- 绕原点打转 ​

旋转是最有趣也最容易出错的变换。

9.3.1 数学原理:2D 旋转矩阵 ​

二维旋转矩阵(逆时针旋转 θ 角度):

| cos(θ)  -sin(θ) |   | x |   | x' |
| sin(θ)   cos(θ) | × | y | = | y' |

展开后:
x' = x * cos(θ) - y * sin(θ)
y' = x * sin(θ) + y * cos(θ)

推导来源: 极坐标旋转公式

9.3.2 GLSL 实现 ​

glsl
// 🔧 2D 旋转矩阵函数
mat2 rotate2d(float angle) {
    float c = cos(angle);
    float s = sin(angle);
    return mat2(
        c, s,    // 第一列
       -s, c     // 第二列
    );
    // ⚠️ 注意:GLSL 的矩阵是列主序(column-major)
}

// 使用
vec2 rotated = rotate2d(PI * 0.5) * st;  // 旋转 90°

🎯 注意:矩阵乘法顺序

glsl
st = rotate2d(angle) * st;  // ✅ 正确:矩阵在前
st = st * rotate2d(angle);  // ❌ 错误:结果不对!

9.3.3 完整示例:旋转矩形 ​

glsl
#define PI 3.14159265359

uniform float u_time;
uniform vec2 u_resolution;

mat2 rotate2d(float angle) {
    float c = cos(angle);
    float s = sin(angle);
    return mat2(c, s, -s, c);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🔄 旋转坐标系
st = rotate2d(u_time) * st;
    
    // 📐 画一个矩形(SDF 方法)
    vec2 size = vec2(0.3, 0.1);
    vec2 d = abs(st) - size;
    float rect = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
    float mask = smoothstep(0.01, -0.01, rect);
    
    vec3 color = vec3(mask);
    gl_FragColor = vec4(color, 1.0);
}

9.3.4 关键陷阱:旋转中心 ​

默认旋转中心是原点 (0,0)!如果你想绕其他点旋转:

glsl
// 🎯 绕屏幕中心 (0.5, 0.5) 旋转的完整流程

vec2 st = gl_FragCoord.xy / u_resolution;  // (0,0) ~ (1,1)

// ❌ 错误:直接旋转(绕左下角转)
// st = rotate2d(u_time) * st;

// ✅ 正确:三步走
st -= 0.5;                      // 1. 平移:中心移到原点
st = rotate2d(u_time) * st;     // 2. 旋转:绕原点转
st += 0.5;                      // 3. 还原:移回原位

记忆口诀:移-转-还

  1. 移到原点
  2. 转动
  3. 还原位置

9.3.5 进阶:组合旋转 + 位移 ​

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🎡 先让图形绕屏幕中心公转
    st = rotate2d(u_time * 0.5) * st;
    
    // 🌍 再让图形在自己位置自转
    vec2 localSt = st - vec2(0.5, 0.0);  // 偏移到公转轨道
    localSt = rotate2d(u_time * 2.0) * localSt;  // 自转(更快)
    
    // 画一个小矩形
    float shape = step(abs(localSt.x), 0.1) * step(abs(localSt.y), 0.05);
    
    gl_FragColor = vec4(vec3(shape), 1.0);
}

9.4 缩放(Scale)- 拉伸与压缩 ​

缩放改变坐标系的"单位长度"。

9.4.1 数学原理 ​

缩放矩阵:

| sx   0  |   | x |   | x' |
|  0  sy  | × | y | = | y' |

结果:
x' = x * sx
y' = y * sy

sx > 1: X 方向拉伸  |  sx < 1: X 方向压缩
sy > 1: Y 方向拉伸  |  sy < 1: Y 方向压缩

9.4.2 GLSL 实现 ​

glsl
mat2 scale2d(vec2 s) {
    return mat2(
        s.x, 0.0,
        0.0, s.y
    );
}

// 或者直接用乘法(更简洁)
st *= vec2(2.0, 0.5);  // X放大2倍,Y缩小0.5倍

9.4.3 完整示例:呼吸效果 ​

glsl
uniform float u_time;
uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🫁 呼吸式缩放(范围:0.5 ~ 1.5)
    float scale = 1.0 + 0.5 * sin(u_time);
    
    // 缩放坐标系(图形看起来缩小了)
    st /= scale;  // 等价于 st *= (1.0 / scale)
    
    // 画圆
    float circle = smoothstep(0.31, 0.30, length(st));
    
    gl_FragColor = vec4(vec3(circle), 1.0);
}

⚠️ 注意:缩放的逆向性

glsl
st *= 2.0;     // 坐标系拉伸 2倍 → 图形看起来缩小到原来的 0.5 倍
st /= 2.0;     // 坐标系缩小 0.5倍 → 图形看起来放大到原来的 2 倍

9.4.4 非均匀缩放:创造变形效果 ​

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🎨 动态椭圆效果
    float scaleX = 1.0 + 0.5 * sin(u_time);
    float scaleY = 1.0 + 0.5 * cos(u_time);
    
    st.x /= scaleX;
    st.y /= scaleY;
    
    float circle = smoothstep(0.31, 0.30, length(st));
    
    gl_FragColor = vec4(vec3(circle), 1.0);
    // 圆会在椭圆和反向椭圆之间变化!
}

9.5 组合变换:TRS 顺序 ​

在实际应用中,我们经常需要同时使用平移、旋转、缩放。

9.5.1 变换顺序很重要! ​

glsl
// ❌ 错误顺序(结果出乎意料)
st -= offset;           // 平移
st = rotate2d(angle) * st;  // 旋转
st /= scale;            // 缩放

// ✅ 标准顺序:TRS(Translate → Rotate → Scale)
// 实际代码中是 **逆序** 执行!

st /= scale;            // 3. 最后缩放
st = rotate2d(angle) * st;  // 2. 然后旋转
st -= offset;           // 1. 先平移

为什么逆序? 因为我们在变换坐标系,不是物体。想象逆向播放录像带。

9.5.2 完整TRS示例 ​

glsl
#define PI 3.14159265359

uniform float u_time;
uniform vec2 u_resolution;

mat2 rotate2d(float a) {
    float c = cos(a), s = sin(a);
    return mat2(c, s, -s, c);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution * 2.0 - 1.0;
    st.x *= u_resolution.x / u_resolution.y;
    
    // 🎯 TRS 变换(逆序执行)
    
    // 3️⃣ Scale:呼吸式缩放
    float scale = 1.0 + 0.3 * sin(u_time);
    st /= scale;
    
    // 2️⃣ Rotate:自转
    st = rotate2d(u_time) * st;
    
    // 1️⃣ Translate:绕圈公转
    vec2 offset = vec2(cos(u_time * 0.5), sin(u_time * 0.5)) * 0.5;
    st -= offset;
    
    // 画一个矩形
    vec2 size = vec2(0.2, 0.1);
    float rect = step(abs(st.x), size.x) * step(abs(st.y), size.y);
    
    vec3 color = vec3(rect);
    gl_FragColor = vec4(color, 1.0);
}

10. 总结与下一步 ​

恭喜你完成了 Shader 的入门学习!你现在掌握了:

✅ 基础概念

  • GPU 并行计算原理
  • 数据流:uniform / attribute / varying
  • 内置变量的使用

✅ 核心技能

  • GLSL 函数库(step / smoothstep / dot / cross 等)
  • Swizzling 语法糖
  • 坐标系变换思维

✅ 实战技巧

  • 多种绘制三角形的方法
  • SDF 距离场的强大应用
  • 2D 矩阵变换(平移/旋转/缩放)

10.1 进阶方向 ​

  1. 3D Shader - 学习顶点着色器、光照模型、法线贴图
  2. 后期处理 - 模糊、辉光、色差、景深
  3. 粒子系统 - GPU 粒子、流体模拟
  4. 程序化纹理 - Perlin Noise、Worley Noise
  5. Ray Marching - 3D SDF、体积渲染

10.2 推荐资源 ​

继续探索 Shader 的无限可能吧!✨