Compare commits
6
Commits
beba491bcb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84cf547704 | ||
|
|
2df6e57b61 | ||
|
|
f107854236 | ||
|
|
22309d24f0 | ||
|
|
e7711134fe | ||
|
|
2024772bac |
@@ -0,0 +1,505 @@
|
||||
//
|
||||
// AssetItemView.cpp:资产条目绘制单元实现。
|
||||
//
|
||||
|
||||
#include "AssetItemView.h"
|
||||
|
||||
#include "AssetKind.h"
|
||||
#include "EditorColors.h"
|
||||
#include "ThumbnailCache.h"
|
||||
#include "panels/AssetBrowserPanel.h" // kPrefabDragPayload / kModelDragPayload
|
||||
#include "panels/ControllerEditorPanel.h" // kControllerDragPayload
|
||||
#include "panels/MaterialEditorPanel.h" // kMaterialDragPayload / kTextureDragPayload
|
||||
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kBoxRounding = 3.0f;
|
||||
|
||||
// 类别色压暗背板 + 描边(p0/p1 来自上一个 item rect)
|
||||
void DrawBoxChrome(const ImVec2& p0, const ImVec2& p1,
|
||||
const ImVec4& accent)
|
||||
{
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddRectFilled(p0, p1,
|
||||
ImGui::GetColorU32(ImVec4(accent.x * 0.25f,
|
||||
accent.y * 0.25f,
|
||||
accent.z * 0.25f, 1.0f)),
|
||||
kBoxRounding);
|
||||
dl->AddRect(p0, p1, ImGui::GetColorU32(accent), kBoxRounding);
|
||||
}
|
||||
|
||||
// 名字按可用宽度截断(UTF-8 边界安全)
|
||||
std::string TruncateToWidth(const std::string& s, float maxW)
|
||||
{
|
||||
if (ImGui::CalcTextSize(s.c_str()).x <= maxW) return s;
|
||||
std::string out = s;
|
||||
const char* ell = "..";
|
||||
while (!out.empty() &&
|
||||
ImGui::CalcTextSize((out + ell).c_str()).x > maxW)
|
||||
{
|
||||
out.pop_back();
|
||||
while (!out.empty() && (static_cast<unsigned char>(out.back()) &
|
||||
0xC0) == 0x80)
|
||||
{
|
||||
out.pop_back(); // 退到 UTF-8 起始字节
|
||||
}
|
||||
}
|
||||
return out + ell;
|
||||
}
|
||||
|
||||
// 已尝试按需加载的材质资产路径:加载失败不再每帧重试(资产缓存接管
|
||||
// 成功路径,重复调用是缓存命中)
|
||||
std::set<std::string>& MaterialLoadTried()
|
||||
{
|
||||
static std::set<std::string> s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// 材质加载闸门:大目录(上百个材质)逐个同步加载(.material 解析 +
|
||||
// GPU 纹理上传——Texture 构造内含 submit + fence 阻塞等待,单个材质
|
||||
// 可含多张大贴图)会顶爆单帧 → "卡成 PPT"。闸门 = 每帧至多 1 个 +
|
||||
// 按上次实测耗时冷却(1 帧≈16ms;重载 ≈ 冷却相应帧数),把加载占用
|
||||
// 压到 ≤ ~50% 帧预算,剩余帧时间还给渲染
|
||||
struct MaterialLoadGate
|
||||
{
|
||||
bool Allow()
|
||||
{
|
||||
const int frame = ImGui::GetFrameCount();
|
||||
return frame != m_LastLoadFrame && frame >= m_CooldownUntil;
|
||||
}
|
||||
void Charge(double elapsedMs)
|
||||
{
|
||||
const int frame = ImGui::GetFrameCount();
|
||||
m_LastLoadFrame = frame;
|
||||
const int cool = static_cast<int>(elapsedMs / 16.0);
|
||||
m_CooldownUntil = frame + 1 + std::min(cool, 30);
|
||||
}
|
||||
private:
|
||||
int m_LastLoadFrame = -1;
|
||||
int m_CooldownUntil = 0;
|
||||
};
|
||||
|
||||
MaterialLoadGate& MaterialLoad()
|
||||
{
|
||||
static MaterialLoadGate s_Gate;
|
||||
return s_Gate;
|
||||
}
|
||||
|
||||
// 已烘焙材质缩略图对应的 (serial, editVersion)。资产重载(新 serial)
|
||||
// 或参数编辑(editVersion++)后与当前不符 = 缩略图过期,须作废重烘
|
||||
//(否则浏览器永远停在旧图)——记录时机 = 发起烘焙请求那一帧
|
||||
std::unordered_map<std::string, std::pair<Engine::u64, Engine::u32>>&
|
||||
BakedIdentity()
|
||||
{
|
||||
static std::unordered_map<std::string,
|
||||
std::pair<Engine::u64, Engine::u32>> s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// 材质球缩略图全部走编辑器缩略图缓存:引擎把预览球渲染一次后 GPU→CPU
|
||||
// 回读,App 注入缓存(与贴图缩略图同一条出图路径),浏览器不再占用
|
||||
// 引擎预览 tile 池——大目录滚动/材质编辑器预览互不饿死
|
||||
void DrawCachedThumb(const ThumbView& v, float size, const ImVec2& p0)
|
||||
{
|
||||
float fw = size, fh = size;
|
||||
if (v.w > 0 && v.h > 0)
|
||||
{
|
||||
if (v.w >= v.h) fh = size * static_cast<float>(v.h) / v.w;
|
||||
else fw = size * static_cast<float>(v.w) / v.h;
|
||||
}
|
||||
const ImVec2 fp0(p0.x + (size - fw) * 0.5f, p0.y + (size - fh) * 0.5f);
|
||||
ImGui::GetWindowDrawList()->AddImage(
|
||||
v.tex, fp0, ImVec2(fp0.x + fw, fp0.y + fh));
|
||||
}
|
||||
|
||||
// 缩略图/占位块:一个命中项 + 绘制层。
|
||||
// interactive=true → InvisibleButton(带 ID,可承载拖拽源/点击;Dummy
|
||||
// 无 ID,在其上 BeginDragDropSource 会断言);false → Dummy(行内小图
|
||||
// 纯展示,不抢行 Selectable 的点击)。
|
||||
// 出图时**不铺任何背板**——透明像素会被类别色染色;背板只在未出图时
|
||||
// 作为占位(类别色压暗 + 短标签)。仅可见项才请求缩略图,离屏项不入
|
||||
// 解码队列(大目录不再一次性排队,出图延迟可控)
|
||||
void DrawThumbBox(const EditorContext& ctx,
|
||||
const std::filesystem::path& path, float size,
|
||||
EditorAsset::AssetKind kind, bool interactive)
|
||||
{
|
||||
const bool visible = ImGui::IsRectVisible(ImVec2(size, size));
|
||||
if (interactive)
|
||||
{
|
||||
ImGui::InvisibleButton("##thumb", ImVec2(size, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Dummy(ImVec2(size, size));
|
||||
}
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
const ImVec4 accent = EditorColors::AssetColor(kind);
|
||||
|
||||
bool drawn = false;
|
||||
if (visible && kind == EditorAsset::AssetKind::Material && ctx.assets)
|
||||
{
|
||||
// 材质资产缩略图 = 引擎烘焙的静态材质球(GPU→CPU 回读 → 缩略图
|
||||
// 缓存;与贴图缩略图同一条出图路径)
|
||||
auto& cache = EditorThumbnailCache::Get();
|
||||
const std::string assetPath = path.generic_string();
|
||||
|
||||
// 当前材质身份:已加载才有;未加载时无法判新旧
|
||||
Engine::Material* curMat = nullptr;
|
||||
if (ctx.assets->IsMaterialLoaded(assetPath))
|
||||
{
|
||||
Engine::MaterialAsset* a =
|
||||
ctx.assets->FindMaterialAsset(assetPath);
|
||||
curMat = a ? a->GetMaterial() : nullptr;
|
||||
}
|
||||
|
||||
const ThumbView v = cache.Peek(assetPath);
|
||||
if (v.state == ThumbView::State::Ready)
|
||||
{
|
||||
// 失效检测:缓存缩略图对应的材质身份 vs 当前身份——资产
|
||||
// 重载(新 serial,换贴图槽)/ 参数编辑(editVersion++)后
|
||||
// 不符 = 过期,作废缓存让下一帧重烘(否则浏览器永远旧图)
|
||||
bool stale = false;
|
||||
if (curMat)
|
||||
{
|
||||
auto it = BakedIdentity().find(assetPath);
|
||||
if (it != BakedIdentity().end() &&
|
||||
(it->second.first != curMat->GetSerial() ||
|
||||
it->second.second != curMat->GetEditVersion()))
|
||||
{
|
||||
stale = true;
|
||||
}
|
||||
}
|
||||
if (!stale)
|
||||
{
|
||||
DrawCachedThumb(v, size, p0);
|
||||
drawn = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cache.Invalidate(assetPath);
|
||||
BakedIdentity().erase(assetPath);
|
||||
}
|
||||
}
|
||||
else if (v.state != ThumbView::State::Pending && ctx.renderer)
|
||||
{
|
||||
if (curMat)
|
||||
{
|
||||
// 未烘焙/已失效:请求烘焙并登记当前身份(key 幂等
|
||||
// keepalive,回读完成即进缓存)
|
||||
ctx.renderer->RequestMaterialPreviewBake(
|
||||
curMat, cache.BakeKeyForPath(assetPath));
|
||||
BakedIdentity()[assetPath] = {
|
||||
curMat->GetSerial(), curMat->GetEditVersion()};
|
||||
}
|
||||
else if (!MaterialLoadTried().count(assetPath) &&
|
||||
MaterialLoad().Allow())
|
||||
{
|
||||
// 按需加载材质(闸门限流,避免大目录同帧卡顿)
|
||||
MaterialLoadTried().insert(assetPath);
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
ctx.assets->LoadMaterialAsset(assetPath);
|
||||
const double ms = std::chrono::duration<double,
|
||||
std::milli>(std::chrono::steady_clock::now() - t0)
|
||||
.count();
|
||||
MaterialLoad().Charge(ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!drawn && visible && EditorAsset::IsThumbnailable(path))
|
||||
{
|
||||
const ThumbView v =
|
||||
EditorThumbnailCache::Get().Request(path.generic_string());
|
||||
if (v.state == ThumbView::State::Ready)
|
||||
{
|
||||
DrawCachedThumb(v, size, p0);
|
||||
drawn = true;
|
||||
}
|
||||
}
|
||||
if (!drawn)
|
||||
{
|
||||
// 占位:类别色压暗背板 + 描边;大框叠加短标签
|
||||
DrawBoxChrome(p0, p1, accent);
|
||||
if (size >= 28.0f)
|
||||
{
|
||||
const char* label = EditorAsset::KindShortLabel(kind);
|
||||
const ImVec2 ts = ImGui::CalcTextSize(label);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(p0.x + (size - ts.x) * 0.5f,
|
||||
p0.y + (size - ts.y) * 0.5f),
|
||||
ImGui::GetColorU32(accent), label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文件夹命中项(黄色)+ 短标签
|
||||
void DrawFolderBox(float size, bool interactive)
|
||||
{
|
||||
if (interactive)
|
||||
{
|
||||
ImGui::InvisibleButton("##dir", ImVec2(size, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Dummy(ImVec2(size, size));
|
||||
}
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
DrawBoxChrome(p0, p1, EditorColors::kFolder);
|
||||
if (size >= 28.0f)
|
||||
{
|
||||
const ImVec2 ts = ImGui::CalcTextSize("DIR");
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
ImVec2(p0.x + (size - ts.x) * 0.5f,
|
||||
p0.y + (size - ts.y) * 0.5f),
|
||||
ImGui::GetColorU32(EditorColors::kFolder), "DIR");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace EditorAssetView {
|
||||
|
||||
void DrawDragSource(const std::filesystem::path& path,
|
||||
const std::string& name)
|
||||
{
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
const char* payload = nullptr;
|
||||
switch (kind)
|
||||
{
|
||||
case EditorAsset::AssetKind::Material: payload = kMaterialDragPayload; break;
|
||||
case EditorAsset::AssetKind::AnimClip: payload = kControllerDragPayload; break;
|
||||
case EditorAsset::AssetKind::Prefab: payload = kPrefabDragPayload; break;
|
||||
case EditorAsset::AssetKind::Model: payload = kModelDragPayload; break;
|
||||
case EditorAsset::AssetKind::Texture: payload = kTextureDragPayload; break;
|
||||
default: return;
|
||||
}
|
||||
if (ImGui::BeginDragDropSource())
|
||||
{
|
||||
const std::string assetPath = path.generic_string();
|
||||
ImGui::SetDragDropPayload(payload, assetPath.c_str(),
|
||||
assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTooltip(const EditorContext& ctx,
|
||||
const std::filesystem::path& path)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
const std::string name = path.filename().string();
|
||||
const std::string assetPath = path.generic_string();
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::Separator();
|
||||
ImGui::Text("type: %s", EditorAsset::KindLabel(kind));
|
||||
|
||||
std::error_code ec;
|
||||
const auto sz = fs::file_size(path, ec);
|
||||
if (!ec)
|
||||
{
|
||||
char szBuf[32];
|
||||
if (sz >= 1024ull * 1024ull)
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f MB",
|
||||
static_cast<double>(sz) / (1024.0 * 1024.0));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
|
||||
static_cast<double>(sz) / 1024.0);
|
||||
}
|
||||
ImGui::Text("size: %s", szBuf);
|
||||
}
|
||||
|
||||
const auto ftime = fs::last_write_time(path, ec);
|
||||
if (!ec)
|
||||
{
|
||||
// file_clock → system_clock 近似转换(C++17 无官方转换)
|
||||
const auto sctp = std::chrono::time_point_cast<
|
||||
std::chrono::system_clock::duration>(
|
||||
ftime - fs::file_time_type::clock::now() +
|
||||
std::chrono::system_clock::now());
|
||||
const std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
|
||||
char tmBuf[32];
|
||||
std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M",
|
||||
std::localtime(&tt));
|
||||
ImGui::Text("modified: %s", tmBuf);
|
||||
}
|
||||
|
||||
if (ctx.assets)
|
||||
{
|
||||
if (kind == EditorAsset::AssetKind::Model ||
|
||||
kind == EditorAsset::AssetKind::Texture)
|
||||
{
|
||||
const bool loaded =
|
||||
(kind == EditorAsset::AssetKind::Model)
|
||||
? ctx.assets->IsModelLoaded(assetPath)
|
||||
: ctx.assets->IsEnvironmentLoaded(assetPath);
|
||||
ImGui::Text("state: %s", loaded ? "loaded" : "not loaded");
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Prefab)
|
||||
{
|
||||
ImGui::TextDisabled(
|
||||
"template — drag into scene / double-click");
|
||||
}
|
||||
if (kind == EditorAsset::AssetKind::Model)
|
||||
{
|
||||
if (const Engine::Model* m =
|
||||
ctx.assets->GetModelByPath(assetPath);
|
||||
m != nullptr)
|
||||
{
|
||||
ImGui::Text("meshes: %u, bones: %u, clips: %u",
|
||||
static_cast<unsigned>(m->GetMeshes().size()),
|
||||
m->GetBoneCount(),
|
||||
static_cast<unsigned>(m->GetClips().size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled("%s", assetPath.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ItemResult DrawRow(const EditorContext& ctx,
|
||||
const std::filesystem::path& path, bool selected)
|
||||
{
|
||||
ItemResult res;
|
||||
const std::string name = path.filename().string();
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
|
||||
ImGui::PushID(name.c_str());
|
||||
|
||||
// 内联小缩略图 / 类别色占位块(非交互:行点击归 Selectable)
|
||||
DrawThumbBox(ctx, path, ImGui::GetTextLineHeight(), kind, false);
|
||||
ImGui::SameLine();
|
||||
|
||||
// 名称(类别色提亮)+ 类别色 hover/选中高亮
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
|
||||
ImGui::PushStyleColor(ImGuiCol_Header,
|
||||
EditorColors::AssetHighlightColor(kind));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
|
||||
EditorColors::AssetHighlightColor(kind));
|
||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
|
||||
EditorColors::AssetHighlightColor(kind));
|
||||
res.clicked = ImGui::Selectable(name.c_str(), selected,
|
||||
ImGuiSelectableFlags_AllowDoubleClick);
|
||||
ImGui::PopStyleColor(4);
|
||||
if (res.clicked && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
res.doubleClicked = true;
|
||||
}
|
||||
|
||||
DrawDragSource(path, name);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
DrawTooltip(ctx, path);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
return res;
|
||||
}
|
||||
|
||||
ItemResult DrawTile(const EditorContext& ctx,
|
||||
const std::filesystem::path& path,
|
||||
float tileSize, bool selected)
|
||||
{
|
||||
ItemResult res;
|
||||
const std::string name = path.filename().string();
|
||||
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
|
||||
const ImVec4 accent = EditorColors::AssetColor(kind);
|
||||
|
||||
ImGui::PushID(name.c_str());
|
||||
ImGui::BeginGroup();
|
||||
|
||||
DrawThumbBox(ctx, path, tileSize, kind, true);
|
||||
|
||||
// 选中/hover 描边(叠在缩略图上)
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
if (selected || hovered)
|
||||
{
|
||||
ImGui::GetWindowDrawList()->AddRect(
|
||||
p0, p1, ImGui::GetColorU32(accent), kBoxRounding, 0,
|
||||
selected ? 2.0f : 1.0f);
|
||||
}
|
||||
res.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
res.doubleClicked = true;
|
||||
}
|
||||
|
||||
DrawDragSource(path, name);
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
DrawTooltip(ctx, path);
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
|
||||
ImGui::TextUnformatted(TruncateToWidth(name, tileSize).c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::PopID();
|
||||
return res;
|
||||
}
|
||||
|
||||
bool DrawFolderRow(const std::filesystem::path& dir)
|
||||
{
|
||||
const std::string dname = dir.filename().string();
|
||||
ImGui::PushID(dname.c_str());
|
||||
DrawFolderBox(ImGui::GetTextLineHeight(), false);
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
|
||||
const bool clicked = ImGui::Selectable(dname.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
|
||||
bool selected)
|
||||
{
|
||||
const std::string dname = dir.filename().string();
|
||||
ImGui::PushID(dname.c_str());
|
||||
ImGui::BeginGroup();
|
||||
|
||||
DrawFolderBox(tileSize, true);
|
||||
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
if (selected || ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::GetWindowDrawList()->AddRect(
|
||||
p0, p1, ImGui::GetColorU32(EditorColors::kFolder), kBoxRounding,
|
||||
0, selected ? 2.0f : 1.0f);
|
||||
}
|
||||
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
|
||||
ImGui::TextUnformatted(TruncateToWidth(dname, tileSize).c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::EndGroup();
|
||||
ImGui::PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
} // namespace EditorAssetView
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// AssetItemView:资产条目的绘制单元(列表行 / 网格瓦片 / 目录 / 拖拽源 /
|
||||
// tooltip)。Content Browser 与材质纹理选择器共用同一套视觉与交互。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETITEMVIEW_H
|
||||
#define HELLOVULKAN_ASSETITEMVIEW_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace EditorAssetView {
|
||||
|
||||
// 单击 / 双击结果(调用方决定选中与打开语义)
|
||||
struct ItemResult {
|
||||
bool clicked = false;
|
||||
bool doubleClicked = false;
|
||||
};
|
||||
|
||||
// 拖拽源:按资产类别发出对应 payload(材质/控制器/预制体/模型/纹理)
|
||||
void DrawDragSource(const std::filesystem::path& path,
|
||||
const std::string& name);
|
||||
|
||||
// 统一 tooltip(类型/大小/修改时间/加载状态)
|
||||
void DrawTooltip(const EditorContext& ctx,
|
||||
const std::filesystem::path& path);
|
||||
|
||||
// 列表行:内联小缩略图 + 彩色短标签 + 类别色名称
|
||||
ItemResult DrawRow(const EditorContext& ctx,
|
||||
const std::filesystem::path& path, bool selected);
|
||||
|
||||
// 网格瓦片:缩略图/类别色占位块 + 名称(截断到瓦片宽)
|
||||
ItemResult DrawTile(const EditorContext& ctx,
|
||||
const std::filesystem::path& path,
|
||||
float tileSize, bool selected);
|
||||
|
||||
// 目录(文件夹色):返回是否被点击(选中/进入)
|
||||
bool DrawFolderRow(const std::filesystem::path& dir);
|
||||
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
|
||||
bool selected);
|
||||
|
||||
} // namespace EditorAssetView
|
||||
|
||||
#endif //HELLOVULKAN_ASSETITEMVIEW_H
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// AssetKind.h:资产分类的单一事实来源(Content Browser / 编辑器共用)。
|
||||
//
|
||||
// 原先分类逻辑散在 AssetBrowserPanel.cpp 匿名 ns(Kind/KindOf/KindTag),
|
||||
// 现在上提为头文件:面板、拖拽源、缩略图缓存按同一份扩展名表分派。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_ASSETKIND_H
|
||||
#define HELLOVULKAN_ASSETKIND_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace EditorAsset {
|
||||
|
||||
// 资产类别(按扩展名分派;同时是类型过滤的身份)
|
||||
enum class AssetKind {
|
||||
Model, Texture, Scene, Material, AnimClip, Prefab, Other
|
||||
};
|
||||
|
||||
// 统一小写扩展名(含点,如 ".png";无扩展名 = 空串)
|
||||
inline std::string LowerExt(const std::filesystem::path& p)
|
||||
{
|
||||
std::string ext = p.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
return ext;
|
||||
}
|
||||
|
||||
// 纹理扩展名表(单一事实来源:分类 / 缩略图 / 材质纹理槽选择器共用)。
|
||||
// 口径 = 引擎 Texture 可直接加载者:
|
||||
// stb_image 8-bit 系 png/jpg/tga/bmp/psd/gif/pnm
|
||||
// + .hdr(环境贴图,stbi_loadf / stbi_load 走内置 hdr→ldr)
|
||||
// + .dds(引擎 DdsLoader,BC 压缩直传)
|
||||
inline bool IsTexturePath(const std::filesystem::path& p)
|
||||
{
|
||||
static const char* kExts[] = {
|
||||
".png", ".jpg", ".jpeg", ".tga", ".bmp", ".psd",
|
||||
".gif", ".ppm", ".pgm", ".pnm", ".hdr", ".dds"};
|
||||
const std::string ext = LowerExt(p);
|
||||
for (const char* e : kExts)
|
||||
{
|
||||
if (ext == e) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 扩展名 → 类别
|
||||
inline AssetKind KindOfPath(const std::filesystem::path& p)
|
||||
{
|
||||
const std::string ext = LowerExt(p);
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
return AssetKind::Model;
|
||||
}
|
||||
if (IsTexturePath(p))
|
||||
{
|
||||
return AssetKind::Texture;
|
||||
}
|
||||
if (ext == ".scene")
|
||||
{
|
||||
return AssetKind::Scene;
|
||||
}
|
||||
if (ext == ".material")
|
||||
{
|
||||
return AssetKind::Material;
|
||||
}
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
return AssetKind::AnimClip;
|
||||
}
|
||||
if (ext == ".prefab")
|
||||
{
|
||||
return AssetKind::Prefab;
|
||||
}
|
||||
return AssetKind::Other;
|
||||
}
|
||||
|
||||
// 全称标签(tooltip / 网格标题;替代原下标对齐数组)
|
||||
inline const char* KindLabel(AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case AssetKind::Model: return "Model";
|
||||
case AssetKind::Texture: return "Texture/Environment";
|
||||
case AssetKind::Scene: return "Scene";
|
||||
case AssetKind::Material: return "Material";
|
||||
case AssetKind::AnimClip: return "Controller";
|
||||
case AssetKind::Prefab: return "Prefab";
|
||||
default: return "File";
|
||||
}
|
||||
}
|
||||
|
||||
// 列表类型列的短标签(替代原 "[M]"/"[T]" 前缀)
|
||||
inline const char* KindShortLabel(AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case AssetKind::Model: return "MDL";
|
||||
case AssetKind::Texture: return "TEX";
|
||||
case AssetKind::Scene: return "SCN";
|
||||
case AssetKind::Material: return "MAT";
|
||||
case AssetKind::AnimClip: return "CTRL";
|
||||
case AssetKind::Prefab: return "PFB";
|
||||
default: return "FILE";
|
||||
}
|
||||
}
|
||||
|
||||
// 可解码为 8-bit 缩略图的扩展名:stbi_load 原生支持者(含 .hdr ——
|
||||
// stbi_load 内部走 hdr→ldr 色调映射)。仅 .dds 例外(BC 压缩块,需
|
||||
// 专用解码器)→ 占位块
|
||||
inline bool IsThumbnailable(const std::filesystem::path& p)
|
||||
{
|
||||
return IsTexturePath(p) && LowerExt(p) != ".dds";
|
||||
}
|
||||
|
||||
} // namespace EditorAsset
|
||||
|
||||
#endif //HELLOVULKAN_ASSETKIND_H
|
||||
@@ -12,6 +12,7 @@
|
||||
//
|
||||
|
||||
#include "Editor.h"
|
||||
#include "ThumbnailCache.h" // 缩略图缓存(每帧 BeginFrame)
|
||||
#include "panels/AssetBrowserPanel.h"
|
||||
#include "panels/HierarchyPanel.h"
|
||||
#include "panels/InspectorPanel.h"
|
||||
@@ -167,6 +168,24 @@ void Editor::TogglePause()
|
||||
|
||||
void Editor::Draw()
|
||||
{
|
||||
// 缩略图缓存推进(解码限量 / 上传就绪 / LRU 淘汰):ImGui::NewFrame
|
||||
// 已由框架 ImGuiLayer 完成,此处处于面板绘制之前
|
||||
EditorThumbnailCache::Get().BeginFrame();
|
||||
|
||||
// 材质球缩略图烘焙:排空引擎异步回读结果 → 注入缩略图缓存(浏览器
|
||||
// 材质条目随后从缓存出图,不再占用引擎预览 tile 池)
|
||||
if (m_Ctx.renderer)
|
||||
{
|
||||
Engine::u64 key = 0;
|
||||
std::vector<unsigned char> pixels;
|
||||
Engine::u32 w = 0, h = 0;
|
||||
while (m_Ctx.renderer->PollMaterialPreviewBake(key, pixels, w, h))
|
||||
{
|
||||
EditorThumbnailCache::Get().CompleteBake(
|
||||
key, pixels.data(), static_cast<int>(w), static_cast<int>(h));
|
||||
}
|
||||
}
|
||||
|
||||
// PIE 镜像同步(面板只读指针消费;权威 = GameFlow,此处适配)
|
||||
if (m_Ctx.flow)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// EditorColors.h:编辑器面板配色的单一事实来源。
|
||||
//
|
||||
// 资产浏览器条目、Outliner 实体名/组件徽标的着色集中在此:面板不再
|
||||
// 各自散落硬编码 ImVec4。ImVec4 构造为 constexpr(imgui.h),故可用
|
||||
// inline constexpr 常量。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_EDITORCOLORS_H
|
||||
#define HELLOVULKAN_EDITORCOLORS_H
|
||||
|
||||
#include "AssetKind.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace EditorColors {
|
||||
|
||||
// ---- 资产类别色(列表标签 / 名称 / 网格瓦片 / 选中高亮)----
|
||||
inline constexpr ImVec4 kModel = ImVec4(0.36f, 0.62f, 0.90f, 1.0f); // 蓝
|
||||
inline constexpr ImVec4 kTexture = ImVec4(0.34f, 0.78f, 0.52f, 1.0f); // 绿
|
||||
inline constexpr ImVec4 kScene = ImVec4(0.62f, 0.50f, 0.90f, 1.0f); // 紫
|
||||
inline constexpr ImVec4 kMaterial = ImVec4(0.92f, 0.60f, 0.25f, 1.0f); // 橙
|
||||
inline constexpr ImVec4 kAnim = ImVec4(0.30f, 0.76f, 0.76f, 1.0f); // 青
|
||||
inline constexpr ImVec4 kPrefab = ImVec4(0.42f, 0.70f, 0.95f, 1.0f); // 亮蓝
|
||||
inline constexpr ImVec4 kOther = ImVec4(0.72f, 0.72f, 0.75f, 1.0f); // 灰
|
||||
inline constexpr ImVec4 kFolder = ImVec4(0.88f, 0.74f, 0.34f, 1.0f); // 黄
|
||||
|
||||
inline ImVec4 AssetColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case EditorAsset::AssetKind::Model: return kModel;
|
||||
case EditorAsset::AssetKind::Texture: return kTexture;
|
||||
case EditorAsset::AssetKind::Scene: return kScene;
|
||||
case EditorAsset::AssetKind::Material: return kMaterial;
|
||||
case EditorAsset::AssetKind::AnimClip: return kAnim;
|
||||
case EditorAsset::AssetKind::Prefab: return kPrefab;
|
||||
default: return kOther;
|
||||
}
|
||||
}
|
||||
|
||||
// 向白提亮(保可读性);t = 0 原色,1 全白
|
||||
inline ImVec4 Lighten(const ImVec4& c, float t)
|
||||
{
|
||||
return ImVec4(c.x + (1.0f - c.x) * t, c.y + (1.0f - c.y) * t,
|
||||
c.z + (1.0f - c.z) * t, c.w);
|
||||
}
|
||||
|
||||
// 名称色:类别色提亮(行/瓦片名用)
|
||||
inline ImVec4 AssetNameColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
return Lighten(AssetColor(kind), 0.40f);
|
||||
}
|
||||
|
||||
// 选中/hover 高亮底色:类别色压暗(ImGuiCol_Header/HeaderHovered)
|
||||
inline ImVec4 AssetHighlightColor(EditorAsset::AssetKind kind)
|
||||
{
|
||||
return ImVec4(AssetColor(kind).x * 0.42f, AssetColor(kind).y * 0.42f,
|
||||
AssetColor(kind).z * 0.42f, 1.0f);
|
||||
}
|
||||
|
||||
// ---- Outliner 组件徽标色 ----
|
||||
inline constexpr ImVec4 kMesh = ImVec4(0.36f, 0.72f, 0.95f, 1.0f);
|
||||
inline constexpr ImVec4 kAnimB = ImVec4(0.95f, 0.72f, 0.35f, 1.0f);
|
||||
inline constexpr ImVec4 kCamera = ImVec4(0.70f, 0.55f, 0.95f, 1.0f);
|
||||
inline constexpr ImVec4 kLight = ImVec4(0.96f, 0.86f, 0.36f, 1.0f);
|
||||
inline constexpr ImVec4 kPhysics = ImVec4(0.45f, 0.85f, 0.55f, 1.0f);
|
||||
inline constexpr ImVec4 kScript = ImVec4(0.90f, 0.55f, 0.76f, 1.0f);
|
||||
inline constexpr ImVec4 kSky = ImVec4(0.46f, 0.80f, 0.92f, 1.0f);
|
||||
// 无组件/兜底实体名色(近白)
|
||||
inline constexpr ImVec4 kEntityNeutral = ImVec4(0.86f, 0.86f, 0.90f, 1.0f);
|
||||
|
||||
} // namespace EditorColors
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCOLORS_H
|
||||
@@ -12,6 +12,8 @@
|
||||
#ifndef HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
#define HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
|
||||
#include "AssetKind.h" // EditorAsset::IsTexturePath(纹理扩展名单一事实来源)
|
||||
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
@@ -53,12 +55,7 @@ namespace EditorMaterialUtil {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" ||
|
||||
ext == ".tga" || ext == ".bmp" || ext == ".hdr")
|
||||
if (EditorAsset::IsTexturePath(entry.path()))
|
||||
{
|
||||
s_Files.push_back(entry.path().generic_string());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
//
|
||||
// ThumbnailCache.cpp:缩略图解碼 + 降采样 + ImGui 用户纹理上传 + LRU 淘汰。
|
||||
//
|
||||
// 生命周期(依赖 vendored ImGui 1.93.0-WIP 语义,与后端/主循环逐帧对齐):
|
||||
// 帧 N :Request 入队 → BeginFrame 解码 → ImTextureData::Create +
|
||||
// RegisterUserTexture(Status=WantCreate,TexID=Invalid)
|
||||
// 帧 N 末:UpdateTexturesEndFrame 把用户纹理并入 PlatformIO.Textures;
|
||||
// ImGui_ImplVulkan_RenderDrawData 内 UpdateTexture 建 VkImage +
|
||||
// 描述符,置 TexID、Status=OK
|
||||
// 帧 N+1:BeginFrame 见 TexID != Invalid → Ready(当帧注册、次帧出图)
|
||||
// 淘汰 :WantDestroyNextFrame=true → 后端 UnusedFrames>=ImageCount 时
|
||||
// 销毁并清 BackendUserData → 下一帧见 Destroyed/无 BackendUserData
|
||||
// → UnregisterUserTexture + IM_DELETE
|
||||
// 关机 :本单例是进程级 function-local static,析构晚于 main 内的
|
||||
// SandboxApp(含 ImGuiLayer)——ImGui_ImplVulkan_Shutdown 已对
|
||||
// RefCount==1 的用户纹理统一释放 GPU;析构仅 IM_DELETE(只
|
||||
// IM_FREE(Pixels),不触任何已销毁的 ImGui 上下文)。
|
||||
//
|
||||
|
||||
#include "ThumbnailCache.h"
|
||||
|
||||
#include <imgui_internal.h> // RegisterUserTexture / UnregisterUserTexture
|
||||
|
||||
#include <stb/stb_image.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kThumbMaxDim = 128; // 缩略图最长边
|
||||
constexpr size_t kMaxEntries = 256; // 缓存上限(≤ 描述符池)
|
||||
constexpr int kMaxDecodesPerFrame = 2; // 每帧最多解码张数
|
||||
constexpr long long kMaxSourcePixels = 4096ll * 4096ll; // 超大源图跳过
|
||||
constexpr int kReadyTimeoutFrames = 3; // 上传超时 → Failed
|
||||
|
||||
// box filter 等比降采样(RGBA8):输出像素在源块内最多取
|
||||
// kMaxSamplesPerAxis² 个采样点(按步长抽样)。大图降采样时避免对
|
||||
// 整块(如 4096² → 128² 时每像素 1024 次读)全量累加——那是单张
|
||||
// 百毫秒级的卡顿源
|
||||
void ResampleBoxRGBA8(const unsigned char* src, int sw, int sh,
|
||||
unsigned char* dst, int dw, int dh)
|
||||
{
|
||||
constexpr int kMaxSamplesPerAxis = 8;
|
||||
for (int y = 0; y < dh; ++y)
|
||||
{
|
||||
const int sy0 = y * sh / dh;
|
||||
int sy1 = (y + 1) * sh / dh;
|
||||
if (sy1 <= sy0) sy1 = sy0 + 1;
|
||||
const int syStep = std::max(
|
||||
1, (sy1 - sy0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
|
||||
for (int x = 0; x < dw; ++x)
|
||||
{
|
||||
const int sx0 = x * sw / dw;
|
||||
int sx1 = (x + 1) * sw / dw;
|
||||
if (sx1 <= sx0) sx1 = sx0 + 1;
|
||||
const int sxStep = std::max(
|
||||
1,
|
||||
(sx1 - sx0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
|
||||
unsigned int r = 0, g = 0, b = 0, a = 0, n = 0;
|
||||
for (int sy = sy0; sy < sy1; sy += syStep)
|
||||
{
|
||||
const unsigned char* row =
|
||||
src + static_cast<size_t>(sy) * sw * 4;
|
||||
for (int sx = sx0; sx < sx1; sx += sxStep)
|
||||
{
|
||||
const unsigned char* p =
|
||||
row + static_cast<size_t>(sx) * 4;
|
||||
r += p[0]; g += p[1]; b += p[2]; a += p[3]; ++n;
|
||||
}
|
||||
}
|
||||
unsigned char* q = dst + (static_cast<size_t>(y) * dw + x) * 4;
|
||||
q[0] = static_cast<unsigned char>(r / n);
|
||||
q[1] = static_cast<unsigned char>(g / n);
|
||||
q[2] = static_cast<unsigned char>(b / n);
|
||||
q[3] = static_cast<unsigned char>(a / n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorThumbnailCache& EditorThumbnailCache::Get()
|
||||
{
|
||||
static EditorThumbnailCache s_Cache;
|
||||
return s_Cache;
|
||||
}
|
||||
|
||||
EditorThumbnailCache::~EditorThumbnailCache()
|
||||
{
|
||||
// 仅回收 CPU 侧(见文件头注释)。勿调 UnregisterUserTexture——
|
||||
// 此刻 ImGui 上下文已随 ImGuiLayer 销毁
|
||||
for (auto& [path, e] : m_Entries)
|
||||
{
|
||||
if (e.tex) IM_DELETE(e.tex);
|
||||
}
|
||||
for (ImTextureData* tex : m_Retire) IM_DELETE(tex);
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::BeginFrame()
|
||||
{
|
||||
++m_Frame;
|
||||
|
||||
DrainRetired();
|
||||
|
||||
// 就绪推进:注册后等后端上传(次帧可见)
|
||||
for (auto& [path, e] : m_Entries)
|
||||
{
|
||||
if (e.state != ThumbView::State::Pending || !e.tex) continue;
|
||||
++e.pendingFrames;
|
||||
if (e.tex->TexID != ImTextureID_Invalid)
|
||||
{
|
||||
e.state = ThumbView::State::Ready;
|
||||
}
|
||||
else if (e.pendingFrames > kReadyTimeoutFrames &&
|
||||
e.tex->Status == ImTextureStatus_Destroyed)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// 限量解码(未解码 = tex 为空)
|
||||
int decodes = 0;
|
||||
while (decodes < kMaxDecodesPerFrame && !m_Queue.empty())
|
||||
{
|
||||
const std::string p = m_Queue.front();
|
||||
m_Queue.pop_front();
|
||||
auto it = m_Entries.find(p);
|
||||
if (it == m_Entries.end()) continue; // 已被淘汰
|
||||
if (it->second.tex) continue; // 已解码(防御)
|
||||
DecodeOne(p, it->second);
|
||||
++decodes;
|
||||
}
|
||||
|
||||
// 超上限 → 淘汰最久未用
|
||||
while (m_Entries.size() > kMaxEntries)
|
||||
{
|
||||
if (!RetireOne()) break; // 全在用:本帧放弃
|
||||
}
|
||||
}
|
||||
|
||||
ThumbView EditorThumbnailCache::Request(const std::string& assetPath)
|
||||
{
|
||||
ThumbView v;
|
||||
if (assetPath.empty())
|
||||
{
|
||||
v.state = ThumbView::State::Failed;
|
||||
return v;
|
||||
}
|
||||
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end())
|
||||
{
|
||||
Entry e;
|
||||
e.lastUseFrame = m_Frame;
|
||||
m_Entries.emplace(assetPath, e);
|
||||
m_Queue.push_back(assetPath);
|
||||
return v; // Pending
|
||||
}
|
||||
|
||||
Entry& e = it->second;
|
||||
e.lastUseFrame = m_Frame;
|
||||
v.state = e.state;
|
||||
v.w = e.w;
|
||||
v.h = e.h;
|
||||
if (e.state == ThumbView::State::Ready && e.tex)
|
||||
{
|
||||
v.tex = e.tex->GetTexRef();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::UploadRGBA8(const unsigned char* src, int sw, int sh,
|
||||
Entry& e)
|
||||
{
|
||||
// 等比缩放到最长边 ≤ kThumbMaxDim
|
||||
int dw = sw, dh = sh;
|
||||
if (sw >= sh && sw > kThumbMaxDim)
|
||||
{
|
||||
dw = kThumbMaxDim;
|
||||
dh = std::max(1, sh * kThumbMaxDim / sw);
|
||||
}
|
||||
else if (sh > sw && sh > kThumbMaxDim)
|
||||
{
|
||||
dh = kThumbMaxDim;
|
||||
dw = std::max(1, sw * kThumbMaxDim / sh);
|
||||
}
|
||||
|
||||
ImTextureData* tex = IM_NEW(ImTextureData)();
|
||||
tex->Create(ImTextureFormat_RGBA32, dw, dh);
|
||||
if (dw == sw && dh == sh)
|
||||
{
|
||||
std::memcpy(tex->GetPixels(), src, static_cast<size_t>(dw) * dh * 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
ResampleBoxRGBA8(src, sw, sh,
|
||||
static_cast<unsigned char*>(tex->GetPixels()), dw, dh);
|
||||
}
|
||||
|
||||
ImGui::RegisterUserTexture(tex);
|
||||
tex->SetStatus(ImTextureStatus_WantCreate);
|
||||
|
||||
e.tex = tex;
|
||||
e.w = dw;
|
||||
e.h = dh;
|
||||
e.state = ThumbView::State::Pending;
|
||||
e.pendingFrames = 0;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::DecodeOne(const std::string& path, Entry& e)
|
||||
{
|
||||
int sw = 0, sh = 0, ch = 0;
|
||||
if (!stbi_info(path.c_str(), &sw, &sh, &ch) || sw <= 0 || sh <= 0 ||
|
||||
static_cast<long long>(sw) * sh > kMaxSourcePixels)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char* src = stbi_load(path.c_str(), &sw, &sh, &ch, 4);
|
||||
if (!src)
|
||||
{
|
||||
e.state = ThumbView::State::Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
UploadRGBA8(src, sw, sh, e);
|
||||
stbi_image_free(src);
|
||||
e.lastUseFrame = m_Frame;
|
||||
}
|
||||
|
||||
ThumbView EditorThumbnailCache::Peek(const std::string& assetPath)
|
||||
{
|
||||
ThumbView v;
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end())
|
||||
{
|
||||
v.state = ThumbView::State::Failed; // 缺省 = 未注入
|
||||
return v;
|
||||
}
|
||||
Entry& e = it->second;
|
||||
e.lastUseFrame = m_Frame;
|
||||
v.state = e.state;
|
||||
v.w = e.w;
|
||||
v.h = e.h;
|
||||
if (e.state == ThumbView::State::Ready && e.tex)
|
||||
{
|
||||
v.tex = e.tex->GetTexRef();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::Invalidate(const std::string& assetPath)
|
||||
{
|
||||
auto it = m_Entries.find(assetPath);
|
||||
if (it == m_Entries.end()) return;
|
||||
if (it->second.tex)
|
||||
{
|
||||
// 交回回收流程(与 LRU 淘汰同一路径:WantDestroyNextFrame →
|
||||
// 后端释放 → DrainRetired 注销 + 回收)
|
||||
it->second.tex->WantDestroyNextFrame = true;
|
||||
m_Retire.push_back(it->second.tex);
|
||||
}
|
||||
m_Entries.erase(it);
|
||||
}
|
||||
|
||||
unsigned long long EditorThumbnailCache::BakeKeyForPath(const std::string& path)
|
||||
{
|
||||
auto it = m_PathToBakeKey.find(path);
|
||||
if (it != m_PathToBakeKey.end()) return it->second;
|
||||
const unsigned long long key = ++m_BakeKeyCounter;
|
||||
m_PathToBakeKey.emplace(path, key);
|
||||
m_BakeKeyToPath.emplace(key, path);
|
||||
return key;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::CompleteBake(unsigned long long key,
|
||||
const unsigned char* rgba, int w, int h)
|
||||
{
|
||||
auto it = m_BakeKeyToPath.find(key);
|
||||
if (it == m_BakeKeyToPath.end()) return; // 已注销(重复回读)
|
||||
const std::string path = it->second;
|
||||
m_BakeKeyToPath.erase(it);
|
||||
m_PathToBakeKey.erase(path);
|
||||
InjectRGBA8(path, rgba, w, h);
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::InjectRGBA8(const std::string& path,
|
||||
const unsigned char* rgba, int w, int h)
|
||||
{
|
||||
if (!rgba || w <= 0 || h <= 0) return;
|
||||
Entry& e = m_Entries[path]; // 材质路径不经解码队列,缺省即新建
|
||||
if (e.tex) return; // 已有纹理(重复注入):忽略
|
||||
UploadRGBA8(rgba, w, h, e);
|
||||
e.lastUseFrame = m_Frame;
|
||||
}
|
||||
|
||||
bool EditorThumbnailCache::RetireOne()
|
||||
{
|
||||
auto worst = m_Entries.end();
|
||||
for (auto it = m_Entries.begin(); it != m_Entries.end(); ++it)
|
||||
{
|
||||
if (it->second.lastUseFrame == m_Frame) continue; // 本帧在用
|
||||
if (worst == m_Entries.end() ||
|
||||
it->second.lastUseFrame < worst->second.lastUseFrame)
|
||||
{
|
||||
worst = it;
|
||||
}
|
||||
}
|
||||
if (worst == m_Entries.end()) return false;
|
||||
|
||||
if (worst->second.tex)
|
||||
{
|
||||
worst->second.tex->WantDestroyNextFrame = true;
|
||||
m_Retire.push_back(worst->second.tex);
|
||||
}
|
||||
m_Entries.erase(worst);
|
||||
return true;
|
||||
}
|
||||
|
||||
void EditorThumbnailCache::DrainRetired()
|
||||
{
|
||||
for (size_t i = 0; i < m_Retire.size();)
|
||||
{
|
||||
ImTextureData* tex = m_Retire[i];
|
||||
// 后端销毁后清 BackendUserData 并置 Destroyed → 可安全回收
|
||||
if (tex->BackendUserData == nullptr &&
|
||||
tex->Status == ImTextureStatus_Destroyed)
|
||||
{
|
||||
ImGui::UnregisterUserTexture(tex);
|
||||
IM_DELETE(tex);
|
||||
m_Retire[i] = m_Retire.back();
|
||||
m_Retire.pop_back();
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// ThumbnailCache.h:编辑器缩略图缓存(Content Browser / 材质纹理槽共用)。
|
||||
//
|
||||
// 逐帧限量 CPU 解码 + box 降采样 → ImGui 用户纹理(ImTextureData +
|
||||
// RegisterUserTexture)→ 后端建/传 VkImage。零引擎改动。
|
||||
//
|
||||
// 生命周期(详见 .cpp 注释):进程级静态单例,析构晚于 ImGuiLayer,
|
||||
// CPU 侧由静态析构回收、GPU 侧由 ImGui_ImplVulkan_Shutdown 回收。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_THUMBNAILCACHE_H
|
||||
#define HELLOVULKAN_THUMBNAILCACHE_H
|
||||
|
||||
#include <imgui.h> // ImTextureData / ImTextureRef
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// 一次请求的结果:State==Ready 时 tex 可用于 ImGui::Image(tex, size)
|
||||
struct ThumbView {
|
||||
enum class State { Pending, Ready, Failed };
|
||||
State state = State::Pending;
|
||||
ImTextureRef tex{};
|
||||
int w = 0; // 缩略图原始像素尺寸(按比例显示用)
|
||||
int h = 0;
|
||||
};
|
||||
|
||||
class EditorThumbnailCache {
|
||||
public:
|
||||
// 进程级单例(编辑器首帧 BeginFrame 时惰性构造)
|
||||
static EditorThumbnailCache& Get();
|
||||
|
||||
// 每帧一次:Editor::Draw 开头(ImGui::NewFrame 之后、面板绘制之前)
|
||||
void BeginFrame();
|
||||
|
||||
// 幂等请求:命中且就绪即返回;未命中则入队,本帧返回 Pending
|
||||
ThumbView Request(const std::string& assetPath);
|
||||
|
||||
// 只读查状态(**不**触发解码入队):缺省 → Failed。材质球烘焙用——
|
||||
// 材质路径不走 stbi 解码,注入前必须能区分"未注入"与"上传中"
|
||||
//(Pending 表示已注入、等后端建纹理)
|
||||
ThumbView Peek(const std::string& assetPath);
|
||||
|
||||
// 作废某条目的缓存纹理(材质重载/参数编辑后缩略图过期 → 重烘)。
|
||||
// 丢弃 CPU 记录、纹理交回回收流程;随后 Peek 缺省 → Failed
|
||||
void Invalidate(const std::string& assetPath);
|
||||
|
||||
// ---- 材质球烘焙(GPU 回读结果注入)----
|
||||
// path ↔ key 幂等映射:Renderer 只接受/回传 u64 令牌,App 侧登记
|
||||
unsigned long long BakeKeyForPath(const std::string& path);
|
||||
// 回读结果注入:RGBA8 像素(w×h)→ 缩略图纹理(内部等比降到最长边
|
||||
// ≤ kThumbMaxDim)。key 未登记则忽略
|
||||
void CompleteBake(unsigned long long key, const unsigned char* rgba,
|
||||
int w, int h);
|
||||
|
||||
EditorThumbnailCache(const EditorThumbnailCache&) = delete;
|
||||
EditorThumbnailCache& operator=(const EditorThumbnailCache&) = delete;
|
||||
|
||||
private:
|
||||
EditorThumbnailCache() = default;
|
||||
~EditorThumbnailCache();
|
||||
|
||||
struct Entry {
|
||||
ImTextureData* tex = nullptr; // 非拥有(删由回收流程)
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
ThumbView::State state = ThumbView::State::Pending;
|
||||
unsigned long long lastUseFrame = 0;
|
||||
int pendingFrames = 0; // 注册后等待后端上传的帧数
|
||||
};
|
||||
|
||||
void DecodeOne(const std::string& path, Entry& e); // 解码→降采样→注册
|
||||
// 源像素(RGBA8,sw×sh)→ 等比降采样 → ImTextureData 注册(解码与
|
||||
// 烘焙回读注入共用同一条上传路径)
|
||||
static void UploadRGBA8(const unsigned char* src, int sw, int sh, Entry& e);
|
||||
void InjectRGBA8(const std::string& path, const unsigned char* rgba,
|
||||
int w, int h);
|
||||
bool RetireOne(); // 淘汰最久未用(返回是否淘汰)
|
||||
void DrainRetired(); // 后端已释放→回收
|
||||
|
||||
std::unordered_map<std::string, Entry> m_Entries;
|
||||
std::deque<std::string> m_Queue; // 待解码(重复请求命 entries,天然去重)
|
||||
std::vector<ImTextureData*> m_Retire; // 已标 WantDestroy,等后端释放
|
||||
unsigned long long m_Frame = 0;
|
||||
|
||||
// 材质球烘焙 path ↔ key 映射(幂等分配;回读完成即注销)
|
||||
unsigned long long m_BakeKeyCounter = 0;
|
||||
std::unordered_map<std::string, unsigned long long> m_PathToBakeKey;
|
||||
std::unordered_map<unsigned long long, std::string> m_BakeKeyToPath;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_THUMBNAILCACHE_H
|
||||
@@ -1,10 +1,17 @@
|
||||
//
|
||||
// AssetBrowserPanel.cpp:项目资产浏览器实现。
|
||||
//
|
||||
// 条目视觉/拖拽/tooltip 单元住 AssetItemView(列表行与网格瓦片共用同一
|
||||
// 套类别色 + 缩略图);本文件只负责:根切换 / 过滤 / 目录树 / 内容区
|
||||
// 铺排 / 选中态 / 双击分派(打开或实例化)。
|
||||
//
|
||||
|
||||
#include "AssetBrowserPanel.h"
|
||||
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
|
||||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||||
|
||||
#include "AssetItemView.h" // 列表行 / 网格瓦片 / 目录条目
|
||||
#include "AssetKind.h" // EditorAsset::AssetKind / KindOfPath
|
||||
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
|
||||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
@@ -13,10 +20,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -25,79 +30,26 @@ namespace {
|
||||
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
|
||||
constexpr const char* kRootPaths[] = {"assets", "scenes"};
|
||||
|
||||
// 类型过滤(下标即身份):0 = All
|
||||
// 类型过滤(下标即身份):0 = All;其余对齐 AssetKind 顺序
|
||||
constexpr const char* kTypeNames[] = {"All", "Models",
|
||||
"Textures", "Scenes",
|
||||
"Materials", "Controllers",
|
||||
"Prefabs"};
|
||||
|
||||
// 资产类别:按扩展名分派(过滤用)
|
||||
enum class Kind { Model, Texture, Scene, Material, AnimClip, Prefab,
|
||||
Other };
|
||||
|
||||
Kind KindOf(const std::filesystem::path& p)
|
||||
{
|
||||
std::string ext = p.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
|
||||
ext == ".obj")
|
||||
{
|
||||
return Kind::Model;
|
||||
}
|
||||
if (ext == ".hdr" || ext == ".png" || ext == ".jpg" ||
|
||||
ext == ".jpeg")
|
||||
{
|
||||
return Kind::Texture;
|
||||
}
|
||||
if (ext == ".scene")
|
||||
{
|
||||
return Kind::Scene;
|
||||
}
|
||||
if (ext == ".material")
|
||||
{
|
||||
return Kind::Material;
|
||||
}
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
return Kind::AnimClip;
|
||||
}
|
||||
if (ext == ".prefab")
|
||||
{
|
||||
return Kind::Prefab;
|
||||
}
|
||||
return Kind::Other;
|
||||
}
|
||||
|
||||
bool PassFilter(int typeFilter, Kind kind)
|
||||
bool PassFilter(int typeFilter, EditorAsset::AssetKind kind)
|
||||
{
|
||||
switch (typeFilter)
|
||||
{
|
||||
case 1: return kind == Kind::Model;
|
||||
case 2: return kind == Kind::Texture;
|
||||
case 3: return kind == Kind::Scene;
|
||||
case 4: return kind == Kind::Material;
|
||||
case 5: return kind == Kind::AnimClip;
|
||||
case 6: return kind == Kind::Prefab;
|
||||
case 1: return kind == EditorAsset::AssetKind::Model;
|
||||
case 2: return kind == EditorAsset::AssetKind::Texture;
|
||||
case 3: return kind == EditorAsset::AssetKind::Scene;
|
||||
case 4: return kind == EditorAsset::AssetKind::Material;
|
||||
case 5: return kind == EditorAsset::AssetKind::AnimClip;
|
||||
case 6: return kind == EditorAsset::AssetKind::Prefab;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表条目前缀符号(ImGui 默认字体无图标字形,用 ASCII 标签)
|
||||
const char* KindTag(Kind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case Kind::Model: return "[M]";
|
||||
case Kind::Texture: return "[T]";
|
||||
case Kind::Scene: return "[S]";
|
||||
case Kind::Material: return "[Ma]";
|
||||
case Kind::AnimClip: return "[An]";
|
||||
case Kind::Prefab: return "[Pf]";
|
||||
default: return "[ ]";
|
||||
}
|
||||
}
|
||||
|
||||
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
|
||||
std::string ToAssetPath(const std::filesystem::path& p)
|
||||
{
|
||||
@@ -105,6 +57,50 @@ namespace {
|
||||
std::replace(s.begin(), s.end(), '\\', '/');
|
||||
return s;
|
||||
}
|
||||
|
||||
// 双击分派:材质/控制器 → 打开编辑器;模型/环境 → 缓存预热;
|
||||
// 场景 → 提示走 File 菜单;预制体 → 快捷实例化(授权位姿落点)
|
||||
void DispatchOpen(const EditorContext& ctx,
|
||||
const std::filesystem::path& path,
|
||||
EditorAsset::AssetKind kind)
|
||||
{
|
||||
const std::string assetPath = path.generic_string();
|
||||
if (kind == EditorAsset::AssetKind::Material && ctx.materialEditor)
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(assetPath);
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::AnimClip && ctx.controllerEditor)
|
||||
{
|
||||
ctx.controllerEditor->Open(assetPath);
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Model && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadModel(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Texture && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadEnvironment(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Scene)
|
||||
{
|
||||
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
|
||||
assetPath);
|
||||
}
|
||||
else if (kind == EditorAsset::AssetKind::Prefab && ctx.scene && ctx.assets)
|
||||
{
|
||||
// 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner 手动改);
|
||||
// 拖拽实例化见 Outliner 落点
|
||||
const auto root = Engine::Serialization::SceneSerializer::
|
||||
InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath);
|
||||
ENGINE_INFO("AssetBrowser: prefab '{}' instantiated (valid={})",
|
||||
assetPath, root != entt::null);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
@@ -152,7 +148,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
m_CurrentDir = root;
|
||||
}
|
||||
|
||||
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)----
|
||||
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)+ 视图切换 ----
|
||||
if (ImGui::Button("Root"))
|
||||
{
|
||||
m_CurrentDir = root;
|
||||
@@ -162,9 +158,28 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
.generic_string();
|
||||
ImGui::TextDisabled("%s/%s", kRootNames[m_Root],
|
||||
rel.empty() ? "" : rel.c_str());
|
||||
|
||||
// 视图切换(列表 / 网格)+ 网格瓦片尺寸
|
||||
ImGui::SameLine();
|
||||
const bool listMode = m_ViewMode == ViewMode::List;
|
||||
if (ImGui::RadioButton("List", listMode))
|
||||
{
|
||||
m_ViewMode = ViewMode::List;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton("Grid", !listMode))
|
||||
{
|
||||
m_ViewMode = ViewMode::Grid;
|
||||
}
|
||||
if (m_ViewMode == ViewMode::Grid)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::SliderFloat("##tile", &m_TileSize, 48.0f, 160.0f, "%.0f px");
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 左:目录树 | 右:当前目录文件列表 ----
|
||||
// ---- 左:目录树 | 右:当前目录内容(列表 / 网格)----
|
||||
const float treeWidth = 180.0f;
|
||||
ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f),
|
||||
ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders);
|
||||
@@ -194,7 +209,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
ImGuiChildFlags_Borders);
|
||||
if (std::filesystem::exists(m_CurrentDir))
|
||||
{
|
||||
DrawFileList(ctx, m_CurrentDir);
|
||||
DrawContent(ctx, m_CurrentDir);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -227,34 +242,25 @@ void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
|
||||
}
|
||||
}
|
||||
|
||||
void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
void AssetBrowserPanel::DrawContent(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir)
|
||||
{
|
||||
// 收集 + 过滤 + 排序(名字序;目录迭代序不稳定)
|
||||
struct Item
|
||||
{
|
||||
std::filesystem::path path;
|
||||
Kind kind;
|
||||
};
|
||||
std::vector<Item> items;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// 收集:目录(不受类型过滤影响)+ 过滤后的文件;目录迭代序不稳定 → 排序
|
||||
std::vector<fs::path> dirs;
|
||||
std::vector<std::pair<fs::path, EditorAsset::AssetKind>> files;
|
||||
const std::string filter = m_NameFilter;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir))
|
||||
std::error_code ec;
|
||||
for (const auto& entry : fs::directory_iterator(dir, ec))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
// 子目录也进列表:单行点击进入(树/列表双向导航)
|
||||
const std::string dname = entry.path().filename().string();
|
||||
ImGui::PushID(dname.c_str());
|
||||
ImGui::TextUnformatted("[D]");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Selectable(dname.c_str()))
|
||||
{
|
||||
m_CurrentDir = entry.path();
|
||||
}
|
||||
ImGui::PopID();
|
||||
dirs.push_back(entry.path());
|
||||
continue;
|
||||
}
|
||||
const Kind kind = KindOf(entry.path());
|
||||
const EditorAsset::AssetKind kind =
|
||||
EditorAsset::KindOfPath(entry.path());
|
||||
if (!PassFilter(m_TypeFilter, kind))
|
||||
{
|
||||
continue;
|
||||
@@ -267,191 +273,106 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
continue;
|
||||
}
|
||||
}
|
||||
items.push_back({entry.path(), kind});
|
||||
files.emplace_back(entry.path(), kind);
|
||||
}
|
||||
std::sort(items.begin(), items.end(),
|
||||
[](const Item& a, const Item& b)
|
||||
std::sort(dirs.begin(), dirs.end(),
|
||||
[](const fs::path& a, const fs::path& b)
|
||||
{
|
||||
return a.path.filename().string() <
|
||||
b.path.filename().string();
|
||||
return a.filename().string() < b.filename().string();
|
||||
});
|
||||
std::sort(files.begin(), files.end(),
|
||||
[](const auto& a, const auto& b)
|
||||
{
|
||||
return a.first.filename().string() <
|
||||
b.first.filename().string();
|
||||
});
|
||||
|
||||
for (const auto& item : items)
|
||||
// 双击打开延迟到循环外:Immediate GUI 语义——绘制循环内不改世界状态
|
||||
//(prefab 实例化会改场景,边遍历边打开会让后续条目的项状态错乱)
|
||||
bool pendingOpen = false;
|
||||
std::filesystem::path pendingOpenPath;
|
||||
EditorAsset::AssetKind pendingOpenKind = EditorAsset::AssetKind::Other;
|
||||
|
||||
if (m_ViewMode == ViewMode::List)
|
||||
{
|
||||
const std::string name = item.path.filename().string();
|
||||
const std::string assetPath = ToAssetPath(item.path);
|
||||
ImGui::PushID(name.c_str());
|
||||
|
||||
ImGui::TextUnformatted(KindTag(item.kind));
|
||||
ImGui::SameLine();
|
||||
ImGui::Selectable(name.c_str());
|
||||
|
||||
// .material 拖拽源:拖到 Details 材质槽球上指派(kMaterialDragPayload)
|
||||
if (item.kind == Kind::Material && ImGui::BeginDragDropSource())
|
||||
for (const auto& d : dirs)
|
||||
{
|
||||
ImGui::SetDragDropPayload(kMaterialDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// .animctrl 拖拽源:拖到 Details Controller 槽指派
|
||||
//(kControllerDragPayload,对齐材质槽交互)
|
||||
if (item.kind == Kind::AnimClip && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kControllerDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// .prefab 拖拽源:拖到 Outliner(节点=挂其下 / 空白区=挂根)
|
||||
// 实例化整棵子树(kPrefabDragPayload)
|
||||
if (item.kind == Kind::Prefab && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kPrefabDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 模型拖拽源:拖到 Outliner 落点创建带 MeshRenderer 的实体
|
||||
//(kModelDragPayload;实体名 = 文件名)
|
||||
if (item.kind == Kind::Model && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kModelDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 双击:.material 打开 Material Editor;.animctrl 打开
|
||||
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
|
||||
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
|
||||
// File 菜单
|
||||
if (ImGui::IsItemHovered() &&
|
||||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
if (item.kind == Kind::Material && ctx.materialEditor)
|
||||
if (EditorAssetView::DrawFolderRow(d))
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::AnimClip && ctx.controllerEditor)
|
||||
{
|
||||
ctx.controllerEditor->Open(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::Model && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadModel(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (item.kind == Kind::Texture && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadEnvironment(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
|
||||
assetPath, ref.Valid());
|
||||
}
|
||||
else if (item.kind == Kind::Scene)
|
||||
{
|
||||
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
|
||||
assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::Prefab && ctx.scene && ctx.assets)
|
||||
{
|
||||
// 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner
|
||||
// 手动改);拖拽实例化见 Outliner 落点
|
||||
const auto root = Engine::Serialization::SceneSerializer::
|
||||
InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath);
|
||||
ENGINE_INFO("AssetBrowser: prefab '{}' instantiated "
|
||||
"(valid={})",
|
||||
assetPath, root != entt::null);
|
||||
m_CurrentDir = d;
|
||||
m_SelectedPath.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态
|
||||
if (ImGui::IsItemHovered())
|
||||
for (const auto& [path, kind] : files)
|
||||
{
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::Separator();
|
||||
// 与 Kind 枚举逐位对齐(Kind::Other = 6)——漏项 = tooltip
|
||||
// 数组越界读
|
||||
static const char* kKindNames[] = {"Model", "Texture/Environment",
|
||||
"Scene", "Material",
|
||||
"Controller", "Prefab",
|
||||
"File"};
|
||||
ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]);
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
const auto sz = fs::file_size(item.path, ec);
|
||||
if (!ec)
|
||||
const std::string assetPath = ToAssetPath(path);
|
||||
const auto res = EditorAssetView::DrawRow(
|
||||
ctx, path, assetPath == m_SelectedPath);
|
||||
if (res.clicked)
|
||||
{
|
||||
char szBuf[32];
|
||||
if (sz >= 1024ull * 1024ull)
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f MB",
|
||||
static_cast<double>(sz) / (1024.0 * 1024.0));
|
||||
}
|
||||
else
|
||||
{
|
||||
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
|
||||
static_cast<double>(sz) / 1024.0);
|
||||
}
|
||||
ImGui::Text("size: %s", szBuf);
|
||||
m_SelectedPath = assetPath;
|
||||
}
|
||||
const auto ftime = fs::last_write_time(item.path, ec);
|
||||
if (!ec)
|
||||
if (res.doubleClicked)
|
||||
{
|
||||
// file_clock → system_clock 近似转换(C++17 无官方转换)
|
||||
const auto sctp = std::chrono::time_point_cast<
|
||||
std::chrono::system_clock::duration>(
|
||||
ftime - fs::file_time_type::clock::now() +
|
||||
std::chrono::system_clock::now());
|
||||
const std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
|
||||
char tmBuf[32];
|
||||
std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M",
|
||||
std::localtime(&tt));
|
||||
ImGui::Text("modified: %s", tmBuf);
|
||||
pendingOpen = true;
|
||||
pendingOpenPath = path;
|
||||
pendingOpenKind = kind;
|
||||
}
|
||||
// 加载状态:只对"加载"型资产有意义(查询不触发加载)。
|
||||
// Prefab 是实例化模板、Scene 走 File 菜单——显示 "not
|
||||
// loaded" 会误导(曾让用户当成 bug)
|
||||
if (ctx.assets)
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// ---- 网格视图:固定列宽表格铺排(列数按可用宽度自适应)----
|
||||
const float tile = m_TileSize;
|
||||
const float cellW = tile + ImGui::GetStyle().CellPadding.x * 2.0f;
|
||||
int cols = static_cast<int>(ImGui::GetContentRegionAvail().x / cellW);
|
||||
if (cols < 1)
|
||||
{
|
||||
cols = 1;
|
||||
}
|
||||
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
|
||||
{
|
||||
for (int i = 0; i < cols; ++i)
|
||||
{
|
||||
if (item.kind == Kind::Model ||
|
||||
item.kind == Kind::Texture)
|
||||
ImGui::TableSetupColumn("##c",
|
||||
ImGuiTableColumnFlags_WidthFixed,
|
||||
cellW);
|
||||
}
|
||||
for (const auto& d : dirs)
|
||||
{
|
||||
ImGui::TableNextColumn();
|
||||
const std::string assetPath = ToAssetPath(d);
|
||||
if (EditorAssetView::DrawFolderTile(
|
||||
d, tile, assetPath == m_SelectedPath))
|
||||
{
|
||||
const bool loaded =
|
||||
(item.kind == Kind::Model)
|
||||
? ctx.assets->IsModelLoaded(assetPath)
|
||||
: ctx.assets->IsEnvironmentLoaded(assetPath);
|
||||
ImGui::Text("state: %s", loaded ? "loaded"
|
||||
: "not loaded");
|
||||
}
|
||||
else if (item.kind == Kind::Prefab)
|
||||
{
|
||||
ImGui::TextDisabled(
|
||||
"template — drag into scene / double-click");
|
||||
}
|
||||
// 已加载的 model 展开资产数据概览(meshes=0 即纯动画
|
||||
// 容器;未加载不显示——查询不触发加载)
|
||||
if (item.kind == Kind::Model)
|
||||
{
|
||||
if (const Engine::Model* m =
|
||||
ctx.assets->GetModelByPath(assetPath);
|
||||
m != nullptr)
|
||||
{
|
||||
ImGui::Text("meshes: %u, bones: %u, clips: %u",
|
||||
static_cast<unsigned>(m->GetMeshes().size()),
|
||||
m->GetBoneCount(),
|
||||
static_cast<unsigned>(m->GetClips().size()));
|
||||
}
|
||||
m_CurrentDir = d;
|
||||
m_SelectedPath.clear();
|
||||
}
|
||||
}
|
||||
ImGui::TextDisabled("%s", assetPath.c_str());
|
||||
ImGui::EndTooltip();
|
||||
for (const auto& [path, kind] : files)
|
||||
{
|
||||
ImGui::TableNextColumn();
|
||||
const std::string assetPath = ToAssetPath(path);
|
||||
const auto res = EditorAssetView::DrawTile(
|
||||
ctx, path, tile, assetPath == m_SelectedPath);
|
||||
if (res.clicked)
|
||||
{
|
||||
m_SelectedPath = assetPath;
|
||||
}
|
||||
if (res.doubleClicked)
|
||||
{
|
||||
pendingOpen = true;
|
||||
pendingOpenPath = path;
|
||||
pendingOpenKind = kind;
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (pendingOpen)
|
||||
{
|
||||
DispatchOpen(ctx, pendingOpenPath, pendingOpenKind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +32,28 @@ public:
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
// 目录树节点(递归);右侧列表画 dir 下的文件
|
||||
// 视图模式:列表(行,含内联小缩略图)/ 网格(缩略图瓦片)
|
||||
enum class ViewMode { List, Grid };
|
||||
|
||||
// 目录树节点(递归);右侧内容区画 dir 下的目录+文件
|
||||
void DrawDirTree(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
void DrawFileList(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
void DrawContent(const EditorContext& ctx,
|
||||
const std::filesystem::path& dir);
|
||||
|
||||
// 固定根(下标即身份):项目资产 / 场景文件
|
||||
int m_Root = 0;
|
||||
// 当前浏览目录(点目录树切换;列表显示这里的文件)
|
||||
// 当前浏览目录(点目录树切换;内容区显示这里的条目)
|
||||
std::filesystem::path m_CurrentDir;
|
||||
// 类型过滤(0 = All;其余见 kTypeNames)
|
||||
int m_TypeFilter = 0;
|
||||
// 名字子串过滤(空 = 不过滤)
|
||||
char m_NameFilter[64] = "";
|
||||
// 视图模式 + 网格瓦片尺寸(像素)
|
||||
ViewMode m_ViewMode = ViewMode::List;
|
||||
float m_TileSize = 96.0f;
|
||||
// 当前选中条目(资产相对路径;空 = 无选中)
|
||||
std::string m_SelectedPath;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_ASSETBROWSERPANEL_H
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "HierarchyPanel.h"
|
||||
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
|
||||
#include "EditorColors.h" // 实体名/组件徽标配色
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
|
||||
#include "Engine/Serialization/SceneSerializer.h"
|
||||
@@ -73,6 +74,25 @@ namespace {
|
||||
return static_cast<entt::entity>(e);
|
||||
}
|
||||
|
||||
// 主组件 → 实体名颜色(优先级高者胜,用于 Outliner 可读性)
|
||||
ImVec4 PrimaryColor(const entt::registry& reg, entt::entity e)
|
||||
{
|
||||
using namespace Engine::ECS;
|
||||
if (reg.all_of<CameraComponent>(e)) return EditorColors::kCamera;
|
||||
if (reg.all_of<LightComponent>(e)) return EditorColors::kLight;
|
||||
if (reg.all_of<SkyboxComponent>(e) ||
|
||||
reg.all_of<SkyLightComponent>(e)) return EditorColors::kSky;
|
||||
if (reg.all_of<MeshRendererComponent>(e)) return EditorColors::kMesh;
|
||||
if (reg.all_of<AnimationComponent>(e)) return EditorColors::kAnimB;
|
||||
if (reg.all_of<ScriptComponent>(e)) return EditorColors::kScript;
|
||||
if (reg.all_of<RigidBodyComponent>(e) ||
|
||||
reg.all_of<CharacterControllerComponent>(e))
|
||||
{
|
||||
return EditorColors::kPhysics;
|
||||
}
|
||||
return EditorColors::kEntityNeutral;
|
||||
}
|
||||
|
||||
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
|
||||
std::string PrefabFileName(std::string name)
|
||||
{
|
||||
@@ -120,7 +140,10 @@ namespace {
|
||||
(kids.empty() ? ImGuiTreeNodeFlags_Leaf : 0) |
|
||||
(selected == e ? ImGuiTreeNodeFlags_Selected : 0);
|
||||
|
||||
// 实体名按主组件着色(无组件 = 中性色)
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, PrimaryColor(reg, e));
|
||||
const bool opened = ImGui::TreeNodeEx(label.c_str(), flags);
|
||||
ImGui::PopStyleColor();
|
||||
const bool toggled = ImGui::IsItemToggledOpen();
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !toggled) {
|
||||
selected = e;
|
||||
@@ -208,20 +231,36 @@ namespace {
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// ---- 组件徽标(与行内同级显示)----
|
||||
std::string badge;
|
||||
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
|
||||
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
|
||||
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
|
||||
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
|
||||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e)) badge += "[P]";
|
||||
if (!badge.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("%s", badge.c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"M=Mesh A=Animation C=Camera L=Light P=Physics");
|
||||
}
|
||||
// ---- 组件徽标(逐组件着色,与行内同级显示)----
|
||||
bool anyBadge = false;
|
||||
auto badge = [&](const char* text, const ImVec4& col)
|
||||
{
|
||||
ImGui::SameLine(0.0f, 6.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, col);
|
||||
ImGui::TextUnformatted(text);
|
||||
ImGui::PopStyleColor();
|
||||
anyBadge = true;
|
||||
};
|
||||
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e))
|
||||
badge("M", EditorColors::kMesh);
|
||||
if (reg.all_of<Engine::ECS::AnimationComponent>(e))
|
||||
badge("A", EditorColors::kAnimB);
|
||||
if (reg.all_of<Engine::ECS::CameraComponent>(e))
|
||||
badge("C", EditorColors::kCamera);
|
||||
if (reg.all_of<Engine::ECS::LightComponent>(e))
|
||||
badge("L", EditorColors::kLight);
|
||||
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e) ||
|
||||
reg.all_of<Engine::ECS::CharacterControllerComponent>(e))
|
||||
badge("P", EditorColors::kPhysics);
|
||||
if (reg.all_of<Engine::ECS::ScriptComponent>(e))
|
||||
badge("S", EditorColors::kScript);
|
||||
if (reg.all_of<Engine::ECS::SkyboxComponent>(e) ||
|
||||
reg.all_of<Engine::ECS::SkyLightComponent>(e))
|
||||
badge("K", EditorColors::kSky);
|
||||
if (anyBadge && ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"M=Mesh A=Animation C=Camera L=Light P=Physics "
|
||||
"S=Script K=Sky");
|
||||
}
|
||||
|
||||
if (opened) {
|
||||
|
||||
@@ -686,8 +686,13 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
}
|
||||
if (ImGui::BeginPopup("##proc_model"))
|
||||
{
|
||||
static int shape = 0; // 0=cube 1=plane 2=transparent plane
|
||||
static float half = 0.5f;
|
||||
// 形状集合与 MeshFactory::ShapeNames 对齐
|
||||
static int shape = 0; // 0=cube 1=plane 2=tplane
|
||||
// 3=sphere 4=cylinder 5=cone 6=torus
|
||||
static float half = 0.5f; // 半径 / 半边长
|
||||
static float height = 1.0f; // 圆柱 / 圆锥高
|
||||
static float majorR = 0.5f; // 圆环主半径
|
||||
static float minorR = 0.15f; // 圆环管半径
|
||||
static float col[3] = {0.55f, 0.36f, 0.28f};
|
||||
static float alpha = 0.5f;
|
||||
ImGui::RadioButton("Cube", &shape, 0);
|
||||
@@ -695,11 +700,32 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
ImGui::RadioButton("Plane", &shape, 1);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Trans.Plane", &shape, 2);
|
||||
ImGui::RadioButton("Sphere", &shape, 3);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Cylinder", &shape, 4);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Cone", &shape, 5);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Torus", &shape, 6);
|
||||
// 固定宽度而非 -1:弹窗是 AlwaysAutoResize,-1(撑满可用
|
||||
// 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕
|
||||
ImGui::SetNextItemWidth(180.0f);
|
||||
ImGui::SliderFloat("half size", &half, 0.01f, 10.0f,
|
||||
"%.2f");
|
||||
ImGui::SliderFloat("size", &half, 0.01f, 10.0f, "%.2f");
|
||||
if (shape == 4 || shape == 5)
|
||||
{
|
||||
ImGui::SetNextItemWidth(180.0f);
|
||||
ImGui::SliderFloat("height", &height, 0.01f, 10.0f,
|
||||
"%.2f");
|
||||
}
|
||||
if (shape == 6)
|
||||
{
|
||||
ImGui::SetNextItemWidth(180.0f);
|
||||
ImGui::SliderFloat("major radius", &majorR, 0.01f, 10.0f,
|
||||
"%.2f");
|
||||
ImGui::SetNextItemWidth(180.0f);
|
||||
ImGui::SliderFloat("minor radius", &minorR, 0.01f, 5.0f,
|
||||
"%.2f");
|
||||
}
|
||||
ImGui::ColorEdit3("color", col);
|
||||
if (shape == 2)
|
||||
{
|
||||
@@ -712,21 +738,37 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
const int r = int(col[0] * 255.0f + 0.5f);
|
||||
const int g = int(col[1] * 255.0f + 0.5f);
|
||||
const int b = int(col[2] * 255.0f + 0.5f);
|
||||
char key[96];
|
||||
if (shape == 2)
|
||||
char key[128];
|
||||
switch (shape)
|
||||
{
|
||||
const int a = int(alpha * 255.0f + 0.5f);
|
||||
case 2:
|
||||
std::snprintf(
|
||||
key, sizeof(key),
|
||||
"procedural://transparent-plane/%.3g/%d,%d,%d,%d",
|
||||
half, r, g, b, a);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::snprintf(
|
||||
key, sizeof(key),
|
||||
half, r, g, b, int(alpha * 255.0f + 0.5f));
|
||||
break;
|
||||
case 3:
|
||||
std::snprintf(key, sizeof(key),
|
||||
"procedural://sphere/%.3g/%d,%d,%d",
|
||||
half, r, g, b);
|
||||
break;
|
||||
case 4:
|
||||
case 5:
|
||||
std::snprintf(key, sizeof(key),
|
||||
"procedural://%s/%.3g_%.3g/%d,%d,%d",
|
||||
shape == 4 ? "cylinder" : "cone",
|
||||
half, height, r, g, b);
|
||||
break;
|
||||
case 6:
|
||||
std::snprintf(key, sizeof(key),
|
||||
"procedural://torus/%.3g_%.3g/%d,%d,%d",
|
||||
majorR, minorR, r, g, b);
|
||||
break;
|
||||
default:
|
||||
std::snprintf(key, sizeof(key),
|
||||
"procedural://%s/%.3g/%d,%d,%d",
|
||||
shape == 0 ? "cube" : "plane", half, r, g, b);
|
||||
break;
|
||||
}
|
||||
if (auto ref = Engine::Serialization::
|
||||
SceneSerializer::ResolveProcedural(
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
#include "MaterialEditorPanel.h"
|
||||
#include "MaterialAssetUtil.h"
|
||||
#include "AssetItemView.h" // 纹理选择器网格(缩略图瓦片)
|
||||
#include "EditorColors.h"
|
||||
#include "ThumbnailCache.h"
|
||||
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
@@ -17,6 +20,7 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -34,6 +38,125 @@ namespace {
|
||||
}
|
||||
|
||||
constexpr float kPreviewSize = 128.0f; // 编辑器顶部大球(Inspector 槽位为 48²)
|
||||
|
||||
// 纹理槽缩略图(同时是拖拽落点):Content Browser 拖纹理资产落入即
|
||||
// 指派该槽(kTextureDragPayload)。空槽显示 "nil" 占位
|
||||
void SlotThumb(const std::string& texPath, float sz, int slotIdx,
|
||||
int& pendingSlot, std::string& pendingPath)
|
||||
{
|
||||
const ImVec4 accent = EditorColors::kTexture;
|
||||
ImGui::Dummy(ImVec2(sz, sz));
|
||||
const ImVec2 p0 = ImGui::GetItemRectMin();
|
||||
const ImVec2 p1 = ImGui::GetItemRectMax();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
bool drawn = false;
|
||||
if (!texPath.empty())
|
||||
{
|
||||
const ThumbView v = EditorThumbnailCache::Get().Request(texPath);
|
||||
if (v.state == ThumbView::State::Ready)
|
||||
{
|
||||
float fw = sz, fh = sz;
|
||||
if (v.w > 0 && v.h > 0)
|
||||
{
|
||||
if (v.w >= v.h) fh = sz * static_cast<float>(v.h) / v.w;
|
||||
else fw = sz * static_cast<float>(v.w) / v.h;
|
||||
}
|
||||
const ImVec2 f0(p0.x + (sz - fw) * 0.5f,
|
||||
p0.y + (sz - fh) * 0.5f);
|
||||
dl->AddImage(v.tex, f0, ImVec2(f0.x + fw, f0.y + fh));
|
||||
drawn = true;
|
||||
}
|
||||
}
|
||||
if (!drawn)
|
||||
{
|
||||
// 背板只在未出图时铺:出图时叠加会污染透明像素
|
||||
dl->AddRectFilled(
|
||||
p0, p1,
|
||||
ImGui::GetColorU32(ImVec4(accent.x * 0.25f, accent.y * 0.25f,
|
||||
accent.z * 0.25f, 1.0f)),
|
||||
3.0f);
|
||||
const char* lbl = texPath.empty() ? "nil" : "TEX";
|
||||
const ImVec2 ts = ImGui::CalcTextSize(lbl);
|
||||
dl->AddText(ImVec2(p0.x + (sz - ts.x) * 0.5f,
|
||||
p0.y + (sz - ts.y) * 0.5f),
|
||||
ImGui::GetColorU32(texPath.empty()
|
||||
? EditorColors::kOther
|
||||
: accent),
|
||||
lbl);
|
||||
}
|
||||
dl->AddRect(p0, p1,
|
||||
ImGui::GetColorU32(hovered ? EditorColors::Lighten(accent,
|
||||
0.3f)
|
||||
: accent),
|
||||
3.0f);
|
||||
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(kTextureDragPayload))
|
||||
{
|
||||
pendingSlot = slotIdx;
|
||||
pendingPath = std::string(static_cast<const char*>(pl->Data));
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
// 纹理选择器内容(弹窗内):<none> 清除入口 + 缩略图网格。
|
||||
// 返回 true = 本次有选择,选中路径写入 out(空串 = <none>)
|
||||
bool TexturePickerContent(const EditorContext& ctx, std::string& out)
|
||||
{
|
||||
bool picked = false;
|
||||
if (ImGui::Selectable("<none>"))
|
||||
{
|
||||
out.clear();
|
||||
picked = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
const auto& files = EditorMaterialUtil::AssetTextureFiles();
|
||||
if (files.empty())
|
||||
{
|
||||
ImGui::TextDisabled("(no textures under assets/)");
|
||||
return picked;
|
||||
}
|
||||
|
||||
constexpr float kTile = 64.0f;
|
||||
const float cellW = kTile + ImGui::GetStyle().CellPadding.x * 2.0f;
|
||||
int cols = static_cast<int>(360.0f / cellW);
|
||||
if (cols < 1)
|
||||
{
|
||||
cols = 1;
|
||||
}
|
||||
ImGui::BeginChild("##texgrid", ImVec2(360.0f, 260.0f),
|
||||
ImGuiChildFlags_Borders);
|
||||
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
|
||||
{
|
||||
for (int i = 0; i < cols; ++i)
|
||||
{
|
||||
ImGui::TableSetupColumn("##c",
|
||||
ImGuiTableColumnFlags_WidthFixed,
|
||||
cellW);
|
||||
}
|
||||
for (const auto& f : files)
|
||||
{
|
||||
ImGui::TableNextColumn();
|
||||
const auto r =
|
||||
EditorAssetView::DrawTile(ctx, f, kTile, false);
|
||||
if (r.clicked)
|
||||
{
|
||||
out = f;
|
||||
picked = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
return picked;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MaterialEditorPanel::OpenAsset(const std::string& path)
|
||||
@@ -211,6 +334,15 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
if (c) { p.misc.z = flipV ? 1.0f : 0.0f; }
|
||||
EditParam(c);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
{
|
||||
// DirectX 法线绿通道翻转(UE 打包 Specular 资产由导入器默认为开;
|
||||
// 方向反了在此逐材质纠正)。写 misc.w,旧 .material 缺省 0 = 关闭
|
||||
bool flipG = p.misc.w > 0.5f;
|
||||
const bool c = ImGui::Checkbox("Flip Green (DirectX)", &flipG);
|
||||
if (c) { p.misc.w = flipG ? 1.0f : 0.0f; }
|
||||
EditParam(c);
|
||||
}
|
||||
|
||||
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
|
||||
ImGui::SeparatorText("Textures");
|
||||
@@ -219,10 +351,17 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
"Emissive", "Ao", "Roughness"};
|
||||
int pendingSlot = -1;
|
||||
std::string pendingPath;
|
||||
const float thumbSz = ImGui::GetTextLineHeight() * 1.8f;
|
||||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||||
{
|
||||
std::string tp = asset->GetData().texPaths[s];
|
||||
ImGui::PushID(static_cast<int>(s));
|
||||
|
||||
// 缩略图块(落点:拖入纹理资产即指派该槽)
|
||||
SlotThumb(tp, thumbSz, static_cast<int>(s), pendingSlot, pendingPath);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
InputTextString(kSlotNames[s], tp);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
@@ -236,20 +375,13 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
}
|
||||
if (ImGui::BeginPopup("tex_pick"))
|
||||
{
|
||||
// 清除入口(Unity/UE 语义):该槽置空 → 回退兜底贴图
|
||||
if (ImGui::Selectable("<none>"))
|
||||
// 弹窗内只做选择;pending 统一在循环外落盘(避免 popup 内
|
||||
// 立即重建导致 asset 指针悬空)
|
||||
std::string chosen;
|
||||
if (TexturePickerContent(ctx, chosen))
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath.clear();
|
||||
}
|
||||
ImGui::Separator();
|
||||
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
|
||||
{
|
||||
if (ImGui::Selectable(f.c_str()))
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath = f;
|
||||
}
|
||||
pendingPath = chosen;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
// 材质资产拖拽 payload(Content Browser 拖 .material → Details 槽位)
|
||||
inline constexpr const char* kMaterialDragPayload = "MAT_PATH";
|
||||
|
||||
// 纹理资产拖拽 payload(Content Browser 拖 .png/.jpg/... → 材质纹理槽)
|
||||
inline constexpr const char* kTextureDragPayload = "TEXTURE_PATH";
|
||||
|
||||
class MaterialEditorPanel {
|
||||
public:
|
||||
// 打开 .material 资产(可编辑模式)
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
|
||||
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
|
||||
//
|
||||
// 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近;
|
||||
// 拉近瞬时、拉远平滑,贴墙最小距离可配。
|
||||
//
|
||||
|
||||
#include <Engine/Scripting/Script.h>
|
||||
#include <Engine/Core/Application.h>
|
||||
@@ -18,17 +21,24 @@
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Physics/PhysicsRay.h> // RayHit(防穿墙射线结果)
|
||||
#include <Engine/Physics/PhysicsWorld.h> // App().GetPhysics().RayCastStatic 门面
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
|
||||
// 拉远回弹响应(1/s,指数逼近):离开墙角后镜头平滑归位。
|
||||
// 拉近是瞬时的(见 OnUpdate)——遮挡晚一帧就会穿墙,不能平滑
|
||||
constexpr float kCameraReturnResponse = 8.0f;
|
||||
|
||||
class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior {
|
||||
public:
|
||||
const char* Name() const override { return "ThirdPersonCamera"; }
|
||||
@@ -50,6 +60,13 @@ namespace {
|
||||
pb.Field("FollowResponse", &m_FollowResponse, 12.0f);
|
||||
// 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附
|
||||
pb.Field("SnapDistance", &m_SnapDistance, 3.0f);
|
||||
// 相机防穿墙(UE SpringArm 同位):枢轴→期望机位之间若有静态
|
||||
// 几何,把相机拉到命中点前。关掉 = 旧行为(可穿墙)
|
||||
pb.Field("CameraCollision", &m_Collision, true);
|
||||
// 贴墙时相机离枢轴的最小距离(再近会切进角色/近平面)
|
||||
pb.Field("MinDistance", &m_MinDistance, 0.35f);
|
||||
// 命中点回退余量(防止相机面贴墙 → z-fighting/切面)
|
||||
pb.Field("CollisionSkin", &m_CollisionSkin, 0.25f);
|
||||
}
|
||||
|
||||
void OnStart() override {
|
||||
@@ -154,8 +171,31 @@ namespace {
|
||||
std::sin(yawR) * std::cos(pitchR),
|
||||
std::sin(pitchR),
|
||||
std::cos(yawR) * std::cos(pitchR));
|
||||
// ---- 相机防穿墙:枢轴 → 期望机位射线拉近 ----
|
||||
// 只打静态几何(碎块/动态物不当遮挡,避免镜头被飘过的碎片
|
||||
// 乱拉);枢轴在角色胶囊内,角色是 CharacterVirtual 不进射线,
|
||||
// 不会自己挡自己。拉近瞬时(遮挡晚一帧就穿墙),拉远平滑
|
||||
float wantDist = m_Distance;
|
||||
if (m_Collision) {
|
||||
RayHit hit;
|
||||
if (App().GetPhysics().RayCastStatic(
|
||||
pivot, dir, m_Distance + m_CollisionSkin, hit)) {
|
||||
wantDist = std::max(hit.distance - m_CollisionSkin,
|
||||
m_MinDistance);
|
||||
}
|
||||
}
|
||||
if (!m_DistInit) { // 首帧吸附(无回弹动画)
|
||||
m_CurDist = wantDist;
|
||||
m_DistInit = true;
|
||||
} else if (wantDist < m_CurDist) {
|
||||
m_CurDist = wantDist; // 拉近:瞬时
|
||||
} else {
|
||||
m_CurDist += (wantDist - m_CurDist) *
|
||||
(1.0f - std::exp(-kCameraReturnResponse * dt));
|
||||
}
|
||||
|
||||
auto& camT = Transform();
|
||||
camT.SetPosition(pivot + dir * m_Distance);
|
||||
camT.SetPosition(pivot + dir * m_CurDist);
|
||||
|
||||
// ---- 朝向锁定目标 ----
|
||||
// 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY)
|
||||
@@ -178,6 +218,11 @@ namespace {
|
||||
float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m
|
||||
float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟)
|
||||
bool m_FollowInit = false; // 首帧吸附基准
|
||||
bool m_Collision = true; // 相机防穿墙开关
|
||||
float m_MinDistance = 0.35f; // 贴墙最小距离
|
||||
float m_CollisionSkin = 0.25f; // 命中点回退余量
|
||||
float m_CurDist = 0.0f; // 防穿墙后的当前距离(拉近平滑态)
|
||||
bool m_DistInit = false; // 距离首帧吸附基准
|
||||
float m_Yaw = 0.0f;
|
||||
float m_Pitch = 20.0f;
|
||||
entt::entity m_Target = entt::null;
|
||||
|
||||
@@ -229,6 +229,12 @@ namespace Engine
|
||||
// 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存
|
||||
ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b);
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名 + 参数):同形状同参数同色
|
||||
// 缓存去重。几何唯一源 = MeshFactory(见 MeshFactory.h)
|
||||
ModelRef CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b);
|
||||
|
||||
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
|
||||
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
|
||||
EnvRef LoadEnvironment(const std::string& path);
|
||||
|
||||
@@ -45,6 +45,12 @@ namespace Engine::Assets {
|
||||
// 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟)
|
||||
virtual Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
|
||||
// cone/torus 等):形状 + 参数 → 单 mesh 模型。非法形状返回 nullptr
|
||||
virtual Scope<Model> CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b) = 0;
|
||||
|
||||
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
|
||||
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Engine::ECS {
|
||||
// 环境类设置已迁为环境实体组件(SkyboxComponent/SkyLightComponent,
|
||||
// UE Skybox/SkyLight Actor 语义);仅保留渲Target清屏色
|
||||
struct Settings {
|
||||
glm::vec4 ClearColor = {0.1, 0.1, 0.1, 1.0};
|
||||
glm::vec4 ClearColor = {0.0, 0.0, 0.0, 1.0};
|
||||
};
|
||||
Settings& GetSettings() { return m_Settings; }
|
||||
const Settings& GetSettings() const { return m_Settings; } // 序列化只读
|
||||
|
||||
@@ -79,6 +79,11 @@ namespace Engine {
|
||||
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
|
||||
f32 maxDist, RayHit& outHit) const;
|
||||
|
||||
// 仅命中静态(非移动层)几何:忽略碎块/动态物/NPC。相机防穿墙用它
|
||||
// ——动态碎块撞进枢轴→相机射线会把镜头乱拉(抖动),静态语义才稳
|
||||
bool RayCastStatic(const glm::vec3& origin, const glm::vec3& dir,
|
||||
f32 maxDist, RayHit& outHit) const;
|
||||
|
||||
// ---- 刚体驱动(游戏逻辑/编辑器调试;按实体位模式寻址)----
|
||||
// Jolt 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
|
||||
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
|
||||
@@ -154,6 +159,10 @@ namespace Engine {
|
||||
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
|
||||
ECS::TransformComponent& tr);
|
||||
void RemoveBody(u32 entityBits);
|
||||
// RayCast / RayCastStatic 共用实现:layerFilter 为空 = 命中所有层
|
||||
bool RayCastImpl(const glm::vec3& origin, const glm::vec3& dir,
|
||||
f32 maxDist, RayHit& outHit,
|
||||
const JPH::ObjectLayerFilter* layerFilter) const;
|
||||
const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址
|
||||
CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址
|
||||
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
|
||||
|
||||
@@ -314,7 +314,10 @@ namespace Engine::RG {
|
||||
void ComputeLifetimes();
|
||||
void PrecomputeBarriers();
|
||||
void AllocatePhysical();
|
||||
void ReleasePhysical();
|
||||
// keepFixedSize = true:固定尺寸资源(desc 显式 width/height,与目标
|
||||
// 渲染尺寸无关)的物理镜像不归还池、保留在 entry 上——重编译后原样
|
||||
// 沿用同一 VkImage,内容存续(材质球预览 atlas 不因视口 resize 清空)
|
||||
void ReleasePhysical(bool keepFixedSize);
|
||||
void PurgePool(vk::Extent2D target);
|
||||
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
|
||||
vk::ImageUsageFlags usage,
|
||||
|
||||
@@ -67,12 +67,10 @@ namespace Engine {
|
||||
static bool Save(const std::string& path, const MaterialAssetData& data);
|
||||
static bool Load(const std::string& path, MaterialAssetData& out);
|
||||
|
||||
// GPU 段注入(仅 VulkanAssetUploader 调用):ownedTextures =
|
||||
// 本资产加载的纹理(独占所有权),material = 组装好的材质
|
||||
//(空槽已由 uploader 绑共享默认纹理)
|
||||
MaterialAsset(MaterialAssetData&& data,
|
||||
std::array<Scope<Texture>, Material::kSlotCount>&& ownedTextures,
|
||||
Scope<Material>&& material);
|
||||
// GPU 段注入(仅 VulkanAssetUploader 调用):material = 组装好的
|
||||
// 材质(空槽已由 uploader 绑共享默认纹理)。纹理所有权归 uploader
|
||||
// 的路径缓存(跨材质去重),本资产只持有绑进 Material 的裸指针
|
||||
MaterialAsset(MaterialAssetData&& data, Scope<Material>&& material);
|
||||
// 析构外置(成员为重资源句柄,销毁序与 GPU 生命周期解耦遵循
|
||||
// Model/Material 既有模式)
|
||||
~MaterialAsset();
|
||||
@@ -87,8 +85,6 @@ namespace Engine {
|
||||
|
||||
private:
|
||||
MaterialAssetData m_Data;
|
||||
// 本资产独占的纹理(共享默认纹理不在其中,由 uploader 持有)
|
||||
std::array<Scope<Texture>, Material::kSlotCount> m_OwnedTextures;
|
||||
Scope<Material> m_Material;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/18.
|
||||
//
|
||||
// MeshFactory:预制图元的 CPU 几何生成端(唯一几何源)。
|
||||
//
|
||||
// 定位对齐 UE 的 Engine\Source\Runtime\Engine\...(内置基础网格)思路:
|
||||
// 引擎只保留一套"参数化图元 → 顶点/索引"的生成器,程序化 Model、
|
||||
// 物理形状、脚本与编辑器下拉全部复用它,避免各路径各写一份立方体。
|
||||
//
|
||||
// 契约:
|
||||
// - 所有图元中心在局部原点(平面在 y=0 的 XZ 面),尺寸为世界单位,
|
||||
// 尺寸变化统一走实体 Transform 缩放,不按尺寸炸资产缓存;
|
||||
// - 面法线朝外、CCW 从外正视,可被背面剔除;
|
||||
// - 切线已按 UV 正交化(法线贴图 TBN 可用),退化为任一与 N 正交向量;
|
||||
// - 输出 LOD0 单级索引(图元顶点少,无简化链价值)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MESHFACTORY_H
|
||||
#define HELLOVULKAN_MESHFACTORY_H
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Vertex.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
// 图元参数(形状无关的标量集):各形状只读取自己关心的字段。
|
||||
// 默认值给"半径/半边长 0.5、高 1"的规格化尺寸,贴合单位立方体约定。
|
||||
struct PrimitiveDesc {
|
||||
f32 size = 0.5f; // 半边长(cube/plane)/ 半径(sphere/cylinder/cone)
|
||||
f32 height = 1.0f; // 圆柱/圆锥高(沿 +Y,几何中心在原点)
|
||||
f32 majorRadius = 0.5f; // 圆环主半径
|
||||
f32 minorRadius = 0.15f; // 圆环管半径
|
||||
u32 segments = 32; // 环绕细分(球/柱/锥/圆环主环)
|
||||
u32 rings = 16; // 纵向细分(球)/ 圆环管环细分
|
||||
u32 subdivisions = 1; // 平面每轴网格细分
|
||||
};
|
||||
|
||||
// 预制图元的 CPU 几何(渲染 Mesh 与物理/脚本共用)。
|
||||
struct MeshData {
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<u32> indices; // LOD0(三角形列表)
|
||||
};
|
||||
|
||||
class MeshFactory {
|
||||
public:
|
||||
// 按名字取形状(ShapeNames 中任一);未知名字返回空 MeshData。
|
||||
static MeshData Make(const std::string& shape, const PrimitiveDesc& desc);
|
||||
|
||||
// ---- 具名构造(编辑器下拉 / 物理形状 / 脚本可直接调用)----
|
||||
static MeshData Cube(f32 halfSize);
|
||||
static MeshData Plane(f32 halfSize, u32 subdivisions = 1);
|
||||
static MeshData Sphere(f32 radius, u32 segments = 32, u32 rings = 16);
|
||||
static MeshData Cylinder(f32 radius, f32 height, u32 segments = 32);
|
||||
static MeshData Cone(f32 radius, f32 height, u32 segments = 32);
|
||||
static MeshData Torus(f32 majorRadius, f32 minorRadius,
|
||||
u32 majorSegments = 32, u32 minorSegments = 16);
|
||||
|
||||
// 已知图元名(UI 下拉 / 伪路径校验的唯一定义点)
|
||||
static const std::vector<std::string>& ShapeNames();
|
||||
static bool IsKnownShape(const std::string& shape);
|
||||
|
||||
private:
|
||||
// 逐三角面积加权累积切线 → 对 N 正交化 → 归一(退化取正交兜底)
|
||||
static void ComputeTangents(MeshData& mesh);
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_MESHFACTORY_H
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Engine/Core/Types.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <Engine/Renderer/MeshFactory.h> // 程序化几何唯一源(MeshData/PrimitiveDesc)
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <vulkan/vulkan.hpp>
|
||||
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
|
||||
@@ -150,9 +151,12 @@ namespace Engine
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ParsedModel&& parsed);
|
||||
|
||||
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||
// size 为半边长(平面总边长 = 2*size)。
|
||||
// 程序化几何标签:几何统一来自 MeshFactory(单一几何源),
|
||||
// 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model
|
||||
struct ProceduralTag {};
|
||||
|
||||
// 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
|
||||
// size 为半边长(平面总边长 = 2*size)。
|
||||
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
@@ -164,15 +168,25 @@ namespace Engine
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
|
||||
|
||||
// 程序化立方体(6 面 24 顶点纯色贴图,halfSize = 半边长):
|
||||
// 程序化立方体(halfSize = 半边长,几何走 MeshFactory::Cube):
|
||||
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
|
||||
// 缩放——同 halfSize 同色共享一份缓存模型
|
||||
struct CubeTag {};
|
||||
static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
f32 halfSize, u8 r, u8 g, u8 b);
|
||||
|
||||
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
|
||||
// cone/torus 等):几何由 MeshFactory::Make 生成,纯色 albedo。
|
||||
// 非法形状名返回 nullptr(调用方回退)
|
||||
static Scope<Model> CreatePrimitive(Device& device,
|
||||
MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b);
|
||||
|
||||
~Model();
|
||||
|
||||
Model(const Model&) = delete;
|
||||
@@ -218,18 +232,14 @@ namespace Engine
|
||||
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
|
||||
|
||||
private:
|
||||
// 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引
|
||||
// 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。
|
||||
// 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
|
||||
bool transparent);
|
||||
|
||||
// 程序化立方体骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 由
|
||||
// 立方体路径填(6 面,顶点法线朝外)
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
CubeTag, f32 halfSize, const std::array<u8, 4>& rgba);
|
||||
ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
|
||||
bool transparent, const std::string& name);
|
||||
|
||||
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
|
||||
Model(Device& device, MemoryAllocator& allocator,
|
||||
|
||||
@@ -37,6 +37,15 @@ namespace Engine::Passes {
|
||||
void RecordSharedState(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd, bool translucent) const;
|
||||
|
||||
// 透明队列绘制单位总数(= 各批实例数之和;排序后扁平单位序)
|
||||
u32 TransparentUnitCount() const;
|
||||
|
||||
// 绘制透明单位区间 [start,end)(批次内连续实例合并为一次 draw):
|
||||
// 透明渲染实例 + 预乘 alpha 混合 + 只测不写深度的全部状态
|
||||
void DrawTransparentRange(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd,
|
||||
u32 start, u32 end) const;
|
||||
|
||||
SceneGpu& m_Gpu;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// "HitProxy" —— 拾取请求帧把不透明 + 透明 batch 用 id 管线画进
|
||||
// R32Uint 目标(片元值 = 实体位模式)。深度自测自写
|
||||
// (重清主深度):遮挡语义 = 可见表面胜出;运行在
|
||||
// Forward/ForwardTrans/PhysicsDebug 之后的 WAW 链尾,
|
||||
// Forward/ForwardTransL*/PhysicsDebug 之后的 WAW 链尾,
|
||||
// 主链下游无人再读深度,下帧 Forward 照常重新 Clear
|
||||
// "HitProxyCopy" —— 请求像素 1×1 CopyImageToBuffer 进 host-visible
|
||||
// 暂存。TransferSrc use 让 RG 自动完成
|
||||
|
||||
@@ -73,6 +73,9 @@ namespace Engine {
|
||||
inline constexpr u32 kPreviewTilesPerSide = 5;
|
||||
inline constexpr u32 kPreviewTileCount =
|
||||
kPreviewTilesPerSide * kPreviewTilesPerSide;
|
||||
// 单 tile 回读字节数(SDR atlas 为 RGBA8)
|
||||
inline constexpr u32 kPreviewTileBytes =
|
||||
kPreviewTileSize * kPreviewTileSize * 4;
|
||||
|
||||
// 本帧待渲染的脏 tile(Material* 来自本帧 UI 请求,活指针)
|
||||
struct PreviewDirtyTile {
|
||||
@@ -80,13 +83,22 @@ namespace Engine {
|
||||
u32 tile = 0;
|
||||
};
|
||||
|
||||
// 本帧待回读烘焙的 tile(key = 调用方令牌,Renderer 不解释)。
|
||||
// 只收录"本帧确实被渲染"的 tile,故条数 ≤ 渲染预算
|
||||
struct PreviewBakeTile {
|
||||
u64 key = 0;
|
||||
u32 tile = 0;
|
||||
};
|
||||
|
||||
// 编辑器 tile 池(Renderer 持有,UE FAssetThumbnailPool 语义)。
|
||||
// 缓存键 = Material serial(出生唯一号,防指针地址复用的 ABA)
|
||||
// + editVersion(参数代数)。游戏构建整体编译裁剪,无运行时成本
|
||||
class PreviewTilePool {
|
||||
public:
|
||||
// 每帧渲染预算:脏 tile 多时逐帧消化(滚动列表渐进出图)
|
||||
static constexpr u32 kPreviewRendersPerFrame = 2;
|
||||
// 每帧渲染预算:脏 tile 多时逐帧消化(浏览器缩略图烘焙 / 滚动
|
||||
// 列表渐进出图)。预览球是 128 三角形的 256² 直绘,8 个/帧的
|
||||
// GPU 成本可忽略;烘焙吞吐 = 预算(回读只收当帧渲染过的 tile)
|
||||
static constexpr u32 kPreviewRendersPerFrame = 8;
|
||||
|
||||
// UI 构建段逐可见行调用(幂等 keepalive,刷新 LRU + 脏检测)。
|
||||
// 返回 tile:>=0 = 三槽位镜像内容齐备可显示(含重渲期间的旧
|
||||
@@ -145,6 +157,44 @@ namespace Engine {
|
||||
return displayable ? static_cast<i32>(tileIdx) : -1;
|
||||
}
|
||||
|
||||
// 烘焙请求(浏览器缩略图回读):强制标脏——即使 tile 已
|
||||
// displayable 也重渲一次,保证"当帧渲染 → 当帧拷贝回读"闭环
|
||||
// (回读只收当帧渲染过的 tile)。返回槽位 tile 索引;-1 =
|
||||
// 池满无槽(调用方下帧重试)。不参与 paintedSlots 记账
|
||||
i32 RequestBake(Material* material)
|
||||
{
|
||||
if (!material) return -1;
|
||||
const u64 serial = material->GetSerial();
|
||||
Entry* slot = nullptr;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == serial) { slot = &e; break; }
|
||||
}
|
||||
if (!slot) {
|
||||
u64 oldest = ~0ull;
|
||||
for (auto& e : m_Entries) {
|
||||
if (e.serial == 0) { slot = &e; break; }
|
||||
if (e.lastUseFrame < m_Frame &&
|
||||
e.lastUseFrame < oldest) {
|
||||
oldest = e.lastUseFrame;
|
||||
slot = &e;
|
||||
}
|
||||
}
|
||||
if (!slot) return -1;
|
||||
slot->serial = serial;
|
||||
slot->paintedSlots = 0;
|
||||
slot->trackedVersion = 0;
|
||||
}
|
||||
slot->lastUseFrame = m_Frame;
|
||||
const u32 tileIdx =
|
||||
static_cast<u32>(slot - m_Entries.data());
|
||||
bool queued = false;
|
||||
for (auto& d : m_FrameDirty) {
|
||||
if (d.tile == tileIdx) { d.material = material; queued = true; break; }
|
||||
}
|
||||
if (!queued) m_FrameDirty.push_back({material, tileIdx});
|
||||
return static_cast<i32>(tileIdx);
|
||||
}
|
||||
|
||||
// Render 段取本帧预算内的脏 tile(frameSlot = 本帧渲染槽位,
|
||||
// 该槽位镜像在本帧被写入)。渲染语义(清矩形+画球)由 pass
|
||||
// 执行后该槽位内容即有效。剩余脏 tile 保持未取——下帧可见行
|
||||
@@ -215,6 +265,7 @@ namespace Engine {
|
||||
class MaterialPreviewPass final : public RenderPass {
|
||||
public:
|
||||
// dirtyTiles = Renderer 持有的当帧脏表(Render 段换入,只读)
|
||||
// bakeTiles = Renderer 持有的当帧回读表(只读;条数 ≤ 渲染预算)
|
||||
MaterialPreviewPass(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
SceneGpu& gpu, FullscreenGpu& fullscreen,
|
||||
@@ -222,8 +273,19 @@ namespace Engine {
|
||||
RG::TextureHandle velocityAtlas,
|
||||
RG::TextureHandle sdrAtlas,
|
||||
const std::vector<PreviewDirtyTile>&
|
||||
dirtyTiles);
|
||||
~MaterialPreviewPass() override = default;
|
||||
dirtyTiles,
|
||||
const std::vector<PreviewBakeTile>&
|
||||
bakeTiles);
|
||||
~MaterialPreviewPass() override;
|
||||
|
||||
// 本槽位的回读暂存(host-visible,持久映射):Renderer 在该
|
||||
// 槽位 fence 通过后 memcpy 取走烘焙像素
|
||||
Buffer* BakeStaging(u32 frameSlot) const
|
||||
{
|
||||
return frameSlot < kMaxFramesInFlight
|
||||
? m_BakeStaging[frameSlot].get()
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
|
||||
@@ -246,6 +308,11 @@ namespace Engine {
|
||||
RG::TextureHandle m_VelocityAtlas;
|
||||
RG::TextureHandle m_SDRAtlas;
|
||||
const std::vector<PreviewDirtyTile>& m_DirtyTiles;
|
||||
const std::vector<PreviewBakeTile>& m_BakeTiles;
|
||||
|
||||
// 回读暂存:每帧槽位一份(容量 = 渲染预算 × 单 tile 字节)。
|
||||
// 拷贝录制在该槽位命令缓冲,Renderer 在该槽位 fence 通过后读
|
||||
std::array<Scope<Buffer>, kMaxFramesInFlight> m_BakeStaging;
|
||||
|
||||
// ---- 预览专用 set0(与 uboSetLayout 同布局,内容恒定)----
|
||||
// UBO = 固定棚拍相机/布光;SSBO = GPUDriven 契约的最小占位
|
||||
|
||||
@@ -35,7 +35,8 @@ namespace Engine {
|
||||
// env 可为空(HDR 资产缺失):仅承担清屏,天空保持清屏色
|
||||
SkyboxPass(Device& device, ShaderManager& shaders,
|
||||
DescriptorManager& descriptors, FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, const Cubemap* env);
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
const Cubemap* env);
|
||||
~SkyboxPass();
|
||||
|
||||
SkyboxPass(const SkyboxPass&) = delete;
|
||||
@@ -49,10 +50,11 @@ namespace Engine {
|
||||
DescriptorManager& m_Descriptors;
|
||||
FrameSceneData& m_Frame;
|
||||
RG::TextureHandle m_HDRColor;
|
||||
RG::TextureHandle m_Velocity; // 天空 velocity 附件(TAA)
|
||||
const Cubemap* m_Env; // 可空
|
||||
|
||||
vk::DescriptorSetLayout m_SetLayout; // Set0 = {samplerCube}
|
||||
vk::PipelineLayout m_Layout; // push = invVP 64B
|
||||
vk::PipelineLayout m_Layout; // push = {invVP, prevVP} 128B
|
||||
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
|
||||
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_Sets{};
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Engine::Passes {
|
||||
public:
|
||||
TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
RG::TextureHandle taaColor);
|
||||
RG::TextureHandle depth, RG::TextureHandle taaColor);
|
||||
~TAAPass() override = default;
|
||||
|
||||
void Register(RG::RenderGraph& rg) override;
|
||||
@@ -34,6 +34,7 @@ namespace Engine::Passes {
|
||||
f32 m_HistoryAlpha; // 开启时的历史权重
|
||||
RG::TextureHandle m_HDR;
|
||||
RG::TextureHandle m_Velocity;
|
||||
RG::TextureHandle m_Depth; // 场景深度(深度感知 dilation 用)
|
||||
RG::TextureHandle m_TAAColor; // 输出 = 下一帧的历史
|
||||
|
||||
std::vector<vk::DescriptorSet> m_Sets; // per-slot
|
||||
|
||||
@@ -111,6 +111,17 @@ namespace Engine {
|
||||
// 脏表引用与槽位——脏 tile 本帧不再解引用已销毁材质(UAF)
|
||||
void DropMaterialPreview(const Material* material);
|
||||
void* GetMaterialPreviewTexture() const;
|
||||
|
||||
// ---- 材质球缩略图烘焙(GPU→CPU 异步回读,无 waitIdle)----
|
||||
// UI 段对"需要静态缩略图"的材质逐帧幂等请求(userKey 由调用方
|
||||
// 定义,Renderer 不解释、原样回传;同 Material serial 去重)。渲染
|
||||
// 段把该材质渲染进预览 tile 后当帧拷进 host 暂存,待该帧槽位
|
||||
// fence 通过(两帧后同槽位再入图)再取走——不阻塞主线程
|
||||
void RequestMaterialPreviewBake(Material* material, u64 userKey);
|
||||
// UI 段轮询:有完成的烘焙则取走(一次性)。像素 = RGBA8,
|
||||
// kPreviewTileSize²(tonemap 后的 SDR,可直接当缩略图)
|
||||
bool PollMaterialPreviewBake(u64& outKey, std::vector<u8>& outPixels,
|
||||
u32& outWidth, u32& outHeight);
|
||||
#endif
|
||||
|
||||
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
|
||||
@@ -213,6 +224,9 @@ namespace Engine {
|
||||
void HandleResize(bool recreateSwapchain);
|
||||
void ReadGpuTimes(u32 frameSlot);
|
||||
void RefreshSceneTextures(); // 物理 view → ImGui 描述符(重建后刷新)
|
||||
#ifdef HELLO_EDITOR
|
||||
void CollectBakeResults(u32 frameSlot); // 槽位 fence 后取走回读结果
|
||||
#endif
|
||||
// 期望渲染分辨率:Editor 视口 = 面板尺寸(未上报时回退 swapchain);
|
||||
// Direct = swapchain 尺寸
|
||||
vk::Extent2D DesiredRenderExtent() const;
|
||||
@@ -300,6 +314,34 @@ namespace Engine {
|
||||
// TakeRenders 取预算内的脏 tile 换入 m_PreviewRenders(pass 只读)
|
||||
Passes::PreviewTilePool m_PreviewPool;
|
||||
std::vector<Passes::PreviewDirtyTile> m_PreviewRenders;
|
||||
// 上一轮注册到 ImGui 的预览 atlas 物理 view:重编译后若镜像真的
|
||||
// 换新(固定尺寸资源跨视口 resize 会保留同一镜像)才需让池重画,
|
||||
// 否则预览球内容存续、不闪烁
|
||||
vk::ImageView m_PreviewViews[kMaxFramesInFlight]{};
|
||||
|
||||
// ---- 材质球缩略图烘焙(GPU→CPU 异步回读)----
|
||||
struct PreviewBakeReq
|
||||
{
|
||||
u64 key = 0; // 调用方令牌(App 侧 path → key)
|
||||
u64 serial = 0; // Material serial(去重/失效判据)
|
||||
Material* material = nullptr; // 仅 !copied 期间被解引用
|
||||
i32 frameTile = -1; // 本帧 RequestBake 返回的槽位
|
||||
bool copied = false; // 拷贝已录制,等待回读
|
||||
u64 lastRequestFrame = 0; // UI keepalive 帧号(老化丢弃判据)
|
||||
};
|
||||
std::vector<PreviewBakeReq> m_BakeReqs; // 跨帧存活至回读完成
|
||||
u64 m_FrameIndex = 0; // 单调帧号(请求老化判据)
|
||||
std::vector<Passes::PreviewBakeTile> m_PreviewBakes; // 当帧(pass 只读)
|
||||
// 每槽位当帧录制的回读 key(帧 N 录,帧 N+2 同槽位 fence 后取走)
|
||||
std::array<std::vector<u64>, kMaxFramesInFlight> m_BakeRecorded;
|
||||
struct PreviewBakeResult
|
||||
{
|
||||
u64 key = 0;
|
||||
std::vector<u8> pixels;
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
};
|
||||
std::vector<PreviewBakeResult> m_BakeDone;
|
||||
#endif
|
||||
// GPU timestamp(pass 级耗时,ImGui 可读)
|
||||
vk::QueryPool m_QueryPool;
|
||||
|
||||
@@ -150,7 +150,8 @@ namespace Engine {
|
||||
std::vector<u32> instanceNext; // 与 instances 平行:同 batch 下一实例
|
||||
// 每 batch 实例链头(与 result.batches 平行;kNoInstance = 空)
|
||||
std::vector<u32> batchHead;
|
||||
// 每 batch 实例距离累加(透明排序用:平均距离降序 = 远→近)
|
||||
// 每 batch 实例视图深度累加(透明排序用:平均深度降序 = 远→近;
|
||||
// 透明队列逐实例成批故 count=1,等价逐实例深度)
|
||||
std::vector<f32> batchDistSum;
|
||||
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
|
||||
// batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销
|
||||
@@ -183,7 +184,7 @@ namespace Engine {
|
||||
// ---- TAA 时域状态 ----
|
||||
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
|
||||
glm::mat4 m_LastView{1.0f}; // 上一帧视图(velocity)
|
||||
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity)
|
||||
glm::mat4 m_LastProjNoJitter{1.0f}; // 上一帧未抖动投影(velocity 用)
|
||||
// 实体的上一帧世界矩阵(key = entt 实体 integral)。
|
||||
// 用 sorted vector + binary_search 替代 unordered_map:
|
||||
// unordered_map 在每帧大量 emplace 时 rehash 抖动明显,
|
||||
|
||||
@@ -164,9 +164,9 @@ namespace Engine {
|
||||
LightData lights[kMaxLights];
|
||||
|
||||
// ---- TAA(velocity 在 vertex 里算)----
|
||||
glm::mat4 projNoJitter; // 未抖动投影(velocity 当前帧 clip 用)
|
||||
glm::mat4 projNoJitter; // 未抖动投影(天空盒反投影/折射同位采样/静态预览)
|
||||
glm::mat4 prevView; // 上一帧视图(velocity)
|
||||
glm::mat4 prevProj; // 上一帧未抖动投影(velocity)
|
||||
glm::mat4 prevProj; // 上一帧"已抖动"投影(velocity 历史 UV 对齐)
|
||||
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
|
||||
};
|
||||
|
||||
@@ -183,7 +183,14 @@ namespace Engine {
|
||||
struct FrameSceneData {
|
||||
UniformBufferObject ubo{};
|
||||
|
||||
glm::vec4 ClearColor{0.1, 0.1, 0.1, 1.0};
|
||||
// 相机裁剪面 + 投影模式(TAA 去遮挡检测用:把 [0,1] 深度线性化为
|
||||
// 视图空间距离再比较。透视深度非线性,需 n·f/(f−d(f−n));正交
|
||||
// 深度本身线性,用 cameraOrtho 分支)
|
||||
f32 cameraNearZ = 0.1f;
|
||||
f32 cameraFarZ = 10000.0f;
|
||||
bool cameraOrtho = false;
|
||||
|
||||
glm::vec4 ClearColor{0.0, 0.0, 0.0, 1.0};
|
||||
bool DrawSkybox = true;
|
||||
// 本帧生效的环境贴图资产路径(来自第一个 active 的
|
||||
// SkyboxComponent;空 = 无环境 → Renderer 回中性兜底 cubemap)。
|
||||
@@ -194,6 +201,12 @@ namespace Engine {
|
||||
//(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push)
|
||||
glm::mat4 skyInvVP{1.0f};
|
||||
|
||||
// 天空盒 velocity 用:上一帧未抖动 proj × 上一帧视图(去平移)。
|
||||
// 天空是"无限远"表面,只受相机旋转影响:frag 把当前世界方向过
|
||||
// 此矩阵得上一帧屏幕位置,差即天空的运动向量(相机移动时天空/
|
||||
// 背景不再闪烁的根治点——此前天空从不写 velocity = 恒 0)
|
||||
glm::mat4 skyPrevVP{1.0f};
|
||||
|
||||
// 实例矩阵数组 = [主视图段 | 阴影视图段](共享同一 per-slot SSBO)
|
||||
std::vector<glm::mat4> instanceModels;
|
||||
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
|
||||
#include <Engine/Assets/IAssetUploader.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
@@ -38,6 +41,9 @@ namespace Engine {
|
||||
Scope<Model> CreateTransparentPlane(f32 halfSize,
|
||||
u8 r, u8 g, u8 b, u8 a) override;
|
||||
Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) override;
|
||||
Scope<Model> CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
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;
|
||||
@@ -46,7 +52,8 @@ namespace Engine {
|
||||
~VulkanAssetUploader() override;
|
||||
|
||||
// ---- 材质资产(.material 数据段 → GPU 段)----
|
||||
// 每槽路径建独占纹理(空/缺失回退共享默认纹理),再组 Material
|
||||
// 每槽经路径纹理缓存(跨材质去重)取纹理,同批新纹理合并为一次
|
||||
// submit;空/缺失回退共享默认纹理,再组 Material
|
||||
Scope<MaterialAsset> CreateMaterial(MaterialAssetData&& data) override;
|
||||
// 设备空闲屏障(编辑器资产重建路径:卸载旧 GPU 资源前全停飞)
|
||||
void WaitDeviceIdle() override;
|
||||
@@ -57,6 +64,12 @@ namespace Engine {
|
||||
void InitSharedDefaultTextures();
|
||||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures{};
|
||||
|
||||
// 路径纹理缓存:键 = 归一化路径 + sRGB 标记 → 独占纹理。多个
|
||||
// 材质引用同一贴图(仓库 4K 贴图被多材质共享)只解码/上传一次。
|
||||
// 生命周期长于所有 MaterialAsset(uploader 在 assets 之后析构),
|
||||
// 故 Material 可安全持裸指针。DDS 与 stbi 同缓存
|
||||
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
|
||||
@@ -42,6 +42,18 @@ namespace Engine {
|
||||
Device& device, MemoryAllocator& allocator,
|
||||
const void* data, u32 bytes, bool srgb);
|
||||
|
||||
// 批式上传(一次 submit + 一次 fence 等待服务同批多张贴图,省掉
|
||||
// 每张各自的命令池创建销毁与阻塞等待):解码 + 建 image/view/
|
||||
// sampler + 把拷贝/barrier 录制进调用方 cmd(**不**提交)。staging
|
||||
// 暂存对象内,调用方在批量 submit fence 通过后逐张
|
||||
// ReleasePendingStaging 释放;析构亦兜底释放。DDS(稀有)仍走
|
||||
// 自带提交,不参与批量
|
||||
static Scope<Texture> CreateDeferred(Device& device,
|
||||
MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb,
|
||||
vk::CommandBuffer cmd);
|
||||
void ReleasePendingStaging();
|
||||
|
||||
~Texture();
|
||||
|
||||
Texture(const Texture&) = delete;
|
||||
@@ -62,9 +74,19 @@ namespace Engine {
|
||||
const std::array<u8, 4>& rgba, bool srgb);
|
||||
|
||||
// 公共路径:staging → device local image → blit 生成完整 mip 链 →
|
||||
// view/sampler(RGBA8,各向异性过滤)
|
||||
// view/sampler(RGBA8,各向异性过滤)。录制进 cmd 后自行提交等待
|
||||
void InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb);
|
||||
|
||||
// 批式路径构造(cmd 由调用方提供,不在本构造内提交)
|
||||
Texture(Device& device, MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb, vk::CommandBuffer cmd);
|
||||
|
||||
// 公共上传路径核心:建 staging/image/view/sampler + 逐 mip
|
||||
// barrier/copy/blit 录制进 cmd(不提交、不销毁 staging,staging 存
|
||||
// m_PendingStaging)。InitFromPixels 与批式路径共用
|
||||
void RecordPixelsUpload(const void* pixels, u32 width, u32 height,
|
||||
bool srgb, vk::CommandBuffer cmd);
|
||||
|
||||
// DDS 路径:压缩数据直传(BC 硬件采样解码)+ 文件自带 mip 链
|
||||
void InitFromDds(const std::string& path, bool srgb);
|
||||
|
||||
@@ -83,6 +105,10 @@ namespace Engine {
|
||||
|
||||
u64 m_TextureBytes = 0;
|
||||
|
||||
// 批式上传暂存的 staging(submit fence 通过前不可释放)
|
||||
bool m_HasPendingStaging = false;
|
||||
MemoryAllocator::Allocation m_PendingStaging{};
|
||||
|
||||
MemoryAllocator::Allocation m_ImageAllocation;
|
||||
MemoryAllocator& m_Allocator;
|
||||
};
|
||||
|
||||
@@ -212,6 +212,33 @@ namespace Engine::Assets {
|
||||
[&](const std::string&) { return m_Uploader.CreateCube(halfSize, r, g, b); });
|
||||
}
|
||||
|
||||
ModelRef AssetManager::CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b)
|
||||
{
|
||||
// 缓存键 = 伪路径 + 形状相关参数(确定性:同形状同参数同色共享)。
|
||||
// 只编码该形状真正使用的字段,避免无关字段变化制造重复模型
|
||||
std::string params;
|
||||
if (shape == "cylinder" || shape == "cone") {
|
||||
params = std::to_string(desc.size) + "_" +
|
||||
std::to_string(desc.height);
|
||||
} else if (shape == "torus") {
|
||||
params = std::to_string(desc.majorRadius) + "_" +
|
||||
std::to_string(desc.minorRadius);
|
||||
} else if (shape == "plane") {
|
||||
params = std::to_string(desc.size) + "_" +
|
||||
std::to_string(desc.subdivisions);
|
||||
} else {
|
||||
params = std::to_string(desc.size);
|
||||
}
|
||||
const std::string key = "procedural://" + shape + "/" + params + "/" +
|
||||
std::to_string(r) + "," + std::to_string(g) +
|
||||
"," + std::to_string(b);
|
||||
return m_Models.Load(key, [&](const std::string&) {
|
||||
return m_Uploader.CreatePrimitive(shape, desc, r, g, b);
|
||||
});
|
||||
}
|
||||
|
||||
EnvRef AssetManager::LoadEnvironment(const std::string& path)
|
||||
{
|
||||
// GPU 生成(compute 预积分)经接口发生在渲染层实现里
|
||||
|
||||
@@ -22,6 +22,10 @@ namespace Engine {
|
||||
s.mUp = JPH::Vec3::sAxisY();
|
||||
s.mMass = std::max(params.mass, 1e-3f);
|
||||
s.mMaxSlopeAngle = glm::radians(params.maxSlopeAngle);
|
||||
// 静态三角网格地面/楼梯的内部边会产生 ghost contact,把角色顶起/
|
||||
// 抖动。Jolt 增强内部边移除可消除(一次穿墙修正的额外开销),
|
||||
// 对箱体/凸包地面无副作用
|
||||
s.mEnhancedInternalEdgeRemoval = true;
|
||||
s.mShape = new JPH::CapsuleShape(
|
||||
std::max(params.radius, 0.01f),
|
||||
std::max(params.halfHeight, 0.0f));
|
||||
|
||||
@@ -33,14 +33,54 @@
|
||||
#include <Jolt/Physics/Collision/CastResult.h>
|
||||
#include <Jolt/Physics/Collision/RayCast.h>
|
||||
#include <Jolt/Physics/Body/BodyLock.h>
|
||||
#include <Jolt/Physics/Body/BodyFilter.h> // BodyDrawFilter(调试绘制按体过滤)
|
||||
|
||||
#include <Jolt/Core/IssueReporting.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Engine {
|
||||
namespace {
|
||||
// Jolt Trace 转发:缺省实现 DummyTrace 内部带 JPH_ASSERT(false)
|
||||
// ("Please override Trace"),任何一次 Trace 都会把调试版直接打断。
|
||||
// Jolt 用 Trace 输出运行期告警——例如 AABBTreeBuilder 对相交三角做
|
||||
// 随机分割并注明"仍能正常工作,仅碰撞树效率降低"——必须接管。
|
||||
// 非捕获函数(GCC 不支持变参 lambda 转函数指针)。同文去重(Jolt
|
||||
// 可能多线程并发 Trace)防重复刷屏
|
||||
void JoltTrace(const char *inFMT, ...) {
|
||||
char buf[512];
|
||||
va_list args;
|
||||
va_start(args, inFMT);
|
||||
std::vsnprintf(buf, sizeof(buf), inFMT, args);
|
||||
va_end(args);
|
||||
static std::mutex mtx;
|
||||
static std::unordered_set<std::string> seen;
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
if (seen.insert(buf).second) {
|
||||
ENGINE_WARN("[Jolt] {}", buf);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 调试绘制三角预算 ----
|
||||
// 巨型三角网格碰撞体(合并全场景 mesh,如 SunTemple 1056 mesh ≈ 百万
|
||||
// 三角)的线框绘制每帧要生成百万级 DrawLine(约 3M 线段)——直接把
|
||||
// 帧率打到个位数。超预算的 MeshShape 不画三角线框,CollectDebugData
|
||||
// 退化为画其包围盒线框(位置/范围仍可见,成本与三角数无关)
|
||||
constexpr JPH::uint kDebugMeshTriangleBudget = 50000;
|
||||
struct SkipHeavyMeshDraw final : JPH::BodyDrawFilter {
|
||||
bool ShouldDraw(const JPH::Body &inBody) const override {
|
||||
return inBody.GetShape()->GetStats().mNumTriangles
|
||||
<= kDebugMeshTriangleBudget;
|
||||
}
|
||||
};
|
||||
|
||||
// ---- 层配置:MVP 两层(非移动/移动),扩软体/传感器时再分层 ----
|
||||
constexpr JPH::ObjectLayer LAYER_NON_MOVING{0};
|
||||
constexpr JPH::ObjectLayer LAYER_MOVING{1};
|
||||
@@ -80,6 +120,15 @@ namespace Engine {
|
||||
}
|
||||
};
|
||||
|
||||
// 射线层过滤:只放行静态(非移动)层——相机防穿墙不把碎块/动态物
|
||||
// 当遮挡(否则碎块飘过枢轴会把镜头乱拉)
|
||||
class OnlyNonMovingLayerFilter final : public JPH::ObjectLayerFilter {
|
||||
public:
|
||||
bool ShouldCollide(JPH::ObjectLayer inLayer) const override {
|
||||
return inLayer == LAYER_NON_MOVING;
|
||||
}
|
||||
};
|
||||
|
||||
// Jolt ↔ glm 四元数直转(TransformComponent 旋转存 glm::quat,
|
||||
// UE FTransform 语义):物理同步/写回不再经欧拉往返 → 姿态
|
||||
// 零漂移(分量序:Jolt ctor (x,y,z,w),glm ctor (w,x,y,z))
|
||||
@@ -167,6 +216,7 @@ namespace Engine {
|
||||
static std::once_flag s_JoltOnce;
|
||||
std::call_once(s_JoltOnce, [] {
|
||||
JPH::RegisterDefaultAllocator();
|
||||
JPH::Trace = &JoltTrace; // 见文件顶部说明
|
||||
JPH::Factory::sInstance = new JPH::Factory();
|
||||
JPH::RegisterTypes();
|
||||
});
|
||||
@@ -288,7 +338,24 @@ namespace Engine {
|
||||
JPH::BodyManager::DrawSettings ds;
|
||||
ds.mDrawShape = true;
|
||||
ds.mDrawShapeWireframe = true;
|
||||
m_System->DrawBodies(ds, m_DebugDraw.get());
|
||||
// 超预算的网格体被过滤(不画三角线框),下面补包围盒线框
|
||||
const SkipHeavyMeshDraw skipHeavy;
|
||||
m_System->DrawBodies(ds, m_DebugDraw.get(), &skipHeavy);
|
||||
|
||||
for (auto &[bits, rec]: m_Bodies) {
|
||||
JPH::BodyLockRead lock(m_System->GetBodyLockInterface(), rec.id);
|
||||
if (!lock.Succeeded()) {
|
||||
continue;
|
||||
}
|
||||
const JPH::Body &body = lock.GetBody();
|
||||
if (body.GetShape()->GetStats().mNumTriangles
|
||||
<= kDebugMeshTriangleBudget) {
|
||||
continue;
|
||||
}
|
||||
// 巨型 MeshShape:包围盒线框(蓝色)替代百万三角线框
|
||||
m_DebugDraw->DrawWireBox(body.GetWorldSpaceBounds(),
|
||||
JPH::Color(64, 200, 255, 255));
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 约束(关节;软体/载具约束接入后自动生效)
|
||||
@@ -857,8 +924,13 @@ namespace Engine {
|
||||
// 退化三角(两索引相同)剔除:MeshShape 构建会因退化报错
|
||||
if (idx[i] != idx[i + 1] && idx[i] != idx[i + 2] &&
|
||||
idx[i + 1] != idx[i + 2]) {
|
||||
tris.emplace_back(idx[i], idx[i + 1], idx[i + 2],
|
||||
static_cast<u32>(0), base);
|
||||
// 索引必须加基址偏移到拼接后的全局顶点表:IndexedTriangle
|
||||
// 第 4/5 参是 materialIndex/userData,不是顶点偏移——写错
|
||||
// 会让每个 mesh 的三角全部指向前一个 mesh 的顶点区,
|
||||
// 碰撞网格形状彻底错位(且三角包围盒乱飞 → AABBTree 随机
|
||||
// 分割告警刷屏)
|
||||
tris.emplace_back(idx[i] + base, idx[i + 1] + base,
|
||||
idx[i + 2] + base, 0u, 0u);
|
||||
}
|
||||
}
|
||||
base += static_cast<u32>(pos.size());
|
||||
@@ -1045,6 +1117,18 @@ namespace Engine {
|
||||
|
||||
bool PhysicsWorld::RayCast(const glm::vec3 &origin, const glm::vec3 &dir,
|
||||
const f32 maxDist, RayHit &outHit) const {
|
||||
return RayCastImpl(origin, dir, maxDist, outHit, nullptr);
|
||||
}
|
||||
|
||||
bool PhysicsWorld::RayCastStatic(const glm::vec3 &origin, const glm::vec3 &dir,
|
||||
const f32 maxDist, RayHit &outHit) const {
|
||||
static const OnlyNonMovingLayerFilter kFilter;
|
||||
return RayCastImpl(origin, dir, maxDist, outHit, &kFilter);
|
||||
}
|
||||
|
||||
bool PhysicsWorld::RayCastImpl(const glm::vec3 &origin, const glm::vec3 &dir,
|
||||
const f32 maxDist, RayHit &outHit,
|
||||
const JPH::ObjectLayerFilter *layerFilter) const {
|
||||
const f32 len = glm::length(dir);
|
||||
if (len < 1e-8f) return false;
|
||||
|
||||
@@ -1054,7 +1138,9 @@ namespace Engine {
|
||||
JPH::Vec3(dir.x / len, dir.y / len, dir.z / len) * maxDist);
|
||||
|
||||
JPH::RayCastResult hit;
|
||||
if (!m_System->GetNarrowPhaseQuery().CastRay(ray, hit)) {
|
||||
const auto &nq = m_System->GetNarrowPhaseQuery();
|
||||
if (!(layerFilter ? nq.CastRay(ray, hit, {}, *layerFilter)
|
||||
: nq.CastRay(ray, hit))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ namespace Engine::RG {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 固定尺寸资源:desc 显式给定 width/height,物理尺寸与目标渲染
|
||||
// 分辨率无关(如材质球预览 atlas)。这类镜像跨重编译沿用,内容存续
|
||||
bool IsResolutionIndependent(const TextureEntry& e)
|
||||
{
|
||||
return !e.imported && e.desc.width != 0 && e.desc.height != 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---------- 构造 / 析构 ----------
|
||||
@@ -39,7 +46,8 @@ namespace Engine::RG {
|
||||
|
||||
RenderGraph::~RenderGraph()
|
||||
{
|
||||
ReleasePhysical();
|
||||
// 析构需全量归还(含固定尺寸资源),池随后统一销毁
|
||||
ReleasePhysical(false);
|
||||
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
for (auto &images: m_Pool | std::views::values) {
|
||||
@@ -172,7 +180,9 @@ namespace Engine::RG {
|
||||
ENGINE_FATAL("RenderGraph: Compile before SetTargetExtent");
|
||||
}
|
||||
|
||||
ReleasePhysical();
|
||||
// 固定尺寸镜像已在 entry 上沿用(SetTargetExtent 只回收尺寸随目标
|
||||
// 变化的镜像),此处再收一次只对非固定尺寸的残留生效
|
||||
ReleasePhysical(true);
|
||||
|
||||
// 清理上一次编译的中间结果
|
||||
for (auto& entry : m_Textures) {
|
||||
@@ -229,11 +239,15 @@ namespace Engine::RG {
|
||||
if (extent == m_TargetExtent) return;
|
||||
|
||||
// 尺寸变化:调用方必须保证 GPU 空闲(通常在 swapchain recreate 的 waitIdle 之后)
|
||||
ReleasePhysical();
|
||||
// 只回收尺寸随目标变化的镜像;固定尺寸资源(预览 atlas)保留在
|
||||
// entry 上,PurgePool 也看不到它们,内容不会被销毁
|
||||
ReleasePhysical(true);
|
||||
PurgePool(extent);
|
||||
|
||||
// 物理镜像全部换新,槽位状态清零
|
||||
// 只有被回收重建的镜像才回到"首次使用"(Undefined 出发);保留的
|
||||
// 镜像仍在上一轮 resting layout,槽位使用态必须跟着保留
|
||||
for (auto& entry : m_Textures) {
|
||||
if (IsResolutionIndependent(entry)) continue;
|
||||
for (auto& slot : entry.slots) slot.used = false;
|
||||
}
|
||||
|
||||
@@ -594,6 +608,19 @@ namespace Engine::RG {
|
||||
};
|
||||
|
||||
for (u32 slot = 0; slot < m_FramesInFlight; ++slot) {
|
||||
// 跨重编译保留的固定尺寸镜像:形态与本次一致则原样沿用
|
||||
//(同一 VkImage → 内容存续);不一致则归还池再重分配
|
||||
PhysicalImage* keep = entry.physical[slot];
|
||||
if (keep) {
|
||||
if (keep->format == entry.desc.format &&
|
||||
keep->extent == extent && keep->usage == entry.usage) {
|
||||
continue;
|
||||
}
|
||||
PoolKey key{static_cast<u32>(keep->format),
|
||||
keep->extent.width, keep->extent.height,
|
||||
static_cast<u32>(keep->usage)};
|
||||
m_Pool[key].push_back(Scope<PhysicalImage>(keep));
|
||||
}
|
||||
entry.physical[slot] = AcquirePhysical(
|
||||
entry.desc.format, extent, entry.usage, entry.aspect);
|
||||
}
|
||||
@@ -601,10 +628,11 @@ namespace Engine::RG {
|
||||
}
|
||||
}
|
||||
|
||||
void RenderGraph::ReleasePhysical()
|
||||
void RenderGraph::ReleasePhysical(bool keepFixedSize)
|
||||
{
|
||||
for (auto& entry : m_Textures) {
|
||||
if (entry.imported) continue;
|
||||
if (keepFixedSize && IsResolutionIndependent(entry)) continue;
|
||||
for (u32 slot = 0; slot < entry.physical.size(); ++slot) {
|
||||
auto* img = entry.physical[slot];
|
||||
if (!img) continue;
|
||||
|
||||
@@ -174,10 +174,8 @@ namespace Engine
|
||||
|
||||
MaterialAsset::MaterialAsset(
|
||||
MaterialAssetData&& data,
|
||||
std::array<Scope<Texture>, Material::kSlotCount>&& ownedTextures,
|
||||
Scope<Material>&& material)
|
||||
: m_Data(std::move(data))
|
||||
, m_OwnedTextures(std::move(ownedTextures))
|
||||
, m_Material(std::move(material))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/18.
|
||||
//
|
||||
// MeshFactory 实现:参数化图元的顶点/索引生成。
|
||||
//
|
||||
// 所有生成器遵循同一套约定(见头文件契约):法线朝外、CCW 正视、
|
||||
// UV 在 [0,1] 内、缝/极点处按需复制顶点以保住 UV 与逐面法线。
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/MeshFactory.h>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace {
|
||||
// 圆柱/圆锥的顶底盖平面 UV:把 xz 以直径归一投影到 [0,1]
|
||||
glm::vec2 PlanarCapUV(f32 x, f32 z, f32 radius)
|
||||
{
|
||||
const f32 d = radius > 1e-6f ? 2.0f * radius : 1.0f;
|
||||
return { x / d + 0.5f, z / d + 0.5f };
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFactory::ComputeTangents(MeshData& mesh)
|
||||
{
|
||||
std::vector<glm::vec3> tan(mesh.vertices.size(), glm::vec3(0.0f));
|
||||
|
||||
for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) {
|
||||
const Vertex& v0 = mesh.vertices[mesh.indices[i]];
|
||||
const Vertex& v1 = mesh.vertices[mesh.indices[i + 1]];
|
||||
const Vertex& v2 = mesh.vertices[mesh.indices[i + 2]];
|
||||
|
||||
const glm::vec3 e1 = v1.position - v0.position;
|
||||
const glm::vec3 e2 = v2.position - v0.position;
|
||||
const glm::vec2 d1 = v1.uv - v0.uv;
|
||||
const glm::vec2 d2 = v2.uv - v0.uv;
|
||||
|
||||
// 退化三角(极点/缝的零面积片、UV 退化为 0 的帽面)跳过,
|
||||
// 否则 1/det 会引入 inf 污染整根切线
|
||||
const f32 det = d1.x * d2.y - d2.x * d1.y;
|
||||
if (std::abs(det) < 1e-8f) continue;
|
||||
|
||||
const glm::vec3 t = (e1 * d2.y - e2 * d1.y) / det;
|
||||
tan[mesh.indices[i]] += t;
|
||||
tan[mesh.indices[i + 1]] += t;
|
||||
tan[mesh.indices[i + 2]] += t;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < mesh.vertices.size(); ++i) {
|
||||
Vertex& v = mesh.vertices[i];
|
||||
// 与 N 正交化(Gram-Schmidt),退化时取任一正交轴兜底
|
||||
glm::vec3 t = tan[i] - v.normal * glm::dot(v.normal, tan[i]);
|
||||
const f32 len = glm::length(t);
|
||||
if (len > 1e-6f) {
|
||||
v.tangent = t / len;
|
||||
continue;
|
||||
}
|
||||
const glm::vec3 axis =
|
||||
std::abs(v.normal.y) < 0.99f ? glm::vec3(0, 1, 0)
|
||||
: glm::vec3(1, 0, 0);
|
||||
v.tangent = glm::normalize(glm::cross(axis, v.normal));
|
||||
}
|
||||
}
|
||||
|
||||
// 轴对齐立方体:6 面各 4 顶点(逐面法线,UV 归一),36 索引
|
||||
MeshData MeshFactory::Cube(f32 halfSize)
|
||||
{
|
||||
const f32 h = halfSize;
|
||||
const glm::vec3 P[24] = {
|
||||
// +Z(前)
|
||||
{-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h},
|
||||
// -Z(后)
|
||||
{ h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h},
|
||||
// +Y(上)
|
||||
{-h, h, h}, { h, h, h}, { h, h,-h}, {-h, h,-h},
|
||||
// -Y(下)
|
||||
{-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h},
|
||||
// +X(右)
|
||||
{ h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h},
|
||||
// -X(左)
|
||||
{-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h},
|
||||
};
|
||||
const glm::vec3 N[6] = {
|
||||
{0, 0, 1}, {0, 0, -1}, {0, 1, 0}, {0, -1, 0}, {1, 0, 0}, {-1, 0, 0},
|
||||
};
|
||||
const glm::vec2 UV[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
|
||||
|
||||
MeshData m;
|
||||
m.vertices.reserve(24);
|
||||
for (u32 f = 0; f < 6; ++f) {
|
||||
for (u32 i = 0; i < 4; ++i) {
|
||||
m.vertices.push_back({P[f * 4 + i], N[f], UV[i]});
|
||||
}
|
||||
}
|
||||
m.indices.reserve(36);
|
||||
for (u32 f = 0; f < 6; ++f) {
|
||||
const u32 b = f * 4;
|
||||
m.indices.insert(m.indices.end(), {b, b + 1, b + 2, b, b + 2, b + 3});
|
||||
}
|
||||
ComputeTangents(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
// XZ 平面:法线 +Y,网格细分 subdivisions(1 = 单四边形)
|
||||
MeshData MeshFactory::Plane(f32 halfSize, u32 subdivisions)
|
||||
{
|
||||
const u32 n = std::max(1u, subdivisions);
|
||||
const u32 stride = n + 1;
|
||||
const f32 step = (2.0f * halfSize) / static_cast<f32>(n);
|
||||
|
||||
MeshData m;
|
||||
m.vertices.reserve(stride * stride);
|
||||
for (u32 r = 0; r <= n; ++r) {
|
||||
for (u32 c = 0; c <= n; ++c) {
|
||||
const f32 x = -halfSize + static_cast<f32>(c) * step;
|
||||
const f32 z = -halfSize + static_cast<f32>(r) * step;
|
||||
m.vertices.push_back({
|
||||
{x, 0.0f, z}, {0, 1, 0},
|
||||
{static_cast<f32>(c) / static_cast<f32>(n),
|
||||
static_cast<f32>(r) / static_cast<f32>(n)}});
|
||||
}
|
||||
}
|
||||
for (u32 r = 0; r < n; ++r) {
|
||||
for (u32 c = 0; c < n; ++c) {
|
||||
const u32 a = r * stride + c;
|
||||
const u32 b = a + 1;
|
||||
const u32 cc = a + stride;
|
||||
const u32 d = cc + 1;
|
||||
// 绕序令法线朝 +Y((a,cc,d) 与 (a,d,b) 叉积均为 +Y)
|
||||
m.indices.insert(m.indices.end(), {a, cc, d, a, d, b});
|
||||
}
|
||||
}
|
||||
ComputeTangents(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
// UV 球:极点/缝复制顶点保住 UV,北极为 +Y
|
||||
MeshData MeshFactory::Sphere(f32 radius, u32 segments, u32 rings)
|
||||
{
|
||||
const u32 seg = std::max(3u, segments);
|
||||
const u32 ring = std::max(2u, rings);
|
||||
const f32 twoPi = glm::two_pi<f32>();
|
||||
|
||||
MeshData m;
|
||||
m.vertices.reserve((ring + 1) * (seg + 1));
|
||||
for (u32 r = 0; r <= ring; ++r) {
|
||||
const f32 v = static_cast<f32>(r) / static_cast<f32>(ring);
|
||||
const f32 theta = v * glm::pi<f32>(); // 0 = +Y 极
|
||||
const f32 st = std::sin(theta);
|
||||
const f32 ct = std::cos(theta);
|
||||
for (u32 s = 0; s <= seg; ++s) {
|
||||
const f32 u = static_cast<f32>(s) / static_cast<f32>(seg);
|
||||
const f32 phi = u * twoPi;
|
||||
const glm::vec3 n(st * std::cos(phi), ct, st * std::sin(phi));
|
||||
m.vertices.push_back({n * radius, n, {u, 1.0f - v}});
|
||||
}
|
||||
}
|
||||
m.indices.reserve(ring * seg * 6);
|
||||
for (u32 r = 0; r < ring; ++r) {
|
||||
for (u32 s = 0; s < seg; ++s) {
|
||||
const u32 a = r * (seg + 1) + s;
|
||||
const u32 b = a + 1;
|
||||
const u32 c = a + (seg + 1);
|
||||
const u32 d = c + 1;
|
||||
m.indices.insert(m.indices.end(), {a, b, d, a, d, c});
|
||||
}
|
||||
}
|
||||
ComputeTangents(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
// 圆柱:侧壁逐列上下两点(法线径向)+ 顶底盖(平面 UV,独立顶点)
|
||||
MeshData MeshFactory::Cylinder(f32 radius, f32 height, u32 segments)
|
||||
{
|
||||
const u32 seg = std::max(3u, segments);
|
||||
const f32 hh = height * 0.5f;
|
||||
const f32 twoPi = glm::two_pi<f32>();
|
||||
|
||||
MeshData m;
|
||||
m.vertices.reserve((seg + 1) * 2 + (seg + 1) * 2 + 2);
|
||||
|
||||
// 侧壁:s 列推 2 顶点(下、上),缝处复制一列
|
||||
for (u32 s = 0; s <= seg; ++s) {
|
||||
const f32 u = static_cast<f32>(s) / static_cast<f32>(seg);
|
||||
const f32 phi = u * twoPi;
|
||||
const f32 cp = std::cos(phi);
|
||||
const f32 sp = std::sin(phi);
|
||||
const glm::vec3 n(cp, 0.0f, sp);
|
||||
m.vertices.push_back(
|
||||
{{radius * cp, -hh, radius * sp}, n, {u, 0.0f}});
|
||||
m.vertices.push_back(
|
||||
{{radius * cp, hh, radius * sp}, n, {u, 1.0f}});
|
||||
}
|
||||
for (u32 s = 0; s < seg; ++s) {
|
||||
const u32 a = s * 2; // 下 s
|
||||
const u32 t = a + 1; // 上 s
|
||||
const u32 b = a + 2; // 下 s+1
|
||||
const u32 d = a + 3; // 上 s+1
|
||||
m.indices.insert(m.indices.end(), {a, d, b, a, t, d});
|
||||
}
|
||||
|
||||
// 顶盖(+Y)
|
||||
const u32 topCenter = static_cast<u32>(m.vertices.size());
|
||||
m.vertices.push_back({{0.0f, hh, 0.0f}, {0, 1, 0}, {0.5f, 0.5f}});
|
||||
for (u32 s = 0; s <= seg; ++s) {
|
||||
const f32 phi = static_cast<f32>(s) / static_cast<f32>(seg) * twoPi;
|
||||
const f32 x = radius * std::cos(phi);
|
||||
const f32 z = radius * std::sin(phi);
|
||||
m.vertices.push_back({{x, hh, z}, {0, 1, 0}, PlanarCapUV(x, z, radius)});
|
||||
}
|
||||
for (u32 s = 0; s < seg; ++s) {
|
||||
const u32 r0 = topCenter + 1 + s;
|
||||
const u32 r1 = topCenter + 1 + s + 1;
|
||||
m.indices.insert(m.indices.end(), {topCenter, r1, r0});
|
||||
}
|
||||
|
||||
// 底盖(-Y)
|
||||
const u32 botCenter = static_cast<u32>(m.vertices.size());
|
||||
m.vertices.push_back({{0.0f, -hh, 0.0f}, {0, -1, 0}, {0.5f, 0.5f}});
|
||||
for (u32 s = 0; s <= seg; ++s) {
|
||||
const f32 phi = static_cast<f32>(s) / static_cast<f32>(seg) * twoPi;
|
||||
const f32 x = radius * std::cos(phi);
|
||||
const f32 z = radius * std::sin(phi);
|
||||
m.vertices.push_back({{x, -hh, z}, {0, -1, 0}, PlanarCapUV(x, z, radius)});
|
||||
}
|
||||
for (u32 s = 0; s < seg; ++s) {
|
||||
const u32 r0 = botCenter + 1 + s;
|
||||
const u32 r1 = botCenter + 1 + s + 1;
|
||||
m.indices.insert(m.indices.end(), {botCenter, r0, r1});
|
||||
}
|
||||
|
||||
ComputeTangents(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
// 圆锥:底环 + 尖端(每段复制尖端以带倾角法线,每段一个三角)+ 底盖
|
||||
MeshData MeshFactory::Cone(f32 radius, f32 height, u32 segments)
|
||||
{
|
||||
const u32 seg = std::max(3u, segments);
|
||||
const f32 baseY = -height * 0.5f;
|
||||
const f32 apexY = height * 0.5f;
|
||||
const f32 twoPi = glm::two_pi<f32>();
|
||||
|
||||
MeshData m;
|
||||
m.vertices.reserve((seg + 1) * 2 + (seg + 1) + 1);
|
||||
|
||||
// 底环(侧壁用,法线含倾角)+ 逐段尖端
|
||||
const u32 baseStart = 0;
|
||||
for (u32 s = 0; s <= seg; ++s) {
|
||||
const f32 u = static_cast<f32>(s) / static_cast<f32>(seg);
|
||||
const f32 phi = u * twoPi;
|
||||
const f32 cp = std::cos(phi);
|
||||
const f32 sp = std::sin(phi);
|
||||
// 侧面法线:竖直分量与半径分量按高/半径配比(高 → 近径向)
|
||||
const glm::vec3 n = glm::normalize(glm::vec3(height * cp, radius,
|
||||
height * sp));
|
||||
m.vertices.push_back({{radius * cp, baseY, radius * sp}, n, {u, 0.0f}});
|
||||
}
|
||||
const u32 apexStart = static_cast<u32>(m.vertices.size());
|
||||
for (u32 s = 0; s <= seg; ++s) {
|
||||
const f32 u = static_cast<f32>(s) / static_cast<f32>(seg);
|
||||
const f32 phi = u * twoPi;
|
||||
const f32 cp = std::cos(phi);
|
||||
const f32 sp = std::sin(phi);
|
||||
const glm::vec3 n = glm::normalize(glm::vec3(height * cp, radius,
|
||||
height * sp));
|
||||
m.vertices.push_back({{0.0f, apexY, 0.0f}, n, {u, 1.0f}});
|
||||
}
|
||||
for (u32 s = 0; s < seg; ++s) {
|
||||
const u32 b0 = baseStart + s;
|
||||
const u32 b1 = baseStart + s + 1;
|
||||
const u32 ap = apexStart + s;
|
||||
m.indices.insert(m.indices.end(), {b0, ap, b1});
|
||||
}
|
||||
|
||||
// 底盖(-Y):独立环顶点(平面法线/UV)
|
||||
const u32 botCenter = static_cast<u32>(m.vertices.size());
|
||||
m.vertices.push_back({{0.0f, baseY, 0.0f}, {0, -1, 0}, {0.5f, 0.5f}});
|
||||
for (u32 s = 0; s <= seg; ++s) {
|
||||
const f32 phi = static_cast<f32>(s) / static_cast<f32>(seg) * twoPi;
|
||||
const f32 x = radius * std::cos(phi);
|
||||
const f32 z = radius * std::sin(phi);
|
||||
m.vertices.push_back(
|
||||
{{x, baseY, z}, {0, -1, 0}, PlanarCapUV(x, z, radius)});
|
||||
}
|
||||
for (u32 s = 0; s < seg; ++s) {
|
||||
const u32 r0 = botCenter + 1 + s;
|
||||
const u32 r1 = botCenter + 1 + s + 1;
|
||||
m.indices.insert(m.indices.end(), {botCenter, r0, r1});
|
||||
}
|
||||
|
||||
ComputeTangents(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
// 圆环(甜甜圈):主环绕 +Y,管截面绕环切线;两向均闭合成缝复制
|
||||
MeshData MeshFactory::Torus(f32 majorRadius, f32 minorRadius,
|
||||
u32 majorSegments, u32 minorSegments)
|
||||
{
|
||||
const u32 ms = std::max(3u, majorSegments);
|
||||
const u32 ns = std::max(3u, minorSegments);
|
||||
const f32 twoPi = glm::two_pi<f32>();
|
||||
|
||||
MeshData m;
|
||||
m.vertices.reserve((ms + 1) * (ns + 1));
|
||||
for (u32 i = 0; i <= ms; ++i) {
|
||||
const f32 u = static_cast<f32>(i) / static_cast<f32>(ms);
|
||||
const f32 phi = u * twoPi;
|
||||
const f32 cp = std::cos(phi);
|
||||
const f32 sp = std::sin(phi);
|
||||
for (u32 j = 0; j <= ns; ++j) {
|
||||
const f32 v = static_cast<f32>(j) / static_cast<f32>(ns);
|
||||
const f32 psi = v * twoPi;
|
||||
const f32 cq = std::cos(psi);
|
||||
const f32 sq = std::sin(psi);
|
||||
const glm::vec3 n(cq * cp, sq, cq * sp);
|
||||
const f32 ringR = majorRadius + minorRadius * cq;
|
||||
const glm::vec3 p(ringR * cp, minorRadius * sq, ringR * sp);
|
||||
m.vertices.push_back({p, n, {u, v}});
|
||||
}
|
||||
}
|
||||
for (u32 i = 0; i < ms; ++i) {
|
||||
for (u32 j = 0; j < ns; ++j) {
|
||||
const u32 a = i * (ns + 1) + j;
|
||||
const u32 b = (i + 1) * (ns + 1) + j;
|
||||
const u32 c = i * (ns + 1) + j + 1;
|
||||
const u32 d = (i + 1) * (ns + 1) + j + 1;
|
||||
m.indices.insert(m.indices.end(), {a, c, d, a, d, b});
|
||||
}
|
||||
}
|
||||
ComputeTangents(m);
|
||||
return m;
|
||||
}
|
||||
|
||||
MeshData MeshFactory::Make(const std::string& shape, const PrimitiveDesc& d)
|
||||
{
|
||||
if (shape == "cube") return Cube(d.size);
|
||||
if (shape == "plane") return Plane(d.size, d.subdivisions);
|
||||
if (shape == "sphere") return Sphere(d.size, d.segments, d.rings);
|
||||
if (shape == "cylinder") return Cylinder(d.size, d.height, d.segments);
|
||||
if (shape == "cone") return Cone(d.size, d.height, d.segments);
|
||||
if (shape == "torus")
|
||||
return Torus(d.majorRadius, d.minorRadius, d.segments, d.rings);
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::vector<std::string>& MeshFactory::ShapeNames()
|
||||
{
|
||||
static const std::vector<std::string> kNames = {
|
||||
"cube", "plane", "sphere", "cylinder", "cone", "torus"};
|
||||
return kNames;
|
||||
}
|
||||
|
||||
bool MeshFactory::IsKnownShape(const std::string& shape)
|
||||
{
|
||||
const auto& names = ShapeNames();
|
||||
return std::find(names.begin(), names.end(), shape) != names.end();
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
@@ -288,12 +288,45 @@ namespace {
|
||||
dst.texPaths[Material::Albedo] =
|
||||
ResolveUfbxTexture(modelDir, *base.texture);
|
||||
}
|
||||
|
||||
// UE/ORCA 打包 Specular 工作流:R=AO, G=Roughness, B=Metalness。
|
||||
// 通道与 glTF MR 合图(G=rough, B=metal)恰好同序 → 同一张贴图
|
||||
// 直供 MetallicRoughness 槽 + Ao 槽,纹理缓存按路径去重共享同一
|
||||
// Texture*。factors=(1,1,0,0):金属/粗糙因子直用贴图(z=0 → shader
|
||||
// 读 MR.g 当粗糙度,同 glTF 语义)。UE 打包 Specular ⇔ DirectX
|
||||
// 法线(同源约定)→ 默认置翻绿位 misc.w
|
||||
if (const ufbx_material_map& spec = mat->pbr.specular_color;
|
||||
spec.texture != nullptr) {
|
||||
const std::string p =
|
||||
ResolveUfbxTexture(modelDir, *spec.texture);
|
||||
dst.texPaths[Material::MetallicRoughness] = p;
|
||||
dst.texPaths[Material::Ao] = p;
|
||||
dst.params.factors = glm::vec4(1.0f, 1.0f, 0.0f, 0.0f);
|
||||
dst.params.misc.w = 1.0f;
|
||||
}
|
||||
if (const ufbx_material_map& nrm = mat->pbr.normal_map;
|
||||
nrm.texture != nullptr) {
|
||||
dst.texPaths[Material::Normal] =
|
||||
ResolveUfbxTexture(modelDir, *nrm.texture);
|
||||
}
|
||||
if (const ufbx_material_map& emi = mat->pbr.emission_color;
|
||||
emi.texture != nullptr) {
|
||||
dst.texPaths[Material::Emissive] =
|
||||
ResolveUfbxTexture(modelDir, *emi.texture);
|
||||
}
|
||||
ENGINE_INFO(
|
||||
"ufbx Mesh '{}': Material '{}': albedo={}", dst.name,
|
||||
mat->name.length ? U8Str(mat->name) : "?",
|
||||
"ufbx Mesh '{}': Material '{}': albedo={} mr={} normal={} ao={} emissive={}",
|
||||
dst.name, mat->name.length ? U8Str(mat->name) : "?",
|
||||
dst.texPaths[Material::Albedo].empty()
|
||||
? "<none>"
|
||||
: dst.texPaths[Material::Albedo]);
|
||||
? "<none>" : dst.texPaths[Material::Albedo],
|
||||
dst.texPaths[Material::MetallicRoughness].empty()
|
||||
? "<none>" : dst.texPaths[Material::MetallicRoughness],
|
||||
dst.texPaths[Material::Normal].empty()
|
||||
? "<none>" : dst.texPaths[Material::Normal],
|
||||
dst.texPaths[Material::Ao].empty()
|
||||
? "<none>" : dst.texPaths[Material::Ao],
|
||||
dst.texPaths[Material::Emissive].empty()
|
||||
? "<none>" : dst.texPaths[Material::Emissive]);
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, u32> boneSlots;
|
||||
@@ -772,8 +805,8 @@ void Model::InitDefaultTextures()
|
||||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
|
||||
bool transparent)
|
||||
ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
|
||||
bool transparent, const std::string& name)
|
||||
: m_Device(device)
|
||||
, m_Allocator(allocator)
|
||||
, m_DescriptorManager(descriptorManager)
|
||||
@@ -792,16 +825,9 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
std::to_string(rgba[3]);
|
||||
m_TextureCache.emplace(texKey, std::move(tex));
|
||||
|
||||
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
|
||||
const f32 h = halfSize;
|
||||
std::vector<Vertex> vertices = {
|
||||
{{-h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||||
{{-h, 0.0f, h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||||
{{ h, 0.0f, h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||||
{{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}},
|
||||
};
|
||||
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
|
||||
const std::vector<std::vector<u32>> lodIndices = {indices};
|
||||
// 几何(顶点/索引 + 逐顶点切线)由 MeshFactory 产出:单一几何源,
|
||||
// 各具名工厂不再各自内联一套立方体/平面
|
||||
const std::vector<std::vector<u32>> lodIndices = {std::move(geo.indices)};
|
||||
|
||||
// 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
|
||||
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
|
||||
@@ -812,11 +838,9 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
overrides);
|
||||
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, vertices, lodIndices, mat));
|
||||
// 程序化平面同样登记内建材质快照(与 m_Meshes 下标对齐,
|
||||
// 编辑器材质槽 UI 不越界)
|
||||
m_MeshMaterialInfos.push_back(
|
||||
{"Plane", {}, MaterialParams{}, transparent});
|
||||
m_Device, m_Allocator, geo.vertices, lodIndices, mat));
|
||||
// 内建材质快照(与 m_Meshes 下标对齐,编辑器材质槽 UI 不越界)
|
||||
m_MeshMaterialInfos.push_back({name, {}, MaterialParams{}, transparent});
|
||||
RebuildUnionBounds();
|
||||
}
|
||||
|
||||
@@ -827,8 +851,9 @@ Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
|
||||
{
|
||||
return Scope<Model>(new Model(device, allocator, descriptorManager,
|
||||
textureLayout, ProceduralTag{},
|
||||
halfSize, std::array<u8, 4>{r, g, b, 255},
|
||||
false));
|
||||
MeshFactory::Plane(halfSize),
|
||||
std::array<u8, 4>{r, g, b, 255}, false,
|
||||
"Plane"));
|
||||
}
|
||||
|
||||
Scope<Model> Model::CreateTransparentPlane(
|
||||
@@ -839,8 +864,9 @@ Scope<Model> Model::CreateTransparentPlane(
|
||||
{
|
||||
return Scope<Model>(new Model(device, allocator, descriptorManager,
|
||||
textureLayout, ProceduralTag{},
|
||||
halfSize, std::array<u8, 4>{r, g, b, a},
|
||||
true));
|
||||
MeshFactory::Plane(halfSize),
|
||||
std::array<u8, 4>{r, g, b, a}, true,
|
||||
"Plane"));
|
||||
}
|
||||
|
||||
Scope<Model> Model::CreateCube(Device& device, MemoryAllocator& allocator,
|
||||
@@ -849,81 +875,36 @@ Scope<Model> Model::CreateCube(Device& device, MemoryAllocator& allocator,
|
||||
f32 halfSize, u8 r, u8 g, u8 b)
|
||||
{
|
||||
return Scope<Model>(new Model(device, allocator, descriptorManager,
|
||||
textureLayout, CubeTag{},
|
||||
halfSize, std::array<u8, 4>{r, g, b, 255}));
|
||||
textureLayout, ProceduralTag{},
|
||||
MeshFactory::Cube(halfSize),
|
||||
std::array<u8, 4>{r, g, b, 255}, false,
|
||||
"Cube"));
|
||||
}
|
||||
|
||||
// 程序化立方体:6 面 24 顶点(面法线朝外,CCW 从外看),36 索引。
|
||||
// 结构镜像平面 ctor:默认纹理 → 纯色 albedo 覆盖 → 单 mesh
|
||||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
CubeTag, f32 halfSize, const std::array<u8, 4>& rgba)
|
||||
: m_Device(device)
|
||||
, m_Allocator(allocator)
|
||||
, m_DescriptorManager(descriptorManager)
|
||||
, m_TextureLayout(textureLayout)
|
||||
// 通用程序化图元:形状名 → MeshFactory 几何。非法形状名/空几何返回
|
||||
// nullptr(调用方回退,绝不把空 mesh 塞进 Mesh)
|
||||
Scope<Model> Model::CreatePrimitive(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b)
|
||||
{
|
||||
// 纯色贴图直供 albedo 槽;生命周期挂进纹理缓存(键含 rgba 值,
|
||||
// 同色共享、异色互不冲突)——与平面 ctor 同一套
|
||||
InitDefaultTextures();
|
||||
Scope<Texture> tex = Texture::CreateSolidColor(device, allocator,
|
||||
rgba[0], rgba[1], rgba[2]);
|
||||
Texture* albedoOverride = tex.get();
|
||||
const std::string texKey = "proc-cube:" + std::to_string(rgba[0]) + ',' +
|
||||
std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' +
|
||||
std::to_string(rgba[3]);
|
||||
m_TextureCache.emplace(texKey, std::move(tex));
|
||||
if (!MeshFactory::IsKnownShape(shape)) return nullptr;
|
||||
MeshData geo = MeshFactory::Make(shape, desc);
|
||||
if (geo.vertices.empty() || geo.indices.empty()) return nullptr;
|
||||
|
||||
const f32 h = halfSize;
|
||||
// 逐面 4 顶点(底左/底右/顶右/顶左,从面外看)+ 单位 UV
|
||||
const glm::vec3 P[24] = {
|
||||
// +Z(前)
|
||||
{-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h},
|
||||
// -Z(后)
|
||||
{ h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h},
|
||||
// +Y(上)
|
||||
{-h, h, h}, { h, h, h}, { h, h,-h}, {-h, h,-h},
|
||||
// -Y(下)
|
||||
{-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h},
|
||||
// +X(右)
|
||||
{ h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h},
|
||||
// -X(左)
|
||||
{-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h},
|
||||
};
|
||||
const glm::vec3 N[6] = {
|
||||
{0, 0, 1}, {0, 0, -1}, {0, 1, 0}, {0, -1, 0}, {1, 0, 0}, {-1, 0, 0},
|
||||
};
|
||||
const glm::vec2 UV[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
|
||||
|
||||
std::vector<Vertex> vertices;
|
||||
vertices.reserve(24);
|
||||
for (u32 f = 0; f < 6; ++f) {
|
||||
for (u32 i = 0; i < 4; ++i) {
|
||||
vertices.push_back({P[f * 4 + i], N[f], UV[i]});
|
||||
}
|
||||
// 显示名 = 形状名首字母大写(编辑器材质槽显示用)
|
||||
std::string name = shape;
|
||||
if (!name.empty()) {
|
||||
name[0] = static_cast<char>(std::toupper(
|
||||
static_cast<unsigned char>(name[0])));
|
||||
}
|
||||
std::vector<u32> indices;
|
||||
indices.reserve(36);
|
||||
for (u32 f = 0; f < 6; ++f) {
|
||||
const u32 b = f * 4;
|
||||
const std::vector<u32> quad = {b, b + 1, b + 2, b, b + 2, b + 3};
|
||||
indices.insert(indices.end(), quad.begin(), quad.end());
|
||||
}
|
||||
const std::vector<std::vector<u32>> lodIndices = {indices};
|
||||
|
||||
std::string paths[Material::kSlotCount] = {"", "", "", ""};
|
||||
std::array<Texture*, Material::kSlotCount> overrides{};
|
||||
overrides[Material::Albedo] = albedoOverride;
|
||||
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, false,
|
||||
overrides);
|
||||
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, vertices, lodIndices, mat));
|
||||
// 内建材质快照(与 m_Meshes 下标对齐,编辑器材质槽 UI 不越界)
|
||||
m_MeshMaterialInfos.push_back(
|
||||
{"Cube", {}, MaterialParams{}, false});
|
||||
RebuildUnionBounds();
|
||||
return Scope<Model>(new Model(device, allocator, descriptorManager,
|
||||
textureLayout, ProceduralTag{},
|
||||
std::move(geo),
|
||||
std::array<u8, 4>{r, g, b, 255}, false,
|
||||
name));
|
||||
}
|
||||
|
||||
// 资产显示名:文件 stem;无信息量(空 / 导入资产库的 "asset")时读
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
|
||||
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
|
||||
//
|
||||
// 透明管线对齐 UE/Unity 标准语义:
|
||||
// 透明管线:分层屏幕空间折射(玻璃见玻璃)
|
||||
// - 不透明("Forward"):深度测试 + 写,无混合直写
|
||||
// - 透明("ForwardTrans"):独立渲染实例,深度测试(对不透明)+
|
||||
// 深度写入关闭,HDR/velocity/depth 全部 Load 承接不透明段结果,
|
||||
// 预乘 alpha 混合(线性空间 over),远 → 近排序(SceneRenderer
|
||||
// 侧按 batch 平均距离排)
|
||||
// - 透明("ForwardTransL*"):按绘制单位拆成至多 kTransparentLayers
|
||||
// 层,远 → 近(SceneRenderer 侧按 batch 平均距离排)。每层绘制前把
|
||||
// HDRColor 刷新进 SceneColor 副本("SceneColorCopy"/"SceneColorCopyL*"),
|
||||
// 使该层玻璃折射采样包含所有更远的透明体。深度测试(对不透明)+
|
||||
// 深度写入关闭,HDR/velocity/depth 全部 Load 承接前序结果,预乘
|
||||
// alpha 混合(线性空间 over)。
|
||||
//
|
||||
// NVIDIA shader-object 实测注意(见 .trae/documents 性能档案):
|
||||
// 早期实验称 depthWriteEnable=FALSE 有灾难病态(~5fps),但该实验
|
||||
@@ -29,6 +31,8 @@
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
@@ -52,6 +56,15 @@ namespace Engine::Passes
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
|
||||
// 透明折射分层上限:RenderGraph 的 pass 只注册一次、编译期定序
|
||||
//(声明与执行分离),层数必须编译期固定。透明队列按远→近拆成至多
|
||||
// kTransparentLayers 层,每层绘制前刷新一次场景色副本,使该层
|
||||
// 玻璃的折射采样包含所有更远的透明体——前层玻璃折射出后层玻璃
|
||||
//(玻璃见玻璃),而非只看到不透明段快照(UE/Unity 单快照的固有
|
||||
// 局限)。超出层数的单位并入最后一层(仍正常绘制,仅同层内不再
|
||||
// 互相折射)。
|
||||
constexpr u32 kTransparentLayers = 8;
|
||||
} // namespace
|
||||
|
||||
// 两段渲染共享的命令录制:shader/描述符绑定 + 几何/光栅/视口状态。
|
||||
@@ -121,7 +134,7 @@ namespace Engine::Passes
|
||||
|
||||
vk::ClearValue clearValue;
|
||||
clearValue.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
|
||||
std::array<float, 4>{0.0f, 0.0f, 0.0f, 0.0f});
|
||||
|
||||
vk::RenderingAttachmentInfo colorAttachments[2];
|
||||
// HDR 附件由 Skybox pass 清屏并画背景,这里 Load 在其上合成几何
|
||||
@@ -131,12 +144,14 @@ namespace Engine::Passes
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(clearValue);
|
||||
|
||||
// velocity 附件:清 0 = 静止(背景无几何 → 历史原位采样)
|
||||
// velocity 附件由 Skybox 段首写(清 0 + 天空运动向量),这里 Load
|
||||
// 承接其结果,再叠加几何体运动向量。若此处 Clear 会擦掉天空
|
||||
// velocity → 相机移动时天空重投影错误 → 背景闪烁。
|
||||
vk::ClearValue velocityClear;
|
||||
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
|
||||
colorAttachments[1].setImageView(ctx.View(m_Velocity))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eClear)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(velocityClear);
|
||||
|
||||
@@ -195,9 +210,8 @@ namespace Engine::Passes
|
||||
vk::ShaderStageFlagBits::eFragment);
|
||||
|
||||
// ---- 场景色副本:不透明段(含天空盒背景)的快照 ----
|
||||
// 透射折射的采样源。必须排在 Forward 之后(RAW 链)、
|
||||
// ForwardTrans 之前(后者对 HDRColor 的写入不能进折射样本,
|
||||
// 且玻璃互相不可见——屏幕空间透射只含不透明场景,与 UE 一致)。
|
||||
// 第 0 层折射的采样源。必须排在 Forward 之后(RAW 链)、透明层
|
||||
// 之前。后续层由 "SceneColorCopyL*" 逐层刷新(含前序透明体)。
|
||||
rg.AddPass("SceneColorCopy", [this](RG::RenderGraphContext& ctx) {
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
@@ -218,118 +232,173 @@ namespace Engine::Passes
|
||||
.Read(m_HDRColor, RG::ResourceUse::TransferSrc)
|
||||
.Write(m_SceneColor, RG::ResourceUse::TransferDst);
|
||||
|
||||
// ---- 透明段:独立渲染实例,UE/Unity 标准语义 ----
|
||||
// 深度测试对不透明深度(eLess)+ 深度写入关闭——玻璃之间
|
||||
// 互相不遮挡(谁先画都留在画面上,先后只影响 over 混合的
|
||||
// 颜色数学),交叉半透明不再整块消失。
|
||||
// 对不透明深度的 RAW 由 depth 的 Write use 链保证排在
|
||||
// 不透明段之后(WAW 边)。
|
||||
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
|
||||
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
|
||||
rg.AddPass("ForwardTrans", [this](RG::RenderGraphContext& ctx) {
|
||||
if (m_Frame.translucentView.batches.empty()) return;
|
||||
// ---- 透明段:分层渲染实例(玻璃见玻璃)----
|
||||
// 深度测试对不透明深度(eLess)+ 深度写入关闭——玻璃之间互相
|
||||
// 不遮挡(先后只影响 over 混合/折射采样的颜色数学),交叉半透明
|
||||
// 不再整块消失。对不透明深度的 RAW 由 depth 的 Write use 链保证
|
||||
// 排在不透明段之后(WAW 边)。
|
||||
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,预乘在 tonemap
|
||||
// 段(rgb * a)完成 —— hard constraint。
|
||||
//
|
||||
// 分层折射:透明队列按远→近(SceneRenderer 排序)。每个绘制单位
|
||||
// 占一层,层 L 绘制第 L 个单位;绘制前把 HDRColor 刷新进 SceneColor
|
||||
// 副本,使层 L 的玻璃折射采样包含所有更远的透明体(含更远玻璃),
|
||||
// 而不是只有不透明段快照。上限 kTransparentLayers(见文件头
|
||||
// 匿名 namespace 注释),超出层数的单位并入最后一层。
|
||||
for (u32 layer = 0; layer < kTransparentLayers; ++layer) {
|
||||
// 层 L 的折射采样源刷新(层 0 用不透明段快照,无需再拷)
|
||||
if (layer > 0) {
|
||||
rg.AddPass("SceneColorCopyL" + std::to_string(layer),
|
||||
[this, layer](RG::RenderGraphContext& ctx) {
|
||||
// 本层无绘制单位:既无新内容可拷,也无玻璃会采样
|
||||
if (TransparentUnitCount() <= layer) return;
|
||||
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
vk::ImageCopy region{};
|
||||
region.srcSubresource =
|
||||
vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor,
|
||||
0, 0, 1);
|
||||
region.dstSubresource =
|
||||
vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor,
|
||||
0, 0, 1);
|
||||
region.setExtent(vk::Extent3D(ctx.extent.width,
|
||||
ctx.extent.height, 1));
|
||||
cmd.copyImage(ctx.Image(m_HDRColor),
|
||||
vk::ImageLayout::eTransferSrcOptimal,
|
||||
ctx.Image(m_SceneColor),
|
||||
vk::ImageLayout::eTransferDstOptimal,
|
||||
{region});
|
||||
})
|
||||
.Read(m_HDRColor, RG::ResourceUse::TransferSrc)
|
||||
.Write(m_SceneColor, RG::ResourceUse::TransferDst);
|
||||
}
|
||||
|
||||
vk::ClearValue clearValue;
|
||||
clearValue.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
|
||||
rg.AddPass("ForwardTransL" + std::to_string(layer),
|
||||
[this, layer](RG::RenderGraphContext& ctx) {
|
||||
const u32 total = TransparentUnitCount();
|
||||
if (total <= layer) return; // 本层无单位
|
||||
// 末层兜底:消化剩余全部单位;其余层每层一个单位
|
||||
const u32 end =
|
||||
(layer + 1 < kTransparentLayers) ? layer + 1 : total;
|
||||
DrawTransparentRange(ctx, ctx.cmd, layer, end);
|
||||
})
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
|
||||
// depth 走写 use(WAW 边):保证透明段排在不透明段之后,
|
||||
// barrier 覆盖透明段对不透明深度的测试读
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
|
||||
// 场景色副本:透射折射采样源(translucent frag set2 binding 6)
|
||||
.Read(m_SceneColor, 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::RenderingAttachmentInfo colorAttachments[2];
|
||||
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(clearValue);
|
||||
u32 ForwardPass::TransparentUnitCount() const
|
||||
{
|
||||
u32 n = 0;
|
||||
for (const auto& b : m_Frame.translucentView.batches) n += b.count;
|
||||
return n;
|
||||
}
|
||||
|
||||
// velocity:Load 不透明段写出的运动向量,玻璃只覆盖自己的像素
|
||||
vk::ClearValue velocityClear;
|
||||
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
|
||||
colorAttachments[1].setImageView(ctx.View(m_Velocity))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(velocityClear);
|
||||
void ForwardPass::DrawTransparentRange(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd,
|
||||
u32 start, u32 end) const
|
||||
{
|
||||
// HDR 颜色 + velocity 均 Load 承接(透明段叠加在前序内容上);
|
||||
// 这两项的 clearValue 在 eLoad 下不生效,仅为结构完整
|
||||
vk::ClearValue clearValue;
|
||||
clearValue.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.0f, 0.0f, 0.0f, 0.0f});
|
||||
|
||||
vk::RenderingAttachmentInfo depthAttachment;
|
||||
depthAttachment.setImageView(ctx.View(m_Depth))
|
||||
// 深度只读测试源:Load 承接不透明段深度,
|
||||
// 透明段 depthWrite=FALSE 不产生写入,
|
||||
// storeOp DontCare(下帧 entry 重新 Clear)
|
||||
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
|
||||
vk::RenderingAttachmentInfo colorAttachments[2];
|
||||
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
|
||||
.setClearValue(vk::ClearValue(
|
||||
vk::ClearDepthStencilValue(1.0f, 0)));
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(clearValue);
|
||||
|
||||
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
|
||||
colorAttachments[0], colorAttachments[1]};
|
||||
vk::ClearValue velocityClear;
|
||||
velocityClear.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
|
||||
colorAttachments[1].setImageView(ctx.View(m_Velocity))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(velocityClear);
|
||||
|
||||
vk::RenderingInfo renderingInfo;
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
|
||||
.setLayerCount(1)
|
||||
.setColorAttachments(colorArray)
|
||||
.setPDepthAttachment(&depthAttachment);
|
||||
// 深度只读测试源:Load 承接不透明段深度,depthWrite=FALSE 不产生
|
||||
// 写入。storeOp 必须 eStore——透明段已拆成多层,后续层的 Load 依赖
|
||||
// 本层写回的有效深度内容(DontCare 会让内容在层间变 undefined)。
|
||||
vk::RenderingAttachmentInfo depthAttachment;
|
||||
depthAttachment.setImageView(ctx.View(m_Depth))
|
||||
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(vk::ClearValue(
|
||||
vk::ClearDepthStencilValue(1.0f, 0)));
|
||||
|
||||
cmd.beginRendering(renderingInfo);
|
||||
RecordSharedState(ctx, cmd, true);
|
||||
// UE/Unity 语义:透明段只测不写深度。写深度会让"先画的玻璃"
|
||||
// 剔掉后画但实际在它前面的玻璃(交叉半透明整块消失)。
|
||||
// 早期 depthWrite=FALSE 引发 NVIDIA ~5fps 病态的实验是在
|
||||
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
|
||||
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
|
||||
cmd.setDepthWriteEnable(VK_FALSE);
|
||||
// 单面玻璃:透明段剔除背面。不透明段 cullNone + depthWrite
|
||||
// 无所谓(深度裁决),透明段 cullNone 会让闭合体(cube)的
|
||||
// 正反面片按 mesh 顺序乱序 over 混合 = "深度错乱";cull back
|
||||
// 每像素至多一面。双面透明(树叶卡片)将来加材质开关
|
||||
cmd.setCullMode(vk::CullModeFlagBits::eBack);
|
||||
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
|
||||
colorAttachments[0], colorAttachments[1]};
|
||||
|
||||
vk::ColorBlendEquationEXT premul;
|
||||
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
// alpha 穿透:玻璃只做场景内 rgb over(透出背景色),
|
||||
// 不修改像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap
|
||||
// rgb *= a 预乘 + 透明窗口桌面合成),归背景专属。若让
|
||||
// 玻璃 over 混入 a(旧写法),a<1 背景上的玻璃会被拖进
|
||||
// 桌面混色链路 → 预乘拉黑
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
vk::RenderingInfo renderingInfo;
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
|
||||
.setLayerCount(1)
|
||||
.setColorAttachments(colorArray)
|
||||
.setPDepthAttachment(&depthAttachment);
|
||||
|
||||
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
|
||||
cmd.setColorBlendEnableEXT(0,
|
||||
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
|
||||
cmd.setColorBlendEquationEXT(0,
|
||||
std::array<vk::ColorBlendEquationEXT, 2>{premul, direct});
|
||||
cmd.setColorWriteMaskEXT(0,
|
||||
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
|
||||
cmd.beginRendering(renderingInfo);
|
||||
RecordSharedState(ctx, cmd, true);
|
||||
// UE/Unity 语义:透明段只测不写深度。写深度会让"先画的玻璃"
|
||||
// 剔掉后画但实际在它前面的玻璃(交叉半透明整块消失)。
|
||||
cmd.setDepthWriteEnable(VK_FALSE);
|
||||
// 单面玻璃:透明段剔除背面。透明段 cullNone 会让闭合体(cube)
|
||||
// 的正反面片按 mesh 顺序乱序 over 混合 = "深度错乱";cull back
|
||||
// 每像素至多一面。双面透明(树叶卡片)将来加材质开关
|
||||
cmd.setCullMode(vk::CullModeFlagBits::eBack);
|
||||
|
||||
for (const auto& batch : m_Frame.translucentView.batches) {
|
||||
vk::ColorBlendEquationEXT premul;
|
||||
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
// alpha 穿透:玻璃只做场景内 rgb over(透出背景色),不修改
|
||||
// 像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap rgb *= a 预乘
|
||||
// + 透明窗口桌面合成),归背景专属
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
|
||||
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
|
||||
cmd.setColorBlendEnableEXT(0,
|
||||
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
|
||||
cmd.setColorBlendEquationEXT(0,
|
||||
std::array<vk::ColorBlendEquationEXT, 2>{premul, direct});
|
||||
cmd.setColorWriteMaskEXT(0,
|
||||
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
|
||||
|
||||
// 绘制单位区间 [start,end):批次内连续实例合并为一次 draw
|
||||
u32 consumed = 0;
|
||||
for (const auto& batch : m_Frame.translucentView.batches) {
|
||||
const u32 bStart = consumed;
|
||||
const u32 bEnd = consumed + batch.count;
|
||||
const u32 s = std::max(start, bStart);
|
||||
const u32 e = std::min(end, bEnd);
|
||||
consumed = bEnd;
|
||||
if (s < e) {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 1,
|
||||
batch.material->GetDescriptorSet(), {});
|
||||
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
|
||||
batch.mesh->Draw(cmd, e - s, batch.first + (s - bStart), batch.lod);
|
||||
}
|
||||
if (consumed >= end) break;
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
|
||||
// depth 走写 use(WAW 边):保证透明段排在不透明段之后,
|
||||
// barrier 覆盖透明段对不透明深度的测试读(Read-Write hazard
|
||||
// 由 src=Write 的内存域保证可见性)
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
|
||||
// 场景色副本:透射折射采样源(translucent frag set2 binding 6)
|
||||
.Read(m_SceneColor, 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);
|
||||
cmd.endRendering();
|
||||
}
|
||||
|
||||
void ForwardPass::OnTargetsRebuilt(RG::RenderGraph& rg)
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace Engine::Passes
|
||||
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
||||
|
||||
// ---- 颜色混合必须显式直写 ----
|
||||
// shader-object 路径混合是动态状态,会从上一个 pass(ForwardTrans
|
||||
// shader-object 路径混合是动态状态,会从上一个 pass(ForwardTransL*
|
||||
// 的 premul 混合)残留:tonemap 输出被二次混合 → rgb = color×a²
|
||||
//(拖低 alpha 平方级变黑),且 alpha 被残留方程与 clear(a=1) 混成
|
||||
// 恒 1 → 透明窗口永远不生效
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <Engine/Vulkan/Buffer.h>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
@@ -103,7 +104,8 @@ namespace Engine::Passes
|
||||
DescriptorManager& descriptors, SceneGpu& gpu,
|
||||
FullscreenGpu& fullscreen, RG::TextureHandle hdrAtlas,
|
||||
RG::TextureHandle velocityAtlas, RG::TextureHandle sdrAtlas,
|
||||
const std::vector<PreviewDirtyTile>& dirtyTiles)
|
||||
const std::vector<PreviewDirtyTile>& dirtyTiles,
|
||||
const std::vector<PreviewBakeTile>& bakeTiles)
|
||||
: m_Device(device)
|
||||
, m_Allocator(allocator)
|
||||
, m_Descriptors(descriptors)
|
||||
@@ -113,6 +115,7 @@ namespace Engine::Passes
|
||||
, m_VelocityAtlas(velocityAtlas)
|
||||
, m_SDRAtlas(sdrAtlas)
|
||||
, m_DirtyTiles(dirtyTiles)
|
||||
, m_BakeTiles(bakeTiles)
|
||||
{
|
||||
// ---- set0:棚拍 UBO + GPUDriven 契约占位(内容恒定,创建期
|
||||
// 一次写入;in-flight 帧并发只读同一份,无需 per-slot ring)----
|
||||
@@ -190,8 +193,22 @@ namespace Engine::Passes
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_TonemapSets[slot] = fullscreen.AllocateSet();
|
||||
}
|
||||
|
||||
// ---- 烘焙回读暂存:每槽位一份 host-visible 持久映射缓冲 ----
|
||||
const vk::DeviceSize bakeBytes =
|
||||
static_cast<vk::DeviceSize>(PreviewTilePool::kPreviewRendersPerFrame) *
|
||||
kPreviewTileBytes;
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_BakeStaging[slot] = CreateScope<Buffer>(
|
||||
device, allocator, bakeBytes,
|
||||
vk::BufferUsageFlagBits::eTransferDst,
|
||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
}
|
||||
}
|
||||
|
||||
MaterialPreviewPass::~MaterialPreviewPass() = default;
|
||||
|
||||
void MaterialPreviewPass::Register(RG::RenderGraph& rg)
|
||||
{
|
||||
// ---- 球体渲染:HDR atlas(loadOp=Load 保留缓存内容,逐脏 tile
|
||||
@@ -325,6 +342,42 @@ namespace Engine::Passes
|
||||
.Read(m_HDRAtlas, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Write(m_SDRAtlas, RG::ResourceUse::ColorAttachment);
|
||||
|
||||
// ---- 烘焙回读:当帧渲染过的 tile 从 SDR atlas 拷进 host 暂存 ----
|
||||
// TransferSrc use 让 RG 自动完成 eColorAttachmentOptimal →
|
||||
// eTransferSrcOptimal 迁移(ImGui 随后仍以 ShaderRead 采样同一图,
|
||||
// RG 按 read-read layout 变化再迁回)。SideEffect 保证空表帧不被
|
||||
// 剔除(barrier 照常插入,无拷贝则 GPU 零实际工作)
|
||||
rg.AddPass("MaterialPreviewBake",
|
||||
[this](RG::RenderGraphContext& ctx) {
|
||||
if (m_BakeTiles.empty()) return;
|
||||
Buffer* staging = m_BakeStaging[ctx.frame.frameSlot].get();
|
||||
if (!staging) return;
|
||||
const vk::Image src = ctx.Image(m_SDRAtlas);
|
||||
for (size_t i = 0; i < m_BakeTiles.size(); ++i) {
|
||||
const u32 tile = m_BakeTiles[i].tile;
|
||||
if (tile >= kPreviewTileCount) continue;
|
||||
const u32 col = tile % kPreviewTilesPerSide;
|
||||
const u32 row = tile / kPreviewTilesPerSide;
|
||||
vk::BufferImageCopy region;
|
||||
region.setBufferOffset(static_cast<vk::DeviceSize>(i) *
|
||||
kPreviewTileBytes)
|
||||
.setBufferRowLength(0)
|
||||
.setBufferImageHeight(0)
|
||||
.setImageSubresource(vk::ImageSubresourceLayers(
|
||||
vk::ImageAspectFlagBits::eColor, 0, 0, 1))
|
||||
.setImageOffset(vk::Offset3D{
|
||||
i32(col * kPreviewTileSize),
|
||||
i32(row * kPreviewTileSize), 0})
|
||||
.setImageExtent(vk::Extent3D{
|
||||
kPreviewTileSize, kPreviewTileSize, 1});
|
||||
ctx.cmd.copyImageToBuffer(
|
||||
src, vk::ImageLayout::eTransferSrcOptimal,
|
||||
staging->Get(), region);
|
||||
}
|
||||
})
|
||||
.Read(m_SDRAtlas, RG::ResourceUse::TransferSrc)
|
||||
.SideEffect();
|
||||
}
|
||||
|
||||
void MaterialPreviewPass::RecordBall(vk::CommandBuffer cmd, u32 tile,
|
||||
|
||||
@@ -289,7 +289,7 @@ namespace Engine::Passes
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
// Write use(WAW 链):保证排在 Forward/ForwardTrans 之后、
|
||||
// Write use(WAW 链):保证排在 Forward/ForwardTransL* 之后、
|
||||
// TAA(Read HDRColor)之前——调试图元进入 TAA/Bloom/Tonemap 主链路
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment);
|
||||
|
||||
@@ -18,11 +18,20 @@
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
// push 布局与 skybox.vert/.frag 的 PushConstants 一致(128B):
|
||||
// invVP = 当前帧反投影;prevVP = 上一帧 proj×view(天空 velocity)
|
||||
struct SkyboxPush
|
||||
{
|
||||
glm::mat4 invVP;
|
||||
glm::mat4 prevVP;
|
||||
};
|
||||
|
||||
SkyboxPass::SkyboxPass(Device& device, ShaderManager& shaders,
|
||||
DescriptorManager& descriptors, FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, const Cubemap* env)
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
const Cubemap* env)
|
||||
: m_Device(device), m_Descriptors(descriptors), m_Frame(frame),
|
||||
m_HDRColor(hdrColor), m_Env(env)
|
||||
m_HDRColor(hdrColor), m_Velocity(velocity), m_Env(env)
|
||||
{
|
||||
// Set0 = {samplerCube}(fragment 采样)
|
||||
std::vector<DescriptorBinding> bindings = {
|
||||
@@ -31,22 +40,22 @@ namespace Engine::Passes
|
||||
};
|
||||
m_SetLayout = descriptors.CreateLayout(bindings);
|
||||
|
||||
// push = invVP 64B(skybox.vert/.frag 声明一致,VUID-08878)
|
||||
// push = {invVP, prevVP} 128B(skybox.vert/.frag 声明一致,VUID-08878)
|
||||
m_Layout = descriptors.CreatePipelineLayout(
|
||||
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
|
||||
sizeof(glm::mat4));
|
||||
sizeof(SkyboxPush));
|
||||
|
||||
const std::string shaderDir = "shaders/env/";
|
||||
auto vert = shaders.CreateFromFile(
|
||||
shaderDir + "skybox.vert.spv",
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
|
||||
sizeof(glm::mat4));
|
||||
sizeof(SkyboxPush));
|
||||
auto frag = shaders.CreateFromFile(
|
||||
shaderDir + "skybox.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
std::vector<vk::DescriptorSetLayout>{m_SetLayout},
|
||||
sizeof(glm::mat4));
|
||||
sizeof(SkyboxPush));
|
||||
|
||||
m_Vert = std::move(vert);
|
||||
m_Frag = std::move(frag);
|
||||
@@ -108,10 +117,26 @@ namespace Engine::Passes
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(clear);
|
||||
|
||||
// velocity 附件:本 pass 是 velocity 的 first writer。清 0 后,
|
||||
// 画天空时写天空运动向量;无天空/不画时保持 0(背景静止)。
|
||||
// Forward 段改为 Load 承接本 pass 的结果
|
||||
vk::ClearValue velocityClear;
|
||||
velocityClear.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
|
||||
vk::RenderingAttachmentInfo velocity;
|
||||
velocity.setImageView(ctx.View(m_Velocity))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eClear)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(velocityClear);
|
||||
|
||||
const std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
|
||||
color, velocity};
|
||||
|
||||
vk::RenderingInfo renderingInfo;
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
|
||||
.setLayerCount(1)
|
||||
.setColorAttachments(color); // 无深度附件
|
||||
.setColorAttachments(colorArray); // 无深度附件
|
||||
|
||||
cmd.beginRendering(renderingInfo);
|
||||
|
||||
@@ -128,11 +153,12 @@ namespace Engine::Passes
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Layout, 0,
|
||||
m_Sets[ctx.frame.frameSlot], {});
|
||||
cmd.pushConstants<glm::mat4>(
|
||||
const SkyboxPush push{m_Frame.skyInvVP, m_Frame.skyPrevVP};
|
||||
cmd.pushConstants<SkyboxPush>(
|
||||
m_Layout,
|
||||
vk::ShaderStageFlagBits::eVertex |
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
0, m_Frame.skyInvVP);
|
||||
0, push);
|
||||
|
||||
// ---- 动态状态 ----
|
||||
cmd.setRasterizerDiscardEnable(VK_FALSE);
|
||||
@@ -180,16 +206,22 @@ namespace Engine::Passes
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
cmd.setColorBlendEnableEXT(0, blendEnable);
|
||||
cmd.setColorBlendEquationEXT(0, blendEquation);
|
||||
cmd.setColorWriteMaskEXT(0, colorWriteMask);
|
||||
cmd.setColorBlendEnableEXT(0,
|
||||
std::array<vk::Bool32, 2>{VK_FALSE, VK_FALSE});
|
||||
const std::array<vk::ColorBlendEquationEXT, 2> blendEquations = {
|
||||
blendEquation, blendEquation};
|
||||
cmd.setColorBlendEquationEXT(0, blendEquations);
|
||||
const std::array<vk::ColorComponentFlags, 2> writeMasks = {
|
||||
colorWriteMask, colorWriteMask};
|
||||
cmd.setColorWriteMaskEXT(0, writeMasks);
|
||||
|
||||
cmd.draw(3, 1, 0, 0);
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment);
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Velocity, RG::ResourceUse::ColorAttachment);
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
@@ -9,9 +9,10 @@ namespace Engine::Passes
|
||||
{
|
||||
TAAPass::TAAPass(FullscreenGpu& gpu, FrameSceneData& frame, f32 historyAlpha,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
|
||||
RG::TextureHandle taaColor)
|
||||
RG::TextureHandle depth, RG::TextureHandle taaColor)
|
||||
: 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_Depth(depth),
|
||||
m_TAAColor(taaColor)
|
||||
{
|
||||
m_Sets.resize(kMaxFramesInFlight);
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
@@ -26,14 +27,25 @@ namespace Engine::Passes
|
||||
// TAA 关闭:alpha=0 → mix(cur, hist, 0) 直通当前帧
|
||||
//(抖动已由 SceneRenderer 同步停掉,画面静止无锯齿晃动)
|
||||
const f32 alpha = m_Frame.taaEnabled ? m_HistoryAlpha : 0.0f;
|
||||
// push 槽位:{ortho, alpha, zNear, zFar}——去遮挡检测在 shader
|
||||
// 内用 near/far 把深度线性化,正交模式走另一分支
|
||||
m_Gpu.RecordFullscreen(ctx, ctx.View(m_TAAColor), m_Gpu.taaFrag,
|
||||
m_Sets[ctx.frame.frameSlot],
|
||||
FullscreenPush{0.0f, alpha, 0.0f, 0.0f});
|
||||
FullscreenPush{m_Frame.cameraOrtho ? 1.0f : 0.0f,
|
||||
alpha,
|
||||
m_Frame.cameraNearZ,
|
||||
m_Frame.cameraFarZ});
|
||||
})
|
||||
.Read(m_HDR, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Read(m_Velocity, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Read(m_Depth, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
// 上一帧深度(同 m_Depth 的上一槽位镜像):去遮挡检测比较
|
||||
// 当前像素深度与重投影点上的历史深度,判断历史是否属于同一表面
|
||||
.ReadHistory(m_Depth, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
// 自引用历史:TAAColor 的上一帧槽位就是累积历史
|
||||
.ReadHistory(m_TAAColor, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
@@ -45,6 +57,11 @@ namespace Engine::Passes
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Gpu.UpdateSetInput(m_Sets[slot], 0, rg, m_HDR, slot);
|
||||
m_Gpu.UpdateSetInput(m_Sets[slot], 1, rg, m_Velocity, slot);
|
||||
// binding3 = 场景深度(深度感知 dilation)
|
||||
m_Gpu.UpdateSetInput(m_Sets[slot], 3, rg, m_Depth, slot);
|
||||
// binding4 = 上一帧深度(去遮挡检测,历史槽位镜像)
|
||||
m_Gpu.UpdateSetInput(m_Sets[slot], 4, rg, m_Depth,
|
||||
rg.HistorySlot(slot));
|
||||
// binding2 = 历史(上一帧槽位镜像)
|
||||
m_Gpu.UpdateSetInput(m_Sets[slot], 2, rg, m_TAAColor,
|
||||
rg.HistorySlot(slot));
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <Engine/Vulkan/CommandManager.h>
|
||||
#include <Engine/Vulkan/SyncManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <Engine/Vulkan/Buffer.h>
|
||||
#include <Engine/Core/Window.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ImGui/ImGuiLayer.h>
|
||||
@@ -426,9 +427,10 @@ namespace Engine
|
||||
"HDRColor",
|
||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat});
|
||||
|
||||
// 场景色副本:不透明段快照(含天空盒背景),透射折射采样源。
|
||||
// 每帧一次 copyImage(Forward 后、ForwardTrans 前),独立于
|
||||
// HDRColor 生命周期(后者还会被透明段/TAA 继续写入)
|
||||
// 场景色副本:透射折射采样源。ForwardPass 分层折射逐层刷新——
|
||||
// 不透明段快照一次(SceneColorCopy),此后每透明层绘制前各拷一次
|
||||
// (SceneColorCopyL*),使前层玻璃折射能采样到更远透明体
|
||||
//(玻璃见玻璃)。独立于 HDRColor 生命周期(后者仍被 TAA 继续写)
|
||||
m_SceneColor = m_RenderGraph->CreateTexture(
|
||||
"SceneColor",
|
||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat});
|
||||
@@ -500,7 +502,7 @@ namespace Engine
|
||||
// Skybox = HDRColor 的 first writer(清屏 + 画背景),先于 Forward 注册
|
||||
m_Passes.push_back(CreateScope<Passes::SkyboxPass>(
|
||||
*m_Device, *m_ShaderManager, *m_DescriptorManager, frame,
|
||||
m_HDRColor, m_Environment.get()));
|
||||
m_HDRColor, m_Velocity, m_Environment.get()));
|
||||
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
|
||||
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth,
|
||||
m_SceneColor, m_ShadowMaps));
|
||||
@@ -521,7 +523,7 @@ namespace Engine
|
||||
frame, m_HDRColor, m_Depth, *m_GizmoIconVB, *m_GizmoWireVB));
|
||||
#endif
|
||||
// UE 式 GPU 拾取:请求帧把 id 画进 R32Uint + 拷贝请求像素进
|
||||
// host 暂存(深度 WAW 链尾,排在 Forward/ForwardTrans/PhysicsDebug
|
||||
// host 暂存(深度 WAW 链尾,排在 Forward/ForwardTransL*/PhysicsDebug
|
||||
// 之后;App 层经 RequestPick/TakePickResult 消费)
|
||||
m_Passes.push_back(CreateScope<Passes::HitProxyPass>(
|
||||
*m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager,
|
||||
@@ -539,7 +541,7 @@ namespace Engine
|
||||
*m_ShaderManager, *m_SceneGpu, frame, m_SelectionMask));
|
||||
m_Passes.push_back(CreateScope<Passes::TAAPass>(
|
||||
*m_FullscreenGpu, frame, /*historyAlpha*/ 0.9f, m_HDRColor,
|
||||
m_Velocity, m_TAAColor));
|
||||
m_Velocity, m_Depth, m_TAAColor));
|
||||
m_Passes.push_back(CreateScope<Passes::TemporalDiffPass>(
|
||||
*m_FullscreenGpu, /*gain*/ 8.0f, m_HDRColor, m_TemporalDiff));
|
||||
m_Passes.push_back(CreateScope<Passes::BloomPass>(
|
||||
@@ -562,7 +564,7 @@ namespace Engine
|
||||
m_Passes.push_back(CreateScope<Passes::MaterialPreviewPass>(
|
||||
*m_Device, *m_Allocator, *m_DescriptorManager, *m_SceneGpu,
|
||||
*m_FullscreenGpu, m_PreviewHDR, m_PreviewVelocity, m_PreviewSDR,
|
||||
m_PreviewRenders));
|
||||
m_PreviewRenders, m_PreviewBakes));
|
||||
m_PreviewPass =
|
||||
static_cast<Passes::MaterialPreviewPass*>(m_Passes.back().get());
|
||||
#endif
|
||||
@@ -672,6 +674,11 @@ namespace Engine
|
||||
void Renderer::DropMaterialPreview(const Material* material)
|
||||
{
|
||||
m_PreviewPool.DropMaterial(material);
|
||||
if (material) {
|
||||
const u64 serial = material->GetSerial();
|
||||
std::erase_if(m_BakeReqs, [serial](const PreviewBakeReq& r)
|
||||
{ return r.serial == serial; });
|
||||
}
|
||||
}
|
||||
|
||||
void* Renderer::GetMaterialPreviewTexture() const
|
||||
@@ -683,6 +690,66 @@ namespace Engine
|
||||
}
|
||||
return tex;
|
||||
}
|
||||
|
||||
void Renderer::RequestMaterialPreviewBake(Material* material, u64 userKey)
|
||||
{
|
||||
if (!material) return;
|
||||
const u64 serial = material->GetSerial();
|
||||
// 同 serial 去重(逐帧 keepalive);同 key 换 serial(资产重载 →
|
||||
// 旧实例销毁)时替换,避免残留悬垂 Material*
|
||||
std::erase_if(m_BakeReqs, [&](const PreviewBakeReq& r)
|
||||
{ return r.key == userKey && r.serial != serial; });
|
||||
for (auto& r : m_BakeReqs) {
|
||||
if (r.serial == serial) {
|
||||
r.material = material; r.key = userKey;
|
||||
r.lastRequestFrame = m_FrameIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
PreviewBakeReq req;
|
||||
req.key = userKey;
|
||||
req.serial = serial;
|
||||
req.material = material;
|
||||
req.lastRequestFrame = m_FrameIndex;
|
||||
m_BakeReqs.push_back(req);
|
||||
}
|
||||
|
||||
bool Renderer::PollMaterialPreviewBake(u64& outKey, std::vector<u8>& outPixels,
|
||||
u32& outWidth, u32& outHeight)
|
||||
{
|
||||
if (m_BakeDone.empty()) return false;
|
||||
PreviewBakeResult res = std::move(m_BakeDone.front());
|
||||
m_BakeDone.erase(m_BakeDone.begin());
|
||||
outKey = res.key;
|
||||
outPixels = std::move(res.pixels);
|
||||
outWidth = res.width;
|
||||
outHeight = res.height;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 槽位 fence 已通过 → 帧 N-2 在该槽位录制的拷贝已完成,取走并出队
|
||||
void Renderer::CollectBakeResults(u32 frameSlot)
|
||||
{
|
||||
std::vector<u64> recorded = std::move(m_BakeRecorded[frameSlot]);
|
||||
m_BakeRecorded[frameSlot].clear();
|
||||
if (recorded.empty()) return;
|
||||
if (!m_PreviewPass) return;
|
||||
Buffer* staging = m_PreviewPass->BakeStaging(frameSlot);
|
||||
const auto* base =
|
||||
static_cast<const u8*>(staging ? staging->MappedPtr() : nullptr);
|
||||
if (!base) return;
|
||||
for (size_t i = 0; i < recorded.size(); ++i) {
|
||||
PreviewBakeResult res;
|
||||
res.key = recorded[i];
|
||||
res.width = Passes::kPreviewTileSize;
|
||||
res.height = Passes::kPreviewTileSize;
|
||||
const u8* src = base + i * Passes::kPreviewTileBytes;
|
||||
res.pixels.assign(src, src + Passes::kPreviewTileBytes);
|
||||
m_BakeDone.push_back(std::move(res));
|
||||
std::erase_if(m_BakeReqs, [&](const PreviewBakeReq& r)
|
||||
{ return r.key == recorded[i]; });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// RG 物理镜像按帧槽位各一份:每槽位注册一个 ImGui 描述符。
|
||||
@@ -690,21 +757,27 @@ namespace Engine
|
||||
void Renderer::RefreshSceneTextures()
|
||||
{
|
||||
if (!m_ImGuiLayer || !m_PresentTarget.Valid()) return;
|
||||
bool previewRecreated = false;
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_ImGuiLayer->UpdateSceneTexture(
|
||||
slot, m_RenderGraph->GetPhysicalView(m_PresentTarget, slot));
|
||||
// 材质球 atlas 同槽位注册(语义与场景纹理一致)
|
||||
#ifdef HELLO_EDITOR
|
||||
if (m_PreviewSDR.Valid()) {
|
||||
m_ImGuiLayer->UpdatePreviewTexture(
|
||||
slot, m_RenderGraph->GetPhysicalView(m_PreviewSDR, slot));
|
||||
const vk::ImageView view =
|
||||
m_RenderGraph->GetPhysicalView(m_PreviewSDR, slot);
|
||||
// 固定尺寸的预览 atlas 跨视口 resize 沿用同一镜像(内容
|
||||
// 存续);只有镜像真的换新时才需要让池重画
|
||||
previewRecreated = previewRecreated || view != m_PreviewViews[slot];
|
||||
m_PreviewViews[slot] = view;
|
||||
m_ImGuiLayer->UpdatePreviewTexture(slot, view);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef HELLO_EDITOR
|
||||
// 重编译 = 物理镜像全部换新(三槽位 atlas 内容清零)——池的
|
||||
// paintedSlots 记录全部失效,可见行 keepalive 请求会重新补画
|
||||
if (m_PreviewSDR.Valid()) {
|
||||
// 物理镜像换新 = atlas 内容清零,池的 paintedSlots 记录全部失效,
|
||||
// 可见行 keepalive 请求会重新补画
|
||||
if (previewRecreated) {
|
||||
m_PreviewPool.InvalidateAll();
|
||||
}
|
||||
#endif
|
||||
@@ -755,6 +828,9 @@ namespace Engine
|
||||
|
||||
const u32 frameSlot = m_CurrentFrame;
|
||||
const auto& vkDevice = m_Device->GetDevice();
|
||||
#ifdef HELLO_EDITOR
|
||||
++m_FrameIndex; // 单调帧号(烘焙请求老化判据)
|
||||
#endif
|
||||
|
||||
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
|
||||
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent, dt)) {
|
||||
@@ -790,6 +866,14 @@ namespace Engine
|
||||
// 2. 等本槽位 fence → 写 UBO/SSBO → reset(写-读竞争关闭)
|
||||
m_SyncManager->WaitFence(frameSlot);
|
||||
ReadGpuTimes(frameSlot); // fence 已等:上一轮该槽位的查询已就绪
|
||||
#ifdef HELLO_EDITOR
|
||||
CollectBakeResults(frameSlot); // 该槽位上一轮拷贝已完成 → 取走
|
||||
// 老化丢弃:UI 已不再 keepalive 的未完成烘焙请求(离屏/换目录),
|
||||
// 避免其持续占用渲染预算
|
||||
std::erase_if(m_BakeReqs, [&](const PreviewBakeReq& r)
|
||||
{ return !r.copied &&
|
||||
m_FrameIndex - r.lastRequestFrame > 4; });
|
||||
#endif
|
||||
m_UniformBuffer->Update(frameSlot, &frame.ubo, sizeof(frame.ubo));
|
||||
m_SyncManager->ResetFence(frameSlot);
|
||||
|
||||
@@ -902,7 +986,31 @@ namespace Engine
|
||||
// 全部跳帧路径,标记 paintedSlots 的槽位镜像本帧必然被写入)。
|
||||
// 预算外的脏 tile 保持未取状态,下帧可见行 keepalive 请求重新入队
|
||||
#ifdef HELLO_EDITOR
|
||||
// 烘焙准备:UI keepalive 请求的材质逐个 RequestBake(强制标脏,
|
||||
// 使其进入本帧脏表;池满无槽者 -1 下帧重试)
|
||||
for (auto& r : m_BakeReqs) {
|
||||
if (r.copied) continue;
|
||||
r.frameTile = m_PreviewPool.RequestBake(r.material);
|
||||
}
|
||||
m_PreviewRenders = m_PreviewPool.TakeRenders(frameSlot);
|
||||
// 回读表只收"本帧确实渲染过"的 tile(条数 ≤ 渲染预算)——内容
|
||||
// 当帧新鲜,拷贝可靠;未渲染的请求留待下帧
|
||||
m_PreviewBakes.clear();
|
||||
u32 renderedTiles = 0;
|
||||
for (const auto& d : m_PreviewRenders) {
|
||||
renderedTiles |= (1u << d.tile);
|
||||
}
|
||||
for (auto& r : m_BakeReqs) {
|
||||
if (r.copied || r.frameTile < 0) continue;
|
||||
if (!(renderedTiles & (1u << r.frameTile))) continue;
|
||||
m_PreviewBakes.push_back(
|
||||
Passes::PreviewBakeTile{r.key, static_cast<u32>(r.frameTile)});
|
||||
r.copied = true;
|
||||
}
|
||||
// 每槽位记录当帧回读 key(帧 N 录 → N+2 同槽位 fence 后取走)
|
||||
auto& recorded = m_BakeRecorded[frameSlot];
|
||||
recorded.clear();
|
||||
for (const auto& b : m_PreviewBakes) recorded.push_back(b.key);
|
||||
#endif
|
||||
|
||||
m_RenderGraph->Execute(frameCtx);
|
||||
|
||||
@@ -466,7 +466,7 @@ namespace Engine
|
||||
// ---- 0. TAA 时域状态先行(velocity = 上一帧姿态与相机)----
|
||||
UniformBufferObject& ubo = m_Frame.ubo;
|
||||
ubo.prevView = m_LastView;
|
||||
ubo.prevProj = m_LastProjNoJitter;
|
||||
ubo.prevProj = m_LastProjNoJitter; // 上一帧未抖动投影(velocity 用)
|
||||
|
||||
// ---- 1. 相机:渲染源按形态分派(UE 语义)。编辑器形态
|
||||
// Scene view 恒用编辑器相机(视口 UI 状态,空场景可用);
|
||||
@@ -506,6 +506,13 @@ namespace Engine
|
||||
// 编辑器形态 = 编辑器相机,游戏形态 = primary 相机实体
|
||||
ubo.proj = camera->GetProjection(aspect);
|
||||
|
||||
// 相机裁剪面 + 投影模式落帧:TAA 去遮挡检测把 [0,1] 深度线性化
|
||||
// 为视图空间距离做比较,需要 near/far 与投影模式
|
||||
m_Frame.cameraNearZ = camera->GetNearZ();
|
||||
m_Frame.cameraFarZ = camera->GetFarZ();
|
||||
m_Frame.cameraOrtho =
|
||||
camera->GetProjectionMode() == Camera::ProjectionMode::Orthographic;
|
||||
|
||||
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
|
||||
ubo.projNoJitter = ubo.proj;
|
||||
|
||||
@@ -581,6 +588,12 @@ namespace Engine
|
||||
m_Frame.skyInvVP =
|
||||
glm::inverse(ubo.projNoJitter * glm::mat4(glm::mat3(ubo.view)));
|
||||
|
||||
// 天空 velocity:上一帧未抖动 proj × 上一帧视图(去平移)。天空是
|
||||
// 无限远表面,方向不随时间平移变化,故用 w=0 传入方向即忽略平移;
|
||||
// 相机旋转/移动时天空的运动向量由此精确得出
|
||||
m_Frame.skyPrevVP =
|
||||
m_LastProjNoJitter * glm::mat4(glm::mat3(m_LastView));
|
||||
|
||||
// ---- 2. 灯光(CSM 阴影光源约定 = lights[0] 必是方向光)----
|
||||
// entt Each 遍历序与实体创建序不保证一致——直接按迭代序填充会让
|
||||
// lights[0] 落到点光上(params.x=1)→ shader 阴影分支整个跳过。
|
||||
@@ -797,6 +810,15 @@ namespace Engine
|
||||
// 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0
|
||||
const f32 dist =
|
||||
glm::length(worldCenter - camTransform->GetWorldPosition());
|
||||
|
||||
// 透明排序键:视图空间深度 = -(view·p).z(沿相机前向
|
||||
// 的投影深度)。绝不能用欧氏距离——侧向偏移会被计入
|
||||
// "距离",使偏侧物体被误判为更远("正方体右上的甜甜圈
|
||||
// 在特定视角被排到后方"的根因)。投影深度只取沿视线
|
||||
// 分量,侧向偏移不干扰序
|
||||
const f32 viewDepth =
|
||||
-(ubo.view * glm::vec4(worldCenter, 1.0f)).z;
|
||||
|
||||
u32 mainLod = 0;
|
||||
if (m_LodEnabled) {
|
||||
if (dist >= worldRadius * 20.0f) {
|
||||
@@ -815,44 +837,61 @@ namespace Engine
|
||||
instanceTruncated = true;
|
||||
return;
|
||||
}
|
||||
// batch 查找:内建材质 = 数组寻址快路径
|
||||
//(Mesh::GetSlot 槽位 + LOD 槽);覆盖材质 =
|
||||
// hash 寻址(键含材质变体:同 mesh 不同覆盖天然
|
||||
// 分批,同覆盖多实体仍合并——材质资产指针仅帧内
|
||||
// 有效,变体表每帧随 ViewScratch::Clear 清空)
|
||||
const bool builtin = (resolved == mesh->GetMaterial());
|
||||
std::array<u32, kMeshLodCount>* lodSlots = nullptr;
|
||||
if (builtin) {
|
||||
const u32 slot = mesh->GetSlot();
|
||||
if (slot >= s.lookup.size()) {
|
||||
s.lookup.resize(slot + 1);
|
||||
}
|
||||
lodSlots = &s.lookup[slot];
|
||||
}
|
||||
else {
|
||||
const auto [vIt, inserted] = s.matVariants.emplace(
|
||||
resolved,
|
||||
static_cast<u32>(s.matVariants.size()));
|
||||
const u64 key =
|
||||
(static_cast<u64>(mesh->GetSlot()) << 32) |
|
||||
static_cast<u64>(vIt->second);
|
||||
lodSlots = &s.ovrLookup[key];
|
||||
}
|
||||
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
|
||||
u32 batchIdx = (*lodSlots)[lod];
|
||||
if (batchIdx == 0) {
|
||||
batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
|
||||
(*lodSlots)[lod] = batchIdx;
|
||||
// first 是占位:真实基址在 appendView 聚合时
|
||||
// 回填(收集序下 batch 实例散布,见链表注释)
|
||||
// 透明队列逐实例成批(不合并):透明段是"每层一个
|
||||
// 绘制单位"(ForwardPass kTransparentLayers 分层),
|
||||
// 层序 = 混合/折射序,必须按实例深度逐实例排。若合并
|
||||
// 成多实例 batch,排序只能以批平均深度参与 → 批内
|
||||
// 实例无法各自定序,层序错。不透明/阴影视图合并
|
||||
//(批内顺序无混合语义,只为少绑状态)
|
||||
const bool perInstance = (&s == &m_TranslucentScratch);
|
||||
u32 bi;
|
||||
if (perInstance) {
|
||||
bi = static_cast<u32>(s.result.batches.size());
|
||||
// first 是占位:真实基址在 appendView 聚合时回填
|
||||
s.result.batches.push_back(
|
||||
{mesh.get(), resolved, 0, 0, lod});
|
||||
s.batchDistSum.push_back(0.0f);
|
||||
s.batchHead.push_back(ViewScratch::kNoInstance);
|
||||
}
|
||||
// 实例归入自己的 batch(mesh 分组):收集序散布,
|
||||
// 组内连接用链表维护(O(1) push,无 per-batch 分配)
|
||||
const u32 bi = batchIdx - 1;
|
||||
else {
|
||||
// batch 查找:内建材质 = 数组寻址快路径
|
||||
//(Mesh::GetSlot 槽位 + LOD 槽);覆盖材质 =
|
||||
// hash 寻址(键含材质变体:同 mesh 不同覆盖天然
|
||||
// 分批,同覆盖多实体仍合并——材质资产指针仅帧内
|
||||
// 有效,变体表每帧随 ViewScratch::Clear 清空)
|
||||
const bool builtin = (resolved == mesh->GetMaterial());
|
||||
std::array<u32, kMeshLodCount>* lodSlots = nullptr;
|
||||
if (builtin) {
|
||||
const u32 slot = mesh->GetSlot();
|
||||
if (slot >= s.lookup.size()) {
|
||||
s.lookup.resize(slot + 1);
|
||||
}
|
||||
lodSlots = &s.lookup[slot];
|
||||
}
|
||||
else {
|
||||
const auto [vIt, inserted] = s.matVariants.emplace(
|
||||
resolved,
|
||||
static_cast<u32>(s.matVariants.size()));
|
||||
const u64 key =
|
||||
(static_cast<u64>(mesh->GetSlot()) << 32) |
|
||||
static_cast<u64>(vIt->second);
|
||||
lodSlots = &s.ovrLookup[key];
|
||||
}
|
||||
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
|
||||
u32 batchIdx = (*lodSlots)[lod];
|
||||
if (batchIdx == 0) {
|
||||
batchIdx =
|
||||
static_cast<u32>(s.result.batches.size()) + 1;
|
||||
(*lodSlots)[lod] = batchIdx;
|
||||
s.result.batches.push_back(
|
||||
{mesh.get(), resolved, 0, 0, lod});
|
||||
s.batchDistSum.push_back(0.0f);
|
||||
s.batchHead.push_back(ViewScratch::kNoInstance);
|
||||
}
|
||||
bi = batchIdx - 1;
|
||||
}
|
||||
// 实例归入自己的 batch(收集序散布,组内连接用链表
|
||||
// 维护:O(1) push,无 per-batch 分配)
|
||||
s.result.batches[bi].count++;
|
||||
const u32 ii =
|
||||
static_cast<u32>(s.instances.size());
|
||||
@@ -861,7 +900,7 @@ namespace Engine
|
||||
s.instanceNext.push_back(s.batchHead[bi]);
|
||||
s.batchHead[bi] = ii; // 头插:链序 = push 逆序(无妨,
|
||||
// 实例对齐性由 (矩阵,id) 成对保证)
|
||||
s.batchDistSum[bi] += dist; // 透明排序用
|
||||
s.batchDistSum[bi] += viewDepth; // 透明排序键:视图深度
|
||||
s.result.instanceCount++;
|
||||
++totalInstances;
|
||||
};
|
||||
@@ -926,9 +965,11 @@ namespace Engine
|
||||
// PackIndirect(m_Frame.shadowCascades[c], false);
|
||||
// }
|
||||
|
||||
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排。
|
||||
// 实例按链表逐 batch 搬运(收集序散布,不能按 (first,count) 切片);
|
||||
// 搬完的新数组天然按 batch 连续 → 链表确定性重建(顺序遍历)
|
||||
// ---- 5. 透明批次排序:视图深度降序(远 → 近),连同实例列表同步重排。
|
||||
// 透明队列逐实例成批(见 tryAppend),故 count=1、batchDistSum = 该
|
||||
// 实例的视图深度 → 批序 = 逐实例深度序。实例按链表逐 batch 搬运
|
||||
//(收集序散布,不能按 (first,count) 切片);搬完的新数组天然按
|
||||
// batch 连续 → 链表确定性重建(顺序遍历)
|
||||
{
|
||||
auto& tb = m_TranslucentScratch.result.batches;
|
||||
if (tb.size() > 1) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
@@ -31,34 +32,84 @@ namespace Engine {
|
||||
InitSharedDefaultTextures();
|
||||
|
||||
// 每槽解析:路径(CWD 相对,反斜杠归一,与 Model::GetOrLoadTexture
|
||||
// 同语义)→ 独占纹理;空/缺失 → 共享默认纹理(非拥有)
|
||||
std::array<Scope<Texture>, Material::kSlotCount> owned;
|
||||
// 同语义)→ 路径缓存命中的共享纹理 / 本批新建;空/缺失 → 共享
|
||||
// 默认纹理(非拥有)。新建纹理的拷贝/barrier 全部录制进同一 cmd,
|
||||
// 末尾一次 submit + 一次 fence 等待服务整批——取代过去"每张贴图
|
||||
// 各自建命令池 + 提交 + 阻塞等待",这是大材质目录首开卡顿的根因
|
||||
std::array<Texture*, Material::kSlotCount> bound{};
|
||||
// 槽位 sRGB 语义:颜色图 true,数据图 false(与 Model 同表)
|
||||
const bool slotSrgb[Material::kSlotCount] = {true, false, false,
|
||||
true, false, false};
|
||||
|
||||
std::vector<Texture*> created; // 本批新建纹理(fence 后释放 staging)
|
||||
vk::CommandPool pool = nullptr;
|
||||
vk::CommandBuffer cmd = nullptr;
|
||||
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
std::string path = data.texPaths[slot];
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
if (!path.empty() && std::filesystem::exists(path)) {
|
||||
owned[slot] = CreateScope<Texture>(
|
||||
m_Device, m_Allocator, path, slotSrgb[slot]);
|
||||
bound[slot] = owned[slot].get();
|
||||
}
|
||||
else {
|
||||
if (path.empty() || !std::filesystem::exists(path)) {
|
||||
if (!path.empty()) {
|
||||
ENGINE_WARN("MaterialAsset: texture not found '{}', "
|
||||
"slot {} falls back to default", path, slot);
|
||||
}
|
||||
bound[slot] = m_DefaultTextures[slot].get();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 缓存键:路径 + sRGB 标记(同一文件按颜色/数据两种语义加载时
|
||||
// 格式不同,必须分开缓存)
|
||||
std::string key = path;
|
||||
key += slotSrgb[slot] ? "|s" : "|l";
|
||||
if (auto it = m_TextureCache.find(key); it != m_TextureCache.end()) {
|
||||
bound[slot] = it->second.get();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 未命中:惰性建批式命令缓冲,解码 + 录制上传(不提交)
|
||||
if (!cmd) {
|
||||
vk::CommandPoolCreateInfo poolInfo;
|
||||
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
|
||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
||||
pool = m_Device.GetDevice().createCommandPool(poolInfo);
|
||||
|
||||
vk::CommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.setCommandPool(pool)
|
||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||
.setCommandBufferCount(1);
|
||||
cmd = m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
|
||||
|
||||
cmd.begin(vk::CommandBufferBeginInfo{
|
||||
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
||||
}
|
||||
|
||||
auto tex = Texture::CreateDeferred(m_Device, m_Allocator, path,
|
||||
slotSrgb[slot], cmd);
|
||||
Texture* raw = tex.get();
|
||||
bound[slot] = raw;
|
||||
created.push_back(raw);
|
||||
m_TextureCache.emplace(std::move(key), std::move(tex));
|
||||
}
|
||||
|
||||
// 整批一次提交 + 一次等待,随后释放各新建纹理的 staging
|
||||
if (cmd) {
|
||||
cmd.end();
|
||||
vk::Fence fence = m_Device.GetDevice().createFence({});
|
||||
m_Device.GetGraphicsQueue().submit(
|
||||
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
|
||||
if (m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX)
|
||||
!= vk::Result::eSuccess) {
|
||||
ENGINE_ERROR("Material texture batch upload: waitForFences failed");
|
||||
}
|
||||
m_Device.GetDevice().destroyFence(fence);
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
for (Texture* t : created) t->ReleasePendingStaging();
|
||||
}
|
||||
|
||||
auto mat = CreateScope<Material>(
|
||||
m_Device, m_Allocator, m_Descriptors, m_TextureLayout,
|
||||
bound, data.params, data.transparent);
|
||||
return CreateScope<MaterialAsset>(std::move(data), std::move(owned),
|
||||
std::move(mat));
|
||||
return CreateScope<MaterialAsset>(std::move(data), std::move(mat));
|
||||
}
|
||||
|
||||
void VulkanAssetUploader::WaitDeviceIdle()
|
||||
@@ -101,6 +152,14 @@ namespace Engine {
|
||||
m_TextureLayout, halfSize, r, g, b);
|
||||
}
|
||||
|
||||
Scope<Model> VulkanAssetUploader::CreatePrimitive(const std::string& shape,
|
||||
const PrimitiveDesc& desc,
|
||||
u8 r, u8 g, u8 b)
|
||||
{
|
||||
return Model::CreatePrimitive(m_Device, m_Allocator, m_Descriptors,
|
||||
m_TextureLayout, shape, desc, r, g, b);
|
||||
}
|
||||
|
||||
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
|
||||
{
|
||||
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
|
||||
|
||||
@@ -1290,6 +1290,57 @@ namespace Engine::Serialization {
|
||||
catch (const std::exception&) { return {}; }
|
||||
}
|
||||
|
||||
// ---- 通用图元(MeshFactory 形状)----
|
||||
// 伪路径:"procedural://<shape>/{p0[_p1]}/{r},{g},{b}",参数按 '_'
|
||||
// 拆分。参数个数按形状校验,非法即拒(回退空句柄)
|
||||
auto tryPrimitive = [&](std::string_view prefix, const std::string& shape,
|
||||
size_t paramCount) -> ModelRef {
|
||||
const std::string rest = path.substr(prefix.size());
|
||||
const auto slash = rest.find('/');
|
||||
if (slash == std::string::npos) return {};
|
||||
|
||||
const std::string paramStr = rest.substr(0, slash);
|
||||
int c[3];
|
||||
if (!ParseCsvInts(rest.substr(slash + 1), c)) return {};
|
||||
|
||||
std::vector<f32> vals;
|
||||
size_t pos = 0;
|
||||
while (true) {
|
||||
const size_t us = paramStr.find('_', pos);
|
||||
const std::string tok = paramStr.substr(
|
||||
pos, us == std::string::npos ? std::string::npos : us - pos);
|
||||
if (tok.empty()) return {};
|
||||
try { vals.push_back(std::stof(tok)); }
|
||||
catch (const std::exception&) { return {}; }
|
||||
if (us == std::string::npos) break;
|
||||
pos = us + 1;
|
||||
}
|
||||
if (vals.size() != paramCount) return {};
|
||||
|
||||
PrimitiveDesc d;
|
||||
if (shape == "cylinder" || shape == "cone") {
|
||||
d.size = vals[0];
|
||||
d.height = vals[1];
|
||||
} else if (shape == "torus") {
|
||||
d.majorRadius = vals[0];
|
||||
d.minorRadius = vals[1];
|
||||
} else {
|
||||
d.size = vals[0];
|
||||
}
|
||||
return assets.CreatePrimitive(shape, d, static_cast<u8>(c[0]),
|
||||
static_cast<u8>(c[1]),
|
||||
static_cast<u8>(c[2]));
|
||||
};
|
||||
|
||||
if (path.rfind("procedural://sphere/", 0) == 0)
|
||||
return tryPrimitive("procedural://sphere/", "sphere", 1);
|
||||
if (path.rfind("procedural://cylinder/", 0) == 0)
|
||||
return tryPrimitive("procedural://cylinder/", "cylinder", 2);
|
||||
if (path.rfind("procedural://cone/", 0) == 0)
|
||||
return tryPrimitive("procedural://cone/", "cone", 2);
|
||||
if (path.rfind("procedural://torus/", 0) == 0)
|
||||
return tryPrimitive("procedural://torus/", "torus", 2);
|
||||
|
||||
return {}; // 未知伪路径
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,11 @@ namespace Engine
|
||||
return tex;
|
||||
}
|
||||
|
||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
|
||||
// 公共上传路径核心:建 staging/image/view/sampler + 逐 mip
|
||||
// barrier/copy/blit 录制进 cmd(**不**提交)。staging 存 m_PendingStaging,
|
||||
// 由调用方在提交 fence 通过后 ReleasePendingStaging 释放
|
||||
void Texture::RecordPixelsUpload(const void* pixels, u32 width, u32 height,
|
||||
bool srgb, vk::CommandBuffer cmd)
|
||||
{
|
||||
// 重入安全:CreateFromEncodedMemory 先经 ProceduralTag ctor 落 1x1
|
||||
// 占位(只为确定 device/allocator 成员),再调本函数传真实像素。
|
||||
@@ -118,15 +122,17 @@ namespace Engine
|
||||
vk::DeviceSize imageSize = static_cast<vk::DeviceSize>(width) *
|
||||
static_cast<vk::DeviceSize>(height) * 4; // RGBA8
|
||||
|
||||
// 2. 创建 Staging Buffer(HOST_VISIBLE,VMA 持久映射)
|
||||
auto stagingAlloc = m_Allocator.CreateBuffer(
|
||||
// 2. 创建 Staging Buffer(HOST_VISIBLE,VMA 持久映射)。暂存对象内,
|
||||
// 由调用方在提交 fence 通过后 ReleasePendingStaging 释放
|
||||
m_PendingStaging = m_Allocator.CreateBuffer(
|
||||
imageSize,
|
||||
vk::BufferUsageFlagBits::eTransferSrc,
|
||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
m_HasPendingStaging = true;
|
||||
|
||||
// 3. 拷贝像素到 Staging Buffer
|
||||
std::memcpy(stagingAlloc.MappedPtr, pixels, static_cast<size_t>(imageSize));
|
||||
std::memcpy(m_PendingStaging.MappedPtr, pixels, static_cast<size_t>(imageSize));
|
||||
|
||||
// 4. 创建 Device Local Image
|
||||
vk::ImageCreateInfo imageInfo;
|
||||
@@ -180,25 +186,9 @@ namespace Engine
|
||||
|
||||
m_Sampler = m_Device.GetDevice().createSampler(samplerInfo);
|
||||
|
||||
// 6. 一次性命令缓冲:Staging → Image
|
||||
vk::CommandPoolCreateInfo poolInfo;
|
||||
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
|
||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
||||
|
||||
vk::CommandPool pool = m_Device.GetDevice().createCommandPool(poolInfo);
|
||||
|
||||
vk::CommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.setCommandPool(pool)
|
||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||
.setCommandBufferCount(1);
|
||||
|
||||
vk::CommandBuffer cmd = m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
|
||||
|
||||
vk::CommandBufferBeginInfo beginInfo;
|
||||
beginInfo.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
|
||||
cmd.begin(beginInfo);
|
||||
|
||||
// 6.1 布局:UNDEFINED → TRANSFER_DST
|
||||
// 6. 布局:UNDEFINED → TRANSFER_DST(整条 mip 链一次性转目标布局:
|
||||
// 后续 blit 的目标 mip 必须已在目标布局;仅转 mip0 会触发
|
||||
// validation 对每个 blit 目标的 UNDEFINED 布局告警)
|
||||
vk::ImageMemoryBarrier2 toTransfer;
|
||||
toTransfer.setSrcStageMask(vk::PipelineStageFlagBits2::eNone)
|
||||
.setSrcAccessMask(vk::AccessFlagBits2::eNone)
|
||||
@@ -207,9 +197,6 @@ namespace Engine
|
||||
.setOldLayout(vk::ImageLayout::eUndefined)
|
||||
.setNewLayout(vk::ImageLayout::eTransferDstOptimal)
|
||||
.setImage(m_Image)
|
||||
// 整条 mip 链一次性 UNDEFINED → TRANSFER_DST:后续 blit
|
||||
// 的目标 mip 必须已在目标布局(仅转 mip0 会触发 validation
|
||||
// 对每个 blit 目标的 UNDEFINED 布局告警)
|
||||
.setSubresourceRange({vk::ImageAspectFlagBits::eColor,
|
||||
0, m_MipLevels, 0, 1});
|
||||
|
||||
@@ -226,7 +213,7 @@ namespace Engine
|
||||
.setImageOffset({0, 0, 0})
|
||||
.setImageExtent({m_Width, m_Height, 1});
|
||||
|
||||
cmd.copyBufferToImage(stagingAlloc.Buffer, m_Image,
|
||||
cmd.copyBufferToImage(m_PendingStaging.Buffer, m_Image,
|
||||
vk::ImageLayout::eTransferDstOptimal, region);
|
||||
|
||||
// 6.3 blit 生成 mip 链:逐级半分辨率缩小(硬件线性滤波)。
|
||||
@@ -291,27 +278,6 @@ namespace Engine
|
||||
dep2.setImageMemoryBarriers(toShader);
|
||||
cmd.pipelineBarrier2(dep2);
|
||||
|
||||
cmd.end();
|
||||
|
||||
// 7. 提交并等待
|
||||
vk::SubmitInfo submit;
|
||||
submit.setCommandBuffers(cmd);
|
||||
|
||||
vk::FenceCreateInfo fenceInfo;
|
||||
vk::Fence fence = m_Device.GetDevice().createFence(fenceInfo);
|
||||
|
||||
m_Device.GetGraphicsQueue().submit(submit, fence);
|
||||
auto waitResult = m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX);
|
||||
if (waitResult != vk::Result::eSuccess)
|
||||
{
|
||||
ENGINE_ERROR("Texture upload: waitForFences failed");
|
||||
}
|
||||
|
||||
// 8. 清理临时对象(staging 资源与分配经 VMA 成对释放)
|
||||
m_Device.GetDevice().destroyFence(fence);
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
m_Allocator.Destroy(stagingAlloc);
|
||||
|
||||
// 显存占用按真实格式 + mip 链估算(与分配器台账同一算法)
|
||||
if (m_TextureBytes != 0) {
|
||||
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
|
||||
@@ -321,6 +287,87 @@ namespace Engine
|
||||
STATS.AddTexture(m_TextureBytes);
|
||||
}
|
||||
|
||||
// 单张自提交路径(程序化/CreateFromEncodedMemory/非批式下载):录制
|
||||
// 完自行建一次性命令池 + 提交 + fence 等待 + 释放 staging
|
||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height,
|
||||
bool srgb)
|
||||
{
|
||||
vk::CommandPoolCreateInfo poolInfo;
|
||||
poolInfo.setQueueFamilyIndex(m_Device.GetGraphicsFamily())
|
||||
.setFlags(vk::CommandPoolCreateFlagBits::eTransient);
|
||||
vk::CommandPool pool = m_Device.GetDevice().createCommandPool(poolInfo);
|
||||
|
||||
vk::CommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.setCommandPool(pool)
|
||||
.setLevel(vk::CommandBufferLevel::ePrimary)
|
||||
.setCommandBufferCount(1);
|
||||
vk::CommandBuffer cmd =
|
||||
m_Device.GetDevice().allocateCommandBuffers(allocInfo)[0];
|
||||
|
||||
cmd.begin(vk::CommandBufferBeginInfo{
|
||||
vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
|
||||
RecordPixelsUpload(pixels, width, height, srgb, cmd);
|
||||
cmd.end();
|
||||
|
||||
vk::Fence fence = m_Device.GetDevice().createFence({});
|
||||
m_Device.GetGraphicsQueue().submit(
|
||||
vk::SubmitInfo{}.setCommandBuffers(cmd), fence);
|
||||
if (m_Device.GetDevice().waitForFences(fence, VK_TRUE, UINT64_MAX) !=
|
||||
vk::Result::eSuccess) {
|
||||
ENGINE_ERROR("Texture upload: waitForFences failed");
|
||||
}
|
||||
|
||||
m_Device.GetDevice().destroyFence(fence);
|
||||
m_Device.GetDevice().destroyCommandPool(pool);
|
||||
ReleasePendingStaging();
|
||||
}
|
||||
|
||||
// 批式路径构造:复用 stbi 解码 + RecordPixelsUpload(录制进外部 cmd,
|
||||
// 不提交;staging 交由调用方在批量 fence 后释放)。DDS 稀有路径退回
|
||||
// 自带提交的 InitFromDds
|
||||
Texture::Texture(Device& device, MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb, vk::CommandBuffer cmd)
|
||||
: m_Device(device), m_Allocator(allocator)
|
||||
{
|
||||
const std::string ext = path.size() >= 4
|
||||
? path.substr(path.size() - 4) : "";
|
||||
std::string lower;
|
||||
for (char c : ext) lower += static_cast<char>(std::tolower(c));
|
||||
|
||||
if (lower == ".dds") {
|
||||
InitFromDds(path, srgb);
|
||||
return;
|
||||
}
|
||||
|
||||
int width = 0, height = 0, channels = 0;
|
||||
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels,
|
||||
STBI_rgb_alpha);
|
||||
if (!pixels) {
|
||||
ENGINE_ERROR("Failed to load texture: {}", path);
|
||||
return;
|
||||
}
|
||||
RecordPixelsUpload(pixels, static_cast<u32>(width),
|
||||
static_cast<u32>(height), srgb, cmd);
|
||||
stbi_image_free(pixels);
|
||||
ENGINE_INFO("Texture loaded (batched): {} ({}x{})", path, m_Width,
|
||||
m_Height);
|
||||
}
|
||||
|
||||
Scope<Texture> Texture::CreateDeferred(Device& device,
|
||||
MemoryAllocator& allocator,
|
||||
const std::string& path, bool srgb,
|
||||
vk::CommandBuffer cmd)
|
||||
{
|
||||
return Scope<Texture>(new Texture(device, allocator, path, srgb, cmd));
|
||||
}
|
||||
|
||||
void Texture::ReleasePendingStaging()
|
||||
{
|
||||
if (!m_HasPendingStaging) return;
|
||||
m_Allocator.Destroy(m_PendingStaging);
|
||||
m_HasPendingStaging = false;
|
||||
}
|
||||
|
||||
// DDS → Vulkan 格式映射。sRGB 语义由贴图槽位决定(渲染侧职责):
|
||||
// 颜色图(BaseColor/Emissive)→ SRGB 变体
|
||||
// 数据图(Normal/Specular 等线性槽位)→ UNORM
|
||||
@@ -520,6 +567,10 @@ namespace Engine
|
||||
{
|
||||
const auto& vkDevice = m_Device.GetDevice();
|
||||
|
||||
// 批式上传若在 fence 通过前被析构(如加载中途失败):兜底释放
|
||||
// staging,避免 VMA 块非空断言崩溃
|
||||
ReleasePendingStaging();
|
||||
|
||||
vkDevice.destroySampler(m_Sampler);
|
||||
vkDevice.destroyImageView(m_View);
|
||||
|
||||
|
||||
Vendored
+12
-1
@@ -6,16 +6,27 @@
|
||||
|
||||
layout(location = 0) in vec2 fragUV;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
layout(location = 1) out vec2 outVelocity; // TAA velocity 附件
|
||||
|
||||
layout(set = 0, binding = 0) uniform samplerCube envMap;
|
||||
|
||||
// push 与 skybox.vert 一致声明(VUID-08878)
|
||||
layout(push_constant) uniform PushConstants {
|
||||
mat4 invVP;
|
||||
mat4 invVP; // 当前帧反投影(方向重建)
|
||||
mat4 prevVP; // 上一帧 proj×view(去平移):天空 velocity
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
vec4 target = pc.invVP * vec4(fragUV * 2.0 - 1.0, 1.0, 1.0);
|
||||
vec3 dir = normalize(target.xyz / target.w);
|
||||
outColor = vec4(texture(envMap, dir).rgb, 1.0);
|
||||
|
||||
// 天空 velocity:天空是无限远表面,方向不随相机平移变化(w=0 忽略
|
||||
// 平移),过上一帧 proj×view 得上一帧屏幕位置,与当前 fragUV 之差即
|
||||
// 运动向量。相机移动时天空/背景因此正确重投影,不再闪烁
|
||||
vec4 prevClip = pc.prevVP * vec4(dir, 0.0);
|
||||
vec2 prevUV = abs(prevClip.w) > 1e-6
|
||||
? (prevClip.xy / prevClip.w) * 0.5 + 0.5
|
||||
: fragUV;
|
||||
outVelocity = fragUV - prevUV;
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -8,7 +8,8 @@ layout(location = 0) out vec2 fragUV;
|
||||
|
||||
// push 与 skybox.frag 一致声明(VUID-08878:shader object 接口一致)
|
||||
layout(push_constant) uniform PushConstants {
|
||||
mat4 invVP;
|
||||
mat4 invVP; // 当前帧反投影(方向重建)
|
||||
mat4 prevVP; // 上一帧 proj×view(去平移):天空 velocity
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -45,7 +45,7 @@ layout(set = 1, binding = 6) uniform MaterialParams {
|
||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||
vec4 surface; // x = ior(F0 换算),y = transmission(不透明段恒 0 语义),z = 自发光强度,w = 体积厚度
|
||||
vec4 misc; // x = unlit(KHR_materials_unlit,1 = 早退直出 albedo),y = UV 左右镜像,z = UV 上下镜像
|
||||
vec4 misc; // x = unlit(KHR_materials_unlit,1 = 早退直出 albedo),y = UV 左右镜像,z = UV 上下镜像,w = 法线绿通道翻转(DirectX)
|
||||
vec4 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
|
||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||
@@ -124,6 +124,8 @@ vec3 MappedNormal(vec3 geomN, vec3 T, vec2 uv) {
|
||||
vec3 nT = normalize(T - N * dot(N, T)); // 正交化
|
||||
vec3 B = cross(N, nT);
|
||||
vec3 sampled = texture(normalMap, uv).xyz * 2.0 - 1.0;
|
||||
// DirectX 法线(绿通道朝下)→ OpenGL:翻绿。UE 打包 Specular 资产默认开
|
||||
if (matParams.misc.w > 0.5) sampled.y = -sampled.y;
|
||||
if (dot(sampled, sampled) < 1e-4) return geomN;
|
||||
|
||||
return normalize(nT * sampled.x + B * sampled.y + N * sampled.z);
|
||||
@@ -257,17 +259,21 @@ void main() {
|
||||
|
||||
vec3 irradiance = texture(irradianceMap, N).rgb;
|
||||
float ao = texture(aoMap, ApplyUv(4, fragUV)).r;
|
||||
vec3 diffuse = kD_amb * irradiance * albedo * ao;
|
||||
// 漫反射环境遮蔽:材料 AO × 阴影软化(较温和,保留阴影内可读性)
|
||||
float diffOcc = ao * mix(0.4, 1.0, shade0);
|
||||
vec3 diffuse = kD_amb * irradiance * albedo * diffOcc;
|
||||
|
||||
vec3 R = reflect(-V, N);
|
||||
const float kMaxPrefilterMip = 4.0; // 与 Cubemap::kPrefilterMips - 1 同步
|
||||
vec3 prefilteredColor = textureLod(prefilteredMap, R,
|
||||
roughness * kMaxPrefilterMip).rgb;
|
||||
vec2 envBRDF = texture(brdfLut, vec2(NdotV, roughness)).rg;
|
||||
vec3 specular = prefilteredColor * (F_amb * envBRDF.x + envBRDF.y);
|
||||
// 高光遮蔽(specular occlusion):比漫反射更狠——室内环境高光必须压暗,
|
||||
// 否则室内表面反射出天空贴图(穿帮根因)
|
||||
float specOcc = ao * mix(0.12, 1.0, shade0);
|
||||
vec3 specular = prefilteredColor * (F_amb * envBRDF.x + envBRDF.y) * specOcc;
|
||||
|
||||
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
|
||||
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
|
||||
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb;
|
||||
|
||||
// 不透明段覆盖率恒 1:本 pass 只画 alphaMode=OPAQUE/MASK 表面,像素必
|
||||
// 被完全覆盖。绝不能拿 albedo 贴图 alpha 当覆盖率——HI3/Genshin 脸部
|
||||
|
||||
@@ -75,7 +75,10 @@ void main() {
|
||||
gl_Position = ubo.proj * ubo.view * worldPos;
|
||||
|
||||
// ---- TAA velocity:当前帧与上一帧的 clip 空间位置差(UV 空间)----
|
||||
// 抖动只进光栅化 gl_Position,不进 velocity(否则抖动本身被当成运动)
|
||||
// 抖动不进 velocity:velocity 描述表面点的真实屏幕位移,必须用未抖动投影。
|
||||
// 静态内容 → velocity=0 → 历史精确采回同一纹素,只做逐帧抖动的超采样平均
|
||||
//(清清楚楚、无重采样模糊)。若把抖动计入 velocity,静止画面也会得到非零
|
||||
// 速度 → 每帧亚纹素重采样历史 → 持续发糊(本次"画面很糊"的根因)。
|
||||
// v1:蒙皮形变不进 velocity(prev 用同一蒙皮姿态),边缘 ghosting 接受
|
||||
vec4 currClip = ubo.projNoJitter * ubo.view * worldPos;
|
||||
vec4 prevWorldPos = prevObjects.prevModels[gl_InstanceIndex] * vec4(skinnedPos, 1.0);
|
||||
|
||||
@@ -45,7 +45,7 @@ layout(set = 1, binding = 6) uniform MaterialParams {
|
||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||
vec4 surface; // x = ior,y = transmission(KHR_materials_transmission),z = 自发光强度,w = 体积厚度
|
||||
vec4 misc; // x = unlit(透射材质不会是 unlit,仅为与 simple_shader 布局一致),y = UV 左右镜像,z = UV 上下镜像
|
||||
vec4 misc; // x = unlit(透射材质不会是 unlit,仅为与 simple_shader 布局一致),y = UV 左右镜像,z = UV 上下镜像,w = 法线绿通道翻转(DirectX)
|
||||
vec4 emissiveFactor; // 自发光颜色(rgb × emissiveMap × surface.z;黑 = 不发光)
|
||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||
@@ -123,6 +123,8 @@ vec3 MappedNormal(vec3 geomN, vec3 T, vec2 uv) {
|
||||
vec3 nT = normalize(T - N * dot(N, T)); // 正交化
|
||||
vec3 B = cross(N, nT);
|
||||
vec3 sampled = texture(normalMap, uv).xyz * 2.0 - 1.0;
|
||||
// DirectX 法线(绿通道朝下)→ OpenGL:翻绿。UE 打包 Specular 资产默认开
|
||||
if (matParams.misc.w > 0.5) sampled.y = -sampled.y;
|
||||
if (dot(sampled, sampled) < 1e-4) return geomN;
|
||||
|
||||
return normalize(nT * sampled.x + B * sampled.y + N * sampled.z);
|
||||
@@ -266,9 +268,10 @@ void main() {
|
||||
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
|
||||
|
||||
// ---- 透射(KHR_materials_transmission):屏幕空间折射 ----
|
||||
// 采样源 = 不透明段快照(天空盒 + 不透明几何,Forward 后
|
||||
// ForwardTrans 前拷贝)。玻璃互相不可见(副本不含透明队列),
|
||||
// 与 UE 屏幕空间透射同一限制。
|
||||
// 采样源 = 场景色副本,每层绘制前逐层刷新(ForwardPass 的分层折射:
|
||||
// 透明队列远→近拆成至多 kTransparentLayers 层,层 L 绘制前把 HDRColor
|
||||
// 拷进副本)。故层 L 玻璃的副本含全部更远透明体——前玻璃能折射出
|
||||
// 后玻璃(玻璃见玻璃),摆脱 UE/Unity 单快照"玻璃互相不可见"的局限。
|
||||
vec3 transmitted = vec3(0.0);
|
||||
if (transmission > 0.0) {
|
||||
// 进入介质 eta = 1/ior;全内反射(TIR)退化为反射方向
|
||||
|
||||
+53
-15
@@ -1,26 +1,32 @@
|
||||
#version 450
|
||||
|
||||
// TAA resolve:时域抗锯齿。
|
||||
// 1. velocity 反推历史像素位置,取历史累积色
|
||||
// 2. 方差裁剪(YCoCg):历史色钳向邻域均值 ± γσ(∩ AABB 极值盒)——
|
||||
// 比 RGB min/max 盒稳得多:AABB 盒子被逐帧抖动的高亮像素拽来拽去,
|
||||
// 收敛历史持续被重钳 → 周期性残差经 bloom 阈值放大 = 静态模型泛光闪烁
|
||||
// 3. mix(cur, hist, alpha) 时域累积
|
||||
// 1. 深度感知 velocity dilation:轮廓处取最近表面的速度,反推历史像素位置
|
||||
// 2. 去遮挡检测(disocclusion):重投影点上的"上一帧深度"若与当前像素
|
||||
// 深度显著不同 → 该像素上一帧属于别的表面(刚露出/刚被遮挡),
|
||||
// 历史颜色不属于当前表面 → 拒绝历史(alpha→0),用当前帧。
|
||||
// 这是"移动时一闪一闪"的结构性根因:只查 histUV 是否出屏、不查历史
|
||||
// 是否同一表面时,新暴露区域会持续采到前/背景串味的历史色
|
||||
// 3. 方差裁剪(YCoCg):历史色钳向邻域均值 ± γσ(∩ AABB 极值盒)
|
||||
// 4. mix(cur, hist, alpha) 时域累积
|
||||
// 输出写入 TAAColor,它同时也是下一帧的历史(ReadHistory 自引用)。
|
||||
|
||||
layout(location = 0) in vec2 fragUV;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D curImage; // 当前帧 HDR
|
||||
layout(set = 0, binding = 1) uniform sampler2D velocityImage; // 运动向量(UV 空间)
|
||||
layout(set = 0, binding = 2) uniform sampler2D historyImage; // 上一帧累积结果
|
||||
layout(set = 0, binding = 0) uniform sampler2D curImage; // 当前帧 HDR
|
||||
layout(set = 0, binding = 1) uniform sampler2D velocityImage; // 运动向量(UV 空间)
|
||||
layout(set = 0, binding = 2) uniform sampler2D historyImage; // 上一帧累积结果
|
||||
layout(set = 0, binding = 3) uniform sampler2D depthImage; // 当前帧深度(近=0,远=1)
|
||||
layout(set = 0, binding = 4) uniform sampler2D historyDepthImage; // 上一帧深度(同资源历史槽位)
|
||||
|
||||
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878)
|
||||
// 全屏 pass 通用 push(16B,程序级统一声明,VUID-08878):
|
||||
// 本 pass 语义 = {ortho, alpha, zNear, zFar}
|
||||
layout(push_constant) uniform PushConstants {
|
||||
float exposure; // 与其它全屏 pass 布局一致(本 pass 不用)
|
||||
float alpha; // 历史权重(0.9 = 90% 历史)
|
||||
float param1;
|
||||
float param2;
|
||||
float ortho; // 1 = 正交投影(深度线性化走线性分支)
|
||||
float alpha; // 历史权重(0.9 = 90% 历史)
|
||||
float zNear; // 相机近裁剪面
|
||||
float zFar; // 相机远裁剪面
|
||||
} pc;
|
||||
|
||||
// YCoCg 感知空间:亮度/色度分离,方差裁剪不在 HDR RGB 直接做
|
||||
@@ -36,20 +42,52 @@ vec3 YCoCgToRgb(vec3 c) {
|
||||
c.x - c.y - c.z);
|
||||
}
|
||||
|
||||
// [0,1] 深度 → 视图空间距离(正值,近小远大)。
|
||||
// 透视:d = 0.5·ndc + 0.5 重映射后,z = n·f / (f − d·(f−n))
|
||||
//(n → d=0,f → d=1)。正交:深度本身线性,z = n + d·(f−n)
|
||||
float linearizeDepth(float d, float n, float f, float ortho) {
|
||||
float persp = (n * f) / (f - d * (f - n));
|
||||
float orthoL = n + d * (f - n);
|
||||
return mix(persp, orthoL, ortho);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 cur = texture(curImage, fragUV);
|
||||
vec2 ts = 1.0 / vec2(textureSize(curImage, 0));
|
||||
|
||||
// 每像素 velocity 直接取自 velocity 缓冲(几何体与天空各自写入),
|
||||
// 不做 dilation:像素中心属谁就用谁的速度。min-depth dilation 会把
|
||||
// 前景(快物体)速度串给轮廓旁的背景像素 → 背景重投影到错误位置 →
|
||||
// 轮廓周围持续闪烁,故移除。
|
||||
vec2 vel = texture(velocityImage, fragUV).xy;
|
||||
float dCenter = texture(depthImage, fragUV).r; // 中心像素当前深度
|
||||
vec2 histUV = fragUV - vel;
|
||||
|
||||
// 运动自适应历史权重:静止 → 高权重(充分累积抗锯齿、稳定锐利);
|
||||
// 运动/遮挡 → 降权重(0.4),快速收敛,消除拖影与移动闪烁。
|
||||
// 固定高权重是"移动闪烁"的根源:新暴露区域要很多帧才收敛(UE/Unity 同款)
|
||||
float speed = clamp(length(vel) * 100.0, 0.0, 1.0);
|
||||
float alpha = mix(pc.alpha, 0.4, speed);
|
||||
|
||||
vec3 result = cur.rgb;
|
||||
|
||||
// 历史采样:越出画面(相机快速平移)直接用当前帧,不累积
|
||||
if (all(greaterThanEqual(histUV, vec2(0.0))) &&
|
||||
all(lessThanEqual(histUV, vec2(1.0)))) {
|
||||
// ---- 去遮挡检测:深度一致性 ----
|
||||
// 比较当前像素的视图距离与重投影点上的上一帧视图距离。差异显著
|
||||
// 说明该像素上一帧被另一表面占据(刚露出/刚被遮挡)→ 历史无效,
|
||||
// alpha 归零(新暴露像素直接吃当前帧,下一帧起重新累积)。
|
||||
// 阈值取相对量:与绝对距离无关,近/远场景同一套参数
|
||||
float zCur = linearizeDepth(dCenter, pc.zNear, pc.zFar, pc.ortho);
|
||||
float zHist = linearizeDepth(texture(historyDepthImage, histUV).r,
|
||||
pc.zNear, pc.zFar, pc.ortho);
|
||||
float relDiff = abs(zCur - zHist) / max(zCur, 1e-4);
|
||||
alpha *= 1.0 - smoothstep(0.05, 0.15, relDiff);
|
||||
|
||||
vec3 hist = texture(historyImage, histUV).rgb;
|
||||
|
||||
// 邻域统计(3×3,YCoCg):均值 m1、均方 m2、极值盒 [mn, mx]
|
||||
vec2 ts = 1.0 / vec2(textureSize(curImage, 0));
|
||||
vec3 m1 = vec3(0.0);
|
||||
vec3 m2 = vec3(0.0);
|
||||
vec3 mn = vec3(1e9);
|
||||
@@ -75,7 +113,7 @@ void main() {
|
||||
vec3 hi = min(m1 + kGamma * sigma, mx);
|
||||
|
||||
vec3 clamped = YCoCgToRgb(clamp(RgbToYCoCg(hist), lo, hi));
|
||||
result = mix(cur.rgb, clamped, pc.alpha);
|
||||
result = mix(cur.rgb, clamped, alpha);
|
||||
}
|
||||
|
||||
outColor = vec4(result, cur.a);
|
||||
|
||||
Reference in New Issue
Block a user