Skip to content

Commit 5a64f45

Browse files
chore: new article written
1 parent 83d3c73 commit 5a64f45

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
---
2+
title: "着色器:仅用 x 和 y 坐标绘制高保真图形"
3+
author: "黄京"
4+
date: "Nov 23, 2025"
5+
description: "着色器编程:用数学公式绘制图形"
6+
latex: true
7+
pdf: true
8+
---
9+
10+
## 探索片段着色器的核心思想,用数学公式代替像素画笔
11+
12+
在计算机图形学中,着色器编程代表了一种革命性的思维方式:它允许开发者通过简洁的数学表达式来定义视觉元素,而非依赖预制的纹理或逐像素绘制。本文将引导您深入理解如何利用片段着色器,仅凭归一化的 x 和 y 坐标,创造出从基础几何到复杂特效的高保真图形。读完本文,您将掌握着色器编程的核心概念,并能亲手编写代码生成各类动态图形。
13+
14+
15+
想象一个场景:复杂的渐变、分形图案或动态光晕效果,这些视觉元素并非来自加载的图片,而是由一个简单的数学函数根据每个像素的 x 和 y 坐标实时计算得出。这引出了一个根本性问题:我们习惯于在画布上绘制形状或加载图像,但如果能通过定义而非绘制来创造图形,会带来怎样的可能性?本文旨在解答这一问题,聚焦于片段着色器的应用,帮助您从传统图形处理转向数学驱动的视觉创作。通过本文的学习,您将能够理解着色器编程的思维模式,并独立实现各种图形效果。
16+
17+
## 基础准备:我们的画布与坐标系
18+
19+
在着色器编程中,片段着色器扮演着核心角色。与顶点着色器处理几何顶点不同,片段着色器负责为每个屏幕像素调用一次,决定其最终颜色。这使其成为图形定义的理想工具。为了通用性,我们引入归一化坐标的概念。通过将像素坐标 `fragCoord.xy` 除以画布尺寸 `iResolution.xy`,我们得到归一化坐标 `uv`,其范围在 [0, 1] 之间。这确保了代码在不同分辨率下的适应性。
20+
21+
例如,以下代码演示了如何计算归一化坐标:
22+
```glsl
23+
vec2 uv = fragCoord.xy / iResolution.xy;
24+
```
25+
在这段代码中,`fragCoord.xy` 代表当前像素的坐标,而 `iResolution.xy` 是画布的宽度和高度。除法操作将坐标映射到 [0, 1] 区间,从而创建一个与分辨率无关的画布。为进一步优化,我们可以将坐标中心调整到画布中点,并修正宽高比以防止图形拉伸。进阶代码可能如下:
26+
```glsl
27+
uv -= 0.5;
28+
uv.x *= iResolution.x / iResolution.y;
29+
```
30+
这里,`uv -= 0.5` 将坐标原点移至画布中心,范围变为 [-0.5, 0.5]。接着,通过乘以宽高比,我们确保画布在 x 和 y 方向上比例一致,避免变形。这些步骤为后续图形定义奠定了坚实基础。
31+
32+
## 核心武器:符号距离函数
33+
34+
符号距离函数是着色器图形定义的核心工具。它本质上是一个数学函数 `f(point)`,返回给定点到目标图形最近边缘的有符号距离。距离的正负值具有明确含义:正值表示点在形状外部,零值表示点在边界上,而负值表示点在形状内部。这类似于一个形状的引力场,负值区域定义了形状本身。
35+
36+
从符号距离函数到可视图形,我们需要借助步进函数。例如,`step(edge, x)` 函数在 x 小于 edge 时返回 0,否则返回 1,而 `smoothstep(edge0, edge1, x)` 提供平滑过渡,有助于抗锯齿。以绘制圆形为例,其符号距离函数公式为 $ \text{length}(uv) - r $,其中 $ r $ 是半径。在代码中,我们可以这样实现:
37+
```glsl
38+
float d = length(uv) - radius;
39+
float circle = step(d, 0.0);
40+
```
41+
在这段代码中,`length(uv)` 计算点到原点的距离,减去半径后得到有符号距离 `d``step(d, 0.0)` 将负值(内部)映射为 1(显示),正值(外部)映射为 0(隐藏),从而生成一个硬边缘圆形。若使用 `smoothstep`,则可以创建软边缘效果:
42+
```glsl
43+
float circle = smoothstep(0.01, 0.0, d);
44+
```
45+
这里,`smoothstep` 在距离接近零时平滑插值,减少锯齿现象。通过直接输出 `d` 作为灰度,我们还可以可视化距离场,直观理解形状的分布。
46+
47+
常见图形的符号距离函数构成了我们的工具箱。例如,圆形的公式为 $ \text{length}(p) - r $,矩形可使用 `box` 函数,直线可通过 `sdSegment` 实现。这些函数允许我们快速构建基础几何,而无需依赖外部资源。
48+
49+
## 融合与组合:图形布尔运算
50+
51+
符号距离函数的强大之处在于其支持布尔运算,从而组合复杂形状。通过数学操作,我们可以实现并集、交集和差集。并集使用 `min(d1, d2)` 函数,取两个距离场中的较小值,融合形状;交集使用 `max(d1, d2)`,仅保留两个形状重叠部分;差集则通过 `max(d1, -d2)` 从第一个形状中减去第二个。
52+
53+
以绘制吃豆人图形为例,我们可以分步实现。首先,定义一个圆形的符号距离函数:
54+
```glsl
55+
float circle_d = length(uv) - radius;
56+
```
57+
接着,定义一个矩形作为嘴巴部分:
58+
```glsl
59+
float mouth_d = sdBox(uv, mouthSize);
60+
```
61+
其中 `sdBox` 是矩形的距离函数。最后,应用差集运算:
62+
```glsl
63+
float pacman_d = max(circle_d, -mouth_d);
64+
float final_shape = step(pacman_d, 0.0);
65+
```
66+
在这段代码中,`max(circle_d, -mouth_d)` 实现了从圆形中减去矩形的效果,因为 `-mouth_d` 将矩形内部变为负值,外部变为正值,结合 `max` 操作后,仅当点在圆形内且不在矩形内时结果为负。`step` 函数将其转换为可视图形。这种方法展示了如何通过简单数学组合出复杂设计。
67+
68+
## 超越几何:着色与质感
69+
70+
一旦定义了几何形状,我们可以通过着色添加颜色和质感。动态颜色可以通过将坐标与颜色通道挂钩实现。例如,使用 `uv.x``uv.y` 驱动 RGB 值,创建线性渐变:
71+
```glsl
72+
vec3 color = vec3(uv.x, uv.y, 0.5);
73+
```
74+
这段代码将 x 坐标映射为红色通道,y 坐标映射为绿色通道,生成一个从左上到右下的渐变。若结合三角函数,如 `sin``cos`,可以创建条纹或波状图案:
75+
```glsl
76+
float pattern = sin(uv.x * 10.0) * cos(uv.y * 10.0);
77+
vec3 color = vec3(pattern);
78+
```
79+
这里,`sin``cos` 函数生成周期性变化,输出波状纹理。
80+
81+
为模拟光照效果,我们引入法线向量和漫反射模型。符号距离函数的梯度近似为法线,可通过 `fwidth` 函数计算:
82+
```glsl
83+
vec3 normal = normalize(vec3(dfdx(d), dfdy(d), 1.0));
84+
```
85+
在这段代码中,`dfdx(d)``dfdy(d)` 计算距离场在 x 和 y 方向的偏导数,构成法线向量的 x 和 y 分量,z 分量设为 1.0 以标准化。接着,定义光源方向 `lightDir`,并应用漫反射模型:
86+
```glsl
87+
float diff = max(dot(normal, lightDir), 0.0);
88+
vec3 color = vec3(diff);
89+
```
90+
`dot(normal, lightDir)` 计算法线与光源的点积,`max` 确保非负,生成亮度变化。这使得一个平面圆形呈现出立体球体效果。进一步结合镜面反射,可以创建金属质感:
91+
```glsl
92+
float spec = pow(max(dot(reflectDir, viewDir), 0.0), 10.0);
93+
vec3 final_color = diff + spec;
94+
```
95+
这里,`reflectDir` 是反射方向,`viewDir` 是视角方向,`pow` 函数增强高光强度。通过这些步骤,寥寥几行代码便能实现逼真的材质效果。
96+
97+
## 进阶魔法:引入噪声与时间
98+
99+
为增加图形的复杂性和动态性,我们可以引入噪声函数和时间变量。噪声函数,如 Simplex 或 Perlin 噪声,通过伪随机扰动打破规则性。例如,用噪声扰动圆形边界创建云朵效果:
100+
```glsl
101+
float noise = snoise(uv * 10.0);
102+
float cloud_d = length(uv) - radius + noise * 0.1;
103+
float cloud = smoothstep(0.0, 0.01, cloud_d);
104+
```
105+
在这段代码中,`snoise` 是噪声函数,输出值用于调整距离场,生成不规则形状。类似地,噪声可用于模拟大理石或木材纹理,通过调制颜色或法线实现。
106+
107+
时间变量 `iTime` 允许图形动态变化。例如,创建一个脉冲发光的球体:
108+
```glsl
109+
float pulse = sin(iTime) * 0.1;
110+
float d = length(uv) - (radius + pulse);
111+
float circle = smoothstep(0.0, 0.01, d);
112+
```
113+
这里,`sin(iTime)` 生成周期性变化,叠加到半径上,使球体大小随时间脉冲。旋转效果可通过修改坐标实现:
114+
```glsl
115+
float angle = iTime;
116+
vec2 rotated_uv = vec2(uv.x * cos(angle) - uv.y * sin(angle), uv.x * sin(angle) + uv.y * cos(angle));
117+
```
118+
这段代码应用旋转矩阵到坐标 `uv`,生成动态旋转图形。这些技术将静态图形转化为生动动画,扩展了创作可能性。
119+
120+
121+
回顾本文,归一化坐标 `uv` 作为万能画笔,符号距离函数作为图形定义语言,结合数学运算和 GLSL 内置函数,构成了着色器编程的核心框架。这种方法优势显著:图形基于数学定义,支持无限分辨率缩放;计算高度并行,契合 GPU 架构;动态和参数化调整简便,激发创意表达。
122+
123+
我们鼓励读者实践这些概念,从修改参数开始,逐步尝试组合不同符号距离函数,最终创造独特图形。资源如 Shadertoy 平台提供丰富示例,可供学习参考。在着色器的世界里,您不再是画家,而是世界的定义者——想象力是唯一的边界。通过持续探索,您将解锁更多视觉魔法,从简单公式中孕育出无限复杂。

0 commit comments

Comments
 (0)