从零开始的Shadertoy生活_01

01 Shadertoy 与 glsl 语言
我们从 Shadertoy 和 glsl 语言 开始谈起。
Shadertoy
Shadertoy 是一个支持编译glsl语言的在线编译器,同时也是一个交流shader(中文翻译:着色器)的黑客社区。这里的学术风气很友好,有不停贡献的萌新,也有热心讲解的大佬。(不过没人来评价我的作品,是太简单了吗)打个广告,欢迎来关注我:aaaaa114514
Shadertoy 社区评判好作品的标准有:画的好看/仿真,短代码配惊艳效果。由于只用GPU而不用CPU内存的特性,glsl语言有极其惊艳的压缩能力,可以将几分钟的视频压缩到几kb(几乎全是代码)。因此,很多系统或游戏的开场界面会使用闲置的GPU来运行Shader,渲染好看的微动画商标。
glsl 语言
配置环境
glsl语言配置环境非常轻松,在这里介绍两种方法:
- 访问 Shadertoy ,点击右上角的“新建”按钮,写完代码后点击代码区左下角三角形按钮即可编译,然后在左侧的预览框中看到运行效果。
- 使用 VSCode。安装插件
Shader Toy (by Adam Stevenson)
和Shader language support for VS Code (by slevesque)
并重启VSCode,然后新建一个后缀名.glsl
的文件即可。需要注意的是,编译时需要右键代码区,点击 Shader Toy: Show GLSL Preview 或 Shader Toy: Show Static GLSL Preview 选项(据说区别是后者不再动态响应代码修改或者键鼠输入,但是实测并没有区别),即可在屏幕右侧或下侧看到运行效果。
glsl语言与C语言
glsl语言是一种C语言的变体,继承了很多C的特性,但是你想要的C的好用功能,glsl几乎都没有。当然,这得从glsl完全脱离内存讲起(这是主线,非常重要)。众所周知,GPU是一堆擅长线性计算的小学生,也就是擅长多次相似的并行计算,而非少量复杂的计算。因此,glsl语言的程序核心,对应C语言里的主函数main
,格式如下:
1 | void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
如上代码其实是Shadertoy官网上新建Shader后的示例代码。主函数 mainImage
固定输出 (注意是输出out
, 而非返回return
) 一个四维数组fragColor
表示颜色,读入一个fragCoord
表示坐标。“像素着色艺术”这个名字起得名副其实,正如主函数输入像素(坐标),输出颜色,你写代码的部分就是着色的过程,产出的就是艺术(能不能欣赏得来见仁见智)。
回到主线,正因为glsl语言不使用内存而只用显存,所以这个主函数会被每个GPU单元、每一帧运行一次,输出的结果会先存在进程的私有寄存器内,然后等待所有线程结束后,统一将每个线程输出的fragColor存入显存里的帧缓冲区的二维数组中,最后统一传给显示器,或者参与下一帧的计算。
因为没有内存,所以glsl语言里没有C语言的类class
(只有struct
,但是是C语言的类,不支持成员函数,另外不支持结构体数组)、地址操作(指针、取地址、解引用,申请内存,引用传参等)、输入输出操作(input
/printf
等)、几乎所有隐式转换(包括任意类型向bool
的转换、int
和float
的转换等),更没有stdio.h
/stdlib.h
中的绝大多数函数,C++的库更是无需多言。另外,glsl语言还不支持全局变量(const
可以,详情见后)、读取前一帧Image内容(Buffer可以,详情见后)和所有非确定运行次数的代码(如while(1)-break
结构、递归调用、动态大小数组等)。现在知道C语言的方便之处了吧
注:需要注意的是,在不同系统、不同环境中运行glsl语言的代码,可能会产生不同的结果。甚至,连glsl语言本身都有好多套不同的标准,这背后有资本的力量。有些良心系统上支持while(1)-break
等结构,但是这并不是符合所有标准的,因此还是要尽可能避免使用。
听起来是不是极其不方便?确实,glsl上手有一定难度。不过当你逐渐熟悉这些限制之后,你会发现这门语言运行效率之高,领略GPU带来的CPU无法取代的并行计算效率,逐渐被这门语言的可视化艺术和各路高手各显神通创造的神作所折服。
差点忘记说了,glsl语言也有自己的语法糖,用起来还算方便。 (还没有从C里面删掉的多)
示例代码中的
vec2
和vec4
就是glsl的原生数组,一共有三种:vec2
/vec3
/vec4
,每种都默认是float数组,还有变体int数组ivec3
,无符号整型数组uvec4
和bool数组bvec2
各三种。这些数组支持比较方便的构造和访问、修改和运算操作,样例如下:1
2
3
4
5
6
7
8
9
10
11
12// 构造
//
// 1. 直接赋值
vec2 uv = vec2(0.5, 0.5);
vec3 color = vec3(0.7, 0.4, 0.2);
// 2. 单值填充
vec3 gray = vec3(0.5); // 等效于 (0.5, 0.5, 0.5)
// 3. 混合构造
vec4 transparentRed = vec4(vec3(1.0, 0.0, 0.0), 0.5); // RGB + Alpha
vec4 rgba = vec4(color, 1.0); // 扩展为 RGBA (等效于vec4(0.7, 0.4, 0.2, 1.0))1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 访问和修改
//
vec4 pos = vec4(1.0, 2.0, 3.0, 4.0);
// 1. 标准下标(类似C数组)
float x = pos[0]; // x = 1.0
// 2. 分量别名(支持 .xyzw / .rgba / .stpq ,但不可混用)
float y = pos.y; // y = 2.0
float a = pos.a; // Alpha = 4.0
float t = pos.t; // 纹理坐标 t = 2.0(与 .y 相同)
// 3. 链式访问(Swizzling)
vec2 xy = pos.xy; // 提取前两维 (1.0, 2.0)
vec3 pos_bgr = pos.bgr; // 反转RGB通道 (3.0, 2.0, 1.0)
vec4 pos_rara = pos.rara; // 允许重复提取 (1.0, 4.0, 1.0, 4.0)
pos.wx = vec2(5.0, 6.0); // 修改 pos = (6.0, 2.0, 3.0, 5.0)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 运算
//
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(0.1, 0.2, 0.3);
// 1. 逐分量运算
vec3 c = a + b; // (1.1, 2.2, 3.3)
vec3 d = a * 2.0; // 标量乘法 (2.0, 4.0, 6.0)
vec3 e = a + 3.0; // 3.0被隐式转换为 vec3(3.0), 运算结果为 (4.0, 5.0, 6.0)
float dotProduct = dot(a, b); // 点积
vec3 crossProduct = cross(a, b); // 叉乘
// 2. 比较运算
bvec3 isGreater = greaterThan(a, b); // (true, true, true)
bool isEqual = (a == b); // falseglsl语言有原生矩阵
mat2
mat3
mat4
(只有方阵):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 定义
//
mat2 m2 = mat2(1.0, 2.0, // 第一 列!!
3.0, 4.0); // 第二 列!!
// 以上代码画出了矩阵:
// [1.0, 3.0]
// [2.0, 4.0]
// 这是符合数学直觉的,但是有点反人类
// n个n维向量填充定义
vec3 col0 = vec3(1.0, 2.0, 3.0); // 第一列
vec3 col1 = vec3(4.0, 5.0, 6.0); // 第二列
vec3 col2 = vec3(7.0, 8.0, 9.0); // 第三列
mat3 m3 = mat3(col0, col1, col2); // 3x3 矩阵
// 也可以向量-标量混合定义
mat2 m4 = mat2(1.0, 2.0, // 第一列 (1.0, 2.0)
vec2(3.0)); // 第二列 (3.0, 0.0)
// 但不可以这样!
vec2 a = vec2(1.0, 2.0);
vec2 b = vec2(3.0, 4.0);
vec2 c = vec2(5.0, 6.0);
vec3 d = vec3(7.0, 8.0, 9.0);
mat3 m = mat3(a, b, c, d); // 编译错误!参数不匹配1
2
3
4
5
6
7// 矩阵与矩阵运算
//
// 矩阵之间的加、减、乘都符合数学规则,数乘同样符合规则
mat2 m = mat2(1.0, 2.0, 3.0, 4.0);
mat2 mInv = inverse(m); // 逆矩阵
float det = determinant(m); // 行列式 = -2.0
mat2 mT = transpose(m); // 矩阵的转置1
2
3
4
5
6// 矩阵与向量乘法
//
mat4 model = mat4(2.0); // 这是左上-右下的主对角线填充2.0的对角矩阵
vec4 pos = vec4(1.0, 0.0, 0.0, 1.0); // vec会被自动认为是列向量
vec4 transformed = pos * model; // 错误!GLSL 默认不支持行向量乘法
vec4 transformed = pos * transpose(model); // 正确:转为列向量乘法
时间不早了,今天就先写到这吧。真希望我能够有能力和时间,把这个系列更新到完结撒花。
欲知后事如何,且听下回分解。
- 标题: 从零开始的Shadertoy生活_01
- 作者: aaaaa
- 创建于 : 2025-06-30 02:30:00
- 更新于 : 2025-06-30 09:53:24
- 链接: https://redefine.ohevan.com/2025/06/30/从零开始的Shadertoy生活_01/
- 版权声明: 版权所有 © aaaaa,禁止转载。