目标:为 Horse3D 引入一套可配置、可缓存、易于扩展的材质系统。着色器代码不再硬编码,而是通过
.materialJSON 文件描述;材质同时负责管理 Shader、Uniform 与纹理的绑定。
Bilibili 同步视频
一、为什么需要材质系统
最早的三角形示例把顶点着色器和片段着色器直接写成 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);
}
三、模块划分
材质相关代码被拆到两个子项目,保持依赖方向清晰:
- Percheron :只依赖 Qt,定义
Material、Uniform、Texture2D等资源。 - Diligencier :依赖 Percheron,负责解析
.material、编译 Shader、加载纹理。 - Dragon:依赖前两者,在渲染线程里真正使用材质。
这样即使以后替换构建工具链,Percheron 里的资源定义也无需改动。
四、核心类关系
4.1 Material
Material 是运行时容器:
- 持有一个
QOpenGLShaderProgram指针(由MaterialBuilder缓存并复用,材质本身不拥有它)。 - 拥有根据 JSON 创建的
IUniform集合和Texture2D列表。 bind()绑定程序并遍历 Uniform 上传;release()解绑程序并释放占用的纹理单元。
4.2 Uniform 体系
IUniform 只暴露一个 useUniform(program) 接口。具体子类负责把值推到 GPU:
ColorUniform、FloatUniform、Vector*Uniform、Matrix4x4Uniform:直接调用setUniformValue。Texture2DUniform:激活指定纹理单元,绑定QOpenGLTexture,并把采样器 Uniform 设为该单元索引。
这样 Material::bind() 无需知道 Uniform 类型,统一遍历即可。
4.3 MaterialBuilder
MaterialBuilder 是单例,职责有三项:
- 读取
.materialJSON; - 按规范路径 缓存编译好的
QOpenGLShaderProgram,避免重复编译; - 根据
Uniforms段为Material填充 Uniform 和纹理。
相对路径以 .material 所在目录为基目录解析,因此材质资源可以整体打包移动。
4.4 TextureLoader & FileKit
FileKit::readText负责读取文本文件(JSON、GLSL)。TextureLoader把QImage转成QOpenGLTexture,并做垂直翻转(mirrored())以匹配 OpenGL 原点。
五、运行时流程
5.1 材质加载
5.2 一帧渲染
渲染线程把材质和几何体组装进 Object3D 后,每帧只需调用 Object3D::render(camera)。对于镜像地板这种需要动态纹理的 Pass,再传入运行时纹理 ID,由 Object3D 设置额外的 u_mirrorTexture 并绑定。
GUI 线程的 IScreen 也使用材质系统,只是它只负责把渲染线程产出的共享纹理画到一个全屏四边形上。
六、设计取舍与踩坑
-
程序缓存 vs. 材质拥有
MaterialBuilder缓存QOpenGLShaderProgram,多个Material共享同一份程序;Material析构时只删自己的 Uniform 和纹理。 -
纹理单元分配
目前按 Uniform 出现顺序依次分配
0, 1, 2...,在 demo 里够用,但后期需要独立的纹理单元管理器。 -
JSON 解析容错
遇到不认识的 Uniform 类型会直接跳过;编译/链接失败会打印 shader log。生产环境建议加入更严格的验证和默认回退纹理。
-
QImage 原点
QImage原点在左上角,OpenGL 纹理原点在左下角,加载时需要用image.mirrored()翻转。
七、当前成果
- 着色器完全外置,通过
.materialJSON 配置加载。 Material统一管理 Shader、Uniform、纹理。MaterialBuilder提供程序缓存,避免重复编译。- 渲染线程和显示端都通过统一的
Material::bind/release使用材质。 - 依赖关系清晰:Percheron 定义资源,Diligencier 负责构建,Dragon 负责渲染。
八、下一步
- 支持材质实例化:共享同一 ShaderProgram,但拥有不同 Uniform 值。
- 引入 Uniform Buffer Object(UBO),批量上传每帧变化的矩阵。
- 材质热重载:监听
.material和.vert/.frag文件变化,运行时重新编译。 - 把
Attributes段的描述真正用起来,由MaterialBuilder驱动顶点属性布局。

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