Horse3D 游戏引擎研发笔记(二):材质系统——从 JSON 配置到 GPU Uniform
- Bilibili 同步视频
- 一、为什么需要材质系统
- 二、`.material` 文件格式
- 三、模块划分
- 四、核心类关系
-
- 4.1 Material
- 4.2 Uniform 体系
- 4.3 MaterialBuilder
- 4.4 TextureLoader & FileKit
- 五、运行时流程
-
- 5.1 材质加载
- 5.2 一帧渲染
- 六、设计取舍与踩坑
- 七、当前成果
- 八、下一步
目标:为 Horse3D 引入一套可配置、可缓存、易于扩展的材质系统。着色器代码不再硬编码,而是通过 .material JSON 文件描述;材质同时负责管理 Shader、Uniform 与纹理的绑定。
Bilibili 同步视频
Horse3D 游戏引擎研发笔记(二):材质系统——从 JSON 配置到 GPU Uniform
一、为什么需要材质系统
最早的三角形示例把顶点着色器和片段着色器直接写成 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);
}
三、模块划分
材质相关代码被拆到两个子项目,保持依赖方向清晰:
#mermaid-svg-DY6qtjfJZCoiQagF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DY6qtjfJZCoiQagF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DY6qtjfJZCoiQagF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DY6qtjfJZCoiQagF .error-icon{fill:#552222;}#mermaid-svg-DY6qtjfJZCoiQagF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DY6qtjfJZCoiQagF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DY6qtjfJZCoiQagF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DY6qtjfJZCoiQagF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DY6qtjfJZCoiQagF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DY6qtjfJZCoiQagF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DY6qtjfJZCoiQagF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DY6qtjfJZCoiQagF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DY6qtjfJZCoiQagF .marker.cross{stroke:#333333;}#mermaid-svg-DY6qtjfJZCoiQagF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DY6qtjfJZCoiQagF p{margin:0;}#mermaid-svg-DY6qtjfJZCoiQagF .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-DY6qtjfJZCoiQagF .cluster-label text{fill:#333;}#mermaid-svg-DY6qtjfJZCoiQagF .cluster-label span{color:#333;}#mermaid-svg-DY6qtjfJZCoiQagF .cluster-label span p{background-color:transparent;}#mermaid-svg-DY6qtjfJZCoiQagF .label text,#mermaid-svg-DY6qtjfJZCoiQagF span{fill:#333;color:#333;}#mermaid-svg-DY6qtjfJZCoiQagF .node rect,#mermaid-svg-DY6qtjfJZCoiQagF .node circle,#mermaid-svg-DY6qtjfJZCoiQagF .node ellipse,#mermaid-svg-DY6qtjfJZCoiQagF .node polygon,#mermaid-svg-DY6qtjfJZCoiQagF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DY6qtjfJZCoiQagF .rough-node .label text,#mermaid-svg-DY6qtjfJZCoiQagF .node .label text,#mermaid-svg-DY6qtjfJZCoiQagF .image-shape .label,#mermaid-svg-DY6qtjfJZCoiQagF .icon-shape .label{text-anchor:middle;}#mermaid-svg-DY6qtjfJZCoiQagF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DY6qtjfJZCoiQagF .rough-node .label,#mermaid-svg-DY6qtjfJZCoiQagF .node .label,#mermaid-svg-DY6qtjfJZCoiQagF .image-shape .label,#mermaid-svg-DY6qtjfJZCoiQagF .icon-shape .label{text-align:center;}#mermaid-svg-DY6qtjfJZCoiQagF .node.clickable{cursor:pointer;}#mermaid-svg-DY6qtjfJZCoiQagF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DY6qtjfJZCoiQagF .arrowheadPath{fill:#333333;}#mermaid-svg-DY6qtjfJZCoiQagF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DY6qtjfJZCoiQagF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DY6qtjfJZCoiQagF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DY6qtjfJZCoiQagF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DY6qtjfJZCoiQagF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DY6qtjfJZCoiQagF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DY6qtjfJZCoiQagF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DY6qtjfJZCoiQagF .cluster text{fill:#333;}#mermaid-svg-DY6qtjfJZCoiQagF .cluster span{color:#333;}#mermaid-svg-DY6qtjfJZCoiQagF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DY6qtjfJZCoiQagF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DY6qtjfJZCoiQagF rect.text{fill:none;stroke-width:0;}#mermaid-svg-DY6qtjfJZCoiQagF .icon-shape,#mermaid-svg-DY6qtjfJZCoiQagF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DY6qtjfJZCoiQagF .icon-shape p,#mermaid-svg-DY6qtjfJZCoiQagF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DY6qtjfJZCoiQagF .icon-shape .label rect,#mermaid-svg-DY6qtjfJZCoiQagF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DY6qtjfJZCoiQagF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DY6qtjfJZCoiQagF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DY6qtjfJZCoiQagF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Percheron(资源层)
Diligencier(工具层)
Dragon(渲染层)
RenderThread
Object3D
Brush
Camera
IScreen
MaterialBuilder
TextureLoader
FileKit
Material
IUniform
Texture2D
- Percheron:只依赖 Qt,定义 Material、Uniform、Texture2D 等资源。
- Diligencier:依赖 Percheron,负责解析 .material、编译 Shader、加载纹理。
- Dragon:依赖前两者,在渲染线程里真正使用材质。
这样即使以后替换构建工具链,Percheron 里的资源定义也无需改动。
四、核心类关系
#mermaid-svg-HOX01cZ1LaNXLoyH{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HOX01cZ1LaNXLoyH .error-icon{fill:#552222;}#mermaid-svg-HOX01cZ1LaNXLoyH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HOX01cZ1LaNXLoyH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HOX01cZ1LaNXLoyH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HOX01cZ1LaNXLoyH .marker.cross{stroke:#333333;}#mermaid-svg-HOX01cZ1LaNXLoyH svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HOX01cZ1LaNXLoyH p{margin:0;}#mermaid-svg-HOX01cZ1LaNXLoyH g.classGroup text{fill:#9370DB;stroke:none;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-HOX01cZ1LaNXLoyH g.classGroup text .title{font-weight:bolder;}#mermaid-svg-HOX01cZ1LaNXLoyH .cluster-label text{fill:#333;}#mermaid-svg-HOX01cZ1LaNXLoyH .cluster-label span{color:#333;}#mermaid-svg-HOX01cZ1LaNXLoyH .cluster-label span p{background-color:transparent;}#mermaid-svg-HOX01cZ1LaNXLoyH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HOX01cZ1LaNXLoyH .cluster text{fill:#333;}#mermaid-svg-HOX01cZ1LaNXLoyH .cluster span{color:#333;}#mermaid-svg-HOX01cZ1LaNXLoyH .nodeLabel,#mermaid-svg-HOX01cZ1LaNXLoyH .edgeLabel{color:#131300;}#mermaid-svg-HOX01cZ1LaNXLoyH .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-HOX01cZ1LaNXLoyH .label text{fill:#131300;}#mermaid-svg-HOX01cZ1LaNXLoyH .labelBkg{background:#ECECFF;}#mermaid-svg-HOX01cZ1LaNXLoyH .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-HOX01cZ1LaNXLoyH .classTitle{font-weight:bolder;}#mermaid-svg-HOX01cZ1LaNXLoyH .node rect,#mermaid-svg-HOX01cZ1LaNXLoyH .node circle,#mermaid-svg-HOX01cZ1LaNXLoyH .node ellipse,#mermaid-svg-HOX01cZ1LaNXLoyH .node polygon,#mermaid-svg-HOX01cZ1LaNXLoyH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HOX01cZ1LaNXLoyH .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH g.clickable{cursor:pointer;}#mermaid-svg-HOX01cZ1LaNXLoyH g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-HOX01cZ1LaNXLoyH g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-HOX01cZ1LaNXLoyH .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-HOX01cZ1LaNXLoyH .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-HOX01cZ1LaNXLoyH .dashed-line{stroke-dasharray:3;}#mermaid-svg-HOX01cZ1LaNXLoyH .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-HOX01cZ1LaNXLoyH #compositionStart,#mermaid-svg-HOX01cZ1LaNXLoyH .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #compositionEnd,#mermaid-svg-HOX01cZ1LaNXLoyH .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #dependencyStart,#mermaid-svg-HOX01cZ1LaNXLoyH .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #dependencyStart,#mermaid-svg-HOX01cZ1LaNXLoyH .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #extensionStart,#mermaid-svg-HOX01cZ1LaNXLoyH .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #extensionEnd,#mermaid-svg-HOX01cZ1LaNXLoyH .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #aggregationStart,#mermaid-svg-HOX01cZ1LaNXLoyH .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #aggregationEnd,#mermaid-svg-HOX01cZ1LaNXLoyH .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #lollipopStart,#mermaid-svg-HOX01cZ1LaNXLoyH .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH #lollipopEnd,#mermaid-svg-HOX01cZ1LaNXLoyH .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-HOX01cZ1LaNXLoyH .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-HOX01cZ1LaNXLoyH .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HOX01cZ1LaNXLoyH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HOX01cZ1LaNXLoyH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HOX01cZ1LaNXLoyH :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
builds
loads
reads JSON
IAsset
Material
ITexture
Texture2D
IUniform
ColorUniform
FloatUniform
IntUniform
Vector2Uniform
Vector3Uniform
Vector4Uniform
Matrix4x4Uniform
Texture2DUniform
MaterialBuilder
TextureLoader
FileKit
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 是单例,职责有三项:
相对路径以 .material 所在目录为基目录解析,因此材质资源可以整体打包移动。
4.4 TextureLoader & FileKit
- FileKit::readText 负责读取文本文件(JSON、GLSL)。
- TextureLoader 把 QImage 转成 QOpenGLTexture,并做垂直翻转(mirrored())以匹配 OpenGL 原点。
五、运行时流程
5.1 材质加载
Material
TextureLoader
FileKit
MaterialBuilder
RenderThread
Material
TextureLoader
FileKit
MaterialBuilder
RenderThread
#mermaid-svg-LgU6NKcagUTJ9txF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LgU6NKcagUTJ9txF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LgU6NKcagUTJ9txF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LgU6NKcagUTJ9txF .error-icon{fill:#552222;}#mermaid-svg-LgU6NKcagUTJ9txF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LgU6NKcagUTJ9txF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LgU6NKcagUTJ9txF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LgU6NKcagUTJ9txF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LgU6NKcagUTJ9txF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LgU6NKcagUTJ9txF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LgU6NKcagUTJ9txF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LgU6NKcagUTJ9txF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LgU6NKcagUTJ9txF .marker.cross{stroke:#333333;}#mermaid-svg-LgU6NKcagUTJ9txF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LgU6NKcagUTJ9txF p{margin:0;}#mermaid-svg-LgU6NKcagUTJ9txF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LgU6NKcagUTJ9txF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-LgU6NKcagUTJ9txF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LgU6NKcagUTJ9txF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-LgU6NKcagUTJ9txF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-LgU6NKcagUTJ9txF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-LgU6NKcagUTJ9txF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-LgU6NKcagUTJ9txF .sequenceNumber{fill:white;}#mermaid-svg-LgU6NKcagUTJ9txF #sequencenumber{fill:#333;}#mermaid-svg-LgU6NKcagUTJ9txF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-LgU6NKcagUTJ9txF .messageText{fill:#333;stroke:none;}#mermaid-svg-LgU6NKcagUTJ9txF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LgU6NKcagUTJ9txF .labelText,#mermaid-svg-LgU6NKcagUTJ9txF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-LgU6NKcagUTJ9txF .loopText,#mermaid-svg-LgU6NKcagUTJ9txF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-LgU6NKcagUTJ9txF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LgU6NKcagUTJ9txF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-LgU6NKcagUTJ9txF .noteText,#mermaid-svg-LgU6NKcagUTJ9txF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-LgU6NKcagUTJ9txF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LgU6NKcagUTJ9txF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LgU6NKcagUTJ9txF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LgU6NKcagUTJ9txF .actorPopupMenu{position:absolute;}#mermaid-svg-LgU6NKcagUTJ9txF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-LgU6NKcagUTJ9txF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LgU6NKcagUTJ9txF .actor-man circle,#mermaid-svg-LgU6NKcagUTJ9txF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-LgU6NKcagUTJ9txF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
initializeMaterial(material)
readText(.material)
JSON
parseShaderSources
readText(.vert / .frag)
source code
compileAndLink
QOpenGLShaderProgram
parseUniforms
loadTexture2D(image)
Texture2D
Uniforms & Textures
5.2 一帧渲染
#mermaid-svg-whnW8ebeMzT6RxOz{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-whnW8ebeMzT6RxOz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-whnW8ebeMzT6RxOz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-whnW8ebeMzT6RxOz .error-icon{fill:#552222;}#mermaid-svg-whnW8ebeMzT6RxOz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-whnW8ebeMzT6RxOz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-whnW8ebeMzT6RxOz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-whnW8ebeMzT6RxOz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-whnW8ebeMzT6RxOz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-whnW8ebeMzT6RxOz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-whnW8ebeMzT6RxOz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-whnW8ebeMzT6RxOz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-whnW8ebeMzT6RxOz .marker.cross{stroke:#333333;}#mermaid-svg-whnW8ebeMzT6RxOz svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-whnW8ebeMzT6RxOz p{margin:0;}#mermaid-svg-whnW8ebeMzT6RxOz .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-whnW8ebeMzT6RxOz .cluster-label text{fill:#333;}#mermaid-svg-whnW8ebeMzT6RxOz .cluster-label span{color:#333;}#mermaid-svg-whnW8ebeMzT6RxOz .cluster-label span p{background-color:transparent;}#mermaid-svg-whnW8ebeMzT6RxOz .label text,#mermaid-svg-whnW8ebeMzT6RxOz span{fill:#333;color:#333;}#mermaid-svg-whnW8ebeMzT6RxOz .node rect,#mermaid-svg-whnW8ebeMzT6RxOz .node circle,#mermaid-svg-whnW8ebeMzT6RxOz .node ellipse,#mermaid-svg-whnW8ebeMzT6RxOz .node polygon,#mermaid-svg-whnW8ebeMzT6RxOz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-whnW8ebeMzT6RxOz .rough-node .label text,#mermaid-svg-whnW8ebeMzT6RxOz .node .label text,#mermaid-svg-whnW8ebeMzT6RxOz .image-shape .label,#mermaid-svg-whnW8ebeMzT6RxOz .icon-shape .label{text-anchor:middle;}#mermaid-svg-whnW8ebeMzT6RxOz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-whnW8ebeMzT6RxOz .rough-node .label,#mermaid-svg-whnW8ebeMzT6RxOz .node .label,#mermaid-svg-whnW8ebeMzT6RxOz .image-shape .label,#mermaid-svg-whnW8ebeMzT6RxOz .icon-shape .label{text-align:center;}#mermaid-svg-whnW8ebeMzT6RxOz .node.clickable{cursor:pointer;}#mermaid-svg-whnW8ebeMzT6RxOz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-whnW8ebeMzT6RxOz .arrowheadPath{fill:#333333;}#mermaid-svg-whnW8ebeMzT6RxOz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-whnW8ebeMzT6RxOz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-whnW8ebeMzT6RxOz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whnW8ebeMzT6RxOz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-whnW8ebeMzT6RxOz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whnW8ebeMzT6RxOz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-whnW8ebeMzT6RxOz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-whnW8ebeMzT6RxOz .cluster text{fill:#333;}#mermaid-svg-whnW8ebeMzT6RxOz .cluster span{color:#333;}#mermaid-svg-whnW8ebeMzT6RxOz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-whnW8ebeMzT6RxOz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-whnW8ebeMzT6RxOz rect.text{fill:none;stroke-width:0;}#mermaid-svg-whnW8ebeMzT6RxOz .icon-shape,#mermaid-svg-whnW8ebeMzT6RxOz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-whnW8ebeMzT6RxOz .icon-shape p,#mermaid-svg-whnW8ebeMzT6RxOz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-whnW8ebeMzT6RxOz .icon-shape .label rect,#mermaid-svg-whnW8ebeMzT6RxOz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-whnW8ebeMzT6RxOz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-whnW8ebeMzT6RxOz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-whnW8ebeMzT6RxOz :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Object3D::render
Material::bind
program->bind
遍历 UniformuseUniform 上传
设置 u_model
Camera::applyu_view / u_projection
Brush::drawGeometry
Material::release
渲染线程把材质和几何体组装进 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() 翻转。
七、当前成果
- 着色器完全外置,通过 .material JSON 配置加载。
- Material 统一管理 Shader、Uniform、纹理。
- MaterialBuilder 提供程序缓存,避免重复编译。
- 渲染线程和显示端都通过统一的 Material::bind/release 使用材质。
- 依赖关系清晰:Percheron 定义资源,Diligencier 负责构建,Dragon 负责渲染。
八、下一步
- 支持材质实例化:共享同一 ShaderProgram,但拥有不同 Uniform 值。
- 引入 Uniform Buffer Object(UBO),批量上传每帧变化的矩阵。
- 材质热重载:监听 .material 和 .vert/.frag 文件变化,运行时重新编译。
- 把 Attributes 段的描述真正用起来,由 MaterialBuilder 驱动顶点属性布局。

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

![set 与 map :从 [] 暗坑到严格弱序,把有序关联容器用明白-171主机测评](https://www.171host.com/wp-content/uploads/2026/10/20261031153944-6ae60bc0ee6df-220x150.png)