Compare commits
11
Commits
d3c3d0cdd9
...
99e6712cdf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e6712cdf | ||
|
|
6df3be2846 | ||
|
|
fa9681aa41 | ||
|
|
25a365fce7 | ||
|
|
641f79d89e | ||
|
|
08439ed015 | ||
|
|
b65856b8a5 | ||
|
|
1d690067bd | ||
|
|
705250df76 | ||
|
|
a1c6ad20be | ||
|
|
708d714459 |
+2
-1
@@ -33,7 +33,8 @@ file(GLOB_RECURSE SHADER_SOURCES
|
|||||||
CONFIGURE_DEPENDS # 新增 shader 时自动重配
|
CONFIGURE_DEPENDS # 新增 shader 时自动重配
|
||||||
RELATIVE ${SHADER_SRC_DIR}
|
RELATIVE ${SHADER_SRC_DIR}
|
||||||
${SHADER_SRC_DIR}/*.vert
|
${SHADER_SRC_DIR}/*.vert
|
||||||
${SHADER_SRC_DIR}/*.frag)
|
${SHADER_SRC_DIR}/*.frag
|
||||||
|
${SHADER_SRC_DIR}/*.comp)
|
||||||
|
|
||||||
foreach (f ${SHADER_SOURCES})
|
foreach (f ${SHADER_SOURCES})
|
||||||
auto_compile_shader(${f})
|
auto_compile_shader(${f})
|
||||||
|
|||||||
+218
-23
@@ -11,25 +11,45 @@
|
|||||||
#include <Engine/Core/Application.h>
|
#include <Engine/Core/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/ECS/Scene.h>
|
#include <Engine/ECS/Scene.h>
|
||||||
#include <Engine/ECS/EntityHandle.h>
|
#include <Engine/ECS/EntityHandle.h>
|
||||||
#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 <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
|
||||||
#include <GLFW/glfw3.h>
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
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";
|
||||||
|
|
||||||
class SandboxApp final : public Application
|
class SandboxApp final : public Application
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -38,6 +58,15 @@ namespace
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~SandboxApp() override
|
||||||
|
{
|
||||||
|
// worker 只做 CPU 解码(不碰 Vulkan),在任何 GPU 资源
|
||||||
|
// 析构之前 join 即安全;派生析构先于基类成员析构执行
|
||||||
|
if (m_EnvWorker.joinable()) {
|
||||||
|
m_EnvWorker.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// ---- 生命周期 ----
|
// ---- 生命周期 ----
|
||||||
|
|
||||||
@@ -48,14 +77,36 @@ namespace
|
|||||||
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
|
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
|
||||||
EnableImGui();
|
EnableImGui();
|
||||||
|
|
||||||
|
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
|
||||||
|
// 其余环境在 ImGui 里按需 lazy 加载后切换
|
||||||
|
const auto env = GetAssets().LoadEnvironment(
|
||||||
|
kEnvPaths[kDefaultEnv]);
|
||||||
|
if (env.Valid()) {
|
||||||
|
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
|
||||||
|
}
|
||||||
|
|
||||||
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
|
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
|
||||||
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
|
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
|
||||||
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
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");
|
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
|
||||||
|
|
||||||
m_Spinners.push_back(scene.CreateEntity("backpack"));
|
// 大模型异步加载:assimp 解析在 worker 线程(SunTemple 约 3s),
|
||||||
m_Spinners[0].AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
|
// 完成后 OnUpdate 收割 → GPU 构建 → 建实体(cm 尺度 → 0.001)
|
||||||
m_Spinners[0].GetComponent<TransformComponent>().SetScale(glm::vec3{1.0});
|
m_TempleTask.Run([] { return Model::ParseFromDisk(kTemplePath); });
|
||||||
|
m_TempleScale = 0.001f;
|
||||||
|
|
||||||
|
|
||||||
|
int countSize = 0;
|
||||||
|
for (int i = 0; i < countSize; i ++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < countSize; j ++)
|
||||||
|
{
|
||||||
|
auto m = scene.CreateEntity();
|
||||||
|
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);
|
||||||
@@ -112,6 +163,24 @@ namespace
|
|||||||
|
|
||||||
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)
|
||||||
@@ -135,13 +204,72 @@ namespace
|
|||||||
|
|
||||||
// ---- 事件 ----
|
// ---- 事件 ----
|
||||||
|
|
||||||
|
// ---- 异步环境加载(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 控件的地方)----
|
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
|
||||||
|
|
||||||
void OnRenderOverlay() override
|
void OnRenderOverlay() override
|
||||||
{
|
{
|
||||||
ImGui::Begin("Scene Debug");
|
ImGui::Begin("Scene Debug");
|
||||||
|
auto& renderer = GetRenderer();
|
||||||
|
|
||||||
// 阴影光源方向:拖动滑条实时看影子变化
|
// ---- Scene:场景内容(灯/动画) ----
|
||||||
|
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
|
{
|
||||||
|
// 阴影光源(无开关,控件平铺聚拢)
|
||||||
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>())
|
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>())
|
||||||
{
|
{
|
||||||
ImGui::Text("Key Light (shadow caster)");
|
ImGui::Text("Key Light (shadow caster)");
|
||||||
@@ -154,11 +282,13 @@ namespace
|
|||||||
light->color.a = 2.0f;
|
light->color.a = 2.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ImGui::Checkbox("Pause Animation", &m_PauseAnim);
|
||||||
|
}
|
||||||
|
|
||||||
// Tonemap + Bloom 参数(HDR 后处理输出端)
|
// ---- Rendering:渲染管线参数与开关 ----
|
||||||
auto& renderer = GetRenderer();
|
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
ImGui::Separator();
|
{
|
||||||
ImGui::Text("Tonemap");
|
// Tonemap(无开关,平铺)
|
||||||
f32 exposure = renderer.GetExposure();
|
f32 exposure = renderer.GetExposure();
|
||||||
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
|
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
|
||||||
{
|
{
|
||||||
@@ -170,7 +300,14 @@ namespace
|
|||||||
renderer.SetUseACES(aces);
|
renderer.SetUseACES(aces);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Text("Bloom");
|
// Bloom:开关即显隐——勾上才显示参数
|
||||||
|
bool bloom = renderer.GetBloomEnabled();
|
||||||
|
if (ImGui::Checkbox("Bloom", &bloom))
|
||||||
|
{
|
||||||
|
renderer.SetBloomEnabled(bloom);
|
||||||
|
}
|
||||||
|
if (bloom)
|
||||||
|
{
|
||||||
f32 threshold = renderer.GetBloomThreshold();
|
f32 threshold = renderer.GetBloomThreshold();
|
||||||
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
|
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
|
||||||
{
|
{
|
||||||
@@ -181,32 +318,78 @@ namespace
|
|||||||
{
|
{
|
||||||
renderer.SetBloomStrength(strength);
|
renderer.SetBloomStrength(strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调试视图:帧历史差异(静止=黑,运动边缘发光=链路正确)
|
|
||||||
bool temporalDiff = renderer.GetShowTemporalDiff();
|
|
||||||
if (ImGui::Checkbox("Temporal Diff View", &temporalDiff))
|
|
||||||
{
|
|
||||||
renderer.SetShowTemporalDiff(temporalDiff);
|
|
||||||
}
|
}
|
||||||
ImGui::Checkbox("Pause Animation", &m_PauseAnim);
|
|
||||||
|
|
||||||
|
// 无参数的纯开关:横排平铺
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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)
|
// Pass 级 GPU 耗时(RG timestamp)
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Text("GPU Timings");
|
|
||||||
ImGui::Text("instances: %u main / %u shadow",
|
ImGui::Text("instances: %u main / %u shadow",
|
||||||
renderer.GetMainInstanceCount(),
|
renderer.GetMainInstanceCount(),
|
||||||
renderer.GetShadowInstanceCount());
|
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& names = renderer.GetPassNames();
|
||||||
const auto& times = renderer.GetPassGpuMs();
|
const auto& times = renderer.GetPassGpuMs();
|
||||||
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
|
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
|
||||||
{
|
{
|
||||||
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
|
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 帧率控制
|
// ---- Frame:帧率控制与显示 ----
|
||||||
|
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
|
||||||
|
{
|
||||||
auto& app = *this;
|
auto& app = *this;
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Text("Frame");
|
|
||||||
bool vsync = app.GetVSync();
|
bool vsync = app.GetVSync();
|
||||||
if (ImGui::Checkbox("VSync", &vsync))
|
if (ImGui::Checkbox("VSync", &vsync))
|
||||||
{
|
{
|
||||||
@@ -225,11 +408,10 @@ namespace
|
|||||||
app.SetTargetFps(target);
|
app.SetTargetFps(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
|
||||||
ImGui::Text("fps: %.1f frame: %llu",
|
ImGui::Text("fps: %.1f frame: %llu",
|
||||||
1.0f / GetDeltaTime(),
|
1.0f / GetDeltaTime(),
|
||||||
static_cast<unsigned long long>(GetFrameCount()));
|
static_cast<unsigned long long>(GetFrameCount()));
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
@@ -305,6 +487,19 @@ namespace
|
|||||||
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; // 冻结动画时钟(帧历史 diff 验证用)
|
||||||
|
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)----
|
||||||
|
AsyncTask<ParsedModel> m_TempleTask;
|
||||||
|
f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,15 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine
|
||||||
namespace Assets {
|
{
|
||||||
|
namespace Assets
|
||||||
|
{
|
||||||
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
|
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
class AssetCache {
|
class AssetCache
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
// loader:path → 资产实例(失败返回 nullptr)
|
// loader:path → 资产实例(失败返回 nullptr)
|
||||||
using LoaderFn = std::function<Scope<T>(const std::string&)>;
|
using LoaderFn = std::function<Scope<T>(const std::string&)>;
|
||||||
@@ -41,23 +43,28 @@ namespace Engine {
|
|||||||
// 加载(带路径去重)。失败返回无效句柄。
|
// 加载(带路径去重)。失败返回无效句柄。
|
||||||
AssetRef<T> Load(const std::string& path, const LoaderFn& loader)
|
AssetRef<T> Load(const std::string& path, const LoaderFn& loader)
|
||||||
{
|
{
|
||||||
if (auto it = m_ByPath.find(path); it != m_ByPath.end()) {
|
if (auto it = m_ByPath.find(path); it != m_ByPath.end())
|
||||||
|
{
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scope<T> asset = loader(path);
|
Scope<T> asset = loader(path);
|
||||||
if (!asset) {
|
if (!asset)
|
||||||
|
{
|
||||||
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
|
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetRef<T> h;
|
AssetRef<T> h;
|
||||||
if (!m_Free.empty()) {
|
if (!m_Free.empty())
|
||||||
|
{
|
||||||
h.index = m_Free.back();
|
h.index = m_Free.back();
|
||||||
m_Free.pop_back();
|
m_Free.pop_back();
|
||||||
h.generation = m_Entries[h.index].generation;
|
h.generation = m_Entries[h.index].generation;
|
||||||
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
|
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
h.index = static_cast<u32>(m_Entries.size());
|
h.index = static_cast<u32>(m_Entries.size());
|
||||||
h.generation = 1;
|
h.generation = 1;
|
||||||
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
|
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
|
||||||
@@ -70,12 +77,14 @@ namespace Engine {
|
|||||||
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
|
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
|
||||||
T* Get(AssetRef<T> h) const
|
T* Get(AssetRef<T> h) const
|
||||||
{
|
{
|
||||||
if (!h.Valid() || h.index >= m_Entries.size()) {
|
if (!h.Valid() || h.index >= m_Entries.size())
|
||||||
|
{
|
||||||
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
|
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
|
||||||
h.index, h.generation);
|
h.index, h.generation);
|
||||||
}
|
}
|
||||||
const Entry& e = m_Entries[h.index];
|
const Entry& e = m_Entries[h.index];
|
||||||
if (e.generation != h.generation) {
|
if (e.generation != h.generation)
|
||||||
|
{
|
||||||
ENGINE_FATAL("AssetCache::Get: stale handle for '{}' (gen {} != {})",
|
ENGINE_FATAL("AssetCache::Get: stale handle for '{}' (gen {} != {})",
|
||||||
e.path, h.generation, e.generation);
|
e.path, h.generation, e.generation);
|
||||||
}
|
}
|
||||||
@@ -114,7 +123,8 @@ namespace Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Entry {
|
struct Entry
|
||||||
|
{
|
||||||
std::string path;
|
std::string path;
|
||||||
u32 generation = 1;
|
u32 generation = 1;
|
||||||
Scope<T> asset;
|
Scope<T> asset;
|
||||||
@@ -128,7 +138,8 @@ namespace Engine {
|
|||||||
|
|
||||||
// ---------- AssetManager:全引擎资产入口 ----------
|
// ---------- AssetManager:全引擎资产入口 ----------
|
||||||
|
|
||||||
class AssetManager {
|
class AssetManager
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
// 上传服务由组合根(Application)注入:渲染层实现接口,
|
// 上传服务由组合根(Application)注入:渲染层实现接口,
|
||||||
// 资产层不感知 Vulkan 具体类
|
// 资产层不感知 Vulkan 具体类
|
||||||
@@ -139,12 +150,23 @@ namespace Engine {
|
|||||||
AssetManager& operator=(const AssetManager&) = delete;
|
AssetManager& operator=(const AssetManager&) = delete;
|
||||||
|
|
||||||
ModelRef LoadModel(const std::string& path);
|
ModelRef LoadModel(const std::string& path);
|
||||||
|
// 异步路径:解析已在后台线程完成(路径已缓存则产物丢弃去重)
|
||||||
|
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
|
||||||
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
|
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
|
||||||
void UnloadModel(ModelRef h) { m_Models.Unload(h); }
|
void UnloadModel(ModelRef h) { m_Models.Unload(h); }
|
||||||
|
|
||||||
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
|
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
|
||||||
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
|
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
|
||||||
|
|
||||||
|
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||||
|
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||||
|
EnvRef LoadEnvironment(const std::string& path);
|
||||||
|
// 异步路径:解码已在后台线程完成(若路径已缓存,解码产物被
|
||||||
|
// 丢弃去重——上层可先查缓存再起 worker,此处不做强约束)
|
||||||
|
EnvRef LoadEnvironment(const std::string& path,
|
||||||
|
DecodedEquirect&& decoded);
|
||||||
|
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
|
||||||
|
|
||||||
AssetCache<Model>& Models() { return m_Models; }
|
AssetCache<Model>& Models() { return m_Models; }
|
||||||
|
|
||||||
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
||||||
@@ -154,8 +176,8 @@ namespace Engine {
|
|||||||
IAssetUploader& m_Uploader;
|
IAssetUploader& m_Uploader;
|
||||||
|
|
||||||
AssetCache<Model> m_Models;
|
AssetCache<Model> m_Models;
|
||||||
|
AssetCache<Cubemap> m_Environments;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Assets
|
} // namespace Assets
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/Model.h>
|
||||||
|
#include <Engine/Renderer/Cubemap.h>
|
||||||
|
|
||||||
namespace Engine::Assets {
|
namespace Engine::Assets {
|
||||||
|
|
||||||
@@ -29,8 +30,19 @@ namespace Engine::Assets {
|
|||||||
// 磁盘模型 → 解析 + GPU 上传(失败返回 nullptr)
|
// 磁盘模型 → 解析 + GPU 上传(失败返回 nullptr)
|
||||||
virtual Scope<Model> LoadModel(const std::string& path) = 0;
|
virtual Scope<Model> LoadModel(const std::string& path) = 0;
|
||||||
|
|
||||||
|
// 异步路径:解析已在后台线程完成,此调用只做 GPU 构建(主线程)
|
||||||
|
virtual Scope<Model> LoadModel(const std::string& path,
|
||||||
|
ParsedModel&& parsed) = 0;
|
||||||
|
|
||||||
// 程序化纯色平面(size 为半边长)
|
// 程序化纯色平面(size 为半边长)
|
||||||
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
||||||
|
|
||||||
|
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||||
|
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||||
|
|
||||||
|
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
|
||||||
|
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||||
|
DecodedEquirect&& decoded) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine::Assets
|
} // namespace Engine::Assets
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
// Engine::Assets::Engine::Model 这类错误嵌套命名空间)
|
// Engine::Assets::Engine::Model 这类错误嵌套命名空间)
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
class Model;
|
class Model;
|
||||||
|
class Cubemap;
|
||||||
class Texture;
|
class Texture;
|
||||||
class Material;
|
class Material;
|
||||||
}
|
}
|
||||||
@@ -39,6 +40,7 @@ namespace Engine {
|
|||||||
|
|
||||||
// 常用别名(新增资产类型时在这里加一行)
|
// 常用别名(新增资产类型时在这里加一行)
|
||||||
using ModelRef = AssetRef<Model>;
|
using ModelRef = AssetRef<Model>;
|
||||||
|
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/9/24.
|
||||||
|
//
|
||||||
|
// AsyncTask:单 in-flight 异步任务的通用收割工具(header-only)。
|
||||||
|
//
|
||||||
|
// 异步加载的公共骨架(已验证两次:环境 DecodeHdr / 模型 ParseFromDisk):
|
||||||
|
// worker 线程跑纯 CPU 任务 → 原子标志通知 → 主线程每帧轮询收割。
|
||||||
|
// Vulkan 提交/描述符等 GPU 工作全部留在主线程(设备非线程安全)。
|
||||||
|
//
|
||||||
|
// AsyncTask<ParsedModel> task;
|
||||||
|
// task.Run([] { return Model::ParseFromDisk(path); }); // 发起
|
||||||
|
// if (task.Done()) { auto p = task.Take(); ... } // 每帧收割
|
||||||
|
// // 析构自动 join(worker 只做纯 CPU 工作,join 恒安全)
|
||||||
|
//
|
||||||
|
// 语义:Run 在忙碌时先 join 并丢弃旧结果(覆盖式);"快速连切只保留
|
||||||
|
// 最终意图"的 pending 接力由调用方持有(参照 main.cpp 环境加载)。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_ASYNCTASK_H
|
||||||
|
#define HELLOVULKAN_ASYNCTASK_H
|
||||||
|
|
||||||
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
class AsyncTask {
|
||||||
|
public:
|
||||||
|
AsyncTask() = default;
|
||||||
|
~AsyncTask() { Join(); }
|
||||||
|
|
||||||
|
AsyncTask(const AsyncTask&) = delete;
|
||||||
|
AsyncTask& operator=(const AsyncTask&) = delete;
|
||||||
|
|
||||||
|
// 发起任务。忙碌时先等待旧任务完成并放弃其结果。
|
||||||
|
void Run(std::function<T()> job)
|
||||||
|
{
|
||||||
|
Join();
|
||||||
|
m_Busy = true;
|
||||||
|
m_Done.store(false);
|
||||||
|
m_Worker = std::thread([this, j = std::move(job)]() {
|
||||||
|
m_Result = j();
|
||||||
|
m_Done.store(true); // release:结果对主线程 acquire 可见
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Busy() const { return m_Busy; }
|
||||||
|
bool Done() const { return m_Busy && m_Done.load(); }
|
||||||
|
|
||||||
|
// 收割:join worker 并移交结果。只在 Done() 为 true 时调用。
|
||||||
|
T Take()
|
||||||
|
{
|
||||||
|
m_Worker.join(); // join = 同步点,此后结果归主线程
|
||||||
|
m_Busy = false;
|
||||||
|
T out = std::move(m_Result);
|
||||||
|
m_Result = T{};
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭前兜底等待(析构自动调用)
|
||||||
|
void Join()
|
||||||
|
{
|
||||||
|
if (m_Worker.joinable()) {
|
||||||
|
m_Worker.join();
|
||||||
|
}
|
||||||
|
m_Busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::thread m_Worker;
|
||||||
|
std::atomic<bool> m_Done{false};
|
||||||
|
T m_Result{};
|
||||||
|
bool m_Busy = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_ASYNCTASK_H
|
||||||
@@ -11,6 +11,12 @@
|
|||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
|
namespace Logger
|
||||||
|
{
|
||||||
|
void Initialize();
|
||||||
|
void Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
#define ENGINE_TRACE(...) ::spdlog::trace(__VA_ARGS__)
|
#define ENGINE_TRACE(...) ::spdlog::trace(__VA_ARGS__)
|
||||||
#define ENGINE_INFO(...) ::spdlog::info(__VA_ARGS__)
|
#define ENGINE_INFO(...) ::spdlog::info(__VA_ARGS__)
|
||||||
#define ENGINE_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
|
#define ENGINE_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ namespace Engine::ECS {
|
|||||||
|
|
||||||
// ---- 场景级设置 ----
|
// ---- 场景级设置 ----
|
||||||
struct Settings {
|
struct Settings {
|
||||||
glm::vec4 ambientColor{0.07f, 0.07f, 0.09f, 0.0f};
|
glm::vec4 ClearColor = {0.1, 0.1, 0.1, 1.0};
|
||||||
|
glm::vec4 AmbientColor{0.07f, 0.07f, 0.09f, 0.0f};
|
||||||
|
bool DrawSkybox = true;
|
||||||
};
|
};
|
||||||
Settings& GetSettings() { return m_Settings; }
|
Settings& GetSettings() { return m_Settings; }
|
||||||
|
|
||||||
|
|||||||
@@ -212,13 +212,6 @@ namespace Engine::RG {
|
|||||||
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
|
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
|
||||||
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
|
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
|
||||||
|
|
||||||
// 调试/截图:取物理 image + 静息布局(帧尾 barrier 恢复用)。
|
|
||||||
// imported 资源返回 false(无物理实例)
|
|
||||||
bool GetCaptureTarget(TextureHandle h, u32 frameSlot,
|
|
||||||
vk::Image& outImage, vk::Extent2D& outExtent,
|
|
||||||
vk::Format& outFormat,
|
|
||||||
vk::ImageLayout& outResting) const;
|
|
||||||
|
|
||||||
// 帧历史槽位 = 上一帧使用的槽位:(slot - 1 + F) mod F
|
// 帧历史槽位 = 上一帧使用的槽位:(slot - 1 + F) mod F
|
||||||
u32 HistorySlot(u32 frameSlot) const {
|
u32 HistorySlot(u32 frameSlot) const {
|
||||||
return (frameSlot + m_FramesInFlight - 1) % m_FramesInFlight;
|
return (frameSlot + m_FramesInFlight - 1) % m_FramesInFlight;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
//
|
//
|
||||||
// Created by atdunbg on 2026/9/23.
|
// Created by atdunbg on 2026/9/23.
|
||||||
//
|
//
|
||||||
// Cubemap:环境立方体贴图 + IBL 预积分资源(阶段②③,split-sum)。
|
// Cubemap:环境立方体贴图 + IBL 预积分资源(split-sum 四件套)。
|
||||||
|
//
|
||||||
|
// 纯 GPU 资源持有类(对齐 Model 的定位):只管 image/view/采样器的
|
||||||
|
// 同生共死,不做任何生成/上传。预积分由 IblGenerator(compute 链)
|
||||||
|
// 产出后注入——环境切换的入口在 Assets 层(AssetManager::LoadEnvironment)。
|
||||||
//
|
//
|
||||||
// 从等距柱状 HDR(.hdr,polyhaven 等)生成,CPU 一次性预积分:
|
|
||||||
// env cube —— 重采样 6 面(Skybox 背景)
|
// env cube —— 重采样 6 面(Skybox 背景)
|
||||||
// irradiance cube —— 半球 Lambert 积分(PBR 漫反射 ambient)
|
// irradiance cube —— 半球 Lambert 积分(PBR 漫反射 ambient)
|
||||||
// prefilter cube —— GGX 重要性采样 × mip 分级 roughness(镜面 IBL)
|
// prefilter cube —— GGX 重要性采样 × mip 分级 roughness(镜面 IBL)
|
||||||
// BRDF LUT —— 2D 积分表(纯数学,与环境内容无关)
|
// BRDF LUT —— 2D 积分表(纯数学,与环境内容无关)
|
||||||
// 全部 fp16 上传,运行时采样零积分开销。
|
// 全部 fp16,运行时采样零积分开销。
|
||||||
//
|
|
||||||
// 后续演进:CPU 预积分 → compute pass(动态环境零卡顿);本类的
|
|
||||||
// 方向表/上传路径届时整体迁移,接口不变。
|
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifndef HELLOVULKAN_CUBEMAP_H
|
#ifndef HELLOVULKAN_CUBEMAP_H
|
||||||
@@ -19,13 +19,24 @@
|
|||||||
|
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
|
|
||||||
|
// 环境的 CPU 源数据(等距柱状 HDR 解码产物,RGBA float 行优先)。
|
||||||
|
// 纯数据无线程状态:异步加载时由后台线程产出(DecodeHdr),
|
||||||
|
// 主线程消费(FromDecoded → GPU 生成)。decode 是切换耗时大头,
|
||||||
|
// 搬出主线程后运行时切换只剩 GPU 段(~40ms)。
|
||||||
|
struct DecodedEquirect {
|
||||||
|
std::vector<f32> pixels;
|
||||||
|
u32 w = 0;
|
||||||
|
u32 h = 0;
|
||||||
|
bool Valid() const { return !pixels.empty() && w > 0 && h > 0; }
|
||||||
|
};
|
||||||
|
|
||||||
// GPU 纹理资源三元组(RHI 惯例):image/view/显存分配同生共死,
|
// GPU 纹理资源三元组(RHI 惯例):image/view/显存分配同生共死,
|
||||||
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
|
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
|
||||||
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
|
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
|
||||||
@@ -37,18 +48,6 @@ namespace Engine {
|
|||||||
|
|
||||||
class Cubemap {
|
class Cubemap {
|
||||||
public:
|
public:
|
||||||
// 从等距柱状 HDR 生成(CPU 重采样 + irradiance/prefilter/BRDF 预积分)
|
|
||||||
static Scope<Cubemap> CreateFromHdrEquirect(Device& device,
|
|
||||||
MemoryAllocator& allocator,
|
|
||||||
const std::string& path,
|
|
||||||
u32 faceSize);
|
|
||||||
|
|
||||||
// 中性兜底(无 HDR 资产时):cube 均为常数色;BRDF LUT 仍为
|
|
||||||
// 真实积分(纯数学)。保证引用侧描述符恒有效(validation)
|
|
||||||
static Scope<Cubemap> CreateNeutral(Device& device,
|
|
||||||
MemoryAllocator& allocator,
|
|
||||||
const glm::vec3& rgb);
|
|
||||||
|
|
||||||
~Cubemap();
|
~Cubemap();
|
||||||
|
|
||||||
Cubemap(const Cubemap&) = delete;
|
Cubemap(const Cubemap&) = delete;
|
||||||
@@ -67,6 +66,8 @@ namespace Engine {
|
|||||||
vk::Sampler GetBrdfLutSampler() const { return m_LutSampler; }
|
vk::Sampler GetBrdfLutSampler() const { return m_LutSampler; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class IblGenerator; // 生成端唯一构造入口
|
||||||
|
|
||||||
Cubemap(Device& device, MemoryAllocator& allocator,
|
Cubemap(Device& device, MemoryAllocator& allocator,
|
||||||
EnvTexture env, EnvTexture irr, EnvTexture pref,
|
EnvTexture env, EnvTexture irr, EnvTexture pref,
|
||||||
EnvTexture lut, vk::Sampler envSampler, vk::Sampler lutSampler)
|
EnvTexture lut, vk::Sampler envSampler, vk::Sampler lutSampler)
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/9/24.
|
||||||
|
//
|
||||||
|
// IblGenerator:IBL 预积分的 compute 生成端(原 Cubemap CPU 积分路径的
|
||||||
|
// GPU 化,"动态环境切换零卡顿"第一步)。
|
||||||
|
//
|
||||||
|
// 管线:等距柱状 HDR → 上传 equirect 中间纹理(免 CPU 逐像素转换)
|
||||||
|
// → 一次性提交四段 dispatch:
|
||||||
|
// ① equirect_to_cube(硬件过滤重采样 6 面)
|
||||||
|
// ② irradiance(半球 Lambert 卷积,采样 env cube)
|
||||||
|
// ③ prefilter × mip(GGX 重要性采样,push roughness)
|
||||||
|
// ④ BRDF LUT(纯数学)
|
||||||
|
// → fence 等待 → 产出 Cubemap(四件套 fp16 资源)。
|
||||||
|
//
|
||||||
|
// 为什么不进 RenderGraph:RG 管理的是帧内 2D 瞬时资源(无 cube/数组层/
|
||||||
|
// storage 写/条件执行),而预积分是"资产加载时跑一次"的持久资源——
|
||||||
|
// 独立一次性提交与原上传路径同形态,侵入最小。残留卡顿 = fence 等待
|
||||||
|
// (毫秒级),异步 compute queue / IO 线程解码是后续演进点。
|
||||||
|
//
|
||||||
|
// 产出与消费端契约(SkyboxPass / SceneGpu set2 binding3-5)不变:
|
||||||
|
// view 形态与原 CPU 路径完全一致,绑定代码零改动。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_IBLGENERATOR_H
|
||||||
|
#define HELLOVULKAN_IBLGENERATOR_H
|
||||||
|
|
||||||
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/Renderer/Cubemap.h> // DecodedEquirect(解码产物数据契约)
|
||||||
|
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <vulkan/vulkan.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
class Device;
|
||||||
|
class DescriptorManager;
|
||||||
|
class ShaderManager;
|
||||||
|
class Cubemap;
|
||||||
|
|
||||||
|
class IblGenerator {
|
||||||
|
public:
|
||||||
|
IblGenerator(Device& device, MemoryAllocator& allocator,
|
||||||
|
DescriptorManager& descriptors, ShaderManager& shaders);
|
||||||
|
~IblGenerator();
|
||||||
|
|
||||||
|
IblGenerator(const IblGenerator&) = delete;
|
||||||
|
IblGenerator& operator=(const IblGenerator&) = delete;
|
||||||
|
|
||||||
|
// ---- 同步便捷路径(等价 DecodeHdr + FromDecoded)----
|
||||||
|
// 等距柱状 HDR → 环境四件套(失败返回 nullptr,上层走 Neutral 兜底)
|
||||||
|
Scope<Cubemap> FromHdrEquirect(const std::string& path, u32 faceSize);
|
||||||
|
|
||||||
|
// ---- 异步加载两段式(decode 是切换耗时大头,可搬后台线程)----
|
||||||
|
// 纯 CPU:HDR 文件 → 像素数据。无线程状态,worker 线程可安全调用。
|
||||||
|
static DecodedEquirect DecodeHdr(const std::string& path);
|
||||||
|
|
||||||
|
// GPU 生成(必须主线程):预解码数据 → 四件套。debugName 仅用于日志。
|
||||||
|
Scope<Cubemap> FromDecoded(const DecodedEquirect& decoded, u32 faceSize,
|
||||||
|
const std::string& debugName);
|
||||||
|
|
||||||
|
// 中性兜底:常数色环境 + 真实 BRDF LUT(描述符恒有效)
|
||||||
|
Scope<Cubemap> Neutral(const glm::vec3& rgb);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// cube 兼容 image(2D array 写 + cube 采样双 view)
|
||||||
|
struct CubeTarget {
|
||||||
|
vk::Image image{};
|
||||||
|
std::vector<vk::ImageView> storageViews; // 每 mip 一个(storage 写必须单 mip)
|
||||||
|
vk::ImageView cubeView{}; // eCube 全 mip(采样端)
|
||||||
|
MemoryAllocator::Allocation allocation;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从 HDR 像素数据生成(Neutral 走同一链,仅 equirect 为 1×1 常数)
|
||||||
|
Scope<Cubemap> Generate(const f32* pixels, u32 w, u32 h,
|
||||||
|
u32 faceSize, const std::string& debugName);
|
||||||
|
|
||||||
|
CubeTarget CreateCubeTarget(u32 size, u32 mips) const;
|
||||||
|
void ReleaseStorageViews(const CubeTarget& t) const;
|
||||||
|
|
||||||
|
vk::Sampler CreateEnvSampler(u32 maxLod) const;
|
||||||
|
vk::Sampler CreateLutSampler() const;
|
||||||
|
|
||||||
|
Device& m_Device;
|
||||||
|
MemoryAllocator& m_Allocator;
|
||||||
|
DescriptorManager& m_Descriptors;
|
||||||
|
ShaderManager& m_Shaders;
|
||||||
|
|
||||||
|
// set 布局:A = {0 采样输入, 1 storage 输出},B = {0 storage}
|
||||||
|
vk::DescriptorSetLayout m_IblSetLayout;
|
||||||
|
vk::DescriptorSetLayout m_LutSetLayout;
|
||||||
|
// 管线布局:无 push(equirect/irradiance/LUT)与 4B push(prefilter)
|
||||||
|
vk::PipelineLayout m_IblLayout;
|
||||||
|
vk::PipelineLayout m_IblLayoutPush;
|
||||||
|
vk::PipelineLayout m_LutLayout;
|
||||||
|
|
||||||
|
vk::ShaderEXT m_ToCubeShader{};
|
||||||
|
vk::ShaderEXT m_IrrShader{};
|
||||||
|
vk::ShaderEXT m_PrefShader{};
|
||||||
|
vk::ShaderEXT m_LutShader{};
|
||||||
|
|
||||||
|
vk::Sampler m_EquirectSampler; // S=repeat(经度环绕)T=clamp
|
||||||
|
vk::CommandPool m_Pool; // 持久复用,每次生成 reset
|
||||||
|
vk::CommandBuffer m_Cmd;
|
||||||
|
vk::Fence m_Fence;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_IBLGENERATOR_H
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <Engine/Renderer/Mesh.h>
|
#include <Engine/Renderer/Mesh.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -28,14 +29,42 @@ namespace Engine
|
|||||||
class Texture;
|
class Texture;
|
||||||
class Material;
|
class Material;
|
||||||
|
|
||||||
|
// ---- CPU 解析产物(异步加载的跨线程交棒数据)----
|
||||||
|
// 纯数据:几何 + 材质槽位引用(路径 + 因子),无任何 GPU 资源。
|
||||||
|
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
|
||||||
|
struct ParsedMesh {
|
||||||
|
std::vector<Vertex> vertices;
|
||||||
|
std::vector<u32> indices;
|
||||||
|
std::array<std::string, Material::kSlotCount> texPaths;
|
||||||
|
MaterialParams params;
|
||||||
|
};
|
||||||
|
struct ParsedModel {
|
||||||
|
std::string path;
|
||||||
|
bool isGltf = false;
|
||||||
|
std::vector<ParsedMesh> meshes;
|
||||||
|
bool Valid() const { return !meshes.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
class Model
|
class Model
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// 同步便捷路径(等价 ParseFromDisk + FromParsed,启动期小模型可用)
|
||||||
Model(Device& device, MemoryAllocator& allocator,
|
Model(Device& device, MemoryAllocator& allocator,
|
||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout textureLayout,
|
vk::DescriptorSetLayout textureLayout,
|
||||||
const std::string& path);
|
const std::string& path);
|
||||||
|
|
||||||
|
// ---- 异步加载两段式(assimp 解析占大模型耗时大头,可搬后台线程)----
|
||||||
|
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
|
||||||
|
static ParsedModel ParseFromDisk(const std::string& path);
|
||||||
|
|
||||||
|
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
|
||||||
|
// 解析失败返回 nullptr。
|
||||||
|
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
|
||||||
|
DescriptorManager& descriptorManager,
|
||||||
|
vk::DescriptorSetLayout textureLayout,
|
||||||
|
ParsedModel&& parsed);
|
||||||
|
|
||||||
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||||
// size 为半边长(平面总边长 = 2*size)。
|
// size 为半边长(平面总边长 = 2*size)。
|
||||||
struct ProceduralTag {};
|
struct ProceduralTag {};
|
||||||
@@ -57,9 +86,19 @@ namespace Engine
|
|||||||
vk::DescriptorSetLayout textureLayout,
|
vk::DescriptorSetLayout textureLayout,
|
||||||
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color);
|
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color);
|
||||||
|
|
||||||
|
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||||||
|
Model(Device& device, MemoryAllocator& allocator,
|
||||||
|
DescriptorManager& descriptorManager,
|
||||||
|
vk::DescriptorSetLayout textureLayout);
|
||||||
|
|
||||||
void LoadModel(const std::string& path);
|
void LoadModel(const std::string& path);
|
||||||
void ProcessNode(aiNode* node, const aiScene* scene);
|
static void ProcessNode(const aiNode* node, const aiScene* scene,
|
||||||
Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene);
|
std::vector<ParsedMesh>& out,
|
||||||
|
const std::filesystem::path& modelDir,
|
||||||
|
bool isGltf);
|
||||||
|
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||||
|
const std::filesystem::path& modelDir,
|
||||||
|
bool isGltf);
|
||||||
void InitDefaultTextures();
|
void InitDefaultTextures();
|
||||||
|
|
||||||
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||||
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ namespace Engine::Passes {
|
|||||||
class BloomPass final : public RenderPass {
|
class BloomPass final : public RenderPass {
|
||||||
public:
|
public:
|
||||||
BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
|
BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
|
||||||
|
FrameSceneData& frame,
|
||||||
RG::TextureHandle hdrColor,
|
RG::TextureHandle hdrColor,
|
||||||
RG::TextureHandle bloomA, RG::TextureHandle bloomB);
|
RG::TextureHandle bloomA, RG::TextureHandle bloomB);
|
||||||
~BloomPass() override = default;
|
~BloomPass() override = default;
|
||||||
@@ -29,6 +31,7 @@ namespace Engine::Passes {
|
|||||||
private:
|
private:
|
||||||
FullscreenGpu& m_Gpu;
|
FullscreenGpu& m_Gpu;
|
||||||
PostProcessParams& m_Params;
|
PostProcessParams& m_Params;
|
||||||
|
FrameSceneData& m_Frame; // bloomEnabled 开关(帧契约)
|
||||||
RG::TextureHandle m_HDR;
|
RG::TextureHandle m_HDR;
|
||||||
RG::TextureHandle m_A; // 提取 + 垂直模糊结果
|
RG::TextureHandle m_A; // 提取 + 垂直模糊结果
|
||||||
RG::TextureHandle m_B; // 水平模糊中间结果
|
RG::TextureHandle m_B; // 水平模糊中间结果
|
||||||
|
|||||||
@@ -31,13 +31,32 @@ namespace Engine {
|
|||||||
f32 param2;
|
f32 param2;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 后处理链参数(ImGui 可调;Renderer 持有,pass 按引用读取)
|
// 后处理数值参数(ImGui 可调;Renderer 持有,pass 按引用读取)。
|
||||||
|
// 运行时开关/调试视图不在这里——统一进 FrameSceneData(帧契约)
|
||||||
struct PostProcessParams {
|
struct PostProcessParams {
|
||||||
f32 exposure = 0.6f;
|
f32 exposure = 0.6f;
|
||||||
bool useACES = true;
|
bool useACES = true;
|
||||||
f32 bloomThreshold = 1.0f; // HDR 域亮度阈值
|
f32 bloomThreshold = 1.0f; // HDR 域亮度阈值
|
||||||
f32 bloomStrength = 0.8f; // 叠加强度
|
f32 bloomStrength = 0.8f; // 叠加强度
|
||||||
bool showTemporalDiff = false; // 调试视图:显示帧历史差异
|
};
|
||||||
|
|
||||||
|
// 调试视图枚举:TonemapPass 按值把对应中间目标路由上屏。
|
||||||
|
// 与 Renderer 的 DebugViewNames 表下标一一对应
|
||||||
|
enum DebugView : int {
|
||||||
|
kDebugFinal = 0, // 正常画面(tonemap 输出)
|
||||||
|
kDebugTemporalDiff, // 帧历史差异(TAA 链路验证)
|
||||||
|
kDebugHDR, // Forward 原始 HDR(无 bloom/TAA)
|
||||||
|
kDebugTAAColor, // TAA resolve 后
|
||||||
|
kDebugBloom, // bloom 模糊结果
|
||||||
|
kDebugVelocity, // 运动向量(|uv| 增益显示)
|
||||||
|
kDebugShadowMap0, // CSM 级联 0 深度(近=白)
|
||||||
|
kDebugShadowMap1, // 级联 1
|
||||||
|
kDebugShadowMap2, // 级联 2
|
||||||
|
kDebugEnvCube, // IBL 环境 cube(经纬展开)
|
||||||
|
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
|
||||||
|
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
|
||||||
|
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
|
||||||
|
kDebugViewCount
|
||||||
};
|
};
|
||||||
|
|
||||||
class FullscreenGpu {
|
class FullscreenGpu {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
|
|
||||||
#include <Engine/RenderGraph/RenderGraph.h>
|
#include <Engine/RenderGraph/RenderGraph.h>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
class Cubemap;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Engine::Passes {
|
namespace Engine::Passes {
|
||||||
|
|
||||||
class RenderPass {
|
class RenderPass {
|
||||||
@@ -27,6 +31,10 @@ namespace Engine::Passes {
|
|||||||
|
|
||||||
virtual void Register(RG::RenderGraph& rg) = 0;
|
virtual void Register(RG::RenderGraph& rg) = 0;
|
||||||
virtual void OnTargetsRebuilt(RG::RenderGraph& rg) { (void)rg; }
|
virtual void OnTargetsRebuilt(RG::RenderGraph& rg) { (void)rg; }
|
||||||
|
|
||||||
|
// 环境资产换绑(动态切换):引用环境描述符的 pass 重绑自己的 set。
|
||||||
|
// 默认无环境影响;env 恒非空(Renderer 有中性兜底)
|
||||||
|
virtual void OnEnvironmentChanged(const Cubemap* /*env*/) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine::Passes
|
} // namespace Engine::Passes
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace Engine {
|
|||||||
SkyboxPass& operator=(const SkyboxPass&) = delete;
|
SkyboxPass& operator=(const SkyboxPass&) = delete;
|
||||||
|
|
||||||
void Register(RG::RenderGraph& rg) override;
|
void Register(RG::RenderGraph& rg) override;
|
||||||
|
void OnEnvironmentChanged(const Cubemap* env) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||||
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ namespace Engine::Passes {
|
|||||||
|
|
||||||
class TAAPass final : public RenderPass {
|
class TAAPass final : public RenderPass {
|
||||||
public:
|
public:
|
||||||
TAAPass(FullscreenGpu& gpu, f32 historyAlpha,
|
TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
|
||||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||||
RG::TextureHandle taaColor);
|
RG::TextureHandle taaColor);
|
||||||
~TAAPass() override = default;
|
~TAAPass() override = default;
|
||||||
@@ -29,7 +30,8 @@ namespace Engine::Passes {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
FullscreenGpu& m_Gpu;
|
FullscreenGpu& m_Gpu;
|
||||||
f32 m_HistoryAlpha;
|
FrameSceneData& m_Frame; // taaEnabled 开关(帧契约)
|
||||||
|
f32 m_HistoryAlpha; // 开启时的历史权重
|
||||||
RG::TextureHandle m_HDR;
|
RG::TextureHandle m_HDR;
|
||||||
RG::TextureHandle m_Velocity;
|
RG::TextureHandle m_Velocity;
|
||||||
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
|
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
|
||||||
|
|||||||
@@ -11,32 +11,54 @@
|
|||||||
|
|
||||||
#include <Engine/Renderer/Passes/RenderPass.h>
|
#include <Engine/Renderer/Passes/RenderPass.h>
|
||||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||||
|
#include <Engine/Renderer/SceneTypes.h>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine::Passes {
|
namespace Engine {
|
||||||
|
|
||||||
|
class Cubemap;
|
||||||
|
class DescriptorManager;
|
||||||
|
|
||||||
|
namespace Passes {
|
||||||
|
|
||||||
class TonemapPass final : public RenderPass {
|
class TonemapPass final : public RenderPass {
|
||||||
public:
|
public:
|
||||||
TonemapPass(FullscreenGpu& gpu, PostProcessParams& params,
|
TonemapPass(FullscreenGpu& gpu, DescriptorManager& descriptors,
|
||||||
|
PostProcessParams& params,
|
||||||
|
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 backbuffer,
|
||||||
|
RG::TextureHandle rawHdr, RG::TextureHandle velocity,
|
||||||
|
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
|
||||||
|
RG::TextureHandle shadowMap2);
|
||||||
~TonemapPass() override = default;
|
~TonemapPass() override = default;
|
||||||
|
|
||||||
void Register(RG::RenderGraph& rg) override;
|
void Register(RG::RenderGraph& rg) override;
|
||||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||||
|
void OnEnvironmentChanged(const Cubemap* env) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FullscreenGpu& m_Gpu;
|
FullscreenGpu& m_Gpu;
|
||||||
|
DescriptorManager& m_Descriptors;
|
||||||
PostProcessParams& m_Params;
|
PostProcessParams& m_Params;
|
||||||
|
FrameSceneData& m_Frame; // debugView / bloomEnabled(帧契约)
|
||||||
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_Backbuffer;
|
||||||
|
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
|
||||||
|
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
|
||||||
|
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
|
||||||
|
RG::TextureHandle m_Velocity;
|
||||||
|
RG::TextureHandle m_ShadowMaps[3];
|
||||||
|
// ---- 环境调试视图源(binding 8-11;随环境切换重绑)----
|
||||||
|
const Cubemap* m_Env = nullptr;
|
||||||
|
|
||||||
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine::Passes
|
} // namespace Passes
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
#endif //HELLOVULKAN_TONEMAPPASS_H
|
#endif //HELLOVULKAN_TONEMAPPASS_H
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ namespace Engine {
|
|||||||
class SceneRenderer;
|
class SceneRenderer;
|
||||||
class VulkanAssetUploader;
|
class VulkanAssetUploader;
|
||||||
class Cubemap;
|
class Cubemap;
|
||||||
|
class IblGenerator;
|
||||||
class ImGuiLayer;
|
class ImGuiLayer;
|
||||||
|
|
||||||
namespace ECS { class Scene; }
|
namespace ECS { class Scene; }
|
||||||
@@ -81,9 +82,25 @@ namespace Engine {
|
|||||||
void SetBloomThreshold(f32 v);
|
void SetBloomThreshold(f32 v);
|
||||||
f32 GetBloomStrength() const;
|
f32 GetBloomStrength() const;
|
||||||
void SetBloomStrength(f32 v);
|
void SetBloomStrength(f32 v);
|
||||||
// 调试视图:显示帧历史差异(TemporalDiff pass 产出)
|
|
||||||
bool GetShowTemporalDiff() const;
|
// ---- 特性开关 / 调试视图(转发到 SceneRenderer,每帧落
|
||||||
void SetShowTemporalDiff(bool b);
|
// FrameSceneData 契约,pass 只读;ImGui 直接对接)----
|
||||||
|
bool GetShadowsEnabled() const;
|
||||||
|
void SetShadowsEnabled(bool b);
|
||||||
|
bool GetTAAEnabled() const;
|
||||||
|
void SetTAAEnabled(bool b);
|
||||||
|
bool GetBloomEnabled() const;
|
||||||
|
void SetBloomEnabled(bool b);
|
||||||
|
int GetDebugView() const;
|
||||||
|
void SetDebugView(int v); // Passes::DebugView 枚举值
|
||||||
|
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
|
||||||
|
const std::vector<const char*>& GetDebugViewNames() const;
|
||||||
|
|
||||||
|
// ---- 动态环境切换(资产归 Assets 层,Renderer 只消费)----
|
||||||
|
// env 传 AssetManager::GetEnvironment(ref) 的指针;nullptr 回中性兜底。
|
||||||
|
// 内部 waitIdle 后统一换绑(引用环境描述符的 pass 全部同步);
|
||||||
|
// compute 预积分已把生成成本压到毫秒级,waitIdle 尾巴 ≈ 单帧
|
||||||
|
void SetEnvironment(const Cubemap* env);
|
||||||
|
|
||||||
// pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应)
|
// pass 级 GPU 耗时(最近完成帧,毫秒;与 GetPassGpuMs 下标对应)
|
||||||
const std::vector<std::string>& GetPassNames() const { return m_PassNames; }
|
const std::vector<std::string>& GetPassNames() const { return m_PassNames; }
|
||||||
@@ -92,6 +109,7 @@ namespace Engine {
|
|||||||
// 双视图剔除后的实例数(遥测)
|
// 双视图剔除后的实例数(遥测)
|
||||||
u32 GetMainInstanceCount() const;
|
u32 GetMainInstanceCount() const;
|
||||||
u32 GetShadowInstanceCount() const;
|
u32 GetShadowInstanceCount() const;
|
||||||
|
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
|
||||||
|
|
||||||
// 垂直同步开关(present mode 切换会重建 swapchain),
|
// 垂直同步开关(present mode 切换会重建 swapchain),
|
||||||
// 只应在帧 UI 构建段调用(Render 之前)
|
// 只应在帧 UI 构建段调用(Render 之前)
|
||||||
@@ -101,7 +119,6 @@ namespace Engine {
|
|||||||
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
|
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
|
||||||
void HandleResize();
|
void HandleResize();
|
||||||
void ReadGpuTimes(u32 frameSlot);
|
void ReadGpuTimes(u32 frameSlot);
|
||||||
void CaptureFrame(); // 调试截图(一次性,写 capture.bmp)
|
|
||||||
|
|
||||||
Window& m_Window;
|
Window& m_Window;
|
||||||
|
|
||||||
@@ -115,7 +132,9 @@ namespace Engine {
|
|||||||
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
|
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
|
||||||
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
|
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
|
||||||
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
|
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
|
||||||
// 环境贴图(IBL 阶段①):HDR → cube,被 SkyboxPass 引用;
|
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
|
||||||
|
Scope<IblGenerator> m_Ibl;
|
||||||
|
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
|
||||||
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
|
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
|
||||||
Scope<Cubemap> m_Environment;
|
Scope<Cubemap> m_Environment;
|
||||||
Scope<Swapchain> m_Swapchain;
|
Scope<Swapchain> m_Swapchain;
|
||||||
@@ -155,11 +174,6 @@ namespace Engine {
|
|||||||
|
|
||||||
vk::Extent2D m_Extent{};
|
vk::Extent2D m_Extent{};
|
||||||
u32 m_CurrentFrame = 0;
|
u32 m_CurrentFrame = 0;
|
||||||
|
|
||||||
// 调试截图(一次性):第 60 帧把 TAAColor(HDR)落成 capture.bmp
|
|
||||||
u32 m_FrameCounter = 0;
|
|
||||||
bool m_Captured = false;
|
|
||||||
MemoryAllocator::Allocation m_CaptureAlloc{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ namespace Engine {
|
|||||||
}
|
}
|
||||||
f32 GetCollectMs() const { return m_Frame.collectMs; }
|
f32 GetCollectMs() const { return m_Frame.collectMs; }
|
||||||
|
|
||||||
|
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面调用)。
|
||||||
|
// 状态唯一持有者在此,BuildFrame 每帧落入 FrameSceneData;
|
||||||
|
// TAA 开关同时控制投影抖动,同一持有者天然同步 ----
|
||||||
|
void SetShadowsEnabled(bool b) { m_ShadowsEnabled = b; }
|
||||||
|
void SetTAAEnabled(bool b) { m_TAAEnabled = b; }
|
||||||
|
void SetBloomEnabled(bool b) { m_BloomEnabled = b; }
|
||||||
|
void SetDebugView(int v) { m_DebugView = v; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
|
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
|
||||||
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
|
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
|
||||||
@@ -60,16 +68,19 @@ namespace Engine {
|
|||||||
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
|
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
|
||||||
struct ViewScratch {
|
struct ViewScratch {
|
||||||
CullResult result;
|
CullResult result;
|
||||||
std::vector<glm::mat4> worlds;
|
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
|
||||||
std::vector<glm::mat4> prevWorlds;
|
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
|
||||||
|
// 时实例分散各处),扁平化成连续数组推迟到 appendView
|
||||||
|
std::vector<std::vector<glm::mat4>> batchWorlds;
|
||||||
|
std::vector<std::vector<glm::mat4>> batchPrevWorlds;
|
||||||
std::unordered_map<Mesh*, u32> lookup;
|
std::unordered_map<Mesh*, u32> lookup;
|
||||||
|
|
||||||
void Clear()
|
void Clear()
|
||||||
{
|
{
|
||||||
result.batches.clear();
|
result.batches.clear();
|
||||||
result.instanceCount = 0;
|
result.instanceCount = 0;
|
||||||
worlds.clear();
|
batchWorlds.clear();
|
||||||
prevWorlds.clear();
|
batchPrevWorlds.clear();
|
||||||
lookup.clear();
|
lookup.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -86,6 +97,12 @@ namespace Engine {
|
|||||||
std::unordered_map<u32, glm::mat4> m_CurWorlds;
|
std::unordered_map<u32, glm::mat4> m_CurWorlds;
|
||||||
|
|
||||||
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
|
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
|
||||||
|
|
||||||
|
// ---- 运行时开关/调试视图状态(本类是唯一写入者)----
|
||||||
|
bool m_ShadowsEnabled = true;
|
||||||
|
bool m_TAAEnabled = true;
|
||||||
|
bool m_BloomEnabled = true;
|
||||||
|
int m_DebugView = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ namespace Engine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constexpr u32 kMaxLights = 4;
|
constexpr u32 kMaxLights = 4;
|
||||||
constexpr u32 kMaxInstances = 16384; // 实例 SSBO 容量(矩阵数)
|
// 实例 SSBO 容量(矩阵数)。四视图(main + 3 级联)共享同一预算:
|
||||||
|
// 需求 = Σ各视图剔除后实例数,大场景斜光时阴影实例可膨胀到主视图
|
||||||
|
// 的 2-3 倍。65536 = 4MB/槽位,覆盖 ~800 实体 × 79 mesh 全级联;
|
||||||
|
// 再往上就该上 LOD / GPU culling,而不是继续加预算
|
||||||
|
constexpr u32 kMaxInstances = 65536;
|
||||||
constexpr u32 kMaxCascades = 3; // CSM 级联数
|
constexpr u32 kMaxCascades = 3; // CSM 级联数
|
||||||
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
|
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
|
||||||
|
|
||||||
@@ -52,7 +56,8 @@ namespace Engine {
|
|||||||
glm::mat4 view;
|
glm::mat4 view;
|
||||||
glm::mat4 proj; // 抖动后的投影(光栅化用)
|
glm::mat4 proj; // 抖动后的投影(光栅化用)
|
||||||
glm::mat4 lightSpace[kMaxCascades]; // CSM 各级联光空间 VP([0,1] 重映射)
|
glm::mat4 lightSpace[kMaxCascades]; // CSM 各级联光空间 VP([0,1] 重映射)
|
||||||
glm::vec4 cascadeSplits; // xyz = 级联 0..2 远边界(视图深度,正值),w 未用
|
glm::vec4 cascadeSplits; // xyz = 级联 0..2 远边界(视图深度,正值)
|
||||||
|
// w = shadowsEnabled(1.0/0.0,frag 阴影短路用)
|
||||||
glm::vec4 ambientColor;
|
glm::vec4 ambientColor;
|
||||||
LightData lights[kMaxLights];
|
LightData lights[kMaxLights];
|
||||||
|
|
||||||
@@ -68,6 +73,9 @@ namespace Engine {
|
|||||||
struct FrameSceneData {
|
struct FrameSceneData {
|
||||||
UniformBufferObject ubo{};
|
UniformBufferObject ubo{};
|
||||||
|
|
||||||
|
glm::vec4 ClearColor{0.1, 0.1, 0.1, 1.0};
|
||||||
|
bool DrawSkybox = true;
|
||||||
|
|
||||||
// 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view)))
|
// 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view)))
|
||||||
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
|
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
|
||||||
glm::mat4 skyInvVP{1.0f};
|
glm::mat4 skyInvVP{1.0f};
|
||||||
@@ -82,6 +90,17 @@ namespace Engine {
|
|||||||
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
|
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
|
||||||
CullResult shadowCascades[kMaxCascades];
|
CullResult shadowCascades[kMaxCascades];
|
||||||
|
|
||||||
|
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面写入,
|
||||||
|
// SceneRenderer 唯一落帧,各 pass 只读;每帧确定性)----
|
||||||
|
bool shadowsEnabled = true; // ShadowPass 空转 + frag ShadowFactor 短路
|
||||||
|
bool taaEnabled = true; // TAAPass 直通(alpha=0) + 停投影抖动
|
||||||
|
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
|
||||||
|
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
|
||||||
|
|
||||||
|
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
|
||||||
|
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警
|
||||||
|
bool instancesTruncated = false;
|
||||||
|
|
||||||
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
|
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,32 @@ namespace Engine {
|
|||||||
class Device;
|
class Device;
|
||||||
class MemoryAllocator;
|
class MemoryAllocator;
|
||||||
class DescriptorManager;
|
class DescriptorManager;
|
||||||
|
class IblGenerator;
|
||||||
|
|
||||||
class VulkanAssetUploader final : public Assets::IAssetUploader {
|
class VulkanAssetUploader final : public Assets::IAssetUploader {
|
||||||
public:
|
public:
|
||||||
VulkanAssetUploader(Device& device, MemoryAllocator& allocator,
|
VulkanAssetUploader(Device& device, MemoryAllocator& allocator,
|
||||||
DescriptorManager& descriptors,
|
DescriptorManager& descriptors,
|
||||||
vk::DescriptorSetLayout textureLayout)
|
vk::DescriptorSetLayout textureLayout,
|
||||||
|
IblGenerator& ibl)
|
||||||
: m_Device(device), m_Allocator(allocator),
|
: m_Device(device), m_Allocator(allocator),
|
||||||
m_Descriptors(descriptors), m_TextureLayout(textureLayout) {}
|
m_Descriptors(descriptors), m_TextureLayout(textureLayout),
|
||||||
|
m_Ibl(ibl) {}
|
||||||
|
|
||||||
Scope<Model> LoadModel(const std::string& path) override;
|
Scope<Model> LoadModel(const std::string& path) override;
|
||||||
|
Scope<Model> LoadModel(const std::string& path,
|
||||||
|
ParsedModel&& parsed) override;
|
||||||
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
|
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
|
||||||
|
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
|
||||||
|
Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||||
|
DecodedEquirect&& decoded) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
MemoryAllocator& m_Allocator;
|
MemoryAllocator& m_Allocator;
|
||||||
DescriptorManager& m_Descriptors;
|
DescriptorManager& m_Descriptors;
|
||||||
vk::DescriptorSetLayout m_TextureLayout;
|
vk::DescriptorSetLayout m_TextureLayout;
|
||||||
|
IblGenerator& m_Ibl; // 环境 IBL 预积分的 compute 生成端
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -40,17 +40,25 @@ namespace Engine {
|
|||||||
void UpdateTexture(vk::DescriptorSet set, u32 binding,
|
void UpdateTexture(vk::DescriptorSet set, u32 binding,
|
||||||
vk::Sampler sampler, vk::ImageView view) const;
|
vk::Sampler sampler, vk::ImageView view) const;
|
||||||
|
|
||||||
|
// storage image 写入(compute 输出;layout = eGeneral)
|
||||||
|
void UpdateStorageImage(vk::DescriptorSet set, u32 binding,
|
||||||
|
vk::ImageView view) const;
|
||||||
|
|
||||||
// 单个 set layout 的旧接口(保留)
|
// 单个 set layout 的旧接口(保留)
|
||||||
vk::PipelineLayout CreatePipelineLayout(vk::DescriptorSetLayout layout,
|
vk::PipelineLayout CreatePipelineLayout(vk::DescriptorSetLayout layout,
|
||||||
u32 pushConstantSize) const;
|
u32 pushConstantSize) const;
|
||||||
|
|
||||||
// 多 set layout 的新接口。
|
// 多 set layout 的新接口。
|
||||||
// push constant range 按程序级 vertex|fragment 声明(与
|
// push constant range 按程序级声明(默认 vertex|fragment,与
|
||||||
// ShaderManager 的 shader EXT 声明一致,VUID-08878);
|
// ShaderManager 的 shader EXT 声明一致,VUID-08878);
|
||||||
// push 调用点必须带同样的双 stage(VUID-01796)
|
// push 调用点必须带同样的 stage(VUID-01796)。
|
||||||
|
// compute 管线传 vk::ShaderStageFlagBits::eCompute
|
||||||
vk::PipelineLayout CreatePipelineLayout(
|
vk::PipelineLayout CreatePipelineLayout(
|
||||||
const std::vector<vk::DescriptorSetLayout>& layouts,
|
const std::vector<vk::DescriptorSetLayout>& layouts,
|
||||||
u32 pushConstantSize) const;
|
u32 pushConstantSize,
|
||||||
|
vk::ShaderStageFlags pushStages =
|
||||||
|
vk::ShaderStageFlagBits::eVertex |
|
||||||
|
vk::ShaderStageFlagBits::eFragment) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Device& m_Device;
|
Device& m_Device;
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ namespace Engine::Assets {
|
|||||||
[this](const std::string& p) { return m_Uploader.LoadModel(p); });
|
[this](const std::string& p) { return m_Uploader.LoadModel(p); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModelRef AssetManager::LoadModel(const std::string& path,
|
||||||
|
ParsedModel&& parsed)
|
||||||
|
{
|
||||||
|
// 预解析数据移交 loader(Load 同步调用 lambda,引用捕获安全);
|
||||||
|
// 路径已缓存则 loader 不执行,parsed 自然丢弃(去重)
|
||||||
|
return m_Models.Load(path,
|
||||||
|
[this, &parsed](const std::string& p) {
|
||||||
|
return m_Uploader.LoadModel(p, std::move(parsed));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ModelRef AssetManager::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
|
ModelRef AssetManager::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
|
||||||
{
|
{
|
||||||
// 程序化资产复用同一缓存:键 = 伪路径 + 参数
|
// 程序化资产复用同一缓存:键 = 伪路径 + 参数
|
||||||
@@ -30,4 +41,22 @@ namespace Engine::Assets {
|
|||||||
[&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); });
|
[&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnvRef AssetManager::LoadEnvironment(const std::string& path)
|
||||||
|
{
|
||||||
|
// GPU 生成(compute 预积分)经接口发生在渲染层实现里
|
||||||
|
return m_Environments.Load(path,
|
||||||
|
[this](const std::string& p) { return m_Uploader.LoadEnvironment(p); });
|
||||||
|
}
|
||||||
|
|
||||||
|
EnvRef AssetManager::LoadEnvironment(const std::string& path,
|
||||||
|
DecodedEquirect&& decoded)
|
||||||
|
{
|
||||||
|
// 预解码数据移交 loader(Load 同步调用 lambda,引用捕获安全);
|
||||||
|
// 路径已缓存则 loader 不执行,decoded 自然丢弃(去重)
|
||||||
|
return m_Environments.Load(path,
|
||||||
|
[this, &decoded](const std::string& p) {
|
||||||
|
return m_Uploader.LoadEnvironment(p, std::move(decoded));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Engine::Assets
|
} // namespace Engine::Assets
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ namespace Engine {
|
|||||||
|
|
||||||
Application::Application(const std::string& title, u32 width, u32 height)
|
Application::Application(const std::string& title, u32 width, u32 height)
|
||||||
{
|
{
|
||||||
|
Logger::Initialize();
|
||||||
|
|
||||||
m_Window = CreateScope<Window>(title, width, height);
|
m_Window = CreateScope<Window>(title, width, height);
|
||||||
|
|
||||||
// 事件通道:GLFW 回调 → 本类 DispatchEvent → 派生类钩子
|
// 事件通道:GLFW 回调 → 本类 DispatchEvent → 派生类钩子
|
||||||
@@ -51,6 +53,10 @@ namespace Engine {
|
|||||||
if (m_Renderer) {
|
if (m_Renderer) {
|
||||||
m_Renderer->GetDevice().GetDevice().waitIdle();
|
m_Renderer->GetDevice().GetDevice().waitIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_ImGuiLayer) m_ImGuiLayer.reset();
|
||||||
|
|
||||||
|
Logger::Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::EnableImGui()
|
void Application::EnableImGui()
|
||||||
@@ -102,16 +108,16 @@ namespace Engine {
|
|||||||
m_Scene->FlushDestroyQueue();
|
m_Scene->FlushDestroyQueue();
|
||||||
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜
|
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜
|
||||||
|
|
||||||
const auto uiStart = std::chrono::steady_clock::now();
|
|
||||||
|
|
||||||
// ---- 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();
|
||||||
m_ImGuiLayer->NewFrame();
|
m_ImGuiLayer->NewFrame();
|
||||||
OnRenderOverlay();
|
OnRenderOverlay();
|
||||||
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 ----
|
||||||
|
|||||||
@@ -1,3 +1,51 @@
|
|||||||
//
|
//
|
||||||
// Created by atdunbg on 2026/9/20.
|
// Created by atdunbg on 2026/9/20.
|
||||||
//
|
//
|
||||||
|
#include <Engine/Core/Log.h>
|
||||||
|
|
||||||
|
#include "spdlog/sinks/basic_file_sink.h"
|
||||||
|
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||||
|
|
||||||
|
namespace Engine::Logger
|
||||||
|
{
|
||||||
|
static bool IsInited = false;
|
||||||
|
|
||||||
|
void Initialize()
|
||||||
|
{
|
||||||
|
if (IsInited)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("spdlog already inited");
|
||||||
|
}
|
||||||
|
IsInited = true;
|
||||||
|
|
||||||
|
// 控制台彩色 sink (多线程安全)
|
||||||
|
auto console = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
|
||||||
|
console->set_pattern("%^[%T.%e] [%l] [%n] %v%$");
|
||||||
|
|
||||||
|
// 文件
|
||||||
|
auto file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("HelloVulkan.log", true);
|
||||||
|
file->set_pattern("[%Y-%m-%d %T.%e] [%l] [%t] %v");
|
||||||
|
|
||||||
|
// 多sink 组合默认 logger
|
||||||
|
|
||||||
|
// auto logger = std::make_shared<spdlog::logger>("hello", spdlog::sinks_init_list{console, file});
|
||||||
|
auto logger = std::make_shared<spdlog::logger>("APP", spdlog::sinks_init_list{console, file});
|
||||||
|
logger->set_level(spdlog::level::trace);
|
||||||
|
logger->flush_on(spdlog::level::warn); // warn 及以上立即 flush,避免崩溃丢日志
|
||||||
|
|
||||||
|
spdlog::set_default_logger(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Shutdown()
|
||||||
|
{
|
||||||
|
if (!IsInited)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("spdlog already shutdown or not init");
|
||||||
|
}
|
||||||
|
IsInited = false;
|
||||||
|
|
||||||
|
spdlog::shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -694,24 +694,6 @@ namespace Engine::RG {
|
|||||||
return entry.physical[frameSlot]->view;
|
return entry.physical[frameSlot]->view;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RenderGraph::GetCaptureTarget(TextureHandle h, u32 frameSlot,
|
|
||||||
vk::Image& outImage, vk::Extent2D& outExtent,
|
|
||||||
vk::Format& outFormat,
|
|
||||||
vk::ImageLayout& outResting) const
|
|
||||||
{
|
|
||||||
const auto& entry = GetEntry(h);
|
|
||||||
if (entry.imported || frameSlot >= entry.physical.size() ||
|
|
||||||
!entry.physical[frameSlot]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const auto& phys = *entry.physical[frameSlot];
|
|
||||||
outImage = phys.image;
|
|
||||||
outExtent = phys.extent;
|
|
||||||
outFormat = phys.format;
|
|
||||||
outResting = entry.restingLayout;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderGraph::SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame)
|
void RenderGraph::SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame)
|
||||||
{
|
{
|
||||||
m_QueryPool = pool;
|
m_QueryPool = pool;
|
||||||
|
|||||||
@@ -1,753 +1,17 @@
|
|||||||
//
|
//
|
||||||
// Created by atdunbg on 2026/9/23.
|
// Created by atdunbg on 2026/9/23.
|
||||||
//
|
//
|
||||||
// Cubemap 实现:HDR 等距柱状 → CPU 预积分四件套 → fp16 上传。
|
// Cubemap 实现:纯资源持有(构造注入 + 成对析构)。
|
||||||
//
|
// 预积分生成见 IblGenerator(compute 链);CPU 积分路径已整体移除,
|
||||||
// 一次性加载成本(秒级 CPU),换取运行时零积分开销;硬件 Cube 采样
|
// 环境切换从秒级阻塞降到毫秒级 GPU 提交。
|
||||||
//(filtering/边缘接缝由 driver 处理)。split-sum 预积分(LearnOpenGL/
|
|
||||||
// UE 拆分方案的离线端):
|
|
||||||
// irradiance —— 半球 Lambert 积分,漫反射 ambient
|
|
||||||
// prefilter —— GGX 重要性采样(Hammersley 序列),mip 分级 roughness
|
|
||||||
// BRDF LUT —— (NdotV, roughness) → (F 缩放, F 偏置),纯数学表
|
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <Engine/Renderer/Cubemap.h>
|
#include <Engine/Renderer/Cubemap.h>
|
||||||
#include <Engine/Vulkan/Device.h>
|
#include <Engine/Vulkan/Device.h>
|
||||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||||
#include <Engine/Core/Log.h>
|
|
||||||
|
|
||||||
#include <stb/stb_image.h>
|
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <glm/gtc/constants.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <bit>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstring>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace Engine
|
namespace Engine
|
||||||
{
|
{
|
||||||
namespace
|
|
||||||
{
|
|
||||||
constexpr u32 kFaceCount = 6;
|
|
||||||
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
|
|
||||||
constexpr u32 kPhiSteps = 32; // 方位角采样数(512 采样/像素)
|
|
||||||
constexpr u32 kThetaSteps = 16; // 极角采样数
|
|
||||||
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0)
|
|
||||||
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
|
|
||||||
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
|
|
||||||
constexpr u32 kBrdfLutSamples = 128; // LUT 每像素采样数
|
|
||||||
|
|
||||||
// ---- GL/Vulkan cube 面方向表(与 GL spec 立方体选择表互逆)----
|
|
||||||
// (u,v)∈[0,1](u 向右 v 向下)→ s,t∈[-1,1] → 单位方向
|
|
||||||
glm::vec3 FaceDir(u32 face, f32 u, f32 v)
|
|
||||||
{
|
|
||||||
const f32 s = u * 2.0f - 1.0f;
|
|
||||||
const f32 t = v * 2.0f - 1.0f;
|
|
||||||
glm::vec3 dir;
|
|
||||||
switch (face) {
|
|
||||||
case 0: dir = { 1.0f, -t, -s }; break; // +X(面片顶行 = +Y)
|
|
||||||
case 1: dir = {-1.0f, -t, s }; break; // -X
|
|
||||||
case 2: dir = { s, 1.0f, t }; break; // +Y
|
|
||||||
case 3: dir = { s,-1.0f, -t }; break; // -Y
|
|
||||||
case 4: dir = { s, -t, 1.0f }; break; // +Z
|
|
||||||
default: dir = {-s, -t,-1.0f }; break; // -Z
|
|
||||||
}
|
|
||||||
return glm::normalize(dir); // equirect 查表要求单位方向
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等距柱状双线性采样(x 环绕跨经度接缝,y 钳制到极点)。
|
|
||||||
// 图像行 0 = 天顶(+Y):v = acos(y)/π,与 stbi 行序一致,无需翻转
|
|
||||||
glm::vec3 SampleEquirect(const f32* img, u32 w, u32 h, const glm::vec3& dir)
|
|
||||||
{
|
|
||||||
const f32 u = std::atan2(dir.z, dir.x) * (0.5f / glm::pi<f32>()) + 0.5f;
|
|
||||||
const f32 v = std::acos(glm::clamp(dir.y, -1.0f, 1.0f)) / glm::pi<f32>();
|
|
||||||
|
|
||||||
const f32 fx = u * static_cast<f32>(w) - 0.5f;
|
|
||||||
const f32 fy = v * static_cast<f32>(h) - 0.5f;
|
|
||||||
const u32 x0 = static_cast<u32>(std::floor(fx));
|
|
||||||
const u32 y0 = static_cast<u32>(std::floor(fy));
|
|
||||||
const f32 tx = fx - std::floor(fx);
|
|
||||||
const f32 ty = fy - std::floor(fy);
|
|
||||||
|
|
||||||
auto fetch = [&](u32 x, u32 y) {
|
|
||||||
const u32 xc = ((x % w) + w) % w; // 经度环绕
|
|
||||||
const u32 yc = glm::clamp(y, 0u, h - 1u); // 纬度钳制
|
|
||||||
const size_t off = (static_cast<size_t>(yc) * w + xc) * 4;
|
|
||||||
return glm::vec3(img[off + 0], img[off + 1], img[off + 2]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const glm::vec3 c00 = fetch(x0, y0);
|
|
||||||
const glm::vec3 c10 = fetch(x0 + 1, y0);
|
|
||||||
const glm::vec3 c01 = fetch(x0, y0 + 1);
|
|
||||||
const glm::vec3 c11 = fetch(x0 + 1, y0 + 1);
|
|
||||||
|
|
||||||
return glm::mix(glm::mix(c00, c10, tx), glm::mix(c01, c11, tx), ty);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 漫反射辐照度半球积分(Lambert):E = ∫ L·cosθ·sinθ dθ dφ。
|
|
||||||
// 切线基 up×N 构造;均匀网格 ≈2048 采样/像素,对 32² 低频图足够
|
|
||||||
glm::vec3 ConvolveIrradiance(const f32* img, u32 w, u32 h,
|
|
||||||
const glm::vec3& N)
|
|
||||||
{
|
|
||||||
const glm::vec3 up = glm::abs(N.y) > 0.999f
|
|
||||||
? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
|
|
||||||
const glm::vec3 T = glm::normalize(glm::cross(up, N));
|
|
||||||
const glm::vec3 B = glm::cross(N, T);
|
|
||||||
|
|
||||||
const f32 dPhi = 2.0f * glm::pi<f32>() / static_cast<f32>(kPhiSteps);
|
|
||||||
const f32 dTheta = 0.5f * glm::pi<f32>() / static_cast<f32>(kThetaSteps);
|
|
||||||
|
|
||||||
glm::vec3 irradiance(0.0f);
|
|
||||||
for (u32 p = 0; p < kPhiSteps; ++p) {
|
|
||||||
const f32 phi = (static_cast<f32>(p) + 0.5f) * dPhi;
|
|
||||||
for (u32 t = 0; t < kThetaSteps; ++t) {
|
|
||||||
const f32 theta = (static_cast<f32>(t) + 0.5f) * dTheta;
|
|
||||||
const f32 sinT = std::sin(theta);
|
|
||||||
const f32 cosT = std::cos(theta);
|
|
||||||
const glm::vec3 sampleDir =
|
|
||||||
T * (sinT * std::cos(phi)) +
|
|
||||||
B * (sinT * std::sin(phi)) +
|
|
||||||
N * cosT;
|
|
||||||
irradiance += SampleEquirect(img, w, h, sampleDir) *
|
|
||||||
(cosT * sinT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return irradiance * (dPhi * dTheta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- GGX 重要性采样(split-sum 镜面端)----
|
|
||||||
|
|
||||||
// van der Corput 基底反转(位反转技巧)。
|
|
||||||
// 注意是整数值转浮点(static_cast),不是位型重解读——
|
|
||||||
// bit_cast 会把反转后的整数当 IEEE754 解读,序列完全错乱
|
|
||||||
f32 RadicalInverseVdC(u32 bits)
|
|
||||||
{
|
|
||||||
bits = (bits << 16u) | (bits >> 16u);
|
|
||||||
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
|
|
||||||
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
|
|
||||||
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
|
|
||||||
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
|
|
||||||
return static_cast<f32>(bits) * 2.3283064365386963e-10f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// n = 当次积分总采样数(xi.x = i/n 必须覆盖 [0,1] 全程)
|
|
||||||
glm::vec2 Hammersley(u32 i, u32 n)
|
|
||||||
{
|
|
||||||
return { static_cast<f32>(i) / static_cast<f32>(n),
|
|
||||||
RadicalInverseVdC(i) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// GGX NDF 按概率密度取半程向量 H(切线空间 → 世界)
|
|
||||||
glm::vec3 ImportanceSampleGGX(const glm::vec2& xi, const glm::vec3& N,
|
|
||||||
f32 roughness)
|
|
||||||
{
|
|
||||||
const f32 a = roughness * roughness;
|
|
||||||
const f32 phi = 2.0f * glm::pi<f32>() * xi.x;
|
|
||||||
const f32 cosTheta =
|
|
||||||
std::sqrt((1.0f - xi.y) / (1.0f + (a * a - 1.0f) * xi.y));
|
|
||||||
const f32 sinTheta = std::sqrt(1.0f - cosTheta * cosTheta);
|
|
||||||
|
|
||||||
const glm::vec3 H(sinTheta * std::cos(phi),
|
|
||||||
sinTheta * std::sin(phi), cosTheta);
|
|
||||||
|
|
||||||
const glm::vec3 up = glm::abs(N.z) > 0.999f
|
|
||||||
? glm::vec3(1.0f, 0.0f, 0.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
|
|
||||||
const glm::vec3 T = glm::normalize(glm::cross(up, N));
|
|
||||||
const glm::vec3 B = glm::cross(N, T);
|
|
||||||
return glm::normalize(T * H.x + B * H.y + N * H.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pre-filter 卷积:R(N) ≈ Σ L·NdotL / Σ NdotL(GGX 加权镜面均值)
|
|
||||||
glm::vec3 Prefilter(const f32* img, u32 w, u32 h,
|
|
||||||
const glm::vec3& N, f32 roughness, u32 samples)
|
|
||||||
{
|
|
||||||
const glm::vec3 V = N; // split-sum 约定:V 沿 N
|
|
||||||
glm::vec3 acc(0.0f);
|
|
||||||
f32 weight = 0.0f;
|
|
||||||
for (u32 i = 0; i < samples; ++i) {
|
|
||||||
const glm::vec3 H = ImportanceSampleGGX(
|
|
||||||
Hammersley(i, samples), N, roughness);
|
|
||||||
const glm::vec3 L = glm::normalize(2.0f * glm::dot(V, H) * H - V);
|
|
||||||
const f32 nDotL = glm::max(glm::dot(N, L), 0.0f);
|
|
||||||
if (nDotL > 0.0f) {
|
|
||||||
acc += SampleEquirect(img, w, h, L) * nDotL;
|
|
||||||
weight += nDotL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return weight > 0.0f ? acc / weight : glm::vec3(0.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// BRDF LUT 积分:(NdotV, roughness) → (A, B)。
|
|
||||||
// A/B 是 Fresnel 的缩放/偏置:specular = prefiltered·(F·A + B)
|
|
||||||
glm::vec2 IntegrateBrdf(f32 nDotV, f32 roughness)
|
|
||||||
{
|
|
||||||
// V 在以 N 为 +Z 的切线空间里(积分只依赖这两角度)
|
|
||||||
const glm::vec3 V(std::sqrt(1.0f - nDotV * nDotV), 0.0f, nDotV);
|
|
||||||
|
|
||||||
f32 a = 0.0f, b = 0.0f;
|
|
||||||
const f32 k = roughness * roughness * 0.5f; // IBL 版 Smith k
|
|
||||||
|
|
||||||
for (u32 i = 0; i < kBrdfLutSamples; ++i) {
|
|
||||||
const glm::vec2 xi = Hammersley(i, kBrdfLutSamples);
|
|
||||||
const glm::vec3 H = ImportanceSampleGGX(xi, glm::vec3(0, 0, 1),
|
|
||||||
roughness);
|
|
||||||
const glm::vec3 L = glm::normalize(
|
|
||||||
2.0f * glm::dot(V, H) * H - V);
|
|
||||||
|
|
||||||
const f32 nDotL = glm::max(L.z, 0.0f);
|
|
||||||
const f32 nDotH = glm::max(H.z, 0.0f);
|
|
||||||
const f32 vDotH = glm::max(glm::dot(V, H), 0.0f);
|
|
||||||
if (nDotL <= 0.0f) continue;
|
|
||||||
|
|
||||||
const f32 g = (nDotV / (nDotV * (1.0f - k) + k)) *
|
|
||||||
(nDotL / (nDotL * (1.0f - k) + k));
|
|
||||||
// 高 roughness 时部分样本 H.z 恰为 0(g 也为 0),
|
|
||||||
// 0/0 = NaN 会累进 LUT——分母钳 epsilon
|
|
||||||
const f32 gVis = g * vDotH /
|
|
||||||
glm::max(nDotH * nDotV, 1e-6f);
|
|
||||||
const f32 fc = 1.0f - vDotH;
|
|
||||||
const f32 f = fc * fc * fc * fc * fc; // (1-vDotH)⁵,乘法链
|
|
||||||
|
|
||||||
a += (1.0f - f) * gVis;
|
|
||||||
b += f * gVis;
|
|
||||||
}
|
|
||||||
return { a / static_cast<f32>(kBrdfLutSamples),
|
|
||||||
b / static_cast<f32>(kBrdfLutSamples) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// IEEE754 f32 → fp16(round-to-nearest-even;溢出饱和为 Inf)
|
|
||||||
u16 FloatToHalf(f32 value)
|
|
||||||
{
|
|
||||||
const u32 x = std::bit_cast<u32>(value);
|
|
||||||
const u16 sign = static_cast<u16>((x >> 16) & 0x8000u);
|
|
||||||
const u32 absVal = x & 0x7fffffffu;
|
|
||||||
|
|
||||||
if (absVal > 0x7f800000u) {
|
|
||||||
return static_cast<u16>(sign | 0x7e00u); // NaN
|
|
||||||
}
|
|
||||||
if (absVal >= 0x47800000u) {
|
|
||||||
return static_cast<u16>(sign | 0x7c00u); // Inf / 溢出饱和
|
|
||||||
}
|
|
||||||
if (absVal >= 0x38800000u) {
|
|
||||||
const u32 bits = absVal - 0x38000000u;
|
|
||||||
const u32 round = 0x0fffu + ((bits >> 13) & 1u);
|
|
||||||
return static_cast<u16>(sign | ((bits + round) >> 13));
|
|
||||||
}
|
|
||||||
if (absVal < 0x33000000u) {
|
|
||||||
return sign; // < 2^-25:舍入为 0
|
|
||||||
}
|
|
||||||
const u32 mantissa = (absVal & 0x007fffffu) | 0x00800000u;
|
|
||||||
const u32 shift = 126u - (absVal >> 23u);
|
|
||||||
const u32 m = (mantissa + (1u << (shift - 1))) >> shift;
|
|
||||||
return static_cast<u16>(sign | std::min(m, 0x400u));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 上传 ----
|
|
||||||
|
|
||||||
vk::ImageView CreateCubeView(Device& device, vk::Image image, u32 mips)
|
|
||||||
{
|
|
||||||
return device.GetDevice().createImageView(
|
|
||||||
vk::ImageViewCreateInfo{}
|
|
||||||
.setImage(image)
|
|
||||||
.setViewType(vk::ImageViewType::eCube)
|
|
||||||
.setFormat(vk::Format::eR16G16B16A16Sfloat)
|
|
||||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, mips, 0, kFaceCount}));
|
|
||||||
}
|
|
||||||
|
|
||||||
vk::ImageView CreateLutView(Device& device, vk::Image image)
|
|
||||||
{
|
|
||||||
return device.GetDevice().createImageView(
|
|
||||||
vk::ImageViewCreateInfo{}
|
|
||||||
.setImage(image)
|
|
||||||
.setViewType(vk::ImageViewType::e2D)
|
|
||||||
.setFormat(vk::Format::eR16G16Sfloat)
|
|
||||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, 1, 0, 1}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// cube mip 链数据:[mip0: 6 面][mip1: 6 面]...,每面 faceSize²×8B
|
|
||||||
struct CubeMipData {
|
|
||||||
const u16* data; // 6 面连续
|
|
||||||
u32 faceSize;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传 cube(含 mip 链)为 eCubeCompatible image(fp16),
|
|
||||||
// 返回完整资源单元(image + view + 显存分配)
|
|
||||||
EnvTexture UploadCube(Device& device, MemoryAllocator& allocator,
|
|
||||||
const std::vector<CubeMipData>& mips,
|
|
||||||
u32 viewMipCount)
|
|
||||||
{
|
|
||||||
const u32 mipCount = static_cast<u32>(mips.size());
|
|
||||||
|
|
||||||
// staging:各 mip 紧凑排布,记偏移
|
|
||||||
vk::DeviceSize total = 0;
|
|
||||||
for (const auto& mip : mips) {
|
|
||||||
total += static_cast<vk::DeviceSize>(mip.faceSize) *
|
|
||||||
mip.faceSize * 8 * kFaceCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto stagingAlloc = allocator.CreateBuffer(
|
|
||||||
total,
|
|
||||||
vk::BufferUsageFlagBits::eTransferSrc,
|
|
||||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
|
||||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
|
||||||
{
|
|
||||||
auto* dst = static_cast<u8*>(stagingAlloc.MappedPtr);
|
|
||||||
vk::DeviceSize off = 0;
|
|
||||||
for (const auto& mip : mips) {
|
|
||||||
const vk::DeviceSize bytes =
|
|
||||||
static_cast<vk::DeviceSize>(mip.faceSize) *
|
|
||||||
mip.faceSize * 8 * kFaceCount;
|
|
||||||
std::memcpy(dst + off, mip.data,
|
|
||||||
static_cast<size_t>(bytes));
|
|
||||||
off += bytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const u32 baseSize = mips[0].faceSize;
|
|
||||||
vk::ImageCreateInfo imageInfo;
|
|
||||||
imageInfo.setImageType(vk::ImageType::e2D)
|
|
||||||
.setFormat(vk::Format::eR16G16B16A16Sfloat)
|
|
||||||
.setExtent({baseSize, baseSize, 1})
|
|
||||||
.setMipLevels(mipCount)
|
|
||||||
.setArrayLayers(kFaceCount)
|
|
||||||
.setSamples(vk::SampleCountFlagBits::e1)
|
|
||||||
.setTiling(vk::ImageTiling::eOptimal)
|
|
||||||
.setFlags(vk::ImageCreateFlagBits::eCubeCompatible)
|
|
||||||
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
|
|
||||||
vk::ImageUsageFlagBits::eSampled)
|
|
||||||
.setSharingMode(vk::SharingMode::eExclusive)
|
|
||||||
.setInitialLayout(vk::ImageLayout::eUndefined);
|
|
||||||
|
|
||||||
auto imageAlloc = allocator.CreateImage(imageInfo,
|
|
||||||
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
|
||||||
const vk::Image image = imageAlloc.Image;
|
|
||||||
|
|
||||||
vk::CommandPoolCreateInfo poolInfo;
|
|
||||||
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
|
|
||||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
|
||||||
vk::CommandPool pool = device.GetDevice().createCommandPool(poolInfo);
|
|
||||||
|
|
||||||
vk::CommandBuffer cmd = device.GetDevice().allocateCommandBuffers(
|
|
||||||
vk::CommandBufferAllocateInfo{}
|
|
||||||
.setCommandPool(pool)
|
|
||||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
|
||||||
.setCommandBufferCount(1))[0];
|
|
||||||
|
|
||||||
cmd.begin(vk::CommandBufferBeginInfo{
|
|
||||||
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
|
||||||
|
|
||||||
// 全 mip × 全层一起转 layout
|
|
||||||
vk::ImageMemoryBarrier2 toTransfer;
|
|
||||||
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
|
||||||
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
|
||||||
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
|
||||||
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
|
||||||
.setOldLayout(vk::ImageLayout::eUndefined)
|
|
||||||
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
|
|
||||||
.setImage(image)
|
|
||||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, mipCount, 0, kFaceCount});
|
|
||||||
cmd.pipelineBarrier2(
|
|
||||||
vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
|
|
||||||
|
|
||||||
// 逐 mip 逐面拷贝;buffer 偏移必须按 mips[0..mip) 字节数累积
|
|
||||||
//(mip 面尺寸递减,用当前 dim 乘 mip 序号会错位 → 读到
|
|
||||||
// staging 未初始化数据 → fp16 NaN 污染整屏)
|
|
||||||
vk::DeviceSize offset = 0;
|
|
||||||
for (u32 mip = 0; mip < mipCount; ++mip) {
|
|
||||||
const u32 dim = mips[mip].faceSize;
|
|
||||||
for (u32 f = 0; f < kFaceCount; ++f) {
|
|
||||||
vk::BufferImageCopy region;
|
|
||||||
region.setBufferOffset(
|
|
||||||
offset + static_cast<vk::DeviceSize>(dim) *
|
|
||||||
dim * 8 * f)
|
|
||||||
.setBufferRowLength(0)
|
|
||||||
.setBufferImageHeight(0)
|
|
||||||
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
|
|
||||||
mip, f, 1})
|
|
||||||
.setImageOffset({0, 0, 0})
|
|
||||||
.setImageExtent({dim, dim, 1});
|
|
||||||
cmd.copyBufferToImage(stagingAlloc.Buffer, image,
|
|
||||||
vk::ImageLayout::eTransferDstOptimal,
|
|
||||||
region);
|
|
||||||
}
|
|
||||||
offset += static_cast<vk::DeviceSize>(dim) * dim * 8 *
|
|
||||||
kFaceCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
vk::ImageMemoryBarrier2 toShader;
|
|
||||||
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
|
||||||
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
|
||||||
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
|
|
||||||
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
|
||||||
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
|
|
||||||
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
|
||||||
.setImage(image)
|
|
||||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, mipCount, 0, kFaceCount});
|
|
||||||
cmd.pipelineBarrier2(
|
|
||||||
vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
|
|
||||||
|
|
||||||
cmd.end();
|
|
||||||
|
|
||||||
vk::Fence fence = device.GetDevice().createFence({});
|
|
||||||
device.GetGraphicsQueue().submit(
|
|
||||||
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
|
|
||||||
if (device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
|
|
||||||
vk::Result::eSuccess) {
|
|
||||||
ENGINE_ERROR("Cubemap upload: waitForFences failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
device.GetDevice().destroyFence(fence);
|
|
||||||
device.GetDevice().destroyCommandPool(pool);
|
|
||||||
allocator.Destroy(stagingAlloc);
|
|
||||||
|
|
||||||
return {imageAlloc.Image,
|
|
||||||
CreateCubeView(device, imageAlloc.Image, viewMipCount),
|
|
||||||
imageAlloc};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上传 BRDF LUT(2D,R16G16Sfloat,8B/px),返回完整资源单元
|
|
||||||
EnvTexture UploadTexture2D(Device& device,
|
|
||||||
MemoryAllocator& allocator,
|
|
||||||
const std::vector<u16>& texels,
|
|
||||||
u32 size, vk::Format format)
|
|
||||||
{
|
|
||||||
const vk::DeviceSize total =
|
|
||||||
static_cast<vk::DeviceSize>(size) * size * 8;
|
|
||||||
|
|
||||||
auto stagingAlloc = allocator.CreateBuffer(
|
|
||||||
total,
|
|
||||||
vk::BufferUsageFlagBits::eTransferSrc,
|
|
||||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
|
||||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
|
||||||
std::memcpy(stagingAlloc.MappedPtr, texels.data(),
|
|
||||||
static_cast<size_t>(total));
|
|
||||||
|
|
||||||
vk::ImageCreateInfo imageInfo;
|
|
||||||
imageInfo.setImageType(vk::ImageType::e2D)
|
|
||||||
.setFormat(format)
|
|
||||||
.setExtent({size, size, 1})
|
|
||||||
.setMipLevels(1)
|
|
||||||
.setArrayLayers(1)
|
|
||||||
.setSamples(vk::SampleCountFlagBits::e1)
|
|
||||||
.setTiling(vk::ImageTiling::eOptimal)
|
|
||||||
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
|
|
||||||
vk::ImageUsageFlagBits::eSampled)
|
|
||||||
.setSharingMode(vk::SharingMode::eExclusive)
|
|
||||||
.setInitialLayout(vk::ImageLayout::eUndefined);
|
|
||||||
|
|
||||||
auto imageAlloc = allocator.CreateImage(imageInfo,
|
|
||||||
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
|
||||||
const vk::Image image = imageAlloc.Image;
|
|
||||||
|
|
||||||
vk::CommandPoolCreateInfo poolInfo;
|
|
||||||
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
|
|
||||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
|
||||||
vk::CommandPool pool = device.GetDevice().createCommandPool(poolInfo);
|
|
||||||
|
|
||||||
vk::CommandBuffer cmd = device.GetDevice().allocateCommandBuffers(
|
|
||||||
vk::CommandBufferAllocateInfo{}
|
|
||||||
.setCommandPool(pool)
|
|
||||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
|
||||||
.setCommandBufferCount(1))[0];
|
|
||||||
|
|
||||||
cmd.begin(vk::CommandBufferBeginInfo{
|
|
||||||
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
|
||||||
|
|
||||||
vk::ImageMemoryBarrier2 toTransfer;
|
|
||||||
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
|
||||||
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
|
||||||
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
|
||||||
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
|
||||||
.setOldLayout(vk::ImageLayout::eUndefined)
|
|
||||||
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
|
|
||||||
.setImage(image)
|
|
||||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, 1, 0, 1});
|
|
||||||
cmd.pipelineBarrier2(
|
|
||||||
vk::DependencyInfo{}.setImageMemoryBarriers(toTransfer));
|
|
||||||
|
|
||||||
vk::BufferImageCopy region;
|
|
||||||
region.setBufferOffset(0)
|
|
||||||
.setBufferRowLength(0)
|
|
||||||
.setBufferImageHeight(0)
|
|
||||||
.setImageSubresource({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, 0, 1})
|
|
||||||
.setImageOffset({0, 0, 0})
|
|
||||||
.setImageExtent({size, size, 1});
|
|
||||||
cmd.copyBufferToImage(stagingAlloc.Buffer, image,
|
|
||||||
vk::ImageLayout::eTransferDstOptimal, region);
|
|
||||||
|
|
||||||
vk::ImageMemoryBarrier2 toShader;
|
|
||||||
toShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
|
||||||
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
|
||||||
.setDstStageMask(vk::PipelineStageFlagBits2::eFragmentShader)
|
|
||||||
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
|
||||||
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
|
|
||||||
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
|
||||||
.setImage(image)
|
|
||||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, 1, 0, 1});
|
|
||||||
cmd.pipelineBarrier2(
|
|
||||||
vk::DependencyInfo{}.setImageMemoryBarriers(toShader));
|
|
||||||
|
|
||||||
cmd.end();
|
|
||||||
|
|
||||||
vk::Fence fence = device.GetDevice().createFence({});
|
|
||||||
device.GetGraphicsQueue().submit(
|
|
||||||
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
|
|
||||||
if (device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
|
|
||||||
vk::Result::eSuccess) {
|
|
||||||
ENGINE_ERROR("BRDF LUT upload: waitForFences failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
device.GetDevice().destroyFence(fence);
|
|
||||||
device.GetDevice().destroyCommandPool(pool);
|
|
||||||
allocator.Destroy(stagingAlloc);
|
|
||||||
|
|
||||||
return {imageAlloc.Image,
|
|
||||||
CreateLutView(device, imageAlloc.Image), imageAlloc};
|
|
||||||
}
|
|
||||||
|
|
||||||
// fp16 NaN 自检(指数全 1 且尾数非 0):预积分数据上传前的
|
|
||||||
// 最后一道防线——NaN 进 GPU 会污染整屏输出
|
|
||||||
bool ContainsNaNHalf(const u16* data, size_t count)
|
|
||||||
{
|
|
||||||
for (size_t i = 0; i < count; ++i) {
|
|
||||||
const u32 abs = data[i] & 0x7FFFu;
|
|
||||||
if (abs > 0x7C00u) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
vk::Sampler CreateEnvSampler(Device& device, u32 maxLod)
|
|
||||||
{
|
|
||||||
vk::SamplerCreateInfo samplerInfo;
|
|
||||||
samplerInfo.setMagFilter(vk::Filter::eLinear)
|
|
||||||
.setMinFilter(vk::Filter::eLinear)
|
|
||||||
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
|
|
||||||
.setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
|
|
||||||
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
|
|
||||||
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge)
|
|
||||||
.setAnisotropyEnable(VK_FALSE)
|
|
||||||
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
|
|
||||||
.setUnnormalizedCoordinates(VK_FALSE)
|
|
||||||
.setCompareEnable(VK_FALSE)
|
|
||||||
.setMinLod(0.0f)
|
|
||||||
.setMaxLod(static_cast<f32>(maxLod));
|
|
||||||
return device.GetDevice().createSampler(samplerInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// BRDF LUT 专用:clamp-to-edge(共享 border 白采样器会在 uv=1.0
|
|
||||||
// 处混入白色 border,高 roughness 镜面偏大)
|
|
||||||
vk::Sampler CreateLutSampler(Device& device)
|
|
||||||
{
|
|
||||||
return CreateEnvSampler(device, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scope<Cubemap> Cubemap::CreateFromHdrEquirect(Device& device,
|
|
||||||
MemoryAllocator& allocator,
|
|
||||||
const std::string& path,
|
|
||||||
u32 faceSize)
|
|
||||||
{
|
|
||||||
// ---- 1. CPU 加载 HDR(float RGBA)----
|
|
||||||
int w = 0, h = 0, channels = 0;
|
|
||||||
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
|
|
||||||
if (!pixels) {
|
|
||||||
ENGINE_ERROR("Cubemap: failed to load HDR: {}", path);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
const u32 srcW = static_cast<u32>(w);
|
|
||||||
const u32 srcH = static_cast<u32>(h);
|
|
||||||
|
|
||||||
// ---- 2a. 环境 cube:重采样 6 面 ----
|
|
||||||
const u32 facePixels = faceSize * faceSize;
|
|
||||||
std::vector<u16> envFaces(kFaceCount * facePixels * 4);
|
|
||||||
for (u32 f = 0; f < kFaceCount; ++f) {
|
|
||||||
u16* dst = envFaces.data() + f * facePixels * 4;
|
|
||||||
for (u32 y = 0; y < faceSize; ++y) {
|
|
||||||
for (u32 x = 0; x < faceSize; ++x) {
|
|
||||||
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(faceSize);
|
|
||||||
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(faceSize);
|
|
||||||
const glm::vec3 rgb =
|
|
||||||
SampleEquirect(pixels, srcW, srcH, FaceDir(f, u, v));
|
|
||||||
u16* px = dst + (static_cast<size_t>(y) * faceSize + x) * 4;
|
|
||||||
px[0] = FloatToHalf(rgb.r);
|
|
||||||
px[1] = FloatToHalf(rgb.g);
|
|
||||||
px[2] = FloatToHalf(rgb.b);
|
|
||||||
px[3] = 0x3C00; // 1.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 2b. irradiance cube:半球 Lambert 积分 ----
|
|
||||||
const u32 irrSize = kIrradianceFaceSize;
|
|
||||||
const u32 irrPixels = irrSize * irrSize;
|
|
||||||
std::vector<u16> irrFaces(kFaceCount * irrPixels * 4);
|
|
||||||
for (u32 f = 0; f < kFaceCount; ++f) {
|
|
||||||
u16* dst = irrFaces.data() + f * irrPixels * 4;
|
|
||||||
for (u32 y = 0; y < irrSize; ++y) {
|
|
||||||
for (u32 x = 0; x < irrSize; ++x) {
|
|
||||||
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(irrSize);
|
|
||||||
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(irrSize);
|
|
||||||
const glm::vec3 rgb = ConvolveIrradiance(
|
|
||||||
pixels, srcW, srcH, FaceDir(f, u, v));
|
|
||||||
u16* px = dst + (static_cast<size_t>(y) * irrSize + x) * 4;
|
|
||||||
px[0] = FloatToHalf(rgb.r);
|
|
||||||
px[1] = FloatToHalf(rgb.g);
|
|
||||||
px[2] = FloatToHalf(rgb.b);
|
|
||||||
px[3] = 0x3C00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 2c. pre-filter cube:GGX 重要性采样 × mip 分级 roughness ----
|
|
||||||
// 采样数随 mip 递增(高 roughness 分布更宽需要更多样本)。
|
|
||||||
// 注意:必须一次性分配总容量——逐 mip resize 会 realloc,使
|
|
||||||
// prefMips 里已记录的 data 指针悬空(上传读到垃圾 = NaN 全黑)
|
|
||||||
u32 prefTotal = 0;
|
|
||||||
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
|
|
||||||
const u32 dim = kPrefilterFaceSize >> mip;
|
|
||||||
prefTotal += kFaceCount * dim * dim * 4;
|
|
||||||
}
|
|
||||||
std::vector<u16> prefFaces(prefTotal);
|
|
||||||
std::vector<CubeMipData> prefMips;
|
|
||||||
prefMips.reserve(kPrefilterMips);
|
|
||||||
size_t prefOffset = 0;
|
|
||||||
for (u32 mip = 0; mip < kPrefilterMips; ++mip) {
|
|
||||||
const u32 dim = kPrefilterFaceSize >> mip;
|
|
||||||
const f32 roughness =
|
|
||||||
static_cast<f32>(mip) / static_cast<f32>(kPrefilterMips - 1);
|
|
||||||
const u32 samples = std::min(32u << mip, 256u);
|
|
||||||
|
|
||||||
u16* base = prefFaces.data() + prefOffset;
|
|
||||||
for (u32 f = 0; f < kFaceCount; ++f) {
|
|
||||||
u16* dst = base + f * dim * dim * 4;
|
|
||||||
for (u32 y = 0; y < dim; ++y) {
|
|
||||||
for (u32 x = 0; x < dim; ++x) {
|
|
||||||
const f32 u = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(dim);
|
|
||||||
const f32 v = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(dim);
|
|
||||||
const glm::vec3 rgb = Prefilter(
|
|
||||||
pixels, srcW, srcH, FaceDir(f, u, v),
|
|
||||||
roughness, samples);
|
|
||||||
u16* px = dst + (static_cast<size_t>(y) * dim + x) * 4;
|
|
||||||
px[0] = FloatToHalf(rgb.r);
|
|
||||||
px[1] = FloatToHalf(rgb.g);
|
|
||||||
px[2] = FloatToHalf(rgb.b);
|
|
||||||
px[3] = 0x3C00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prefMips.push_back({base, dim});
|
|
||||||
prefOffset += kFaceCount * dim * dim * 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
stbi_image_free(pixels); // 源数据消费完毕
|
|
||||||
|
|
||||||
// ---- 2d. BRDF LUT:纯数学积分表(与环境内容无关)----
|
|
||||||
std::vector<u16> lutTexels(kBrdfLutSize * kBrdfLutSize * 2);
|
|
||||||
for (u32 y = 0; y < kBrdfLutSize; ++y) {
|
|
||||||
const f32 roughness = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(kBrdfLutSize);
|
|
||||||
for (u32 x = 0; x < kBrdfLutSize; ++x) {
|
|
||||||
const f32 nDotV = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(kBrdfLutSize);
|
|
||||||
const glm::vec2 ab = IntegrateBrdf(nDotV, roughness);
|
|
||||||
u16* px = lutTexels.data() + (static_cast<size_t>(y) * kBrdfLutSize + x) * 2;
|
|
||||||
px[0] = FloatToHalf(ab.x);
|
|
||||||
px[1] = FloatToHalf(ab.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 3. 上传:env / irradiance / prefilter / LUT(NaN 自检)----
|
|
||||||
if (ContainsNaNHalf(envFaces.data(), envFaces.size()) ||
|
|
||||||
ContainsNaNHalf(irrFaces.data(), irrFaces.size()) ||
|
|
||||||
ContainsNaNHalf(prefFaces.data(), prefFaces.size())) {
|
|
||||||
ENGINE_ERROR("Cubemap: NaN in pre-integrated data, "
|
|
||||||
"skipping IBL upload");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (ContainsNaNHalf(lutTexels.data(), lutTexels.size())) {
|
|
||||||
ENGINE_ERROR("Cubemap: NaN in BRDF LUT");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
EnvTexture env = UploadCube(device, allocator,
|
|
||||||
{{envFaces.data(), faceSize}}, 1);
|
|
||||||
EnvTexture irr = UploadCube(device, allocator,
|
|
||||||
{{irrFaces.data(), irrSize}}, 1);
|
|
||||||
EnvTexture pref = UploadCube(device, allocator, prefMips,
|
|
||||||
kPrefilterMips);
|
|
||||||
EnvTexture lut = UploadTexture2D(device, allocator, lutTexels,
|
|
||||||
kBrdfLutSize,
|
|
||||||
vk::Format::eR16G16Sfloat);
|
|
||||||
|
|
||||||
envFaces.clear(); envFaces.shrink_to_fit();
|
|
||||||
irrFaces.clear(); irrFaces.shrink_to_fit();
|
|
||||||
prefFaces.clear(); prefFaces.shrink_to_fit();
|
|
||||||
|
|
||||||
// cube sampler 共享:maxLod 覆盖 pre-filter mip 链(textureLod 显式
|
|
||||||
// 采样会被 clamp 到 maxLod,必须 ≥ mipCount-1)
|
|
||||||
vk::Sampler sampler = CreateEnvSampler(device, kPrefilterMips - 1);
|
|
||||||
vk::Sampler lutSampler = CreateLutSampler(device);
|
|
||||||
|
|
||||||
ENGINE_INFO("Environment IBL: {} ({}x6 env, {}x6 irradiance, "
|
|
||||||
"{}x6 prefilter {} mips, {}² BRDF LUT)",
|
|
||||||
path, faceSize, irrSize, kPrefilterFaceSize,
|
|
||||||
kPrefilterMips, kBrdfLutSize);
|
|
||||||
|
|
||||||
return Scope<Cubemap>(new Cubemap(device, allocator,
|
|
||||||
env, irr, pref, lut, sampler, lutSampler));
|
|
||||||
}
|
|
||||||
|
|
||||||
Scope<Cubemap> Cubemap::CreateNeutral(Device& device,
|
|
||||||
MemoryAllocator& allocator,
|
|
||||||
const glm::vec3& rgb)
|
|
||||||
{
|
|
||||||
// 1×1×6 常数 cube(prefilter 仅 1 mip——1px 图不能建 mip 链,
|
|
||||||
// 采样越界 lod 会被 view range clamp 到 mip0,效果等同);
|
|
||||||
// BRDF LUT 仍为真实积分(纯数学,无环境依赖)——描述符恒有效
|
|
||||||
const u16 px[4] = { FloatToHalf(rgb.r), FloatToHalf(rgb.g),
|
|
||||||
FloatToHalf(rgb.b), 0x3C00 };
|
|
||||||
std::vector<u16> faces(kFaceCount * 4, 0);
|
|
||||||
for (u32 f = 0; f < kFaceCount; ++f) {
|
|
||||||
std::memcpy(faces.data() + f * 4, px, sizeof(px));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<u16> prefFaces(kFaceCount * 4, 0);
|
|
||||||
std::vector<CubeMipData> prefMips = {{prefFaces.data(), 1}};
|
|
||||||
|
|
||||||
std::vector<u16> lutTexels(kBrdfLutSize * kBrdfLutSize * 2);
|
|
||||||
for (u32 y = 0; y < kBrdfLutSize; ++y) {
|
|
||||||
const f32 roughness = (static_cast<f32>(y) + 0.5f) / static_cast<f32>(kBrdfLutSize);
|
|
||||||
for (u32 x = 0; x < kBrdfLutSize; ++x) {
|
|
||||||
const f32 nDotV = (static_cast<f32>(x) + 0.5f) / static_cast<f32>(kBrdfLutSize);
|
|
||||||
const glm::vec2 ab = IntegrateBrdf(nDotV, roughness);
|
|
||||||
u16* t = lutTexels.data() + (static_cast<size_t>(y) * kBrdfLutSize + x) * 2;
|
|
||||||
t[0] = FloatToHalf(ab.x);
|
|
||||||
t[1] = FloatToHalf(ab.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EnvTexture env = UploadCube(device, allocator, {{faces.data(), 1}}, 1);
|
|
||||||
EnvTexture irr = UploadCube(device, allocator, {{faces.data(), 1}}, 1);
|
|
||||||
EnvTexture pref = UploadCube(device, allocator, prefMips, 1);
|
|
||||||
EnvTexture lut = UploadTexture2D(device, allocator, lutTexels,
|
|
||||||
kBrdfLutSize, vk::Format::eR16G16Sfloat);
|
|
||||||
|
|
||||||
vk::Sampler sampler = CreateEnvSampler(device, kPrefilterMips - 1);
|
|
||||||
vk::Sampler lutSampler = CreateLutSampler(device);
|
|
||||||
|
|
||||||
ENGINE_INFO("Neutral IBL environment (constant fallback, real BRDF LUT)");
|
|
||||||
return Scope<Cubemap>(new Cubemap(device, allocator,
|
|
||||||
env, irr, pref, lut, sampler, lutSampler));
|
|
||||||
}
|
|
||||||
|
|
||||||
Cubemap::~Cubemap()
|
Cubemap::~Cubemap()
|
||||||
{
|
{
|
||||||
const auto& vkDevice = m_Device.GetDevice();
|
const auto& vkDevice = m_Device.GetDevice();
|
||||||
|
|||||||
@@ -0,0 +1,549 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/9/24.
|
||||||
|
//
|
||||||
|
// IblGenerator 实现:一次性 compute 提交完成全部预积分。
|
||||||
|
//
|
||||||
|
// 同步与生命周期模型:
|
||||||
|
// - 提交用独立 command buffer + fence,与帧管线(RG 提交)互不干扰;
|
||||||
|
// - fence 等待后所有产物 layout 恒为 eShaderReadOnlyOptimal,后续
|
||||||
|
// 帧提交经队列序自动可见(无需额外 barrier);
|
||||||
|
// - 中间资源(equirect 纹理 / staging / storage view)用完即毁;
|
||||||
|
// - 描述符集不回收(growable pool 吸收;环境加载是低频事件)。
|
||||||
|
//
|
||||||
|
// 计时打点输出各段耗时,作为 CPU→GPU 迁移的前后对比依据。
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <Engine/Renderer/IblGenerator.h>
|
||||||
|
#include <Engine/Renderer/Cubemap.h>
|
||||||
|
#include <Engine/Vulkan/Device.h>
|
||||||
|
#include <Engine/Vulkan/DescriptorManager.h>
|
||||||
|
#include <Engine/Vulkan/ShaderManager.h>
|
||||||
|
#include <Engine/Core/Log.h>
|
||||||
|
|
||||||
|
#include <stb/stb_image.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace Engine
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr u32 kFaceCount = 6;
|
||||||
|
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
|
||||||
|
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0)
|
||||||
|
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
|
||||||
|
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
|
||||||
|
constexpr u32 kGroupSize = 8; // compute local_size(shader 同步)
|
||||||
|
|
||||||
|
f32 MsSince(const std::chrono::steady_clock::time_point& t0)
|
||||||
|
{
|
||||||
|
return std::chrono::duration<f32, std::milli>(
|
||||||
|
std::chrono::steady_clock::now() - t0).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IblGenerator::IblGenerator(Device& device, MemoryAllocator& allocator,
|
||||||
|
DescriptorManager& descriptors,
|
||||||
|
ShaderManager& shaders)
|
||||||
|
: m_Device(device), m_Allocator(allocator),
|
||||||
|
m_Descriptors(descriptors), m_Shaders(shaders)
|
||||||
|
{
|
||||||
|
// Set A:{0 采样输入(cube/2D 共用 combined 槽),1 storage 输出}
|
||||||
|
const std::vector<DescriptorBinding> iblBindings = {
|
||||||
|
{0, vk::DescriptorType::eCombinedImageSampler,
|
||||||
|
vk::ShaderStageFlagBits::eCompute},
|
||||||
|
{1, vk::DescriptorType::eStorageImage,
|
||||||
|
vk::ShaderStageFlagBits::eCompute},
|
||||||
|
};
|
||||||
|
m_IblSetLayout = descriptors.CreateLayout(iblBindings);
|
||||||
|
|
||||||
|
// Set B:LUT 只有 storage 输出
|
||||||
|
const std::vector<DescriptorBinding> lutBindings = {
|
||||||
|
{0, vk::DescriptorType::eStorageImage,
|
||||||
|
vk::ShaderStageFlagBits::eCompute},
|
||||||
|
};
|
||||||
|
m_LutSetLayout = descriptors.CreateLayout(lutBindings);
|
||||||
|
|
||||||
|
// 管线布局:push 仅 prefilter 用(compute 单独按 eCompute 声明)
|
||||||
|
m_IblLayout = descriptors.CreatePipelineLayout(
|
||||||
|
{m_IblSetLayout}, 0, vk::ShaderStageFlagBits::eCompute);
|
||||||
|
m_IblLayoutPush = descriptors.CreatePipelineLayout(
|
||||||
|
{m_IblSetLayout}, sizeof(f32), vk::ShaderStageFlagBits::eCompute);
|
||||||
|
m_LutLayout = descriptors.CreatePipelineLayout(
|
||||||
|
{m_LutSetLayout}, 0, vk::ShaderStageFlagBits::eCompute);
|
||||||
|
|
||||||
|
const std::string dir = "shaders/";
|
||||||
|
m_ToCubeShader = shaders.CreateFromFile(
|
||||||
|
dir + "ibl_equirect_to_cube.comp.spv",
|
||||||
|
vk::ShaderStageFlagBits::eCompute,
|
||||||
|
{m_IblSetLayout}).Handle;
|
||||||
|
m_IrrShader = shaders.CreateFromFile(
|
||||||
|
dir + "ibl_irradiance.comp.spv",
|
||||||
|
vk::ShaderStageFlagBits::eCompute,
|
||||||
|
{m_IblSetLayout}).Handle;
|
||||||
|
m_PrefShader = shaders.CreateFromFile(
|
||||||
|
dir + "ibl_prefilter.comp.spv",
|
||||||
|
vk::ShaderStageFlagBits::eCompute,
|
||||||
|
{m_IblSetLayout}, sizeof(f32)).Handle;
|
||||||
|
m_LutShader = shaders.CreateFromFile(
|
||||||
|
dir + "ibl_brdf_lut.comp.spv",
|
||||||
|
vk::ShaderStageFlagBits::eCompute,
|
||||||
|
{m_LutSetLayout}).Handle;
|
||||||
|
|
||||||
|
if (!m_ToCubeShader || !m_IrrShader || !m_PrefShader || !m_LutShader) {
|
||||||
|
ENGINE_FATAL("IblGenerator: failed to create compute shaders");
|
||||||
|
}
|
||||||
|
|
||||||
|
// equirect 中间纹理采样器:硬件双线性,S=repeat 吃掉经度接缝
|
||||||
|
vk::SamplerCreateInfo sInfo;
|
||||||
|
sInfo.setMagFilter(vk::Filter::eLinear)
|
||||||
|
.setMinFilter(vk::Filter::eLinear)
|
||||||
|
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
|
||||||
|
.setAddressModeU(vk::SamplerAddressMode::eRepeat)
|
||||||
|
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
|
||||||
|
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge);
|
||||||
|
m_EquirectSampler = device.GetDevice().createSampler(sInfo);
|
||||||
|
|
||||||
|
// 持久命令设施:每次生成 reset 复用(避免反复建池)
|
||||||
|
vk::CommandPoolCreateInfo poolInfo;
|
||||||
|
poolInfo.setQueueFamilyIndex(device.GetGraphicsFamily())
|
||||||
|
.setFlags(vk::CommandPoolCreateFlagBits::eTransient |
|
||||||
|
vk::CommandPoolCreateFlagBits::eResetCommandBuffer);
|
||||||
|
m_Pool = device.GetDevice().createCommandPool(poolInfo);
|
||||||
|
m_Cmd = device.GetDevice().allocateCommandBuffers(
|
||||||
|
vk::CommandBufferAllocateInfo{}
|
||||||
|
.setCommandPool(m_Pool)
|
||||||
|
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||||
|
.setCommandBufferCount(1))[0];
|
||||||
|
m_Fence = device.GetDevice().createFence(
|
||||||
|
vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled});
|
||||||
|
}
|
||||||
|
|
||||||
|
IblGenerator::~IblGenerator()
|
||||||
|
{
|
||||||
|
const auto& vkDevice = m_Device.GetDevice();
|
||||||
|
|
||||||
|
for (const vk::ShaderEXT s : {m_ToCubeShader, m_IrrShader,
|
||||||
|
m_PrefShader, m_LutShader}) {
|
||||||
|
if (s) vkDevice.destroyShaderEXT(s);
|
||||||
|
}
|
||||||
|
vkDevice.destroySampler(m_EquirectSampler);
|
||||||
|
vkDevice.destroyPipelineLayout(m_IblLayout);
|
||||||
|
vkDevice.destroyPipelineLayout(m_IblLayoutPush);
|
||||||
|
vkDevice.destroyPipelineLayout(m_LutLayout);
|
||||||
|
vkDevice.destroyDescriptorSetLayout(m_IblSetLayout);
|
||||||
|
vkDevice.destroyDescriptorSetLayout(m_LutSetLayout);
|
||||||
|
vkDevice.destroyFence(m_Fence);
|
||||||
|
vkDevice.freeCommandBuffers(m_Pool, m_Cmd);
|
||||||
|
vkDevice.destroyCommandPool(m_Pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- cube 兼容目标:storage(2DArray per-mip view)+ 采样(eCube view)----
|
||||||
|
|
||||||
|
IblGenerator::CubeTarget IblGenerator::CreateCubeTarget(u32 size, u32 mips) const
|
||||||
|
{
|
||||||
|
// fp16 storage 支持(桌面 GPU 普遍具备,显式校验防退化平台)
|
||||||
|
const vk::Format format = m_Device.FindSupportedFormat(
|
||||||
|
{vk::Format::eR16G16B16A16Sfloat},
|
||||||
|
vk::ImageTiling::eOptimal,
|
||||||
|
vk::FormatFeatureFlagBits::eStorageImage);
|
||||||
|
|
||||||
|
vk::ImageCreateInfo info;
|
||||||
|
info.setImageType(vk::ImageType::e2D)
|
||||||
|
.setFormat(format)
|
||||||
|
.setExtent({size, size, 1})
|
||||||
|
.setMipLevels(mips)
|
||||||
|
.setArrayLayers(kFaceCount)
|
||||||
|
.setSamples(vk::SampleCountFlagBits::e1)
|
||||||
|
.setTiling(vk::ImageTiling::eOptimal)
|
||||||
|
.setFlags(vk::ImageCreateFlagBits::eCubeCompatible)
|
||||||
|
.setUsage(vk::ImageUsageFlagBits::eStorage |
|
||||||
|
vk::ImageUsageFlagBits::eSampled)
|
||||||
|
.setInitialLayout(vk::ImageLayout::eUndefined);
|
||||||
|
|
||||||
|
CubeTarget t;
|
||||||
|
t.allocation = m_Allocator.CreateImage(info,
|
||||||
|
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
||||||
|
t.image = t.allocation.Image;
|
||||||
|
|
||||||
|
// storage 写:2DArray view,每 mip 一个(storage 必须单 mip 层)
|
||||||
|
for (u32 mip = 0; mip < mips; ++mip) {
|
||||||
|
t.storageViews.push_back(m_Device.GetDevice().createImageView(
|
||||||
|
vk::ImageViewCreateInfo{}
|
||||||
|
.setImage(t.image)
|
||||||
|
.setViewType(vk::ImageViewType::e2DArray)
|
||||||
|
.setFormat(format)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
mip, 1, 0, kFaceCount})));
|
||||||
|
}
|
||||||
|
// 采样读:eCube view 全 mip 链(消费端契约与原 CPU 路径一致)
|
||||||
|
t.cubeView = m_Device.GetDevice().createImageView(
|
||||||
|
vk::ImageViewCreateInfo{}
|
||||||
|
.setImage(t.image)
|
||||||
|
.setViewType(vk::ImageViewType::eCube)
|
||||||
|
.setFormat(format)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, mips, 0, kFaceCount}));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只释放 storage 中转 view(image/cubeView/显存交给 Cubemap 接管)
|
||||||
|
void IblGenerator::ReleaseStorageViews(const CubeTarget& t) const
|
||||||
|
{
|
||||||
|
for (const vk::ImageView v : t.storageViews) {
|
||||||
|
m_Device.GetDevice().destroyImageView(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DecodedEquirect IblGenerator::DecodeHdr(const std::string& path)
|
||||||
|
{
|
||||||
|
// stbi_loadf 无共享可变状态(失败原因每次调用独立),可并发调用
|
||||||
|
int w = 0, h = 0, channels = 0;
|
||||||
|
f32* pixels = stbi_loadf(path.c_str(), &w, &h, &channels, STBI_rgb_alpha);
|
||||||
|
if (!pixels) {
|
||||||
|
ENGINE_ERROR("IblGenerator: failed to load HDR: {}", path);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
DecodedEquirect decoded;
|
||||||
|
decoded.w = static_cast<u32>(w);
|
||||||
|
decoded.h = static_cast<u32>(h);
|
||||||
|
decoded.pixels.assign(pixels,
|
||||||
|
pixels + static_cast<size_t>(w) * h * 4);
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scope<Cubemap> IblGenerator::FromDecoded(const DecodedEquirect& decoded,
|
||||||
|
u32 faceSize,
|
||||||
|
const std::string& debugName)
|
||||||
|
{
|
||||||
|
if (!decoded.Valid()) {
|
||||||
|
ENGINE_ERROR("IblGenerator: invalid decoded equirect ({})",
|
||||||
|
debugName);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return Generate(decoded.pixels.data(), decoded.w, decoded.h,
|
||||||
|
faceSize, debugName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scope<Cubemap> IblGenerator::FromHdrEquirect(const std::string& path,
|
||||||
|
u32 faceSize)
|
||||||
|
{
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
DecodedEquirect decoded = DecodeHdr(path);
|
||||||
|
if (!decoded.Valid()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
ENGINE_INFO("IblGenerator: HDR decode {:.2f} ms", MsSince(t0));
|
||||||
|
return FromDecoded(decoded, faceSize, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scope<Cubemap> IblGenerator::Neutral(const glm::vec3& rgb)
|
||||||
|
{
|
||||||
|
// 1×1 常数 equirect:全链复用(LUT 仍是真实积分,纯数学)
|
||||||
|
const f32 pixel[4] = {rgb.r, rgb.g, rgb.b, 1.0f};
|
||||||
|
return Generate(pixel, 1, 1, 1, "neutral");
|
||||||
|
}
|
||||||
|
|
||||||
|
Scope<Cubemap> IblGenerator::Generate(const f32* pixels, u32 w, u32 h,
|
||||||
|
u32 faceSize,
|
||||||
|
const std::string& debugName)
|
||||||
|
{
|
||||||
|
const auto t0 = std::chrono::steady_clock::now();
|
||||||
|
const auto& vkDevice = m_Device.GetDevice();
|
||||||
|
|
||||||
|
// ---- 1. equirect 中间纹理(R32G32B32A32,免 CPU fp16 转换)----
|
||||||
|
const vk::DeviceSize eqBytes =
|
||||||
|
static_cast<vk::DeviceSize>(w) * h * 16;
|
||||||
|
auto staging = m_Allocator.CreateBuffer(
|
||||||
|
eqBytes,
|
||||||
|
vk::BufferUsageFlagBits::eTransferSrc,
|
||||||
|
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||||
|
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||||
|
std::memcpy(staging.MappedPtr, pixels, static_cast<size_t>(eqBytes));
|
||||||
|
|
||||||
|
vk::ImageCreateInfo eqInfo;
|
||||||
|
eqInfo.setImageType(vk::ImageType::e2D)
|
||||||
|
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||||
|
.setExtent({w, h, 1})
|
||||||
|
.setMipLevels(1)
|
||||||
|
.setArrayLayers(1)
|
||||||
|
.setSamples(vk::SampleCountFlagBits::e1)
|
||||||
|
.setTiling(vk::ImageTiling::eOptimal)
|
||||||
|
.setUsage(vk::ImageUsageFlagBits::eTransferDst |
|
||||||
|
vk::ImageUsageFlagBits::eSampled)
|
||||||
|
.setInitialLayout(vk::ImageLayout::eUndefined);
|
||||||
|
auto eqAlloc = m_Allocator.CreateImage(eqInfo,
|
||||||
|
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
||||||
|
vk::ImageView eqView = vkDevice.createImageView(
|
||||||
|
vk::ImageViewCreateInfo{}
|
||||||
|
.setImage(eqAlloc.Image)
|
||||||
|
.setViewType(vk::ImageViewType::e2D)
|
||||||
|
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, 1, 0, 1}));
|
||||||
|
|
||||||
|
// ---- 2. 输出资源 + 消费端采样器 ----
|
||||||
|
CubeTarget env = CreateCubeTarget(faceSize, 1);
|
||||||
|
CubeTarget irr = CreateCubeTarget(kIrradianceFaceSize, 1);
|
||||||
|
CubeTarget pref = CreateCubeTarget(kPrefilterFaceSize, kPrefilterMips);
|
||||||
|
|
||||||
|
const vk::Sampler envSampler = CreateEnvSampler(kPrefilterMips - 1);
|
||||||
|
const vk::Sampler lutSampler = CreateLutSampler();
|
||||||
|
|
||||||
|
const vk::Format lutFormat = m_Device.FindSupportedFormat(
|
||||||
|
{vk::Format::eR16G16Sfloat},
|
||||||
|
vk::ImageTiling::eOptimal,
|
||||||
|
vk::FormatFeatureFlagBits::eStorageImage);
|
||||||
|
vk::ImageCreateInfo lutInfo;
|
||||||
|
lutInfo.setImageType(vk::ImageType::e2D)
|
||||||
|
.setFormat(lutFormat)
|
||||||
|
.setExtent({kBrdfLutSize, kBrdfLutSize, 1})
|
||||||
|
.setMipLevels(1)
|
||||||
|
.setArrayLayers(1)
|
||||||
|
.setSamples(vk::SampleCountFlagBits::e1)
|
||||||
|
.setTiling(vk::ImageTiling::eOptimal)
|
||||||
|
.setUsage(vk::ImageUsageFlagBits::eStorage |
|
||||||
|
vk::ImageUsageFlagBits::eSampled)
|
||||||
|
.setInitialLayout(vk::ImageLayout::eUndefined);
|
||||||
|
auto lutAlloc = m_Allocator.CreateImage(lutInfo,
|
||||||
|
vk::MemoryPropertyFlagBits::eDeviceLocal);
|
||||||
|
vk::ImageView lutView = vkDevice.createImageView(
|
||||||
|
vk::ImageViewCreateInfo{}
|
||||||
|
.setImage(lutAlloc.Image)
|
||||||
|
.setViewType(vk::ImageViewType::e2D)
|
||||||
|
.setFormat(lutFormat)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, 1, 0, 1}));
|
||||||
|
|
||||||
|
const auto resourceMs = MsSince(t0);
|
||||||
|
|
||||||
|
// ---- 3. 描述符(低频事件,growable pool 吸收,不回收)----
|
||||||
|
vk::DescriptorSet setToCube =
|
||||||
|
m_Descriptors.AllocateDescriptorSet(m_IblSetLayout);
|
||||||
|
vk::DescriptorSet setIrr =
|
||||||
|
m_Descriptors.AllocateDescriptorSet(m_IblSetLayout);
|
||||||
|
vk::DescriptorSet setLut =
|
||||||
|
m_Descriptors.AllocateDescriptorSet(m_LutSetLayout);
|
||||||
|
std::array<vk::DescriptorSet, kPrefilterMips> setPref{};
|
||||||
|
for (u32 m = 0; m < kPrefilterMips; ++m) {
|
||||||
|
setPref[m] = m_Descriptors.AllocateDescriptorSet(m_IblSetLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Descriptors.UpdateTexture(setToCube, 0, m_EquirectSampler, eqView);
|
||||||
|
m_Descriptors.UpdateStorageImage(setToCube, 1, env.storageViews[0]);
|
||||||
|
// cube 采样必须用带 mip 的 env 采样器(textureLod 被 clamp 到 maxLod)
|
||||||
|
m_Descriptors.UpdateTexture(setIrr, 0, envSampler, env.cubeView);
|
||||||
|
m_Descriptors.UpdateStorageImage(setIrr, 1, irr.storageViews[0]);
|
||||||
|
for (u32 m = 0; m < kPrefilterMips; ++m) {
|
||||||
|
m_Descriptors.UpdateTexture(setPref[m], 0, envSampler, env.cubeView);
|
||||||
|
m_Descriptors.UpdateStorageImage(setPref[m], 1, pref.storageViews[m]);
|
||||||
|
}
|
||||||
|
m_Descriptors.UpdateStorageImage(setLut, 0, lutView);
|
||||||
|
|
||||||
|
// ---- 4. 录制 + 一次性提交 ----
|
||||||
|
vkDevice.resetFences(m_Fence);
|
||||||
|
vkDevice.resetCommandPool(m_Pool);
|
||||||
|
m_Cmd.begin(vk::CommandBufferBeginInfo{
|
||||||
|
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
||||||
|
|
||||||
|
// 4a. equirect staging → image → shader 读 layout
|
||||||
|
vk::ImageMemoryBarrier2 eqToTransfer;
|
||||||
|
eqToTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
||||||
|
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
||||||
|
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
||||||
|
.setDstAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
||||||
|
.setOldLayout(vk::ImageLayout::eUndefined)
|
||||||
|
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
|
||||||
|
.setImage(eqAlloc.Image)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, 1, 0, 1});
|
||||||
|
m_Cmd.pipelineBarrier2(
|
||||||
|
vk::DependencyInfo{}.setImageMemoryBarriers(eqToTransfer));
|
||||||
|
|
||||||
|
vk::BufferImageCopy eqCopy;
|
||||||
|
eqCopy.setBufferOffset(0)
|
||||||
|
.setBufferRowLength(0)
|
||||||
|
.setBufferImageHeight(0)
|
||||||
|
.setImageSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1})
|
||||||
|
.setImageOffset({0, 0, 0})
|
||||||
|
.setImageExtent({w, h, 1});
|
||||||
|
m_Cmd.copyBufferToImage(staging.Buffer, eqAlloc.Image,
|
||||||
|
vk::ImageLayout::eTransferDstOptimal, eqCopy);
|
||||||
|
|
||||||
|
vk::ImageMemoryBarrier2 eqToShader;
|
||||||
|
eqToShader.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
||||||
|
.setSrcAccessMask(vk::AccessFlagBits2::eTransferWrite)
|
||||||
|
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||||
|
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
||||||
|
.setOldLayout(vk::ImageLayout::eTransferDstOptimal)
|
||||||
|
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
||||||
|
.setImage(eqAlloc.Image)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, 1, 0, 1});
|
||||||
|
m_Cmd.pipelineBarrier2(
|
||||||
|
vk::DependencyInfo{}.setImageMemoryBarriers(eqToShader));
|
||||||
|
|
||||||
|
// 4b. 输出资源 Undefined → General(compute storage 写的要求)
|
||||||
|
std::array<vk::ImageMemoryBarrier2, 4> toGeneral{};
|
||||||
|
const vk::Image images[4] = {env.image, irr.image, pref.image,
|
||||||
|
lutAlloc.Image};
|
||||||
|
const u32 mipCounts[4] = {1, 1, kPrefilterMips, 1};
|
||||||
|
for (u32 i = 0; i < 4; ++i) {
|
||||||
|
toGeneral[i]
|
||||||
|
.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
||||||
|
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
||||||
|
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||||
|
.setDstAccessMask(vk::AccessFlagBits2::eShaderWrite)
|
||||||
|
.setOldLayout(vk::ImageLayout::eUndefined)
|
||||||
|
.setNewLayout(vk::ImageLayout::eGeneral)
|
||||||
|
.setImage(images[i])
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, mipCounts[i], 0, kFaceCount});
|
||||||
|
}
|
||||||
|
toGeneral[3].setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, 1, 0, 1}); // LUT 是 2D
|
||||||
|
m_Cmd.pipelineBarrier2(
|
||||||
|
vk::DependencyInfo{}.setImageMemoryBarriers(toGeneral));
|
||||||
|
|
||||||
|
// 4c. ① equirect → env cube
|
||||||
|
std::array<vk::ShaderStageFlagBits, 1> computeStage = {
|
||||||
|
vk::ShaderStageFlagBits::eCompute};
|
||||||
|
std::array<vk::ShaderEXT, 1> toCubeShaders = {m_ToCubeShader};
|
||||||
|
m_Cmd.bindShadersEXT(computeStage, toCubeShaders);
|
||||||
|
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||||
|
m_IblLayout, 0, setToCube, {});
|
||||||
|
m_Cmd.dispatch((faceSize + kGroupSize - 1) / kGroupSize,
|
||||||
|
(faceSize + kGroupSize - 1) / kGroupSize, kFaceCount);
|
||||||
|
|
||||||
|
// env 转采样读(②③的输入)
|
||||||
|
vk::ImageMemoryBarrier2 envToRead;
|
||||||
|
envToRead.setSrcStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||||
|
.setSrcAccessMask(vk::AccessFlagBits2::eShaderWrite)
|
||||||
|
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||||
|
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
||||||
|
.setOldLayout(vk::ImageLayout::eGeneral)
|
||||||
|
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
||||||
|
.setImage(env.image)
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, 1, 0, kFaceCount});
|
||||||
|
m_Cmd.pipelineBarrier2(
|
||||||
|
vk::DependencyInfo{}.setImageMemoryBarriers(envToRead));
|
||||||
|
|
||||||
|
// 4d. ② irradiance(env → irr)
|
||||||
|
std::array<vk::ShaderEXT, 1> irrShaders = {m_IrrShader};
|
||||||
|
m_Cmd.bindShadersEXT(computeStage, irrShaders);
|
||||||
|
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||||
|
m_IblLayout, 0, setIrr, {});
|
||||||
|
m_Cmd.dispatch((kIrradianceFaceSize + kGroupSize - 1) / kGroupSize,
|
||||||
|
(kIrradianceFaceSize + kGroupSize - 1) / kGroupSize,
|
||||||
|
kFaceCount);
|
||||||
|
|
||||||
|
// 4e. ③ prefilter(env → pref,每 mip 一次 dispatch + push roughness)
|
||||||
|
std::array<vk::ShaderEXT, 1> prefShaders = {m_PrefShader};
|
||||||
|
m_Cmd.bindShadersEXT(computeStage, prefShaders);
|
||||||
|
for (u32 m = 0; m < kPrefilterMips; ++m) {
|
||||||
|
const u32 dim = kPrefilterFaceSize >> m;
|
||||||
|
const f32 roughness =
|
||||||
|
static_cast<f32>(m) / static_cast<f32>(kPrefilterMips - 1);
|
||||||
|
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||||
|
m_IblLayoutPush, 0, setPref[m], {});
|
||||||
|
m_Cmd.pushConstants<f32>(m_IblLayoutPush,
|
||||||
|
vk::ShaderStageFlagBits::eCompute,
|
||||||
|
0, roughness);
|
||||||
|
m_Cmd.dispatch((dim + kGroupSize - 1) / kGroupSize,
|
||||||
|
(dim + kGroupSize - 1) / kGroupSize, kFaceCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4f. ④ BRDF LUT(独立纯数学)
|
||||||
|
std::array<vk::ShaderEXT, 1> lutShaders = {m_LutShader};
|
||||||
|
m_Cmd.bindShadersEXT(computeStage, lutShaders);
|
||||||
|
m_Cmd.bindDescriptorSets(vk::PipelineBindPoint::eCompute,
|
||||||
|
m_LutLayout, 0, setLut, {});
|
||||||
|
m_Cmd.dispatch((kBrdfLutSize + kGroupSize - 1) / kGroupSize,
|
||||||
|
(kBrdfLutSize + kGroupSize - 1) / kGroupSize, 1);
|
||||||
|
|
||||||
|
// 4g. irr/pref/lut 收尾:General → ShaderReadOnly(消费端采样 layout;
|
||||||
|
// fence 等待后对后续队列提交自动可见)
|
||||||
|
std::array<vk::ImageMemoryBarrier2, 3> toRead{};
|
||||||
|
const vk::Image readImages[3] = {irr.image, pref.image,
|
||||||
|
lutAlloc.Image};
|
||||||
|
const u32 readMips[3] = {1, kPrefilterMips, 1};
|
||||||
|
const u32 readLayers[3] = {kFaceCount, kFaceCount, 1}; // LUT 是 2D
|
||||||
|
for (u32 i = 0; i < 3; ++i) {
|
||||||
|
toRead[i]
|
||||||
|
.setSrcStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||||
|
.setSrcAccessMask(vk::AccessFlagBits2::eShaderWrite)
|
||||||
|
.setDstStageMask(vk::PipelineStageFlagBits2::eComputeShader)
|
||||||
|
.setDstAccessMask(vk::AccessFlagBits2::eShaderRead)
|
||||||
|
.setOldLayout(vk::ImageLayout::eGeneral)
|
||||||
|
.setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal)
|
||||||
|
.setImage(readImages[i])
|
||||||
|
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||||
|
0, readMips[i], 0, readLayers[i]});
|
||||||
|
}
|
||||||
|
m_Cmd.pipelineBarrier2(
|
||||||
|
vk::DependencyInfo{}.setImageMemoryBarriers(toRead));
|
||||||
|
|
||||||
|
m_Cmd.end();
|
||||||
|
|
||||||
|
m_Device.GetGraphicsQueue().submit(
|
||||||
|
vk::SubmitInfo{}.setCommandBuffers(m_Cmd), m_Fence);
|
||||||
|
if (vkDevice.waitForFences(m_Fence, VK_TRUE, UINT64_MAX) !=
|
||||||
|
vk::Result::eSuccess) {
|
||||||
|
ENGINE_ERROR("IblGenerator: waitForFences failed ({})", debugName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 5. 中间资源即毁;image/cubeView/显存交棒 Cubemap ----
|
||||||
|
vkDevice.destroyImageView(eqView);
|
||||||
|
m_Allocator.Destroy(eqAlloc);
|
||||||
|
m_Allocator.Destroy(staging);
|
||||||
|
ReleaseStorageViews(env);
|
||||||
|
ReleaseStorageViews(irr);
|
||||||
|
ReleaseStorageViews(pref);
|
||||||
|
|
||||||
|
const f32 computeMs = MsSince(t0) - resourceMs;
|
||||||
|
ENGINE_INFO("IblGenerator[{}]: {}x6 env, {}x6 irradiance, "
|
||||||
|
"{}x6 prefilter {} mips, {}² BRDF LUT — "
|
||||||
|
"resources {:.2f} ms, compute+wait {:.2f} ms",
|
||||||
|
debugName, faceSize, kIrradianceFaceSize,
|
||||||
|
kPrefilterFaceSize, kPrefilterMips, kBrdfLutSize,
|
||||||
|
resourceMs, computeMs);
|
||||||
|
|
||||||
|
return Scope<Cubemap>(new Cubemap(
|
||||||
|
m_Device, m_Allocator,
|
||||||
|
EnvTexture{env.image, env.cubeView, env.allocation},
|
||||||
|
EnvTexture{irr.image, irr.cubeView, irr.allocation},
|
||||||
|
EnvTexture{pref.image, pref.cubeView, pref.allocation},
|
||||||
|
EnvTexture{lutAlloc.Image, lutView, lutAlloc},
|
||||||
|
envSampler, lutSampler));
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::Sampler IblGenerator::CreateEnvSampler(u32 maxLod) const
|
||||||
|
{
|
||||||
|
vk::SamplerCreateInfo sInfo;
|
||||||
|
sInfo.setMagFilter(vk::Filter::eLinear)
|
||||||
|
.setMinFilter(vk::Filter::eLinear)
|
||||||
|
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
|
||||||
|
.setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
|
||||||
|
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
|
||||||
|
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge)
|
||||||
|
.setAnisotropyEnable(VK_FALSE)
|
||||||
|
.setBorderColor(vk::BorderColor::eIntOpaqueBlack)
|
||||||
|
.setUnnormalizedCoordinates(VK_FALSE)
|
||||||
|
.setCompareEnable(VK_FALSE)
|
||||||
|
.setMinLod(0.0f)
|
||||||
|
.setMaxLod(static_cast<f32>(maxLod));
|
||||||
|
return m_Device.GetDevice().createSampler(sInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
vk::Sampler IblGenerator::CreateLutSampler() const
|
||||||
|
{
|
||||||
|
// clamp-to-edge:共享 border 白采样器会在 uv=1.0 处混入白色 border
|
||||||
|
return CreateEnvSampler(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Engine
|
||||||
@@ -23,14 +23,22 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
|||||||
DescriptorManager& descriptorManager,
|
DescriptorManager& descriptorManager,
|
||||||
vk::DescriptorSetLayout textureLayout,
|
vk::DescriptorSetLayout textureLayout,
|
||||||
const std::string& path)
|
const std::string& path)
|
||||||
|
: Model(device, allocator, descriptorManager, textureLayout)
|
||||||
|
{
|
||||||
|
// 同步便捷路径 = 两段式串行(等价异步收割后的主线程消费)
|
||||||
|
LoadModel(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空模型骨架:只建默认纹理;mesh/材质由 FromParsed 填充
|
||||||
|
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||||
|
DescriptorManager& descriptorManager,
|
||||||
|
vk::DescriptorSetLayout textureLayout)
|
||||||
: m_Device(device)
|
: m_Device(device)
|
||||||
, m_Allocator(allocator)
|
, m_Allocator(allocator)
|
||||||
, m_DescriptorManager(descriptorManager)
|
, m_DescriptorManager(descriptorManager)
|
||||||
, m_TextureLayout(textureLayout)
|
, m_TextureLayout(textureLayout)
|
||||||
{
|
{
|
||||||
InitDefaultTextures();
|
InitDefaultTextures();
|
||||||
|
|
||||||
LoadModel(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Model::~Model() = default;
|
Model::~Model() = default;
|
||||||
@@ -92,9 +100,10 @@ Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
|
|||||||
halfSize, std::array<u8, 3>{r, g, b}));
|
halfSize, std::array<u8, 3>{r, g, b}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Model::LoadModel(const std::string& path)
|
ParsedModel Model::ParseFromDisk(const std::string& path)
|
||||||
{
|
{
|
||||||
m_ModelPath = path;
|
ParsedModel out;
|
||||||
|
out.path = path;
|
||||||
|
|
||||||
// UV 翻转按格式区分:
|
// UV 翻转按格式区分:
|
||||||
// OBJ/FBX —— v 原点在底部,需翻到左上惯例(配合"v=0=图像顶"采样)
|
// OBJ/FBX —— v 原点在底部,需翻到左上惯例(配合"v=0=图像顶"采样)
|
||||||
@@ -106,36 +115,84 @@ void Model::LoadModel(const std::string& path)
|
|||||||
[](unsigned char c) { return std::tolower(c); });
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
return s;
|
return s;
|
||||||
}();
|
}();
|
||||||
m_IsGltf = lower.size() >= 5 &&
|
out.isGltf = lower.size() >= 5 &&
|
||||||
(lower.rfind(".gltf") == lower.size() - 5 ||
|
(lower.rfind(".gltf") == lower.size() - 5 ||
|
||||||
lower.rfind(".glb") == lower.size() - 4);
|
lower.rfind(".glb") == lower.size() - 4);
|
||||||
|
|
||||||
Assimp::Importer importer;
|
Assimp::Importer importer;
|
||||||
const aiScene* scene = importer.ReadFile(path,
|
const aiScene* scene = importer.ReadFile(path,
|
||||||
aiProcess_Triangulate |
|
aiProcess_Triangulate |
|
||||||
(m_IsGltf ? 0 : aiProcess_FlipUVs) |
|
(out.isGltf ? 0 : aiProcess_FlipUVs) |
|
||||||
aiProcess_GenSmoothNormals |
|
aiProcess_GenSmoothNormals |
|
||||||
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
||||||
aiProcess_JoinIdenticalVertices);
|
aiProcess_JoinIdenticalVertices);
|
||||||
|
|
||||||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
||||||
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
|
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
|
||||||
return;
|
return out; // meshes 空 = 无效
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessNode(scene->mRootNode, scene);
|
const std::filesystem::path modelDir =
|
||||||
|
std::filesystem::path(path).parent_path();
|
||||||
|
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf);
|
||||||
|
ENGINE_INFO("Model parsed: {} ({} meshes)", path, out.meshes.size());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
||||||
|
DescriptorManager& descriptorManager,
|
||||||
|
vk::DescriptorSetLayout textureLayout,
|
||||||
|
ParsedModel&& parsed)
|
||||||
|
{
|
||||||
|
if (!parsed.Valid()) {
|
||||||
|
ENGINE_ERROR("Model::FromParsed: invalid parsed model ({})", parsed.path);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto model = Scope<Model>(new Model(device, allocator,
|
||||||
|
descriptorManager, textureLayout));
|
||||||
|
model->m_ModelPath = parsed.path;
|
||||||
|
model->m_IsGltf = parsed.isGltf;
|
||||||
|
|
||||||
|
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
|
||||||
|
for (auto& pm : parsed.meshes) {
|
||||||
|
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||||
|
model->m_Meshes.push_back(CreateScope<Mesh>(
|
||||||
|
device, allocator, std::move(pm.vertices), std::move(pm.indices), mat));
|
||||||
|
}
|
||||||
|
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||||||
|
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Model::LoadModel(const std::string& path)
|
||||||
|
{
|
||||||
|
// 同步路径:解析 + GPU 构建串行(与 FromParsed 相同的消费逻辑)
|
||||||
|
ParsedModel parsed = ParseFromDisk(path);
|
||||||
|
if (!parsed.Valid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_ModelPath = parsed.path;
|
||||||
|
m_IsGltf = parsed.isGltf;
|
||||||
|
for (auto& pm : parsed.meshes) {
|
||||||
|
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||||
|
m_Meshes.push_back(CreateScope<Mesh>(
|
||||||
|
m_Device, m_Allocator, std::move(pm.vertices), std::move(pm.indices), mat));
|
||||||
|
}
|
||||||
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||||||
path, m_Meshes.size(), m_TextureCache.size());
|
path, m_Meshes.size(), m_TextureCache.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Model::ProcessNode(aiNode* node, const aiScene* scene)
|
void Model::ProcessNode(const aiNode* node, const aiScene* scene,
|
||||||
|
std::vector<ParsedMesh>& out,
|
||||||
|
const std::filesystem::path& modelDir, bool isGltf)
|
||||||
{
|
{
|
||||||
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]];
|
||||||
m_Meshes.push_back(ProcessMesh(mesh, scene));
|
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf));
|
||||||
}
|
}
|
||||||
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
||||||
ProcessNode(node->mChildren[i], scene);
|
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,10 +250,13 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
|
|||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
|
ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||||
|
const std::filesystem::path& modelDir,
|
||||||
|
bool isGltf)
|
||||||
{
|
{
|
||||||
std::vector<Vertex> vertices;
|
ParsedMesh out;
|
||||||
std::vector<u32> indices;
|
std::vector<Vertex>& vertices = out.vertices;
|
||||||
|
std::vector<u32>& indices = out.indices;
|
||||||
|
|
||||||
vertices.reserve(mesh->mNumVertices);
|
vertices.reserve(mesh->mNumVertices);
|
||||||
|
|
||||||
@@ -248,9 +308,7 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
|
|||||||
MaterialParams params;
|
MaterialParams params;
|
||||||
|
|
||||||
if (mesh->mMaterialIndex >= 0) {
|
if (mesh->mMaterialIndex >= 0) {
|
||||||
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
||||||
const std::filesystem::path modelDir =
|
|
||||||
std::filesystem::path(m_ModelPath).parent_path();
|
|
||||||
|
|
||||||
// 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)。
|
||||||
@@ -304,7 +362,7 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
|
|||||||
// roughness 有独立贴图 → 因子 1(G 通道直达);
|
// roughness 有独立贴图 → 因子 1(G 通道直达);
|
||||||
// 无贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
|
// 无贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
|
||||||
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
|
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
|
||||||
if (m_IsGltf) {
|
if (isGltf) {
|
||||||
ai_real metallic = 1.0f, roughness = 1.0f;
|
ai_real metallic = 1.0f, roughness = 1.0f;
|
||||||
material->Get(AI_MATKEY_METALLIC_FACTOR, metallic);
|
material->Get(AI_MATKEY_METALLIC_FACTOR, metallic);
|
||||||
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
|
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
|
||||||
@@ -323,11 +381,12 @@ Scope<Mesh> Model::ProcessMesh(aiMesh* mesh, const aiScene* scene)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 材质缓存:同贴图组合+因子 → 同一 Material(一个描述符集)
|
// 材质创建延迟到 GPU 构建段(FromParsed):解析期只留引用
|
||||||
Material* mat = GetOrCreateMaterial(texPaths, params);
|
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||||
|
out.texPaths[slot] = std::move(texPaths[slot]);
|
||||||
return CreateScope<Mesh>(
|
}
|
||||||
m_Device, m_Allocator, vertices, indices, mat);
|
out.params = params;
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
@@ -8,9 +8,10 @@
|
|||||||
namespace Engine::Passes
|
namespace Engine::Passes
|
||||||
{
|
{
|
||||||
BloomPass::BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
|
BloomPass::BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
|
||||||
|
FrameSceneData& frame,
|
||||||
RG::TextureHandle hdrColor,
|
RG::TextureHandle hdrColor,
|
||||||
RG::TextureHandle bloomA, RG::TextureHandle bloomB)
|
RG::TextureHandle bloomA, RG::TextureHandle bloomB)
|
||||||
: m_Gpu(gpu), m_Params(params),
|
: m_Gpu(gpu), m_Params(params), m_Frame(frame),
|
||||||
m_HDR(hdrColor), m_A(bloomA), m_B(bloomB)
|
m_HDR(hdrColor), m_A(bloomA), m_B(bloomB)
|
||||||
{
|
{
|
||||||
m_ExtractSets.resize(kMaxFramesInFlight);
|
m_ExtractSets.resize(kMaxFramesInFlight);
|
||||||
@@ -25,9 +26,13 @@ namespace Engine::Passes
|
|||||||
|
|
||||||
void BloomPass::Register(RG::RenderGraph& rg)
|
void BloomPass::Register(RG::RenderGraph& rg)
|
||||||
{
|
{
|
||||||
// Extract:HDR → 半分辨率 A(软阈值由 frag 的 param0 传入)
|
// bloom 关闭:三个子 pass 全部空转(省两次模糊 + 一次提取),
|
||||||
|
// tonemap 侧强度同步归零不采样叠加。
|
||||||
|
// 注意开关判断必须在录制 lambda 内(每帧评估),Register 只在
|
||||||
|
// RG 编译时跑一次
|
||||||
rg.AddPass("BloomExtract",
|
rg.AddPass("BloomExtract",
|
||||||
[this](RG::RenderGraphContext& ctx) {
|
[this](RG::RenderGraphContext& ctx) {
|
||||||
|
if (!m_Frame.bloomEnabled) return;
|
||||||
m_Gpu.RecordFullscreen(ctx, ctx.View(m_A), m_Gpu.extractFrag,
|
m_Gpu.RecordFullscreen(ctx, ctx.View(m_A), m_Gpu.extractFrag,
|
||||||
m_ExtractSets[ctx.frame.frameSlot],
|
m_ExtractSets[ctx.frame.frameSlot],
|
||||||
FullscreenPush{0.0f, m_Params.bloomThreshold, 0.0f, 0.0f});
|
FullscreenPush{0.0f, m_Params.bloomThreshold, 0.0f, 0.0f});
|
||||||
@@ -39,6 +44,7 @@ namespace Engine::Passes
|
|||||||
// BlurH:A → B(水平步长经 push 传入)
|
// BlurH:A → B(水平步长经 push 传入)
|
||||||
rg.AddPass("BloomBlurH",
|
rg.AddPass("BloomBlurH",
|
||||||
[this](RG::RenderGraphContext& ctx) {
|
[this](RG::RenderGraphContext& ctx) {
|
||||||
|
if (!m_Frame.bloomEnabled) return;
|
||||||
m_Gpu.RecordFullscreen(ctx, ctx.View(m_B), m_Gpu.blurFrag,
|
m_Gpu.RecordFullscreen(ctx, ctx.View(m_B), m_Gpu.blurFrag,
|
||||||
m_BlurHSets[ctx.frame.frameSlot],
|
m_BlurHSets[ctx.frame.frameSlot],
|
||||||
FullscreenPush{0.0f,
|
FullscreenPush{0.0f,
|
||||||
@@ -52,6 +58,7 @@ namespace Engine::Passes
|
|||||||
// BlurV:B → A(垂直步长)
|
// BlurV:B → A(垂直步长)
|
||||||
rg.AddPass("BloomBlurV",
|
rg.AddPass("BloomBlurV",
|
||||||
[this](RG::RenderGraphContext& ctx) {
|
[this](RG::RenderGraphContext& ctx) {
|
||||||
|
if (!m_Frame.bloomEnabled) return;
|
||||||
m_Gpu.RecordFullscreen(ctx, ctx.View(m_A), m_Gpu.blurFrag,
|
m_Gpu.RecordFullscreen(ctx, ctx.View(m_A), m_Gpu.blurFrag,
|
||||||
m_BlurVSets[ctx.frame.frameSlot],
|
m_BlurVSets[ctx.frame.frameSlot],
|
||||||
FullscreenPush{0.0f, 0.0f,
|
FullscreenPush{0.0f, 0.0f,
|
||||||
|
|||||||
@@ -16,17 +16,17 @@ namespace Engine::Passes
|
|||||||
ShaderManager& shaders)
|
ShaderManager& shaders)
|
||||||
: m_Device(device), m_Descriptors(descriptors)
|
: m_Device(device), m_Descriptors(descriptors)
|
||||||
{
|
{
|
||||||
// Set0 = {HDR 颜色图, Bloom 图, 调试输入},push = 16B 通用结构
|
// Set0 = {HDR 颜色图, Bloom 图, 调试输入, TAA, Velocity,
|
||||||
//(binding2 只被 tonemap 的调试视图使用;bloom 系 shader 静态
|
// ShadowMap0/1/2},push = 16B 通用结构(VUID-08878)。
|
||||||
// 使用的 binding 是布局的子集,合法)
|
// 后 6 个 binding 供 Tonemap 调试视图使用;其余全屏 shader
|
||||||
std::vector<DescriptorBinding> bindings = {
|
// 静态使用的 binding 是布局的子集,合法
|
||||||
{0, vk::DescriptorType::eCombinedImageSampler,
|
std::vector<DescriptorBinding> bindings;
|
||||||
vk::ShaderStageFlagBits::eFragment},
|
// 0-7 帧 RG 中间目标 + 8-11 环境调试视图(cube 三件套 + LUT)
|
||||||
{1, vk::DescriptorType::eCombinedImageSampler,
|
for (u32 b = 0; b < 12; ++b) {
|
||||||
vk::ShaderStageFlagBits::eFragment},
|
bindings.push_back(
|
||||||
{2, vk::DescriptorType::eCombinedImageSampler,
|
{b, vk::DescriptorType::eCombinedImageSampler,
|
||||||
vk::ShaderStageFlagBits::eFragment},
|
vk::ShaderStageFlagBits::eFragment});
|
||||||
};
|
}
|
||||||
setLayout = descriptors.CreateLayout(bindings);
|
setLayout = descriptors.CreateLayout(bindings);
|
||||||
|
|
||||||
layout = descriptors.CreatePipelineLayout(
|
layout = descriptors.CreatePipelineLayout(
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ namespace Engine::Passes
|
|||||||
.setBorderColor(vk::BorderColor::eFloatOpaqueWhite)
|
.setBorderColor(vk::BorderColor::eFloatOpaqueWhite)
|
||||||
.setUnnormalizedCoordinates(VK_FALSE)
|
.setUnnormalizedCoordinates(VK_FALSE)
|
||||||
.setCompareEnable(VK_TRUE)
|
.setCompareEnable(VK_TRUE)
|
||||||
.setCompareOp(vk::CompareOp::eGreater); // 【诊断】方向对照
|
// ref < stored → lit=1:片元比遮挡物近即被照亮
|
||||||
|
.setCompareOp(vk::CompareOp::eLess);
|
||||||
shadowSampler = device.GetDevice().createSampler(shadowSamplerInfo);
|
shadowSampler = device.GetDevice().createSampler(shadowSamplerInfo);
|
||||||
|
|
||||||
// per-slot 描述符分配 + Set0 绑定(buffer 句柄全程稳定,不用重绑)
|
// per-slot 描述符分配 + Set0 绑定(buffer 句柄全程稳定,不用重绑)
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ namespace Engine::Passes
|
|||||||
// 材质/实体数据驱动剔除,而非 pass 逻辑特判。
|
// 材质/实体数据驱动剔除,而非 pass 逻辑特判。
|
||||||
// (每级联独立剔除是后续优化点:shadowView 细分为 per-cascade 列表)
|
// (每级联独立剔除是后续优化点:shadowView 细分为 per-cascade 列表)
|
||||||
rg.AddPass("Shadow", [this](RG::RenderGraphContext& ctx) {
|
rg.AddPass("Shadow", [this](RG::RenderGraphContext& ctx) {
|
||||||
|
// 阴影关闭:整 pass 空转(省一整趟场景绘制)。图仍是
|
||||||
|
// Clear=1.0,frag 侧 cascadeSplits.w 短路根本不采样
|
||||||
|
if (!m_Frame.shadowsEnabled) return;
|
||||||
|
|
||||||
const vk::CommandBuffer cmd = ctx.cmd;
|
const vk::CommandBuffer cmd = ctx.cmd;
|
||||||
|
|
||||||
for (u32 c = 0; c < kMaxCascades; ++c) {
|
for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||||
|
|||||||
@@ -74,15 +74,30 @@ namespace Engine::Passes
|
|||||||
vkDevice.destroyDescriptorSetLayout(m_SetLayout);
|
vkDevice.destroyDescriptorSetLayout(m_SetLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SkyboxPass::OnEnvironmentChanged(const Cubemap* env)
|
||||||
|
{
|
||||||
|
m_Env = env;
|
||||||
|
if (!m_Env) return;
|
||||||
|
|
||||||
|
// cube view 全程稳定语义是"单个环境内";切换环境 = 新 view,
|
||||||
|
// per-slot set 全部重绑
|
||||||
|
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||||
|
m_Descriptors.UpdateTexture(m_Sets[slot], 0,
|
||||||
|
m_Env->GetSampler(),
|
||||||
|
m_Env->GetView());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SkyboxPass::Register(RG::RenderGraph& rg)
|
void SkyboxPass::Register(RG::RenderGraph& rg)
|
||||||
{
|
{
|
||||||
rg.AddPass("Skybox", [this](RG::RenderGraphContext& ctx) {
|
rg.AddPass("Skybox", [this](RG::RenderGraphContext& ctx) {
|
||||||
ZoneScopedN("Skybox");
|
ZoneScopedN("Skybox");
|
||||||
const vk::CommandBuffer cmd = ctx.cmd;
|
const vk::CommandBuffer cmd = ctx.cmd;
|
||||||
|
|
||||||
|
const glm::vec4& cc = m_Frame.ClearColor;
|
||||||
vk::ClearValue clear;
|
vk::ClearValue clear;
|
||||||
clear.color = vk::ClearColorValue(
|
clear.color = vk::ClearColorValue(
|
||||||
std::array<float, 4>{0.1f, 0.1f, 0.1f, 1.0f});
|
std::array<float, 4>{cc.r, cc.g, cc.b, cc.a});
|
||||||
|
|
||||||
vk::RenderingAttachmentInfo color;
|
vk::RenderingAttachmentInfo color;
|
||||||
color.setImageView(ctx.View(m_HDRColor))
|
color.setImageView(ctx.View(m_HDRColor))
|
||||||
@@ -98,13 +113,13 @@ namespace Engine::Passes
|
|||||||
|
|
||||||
cmd.beginRendering(renderingInfo);
|
cmd.beginRendering(renderingInfo);
|
||||||
|
|
||||||
if (m_Env) {
|
if (m_Env && m_Frame.DrawSkybox) {
|
||||||
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
|
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
|
||||||
std::array<vk::ShaderStageFlagBits, 2> stages = {
|
const std::array<vk::ShaderStageFlagBits, 2> stages = {
|
||||||
vk::ShaderStageFlagBits::eVertex,
|
vk::ShaderStageFlagBits::eVertex,
|
||||||
vk::ShaderStageFlagBits::eFragment
|
vk::ShaderStageFlagBits::eFragment
|
||||||
};
|
};
|
||||||
std::array<vk::ShaderEXT, 2> shaders = {m_Vert, m_Frag};
|
const std::array<vk::ShaderEXT, 2> shaders = {m_Vert, m_Frag};
|
||||||
cmd.bindShadersEXT(stages, shaders);
|
cmd.bindShadersEXT(stages, shaders);
|
||||||
|
|
||||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||||
@@ -135,12 +150,12 @@ namespace Engine::Passes
|
|||||||
cmd.setVertexInputEXT({}, {});
|
cmd.setVertexInputEXT({}, {});
|
||||||
|
|
||||||
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
|
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
|
||||||
vk::SampleMask sampleMask = 0xFFFFFFFF;
|
constexpr vk::SampleMask sampleMask = 0xFFFFFFFF;
|
||||||
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
|
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
|
||||||
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
|
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
|
||||||
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
||||||
|
|
||||||
vk::Viewport viewport(0.0f, 0.0f,
|
const 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),
|
||||||
0.0f, 1.0f);
|
0.0f, 1.0f);
|
||||||
@@ -156,8 +171,8 @@ namespace Engine::Passes
|
|||||||
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
|
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||||
// ArrayProxyNoTemporaries 拒绝临时量:具名局部变量传入
|
// ArrayProxyNoTemporaries 拒绝临时量:具名局部变量传入
|
||||||
vk::Bool32 blendEnable = VK_FALSE;
|
constexpr vk::Bool32 blendEnable = VK_FALSE;
|
||||||
vk::ColorComponentFlags colorWriteMask =
|
constexpr vk::ColorComponentFlags colorWriteMask =
|
||||||
vk::ColorComponentFlagBits::eR |
|
vk::ColorComponentFlagBits::eR |
|
||||||
vk::ColorComponentFlagBits::eG |
|
vk::ColorComponentFlagBits::eG |
|
||||||
vk::ColorComponentFlagBits::eB |
|
vk::ColorComponentFlagBits::eB |
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
namespace Engine::Passes
|
namespace Engine::Passes
|
||||||
{
|
{
|
||||||
TAAPass::TAAPass(FullscreenGpu& gpu, f32 historyAlpha,
|
TAAPass::TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
|
||||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||||
RG::TextureHandle taaColor)
|
RG::TextureHandle taaColor)
|
||||||
: m_Gpu(gpu), m_HistoryAlpha(historyAlpha),
|
: m_Gpu(gpu), m_Frame(frame), m_HistoryAlpha(historyAlpha),
|
||||||
m_HDR(hdrColor), m_Velocity(velocity), m_TAAColor(taaColor)
|
m_HDR(hdrColor), m_Velocity(velocity), m_TAAColor(taaColor)
|
||||||
{
|
{
|
||||||
m_Sets.resize(kMaxFramesInFlight);
|
m_Sets.resize(kMaxFramesInFlight);
|
||||||
@@ -23,9 +23,12 @@ namespace Engine::Passes
|
|||||||
{
|
{
|
||||||
rg.AddPass("TAA",
|
rg.AddPass("TAA",
|
||||||
[this](RG::RenderGraphContext& ctx) {
|
[this](RG::RenderGraphContext& ctx) {
|
||||||
|
// TAA 关闭:alpha=0 → mix(cur, hist, 0) 直通当前帧
|
||||||
|
//(抖动已由 SceneRenderer 同步停掉,画面静止无锯齿晃动)
|
||||||
|
const f32 alpha = m_Frame.taaEnabled ? m_HistoryAlpha : 0.0f;
|
||||||
m_Gpu.RecordFullscreen(ctx, ctx.View(m_TAAColor), m_Gpu.taaFrag,
|
m_Gpu.RecordFullscreen(ctx, ctx.View(m_TAAColor), m_Gpu.taaFrag,
|
||||||
m_Sets[ctx.frame.frameSlot],
|
m_Sets[ctx.frame.frameSlot],
|
||||||
FullscreenPush{0.0f, m_HistoryAlpha, 0.0f, 0.0f});
|
FullscreenPush{0.0f, alpha, 0.0f, 0.0f});
|
||||||
})
|
})
|
||||||
.Read(m_HDR, RG::ResourceUse::ShaderRead,
|
.Read(m_HDR, RG::ResourceUse::ShaderRead,
|
||||||
vk::ShaderStageFlagBits::eFragment)
|
vk::ShaderStageFlagBits::eFragment)
|
||||||
|
|||||||
@@ -3,22 +3,35 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include <Engine/Renderer/Passes/TonemapPass.h>
|
#include <Engine/Renderer/Passes/TonemapPass.h>
|
||||||
|
#include <Engine/Renderer/Cubemap.h>
|
||||||
|
#include <Engine/Vulkan/DescriptorManager.h>
|
||||||
#include <Engine/Core/FrameContext.h>
|
#include <Engine/Core/FrameContext.h>
|
||||||
|
|
||||||
namespace Engine::Passes
|
namespace Engine::Passes
|
||||||
{
|
{
|
||||||
TonemapPass::TonemapPass(FullscreenGpu& gpu, PostProcessParams& params,
|
TonemapPass::TonemapPass(FullscreenGpu& gpu, DescriptorManager& descriptors,
|
||||||
|
PostProcessParams& params,
|
||||||
|
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 backbuffer,
|
||||||
: m_Gpu(gpu), m_Params(params),
|
RG::TextureHandle rawHdr,
|
||||||
|
RG::TextureHandle velocity,
|
||||||
|
RG::TextureHandle shadowMap0,
|
||||||
|
RG::TextureHandle shadowMap1,
|
||||||
|
RG::TextureHandle shadowMap2)
|
||||||
|
: m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame),
|
||||||
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
|
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
|
||||||
m_Backbuffer(backbuffer)
|
m_Backbuffer(backbuffer),
|
||||||
|
m_RawHDR(rawHdr), m_Velocity(velocity),
|
||||||
|
m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2}
|
||||||
{
|
{
|
||||||
m_Sets.resize(kMaxFramesInFlight);
|
m_Sets.resize(kMaxFramesInFlight);
|
||||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||||
m_Sets[slot] = m_Gpu.AllocateSet();
|
m_Sets[slot] = m_Gpu.AllocateSet();
|
||||||
}
|
}
|
||||||
|
// 环境调试视图源(binding 8-11)由 OnEnvironmentChanged 填充
|
||||||
|
//(Renderer 构造尾部统一广播一次)
|
||||||
}
|
}
|
||||||
|
|
||||||
void TonemapPass::Register(RG::RenderGraph& rg)
|
void TonemapPass::Register(RG::RenderGraph& rg)
|
||||||
@@ -30,17 +43,31 @@ namespace Engine::Passes
|
|||||||
m_Sets[ctx.frame.frameSlot],
|
m_Sets[ctx.frame.frameSlot],
|
||||||
FullscreenPush{m_Params.exposure,
|
FullscreenPush{m_Params.exposure,
|
||||||
m_Params.useACES ? 1.0f : 0.0f,
|
m_Params.useACES ? 1.0f : 0.0f,
|
||||||
m_Params.bloomStrength,
|
m_Frame.bloomEnabled
|
||||||
m_Params.showTemporalDiff ? 1.0f : 0.0f});
|
? m_Params.bloomStrength
|
||||||
|
: 0.0f,
|
||||||
|
static_cast<f32>(
|
||||||
|
m_Frame.debugView)});
|
||||||
})
|
})
|
||||||
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
|
.Write(m_Backbuffer, 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,
|
||||||
vk::ShaderStageFlagBits::eFragment)
|
vk::ShaderStageFlagBits::eFragment)
|
||||||
// shader 静态采样 binding2(调试视图开关在 push constant 里),
|
// 调试视图源全部声明读:路由开关在 push constant(每帧变),
|
||||||
// 声明读否则本 pass 的产出无人消费会被剔除、镜像也不会分配
|
// 绑定集与 barrier 必须静态成立——这是"声明超集、运行时路由"
|
||||||
|
// 模式,避免切换视图时重绑描述符
|
||||||
.Read(m_TemporalDiff, RG::ResourceUse::ShaderRead,
|
.Read(m_TemporalDiff, RG::ResourceUse::ShaderRead,
|
||||||
|
vk::ShaderStageFlagBits::eFragment)
|
||||||
|
.Read(m_RawHDR, RG::ResourceUse::ShaderRead,
|
||||||
|
vk::ShaderStageFlagBits::eFragment)
|
||||||
|
.Read(m_Velocity, RG::ResourceUse::ShaderRead,
|
||||||
|
vk::ShaderStageFlagBits::eFragment)
|
||||||
|
.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);
|
vk::ShaderStageFlagBits::eFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +77,34 @@ namespace Engine::Passes
|
|||||||
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR, slot);
|
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR, slot);
|
||||||
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_A, slot);
|
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_A, slot);
|
||||||
m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TemporalDiff, slot);
|
m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TemporalDiff, slot);
|
||||||
|
m_Gpu.UpdateSetInput(m_Sets[slot], 3, rg, m_RawHDR, slot);
|
||||||
|
m_Gpu.UpdateSetInput(m_Sets[slot], 4, rg, m_Velocity, slot);
|
||||||
|
m_Gpu.UpdateSetInput(m_Sets[slot], 5, rg, m_ShadowMaps[0], slot);
|
||||||
|
m_Gpu.UpdateSetInput(m_Sets[slot], 6, rg, m_ShadowMaps[1], slot);
|
||||||
|
m_Gpu.UpdateSetInput(m_Sets[slot], 7, rg, m_ShadowMaps[2], slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TonemapPass::OnEnvironmentChanged(const Cubemap* env)
|
||||||
|
{
|
||||||
|
m_Env = env;
|
||||||
|
if (!m_Env) return;
|
||||||
|
|
||||||
|
// 环境调试视图源(binding 8-11):cube 三件套 + LUT。
|
||||||
|
// 静态绑定超集模式——只在切换环境时重绑,每帧零成本
|
||||||
|
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||||
|
m_Descriptors.UpdateTexture(m_Sets[slot], 8,
|
||||||
|
m_Env->GetSampler(),
|
||||||
|
m_Env->GetView());
|
||||||
|
m_Descriptors.UpdateTexture(m_Sets[slot], 9,
|
||||||
|
m_Env->GetSampler(),
|
||||||
|
m_Env->GetIrradianceView());
|
||||||
|
m_Descriptors.UpdateTexture(m_Sets[slot], 10,
|
||||||
|
m_Env->GetSampler(),
|
||||||
|
m_Env->GetPrefilteredView());
|
||||||
|
m_Descriptors.UpdateTexture(m_Sets[slot], 11,
|
||||||
|
m_Env->GetBrdfLutSampler(),
|
||||||
|
m_Env->GetBrdfLutView());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <Engine/Renderer/Passes/ShadowPass.h>
|
#include <Engine/Renderer/Passes/ShadowPass.h>
|
||||||
#include <Engine/Renderer/Passes/ForwardPass.h>
|
#include <Engine/Renderer/Passes/ForwardPass.h>
|
||||||
#include <Engine/Renderer/Cubemap.h>
|
#include <Engine/Renderer/Cubemap.h>
|
||||||
|
#include <Engine/Renderer/IblGenerator.h>
|
||||||
#include <Engine/Renderer/Passes/TAAPass.h>
|
#include <Engine/Renderer/Passes/TAAPass.h>
|
||||||
#include <Engine/Renderer/Passes/BloomPass.h>
|
#include <Engine/Renderer/Passes/BloomPass.h>
|
||||||
#include <Engine/Renderer/Passes/TonemapPass.h>
|
#include <Engine/Renderer/Passes/TonemapPass.h>
|
||||||
@@ -40,85 +41,10 @@
|
|||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <filesystem>
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <bit>
|
|
||||||
#include <fstream>
|
|
||||||
|
|
||||||
namespace Engine
|
namespace Engine
|
||||||
{
|
{
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// fp16 → f32(截图用,规范路径足够)
|
|
||||||
f32 HalfToFloat(u16 h)
|
|
||||||
{
|
|
||||||
const u32 sign = (h & 0x8000u) << 16u;
|
|
||||||
const u32 exp = (h >> 10u) & 0x1Fu;
|
|
||||||
const u32 man = h & 0x3FFu;
|
|
||||||
u32 bits;
|
|
||||||
if (exp == 0) {
|
|
||||||
if (man == 0) return std::bit_cast<f32>(sign);
|
|
||||||
u32 e = 127 - 15 + 1;
|
|
||||||
u32 m = man;
|
|
||||||
while (!(m & 0x400u)) { m <<= 1u; --e; }
|
|
||||||
bits = sign | (e << 23u) | ((m & 0x3FFu) << 13u);
|
|
||||||
} else if (exp == 31) {
|
|
||||||
bits = sign | 0x7F800000u | (man << 13u);
|
|
||||||
} else {
|
|
||||||
bits = sign | ((exp - 15u + 127u) << 23u) | (man << 13u);
|
|
||||||
}
|
|
||||||
return std::bit_cast<f32>(bits);
|
|
||||||
}
|
|
||||||
|
|
||||||
// RGBA16F 线性数据 → 24bpp BMP(行倒序;depthMode 读 f32 深度)
|
|
||||||
void WriteHdrBMP(const std::string& path, const void* data,
|
|
||||||
u32 w, u32 h, bool depthMode = false)
|
|
||||||
{
|
|
||||||
const auto* src = static_cast<const u16*>(data);
|
|
||||||
const u32 rowBytes = w * 3u;
|
|
||||||
const u32 pad = (4u - rowBytes % 4u) % 4u;
|
|
||||||
const u32 stride = rowBytes + pad;
|
|
||||||
const u32 size = 54u + stride * h;
|
|
||||||
|
|
||||||
std::vector<u8> bmp(size, 0);
|
|
||||||
auto put = [&bmp](u32 off, u32 v, u32 n) {
|
|
||||||
for (u32 i = 0; i < n; ++i)
|
|
||||||
bmp[off + i] = static_cast<u8>((v >> (8u * i)) & 0xFFu);
|
|
||||||
};
|
|
||||||
bmp[0] = 'B'; bmp[1] = 'M';
|
|
||||||
put(2, size, 4); put(10, 54, 4); put(14, 40, 4);
|
|
||||||
put(18, w, 4); put(22, h, 4); put(26, 1, 2); put(28, 24, 2);
|
|
||||||
put(34, stride * h, 4);
|
|
||||||
|
|
||||||
for (u32 y = 0; y < h; ++y) {
|
|
||||||
const u32 dstRow = h - 1 - y; // BMP 底行在前
|
|
||||||
for (u32 x = 0; x < w; ++x) {
|
|
||||||
const u16* px = src + (static_cast<size_t>(y) * w + x) * 4;
|
|
||||||
const u32 off = 54u + dstRow * stride + x * 3u;
|
|
||||||
for (u32 c = 0; c < 3; ++c) {
|
|
||||||
f32 v;
|
|
||||||
if (depthMode) {
|
|
||||||
// D32 buffer:f32 深度,反相显示(近=白)
|
|
||||||
v = 1.0f - reinterpret_cast<const f32*>(
|
|
||||||
data)[static_cast<size_t>(y) * w + x];
|
|
||||||
} else {
|
|
||||||
v = HalfToFloat(px[c]) * 0.6f; // 匹配默认 exposure
|
|
||||||
// ACES 近似(与 TonemapPass 同族),还原屏幕观感
|
|
||||||
v = (v * (2.51f * v + 0.03f)) /
|
|
||||||
(v * (2.43f * v + 0.59f) + 0.14f);
|
|
||||||
v = glm::pow(glm::clamp(v, 0.0f, 1.0f),
|
|
||||||
1.0f / 2.2f);
|
|
||||||
}
|
|
||||||
bmp[off + c] = static_cast<u8>(v * 255.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ofstream file(path, std::ios::binary);
|
|
||||||
file.write(reinterpret_cast<const char*>(bmp.data()),
|
|
||||||
static_cast<std::streamsize>(bmp.size()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Renderer::Renderer(Window& window)
|
Renderer::Renderer(Window& window)
|
||||||
: m_Window(window)
|
: m_Window(window)
|
||||||
{
|
{
|
||||||
@@ -148,17 +74,12 @@ namespace Engine
|
|||||||
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
|
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
|
||||||
*m_Device, *m_DescriptorManager, *m_ShaderManager);
|
*m_Device, *m_DescriptorManager, *m_ShaderManager);
|
||||||
|
|
||||||
// 3.5 环境贴图(IBL):HDR 等距柱状 → cube + 预积分。
|
// 3.5 环境预积分生成端(compute 链)+ 中性兜底四件套。
|
||||||
// 加载失败/文件缺失一律中性常数兜底(描述符恒有效)
|
// 真实环境由 Application 经 AssetManager 加载后 SetEnvironment
|
||||||
const std::string envPath = "assets/textures/venice_dawn_1_4k.hdr";
|
// (资产路径归资产层,Renderer 不感知具体文件)
|
||||||
if (std::filesystem::exists(envPath)) {
|
m_Ibl = CreateScope<IblGenerator>(
|
||||||
m_Environment = Cubemap::CreateFromHdrEquirect(
|
*m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager);
|
||||||
*m_Device, *m_Allocator, envPath, 1024);
|
m_Environment = m_Ibl->Neutral(glm::vec3(0.1f));
|
||||||
}
|
|
||||||
if (!m_Environment) {
|
|
||||||
m_Environment = Cubemap::CreateNeutral(
|
|
||||||
*m_Device, *m_Allocator, glm::vec3(0.1f));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Swapchain(只管呈现;渲染目标统一归 RenderGraph)
|
// 4. Swapchain(只管呈现;渲染目标统一归 RenderGraph)
|
||||||
m_Swapchain = CreateScope<Swapchain>(*m_Device, *m_Allocator, m_Window);
|
m_Swapchain = CreateScope<Swapchain>(*m_Device, *m_Allocator, m_Window);
|
||||||
@@ -175,22 +96,22 @@ namespace Engine
|
|||||||
// 7. 资产上传服务(渲染层对外提供的 IAssetUploader 实现)
|
// 7. 资产上传服务(渲染层对外提供的 IAssetUploader 实现)
|
||||||
m_AssetUploader = CreateScope<VulkanAssetUploader>(
|
m_AssetUploader = CreateScope<VulkanAssetUploader>(
|
||||||
*m_Device, *m_Allocator, *m_DescriptorManager,
|
*m_Device, *m_Allocator, *m_DescriptorManager,
|
||||||
m_SceneGpu->textureSetLayout);
|
m_SceneGpu->textureSetLayout, *m_Ibl);
|
||||||
|
|
||||||
// 8. RenderGraph:资源声明 + pass 注册 + 首次编译
|
// 8. RenderGraph:资源声明 + pass 注册 + 首次编译
|
||||||
ENGINE_INFO("[diag] BuildGraph begin");
|
|
||||||
m_PostParams = CreateScope<Passes::PostProcessParams>();
|
m_PostParams = CreateScope<Passes::PostProcessParams>();
|
||||||
BuildGraph();
|
BuildGraph();
|
||||||
ENGINE_INFO("[diag] BuildGraph done");
|
|
||||||
|
|
||||||
// 首次编译后物理镜像就位:各 pass 绑定自己的输入描述符
|
// 首次编译后物理镜像就位:各 pass 绑定自己的输入描述符
|
||||||
for (auto& pass : m_Passes) {
|
for (auto& pass : m_Passes) {
|
||||||
pass->OnTargetsRebuilt(*m_RenderGraph);
|
pass->OnTargetsRebuilt(*m_RenderGraph);
|
||||||
}
|
}
|
||||||
ENGINE_INFO("[diag] targets rebuilt");
|
// 环境辐照度绑定(view 在单个环境内全程稳定,绑一次)
|
||||||
// 环境辐照度绑定(view 全程稳定,绑一次)
|
|
||||||
m_SceneGpu->BindEnvironment(*m_Environment);
|
m_SceneGpu->BindEnvironment(*m_Environment);
|
||||||
ENGINE_INFO("[diag] environment bound");
|
// 环境消费端 pass(Skybox 背景 / Tonemap 调试视图)同步初始绑定
|
||||||
|
for (auto& pass : m_Passes) {
|
||||||
|
pass->OnEnvironmentChanged(m_Environment.get());
|
||||||
|
}
|
||||||
|
|
||||||
// 9. GPU timestamp:每 pass 两枚(前后),按帧槽位分区
|
// 9. GPU timestamp:每 pass 两枚(前后),按帧槽位分区
|
||||||
m_QueriesPerFrame = static_cast<u32>(m_RenderGraph->GetPassCount()) * 2;
|
m_QueriesPerFrame = static_cast<u32>(m_RenderGraph->GetPassCount()) * 2;
|
||||||
@@ -246,14 +167,59 @@ namespace Engine
|
|||||||
void Renderer::SetBloomThreshold(f32 v) { m_PostParams->bloomThreshold = v; }
|
void Renderer::SetBloomThreshold(f32 v) { m_PostParams->bloomThreshold = v; }
|
||||||
f32 Renderer::GetBloomStrength() const { return m_PostParams->bloomStrength; }
|
f32 Renderer::GetBloomStrength() const { return m_PostParams->bloomStrength; }
|
||||||
void Renderer::SetBloomStrength(f32 v) { m_PostParams->bloomStrength = v; }
|
void Renderer::SetBloomStrength(f32 v) { m_PostParams->bloomStrength = v; }
|
||||||
bool Renderer::GetShowTemporalDiff() const { return m_PostParams->showTemporalDiff; }
|
|
||||||
void Renderer::SetShowTemporalDiff(bool b) { m_PostParams->showTemporalDiff = b; }
|
// ---- 特性开关 / 调试视图:转发 SceneRenderer(状态唯一持有者)----
|
||||||
|
bool Renderer::GetShadowsEnabled() const
|
||||||
|
{
|
||||||
|
return m_SceneRenderer->Frame().shadowsEnabled;
|
||||||
|
}
|
||||||
|
void Renderer::SetShadowsEnabled(bool b) { m_SceneRenderer->SetShadowsEnabled(b); }
|
||||||
|
bool Renderer::GetTAAEnabled() const { return m_SceneRenderer->Frame().taaEnabled; }
|
||||||
|
void Renderer::SetTAAEnabled(bool b) { m_SceneRenderer->SetTAAEnabled(b); }
|
||||||
|
bool Renderer::GetBloomEnabled() const
|
||||||
|
{
|
||||||
|
return m_SceneRenderer->Frame().bloomEnabled;
|
||||||
|
}
|
||||||
|
void Renderer::SetBloomEnabled(bool b) { m_SceneRenderer->SetBloomEnabled(b); }
|
||||||
|
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
|
||||||
|
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); }
|
||||||
|
|
||||||
|
void Renderer::SetEnvironment(const Cubemap* env)
|
||||||
|
{
|
||||||
|
if (!env) env = m_Environment.get();
|
||||||
|
|
||||||
|
// 描述符 host 更新要求 set 不被 in-flight 帧使用(validation 严格
|
||||||
|
// 校验):waitIdle 两槽位全停飞后统一换绑。环境切换低频,等待
|
||||||
|
// 成本 ≈ 单帧 GPU 尾巴;生成端(compute 预积分)已在资产加载期完成
|
||||||
|
m_Device->GetDevice().waitIdle();
|
||||||
|
m_SceneGpu->BindEnvironment(*env);
|
||||||
|
for (auto& pass : m_Passes) {
|
||||||
|
pass->OnEnvironmentChanged(env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<const char*>& Renderer::GetDebugViewNames() const
|
||||||
|
{
|
||||||
|
static const std::vector<const char*> kNames = {
|
||||||
|
"Final", "TemporalDiff", "HDR", "TAAColor", "Bloom",
|
||||||
|
"Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2",
|
||||||
|
"EnvCube", "Irradiance", "Prefilter", "BrdfLut",
|
||||||
|
};
|
||||||
|
static_assert(int(Passes::kDebugViewCount) == 13,
|
||||||
|
"DebugView 枚举与名表同步");
|
||||||
|
return kNames;
|
||||||
|
}
|
||||||
|
|
||||||
u32 Renderer::GetMainInstanceCount() const
|
u32 Renderer::GetMainInstanceCount() const
|
||||||
{
|
{
|
||||||
return m_SceneRenderer->GetMainInstanceCount();
|
return m_SceneRenderer->GetMainInstanceCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Renderer::GetInstancesTruncated() const
|
||||||
|
{
|
||||||
|
return m_SceneRenderer->Frame().instancesTruncated;
|
||||||
|
}
|
||||||
|
|
||||||
u32 Renderer::GetShadowInstanceCount() const
|
u32 Renderer::GetShadowInstanceCount() const
|
||||||
{
|
{
|
||||||
return m_SceneRenderer->GetShadowInstanceCount();
|
return m_SceneRenderer->GetShadowInstanceCount();
|
||||||
@@ -290,11 +256,9 @@ namespace Engine
|
|||||||
|
|
||||||
// HDR 颜色图:Forward 的输出(线性光照值 + 抖动),TAA 显式完成
|
// HDR 颜色图:Forward 的输出(线性光照值 + 抖动),TAA 显式完成
|
||||||
// 时域累积后写入 TAAColor(对齐 UE/Unity 管线)
|
// 时域累积后写入 TAAColor(对齐 UE/Unity 管线)
|
||||||
// extraUsage:调试截图需要 TRANSFER_SRC(第 60 帧 copy 到 buffer)
|
|
||||||
m_HDRColor = m_RenderGraph->CreateTexture(
|
m_HDRColor = m_RenderGraph->CreateTexture(
|
||||||
"HDRColor",
|
"HDRColor",
|
||||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat, 0, 0, 1.0f,
|
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat});
|
||||||
vk::ImageUsageFlagBits::eTransferSrc});
|
|
||||||
|
|
||||||
// velocity:clip 空间运动向量(TAA 反推历史像素位置)
|
// velocity:clip 空间运动向量(TAA 反推历史像素位置)
|
||||||
m_Velocity = m_RenderGraph->CreateTexture(
|
m_Velocity = m_RenderGraph->CreateTexture(
|
||||||
@@ -332,15 +296,17 @@ namespace Engine
|
|||||||
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
|
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
|
||||||
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps));
|
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps));
|
||||||
m_Passes.push_back(CreateScope<Passes::TAAPass>(
|
m_Passes.push_back(CreateScope<Passes::TAAPass>(
|
||||||
*m_FullscreenGpu, /*historyAlpha*/ 0.9f, m_HDRColor, m_Velocity,
|
*m_FullscreenGpu, frame, /*historyAlpha*/ 0.9f, m_HDRColor,
|
||||||
m_TAAColor));
|
m_Velocity, m_TAAColor));
|
||||||
m_Passes.push_back(CreateScope<Passes::TemporalDiffPass>(
|
m_Passes.push_back(CreateScope<Passes::TemporalDiffPass>(
|
||||||
*m_FullscreenGpu, /*gain*/ 8.0f, m_HDRColor, m_TemporalDiff));
|
*m_FullscreenGpu, /*gain*/ 8.0f, m_HDRColor, m_TemporalDiff));
|
||||||
m_Passes.push_back(CreateScope<Passes::BloomPass>(
|
m_Passes.push_back(CreateScope<Passes::BloomPass>(
|
||||||
*m_FullscreenGpu, *m_PostParams, m_TAAColor, m_BloomA, m_BloomB));
|
*m_FullscreenGpu, *m_PostParams, frame, m_TAAColor, m_BloomA,
|
||||||
|
m_BloomB));
|
||||||
m_Passes.push_back(CreateScope<Passes::TonemapPass>(
|
m_Passes.push_back(CreateScope<Passes::TonemapPass>(
|
||||||
*m_FullscreenGpu, *m_PostParams, m_TAAColor, m_BloomA,
|
*m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame,
|
||||||
m_TemporalDiff, m_Backbuffer));
|
m_TAAColor, m_BloomA, m_TemporalDiff, m_Backbuffer, m_HDRColor,
|
||||||
|
m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2]));
|
||||||
m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
|
m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
|
||||||
&m_ImGuiLayer, m_Backbuffer));
|
&m_ImGuiLayer, m_Backbuffer));
|
||||||
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
|
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
|
||||||
@@ -472,68 +438,6 @@ namespace Engine
|
|||||||
|
|
||||||
m_RenderGraph->Execute(frameCtx);
|
m_RenderGraph->Execute(frameCtx);
|
||||||
|
|
||||||
// 调试截图:帧尾把 TAAColor 拷进 host buffer(一次)
|
|
||||||
++m_FrameCounter;
|
|
||||||
const bool doCapture = !m_Captured && m_FrameCounter == 60;
|
|
||||||
vk::Image capImage{};
|
|
||||||
vk::Extent2D capExtent{};
|
|
||||||
vk::Format capFormat{};
|
|
||||||
vk::ImageLayout capResting{};
|
|
||||||
if (doCapture &&
|
|
||||||
m_RenderGraph->GetCaptureTarget(m_HDRColor, frameSlot,
|
|
||||||
capImage, capExtent, capFormat, capResting)) {
|
|
||||||
const vk::DeviceSize capBytes = static_cast<vk::DeviceSize>(
|
|
||||||
capExtent.width) * capExtent.height * 8; // RGBA16F
|
|
||||||
const bool capDepth = capFormat == vk::Format::eD32Sfloat;
|
|
||||||
|
|
||||||
m_CaptureAlloc = m_Allocator->CreateBuffer(
|
|
||||||
capBytes,
|
|
||||||
vk::BufferUsageFlagBits::eTransferDst,
|
|
||||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
|
||||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
|
||||||
|
|
||||||
// resting → TRANSFER_SRC → 拷贝 → 恢复 resting
|
|
||||||
const vk::ImageAspectFlagBits capAspect =
|
|
||||||
capDepth ? vk::ImageAspectFlagBits::eDepth
|
|
||||||
: vk::ImageAspectFlagBits::eColor;
|
|
||||||
vk::ImageMemoryBarrier2 toSrc;
|
|
||||||
toSrc.setSrcStageMask(vk::PipelineStageFlagBits2::eAllCommands)
|
|
||||||
.setSrcAccessMask(vk::AccessFlagBits2::eMemoryWrite)
|
|
||||||
.setDstStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
|
||||||
.setDstAccessMask(vk::AccessFlagBits2::eTransferRead)
|
|
||||||
.setOldLayout(capResting)
|
|
||||||
.setNewLayout(vk::ImageLayout::eTransferSrcOptimal)
|
|
||||||
.setImage(capImage)
|
|
||||||
.setSubresourceRange({capAspect, 0, 1, 0, 1});
|
|
||||||
frameCtx.cmd.pipelineBarrier2(
|
|
||||||
vk::DependencyInfo{}.setImageMemoryBarriers(toSrc));
|
|
||||||
|
|
||||||
vk::BufferImageCopy capRegion;
|
|
||||||
capRegion.setBufferOffset(0)
|
|
||||||
.setBufferRowLength(0)
|
|
||||||
.setBufferImageHeight(0)
|
|
||||||
.setImageSubresource({capAspect, 0, 0, 1})
|
|
||||||
.setImageOffset({0, 0, 0})
|
|
||||||
.setImageExtent({capExtent.width, capExtent.height, 1});
|
|
||||||
frameCtx.cmd.copyImageToBuffer(
|
|
||||||
capImage, vk::ImageLayout::eTransferSrcOptimal,
|
|
||||||
m_CaptureAlloc.Buffer, capRegion);
|
|
||||||
|
|
||||||
vk::ImageMemoryBarrier2 back;
|
|
||||||
back.setSrcStageMask(vk::PipelineStageFlagBits2::eTransfer)
|
|
||||||
.setSrcAccessMask(vk::AccessFlagBits2::eTransferRead)
|
|
||||||
.setDstStageMask(vk::PipelineStageFlagBits2::eAllCommands)
|
|
||||||
.setDstAccessMask(vk::AccessFlagBits2::eMemoryRead |
|
|
||||||
vk::AccessFlagBits2::eMemoryWrite)
|
|
||||||
.setOldLayout(vk::ImageLayout::eTransferSrcOptimal)
|
|
||||||
.setNewLayout(capResting)
|
|
||||||
.setImage(capImage)
|
|
||||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
|
||||||
0, 1, 0, 1});
|
|
||||||
frameCtx.cmd.pipelineBarrier2(
|
|
||||||
vk::DependencyInfo{}.setImageMemoryBarriers(back));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_CommandManager->EndFrame(frameSlot);
|
m_CommandManager->EndFrame(frameSlot);
|
||||||
|
|
||||||
// 5. 提交
|
// 5. 提交
|
||||||
@@ -577,21 +481,6 @@ namespace Engine
|
|||||||
HandleResize();
|
HandleResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调试截图落盘:等 fence(copy 已提交完成)→ fp16 → BMP
|
|
||||||
if (doCapture) {
|
|
||||||
// 诊断截图落盘:等 fence(copy 已提交完成)→ BMP(返回值仅超时标志)
|
|
||||||
(void)m_Device->GetDevice().waitForFences(
|
|
||||||
m_SyncManager->GetFrameFence(frameSlot), VK_TRUE,
|
|
||||||
UINT64_MAX);
|
|
||||||
WriteHdrBMP("capture.bmp", m_CaptureAlloc.MappedPtr,
|
|
||||||
capExtent.width, capExtent.height, false);
|
|
||||||
m_Allocator->Destroy(m_CaptureAlloc);
|
|
||||||
m_Captured = true;
|
|
||||||
ENGINE_INFO("Debug capture written: capture.bmp (HDRColor) "
|
|
||||||
"shadowInstances={}",
|
|
||||||
m_SceneRenderer->GetShadowInstanceCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
|
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ namespace Engine
|
|||||||
ubo.projNoJitter = ubo.proj;
|
ubo.projNoJitter = ubo.proj;
|
||||||
|
|
||||||
// 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。
|
// 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。
|
||||||
// NDC 偏移 = 像素偏移 × 2 / 尺寸
|
// NDC 偏移 = 像素偏移 × 2 / 尺寸。TAA 关闭时停抖(直通画面静止)
|
||||||
{
|
if (m_TAAEnabled) {
|
||||||
const u32 idx = static_cast<u32>(m_FrameIndex % 8);
|
const u32 idx = static_cast<u32>(m_FrameIndex % 8);
|
||||||
const f32 jx = Halton(idx + 1, 2) - 0.5f;
|
const f32 jx = Halton(idx + 1, 2) - 0.5f;
|
||||||
const f32 jy = Halton(idx + 1, 3) - 0.5f;
|
const f32 jy = Halton(idx + 1, 3) - 0.5f;
|
||||||
@@ -166,7 +166,16 @@ namespace Engine
|
|||||||
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(targetExtent.height);
|
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(targetExtent.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
ubo.ambientColor = scene.GetSettings().ambientColor;
|
ubo.ambientColor = scene.GetSettings().AmbientColor;
|
||||||
|
|
||||||
|
// ---- 运行时开关/调试视图落帧(各 pass 与 frag 只读契约字段)----
|
||||||
|
m_Frame.ClearColor = scene.GetSettings().ClearColor;
|
||||||
|
m_Frame.DrawSkybox = scene.GetSettings().DrawSkybox;
|
||||||
|
m_Frame.shadowsEnabled = m_ShadowsEnabled;
|
||||||
|
m_Frame.taaEnabled = m_TAAEnabled;
|
||||||
|
m_Frame.bloomEnabled = m_BloomEnabled;
|
||||||
|
m_Frame.debugView = m_DebugView;
|
||||||
|
ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f;
|
||||||
|
|
||||||
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
|
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
|
||||||
// 反投影远平面点 = 相机相对世界方向,frag normalize 后采样 cube
|
// 反投影远平面点 = 相机相对世界方向,frag normalize 后采样 cube
|
||||||
@@ -296,16 +305,24 @@ namespace Engine
|
|||||||
instanceTruncated = true;
|
instanceTruncated = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// lookup 值 = batches 索引(此前误存 worlds 索引:
|
||||||
|
// 单实体时两序号恰好恒等,多实体共享 mesh 后分叉
|
||||||
|
// → batches[worldsIdx] 越界)。batch.first(GPU
|
||||||
|
// firstInstance 基址)才是 worlds 索引,语义别混
|
||||||
const auto [it, inserted] = s.lookup.emplace(
|
const auto [it, inserted] = s.lookup.emplace(
|
||||||
mesh.get(), static_cast<u32>(s.worlds.size()));
|
mesh.get(), static_cast<u32>(s.result.batches.size()));
|
||||||
if (inserted) {
|
if (inserted) {
|
||||||
s.result.batches.push_back({mesh.get(),
|
s.result.batches.push_back({mesh.get(), 0, 0});
|
||||||
static_cast<u32>(s.worlds.size()), 0});
|
s.batchWorlds.emplace_back();
|
||||||
|
s.batchPrevWorlds.emplace_back();
|
||||||
}
|
}
|
||||||
s.result.batches[it->second].count++;
|
// 实例归入自己的 batch(mesh 分组),保证同 batch
|
||||||
|
// 实例在扁平化后连续——firstInstance 语义的前提
|
||||||
|
const u32 batchIdx = it->second;
|
||||||
|
s.result.batches[batchIdx].count++;
|
||||||
|
s.batchWorlds[batchIdx].push_back(t.World());
|
||||||
|
s.batchPrevWorlds[batchIdx].push_back(prevWorld);
|
||||||
s.result.instanceCount++;
|
s.result.instanceCount++;
|
||||||
s.worlds.push_back(t.World());
|
|
||||||
s.prevWorlds.push_back(prevWorld);
|
|
||||||
++totalInstances;
|
++totalInstances;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,21 +333,25 @@ namespace Engine
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 4. 拼接:[主段|级联0|级联1|级联2],batch.first 加段基址 ----
|
// ---- 4. 拼接:[主段|级联0|级联1|级联2],batch 内实例连续扁平化 ----
|
||||||
auto appendView = [&](CullResult& dst, ViewScratch& s) {
|
auto appendView = [&](CullResult& dst, ViewScratch& s) {
|
||||||
const u32 base = static_cast<u32>(m_Frame.instanceModels.size());
|
u32 base = static_cast<u32>(m_Frame.instanceModels.size());
|
||||||
dst.batches.clear();
|
dst.batches.clear();
|
||||||
dst.batches.reserve(s.result.batches.size());
|
dst.batches.reserve(s.result.batches.size());
|
||||||
for (auto& b : s.result.batches) {
|
for (size_t i = 0; i < s.result.batches.size(); ++i) {
|
||||||
b.first += base;
|
InstanceBatch b = s.result.batches[i];
|
||||||
|
b.first = base; // 扁平化后的连续区间基址
|
||||||
|
base += b.count;
|
||||||
dst.batches.push_back(b);
|
dst.batches.push_back(b);
|
||||||
|
m_Frame.instanceModels.insert(m_Frame.instanceModels.end(),
|
||||||
|
s.batchWorlds[i].begin(),
|
||||||
|
s.batchWorlds[i].end());
|
||||||
|
m_Frame.prevInstanceModels.insert(
|
||||||
|
m_Frame.prevInstanceModels.end(),
|
||||||
|
s.batchPrevWorlds[i].begin(),
|
||||||
|
s.batchPrevWorlds[i].end());
|
||||||
}
|
}
|
||||||
dst.instanceCount = s.result.instanceCount;
|
dst.instanceCount = s.result.instanceCount;
|
||||||
m_Frame.instanceModels.insert(m_Frame.instanceModels.end(),
|
|
||||||
s.worlds.begin(), s.worlds.end());
|
|
||||||
m_Frame.prevInstanceModels.insert(m_Frame.prevInstanceModels.end(),
|
|
||||||
s.prevWorlds.begin(),
|
|
||||||
s.prevWorlds.end());
|
|
||||||
};
|
};
|
||||||
appendView(m_Frame.mainView, m_MainScratch);
|
appendView(m_Frame.mainView, m_MainScratch);
|
||||||
for (u32 c = 0; c < kMaxCascades; ++c) {
|
for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||||
@@ -344,11 +365,13 @@ namespace Engine
|
|||||||
m_LastProjNoJitter = ubo.projNoJitter;
|
m_LastProjNoJitter = ubo.projNoJitter;
|
||||||
++m_FrameIndex;
|
++m_FrameIndex;
|
||||||
|
|
||||||
|
// 截断状态每帧落遥测(ImGui 可见)——"只告警一次"的日志对
|
||||||
|
// 大场景不够:预算是动态穿越的,用户必须实时看到截断发生
|
||||||
|
m_Frame.instancesTruncated = instanceTruncated;
|
||||||
if (instanceTruncated && !m_TruncationWarned) {
|
if (instanceTruncated && !m_TruncationWarned) {
|
||||||
// 只告警一次(每帧告警会刷屏)
|
|
||||||
m_TruncationWarned = true;
|
m_TruncationWarned = true;
|
||||||
ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated",
|
ENGINE_WARN("GPUDriven: instance buffer full ({}), entities truncated "
|
||||||
kMaxInstances);
|
"(见 Scene Debug 面板红字)", kMaxInstances);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Frame.collectMs = std::chrono::duration<f32, std::milli>(
|
m_Frame.collectMs = std::chrono::duration<f32, std::milli>(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <Engine/Renderer/VulkanAssetUploader.h>
|
#include <Engine/Renderer/VulkanAssetUploader.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/Model.h>
|
||||||
|
#include <Engine/Renderer/IblGenerator.h>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
@@ -14,10 +15,31 @@ namespace Engine {
|
|||||||
m_Device, m_Allocator, m_Descriptors, m_TextureLayout, path);
|
m_Device, m_Allocator, m_Descriptors, m_TextureLayout, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scope<Model> VulkanAssetUploader::LoadModel(const std::string& path,
|
||||||
|
ParsedModel&& parsed)
|
||||||
|
{
|
||||||
|
// 异步路径:assimp 解析已在 worker 线程完成,此处只剩 GPU 构建
|
||||||
|
return Model::FromParsed(m_Device, m_Allocator, m_Descriptors,
|
||||||
|
m_TextureLayout, std::move(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
Scope<Model> VulkanAssetUploader::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
|
Scope<Model> VulkanAssetUploader::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
|
||||||
{
|
{
|
||||||
return Model::CreatePlane(m_Device, m_Allocator, m_Descriptors,
|
return Model::CreatePlane(m_Device, m_Allocator, m_Descriptors,
|
||||||
m_TextureLayout, halfSize, r, g, b);
|
m_TextureLayout, halfSize, r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
|
||||||
|
{
|
||||||
|
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
|
||||||
|
return m_Ibl.FromHdrEquirect(path, 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(
|
||||||
|
const std::string& path, DecodedEquirect&& decoded)
|
||||||
|
{
|
||||||
|
// 异步路径:decode 已在 worker 线程完成,此处只剩 GPU 段
|
||||||
|
return m_Ibl.FromDecoded(decoded, 1024, path);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -25,10 +25,11 @@ namespace Engine
|
|||||||
|
|
||||||
vk::DescriptorPool DescriptorManager::CreateChunk() const
|
vk::DescriptorPool DescriptorManager::CreateChunk() const
|
||||||
{
|
{
|
||||||
std::array<vk::DescriptorPoolSize, 3> poolSizes = {
|
std::array<vk::DescriptorPoolSize, 4> poolSizes = {
|
||||||
vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, kChunkSets),
|
vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, kChunkSets),
|
||||||
vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, kChunkSets),
|
vk::DescriptorPoolSize(vk::DescriptorType::eCombinedImageSampler, kChunkSets),
|
||||||
vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, kChunkSets),
|
vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, kChunkSets),
|
||||||
|
vk::DescriptorPoolSize(vk::DescriptorType::eStorageImage, kChunkSets),
|
||||||
};
|
};
|
||||||
|
|
||||||
vk::DescriptorPoolCreateInfo poolInfo;
|
vk::DescriptorPoolCreateInfo poolInfo;
|
||||||
@@ -133,6 +134,26 @@ namespace Engine
|
|||||||
m_Device.GetDevice().updateDescriptorSets(write, {});
|
m_Device.GetDevice().updateDescriptorSets(write, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DescriptorManager::UpdateStorageImage(const vk::DescriptorSet set,
|
||||||
|
const u32 binding,
|
||||||
|
const vk::ImageView view) const
|
||||||
|
{
|
||||||
|
// storage image 写(compute 输出):layout 必须 eGeneral
|
||||||
|
vk::DescriptorImageInfo imageInfo;
|
||||||
|
imageInfo.setImageView(view)
|
||||||
|
.setImageLayout(vk::ImageLayout::eGeneral);
|
||||||
|
|
||||||
|
vk::WriteDescriptorSet write;
|
||||||
|
write.setDstSet(set)
|
||||||
|
.setDstBinding(binding)
|
||||||
|
.setDstArrayElement(0)
|
||||||
|
.setDescriptorType(vk::DescriptorType::eStorageImage)
|
||||||
|
.setDescriptorCount(1)
|
||||||
|
.setImageInfo(imageInfo);
|
||||||
|
|
||||||
|
m_Device.GetDevice().updateDescriptorSets(write, {});
|
||||||
|
}
|
||||||
|
|
||||||
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(vk::DescriptorSetLayout layout,
|
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(vk::DescriptorSetLayout layout,
|
||||||
const u32 pushConstantSize) const
|
const u32 pushConstantSize) const
|
||||||
{
|
{
|
||||||
@@ -154,18 +175,18 @@ namespace Engine
|
|||||||
}
|
}
|
||||||
|
|
||||||
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(const std::vector<vk::DescriptorSetLayout>& layouts,
|
vk::PipelineLayout DescriptorManager::CreatePipelineLayout(const std::vector<vk::DescriptorSetLayout>& layouts,
|
||||||
const u32 pushConstantSize) const
|
const u32 pushConstantSize,
|
||||||
|
const vk::ShaderStageFlags pushStages) const
|
||||||
{
|
{
|
||||||
vk::PipelineLayoutCreateInfo info;
|
vk::PipelineLayoutCreateInfo info;
|
||||||
info.setSetLayouts(layouts);
|
info.setSetLayouts(layouts);
|
||||||
|
|
||||||
std::array<vk::PushConstantRange, 1> pushRanges;
|
std::array<vk::PushConstantRange, 1> pushRanges;
|
||||||
if (pushConstantSize > 0) {
|
if (pushConstantSize > 0) {
|
||||||
// 与 ShaderManager 一致:程序级 vertex|fragment 声明。
|
// 与 ShaderManager 一致:程序级声明(默认 vertex|fragment)。
|
||||||
// 注意 vkCmdPushConstants 的 stageFlags 必须覆盖 range 的
|
// 注意 vkCmdPushConstants 的 stageFlags 必须覆盖 range 的
|
||||||
// 全部 stage(VUID-01796),因此 push 调用点也要带双 stage
|
// 全部 stage(VUID-01796),因此 push 调用点也要带同样的 stage
|
||||||
pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex |
|
pushRanges[0].setStageFlags(pushStages)
|
||||||
vk::ShaderStageFlagBits::eFragment)
|
|
||||||
.setOffset(0)
|
.setOffset(0)
|
||||||
.setSize(pushConstantSize);
|
.setSize(pushConstantSize);
|
||||||
info.setPushConstantRanges(pushRanges);
|
info.setPushConstantRanges(pushRanges);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector<u32>& spirv,
|
|||||||
if (stage == vk::ShaderStageFlagBits::eVertex) {
|
if (stage == vk::ShaderStageFlagBits::eVertex) {
|
||||||
nextStage = vk::ShaderStageFlagBits::eFragment;
|
nextStage = vk::ShaderStageFlagBits::eFragment;
|
||||||
}
|
}
|
||||||
|
// compute 单阶段:无 nextStage(VUID-09183 只约束 graphics 链)
|
||||||
|
|
||||||
vk::ShaderCreateInfoEXT createInfo;
|
vk::ShaderCreateInfoEXT createInfo;
|
||||||
createInfo.setStage(stage)
|
createInfo.setStage(stage)
|
||||||
@@ -60,8 +61,11 @@ ShaderObject ShaderManager::CreateFromSPIRV(const std::vector<u32>& spirv,
|
|||||||
// push range 是"程序级"声明:shader object 要求同一程序绑定的
|
// push range 是"程序级"声明:shader object 要求同一程序绑定的
|
||||||
// 全部 shader(vert+frag)push range 定义完全一致(VUID-08878),
|
// 全部 shader(vert+frag)push range 定义完全一致(VUID-08878),
|
||||||
// 因此统一按 vertex|fragment 声明;shader 实际读取哪些字节由
|
// 因此统一按 vertex|fragment 声明;shader 实际读取哪些字节由
|
||||||
// GLSL 的 push_constant block 决定,声明范围只是能力上限
|
// GLSL 的 push_constant block 决定,声明范围只是能力上限。
|
||||||
pushRanges[0].setStageFlags(vk::ShaderStageFlagBits::eVertex |
|
// compute 无程序链,单独按 eCompute 声明(layout/push 调用点一致)
|
||||||
|
pushRanges[0].setStageFlags(stage == vk::ShaderStageFlagBits::eCompute
|
||||||
|
? vk::ShaderStageFlagBits::eCompute
|
||||||
|
: vk::ShaderStageFlagBits::eVertex |
|
||||||
vk::ShaderStageFlagBits::eFragment)
|
vk::ShaderStageFlagBits::eFragment)
|
||||||
.setOffset(0)
|
.setOffset(0)
|
||||||
.setSize(pushConstantSize);
|
.setSize(pushConstantSize);
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// IBL ④:BRDF 积分 LUT(2D,纯数学,与环境内容无关)。
|
||||||
|
// (NdotV, roughness) → (F 缩放 A, F 偏置 B):specular = prefiltered·(F·A + B)。
|
||||||
|
// 与环境切换无关,但随生成链一起跑(GPU 亚毫秒级),省去跨环境缓存。
|
||||||
|
|
||||||
|
#include "ibl_common.glsl"
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0, rg16f) uniform restrict writeonly image2D outLut;
|
||||||
|
|
||||||
|
const uint SAMPLES = 128;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec2 gid = ivec2(gl_GlobalInvocationID);
|
||||||
|
if (gid.x >= imageSize(outLut).x || gid.y >= imageSize(outLut).y) return;
|
||||||
|
|
||||||
|
vec2 uv = (vec2(gid) + 0.5) / vec2(imageSize(outLut));
|
||||||
|
float nDotV = uv.x;
|
||||||
|
float roughness = uv.y;
|
||||||
|
|
||||||
|
// V 在以 N 为 +Z 的切线空间里(积分只依赖这两角度)
|
||||||
|
vec3 V = vec3(sqrt(1.0 - nDotV * nDotV), 0.0, nDotV);
|
||||||
|
vec3 Nz = vec3(0.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
float a = 0.0, b = 0.0;
|
||||||
|
float k = roughness * roughness * 0.5; // IBL 版 Smith k
|
||||||
|
|
||||||
|
for (uint i = 0u; i < SAMPLES; ++i) {
|
||||||
|
vec3 H = iblImportanceSampleGGX(iblHammersley(i, SAMPLES), Nz, roughness);
|
||||||
|
vec3 L = normalize(2.0 * dot(V, H) * H - V);
|
||||||
|
|
||||||
|
float lDotN = L.z;
|
||||||
|
float nDotL = max(lDotN, 0.0);
|
||||||
|
float nDotH = max(H.z, 0.0);
|
||||||
|
float vDotH = max(dot(V, H), 0.0);
|
||||||
|
if (nDotL <= 0.0) continue;
|
||||||
|
|
||||||
|
float g = (nDotV / (nDotV * (1.0 - k) + k)) *
|
||||||
|
(nDotL / (nDotL * (1.0 - k) + k));
|
||||||
|
// 高 roughness 时部分样本 H.z 恰为 0(g 也为 0),0/0 = NaN
|
||||||
|
// 会累进 LUT——分母钳 epsilon(与 CPU 版一致)
|
||||||
|
float gVis = g * vDotH / max(nDotH * nDotV, 1e-6);
|
||||||
|
float fc = 1.0 - vDotH;
|
||||||
|
float f = fc * fc * fc * fc * fc; // (1-vDotH)⁵
|
||||||
|
|
||||||
|
a += (1.0 - f) * gVis;
|
||||||
|
b += f * gVis;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageStore(outLut, gid, vec4(a / float(SAMPLES), b / float(SAMPLES), 0.0, 1.0));
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// ibl_common:IBL compute 预积分共享数学(与原 CPU 实现逐式对齐)。
|
||||||
|
//
|
||||||
|
// 被 4 个 .comp 包含(glslc 相对 include)。改造自 Cubemap.cpp 的
|
||||||
|
// CPU 积分路径——公式一致保证视觉无回归,可直接对比验证。
|
||||||
|
|
||||||
|
#ifndef IBL_COMMON_GLSL
|
||||||
|
#define IBL_COMMON_GLSL
|
||||||
|
|
||||||
|
const float IBL_PI = 3.14159265358979;
|
||||||
|
|
||||||
|
// ---- GL/Vulkan cube 面方向表(与 GL spec 立方体选择表互逆)----
|
||||||
|
// (u,v)∈[0,1](u 向右 v 向下)→ s,t∈[-1,1] → 单位方向
|
||||||
|
vec3 iblFaceDir(uint face, float u, float v)
|
||||||
|
{
|
||||||
|
float s = u * 2.0 - 1.0;
|
||||||
|
float t = v * 2.0 - 1.0;
|
||||||
|
vec3 dir;
|
||||||
|
if (face == 0u) dir = vec3( 1.0, -t, -s); // +X(面片顶行 = +Y)
|
||||||
|
else if (face == 1u) dir = vec3(-1.0, -t, s); // -X
|
||||||
|
else if (face == 2u) dir = vec3( s, 1.0, t); // +Y
|
||||||
|
else if (face == 3u) dir = vec3( s,-1.0, -t); // -Y
|
||||||
|
else if (face == 4u) dir = vec3( s, -t, 1.0); // +Z
|
||||||
|
else dir = vec3(-s, -t,-1.0); // -Z
|
||||||
|
return normalize(dir); // equirect 查表要求单位方向
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单位方向 → 等距柱状 uv。行 0 = 天顶(+Y):v = acos(y)/π,
|
||||||
|
// 与 stbi 行序一致;u 经度环绕交给采样器 S = eRepeat 处理接缝
|
||||||
|
vec2 iblDirToEquirectUv(vec3 dir)
|
||||||
|
{
|
||||||
|
float u = atan(dir.z, dir.x) / (2.0 * IBL_PI) + 0.5;
|
||||||
|
float v = acos(clamp(dir.y, -1.0, 1.0)) / IBL_PI;
|
||||||
|
return vec2(u, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// van der Corput 基底反转。硬件位反转指令替代 CPU 手写交换链;
|
||||||
|
// 整数值转浮点(不是位型重解读),序列与 CPU 版一致
|
||||||
|
vec2 iblHammersley(uint i, uint n)
|
||||||
|
{
|
||||||
|
return vec2(float(i) / float(n),
|
||||||
|
float(bitfieldReverse(i)) * 2.3283064365386963e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GGX NDF 按概率密度取半程向量 H(切线空间 → 世界)
|
||||||
|
vec3 iblImportanceSampleGGX(vec2 xi, vec3 N, float roughness)
|
||||||
|
{
|
||||||
|
float a = roughness * roughness;
|
||||||
|
float phi = 2.0 * IBL_PI * xi.x;
|
||||||
|
float cosTheta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));
|
||||||
|
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
|
||||||
|
|
||||||
|
vec3 H = vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
|
||||||
|
|
||||||
|
vec3 up = abs(N.z) > 0.999 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
|
||||||
|
vec3 T = normalize(cross(up, N));
|
||||||
|
vec3 B = cross(N, T);
|
||||||
|
return normalize(T * H.x + B * H.y + N * H.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // IBL_COMMON_GLSL
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// IBL ①:等距柱状 HDR → 环境 cube(6 面重采样)。
|
||||||
|
// 替代原 CPU 逐像素 SampleEquirect 双线性查表——GPU 上直接用硬件
|
||||||
|
// 线性过滤采样(经度接缝由 S=eRepeat 处理),观感与 CPU 版一致。
|
||||||
|
// 输出为 image2DArray(layer = face),图像本体带 CUBE_COMPATIBLE,
|
||||||
|
// 采样端另建 eCube view。
|
||||||
|
|
||||||
|
#include "ibl_common.glsl"
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform sampler2D equirect;
|
||||||
|
layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec3 gid = ivec3(gl_GlobalInvocationID);
|
||||||
|
ivec2 size = imageSize(outCube).xy;
|
||||||
|
if (gid.x >= size.x || gid.y >= size.y) return;
|
||||||
|
|
||||||
|
vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size);
|
||||||
|
vec3 dir = iblFaceDir(uint(gid.z), uv.x, uv.y);
|
||||||
|
vec3 rgb = texture(equirect, iblDirToEquirectUv(dir)).rgb;
|
||||||
|
|
||||||
|
imageStore(outCube, gid, vec4(rgb, 1.0));
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// IBL ②:环境 cube → 漫反射辐照度 cube(半球 Lambert 卷积)。
|
||||||
|
// E = ∫ L·cosθ·sinθ dθ dφ,切线基 up×N 构造。64×32 = 2048 采样/像素
|
||||||
|
// (与 CPU 版同级精度),32² 低频输出上 GPU 耗时可忽略。
|
||||||
|
|
||||||
|
#include "ibl_common.glsl"
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform samplerCube env;
|
||||||
|
layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube;
|
||||||
|
|
||||||
|
const int PHI_STEPS = 64;
|
||||||
|
const int THETA_STEPS = 32;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec3 gid = ivec3(gl_GlobalInvocationID);
|
||||||
|
ivec2 size = imageSize(outCube).xy;
|
||||||
|
if (gid.x >= size.x || gid.y >= size.y) return;
|
||||||
|
|
||||||
|
vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size);
|
||||||
|
vec3 N = iblFaceDir(uint(gid.z), uv.x, uv.y);
|
||||||
|
|
||||||
|
vec3 up = abs(N.y) > 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(0.0, 1.0, 0.0);
|
||||||
|
vec3 T = normalize(cross(up, N));
|
||||||
|
vec3 B = cross(N, T);
|
||||||
|
|
||||||
|
float dPhi = 2.0 * IBL_PI / float(PHI_STEPS);
|
||||||
|
float dTheta = 0.5 * IBL_PI / float(THETA_STEPS);
|
||||||
|
|
||||||
|
vec3 irradiance = vec3(0.0);
|
||||||
|
for (int p = 0; p < PHI_STEPS; ++p) {
|
||||||
|
float phi = (float(p) + 0.5) * dPhi;
|
||||||
|
for (int t = 0; t < THETA_STEPS; ++t) {
|
||||||
|
float theta = (float(t) + 0.5) * dTheta;
|
||||||
|
float sinT = sin(theta);
|
||||||
|
float cosT = cos(theta);
|
||||||
|
vec3 sampleDir = T * (sinT * cos(phi)) +
|
||||||
|
B * (sinT * sin(phi)) +
|
||||||
|
N * cosT;
|
||||||
|
irradiance += texture(env, sampleDir).rgb * (cosT * sinT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageStore(outCube, gid, vec4(irradiance * (dPhi * dTheta), 1.0));
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// IBL ③:环境 cube → pre-filtered cube(GGX 重要性采样,split-sum 镜面端)。
|
||||||
|
// R(N) ≈ Σ L·NdotL / Σ NdotL。每 mip 一次 dispatch(storage view 必须
|
||||||
|
// 单 mip),roughness 经 push constant 传入;256 采样统一(高 roughness
|
||||||
|
// 分布更宽,取 CPU 版采样数上限保证收敛)。
|
||||||
|
|
||||||
|
#include "ibl_common.glsl"
|
||||||
|
|
||||||
|
layout(local_size_x = 8, local_size_y = 8) in;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform samplerCube env;
|
||||||
|
layout(set = 0, binding = 1, rgba16f) uniform restrict writeonly image2DArray outCube;
|
||||||
|
|
||||||
|
layout(push_constant) uniform Push {
|
||||||
|
float roughness;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
const uint SAMPLES = 256;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec3 gid = ivec3(gl_GlobalInvocationID);
|
||||||
|
ivec2 size = imageSize(outCube).xy;
|
||||||
|
if (gid.x >= size.x || gid.y >= size.y) return;
|
||||||
|
|
||||||
|
vec2 uv = (vec2(gid.xy) + 0.5) / vec2(size);
|
||||||
|
vec3 N = iblFaceDir(uint(gid.z), uv.x, uv.y);
|
||||||
|
vec3 V = N; // split-sum 约定:V 沿 N
|
||||||
|
|
||||||
|
vec3 acc = vec3(0.0);
|
||||||
|
float weight = 0.0;
|
||||||
|
for (uint i = 0u; i < SAMPLES; ++i) {
|
||||||
|
vec3 H = iblImportanceSampleGGX(iblHammersley(i, SAMPLES), N, pc.roughness);
|
||||||
|
vec3 L = normalize(2.0 * dot(V, H) * H - V);
|
||||||
|
float nDotL = max(dot(N, L), 0.0);
|
||||||
|
if (nDotL > 0.0) {
|
||||||
|
acc += texture(env, L).rgb * nDotL;
|
||||||
|
weight += nDotL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageStore(outCube, gid, vec4(weight > 0.0 ? acc / weight : vec3(0.0), 1.0));
|
||||||
|
}
|
||||||
@@ -57,6 +57,10 @@ layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分
|
|||||||
// 等效 36 次比较)→ 软阴影边缘;采样器 clamp-to-border 白色:
|
// 等效 36 次比较)→ 软阴影边缘;采样器 clamp-to-border 白色:
|
||||||
// 光视锥外自动 1.0(无阴影),省出界分支
|
// 光视锥外自动 1.0(无阴影),省出界分支
|
||||||
float ShadowFactor(vec3 N, float viewDepth) {
|
float ShadowFactor(vec3 N, float viewDepth) {
|
||||||
|
// 阴影总开关(ubo.cascadeSplits.w 由 CPU 每帧写入):关闭即全亮,
|
||||||
|
// 配合 ShadowPass 空转(深度图无内容也读不到这里)
|
||||||
|
if (ubo.cascadeSplits.w < 0.5) return 1.0;
|
||||||
|
|
||||||
int cascade = 2;
|
int cascade = 2;
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
if (viewDepth < ubo.cascadeSplits[i]) { cascade = i; break; }
|
if (viewDepth < ubo.cascadeSplits[i]) { cascade = i; break; }
|
||||||
@@ -142,7 +146,7 @@ void main() {
|
|||||||
vec3 F0 = mix(vec3(0.04), albedo, metallic); // 电介质基准反射率 4%
|
vec3 F0 = mix(vec3(0.04), albedo, metallic); // 电介质基准反射率 4%
|
||||||
|
|
||||||
vec3 Lo = vec3(0.0);
|
vec3 Lo = vec3(0.0);
|
||||||
float shade0 = 1.0f; // 【诊断】阴影系数可视化用
|
float shade0 = 1.0f; // 第 0 盏光的阴影因子(环境光同步衰减用)
|
||||||
|
|
||||||
for (int i = 0; i < MAX_LIGHTS; ++i) {
|
for (int i = 0; i < MAX_LIGHTS; ++i) {
|
||||||
// 按类型分支:方向光无衰减;点光方向指向光源 + 平方距离衰减
|
// 按类型分支:方向光无衰减;点光方向指向光源 + 平方距离衰减
|
||||||
@@ -213,8 +217,6 @@ 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); // 阴影区环境同步衰减(简化遮蔽)
|
||||||
|
|
||||||
vec3 color = ambient + Lo + emissive;
|
outColor = vec4(ambient + Lo + emissive, 1.0);
|
||||||
|
|
||||||
outColor = vec4(color, 1.0);
|
|
||||||
outVelocity = fragVelocity;
|
outVelocity = fragVelocity;
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-20
@@ -1,47 +1,123 @@
|
|||||||
#version 450
|
#version 450
|
||||||
|
|
||||||
// Tonemap pass:HDR 线性颜色 → 显示空间。
|
// Tonemap pass:HDR(+bloom)→ 显示空间,兼调试视图路由上屏。
|
||||||
// 这是显示变换的显式建模(对齐 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(旧观感);bloom 等后处理在 HDR 域(Tonemap 之前)插 pass。
|
// 退回纯 gamma(旧观感)。
|
||||||
|
//
|
||||||
|
// 调试视图:pc.debugView 整型路由到已绑定的中间目标(绑定超集静态成立,
|
||||||
|
// 运行时零重绑)。HDR 域资源过同一套 exposure/tonemap 保证观感可比;
|
||||||
|
// LDR 类(velocity/深度)直接可视化。
|
||||||
|
|
||||||
layout(location = 0) in vec2 fragUV;
|
layout(location = 0) in vec2 fragUV;
|
||||||
layout(location = 0) out vec4 outColor;
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
layout(set = 0, binding = 0) uniform sampler2D hdrImage;
|
layout(set = 0, binding = 0) uniform sampler2D taaImage; // TAA 后 HDR(主输入)
|
||||||
layout(set = 0, binding = 1) uniform sampler2D bloomImage;
|
layout(set = 0, binding = 1) uniform sampler2D bloomImage;
|
||||||
layout(set = 0, binding = 2) uniform sampler2D temporalDiffImage; // 帧历史差异(调试视图)
|
layout(set = 0, binding = 2) uniform sampler2D temporalDiffImage;
|
||||||
|
layout(set = 0, binding = 3) uniform sampler2D rawHdrImage; // Forward 直出(TAA 前)
|
||||||
|
layout(set = 0, binding = 4) uniform sampler2D velocityImage;
|
||||||
|
layout(set = 0, binding = 5) uniform sampler2D shadowMap0Image; // D32 深度(r 分量)
|
||||||
|
layout(set = 0, binding = 6) uniform sampler2D shadowMap1Image;
|
||||||
|
layout(set = 0, binding = 7) uniform sampler2D shadowMap2Image;
|
||||||
|
// 环境调试视图源(随环境切换重绑,静态绑定超集)
|
||||||
|
layout(set = 0, binding = 8) uniform samplerCube envCubeImage;
|
||||||
|
layout(set = 0, binding = 9) uniform samplerCube irradianceImage;
|
||||||
|
layout(set = 0, binding = 10) uniform samplerCube prefilterImage;
|
||||||
|
layout(set = 0, binding = 11) uniform sampler2D brdfLutImage;
|
||||||
|
|
||||||
|
// 与 Passes::DebugView 枚举同步(FullscreenGpu.h)
|
||||||
|
const int VIEW_FINAL = 0;
|
||||||
|
const int VIEW_TEMPORALDIFF = 1;
|
||||||
|
const int VIEW_RAW_HDR = 2;
|
||||||
|
const int VIEW_TAA = 3;
|
||||||
|
const int VIEW_BLOOM = 4;
|
||||||
|
const int VIEW_VELOCITY = 5;
|
||||||
|
const int VIEW_SHADOW0 = 6;
|
||||||
|
const int VIEW_SHADOW1 = 7;
|
||||||
|
const int VIEW_SHADOW2 = 8;
|
||||||
|
const int VIEW_ENV_CUBE = 9;
|
||||||
|
const int VIEW_IRRADIANCE = 10;
|
||||||
|
const int VIEW_PREFILTER = 11;
|
||||||
|
const int VIEW_BRDF_LUT = 12;
|
||||||
|
|
||||||
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
|
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
|
||||||
layout(push_constant) uniform PushConstants {
|
layout(push_constant) uniform PushConstants {
|
||||||
float exposure; // 曝光系数
|
float exposure; // 曝光系数
|
||||||
float aces; // >0.5 启用 ACES 曲线,否则纯 gamma
|
float aces; // >0.5 启用 ACES 曲线,否则纯 gamma
|
||||||
float bloomStrength; // bloom 叠加强度(0 = 关闭)
|
float bloomStrength; // bloom 叠加强度(0 = 关闭)
|
||||||
float debugView; // >0.5 显示帧历史差异视图(TemporalDiff)
|
float debugView; // 调试视图枚举(整型经 float 传)
|
||||||
} pc;
|
} pc;
|
||||||
|
|
||||||
vec3 acesFilm(vec3 x) {
|
vec3 acesFilm(vec3 x) {
|
||||||
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
|
return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vec3 tonemap(vec3 hdr) {
|
||||||
|
hdr *= pc.exposure;
|
||||||
|
vec3 mapped = (pc.aces > 0.5) ? acesFilm(hdr) : clamp(hdr, 0.0, 1.0);
|
||||||
|
return pow(mapped, vec3(1.0 / 2.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 深度可视化:反相(近=白,与调试截图风格一致)
|
||||||
|
vec3 depthView(sampler2D depthImage) {
|
||||||
|
float d = texture(depthImage, fragUV).r;
|
||||||
|
return vec3(pow(1.0 - d, 2.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏 uv → 球面方向(cube 调试视图的经纬展开:横向经度、纵向极角)
|
||||||
|
vec3 latLongDir(vec2 uv) {
|
||||||
|
float phi = (uv.x * 2.0 - 1.0) * 3.14159265358979; // -π..π 经度
|
||||||
|
float theta = (1.0 - uv.y) * 3.14159265358979; // 0..π 极角(顶=天顶)
|
||||||
|
return vec3(sin(theta) * cos(phi), cos(theta), sin(theta) * sin(phi));
|
||||||
|
}
|
||||||
|
|
||||||
|
// cube 视图统一走 tonemap(HDR 域,观感与场景一致);
|
||||||
|
// prefilter 纵向映射 mip 链(上锐下糙,直观验证 roughness 分级)
|
||||||
|
vec3 cubeView(samplerCube cube, float lod) {
|
||||||
|
return tonemap(textureLod(cube, latLongDir(fragUV), lod).rgb);
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// 调试视图:帧历史差异(静止=黑,运动边缘发光)
|
int view = int(pc.debugView + 0.5);
|
||||||
if (pc.debugView > 0.5) {
|
|
||||||
|
// ---- 调试视图路由 ----
|
||||||
|
if (view != VIEW_FINAL) {
|
||||||
|
switch (view) {
|
||||||
|
case VIEW_TEMPORALDIFF: {
|
||||||
|
// 帧历史差异:静止=黑,运动边缘发光
|
||||||
vec3 d = clamp(texture(temporalDiffImage, fragUV).rgb, 0.0, 1.0);
|
vec3 d = clamp(texture(temporalDiffImage, fragUV).rgb, 0.0, 1.0);
|
||||||
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;
|
||||||
// bloom 在 HDR 域叠加(半分辨率模糊图线性采样上采样)
|
case VIEW_TAA: outColor = vec4(tonemap(texture(taaImage, fragUV).rgb), 1.0); return;
|
||||||
vec4 hdrTex = texture(hdrImage, fragUV);
|
case VIEW_BLOOM: outColor = vec4(tonemap(texture(bloomImage, fragUV).rgb * 4.0), 1.0); return;
|
||||||
vec3 hdr = hdrTex.rgb + texture(bloomImage, fragUV).rgb * pc.bloomStrength;
|
case VIEW_VELOCITY: {
|
||||||
float alpha = hdrTex.a;
|
// |velocity| × 增益:静止=黑,运动越快越亮
|
||||||
|
vec2 vel = texture(velocityImage, fragUV).xy;
|
||||||
hdr *= pc.exposure;
|
float mag = clamp(length(vel) * 8.0, 0.0, 1.0);
|
||||||
|
outColor = vec4(vec3(mag), 1.0);
|
||||||
vec3 mapped = (pc.aces > 0.5)
|
return;
|
||||||
? acesFilm(hdr)
|
}
|
||||||
: clamp(hdr, 0.0, 1.0);
|
case VIEW_SHADOW0: outColor = vec4(depthView(shadowMap0Image), 1.0); return;
|
||||||
|
case VIEW_SHADOW1: outColor = vec4(depthView(shadowMap1Image), 1.0); return;
|
||||||
outColor = vec4(pow(mapped, vec3(1.0 / 2.2)) * alpha, alpha);
|
case VIEW_SHADOW2: outColor = vec4(depthView(shadowMap2Image), 1.0); return;
|
||||||
|
case VIEW_ENV_CUBE: outColor = vec4(cubeView(envCubeImage, 0.0), 1.0); return;
|
||||||
|
case VIEW_IRRADIANCE: outColor = vec4(cubeView(irradianceImage, 0.0), 1.0); return;
|
||||||
|
case VIEW_PREFILTER: outColor = vec4(cubeView(prefilterImage, fragUV.y * 4.0), 1.0); return;
|
||||||
|
case VIEW_BRDF_LUT: {
|
||||||
|
// LUT 是 (F 缩放, F 偏置) 双通道,R/G 直读可视化
|
||||||
|
vec2 ab = texture(brdfLutImage, fragUV).rg;
|
||||||
|
outColor = vec4(ab, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 正常路径:TAA 后 HDR + bloom 叠加 → tonemap ----
|
||||||
|
vec4 hdrTex = texture(taaImage, fragUV);
|
||||||
|
vec3 hdr = hdrTex.rgb + texture(bloomImage, fragUV).rgb * pc.bloomStrength;
|
||||||
|
|
||||||
|
outColor = vec4(tonemap(hdr) * hdrTex.a, hdrTex.a);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user