ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

The Book of Shaders 第三章:理解 uniform 变量与 gl_FragCoord 坐标的着色器编程

The Book of Shaders 第三章:理解 uniform 变量与 gl_FragCoord 坐标的着色器编程

【免费下载链接】thebookofshaders

Step-by-step guide through the abstract and complex universe of Fragment Shaders.

项目地址:https://gitcode.com/gh_mirrors/th/thebookofshaders
点击查看免费下载

导读:本文以《The Book of Shaders》第 03 章(仓库 03/README.md 及其波兰语译本 03/README-pl.md)为骨架,系统讲解片段着色器中 CPU 与 GPU 之间的数据通道:uniform变量与内置输入gl_FragCoord。你将掌握u_time、u_resolution、u_mouse三个核心 uniform 的声明方式与使用场景,学会用gl_FragCoord归一化坐标驱动 RGB 颜色,并通过仓库内的 03/time.frag 与 03/space.frag 两个可运行示例完成动手练习。

一、为什么片段着色器需要 uniform

在前一章中我们已经看到 GPU 如何管理数量庞大的并行线程——每个线程负责为最终图像的一小块区域计算颜色。尽管每个并行线程彼此"看不见"对方,我们依然需要一种机制,把来自 CPU 的输入数据发送给所有线程。

由于显卡的硬件架构,这种输入必须满足两个硬性条件:

  1. 对所有线程完全一致(uniform):每个线程拿到的数据必须完全相同;
  2. 严格只读(read-only):线程可以读取这些数据,但绝不能改写它们。

换句话说,每个线程接收同一份数据,只能读、不能改。这份数据就是 GLSL 中的uniform变量。从数据流角度看,可以把 uniform 想象成 CPU 与 GPU 之间的一座座单向小桥——数据只从 CPU 流向 GPU(仓库作者在 03/notes.md 中也强调它是 "one-way bridges")。

二、uniform 的声明:类型、位置与命名规范

uniform变量可以拥有 GLSL 支持的大多数数据类型,包括:

类别类型
标量float
向量vec2、vec3、vec4
矩阵mat2、mat3、mat4
采样器sampler2D、samplerCube

按照惯例,uniform 通常声明在着色器最顶部,紧跟在默认浮点精度设置之后:

#ifdef GL_ES precision mediump float; #endif uniform vec2 u_resolution; // 画布尺寸(宽,高) uniform vec2 u_mouse; // 鼠标位置(以屏幕像素为单位) uniform float u_time; // 着色器加载后经过的秒数

2.1 命名约定:u_前缀

本书系列示例统一传递三个 uniform:u_time(着色器启动以来的秒数)、u_resolution(着色器所绘制的画布/公告板尺寸)、u_mouse(画布内部鼠标位置,以像素为单位)。作者遵循在 uniform 名前加u_前缀的约定,目的是让读者一眼看出这是 uniform 而非普通局部变量。不过这只是一种风格偏好,实际项目中你还会见到各式各样的命名。

例如 ShaderToy 使用同语义的 uniform,但采用i前缀命名:

uniform vec3 iResolution; // 视口分辨率(像素) uniform vec4 iMouse; // 鼠标像素坐标:xy 为当前位置,zw 为点击状态 uniform float iTime; // 着色器播放时间(秒)

注意两个平台的细微差别:iResolution是vec3(额外携带了像素宽高比信息),iMouse是vec4(xy 保存当前位置、zw 保存鼠标按键按下/按住的状态)。这也印证了原文的提示——uniform 的名字在不同实现中千差万别,但语义是一致的。

三、实战一:用 u_time 驱动颜色动画

让我们用 03/time.frag 看看 uniform 的实际效果。这段代码用u_time(着色器运行秒数)配合正弦函数,让画布在红色与黑色之间来回过渡:

#ifdef GL_ES precision mediump float; #endif uniform float u_time; void main() { gl_FragColor = vec4(abs(sin(u_time)),0.0,0.0,1.0); }

核心逻辑只有一行:abs(sin(u_time))将u_time的连续增长值映射到0.0~1.0的周期区间,作为红色通道强度;绿色与蓝色通道恒为0.0,alpha 恒为1.0。于是画布在"全红"与"纯黑"之间平滑脉动。

3.1 GLSL 硬件加速的内置函数

这段代码还引出了 GLSL 的另一个惊喜:GPU 在硬件层面加速实现了大量角度、三角函数与指数函数,无需软件模拟。本书涉及的核心内置函数如下(仓库glossary目录收录了它们的详细说明):

sin()、cos()、tan()、asin()、acos()、atan()、pow()、exp()、log()、sqrt()、abs()、sign()、floor()、ceil()、fract()、mod()、min()、max()、clamp()。

以sin()为例,词条给出了完整的函数重载签名:float sin(float angle)、vec2 sin(vec2 angle)、vec3 sin(vec3 angle)、vec4 sin(vec4 angle),参数以弧度为单位,返回值即角度的三角正弦——因此它也天然支持向量化运算。

3.2 动手练习

在仓库的交互式页面(由 src/main.js 中的GlslEditor加载data="time.frag"实现)中反复调参:

  • 降低频率:把u_time的系数调小(例如sin(u_time * 0.4),见 03/notes.md 中的读者记录),直到颜色变化几乎不可察觉;
  • 提高频率:把系数调大,直到你看到单一颜色且不再闪烁;
  • 多通道调制:为红、绿、蓝三个通道分别配置不同频率的正弦函数,观察叠加出的有趣色彩行为:
gl_FragColor = vec4( abs(sin(u_time * 1.0)), abs(sin(u_time * 2.0)), abs(sin(u_time * 3.0)), 1.0 );

四、gl_FragCoord:默认的输入 varying

GLSL 不仅提供了默认输出vec4 gl_FragColor,还提供了默认输入vec4 gl_FragCoord,它保存当前活动线程正在处理的像素(或称屏幕片段)的屏幕坐标。借助gl_FragCoord,每个线程都能知道自己工作在画布的哪个位置。

这里有一个关键概念区分:

  • uniform:对所有线程取值相同;
  • gl_FragCoord:对每个线程取值不同(每个像素坐标都不同),因此它被称为varying(变化值)。

也正因如此,gl_FragCoord不需要你在着色器中显式声明——它是 GLSL 内置的保留字,由运行时隐式提供(这一点在 03/notes.md 中作者专门提醒读者:"these are reserved names")。

五、实战二:用 gl_FragCoord 归一化坐标驱动颜色

03/space.frag 演示了gl_FragCoord的典型用法——归一化坐标:

#ifdef GL_ES precision mediump float; #endif uniform vec2 u_resolution; uniform vec2 u_mouse; uniform float u_time; void main() { vec2 st = gl_FragCoord.xy/u_resolution; gl_FragColor = vec4(st.x,st.y,0.0,1.0); }

5.1 归一化的原理

gl_FragCoord.xy是像素坐标系下的坐标(例如画布为 500×500 时,取值范围是0.0~500.0)。将它除以画布尺寸u_resolution后,得到的就是0.0~1.0区间的归一化坐标st:

// 左下角 (0,0) → 归一化后 (0.0, 0.0) // 右上角 (500,500) → 归一化后 (1.0, 1.0) vec2 st = gl_FragCoord.xy / u_resolution; // ST 范围: vec2(0.0,0.0) ~ vec2(1.0,1.0)

这正是 GLSL 向量运算的便利之处:vec2除以vec2会逐分量相除,一步同时完成 X 与 Y 两个维度的归一化(03/notes.md 中读者感叹,同样的操作在 Processing 里需要调用vector.divide()之类的方法)。

5.2 把坐标"画"成颜色

归一化之后,坐标映射到颜色通道变得异常直接——把st.x送入红色通道、st.y送入绿色通道:

gl_FragColor = vec4(st.x, st.y, 0.0, 1.0); // RED GREEN BLUE ALPHA

于是屏幕从左到右呈现红色渐强、从下到上呈现绿色渐强,画布左下角为黑色(0,0,0)、右上角为黄色(1,1,0),对角线方向则是红绿等比例混合的过渡色带,效果正如下方示意图:

5.3 用颜色"调试"着色器

在着色器世界里,调试手段非常有限:最常见的做法就是把变量赋成醒目的颜色,再根据看到的图像反推行为。正如原书所言,用 GLSL 编程常常像把船装进瓶子里——同样艰难,同样美丽,同样令人满足。

5.4 理解自测

对照 03/space.frag 的运行结果,检验你的理解:

  • 归一化坐标(0.0, 0.0)对应画布的哪个位置?(答案:左下角,颜色为纯黑)
  • (1.0, 0.0)、(0.0, 1.0)、(0.5, 0.5)、(1.0, 1.0)又分别对应哪里?(依次是:右下角纯红、左上角纯绿、中心暗黄、右上角亮黄)
  • 能否利用u_mouse?注意它的值是以像素为单位的未归一化坐标,需要先除以u_resolution才能与st处于同一量纲。试试用鼠标移动驱动颜色:
    vec2 ms = u_mouse / u_resolution; gl_FragColor = vec4(ms.x, ms.y, 0.0, 1.0);
  • 能否把u_mouse与u_time结合起来,设计一种随鼠标与时间共同变化的颜色图案?

六、uniform 在运行环境中由谁赋值

uniform 的"桥"需要 CPU 端主动放行数据。本书第四章 04/README.md 展示了在浏览器与主流框架中如何为这些 uniform 赋值:

  • 浏览器/glslCanvas:直接用<canvas class="glslCanvas">uniforms = { u_time: { type: "f", value: 1.0 }, u_resolution: { type: "v2", value: new THREE.Vector2() }, u_mouse: { type: "v2", value: new THREE.Vector2() } }; // 渲染循环中 uniforms.u_time.value += clock.getDelta();

    并在onWindowResize中把画布宽高同步给u_resolution、在onmousemove中把鼠标位置同步给u_mouse;

  • Processing:shader.set("u_resolution", float(width), float(height))、shader.set("u_mouse", ...)、shader.set("u_time", millis() / 1000.0);
  • openFrameworks:shader.setUniform1f("u_time", ofGetElapsedTimef())、shader.setUniform2f("u_resolution", ofGetWidth(), ofGetHeight())。

这证实了一个重要事实:uniform 的名字是你自己在 GLSL 中声明的,CPU 端必须按相同名字调用对应的 set 接口才能把数据送进着色器(03/notes.md 中读者提出的疑问正是这一点)。当你在框架中换用不同的 uniform 名时,务必同步修改两端的引用。

七、小结

本章的核心收获可以概括为三条:

  1. uniform 是 CPU→GPU 的只读广播通道,对所有并行线程取值一致,声明位置在着色器顶部、精度设置之后,支持float、vec、mat、sampler等类型;
  2. u_time/u_resolution/u_mouse是本书贯穿始终的三大 uniform,分别代表时间、画布尺寸与鼠标位置,对应 ShaderToy 中的iTime/iResolution/iMouse;
  3. gl_FragCoord是默认的 varying 输入,保存当前像素坐标,除以u_resolution即可归一化到0.0~1.0,从而直接映射为颜色通道——这是所有基于坐标的图案(渐变、分形、程序化纹理)的起点。

掌握 uniform 与坐标归一化之后,就可以进入后续章节,用第三章的u_time、u_mouse与gl_FragCoord组合出形状、噪声与更加复杂的程序化艺术。

【免费下载链接】thebookofshaders

Step-by-step guide through the abstract and complex universe of Fragment Shaders.

项目地址:https://gitcode.com/gh_mirrors/th/thebookofshaders
点击查看免费下载
上一篇:【亲测免费】 太吾绘卷游戏模组(Taiwu_mods) 安装与使用教程
下一篇:Adobe Illustrator批量替换终极指南:用ReplaceItems.jsx提升10倍工作效率

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进