Horse3D 游戏引擎研发笔记(二):材质系统——从 JSON 配置到 GPU Uniform

目标:为 Horse3D 引入一套可配置、可缓存、易于扩展的材质系统。着色器代码不再硬编码,而是通过 .material JSON 文件描述;材质同时负责管理 Shader、Uniform 与纹理的绑定。

Bilibili 同步视频

Horse3D 游戏引擎研发笔记(二):材质系统------从 JSON 配置到 GPU Uniform

一、为什么需要材质系统

最早的三角形示例把顶点着色器和片段着色器直接写成 C++ 字符串。虽然能跑,但改一个颜色或加一个 Uniform 就要重新编译整个项目,多个物体共用 Shader 时还会重复编译。

因此把"材质"抽象成一等公民:一个材质文件同时描述

  • 使用哪几个着色器阶段;
  • 顶点属性布局;
  • 默认 Uniform 值(颜色、标量、向量、矩阵、纹理)。

这样美术或策划只需要改 JSON 和 GLSL,不需要碰 C++。

二、.material 文件格式

材质文件放在 Materials/ 目录下,以 JSON 描述。以三角形材质为例:

json 复制代码
{
    "Attributes": [
        { "Name": "aPos", "Dimension": 3 },
        { "Name": "aTexCoord", "Dimension": 2 }
    ],
    "Uniforms": [
        { "Name": "u_texture", "Type": "Texture2D", "Value": "Triangle.jpeg" }
    ],
    "Shaders": [
        { "ShaderEnum": "Vertex",   "SourceFile": "triangle.vert" },
        { "ShaderEnum": "Fragment", "SourceFile": "triangle.frag" }
    ]
}

对应的片段着色器只做了一个纹理采样:

glsl 复制代码
uniform sampler2D u_texture;

void main()
{
    fragColor = texture(u_texture, vTexCoord);
}

三、模块划分

材质相关代码被拆到两个子项目,保持依赖方向清晰:

flowchart TB subgraph Dragon["Dragon(渲染层)"] RT[RenderThread] OB[Object3D] BR[Brush] CA[Camera] IS[IScreen] end subgraph Diligencier["Diligencier(工具层)"] MB[MaterialBuilder] TL[TextureLoader] FK[FileKit] end subgraph Percheron["Percheron(资源层)"] MA[Material] IU[IUniform] TX[Texture2D] end RT --> OB OB --> MA OB --> BR & CA IS --> MA MB --> MA & FK & TL TL --> TX MA --> IU IU --> TX
  • Percheron :只依赖 Qt,定义 MaterialUniformTexture2D 等资源。
  • Diligencier :依赖 Percheron,负责解析 .material、编译 Shader、加载纹理。
  • Dragon:依赖前两者,在渲染线程里真正使用材质。

这样即使以后替换构建工具链,Percheron 里的资源定义也无需改动。

四、核心类关系

classDiagram IAsset <|-- Material IAsset <|-- ITexture ITexture <|-- Texture2D Material o-- IUniform Material o-- Texture2D IUniform <|-- ColorUniform IUniform <|-- FloatUniform IUniform <|-- IntUniform IUniform <|-- Vector2Uniform IUniform <|-- Vector3Uniform IUniform <|-- Vector4Uniform IUniform <|-- Matrix4x4Uniform IUniform <|-- Texture2DUniform Texture2DUniform --> Texture2D MaterialBuilder ..> Material : builds TextureLoader ..> Texture2D : loads FileKit ..> MaterialBuilder : reads JSON

4.1 Material

Material 是运行时容器:

  • 持有一个 QOpenGLShaderProgram 指针(由 MaterialBuilder 缓存并复用,材质本身不拥有它)。
  • 拥有根据 JSON 创建的 IUniform 集合和 Texture2D 列表。
  • bind() 绑定程序并遍历 Uniform 上传;release() 解绑程序并释放占用的纹理单元。

4.2 Uniform 体系

IUniform 只暴露一个 useUniform(program) 接口。具体子类负责把值推到 GPU:

  • ColorUniformFloatUniformVector*UniformMatrix4x4Uniform:直接调用 setUniformValue
  • Texture2DUniform:激活指定纹理单元,绑定 QOpenGLTexture,并把采样器 Uniform 设为该单元索引。

这样 Material::bind() 无需知道 Uniform 类型,统一遍历即可。

4.3 MaterialBuilder

MaterialBuilder 是单例,职责有三项:

  1. 读取 .material JSON;
  2. 规范路径 缓存编译好的 QOpenGLShaderProgram,避免重复编译;
  3. 根据 Uniforms 段为 Material 填充 Uniform 和纹理。

相对路径以 .material 所在目录为基目录解析,因此材质资源可以整体打包移动。

4.4 TextureLoader & FileKit

  • FileKit::readText 负责读取文本文件(JSON、GLSL)。
  • TextureLoaderQImage 转成 QOpenGLTexture,并做垂直翻转(mirrored())以匹配 OpenGL 原点。

五、运行时流程

5.1 材质加载

sequenceDiagram participant RT as RenderThread participant MB as MaterialBuilder participant FK as FileKit participant TL as TextureLoader participant MA as Material RT->>MB: initializeMaterial(material) MB->>FK: readText(.material) FK-->>MB: JSON MB->>MB: parseShaderSources MB->>FK: readText(.vert / .frag) FK-->>MB: source code MB->>MB: compileAndLink MB-->>MA: QOpenGLShaderProgram MB->>MB: parseUniforms MB->>TL: loadTexture2D(image) TL-->>MB: Texture2D MB-->>MA: Uniforms & Textures

5.2 一帧渲染

flowchart LR A[Object3D::render] --> B[Material::bind] B --> C[program->bind] C --> D[遍历 Uniform<br/>useUniform 上传] D --> E[设置 u_model] E --> F[Camera::apply<br/>u_view / u_projection] F --> G[Brush::drawGeometry] G --> H[Material::release]

渲染线程把材质和几何体组装进 Object3D 后,每帧只需调用 Object3D::render(camera)。对于镜像地板这种需要动态纹理的 Pass,再传入运行时纹理 ID,由 Object3D 设置额外的 u_mirrorTexture 并绑定。

GUI 线程的 IScreen 也使用材质系统,只是它只负责把渲染线程产出的共享纹理画到一个全屏四边形上。

六、设计取舍与踩坑

  1. 程序缓存 vs. 材质拥有

    MaterialBuilder 缓存 QOpenGLShaderProgram,多个 Material 共享同一份程序;Material 析构时只删自己的 Uniform 和纹理。

  2. 纹理单元分配

    目前按 Uniform 出现顺序依次分配 0, 1, 2...,在 demo 里够用,但后期需要独立的纹理单元管理器。

  3. JSON 解析容错

    遇到不认识的 Uniform 类型会直接跳过;编译/链接失败会打印 shader log。生产环境建议加入更严格的验证和默认回退纹理。

  4. QImage 原点

    QImage 原点在左上角,OpenGL 纹理原点在左下角,加载时需要用 image.mirrored() 翻转。

七、当前成果

  • 着色器完全外置,通过 .material JSON 配置加载。
  • Material 统一管理 Shader、Uniform、纹理。
  • MaterialBuilder 提供程序缓存,避免重复编译。
  • 渲染线程和显示端都通过统一的 Material::bind/release 使用材质。
  • 依赖关系清晰:Percheron 定义资源,Diligencier 负责构建,Dragon 负责渲染。

八、下一步

  • 支持材质实例化:共享同一 ShaderProgram,但拥有不同 Uniform 值。
  • 引入 Uniform Buffer Object(UBO),批量上传每帧变化的矩阵。
  • 材质热重载:监听 .material.vert/.frag 文件变化,运行时重新编译。
  • Attributes 段的描述真正用起来,由 MaterialBuilder 驱动顶点属性布局。

本系列记录 Horse3D 游戏引擎从零开始的研发过程,欢迎交流。

相关推荐
SmalBox1 天前
【节点】[EvaluateSimulationDisplacement节点]原理解析与实际应用
unity3d·游戏开发·图形学
晓杰在写后端2 天前
从0到1实现Balatro游戏后端(11):商店商品池与运行时商品实例生成
后端·游戏开发
SmalBox2 天前
【节点】[EvaluateSimulationCaustics节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[EvaluateSimulationAdditionalData节点]原理解析与实际应用
unity3d·游戏开发·图形学
在水一缸4 天前
Box3D 深度解析:当物理引擎传奇迈向三维空间
3d·游戏开发·开源项目·物理引擎·box3d·3d仿真·box2d
SmalBox4 天前
【节点】[EvaluateScatteringColor节点]原理解析与实际应用
unity3d·游戏开发·图形学
孟无岐4 天前
LayaAir-MCP 插件上手指南:让 AI 直接操控你的 LayaAir IDE
ai编程·游戏开发·mcp·layaair·workbuddy·layaair-mcp