// // WallFracture.cpp:侧线A 验收脚本——可碎墙 + 碎片刚体群 + 寿命清理。 // // 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D // Voronoi 分割: // 碎裂瞬间在墙面上撒种子点(命中点周边加密 → 孔洞周围碎片小、 // 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面 // 交集得到不规则凸多边形 cell,沿墙厚挤出成凸棱柱——碎片在碎裂 // 前一刻完美拼合成整面墙,碎开后沿裂纹分离。碰撞形状不走 Box, // 直接用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 // CPU 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连 // 碰撞体都是真的,落地翻滚/堆积接触自然。 // // 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处 // 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛 // ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在 // 墙脚"的观感。 // // WallFracture(挂墙实体:Static Box 刚体 + MeshRenderer 立方体): // OnCollisionEnter(Dynamic 体以足够速度命中)→ 触发一次: // 藏墙(MeshRenderer/RigidBody.active = false,PhysicsWorld 对账 // 自然消化:不渲染 + body 销毁)→ Voronoi 切分 → 每碎片运行时 // 构建独立 ParsedModel(AssetManager::LoadModel(path, parsed) // 同步 GPU 构建,缓存去重)+ spawn 碎片实体 // OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet // OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy; // 总量上限防泄漏 // 碎片模型生命周期:每代唯一缓存键(形状每次随机)+ 碎片销毁的 // 下一帧 UnloadModelByPath 回收(Destroy 帧尾延迟,本帧渲染收集 // 仍引用模型,同帧卸载 = 渲染悬空)+ OnDestroy 兜底 // // 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界 // 尺寸烘焙、碎片实体复用墙 world pos/rot(scale 恒 1,防 ConvexHull // 顶点二次缩放)——墙须无旋转摆放。 // // 覆盖的引擎设施: // - Spawn()/Destroy():帧内即时创建 / 帧尾延迟销毁 // - 组件运行时装配:MeshRenderer(运行时 ParsedModel 程序化资产)+ // RigidBody(MeshCollider → ConvexHull;active=false 对账销毁) // - OnCollisionEnter:命中点/法线 + 对方实线速度查询 // - SetLinearVelocity:body 建好后的初速注入 // // 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube + // RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里 // 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击: // 墙沿裂纹碎成不规则碎片,孔洞周边碎块小、飞散明显,边缘大块 // 塌落堆积在墙脚,若干秒后清空。 // #include #include #include #include // LoadModel(门面只前向声明) #include // ParsedModel/ParsedMesh(程序化碎片几何) #include // Vertex 聚合布局 #include #include #include #include #include #include using namespace Engine; // u32/f32/Entity 等基础类型 + 门面(同 TestShoot) namespace { // bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验) ECS::Entity EntityFromBits(u32 bits) { return static_cast(bits); } // 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次 // 命中都重新随机生成形状(命中点量化键会让同位置重复命中 // 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了) u32 s_GenSeq = 0; // 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记 // 需要任意实体的同款换算) u32 BitsOf(const ECS::EntityHandle& e) { return static_cast( entt::to_integral(static_cast(e))); } // ---- 2D Voronoi(碎裂裂纹)---- // // cell = 相对其它种子的垂直平分线半平面交集,初始域 = 墙面矩形。 // Sutherland–Hodgman 凸多边形裁剪:保留靠近 p 的一侧 //(dot(a,x) <= b,a = normalize(q-p),b = dot(a,p)+|q-p|/2)。 // 输入多边形须 CCW(初始矩形即 CCW,裁剪保序)。 std::vector ClipByBisector(std::vector 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 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] 的墙面矩形) std::vector VoronoiCell(const glm::vec2& p, const std::vector& seeds, f32 hw, f32 hh) { std::vector 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 vertices; std::vector indices; std::vector cpuPositions; }; PrismMesh CellToPrism(const std::vector& 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; } // ---- 可碎墙(挂墙实体;触发后自管一整批碎片)---- class WallFracture final : public Engine::Scripting::ScriptBehavior { public: const char* Name() const override { return "WallFracture"; } void DeclareParams(Engine::Scripting::ParamBuilder& pb) override { // Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞 // 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化) pb.Field("Count", &m_Count, 30); pb.Field("SeedRadius", &m_SeedRadius, 0.9f); // 加密半径 m pb.Field("MinSeedDist", &m_MinSeedDist, 0.12f); // 种子最小间距 m // 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散: // 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线 // 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导 pb.Field("BurstSpeed", &m_BurstSpeed, 1.2f); // 近点径向初速 m/s pb.Field("Falloff", &m_Falloff, 1.0f); // 距离衰减率 1/m pb.Field("NormalPop", &m_NormalPop, 0.4f); // 法线外推 m/s pb.Field("UpBias", &m_UpBias, 0.3f); // 上抛偏置 m/s pb.Field("Lifetime", &m_Lifetime, 6.0f); // 碎片寿命(秒) pb.Field("MaxFragments", &m_MaxFragments, 128); // 活碎片上限 pb.Field("MinImpactSpeed", &m_MinImpactSpeed, 2.0f); // 触发阈值 } void OnCollisionEnter(u32 other, const glm::vec3& point, const glm::vec3& normal) override { if (m_Fired) return; // 触发条件:对方是活的 Dynamic 刚体且速度够快(过滤 // 静止贴靠/角色轻碰——子弹/抛体必中) auto& scene = App().GetScene(); const ECS::Entity otherE = EntityFromBits(other); if (!scene.GetRegistry().valid(otherE)) return; const auto* otherRb = scene.GetRegistry().try_get( otherE); if (!otherRb || otherRb->type != Engine::ECS::RigidBodyComponent::BodyType::Dynamic) { return; } const f32 speed = glm::length( App().GetPhysics().GetLinearVelocity(other)); if (speed < m_MinImpactSpeed) return; Fire(point, normal); } 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) { m_Fired = true; // 藏墙:渲染/拾取跳过 + body 对账销毁(组件级 active, // 与 Unity Renderer.enabled + Rigidbody 摘除同语义) if (auto* mr = TryGetComponent< Engine::ECS::MeshRendererComponent>()) { mr->active = false; } if (auto* rb = TryGetComponent< Engine::ECS::RigidBodyComponent>()) { rb->active = false; } // 墙几何:碎片网格顶点按墙世界尺寸烘焙(墙面 = XY 平面, // 厚度沿 Z;墙须无旋转摆放,见头注释占位局限) const auto& wall = Transform(); 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(m_Count, 4, 96); const u32 nearCount = count * 2 / 5; std::vector seeds; seeds.reserve(count); const f32 minD2 = m_MinSeedDist * m_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()); const f32 r = m_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 ---- // 唯一代键(见 s_GenSeq 注释):形状每次随机;模型回收走 // Fragment.modelPath + 待卸载队列(ApplyPendingUnload) const std::string keyBase = "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}; 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); glm::vec2 c2{0.0f}; for (const glm::vec2& p : cell) c2 += p; c2 /= static_cast(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 = brick; 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 = 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 * m_Falloff); const glm::vec3 vel = dir * m_BurstSpeed * falloff * Rand(0.4f, 1.1f) + hitNormal * m_NormalPop * Rand(0.3f, 0.8f) + glm::vec3{0.0f, m_UpBias * Rand(0.0f, 0.5f), 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( 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(); 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) { App().GetPhysics().SetLinearVelocity(it->bits, it->velocity); it->applied = true; } ++it; // body 未建:留待下一帧(对账晚一帧必到) } } // ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)---- void AgeAndCleanup(f32 dt) { auto& registry = App().GetScene().GetRegistry(); for (auto it = m_Fragments.begin(); it != m_Fragments.end();) { it->age += dt; if (it->age < m_Lifetime) { ++it; continue; } const ECS::Entity e = EntityFromBits(it->bits); if (registry.valid(e)) { Destroy(e); } // 模型延迟一帧卸载(本帧渲染收集还引用它) if (!it->modelPath.empty()) { m_PendingUnload.push_back(std::move(it->modelPath)); } it = m_Fragments.erase(it); } } static f32 Rand(f32 lo, f32 hi) { thread_local std::mt19937 rng{std::random_device{}()}; return std::uniform_real_distribution(lo, hi)(rng); } struct Fragment { u32 bits = 0; // PhysicsWorld 键(实体位模式) glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入) f32 age = 0.0f; // 存活秒数(寿命清理用) bool applied = false; // 初速已注入 std::string modelPath; // 独占模型缓存键(回收时按路径卸载) }; int m_Count = 30; f32 m_SeedRadius = 0.9f; f32 m_MinSeedDist = 0.12f; f32 m_BurstSpeed = 1.2f; f32 m_Falloff = 1.0f; f32 m_NormalPop = 0.4f; f32 m_UpBias = 0.3f; f32 m_Lifetime = 6.0f; int m_MaxFragments = 128; f32 m_MinImpactSpeed = 2.0f; bool m_Fired = false; // 一次性触发保护 std::vector m_Fragments; // 活碎片簿记 // 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟, // 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针) std::vector m_PendingUnload; }; } // namespace ENGINE_REGISTER_SCRIPT(WallFracture)