Compare commits

...
25 Commits
Author SHA1 Message Date
atdunbg d8c481ce48 fix: Tracy 主线程命名(未命名线程在 GUI 显示 Unknown) 2026-09-23 16:44:55 +08:00
atdunbg b79df3c60e fix: tracy 子模块钉到 v0.14.1,与 GUI 协议版本对齐
master 开发提交(81c5005)的协议号已偏离 0.14.1 release,
GUI 连接报 incompatible protocol。子模块记录精确 commit hash,
clone --recursive 的机器拿到的一直是锁定的版本
2026-09-23 16:39:06 +08:00
atdunbg b6982d426f feat: Tracy profiler 接入(CPU 帧时间线)
- vendored tracy 子模块;TRACY_ENABLE 全局生效,宏关闭时零开销
- 插桩:Application::Run FrameMark(帧边界)、Renderer::Render、
  SceneRenderer::BuildFrame(ZoneScopedN)+ GPUDriven Collect
  (ZoneNamedN——同作用域第二个 zone 不能用 ZoneScopedN,变量名
  固定会重声明)、Scene::UpdateTransforms
- 覆盖 CPU 侧关键路径:帧时间线 / 场景收集 / transform 重算 / 录制提交
- 用法:游戏运行时打开 Tracy GUI(release 下载)即自动连接(8086 端口)
2026-09-23 16:10:46 +08:00
atdunbg 7d137fa9b1 refactor: Transform 字段封装 + dirty 标记(对齐 UE USceneComponent 真实现)
- 修正上一版"全量暴力重算":UE 实为 dirty 标记 + 惰性计算,前提是
  字段私有(SetRelativeLocation 等 setter 标脏 + 子级传播)
- TransformComponent:SRT 字段私有化,Set/Get/World()/IsDirty 访问器;
  Set* 变更才标脏;AddRotationY 便捷接口
- Scene::UpdateTransforms 只重算脏实体——静止实体零成本;
  层级(父子)传播在脏标记基础上扩展
- 迁移 Camera/SceneRenderer/main.cpp 到封装 API
2026-09-23 15:35:20 +08:00
atdunbg 57ab8901a9 refactor: transform world 矩阵改由帧循环统一重算,删除手动 Compose
- Scene::UpdateTransforms():渲染前全量 Compose(UE transform-update
  阶段模式),Application::Run 在 FlushDestroyQueue 后调用
- 用户代码永远不需要手动 Compose——Set*/直接写字段均安全;
  Compose 降级为引擎内部调用
- 选择全量而非 dirty 标记:字段直接写(t.rotation.y = x)无法可靠
  追踪脏,无条件重算零脚枪;万级实体内成本可忽略,更大规模再引入
  dirty+封装
- main.cpp/Camera 所有手动 Compose 调用点删除
2026-09-23 15:26:58 +08:00
atdunbg 1bc44e261b refactor: Camera 位姿统一到 TransformComponent(单一事实源)
- Camera 删除内部 position/yaw/pitch 状态:Update 把鼠标旋转与 WASD
  移动直接写回实体 Transform(rotation.y=yaw / rotation.x=pitch,
  移动基向量由实体旋转推导,有变更才 Compose)
- GetView(transform) = 实体世界矩阵的逆——相机即实体,视图矩阵无
  第二来源;Scene::GetPrimaryCamera 可选带出实体 Transform
- 默认朝向约定变化:rotation.y=0 = 看向 -Z(与旧 yaw=-90 等价)
- 消除 CameraComponent 位置与 Transform 脱节的遗留债
2026-09-23 15:14:29 +08:00
atdunbg 019c17c7e7 feat: 实体出生即挂 TransformComponent(UUID + Transform 固有化)
- CreateEntity 自动挂 IdComponent(已有)+ TransformComponent(identity),
  对齐 UE/Unity 语义(GameObject↔Transform 不可分离 / AActor 必有
  RootComponent)——游戏实体的本质是"空间中存在的东西"
- 调用点 AddComponent<Transform> 改 GetComponent(重复 emplace 会断言)
- 遗留不一致记录在案:CameraComponent 位置存于 Camera 内部而非
  Transform,后续统一
2026-09-23 15:07:00 +08:00
atdunbg 0ed7b4c5b8 perf: CSM 每级联独立剔除 + 光向 caster pad
- 场景剔除从 [主|阴影] 两视图扩展为 [主|级联0|级联1|级联2] 四视图:
  每级联只画自己盒内的 batch,消除原先 3× 全量阴影 draw 的浪费
- 实例数组拼接为四段,batch.first 加段基址保持段内连续;
  ViewScratch 分桶收集复用容量,稳态零堆分配
- ComputeCascades 沿光方向扩展级联深度范围(pad=30)——切片盒外
  caster(高树/城墙)投影进级联的影子不会被剔除裁掉,per-cascade
  剔除正确性的前提;只扩 z 不缩 XY,分辨率不受影响
- 遥测 shadow 计数 = 各级联求和
- SunTemple 1056 mesh 全量加载,validation 零警告
2026-09-23 14:59:08 +08:00
atdunbg c54f60c5ea feat: 材质系统 v1——4 纹理槽(albedo/normal/specular/emissive)+ 法线贴图
- Material:单贴图 → 4 槽(binding=Slot 枚举),MakeCacheKey 组合键
  保持材质去重(同贴图组合共享描述符集)
- Model:读取 aiTextureType DIFFUSE/NORMALS/SPECULAR/EMISSIVE 四类路径;
  aiProcess_CalcTangentSpace 生成切线(Vertex +tangent 属性,location 3);
  每槽默认纹理兜底(白/平法线(128,128,255)/黑/黑,1x1)
- 纹理 sRGB 语义按槽位:颜色图 SRGB 变体,数据图(Normal/Specular)线性
  UNORM——Texture 路径加载加 srgb 参数;修复 InitFromPixels 图文 format
  不一致的 validation 警告
- UBO 尾部追加 cameraPos(Blinn-Phong V 向量)
- simple_shader.frag:TBN 法线贴图(无切线/平法线退化回几何法线)+
  Blinn-Phong 高光(specular 图强度系数)+ 自发光直加
- SunTemple 实测:148 张 DDS 全槽位加载(96 张线性变体)、1056 mesh、
  validation 零警告
2026-09-23 14:50:10 +08:00
atdunbg 8253c31ad5 fix: 视锥剔除平面归一化(透视因数误剔)+ 材质去重 + 描述符池可增长
- Frustum::FromMatrix 平面归一化:Gribb-Hartmann 提取的 left/right/
  bottom/top 法线模长含 1/tan(fov/2) 因子(>1),未归一化时包围球
  距离测试被放大,视锥边缘物体被误剔(SunTemple 全场景可见)——
  修正注释:必须传 [0,1] 重映射矩阵
- 材质去重:Model 新增 MaterialCache(按贴图路径共享 Material),
  Mesh 改持 Material* 非拥有指针。1056 mesh 渲染只需 48 个描述符集
  (对齐 UE/Unity 材质实例化思路,大场景第一道闸门)
- DescriptorManager 池改可增长分块(chunk=4096,OutOfPool/Fragmented
  自动续块):材质集数量不可预估(1G 资产可能上万),固定上限迟早爆
2026-09-23 14:32:35 +08:00
atdunbg a0c92af5db feat: DDS 纹理加载器(BC 压缩直传 + mip 链,参考 Horizon 实现)
- Assets::Dds 解析器:中性 DdsFormat 枚举(Assets 层禁 Vulkan 头铁律),
  支持 legacy fourcc(DXT1/3/5/ATI1/2)+ DX10 头(BC1..BC7/RGBA8/BGRA8)
  + 未压缩 32bpp + 2D mip 链;数据不解码,直接上传由硬件采样解码
- Texture:按扩展名分发(.dds → InitFromDds);渲染侧负责格式映射与
  sRGB 语义(颜色图走 SRGB 变体,BC4/5/6H 数据图保持线性);view/sampler
  覆盖完整 mip 链;逐 mip copyBufferToImage(紧凑排布)
- Model::GetOrLoadTexture:FBX/DCC 路径反斜杠归一化(否则 Linux 全挂)
- DescriptorManager 池 1000 → 4096(SunTemple 1056 材质集打爆过;
  材质去重属材质系统任务)
- main.cpp 临时挂 SunTemple 场景验证:48 张 DDS 全部加载(2048²、12 mips),
  1056 mesh 渲染正常,validation 零警告
2026-09-23 14:21:39 +08:00
atdunbg 0328d4aaeb feat: CSM 级联阴影(3 级联 × 1024²,实用分割 + 纹素对齐)
- SceneRenderer::ComputeCascades:相机视锥实用分割(λ=0.6)→ 反投影
  取切片角点 → 光空间 AABB 拟合 → 纹素对齐(消除相机移动时阴影边缘
  闪烁)→ [0,1] 深度重映射
- Shadow pass 单 pass 渲 3 个 region,每级联 push 各自光空间 VP
- Forward:UBO lightSpace[3]+cascadeSplits(视图深度选级联),Set2
  三张级联图按 binding 0/1/2 采样;vertex 输出 fragViewDepth
- 光源视锥剔除用最远级联(覆盖全阴影范围);双视锥独立剔除铁律保留
- 旧单张 2048 全场景图退役;近处级联分辨率 ~128px/unit(约 2× 提升)
- 编译零错误、validation 零警告、8 秒冒烟通过(10 pass 全活跃)
2026-09-23 13:48:43 +08:00
atdunbg 27b7f8785c feat: TAA 时域抗锯齿(velocity + 邻域 clamp + 子像素抖动)
- Velocity buffer:Forward 第二颜色附件,vertex 计算 clip 空间运动向量
  (UBO 尾部追加 projNoJitter/prevView/prevProj;上一帧实例矩阵 SSBO
  Set0 binding2,与当前实例数组平行对齐——转动的物体速度正确不鬼影)
- 子像素抖动:SceneRenderer Halton(2,3)×8 序列,抖动只进光栅化投影
- TAA resolve pass:velocity 反推历史位置 + 3×3 邻域 clamp 去鬼影 +
  mix(cur,hist,0.9) 时域累积;TAAColor 自引用历史
- RG 历史读补 post-barrier:采样后恢复镜像 resting layout(TAA 的历史
  是 ColorAttachment layout,不恢复则下帧 entry oldLayout 脱节)
- 管线:Forward(HDR+Velocity) → TAA(TAAColor) → Bloom/Tonemap 改源 TAAColor
- 编译零错误、validation 零警告、8 秒冒烟通过(10 pass 全活跃)
2026-09-23 13:29:31 +08:00
atdunbg a29aed2803 feat: Pause Animation 开关(帧历史 diff 冻结验证用) 2026-09-23 13:11:29 +08:00
atdunbg c75832e73e feat: RenderGraph 帧历史资源(ReadHistory)+ TemporalDiff 验证 pass
- PassBuilder::ReadHistory:读上一帧槽位 ((slot-1+F) mod F) 的物理镜像,
  TAA/运动模糊等时域效果的前置能力,RG 最后一个已知边界补齐
- 历史读不建拓扑边(跨帧依赖由同队列提交序保证)、不入本帧状态链、
  不改生命周期;剔除照常参与(生产者存活判定)
- 历史 barrier:首帧走 first(old=Undefined),之后走 steady 且
  src=ALL_COMMANDS|MEMORY_WRITE 显式闭合跨帧可用性+可见性;
  有历史读的资源,常规 entry steady 的 src 同样升级(历史读打破了
  同槽位 fence 全等保前提)
- TemporalDiff 调试 pass:|上一帧−当前帧|×增益,静止=黑/运动边缘发光
  即链路正确;Tonemap binding2 经 push 开关切调试视图
- 编译零错误、validation 零警告、8 秒冒烟通过(9 pass 全活跃)
2026-09-23 13:05:10 +08:00
atdunbg 0de8498350 feat: 显存分配器迁移到 VMA(P1 路线第一项)
- vendored VulkanMemoryAllocator 子模块,唯一实现编译单元 VmaImpl.cpp
- MemoryAllocator 重写为 VMA 后端:接口从"Allocate(reqs)+手绑"升级为
  "创建即分配即绑定"(CreateBuffer/CreateImage/Destroy/Flush),
  VMA 要求资源与内存成对释放,约束成单一出口
- Buffer/Texture/RenderGraph 三处调用方迁移;staging 持久映射 + 按分配
  精确 flush(原来按整块 flush)
- 获得:TLSF 通用分配、DedicatedAllocation 自动启用、memory budget 感知、
  内存别名基础(TAA/复用前置)
- 编译零错误、validation 零警告、8 秒冒烟通过
2026-09-23 12:51:11 +08:00
atdunbg 6f10fd624d chore: gitignore 屏蔽本地 Docs 目录 2026-09-23 12:43:42 +08:00
atdunbg 718a658f5d refactor: 解耦重构——还清四处架构债(对齐 UE/Unity 分层)
- SceneRenderer:场景→渲染数据翻译层(UE FSceneRenderer / Unity CullingResults
  模式),收口相机/灯光/光空间矩阵/双视图剔除,Renderer 只编排
- pass 对象化(Unity ScriptableRenderPass 模式):8 个 pass lambda 拆为
  Passes::{Shadow,Forward,Bloom,Tonemap,ImGui,Present},BuildGraph 超级函数消失;
  SceneGpu/FullscreenGpu 承接共享装配
- 中性 AssetRef(Core 层):ECS 组件不再依赖 Assets 模块(Unity InstanceID 思路)
- IAssetUploader 接口:AssetManager 不感知 Vulkan 具体类,上传服务由渲染层
  VulkanAssetUploader 提供
- Renderer 公共 API 不变(App 层零改动),编译零错误 validation 零警告
2026-09-23 12:02:49 +08:00
atdunbg 91880d88fe fix: tonemap 输出预乘 alpha 匹配 PRE_MULTIPLIED 合成模式 + 调整地面位置 2026-09-23 11:35:05 +08:00
atdunbg fa9082706b feat: 双视图视锥剔除(相机/光源各自剔除)
- Frustum(Gribb-Hartmann):从 view-proj 行组合提取 6 平面,
  包围球相交测试;按 Vulkan [0,1] 深度约定取 near=row2
- Mesh 构造时缓存局部空间包围球(顶点包围盒 → 中心+半径)
- 实例收集重写:单趟遍历,每个 (实体, mesh 部件) 对相机视锥与
  光源视锥分别做剔除——相机外实体仍可投影子进视野,Shadow 必须用
  光源视锥独立剔除,不能用相机结果
- 实例矩阵数组 = [主视图段 | 阴影视图段],batch.first 全局下标;
  两视图共享同一份 ObjectBuffer SSBO
- 聚合改哈希(unordered_map),单趟填充替代原两趟遍历
- 面板显示 instances main/shadow 计数(剔除效果实时可见)

实测:1 背包 80 main / 80 shadow(修复前 shadow=21——near 平面
按 [-1,1] 矩阵误用 [0,1] 公式,光锥近半段被误剔)
2026-09-23 11:21:28 +08:00
atdunbg df4871a49b feat: 透明窗口支持 + 退出确认流程 + ImGui 总开关
用户自主改进批次:
- 透明窗口:GLFW_TRANSPARENT_FRAMEBUFFER + 窗口 opacity 0.5,
  swapchain composite alpha 按能力自适应(Opaque/Pre/PostMultiplied),
  Forward 清屏 alpha=0,tonemap 透传 HDR alpha——物体实体、背景透明
- 退出确认:Window 挂 WindowCloseCallback 发事件,App 侧 OnWindowClose
  置确认标记,OnRenderOverlay 弹 ImGui 确认框后 Close();
  主循环退出条件改为 m_Running(close 事件驱动)
- ImGui 总开关:` 键切换 ImGuiLayer::SetEnabled,禁用时跳过
  NewFrame/OnRenderOverlay/BuildDrawData 整段
- 帧率控制简化:去掉 LimitFps 开关,非 vsync 时恒按 target 限帧
  (默认 60,可调 ≥24);m_VSync 默认 false(Mailbox 不限速)
- 清理:Model.cpp const 修正,swapchain 日志带 present mode,
  GPU 遥测暂时注释

验证:0 error 0 warning,composite alpha 实测选 PreMultiplied(0x2),
present=Mailbox
2026-09-23 10:57:48 +08:00
atdunbg d8274e8bdc feat: 帧率控制——手动限帧 + VSync 开关
- Application 帧率控制 API:SetLimitFps/SetTargetFps/SetVSync。
  限帧目标最低 24fps(防止过低刷新导致交互卡死),无上限;
  vsync 开启时 present 自带节奏,手动限帧不生效
- 限帧策略:sleep 到剩 ~2ms 后自旋,精度高且不烧 CPU
- Swapchain 加 VSync 开关:ChoosePresentMode 按开关选
  Mailbox(低延迟垂直同步)/ Immediate(不限速可撕裂),
  FIFO 兜底;切换经 SetVSync → 重建 swapchain(复用 HandleResize)
- Scene Debug 面板新增 Frame 区块(vsync/limit/目标滑条)
- Run 循环新增 CPU 归属遥测(update/ui/render/total,每 120 帧)
2026-09-22 20:33:10 +08:00
atdunbg cef32ea1ba fix: Shadow pass 接入 GPUDriven batch,实例上限提至 16384
100 背包掉帧 + 警告刷屏的根因与修复:
1. backpack.obj 有 44 个 mesh 部件,100 实体 × 44 = 4400 实例
   打爆 4096 上限 → 每帧截断 + 告警刷屏。上限提至 16384
  (1MB/slot),告警改一次性
2. Shadow pass 仍逐实体逐部件画(4400 draw,CPU 提交瓶颈)。
   改为与 Forward 共用同一批 batch:Set0 绑 ObjectBuffer,
   lightVP 一次 push,44 draw 完成。shadow.vert 经
   gl_InstanceIndex 从 SSBO 取模型矩阵
3. 遥测补全:Application::Run 每 120 帧输出 CPU 归属
   (update/ui/render/total),Render 输出实例收集耗时——
   实测 100 背包 CPU 2.0ms/帧 GPU 0.6ms/帧 ≈ 500fps
2026-09-22 20:17:05 +08:00
atdunbg 6a3fcac072 fix: Shadow pass 接入 GPUDriven batch,实例上限提至 16384
100 背包掉帧 + 警告刷屏的根因:
1. backpack.obj 有 44 个 mesh 部件,100 实体 × 44 = 4400 实例
   打爆 4096 上限 → 每帧截断 + 告警刷屏。上限提至 16384
  (1MB/slot),告警改一次性
2. Shadow pass 仍逐实体逐部件画(4400 draw,CPU 提交瓶颈)。
   改为与 Forward 共用同一批 batch:Set0 绑 ObjectBuffer,
   lightVP 一次 push,44 draw 完成。shadow.vert 经
   gl_InstanceIndex 从 SSBO 取模型矩阵

实测(100 背包):GPU 总耗时 ~5.7ms(Shadow 2.9 + Forward 2.6 +
后处理 0.1),0 警告,帧率受 mailbox 垂直同步 pacing 限制
2026-09-22 20:06:43 +08:00
atdunbg 7fca721a24 feat: GPUDriven 实例化渲染——draw 数与实体数解耦
- UniformBuffer ring 支持 eStorageBuffer usage(GPUDriven 实例矩阵
  per-frame-slot,正确性保证与 UBO 一致:fence 等待后写入)
- DescriptorManager 加 UpdateStorageBuffer;池补 STORAGE_BUFFER 类型
- Set0 新增 binding1 = 实例矩阵 SSBO(vertex 读取,std430)
- Forward pass 重写:每帧从 ECS 收集实例矩阵(按 mesh 聚合 batch,
  两趟遍历保证 batch 区间连续),同 mesh 的 N 个实体合并为
  一次 drawIndexed(instanceCount=N),firstInstance 指向 batch 起点;
  vertex shader 经 gl_InstanceIndex 取模型矩阵,forward 不再用 push
- draw 数 = 唯一 mesh 数,与实体数量解耦(4096 实例上限,
  超出截断告警;聚合改哈希为后续 TODO)

验证:8 pass 全活,validation 0 error 0 warning,GPU Timings 面板
实测各 pass 毫秒,场景渲染与阴影正确
2026-09-22 19:46:17 +08:00
79 changed files with 4063 additions and 1544 deletions
+3 -1
View File
@@ -5,4 +5,6 @@ out/
build/ build/
# 目前只上传代码,暂不上传assets # 目前只上传代码,暂不上传assets
assets/ assets/
# 本地文档暂不上传
Docs/
+6
View File
@@ -17,3 +17,9 @@
path = Lib/imgui path = Lib/imgui
url = https://github.com/ocornut/imgui.git url = https://github.com/ocornut/imgui.git
branch = docking branch = docking
[submodule "Lib/VulkanMemoryAllocator"]
path = Lib/VulkanMemoryAllocator
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "Lib/tracy"]
path = Lib/tracy
url = https://github.com/wolfpld/tracy.git
+146 -52
View File
@@ -24,7 +24,6 @@
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include "Engine/ImGui/ImGuiLayer.h"
using namespace Engine; using namespace Engine;
using namespace Engine::ECS; using namespace Engine::ECS;
@@ -48,64 +47,73 @@ protected:
EnableImGui(); EnableImGui();
// 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄) // 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄)
auto backpack = GetAssets().LoadModel("assets/models/backpack/backpack.obj"); // SunTemple:FBX + DDS 压缩贴图(DdsLoader 验证场景)
const auto model = GetAssets().LoadModel("assets/models/viking_room/viking_room.obj");
const auto temple = GetAssets().LoadModel("assets/models/SunTemple/SunTemple.fbx");
m_Spinners.push_back(scene.CreateEntity("backpack"));
m_Spinners[0].AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
// 地面:程序化纯色平面(半边长 15,中心 y=-2) // 地面:程序化纯色平面(半边长 15,中心 y=-2)
auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145); const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground"); m_Ground = scene.CreateEntity("Ground");
m_Ground.AddComponent<TransformComponent>() m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)) .SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
.Compose();
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground}); m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
// 相机实体(primary = 当前渲染视角) // 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
m_Camera = scene.CreateEntity("MainCamera"); m_Camera = scene.CreateEntity("MainCamera");
auto& camera = m_Camera.AddComponent<CameraComponent>(); auto& camera = m_Camera.AddComponent<CameraComponent>();
camera.primary = true; camera.primary = true;
camera.camera.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f)); m_Camera.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
// 背包实体 × 3(悬浮在地面上方) // 神庙实体 × 1( FBX 单位较大,缩放到场景尺度)
for (int i = 0; i < 3; ++i) { {
auto e = scene.CreateEntity("Backpack" + std::to_string(i)); auto e = scene.CreateEntity("SunTemple");
e.GetComponent<TransformComponent>()
e.AddComponent<TransformComponent>() .SetScale(glm::vec3(0.05f));
.SetPosition(glm::vec3((i - 1) * 1.5f, 0.0f, 0.0f)) e.AddComponent<MeshRendererComponent>(MeshRendererComponent{temple});
.SetScale(glm::vec3(0.5f))
.Compose();
e.AddComponent<MeshRendererComponent>(MeshRendererComponent{backpack});
m_Spinners.push_back(e);
} }
// 灯光实体(第一个灯 = 阴影光源) // 灯光实体(第一个灯 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight"); m_KeyLight = scene.CreateEntity("KeyLight");
m_KeyLight.AddComponent<LightComponent>( m_KeyLight.AddComponent<LightComponent>(
LightComponent{glm::vec3(-0.5f, -0.6f, -0.35f), LightComponent{
glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)}); glm::vec3(-0.5f, -0.6f, -0.35f),
glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)
});
scene.CreateEntity("FillLight") scene.CreateEntity("FillLight")
.AddComponent<LightComponent>( .AddComponent<LightComponent>(
LightComponent{glm::vec3(0.5f, -0.5f, 0.3f), LightComponent{
glm::vec4(0.3f, 0.5f, 1.0f, 0.8f)}); glm::vec3(0.5f, -0.5f, 0.3f),
glm::vec4(0.3f, 0.5f, 1.0f, 0.8f)
});
} }
void OnUpdate(f32 deltaTime) override void OnUpdate(f32 deltaTime) override
{ {
m_Time += deltaTime; // 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
if (!m_PauseAnim)
{
m_Time += deltaTime;
}
// 相机(WASD/Space/Shift 移动) // 相机(WASD/Space/Shift 移动;位姿读写实体 Transform)
m_Camera.GetComponent<CameraComponent>() m_Camera.GetComponent<CameraComponent>()
.camera.Update(GetWindow(), deltaTime); .camera.Update(GetWindow(),
m_Camera.GetComponent<TransformComponent>(),
deltaTime);
// 旋转动画:编辑 Transform 分量后 Compose 重算 world // 旋转动画:编辑 Transform 分量后 Compose 重算 world
for (size_t i = 0; i < m_Spinners.size(); ++i) { for (size_t i = 0; i < m_Spinners.size(); ++i)
{
auto& t = m_Spinners[i].GetComponent<TransformComponent>(); auto& t = m_Spinners[i].GetComponent<TransformComponent>();
t.position = glm::vec3((static_cast<f32>(i) - 1.0f) * 1.5f, 0.0f, 0.0f); t.SetRotation(glm::vec3(0.0f, m_Time * 60.0f + static_cast<f32>(i) * 40.0f, 0.0f));
t.rotation.y = m_Time * 60.0f + static_cast<f32>(i) * 40.0f; t.SetScale(glm::vec3(0.5f));
t.scale = glm::vec3(0.5f);
t.Compose();
} }
} }
@@ -115,18 +123,18 @@ protected:
void OnRenderOverlay() override void OnRenderOverlay() override
{ {
if (!m_OverlayVisible) return;
ImGui::ShowDemoWindow(); ImGui::ShowDemoWindow();
ImGui::Begin("Scene Debug"); ImGui::Begin("Scene Debug");
// 阴影光源方向:拖动滑条实时看影子变化 // 阴影光源方向:拖动滑条实时看影子变化
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>()) { if (auto* light = m_KeyLight.TryGetComponent<LightComponent>())
{
ImGui::Text("Key Light (shadow caster)"); ImGui::Text("Key Light (shadow caster)");
ImGui::SliderFloat3("direction", ImGui::SliderFloat3("direction",
&light->direction.x, -1.0f, 1.0f); &light->direction.x, -1.0f, 1.0f);
if (ImGui::Button("reset")) { if (ImGui::Button("reset"))
{
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f); light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
} }
} }
@@ -136,76 +144,162 @@ protected:
ImGui::Separator(); ImGui::Separator();
ImGui::Text("Tonemap"); ImGui::Text("Tonemap");
f32 exposure = renderer.GetExposure(); f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f)) { if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure); renderer.SetExposure(exposure);
} }
bool aces = renderer.GetUseACES(); bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces)) { if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces); renderer.SetUseACES(aces);
} }
ImGui::Text("Bloom"); ImGui::Text("Bloom");
f32 threshold = renderer.GetBloomThreshold(); f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f)) { if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
{
renderer.SetBloomThreshold(threshold); renderer.SetBloomThreshold(threshold);
} }
f32 strength = renderer.GetBloomStrength(); f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f)) { if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength); renderer.SetBloomStrength(strength);
} }
// 调试视图:帧历史差异(静止=黑,运动边缘发光=链路正确)
bool temporalDiff = renderer.GetShowTemporalDiff();
if (ImGui::Checkbox("Temporal Diff View", &temporalDiff))
{
renderer.SetShowTemporalDiff(temporalDiff);
}
ImGui::Checkbox("Pause Animation", &m_PauseAnim);
// Pass 级 GPU 耗时(RG timestamp) // Pass 级 GPU 耗时(RG timestamp)
ImGui::Separator(); ImGui::Separator();
ImGui::Text("GPU Timings"); ImGui::Text("GPU Timings");
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
const auto& names = renderer.GetPassNames(); const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs(); const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i) { for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]); ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
} }
// 帧率控制
auto& app = *this;
ImGui::Separator();
ImGui::Text("Frame");
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Separator(); ImGui::Separator();
ImGui::Text("fps: %.1f frame: %llu", ImGui::Text("fps: %.1f frame: %llu",
1.0f / GetDeltaTime(), 1.0f / GetDeltaTime(),
static_cast<unsigned long long>(GetFrameCount())); static_cast<unsigned long long>(GetFrameCount()));
ImGui::End(); ImGui::End();
// 自定义重载事件: 重载关闭事件模态框
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
Close();
}
}
}
void OnWindowClose(const Event& event) override
{
m_ShowQuitConfirm = true;
} }
void OnKeyPress(const Event& event) override void OnKeyPress(const Event& event) override
{ {
if (event.key == GLFW_KEY_GRAVE_ACCENT) { if (event.key == GLFW_KEY_GRAVE_ACCENT)
m_OverlayVisible = !m_OverlayVisible; // ` 反折叠加层 {
SetImGuiEnabled(!GetImGuiEnabled());
} }
if (event.key == GLFW_KEY_F1) { if (event.key == GLFW_KEY_F1)
{
STATS.PrintComparison(); STATS.PrintComparison();
} }
if (event.key == GLFW_KEY_ESCAPE) {
Close();
}
} }
void OnWindowResize(const Event& event) override void OnWindowResize(const Event& event) override
{ {
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height); ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
} }
private: private:
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空 bool m_ShowQuitConfirm = false;
bool m_QuitConfirmed = false;
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
EntityHandle m_Ground{}; EntityHandle m_Ground{};
EntityHandle m_KeyLight{}; EntityHandle m_KeyLight{};
std::vector<EntityHandle> m_Spinners; std::vector<EntityHandle> m_Spinners;
bool m_OverlayVisible = true;
f32 m_Time = 0.0f; f32 m_Time = 0.0f;
bool m_PauseAnim = false; // 冻结动画时钟(帧历史 diff 验证用)
}; };
int main() int main()
{ {
SandboxApp app; SandboxApp app;
try { try
{
app.Run(); app.Run();
} }
catch (const std::exception& e) { catch (const std::exception& e)
{
ENGINE_FATAL("Exception: {}", e.what()); ENGINE_FATAL("Exception: {}", e.what());
return -1; return -1;
} }
+10
View File
@@ -21,6 +21,7 @@ add_library(Engine STATIC ${ENGINE_SOURCES} ${IMGUI_SOURCES})
target_include_directories(Engine PUBLIC target_include_directories(Engine PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/Lib/entt/src ${CMAKE_SOURCE_DIR}/Lib/entt/src
${CMAKE_SOURCE_DIR}/Lib/VulkanMemoryAllocator/include
${IMGUI_DIR} ${IMGUI_DIR}
${IMGUI_DIR}/backends ${IMGUI_DIR}/backends
) )
@@ -34,4 +35,13 @@ target_link_libraries(Engine PUBLIC
assimp::assimp assimp::assimp
) )
# Tracy(CPU/GPU 帧分析):宏关闭时全部退化空操作,零开销
set(TRACY_DIR ${CMAKE_SOURCE_DIR}/Lib/tracy)
target_include_directories(Engine PUBLIC ${TRACY_DIR}/public)
target_sources(Engine PRIVATE ${TRACY_DIR}/public/TracyClient.cpp)
target_compile_definitions(Engine PUBLIC TRACY_ENABLE)
find_package(Threads REQUIRED)
target_link_libraries(Engine PUBLIC Threads::Threads)
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1) target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
@@ -1,37 +0,0 @@
//
// Created by atdunbg on 2026/9/22.
//
// AssetHandle:资产句柄 = (池内索引, 生成号),与 Entity/RG 句柄同一哲学。
//
// 组件、场景数据只存句柄;所有权归 AssetManager。
// 悬空句柄(generation 不匹配)会被 Get 显式拒绝,杜绝 use-after-free。
//
#ifndef HELLOVULKAN_ASSETHANDLE_H
#define HELLOVULKAN_ASSETHANDLE_H
#include <Engine/Core/Types.h>
// Model 前向声明(句柄不依赖完整类型)
namespace Engine { class Model; }
namespace Engine::Assets {
inline constexpr u32 kInvalidAssetIndex = 0xFFFFFFFF;
template<typename T>
struct AssetHandle {
u32 index = kInvalidAssetIndex;
u32 generation = 0;
bool Valid() const { return index != kInvalidAssetIndex; }
explicit operator bool() const { return Valid(); }
bool operator==(const AssetHandle& o) const = default;
};
// 常用别名
using ModelHandle = AssetHandle<Engine::Model>;
} // namespace Engine::Assets
#endif //HELLOVULKAN_ASSETHANDLE_H
+19 -33
View File
@@ -3,14 +3,9 @@
// //
// AssetManager:引擎资产层。 // AssetManager:引擎资产层。
// //
// 职责划分(对照 Renderer): // 职责:路径 → 解析/加载 → 缓存去重 → 句柄分发 + 生命周期管理。
// - 资产层:路径 → 解析/加载 → 缓存去重 → 句柄分发 + 生命周期管理 // GPU 创建不在这里:经 IAssetUploader 接口(渲染层提供实现)完成,
// - 渲染层:拿已解析资产创建/绑定 GPU 资源 // 本类不感知 Vulkan——依赖方向 Assets → 接口,实现由组合根注入。
//
// Model 的构造仍需要 GPU 上下文(buffer/image upload),这是现阶段的
// 折中:资产层"使用"GPU 层的接口,但 Renderer 不再知道磁盘和缓存。
// 后续演进:把 Model 拆成 CPU 解析数据 + GPU 上传两段,实现真正的
// 异步加载(IO 线程解析 → 主线程/上传队列提交),本文件结构已为此留好位置。
// //
// 组件存句柄不存指针的理由: // 组件存句柄不存指针的理由:
// shared_ptr 会引入共享所有权 → 原子引用计数(swap-remove 拷贝时) // shared_ptr 会引入共享所有权 → 原子引用计数(swap-remove 拷贝时)
@@ -23,7 +18,8 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/Assets/AssetHandle.h> #include <Engine/Core/AssetRef.h>
#include <Engine/Assets/IAssetUploader.h>
#include <Engine/Renderer/Model.h> #include <Engine/Renderer/Model.h>
#include <functional> #include <functional>
@@ -32,11 +28,6 @@
#include <vector> #include <vector>
namespace Engine { namespace Engine {
class Device;
class MemoryAllocator;
class DescriptorManager;
namespace Assets { namespace Assets {
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ---------- // ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
@@ -48,7 +39,7 @@ namespace Engine {
using LoaderFn = std::function<Scope<T>(const std::string&)>; using LoaderFn = std::function<Scope<T>(const std::string&)>;
// 加载(带路径去重)。失败返回无效句柄。 // 加载(带路径去重)。失败返回无效句柄。
AssetHandle<T> Load(const std::string& path, const LoaderFn& loader) AssetRef<T> Load(const std::string& path, const LoaderFn& loader)
{ {
if (auto it = m_ByPath.find(path); it != m_ByPath.end()) { if (auto it = m_ByPath.find(path); it != m_ByPath.end()) {
return it->second; return it->second;
@@ -60,7 +51,7 @@ namespace Engine {
return {}; return {};
} }
AssetHandle<T> h; AssetRef<T> h;
if (!m_Free.empty()) { if (!m_Free.empty()) {
h.index = m_Free.back(); h.index = m_Free.back();
m_Free.pop_back(); m_Free.pop_back();
@@ -77,7 +68,7 @@ namespace Engine {
} }
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。 // 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
T* Get(AssetHandle<T> h) const T* Get(AssetRef<T> h) const
{ {
if (!h.Valid() || h.index >= m_Entries.size()) { if (!h.Valid() || h.index >= m_Entries.size()) {
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})", ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
@@ -94,7 +85,7 @@ namespace Engine {
// 卸载:句柄即刻失效(generation 递增),路径缓存清除。 // 卸载:句柄即刻失效(generation 递增),路径缓存清除。
// 注意:GPU 侧何时安全释放由上层(Renderer fence)保证—— // 注意:GPU 侧何时安全释放由上层(Renderer fence)保证——
// 本类只管所有权,不管 GPU 生命周期。 // 本类只管所有权,不管 GPU 生命周期。
void Unload(AssetHandle<T> h) void Unload(AssetRef<T> h)
{ {
if (!h.Valid() || h.index >= m_Entries.size()) return; if (!h.Valid() || h.index >= m_Entries.size()) return;
@@ -109,7 +100,7 @@ namespace Engine {
m_Free.push_back(h.index); m_Free.push_back(h.index);
} }
const std::string& Path(AssetHandle<T> h) const const std::string& Path(AssetRef<T> h) const
{ {
static const std::string kEmpty; static const std::string kEmpty;
if (!h.Valid() || h.index >= m_Entries.size()) return kEmpty; if (!h.Valid() || h.index >= m_Entries.size()) return kEmpty;
@@ -132,29 +123,27 @@ namespace Engine {
std::vector<Entry> m_Entries; std::vector<Entry> m_Entries;
std::vector<u32> m_Free; std::vector<u32> m_Free;
std::unordered_map<std::string, AssetHandle<T>> m_ByPath; std::unordered_map<std::string, AssetRef<T>> m_ByPath;
}; };
// ---------- AssetManager:全引擎资产入口 ---------- // ---------- AssetManager:全引擎资产入口 ----------
// (ModelHandle 别名来自 AssetHandle.h)
class AssetManager { class AssetManager {
public: public:
// GPU 上下文由组合根(Application)在 Renderer 装配完成后注入 // 上传服务由组合根(Application)注入:渲染层实现接口,
AssetManager(Device& device, MemoryAllocator& allocator, // 资产层不感知 Vulkan 具体类
DescriptorManager& descriptors, explicit AssetManager(IAssetUploader& uploader);
vk::DescriptorSetLayout textureLayout);
~AssetManager() = default; ~AssetManager() = default;
AssetManager(const AssetManager&) = delete; AssetManager(const AssetManager&) = delete;
AssetManager& operator=(const AssetManager&) = delete; AssetManager& operator=(const AssetManager&) = delete;
ModelHandle LoadModel(const std::string& path); ModelRef LoadModel(const std::string& path);
Model* GetModel(ModelHandle h) const { return m_Models.Get(h); } Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
void UnloadModel(ModelHandle h) { m_Models.Unload(h); } void UnloadModel(ModelRef h) { m_Models.Unload(h); }
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。 // 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
ModelHandle CreatePlane(f32 halfSize, u8 r, u8 g, u8 b); ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
AssetCache<Model>& Models() { return m_Models; } AssetCache<Model>& Models() { return m_Models; }
@@ -162,10 +151,7 @@ namespace Engine {
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。 // 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
private: private:
Device& m_Device; IAssetUploader& m_Uploader;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
vk::DescriptorSetLayout m_TextureLayout;
AssetCache<Model> m_Models; AssetCache<Model> m_Models;
}; };
+55
View File
@@ -0,0 +1,55 @@
//
// Created by atdunbg on 2026/9/23.
//
// DDS 文件解析(最小实现,不解码压缩块)。
//
// 设计:GPU 原生支持 BC 压缩格式——DDS 的压缩数据**直接上传**为
// BC1/BC2/BC3/BC4/BC5/BC6H/BC7 纹理由硬件采样解码,不做 CPU 解码。
// 输出中性 DdsFormat(本层禁 Vulkan 头),vk::Format 映射与 sRGB
// 语义由调用方(Texture,渲染侧)决定。
//
// 支持:
// - legacy fourcc:DXT1/DXT3/DXT5/ATI1/BC4U/ATI2/BC5U
// - DX10 扩展头:BC1..BC7、R8G8B8A8、B8G8R8A8
// - 未压缩 32bpp(DDPF_RGB | DDPF_ALPHAPIXELS)
// - 2D + mip 链(cubemap 面标志暂不支持,报错)
//
// 输出:Mips[r] = 第 r 级完整数据(BC 按块行紧凑排布:
// bytes = ceil(w/4)*ceil(h/4)*blockBytes)。
//
#ifndef HELLOVULKAN_DDSLOADER_H
#define HELLOVULKAN_DDSLOADER_H
#include <Engine/Core/Types.h>
#include <string>
#include <vector>
namespace Engine::Assets::Dds {
// 中性格式(Assets 层不感知 Vulkan;映射在渲染侧 Texture)
enum class DdsFormat {
BC1, BC2, BC3, BC4, BC5, BC6H, BC7,
RGBA8,
BGRA8, // 按字节序上传后视作 RGBA(近似,触发警告)
};
struct DdsData {
u32 Width = 0;
u32 Height = 0;
u32 MipLevels = 1;
DdsFormat Format = DdsFormat::RGBA8;
std::vector<std::vector<u8>> Mips; // 每级完整数据(紧凑排布)
bool BlockCompressed = false; // BC 格式(rowPitch 按块计算)
};
// 解析 DDS 文件;失败返回 false 并写 errMsg
bool Load(const std::string& path, DdsData& out, std::string& errMsg);
// 指定 mip 级的单行字节跨度(BC = 块行;RGBA8 = w*4)
u32 RowPitch(const DdsData& data, u32 mipLevel);
} // namespace Engine::Assets::Dds
#endif //HELLOVULKAN_DDSLOADER_H
@@ -0,0 +1,38 @@
//
// Created by atdunbg on 2026/9/23.
//
// IAssetUploader:资产层 → GPU 的上传接口。
//
// 资产层(AssetManager/AssetCache)只认这个接口,不感知 Vulkan 具体类
// (Device/Allocator/DescriptorManager)。实现由渲染层提供
// (VulkanAssetUploader),组合根(Renderer)装配后注入。
//
// 对齐 UE:资产系统(AssetManager/StreamableManager)与渲染资源创建
// (rendering thread / RHI)分离,资产层只拿到"可用的渲染资源"。
//
// 后续演进点:接口加返回 future / 回调即为异步加载
// (IO 线程解析 → 上传队列提交),本接口已是正确的接缝。
//
#ifndef HELLOVULKAN_IASSETUPLOADER_H
#define HELLOVULKAN_IASSETUPLOADER_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
namespace Engine::Assets {
class IAssetUploader {
public:
virtual ~IAssetUploader() = default;
// 磁盘模型 → 解析 + GPU 上传(失败返回 nullptr)
virtual Scope<Model> LoadModel(const std::string& path) = 0;
// 程序化纯色平面(size 为半边长)
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
};
} // namespace Engine::Assets
#endif //HELLOVULKAN_IASSETUPLOADER_H
+19
View File
@@ -26,6 +26,8 @@
#include <chrono> #include <chrono>
#include <string> #include <string>
#include "Engine/ImGui/ImGuiLayer.h"
namespace Engine { namespace Engine {
class Window; class Window;
@@ -54,6 +56,14 @@ namespace Engine {
// 主渲染(ImGui pass 把 draw data 叠加到场景上)。 // 主渲染(ImGui pass 把 draw data 叠加到场景上)。
void EnableImGui(); void EnableImGui();
// ---- 帧率控制 ----
// vsync 开启时 present 自带节奏,手动限帧不生效;
// 限帧目标最低 24fps(防止过低刷新导致交互卡死),无上限
void SetVSync(bool vsync);
bool GetVSync() const { return m_VSync; }
void SetTargetFps(f32 fps) { m_TargetFps = std::max(24.0f, fps); }
f32 GetTargetFps() const { return m_TargetFps; }
// ---- 供派生类访问的模块 ---- // ---- 供派生类访问的模块 ----
Window& GetWindow() const { return *m_Window; } Window& GetWindow() const { return *m_Window; }
Renderer& GetRenderer() const { return *m_Renderer; } Renderer& GetRenderer() const { return *m_Renderer; }
@@ -62,6 +72,11 @@ namespace Engine {
f32 GetDeltaTime() const { return m_DeltaTime; } f32 GetDeltaTime() const { return m_DeltaTime; }
u64 GetFrameCount() const { return m_FrameCount; } u64 GetFrameCount() const { return m_FrameCount; }
// ImGui相关
void SetImGuiEnabled(const bool enable) { if (m_ImGuiLayer) m_ImGuiLayer->SetEnabled(enable); }
// 理论上ImGuiLayer不应该会空,暂时静默处理
bool GetImGuiEnabled() const { return m_ImGuiLayer ? m_ImGuiLayer->IsEnabled() : false; }
protected: protected:
// ---- 生命周期钩子 ---- // ---- 生命周期钩子 ----
@@ -108,6 +123,10 @@ namespace Engine {
f32 m_DeltaTime = 0.0f; f32 m_DeltaTime = 0.0f;
u64 m_FrameCount = 0; u64 m_FrameCount = 0;
// 帧率控制
bool m_VSync = true; // 垂直同步(present 自带节奏)
f32 m_TargetFps = 60.0f; // 限帧目标(≥24)
std::chrono::steady_clock::time_point m_LastFrameTime; std::chrono::steady_clock::time_point m_LastFrameTime;
}; };
+45
View File
@@ -0,0 +1,45 @@
//
// Created by atdunbg on 2026/9/23.
//
// AssetRef:中性资产引用 = (池内索引, 生成号),与 Entity/RG 句柄同一哲学。
//
// 放在 Core 层(不依赖 Assets/Vulkan):ECS 组件、序列化数据只存这个
// 纯值类型。对齐 UE/Unity 的做法——组件持有"资产引用",但不感知资产
// 系统内部:Unity 的 InstanceID、UE 的 FSoftObjectPath 本质都是中性引用
// 值,解析归资产系统(AssetManager)。
// 悬空引用(generation 不匹配)由 AssetCache::Get 显式拒绝。
//
#ifndef HELLOVULKAN_ASSETREF_H
#define HELLOVULKAN_ASSETREF_H
#include <Engine/Core/Types.h>
// 资产类型前向声明(必须放全局层:namespace 内前向声明会创建
// Engine::Assets::Engine::Model 这类错误嵌套命名空间)
namespace Engine {
class Model;
class Texture;
class Material;
}
namespace Engine {
inline constexpr u32 kInvalidAssetIndex = 0xFFFFFFFF;
template<typename T>
struct AssetRef {
u32 index = kInvalidAssetIndex;
u32 generation = 0;
bool Valid() const { return index != kInvalidAssetIndex; }
explicit operator bool() const { return Valid(); }
bool operator==(const AssetRef& o) const = default;
};
// 常用别名(新增资产类型时在这里加一行)
using ModelRef = AssetRef<Model>;
} // namespace Engine
#endif //HELLOVULKAN_ASSETREF_H
+2
View File
@@ -87,6 +87,8 @@ namespace Engine {
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos); static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos);
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset); static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset);
static void WindowFocusCallback(GLFWwindow* window, int focused); static void WindowFocusCallback(GLFWwindow* window, int focused);
static void WindowCloseCallback(GLFWwindow* window);
}; };
} // namespace Engine } // namespace Engine
+36 -20
View File
@@ -10,8 +10,8 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Core/UUID.h> #include <Engine/Core/UUID.h>
#include <Engine/Core/AssetRef.h>
#include <Engine/Renderer/Camera.h> #include <Engine/Renderer/Camera.h>
#include <Engine/Assets/AssetHandle.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
@@ -34,37 +34,53 @@ namespace Engine::ECS {
std::string name; std::string name;
}; };
// 世界变换:可编辑的 SRT 分量 + 派生的世界矩阵。 // 世界变换:UE USceneComponent 的 encapsulation 模式——
// 编辑分量后调用 Compose() 重算 world;渲染热路径只读 world。 // SRT 分量私有,所有写入口标脏;world 矩阵由帧循环的
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。
// 层级(父子变换)留给后续的层级同步系统。 // 层级(父子变换)留给后续的层级同步系统。
struct TransformComponent { struct TransformComponent {
glm::vec3 position{0.0f}; // ---- 读取 ----
glm::vec3 rotation{0.0f}; // 欧拉角(度),应用顺序 Y→X→Z const glm::vec3& GetPosition() const { return m_Position; }
glm::vec3 scale{1.0f}; const glm::vec3& GetRotation() const { return m_Rotation; } // 欧拉角(度)
glm::mat4 world{1.0f}; const glm::vec3& GetScale() const { return m_Scale; }
const glm::mat4& World() const { return m_World; } // 要求已由 UpdateTransforms 计算
TransformComponent& SetPosition(const glm::vec3& p) { position = p; return *this; } // ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
TransformComponent& SetRotation(const glm::vec3& r) { rotation = r; return *this; } TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
TransformComponent& SetScale(const glm::vec3& s) { scale = s; return *this; } TransformComponent& SetRotation(const glm::vec3& r) { if (m_Rotation != r) { m_Rotation = r; m_Dirty = true; } return *this; }
TransformComponent& SetScale(const glm::vec3& s) { if (m_Scale != s) { m_Scale = s; m_Dirty = true; } return *this; }
// 分量 → world 矩阵(T * R * S) bool IsDirty() const { return m_Dirty; }
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
// 由 Scene::UpdateTransforms 调用(或层级系统),勿手动
TransformComponent& Compose() TransformComponent& Compose()
{ {
glm::mat4 t = glm::translate(glm::mat4(1.0f), position); const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position);
glm::mat4 r = const glm::mat4 r =
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(0, 1, 0)) * glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.y), glm::vec3(0, 1, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(1, 0, 0)) * glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.x), glm::vec3(1, 0, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.z), glm::vec3(0, 0, 1)); glm::rotate(glm::mat4(1.0f), glm::radians(m_Rotation.z), glm::vec3(0, 0, 1));
glm::mat4 s = glm::scale(glm::mat4(1.0f), scale); const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale);
world = t * r * s; m_World = t * r * s;
m_Dirty = false;
return *this; return *this;
} }
private:
glm::vec3 m_Position{0.0f};
glm::vec3 m_Rotation{0.0f};
glm::vec3 m_Scale{1.0f};
glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
}; };
// 网格渲染体:引用共享的 Model 资产。 // 网格渲染体:引用共享的 Model 资产。
// 存句柄不存指针/Ref:所有权归 AssetManager,悬空由句柄 generation 拦截。 // 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
struct MeshRendererComponent { struct MeshRendererComponent {
Assets::ModelHandle model; ModelRef model;
}; };
// 相机。一个场景中 primary=true 的实体是当前渲染视角。 // 相机。一个场景中 primary=true 的实体是当前渲染视角。
+9 -2
View File
@@ -51,6 +51,11 @@ namespace Engine::ECS {
EntityHandle CreateEntity(std::string name = {}); EntityHandle CreateEntity(std::string name = {});
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁 void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
void FlushDestroyQueue(); // 每帧末尾由 Application 调用 void FlushDestroyQueue(); // 每帧末尾由 Application 调用
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
// 用户代码永远不需要手动 Compose
void UpdateTransforms();
bool IsAlive(Entity e) const { return m_Registry.valid(e); } bool IsAlive(Entity e) const { return m_Registry.valid(e); }
// 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null // 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null
@@ -86,8 +91,10 @@ namespace Engine::ECS {
// ---- 常用查询 ---- // ---- 常用查询 ----
// primary 相机(渲染视角来源);无则返回 nullptr // 主相机(primary=true,渲染视角来源);无则返回 nullptr。
CameraComponent* GetPrimaryCamera(); // outTransform 可选:带出该实体的 TransformComponent
//(相机位姿的单一事实源)
CameraComponent* GetPrimaryCamera(TransformComponent** outTransform = nullptr);
private: private:
Registry m_Registry; Registry m_Registry;
@@ -16,7 +16,6 @@
// //
// 已知边界(后续演进点): // 已知边界(后续演进点):
// - 暂不支持异步 compute queue / 跨队列信号量 // - 暂不支持异步 compute queue / 跨队列信号量
// - 暂不支持内存别名(需 VMA)与帧历史资源(TAA 等)
// - 同一 pass 内对同一资源"读+写"(input attachment / RWM)未建模 // - 同一 pass 内对同一资源"读+写"(input attachment / RWM)未建模
// //
@@ -98,6 +97,10 @@ namespace Engine::RG {
vk::ImageView View(TextureHandle h) const; vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const; vk::Image Image(TextureHandle h) const;
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
u32 HistorySlot(u32 frameSlot) const;
class RenderGraph* graph = nullptr; class RenderGraph* graph = nullptr;
}; };
@@ -131,6 +134,10 @@ namespace Engine::RG {
}; };
std::vector<SlotState> slots; std::vector<SlotState> slots;
// 是否存在帧历史读(Compile 期校验:历史资源必须有同帧正常使用,
// 否则没有物理镜像可读)
bool hasHistoryUse = false;
// Compile 期状态链解算用(执行序上的"当前"状态) // Compile 期状态链解算用(执行序上的"当前"状态)
vk::ImageLayout chainLayout = vk::ImageLayout::eUndefined; vk::ImageLayout chainLayout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags2 chainStage{}; vk::PipelineStageFlags2 chainStage{};
@@ -150,6 +157,7 @@ namespace Engine::RG {
u32 resIndex; u32 resIndex;
ResourceUse use; ResourceUse use;
vk::ShaderStageFlags readStage; // ShaderRead 时的消费 stage vk::ShaderStageFlags readStage; // ShaderRead 时的消费 stage
bool history = false; // 帧历史读:读上一帧槽位的物理镜像
}; };
std::vector<Use> uses; std::vector<Use> uses;
ExecuteFn exec; ExecuteFn exec;
@@ -161,12 +169,19 @@ namespace Engine::RG {
// steady = 该槽位已被使用过(从 resting 状态出发) // steady = 该槽位已被使用过(从 resting 状态出发)
// first = 该槽位首次使用(从 Undefined 出发) // first = 该槽位首次使用(从 Undefined 出发)
// imported 资源恒用 first(acquire 后 layout 语义上为 Undefined) // imported 资源恒用 first(acquire 后 layout 语义上为 Undefined)
// history = 帧历史读(操作上一帧槽位的镜像,steady 变体带跨帧可见性同步)
struct BarrierPair { struct BarrierPair {
u32 resIndex; u32 resIndex;
vk::ImageMemoryBarrier2 steady; vk::ImageMemoryBarrier2 steady;
vk::ImageMemoryBarrier2 first; vk::ImageMemoryBarrier2 first;
bool history = false;
}; };
std::vector<BarrierPair> barriers; std::vector<BarrierPair> barriers;
// 历史读的出口 barrier(exec 之后执行):历史镜像被临时转进读
// layout 采样,读完必须恢复 resting layout,否则下一帧该槽位
// entry barrier 的 oldLayout 与实际 layout 脱节
std::vector<BarrierPair> postBarriers;
}; };
// ---------- RenderGraph ---------- // ---------- RenderGraph ----------
@@ -195,6 +210,11 @@ namespace Engine::RG {
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。 // 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const; vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
// 帧历史槽位 = 上一帧使用的槽位:(slot - 1 + F) mod F
u32 HistorySlot(u32 frameSlot) const {
return (frameSlot + m_FramesInFlight - 1) % m_FramesInFlight;
}
// ---- Pass 声明 ---- // ---- Pass 声明 ----
class PassBuilder { class PassBuilder {
@@ -202,6 +222,12 @@ namespace Engine::RG {
PassBuilder& Write(TextureHandle h, ResourceUse use); PassBuilder& Write(TextureHandle h, ResourceUse use);
PassBuilder& Read(TextureHandle h, ResourceUse use, PassBuilder& Read(TextureHandle h, ResourceUse use,
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment); vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
// 帧历史读:读上一帧槽位的物理镜像(TAA/运动模糊/时域滤波用)。
// - 不产生图内边:跨帧依赖由同队列提交序 + 历史 barrier 保证
// - 资源必须有同帧正常使用(否则无物理镜像),Compile 期校验
// - 采样描述符在 pass 内用 GetPhysicalView(h, HistorySlot(slot)) 预绑
PassBuilder& ReadHistory(TextureHandle h, ResourceUse use,
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
PassBuilder& SideEffect(); PassBuilder& SideEffect();
private: private:
+11 -27
View File
@@ -2,8 +2,11 @@
// Created by atdunbg on 2026/9/22. // Created by atdunbg on 2026/9/22.
// //
// Camera:独立的相机模块。 // Camera:独立的相机模块。
// 职责:输入驱动的运动更新 + 视图/投影矩阵计算。 //
// 不再依赖具体渲染器——Renderer/RenderGraph 只消费它的矩阵输出。 // 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
// TransformComponent 承载——Update 把输入直接写回 transform(末尾
// Compose),视图矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。
// 相机类自身不保存任何位姿状态,杜绝"位置两套来源"的脱节。
// //
#ifndef HELLOVULKAN_CAMERA_H #ifndef HELLOVULKAN_CAMERA_H
@@ -15,25 +18,20 @@
namespace Engine { namespace Engine {
class Window; class Window;
namespace ECS { struct TransformComponent; }
class Camera { class Camera {
public: public:
// 每帧调用:读取窗口输入,更新位置与朝向 // 输入驱动:WASD/Space/Shift 移动 + 鼠标视角(需 SetMouseLookEnabled)。
void Update(Window& window, f32 deltaTime); // 全部写回传入的 transform,有变更才 Compose。
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime);
glm::mat4 GetView() const; // 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度 // Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度
glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const; glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const;
glm::vec3 GetPosition() const { return m_Position; }
void SetPosition(const glm::vec3& pos) { m_Position = pos; }
void SetYaw(f32 yaw) { m_Yaw = yaw; }
void SetPitch(f32 pitch) { m_Pitch = pitch; }
f32 GetYaw() const { return m_Yaw; }
f32 GetPitch() const { return m_Pitch; }
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; } void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; } void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
@@ -41,24 +39,10 @@ namespace Engine {
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; } void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; } bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
glm::vec3 GetForward() const { return m_Forward; }
glm::vec3 GetRight() const { return m_Right; }
glm::vec3 GetUp() const { return m_Up; }
private: private:
void UpdateBasis();
glm::vec3 m_Position{0.0f, 0.0f, 7.0f};
f32 m_Yaw = -90.0f; // 朝向 -Z
f32 m_Pitch = 0.0f;
f32 m_MoveSpeed = 2.5f; f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f; f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = false; bool m_MouseLookEnabled = false;
// 缓存的方向基向量,Update 时刷新,避免每帧三角函数重复计算
glm::vec3 m_Forward{0.0f, 0.0f, -1.0f};
glm::vec3 m_Right{1.0f, 0.0f, 0.0f};
glm::vec3 m_Up{0.0f, 1.0f, 0.0f};
}; };
} // namespace Engine } // namespace Engine
+75
View File
@@ -0,0 +1,75 @@
//
// Created by atdunbg on 2026/9/23.
//
// Frustum:Gribb-Hartmann 视锥平面提取 + 包围球相交测试。
//
// 平面从 view-proj 矩阵的行组合中直接抽出(无需分解参数),
// 用于按视图剔除(相机视图 / 光源视图各自独立一份)。
// 注意:near/far 平面按 Vulkan [0,1] 深度约定提取
//(z_clip ∈ [0,1]:near = row2,far = row3 - row2)——
// 因此必须传已做 [0,1] 重映射的矩阵(相机 proj / lightSpace 均是)。
// 平面在提取后归一化:包围球测试的 radius 是欧氏长度,
// 未归一化的平面距离会被透视因数放大,导致视锥边缘误剔。
//
#ifndef HELLOVULKAN_FRUSTUM_H
#define HELLOVULKAN_FRUSTUM_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <array>
namespace Engine {
struct Frustum {
// 依次:left, right, bottom, top, near, far
// xyz = 单位法线,w = 原点到平面的有符号距离
std::array<glm::vec4, 6> planes{};
// 从 view-proj 矩阵提取 6 平面(列主序 glm 矩阵的"行"组合)。
// 必须传 [0,1] 深度重映射后的矩阵。
static Frustum FromMatrix(const glm::mat4& m)
{
Frustum f;
// 列主序下的"行 i": (m[0][i], m[1][i], m[2][i], m[3][i])
const auto row = [&m](u32 i) {
return glm::vec4(m[0][i], m[1][i], m[2][i], m[3][i]);
};
const glm::vec4 r0 = row(0);
const glm::vec4 r1 = row(1);
const glm::vec4 r2 = row(2);
const glm::vec4 r3 = row(3);
f.planes[0] = r3 + r0; // left: x' >= -w
f.planes[1] = r3 - r0; // right: x' <= w
f.planes[2] = r3 + r1; // bottom: y' >= -w
f.planes[3] = r3 - r1; // top: y' <= w
f.planes[4] = r2; // near: z' >= 0(Vulkan [0,1])
f.planes[5] = r3 - r2; // far: z' <= w
// 归一化:法线长度把透视/缩放因数从距离里除掉
for (auto& p : f.planes) {
const f32 len = glm::length(glm::vec3(p));
if (len > 1e-8f) {
p /= len;
}
}
return f;
}
// 包围球与视锥相交测试(外接球近似,略保守——宁多画不漏画)。
// 对每个平面:球心在平面负侧且距离超过 radius 才算外。
bool IntersectsSphere(const glm::vec3& center, f32 radius) const
{
for (const auto& p : planes) {
if (glm::dot(glm::vec3(p), center) + p.w < -radius) {
return false;
}
}
return true;
}
};
} // namespace Engine
#endif //HELLOVULKAN_FRUSTUM_H
+25 -3
View File
@@ -1,12 +1,22 @@
// //
// Created by atdunbg on 2026/9/21. // Created by atdunbg on 2026/9/21.
// //
// Material:着色材质 = 一组纹理槽 + 一个描述符集。
//
// 槽位(binding = 槽位序号,与 simple_shader.frag 契约一致):
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
// 2 specular(高光强度/颜色,线性) 3 emissive(自发光,sRGB)
//
// 纹理非拥有(Model 的纹理缓存持有);空槽由调用方传默认纹理
//(白/平法线/黑/黑,Model 持有),shader 永远可以安全采样。
//
#ifndef HELLOVULKAN_MATERIAL_H #ifndef HELLOVULKAN_MATERIAL_H
#define HELLOVULKAN_MATERIAL_H #define HELLOVULKAN_MATERIAL_H
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <array>
#include <string> #include <string>
namespace Engine namespace Engine
@@ -18,20 +28,32 @@ namespace Engine
class Material class Material
{ {
public: public:
// 槽位序号(与 binding 对齐)
enum Slot : u32 {
Albedo = 0,
Normal = 1,
Specular = 2,
Emissive = 3,
kSlotCount = 4,
};
Material(Device& device, Material(Device& device,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout, vk::DescriptorSetLayout layout,
Texture* texture); const std::array<Texture*, kSlotCount>& textures);
~Material(); ~Material();
Material(const Material&) = delete; Material(const Material&) = delete;
Material& operator=(const Material&) = delete; Material& operator=(const Material&) = delete;
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; } vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
Texture* GetTexture() const { return m_Texture; } Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
// 材质缓存键:同贴图组合共享一份(Model 层使用)
static std::string MakeCacheKey(const std::string paths[kSlotCount]);
private: private:
Texture* m_Texture = nullptr; std::array<Texture*, kSlotCount> m_Textures{};
vk::DescriptorSet m_DescriptorSet; vk::DescriptorSet m_DescriptorSet;
}; };
} // namespace Engine } // namespace Engine
+15 -4
View File
@@ -21,25 +21,36 @@ namespace Engine
class Mesh class Mesh
{ {
public: public:
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
Mesh(Device& device, MemoryAllocator& allocator, Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<u32>& indices, const std::vector<u32>& indices,
Scope<Material> material); Material* material);
~Mesh(); ~Mesh();
Mesh(const Mesh&) = delete; Mesh(const Mesh&) = delete;
Mesh& operator=(const Mesh&) = delete; Mesh& operator=(const Mesh&) = delete;
void Draw(vk::CommandBuffer cmd) const; // instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const;
Material* GetMaterial() const { return m_Material.get(); } Material* GetMaterial() const { return m_Material; }
u32 GetIndexCount() const { return m_IndexCount; } u32 GetIndexCount() const { return m_IndexCount; }
// 局部空间包围球(构造时从顶点算出),视锥剔除用
struct Bounds {
glm::vec3 center{0.0f};
f32 radius = 0.0f;
};
const Bounds& GetBounds() const { return m_Bounds; }
private: private:
Scope<Buffer> m_VertexBuffer; Scope<Buffer> m_VertexBuffer;
Scope<Buffer> m_IndexBuffer; Scope<Buffer> m_IndexBuffer;
Scope<Material> m_Material; Material* m_Material;
u32 m_IndexCount = 0; u32 m_IndexCount = 0;
Bounds m_Bounds;
u64 m_VertexBytes = 0; u64 m_VertexBytes = 0;
u64 m_IndexBytes = 0; u64 m_IndexBytes = 0;
+16 -5
View File
@@ -8,6 +8,7 @@
#pragma once #pragma once
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <array> #include <array>
@@ -25,6 +26,7 @@ namespace Engine
class MemoryAllocator; class MemoryAllocator;
class DescriptorManager; class DescriptorManager;
class Texture; class Texture;
class Material;
class Model class Model
{ {
@@ -58,20 +60,29 @@ namespace Engine
void LoadModel(const std::string& path); void LoadModel(const std::string& path);
void ProcessNode(aiNode* node, const aiScene* scene); void ProcessNode(aiNode* node, const aiScene* scene);
Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene); Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene);
void InitDefaultTextures();
Texture* GetOrLoadTexture(const std::string& path); Texture* GetOrLoadTexture(const std::string& path, bool srgb);
// 材质缓存入口:同贴图组合共享同一 Material(一个描述符集),
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot(albedo/normal/specular/emissive)
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount]);
Device& m_Device; Device& m_Device;
MemoryAllocator& m_Allocator; MemoryAllocator& m_Allocator;
DescriptorManager& m_DescriptorManager; DescriptorManager& m_DescriptorManager;
vk::DescriptorSetLayout m_TextureLayout; vk::DescriptorSetLayout m_TextureLayout;
// 缓存声明在 m_Meshes 之前:mesh 持 Material* 裸指针,
// 析构序 = meshes 先于材质/纹理缓存
std::unordered_map<std::string, Scope<Material>> m_MaterialCache;
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
// 各槽位默认纹理(材质缺图时兜底):白 / 平法线 / 黑高光 / 黑自发光
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
std::vector<Scope<Mesh>> m_Meshes; std::vector<Scope<Mesh>> m_Meshes;
std::string m_ModelPath; std::string m_ModelPath;
// 纹理缓存:同一路径只加载一次
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
Scope<Texture> m_DefaultTexture;
}; };
} // namespace Engine } // namespace Engine
@@ -0,0 +1,44 @@
//
// Created by atdunbg on 2026/9/23.
//
// BloomPass:Extract(HDR 软阈值)→ BlurH → BlurV,全部半分辨率。
// 一个"效果"对应多个 RG pass 的例子(Unity 的一个 ScriptableRenderPass
// 常含多个 PassEvent 阶段,同构)。
//
#ifndef HELLOVULKAN_BLOOMPASS_H
#define HELLOVULKAN_BLOOMPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine::Passes {
class BloomPass final : public RenderPass {
public:
BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
RG::TextureHandle hdrColor,
RG::TextureHandle bloomA, RG::TextureHandle bloomB);
~BloomPass() override = default;
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
FullscreenGpu& m_Gpu;
PostProcessParams& m_Params;
RG::TextureHandle m_HDR;
RG::TextureHandle m_A; // 提取 + 垂直模糊结果
RG::TextureHandle m_B; // 水平模糊中间结果
// per-slot 输入描述符
std::vector<vk::DescriptorSet> m_ExtractSets;
std::vector<vk::DescriptorSet> m_BlurHSets;
std::vector<vk::DescriptorSet> m_BlurVSets;
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_BLOOMPASS_H
@@ -0,0 +1,41 @@
//
// Created by atdunbg on 2026/9/23.
//
// ForwardPass:相机视角渲染到 HDR 颜色图(线性光照值)。
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
//
#ifndef HELLOVULKAN_FORWARDPASS_H
#define HELLOVULKAN_FORWARDPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/SceneTypes.h>
namespace Engine::Passes {
class ForwardPass final : public RenderPass {
public:
ForwardPass(SceneGpu& gpu, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
RG::TextureHandle depth,
const std::array<RG::TextureHandle, kMaxCascades>& shadowMaps)
: m_Gpu(gpu), m_Frame(frame),
m_HDRColor(hdrColor), m_Velocity(velocity),
m_Depth(depth), m_ShadowMaps(shadowMaps) {}
void Register(RG::RenderGraph& rg) override;
private:
SceneGpu& m_Gpu;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件)
RG::TextureHandle m_Depth;
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_FORWARDPASS_H
@@ -0,0 +1,81 @@
//
// Created by atdunbg on 2026/9/23.
//
// FullscreenGpu:全屏后处理共享装配(Tonemap/Bloom 系 pass 共用)。
//
// 全屏 pass 的共性收口在这里:Set0 = {输入0, 输入1}、push 16B 通用结构
// (规避 VUID-08878 的"range 定义必须一致")、无顶点输入全屏三角、
// 公共录制函数。新增全屏效果 = 复用本类 + 新增一个 frag shader。
//
#ifndef HELLOVULKAN_FULLSCREENGPU_H
#define HELLOVULKAN_FULLSCREENGPU_H
#include <Engine/Core/Types.h>
#include <Engine/RenderGraph/RenderGraph.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
class Device;
class DescriptorManager;
class ShaderManager;
namespace Passes {
// 全屏 pass 通用 push(16B):所有全屏 pass 统一声明与推送
struct FullscreenPush {
f32 exposure;
f32 param0; // 各 pass 自定语义(tonemap: aces 开关)
f32 param1;
f32 param2;
};
// 后处理链参数(ImGui 可调;Renderer 持有,pass 按引用读取)
struct PostProcessParams {
f32 exposure = 1.0f;
bool useACES = true;
f32 bloomThreshold = 1.0f; // HDR 域亮度阈值
f32 bloomStrength = 0.8f; // 叠加强度
bool showTemporalDiff = false; // 调试视图:显示帧历史差异
};
class FullscreenGpu {
public:
FullscreenGpu(Device& device, DescriptorManager& descriptors,
ShaderManager& shaders);
~FullscreenGpu();
FullscreenGpu(const FullscreenGpu&) = delete;
FullscreenGpu& operator=(const FullscreenGpu&) = delete;
vk::DescriptorSetLayout setLayout; // Set0 = {输入0, 输入1, 调试输入}
vk::PipelineLayout layout; // push 16B(FullscreenPush)
vk::Sampler sampler;
vk::ShaderEXT vert{}, extractFrag{}, blurFrag{}, tonemapFrag{};
vk::ShaderEXT diffFrag{}; // temporal_diff(帧历史调试)
vk::ShaderEXT taaFrag{}; // taa(时域抗锯齿 resolve)
// 各 pass 自己持有 per-slot set;分配统一走这里
vk::DescriptorSet AllocateSet();
// 输入图片绑定(RG 重编译后重绑;物理镜像按槽位各一份)
void UpdateSetInput(vk::DescriptorSet set, u32 binding,
RG::RenderGraph& rg, RG::TextureHandle input,
u32 slot);
// 全屏 pass 公共录制:动态状态/draw 全一致,仅附件 view、
// 输入 set、push 值不同
void RecordFullscreen(RG::RenderGraphContext& ctx, vk::ImageView view,
vk::ShaderEXT frag, const vk::DescriptorSet& set,
const FullscreenPush& push);
private:
Device& m_Device;
DescriptorManager& m_Descriptors;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_FULLSCREENGPU_H
@@ -0,0 +1,38 @@
//
// Created by atdunbg on 2026/9/23.
//
// ImGuiPass:叠加层(loadOp=Load,不清屏)。
// 框架只负责把 draw data 录进 cmd;UI 内容全部由派生类在
// OnRenderOverlay() 里构建,框架自身不画任何 ImGui 控件。
// 未启用叠加层时空转(begin/end 一次空 rendering region)。
//
#ifndef HELLOVULKAN_IMGUIPASS_H
#define HELLOVULKAN_IMGUIPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
namespace Engine {
class ImGuiLayer;
namespace Passes {
class ImGuiPass final : public RenderPass {
public:
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
//(EnableImGui 时机由派生类决定),执行期解引用取最新值
ImGuiPass(ImGuiLayer** layer, RG::TextureHandle backbuffer)
: m_Layer(layer), m_Backbuffer(backbuffer) {}
void Register(RG::RenderGraph& rg) override;
private:
ImGuiLayer** m_Layer;
RG::TextureHandle m_Backbuffer;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_IMGUIPASS_H
@@ -0,0 +1,32 @@
//
// Created by atdunbg on 2026/9/23.
//
// PresentPass:transition 到 ePresentSrcKHR。
// layout 转换由 RG 的 barrier 完成,实际 vkQueuePresentKHR 由 Renderer 驱动。
//
#ifndef HELLOVULKAN_PRESENTPASS_H
#define HELLOVULKAN_PRESENTPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
namespace Engine::Passes {
class PresentPass final : public RenderPass {
public:
explicit PresentPass(RG::TextureHandle backbuffer)
: m_Backbuffer(backbuffer) {}
void Register(RG::RenderGraph& rg) override
{
rg.AddPass("Present", [](RG::RenderGraphContext&) {})
.Read(m_Backbuffer, RG::ResourceUse::Present);
}
private:
RG::TextureHandle m_Backbuffer;
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_PRESENTPASS_H
@@ -0,0 +1,34 @@
//
// Created by atdunbg on 2026/9/23.
//
// RenderPass:渲染 pass 对象基类(Unity ScriptableRenderPass 思路)。
//
// pass 自己负责两件事,框架(Renderer)不再手写"超级函数":
// Register —— 向 RenderGraph 声明资源使用 + 录制 lambda;
// 执行序由 RG 拓扑排序决定(writer 必先于 reader),
// 与注册顺序无关
// OnTargetsRebuilt —— RG 重编译(resize / vsync 切换)后重绑描述符
// (物理镜像按槽位可能全部换新)
//
// pass 之间不互相依赖:数据流只经 RG 资源句柄。新增效果 = 新增一个
// RenderPass 子类 + Renderer::BuildGraph 里注册一行,其它代码不动。
//
#ifndef HELLOVULKAN_RENDERPASS_H
#define HELLOVULKAN_RENDERPASS_H
#include <Engine/RenderGraph/RenderGraph.h>
namespace Engine::Passes {
class RenderPass {
public:
virtual ~RenderPass() = default;
virtual void Register(RG::RenderGraph& rg) = 0;
virtual void OnTargetsRebuilt(RG::RenderGraph& rg) { (void)rg; }
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_RENDERPASS_H
@@ -0,0 +1,71 @@
//
// Created by atdunbg on 2026/9/23.
//
// SceneGpu:场景级 GPU 共享装配(Shadow/Forward 及未来几何类 pass 共用)。
//
// 从 Renderer 拆出的装配体:set 布局 / pipeline 布局 / shader object /
// 采样器 / per-slot 描述符集合。新增几何 pass(延迟、调试线框等)直接
// 复用本装配,不必碰 Renderer。
//
#ifndef HELLOVULKAN_SCENEGPU_H
#define HELLOVULKAN_SCENEGPU_H
#include <Engine/Core/FrameContext.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/RenderGraph/RenderGraph.h>
#include <vulkan/vulkan.hpp>
#include <array>
#include <vector>
namespace Engine {
class Device;
class DescriptorManager;
class ShaderManager;
class UniformBuffer;
namespace Passes {
class SceneGpu {
public:
SceneGpu(Device& device, DescriptorManager& descriptors,
ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer);
~SceneGpu();
SceneGpu(const SceneGpu&) = delete;
SceneGpu& operator=(const SceneGpu&) = delete;
// ---- set 布局(shader 契约,改绑定必须同步 shaders/)----
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer}
vk::DescriptorSetLayout textureSetLayout; // Set1 = {albedo}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图}
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
vk::PipelineLayout shadowLayout; // Set0,push = lightVP 64B
// per-slot 描述符:Set0(UBO+实例 SSBO)与 Set2(阴影图采样)
std::vector<vk::DescriptorSet> uboSets;
std::vector<vk::DescriptorSet> shadowSets;
// 阴影采样器:clamp-to-border 白色 = 光视锥外自动无阴影
//(省 shader 出界分支)
vk::Sampler shadowSampler;
vk::ShaderEXT forwardVert{}, forwardFrag{};
vk::ShaderEXT shadowVert{}, shadowFrag{};
// RG 重编译后重绑阴影图(物理镜像按槽位各一份)
void BindShadowMaps(RG::RenderGraph& rg,
const std::array<RG::TextureHandle, kMaxCascades>& maps);
private:
Device& m_Device;
DescriptorManager& m_Descriptors;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_SCENEGPU_H
@@ -0,0 +1,36 @@
//
// Created by atdunbg on 2026/9/23.
//
// ShadowPass:CSM 级联阴影。单 pass 渲 kMaxCascades 个 region,
// 每级联独立光源 VP(FrameSceneData::lightSpace[c],SceneRenderer 拟合),
// 消费光源视锥剔除结果(FrameSceneData::shadowView——相机外实体仍投影子)。
//
#ifndef HELLOVULKAN_SHADOWPASS_H
#define HELLOVULKAN_SHADOWPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/SceneTypes.h>
#include <array>
namespace Engine::Passes {
class ShadowPass final : public RenderPass {
public:
ShadowPass(SceneGpu& gpu, FrameSceneData& frame,
const std::array<RG::TextureHandle, kMaxCascades>& shadowMaps)
: m_Gpu(gpu), m_Frame(frame), m_ShadowMaps(shadowMaps) {}
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
SceneGpu& m_Gpu;
FrameSceneData& m_Frame;
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_SHADOWPASS_H
@@ -0,0 +1,42 @@
//
// Created by atdunbg on 2026/9/23.
//
// TAAPass:时域抗锯齿 resolve。
// 读当前帧 HDR + velocity + 历史累积(ReadHistory 自引用 TAAColor),
// 邻域 clamp 去鬼影后时域混合。TAAColor 既是输出也是下一帧的历史——
// 这是帧历史资源最典型的"自引用"形态(UE 的 history pool 同构)。
//
#ifndef HELLOVULKAN_TAAPASS_H
#define HELLOVULKAN_TAAPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine::Passes {
class TAAPass final : public RenderPass {
public:
TAAPass(FullscreenGpu& gpu, f32 historyAlpha,
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
RG::TextureHandle taaColor);
~TAAPass() override = default;
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
FullscreenGpu& m_Gpu;
f32 m_HistoryAlpha;
RG::TextureHandle m_HDR;
RG::TextureHandle m_Velocity;
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
std::vector<vk::DescriptorSet> m_Sets; // per-slot
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_TAAPASS_H
@@ -0,0 +1,40 @@
//
// Created by atdunbg on 2026/9/23.
//
// TemporalDiffPass:帧历史链路调试视图。
// 读 HDRColor 的帧历史镜像(ReadHistory)与当前帧,输出 |prev − cur| × 增益。
// 静止画面纯黑、运动边缘发光 = 帧历史机制正确;噪声/残影错位 = 有 bug。
// 也是 RenderGraph::ReadHistory 的参考实现(TAA 照此模式接入)。
//
#ifndef HELLOVULKAN_TEMPORALDIFFPASS_H
#define HELLOVULKAN_TEMPORALDIFFPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine::Passes {
class TemporalDiffPass final : public RenderPass {
public:
TemporalDiffPass(FullscreenGpu& gpu, f32 gain,
RG::TextureHandle hdrColor, RG::TextureHandle output);
~TemporalDiffPass() override = default;
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
FullscreenGpu& m_Gpu;
f32 m_Gain;
RG::TextureHandle m_HDR;
RG::TextureHandle m_Output;
std::vector<vk::DescriptorSet> m_Sets; // per-slot
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_TEMPORALDIFFPASS_H
@@ -0,0 +1,42 @@
//
// Created by atdunbg on 2026/9/23.
//
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 Backbuffer。
// 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
// Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。
//
#ifndef HELLOVULKAN_TONEMAPPASS_H
#define HELLOVULKAN_TONEMAPPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine::Passes {
class TonemapPass final : public RenderPass {
public:
TonemapPass(FullscreenGpu& gpu, PostProcessParams& params,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer);
~TonemapPass() override = default;
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
FullscreenGpu& m_Gpu;
PostProcessParams& m_Params;
RG::TextureHandle m_HDR;
RG::TextureHandle m_A;
RG::TextureHandle m_TemporalDiff; // 调试视图输入(当前帧槽位)
RG::TextureHandle m_Backbuffer;
std::vector<vk::DescriptorSet> m_Sets; // per-slot
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_TONEMAPPASS_H
+68 -81
View File
@@ -1,6 +1,19 @@
// //
// Created by atdunbg on 2026/9/20. // Created by atdunbg on 2026/9/20.
// //
// Renderer:渲染门面 + 渲染侧组合根。
//
// 分工(对照 UE/Unity 的渲染架构):
// Renderer —— 装配模块、声明 RG 资源、注册 pass 对象、
// 每帧编排(翻译场景 → 写 buffer → 驱动 RG → 提交呈现)。
// 不理解场景细节,不含 pass 实现。
// SceneRenderer —— 场景 → FrameSceneData 的翻译层(相机/灯光/剔除,
// 对应 UE FSceneRenderer / Unity CullingResults)。
// Passes::* —— 各 pass 的声明与录制(对应 Unity ScriptableRenderPass)。
// 新增效果 = 新增 pass 类 + BuildGraph 注册一行。
//
// 对外只暴露两个入口:Render(Scene&, Assets&) 和给 App 层调参的访问器。
//
#ifndef HELLOVULKAN_RENDERER_H #ifndef HELLOVULKAN_RENDERER_H
#define HELLOVULKAN_RENDERER_H #define HELLOVULKAN_RENDERER_H
@@ -9,12 +22,12 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h> #include <Engine/Core/FrameContext.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/RenderGraph/RenderGraph.h> #include <Engine/RenderGraph/RenderGraph.h>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <array>
#include <vector> #include <vector>
#include <glm/glm.hpp>
namespace Engine { namespace Engine {
class Window; class Window;
@@ -26,18 +39,14 @@ namespace Engine {
class SyncManager; class SyncManager;
class CommandManager; class CommandManager;
class UniformBuffer; class UniformBuffer;
class Model; class SceneRenderer;
class VulkanAssetUploader;
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
class ImGuiLayer; class ImGuiLayer;
// Renderer 职责(收敛后): namespace ECS { class Scene; }
// 1. 装配底层模块(Device/Swapchain/Sync/Command/Descriptor/Shader) namespace Assets { class AssetManager; class IAssetUploader; }
// 2. 声明并驱动 RenderGraph(pass 结构在这里组装) namespace Passes { class RenderPass; class SceneGpu; class FullscreenGpu; class PostProcessParams; }
// 3. Render(Scene&, AssetManager&):从 ECS 场景读取相机/灯光/渲染体,
// 经资产层解析句柄后绘制
// 不拥有场景,不加载资产(资产归 AssetManager,磁盘 IO 与这里无关)。
class Renderer { class Renderer {
public: public:
Renderer(Window& window); Renderer(Window& window);
@@ -52,91 +61,85 @@ namespace Engine {
Device& GetDevice() const { return *m_Device; } Device& GetDevice() const { return *m_Device; }
MemoryAllocator& GetAllocator() const { return *m_Allocator; } MemoryAllocator& GetAllocator() const { return *m_Allocator; }
DescriptorManager& GetDescriptorManager() const { return *m_DescriptorManager; } DescriptorManager& GetDescriptorManager() const { return *m_DescriptorManager; }
vk::DescriptorSetLayout GetTextureSetLayout() const { return m_TextureSetLayout; } vk::DescriptorSetLayout GetTextureSetLayout() const; // 实现在 cpp(SceneGpu 成员)
vk::Format GetSwapchainFormat() const; // Swapchain 不完整,实现在 cpp vk::Format GetSwapchainFormat() const;
u32 GetSwapchainImageCount() const; u32 GetSwapchainImageCount() const;
// 资产上传服务(渲染层对资产层提供的 IAssetUploader 实现)
Assets::IAssetUploader& GetAssetUploader() const;
// 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。 // 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。
// 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。 // 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。
void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; } void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; }
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
f32 GetExposure() const;
void SetExposure(f32 e);
bool GetUseACES() const;
void SetUseACES(bool b);
f32 GetBloomThreshold() const;
void SetBloomThreshold(f32 v);
f32 GetBloomStrength() const;
void SetBloomStrength(f32 v);
// 调试视图:显示帧历史差异(TemporalDiff pass 产出)
bool GetShowTemporalDiff() const;
void SetShowTemporalDiff(bool b);
// pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应)
const std::vector<std::string>& GetPassNames() const { return m_PassNames; }
const std::vector<f32>& GetPassGpuMs() const { return m_PassGpuMs; }
// 双视图剔除后的实例数(遥测)
u32 GetMainInstanceCount() const;
u32 GetShadowInstanceCount() const;
// 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前)
void SetVSync(bool vsync);
private: private:
void BuildGraph(); // 声明 pass 结构 + 首次 Compile void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
void HandleResize(); void HandleResize();
void UpdateShadowDescriptors();
void UpdateFullscreenDescriptors();
void ReadGpuTimes(u32 frameSlot); void ReadGpuTimes(u32 frameSlot);
Window& m_Window; Window& m_Window;
// ---- 底层模块(析构序:后声明者先析构,RG 在 Device/Allocator 之前)---- // ---- 底层模块(析构序:后声明者先析构;引用他人者先析构)----
Scope<Device> m_Device; Scope<Device> m_Device;
Scope<MemoryAllocator> m_Allocator; Scope<MemoryAllocator> m_Allocator;
Scope<DescriptorManager> m_DescriptorManager; Scope<DescriptorManager> m_DescriptorManager;
Scope<ShaderManager> m_ShaderManager; Scope<ShaderManager> m_ShaderManager;
Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
Scope<Swapchain> m_Swapchain; Scope<Swapchain> m_Swapchain;
Scope<SyncManager> m_SyncManager; Scope<SyncManager> m_SyncManager;
Scope<CommandManager> m_CommandManager; Scope<CommandManager> m_CommandManager;
Scope<UniformBuffer> m_UniformBuffer; Scope<SceneRenderer> m_SceneRenderer; // 场景翻译层(先于 pass 构造)
Scope<RG::RenderGraph> m_RenderGraph; Scope<RG::RenderGraph> m_RenderGraph;
Scope<VulkanAssetUploader> m_AssetUploader;
// ---- 管线装配(shader object 方案,无 vk::Pipeline)---- // pass 对象(注册序;执行序由 RG 拓扑排序决定)
// Forward layout:3 个 set(UBO/贴图/阴影图)+ 128B push(与 Shadow pass std::vector<Scope<Passes::RenderPass>> m_Passes;
// 共用上限;forward 的 shader block 只有 64B,block ⊂ range 合法)
vk::PipelineLayout m_PipelineLayout;
vk::DescriptorSetLayout m_UBOSetLayout;
vk::DescriptorSetLayout m_TextureSetLayout;
vk::DescriptorSetLayout m_ShadowSetLayout;
std::vector<vk::DescriptorSet> m_UBODescriptorSets; // 每帧槽位一个
std::vector<vk::DescriptorSet> m_ShadowDescriptorSets; // 每帧槽位一个
vk::Sampler m_ShadowSampler;
vk::ShaderEXT m_VertShader{};
vk::ShaderEXT m_FragShader{};
// Shadow pass 独立装配:无 set,push constant = {lightVP, model} 128B
vk::PipelineLayout m_ShadowPipelineLayout;
vk::ShaderEXT m_ShadowVertShader{};
vk::ShaderEXT m_ShadowFragShader{};
// Tonemap pass 装配:Set0 = {HDR 颜色图, Bloom 图},push = 16B
//(全屏 pass 通用:exposure/aces/bloomStrength/pad)
vk::PipelineLayout m_TonemapPipelineLayout;
vk::DescriptorSetLayout m_TonemapSetLayout;
std::vector<vk::DescriptorSet> m_TonemapDescriptorSets; // 每帧槽位一个
vk::Sampler m_TonemapSampler;
vk::ShaderEXT m_TonemapVertShader{};
vk::ShaderEXT m_TonemapFragShader{};
// Bloom:Extract(HDR→A) → BlurH(A→B) → BlurV(B→A),半分辨率
// 与 Tonemap 共用 set layout/pipeline layout/vertex shader
std::vector<vk::DescriptorSet> m_BloomExtractSets; // 每帧槽位
std::vector<vk::DescriptorSet> m_BloomBlurHSets;
std::vector<vk::DescriptorSet> m_BloomBlurVSets;
vk::ShaderEXT m_BloomExtractFrag{};
vk::ShaderEXT m_BloomBlurFrag{};
// ---- RenderGraph 资源句柄 ---- // ---- RenderGraph 资源句柄 ----
RG::TextureHandle m_Backbuffer{}; RG::TextureHandle m_Backbuffer{};
RG::TextureHandle m_Depth{}; RG::TextureHandle m_Depth{};
RG::TextureHandle m_ShadowMap{}; std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),Tonemap 归一 RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
RG::TextureHandle m_Velocity{}; // Forward 输出(clip 空间运动向量)
RG::TextureHandle m_TAAColor{}; // TAA 累积输出(自引用历史),后处理源
RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果 RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果
RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果 RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果
RG::TextureHandle m_TemporalDiff{}; // 半分辨率:帧历史差异(调试)
// ---- 当帧绑定(Render() 期间有效,供 pass 回调读取)---- // pass 共享的可调参数(Renderer 持有,App 层访问器转发)
ECS::Scene* m_BoundScene = nullptr; Scope<Passes::PostProcessParams> m_PostParams;
Assets::AssetManager* m_BoundAssets = nullptr;
glm::mat4 m_LightSpace{1.0f}; // 阴影光源 VP(每帧由 Render 算好)
// 可选叠加层(不拥有;Application 持有) // 可选叠加层(不拥有;Application 持有)
ImGuiLayer* m_ImGuiLayer = nullptr; ImGuiLayer* m_ImGuiLayer = nullptr;
// ---- Tonemap/Bloom 参数(ImGui 可调)----
f32 m_Exposure = 1.0f;
bool m_UseACES = true;
f32 m_BloomThreshold = 1.0f; // HDR 域亮度阈值
f32 m_BloomStrength = 0.8f; // 叠加强度
// GPU timestamp(pass 级耗时,ImGui 可读) // GPU timestamp(pass 级耗时,ImGui 可读)
vk::QueryPool m_QueryPool; vk::QueryPool m_QueryPool;
u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2 u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2
@@ -145,22 +148,6 @@ namespace Engine {
std::vector<f32> m_PassGpuMs; // 最近一帧各 pass 耗时 std::vector<f32> m_PassGpuMs; // 最近一帧各 pass 耗时
bool m_TimesPrimed[2] = {false, false}; // 每槽位首帧跳过读取 bool m_TimesPrimed[2] = {false, false}; // 每槽位首帧跳过读取
public:
f32 GetExposure() const { return m_Exposure; }
void SetExposure(f32 e) { m_Exposure = e; }
bool GetUseACES() const { return m_UseACES; }
void SetUseACES(bool b) { m_UseACES = b; }
f32 GetBloomThreshold() const { return m_BloomThreshold; }
void SetBloomThreshold(f32 v) { m_BloomThreshold = v; }
f32 GetBloomStrength() const { return m_BloomStrength; }
void SetBloomStrength(f32 v) { m_BloomStrength = v; }
// pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应)
const std::vector<std::string>& GetPassNames() const { return m_PassNames; }
const std::vector<f32>& GetPassGpuMs() const { return m_PassGpuMs; }
private:
vk::Extent2D m_Extent{}; vk::Extent2D m_Extent{};
u32 m_CurrentFrame = 0; u32 m_CurrentFrame = 0;
}; };
@@ -0,0 +1,93 @@
//
// Created by atdunbg on 2026/9/23.
//
// SceneRenderer:场景 → 渲染数据的每帧翻译层(UE FSceneRenderer /
// Unity CullingResults 的对应物)。
//
// 职责(全部是"读场景、产数据",不碰 GPU、不碰 Vulkan):
// 1. 相机/灯光/环境色 → UniformBufferObject 语义组装(含阴影光空间矩阵)
// 2. 单趟实例收集 + 双视图剔除(相机视锥/光源视锥各自独立可见列表——
// 相机外实体仍可能投影子进视野,shadow popping 的教训)
// Renderer 拿 FrameSceneData 写 buffer、驱动 pass;场景理解全部收口在此。
//
// 扩展性:
// - 多光源:lights 数组已就位(kMaxLights),每灯各自 lightSpace 即 CSM 前置
// - 级联阴影:BuildFrame 内按级联多出几份 lightSpace + shadowView,
// pass 侧按索引消费,本类结构不变
// - 遮挡剔除/GPU 剔除:只改本类收集逻辑,下游无感知
//
#ifndef HELLOVULKAN_SCENERENDERER_H
#define HELLOVULKAN_SCENERENDERER_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/SceneTypes.h>
#include <vulkan/vulkan.hpp>
#include <unordered_map>
namespace Engine {
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
class SceneRenderer {
public:
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent);
FrameSceneData& Frame() { return m_Frame; }
// ---- 遥测 ----
u32 GetMainInstanceCount() const { return m_Frame.mainView.instanceCount; }
u32 GetShadowInstanceCount() const
{
u32 sum = 0;
for (const auto& c : m_Frame.shadowCascades) sum += c.instanceCount;
return sum;
}
f32 GetCollectMs() const { return m_Frame.collectMs; }
private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
//(Forward UBO)共用同一组矩阵。
void ComputeCascades(const glm::vec3& shadowDir);
FrameSceneData m_Frame;
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
struct ViewScratch {
CullResult result;
std::vector<glm::mat4> worlds;
std::vector<glm::mat4> prevWorlds;
std::unordered_map<Mesh*, u32> lookup;
void Clear()
{
result.batches.clear();
result.instanceCount = 0;
worlds.clear();
prevWorlds.clear();
lookup.clear();
}
};
ViewScratch m_MainScratch;
ViewScratch m_CascScratch[kMaxCascades];
// ---- TAA 时域状态 ----
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity)
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity)
// 实体的上一帧世界矩阵(key = entt 实体 integral)。
// 每帧重建 m_CurWorlds 后与 m_PrevWorlds swap——自动清理销毁实体
std::unordered_map<u32, glm::mat4> m_PrevWorlds;
std::unordered_map<u32, glm::mat4> m_CurWorlds;
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
};
} // namespace Engine
#endif //HELLOVULKAN_SCENERENDERER_H
@@ -0,0 +1,83 @@
//
// Created by atdunbg on 2026/9/23.
//
// SceneTypes:场景 → GPU 的数据契约(CPU 结构镜像 shader 布局)。
//
// SceneRenderer 生产、Renderer/各 pass 消费;改字段必须同步 shaders/。
// 常量也集中在这里:SSBO 容量、灯数、阴影图尺寸都是"渲染预算",
// 改任何一个都要同步评估 buffer/池容量。
//
#ifndef HELLOVULKAN_SCENETYPES_H
#define HELLOVULKAN_SCENETYPES_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <vector>
namespace Engine {
class Mesh;
// GPUDriven:按 mesh 聚合的绘制批
struct InstanceBatch {
Mesh* mesh;
u32 first; // 在实例矩阵数组中的起始下标
u32 count; // 实例数
};
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)
struct CullResult {
std::vector<InstanceBatch> batches;
u32 instanceCount = 0;
};
constexpr u32 kMaxLights = 4;
constexpr u32 kMaxInstances = 16384; // 实例 SSBO 容量(矩阵数)
constexpr u32 kMaxCascades = 3; // CSM 级联数
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
// 镜像 shader 侧 UBO 布局的灯光项
struct LightData {
glm::vec4 direction; // xyz = 方向(已归一),w 未用
glm::vec4 color; // rgb = 颜色,a = 强度
};
// 镜像 simple_shader 的 UBO 布局(std140 语义由 shader 侧对齐)。
// 字段顺序改动必须同步 simple_shader.vert/frag(shadow.vert 只用 SSBO)。
struct UniformBufferObject {
glm::mat4 view;
glm::mat4 proj; // 抖动后的投影(光栅化用)
glm::mat4 lightSpace[kMaxCascades]; // CSM 各级联光空间 VP([0,1] 重映射)
glm::vec4 cascadeSplits; // xyz = 级联 0..2 远边界(视图深度,正值),w 未用
glm::vec4 ambientColor;
LightData lights[kMaxLights];
// ---- TAA(velocity 在 vertex 里算)----
glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用)
glm::mat4 prevView; // 上一帧视图(velocity)
glm::mat4 prevProj; // 上一帧未抖动投影(velocity)
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
};
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
// 复用成员容量:clear() 不缩容,稳态零每帧堆分配。
struct FrameSceneData {
UniformBufferObject ubo{};
// 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO)
std::vector<glm::mat4> instanceModels;
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
std::vector<glm::mat4> prevInstanceModels;
CullResult mainView; // 相机视锥 → Forward pass
// 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
CullResult shadowCascades[kMaxCascades];
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
};
} // namespace Engine
#endif //HELLOVULKAN_SCENETYPES_H
+8 -2
View File
@@ -14,6 +14,7 @@ namespace Engine {
glm::vec3 position; glm::vec3 position;
glm::vec3 normal; glm::vec3 normal;
glm::vec2 uv; glm::vec2 uv;
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace)
static vk::VertexInputBindingDescription2EXT GetBindingDescription() { static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
vk::VertexInputBindingDescription2EXT binding; vk::VertexInputBindingDescription2EXT binding;
@@ -24,8 +25,8 @@ namespace Engine {
return binding; return binding;
} }
static std::array<vk::VertexInputAttributeDescription2EXT, 3> GetAttributeDescriptions() { static std::array<vk::VertexInputAttributeDescription2EXT, 4> GetAttributeDescriptions() {
std::array<vk::VertexInputAttributeDescription2EXT, 3> attrs; std::array<vk::VertexInputAttributeDescription2EXT, 4> attrs;
attrs[0].setLocation(0) attrs[0].setLocation(0)
.setBinding(0) .setBinding(0)
@@ -42,6 +43,11 @@ namespace Engine {
.setFormat(vk::Format::eR32G32Sfloat) .setFormat(vk::Format::eR32G32Sfloat)
.setOffset(offsetof(Vertex, uv)); .setOffset(offsetof(Vertex, uv));
attrs[3].setLocation(3)
.setBinding(0)
.setFormat(vk::Format::eR32G32B32Sfloat)
.setOffset(offsetof(Vertex, tangent));
return attrs; return attrs;
} }
}; };
@@ -0,0 +1,43 @@
//
// Created by atdunbg on 2026/9/23.
//
// VulkanAssetUploader:IAssetUploader 的渲染层实现。
//
// 属于渲染层对资产层提供的服务:持有 GPU 上下文,负责把解析后的
// 资产数据创建为 Vulkan 资源。资产层(AssetManager)只看到接口,
// 不知道 Device/Allocator 的存在——依赖方向:Assets → IAssetUploader
// ← VulkanAssetUploader(渲染层),永远不出现 Assets → Vulkan 具体。
//
#ifndef HELLOVULKAN_VULKANASSETUPLOADER_H
#define HELLOVULKAN_VULKANASSETUPLOADER_H
#include <Engine/Assets/IAssetUploader.h>
namespace Engine {
class Device;
class MemoryAllocator;
class DescriptorManager;
class VulkanAssetUploader final : public Assets::IAssetUploader {
public:
VulkanAssetUploader(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
vk::DescriptorSetLayout textureLayout)
: m_Device(device), m_Allocator(allocator),
m_Descriptors(descriptors), m_TextureLayout(textureLayout) {}
Scope<Model> LoadModel(const std::string& path) override;
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
private:
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
vk::DescriptorSetLayout m_TextureLayout;
};
} // namespace Engine
#endif //HELLOVULKAN_VULKANASSETUPLOADER_H
-5
View File
@@ -36,11 +36,6 @@ namespace Engine {
vk::Buffer m_Buffer; vk::Buffer m_Buffer;
vk::DeviceSize m_Size; vk::DeviceSize m_Size;
vk::DeviceMemory m_Memory;
vk::DeviceSize m_Offset;
vk::DeviceSize m_BlockSize; // 所属内存块的总大小
MemoryAllocator::Allocation m_Allocation; MemoryAllocator::Allocation m_Allocation;
MemoryAllocator& m_Allocator; MemoryAllocator& m_Allocator;
@@ -34,6 +34,8 @@ namespace Engine {
void UpdateUniformBuffer(vk::DescriptorSet set, u32 binding, void UpdateUniformBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size); vk::Buffer buffer, u64 size);
void UpdateStorageBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size);
void UpdateTexture(vk::DescriptorSet set, u32 binding, void UpdateTexture(vk::DescriptorSet set, u32 binding,
vk::Sampler sampler, vk::ImageView view); vk::Sampler sampler, vk::ImageView view);
@@ -52,9 +54,17 @@ namespace Engine {
private: private:
Device& m_Device; Device& m_Device;
vk::DescriptorPool m_Pool;
static constexpr u32 kMaxSets = 1000; // 可增长描述符池(分块):场景材质集数量不可预估(1G 资产场景
// 可能上万),固定上限迟早打爆。耗尽时追加一块继续分配——
// 对齐 UE/Unity 的"growable pool/bindless 堆"思路的 Vulkan 版。
// 材质去重(同贴图共享 Material)仍是大场景的第一道闸门。
std::vector<vk::DescriptorPool> m_Pools;
// 每块的容量(set 数;各类型描述符数同值)
static constexpr u32 kChunkSets = 4096;
vk::DescriptorPool CreateChunk();
}; };
} // namespace Engine } // namespace Engine
+37 -31
View File
@@ -1,68 +1,74 @@
// //
// Created by atdunbg on 2026/9/20. // Created by atdunbg on 2026/9/20.
// //
// MemoryAllocator:显存分配器,VMA 后端。
//
// 2026-09-23 从手写 bump+free-list 块分配迁移到 VMA(VulkanMemoryAllocator):
// - TLSF 式通用分配 + DedicatedAllocation 扩展自动启用
// - 线性分配器(staging 类短命资源自动走线性算法)
// - VK_EXT_memory_budget 感知(分配决策参考真实显存余量)
// - 为后续内存别名(同一 VkDeviceMemory 多资源重绑)铺路
//
// 接口哲学:不再是"Allocate(reqs) + 手动 bind"——VMA 明确不推荐把分配与
// 绑定拆开。创建即分配即绑定,销毁必须走 Destroy(资源与内存成对释放),
// 这是本类存在的意义:把 VMA 的正确用法约束成一个出口。
//
#ifndef HELLOVULKAN_MEMORYALLOCATOR_H #ifndef HELLOVULKAN_MEMORYALLOCATOR_H
#define HELLOVULKAN_MEMORYALLOCATOR_H #define HELLOVULKAN_MEMORYALLOCATOR_H
#pragma once #pragma once
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <array>
#include <vector>
#include "Engine/Core/Types.h" #include "Engine/Core/Types.h"
// VMA 内部类型前向声明(实现在 VmaImpl.cpp,头文件不引入 vk_mem_alloc.h)
struct VmaAllocator_T;
struct VmaAllocation_T;
namespace Engine { namespace Engine {
class Device; class Device;
class MemoryAllocator { class MemoryAllocator {
public: public:
MemoryAllocator(Device& device); explicit MemoryAllocator(Device& device);
~MemoryAllocator(); ~MemoryAllocator();
MemoryAllocator(const MemoryAllocator&) = delete; MemoryAllocator(const MemoryAllocator&) = delete;
MemoryAllocator& operator=(const MemoryAllocator&) = delete; MemoryAllocator& operator=(const MemoryAllocator&) = delete;
// 分配结果:资源句柄 + 分配句柄合一。
// Buffer/Image 二选一有值;MappedPtr 仅 HOST_VISIBLE 非空(持久映射)。
struct Allocation { struct Allocation {
vk::DeviceMemory Memory; vk::Buffer Buffer{};
vk::DeviceSize Offset; vk::Image Image{};
vk::DeviceSize Size;
void* MappedPtr = nullptr; void* MappedPtr = nullptr;
vk::DeviceSize Size = 0;
// 内部用于 Free
u32 BlockIndex = 0;
u32 TypeIndex = 0;
bool Valid = false; bool Valid = false;
// VMA 内部句柄(上层不要碰)
VmaAllocation_T* Handle = nullptr;
}; };
Allocation Allocate(vk::MemoryRequirements reqs, vk::MemoryPropertyFlags props); // 创建 + 分配 + 绑定一步完成(VMA 惯用法)
void Free(const Allocation& alloc); Allocation CreateBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
vk::MemoryPropertyFlags props);
Allocation CreateImage(const vk::ImageCreateInfo& imageInfo,
vk::MemoryPropertyFlags props);
// 销毁资源并释放分配(按 alloc.Buffer/Image 判断类型),随后 alloc 归零
void Destroy(Allocation& alloc);
// HOST_VISIBLE 非 coherent 写入后冲刷(VMA 内部对 coherent 自动跳过)
void Flush(const Allocation& alloc);
void PrintStats() const; void PrintStats() const;
private: private:
struct FreeRange {
vk::DeviceSize Offset;
vk::DeviceSize Size;
};
struct Block {
vk::DeviceMemory Memory;
vk::DeviceSize Size;
vk::DeviceSize Used = 0; // bump 分配用
void* MappedPtr = nullptr;
bool IsMapped = false;
std::vector<FreeRange> FreeList; // 已释放的区域
};
Device& m_Device; Device& m_Device;
std::array<std::vector<Block>, VK_MAX_MEMORY_TYPES> m_Blocks; VmaAllocator_T* m_Vma = nullptr;
std::array<vk::DeviceSize, VK_MAX_MEMORY_TYPES> m_TotalAllocated{};
vk::DeviceSize GetBlockSize(vk::MemoryPropertyFlags props) const;
}; };
} // namespace Engine } // namespace Engine
+6
View File
@@ -28,6 +28,11 @@ namespace Engine
vk::SwapchainKHR Get() const { return m_Swapchain; } vk::SwapchainKHR Get() const { return m_Swapchain; }
vk::Format GetFormat() const { return m_Format; } vk::Format GetFormat() const { return m_Format; }
vk::Extent2D GetExtent() const { return m_Extent; } vk::Extent2D GetExtent() const { return m_Extent; }
// 垂直同步开关:影响 present mode(Mailbox ↔ Immediate),
// 切换后需 Recreate 才生效
void SetVSync(bool vsync) { m_VSync = vsync; }
bool GetVSync() const { return m_VSync; }
uint32_t GetImageCount() const { return static_cast<u32>(m_Images.size()); } uint32_t GetImageCount() const { return static_cast<u32>(m_Images.size()); }
const std::vector<vk::Image>& GetImages() const { return m_Images; } const std::vector<vk::Image>& GetImages() const { return m_Images; }
@@ -56,6 +61,7 @@ namespace Engine
vk::Format m_Format; vk::Format m_Format;
vk::Extent2D m_Extent; vk::Extent2D m_Extent;
bool m_VSync = true;
}; };
} // namespace Engine } // namespace Engine
+17 -7
View File
@@ -21,13 +21,16 @@ namespace Engine {
class Texture { class Texture {
public: public:
// 从磁盘图片加载 // 从磁盘图片加载(按扩展名分发:.dds → DdsLoader,其它 → stbi)。
Texture(Device& device, MemoryAllocator& allocator, const std::string& path); // srgb:颜色贴图语义(BaseColor/Emissive)为 true;数据图
//(Normal/Specular 等)为 false —— 决定 vk 格式的 _SRGB 变体
Texture(Device& device, MemoryAllocator& allocator, const std::string& path,
bool srgb = true);
// 程序化纹理:纯色(1x1),用于地面/调试等不依赖磁盘资产的场景 // 程序化纹理:纯色(1x1),用于地面/调试等不依赖磁盘资产的场景
struct ProceduralTag {}; struct ProceduralTag {};
static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator, static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b); u8 r, u8 g, u8 b, bool srgb = true);
~Texture(); ~Texture();
@@ -38,21 +41,28 @@ namespace Engine {
vk::ImageView GetView() const { return m_View; } vk::ImageView GetView() const { return m_View; }
u32 GetWidth() const { return m_Width; } u32 GetWidth() const { return m_Width; }
u32 GetHeight() const { return m_Height; } u32 GetHeight() const { return m_Height; }
vk::Format GetFormat() const { return m_Format; }
u32 GetMipLevels() const { return m_MipLevels; }
vk::Sampler GetSampler() const { return m_Sampler; } vk::Sampler GetSampler() const { return m_Sampler; }
private: private:
// 两段式构造:先确定像素来源(磁盘 / 程序化),再走公共上传路径 // 两段式构造:先确定像素来源(磁盘 / 程序化),再走公共上传路径
Texture(Device& device, MemoryAllocator& allocator, ProceduralTag, Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
const std::array<u8, 4>& rgba); const std::array<u8, 4>& rgba, bool srgb);
// 公共路径:staging → device local image → view/sampler // 公共路径:staging → device local image → view/sampler(RGBA8 单 mip)
void InitFromPixels(const void* pixels, u32 width, u32 height); void InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb);
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
void InitFromDds(const std::string& path, bool srgb);
Device& m_Device; Device& m_Device;
vk::Image m_Image; vk::Image m_Image;
vk::ImageView m_View; vk::ImageView m_View;
vk::DeviceMemory m_Memory;
vk::Format m_Format = vk::Format::eUndefined;
u32 m_MipLevels = 1;
vk::Sampler m_Sampler; vk::Sampler m_Sampler;
+4 -1
View File
@@ -25,8 +25,11 @@ namespace Engine {
// 接口已按"写入哪个槽位"设计,届时只是内部实现替换。 // 接口已按"写入哪个槽位"设计,届时只是内部实现替换。
class UniformBuffer { class UniformBuffer {
public: public:
// usage:默认 eUniformBuffer;GPUDriven 实例数据传 eStorageBuffer
//(ring 化的正确性保证对所有 per-frame 数据一致)
UniformBuffer(Device& device, MemoryAllocator& allocator, UniformBuffer(Device& device, MemoryAllocator& allocator,
u64 size, u32 frameCount); u64 size, u32 frameCount,
vk::BufferUsageFlags usage = vk::BufferUsageFlagBits::eUniformBuffer);
~UniformBuffer() = default; ~UniformBuffer() = default;
UniformBuffer(const UniformBuffer&) = delete; UniformBuffer(const UniformBuffer&) = delete;
+9 -19
View File
@@ -3,41 +3,31 @@
// //
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
namespace Engine::Assets { namespace Engine::Assets {
AssetManager::AssetManager(Device& device, MemoryAllocator& allocator, AssetManager::AssetManager(IAssetUploader& uploader)
DescriptorManager& descriptors, : m_Uploader(uploader)
vk::DescriptorSetLayout textureLayout)
: m_Device(device), m_Allocator(allocator),
m_Descriptors(descriptors), m_TextureLayout(textureLayout)
{ {
ENGINE_INFO("AssetManager created"); ENGINE_INFO("AssetManager created");
} }
ModelHandle AssetManager::LoadModel(const std::string& path) ModelRef AssetManager::LoadModel(const std::string& path)
{ {
return m_Models.Load(path, [this](const std::string& p) -> Scope<Model> { // GPU 上传经接口发生在渲染层实现里,本层只管缓存与句柄
// GPU 上传在此发生:资产层调用渲染层接口完成 buffer/image 创建 return m_Models.Load(path,
return CreateScope<Model>( [this](const std::string& p) { return m_Uploader.LoadModel(p); });
m_Device, m_Allocator, m_Descriptors, m_TextureLayout, p);
});
} }
ModelHandle AssetManager::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) ModelRef AssetManager::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
{ {
// 程序化资产复用同一缓存:键 = 伪路径 + 参数 // 程序化资产复用同一缓存:键 = 伪路径 + 参数
std::string key = "procedural://plane/" + std::to_string(halfSize) + std::string key = "procedural://plane/" + std::to_string(halfSize) +
"/" + std::to_string(r) + "," + std::to_string(g) + "/" + std::to_string(r) + "," + std::to_string(g) +
"," + std::to_string(b); "," + std::to_string(b);
return m_Models.Load(key, [&](const std::string&) -> Scope<Model> { return m_Models.Load(key,
return Model::CreatePlane(m_Device, m_Allocator, m_Descriptors, [&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); });
m_TextureLayout, halfSize, r, g, b);
});
} }
} // namespace Engine::Assets } // namespace Engine::Assets
+170
View File
@@ -0,0 +1,170 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Assets/DdsLoader.h>
#include <Engine/Core/Log.h>
#include <algorithm>
#include <cstring>
#include <fstream>
namespace Engine::Assets::Dds
{
namespace
{
constexpr u32 kMagic = 0x20534444; // "DDS "
// 头部偏移(dwSpfFourCC @ 84)
constexpr size_t kOffsetHeight = 12;
constexpr size_t kOffsetWidth = 16;
constexpr size_t kOffsetMips = 28;
constexpr size_t kOffsetFourCC = 84;
constexpr size_t kHeaderSize = 128;
constexpr size_t kDx10Size = 20;
u32 ReadU32(const u8* p) { return *reinterpret_cast<const u32*>(p); }
// legacy fourcc → 中性格式
bool MapFourCC(u32 fourcc, DdsFormat& out)
{
switch (fourcc) {
case 0x31545844: out = DdsFormat::BC1; return true; // "DXT1"
case 0x33545844: out = DdsFormat::BC2; return true; // "DXT3"
case 0x35545844: out = DdsFormat::BC3; return true; // "DXT5"
case 0x31495441: out = DdsFormat::BC4; return true; // "ATI1"
case 0x55344342: out = DdsFormat::BC4; return true; // "BC4U"
case 0x32495441: out = DdsFormat::BC5; return true; // "ATI2"
case 0x55354342: out = DdsFormat::BC5; return true; // "BC5U"
default: return false;
}
}
// DX10 DXGI 值 → 中性格式
bool MapDxgi(u32 dxgi, DdsFormat& out)
{
switch (dxgi) {
case 70: case 71: out = DdsFormat::BC1; return true; // BC1
case 73: case 74: out = DdsFormat::BC2; return true; // BC2
case 77: case 78: out = DdsFormat::BC3; return true; // BC3
case 79: case 80: out = DdsFormat::BC4; return true; // BC4
case 82: case 83: out = DdsFormat::BC5; return true; // BC5
case 94: case 95: out = DdsFormat::BC6H; return true; // BC6H
case 97: case 98: out = DdsFormat::BC7; return true; // BC7
case 28: case 29: out = DdsFormat::RGBA8; return true; // R8G8B8A8
case 87: case 91: out = DdsFormat::BGRA8; return true; // B8G8R8A8(近似)
default: return false;
}
}
u32 BytesPerBlock(DdsFormat format)
{
switch (format) {
case DdsFormat::BC1:
case DdsFormat::BC4:
return 8;
case DdsFormat::BC2:
case DdsFormat::BC3:
case DdsFormat::BC5:
case DdsFormat::BC6H:
case DdsFormat::BC7:
return 16;
default:
return 4; // RGBA8/BGRA8 系
}
}
bool IsBlockCompressed(DdsFormat format)
{
return format != DdsFormat::RGBA8 && format != DdsFormat::BGRA8;
}
}
bool Load(const std::string& path, DdsData& out, std::string& errMsg)
{
std::ifstream f(path, std::ios::binary);
if (!f) {
errMsg = "无法打开文件";
return false;
}
u8 header[kHeaderSize];
f.read(reinterpret_cast<char*>(header), kHeaderSize);
if (!f || ReadU32(header) != kMagic) {
errMsg = "DDS 魔数不符";
return false;
}
out.Width = ReadU32(header + kOffsetWidth);
out.Height = ReadU32(header + kOffsetHeight);
const u32 mips = ReadU32(header + kOffsetMips);
out.MipLevels = mips ? mips : 1;
const u32 fourcc = ReadU32(header + kOffsetFourCC);
size_t dataOffset = kHeaderSize;
if (fourcc == 0x30313044) { // "DX10"
u8 dx10[kDx10Size];
f.read(reinterpret_cast<char*>(dx10), kDx10Size);
if (!f) {
errMsg = "DX10 头不完整";
return false;
}
const u32 dxgi = ReadU32(dx10);
if (!MapDxgi(dxgi, out.Format)) {
errMsg = "DX10 DXGI 格式不支持";
return false;
}
dataOffset += kDx10Size;
} else if (!MapFourCC(fourcc, out.Format)) {
// 未压缩路径:仅支持 32bpp RGBA(DDPF_ALPHAPIXELS(0x1) | DDPF_RGB(0x40))
const u32 ddspfFlags = ReadU32(header + 80);
if (ddspfFlags & 0x41) {
out.Format = DdsFormat::BGRA8; // 常见 32bpp DDS 是 B8G8R8A8 字节序
ENGINE_WARN("DDS: 未压缩 32bpp 按 BGRA8 上传 '{}'", path);
} else {
errMsg = "fourcc 不支持";
return false;
}
}
out.BlockCompressed = IsBlockCompressed(out.Format);
f.seekg(dataOffset);
// 数据量校验 + 逐级读取(紧凑排布)
const u32 bpb = BytesPerBlock(out.Format);
const u32 cols = out.BlockCompressed ? 4 : 1;
out.Mips.resize(out.MipLevels);
u32 w = out.Width, h = out.Height;
for (u32 mip = 0; mip < out.MipLevels; ++mip) {
const u32 blocksW = std::max(1u, (w + cols - 1) / cols);
const u32 blocksH = std::max(1u, (h + cols - 1) / cols);
const size_t bytes = size_t(blocksW) * blocksH * bpb;
out.Mips[mip].resize(bytes);
f.read(reinterpret_cast<char*>(out.Mips[mip].data()),
std::streamsize(bytes));
if (!f) {
errMsg = "mip 数据不完整";
return false;
}
w = std::max(1u, w / 2);
h = std::max(1u, h / 2);
}
return true;
}
u32 RowPitch(const DdsData& data, u32 mipLevel)
{
u32 w = data.Width;
for (u32 m = 0; m < mipLevel; ++m) {
w = std::max(1u, w / 2);
}
const u32 bpb = BytesPerBlock(data.Format);
if (data.BlockCompressed) {
return std::max(1u, (w + 3) / 4) * bpb;
}
return w * bpb;
}
} // namespace Engine::Assets::Dds
+45 -14
View File
@@ -13,7 +13,10 @@
#include <imgui.h> #include <imgui.h>
#include <tracy/Tracy.hpp>
#include <algorithm> #include <algorithm>
#include <thread>
namespace Engine { namespace Engine {
@@ -29,13 +32,10 @@ namespace Engine {
m_Scene = CreateScope<ECS::Scene>(); m_Scene = CreateScope<ECS::Scene>();
m_Renderer = CreateScope<Renderer>(*m_Window); m_Renderer = CreateScope<Renderer>(*m_Window);
// 资产层:复用渲染器的 GPU 上下文; // 资产层:上传服务由渲染层提供(IAssetUploader 实现),
// 资产层不感知 Vulkan 具体类。
// 声明在 Renderer 之后 → 析构先于 Renderer(它引用其子模块) // 声明在 Renderer 之后 → 析构先于 Renderer(它引用其子模块)
m_Assets = CreateScope<Assets::AssetManager>( m_Assets = CreateScope<Assets::AssetManager>(m_Renderer->GetAssetUploader());
m_Renderer->GetDevice(),
m_Renderer->GetAllocator(),
m_Renderer->GetDescriptorManager(),
m_Renderer->GetTextureSetLayout());
} }
Application::~Application() Application::~Application()
@@ -67,6 +67,13 @@ namespace Engine {
[] { return ImGui::GetIO().WantCaptureMouse; }); [] { return ImGui::GetIO().WantCaptureMouse; });
} }
void Application::SetVSync(bool vsync)
{
if (m_VSync == vsync) return;
m_VSync = vsync;
m_Renderer->SetVSync(vsync);
}
void Application::Run() void Application::Run()
{ {
ENGINE_INFO("Application starting"); ENGINE_INFO("Application starting");
@@ -74,9 +81,13 @@ namespace Engine {
m_Running = true; m_Running = true;
m_LastFrameTime = std::chrono::steady_clock::now(); m_LastFrameTime = std::chrono::steady_clock::now();
tracy::SetThreadName("Main"); // 线程命名:否则 GUI 显示 Unknown
OnInit(); OnInit();
while (m_Running && !m_Window->ShouldClose()) { while (m_Running) {
const auto frameStart = std::chrono::steady_clock::now();
m_Window->PollEvents(); m_Window->PollEvents();
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
@@ -89,23 +100,44 @@ namespace Engine {
// 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变 // 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变
m_Scene->FlushDestroyQueue(); m_Scene->FlushDestroyQueue();
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜
const auto uiStart = std::chrono::steady_clock::now();
// ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ---- // ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ----
if (m_ImGuiLayer) { if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled())
{
m_ImGuiLayer->NewFrame(); m_ImGuiLayer->NewFrame();
} OnRenderOverlay();
OnRenderOverlay();
if (m_ImGuiLayer) {
m_ImGuiLayer->BuildDrawData(); m_ImGuiLayer->BuildDrawData();
} }
const auto uiEnd = std::chrono::steady_clock::now();
// ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) → // ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) →
// Present,draw data 在 ImGui pass 里才真正提交 GPU ---- // Present,draw data 在 ImGui pass 里才真正提交 GPU ----
m_Renderer->Render(*m_Scene, *m_Assets); m_Renderer->Render(*m_Scene, *m_Assets);
// ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ----
// 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU
if (!m_VSync) {
const auto deadline = frameStart +
std::chrono::microseconds(static_cast<long long>(
1e6 / m_TargetFps));
auto now2 = std::chrono::steady_clock::now();
if (now2 < deadline) {
const auto remaining = std::chrono::duration_cast<
std::chrono::milliseconds>(deadline - now2);
if (remaining.count() > 2) {
std::this_thread::sleep_for(
remaining - std::chrono::milliseconds(2));
}
while (std::chrono::steady_clock::now() < deadline) {
// 自旋等待最后 ~2ms
}
}
}
++m_FrameCount; ++m_FrameCount;
FrameMark; // Tracy:帧边界(一帧的时间线切分)
} }
OnShutdown(); OnShutdown();
@@ -133,7 +165,6 @@ namespace Engine {
void Application::OnWindowClose(const Event& event) void Application::OnWindowClose(const Event& event)
{ {
// 默认行为:直接退出。需要"退出确认"的程序覆写本钩子。
Close(); Close();
} }
+20 -5
View File
@@ -26,7 +26,7 @@ namespace Engine
void Window::KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) void Window::KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{ {
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window)); const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback || key == GLFW_KEY_UNKNOWN) return; if (!self->m_EventCallback || key == GLFW_KEY_UNKNOWN) return;
// UI 捕获键盘时,游戏侧不收键盘事件(ImGui 后端链式回调自行处理) // UI 捕获键盘时,游戏侧不收键盘事件(ImGui 后端链式回调自行处理)
@@ -42,7 +42,7 @@ namespace Engine
void Window::MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) void Window::MouseButtonCallback(GLFWwindow* window, int button, int action, int mods)
{ {
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window)); const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return; if (!self->m_EventCallback) return;
if (self->m_MouseCapture && self->m_MouseCapture()) return; if (self->m_MouseCapture && self->m_MouseCapture()) return;
@@ -55,7 +55,7 @@ namespace Engine
void Window::CursorPosCallback(GLFWwindow* window, double xpos, double ypos) void Window::CursorPosCallback(GLFWwindow* window, double xpos, double ypos)
{ {
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window)); const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return; if (!self->m_EventCallback) return;
if (self->m_MouseCapture && self->m_MouseCapture()) return; if (self->m_MouseCapture && self->m_MouseCapture()) return;
@@ -68,7 +68,7 @@ namespace Engine
void Window::ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) void Window::ScrollCallback(GLFWwindow* window, double xoffset, double yoffset)
{ {
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window)); const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return; if (!self->m_EventCallback) return;
if (self->m_MouseCapture && self->m_MouseCapture()) return; if (self->m_MouseCapture && self->m_MouseCapture()) return;
@@ -81,7 +81,7 @@ namespace Engine
void Window::WindowFocusCallback(GLFWwindow* window, int focused) void Window::WindowFocusCallback(GLFWwindow* window, int focused)
{ {
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window)); const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return; if (!self->m_EventCallback) return;
Event e; Event e;
@@ -89,6 +89,16 @@ namespace Engine
self->m_EventCallback(e); self->m_EventCallback(e);
} }
void Window::WindowCloseCallback(GLFWwindow* window)
{
const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return;
Event e;
e.type = EventType::WindowClose;
self->m_EventCallback(e);
}
Window::Window(const std::string& title, u32 width, u32 height) Window::Window(const std::string& title, u32 width, u32 height)
: m_Width(width), m_Height(height), m_Title(title) : m_Width(width), m_Height(height), m_Title(title)
{ {
@@ -100,6 +110,8 @@ namespace Engine
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
// glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); // glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
// 设置窗口为半透明
m_Window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr); m_Window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (!m_Window) if (!m_Window)
@@ -108,6 +120,7 @@ namespace Engine
glfwTerminate(); glfwTerminate();
return; return;
} }
glfwSetWindowOpacity(m_Window, 0.5f);
ENGINE_INFO("Window created: {} ({}x{})", title, width, height); ENGINE_INFO("Window created: {} ({}x{})", title, width, height);
@@ -120,6 +133,8 @@ namespace Engine
glfwSetCursorPosCallback(m_Window, CursorPosCallback); glfwSetCursorPosCallback(m_Window, CursorPosCallback);
glfwSetScrollCallback(m_Window, ScrollCallback); glfwSetScrollCallback(m_Window, ScrollCallback);
glfwSetWindowFocusCallback(m_Window, WindowFocusCallback); glfwSetWindowFocusCallback(m_Window, WindowFocusCallback);
glfwSetWindowCloseCallback(m_Window, WindowCloseCallback);
} }
Window::~Window() Window::~Window()
+25 -4
View File
@@ -6,13 +6,16 @@
#include <Engine/ECS/EntityHandle.h> #include <Engine/ECS/EntityHandle.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <tracy/Tracy.hpp>
namespace Engine::ECS { namespace Engine::ECS {
EntityHandle Scene::CreateEntity(std::string name) EntityHandle Scene::CreateEntity(std::string name)
{ {
Entity e = m_Registry.create(); Entity e = m_Registry.create();
// IdComponent 自动挂载:每个实体出生即有稳定 UUID // 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义)
m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()}); m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()});
m_Registry.emplace<TransformComponent>(e);
if (!name.empty()) { if (!name.empty()) {
m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)}); m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)});
} }
@@ -52,12 +55,30 @@ namespace Engine::ECS {
m_PendingDestroy.clear(); m_PendingDestroy.clear();
} }
CameraComponent* Scene::GetPrimaryCamera() void Scene::UpdateTransforms()
{
ZoneScoped;
// 只重算脏的:UE transform-update 阶段模式。
// 静止实体(绝大多数)零成本;层级传播在此扩展
m_Registry.view<TransformComponent>().each([](TransformComponent& t) {
if (t.IsDirty()) t.Compose();
});
}
CameraComponent* Scene::GetPrimaryCamera(TransformComponent** outTransform)
{ {
CameraComponent* found = nullptr; CameraComponent* found = nullptr;
m_Registry.view<CameraComponent>().each([&](Entity, CameraComponent& cc) { Entity foundEntity = entt::null;
if (!found && cc.primary) found = &cc; m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) {
if (!found && cc.primary) {
found = &cc;
foundEntity = e;
}
}); });
if (outTransform) {
*outTransform = (found && foundEntity != entt::null)
? &m_Registry.get<TransformComponent>(foundEntity) : nullptr;
}
return found; return found;
} }
+128 -15
View File
@@ -45,8 +45,7 @@ namespace Engine::RG {
for (auto& img : images) { for (auto& img : images) {
if (!img->valid) continue; if (!img->valid) continue;
vkDevice.destroyImageView(img->view); vkDevice.destroyImageView(img->view);
vkDevice.destroyImage(img->image); m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放
m_Allocator.Free(img->allocation);
} }
} }
m_Pool.clear(); m_Pool.clear();
@@ -131,6 +130,23 @@ namespace Engine::RG {
return *this; return *this;
} }
RenderGraph::PassBuilder& RenderGraph::PassBuilder::ReadHistory(
TextureHandle h, ResourceUse use, vk::ShaderStageFlags stage)
{
m_Graph.CheckMutable();
m_Graph.CheckHandle(h);
auto& entry = m_Graph.m_Textures[h.index];
if (entry.imported) {
// swapchain image 不按槽位持久存在,历史读无意义
throw std::runtime_error(
"RenderGraph: history read of imported resource '" + entry.name +
"' is not supported");
}
entry.hasHistoryUse = true;
m_Graph.m_Passes[m_PassIndex].uses.push_back({h.index, use, stage, true});
return *this;
}
RenderGraph::PassBuilder& RenderGraph::PassBuilder::SideEffect() RenderGraph::PassBuilder& RenderGraph::PassBuilder::SideEffect()
{ {
m_Graph.CheckMutable(); m_Graph.CheckMutable();
@@ -161,6 +177,7 @@ namespace Engine::RG {
for (auto& pass : m_Passes) { for (auto& pass : m_Passes) {
pass.culled = false; pass.culled = false;
pass.barriers.clear(); pass.barriers.clear();
pass.postBarriers.clear();
} }
for (auto& entry : m_Textures) ResolveUsage(entry); for (auto& entry : m_Textures) ResolveUsage(entry);
@@ -266,6 +283,7 @@ namespace Engine::RG {
bool keep = pass.sideEffect; bool keep = pass.sideEffect;
for (const auto& use : pass.uses) { for (const auto& use : pass.uses) {
// 帧历史读照常参与:它消费了上一帧的产出,生产者必须存活
if (IsWriteUse(use.use) && readByKept[use.resIndex]) { if (IsWriteUse(use.use) && readByKept[use.resIndex]) {
keep = true; keep = true;
} else if (m_Textures[use.resIndex].imported) { } else if (m_Textures[use.resIndex].imported) {
@@ -318,6 +336,11 @@ namespace Engine::RG {
auto& lw = lastWriter[use.resIndex]; auto& lw = lastWriter[use.resIndex];
auto& readers = pendingReaders[use.resIndex]; auto& readers = pendingReaders[use.resIndex];
// 帧历史读不建边:它消费的是上一帧槽位的物理镜像(与本帧
// 任何写操作无资源竞争),跨帧依赖由同队列提交序 + 历史
// barrier(ALL_COMMANDS 可见性同步)保证
if (use.history) continue;
if (IsWriteUse(use.use)) { if (IsWriteUse(use.use)) {
for (u32 r : readers) addEdge(r, i); // WAR for (u32 r : readers) addEdge(r, i); // WAR
readers.clear(); readers.clear();
@@ -364,6 +387,7 @@ namespace Engine::RG {
for (u32 pos = 0; pos < m_ExecutionOrder.size(); ++pos) { for (u32 pos = 0; pos < m_ExecutionOrder.size(); ++pos) {
const u32 passIdx = m_ExecutionOrder[pos]; const u32 passIdx = m_ExecutionOrder[pos];
for (const auto& use : m_Passes[passIdx].uses) { for (const auto& use : m_Passes[passIdx].uses) {
if (use.history) continue; // 帧历史读不改变本帧状态链与生命周期
auto& entry = m_Textures[use.resIndex]; auto& entry = m_Textures[use.resIndex];
if (entry.firstPass < 0) entry.firstPass = static_cast<i32>(pos); if (entry.firstPass < 0) entry.firstPass = static_cast<i32>(pos);
entry.lastPass = static_cast<i32>(pos); entry.lastPass = static_cast<i32>(pos);
@@ -388,6 +412,7 @@ namespace Engine::RG {
// ---- Pass A:状态链推演 ---- // ---- Pass A:状态链推演 ----
for (const u32 passIdx : m_ExecutionOrder) { for (const u32 passIdx : m_ExecutionOrder) {
for (const auto& use : m_Passes[passIdx].uses) { for (const auto& use : m_Passes[passIdx].uses) {
if (use.history) continue; // 历史读操作另一槽位的镜像,不入本帧链
auto& entry = m_Textures[use.resIndex]; auto& entry = m_Textures[use.resIndex];
entry.chainLayout = UseLayout(use.use); entry.chainLayout = UseLayout(use.use);
entry.chainStage = UseDstStage(use); entry.chainStage = UseDstStage(use);
@@ -417,6 +442,50 @@ namespace Engine::RG {
for (const auto& use : pass.uses) { for (const auto& use : pass.uses) {
auto& entry = m_Textures[use.resIndex]; auto& entry = m_Textures[use.resIndex];
// ---- 帧历史读:操作上一帧槽位的物理镜像,不进入本帧状态链。
// first = 该槽位镜像从未用过(图刚编译/重编译),old=Undefined
// steady = 跨帧可见性同步:src=ALL_COMMANDS|MEMORY_WRITE
// 覆盖上一帧提交对该镜像的全部写入(同队列提交序
// 保证 barrier 执行时上一帧命令已全部完成,
// ALL_COMMANDS 把可用性+可见性显式闭合),
// old/new = resting layout(历史镜像静止在读布局)
if (use.history) {
const vk::ImageLayout desiredLayout = UseLayout(use.use);
const vk::PipelineStageFlags2 dstStage = UseDstStage(use);
const vk::AccessFlags2 dstAccess = UseDstAccess(use);
vk::ImageMemoryBarrier2 first;
first.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
.setDstStageMask(dstStage)
.setDstAccessMask(dstAccess)
.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(desiredLayout)
.setSubresourceRange({entry.aspect, 0, 1, 0, 1});
vk::ImageMemoryBarrier2 steady = first;
steady.setSrcStageMask(vk::PipelineStageFlagBits2::eAllCommands)
.setSrcAccessMask(vk::AccessFlagBits2::eMemoryWrite)
.setOldLayout(entry.restingLayout);
pass.barriers.push_back({use.resIndex, steady, first, true});
// 出口 barrier:采样完成后把历史镜像转回 resting layout。
// (若 desired == resting 则是无害的空转换,但保留它以
// 统一语义——src scope 同时给出读取完成的内存序)
vk::ImageMemoryBarrier2 exit;
exit.setSrcStageMask(dstStage)
.setSrcAccessMask(dstAccess)
.setDstStageMask(vk::PipelineStageFlagBits2::eNone)
.setDstAccessMask(vk::AccessFlagBits2::eNone)
.setOldLayout(desiredLayout)
.setNewLayout(entry.restingLayout)
.setSubresourceRange({entry.aspect, 0, 1, 0, 1});
pass.postBarriers.push_back({use.resIndex, exit, exit, true});
continue; // 不推进 chain:本帧链只描述当前槽位镜像
}
const vk::ImageLayout desiredLayout = UseLayout(use.use); const vk::ImageLayout desiredLayout = UseLayout(use.use);
const vk::PipelineStageFlags2 dstStage = UseDstStage(use); const vk::PipelineStageFlags2 dstStage = UseDstStage(use);
const vk::AccessFlags2 dstAccess = UseDstAccess(use); const vk::AccessFlags2 dstAccess = UseDstAccess(use);
@@ -452,6 +521,17 @@ namespace Engine::RG {
steady = makeBarrier(entry.restingLayout, steady = makeBarrier(entry.restingLayout,
entry.restingStage, entry.restingStage,
entry.restingAccess); entry.restingAccess);
if (entry.hasHistoryUse) {
// 历史读打破了"同槽位两次进入之间有 fence 全等保"
// 的前提(上一帧对该槽位镜像的历史读不在本槽位
// fence 的保护范围内),src 升级为 ALL_COMMANDS
// 覆盖跨帧的一切触碰
steady.setSrcStageMask(
vk::PipelineStageFlagBits2::eAllCommands)
.setSrcAccessMask(
vk::AccessFlagBits2::eMemoryWrite |
vk::AccessFlagBits2::eMemoryRead);
}
} }
pass.barriers.push_back({use.resIndex, steady, first}); pass.barriers.push_back({use.resIndex, steady, first});
@@ -482,7 +562,16 @@ namespace Engine::RG {
void RenderGraph::AllocatePhysical() void RenderGraph::AllocatePhysical()
{ {
for (auto& entry : m_Textures) { for (auto& entry : m_Textures) {
if (entry.imported || entry.firstPass < 0) continue; if (entry.imported) continue;
// 帧历史校验:历史资源必须有同帧正常使用(否则既无产出也无镜像)
if (entry.hasHistoryUse && entry.firstPass < 0) {
throw std::runtime_error(
"RenderGraph: history-read resource '" + entry.name +
"' has no same-frame use (nothing to read history of)");
}
if (entry.firstPass < 0) continue;
// 显式尺寸优先;否则 = 目标尺寸 × sizeScale(后处理降采样) // 显式尺寸优先;否则 = 目标尺寸 × sizeScale(后处理降采样)
vk::Extent2D extent{ vk::Extent2D extent{
@@ -530,8 +619,7 @@ namespace Engine::RG {
for (auto& img : it->second) { for (auto& img : it->second) {
if (!img->valid) continue; if (!img->valid) continue;
vkDevice.destroyImageView(img->view); vkDevice.destroyImageView(img->view);
vkDevice.destroyImage(img->image); m_Allocator.Destroy(img->allocation);
m_Allocator.Free(img->allocation);
} }
it = m_Pool.erase(it); it = m_Pool.erase(it);
} else { } else {
@@ -576,11 +664,10 @@ namespace Engine::RG {
.setSharingMode(vk::SharingMode::eExclusive) .setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined); .setInitialLayout(vk::ImageLayout::eUndefined);
img->image = vkDevice.createImage(imageInfo); // VMA:创建 + 分配 + 绑定一步完成
img->allocation = m_Allocator.CreateImage(imageInfo,
auto reqs = vkDevice.getImageMemoryRequirements(img->image); vk::MemoryPropertyFlagBits::eDeviceLocal);
img->allocation = m_Allocator.Allocate(reqs, vk::MemoryPropertyFlagBits::eDeviceLocal); img->image = img->allocation.Image;
vkDevice.bindImageMemory(img->image, img->allocation.Memory, img->allocation.Offset);
vk::ImageViewCreateInfo viewInfo; vk::ImageViewCreateInfo viewInfo;
viewInfo.setImage(img->image) viewInfo.setImage(img->image)
@@ -652,21 +739,27 @@ namespace Engine::RG {
for (const auto& bp : pass.barriers) { for (const auto& bp : pass.barriers) {
auto& entry = m_Textures[bp.resIndex]; auto& entry = m_Textures[bp.resIndex];
u32 imageSlot = frame.frameSlot;
bool useFirst; bool useFirst;
if (entry.imported) { if (entry.imported) {
useFirst = true; // 恒为 Undefined 起点 useFirst = true; // 恒为 Undefined 起点
} else if (bp.history) {
// 帧历史:操作上一帧槽位的物理镜像。
// used 标记持久(跨帧),首帧走 first,之后走 steady
imageSlot = HistorySlot(frame.frameSlot);
useFirst = !entry.slots[imageSlot].used;
entry.slots[imageSlot].used = true;
} else { } else {
auto& slot = entry.slots[frame.frameSlot]; useFirst = !entry.slots[imageSlot].used;
useFirst = !slot.used; entry.slots[imageSlot].used = true;
slot.used = true;
} }
vk::ImageMemoryBarrier2 b = useFirst ? bp.first : bp.steady; vk::ImageMemoryBarrier2 b = useFirst ? bp.first : bp.steady;
// image 句柄在执行期绑定:imported 用当帧 acquire 的 image, // image 句柄在执行期绑定:imported 用当帧 acquire 的 image,
// created 用当前帧槽位的物理镜像(每个槽位各有一份) // created 用当前帧槽位(历史 barrier 用上一帧槽位)的物理镜像
b.setImage(entry.imported b.setImage(entry.imported
? entry.importedImage ? entry.importedImage
: entry.physical[frame.frameSlot]->image); : entry.physical[imageSlot]->image);
m_ScratchBarriers.push_back(b); m_ScratchBarriers.push_back(b);
} }
@@ -685,6 +778,21 @@ namespace Engine::RG {
pass.exec(ctx); pass.exec(ctx);
} }
// 历史读出口:恢复上一帧槽位镜像的 resting layout
if (!pass.postBarriers.empty()) {
m_ScratchBarriers.clear();
for (const auto& bp : pass.postBarriers) {
auto& entry = m_Textures[bp.resIndex];
const u32 histSlot = HistorySlot(frame.frameSlot);
vk::ImageMemoryBarrier2 b = bp.steady;
b.setImage(entry.physical[histSlot]->image);
m_ScratchBarriers.push_back(b);
}
vk::DependencyInfo dep;
dep.setImageMemoryBarriers(m_ScratchBarriers);
frame.cmd.pipelineBarrier2(dep);
}
if (m_QueryPool) { if (m_QueryPool) {
frame.cmd.writeTimestamp2( frame.cmd.writeTimestamp2(
vk::PipelineStageFlagBits2::eBottomOfPipe, vk::PipelineStageFlagBits2::eBottomOfPipe,
@@ -710,6 +818,11 @@ namespace Engine::RG {
return entry.physical[frame.frameSlot]->image; return entry.physical[frame.frameSlot]->image;
} }
u32 RenderGraphContext::HistorySlot(u32 frameSlot) const
{
return graph->HistorySlot(frameSlot);
}
// ---------- 工具 / 统计 ---------- // ---------- 工具 / 统计 ----------
TextureEntry& RenderGraph::GetEntry(TextureHandle h) TextureEntry& RenderGraph::GetEntry(TextureHandle h)
+27 -25
View File
@@ -4,6 +4,7 @@
#include <Engine/Renderer/Camera.h> #include <Engine/Renderer/Camera.h>
#include <Engine/Core/Window.h> #include <Engine/Core/Window.h>
#include <Engine/ECS/Components.h>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
@@ -11,9 +12,9 @@
namespace Engine { namespace Engine {
void Camera::Update(Window& window, f32 deltaTime) void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime)
{ {
// 鼠标旋转(需显式启用,配合 cursor capture 使用) // 鼠标旋转:直接写实体旋转(yaw → rotation.y,pitch → rotation.x)
if (m_MouseLookEnabled) if (m_MouseLookEnabled)
{ {
f32 mouseDX = 0, mouseDY = 0; f32 mouseDX = 0, mouseDY = 0;
@@ -21,37 +22,38 @@ namespace Engine {
if (mouseDX != 0 || mouseDY != 0) if (mouseDX != 0 || mouseDY != 0)
{ {
m_Yaw += mouseDX * m_MouseSensitivity; glm::vec3 rot = t.GetRotation();
m_Pitch -= mouseDY * m_MouseSensitivity; rot.y += mouseDX * m_MouseSensitivity;
m_Pitch = glm::clamp(m_Pitch, -89.0f, 89.0f); rot.x = glm::clamp(
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
t.SetRotation(rot);
} }
} }
UpdateBasis(); // 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
const glm::vec3& rot = t.GetRotation();
const glm::mat3 r = static_cast<glm::mat3>(
glm::rotate(glm::mat4(1.0f), glm::radians(rot.y),
glm::vec3(0, 1, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(rot.x),
glm::vec3(1, 0, 0)));
const glm::vec3 forward = r * glm::vec3(0, 0, -1);
const glm::vec3 right = r * glm::vec3(1, 0, 0);
f32 speed = m_MoveSpeed * deltaTime; f32 speed = m_MoveSpeed * deltaTime;
if (window.IsKeyPressed(GLFW_KEY_W)) m_Position += m_Forward * speed; if (window.IsKeyPressed(GLFW_KEY_W)) { t.SetPosition(t.GetPosition() + forward * speed); }
if (window.IsKeyPressed(GLFW_KEY_S)) m_Position -= m_Forward * speed; if (window.IsKeyPressed(GLFW_KEY_S)) { t.SetPosition(t.GetPosition() - forward * speed); }
if (window.IsKeyPressed(GLFW_KEY_A)) m_Position -= m_Right * speed; if (window.IsKeyPressed(GLFW_KEY_A)) { t.SetPosition(t.GetPosition() - right * speed); }
if (window.IsKeyPressed(GLFW_KEY_D)) m_Position += m_Right * speed; if (window.IsKeyPressed(GLFW_KEY_D)) { t.SetPosition(t.GetPosition() + right * speed); }
if (window.IsKeyPressed(GLFW_KEY_SPACE)) m_Position.y += speed; if (window.IsKeyPressed(GLFW_KEY_SPACE)) { t.SetPosition(t.GetPosition() + glm::vec3(0, speed, 0)); }
if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) m_Position.y -= speed; if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { t.SetPosition(t.GetPosition() - glm::vec3(0, speed, 0)); }
} }
void Camera::UpdateBasis() glm::mat4 Camera::GetView(const ECS::TransformComponent& t) const
{ {
m_Forward.x = cosf(glm::radians(m_Yaw)) * cosf(glm::radians(m_Pitch)); // 相机作为实体:视图 = 世界矩阵的逆(无重复位姿状态)
m_Forward.y = sinf(glm::radians(m_Pitch)); return glm::inverse(t.World());
m_Forward.z = sinf(glm::radians(m_Yaw)) * cosf(glm::radians(m_Pitch));
m_Forward = glm::normalize(m_Forward);
m_Right = glm::normalize(glm::cross(m_Forward, glm::vec3(0, 1, 0)));
m_Up = glm::normalize(glm::cross(m_Right, m_Forward));
}
glm::mat4 Camera::GetView() const
{
return glm::lookAt(m_Position, m_Position + m_Forward, m_Up);
} }
glm::mat4 Camera::GetProjection(f32 aspect, f32 nearZ, f32 farZ) const glm::mat4 Camera::GetProjection(f32 aspect, f32 nearZ, f32 farZ) const
+20 -8
View File
@@ -12,19 +12,31 @@
namespace Engine namespace Engine
{ {
Material::Material(Device& device, Material::Material(Device& device,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout, vk::DescriptorSetLayout layout,
Texture* texture) const std::array<Texture*, kSlotCount>& textures)
: m_Texture(texture) : m_Textures(textures)
{ {
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout); m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
descriptorManager.UpdateTexture( for (u32 slot = 0; slot < kSlotCount; ++slot) {
m_DescriptorSet, 0, descriptorManager.UpdateTexture(
m_Texture->GetSampler(), m_DescriptorSet, slot,
m_Texture->GetView()); m_Textures[slot]->GetSampler(),
m_Textures[slot]->GetView());
}
} }
Material::~Material() = default; Material::~Material() = default;
std::string Material::MakeCacheKey(const std::string paths[kSlotCount])
{
std::string key;
for (u32 slot = 0; slot < kSlotCount; ++slot) {
if (slot) key += '\n';
key += paths[slot];
}
return key;
}
} // namespace Engine } // namespace Engine
+22 -4
View File
@@ -9,6 +9,8 @@
#include <Engine/Vulkan/Buffer.h> #include <Engine/Vulkan/Buffer.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <limits>
#include "Engine/Core/Stats.h" #include "Engine/Core/Stats.h"
namespace Engine namespace Engine
@@ -16,8 +18,8 @@ namespace Engine
Mesh::Mesh(Device& device, MemoryAllocator& allocator, Mesh::Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<u32>& indices, const std::vector<u32>& indices,
Scope<Material> material) Material* material)
: m_Material(std::move(material)) : m_Material(material)
{ {
if (vertices.empty() || indices.empty()) if (vertices.empty() || indices.empty())
{ {
@@ -43,6 +45,22 @@ namespace Engine
m_IndexCount = static_cast<u32>(indices.size()); m_IndexCount = static_cast<u32>(indices.size());
// 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用)
{
glm::vec3 mn(std::numeric_limits<f32>::max());
glm::vec3 mx(std::numeric_limits<f32>::lowest());
for (const auto& v : vertices) {
mn = glm::min(mn, v.position);
mx = glm::max(mx, v.position);
}
m_Bounds.center = (mn + mx) * 0.5f;
m_Bounds.radius = 0.0f;
for (const auto& v : vertices) {
m_Bounds.radius = glm::max(m_Bounds.radius,
glm::length(v.position - m_Bounds.center));
}
}
ENGINE_INFO("Mesh created: {} vertices, {} indices", vertices.size(), indices.size()); ENGINE_INFO("Mesh created: {} vertices, {} indices", vertices.size(), indices.size());
m_VertexBytes = sizeof(Vertex) * vertices.size(); m_VertexBytes = sizeof(Vertex) * vertices.size();
@@ -55,10 +73,10 @@ namespace Engine
STATS.RemoveMesh(m_VertexBytes, m_IndexBytes); STATS.RemoveMesh(m_VertexBytes, m_IndexBytes);
} }
void Mesh::Draw(vk::CommandBuffer cmd) const void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance) const
{ {
cmd.bindVertexBuffers(0, m_VertexBuffer->Get(), {0}); cmd.bindVertexBuffers(0, m_VertexBuffer->Get(), {0});
cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32); cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32);
cmd.drawIndexed(m_IndexCount, 1, 0, 0, 0); cmd.drawIndexed(m_IndexCount, instanceCount, 0, 0, firstInstance);
} }
} // namespace Engine } // namespace Engine
+90 -30
View File
@@ -15,6 +15,7 @@
#include <assimp/postprocess.h> #include <assimp/postprocess.h>
#include <filesystem> #include <filesystem>
#include <algorithm>
namespace Engine { namespace Engine {
@@ -27,15 +28,27 @@ Model::Model(Device& device, MemoryAllocator& allocator,
, m_DescriptorManager(descriptorManager) , m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout) , m_TextureLayout(textureLayout)
{ {
// 兜底纹理 InitDefaultTextures();
m_DefaultTexture = CreateScope<Texture>(
m_Device, m_Allocator, "assets/textures/default.tga");
LoadModel(path); LoadModel(path);
} }
Model::~Model() = default; Model::~Model() = default;
void Model::InitDefaultTextures()
{
// 各材质槽位的兜底纹理(1x1,开销可忽略):
// albedo 白(受光) / normal 平法线 (0.5,0.5,1) / specular 黑 / emissive 黑
m_DefaultTextures[Material::Albedo] =
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255);
m_DefaultTextures[Material::Normal] =
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 255, false);
m_DefaultTextures[Material::Specular] =
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0, false);
m_DefaultTextures[Material::Emissive] =
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
}
Model::Model(Device& device, MemoryAllocator& allocator, Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
@@ -45,7 +58,8 @@ Model::Model(Device& device, MemoryAllocator& allocator,
, m_DescriptorManager(descriptorManager) , m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout) , m_TextureLayout(textureLayout)
{ {
// 纯色 1x1 贴图 // 纯色 1x1 贴图(albedo 兜底)
InitDefaultTextures();
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]); auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]);
// XZ 平面,法线 +Y,从上往下看 CCW 绕序 // XZ 平面,法线 +Y,从上往下看 CCW 绕序
@@ -58,14 +72,12 @@ Model::Model(Device& device, MemoryAllocator& allocator,
}; };
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3}; const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
auto mat = CreateScope<Material>( // 材质/纹理生命周期随模型(缓存持有)
m_Device, m_DescriptorManager, m_TextureLayout, tex.get()); std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths); // 全默认材质
m_Meshes.push_back(CreateScope<Mesh>( m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, std::move(mat))); m_Device, m_Allocator, vertices, indices, mat));
// 贴图生命周期随模型
m_DefaultTexture = std::move(tex);
} }
Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator, Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
@@ -87,6 +99,7 @@ void Model::LoadModel(const std::string& path)
aiProcess_Triangulate | aiProcess_Triangulate |
aiProcess_FlipUVs | aiProcess_FlipUVs |
aiProcess_GenSmoothNormals | aiProcess_GenSmoothNormals |
aiProcess_CalcTangentSpace | // 法线贴图 TBN
aiProcess_JoinIdenticalVertices); aiProcess_JoinIdenticalVertices);
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
@@ -110,9 +123,15 @@ void Model::ProcessNode(aiNode* node, const aiScene* scene)
} }
} }
Texture* Model::GetOrLoadTexture(const std::string& path) Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
{ {
auto it = m_TextureCache.find(path); // FBX/DCC 导出的路径常带反斜杠(Windows 语义),统一成正斜杠
std::string path = rawPath;
std::replace(path.begin(), path.end(), '\\', '/');
// 缓存键带 sRGB 语义:同一路径可能同时被颜色槽与数据槽引用
std::string key = (srgb ? "s:" : "l:") + path;
auto it = m_TextureCache.find(key);
if (it != m_TextureCache.end()) { if (it != m_TextureCache.end()) {
return it->second.get(); return it->second.get();
} }
@@ -120,12 +139,39 @@ Texture* Model::GetOrLoadTexture(const std::string& path)
// 文件不存在则用兜底纹理 // 文件不存在则用兜底纹理
if (!std::filesystem::exists(path)) { if (!std::filesystem::exists(path)) {
ENGINE_WARN("Texture not found, using default: {}", path); ENGINE_WARN("Texture not found, using default: {}", path);
return m_DefaultTexture.get(); return nullptr;
} }
auto tex = CreateScope<Texture>(m_Device, m_Allocator, path); auto tex = CreateScope<Texture>(m_Device, m_Allocator, path, srgb);
Texture* ptr = tex.get(); Texture* ptr = tex.get();
m_TextureCache.emplace(path, std::move(tex)); m_TextureCache.emplace(key, std::move(tex));
return ptr;
}
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount])
{
const std::string key = Material::MakeCacheKey(paths);
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
return it->second.get();
}
// 每槽位解析纹理:空/缺失折到该槽位默认纹理
std::array<Texture*, Material::kSlotCount> textures{};
// 槽位 sRGB 语义:颜色图 true,数据图 false
const bool slotSrgb[Material::kSlotCount] = {true, false, false, true};
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
Texture* tex = nullptr;
if (!paths[slot].empty()) {
tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]);
}
textures[slot] = tex ? tex : m_DefaultTextures[slot].get();
}
auto mat = CreateScope<Material>(
m_Device, m_DescriptorManager, m_TextureLayout, textures);
Material* ptr = mat.get();
m_MaterialCache.emplace(key, std::move(mat));
return ptr; return ptr;
} }
@@ -160,6 +206,15 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
}; };
} }
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
if (mesh->mTangents) {
vertex.tangent = {
mesh->mTangents[i].x,
mesh->mTangents[i].y,
mesh->mTangents[i].z
};
}
vertices.push_back(vertex); vertices.push_back(vertex);
} }
@@ -170,30 +225,35 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
} }
} }
// 读取材质纹理路径 // 读取材质纹理路径(4 槽:albedo/normal/specular/emissive)
std::string texturePath; std::string texPaths[Material::kSlotCount];
if (mesh->mMaterialIndex >= 0) { if (mesh->mMaterialIndex >= 0) {
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
const std::filesystem::path modelDir =
std::filesystem::path(m_ModelPath).parent_path();
aiString path; const std::pair<aiTextureType, u32> slots[] = {
if (material->GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS) { {aiTextureType_DIFFUSE, Material::Albedo},
std::filesystem::path modelDir = {aiTextureType_NORMALS, Material::Normal},
std::filesystem::path(m_ModelPath).parent_path(); {aiTextureType_SPECULAR, Material::Specular},
texturePath = (modelDir / path.C_Str()).string(); {aiTextureType_EMISSIVE, Material::Emissive},
};
for (const auto& [type, slot] : slots) {
aiString path;
if (material->GetTexture(type, 0, &path) == AI_SUCCESS &&
path.length > 0) {
texPaths[slot] = (modelDir / path.C_Str()).string();
}
} }
} }
// 从缓存拿,没有才加载 // 材质缓存:同贴图组合 → 同一 Material(一个描述符集),mesh 数 ≠ 集数
Texture* tex = texturePath.empty() Material* mat = GetOrCreateMaterial(texPaths);
? m_DefaultTexture.get()
: GetOrLoadTexture(texturePath);
auto mat = CreateScope<Material>(
m_Device, m_DescriptorManager, m_TextureLayout, tex);
return CreateScope<Mesh>( return CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, std::move(mat)); m_Device, m_Allocator, vertices, indices, mat);
} }
} // namespace Engine } // namespace Engine
+75
View File
@@ -0,0 +1,75 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/BloomPass.h>
#include <Engine/Core/FrameContext.h>
namespace Engine::Passes
{
BloomPass::BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
RG::TextureHandle hdrColor,
RG::TextureHandle bloomA, RG::TextureHandle bloomB)
: m_Gpu(gpu), m_Params(params),
m_HDR(hdrColor), m_A(bloomA), m_B(bloomB)
{
m_ExtractSets.resize(kMaxFramesInFlight);
m_BlurHSets.resize(kMaxFramesInFlight);
m_BlurVSets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_ExtractSets[slot] = m_Gpu.AllocateSet();
m_BlurHSets[slot] = m_Gpu.AllocateSet();
m_BlurVSets[slot] = m_Gpu.AllocateSet();
}
}
void BloomPass::Register(RG::RenderGraph& rg)
{
// Extract:HDR → 半分辨率 A(软阈值由 frag 的 param0 传入)
rg.AddPass("BloomExtract",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_A), m_Gpu.extractFrag,
m_ExtractSets[ctx.frame.frameSlot],
FullscreenPush{0.0f, m_Params.bloomThreshold, 0.0f, 0.0f});
})
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Write(m_A, RG::ResourceUse::ColorAttachment);
// BlurH:A → B(水平步长经 push 传入)
rg.AddPass("BloomBlurH",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_B), m_Gpu.blurFrag,
m_BlurHSets[ctx.frame.frameSlot],
FullscreenPush{0.0f,
1.0f / ctx.extent.width, 0.0f,
0.0f});
})
.Read(m_A, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Write(m_B, RG::ResourceUse::ColorAttachment);
// BlurV:B → A(垂直步长)
rg.AddPass("BloomBlurV",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_A), m_Gpu.blurFrag,
m_BlurVSets[ctx.frame.frameSlot],
FullscreenPush{0.0f, 0.0f,
1.0f / ctx.extent.height,
0.0f});
})
.Read(m_B, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Write(m_A, RG::ResourceUse::ColorAttachment);
}
void BloomPass::OnTargetsRebuilt(RG::RenderGraph& rg)
{
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Gpu.UpdateSetInput(m_ExtractSets[slot], 0, rg, m_HDR, slot);
m_Gpu.UpdateSetInput(m_BlurHSets[slot], 0, rg, m_A, slot);
m_Gpu.UpdateSetInput(m_BlurVSets[slot], 0, rg, m_B, slot);
}
}
} // namespace Engine::Passes
+157
View File
@@ -0,0 +1,157 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/Material.h>
#include <array>
#include <span>
namespace Engine::Passes
{
void ForwardPass::Register(RG::RenderGraph& rg)
{
rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
vk::ClearValue clearValue;
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
vk::RenderingAttachmentInfo colorAttachments[2];
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity 附件:清 0 = 静止(背景无几何 → 历史原位采样)
vk::ClearValue velocityClear;
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
colorAttachments[1].setImageView(ctx.View(m_Velocity))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_Depth))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
colorAttachments[0], colorAttachments[1]};
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(colorArray)
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
// 绑定 shader object
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {m_Gpu.forwardVert, m_Gpu.forwardFrag};
cmd.bindShadersEXT(stages, shaders);
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 0,
m_Gpu.uboSets[ctx.frame.frameSlot], {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 2,
m_Gpu.shadowSets[ctx.frame.frameSlot], {});
// ---- 动态状态 ----
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_TRUE);
cmd.setDepthWriteEnable(VK_TRUE);
cmd.setDepthCompareOp(vk::CompareOp::eLess);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
cmd.setDepthBiasEnable(VK_FALSE);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
auto binding = Vertex::GetBindingDescription();
auto attributes = Vertex::GetAttributeDescriptions();
cmd.setVertexInputEXT(binding, attributes);
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
vk::Rect2D scissor({0, 0}, ctx.extent);
cmd.setScissorWithCount(scissor);
vk::Bool32 blendEnable = VK_FALSE;
vk::ColorBlendEquationEXT blendEquation;
blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
vk::ColorComponentFlags colorWriteMask =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
// 两个颜色附件(HDR + velocity)都是无混合直写
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{blendEnable, blendEnable});
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{blendEquation, blendEquation});
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{colorWriteMask, colorWriteMask});
// ---- 绘制:GPUDriven 按 mesh 分 batch(相机视锥剔除后)----
// 实例矩阵已由 Renderer 写入 ObjectBuffer(per-slot SSBO);
// 同 mesh 的 N 个可见实体合并为一次 drawIndexed(instanceCount=N),
// draw 数 = 可见唯一 mesh 数,与实体数量解耦
for (const auto& batch : m_Frame.mainView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first);
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_ShadowMaps[1], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_ShadowMaps[2], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
}
} // namespace Engine::Passes
@@ -0,0 +1,193 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Core/Log.h>
#include <array>
namespace Engine::Passes
{
FullscreenGpu::FullscreenGpu(Device& device, DescriptorManager& descriptors,
ShaderManager& shaders)
: m_Device(device), m_Descriptors(descriptors)
{
// Set0 = {HDR 颜色图, Bloom 图, 调试输入},push = 16B 通用结构
//(binding2 只被 tonemap 的调试视图使用;bloom 系 shader 静态
// 使用的 binding 是布局的子集,合法)
std::vector<DescriptorBinding> bindings = {
{0, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
{2, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment},
};
setLayout = descriptors.CreateLayout(bindings);
layout = descriptors.CreatePipelineLayout(
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
sampler = device.GetDevice().createSampler(
vk::SamplerCreateInfo{}
.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear)
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
.setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge));
const std::string shaderDir = "shaders/";
auto vertShader = shaders.CreateFromFile(
shaderDir + "tonemap.vert.spv",
vk::ShaderStageFlagBits::eVertex,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
auto tonemapF = shaders.CreateFromFile(
shaderDir + "tonemap.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
auto extractF = shaders.CreateFromFile(
shaderDir + "bloom_extract.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
auto blurF = shaders.CreateFromFile(
shaderDir + "bloom_blur.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
auto diffF = shaders.CreateFromFile(
shaderDir + "temporal_diff.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
auto taaF = shaders.CreateFromFile(
shaderDir + "taa.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{setLayout},
static_cast<u32>(sizeof(FullscreenPush)));
vert = vertShader.Handle;
tonemapFrag = tonemapF.Handle;
extractFrag = extractF.Handle;
blurFrag = blurF.Handle;
diffFrag = diffF.Handle;
taaFrag = taaF.Handle;
if (!vert || !tonemapFrag || !extractFrag || !blurFrag ||
!diffFrag || !taaFrag) {
ENGINE_FATAL("FullscreenGpu: failed to create shader objects");
}
}
FullscreenGpu::~FullscreenGpu()
{
const auto& vkDevice = m_Device.GetDevice();
if (vert) vkDevice.destroyShaderEXT(vert);
if (tonemapFrag) vkDevice.destroyShaderEXT(tonemapFrag);
if (extractFrag) vkDevice.destroyShaderEXT(extractFrag);
if (blurFrag) vkDevice.destroyShaderEXT(blurFrag);
if (diffFrag) vkDevice.destroyShaderEXT(diffFrag);
if (taaFrag) vkDevice.destroyShaderEXT(taaFrag);
vkDevice.destroySampler(sampler);
vkDevice.destroyPipelineLayout(layout);
vkDevice.destroyDescriptorSetLayout(setLayout);
}
vk::DescriptorSet FullscreenGpu::AllocateSet()
{
return m_Descriptors.AllocateDescriptorSet(setLayout);
}
void FullscreenGpu::UpdateSetInput(vk::DescriptorSet set, u32 binding,
RG::RenderGraph& rg, RG::TextureHandle input,
u32 slot)
{
// 物理镜像按帧槽位各一份:每个槽位的描述符绑各自的 view
m_Descriptors.UpdateTexture(set, binding, sampler,
rg.GetPhysicalView(input, slot));
}
void FullscreenGpu::RecordFullscreen(RG::RenderGraphContext& ctx,
vk::ImageView view,
vk::ShaderEXT frag,
const vk::DescriptorSet& set,
const FullscreenPush& push)
{
const vk::CommandBuffer cmd = ctx.cmd;
vk::ClearValue clearValue;
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{0.0f, 0.0f, 0.0f, 1.0f});
vk::RenderingAttachmentInfo colorAttachment;
colorAttachment.setImageView(view)
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(colorAttachment);
cmd.beginRendering(renderingInfo);
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {vert, frag};
cmd.bindShadersEXT(stages, shaders);
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
layout, 0, set, {});
// ---- 动态状态(无深度附件、无顶点输入)----
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_FALSE);
cmd.setDepthWriteEnable(VK_FALSE);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
cmd.setDepthBiasEnable(VK_FALSE);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
cmd.setVertexInputEXT(
std::array<vk::VertexInputBindingDescription2EXT, 0>{},
std::array<vk::VertexInputAttributeDescription2EXT, 0>{});
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent));
cmd.pushConstants(layout,
vk::ShaderStageFlagBits::eVertex |
vk::ShaderStageFlagBits::eFragment,
0, sizeof(FullscreenPush), &push);
cmd.draw(3, 1, 0, 0);
cmd.endRendering();
}
} // namespace Engine::Passes
+36
View File
@@ -0,0 +1,36 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/ImGuiPass.h>
#include <Engine/ImGui/ImGuiLayer.h>
namespace Engine::Passes
{
void ImGuiPass::Register(RG::RenderGraph& rg)
{
rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
vk::RenderingAttachmentInfo colorAttachment;
colorAttachment.setImageView(ctx.View(m_Backbuffer))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad) // 关键:叠加
.setStoreOp(vk::AttachmentStoreOp::eStore);
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(colorAttachment);
cmd.beginRendering(renderingInfo);
if (*m_Layer) {
(*m_Layer)->RenderDrawData(cmd);
}
cmd.endRendering();
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment);
}
} // namespace Engine::Passes
+157
View File
@@ -0,0 +1,157 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Core/Log.h>
#include <glm/glm.hpp>
namespace Engine::Passes
{
SceneGpu::SceneGpu(Device& device, DescriptorManager& descriptors,
ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer)
: m_Device(device), m_Descriptors(descriptors)
{
// Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取)
// + 上一帧实例矩阵(TAA velocity,vertex 读取)
std::vector<DescriptorBinding> uboBindings = {
{0, vk::DescriptorType::eUniformBuffer,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{1, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
{2, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
};
uboSetLayout = descriptors.CreateLayout(uboBindings);
// Set 1: 材质纹理槽(binding = Material::Slot:0 albedo / 1 normal /
// 2 specular / 3 emissive)
std::vector<DescriptorBinding> texBindings;
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
texBindings.push_back(
{slot, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment});
}
textureSetLayout = descriptors.CreateLayout(texBindings);
// Set 2: CSM 级联阴影图(fragment 采样,binding = 级联序号)
std::vector<DescriptorBinding> shadowBindings;
for (u32 c = 0; c < kMaxCascades; ++c) {
shadowBindings.push_back(
{c, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment});
}
shadowSetLayout = descriptors.CreateLayout(shadowBindings);
// Forward pipeline layout:三个 set;无 push(GPUDriven 经 SSBO)
forwardLayout = descriptors.CreatePipelineLayout(
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// Shadow pipeline layout:Set0 = UBO/ObjectBuffer(实例矩阵),
// push = lightVP 64B
shadowLayout = descriptors.CreatePipelineLayout(
std::vector<vk::DescriptorSetLayout>{uboSetLayout},
sizeof(glm::mat4));
const std::string shaderDir = "shaders/";
// forward vert/frag 均无 push(range 定义一致:都没有,VUID-08878)
auto vert = shaders.CreateFromFile(
shaderDir + "simple_shader.vert.spv",
vk::ShaderStageFlagBits::eVertex,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
auto frag = shaders.CreateFromFile(
shaderDir + "simple_shader.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// shadow shader:depth-only,push = lightVP
auto shadowVertShader = shaders.CreateFromFile(
shaderDir + "shadow.vert.spv",
vk::ShaderStageFlagBits::eVertex,
std::vector<vk::DescriptorSetLayout>{uboSetLayout},
sizeof(glm::mat4));
auto shadowFragShader = shaders.CreateFromFile(
shaderDir + "shadow.frag.spv",
vk::ShaderStageFlagBits::eFragment,
std::vector<vk::DescriptorSetLayout>{uboSetLayout},
sizeof(glm::mat4));
forwardVert = vert.Handle;
forwardFrag = frag.Handle;
shadowVert = shadowVertShader.Handle;
shadowFrag = shadowFragShader.Handle;
if (!forwardVert || !forwardFrag || !shadowVert || !shadowFrag) {
ENGINE_FATAL("SceneGpu: failed to create shader objects");
}
// 阴影采样器:clamp-to-border 白色 = 光视锥之外无阴影
vk::SamplerCreateInfo shadowSamplerInfo;
shadowSamplerInfo.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear)
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
.setAddressModeU(vk::SamplerAddressMode::eClampToBorder)
.setAddressModeV(vk::SamplerAddressMode::eClampToBorder)
.setAddressModeW(vk::SamplerAddressMode::eClampToBorder)
.setBorderColor(vk::BorderColor::eFloatOpaqueWhite)
.setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE);
shadowSampler = device.GetDevice().createSampler(shadowSamplerInfo);
// per-slot 描述符分配 + Set0 绑定(buffer 句柄全程稳定,不用重绑)
uboSets.resize(kMaxFramesInFlight);
shadowSets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
uboSets[slot] = descriptors.AllocateDescriptorSet(uboSetLayout);
descriptors.UpdateUniformBuffer(
uboSets[slot], 0, ubo.Get(slot), ubo.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 1, objectBuffer.Get(slot), objectBuffer.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 2, prevObjectBuffer.Get(slot),
prevObjectBuffer.GetSize());
shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout);
}
// shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成
}
SceneGpu::~SceneGpu()
{
const auto& vkDevice = m_Device.GetDevice();
if (forwardVert) vkDevice.destroyShaderEXT(forwardVert);
if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag);
if (shadowVert) vkDevice.destroyShaderEXT(shadowVert);
if (shadowFrag) vkDevice.destroyShaderEXT(shadowFrag);
vkDevice.destroySampler(shadowSampler);
vkDevice.destroyPipelineLayout(forwardLayout);
vkDevice.destroyPipelineLayout(shadowLayout);
vkDevice.destroyDescriptorSetLayout(uboSetLayout);
vkDevice.destroyDescriptorSetLayout(textureSetLayout);
vkDevice.destroyDescriptorSetLayout(shadowSetLayout);
}
void SceneGpu::BindShadowMaps(RG::RenderGraph& rg,
const std::array<RG::TextureHandle, kMaxCascades>& maps)
{
// 物理镜像按帧槽位各一份:每个槽位的描述符绑各自的 view
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
for (u32 c = 0; c < kMaxCascades; ++c) {
m_Descriptors.UpdateTexture(
shadowSets[slot], c, shadowSampler,
rg.GetPhysicalView(maps[c], slot));
}
}
}
} // namespace Engine::Passes
+112
View File
@@ -0,0 +1,112 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/ShadowPass.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h>
#include <array>
namespace Engine::Passes
{
void ShadowPass::Register(RG::RenderGraph& rg)
{
// 所有渲染体都写入每个级联,不做实体级开关——castShadow 应由
// 材质/实体数据驱动剔除,而非 pass 逻辑特判。
// (每级联独立剔除是后续优化点:shadowView 细分为 per-cascade 列表)
rg.AddPass("Shadow", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
for (u32 c = 0; c < kMaxCascades; ++c) {
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_ShadowMaps[c]))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, {kCascadeRes, kCascadeRes}})
.setLayerCount(1)
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {m_Gpu.shadowVert, m_Gpu.shadowFrag};
cmd.bindShadersEXT(stages, shaders);
// ---- 动态状态(depth-only)----
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_TRUE);
cmd.setDepthWriteEnable(VK_TRUE);
cmd.setDepthCompareOp(vk::CompareOp::eLess);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
// 深度 bias 压平 shadow acne(NDC 量级)
cmd.setDepthBiasEnable(VK_TRUE);
cmd.setDepthBias(0.0025f, 0.0f, 0.003f);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
auto binding = Vertex::GetBindingDescription();
auto attributes = Vertex::GetAttributeDescriptions();
cmd.setVertexInputEXT(binding, attributes);
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(kCascadeRes),
static_cast<float>(kCascadeRes),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
vk::Rect2D scissor({0, 0}, {kCascadeRes, kCascadeRes});
cmd.setScissorWithCount(scissor);
// ---- 绘制:GPUDriven——与 Forward 共用实例数据,但用
// 光源视锥剔除后的 batch(相机外实体仍投影子)----
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.shadowLayout, 0,
m_Gpu.uboSets[ctx.frame.frameSlot], {});
// stage 必须覆盖 layout range 声明的全部 stage(VUID-01796)
// 每级联 push 各自的光空间 VP
cmd.pushConstants(m_Gpu.shadowLayout,
vk::ShaderStageFlagBits::eVertex |
vk::ShaderStageFlagBits::eFragment,
0, sizeof(glm::mat4),
&m_Frame.ubo.lightSpace[c]);
for (const auto& batch : m_Frame.shadowCascades[c].batches) {
batch.mesh->Draw(cmd, batch.count, batch.first);
}
cmd.endRendering();
}
})
.Write(m_ShadowMaps[0], RG::ResourceUse::DepthAttachment)
.Write(m_ShadowMaps[1], RG::ResourceUse::DepthAttachment)
.Write(m_ShadowMaps[2], RG::ResourceUse::DepthAttachment);
}
void ShadowPass::OnTargetsRebuilt(RG::RenderGraph& rg)
{
m_Gpu.BindShadowMaps(rg, m_ShadowMaps);
}
} // namespace Engine::Passes
+51
View File
@@ -0,0 +1,51 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/TAAPass.h>
#include <Engine/Core/FrameContext.h>
namespace Engine::Passes
{
TAAPass::TAAPass(FullscreenGpu& gpu, f32 historyAlpha,
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
RG::TextureHandle taaColor)
: m_Gpu(gpu), m_HistoryAlpha(historyAlpha),
m_HDR(hdrColor), m_Velocity(velocity), m_TAAColor(taaColor)
{
m_Sets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Sets[slot] = m_Gpu.AllocateSet();
}
}
void TAAPass::Register(RG::RenderGraph& rg)
{
rg.AddPass("TAA",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_TAAColor), m_Gpu.taaFrag,
m_Sets[ctx.frame.frameSlot],
FullscreenPush{0.0f, m_HistoryAlpha, 0.0f, 0.0f});
})
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_Velocity, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
// 自引用历史:TAAColor 的上一帧槽位就是累积历史
.ReadHistory(m_TAAColor, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Write(m_TAAColor, RG::ResourceUse::ColorAttachment);
}
void TAAPass::OnTargetsRebuilt(RG::RenderGraph& rg)
{
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR, slot);
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_Velocity, slot);
// binding2 = 历史(上一帧槽位镜像)
m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TAAColor,
rg.HistorySlot(slot));
}
}
} // namespace Engine::Passes
@@ -0,0 +1,49 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/TemporalDiffPass.h>
#include <Engine/Core/FrameContext.h>
namespace Engine::Passes
{
TemporalDiffPass::TemporalDiffPass(FullscreenGpu& gpu, f32 gain,
RG::TextureHandle hdrColor,
RG::TextureHandle output)
: m_Gpu(gpu), m_Gain(gain), m_HDR(hdrColor), m_Output(output)
{
m_Sets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Sets[slot] = m_Gpu.AllocateSet();
}
}
void TemporalDiffPass::Register(RG::RenderGraph& rg)
{
// 帧历史读 + 当前帧读:同一虚拟资源的两个槽位镜像在同一个
// pass 里采样(TAA 的标准形态)。历史读不建图内边,跨帧同步
// 由 RG 的历史 barrier(ALL_COMMANDS 可见性)保证。
rg.AddPass("TemporalDiff",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Output), m_Gpu.diffFrag,
m_Sets[ctx.frame.frameSlot],
FullscreenPush{0.0f, m_Gain, 0.0f, 0.0f});
})
.ReadHistory(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Write(m_Output, RG::ResourceUse::ColorAttachment);
}
void TemporalDiffPass::OnTargetsRebuilt(RG::RenderGraph& rg)
{
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
// binding0 = 帧历史(上一帧槽位镜像),binding1 = 当前帧
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR,
rg.HistorySlot(slot));
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_HDR, slot);
}
}
} // namespace Engine::Passes
@@ -0,0 +1,56 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/Passes/TonemapPass.h>
#include <Engine/Core/FrameContext.h>
namespace Engine::Passes
{
TonemapPass::TonemapPass(FullscreenGpu& gpu, PostProcessParams& params,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff,
RG::TextureHandle backbuffer)
: m_Gpu(gpu), m_Params(params),
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
m_Backbuffer(backbuffer)
{
m_Sets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Sets[slot] = m_Gpu.AllocateSet();
}
}
void TonemapPass::Register(RG::RenderGraph& rg)
{
rg.AddPass("Tonemap",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Backbuffer),
m_Gpu.tonemapFrag,
m_Sets[ctx.frame.frameSlot],
FullscreenPush{m_Params.exposure,
m_Params.useACES ? 1.0f : 0.0f,
m_Params.bloomStrength,
m_Params.showTemporalDiff ? 1.0f : 0.0f});
})
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_A, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
// shader 静态采样 binding2(调试视图开关在 push constant 里),
// 声明读否则本 pass 的产出无人消费会被剔除、镜像也不会分配
.Read(m_TemporalDiff, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
}
void TonemapPass::OnTargetsRebuilt(RG::RenderGraph& rg)
{
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR, slot);
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_A, slot);
m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TemporalDiff, slot);
}
}
} // namespace Engine::Passes
File diff suppressed because it is too large Load Diff
+312
View File
@@ -0,0 +1,312 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/SceneRenderer.h>
#include <Engine/ECS/Scene.h>
#include <Engine/ECS/Components.h>
#include <Engine/Renderer/Camera.h>
#include <Engine/Renderer/Frustum.h>
#include <Engine/Renderer/Model.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Core/Log.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <tracy/Tracy.hpp>
#include <chrono>
#include <limits>
namespace Engine
{
using namespace ECS; // TransformComponent / Entity 等直接使用
namespace
{
// 相机裁剪范围(与 BuildFrame 中 GetProjection 的参数一致)
constexpr f32 kCameraNear = 0.1f;
constexpr f32 kCameraFar = 100.0f;
// 实用分割插值因子(0=均匀,1=对数;0.6 是业界常用值)
constexpr f32 kCascadeLambda = 0.6f;
// Halton 低差异序列(TAA 子像素抖动)
f32 Halton(u32 index, u32 base)
{
f32 f = 1.0f, r = 0.0f;
while (index > 0) {
f /= static_cast<f32>(base);
r += f * static_cast<f32>(index % base);
index /= base;
}
return r;
}
}
void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir)
{
constexpr u32 N = kMaxCascades;
// ---- 1. 实用分割:均匀与对数分割按 λ 插值(视图深度,正值)----
f32 splits[N];
for (u32 i = 1; i <= N; ++i) {
const f32 t = static_cast<f32>(i) / N;
const f32 uniform = kCameraNear + (kCameraFar - kCameraNear) * t;
const f32 log = kCameraNear * glm::pow(kCameraFar / kCameraNear, t);
splits[i - 1] = glm::mix(uniform, log, kCascadeLambda);
}
for (u32 c = 0; c < N; ++c) {
m_Frame.ubo.cascadeSplits[c] = splits[c];
}
// ---- 2. 全视锥 8 角反投影到世界(NDC z: 0=近 1=远,Vulkan [0,1])----
const glm::mat4 invVP = glm::inverse(m_Frame.ubo.projNoJitter * m_Frame.ubo.view);
glm::vec3 nearC[4], farC[4];
const glm::vec2 ndcXY[4] = {{-1,-1},{1,-1},{-1,1},{1,1}};
for (u32 i = 0; i < 4; ++i) {
glm::vec4 p = invVP * glm::vec4(ndcXY[i], 0.0f, 1.0f);
nearC[i] = glm::vec3(p / p.w);
p = invVP * glm::vec4(ndcXY[i], 1.0f, 1.0f);
farC[i] = glm::vec3(p / p.w);
}
// ---- 3. 每级联:切片角点 → 光空间 AABB 拟合 → 纹素对齐 ----
const glm::vec3 up = glm::abs(shadowDir.y) > 0.999f
? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
for (u32 c = 0; c < N; ++c) {
const f32 d0 = (c == 0) ? kCameraNear : splits[c - 1];
const f32 d1 = splits[c];
const f32 t0 = (d0 - kCameraNear) / (kCameraFar - kCameraNear);
const f32 t1 = (d1 - kCameraNear) / (kCameraFar - kCameraNear);
glm::vec3 slice[8];
for (u32 i = 0; i < 4; ++i) {
slice[i] = glm::mix(nearC[i], farC[i], t0);
slice[4 + i] = glm::mix(nearC[i], farC[i], t1);
}
// 光视图:位于切片中心,沿光方向看
glm::vec3 center(0.0f);
for (const auto& p : slice) center += p;
center *= (1.0f / 8.0f);
const glm::mat4 lightView = glm::lookAt(center, center + shadowDir, up);
glm::vec3 mn( std::numeric_limits<f32>::max());
glm::vec3 mx( std::numeric_limits<f32>::lowest());
for (const auto& p : slice) {
const glm::vec3 lv = glm::vec3(lightView * glm::vec4(p, 1.0f));
mn = glm::min(mn, lv);
mx = glm::max(mx, lv);
}
// 沿光方向扩展深度范围:切片盒外的 caster(如高树、城墙)
// 也能投影子进本级联——per-cascade 剔除的正确性前提。
// 只扩 z 不缩 XY,阴影分辨率不受影响
constexpr f32 kCasterPad = 30.0f;
mn.z -= kCasterPad;
mx.z += kCasterPad;
// 纹素对齐:X/Y 边界量化到纹素网格(消除相机移动时的阴影边缘闪烁)
const f32 texelX = (mx.x - mn.x) / static_cast<f32>(kCascadeRes);
const f32 texelY = (mx.y - mn.y) / static_cast<f32>(kCascadeRes);
mn.x = glm::floor(mn.x / texelX) * texelX;
mn.y = glm::floor(mn.y / texelY) * texelY;
mx.x = mn.x + glm::ceil((mx.x - mn.x) / texelX) * texelX;
mx.y = mn.y + glm::ceil((mx.y - mn.y) / texelY) * texelY;
// 光视图空间里 z 为负(看向 -Z);近远距离取正
glm::mat4 lightProj = glm::ortho(mn.x, mx.x, mn.y, mx.y,
-mx.z, -mn.z);
// Vulkan clip 深度 [0,1](ortho 的 w_clip=1,偏移折进 z 平移分量)
lightProj[2][2] *= 0.5f;
lightProj[3][2] = lightProj[3][2] * 0.5f + 0.5f;
m_Frame.ubo.lightSpace[c] = lightProj * lightView;
}
}
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent)
{
ZoneScopedN("BuildFrame");
const auto collectStart = std::chrono::steady_clock::now();
// ---- 0. TAA 时域状态先行(velocity = 上一帧姿态与相机)----
UniformBufferObject& ubo = m_Frame.ubo;
ubo.prevView = m_LastView;
ubo.prevProj = m_LastProjNoJitter;
// ---- 1. 相机(位姿单一事实源 = 相机实体的 Transform)----
TransformComponent* camTransform = nullptr;
auto* cameraComp = scene.GetPrimaryCamera(&camTransform);
if (!cameraComp || !camTransform) {
// 无相机:本帧跳过(正常场景必有相机,安全网)
return false;
}
const Camera& camera = cameraComp->camera;
const f32 aspect = static_cast<f32>(targetExtent.width) /
static_cast<f32>(targetExtent.height);
ubo.view = camera.GetView(*camTransform);
ubo.proj = camera.GetProjection(aspect, 0.1f, 100.0f);
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
ubo.projNoJitter = ubo.proj;
// 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。
// NDC 偏移 = 像素偏移 × 2 / 尺寸
{
const u32 idx = static_cast<u32>(m_FrameIndex % 8);
const f32 jx = Halton(idx + 1, 2) - 0.5f;
const f32 jy = Halton(idx + 1, 3) - 0.5f;
ubo.proj[2][0] += jx * 2.0f / static_cast<f32>(targetExtent.width);
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(targetExtent.height);
}
ubo.ambientColor = scene.GetSettings().ambientColor;
// ---- 2. 灯光(第一个灯作为阴影光源)----
u32 lightCount = 0;
glm::vec3 shadowDir{0.0f, -1.0f, 0.0f}; // 无灯兜底:头顶正射
scene.Each<LightComponent>([&](Entity, LightComponent& light) {
if (lightCount >= kMaxLights) return;
glm::vec3 dir = glm::length(light.direction) > 0.0f
? glm::normalize(light.direction)
: glm::vec3(0.0f);
ubo.lights[lightCount].direction = glm::vec4(dir, 0.0f);
ubo.lights[lightCount].color = light.color;
if (lightCount == 0) shadowDir = dir;
++lightCount;
});
for (u32 i = lightCount; i < kMaxLights; ++i) {
ubo.lights[i].direction = glm::vec4(0.0f);
ubo.lights[i].color = glm::vec4(0.0f);
}
ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f);
// ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
// 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN
ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true );
// 每个实例的包围球对 4 个视锥分别测试:
// 相机视锥 → 主视图(Forward 画)
// 各级联光视锥 → 各自的阴影 batch(Shadow 按 region 各画各的)
// 级联盒沿光方向加了 caster pad(见 ComputeCascades):
// 盒外 caster 投进级联的影子不会被剔除裁掉。
// 收集按视图分桶,最后拼接成 [主段|级联0|级联1|级联2],
// batch.first 拼接时加上段基址,保证段内区间连续。
m_Frame.instanceModels.clear();
m_Frame.prevInstanceModels.clear();
m_MainScratch.Clear();
for (auto& s : m_CascScratch) s.Clear();
u32 totalInstances = 0;
bool instanceTruncated = false;
// 剔除平面提取按 [0,1] 深度约定——相机 proj 与 lightSpace 都已
// 做 [0,1] 重映射;原始 [-1,1] 矩阵的 near 平面公式不同,勿用
const Frustum mainFrustum = Frustum::FromMatrix(ubo.proj * ubo.view);
Frustum cascFrustum[kMaxCascades];
for (u32 c = 0; c < kMaxCascades; ++c) {
cascFrustum[c] = Frustum::FromMatrix(ubo.lightSpace[c]);
}
scene.Each<TransformComponent, MeshRendererComponent>(
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
Model* model = assets.GetModel(mr.model);
if (!model) return;
// TAA velocity:本实例的上一帧世界矩阵(首帧/新实体 = 当前
// 矩阵 → 速度为 0,不产生假运动)
const u32 entKey = static_cast<u32>(entt::to_integral(entity));
glm::mat4 prevWorld = t.World();
if (auto it = m_PrevWorlds.find(entKey); it != m_PrevWorlds.end()) {
prevWorld = it->second;
}
m_CurWorlds.emplace(entKey, t.World());
// 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放)
const f32 worldScale = glm::max(
glm::length(glm::vec3(t.World()[0])),
glm::max(glm::length(glm::vec3(t.World()[1])),
glm::length(glm::vec3(t.World()[2]))));
for (auto& mesh : model->GetMeshes()) {
const auto& bounds = mesh->GetBounds();
const glm::vec3 worldCenter =
glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f));
const f32 worldRadius = bounds.radius * worldScale;
auto tryAppend = [&](ViewScratch& s, const Frustum& frustum) {
if (!frustum.IntersectsSphere(worldCenter, worldRadius)) {
return;
}
if (totalInstances >= kMaxInstances) {
instanceTruncated = true;
return;
}
const auto [it, inserted] = s.lookup.emplace(
mesh.get(), static_cast<u32>(s.worlds.size()));
if (inserted) {
s.result.batches.push_back({mesh.get(),
static_cast<u32>(s.worlds.size()), 0});
}
s.result.batches[it->second].count++;
s.result.instanceCount++;
s.worlds.push_back(t.World());
s.prevWorlds.push_back(prevWorld);
++totalInstances;
};
tryAppend(m_MainScratch, mainFrustum);
for (u32 c = 0; c < kMaxCascades; ++c) {
tryAppend(m_CascScratch[c], cascFrustum[c]);
}
}
});
// ---- 4. 拼接:[主段|级联0|级联1|级联2],batch.first 加段基址 ----
auto appendView = [&](CullResult& dst, ViewScratch& s) {
const u32 base = static_cast<u32>(m_Frame.instanceModels.size());
dst.batches.clear();
dst.batches.reserve(s.result.batches.size());
for (auto& b : s.result.batches) {
b.first += base;
dst.batches.push_back(b);
}
dst.instanceCount = s.result.instanceCount;
m_Frame.instanceModels.insert(m_Frame.instanceModels.end(),
s.worlds.begin(), s.worlds.end());
m_Frame.prevInstanceModels.insert(m_Frame.prevInstanceModels.end(),
s.prevWorlds.begin(),
s.prevWorlds.end());
};
appendView(m_Frame.mainView, m_MainScratch);
for (u32 c = 0; c < kMaxCascades; ++c) {
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]);
}
// 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出)
m_PrevWorlds.swap(m_CurWorlds);
m_CurWorlds.clear();
m_LastView = ubo.view;
m_LastProjNoJitter = ubo.projNoJitter;
++m_FrameIndex;
if (instanceTruncated && !m_TruncationWarned) {
// 只告警一次(每帧告警会刷屏)
m_TruncationWarned = true;
ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated",
kMaxInstances);
}
m_Frame.collectMs = std::chrono::duration<f32, std::milli>(
std::chrono::steady_clock::now() - collectStart).count();
return true;
}
} // namespace Engine
@@ -0,0 +1,23 @@
//
// Created by atdunbg on 2026/9/23.
//
#include <Engine/Renderer/VulkanAssetUploader.h>
#include <Engine/Renderer/Model.h>
namespace Engine {
Scope<Model> VulkanAssetUploader::LoadModel(const std::string& path)
{
// GPU 上传在此发生:解析(assimp)+ buffer/image 创建一步完成
return CreateScope<Model>(
m_Device, m_Allocator, m_Descriptors, m_TextureLayout, path);
}
Scope<Model> VulkanAssetUploader::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
{
return Model::CreatePlane(m_Device, m_Allocator, m_Descriptors,
m_TextureLayout, halfSize, r, g, b);
}
} // namespace Engine
+9 -27
View File
@@ -18,31 +18,18 @@ namespace Engine {
vk::MemoryPropertyFlags memProps) vk::MemoryPropertyFlags memProps)
: m_Device(device), m_Allocator(allocator) , m_Size(size) : m_Device(device), m_Allocator(allocator) , m_Size(size)
{ {
vk::BufferCreateInfo bufferInfo; // VMA 惯用法:创建 + 分配 + 绑定一步完成;
bufferInfo.setSize(size) // HOST_VISIBLE 时持久映射,MappedPtr 直接可用
.setUsage(usage) m_Allocation = allocator.CreateBuffer(size, usage, memProps);
.setSharingMode(vk::SharingMode::eExclusive); m_Buffer = m_Allocation.Buffer;
m_MappedPtr = m_Allocation.MappedPtr;
m_Buffer = m_Device.GetDevice().createBuffer(bufferInfo);
auto reqs = m_Device.GetDevice().getBufferMemoryRequirements(m_Buffer);
auto alloc = allocator.Allocate(reqs, memProps);
m_Memory = alloc.Memory;
m_Offset = alloc.Offset;
m_MappedPtr = alloc.MappedPtr;
m_Device.GetDevice().bindBufferMemory(m_Buffer, m_Memory, m_Offset);
m_Allocation = alloc; // 成员变量,保存起来
STATS.AddBuffer(m_Size); STATS.AddBuffer(m_Size);
} }
Buffer::~Buffer() Buffer::~Buffer()
{ {
m_Device.GetDevice().destroyBuffer(m_Buffer); m_Allocator.Destroy(m_Allocation);
m_Allocator.Free(m_Allocation);
STATS.RemoveBuffer(m_Size); STATS.RemoveBuffer(m_Size);
} }
@@ -54,12 +41,7 @@ namespace Engine {
} }
memcpy(static_cast<char*>(m_MappedPtr) + offset, data, size); memcpy(static_cast<char*>(m_MappedPtr) + offset, data, size);
// flush 整个内存块,避免对齐问题 // 非 coherent 内存写后冲刷(coherent 时 VMA 内部自动跳过)
vk::MappedMemoryRange range; m_Allocator.Flush(m_Allocation);
range.setMemory(m_Memory)
.setOffset(0)
.setSize(VK_WHOLE_SIZE);
m_Device.GetDevice().flushMappedMemoryRanges(range);
} }
} // namespace Engine } // namespace Engine
+57 -19
View File
@@ -11,23 +11,32 @@ namespace Engine
DescriptorManager::DescriptorManager(Device& device) DescriptorManager::DescriptorManager(Device& device)
: m_Device(device) : m_Device(device)
{ {
std::array<vk::DescriptorPoolSize, 2> poolSizes = { m_Pools.push_back(CreateChunk());
vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, kMaxSets), ENGINE_INFO("DescriptorManager created (growable, chunk = {} sets)",
vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, kMaxSets), kChunkSets);
};
vk::DescriptorPoolCreateInfo poolInfo;
poolInfo.setMaxSets(kMaxSets)
.setPoolSizes(poolSizes)
.setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet);
m_Pool = m_Device.GetDevice().createDescriptorPool(poolInfo);
ENGINE_INFO("DescriptorManager created");
} }
DescriptorManager::~DescriptorManager() DescriptorManager::~DescriptorManager()
{ {
m_Device.GetDevice().destroyDescriptorPool(m_Pool); for (auto pool : m_Pools) {
m_Device.GetDevice().destroyDescriptorPool(pool);
}
}
vk::DescriptorPool DescriptorManager::CreateChunk()
{
std::array<vk::DescriptorPoolSize, 3> poolSizes = {
vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, kChunkSets),
vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, kChunkSets),
vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, kChunkSets),
};
vk::DescriptorPoolCreateInfo poolInfo;
poolInfo.setMaxSets(kChunkSets)
.setPoolSizes(poolSizes)
.setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet);
return m_Device.GetDevice().createDescriptorPool(poolInfo);
} }
vk::DescriptorSetLayout DescriptorManager::CreateLayout(const std::vector<DescriptorBinding>& bindings) vk::DescriptorSetLayout DescriptorManager::CreateLayout(const std::vector<DescriptorBinding>& bindings)
@@ -51,12 +60,24 @@ namespace Engine
vk::DescriptorSet DescriptorManager::AllocateDescriptorSet(vk::DescriptorSetLayout layout) vk::DescriptorSet DescriptorManager::AllocateDescriptorSet(vk::DescriptorSetLayout layout)
{ {
vk::DescriptorSetAllocateInfo allocInfo; // 从最新块分配;耗尽(OutOfPool/Fragmented)则追加一块重试
allocInfo.setDescriptorPool(m_Pool) for (;;) {
.setSetLayouts(layout); try {
vk::DescriptorSetAllocateInfo allocInfo;
auto sets = m_Device.GetDevice().allocateDescriptorSets(allocInfo); allocInfo.setDescriptorPool(m_Pools.back())
return sets[0]; .setSetLayouts(layout);
auto sets = m_Device.GetDevice().allocateDescriptorSets(allocInfo);
return sets[0];
} catch (const vk::OutOfPoolMemoryError&) {
m_Pools.push_back(CreateChunk());
ENGINE_INFO("DescriptorManager: pool chunk grown -> {} chunks",
m_Pools.size());
} catch (const vk::FragmentedPoolError&) {
m_Pools.push_back(CreateChunk());
ENGINE_INFO("DescriptorManager: pool fragmented, new chunk -> {}",
m_Pools.size());
}
}
} }
void DescriptorManager::UpdateUniformBuffer(vk::DescriptorSet set, u32 binding, void DescriptorManager::UpdateUniformBuffer(vk::DescriptorSet set, u32 binding,
@@ -76,6 +97,23 @@ namespace Engine
m_Device.GetDevice().updateDescriptorSets(write, {}); m_Device.GetDevice().updateDescriptorSets(write, {});
} }
void DescriptorManager::UpdateStorageBuffer(vk::DescriptorSet set, u32 binding,
vk::Buffer buffer, u64 size)
{
vk::DescriptorBufferInfo bufferInfo;
bufferInfo.setBuffer(buffer).setOffset(0).setRange(size);
vk::WriteDescriptorSet write;
write.setDstSet(set)
.setDstBinding(binding)
.setDstArrayElement(0)
.setDescriptorType(vk::DescriptorType::eStorageBuffer)
.setDescriptorCount(1)
.setBufferInfo(bufferInfo);
m_Device.GetDevice().updateDescriptorSets(write, {});
}
void DescriptorManager::UpdateTexture(vk::DescriptorSet set, u32 binding, void DescriptorManager::UpdateTexture(vk::DescriptorSet set, u32 binding,
vk::Sampler sampler, vk::ImageView view) vk::Sampler sampler, vk::ImageView view)
{ {
+118 -191
View File
@@ -6,220 +6,147 @@
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <algorithm> #include <vk_mem_alloc.h>
#include <stdexcept>
#include "Engine/Core/Stats.h" #include "Engine/Core/Stats.h"
namespace Engine { namespace Engine {
MemoryAllocator::MemoryAllocator(Device& device) namespace
: m_Device(device) {
{ // props → VMA 创建参数:
ENGINE_INFO("MemoryAllocator created"); // HOST_VISIBLE → AUTO + 持久映射 + 顺序写访问(VMA 自动挑 host 内存类型)
} // 其余(DeviceLocal)→ AUTO_PREFER_DEVICE(优先显存,允许溢出到 BAR)
VmaAllocationCreateInfo MakeCreateInfo(vk::MemoryPropertyFlags props)
MemoryAllocator::~MemoryAllocator() {
{ VmaAllocationCreateInfo ci{};
const auto& vkDevice = m_Device.GetDevice(); if (props & vk::MemoryPropertyFlagBits::eHostVisible) {
ci.usage = VMA_MEMORY_USAGE_AUTO;
for (auto& blocks : m_Blocks) { ci.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT
for (auto& block : blocks) { | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
if (block.IsMapped) { } else {
vkDevice.unmapMemory(block.Memory); ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
}
vkDevice.freeMemory(block.Memory);
STATS.RemoveMemoryBlock(block.Size);
}
blocks.clear();
}
}
vk::DeviceSize MemoryAllocator::GetBlockSize(vk::MemoryPropertyFlags props) const
{
if (props & vk::MemoryPropertyFlagBits::eHostVisible) {
return 16ull * 1024 * 1024; // 16 MB
}
return 64ull * 1024 * 1024; // 64 MB
}
MemoryAllocator::Allocation MemoryAllocator::Allocate(
vk::MemoryRequirements reqs, vk::MemoryPropertyFlags props)
{
u32 typeIndex = m_Device.FindMemoryType(reqs.memoryTypeBits, props);
auto& blocks = m_Blocks[typeIndex];
// 1. 扫描已有块的 FreeList,找能放下的区域(first-fit)
for (u32 i = 0; i < blocks.size(); ++i) {
auto& block = blocks[i];
for (auto it = block.FreeList.begin(); it != block.FreeList.end(); ++it) {
vk::DeviceSize alignedOffset =
(it->Offset + reqs.alignment - 1) & ~(reqs.alignment - 1);
vk::DeviceSize end = alignedOffset + reqs.size;
if (end <= it->Offset + it->Size) {
// 找到了
Allocation alloc;
alloc.Memory = block.Memory;
alloc.Offset = alignedOffset;
alloc.Size = reqs.size;
alloc.MappedPtr = block.IsMapped
? static_cast<char*>(block.MappedPtr) + alignedOffset
: nullptr;
alloc.BlockIndex = i;
alloc.TypeIndex = typeIndex;
alloc.Valid = true;
// 更新 FreeList
if (alignedOffset == it->Offset && reqs.size == it->Size) {
// 整个区域被用了,移除
block.FreeList.erase(it);
} else if (alignedOffset == it->Offset) {
// 从头部切
it->Offset += reqs.size;
it->Size -= reqs.size;
} else if (end == it->Offset + it->Size) {
// 从尾部切
it->Size = alignedOffset - it->Offset;
} else {
// 从中间切,拆成两段
FreeRange tail;
tail.Offset = end;
tail.Size = (it->Offset + it->Size) - end;
it->Size = alignedOffset - it->Offset;
block.FreeList.insert(it + 1, tail);
}
return alloc;
} }
// requiredFlags 兜底:调用方明确要求的属性 VMA 一定满足
ci.requiredFlags = static_cast<VkMemoryPropertyFlags>(props);
return ci;
} }
} }
// 2. 在已有块的尾部 bump 分配 MemoryAllocator::MemoryAllocator(Device& device)
for (u32 i = 0; i < blocks.size(); ++i) { : m_Device(device)
auto& block = blocks[i]; {
vk::DeviceSize alignedOffset = VmaAllocatorCreateInfo ci{};
(block.Used + reqs.alignment - 1) & ~(reqs.alignment - 1); ci.instance = static_cast<VkInstance>(device.GetInstance());
ci.physicalDevice = static_cast<VkPhysicalDevice>(device.GetPhysicalDevice());
ci.device = static_cast<VkDevice>(device.GetDevice());
// 与 Device 创建实例时的 API 版本一致(启用 1.1+ 核心化的
// dedicated allocation / memory requirements 控制等路径)
ci.vulkanApiVersion = VK_API_VERSION_1_4;
// VMA_STATIC_VULKAN_FUNCTIONS=1:直接用链接的 Vulkan loader 函数表
if (alignedOffset + reqs.size <= block.Size) { if (vmaCreateAllocator(&ci, &m_Vma) != VK_SUCCESS) {
Allocation alloc; throw std::runtime_error("MemoryAllocator: vmaCreateAllocator failed");
alloc.Memory = block.Memory; }
alloc.Offset = alignedOffset;
alloc.Size = reqs.size;
alloc.MappedPtr = block.IsMapped
? static_cast<char*>(block.MappedPtr) + alignedOffset
: nullptr;
alloc.BlockIndex = i;
alloc.TypeIndex = typeIndex;
alloc.Valid = true;
block.Used = alignedOffset + reqs.size; ENGINE_INFO("MemoryAllocator created (VMA backend)");
return alloc; }
MemoryAllocator::~MemoryAllocator()
{
if (m_Vma) {
vmaDestroyAllocator(m_Vma);
} }
} }
// 3. 创建新块 MemoryAllocator::Allocation MemoryAllocator::CreateBuffer(
vk::DeviceSize blockSize = std::max(GetBlockSize(props), reqs.size); vk::DeviceSize size, vk::BufferUsageFlags usage, vk::MemoryPropertyFlags props)
{
VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bi.size = size;
bi.usage = static_cast<VkBufferUsageFlags>(usage);
bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
vk::MemoryAllocateInfo allocInfo; VmaAllocationCreateInfo aci = MakeCreateInfo(props);
allocInfo.setAllocationSize(blockSize)
.setMemoryTypeIndex(typeIndex);
Block block; VkBuffer buffer = nullptr;
block.Memory = m_Device.GetDevice().allocateMemory(allocInfo); VmaAllocation handle = nullptr;
block.Size = blockSize; if (vmaCreateBuffer(m_Vma, &bi, &aci, &buffer, &handle, nullptr) != VK_SUCCESS) {
block.Used = 0; ENGINE_ERROR("MemoryAllocator: CreateBuffer({} bytes) failed", size);
return {};
if (props & vk::MemoryPropertyFlagBits::eHostVisible) {
block.MappedPtr = m_Device.GetDevice().mapMemory(
block.Memory, 0, VK_WHOLE_SIZE);
block.IsMapped = true;
}
blocks.push_back(std::move(block));
m_TotalAllocated[typeIndex] += blockSize;
u32 newIndex = static_cast<u32>(blocks.size() - 1);
auto& newBlock = blocks[newIndex];
vk::DeviceSize alignedOffset = (reqs.alignment - 1) & ~(reqs.alignment - 1);
Allocation alloc;
alloc.Memory = newBlock.Memory;
alloc.Offset = alignedOffset;
alloc.Size = reqs.size;
alloc.MappedPtr = newBlock.IsMapped
? static_cast<char*>(newBlock.MappedPtr) + alignedOffset
: nullptr;
alloc.BlockIndex = newIndex;
alloc.TypeIndex = typeIndex;
alloc.Valid = true;
newBlock.Used = alignedOffset + reqs.size;
ENGINE_INFO("MemoryAllocator: new block of {} bytes (type {})",
blockSize, typeIndex);
STATS.AddMemoryBlock(blockSize);
STATS.AddMemoryUsed(reqs.size);
return alloc;
}
void MemoryAllocator::Free(const Allocation& alloc)
{
if (!alloc.Valid) return;
STATS.RemoveMemoryUsed(alloc.Size);
auto& block = m_Blocks[alloc.TypeIndex][alloc.BlockIndex];
FreeRange range;
range.Offset = alloc.Offset;
range.Size = alloc.Size;
// 插入 FreeList,保持按 offset 排序,方便合并
auto it = std::lower_bound(
block.FreeList.begin(), block.FreeList.end(), range,
[](const FreeRange& a, const FreeRange& b) {
return a.Offset < b.Offset;
});
it = block.FreeList.insert(it, range);
// 向前合并
if (it != block.FreeList.begin()) {
auto prev = it - 1;
if (prev->Offset + prev->Size == it->Offset) {
prev->Size += it->Size;
block.FreeList.erase(it);
it = prev;
} }
Allocation out;
out.Buffer = buffer;
out.Size = size;
out.Handle = handle;
out.Valid = true;
VmaAllocationInfo info{};
vmaGetAllocationInfo(m_Vma, handle, &info);
out.MappedPtr = info.pMappedData; // 非 MAPPED 标记时为 null
STATS.AddMemoryUsed(size);
return out;
} }
// 向后合并 MemoryAllocator::Allocation MemoryAllocator::CreateImage(
auto next = it + 1; const vk::ImageCreateInfo& imageInfo, vk::MemoryPropertyFlags props)
if (next != block.FreeList.end()) { {
if (it->Offset + it->Size == next->Offset) { VkImageCreateInfo ii = imageInfo; // 同布局,隐式转换
it->Size += next->Size;
block.FreeList.erase(next); VmaAllocationCreateInfo aci = MakeCreateInfo(props);
VkImage image = nullptr;
VmaAllocation handle = nullptr;
if (vmaCreateImage(m_Vma, &ii, &aci, &image, &handle, nullptr) != VK_SUCCESS) {
ENGINE_ERROR("MemoryAllocator: CreateImage({}x{}) failed",
imageInfo.extent.width, imageInfo.extent.height);
return {};
} }
Allocation out;
out.Image = image;
out.Size = ii.extent.depth * ii.extent.height * ii.extent.width; // 近似值(统计用)
out.Handle = handle;
out.Valid = true;
STATS.AddMemoryUsed(out.Size);
return out;
} }
}
void MemoryAllocator::PrintStats() const void MemoryAllocator::Destroy(Allocation& alloc)
{ {
for (u32 type = 0; type < VK_MAX_MEMORY_TYPES; ++type) { if (!alloc.Valid) return;
if (m_Blocks[type].empty()) continue;
vk::DeviceSize totalUsed = 0; // VMA 要求资源与分配成对销毁,顺序不限但句柄必须配对
for (auto& b : m_Blocks[type]) totalUsed += b.Used; if (alloc.Buffer) {
vmaDestroyBuffer(m_Vma, alloc.Buffer, alloc.Handle);
} else if (alloc.Image) {
vmaDestroyImage(m_Vma, alloc.Image, alloc.Handle);
}
ENGINE_INFO(" Type {}: {} blocks, total allocated {} MB, used {} MB", STATS.RemoveMemoryUsed(alloc.Size);
type, alloc = {};
m_Blocks[type].size(),
m_TotalAllocated[type] / (1024 * 1024),
totalUsed / (1024 * 1024));
} }
}
} // namespace Engine void MemoryAllocator::Flush(const Allocation& alloc)
{
if (!alloc.Valid || !alloc.Handle) return;
// HOST_COHERENT 时 VMA 内部自动跳过
vmaFlushAllocation(m_Vma, alloc.Handle, 0, VK_WHOLE_SIZE);
}
void MemoryAllocator::PrintStats() const
{
VmaTotalStatistics stats;
vmaCalculateStatistics(m_Vma, &stats);
ENGINE_INFO(" VMA: {} allocations in {} blocks, total {} MB (used {} MB)",
stats.total.statistics.allocationCount,
stats.total.statistics.blockCount,
stats.total.statistics.blockBytes / (1024 * 1024),
stats.total.statistics.allocationBytes / (1024 * 1024));
}
} // namespace Engine
+29 -4
View File
@@ -51,6 +51,21 @@ namespace Engine
} }
vk::SwapchainCreateInfoKHR createInfo; vk::SwapchainCreateInfoKHR createInfo;
vk::CompositeAlphaFlagBitsKHR compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque;
if (caps.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::ePreMultiplied)
{
compositeAlpha = vk::CompositeAlphaFlagBitsKHR::ePreMultiplied;
}
else if (caps.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::ePostMultiplied)
{
compositeAlpha = vk::CompositeAlphaFlagBitsKHR::ePostMultiplied;
}
ENGINE_INFO("supportedCompositeAlpha = 0x{:x}, chosen = 0x{:x}",
static_cast<u32>(caps.supportedCompositeAlpha),
static_cast<u32>(compositeAlpha));
createInfo.setSurface(surface) createInfo.setSurface(surface)
.setMinImageCount(imageCount) .setMinImageCount(imageCount)
.setImageFormat(surfaceFormat.format) .setImageFormat(surfaceFormat.format)
@@ -60,7 +75,7 @@ namespace Engine
.setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment)
.setImageSharingMode(vk::SharingMode::eExclusive) .setImageSharingMode(vk::SharingMode::eExclusive)
.setPreTransform(caps.currentTransform) .setPreTransform(caps.currentTransform)
.setCompositeAlpha(vk::CompositeAlphaFlagBitsKHR::eOpaque) .setCompositeAlpha(compositeAlpha)
.setPresentMode(presentMode) .setPresentMode(presentMode)
.setClipped(VK_TRUE) .setClipped(VK_TRUE)
.setOldSwapchain(nullptr); .setOldSwapchain(nullptr);
@@ -95,10 +110,11 @@ namespace Engine
m_ImageViews[i] = vkDevice.createImageView(viewInfo); m_ImageViews[i] = vkDevice.createImageView(viewInfo);
} }
ENGINE_INFO("Swapchain created: {}x{}, {} images, format={}", ENGINE_INFO("Swapchain created: {}x{}, {} images, format={}, present={}",
m_Extent.width, m_Extent.height, m_Extent.width, m_Extent.height,
m_Images.size(), m_Images.size(),
vk::to_string(m_Format)); vk::to_string(m_Format),
vk::to_string(presentMode));
} }
void Swapchain::Destroy() void Swapchain::Destroy()
@@ -151,9 +167,18 @@ namespace Engine
vk::PresentModeKHR Swapchain::ChoosePresentMode(const std::vector<vk::PresentModeKHR>& modes) vk::PresentModeKHR Swapchain::ChoosePresentMode(const std::vector<vk::PresentModeKHR>& modes)
{ {
// 实测 Wayland/NVIDIA:Mailbox 不阻塞应用循环(compositor 取最新
// 缓冲,应用层计数可远超刷新率);Immediate 常不可用而退到 FIFO。
// 因此映射按"用户预期"定义:
// vsync 开 → FIFO:应用被 pacing 到刷新率(真正的垂直同步)
// vsync 关 → Mailbox:应用循环不限速(Wayland 下无撕裂)
const vk::PresentModeKHR preferred = m_VSync
? vk::PresentModeKHR::eFifo
: vk::PresentModeKHR::eMailbox;
for (const auto& m : modes) for (const auto& m : modes)
{ {
if (m == vk::PresentModeKHR::eMailbox) if (m == preferred)
{ {
return m; return m;
} }
+241 -53
View File
@@ -5,80 +5,88 @@
#include <Engine/Vulkan/Texture.h> #include <Engine/Vulkan/Texture.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h> #include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Assets/DdsLoader.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <stb/stb_image.h> #include <stb/stb_image.h>
#include <algorithm>
#include <cctype>
#include <cstring> #include <cstring>
#include "Engine/Core/Stats.h" #include "Engine/Core/Stats.h"
namespace Engine namespace Engine
{ {
Texture::Texture(Device& device, MemoryAllocator& allocator, const std::string& path) Texture::Texture(Device& device, MemoryAllocator& allocator, const std::string& path,
bool srgb)
: m_Device(device) , m_Allocator(allocator) : m_Device(device) , m_Allocator(allocator)
{ {
// 1. CPU 加载图片 // 按扩展名分发:DDS(BC 压缩直传 + mip 链)vs stbi(无压缩单 mip)
int width = 0, height = 0, channels = 0; const std::string ext = path.size() >= 4
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, STBI_rgb_alpha); ? path.substr(path.size() - 4) : "";
std::string lower;
for (char c : ext) lower += static_cast<char>(std::tolower(c));
if (!pixels) if (lower == ".dds") {
{ InitFromDds(path, srgb);
ENGINE_ERROR("Failed to load texture: {}", path); } else {
return; // 1. CPU 加载图片
int width = 0, height = 0, channels = 0;
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels,
STBI_rgb_alpha);
if (!pixels) {
ENGINE_ERROR("Failed to load texture: {}", path);
return;
}
InitFromPixels(pixels, static_cast<u32>(width), static_cast<u32>(height), srgb);
stbi_image_free(pixels);
ENGINE_INFO("Texture loaded: {} ({}x{})", path, m_Width, m_Height);
} }
InitFromPixels(pixels, static_cast<u32>(width), static_cast<u32>(height));
stbi_image_free(pixels);
ENGINE_INFO("Texture loaded: {} ({}x{})", path, m_Width, m_Height);
} }
Texture::Texture(Device& device, MemoryAllocator& allocator, ProceduralTag, Texture::Texture(Device& device, MemoryAllocator& allocator, ProceduralTag,
const std::array<u8, 4>& rgba) const std::array<u8, 4>& rgba, bool srgb)
: m_Device(device) , m_Allocator(allocator) : m_Device(device) , m_Allocator(allocator)
{ {
const u8 pixels[4] = { rgba[0], rgba[1], rgba[2], rgba[3] }; const u8 pixels[4] = { rgba[0], rgba[1], rgba[2], rgba[3] };
InitFromPixels(pixels, 1, 1); InitFromPixels(pixels, 1, 1, srgb);
} }
Scope<Texture> Texture::CreateSolidColor(Device& device, MemoryAllocator& allocator, Scope<Texture> Texture::CreateSolidColor(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b) u8 r, u8 g, u8 b, bool srgb)
{ {
return Scope<Texture>(new Texture(device, allocator, ProceduralTag{}, {r, g, b, 255})); return Scope<Texture>(new Texture(device, allocator, ProceduralTag{},
{r, g, b, 255}, srgb));
} }
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height) void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
{ {
m_Width = width; m_Width = width;
m_Height = height; m_Height = height;
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
m_MipLevels = 1;
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) * vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
static_cast<vk::DeviceSize>(height) * 4; // RGBA8 static_cast<vk::DeviceSize>(height) * 4; // RGBA8
// 2. 创建 Staging Buffer(HOST_VISIBLE) // 2. 创建 Staging Buffer(HOST_VISIBLE,VMA 持久映射)
vk::BufferCreateInfo stagingInfo; auto stagingAlloc = m_Allocator.CreateBuffer(
stagingInfo.setSize(imageSize) imageSize,
.setUsage(vk::BufferUsageFlagBits::eTransferSrc) vk::BufferUsageFlagBits::eTransferSrc,
.setSharingMode(vk::SharingMode::eExclusive);
vk::Buffer stagingBuffer = m_Device.GetDevice().createBuffer(stagingInfo);
auto stagingReqs = m_Device.GetDevice().getBufferMemoryRequirements(stagingBuffer);
auto stagingAlloc = m_Allocator.Allocate(
stagingReqs,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent); vk::MemoryPropertyFlagBits::eHostCoherent);
m_Device.GetDevice().bindBufferMemory(stagingBuffer, stagingAlloc.Memory, stagingAlloc.Offset);
// 3. 拷贝像素到 Staging Buffer // 3. 拷贝像素到 Staging Buffer
std::memcpy(stagingAlloc.MappedPtr, pixels, static_cast<size_t>(imageSize)); std::memcpy(stagingAlloc.MappedPtr, pixels, static_cast<size_t>(imageSize));
// 4. 创建 Device Local Image // 4. 创建 Device Local Image
vk::ImageCreateInfo imageInfo; vk::ImageCreateInfo imageInfo;
imageInfo.setImageType(vk::ImageType::e2D) imageInfo.setImageType(vk::ImageType::e2D)
.setFormat(vk::Format::eR8G8B8A8Srgb) .setFormat(m_Format)
.setExtent({m_Width, m_Height, 1}) .setExtent({m_Width, m_Height, 1})
.setMipLevels(1) .setMipLevels(1)
.setArrayLayers(1) .setArrayLayers(1)
@@ -88,24 +96,18 @@ namespace Engine
.setSharingMode(vk::SharingMode::eExclusive) .setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined); .setInitialLayout(vk::ImageLayout::eUndefined);
m_Image = m_Device.GetDevice().createImage(imageInfo); m_ImageAllocation = m_Allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
auto imageReqs = m_Device.GetDevice().getImageMemoryRequirements(m_Image); m_Image = m_ImageAllocation.Image;
auto imageAlloc = m_Allocator.Allocate(imageReqs, vk::MemoryPropertyFlagBits::eDeviceLocal);
m_ImageAllocation = imageAlloc;
m_Memory = imageAlloc.Memory;
m_Device.GetDevice().bindImageMemory(m_Image, m_Memory, imageAlloc.Offset);
// 5. 创建 ImageView // 5. 创建 ImageView
vk::ImageViewCreateInfo viewInfo; vk::ImageViewCreateInfo viewInfo;
viewInfo.setImage(m_Image) viewInfo.setImage(m_Image)
.setViewType(vk::ImageViewType::e2D) .setViewType(vk::ImageViewType::e2D)
.setFormat(vk::Format::eR8G8B8A8Srgb) .setFormat(m_Format)
.setSubresourceRange({ .setSubresourceRange({
vk::ImageAspectFlagBits::eColor, vk::ImageAspectFlagBits::eColor,
0, 1, 0, 1 0, m_MipLevels, 0, 1
}); });
m_View = m_Device.GetDevice().createImageView(viewInfo); m_View = m_Device.GetDevice().createImageView(viewInfo);
@@ -122,7 +124,9 @@ namespace Engine
.setBorderColor(vk::BorderColor::eIntOpaqueBlack) .setBorderColor(vk::BorderColor::eIntOpaqueBlack)
.setUnnormalizedCoordinates(VK_FALSE) .setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE) .setCompareEnable(VK_FALSE)
.setMipmapMode(vk::SamplerMipmapMode::eLinear); .setMipmapMode(vk::SamplerMipmapMode::eLinear)
.setMinLod(0.0f)
.setMaxLod(static_cast<f32>(m_MipLevels));
m_Sampler = m_Device.GetDevice().createSampler(samplerInfo); m_Sampler = m_Device.GetDevice().createSampler(samplerInfo);
@@ -168,7 +172,7 @@ namespace Engine
.setImageOffset({0, 0, 0}) .setImageOffset({0, 0, 0})
.setImageExtent({m_Width, m_Height, 1}); .setImageExtent({m_Width, m_Height, 1});
cmd.copyBufferToImage(stagingBuffer, m_Image, cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image,
vk::ImageLayout::eTransferDstOptimal, region); vk::ImageLayout::eTransferDstOptimal, region);
// 6.3 布局:TRANSFER_DST → SHADER_READ_ONLY // 6.3 布局:TRANSFER_DST → SHADER_READ_ONLY
@@ -202,28 +206,212 @@ namespace Engine
ENGINE_ERROR("Texture upload: waitForFences failed"); ENGINE_ERROR("Texture upload: waitForFences failed");
} }
// 8. 清理临时对象 // 8. 清理临时对象(staging 资源与分配经 VMA 成对释放)
m_Device.GetDevice().destroyFence(fence); m_Device.GetDevice().destroyFence(fence);
m_Device.GetDevice().destroyCommandPool(pool); m_Device.GetDevice().destroyCommandPool(pool);
m_Device.GetDevice().destroyBuffer(stagingBuffer); m_Allocator.Destroy(stagingAlloc);
m_Allocator.Free(stagingAlloc);
// staging 内存由 MemoryAllocator 统一释放
// 显存占用 = 宽 * 高 * 4 字节(RGBA8) // 显存占用 = 宽 * 高 * 4 字节(RGBA8)
m_TextureBytes = static_cast<u64>(m_Width) * m_Height * 4; m_TextureBytes = static_cast<u64>(m_Width) * m_Height * 4;
STATS.AddTexture(m_TextureBytes); STATS.AddTexture(m_TextureBytes);
} }
// DDS → Vulkan 格式映射。sRGB 语义由贴图槽位决定(渲染侧职责):
// 颜色图(BaseColor/Emissive)→ SRGB 变体
// 数据图(Normal/Specular 等线性槽位)→ UNORM
vk::Format ToVkFormat(Assets::Dds::DdsFormat f, bool srgb)
{
using DF = Assets::Dds::DdsFormat;
switch (f) {
case DF::BC1: return srgb ? vk::Format::eBc1RgbaSrgbBlock
: vk::Format::eBc1RgbaUnormBlock;
case DF::BC2: return srgb ? vk::Format::eBc2SrgbBlock
: vk::Format::eBc2UnormBlock;
case DF::BC3: return srgb ? vk::Format::eBc3SrgbBlock
: vk::Format::eBc3UnormBlock;
case DF::BC4: return vk::Format::eBc4UnormBlock;
case DF::BC5: return vk::Format::eBc5UnormBlock;
case DF::BC6H: return vk::Format::eBc6HUfloatBlock;
case DF::BC7: return srgb ? vk::Format::eBc7SrgbBlock
: vk::Format::eBc7UnormBlock;
case DF::RGBA8: return srgb ? vk::Format::eR8G8B8A8Srgb
: vk::Format::eR8G8B8A8Unorm;
case DF::BGRA8: return srgb ? vk::Format::eB8G8R8A8Srgb
: vk::Format::eB8G8R8A8Unorm;
}
return vk::Format::eUndefined;
}
u32 MipDim(u32 v, u32 level)
{
return std::max(1u, v >> level);
}
void Texture::InitFromDds(const std::string& path, bool srgb)
{
Assets::Dds::DdsData data;
std::string err;
if (!Assets::Dds::Load(path, data, err)) {
ENGINE_ERROR("DDS: {} ({})", err, path);
return;
}
m_Width = data.Width;
m_Height = data.Height;
m_MipLevels = data.MipLevels;
m_Format = ToVkFormat(data.Format, srgb);
ENGINE_INFO("DDS loaded: {} ({}x{}, {} mips, {})", path,
m_Width, m_Height, m_MipLevels, vk::to_string(m_Format));
// 1. staging:各 mip 紧凑排布,记偏移
vk::DeviceSize total = 0;
for (const auto& mip : data.Mips) {
total += mip.size();
}
auto stagingAlloc = m_Allocator.CreateBuffer(
total,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
std::vector<vk::DeviceSize> offsets(m_MipLevels);
{
vk::DeviceSize off = 0;
for (u32 mip = 0; mip < m_MipLevels; ++mip) {
offsets[mip] = off;
std::memcpy(stagingAlloc.MappedPtr + off,
data.Mips[mip].data(), data.Mips[mip].size());
off += data.Mips[mip].size();
}
}
// 2. device local image(带完整 mip 链)
vk::ImageCreateInfo imageInfo;
imageInfo.setImageType(vk::ImageType::e2D)
.setFormat(m_Format)
.setExtent({m_Width, m_Height, 1})
.setMipLevels(m_MipLevels)
.setArrayLayers(1)
.setSamples(vk::SampleCountFlagBits::e1)
.setTiling(vk::ImageTiling::eOptimal)
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled)
.setSharingMode(vk::SharingMode::eExclusive)
.setInitialLayout(vk::ImageLayout::eUndefined);
m_ImageAllocation = m_Allocator.CreateImage(imageInfo,
vk::MemoryPropertyFlagBits::eDeviceLocal);
m_Image = m_ImageAllocation.Image;
// 3. view / sampler(覆盖整个 mip 链)
m_View = m_Device.GetDevice().createImageView(
vk::ImageViewCreateInfo{}
.setImage(m_Image)
.setViewType(vk::ImageViewType::e2D)
.setFormat(m_Format)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, m_MipLevels, 0, 1}));
m_Sampler = m_Device.GetDevice().createSampler(
vk::SamplerCreateInfo{}
.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear)
.setMipmapMode(vk::SamplerMipmapMode::eLinear)
.setAddressModeU(vk::SamplerAddressMode::eRepeat)
.setAddressModeV(vk::SamplerAddressMode::eRepeat)
.setAddressModeW(vk::SamplerAddressMode::eRepeat)
.setAnisotropyEnable(VK_FALSE)
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
.setUnnormalizedCoordinates(VK_FALSE)
.setCompareEnable(VK_FALSE)
.setMinLod(0.0f)
.setMaxLod(static_cast<f32>(m_MipLevels)));
// 4. 一次性命令缓冲:整图 → TransferDst,逐 mip 拷贝 → ShaderRead
vk::CommandPoolCreateInfo poolInfo;
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
vk::CommandPool pool = m_Device.GetDevice().createCommandPool(poolInfo);
vk::CommandBufferAllocateInfo allocInfo;
allocInfo.setCommandPool(pool)
.setLevel(vk::CommandBufferLevel::ePrimary)
.setCommandBufferCount(1);
vk::CommandBuffer cmd =
m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
cmd.begin(vk::CommandBufferBeginInfo{
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
vk::ImageMemoryBarrier2 toTransfer;
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setOldLayout(vk::ImageLayout::eUndefined)
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
.setImage(m_Image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, m_MipLevels, 0, 1});
cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
// BC 数据按块紧凑排布 → bufferRowLength/Height 留 0(紧凑)
for (u32 mip = 0; mip < m_MipLevels; ++mip) {
vk::BufferImageCopy region;
region.setBufferOffset(offsets[mip])
.setBufferRowLength(0)
.setBufferImageHeight(0)
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
mip, 0, 1})
.setImageOffset({0, 0, 0})
.setImageExtent({MipDim(m_Width, mip),
MipDim(m_Height, mip), 1});
cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image,
vk::ImageLayout::eTransferDstOptimal, region);
}
vk::ImageMemoryBarrier2 toShader;
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
.setImage(m_Image)
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
0, m_MipLevels, 0, 1});
cmd.pipelineBarrier2(vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
cmd.end();
// 5. 提交并等待,清理 staging
vk::Fence fence = m_Device.GetDevice().createFence({});
m_Device.GetGraphicsQueue().submit(
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
if (m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
vk::Result::eSuccess) {
ENGINE_ERROR("DDS upload: waitForFences failed");
}
m_Device.GetDevice().destroyFence(fence);
m_Device.GetDevice().destroyCommandPool(pool);
m_Allocator.Destroy(stagingAlloc);
m_TextureBytes = total;
STATS.AddTexture(m_TextureBytes);
}
Texture::~Texture() Texture::~Texture()
{ {
const auto& vkDevice = m_Device.GetDevice(); const auto& vkDevice = m_Device.GetDevice();
vkDevice.destroySampler(m_Sampler); vkDevice.destroySampler(m_Sampler);
vkDevice.destroyImageView(m_View); vkDevice.destroyImageView(m_View);
vkDevice.destroyImage(m_Image);
// 归还 image 的显存 // image 资源与显存经 VMA 成对释放
m_Allocator.Free(m_ImageAllocation); m_Allocator.Destroy(m_ImageAllocation);
STATS.RemoveTexture(m_TextureBytes); STATS.RemoveTexture(m_TextureBytes);
} }
+4 -4
View File
@@ -10,15 +10,15 @@
namespace Engine { namespace Engine {
UniformBuffer::UniformBuffer(Device& device, MemoryAllocator& allocator, UniformBuffer::UniformBuffer(Device& device, MemoryAllocator& allocator,
u64 size, u32 frameCount) u64 size, u32 frameCount,
vk::BufferUsageFlags usage)
: m_Size(size) : m_Size(size)
{ {
m_Buffers.reserve(frameCount); m_Buffers.reserve(frameCount);
for (u32 i = 0; i < frameCount; ++i) { for (u32 i = 0; i < frameCount; ++i) {
// Uniform Buffer 需要 CPU 每帧更新,所以用 HOST_VISIBLE | HOST_COHERENT // CPU 每帧更新,所以用 HOST_VISIBLE | HOST_COHERENT
m_Buffers.push_back(CreateScope<Buffer>( m_Buffers.push_back(CreateScope<Buffer>(
device, allocator, size, device, allocator, size, usage,
vk::BufferUsageFlagBits::eUniformBuffer,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent)); vk::MemoryPropertyFlagBits::eHostCoherent));
} }
+12
View File
@@ -0,0 +1,12 @@
//
// Created by atdunbg on 2026/9/23.
//
// VMA 实现编译单元:整个引擎唯一一份 vk_mem_alloc 实现。
//
// 静态链接 Vulkan loader(项目已链接 Vulkan::Vulkan),与 vulkan-hpp 的
// dynamic loader 互不干扰(各自持有函数表)。
//
#define VMA_STATIC_VULKAN_FUNCTIONS 1
#define VMA_IMPLEMENTATION
#include <vk_mem_alloc.h>
-6
View File
@@ -1,6 +0,0 @@
//
// Created by atdunbg on 2026/9/20.
//
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>
Submodule
+1
Submodule Lib/tracy added at 30997d5ca6
+8 -6
View File
@@ -1,17 +1,19 @@
#version 450 #version 450
// Shadow pass vertex:光源视角写深度。 // Shadow pass vertex:光源视角写深度。
// push constant = { lightVP, model },与 Forward pass 用同一份 // GPUDriven:模型矩阵经 gl_InstanceIndex 从实例缓冲取(与 Forward
// lightVP(UBO.lightSpace)和同一份 model(TransformComponent.world), // 共用同一份数据),push 只带 lightVP(全 pass 一次)。
// 保证阴影贴图与主视角几何严格一致。
layout(location = 0) in vec3 inPosition; layout(location = 0) in vec3 inPosition;
layout(push_constant) uniform ShadowPush { layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer {
mat4 models[];
} objects;
layout(push_constant) uniform PushConstants {
mat4 lightVP; mat4 lightVP;
mat4 model;
} pc; } pc;
void main() { void main() {
gl_Position = pc.lightVP * pc.model * vec4(inPosition, 1.0); gl_Position = pc.lightVP * objects.models[gl_InstanceIndex] * vec4(inPosition, 1.0);
} }
+70 -15
View File
@@ -10,47 +10,102 @@ struct Light {
layout(location = 0) in vec2 fragUV; layout(location = 0) in vec2 fragUV;
layout(location = 1) in vec3 fragNormal; layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec3 fragWorldPos; layout(location = 2) in vec3 fragWorldPos;
layout(location = 3) in vec3 fragLightSpacePos; layout(location = 3) in float fragViewDepth;
layout(location = 4) in vec2 fragVelocity;
layout(location = 5) in vec3 fragTangent;
layout(location = 0) out vec4 outColor; layout(location = 0) out vec4 outColor;
layout(location = 1) out vec2 outVelocity; // TAA velocity 附件
layout(set = 0, binding = 0) uniform UniformBufferObject { layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view; mat4 view;
mat4 proj; mat4 proj;
mat4 lightSpace; mat4 lightSpace[3]; // CSM 各级联光空间 VP
vec4 cascadeSplits; // xyz = 级联 0..2 远边界(视图深度)
vec4 ambientColor; vec4 ambientColor;
Light lights[MAX_LIGHTS]; Light lights[MAX_LIGHTS];
mat4 projNoJitter;
mat4 prevView;
mat4 prevProj;
vec4 cameraPos;
} ubo; } ubo;
layout(set = 1, binding = 0) uniform sampler2D texSampler; // 材质纹理槽(binding = Material::Slot)
layout(set = 2, binding = 0) uniform sampler2D shadowMap; layout(set = 1, binding = 0) uniform sampler2D albedoMap;
layout(set = 1, binding = 1) uniform sampler2D normalMap;
layout(set = 1, binding = 2) uniform sampler2D specularMap;
layout(set = 1, binding = 3) uniform sampler2D emissiveMap;
// 第 0 盏灯(KeyLight)的阴影因子:0.3 影中 / 1.0 受光。 layout(set = 2, binding = 0) uniform sampler2D shadowMap0; // CSM 级联 0(最近)
// 采样器为 clamp-to-border 白色:光视锥外自动 1.0(无阴影)。 layout(set = 2, binding = 1) uniform sampler2D shadowMap1; // 级联 1
float ShadowFactor(vec3 N) { layout(set = 2, binding = 2) uniform sampler2D shadowMap2; // 级联 2(最远)
// NDC [-1,1] → UV [0,1](ortho 的 w=1,无需透视除法)
vec2 uv = fragLightSpacePos.xy * 0.5 + 0.5; // 第 0 盏灯(KeyLight)的 CSM 阴影因子:0.3 影中 / 1.0 受光。
float closest = texture(shadowMap, uv).r; // 级联按视图深度选择;采样器均 clamp-to-border 白色:
// 光视锥外自动 1.0(无阴影),省出界分支。
float ShadowFactor(vec3 N, float viewDepth) {
// 级联选择:第一个 viewDepth < split 的级联
int cascade = 2;
for (int i = 0; i < 3; ++i) {
if (viewDepth < ubo.cascadeSplits[i]) { cascade = i; break; }
}
vec4 ls = ubo.lightSpace[cascade] * vec4(fragWorldPos, 1.0);
vec3 proj = ls.xyz / ls.w;
// NDC [-1,1] → UV [0,1](ortho 的 w=1)
vec2 uv = proj.xy * 0.5 + 0.5;
float closest;
if (cascade == 0) closest = texture(shadowMap0, uv).r;
else if (cascade == 1) closest = texture(shadowMap1, uv).r;
else closest = texture(shadowMap2, uv).r;
// 斜率缩放 bias:掠射面用更大的 bias 压 acne // 斜率缩放 bias:掠射面用更大的 bias 压 acne
vec3 L = normalize(-ubo.lights[0].direction.xyz); vec3 L = normalize(-ubo.lights[0].direction.xyz);
float bias = max(0.004 * (1.0 - dot(N, L)), 0.0012); float bias = max(0.004 * (1.0 - dot(N, L)), 0.0012);
return (fragLightSpacePos.z - bias > closest) ? 0.3 : 1.0; return (proj.z - bias > closest) ? 0.3 : 1.0;
}
// 法线贴图 → 世界空间法线(TBN 构建)。
// 无切线数据(零向量)或平法线贴图时退回几何法线。
vec3 MappedNormal(vec3 geomN, vec3 T, vec2 uv) {
if (dot(T, T) < 1e-4) return geomN;
vec3 N = geomN;
vec3 nT = normalize(T - N * dot(N, T)); // 正交化
vec3 B = cross(N, nT);
vec3 sampled = texture(normalMap, uv).xyz * 2.0 - 1.0;
if (dot(sampled, sampled) < 1e-4) return geomN;
return normalize(nT * sampled.x + B * sampled.y + N * sampled.z);
} }
void main() { void main() {
vec3 albedo = texture(texSampler, fragUV).rgb; vec3 albedo = texture(albedoMap, fragUV).rgb;
vec3 N = normalize(fragNormal); float specIntensity = texture(specularMap, fragUV).r;
vec3 emissive = texture(emissiveMap, fragUV).rgb;
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, fragUV);
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
vec3 result = ubo.ambientColor.rgb * albedo; vec3 result = ubo.ambientColor.rgb * albedo;
for (int i = 0; i < MAX_LIGHTS; ++i) { for (int i = 0; i < MAX_LIGHTS; ++i) {
vec3 L = normalize(-ubo.lights[i].direction.xyz); vec3 L = normalize(-ubo.lights[i].direction.xyz);
float diff = max(dot(N, L), 0.0); float diff = max(dot(N, L), 0.0);
float shade = (i == 0) ? ShadowFactor(N) : 1.0; // Blinn-Phong 高光(半程向量;specular 图做强度系数)
result += ubo.lights[i].color.rgb * ubo.lights[i].color.a * diff * albedo * shade; vec3 H = normalize(L + V);
float spec = pow(max(dot(N, H), 0.0), 32.0) * specIntensity;
float shade = (i == 0) ? ShadowFactor(N, fragViewDepth) : 1.0;
result += ubo.lights[i].color.rgb * ubo.lights[i].color.a *
(albedo * diff + vec3(spec)) * shade;
} }
// 自发光不经光照/阴影直接叠加
result += emissive;
outColor = vec4(result, 1.0); outColor = vec4(result, 1.0);
outVelocity = fragVelocity;
} }
+40 -11
View File
@@ -10,34 +10,63 @@ struct Light {
layout(location = 0) in vec3 inPosition; layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal; layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inUV; layout(location = 2) in vec2 inUV;
layout(location = 3) in vec3 inTangent;
layout(location = 0) out vec2 fragUV; layout(location = 0) out vec2 fragUV;
layout(location = 1) out vec3 fragNormal; layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec3 fragWorldPos; layout(location = 2) out vec3 fragWorldPos;
layout(location = 3) out vec3 fragLightSpacePos; layout(location = 3) out float fragViewDepth; // CSM:视图深度(级联选择)
layout(location = 4) out vec2 fragVelocity; // TAA:clip 空间运动向量
layout(location = 5) out vec3 fragTangent; // 世界空间切线(法线贴图)
layout(push_constant) uniform PushConstants { // GPUDriven:模型矩阵从实例缓冲按实例索引取。
mat4 model; // 同一 mesh 的 N 个实体 = 1 次 drawIndexed(instanceCount=N),
} pc; // draw 侧用 firstInstance 指向该 batch 在实例数组中的起始下标。
layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer {
mat4 models[]; // 连续 mat4 数组(16B 对齐,std430/std140 布局一致)
} objects;
// 上一帧实例矩阵(与 ObjectBuffer 同下标 = 同一实例的上一帧姿态)
layout(std430, set = 0, binding = 2) readonly buffer PrevObjectBuffer {
mat4 prevModels[];
} prevObjects;
layout(set = 0, binding = 0) uniform UniformBufferObject { layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view; mat4 view;
mat4 proj; mat4 proj;
mat4 lightSpace; mat4 lightSpace[3]; // CSM 各级联光空间 VP
vec4 cascadeSplits; // xyz = 级联 0..2 远边界(视图深度)
vec4 ambientColor; vec4 ambientColor;
Light lights[MAX_LIGHTS]; Light lights[MAX_LIGHTS];
mat4 projNoJitter;
mat4 prevView;
mat4 prevProj;
vec4 cameraPos;
} ubo; } ubo;
void main() { void main() {
vec4 worldPos = pc.model * vec4(inPosition, 1.0); mat4 model = objects.models[gl_InstanceIndex];
vec4 worldPos = model * vec4(inPosition, 1.0);
gl_Position = ubo.proj * ubo.view * worldPos; gl_Position = ubo.proj * ubo.view * worldPos;
fragWorldPos = worldPos.xyz; // ---- TAA velocity:当前帧与上一帧的 clip 空间位置差(UV 空间)----
// 光空间坐标(含 Vulkan [0,1] 深度重映射,与 shadow pass 写入端共用同一矩阵) // 抖动只进光栅化 gl_Position,不进 velocity(否则抖动本身被当成运动)
fragLightSpacePos = (ubo.lightSpace * worldPos).xyz; vec4 currClip = ubo.projNoJitter * ubo.view * worldPos;
vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(inPosition, 1.0);
vec4 prevClip = ubo.prevProj * ubo.prevView * prevWorldPos;
fragVelocity = (currClip.xy / currClip.w - prevClip.xy / prevClip.w) * 0.5;
mat3 normalMatrix = mat3(transpose(inverse(pc.model))); fragWorldPos = worldPos.xyz;
// CSM:视图深度(正值,与 cascadeSplits 同尺度)
fragViewDepth = -(ubo.view * worldPos).z;
mat3 normalMatrix = mat3(transpose(inverse(model)));
fragNormal = normalize(normalMatrix * inNormal); fragNormal = normalize(normalMatrix * inNormal);
// 切线随模型变换(法线贴图 TBN;无切线数据保持零,frag 侧退回几何法线)
fragTangent = normalize(mat3(model) * inTangent);
// 零向量 normalize 产生 NaN,这里直接透传零,frag 判长度
fragTangent = dot(inTangent, inTangent) > 0.0 ? fragTangent : vec3(0.0);
fragUV = inUV; fragUV = inUV;
} }
+52
View File
@@ -0,0 +1,52 @@
#version 450
// TAA resolve:时域抗锯齿。
// 1. velocity 反推历史像素位置,取历史累积色
// 2. 邻域 clamp(3×3 AABB)压制鬼影:历史色超出当前帧邻域范围的部分丢弃
// 3. mix(cur, hist, alpha) 时域累积
// 输出写入 TAAColor,它同时也是下一帧的历史(ReadHistory 自引用)。
layout(location = 0) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D curImage; // 当前帧 HDR
layout(set = 0, binding = 1) uniform sampler2D velocityImage; // 运动向量(UV 空间)
layout(set = 0, binding = 2) uniform sampler2D historyImage; // 上一帧累积结果
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
layout(push_constant) uniform PushConstants {
float exposure; // 与其它全屏 pass 布局一致(本 pass 不用)
float alpha; // 历史权重(0.9 = 90% 历史)
float param1;
float param2;
} pc;
void main() {
vec4 cur = texture(curImage, fragUV);
vec2 vel = texture(velocityImage, fragUV).xy;
vec2 histUV = fragUV - vel;
vec3 result = cur.rgb;
// 历史采样:越出画面(相机快速平移)直接用当前帧,不累积
if (all(greaterThanEqual(histUV, vec2(0.0))) &&
all(lessThanEqual(histUV, vec2(1.0)))) {
vec3 hist = texture(historyImage, histUV).rgb;
// 邻域 clamp:当前帧 3×3 邻域的 min/max 包围盒
vec2 ts = 1.0 / vec2(textureSize(curImage, 0));
vec3 minC = cur.rgb;
vec3 maxC = cur.rgb;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
vec3 c = texture(curImage, fragUV + vec2(x, y) * ts).rgb;
minC = min(minC, c);
maxC = max(maxC, c);
}
}
vec3 clamped = clamp(hist, minC, maxC);
result = mix(cur.rgb, clamped, pc.alpha);
}
outColor = vec4(result, cur.a);
}
+28
View File
@@ -0,0 +1,28 @@
#version 450
// TemporalDiff:帧历史链路调试视图。
// 输出 |上一帧 HDR − 当前帧 HDR| × 增益:
// - 静止画面应接近纯黑(两帧内容一致)
// - 运动的实体边缘发光(时域差异可见)
// - 全屏噪声 / 错位残影 = 帧历史 barrier 或槽位镜像绑定错误
// 这是 RenderGraph 帧历史资源(ReadHistory)的验证消费端。
layout(location = 0) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D prevImage; // 帧历史(上一帧槽位镜像)
layout(set = 0, binding = 1) uniform sampler2D currImage; // 当前帧
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
layout(push_constant) uniform PushConstants {
float exposure; // 与其它全屏 pass 布局一致(本 pass 不用)
float gain; // 差异增益
float param1;
float param2;
} pc;
void main() {
vec3 prev = texture(prevImage, fragUV).rgb;
vec3 curr = texture(currImage, fragUV).rgb;
outColor = vec4(abs(prev - curr) * pc.gain, 1.0);
}
+13 -4
View File
@@ -10,13 +10,14 @@ layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D hdrImage; layout(set = 0, binding = 0) uniform sampler2D hdrImage;
layout(set = 0, binding = 1) uniform sampler2D bloomImage; layout(set = 0, binding = 1) uniform sampler2D bloomImage;
layout(set = 0, binding = 2) uniform sampler2D temporalDiffImage; // 帧历史差异(调试视图)
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878) // 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
layout(push_constant) uniform PushConstants { layout(push_constant) uniform PushConstants {
float exposure; // 曝光系数 float exposure; // 曝光系数
float aces; // >0.5 启用 ACES 曲线,否则纯 gamma float aces; // >0.5 启用 ACES 曲线,否则纯 gamma
float bloomStrength; // bloom 叠加强度(0 = 关闭) float bloomStrength; // bloom 叠加强度(0 = 关闭)
float _pad0; float debugView; // >0.5 显示帧历史差异视图(TemporalDiff)
} pc; } pc;
vec3 acesFilm(vec3 x) { vec3 acesFilm(vec3 x) {
@@ -24,9 +25,17 @@ vec3 acesFilm(vec3 x) {
} }
void main() { void main() {
// 调试视图:帧历史差异(静止=黑,运动边缘发光)
if (pc.debugView > 0.5) {
vec3 d = clamp(texture(temporalDiffImage, fragUV).rgb, 0.0, 1.0);
outColor = vec4(pow(d, vec3(1.0 / 2.2)), 1.0);
return;
}
// bloom 在 HDR 域叠加(半分辨率模糊图线性采样上采样) // bloom 在 HDR 域叠加(半分辨率模糊图线性采样上采样)
vec3 hdr = texture(hdrImage, fragUV).rgb vec4 hdrTex = texture(hdrImage, fragUV);
+ texture(bloomImage, fragUV).rgb * pc.bloomStrength; vec3 hdr = hdrTex.rgb + texture(bloomImage, fragUV).rgb * pc.bloomStrength;
float alpha = hdrTex.a;
hdr *= pc.exposure; hdr *= pc.exposure;
@@ -34,5 +43,5 @@ void main() {
? acesFilm(hdr) ? acesFilm(hdr)
: clamp(hdr, 0.0, 1.0); : clamp(hdr, 0.0, 1.0);
outColor = vec4(pow(mapped, vec3(1.0 / 2.2)), 1.0); outColor = vec4(pow(mapped, vec3(1.0 / 2.2)) * alpha, alpha);
} }