“Depicting depth perception in 3D models or illustrations by varying levels of darkness” — Wikipedia
着色器就是“告诉计算机如何绘制某物”的程序。
也就是说,着色器是小程序,运行在 GPU 上,决定像素或顶点如何显示。
SHADERS ON MODERN GPU’S
- Computer graphics / Images
- Highly parallel computing
- Mining for cryptocurrency
这一页列出现代 GPU 的几个主要用途,其中“着色器”正是其中一个核心应用:
概念 | 理解 |
---|---|
Shading(阴影) | 通过改变亮度模拟三维效果 |
Shader(着色器) | 小程序,用来告诉 GPU 如何绘制一个像素或顶点 |
GPU 特点 | 强大并行计算能力,适合做图形渲染、AI训练、挖矿等 |
add
(加法)特征 | 是否适合 GPU |
---|---|
同时处理数百万像素 | 是 |
每个像素运行相同着色程序 | 是 |
逻辑简单,数据密集 | 是 |
分支少、计算密集型任务 | 是 |
比较维度 | CPU | GPU |
---|---|---|
核心数量 | 少(4-16) | 多(几百到上千) |
每个核心能力 | 强大 | 简单 |
并行处理能力 | 差 | 强 |
适合任务类型 | 控制、逻辑复杂 | 数据/图像并行 |
是否适合图像渲染 | 不适合 | 非常适合 |
在一个典型的 GPU 渲染流程中,数据从顶点开始,经过各种处理,最终变成图像上的像素。这些处理由不同的 Shader 负责。
包括两个阶段:
VERTEX DATA[]
↓
Vertex Shader
↓
(可选) Tessellation
↓
(可选) Geometry Shader
↓
Shape Assembly
↓
Rasterization
↓
Fragment Shader
↓
Tests and Blending
↓
Framebuffer (最终图像)
如果你希望我提供某个 shader 类型的具体代码示例(如 GLSL 中的 Vertex Shader 或 Fragment Shader),我可以帮你写一个基础版本。
vec4
类型,RGBA)。在 WebGL、GLSL 中,Pixel Shader 和 Fragment Shader 指的是同一类程序,运行在每一个屏幕像素上。
以下是一个经典的“基于坐标生成彩色渐变”的 fragment shader:
// 一个程序化图像 Fragment Shader 示例(GLSL)
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution; // 画布分辨率(例如:800x600)
uniform float u_time; // 当前时间(用于动画)
void main() {
// 将像素坐标归一化为 0 ~ 1
vec2 uv = gl_FragCoord.xy / u_resolution;
// 简单的程序化颜色:色相随 x 坐标变化
vec3 color = vec3(uv.x, uv.y, abs(sin(u_time)));
gl_FragColor = vec4(color, 1.0);
}
gl_FragCoord.xy
:当前像素的屏幕坐标(单位:像素)/ u_resolution
:标准化为 [0,1]
区间vec3(uv.x, uv.y, sin(time))
:R = x, G = y, B = 动态变化 → 生成彩色动画背景你可以使用下面任意一种平台来运行此 shader:
glsl
语言写入项目 | 内容 |
---|---|
Shader 类型 | Fragment / Pixel Shader |
特征 | 每个像素独立运行,生成最终颜色 |
程序化图像 | 用数学函数代替贴图,实现图像或动画生成 |
应用 | 游戏、动态图形、视觉特效、背景生成、VJ 视觉、ShaderToy 等 |
下面我来帮你理清并总结这个动机内容:
你希望能做到像以下这些视觉效果(slide 11-14 的内容):
你并不是在最强的机器上开发:
开发时间很碎片化,例如:
即使在桌面端的Nvidia GTX 1060 这样相对强的显卡上:
动机元素 | 解释 |
---|---|
想实现的目标 | 实时图形效果 / 程序化可视化 / 动态特效 |
遇到的限制 | 目标平台性能差 / 图形资源受限 |
开发场景 | 长时间通勤时希望也能创作 / 在弱设备上开发 |
遇到的技术问题 | GPU 驱动兼容性差 / 不同平台行为不同 / 某些 shader 不兼容 |
结果(暗示) | 促使你去了解更底层的 shader 编程、优化渲染管线、适配多平台 |
如果你是在准备一个讲座/展示幻灯片,这部分可以自然引出你的主题: |
“在这种场景下,我开始探索如何写出高效、可移植、程序化的 Shader —— 既能跑得快,又能表现丰富。”
链接:https://github.com/valentingalea/vml
VML 是一个用于 快速开发、调试和测试 GLSL / Shadertoy 风格 Shader 的小型语言和工具集,适合:
功能 | 描述 |
---|---|
Debug / Decompile | 反编译或调试其他平台的着色器代码,例如 Shadertoy 上的效果。方便分析或重构他人算法。 |
Texture Generator | 快速生成程序化纹理,如噪声图案、渐变图、波纹等。用于实验性渲染或素材制作。 |
Unit Test Shaders | 给 shader 写单元测试,验证视觉输出是否一致,特别适合开发底层工具链时使用。 |
Quick Prototype | 方便进行着色器算法的快速原型开发,不必每次都跑完整管线或集成到主工程中。 |
VML 是 Shader 开发者的“实验室”工具箱,尤其适合做“黑魔法”的人!
如果你是做图形教学、Shadertoy 创作、VFX 研发、或者喜欢 GPU 上动手的人,这个项目很值一试。
RenderMan 是由 Pixar 开发的图形渲染系统,它使用一种 面向过程的着色语言 来定义物体的表面外观 —— 比如颜色、光照、透明度等。
/*
* red mesh
*/
surface basic() {
Ci = (1.0, 0.0, 0.0); // 输出颜色 (R=1, G=0, B=0) 红色
Oi = 1; // 输出不透明度 = 1(完全不透明)
}
Ci
是 颜色输出变量(Color output)。Oi
是 不透明度输出变量(Opacity output)。surface simple(color myOpacity = 1) {
color myColor = (1.0, 0.0, 0.0); // 定义颜色为红色
normal Nn = normalize(N); // 单位化法线(用于光照计算)
Ci = myColor * myOpacity * diff; // 着色公式(乘上漫反射)
Oi = myOpacity; // 设置输出不透明度
}
myOpacity
是一个参数,默认值为 1(完全不透明)。diff
是一个 预定义变量,表示漫反射光照强度(diffuse)。N
是几何体的法线向量;Nn = normalize(N)
对其单位化,用于光照计算。Ci
乘上光照后表示:红色受到光照后的颜色效果。概念 | 含义 |
---|---|
surface |
定义一个表面着色器(Surface Shader) |
Ci |
输出的颜色(Color) |
Oi |
输出的不透明度(Opacity) |
diff |
默认的漫反射光照因子 |
normal N |
表面法线 |
normalize |
向量单位化 |
这些语言都用于编写 GPU 上运行的 shader 程序(如顶点着色器、片元着色器等),在游戏或交互式图形中广泛应用。
TEMP R0;
DP3 R0, vertex.normal, light.direction;
in
/out
、uniform、struct、mat4 等)时期 | 语言 | 特点 |
---|---|---|
早期 | ARB ASM | 汇编风格,硬件近 |
2002+ | Cg | 类 C,跨平台,NVIDIA 推出 |
2004+ | GLSL | OpenGL 官方,现代语法 |
DirectX9+ | HLSL | DirectX 专用,PC/Xbox 常用 |
PS平台 | PS Shader | 类 HLSL,专有语言 |
这段代码是一个 光照计算(diffuse 漫反射) 的片元着色器,它根据光线方向与表面法线的夹角来计算光照强度。
varying vec3 N; // 表面法线 (从顶点着色器插值传入)
varying vec3 v; // 顶点位置 (从顶点着色器插值传入)
void main(void) {
vec3 L = normalize(gl_LightSource[0].position.xyz - v); // 光线方向
vec4 Idiff = gl_FrontLightProduct[0].diffuse * max(dot(N,L), 0); // 漫反射强度
Idiff = clamp(Idiff, 0.0, 1.0); // 限制值在 [0,1]
gl_FragColor = Idiff; // 设置输出颜色
}
gl_LightSource
和 gl_FrontLightProduct
(旧版 OpenGL 风格)。varying
是从 vertex shader 传入的插值值。gl_FragColor
输出片元颜色。Phong
或 Lambert
光照模型。float4 main(
float3 Light : TEXCOORD0, // 光线方向
float3 Norm : TEXCOORD1 // 法线
) : COLOR {
float4 diffuse = { 1.0, 0.0, 0.0, 1.0 }; // 红色漫反射
float4 ambient = { 0.1, 0.0, 0.0, 1.0 }; // 环境光
return ambient + diffuse * saturate(dot(Light, Norm));
}
: TEXCOORD0
等)绑定。: COLOR
。saturate()
(相当于 clamp(x, 0, 1)
)。方面 | GLSL | HLSL |
---|---|---|
所属平台 | OpenGL, WebGL | DirectX(Windows / Xbox) |
变量绑定方式 | uniform , in , out , varying |
: SEMANTIC (语义,如 POSITION ) |
内建变量 | gl_Position , gl_FragColor 等 |
通常由应用传入,较自由 |
函数库 | 标准库多,接口更规范 | 更灵活、控制粒度更细 |
语言语法风格 | 更接近 C | 更接近 C/C++ |
两者在效果上都计算了:
ambient + diffuse * max(dot(light_dir, normal), 0)
即:漫反射光照模型,结果是根据法线与光线夹角调节颜色亮度。
类型类别 | GLSL 示例 | HLSL 示例 | 说明 |
---|---|---|---|
标量类型 | bool , int , uint , float , double |
bool , int , uint , float , double |
两者基本相同,表示布尔、整数、无符号整数、浮点、双精度 |
向量类型 | vec2 , vec3 , vec4 |
float2 , float3 , float4 |
表示二维、三维、四维向量,常用于位置、颜色、法线等 |
矩阵类型 | mat2 , mat3 , mat4 |
float2x2 , float3x3 , float4x4 |
矩阵类型,用于变换、旋转等数学操作 |
其他类型 | textures, samplers, precision modifiers | textures, samplers, precision modifiers | 纹理采样器、精度限定符等,细节语法略有差异 |
vec*
和 mat*
作为向量和矩阵类型的命名规范,语义更直观。float2
, float3x3
的写法,类型名直接带出元素类型和维度。highp
, mediump
, lowp
),尤其在 OpenGL ES(移动端)中很重要,HLSL 也支持类似功能但通常由编译器和硬件自动管理。方面 | GLSL | HLSL | C++ |
---|---|---|---|
变量声明 | T name = T( ... ); |
T name = { ... }; |
T name = { ... }; |
数组声明 | T name[size]; |
T name[size]; |
T name[size]; |
结构体声明 | struct { ... } name; |
struct { ... } name; |
struct { ... } name; |
结构体初始化 | T name = T( ... ); |
T name = { ... }; |
T name = { ... }; |
vec3 v = vec3(1.0, 2.0, 3.0);
float3 v = {1.0, 2.0, 3.0};
float arr[4];
GLSL 的声明和初始化更偏向函数式风格(构造函数),HLSL 和 C++ 更偏向传统的 C 风格初始化。理解这些差异有助于跨语言移植 shader 代码。
含义 | GLSL / HLSL | C++ 对应写法 |
---|---|---|
只读(输入) | in T |
const T 或 T (值传递) |
只写(输出) | out T |
T& (非 const 引用) |
可读可写(输入输出) | inout T |
T& (非 const 引用) |
in T
:该参数作为输入使用,函数体内可以读取但不能修改传入变量。out T
:该参数用于输出,函数体必须赋值,外部才能获得结果。inout T
:可同时读取和修改,传入的值可以被函数修改并传出。T
:默认值传递,即复制一份进去,函数内修改不会影响外部。T&
:引用传递,函数可以修改外部变量。const T&
:只读引用传递,节省内存又不会修改外部变量。const T
:传值但承诺不修改。void process(in float x, out float y, inout float z) {
y = x * 2.0;
z += 1.0;
}
void process(const float x, float& y, float& z) {
y = x * 2.0f;
z += 1.0f;
}
传递方式 | 是否可读 | 是否可写 | 作用 |
---|---|---|---|
in |
是 | 否 | 输入参数(只读) |
out |
否 | 是 | 输出参数(必须赋值) |
inout |
是 | 是 | 输入输出(可修改原值) |
GLSL 中的向量是通用的(generic),可以存储多种数据类型,比如:
类型 | 说明 | 示例 |
---|---|---|
vec2 |
两个 float 组成的向量 |
vec2 texcoord1; |
vec3 |
三个 float 组成的向量 |
vec3 position; |
vec4 |
四个 float 组成的向量 |
vec4 myRGBA; |
ivec2 |
两个 int 整数向量 |
ivec2 texLookup; |
bvec3 |
三个 bool 布尔值向量 |
bvec3 less; |
vec4 color = vec4(1.0, 0.5, 0.2, 1.0); // RGBA 颜色
ivec2 coords = ivec2(10, 20); // 整数纹理坐标
bvec3 flags = bvec3(true, false, true); // 条件组合
GLSL 中的矩阵类型 仅支持浮点类型(float),常见的有:
类型 | 说明 |
---|---|
mat2 |
2x2 浮点矩阵(线性变换) |
mat3 |
3x3 浮点矩阵(法线变换等) |
mat4 |
4x4 浮点矩阵(3D 投影/变换) |
mat4 model = mat4(1.0); // 单位矩阵
vec4 worldPos = model * position; // 变换顶点位置
vecX
, ivecX
, bvecX
)支持不同数据类型:float
、int
、bool
。float
类型,没有 imat4
或 bmat3
。Swizzling 是一种语法方式,允许你:
类型 | 分量名 | 说明 |
---|---|---|
空间坐标 | x, y, z, w |
如:位置、法线、速度等 |
颜色 | r, g, b, a |
red、green、blue、alpha |
纹理坐标 | s, t, p, q |
通常用于纹理 UV 坐标 |
实际上这些是 同一个向量的别名访问方式,根据用途选取最直观的方式书写。
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);
// 访问分量
float a = v.x; // == 1.0
float b = v.g; // == 2.0 (同 v.y)
float c = v.q; // == 4.0 (同 v.w)
// 创建新向量
vec2 pos = v.xy; // == vec2(1.0, 2.0)
vec3 color = v.rgb; // == vec3(1.0, 2.0, 3.0)
// 交换顺序或复制
vec3 flipped = v.zyx; // == vec3(3.0, 2.0, 1.0)
vec2 dupe = v.xx; // == vec2(1.0, 1.0)
v.xy = vec2(5.0, 6.0); // 合法
v.xx = vec2(7.0, 8.0); // 非法,重复写入 x
Swizzling 是 GLSL 提供的一种强大且简洁的方式:
Swizzling 是一种语法糖,让你可以:
vec4 v4;
v4.rgba; // == v4,完整的4维向量
v4.rgb; // vec3,取前三个分量
v4.b; // float,只取第3个分量(z)
v4.xy; // vec2,取前两个分量(x, y)
这些是只读操作,即 Swizzle 作为右值(r-value)。
vec4 pos = vec4(1.0, 2.0, 3.0, 4.0);
vec4 swiz = pos.wzyx; // swiz = (4.0, 3.0, 2.0, 1.0)
这是 Swizzle 的“重排功能”,你可以任意顺序组合分量。
vec4 dup = vec4(pos.xx, pos.yy);
// dup = vec4(1.0, 1.0, 2.0, 2.0)
你可以将多个 swizzle 结果拼接成新向量。
pos.xw = vec2(5.0, 6.0);
// pos = (5.0, 2.0, 3.0, 6.0)
pos.xw
是合法左值,因为 x
和 w
不重复,可以同时写入。
pos.xx = vec2(3.0, 4.0); // 非法
因为 x
被重复使用,写入时会产生冲突,这是 GLSL 不允许 的。
Swizzle 功能 | 是否允许 | 示例 |
---|---|---|
读取分量组合 | 是 | v4.zyx , v4.xy |
重排序/复制 | 是 | vec4(v4.xx, v4.yy) |
左值写入(非重复) | 是 | v4.xw = vec2(...) |
左值写入(有重复) | 否 | v4.xx = ... |
用于快速近似计算法线向量(normal vector),例如在基于高度场(heightmap)的 raymarching 或 SDF 渲染中。
vec3 calcNormal(in vec3 pos) {
vec2 e = vec2(1.0, -1.0) * 0.0005;
return normalize(
e.xyy * map(pos + e.xyy).x +
e.yyx * map(pos + e.yyx).x +
e.yxy * map(pos + e.yxy).x +
e.xxx * map(pos + e.xxx).x
);
}
使用一个有限差分法(finite difference),在 pos
周围的不同方向微小偏移,通过采样值变化来近似法线方向。
e.xyy
, e.yyx
等是什么意思?vec2 e = vec2(1.0, -1.0) * 0.0005;
// => e = vec2(0.0005, -0.0005)
Swizzle 用于生成方向向量的不同组合:
swizzle | 展开成 vec3 |
---|---|
e.xyy |
(0.0005, -0.0005, -0.0005) |
e.yyx |
(-0.0005, -0.0005, 0.0005) |
e.yxy |
(-0.0005, 0.0005, -0.0005) |
e.xxx |
(0.0005, 0.0005, 0.0005) |
这些是用于沿不同方向轻微扰动 pos 。 |
e.xyy * map(pos + e.xyy).x
pos + e.xyy
:在某个方向上微小偏移位置map(...)
:通常是距离场函数,返回 vec2
或 vec3
,其中 .x
是距离map(...).x
:取该位置的标量值(如距离)e.xyy * ...
:缩放向量,用于累加权重return normalize(...);
将所有方向的偏移量影响整合成一个向量,并归一化为单位长度的法线向量。
优点 | 说明 |
---|---|
简洁 | e.xyy 这种写法比 vec3(e.x, e.y, e.y) 清晰简短 |
可读性 | 表达的是“方向”,而不是计算过程 |
性能好 | Swizzle 是语法糖,编译器可优化得非常好 |
实用 | 在法线估算、偏移、光照、噪声扰动等都有大量应用 |
假设我们有一个函数 f ( x ) f(x) f(x),其导数定义是:
f ′ ( x ) = lim h → 0 f ( x + h ) − f ( x ) h f'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h} f′(x)=h→0limhf(x+h)−f(x)
而有限差分法用一个 小但非零的 h 值 来近似这个导数:
f ′ ( x ) ≈ f ( x + h ) − f ( x ) h f'(x) \approx \frac{f(x+h) - f(x)}{h} f′(x)≈hf(x+h)−f(x)
这个就是所谓的 前向差分(forward difference)。
名称 | 公式 | 特点 |
---|---|---|
前向差分 | f ′ ( x ) ≈ f ( x + h ) − f ( x ) h f'(x) \approx \frac{f(x+h) - f(x)}{h} f′(x)≈hf(x+h)−f(x) | 简单,精度较低 |
后向差分 | f ′ ( x ) ≈ f ( x ) − f ( x − h ) h f'(x) \approx \frac{f(x) - f(x-h)}{h} f′(x)≈hf(x)−f(x−h) | 类似前向差分 |
中心差分 | f ′ ( x ) ≈ f ( x + h ) − f ( x − h ) 2 h f'(x) \approx \frac{f(x+h) - f(x-h)}{2h} f′(x)≈2hf(x+h)−f(x−h) | 更精确,误差为 O ( h 2 ) O(h^2) O(h2) |
二阶导数近似 | f ′ ′ ( x ) ≈ f ( x + h ) − 2 f ( x ) + f ( x − h ) h 2 f''(x) \approx \frac{f(x+h) - 2f(x) + f(x-h)}{h^2} f′′(x)≈h2f(x+h)−2f(x)+f(x−h) | 用于加速度/曲率等 |
应用场景 | 描述 |
---|---|
法线估算 | 使用函数在多个偏移点的值来估计梯度方向(即法线) |
边缘检测 | Sobel、Laplacian 等滤波器就是差分近似 |
光照/阴影贴图 | 对深度图进行差分估计斜率等 |
距离场计算 | SDF 渲染中估算梯度来生成表面方向 |
float h = 0.01;
float height(float x) {
return sin(x);
}
float approximate_derivative(float x) {
return (height(x + h) - height(x)) / h;
}
如果你在做 3D 图形,比如一个地形函数 f ( x , y ) f(x, y) f(x,y),你可以用偏微分来估算表面的法线方向:
// in GLSL
float h = 0.001;
float center = map(p);
float dx = map(p + vec3(h, 0.0, 0.0)) - center;
float dy = map(p + vec3(0.0, h, 0.0)) - center;
float dz = map(p + vec3(0.0, 0.0, h)) - center;
vec3 normal = normalize(vec3(dx, dy, dz));
特点 | 描述 |
---|---|
实用 | 在无解析导数时仍能估算导数 |
快速 | 适合 GPU 或数值计算 |
可调精度 | 通过调整 h 控制精度与性能 |
简洁实现 | 不依赖于复杂数学库 |
SDF 是一个函数,它告诉你某个点离某个形状的边界有多远(以及是在形状内还是外)。渲染时用这个函数去生成视觉效果。
一个 Signed Distance Field 是一个函数:
f ( p ) = 距离 ( p , 最近的表面 ) f(\mathbf{p}) = \text{距离}(\mathbf{p}, \text{最近的表面}) f(p)=距离(p,最近的表面)
对一个半径为 1 的圆的 SDF 函数:
float sdf_circle(vec2 p) {
return length(p) - 1.0;
}
应用 | 描述 |
---|---|
字体渲染 | 常用于游戏引擎(如 Unity、Unreal)渲染高质量、可缩放字体 |
2D 图形 | 高质量 UI 图标、符号、按钮边缘抗锯齿 |
3D 建模 | 构建复杂形状的隐式表达,如云、液体、软体物体 |
距离场光照 | 利用 SDF 计算光照、阴影和 GI 效果 |
程序生成图形 | ShaderToy 等中广泛使用,用代码画出视觉艺术图形 |
// Signed distance to a circle at (0, 0), radius 0.5
float sdf_circle(vec2 p) {
return length(p) - 0.5;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy * 2.0 - 1.0;
float d = sdf_circle(uv);
// Smooth edge between -0.01 and 0.01
float alpha = smoothstep(0.01, -0.01, d);
fragColor = vec4(vec3(1.0), alpha);
}
优势 | 描述 |
---|---|
抗锯齿自然 | 距离值平滑过渡,避免锯齿 |
高度可缩放 | 只需存储距离信息,可任意缩放图形 |
程序生成 | 用数学公式定义图形,无需纹理 |
易做布尔操作 | 可组合多个 SDF:并集、交集、差集 |
局限性 | 描述 |
---|---|
复杂形状效率低 | 对非常复杂的形状,SDF 表达可能较慢 |
不适合传统建模管线 | 对艺术家不太友好 |
难以精确控制边缘细节 | 特别是手绘的细节或纹理 |
特性 | 描述 |
---|---|
是什么 | 表示形状到最近边界的有符号距离函数 |
用于做什么 | 渲染文字、图标、程序图形、隐式建模 |
如何使用 | 写距离函数、在 shader 中采样、做抗锯齿 |
优点 | 精准抗锯齿、可缩放、合成灵活 |
GLSL 允许你使用 简洁的操作符语法 来对向量和矩阵进行数学运算,比如 +
和 *
,这些运算符已经被重载来表示常见的线性代数操作。
vec3 v, u, w;
w = v + u;
这是一个向量加法。假设 v
和 u
是 vec3
类型,即包含 3 个浮点分量(x, y, z)的向量。
w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;
vec3 u = v * m; // v 是 vec3,m 是 mat3
这是一个向量 × 矩阵乘法,其中:
v
是一个 3 分量向量(vec3
)m
是一个 3x3 矩阵(mat3
)v
看作行向量,矩阵 m
看作列主序排列,进行右乘运算。u.x = dot(v, m[0]); // m[0] 是矩阵的第 1 列
u.y = dot(v, m[1]); // m[1] 是矩阵的第 2 列
u.z = dot(v, m[2]); // m[2] 是矩阵的第 3 列
dot(a, b)
表示点积(即内积):
dot ( a , b ) = a . x ∗ b . x + a . y ∗ b . y + a . z ∗ b . z \text{dot}(a, b) = a.x * b.x + a.y * b.y + a.z * b.z dot(a,b)=a.x∗b.x+a.y∗b.y+a.z∗b.z
表达式 | 含义 | 等价低级操作 |
---|---|---|
w = v + u |
向量逐分量相加 | w.x = v.x + u.x; ... |
u = v * m |
向量乘以矩阵(线性变换) | u.x = dot(v, m[0]); ... |
这些是基本的数学运算,作用与 C++ 或 Python 中相似:
函数名 | 说明 |
---|---|
sin(x) |
正弦函数 |
cos(x) |
余弦函数 |
radians(x) |
把角度转换为弧度(degrees → radians) |
pow(x, y) |
幂运算, x y x^y xy |
exp(x) |
自然指数函数, e x e^x ex |
这些函数主要用于数据清理、处理边界、处理符号等:
函数名 | 说明 |
---|---|
abs(x) |
绝对值 |
sign(x) |
返回符号(负数为 -1,正数为 1,0 为 0) |
floor(x) |
向下取整 |
mod(x, y) |
模运算(取余),如 x % y |
min(x, y) |
取最小值 |
max(x, y) |
取最大值 |
clamp(x, a, b) |
限制值在区间 [ a , b ] [a, b] [a,b] 之间 |
这些在图形插值、过渡动画中非常常用:
函数名 | 说明 |
---|---|
mix(a, b, t) |
线性插值:返回 a ⋅ ( 1 − t ) + b ⋅ t a \cdot (1 - t) + b \cdot t a⋅(1−t)+b⋅t,通常用于颜色混合 |
step(edge, x) |
阶跃函数, x < e d g e x < edge x<edge 为 0,其他为 1 |
smoothstep(edge0, edge1, x) |
平滑插值(带缓入缓出过渡) |
适用于 vec2
, vec3
, vec4
等向量数据类型:
函数名 | 说明 |
---|---|
length(v) |
向量长度 x 2 + y 2 + z 2 \sqrt{x^2 + y^2 + z^2} x2+y2+z2 |
dot(v1, v2) |
向量点积(用于投影、角度判断) |
cross(v1, v2) |
向量叉积(仅 vec3 ,用于计算法线) |
distance(a, b) |
两点之间的欧几里得距离 |
normalize(v) |
向量单位化(长度变成 1) |
这些用于从纹理中读取数据,在 fragment shader 中尤为常用:
示例函数 | 说明 |
---|---|
texture(sampler, coord) |
从纹理中采样颜色值 |
texelFetch() |
精确读取某像素(不插值) |
GLSL 的标准函数库为 shader 编程提供了丰富的数学与图形处理能力,使得你可以:
dot
, normalize
, length
)mix
, smoothstep
)texture
、mod
)着色语言 | 语言基底 | 平台 | 备注 |
---|---|---|---|
Metal Shading Language | 基于 C++14 | Apple iOS 设备 | 仅限苹果设备 |
CUDA (NVIDIA) | 基于 C++11 | 仅用于计算(非图形) | 主要用于异构计算和GPU编程 |
HLSL 6.x | 类似 C++98 | 微软平台 | 目前尚未正式发布 |
这方案的重点是用 C++ 语言的强大功能和灵活的预处理器系统,把着色语言的代码转换成 C++ 代码,方便调试、测试、重用,甚至跨平台开发。
vector
模板类,用来模拟 GLSL 中的向量类型,比如 vec2
, vec3
, vec4
。核心点如下:vector
来实现任意长度的向量vector_base
,实现基础功能template<typename T, size_t N>
struct vector : public vector_base<T, N> {
using scalar_type = T; // 向量元素类型,比如 float、int 等
using vector_type = vector<T, N>; // 当前向量类型本身,方便返回自身类型
// 默认构造函数,将所有元素初始化为0
vector() {
// static_for 是一个编译期循环,展开为对每个元素赋值
static_for<0, N>()([this](size_t i) {
data[i] = 0; // 将第 i 个元素置零
});
}
// 标量构造函数,用同一个值初始化所有元素
explicit vector(scalar_type s) {
static_for<0, N>()([s, this](size_t i) {
data[i] = s; // 将第 i 个元素赋值为 s
});
}
// 可变参数构造函数,支持按元素逐个传值初始化
// 例如 vector v(1.0f, 2.0f, 3.0f);
template<typename... Args>
explicit vector(Args... args);
// 元素访问操作符,非 const 版本
scalar_type& operator[](size_t i);
// 元素访问操作符,const 版本
const scalar_type& operator[](size_t i) const;
// 向量加标量,所有元素加上同一个标量,返回自身引用(支持链式调用)
vector_type& operator +=(scalar_type s);
// 向量加向量,元素对应相加,返回自身引用(支持链式调用)
vector_type& operator +=(const vector_type& v);
// 这里还会有其它运算符重载,比如 -=, *=, /= 等
};
static_for<0, N>()
是一种编译期循环,用来展开对每个元素的初始化operator[]
支持访问元素static_for
是一种在编译期递归展开的“静态循环”工具,利用模板递归实现从 Begin
到 End-1
逐个调用传入的函数对象 f
,用法类似于运行时的 for
循环,但循环次数在编译期固定。// 模板结构,代表一个从 Begin 到 End 的编译期递归循环
template<size_t Begin, size_t End>
struct static_for {
// 传入一个函数对象 f,这个函数会被调用多次,每次传入当前索引
template<class Func>
constexpr void operator()(Func&& f) {
f(Begin); // 调用函数 f,传入当前索引 Begin
// 递归调用下一个索引的 static_for,直到 Begin == End 时终止
static_for<Begin + 1, End>()(std::forward<Func>(f));
}
};
// 递归终止条件:当 Begin == End 时,什么都不做,停止递归
template<size_t N>
struct static_for<N, N> {
template<class Func>
constexpr void operator()(Func&&) {
// 终止递归,不调用任何函数
}
};
假设调用:
static_for<0, 3>()([](size_t i) {
std::cout << i << " ";
});
运行时效果类似:
0 1 2
因为:
static_for<0, 3>
调用 f(0)
,然后递归调用 static_for<1, 3>
static_for<1, 3>
调用 f(1)
,递归调用 static_for<2, 3>
static_for<2, 3>
调用 f(2)
,递归调用 static_for<3, 3>
static_for<3, 3>
是终止条件,不再调用vector<>
类模板的一个比较高级的构造函数(constructor),支持用多参数列表初始化向量元素。它用了模板技巧来启用(SFINAE)这构造函数,只在特定条件满足时才生效。template<typename A0, typename... Args,
// SFINAE启用条件:只有满足以下之一才启用这个构造函数
class = typename std::enable_if<
(
// 参数包中有多个参数,或
(sizeof...(Args) >= 1) ||
// 参数包只有一个参数且该参数不是标量类型(例如是向量或结构体)
((sizeof...(Args) == 0) && !std::is_scalar_v<A0>)
)
>::type>
explicit vector(A0&& a0, Args&&... args)
{
// 调用递归模板函数static_recurse<0>,从第0个元素开始
// 将传入的参数逐个“展开”并赋值给vector的元素
static_recurse<0>(
std::forward<A0>(a0), // 完美转发第一个参数
std::forward<Args>(args)... // 完美转发剩余参数包
);
}
A0
是第一个参数类型,Args...
是剩余参数包std::enable_if<...>
用于限制该构造函数的启用条件sizeof...(Args) >= 1
)——代表显式传入多个值static_recurse<0>
,很可能是递归地将这些参数赋值给 vector
的各个元素(你之前提到过 static_for
,这个递归赋值实现就是类似的思想)vector
通过类似 vector v(1.0f, 2.0f, 3.0f);
来初始化元素enable_if
,避免构造函数冲突和歧义(例如不允许用单个标量参数调用这个构造函数,可能是另一个构造函数处理这个情况)static_recurse
模板函数的实现,它是上面 vector
构造函数中用来递归初始化每个元素的辅助函数。template<size_t I, typename Arg0, typename... Args>
void static_recurse(Arg0&& a0, Args&&... args)
{
construct_at_index<I>(std::forward<Arg0>(a0)); // 在索引 I 处构造元素,使用参数 a0
static_recurse<I + get_size<Arg0>()>( // 递归调用下一个索引,偏移 get_size()(参数a0占用多少元素)
std::forward<Args>(args)... // 递归展开剩余参数
);
}
// 终止递归的空版本,匹配无参数时调用
template<size_t I>
void static_recurse()
{
// 终止条件,不执行任何操作
}
I
是当前处理的元素索引,从 0
开始递归向后推进。construct_at_index(...)
:这个函数负责把传入的参数放到向量第 I
个元素上,具体细节依赖实现。get_size()
:计算参数 a0
实际占据的元素数量(标量就是 1,向量或其他类型可能是多个元素)。static_recurse
,依次给向量的每个元素赋值。static_recurse()
,什么都不做,终止递归。vector
各元素的初始化,支持多参数以及复杂参数类型。配合前面那个高级构造函数,实现灵活且高效的初始化。construct_at_index
的两种重载实现,用于支持用标量或者另一个 vector
来初始化当前 vector
对象的部分或全部元素。下面帮你详细解析:// 当传入的是标量类型(scalar_type)时
template<size_t i>
void construct_at_index(scalar_type arg)
{
data[i] = arg; // 直接把标量赋值给第 i 个元素
}
// 当传入的是另一个 vector,且是右值引用时(用来移动或转移)
template<size_t i, typename Other, size_t Other_N>
void construct_at_index(vector<Other, Other_N>&& arg)
{
// 计算赋值的最大索引,防止越界
constexpr auto count = std::min(i + Other_N, num_components);
// 利用静态循环,遍历 [i, count) 范围的索引 j
static_for<i, count>()([&](size_t j) {
// 赋值对应索引的元素,arg 的元素索引是 j - i
data[j] = arg.data[j - i];
});
}
data[i]
这个单元素。data
数组从 i
开始的连续位置,最多到本 vector 的最后一个元素(num_components
表示 vector 总大小)。static_for()
是模板编译期递归展开的循环,效率高且不产生运行时循环开销。vector
的构造函数可以灵活接收混合类型的参数(标量和向量),并且支持从任意位置开始批量初始化元素,方便初始化长向量。vector<>
设计在构造函数里递归处理混合参数的能力:using vec2 = vector<int, 2>;
using vec3 = vector<int, 3>;
vec3 v = vec3(98, vec2(99, 100));
vec3(98, vec2(99, 100))
表示用一个标量 98
和一个 vec2
来构造一个 3 元素的 vec3
。98
初始化 v.data[0]
。vec2(99, 100)
递归调用,初始化剩下两个元素 v.data[1]
和 v.data[2]
。static_recurse
和 construct_at_index
机制实现。main
例子:int main()
{
float a, b;
scanf("%f %f", &a, &b);
auto v = vec3(1.f, vec2(a, b));
printf("%f %f", v.x, v.y);
}
a, b
。vec3(1.f, vec2(a, b))
构造 v
,等价于 v = {1.f, a, b}
。v.x
和 v.y
,它们分别是 1.f
和 a
。.x
, .y
是 vector
内部访问元素的便捷方式。#include
namespace swizzle {
namespace detail {
// 模板递归静态循环工具,编译期展开循环
template <size_t Begin, size_t End>
struct static_for {
template <class Func>
constexpr void operator()(Func &&f) {
f(Begin); // 调用传入的函数,参数是当前索引 Begin
static_for<Begin + 1, End>()(std::forward<Func>(f)); // 递归调用,处理下一个索引
}
};
// 递归终止条件,Begin == End时不再调用
template <size_t N>
struct static_for<N, N> {
template <class Func>
constexpr void operator()(Func &&) {}
};
// decay辅助函数,尝试调用对象的 decay() 方法返回衰减类型
template <class T>
constexpr auto decay(T &&t) -> decltype(t.decay()) {
return t.decay();
}
// decay重载版本:如果是标量类型,则直接返回原值
template <class T>
constexpr
typename std::enable_if<std::is_scalar<typename std::remove_reference<T>::type>::value, T>::type
decay(T &&t) {
return t;
}
// 判断一组类型是否都可转换为某类型V(所有Ts是否均能隐式转换为V)
template <typename V, typename... Ts>
constexpr bool converts_to() {
return (std::is_convertible<Ts, V>::value || ...);
}
} // namespace detail
} // namespace swizzle
namespace swizzle {
namespace detail {
// 用于实现向量分量混排(swizzle)的辅助模板
// vector_type: 原向量类型
// T: 基础数据类型(如float)
// N: 当前swizzle的分量数(如2表示.xy,3表示.xyz)
// indices...: 分量索引序列(如0,1表示xy)
template <typename vector_type, typename T, size_t N, size_t... indices>
struct swizzler {
T data
[N]; // 存储混排的分量数据,数量可能与vector_type的分量数不同,比如vec3的.xxx是3个相同分量
// 转换为原始向量类型(把混排的分量复制到一个新的vector_type对象)
vector_type decay() const {
vector_type vec;
assign_across(vec, 0, indices...);
return vec;
}
// 转换操作符,允许隐式转换为vector_type
operator vector_type() const { return decay(); }
operator vector_type() { return decay(); }
// 赋值操作符,从vector_type赋值给这个swizzler,修改data里的分量
swizzler &operator=(const vector_type &vec) {
assign_across(vec, 0, indices...);
return *this;
}
private:
// 赋值辅助,data里的值赋给vec对应分量
template <typename... Indices>
void assign_across(vector_type &vec, size_t i, Indices... swizz_i) const {
((vec[i++] = data[swizz_i]), ...);
}
// 赋值辅助,vec里的值赋给data对应分量
template <typename... Indices>
void assign_across(const vector_type &vec, size_t i, Indices... swizz_i) {
((data[swizz_i] = vec[i++]), ...);
}
};
} // namespace detail
} // namespace swizzle
// MSVC特有警告关闭(使用匿名结构体/联合体)
#pragma warning(disable : 4201)
namespace swizzle {
// vector_base模版针对不同大小的向量基础结构,包含各种分量访问别名和swizzle成员
template <typename T, size_t N, template <size_t...> class swizzler_wrapper>
struct vector_base;
// N=1的特化
template <typename T, template <size_t...> class swizzler_wrapper>
struct vector_base<T, 1, swizzler_wrapper> {
union {
T data[1]; // 底层存储
struct {
typename swizzler_wrapper<0>::type x; // .x 访问
};
struct {
typename swizzler_wrapper<0>::type r; // .r 访问(颜色用)
};
struct {
typename swizzler_wrapper<0>::type s; // .s 访问(纹理坐标用)
};
// 下面是重复的混排组合,vec1的多次重复
typename swizzler_wrapper<0, 0>::type xx, rr, ss;
typename swizzler_wrapper<0, 0, 0>::type xxx, rrr, sss;
typename swizzler_wrapper<0, 0, 0, 0>::type xxxx, rrrr, ssss;
};
};
// N=2的特化
template <typename T, template <size_t...> class swizzler_wrapper>
struct vector_base<T, 2, swizzler_wrapper> {
union {
T data[2];
struct {
typename swizzler_wrapper<0>::type x;
typename swizzler_wrapper<1>::type y;
};
struct {
typename swizzler_wrapper<0>::type r;
typename swizzler_wrapper<1>::type g;
};
struct {
typename swizzler_wrapper<0>::type s;
typename swizzler_wrapper<1>::type t;
};
// 2分量的各种swizzle组合,例:.xy, .yx, .xx, .yy等
typename swizzler_wrapper<0, 0>::type xx, rr, ss;
typename swizzler_wrapper<0, 1>::type xy, rg, st;
typename swizzler_wrapper<1, 0>::type yx, gr, ts;
typename swizzler_wrapper<1, 1>::type yy, gg, tt;
// 3分量的混排
typename swizzler_wrapper<0, 0, 0>::type xxx, rrr, sss;
typename swizzler_wrapper<0, 0, 1>::type xxy, rrg, sst;
typename swizzler_wrapper<0, 1, 0>::type xyx, rgr, sts;
typename swizzler_wrapper<0, 1, 1>::type xyy, rgg, stt;
typename swizzler_wrapper<1, 0, 0>::type yxx, grr, tss;
typename swizzler_wrapper<1, 0, 1>::type yxy, grg, tst;
typename swizzler_wrapper<1, 1, 0>::type yyx, ggr, tts;
typename swizzler_wrapper<1, 1, 1>::type yyy, ggg, ttt;
};
};
// N=3的特化
template <typename T, template <size_t...> class swizzler_wrapper>
struct vector_base<T, 3, swizzler_wrapper> {
union {
T data[3];
struct {
typename swizzler_wrapper<0>::type x;
typename swizzler_wrapper<1>::type y;
typename swizzler_wrapper<2>::type z;
};
struct {
typename swizzler_wrapper<0>::type r;
typename swizzler_wrapper<1>::type g;
typename swizzler_wrapper<2>::type b;
};
struct {
typename swizzler_wrapper<0>::type s;
typename swizzler_wrapper<1>::type t;
typename swizzler_wrapper<2>::type p;
};
// 2分量swizzle组合,如.xy, .xz, .yz等
typename swizzler_wrapper<0, 0>::type xx, rr, ss;
typename swizzler_wrapper<0, 1>::type xy, rg, st;
typename swizzler_wrapper<0, 2>::type xz, rb, sp;
typename swizzler_wrapper<1, 0>::type yx, gr, ts;
typename swizzler_wrapper<1, 1>::type yy, gg, tt;
typename swizzler_wrapper<1, 2>::type yz, gb, tp;
typename swizzler_wrapper<2, 0>::type zx, br, ps;
typename swizzler_wrapper<2, 1>::type zy, bg, pt;
typename swizzler_wrapper<2, 2>::type zz, bb, pp;
// 3分量swizzle组合,如.xxx, .xxy, .xyz等
typename swizzler_wrapper<0, 0, 0>::type xxx, rrr, sss;
typename swizzler_wrapper<0, 0, 1>::type xxy, rrg, sst;
typename swizzler_wrapper<0, 0, 2>::type xxz, rrb, ssp;
typename swizzler_wrapper<0, 1, 0>::type xyx, rgr, sts;
typename swizzler_wrapper<0, 1, 1>::type xyy, rgg, stt;
typename swizzler_wrapper<0, 1, 2>::type xyz, rgb, stp;
typename swizzler_wrapper<0, 2, 0>::type xzx, rbr, sps;
typename swizzler_wrapper<0, 2, 1>::type xzy, rbg, spt;
typename swizzler_wrapper<0, 2, 2>::type xzz, rbb, spp;
// 3分量swizzle组合,如.yxx, .yxy, .yyz等
typename swizzler_wrapper<1, 0, 0>::type yxx, grr, tss;
typename swizzler_wrapper<1, 0, 1>::type yxy, grg, tst;
typename swizzler_wrapper<1, 0, 2>::type yxz, grb, tsp;
typename swizzler_wrapper<1, 1, 0>::type yyx, ggr, tts;
typename swizzler_wrapper<1, 1, 1>::type yyy, ggg, ttt;
typename swizzler_wrapper<1, 1, 2>::type yyz, ggb, ttp;
typename swizzler_wrapper<1, 2, 0>::type yzx, gbr, tps;
typename swizzler_wrapper<1, 2, 1>::type yzy, gbg, tpt;
typename swizzler_wrapper<1, 2, 2>::type yzz, gbb, tpp;
// 3分量swizzle组合,如.zxx, .zxy, .zyz等
typename swizzler_wrapper<2, 0, 0>::type zxx, brr, pss;
typename swizzler_wrapper<2, 0, 1>::type zxy, brg, pst;
typename swizzler_wrapper<2, 0, 2>::type zxz, brb, psp;
typename swizzler_wrapper<2, 1, 0>::type zyx, bgr, pts;
typename swizzler_wrapper<2, 1, 1>::type zyy, bgg, ptt;
typename swizzler_wrapper<2, 1, 2>::type zyz, bgb, ptp;
typename swizzler_wrapper<2, 2, 0>::type zzx, bbr, pps;
typename swizzler_wrapper<2, 2, 1>::type zzy, bbg, ppt;
typename swizzler_wrapper<2, 2, 2>::type zzz, bbb, ppp;
};
};
} // namespace swizzle
namespace swizzle {
template <typename T, size_t N>
struct vector;
namespace util {
// 根据向量大小N,选择对应的vector_base,并定义swizzler包装器类型
template <typename T, size_t N>
struct vector_base_selector {
template <size_t... indices>
struct swizzler_wrapper_factory {
// 定义类型为swizzler,用于实现混排,vector的大小为indices的数量
using type = detail::swizzler<vector<T, sizeof...(indices)>, T, N, indices...>;
};
// 对于单个索引时,直接使用标量类型T,不使用swizzler包装
template <size_t x>
struct swizzler_wrapper_factory<x> {
using type = T;
};
using base_type = vector_base<T, N, swizzler_wrapper_factory>;
};
} // namespace util
// 具体向量类型定义,继承自vector_base_selector选出的基础类型
template <typename T, size_t N>
struct
#ifdef _MSC_VER
__declspec(empty_bases) // MSVC特有优化,避免空基类开销
#endif
vector : public util::vector_base_selector<T, N>::base_type {
using scalar_type = T;
using vector_type = vector<T, N>;
using base_type = typename util::vector_base_selector<T, N>::base_type;
// decay_type是一个简化的类型,如果N=1则退化为标量T,否则是本向量类型
using decay_type = typename std::conditional_t<N == 1, scalar_type, vector>;
// 把基类的data成员带入作用域(联合体数组)
using base_type::data;
// 默认构造函数,所有分量初始化为0
vector() {
iterate([this](size_t i) { data[i] = 0; });
}
// 单标量参数构造函数,所有分量赋值为同一个标量
explicit vector(scalar_type s) {
iterate([s, this](size_t i) { data[i] = s; });
}
// 默认拷贝构造和移动构造
vector(const vector_type &) = default;
vector(vector_type &&) = default;
// 支持多参数构造,允许使用混合标量和子向量构造
// 例如:vector v(1.f, vector(a,b));
template <
typename A0, typename... Args,
class = typename std::enable_if<((sizeof...(Args) >= 1) ||
((sizeof...(Args) == 0) && !std::is_scalar_v<A0>))>::type>
explicit vector(A0 &&a0, Args &&...args) {
static_assert((sizeof...(args) < N), "too many arguments");
size_t i = 0; // 当前写入data的索引
// 先构造第一个参数
construct_at_index(i, detail::decay(std::forward<A0>(a0)));
// 递归构造剩余参数
(construct_at_index(i, detail::decay(std::forward<Args>(args))), ...);
}
// const版本下标访问运算符,返回对应分量值
scalar_type const operator[](size_t i) const { return data[i]; }
// 非const版本下标访问,返回对应分量引用
scalar_type &operator[](size_t i) { return data[i]; }
// decay返回简化类型的常量引用
decay_type decay() const { return static_cast<const decay_type &>(*this); }
// 静态遍历0到N的函数模板,传入lambda等调用对象逐个调用
template <class Func>
static constexpr void iterate(Func &&f) {
detail::static_for<0, N>()(std::forward<Func>(f));
}
private:
// 单标量构造,写入data[i]后i自增
void construct_at_index(size_t &i, scalar_type arg) { data[i++] = arg; }
// 向量构造,从传入的其他向量arg复制分量,最多复制N个分量,i自增
template <typename Other, size_t Other_N>
void construct_at_index(size_t &i, const vector<Other, Other_N> &arg) {
constexpr auto count = std::min(N, Other_N);
detail::static_for<0, count>()([&](size_t j) { data[i++] = arg.data[j]; });
}
};
} // namespace swizzle
#include
typedef swizzle::vector<float, 3> vec3;
typedef swizzle::vector<float, 2> vec2;
int main() {
float a = 2.0f, b = 3.0f;
// 测试1:用标量 + vec2 构造 vec3
auto v1 = vec3(1.f, vec2(a, b));
printf("v1 = %f %f %f\n", v1.x, v1.y, v1.z);
// 测试2:访问和修改单个分量
v1.x = 5.f;
printf("v1 after x=5: %f %f %f\n", v1.x, v1.y, v1.z);
// 测试3:swizzle读取,例如 .xy .yx .xxx
vec2 v1_xy = v1.xy;
vec2 v1_yx = v1.yx;
vec3 v1_xxx = v1.xxx;
printf("v1.xy = %f %f\n", v1_xy.x, v1_xy.y);
printf("v1.yx = %f %f\n", v1_yx.x, v1_yx.y);
printf("v1.xxx = %f %f %f\n", v1_xxx.x, v1_xxx.y, v1_xxx.z);
// 测试4:swizzle赋值,给 .xy 赋一个 vec2
v1.xy = vec2(9.f, 8.f);
printf("v1 after xy=vec2(9,8): %f %f %f\n", v1.x, v1.y, v1.z);
// 测试5:用3个标量构造 vec3
vec3 v2(7.f, 6.f, 5.f);
printf("v2 = %f %f %f\n", v2.x, v2.y, v2.z);
// 测试6:用 vec3 构造 vec3(拷贝构造)
vec3 v3(v2);
printf("v3(copy of v2) = %f %f %f\n", v3.x, v3.y, v3.z);
// 测试7:用 vec3 构造 vec2(截断)
vec2 v4(v3);
printf("v4(from v3) = %f %f\n", v4.x, v4.y);
// 测试8:用标量构造 vec2,所有分量都相同
vec2 v5(4.f);
printf("v5(all 4.f) = %f %f\n", v5.x, v5.y);
// 测试9:复杂混合构造 vec3(标量, vec2, 标量)
vec3 v6(1.f, vec2(2.f, 3.f), 4.f);
printf("v6 = %f %f %f\n", v6.x, v6.y, v6.z);
// 测试10:swizzle赋值给复杂组合
v6.yzx = vec3(10.f, 11.f, 12.f);
printf("v6 after yzx=vec3(10,11,12): %f %f %f\n", v6.x, v6.y, v6.z);
return 0;
}
.x/.y/.z
、.r/.g/.b
、.s/.t/.p
及任意组合的混排访问。vec3(1.f, vec2(a,b))
构造三维向量。swizzler
类型包装混排成员,支持隐式转换回原向量。static_for
)在 Godbolt 上编译后的汇编输出。你希望理解这些 Godbolt 输出、static_for 的展开过程以及调试信息的含义,我们来逐步解析。你有如下模板:
template <size_t Begin, size_t End>
struct static_for {
template <class Func>
constexpr void operator()(Func&& f) {
f(Begin);
static_for<Begin + 1, End>()(std::forward<Func>(f));
}
};
这个 static_for
是一个编译期递归结构体调用,用来替代 C++14 之前没有 constexpr for
的限制,能在模板参数已知时静态展开循环。
你在 vector
的构造函数里用了它:
vector() {
iterate([this](size_t i) { data[i] = 0; });
}
这是你看到的 Godbolt 汇编片段的分析:
push rbp
mov rbp, rsp
sub rsp, 32
这些是标准的函数栈帧设置语句(用于调试符号、局部变量等):
push rbp / mov rbp, rsp
:保存旧的基地址并设置新帧。sub rsp, 32
:分配 32 字节栈空间用于局部变量。mov QWORD PTR [rbp-24], rdi
mov QWORD PTR [rbp-32], rsi
将传入的 lambda
对象保存到局部变量。你用的 lambda 是 [this](size_t i){ data[i] = 0; }
,它需要捕获 this
指针,所以是个状态 lambda,需要栈空间。
mov esi, 0
mov rdi, rax
call vector::vector(float)::{lambda(unsigned long)::operator()}
上面的指令相当于:
f(0); // static_for<0, 3>() 调用 lambda(0)
之后:
call static_for<1ul, 3ul>::operator()
表示编译器在递归调用下一层 static_for,即:
static_for<1, 3>()(f);
这会递归调用到 static_for<2,3>,再到 static_for<3,3>,最后终止。
你遇到的问题关键在于:
static_for
是模板递归,每层递归生成一个新的函数体,导致栈帧很多,看起来难以调试。
特别是 debug 模式下,你会看到多个重复的堆栈调用:
call lambda(0)
call lambda(1)
call lambda(2)
如果你开启了 -O0
优化选项(比如 Debug 配置),每个 lambda 调用都会展开成完整的函数调用,并不会被优化成 inline。
data
,用 std::fill
、std::iota
之类的会更简单。static_for
,可使用 constexpr if
+ C++17 fold expression
实现更清晰的静态循环。-O2
以上优化选项,这样会将 static_for 递归“完全展开”,去掉 call/ret。项目 | 说明 |
---|---|
static_for |
用模板递归模拟 for 循环,生成多个函数帧 |
f(Begin) |
实际调用 lambda 的入口,在 Godbolt 汇编中就是一个 call |
调试信息臃肿 | 是因为编译器保留了每一层递归帧(调试模式),可以通过优化级别改善 |
编译器行为分析 | GCC 和 Clang 的行为在 -O0 、-O2 等优化等级下会不同 |
vector<>
模板设计,核心在于利用编译期参数包(parameter pack)+ 折叠表达式简化并优化 vector
构造器中的初始化逻辑。下面我将详细解释你提供的代码内容、语法原理、以及它如何改善先前的 static_for 实现:
C++17 引入了 折叠表达式(fold expression),用于对可变参数包(parameter pack)进行自动归约计算。
((data[Ns] = 0), ...);
对于参数包 Ns... = 0, 1, 2
,展开为:
((data[0] = 0), (data[1] = 0), (data[2] = 0));
这就避免了复杂的递归结构或 static_for
。
vector<>
模板设计(带参数索引)你引用了如下设计:
template<typename T, size_t... Ns>
struct vector : public vector_base<T, Ns...>
{
using scalar_type = T;
scalar_type data[sizeof...(Ns)];
vector() {
((data[Ns] = 0), ...);
}
explicit vector(scalar_type s) {
((data[Ns] = s), ...);
}
template<typename A0, typename... Args>
explicit vector(A0&& a0, Args&&... args) {
size_t i = 0;
construct_at_index(i, std::forward<A0>(a0));
(construct_at_index(i, std::forward<Args>(args)), ...);
}
// 其他成员函数...
};
部分 | 作用 |
---|---|
vector |
继承自 vector_base ,模板参数中包含数据索引 0,1,2... |
((data[Ns] = 0), ...) |
使用 fold expression 初始化所有元素为 0 |
construct_at_index(i, arg) |
按顺序将每个参数放入 data[i++] 中 |
data[sizeof...(Ns)] |
自动根据 Ns 数量决定数组大小 |
static_for
相比的优势:特性 | static_for |
fold expression |
---|---|---|
写法 | 模板递归,容易嵌套复杂 | 一行表达式解决 |
编译速度 | 编译器生成多个递归实例 | 更快(编译期归约) |
可读性 | 中等 | 极高 |
调试难度 | 高(call stack 臃肿) | 非常低 |
vector<float, 0, 1, 2> v;
构造函数展开如下:
v.data[0] = 0;
v.data[1] = 0;
v.data[2] = 0;
vector<float, 0, 1, 2> v(5.0f);
展开:
v.data[0] = 5.0f;
v.data[1] = 5.0f;
v.data[2] = 5.0f;
template<typename A0, typename... Args>
explicit vector(A0&& a0, Args&&... args) {
size_t i = 0;
construct_at_index(i, std::forward<A0>(a0));
(construct_at_index(i, std::forward<Args>(args)), ...);
}
这个构造函数支持:
vector(1.f, 2.f, 3.f)
vector(vec2(1.f,2.f), 3.f)
construct_at_index
的行为:void construct_at_index(size_t& i, scalar_type s) {
data[i++] = s;
}
template <typename Other, size_t M>
void construct_at_index(size_t& i, const vector<Other, M>& v) {
constexpr auto count = std::min(N, M);
for (size_t j = 0; j < count; ++j) data[i++] = v[j];
}
支持嵌套 vector 的分解赋值。
优点 | 描述 |
---|---|
编译期性能提升 | 避免模板递归,直接展开发送给编译器 |
简洁 | 一行代码可完成初始化、赋值等 |
C++17 特色 | 利用 fold expressions 显著简化代码 |
可拓展性强 | 与 SFINAE、std::enable_if 、constexpr 配合使用 |
如你愿意,我还可以提供一个对比 Godbolt 链接,展示: |
static_for
vs fold expression
的汇编差异.xy
, .rgba
, .stpq
等 swizzle 写法,以方便向量数据的灵活组合、拆分、读写。下面我会详细解释你展示内容中的每一个关键点,并配图解读设计原理。
在 GLSL/HLSL 语言中,向量(如 vec3
)的分量可以用多种方式访问:
vec3 v = vec3(1.0, 2.0, 3.0);
v.xyz; // 正常访问
v.zyx; // 倒序访问
v.rg; // 用于颜色通道
v.st; // 用于纹理坐标
v.xzxx; // 可以重复分量
在 C++ 中复现这种 组合式成员访问与赋值语法,需要:
x
, y
, z
, w
等基本字段。xy
, yzx
, xxzz
等组合字段(最多支持 4 分量)。auto v2 = v.xy;
v.xy = vec2(1.0, 2.0);
v = vec3(1.0, vec2(2.0, 3.0));
template<typename T>
struct vector_base<T, 2> {
union {
T data[2];
struct { T x, y; };
struct { T s, t; };
struct { T u, v; };
};
};
.yx
、.xx
等)。swizzler<>
模板template<class vector_type, class T, size_t N, size_t... indices>
struct swizzler {
T data[N]; // 父向量数据
// 支持隐式转换为 vector_type
operator vector_type() const {
vector_type vec;
assign_across(vec, 0, indices...);
return vec;
}
// 支持赋值
swizzler &operator=(const vector_type &vec) {
assign_across(vec, 0, indices...);
return *this;
}
private:
template <typename... Idx>
void assign_across(vector_type &vec, size_t i, Idx... swizz_i) const {
((vec[i++] = data[swizz_i]), ...); // 使用 fold expression
}
};
indices...
控制 swizzle 的分量顺序。data[]
的组合访问读写。以 vector_base
为例:
union {
T data[3];
// 单个分量
typename swizzler_wrapper<0>::type x, r, s;
typename swizzler_wrapper<1>::type y, g, t;
typename swizzler_wrapper<2>::type z, b, p;
// 两个分量组合
typename swizzler_wrapper<0, 0>::type xx, rr, ss;
typename swizzler_wrapper<0, 1>::type xy, rg, st;
...
// 三个分量组合
typename swizzler_wrapper<0, 1, 2>::type xyz, rgb, stp;
typename swizzler_wrapper<2, 1, 0>::type zyx, bgr, pts;
// 四个分量组合
typename swizzler_wrapper<0, 0, 1, 1>::type xxyy, rrgg, sstt;
typename swizzler_wrapper<2, 2, 2, 2>::type zzzz, bbbb, pppp;
};
template <size_t... indices>
using swizzler_wrapper_factory = detail::swizzler<vector<T, sizeof...(indices)>, T, N, indices...>;
利用 sizeof...(indices)
判断 swizzle 的长度,自动生成合适的向量类型。
using vec3 = swizzle::vector<float, 3>;
vec3 a = {1.0f, 2.0f, 3.0f};
vec2 b = a.xy; // 自动转换成 vec2(1.0, 2.0)
a.yz = vec2(5.0, 6.0); // 自动赋值 a = (1.0, 5.0, 6.0)
vec3 c = a.zyx; // 倒序 swizzle
a.xz = vec2(9.0, 8.0);
特性 | 描述 |
---|---|
高度相似 GLSL | .xyz .stp .rgb 等皆可用 |
读写一体 | 支持向量赋值或读取子分量 |
自动生成 | 利用模板递归/元编程构建所有 swizzle 组合 |
无性能损耗 | 都是 inline 展开,编译器可高度优化 |
如果你感兴趣,我可以: |
Godbolt
上的编译优化;constexpr
/ SIMD
支持优化。.xy
, .rgba
, .zwxy
等)的 swizzler<>
设计与实现细节,我来为你系统地分析并用通俗语言逐步解释:swizzler<>
?swizzler
是一个模板结构,它作为 vector
的成员,模拟 GLSL 中的 .xy
, .rgba
等访问和赋值操作。
它的设计目标是:
vec2 p = v.xy;
v.xy = vec2(1, 2);
fold expressions
进行快速实现(C++17 特性)template<
typename vector_type, // 对应 vec2/vec3 等类型
typename scalar_type, // 例如 float、int
size_t N, // 父向量长度(如 vec4 是 4)
size_t... indices // 如 xy 为 <0, 1>
>
struct swizzler {
T data[N]; // 指向外部 vector 的 data[],不自己管理内存
indices...
是 swizzle 访问的具体位置,例如 .xy
对应 <0, 1>
,.zyx
对应 <2, 1, 0>
。.xx
)或混合组合。operator vector_type() const {
vector_type vec;
assign_across(vec, 0, indices...); // 赋值
return vec;
}
swizzler &operator=(const vector_type &vec) {
assign_across(vec, 0, indices...); // 反向赋值
return *this;
}
vec3 v = somevec.xy; // 隐式构造
somevec.xy = vec2(1, 2); // 隐式赋值
fold expression 是 C++17 的语法糖:
((vec[i++] = data[j]), ...); // 多参数一次性展开
用于批量处理参数包 indices...
,等价于:
vec[0] = data[indices[0]];
vec[1] = data[indices[1]];
...
也有反向赋值:
((data[j] = vec[i++]), ...); // 支持 swizzle 写入
vec4 other;
vec3 v = vec3(other.xy, other.z); // 错误!
C++ 编译器无法推导 other.xy
的类型,因为 swizzler 不是严格的 vector
类型,而是 swizzle proxy,需要显示“解包”或“转换”。
decay()
函数template <class T>
constexpr auto decay(T &&t) -> decltype(t.decay()) {
return t.decay(); // swizzler -> vector
}
template <class T>
constexpr typename std::enable_if<std::is_scalar<...>::value, T>::type
decay(T &&t) {
return t; // scalar 保持不变
}
vec3 v = vec3(decay(other.xy), other.z); // 正确
swizzler
和 vector
均可通过 decay()
转换为原生类型。vec4 a = vec4(1.0, 2.0, 3.0, 4.0);
// swizzle读取
vec2 v2 = a.xy; // v2 = (1, 2)
vec3 v3 = a.zyx; // v3 = (3, 2, 1)
vec4 v4 = a.zzzz; // v4 = (3, 3, 3, 3)
// swizzle赋值
a.yx = vec2(10, 20); // a = (20, 10, 3, 4)
a.xz = vec2(99, 77); // a = (99, 10, 77, 4)
特性 | 说明 |
---|---|
operator vector_type() |
支持隐式转换为 vecN |
operator= |
支持 swizzle 的赋值 |
assign_across |
使用 fold 表达式统一展开处理 |
decay() |
显式处理 swizzle -> vector 转换 |
可组合 | 支持链式构造如 .xyz , .xzy , .rrgg , .stpq |
如果你还想要: |
vector
类型时,遇到的 泛型函数类型推导问题,尤其是在处理 swizzled 向量(例如 .xzx
、.zyx
)时的 dot()
点积运算。我们来逐步理解:我们定义了一个泛型 dot()
函数:
template<typename T, size_t... Ns>
T dot(const vector<T, Ns...>& a, const vector<T, Ns...>& b);
并尝试如下调用:
vec3 v = vec3(1, 0, 0);
float n = dot(v.xzx, v.zyx); // 编译错误
错误:dot()
模板不能推导类型
原因是 v.xzx
和 v.zyx
是 swizzler
类型,而不是 vector
类型。
虽然 swizzler
可以隐式转换成 vector
,但 模板参数推导不会考虑隐式转换!
float n = dot<float, 0, 1, 2>(v.xzx, v.zyx); // 显式指明类型和参数包
缺点:对用户极其不友好,完全不泛化。
.decay()
或构造新的 vectorfloat n = dot(vec3(v.xzx), vec3(v.zyx)); // OK!
或更底层版本:
float n = dot(decay(v.xzx), decay(v.zyx)); // OK
好处:让
dot()
使用明确的vector
类型参与模板推导。
enable_if
,实现自动接受 swizzler:template<typename T, size_t... Ns>
T dot(const vector<T, Ns...>& a, const vector<T, Ns...>& b);
// 自动 decay 的重载版本
template<typename A, typename B>
auto dot(const A& a, const B& b)
-> decltype(dot(decay(a), decay(b)))
{
return dot(decay(a), decay(b));
}
这样即使传入 swizzler,模板推导也不会失败。
template <typename T, size_t... Ns>
T dot(const vector<T, Ns...>& a, const vector<T, Ns...>& b) {
T result = T{};
((result += a[Ns] * b[Ns]), ...); // C++17 fold expression
return result;
}
vec3 v = vec3(1, 0, 0);
// 这三个写法都可行(推荐最后两种)
float a = dot(vec3(v.xzx), vec3(v.zyx));
float b = dot(decay(v.xzx), decay(v.zyx));
float c = dot(v.xzx, v.zyx); // 若引入泛型 decay 支持(SFINAE)
问题 | 模板无法从 swizzler 类型中推导出 vector 类型 |
---|---|
原因 | 模板类型推导不会考虑隐式转换 |
方法① | 显式提供模板参数 → 冗长,不推荐 |
方法② | 使用 decay() 或 vec 构造函数包装 → 推荐 |
方法③ | 写重载模板,让其自动调用 decay() → 最推荐 |
当写下类似:
float n = dot(v.xzx, v.zyx); // 无法推导模板参数
原因:
dot()
是模板函数,期待 vector
类型;swizzler
类型,它虽能隐式转换为 vector
,但:
C++ 模板推导不会考虑隐式转换!
friend
定义在类中template<typename T, size_t... Ns>
struct vector {
// ...
// 将 dot 作为 inline friend 函数定义在 vector 内部
friend T dot(const vector& a, const vector& b) {
T result{};
((result += a[Ns] * b[Ns]), ...); // C++17 fold expression
return result;
}
};
优势 | 说明 |
---|---|
类型不需要推导 | dot() 不再依赖模板推导,因为 vector 的类型已知 |
更像类方法 | 虽然是 friend ,但用法接近成员函数 |
支持 ADL(Argument-Dependent Lookup) | 可以像全局函数一样使用 dot(a, b) ,不必限定命名空间 |
ARE WE DONE?
虽然 vector
间运算支持得很好,但 很多着色器代码也会使用标量操作(float 之间的函数):
float opS(float d1, float d2) {
return max(-d2, d1);
}
vector
的重载:friend vector max(const vector& a, const vector& b) {
return vector((a.data[Ns] < b.data[Ns] ? a.data[Ns] : b.data[Ns])...);
}
但是如果写:
float a = 1.0f, b = 2.0f;
float c = max(-b, a); // 编译失败
因为我们没定义 scalar 版本的
max()
!
在同一命名空间中,重载标量函数:
template <typename T>
T max(const T& a, const T& b) {
return (a > b) ? a : b;
}
这样用户既可以写:
float r = max(-b, a); // 标量 max
vec3 r2 = max(v1, v2); // 向量 max
只要命名空间正确,编译器会选对的函数!
问题 | 解决方式 |
---|---|
向量函数类型推导失败 | 把函数作为 vector 内部的 friend inline 定义 |
缺少标量函数支持 | 添加标量版本 max/min/abs 等函数 |
Swizzle 类型转 vector | 使用 decay() 或构造函数包装 |
vector
内部作为 friend
。float max(float, float)
、float step(float, float)
等。swizzler
的 .decay()
方法或 vector
构造函数兼容性。smoothstep()
)在 C++ 模拟中的类型歧义问题,尤其是处理 标量和向量混用 的麻烦。这确实是构建类 GLSL 语言系统时必须精心处理的问题。smoothstep(0, 1, v.xyz); // 编译器报错
为什么报错?
0
和 1
是整型字面值 → int
v.xyz
是 vec3
smoothstep(scalar_type, scalar_type, const vector&)
无法匹配混合类型promote_to_vec::type
// 推导出多个类型的“最通用向量类型”
template <typename... Ts>
struct promote_to_vec;
// 单个标量 → 对应最小向量(如 float → vec1)
template <>
struct promote_to_vec<float> {
using type = vec1<float>;
};
// 已是 vector,直接保留
template <typename T, size_t N>
struct promote_to_vec<vector<T, N>> {
using type = vector<T, N>;
};
// 混合情况:float + vec3 → 提升为 vec3
template <>
struct promote_to_vec<float, vector<float, 3>> {
using type = vector<float, 3>;
};
// 泛化处理(使用 common_type 技术)
template <typename... Ts>
using promote_to_vec_t = typename promote_to_vec<Ts...>::type;
// 示例:GLSL 风格 smoothstep 实现
template <typename A, typename B, typename C>
auto smoothstep(A edge0, B edge1, C x) {
using vec_t = promote_to_vec_t<A, B, C>; // 推导最终 vector 类型
return clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); // 假设定义了 clamp
}
技术 | 作用 |
---|---|
std::is_convertible |
判断 vector 是否可看作标量,用于类型推广 |
std::common_type_t<> |
合并不同类型到一个最兼容类型 |
decltype() + swizzler 的 .decay() |
解包 swizzle 类型获得原始 vector 类型 |
模板折叠参数 + traits(type traits) | 实现多个参数推导逻辑 |
vec3 v(0.5f, 0.6f, 0.7f);
auto s1 = smoothstep(0.f, 1.f, v); // ok
auto s2 = smoothstep(0, 1, v.xyz); // ok now!
auto s3 = smoothstep(0.2f, 0.8f, v.xy); // vec2
auto s4 = smoothstep(0.1, 1.0, vec1<float>(0.5)); // vec1
问题 | 解决方法 |
---|---|
标量与向量类型混用不推导 | 自定义 promote_to_vec<> |
smoothstep(0, 1, vec) 报错 |
模板函数使用推广后的通用类型 |
swizzler 不易识别 | 提供 .decay() 或隐式转换操作符 |
max
, smoothstep
等)过程中所面临的 模板推导和泛化调用问题 的进一步解决方法。max
, min
, smoothstep
等函数,不管传的是标量(float
、int
)还是向量(vec2
, vec3
, swizzler等)vec3 v = vec3(1, 0, 0); dot(v.xyx, v.yyy)
会报错static
成员放进 struct builtin_func_lib
里template<template<class, size_t...> class vector, class T, size_t... Ns>
struct builtin_func_lib {
static vector<T, Ns...> max(const vector<T, Ns...>& a, const vector<T, Ns...>& b) {
return vector<T, Ns...>((a.data[Ns] > b.data[Ns] ? a.data[Ns] : b.data[Ns])...);
}
};
不再是 friend
,而是明确在类里定义为 static
。
func(...)
用一个统一的调用接口 func(...)
来调用正确版本的 max
, min
, smoothstep
等:
template<class... Args>
inline auto func(Args&&... args) ->
decltype(decay(
typename promote_to_vec<Args...>::type::
func(std::forward<Args>(args)...)))
{
return typename promote_to_vec<Args...>::type::
func(std::forward<Args>(args)...);
}
这个函数:
vecN
swizzler
, scalar
, vector
的组合decay()
(如果是 swizzler
,转换成真正的 vector)#define MAKE_LIB_FUNC(NAME) \
template<class... Args> \
inline auto NAME(Args&&... args) { \
return func<decltype(&builtin_func_lib<...>::NAME)>(std::forward<Args>(args)...); \
}
你可以生成一批标准库函数:
MAKE_LIB_FUNC(abs)
MAKE_LIB_FUNC(sign)
MAKE_LIB_FUNC(floor)
MAKE_LIB_FUNC(trunc)
MAKE_LIB_FUNC(ceil)
MAKE_LIB_FUNC(fract)
MAKE_LIB_FUNC(mod)
MAKE_LIB_FUNC(min)
MAKE_LIB_FUNC(max)
MAKE_LIB_FUNC(clamp)
MAKE_LIB_FUNC(mix)
MAKE_LIB_FUNC(step)
MAKE_LIB_FUNC(smoothstep)
vec3 v(0.3f, 0.5f, 0.9f);
auto result = max(0.6f, v.xy);
// 自动提升为 vec2 并调用 vector<>::max()
等价于:
promote_to_vec<float, vec2>::type::max(0.6f, v.xy);
问题 | 解决方法 |
---|---|
标量/向量混用导致推导失败 | 使用 promote_to_vec 推导 vector 类型 |
多个函数模板重载重复工作量大 | 用 builtin_func_lib + 宏定义生成 |
推导不考虑隐式转换 | 手动 decay swizzler,自动转换 |
friend 不易统一管理 |
用 static + 统一入口函数转发 |
matrix<>
,配合之前实现的 vector<>
。这正是构建类似于 GLSL/GLM 数学库的核心内容。我们已经有一个通用 vector
类型,用来表示像 vec2
, vec3
, vec4
这类的 向量,同时通过模板参数 size_t...
来选择对应的 swizzle 索引。
你现在要构建的是一个通用 matrix<>
模板,用来支持如下各种类型:
mat2 // 2x2
mat3 // 3x3
mat2x3 // 2 columns, 3 rows → 2x3
indices_pack
template <size_t...> struct indices_pack {};
这是一个编译期索引列表,例如:
indices_pack<0, 1> // 2 元素
indices_pack<0, 1, 2> // 3 元素
template<
typename scalar_type,
template<typename, size_t...> class vector_type,
typename ColumnsPack,
typename RowsPack
>
struct matrix;
这里:
scalar_type
:元素类型(如 float
, int
)vector_type
:我们之前的 vector<>
模板ColumnsPack
, RowsPack
:分别表示列和行索引(不是具体数量,而是模板包)template<
typename scalar_type,
template<typename, size_t...> class vector_type,
size_t... Columns,
size_t... Rows
>
struct matrix<scalar_type, vector_type, indices_pack<Columns...>, indices_pack<Rows...>> {
static constexpr auto N = sizeof...(Columns); // 列数
static constexpr auto M = sizeof...(Rows); // 行数
using column_type = vector_type<scalar_type, Rows...>;
using row_type = vector_type<scalar_type, Columns...>;
column_type data[N]; // N 列,每列是一个向量
};
using vec2 = vector<float, 0, 1>; // 2D 向量
using vec3 = vector<float, 0, 1, 2>; // 3D 向量
using mat2 = matrix<float, vector, indices_pack<0, 1>, indices_pack<0, 1>>;
// → mat2: 2 列,每列是 vec2 (float[2])
// → 所以是 2x2 矩阵(列主存储)
using mat3 = matrix<float, vector, indices_pack<0, 1, 2>, indices_pack<0, 1, 2>>;
// → 3x3 矩阵
using mat2x3 = matrix<float, vector, indices_pack<0, 1>, indices_pack<0, 1, 2>>;
// → 2 列 × 3 行
特性 | 描述 |
---|---|
模板完全编译期静态分布 | 无需运行时维度信息 |
vector<> 为构建单元 |
每列是一个 vector |
索引辅助类型 indices_pack |
模拟行列维度 |
类似 GLSL / GLM 矩阵模型 | 列主存储,支持 mat2 , mat3 , mat4 , mat2x3 等 |
易于扩展矩阵乘法、转置等 | 后续可加 operator 重载、函数库 |
matrix<>
类型的构造函数(Constructors)设计,以及如何支持矩阵相关的运算(如乘法),并对比现有方案如 GLM、Clang 向量扩展、CXXSwizzle 的优劣。我来详细解释各段内容,并给出可运行的构造函数实现思路。
matrix<>
构造函数设计详解matrix() = default; // 零初始化所有 data
默认构造函数什么都不做,但你可以显式清零:
for (auto& col : data)
for (auto& v : col.data)
v = 0;
explicit matrix(scalar_type s) {
((data[Rows][Rows] = s), ...);
}
这个表达式有点不清晰,其实真正目标是:
将矩阵对角线上的元素设为
s
,其余为0
,构造单位矩阵s * Identity
一个实际可编译写法如下(假设是 3x3):
explicit matrix(scalar_type s) {
for (size_t r = 0; r < M; ++r)
for (size_t c = 0; c < N; ++c)
data[c][r] = (r == c ? s : 0);
}
或者用 fold expression 编译期展开:
// C++17 fold expression based diagonal init
template<size_t... Rs>
static constexpr void fill_diagonal(matrix& mat, scalar_type s, std::index_sequence<Rs...>) {
((mat.data[Rs][Rs] = s), ...); // 对角元素赋值
}
template<typename... Args>
explicit matrix(Args&&... args) {
size_t i = 0;
(construct_at_index(i, decay(std::forward<Args>(args))), ...);
}
你应该已经熟悉了这模式:从标量或向量构造,遍历填入 data:
void construct_at_index(size_t& i, scalar_type arg) {
size_t col = i / M;
size_t row = i % M;
data[col][row] = arg;
++i;
}
template<typename OtherT, size_t OtherN>
void construct_at_index(size_t& i, const vector<OtherT, OtherN>& v) {
for (size_t j = 0; j < OtherN; ++j)
construct_at_index(i, v[j]);
}
大多数操作(如 +
, -
, /
, ==
)都可以像 vector<>
一样写模板:
friend matrix operator+(const matrix& a, const matrix& b) {
matrix res;
for (size_t c = 0; c < N; ++c)
res.data[c] = a.data[c] + b.data[c];
return res;
}
矩阵乘法不能简单逐元素相乘。形式为:
mat3 = mat3 * mat3;
vec3 = mat3 * vec3;
可写如下乘法函数:
friend matrix operator*(const matrix& a, const matrix& b) {
matrix result(0.0f);
for (size_t i = 0; i < N; ++i)
for (size_t j = 0; j < M; ++j)
for (size_t k = 0; k < N; ++k)
result.data[i][j] += a.data[k][j] * b.data[i][k];
return result;
}
库 | 优点 | 缺点 |
---|---|---|
Clang ext_vector_type(n) |
支持 swizzle | 初始化非常受限 |
GLM | 非常广泛使用,兼容 GLSL | 巨量预处理器宏,难以调试和扩展 |
CXXSwizzle | 全特性支持,GLSL风格 | Debug 模式运行非常慢 |
你现在实现的 vector<> + matrix<> 模板,比上述方案在: |
你可以继续为 matrix<>
添加:
transpose()
转置函数determinant()
行列式(小矩阵)inverse()
逆矩阵(可选)mat * vec
运算print()
/debug_dump()
输出函数identity()
静态构造简单总结和分析:
示例 | 240x240 px (FPS) | 120x120 px (FPS) |
---|---|---|
Hello World (CPU) | 85.62 / 100.27 / 166.77 / 468.49 | |
Planet (CPU) | 1.92 / 7.30 / 0.83 / 3.34 | |
Clouds (CPU) | 2.54 / 9.63 / 2.44 / 9.64 | |
Vinyl Turntable (CPU) | 8.44 / 28.11 / 2.94 / 12.82 |
这些数据可能是不同设备对应的FPS(不同设备下多组数据),越高说明性能越好。