【免费下载链接】thebookofshaders
Step-by-step guide through the abstract and complex universe of Fragment Shaders.
导读
本章是《The Book of Shaders》(仓库根目录)的第八课,主题为2D Matrices(二维矩阵)。在上一章用坐标系统绘制出形状之后,本章解决"如何让形状动起来"的问题:通过矩阵这一强大的向量操作工具,自由地对图形进行平移(Translate)、旋转(Rotate)与缩放(Scale)。由于该技术可以等量应用于任何用向量表达的事物,学完本章后,你不仅能操控几何图形,还能用同样的手段处理颜色空间(如 YUV),为后续的程序化图案创作打下扎实基础。
本章核心思想:矩阵是操作向量的万能工具
本章开篇(08/SUMMARY-pt.md)给出了全章提纲挈领的论断:
矩阵是操作向量的强大工具。精通矩阵用法后,你可以自由地平移、缩放和旋转形状。由于这套技术可以等量地应用于任何用向量表达的事物,本书后面还会看到矩阵更多进阶用法。矩阵乍看之下很复杂,但一旦掌握概念就会发现它非常实用。让我们在这里通过一些简单示例练习并掌握基础。
这段概括蕴含三个要点:
- 矩阵的本质是"向量变换器":在片元着色器中,坐标、颜色、法线都是向量,矩阵乘法是改变向量取值最规范的手段;
- 操作对象是坐标系而非形状本身:Shader 里没有"选中某个物体"的概念,所有变换都是对每个片元的坐标空间进行整体操作;
- 学的是可迁移的方法论:同样的
mat2/mat3/mat4技巧,后面章节会在噪声、纹理、光照等场景反复出现。
前置认知:变换形状 = 变换坐标系
在上一章(07 形状)中,我们学会了用smoothstep在归一化坐标st(即gl_FragCoord.xy / u_resolution.xy,取值范围0.0 ~ 1.0)上"裁出"矩形、圆形等形状。移动这些形状的诀窍,不是修改形状的定义,而是移动坐标系统本身——只需向保存每个片元位置的变量st加上一个向量,整个空间坐标系就会随之移动。
这种"动空间而不是动物体"的思想贯穿本章全部三个操作:平移是加向量,旋转和缩放则是左乘一个矩阵。
平移(Translate):给 st 加一个向量
平移是最直观的变换。参考仓库中的 cross-translate.frag(作者 @patriciogv,2015):
// Author @patriciogv ( patriciogonzalezvivo.com ) - 2015 #ifdef GL_ES precision mediump float; #endif uniform vec2 u_resolution; uniform float u_time; float box(in vec2 _st, in vec2 _size){ _size = vec2(0.5) - _size*0.5; vec2 uv = smoothstep(_size, _size+vec2(0.001), _st); uv *= smoothstep(_size, _size+vec2(0.001), vec2(1.0)-_st); return uv.x*uv.y; } float cross(in vec2 _st, float _size){ return box(_st, vec2(_size,_size/4.)) + box(_st, vec2(_size/4.,_size)); } void main(){ vec2 st = gl_FragCoord.xy/u_resolution.xy; vec3 color = vec3(0.0); // To move the cross we move the space vec2 translate = vec2(cos(u_time),sin(u_time)); st += translate*0.35; // Show the coordinates of the space on the background // color = vec3(st.x,st.y,0.0); // Add the shape on the foreground color += vec3(cross(st,0.25)); gl_FragColor = vec4(color,1.0); }代码要点:
box()与cross()是上一章的形状函数:cross由两个矩形拼成十字;- 关键一行:
st += translate * 0.35;,其中translate = vec2(cos(u_time), sin(u_time))是一个绕单位圆运动的向量,乘以0.35把运动幅度限制在画布中央附近; - 练习一:取消第 35 行
color = vec3(st.x,st.y,0.0);的注释,即可看到空间本身的坐标颜色随十字一起移动,直观验证"移动的是空间"。
本章给出的进阶练习很有价值:结合u_time与第 5 章的塑造函数(shaping functions)(参见 05 章),让十字按你感兴趣的运动品质移动——例如海浪的往复、单摆的摆动、弹跳的球、加速的汽车或刹车的自行车。建议先从"真实世界"录制一段参考运动,再反推数学表达,这是训练"运动感"的经典方法。
旋转(Rotate):2D 旋转矩阵
旋转同样需要移动整个空间系统,但手段升级为矩阵:矩阵是按行列组织的一组数字,向量按照精确的规则与矩阵相乘,从而以特定方式修改向量取值。
GLSL 中的矩阵类型
GLSL 原生支持二、三、四维矩阵:
mat2(2×2)、mat3(3×3)、mat4(4×4)——浮点矩阵类型;- 支持矩阵乘法运算符
*; - 内置矩阵专用函数
matrixCompMult(),对两个矩阵做逐分量相乘。
仓库 glossary/mat2/README.md 给出了mat2的完整声明方式,值得细读:
mat2 aMat2 = mat2(1.0, 0.0, // 第 1 列 0.0, 1.0); // 第 2 列 mat2 bMat2 = mat2(1.0); // 只给一个值,填充主对角线 mat2 cMat2 = mat2(aVec2, bVec2); // 用向量组合 mat2 dMat2 = mat2(aVec3, aFloat); // 向量与标量混合关键细节:GLSL 矩阵按"列主序"(column-major)初始化,即参数按列填写;同时支持分量访问与按列访问:
mat2 aMat2; aMat2[1][1] = 1.0; float aFloat = aMat2[1][1]; aMat2[0] = vec2(1.0); vec2 aVec2 = aMat2[0];mat3、mat4的声明规则相同(见 glossary/mat3/README.md);matrixCompMult()则逐分量相乘result[i][j] = x[i][j] * y[i][j](见 glossary/matrixCompMult/README.md)。
构造 2D 旋转矩阵
基于矩阵行为可以构造出特定功能的矩阵。二维旋转矩阵的标准公式如下:
mat2 rotate2d(float _angle){ return mat2(cos(_angle),-sin(_angle), sin(_angle),cos(_angle)); }按照列主序展开,这个矩阵的第一列是(cos, sin),第二列是(-sin, cos),对应二维向量绕原点vec2(0.0)逆时针旋转_angle弧度的标准公式。
关键套路:移到原点 → 旋转 → 移回去
但直接套用上面的公式并不满足我们的需求:本章的十字形状画在画布中心,即位置vec2(0.5)附近,而rotate2d绕原点vec2(0.0)旋转。因此旋转空间之前,必须先把形状从center移到vec2(0.0)坐标,旋转空间,最后再移回原位。
对应代码见 cross-rotate.frag:
// Author @patriciogv ( patriciogonzalezvivo.com ) - 2015 #ifdef GL_ES precision mediump float; #endif #define PI 3.14159265359 uniform vec2 u_resolution; uniform float u_time; mat2 rotate2d(float _angle){ return mat2(cos(_angle),-sin(_angle), sin(_angle),cos(_angle)); } float box(in vec2 _st, in vec2 _size){ _size = vec2(0.5) - _size*0.5; vec2 uv = smoothstep(_size, _size+vec2(0.001), _st); uv *= smoothstep(_size, _size+vec2(0.001), vec2(1.0)-_st); return uv.x*uv.y; } float cross(in vec2 _st, float _size){ return box(_st, vec2(_size,_size/4.)) + box(_st, vec2(_size/4.,_size)); } void main(){ vec2 st = gl_FragCoord.xy/u_resolution.xy; vec3 color = vec3(0.0); // move space from the center to the vec2(0.0) st -= vec2(0.5); // rotate the space st = rotate2d( sin(u_time)*PI ) * st; // move it back to the original place st += vec2(0.5); // Show the coordinates of the space on the background // color = vec3(st.x,st.y,0.0); // Add the shape on the foreground color += vec3(cross(st,0.4)); gl_FragColor = vec4(color,1.0); }代码中的三段式注释直接点明了套路:
- 第 38 行
st -= vec2(0.5);—— 把空间从中心移到原点; - 第 40 行
st = rotate2d( sin(u_time)*PI ) * st;—— 用随时间变化的角度(sin(u_time)*PI,范围-π ~ π,做往复摆动)旋转空间; - 第 42 行
st += vec2(0.5);—— 旋转完成后移回原位。
本章给出的旋转练习同样值得亲手验证:
- 取消第 45 行背景坐标代码的注释,观察空间坐标如何随旋转改变;
- 注释掉第 37、39 行的平移(即旋转前后的两次位移),观察后果——会发现十字绕着左下角原点旋转,而不是绕画布中心,从而深刻理解"原点居中"的必要性;
- 用旋转改进你在平移练习中模拟的动画(例如让摆动中的物体同时自转)。
缩放(Scale):2D 缩放矩阵
如果你用过 3D 建模软件或 Processing 的pushMatrix/popMatrix,就知道矩阵同样可以用来改变物体大小。仿照旋转矩阵的推导思路,二维缩放矩阵为:
mat2 scale(vec2 _scale){ return mat2(_scale.x,0.0, 0.0,_scale.y); }对角线上的两个因子分别控制 x、y 方向的缩放倍数(大于 1 放大,小于 1 缩小,负值可做镜像)。
完整示例见 cross-scale.frag:
// Author @patriciogv ( patriciogonzalezvivo.com ) - 2015 #ifdef GL_ES precision mediump float; #endif #define PI 3.14159265359 uniform vec2 u_resolution; uniform float u_time; mat2 scale(vec2 _scale){ return mat2(_scale.x,0.0, 0.0,_scale.y); } float box(in vec2 _st, in vec2 _size){ _size = vec2(0.5) - _size*0.5; vec2 uv = smoothstep(_size, _size+vec2(0.001), _st); uv *= smoothstep(_size, _size+vec2(0.001), vec2(1.0)-_st); return uv.x*uv.y; } float cross(in vec2 _st, float _size){ return box(_st, vec2(_size,_size/4.)) + box(_st, vec2(_size/4.,_size)); } void main(){ vec2 st = gl_FragCoord.xy/u_resolution.xy; vec3 color = vec3(0.0); st -= vec2(0.5); st = scale( vec2(sin(u_time)+1.0) ) * st; st += vec2(0.5); // Show the coordinates of the space on the background // color = vec3(st.x,st.y,0.0); // Add the shape on the foreground color += vec3(cross(st,0.2)); gl_FragColor = vec4(color,1.0); }注意第 38 行scale( vec2(sin(u_time)+1.0) ):sin(u_time)范围-1.0 ~ 1.0,加1.0后缩放因子范围是0.0 ~ 2.0,十字在"缩到看不见"与"放大两倍"之间脉动。同样沿用了"移到原点 → 缩放 → 移回"的三段式。
本章的缩放练习给出了几个极具工程价值的训练:
- 取消第 42 行注释,查看空间坐标被缩放的效果;
- 注释第 37、39 行的平移,观察缩放中心从画布中心漂移到原点;
- 尝试把旋转矩阵与缩放矩阵组合——牢记矩阵乘法顺序敏感:先构造矩阵相乘,再用结果矩阵乘向量(即
M_combined * st,其中M_combined = rotate2d(...) * scale(...)); - 综合运用:既然已经会画形状,又会移动、旋转、缩放,就可以做真正的"合成"了——设计并构造一个假的 UI 或 HUD(抬头显示)界面,参考 ShaderToy 社区中 Ndel 的 HUD 类示例作为灵感。
矩阵不止用于几何:YUV 颜色空间的矩阵变换
本章最精彩的收尾是把矩阵从"几何空间"迁移到"颜色空间"。YUV是用于照片和视频模拟编码的一种颜色空间,它考虑到人类视觉感知的特性,以降低色度(chrominance)分量的带宽。GLSL 中可以直接用矩阵把颜色从一个空间变换到另一个空间,见 yuv.frag:
// Author @patriciogv - 2015 // http://patriciogonzalezvivo.com #ifdef GL_ES precision mediump float; #endif uniform vec2 u_resolution; uniform float u_time; // YUV to RGB matrix mat3 yuv2rgb = mat3(1.0, 0.0, 1.13983, 1.0, -0.39465, -0.58060, 1.0, 2.03211, 0.0); // RGB to YUV matrix mat3 rgb2yuv = mat3(0.2126, 0.7152, 0.0722, -0.09991, -0.33609, 0.43600, 0.615, -0.5586, -0.05639); void main(){ vec2 st = gl_FragCoord.xy/u_resolution; vec3 color = vec3(0.0); // UV values goes from -1 to 1 // So we need to remap st (0.0 to 1.0) st -= 0.5; // becomes -0.5 to 0.5 st *= 2.0; // becomes -1.0 to 1.0 // we pass st as the y & z values of // a three dimensional vector to be // properly multiply by a 3x3 matrix color = yuv2rgb * vec3(0.5, st.x, st.y); gl_FragColor = vec4(color,1.0); }这段代码的示范意义在于:把颜色当作向量,与矩阵相乘,从而"搬动"颜色分量的取值:
rgb2yuv:RGB → YUV 的 3×3 转换矩阵,第一行是亮度系数(0.2126、0.7152、0.0722即 Rec.601 标准亮度权重);yuv2rgb:其逆变换矩阵;- 由于 UV 分量范围是
-1 ~ 1,而st范围是0 ~ 1,需要先st -= 0.5再st *= 2.0完成重映射; - 最终以
vec3(0.5, st.x, st.y)作为 YUV 输入(Y 固定为 0.5 中灰),左乘yuv2rgb得到 RGB 彩色输出,画布上会呈现与坐标位置对应的色相渐变。
仓库源码级深化:从 2D 到 3D/4D 的矩阵工具箱
本章仓库内还有两份源码可以帮你把视野从 2D 拓展到更高维度:
- 章节头部的动态演示 matrix.frag:这是章节页面顶部 canvas 播放的"矩阵雨"效果——用
floor(st*rows)切出 80 行网格,借助random()与u_time让字符按行坠落,本质上是"把空间重复切分 + 随机函数"的预演,为第 9 章程序化图案埋下伏笔; - 进阶矩阵库 matrices.frag:完整给出了
rotate2D以及三维、四维版本——rotateX3D/rotateY3D/rotateZ3D、对应的mat4四维版本,以及scale4D、translate4D。例如绕 X 轴旋转的 3×3 矩阵:
mat3 rotateX3D(float phi){ return mat3( vec3(1.,0.,0.), vec3(0.,cos(phi),-sin(phi)), vec3(0.,sin(phi),cos(phi))); }这份文件展示了矩阵变换的可组合、可复用特性:旋转、缩放、平移都是"把单位矩阵相应位置替换为变换参数",并且低维公式可以一致地推广到高维,这正是后续章节做复杂空间操作(如噪声扰动、纹理坐标变换)的基础工具。
另外值得留意的是 GLSL 的矩阵乘法语义:matrixCompMult()与*完全不同——前者逐分量相乘(对应数学上的 Hadamard 积),后者才是线性代数意义上的矩阵乘法。做几何变换时必须使用*,本章所有rotate2d(...) * st、scale(...) * st均是后者。
本章小结与下一步
本章我们学会了用矩阵变换来平移、旋转、缩放向量:平移是给st加向量,旋转与缩放是构造mat2并左乘坐标,且都遵循"移到原点 → 变换 → 移回"的通用套路。这些变换对把上一章学到的形状组织成复杂构图至关重要,而 YUV 示例则证明了同一套矩阵工具可以跨界作用于颜色空间——凡是向量,皆可变换。
下一章(09 图案)将把本章所学全部应用到程序化图案的创作中:通过包装与重复"空间",用并行而非逐对象的方式绘制重复图案。你会发现"编码重复与变化"是一件令人兴奋的实践。
工具箱备注:若想深入学习用矩阵操纵空间的实用函数集合,可参考 LYGIA 的 space functions 库——一套面向 GLSL 空间操作的细粒度、可复用函数库,设计上注重复用、性能与灵活性,可轻松集成到任意项目与框架中。
【免费下载链接】thebookofshaders
Step-by-step guide through the abstract and complex universe of Fragment Shaders.
相关推荐
The Book of Shaders 精读:用 GLSL 二维矩阵变换平移、旋转与缩放坐标空间
The Book of Shaders 精读:用 GLSL 二维矩阵变换平移、旋转与缩放坐标空间 本篇文章精读开源项目 thebookofshaders htt
The Book of Shaders 第 8 章精读:用 GLSL 二维矩阵驱动平移、旋转与缩放变换
The Book of Shaders 第 8 章精读:用 GLSL 二维矩阵驱动平移、旋转与缩放变换 本文基于开源仓库 thebookofshaders 第
The Book of Shaders 第 08 章:GLSL 二维矩阵变换实战指南 —— 平移、旋转、缩放与 YUV 色彩空间
The Book of Shaders 第 08 章:GLSL 二维矩阵变换实战指南 —— 平移、旋转、缩放与 YUV 色彩空间 本章( 08/README v
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考