从零开始的Shadertoy生活_01

aaaaa Lv2

01 Shadertoy 与 glsl 语言

我们从 Shadertoyglsl 语言 开始谈起。

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 PreviewShader Toy: Show Static GLSL Preview 选项(据说区别是后者不再动态响应代码修改或者键鼠输入,但是实测并没有区别),即可在屏幕右侧或下侧看到运行效果。

glsl语言与C语言

glsl语言是一种C语言的变体,继承了很多C的特性,但是你想要的C的好用功能,glsl几乎都没有。当然,这得从glsl完全脱离内存讲起(这是主线,非常重要)。众所周知,GPU是一堆擅长线性计算的小学生,也就是擅长多次相似的并行计算,而非少量复杂的计算。因此,glsl语言的程序核心,对应C语言里的主函数main,格式如下:

1
2
3
4
5
6
7
8
9
10
11
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;

// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));

// Output to screen
fragColor = vec4(col,1.0);
}

如上代码其实是Shadertoy官网上新建Shader后的示例代码。主函数 mainImage 固定输出 (注意是输出out, 而非返回return) 一个四维数组fragColor表示颜色,读入一个fragCoord表示坐标。“像素着色艺术”这个名字起得名副其实,正如主函数输入像素(坐标),输出颜色,你写代码的部分就是着色的过程,产出的就是艺术(能不能欣赏得来见仁见智)

回到主线,正因为glsl语言不使用内存而只用显存,所以这个主函数会被每个GPU单元、每一帧运行一次,输出的结果会先存在进程的私有寄存器内,然后等待所有线程结束后,统一将每个线程输出的fragColor存入显存里的帧缓冲区的二维数组中,最后统一传给显示器,或者参与下一帧的计算。

因为没有内存,所以glsl语言里没有C语言的类class(只有struct,但是是C语言的类,不支持成员函数,另外不支持结构体数组)、地址操作(指针、取地址、解引用,申请内存,引用传参等)、输入输出操作(input/printf等)、几乎所有隐式转换(包括任意类型向bool的转换、intfloat的转换等),更没有stdio.h/stdlib.h中的绝大多数函数,C++的库更是无需多言。另外,glsl语言还不支持全局变量(const可以,详情见后)、读取前一帧Image内容(Buffer可以,详情见后)和所有非确定运行次数的代码(如while(1)-break结构、递归调用、动态大小数组等)现在知道C语言的方便之处了吧

注:需要注意的是,在不同系统、不同环境中运行glsl语言的代码,可能会产生不同的结果。甚至,连glsl语言本身都有好多套不同的标准,这背后有资本的力量。有些良心系统上支持while(1)-break等结构,但是这并不是符合所有标准的,因此还是要尽可能避免使用。

听起来是不是极其不方便?确实,glsl上手有一定难度。不过当你逐渐熟悉这些限制之后,你会发现这门语言运行效率之高,领略GPU带来的CPU无法取代的并行计算效率,逐渐被这门语言的可视化艺术各路高手各显神通创造的神作所折服。

差点忘记说了,glsl语言也有自己的语法糖,用起来还算方便。 (还没有从C里面删掉的多)

  • 示例代码中的 vec2vec4 就是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); // false
  • glsl语言有原生矩阵 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,禁止转载。