DaydreamerH DaydreamerH Game Dev Training Desk

Rendering / Engine Architecture

StylizedRenderer 第四阶段:MToon 材质、编辑器与 Sidecar 工作流

解析第四阶段的模块目录重组、MToon 参数模型、法线与纹理导入、运行时材质绑定、卡通光照组成、组件调试视图,以及材质 Sidecar 的保存和加载流程。

前三个阶段已经建立从模型资产到多 Pass HDR 输出的完整路径,但表面着色仍以 Unlit、法线调试和基础 PBR 为主。第四阶段沿用既有的 RenderWorld → FramePipeline → ForwardOpaquePass 框架,在材质层加入 MToon:同一个模型可以使用分段明暗、法线贴图、半球环境光、MatCap、边缘光和自发光形成风格化结果,并通过 Viewer 直接编辑每个导入材质。

MToon 被实现为新的 MaterialKind,继续使用 MaterialTemplateMaterialInstanceRuntimeMaterialRuntimeResourceCache。参数、纹理、运行时绑定、交互编辑和磁盘持久化分别进入对应模块,使 MToon 可以直接复用现有模型绘制与多 Pass 渲染框架。

Model / Texture Files
  → ModelImporter / TextureImporter
  → MaterialAsset + TextureAsset
  → MaterialInstance<MToonMaterialParameters>
  → RuntimeResourceCache
  → RuntimeMaterial::bind
  → StaticModelRenderer
  → MToon Shader
  → HDR Forward Target

ViewerPanels
  ↔ editable MaterialInstance
  ↔ MToonMaterialSidecar (.mtoon.json)

系统结构调整

Graphics、Render 与 Viewer 按照资源所有权和执行职责重新组织目录,使设备接口、运行时资源、逐帧数据、渲染 Pass 和工具界面各自形成清晰模块:

graphics/
├── device/
│   ├── GraphicsDevice
│   ├── GraphicsCommands
│   ├── GraphicsTypes
│   └── OpenGLContext
└── resources/
    ├── Buffer / VertexArray
    ├── Texture2D / RenderTexture / DepthTexture
    ├── Framebuffer / ShaderProgram
    └── GpuTimerQuery

render/
├── pipeline/   FrameContext / IRenderPass / FramePipeline
├── passes/     ShadowPass / ForwardOpaquePass / PostProcessPass
├── renderers/  StaticModelRenderer
├── resources/  RuntimeMesh / RuntimeMaterial / RuntimeResourceCache
└── world/      RenderWorld / RenderExtractor

viewer/
├── camera/     OrbitCameraController
└── ui/         ViewerPanels / ImGui backend

GraphicsDeviceGraphicsTypes 表达图形 API 入口,resources 保存有所有权的 GPU 对象;Render Pipeline 负责编排 Pass,Renderer 负责提交一类绘制,Render Resources 连接 Asset 与 GPU,Render World 则保存当前帧的扁平视图。这样的目录结构与前三阶段形成的职责一致,只是把原本位于同级目录的类型按照用途显式分组。

MToon 材质工作流涉及的主要类型如下:

层级类型职责
AssetMaterialAsset保存导入的 Base Color、Normal Texture 与 Normal Scale
ImporterTextureImporter从独立图片创建指定颜色空间的 TextureAsset
MaterialMToonMaterialParameters保存 MToon 标量、颜色和纹理 Handle
MaterialMToonMaterialSidecar表示可持久化的材质记录与相对纹理路径
MaterialMToonMaterialSidecarSerializer校验 JSON、读写文件并报告字段级错误
RuntimeMaterialInstance持有可编辑的 MToon 参数实例
RuntimeRuntimeMaterial绑定 MToon Uniform 与 8 个纹理槽
RuntimeRuntimeResourceCache缓存可编辑实例并提供语义化兜底纹理
RenderEnvironmentLightData向 MToon 提供天空/地面半球环境光
RenderMToonDebugView选择最终结果或单独输出某个着色分量
ViewerViewerPanels选择材质、编辑参数、重置以及保存/加载 Sidecar

MaterialAsset:导入法线数据

MaterialAsset 仍然是与图形 API 无关的 CPU 资产。第四阶段新增两个字段:

AssetHandle<TextureAsset> normalTexture;
float normalScale = 1.0F;

Base Color Texture 表示颜色,采样前需要从 sRGB 解码到线性空间;Normal Texture 表示方向数据,必须保持线性。即使两张贴图引用同一图片路径,Importer 也不能只按路径合并,因为它们需要不同的颜色空间和预处理语义。

Assimp 材质导入器因此引入 ImportedTextureSemantic,并把纹理缓存键写成:

color:<texture path>
normal:<texture path>

颜色纹理以 ColorSpace::Srgb 注册,法线纹理以 ColorSpace::Linear 注册。normalScale 从 glTF Normal Texture Scale 读取,并限制为非负值。后续 MaterialInstance 创建 MToon 参数时,会把这两个字段复制到 normalTexturenormalScale

glTF UV 与切线手性的对齐

模型导入一直包含一次纹理垂直翻转,用于协调图片行方向与 OpenGL UV 约定。Base Color 只受 UV 位置影响,而 Normal Map 还依赖 Tangent、Bitangent 与 Normal 构成的 TBN 坐标系;只翻转图片或 UV 而不调整切线手性,会让法线的 Y 方向反转,凹凸看起来内外颠倒。

第四阶段将 Assimp 处理拆成两个步骤:先读取原始 Scene,对 glTF/GLB 中已有的 Bitangent 取反,再执行 Assimp Post Processing。Normal Map 的绿色通道也在导入时执行 G = 255 - G。这两项处理共同对齐当前 UV 翻转约定与切线空间方向。

这类修正集中在 Importer。TextureAsset 和顶点 Tangent 进入 Runtime 层后,Shader 便可以按统一约定构造 TBN,无需根据文件扩展名再次判断方向。

TextureImporter:导入 Sidecar 外部纹理

模型导入器能够处理 glTF 内嵌和外部纹理,但 Sidecar 中的 Shade、MatCap、Rim Mask 或 Emission 可能并不属于原模型。TextureImporter 为这些独立图片提供统一入口:

TextureImporter importer{assetRegistry};

auto texture = importer.import(
    imagePath,
    ColorSpace::Linear);

import() 依次验证路径非空、文件存在且为普通文件,调用已有图片解码函数生成 TextureAsset,写入调用方指定的颜色空间,最后注册到 AssetRegistry。失败时返回空 Handle,不会向 Registry 留下半初始化资产。

颜色空间由纹理用途决定:

通道颜色空间原因
Base ColorsRGB保存视觉颜色,采样时需要解码
ShadesRGB保存阴影侧表面颜色
NormalLinear保存方向分量
Shading ShiftLinear保存控制标量
MatCapsRGB保存附加视觉颜色
Rim MaskLinear保存强度遮罩
EmissionsRGB保存自发光颜色

MToonMaterialParameters:组织材质实例

MaterialKind 新增 MToonMaterialInstance 则新增:

std::optional<MToonMaterialParameters> mtoonParameters;

使用 optional 而不是让所有材质实例都包含一套 MToon 参数,能够表达参数块是否与当前模板匹配。makeMaterialInstance() 只有在 Kind 为 MToon 时才 emplace() 参数;RuntimeMaterial::bind() 也会验证参数存在。模板与参数块不一致时,绑定直接失败。

MToonMaterialParameters 将数值参数与纹理引用分开:

MToonMaterialParameters
├── Shade
│   ├── shadeColor
│   ├── shadingShift
│   ├── shadingShiftTextureScale
│   └── shadingToony
├── Normal
│   └── normalScale
├── GI
│   └── giEqualization
├── MatCap
│   ├── matcapColor
│   └── matcapStrength
├── Rim
│   ├── rimColor
│   ├── rimFresnelPower
│   ├── rimLift
│   └── rimLightingMix
├── Emission
│   ├── emissionColor
│   └── emissionStrength
└── textures
    ├── shade / normal / shadingShift
    ├── matcap / rimMask / emission
    └── Base Color 仍位于 MaterialInstance

由模型资产创建 MToon Instance 时,Base Color 沿用源材质;默认 Shade Color 为 Base Color 的 45%,Shade Texture 暂时复用 Base Color Texture;Normal Texture 与 Normal Scale 则直接沿用导入值。其余效果使用保守默认值:MatCap 和 Rim 默认无贡献,Emission 由黑色兜底纹理关闭。

这种初始化策略使普通 glTF 材质切换到 MToon 后立即可见,同时不会凭空加入强烈的边缘光或 MatCap。之后再由 Viewer 对具体材质进行调整。

RuntimeResourceCache:让材质实例可编辑

第三阶段中,getOrCreateMaterialInstance() 返回 const MaterialInstance*。第四阶段的材质编辑器需要直接修改缓存实例,因此返回值改为可写指针:

MaterialInstance* instance = cache.getOrCreateMaterialInstance(
    materialHandle,
    mtoonTemplateHandle,
    assets);

缓存键仍然是 {materialId, templateId}。一个 MaterialAsset 可以同时拥有 Unlit、PBR 和 MToon 实例;Viewer 对 MToon 参数的修改不会污染其他模板实例,也不会修改原始 MaterialAsset。

resetMaterialInstance() 使用同一 Material Asset 与 Template 重新调用 makeMaterialInstance(),再覆盖缓存中的现有实例。重置的是运行时编辑状态,源模型资产和纹理资产不变。

语义化兜底纹理

MToon 需要七类纹理。要求每个材质都提供全部图片会让最小材质难以使用,因此 Cache 新增黑色和中性法线纹理,与已有白色、错误纹理组成四种兜底资源:

纹理像素值用途
White(1,1,1,1)Base、Shade、Rim Mask 的乘法单位元
Black(0,0,0,1)Shift、MatCap、Emission 的零贡献
Neutral Normal编码后的 (0,0,1)不改变几何法线
Error黑紫棋盘Handle 存在但资产无效或上传失败

兜底值按公式选择,而不是所有缺失纹理都返回白色。例如缺失 Emission 若返回白色,会让非自发光材质凭空发亮;缺失 Shading Shift 若返回白色,会把明暗分界整体偏移;缺失 Rim Mask 使用白色,则表示不额外遮蔽由颜色和 Fresnel 参数定义的边缘光。

RuntimeMaterial:绑定八个纹理槽

MToon Runtime Material 初始化 Shader 后固定 Sampler 槽位:

slot 0  Base Color
slot 1  Shadow Map
slot 2  Normal
slot 3  Shade
slot 4  Shading Shift
slot 5  MatCap
slot 6  Rim Mask
slot 7  Emission

slot 1 仍由 StaticModelRenderer 在 Shadow Map 有效时绑定;其余材质纹理由 RuntimeMaterial::bind() 解析。Bind 先取得所有 Texture2D,验证每个资源有效,再上传颜色与标量 Uniform,最后绑定纹理。任何步骤失败都不会提交当前 Draw Call。

这种职责分配与前三阶段保持一致:Runtime Material 处理实例级参数,StaticModelRenderer 处理 View、Light、Shadow 等逐帧参数。MToon 增加的 uView、环境光和 Debug View 因此由 Renderer 设置,而不是塞进每个 Material Instance。

顶点 Shader:构造稳定的世界空间切线

MToon Normal Mapping 要求顶点 Shader 额外输出 vertexWorldTangent。Normal 使用逆转置矩阵,Tangent 使用 Model Matrix 的 3×3 部分,再通过 Gram-Schmidt 从 Tangent 中去掉沿 Normal 的分量:

T' = M · T
T  = normalize(T' - N dot(N, T'))

非均匀缩放后,直接变换得到的 Tangent 不一定与 Normal 正交。重新正交化能够保证 TBN 基底用于方向变换时不会引入额外倾斜。

若输入 Tangent 退化,Shader 根据 Normal 选择一个不平行的参考轴,通过叉积重建 Tangent。inTangent.w 保存 Bitangent 的手性符号,片元 Shader 使用:

B = cross(N, T) × tangentSign

这样镜像 UV 岛可以使用相反手性的切线空间,而不需要额外传输完整 Bitangent。

MToon Shader:组合风格化光照

MToon 片元结果由五部分相加:

finalColor = directColor
           + indirectColor
           + matcapContribution
           + rimContribution
           + emissionContribution

与 PBR 不同,这里不追求微表面能量守恒,而是提供艺术可控的明暗分区与附加光照分量。每一部分都有独立参数和调试视图。

Normal Mapping 与双面法线

calculateSurfaceNormal() 先根据 gl_FrontFacing 决定几何法线方向。背面使用负法线,使双面几何在背面观察时仍得到朝向观察侧的表面基底。随后重新正交化 Tangent,结合 Handedness 构造 TBN。

Normal Texture 只读取 XY:

xy = texture(normalMap).xy × 2 - 1
xy = xy × normalScale
z  = sqrt(max(1 - dot(xy, xy), 0))

Z 由单位半球约束重建,再归一化并乘以 TBN 转到世界空间。只缩放 XY 可以控制法线扰动强度,同时保持 Z 与单位长度一致;若直接把采样 Z 与缩放后的 XY 拼接,Normal Scale 变化时方向分布会失真。

Base、Shade 与明暗分界

方向光强度先计算 N·L。MToon 不把它直接作为连续 Lambert 亮度,而是通过 Shift 与 Toony 控制分区:

finalShift = shadingShift
           + shiftTexture.r × shadingShiftTextureScale

transitionWidth = max(1 - shadingToony, 0.0001)

shadingFactor = clamp(
    (N·L + finalShift) / transitionWidth,
    0,
    1)

shadingShift 移动明暗边界;Shift Texture 允许按区域局部移动边界;shadingToony 越接近 1,Transition Width 越窄,Base 与 Shade 之间越接近硬切。较低值则产生更宽的过渡带。

Base Surface Color 来自 Base Texture 与 Base Color Factor。Shade Surface Color 来自 Shade Texture 与 Shade Color。二者分别乘方向光 Radiance,再按 shadingFactor 插值。因此 Shade 不是简单把 Base 变暗,而是可以拥有独立色相与图案。

阴影接收

MToon 复用第三阶段的 Directional Shadow Map、斜率相关 Bias 与 3×3 PCF。不同之处是 Shadow Visibility 乘在 Shading Factor 上:

visibleShadingFactor = shadingFactor × shadowVisibility
directColor = mix(shadeColor, litColor, visibleShadingFactor)

被 Shadow Map 遮挡时,明面权重下降,颜色转向 Shade,而不是在最终颜色上简单乘黑。这种方式更符合 Toon 材质的两段式控制:投影区域仍保留由 Shade Color 定义的可读颜色。

半球环境光与 GI Equalization

EnvironmentLightData 保存 Sky Color、Ground Color 与 Intensity。Shader 根据世界法线 Y 分量计算半球权重:

hemisphereWeight = normal.y × 0.5 + 0.5
directionalEnvironment = mix(ground, sky, hemisphereWeight)
uniformEnvironment = (sky + ground) × 0.5
environment = mix(
    directionalEnvironment,
    uniformEnvironment,
    giEqualization) × intensity

giEqualization = 0 时,朝上的表面偏向天空色,朝下的表面偏向地面色;接近 1 时,方向差异被压平为均匀环境光。它控制环境光对模型朝向的敏感度,减少复杂法线导致的零碎明暗,同时保留整体可读性。

MatCap

MatCap 使用 View Space Normal 的 XY 映射纹理坐标:

viewNormal = normalize(mat3(view) × worldNormal)
matcapUv = viewNormal.xy × 0.5 + 0.5
matcap = texture(matcapTexture, matcapUv)
       × matcapColor
       × matcapStrength

纹理看起来像包裹在相机周围的球面光照,其方向随观察坐标系保持稳定。MatCap 不依赖方向光位置,适合加入高光、材质质感或手绘明暗,但它是附加颜色而不是物理 BRDF。

Rim Lighting

边缘光使用视线与法线夹角:

rimBase = clamp(1 - N·V + rimLift, 0, 1)
rimFactor = pow(rimBase, rimFresnelPower)

rimLift 调整边缘光出现阈值,rimFresnelPower 控制轮廓宽度与集中程度,Rim Mask 则按 UV 局部抑制效果。rimLightingMix 在常量白光和受方向光/阴影影响的 Rim 之间插值:0 表示不受场景主光控制,1 表示边缘光随当前可见明面衰减。

Emission

自发光由 Emission Texture、Emission Color 和非负 Strength 相乘:

emission = texture(emissionTexture)
         × emissionColor
         × emissionStrength

它不乘方向光、环境光或阴影可见度,直接加入 HDR 输出。Strength 大于 1 时可以产生超过显示范围的颜色,后续由 Tone Mapping 处理,也为未来 Bloom 提供亮部来源。

MToonDebugView:拆分着色结果

最终画面很难直接判断颜色来自 Shade、Rim、MatCap 还是 Emission。MToonDebugView 因此进入 RenderView,由 StaticModelRenderer 设置到 uMToonDebugView

模式输出
Final所有分量相加
BaseBase Color
ShadeShade Surface Color
LightingvisibleShadingFactor 灰度
RimRim Contribution
MatCapMatCap Contribution
EmissionEmission Contribution

Debug View 是视图级状态,不属于某个材质。切换后所有 MToon Draw Call 使用同一种分量输出,便于比较多个材质在相同光照下的响应。

Lighting 模式尤其适合检查 Shift、Toony、Normal Map 和 Shadow Map,因为这些因素最终都会进入 visibleShadingFactor。若 Lighting 分区正确而最终颜色异常,问题更可能位于 Base/Shade 颜色或附加分量。

StaticModelRenderer:补充 MToon 视图参数

StaticModelRenderer 将 Basic PBR 与 MToon 共享的 Camera、Directional Light 和 Shadow Uniform 放在同一分支中。MToon 再额外绑定:

  • View Matrix,用于 MatCap 的 View Space Normal;
  • Sky/Ground Environment Color 与 Intensity;
  • 当前 MToonDebugView

每个 RenderItem 仍先调用 RuntimeMaterial::bind() 绑定实例参数,然后上传 World、Normal、View 与 Light 参数,最后构造 DrawIndexedCommand。MToon 没有绕开 ForwardOpaquePass,输出继续写入 RGBA16F HDR Color,之后经过统一的 Exposure、Tone Mapping 与 Gamma Correction。

ViewerPanels:从渲染诊断扩展为材质编辑器

Viewer 原有面板只显示渲染统计和全局开关。第四阶段让 ViewerPanels::draw() 接收可写的 AssetRegistryRuntimeResourceCache、当前 Material Template 与 RenderWorld,从而能够选择并修改缓存中的 MToon Instance。

收集与选择材质

面板遍历 SceneAsset 引用的 Mesh,再遍历 Mesh Primitive,收集去重后的 Material Handle。selectedMaterial_ 保存当前选项;组合框显示 MaterialAsset Name。编辑目标是源材质与当前 MToon Template 组成的缓存实例,而不是所有使用同名 Shader 的物体。

编辑粒度与导入模型的材质分配一致:多个 Primitive 引用同一个 Material Handle 时会共享编辑结果,引用不同材质时可以分别调整。

参数分组

MToon 控件按计算阶段组织:

  • Base / Shade:Base Color、Shade Color、Shift、Shift Scale、Toony 及纹理状态;
  • Normal:Normal Scale 与 Normal Texture;
  • GI:GI Equalization;
  • MatCap:Color、Strength 与 Texture;
  • Rim:Color、Power、Lift、Lighting Mix 与 Mask;
  • Emission:Color、Strength 与 Texture。

面板直接修改 MaterialInstance,下一次 Draw Call 的 RuntimeMaterial::bind() 会读取新值,因此调节无需重新导入模型、重新编译 Shader 或重建 Pipeline。

纹理状态会显示资产来源路径;空 Handle 显示实际使用的 White、Black 或 Neutral Normal Fallback;非空但 Registry 查询失败则显示错误状态。第四阶段的界面主要编辑数值并观察已有纹理,Sidecar 加载负责把外部纹理路径恢复成新的 TextureAsset。

重置与输入捕获

“Reset Selected Material”调用 RuntimeResourceCache::resetMaterialInstance(),以源 MaterialAsset 重新生成当前 MToon Instance。它可以撤销本次运行中的参数编辑和 Sidecar 覆盖。

ViewerPanels::wantsMouseCapture() 返回 ImGui 的 WantCaptureMouse。Orbit Camera 在 UI 捕获鼠标时停止旋转、平移和缩放,避免拖动 Slider 或操作 Combo 时同时改变相机。这属于 Viewer 输入路由,而不是 Camera 类内部对 ImGui 的依赖。

MToon Sidecar:保存模型之外的材质编辑

运行时 MaterialInstance 不属于模型文件,关闭 Viewer 后会消失。第四阶段使用与模型并列的 JSON Sidecar 保存 MToon 编辑结果:

character.glb
character.mtoon.json
textures/
├── shade.png
├── normal.png
├── matcap.png
└── emission.png

Sidecar 路径由模型路径替换扩展名得到。文件根对象包含版本和材质数组:

{
  "version": 1,
  "materials": [
    {
      "name": "Face",
      "baseColorFactor": [1.0, 1.0, 1.0, 1.0],
      "shadeColor": [0.45, 0.40, 0.40],
      "shadingShift": 0.0,
      "shadingShiftTextureScale": 1.0,
      "shadingToony": 0.9,
      "normalScale": 1.0,
      "giEqualization": 0.9,
      "matcapColor": [1.0, 1.0, 1.0],
      "matcapStrength": 0.0,
      "rimColor": [0.0, 0.0, 0.0],
      "rimFresnelPower": 5.0,
      "rimLift": 0.0,
      "rimLightingMix": 1.0,
      "emissionColor": [0.0, 0.0, 0.0],
      "emissionStrength": 1.0,
      "textures": {
        "baseColor": "textures/base.png",
        "shade": "textures/shade.png",
        "normal": "textures/normal.png",
        "shadingShift": "",
        "matcap": "textures/matcap.png",
        "rimMask": "",
        "emission": "textures/emission.png"
      }
    }
  ]
}

MToonMaterialSidecar 与运行时实例的区别

MToonSidecarMaterial 不保存 Asset Handle,因为 Handle 只在一次运行的 AssetRegistry 中有效。它保存普通数值和文件路径;Capture 阶段把 Handle 解析为相对路径,Apply 阶段再通过 TextureImporter 将路径转换为新的 Handle。

纹理路径必须相对 Sidecar 目录。绝对路径会被 Serializer 拒绝,避免配置文件绑定某台机器的盘符或用户目录。保存时,TextureAsset 必须具有 Source Path,无法追溯来源的程序生成纹理不能直接写成 Sidecar 路径。

Serializer 的验证规则

MToonMaterialSidecarSerializer 使用 nlohmann/json,但不会把 JSON 对象直接反序列化进结构体。它逐字段检查:

  • Root 必须为 Object;
  • Version 必须为无符号整数且等于当前版本 1;
  • Materials 必须为 Array;
  • Material Name 必须存在、非空且不能重复;
  • 向量必须拥有正确分量数,标量和分量必须为有限数;
  • 参数根据语义限制到允许范围;
  • Texture Path 必须为字符串、为空或相对路径。

MToonSidecarError 保存 materialfieldmessage。Viewer 可以显示“哪个材质的哪个字段为什么失败”,而不是只返回一个无法定位的 JSON Parse Error。

保存:临时文件与备份替换

saveMToonSidecarFile() 先序列化到字符串,再写入 <path>.tmp。若目标文件已经存在,先重命名为 <path>.bak,随后把临时文件重命名为正式路径;替换失败时尝试恢复备份。

serialize
  → write .tmp
  → old file rename to .bak
  → .tmp rename to target
  → success: remove .bak
  → failure: restore .bak

这种流程避免程序在写入一半时留下截断 JSON。它不是跨所有文件系统都绝对原子的数据库事务,但比直接覆盖目标文件具有更可靠的失败恢复能力。

加载:先验证全部,再统一应用

Viewer 加载 Sidecar 后,按 Material Name 在当前模型中查找对应 Material Handle。每条记录先复制当前 MaterialInstance,在副本上调用 applyMToonSidecarMaterial();纹理路径被解析到 Sidecar 目录,并按语义颜色空间导入。

所有记录都成功后,Viewer 才把 PendingMaterial 中的副本移动覆盖到真正实例。若中途出现不存在的材质、非法字段或纹理导入失败,现有实例不会被部分覆盖。这个两阶段应用方式使一次 Load 对材质参数具有整体提交语义。

空纹理路径表示保留当前 Handle,而不是强制清空纹理。相同的已加载 Source Path 会复用现有 Handle,避免每次点击 Load 都重复注册同一图片。

一帧中的调用关系

第四阶段没有改变三个 Render Pass 的顺序,变化集中在 Render Extraction 前后的材质数据和 Forward Shader:

flowchart TD
    A["ModelImporter"] --> B["MaterialAsset / Normal Texture"]
    B --> C["RuntimeResourceCache"]
    C --> D["MToon MaterialInstance"]
    D --> E["RenderExtractor"]
    E --> F["RenderWorld"]
    F --> G["ShadowPass"]
    G --> H["ForwardOpaquePass"]
    D --> I["RuntimeMaterial::bind"]
    I --> H
    H --> J["MToon Shader"]
    J --> K["HDR Color"]
    K --> L["PostProcessPass"]
    L --> M["Backbuffer"]
    N["ViewerPanels"] --> D
    N <--> O["MToon Sidecar"]

Viewer 每帧先根据当前 MaterialKind 选择 MToon Template,RenderExtractor 为可见 Primitive 取得对应 MaterialInstance 和 RuntimeMaterial。Shadow Pass 仍只消费几何体;Forward Pass 绘制时,RuntimeMaterial 绑定 MToon 实例参数,StaticModelRenderer 再绑定 Camera、Light、Environment、Debug View 与 Shadow Map。

ViewerPanels 在场景完成后绘制。用户本帧修改的参数保存在缓存实例中,下一帧提取仍会取得同一个 {Material, Template} Key,因此修改持续生效。切换到其他 MaterialKind 不会删除 MToon Instance;切回 MToon 后可以继续使用先前编辑状态。

使用与调试顺序

MToon 材质可以按以下顺序调整,减少多个分量同时变化造成的干扰:

  1. 切换到 MToon,选择目标 Material;
  2. 使用 Base Debug View 检查 Base Color 与 Base Texture;
  3. 使用 Lighting Debug View 调整 Normal Scale、Shading Shift 与 Toony;
  4. 切换 Shade View,确定 Shade Color 与 Shade Texture;
  5. 调整 GI Equalization,使背光面保持需要的方向感;
  6. 分别使用 MatCap、Rim、Emission View 调整附加分量;
  7. 回到 Final 检查各分量叠加和 HDR Tone Mapping;
  8. 点击 Save Materials 写入 .mtoon.json

常见问题可以按数据层定位:

现象检查位置可能原因
法线凹凸方向相反Importer / Lighting ViewGreen Channel 或 Tangent Handedness 不一致
Normal Map 缺失但表面异常Neutral Normal Fallback空 Handle 没有解析到中性法线
Shade 始终等于 BaseShade Texture / Color默认复用 Base Texture,Shade Color 对比不足
明暗边界过硬Shading Toony数值过于接近 1
阴影区域变黑Shade ColorShade 定义过暗,而不是 Shadow Map 直接乘黑
MatCap 随模型旋转不符合预期View Matrix / View NormalMatCap 本身固定在观察空间
Rim 覆盖整个表面Lift / Fresnel PowerLift 过高或 Power 过低
Sidecar 无法保存Texture Source Path纹理没有可转换的来源路径
Sidecar 无法加载Material Name / Field Error当前模型材质名不匹配或 JSON 字段非法
操作滑块时相机移动ImGui Mouse CaptureViewer 未拦截 UI 捕获状态

材质工作流的延伸

MToon 已经覆盖 Base/Shade、Normal、Directional Shadow、半球环境光、MatCap、Rim、Emission、分量调试和外部 Sidecar 编辑,并通过 Opaque Forward Pass 进入 HDR 后处理链路。

现有结构可以继续增加 Masked 与 Transparent Pass,并把 Outline、UV Animation、Alpha Cutoff/Blend、Cull Mode、Render Queue 和纹理变换接入材质参数。Sidecar 也可以从材质名称匹配扩展到稳定资产标识,使模型材质重名和改名后的配置迁移更加可靠。

材质由此从模型导入后的静态参数,扩展为可以在运行时选择、编辑、调试、重置并持久化的工作对象。Asset 保存源数据,MaterialInstance 保存模板相关编辑状态,RuntimeMaterial 负责 GPU 绑定,Viewer 负责工具交互,Sidecar 负责跨运行保存。MToon 的各项效果沿着这条职责链进入现有渲染流程,模型导入、逐帧提取和 Frame Pipeline 仍保持各自清晰的功能划分。

Coach

基于本文开始提问

跳转到 AI Coach,并将当前文章作为测试上下文。

开始测试