欢迎光临
我们一直在努力

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

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 JSON;
  • 按规范路径缓存编译好的 QOpenGLShaderProgram,避免重复编译;
  • 根据 Uniforms 段为 Material 填充 Uniform 和纹理。
  • 相对路径以 .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 游戏引擎研发笔记(二):材质系统——从 JSON 配置到 GPU Uniform

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

    赞(0)
    未经允许不得转载:171主机测评 » Horse3D 游戏引擎研发笔记(二):材质系统——从 JSON 配置到 GPU Uniform
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址