尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Horse3D 游戏引擎研发笔记(四):光照与 Shader 预编译——从 material 到 GPU UBO
Horse3D 游戏引擎研发笔记四光照与 Shader 预编译——从 .material 到 GPU UBOBilibili 同步视频一、为什么需要预编译和 UBO二、整体流程三、.material 中的光照声明四、Shader 预处理器4.1 递归 include4.2 占位符替换五、编译、链接与缓存六、三块 UBO 的内存布局6.1 FrameBlock6.2 LightBlock6.3 PhongMaterialBlock七、LightingBinder把场景光推给 GPU八、Phong 光照模型九、设计取舍十、当前成果十一、下一步项目仓库目标梳理 Horse3D 中多光源渲染的完整链路包括.material配置的 Shader 预处理、编译缓存、Uniform Block ObjectUBO组织以及 Phong 光照模型在 GPU 上的实现。Bilibili 同步视频Horse3D 游戏引擎研发笔记四光照与 Shader 预编译一、为什么需要预编译和 UBO在笔记二里材质系统已经能把 Shader 从 JSON 配置中加载出来。但当场景引入光源后两个新问题立刻出现GLSL 中光源数组大小必须是编译期常量。运行时动态决定的光源数量无法直接声明为uniform Light lights[n]除非用 SSBO 或动态索引扩展。逐光源逐个glUniform*上传效率低。每帧每个光源都要走多次 CPU → GPU 的小数据拷贝光源一多就形成瓶颈。因此项目采用两条策略预编译在 Shader 编译前用预处理器把{{DIRECTIONAL_LIGHT_COUNT}}等占位符替换成实际数字为不同光源数量生成独立的 Shader Program 变体。UBO把每帧变化的帧数据view/projection/camera/ambient和光源数据打包成两个 Uniform Block一次glBufferSubData上传所有物体共享。二、整体流程从磁盘文件到 GPU 光照计算整个链路可以概括为编译构建预处理磁盘资源Program Uniforms运行时binding 0 / 1LightingBinder上传 UBOPhong Shader采样 UBO 计算光照Phong.materialphong.vertphong.fragLighting.glslFrameBlock.glslShaderPreprocessor展开 #includeShaderPreprocessor替换 {{LIGHT_COUNT}}MaterialBuilder编译 / 链接 / 缓存图 1光照渲染链路总览。磁盘上的.material与.vert/.frag经过预处理后编译为 Program每帧 LightingBinder 把相机和光源数据写入 UBO片元着色器直接读取完成光照计算。三、.material中的光照声明以Materials/Phong/Phong.material为例除了顶点属性和普通 Uniform 外还声明了一个 Uniform Block{Attributes:[{Name:aPos,Dimension:3},{Name:aTexCoord,Dimension:2},{Name:aNormal,Dimension:3}],Uniforms:[{Name:u_baseColor,Type:Color,Value:#d8dce6},{Name:u_diffuseStrength,Type:Float,Value:1.0},{Name:u_specularStrength,Type:Float,Value:0.45},{Name:u_shininess,Type:Float,Value:48.0}],UniformBlock:{Name:PhongMaterialBlock,Binding:2},Shaders:[{ShaderEnum:Vertex,SourceFile:phong.vert},{ShaderEnum:Fragment,SourceFile:phong.frag}]}PhongMaterialBlock用于在 GPU 端以std140布局存放材质参数。C 端由Material::uploadUniformBlock()维护一块 UBO保证与 GLSL 端内存布局一致。四、Shader 预处理器ShaderPreprocessor是一个纯静态工具类做两件事递归展开#include、替换{{NAME}}占位符。4.1 递归 include支持两种语法#include FrameBlock.glsl // 从系统 include 目录查找 #include phong.glsl // 从当前源文件目录查找核心逻辑expandIncludes会维护一个includeStack遇到循环引用立即报错返回。例如phong.frag这样写#include FrameBlock.glsl #include Lighting.glsl #include phong.glsl预处理后这三个文件的内容会被直接内联到phong.frag中。4.2 占位符替换Lighting.glsl中使用占位符定义光源数组上限#define MAX_DIRECTIONAL_LIGHTS {{DIRECTIONAL_LIGHT_COUNT}} #define MAX_POINT_LIGHTS {{POINT_LIGHT_COUNT}} #define MAX_SPOT_LIGHTS {{SPOT_LIGHT_COUNT}}MaterialBuilder::acquire在编译前传入实际数字constShaderPreprocessor::Definitions definitions{{QStringLiteral(DIRECTIONAL_LIGHT_COUNT),QString::number(directionalLightCount)},{QStringLiteral(POINT_LIGHT_COUNT),QString::number(pointLightCount)},{QStringLiteral(SPOT_LIGHT_COUNT),QString::number(spotLightCount)}};source.secondShaderPreprocessor::process(source.second,definitions);替换完成后预处理器还会检查残留占位符避免把不合法的 GLSL 交给驱动编译。五、编译、链接与缓存预处理完成后MaterialBuilder::compileAndLink使用QOpenGLShaderProgram完成 GPU 编译auto*programnewQOpenGLShaderProgram();for(constautosource:sources){auto*shadernewQOpenGLShader(source.first,program);if(!shader-compileSourceCode(source.second)){qWarning(MaterialBuilder: shader compile error:\n%s,qPrintable(shader-log()));deleteprogram;returnnullptr;}program-addShader(shader);}if(!program-link()){...}为了避免同一材质、同一光源配置重复编译MaterialBuilder用(materialPath, directionalCount, pointCount, spotCount)作为缓存键QString keyQFileInfo(materialPath).canonicalFilePath();keyQStringLiteral(|lights:%1,%2,%3).arg(directionalLightCount).arg(pointLightCount).arg(spotLightCount);if(m_cache.contains(key))returnm_cache.value(key);这样场景中有 1 方向光 4 点光源时会生成一份独立的 Program切回 1 方向光 1 点光源时如果之前编译过就直接复用。六、三块 UBO 的内存布局Horse3D 把整个渲染需要用到的常量数据分成三个 Uniform BlockBindingBlock 名称内容更新频率0FrameBlockview、projection、camera position、ambient color/intensity每帧一次1LightBlockdirectional / point / spot 光源数组每帧一次2PhongMaterialBlockbaseColor、diffuseStrength、specularStrength、shininess材质绑定时6.1 FrameBlocklayout(std140, binding 0) uniform FrameBlock { mat4 u_view; mat4 u_projection; vec4 u_cameraPosition; vec4 u_ambientColorIntensity; };C 端对应LightingBinder::FrameDatastructalignas(16)FrameData{floatview[16];floatprojection[16];QVector4D cameraPosition;QVector4D ambientColorIntensity;};6.2 LightBlockstruct DirectionalLightData { vec4 direction; vec4 colorIntensity; }; struct PointLightData { vec4 positionRange; vec4 colorIntensity; }; struct SpotLightData { vec4 positionRange; vec4 directionInnerCos; vec4 colorIntensity; vec4 coneParameters; }; layout(std140, binding 1) uniform LightBlock { DirectionalLightData u_directionalLights[MAX_DIRECTIONAL_LIGHTS]; PointLightData u_pointLights[MAX_POINT_LIGHTS]; SpotLightData u_spotLights[MAX_SPOT_LIGHTS]; };C 端使用std::vectorGpuDirectionalLight等结构体按std140对齐顺序打包成一块QByteArray然后一次性上传。6.3 PhongMaterialBlocklayout(std140, binding 2) uniform PhongMaterialBlock { vec4 u_baseColor; float u_diffuseStrength; float u_specularStrength; float u_shininess; };注意std140布局要求vec4后紧跟的标量会填充到vec4边界因此 C 端必须显式补paddingstructalignas(16)PhongMaterialData{QVector4D baseColor;floatdiffuseStrength;floatspecularStrength;floatshininess;floatpadding;};static_assert(sizeof(PhongMaterialData)32,Phong material data must match the std140 block layout);七、LightingBinder把场景光推给 GPULightingBinder是连接场景与 Shader 的桥梁。每帧渲染 Pass 开始时调用beginPassvoidLightingBinder::beginPass(constScenarioscenario,constCameracamera,quint32 lightMask)执行步骤把camera.viewMatrix()、camera.projectionMatrix()、相机位置、环境光写入FrameData遍历scenario.lights()根据lightMask和各类光源容量筛选有效光源将光源数组顺序拷贝到一块连续的QByteArray调用glBufferSubData更新 UBO并绑定到 binding 0 和 1。auto*glBrush::brush()-gl();gl-glBindBuffer(GL_UNIFORM_BUFFER,m_frameUbo);gl-glBufferSubData(GL_UNIFORM_BUFFER,0,sizeof(frame),frame);gl-glBindBufferBase(GL_UNIFORM_BUFFER,0,m_frameUbo);gl-glBindBuffer(GL_UNIFORM_BUFFER,m_lightUbo);gl-glBufferSubData(GL_UNIFORM_BUFFER,0,lightBuffer.size(),lightBuffer.constData());gl-glBindBufferBase(GL_UNIFORM_BUFFER,1,m_lightUbo);在绘制具体 Object3D 之前LightingBinder::bind()再次确认 UBO 绑定gl-glBindBufferBase(GL_UNIFORM_BUFFER,0,m_frameUbo);gl-glBindBufferBase(GL_UNIFORM_BUFFER,1,m_lightUbo);八、Phong 光照模型片元着色器phong.frag在得到 UBO 数据后按经典 Phong 模型叠加各光源贡献vec3 normal normalize(vNormal); vec3 viewDirection normalize(u_cameraPosition.xyz - vWorldPosition); vec3 color calculateAmbientLight(); for (int i 0; i MAX_DIRECTIONAL_LIGHTS; i) color calculateDirectionalLight(u_directionalLights[i], normal, viewDirection); for (int i 0; i MAX_POINT_LIGHTS; i) color calculatePointLight(u_pointLights[i], normal, viewDirection); for (int i 0; i MAX_SPOT_LIGHTS; i) color calculateSpotLight(u_spotLights[i], normal, viewDirection); fragColor vec4(color, u_baseColor.a);核心评估函数evaluatePhong实现漫反射与 Blinn-Phong 风格的镜面反射vec3 evaluatePhong(vec3 normal, vec3 viewDirection, vec3 lightDirection, vec3 radiance) { float diffuse max(dot(normal, lightDirection), 0.0) * u_diffuseStrength; float specular 0.0; if (diffuse 0.0) { vec3 reflectionDirection reflect(-lightDirection, normal); specular pow(max(dot(viewDirection, reflectionDirection), 0.0), max(u_shininess, 1.0)) * u_specularStrength; } return radiance * (u_baseColor.rgb * diffuse vec3(specular)); }点光源和聚光灯额外计算距离衰减与锥形裁剪float distanceAttenuation(float distanceToLight, float range) { float rangeFactor clamp(1.0 - distanceToLight / range, 0.0, 1.0); return rangeFactor * rangeFactor / max(distanceToLight * distanceToLight, 0.01); }聚光灯通过内外锥角度余弦做smoothstep边缘过渡避免硬边锯齿。九、设计取舍设计点当前方案替代方案选择理由光源数组大小预编译期通过占位符确定生成 Shader 变体运行时 SSBO 动态数组兼容性好实现简单光源数量有限时变体数可控缓存键(materialPath, dirCount, pointCount, spotCount)只按 path 缓存不同光照配置需要不同 Program必须区分光照传输两个 UBOFrame Light单个超大 UBO 或逐 Uniform职责清晰Frame 数据与 Light 数据更新时机不同材质参数单独 UBO binding 2普通 Uniform减少绑定切换后续可扩展为通用材质参数块Spot 锥形衰减smoothstep内外锥余弦硬裁剪或指数衰减边缘柔和更符合现代渲染习惯环境光放在 FrameBlock 中单独 Uniform环境光是全局属性与相机数据同属每帧常量十、当前成果.material支持声明UniformBlock材质参数可通过 UBO 上传。ShaderPreprocessor实现#include展开与{{LIGHT_COUNT}}占位符替换。MaterialBuilder按光照配置缓存 Shader Program 变体避免重复编译。LightingBinder用两个 UBO 批量传输帧数据与光源数据。片元着色器实现方向光、点光源、聚光灯的 Phong 光照叠加。十一、下一步方向目标依赖基础SSBO 光源光源数量动态化彻底消除 Shader 变体当前 UBO 预编译体系Shadow Map为方向光/聚光灯生成深度贴图多 Pass 管线 多相机PBR 材质用 Metallic-Roughness 模型替换 PhongUniformBlock 材质参数体系Shader 热重载监听.material/.frag/.vert变化并重新编译MaterialBuilder 缓存IBL 环境光照基于 CubeMap 的漫反射与镜面反射TextureCube LightBlock项目仓库Giteehttps://gitee.com/shendeyidi/softwarer-horse本系列记录 Horse3D 游戏引擎从零开始的研发过程欢迎交流。
RELATED

相关推荐

7 OSEK OS Alarm/8 Messages

7 OSEK OS Alarm/8 Messages

7 Alarm7.1 General操作系统提供处理重复事件的服务。这些事件可能是,例如,提供定期中断的timer,或轴(凸轮轴或曲轴)的角度不断变化的情况下产生中断的编码器,或其他常规应用程序特定的触发器。OS提供了两个阶段的概念来处理此类事…

📅 2026/9/23 4:41:32
Hide Mock Location完整指南:三步隐藏Android模拟位置设置,轻松绕过应用检测

Hide Mock Location完整指南:三步隐藏Android模拟位置设置,轻松绕过应用检测

Hide Mock Location完整指南:三步隐藏Android模拟位置设置,轻松绕过应用检测 【免费下载链接】HideMockLocation Xposed module to hide the mock location setting. 项目地址: https://gitcode.com/gh_mirrors/hi/HideMockLocation 你是否在使用…

📅 2026/9/22 5:03:04
2026年邵阳专业包装设计公司费用明细大揭秘

2026年邵阳专业包装设计公司费用明细大揭秘

导语在商业市场中,产品的包装设计至关重要,它不仅是产品的“外衣”,更承载着品牌形象与营销信息。2026年,邵阳的专业包装设计公司费用情况备受关注。相传国际作为行业内有一定影响力的品牌,一直关注着行业动态。接下来…

📅 2026/9/16 16:59:43
MORE NEWS

更多资讯

📰

OpenCV图像灰度化处理详解:原理、方法与常见坑

写这一篇的时候,我其实有点感触。上一篇我们花了很大功夫把OpenCV的环境跑通,很多读者反馈说卡在安装、卡在第一个imread上,但只要你把那张彩色图片成功显示出来,OpenCV的大门就算迈进来了。这一篇我打算讲一个看起来特别简单、但…

📰

3个技巧搞定裴讯路由器升级后API全变痛点

3个技巧搞定裴讯路由器升级后API全变痛点 昨晚十点,刚把裴讯路由器刷完新固件,准备跑一遍之前写好的自动化测试脚本。结果一执行,满屏红色的 404 Not Found 和 401 Unauthorized 。…

📰

图像压缩技术:传统DCT与深度学习方案对比

1. 项目背景与核心价值在数字图像处理领域,图像压缩技术始终是基础而关键的课题。这个项目同时实现了传统压缩算法和基于深度学习的现代方案,为研究者提供了难得的横向对比平台。我完整复现了该系统的Matlab实现,过程中发现其设计有三大亮点&…

📰

Windows多用户与NTFS权限实现程序数据隔离的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

STM32H7裸机移植LWIP实战:LAN8720以太网通信与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Redwood Cells 完全指南:用声明式组件优雅地处理 GraphQL 数据获取

Redwood Cells 完全指南:用声明式组件优雅地处理 GraphQL 数据获取 【免费下载链接】redwood RedwoodGraphQL 项目地址: https://gitcode.com/gh_mirrors/re/redwood Redwood 的 Cells 是一种"声明式数据获取"模式:你只需按约定导出 QU…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬