WallFracture 碎片几何升级:网格方块 → 2D Voronoi 不规则凸棱柱(命中点周边种子加密,MeshCollider 凸包,运行时 ParsedModel 程序化资产)
This commit is contained in:
+308
-111
@@ -1,48 +1,64 @@
|
|||||||
//
|
//
|
||||||
// WallFracture.cpp:侧线A 验收脚本——可碎墙 + 碎片刚体群 + 寿命清理。
|
// WallFracture.cpp:侧线A 验收脚本——可碎墙 + 碎片刚体群 + 寿命清理。
|
||||||
//
|
//
|
||||||
// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散:
|
// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D
|
||||||
// 碎片沿裂纹网格分离,初速 = 径向 × 距离指数衰减(只有命中点
|
// Voronoi 分割:
|
||||||
// 周边的碎片被推动,远处几乎原地悬置)+ 沿命中法线轻微外推
|
// 碎裂瞬间在墙面上撒种子点(命中点周边加密 → 孔洞周围碎片小、
|
||||||
// (朝弹侧剥落)+ 微弱上抛——重力是塌落的主导,整体呈
|
// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面
|
||||||
// "裂纹分离 → 孔洞塌落 → 碎块堆在墙脚"的观感。
|
// 交集得到不规则凸多边形 cell,沿墙厚挤出成凸棱柱——碎片在碎裂
|
||||||
|
// 前一刻完美拼合成整面墙,碎开后沿裂纹分离。碰撞形状不走 Box,
|
||||||
|
// 直接用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格
|
||||||
|
// CPU 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连
|
||||||
|
// 碰撞体都是真的,落地翻滚/堆积接触自然。
|
||||||
|
//
|
||||||
|
// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处
|
||||||
|
// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛
|
||||||
|
// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在
|
||||||
|
// 墙脚"的观感。
|
||||||
//
|
//
|
||||||
// WallFracture(挂墙实体:Static Box 刚体 + MeshRenderer 立方体):
|
// WallFracture(挂墙实体:Static Box 刚体 + MeshRenderer 立方体):
|
||||||
// OnCollisionEnter(Dynamic 体以足够速度命中)→ 触发一次:
|
// OnCollisionEnter(Dynamic 体以足够速度命中)→ 触发一次:
|
||||||
// 藏墙(MeshRenderer/RigidBody.active = false,PhysicsWorld 对账
|
// 藏墙(MeshRenderer/RigidBody.active = false,PhysicsWorld 对账
|
||||||
// 自然消化:不渲染 + body 销毁)→ 按网格把墙体积切成 N 个碎片
|
// 自然消化:不渲染 + body 销毁)→ Voronoi 切分 → 每碎片运行时
|
||||||
// 实体(共享一份程序化立方体模型 + Dynamic Box 刚体),初速记入
|
// 构建独立 ParsedModel(AssetManager::LoadModel(path, parsed)
|
||||||
// 待办表
|
// 同步 GPU 构建,缓存去重)+ spawn 碎片实体
|
||||||
// OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet
|
// OnUpdate:碎片簿记——body 建好(下一帧 Step 对账,同 Bullet
|
||||||
// OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy;
|
// OnCreate 施速的时序契约)即统一施加初速;寿命到期统一 Destroy;
|
||||||
// 总量上限防泄漏
|
// 总量上限防泄漏
|
||||||
|
// 碎片模型缓存驻留(与场景模型同策略;同代碎片共享缓存 ref,不
|
||||||
|
// 能 per-fragment 卸载,门面亦无按路径卸载——TODO 归资产系统
|
||||||
|
// 侧线:AssetCache 伪资产 LRU/引用计数)
|
||||||
//
|
//
|
||||||
// 碎片不挂脚本:单一 WallFracture 集中管理全部碎片的施速/寿命/
|
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界
|
||||||
// 清理(一帧 spawn 几十个实体,每颗一个脚本实例纯属浪费——
|
// 尺寸烘焙、碎片实体复用墙 world pos/rot(scale 恒 1,防 ConvexHull
|
||||||
// Unity 也用同一个 Explosion 脚本管一片 Fragments)。
|
// 顶点二次缩放)——墙须无旋转摆放。
|
||||||
//
|
//
|
||||||
// 覆盖的引擎设施:
|
// 覆盖的引擎设施:
|
||||||
// - Spawn()/Destroy():帧内即时创建 / 帧尾延迟销毁
|
// - Spawn()/Destroy():帧内即时创建 / 帧尾延迟销毁
|
||||||
// - 组件运行时装配:MeshRenderer(程序化 CreateCube 资产)+
|
// - 组件运行时装配:MeshRenderer(运行时 ParsedModel 程序化资产)+
|
||||||
// RigidBody(下一帧对账建 body;active=false 对账销毁)
|
// RigidBody(MeshCollider → ConvexHull;active=false 对账销毁)
|
||||||
// - OnCollisionEnter:命中点/法线 + 对方实线速度查询
|
// - OnCollisionEnter:命中点/法线 + 对方实线速度查询
|
||||||
// - SetLinearVelocity:body 建好后的初速注入
|
// - SetLinearVelocity:body 建好后的初速注入
|
||||||
//
|
//
|
||||||
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
|
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
|
||||||
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
|
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
|
||||||
// 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击:
|
// 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击:
|
||||||
// 墙消失,碎片群从命中点径向飞散、翻滚、落地堆积,若干秒后清空。
|
// 墙沿裂纹碎成不规则碎片,孔洞周边碎块小、飞散明显,边缘大块
|
||||||
|
// 塌落堆积在墙脚,若干秒后清空。
|
||||||
//
|
//
|
||||||
|
|
||||||
#include <Engine/Scripting/Script.h>
|
#include <Engine/Scripting/Script.h>
|
||||||
#include <Engine/Core/Application.h>
|
#include <Engine/Core/Application.h>
|
||||||
#include <Engine/Core/Types.h>
|
#include <Engine/Core/Types.h>
|
||||||
#include <Engine/Assets/AssetManager.h> // CreateCube(门面只前向声明)
|
#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 <Engine/Physics/PhysicsWorld.h>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtc/quaternion.hpp>
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
@@ -62,17 +78,139 @@ namespace {
|
|||||||
entt::to_integral(static_cast<entt::entity>(e)));
|
entt::to_integral(static_cast<entt::entity>(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<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] 的墙面矩形)
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 可碎墙(挂墙实体;触发后自管一整批碎片)----
|
// ---- 可碎墙(挂墙实体;触发后自管一整批碎片)----
|
||||||
class WallFracture final : public Engine::Scripting::ScriptBehavior {
|
class WallFracture final : public Engine::Scripting::ScriptBehavior {
|
||||||
public:
|
public:
|
||||||
const char* Name() const override { return "WallFracture"; }
|
const char* Name() const override { return "WallFracture"; }
|
||||||
|
|
||||||
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
|
||||||
// 碎片网格:每轴切几刀(总碎片 = x*y*z;占位档位 = 每碎片
|
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
|
||||||
// 一实体,网格 8*6*1=48 起步,中等规模才考虑 instanced draw)
|
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
|
||||||
pb.Field("GridX", &m_GridX, 8);
|
pb.Field("Count", &m_Count, 30);
|
||||||
pb.Field("GridY", &m_GridY, 6);
|
pb.Field("SeedRadius", &m_SeedRadius, 0.9f); // 加密半径 m
|
||||||
pb.Field("GridZ", &m_GridZ, 1);
|
pb.Field("MinSeedDist", &m_MinSeedDist, 0.12f); // 种子最小间距 m
|
||||||
// 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散:
|
// 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散:
|
||||||
// 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线
|
// 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线
|
||||||
// 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导
|
// 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导
|
||||||
@@ -115,10 +253,9 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// ---- 触发碎裂:藏墙 + 切网格 + spawn 碎片群 ----
|
// ---- 触发碎裂:藏墙 + Voronoi 切分 + spawn 碎片群 ----
|
||||||
void Fire(const glm::vec3& hitPoint, const glm::vec3& hitNormal) {
|
void Fire(const glm::vec3& hitPoint, const glm::vec3& hitNormal) {
|
||||||
m_Fired = true;
|
m_Fired = true;
|
||||||
m_HitNormal = hitNormal;
|
|
||||||
|
|
||||||
// 藏墙:渲染/拾取跳过 + body 对账销毁(组件级 active,
|
// 藏墙:渲染/拾取跳过 + body 对账销毁(组件级 active,
|
||||||
// 与 Unity Renderer.enabled + Rigidbody 摘除同语义)
|
// 与 Unity Renderer.enabled + Rigidbody 摘除同语义)
|
||||||
@@ -131,97 +268,158 @@ namespace {
|
|||||||
rb->active = false;
|
rb->active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 墙几何:立方体模型半边长 0.5(单位体),墙尺寸 =
|
// 墙几何:碎片网格顶点按墙世界尺寸烘焙(墙面 = XY 平面,
|
||||||
// Transform 世界缩放;碎片尺寸 = 墙尺寸 / 网格
|
// 厚度沿 Z;墙须无旋转摆放,见头注释占位局限)
|
||||||
const auto& wall = Transform();
|
const auto& wall = Transform();
|
||||||
const glm::vec3 wallPos = wall.GetWorldPosition();
|
const glm::vec3 wallPos = wall.GetWorldPosition();
|
||||||
const glm::quat wallRot = wall.GetWorldRotationQuat();
|
|
||||||
const glm::vec3 wallSize = wall.GetWorldScale();
|
const glm::vec3 wallSize = wall.GetWorldScale();
|
||||||
const glm::ivec3 grid{m_GridX, m_GridY, m_GridZ};
|
const f32 hw = 0.5f * wallSize.x;
|
||||||
const glm::vec3 fragSize = wallSize /
|
const f32 hh = 0.5f * wallSize.y;
|
||||||
glm::vec3{std::max(1, m_GridX), std::max(1, m_GridY),
|
const f32 hz = 0.5f * wallSize.z;
|
||||||
std::max(1, m_GridZ)};
|
|
||||||
|
|
||||||
// 共享一份程序化立方体(缓存去重:同参数全场景一把 mesh)
|
// 命中点 → 墙局部(逆旋转 + 平移),XY 即裂纹平面坐标
|
||||||
const Engine::ModelRef brick =
|
const glm::quat invRot = glm::conjugate(
|
||||||
App().GetAssets().CreateCube(0.5f, 140, 92, 72);
|
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) {
|
// ---- 种子:40% 命中点半径内加密 + 60% 全墙均匀,最小
|
||||||
for (int j = 0; j < grid.y; ++j) {
|
// 间距拒绝采样(防碎片退化为细条)----
|
||||||
for (int k = 0; k < grid.z; ++k) {
|
const u32 count = std::clamp(m_Count, 4, 96);
|
||||||
// 单元中心(墙局部)→ 世界(墙旋转/平移搬运;
|
const u32 nearCount = count * 2 / 5;
|
||||||
// 加单元内抖动避免完美网格的假感)
|
std::vector<glm::vec2> seeds;
|
||||||
const glm::vec3 cell{
|
seeds.reserve(count);
|
||||||
(i + 0.5f) * fragSize.x - wallSize.x * 0.5f,
|
const f32 minD2 = m_MinSeedDist * m_MinSeedDist;
|
||||||
(j + 0.5f) * fragSize.y - wallSize.y * 0.5f,
|
auto tryPush = [&](const glm::vec2& s) {
|
||||||
(k + 0.5f) * fragSize.z - wallSize.z * 0.5f};
|
if (s.x < -hw || s.x > hw || s.y < -hh || s.y > hh) return;
|
||||||
const glm::vec3 jitter{
|
for (const glm::vec2& e : seeds) {
|
||||||
Rand(-0.3f, 0.3f) * fragSize.x,
|
if (glm::dot(e - s, e - s) < minD2) return;
|
||||||
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<size_t>(m_MaxFragments)) {
|
|
||||||
Destroy(EntityFromBits(m_Fragments.front().bits));
|
|
||||||
m_Fragments.erase(m_Fragments.begin());
|
|
||||||
}
|
|
||||||
m_Fragments.push_back({bits, vel, 0.0f});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 = 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<Engine::ModelRef> refs;
|
||||||
|
std::vector<glm::vec3> centroids; // 碎片质心(世界系,初速径向用)
|
||||||
|
refs.reserve(seeds.size());
|
||||||
|
centroids.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();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PrismMesh pm = CellToPrism(cell, hz);
|
||||||
|
glm::vec2 c2{0.0f};
|
||||||
|
for (const glm::vec2& p : cell) c2 += p;
|
||||||
|
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 = 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<size_t>(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; // 初速已注入
|
bool applied = false; // 初速已注入
|
||||||
};
|
};
|
||||||
|
|
||||||
int m_GridX = 8;
|
int m_Count = 30;
|
||||||
int m_GridY = 6;
|
f32 m_SeedRadius = 0.9f;
|
||||||
int m_GridZ = 1;
|
f32 m_MinSeedDist = 0.12f;
|
||||||
f32 m_BurstSpeed = 1.2f;
|
f32 m_BurstSpeed = 1.2f;
|
||||||
f32 m_Falloff = 1.0f;
|
f32 m_Falloff = 1.0f;
|
||||||
f32 m_NormalPop = 0.4f;
|
f32 m_NormalPop = 0.4f;
|
||||||
@@ -285,7 +483,6 @@ namespace {
|
|||||||
int m_MaxFragments = 128;
|
int m_MaxFragments = 128;
|
||||||
f32 m_MinImpactSpeed = 2.0f;
|
f32 m_MinImpactSpeed = 2.0f;
|
||||||
|
|
||||||
glm::vec3 m_HitNormal{0.0f, 0.0f, 1.0f}; // 命中法线(剥落方向)
|
|
||||||
bool m_Fired = false; // 一次性触发保护
|
bool m_Fired = false; // 一次性触发保护
|
||||||
std::vector<Fragment> m_Fragments; // 活碎片簿记
|
std::vector<Fragment> m_Fragments; // 活碎片簿记
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user