From affa1a7a564ffcce3c146f91a3b50eb7a93d16ad Mon Sep 17 00:00:00 2001 From: atdunbg Date: Thu, 8 Oct 2026 23:18:35 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A2=8E=E7=89=87=E5=BD=A2=E7=8A=B6=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=E5=8C=96=EF=BC=9A=E5=94=AF=E4=B8=80=E4=BB=A3=E9=94=AE?= =?UTF-8?q?=E6=9B=BF=E4=BB=A3=E5=91=BD=E4=B8=AD=E7=82=B9=E9=87=8F=E5=8C=96?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E9=94=AE=EF=BC=88=E6=AF=8F=E6=AC=A1=E5=91=BD?= =?UTF-8?q?=E4=B8=AD=E9=87=8D=E6=96=B0=E9=9A=8F=E6=9C=BA=20Voronoi?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E9=85=8D=E5=A5=97=E7=A2=8E=E7=89=87=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E5=9B=9E=E6=94=B6?= =?UTF-8?q?=EF=BC=88=E9=94=80=E6=AF=81=E6=AC=A1=E5=B8=A7=E5=8D=B8=E8=BD=BD?= =?UTF-8?q?=20+=20OnDestroy=20=E5=85=9C=E5=BA=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/scripts/WallFracture.cpp | 77 +++++++++++++++++---- Engine/include/Engine/Assets/AssetManager.h | 6 ++ 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/App/scripts/WallFracture.cpp b/App/scripts/WallFracture.cpp index 64a867e..a191ec5 100644 --- a/App/scripts/WallFracture.cpp +++ b/App/scripts/WallFracture.cpp @@ -25,9 +25,9 @@ // OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet // OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy; // 总量上限防泄漏 -// 碎片模型缓存驻留(与场景模型同策略;同代碎片共享缓存 ref,不 -// 能 per-fragment 卸载,门面亦无按路径卸载——TODO 归资产系统 -// 侧线:AssetCache 伪资产 LRU/引用计数) +// 碎片模型生命周期:每代唯一缓存键(形状每次随机)+ 碎片销毁的 +// 下一帧 UnloadModelByPath 回收(Destroy 帧尾延迟,本帧渲染收集 +// 仍引用模型,同帧卸载 = 渲染悬空)+ OnDestroy 兜底 // // 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界 // 尺寸烘焙、碎片实体复用墙 world pos/rot(scale 恒 1,防 ConvexHull @@ -71,6 +71,11 @@ namespace { return static_cast(bits); } + // 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次 + // 命中都重新随机生成形状(命中点量化键会让同位置重复命中 + // 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了) + u32 s_GenSeq = 0; + // 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记 // 需要任意实体的同款换算) u32 BitsOf(const ECS::EntityHandle& e) { @@ -248,10 +253,27 @@ namespace { } void OnUpdate(f32 dt) override { + ApplyPendingUnload(); ApplyPendingVelocities(); AgeAndCleanup(dt); } + // 墙销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡处理, + // 独占碎片模型全部回收(模型不被快照恢复的场景引用——唯一代键) + void OnDestroy() override { + auto& assets = App().GetAssets(); + for (const Fragment& f : m_Fragments) { + if (!f.modelPath.empty()) { + assets.UnloadModelByPath(f.modelPath); + } + } + m_Fragments.clear(); + for (const std::string& p : m_PendingUnload) { + assets.UnloadModelByPath(p); + } + m_PendingUnload.clear(); + } + private: // ---- 触发碎裂:藏墙 + Voronoi 切分 + spawn 碎片群 ---- void Fire(const glm::vec3& hitPoint, const glm::vec3& hitNormal) { @@ -311,16 +333,11 @@ namespace { if (seeds.size() < 4) return; // 墙面太小采样失败:不碎 // ---- 每种子 → cell → 棱柱 mesh → ParsedModel ---- - // 缓存键量化命中点(0.25m)+ 墙尺寸:同条件重复命中复用 - // 同一批模型缓存(Load 去重,loader 不再执行) - auto q = [](f32 v) { return int(std::round(v * 4.0f)); }; + // 唯一代键(见 s_GenSeq 注释):形状每次随机;模型回收走 + // Fragment.modelPath + 待卸载队列(ApplyPendingUnload) const std::string keyBase = - "procedural://vorfrag/" + std::to_string(q(wallSize.x)) + - 'x' + std::to_string(q(wallSize.y)) + - 'x' + std::to_string(q(wallSize.z)) + - '/' + std::to_string(count) + - '/' + std::to_string(q(uvHit.x)) + - '/' + std::to_string(q(uvHit.y)) + '/'; + "procedural://vorfrag/" + + std::to_string(s_GenSeq++) + '/'; // 砖色 baseColor(glTF baseColorFactor 语义:rgb 乘进 albedo) const glm::vec4 brick{140.0f / 255.0f, 92.0f / 255.0f, 72.0f / 255.0f, 1.0f}; @@ -328,14 +345,17 @@ namespace { auto& assets = App().GetAssets(); std::vector refs; std::vector centroids; // 碎片质心(世界系,初速径向用) + std::vector 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 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); @@ -359,6 +379,7 @@ namespace { const Engine::ModelRef ref = assets.LoadModel(parsed.path, std::move(parsed)); refs.push_back(ref); + paths.push_back(parsed.path); } // ---- spawn 碎片实体:pos/rot 复用墙位姿(mesh 已真尺寸, @@ -412,17 +433,32 @@ namespace { 0.0f}; // 总量上限:超限立即回收最老碎片(防泄漏的兜底闸; - // 寿命清理才是常态回收) + // 寿命清理才是常态回收)。模型延迟一帧卸载 const u32 bits = BitsOf(frag); if (m_Fragments.size() >= static_cast(m_MaxFragments)) { 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({bits, vel, 0.0f}); + m_Fragments.push_back( + Fragment{bits, vel, 0.0f, false, paths[s]}); } } + // ---- 模型回收:延迟一帧执行(Destroy 是帧尾延迟,碎片实体 + // 本帧渲染收集仍会引用模型;下一帧实体已灭,卸载才安全)---- + void ApplyPendingUnload() { + auto& assets = App().GetAssets(); + for (const std::string& p : m_PendingUnload) { + assets.UnloadModelByPath(p); + } + m_PendingUnload.clear(); + } + // ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ---- void ApplyPendingVelocities() { auto& registry = App().GetScene().GetRegistry(); @@ -431,6 +467,11 @@ namespace { 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; } @@ -456,6 +497,10 @@ namespace { if (registry.valid(e)) { Destroy(e); } + // 模型延迟一帧卸载(本帧渲染收集还引用它) + if (!it->modelPath.empty()) { + m_PendingUnload.push_back(std::move(it->modelPath)); + } it = m_Fragments.erase(it); } } @@ -470,6 +515,7 @@ namespace { glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入) f32 age = 0.0f; // 存活秒数(寿命清理用) bool applied = false; // 初速已注入 + std::string modelPath; // 独占模型缓存键(回收时按路径卸载) }; int m_Count = 30; @@ -485,6 +531,9 @@ namespace { bool m_Fired = false; // 一次性触发保护 std::vector m_Fragments; // 活碎片簿记 + // 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟, + // 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针) + std::vector m_PendingUnload; }; } // namespace diff --git a/Engine/include/Engine/Assets/AssetManager.h b/Engine/include/Engine/Assets/AssetManager.h index 7a90d27..c22905e 100644 --- a/Engine/include/Engine/Assets/AssetManager.h +++ b/Engine/include/Engine/Assets/AssetManager.h @@ -182,6 +182,12 @@ 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;手上只有路径的场景, + // 如脚本程序化碎片模型回收) + void UnloadModelByPath(const std::string& path) + { + m_Models.UnloadByPath(path); + } // 按路径查已加载的模型(未加载返回 null,不触发加载)—— // 面板 hover 信息卡用 Model* GetModelByPath(const std::string& path) const