Compare commits

..
24 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
atdunbg beba491bcb 阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 vendored ufbx
- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对),
  指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis
  行走基朝向与跨骨架去重、根运动通道
- 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与
  字符冲量判定
- 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、
  不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖
- 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增
  ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target
- 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板
- 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
2026-10-11 22:22:19 +08:00
atdunbg 334819bf97 修复打靶场射击链路:发射源/玩家控制走 primary 相机实体
- TestShoot 发射源从 EditorCamera(视口 UI 状态,PIE 渲染不用它)
  改为绑定实体位姿,挂 primary 相机实体即第一人称枪口;
  仅光标捕捉态开火,防编辑器交互误射
- 新增 PlayerController:FPS 控制(鼠标捕捉/Esc 释放/点视口重捕、
  yaw+pitch 转向、WASD+Space/Ctrl 移动+Shift 加速),驱动 primary
  相机实体 Transform,PIE 所见即玩家所见
- 靶子/碎片玻璃 alpha 0.42/0.45→0.85、自发光 2→3:低不透明度在
  亮背景下靶子几乎隐形(用户误认 HUD 横条为靶子)
2026-10-09 13:13:52 +08:00
atdunbg f15b104e77 60 秒限时打靶场玩法原型:碎裂逻辑提取 FractureLib 共享库(HostView 桥接 protected 底座)+ 玻璃靶 Target(跨脚本参数计分)+ GameDirector(靶阵/倒计时/重开/图元 HUD) 2026-10-09 13:00:18 +08:00
atdunbg 6854d53f11 per-mesh 引用:MeshRendererComponent.meshIndex(-1=整模型)贯穿渲染收集/物理 Mesh 碰撞(自动跟随)/场景序列化(缺省键向前兼容)/Inspector mesh 下拉 2026-10-09 12:38:05 +08:00
atdunbg 505217ff7e Spawn/Instantiate 选型约定落进基类与注册宏文档(判定问题+三条纪律;注册=Blueprintable 表态,UE 同题靠约定治理) 2026-10-09 12:31:33 +08:00
atdunbg f14aba4238 修自发光回归:glTF 导入端写 emissiveFactor(EMISSIVE_COLOR,贴图无 factor 时设白);Emissive 兜底纹理黑→白(黑兜底会把 factor/strength 一起乘没) 2026-10-09 00:12:35 +08:00
atdunbg 6447b5c0fa 材质表面参数接通:新增 emissiveFactor(无 emissive 贴图时的发光入口,glTF 语义)入 UBO/YAML/缓存键/两 shader;Transmission/Thickness 未勾 Transparent 时禁用编辑(不透明段 shader 不读) 2026-10-09 00:04:55 +08:00
atdunbg 235482733c 修渲染两处:透明段剔除背面(闭合体正反面乱序 over 混合=深度错乱,单面玻璃 UE/Unity 语义);prefilter mip0 128→256(roughness 0 镜面反射块状马赛克) 2026-10-08 23:50:11 +08:00
atdunbg c43adc339d 资产系统侧线第一波:ResolveProcedural 提为公共静态(键格式唯一定义点);Inspector 程序化模型入口;修 Prefab hover 误导性 not loaded;Content Browser 模型拖拽 → Outliner 创建实体 2026-10-08 23:36:08 +08:00
atdunbg 2cf8d6e076 修复碎片模型卸载的在飞 GPU 引用(vkDestroyBuffer in-use):门面批量卸载先 WaitDeviceIdle 再销毁(与 ReloadMaterialAsset 同一先例) 2026-10-08 23:23:15 +08:00
atdunbg affa1a7a56 碎片形状随机化:唯一代键替代命中点量化缓存键(每次命中重新随机 Voronoi),配套碎片模型生命周期回收(销毁次帧卸载 + OnDestroy 兜底) 2026-10-08 23:18:35 +08:00
atdunbg d4d37b4b28 修复 PIE 快照丢相机组件(序列化写出键 Attached ≠ 读侧 Camera)与子弹同帧多接触重复特效(Bullet 命中防重入) 2026-10-08 23:06:18 +08:00
atdunbg 21da38db92 WallFracture 碎片几何升级:网格方块 → 2D Voronoi 不规则凸棱柱(命中点周边种子加密,MeshCollider 凸包,运行时 ParsedModel 程序化资产) 2026-10-08 22:58:40 +08:00
atdunbg f20578bc4b TestShoot 弹头半径 0.08→0.12 反隧穿:离散检测下薄板碰撞窗口须大于每步位移(无 CCD) 2026-10-08 22:48:20 +08:00
atdunbg bca50976f3 WallFracture 碎裂手感修正:爆炸飞散 → 玻璃式碎裂(初速距离衰减 + 法线剥落外推 + 网格切细至 8×6) 2026-10-08 22:43:36 +08:00
atdunbg d2701a3d9b 新增程序化立方体与可碎墙验收:Model::CreateCube 五层链路 + WallFracture 碎片刚体群(命中碎裂/径向飞散/寿命清理) 2026-10-08 22:28:49 +08:00
atdunbg ed7cb417bd 新增 Prefab 系统:实体子树存取 + 编辑器 Save As Prefab/拖入实例化 + 脚本 Instantiate 2026-10-08 22:01:26 +08:00
atdunbg b89f431896 新增脚本实体底座与碰撞事件:Spawn/Destroy + OnCollisionEnter/Exit + TestShoot 验收脚本
- ScriptBehavior::Spawn/Destroy:Scene::CreateEntity / 延迟销毁队列直达(实现下沉 ScriptRuntime.cpp,避 Application 不完整类型)
- ScriptRuntime::DispatchCollisions:帧首把 PhysicsWorld::GetEvents 派发到涉事实例(Persisted 跳过、禁用不收;回调内 spawn/destroy 安全)
- TestShoot:左键 spawn 动态球刚体子弹,OnCollisionEnter 命中点生成 ImpactFx 特效实体并自毁,超时防泄漏
2026-10-08 21:45:10 +08:00
102 changed files with 48593 additions and 1568 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
+38
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"
@@ -19,6 +20,8 @@
#include "panels/ControllerEditorPanel.h"
#include "panels/ViewportPanel.h"
#include "panels/StatsPanel.h"
#include "panels/LogPanel.h"
#include "panels/GamepadPanel.h"
#include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
@@ -60,6 +63,8 @@ void Editor::Init(const EditorContext& ctx)
m_Inspector = std::make_unique<InspectorPanel>();
m_Viewport = std::make_unique<ViewportPanel>();
m_Stats = std::make_unique<StatsPanel>();
m_Log = std::make_unique<LogPanel>();
m_Gamepad = std::make_unique<GamepadPanel>();
m_AssetBrowser = std::make_unique<AssetBrowserPanel>();
m_MaterialEditor = std::make_unique<MaterialEditorPanel>();
m_Ctx.materialEditor = m_MaterialEditor.get();
@@ -163,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)
{
@@ -219,6 +242,8 @@ void Editor::Draw()
}
}
if (m_ShowStats) m_Stats->Draw(m_Ctx);
if (m_ShowLog) m_Log->Draw(m_Ctx);
m_Gamepad->Draw(m_Ctx); // Tools 弹窗(内部 m_Open 门控)
if (m_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx);
if (m_ShowMaterialEditor) m_MaterialEditor->Draw(m_Ctx);
m_ControllerEditor->Draw(m_Ctx); // 面板内部 m_Open 门控
@@ -236,6 +261,11 @@ bool Editor::BlockingKeyboard() const
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
// PIE 运行态:键盘全给游戏——编辑热键/Gizmo 在运行态本就让位
//(ViewportPanel !playing 分支),不再因视口失焦拦截。否则点
// Play 后焦点停在工具栏,WASD 整局全死(玩家输入无响应的根因)
if (IsPlaying()) return false;
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
@@ -298,11 +328,18 @@ void Editor::DrawMenuBar()
ImGui::MenuItem("Details", nullptr, &m_ShowInspector);
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
ImGui::MenuItem("Log", nullptr, &m_ShowLog);
ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser);
ImGui::MenuItem("Material Editor", nullptr, &m_ShowMaterialEditor);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Tools"))
{
ImGui::MenuItem("Gamepad Test", nullptr, &m_Gamepad->m_Open);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
@@ -428,6 +465,7 @@ void Editor::DrawDockSpace()
ImGui::DockBuilderDockWindow("Outliner", dockLeft);
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab
ImGui::DockBuilderDockWindow("Log", dockBottom); // Content Browser 旁 tab
ImGui::DockBuilderDockWindow("Content Browser", dockBottom);
ImGui::DockBuilderDockWindow("Details", dockRight);
ImGui::DockBuilderDockWindow("Material Editor", dockRight); // Details 旁 tab
+5
View File
@@ -29,6 +29,8 @@ class HierarchyPanel;
class InspectorPanel;
class ViewportPanel;
class StatsPanel;
class LogPanel;
class GamepadPanel;
class AssetBrowserPanel;
class MaterialEditorPanel;
class ControllerEditorPanel;
@@ -126,6 +128,8 @@ private:
std::unique_ptr<InspectorPanel> m_Inspector;
std::unique_ptr<ViewportPanel> m_Viewport;
std::unique_ptr<StatsPanel> m_Stats;
std::unique_ptr<LogPanel> m_Log;
std::unique_ptr<GamepadPanel> m_Gamepad;
std::unique_ptr<AssetBrowserPanel> m_AssetBrowser;
std::unique_ptr<MaterialEditorPanel> m_MaterialEditor;
std::unique_ptr<ControllerEditorPanel> m_ControllerEditor;
@@ -137,6 +141,7 @@ private:
bool m_ShowInspector = true;
bool m_ShowViewport = true;
bool m_ShowStats = true;
bool m_ShowLog = true;
bool m_ShowAssetBrowser = true;
bool m_ShowMaterialEditor = true;
+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
+168 -202
View File
@@ -1,21 +1,27 @@
//
// AssetBrowserPanel.cpp:项目资产浏览器实现。
//
// 条目视觉/拖拽/tooltip 单元住 AssetItemView(列表行与网格瓦片共用同一
// 套类别色 + 缩略图);本文件只负责:根切换 / 过滤 / 目录树 / 内容区
// 铺排 / 选中态 / 双击分派(打开或实例化)。
//
#include "AssetBrowserPanel.h"
#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>
#include <imgui.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <filesystem>
#include <utility>
#include <vector>
namespace {
@@ -24,71 +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"};
"Materials", "Controllers",
"Prefabs"};
// 资产类别:按扩展名分派(过滤用)
enum class Kind { Model, Texture, Scene, Material, AnimClip, 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;
}
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 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]";
default: return "[ ]";
}
}
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
std::string ToAssetPath(const std::filesystem::path& p)
{
@@ -96,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)
@@ -121,7 +126,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
}
ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f);
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 6);
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7);
ImGui::SameLine();
ImGui::SetNextItemWidth(140.0f);
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
@@ -143,7 +148,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
m_CurrentDir = root;
}
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)----
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)+ 视图切换 ----
if (ImGui::Button("Root"))
{
m_CurrentDir = root;
@@ -153,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);
@@ -185,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();
@@ -218,34 +242,25 @@ void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
}
}
void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
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;
@@ -258,155 +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)
{
const std::string name = item.path.filename().string();
const std::string assetPath = ToAssetPath(item.path);
ImGui::PushID(name.c_str());
// 双击打开延迟到循环外:Immediate GUI 语义——绘制循环内不改世界状态
//(prefab 实例化会改场景,边遍历边打开会让后续条目的项状态错乱)
bool pendingOpen = false;
std::filesystem::path pendingOpenPath;
EditorAsset::AssetKind pendingOpenKind = EditorAsset::AssetKind::Other;
ImGui::TextUnformatted(KindTag(item.kind));
ImGui::SameLine();
ImGui::Selectable(name.c_str());
// .material 拖拽源:拖到 Details 材质槽球上指派(kMaterialDragPayload)
if (item.kind == Kind::Material && ImGui::BeginDragDropSource())
if (m_ViewMode == ViewMode::List)
{
ImGui::SetDragDropPayload(kMaterialDragPayload,
assetPath.c_str(), assetPath.size() + 1);
ImGui::TextUnformatted(name.c_str());
ImGui::EndDragDropSource();
for (const auto& d : dirs)
{
if (EditorAssetView::DrawFolderRow(d))
{
m_CurrentDir = d;
m_SelectedPath.clear();
}
// .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();
}
// 双击:.material 打开 Material Editor;.animctrl 打开
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
// File 菜单
if (ImGui::IsItemHovered() &&
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
for (const auto& [path, kind] : files)
{
if (item.kind == Kind::Material && ctx.materialEditor)
const std::string assetPath = ToAssetPath(path);
const auto res = EditorAssetView::DrawRow(
ctx, path, assetPath == m_SelectedPath);
if (res.clicked)
{
ctx.materialEditor->OpenAsset(assetPath);
m_SelectedPath = assetPath;
}
else if (item.kind == Kind::AnimClip && ctx.controllerEditor)
if (res.doubleClicked)
{
ctx.controllerEditor->Open(assetPath);
pendingOpen = true;
pendingOpenPath = path;
pendingOpenKind = kind;
}
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);
}
}
// hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态
if (ImGui::IsItemHovered())
{
ImGui::BeginTooltip();
ImGui::TextUnformatted(name.c_str());
ImGui::Separator();
// 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip
// 数组越界读
static const char* kKindNames[] = {"Model", "Texture/Environment",
"Scene", "Material",
"Controller", "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)
{
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(item.path, ec);
if (!ec)
// ---- 网格视图:固定列宽表格铺排(列数按可用宽度自适应)----
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)
{
// 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);
cols = 1;
}
// 加载状态(只查询不触发加载;model/HDR 进过缓存才显示 loaded)
if (ctx.assets)
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
{
const char* state = "not loaded";
if (item.kind == Kind::Model &&
ctx.assets->IsModelLoaded(assetPath))
for (int i = 0; i < cols; ++i)
{
state = "loaded";
ImGui::TableSetupColumn("##c",
ImGuiTableColumnFlags_WidthFixed,
cellW);
}
else if (item.kind == Kind::Texture &&
ctx.assets->IsEnvironmentLoaded(assetPath))
for (const auto& d : dirs)
{
state = "loaded";
}
ImGui::Text("state: %s", state);
// 已加载的 model 展开资产数据概览(meshes=0 即纯动画
// 容器;未加载不显示——查询不触发加载)
if (item.kind == Kind::Model)
ImGui::TableNextColumn();
const std::string assetPath = ToAssetPath(d);
if (EditorAssetView::DrawFolderTile(
d, tile, assetPath == m_SelectedPath))
{
if (const Engine::Model* m =
ctx.assets->GetModelByPath(assetPath);
m != nullptr)
m_CurrentDir = d;
m_SelectedPath.clear();
}
}
for (const auto& [path, kind] : files)
{
ImGui::Text("meshes: %u, bones: %u, clips: %u",
static_cast<unsigned>(m->GetMeshes().size()),
m->GetBoneCount(),
static_cast<unsigned>(m->GetClips().size()));
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::TextDisabled("%s", assetPath.c_str());
ImGui::EndTooltip();
}
ImGui::PopID();
if (pendingOpen)
{
DispatchOpen(ctx, pendingOpenPath, pendingOpenKind);
}
}
+20 -3
View File
@@ -18,25 +18,42 @@
#include <filesystem>
#include <string>
// Prefab 资产拖拽 payload(Content Browser 拖 .prefab → Outliner 落点
// 实例化;字符串 = 资产相对路径)。对齐 kMaterialDragPayload 模式:
// 常量住源面板头
inline constexpr const char* kPrefabDragPayload = "PREFAB_PATH";
// 模型资产拖拽 payload(Content Browser 拖 .glb/.fbx/... → Outliner
// 落点创建带 MeshRenderer 的实体;字符串 = 资产相对路径)
inline constexpr const char* kModelDragPayload = "MODEL_PATH";
class AssetBrowserPanel {
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,
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
+199
View File
@@ -0,0 +1,199 @@
//
// GamepadPanel.cpp:手柄输入可视化回显(极简手柄视图)。
//
// 一块画布 = 手柄俯视图:顶部扳机条 + 肩键,左上左摇杆、左下十字键、
// 右上 ABXY(Xbox 语义色)、右下右摇杆,中央 Back/Guide/Start 小键。
// 按下即亮蓝(面键亮语义色),无数值无卡片——位置即实体,一眼对上
// 手里的手柄。数据源 = Window::PollGamepad(GLFW 原始轴值/按键)。
//
#include "GamepadPanel.h"
#include <Engine/Core/Window.h>
#include <imgui.h>
#include <cmath>
namespace {
// GLFW 标准布局下标
enum PadBtn {
kA = 0, kB = 1, kX = 2, kY = 3,
kLB = 4, kRB = 5, kBack = 6, kStart = 7, kGuide = 8,
kLS = 9, kRS = 10, kUp = 11, kRight = 12, kDown = 13, kLeft = 14,
};
constexpr ImU32 kLit = IM_COL32(90, 170, 250, 255); // 按下亮蓝
constexpr ImU32 kFill = IM_COL32(52, 52, 60, 255); // 暗态填充
constexpr ImU32 kEdge = IM_COL32(104, 104, 114, 255); // 暗态描边
constexpr ImU32 kTxt = IM_COL32(215, 215, 220, 255); // 标签文字
// 块状键(肩键 / Back / Start)
void Block(ImDrawList* dl, const ImVec2& c, float w, float h,
const char* label, bool pressed)
{
const ImVec2 mn(c.x - w * 0.5f, c.y - h * 0.5f);
const ImVec2 mx(c.x + w * 0.5f, c.y + h * 0.5f);
dl->AddRectFilled(mn, mx, pressed ? kLit : kFill, 5.0f);
dl->AddRect(mn, mx, pressed ? IM_COL32(160, 210, 255, 255) : kEdge,
5.0f);
const ImVec2 ts = ImGui::CalcTextSize(label);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), kTxt, label);
}
// 圆键(Guide)
void Circle(ImDrawList* dl, const ImVec2& c, float r, const char* label,
bool pressed)
{
dl->AddCircleFilled(c, r, pressed ? kLit : kFill);
dl->AddCircle(c, r, pressed ? IM_COL32(160, 210, 255, 255) : kEdge, 0,
1.5f);
const ImVec2 ts = ImGui::CalcTextSize(label);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), kTxt, label);
}
// Xbox 语义色面键
void Face(ImDrawList* dl, const ImVec2& c, float r, char label,
bool pressed)
{
ImU32 col;
switch (label)
{
case 'A': col = IM_COL32(32, 150, 40, 255); break;
case 'B': col = IM_COL32(200, 45, 45, 255); break;
case 'X': col = IM_COL32(50, 110, 220, 255); break;
default: col = IM_COL32(225, 185, 40, 255); break;
}
dl->AddCircleFilled(c, r, pressed ? col : IM_COL32(col & 0xFF,
(col >> 8) & 0xFF,
(col >> 16) & 0xFF,
60));
dl->AddCircle(c, r, pressed ? col : kEdge, 0, 2.0f);
const ImVec2 ts = ImGui::CalcTextSize(&label, &label + 1);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f),
pressed ? IM_COL32(255, 255, 255, 255) : kTxt, &label);
}
// 摇杆画布(底座 + 死区圈 + 轴点)
void Stick(ImDrawList* dl, const ImVec2& c, float r, float ax, float ay,
float deadzone)
{
dl->AddCircleFilled(c, r, IM_COL32(32, 32, 37, 255));
dl->AddCircle(c, r, kEdge, 0, 1.5f);
dl->AddCircle(c, r * deadzone, IM_COL32(90, 140, 210, 110), 0, 1.2f);
dl->AddLine(ImVec2(c.x - r * 0.93f, c.y), ImVec2(c.x + r * 0.93f, c.y),
IM_COL32(50, 50, 58, 255), 1.0f);
dl->AddLine(ImVec2(c.x, c.y - r * 0.93f), ImVec2(c.x, c.y + r * 0.93f),
IM_COL32(50, 50, 58, 255), 1.0f);
const float mag = std::sqrt(ax * ax + ay * ay);
const float cl = (mag > 1.0f) ? 1.0f / mag : 1.0f;
const ImVec2 pt(c.x + ax * cl * (r - 8.0f),
c.y + ay * cl * (r - 8.0f));
if (mag > deadzone)
{
dl->AddCircleFilled(pt, 10.0f, IM_COL32(90, 170, 250, 70));
}
dl->AddCircleFilled(pt, 7.0f,
mag > deadzone ? kLit
: IM_COL32(140, 140, 148, 255));
}
// 十字键(真十字形,按下臂亮)
void Dpad(ImDrawList* dl, const ImVec2& c, float arm, float w,
const bool* b)
{
const ImVec2 l0(c.x - arm, c.y - w * 0.5f);
const ImVec2 l1(c.x, c.y + w * 0.5f);
const ImVec2 r0(c.x, c.y - w * 0.5f);
const ImVec2 r1(c.x + arm, c.y + w * 0.5f);
const ImVec2 u0(c.x - w * 0.5f, c.y - arm);
const ImVec2 u1(c.x + w * 0.5f, c.y);
const ImVec2 d0(c.x - w * 0.5f, c.y);
const ImVec2 d1(c.x + w * 0.5f, c.y + arm);
dl->AddRectFilled(l0, l1, b[kLeft] ? kLit : kFill, 4.0f);
dl->AddRectFilled(r0, r1, b[kRight] ? kLit : kFill, 4.0f);
dl->AddRectFilled(u0, u1, b[kUp] ? kLit : kFill, 4.0f);
dl->AddRectFilled(d0, d1, b[kDown] ? kLit : kFill, 4.0f);
dl->AddRect(l0, l1, kEdge, 4.0f);
dl->AddRect(r0, r1, kEdge, 4.0f);
dl->AddRect(u0, u1, kEdge, 4.0f);
dl->AddRect(d0, d1, kEdge, 4.0f);
}
// 线性扳机条(GLFW -1..1 → 0..1 填充)
void Trigger(ImDrawList* dl, const ImVec2& mn, const ImVec2& mx,
float raw)
{
dl->AddRectFilled(mn, mx, IM_COL32(32, 32, 37, 255), 4.0f);
const float t = (raw + 1.0f) * 0.5f;
if (t > 0.02f)
{
dl->AddRectFilled(mn, ImVec2(mn.x + (mx.x - mn.x) * t, mx.y),
IM_COL32(235, 120, 55, 255), 4.0f);
}
dl->AddRect(mn, mx, kEdge, 4.0f);
}
} // namespace
void GamepadPanel::Draw(const EditorContext& ctx)
{
if (!m_Open)
{
return;
}
if (!ImGui::Begin("Gamepad", &m_Open))
{
ImGui::End();
return;
}
Engine::GamepadState gp;
const bool connected =
ctx.app && ctx.app->GetWindow().PollGamepad(gp);
if (!connected)
{
ImGui::TextColored(ImVec4(0.60f, 0.60f, 0.60f, 1.0f),
"○ Not connected (JOYSTICK_1)");
ImGui::End();
return;
}
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f), "● Connected");
ImGui::Spacing();
// ---- 手柄画布(固定尺寸,元素 = 手柄真实位置;无背景板)----
constexpr float kW = 440.0f, kH = 262.0f;
constexpr float kDead = 0.15f; // = InputMap kRadialDeadZone
ImGui::BeginChild("pad", ImVec2(kW, kH), false);
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImVec2 o = ImGui::GetWindowPos();
auto C = [&](float x, float y) { return ImVec2(o.x + x, o.y + y); };
// 顶部:线性扳机条 + 肩键
Trigger(dl, C(52, 8), C(122, 20), gp.axes[4]); // LT
Trigger(dl, C(kW - 122, 8), C(kW - 52, 20), gp.axes[5]); // RT
Block(dl, C(78, 40), 76.0f, 20.0f, "LB", gp.buttons[kLB]);
Block(dl, C(kW - 78, 40), 76.0f, 20.0f, "RB", gp.buttons[kRB]);
// 左上:左摇杆|左下:十字键(向右错开——真实手柄摇杆与十字不对齐)
Stick(dl, C(76, 118), 46.0f, gp.axes[0], gp.axes[1], kDead);
Dpad(dl, C(128, 210), 36.0f, 24.0f, gp.buttons);
// 中央:Back / Guide / Start
Block(dl, C(184, 74), 40.0f, 16.0f, "Back", gp.buttons[kBack]);
Circle(dl, C(220, 58), 13.0f, "G", gp.buttons[kGuide]);
Block(dl, C(256, 74), 40.0f, 16.0f, "Start", gp.buttons[kStart]);
// 右上:ABXY 菱形|右下:右摇杆(对称错开,向中心收)
const ImVec2 fc = C(360, 118);
Face(dl, ImVec2(fc.x, fc.y - 24), 17.0f, 'Y', gp.buttons[kY]);
Face(dl, ImVec2(fc.x - 24, fc.y), 17.0f, 'X', gp.buttons[kX]);
Face(dl, ImVec2(fc.x + 24, fc.y), 17.0f, 'B', gp.buttons[kB]);
Face(dl, ImVec2(fc.x, fc.y + 24), 17.0f, 'A', gp.buttons[kA]);
Stick(dl, C(312, 210), 46.0f, gp.axes[2], gp.axes[3], kDead);
ImGui::EndChild();
ImGui::End();
}
+21
View File
@@ -0,0 +1,21 @@
//
// GamepadPanel:手柄输入可视化回显(Tools 菜单弹出窗,非 dock 常驻)。
//
// 三块:摇杆画布(原始轴值 + 引擎死区参考圈)/ 按键网格(按下高亮)/
// 动作层回显(引擎死区+钳制后脚本真正拿到的 Move/Look/Jump)。
// 测试用途:不接手柄时显示未连接状态。
//
#ifndef HELLOVULKAN_GAMEPADPANEL_H
#define HELLOVULKAN_GAMEPADPANEL_H
#include "EditorContext.h"
class GamepadPanel {
public:
void Draw(const EditorContext& ctx);
bool m_Open = false; // Tools 菜单开关(窗口 X 也能关)
};
#endif //HELLOVULKAN_GAMEPADPANEL_H
+162 -15
View File
@@ -7,12 +7,17 @@
//
#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"
#include "Engine/Core/Log.h"
#include <imgui.h>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>
@@ -33,6 +38,77 @@ namespace {
return "(unnamed)";
}
// Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、
// 保留授权位姿;返回根实体(失败 entt::null)
entt::entity InstantiatePrefabDrop(const EditorContext& ctx,
const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
return Engine::Serialization::SceneSerializer::InstantiatePrefab(
*ctx.scene, *ctx.assets, path);
}
// 模型拖入落点:创建带 MeshRenderer 的单实体(名字 = 文件名
// stem,默认位姿;模型句柄 = LoadModel 磁盘路径)。拖到节点下 =
// SetParent keepWorld;空白区 = 场景根。加载失败不建空实体
entt::entity SpawnModelDrop(const EditorContext& ctx, const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
const auto model = ctx.assets->LoadModel(path);
if (!model.Valid()) {
return entt::null;
}
std::string name =
std::filesystem::path(path).stem().string();
if (name.empty()) {
name = "model entity";
}
auto e = ctx.scene->CreateEntity(name);
e.AddComponent<Engine::ECS::MeshRendererComponent>().model = model;
ENGINE_INFO("Hierarchy: model entity '{}' created from '{}'",
name, path);
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)
{
for (char& c : name) {
if (c == '/' || c == '\\' || c == ':' || c == '*' ||
c == '?' || c == '"' || c == '<' || c == '>' ||
c == '|') {
c = '_';
}
}
if (name.empty() || name == "(unnamed)") {
name = "prefab";
}
return name;
}
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected
void DrawEntityNode(const EditorContext& ctx, entt::entity e,
entt::entity& selected)
@@ -64,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;
@@ -78,7 +157,9 @@ namespace {
ImGui::EndDragDropSource();
}
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变)----
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
// 授权世界位姿)----
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kHierPayload)) {
@@ -91,6 +172,26 @@ namespace {
ctx.scene->SetParent(dragged, e);
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root =
InstantiatePrefabDrop(ctx,
static_cast<const char*>(
pl->Data));
root != entt::null) {
ctx.scene->SetParent(root, e); // keepWorld 默认 true
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
ctx.scene->SetParent(me, e); // keepWorld 默认 true
selected = me;
}
}
ImGui::EndDragDropTarget();
}
@@ -102,6 +203,19 @@ namespace {
ctx.scene->SetParent(child, e);
selected = child;
}
if (ImGui::MenuItem("Save As Prefab")) {
// 固定目录 + 实体名派生文件名(无文件对话框库,与场景
// File 菜单同策略;重名覆盖,改名走文件系统)
const std::string path =
"assets/prefabs/" +
PrefabFileName(DisplayName(reg, e)) + ".prefab";
std::error_code ec;
std::filesystem::create_directories("assets/prefabs", ec);
if (Serialization::SceneSerializer::SerializePrefab(
*ctx.scene, *ctx.assets, e, path)) {
ENGINE_INFO("Hierarchy: prefab saved to '{}'", path);
}
}
if (ctx.scene->GetParent(e) != entt::null &&
ImGui::MenuItem("Detach to Root")) {
ctx.scene->SetParent(e, entt::null);
@@ -117,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()) {
// ---- 组件徽标(逐组件着色,与行内同级显示)----
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");
}
"M=Mesh A=Animation C=Camera L=Light P=Physics "
"S=Script K=Sky");
}
if (opened) {
@@ -194,7 +324,8 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
DrawEntityNode(ctx, e, selected);
});
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层)----
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
// Prefab 拖入 = 实例化到场景根(授权位姿)----
ImVec2 rem = ImGui::GetContentRegionAvail();
if (rem.y > 4.0f) {
ImGui::InvisibleButton("##hier_drop_root", rem);
@@ -206,6 +337,22 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
ctx.scene->SetParent(static_cast<entt::entity>(bits),
entt::null);
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root = InstantiatePrefabDrop(
ctx, static_cast<const char*>(pl->Data));
root != entt::null) {
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
selected = me;
}
}
ImGui::EndDragDropTarget();
}
}
+178 -7
View File
@@ -18,6 +18,7 @@
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
#include "Engine/Scripting/ScriptRuntime.h" // 参数编辑 → RefreshParams
#include "Engine/Serialization/SceneSerializer.h" // ResolveProcedural(程序化入口)
#include "Engine/Core/Log.h" // Create Override 失败告警
#include <imgui.h>
@@ -197,6 +198,12 @@ namespace
for (u32 i = 0; i < meshes.size(); ++i)
{
// 「mesh」下拉的过滤:选了具体子 mesh 就只列该槽位(与渲染/
// 物理的子引用语义一致);All meshes(-1)= 全列
if (mr.meshIndex >= 0 && static_cast<int>(i) != mr.meshIndex)
{
continue;
}
ImGui::PushID(static_cast<int>(i));
const std::string meshName =
@@ -670,6 +677,111 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
mr->materialOverrides.clear();
}
ImGui::Spacing();
// 程序化模型入口:伪资产不在磁盘(下拉框列不到),由
// 弹窗组键生成 → ResolveProcedural(与场景反解同一
// 键格式唯一定义点;确定性键必然命中缓存)
if (ImGui::SmallButton("Procedural..."))
{
ImGui::OpenPopup("##proc_model");
}
if (ImGui::BeginPopup("##proc_model"))
{
// 形状集合与 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);
ImGui::SameLine();
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("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)
{
ImGui::SetNextItemWidth(180.0f);
ImGui::SliderFloat("alpha", &alpha, 0.0f, 1.0f,
"%.2f");
}
if (ImGui::Button("Create & Assign"))
{
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[128];
switch (shape)
{
case 2:
std::snprintf(
key, sizeof(key),
"procedural://transparent-plane/%.3g/%d,%d,%d,%d",
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(
*ctx.assets, key);
ref.Valid())
{
mr->model = ref;
mr->materialOverrides.clear();
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
}
// 无效句柄不进 GetModel(缓存对无效/悬空句柄 FATAL,是 bug 信号)
if (mr->model.Valid())
@@ -677,6 +789,40 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
{
// 单 mesh 子引用下拉(-1 = 整模型全部):名字用
// GetMeshMaterialInfos(材质名优先,空回退 Slot N)
{
const auto& infos = model->GetMeshMaterialInfos();
const int sel = mr->meshIndex + 1; // 0 = All
std::string preview = "All meshes";
if (sel > 0 &&
static_cast<size_t>(sel - 1) < infos.size()) {
preview = infos[static_cast<size_t>(sel) - 1]
.name.empty()
? "Slot " + std::to_string(sel - 1)
: infos[static_cast<size_t>(sel) - 1].name;
}
if (ImGui::BeginCombo("mesh", preview.c_str()))
{
if (ImGui::Selectable("All meshes", sel <= 0)) {
mr->meshIndex = -1;
}
for (size_t i = 0; i < infos.size(); ++i)
{
const std::string label =
(infos[i].name.empty()
? "Slot " + std::to_string(i)
: infos[i].name) +
"##" + std::to_string(i);
const bool cur =
mr->meshIndex == static_cast<int>(i);
if (ImGui::Selectable(label.c_str(), cur)) {
mr->meshIndex = static_cast<int>(i);
}
}
ImGui::EndCombo();
}
}
const auto& clips = model->GetClips();
if (model->GetMeshes().empty())
{
@@ -1175,6 +1321,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::DragFloat("Radius", &cc->radius, 0.02f, 0.05f, 5.0f);
ImGui::DragFloat("Half Height", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
ImGui::DragFloat("Mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
// 形状附加偏移:叠加在脚底约定之上(模型 pivot 对齐用)
ImGui::DragFloat3("Shape Offset", &cc->shapeOffset.x, 0.02f, -5.0f, 5.0f);
// ---- Movement:扫掠运动学参数 ----
ImGui::SeparatorText("Movement");
ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
@@ -1805,11 +1953,31 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// 警告跳过)----
ImGui::SeparatorText("Clips");
const auto& files = AssetModelFiles();
std::vector<const char*> opts;
opts.push_back("<attach animation file...>");
// 显示真名(AssetDisplayName:文件 stem,导入资产库的
// 哈希 "asset" 读同目录 pathname 取原始名);重名者补
// 父目录区分——不再把哈希路径直接摆给用户(= 显示成 id)
std::vector<std::string> labels;
labels.reserve(files.size());
for (const auto& f : files)
{
opts.push_back(f.c_str());
std::string label = Engine::Model::AssetDisplayName(f);
if (std::find(labels.begin(), labels.end(), label) !=
labels.end())
{
label += " (" +
std::filesystem::path(f)
.parent_path()
.filename()
.string() +
")";
}
labels.push_back(std::move(label));
}
std::vector<const char*> opts;
opts.push_back("<attach animation file...>");
for (const auto& l : labels)
{
opts.push_back(l.c_str());
}
int sel = 0;
ImGui::SetNextItemWidth(-1.0f);
@@ -1829,7 +1997,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
}
// attach 遥测(骨骼匹配结果,面板下方显示)
s_LastAttachFile =
std::filesystem::path(path).filename().string();
Engine::Model::AssetDisplayName(path);
s_LastAttachMatched =
res.channelsMatched + res.channelsRetargeted;
s_LastAttachRetargeted = res.channelsRetargeted;
@@ -1842,9 +2010,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
for (size_t i = 0; i < anim->attachedAnimPaths.size(); ++i)
{
const auto& p = anim->attachedAnimPaths[i];
ImGui::BulletText("%s", std::filesystem::path(p)
.filename()
.string()
ImGui::BulletText("%s",
Engine::Model::AssetDisplayName(p)
.c_str());
ImGui::SameLine();
ImGui::PushID(static_cast<int>(i));
@@ -1951,6 +2118,10 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// CrossFade 与 Direct 的 crossfade 共用 fade)
ImGui::SliderFloat("Speed", &anim->speed, -2.0f, 2.0f);
ImGui::SliderFloat("Fade", &anim->fadeDuration, 0.0f, 1.0f);
// 根运动(Unity Animator 同名开关):关 = 原地播放,
// 位移归脚本/物理驱动(CharacterController 角色用)
ImGui::Checkbox("Apply Root Motion",
&anim->applyRootMotion);
// 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的
// 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼
// 命名不同源被跳过)
+152
View File
@@ -0,0 +1,152 @@
//
// LogPanel.cpp:运行时日志面板实现。
//
// 数据源 = Engine::Debug::LogRing(spdlog ring sink 落的整行)。
// 级别着色(trace 灰 / debug 青 / info 浅白 / warn 黄 / error 红 /
// fatal 亮红带背景条)+ 级别过滤 + 文本过滤 + Auto-scroll(滚动条
// 贴底才自动拉,上翻看历史不被拽回)。
//
#include "LogPanel.h"
#include <Engine/Core/LogRing.h>
#include <imgui.h>
#include <cctype>
#include <cstring>
#include <string>
void LogPanel::Draw(const EditorContext& ctx)
{
(void)ctx;
if (!ImGui::Begin("Log"))
{
ImGui::End();
return;
}
// ---- 工具栏:清空 / 自动滚动 / 文本过滤 / 级别过滤 ----
if (ImGui::Button("Clear"))
{
Engine::Debug::LogRing().Clear();
}
ImGui::SameLine();
ImGui::Checkbox("Auto-scroll", &m_AutoScroll);
ImGui::SameLine();
ImGui::SetNextItemWidth(160.0f);
ImGui::InputTextWithHint("##filter", "filter...", m_Filter, sizeof(m_Filter));
ImGui::SameLine();
ImGui::Checkbox("Trace", &m_ShowTrace);
ImGui::SameLine();
ImGui::Checkbox("Info", &m_ShowInfo);
ImGui::SameLine();
ImGui::Checkbox("Warn", &m_ShowWarn);
ImGui::SameLine();
ImGui::Checkbox("Error", &m_ShowError);
ImGui::Separator();
// ---- 版本感知:有新日志且滚动条贴底 → 拉底(上翻历史不拽回)----
const Engine::u64 ver = Engine::Debug::LogRing().Version();
bool scrollToBottom = false;
if (ver != m_LastVersion)
{
m_LastVersion = ver;
if (m_AutoScroll)
{
scrollToBottom = ImGui::GetScrollY() >=
ImGui::GetScrollMaxY() - 24.0f;
}
}
if (ImGui::BeginChild("loglist", {0.0f, 0.0f}, false,
ImGuiWindowFlags_HorizontalScrollbar))
{
const auto entries = Engine::Debug::LogRing().Snapshot();
for (const auto& e : entries)
{
using lv = spdlog::level::level_enum;
bool show = false;
switch (e.level)
{
case lv::trace:
case lv::debug: show = m_ShowTrace; break;
case lv::info: show = m_ShowInfo; break;
case lv::warn: show = m_ShowWarn; break;
case lv::err:
case lv::critical: show = m_ShowError; break;
default: show = m_ShowInfo; break;
}
if (!show) continue;
if (m_Filter[0] != '\0')
{
// 简单子串过滤(大小写不敏感)
std::string lower;
lower.reserve(e.msg.size());
for (char c : e.msg)
{
lower.push_back(static_cast<char>(
std::tolower(static_cast<unsigned char>(c))));
}
std::string needle;
needle.reserve(strlen(m_Filter));
for (const char* p = m_Filter; *p; ++p)
{
needle.push_back(static_cast<char>(
std::tolower(static_cast<unsigned char>(*p))));
}
if (lower.find(needle) == std::string::npos)
{
continue;
}
}
// ---- 渲染:级别徽标(恒彩色,固定列)+ 消息本体 ----
// 消息色只有 warn/error/fatal 染色(info 白是惯例),颜色
// 辨识主要靠徽标列——UE Output Log 的 Verbosity 列同位
ImVec4 tagCol, msgCol;
const char* tag = "INFO";
bool bg = false;
switch (e.level)
{
case lv::trace:
tag = "TRACE"; tagCol = {0.55f, 0.55f, 0.55f, 1.0f};
msgCol = {0.55f, 0.55f, 0.55f, 1.0f}; break;
case lv::debug:
tag = "DEBUG"; tagCol = {0.40f, 0.80f, 0.90f, 1.0f};
msgCol = {0.70f, 0.70f, 0.70f, 1.0f}; break;
case lv::info:
tag = "INFO"; tagCol = {0.45f, 0.72f, 0.95f, 1.0f};
msgCol = {0.90f, 0.90f, 0.90f, 1.0f}; break;
case lv::warn:
tag = "WARN"; tagCol = {1.00f, 0.78f, 0.20f, 1.0f};
msgCol = {1.00f, 0.78f, 0.20f, 1.0f}; break;
case lv::err:
tag = "ERROR"; tagCol = {1.00f, 0.35f, 0.35f, 1.0f};
msgCol = {1.00f, 0.35f, 0.35f, 1.0f}; break;
default:
tag = "FATAL"; tagCol = {1.00f, 0.15f, 0.15f, 1.0f};
msgCol = {1.00f, 0.15f, 0.15f, 1.0f};
bg = true; break;
}
if (bg)
{
// fatal:整行暗红背景条提级(先画背景再画文本)
const ImVec2 p0 = ImGui::GetCursorScreenPos();
const ImVec2 p1(p0.x + ImGui::GetContentRegionAvail().x,
p0.y + ImGui::GetTextLineHeight());
ImGui::GetWindowDrawList()->AddRectFilled(
p0, p1, IM_COL32(90, 10, 10, 160));
}
ImGui::TextColored(tagCol, "%s", tag);
ImGui::SameLine(40.0f); // 徽标固定列宽,消息对齐成两列
ImGui::TextColored(msgCol, "%s", e.msg.c_str());
}
if (scrollToBottom)
{
ImGui::SetScrollHereY(1.0f);
}
}
ImGui::EndChild();
ImGui::End();
}
+26
View File
@@ -0,0 +1,26 @@
//
// LogPanel:运行时日志面板(环形缓冲渲染,级别着色 + 过滤)。
//
#ifndef HELLOVULKAN_LOGPANEL_H
#define HELLOVULKAN_LOGPANEL_H
#include <Engine/Core/Types.h>
#include "EditorContext.h"
class LogPanel {
public:
void Draw(const EditorContext& ctx);
private:
bool m_ShowTrace = false;
bool m_ShowInfo = true;
bool m_ShowWarn = true;
bool m_ShowError = true;
bool m_AutoScroll = true;
char m_Filter[128] = "";
Engine::u64 m_LastVersion = 0;
};
#endif //HELLOVULKAN_LOGPANEL_H
+186 -8
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)
@@ -173,13 +296,53 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
EditParam(
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f));
EditParam(ImGui::ColorEdit4("Base Color", &p.baseColor.x));
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等):rgb 叠加色,
// a 强度(0 = 关闭);需 albedo 贴图自带 alpha 遮罩才有局部效果
EditParam(ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x));
EditParam(ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f));
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
1.0f));
// 发光 = emissiveFactor × emissive 贴图 × Strength(Emissive 槽
// 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
EditParam(ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x));
EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f,
0.0f, 100.0f));
// transmission/thickness 只有不透明段不读——透明 shader 专属
//(未勾 Transparent 时禁用编辑,防"拖了没反应"的困惑)
ImGui::BeginDisabled(!trans);
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
1.0f));
EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f,
10.0f));
ImGui::EndDisabled();
if (!trans)
{
ImGui::TextDisabled("Transmission/Thickness need Transparent");
}
// UV 镜像(材质级开关,作用于全部纹理槽):源资产 UV 朝向与引擎
// 上左原点约定相反时贴图整体翻转,这里一键纠正。写入 misc.y/z,
// 旧 .material 无该分量 → 缺省 0 = 关闭
{
bool flipU = p.misc.y > 0.5f;
const bool c = ImGui::Checkbox("Flip U (left-right)", &flipU);
if (c) { p.misc.y = flipU ? 1.0f : 0.0f; }
EditParam(c);
}
ImGui::SameLine();
{
bool flipV = p.misc.z > 0.5f;
const bool c = ImGui::Checkbox("Flip V (up-down)", &flipV);
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");
@@ -188,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())
{
@@ -205,13 +375,13 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
}
if (ImGui::BeginPopup("tex_pick"))
{
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
{
if (ImGui::Selectable(f.c_str()))
// 弹窗内只做选择;pending 统一在循环外落盘(避免 popup 内
// 立即重建导致 asset 指针悬空)
std::string chosen;
if (TexturePickerContent(ctx, chosen))
{
pendingSlot = static_cast<int>(s);
pendingPath = f;
}
pendingPath = chosen;
}
ImGui::EndPopup();
}
@@ -302,10 +472,18 @@ void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f);
ImGui::ColorEdit4("Base Color", &p.baseColor.x);
ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x);
ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f);
ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x);
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
ImGui::BeginDisabled(!trans);
ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f);
ImGui::EndDisabled();
if (!trans)
{
ImGui::TextDisabled("Transmission/Thickness need Transparent");
}
ImGui::SeparatorText("Textures");
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
"Albedo", "Normal", "MetallicRoughness",
+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 资产(可编辑模式)
+21
View File
@@ -9,6 +9,7 @@
#include "Engine/ECS/Components.h"
#include "Engine/Physics/PhysicsWorld.h"
#include "Engine/Core/Time.h"
#include "Engine/Core/Stats.h"
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
@@ -208,6 +209,26 @@ void StatsPanel::Draw(const EditorContext& ctx)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
// ---- GPU 资源台账(Stats 记账中枢的唯一消费者)----
const auto st = Engine::Stats::Get().TakeSnapshot();
auto MB = [](u64 bytes)
{ return static_cast<double>(bytes) / (1024.0 * 1024.0); };
ImGui::Separator();
ImGui::Text("textures: %llu (%.2f MB)",
static_cast<unsigned long long>(st.textureCount),
MB(st.textureBytes));
ImGui::Text("meshes: %llu (%.2f MB)",
static_cast<unsigned long long>(st.meshCount),
MB(st.meshBytes));
ImGui::Text("buffers: %llu (%.2f MB)",
static_cast<unsigned long long>(st.bufferCount),
MB(st.bufferBytes));
ImGui::Text("allocator (VMA 1:1): %.2f MB",
MB(st.memoryUsedBytes));
ImGui::Text("churn: %.2f MB / %.2f MB (total %.2f / %.2f)",
MB(st.frameAllocation), MB(st.frameDeallocation),
MB(st.totalAllocation), MB(st.totalDeallocation));
}
ImGui::End();
+8
View File
@@ -234,6 +234,14 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
// PIE 运行态:输入归游戏(脚本读 GLFW 轮询),Gizmo/编辑器
// 热键全部让位(UE Play 语义:Scene view 编辑操作冻结)
const bool playing = ctx.playState && *ctx.playState;
// PIE 启动瞬间:视口抢焦点(UE Play 语义——游戏视角接管)。
// 不抢的话焦点停在工具栏 Play 按钮上,编辑器相机飞行等
// 持焦操作下一局全失灵
static bool s_wasPlaying = false;
if (playing && !s_wasPlaying) {
ImGui::SetWindowFocus();
}
s_wasPlaying = playing;
if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
// Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位,
-5
View File
@@ -20,7 +20,6 @@
#include <Engine/Core/Time.h>
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
#include <Engine/Core/Log.h>
#include <Engine/Core/Stats.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Serialization/SceneSerializer.h>
@@ -146,10 +145,6 @@ namespace
SetImGuiEnabled(!GetImGuiEnabled());
}
#endif
if (event.key == static_cast<int>(Input::Key::F1))
{
STATS.PrintComparison();
}
}
+203
View File
@@ -0,0 +1,203 @@
//
// Follower.cpp:跟随者行为(NPC 自动跟随主角,UE AIController 的简化位)。
//
// 挂在每个 NPC 实体上(各自 CharacterController + Animation)。主角由
// TargetName 指定(默认 "player",按 NameComponent 名称匹配——引擎无实体
// 引用参数类型,名称匹配是标准做法)。逻辑分四层,与 WASDMove 同一套手感:
// - 距离闸门(迟滞):拉开到 StopDistance+ResumeGap 才起步,靠近到
// StopDistance 即停——"到附近就停止跟随",边界不抖动
// - 速度分档(迟滞):距离超过 RunDistance 改跑,回落到 0.85× 才退回
// 走——"距离越大走/跑越快"
// - 动量整形:真实速度逐帧逼近期望速度(起步渐快、到点刹停),帧率无关
// - 动画按真实速度分档(idle/walk/run),迟滞防抖
// 移动驱动走 PhysicsWorld 的 CharacterController 门面(水平喂意图速度,
// Y 归物理重力);无 CC 时回退直接平移。多个跟随者用 Offset 错开站位。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Physics/PhysicsWorld.h>
#include "ScriptLog.h"
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
using namespace Engine;
namespace {
class Follower final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "Follower"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("TargetName", &m_TargetName, std::string("player"));
// 相对目标的站位偏移(世界系;多个跟随者错开,防叠一起)
pb.Field("Offset", &m_Offset, glm::vec3{0.0f});
pb.Field("WalkSpeed", &m_WalkSpeed, 3.0f); // 走路 m/s
pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s
pb.Field("StopDistance", &m_StopDistance, 2.5f); // 到点停(米)
pb.Field("ResumeGap", &m_ResumeGap, 1.5f); // 再起步的距离余量
pb.Field("RunDistance", &m_RunDistance, 10.0f); // 超过即改跑(米)
pb.Field("TurnResponse", &m_TurnResponse, 10.0f);// 转向阻尼
pb.Field("AccelResponse", &m_AccelResponse, 6.0f); // 加速响应
pb.Field("StopResponse", &m_StopResponse, 12.0f); // 制动响应
// clip 按名引用(Unity anim.Play("Walk") 语义)
pb.Field("IdleClip", &m_IdleClip, std::string("idle"));
pb.Field("WalkClip", &m_WalkClip, std::string("walk"));
pb.Field("RunClip", &m_RunClip, std::string("run"));
}
void OnStart() override {
if (ResolveTarget()) {
SLOG_INFO("[Follower] 跟随目标 '{}' 就绪", m_TargetName);
} else {
SLOG_WARN("[Follower] 未找到目标 '{}'(按 NameComponent 匹配)",
m_TargetName);
}
}
void OnUpdate(float dt) override {
auto& reg = App().GetScene().GetRegistry();
// 目标失效(被销毁/未就绪)→ 重解析;仍无则静止待命
if (m_Target == entt::null || !reg.valid(m_Target)) {
m_Target = entt::null;
if (!ResolveTarget()) {
return;
}
}
const glm::vec3 tp =
reg.get<Engine::ECS::TransformComponent>(m_Target)
.GetPosition();
const glm::vec3 self = Transform().GetPosition();
glm::vec3 delta = (tp + m_Offset) - self;
delta.y = 0.0f; // 只取水平面(角色不俯仰)
const float dist = glm::length(delta);
// ---- 距离闸门(迟滞):到点停、拉开距离再起步 ----
if (!m_Moving) {
if (dist > m_StopDistance + m_ResumeGap) {
m_Moving = true;
}
} else if (dist <= m_StopDistance) {
m_Moving = false;
}
// ---- 速度分档(迟滞):远则跑、近则走 ----
glm::vec3 desired{0.0f};
if (m_Moving && dist > 1e-4f) {
if (!m_Sprinting && dist >= m_RunDistance) {
m_Sprinting = true;
} else if (m_Sprinting && dist <= m_RunDistance * 0.85f) {
m_Sprinting = false;
}
const float speed = m_Sprinting ? m_RunSpeed : m_WalkSpeed;
desired = (delta / dist) * speed;
}
// ---- 动量整形:真实速度逐帧逼近期望(起步渐快/到点刹停)----
const float response =
glm::length(desired) > 0.01f ? m_AccelResponse : m_StopResponse;
m_PlanarVel +=
(desired - m_PlanarVel) * (1.0f - std::exp(-response * dt));
if (glm::length(desired - m_PlanarVel) < 0.02f) {
m_PlanarVel = desired; // 贴齐,防低速蠕动
}
// ---- 驱动:角色控制器喂水平速度(Y 归物理);无则直接平移 ----
auto& phy = App().GetPhysics();
const bool hasCC =
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
nullptr;
if (hasCC) {
const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits());
phy.SetCharacterVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
} else {
Transform().SetPosition(self + m_PlanarVel * dt);
}
// ---- 朝向:面向真实速度方向(模型默认 +Z),低速锁定 ----
if (glm::length(m_PlanarVel) > 0.3f) {
const float targetYaw =
std::atan2(m_PlanarVel.x, m_PlanarVel.z);
const glm::vec3 modelFwd =
Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f);
const float curYaw = std::atan2(modelFwd.x, modelFwd.z);
const float dyaw = std::atan2(std::sin(targetYaw - curYaw),
std::cos(targetYaw - curYaw));
const float k = 1.0f - std::exp(-m_TurnResponse * dt);
Transform().SetRotationQuat(
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
}
// ---- 动画:按真实速度分档(迟滞防临界抖)----
const float planarSpeed = glm::length(m_PlanarVel);
std::string target;
if (planarSpeed < 0.15f) {
target = m_IdleClip;
} else if (m_CurClip == m_RunClip) {
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
: m_WalkClip;
} else {
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
: m_WalkClip;
}
if (target != m_CurClip) {
if (auto* anim =
TryGetComponent<Engine::ECS::AnimationComponent>();
anim != nullptr && anim->Play(target, 0.35f)) {
m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试)
}
}
}
private:
// 按 NameComponent 名称解析目标(跳过自身);失败返回 false
bool ResolveTarget() {
auto& reg = App().GetScene().GetRegistry();
auto view = reg.view<Engine::ECS::NameComponent>();
for (auto e : view) {
if (static_cast<u32>(entt::to_integral(e)) == EntityBits()) {
continue;
}
if (view.get<Engine::ECS::NameComponent>(e).name ==
m_TargetName) {
m_Target = e;
return true;
}
}
return false;
}
std::string m_TargetName = "player";
glm::vec3 m_Offset{0.0f};
float m_WalkSpeed = 3.0f;
float m_RunSpeed = 6.0f;
float m_StopDistance = 2.5f;
float m_ResumeGap = 1.5f;
float m_RunDistance = 10.0f;
float m_TurnResponse = 10.0f;
float m_AccelResponse = 6.0f;
float m_StopResponse = 12.0f;
std::string m_IdleClip = "idle";
std::string m_WalkClip = "walk";
std::string m_RunClip = "run";
entt::entity m_Target = entt::null; // 运行时状态(非参数)
bool m_Moving = false; // 距离闸门状态(迟滞)
bool m_Sprinting = false; // 速度档状态(迟滞)
glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度
std::string m_CurClip = "idle"; // 当前 clip(边沿检测基准)
};
ENGINE_REGISTER_SCRIPT(Follower)
} // namespace
+704
View File
@@ -0,0 +1,704 @@
//
// FractureLib.h:共享碎裂库(自 WallFracture.cpp 提取,行为逐行保持)。
//
// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D
// Voronoi 分割:
// 碎裂瞬间在面上撒种子点(命中点周边加密 → 孔洞周围碎片小、
// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面
// 交集得到不规则凸多边形 cell,沿面厚挤出成凸棱柱——碎片在碎裂
// 前一刻完美拼合原面,碎开后沿裂纹分离。碰撞形状不走 Box,直接
// 用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 CPU
// 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连碰撞体
// 都是真的,落地翻滚/堆积接触自然。
//
// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处
// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛
// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在
// 墙脚"的观感。
//
// 用法(宿主 = 挂在可碎实体上的脚本):
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
// DeclareParams 直绑 burst_.p 各字段(pb.Field("Count",
// &burst_.p.count, ...))——参数必须与碎裂逻辑读的是同一对象,
// 绑到独立副本会让 Count/Lifetime 等全部失效
// HostView HV() { // 桥接(脚本自身成员函数内可访问
// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API)
// [this](auto& n) { return Spawn(n); },
// [this](auto e) { Destroy(e); }};
// }
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
// OnUpdate → burst_.Tick(HV(), dt)
// OnDestroy → burst_.Shutdown(HV())
// 外观:默认继承宿主(墙)的生效材质(底色/贴图/透明),碎块与墙面
// 一致;要独立外观(玻璃靶 = 半透明 + 自发光)Fire 前置
// burst_.look.inheritFromHost = false 再填 baseColor/emissive
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
//
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
// 尺寸烘焙、碎片实体复用宿主 world pos/rot(scale 恒 1,防
// ConvexHull 顶点二次缩放)——宿主须无旋转摆放。
//
#ifndef HELLOVULKAN_FRACTURE_LIB_H
#define HELLOVULKAN_FRACTURE_LIB_H
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Types.h>
#include <Engine/Assets/AssetManager.h> // LoadModel(门面只前向声明)
#include <Engine/Renderer/Model.h> // ParsedModel/ParsedMesh(程序化碎片几何)
#include <Engine/Renderer/Vertex.h> // Vertex 聚合布局
#include <Engine/Physics/PhysicsWorld.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <cmath>
#include <functional>
#include <random>
namespace Engine::Fracture {
// bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验)
inline ECS::Entity EntityFromBits(u32 bits) {
return static_cast<ECS::Entity>(bits);
}
// 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记
// 需要任意实体的同款换算)
inline u32 BitsOf(const ECS::EntityHandle& e) {
return static_cast<u32>(
entt::to_integral(static_cast<entt::entity>(e)));
}
inline f32 Rand(f32 lo, f32 hi) {
thread_local std::mt19937 rng{std::random_device{}()};
return std::uniform_real_distribution<f32>(lo, hi)(rng);
}
// 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次
// 命中都重新随机生成形状(命中点量化键会让同位置重复命中
// 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了)
inline u32& GenSeq() {
static u32 s = 0;
return s;
}
// ---- 宿主桥接(脚本侧现填;库内只走公共 API)----
// spawn/destroy 走脚本成员(内部才能访问 ScriptBehavior 的
// protected 底座),app/实体走数据。std::function 捕 this 无
// 分配(小对象优化),随调用栈存亡,无悬垂面
struct HostView {
Application& app;
u32 entityBits = 0;
ECS::Entity entity() const { return EntityFromBits(entityBits); }
std::function<ECS::EntityHandle(const std::string&)> spawn;
std::function<void(ECS::Entity)> destroy;
};
// ---- 2D Voronoi(碎裂裂纹)----
//
// cell = 相对其它种子的垂直平分线半平面交集,初始域 = 墙面矩形。
// Sutherland–Hodgman 凸多边形裁剪:保留靠近 p 的一侧
//(dot(a,x) <= b,a = normalize(q-p),b = dot(a,p)+|q-p|/2)。
// 输入多边形须 CCW(初始矩形即 CCW,裁剪保序)。
inline std::vector<glm::vec2> ClipByBisector(std::vector<glm::vec2> poly,
const glm::vec2& p,
const glm::vec2& q) {
const glm::vec2 d = q - p;
const f32 len = glm::length(d);
if (len < 1e-6f) return poly;
const glm::vec2 a = d / len;
const f32 b = glm::dot(a, p) + 0.5f * len;
std::vector<glm::vec2> out;
out.reserve(poly.size() + 1);
for (size_t i = 0; i < poly.size(); ++i) {
const glm::vec2& c = poly[i];
const glm::vec2& n = poly[(i + 1) % poly.size()];
const f32 dc = glm::dot(a, c) - b;
const f32 dn = glm::dot(a, n) - b;
if (dc <= 0.0f) out.push_back(c);
if ((dc < 0.0f) != (dn < 0.0f)) { // 边跨越裁剪线 → 求交点
const f32 t = dc / (dc - dn);
out.push_back(c + t * (n - c));
}
}
return out;
}
// 种子 p 的 Voronoi cell(域 = [-hw,hw]×[-hh,hh] 的墙面矩形)
inline std::vector<glm::vec2> VoronoiCell(const glm::vec2& p,
const std::vector<glm::vec2>& seeds,
f32 hw, f32 hh) {
std::vector<glm::vec2> poly = {
{-hw, -hh}, {hw, -hh}, {hw, hh}, {-hw, hh}};
for (const glm::vec2& q : seeds) {
if (glm::dot(q - p, q - p) < 1e-10f) continue; // 自身
poly = ClipByBisector(std::move(poly), p, q);
if (poly.size() < 3) return {}; // 退化(最小间距保护,罕见)
}
return poly;
}
// cell(CCW 凸多边形,墙局部 XY)沿 z ∈ [-hz, +hz] 挤出成棱柱:
// 顶/底面 fan 三角化 + 侧面逐边 quad,per-face 法线。返回 mesh
// 数据;cpuPositions 单独回传(ConvexHull 物理数据源)。
struct PrismMesh {
std::vector<Engine::Vertex> vertices;
std::vector<u32> indices;
std::vector<glm::vec3> cpuPositions;
};
// hw/hh = 宿主半尺寸:顶/底面 UV 由 cell 的墙局部坐标归一化到 [0,1]
//(墙面矩形 UV 约定,与立方体面 UV 对齐)——继承墙材质贴图时,碎片
// 贴图与墙面连续,不会各碎片自成一格
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz,
f32 hw, f32 hh) {
PrismMesh m;
const size_t n = cell.size();
const size_t topBase = 0;
const size_t botBase = n;
m.vertices.reserve(n * 2);
const f32 invW = hw > 1e-6f ? 0.5f / hw : 0.0f;
const f32 invH = hh > 1e-6f ? 0.5f / hh : 0.0f;
const auto uvOf = [&](const glm::vec2& p) {
return glm::vec2{p.x * invW + 0.5f, p.y * invH + 0.5f};
};
for (const glm::vec2& p : cell) {
const Engine::Vertex v{
{p.x, p.y, hz}, {0.0f, 0.0f, 1.0f},
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
for (const glm::vec2& p : cell) {
const Engine::Vertex v{
{p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f},
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
// top fan(CCW 从 +Z 看朝外)+ bottom fan(反向)
for (size_t i = 1; i + 1 < n; ++i) {
m.indices.push_back(topBase);
m.indices.push_back(topBase + i);
m.indices.push_back(topBase + i + 1);
m.indices.push_back(botBase);
m.indices.push_back(botBase + i + 1);
m.indices.push_back(botBase + i);
}
// 侧面:(i, N+i, N+i+1, i+1)——外法线 = CCW 边的右侧法向
for (size_t i = 0; i < n; ++i) {
const size_t j = (i + 1) % n;
const glm::vec2 e = cell[j] - cell[i];
const f32 el = glm::length(e);
const glm::vec3 sn = el > 1e-6f
? glm::vec3{e.y / el, -e.x / el, 0.0f}
: glm::vec3{0.0f, -1.0f, 0.0f};
const size_t b = m.vertices.size();
const Engine::Vertex q0{
{cell[i].x, cell[i].y, hz}, sn,
{0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q1{
{cell[i].x, cell[i].y, -hz}, sn,
{0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q2{
{cell[j].x, cell[j].y, -hz}, sn,
{1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q3{
{cell[j].x, cell[j].y, hz}, sn,
{1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(q0);
m.vertices.push_back(q1);
m.vertices.push_back(q2);
m.vertices.push_back(q3);
m.indices.insert(m.indices.end(),
{u32(b), u32(b + 1), u32(b + 2),
u32(b), u32(b + 2), u32(b + 3)});
}
// 物理凸包数据源(BuildMeshShape Dynamic 分支逐顶点收集)
m.cpuPositions.reserve(m.vertices.size());
for (const auto& v : m.vertices) {
m.cpuPositions.push_back(v.position);
}
return m;
}
// ---- 程序化纯色还原(引擎零改动:颜色只存在于伪路径里)----
// 程序化 Cube/Plane 把颜色烘进 1x1 sRGB 纹理,MaterialParams 留白、
// 内建材质快照也没有颜色字段,故从伪路径尾段 "r,g,b[,a]" 还原线性
// 底色——与 GPU 采样 sRGB 纹理的硬件线性化同式,直乘 baseColor 才等价
inline f32 SrgbByteToLinear(u8 c) {
const f32 s = static_cast<f32>(c) / 255.0f;
return s <= 0.04045f ? s / 12.92f
: std::pow((s + 0.055f) / 1.055f, 2.4f);
}
// "procedural://cube/0.5/140,92,72" → 取最后一个 '/' 之后的纯数字
// 逗号段;段数 < 3 或含非数字(如 halfSize 段)判失败
inline bool ParseProceduralColor(const std::string& path, glm::vec4& out) {
if (path.rfind("procedural://", 0) != 0) return false;
const size_t slash = path.find_last_of('/');
if (slash == std::string::npos) return false;
const std::string tail = path.substr(slash + 1);
int c[3] = {0, 0, 0};
int n = 0;
size_t pos = 0;
while (n < 3 && pos < tail.size()) {
const size_t comma = tail.find(',', pos);
const size_t end =
(comma == std::string::npos) ? tail.size() : comma;
if (end == pos) return false;
int v = 0;
for (size_t i = pos; i < end; ++i) {
const char ch = tail[i];
if (ch < '0' || ch > '9') return false;
v = v * 10 + (ch - '0');
}
c[n++] = v;
if (comma == std::string::npos) break;
pos = comma + 1;
}
if (n < 3) return false;
out = glm::vec4{SrgbByteToLinear(static_cast<u8>(c[0])),
SrgbByteToLinear(static_cast<u8>(c[1])),
SrgbByteToLinear(static_cast<u8>(c[2])), 1.0f};
return true;
}
// ---- 参数(原 WallFracture 全量参数,字段类型随 Inspector 控件)----
struct Params {
i32 count = 30; // Voronoi 种子数 = 碎片数
f32 seedRadius = 0.9f; // 加密半径 m
f32 minSeedDist = 0.12f; // 种子最小间距 m
f32 burstSpeed = 1.2f; // 近点径向初速 m/s
f32 falloff = 1.0f; // 距离衰减率 1/m
f32 normalPop = 0.4f; // 法线外推 m/s
f32 upBias = 0.3f; // 上抛偏置 m/s
f32 lifetime = 6.0f; // 碎片寿命(秒)
i32 maxFragments = 128; // 活碎片上限
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
};
// ---- 碎片外观 ----
// 默认 inheritFromHost = true:碎块继承宿主(墙)的"生效材质"——
// 底色/贴图/透明/自发光全套沿用,碎掉后与墙面目视一致。宿主材质
// 取实体级覆盖(.material 资产)优先、模型内建材质兜底,与
// SceneRenderer 收集期的 resolve 同序
// 需要独立外观(玻璃靶 = 半透明 + 自发光等)时置 false,改用下面
// 的显式字段(Fire 前设置)
struct Appearance {
bool inheritFromHost = true; // true = 材质沿用宿主,忽略下列字段
glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f,
72.0f / 255.0f, 1.0f};
bool transparent = false; // 透明绘制队列(玻璃碎片)
glm::vec3 emissive{0.0f}; // 自发光颜色(黑 = 不发光)
f32 emissiveStrength = 1.0f;
};
// ---- 碎裂执行体(宿主脚本持有;一次 Fire 管一整批碎片)----
class Burst {
public:
Params p; // 宿主 DeclareParams 直绑
Appearance look; // 碎片外观(Fire 前设置)
// 触发流程一次做完:一次性保护 → 对方校验(活 Dynamic +
// 速度阈值)→ 藏宿主 → 切分 → spawn 碎片群。未触发返回
// false(速度不够/重复触发),宿主无需再写判定
bool TryFire(const HostView& host,
u32 other, const glm::vec3& point,
const glm::vec3& normal) {
if (m_Fired) return false;
// 对方须"够快"且是可动的撞击来源,二选一:
// - 动态刚体(子弹/抛体):用实时线速度
// - 角色(CharacterVirtual,非刚体、无 RigidBodyComponent):
// 用"扫掠前"速度——扫掠中朝墙面的分量会被 Jolt 抵消,
// 实时速度往往已归零,会漏判(见
// PhysicsWorld::GetCharacterImpactVelocity)
// 速度低于阈值(静止贴靠/轻碰)则忽略
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity otherE = EntityFromBits(other);
if (!registry.valid(otherE)) return false;
f32 speed = 0.0f;
if (const auto* otherRb =
registry.try_get<Engine::ECS::RigidBodyComponent>(
otherE)) {
if (otherRb->type !=
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
return false; // 静态/运动学体不算撞击
}
speed = glm::length(
host.app.GetPhysics().GetLinearVelocity(other));
} else if (registry.all_of<
Engine::ECS::CharacterControllerComponent>(
otherE)) {
// 玩家/角色撞击:角色碰撞事件由 PhysicsWorld 合成
// (CharacterVirtual 不进 ContactListener)
speed = glm::length(
host.app.GetPhysics().GetCharacterImpactVelocity(other));
} else {
return false; // 无物理来源(纯 Transform 实体)
}
if (speed < p.minImpactSpeed) return false;
m_Fired = true;
Fire(host, point, normal);
return true;
}
// 每帧簿记(宿主 OnUpdate 直调;未 Fire 过无簿记可理)
void Tick(const HostView& host, f32 dt) {
if (!m_Fired) return;
ApplyPendingUnload(host);
ApplyPendingVelocities(host);
AgeAndCleanup(host, dt);
}
// 宿主销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡
// 处理,独占碎片模型统一批量回收(模型不被快照恢复的场景
// 引用——唯一代键;批量接口内部 GPU 停飞后再销毁)
void Shutdown(const HostView& host) {
if (!m_Fired) return; // 从未 Fire:无碎片无模型
std::vector<std::string> paths;
paths.reserve(m_Fragments.size() + m_PendingUnload.size());
for (const Fragment& f : m_Fragments) {
if (!f.modelPath.empty()) paths.push_back(f.modelPath);
}
m_Fragments.clear();
paths.insert(paths.end(),
std::make_move_iterator(m_PendingUnload.begin()),
std::make_move_iterator(m_PendingUnload.end()));
m_PendingUnload.clear();
if (!paths.empty()) {
host.app.GetAssets().UnloadModelsByPaths(paths);
}
}
bool Fired() const { return m_Fired; }
private:
// ---- 触发碎裂:藏宿主 + Voronoi 切分 + spawn 碎片群 ----
void Fire(const HostView& host,
const glm::vec3& hitPoint, const glm::vec3& hitNormal) {
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity self = host.entity();
// 藏宿主:渲染/拾取跳过 + body 对账销毁(组件级 active,
// 与 Unity Renderer.enabled + Rigidbody 摘除同语义)
if (auto* mr =
registry.try_get<Engine::ECS::MeshRendererComponent>(
self)) {
mr->active = false;
}
if (auto* rb =
registry.try_get<Engine::ECS::RigidBodyComponent>(
self)) {
rb->active = false;
}
// 宿主几何:碎片网格顶点按宿主世界尺寸烘焙(面 = XY 平面,
// 厚度沿 Z;宿主须无旋转摆放,见头注释占位局限)
const auto& wall = registry.get<Engine::ECS::TransformComponent>(
self);
const glm::vec3 wallPos = wall.GetWorldPosition();
const glm::vec3 wallSize = wall.GetWorldScale();
const f32 hw = 0.5f * wallSize.x;
const f32 hh = 0.5f * wallSize.y;
const f32 hz = 0.5f * wallSize.z;
// ---- 碎片外观:解析宿主(墙)的生效材质 ----
// 与 SceneRenderer 收集期 resolve 同序:实体级覆盖
//(.material 资产,按 mesh 下标查表)优先,模型内建材质兜底
//(MeshMaterialInfo 为权威只读描述)。不继承则碎片只剩默认
// 砖色,与墙面目视不符
Engine::MaterialAssetData effMat;
glm::vec4 effSolid{0.0f}; // 程序化纯色 albedo(a>0 = 有效)
std::string hostOverridePath; // 宿主 .material 覆盖(空 = 内建材质)
bool hasEffMat = false;
if (look.inheritFromHost) {
const auto* mr = registry.try_get<
Engine::ECS::MeshRendererComponent>(self);
const int slot =
(mr && mr->meshIndex >= 0) ? mr->meshIndex : 0;
if (mr) {
const auto oIt =
mr->materialOverrides.find(static_cast<u32>(slot));
if (oIt != mr->materialOverrides.end()) {
if (auto* asset =
host.app.GetAssets().FindMaterialAsset(
oIt->second)) {
effMat = asset->GetData();
hostOverridePath = oIt->second;
hasEffMat = true;
}
}
}
if (!hasEffMat && mr && mr->model.Valid()) {
if (auto* model =
host.app.GetAssets().GetModel(mr->model)) {
const auto& infos = model->GetMeshMaterialInfos();
if (slot < static_cast<int>(infos.size())) {
effMat.texPaths = infos[slot].texPaths;
effMat.params = infos[slot].params;
effMat.transparent = infos[slot].transparent;
hasEffMat = true;
}
}
// 程序化纯色墙:颜色在生成的 1x1 纹理里(快照无颜色
// 字段),改由脚本从模型伪路径还原线性底色
ParseProceduralColor(
host.app.GetAssets().Models().Path(mr->model),
effSolid);
}
}
// 命中点 → 宿主局部(逆旋转 + 平移),XY 即裂纹平面坐标
const glm::quat invRot = glm::conjugate(
wall.GetWorldRotationQuat());
const glm::vec3 localHit = invRot * (hitPoint - wallPos);
const glm::vec2 uvHit{localHit.x, localHit.y};
// ---- 种子:40% 命中点半径内加密 + 60% 全面均匀,最小
// 间距拒绝采样(防碎片退化为细条)----
const u32 count = std::clamp(p.count, 4, 96);
const u32 nearCount = count * 2 / 5;
std::vector<glm::vec2> seeds;
seeds.reserve(count);
const f32 minD2 = p.minSeedDist * p.minSeedDist;
auto tryPush = [&](const glm::vec2& s) {
if (s.x < -hw || s.x > hw || s.y < -hh || s.y > hh) return;
for (const glm::vec2& e : seeds) {
if (glm::dot(e - s, e - s) < minD2) return;
}
seeds.push_back(s);
};
// 命中点周边:均匀盘采样(sqrt(Rand) 面积均匀)
for (u32 i = 0; i < nearCount * 12 && seeds.size() < nearCount;
++i) {
const f32 a = Rand(0.0f, glm::two_pi<f32>());
const f32 r = p.seedRadius * std::sqrt(Rand(0.0f, 1.0f));
tryPush(uvHit + r * glm::vec2{std::cos(a), std::sin(a)});
}
// 全面均匀补足(同款拒绝采样)
for (u32 i = 0; i < count * 12 && seeds.size() < count; ++i) {
tryPush({Rand(-hw, hw), Rand(-hh, hh)});
}
if (seeds.size() < 4) return; // 面太小采样失败:不碎
// ---- 每种子 → cell → 棱柱 mesh → ParsedModel ----
// 唯一代键(见 GenSeq 注释):形状每次随机;模型回收走
// Fragment.modelPath + 待卸载队列(ApplyPendingUnload)
const std::string keyBase =
"procedural://vorfrag/" +
std::to_string(GenSeq()++) + '/';
auto& assets = host.app.GetAssets();
std::vector<Engine::ModelRef> refs;
std::vector<glm::vec3> centroids; // 碎片质心(世界系,初速径向用)
std::vector<std::string> paths; // 模型缓存键(回收用,与 refs 对齐)
refs.reserve(seeds.size());
centroids.reserve(seeds.size());
paths.reserve(seeds.size());
for (size_t s = 0; s < seeds.size(); ++s) {
const std::vector<glm::vec2> cell =
VoronoiCell(seeds[s], seeds, hw, hh);
if (cell.size() < 3) {
refs.push_back({}); // 退化 cell:占位,spawn 跳过
centroids.emplace_back();
paths.emplace_back();
continue;
}
PrismMesh pm = CellToPrism(cell, hz, hw, hh);
glm::vec2 c2{0.0f};
for (const glm::vec2& q : cell) c2 += q;
c2 /= static_cast<f32>(cell.size());
centroids.push_back(
wallPos + wall.GetWorldRotationQuat() *
glm::vec3{c2.x, c2.y, 0.0f});
Engine::ParsedModel parsed;
parsed.path = keyBase + std::to_string(s);
Engine::ParsedMesh& mesh =
parsed.meshes.emplace_back();
mesh.vertices = std::move(pm.vertices);
mesh.lods.push_back(std::move(pm.indices));
mesh.cpuPositions = std::move(pm.cpuPositions);
mesh.name = "frag" + std::to_string(s);
if (hasEffMat) {
// 继承墙的生效材质:底色/贴图/UV 变换/透明全套沿用,
// 碎块与墙面目视一致(UV 已在 CellToPrism 归一到墙面)
mesh.params = effMat.params;
mesh.texPaths = effMat.texPaths;
mesh.transparent = effMat.transparent;
// 程序化纯色墙(无贴图路径):用脚本解析出的线性底色当
// baseColor——碎片 albedo 走默认白纹理,直乘即该色;
// 不走这步会退回 MaterialParams 的白色默认值
if (effSolid.a > 0.0f) {
mesh.params.baseColor = effSolid;
}
} else {
// 无宿主材质可继承(或 look.inheritFromHost=false):
// 用显式外观(玻璃靶 = 半透明 + 自发光)
mesh.params.baseColor = look.baseColor;
mesh.transparent = look.transparent;
mesh.params.emissiveFactor =
glm::vec4{look.emissive, 1.0f};
mesh.params.surface.z = look.emissiveStrength;
}
const Engine::ModelRef ref =
assets.LoadModel(parsed.path, std::move(parsed));
refs.push_back(ref);
paths.push_back(parsed.path);
}
// ---- spawn 碎片实体:pos/rot 复用宿主位姿(mesh 已真
// 尺寸,scale 恒 1 防 ConvexHull 顶点二次缩放),碎裂
// 瞬间完美拼合原面,动起来后沿裂纹自然分离 ----
for (size_t s = 0; s < refs.size(); ++s) {
if (!refs[s].Valid()) continue;
auto frag = host.spawn("fragment");
auto& tr = frag.GetComponent<
Engine::ECS::TransformComponent>();
tr.SetPosition(wallPos);
tr.SetRotationQuat(wall.GetWorldRotationQuat());
tr.SetScale(glm::vec3{1.0f});
auto& fmr =
frag.AddComponent<
Engine::ECS::MeshRendererComponent>();
fmr.model = refs[s];
// 宿主墙用 .material 覆盖时,碎块直接复用同一份资产:外观
//(含透明通道)与墙完全一致,且不需要引擎改判定
if (!hostOverridePath.empty()) {
fmr.materialOverrides[0] = hostOverridePath;
}
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
// 的 cpuPositions;Box 字段不参与 Mesh 形状)
auto& frb =
frag.AddComponent<
Engine::ECS::RigidBodyComponent>();
frb.type =
Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
frb.collider =
Engine::ECS::RigidBodyComponent::ColliderType::Mesh;
frb.mass = 0.3f;
frb.friction = 0.7f;
frb.restitution = 0.05f;
// 碎裂初速(玻璃式,非爆炸):径向 = 碎片质心 - 命中
// 点,离命中点越远越接近静止(指数衰减)——只有孔洞
// 周边的碎片被冲击波推动;法线外推让碎片朝弹侧剥出
glm::vec3 dir = centroids[s] - hitPoint;
const f32 len = glm::length(dir);
if (len < 1e-3f) {
dir = glm::vec3{Rand(-1.0f, 1.0f), 0.6f,
Rand(-1.0f, 1.0f)};
} else {
dir /= len;
}
const f32 falloff = std::exp(-len * p.falloff);
const glm::vec3 vel =
dir * p.burstSpeed * falloff *
Rand(0.4f, 1.1f) +
hitNormal * p.normalPop * Rand(0.3f, 0.8f) +
glm::vec3{0.0f, p.upBias * Rand(0.0f, 0.5f),
0.0f};
// 总量上限:超限立即回收最老碎片(防泄漏的兜底闸;
// 寿命清理才是常态回收)。模型延迟一帧卸载
const u32 bits = BitsOf(frag);
if (m_Fragments.size() >=
static_cast<size_t>(p.maxFragments)) {
host.destroy(EntityFromBits(m_Fragments.front().bits));
if (!m_Fragments.front().modelPath.empty()) {
m_PendingUnload.push_back(
std::move(m_Fragments.front().modelPath));
}
m_Fragments.erase(m_Fragments.begin());
}
m_Fragments.push_back(
Fragment{bits, vel, 0.0f, false, paths[s]});
}
}
// ---- 模型回收:延迟一帧 + 批量停飞卸载(Destroy 是帧尾延迟,
// 碎片实体本帧渲染收集仍会引用模型;下一帧实体已灭,但上一帧
// 提交的命令缓冲仍可能引用顶点缓冲/sampler——批量接口先
// WaitDeviceIdle 再销毁,与资产重建换绑同一引擎先例)----
void ApplyPendingUnload(const HostView& host) {
if (m_PendingUnload.empty()) return;
host.app.GetAssets().UnloadModelsByPaths(m_PendingUnload);
m_PendingUnload.clear();
}
// ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ----
void ApplyPendingVelocities(const HostView& host) {
auto& registry = host.app.GetScene().GetRegistry();
for (auto it = m_Fragments.begin();
it != m_Fragments.end();) {
if (it->applied) { ++it; continue; }
const ECS::Entity e = EntityFromBits(it->bits);
if (!registry.valid(e)) { // 实体没了(外部销毁)
// 模型是独占资源(一代一键),实体没了模型也回收
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(
std::move(it->modelPath));
}
it = m_Fragments.erase(it);
continue;
}
const auto* rb = registry.try_get<
Engine::ECS::RigidBodyComponent>(e);
if (rb && rb->bodyValid) {
host.app.GetPhysics().SetLinearVelocity(
it->bits, it->velocity);
it->applied = true;
}
++it; // body 未建:留待下一帧(对账晚一帧必到)
}
}
// ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)----
void AgeAndCleanup(const HostView& host, f32 dt) {
auto& registry = host.app.GetScene().GetRegistry();
for (auto it = m_Fragments.begin();
it != m_Fragments.end();) {
it->age += dt;
if (it->age < p.lifetime) { ++it; continue; }
const ECS::Entity e = EntityFromBits(it->bits);
if (registry.valid(e)) {
host.destroy(e);
}
// 模型延迟一帧卸载(本帧渲染收集还引用它)
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(std::move(it->modelPath));
}
it = m_Fragments.erase(it);
}
}
struct Fragment {
u32 bits = 0; // PhysicsWorld 键(实体位模式)
glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入)
f32 age = 0.0f; // 存活秒数(寿命清理用)
bool applied = false; // 初速已注入
std::string modelPath; // 独占模型缓存键(回收时按路径卸载)
};
bool m_Fired = false; // 一次性触发保护
std::vector<Fragment> m_Fragments; // 活碎片簿记
// 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟,
// 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针)
std::vector<std::string> m_PendingUnload;
};
} // namespace Engine::Fracture
#endif //HELLOVULKAN_FRACTURE_LIB_H
+88
View File
@@ -0,0 +1,88 @@
//
// PrefabTest.cpp:阶段2 验收脚本——脚本侧 Instantiate(prefab) 运行时
// 实例化链路。
//
// PrefabTest(挂任意空实体,作为实例化锚点):
// OnStart 按参数路径实例化一次 prefab;Interval > 0 时周期性再实例化
// (多实例验收:每次 UUID 全量再生、物理体独立、沿 X 向排开下落)。
//
// 覆盖的引擎设施:
// - ScriptBehavior::Instantiate(path, pos, rot)(Unity Object.
// Instantiate 同位)→ SceneSerializer::InstantiatePrefab
// - prefab 子树含 RigidBody/ScriptComponent 时由 PhysicsWorld/
// ScriptRuntime 下一帧对账消化(与 Spawn 同语义)
//
// 验收步骤:
// 1. 编辑器搭一个实体子树(如 Dynamic 盒刚体 + MeshRenderer 的箱子,
// 可再挂个子实体验证层级一起走)→ Outliner 右键 Save As Prefab
// (落盘 assets/prefabs/<名字>.prefab)
// 2. 建空实体挂 PrefabTest,参数 PrefabPath 填该路径 → Play:
// 锚点处出现实例;Interval 设 1.0 可看连续多实例物理下落互不串扰
// 3. 编辑态也可从 Content Browser 把 .prefab 拖进 Outliner(节点=
// 挂其下,空白区=挂根)或双击直接实例化——UUID 独立、资产引用有效
//
#include <Engine/Scripting/Script.h>
#include <glm/glm.hpp>
using namespace Engine; // f32 / ScriptBehavior 等免前缀(对齐 TestShoot)
namespace {
using Engine::Scripting::ParamBuilder;
class PrefabTest final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "PrefabTest"; }
void DeclareParams(ParamBuilder& pb) override {
// prefab 资产路径(工作目录相对,同场景/资产管理约定)
pb.Field("PrefabPath", &m_PrefabPath,
std::string{"assets/prefabs/crate.prefab"});
// 周期实例化间隔(秒;<=0 = 仅 OnStart 实例化一次)
pb.Field("Interval", &m_Interval, 0.0f);
// 相邻实例的 X 向排开间距(多实例不叠在一起)
pb.Field("SpreadX", &m_SpreadX, 2.0f);
}
void OnStart() override {
m_Timer = m_Interval;
SpawnInstance();
}
void OnUpdate(f32 dt) override {
if (m_Interval <= 0.0f) {
return;
}
m_Timer -= dt;
if (m_Timer <= 0.0f) {
m_Timer = m_Interval;
SpawnInstance();
}
}
private:
void SpawnInstance() {
// 锚点实体位姿 + 排开偏移;旋转 = 锚点自身旋转
const auto& tr = Transform();
const glm::vec3 pos =
tr.GetPosition() + glm::vec3{m_SpreadX * f32(m_Count),
0.0f, 0.0f};
const glm::quat rot = tr.GetRotationQuat();
if (Instantiate(m_PrefabPath, pos, rot) == entt::null) {
return; // 路径无效/解析失败:日志已在序列化器里打过
}
++m_Count;
}
std::string m_PrefabPath;
f32 m_Interval = 0.0f;
f32 m_SpreadX = 2.0f;
f32 m_Timer = 0.0f;
int m_Count = 0; // 已实例化份数(排开偏移基准)
};
ENGINE_REGISTER_SCRIPT(PrefabTest)
} // namespace
+17
View File
@@ -0,0 +1,17 @@
//
// ScriptLog.h:脚本日志门面(ScriptDbg 的文字版同位物)。
//
// 脚本打日志统一走这里——直接转发引擎 spdlog 默认 logger(终端
// + HelloVulkan.log + 编辑器 Log Panel 三处同收)。约定:消息开头
// 写 [脚本名] 前缀(面板过滤好定位):
// ScriptLog::Info("[WASDMove] jump");
//
#pragma once
#include <Engine/Core/Log.h>
#define SLOG_TRACE(...) ::spdlog::trace(__VA_ARGS__)
#define SLOG_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
#define SLOG_INFO(...) ::spdlog::info(__VA_ARGS__)
#define SLOG_WARN(...) ::spdlog::warn(__VA_ARGS__)
#define SLOG_ERROR(...) ::spdlog::error(__VA_ARGS__)
+226
View File
@@ -0,0 +1,226 @@
//
// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件
// 转发的端到端链路。
//
// 三个脚本协作(放一个文件方便对照):
// TestShoot(挂任意实体,如相机实体):
// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入)
// Bullet(子弹实体,由 TestShoot 运行时装配):
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
// 超时自毁防飞出地图泄漏;存活期画调试十字 = 弹道可视化
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
//
// 覆盖的引擎设施:
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
// DestroyEntity 延迟队列)
// - 组件运行时装配:RigidBody(PhysicsWorld 下一帧对账建 body)、
// ScriptComponent(ScriptRuntime 下一帧对账建实例)
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
// 帧首派发
//
// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准
// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <glm/glm.hpp>
using namespace Engine; // Input / Debug 门面
namespace {
// ---- 子弹脚本(TestShoot 运行时挂载;不手动挂场景)----
class Bullet final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "Bullet"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
// 枪口初速(世界系 m/s):TestShoot spawn 时按视线方向写进
// 脚本项参数——数据经参数层传递,与序列化同一条路
pb.Field("Velocity", &m_Velocity, glm::vec3{0.0f});
pb.Field("Lifetime", &m_Lifetime, 5.0f); // 超时自毁(秒)
}
void OnCreate() override {
// body 已由本帧 Step 的 SyncScene 建好(spawn 发生在上帧
// OnUpdate):这里施加初速,物理从下步起按弹道积分
if (glm::length(m_Velocity) > 1e-4f) {
App().GetPhysics().SetLinearVelocity(EntityBits(), m_Velocity);
}
}
void OnUpdate(f32 dt) override {
m_Age += dt;
if (m_Age >= m_Lifetime) {
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
return;
}
// 弹道可视化:命中点即弹着点(橙十字 = 弹头,速度线 = 尾迹)
const glm::vec3 pos = Transform().GetPosition();
const glm::vec3 vel =
App().GetPhysics().GetLinearVelocity(EntityBits());
Debug::Cross(pos, 0.09f, Debug::Rgb(0xFF8C1A),
/*width*/ 2.0f, /*duration*/ 0.0f,
/*depthTest*/ false);
if (glm::length(vel) > 0.5f) {
Debug::Arrow(pos, pos + vel * 0.03f, Debug::Rgb(0xFFB366),
/*headSize*/ 0.04f, /*width*/ 2.0f,
/*duration*/ 0.0f, /*depthTest*/ false);
}
}
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 防重入:Destroy 是帧尾延迟执行,命中帧内子弹与碎片群
// 可产生多对接触事件(碎片云一块挨一块),无标志会一帧
// 爆出 N 个特效(同 WallFracture m_Fired 的一次性触发模式)
if (m_Hit) return;
m_Hit = true;
(void)other; (void)normal; // 法线留给将来定向溅射
// 命中特效:空实体锚在命中点,ImpactFx 脚本项由下一帧对账
// 实例化(Spawn 即时建实体,脚本实例化天然晚一帧)
auto fx = Spawn("impact");
fx.GetComponent<Engine::ECS::TransformComponent>()
.SetPosition(point);
auto& sc = fx.AddComponent<Engine::ECS::ScriptComponent>();
Engine::ECS::ScriptComponent::Item item;
item.name = "ImpactFx";
sc.items.push_back(std::move(item));
// 自毁:延迟队列,帧尾统一执行(事件派发循环安全)
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
}
private:
glm::vec3 m_Velocity{0.0f};
f32 m_Lifetime = 5.0f;
f32 m_Age = 0.0f;
bool m_Hit = false; // 首次命中置位(防同帧多次接触重复特效)
};
// ---- 命中特效脚本(Bullet 运行时挂载;空实体 + 调试点标占位)----
class ImpactFx final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "ImpactFx"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Duration", &m_Duration, 0.4f); // 特效寿命(秒)
}
void OnUpdate(f32 dt) override {
m_Age += dt;
if (m_Age >= m_Duration) {
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
return;
}
// 扩张十字 + 白→红渐隐(占位:将来换 VFX/粒子系统)
const f32 t = m_Age / m_Duration;
const glm::vec3 pos = Transform().GetPosition();
Debug::Cross(pos, 0.12f + t * 0.55f,
glm::vec4{1.0f, 1.0f - t, 1.0f - t, 1.0f},
/*width*/ 3.0f, /*duration*/ 0.0f,
/*depthTest*/ false);
}
private:
f32 m_Duration = 0.4f;
f32 m_Age = 0.0f;
};
// ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)----
class TestShoot final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "TestShoot"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("BulletSpeed", &m_BulletSpeed, 25.0f); // 枪口初速 m/s
pb.Field("MuzzleOffset", &m_MuzzleOffset, 0.6f); // 出膛距离 m
pb.Field("FireCooldown", &m_FireCooldown, 0.15f); // 最小射击间隔 s
pb.Field("BulletRadius", &m_BulletRadius, 0.12f); // 弹头碰撞半径 m
// 半径 0.12 的反隧穿账:物理离散检测(无 CCD),检测带 =
// 半径 + 障碍半厚;薄板(如 0.2m 墙)窗口 2*(0.12+0.1)=0.44m
// > 每步进位移 0.42m(25 m/s @ 1/60),垂直命中保证检出。
// 半径越小高速弹越易整步跨过薄碰撞体(无接触=无事件)
}
void OnUpdate(f32 dt) override {
m_Cooldown -= dt;
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
// GetMouseButtonDown 边沿语义的手工等价物)
const bool down =
Input::IsMouseButtonDown(Input::MouseButton::Left);
const bool fire = down && !m_FireWasDown &&
m_Cooldown <= 0.0f;
m_FireWasDown = down;
if (fire) {
SpawnBullet();
m_Cooldown = m_FireCooldown;
}
}
private:
void SpawnBullet() {
// 枪口 = 编辑器相机视线前方(编辑形态 RMB 飞行瞄准;
// 与 WASDMove 的朝向来源一致)
const auto& cam =
App().GetRenderer().GetEditorCamera().GetTransform();
const glm::vec3 fwd =
cam.GetRotationQuat() * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 muzzle =
cam.GetWorldPosition() + fwd * m_MuzzleOffset;
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
auto bullet = Spawn("bullet");
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
tr.SetPosition(muzzle);
tr.SetRotationQuat(cam.GetRotationQuat());
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
rb.type = Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
rb.collider =
Engine::ECS::RigidBodyComponent::ColliderType::Sphere;
rb.sphereRadius = m_BulletRadius;
rb.mass = 0.2f;
rb.friction = 0.3f;
rb.restitution = 0.1f;
// Bullet 脚本项:初速/寿命经参数层传入(与序列化同一条路,
// 下一帧 ScriptRuntime 对账实例化)
auto& sc = bullet.AddComponent<Engine::ECS::ScriptComponent>();
Engine::ECS::ScriptComponent::Item item;
item.name = "Bullet";
item.params.push_back(
{"Velocity",
Engine::Scripting::ParamValue{fwd * m_BulletSpeed}});
item.params.push_back(
{"Lifetime", Engine::Scripting::ParamValue{5.0f}});
sc.items.push_back(std::move(item));
}
f32 m_BulletSpeed = 25.0f;
f32 m_MuzzleOffset = 0.6f;
f32 m_FireCooldown = 0.15f;
f32 m_BulletRadius = 0.08f;
f32 m_Cooldown = 0.0f;
bool m_FireWasDown = false; // 左键上升沿检测基准
};
ENGINE_REGISTER_SCRIPT(Bullet)
ENGINE_REGISTER_SCRIPT(ImpactFx)
ENGINE_REGISTER_SCRIPT(TestShoot)
} // namespace
+233
View File
@@ -0,0 +1,233 @@
//
// ThirdPersonCamera.cpp:第三人称环绕相机(UE SpringArm 同位)。
//
// 挂 primary 相机实体。鼠标增量 → yaw/pitch,相机位置 = 目标实体
// (按名解析,默认 "player")+ 球面偏移,朝向锁定目标。WASDMove 的
// 移动参考 = 本相机朝向——WASD 即"相机水平朝向的前后左右"(UE
// gameplay 语义)。枢轴水平硬跟、竖直带阻尼(FollowResponse):XZ
// 与角色零滞后(跑动不错位),Y 滤掉脚底每物理步的微弹跳(踩碎块/
// 撞墙)避免画面抖动。
//
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
//
// 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近;
// 拉近瞬时、拉远平滑,贴墙最小距离可配。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#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"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Sensitivity", &m_Sensitivity, 1.0f);
pb.Field("Distance", &m_Distance, 0.0f); // 0 = 吸编辑器机位
pb.Field("HeightOffset", &m_HeightOffset, 1.2f);
pb.Field("PitchMinDeg", &m_PitchMin, -35.0f);
pb.Field("PitchMaxDeg", &m_PitchMax, 75.0f);
pb.Field("GamepadTurnSpeed", &m_PadTurnSpeed, 180.0f);
// 跟随目标按名锁定(默认 "player"):场景有多个角色控制器
// 实体(如跟随 NPC)时,按名解析保证相机锁主角而非随机命中
pb.Field("TargetName", &m_TargetName, std::string("player"));
// 竖直跟随阻尼(1/s,指数逼近;0 = 硬跟=旧行为)。水平 XZ
// 恒硬跟(保证跑动时相机/角色不错位、无拖影——全轴阻尼会
// 让角色在高速移动时被"甩"出画面并抖动);仅 Y 阻尼吸收
// 角色脚底每物理步的微弹跳(踩碎块/撞墙),帧率无关
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 {
auto& reg = App().GetScene().GetRegistry();
// ---- 找目标:优先按名(TargetName)命中主角 ----
auto byName = reg.view<Engine::ECS::NameComponent>();
for (auto e : byName) {
if (byName.get<Engine::ECS::NameComponent>(e).name ==
m_TargetName) {
m_Target = e;
break;
}
}
// 名称未命中(未命名/改名)→ 回退第一个角色控制器实体
if (m_Target == entt::null) {
auto view = reg.view<Engine::ECS::CharacterControllerComponent>();
if (view.empty()) {
ENGINE_WARN(
"[ThirdPersonCamera] 场景无 CharacterController 实体,相机静止");
return;
}
m_Target = *view.begin();
}
// ---- 从编辑器机位反推 yaw/pitch/dist(摆哪开场在哪)----
const auto& camT = Transform();
const auto& tgtT =
App().GetScene().GetRegistry().get<
Engine::ECS::TransformComponent>(m_Target);
const glm::vec3 offset =
camT.GetPosition() -
(tgtT.GetPosition() +
glm::vec3(0.0f, m_HeightOffset, 0.0f));
const float dist = glm::length(offset);
if (dist > 1e-4f && m_Distance <= 0.0f) {
m_Distance = dist;
m_Pitch =
glm::degrees(std::asin(std::clamp(offset.y / dist,
-1.0f, 1.0f)));
m_Yaw = glm::degrees(std::atan2(offset.x, offset.z));
}
ENGINE_INFO(
"[ThirdPersonCamera] 目标已锁定,dist={:.2f} yaw={:.1f} pitch={:.1f}",
m_Distance, m_Yaw, m_Pitch);
Input::SetCursorCaptured(true);
}
void OnUpdate(float dt) override {
if (m_Target == entt::null) {
return;
}
// 光标管理:Esc 释放(Inspector 调参),左键重捕。
// 手柄玩家不碰鼠标——光标保持捕获态不影响摇杆输入
if (Input::IsKeyDown(Input::Key::Escape)) {
Input::SetCursorCaptured(false);
}
if (Input::IsMouseButtonDown(Input::MouseButton::Left)) {
Input::SetCursorCaptured(true);
}
// ---- 鼠标增量 + 右摇杆 → 球面角 ----
// 鼠标 = 相对增量(GetMouseDelta);右摇杆 = 持续量
//(Look 动作,满推 m_PadTurnSpeed 度/秒),两者叠加
float dx = 0.0f, dy = 0.0f;
Input::GetMouseDelta(dx, dy);
const glm::vec2 padLook = Input::ActionValue2("Look");
const float k = 0.12f * m_Sensitivity;
m_Yaw -= dx * k + padLook.x * m_PadTurnSpeed * dt;
m_Pitch += dy * k + padLook.y * m_PadTurnSpeed * dt;
m_Pitch = std::clamp(m_Pitch, m_PitchMin, m_PitchMax);
m_Yaw = std::fmod(m_Yaw, 360.0f);
// ---- 位置 = 目标枢轴 + 球面偏移 ----
// 水平 XZ 硬跟(与角色零滞后——跑动时相机/角色不错位、无
// 拖影),仅 Y 经阻尼逼近(FollowResponse):吸收目标脚底
// 每物理步的微弹跳(踩碎块/撞墙),相机竖直方向不再 1:1
// 抖动。首帧、瞬移(超 SnapDistance)或阻尼为 0 时直接吸附
// = 旧硬跟语义
const auto& tgtT =
App().GetScene().GetRegistry().get<
Engine::ECS::TransformComponent>(m_Target);
const glm::vec3 rawPivot =
tgtT.GetPosition() +
glm::vec3(0.0f, m_HeightOffset, 0.0f);
if (!m_FollowInit || m_FollowResponse <= 0.0f ||
std::abs(rawPivot.y - m_FollowPivotY) > m_SnapDistance) {
m_FollowPivotY = rawPivot.y;
m_FollowInit = true;
} else {
m_FollowPivotY +=
(rawPivot.y - m_FollowPivotY) *
(1.0f - std::exp(-m_FollowResponse * dt));
}
glm::vec3 pivot = rawPivot; // XZ 硬跟
pivot.y = m_FollowPivotY; // Y 阻尼
const float yawR = glm::radians(m_Yaw);
const float pitchR = glm::radians(m_Pitch);
const glm::vec3 dir( // 枢轴 → 相机(yaw 绕 Y,pitch 抬升)
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_CurDist);
// ---- 朝向锁定目标 ----
// 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY)
// 反解:pitch = asin(vd.y),yaw = atan2(-vd.x, -vd.z)
const glm::vec3 vd = glm::normalize(pivot - camT.GetPosition());
const float p = glm::degrees(std::asin(std::clamp(vd.y, -1.0f, 1.0f)));
const float y = glm::degrees(std::atan2(-vd.x, -vd.z));
camT.SetRotation(glm::vec3(p, y, 0.0f));
}
private:
float m_Sensitivity = 1.0f;
float m_Distance = 0.0f; // 0 = OnStart 吸编辑器机位
float m_HeightOffset = 1.2f; // 枢轴高度(瞄胸口)
float m_PitchMin = -35.0f;
float m_PitchMax = 75.0f;
float m_PadTurnSpeed = 180.0f; // 右摇杆满推角速度(度/秒)
std::string m_TargetName = "player"; // 目标实体名(NameComponent)
float m_FollowResponse = 12.0f; // 竖直跟随阻尼 1/s(0 = 硬跟)
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;
};
ENGINE_REGISTER_SCRIPT(ThirdPersonCamera)
} // namespace
+76 -28
View File
@@ -1,5 +1,9 @@
//
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
// WASDMove.cpp:第三人称移动(WASD/手柄摇杆,相机朝向参考,UE gameplay 语义)。
//
// 输入走引擎具名动作层(Move/Jump/Run 默认绑定):WASD + 方向键 +
// 手柄左摇杆移动(引擎径向死区),Space/手柄 A 跳;跑 = Run 动作
//(Shift / 手柄 L3 按下左摇杆)。摇杆模拟量直接驱动动量整形(轻推慢走)。
//
// 移动语义分派(按实体配置自动选择,优先级从上到下):
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
@@ -19,6 +23,7 @@
#include <Engine/Core/Input.h>
#include <Engine/Debug/DebugDraw.h>
#include "ScriptDebug.h"
#include "ScriptLog.h"
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
@@ -49,37 +54,68 @@ namespace {
pb.Field("RunClip", &m_RunClip, std::string("run"));
}
void OnStart() override {
SLOG_TRACE("hello");
SLOG_INFO("hello");
SLOG_DEBUG("hello");
SLOG_WARN("hello");
SLOG_ERROR("hello");
SLOG_INFO("[WASDMove] 就绪:走 {:.1f} m/s / 跑 {:.1f} m/s / 跳 {:.1f} m/s",
m_Speed, m_RunSpeed, m_JumpSpeed);
}
void OnUpdate(float dt) override {
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。
// 输入直读按键(Unity Input.GetKey(KeyCode.W) 同形);UI 捕获
// 键盘时门面静默。要手柄/运行时改键再换动作层
//(Input::ActionValue2("Move")——Unity6 新 Input System /
// UE5 Enhanced Input 对应物),本示例保持最简
const glm::quat cq = App().GetRenderer()
// 输入 = 具名动作层(Unity6 Input System / UE5 Enhanced Input
// 同位):Move 默认混绑 WASD/方向键/左摇杆(引擎做径向死区 +
// 单位圆钳制,斜向不超速),Jump 混绑 Space/手柄 A——键盘
// 手柄一套代码,将来改键/加绑定不进脚本
// 移动参考 = 场景 primary 相机(第三人称相机脚本每帧写它
// 的朝向——玩家看到的视角就是移动参考);无相机实体时
//(纯独立形态)回退编辑器相机
glm::quat cq;
bool refCam = false;
auto camView = App().GetScene()
.GetRegistry()
.view<Engine::ECS::CameraComponent>();
for (auto e : camView) {
const auto& cam =
camView.get<Engine::ECS::CameraComponent>(e);
if (cam.primary && cam.active) {
cq = App().GetScene()
.GetRegistry()
.get<Engine::ECS::TransformComponent>(e)
.GetRotationQuat();
refCam = true;
break;
}
}
if (!refCam) {
cq = App().GetRenderer()
.GetEditorCamera()
.GetTransform()
.GetRotationQuat();
}
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f);
// 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
glm::vec2 mv(0.0f);
if (Input::IsKeyDown(Input::Key::W)) mv.y -= 1.0f;
if (Input::IsKeyDown(Input::Key::S)) mv.y += 1.0f;
if (Input::IsKeyDown(Input::Key::A)) mv.x -= 1.0f;
if (Input::IsKeyDown(Input::Key::D)) mv.x += 1.0f;
const glm::vec2 mvRaw = Input::ActionValue2("Move");
glm::vec2 mv = mvRaw;
if (glm::length(mv) > 1.0f) mv = glm::normalize(mv);
glm::vec3 move = fwd * (-mv.y) + right * mv.x;
move.y = 0.0f;
if (glm::length(move) > 1e-4f) {
// 只钳超速,不放大——摇杆模拟量(幅度 <1)保留进速度:
// 轻推慢走、推满全速(归一化会把幅度吃掉,一动就满速)
if (glm::length(move) > 1.0f) {
move = glm::normalize(move);
}
// 走/跑
const bool moving = glm::length(mv) > 0.1f;
const bool running =
moving && Input::IsKeyDown(Input::Key::LeftShift);
// 走/跑:Run 动作(默认绑 Shift + 手柄 L3,按下左摇杆冲刺
// 的 Xbox 惯例)。不判"摇杆推满"——键盘 WASD 幅度恒 1.0,
// 那样判会把走路误判成跑
const bool running = Input::ActionDown("Run");
const float speed = running ? m_RunSpeed : m_Speed;
const glm::vec3 desired = move * speed; // 期望速度(输入意图)
@@ -95,6 +131,7 @@ namespace {
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
//(起跳后不宜急转弯)。指数逼近,帧率无关
const bool moving = glm::length(mvRaw) > 0.1f;
float response = moving ? m_AccelResponse : m_StopResponse;
if (!grounded) {
response *= 0.25f;
@@ -113,15 +150,14 @@ namespace {
phy.SetCharacterVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
// 跳跃:着地 + 上升沿触发。IsKeyDown 是电平量(按住
// 每帧都真),不设边沿会变成贴地火箭;IsGrounded 防
// 空中连跳
const bool jumpDown = Input::IsKeyDown(Input::Key::Space);
if (jumpDown && !m_JumpWasDown &&
// 跳跃:动作层边沿(Pressed = prev 0 → cur 非 0,键盘
// Space / 手柄 A 都产生一次上升沿)。IsGrounded 防空中连跳
if (Input::ActionPressed("Jump") &&
phy.IsCharacterGrounded(EntityBits())) {
phy.Jump(EntityBits(), m_JumpSpeed);
SLOG_INFO("[WASDMove] jump(着地起跳 {:.1f} m/s)",
m_JumpSpeed);
}
m_JumpWasDown = jumpDown;
} else if (auto* rb =
TryGetComponent<Engine::ECS::RigidBodyComponent>();
rb != nullptr && rb->bodyValid &&
@@ -155,11 +191,24 @@ namespace {
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
}
// ---- 动画:idle/walk/run 三态切换(Direct 模式 crossfade)----
// 目标 clip 是"电平量":Shift 中途按下/松开都要能换,所以
// 每帧重算目标,只在目标与上帧不同才 Play(边沿)。每帧无
// 条件 Play 会不停重置播放头,动画永远播不完
const std::string& target = !moving ? m_IdleClip : (running ? m_RunClip : m_WalkClip);
// ---- 动画:按真实速度分档(迟滞防临界抖)----
// 判据 = 动量整形后的真实水平速度,不是输入原始值:起步
// 滑行期已是 walk,松手刹车期保持 walk 直到真停——动画与
// 位移观感一致,按键瞬通瞬断不再瞬间换挡。run 双阈值迟滞
//(进 0.9×RunSpeed / 退 0.7×RunSpeed):摇杆在阈值附近晃
// 不抖档。引擎 Play(名) 自带相位对齐 + crossfade,切档
// 突兀感只剩判据本身的问题
const float planarSpeed = glm::length(m_PlanarVel);
std::string target;
if (planarSpeed < 0.15f) {
target = m_IdleClip;
} else if (m_CurClip == m_RunClip) {
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
: m_WalkClip;
} else {
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
: m_WalkClip;
}
if (target != m_CurClip) {
if (auto* anim = TryGetComponent<Engine::ECS::AnimationComponent>();
anim != nullptr && anim->Play(target, 0.35f)) {
@@ -221,7 +270,6 @@ namespace {
std::string m_WalkClip = "walk";
std::string m_RunClip = "run";
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
bool m_JumpWasDown = false; // 上帧跳跃键(上升沿检测)
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
};
+87
View File
@@ -0,0 +1,87 @@
//
// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h
//(打靶场 Target 复用同一条路径),本文件只剩宿主职责:
// DeclareParams 直绑 Burst::p 各字段(参数名/默认值与历史档位
// 逐一同名同值,旧场景不受影响)
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方撞击来源
// 与速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)。
// 撞击来源 = 动态刚体(子弹/抛体)或角色(玩家撞墙)——角色是
// CharacterVirtual,其碰撞事件由 PhysicsWorld 从角色接触合成
// (Jolt 的 ContactListener 只回调刚体,不覆盖角色)
// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收)
// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载)
//
// 外观默认继承墙的生效材质(底色/贴图/透明),碎块与墙面观感一致;
// 要独立外观(玻璃/发光碎片)置 Burst::look.inheritFromHost = false
// 再用 baseColor/emissive 显式指定。
//
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
// 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击:
// 墙沿裂纹碎成不规则碎片,孔洞周边碎块小、飞散明显,边缘大块
// 塌落堆积在墙脚,若干秒后清空。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Types.h>
#include "FractureLib.h"
using namespace Engine; // u32/f32/i32 基础类型(历史脚本同款)
namespace {
// ---- 可碎墙(挂墙实体;触发后由 Burst 管一整批碎片)----
class WallFracture final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "WallFracture"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
// 直绑 Burst::p:碎裂逻辑只读 Burst::p,绑到别处(曾经的
// 独立 m_P)会让计数器/寿命等全部失效
pb.Field("Count", &m_Burst.p.count, 30);
pb.Field("SeedRadius", &m_Burst.p.seedRadius, 0.9f); // 加密半径 m
pb.Field("MinSeedDist", &m_Burst.p.minSeedDist, 0.12f); // 种子最小间距 m
// 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散:
// 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线
// 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导
pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.2f); // 近点径向初速 m/s
pb.Field("Falloff", &m_Burst.p.falloff, 1.0f); // 距离衰减率 1/m
pb.Field("NormalPop", &m_Burst.p.normalPop, 0.4f); // 法线外推 m/s
pb.Field("UpBias", &m_Burst.p.upBias, 0.3f); // 上抛偏置 m/s
pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f); // 碎片寿命(秒)
pb.Field("MaxFragments", &m_Burst.p.maxFragments, 128); // 活碎片上限
pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f); // 触发阈值
}
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 触发条件在库内:对方须是够快的撞击来源——动态刚体
//(子弹/抛体)或角色(玩家撞墙);速度阈值过滤静止贴靠
m_Burst.TryFire(HV(), other, point, normal);
}
void OnUpdate(f32 dt) override { m_Burst.Tick(HV(), dt); }
// 墙销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡处理,
// 独占碎片模型统一批量回收
void OnDestroy() override { m_Burst.Shutdown(HV()); }
private:
// 库桥接:本成员函数内可访问 protected 基类设施(App/Spawn/
// Destroy),库内只走公共 API——见 FractureLib.h HostView 注
Engine::Fracture::HostView HV() {
return {App(), EntityBits(),
[this](const std::string& n) { return Spawn(n); },
[this](Engine::ECS::Entity e) { Destroy(e); }};
}
// 碎片簿记 + 生命周期;参数即 m_Burst.p(DeclareParams 直绑其字段)
Engine::Fracture::Burst m_Burst;
};
} // namespace
ENGINE_REGISTER_SCRIPT(WallFracture)
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(HelloVulkan CXX)
project(HelloVulkan C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+8
View File
@@ -27,6 +27,13 @@ target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR})
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
target_link_libraries(ImGuizmo PUBLIC imgui)
# ufbx(FBX 全版本导入,vendored 单文件 C 库):assimp 的 FBX importer
# 只认 FBX 2011+,Mixamo 及大量网上资产是 FBX 6.1 binary(二进制布局
# 与 7.x 在对象 ID 层不兼容,assimp DOM 直接拒读)。ufbx 官方支持
# FBX 5.x/6.x/7.x 全版本,引擎的 .fbx 解析统一走它(glTF/OBJ 仍 assimp)
add_library(ufbx STATIC ${CMAKE_SOURCE_DIR}/Lib/ufbx/ufbx.c)
target_include_directories(ufbx PUBLIC ${CMAKE_SOURCE_DIR}/Lib/ufbx)
# Tracy(CPU/GPU 帧分析)
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
@@ -80,6 +87,7 @@ target_link_libraries(Engine PUBLIC
spdlog::spdlog
stb
assimp::assimp
ufbx
meshoptimizer
Jolt::Jolt
yaml-cpp::yaml-cpp
@@ -16,6 +16,7 @@
#define HELLOVULKAN_ANIMATIONATTACH_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <string>
#include <vector>
@@ -51,6 +52,16 @@ namespace Engine {
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
const std::string& sourcePath);
// 纯算法核心(headless 探针可直测):轨道重映射进 dstNodes
// 坐标系,产出的 clips 追加到 outExtra。幂等/命名冲突检查
// 基于 existingClips。AttachClipsToModel 只是
// "取 nodes/clips + 回填" 的薄封装
AttachResult AttachClipsToNodes(
const std::vector<ModelNode>& dstNodes,
const std::vector<AnimationClip>& existingClips,
ParsedModel&& src, const std::string& sourcePath,
std::vector<AnimationClip>& outExtra);
} // namespace Animation
@@ -64,15 +64,19 @@ namespace Engine::Animation {
// outGlobals 尺寸 = model.GetNodes().size(),父序 DFS 累乘。
// (crossfade 双层 = EvaluateLayers 的 count==2 特例,同一套
// 加权混合数学,消掉历史双份代码)
// inPlace = 丢弃动画子树根节点的位移(Unity in-place 语义:
// 根运动开关关闭,原地播放,旋转保留)
void Evaluate(const Model& model, const AnimationClip& cur,
f32 curTime, const CrossfadeState& xf,
std::vector<glm::mat4>& outGlobals);
std::vector<glm::mat4>& outGlobals,
bool inPlace = false);
// N 层加权混合(BlendTree / 状态机过渡的通用入口):
// 逐层采样局部 TRS → 权重归一化混合(pos/scl lerp、rot 渐进
// slerp)→ global 累乘。全空/全零权重 = rest 姿态
void EvaluateLayers(const Model& model, const PoseLayer* layers,
u32 count, std::vector<glm::mat4>& outGlobals);
u32 count, std::vector<glm::mat4>& outGlobals,
bool inPlace = false);
// 绑定姿势:全节点 untouched = localTransform 原样累乘。
// 与渲染 identity palette 的表现一致(无动画/组件禁用实体的
+112 -15
View File
@@ -1,13 +1,24 @@
//
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
//
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
// 不同源(英文/Mixamo/UE/Blender/MMD 日文)。Unity Humanoid 的解法
// 是把任意命名骨架映射到标准人形槽位(Avatar bone mapping),动画
// 经槽位转换后即可驱动任意人形角色。
// 架构分层(对齐 Unity Mecanim / UE IK Retargeter 的做法):
//
// 这里实现其中枢:名字归一化 → 槽位识别(关键词表,en/Mixamo/MMD)
// → 源/目标骨架间生成 node 下标映射对。同名直映是映射的特例。
// ┌─ standard ─ 标准人形拓扑:HumanoidSlot 52 槽(主干 6 + 臂链
// │ 4×2 + 指骨 15×2 + 腿链 3×2 + 趾 2),带侧别/镜像/
// │ 指骨结构化访问。重定向的"标准空间"。
// ├─ dialects ─ 命名方言数据表:各来源(Mixamo/Rigify/UE/MMD)的
// │ "槽位 → 归一化骨名"候选模式,纯数据无特判。命名
// │ 习惯差异全部收敛在这层(Unity 把 token 规则编成
// │ 数据表、UE 用链名映射,同思想:识别=查表)。
// └─ mapper ── 归一化(NormalizeLower → side 抽取 → 分隔符剥离)
// + 查表识别(MatchHumanoidSlot)+ 跨骨架配对
// (AutoMapHumanoid:同名直映优先,余下槽位一对一)。
//
// 动画重定向本身在 AnimationAttach:src 动画经映射对烘焙到 dst
// 骨架(两遍式 T-pose 参考搬运),本模块只负责"名字 → 槽位 → 配对"。
//
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
// 不同源。同名直映是映射的特例(同源拆分资产零损耗)。
//
#ifndef HELLOVULKAN_HUMANOIDRIG_H
@@ -16,13 +27,24 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <glm/glm.hpp>
#include <string_view>
#include <utility>
#include <vector>
namespace Engine::Animation {
// 标准人形槽位(最小主干集,不含手指/表情——主干驱动即可行走)
// ---- standard:标准人形槽位(Unity Avatar 52 骨拓扑)----
//
// 布局保证算术可推导(镜像/指骨反查都走位运算友好的分段):
// [0..5] 主干:Hips→Spine→Chest→UpperChest→Neck→Head
// [6..9] 左臂链:Shoulder→UpperArm→LowerArm→Hand
// [10..24] 左指骨:5 指 × 3 段(Thumb/Index/Middle/Ring/Pinky,
// 段 1=近节 → 3=末节,与 Mixamo HandThumb1-3、Rigify
// .01-.03、UE Proximal/Distal 对齐)
// [25..28] 左腿链:UpperLeg→LowerLeg→Foot→Toes
// [29..51] 右侧镜像(+23 偏移,镜像布局与左完全同构)
enum class HumanoidSlot : u32 {
Hips = 0,
Spine,
@@ -34,38 +56,113 @@ namespace Engine::Animation {
LeftUpperArm,
LeftLowerArm,
LeftHand,
LeftThumb1, // 指骨区起点(FingerSlot 算术依赖连续布局)
LeftThumb2,
LeftThumb3,
LeftIndex1,
LeftIndex2,
LeftIndex3,
LeftMiddle1,
LeftMiddle2,
LeftMiddle3,
LeftRing1,
LeftRing2,
LeftRing3,
LeftPinky1,
LeftPinky2,
LeftPinky3,
LeftUpperLeg,
LeftLowerLeg,
LeftFoot,
LeftToes,
RightShoulder,
RightUpperArm,
RightLowerArm,
RightHand,
LeftUpperLeg,
LeftLowerLeg,
LeftFoot,
RightThumb1,
RightThumb2,
RightThumb3,
RightIndex1,
RightIndex2,
RightIndex3,
RightMiddle1,
RightMiddle2,
RightMiddle3,
RightRing1,
RightRing2,
RightRing3,
RightPinky1,
RightPinky2,
RightPinky3,
RightUpperLeg,
RightLowerLeg,
RightFoot,
RightToes,
Count
};
// 槽位侧别(左右对称骨骼的识别结果)
enum class Side : u8 { None, Left, Right };
// 槽位显示名("Hips"/"LeftUpperArm"...;调试映射表用)
// 指别(0=拇指 … 4=小指;方言表里 Mixamo pinky / UE little 同槽)
enum class FingerKind : u32 { Thumb = 0, Index, Middle, Ring, Pinky };
// ---- standard:槽位结构化访问 ----
// 槽位侧别(主干槽返回 None;镜像侧与枚举名一致)
Side SideOfSlot(HumanoidSlot slot);
// 左右镜像(主干槽原样返回)
HumanoidSlot MirrorSlot(HumanoidSlot slot);
// 指骨槽位合成:(side, 指, 段 1..3) → 槽位
HumanoidSlot FingerSlot(Side side, FingerKind kind, u32 seg);
// 指骨槽位反解(非指骨槽位返回 false)
bool SlotFinger(HumanoidSlot slot, Side& side, FingerKind& kind,
u32& seg);
// 槽位显示名(Unity 语义:"Hips"/"LeftUpperArm"/"LeftHandThumb2"...;
// 调试映射表/Inspector 明细行用)
const char* HumanoidSlotName(HumanoidSlot slot);
// 名字归一化 + 槽位识别。
// ---- mapper:名字归一化 + 槽位识别 ----
// 名字归一化 + 查方言表识别槽位。
// "mixamorig:LeftForeArm" → {LowerArm, Left};"左ひじ" → {LowerArm, Left};
// "Upper Arm_L" → {UpperArm, Left}。识别失败 ok=false。
// "Upper Arm_L" → {UpperArm, Left};"DEF-thumb.01.L" → {Thumb1, Left};
// "Thumb Proximal_R" → {Thumb1, Right}。识别失败 ok=false。
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
bool& ok);
// 跨骨架自动映射:返回 (srcNodeIdx, dstNodeIdx) 对列表。
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位匹配;
// 一槽多候选时取语义关键词优先级表中的顺序(先高后低)。
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位一对一配对;
// 同槽多候选时取方言表内的优先序(先高后低)。
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
const std::vector<ModelNode>& srcNodes,
const std::vector<ModelNode>& dstNodes);
// 估计源/目标骨架的根对齐旋转(rest 姿态 basis 对齐,src 世界 →
// dst 世界)。跨骨架 retarget 时两套骨架常不同朝向(kwy DummyDoll
// Z-up vs X Bot Y-up),增量变换须经该旋转共轭才能跨空间传递。
// up = 头 - 双脚中点;right = 右臂 - 左臂(去 up 分量);
// fwd = up × right,脚趾朝向消歧。槽位不全(缺头/双脚/双臂)
// 或 rest 退化时返回 identity。
glm::quat EstimateHumanoidRootAlignment(
const std::vector<ModelNode>& srcNodes,
const std::vector<ModelNode>& dstNodes);
// 估计骨架的 T-pose 参考姿势(每节点**世界**旋转)。
// 人形动画资产的摆动量以 T-pose 为零位;rest 是 A-pose(臂下垂)
// 时,旋转增量直接搬运会过摆(臂越过中线)。此处把臂链
// (upper_arm 子树)绕肩刚体摆到水平侧向作为参考,其余骨取 rest
// (手指/趾走 rest 零位局部增量通道,见 AnimationAttach——指根
// 扇形编码在 rest 平移里,不参与世界系零位搬运);rest 本为
// T-pose 的资产摆动量≈0 自动退化。行走基不可用时全 rest。
std::vector<glm::quat> EstimateHumanoidTPose(
const std::vector<ModelNode>& nodes,
const std::vector<glm::mat4>& restGlobals);
} // namespace Engine::Animation
#endif // HELLOVULKAN_HUMANOIDRIG_H
@@ -182,6 +182,18 @@ namespace Engine
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
void UnloadModel(ModelRef h) { m_Models.Unload(h); }
// 批量按路径卸载模型(未加载为 no-op)。先 GPU 全停飞再销毁
//(在飞命令缓冲可能仍引用顶点缓冲/默认纹理 sampler——
// 与 ReloadMaterialAsset 的 WaitDeviceIdle 同一先例);一次
// 停飞摊薄整批,调用方攒批降频(脚本碎片回收每帧一批)
void UnloadModelsByPaths(const std::vector<std::string>& paths)
{
if (paths.empty()) return;
m_Uploader.WaitDeviceIdle();
for (const std::string& p : paths) {
m_Models.UnloadByPath(p);
}
}
// 按路径查已加载的模型(未加载返回 null,不触发加载)——
// 面板 hover 信息卡用
Model* GetModelByPath(const std::string& path) const
@@ -213,6 +225,15 @@ namespace Engine
// 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重
ModelRef CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a);
// 程序化立方体(halfSize 为半边长):同参数缓存去重。
// 尺寸变化走实体 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 同一路径零成本(动态切换场景直接反复调用)
@@ -42,6 +42,15 @@ namespace Engine::Assets {
virtual Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) = 0;
// 程序化纯色立方体(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;
+113
View File
@@ -0,0 +1,113 @@
//
// LogRing.h:进程级日志环形缓冲(编辑器 Log Panel 的数据源)。
//
// RingSink 注册进默认 logger 的 sinks 后,所有日志(引擎 + 脚本经
// ScriptLog/ENGINE_* 打印的)自动落缓冲;面板按 version 增量感知、
// Snapshot 整体拉取。头文件设施(sink_it_ 内联),无 cpp。
//
#ifndef HELLOVULKAN_LOGRING_H
#define HELLOVULKAN_LOGRING_H
#include <Engine/Core/Types.h>
#include <spdlog/sinks/base_sink.h>
#include <spdlog/details/log_msg.h>
#include <atomic>
#include <mutex>
#include <string>
#include <vector>
namespace Engine::Debug {
struct LogEntry {
spdlog::level::level_enum level;
std::string msg; // 整行(已按 ring pattern 格式化,无色码)
};
class LogRingBuffer {
public:
static constexpr size_t kCap = 2048;
// 门控:启动期系统初始化日志(Vulkan/UBO/IBL...)不进面板,
// 与 Unity Console 语义一致(只收运行期游戏侧日志)。
// Application::Run 进入主循环时开闸
bool IsEnabled() const {
return m_Enabled.load(std::memory_order_relaxed);
}
void SetEnabled(bool v) {
m_Enabled.store(v, std::memory_order_relaxed);
}
void Push(spdlog::level::level_enum lvl, std::string msg) {
std::lock_guard<std::mutex> lk(m_Mutex);
if (m_Entries.size() < kCap) {
m_Entries.push_back({lvl, std::move(msg)});
} else {
m_Entries[m_Head] = {lvl, std::move(msg)};
m_Head = (m_Head + 1) % kCap; // 满后覆写最旧
}
++m_Version;
}
// 快照(满环时按时间序重排)
std::vector<LogEntry> Snapshot() const {
std::lock_guard<std::mutex> lk(m_Mutex);
if (m_Entries.size() < kCap) {
return m_Entries;
}
std::vector<LogEntry> out;
out.reserve(kCap);
out.insert(out.end(), m_Entries.begin() + static_cast<long>(m_Head),
m_Entries.end());
out.insert(out.end(), m_Entries.begin(),
m_Entries.begin() + static_cast<long>(m_Head));
return out;
}
void Clear() {
std::lock_guard<std::mutex> lk(m_Mutex);
m_Entries.clear();
m_Head = 0;
++m_Version;
}
u64 Version() const {
return m_Version.load(std::memory_order_relaxed);
}
private:
mutable std::mutex m_Mutex;
std::vector<LogEntry> m_Entries;
size_t m_Head = 0;
std::atomic<u64> m_Version{0};
std::atomic<bool> m_Enabled{false};
};
inline LogRingBuffer& LogRing() {
static LogRingBuffer inst;
return inst;
}
// spdlog sink 适配:log() 整行入环。pattern 由装配端设置(无色码)
class RingSink : public spdlog::sinks::base_sink<std::mutex> {
protected:
void sink_it_(const spdlog::details::log_msg& msg) override {
if (!LogRing().IsEnabled()) {
return; // 启动期:只落终端/文件,不进面板
}
spdlog::memory_buf_t formatted;
base_sink<std::mutex>::formatter_->format(msg, formatted);
std::string line(formatted.data(), formatted.size());
while (!line.empty() &&
(line.back() == '\n' || line.back() == '\r')) {
line.pop_back();
}
LogRing().Push(msg.level, std::move(line));
}
void flush_() override {}
};
} // namespace Engine::Debug
#endif //HELLOVULKAN_LOGRING_H
+17 -22
View File
@@ -37,29 +37,27 @@ namespace Engine
void AddAllocation(u64 bytes);
void AddDeallocation(u64 bytes);
// ---- 采样进程真实内存 ----
void SampleProcessMemory();
// ---- 打印 ----
void Print() const;
void PrintDetailed() const;
void PrintComparison() const;
// ---- 只读快照(面板/遥测消费)----
struct Snapshot
{
u64 textureCount = 0;
u64 textureBytes = 0;
u64 meshCount = 0;
u64 meshBytes = 0;
u64 bufferCount = 0;
u64 bufferBytes = 0;
u64 memoryBlockBytes = 0; // MemoryAllocator 已分配块
u64 memoryUsedBytes = 0; // 块内实际占用
u64 frameAllocation = 0; // 本帧新增(BeginFrame 清零)
u64 frameDeallocation = 0; // 本帧释放
u64 totalAllocation = 0;
u64 totalDeallocation = 0;
};
Snapshot TakeSnapshot() const;
private:
Stats() = default;
struct ProcessMemory
{
u64 VmSize = 0; // /proc/self/status VmSize
u64 Rss = 0; // /proc/self/status VmRSS
u64 Pss = 0; // smaps_rollup
u64 SharedClean = 0;
u64 PrivateDirty = 0;
u64 PrivateClean = 0;
};
ProcessMemory ReadProcessMemory() const;
mutable std::mutex m_Mutex;
// 资源计数
@@ -81,9 +79,6 @@ namespace Engine
u64 m_FrameDeallocation = 0;
u64 m_TotalAllocation = 0;
u64 m_TotalDeallocation = 0;
// 进程快照
ProcessMemory m_ProcessMem{};
};
#define STATS Engine::Stats::Get()
+12
View File
@@ -203,6 +203,10 @@ namespace Engine::ECS {
struct MeshRendererComponent {
ModelRef model;
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
// 单 mesh 子引用:-1 = 整模型全部;N = 只画第 N 个
//(GetMeshes 下标)。物理 Mesh 碰撞体自动跟随(BuildMeshShape
// 读本组件)。序列化缺省键 = -1 向前兼容
int meshIndex = -1;
std::unordered_map<u32, std::string> materialOverrides;
};
@@ -222,6 +226,11 @@ namespace Engine::ECS {
bool playing = true;
bool loop = true;
f32 speed = 1.0f;
// 根运动开关(Unity Animator "Apply Root Motion" 同语义):
// true = 保留根节点位移轨道(跑动 clip 自带位移会漂出锚点);
// false = 原地播放(丢弃动画子树根节点位移,旋转保留)——
// 位移归脚本/物理驱动的角色(CharacterController 等)用 false
bool applyRootMotion = true;
// ---- 播放头 / crossfade(瞬态,不落盘——重启从 0 重播)----
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
@@ -471,6 +480,9 @@ namespace Engine::ECS {
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
f32 gravityScale = 2.0f;
// 形状附加偏移:叠加在脚底约定(胶囊底端=原点)之上,模型
// pivot 不在脚底时对齐视觉网格用(Y+ 胶囊上抬)
glm::vec3 shapeOffset{0.0f};
bool operator==(const CharacterControllerComponent&) const = default;
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
+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; } // 序列化只读
@@ -6,6 +6,11 @@
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
//
// 角色间碰撞(Jolt 设计行为):CharacterVirtual 彼此默认穿透,需经
// CharacterVsCharacterCollision 显式互查。PhysicsWorld 持有一份
// CharacterVsCharacterCollisionSimple 并注入每个角色(SetCharacterCollision),
// 使角色扫掠时也撞其它角色——进/出列表由本类构造/析构自动维护。
//
// 速度语义(游戏逻辑职责分工):
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
@@ -36,13 +41,21 @@ namespace Engine {
f32 mass = 70.0f;
f32 maxSlopeAngle = 45.0f; // 度
f32 stepHeight = 0.4f; // 台阶攀爬高
// 形状附加偏移(相对"胶囊底端钉实体原点"的脚底约定):
// 模型 pivot 不在脚底时对齐用(Y+ 上抬)
glm::vec3 shapeOffset{0.0f};
bool operator==(const Params&) const = default;
};
CharacterController(JPH::PhysicsSystem& system, const Params& params,
const glm::vec3& position);
~CharacterController() = default; // Ref 引用计数自动释放
// 析构:先从角色间碰撞列表摘除(防悬垂指针),Ref 随即释放
~CharacterController();
// 注入角色间碰撞列表(PhysicsWorld 单份共享):登记本角色并打开
// 互查。构造后由 PhysicsWorld 创建时调用一次;列表须比本对象活得久
void SetCharacterCollision(JPH::CharacterVsCharacterCollisionSimple& list);
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
@@ -59,6 +72,16 @@ namespace Engine {
const Params& GetParams() const { return m_Params; }
// ---- 碰撞事件支持(PhysicsWorld 合成 Enter/Exit 用)----
// 本步活跃接触列表(CharacterVirtual 直通;含对方体 ID/命中点/
// 法线/距离)。CharacterVirtual 非刚体,Jolt 的 ContactListener
// 不会为它回调——由 PhysicsWorld 读本列表对比出事件边沿
const JPH::CharacterVirtual::ContactList& GetActiveContacts() const;
// 上一次 Update 扫掠"之前"的速度(撞击强度判定)。Jolt 在扫掠
// 中会抵消朝向墙面/陡面的速度分量(CancelVelocityTowardsSteepSlopes),
// 扫掠后再读 GetLinearVelocity 往往已归零——撞墙判定必须用这里
glm::vec3 GetSweepVelocity() const;
// 调试渲染:非 body,PhysicsSystem::DrawBodies 画不到,须手动
// (脚本在物理位姿/脚底原点处画胶囊线框)
void DebugDraw(JPH::DebugRenderer& renderer) const;
@@ -67,6 +90,8 @@ namespace Engine {
Params m_Params;
JPH::Ref<JPH::CharacterVirtual> m_Character;
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
JPH::CharacterVsCharacterCollisionSimple* m_VsChar = nullptr; // 非拥有
JPH::Vec3 m_SweepVelocity{0.0f, 0.0f, 0.0f}; // 扫掠前速度快照
};
} // namespace Engine
+21 -1
View File
@@ -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 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
@@ -102,6 +107,9 @@ namespace Engine {
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
glm::vec3 GetCharacterPosition(u32 entityBits) const;
// 角色"撞击强度"速度(上一次扫掠前的速度):撞墙/碎墙等判定用它,
// 而非 GetCharacterVelocity——后者已被 Jolt 抵消掉朝向墙面的分量
glm::vec3 GetCharacterImpactVelocity(u32 entityBits) const;
private:
// 创建时缓存的参数(变更检测 → 重建 body)
@@ -138,7 +146,8 @@ namespace Engine {
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
void SyncCharacters();
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)+ 合成角色
// 碰撞事件(CharacterVirtual 不进 ContactListener,见函数注释)
void UpdateCharacters(f32 dt);
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
@@ -150,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(多线程并发,加锁)
@@ -165,9 +178,16 @@ namespace Engine {
std::unordered_map<u32, ConstraintRecord> m_Constraints;
// ---- 角色(entityBits → CharacterVirtual 封装)----
// 角色间碰撞互查表:Jolt CharacterVirtual 默认彼此穿透,须显式注入
// CharacterVsCharacterCollisionSimple 才互撞。声明在 m_Characters
// 之前(析构更晚)——角色析构时会回表 Remove,列表须后于角色释放
Scope<JPH::CharacterVsCharacterCollisionSimple> m_VsCharacterCollision;
struct CharacterEntry {
std::unique_ptr<CharacterController> cc;
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
// 上一步活跃接触的"对方实体"集合:本步对比出角色碰撞
// Enter/Exit(CharacterVirtual 非刚体,ContactListener 不收)
std::unordered_set<u32> contacts;
};
std::unordered_map<u32, CharacterEntry> m_Characters;
@@ -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,
+28 -4
View File
@@ -47,7 +47,13 @@ namespace Engine {
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
struct MaterialParams {
// x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
// 默认 = 电介质表面推荐值(Unity/UE 同款):
// metallic = 0 皮肤/头发/布料等非金属的正确值
// roughness = 1.0 因子语义是"乘贴图",1.0 = 贴图值直用(同 glTF
// roughnessFactor 缺省);配合兜底 MR 纹理 G=128
// 得 effective roughness ≈ 0.5,不再是 ×0.5 后的
// 0.25 假高光(ufbx 路径不写 factors,全靠这里兜底)
glm::vec4 factors{0.0f, 1.0f, 0.0f, 0.0f};
// glTF baseColorFactor:rgb 乘进 albedo,a 作 BLEND 透明度
glm::vec4 baseColor{1.0f, 1.0f, 1.0f, 1.0f};
// glTF 表面扩展(KHR_materials_ior / transmission / volume /
@@ -55,12 +61,25 @@ namespace Engine {
// y = 透射率(0 = 普通表面),z = 自发光强度,w = 体积厚度
//(折射屏幕偏移的世界距离,0 = 无弯曲直透)
glm::vec4 surface{1.5f, 0.0f, 1.0f, 0.0f};
// 杂项语义:x = unlit(KHR_materials_unlit,shader 早退直出 albedo)
// 杂项语义:
// x = unlit(KHR_materials_unlit,shader 早退直出 albedo)
// y = UV 左右镜像(u → 1-u,1 = 开启)
// z = UV 上下镜像(v → 1-v,1 = 开启)
// w = 保留
// 镜像作用于材质全部纹理槽,叠加在每槽 UV 变换(uvA/uvT)之后:
// 用于纠正源资产 UV 朝向(部分角色 rip 的 UV 与 glTF 上左原点
// 约定相反,贴图整体翻转)。材质级语义,非单槽
glm::vec4 misc{0.0f, 0.0f, 0.0f, 0.0f};
// glTF emissiveFactor:自发光颜色(rgb;黑 = 不发光)。emissive
// 槽默认兜底纹理是纯黑,无贴图时 Emissive Strength 乘出来恒黑
// ——无贴图想要发光就调这里(rgb × 贴图 × surface.z)
glm::vec4 emissiveFactor{0.0f, 0.0f, 0.0f, 0.0f};
// 每槽 KHR_texture_transform(glTF2 每个 textureInfo 独立声明
// offset/scale/rotation——烘焙单份顶点 UV 无法表达"一材质多槽
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在本引擎
// v-up 顶点 UV 空间,换算推导见 Model.cpp glTF 分支注释)。
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在 glTF
// 规范 UV 空间(v=0 = 图像顶);顶点 UV 经 aiProcess_FlipUVs 已
// 归一至该空间,A/t 即规范式 off + R(rot)·S(scale)·uv 的摊平,
// 换算推导见 Model.cpp glTF 分支注释)。
// uvA[slot] = (a11, a12, a21, a22)(行主 2×2),uvT[slot] = (tx, ty, 0, 0)。
// 长度必须 = Material::kSlotCount;std140 vec4 数组步长 16B 与
// CPU 布局天然对齐
@@ -68,6 +87,11 @@ namespace Engine {
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f},
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f}};
glm::vec4 uvT[6] = {};
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等局部效果):
// rgb = 叠加色(与 baseColor 同款直乘/直用,不做空间换算),
// a = 强度(0 = 关闭,mix 恒等无副作用)。alpha 遮罩来自贴图本身,
// 无 alpha 或 alpha 恒 1 的贴图开启后即等效整体染色
glm::vec4 alphaOverlay{0.0f, 0.0f, 0.0f, 0.0f};
};
class Material {
+7 -10
View File
@@ -47,8 +47,9 @@ namespace Engine {
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
// glb 内嵌纹理无独立文件路径 → 对 glb 模型 Create Override 时
// 纹理槽为空(回退默认纹理),参数/透明照常可编辑。
// glb 内嵌纹理在模型导入期已抽到 <模型目录>/textures/ 落盘并回写为
// 真实路径(Model::ParseFromDisk),故此处与文件纹理同语义——覆盖
// 材质可正常继承
struct MaterialAssetData {
std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params;
@@ -66,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();
@@ -86,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
+36 -4
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> // 默认参数需完整类型(烘焙节点变换用)
@@ -137,6 +138,12 @@ namespace Engine
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
static ParsedModel ParseFromDisk(const std::string& path);
// 资产显示名(编辑器 UI / clip 命名的唯一来源):取文件 stem;
// stem 无信息量(空 / 导入资产库的 "asset")时读同目录 pathname
// 文件取原始资产 basename,再回退父目录名。避免 UI 直接显示
// 哈希路径(用户看到的是 id)
static std::string AssetDisplayName(const std::string& path);
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
// 解析失败返回 nullptr。
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
@@ -144,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,
@@ -158,6 +168,25 @@ namespace Engine
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
// 程序化立方体(halfSize = 半边长,几何走 MeshFactory::Cube):
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
// 缩放——同 halfSize 同色共享一份缓存模型
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;
@@ -203,11 +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);
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;
@@ -37,6 +40,10 @@ namespace Engine {
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
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;
@@ -45,7 +52,8 @@ namespace Engine {
~VulkanAssetUploader() override;
// ---- 材质资产(.material 数据段 → GPU 段)----
// 每槽路径建独占纹理(空/缺失回退共享默认纹理),再组 Material
// 每槽经路径纹理缓存(跨材质去重)取纹理,同批新纹理合并为一次
// submit;空/缺失回退共享默认纹理,再组 Material
Scope<MaterialAsset> CreateMaterial(MaterialAssetData&& data) override;
// 设备空闲屏障(编辑器资产重建路径:卸载旧 GPU 资源前全停飞)
void WaitDeviceIdle() override;
@@ -56,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;
+55 -1
View File
@@ -28,6 +28,7 @@
#include <Engine/Core/Types.h>
#include <Engine/ECS/Components.h>
#include <Engine/ECS/Scene.h>
#include <Engine/ECS/EntityHandle.h> // 须在 Scene.h 之后(内联方法需完整 Scene)
#include <Engine/Scripting/ScriptParam.h>
#include <string>
@@ -69,6 +70,22 @@ namespace Engine {
// 在此按名取值)
virtual void OnParamsApplied() {}
// ---- 碰撞回调(PhysicsWorld::GetEvents 帧首派发,ScriptRuntime
// 转发;事件语义 = 最近一次物理步进)----
// Enter/Exit 由 Jolt Added/Removed 转发;Persisted 不回调
//(最小 API,接触持续期无每帧噪音)。对方是触发器(sensor)
// 时同样触发——后续按需拆 OnTriggerEnter/Exit。
// other = 对方实体位模式(还原:static_cast<entt::entity>(other),
// 回调时对方可能已销毁,用前自行 Scene::IsAlive 校验)。
// 派发在 Sync/OnUpdate 之前:回调里 spawn/destroy 安全
//(destroy 入延迟队列,spawn 由下一帧对账消化)
virtual void OnCollisionEnter(u32 other,
const glm::vec3& point,
const glm::vec3& normal) {
(void)other; (void)point; (void)normal;
}
virtual void OnCollisionExit(u32 other) { (void)other; }
protected:
// 便捷访问:绑定实体的 TransformComponent
ECS::TransformComponent& Transform() {
@@ -96,6 +113,38 @@ namespace Engine {
bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; }
// ---- 场景实体 spawn/destroy 底座(Unity Instantiate/Destroy
// 同位)----
// spawn:帧内即时创建(UUID + Transform + Name 出生即有),
// 组件由脚本自行 AddComponent 装配;RigidBody/ScriptComponent
// 分别由 PhysicsWorld/ScriptRuntime 下一帧对账消化
//
// 【选型约定:Spawn vs Instantiate】(Unity/UE 同题,靠约定
// 治理,判定只有一问):
// "这东西的参数要不要让非程序员改?"
// 要 → Save As Prefab + Instantiate(数据 = 真理来源)
// 不要 → Spawn(代码 = 真理来源:子弹/特效触发器等纯逻辑体)
// 三条纪律:
// 1. 一个对象只走一条路径——prefab 化后 Spawn 版本撤注册;
// 2. Spawn 的脚本构造保持"零参数搭建":发现 Spawn 后手动
// set 一堆组件 = 该 prefab 化的信号;
// 3. 注册表即白名单(ENGINE_REGISTER_SCRIPT 的类才能按名
// spawn),别为绕过约定注册"伪逻辑体"。
ECS::EntityHandle Spawn(const std::string& name);
// destroy:延迟——入队,帧尾 FlushDestroyQueue 统一执行
//(遍历安全);调用方持有的句柄此后悬空
void Destroy(ECS::Entity e);
// ---- Prefab 实例化(Unity Object.Instantiate 同位)----
// .prefab 文件 → 整棵子树追加进场景(UUID 全量再生,子树内
// Joint.target 自动重映射)。pos/rot = 根实体 local 位姿
//(根挂场景根时即世界位姿)。失败返回 entt::null;
// RigidBody/ScriptComponent 由 PhysicsWorld/ScriptRuntime
// 下一帧对账消化(与 Spawn 同语义)
ECS::Entity Instantiate(const std::string& prefabPath,
const glm::vec3& pos,
const glm::quat& rot = glm::quat{});
// ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)----
// 自身组件总开关(整个脚本组件停 tick,实例保留)
void SetActive(bool on) {
@@ -236,7 +285,12 @@ namespace Engine {
} // namespace Engine
// 文件级自注册:脚本 cpp 里写一行即可,Inspector 下拉与序列化按
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior。
//
// 注册 = UE 的 UCLASS(Blueprintable) 表态:本类声明为"逻辑体"——
// 允许运行时按名 Spawn(脚本 Spawn()/运行时注册表白名单)。需要
// 美术调参/编辑器复用的组合体不要注册(走 Save As Prefab +
// Instantiate 路径),防止同一对象出现 Spawn/prefab 两条权威链
#define ENGINE_REGISTER_SCRIPT(T) \
static const bool s_##T##_registered = [] { \
::Engine::Scripting::ScriptRegistry::Register( \
@@ -66,6 +66,10 @@ namespace Engine {
private:
void Sync();
// 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近
// 一次步进的事件转发到涉事实例的 OnCollisionEnter/Exit。
// Persisted 跳过;实例未建/已禁用不收
void DispatchCollisions();
ECS::Scene& m_Scene;
Application* m_App; // 注入脚本上下文
@@ -23,6 +23,9 @@
#ifndef HELLOVULKAN_SCENESERIALIZER_H
#define HELLOVULKAN_SCENESERIALIZER_H
#include <Engine/ECS/Scene.h> // Entity + glm vec3/quat(Prefab API 签名用)
#include <Engine/Core/AssetRef.h> // ModelRef(ResolveProcedural 返回值)
#include <string>
namespace Engine::ECS { class Scene; }
@@ -66,6 +69,38 @@ namespace Engine::Serialization {
static bool DeserializeFromString(ECS::Scene& scene,
Assets::AssetManager& assets,
const std::string& yamlText);
// ---- Prefab(实体子树存取;与场景共用实体段读写,实现见 .cpp)----
// 保存 root 子树(根 + 全部后代,DFS 序:父先于子)到 outPath。
// 格式 = 场景实体段子集:"Prefab:" 文档头 + Version + Entities
// 列表(组件段与场景逐键一致)。首实体 = 根(实例化约定)。
// 失败(根无效/无法写文件)返回 false
static bool SerializePrefab(const ECS::Scene& scene,
Assets::AssetManager& assets,
ECS::Entity root,
const std::string& outPath);
// 实例化 Prefab 到 scene(追加式,不换血)。UUID 全量再生——
// 同一 prefab 多次实例化、与场景既有实体均不冲突;子树内
// Joint.target 按旧→新重映射,指向子树外的引用保持原值。
// overridePos/overrideRot 非空时覆写根实体 local 位姿(根挂
// 场景根时即世界位姿);空指针 = 保留 prefab 授权位姿。
// 返回根实体(= 文件首实体);失败(解析失败/无实体)返回
// entt::null——逐实体容错与场景加载同语义,失败前可能已部分
// 实例化(单实体损坏只跳过该项)
static ECS::Entity InstantiatePrefab(
ECS::Scene& scene, Assets::AssetManager& assets,
const std::string& inPath,
const glm::vec3* overridePos = nullptr,
const glm::quat* overrideRot = nullptr);
// procedural:// 伪路径 → 参数反解重建(cube/plane/
// transparent-plane)。确定性键必然命中缓存;绝不走 assimp。
// 未知/格式坏伪路径返回无效句柄。场景加载与编辑器程序化
// 入口共用——键格式唯一定义点
static ModelRef ResolveProcedural(
Assets::AssetManager& assets, const std::string& path);
};
} // namespace Engine::Serialization
@@ -31,6 +31,10 @@ namespace Engine {
class Device;
// 显存估算(按格式字节宽与 mip 链逐级累加;BC 压缩按 4x4 块计)——
// 分配器台账与 Texture 资源台账共用同一算法,两边永不漂移
u64 EstimateImageBytes(vk::Format fmt, u32 w, u32 h, u32 depth, u32 mips);
class MemoryAllocator {
public:
explicit MemoryAllocator(Device& device);
+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;
};
+528 -113
View File
@@ -1,17 +1,42 @@
//
// AnimationAttach 实现:外部动画资产 → 目标模型并入。
//
// 轨道映射两级:
// ① 同名直映——同源拆分资产(角色/动画同一骨架导出)零损耗;
// ② Humanoid 槽位重定向——名字不同源的组装资产(角色 FBX +
// 动画 FBX 各自制作),经 Unity Avatar 语义的标准人形槽位配对,
// 逐 key 烘焙"rest→anim 的增量变换"到目标骨架局部空间:
// 平移 out.pos = dstRestPos + (srcPos - srcRestPos)
// 旋转 Δ = srcRot * inv(srcRestRot);out.rot = Δ * dstRestRot
// (Δ 父空间左乘 = 骨骼自身摆动在两套骨架间等价传递)
// 烘焙后 clip 即目标骨架空间数据,评估器零改动。
// 五段管线(映射表一次构建后**冻结**,逐 clip 只读消费,杜绝循环内
// 惰性登记/反向表重建):
//
// AssetManager 是 facade:取模型 + 解析磁盘,纯算法在这里。
// ① 资产导入(Model.cpp) clip 命名与重定向解耦(文件 stem 真名)
// ② 映射表(HumanoidRig) 同名直映 / 52 槽配对 → 冻结 Plan
// ③ 根运动通道 骨架上方"承载节点"(未识别的映射骨祖先)
// 取最深者 → dst 根,世界增量 Δ·RdstRest
// ④ 关节烘焙 两遍式 T-pose 参考搬运(见下数学);手指/
// 趾与主干同通道(Unity/UE 语义:指骨即普通
// 骨骼,无独立局部增量通道——局部系约定免疫)
//
// 关键不变量(本次重写的核心,此前版本违反即整体崩坏):
// * Plan 构建后不可变;不存在 clip 循环内的 emplace 登记
// * 每个 dst 节点至多一条轨道(owner 去重)——杜绝同节点重复轨道
// * "承载节点"= 未识别的骨骼祖先(骨架上方),恒不参与同名直搬;
// 判定基于**槽位识别**而非"所有映射骨祖先"(后者会把 hips/spine
// 等内部骨全部误判成承载节点 → 大半骨架走烘焙而叶走直搬 → 局部
// 化基准错位、全身崩坏,即用户反馈"全部是崩溃的"的根因)
// * 仅"跨骨架对"(两侧骨名不同)走烘焙;同名对零损耗原样直搬
// * 未动画的 src 骨不发旋转键(目标保持 rest,不被 T-pose 零位拉走)
//
// 数学(旋转,两遍式):
// 零位 = src 侧 T-pose 参考 RsT(EstimateHumanoidTPose,两侧对称)
// 世界增量 dW = A·(Rsrc(t)·RsT⁻¹)·A⁻¹
// 目标世界 RwI = dW·RdT(dst)
// 键 = inv(Rw[父])·RwI(父用 anim 后世界旋转)
// rest 即 T-pose 的资产自动退化(dW 恒等 → 与旧直搬等价);A 为
// src 世界 → dst 世界的行走基对齐(EstimateHumanoidRootAlignment)
//
// 数学(平移):
// Root : dstRest + A·(srcPos - srcRest)·unitScale(整体尺度)
// Joint : dstRest + M·(srcPos - srcRest)·ratio(per-骨长比)
// M = inv(dstParW)·A·srcParW(局部增量系转换),退化取 unitScale
//
// 纯算法在 AttachClipsToNodes(headless 可直测);
// AttachClipsToModel 是 facade:取模型 nodes/clips + 回填。
//
#include <Engine/Animation/AnimationAttach.h>
@@ -28,11 +53,15 @@
#include <filesystem>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Engine::Animation {
namespace {
constexpr u32 kNoNode = ~0u;
const glm::quat kIdentity(1.0f, 0.0f, 0.0f, 0.0f);
// mat4 → TRS 分解(rest 局部变换;引擎 node 无 shear)
struct TRS {
glm::vec3 pos{};
@@ -47,8 +76,7 @@ namespace Engine::Animation {
const glm::vec3 colX(m[0]);
const glm::vec3 colY(m[1]);
const glm::vec3 colZ(m[2]);
t.scl = {glm::length(colX), glm::length(colY),
glm::length(colZ)};
t.scl = {glm::length(colX), glm::length(colY), glm::length(colZ)};
if (t.scl.x > 1e-8f && t.scl.y > 1e-8f && t.scl.z > 1e-8f) {
t.rot = glm::quat_cast(glm::mat3(
colX / t.scl.x, colY / t.scl.y, colZ / t.scl.z));
@@ -56,14 +84,336 @@ namespace Engine::Animation {
return t;
}
// 世界矩阵 → 旋转(列归一化抗缩放;退化列取 identity)
glm::quat RotOf(const glm::mat4& m)
{
const glm::vec3 cx(m[0]);
const glm::vec3 cy(m[1]);
const glm::vec3 cz(m[2]);
if (glm::length(cx) < 1e-8f || glm::length(cy) < 1e-8f ||
glm::length(cz) < 1e-8f) {
return kIdentity;
}
return glm::quat_cast(glm::mat3(
glm::normalize(cx), glm::normalize(cy), glm::normalize(cz)));
}
// rest 全骨架世界矩阵(node 表父先子后,直接链乘)
std::vector<glm::mat4> RestGlobals(const std::vector<ModelNode>& nodes)
{
std::vector<glm::mat4> g(nodes.size());
for (size_t i = 0; i < nodes.size(); ++i) {
const i32 p = nodes[i].parent;
g[i] = (p >= 0 ? g[static_cast<size_t>(p)]
: glm::mat4(1.0f)) *
nodes[i].localTransform;
}
return g;
}
// ---- 键采样(烘焙用;线性/slerp,rest 兜底)----
glm::vec3 SampleV(const std::vector<PosKey>& ks, float t,
const glm::vec3& def)
{
if (ks.empty()) return def;
if (t <= ks.front().time) return ks.front().value;
for (size_t i = 1; i < ks.size(); ++i) {
if (t <= ks[i].time) {
const f32 a = (t - ks[i - 1].time) /
(ks[i].time - ks[i - 1].time);
return glm::mix(ks[i - 1].value, ks[i].value, a);
}
}
return ks.back().value;
}
glm::vec3 SampleV(const std::vector<SclKey>& ks, float t,
const glm::vec3& def)
{
if (ks.empty()) return def;
if (t <= ks.front().time) return ks.front().value;
for (size_t i = 1; i < ks.size(); ++i) {
if (t <= ks[i].time) {
const f32 a = (t - ks[i - 1].time) /
(ks[i].time - ks[i - 1].time);
return glm::mix(ks[i - 1].value, ks[i].value, a);
}
}
return ks.back().value;
}
glm::quat SampleQ(const std::vector<RotKey>& ks, float t,
const glm::quat& def)
{
if (ks.empty()) return def;
if (t <= ks.front().time) return ks.front().value;
for (size_t i = 1; i < ks.size(); ++i) {
if (t <= ks[i].time) {
const f32 a = (t - ks[i - 1].time) /
(ks[i].time - ks[i - 1].time);
return glm::slerp(ks[i - 1].value, ks[i].value, a);
}
}
return ks.back().value;
}
// 源 clip 于时刻 t 的全骨架世界矩阵(层级 T·R·S 链乘)
std::vector<glm::mat4> SrcWorldAt(
const std::vector<ModelNode>& nodes,
const std::vector<const AnimChannel*>& byNode, float t)
{
std::vector<glm::mat4> world(nodes.size());
for (size_t i = 0; i < nodes.size(); ++i) {
const TRS rest = Decompose(nodes[i].localTransform);
const AnimChannel* ch =
i < byNode.size() ? byNode[i] : nullptr;
const glm::vec3 p =
ch ? SampleV(ch->positions, t, rest.pos) : rest.pos;
const glm::quat r =
ch ? SampleQ(ch->rotations, t, rest.rot) : rest.rot;
const glm::vec3 s =
ch ? SampleV(ch->scales, t, rest.scl) : rest.scl;
const glm::mat4 local = glm::translate(glm::mat4(1.0f), p) *
glm::mat4_cast(r) *
glm::scale(glm::mat4(1.0f), s);
const i32 pi = nodes[i].parent;
world[i] = (pi >= 0 ? world[static_cast<size_t>(pi)]
: glm::mat4(1.0f)) * local;
}
return world;
}
bool ChannelEmpty(const AnimChannel& ch)
{
return ch.positions.empty() && ch.rotations.empty() &&
ch.scales.empty();
}
// 槽位识别(未命中 → Count)
HumanoidSlot SlotAt(const std::vector<ModelNode>& nodes, u32 i)
{
Side side = Side::None;
bool ok = false;
const HumanoidSlot s = MatchHumanoidSlot(nodes[i].name, side, ok);
return ok ? s : HumanoidSlot::Count;
}
// ---- 轨道归类 ----
enum class Kind : u8 {
Skip, // 无对应(道具/辅助骨)
Direct, // 同名直搬(同源拆分资产,原样回放)
Joint, // 跨骨架关节烘焙(含手指/趾:统一世界增量通道)
Root, // 根运动(骨架上方承载节点 → dst 根)
};
struct SrcSlot {
Kind kind = Kind::Skip;
u32 dst = kNoNode;
// 平移烘焙元数据
glm::quat M{kIdentity}; // inv(dstParW)·A·srcParW
glm::vec3 dstRestPos{};
f32 posScale = 1.0f; // 骨长比(Root 用 unitScale)
};
// 冻结的重定向计划(构建后不可变)
struct Plan {
std::vector<SrcSlot> bySrc; // index = src node
std::vector<u32> carriers; // 承载节点(骨架上方,浅→深)
std::vector<glm::quat> srcTPose;
std::vector<glm::quat> dstTPose;
glm::quat align{kIdentity}; // src 世界 → dst 世界
f32 unitScale = 1.0f; // src→dst 整体尺度
u32 dstRoot = kNoNode;
bool needsBake = false;
};
// 骨长尺度(hips→head 世界距离;槽位不全 → 0)
f32 SkeletonSpan(const std::vector<ModelNode>& nodes,
const std::vector<glm::mat4>& g)
{
i32 hips = -1;
i32 head = -1;
for (u32 i = 0; i < nodes.size(); ++i) {
const HumanoidSlot slot = SlotAt(nodes, i);
if (slot == HumanoidSlot::Hips && hips < 0) {
hips = static_cast<i32>(i);
} else if (slot == HumanoidSlot::Head && head < 0) {
head = static_cast<i32>(i);
}
}
if (hips < 0 || head < 0) return 0.0f;
return glm::length(glm::vec3(g[static_cast<size_t>(head)][3]) -
glm::vec3(g[static_cast<size_t>(hips)][3]));
}
// 构建冻结计划:归类每个 src 轨道 + 预计算几何补偿元数据
Plan BuildPlan(const std::vector<ModelNode>& srcNodes,
const std::vector<ModelNode>& dstNodes)
{
Plan pl;
pl.bySrc.assign(srcNodes.size(), SrcSlot{});
// dst 根(node 0 即 DFS 首节点)
for (u32 i = 0; i < dstNodes.size(); ++i) {
if (dstNodes[i].parent < 0) {
pl.dstRoot = i;
break;
}
}
// 跨骨架配对(AutoMapHumanoid:同名直映 + 槽位一对一)
const auto pairs = AutoMapHumanoid(srcNodes, dstNodes);
std::vector<u8> slotBone(srcNodes.size(), 0);
for (const auto& [si, di] : pairs) {
(void)di;
if (SlotAt(srcNodes, si) != HumanoidSlot::Count) {
slotBone[si] = 1;
}
}
// 承载节点 = 已识别槽位骨的祖先里**未识别**者(骨架上方根
// 运动承载链)。判定必须基于槽位识别而非"所有映射骨祖先"
// ——后者把 hips/spine/… 全部内部骨误判进来,导致大半骨架
// 走烘焙、叶走直搬,局部化基准错位(整体崩坏根因)
std::vector<u8> isCarrier(srcNodes.size(), 0);
for (u32 si = 0; si < srcNodes.size(); ++si) {
if (!slotBone[si]) continue;
for (i32 p = srcNodes[si].parent; p >= 0;
p = srcNodes[static_cast<size_t>(p)].parent) {
const u32 pu = static_cast<u32>(p);
if (!slotBone[pu]) isCarrier[pu] = 1;
}
}
for (u32 i = 0; i < srcNodes.size(); ++i) {
if (isCarrier[i]) pl.carriers.push_back(i);
}
// owner:每个 dst 至多一条轨道(跨骨架对优先占用)
std::vector<u32> owner(dstNodes.size(), kNoNode);
// ① 跨骨架对分类(含手指/趾,统一 → Joint 世界增量烘焙)
std::vector<std::pair<u32, u32>> directPairs; // (si,di) 同名
for (const auto& [si, di] : pairs) {
if (isCarrier[si]) continue; // 承载节点另归
if (srcNodes[si].name == dstNodes[di].name) {
directPairs.emplace_back(si, di); // 待 ② 定夺
continue;
}
if (owner[di] != kNoNode) continue; // 已被占
SrcSlot s;
s.dst = di;
// 手指/趾与主干同走世界增量烘焙——Unity/UE 语义:指骨即
// 普通骨骼,无独立通道。世界增量对骨骼局部轴向约定免疫:
// 此前的"指根局部增量"以手骨局部系为基准做共轭,而
// Mixamo/Rigify/UE 的手骨轴向约定互异(沿 +X vs 沿 +Y),
// 共轭后弯曲轴被拧转 → X Bot 指弯向手背、kwy 指呈爪状
//(用户实测)。世界增量只用两侧世界旋转 + 行走基对齐 A
s.kind = Kind::Joint;
owner[di] = si;
pl.bySrc[si] = s;
}
// ② 同名对 → 直搬(仅当目标未被跨骨架对占用;承载节点已排除)
for (const auto& [si, di] : directPairs) {
if (owner[di] != kNoNode) continue;
SrcSlot s;
s.kind = Kind::Direct;
s.dst = di;
pl.bySrc[si] = s;
owner[di] = si;
}
// ③ 承载节点 → 根运动(恒不直搬)。仅当 dst 根未被映射骨占用
// 时才需要独立通道:dst 根若本身是映射骨(Mixamo 的
// mixamorig:Hips 即根),其绝对世界增量已含承载链运动,
// 独立通道反而与之重复
if (pl.dstRoot != kNoNode && owner[pl.dstRoot] == kNoNode) {
for (const u32 c : pl.carriers) {
SrcSlot s;
s.kind = Kind::Root;
s.dst = pl.dstRoot;
pl.bySrc[c] = s;
}
}
// 是否需要几何烘焙(纯同名资产走快速直搬路径,零开销)
for (const auto& s : pl.bySrc) {
if (s.kind == Kind::Joint || s.kind == Kind::Root) {
pl.needsBake = true;
break;
}
}
if (!pl.needsBake) return pl;
// ---- 几何补偿元数据(A / T-pose / 平移尺度)----
const auto srcG = RestGlobals(srcNodes);
const auto dstG = RestGlobals(dstNodes);
pl.align = EstimateHumanoidRootAlignment(srcNodes, dstNodes);
pl.srcTPose = EstimateHumanoidTPose(srcNodes, srcG);
pl.dstTPose = EstimateHumanoidTPose(dstNodes, dstG);
const f32 srcSpan = SkeletonSpan(srcNodes, srcG);
const f32 dstSpan = SkeletonSpan(dstNodes, dstG);
pl.unitScale = srcSpan > 1e-6f ? dstSpan / srcSpan : 1.0f;
for (u32 si = 0; si < srcNodes.size(); ++si) {
SrcSlot& s = pl.bySrc[si];
if (s.kind == Kind::Skip) continue;
const u32 di = s.dst;
// 平移元数据
const i32 sp = srcNodes[si].parent;
const i32 dp = dstNodes[di].parent;
const glm::quat srcParW =
sp >= 0 ? RotOf(srcG[static_cast<size_t>(sp)]) : kIdentity;
const glm::quat dstParW =
dp >= 0 ? RotOf(dstG[static_cast<size_t>(dp)]) : kIdentity;
s.M = glm::inverse(dstParW) * pl.align * srcParW;
s.dstRestPos = Decompose(dstNodes[di].localTransform).pos;
if (s.kind == Kind::Root) {
s.posScale = pl.unitScale;
} else {
const f32 srcLen =
glm::length(Decompose(srcNodes[si].localTransform).pos);
const f32 dstLen =
glm::length(s.dstRestPos);
s.posScale = srcLen > 1e-6f ? dstLen / srcLen
: pl.unitScale;
}
}
return pl;
}
// 唯一 clip 名(同名冲突逐轮加源文件名前缀)
std::string UniqueName(const std::string& base,
const std::string& stem,
const std::vector<AnimationClip>& existing,
const std::vector<AnimationClip>& pending)
{
const auto collides = [&](const std::string& n) {
for (const auto& c : existing) {
if (c.name == n) return true;
}
for (const auto& c : pending) {
if (c.name == n) return true;
}
return false;
};
std::string name = base;
while (collides(name)) name = stem + "_" + name;
return name;
}
} // namespace
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
const std::string& sourcePath)
AttachResult AttachClipsToNodes(
const std::vector<ModelNode>& dstNodes,
const std::vector<AnimationClip>& existingClips,
ParsedModel&& src, const std::string& sourcePath,
std::vector<AnimationClip>& outExtra)
{
AttachResult res;
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
for (const auto& c : target.GetClips()) {
for (const auto& c : existingClips) {
if (c.sourcePath == sourcePath) {
return res;
}
@@ -73,150 +423,215 @@ namespace Engine::Animation {
return res;
}
const std::vector<ModelNode>& dstNodes = target.GetNodes();
const std::vector<ModelNode>& srcNodes = src.nodes;
// ① 同名直映表
std::unordered_map<std::string, u32> nameToIdx;
nameToIdx.reserve(dstNodes.size());
for (u32 i = 0; i < dstNodes.size(); ++i) {
nameToIdx.emplace(dstNodes[i].name, i);
}
// ② Humanoid 槽位映射表(src nodeIdx → dst nodeIdx)
std::unordered_map<u32, u32> retargetMap;
std::unordered_map<u32, HumanoidSlot> retargetSlot;
{
const auto pairs = AutoMapHumanoid(srcNodes, dstNodes);
for (const auto& [si, di] : pairs) {
retargetMap.emplace(si, di);
// 槽位回查(明细行显示用):源骨骼名识别即可
Side side;
bool ok = false;
retargetSlot.emplace(si, MatchHumanoidSlot(
srcNodes[si].name, side, ok));
}
}
// 重定向骨架的 rest(逐 nodeIdx 缓存,避免每 key 重分解)
std::unordered_map<u32, TRS> dstRestCache;
dstRestCache.reserve(retargetMap.size());
for (const auto& [si, di] : retargetMap) {
dstRestCache.emplace(si, Decompose(dstNodes[di].localTransform));
}
const Plan pl = BuildPlan(srcNodes, dstNodes);
const std::string stem =
std::filesystem::path(sourcePath).stem().string();
std::vector<AnimationClip> extra;
extra.reserve(src.clips.size());
for (auto& clip : src.clips) {
AnimationClip out;
out.name = clip.name;
out.name = UniqueName(clip.name, stem, existingClips, outExtra);
out.duration = clip.duration;
out.tps = clip.tps;
out.sourcePath = sourcePath;
// 同名冲突(内嵌 clip 或其它源):源文件名循环加前缀直到
// 唯一。单轮前缀在"前缀名也撞"(如两次 attach 同名资产)时
// 仍会产出重复名——Controller/脚本按 clip 名引用,重名即歧义
// 每 clip 的 src 轨道索引表(按 nodeIdx)
std::vector<const AnimChannel*> srcByNode(srcNodes.size(),
nullptr);
for (const auto& ch : clip.channels) {
if (ch.nodeIdx < srcByNode.size() && !ChannelEmpty(ch)) {
srcByNode[ch.nodeIdx] = &ch;
}
}
// ---- 本 clip 的根运动源:承载节点里最深且有轨道者 ----
// (更深者世界变换已含其全部祖先运动,取一个即全包括)
u32 rootSrc = kNoNode;
for (const u32 c : pl.carriers) { // carriers 浅→深
if (srcByNode[c] != nullptr) rootSrc = c;
}
// ---- 目标节点 → 源轨道(可控节点,供烘焙局部化)----
std::unordered_map<u32, u32> jointBakeDst; // dst → src(Joint/Root)
std::unordered_map<u32, u32> directDst; // dst → src(Direct)
for (u32 si = 0; si < srcNodes.size(); ++si) {
const SrcSlot& s = pl.bySrc[si];
if (srcByNode[si] == nullptr) continue;
switch (s.kind) {
case Kind::Direct:
directDst.emplace(s.dst, si);
break;
case Kind::Joint:
jointBakeDst.emplace(s.dst, si);
break;
case Kind::Root:
if (si == rootSrc) jointBakeDst.emplace(s.dst, si);
break;
default:
break;
}
}
// ---- 旋转键:两遍式烘焙 ----
// 采样时刻 = 全部需烘焙通道(Joint/Root)的旋转键时刻并集;
// 逐 t 跑源全骨架 FK → 逐 dst 递推世界旋转 → 局部化写键
std::unordered_map<u32, std::vector<RotKey>> baked;
{
const auto collides = [&](const std::string& n) {
for (const auto& c : target.GetClips()) {
if (c.name == n) return true;
std::vector<f32> times;
for (u32 si = 0; si < srcNodes.size(); ++si) {
const SrcSlot& s = pl.bySrc[si];
const AnimChannel* ch = srcByNode[si];
if (ch == nullptr) continue;
const bool bakeThis =
s.kind == Kind::Joint ||
(s.kind == Kind::Root && si == rootSrc);
if (!bakeThis || ch->rotations.empty()) continue;
for (const auto& k : ch->rotations) times.push_back(k.time);
}
for (const auto& c : extra) {
if (c.name == n) return true;
std::sort(times.begin(), times.end());
times.erase(std::unique(times.begin(), times.end()),
times.end());
if (!times.empty()) {
std::vector<glm::quat> Rw(dstNodes.size(), kIdentity);
for (const f32 t : times) {
const auto Wsrc = SrcWorldAt(srcNodes, srcByNode, t);
for (u32 i = 0; i < dstNodes.size(); ++i) {
const i32 pi = dstNodes[i].parent;
const glm::quat Rpar =
pi >= 0 ? Rw[static_cast<size_t>(pi)]
: kIdentity;
glm::quat RwI;
const auto jb = jointBakeDst.find(i);
if (jb != jointBakeDst.end()) {
// 关节/根/手指/趾:世界增量搬运(对两侧
// 骨骼的局部轴向约定免疫)
const u32 si = jb->second;
const glm::quat dW =
pl.align *
(RotOf(Wsrc[si]) *
glm::inverse(pl.srcTPose
[static_cast<size_t>(si)])) *
glm::inverse(pl.align);
RwI = dW * pl.dstTPose[static_cast<size_t>(i)];
} else if (auto db = directDst.find(i);
db != directDst.end()) {
// 直搬骨:anim 后世界 = 父 × 源局部(供其子
// 关节局部化用;自身轨道另走原样直搬)
const u32 si = db->second;
const glm::quat loc = SampleQ(
srcByNode[si]->rotations, t,
Decompose(srcNodes[si].localTransform).rot);
RwI = Rpar * loc;
} else {
RwI = Rpar * Decompose(
dstNodes[i].localTransform).rot;
}
return false;
};
while (collides(out.name)) {
out.name = stem + "_" + out.name;
Rw[static_cast<size_t>(i)] = RwI;
if (jb != jointBakeDst.end()) {
baked[i].push_back(
{t, glm::inverse(Rpar) * RwI});
}
}
}
}
}
// ---- 组装轨道(每 dst 至多一条;owner 在 Plan 阶段已保证)----
// 非 const 引用:同名直搬路径整段 move 源轨道(此后 src 不再
// 被读——烘焙已在上面完成)
for (auto& ch : clip.channels) {
if (ch.positions.empty() && ch.rotations.empty() &&
ch.scales.empty()) {
continue; // 空轨道无用
}
if (ChannelEmpty(ch)) continue;
if (ch.nodeIdx >= srcNodes.size()) {
++res.channelsSkipped;
continue;
}
const SrcSlot& s = pl.bySrc[ch.nodeIdx];
const std::string& boneName = srcNodes[ch.nodeIdx].name;
AnimChannel remapped;
if (auto it = nameToIdx.find(boneName);
it != nameToIdx.end()) {
// ① 同名直搬
remapped.nodeIdx = it->second;
remapped.positions = std::move(ch.positions);
remapped.rotations = std::move(ch.rotations);
remapped.scales = std::move(ch.scales);
out.channels.push_back(std::move(remapped));
if (s.kind == Kind::Skip) {
++res.channelsSkipped;
continue;
}
if (s.kind == Kind::Root && ch.nodeIdx != rootSrc) {
continue; // 更深的承载节点已承载根运动,其余并入
}
AnimChannel outCh;
outCh.nodeIdx = s.dst;
if (s.kind == Kind::Direct) {
// 同名直搬:原样(同资产体系局部空间同源)
outCh.positions = std::move(ch.positions);
outCh.rotations = std::move(ch.rotations);
outCh.scales = std::move(ch.scales);
out.channels.push_back(std::move(outCh));
++res.channelsMatched;
continue;
}
// ② 槽位重定向:增量烘焙到目标骨架局部空间。
// 平移增量按骨长比缩放(|dstRest| / |srcRest|)——
// 两资产单位/比例不同源(FBX 常为 cm、glTF 为 m)时
// 不缩放会把模型拽飞;rest≈0 的根骨骼比例取 1
const auto mapIt = retargetMap.find(ch.nodeIdx);
if (mapIt == retargetMap.end()) {
++res.channelsSkipped; // 两级都落不上(手指/道具等)
continue;
}
const TRS srcRest = Decompose(
srcNodes[ch.nodeIdx].localTransform);
const TRS& dstRest = dstRestCache[ch.nodeIdx];
remapped.nodeIdx = mapIt->second;
const f32 srcLen = glm::length(srcRest.pos);
const f32 ratio =
(srcLen > 1e-6f) ? (glm::length(dstRest.pos) / srcLen)
: 1.0f;
remapped.positions.reserve(ch.positions.size());
} else {
// 平移:局部增量 × 尺度
const TRS srcRest =
Decompose(srcNodes[ch.nodeIdx].localTransform);
outCh.positions.reserve(ch.positions.size());
for (const auto& k : ch.positions) {
remapped.positions.push_back(
{k.time, dstRest.pos +
(k.value - srcRest.pos) * ratio});
outCh.positions.push_back(
{k.time, s.dstRestPos +
(s.M * (k.value - srcRest.pos)) *
s.posScale});
}
remapped.rotations.reserve(ch.rotations.size());
for (const auto& k : ch.rotations) {
const glm::quat delta =
k.value * glm::inverse(srcRest.rot);
remapped.rotations.push_back(
{k.time, delta * dstRest.rot});
// 旋转:烘焙结果(多源汇一 dst 时共享同份)
if (auto bk = baked.find(s.dst); bk != baked.end()) {
outCh.rotations = bk->second;
}
remapped.scales = std::move(ch.scales);
out.channels.push_back(std::move(remapped));
// 缩放不搬运:跨骨架源的缩放轨道常承载单位换算
// (kwy 根节点的 cm 补偿等),原样拷到目标会整体放大
// 骨骼;目标保持自身 rest 缩放(人体动画一般无缩放)
out.channels.push_back(std::move(outCh));
++res.channelsRetargeted;
// 明细行(同骨骼多 clip 去重)
const bool newRow = std::none_of(
res.retargetRows.begin(), res.retargetRows.end(),
[&](const RetargetRow& r)
{ return r.srcName == boneName; });
if (newRow) {
const HumanoidSlot slot = SlotAt(srcNodes, ch.nodeIdx);
res.retargetRows.push_back(
{HumanoidSlotName(retargetSlot[ch.nodeIdx]),
boneName, dstNodes[mapIt->second].name});
{s.kind == Kind::Root
? "RootMotion"
: HumanoidSlotName(slot),
boneName, dstNodes[s.dst].name});
}
}
// 全部轨道落不上 = 无可驱动骨骼,clip 不入表
}
if (out.channels.empty()) {
ENGINE_WARN("AttachClipsToModel: clip '{}' has no matching "
"bones in target (skipped)",
clip.name);
continue;
}
extra.push_back(std::move(out));
outExtra.push_back(std::move(out));
++res.clipsAdded;
}
if (!extra.empty()) {
target.AppendClips(std::move(extra));
}
if (!outExtra.empty()) {
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
"({} named, {} retargeted, {} skipped)",
sourcePath, res.clipsAdded, res.channelsMatched,
res.channelsRetargeted, res.channelsSkipped);
}
return res;
}
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
const std::string& sourcePath)
{
std::vector<AnimationClip> extra;
AttachResult res = AttachClipsToNodes(
target.GetNodes(), target.GetClips(), std::move(src),
sourcePath, extra);
if (!extra.empty()) {
target.AppendClips(std::move(extra));
}
return res;
}
+46 -9
View File
@@ -44,25 +44,62 @@ namespace Engine::Animation {
}
}
if (r <= 1e-4f || ch.empty()) return;
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π),wrap 补全;方向子
// ≤16 个,栈数组零堆分配)
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π);方向子 ≤16 个,
// 栈数组零堆分配)
constexpr f32 kTwoPi = 6.2831853f;
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
u32 idx[16];
f32 ang[16];
constexpr u32 kMaxDir = 16;
u32 idx[kMaxDir];
f32 ang[kMaxDir];
u32 nd = 0;
for (u32 i = 0; i < ch.size() && nd < 16; ++i) {
for (u32 i = 0; i < ch.size() && nd < kMaxDir; ++i) {
if (glm::length(ch[i].pos) < 1e-4f) continue;
idx[nd] = i;
ang[nd] = glm::mod(glm::atan(ch[i].pos.y, ch[i].pos.x), kTwoPi);
++nd;
}
if (nd == 0) return;
// 环序自排序(插入排序,nd≤16 就地零堆分配):扇区数学假设
// "数组相邻 = 角度环相邻",输入顺序不可信——编辑器实时改树
// 不重跑 Load 期排序,乱序时右侧拖拽会配到错误的扇区邻居
for (u32 i = 1; i < nd; ++i) {
const u32 ki = idx[i];
const f32 ai = ang[i];
u32 j = i;
while (j > 0 && ang[j - 1] > ai) {
idx[j] = idx[j - 1];
ang[j] = ang[j - 1];
--j;
}
idx[j] = ki;
ang[j] = ai;
}
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
if (nd == 1) {
// 单方向子:无扇区可插值,全权重归它(否则角度不等于
// 该方向时落空退化成纯 idle)
out.push_back({idx[0], r});
return;
}
for (u32 i = 0; i < nd; ++i) {
const u32 j = (i + 1) % nd;
const f32 ai = ang[i];
const f32 aj = (j == 0) ? ang[0] + kTwoPi : ang[j];
if (theta >= ai && theta <= aj) {
const f32 span = aj - ai;
if (j == 0) {
// wrap 扇区 [ai,2π)∪[0,ang0]:用 || 连接,&& 会让
// theta ∈ [0,ang0) 落空(首方向不在 0° 时拖右侧无匹配)
if (theta >= ai || theta <= ang[0]) {
const f32 span = ang[0] + kTwoPi - ai;
const f32 tt =
theta >= ai ? theta - ai : theta + kTwoPi - ai;
const f32 t = span > 1e-6f ? tt / span : 0.0f;
const f32 wi = (1.0f - t) * r;
const f32 wj = t * r;
if (wi >= kMinBlendWeight) out.push_back({idx[i], wi});
if (wj >= kMinBlendWeight)
out.push_back({idx[0], wj});
break;
}
} else if (theta >= ai && theta <= ang[j]) {
const f32 span = ang[j] - ai;
const f32 t = span > 1e-6f ? (theta - ai) / span : 0.0f;
const f32 wi = (1.0f - t) * r;
const f32 wj = t * r;
+23 -3
View File
@@ -128,7 +128,8 @@ namespace Engine::Animation {
void AnimationEvaluator::Evaluate(const Model& model,
const AnimationClip& cur, f32 curTime,
const CrossfadeState& xf,
std::vector<glm::mat4>& outGlobals)
std::vector<glm::mat4>& outGlobals,
bool inPlace)
{
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
// 过渡共用同一套加权数学)。两权重和恒为 1,渐进归一化后精确
@@ -141,13 +142,14 @@ namespace Engine::Animation {
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
count = 2;
}
EvaluateLayers(model, layers, count, outGlobals);
EvaluateLayers(model, layers, count, outGlobals, inPlace);
}
void AnimationEvaluator::EvaluateLayers(const Model& model,
const PoseLayer* layers,
u32 count,
std::vector<glm::mat4>& outGlobals)
std::vector<glm::mat4>& outGlobals,
bool inPlace)
{
// ---- 逐层采样(容量跨帧复用;nullptr/零权重层跳过)----
if (m_Layers.size() < count) {
@@ -208,6 +210,24 @@ namespace Engine::Animation {
acc += m_Sampled[l].weight;
}
}
// ---- 根运动裁剪(inPlace,Unity "Apply Root Motion" 关闭同
// 语义):动画子树根节点(touched 且父未被触碰)位移回
// 静止值,旋转/缩放保留——跑动 clip 不再漂出实体锚点,
// 位移归脚本/物理驱动。混合后统一裁剪,crossfade 双层
// 与 BlendTree N 层一个入口全覆盖
if (inPlace) {
const auto& nodes = model.GetNodes();
for (size_t k = 0; k < nodes.size(); ++k) {
if (!m_A.touched[k]) continue;
const i32 parent = nodes[k].parent;
if (parent < 0 ||
!m_A.touched[static_cast<size_t>(parent)]) {
// FBX PreRotation 折在 localTransform 里,静止位移
// 直接取平移列(不拆 TRS 重建,避免丢 PreRotation)
m_A.pos[k] = glm::vec3(nodes[k].localTransform[3]);
}
}
}
ComposeGlobals(model, m_A, outGlobals);
}
+4 -2
View File
@@ -160,7 +160,8 @@ namespace Engine::Animation {
}
// ---- 姿态求值(双层混合在求值器内部)----
m_Evaluator.Evaluate(*model, cur, anim.time, xf, m_Globals);
m_Evaluator.Evaluate(*model, cur, anim.time, xf, m_Globals,
!anim.applyRootMotion);
PushPalette(entity, model);
});
@@ -493,7 +494,8 @@ namespace Engine::Animation {
m_Evaluator.EvaluateLayers(*model, m_LayerScratch.data(),
static_cast<u32>(
m_LayerScratch.size()),
m_Globals);
m_Globals,
!anim.applyRootMotion);
PushPalette(entity, model);
});
+617 -115
View File
@@ -1,52 +1,359 @@
//
// HumanoidRig 实现:关键词表 + 归一化 + 自动映射。
// HumanoidRig 实现:standard / dialects / mapper 三层 + 行走基估计。
//
// 分层职责(Unity Avatar 语义,见头文件架构说明):
// standard — 槽位结构化访问(侧别/镜像/指骨算术/显示名)
// dialects — 命名方言数据表(主干统一表 + 指骨三约定展开表)
// mapper — 归一化 + 查表识别 + 跨骨架配对
// 行走基/T-pose 估计 — rest 姿态几何推导,与命名无关
//
#include <Engine/Animation/HumanoidRig.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <array>
#include <cctype>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine::Animation {
namespace {
// ==================================================================
// standard:槽位结构化访问
// (概念分层:公共 API 在 Engine::Animation 作用域,与头文件声明
// 一致;真正的 C++ 内层 namespace 只用于 dialects/mapper 内部表)
// ==================================================================
// ---- 名字归一化:拆 (body, side) ----
//
// 规则:
// 1) 去 "mixamorig:" 等前缀
// 2) 小写;分隔符(空格/_/.-:)一律删除
// 3) left/right(Mixamo "LeftArm")与 左/右(MMD "左腕")统一
// 抽成 side,body 里剔除;.L/.R、_L/_R 后缀同上
//
// body 用**精确相等**匹配关键词(非子串)——"lowerarm"
// 不会误命中 "arm"
Side ExtractSide(std::string& body)
Side SideOfSlot(HumanoidSlot slot)
{
Side s = Side::None;
auto cut = [&](std::string_view token, Side which)
{
size_t pos = 0;
while ((pos = body.find(token, pos)) != std::string::npos) {
body.erase(pos, token.size());
if (s == Side::None) s = which;
const u32 s = static_cast<u32>(slot);
if (s < 6) return Side::None; // 主干
return s < 29 ? Side::Left : Side::Right;
}
HumanoidSlot MirrorSlot(HumanoidSlot slot)
{
const u32 s = static_cast<u32>(slot);
// 布局:主干 [0..5] 无镜像;左区 [6..28] 与右区 [29..51]
// 完全同构(4 臂 + 15 指 + 4 腿趾),偏移 23
if (s < 6 || s >= static_cast<u32>(HumanoidSlot::Count)) {
return slot;
}
return static_cast<HumanoidSlot>(s < 29 ? s + 23 : s - 23);
}
HumanoidSlot FingerSlot(Side side, FingerKind kind, u32 seg)
{
// 布局:LeftThumb1=10 起,5 指 × 3 段连续;右侧同构 +23
const u32 base =
side == Side::Left ? 10 : 10 + 23;
return static_cast<HumanoidSlot>(
base + static_cast<u32>(kind) * 3 + (seg - 1));
}
bool SlotFinger(HumanoidSlot slot, Side& side, FingerKind& kind,
u32& seg)
{
const u32 s = static_cast<u32>(slot);
const bool inL = s >= 10 && s <= 24;
const bool inR = s >= 33 && s <= 47;
if (!inL && !inR) return false;
side = inL ? Side::Left : Side::Right;
const u32 rel = s - (inL ? 10u : 33u);
kind = static_cast<FingerKind>(rel / 3);
seg = rel % 3 + 1;
return true;
}
const char* HumanoidSlotName(HumanoidSlot slot)
{
static constexpr std::array<const char*, 52> kNames{
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
"LeftThumb1", "LeftThumb2", "LeftThumb3",
"LeftIndex1", "LeftIndex2", "LeftIndex3",
"LeftMiddle1", "LeftMiddle2", "LeftMiddle3",
"LeftRing1", "LeftRing2", "LeftRing3",
"LeftPinky1", "LeftPinky2", "LeftPinky3",
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot", "LeftToes",
"RightShoulder", "RightUpperArm", "RightLowerArm",
"RightHand",
"RightThumb1", "RightThumb2", "RightThumb3",
"RightIndex1", "RightIndex2", "RightIndex3",
"RightMiddle1", "RightMiddle2", "RightMiddle3",
"RightRing1", "RightRing2", "RightRing3",
"RightPinky1", "RightPinky2", "RightPinky3",
"RightUpperLeg", "RightLowerLeg", "RightFoot",
"RightToes",
};
cut("left", Side::Left);
cut("right", Side::Right);
cut("左", Side::Left); // MMD 前缀
cut("右", Side::Right);
cut(".l", Side::Left); // blender 惯例后缀
cut(".r", Side::Right);
cut("_l", Side::Left);
cut("_r", Side::Right);
return s;
const u32 s = static_cast<u32>(slot);
return s < kNames.size() ? kNames[s] : "?";
}
// ① 前缀剥离 + 小写(分隔符**保留**——后缀式 side 标记
// ==================================================================
// dialects:命名方言数据表(按"命名规定 / convention"分区)
//
// 识别 = 归一化 body 精确查表(Unity 的 token 规则表同思想)。
// 覆盖的命名规定:
// 通用英文 Unity / Blender / 多数 DCC 导出的英文名
// Mixamo Adobe Mixamo(导入前已剥 "mixamorig:" 前缀)
// MMD / PMX MikuMikuDance 標準ボーン(日文名 + 全角数字)
// Rigify Blender Rigify 变形骨(DEF- 前缀)
// UE Unreal Mannequin / IK Rig(段词 Proximal…)
// 3ds Max 3ds Max Biped(bip01)
// 主干(trunk)与指/趾(finger)各一张表:两表键空间不重叠,
// 查表主干优先、指骨兜底。各分区键名互不重叠,查表取首个命中,
// 分区顺序仅为归类、无功能含义。
// ==================================================================
namespace dialects {
// ---- 主干表(body = side 抽取 + 分隔符剥离后的左槽名)----
struct TrunkEntry {
std::string_view body;
HumanoidSlot slot;
};
constexpr std::array<TrunkEntry, 61> kTrunk{{
// ==== 通用英文(Unity / Blender / DCC)====
// 躯干:spine…/chest/spine1 与 upperchest/spine2 同义梯队
{"hips", HumanoidSlot::Hips},
{"pelvis", HumanoidSlot::Hips},
{"spine", HumanoidSlot::Spine},
{"chest", HumanoidSlot::Chest},
{"spine1", HumanoidSlot::Chest},
{"upperchest", HumanoidSlot::UpperChest},
{"spine2", HumanoidSlot::UpperChest},
{"neck", HumanoidSlot::Neck},
{"head", HumanoidSlot::Head},
// 臂:arm/upperarm/bicep 同义;lowerarm/forearm/elbow 同义
{"shoulder", HumanoidSlot::LeftShoulder},
{"clavicle", HumanoidSlot::LeftShoulder},
{"arm", HumanoidSlot::LeftUpperArm},
{"upperarm", HumanoidSlot::LeftUpperArm},
{"bicep", HumanoidSlot::LeftUpperArm},
{"lowerarm", HumanoidSlot::LeftLowerArm},
{"forearm", HumanoidSlot::LeftLowerArm},
{"elbow", HumanoidSlot::LeftLowerArm},
{"hand", HumanoidSlot::LeftHand},
// 腿:upperleg/upleg/thigh 同义;lowerleg/leg/knee/calf 同义
{"upperleg", HumanoidSlot::LeftUpperLeg},
{"upleg", HumanoidSlot::LeftUpperLeg},
{"thigh", HumanoidSlot::LeftUpperLeg},
{"lowerleg", HumanoidSlot::LeftLowerLeg},
{"leg", HumanoidSlot::LeftLowerLeg}, // Mixamo LeftLeg = 小腿
{"knee", HumanoidSlot::LeftLowerLeg},
{"calf", HumanoidSlot::LeftLowerLeg},
{"foot", HumanoidSlot::LeftFoot},
{"ankle", HumanoidSlot::LeftFoot},
{"toebase", HumanoidSlot::LeftToes},
{"toes", HumanoidSlot::LeftToes},
{"toe", HumanoidSlot::LeftToes},
// ==== MMD / PMX 標準ボーン ====
// 下半身=骨盆、上半身链=spine→chest→upperchest、腕=上臂、
// ひじ/肘=肘、手首=腕关、足=大腿、ひざ=膝、足首=踝、つま先=趾。
// センター(根运动控制骨)与下半身同为 Hips 候选,实际取
// DFS 首个命中者(下半身),センター 落为承载节点走根运动
{"下半身", HumanoidSlot::Hips},
{"センター", HumanoidSlot::Hips},
{"上半身", HumanoidSlot::Spine},
{"上半身1", HumanoidSlot::Chest},
{"上半身2", HumanoidSlot::UpperChest},
{"首", HumanoidSlot::Neck},
{"頭", HumanoidSlot::Head},
{"肩", HumanoidSlot::LeftShoulder},
{"腕", HumanoidSlot::LeftUpperArm},
{"ひじ", HumanoidSlot::LeftLowerArm},
{"肘", HumanoidSlot::LeftLowerArm},
{"手首", HumanoidSlot::LeftHand},
{"足", HumanoidSlot::LeftUpperLeg},
{"ひざ", HumanoidSlot::LeftLowerLeg},
{"足首", HumanoidSlot::LeftFoot},
{"つま先", HumanoidSlot::LeftToes},
// ==== Rigify(Blender)DEF- 变形骨 ====
// 前缀保留参与精确匹配——裸 "spine" 在 Mixamo(脊柱段)与
// Rigify(DEF-spine=骨盆变形骨)间语义冲突,独立键名避免
// 误伤既有约定。段号实测自 Lumine(.003 挂肩=胸上段、.005
// 颈、.006 头——眼/牙/发均挂其下)
{"defspine", HumanoidSlot::Hips},
{"defspine001", HumanoidSlot::Spine},
{"defspine002", HumanoidSlot::Chest},
{"defspine003", HumanoidSlot::UpperChest},
{"defspine005", HumanoidSlot::Neck},
{"defspine006", HumanoidSlot::Head},
{"defshoulder", HumanoidSlot::LeftShoulder},
{"defupperarm", HumanoidSlot::LeftUpperArm},
{"defforearm", HumanoidSlot::LeftLowerArm},
{"defhand", HumanoidSlot::LeftHand},
{"defthigh", HumanoidSlot::LeftUpperLeg},
{"defshin", HumanoidSlot::LeftLowerLeg},
{"deffoot", HumanoidSlot::LeftFoot},
{"deftoe", HumanoidSlot::LeftToes},
// ==== 3ds Max Biped ====
{"bip01", HumanoidSlot::Hips},
}};
// ---- 指骨展开表(按命名规定分区)----
// 四种段号约定(实测来源):
// Mixamo LeftHandThumb1..3 → hand + 指名 + 裸数字
// Rigify DEF-thumb.01..03 → (def)? + 指名 + 零填充两位;
// 四指带 f_ 前缀(findex...),
// 拇指没有;master 裸名同构
// UE/kwy Thumb Proximal_L 等 → 指名 + 段词
// (Proximal/Intermediate/Distal = 近/中/末节,
// 小指叫 little 非 pinky);趾为复数 Toes
// MMD/PMX 親指0..2 / 人指1..3 / 中指1..3 / 薬指1..3 / 小指1..3
// → 指名 + 数字(拇指 0 起、其余 1 起;全角数字已由
// mapper::FoldFullWidthDigits 折半)
// 表键为左侧 body(side 由归一化抽取),右槽经 MirrorSlot。
// 各约定的「…先 / 段 4」指尖均不展开——标准拓扑无对应槽位,
// 源轨道识别失败由 attach 侧 skip(与旧行为一致)。
struct BodySlot {
std::string body;
HumanoidSlot slot;
};
const std::vector<BodySlot>& FingerPatterns()
{
static const std::vector<BodySlot> kTable = [] {
std::vector<BodySlot> t;
t.reserve(80);
// Mixamo:handthumb1 ... handpinky3
constexpr std::array<std::string_view, 5> kMixamo{
"thumb", "index", "middle", "ring", "pinky"};
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
const auto slot = static_cast<u32>(
FingerSlot(Side::Left,
static_cast<FingerKind>(kindU),
1));
for (u32 seg = 1; seg <= 3; ++seg) {
t.push_back({std::string("hand") +
std::string(kMixamo[kindU]) +
std::to_string(seg),
static_cast<HumanoidSlot>(
slot + seg - 1)});
}
}
// Rigify:defthumb01 ... deffpinky03(+ 无前缀 master 变体)
constexpr std::array<std::string_view, 5> kRigify{
"thumb", "findex", "fmiddle", "fring", "fpinky"};
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
const auto slot = static_cast<u32>(
FingerSlot(Side::Left,
static_cast<FingerKind>(kindU),
1));
for (u32 seg = 1; seg <= 3; ++seg) {
const std::string z =
std::to_string(100 + seg).substr(1); // 01..03
const std::string core =
std::string(kRigify[kindU]) + z;
t.push_back({"def" + core,
static_cast<HumanoidSlot>(
slot + seg - 1)});
t.push_back({core,
static_cast<HumanoidSlot>(
slot + seg - 1)});
}
}
// UE:thumbproximal ... littledistal
constexpr std::array<std::string_view, 5> kUe{
"thumb", "index", "middle", "ring", "little"};
constexpr std::array<std::string_view, 3> kUeSeg{
"proximal", "intermediate", "distal"};
for (const auto kindU : {0u, 1u, 2u, 3u, 4u}) {
const auto slot = static_cast<u32>(
FingerSlot(Side::Left,
static_cast<FingerKind>(kindU),
1));
for (u32 seg = 1; seg <= 3; ++seg) {
t.push_back({std::string(kUe[kindU]) +
std::string(kUeSeg[seg - 1]),
static_cast<HumanoidSlot>(
slot + seg - 1)});
}
}
// MMD/PMX:親指0..2 / 人指1..3 / 中指1..3 / 薬指1..3 /
// 小指1..3(拇指 0 起、其余 1 起;数字已折半角)
{
constexpr std::array<std::string_view, 5> kMmd{
"親指", "人指", "中指", "薬指", "小指"};
for (u32 kindU = 0; kindU < 5; ++kindU) {
const u32 numBase = kindU == 0 ? 0u : 1u;
for (u32 j = 0; j < 3; ++j) {
const auto slot = static_cast<u32>(
FingerSlot(Side::Left,
static_cast<FingerKind>(kindU),
j + 1));
t.push_back({std::string(kMmd[kindU]) +
std::to_string(numBase + j),
static_cast<HumanoidSlot>(slot)});
}
}
}
return t;
}();
return kTable;
}
// 查表:主干优先,指骨兜底(两表键空间无交集)
bool Lookup(const std::string& body, HumanoidSlot& out)
{
for (const auto& e : kTrunk) {
if (body == e.body) {
out = e.slot;
return true;
}
}
for (const auto& e : FingerPatterns()) {
if (body == e.body) {
out = e.slot;
return true;
}
}
return false;
}
} // namespace dialects
// ==================================================================
// mapper:归一化 + 识别 + 配对
// ==================================================================
namespace mapper {
// ⓪ 全角数字折半角:U+FF10..U+FF19 → '0'..'9'(UTF-8 EF BC 90..99)。
// MMD 規範骨常写全角数字(親指0/中指1…),折半后各命名规定可在
// 表里统一用半角书写。仅折数字,全角字母(如「つま先IK」)不动
// ——避免 IK 骨误命中「つま先」
void FoldFullWidthDigits(std::string& s)
{
std::string out;
out.reserve(s.size());
for (size_t i = 0; i < s.size();) {
if (i + 3 <= s.size() &&
static_cast<unsigned char>(s[i]) == 0xEF &&
static_cast<unsigned char>(s[i + 1]) == 0xBC) {
const auto b2 = static_cast<unsigned char>(s[i + 2]);
if (b2 >= 0x90 && b2 <= 0x99) {
out.push_back(
static_cast<char>('0' + (b2 - 0x90)));
i += 3;
continue;
}
}
out.push_back(s[i++]);
}
s.swap(out);
}
// ① 前缀剥离 + 小写(分隔符**保留**——末尾式 side 标记
// ".l"/"_l" 依赖分隔符存在,必须先于分隔符删除识别)
std::string NormalizeLower(std::string_view name)
{
@@ -63,10 +370,11 @@ namespace Engine::Animation {
c = static_cast<char>(std::tolower(
static_cast<unsigned char>(c)));
}
FoldFullWidthDigits(s);
return s;
}
// ② 分隔符删除(在 ExtractSide 之后调用;CJK 多字节原样过)
// ② 分隔符删除(side 抽取之后调用;CJK 多字节原样过)
std::string StripSeparators(const std::string& s)
{
std::string out;
@@ -80,105 +388,99 @@ namespace Engine::Animation {
return out;
}
// ---- body → 左侧槽位 关键词表(右侧 = 左槽 + 段偏移)----
// 精确相等匹配;顺序即优先级(先命中先用)
struct BodyRule {
std::string_view body;
HumanoidSlot left;
};
constexpr std::array<BodyRule, 43> kBodyRules{{
// 主干(MMD 语义:下半身=hips、上半身链=spine→chest)
{"hips", HumanoidSlot::Hips},
{"pelvis", HumanoidSlot::Hips},
{"bip01", HumanoidSlot::Hips},
{"下半身", HumanoidSlot::Hips},
{"センター", HumanoidSlot::Hips},
{"spine", HumanoidSlot::Spine},
{"上半身", HumanoidSlot::Spine},
{"chest", HumanoidSlot::Chest},
{"spine1", HumanoidSlot::Chest},
{"上半身1", HumanoidSlot::Chest},
{"upperchest", HumanoidSlot::UpperChest},
{"spine2", HumanoidSlot::UpperChest},
{"上半身2", HumanoidSlot::UpperChest},
{"neck", HumanoidSlot::Neck},
{"首", HumanoidSlot::Neck},
{"head", HumanoidSlot::Head},
{"頭", HumanoidSlot::Head},
// 臂段(MMD:腕=upper arm、ひじ/肘=elbow、手首=wrist)
{"shoulder", HumanoidSlot::LeftShoulder},
{"clavicle", HumanoidSlot::LeftShoulder},
{"肩", HumanoidSlot::LeftShoulder},
{"arm", HumanoidSlot::LeftUpperArm},
{"upperarm", HumanoidSlot::LeftUpperArm},
{"bicep", HumanoidSlot::LeftUpperArm},
{"腕", HumanoidSlot::LeftUpperArm},
{"lowerarm", HumanoidSlot::LeftLowerArm},
{"forearm", HumanoidSlot::LeftLowerArm},
{"elbow", HumanoidSlot::LeftLowerArm},
{"ひじ", HumanoidSlot::LeftLowerArm},
{"肘", HumanoidSlot::LeftLowerArm},
{"hand", HumanoidSlot::LeftHand},
{"手首", HumanoidSlot::LeftHand},
// 腿段(MMD:足=thigh、ひざ=knee、足首=ankle)
{"upperleg", HumanoidSlot::LeftUpperLeg},
{"upleg", HumanoidSlot::LeftUpperLeg},
{"thigh", HumanoidSlot::LeftUpperLeg},
{"足", HumanoidSlot::LeftUpperLeg},
{"lowerleg", HumanoidSlot::LeftLowerLeg},
{"leg", HumanoidSlot::LeftLowerLeg}, // Mixamo:LeftLeg=小腿(大腿是 UpLeg)
{"knee", HumanoidSlot::LeftLowerLeg},
{"calf", HumanoidSlot::LeftLowerLeg},
{"ひざ", HumanoidSlot::LeftLowerLeg},
{"foot", HumanoidSlot::LeftFoot},
{"ankle", HumanoidSlot::LeftFoot},
{"足首", HumanoidSlot::LeftFoot},
}};
// 左→右槽位偏移:臂/肩段(Shoulder..Hand)+4,腿段(UpperLeg..Foot)+3
HumanoidSlot ToSide(HumanoidSlot leftSlot, Side side)
// 窄 side 抽取:side 只出现在 Mixamo 前缀(LeftHand...)、
// MMD 前缀(左手首)与 Blender 末尾后缀(..._L/.L)。
// 不用任意位置 cut——Rigify "f_ring" 中间的 "_r" 会被误当
// 右侧后缀删掉(body 变 "fing" → Ring 指骨全军覆没的历史
// 陷阱),末尾匹配天然规避
Side ExtractSideNarrow(std::string& body)
{
if (side != Side::Right) return leftSlot;
const u32 s = static_cast<u32>(leftSlot);
const u32 legStart = static_cast<u32>(HumanoidSlot::LeftUpperLeg);
return static_cast<HumanoidSlot>(s + (s >= legStart ? 3 : 4));
Side s = Side::None;
if (body.rfind("left", 0) == 0) {
body.erase(0, 4);
s = Side::Left;
} else if (body.rfind("right", 0) == 0) {
body.erase(0, 5);
s = Side::Right;
} else if (body.rfind("左", 0) == 0) {
body.erase(0, strlen("左")); // UTF-8 3 字节
s = Side::Left;
} else if (body.rfind("右", 0) == 0) {
body.erase(0, strlen("右"));
s = Side::Right;
}
const auto cutSuffix = [&](std::string_view token,
Side which) {
if (body.size() >= token.size() &&
body.compare(body.size() - token.size(),
token.size(), token) == 0) {
body.erase(body.size() - token.size(), token.size());
if (s == Side::None) s = which;
}
};
cutSuffix(".l", Side::Left);
cutSuffix(".r", Side::Right);
cutSuffix("_l", Side::Left);
cutSuffix("_r", Side::Right);
return s;
}
} // namespace
const char* HumanoidSlotName(HumanoidSlot slot)
// 宽 side 抽取(fallback):任意位置删 token,兜底命名习惯
// 不在首尾的资产。窄规则先行,本函数只在查表失败后启用
Side ExtractSideWide(std::string& body)
{
static constexpr std::array<const char*, 20> kNames{
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
"RightUpperLeg", "RightLowerLeg", "RightFoot",
Side s = Side::None;
auto cut = [&](std::string_view token, Side which)
{
size_t pos = 0;
while ((pos = body.find(token, pos)) != std::string::npos) {
body.erase(pos, token.size());
if (s == Side::None) s = which;
}
};
const u32 s = static_cast<u32>(slot);
return s < kNames.size() ? kNames[s] : "?";
cut("left", Side::Left);
cut("right", Side::Right);
cut("左", Side::Left);
cut("右", Side::Right);
cut(".l", Side::Left);
cut(".r", Side::Right);
cut("_l", Side::Left);
cut("_r", Side::Right);
return s;
}
} // namespace mapper
// 名字归一化 + 方言查表识别。两轮:窄 side 抽取优先(避开
// f_ring 类中间子串陷阱),查表失败再用宽抽取兜底
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
bool& ok)
{
side = Side::None;
ok = false;
std::string body = NormalizeLower(name);
side = ExtractSide(body);
body = StripSeparators(body);
if (body.empty()) return HumanoidSlot::Hips;
for (const BodyRule& r : kBodyRules) {
if (body == r.body) {
const std::string lower = mapper::NormalizeLower(name);
for (int pass = 0; pass < 2; ++pass) {
std::string body = lower;
const Side s = pass == 0
? mapper::ExtractSideNarrow(body)
: mapper::ExtractSideWide(body);
body = mapper::StripSeparators(body);
if (body.empty()) {
return HumanoidSlot::Hips;
}
HumanoidSlot slot;
if (dialects::Lookup(body, slot)) {
side = s;
ok = true;
return ToSide(r.left, side);
// 表存左槽名:右 side 经镜像换槽(主干槽镜像原样)
return s == Side::Right ? MirrorSlot(slot) : slot;
}
}
return HumanoidSlot::Hips;
}
// 跨骨架自动映射:同名直映 + 52 槽一对一配对(指/趾骨同为主干
// 槽位走统一通道,无独立特判)
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
const std::vector<ModelNode>& srcNodes,
const std::vector<ModelNode>& dstNodes)
@@ -202,7 +504,7 @@ namespace Engine::Animation {
}
}
// 2) 槽位匹配:两侧各自识别 (slot → nodeIdx),槽位一对一配对
// 2) 槽位配对:两侧各自识别 (slot → nodeIdx),槽位一对一
constexpr u32 kSlotCount = static_cast<u32>(HumanoidSlot::Count);
auto slotOf = [](const std::vector<ModelNode>& nodes,
std::array<i32, kSlotCount>& out)
@@ -218,7 +520,7 @@ namespace Engine::Animation {
}
const u32 s = static_cast<u32>(slot);
if (out[s] < 0) {
out[s] = static_cast<i32>(i); // 关键词先命中优先
out[s] = static_cast<i32>(i); // 方言表先命中优先
}
}
};
@@ -243,4 +545,204 @@ namespace Engine::Animation {
return pairs;
}
// ==================================================================
// 行走基 / T-pose 估计(rest 姿态几何推导,与命名方言无关)
// ==================================================================
namespace {
// 槽位 → 节点下标(第一个命中;无则 -1)
i32 FindSlotNode(const std::vector<ModelNode>& nodes,
HumanoidSlot slot)
{
for (u32 i = 0; i < nodes.size(); ++i) {
Side side;
bool ok = false;
if (MatchHumanoidSlot(nodes[i].name, side, ok) == slot &&
ok) {
return static_cast<i32>(i);
}
}
return -1;
}
// 脚趾节点(按 Toes 槽位 + 侧别匹配;无则 -1)。走槽位而非
// 名字含 "toe"——否则 MMD「つま先」等 CJK 趾骨检测不到
i32 FindToeNode(const std::vector<ModelNode>& nodes, Side want)
{
for (u32 i = 0; i < nodes.size(); ++i) {
Side side;
bool ok = false;
const HumanoidSlot slot =
MatchHumanoidSlot(nodes[i].name, side, ok);
if (!ok || SideOfSlot(slot) != want) {
continue;
}
if (slot == HumanoidSlot::LeftToes ||
slot == HumanoidSlot::RightToes) {
return static_cast<i32>(i);
}
}
return -1;
}
// rest 姿态的行走基(right/up/fwd,右手系)。up = 头 - 双脚
// 中点;right = 臂差(armR - armL)去 up 分量;fwd = up × right。
// 脚趾朝向消歧:fwd 与脚尖反向时 right/fwd 绕 up 同翻 180°
// (镜像/腿型特异防呆,同翻保持右手系)。槽位不足 → false
bool HumanoidBasis(const std::vector<ModelNode>& nodes,
const std::vector<glm::mat4>& globals,
glm::vec3& right, glm::vec3& up, glm::vec3& fwd)
{
const i32 head = FindSlotNode(nodes, HumanoidSlot::Head);
const i32 footL = FindSlotNode(nodes, HumanoidSlot::LeftFoot);
const i32 footR = FindSlotNode(nodes, HumanoidSlot::RightFoot);
const i32 armL = FindSlotNode(nodes, HumanoidSlot::LeftUpperArm);
const i32 armR = FindSlotNode(nodes, HumanoidSlot::RightUpperArm);
if (head < 0 || footL < 0 || footR < 0 || armL < 0 || armR < 0) {
return false;
}
const auto pos = [&](i32 i) {
return glm::vec3(globals[static_cast<size_t>(i)][3]);
};
const glm::vec3 midFoot = 0.5f * (pos(footL) + pos(footR));
const glm::vec3 lat = pos(armR) - pos(armL);
up = pos(head) - midFoot;
if (glm::length(up) < 1e-6f || glm::length(lat) < 1e-6f) {
return false;
}
up = glm::normalize(up);
right = lat - up * glm::dot(lat, up); // 去除 up 分量
if (glm::length(right) < 1e-6f) {
return false;
}
right = glm::normalize(right);
// fwd = up × right(右手系:面朝 fwd 时 right 恒在体右侧)。
// 原 right × up 恒指向背后,正常资产(X Bot/Mixamo:脚尖
// +Z、右臂 −X)必触发下方脚趾消歧的 180° 翻转——翻转虽把
// fwd 纠正,却把 right 连带翻成解剖学左,行走基被镜像污染;
// 与未翻转的目标骨架(Lumine 等)对齐后,臂部增量被解释成
// 朝身体内侧——两臂交缠的根因。腿/躯干增量前后对称,镜像
// 下不显形,故此前未暴露
fwd = glm::cross(up, right);
const i32 toeL = FindToeNode(nodes, Side::Left);
const i32 toeR = FindToeNode(nodes, Side::Right);
if (toeL >= 0 && toeR >= 0) {
const glm::vec3 toeDir =
0.5f * (pos(toeL) + pos(toeR)) - midFoot;
if (glm::dot(fwd, toeDir) < 0.0f) {
right = -right;
fwd = -fwd;
}
}
return true;
}
} // namespace
glm::quat EstimateHumanoidRootAlignment(
const std::vector<ModelNode>& srcNodes,
const std::vector<ModelNode>& dstNodes)
{
const glm::quat kIdentity(1.0f, 0.0f, 0.0f, 0.0f);
// rest 世界变换(node 表父先子后,直接链乘)
auto restGlobals = [](const std::vector<ModelNode>& nodes) {
std::vector<glm::mat4> g(nodes.size());
for (size_t i = 0; i < nodes.size(); ++i) {
const i32 p = nodes[i].parent;
g[i] = (p >= 0 ? g[static_cast<size_t>(p)]
: glm::mat4(1.0f)) *
nodes[i].localTransform;
}
return g;
};
const auto srcG = restGlobals(srcNodes);
const auto dstG = restGlobals(dstNodes);
glm::vec3 sr, su, sf, dr, du, df;
if (!HumanoidBasis(srcNodes, srcG, sr, su, sf) ||
!HumanoidBasis(dstNodes, dstG, dr, du, df)) {
return kIdentity;
}
// 列 = [right | up | fwd];A = Bd × Bsᵀ(src 世界 → dst 世界)
const glm::mat3 Bs(sr, su, sf);
const glm::mat3 Bd(dr, du, df);
return glm::quat_cast(Bd * glm::transpose(Bs));
}
std::vector<glm::quat> EstimateHumanoidTPose(
const std::vector<ModelNode>& nodes,
const std::vector<glm::mat4>& restGlobals)
{
// T-pose 参考姿势(每节点世界旋转):
// 人形动画资产(Mixamo 等)的摆动量以 T-pose 为零位,目标骨架
// rest 是 A-pose 时直接搬运"旋转变化"会过摆(臂从已下垂 45°
// 再摆 T-pose 基准的全幅)——手越过中线交叉。此处为目标估计
// T-pose 参考:臂链(upper_arm 子树)绕肩刚体摆到水平侧向,
// 指骨按 seg1 方向伸直(kwy 等 rest 半握骨架与 Mixamo 等
// rest 伸直骨架的手指零位不同源,见下)。其余骨取 rest。
// rest 已是 T-pose 的资产(臂≈水平、指伸直)摆动量≈0,
// 自动退化。
auto rotOf = [](const glm::mat4& m) {
return glm::quat_cast(glm::mat3(
glm::normalize(glm::vec3(m[0])),
glm::normalize(glm::vec3(m[1])),
glm::normalize(glm::vec3(m[2]))));
};
std::vector<glm::quat> g(nodes.size(), glm::quat(1.0f, 0.0f, 0.0f,
0.0f));
for (size_t i = 0; i < nodes.size(); ++i) {
g[i] = rotOf(restGlobals[i]);
}
glm::vec3 right, up, fwd;
if (!HumanoidBasis(nodes, restGlobals, right, up, fwd)) {
return g; // 行走基不可用 → 全 rest(不补偿)
}
const auto posOf = [&](i32 i) {
return glm::vec3(restGlobals[static_cast<size_t>(i)][3]);
};
for (const Side side : {Side::Left, Side::Right}) {
const HumanoidSlot uaSlot = side == Side::Left
? HumanoidSlot::LeftUpperArm : HumanoidSlot::RightUpperArm;
const HumanoidSlot faSlot = side == Side::Left
? HumanoidSlot::LeftLowerArm : HumanoidSlot::RightLowerArm;
const i32 ua = FindSlotNode(nodes, uaSlot);
const i32 fa = FindSlotNode(nodes, faSlot);
if (ua < 0 || fa < 0) {
continue;
}
const glm::vec3 v =
glm::normalize(posOf(fa) - posOf(ua)); // 上臂 rest 指向
glm::vec3 vlat = v - up * glm::dot(v, up) -
fwd * glm::dot(v, fwd);
if (glm::length(vlat) < 1e-6f) {
continue; // 臂纯沿 up(退化)→ 不摆
}
vlat = glm::normalize(vlat);
const float d = glm::clamp(glm::dot(v, vlat), -1.0f, 1.0f);
glm::vec3 axis = glm::cross(v, vlat);
const glm::quat swing =
glm::length(axis) < 1e-6f
? glm::quat(1.0f, 0.0f, 0.0f, 0.0f)
: glm::angleAxis(glm::acos(d),
glm::normalize(axis));
// upper_arm 子树刚体摆动(保持链内相对姿态;含自身)
for (size_t j = 0; j < nodes.size(); ++j) {
for (i32 p = static_cast<i32>(j); p >= 0;
p = nodes[static_cast<size_t>(p)].parent) {
if (p == ua) {
g[j] = swing * g[j];
break;
}
}
}
}
// 手指不参与 T-pose 零位:指根扇形编码在 rest 平移里(旋转
// 搬运无法复刻扇形),绝对复刻语义会让 dst 指根方向偏转 +
// 跨手型穿插。手指走 rest 零位局部增量通道(Unity muscle
// 语义,见 AnimationAttach),各资产保留自然手型,此处取 rest。
return g;
}
} // namespace Engine::Animation
+37
View File
@@ -202,6 +202,43 @@ namespace Engine::Assets {
});
}
ModelRef AssetManager::CreateCube(f32 halfSize, u8 r, u8 g, u8 b)
{
// 程序化资产复用同一缓存:键 = 伪路径 + 参数(与 plane 同约定)
std::string key = "procedural://cube/" + std::to_string(halfSize) +
"/" + std::to_string(r) + "," + std::to_string(g) +
"," + std::to_string(b);
return m_Models.Load(key,
[&](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 预积分)经接口发生在渲染层实现里
+10
View File
@@ -6,6 +6,8 @@
#include <Engine/Core/Window.h>
#include <Engine/Core/Input.h>
#include <Engine/Core/Log.h>
#include <Engine/Core/LogRing.h>
#include <Engine/Core/Stats.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Scripting/ScriptRuntime.h>
@@ -112,6 +114,12 @@ namespace Engine {
void Application::Run()
{
// Log Panel 开闸:到此为止之前的启动日志(窗口/Vulkan/UBO/
// IBL/网格构建)只落终端与文件——Unity Console 语义,面板只
// 收运行期日志(场景/物理/脚本),启动噪音不进面板
Engine::Debug::LogRing().SetEnabled(true);
ENGINE_DEBUG("[APP] 主循环启动(Log Panel 开启;此行为 DEBUG 级,"
"勾选 Trace 过滤可见青色 DEBUG 徽标)");
ENGINE_INFO("Application starting");
m_Running = true;
@@ -133,6 +141,8 @@ namespace Engine {
while (m_Running) {
const auto frameStart = std::chrono::steady_clock::now();
STATS.BeginFrame(); // 帧内资源分配清零(面板帧流失真修正)
m_Window->PollEvents();
auto now = std::chrono::steady_clock::now();
+8 -1
View File
@@ -2,6 +2,7 @@
// Created by atdunbg on 2026/9/20.
//
#include <Engine/Core/Log.h>
#include <Engine/Core/LogRing.h>
#include "spdlog/sinks/basic_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
@@ -26,10 +27,16 @@ namespace Engine::Logger
auto file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("HelloVulkan.log", true);
file->set_pattern("[%Y-%m-%d %T.%e] [%l] [%t] %v");
// 环形缓冲(编辑器 Log Panel 数据源):时间 + 消息——级别词
// 不进文本,由面板按 msg.level 画彩色徽标列(文本里重复一遍
// 级别既冗余又抢徽标的视觉作用)
auto ring = std::make_shared<Engine::Debug::RingSink>();
ring->set_pattern("[%H:%M:%S.%e] %v");
// 多sink 组合默认 logger
// auto logger = std::make_shared<spdlog::logger>("hello", spdlog::sinks_init_list{console, file});
auto logger = std::make_shared<spdlog::logger>("APP", spdlog::sinks_init_list{console, file});
auto logger = std::make_shared<spdlog::logger>("APP", spdlog::sinks_init_list{console, file, ring});
logger->set_level(spdlog::level::trace);
logger->flush_on(spdlog::level::warn); // warn 及以上立即 flush,避免崩溃丢日志
+16 -196
View File
@@ -4,10 +4,6 @@
#include <Engine/Core/Stats.h>
#include <Engine/Core/Log.h>
#include <fstream>
#include <sstream>
#include <string>
namespace Engine
{
Stats& Stats::Get()
@@ -125,200 +121,24 @@ namespace Engine
m_TotalDeallocation += bytes;
}
// ============ 进程内存采样 ============
// ============ 只读快照 ============
Stats::ProcessMemory Stats::ReadProcessMemory() const
{
ProcessMemory result;
// ---- 1. 从 /proc/self/status 读 VmRSS 和 VmSize ----
// 这是内核报告的权威 RSS。smaps_rollup 会漏掉某些 NVIDIA 显存映射,
// 但 status 的 VmRSS 包含所有 VMA。
{
std::ifstream status("/proc/self/status");
std::string line;
while (std::getline(status, line))
{
if (line.rfind("VmRSS:", 0) == 0)
{
std::istringstream iss(line.substr(6));
u64 kb = 0;
iss >> kb;
result.Rss = kb * 1024;
}
else if (line.rfind("VmSize:", 0) == 0)
{
std::istringstream iss(line.substr(7));
u64 kb = 0;
iss >> kb;
result.VmSize = kb * 1024;
}
}
}
// ---- 2. 从 /proc/self/smaps_rollup 读 PSS 和分项 ----
// 注意:Rss 不从这里读,用上面的。
{
std::ifstream smaps("/proc/self/smaps_rollup");
std::string line;
while (std::getline(smaps, line))
{
std::istringstream iss(line);
std::string key;
u64 value = 0;
std::string unit;
iss >> key;
if (key.empty()) continue;
if (key.back() == ':') key.pop_back();
if (key == "Pss")
{
iss >> value >> unit;
result.Pss = value * 1024;
}
else if (key == "Shared_Clean")
{
iss >> value >> unit;
result.SharedClean = value * 1024;
}
else if (key == "Private_Dirty")
{
iss >> value >> unit;
result.PrivateDirty = value * 1024;
}
else if (key == "Private_Clean")
{
iss >> value >> unit;
result.PrivateClean = value * 1024;
}
}
}
return result;
}
void Stats::SampleProcessMemory()
Stats::Snapshot Stats::TakeSnapshot() const
{
std::lock_guard lock(m_Mutex);
m_ProcessMem = ReadProcessMemory();
}
// ============ 打印 ============
static double MB(const u64 bytes) { return bytes / (1024.0 * 1024.0); }
void Stats::Print() const
{
std::lock_guard lock(m_Mutex);
const auto proc = ReadProcessMemory();
ENGINE_INFO("========== Resource Stats ==========");
ENGINE_INFO("Textures: {:>4} ({:.2f} MB)", m_TextureCount, MB(m_TextureBytes));
ENGINE_INFO("Meshes: {:>4} ({:.2f} MB)", m_MeshCount, MB(m_MeshBytes));
ENGINE_INFO("Buffers: {:>4} ({:.2f} MB)", m_BufferCount, MB(m_BufferBytes));
ENGINE_INFO("--- GPU MemoryAllocator ---");
ENGINE_INFO("Allocated: {:.2f} MB", MB(m_MemoryBlockBytes));
ENGINE_INFO("Used: {:.2f} MB", MB(m_MemoryUsedBytes));
ENGINE_INFO("Wasted: {:.2f} MB", MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
ENGINE_INFO("--- Process Memory ---");
ENGINE_INFO("VmSize: {:.2f} MB", MB(proc.VmSize));
ENGINE_INFO("RSS (resident): {:.2f} MB", MB(proc.Rss));
ENGINE_INFO("PSS: {:.2f} MB", MB(proc.Pss));
ENGINE_INFO(" Shared Clean: {:.2f} MB", MB(proc.SharedClean));
ENGINE_INFO(" Private Dirty: {:.2f} MB", MB(proc.PrivateDirty));
ENGINE_INFO(" Private Clean: {:.2f} MB", MB(proc.PrivateClean));
ENGINE_INFO("====================================");
}
void Stats::PrintDetailed() const
{
std::lock_guard lock(m_Mutex);
auto proc = ReadProcessMemory();
ENGINE_INFO("==================== Detailed Memory Stats ====================");
ENGINE_INFO(" Resources");
ENGINE_INFO(" Textures: {:>6} {:>10.2f} MB", m_TextureCount, MB(m_TextureBytes));
ENGINE_INFO(" Meshes: {:>6} {:>10.2f} MB", m_MeshCount, MB(m_MeshBytes));
ENGINE_INFO(" Buffers: {:>6} {:>10.2f} MB", m_BufferCount, MB(m_BufferBytes));
ENGINE_INFO(" Total: {:>10.2f} MB",
MB(m_TextureBytes + m_MeshBytes + m_BufferBytes));
ENGINE_INFO(" GPU MemoryAllocator");
ENGINE_INFO(" Allocated: {:>10.2f} MB", MB(m_MemoryBlockBytes));
ENGINE_INFO(" Used: {:>10.2f} MB", MB(m_MemoryUsedBytes));
ENGINE_INFO(" Wasted: {:>10.2f} MB",
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
ENGINE_INFO(" Allocation Churn");
ENGINE_INFO(" This frame: +{:.2f} MB / -{:.2f} MB",
MB(m_FrameAllocation), MB(m_FrameDeallocation));
ENGINE_INFO(" Total: +{:.2f} MB / -{:.2f} MB",
MB(m_TotalAllocation), MB(m_TotalDeallocation));
ENGINE_INFO(" Process Memory (/proc/self/status + smaps_rollup)");
ENGINE_INFO(" VmSize: {:>10.2f} MB", MB(proc.VmSize));
ENGINE_INFO(" RSS: {:>10.2f} MB", MB(proc.Rss));
ENGINE_INFO(" PSS: {:>10.2f} MB", MB(proc.Pss));
ENGINE_INFO(" Shared Clean: {:>10.2f} MB", MB(proc.SharedClean));
ENGINE_INFO(" Private Dirty: {:>10.2f} MB", MB(proc.PrivateDirty));
ENGINE_INFO(" Private Clean: {:>10.2f} MB", MB(proc.PrivateClean));
ENGINE_INFO(" Unaccounted (RSS - GPU Blocks)");
u64 engineTotal = m_MemoryBlockBytes;
if (proc.Rss > engineTotal)
{
ENGINE_INFO(" {:.2f} MB (Vulkan driver, libs, stack, heap, ...)",
MB(proc.Rss - engineTotal));
}
else
{
ENGINE_INFO(" (GPU blocks are the bulk of RSS)");
}
ENGINE_INFO("===============================================================");
}
void Stats::PrintComparison() const
{
std::lock_guard lock(m_Mutex);
auto proc = ReadProcessMemory();
u64 resourceTotal = m_TextureBytes + m_MeshBytes + m_BufferBytes;
ENGINE_INFO("=============== Memory Comparison ===============");
ENGINE_INFO("");
ENGINE_INFO("[Engine 自己统计]");
ENGINE_INFO(" Textures {:>10.2f} MB", MB(m_TextureBytes));
ENGINE_INFO(" Meshes {:>10.2f} MB", MB(m_MeshBytes));
ENGINE_INFO(" Buffers {:>10.2f} MB", MB(m_BufferBytes));
ENGINE_INFO(" ─────────────────────────────────────");
ENGINE_INFO(" 资源合计 {:>10.2f} MB", MB(resourceTotal));
ENGINE_INFO("");
ENGINE_INFO(" Allocator Used {:>10.2f} MB", MB(m_MemoryUsedBytes));
ENGINE_INFO(" Allocator Allocated {:>10.2f} MB", MB(m_MemoryBlockBytes));
ENGINE_INFO(" Allocator Wasted {:>10.2f} MB",
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
ENGINE_INFO("");
ENGINE_INFO("[系统 /proc 读出的]");
ENGINE_INFO(" VmRSS {:>10.2f} MB", MB(proc.Rss));
ENGINE_INFO(" VmSize {:>10.2f} MB", MB(proc.VmSize));
ENGINE_INFO(" PSS {:>10.2f} MB", MB(proc.Pss));
ENGINE_INFO(" Shared Clean {:>10.2f} MB", MB(proc.SharedClean));
ENGINE_INFO(" Private Dirty {:>10.2f} MB", MB(proc.PrivateDirty));
ENGINE_INFO(" Private Clean {:>10.2f} MB", MB(proc.PrivateClean));
ENGINE_INFO("");
ENGINE_INFO("[差值]");
ENGINE_INFO(" RSS - Allocator {:>10.2f} MB (Vulkan 驱动 + libs + 栈堆)",
MB(proc.Rss > m_MemoryBlockBytes ? proc.Rss - m_MemoryBlockBytes : 0));
ENGINE_INFO(" RSS - 资源合计 {:>10.2f} MB (含分配器碎片)",
MB(proc.Rss > resourceTotal ? proc.Rss - resourceTotal : 0));
ENGINE_INFO(" Allocated - Used {:>10.2f} MB (分配器空闲块)",
MB(m_MemoryBlockBytes - m_MemoryUsedBytes));
ENGINE_INFO("================================================");
Snapshot s;
s.textureCount = m_TextureCount;
s.textureBytes = m_TextureBytes;
s.meshCount = m_MeshCount;
s.meshBytes = m_MeshBytes;
s.bufferCount = m_BufferCount;
s.bufferBytes = m_BufferBytes;
s.memoryBlockBytes = m_MemoryBlockBytes;
s.memoryUsedBytes = m_MemoryUsedBytes;
s.frameAllocation = m_FrameAllocation;
s.frameDeallocation = m_FrameDeallocation;
s.totalAllocation = m_TotalAllocation;
s.totalDeallocation = m_TotalDeallocation;
return s;
}
} // namespace Engine
+17
View File
@@ -23,10 +23,14 @@ namespace Engine {
constexpr i32 kKeyUp = 265, kKeyDown = 264,
kKeyLeft = 263, kKeyRight = 262;
constexpr i32 kKeySpace = 32;
constexpr i32 kKeyLShift = 340; // GLFW_KEY_LEFT_SHIFT
// 手柄标准布局
constexpr i32 kPadBtnA = 0; // GLFW_GAMEPAD_BUTTON_A
constexpr i32 kPadBtnL3 = 9; // GLFW_GAMEPAD_BUTTON_LEFT_THUMB
constexpr i32 kPadAxisLeftX = 0; // GLFW_GAMEPAD_AXIS_LEFT_X
constexpr i32 kPadAxisLeftY = 1; // GLFW_GAMEPAD_AXIS_LEFT_Y
constexpr i32 kPadAxisRightX = 2; // GLFW_GAMEPAD_AXIS_RIGHT_X
constexpr i32 kPadAxisRightY = 3; // GLFW_GAMEPAD_AXIS_RIGHT_Y
constexpr f32 kRadialDeadZone = 0.15f;
@@ -139,6 +143,19 @@ namespace Engine {
const size_t jump = DefineAction("Jump", ActionKind::Button);
Bind(jump, {SourceKind::Key, kKeySpace, 1.0f, kX});
Bind(jump, {SourceKind::GamepadButton, kPadBtnA, 1.0f, kX});
// Run:Shift + 手柄 L3(按下左摇杆 = 冲刺,Xbox 惯例)。
// 注意不能用"摇杆推满"判跑——键盘 WASD 经死区/钳制后幅度
// 恒为 1.0,会把走路误判成跑(真实踩坑:WASD 一动就 run)
const size_t run = DefineAction("Run", ActionKind::Button);
Bind(run, {SourceKind::Key, kKeyLShift, 1.0f, kX});
Bind(run, {SourceKind::GamepadButton, kPadBtnL3, 1.0f, kX});
// Look:右摇杆(相机环绕)。鼠标是相对增量不走动作层,
// 相机脚本里 GetMouseDelta 叠加
const size_t look = DefineAction("Look", ActionKind::Axis2D);
Bind(look, {SourceKind::GamepadAxis, kPadAxisRightX, +1.0f, kX});
Bind(look, {SourceKind::GamepadAxis, kPadAxisRightY, +1.0f, kY});
}
// ---- 运行期 ----
+36 -2
View File
@@ -22,11 +22,17 @@ 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));
s.mShapeOffset = JPH::Vec3(0.0f,
params.halfHeight + params.radius, 0.0f);
s.mShapeOffset = JPH::Vec3(
params.shapeOffset.x,
params.shapeOffset.y + params.halfHeight + params.radius,
params.shapeOffset.z);
m_Character = new JPH::CharacterVirtual(
&s, JPH::RVec3(position.x, position.y, position.z),
@@ -37,6 +43,20 @@ namespace Engine {
m_Ext.mWalkStairsStepUp = JPH::Vec3(0.0f, std::max(params.stepHeight, 0.0f), 0.0f);
}
CharacterController::~CharacterController() {
// 先摘除再释放:列表持裸 CharacterVirtual*,否则悬垂
if (m_VsChar != nullptr) {
m_VsChar->Remove(m_Character.GetPtr());
}
}
void CharacterController::SetCharacterCollision(
JPH::CharacterVsCharacterCollisionSimple &list) {
m_VsChar = &list;
m_Character->SetCharacterVsCharacterCollision(&list);
list.Add(m_Character.GetPtr());
}
void CharacterController::Update(const f32 dt, const JPH::Vec3 &gravity,
JPH::TempAllocator &allocator) {
// 重力积分:着地时清掉向下分量(防贴地累积穿透速度),悬空时累加
@@ -50,6 +70,10 @@ namespace Engine {
m_Character->SetLinearVelocity(v + gravity * dt);
}
// 扫掠前速度快照:撞墙判定用。ExtendedUpdate 会抵消朝向陡面/
// 墙面的速度分量,扫掠后再读就归零了(见头文件说明)
m_SweepVelocity = m_Character->GetLinearVelocity();
// 扫掠推进(撞墙滑动 + 斜坡限制 + StickToFloor 贴地 + WalkStairs
// 上台阶)。过滤器默认全放行:角色与静/动层全碰
m_Character->ExtendedUpdate(dt, gravity, m_Ext,
@@ -78,6 +102,16 @@ namespace Engine {
return m_Character->IsSupported();
}
const JPH::CharacterVirtual::ContactList&
CharacterController::GetActiveContacts() const {
return m_Character->GetActiveContacts();
}
glm::vec3 CharacterController::GetSweepVelocity() const {
return {m_SweepVelocity.GetX(), m_SweepVelocity.GetY(),
m_SweepVelocity.GetZ()};
}
glm::vec3 CharacterController::GetPosition() const {
const JPH::RVec3 p = m_Character->GetPosition();
return {static_cast<f32>(p.GetX()), static_cast<f32>(p.GetY()),
+161 -10
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();
});
@@ -176,6 +226,10 @@ namespace Engine {
// 直接成员会先于 call_once 构造 → 函数指针未注册 → SIGSEGV at 0x0
m_DebugDraw = CreateScope<PhysicsDebugDraw>();
// 角色间碰撞互查表(内部 Array 走 JPH 分配器,须在 call_once 之后)
m_VsCharacterCollision =
CreateScope<JPH::CharacterVsCharacterCollisionSimple>();
m_TempAllocator = CreateScope<JPH::TempAllocatorImpl>(16 * 1024 * 1024);
const u32 threads = std::max(1u, std::thread::hardware_concurrency() - 1);
m_JobSystem = CreateScope<JPH::JobSystemThreadPool>(
@@ -208,6 +262,7 @@ namespace Engine {
// 释放烘焙几何——必须先于 UnregisterTypes
m_DebugDraw.reset();
m_Bodies.clear();
m_Characters.clear(); // 角色析构回 m_VsCharacterCollision 摘除,先于 Jolt 反注册
m_ContactListener.reset(); // 监听器先于 system 析构(持 m_Owner 引用)
m_System.reset();
// 单世界假设:进程退出时统一反注册(多世界时改为引用计数)
@@ -283,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) 约束(关节;软体/载具约束接入后自动生效)
@@ -695,20 +767,24 @@ namespace Engine {
p.mass = cc.mass;
p.maxSlopeAngle = cc.maxSlopeAngle;
p.stepHeight = cc.stepHeight;
p.shapeOffset = cc.shapeOffset;
auto found = m_Characters.find(bits);
if (found == m_Characters.end()) {
CharacterEntry entry;
entry.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetWorldPosition());
entry.cc->SetCharacterCollision(*m_VsCharacterCollision);
entry.lastPos = tr.GetWorldPosition();
m_Characters.emplace(bits, std::move(entry));
continue;
}
if (found->second.cc->GetParams() != p) {
// 参数变更 → 重建(位置沿用当前 Transform)
// 参数变更 → 重建(位置沿用当前 Transform);旧实例析构
// 自动摘除旧条目,新实例重新登记
found->second.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetWorldPosition());
found->second.cc->SetCharacterCollision(*m_VsCharacterCollision);
found->second.lastPos = tr.GetWorldPosition();
continue;
}
@@ -742,6 +818,42 @@ namespace Engine {
static_cast<entt::entity>(bits), p,
tr->GetWorldRotationQuat(), tr->GetWorldScale());
entry.lastPos = p;
// ---- 角色碰撞事件合成(Enter/Exit)----
// CharacterVirtual 非刚体,Jolt 的 ContactListener 不会为角色
// 回调——脚本因此收不到"玩家撞墙"。这里读角色本步活跃接触,
// 与上一步集合对比出边沿,合成与刚体同款的 CollisionEvent
// (a = 角色实体,b = 被撞实体)。只处理刚体接触(mBodyB 有效):
// 角色↔角色由 CharacterVsCharacterCollision 的物理互查处理,
// 不发脚本事件(对方是角色时 mBodyB 无效,天然跳过)
std::unordered_set<u32> now;
now.reserve(entry.contacts.size() + 4);
for (const JPH::CharacterContact& c :
entry.cc->GetActiveContacts()) {
if (c.mBodyB.IsInvalid()) continue; // 对方非刚体(角色)
if (c.mWasDiscarded) continue; // 被丢弃/传感器
if (c.mDistance > 0.0f) continue; // >0 = 预测接触,未真相交
u32 other = 0;
if (!TryBitsByBodyId(c.mBodyB, other)) continue;
now.insert(other);
if (entry.contacts.count(other) == 0) {
const glm::vec3 pt(
static_cast<f32>(c.mPosition.GetX()),
static_cast<f32>(c.mPosition.GetY()),
static_cast<f32>(c.mPosition.GetZ()));
const glm::vec3 n(c.mContactNormal.GetX(),
c.mContactNormal.GetY(),
c.mContactNormal.GetZ());
QueueEvent({bits, other, CollisionEvent::Kind::Added, pt, n});
}
}
for (const u32 prev : entry.contacts) {
if (now.count(prev) == 0) {
QueueEvent(
{bits, prev, CollisionEvent::Kind::Removed, {}, {}});
}
}
entry.contacts = std::move(now);
}
}
@@ -767,8 +879,14 @@ namespace Engine {
// 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动)
if (rb.type != ECS::RigidBodyComponent::BodyType::Static) {
JPH::ConvexHullShapeSettings cs;
for (const auto &mesh: model->GetMeshes()) {
for (const auto &p: mesh->GetCpuPositions()) {
const auto &meshes = model->GetMeshes();
for (size_t mi = 0; mi < meshes.size(); ++mi) {
// 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随)
if (mc->meshIndex >= 0 &&
static_cast<int>(mi) != mc->meshIndex) {
continue;
}
for (const auto &p: meshes[mi]->GetCpuPositions()) {
cs.mPoints.emplace_back(p.x * s.x, p.y * s.y, p.z * s.z);
}
}
@@ -788,9 +906,15 @@ namespace Engine {
JPH::VertexList verts;
JPH::IndexedTriangleList tris;
u32 base = 0;
for (const auto &mesh: model->GetMeshes()) {
const auto &pos = mesh->GetCpuPositions();
const auto &idx = mesh->GetCpuIndices();
const auto &meshes = model->GetMeshes();
for (size_t mi = 0; mi < meshes.size(); ++mi) {
// 单 mesh 子引用:碰撞只取渲染可见的同一 mesh(语义跟随)
if (mc->meshIndex >= 0 &&
static_cast<int>(mi) != mc->meshIndex) {
continue;
}
const auto &pos = meshes[mi]->GetCpuPositions();
const auto &idx = meshes[mi]->GetCpuIndices();
verts.reserve(verts.size() + pos.size());
for (const auto &p: pos) {
verts.push_back(JPH::Float3(p.x * s.x, p.y * s.y, p.z * s.z));
@@ -800,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());
@@ -988,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;
@@ -997,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;
}
@@ -1112,6 +1255,14 @@ namespace Engine {
: glm::vec3{0.0f};
}
glm::vec3
PhysicsWorld::GetCharacterImpactVelocity(const u32 entityBits) const {
const auto found = m_Characters.find(entityBits);
return found != m_Characters.end()
? found->second.cc->GetSweepVelocity()
: glm::vec3{0.0f};
}
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
void PhysicsWorld::Writeback() {
+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;
+3 -1
View File
@@ -32,7 +32,9 @@ namespace Engine
{
constexpr u32 kFaceCount = 6;
constexpr u32 kIrradianceFaceSize = 32; // 漫反射辐照度面边长(低频)
constexpr u32 kPrefilterFaceSize = 128; // pre-filter 面边长(mip0)
// pre-filter 面边长(mip0):roughness→0 的镜面反射采 mip0,
// 128²/面放大到全屏 = 块状马赛克;256 = 无 SSR 时的清晰度底线
constexpr u32 kPrefilterFaceSize = 256;
constexpr u32 kPrefilterMips = 5; // mip 分级 roughness 数
constexpr u32 kBrdfLutSize = 128; // BRDF LUT 边长
constexpr u32 kGroupSize = 8; // compute local_size(shader 同步)
+15 -5
View File
@@ -84,11 +84,21 @@ namespace Engine
",s:" + std::to_string(params.surface.x) +
"," + std::to_string(params.surface.y) +
"," + std::to_string(params.surface.z) +
"," + std::to_string(params.surface.w);
// per-slot UV 变换 + unlit 入键:同一张贴图配不同变换是不同材质
//(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质以不同
// 变换共享,键漏了会串成同一份)
key += ",m:" + std::to_string(params.misc.x);
"," + std::to_string(params.surface.w) +
",e:" + std::to_string(params.emissiveFactor.x) +
"," + std::to_string(params.emissiveFactor.y) +
"," + std::to_string(params.emissiveFactor.z);
// per-slot UV 变换 + unlit + UV 镜像入键:同一张贴图配不同变换是
// 不同材质(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质
// 以不同变换共享,键漏了会串成同一份)
key += ",m:" + std::to_string(params.misc.x) +
"," + std::to_string(params.misc.y) +
"," + std::to_string(params.misc.z);
// alpha 叠加遮罩入键:同贴图不同叠加色是不同材质
key += ",o:" + std::to_string(params.alphaOverlay.x) +
"," + std::to_string(params.alphaOverlay.y) +
"," + std::to_string(params.alphaOverlay.z) +
"," + std::to_string(params.alphaOverlay.w);
for (u32 slot = 0; slot < kSlotCount; ++slot) {
key += ",u" + std::to_string(slot) + ":" +
std::to_string(params.uvA[slot].x) + "," +
+14 -3
View File
@@ -29,7 +29,12 @@ namespace Engine
return Texture::CreateSolidColor(device, allocator,
0, 128, 0, false);
case Material::Emissive:
return Texture::CreateSolidColor(device, allocator, 0, 0, 0);
// 兜底白(非黑):emissive = 贴图 × factor × strength——黑
// 兜底会把无贴图材质的 factor/strength 一起乘没(调了恒黑
// 的困惑源);白兜底 = 无贴图时 factor 即发光色,有贴图时
// glTF 语义不变(factor 黑的普通模型依旧不发光)
return Texture::CreateSolidColor(device, allocator,
255, 255, 255);
case Material::Ao:
return Texture::CreateSolidColor(device, allocator,
255, 0, 0, false);
@@ -84,8 +89,12 @@ namespace Engine
EmitVec4(out, data.params.baseColor);
out << YAML::Key << "Surface" << YAML::Value;
EmitVec4(out, data.params.surface);
out << YAML::Key << "EmissiveFactor" << YAML::Value;
EmitVec4(out, data.params.emissiveFactor);
out << YAML::Key << "Misc" << YAML::Value;
EmitVec4(out, data.params.misc);
out << YAML::Key << "AlphaOverlay" << YAML::Value;
EmitVec4(out, data.params.alphaOverlay);
// per-slot UV 变换(KHR_texture_transform):6 槽各 A(4 分量) + T
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
out << YAML::Key << ("UvA" + std::to_string(slot)) << YAML::Value;
@@ -145,8 +154,12 @@ namespace Engine
m["BaseColor"], MaterialParams{}.baseColor);
out.params.surface = ReadVec4(
m["Surface"], MaterialParams{}.surface);
out.params.emissiveFactor = ReadVec4(
m["EmissiveFactor"], MaterialParams{}.emissiveFactor);
out.params.misc = ReadVec4(
m["Misc"], MaterialParams{}.misc);
out.params.alphaOverlay = ReadVec4(
m["AlphaOverlay"], MaterialParams{}.alphaOverlay);
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
out.params.uvA[slot] = ReadVec4(
m["UvA" + std::to_string(slot)],
@@ -161,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
File diff suppressed because it is too large Load Diff
+127 -53
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,22 +232,89 @@ 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;
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);
}
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);
}
}
u32 ForwardPass::TransparentUnitCount() const
{
u32 n = 0;
for (const auto& b : m_Frame.translucentView.batches) n += b.count;
return n;
}
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.1f, 0.1f, 0.1f, 0.0f});
std::array<float, 4>{0.0f, 0.0f, 0.0f, 0.0f});
vk::RenderingAttachmentInfo colorAttachments[2];
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
@@ -242,23 +323,23 @@ namespace Engine::Passes
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity:Load 不透明段写出的运动向量,玻璃只覆盖自己的像素
vk::ClearValue velocityClear;
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
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);
// 深度只读测试源:Load 承接不透明段深度,depthWrite=FALSE 不产生
// 写入。storeOp 必须 eStore——透明段已拆成多层,后续层的 Load 依赖
// 本层写回的有效深度内容(DontCare 会让内容在层间变 undefined)。
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_Depth))
// 深度只读测试源:Load 承接不透明段深度,
// 透明段 depthWrite=FALSE 不产生写入,
// storeOp DontCare(下帧 entry 重新 Clear)
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
@@ -275,20 +356,19 @@ namespace Engine::Passes
RecordSharedState(ctx, cmd, true);
// UE/Unity 语义:透明段只测不写深度。写深度会让"先画的玻璃"
// 剔掉后画但实际在它前面的玻璃(交叉半透明整块消失)。
// 早期 depthWrite=FALSE 引发 NVIDIA ~5fps 病态的实验是在
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
cmd.setDepthWriteEnable(VK_FALSE);
// 单面玻璃:透明段剔除背面。透明段 cullNone 会让闭合体(cube)
// 的正反面片按 mesh 顺序乱序 over 混合 = "深度错乱";cull back
// 每像素至多一面。双面透明(树叶卡片)将来加材质开关
cmd.setCullMode(vk::CullModeFlagBits::eBack);
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 背景上的玻璃会被拖进
// 桌面混色链路 → 预乘拉黑
// alpha 穿透:玻璃只做场景内 rgb over(透出背景色),不修改
// 像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap rgb *= a 预乘
// + 透明窗口桌面合成),归背景专属
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
.setAlphaBlendOp(vk::BlendOp::eAdd);
@@ -301,30 +381,24 @@ namespace Engine::Passes
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);
}
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,7 +193,21 @@ 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)
{
@@ -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);
+58 -11
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 阴影分支整个跳过。
@@ -757,6 +770,12 @@ namespace Engine
const auto& meshes = model->GetMeshes();
for (size_t meshIdx = 0; meshIdx < meshes.size(); ++meshIdx) {
// 单 mesh 子引用:只收集指派的那一个(覆盖表/物理
// 均按原模型下标,语义不变)
if (mr.meshIndex >= 0 &&
static_cast<int>(meshIdx) != mr.meshIndex) {
continue;
}
auto& mesh = meshes[meshIdx];
const auto& bounds = mesh->GetBounds();
const glm::vec3 worldCenter =
@@ -791,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) {
@@ -809,6 +837,23 @@ namespace Engine
instanceTruncated = true;
return;
}
// 透明队列逐实例成批(不合并):透明段是"每层一个
// 绘制单位"(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);
}
else {
// batch 查找:内建材质 = 数组寻址快路径
//(Mesh::GetSlot 槽位 + LOD 槽);覆盖材质 =
// hash 寻址(键含材质变体:同 mesh 不同覆盖天然
@@ -835,18 +880,18 @@ namespace Engine
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
u32 batchIdx = (*lodSlots)[lod];
if (batchIdx == 0) {
batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
batchIdx =
static_cast<u32>(s.result.batches.size()) + 1;
(*lodSlots)[lod] = batchIdx;
// first 是占位:真实基址在 appendView 聚合时
// 回填(收集序下 batch 实例散布,见链表注释)
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;
bi = batchIdx - 1;
}
// 实例归入自己的 batch(收集序散布,组内连接用链表
// 维护:O(1) push,无 per-batch 分配)
s.result.batches[bi].count++;
const u32 ii =
static_cast<u32>(s.instances.size());
@@ -855,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;
};
@@ -920,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) {
+75 -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()
@@ -95,6 +146,20 @@ namespace Engine {
halfSize, r, g, b, a);
}
Scope<Model> VulkanAssetUploader::CreateCube(f32 halfSize, u8 r, u8 g, u8 b)
{
return Model::CreateCube(m_Device, m_Allocator, m_Descriptors,
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
+63
View File
@@ -7,6 +7,10 @@
#include <Engine/Core/Application.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h>
#include <Engine/ECS/EntityHandle.h>
#include <Engine/Physics/PhysicsEvents.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <algorithm>
@@ -19,6 +23,25 @@ namespace Engine {
return *m_App;
}
// ---- 场景实体 spawn/destroy 底座(声明在 Script.h,实现下沉
// 此处——Application 完整类型只在 cpp 可见)----
ECS::EntityHandle ScriptBehavior::Spawn(const std::string& name) {
return App().GetScene().CreateEntity(name);
}
void ScriptBehavior::Destroy(ECS::Entity e) {
App().GetScene().DestroyEntity(e);
}
// Prefab 实例化(声明在 Script.h,实现下沉此处——同 Spawn/
// Destroy 模式;序列化器与 Application 门面都在引擎内可达)
ECS::Entity ScriptBehavior::Instantiate(const std::string& prefabPath,
const glm::vec3& pos,
const glm::quat& rot) {
return Serialization::SceneSerializer::InstantiatePrefab(
App().GetScene(), App().GetAssets(), prefabPath, &pos, &rot);
}
// ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿
// Item.params 由运行时 SetParam 完成)----
bool ScriptBehavior::WriteParam(const std::string& name,
@@ -41,7 +64,47 @@ namespace Engine {
return false;
}
// ---- 碰撞事件派发(Update 开头、Sync 之前):PhysicsWorld 最近
// 一次步进收集的事件转发到涉事实例。时序保证:Step 先于
// Update(帧循环序)→ 此处读到的是本帧物理结果;回调里
// spawn/destroy 安全——destroy 入延迟队列(帧尾 flush),
// spawn 不改 m_Instances(下一帧 Sync 对账建实例)。禁用
// 实例不收(Unity enabled 语义对齐);Persisted 每步都来,
// 直接跳过(Enter/Exit 已够 gameplay 用)
void ScriptRuntime::DispatchCollisions() {
if (!m_App) {
return;
}
const std::vector<CollisionEvent>& events =
m_App->GetPhysics().GetEvents();
for (const auto& ev : events) {
if (ev.kind == CollisionEvent::Kind::Persisted) {
continue;
}
const bool enter = ev.kind == CollisionEvent::Kind::Added;
for (const u32 self : {ev.a, ev.b}) {
const u32 other = (self == ev.a) ? ev.b : ev.a;
const auto it = m_Instances.find(self);
if (it == m_Instances.end()) {
continue;
}
for (auto& inst : it->second) {
if (!inst.behavior || !inst.enabled) {
continue;
}
if (enter) {
inst.behavior->OnCollisionEnter(
other, ev.point, ev.normal);
} else {
inst.behavior->OnCollisionExit(other);
}
}
}
}
}
void ScriptRuntime::Update(f32 dt) {
DispatchCollisions();
Sync();
auto& reg = m_Scene.GetRegistry();
File diff suppressed because it is too large Load Diff
+51 -1
View File
@@ -8,11 +8,55 @@
#include <vk_mem_alloc.h>
#include <stdexcept>
#include <algorithm>
#include "Engine/Core/Stats.h"
namespace Engine {
u64 EstimateImageBytes(const vk::Format fmt, const u32 w, const u32 h,
const u32 depth, const u32 mips)
{
// 线性格式 = 字节/像素;BC 压缩 = 字节/4x4 块(按块向上取整)。
// 未识别格式按 RGBA8 保守估算。逐 mip 累加(mip 链约占 1/3 额外)
f32 bpp = 0;
u32 blockBytes = 0;
switch (fmt) {
case vk::Format::eR8Unorm:
case vk::Format::eR8Srgb: bpp = 1; break;
case vk::Format::eR8G8Unorm:
case vk::Format::eR8G8Srgb: bpp = 2; break;
case vk::Format::eR16G16B16A16Sfloat: bpp = 8; break;
case vk::Format::eR32G32B32A32Sfloat: bpp = 16; break;
case vk::Format::eB10G11R11UfloatPack32: bpp = 4; break;
case vk::Format::eBc1RgbaUnormBlock:
case vk::Format::eBc1RgbaSrgbBlock:
case vk::Format::eBc4UnormBlock: blockBytes = 8; break;
case vk::Format::eBc2SrgbBlock:
case vk::Format::eBc2UnormBlock:
case vk::Format::eBc3SrgbBlock:
case vk::Format::eBc3UnormBlock:
case vk::Format::eBc5UnormBlock:
case vk::Format::eBc6HUfloatBlock:
case vk::Format::eBc7SrgbBlock:
case vk::Format::eBc7UnormBlock: blockBytes = 16; break;
default: bpp = 4; break;
}
u64 total = 0;
const u32 levels = std::max(1u, mips);
for (u32 mip = 0; mip < levels; ++mip) {
const u64 mw = std::max(1u, w >> mip);
const u64 mh = std::max(1u, h >> mip);
if (blockBytes != 0) {
total += static_cast<u64>((mw + 3) / 4) *
static_cast<u64>((mh + 3) / 4) * blockBytes * depth;
} else {
total += mw * mh * static_cast<u64>(bpp) * depth;
}
}
return total;
}
namespace
{
// props → VMA 创建参数:
@@ -88,6 +132,7 @@ namespace Engine {
out.MappedPtr = info.pMappedData; // 非 MAPPED 标记时为 null
STATS.AddMemoryUsed(size);
STATS.AddMemoryBlock(size); // 1:1 VMA 分配:块 == 占用(无子分配)
return out;
}
@@ -108,11 +153,15 @@ namespace Engine {
Allocation out;
out.Image = image;
out.Size = ii.extent.depth * ii.extent.height * ii.extent.width; // 近似值(统计用)
// 按真实格式 + mip 链估算(原来 w*h*d 近似值对 HDR/压缩全错)
out.Size = EstimateImageBytes(imageInfo.format,
ii.extent.width, ii.extent.height,
ii.extent.depth, imageInfo.mipLevels);
out.Handle = handle;
out.Valid = true;
STATS.AddMemoryUsed(out.Size);
STATS.AddMemoryBlock(out.Size);
return out;
}
@@ -128,6 +177,7 @@ namespace Engine {
}
STATS.RemoveMemoryUsed(alloc.Size);
STATS.RemoveMemoryBlock(alloc.Size);
alloc = {};
}
+114 -43
View File
@@ -93,8 +93,25 @@ 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 成员),再调本函数传真实像素。
// 必须先成对释放占位资源,否则 1x1 的 image/view/sampler 泄漏 →
// allocator 析构时 VMA 块非空断言崩溃(GLB 内嵌贴图模型关闭必崩)
if (m_Image) {
const auto& vkDevice = m_Device.GetDevice();
vkDevice.destroySampler(m_Sampler);
vkDevice.destroyImageView(m_View);
m_Allocator.Destroy(m_ImageAllocation);
m_Sampler = nullptr;
m_View = nullptr;
m_Image = nullptr;
}
m_Width = width;
m_Height = height;
m_Format = srgb ? vk::Format::eR8G8B8A8Srgb : vk::Format::eR8G8B8A8Unorm;
@@ -105,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;
@@ -167,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)
@@ -194,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});
@@ -213,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 链:逐级半分辨率缩小(硬件线性滤波)。
@@ -278,30 +278,94 @@ namespace Engine
dep2.setImageMemoryBarriers(toShader);
cmd.pipelineBarrier2(dep2);
// 显存占用按真实格式 + mip 链估算(与分配器台账同一算法)
if (m_TextureBytes != 0) {
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
}
m_TextureBytes = EstimateImageBytes(m_Format, m_Width, m_Height, 1,
m_MipLevels);
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();
// 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)
{
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");
}
// 8. 清理临时对象(staging 资源与分配经 VMA 成对释放)
m_Device.GetDevice().destroyFence(fence);
m_Device.GetDevice().destroyCommandPool(pool);
m_Allocator.Destroy(stagingAlloc);
ReleasePendingStaging();
}
// 显存占用 = 宽 * 高 * 4 字节(RGBA8)
m_TextureBytes = static_cast<u64>(m_Width) * m_Height * 4;
STATS.AddTexture(m_TextureBytes);
// 批式路径构造:复用 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 语义由贴图槽位决定(渲染侧职责):
@@ -492,6 +556,9 @@ namespace Engine
m_Device.GetDevice().destroyCommandPool(pool);
m_Allocator.Destroy(stagingAlloc);
if (m_TextureBytes != 0) {
STATS.RemoveTexture(m_TextureBytes); // 重建防双计
}
m_TextureBytes = total;
STATS.AddTexture(m_TextureBytes);
}
@@ -500,6 +567,10 @@ namespace Engine
{
const auto& vkDevice = m_Device.GetDevice();
// 批式上传若在 fence 通过前被析构(如加载中途失败):兜底释放
// staging,避免 VMA 块非空断言崩溃
ReleasePendingStaging();
vkDevice.destroySampler(m_Sampler);
vkDevice.destroyImageView(m_View);
+45
View File
@@ -0,0 +1,45 @@
MIT License
Copyright (c) 2026 ufbx-python contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
This project includes ufbx (https://github.com/ufbx/ufbx), which is licensed under the MIT License:
Copyright (c) 2020-2023 Samuli Raivio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+33096
View File
File diff suppressed because it is too large Load Diff
+6062
View File
File diff suppressed because it is too large Load Diff
+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() {
+33 -12
View File
@@ -45,18 +45,25 @@ 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)
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)
vec4 alphaOverlay; // rgb = albedo alpha 叠加色,a = 强度(0 = 关闭)
} matParams;
// 每槽 UV 变换:uv' = A·uv + t(identity 时退化为直通)。slot 必须
// 字面量调用点(编译期展开索引);6 = Material::kSlotCount
// 末尾叠加材质级镜像(misc.y = 左右,misc.z = 上下),作用全部槽:
// 纠正源资产 UV 朝向(部分角色 rip 与 glTF 上左原点约定相反)
vec2 ApplyUv(int slot, vec2 uv) {
vec4 a = matParams.uvA[slot];
vec4 t = matParams.uvT[slot];
return vec2(a.x * uv.x + a.y * uv.y + t.x,
vec2 o = vec2(a.x * uv.x + a.y * uv.y + t.x,
a.z * uv.x + a.w * uv.y + t.y);
if (matParams.misc.y > 0.5) o.x = 1.0 - o.x;
if (matParams.misc.z > 0.5) o.y = 1.0 - o.y;
return o;
}
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
@@ -117,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);
@@ -153,9 +162,13 @@ void main() {
// alphaMode=MASK 镂空(表针/栅栏等硬边贴图):低于阈值的纹素直接丢弃
if (matParams.factors.w > 0.5 && albedoTex.a < 0.5) discard;
vec3 albedo = albedoTex.rgb * matParams.baseColor.rgb;
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等局部效果):
// 强度 0 = mix 恒等,无副作用
albedo = mix(albedo, matParams.alphaOverlay.rgb,
albedoTex.a * matParams.alphaOverlay.a);
// KHR_materials_unlit:无光照语义(文字标签/示意图),albedo 直出
if (matParams.misc.x > 0.5) {
outColor = vec4(albedo, albedoTex.a * matParams.baseColor.a);
outColor = vec4(albedo, 1.0);
outVelocity = fragVelocity;
return;
}
@@ -166,8 +179,10 @@ void main() {
matParams.factors.z) * matParams.factors.y,
0.045, 1.0);
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
// 自发光强度(KHR_materials_emissive_strength,缺省 1)
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb * matParams.surface.z;
// 自发光强度(KHR_materials_emissive_strength,缺省 1);
// emissiveFactor = 颜色因子(无贴图时发光的入口——兜底贴图恒黑)
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb
* matParams.emissiveFactor.rgb * matParams.surface.z;
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, ApplyUv(1, fragUV));
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
@@ -244,21 +259,27 @@ 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;
// alpha = albedo 贴图覆盖率 × baseColorFactor.a(glTF alphaMode=BLEND
// 混合源);不透明资产两者恒 1 行为不变。预乘在 tonemap 段完成。
outColor = vec4(ambient + Lo + emissive, albedoTex.a * matParams.baseColor.a);
// 不透明段覆盖率恒 1:本 pass 只画 alphaMode=OPAQUE/MASK 表面,像素必
// 被完全覆盖。绝不能拿 albedo 贴图 alpha 当覆盖率——HI3/Genshin 脸部
// 贴图的 alpha 是腮红/卧蚕遮罩(大面积 0),写进 HDR.a 后会被 tonemap
// 的预乘 (rgb * a) 拉成纯黑。贴图 alpha 的细节用法见 alphaOverlay。
outColor = vec4(ambient + Lo + emissive, 1.0);
// ==== DEBUG B1: 纯 albedo 直出(验证贴图绑定/UV 采样)====
// outColor = vec4(albedo, 1.0);
outVelocity = fragVelocity;
+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);

Some files were not shown because too many files have changed in this diff Show More