ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Slang 语言参考:向量与矩阵类型(Vector/Matrix Types)完整指南

Slang 语言参考:向量与矩阵类型(Vector/Matrix Types)完整指南 Slang 语言参考向量与矩阵类型Vector/Matrix Types完整指南【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang本篇指南以 Slang 官方语言参考中《Vector and Matrix Types》为核心系统讲解vectorT, N与matrixT, R, C两种内建数学类型的完整语法从类型参数与默认值、元素访问与 swizzle 操作、运算符语义到标准类型别名、内存布局与对齐规则并深入剖析矩阵在 OpenGL/Vulkan/Metal/WebGPU 等目标的行列主序约定差异。读完本文你将能在 Slang 着色器中正确声明、索引、运算向量与矩阵并安全地在不同图形 API 目标之间移植矩阵代码。向量类型vectorT, N表示由N个类型为T的元素组成的向量其中T是基础标量类型布尔、整型、浮点型等N是实例化期常量整数取值范围为[1, 4]表示元素个数。为了向后兼容T和N的默认值分别为float和4。也就是说直接写vector等价于vectorfloat, 4。从源码来看该类型在source/slang/core.meta.slang中定义第 2283-2303 行其默认参数与文档完全一致/// category math_types Math types /// An N component vector with elements of type T. __genericT float, let N : int 4 __magic_type(VectorExpressionType) struct vector : IRWArrayT { /// The element type of the vector typedef T Element; /// Initialize a vector where all elements have the same scalar value. __implicit_conversion($(kConversionCost_ScalarToVector)) __intrinsic_op($(kIROp_MakeVectorFromScalar)) __init(T value); [ForceInline] int getCount() { return N; } }这里__magic_type(VectorExpressionType)将该结构映射到编译器内部的 AST 类型节点见 source/slang/slang-ast-type.hstruct vector : IRWArrayT表明向量表现为一个可随机访问、可写RW的T元素数组。此外标量到向量的__init(T value)支持标量广播初始化——用一个标量填充所有分量kConversionCost_ScalarToVector是隐式转换的成本标记说明标量可以隐式转换为向量。元素访问向量元素可以通过以下两种方式访问下标运算符[]索引0表示第一个元素成员访问运算符.元素按索引依次命名为x、y、z、w分别对应索引0、1、2、3。vectorint, 4 v { 1, 2, 3, 4 }; int tmp; tmp v[0]; // tmp is 1 tmp v.w; // tmp is 4 v[1] 9; // v is { 1, 9, 3, 4 };注意下标和成员名都可用于读与写上面v[1] 9演示了通过下标写入元素。多元素访问Swizzle在成员访问运算符后指定两个或更多元素名可以一次性引用多个元素。这可用于提取多个元素结果类型为以选中元素个数为长度的新向量同一个元素可以被多次指定例如v.xww用向量为多个元素赋值右侧必须是长度与选中元素个数相等的向量且被赋值的元素必须唯一不能出现v.xx ...这样的重复目标。vectorint, 4 v { 1, 2, 3, 4 }; int2 tmp2; int3 tmp3; tmp2 v.xy; // tmp2 is { 1, 2 } tmp3 v.xww; // tmp3 is { 1, 4, 4 } v.xz vectorint, 2(-1, -3); // v becomes { -1, 2, -3, 4 }Swizzle 是着色器语言中最常用的便捷特性之一v.xy常用于取二维坐标v.xww这类重复提取可用于构建新的向量。反向自动微分相关的测试也大量依赖它例如 tests/autodiff/reverse-matrix-swizzle.slang 通过_XY成员 swizzle 读取矩阵元素来完成可微的矩阵运算。运算符一元算术运算符应用于向量时作用于所有元素。vectorint, 4 v { 1, 2, 3, 4 }; vectorint, 4 tmp; tmp -v; // tmp is { -1, -2, -3, -4 };标量与向量的二元算术运算标量操作数作用于向量的每一个元素广播语义标量既可以出现在右侧也可以出现在左侧。vectorint, 4 v { 1, 2, 3, 4 }; vectorint, 4 tmp; tmp v - 1; // tmp is { 0, 1, 2, 3 }; tmp 4 - v; // tmp is { 4, 3, 2, 1 };标量与向量的复合赋值右侧为标量时赋值作用于所有元素。vectorint, 4 v { 1, 2, 3, 4 }; v 1; // v becomes { 2, 3, 4, 5 }; v 42; // v becomes { 42, 42, 42, 42 };两个等长向量的二元运算算术、赋值和比较运算符均按元素逐分量element-wise应用。vectorint, 4 v1 { 1, 2, 3, 4 }; vectorint, 4 v2 { 5, 6, 7, 8 }; vectorint, 4 tmp; tmp v1; // tmp is { 1, 2, 3, 4 }; tmp v1 v2; // tmp is { 6, 8, 10, 12 }; tmp v1 * v2; // tmp is { 5, 12, 21, 32 }; vectorbool, 4 cmpResult; cmpResult (v1 vectorint, 4(1, 3, 2, 4)); // cmpResult is { true, false, false, true } v1 - v2; // v1 becomes { -4, -4, -4, -4 };注意比较运算的结果是vectorbool, 4——逐元素比较每个分量各自得到布尔结果。若需要全部相等这类聚合判断可以配合all()、any()等内建函数使用在core.meta.slang中vectorT,N : IInteger扩展的equals即通过all(__vectorEql(this, other))实现。标准类型别名Slang 为所有基础标量类型的 1 到 4 维向量提供了类型别名。别名命名为基础类型名N其中基础类型名是某个基础类型N是向量长度。float4 v { 1.0f, 2.0f, 3.0f, 4.0f }; // vectorfloat, 4 int32_t2 i2 { 1, 2 }; // vectorint, 2 bool3 b3 { true, false, false }; // vectorbool, 3因此float4、int2、uint3、bool4、half4等都是合法类型它们与显式书写vectorT, N完全等价。内存布局向量的内存布局为N个连续存放的T值没有任何填充padding。向量的对齐方式是目标平台定义的target-definedvectorT, N的对齐至少是T的对齐至多不超过N倍T的对齐。因此当向量被嵌入结构体或常量缓冲区时其占用的字节数由N * sizeof(T)决定但起始地址的偏移量取决于目标平台的向量对齐规则。这一点在编写需要精确内存布局的跨目标数据交换代码时务必注意。矩阵类型matrixT, R, C表示一个R×C的矩阵元素类型为T其中T是基础标量类型R是实例化期常量整数取值范围[1, 4]表示行数C是实例化期常量整数取值范围[1, 4]表示列数。与向量类似T、R、C的默认值分别为float、4、4即裸写matrix等价于matrixfloat, 4, 4这是为了向后兼容。从源码 source/slang/core.meta.slang 可以看到矩阵类型实际上还有第四个泛型参数L内存布局模式并定义了MatrixLayoutMode枚举//hidden: // The in-memory element order of a matrix. An enum rather than an int so that an // unresolved layout is still recognizable as a generic argument, where it sits beside // the row and column counts. enum MatrixLayoutMode : int { Unknown $(SLANG_MATRIX_LAYOUT_MODE_UNKNOWN), RowMajor $(SLANG_MATRIX_LAYOUT_ROW_MAJOR), ColumnMajor $(SLANG_MATRIX_LAYOUT_COLUMN_MAJOR), } //public: /// A matrix with R rows and C columns, with elements of type T. __genericT float, let R : int 4, let C : int 4, let L : MatrixLayoutMode MatrixLayoutMode.Unknown __magic_type(MatrixExpressionType) struct matrix : IRWArrayvectorT,C { __intrinsic_op($(kIROp_MakeMatrixFromScalar)) __implicit_conversion($(kConversionCost_ScalarToMatrix)) __init(T val); __intrinsic_op(0) __init(This value); [ForceInline] int getCount() { return R; } }struct matrix : IRWArrayvectorT, C说明矩阵在语言层面被建模为行向量数组R个vectorT, C这与下文Slang 视矩阵为行向量集合row-major的语义一致。__magic_type(MatrixExpressionType)对应的 AST 节点见 source/slang/slang-ast-type.h还额外维护了rowType行类型缓存并提供getElementType()、getRowCount()、getColumnCount()、getLayout()等访问器。行与元素访问矩阵的一行通过下标运算符[]访问索引0表示第一行。行内元素可通过以下方式访问下标运算符[]索引0表示第一列成员访问运算符.列按索引依次命名为x、y、z、w。matrixint, 3, 4 v { 1, 2, 3, 4, // row index 0 5, 6, 7, 8, // row index 1 9, 10, 11, 12 // row index 2 }; int tmp1 v[1][2]; // tmp1 is 7 (row index 1, column index 2) int tmp2 v[1].w; // tmp2 is 8 (row index 1, column index 3) int4 tmp3 v[2]; // tmp3 is { 9, 10, 11, 12 } int2 tmp4 v[0].yx; // tmp4 is { 2, 1 }可见v[row][column]是基本访问方式而v[2]直接取出整行得到一个向量v[0].yx则对行向量再应用 swizzle实现任意行列组合的便捷读取。这也印证了矩阵是行的数组这一建模m[i]的返回类型是vectorT, C。运算符一元运算符应用于矩阵时作用于所有元素例如-m取所有元素的相反数。二元运算符按元素逐分量应用左右操作数必须是同维度的矩阵例如m1 m2要求两者都是R×C。矩阵乘法通过mul()函数执行基本形式有三种matrix/matrix 形式mul(m1, m2)m1为M×N矩阵m2为N×P矩阵结果为M×P矩阵vector/matrix 形式mul(v, m)v为长度N的向量m为N×P矩阵结果为长度P的向量。此时v被解释为行向量即1×N矩阵matrix/vector 形式mul(m, v)m为M×N矩阵v为长度N的向量结果为长度M的向量。此时v被解释为列向量即N×1矩阵。// matrix/vector 形式M×N 矩阵乘 N 维列向量得到 M 维向量 float4 result mul(float4x3 m, float3 v);源码层面mul()的各目标实现在 source/slang/hlsl.meta.slang 中通过__target_switch分派到不同后端GLSL/Metal/WGSL 直接映射为($1 * $0)注意操作数顺序已交换见下文目标约定HLSL 映射为内建mulSPIR-V 使用OpVectorTimesMatrix其余目标如 CPU/CUDA则回退到显式循环累加实现。Remark 1运算符*执行的是逐元素乘法。只有当确实需要两个同尺寸矩阵的逐元素相乘时才应使用*。例如m1 * m2两个4×4矩阵得到的是对应元素相乘的结果而不是数学上的矩阵积。Remark 2*的语义与 GLSL 不同——在 GLSL 中*对矩阵执行矩阵乘法。因此从 GLSL 移植代码到 Slang 时必须把用*表示的矩阵乘法替换为mul()调用否则会得到错误的逐元素乘积结果。标准类型别名Slang 为基础标量类型的所有行数、列数在 1 到 4 之间的矩阵提供了类型别名。别名命名为基础类型名RxC其中基础类型名是某个基础类型R是行数C是列数。// matrixfloat, 4, 3 float4x3 m { 1.1f, 1.2f, 1.3f, 2.1f, 2.2f, 2.3f, 3.1f, 3.2f, 3.3f, 4.1f, 4.2f, 4.3f, };例如float4x4matrixfloat, 4, 4、float3x4、int2x2、bool1x3等。初始化时元素按行优先顺序书写上面示例中每行三个元素依次排列成 4 行。从 source/slang/core.meta.slang 可以看到别名对应的构造器由元编程循环为R×C组合逐一生成既支持R*C个标量参数的初始化也支持按行向量初始化R个vectorT, C。内存布局行主序与列主序矩阵类型同时支持**行主序row-major与列主序column-major**两种内存布局。实现可以通过命令行标志或 API 选项来控制矩阵使用的默认布局。行主序row-major矩阵在内存中等价于R个vectorT, C组成的数组列主序column-major矩阵在内存中等价于其转置的行主序布局即等价于C个vectorT, R组成的数组。RemarkSlang 目前不支持HLSL 的row_major/column_major修饰符来为单个声明单独指定布局。布局的控制是全局默认层面的通过编译选项或 API。在编译器实现中矩阵布局模式由CompilerOptionSet管理getMatrixLayoutMode()在未显式设置时默认返回行主序见 source/slang/slang-compiler-options.cpp。对应的命令行选项定义在 source/slang/slang-options.cpp命令行选项说明-matrix-layout-row-major将默认矩阵布局设置为行主序-matrix-layout-column-major将默认矩阵布局设置为列主序例如通过slangc或测试工具指定-xslang -matrix-layout-column-major即可全局切换到列主序。仓库测试 tests/language-feature/matrix-select.slang 就在 DX12、Vulkan、Metal、WebGPU 四个后端上分别用-matrix-layout-row-major与-matrix-layout-column-major编译同一份着色器做对比验证可以直接作为这两种布局行为差异的参考用例。对齐规则矩阵的对齐方式是目标平台指定的。一般来说矩阵的对齐至少是元素的对齐至多不超过矩阵大小向上取整到下一个 2 的幂。例如一个3×3的float矩阵大小为 36 字节其对齐可能被提升到 64 字节36 向上取整到 2 的幂具体取决于目标平台。OpenGL、Vulkan、Metal 与 WebGPU 目标的重要约定Slang 将矩阵视为行向量集合行主序这与 HLSL 及通常的数学约定一致。然而OpenGL、Vulkan、Metal、WebGPU 等众多图形 API 却将矩阵视为列向量集合列主序。这一根本差异会在跨目标移植时造成初始化顺序、类型命名与元素索引上的混乱差异总结如下方面Slang 和 HLSLGLSL、SPIR-V、MSL、WGSL初始化元素顺序行主序Row major列主序Column major3 行 × 4 列的 float 类型float3x4mat4x3或类似命名元素访问m[row][column]m[column][row]关键点在于为了在目标代码中仍能高效地使用下标运算符[]访问元素Slang 在这些目标上会把列重新解释为行、把行重新解释为列。也就是说一个 Slang 的float3x4矩阵类型在 GLSL 中会映射为mat3x4类型注意行与列的命名随重新解释发生对调行主序与列主序的内存布局也遵循同样的重新解释。其他编译器在把 HLSL 编译到 SPIR-V 时也会执行类似的重新解释。这种重新解释最显著的结果是矩阵乘法在目标代码中操作数顺序被交换。例如Slang 源码float4 doMatMul(float4x3 m, float3 v) { return mul(m, v); }翻译后的 GLSL 目标代码vec4 doMatMul_0(mat4x3 m_0, vec3 v_0) { return (((v_0) * (m_0))); }可以看到Slang 中的mul(m, v)M×N矩阵乘N维列向量在 GLSL 中变成了v_0 * m_0GLSL 中向量左乘矩阵即按行向量处理等价于数学上的列向量右乘。这正对应 source/slang/hlsl.meta.slang 中 GLSL/Metal/WGSL 分支的实现__intrinsic_asm ($1 * $0)—— 操作数顺序相对于源码被交换。因此在 Slang 源码层面请始终使用mul(m, v)这类符合数学直觉的写法不要依赖目标语言中*的方向语义阅读或调试生成的目标代码GLSL/SPIR-V/MSL/WGSL时若看到矩阵乘法的操作数顺序与源码不同这是正常的重新解释结果不要误判为编译错误。总结向量与矩阵是着色器编程最基础也最常用的数学类型。Slang 通过vectorT, N与matrixT, R, C提供了完整的声明、索引、swizzle、运算符与mul()矩阵乘法能力并借助float4、float3x4等别名与 HLSL 风格保持一致同时通过默认行主序建模与目标端的行列重新解释在 GLSL/SPIR-V/MSL/WGSL 等列主序后端上维持了正确的语义。需要特别牢记的三件事是向量/矩阵的*是逐元素乘法矩阵乘法一律使用mul()默认矩阵布局为行主序可通过-matrix-layout-row-major/-matrix-layout-column-major全局切换但不支持 HLSL 式的逐声明row_major/column_major修饰符在列主序的图形 API 目标上Slang 会重新解释行列并交换矩阵乘法操作数移植与调试目标代码时需留意这一约定。如需进一步了解相关主题可继续阅读 类型总览含数组、结构体、元组、可选类型等、基础标量类型、泛型 以及目标平台兼容性说明。【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

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