563 lines
26 KiB
C++
563 lines
26 KiB
C++
//
|
||
// FractureLib.h:共享碎裂库(自 WallFracture.cpp 提取,行为逐行保持)。
|
||
//
|
||
// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D
|
||
// Voronoi 分割:
|
||
// 碎裂瞬间在面上撒种子点(命中点周边加密 → 孔洞周围碎片小、
|
||
// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面
|
||
// 交集得到不规则凸多边形 cell,沿面厚挤出成凸棱柱——碎片在碎裂
|
||
// 前一刻完美拼合原面,碎开后沿裂纹分离。碰撞形状不走 Box,直接
|
||
// 用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 CPU
|
||
// 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连碰撞体
|
||
// 都是真的,落地翻滚/堆积接触自然。
|
||
//
|
||
// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处
|
||
// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛
|
||
// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在
|
||
// 墙脚"的观感。
|
||
//
|
||
// 用法(宿主 = 挂在可碎实体上的脚本):
|
||
// Fracture::Params p_; // DeclareParams 直绑成员(pb.Field)
|
||
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
|
||
// 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(默认砖色不透明;玻璃靶 = 半透明
|
||
// + 自发光)
|
||
// 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;
|
||
};
|
||
|
||
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz) {
|
||
PrismMesh m;
|
||
const size_t n = cell.size();
|
||
const size_t topBase = 0;
|
||
const size_t botBase = n;
|
||
m.vertices.reserve(n * 2);
|
||
|
||
for (const glm::vec2& p : cell) {
|
||
const Engine::Vertex v{
|
||
{p.x, p.y, hz}, {0.0f, 0.0f, 1.0f},
|
||
{p.x, p.y}, {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},
|
||
{p.x, p.y}, {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;
|
||
}
|
||
|
||
// ---- 参数(原 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)
|
||
};
|
||
|
||
// ---- 碎片外观(Fire 时套用到全部碎片网格;默认砖色不透明 =
|
||
// 原 WallFracture 观感)----
|
||
struct Appearance {
|
||
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;
|
||
|
||
// 对方须是活的 Dynamic 刚体且速度够快(过滤静止贴靠/
|
||
// 角色轻碰——子弹/抛体必中)
|
||
auto& registry = host.app.GetScene().GetRegistry();
|
||
const ECS::Entity otherE = EntityFromBits(other);
|
||
if (!registry.valid(otherE)) return false;
|
||
const auto* otherRb =
|
||
registry.try_get<Engine::ECS::RigidBodyComponent>(otherE);
|
||
if (!otherRb ||
|
||
otherRb->type !=
|
||
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||
return false;
|
||
}
|
||
const f32 speed = glm::length(
|
||
host.app.GetPhysics().GetLinearVelocity(other));
|
||
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;
|
||
|
||
// 命中点 → 宿主局部(逆旋转 + 平移),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);
|
||
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);
|
||
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];
|
||
|
||
// 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
|