Skip to content

Shader 构建与打包

MGFX 会把编译后的 shader VCS 文件嵌入到 lua/mgfx/cl_mgfx_shaderpack.lua。运行时 shaderpack 会被挂载成临时 GMA,然后由 cl_mgfx_materials.lua 创建 screenspace_general 材质使用。

这页记录的是 shader 构建、参数布局和 Garry's Mod/Source 里的实际限制。改 shader、gradient LUT、alpha 或 shaderpack 时,先看这里。

源文件结构

text
shadersrc/mgfx/
  build.py
  build_shaders.bat
  compile_shader_list.txt
  src/
    *.hlsl
    shaders/fxc/*.vcs

tools/sdk_screenspace_shaders/
  shadersrc/bin/ShaderCompile.exe

tools/sdk_screenspace_shaders 只在构建时使用,GMA 打包时必须忽略。

重新构建

在 addon 根目录执行:

powershell
python .\shadersrc\mgfx\build.py

构建脚本会:

  1. 调用 build_shaders.bat
  2. 编译 compile_shader_list.txt 中所有 mgfx_* shader。
  3. 把生成的 src/shaders/fxc/*.vcs 打包进内存 GMA。
  4. 把 base64 shaderpack 写入 lua/mgfx/cl_mgfx_shaderpack.lua

shaderpack 版本是 Unix timestamp:

lua
local shaderPack = include("mgfx/cl_mgfx_shaderpack.lua")
print(shaderPack.Version)

安装 devtools 后,可在游戏中用 mgfx_status 确认当前挂载版本。

诊断开关

安装 mgfx/devtools.lua@lux/mgfx/devtools 后,开发时可以打开:

text
mgfx_profile 1
mgfx_draw_counts 1

正式测 FPS 时要关闭:

text
mgfx_profile 0
mgfx_draw_counts 0

每族 draw counter 默认关闭。复杂 immediate UI 中,几百次绘制的计数本身也可能变成可测开销。

GMod Shader 规则

这些不是风格建议,是实际踩坑后的硬规则:

  • 对生成材质不要直接依赖 DrawTexturedRectUV,除非已经按稳定的 $basetexture mapping size 处理 UV 修正。热路径可以使用修正后的 DrawTexturedRectUV,但不能在绘制期间改变作为 mapping carrier 的基底纹理。
  • 抗锯齿需要最终屏幕空间尺寸或 UV derivative。不要把逻辑尺寸当成 shader AA 所需的物理像素尺寸。
  • 进入 SDF 计算前先 clamp radius 和 chamfer cuts。
  • 不要随手加 data texture 参数路径。被移除的 batch prototype 已经证明,上传和调度成本可能压过 draw call 收益。
  • screenspace_general 的常量寄存器必须遵守 MGFX 文档化布局。MGFX 当前把 $viewprojmat / c11 当作主参数页,把 $invviewprojmat / c15 当作辅助参数页,把 $c4..$c7 当作 texture-size 寄存器。不要用 $c8 之类临时寄存器,它们可能能编译但运行时读到 0 或未定义值。

参数页布局

热路径 shape shader 的 16 个常规 float 参数走 $viewprojmat,在 pixel shader 中读作:

hlsl
const float4x4 MGFXExtraParams : register(c11);

Lua 端用 Matrix():SetUnpacked(...) 后调用:

lua
mat:SetMatrix("$viewprojmat", matrix)

额外 16 个辅助 float 走 $invviewprojmat,在 pixel shader 中读作:

hlsl
const float4x4 MGFXAuxParams : register(c15);

Lua 端同样用 Matrix():SetUnpacked(...) 后调用:

lua
mat:SetMatrix("$invviewprojmat", matrix)

GMod/Source 的矩阵索引会按列抵达 HLSL:matrix[0] 读到的是 1,5,9,13matrix[1] 读到的是 2,6,10,14,依此类推。Lua 端必须使用 MGFX 的统一打包 helper,不要在调用点猜行列顺序。

MGFXExtraParams 是主参数页。MGFXAuxParams 是辅助参数页,只给参数超过 16 个 float 的 fused shader 使用,例如 chamfer cuts + inner glow、ring stroke + inner glow、polygon 顶点、text atlas 数据,以及 shadow + outerGlow 合成参数。$c0..$c3 仍然在 shader common 中声明,主要用于兼容和诊断;新的热路径不要默认回到逐 float 上传。

本地 GMod benchmark 中,16 个独立 SetFloat 大约是 SetUnpacked + SetMatrix 的 7 倍成本,所以 hot shape path 不应回到逐 float 上传。

Image sampler 与 material capture

mgfx_roundrect_texturemgfx_chamfer_texturemgfx_image_mask$basetexture 都固定为 color/white。它只是 DrawTexturedRectUV 半像素 修正所依赖的稳定 mapping carrier,不是真正要显示的图像。运行时 sampler 分工如下:

  • TexBase:固定的局部 UV mapping carrier。
  • Tex1:源图像或 render target。
  • Tex2mgfx_image_mask 可选的纹理 mask。

screenspace_general 材质模板中的每个运行时 sampler slot 都必须用非空纹理名初始化。空的 $textureN 不会被 Source 注册为 texture 类型,因此之后的 SetTexture 会被拒绝,shader 最终只能采到 error texture。

修正后的 i.uv 只表示归一化 shape 坐标,再由 SOURCE_UV 映射到源图。不要把源图重新绑定到 $basetexture;否则材质 mapping 尺寸会把源图采样和 procedural SDF 坐标重新耦合,circle 和 rounded mask 的 coverage 会发生畸变。

IMaterial 始终是会被执行的 source。ImageUV、无正半径的 Image,以及 不需要 effect 的 ImageEx 不依赖 MGFX image shader,会直接绑定原始材质; 其余 image draw 则在完整的 render-state transaction 内先把原材质绘制到两张 可复用 full-frame scratch RT 之一:slot 1 保存 source,slot 2 保存可选 material mask,随后分别绑定到 Tex1 / Tex2。这样 AnimatedTexture 等 material proxy 会真正运行,而不是被一次 $basetexture 读取替代。source-alpha mask 直接复用 slot 1,不重复捕获。

Clip Composite 与 $basetexture 陷阱

mgfx_shape_clip 是 framebuffer transaction shader,不是 stencil mask。Sampler 分工刻意不占用 TexBase

  • TexBase:固定为 color/white,绘制期间绝不替换。
  • Tex1:Clip callback 绘制后的 framebuffer。
  • Tex2:painter 自定义 Mask 的 coverage RT。
  • Tex3:Clip callback 绘制前的 framebuffer。

解析 preset 直接计算 rounded/chamfer/circle/capsule coverage;自定义 Mask 则采样每侧最多带一像素透明边界的 coverage raster。到达 framebuffer 边缘时会减少对应 padding,因此全屏 Mask 仍然合法。自定义分支不能再乘一个矩形 coverage,也不能 clamp 掉透明边界,否则靠近 Clip bounds 的矢量 AA 会再次出现硬截断。矩形 scissor 只限制 composite 工作量,不决定最终边缘。

surface.DrawTexturedRectUV 会依据当前 $basetexture 的尺寸执行隐式半像素修正。如果在局部 UV 已准备好之后,把共享材质的 $basetexture 从固定 placeholder 动态替换成全屏 RT,隐藏的 mapping size 就会改变,相当于又做了一次错误修正:局部 Mask UV 会偏移、拉伸或丢失边缘像素。

需要在 shader 中采样运行时 RT 时,应在创建材质时就把 $texture1$texture2$texture3 声明为 texture 类型,运行时只替换这些 sampler,并保持 $basetexture 稳定。只有一种情况可以把 RT 放进 $basetexture:该 binding 在材质整个生命周期内固定,而且提交的 UV 就是按这张纹理的尺寸计算的。MGFX 的 coverage copy 专用材质符合这一条件;Clip composite 共享材质不符合,所以禁止动态替换 $basetexture

融合 Shape 快速路径

MGFX 允许小型专用 fused shader,但它们必须精确复刻原始分层结果。

当前保留的路径:

  • mgfx_roundrect_fx_ps30:roundrect fill/stroke + inner glow。只有 inner glow 会导致额外 pass 时才启用。
  • mgfx_roundrect_shadow_outer_ps30:roundrect shadow + outerGlow。API 字段保持分离,但同 bounds 的外部效果共享一次 draw。
  • mgfx_chamfer_ps30:chamfer fill/stroke + optional inner glow。fill/stroke 使用 MGFXExtraParams,cuts 和 inner-glow 数据使用 MGFXAuxParams
  • mgfx_chamfer_shadow_outer_ps30:chamfer shadow + outerGlow。
  • mgfx_ring_fx_ps30:ring/arc/sector fill + optional inner glow + stroke。fill-only ring 仍走更轻的 mgfx_ring_ps30
  • mgfx_ring_shadow_outer_ps30:ring/arc/sector shadow + outerGlow。
  • mgfx_image_mask_shadow_outer_ps30:rounded/chamfer/circle/capsule/texture image mask shadow + outerGlow。

这些不是通用 “everything shader”。Shadow 和 outerGlow 在 API 语义上仍然是两个层:shadow 是 CSS-like 的投影实体 mask,outerGlow 是外部边缘光。当前只融合能证明同一 draw rect/source-over 结果一致的路径。Convex poly 仍保持 shadow/outerGlow 独立,因为顶点参数已经占用辅助页;backdrop 也保持独立,因为它读取 framebuffer,source texture 和 blend order 是可见行为。未来要融合新层,必须证明 source-over 结果像素级一致,包括透明渐变和抗锯齿边缘。

Shape 空间渐变

MGFX 有两类渐变空间:

  • 矩形图元空间:LinearGradientRadialGradientEllipticalRadialGradientConicGradient 在图元 bounds 内采样归一化 UV。RadialGradient 按短边补偿 aspect ratio 以保持像素空间正圆;椭圆版本在同一 shader 分支中使用独立的本地横纵半径。
  • 圆环/扇区空间:RingRadialGradientSectorRadialGradient*AngularGradient 由 ring shader 按当前 ring/arc/sector 几何解释。

ArcExSectorEx 不能合并成同一个概念。ArcEx 是圆弧段,端点按 round cap 距离计算,适合 gauge mark;SectorEx 是直边径向扇区,按 start/end radial boundary 计算,适合轮盘 wedge。它们可以共享材质族,但 signed-distance 边界数学不同。

圆环/扇区局部径向填充:

text
t = (r - innerRadius) / (outerRadius - innerRadius)

局部周向填充:

text
t = (angle - startDeg) / (endDeg - startDeg)

这不等价于全局 ConicGradientConicGradient 始终描述围绕中心的 360 度角场。

Gradient LUT

多 stop 渐变统一走一维 LUT:

  • Lua 规范化、排序并补齐 0/1 端点,然后烘焙 256x4 render target。
  • shader 先算出 t,再从 $texture1 采样 LUT。
  • Linear、Radial、Elliptical Radial、Conic、Ring/Sector radial、Shape/Ring/Arc/Sector angular 都走同一套 LUT 采样。
  • LUT 按 stop table 缓存在有界 LRU 中。快速动画 stop 颜色或位置会 churn cache,优先动画几何、opacity 或显式 offset。
  • MGFX.LinearGradientMGFX.RadialGradientMGFX.EllipticalRadialGradientMGFX.ConicGradient 返回的 fill record 被视为 immutable。要改 stop 或颜色,应创建新的 fill record,不要原地改表。

Alpha 踩坑

不要把 gradient stop alpha 写进 render target alpha channel 后再用 tex2D(...).a 读取。GMod 的生成材质/render-target 路径里,alpha 写入和后续采样会出现“看似 opaque stop 正常,但透明 stop 变成不透明黑色”的问题。

可见症状是:本来应该淡出的径向或线性高光,消失后没有回到 alpha = 0,而是变成一块纯黑矩形或扇区,盖住下面所有图层。

当前 lut-rgba16-rgb-v4 方案把每个 RGBA 通道编码为 16-bit 定点数,同时刻意让 render target alpha 保持 255:

text
row 0      RGB 的高字节
row 1      RGB 的低字节
rows 2..3  在 R/G 通道存 alpha 的高/低字节

shader 的 mgfx_gradient_lut() 会执行三次过滤采样并重建 float4(rgb, alpha)。高低字节分别过滤后再线性组合,与直接过滤 16-bit 数值等价;alpha 仍只走 RGB 通道,从根源上避开 Source RT/blend/alpha-write 的不稳定行为。

重建后,同一个 pixel shader 根据整数 VPOS 生成稳定的屏幕空间 IGN,在 RGBA 上加入 ±0.5/255 的相关噪声。0/1 端点不会改变,也不会新增 sampler、噪声纹理或 draw pass。16-bit LUT 负责消除输入端台阶,IGN 负责打散最终 8-bit framebuffer 的量化分层。

如果改这条路径,至少验证:

  • Color(r, g, b, 0) stop 输出最终 alpha 0,而不是黑色。
  • 叠加在彩色背景上的径向渐变能露出下面图层。
  • 文本、线条、圆角矩形、ring radial 和 shape-local angular 渐变都读到同一套重建 alpha。

运行时资源

当前 addon 自带资源:

text
resource/fonts/notosanssc-vf.ttf

服务端 loader 会通过 resource.AddFile 下发字体。scoreboard 专用图标属于独立的 mgfx_zs_scoreboard addon,不放在 MGFX 主库里。

Workshop/GMA 要求文件名小写且扩展名在白名单内。之前的 NotoSansCJKsc-Regular.otf 不适合 GMA,因此现在使用小写 TTF notosanssc-vf.ttf,字体 face name 是 Noto Sans SC

包验证

运行:

powershell
gmad.exe create -folder "garrysmod/addons/mgfx" -out "%TEMP%/mgfx.gma"

期望运行时包内容:

text
lua/autorun/client/mgfx_loader.lua
lua/autorun/server/mgfx_loader.lua
lua/mgfx/*.lua
resource/fonts/notosanssc-vf.ttf

期望忽略内容:

text
shadersrc/*
tools/*
*.md

共享文档和 npm 站点文件位于仓库根目录,不进入 lua-mgfx addon 打包边界。

批处理原型的失败原因见 已移除的批处理设计