feat: ImGui docking Editor + 显示链/帧同步/输入语义重构

- App/editor: Hierarchy/Inspector/Viewport/Stats 四面板 + DockBuilder 默认布局,main.cpp 瘦身为场景搭建与生命周期
- 显示链: Tonemap→PresentTarget 经 ImGui::Image 呈现(零色偏),backbuffer 仅承载 ImGui;Direct/Viewport 双显示模式 + 面板分辨率上报
- 帧首同步 BeginFrameSync: resize/vsync/尺寸/模式意图统一帧首落定(修描述符悬垂);修复 vsync 意图未落 swapchain 导致的每帧重建交换链
- 输入: Editor 每帧判定键盘/鼠标归属(打字让位、Viewport hover/focus 放行、Fullscreen 全放行)
- 材质: roughness 独立槽位 + 非 glTF 金属/粗糙贴图映射;透明队列 depthWrite=FALSE + translucent 独立 shader + scratch 清帧
- FBX: 非蒙皮网格烘焙节点链全局变换(Z-up 旋转/单位换算落顶点)
- Hierarchy 行 ImGui ID 以 UUID 作用域(重名不冲突、改名不丢选中)
This commit is contained in:
2026-09-30 00:24:53 +08:00
parent b76922824c
commit dd7c67a116
57 changed files with 3067 additions and 622 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ namespace Engine {
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
virtual void OnRenderOverlay() {}
virtual void OnImGuiRender() {}
// 主循环结束后、析构前(保存、清理场景等)
virtual void OnShutdown() {}
+13
View File
@@ -63,8 +63,15 @@ namespace Engine {
// 输入事件系统
bool IsKeyPressed(int key) const;
// UI 捕获鼠标(ImGui 悬停)时返回 false——面板上的按键归 ImGui
bool IsMouseButtonPressed(int button) const;
void GetMouseDelta(float& dx, float& dy) const;
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
void SetCursorCaptured(bool captured);
bool IsCursorCaptured() const { return m_CursorCaptured; }
private:
@@ -80,6 +87,12 @@ namespace Engine {
CaptureFn m_KeyboardCapture;
CaptureFn m_MouseCapture;
// 光标捕获状态与鼠标增量参考点(GetMouseDelta 为 const,用 mutable)
bool m_CursorCaptured = false;
mutable bool m_FirstMouse = true;
mutable double m_LastMouseX = 0.0;
mutable double m_LastMouseY = 0.0;
// GLFW 静态回调(通过 window user pointer 找回 this)
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
+33
View File
@@ -27,6 +27,8 @@ namespace Engine::ECS {
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
struct IdComponent {
UUID uuid;
operator UUID() const { return uuid; }
};
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
@@ -83,6 +85,37 @@ namespace Engine::ECS {
ModelRef model;
};
// 骨骼动画播放状态(纯数据 + 便捷控制)。
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
struct AnimationComponent {
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true;
bool loop = true;
f32 speed = 1.0f;
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
// ---- crossfade(clip 切换平滑过渡)----
// 来源 clip 淡出期间与目标 clip 并行采样,局部 TRS 层混合
//(palette 数量不变,只是求值端多采一组姿态)
int fadeFrom = -1; // 来源 clip 下标(-1 = 无过渡)
f32 fadeFromTime = 0.0f; // 来源 clip 播放头
f32 fadeElapsed = 0.0f; // 已过渡时长
f32 fadeDuration = 0.3f; // 过渡总时长(秒)
// 切换 clip(带 crossfade)。越界/空 clip 保护在消费端。
void Play(int index, f32 fadeSeconds = 0.3f)
{
if (index == clipIndex) return;
fadeFrom = clipIndex;
fadeFromTime = time;
fadeElapsed = 0.0f;
fadeDuration = fadeSeconds;
clipIndex = index;
time = 0.0f;
}
};
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent {
Camera camera;
+1 -1
View File
@@ -50,7 +50,7 @@ namespace Engine::ECS {
// ---- 实体 ----
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
EntityHandle CreateEntity(std::string name = {});
EntityHandle CreateEntity(std::string name = "new entity");
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
+14
View File
@@ -18,6 +18,8 @@
#pragma once
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h> // kMaxFramesInFlight
#include <imgui.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
@@ -35,6 +37,17 @@ namespace Engine {
ImGuiLayer(const ImGuiLayer&) = delete;
ImGuiLayer& operator=(const ImGuiLayer&) = delete;
// ---- 场景显示纹理(RG PresentTarget → ImGui::Image 采样)----
// RG 物理镜像按帧槽位各一份,每槽位各注册一个描述符;
// 重编译(首次/resize)后 view 变化,Renderer 调 Update 重注册。
// 返回值直接传给 ImGui::Image(无场景纹理时为 ImTextureID_Invalid)
ImTextureID GetSceneTexture(u32 slot) const
{
return slot < kMaxFramesInFlight
? m_SceneTextures[slot] : ImTextureID_Invalid;
}
void UpdateSceneTexture(u32 slot, vk::ImageView view);
// ---- 每帧三段式(由 Application::Run 驱动)----
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
@@ -57,6 +70,7 @@ namespace Engine {
Device& m_Device;
vk::DescriptorPool m_Pool;
bool m_Enabled = true;
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
};
} // namespace Engine
@@ -98,6 +98,8 @@ namespace Engine::RG {
// 按句柄取当前帧槽位的物理资源(image/view)
vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const;
// 物理尺寸(导入资源 = SetImportedExtent 设定值;创建资源 = 物理镜像)
vk::Extent2D Extent(TextureHandle h) const;
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
@@ -129,6 +131,7 @@ namespace Engine::RG {
// 导入资源每帧绑定
vk::Image importedImage{};
vk::ImageView importedView{};
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
struct SlotState {
@@ -207,6 +210,11 @@ namespace Engine::RG {
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
// 导入资源的物理尺寸(swapchain 重建后同步更新)。pass 渲染区域
// 跟实际附件尺寸走:backbuffer = 窗口尺寸,与目标尺寸(场景渲染
// 分辨率)解耦——Editor 视口模式下二者不同
void SetImportedExtent(TextureHandle h, vk::Extent2D extent);
// 指定帧槽位的物理镜像 view(Compile 后有效)。
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
+4 -3
View File
@@ -22,7 +22,7 @@ namespace Engine {
class Camera {
public:
// 输入驱动:WASD/Space/Shift 移动 + 鼠标视角(需 SetMouseLookEnabled)。
// 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。
// 全部写回传入的 transform,有变更才 Compose。
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime);
@@ -35,14 +35,15 @@ namespace Engine {
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
// 鼠标视角默认关闭(需要先做 cursor capture 再打开)
// 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获),
// 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
private:
f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = false;
bool m_MouseLookEnabled = true;
};
} // namespace Engine
+18 -9
View File
@@ -7,13 +7,19 @@
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
// 5 材质参数 UBO(fragment,kParamsBinding)
// 5 roughness(独立粗糙度图,R 通道——FBX/OBJ specular 工作流,
// glTF 合图工作流不用此槽)
// 6 材质参数 UBO(fragment,kParamsBinding)
//
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
// 因子(metallicFactor/roughnessFactor)乘在 MR 贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1)
// OBJ(specular 工作流,独立 roughness 图或无图)→ metallic=0,
// roughness 因子按贴图有无/Ns 换算——金属度强制 0
// 因子(metallicFactor/roughnessFactor)乘在贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1),
// factors.z=0 → shader 直接读 MR.g 当粗糙度
// FBX(metalness 独立图 + roughness 独立图)→ metal 图落 MR 槽
// (灰度 R=G=B,.b 读金属度恰好正确),roughness 图落 Roughness
// 槽,factors.z=1 → shader 用 Roughness.r 替换 MR.g
// OBJ(specular 工作流)→ metallic=0,独立 roughness 图同 FBX;
// 无图时 roughness 因子按 Ns 换算
//
#ifndef HELLOVULKAN_MATERIAL_H
@@ -33,9 +39,11 @@ namespace Engine {
class Texture;
class UniformBuffer;
// 材质因子(乘 MR 贴图;无贴图时即最终值)
// 材质因子(乘贴图;无贴图时即最终值)
// x = metallic 因子 y = roughness 因子
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
struct MaterialParams {
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f}; // x=metallic y=roughness
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
};
class Material {
@@ -47,8 +55,9 @@ namespace Engine {
MetallicRoughness = 2,
Emissive = 3,
Ao = 4,
kSlotCount = 5, // 纹理槽数
kParamsBinding = 5, // 参数 UBO binding
Roughness = 5, // 独立粗糙度图(R 通道)
kSlotCount = 6, // 纹理槽数
kParamsBinding = 6, // 参数 UBO binding
};
Material(Device& device, MemoryAllocator& allocator,
+10
View File
@@ -8,6 +8,7 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Vulkan/Buffer.h>
#include <vulkan/vulkan.hpp>
#include <vector>
@@ -50,6 +51,15 @@ namespace Engine
// 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash)
u32 GetSlot() const { return m_Slot; }
// ---- MDI 兼容取数(per-mesh buffer:偏移恒 0,firstIndex 即全局)----
vk::Buffer GetVertexBuffer() const { return m_VertexBuffer->Get(); }
vk::Buffer GetIndexBuffer() const { return m_IndexBuffer->Get(); }
u32 GetVertexOffset() const { return 0; }
u32 GetGlobalFirstIndex(u32 lod) const
{
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].firstIndex;
}
// 局部空间包围球(构造时从顶点算出),视锥剔除用
struct Bounds {
glm::vec3 center{0.0f};
+83 -4
View File
@@ -10,7 +10,9 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
#include <array>
#include <filesystem>
#include <string>
@@ -41,10 +43,52 @@ namespace Engine
MaterialParams params;
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
};
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
// 关键帧(time 单位 = clip 原生 tick,运行时按 tps 换算秒)
struct PosKey { f32 time; glm::vec3 value; };
struct RotKey { f32 time; glm::quat value; };
struct SclKey { f32 time; glm::vec3 value; };
// 一条节点动画轨道:作用于 node 表中下标为 nodeIdx 的节点;
// 无轨道的分量运行时用节点静止姿态(localTransform)
struct AnimChannel {
u32 nodeIdx = 0;
std::vector<PosKey> positions;
std::vector<RotKey> rotations;
std::vector<SclKey> scales;
};
struct AnimationClip {
std::string name;
f32 duration = 0.0f; // 秒(已按 tps 换算)
f32 tps = 1.0f; // ticks per second(0 按 1,assimp 惯例)
std::vector<AnimChannel> channels;
};
// aiNode 树拍平表:骨骼 global 变换的累乘骨架(解析期 ProcessNode
// 原本丢弃 mTransformation,骨骼层级必须保留)
struct ModelNode {
std::string name;
i32 parent = -1; // 根节点 = -1
glm::mat4 localTransform{1.0f}; // 静止姿态 local TRS(assimp 行主→glm 列主已转置)
};
// 骨骼 = palette 槽位 ↔ 节点 ↔ 逆绑定矩阵
// palette[i] = global(bone.nodeIdx) × invBind
struct ModelBone {
std::string name;
u32 nodeIdx = 0;
glm::mat4 invBind{1.0f}; // aiBone mOffsetMatrix(绑定姿势全局逆)
};
struct ParsedModel {
std::string path;
bool isGltf = false;
std::vector<ParsedMesh> meshes;
std::vector<ModelNode> nodes;
std::vector<ModelBone> bones;
std::vector<AnimationClip> clips;
bool Valid() const { return !meshes.empty(); }
};
@@ -92,6 +136,13 @@ namespace Engine
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
const Mesh::Bounds& GetBounds() const { return m_Bounds; }
// ---- 骨骼动画运行时数据(无动画模型全部为空,零开销)----
bool HasAnimations() const { return !m_Clips.empty(); }
u32 GetBoneCount() const { return static_cast<u32>(m_Bones.size()); }
const std::vector<AnimationClip>& GetClips() const { return m_Clips; }
const std::vector<ModelNode>& GetNodes() const { return m_Nodes; }
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
private:
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
@@ -105,13 +156,33 @@ namespace Engine
vk::DescriptorSetLayout textureLayout);
void LoadModel(const std::string& path);
// aiNode 树拍平成 node 表(DFS 序,parent 下标回填)
static u32 FlattenNodes(const aiNode* node, i32 parent,
std::vector<ModelNode>& out);
// 每帧动画的 channel 轨道提取(nodeName → node 表下标)
static void ParseAnimations(const aiScene* scene,
const std::vector<ModelNode>& nodes,
std::vector<AnimationClip>& out);
static void ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir,
bool isGltf);
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& parentGlobal
= aiMatrix4x4());
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir,
bool isGltf);
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& globalTransform
= aiMatrix4x4());
// mesh 的 aiBone 权重灌进顶点 + 模型级骨骼表注册去重
static void CollectMeshBones(const aiMesh* mesh,
std::vector<Vertex>& vertices,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots);
void InitDefaultTextures();
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
@@ -121,10 +192,13 @@ namespace Engine
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot;params = metallic/roughness 因子
// paths 下标 = Material::Slot;params = metallic/roughness 因子;
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params,
bool transparent = false);
bool transparent = false,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides = {});
Device& m_Device;
MemoryAllocator& m_Allocator;
@@ -142,6 +216,11 @@ namespace Engine
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
std::string m_ModelPath;
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
// ---- 骨骼动画运行时数据(FromParsed/LoadModel 从解析产物 move)----
std::vector<ModelNode> m_Nodes;
std::vector<ModelBone> m_Bones;
std::vector<AnimationClip> m_Clips;
};
} // namespace Engine
@@ -0,0 +1,72 @@
//
// Created by atdunbg on 2026/9/29.
//
// BlitPass:显示模式适配器(kDisplayDirect 专用)。
// Direct 模式:vkCmdBlitImage 把 PresentTarget(tonemap 输出)拷进
// backbuffer,ImGui 随后 loadOp=Load 叠加 —— 场景铺满窗口的游戏形态。
// Viewport(Editor)模式:空转,backbuffer 由 ImGui 清屏独占,场景
// 经 ImGui::Image 呈现。
//
// 用 vkCmdBlitImage 而非着色器:同格式全屏拷贝零 shader 成本(声明
// TransferSrc/TransferDst use,layout/barrier 由 RG 推导)。Blit 不在
// rendering scope 内,直接录在 cmd 上,合法。
//
// 注:Blit 只在 Direct 模式运行,此时 PresentTarget 与 backbuffer 同为
// 目标尺寸(1:1),src/dst offset 均取本 pass 渲染区域即可。
//
#ifndef HELLOVULKAN_BLITPASS_H
#define HELLOVULKAN_BLITPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <array>
namespace Engine::Passes {
class BlitPass final : public RenderPass {
public:
BlitPass(FrameSceneData& frame, RG::TextureHandle src,
RG::TextureHandle dst)
: m_Frame(frame), m_Src(src), m_Dst(dst) {}
void Register(RG::RenderGraph& rg) override
{
rg.AddPass("Blit", [this](RG::RenderGraphContext& ctx) {
if (m_Frame.displayMode != kDisplayDirect) return; // Editor 模式空转
vk::ImageBlit region;
region.setSrcSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1})
.setDstSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1});
// src/dst 各按自己附件的物理尺寸取 offset(Direct 模式下
// 二者同为窗口尺寸;尺寸解耦时此写法也各自正确)
const vk::Extent2D srcExt = ctx.Extent(m_Src);
const vk::Extent2D dstExt = ctx.Extent(m_Dst);
const std::array<vk::Offset3D, 2> srcSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(srcExt.width),
static_cast<i32>(srcExt.height), 1}};
const std::array<vk::Offset3D, 2> dstSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(dstExt.width),
static_cast<i32>(dstExt.height), 1}};
region.setSrcOffsets(srcSpan).setDstOffsets(dstSpan);
ctx.cmd.blitImage(
ctx.Image(m_Src), vk::ImageLayout::eTransferSrcOptimal,
ctx.Image(m_Dst), vk::ImageLayout::eTransferDstOptimal,
{region}, vk::Filter::eLinear);
})
.Read(m_Src, RG::ResourceUse::TransferSrc)
.Write(m_Dst, RG::ResourceUse::TransferDst);
}
private:
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Src; // PresentTarget(tonemap 输出)
RG::TextureHandle m_Dst; // backbuffer
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_BLITPASS_H
@@ -28,6 +28,12 @@ namespace Engine::Passes {
void Register(RG::RenderGraph& rg) override;
private:
// 两段渲染共享的命令录制(shader/描述符绑定 + 几何/光栅/视口状态)。
// translucent=true 绑定 gl_FragDepth=1.0 的透明 frag 变体;
// 深度写与混合状态由各渲染实例作为初始态设置,见 ForwardPass.cpp
void RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd, bool translucent) const;
SceneGpu& m_Gpu;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
@@ -56,6 +56,7 @@ namespace Engine {
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容)
kDebugViewCount
};
@@ -11,6 +11,7 @@
#define HELLOVULKAN_IMGUIPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h> // FrameSceneData(displayMode 帧契约)
namespace Engine {
@@ -21,15 +22,22 @@ namespace Engine {
class ImGuiPass final : public RenderPass {
public:
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
//(EnableImGui 时机由派生类决定),执行期解引用取最新值
ImGuiPass(ImGuiLayer** layer, RG::TextureHandle backbuffer)
: m_Layer(layer), m_Backbuffer(backbuffer) {}
//(EnableImGui 时机由派生类决定),执行期解引用取最新值。
// presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
// loadOp 按 displayMode 分支:Direct = Load(blit 已铺场景),
// Viewport = Clear(唯一写入者,清"桌面底色")
ImGuiPass(ImGuiLayer** layer, FrameSceneData& frame,
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget)
: m_Layer(layer), m_Frame(frame), m_Backbuffer(backbuffer),
m_PresentTarget(presentTarget) {}
void Register(RG::RenderGraph& rg) override;
private:
ImGuiLayer** m_Layer;
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_PresentTarget;
};
} // namespace Passes
@@ -25,22 +25,36 @@ namespace Engine {
class ShaderManager;
class UniformBuffer;
class Cubemap;
class Buffer;
class MemoryAllocator;
namespace Passes {
class SceneGpu {
public:
SceneGpu(Device& device, DescriptorManager& descriptors,
SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer);
UniformBuffer& prevObjectBuffer,
UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer);
~SceneGpu();
SceneGpu(const SceneGpu&) = delete;
SceneGpu& operator=(const SceneGpu&) = delete;
// ---- MDI 间接绘制 ----
// CPU 打包的命令/count 落进本槽位 ring(Renderer 在 fence
// 等待后调用——与 UBO/SSBO 上传同一安全窗口)
void UploadIndirect(u32 frameSlot, const FrameSceneData& frame);
// 一次组绘制:bind 共享 megabuffer + drawIndexedIndirectCount
void DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
u32 viewIndex, const IndirectGroup& g);
// ---- set 布局(shader 契约,改绑定必须同步 shaders/)----
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer}
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer,
// SkinOffsets, SkinPalettes}
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube}
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
@@ -54,7 +68,16 @@ namespace Engine {
//(省 shader 出界分支)
vk::Sampler shadowSampler;
// ---- MDI per-slot ring ----
// 布局:[main | cascade0 | cascade1 | cascade2],每区
// kMaxInstances 条命令预算(每实例至多 1 条命令,必不越界)。
// count buffer 同分区(每组一个 u32,下标 = 组首命令下标)
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCmdBuffers;
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCountBuffers;
vk::ShaderEXT forwardVert{}, forwardFrag{};
// 透明变体:同 simple_shader + gl_FragDepth=1.0(见 frag 注释)
vk::ShaderEXT translucentFrag{};
vk::ShaderEXT shadowVert{}, shadowFrag{};
// RG 重编译后重绑阴影图(物理镜像按槽位各一份)
@@ -66,6 +89,7 @@ namespace Engine {
private:
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
};
@@ -1,9 +1,10 @@
//
// Created by atdunbg on 2026/9/23.
//
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 Backbuffer。
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 PresentTarget。
// 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
// Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。
// PresentTarget 是 offscreen 可采样纹理:由 ImGui::Image 呈现(输出位置
// 可自由摆放),backbuffer 不再接收场景画面。
//
#ifndef HELLOVULKAN_TONEMAPPASS_H
@@ -28,7 +29,8 @@ namespace Engine {
PostProcessParams& params,
FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer,
RG::TextureHandle temporalDiff,
RG::TextureHandle presentTarget,
RG::TextureHandle rawHdr, RG::TextureHandle velocity,
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
RG::TextureHandle shadowMap2);
@@ -46,7 +48,7 @@ namespace Engine {
RG::TextureHandle m_HDR;
RG::TextureHandle m_A;
RG::TextureHandle m_TemporalDiff;
RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_Target; // PresentTarget(offscreen,ImGui 采样呈现)
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
+42 -4
View File
@@ -58,7 +58,7 @@ namespace Engine {
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
void Render(ECS::Scene& scene, Assets::AssetManager& assets);
void Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt);
// ---- 组合根(Application)装配其他模块时需要的访问器 ----
Device& GetDevice() const { return *m_Device; }
@@ -72,7 +72,18 @@ namespace Engine {
// 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。
// 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。
void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; }
void SetImGuiLayer(ImGuiLayer* layer)
{
m_ImGuiLayer = layer;
if (layer) RefreshSceneTextures(); // 后端就绪:注册场景纹理
}
// ---- 场景显示纹理(tonemap 输出,ImGui::Image 可采样显示)----
// 按 m_CurrentFrame 取当帧槽位的描述符(UI 构建段与紧随的
// Render 同槽位)。无 ImGui 层时返回 nullptr
void* GetSceneTexture() const;
// 渲染分辨率(PresentTarget 尺寸,等比适配用)
vk::Extent2D GetRenderExtent() const { return m_Extent; }
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
f32 GetExposure() const;
@@ -115,13 +126,32 @@ namespace Engine {
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
f32 GetCollectMs() const; // CPU 收集+剔除段耗时
// ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)----
int GetDisplayMode() const { return m_DesiredDisplayMode; }
void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明)
// Editor 视口的期望渲染尺寸(像素;UI 面板每帧上报,帧首统一生效)
void SetViewportSize(u32 w, u32 h)
{
m_DesiredViewport = vk::Extent2D{w, h};
}
// 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前)
void SetVSync(bool vsync);
void SetVSync(bool vsync) { m_DesiredVSync = vsync; } // 帧首同步生效
// 帧首同步:窗口 resize / 视口尺寸 / 显示模式 / vsync 意图统一在
// UI 构建之前落定(waitIdle → 重编译 → 重绑描述符)。必须先于
// UI 构建段调用:本帧 draw data 引用的描述符与物理资源由此保证
// 全程新鲜(resize 中途换描述符会绑出已销毁句柄)
void BeginFrameSync();
private:
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
void HandleResize();
void HandleResize(bool recreateSwapchain);
void ReadGpuTimes(u32 frameSlot);
void RefreshSceneTextures(); // 物理 view → ImGui 描述符(重建后刷新)
// 期望渲染分辨率:Editor 视口 = 面板尺寸(未上报时回退 swapchain);
// Direct = swapchain 尺寸
vk::Extent2D DesiredRenderExtent() const;
Window& m_Window;
@@ -133,6 +163,8 @@ namespace Engine {
Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
Scope<UniformBuffer> m_SkinOffsetBuffer; // 实例→palette 基址 ring(SSBO,与实例序平行)
Scope<UniformBuffer> m_SkinPaletteBuffer; // 骨骼蒙皮矩阵 palette ring(SSBO,帧首 bump)
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
@@ -152,6 +184,7 @@ namespace Engine {
// ---- RenderGraph 资源句柄 ----
RG::TextureHandle m_Backbuffer{};
RG::TextureHandle m_PresentTarget{}; // tonemap 输出(offscreen,ImGui 采样)
RG::TextureHandle m_Depth{};
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
@@ -180,6 +213,11 @@ namespace Engine {
vk::Extent2D m_Extent{};
u32 m_CurrentFrame = 0;
// ---- 帧首同步的意图缓冲(UI 段写入,下一帧帧首落定)----
int m_DesiredDisplayMode = kDisplayDirect; // 默认直接渲染, 视口渲染需要手动指定一下
vk::Extent2D m_DesiredViewport{}; // Editor 视口渲染分辨率(面板上报)
bool m_DesiredVSync = true;
};
} // namespace Engine
+23 -1
View File
@@ -23,6 +23,7 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/SceneTypes.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <array>
#include <unordered_map>
@@ -36,8 +37,9 @@ namespace Engine {
public:
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
// dt 驱动骨骼动画采样推进(crossfade 计时)。
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent);
vk::Extent2D targetExtent, f32 dt);
FrameSceneData& Frame() { return m_Frame; }
@@ -59,6 +61,7 @@ namespace Engine {
void SetBloomEnabled(bool b) { m_BloomEnabled = b; }
void SetLodEnabled(bool b) { m_LodEnabled = b; }
void SetDebugView(int v) { m_DebugView = v; }
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
@@ -66,6 +69,12 @@ namespace Engine {
//(Forward UBO)共用同一组矩阵。
void ComputeCascades(const glm::vec3& shadowDir);
// MDI 打包:batch 按组键排序(megabuffer + 可选材质)后压成
// IndirectDrawCmd 连续数组 + IndirectGroup 区间表。
// byMaterial=true(Forward):每材质一次 bind + 一次 MDI;
// byMaterial=false(Shadow):只按 megabuffer 分组(depth-only)
static void PackIndirect(CullResult& view, bool byMaterial);
FrameSceneData m_Frame;
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
@@ -74,6 +83,7 @@ namespace Engine {
struct InstanceMatrices {
glm::mat4 cur;
glm::mat4 prev;
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
};
CullResult result;
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
@@ -108,6 +118,17 @@ namespace Engine {
std::unordered_map<u32, glm::mat4> m_PrevWorlds;
std::unordered_map<u32, glm::mat4> m_CurWorlds;
// ---- 骨骼动画 scratch(帧首重建/容量复用)----
// entKey → palette 区段基址(第 2.5 节预采样填,收集段查)
std::unordered_map<u32, u32> m_EntitySkinBase;
// 采样层 A(当前 clip)/ B(fade 来源 clip)与 global 累乘
std::vector<glm::vec3> m_PosePosA, m_PosePosB;
std::vector<glm::quat> m_PoseRotA, m_PoseRotB;
std::vector<glm::vec3> m_PoseSclA, m_PoseSclB;
std::vector<u8> m_TouchedA, m_TouchedB;
std::vector<glm::mat4> m_Globals;
bool m_SkinTruncWarned = false; // palette 预算溢出只告警一次
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
// ---- 运行时开关/调试视图状态(本类是唯一写入者)----
@@ -116,6 +137,7 @@ namespace Engine {
bool m_BloomEnabled = true;
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
int m_DebugView = 0;
int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态
};
} // namespace Engine
+57 -1
View File
@@ -18,6 +18,7 @@
namespace Engine {
class Mesh;
class Material;
// GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例
// 索引区间不同,不能并成一次 draw
@@ -28,10 +29,38 @@ namespace Engine {
u32 lod = 0; // 绘制用索引区间(Mesh::Draw)
};
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)
// MDI:一次 vkCmdDrawIndexedIndirectCount 消费的命令(CPU 侧镜像,
// 内存布局 = VkDrawIndexedIndirectCommand,20B)
struct IndirectDrawCmd {
u32 indexCount;
u32 instanceCount;
u32 firstIndex; // megabuffer 里的全局索引下标
u32 vertexOffset; // megabuffer 里的顶点元素偏移
u32 firstInstance; // 实例矩阵数组下标
};
static_assert(sizeof(IndirectDrawCmd) == 20, "布局必须镜像 VkDrawIndexedIndirectCommand");
// MDI:共享同一 megabuffer(同 Model)的一组命令——
// 一次 bindVertex/IndexBuffer +(Forward)一次材质 Set1 绑定 +
// 一次 drawIndexedIndirectCount。组内命令在 indirect buffer 中连续
struct IndirectGroup {
const Mesh* mesh = nullptr; // 取共享 VB/IB 句柄(组内同一份)
Material* material = nullptr; // shadow 组为 null(depth-only 无材质)
u32 firstCommand = 0; // 命令区起始下标(count 同下标)
u32 commandCount = 0;
};
// indirect buffer 的视图分区:[main | cascade0 | cascade1 | cascade2],
// 每区 kMaxInstances 条命令预算(每实例至多 1 条,必不越界)
constexpr u32 kIndirectViewMain = 0;
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)。
// batches = 逐实例批次(透明队列仍逐批 draw);indirect = MDI 打包产物
struct CullResult {
std::vector<InstanceBatch> batches;
u32 instanceCount = 0;
std::vector<IndirectDrawCmd> indirectCommands;
std::vector<IndirectGroup> indirectGroups;
};
constexpr u32 kMaxLights = 4;
@@ -43,6 +72,13 @@ namespace Engine {
constexpr u32 kMaxCascades = 3; // CSM 级联数
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
// 蒙皮矩阵预算(palette 总槽位数/帧):每动画实体一段(按实体分配,
// 不按模型去重——同模型两实体可不同播放状态)。4096×64B = 256KB/槽位
constexpr u32 kMaxSkinMatrices = 4096;
// palette slot 0 固定 identity:无骨骼顶点(weights=(1,0,0,0), idx=0)
// 直通它 → 静态模型蒙皮零影响
constexpr u32 kIdentityPaletteSlot = 0;
// 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。
// 字段未用部分置零——shader 按类型分支读取
struct LightData {
@@ -70,6 +106,14 @@ namespace Engine {
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
};
// 显示模式(帧契约 displayMode 字段;ImGui 经 Renderer 门面写入):
// Direct = 全屏直出:Blit pass 把 PresentTarget 拷进 backbuffer,
// ImGui 叠加其上(游戏形态)
// Viewport = Editor 视口:backbuffer 只画 ImGui(清屏),场景经
// ImGui::Image 呈现;渲染分辨率跟随 ImGui 面板尺寸
inline constexpr int kDisplayDirect = 0;
inline constexpr int kDisplayViewport = 1;
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
// 复用成员容量:clear() 不缩容,稳态零每帧堆分配。
struct FrameSceneData {
@@ -87,6 +131,15 @@ namespace Engine {
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
std::vector<glm::mat4> prevInstanceModels;
// ---- 蒙皮(与 instanceModels 平行的 per-instance 间接索引)----
// skinOffsets[i] = 第 i 个实例的 palette 区段起点(kMaxSkinMatrices
// 全局数组内下标);无动画实体指向 slot0 identity
std::vector<u32> skinOffsets;
// palette 区段池:帧首 bump allocator 按动画实体分配
// [skinBase .. skinBase+boneCount) = 该实体全部骨骼的蒙皮矩阵
std::vector<glm::mat4> skinPalettes;
CullResult mainView; // 相机视锥 → Forward pass
// 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
@@ -102,10 +155,13 @@ namespace Engine {
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
bool lodEnabled = true; // 关 = 实例收集恒选 LOD0(绘制原网格)
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
int displayMode = kDisplayViewport; // 显示模式(Blit 路由 + ImGui loadOp)
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警
bool instancesTruncated = false;
// palette 预算溢出(kMaxSkinMatrices):动画实体 palette 被截断
bool skinPaletteTruncated = false;
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
};
+20 -2
View File
@@ -16,6 +16,14 @@ namespace Engine {
glm::vec2 uv;
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace)
// ---- 骨骼蒙皮属性 ----
// 无骨骼资产保持默认值(weights=(1,0,0,0) + palette[0]=identity)
// → skin 矩阵 = identity,蒙皮路径对静态模型零影响。
// weights 必须带默认初始化器:聚合初始化 Vertex{pos,normal,uv,tangent}
// 会把未列出的成员零初始化 → skin=mat4(0) → 顶点全部塌缩到原点
glm::uvec4 boneIds{0, 0, 0, 0};
glm::vec4 weights{1.0f, 0.0f, 0.0f, 0.0f};
static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
vk::VertexInputBindingDescription2EXT binding;
binding.setBinding(0)
@@ -25,8 +33,8 @@ namespace Engine {
return binding;
}
static std::array<vk::VertexInputAttributeDescription2EXT, 4> GetAttributeDescriptions() {
std::array<vk::VertexInputAttributeDescription2EXT, 4> attrs;
static std::array<vk::VertexInputAttributeDescription2EXT, 6> GetAttributeDescriptions() {
std::array<vk::VertexInputAttributeDescription2EXT, 6> attrs;
attrs[0].setLocation(0)
.setBinding(0)
@@ -48,6 +56,16 @@ namespace Engine {
.setFormat(vk::Format::eR32G32B32Sfloat)
.setOffset(offsetof(Vertex, tangent));
attrs[4].setLocation(4)
.setBinding(0)
.setFormat(vk::Format::eR32G32B32A32Uint)
.setOffset(offsetof(Vertex, boneIds));
attrs[5].setLocation(5)
.setBinding(0)
.setFormat(vk::Format::eR32G32B32A32Sfloat)
.setOffset(offsetof(Vertex, weights));
return attrs;
}
};
+3 -1
View File
@@ -26,7 +26,9 @@ namespace Engine {
Buffer& operator=(const Buffer&) = delete;
vk::Buffer Get() const { return m_Buffer; }
vk::DeviceSize GetSize() const { return m_Size; }
vk::DeviceSize Size() const { return m_Size; }
// 持久映射指针(非 host visible 为 nullptr;诊断读回用)
void* MappedPtr() const { return m_MappedPtr; }
// 只有 HOST_VISIBLE 的 Buffer 能调
void Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset = 0) const;
+4
View File
@@ -43,6 +43,8 @@ public:
bool IsDynamicRenderingSupported() const { return m_DynamicRenderingSupported; }
bool IsShaderObjectSupported() const { return m_ShaderObjectSupported; }
bool IsSynchronization2Supported() const { return m_Synchronization2Supported; }
// MDI:drawCount>1 的 drawIndexedIndirect(Count) 必需(core 特性位)
bool IsMultiDrawIndirectSupported() const { return m_MultiDrawIndirectSupported; }
private:
void CreateInstance();
@@ -72,6 +74,8 @@ private:
bool m_DynamicRenderingSupported = false;
bool m_ShaderObjectSupported = false;
bool m_Synchronization2Supported = false;
bool m_MultiDrawIndirectSupported = false;
bool m_DrawIndirectCountSupported = false;
std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" };
std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+14 -4
View File
@@ -61,7 +61,10 @@ namespace Engine {
void Application::EnableImGui()
{
if (m_ImGuiLayer) return;
if (m_ImGuiLayer) {
ENGINE_WARN("ImGui layer already enabled, do not enable twice!");
return;
}
m_ImGuiLayer = CreateScope<ImGuiLayer>(GetWindow(), *m_Renderer);
m_Renderer->SetImGuiLayer(m_ImGuiLayer.get());
@@ -108,24 +111,31 @@ namespace Engine {
m_Scene->FlushDestroyQueue();
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜
// 帧首同步:窗口 resize / 渲染尺寸(面板跟随)/ 显示模式 / vsync
// 统一在 UI 构建"之前"落定——此处可能重建 swapchain 与 RG 目标
// 并重注册 ImGui 场景纹理;若挪到 UI 构建之后,本帧 draw data
// 会引用刚被销毁的描述符(validation: Invalid VkDescriptorSet)
m_Renderer->BeginFrameSync();
// ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ----
if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled())
{
const auto uiStart = std::chrono::steady_clock::now();
m_ImGuiLayer->NewFrame();
OnRenderOverlay();
OnImGuiRender();
m_ImGuiLayer->BuildDrawData();
const auto uiEnd = std::chrono::steady_clock::now();
}
// ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) →
// Present,draw data 在 ImGui pass 里才真正提交 GPU ----
m_Renderer->Render(*m_Scene, *m_Assets);
m_Renderer->Render(*m_Scene, *m_Assets, m_DeltaTime);
// ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ----
// 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU
if (!m_VSync) {
// TargetFps <= 0 表示不限帧(跳过 sleep,避免 1e6/0 除零 UB)
if (!m_VSync && m_TargetFps > 0) {
const auto deadline = frameStart +
std::chrono::microseconds(static_cast<long long>(
1e6 / m_TargetFps));
+41 -17
View File
@@ -81,12 +81,19 @@ namespace Engine
void Window::WindowFocusCallback(GLFWwindow* window, int focused)
{
const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return;
Event e;
e.type = focused ? EventType::WindowFocus : EventType::WindowLostFocus;
self->m_EventCallback(e);
// 失焦时释放光标捕获(切窗口不会留下隐形被捕获的光标;
// 相机下一帧按右键状态自行决定是否重新捕获)
if (!focused && self->m_CursorCaptured)
{
self->SetCursorCaptured(false);
}
}
void Window::WindowCloseCallback(GLFWwindow* window)
@@ -120,7 +127,7 @@ namespace Engine
glfwTerminate();
return;
}
glfwSetWindowOpacity(m_Window, 0.5f);
glfwSetWindowOpacity(m_Window, 0.0f);
ENGINE_INFO("Window created: {} ({}x{})", title, width, height);
@@ -191,32 +198,49 @@ namespace Engine
return glfwGetKey(m_Window, key) == GLFW_PRESS;
}
bool Window::IsMouseButtonPressed(const int button) const
{
// UI 捕获鼠标时(ImGui 悬停)不占用:面板上的点击归 ImGui。
// 已捕获光标(自由视角中)时绕过——虚拟光标位置无意义
if (!m_CursorCaptured && m_MouseCapture && m_MouseCapture()) return false;
return glfwGetMouseButton(m_Window, button) == GLFW_PRESS;
}
void Window::SetCursorCaptured(const bool captured)
{
if (m_CursorCaptured == captured) return;
m_CursorCaptured = captured;
glfwSetInputMode(m_Window, GLFW_CURSOR,
captured ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
// DISABLED 模式光标是虚拟无界坐标,与上次普通坐标不连续——
// 重置参考点,下一帧只建立基准不出增量(防视角跳变)
m_FirstMouse = true;
}
void Window::GetMouseDelta(float& dx, float& dy) const
{
double mx, my;
glfwGetCursorPos(m_Window, &mx, &my);
static double lastX = mx;
static double lastY = my;
static bool first = true;
if (first)
// 首帧(或捕获切换后):只建立参考点
if (m_FirstMouse)
{
lastX = mx;
lastY = my;
first = false;
m_LastMouseX = mx;
m_LastMouseY = my;
m_FirstMouse = false;
dx = dy = 0.0f;
return;
}
dx = static_cast<float>(mx - lastX);
dy = static_cast<float>(my - lastY);
dx = static_cast<float>(mx - m_LastMouseX);
dy = static_cast<float>(my - m_LastMouseY);
m_LastMouseX = mx;
m_LastMouseY = my;
lastX = mx;
lastY = my;
// UI 捕获鼠标时 delta 归零;lastX/Y 照常更新,避免释放后跳变
if (m_MouseCapture && m_MouseCapture())
// UI 捕获鼠标(ImGui 悬停)时 delta 归零;参考点照常更新,避免
// 释放后跳变。光标被相机捕获时绕过——虚拟光标可能悬在 ImGui
// 面板上方,若被归零视角会卡死
if (!m_CursorCaptured && m_MouseCapture && m_MouseCapture())
{
dx = dy = 0.0f;
}
+1 -3
View File
@@ -16,9 +16,7 @@ namespace Engine::ECS {
// 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义)
m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()});
m_Registry.emplace<TransformComponent>(e);
if (!name.empty()) {
m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)});
}
m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)});
return EntityHandle{this, e};
}
+25
View File
@@ -95,6 +95,11 @@ namespace Engine {
// 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构)
m_Device.GetDevice().waitIdle();
// 场景纹理描述符先归还(后端 shutdown 前必须完成)
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
UpdateSceneTexture(slot, vk::ImageView{});
}
ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
@@ -104,6 +109,26 @@ namespace Engine {
ENGINE_INFO("ImGuiLayer shut down");
}
void ImGuiLayer::UpdateSceneTexture(u32 slot, vk::ImageView view)
{
if (slot >= kMaxFramesInFlight) return;
// 旧描述符先归还(池带 eFreeDescriptorSet;resize 重建时 GPU 已
// waitIdle,无在飞引用)
if (m_SceneTextures[slot]) {
ImGui_ImplVulkan_RemoveTexture(
(VkDescriptorSet)(intptr_t)m_SceneTextures[slot]);
m_SceneTextures[slot] = ImTextureID_Invalid;
}
if (view) {
// 后端自有采样器(linear):只需提供 view 与采样期 layout——
// RG 已保证 ImGuiPass 执行期处于 eShaderReadOnlyOptimal
m_SceneTextures[slot] = (ImTextureID)(intptr_t)
ImGui_ImplVulkan_AddTexture(
(VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
}
void ImGuiLayer::NewFrame()
{
ImGui_ImplVulkan_NewFrame();
+20 -3
View File
@@ -7,6 +7,7 @@
#include <Engine/Core/Log.h>
#include <algorithm>
#include <ranges>
#include <set>
#include <stdexcept>
@@ -41,8 +42,8 @@ namespace Engine::RG {
ReleasePhysical();
const auto& vkDevice = m_Device.GetDevice();
for (auto& [key, images] : m_Pool) {
for (auto& img : images) {
for (auto &images: m_Pool | std::views::values) {
for (const auto& img : images) {
if (!img->valid) continue;
vkDevice.destroyImageView(img->view);
m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放
@@ -57,7 +58,7 @@ namespace Engine::RG {
{
CheckMutable();
TextureHandle h{static_cast<u32>(m_Textures.size()), 1};
const TextureHandle h{static_cast<u32>(m_Textures.size()), 1};
TextureEntry entry;
entry.name = std::move(name);
@@ -101,6 +102,15 @@ namespace Engine::RG {
entry.importedView = view;
}
void RenderGraph::SetImportedExtent(TextureHandle h, vk::Extent2D extent)
{
auto& entry = GetEntry(h);
if (!entry.imported) {
ENGINE_FATAL("RenderGraph: SetImportedExtent on non-imported resource '{}'", entry.name);
}
entry.importedExtent = extent;
}
// ---------- Pass 声明 ----------
RenderGraph::PassBuilder RenderGraph::AddPass(std::string name, ExecuteFn exec)
@@ -832,6 +842,13 @@ namespace Engine::RG {
return entry.physical[frame.frameSlot]->image;
}
vk::Extent2D RenderGraphContext::Extent(TextureHandle h) const
{
const auto& entry = graph->GetEntry(h);
if (entry.imported) return entry.importedExtent;
return entry.physical[frame.frameSlot]->extent;
}
u32 RenderGraphContext::HistorySlot(u32 frameSlot) const
{
return graph->HistorySlot(frameSlot);
+18 -3
View File
@@ -14,21 +14,36 @@ namespace Engine {
void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime)
{
// 鼠标旋转:直接写实体旋转(yaw → rotation.y,pitch → rotation.x)
if (m_MouseLookEnabled)
// 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角,
// 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关
const bool wantLook = m_MouseLookEnabled &&
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT);
if (wantLook)
{
if (!window.IsCursorCaptured())
{
window.SetCursorCaptured(true);
}
f32 mouseDX = 0, mouseDY = 0;
window.GetMouseDelta(mouseDX, mouseDY);
if (mouseDX != 0 || mouseDY != 0)
{
glm::vec3 rot = t.GetRotation();
rot.y += mouseDX * m_MouseSensitivity;
// 右手系绕 Y 正角度 = 向左转,鼠标右移(DX>0)应看向右
// → yaw 取负。pitch 正角度 = 抬头,鼠标下移(DY>0)低头
// → rot.x 直接用负号,两者符号已核对一致
rot.y -= mouseDX * m_MouseSensitivity;
rot.x = glm::clamp(
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
t.SetRotation(rot);
}
}
else if (window.IsCursorCaptured())
{
window.SetCursorCaptured(false);
}
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
+293 -38
View File
@@ -22,6 +22,19 @@
namespace Engine {
namespace {
// assimp 行主矩阵 → glm 列主(转置搬运;骨骼 node TRS / invBind 共用)
glm::mat4 ToGlm(const aiMatrix4x4& m)
{
glm::mat4 out;
out[0][0] = m.a1; out[1][0] = m.a2; out[2][0] = m.a3; out[3][0] = m.a4;
out[0][1] = m.b1; out[1][1] = m.b2; out[2][1] = m.b3; out[3][1] = m.b4;
out[0][2] = m.c1; out[1][2] = m.c2; out[2][2] = m.c3; out[3][2] = m.c4;
out[0][3] = m.d1; out[1][3] = m.d2; out[2][3] = m.d3; out[3][3] = m.d4;
return out;
}
}
Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
@@ -76,7 +89,10 @@ void Model::InitDefaultTextures()
m_DefaultTextures[Material::Emissive] =
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
m_DefaultTextures[Material::Ao] =
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255, false);
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 0, 0, false);
// 独立粗糙度槽兜底:0.5 灰(无图资产 = 中性粗糙度)
m_DefaultTextures[Material::Roughness] =
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 128, false);
}
Model::Model(Device& device, MemoryAllocator& allocator,
@@ -89,12 +105,18 @@ Model::Model(Device& device, MemoryAllocator& allocator,
, m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout)
{
// 纯色贴图(albedo 兜底;alpha 非全 1 时为半透明材质)
// 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质);
// 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突)
InitDefaultTextures();
auto tex = (rgba[3] == 255)
Scope<Texture> tex = (rgba[3] == 255)
? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2])
: Texture::CreateSolidColorRGBA(device, allocator,
rgba[0], rgba[1], rgba[2], rgba[3]);
Texture* albedoOverride = tex.get();
const std::string texKey = "proc:" + std::to_string(rgba[0]) + ',' +
std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' +
std::to_string(rgba[3]);
m_TextureCache.emplace(texKey, std::move(tex));
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
const f32 h = halfSize;
@@ -107,9 +129,13 @@ Model::Model(Device& device, MemoryAllocator& allocator,
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
const std::vector<std::vector<u32>> lodIndices = {indices};
// 材质/纹理生命周期随模型(缓存持有)
// 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent);
std::array<Texture*, Material::kSlotCount> overrides{};
overrides[Material::Albedo] = albedoOverride;
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent,
overrides);
m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, lodIndices, mat));
@@ -164,20 +190,177 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
(out.isGltf ? 0 : aiProcess_FlipUVs) |
aiProcess_GenSmoothNormals |
aiProcess_CalcTangentSpace | // 法线贴图 TBN
aiProcess_JoinIdenticalVertices);
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights); // 每顶点限 4 权重(= 顶点格式 boneIds 容量)
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
if (!scene->mNumMeshes) {
ENGINE_WARN("Assimp warn: model dose not have mesh assets\nmodel has following asset:");
ENGINE_WARN("\tMeshes Count: {}", scene->mNumMeshes);
ENGINE_WARN("\tAnimations Count: {}", scene->mNumAnimations);
ENGINE_WARN("\tCameras Count: {}", scene->mNumCameras);
ENGINE_WARN("\tLights Count: {}", scene->mNumLights);
ENGINE_WARN("\tMaterials Count: {}", scene->mNumMaterials);
ENGINE_WARN("\tSkeletons Count: {}", scene->mNumSkeletons);
ENGINE_WARN("\tTextures Count: {}", scene->mNumTextures);
}
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
return out; // meshes 空 = 无效
}
// 骨骼动画三件套:node 表(global 累乘骨架)→ 动画轨道 → 骨骼注册
//(DFS 序;ProcessNode 的 mesh 遍历序与其独立,nodeIdx 靠 name 查表)
FlattenNodes(scene->mRootNode, -1, out.nodes);
ParseAnimations(scene, out.nodes, out.clips);
const std::filesystem::path modelDir =
std::filesystem::path(path).parent_path();
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf);
ENGINE_INFO("Model parsed: {} ({} meshes)", path, out.meshes.size());
// 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
std::unordered_map<std::string, u32> boneSlots;
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf,
out.bones, boneSlots);
// 骨骼 nodeIdx 回填(注册发生在各 mesh 的 ProcessMesh 内,
// 这里按 node 表查 name 定位累乘路径)
if (!out.bones.empty()) {
std::unordered_map<std::string, u32> nodeByName;
nodeByName.reserve(out.nodes.size());
for (u32 i = 0; i < out.nodes.size(); ++i) {
nodeByName.emplace(out.nodes[i].name, i);
}
for (auto& bone : out.bones) {
if (auto it = nodeByName.find(bone.name); it != nodeByName.end()) {
bone.nodeIdx = it->second;
} else {
ENGINE_WARN("Bone '{}' has no matching node, palette entry static",
bone.name);
}
}
}
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
path, out.meshes.size(), out.bones.size(), out.clips.size());
return out;
}
u32 Model::FlattenNodes(const aiNode* node, i32 parent,
std::vector<ModelNode>& out)
{
const u32 idx = static_cast<u32>(out.size());
ModelNode n;
n.name = node->mName.C_Str();
n.parent = parent;
// 完整 mTransformation:FBX 的 PreRotation/单位换算都折在这里,
// 骨骼 global 累乘必须用全量矩阵,不能拆 TRS 重建
n.localTransform = ToGlm(node->mTransformation);
out.push_back(std::move(n));
for (u32 i = 0; i < node->mNumChildren; ++i) {
FlattenNodes(node->mChildren[i], static_cast<i32>(idx), out);
}
return idx;
}
void Model::ParseAnimations(const aiScene* scene,
const std::vector<ModelNode>& nodes,
std::vector<AnimationClip>& out)
{
if (scene->mNumAnimations == 0) return;
// nodeName → node 表下标(channel 的目标必须能在层级里累乘)
std::unordered_map<std::string, u32> nodeByName;
nodeByName.reserve(nodes.size());
for (u32 i = 0; i < nodes.size(); ++i) {
nodeByName.emplace(nodes[i].name, i);
}
for (u32 a = 0; a < scene->mNumAnimations; ++a) {
const aiAnimation* anim = scene->mAnimations[a];
AnimationClip clip;
clip.name = anim->mName.length > 0 ? anim->mName.C_Str()
: ("clip_" + std::to_string(a));
// tps=0 按 1(assimp 对部分导出器的惯例缺省)
clip.tps = anim->mTicksPerSecond > 0.0
? static_cast<f32>(anim->mTicksPerSecond) : 1.0f;
clip.duration = static_cast<f32>(anim->mDuration) / clip.tps;
for (u32 c = 0; c < anim->mNumChannels; ++c) {
const aiNodeAnim* ch = anim->mChannels[c];
auto it = nodeByName.find(ch->mNodeName.C_Str());
if (it == nodeByName.end()) {
ENGINE_WARN("Anim '{}': channel node '{}' not in hierarchy, skipped",
clip.name, ch->mNodeName.C_Str());
continue;
}
AnimChannel chan;
chan.nodeIdx = it->second;
// 关键帧 time 统一换算成秒(运行时采样免除法)
chan.positions.reserve(ch->mNumPositionKeys);
for (u32 k = 0; k < ch->mNumPositionKeys; ++k) {
const auto& key = ch->mPositionKeys[k];
chan.positions.push_back({
static_cast<f32>(key.mTime) / clip.tps,
{key.mValue.x, key.mValue.y, key.mValue.z}});
}
chan.rotations.reserve(ch->mNumRotationKeys);
for (u32 k = 0; k < ch->mNumRotationKeys; ++k) {
const auto& key = ch->mRotationKeys[k];
// aiQuaternion 存储序 (w,x,y,z) → glm 构造序一致
chan.rotations.push_back({
static_cast<f32>(key.mTime) / clip.tps,
glm::quat(key.mValue.w, key.mValue.x,
key.mValue.y, key.mValue.z)});
}
chan.scales.reserve(ch->mNumScalingKeys);
for (u32 k = 0; k < ch->mNumScalingKeys; ++k) {
const auto& key = ch->mScalingKeys[k];
chan.scales.push_back({
static_cast<f32>(key.mTime) / clip.tps,
{key.mValue.x, key.mValue.y, key.mValue.z}});
}
clip.channels.push_back(std::move(chan));
}
out.push_back(std::move(clip));
}
}
void Model::CollectMeshBones(const aiMesh* mesh,
std::vector<Vertex>& vertices,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots)
{
for (u32 b = 0; b < mesh->mNumBones; ++b) {
const aiBone* bone = mesh->mBones[b];
const char* name = bone->mName.C_Str();
u32 slot;
if (auto it = boneSlots.find(name); it != boneSlots.end()) {
slot = it->second; // 多 mesh 共享同一骨架(身体/附件分区)
} else {
slot = static_cast<u32>(bones.size());
boneSlots.emplace(name, slot);
// nodeIdx 由 ParseFromDisk 统一回填(node 表此时已就绪)
bones.push_back({name, 0, ToGlm(bone->mOffsetMatrix)});
}
// 顶点权重累加:LimitBoneWeights 保证每顶点 ≤4 根骨,逐槽找空位;
// 4 槽满的理论残留权重直接忽略(量化误差级)
for (u32 w = 0; w < bone->mNumWeights; ++w) {
const aiVertexWeight& vw = bone->mWeights[w];
if (vw.mVertexId >= vertices.size() || vw.mWeight <= 0.0f) continue;
Vertex& v = vertices[vw.mVertexId];
for (u32 k = 0; k < 4; ++k) {
if (v.weights[k] == 0.0f) {
v.boneIds[k] = slot;
v.weights[k] = vw.mWeight;
break;
}
}
}
}
}
Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
@@ -192,6 +375,9 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
descriptorManager, textureLayout));
model->m_ModelPath = parsed.path;
model->m_IsGltf = parsed.isGltf;
model->m_Nodes = std::move(parsed.nodes);
model->m_Bones = std::move(parsed.bones);
model->m_Clips = std::move(parsed.clips);
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
for (auto& pm : parsed.meshes) {
@@ -214,6 +400,9 @@ void Model::LoadModel(const std::string& path)
}
m_ModelPath = parsed.path;
m_IsGltf = parsed.isGltf;
m_Nodes = std::move(parsed.nodes);
m_Bones = std::move(parsed.bones);
m_Clips = std::move(parsed.clips);
for (auto& pm : parsed.meshes) {
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
m_Meshes.push_back(CreateScope<Mesh>(
@@ -226,14 +415,24 @@ void Model::LoadModel(const std::string& path)
void Model::ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir, bool isGltf)
const std::filesystem::path& modelDir, bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& parentGlobal)
{
// 累乘节点链全局变换:Blender/Sketchfab 导出的 FBX(未勾 Apply
// Transform)把物体旋转(含 Z-up→Y-up)与 cm→m 单位换算全折在
// 节点变换上,忽略会让每个零件按原始局部空间乱画(朝向丢失、
// 尺度差百倍,车都不成车)
const aiMatrix4x4 global = parentGlobal * node->mTransformation;
for (u32 i = 0; i < node->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf));
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf,
bones, boneSlots, global));
}
for (u32 i = 0; i < node->mNumChildren; ++i) {
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf);
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf,
bones, boneSlots, global);
}
}
@@ -264,21 +463,37 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params,
bool transparent)
bool transparent,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides)
{
const std::string key = Material::MakeCacheKey(paths, params, transparent);
// 程序化覆盖纹理以指针地址入键:同模型内不同颜色的覆盖组合
//(如两块玻璃板)各得一份材质,不与纯路径键的材质串缓存
std::string key = Material::MakeCacheKey(paths, params, transparent);
bool hasOverride = false;
for (const Texture* t : slotOverrides) {
if (t) { hasOverride = true; break; }
}
if (hasOverride) {
key += "@ovr";
for (const Texture* t : slotOverrides) {
key += t ? ":" + std::to_string(reinterpret_cast<uintptr_t>(t))
: ":0";
}
}
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()) {
Texture* tex = slotOverrides[slot];
if (!tex && !paths[slot].empty()) {
tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]);
}
textures[slot] = tex ? tex : m_DefaultTextures[slot].get();
@@ -294,29 +509,47 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir,
bool isGltf)
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& globalTransform)
{
ParsedMesh out;
std::vector<Vertex>& vertices = out.vertices;
std::vector<u32> indices;
vertices.reserve(mesh->mNumVertices);
// 有骨骼的 mesh:顶点权重先清零再由 CollectMeshBones 灌入——
// 默认 w=(1,0,0,0) 的槽 0 若不清,palette[0] 的 identity 会以
// 权重 1 混进蒙皮矩阵(顶点永远钉在原点)
const bool hasBones = mesh->mNumBones > 0;
// 非蒙皮 mesh:烘焙节点链全局变换(旋转/单位换算折进顶点);
// 蒙皮 mesh 保持 mesh 空间——palette × offsetMatrix 的蒙皮
// 约定依赖原空间,烘焙会与动画路径打架
const bool bakeTransform = !hasBones;
const aiMatrix3x3 normalMat(globalTransform);
for (u32 i = 0; i < mesh->mNumVertices; ++i) {
Vertex vertex{};
vertex.position = {
mesh->mVertices[i].x,
mesh->mVertices[i].y,
mesh->mVertices[i].z
};
if (bakeTransform) {
const aiVector3D p = globalTransform * mesh->mVertices[i];
vertex.position = {p.x, p.y, p.z};
} else {
vertex.position = {
mesh->mVertices[i].x,
mesh->mVertices[i].y,
mesh->mVertices[i].z
};
}
if (mesh->HasNormals()) {
vertex.normal = {
mesh->mNormals[i].x,
mesh->mNormals[i].y,
mesh->mNormals[i].z
};
const aiVector3D n = bakeTransform
? normalMat * mesh->mNormals[i]
: mesh->mNormals[i];
const glm::vec3 nn{n.x, n.y, n.z};
vertex.normal = bakeTransform ? glm::normalize(nn) : nn;
}
if (mesh->mTextureCoords[0]) {
@@ -328,16 +561,23 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
if (mesh->mTangents) {
vertex.tangent = {
mesh->mTangents[i].x,
mesh->mTangents[i].y,
mesh->mTangents[i].z
};
const aiVector3D t = bakeTransform
? normalMat * mesh->mTangents[i]
: mesh->mTangents[i];
const glm::vec3 tt{t.x, t.y, t.z};
vertex.tangent = bakeTransform ? glm::normalize(tt) : tt;
}
if (hasBones) {
vertex.weights = glm::vec4(0.0f);
}
vertices.push_back(vertex);
}
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
CollectMeshBones(mesh, vertices, bones, boneSlots);
for (u32 i = 0; i < mesh->mNumFaces; ++i) {
aiFace face = mesh->mFaces[i];
for (u32 j = 0; j < face.mNumIndices; ++j) {
@@ -355,16 +595,19 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
// 先到先得,数组顺序即优先级
// 先到先得,数组顺序即优先级。
// 注意:FBX 独立 metalness/roughness 是两张图——metal 落 MR 槽
//(灰度 R=G=B,shader 读 .b 当金属度恰好正确),roughness 落
// 独立 Roughness 槽(factors.z=1 启用),不再挤占 MR 槽的 .g
const std::pair<aiTextureType, u32> slots[] = {
{aiTextureType_DIFFUSE, Material::Albedo},
{aiTextureType_BASE_COLOR, Material::Albedo},
{aiTextureType_NORMALS, Material::Normal},
{aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点
{aiTextureType_METALNESS, Material::MetallicRoughness},
{aiTextureType_DIFFUSE_ROUGHNESS, Material::MetallicRoughness},
{aiTextureType_DIFFUSE_ROUGHNESS, Material::Roughness},
{aiTextureType_SHININESS, Material::Roughness}, // OBJ map_Ns
{aiTextureType_UNKNOWN, Material::MetallicRoughness},
{aiTextureType_SHININESS, Material::MetallicRoughness},
{aiTextureType_AMBIENT_OCCLUSION, Material::Ao},
{aiTextureType_LIGHTMAP, Material::Ao},
{aiTextureType_AMBIENT, Material::Ao},
@@ -385,7 +628,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 材质加载可见性:贴图槽缺失/路径解析错误会静默退化成默认
// 纹理(金属全没 → 塑料感),这里留下诊断锚点
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} emissive={} ao={}",
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} roughness={} "
"emissive={} ao={}",
material->GetName().C_Str(),
texPaths[Material::Albedo].empty() ? "<none>"
: texPaths[Material::Albedo],
@@ -393,6 +637,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
: texPaths[Material::Normal],
texPaths[Material::MetallicRoughness].empty() ? "<none>"
: texPaths[Material::MetallicRoughness],
texPaths[Material::Roughness].empty() ? "<none>"
: texPaths[Material::Roughness],
texPaths[Material::Emissive].empty() ? "<none>"
: texPaths[Material::Emissive],
texPaths[Material::Ao].empty() ? "<none>"
@@ -419,15 +665,24 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
out.transparent = (alphaMode == aiString("BLEND"));
}
} else {
// FBX/OBJ 独立贴图工作流
const bool hasSepRough = !texPaths[Material::Roughness].empty();
f32 roughnessFactor = 1.0f;
if (texPaths[Material::MetallicRoughness].empty()) {
if (!hasSepRough && texPaths[Material::MetallicRoughness].empty()) {
// 无粗糙度贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
ai_real shininess = 10.0f;
material->Get(AI_MATKEY_SHININESS, shininess);
const f32 rough =
std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f));
roughnessFactor = 2.0f * glm::clamp(rough, 0.05f, 1.0f);
}
params.factors = glm::vec4(0.0f, roughnessFactor, 0.0f, 0.0f);
// 金属度图落 MR 槽(灰度 .b 即金属度语义):
// 有图 metallic 因子 = 1,无图 = 0(OBJ specular 工作流无金属概念)
const f32 metallicFactor =
texPaths[Material::MetallicRoughness].empty() ? 0.0f : 1.0f;
params.factors = glm::vec4(metallicFactor, roughnessFactor,
hasSepRough ? 1.0f : 0.0f, 0.0f);
}
}
+219 -102
View File
@@ -1,8 +1,28 @@
//
// Created by atdunbg on 2026/9/23.
//
// ForwardPass:相机视角渲染到 HDR 颜色图(线性光照值)。
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
//
// 透明管线对齐 UE/Unity 标准语义:
// - 不透明("Forward"):深度测试 + 写,无混合直写
// - 透明("ForwardTrans"):独立渲染实例,深度测试(对不透明)+
// 深度写入关闭,HDR/velocity/depth 全部 Load 承接不透明段结果,
// 预乘 alpha 混合(线性空间 over),远 → 近排序(SceneRenderer
// 侧按 batch 平均距离排)
//
// NVIDIA shader-object 实测注意(见 .trae/documents 性能档案):
// 早期实验称 depthWriteEnable=FALSE 有灾难病态(~5fps),但该实验
// 是在 frag 仍写 gl_FragDepth 的时期做的(写 gl_FragDepth 会禁用
// early-Z,归因很可能在它而非 FALSE 本身)。gl_FragDepth 已删除,
// 透明段现为 depthWrite=FALSE;若病态复现,先排查二者再下结论。
// 另:勿用 gl_FragDepth=far 模拟"只测不写"(eLess 下片元自身
// 永远测试失败,透明队列全灭)。
//
#include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/Material.h>
@@ -12,8 +32,90 @@
namespace Engine::Passes
{
namespace
{
// 直写方程:(ONE, ZERO) → dst = src,数学上等价于无混合
vk::ColorBlendEquationEXT MakeDirectEquation()
{
vk::ColorBlendEquationEXT eq;
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
return eq;
}
constexpr vk::ColorComponentFlags kColorWriteMask =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
} // namespace
// 两段渲染共享的命令录制:shader/描述符绑定 + 几何/光栅/视口状态。
// translucent=true 时绑定真实深度写入的透明 frag 变体。
// 深度写与混合状态由各渲染实例作为初始态设置(见文件头注释)。
void ForwardPass::RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd,
bool translucent) const
{
// 绑定 shader object
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {
m_Gpu.forwardVert,
translucent ? m_Gpu.translucentFrag : 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.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);
}
void ForwardPass::Register(RG::RenderGraph& rg)
{
// ---- 不透明段:深度测试 + 写,无混合直写 ----
rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
@@ -42,6 +144,96 @@ namespace Engine::Passes
depthAttachment.setImageView(ctx.View(m_Depth))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
// 透明段要 Load 本段写出的深度做测试,必须 store
.setStoreOp(vk::AttachmentStoreOp::eStore)
.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);
RecordSharedState(ctx, cmd, false);
cmd.setDepthWriteEnable(VK_TRUE);
// 两个颜色附件(HDR + velocity)都是无混合直写
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{VK_FALSE, VK_FALSE});
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{direct, direct});
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
// 调试视图 Transparent:只看透明队列——跳过不透明绘制,
// HDR 颜色 = 清屏色 + 透明段内容(Skybox 同步跳过背景)
if (m_Frame.debugView != Passes::kDebugTransparent) {
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, batch.lod);
}
}
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);
// ---- 透明段:独立渲染实例,UE/Unity 标准语义 ----
// 深度测试对不透明深度(eLess)+ 深度写入关闭——玻璃之间
// 互相不遮挡(谁先画都留在画面上,先后只影响 over 混合的
// 颜色数学),交叉半透明不再整块消失。
// 对不透明深度的 RAW 由 depth 的 Write use 链保证排在
// 不透明段之后(WAW 边)。
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
rg.AddPass("ForwardTrans", [this](RG::RenderGraphContext& ctx) {
if (m_Frame.translucentView.batches.empty()) return;
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::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity:Load 不透明段写出的运动向量,玻璃只覆盖自己的像素
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::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_Depth))
// 深度只读测试源:Load 承接不透明段深度,
// 透明段 depthWrite=FALSE 不产生写入,
// storeOp DontCare(下帧 entry 重新 Clear)
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
@@ -56,124 +248,49 @@ namespace Engine::Passes
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
RecordSharedState(ctx, cmd, true);
// UE/Unity 语义:透明段只测不写深度。写深度会让"先画的玻璃"
// 剔掉后画但实际在它前面的玻璃(交叉半透明整块消失)。
// 早期 depthWrite=FALSE 引发 NVIDIA ~5fps 病态的实验是在
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
cmd.setDepthWriteEnable(VK_FALSE);
// 绑定 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);
vk::ColorBlendEquationEXT premul;
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
.setColorBlendOp(vk::BlendOp::eAdd)
// alpha 穿透:玻璃只做场景内 rgb over(透出背景色),
// 不修改像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap
// rgb *= a 预乘 + 透明窗口桌面合成),归背景专属。若让
// 玻璃 over 混入 a(旧写法),a<1 背景上的玻璃会被拖进
// 桌面混色链路 → 预乘拉黑
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
.setAlphaBlendOp(vk::BlendOp::eAdd);
// 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)都是无混合直写
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{blendEnable, blendEnable});
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{blendEquation, blendEquation});
std::array<vk::ColorBlendEquationEXT, 2>{premul, direct});
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{colorWriteMask, colorWriteMask});
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
// ---- 不透明批次:无混合直写,深度测试 + 写 ----
for (const auto& batch : m_Frame.mainView.batches) {
for (const auto& batch : m_Frame.translucentView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
// ---- 透明批次:预乘 alpha 混合(线性空间 over),深度只测
// 不写,远 → 近排序(SceneRenderer 侧已按 batch 平均距离排)。
// 附件 0(HDR)混合、附件 1(velocity)直写——透明表面的
// velocity 就是它自身的运动,覆盖自己的像素即正确。
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
if (!m_Frame.translucentView.batches.empty()) {
vk::ColorBlendEquationEXT premul;
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
.setAlphaBlendOp(vk::BlendOp::eAdd);
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{premul, blendEquation});
cmd.setDepthWriteEnableEXT(VK_FALSE);
for (const auto& batch : m_Frame.translucentView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
cmd.setDepthWriteEnableEXT(VK_TRUE); // 恢复(下帧 opaque 段也会重设)
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
// depth 走写 use(WAW 边):保证透明段排在不透明段之后,
// barrier 覆盖透明段对不透明深度的测试读(Read-Write hazard
// 由 src=Write 的内存域保证可见性)
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
@@ -174,6 +174,32 @@ namespace Engine::Passes
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
// ---- 颜色混合必须显式直写 ----
// shader-object 路径混合是动态状态,会从上一个 pass(ForwardTrans
// 的 premul 混合)残留:tonemap 输出被二次混合 → rgb = color×a²
//(拖低 alpha 平方级变黑),且 alpha 被残留方程与 clear(a=1) 混成
// 恒 1 → 透明窗口永远不生效
constexpr vk::Bool32 kBlendOff = VK_FALSE;
constexpr vk::ColorComponentFlags kWriteAll =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
// 直写方程:(ONE, ZERO) → dst = src(无混合),alpha 同样直写
static const vk::ColorBlendEquationEXT kDirect = [] {
vk::ColorBlendEquationEXT eq;
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
return eq;
}();
cmd.setColorBlendEnableEXT(0, kBlendOff);
cmd.setColorBlendEquationEXT(0, kDirect);
cmd.setColorWriteMaskEXT(0, kWriteAll);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
+22 -4
View File
@@ -12,14 +12,28 @@ namespace Engine::Passes
rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
// loadOp 按显示模式分支:Direct = blit 已铺满场景(Load 叠加);
// Viewport = 本 pass 是唯一写入者(Clear 为"桌面底色")
const bool direct = (m_Frame.displayMode == kDisplayDirect);
vk::ClearValue clearValue;
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{0.04f, 0.045f, 0.06f, 1.0f});
vk::RenderingAttachmentInfo colorAttachment;
colorAttachment.setImageView(ctx.View(m_Backbuffer))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad) // 关键:叠加
.setStoreOp(vk::AttachmentStoreOp::eStore);
.setLoadOp(direct ? vk::AttachmentLoadOp::eLoad
: vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
// 渲染区域 = backbuffer 物理尺寸(窗口大小),不是 ctx.extent
//(= RG 目标尺寸 = 场景渲染分辨率)。Editor 视口模式下场景链
// 按面板尺寸渲染,而 backbuffer 是全窗口——若跟随目标尺寸,
// 整个 ImGui 会被裁剪到面板大小
renderingInfo.setRenderArea({{0, 0}, ctx.Extent(m_Backbuffer)})
.setLayerCount(1)
.setColorAttachments(colorAttachment);
@@ -30,7 +44,11 @@ namespace Engine::Passes
}
cmd.endRendering();
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment);
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
// 场景纹理在本 pass 内被 ImGui::Image 的描述符采样:声明读让 RG
// 补 ColorAttachment→ShaderRead barrier,并推导 eSampled usage
.Read(m_PresentTarget, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
}
} // namespace Engine::Passes
+92 -4
View File
@@ -4,8 +4,10 @@
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> // IndirectGroup 取数(inline 经 Mesh)
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/Buffer.h>
#include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Vulkan/UniformBuffer.h>
@@ -15,14 +17,18 @@
namespace Engine::Passes
{
SceneGpu::SceneGpu(Device& device, DescriptorManager& descriptors,
SceneGpu::SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer)
: m_Device(device), m_Descriptors(descriptors)
UniformBuffer& prevObjectBuffer,
UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer)
: m_Device(device), m_Allocator(allocator), m_Descriptors(descriptors)
{
// Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取)
// + 上一帧实例矩阵(TAA velocity,vertex 读取)
// + 骨骼蒙皮:SkinOffsets(实例→palette 基址)+ SkinPalettes(骨骼矩阵)
std::vector<DescriptorBinding> uboBindings = {
{0, vk::DescriptorType::eUniformBuffer,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
@@ -30,6 +36,10 @@ namespace Engine::Passes
vk::ShaderStageFlagBits::eVertex},
{2, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
{3, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
{4, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
};
uboSetLayout = descriptors.CreateLayout(uboBindings);
@@ -81,6 +91,11 @@ namespace Engine::Passes
shaderDir + "simple_shader.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// 透明变体:同一套布局,frag 末尾写 gl_FragDepth=1.0
auto transFrag = shaders.CreateFromFile(
shaderDir + "simple_shader_translucent.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// shadow shader:depth-only,push = lightVP
auto shadowVertShader = shaders.CreateFromFile(
@@ -96,10 +111,12 @@ namespace Engine::Passes
forwardVert = vert.Handle;
forwardFrag = frag.Handle;
translucentFrag = transFrag.Handle;
shadowVert = shadowVertShader.Handle;
shadowFrag = shadowFragShader.Handle;
if (!forwardVert || !forwardFrag || !shadowVert || !shadowFrag) {
if (!forwardVert || !forwardFrag || !translucentFrag ||
!shadowVert || !shadowFrag) {
ENGINE_FATAL("SceneGpu: failed to create shader objects");
}
@@ -132,10 +149,34 @@ namespace Engine::Passes
descriptors.UpdateStorageBuffer(
uboSets[slot], 2, prevObjectBuffer.Get(slot),
prevObjectBuffer.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 3, skinOffsetBuffer.Get(slot),
skinOffsetBuffer.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 4, skinPaletteBuffer.Get(slot),
skinPaletteBuffer.GetSize());
shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout);
}
// shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成
// ---- MDI per-slot ring:命令 + count(CPU 直写,host visible)----
// 布局 [main | casc0 | casc1 | casc2] × kMaxInstances 命令预算;
// 两者 usage 都要 eIndirectBuffer(count buffer 的 spec 要求)
const vk::DeviceSize cmdBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
kMaxInstances * sizeof(IndirectDrawCmd);
const vk::DeviceSize cntBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
kMaxInstances * sizeof(u32);
constexpr auto memProps = vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent;
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
indirectCmdBuffers[slot] = CreateScope<Buffer>(
device, allocator, cmdBytes,
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
indirectCountBuffers[slot] = CreateScope<Buffer>(
device, allocator, cntBytes,
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
}
}
SceneGpu::~SceneGpu()
@@ -144,6 +185,7 @@ namespace Engine::Passes
if (forwardVert) vkDevice.destroyShaderEXT(forwardVert);
if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag);
if (translucentFrag) vkDevice.destroyShaderEXT(translucentFrag);
if (shadowVert) vkDevice.destroyShaderEXT(shadowVert);
if (shadowFrag) vkDevice.destroyShaderEXT(shadowFrag);
@@ -168,6 +210,52 @@ namespace Engine::Passes
}
}
// ---- MDI:per-slot 上传 + 组绘制 ----
void SceneGpu::UploadIndirect(u32 frameSlot, const FrameSceneData& frame)
{
auto uploadView = [&](u32 viewIdx, const CullResult& v) {
if (v.indirectCommands.empty()) return;
const vk::DeviceSize base =
vk::DeviceSize(viewIdx) * kMaxInstances;
indirectCmdBuffers[frameSlot]->Upload(
v.indirectCommands.data(),
v.indirectCommands.size() * sizeof(IndirectDrawCmd),
base * sizeof(IndirectDrawCmd));
// 每组一个 count,下标 = 组首命令下标(drawIndexedIndirectCount
// 从此读取实际执行条数——GPU 剔除切片接管时只换写入者)
for (const auto& g : v.indirectGroups) {
indirectCountBuffers[frameSlot]->Upload(
&g.commandCount, sizeof(u32),
(base + g.firstCommand) * sizeof(u32));
}
};
uploadView(kIndirectViewMain, frame.mainView);
for (u32 c = 0; c < kMaxCascades; ++c) {
uploadView(kIndirectViewMain + 1 + c, frame.shadowCascades[c]);
}
}
void SceneGpu::DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
u32 viewIndex, const IndirectGroup& g)
{
// 组内全部命令共享同一 megabuffer:bind 一次,逐 mesh 定位由
// 命令里的 firstIndex/vertexOffset 承担
cmd.bindVertexBuffers(0, g.mesh->GetVertexBuffer(), {vk::DeviceSize(0)});
cmd.bindIndexBuffer(g.mesh->GetIndexBuffer(), vk::DeviceSize(0),
vk::IndexType::eUint32);
const vk::DeviceSize base =
vk::DeviceSize(viewIndex) * kMaxInstances;
cmd.drawIndexedIndirectCount(
indirectCmdBuffers[frameSlot]->Get(),
base * sizeof(IndirectDrawCmd),
indirectCountBuffers[frameSlot]->Get(),
(base + g.firstCommand) * sizeof(u32),
g.commandCount, // maxDrawCount = CPU 打包的组大小
sizeof(IndirectDrawCmd));
}
void SceneGpu::BindEnvironment(const Cubemap& env)
{
// IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。
+5 -1
View File
@@ -3,6 +3,7 @@
//
#include <Engine/Renderer/Passes/SkyboxPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h>
@@ -96,6 +97,8 @@ namespace Engine::Passes
const glm::vec4& cc = m_Frame.ClearColor;
vk::ClearValue clear;
// alpha 语义 = 窗口透过率(透明窗口与桌面合成;tonemap 段
// rgb *= a 预乘)。滑条由用户控制:a<1 = 背景透出桌面
clear.color = vk::ClearColorValue(
std::array<float, 4>{cc.r, cc.g, cc.b, cc.a});
@@ -113,7 +116,8 @@ namespace Engine::Passes
cmd.beginRendering(renderingInfo);
if (m_Env && m_Frame.DrawSkybox) {
if (m_Env && m_Frame.DrawSkybox &&
m_Frame.debugView != Passes::kDebugTransparent) {
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
const std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
+4 -4
View File
@@ -14,7 +14,7 @@ namespace Engine::Passes
FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff,
RG::TextureHandle backbuffer,
RG::TextureHandle presentTarget,
RG::TextureHandle rawHdr,
RG::TextureHandle velocity,
RG::TextureHandle shadowMap0,
@@ -22,7 +22,7 @@ namespace Engine::Passes
RG::TextureHandle shadowMap2)
: m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame),
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
m_Backbuffer(backbuffer),
m_Target(presentTarget),
m_RawHDR(rawHdr), m_Velocity(velocity),
m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2}
{
@@ -38,7 +38,7 @@ namespace Engine::Passes
{
rg.AddPass("Tonemap",
[this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Backbuffer),
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Target),
m_Gpu.tonemapFrag,
m_Sets[ctx.frame.frameSlot],
FullscreenPush{m_Params.exposure,
@@ -49,7 +49,7 @@ namespace Engine::Passes
static_cast<f32>(
m_Frame.debugView)});
})
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
.Write(m_Target, RG::ResourceUse::ColorAttachment)
.Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_A, RG::ResourceUse::ShaderRead,
+128 -27
View File
@@ -24,6 +24,7 @@
#include <Engine/Renderer/Passes/TemporalDiffPass.h>
#include <Engine/Renderer/Passes/ImGuiPass.h>
#include <Engine/Renderer/Passes/PresentPass.h>
#include <Engine/Renderer/Passes/BlitPass.h>
#include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Vulkan/DescriptorManager.h>
@@ -34,6 +35,7 @@
#include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Core/Window.h>
#include <Engine/Core/Log.h>
#include <Engine/ImGui/ImGuiLayer.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h>
@@ -43,6 +45,7 @@
#include <array>
#include <limits>
#include <string>
namespace Engine
{
@@ -67,11 +70,22 @@ namespace Engine
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
// 骨骼蒙皮:offsets 与实例序平行(每实例一个 palette 基址);
// palette 每实体一个 slot(帧首 bump 分配),容量 kMaxSkinMatrices
m_SkinOffsetBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(u32) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
m_SkinPaletteBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxSkinMatrices, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
// 3. pass 装配体:布局/shader/采样器/per-slot 描述符
m_SceneGpu = CreateScope<Passes::SceneGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager,
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer);
*m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager,
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer,
*m_SkinOffsetBuffer, *m_SkinPaletteBuffer);
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager);
@@ -191,6 +205,12 @@ namespace Engine
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); }
void Renderer::SetDisplayMode(int mode)
{
m_DesiredDisplayMode = mode;
m_SceneRenderer->SetDisplayMode(mode);
}
void Renderer::SetEnvironment(const Cubemap* env)
{
if (!env) env = m_Environment.get();
@@ -210,9 +230,9 @@ namespace Engine
static const std::vector<const char*> kNames = {
"Final", "TemporalDiff", "HDR", "TAAColor", "Bloom",
"Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2",
"EnvCube", "Irradiance", "Prefilter", "BrdfLut",
"EnvCube", "Irradiance", "Prefilter", "BrdfLut", "Transparent",
};
static_assert(int(Passes::kDebugViewCount) == 13,
static_assert(int(Passes::kDebugViewCount) == 14,
"DebugView 枚举与名表同步");
return kNames;
}
@@ -244,9 +264,17 @@ namespace Engine
// ---- 资源声明(命名资源;pass 经句柄引用,数据流由 uses 决定)----
// 导入 swapchain 颜色附件
// 导入 swapchain 颜附件(物理尺寸 = 窗口,与场景渲染分辨率解耦)
m_Backbuffer = m_RenderGraph->ImportSwapchain(
"Backbuffer", m_Swapchain->GetFormat());
m_RenderGraph->SetImportedExtent(m_Backbuffer, m_Swapchain->GetExtent());
// 显示目标:tonemap 写这里(offscreen),ImGui::Image 采样后自由
// 摆放;backbuffer 只承载 ImGui。格式随 swapchain(Unorm:tonemap
// 自带 gamma 编码,采样 → ImGui 写回全程无二次编解码,零色偏)
m_PresentTarget = m_RenderGraph->CreateTexture(
"PresentTarget",
RG::TextureDesc{m_Swapchain->GetFormat()});
// 深度图:目标尺寸,格式由 Device 能力决定
const vk::Format depthFormat = m_Device->FindSupportedFormat(
@@ -317,10 +345,15 @@ namespace Engine
m_BloomB));
m_Passes.push_back(CreateScope<Passes::TonemapPass>(
*m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame,
m_TAAColor, m_BloomA, m_TemporalDiff, m_Backbuffer, m_HDRColor,
m_TAAColor, m_BloomA, m_TemporalDiff, m_PresentTarget, m_HDRColor,
m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2]));
// Direct 模式适配器:PresentTarget → backbuffer(Viewport 模式空转)。
// 必须注册在 ImGui 之前:两者都写 backbuffer,WAW 边按声明序
// 连接(前 writer → 后 writer),反序会让 blit 盖掉 UI
m_Passes.push_back(CreateScope<Passes::BlitPass>(
frame, m_PresentTarget, m_Backbuffer));
m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
&m_ImGuiLayer, m_Backbuffer));
&m_ImGuiLayer, frame, m_Backbuffer, m_PresentTarget));
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
for (auto& pass : m_Passes) {
@@ -330,29 +363,88 @@ namespace Engine
m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile
}
void Renderer::SetVSync(bool vsync)
// ---- 帧首同步:所有"会换物理资源/描述符"的意图统一在这里落定 ----
vk::Extent2D Renderer::DesiredRenderExtent() const
{
if (m_Swapchain->GetVSync() == vsync) return;
m_Swapchain->SetVSync(vsync);
HandleResize(); // present mode 变化需要重建 swapchain
// Editor 视口模式:渲染分辨率跟随 ImGui 面板(面板未上报/关闭时
// 回退 swapchain 尺寸);Direct 模式:全窗口
return (m_DesiredDisplayMode == kDisplayViewport && m_DesiredViewport.width)
? m_DesiredViewport
: m_Swapchain->GetExtent();
}
void Renderer::HandleResize()
void Renderer::BeginFrameSync()
{
const bool vsyncChanged = (m_DesiredVSync != m_Swapchain->GetVSync());
const bool windowResized = m_Window.WasResized();
const vk::Extent2D want = DesiredRenderExtent();
const bool extentChanged =
(want.width != m_Extent.width || want.height != m_Extent.height);
if (!vsyncChanged && !windowResized && !extentChanged) return;
if (windowResized) m_Window.ResetResizedFlag();
// swapchain 重建仅由窗口 resize / vsync 切换触发;
// 面板拖拽只重编译 RG(swapchain 与渲染分辨率解耦)
HandleResize(windowResized || vsyncChanged);
}
// GPU 空闲 → (可选)重建 swapchain → RG 重编译到期望渲染分辨率 →
// 各 pass 重绑输入描述符 → 场景纹理重新注册到 ImGui。
// 调用点:BeginFrameSync(UI 构建前)与帧中止路径(acquire/present
// 失败)——都不存在"已构建 draw data 引用旧描述符"的窗口
void Renderer::HandleResize(bool recreateSwapchain)
{
m_Device->GetDevice().waitIdle();
m_Swapchain->Recreate(m_Window);
m_Extent = m_Swapchain->GetExtent();
if (recreateSwapchain) {
// 先把 vsync 意图落进 swapchain(Create 按 m_VSync 选 present
// mode 且从不自行修改它)——否则 vsyncChanged 每帧恒真,
// swapchain 被无限重建,present mode 也永远切不过去
m_Swapchain->SetVSync(m_DesiredVSync);
m_Swapchain->Recreate(m_Window);
// image 数量可能变化
m_SyncManager = CreateScope<SyncManager>(
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
// backbuffer 物理尺寸跟新窗口走(ImGui 渲染区域 = 全窗口)
m_RenderGraph->SetImportedExtent(m_Backbuffer,
m_Swapchain->GetExtent());
}
// image 数量可能变化
m_SyncManager = CreateScope<SyncManager>(
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
m_Extent = DesiredRenderExtent(); // 重建后取(swapchain 尺寸已新鲜)
// RG 重编译:物理镜像按新尺寸轮换,各 pass 重绑输入描述符
m_RenderGraph->SetTargetExtent(m_Extent);
for (auto& pass : m_Passes) {
pass->OnTargetsRebuilt(*m_RenderGraph);
}
RefreshSceneTextures();
}
// ---- 场景显示纹理(PresentTarget → ImGui::Image)----
void* Renderer::GetSceneTexture() const
{
// UI 构建段时 m_CurrentFrame 已指向本帧 Render 将使用的槽位
//(上一帧 Render 尾部推进),UI 与渲染同槽位
void* tex = nullptr;
if (m_ImGuiLayer) {
tex = (void*)(intptr_t)
m_ImGuiLayer->GetSceneTexture(m_CurrentFrame);
}
return tex;
}
// RG 物理镜像按帧槽位各一份:每槽位注册一个 ImGui 描述符。
// 首次编译后(SetImGuiLayer)与每次重编译后(HandleResize)调用
void Renderer::RefreshSceneTextures()
{
if (!m_ImGuiLayer || !m_PresentTarget.Valid()) return;
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_ImGuiLayer->UpdateSceneTexture(
slot, m_RenderGraph->GetPhysicalView(m_PresentTarget, slot));
}
}
void Renderer::ReadGpuTimes(u32 frameSlot)
@@ -386,19 +478,16 @@ namespace Engine
}
}
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets)
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt)
{
ZoneScoped; // CPU 侧:UBO/SSBO 上传 + 录制 + 提交(GPU 耗时见 RG timestamp)
if (m_Window.WasResized()) {
m_Window.ResetResizedFlag();
HandleResize();
}
// resize 已在 BeginFrameSync(UI 构建前)处理,此处无需再查
const u32 frameSlot = m_CurrentFrame;
const auto& vkDevice = m_Device->GetDevice();
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent)) {
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent, dt)) {
return; // 无 primary 相机:跳帧
}
const FrameSceneData& frame = m_SceneRenderer->Frame();
@@ -417,6 +506,18 @@ namespace Engine
frame.prevInstanceModels.size() *
sizeof(glm::mat4));
}
// 蒙皮数据与实例矩阵同窗口落槽位 ring(fence 已等,写-读竞争关闭)
if (!frame.skinOffsets.empty()) {
m_SkinOffsetBuffer->Update(frameSlot, frame.skinOffsets.data(),
frame.skinOffsets.size() * sizeof(u32));
}
if (!frame.skinPalettes.empty()) {
m_SkinPaletteBuffer->Update(frameSlot, frame.skinPalettes.data(),
frame.skinPalettes.size() *
sizeof(glm::mat4));
}
// MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭)
m_SceneGpu->UploadIndirect(frameSlot, frame);
// 3. acquire swapchain image
u32 imageIndex = 0;
@@ -427,13 +528,13 @@ namespace Engine
m_SyncManager->GetImageAvailable(frameSlot));
if (result.result == vk::Result::eErrorOutOfDateKHR) {
HandleResize();
HandleResize(true); // 帧中止:无 draw data 引用旧资源,安全
return;
}
imageIndex = result.value;
}
catch (const vk::OutOfDateKHRError&) {
HandleResize();
HandleResize(true);
return;
}
@@ -486,11 +587,11 @@ namespace Engine
try {
const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo);
if (result == vk::Result::eSuboptimalKHR) {
HandleResize();
HandleResize(true);
}
}
catch (const vk::OutOfDateKHRError&) {
HandleResize();
HandleResize(true);
}
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
+285 -2
View File
@@ -14,6 +14,7 @@
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <numeric>
@@ -46,6 +47,121 @@ namespace Engine
}
return r;
}
// ---- 骨骼动画采样(BuildFrame 第 2.5 节专用)----
// 静止 local TRS 分解(列主序 SRT 矩阵;假设无负缩放/剪切)。
// 被动画轨道触碰的节点用关键帧重建,未触碰的保持原矩阵——
// FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢
void DecomposeTRS(const glm::mat4& m, glm::vec3& pos, glm::quat& rot,
glm::vec3& scl)
{
pos = glm::vec3(m[3]);
scl = {glm::length(glm::vec3(m[0])),
glm::length(glm::vec3(m[1])),
glm::length(glm::vec3(m[2]))};
const glm::mat3 r(glm::vec3(m[0]) / scl.x,
glm::vec3(m[1]) / scl.y,
glm::vec3(m[2]) / scl.z);
rot = glm::quat_cast(r);
}
// 关键帧采样:前后键插值(位置/缩放 lerp,旋转 slerp)。
// key 数通常几十个,线性扫描对 cache 更友好
glm::vec3 SampleKeys(const std::vector<PosKey>& keys, f32 t,
const glm::vec3& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::mix(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
glm::vec3 SampleKeys(const std::vector<SclKey>& keys, f32 t,
const glm::vec3& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::mix(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
glm::quat SampleKeys(const std::vector<RotKey>& keys, f32 t,
const glm::quat& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::slerp(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
// 单 clip 在 time 的全节点局部 TRS(无轨道分量 = 静止分解值)。
// touched = 被任意轨道触碰的节点标记(ComposeGlobals 决定重建或原样)
void SampleLocalPose(const Model& model, const AnimationClip& clip,
f32 time, std::vector<glm::vec3>& pos,
std::vector<glm::quat>& rot,
std::vector<glm::vec3>& scl,
std::vector<u8>& touched)
{
const auto& nodes = model.GetNodes();
pos.resize(nodes.size());
rot.resize(nodes.size());
scl.resize(nodes.size());
touched.assign(nodes.size(), 0);
for (u32 i = 0; i < nodes.size(); ++i) {
DecomposeTRS(nodes[i].localTransform, pos[i], rot[i], scl[i]);
}
if (clip.duration <= 0.0f) return;
const f32 t = glm::mod(time, clip.duration);
for (const auto& ch : clip.channels) {
if (ch.nodeIdx >= nodes.size()) continue;
touched[ch.nodeIdx] = 1;
if (!ch.positions.empty())
pos[ch.nodeIdx] = SampleKeys(ch.positions, t, pos[ch.nodeIdx]);
if (!ch.rotations.empty())
rot[ch.nodeIdx] = SampleKeys(ch.rotations, t, rot[ch.nodeIdx]);
if (!ch.scales.empty())
scl[ch.nodeIdx] = SampleKeys(ch.scales, t, scl[ch.nodeIdx]);
}
}
// 局部 TRS → 全节点 global 矩阵(层级累乘;父序 DFS 保证父先算)
void ComposeGlobals(const Model& model, const std::vector<glm::vec3>& pos,
const std::vector<glm::quat>& rot,
const std::vector<glm::vec3>& scl,
const std::vector<u8>& touched,
std::vector<glm::mat4>& globals)
{
const auto& nodes = model.GetNodes();
globals.resize(nodes.size());
for (u32 i = 0; i < nodes.size(); ++i) {
const glm::mat4 local = touched[i]
? glm::translate(glm::mat4(1.0f), pos[i]) *
glm::mat4_cast(rot[i]) * glm::scale(glm::mat4(1.0f), scl[i])
: nodes[i].localTransform;
globals[i] = nodes[i].parent >= 0
? globals[static_cast<u32>(nodes[i].parent)] * local
: local;
}
}
}
void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir)
@@ -132,7 +248,7 @@ namespace Engine
}
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent)
vk::Extent2D targetExtent, f32 dt)
{
ZoneScopedN("BuildFrame");
const auto collectStart = std::chrono::steady_clock::now();
@@ -179,6 +295,7 @@ namespace Engine
m_Frame.bloomEnabled = m_BloomEnabled;
m_Frame.lodEnabled = m_LodEnabled;
m_Frame.debugView = m_DebugView;
m_Frame.displayMode = m_DisplayMode;
ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f;
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
@@ -250,6 +367,107 @@ namespace Engine
ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f);
// ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走,
// 否则被剔除的实体回屏时动画跳变)----
// palette 槽 0 固定 identity:静态模型/无动画实例的蒙皮直通
m_Frame.skinPalettes.clear();
m_Frame.skinPalettes.push_back(glm::mat4(1.0f));
m_Frame.skinOffsets.clear();
m_Frame.skinPaletteTruncated = false;
m_EntitySkinBase.clear();
scene.Each<MeshRendererComponent, AnimationComponent>(
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
const Model* model = assets.GetModel(mr.model);
if (!model || !model->HasAnimations()) return;
const auto& clips = model->GetClips();
// clip 下标合法性(模型资产可能已换)
if (anim.clipIndex < 0 ||
anim.clipIndex >= static_cast<int>(clips.size())) {
anim.clipIndex = 0;
anim.fadeFrom = -1;
if (clips.empty()) return;
}
if (anim.fadeFrom >= static_cast<int>(clips.size())) {
anim.fadeFrom = -1;
}
// ---- 播放头推进(wrap/clamp)----
if (anim.playing && anim.speed != 0.0f) {
const f32 d = dt * anim.speed;
anim.time += d;
if (anim.fadeFrom >= 0) anim.fadeFromTime += d;
}
const auto wrapTime = [&](f32 t, const AnimationClip& c) {
if (c.duration <= 0.0f) return 0.0f;
return anim.loop ? glm::mod(t, c.duration)
: glm::clamp(t, 0.0f, c.duration - 1e-3f);
};
const AnimationClip& cur = clips[anim.clipIndex];
anim.time = wrapTime(anim.time, cur);
const AnimationClip* src = nullptr;
f32 srcTime = 0.0f;
f32 wCur = 1.0f; // 当前 clip 权重(crossfade 0→1)
if (anim.fadeFrom >= 0) {
// 淡出计时(暂停时冻结,与播放头一致)
if (anim.playing) anim.fadeElapsed += dt;
const f32 f = glm::min(
anim.fadeElapsed / glm::max(anim.fadeDuration, 1e-4f),
1.0f);
wCur = f * f * (3.0f - 2.0f * f); // smoothstep
if (f >= 1.0f) {
anim.fadeFrom = -1; // 过渡完成
} else {
src = &clips[anim.fadeFrom];
anim.fadeFromTime = wrapTime(anim.fadeFromTime, *src);
srcTime = anim.fadeFromTime;
}
}
// ---- 采样 + 局部 TRS 层混合 → global → palette ----
SampleLocalPose(*model, cur, anim.time,
m_PosePosA, m_PoseRotA, m_PoseSclA, m_TouchedA);
if (src) {
SampleLocalPose(*model, *src, srcTime,
m_PosePosB, m_PoseRotB, m_PoseSclB,
m_TouchedB);
const size_t n = m_PosePosA.size();
for (size_t i = 0; i < n; ++i) {
// touched = 两层轨道的并集(重建集合)
m_TouchedA[i] = static_cast<u8>(
m_TouchedA[i] | m_TouchedB[i]);
m_PosePosA[i] = glm::mix(m_PosePosB[i], m_PosePosA[i], wCur);
m_PoseSclA[i] = glm::mix(m_PoseSclB[i], m_PoseSclA[i], wCur);
// 旋转必须 slerp(lerp 单位四元数会走样)
m_PoseRotA[i] = glm::slerp(m_PoseRotB[i], m_PoseRotA[i], wCur);
}
}
ComposeGlobals(*model, m_PosePosA, m_PoseRotA, m_PoseSclA,
m_TouchedA, m_Globals);
// palette bump 分配(按实体,不按模型去重——同模型两实体
// 可不同播放状态)。预算打满 → 该实体退回绑定姿势
const auto& bones = model->GetBones();
const u32 base = static_cast<u32>(m_Frame.skinPalettes.size());
if (base + bones.size() > kMaxSkinMatrices) {
m_Frame.skinPaletteTruncated = true;
if (!m_SkinTruncWarned) {
m_SkinTruncWarned = true;
ENGINE_WARN("GPUDriven: skin palette full ({}), "
"animated entities fall back to bind pose",
kMaxSkinMatrices);
}
return;
}
for (const auto& b : bones) {
const u32 ni = b.nodeIdx < m_Globals.size()
? b.nodeIdx : 0;
m_Frame.skinPalettes.push_back(m_Globals[ni] * b.invBind);
}
const u32 entKey = static_cast<u32>(entt::to_integral(entity));
m_EntitySkinBase[entKey] = base;
});
// ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
// 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN
ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true );
@@ -262,8 +480,10 @@ namespace Engine
// batch.first 拼接时加上段基址,保证段内区间连续。
m_Frame.instanceModels.clear();
m_Frame.prevInstanceModels.clear();
m_Frame.skinOffsets.clear();
m_MainScratch.Clear();
for (auto& s : m_CascScratch) s.Clear();
m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建
u32 totalInstances = 0;
bool instanceTruncated = false;
@@ -289,6 +509,13 @@ namespace Engine
}
m_CurWorlds.emplace(entKey, t.World());
// palette 基址(第 2.5 节预采样;无动画实体 = identity 槽)
u32 skinBase = kIdentityPaletteSlot;
if (auto it = m_EntitySkinBase.find(entKey);
it != m_EntitySkinBase.end()) {
skinBase = it->second;
}
// 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放)
const f32 worldScale = glm::max(
glm::length(glm::vec3(t.World()[0])),
@@ -359,7 +586,7 @@ namespace Engine
// 实例在扁平化后连续——firstInstance 语义的前提
const u32 bi = batchIdx - 1;
s.result.batches[bi].count++;
s.batchMats[bi].push_back({t.World(), prevWorld});
s.batchMats[bi].push_back({t.World(), prevWorld, skinBase});
s.batchDistSum[bi] += dist; // 透明排序用
s.result.instanceCount++;
++totalInstances;
@@ -398,6 +625,8 @@ namespace Engine
for (const auto& m : s.batchMats[i]) {
m_Frame.instanceModels.push_back(m.cur);
m_Frame.prevInstanceModels.push_back(m.prev);
// 与实例序平行对齐(shader 按 gl_InstanceIndex 取)
m_Frame.skinOffsets.push_back(m.skinBase);
}
}
dst.instanceCount = s.result.instanceCount;
@@ -407,6 +636,12 @@ namespace Engine
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]);
}
// ---- 4.5 MDI 打包(已下线:移动视角下存在几何消失缺陷,待隔离修复)----
// PackIndirect(m_Frame.mainView, true);
// for (u32 c = 0; c < kMaxCascades; ++c) {
// PackIndirect(m_Frame.shadowCascades[c], false);
// }
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排
{
auto& tb = m_TranslucentScratch.result.batches;
@@ -466,4 +701,52 @@ namespace Engine
return true;
}
void SceneRenderer::PackIndirect(CullResult& view, bool byMaterial)
{
view.indirectCommands.clear();
view.indirectGroups.clear();
if (view.batches.empty()) return;
// 组键排序:megabuffer(VB 句柄)→ 材质(可选)→ mesh 槽位。
// 同 Model 的全部 mesh 共享同一 megabuffer,材质是主分组轴;
// 槽位兜底保证排序稳定(组内顺序无语义,防逐帧抖动)
std::sort(view.batches.begin(), view.batches.end(),
[byMaterial](const InstanceBatch& a, const InstanceBatch& b) {
const u64 va = reinterpret_cast<u64>(static_cast<VkBuffer>(
a.mesh->GetVertexBuffer()));
const u64 vb = reinterpret_cast<u64>(static_cast<VkBuffer>(
b.mesh->GetVertexBuffer()));
if (va != vb) return va < vb;
if (byMaterial && a.mesh->GetMaterial() != b.mesh->GetMaterial()) {
return reinterpret_cast<u64>(a.mesh->GetMaterial()) <
reinterpret_cast<u64>(b.mesh->GetMaterial());
}
return a.mesh->GetSlot() < b.mesh->GetSlot();
});
for (const auto& b : view.batches) {
const vk::Buffer vb = b.mesh->GetVertexBuffer();
Material* mat = byMaterial ? b.mesh->GetMaterial() : nullptr;
bool newGroup = view.indirectGroups.empty();
if (!newGroup) {
const IndirectGroup& g = view.indirectGroups.back();
newGroup = g.mesh->GetVertexBuffer() != vb ||
g.material != mat;
}
if (newGroup) {
view.indirectGroups.push_back({b.mesh, mat,
static_cast<u32>(view.indirectCommands.size()), 0});
}
view.indirectCommands.push_back({
b.mesh->GetLodIndexCount(b.lod),
b.count,
b.mesh->GetGlobalFirstIndex(b.lod),
b.mesh->GetVertexOffset(),
b.first});
++view.indirectGroups.back().commandCount;
}
}
} // namespace Engine
+18 -2
View File
@@ -166,6 +166,11 @@ void Device::PickPhysicalDevice()
m_DynamicRenderingSupported = features13.dynamicRendering;
m_Synchronization2Supported = features13.synchronization2;
// core 特性:MDI(drawCount>1)必需
m_MultiDrawIndirectSupported = features2.features.multiDrawIndirect == VK_TRUE;
// 1.2 特性:drawIndexedIndirectCount 必需(VUID-04445)
m_DrawIndirectCountSupported = features12.drawIndirectCount == VK_TRUE;
// 1.4 的新特性
bool localReadSupported = features14.dynamicRenderingLocalRead;
bool pushDescriptorSupported = features14.pushDescriptor;
@@ -183,6 +188,10 @@ void Device::PickPhysicalDevice()
ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No");
ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No");
ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "Yes" : "No");
ENGINE_INFO(" Multi Draw Indirect (1.0 core): {}",
m_MultiDrawIndirectSupported ? "Yes" : "No");
ENGINE_INFO(" Draw Indirect Count (1.2): {}",
m_DrawIndirectCountSupported ? "Yes" : "No");
}
bool Device::IsDeviceSuitable(vk::PhysicalDevice device)
@@ -244,6 +253,8 @@ void Device::CreateLogicalDevice()
}
vk::PhysicalDeviceFeatures features;
// MDI:按材质分组的 drawIndexedIndirectCount,drawCount>1 必需
features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE);
// ========================================================
// 启用 1.4 特性链
@@ -254,23 +265,28 @@ void Device::CreateLogicalDevice()
vk::PhysicalDeviceVulkan14Features features14{};
vk::PhysicalDeviceVulkan13Features features13{};
vk::PhysicalDeviceVulkan12Features features12{};
// 启用 1.3 的特性
features13.dynamicRendering = VK_TRUE;
features13.synchronization2 = VK_TRUE;
// 启用 1.2 的特性:MDI 的 drawIndexedIndirectCount(VUID-04445)
features12.drawIndirectCount = m_DrawIndirectCountSupported ? VK_TRUE : VK_FALSE;
// 启用 1.4 的新特性
features14.dynamicRenderingLocalRead = VK_TRUE;
features14.pushDescriptor = VK_TRUE;
// 链接:1.4 → 1.3 → (shader object)
// 链接:1.4 → 1.3 → 1.2 → (shader object)
features14.pNext = &features13;
features13.pNext = &features12;
// Shader Object 特性(如果支持)
vk::PhysicalDeviceShaderObjectFeaturesEXT shaderObjFeatures{};
if (m_ShaderObjectSupported) {
shaderObjFeatures.shaderObject = VK_TRUE;
features13.pNext = &shaderObjFeatures;
features12.pNext = &shaderObjFeatures;
}
// 扩展列表
+4 -1
View File
@@ -72,7 +72,10 @@ namespace Engine
.setImageColorSpace(surfaceFormat.colorSpace)
.setImageExtent(extent)
.setImageArrayLayers(1)
.setImageUsage(vk::ImageUsageFlagBits::eColorAttachment)
// COLOR_ATTACHMENT:ImGui 后端渲染;TRANSFER_DST:
// Direct 模式 BlitPass 直拷场景到 backbuffer
.setImageUsage(vk::ImageUsageFlagBits::eColorAttachment |
vk::ImageUsageFlagBits::eTransferDst)
.setImageSharingMode(vk::SharingMode::eExclusive)
.setPreTransform(caps.currentTransform)
.setCompositeAlpha(compositeAlpha)