Files
HelloVulkan/App/scripts/FractureLib.h
T

563 lines
26 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.
//
// 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