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
+8 -1
View File
@@ -1,4 +1,11 @@
add_executable(HelloVulkan main.cpp) # App 层源文件:main + editor/(docking 编辑器,纯 App 代码)
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h)
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES})
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
target_link_libraries(HelloVulkan PRIVATE Engine) target_link_libraries(HelloVulkan PRIVATE Engine)
+202
View File
@@ -0,0 +1,202 @@
//
// Editor.cpp:docking 骨架实现。
//
// 布局方案(首帧 DockBuilder 一次,之后 imgui.ini 接管):
// ┌──────────────────────────────┬───────────────┐
// │ Hierarchy │ │
// ├──────────────────────────────┤ Inspector │
// │ Stats │ (28%) │
// └──────────────────────────────┴───────────────┘
//
#include "Editor.h"
#include "HierarchyPanel.h"
#include "InspectorPanel.h"
#include "ViewportPanel.h"
#include "StatsPanel.h"
#include <imgui.h>
#include <imgui_internal.h> // DockBuilder*(内部 API)
#include <GLFW/glfw3.h>
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
Editor::Editor() = default;
Editor::~Editor() = default;
void Editor::Init(const EditorContext& ctx)
{
m_Ctx = ctx;
m_Hierarchy = std::make_unique<HierarchyPanel>();
m_Inspector = std::make_unique<InspectorPanel>();
m_Viewport = std::make_unique<ViewportPanel>();
m_Stats = std::make_unique<StatsPanel>();
}
void Editor::Draw()
{
// host 窗口:铺满主视口,承载菜单栏 + DockSpace
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->Pos);
ImGui::SetNextWindowSize(viewport->Size);
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("##EditorDockHost", nullptr,
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus);
ImGui::PopStyleVar(3);
DrawMenuBar();
DrawDockSpace();
ImGui::End();
// 常规面板窗口(窗口名 = docking 身份)
if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected);
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
if (m_ShowViewport) m_Viewport->Draw(m_Ctx, m_VpInput);
if (m_ShowStats) m_Stats->Draw(m_Ctx);
DrawQuitModal();
}
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
// 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到
// Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查
// GetImGuiEnabled() 再问这里。
bool Editor::BlockingKeyboard() const
{
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
{
return false;
}
// 编辑器形态:Viewport 面板持焦才拥有键盘(UE Scene view 语义)
return !m_VpInput.focused;
}
bool Editor::BlockingMouse() const
{
// 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false)
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
{
return ImGui::GetIO().WantCaptureMouse;
}
// 编辑器形态:悬停 Viewport 才放行(模态框遮挡时 hovered=false)
return !m_VpInput.hovered;
}
void Editor::DrawMenuBar()
{
if (!ImGui::BeginMenuBar())
{
return;
}
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Quit", "Alt+F4"))
{
RequestQuit();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View"))
{
ImGui::MenuItem("Hierarchy", nullptr, &m_ShowHierarchy);
ImGui::MenuItem("Inspector", nullptr, &m_ShowInspector);
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
void Editor::DrawDockSpace()
{
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
// 默认布局:仅当 dock node 不存在时构建一次
//(imgui.ini 已保存过布局 → DockBuilderGetNode 非空 → 跳过)
if (!m_LayoutBuilt && ImGui::DockBuilderGetNode(dockspaceId) == nullptr)
{
m_LayoutBuilt = true;
ImGui::DockBuilderRemoveNode(dockspaceId);
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size);
ImGuiID dockLeft = 0, dockRight = 0, dockCenter = 0, dockStats = 0;
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Left, 0.20f,
&dockLeft, &dockCenter);
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Right, 0.28f,
&dockRight, &dockCenter);
ImGui::DockBuilderSplitNode(dockLeft, ImGuiDir_Down, 0.38f,
&dockStats, &dockLeft);
ImGui::DockBuilderDockWindow("Hierarchy", dockLeft);
ImGui::DockBuilderDockWindow("Stats", dockStats);
ImGui::DockBuilderDockWindow("Inspector", dockRight);
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
ImGui::DockBuilderFinish(dockspaceId);
}
ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f),
ImGuiDockNodeFlags_None);
}
void Editor::DrawQuitModal()
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
m_Ctx.app->Close();
}
}
+89
View File
@@ -0,0 +1,89 @@
//
// Editor:docking 编辑器骨架。
//
// 职责:
// - 顶栏菜单(File/View)+ 全屏 DockSpace + 首帧默认布局(DockBuilder)
// - 实体选中状态(entt::entity,Hierarchy 写 / Inspector 读)
// - 退出确认模态框(窗口关闭 → RequestQuit → Yes 时 Close)
//
// 面板窗口名(Hierarchy/Inspector/Viewport/Stats)即 docking 身份,
// imgui.ini 持久化布局;默认布局只在 dock node 为空时构建一次。
//
#ifndef HELLOVULKAN_EDITOR_H
#define HELLOVULKAN_EDITOR_H
#include "EditorContext.h"
#include "ViewportPanel.h" // ViewportInputState(成员按值持有)
#include <entt/entt.hpp>
#include <memory>
class HierarchyPanel;
class InspectorPanel;
class ViewportPanel;
class StatsPanel;
class Editor {
public:
Editor();
~Editor();
Editor(const Editor&) = delete;
Editor& operator=(const Editor&) = delete;
// 注入上下文(OnInit 阶段,指针必须全部有效)
void Init(const EditorContext& ctx);
// 每帧 UI 构建(OnImGuiRender 唯一入口)
void Draw();
// ---- 输入归属判定(Window 层输入捕获谓词;UE/Unity 的
// "viewport 持焦才拥有输入" 语义,ImGui 侧经自身后端
// 链式回调永远正常收输入)----
// 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态
// (Fullscreen)只在文本输入中让位(打字不触发快捷键)
bool BlockingKeyboard() const;
// 鼠标:编辑器形态 = 悬停 Viewport 才放行;游戏形态沿用
// WantCaptureMouse(UI 隐藏时自动放行)
bool BlockingMouse() const;
// 请求退出(弹确认框;窗口关闭事件走这里)
void RequestQuit() { m_ShowQuitConfirm = true; }
// ---- 选中状态 ----
void SelectEntity(entt::entity e) { m_Selected = e; }
entt::entity GetSelected() const { return m_Selected; }
private:
void DrawMenuBar(); // host 窗口内顶栏
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
void DrawQuitModal(); // 退出确认模态框
EditorContext m_Ctx{};
ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新)
std::unique_ptr<HierarchyPanel> m_Hierarchy;
std::unique_ptr<InspectorPanel> m_Inspector;
std::unique_ptr<ViewportPanel> m_Viewport;
std::unique_ptr<StatsPanel> m_Stats;
entt::entity m_Selected{entt::null};
// 面板显隐(View 菜单)
bool m_ShowHierarchy = true;
bool m_ShowInspector = true;
bool m_ShowViewport = true;
bool m_ShowStats = true;
// 默认布局只建一次(imgui.ini 已有布局则跳过)
bool m_LayoutBuilt = false;
// 退出确认链
bool m_ShowQuitConfirm = false;
bool m_QuitConfirmed = false;
};
#endif //HELLOVULKAN_EDITOR_H
+48
View File
@@ -0,0 +1,48 @@
//
// EditorContext:Editor 层与 SandboxApp 之间的桥接上下文。
//
// Editor 是 App 层代码(不属于引擎),它需要的引擎门面与沙盒能力
// 全部经此聚合注入——面板只认识 EditorContext,不反向依赖 main.cpp。
//
#ifndef HELLOVULKAN_EDITORCONTEXT_H
#define HELLOVULKAN_EDITORCONTEXT_H
#include <Engine/Core/Application.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
#include <functional>
// 可切换环境(IBL 动态切换):StatsPanel 的 Combo 与 SandboxApp 的
// 加载逻辑共用这一张表(下标即身份)。
inline constexpr const char *kEnvPaths[] = {
"assets/textures/venice_dawn_1_4k.hdr",
"assets/textures/rooitou_park_4k.hdr",
"assets/textures/pink_sunrise_4k.hdr",
"assets/textures/birchwood_4k.hdr",
};
inline constexpr const char *kEnvNames[] = {
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
};
inline constexpr int kEnvCount = 4;
inline constexpr int kDefaultEnv = 0;
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
struct EditorContext {
// 引擎门面
Engine::Renderer *renderer = nullptr;
Engine::ECS::Scene *scene = nullptr;
Engine::Assets::AssetManager *assets = nullptr;
Engine::Application *app = nullptr;
std::function<int()> getEnvIndex; // 当前环境表下标
std::function<void(int)> setEnvIndex;
// 冻结动画时钟(沙盒 spinners 用;保留原 Scene Debug 行为)
bool *pauseAnim = nullptr;
};
#endif //HELLOVULKAN_EDITORCONTEXT_H
+86
View File
@@ -0,0 +1,86 @@
//
// HierarchyPanel.cpp:实体列表实现。
//
#include "HierarchyPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
#include <imgui.h>
using namespace Engine;
namespace {
struct ScopedID {
explicit ScopedID(const u64 uuid) {
ImGui::PushID(reinterpret_cast<const void*>(uuid));
}
~ScopedID() {ImGui::PopID(); }
};
}
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
{
ImGui::Begin("Hierarchy");
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)
if (ImGui::Button("+ Create Entity"))
{
const auto e = ctx.scene->CreateEntity("Entity");
selected = e;
}
ImGui::Separator();
auto& reg = ctx.scene->GetRegistry();
reg.view<Engine::ECS::NameComponent>().each(
[&](const entt::entity e, const Engine::ECS::NameComponent& name)
{
const bool isSelected = (selected == e);
u64 rowId = static_cast < uint64_t >( static_cast < uint32_t >(e));
if (const auto * id = reg.try_get<Engine::ECS::IdComponent>(e)) {
rowId = id->uuid.Value();
}
const ScopedID scope (rowId);
const std::string label = name.name.empty() ? "(unnamed)" : name.name;
if (ImGui::Selectable(label.c_str(), isSelected))
{
selected = e;
}
// 右键菜单:删除(延迟销毁,帧尾统一 flush)
if (ImGui::BeginPopupContextItem("entity_ctx"))
{
if (ImGui::MenuItem("Delete"))
{
ctx.scene->DestroyEntity(e);
if (selected == e)
{
selected = entt::null;
}
}
ImGui::EndPopup();
}
// 组件徽标 + 悬停清单
std::string badge;
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
if (!badge.empty())
{
ImGui::SameLine();
ImGui::TextDisabled("%s", badge.c_str());
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(
"M=Mesh A=Animation C=Camera L=Light");
}
}
});
ImGui::End();
}
+20
View File
@@ -0,0 +1,20 @@
//
// HierarchyPanel:实体列表面板。
//
// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除
//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。
//
#ifndef HELLOVULKAN_HIERARCHYPANEL_H
#define HELLOVULKAN_HIERARCHYPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class HierarchyPanel {
public:
void Draw(const EditorContext& ctx, entt::entity& selected);
};
#endif //HELLOVULKAN_HIERARCHYPANEL_H
+220
View File
@@ -0,0 +1,220 @@
//
// InspectorPanel.cpp:组件编辑实现。
//
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
//
#include "InspectorPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/Renderer/Model.h"
#include <imgui.h>
#include <cstdio>
#include <vector>
namespace
{
// InputText 需要 char 缓冲;提交时写回 std::string
bool InputTextString(const char* label, std::string& str)
{
char buf[256];
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
if (ImGui::InputText(label, buf, sizeof(buf)))
{
str = buf;
return true;
}
return false;
}
}
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
ImGui::Begin("Inspector");
auto& reg = ctx.scene->GetRegistry();
if (selected == entt::null || !reg.valid(selected))
{
ImGui::TextDisabled("(no entity selected)");
ImGui::End();
return;
}
// ---- Name ----
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
{
InputTextString("Name", name->name);
}
ImGui::Separator();
// ---- Add Component ----
if (ImGui::Button("Add Component"))
{
ImGui::OpenPopup("add_component");
}
if (ImGui::BeginPopup("add_component"))
{
if (!reg.all_of<Engine::ECS::LightComponent>(selected) &&
ImGui::MenuItem("Light"))
{
reg.emplace<Engine::ECS::LightComponent>(selected);
}
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) &&
ImGui::MenuItem("Camera"))
{
reg.emplace<Engine::ECS::CameraComponent>(selected);
}
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) &&
ImGui::MenuItem("Animation"))
{
reg.emplace<Engine::ECS::AnimationComponent>(selected);
}
if (!reg.all_of<Engine::ECS::NameComponent>(selected) &&
ImGui::MenuItem("Name"))
{
reg.emplace<Engine::ECS::NameComponent>(selected);
}
ImGui::EndPopup();
}
ImGui::Separator();
// ---- Transform ----
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
{
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
{
glm::vec3 pos = t->GetPosition();
if (ImGui::DragFloat3("position", &pos.x, 0.05f))
{
t->SetPosition(pos);
}
glm::vec3 rot = t->GetRotation(); // 欧拉角(度)
if (ImGui::DragFloat3("rotation", &rot.x, 0.5f))
{
t->SetRotation(rot);
}
glm::vec3 scl = t->GetScale();
if (ImGui::DragFloat3("scale", &scl.x, 0.02f, 0.001f, 100.0f))
{
t->SetScale(scl);
}
}
}
// ---- MeshRenderer ----
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
{
if (ImGui::CollapsingHeader("MeshRenderer",
ImGuiTreeNodeFlags_DefaultOpen))
{
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
{
const auto& clips = model->GetClips();
ImGui::Text("model: valid (%u clips, %u bones)",
static_cast<unsigned>(clips.size()),
static_cast<unsigned>(model->GetBoneCount()));
ImGui::TextDisabled("(mesh asset is read-only here)");
}
else
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)");
}
}
}
// ---- Light ----
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
{
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
{
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
}
ImGui::ColorEdit3("color", &light->color.x);
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
if (light->type == Engine::ECS::LightComponent::Type::Directional)
{
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
ImGui::Checkbox("castShadow", &light->castShadow);
}
else
{
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)");
}
}
}
// ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
{
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
{
// primary 互斥:启用本相机时清掉其余实体的 primary
//(引擎取第一个 primary 作为渲染视角)
reg.view<Engine::ECS::CameraComponent>().each(
[&](entt::entity e, Engine::ECS::CameraComponent& c)
{
if (e != selected)
{
c.primary = false;
}
});
}
}
}
// ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{
if (ImGui::CollapsingHeader("Animation",
ImGuiTreeNodeFlags_DefaultOpen))
{
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr;
if (model != nullptr && model->HasAnimations())
{
const auto& clips = model->GetClips();
std::vector<const char*> clipNames;
clipNames.reserve(clips.size());
for (const auto& c : clips)
{
clipNames.push_back(c.name.c_str());
}
int cur = anim->clipIndex;
if (ImGui::Combo("clip", &cur, clipNames.data(),
static_cast<int>(clipNames.size())))
{
anim->Play(cur); // 触发 crossfade(fade 秒)
}
ImGui::Checkbox("playing", &anim->playing);
ImGui::SameLine();
ImGui::Checkbox("loop", &anim->loop);
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
if (anim->clipIndex >= 0 &&
anim->clipIndex < static_cast<int>(clips.size()))
{
ImGui::Text("t: %.2f / %.2f s", anim->time,
clips[anim->clipIndex].duration);
}
}
else
{
ImGui::TextDisabled("(no animation clips on model)");
}
}
}
ImGui::End();
}
+22
View File
@@ -0,0 +1,22 @@
//
// InspectorPanel:选中实体的组件检查/编辑面板。
//
// 泛化编辑:任何实体(不是沙盒特例)的 Name / Transform /
// MeshRenderer / Light / Camera / Animation 全部在此编辑。
// 沙盒原 Scene Debug 里的 KeyLight、动画 clip、gun scale 控件
// 全部由这里的通用控件替代。
//
#ifndef HELLOVULKAN_INSPECTORPANEL_H
#define HELLOVULKAN_INSPECTORPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class InspectorPanel {
public:
void Draw(const EditorContext& ctx, entt::entity selected);
};
#endif //HELLOVULKAN_INSPECTORPANEL_H
+173
View File
@@ -0,0 +1,173 @@
//
// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。
//
#include "StatsPanel.h"
#include "Engine/ECS/Components.h"
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
using namespace Engine; // f32(Engine/Core/Types.h)
void StatsPanel::Draw(const EditorContext& ctx)
{
ImGui::Begin("Stats");
auto& renderer = *ctx.renderer;
auto& app = *ctx.app;
// ---- Camera:主相机位姿(只读遥测;位姿编辑走 Inspector)----
{
Engine::ECS::TransformComponent* camT = nullptr;
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
{
const glm::vec3& pos = camT->GetPosition();
ImGui::Text("Camera pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
}
else
{
ImGui::TextDisabled("(no primary camera)");
}
ImGui::Separator();
}
// ---- Scene:动画时钟冻结(沙盒 spinners 用)----
if (ctx.pauseAnim != nullptr)
{
ImGui::Checkbox("Pause Animation", ctx.pauseAnim);
}
// ---- Rendering:渲染管线参数与开关 ----
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
{
f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure);
}
bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces);
}
// Bloom:开关即显隐——勾上才显示参数
bool bloom = renderer.GetBloomEnabled();
if (ImGui::Checkbox("Bloom", &bloom))
{
renderer.SetBloomEnabled(bloom);
}
if (bloom)
{
f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
{
renderer.SetBloomThreshold(threshold);
}
f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength);
}
}
// 无参数的纯开关:横排平铺
bool shadows = renderer.GetShadowsEnabled();
if (ImGui::Checkbox("Shadows", &shadows))
{
renderer.SetShadowsEnabled(shadows);
}
ImGui::SameLine();
bool taa = renderer.GetTAAEnabled();
if (ImGui::Checkbox("TAA", &taa))
{
renderer.SetTAAEnabled(taa);
}
ImGui::SameLine();
bool lod = renderer.GetLodEnabled();
if (ImGui::Checkbox("LOD", &lod))
{
renderer.SetLodEnabled(lod); // 关 = 恒绘原网格
}
}
// ---- Environment:IBL 环境 + 调试视图(决定"看什么")----
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
{
bool& drawSkybox = ctx.scene->GetSettings().DrawSkybox;
ImGui::Checkbox("DrawSkybox", &drawSkybox);
if (drawSkybox && ctx.getEnvIndex)
{
int envIdx = ctx.getEnvIndex();
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
{
ctx.setEnvIndex(envIdx);
const auto env = ctx.assets->LoadEnvironment(kEnvPaths[envIdx]);
ctx.renderer->SetEnvironment(ctx.assets->GetEnvironment(env));
}
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}
else
{
ImGui::ColorEdit4("ClearColor",
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
}
}
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度)----
if (ImGui::CollapsingHeader("GPU Timings"))
{
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated())
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
}
// ---- Frame:帧率控制与显示 ----
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
{
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / app.GetDeltaTime(),
static_cast<unsigned long long>(app.GetFrameCount()));
}
ImGui::End();
}
+19
View File
@@ -0,0 +1,19 @@
//
// StatsPanel:遥测与全局设置面板(原 Scene Debug 拆入)。
//
// Camera 位姿 / Rendering 开关 / Environment(IBL+DebugView+ClearColor) /
// GPU Timings / Frame(VSync、fps)。KeyLight、动画 clip、gun scale
// 等沙盒特例编辑已泛化进 Inspector,此处不再重复。
//
#ifndef HELLOVULKAN_STATSPANEL_H
#define HELLOVULKAN_STATSPANEL_H
#include "EditorContext.h"
class StatsPanel {
public:
void Draw(const EditorContext& ctx);
};
#endif //HELLOVULKAN_STATSPANEL_H
+53
View File
@@ -0,0 +1,53 @@
//
// ViewportPanel.cpp:显示模式单选 + ImGui::Image + 分辨率上报。
// 逻辑照搬原 main.cpp 的 Viewport 窗口(行为不变)。
//
#include "ViewportPanel.h"
#include <imgui.h>
#include <vulkan/vulkan.h> // vk::Extent2D 经 Renderer 头已带入,显式无妨
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
{
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
out.hovered = ImGui::IsWindowHovered();
out.focused = ImGui::IsWindowFocused();
auto &renderer = *ctx.renderer;
const int mode = renderer.GetDisplayMode();
const void *sceneTex = renderer.GetSceneTexture();
if (sceneTex != nullptr && mode == kDisplayViewport) {
// 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满
// 面板;下一帧帧首 BeginFrameSync 生效(一帧延迟,不可见)
const ImVec2 avail = ImGui::GetContentRegionAvail();
const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale;
const u32 wantW = static_cast<u32>(avail.x * fb.x + 0.5f);
const u32 wantH = static_cast<u32>(avail.y * fb.y + 0.5f);
if (wantW >= 16 && wantH >= 16) {
renderer.SetViewportSize(wantW, wantH);
}
// 纹理 = 渲染分辨率:等比适配面板可用区域
const vk::Extent2D ext = renderer.GetRenderExtent();
const f32 aspect =
ext.height != 0 ? f32(ext.width) / f32(ext.height) : 1.0f;
ImVec2 size = avail;
if (avail.x / avail.y > aspect) size.x = avail.y * aspect;
else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
} else {
ImGui::TextDisabled("(scene texture not ready)");
}
ImGui::End();
}
ImGui::PopStyleVar();
}
+26
View File
@@ -0,0 +1,26 @@
//
// ViewportPanel:场景渲染输出面板。
//
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
//
#ifndef HELLOVULKAN_VIEWPORTPANEL_H
#define HELLOVULKAN_VIEWPORTPANEL_H
#include "EditorContext.h"
// Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定
// 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标)
struct ViewportInputState {
bool hovered = false; // 鼠标悬停在 Viewport 面板上
bool focused = false; // Viewport 面板持有键盘焦点
};
class ViewportPanel {
public:
void Draw(const EditorContext& ctx, ViewportInputState& out);
};
#endif //HELLOVULKAN_VIEWPORTPANEL_H
+118 -356
View File
@@ -3,12 +3,18 @@
// //
// SandboxApp:第一个基于 Application 框架的程序。 // SandboxApp:第一个基于 Application 框架的程序。
// //
// main 的全部职责:定义一个 Application 派生类并运行它。 // main 的职责边界:
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理; // - 定义 Application 派生类并运行它
// 这里只写"程序逻辑":创建实体、逐帧更新组件、事件响应。 // - "程序逻辑":场景搭建(实体/组件)、逐帧更新、事件响应、
// 异步环境加载
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
// 任何 ImGui 控件)
//
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
// //
#include <Engine/Core/Application.h> #include <Engine/Core/Application.h>
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/Core/Stats.h> #include <Engine/Core/Stats.h>
#include <Engine/Core/AsyncTask.h> #include <Engine/Core/AsyncTask.h>
@@ -17,37 +23,20 @@
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/IblGenerator.h> #include <Engine/Renderer/Model.h>
#include <imgui.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include <atomic> #include "Editor.h"
#include <thread> #include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
using namespace Engine; using namespace Engine;
using namespace Engine::ECS; using namespace Engine::ECS;
namespace namespace
{ {
// 可切换环境(IBL 动态切换演示):路径 → AssetManager 缓存去重,
// 重复选择同一环境零成本;首选路径触发一次 compute 预积分
constexpr const char* kEnvPaths[] = {
"assets/textures/venice_dawn_1_4k.hdr",
"assets/textures/rooitou_park_4k.hdr",
"assets/textures/pink_sunrise_4k.hdr",
"assets/textures/birchwood_4k.hdr",
};
constexpr const char* kEnvNames[] = {
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
};
constexpr int kEnvCount = 4;
constexpr int kDefaultEnv = 0;
constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx"; constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx";
class SandboxApp final : public Application class SandboxApp final : public Application
@@ -60,11 +49,6 @@ namespace
~SandboxApp() override ~SandboxApp() override
{ {
// worker 只做 CPU 解码(不碰 Vulkan),在任何 GPU 资源
// 析构之前 join 即安全;派生析构先于基类成员析构执行
if (m_EnvWorker.joinable()) {
m_EnvWorker.join();
}
} }
protected: protected:
@@ -73,69 +57,83 @@ namespace
void OnInit() override void OnInit() override
{ {
Scene& scene = GetScene(); Scene& scene = GetScene();
Renderer& renderer = GetRenderer();
renderer.SetDisplayMode(kDisplayViewport);
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
// ImGui 叠加层(框架只做管线桥接;UI 内容 = docking Editor)
EnableImGui(); EnableImGui();
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级), // Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
// 其余环境在 ImGui 里按需 lazy 加载后切换 InitEditor();
const auto env = GetAssets().LoadEnvironment(
kEnvPaths[kDefaultEnv]); SetEnv();
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env)); const auto suntemple = GetAssets().LoadModel("assets/models/demo/asset.fbx");
auto entityhandle = scene.CreateEntity("SunTemple");
entityhandle.AddComponent<MeshRendererComponent>(MeshRendererComponent{suntemple});
scene.CreateEntity();
scene.CreateEntity();
scene.CreateEntity();
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
} }
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产, /*
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5) const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj"); if (gun.Valid()) {
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf"); m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
// 大模型异步加载:assimp 解析在 worker 线程(SunTemple 约 3s), m_Gun.AddComponent<AnimationComponent>();
// 完成后 OnUpdate 收割 → GPU 构建 → 建实体(cm 尺度 → 0.001) auto& gt = m_Gun.GetComponent<TransformComponent>();
m_TempleTask.Run([] { return Model::ParseFromDisk(kTemplePath); }); gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
m_TempleScale = 0.001f; } else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
int countSize = 0; int countSize = 0;
for (int i = 0; i < countSize; i ++) if (countSize) {
{ // 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
for (int j = 0; j < countSize; j ++) // 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
for (int i = 0; i < countSize; i ++)
{ {
auto m = scene.CreateEntity(); for (int j = 0; j < countSize; j ++)
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model}); {
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2}); auto m = scene.CreateEntity();
m_Spinners.push_back(m); m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
m_Spinners.push_back(m);
}
} }
} }
// 地面:程序化纯色平面(半边长 15,中心 y=-2) // 地面:程序化纯色平面(半边长 15,中心 y=-2)
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145); const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
// 透明管线验证:两片叠放的半透明平面(前后各一,看混合与排序)
const auto glass1 = GetAssets().CreateTransparentPlane(
2.0f, 80, 180, 255, 110);
const auto glass2 = GetAssets().CreateTransparentPlane(
2.0f, 255, 120, 80, 110);
auto g1 = scene.CreateEntity("GlassNear");
g1.AddComponent<MeshRendererComponent>(MeshRendererComponent{glass1});
g1.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 0.5f, -1.0f));
auto g2 = scene.CreateEntity("GlassFar");
g2.AddComponent<MeshRendererComponent>(MeshRendererComponent{glass2});
g2.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 1.0f, -3.0f));
m_Ground = scene.CreateEntity("Ground"); m_Ground = scene.CreateEntity("Ground");
m_Ground.GetComponent<TransformComponent>() m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose .SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground}); m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
*/
// 相机实体(primary = 当前渲染视角;位姿存实体 Transform) // 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
m_Camera = scene.CreateEntity("MainCamera"); m_Camera = scene.CreateEntity("MainCamera");
auto& camera = m_Camera.AddComponent<CameraComponent>(); auto& camera = m_Camera.AddComponent<CameraComponent>();
camera.primary = true; camera.primary = true;
m_Camera.GetComponent<TransformComponent>() m_Camera.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f)); .SetPosition(glm::vec3(0.0f, 12.0f, 22.0f));
m_Camera.GetComponent<TransformComponent>()
.SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f));
// 灯光实体(第一个方向光 = 阴影光源) // 灯光实体(第一个方向光 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight"); m_KeyLight = scene.CreateEntity("KeyLight");
@@ -147,6 +145,7 @@ namespace
.castShadow = true, .castShadow = true,
}); });
/*
scene.CreateEntity("FillLight") scene.CreateEntity("FillLight")
.AddComponent<LightComponent>( .AddComponent<LightComponent>(
LightComponent{ LightComponent{
@@ -154,7 +153,9 @@ namespace
.direction = glm::vec3(0.5f, -0.5f, 0.3f), .direction = glm::vec3(0.5f, -0.5f, 0.3f),
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f) .color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
}); });
*/
/*
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减) // 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
auto fire = scene.CreateEntity("FirePitLight"); auto fire = scene.CreateEntity("FirePitLight");
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f)); fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
@@ -173,28 +174,11 @@ namespace
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝 .color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
.radius = 10.0f, .radius = 10.0f,
}); });
*/
} }
void OnUpdate(f32 deltaTime) override void OnUpdate(f32 deltaTime) override
{ {
// 异步模型收割:解析完成 → GPU 构建 → 建实体
if (m_TempleTask.Done()) {
ParsedModel parsed = m_TempleTask.Take();
const auto ref = GetAssets().LoadModel(kTemplePath,
std::move(parsed));
if (ref.Valid()) {
auto temple = GetScene().CreateEntity("SunTemple");
temple.AddComponent<MeshRendererComponent>(
MeshRendererComponent{ref});
temple.GetComponent<TransformComponent>()
.SetScale(glm::vec3{m_TempleScale});
ENGINE_INFO("[ASYNC] SunTemple ready, entity created");
}
}
// 异步环境加载:每帧收割完成的解码结果
PollEnvironment();
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff // 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到) // 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
if (!m_PauseAnim) if (!m_PauseAnim)
@@ -216,268 +200,20 @@ namespace
} }
} }
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
void OnImGuiRender() override
{
m_Editor->Draw();
}
// ---- 事件 ---- // ---- 事件 ----
// ---- 异步环境加载(decode 后台线程 / GPU 生成主线程)----
// decode 占切换耗时大头(4k ≈ 130ms);worker 只产出纯 CPU
// 像素数据,Vulkan 生成回主线程(设备非线程安全)。
// 同一时刻最多一个 in-flight,忙碌期的新请求记为 pending 接力。
void RequestEnvironment(int idx)
{
if (m_EnvLoadBusy)
{
m_EnvPending = idx;
return;
}
StartEnvDecode(idx);
}
void StartEnvDecode(int idx)
{
m_EnvLoadBusy = true;
m_EnvLoadDone.store(false);
m_EnvLoadingPath = kEnvPaths[idx];
m_EnvWorker = std::thread([this, idx]() {
m_EnvDecoded = IblGenerator::DecodeHdr(kEnvPaths[idx]);
m_EnvLoadDone.store(true); // release:解码结果对主线程可见
});
}
void PollEnvironment()
{
if (!m_EnvLoadBusy || !m_EnvLoadDone.load())
{
return;
}
m_EnvWorker.join(); // join = 同步点,解码结果此后归主线程
m_EnvLoadBusy = false;
if (m_EnvDecoded.Valid())
{
const auto ref = GetAssets().LoadEnvironment(
m_EnvLoadingPath, std::move(m_EnvDecoded));
if (ref.Valid())
{
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(ref));
}
}
m_EnvDecoded = {};
// 忙碌期间的最新请求接力(rapid switch 只保留最终意图)
if (m_EnvPending >= 0)
{
const int next = m_EnvPending;
m_EnvPending = -1;
StartEnvDecode(next);
}
}
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
void OnRenderOverlay() override
{
ImGui::Begin("Scene Debug");
auto& renderer = GetRenderer();
// ---- Scene:场景内容(灯/动画) ----
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
{
// 阴影光源(无开关,控件平铺聚拢)
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>())
{
ImGui::Text("Key Light (shadow caster)");
ImGui::SliderFloat3("direction",
&light->direction.x, -1.0f, 1.0f);
ImGui::SliderFloat("intensity", &light->color.a, 0.0f, 20.0f);
if (ImGui::Button("reset"))
{
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
light->color.a = 2.0f;
}
}
ImGui::Checkbox("Pause Animation", &m_PauseAnim);
}
// ---- Rendering:渲染管线参数与开关 ----
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
{
// Tonemap(无开关,平铺)
f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure);
}
bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces);
}
// Bloom:开关即显隐——勾上才显示参数
bool bloom = renderer.GetBloomEnabled();
if (ImGui::Checkbox("Bloom", &bloom))
{
renderer.SetBloomEnabled(bloom);
}
if (bloom)
{
f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
{
renderer.SetBloomThreshold(threshold);
}
f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength);
}
}
// 无参数的纯开关:横排平铺
bool shadows = renderer.GetShadowsEnabled();
if (ImGui::Checkbox("Shadows", &shadows))
{
renderer.SetShadowsEnabled(shadows);
}
ImGui::SameLine();
bool taa = renderer.GetTAAEnabled();
if (ImGui::Checkbox("TAA", &taa))
{
renderer.SetTAAEnabled(taa);
}
ImGui::SameLine();
bool lod = renderer.GetLodEnabled();
if (ImGui::Checkbox("LOD", &lod))
{
renderer.SetLodEnabled(lod); // 关 = 恒绘原网格
}
}
// ---- Environment:IBL 环境 + 调试视图(决定"看什么") ----
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
{
// 动态环境切换(Assets 层加载 → Renderer 换绑;缓存去重)
ImGui::Text("Environment (IBL)");
bool& DrawSkybox = GetScene().GetSettings().DrawSkybox;
ImGui::Checkbox("DrawSkybox", &DrawSkybox);
if (DrawSkybox)
{
int envIdx = m_EnvIndex;
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
{
// 异步:解码后台线程,完成后 OnUpdate 里换绑
m_EnvIndex = envIdx;
RequestEnvironment(envIdx);
}
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" :
"(switch = compute pre-integrate, ms-level)");
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}else
{
ImGui::ColorEdit4("ClearColor", glm::value_ptr(GetScene().GetSettings().ClearColor));
}
}
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度) ----
if (ImGui::CollapsingHeader("GPU Timings"))
{
// Pass 级 GPU 耗时(RG timestamp)
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
}
// ---- Frame:帧率控制与显示 ----
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
{
auto& app = *this;
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / GetDeltaTime(),
static_cast<unsigned long long>(GetFrameCount()));
}
ImGui::End();
// 自定义重载事件: 重载关闭事件模态框
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
Close();
}
}
}
void OnWindowClose(const Event& event) override void OnWindowClose(const Event& event) override
{ {
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
SetImGuiEnabled(true); SetImGuiEnabled(true);
m_ShowQuitConfirm = true; m_Editor->RequestQuit();
} }
void OnKeyPress(const Event& event) override void OnKeyPress(const Event& event) override
@@ -498,25 +234,51 @@ namespace
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height); ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
} }
void SetEnv(int index = 0) {
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
// 其余环境在 Stats 面板里按需 lazy 加载后切换
const auto env = GetAssets().LoadEnvironment(kEnvPaths[index]);
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
}
}
private: private:
bool m_ShowQuitConfirm = false; // Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
bool m_QuitConfirmed = false; void InitEditor()
{
EditorContext ctx;
ctx.renderer = &GetRenderer();
ctx.scene = &GetScene();
ctx.assets = &GetAssets();
ctx.app = this;
ctx.getEnvIndex = [this] { return m_EnvIndex; };
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
ctx.pauseAnim = &m_PauseAnim;
m_Editor->Init(ctx);
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
//(全屏 dock host 让它恒真,输入全死)。改为 Editor 按帧
// 判定——Viewport hover/focus 拥有输入(Hazel/UE 语义);
// UI 隐藏(反引号)时 Draw 不再执行,直接全放行
GetWindow().SetInputCapture(
[this] { return GetImGuiEnabled() && m_Editor->BlockingKeyboard(); },
[this] { return GetImGuiEnabled() && m_Editor->BlockingMouse(); });
}
private:
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空 EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
EntityHandle m_Ground{};
EntityHandle m_KeyLight{}; EntityHandle m_KeyLight{};
EntityHandle m_Ground{};
EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911)
std::vector<EntityHandle> m_Spinners; std::vector<EntityHandle> m_Spinners;
f32 m_Time = 0.0f; f32 m_Time = 0.0f;
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用) bool m_PauseAnim = true; // 冻结动画时钟(Editor 面板可开关)
int m_EnvIndex = kDefaultEnv; int m_EnvIndex = kDefaultEnv;
// ---- 异步环境加载状态(单 in-flight + pending 接力)----
std::thread m_EnvWorker;
std::string m_EnvLoadingPath;
DecodedEquirect m_EnvDecoded; // worker 写 → join 后主线程读
std::atomic<bool> m_EnvLoadDone{false};
bool m_EnvLoadBusy = false;
int m_EnvPending = -1;
// ---- 异步模型加载(SunTemple)---- // ---- 异步模型加载(SunTemple)----
AsyncTask<ParsedModel> m_TempleTask; AsyncTask<ParsedModel> m_TempleTask;
f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用) f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用)
+1 -1
View File
@@ -88,7 +88,7 @@ namespace Engine {
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。 // 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。 // 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
virtual void OnRenderOverlay() {} virtual void OnImGuiRender() {}
// 主循环结束后、析构前(保存、清理场景等) // 主循环结束后、析构前(保存、清理场景等)
virtual void OnShutdown() {} virtual void OnShutdown() {}
+13
View File
@@ -63,8 +63,15 @@ namespace Engine {
// 输入事件系统 // 输入事件系统
bool IsKeyPressed(int key) const; bool IsKeyPressed(int key) const;
// UI 捕获鼠标(ImGui 悬停)时返回 false——面板上的按键归 ImGui
bool IsMouseButtonPressed(int button) const;
void GetMouseDelta(float& dx, float& dy) const; void GetMouseDelta(float& dx, float& dy) const;
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
void SetCursorCaptured(bool captured);
bool IsCursorCaptured() const { return m_CursorCaptured; }
private: private:
@@ -80,6 +87,12 @@ namespace Engine {
CaptureFn m_KeyboardCapture; CaptureFn m_KeyboardCapture;
CaptureFn m_MouseCapture; 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) // GLFW 静态回调(通过 window user pointer 找回 this)
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height); static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); 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 句柄,性能)。 // ECS 内部遍历不用它(用 index+generation 句柄,性能)。
struct IdComponent { struct IdComponent {
UUID uuid; UUID uuid;
operator UUID() const { return uuid; }
}; };
// 实体名(调试/查询用;大规模场景建议演进为哈希 id) // 实体名(调试/查询用;大规模场景建议演进为哈希 id)
@@ -83,6 +85,37 @@ namespace Engine::ECS {
ModelRef model; 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 的实体是当前渲染视角。 // 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent { struct CameraComponent {
Camera camera; Camera camera;
+1 -1
View File
@@ -50,7 +50,7 @@ namespace Engine::ECS {
// ---- 实体 ---- // ---- 实体 ----
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy() // 返回句柄表皮: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 DestroyEntity(Entity e); // 延迟:帧尾统一销毁
void FlushDestroyQueue(); // 每帧末尾由 Application 调用 void FlushDestroyQueue(); // 每帧末尾由 Application 调用
+14
View File
@@ -18,6 +18,8 @@
#pragma once #pragma once
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h> // kMaxFramesInFlight
#include <imgui.h>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
namespace Engine { namespace Engine {
@@ -35,6 +37,17 @@ namespace Engine {
ImGuiLayer(const ImGuiLayer&) = delete; ImGuiLayer(const ImGuiLayer&) = delete;
ImGuiLayer& operator=(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 驱动)---- // ---- 每帧三段式(由 Application::Run 驱动)----
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间, // 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
@@ -57,6 +70,7 @@ namespace Engine {
Device& m_Device; Device& m_Device;
vk::DescriptorPool m_Pool; vk::DescriptorPool m_Pool;
bool m_Enabled = true; bool m_Enabled = true;
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
}; };
} // namespace Engine } // namespace Engine
@@ -98,6 +98,8 @@ namespace Engine::RG {
// 按句柄取当前帧槽位的物理资源(image/view) // 按句柄取当前帧槽位的物理资源(image/view)
vk::ImageView View(TextureHandle h) const; vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const; vk::Image Image(TextureHandle h) const;
// 物理尺寸(导入资源 = SetImportedExtent 设定值;创建资源 = 物理镜像)
vk::Extent2D Extent(TextureHandle h) const;
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。 // 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。 // 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
@@ -129,6 +131,7 @@ namespace Engine::RG {
// 导入资源每帧绑定 // 导入资源每帧绑定
vk::Image importedImage{}; vk::Image importedImage{};
vk::ImageView importedView{}; vk::ImageView importedView{};
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体) // 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
struct SlotState { struct SlotState {
@@ -207,6 +210,11 @@ namespace Engine::RG {
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源 // 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view); void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
// 导入资源的物理尺寸(swapchain 重建后同步更新)。pass 渲染区域
// 跟实际附件尺寸走:backbuffer = 窗口尺寸,与目标尺寸(场景渲染
// 分辨率)解耦——Editor 视口模式下二者不同
void SetImportedExtent(TextureHandle h, vk::Extent2D extent);
// 指定帧槽位的物理镜像 view(Compile 后有效)。 // 指定帧槽位的物理镜像 view(Compile 后有效)。
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。 // 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。 // 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
+4 -3
View File
@@ -22,7 +22,7 @@ namespace Engine {
class Camera { class Camera {
public: public:
// 输入驱动:WASD/Space/Shift 移动 + 鼠标视角(需 SetMouseLookEnabled)。 // 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。
// 全部写回传入的 transform,有变更才 Compose。 // 全部写回传入的 transform,有变更才 Compose。
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime); void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime);
@@ -35,14 +35,15 @@ namespace Engine {
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; } void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; } void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
// 鼠标视角默认关闭(需要先做 cursor capture 再打开) // 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获),
// 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; } void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; } bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
private: private:
f32 m_MoveSpeed = 2.5f; f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f; f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = false; bool m_MouseLookEnabled = true;
}; };
} // namespace Engine } // namespace Engine
+18 -9
View File
@@ -7,13 +7,19 @@
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性) // 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定) // 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性) // 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
// 5 材质参数 UBO(fragment,kParamsBinding) // 5 roughness(独立粗糙度图,R 通道——FBX/OBJ specular 工作流,
// glTF 合图工作流不用此槽)
// 6 材质参数 UBO(fragment,kParamsBinding)
// //
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。 // 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
// 因子(metallicFactor/roughnessFactor)乘在 MR 贴图上: // 因子(metallicFactor/roughnessFactor)乘在贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1) // glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1),
// OBJ(specular 工作流,独立 roughness 图或无图)→ metallic=0, // factors.z=0 → shader 直接读 MR.g 当粗糙度
// roughness 因子按贴图有无/Ns 换算——金属度强制 0 // 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 #ifndef HELLOVULKAN_MATERIAL_H
@@ -33,9 +39,11 @@ namespace Engine {
class Texture; class Texture;
class UniformBuffer; class UniformBuffer;
// 材质因子(乘 MR 贴图;无贴图时即最终值) // 材质因子(乘贴图;无贴图时即最终值)
// x = metallic 因子 y = roughness 因子
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
struct MaterialParams { 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 { class Material {
@@ -47,8 +55,9 @@ namespace Engine {
MetallicRoughness = 2, MetallicRoughness = 2,
Emissive = 3, Emissive = 3,
Ao = 4, Ao = 4,
kSlotCount = 5, // 纹理槽数 Roughness = 5, // 独立粗糙度图(R 通道)
kParamsBinding = 5, // 参数 UBO binding kSlotCount = 6, // 纹理槽数
kParamsBinding = 6, // 参数 UBO binding
}; };
Material(Device& device, MemoryAllocator& allocator, Material(Device& device, MemoryAllocator& allocator,
+10
View File
@@ -8,6 +8,7 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Vertex.h> #include <Engine/Renderer/Vertex.h>
#include <Engine/Vulkan/Buffer.h>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <vector> #include <vector>
@@ -50,6 +51,15 @@ namespace Engine
// 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash) // 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash)
u32 GetSlot() const { return m_Slot; } 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 { struct Bounds {
glm::vec3 center{0.0f}; glm::vec3 center{0.0f};
+83 -4
View File
@@ -10,7 +10,9 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h> #include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
@@ -41,10 +43,52 @@ namespace Engine
MaterialParams params; MaterialParams params;
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列) 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 { struct ParsedModel {
std::string path; std::string path;
bool isGltf = false; bool isGltf = false;
std::vector<ParsedMesh> meshes; std::vector<ParsedMesh> meshes;
std::vector<ModelNode> nodes;
std::vector<ModelBone> bones;
std::vector<AnimationClip> clips;
bool Valid() const { return !meshes.empty(); } bool Valid() const { return !meshes.empty(); }
}; };
@@ -92,6 +136,13 @@ namespace Engine
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环 // 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
const Mesh::Bounds& GetBounds() const { return m_Bounds; } 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: private:
Model(Device& device, MemoryAllocator& allocator, Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
@@ -105,13 +156,33 @@ namespace Engine
vk::DescriptorSetLayout textureLayout); vk::DescriptorSetLayout textureLayout);
void LoadModel(const std::string& path); 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, static void ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out, std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir, 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, static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir, 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(); void InitDefaultTextures();
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除) // 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
@@ -121,10 +192,13 @@ namespace Engine
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material, // 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。 // 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot;params = metallic/roughness 因子 // paths 下标 = Material::Slot;params = metallic/roughness 因子;
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount], Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params, const MaterialParams& params,
bool transparent = false); bool transparent = false,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides = {});
Device& m_Device; Device& m_Device;
MemoryAllocator& m_Allocator; MemoryAllocator& m_Allocator;
@@ -142,6 +216,11 @@ namespace Engine
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除) Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
std::string m_ModelPath; std::string m_ModelPath;
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV 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 } // 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; void Register(RG::RenderGraph& rg) override;
private: 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; SceneGpu& m_Gpu;
FrameSceneData& m_Frame; FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor; RG::TextureHandle m_HDRColor;
@@ -56,6 +56,7 @@ namespace Engine {
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频) kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级) kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道) kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容)
kDebugViewCount kDebugViewCount
}; };
@@ -11,6 +11,7 @@
#define HELLOVULKAN_IMGUIPASS_H #define HELLOVULKAN_IMGUIPASS_H
#include <Engine/Renderer/Passes/RenderPass.h> #include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h> // FrameSceneData(displayMode 帧契约)
namespace Engine { namespace Engine {
@@ -21,15 +22,22 @@ namespace Engine {
class ImGuiPass final : public RenderPass { class ImGuiPass final : public RenderPass {
public: public:
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调 // 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
//(EnableImGui 时机由派生类决定),执行期解引用取最新值 //(EnableImGui 时机由派生类决定),执行期解引用取最新值。
ImGuiPass(ImGuiLayer** layer, RG::TextureHandle backbuffer) // presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
: m_Layer(layer), m_Backbuffer(backbuffer) {} // 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; void Register(RG::RenderGraph& rg) override;
private: private:
ImGuiLayer** m_Layer; ImGuiLayer** m_Layer;
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Backbuffer; RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_PresentTarget;
}; };
} // namespace Passes } // namespace Passes
@@ -25,22 +25,36 @@ namespace Engine {
class ShaderManager; class ShaderManager;
class UniformBuffer; class UniformBuffer;
class Cubemap; class Cubemap;
class Buffer;
class MemoryAllocator;
namespace Passes { namespace Passes {
class SceneGpu { class SceneGpu {
public: public:
SceneGpu(Device& device, DescriptorManager& descriptors, SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders, ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer, UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer); UniformBuffer& prevObjectBuffer,
UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer);
~SceneGpu(); ~SceneGpu();
SceneGpu(const SceneGpu&) = delete; SceneGpu(const SceneGpu&) = delete;
SceneGpu& operator=(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/)---- // ---- 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 textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube} vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube}
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO) vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
@@ -54,7 +68,16 @@ namespace Engine {
//(省 shader 出界分支) //(省 shader 出界分支)
vk::Sampler shadowSampler; 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{}; vk::ShaderEXT forwardVert{}, forwardFrag{};
// 透明变体:同 simple_shader + gl_FragDepth=1.0(见 frag 注释)
vk::ShaderEXT translucentFrag{};
vk::ShaderEXT shadowVert{}, shadowFrag{}; vk::ShaderEXT shadowVert{}, shadowFrag{};
// RG 重编译后重绑阴影图(物理镜像按槽位各一份) // RG 重编译后重绑阴影图(物理镜像按槽位各一份)
@@ -66,6 +89,7 @@ namespace Engine {
private: private:
Device& m_Device; Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors; DescriptorManager& m_Descriptors;
}; };
@@ -1,9 +1,10 @@
// //
// Created by atdunbg on 2026/9/23. // Created by atdunbg on 2026/9/23.
// //
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 Backbuffer。 // TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 PresentTarget。
// 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。 // 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
// Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。 // PresentTarget 是 offscreen 可采样纹理:由 ImGui::Image 呈现(输出位置
// 可自由摆放),backbuffer 不再接收场景画面。
// //
#ifndef HELLOVULKAN_TONEMAPPASS_H #ifndef HELLOVULKAN_TONEMAPPASS_H
@@ -28,7 +29,8 @@ namespace Engine {
PostProcessParams& params, PostProcessParams& params,
FrameSceneData& frame, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA, 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 rawHdr, RG::TextureHandle velocity,
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1, RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
RG::TextureHandle shadowMap2); RG::TextureHandle shadowMap2);
@@ -46,7 +48,7 @@ namespace Engine {
RG::TextureHandle m_HDR; RG::TextureHandle m_HDR;
RG::TextureHandle m_A; RG::TextureHandle m_A;
RG::TextureHandle m_TemporalDiff; RG::TextureHandle m_TemporalDiff;
RG::TextureHandle m_Backbuffer; RG::TextureHandle m_Target; // PresentTarget(offscreen,ImGui 采样呈现)
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)---- // ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑 // 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前) RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
+42 -4
View File
@@ -58,7 +58,7 @@ namespace Engine {
Renderer(const Renderer&) = delete; Renderer(const Renderer&) = delete;
Renderer& operator=(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)装配其他模块时需要的访问器 ---- // ---- 组合根(Application)装配其他模块时需要的访问器 ----
Device& GetDevice() const { return *m_Device; } Device& GetDevice() const { return *m_Device; }
@@ -72,7 +72,18 @@ namespace Engine {
// 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。 // 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。
// 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。 // 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。
void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; } void SetImGuiLayer(ImGuiLayer* layer)
{
m_ImGuiLayer = layer;
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)---- // ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
f32 GetExposure() const; f32 GetExposure() const;
@@ -115,13 +126,32 @@ namespace Engine {
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字) bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
f32 GetCollectMs() const; // CPU 收集+剔除段耗时 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), // 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前) // 只应在帧 UI 构建段调用(Render 之前)
void SetVSync(bool vsync); void SetVSync(bool vsync) { m_DesiredVSync = vsync; } // 帧首同步生效
// 帧首同步:窗口 resize / 视口尺寸 / 显示模式 / vsync 意图统一在
// UI 构建之前落定(waitIdle → 重编译 → 重绑描述符)。必须先于
// UI 构建段调用:本帧 draw data 引用的描述符与物理资源由此保证
// 全程新鲜(resize 中途换描述符会绑出已销毁句柄)
void BeginFrameSync();
private: private:
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象 void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
void HandleResize(); void HandleResize(bool recreateSwapchain);
void ReadGpuTimes(u32 frameSlot); void ReadGpuTimes(u32 frameSlot);
void RefreshSceneTextures(); // 物理 view → ImGui 描述符(重建后刷新)
// 期望渲染分辨率:Editor 视口 = 面板尺寸(未上报时回退 swapchain);
// Direct = swapchain 尺寸
vk::Extent2D DesiredRenderExtent() const;
Window& m_Window; Window& m_Window;
@@ -133,6 +163,8 @@ namespace Engine {
Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO) Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity) 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::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配 Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造 // 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
@@ -152,6 +184,7 @@ namespace Engine {
// ---- RenderGraph 资源句柄 ---- // ---- RenderGraph 资源句柄 ----
RG::TextureHandle m_Backbuffer{}; RG::TextureHandle m_Backbuffer{};
RG::TextureHandle m_PresentTarget{}; // tonemap 输出(offscreen,ImGui 采样)
RG::TextureHandle m_Depth{}; RG::TextureHandle m_Depth{};
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联 std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一 RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
@@ -180,6 +213,11 @@ namespace Engine {
vk::Extent2D m_Extent{}; vk::Extent2D m_Extent{};
u32 m_CurrentFrame = 0; u32 m_CurrentFrame = 0;
// ---- 帧首同步的意图缓冲(UI 段写入,下一帧帧首落定)----
int m_DesiredDisplayMode = kDisplayDirect; // 默认直接渲染, 视口渲染需要手动指定一下
vk::Extent2D m_DesiredViewport{}; // Editor 视口渲染分辨率(面板上报)
bool m_DesiredVSync = true;
}; };
} // namespace Engine } // namespace Engine
+23 -1
View File
@@ -23,6 +23,7 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/SceneTypes.h> #include <Engine/Renderer/SceneTypes.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <array> #include <array>
#include <unordered_map> #include <unordered_map>
@@ -36,8 +37,9 @@ namespace Engine {
public: public:
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。 // 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。 // targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
// dt 驱动骨骼动画采样推进(crossfade 计时)。
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent); vk::Extent2D targetExtent, f32 dt);
FrameSceneData& Frame() { return m_Frame; } FrameSceneData& Frame() { return m_Frame; }
@@ -59,6 +61,7 @@ namespace Engine {
void SetBloomEnabled(bool b) { m_BloomEnabled = b; } void SetBloomEnabled(bool b) { m_BloomEnabled = b; }
void SetLodEnabled(bool b) { m_LodEnabled = b; } void SetLodEnabled(bool b) { m_LodEnabled = b; }
void SetDebugView(int v) { m_DebugView = v; } void SetDebugView(int v) { m_DebugView = v; }
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
private: private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影 // CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
@@ -66,6 +69,12 @@ namespace Engine {
//(Forward UBO)共用同一组矩阵。 //(Forward UBO)共用同一组矩阵。
void ComputeCascades(const glm::vec3& shadowDir); 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; FrameSceneData m_Frame;
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)---- // ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
@@ -74,6 +83,7 @@ namespace Engine {
struct InstanceMatrices { struct InstanceMatrices {
glm::mat4 cur; glm::mat4 cur;
glm::mat4 prev; glm::mat4 prev;
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
}; };
CullResult result; CullResult result;
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 = // 每 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_PrevWorlds;
std::unordered_map<u32, glm::mat4> m_CurWorlds; 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; // 实例缓冲打满只告警一次 bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
// ---- 运行时开关/调试视图状态(本类是唯一写入者)---- // ---- 运行时开关/调试视图状态(本类是唯一写入者)----
@@ -116,6 +137,7 @@ namespace Engine {
bool m_BloomEnabled = true; bool m_BloomEnabled = true;
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过) bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
int m_DebugView = 0; int m_DebugView = 0;
int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态
}; };
} // namespace Engine } // namespace Engine
+57 -1
View File
@@ -18,6 +18,7 @@
namespace Engine { namespace Engine {
class Mesh; class Mesh;
class Material;
// GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例 // GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例
// 索引区间不同,不能并成一次 draw // 索引区间不同,不能并成一次 draw
@@ -28,10 +29,38 @@ namespace Engine {
u32 lod = 0; // 绘制用索引区间(Mesh::Draw) 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 { struct CullResult {
std::vector<InstanceBatch> batches; std::vector<InstanceBatch> batches;
u32 instanceCount = 0; u32 instanceCount = 0;
std::vector<IndirectDrawCmd> indirectCommands;
std::vector<IndirectGroup> indirectGroups;
}; };
constexpr u32 kMaxLights = 4; constexpr u32 kMaxLights = 4;
@@ -43,6 +72,13 @@ namespace Engine {
constexpr u32 kMaxCascades = 3; // CSM 级联数 constexpr u32 kMaxCascades = 3; // CSM 级联数
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长 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 侧 UBO 布局的灯光项(std140,每灯 64B)。
// 字段未用部分置零——shader 按类型分支读取 // 字段未用部分置零——shader 按类型分支读取
struct LightData { struct LightData {
@@ -70,6 +106,14 @@ namespace Engine {
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量) 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 消费)。 // 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
// 复用成员容量:clear() 不缩容,稳态零每帧堆分配。 // 复用成员容量:clear() 不缩容,稳态零每帧堆分配。
struct FrameSceneData { struct FrameSceneData {
@@ -87,6 +131,15 @@ namespace Engine {
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一 // 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵 // 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
std::vector<glm::mat4> prevInstanceModels; 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 CullResult mainView; // 相机视锥 → Forward pass
// 每级联独立剔除结果(Shadow pass 按 region 各画各的—— // 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉) // 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
@@ -102,10 +155,13 @@ namespace Engine {
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零 bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
bool lodEnabled = true; // 关 = 实例收集恒选 LOD0(绘制原网格) bool lodEnabled = true; // 关 = 实例收集恒选 LOD0(绘制原网格)
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏) int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
int displayMode = kDisplayViewport; // 显示模式(Blit 路由 + ImGui loadOp)
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃—— // 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警 // 必须遥测可见,"只告警一次"的大场景截断等于没有告警
bool instancesTruncated = false; bool instancesTruncated = false;
// palette 预算溢出(kMaxSkinMatrices):动画实体 palette 被截断
bool skinPaletteTruncated = false;
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测) f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
}; };
+20 -2
View File
@@ -16,6 +16,14 @@ namespace Engine {
glm::vec2 uv; glm::vec2 uv;
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace) 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() { static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
vk::VertexInputBindingDescription2EXT binding; vk::VertexInputBindingDescription2EXT binding;
binding.setBinding(0) binding.setBinding(0)
@@ -25,8 +33,8 @@ namespace Engine {
return binding; return binding;
} }
static std::array<vk::VertexInputAttributeDescription2EXT, 4> GetAttributeDescriptions() { static std::array<vk::VertexInputAttributeDescription2EXT, 6> GetAttributeDescriptions() {
std::array<vk::VertexInputAttributeDescription2EXT, 4> attrs; std::array<vk::VertexInputAttributeDescription2EXT, 6> attrs;
attrs[0].setLocation(0) attrs[0].setLocation(0)
.setBinding(0) .setBinding(0)
@@ -48,6 +56,16 @@ namespace Engine {
.setFormat(vk::Format::eR32G32B32Sfloat) .setFormat(vk::Format::eR32G32B32Sfloat)
.setOffset(offsetof(Vertex, tangent)); .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; return attrs;
} }
}; };
+3 -1
View File
@@ -26,7 +26,9 @@ namespace Engine {
Buffer& operator=(const Buffer&) = delete; Buffer& operator=(const Buffer&) = delete;
vk::Buffer Get() const { return m_Buffer; } 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 能调 // 只有 HOST_VISIBLE 的 Buffer 能调
void Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset = 0) const; 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 IsDynamicRenderingSupported() const { return m_DynamicRenderingSupported; }
bool IsShaderObjectSupported() const { return m_ShaderObjectSupported; } bool IsShaderObjectSupported() const { return m_ShaderObjectSupported; }
bool IsSynchronization2Supported() const { return m_Synchronization2Supported; } bool IsSynchronization2Supported() const { return m_Synchronization2Supported; }
// MDI:drawCount>1 的 drawIndexedIndirect(Count) 必需(core 特性位)
bool IsMultiDrawIndirectSupported() const { return m_MultiDrawIndirectSupported; }
private: private:
void CreateInstance(); void CreateInstance();
@@ -72,6 +74,8 @@ private:
bool m_DynamicRenderingSupported = false; bool m_DynamicRenderingSupported = false;
bool m_ShaderObjectSupported = false; bool m_ShaderObjectSupported = false;
bool m_Synchronization2Supported = 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_ValidationLayers = { "VK_LAYER_KHRONOS_validation" };
std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+14 -4
View File
@@ -61,7 +61,10 @@ namespace Engine {
void Application::EnableImGui() 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_ImGuiLayer = CreateScope<ImGuiLayer>(GetWindow(), *m_Renderer);
m_Renderer->SetImGuiLayer(m_ImGuiLayer.get()); m_Renderer->SetImGuiLayer(m_ImGuiLayer.get());
@@ -108,24 +111,31 @@ namespace Engine {
m_Scene->FlushDestroyQueue(); m_Scene->FlushDestroyQueue();
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜 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 ---- // ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ----
if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled()) if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled())
{ {
const auto uiStart = std::chrono::steady_clock::now(); const auto uiStart = std::chrono::steady_clock::now();
m_ImGuiLayer->NewFrame(); m_ImGuiLayer->NewFrame();
OnRenderOverlay(); OnImGuiRender();
m_ImGuiLayer->BuildDrawData(); m_ImGuiLayer->BuildDrawData();
const auto uiEnd = std::chrono::steady_clock::now(); const auto uiEnd = std::chrono::steady_clock::now();
} }
// ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) → // ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) →
// Present,draw data 在 ImGui pass 里才真正提交 GPU ---- // Present,draw data 在 ImGui pass 里才真正提交 GPU ----
m_Renderer->Render(*m_Scene, *m_Assets); m_Renderer->Render(*m_Scene, *m_Assets, m_DeltaTime);
// ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ---- // ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ----
// 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU // 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU
if (!m_VSync) { // TargetFps <= 0 表示不限帧(跳过 sleep,避免 1e6/0 除零 UB)
if (!m_VSync && m_TargetFps > 0) {
const auto deadline = frameStart + const auto deadline = frameStart +
std::chrono::microseconds(static_cast<long long>( std::chrono::microseconds(static_cast<long long>(
1e6 / m_TargetFps)); 1e6 / m_TargetFps));
+41 -17
View File
@@ -81,12 +81,19 @@ namespace Engine
void Window::WindowFocusCallback(GLFWwindow* window, int focused) 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; if (!self->m_EventCallback) return;
Event e; Event e;
e.type = focused ? EventType::WindowFocus : EventType::WindowLostFocus; e.type = focused ? EventType::WindowFocus : EventType::WindowLostFocus;
self->m_EventCallback(e); self->m_EventCallback(e);
// 失焦时释放光标捕获(切窗口不会留下隐形被捕获的光标;
// 相机下一帧按右键状态自行决定是否重新捕获)
if (!focused && self->m_CursorCaptured)
{
self->SetCursorCaptured(false);
}
} }
void Window::WindowCloseCallback(GLFWwindow* window) void Window::WindowCloseCallback(GLFWwindow* window)
@@ -120,7 +127,7 @@ namespace Engine
glfwTerminate(); glfwTerminate();
return; return;
} }
glfwSetWindowOpacity(m_Window, 0.5f); glfwSetWindowOpacity(m_Window, 0.0f);
ENGINE_INFO("Window created: {} ({}x{})", title, width, height); ENGINE_INFO("Window created: {} ({}x{})", title, width, height);
@@ -191,32 +198,49 @@ namespace Engine
return glfwGetKey(m_Window, key) == GLFW_PRESS; 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 void Window::GetMouseDelta(float& dx, float& dy) const
{ {
double mx, my; double mx, my;
glfwGetCursorPos(m_Window, &mx, &my); glfwGetCursorPos(m_Window, &mx, &my);
static double lastX = mx; // 首帧(或捕获切换后):只建立参考点
static double lastY = my; if (m_FirstMouse)
static bool first = true;
if (first)
{ {
lastX = mx; m_LastMouseX = mx;
lastY = my; m_LastMouseY = my;
first = false; m_FirstMouse = false;
dx = dy = 0.0f; dx = dy = 0.0f;
return; return;
} }
dx = static_cast<float>(mx - lastX); dx = static_cast<float>(mx - m_LastMouseX);
dy = static_cast<float>(my - lastY); dy = static_cast<float>(my - m_LastMouseY);
m_LastMouseX = mx;
m_LastMouseY = my;
lastX = mx; // UI 捕获鼠标(ImGui 悬停)时 delta 归零;参考点照常更新,避免
lastY = my; // 释放后跳变。光标被相机捕获时绕过——虚拟光标可能悬在 ImGui
// 面板上方,若被归零视角会卡死
// UI 捕获鼠标时 delta 归零;lastX/Y 照常更新,避免释放后跳变 if (!m_CursorCaptured && m_MouseCapture && m_MouseCapture())
if (m_MouseCapture && m_MouseCapture())
{ {
dx = dy = 0.0f; dx = dy = 0.0f;
} }
+1 -3
View File
@@ -16,9 +16,7 @@ namespace Engine::ECS {
// 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义) // 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义)
m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()}); m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()});
m_Registry.emplace<TransformComponent>(e); 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}; return EntityHandle{this, e};
} }
+25
View File
@@ -95,6 +95,11 @@ namespace Engine {
// 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构) // 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构)
m_Device.GetDevice().waitIdle(); m_Device.GetDevice().waitIdle();
// 场景纹理描述符先归还(后端 shutdown 前必须完成)
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
UpdateSceneTexture(slot, vk::ImageView{});
}
ImGui_ImplVulkan_Shutdown(); ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown(); ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext(); ImGui::DestroyContext();
@@ -104,6 +109,26 @@ namespace Engine {
ENGINE_INFO("ImGuiLayer shut down"); 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() void ImGuiLayer::NewFrame()
{ {
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
+20 -3
View File
@@ -7,6 +7,7 @@
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <algorithm> #include <algorithm>
#include <ranges>
#include <set> #include <set>
#include <stdexcept> #include <stdexcept>
@@ -41,8 +42,8 @@ namespace Engine::RG {
ReleasePhysical(); ReleasePhysical();
const auto& vkDevice = m_Device.GetDevice(); const auto& vkDevice = m_Device.GetDevice();
for (auto& [key, images] : m_Pool) { for (auto &images: m_Pool | std::views::values) {
for (auto& img : images) { for (const auto& img : images) {
if (!img->valid) continue; if (!img->valid) continue;
vkDevice.destroyImageView(img->view); vkDevice.destroyImageView(img->view);
m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放 m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放
@@ -57,7 +58,7 @@ namespace Engine::RG {
{ {
CheckMutable(); CheckMutable();
TextureHandle h{static_cast<u32>(m_Textures.size()), 1}; const TextureHandle h{static_cast<u32>(m_Textures.size()), 1};
TextureEntry entry; TextureEntry entry;
entry.name = std::move(name); entry.name = std::move(name);
@@ -101,6 +102,15 @@ namespace Engine::RG {
entry.importedView = view; 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 声明 ---------- // ---------- Pass 声明 ----------
RenderGraph::PassBuilder RenderGraph::AddPass(std::string name, ExecuteFn exec) RenderGraph::PassBuilder RenderGraph::AddPass(std::string name, ExecuteFn exec)
@@ -832,6 +842,13 @@ namespace Engine::RG {
return entry.physical[frame.frameSlot]->image; 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 u32 RenderGraphContext::HistorySlot(u32 frameSlot) const
{ {
return graph->HistorySlot(frameSlot); return graph->HistorySlot(frameSlot);
+18 -3
View File
@@ -14,21 +14,36 @@ namespace Engine {
void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime) 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; f32 mouseDX = 0, mouseDY = 0;
window.GetMouseDelta(mouseDX, mouseDY); window.GetMouseDelta(mouseDX, mouseDY);
if (mouseDX != 0 || mouseDY != 0) if (mouseDX != 0 || mouseDY != 0)
{ {
glm::vec3 rot = t.GetRotation(); 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 = glm::clamp(
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f); rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
t.SetRotation(rot); t.SetRotation(rot);
} }
} }
else if (window.IsCursorCaptured())
{
window.SetCursorCaptured(false);
}
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X, // 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机) // 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
+293 -38
View File
@@ -22,6 +22,19 @@
namespace Engine { 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, Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
@@ -76,7 +89,10 @@ void Model::InitDefaultTextures()
m_DefaultTextures[Material::Emissive] = m_DefaultTextures[Material::Emissive] =
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0); Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
m_DefaultTextures[Material::Ao] = 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, Model::Model(Device& device, MemoryAllocator& allocator,
@@ -89,12 +105,18 @@ Model::Model(Device& device, MemoryAllocator& allocator,
, m_DescriptorManager(descriptorManager) , m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout) , m_TextureLayout(textureLayout)
{ {
// 纯色贴图(albedo 兜底;alpha 非全 1 时为半透明材质) // 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质);
// 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突)
InitDefaultTextures(); InitDefaultTextures();
auto tex = (rgba[3] == 255) Scope<Texture> tex = (rgba[3] == 255)
? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2]) ? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2])
: Texture::CreateSolidColorRGBA(device, allocator, : Texture::CreateSolidColorRGBA(device, allocator,
rgba[0], rgba[1], rgba[2], rgba[3]); 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 绕序 // XZ 平面,法线 +Y,从上往下看 CCW 绕序
const f32 h = halfSize; 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<u32> indices = {0, 1, 2, 0, 2, 3};
const std::vector<std::vector<u32>> lodIndices = {indices}; const std::vector<std::vector<u32>> lodIndices = {indices};
// 材质/纹理生命周期随模型(缓存持有) // 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
std::string paths[Material::kSlotCount] = {"", "", "", ""}; 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_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, lodIndices, mat)); m_Device, m_Allocator, vertices, lodIndices, mat));
@@ -164,20 +190,177 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
(out.isGltf ? 0 : aiProcess_FlipUVs) | (out.isGltf ? 0 : aiProcess_FlipUVs) |
aiProcess_GenSmoothNormals | aiProcess_GenSmoothNormals |
aiProcess_CalcTangentSpace | // 法线贴图 TBN aiProcess_CalcTangentSpace | // 法线贴图 TBN
aiProcess_JoinIdenticalVertices); aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights); // 每顶点限 4 权重(= 顶点格式 boneIds 容量)
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { 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()); ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
return out; // meshes 空 = 无效 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 = const std::filesystem::path modelDir =
std::filesystem::path(path).parent_path(); std::filesystem::path(path).parent_path();
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf); // 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
ENGINE_INFO("Model parsed: {} ({} meshes)", path, out.meshes.size()); 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; 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, Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
@@ -192,6 +375,9 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
descriptorManager, textureLayout)); descriptorManager, textureLayout));
model->m_ModelPath = parsed.path; model->m_ModelPath = parsed.path;
model->m_IsGltf = parsed.isGltf; 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,主线程) // 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
for (auto& pm : parsed.meshes) { for (auto& pm : parsed.meshes) {
@@ -214,6 +400,9 @@ void Model::LoadModel(const std::string& path)
} }
m_ModelPath = parsed.path; m_ModelPath = parsed.path;
m_IsGltf = parsed.isGltf; 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) { for (auto& pm : parsed.meshes) {
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params); Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
m_Meshes.push_back(CreateScope<Mesh>( 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, void Model::ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out, 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) { for (u32 i = 0; i < node->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[node->mMeshes[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) { 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], Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params, 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()) { if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
return it->second.get(); return it->second.get();
} }
// 每槽位解析纹理:空/缺失折到该槽位默认纹理 // 每槽位解析纹理:程序化覆盖优先,路径次之,空/缺失折到默认纹理
std::array<Texture*, Material::kSlotCount> textures{}; std::array<Texture*, Material::kSlotCount> textures{};
// 槽位 sRGB 语义:颜色图 true,数据图 false // 槽位 sRGB 语义:颜色图 true,数据图 false
const bool slotSrgb[Material::kSlotCount] = {true, false, false, true}; const bool slotSrgb[Material::kSlotCount] = {true, false, false, true};
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) { for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
Texture* tex = nullptr; Texture* tex = slotOverrides[slot];
if (!paths[slot].empty()) { if (!tex && !paths[slot].empty()) {
tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]); tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]);
} }
textures[slot] = tex ? tex : m_DefaultTextures[slot].get(); 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, ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir, 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; ParsedMesh out;
std::vector<Vertex>& vertices = out.vertices; std::vector<Vertex>& vertices = out.vertices;
std::vector<u32> indices; std::vector<u32> indices;
vertices.reserve(mesh->mNumVertices); 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) { for (u32 i = 0; i < mesh->mNumVertices; ++i) {
Vertex vertex{}; Vertex vertex{};
vertex.position = { if (bakeTransform) {
mesh->mVertices[i].x, const aiVector3D p = globalTransform * mesh->mVertices[i];
mesh->mVertices[i].y, vertex.position = {p.x, p.y, p.z};
mesh->mVertices[i].z } else {
}; vertex.position = {
mesh->mVertices[i].x,
mesh->mVertices[i].y,
mesh->mVertices[i].z
};
}
if (mesh->HasNormals()) { if (mesh->HasNormals()) {
vertex.normal = { const aiVector3D n = bakeTransform
mesh->mNormals[i].x, ? normalMat * mesh->mNormals[i]
mesh->mNormals[i].y, : mesh->mNormals[i];
mesh->mNormals[i].z const glm::vec3 nn{n.x, n.y, n.z};
}; vertex.normal = bakeTransform ? glm::normalize(nn) : nn;
} }
if (mesh->mTextureCoords[0]) { if (mesh->mTextureCoords[0]) {
@@ -328,16 +561,23 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线) // 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
if (mesh->mTangents) { if (mesh->mTangents) {
vertex.tangent = { const aiVector3D t = bakeTransform
mesh->mTangents[i].x, ? normalMat * mesh->mTangents[i]
mesh->mTangents[i].y, : mesh->mTangents[i];
mesh->mTangents[i].z 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); vertices.push_back(vertex);
} }
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
CollectMeshBones(mesh, vertices, bones, boneSlots);
for (u32 i = 0; i < mesh->mNumFaces; ++i) { for (u32 i = 0; i < mesh->mNumFaces; ++i) {
aiFace face = mesh->mFaces[i]; aiFace face = mesh->mFaces[i];
for (u32 j = 0; j < face.mNumIndices; ++j) { 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)/ // glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。 // DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。 // 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[] = { const std::pair<aiTextureType, u32> slots[] = {
{aiTextureType_DIFFUSE, Material::Albedo}, {aiTextureType_DIFFUSE, Material::Albedo},
{aiTextureType_BASE_COLOR, Material::Albedo}, {aiTextureType_BASE_COLOR, Material::Albedo},
{aiTextureType_NORMALS, Material::Normal}, {aiTextureType_NORMALS, Material::Normal},
{aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点 {aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点
{aiTextureType_METALNESS, Material::MetallicRoughness}, {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_UNKNOWN, Material::MetallicRoughness},
{aiTextureType_SHININESS, Material::MetallicRoughness},
{aiTextureType_AMBIENT_OCCLUSION, Material::Ao}, {aiTextureType_AMBIENT_OCCLUSION, Material::Ao},
{aiTextureType_LIGHTMAP, Material::Ao}, {aiTextureType_LIGHTMAP, Material::Ao},
{aiTextureType_AMBIENT, 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(), material->GetName().C_Str(),
texPaths[Material::Albedo].empty() ? "<none>" texPaths[Material::Albedo].empty() ? "<none>"
: texPaths[Material::Albedo], : texPaths[Material::Albedo],
@@ -393,6 +637,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
: texPaths[Material::Normal], : texPaths[Material::Normal],
texPaths[Material::MetallicRoughness].empty() ? "<none>" texPaths[Material::MetallicRoughness].empty() ? "<none>"
: texPaths[Material::MetallicRoughness], : texPaths[Material::MetallicRoughness],
texPaths[Material::Roughness].empty() ? "<none>"
: texPaths[Material::Roughness],
texPaths[Material::Emissive].empty() ? "<none>" texPaths[Material::Emissive].empty() ? "<none>"
: texPaths[Material::Emissive], : texPaths[Material::Emissive],
texPaths[Material::Ao].empty() ? "<none>" texPaths[Material::Ao].empty() ? "<none>"
@@ -419,15 +665,24 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
out.transparent = (alphaMode == aiString("BLEND")); out.transparent = (alphaMode == aiString("BLEND"));
} }
} else { } else {
// FBX/OBJ 独立贴图工作流
const bool hasSepRough = !texPaths[Material::Roughness].empty();
f32 roughnessFactor = 1.0f; 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; ai_real shininess = 10.0f;
material->Get(AI_MATKEY_SHININESS, shininess); material->Get(AI_MATKEY_SHININESS, shininess);
const f32 rough = const f32 rough =
std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f)); std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f));
roughnessFactor = 2.0f * glm::clamp(rough, 0.05f, 1.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. // 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/ForwardPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Vertex.h> #include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/Material.h> #include <Engine/Renderer/Material.h>
@@ -12,8 +32,90 @@
namespace Engine::Passes 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) void ForwardPass::Register(RG::RenderGraph& rg)
{ {
// ---- 不透明段:深度测试 + 写,无混合直写 ----
rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) { rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd; const vk::CommandBuffer cmd = ctx.cmd;
@@ -42,6 +144,96 @@ namespace Engine::Passes
depthAttachment.setImageView(ctx.View(m_Depth)) depthAttachment.setImageView(ctx.View(m_Depth))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal) .setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear) .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) .setStoreOp(vk::AttachmentStoreOp::eDontCare)
.setClearValue(vk::ClearValue( .setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0))); vk::ClearDepthStencilValue(1.0f, 0)));
@@ -56,124 +248,49 @@ namespace Engine::Passes
.setPDepthAttachment(&depthAttachment); .setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo); 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 vk::ColorBlendEquationEXT premul;
std::array<vk::ShaderStageFlagBits, 2> stages = { premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
vk::ShaderStageFlagBits::eVertex, .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
vk::ShaderStageFlagBits::eFragment .setColorBlendOp(vk::BlendOp::eAdd)
}; // alpha 穿透:玻璃只做场景内 rgb over(透出背景色),
std::array<vk::ShaderEXT, 2> shaders = {m_Gpu.forwardVert, m_Gpu.forwardFrag}; // 不修改像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap
cmd.bindShadersEXT(stages, shaders); // rgb *= a 预乘 + 透明窗口桌面合成),归背景专属。若让
// 玻璃 over 混入 a(旧写法),a<1 背景上的玻璃会被拖进
// 桌面混色链路 → 预乘拉黑
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
.setAlphaBlendOp(vk::BlendOp::eAdd);
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图 const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 0,
m_Gpu.uboSets[ctx.frame.frameSlot], {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 2,
m_Gpu.shadowSets[ctx.frame.frameSlot], {});
// ---- 动态状态 ----
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_TRUE);
cmd.setDepthWriteEnable(VK_TRUE);
cmd.setDepthCompareOp(vk::CompareOp::eLess);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
cmd.setDepthBiasEnable(VK_FALSE);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
auto binding = Vertex::GetBindingDescription();
auto attributes = Vertex::GetAttributeDescriptions();
cmd.setVertexInputEXT(binding, attributes);
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
vk::Rect2D scissor({0, 0}, ctx.extent);
cmd.setScissorWithCount(scissor);
vk::Bool32 blendEnable = VK_FALSE;
vk::ColorBlendEquationEXT blendEquation;
blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
vk::ColorComponentFlags colorWriteMask =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
// 两个颜色附件(HDR + velocity)都是无混合直写
cmd.setColorBlendEnableEXT(0, cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{blendEnable, blendEnable}); std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
cmd.setColorBlendEquationEXT(0, cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{blendEquation, blendEquation}); std::array<vk::ColorBlendEquationEXT, 2>{premul, direct});
cmd.setColorWriteMaskEXT(0, cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{colorWriteMask, colorWriteMask}); std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
// ---- 不透明批次:无混合直写,深度测试 + 写 ---- for (const auto& batch : m_Frame.translucentView.batches) {
for (const auto& batch : m_Frame.mainView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1, m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {}); batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod); 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(); cmd.endRendering();
}) })
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment) .Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
.Write(m_Velocity, RG::ResourceUse::ColorAttachment) .Write(m_Velocity, RG::ResourceUse::ColorAttachment)
// depth 走写 use(WAW 边):保证透明段排在不透明段之后,
// barrier 覆盖透明段对不透明深度的测试读(Read-Write hazard
// 由 src=Write 的内存域保证可见性)
.Write(m_Depth, RG::ResourceUse::DepthAttachment) .Write(m_Depth, RG::ResourceUse::DepthAttachment)
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead, .Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment) vk::ShaderStageFlagBits::eFragment)
@@ -174,6 +174,32 @@ namespace Engine::Passes
cmd.setAlphaToCoverageEnableEXT(VK_FALSE); cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(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, vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width), static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height), static_cast<float>(ctx.extent.height),
+22 -4
View File
@@ -12,14 +12,28 @@ namespace Engine::Passes
rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) { rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd; 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; vk::RenderingAttachmentInfo colorAttachment;
colorAttachment.setImageView(ctx.View(m_Backbuffer)) colorAttachment.setImageView(ctx.View(m_Backbuffer))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad) // 关键:叠加 .setLoadOp(direct ? vk::AttachmentLoadOp::eLoad
.setStoreOp(vk::AttachmentStoreOp::eStore); : vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
vk::RenderingInfo renderingInfo; 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) .setLayerCount(1)
.setColorAttachments(colorAttachment); .setColorAttachments(colorAttachment);
@@ -30,7 +44,11 @@ namespace Engine::Passes
} }
cmd.endRendering(); 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 } // namespace Engine::Passes
+92 -4
View File
@@ -4,8 +4,10 @@
#include <Engine/Renderer/Passes/SceneGpu.h> #include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Material.h> #include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> // IndirectGroup 取数(inline 经 Mesh)
#include <Engine/Renderer/Cubemap.h> #include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/Buffer.h>
#include <Engine/Vulkan/DescriptorManager.h> #include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h> #include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Vulkan/UniformBuffer.h> #include <Engine/Vulkan/UniformBuffer.h>
@@ -15,14 +17,18 @@
namespace Engine::Passes namespace Engine::Passes
{ {
SceneGpu::SceneGpu(Device& device, DescriptorManager& descriptors, SceneGpu::SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders, ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer, UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer) UniformBuffer& prevObjectBuffer,
: m_Device(device), m_Descriptors(descriptors) UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer)
: m_Device(device), m_Allocator(allocator), m_Descriptors(descriptors)
{ {
// Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取) // Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取)
// + 上一帧实例矩阵(TAA velocity,vertex 读取) // + 上一帧实例矩阵(TAA velocity,vertex 读取)
// + 骨骼蒙皮:SkinOffsets(实例→palette 基址)+ SkinPalettes(骨骼矩阵)
std::vector<DescriptorBinding> uboBindings = { std::vector<DescriptorBinding> uboBindings = {
{0, vk::DescriptorType::eUniformBuffer, {0, vk::DescriptorType::eUniformBuffer,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment}, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
@@ -30,6 +36,10 @@ namespace Engine::Passes
vk::ShaderStageFlagBits::eVertex}, vk::ShaderStageFlagBits::eVertex},
{2, vk::DescriptorType::eStorageBuffer, {2, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex}, vk::ShaderStageFlagBits::eVertex},
{3, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
{4, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
}; };
uboSetLayout = descriptors.CreateLayout(uboBindings); uboSetLayout = descriptors.CreateLayout(uboBindings);
@@ -81,6 +91,11 @@ namespace Engine::Passes
shaderDir + "simple_shader.frag.spv", shaderDir + "simple_shader.frag.spv",
vk::ShaderStageFlagBits::eFragment, vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0); {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 // shadow shader:depth-only,push = lightVP
auto shadowVertShader = shaders.CreateFromFile( auto shadowVertShader = shaders.CreateFromFile(
@@ -96,10 +111,12 @@ namespace Engine::Passes
forwardVert = vert.Handle; forwardVert = vert.Handle;
forwardFrag = frag.Handle; forwardFrag = frag.Handle;
translucentFrag = transFrag.Handle;
shadowVert = shadowVertShader.Handle; shadowVert = shadowVertShader.Handle;
shadowFrag = shadowFragShader.Handle; shadowFrag = shadowFragShader.Handle;
if (!forwardVert || !forwardFrag || !shadowVert || !shadowFrag) { if (!forwardVert || !forwardFrag || !translucentFrag ||
!shadowVert || !shadowFrag) {
ENGINE_FATAL("SceneGpu: failed to create shader objects"); ENGINE_FATAL("SceneGpu: failed to create shader objects");
} }
@@ -132,10 +149,34 @@ namespace Engine::Passes
descriptors.UpdateStorageBuffer( descriptors.UpdateStorageBuffer(
uboSets[slot], 2, prevObjectBuffer.Get(slot), uboSets[slot], 2, prevObjectBuffer.Get(slot),
prevObjectBuffer.GetSize()); 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[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout);
} }
// shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成 // 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() SceneGpu::~SceneGpu()
@@ -144,6 +185,7 @@ namespace Engine::Passes
if (forwardVert) vkDevice.destroyShaderEXT(forwardVert); if (forwardVert) vkDevice.destroyShaderEXT(forwardVert);
if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag); if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag);
if (translucentFrag) vkDevice.destroyShaderEXT(translucentFrag);
if (shadowVert) vkDevice.destroyShaderEXT(shadowVert); if (shadowVert) vkDevice.destroyShaderEXT(shadowVert);
if (shadowFrag) vkDevice.destroyShaderEXT(shadowFrag); 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) void SceneGpu::BindEnvironment(const Cubemap& env)
{ {
// IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。 // IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。
+5 -1
View File
@@ -3,6 +3,7 @@
// //
#include <Engine/Renderer/Passes/SkyboxPass.h> #include <Engine/Renderer/Passes/SkyboxPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Cubemap.h> #include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h> #include <Engine/Vulkan/DescriptorManager.h>
@@ -96,6 +97,8 @@ namespace Engine::Passes
const glm::vec4& cc = m_Frame.ClearColor; const glm::vec4& cc = m_Frame.ClearColor;
vk::ClearValue clear; vk::ClearValue clear;
// alpha 语义 = 窗口透过率(透明窗口与桌面合成;tonemap 段
// rgb *= a 预乘)。滑条由用户控制:a<1 = 背景透出桌面
clear.color = vk::ClearColorValue( clear.color = vk::ClearColorValue(
std::array<float, 4>{cc.r, cc.g, cc.b, cc.a}); std::array<float, 4>{cc.r, cc.g, cc.b, cc.a});
@@ -113,7 +116,8 @@ namespace Engine::Passes
cmd.beginRendering(renderingInfo); cmd.beginRendering(renderingInfo);
if (m_Env && m_Frame.DrawSkybox) { if (m_Env && m_Frame.DrawSkybox &&
m_Frame.debugView != Passes::kDebugTransparent) {
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写) // 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
const std::array<vk::ShaderStageFlagBits, 2> stages = { const std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex, vk::ShaderStageFlagBits::eVertex,
+4 -4
View File
@@ -14,7 +14,7 @@ namespace Engine::Passes
FrameSceneData& frame, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA, RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff, RG::TextureHandle temporalDiff,
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget,
RG::TextureHandle rawHdr, RG::TextureHandle rawHdr,
RG::TextureHandle velocity, RG::TextureHandle velocity,
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap0,
@@ -22,7 +22,7 @@ namespace Engine::Passes
RG::TextureHandle shadowMap2) RG::TextureHandle shadowMap2)
: m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame), : m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame),
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff), m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
m_Backbuffer(backbuffer), m_Target(presentTarget),
m_RawHDR(rawHdr), m_Velocity(velocity), m_RawHDR(rawHdr), m_Velocity(velocity),
m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2} m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2}
{ {
@@ -38,7 +38,7 @@ namespace Engine::Passes
{ {
rg.AddPass("Tonemap", rg.AddPass("Tonemap",
[this](RG::RenderGraphContext& ctx) { [this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Backbuffer), m_Gpu.RecordFullscreen(ctx, ctx.View(m_Target),
m_Gpu.tonemapFrag, m_Gpu.tonemapFrag,
m_Sets[ctx.frame.frameSlot], m_Sets[ctx.frame.frameSlot],
FullscreenPush{m_Params.exposure, FullscreenPush{m_Params.exposure,
@@ -49,7 +49,7 @@ namespace Engine::Passes
static_cast<f32>( static_cast<f32>(
m_Frame.debugView)}); m_Frame.debugView)});
}) })
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment) .Write(m_Target, RG::ResourceUse::ColorAttachment)
.Read(m_HDR, RG::ResourceUse::ShaderRead, .Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment) vk::ShaderStageFlagBits::eFragment)
.Read(m_A, RG::ResourceUse::ShaderRead, .Read(m_A, RG::ResourceUse::ShaderRead,
+128 -27
View File
@@ -24,6 +24,7 @@
#include <Engine/Renderer/Passes/TemporalDiffPass.h> #include <Engine/Renderer/Passes/TemporalDiffPass.h>
#include <Engine/Renderer/Passes/ImGuiPass.h> #include <Engine/Renderer/Passes/ImGuiPass.h>
#include <Engine/Renderer/Passes/PresentPass.h> #include <Engine/Renderer/Passes/PresentPass.h>
#include <Engine/Renderer/Passes/BlitPass.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h> #include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Vulkan/DescriptorManager.h> #include <Engine/Vulkan/DescriptorManager.h>
@@ -34,6 +35,7 @@
#include <Engine/Vulkan/UniformBuffer.h> #include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Core/Window.h> #include <Engine/Core/Window.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/ImGui/ImGuiLayer.h>
#include <Engine/ECS/Scene.h> #include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
@@ -43,6 +45,7 @@
#include <array> #include <array>
#include <limits> #include <limits>
#include <string>
namespace Engine namespace Engine
{ {
@@ -67,11 +70,22 @@ namespace Engine
*m_Device, *m_Allocator, *m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight, sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer); 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 描述符 // 3. pass 装配体:布局/shader/采样器/per-slot 描述符
m_SceneGpu = CreateScope<Passes::SceneGpu>( m_SceneGpu = CreateScope<Passes::SceneGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager, *m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager,
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer); *m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer,
*m_SkinOffsetBuffer, *m_SkinPaletteBuffer);
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>( m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager); *m_Device, *m_DescriptorManager, *m_ShaderManager);
@@ -191,6 +205,12 @@ namespace Engine
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; } int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); } 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) void Renderer::SetEnvironment(const Cubemap* env)
{ {
if (!env) env = m_Environment.get(); if (!env) env = m_Environment.get();
@@ -210,9 +230,9 @@ namespace Engine
static const std::vector<const char*> kNames = { static const std::vector<const char*> kNames = {
"Final", "TemporalDiff", "HDR", "TAAColor", "Bloom", "Final", "TemporalDiff", "HDR", "TAAColor", "Bloom",
"Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2", "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 枚举与名表同步"); "DebugView 枚举与名表同步");
return kNames; return kNames;
} }
@@ -244,9 +264,17 @@ namespace Engine
// ---- 资源声明(命名资源;pass 经句柄引用,数据流由 uses 决定)---- // ---- 资源声明(命名资源;pass 经句柄引用,数据流由 uses 决定)----
// 导入 swapchain 颜色附件 // 导入 swapchain 颜附件(物理尺寸 = 窗口,与场景渲染分辨率解耦)
m_Backbuffer = m_RenderGraph->ImportSwapchain( m_Backbuffer = m_RenderGraph->ImportSwapchain(
"Backbuffer", m_Swapchain->GetFormat()); "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 能力决定 // 深度图:目标尺寸,格式由 Device 能力决定
const vk::Format depthFormat = m_Device->FindSupportedFormat( const vk::Format depthFormat = m_Device->FindSupportedFormat(
@@ -317,10 +345,15 @@ namespace Engine
m_BloomB)); m_BloomB));
m_Passes.push_back(CreateScope<Passes::TonemapPass>( m_Passes.push_back(CreateScope<Passes::TonemapPass>(
*m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame, *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])); 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_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)); m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
for (auto& pass : m_Passes) { for (auto& pass : m_Passes) {
@@ -330,29 +363,88 @@ namespace Engine
m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile
} }
void Renderer::SetVSync(bool vsync) // ---- 帧首同步:所有"会换物理资源/描述符"的意图统一在这里落定 ----
vk::Extent2D Renderer::DesiredRenderExtent() const
{ {
if (m_Swapchain->GetVSync() == vsync) return; // Editor 视口模式:渲染分辨率跟随 ImGui 面板(面板未上报/关闭时
m_Swapchain->SetVSync(vsync); // 回退 swapchain 尺寸);Direct 模式:全窗口
HandleResize(); // present mode 变化需要重建 swapchain 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_Device->GetDevice().waitIdle();
m_Swapchain->Recreate(m_Window); if (recreateSwapchain) {
m_Extent = m_Swapchain->GetExtent(); // 先把 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_Extent = DesiredRenderExtent(); // 重建后取(swapchain 尺寸已新鲜)
m_SyncManager = CreateScope<SyncManager>(
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
// RG 重编译:物理镜像按新尺寸轮换,各 pass 重绑输入描述符 // RG 重编译:物理镜像按新尺寸轮换,各 pass 重绑输入描述符
m_RenderGraph->SetTargetExtent(m_Extent); m_RenderGraph->SetTargetExtent(m_Extent);
for (auto& pass : m_Passes) { for (auto& pass : m_Passes) {
pass->OnTargetsRebuilt(*m_RenderGraph); 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) 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) ZoneScoped; // CPU 侧:UBO/SSBO 上传 + 录制 + 提交(GPU 耗时见 RG timestamp)
if (m_Window.WasResized()) { // resize 已在 BeginFrameSync(UI 构建前)处理,此处无需再查
m_Window.ResetResizedFlag();
HandleResize();
}
const u32 frameSlot = m_CurrentFrame; const u32 frameSlot = m_CurrentFrame;
const auto& vkDevice = m_Device->GetDevice(); const auto& vkDevice = m_Device->GetDevice();
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU) // 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent)) { if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent, dt)) {
return; // 无 primary 相机:跳帧 return; // 无 primary 相机:跳帧
} }
const FrameSceneData& frame = m_SceneRenderer->Frame(); const FrameSceneData& frame = m_SceneRenderer->Frame();
@@ -417,6 +506,18 @@ namespace Engine
frame.prevInstanceModels.size() * frame.prevInstanceModels.size() *
sizeof(glm::mat4)); 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 // 3. acquire swapchain image
u32 imageIndex = 0; u32 imageIndex = 0;
@@ -427,13 +528,13 @@ namespace Engine
m_SyncManager->GetImageAvailable(frameSlot)); m_SyncManager->GetImageAvailable(frameSlot));
if (result.result == vk::Result::eErrorOutOfDateKHR) { if (result.result == vk::Result::eErrorOutOfDateKHR) {
HandleResize(); HandleResize(true); // 帧中止:无 draw data 引用旧资源,安全
return; return;
} }
imageIndex = result.value; imageIndex = result.value;
} }
catch (const vk::OutOfDateKHRError&) { catch (const vk::OutOfDateKHRError&) {
HandleResize(); HandleResize(true);
return; return;
} }
@@ -486,11 +587,11 @@ namespace Engine
try { try {
const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo); const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo);
if (result == vk::Result::eSuboptimalKHR) { if (result == vk::Result::eSuboptimalKHR) {
HandleResize(); HandleResize(true);
} }
} }
catch (const vk::OutOfDateKHRError&) { catch (const vk::OutOfDateKHRError&) {
HandleResize(); HandleResize(true);
} }
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight; m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
+285 -2
View File
@@ -14,6 +14,7 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm> #include <algorithm>
#include <numeric> #include <numeric>
@@ -46,6 +47,121 @@ namespace Engine
} }
return r; 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) void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir)
@@ -132,7 +248,7 @@ namespace Engine
} }
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent) vk::Extent2D targetExtent, f32 dt)
{ {
ZoneScopedN("BuildFrame"); ZoneScopedN("BuildFrame");
const auto collectStart = std::chrono::steady_clock::now(); const auto collectStart = std::chrono::steady_clock::now();
@@ -179,6 +295,7 @@ namespace Engine
m_Frame.bloomEnabled = m_BloomEnabled; m_Frame.bloomEnabled = m_BloomEnabled;
m_Frame.lodEnabled = m_LodEnabled; m_Frame.lodEnabled = m_LodEnabled;
m_Frame.debugView = m_DebugView; m_Frame.debugView = m_DebugView;
m_Frame.displayMode = m_DisplayMode;
ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f; ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f;
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass; // 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
@@ -250,6 +367,107 @@ namespace Engine
ComputeCascades(shadowDir); ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f); 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:单趟收集 + 多视图剔除 ---- // ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
// 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN // 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN
ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true ); ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true );
@@ -262,8 +480,10 @@ namespace Engine
// batch.first 拼接时加上段基址,保证段内区间连续。 // batch.first 拼接时加上段基址,保证段内区间连续。
m_Frame.instanceModels.clear(); m_Frame.instanceModels.clear();
m_Frame.prevInstanceModels.clear(); m_Frame.prevInstanceModels.clear();
m_Frame.skinOffsets.clear();
m_MainScratch.Clear(); m_MainScratch.Clear();
for (auto& s : m_CascScratch) s.Clear(); for (auto& s : m_CascScratch) s.Clear();
m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建
u32 totalInstances = 0; u32 totalInstances = 0;
bool instanceTruncated = false; bool instanceTruncated = false;
@@ -289,6 +509,13 @@ namespace Engine
} }
m_CurWorlds.emplace(entKey, t.World()); 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( const f32 worldScale = glm::max(
glm::length(glm::vec3(t.World()[0])), glm::length(glm::vec3(t.World()[0])),
@@ -359,7 +586,7 @@ namespace Engine
// 实例在扁平化后连续——firstInstance 语义的前提 // 实例在扁平化后连续——firstInstance 语义的前提
const u32 bi = batchIdx - 1; const u32 bi = batchIdx - 1;
s.result.batches[bi].count++; 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.batchDistSum[bi] += dist; // 透明排序用
s.result.instanceCount++; s.result.instanceCount++;
++totalInstances; ++totalInstances;
@@ -398,6 +625,8 @@ namespace Engine
for (const auto& m : s.batchMats[i]) { for (const auto& m : s.batchMats[i]) {
m_Frame.instanceModels.push_back(m.cur); m_Frame.instanceModels.push_back(m.cur);
m_Frame.prevInstanceModels.push_back(m.prev); m_Frame.prevInstanceModels.push_back(m.prev);
// 与实例序平行对齐(shader 按 gl_InstanceIndex 取)
m_Frame.skinOffsets.push_back(m.skinBase);
} }
} }
dst.instanceCount = s.result.instanceCount; dst.instanceCount = s.result.instanceCount;
@@ -407,6 +636,12 @@ namespace Engine
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]); 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. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排 // ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排
{ {
auto& tb = m_TranslucentScratch.result.batches; auto& tb = m_TranslucentScratch.result.batches;
@@ -466,4 +701,52 @@ namespace Engine
return true; 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 } // namespace Engine
+18 -2
View File
@@ -166,6 +166,11 @@ void Device::PickPhysicalDevice()
m_DynamicRenderingSupported = features13.dynamicRendering; m_DynamicRenderingSupported = features13.dynamicRendering;
m_Synchronization2Supported = features13.synchronization2; 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 的新特性 // 1.4 的新特性
bool localReadSupported = features14.dynamicRenderingLocalRead; bool localReadSupported = features14.dynamicRenderingLocalRead;
bool pushDescriptorSupported = features14.pushDescriptor; bool pushDescriptorSupported = features14.pushDescriptor;
@@ -183,6 +188,10 @@ void Device::PickPhysicalDevice()
ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No"); ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No");
ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No"); ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No");
ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "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) bool Device::IsDeviceSuitable(vk::PhysicalDevice device)
@@ -244,6 +253,8 @@ void Device::CreateLogicalDevice()
} }
vk::PhysicalDeviceFeatures features; vk::PhysicalDeviceFeatures features;
// MDI:按材质分组的 drawIndexedIndirectCount,drawCount>1 必需
features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE);
// ======================================================== // ========================================================
// 启用 1.4 特性链 // 启用 1.4 特性链
@@ -254,23 +265,28 @@ void Device::CreateLogicalDevice()
vk::PhysicalDeviceVulkan14Features features14{}; vk::PhysicalDeviceVulkan14Features features14{};
vk::PhysicalDeviceVulkan13Features features13{}; vk::PhysicalDeviceVulkan13Features features13{};
vk::PhysicalDeviceVulkan12Features features12{};
// 启用 1.3 的特性 // 启用 1.3 的特性
features13.dynamicRendering = VK_TRUE; features13.dynamicRendering = VK_TRUE;
features13.synchronization2 = VK_TRUE; features13.synchronization2 = VK_TRUE;
// 启用 1.2 的特性:MDI 的 drawIndexedIndirectCount(VUID-04445)
features12.drawIndirectCount = m_DrawIndirectCountSupported ? VK_TRUE : VK_FALSE;
// 启用 1.4 的新特性 // 启用 1.4 的新特性
features14.dynamicRenderingLocalRead = VK_TRUE; features14.dynamicRenderingLocalRead = VK_TRUE;
features14.pushDescriptor = VK_TRUE; features14.pushDescriptor = VK_TRUE;
// 链接:1.4 → 1.3 → (shader object) // 链接:1.4 → 1.3 → 1.2 → (shader object)
features14.pNext = &features13; features14.pNext = &features13;
features13.pNext = &features12;
// Shader Object 特性(如果支持) // Shader Object 特性(如果支持)
vk::PhysicalDeviceShaderObjectFeaturesEXT shaderObjFeatures{}; vk::PhysicalDeviceShaderObjectFeaturesEXT shaderObjFeatures{};
if (m_ShaderObjectSupported) { if (m_ShaderObjectSupported) {
shaderObjFeatures.shaderObject = VK_TRUE; shaderObjFeatures.shaderObject = VK_TRUE;
features13.pNext = &shaderObjFeatures; features12.pNext = &shaderObjFeatures;
} }
// 扩展列表 // 扩展列表
+4 -1
View File
@@ -72,7 +72,10 @@ namespace Engine
.setImageColorSpace(surfaceFormat.colorSpace) .setImageColorSpace(surfaceFormat.colorSpace)
.setImageExtent(extent) .setImageExtent(extent)
.setImageArrayLayers(1) .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) .setImageSharingMode(vk::SharingMode::eExclusive)
.setPreTransform(caps.currentTransform) .setPreTransform(caps.currentTransform)
.setCompositeAlpha(compositeAlpha) .setCompositeAlpha(compositeAlpha)
+24 -2
View File
@@ -3,17 +3,39 @@
// Shadow pass vertex:光源视角写深度。 // Shadow pass vertex:光源视角写深度。
// GPUDriven:模型矩阵经 gl_InstanceIndex 从实例缓冲取(与 Forward // GPUDriven:模型矩阵经 gl_InstanceIndex 从实例缓冲取(与 Forward
// 共用同一份数据),push 只带 lightVP(全 pass 一次)。 // 共用同一份数据),push 只带 lightVP(全 pass 一次)。
// 骨骼蒙皮与 Forward 同一套数据(binding 3/4),阴影与主体形变一致。
layout(location = 0) in vec3 inPosition; layout(location = 0) in vec3 inPosition;
layout(location = 4) in uvec4 inBoneIds; // 骨骼槽(palette 下标)
layout(location = 5) in vec4 inWeights; // 蒙皮权重(无骨骼顶点 = (1,0,0,0))
layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer { layout(std430, set = 0, binding = 1) readonly buffer ObjectBuffer {
mat4 models[]; mat4 models[];
} objects; } objects;
// 骨骼蒙皮:与 simple_shader.vert 同布局(Set0 binding 3/4)
layout(std430, set = 0, binding = 3) readonly buffer SkinOffsetBuffer {
uint skinBases[];
} skinOffsets;
layout(std430, set = 0, binding = 4) readonly buffer SkinPaletteBuffer {
mat4 bones[];
} skinPalettes;
layout(push_constant) uniform PushConstants { layout(push_constant) uniform PushConstants {
mat4 lightVP; mat4 lightVP;
} pc; } pc;
void main() { mat4 SkinMatrix() {
gl_Position = pc.lightVP * objects.models[gl_InstanceIndex] * vec4(inPosition, 1.0); uint base = skinOffsets.skinBases[gl_InstanceIndex];
return skinPalettes.bones[base + inBoneIds.x] * inWeights.x +
skinPalettes.bones[base + inBoneIds.y] * inWeights.y +
skinPalettes.bones[base + inBoneIds.z] * inWeights.z +
skinPalettes.bones[base + inBoneIds.w] * inWeights.w;
}
void main() {
mat4 skin = SkinMatrix();
vec3 skinnedPos = (skin * vec4(inPosition, 1.0)).xyz;
gl_Position = pc.lightVP * objects.models[gl_InstanceIndex] * vec4(skinnedPos, 1.0);
} }
+13 -5
View File
@@ -40,8 +40,9 @@ layout(set = 1, binding = 1) uniform sampler2D normalMap;
layout(set = 1, binding = 2) uniform sampler2D metallicRoughnessMap; // G=粗糙度 B=金属度 layout(set = 1, binding = 2) uniform sampler2D metallicRoughnessMap; // G=粗糙度 B=金属度
layout(set = 1, binding = 3) uniform sampler2D emissiveMap; layout(set = 1, binding = 3) uniform sampler2D emissiveMap;
layout(set = 1, binding = 4) uniform sampler2D aoMap; // 环境光遮蔽(R 通道) layout(set = 1, binding = 4) uniform sampler2D aoMap; // 环境光遮蔽(R 通道)
layout(set = 1, binding = 5) uniform MaterialParams { layout(set = 1, binding = 5) uniform sampler2D roughnessMap; // 独立粗糙度图(R 通道,specular 工作流)
vec4 factors; // x = metallic 因子,y = roughness 因子 layout(set = 1, binding = 6) uniform MaterialParams {
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关
} matParams; } matParams;
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF) // CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
@@ -134,9 +135,14 @@ vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
} }
void main() { void main() {
vec3 albedo = texture(albedoMap, fragUV).rgb; vec4 albedoTex = texture(albedoMap, fragUV);
vec3 albedo = albedoTex.rgb;
vec4 mr = texture(metallicRoughnessMap, fragUV); vec4 mr = texture(metallicRoughnessMap, fragUV);
float roughness = clamp(mr.g * matParams.factors.y, 0.045, 1.0); // 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
float roughness = clamp(mix(mr.g, texture(roughnessMap, fragUV).r,
matParams.factors.z) * matParams.factors.y,
0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0); float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
vec3 emissive = texture(emissiveMap, fragUV).rgb; vec3 emissive = texture(emissiveMap, fragUV).rgb;
@@ -217,6 +223,8 @@ void main() {
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽) * mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
outColor = vec4(ambient + Lo + emissive, 1.0); // alpha = albedo 贴图覆盖率(glTF alphaMode=BLEND 混合源);
// 不透明资产贴图 a=1 行为不变。预乘在 tonemap 段完成。
outColor = vec4(ambient + Lo + emissive, albedoTex.a);
outVelocity = fragVelocity; outVelocity = fragVelocity;
} }
+28 -4
View File
@@ -13,6 +13,8 @@ layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal; layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec2 inUV; layout(location = 2) in vec2 inUV;
layout(location = 3) in vec3 inTangent; layout(location = 3) in vec3 inTangent;
layout(location = 4) in uvec4 inBoneIds; // 骨骼槽(palette 下标,非全局节点号)
layout(location = 5) in vec4 inWeights; // 蒙皮权重(无骨骼顶点 = (1,0,0,0))
layout(location = 0) out vec2 fragUV; layout(location = 0) out vec2 fragUV;
layout(location = 1) out vec3 fragNormal; layout(location = 1) out vec3 fragNormal;
@@ -33,6 +35,15 @@ layout(std430, set = 0, binding = 2) readonly buffer PrevObjectBuffer {
mat4 prevModels[]; mat4 prevModels[];
} prevObjects; } prevObjects;
// 骨骼蒙皮:每实例一个 palette 基址(与 models 平行),骨骼矩阵全局扁平
layout(std430, set = 0, binding = 3) readonly buffer SkinOffsetBuffer {
uint skinBases[]; // 实例 → palette 基址
} skinOffsets;
layout(std430, set = 0, binding = 4) readonly buffer SkinPaletteBuffer {
mat4 bones[]; // 全局 palette(槽 0 = identity,静态模型直通)
} skinPalettes;
layout(set = 0, binding = 0) uniform UniformBufferObject { layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view; mat4 view;
mat4 proj; mat4 proj;
@@ -46,16 +57,28 @@ layout(set = 0, binding = 0) uniform UniformBufferObject {
vec4 cameraPos; vec4 cameraPos;
} ubo; } ubo;
// 4 权重混合的蒙皮矩阵。无骨骼顶点 weights=(1,0,0,0)+ids=0 → identity。
mat4 SkinMatrix() {
uint base = skinOffsets.skinBases[gl_InstanceIndex];
return skinPalettes.bones[base + inBoneIds.x] * inWeights.x +
skinPalettes.bones[base + inBoneIds.y] * inWeights.y +
skinPalettes.bones[base + inBoneIds.z] * inWeights.z +
skinPalettes.bones[base + inBoneIds.w] * inWeights.w;
}
void main() { void main() {
mat4 model = objects.models[gl_InstanceIndex]; mat4 model = objects.models[gl_InstanceIndex];
mat4 skin = SkinMatrix();
vec4 worldPos = model * vec4(inPosition, 1.0); vec3 skinnedPos = (skin * vec4(inPosition, 1.0)).xyz;
vec4 worldPos = model * vec4(skinnedPos, 1.0);
gl_Position = ubo.proj * ubo.view * worldPos; gl_Position = ubo.proj * ubo.view * worldPos;
// ---- TAA velocity:当前帧与上一帧的 clip 空间位置差(UV 空间)---- // ---- TAA velocity:当前帧与上一帧的 clip 空间位置差(UV 空间)----
// 抖动只进光栅化 gl_Position,不进 velocity(否则抖动本身被当成运动) // 抖动只进光栅化 gl_Position,不进 velocity(否则抖动本身被当成运动)
// v1:蒙皮形变不进 velocity(prev 用同一蒙皮姿态),边缘 ghosting 接受
vec4 currClip = ubo.projNoJitter * ubo.view * worldPos; vec4 currClip = ubo.projNoJitter * ubo.view * worldPos;
vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(inPosition, 1.0); vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(skinnedPos, 1.0);
vec4 prevClip = ubo.prevProj * ubo.prevView * prevWorldPos; vec4 prevClip = ubo.prevProj * ubo.prevView * prevWorldPos;
fragVelocity = (currClip.xy / currClip.w - prevClip.xy / prevClip.w) * 0.5; fragVelocity = (currClip.xy / currClip.w - prevClip.xy / prevClip.w) * 0.5;
@@ -64,9 +87,10 @@ void main() {
fragViewDepth = -(ubo.view * worldPos).z; fragViewDepth = -(ubo.view * worldPos).z;
mat3 normalMatrix = mat3(transpose(inverse(model))); mat3 normalMatrix = mat3(transpose(inverse(model)));
fragNormal = normalize(normalMatrix * inNormal); // 法线先过蒙皮旋转(形变方向),再过模型逆变换
fragNormal = normalize(normalMatrix * (mat3(skin) * inNormal));
// 切线随模型变换(法线贴图 TBN;无切线数据保持零,frag 侧退回几何法线) // 切线随模型变换(法线贴图 TBN;无切线数据保持零,frag 侧退回几何法线)
fragTangent = normalize(mat3(model) * inTangent); fragTangent = normalize(mat3(model) * (mat3(skin) * inTangent));
// 零向量 normalize 产生 NaN,这里直接透传零,frag 判长度 // 零向量 normalize 产生 NaN,这里直接透传零,frag 判长度
fragTangent = dot(inTangent, inTangent) > 0.0 ? fragTangent : vec3(0.0); fragTangent = dot(inTangent, inTangent) > 0.0 ? fragTangent : vec3(0.0);
+238
View File
@@ -0,0 +1,238 @@
#version 450
const int MAX_LIGHTS = 4;
const float PI = 3.14159265359;
const int kCascadeRes = 1024; // CSM 级联图边长(与引擎 SceneTypes.h 同步)
struct Light {
vec4 color; // rgb = 颜色,a = 强度
vec4 direction; // 方向光:xyz = 方向(已归一)
vec4 position; // 点光:xyz = 世界位置
vec4 params; // x = 类型(0=方向,1=点),y = 衰减半径
};
layout(location = 0) in vec2 fragUV;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec3 fragWorldPos;
layout(location = 3) in float fragViewDepth;
layout(location = 4) in vec2 fragVelocity;
layout(location = 5) in vec3 fragTangent;
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec2 outVelocity; // TAA velocity 附件
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view;
mat4 proj;
mat4 lightSpace[3]; // CSM 各级联光空间 VP
vec4 cascadeSplits; // xyz = 级联 0..2 远边界(视图深度)
vec4 ambientColor;
Light lights[MAX_LIGHTS];
mat4 projNoJitter;
mat4 prevView;
mat4 prevProj;
vec4 cameraPos;
} ubo;
// 材质纹理槽(binding = Material::Slot)
layout(set = 1, binding = 0) uniform sampler2D albedoMap;
layout(set = 1, binding = 1) uniform sampler2D normalMap;
layout(set = 1, binding = 2) uniform sampler2D metallicRoughnessMap; // G=粗糙度 B=金属度
layout(set = 1, binding = 3) uniform sampler2D emissiveMap;
layout(set = 1, binding = 4) uniform sampler2D aoMap; // 环境光遮蔽(R 通道)
layout(set = 1, binding = 5) uniform sampler2D roughnessMap; // 独立粗糙度图(R 通道,specular 工作流)
layout(set = 1, binding = 6) uniform MaterialParams {
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关
} matParams;
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
layout(set = 2, binding = 0) uniform sampler2DShadow shadowMap0; // CSM 级联 0(最近)
layout(set = 2, binding = 1) uniform sampler2DShadow shadowMap1; // 级联 1
layout(set = 2, binding = 2) uniform sampler2DShadow shadowMap2; // 级联 2(最远)
layout(set = 2, binding = 3) uniform samplerCube irradianceMap; // 漫反射环境光(IBL 阶段②)
layout(set = 2, binding = 4) uniform samplerCube prefilteredMap; // 镜面环境(mip 分 roughness,阶段③)
layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分表 (NdotV, roughness) → (scale, bias)
// 第 0 盏方向光(KeyLight)的 CSM 阴影因子。
// 级联按视图深度选择;3×3 纹素偏移采样(每 tap 硬件 2×2 PCF,
// 等效 36 次比较)→ 软阴影边缘;采样器 clamp-to-border 白色:
// 光视锥外自动 1.0(无阴影),省出界分支
float ShadowFactor(vec3 N, float viewDepth) {
// 阴影总开关(ubo.cascadeSplits.w 由 CPU 每帧写入):关闭即全亮,
// 配合 ShadowPass 空转(深度图无内容也读不到这里)
if (ubo.cascadeSplits.w < 0.5) return 1.0;
int cascade = 2;
for (int i = 0; i < 3; ++i) {
if (viewDepth < ubo.cascadeSplits[i]) { cascade = i; break; }
}
vec4 ls = ubo.lightSpace[cascade] * vec4(fragWorldPos, 1.0);
vec3 proj = ls.xyz / ls.w;
vec2 uv = proj.xy * 0.5 + 0.5;
vec3 L = normalize(-ubo.lights[0].direction.xyz);
float bias = max(0.004 * (1.0 - dot(N, L)), 0.0012);
float texel = 1.0 / float(kCascadeRes); // 与引擎 kCascadeRes 同步
float shadow = 0.0;
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
float lit;
if (cascade == 0)
lit = texture(shadowMap0, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
else if (cascade == 1)
lit = texture(shadowMap1, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
else
lit = texture(shadowMap2, vec3(uv + vec2(x, y) * texel,
proj.z - bias));
shadow += mix(0.3, 1.0, lit); // 影中保留 0.3 环境感
}
}
return shadow / 9.0;
}
// 法线贴图 → 世界空间法线(TBN 构建)。
// 无切线数据(零向量)或平法线贴图时退回几何法线。
vec3 MappedNormal(vec3 geomN, vec3 T, vec2 uv) {
if (dot(T, T) < 1e-4) return geomN;
vec3 N = geomN;
vec3 nT = normalize(T - N * dot(N, T)); // 正交化
vec3 B = cross(N, nT);
vec3 sampled = texture(normalMap, uv).xyz * 2.0 - 1.0;
if (dot(sampled, sampled) < 1e-4) return geomN;
return normalize(nT * sampled.x + B * sampled.y + N * sampled.z);
}
// ---- Cook-Torrance BRDF 各项 ----
float DistributionGGX(float NdotH, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
float GeometrySmith(float NdotV, float NdotL, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0; // 解析光源用此 k(IBL 才用 IBL 版本)
float gv = NdotV / (NdotV * (1.0 - k) + k);
float gl = NdotL / (NdotL * (1.0 - k) + k);
return gv * gl;
}
vec3 FresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
// 粗糙度感知的 Fresnel(IBL 用):粗糙表面掠射边缘的 F 上升更平缓
vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
vec3 Fmax = max(vec3(1.0 - roughness), F0);
return F0 + (Fmax - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
void main() {
vec4 albedoTex = texture(albedoMap, fragUV);
vec3 albedo = albedoTex.rgb;
vec4 mr = texture(metallicRoughnessMap, fragUV);
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
float roughness = clamp(mix(mr.g, texture(roughnessMap, fragUV).r,
matParams.factors.z) * matParams.factors.y,
0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
vec3 emissive = texture(emissiveMap, fragUV).rgb;
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, fragUV);
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
float NdotV = max(dot(N, V), 1e-4);
vec3 F0 = mix(vec3(0.04), albedo, metallic); // 电介质基准反射率 4%
vec3 Lo = vec3(0.0);
float shade0 = 1.0f; // 第 0 盏光的阴影因子(环境光同步衰减用)
for (int i = 0; i < MAX_LIGHTS; ++i) {
// 按类型分支:方向光无衰减;点光方向指向光源 + 平方距离衰减
vec3 L;
float atten = 1.0;
if (ubo.lights[i].params.x > 0.5) {
vec3 toLight = ubo.lights[i].position.xyz - fragWorldPos;
float d = length(toLight);
if (d >= ubo.lights[i].params.y) continue;
L = toLight / d;
float a = clamp(1.0 - (d * d) /
(ubo.lights[i].params.y * ubo.lights[i].params.y),
0.0, 1.0);
atten = a * a;
} else {
L = normalize(-ubo.lights[i].direction.xyz);
}
vec3 radiance = ubo.lights[i].color.rgb * ubo.lights[i].color.a * atten;
float NdotL = max(dot(N, L), 0.0);
if (NdotL <= 0.0) continue;
// Cook-Torrance 镜面项
vec3 H = normalize(V + L);
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = max(dot(H, V), 0.0);
float D = DistributionGGX(NdotH, roughness);
float G = GeometrySmith(NdotV, NdotL, roughness);
vec3 F = FresnelSchlick(NdotH2, F0);
vec3 kS = F;
vec3 kD = (1.0 - kS) * (1.0 - metallic); // 金属不参与漫反射
vec3 specular = (D * G * F) /
max(4.0 * NdotV * NdotL, 1e-4);
// 阴影只来自第 0 盏方向光(CSM)
float shade = (i == 0 && ubo.lights[0].params.x < 0.5)
? ShadowFactor(N, fragViewDepth) : 1.0;
if (i == 0) shade0 = shade;
// 漫反射影中保留 0.3(间接光近似);镜面项是"光源的镜面像",
// 光源被遮挡即完全消失——按 shade 重映射(0.3→0,1.0→1)。
// 否则掠射角下 spec(HDR 8)×0.3 仍是白块,影子比天空还亮
float shadeSpec = max((shade - 0.3f) / 0.7f, 0.0f);
Lo += (kD * albedo / PI) * radiance * NdotL * shade
+ specular * radiance * NdotL * shadeSpec;
}
// 环境光:IBL split-sum(阶段②③)。
// 漫反射:N 采样 irradiance cube(半球 Lambert 预卷积)
// 镜面:R 采样 pre-filtered cube(mip = roughness 分级)
// × BRDF LUT 查表得 F 缩放/偏置
// ambientColor = 用户可调强度/色调乘子
vec3 F_amb = FresnelSchlickRoughness(NdotV, F0, roughness);
vec3 kD_amb = (1.0 - F_amb) * (1.0 - metallic);
vec3 irradiance = texture(irradianceMap, N).rgb;
float ao = texture(aoMap, fragUV).r;
vec3 diffuse = kD_amb * irradiance * albedo * ao;
vec3 R = reflect(-V, N);
const float kMaxPrefilterMip = 4.0; // 与 Cubemap::kPrefilterMips - 1 同步
vec3 prefilteredColor = textureLod(prefilteredMap, R,
roughness * kMaxPrefilterMip).rgb;
vec2 envBRDF = texture(brdfLut, vec2(NdotV, roughness)).rg;
vec3 specular = prefilteredColor * (F_amb * envBRDF.x + envBRDF.y);
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
// alpha = albedo 贴图覆盖率(glTF alphaMode=BLEND 混合源);
// 不透明资产贴图 a=1 行为不变。预乘在 tonemap 段完成。
outColor = vec4(ambient + Lo + emissive, albedoTex.a);
outVelocity = fragVelocity;
// 深度语义:真实深度测试 + 真实写入(depthWrite 状态保持 TRUE——
// 绕开 NVIDIA shader-object "depthWrite=FALSE 下绘制" 的灾难病态)。
// 早期版本在此写 gl_FragDepth=1.0 模拟"只测不写",但深度比较符是
// eLess:片元自身的 1.0 对清屏值 1.0 与不透明深度(更小)永远测试
// 失败,透明队列一个像素都出不来(Transparent 调试视图实锤)。
// 透明段是帧内最后一个 pass 且深度每帧清空,真实写入无副作用;
// back-to-front 排序下近面后画恒通过,混合顺序仍由排序保证。
}
+8 -2
View File
@@ -1,6 +1,7 @@
#version 450 #version 450
// Tonemap pass:HDR(+bloom)→ 显示空间,兼调试视图路由上屏。 // Tonemap pass:HDR(+bloom)→ 显示空间,写 PresentTarget(offscreen,
// 由 ImGui::Image 呈现;输出位置可自由摆放)。
// 这是显示变换的显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。 // 这是显示变换的显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
// ACES = Narkowicz 拟合(线性域映射到 [0,1]),可经 push constant 关闭 // ACES = Narkowicz 拟合(线性域映射到 [0,1]),可经 push constant 关闭
// 退回纯 gamma(旧观感)。 // 退回纯 gamma(旧观感)。
@@ -40,6 +41,7 @@ const int VIEW_ENV_CUBE = 9;
const int VIEW_IRRADIANCE = 10; const int VIEW_IRRADIANCE = 10;
const int VIEW_PREFILTER = 11; const int VIEW_PREFILTER = 11;
const int VIEW_BRDF_LUT = 12; const int VIEW_BRDF_LUT = 12;
const int VIEW_TRANSPARENT = 13;
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878) // 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
layout(push_constant) uniform PushConstants { layout(push_constant) uniform PushConstants {
@@ -90,7 +92,11 @@ void main() {
outColor = vec4(pow(d, vec3(1.0 / 2.2)), 1.0); outColor = vec4(pow(d, vec3(1.0 / 2.2)), 1.0);
return; return;
} }
case VIEW_RAW_HDR: outColor = vec4(tonemap(texture(rawHdrImage, fragUV).rgb), 1.0); return; // Transparent 视图:场景侧已跳过天空盒/不透明绘制,
// raw HDR = 清屏色 + 透明队列内容(与 RAW_HDR 同一显示路径)
case VIEW_RAW_HDR:
case VIEW_TRANSPARENT:
outColor = vec4(tonemap(texture(rawHdrImage, fragUV).rgb), 1.0); return;
case VIEW_TAA: outColor = vec4(tonemap(texture(taaImage, fragUV).rgb), 1.0); return; case VIEW_TAA: outColor = vec4(tonemap(texture(taaImage, fragUV).rgb), 1.0); return;
case VIEW_BLOOM: outColor = vec4(tonemap(texture(bloomImage, fragUV).rgb * 4.0), 1.0); return; case VIEW_BLOOM: outColor = vec4(tonemap(texture(bloomImage, fragUV).rgb * 4.0), 1.0); return;
case VIEW_VELOCITY: { case VIEW_VELOCITY: {