T.TAO
Back to Works
2026/graphics / tools

Metal Shader Canvas

原生 macOS 实时着色器工作台:图层式 MSL 编写、热重载编译、多 Pass 后处理,以及一个由 AI 自行验证渲染结果的智能体编辑闭环。

MetalShadersToolsAI
Metal Shader Canvas

Metal Shader Canvas 是一个原生 macOS 应用(SwiftUI + Metal,约 2.2 万行 Swift),本质上是一个用于着色器编写的轻量级实时渲染引擎:你可以在 3D 网格或 2D 画布之上,以图层的方式叠加顶点、片段和全屏后处理着色器,应用会在运行时编译你的 MSL 源码并立即展示结果。

它最初是我的游戏引擎 MetalCaster 的着色器迭代工具,后来成长为独立项目——也成为了我探索一个重要问题的主要试验场:如何让 LLM 智能体在渲染工具里真正有用?

项目已在 GitHub 开源,我还在本站写了一个上手教程系列:Shader Canvas 使用基础

Shader Canvas Editor

架构

应用被严格拆分为两层:

  • SwiftUI 前端持有所有状态:代码编辑器(MSL 语法高亮、自动缩进、代码片段)、图层管理、教程、AI 聊天,以及 .shadercanvas / .shaderlab 工作区持久化。
  • MetalRenderer 后端(一个 MTKViewDelegate,约 1600 行)独占 GPU 资源:管线状态、离屏纹理、通过 ModelIO 加载网格,以及逐帧编码。

两者通过一个轻薄的 NSViewRepresentable 桥接和解耦的通知机制通信——渲染器从不触碰 UI 状态,这正是运行时着色器重编译从不阻塞帧的原因。

一帧的构成,逐 Pass 拆解

3D 模式按经典方式渲染:

  1. 背景 + 网格 Pass 渲染到离屏 HDR 纹理,带深度测试(Depth32F)、右手系透视投影和 Rodrigues 旋转的模型变换。
  2. 全屏后处理链在两张离屏纹理之间 ping-pong——每个用户图层就是一个 Pass。
  3. 最终 blit 呈现到 drawable,使用全屏三角形(3 个顶点覆盖 NDC,而非四边形)和 .private 存储模式的 GPU 纹理。

2D 模式在此之上扩展了逐对象管线:每个 2D 对象携带自己的顶点着色器(形变)、片段着色器(着色),以及一个系统注入的 SDF 包装器,负责基于有符号距离的 alpha 遮罩和抗锯齿边缘。每个对象绘制前,当前 backbuffer 会先被 blit 一份,让对象的着色器可以采样身后的画面——从而在 2D 里实现类似折射和阴影的效果。

Fullscreen Post-Processing

不添乱的热重载编译

每次编辑都会触发运行时的 device.makeLibrary(source:) 编译,但朴素地全量重编译会让编辑器没法用。渲染器的做法是:

  • 对着色器状态做 diff,只重建实际发生变化的类别对应的管线(mesh、fullscreen、逐对象各自独立)。
  • 在专用队列上串行化编译,配合 epoch 计数器,被后续编辑取代的过期结果会被直接丢弃,而不是与当前结果竞争。
  • 对瞬时失败自动重试,并为 Metal 编译器守护进程设置冷却时间;结构化提取 MSL 错误信息回传给编辑器。
  • 帧信号量限制在途帧数,重着色器不会挂起 GPU 队列。

数据驱动的顶点接口

应用没有固定的顶点格式,而是提供一个 Data Flow 面板,让你勾选哪些属性(法线、UV、世界坐标、视线方向……)流入着色器——应用据此生成共享的 MSL 头文件。这是每个 shader graph 系统都要解决的接口问题的一个微缩版本。

Data Flow

着色器还可以内联声明可调参数——// @param _rimPower float 2.0 0.5 8.0——应用会把它变成由 GPU buffer 支撑的 UI 滑杆,Houdini 风格:

Parameters

内置着色库

十种经典光照模型(Lambert、Phong、Blinn-Phong、卡通、边缘光、菲涅尔……)和五种后处理效果——Bloom(阈值提取 + 高斯模糊 + 叠加合成)、ACES 色调映射、HSV 调整、高斯模糊、边缘检测——外加一个九步交互式 Metal 教程。每个预设在编辑器里一键即可套用,直接写入当前图层的源码:

在交互式教程中应用 Blinn-Phong 预设

Fresnel 边缘光照预设

在网格 Pass 之上叠加一个全屏图层,整帧画面就会进入后处理链:

叠加在网格 Pass 之上的全屏后处理图层

Bloom

智能体工作流:与渲染器形成闭环

这个项目最有意思的部分不是某个具体的着色器,而是 AI 系统的设计。大多数"AI + shader"工具止步于把生成的代码粘贴进编辑器。Shader Canvas 则把实时渲染器当作智能体的事实基准(ground truth):智能体做的每件事都要经过真实编译和真实渲染的验证,渲染结果再回传给模型。正是渲染器让智能体闭环变得又快又可信——人类需要"复制-编译-看效果"好几分钟的一次着色器迭代,智能体几秒就能完成,因为编译结果和渲染帧会自动流回它的上下文。

1. 结构化操作,而非代码倾倒

智能体以 JSON 格式的 AgentResponse 回复——包含 canFulfillexplanation、一组类型化的 actionsaddLayermodifyLayeraddObject2DsetObjectShader2D……),以及当请求超出管线能力时的显式 barriers。这些操作通过与 UI 完全相同的代码路径直接修改渲染图,且每次运行前都会保存工作区检查点,所有智能体编辑均可一键回滚。

2. 编译 → 渲染 → 验证,全自动

在 Plan 模式下,多步任务会变成一棵任务树,每一步都运行一个验证循环:

  1. 生成并应用着色器操作。
  2. 等待渲染器返回真实的编译结果
  3. 编译失败时,把 MSL 错误信息回传并自动修复(有限次重试)。
  4. 编译成功后,截取渲染帧,由视觉模型对照该步骤的意图验证视觉结果。
  5. 只有通过验证才标记该步骤完成。

截图路径是精心设计的,而非顺手为之:渲染器在 GPU 时间线上回读离屏 HDR 目标,缩放到 512px,在主线程之外压缩为 JPEG,再附加到多模态上下文中。在 2D 模式下,智能体甚至可以隔离渲染一个仅包含其修改对象的预览克隆——在用户接受之前,它的改动绝不会覆盖用户的对象。

3. 面向着色器的上下文工程

把原始 MSL 直接塞进 prompt 的做法扩展性很差。ContextManager 负责组装分层的、感知 token 预算的上下文,而 ShaderAnalyzer 对用户的 MSL 做静态分析生成语义摘要——能识别 30 多种效果模式(PBR 近似、bloom、SDF 布尔运算、raymarching、时间动画……),让模型推理的是着色器在做什么,而不只是它的文本。随着预算收紧,对话历史也会被压缩。

4. Lab 模式:文档驱动的协作

Lab 模式把较长的工作组织为阶段——参考资料 → 分析 → 设计文档 → 实现 → 调参 → 对抗式评审——单次智能体响应可以同时更新设计文档、注册可调参数并编写着色器代码。在对抗阶段,AI 会针对当前着色器提出多个竞争性变体(参数与代码修改),用户可以接受、拒绝或部分采纳,并借助参数快照做 A/B 对比。

这个项目教会我的事

智能体的上限取决于你给它的反馈。把渲染器接入闭环——编译结果、渲染帧、语义化的着色器分析——让 AI 从一个代码生成器变成了更接近"会自己检查工作的初级技术美术"的存在。这个原则直接延续到了 MetalCaster 的多智能体系统中。

如实说明范围

Shader Canvas 在设计上是一条光栅化管线:顶点/片段阶段加全屏 Pass,2D 模式下有基于 SDF 的程序化着色(用户也可以手写 raymarching 片段着色器)。它没有实现硬件光线追踪、计算管线或阴影贴图——智能体的系统提示词中把这些明确声明为能力边界,因此它绝不会假装能做到。GPU ray marching 在它的姊妹项目 SDF Canvas(位于 MetalCaster 仓库)中实现。

技术栈: Swift、SwiftUI、Metal、MetalKit、ModelIO、simd、MSL · macOS 26+