Files
HelloVulkan/App/scripts/WallFracture.cpp
T

493 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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;
// 总量上限防泄漏
// 碎片模型缓存驻留(与场景模型同策略;同代碎片共享缓存 ref,不
// 能 per-fragment 卸载,门面亦无按路径卸载——TODO 归资产系统
// 侧线:AssetCache 伪资产 LRU/引用计数)
//
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按墙世界
// 尺寸烘焙、碎片实体复用墙 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 <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 <random>
using namespace Engine; // u32/f32/Entity 等基础类型 + 门面(同 TestShoot)
namespace {
// bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验)
ECS::Entity EntityFromBits(u32 bits) {
return static_cast<ECS::Entity>(bits);
}
// 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记
// 需要任意实体的同款换算)
u32 BitsOf(const ECS::EntityHandle& e) {
return static_cast<u32>(
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 {
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<Engine::ECS::RigidBodyComponent>(
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 {
ApplyPendingVelocities();
AgeAndCleanup(dt);
}
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<glm::vec2> 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<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});
}
}
// ---- 施速待办: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)) { // 实体没了(外部销毁)
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);
}
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<f32>(lo, hi)(rng);
}
struct Fragment {
u32 bits = 0; // PhysicsWorld 键(实体位模式)
glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入)
f32 age = 0.0f; // 存活秒数(寿命清理用)
bool applied = false; // 初速已注入
};
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<Fragment> m_Fragments; // 活碎片簿记
};
} // namespace
ENGINE_REGISTER_SCRIPT(WallFracture)