Shader 构建与打包
MGFX 现在把 shader bytecode 作为 Lux 客户端模块发布,而不是作为 addon 树下的 手写 Lua 文件发布。生成后的 shaderpack 位于:
lux-mgfx/lux/mgfx/shaderpack/src/cl_module.lux这个模块导出当前 VERSION、base64 GMA 载荷,以及 @lux/mgfx/materials 会使用的 辅助函数。项目导入 @lux/mgfx 后,luxc gmod build 会把 shaderpack module 编译进 生成的客户端 Lua,并由生成的 GMod loader 和其他客户端产物一起下发。
运行时流程
启动时,@lux/mgfx 会安装 shaderpack 并创建材质:
import * as shaderpack from "@lux/mgfx/shaderpack"
import * as materials from "@lux/mgfx/materials"
client fn createMgfxState() {
shaderpack.installGlobal()
materials.create()
}生成 Lua 的形状等价于导入这两个编译后的模块,并调用相同的运行时函数。这里不再需要 项目维护 include(...) 链,也不需要 MGFX 专用 autorun loader。
内部的 materials.create() 会:
- 通过
shaderpack.current()读取当前 shaderpack。 - 解码嵌入的 base64 GMA 载荷。
- 写入
data/mgfx_shaders_<version>.gma。 - 通过
game.MountGMA挂载。 - 创建指向带版本号 pixel shader 和 vertex shader 的
screenspace_general材质。
如果项目有意在 MGFX 初始化前提供全局 MGFXShaderPack,shaderpack.current() 可以 使用这个外部 pack。普通 Lux 项目应直接使用 package 自带的模块。
源码结构
运行时 package 源码在:
lux-mgfx/
lux/
mgfx/
shadersrc/
build.py
build_shaders.bat
compile_shader_list.txt
src/
shaderpack/src/cl_module.lux
materials/src/
module.lux
cl_base.lux
cl_create.lux
cl_status.lux
cl_texture.lux
cl_install.lux
roundrect/src/
primitives/src/
widgets/src/
text/src/
style/src/
precompiled/
dist/lua/shadersrc/src/ 保存 MGFX 的 HLSL 源码,并在 shaders/fxc/ 下提交当前 .vcs 产物。提交 .vcs 的目的,是让维护者即使暂时不能运行 shader compiler,也能用 pack-only 模式重新生成 Lux shaderpack。
Windows shader compiler 作为仓库级构建工具随仓库携带,但它不放进 lux/mgfx package 树:
lux-mgfx/tools/mgfx/sdk_screenspace_shaders/shadersrc/bin/ShaderCompile.exe这个二进制工具链不是 lux/mgfx 的 module layout,也不会被复制进生成 addon。它只给 MGFX 维护者从源码重新编译 shader bytecode 使用。Garry's Mod 本身不自带 shader compiler,因此构建只能使用这个随仓库携带的工具,或者显式设置 MGFX_SHADERCOMPILE 指向另一个 ShaderCompile.exe。
重新生成契约
当 shader bytecode 变化时,最终结果必须重新生成到 lux-mgfx/lux/mgfx/shaderpack/src/cl_module.lux。dist/lua 下的 Plain GLua loader 构建会通过 precompiled/ 项目消费同一个生成后的 shaderpack。
在 lux-mgfx/lux/mgfx/shadersrc 下常用的维护命令:
# 把已提交的 .vcs 文件重新打包进 Lux shaderpack module。
python .\build.py --pack-only
# 复现当前已提交版本。
python .\build.py --pack-only --version 1781243087 --gma-timestamp 1781243088
# 使用随仓库携带的 compiler 重新编译 HLSL,然后生成 Lux module。
python .\build.py
# 测试其他 ShaderCompile.exe 构建时,可以覆盖默认 compiler。
$env:MGFX_SHADERCOMPILE = "C:\Path\To\ShaderCompile.exe"
python .\build.py生成模块需要提供这个公开形状:
export client const VERSION = "..."
export client fn gma()
export client fn pack()
export client fn current()
export client fn installGlobal(name = "MGFXShaderPack")pack() 返回:
{
Version = VERSION,
GMA = "<base64 gma payload>",
}VERSION 必须和 GMA 内部的 shader 文件名保持一致。MGFX 创建材质时会把版本号加到 shader 名称前,例如:
<version>_mgfx_roundrect_ps30
<version>_mgfx_vs30如果版本号和编译出的 shader 文件名不一致,材质对象可能能创建出来,但无法绑定真正的 shader,MGFX 会退回 fallback 路径。
诊断
开发时安装 console package:
import * as mgfx from "@lux/mgfx"
import * as console from "@lux/mgfx/console"
client fn installTools() {
local api = mgfx.installGlobal("MGFX")
console.install(api)
}然后使用:
mgfx_status
mgfx_param_probe
mgfx_param_bench
mgfx_profile 1
mgfx_draw_counts 1正式测 FPS 时关闭:
mgfx_profile 0
mgfx_draw_counts 0每族 draw counter 默认关闭。复杂 immediate UI 中,几百次绘制的计数本身也可能变成 可测开销。
GMod Shader 规则
这些不是风格建议,而是实际踩坑后的硬规则:
- 对生成材质不要直接依赖
DrawTexturedRectUV,除非已经处理 UV 修正。优先使用显式 UV 的四点 textured quad。 - 抗锯齿需要最终屏幕空间尺寸或 UV derivative。逻辑尺寸不足以代表 shader AA 所需的 物理像素尺寸。
- 进入 SDF 计算前先 clamp radius 和 chamfer cuts。
- 不要随手增加 data texture 参数路径。被移除的 batch prototype 已经证明,上传和 调度成本可能压过 draw call 收益。
screenspace_general的常量寄存器必须遵守 MGFX 文档化布局。MGFX 当前把$c0..$c3当作自定义 draw 参数,把$c4..$c7当作 texture-size 寄存器。不要使用$c8这类临时寄存器,它们可能能编译,但运行时读到 0 或未定义值。
参数页布局
热路径 shape shader 的 16 个常规 float 参数走 $viewprojmat,在 pixel shader 中读作:
const float4x4 MGFXExtraParams : register(c11);Lux 生成的 Lua 会这样写入矩阵:
mat:SetMatrix("$viewprojmat", matrix)GMod/Source 的矩阵索引会按列抵达 HLSL:matrix[0] 读到 1,5,9,13, matrix[1] 读到 2,6,10,14,依此类推。运行时代码必须使用 MGFX 的统一打包 helper,不要在调用点猜行列顺序。
MGFXExtraParams 是主参数页。$c0..$c3 是辅助参数页,只给参数超过 16 个 float 的 fused shader 使用,例如 chamfer cuts + inner glow,或者 ring stroke + inner glow。 能放进主参数页的参数不要占用辅助页。
本地 GMod benchmark 中,16 个独立 SetFloat 大约是 SetUnpacked + SetMatrix 的 7 倍成本,所以 hot shape path 不应回到逐 float 上传。
融合 Shape 快速路径
MGFX 允许小型专用 fused shader,但它们必须精确复刻原始分层结果。
当前保留的路径:
mgfx_roundrect_fx_ps30:roundrect fill/stroke + inner glow。只有 inner glow 会导致 额外 pass 时才启用。mgfx_chamfer_ps30:chamfer fill/stroke + optional inner glow。fill/stroke 使用MGFXExtraParams,cuts 和 inner-glow 数据使用$c0..$c3。mgfx_ring_fx_ps30:ring/arc/sector fill + optional inner glow + stroke。fill-only ring 仍走更轻的mgfx_ring_ps30。
这些不是通用 “everything shader”。pattern、shadow、outerGlow 和 backdrop 仍可能保持独立,因为它们的 draw bounds、source texture 或 blend order 是可见行为。 未来要融合这些层,必须证明 source-over 结果像素级一致,包括透明渐变和抗锯齿边缘。
Shape 空间渐变
MGFX 有两类渐变空间:
- 矩形图元空间:
linearGradient、radialGradient和conicGradient在图元 bounds 内 采样归一化 UV。矩形径向渐变必须按短边补偿 aspect ratio,避免被 shape 拉伸。 - 圆环/扇区空间:
ringRadialGradient、sectorRadialGradient和角向填充由 ring shader 按当前 ring/arc/sector 几何解释。
arcEx 和 sectorEx 不是同一个概念。arcEx 是圆弧段,端点按 round cap 距离计算, 适合 gauge mark;sectorEx 是直边径向扇区,按 start/end radial boundary 计算,适合 轮盘 wedge。它们可以共享材质族,但 signed-distance 边界数学不同。
圆环/扇区局部径向填充:
t = (r - innerRadius) / (outerRadius - innerRadius)局部周向填充:
t = (angle - startDeg) / (endDeg - startDeg)这不等价于全局 conicGradient。conicGradient 始终描述围绕中心的 360 度角场。
Gradient LUT
多 stop 渐变统一走一维 LUT:
- 运行时代码规范化、排序并补齐 0/1 端点,然后烘焙 256x4 render target。
- shader 先算出
t,再从$texture1采样 LUT。 - Linear、radial、conic、ring/sector radial、shape/ring/arc/sector angular 都走同一套 LUT 采样。
- LUT 按 stop table 缓存在有界 LRU 中。快速动画 stop 颜色或位置会 churn cache,优先 动画几何、opacity 或显式 offset。
mgfx.api.linearGradient、mgfx.api.radialGradient、mgfx.api.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-alpha-rgb-v3 方案刻意把 alpha 当普通颜色数据保存:
rows 0..1 存 visible RGB,alpha 强制 255
rows 2..3 用 grayscale RGB 存 stop alphashader 的 mgfx_gradient_lut() 会采样两组数据并重建 float4(rgb, alpha)。这是绕开 Source RT/blend/alpha-write 行为的兼容保护。
如果改这条路径,至少验证:
Color(r, g, b, 0)stop 输出最终 alpha 0,而不是黑色。- 叠加在彩色背景上的径向渐变能露出下面图层。
- 文本、线条、圆角矩形、ring radial 和 shape-local angular 渐变都读到同一套重建 alpha。
生成 addon 内容
Lux 项目不会直接打包 MGFX 源码树,而是打包 luxc gmod build 生成的 GMod 输出。
期望运行时形状:
generated/
lua/
autorun/
<bundle-specific loader>.lua
lux/
client/
...具体生成文件名由 Lux GMod 后端负责,并包含项目 bundle/package 身份,避免 GMod addon 全局 lua/ 目录中的文件名冲突。不要手写 MGFX 专用 autorun loader。
文档仓库中的这些内容不是运行时文件:
docs/
site_build/
node_modules/
package.json
package-lock.json它们属于本文档仓库,不应该复制进运行时 GMA。
批处理原型的失败原因见 已移除的批处理设计。