UE5 通过Dynamic Mesh制作实时子弹击中玻璃破碎效果
首先了解Dynamic Mesh的原理Dynamic Mesh 是什么DynamicMesh在 UE 里主要指UE::Geometry::FDynamicMesh3这一套东西。它本质上是一个CPU 侧的运行时网格数据结构。也就是说它不是 GPU 上已经画出来的东西也不是一个提前导入好的 Static Mesh 资产。它是一份可以在运行时被 C 修改的几何数据有哪些点 哪些点组成三角形 每个三角形用什么材质 每个顶点/三角形的法线是什么 UV 是什么 颜色是什么这些数据存在 CPU 内存里。你可以随时增删顶点、增删三角形、重新生成法线、重新生成 UV然后再交给组件显示。可以把它理解成FDynamicMesh3 网格数据本体 UDynamicMeshComponent 把这份网格显示到世界里的组件Static Mesh 和 Dynamic Mesh 的区别StaticMesh更像是“烤好的模型资产”。例如你从 Maya/Blender 导入一块玻璃SM_GlassPane.uasset它的顶点、三角形、UV、LOD、碰撞通常提前生成好。运行时可以移动它、换材质但不适合频繁改它的拓扑。DynamicMesh是“运行时现场捏出来的模型”。比如你想在游戏运行时鼠标切割模型 布尔运算 生成洞 实时破碎 运行时建模工具 程序化地形 动态切片这些就更适合DynamicMesh。区别可以这样看Static Mesh 提前做好的模型 运行时主要用来显示 修改几何结构不方便 Dynamic Mesh 运行时生成的模型数据 可以随时改点、线、面 适合程序化几何和破碎Dynamic Mesh 的基本组成一个网格最基础的是Vertex顶点 Triangle三角形 Edge边 Attributes属性比如一块矩形玻璃v3 -------- v2 | / | | / | | / | | / | v0 -------- v1它有 4 个顶点v0 (-100, -50, 0) v1 ( 100, -50, 0) v2 ( 100, 50, 0) v3 (-100, 50, 0)但 GPU 通常画三角形所以矩形会拆成两个三角形t0 v0, v1, v2 t1 v0, v2, v3在FDynamicMesh3里大概就是UE::Geometry::FDynamicMesh3 Mesh; int V0 Mesh.AppendVertex(FVector3d(-100, -50, 0)); int V1 Mesh.AppendVertex(FVector3d( 100, -50, 0)); int V2 Mesh.AppendVertex(FVector3d( 100, 50, 0)); int V3 Mesh.AppendVertex(FVector3d(-100, 50, 0)); Mesh.AppendTriangle(V0, V1, V2); Mesh.AppendTriangle(V0, V2, V3);到这一步Mesh只是数据还没有出现在场景里。要显示它需要UDynamicMeshComponent* Comp; Comp-SetMesh(MoveTemp(Mesh));这样UDynamicMeshComponent才会把它送到渲染线程让你在场景里看到。为什么叫 Dynamic因为它可以动态改。比如我想让玻璃裂开我可以不是换模型而是运行时重新生成碎片 mesh。假设原来是一整块------------------- | | | | | | -------------------被子弹打中后我生成很多 polygon------------------ |\ | /|\ /| | \ | / | \ / | | \ | / | \ / | |--- * ---|---\ /---| | / | \ | / \ | | / | \ | / \ | |/ | \| / \ | ------------------每一个碎片 polygon 再单独变成一个FDynamicMesh3Shard 0 - DynamicMeshComponent 0 Shard 1 - DynamicMeshComponent 1 Shard 2 - DynamicMeshComponent 2 ...然后每个 Component 都可以SetSimulatePhysics(true); SetCollisionEnabled(QueryAndPhysics); SetPhysicsLinearVelocity(...);这样每块碎片就变成独立物理物体。DynamicMesh 不是 Actor这个点很重要。FDynamicMesh3不是 Actor也不是 Component。它只是数据结构。它自己不会显示不会碰撞不会 Tick不会物理模拟。关系是AActor └── UDynamicMeshComponent └── FDynamicMesh3AActor是场景对象。UDynamicMeshComponent是挂在 Actor 上的组件负责显示、材质、碰撞、物理。FDynamicMesh3是组件内部拿来显示的几何数据。所以你的玻璃 Actor 里会有IntactGlass CreateDefaultSubobjectUDynamicMeshComponent(TEXT(IntactGlass));这代表完整玻璃的显示组件。破碎时会UDynamicMeshComponent* Comp NewObjectUDynamicMeshComponent(this); Comp-RegisterComponent(); Comp-SetMesh(MoveTemp(Mesh));这代表运行时生成一个碎片组件。DynamicMesh 怎么从 polygon 变成立体碎片你这个插件里不是只画一张薄片而是要有厚度。比如一个 2D 碎片轮廓p0 ----- p1 \ | \ | p2 ---p3如果只是 2D它没有厚度只是一张面。要变成玻璃碎片需要前后两个面正面 z Thickness / 2 背面 z -Thickness / 2比如每个 2D 点会生成两个 3D 顶点p0_front (p0.x, p0.y, t/2) p0_back (p0.x, p0.y, -t/2) p1_front (p1.x, p1.y, t/2) p1_back (p1.x, p1.y, -t/2)然后要生成三类三角形1. 正面三角形 2. 背面三角形 3. 侧边三角形例如正面可以从中心扇形三角化front: center, p0, p1 center, p1, p2 center, p2, p3背面也来一套但顺序反过来保证法线朝外。侧边就是连接前后点p0_front ---- p1_front | | p0_back ---- p1_back拆成两个三角形p0_front, p1_front, p1_back p0_front, p1_back, p0_back这样这个碎片就有厚度了。为什么三角形顺序重要三角形不是只记录“三个点”还记录顺序。Mesh.AppendTriangle(A, B, C);和Mesh.AppendTriangle(A, C, B);是不一样的。顺序决定法线方向。一般用右手法则逆时针正面朝向你 顺时针背面朝向你如果顺序错了会出现看不见 背面剔除 法线反了 光照奇怪 碰撞奇怪所以玻璃碎片生成时正面和背面的三角形顺序要相反。DynamicMesh 的 Attribute 是什么只存顶点和三角形模型可以显示但会很粗糙。还需要属性Normal 法线 UV 贴图坐标 Material ID 材质槽 Vertex Color 顶点色法线决定光照。UV 决定材质怎么贴。Material ID 决定某个三角形用哪个材质槽。比如玻璃碎片可以有正面/背面玻璃材质 侧边破碎边缘材质这时候就可以给不同三角形设置不同 Material ID。如果不设置 UV材质可能还能显示颜色但贴图、法线贴图、裂纹图就不一定正确。DynamicMesh 和碰撞FDynamicMesh3本身只是渲染网格不天然等于物理碰撞。要让碎片能掉到地上需要给UDynamicMeshComponent设置碰撞。常见方式有Comp-SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); Comp-SetSimulatePhysics(true);但还不够。物理需要知道碰撞形状。你这个插件里用了类似Comp-SetSimpleCollisionShapes(RuntimeGlass::BuildConvexCollision(...), true);意思是根据碎片轮廓生成一个简单凸包碰撞。为什么不用复杂三角网格碰撞因为小碎片很多如果每片都用复杂三角形碰撞性能很差而且动态物理也不一定支持得好。所以通常做法是显示网格细一点可以真实 物理碰撞简化凸包这也是游戏里常见的做法。DynamicMesh 在玻璃破碎里的完整流程你的玻璃插件大概是这个逻辑1. 生成完整玻璃矩形 2. 子弹命中拿到 HitPoint 3. 把 HitPoint 转到玻璃本地坐标 4. 从命中点向外发射很多射线 5. 生成一圈一圈的碎片 polygon 6. 每个 polygon 裁剪到玻璃矩形边界内 7. 每个 polygon 变成带厚度的 DynamicMesh 8. 每个 DynamicMesh 放进一个 UDynamicMeshComponent 9. 给每个碎片设置材质和碰撞 10. 开启物理 11. 给速度 12. 碎片掉落核心图是完整玻璃 local 2D 平面 ----------------------- | / | \ | | / | \ | | / | \ | |----/-------*-------\--| | / | \ | | / | | ----------------------- * HitPoint 每个小区域都是一个 2D polygon 每个 polygon - 立体 DynamicMesh 碎片为什么用 DynamicMesh 做玻璃破碎因为它适合运行时生成不同形状。玻璃每次命中点不同打左上角碎片分布不同 打中间碎片分布不同 打边缘碎片分布不同如果用 Static Mesh就需要提前准备很多破碎模型。用 DynamicMesh 可以根据命中点实时生成碎片 每次破碎形状不同 可以调 RayCount / RingCount / Jitter 可以控制碎片数量和大小这就是它适合你的插件的原因。但是 DynamicMesh 不等于好看的破碎这个也必须说清楚。DynamicMesh只解决我能不能运行时生成碎片几何它不自动解决碎片怎么飞 裂纹怎么扩散 玻璃材质怎么表现 碰撞是否稳定 过程是否有观赏性这些是另外的系统破碎图案生成算法 动画阶段 物理参数 材质裂纹 碰撞形状 相机距离 帧率所以你现在一直不满意的“没有掉落过程”不是 DynamicMesh 本身的问题而是碎片生成后物理运动和视觉接管没有设计好目录结构YourProject/ Plugins/ RuntimeGlassDestruction/ RuntimeGlassDestruction.uplugin Source/ RuntimeGlassDestruction/ RuntimeGlassDestruction.Build.cs Public/ RuntimeGlassPaneActor.h Private/ RuntimeGlassDestructionModule.cpp RuntimeGlassPaneActor.cpp下面是完整代码。RuntimeGlassDestruction.uplugin{ FileVersion: 3, Version: 1, VersionName: 1.0, FriendlyName: Runtime Glass Destruction, Description: Runtime bullet-hit spiderweb glass fracture using Dynamic Mesh shards., Category: Runtime, EnabledByDefault: false, CanContainContent: false, Modules: [ { Name: RuntimeGlassDestruction, Type: Runtime, LoadingPhase: Default } ] }Source/RuntimeGlassDestruction/RuntimeGlassDestruction.Build.csusing UnrealBuildTool; public class RuntimeGlassDestruction : ModuleRules { public RuntimeGlassDestruction(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new[] { Core, CoreUObject, Engine, GeometryCore, GeometryFramework, PhysicsCore }); } }Private/RuntimeGlassDestructionModule.cpp#include Modules/ModuleManager.h class FRuntimeGlassDestructionModule : public IModuleInterface { }; IMPLEMENT_MODULE(FRuntimeGlassDestructionModule, RuntimeGlassDestruction)Public/RuntimeGlassPaneActor.h#pragma once #include CoreMinimal.h #include Engine/HitResult.h #include GameFramework/Actor.h #include RuntimeGlassPaneActor.generated.h class UDynamicMeshComponent; class USceneComponent; class UMaterialInterface; USTRUCT(BlueprintType) struct FRuntimeGlassShard2D { GENERATED_BODY() UPROPERTY(VisibleAnywhere, BlueprintReadOnly) TArrayFVector2D Points; }; UCLASS(Blueprintable) class RUNTIMEGLASSDESTRUCTION_API ARuntimeGlassPaneActor : public AActor { GENERATED_BODY() public: ARuntimeGlassPaneActor(); UPROPERTY(VisibleAnywhere, BlueprintReadOnly) TObjectPtrUSceneComponent SceneRoot; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) TObjectPtrUDynamicMeshComponent IntactGlass; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Shape) float Width 300.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Shape) float Height 200.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Shape) float Thickness 2.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Fracture) int32 RayCount 18; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Fracture) int32 RingCount 5; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Fracture) float AngleJitter 0.12f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Fracture) float RadiusJitter 0.12f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Fracture) float MinShardArea 10.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Physics) float ShotImpulse 1200.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Physics) float RadialImpulse 350.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Physics) float ShardMassKg 0.08f; UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryGlass|Rendering) TObjectPtrUMaterialInterface GlassMaterial; virtual void OnConstruction(const FTransform Transform) override; virtual void BeginPlay() override; UFUNCTION(BlueprintCallable, CategoryGlass) void RebuildGlass(); UFUNCTION(BlueprintCallable, CategoryGlass) void BreakAtWorldPoint(FVector WorldHitPoint, FVector ShotDirection, int32 Seed 12345); UFUNCTION(BlueprintCallable, CategoryGlass) void BreakFromHitResult(const FHitResult Hit, FVector ShotDirection, int32 Seed 12345); UFUNCTION(BlueprintCallable, CategoryGlass) void ResetGlass(); private: UPROPERTY(Transient) bool bBroken false; UPROPERTY(Transient) TArrayTObjectPtrUDynamicMeshComponent Shards; void GenerateShards(FVector2D LocalHitPoint, int32 Seed, TArrayFRuntimeGlassShard2D OutShards) const; void SpawnShard(const FRuntimeGlassShard2D Shard, FVector WorldHitPoint, FVector ShotDirection); void ClearShards(); float GetOuterRadius() const; static bool ClipToPane(const TArrayFVector2D InPoly, float W, float H, TArrayFVector2D OutPoly); static double SignedArea(const TArrayFVector2D Poly); static FVector2D Centroid(const TArrayFVector2D Poly); };Private/RuntimeGlassPaneActor.cpp#include RuntimeGlassPaneActor.h #include Algo/Reverse.h #include Components/DynamicMeshComponent.h #include Components/SceneComponent.h #include DynamicMesh/DynamicMesh3.h #include PhysicsEngine/AggregateGeom.h namespace RuntimeGlass { enum class EClipSide : uint8 { Left, Right, Bottom, Top }; static bool Inside(FVector2D P, EClipSide Side, FVector2D Min, FVector2D Max) { switch (Side) { case EClipSide::Left: return P.X Min.X; case EClipSide::Right: return P.X Max.X; case EClipSide::Bottom: return P.Y Min.Y; case EClipSide::Top: return P.Y Max.Y; } return true; } static FVector2D Intersect(FVector2D A, FVector2D B, EClipSide Side, FVector2D Min, FVector2D Max) { const FVector2D D B - A; if (Side EClipSide::Left || Side EClipSide::Right) { const double X Side EClipSide::Left ? Min.X : Max.X; const double T FMath::IsNearlyZero(D.X) ? 0.0 : (X - A.X) / D.X; return A D * T; } const double Y Side EClipSide::Bottom ? Min.Y : Max.Y; const double T FMath::IsNearlyZero(D.Y) ? 0.0 : (Y - A.Y) / D.Y; return A D * T; } static void ClipSide(const TArrayFVector2D Input, EClipSide Side, FVector2D Min, FVector2D Max, TArrayFVector2D Output) { Output.Reset(); if (Input.Num() 0) return; FVector2D Prev Input.Last(); bool bPrevInside Inside(Prev, Side, Min, Max); for (FVector2D Curr : Input) { const bool bCurrInside Inside(Curr, Side, Min, Max); if (bCurrInside ! bPrevInside) { Output.Add(Intersect(Prev, Curr, Side, Min, Max)); } if (bCurrInside) { Output.Add(Curr); } Prev Curr; bPrevInside bCurrInside; } } static void RemoveDuplicates(TArrayFVector2D Poly) { constexpr double TolSq 0.01; for (int32 i Poly.Num() - 1; i 0; --i) { if ((Poly[i] - Poly[i - 1]).SizeSquared() TolSq) { Poly.RemoveAt(i); } } if (Poly.Num() 1 (Poly[0] - Poly.Last()).SizeSquared() TolSq) { Poly.RemoveAt(Poly.Num() - 1); } } static void BuildMesh(const TArrayFVector2D Poly, float Thickness, UE::Geometry::FDynamicMesh3 Mesh) { Mesh.Clear(); Mesh.EnableTriangleGroups(); const int32 N Poly.Num(); if (N 3) return; const double HalfT FMath::Max(Thickness, 0.1f) * 0.5; TArrayint32 Front; TArrayint32 Back; Front.Reserve(N); Back.Reserve(N); for (FVector2D P : Poly) { Front.Add(Mesh.AppendVertex(FVector3d(P.X, P.Y, HalfT))); Back.Add(Mesh.AppendVertex(FVector3d(P.X, P.Y, -HalfT))); } for (int32 i 1; i N - 1; i) { Mesh.AppendTriangle(Front[0], Front[i], Front[i 1], 0); Mesh.AppendTriangle(Back[0], Back[i 1], Back[i], 1); } for (int32 i 0; i N; i) { const int32 j (i 1) % N; Mesh.AppendTriangle(Front[i], Back[i], Back[j], 2); Mesh.AppendTriangle(Front[i], Back[j], Front[j], 2); } } static FKAggregateGeom BuildConvexCollision(const TArrayFVector2D Poly, float Thickness) { FKAggregateGeom Agg; FKConvexElem Convex Agg.ConvexElems.AddDefaulted_GetRef(); const float HalfT FMath::Max(Thickness, 0.1f) * 0.5f; for (FVector2D P : Poly) { Convex.VertexData.Add(FVector(P.X, P.Y, HalfT)); Convex.VertexData.Add(FVector(P.X, P.Y, -HalfT)); } Convex.UpdateElemBox(); return Agg; } } ARuntimeGlassPaneActor::ARuntimeGlassPaneActor() { PrimaryActorTick.bCanEverTick false; SceneRoot CreateDefaultSubobjectUSceneComponent(TEXT(SceneRoot)); SetRootComponent(SceneRoot); IntactGlass CreateDefaultSubobjectUDynamicMeshComponent(TEXT(IntactGlass)); IntactGlass-SetupAttachment(SceneRoot); IntactGlass-SetMobility(EComponentMobility::Movable); IntactGlass-SetCollisionProfileName(TEXT(BlockAll)); IntactGlass-bEnableComplexCollision false; } void ARuntimeGlassPaneActor::OnConstruction(const FTransform Transform) { Super::OnConstruction(Transform); if (!bBroken) RebuildGlass(); } void ARuntimeGlassPaneActor::BeginPlay() { Super::BeginPlay(); if (!bBroken) RebuildGlass(); } void ARuntimeGlassPaneActor::RebuildGlass() { TArrayFVector2D Rect; Rect.Add(FVector2D(-Width * 0.5f, -Height * 0.5f)); Rect.Add(FVector2D( Width * 0.5f, -Height * 0.5f)); Rect.Add(FVector2D( Width * 0.5f, Height * 0.5f)); Rect.Add(FVector2D(-Width * 0.5f, Height * 0.5f)); UE::Geometry::FDynamicMesh3 Mesh; RuntimeGlass::BuildMesh(Rect, Thickness, Mesh); IntactGlass-SetMesh(MoveTemp(Mesh)); IntactGlass-SetSimpleCollisionShapes(RuntimeGlass::BuildConvexCollision(Rect, Thickness), true); IntactGlass-SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); IntactGlass-SetVisibility(true, true); IntactGlass-SetSimulatePhysics(false); if (GlassMaterial) { IntactGlass-SetMaterial(0, GlassMaterial); } } void ARuntimeGlassPaneActor::BreakFromHitResult(const FHitResult Hit, FVector ShotDirection, int32 Seed) { BreakAtWorldPoint(Hit.ImpactPoint, ShotDirection, Seed); } void ARuntimeGlassPaneActor::BreakAtWorldPoint(FVector WorldHitPoint, FVector ShotDirection, int32 Seed) { if (bBroken) return; bBroken true; IntactGlass-SetVisibility(false, true); IntactGlass-SetCollisionEnabled(ECollisionEnabled::NoCollision); const FVector LocalHit3 GetActorTransform().InverseTransformPosition(WorldHitPoint); FVector2D LocalHit(LocalHit3.X, LocalHit3.Y); LocalHit.X FMath::Clamp(LocalHit.X, -Width * 0.5f 0.1f, Width * 0.5f - 0.1f); LocalHit.Y FMath::Clamp(LocalHit.Y, -Height * 0.5f 0.1f, Height * 0.5f - 0.1f); TArrayFRuntimeGlassShard2D GeneratedShards; GenerateShards(LocalHit, Seed, GeneratedShards); FVector SafeShotDir ShotDirection.GetSafeNormal(); if (SafeShotDir.IsNearlyZero()) { SafeShotDir GetActorForwardVector(); } for (const FRuntimeGlassShard2D Shard : GeneratedShards) { SpawnShard(Shard, WorldHitPoint, SafeShotDir); } } void ARuntimeGlassPaneActor::ResetGlass() { ClearShards(); bBroken false; RebuildGlass(); } void ARuntimeGlassPaneActor::GenerateShards(FVector2D LocalHitPoint, int32 Seed, TArrayFRuntimeGlassShard2D OutShards) const { OutShards.Reset(); const int32 Rays FMath::Clamp(RayCount, 3, 96); const int32 Rings FMath::Clamp(RingCount, 1, 32); const float OuterRadius GetOuterRadius(); const float Step UE_TWO_PI / Rays; const float SafeAngleJitter FMath::Min(AngleJitter, Step * 0.3f); const float SafeRadiusJitter FMath::Clamp(RadiusJitter, 0.0f, 0.75f); FRandomStream Rand(Seed); TArrayfloat Angles; for (int32 i 0; i Rays; i) { Angles.Add(i * Step Rand.FRandRange(-SafeAngleJitter, SafeAngleJitter)); } TArrayTArrayFVector2D Points; Points.SetNum(Rays); for (int32 i 0; i Rays; i) { Points[i].SetNum(Rings 1); Points[i][0] LocalHitPoint; float PrevRadius 0.0f; for (int32 r 1; r Rings; r) { const float T float(r) / float(Rings); float Radius OuterRadius * FMath::Pow(T, 1.45f); if (r Rings) { Radius * Rand.FRandRange(1.0f - SafeRadiusJitter, 1.0f SafeRadiusJitter); } else { Radius OuterRadius; } Radius FMath::Max(Radius, PrevRadius 2.0f); PrevRadius Radius; const float Angle Angles[i] Rand.FRandRange(-SafeAngleJitter, SafeAngleJitter) * T; Points[i][r] LocalHitPoint FVector2D(FMath::Cos(Angle), FMath::Sin(Angle)) * Radius; } } auto AddShard [this, OutShards](const TArrayFVector2D Source) { TArrayFVector2D Clipped; if (!ClipToPane(Source, Width, Height, Clipped)) return; const double Area SignedArea(Clipped); if (FMath::Abs(Area) MinShardArea) return; if (Area 0.0) { Algo::Reverse(Clipped); } FRuntimeGlassShard2D NewShard OutShards.AddDefaulted_GetRef(); NewShard.Points MoveTemp(Clipped); }; for (int32 i 0; i Rays; i) { const int32 Next (i 1) % Rays; TArrayFVector2D Center; Center.Add(LocalHitPoint); Center.Add(Points[i][1]); Center.Add(Points[Next][1]); AddShard(Center); for (int32 r 1; r Rings; r) { TArrayFVector2D Cell; Cell.Add(Points[i][r]); Cell.Add(Points[i][r 1]); Cell.Add(Points[Next][r 1]); Cell.Add(Points[Next][r]); AddShard(Cell); } } } void ARuntimeGlassPaneActor::SpawnShard(const FRuntimeGlassShard2D Shard, FVector WorldHitPoint, FVector ShotDirection) { if (Shard.Points.Num() 3) return; UDynamicMeshComponent* Comp NewObjectUDynamicMeshComponent(this); Comp-SetMobility(EComponentMobility::Movable); Comp-AttachToComponent(SceneRoot, FAttachmentTransformRules::SnapToTargetNotIncludingScale); AddInstanceComponent(Comp); Comp-RegisterComponent(); UE::Geometry::FDynamicMesh3 Mesh; RuntimeGlass::BuildMesh(Shard.Points, Thickness, Mesh); Comp-SetMesh(MoveTemp(Mesh)); Comp-SetSimpleCollisionShapes(RuntimeGlass::BuildConvexCollision(Shard.Points, Thickness), true); Comp-SetCollisionProfileName(TEXT(PhysicsActor)); Comp-SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); Comp-bEnableComplexCollision false; if (GlassMaterial) { Comp-SetMaterial(0, GlassMaterial); } Comp-SetMassOverrideInKg(NAME_None, FMath::Max(0.001f, ShardMassKg), true); Comp-SetSimulatePhysics(true); const FVector2D C2 Centroid(Shard.Points); const FVector WorldCenter GetActorTransform().TransformPosition(FVector(C2.X, C2.Y, 0.0)); const FVector RadialDir (WorldCenter - WorldHitPoint).GetSafeNormal(); const FVector Impulse ShotDirection.GetSafeNormal() * ShotImpulse RadialDir * RadialImpulse; Comp-WakeAllRigidBodies(); Comp-AddImpulseAtLocation(Impulse, FMath::Lerp(WorldHitPoint, WorldCenter, 0.5f)); Shards.Add(Comp); } void ARuntimeGlassPaneActor::ClearShards() { for (UDynamicMeshComponent* Comp : Shards) { if (Comp) { Comp-DestroyComponent(); } } Shards.Reset(); } float ARuntimeGlassPaneActor::GetOuterRadius() const { return FVector2D(Width, Height).Size() * 1.45f; } bool ARuntimeGlassPaneActor::ClipToPane(const TArrayFVector2D InPoly, float W, float H, TArrayFVector2D OutPoly) { if (InPoly.Num() 3) return false; const FVector2D Min(-W * 0.5f, -H * 0.5f); const FVector2D Max( W * 0.5f, H * 0.5f); TArrayFVector2D A InPoly; TArrayFVector2D B; RuntimeGlass::ClipSide(A, RuntimeGlass::EClipSide::Left, Min, Max, B); RuntimeGlass::ClipSide(B, RuntimeGlass::EClipSide::Right, Min, Max, A); RuntimeGlass::ClipSide(A, RuntimeGlass::EClipSide::Bottom, Min, Max, B); RuntimeGlass::ClipSide(B, RuntimeGlass::EClipSide::Top, Min, Max, OutPoly); RuntimeGlass::RemoveDuplicates(OutPoly); return OutPoly.Num() 3; } double ARuntimeGlassPaneActor::SignedArea(const TArrayFVector2D Poly) { double A 0.0; for (int32 i 0; i Poly.Num(); i) { const FVector2D P Poly[i]; const FVector2D Q Poly[(i 1) % Poly.Num()]; A P.X * Q.Y - Q.X * P.Y; } return A * 0.5; } FVector2D ARuntimeGlassPaneActor::Centroid(const TArrayFVector2D Poly) { const double A SignedArea(Poly); if (FMath::IsNearlyZero(A)) { FVector2D Sum FVector2D::ZeroVector; for (FVector2D P : Poly) Sum P; return Sum / double(Poly.Num()); } FVector2D C FVector2D::ZeroVector; for (int32 i 0; i Poly.Num(); i) { const FVector2D P Poly[i]; const FVector2D Q Poly[(i 1) % Poly.Num()]; const double Cross P.X * Q.Y - Q.X * P.Y; C (P Q) * Cross; } return C / (6.0 * A); }用法把ARuntimeGlassPaneActor拖进关卡子弹 LineTrace 命中后调用if (ARuntimeGlassPaneActor* Glass CastARuntimeGlassPaneActor(Hit.GetActor())) { Glass-BreakFromHitResult(Hit, BulletDirection, RandomSeed); }1. 核心思路玻璃破碎不是播放预制动画而是在运行时命中点 - 生成 2D 碎片轮廓 - 每个碎片生成 DynamicMesh - 每个碎片注册成 DynamicMeshComponent - 给每片碎片设置材质、碰撞、物理 - 播放裂缝 / 飞出 / 掉落过程也就是说一整块玻璃 DynamicMeshComponent 破碎后变成多个碎片 DynamicMeshComponent每个碎片都是独立对象可以单独移动、旋转、碰撞、物理掉落。2. DynamicMesh 是什么FDynamicMesh3是运行时可编辑的网格数据。它主要保存顶点 Vertex 三角形 Triangle 三角形组 Group UV / Normal / MaterialID 等属性FDynamicMesh3本身只是数据不会显示。真正显示的是UDynamicMeshComponent关系是Actor - DynamicMeshComponent - FDynamicMesh33. 完整玻璃如何生成完整玻璃先在本地 XY 平面生成一个矩形(-W/2, -H/2) ( W/2, -H/2) ( W/2, H/2) (-W/2, H/2)然后通过BuildMesh生成有厚度的 3D 网格正面 z Thickness / 2 背面 z -Thickness / 2 侧面连接正背面所以玻璃并不是一张无限薄的面而是有厚度的立体碎片。4. BuildMesh 做了什么BuildMesh(Poly, Thickness, Mesh)把一个 2D 多边形变成 3D 有厚度 mesh。它做三件事1. 给每个 2D 点生成 Front 顶点 2. 给每个 2D 点生成 Back 顶点 3. 用三角形生成正面、背面、侧边例如Front.Add(Mesh.AppendVertex(FVector3d(P.X, P.Y, HalfT))); Back.Add(Mesh.AppendVertex(FVector3d(P.X, P.Y, -HalfT)));正面用扇形三角化Front[0], Front[1], Front[2] Front[0], Front[2], Front[3] ...背面顺序反过来保证法线方向正确Back[0], Back[2], Back[1]侧边每条边生成两个三角形Mesh.AppendTriangle(Front[i], Back[i], Back[j], 2); Mesh.AppendTriangle(Front[i], Back[j], Front[j], 2);意思是Fi ---- Fj | | Bi ---- Bj用两个三角形拼成一个侧面矩形。后面的数字0 正面 group 1 背面 group 2 侧边 group5. 命中点如何处理蓝图传入HitResult ShotDirection代码把世界命中点转到玻璃本地空间LocalHit ActorTransform.InverseTransformPosition(WorldHitPoint)然后只取 XYFVector2D LocalHit(LocalHit3.X, LocalHit3.Y);因为玻璃是在本地 XY 平面上破碎。命中点会被 Clamp 到玻璃内部X: -Width/2 0.1 到 Width/2 - 0.1 Y: -Height/2 0.1 到 Height/2 - 0.10.1是为了避免命中点刚好在边界上产生退化碎片。6. 如何生成 2D 碎片从命中点向外发射Rays条射线\ | / \ | / -------- * -------- / | \ / | \每条射线上生成Rings圈点。参数含义Rays裂缝方向数量 Rings从中心到外圈分几层 AngleJitter角度随机扰动 RadiusJitter半径随机扰动 Seed随机种子保证可复现每条射线的角度Angle i * Step RandomJitter每个点位置Point LocalHitPoint Direction * RadiusRadius OuterRadius * Pow(T, 1.45)让中心附近碎片更密 外圈碎片更大7. 如何拼出碎片 polygon相邻射线 相邻圈组成碎片。中心区域LocalHitPoint Points[i][1] Points[next][1]形成三角碎片。外圈区域Points[i][r] Points[i][r1] Points[next][r1] Points[next][r]形成四边形碎片。这些碎片还只是FRuntimeGlassShard2D也就是 2D 点数组不是 3D mesh。8. ClipToPane 做什么外圈点可能超出玻璃矩形所以要裁剪。ClipToPane依次裁左边界 右边界 下边界 上边界它不是简单删除外面的点而是删除玻璃外面的部分 在边界交叉处补新交点 生成新的有效 polygon裁剪完后RemoveDuplicates SignedArea去掉重复点过滤面积太小的碎片并统一顶点顺序。9. Shard 是什么Shard指一片玻璃碎片。但分两个阶段FRuntimeGlassShard2D 只是 2D 碎片轮廓 只有 Points UDynamicMeshComponent 真正显示出来的 3D 碎片 有 mesh / 材质 / 碰撞 / 物理前半段代码生成的是 2D Shard。后面SpawnShard把 2D Shard 变成真正的 3D 碎片。10. SpawnShard 做什么每个碎片都会1. 计算碎片中心 Centroid 2. 把点转换成相对自身中心的坐标 3. 创建一个 UDynamicMeshComponent 4. 把组件放到碎片中心位置 5. 根据点生成 DynamicMesh 6. 设置材质 7. 设置碰撞 8. 设置质量、重力、阻尼 9. 记录动画参数关键点CenteredPoints.Add(Point - C2); Comp-SetRelativeLocation(FVector(C2.X, C2.Y, 0));这样Component 原点 碎片自身中心 Mesh 顶点 相对碎片中心物理模拟才会围绕碎片自身中心而不是整块玻璃中心。

相关新闻

Simulink燃料电池建模:从单片电压到半经验模型实战

Simulink燃料电池建模:从单片电压到半经验模型实战

1. 燃料电池Simulink建模实战:从单片电压到半经验模型燃料电池系统建模一直是新能源领域的热点研究方向。作为一名长期从事燃料电池控制系统开发的工程师,我发现在Simulink环境下建立精确的电池模型对系统性能分析和控制器设计至关重要。这次我将分享两种…

2026/8/10 12:04:21 阅读更多 →
终极指南:3分钟解决DistroAV NDI Runtime缺失问题

终极指南:3分钟解决DistroAV NDI Runtime缺失问题

终极指南:3分钟解决DistroAV NDI Runtime缺失问题 【免费下载链接】obs-ndi DistroAV (formerly OBS-NDI): NDI integration for OBS Studio 项目地址: https://gitcode.com/gh_mirrors/ob/obs-ndi DistroAV(原OBS-NDI)是OBS Studio中…

2026/8/10 12:04:21 阅读更多 →
Win11Debloat终极指南:5分钟彻底告别Windows臃肿,让电脑重获新生

Win11Debloat终极指南:5分钟彻底告别Windows臃肿,让电脑重获新生

Win11Debloat终极指南:5分钟彻底告别Windows臃肿,让电脑重获新生 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other change…

2026/8/10 12:04:21 阅读更多 →

最新新闻

在信号调理中加入Teager-Kaiser能量算子(TKEO)提高了流行的肌电图(EMG)发病检测方法的准确性研究(Matlab代码实现)

在信号调理中加入Teager-Kaiser能量算子(TKEO)提高了流行的肌电图(EMG)发病检测方法的准确性研究(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 &#x1f381…

2026/8/11 0:42:21 阅读更多 →
ChatGPT、Codex实战:MCP接上以后为什么还是不好用?从工具调用、权限到上下文边界的7项排查

ChatGPT、Codex实战:MCP接上以后为什么还是不好用?从工具调用、权限到上下文边界的7项排查

很多人第一次给Codex接MCP时,都会有一个很自然的预期:MCP连接成功以后,Codex是不是马上就能更聪明地使用浏览器、文档、数据库或者外部开发工具?真正用起来却经常不是这样。你可能已经看到MCP Server正常连接:Connecte…

2026/8/11 0:41:20 阅读更多 →
2026国自然博士生基金项目申报攻略

2026国自然博士生基金项目申报攻略

一、全流程关键时间节点1.校内遴选与函评阶段8月初:完成申报书初版,启动持续优化迭代8月底:学院层级组织内部评审9月初:学校层级组织校内评审9月底:集中优化申报书,完成格式全面核查10月初:最终…

2026/8/11 0:40:20 阅读更多 →
2026国自然放榜倒计时!看懂中标底层逻辑,告别无效申报!

2026国自然放榜倒计时!看懂中标底层逻辑,告别无效申报!

2026年8月中下旬国自然即将迎来正式放榜公示,每年这个阶段,都是科研人集中复盘申报差距的关键节点。不少申请人都会产生同款疑惑:自己的研究方向、前期基础、论文成果和中标同行相差无几,甚至实验积累更扎实,最终却遗憾…

2026/8/11 0:40:20 阅读更多 →
基于plc与MCGS的多级传送带控制系1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于plc与MCGS的多级传送带控制系1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于plc与MCGS的多级传送带控制系1(设计源文件万字报告讲解)(支持资料、图片参考_相关定制)_ 30.基于plc与MCGS的多级传送带控制系统,包括博途plc梯形图程序,MCGSPRO组态仿真画面,电气原理图,plc接线图&…

2026/8/11 0:40:20 阅读更多 →
基于PLC 博图 1200 银行 排队 叫号 控制系统设计1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于PLC 博图 1200 银行 排队 叫号 控制系统设计1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_

基于PLC 博图 1200 银行 排队 叫号 控制系统设计1(设计源文件万字报告讲解)(支持资料、图片参考_相关定制)_ 带配套报告1W字 配套的 流程图 接线图 博图V15 程序仿真完美运行

2026/8/11 0:39:20 阅读更多 →

日新闻

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南 【免费下载链接】video2x A machine learning-based video super resolution and frame interpolation framework. Est. Hack the Valley II, 2018. 项目地址: https://gitcode.com/GitHub_Trending/vi/v…

2026/8/11 0:00:02 阅读更多 →
前后端分离项目中控制台与接口工具数据差异排查指南

前后端分离项目中控制台与接口工具数据差异排查指南

1. 问题现象解析:控制台与Apifox的数据差异 最近在调试一个前后端分离项目时,遇到了一个典型问题:后端服务在本地开发环境控制台能正常输出查询数据,但通过Apifox测试时却返回空结果。这种"控制台有数据,接口工具…

2026/8/11 0:00:03 阅读更多 →
AI编程实战:从Claude Code踩坑到游戏开发入门

AI编程实战:从Claude Code踩坑到游戏开发入门

1. 从“AI能帮我做游戏”到“AI让我重新学编程”最近身边不少朋友,尤其是一些非技术背景、但对游戏开发有浓厚兴趣的朋友,都在问我同一个问题:“听说现在用Claude Code这种AI编程工具,小白也能做游戏了,是真的吗&#…

2026/8/11 0:00:03 阅读更多 →

周新闻

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁 【免费下载链接】baidupankey 在线查询网盘提取码(维护中 rm repo) 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 你是否曾经在深夜寻找一份重要资料&#x…

2026/8/10 1:05:29 阅读更多 →
如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/10 1:05:29 阅读更多 →
收藏!小白程序员轻松入门大模型,从Harness工程开始实践

收藏!小白程序员轻松入门大模型,从Harness工程开始实践

文章强调学习大模型不应只关注模型本身,而应重视模型外的系统搭建,即Harness。提出AgentModelHarness的实用公式,详细介绍Harness的四个层次:持久化层、执行层、控制层和观察与验证层。文章还探讨了上下文工程、工具设计、AGENTS.…

2026/8/10 1:05:29 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/10 17:07:33 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/10 1:05:29 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/10 17:07:33 阅读更多 →