碎片形状随机化:唯一代键替代命中点量化缓存键(每次命中重新随机 Voronoi),配套碎片模型生命周期回收(销毁次帧卸载 + OnDestroy 兜底)

This commit is contained in:
2026-10-08 23:18:35 +08:00
parent d4d37b4b28
commit affa1a7a56
2 changed files with 69 additions and 14 deletions
+63 -14
View File
@@ -25,9 +25,9 @@
// OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet // OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet
// OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy; // OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy;
// 总量上限防泄漏 // 总量上限防泄漏
// 碎片模型缓存驻留(与场景模型同策略;同代碎片共享缓存 ref,不 // 碎片模型生命周期:每代唯一缓存键(形状每次随机)+ 碎片销毁的
// 能 per-fragment 卸载,门面亦无按路径卸载——TODO 归资产系统 // 下一帧 UnloadModelByPath 回收(Destroy 帧尾延迟,本帧渲染收集
// 侧线:AssetCache 伪资产 LRU/引用计数) // 仍引用模型,同帧卸载 = 渲染悬空)+ OnDestroy 兜底
// //
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界 // 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界
// 尺寸烘焙、碎片实体复用墙 world pos/rot(scale 恒 1,防 ConvexHull // 尺寸烘焙、碎片实体复用墙 world pos/rot(scale 恒 1,防 ConvexHull
@@ -71,6 +71,11 @@ namespace {
return static_cast<ECS::Entity>(bits); return static_cast<ECS::Entity>(bits);
} }
// 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次
// 命中都重新随机生成形状(命中点量化键会让同位置重复命中
// 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了)
u32 s_GenSeq = 0;
// 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记 // 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记
// 需要任意实体的同款换算) // 需要任意实体的同款换算)
u32 BitsOf(const ECS::EntityHandle& e) { u32 BitsOf(const ECS::EntityHandle& e) {
@@ -248,10 +253,27 @@ namespace {
} }
void OnUpdate(f32 dt) override { void OnUpdate(f32 dt) override {
ApplyPendingUnload();
ApplyPendingVelocities(); ApplyPendingVelocities();
AgeAndCleanup(dt); 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: private:
// ---- 触发碎裂:藏墙 + Voronoi 切分 + spawn 碎片群 ---- // ---- 触发碎裂:藏墙 + Voronoi 切分 + spawn 碎片群 ----
void Fire(const glm::vec3& hitPoint, const glm::vec3& hitNormal) { void Fire(const glm::vec3& hitPoint, const glm::vec3& hitNormal) {
@@ -311,16 +333,11 @@ namespace {
if (seeds.size() < 4) return; // 墙面太小采样失败:不碎 if (seeds.size() < 4) return; // 墙面太小采样失败:不碎
// ---- 每种子 → cell → 棱柱 mesh → ParsedModel ---- // ---- 每种子 → cell → 棱柱 mesh → ParsedModel ----
// 缓存键量化命中点(0.25m)+ 墙尺寸:同条件重复命中复用 // 唯一代键(见 s_GenSeq 注释):形状每次随机;模型回收走
// 同一批模型缓存(Load 去重,loader 不再执行) // Fragment.modelPath + 待卸载队列(ApplyPendingUnload)
auto q = [](f32 v) { return int(std::round(v * 4.0f)); };
const std::string keyBase = const std::string keyBase =
"procedural://vorfrag/" + std::to_string(q(wallSize.x)) + "procedural://vorfrag/" +
'x' + std::to_string(q(wallSize.y)) + std::to_string(s_GenSeq++) + '/';
'x' + std::to_string(q(wallSize.z)) +
'/' + std::to_string(count) +
'/' + std::to_string(q(uvHit.x)) +
'/' + std::to_string(q(uvHit.y)) + '/';
// 砖色 baseColor(glTF baseColorFactor 语义:rgb 乘进 albedo) // 砖色 baseColor(glTF baseColorFactor 语义:rgb 乘进 albedo)
const glm::vec4 brick{140.0f / 255.0f, 92.0f / 255.0f, const glm::vec4 brick{140.0f / 255.0f, 92.0f / 255.0f,
72.0f / 255.0f, 1.0f}; 72.0f / 255.0f, 1.0f};
@@ -328,14 +345,17 @@ namespace {
auto& assets = App().GetAssets(); auto& assets = App().GetAssets();
std::vector<Engine::ModelRef> refs; std::vector<Engine::ModelRef> refs;
std::vector<glm::vec3> centroids; // 碎片质心(世界系,初速径向用) std::vector<glm::vec3> centroids; // 碎片质心(世界系,初速径向用)
std::vector<std::string> paths; // 模型缓存键(回收用,与 refs 对齐)
refs.reserve(seeds.size()); refs.reserve(seeds.size());
centroids.reserve(seeds.size()); centroids.reserve(seeds.size());
paths.reserve(seeds.size());
for (size_t s = 0; s < seeds.size(); ++s) { for (size_t s = 0; s < seeds.size(); ++s) {
const std::vector<glm::vec2> cell = const std::vector<glm::vec2> cell =
VoronoiCell(seeds[s], seeds, hw, hh); VoronoiCell(seeds[s], seeds, hw, hh);
if (cell.size() < 3) { if (cell.size() < 3) {
refs.push_back({}); // 退化 cell:占位,spawn 跳过 refs.push_back({}); // 退化 cell:占位,spawn 跳过
centroids.emplace_back(); centroids.emplace_back();
paths.emplace_back();
continue; continue;
} }
PrismMesh pm = CellToPrism(cell, hz); PrismMesh pm = CellToPrism(cell, hz);
@@ -359,6 +379,7 @@ namespace {
const Engine::ModelRef ref = const Engine::ModelRef ref =
assets.LoadModel(parsed.path, std::move(parsed)); assets.LoadModel(parsed.path, std::move(parsed));
refs.push_back(ref); refs.push_back(ref);
paths.push_back(parsed.path);
} }
// ---- spawn 碎片实体:pos/rot 复用墙位姿(mesh 已真尺寸, // ---- spawn 碎片实体:pos/rot 复用墙位姿(mesh 已真尺寸,
@@ -412,17 +433,32 @@ namespace {
0.0f}; 0.0f};
// 总量上限:超限立即回收最老碎片(防泄漏的兜底闸; // 总量上限:超限立即回收最老碎片(防泄漏的兜底闸;
// 寿命清理才是常态回收) // 寿命清理才是常态回收)。模型延迟一帧卸载
const u32 bits = BitsOf(frag); const u32 bits = BitsOf(frag);
if (m_Fragments.size() >= if (m_Fragments.size() >=
static_cast<size_t>(m_MaxFragments)) { static_cast<size_t>(m_MaxFragments)) {
Destroy(EntityFromBits(m_Fragments.front().bits)); 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.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)即注入,勿重复 ---- // ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ----
void ApplyPendingVelocities() { void ApplyPendingVelocities() {
auto& registry = App().GetScene().GetRegistry(); auto& registry = App().GetScene().GetRegistry();
@@ -431,6 +467,11 @@ namespace {
if (it->applied) { ++it; continue; } if (it->applied) { ++it; continue; }
const ECS::Entity e = EntityFromBits(it->bits); const ECS::Entity e = EntityFromBits(it->bits);
if (!registry.valid(e)) { // 实体没了(外部销毁) if (!registry.valid(e)) { // 实体没了(外部销毁)
// 模型是独占资源(一代一键),实体没了模型也回收
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(
std::move(it->modelPath));
}
it = m_Fragments.erase(it); it = m_Fragments.erase(it);
continue; continue;
} }
@@ -456,6 +497,10 @@ namespace {
if (registry.valid(e)) { if (registry.valid(e)) {
Destroy(e); Destroy(e);
} }
// 模型延迟一帧卸载(本帧渲染收集还引用它)
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(std::move(it->modelPath));
}
it = m_Fragments.erase(it); it = m_Fragments.erase(it);
} }
} }
@@ -470,6 +515,7 @@ namespace {
glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入) glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入)
f32 age = 0.0f; // 存活秒数(寿命清理用) f32 age = 0.0f; // 存活秒数(寿命清理用)
bool applied = false; // 初速已注入 bool applied = false; // 初速已注入
std::string modelPath; // 独占模型缓存键(回收时按路径卸载)
}; };
int m_Count = 30; int m_Count = 30;
@@ -485,6 +531,9 @@ namespace {
bool m_Fired = false; // 一次性触发保护 bool m_Fired = false; // 一次性触发保护
std::vector<Fragment> m_Fragments; // 活碎片簿记 std::vector<Fragment> m_Fragments; // 活碎片簿记
// 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟,
// 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针)
std::vector<std::string> m_PendingUnload;
}; };
} // namespace } // namespace
@@ -182,6 +182,12 @@ namespace Engine
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed); ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
Model* GetModel(ModelRef h) const { return m_Models.Get(h); } Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
void UnloadModel(ModelRef h) { m_Models.Unload(h); } void UnloadModel(ModelRef h) { m_Models.Unload(h); }
// 按路径卸载模型(未加载为 no-op;手上只有路径的场景,
// 如脚本程序化碎片模型回收)
void UnloadModelByPath(const std::string& path)
{
m_Models.UnloadByPath(path);
}
// 按路径查已加载的模型(未加载返回 null,不触发加载)—— // 按路径查已加载的模型(未加载返回 null,不触发加载)——
// 面板 hover 信息卡用 // 面板 hover 信息卡用
Model* GetModelByPath(const std::string& path) const Model* GetModelByPath(const std::string& path) const