Compare commits

..
6 Commits
Author SHA1 Message Date
atdunbg 84cf547704 资产浏览器可视化改造 + 材质缩略图 GPU 烘焙 + 纹理上传批式化
- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、AssetKind 分类、
  EditorColors 统一配色、AssetItemView 抽出条目绘制;HierarchyPanel/
  MaterialEditorPanel 同步调整
- 材质缩略图改走 GPU 回读烘焙:MaterialPreviewPass 新增 bake pass
  (SDR atlas → host staging copyImageToBuffer)、Renderer 提供
  Request/PollMaterialPreviewBake(无 waitIdle,依赖 per-slot fence),
  Editor 每帧排空后注入 ThumbnailCache——脱离预览 tile 池,消除"滚出视野
  即失效/池耗尽"两问题
- 缩略图失效:BakedIdentity(serial, editVersion) 与当前材质身份比对,
  换贴图槽(重载新 serial)/调参数(editVersion++)不符即 Invalidate 重烘,
  修"修改材质后资产浏览器缩略图不更新"
- 材质加载闸门:每帧至多 1 个 + 按实测耗时冷却,替代固定 2/帧
- 纹理上传批式化:VulkanAssetUploader 新增路径纹理缓存(跨材质去重,
  同一贴图只解码/上传一次)+ 同批新纹理合并为单 cmd/单 submit/单 fence;
  Texture 抽出 RecordPixelsUpload/CreateDeferred/ReleasePendingStaging,
  析构兜底释放 staging;MaterialAsset 纹理所有权移交 uploader 缓存——
  修大材质目录首次打开卡顿
2026-10-18 20:05:29 +08:00
atdunbg 2df6e57b61 透明分层屏幕空间折射 + 透明排序修正 + 默认清屏色归零
默认 ClearColor 0.1 → 0(策划阶段结束,无用)。

透明分层折射(玻璃见玻璃):透明段原来单个 ForwardTrans pass,折射
采样源 SceneColor 副本只在不透明段后快照一次(不含任何透明体)→ 前
玻璃折射不出后玻璃。改为按绘制单位远→近拆成至多 kTransparentLayers
层,每层绘制前刷新 SceneColor 副本,使层 L 玻璃采样含所有更远透明体。
RenderGraph pass 编译期定序,故层数固定;超出并入末层。深度 storeOp
改 eStore(多层需 Load 承接有效深度内容)。shader 不改。

透明排序修正:排序键由"相机到实例的欧氏距离"改为视图空间深度
(-(view·p).z)——欧氏距离把侧向偏移计入距离,偏侧物体被误判为更远
(正方体右上的甜甜圈在特定视角被排到后方)。并让透明队列逐实例成批
(不合并):透明本就是每层一个绘制单位,合并成多实例 batch 会让排序
只能用批平均深度、批内实例无法各自定序。零额外 draw 代价。
2026-10-18 17:09:42 +08:00
atdunbg f107854236 MeshFactory:程序化几何单一源 + CreatePrimitive 链路
新增 MeshFactory(Cube/Plane/Sphere/Cylinder/Cone/Torus + 切线计算),
Model 程序化几何改走 MeshFactory(收敛为单一几何源,消除重复的顶点
生成),并新增 CreatePrimitive;打通 AssetManager/IAssetUploader/
VulkanAssetUploader 的 CreatePrimitive 链路;编辑器 Procedural 弹窗
下拉 + ResolveProcedural 伪路径反解 + 场景序列化(程序化图元可存取)。
2026-10-18 17:09:25 +08:00
atdunbg 22309d24f0 材质与环境光照:UE 打包 Specular 导入(R=AO/G=Rough/B=Metal 与 glTF MR 同序 → 同一贴图直供 MetallicRoughness + Ao 槽,factors 直用贴图,默认置翻绿位 misc.w);MappedNormal 支持 DirectX 法线翻绿(misc.w,编辑器加 Flip Green 开关);环境遮蔽拆分漫反射与高光(高光遮蔽更狠:室内高光须压暗,此前表面反射天空贴图 = 室内穿帮);ufbx 导入日志补齐 mr/normal/ao/emissive 槽位 2026-10-18 15:23:07 +08:00
atdunbg e7711134fe 物理修复与相机防穿墙:Jolt Trace 接管(缺省 DummyTrace 带 JPH_ASSERT(false),调试版一被调用即断;同文去重防多线程刷屏);MeshShape 三角索引补基址(此前把 materialIndex/userData 当顶点偏移传 → 每个 mesh 三角全指向前一 mesh 顶点区,碰撞形状错位 + AABBTree 随机分割告警);调试绘制巨型网格三角预算(>5 万三角退化为包围盒线框,避免百万级 DrawLine 打到个位数帧率);角色 mEnhancedInternalEdgeRemoval 消网格地面 ghost contact 顶起/抖动;新增 RayCastStatic(只命中非移动层,碎块/动态物不当遮挡)→ ThirdPersonCamera 防穿墙拉近(瞬时拉近、平滑回弹、贴墙最小距离与回退余量可配) 2026-10-18 15:23:01 +08:00
atdunbg 2024772bac TAA 时域速度链路贯通:天空盒补写 velocity(此前恒 0,相机移动时天空/背景重投影错误 → 闪烁);skyPrevVP(上一帧未抖动 proj × 去平移视图)落帧;TAA 加去遮挡深度检测(重投影点历史深度与当前像素比对,新暴露区拒历史,需 near/far + 投影模式落帧);移除 min-depth dilation(把前景速度泄漏给轮廓旁背景像素 → 边界闪);Forward 不透明段 velocity 附件 Clear→Load 承接天空结果;velocity 不入抖动注释订正(抖动进 velocity = 静止画面持续亚纹素重采样发糊) 2026-10-18 15:22:48 +08:00
57 changed files with 3407 additions and 711 deletions
+505
View File
@@ -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
+46
View File
@@ -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
+122
View File
@@ -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
+19
View File
@@ -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)
{
+75
View File
@@ -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
+3 -6
View File
@@ -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());
}
+341
View File
@@ -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;
}
}
}
+94
View File
@@ -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
+178 -257
View File
@@ -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);
}
}
+12 -4
View File
@@ -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
+53 -14
View File
@@ -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) {
+55 -13
View File
@@ -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(
+144 -12
View File
@@ -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();
}
+3
View File
@@ -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 资产(可编辑模式)
+46 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+23 -13
View File
@@ -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
+42
View File
@@ -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 抖动明显,
+16 -3
View File
@@ -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;
+27 -1
View File
@@ -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;
};
+27
View File
@@ -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));
+90 -4
View File
@@ -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;
}
+33 -5
View File
@@ -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;
-2
View File
@@ -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))
{
}
+364
View File
@@ -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
+76 -95
View File
@@ -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")时读
+175 -106
View File
@@ -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)
+1 -1
View File
@@ -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);
+45 -13
View File
@@ -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
+20 -3
View File
@@ -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));
+120 -12
View File
@@ -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);
+79 -38
View File
@@ -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) {
+69 -10
View File
@@ -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 {}; // 未知伪路径
}
+99 -48
View File
@@ -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);
+12 -1
View File
@@ -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;
}
+2 -1
View File
@@ -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() {
+11 -5
View File
@@ -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 脸部
+4 -1
View File
@@ -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);
+7 -4
View File
@@ -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
View File
@@ -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);