首页 > 教程攻略 > ai教程 >Horse3D 游戏引擎研发笔记(二):材质系统——从 JSON 配置到 GPU Uniform

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

来源:互联网 时间:2026-07-21 07:20:08

Bilibili 同步视频

一、为什么需要材质系统

想想看,最早的三角形示例里,顶点着色器和片段着色器是直接塞在C++字符串里的。能跑是能跑,但改个颜色或者加个Uniform,就得重新编译整个项目。更别提多个物体共用Shader时的重复编译问题了。

所以,干脆把“材质”抽象成引擎里的一等公民。一个材质文件,同时说清楚三件事:用哪几个着色器阶段、顶点属性怎么布局、默认Uniform值是什么(颜色、标量、向量、矩阵、纹理)。这样一来,美术或策划同学改改JSON和GLSL就好,完全不用碰C++代码,这才是真正的解耦。

二、.material 文件格式

材质文件统一放在 Materials/ 目录下,用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" }]}

对应的片段着色器也很简单,就一个纹理采样:

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

三、模块划分

材质相关的代码,拆到了两个子项目里,依赖方向必须清晰:

flowchart TBsubgraph Dragon["Dragon(渲染层)"]RT[RenderThread]OB[Object3D]BR[Brush]CA[Camera]IS[IScreen]endsubgraph Diligencier["Diligencier(工具层)"]MB[MaterialBuilder]TL[TextureLoader]FK[FileKit]endsubgraph Percheron["Percheron(资源层)"]MA[Material]IU[IUniform]TX[Texture2D]endRT --> OBOB --> MAOB --> BR & CAIS --> MAMB --> MA & FK & TLTL --> TXMA --> IUIU --> TX

  • Percheron

    :只依赖Qt,定义 MaterialUniformTexture2D 这些基础资源。
  • Diligencier

    :依赖 Percheron,负责解析 .material 文件、编译Shader、加载纹理。
  • Dragon

    :依赖前两者,在渲染线程里真正使用材质。

这样分层之后,即使以后要替换构建工具链,Percheron 里的资源定义也完全不需要改动。

四、核心类关系

classDiagramIAsset <|-- MaterialIAsset <|-- ITextureITexture <|-- Texture2DMaterial o-- IUniformMaterial o-- Texture2DIUniform <|-- ColorUniformIUniform <|-- FloatUniformIUniform <|-- IntUniformIUniform <|-- Vector2UniformIUniform <|-- Vector3UniformIUniform <|-- Vector4UniformIUniform <|-- Matrix4x4UniformIUniform <|-- Texture2DUniformTexture2DUniform --> Texture2DMaterialBuilder ..> Material : buildsTextureLoader ..> Texture2D : loadsFileKit ..> 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 材质加载

sequenceDiagramparticipant RT as RenderThreadparticipant MB as MaterialBuilderparticipant FK as FileKitparticipant TL as TextureLoaderparticipant MA as MaterialRT->>MB: initializeMaterial(material)MB->>FK: readText(.material)FK-->>MB: JSONMB->>MB: parseShaderSourcesMB->>FK: readText(.vert / .frag)FK-->>MB: source codeMB->>MB: compileAndLinkMB-->>MA: QOpenGLShaderProgramMB->>MB: parseUniformsMB->>TL: loadTexture2D(image)TL-->>MB: Texture2DMB-->>MA: Uniforms & Textures

5.2 一帧渲染

flowchart LRA[Object3D::render] --> B[Material::bind]B --> C[program->bind]C --> D[遍历 Uniform
useUniform 上传]D --> E[设置 u_model]E --> F[Camera::apply
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 游戏引擎从零开始的研发过程。