From 21da38db92324d7adf48a79346a5b020ca4483ab Mon Sep 17 00:00:00 2001 From: atdunbg Date: Thu, 8 Oct 2026 22:58:40 +0800 Subject: [PATCH] =?UTF-8?q?WallFracture=20=E7=A2=8E=E7=89=87=E5=87=A0?= =?UTF-8?q?=E4=BD=95=E5=8D=87=E7=BA=A7=EF=BC=9A=E7=BD=91=E6=A0=BC=E6=96=B9?= =?UTF-8?q?=E5=9D=97=20=E2=86=92=202D=20Voronoi=20=E4=B8=8D=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=87=B8=E6=A3=B1=E6=9F=B1=EF=BC=88=E5=91=BD=E4=B8=AD?= =?UTF-8?q?=E7=82=B9=E5=91=A8=E8=BE=B9=E7=A7=8D=E5=AD=90=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=EF=BC=8CMeshCollider=20=E5=87=B8=E5=8C=85=EF=BC=8C=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=20ParsedModel=20=E7=A8=8B=E5=BA=8F=E5=8C=96?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/scripts/WallFracture.cpp | 419 +++++++++++++++++++++++++---------- 1 file changed, 308 insertions(+), 111 deletions(-) diff --git a/App/scripts/WallFracture.cpp b/App/scripts/WallFracture.cpp index dbf41c5..64a867e 100644 --- a/App/scripts/WallFracture.cpp +++ b/App/scripts/WallFracture.cpp @@ -1,48 +1,64 @@ // // 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 销毁)→ 按网格把墙体积切成 N 个碎片 -// 实体(共享一份程序化立方体模型 + Dynamic Box 刚体),初速记入 -// 待办表 +// 自然消化:不渲染 + body 销毁)→ Voronoi 切分 → 每碎片运行时 +// 构建独立 ParsedModel(AssetManager::LoadModel(path, parsed) +// 同步 GPU 构建,缓存去重)+ spawn 碎片实体 // OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet // OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy; // 总量上限防泄漏 +// 碎片模型缓存驻留(与场景模型同策略;同代碎片共享缓存 ref,不 +// 能 per-fragment 卸载,门面亦无按路径卸载——TODO 归资产系统 +// 侧线:AssetCache 伪资产 LRU/引用计数) // -// 碎片不挂脚本:单一 WallFracture 集中管理全部碎片的施速/寿命/ -// 清理(一帧 spawn 几十个实体,每颗一个脚本实例纯属浪费—— -// Unity 也用同一个 Explosion 脚本管一片 Fragments)。 +// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界 +// 尺寸烘焙、碎片实体复用墙 world pos/rot(scale 恒 1,防 ConvexHull +// 顶点二次缩放)——墙须无旋转摆放。 // // 覆盖的引擎设施: // - Spawn()/Destroy():帧内即时创建 / 帧尾延迟销毁 -// - 组件运行时装配:MeshRenderer(程序化 CreateCube 资产)+ -// RigidBody(下一帧对账建 body;active=false 对账销毁) +// - 组件运行时装配:MeshRenderer(运行时 ParsedModel 程序化资产)+ +// RigidBody(MeshCollider → ConvexHull;active=false 对账销毁) // - OnCollisionEnter:命中点/法线 + 对方实线速度查询 // - SetLinearVelocity:body 建好后的初速注入 // // 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube + // RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里 // 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击: -// 墙消失,碎片群从命中点径向飞散、翻滚、落地堆积,若干秒后清空。 +// 墙沿裂纹碎成不规则碎片,孔洞周边碎块小、飞散明显,边缘大块 +// 塌落堆积在墙脚,若干秒后清空。 // #include #include #include -#include // CreateCube(门面只前向声明) +#include // LoadModel(门面只前向声明) +#include // ParsedModel/ParsedMesh(程序化碎片几何) +#include // Vertex 聚合布局 #include #include #include +#include #include #include @@ -62,17 +78,139 @@ namespace { 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 { - // 碎片网格:每轴切几刀(总碎片 = x*y*z;占位档位 = 每碎片 - // 一实体,网格 8*6*1=48 起步,中等规模才考虑 instanced draw) - pb.Field("GridX", &m_GridX, 8); - pb.Field("GridY", &m_GridY, 6); - pb.Field("GridZ", &m_GridZ, 1); + // Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞 + // 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化) + pb.Field("Count", &m_Count, 30); + pb.Field("SeedRadius", &m_SeedRadius, 0.9f); // 加密半径 m + pb.Field("MinSeedDist", &m_MinSeedDist, 0.12f); // 种子最小间距 m // 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散: // 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线 // 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导 @@ -115,10 +253,9 @@ namespace { } private: - // ---- 触发碎裂:藏墙 + 切网格 + spawn 碎片群 ---- + // ---- 触发碎裂:藏墙 + Voronoi 切分 + spawn 碎片群 ---- void Fire(const glm::vec3& hitPoint, const glm::vec3& hitNormal) { m_Fired = true; - m_HitNormal = hitNormal; // 藏墙:渲染/拾取跳过 + body 对账销毁(组件级 active, // 与 Unity Renderer.enabled + Rigidbody 摘除同语义) @@ -131,97 +268,158 @@ namespace { rb->active = false; } - // 墙几何:立方体模型半边长 0.5(单位体),墙尺寸 = - // Transform 世界缩放;碎片尺寸 = 墙尺寸 / 网格 + // 墙几何:碎片网格顶点按墙世界尺寸烘焙(墙面 = XY 平面, + // 厚度沿 Z;墙须无旋转摆放,见头注释占位局限) const auto& wall = Transform(); const glm::vec3 wallPos = wall.GetWorldPosition(); - const glm::quat wallRot = wall.GetWorldRotationQuat(); const glm::vec3 wallSize = wall.GetWorldScale(); - const glm::ivec3 grid{m_GridX, m_GridY, m_GridZ}; - const glm::vec3 fragSize = wallSize / - glm::vec3{std::max(1, m_GridX), std::max(1, m_GridY), - std::max(1, m_GridZ)}; + const f32 hw = 0.5f * wallSize.x; + const f32 hh = 0.5f * wallSize.y; + const f32 hz = 0.5f * wallSize.z; - // 共享一份程序化立方体(缓存去重:同参数全场景一把 mesh) - const Engine::ModelRef brick = - App().GetAssets().CreateCube(0.5f, 140, 92, 72); + // 命中点 → 墙局部(逆旋转 + 平移),XY 即裂纹平面坐标 + const glm::quat invRot = glm::conjugate( + wall.GetWorldRotationQuat()); + const glm::vec3 localHit = invRot * (hitPoint - wallPos); + const glm::vec2 uvHit{localHit.x, localHit.y}; - for (int i = 0; i < grid.x; ++i) { - for (int j = 0; j < grid.y; ++j) { - for (int k = 0; k < grid.z; ++k) { - // 单元中心(墙局部)→ 世界(墙旋转/平移搬运; - // 加单元内抖动避免完美网格的假感) - const glm::vec3 cell{ - (i + 0.5f) * fragSize.x - wallSize.x * 0.5f, - (j + 0.5f) * fragSize.y - wallSize.y * 0.5f, - (k + 0.5f) * fragSize.z - wallSize.z * 0.5f}; - const glm::vec3 jitter{ - Rand(-0.3f, 0.3f) * fragSize.x, - Rand(-0.3f, 0.3f) * fragSize.y, - Rand(-0.3f, 0.3f) * fragSize.z}; - const glm::vec3 pos = - wallPos + wallRot * (cell + jitter); - - auto frag = Spawn("fragment"); - auto& tr = frag.GetComponent< - Engine::ECS::TransformComponent>(); - tr.SetPosition(pos); - // 随机小倾角:落地堆积不齐整,视觉更像碎块 - tr.SetRotationQuat(glm::quat(glm::vec3{ - glm::radians(Rand(-30.0f, 30.0f)), - glm::radians(Rand(-180.0f, 180.0f)), - glm::radians(Rand(-30.0f, 30.0f))})); - tr.SetScale(fragSize); - - auto& fmr = - frag.AddComponent< - Engine::ECS::MeshRendererComponent>(); - fmr.model = brick; - - // Dynamic Box:boxHalfExtents 默认 0.5(单位体) - // × Transform 缩放 = 恰好碎片尺寸,无需改字段 - auto& frb = - frag.AddComponent< - Engine::ECS::RigidBodyComponent>(); - frb.type = - Engine::ECS::RigidBodyComponent::BodyType::Dynamic; - frb.collider = - Engine::ECS::RigidBodyComponent::ColliderType::Box; - frb.mass = 0.4f; - frb.friction = 0.7f; - frb.restitution = 0.05f; - - // 碎裂初速(玻璃式,非爆炸):离命中点越远越 - // 接近静止(指数衰减)——只有孔洞周边的碎片被 - // 冲击波推动,整体呈"裂纹分离→重力塌落"; - // 法线外推让碎片朝弹侧剥出,而非向墙内挤压 - glm::vec3 dir = pos - 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)); - m_Fragments.erase(m_Fragments.begin()); - } - m_Fragments.push_back({bits, vel, 0.0f}); - } + // ---- 种子: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 ---- + // 缓存键量化命中点(0.25m)+ 墙尺寸:同条件重复命中复用 + // 同一批模型缓存(Load 去重,loader 不再执行) + auto q = [](f32 v) { return int(std::round(v * 4.0f)); }; + 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)) + '/'; + // 砖色 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; // 碎片质心(世界系,初速径向用) + refs.reserve(seeds.size()); + centroids.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(); + 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); + } + + // ---- 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)); + m_Fragments.erase(m_Fragments.begin()); + } + m_Fragments.push_back({bits, vel, 0.0f}); } } @@ -274,9 +472,9 @@ namespace { bool applied = false; // 初速已注入 }; - int m_GridX = 8; - int m_GridY = 6; - int m_GridZ = 1; + 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; @@ -285,7 +483,6 @@ namespace { int m_MaxFragments = 128; f32 m_MinImpactSpeed = 2.0f; - glm::vec3 m_HitNormal{0.0f, 0.0f, 1.0f}; // 命中法线(剥落方向) bool m_Fired = false; // 一次性触发保护 std::vector m_Fragments; // 活碎片簿记 };