外观
簇、应变与运行时物理
约 9749 字大约 33 分钟
2026-10-14
引擎版本:UE 5.7.3(
E:\Main\Engine,只读检索)。 引用格式:文件名:行号(文件名在引擎内基本唯一,完整相对路径见 §0.2)。 标注「(推断)」= 由代码阅读得出、未运行验证;其余均为源码直接可见的事实。
0. 速览
0.1 十句话版本
- 一个未破碎的 GC = 求解器里的 1 个刚体。
CreateClusterParticle把所有子粒子DisableParticles(PBDRigidClustering.cpp:253),子粒子不进宽相、不进约束图、不积分,只剩内存。 - 簇的数据全部放在
TPBDRigidClusteredParticles的 SOA 里:ClusterIds(父指针+子数)、ChildToParent、Strains(=内部阈值)、ExternalStrains、CollisionImpulses、ConnectivityEdges、RigidClusteredFlags(PBDRigidClusteredParticles.h:265-288)。 - 「Strain」其实有两个相反角色:
Strains/InternalStrain= 子块脱离父簇所需的阈值;CollisionImpulses/ExternalStrains= 本步施加的载荷。判定式只有一条:max(CollisionImpulse, ExternalStrain) >= InternalStrain(PBDRigidClustering.cpp:1178-1179)。 - 载荷从哪来:①碰撞接触的
AccumulatedImpulse(ComputeStrainFromCollision,:2226-2426);②Field/BPApplyExternalStrain(:2837-2858+FieldSystemProxyHelper.h:502-521);③伤害传播(:1257-1299)。无跨帧累积:每步重置/清零(:1247、:2431-2438)。 - 「释放(Release)」= 子粒子从父簇摘下并重新进入求解器(
RemoveChildFromParent,:910-958),位姿由ChildToParent × 父簇 P/Q还原,速度 =v_parent + ω_parent × r(:936-940)。 - 只评估「活跃簇的直接子节点」:嵌套更深的子节点还是 disabled,所以天然「先破大块、再破小块」;默认阈值也逐级递减
500000 / 50000 / 5000(GeometryCollectionObject.cpp:108,GeometryCollectionComponent.cpp:613)。 - 连接图 = 破碎后的拓扑裁判:释放后删边(
RemoveNodeConnections)→对剩余子节点做连通分量(DFS)→孤立块各自成 1 个 internal cluster,含锚点的岛保持 Kinematic、无锚点的岛变 Dynamic 并坠落(:1310-1319、:1352-1575、PBDRigidClusteringAlgo.cpp:147-228)。这就是「结构倒塌」的全部机制,没有应力求解。 - 连接图默认方法
MinimalSpanningSubsetDelaunayTriangulation(GeometryCollectionObject.cpp:112):Voronoi/Delaunay 邻接 + 并查集补边,且 500cm×√ClusterConnectionFactor 以内的 Delaunay 边一律保留(PBDRigidClustering.cpp:2909)。 - Cluster Union(5.4+) = 运行时动态把任意刚体/GC 并成 1 个刚体:1 个 child ↔ 1 个 shape,支持增量增删、跨 GC 的 intercluster edge(
ClusterUnionManager.h/.cpp)。ClusterGroupIndex>0是它的旧入口(PBDRigidClustering.cpp:315-328)。 - 没有单独的「cluster 迭代次数」:
AdvanceClustering每个子步在 Solve 之后只跑一遍(PBDRigidsEvolutionGBF.cpp:807);多层级联破碎是「每步一层/每步一跳」的跨帧级联。
0.2 文件索引(相对 E:\Main\Engine)
| 简称 | 相对路径 |
|---|---|
| PBDRigidClustering.h/.cpp | Source/Runtime/Experimental/Chaos/Public/Chaos/ / .../Private/Chaos/ |
| PBDRigidClusteringAlgo.h/.cpp | 同上(Public/Private) |
| PBDRigidClusteringTypes.h、PBDRigidClusteredParticles.h、ClusterCreationParameters.h、ClusterUnionManager.h、StrainModification.h、ParticleHandle.h、Defines.h | Source/Runtime/Experimental/Chaos/Public/Chaos/ |
| ClusterUnionManager.cpp、ClusterUnionStressSolver.cpp/.h、ConvexOptimizer.cpp、ImplicitObjectUnion.cpp、PBDRigidsEvolutionGBF.cpp | Source/Runtime/Experimental/Chaos/Private/Chaos/ |
| PBDRigidsSolver.cpp、ChaosSolverConfiguration.cpp | Source/Runtime/Experimental/Chaos/Private/ |
| ChaosSolverConfiguration.h | Source/Runtime/Experimental/Chaos/Public/ |
| GeometryCollectionSimulationCoreTypes.h / SimulationTypes.h / ConvexUtility.h / ProximityUtility.h | Source/Runtime/Experimental/Chaos/Public/GeometryCollection/ |
| GeometryCollectionSizeSpecificUtility.cpp / ConnectionGraphUtility.cpp / ConvexUtility.cpp / ProximityUtility.cpp / CollisionStructureManager.cpp | Source/Runtime/Experimental/Chaos/Private/GeometryCollection/ |
| GeometryCollectionPhysicsProxy.cpp / ClusterUnionPhysicsProxy.cpp | Source/Runtime/Experimental/Chaos/Private/PhysicsProxy/(头文件在 Public/PhysicsProxy/) |
| FieldSystemProxyHelper.h | Source/Runtime/Experimental/Chaos/Public/PhysicsProxy/ |
| GeometryCollectionObject.h/.cpp、GeometryCollectionComponent.h/.cpp、GeometryCollectionDamagePropagationData.h、GeometryCollectionEngineRemoval.h/.cpp | Source/Runtime/Experimental/GeometryCollectionEngine/{Public,Private}/GeometryCollection/ |
| ChaosSolverActor.h | Source/Runtime/Experimental/ChaosSolverEngine/Public/Chaos/ |
| PhysicalMaterial.h/.cpp | Source/Runtime/PhysicsCore/{Public/PhysicalMaterials,Private}/ |
1. 五个关键问题(直接回答)
Q1 层级破碎(Level)是怎么节约性能的?
| 机制 | 源码证据 |
|---|---|
| 子粒子 disabled:不在宽相/窄相/约束图/积分里,一个 10k 块的墙 = 1 个刚体 | CreateClusterParticle:MEvolution.DisableParticles(ChildrenSet) PBDRigidClustering.cpp:253 |
| 活跃簇的碰撞用离线算好的简化代理形状(默认凸包/胶囊等),不是 10k 个子形状 | BuildClusters_Internal 把 Implicits[簇] 作为 ProxyGeometry 传入(GeometryCollectionPhysicsProxy.cpp:2172-2180)→ UpdateGeometry→BuildScaledGeometry(PBDRigidClusteringAlgo.cpp:394-397, :237-264) |
子形状只用于「定位被击中的是哪几个子块」(ChildrenSpatial = FImplicitObjectUnionClustered + BVH),不参与碰撞 | PBDRigidClusteringAlgo.cpp:372-380;用法 PBDRigidClustering.cpp:2389-2403 |
| 评估只访问「本步有载荷的活跃簇」,而不是所有簇 | TopLevelClusterParentsStrained + p.chaos.clustering.breakonlystrained=1(:1652-1654、:1765-1817) |
| 破裂代价 ∝ 被释放块数 + 父簇子节点数,不 ∝ 资产总复杂度 | ReleaseClusterParticlesImpl 只遍历直接子节点 :1168-1248 |
MaxSimulatedLevel 之下的层级根本不创建粒子 | CalculateEffectiveParticles GeometryCollectionPhysicsProxy.cpp:3190-3221;注释 GeometryCollectionComponent.h:1012-1018 |
| 预算阀:每步最多破裂数 / 释放粒子限流 | PerAdvanceBreaksAllowed、ClusteringParticleReleaseThrottling*(见 §6.4) |
一句话:刚体数量随「被打到的深度」增长,而不是随资产复杂度增长。
Q2 为什么先破大簇、再破小簇?
- 机制上必然:释放判定只看「活跃簇的直接子节点」(
:1168-1170)。深层子节点仍 disabled,没有碰撞、收不到冲量,除非它们的父块先被释放成为新的活跃簇。 - 参数上刻意:阈值按层递减。
ComputeUserDefinedDamageThreshold_Internal取Level = 层级-1(GeometryCollectionPhysicsProxy.cpp:1950-1951),查表DamageThreshold[Level],超出数组用最后一个值(:1977-1979)。默认{500000, 50000, 5000}。 - 工程上合理:大块需要大载荷才能脱离,一次撞击只掰下少数大块(少量新刚体);这些大块作为整体刚体落地,落地冲击再按较小阈值把它碎开 —— 把「爆炸式刚体增长」摊成多帧、多次撞击。
- 上限:
Level >= min(MaxClusterLevel, p.gc.GlobalMaxSimulatedLevel, MaxSimulatedLevel)→ 阈值FLT_MAX(不可再破)(:1952-1955)。
Q3 应变(Strain)从哪里来?
三个入口、一条判定、一次清零(详见 §5):
入口A 碰撞 : ContactHandle->GetAccumulatedImpulse() → 各子块.CollisionImpulses += ... (ComputeStrainFromCollision)
入口B 外部 : Field_ExternalClusterStrain / ApplyExternalStrain → 子块.ExternalStrains = max(旧, 新)
入口C 传播 : 子块破裂/受震 → 邻居(连接图).ExternalStrains = max(旧, 传播值) (下一步才生效)
判定 : max(CollisionImpulses, ExternalStrains) >= Strains(InternalStrain) → 释放
清零 : 每个被评估的子块 ExternalStrains = 0(无论是否破);CollisionImpulses 在下一步开头整体 Fill(0)阈值侧 InternalStrain 可被 Field_InternalClusterStrain(加上场值,正负皆可)/ ApplyInternalStrain(减去,下限 0)持久修改(FieldSystemProxyHelper.h:568-583、GeometryCollectionPhysicsProxy.cpp:2860-2879);默认模型没有疲劳累积,需要累积请用 StrainModifier 回调(§5.8)。
Q4 什么叫 strain「释放」?
两层含义,别混:
- 粒子释放(release):子粒子
ClusterIds.Id=null、EnableParticle、加入TopLevelClusterParents、X/R/P/Q 由ChildToParent * 父簇(P,Q)还原、速度继承、父簇MChildren减员、发FBreakingData事件(:910-958、:1232-1241)。 - 载荷释放(consume):
Child->SetExternalStrains(0)对每个被评估子块都执行(:1247),碰撞冲量下一步整体清零(:2429-2438)。即应变是一次性载荷,不是「血量」。
Q5 连接图(ConnectivityEdges)剪断对稳定性(结构倒塌)有什么作用?
- 图的拓扑 = 支撑关系。释放若干子块后:
RemoveNodeConnections(被释放块)(:1314-1317)→FindIslandsInChildren(DFS 连通分量,:839-908)→- 孤岛(1 块):直接释放成独立刚体;
- 多块岛:
CreateClustersFromNewIslands建 internal cluster(:975-1047),其 ObjectState 由UpdateKinematicProperties重新计算:岛内任一子块 Anchored/Kinematic ⇒ 岛保持 Kinematic(挂住);无锚点 ⇒ Dynamic(整块坠落)(PBDRigidClusteringAlgo.cpp:147-228)。
- 因此「切断承重路径 → 上部整体失去锚点 → 作为刚体塌下来」是纯拓扑结果,零成本、稳定、可预测;不依赖求解器去解约束。
- 材料模型下,边上存的是接触面积:块失去邻居后会被重算更低的阈值(
HandleConnectivityOnRelease...:1411-1414+ComputeMaterialBasedDamageThreshold_Internal:1902-1944),所以「失去支撑的块更脆」。 - 5.7 里也有真应力求解
FClusterUnionStressSolver(拉/压/剪应力比,ClusterUnionStressSolver.cpp:127-203),但它在Private/,且Source/Runtime/Experimental/Chaos内无任何调用点(推断:实验性/未接线)。 - 局限:边不承载「受力」,一次只裁判拓扑;悬挑/细长结构的「受弯断裂」不会自发出现,除非载荷(冲击/Field/传播)真的把关键子块的阈值打穿。
2. 数据结构
2.1 粒子 SOA(PBDRigidClusteredParticles.h)
| 字段 | 类型/语义 | 行 |
|---|---|---|
ClusterIds | ClusterId{ Id=父粒子指针(null=顶层/非簇化), NumChildren } | :24-36, :265 |
ChildToParent | 子在父簇局部空间的变换(释放时用来还原世界位姿) | :266 |
ClusterGroupIndex | >0=待并入同号 union;<0=union 自身;0=不参与 | :267;语义见 PBDRigidClustering.h:288-295 |
ChildrenSpatial | FImplicitObjectUnionClusteredPtr:子形状并集(+BVH),用于「接触点 → 子块」查询 | :268 |
PhysicsProxies | TSet<IPhysicsProxyBase*>,内部簇/union 可含多个 proxy | :270-271 |
CollisionImpulses | 本步碰撞载荷(float) | :273-274 |
ExternalStrains | Field/BP/传播载荷;注释「应并入 CollisionImpulses」 | :276-278 |
Strains | 「用户设定参数」= InternalStrain 阈值;API 名 GetInternalStrains() | :280-281;ParticleHandle.h:1625-1636 |
ConnectivityEdges | TArray<TConnectivityEdge>:{Sibling, Strain},Strain 同时可存面积(SetArea/GetArea) | :283, :46-77 |
RigidClusteredFlags | bInternalCluster / bAnchored / bUnbreakable / bChildToParentLocked | :98-141 |
ConvexOptimizers | 可选:把整个层级的凸体简化成 kDOP18 tribox(p.Chaos.Convex.SimplifyUnion 默认 false) | :287-288;ConvexOptimizer.cpp:15-16 |
MaxStrain = FLT_MAX(ParticleHandle.h:1667),SetMaximumInternalStrain()(:1636)。TPBDGeometryCollectionParticleHandleImp继承自 Clustered handle(ParticleHandle.h:1670-1719),GC 叶子也是「clustered 粒子」,只是NumChildren=0。IsInterclusterEdge:两端Parent()不同的边(GC 与 union 内别的 GC 相连时出现):79-96。- internal cluster:
bInternalCluster=true,运行时为「破裂后剩余岛」创建;与 GC 原生簇不同,它没有离线代理形状。
2.2 FRigidClustering 成员(PBDRigidClustering.h:598-641)
| 成员 | 作用 |
|---|---|
MChildren : TMap<簇, TArray<子>> | 活跃/非活跃簇 → 直接子节点 |
TopLevelClusterParents | 当前无父的簇/粒子集合 |
TopLevelClusterParentsStrained : TMap<簇, int64时间戳> | 本步有新载荷的父簇;按时间戳升序处理(FIFO) :1762 |
ClusterUnionManager | union 管理 |
EmptyInternalClustersPerProxy | 已空的 internal cluster,proxy 销毁时才清理(:1900-1911;PBDRigidsSolver.cpp:1461) |
MomentumRestoringDataByParticle | 「撞击者动量回补」暂存 |
MClusterConnectionFactor(=1.0) / MClusterUnionConnectionType(=Delaunay) | 来自 FChaosSolverConfiguration(:196-205、:3129-3137) |
StrainModifiers | ISimCallbackObject 数组(§5.8) |
DestructionSettings | 每步破裂预算/限流(§6.4) |
实例由 Evolution 持有:PBDRigidsEvolutionGBF.h:145-146, 332;构造 PBDRigidsEvolutionGBF.cpp:1364。
2.3 资产侧(FGeometryCollection)与运行时的映射
- 节点类型
ESimulationTypes { FST_None=0, FST_Rigid=1, FST_Clustered=2 }(GeometryCollection.h:130-136);叶子=Rigid,内部节点=Clustered。 - 资产预烘焙属性:
Parent/Children、Level、Size、Volume、Mass、InertiaTensor、MassToLocal、TransformToConvexIndices+ConvexHull组、Proximity、连接图 facade(ConnectionEdgeStart/End/Contact)、InitialDynamicState、Anchoring facade、RemoveOnBreak(FVector4f打包)。 Size=cbrt(体积)/cbrt(最大叶子体积)(GeometryCollectionConvexUtility.cpp:2844-2929;p.gc.UseVolumeToComputeRelativeSize可改用体积),用于 SizeSpecificData 选档。
3. 簇的创建、质量/惯量合并、碰撞近似
3.1 运行时创建伪代码(CreateClusterParticle,PBDRigidClustering.cpp:211-331)
NewParticle = Parameters.ClusterParticleHandle ?? CreateClusteredParticles(1)
EnableParticle(NewParticle); CollisionGroup = INT_MAX; TopLevelClusterParents.Add
ClusterId = (null, NumChildren); ClusterGroupIndex; InternalStrain = 0; IsAnchored = Parameters.bIsAnchored
MChildren[New] = Children; DisableParticles(Children) // :253
for Child in Children:
bClusterIsAsleep &= Child.Sleeping; OneWay &= ...; MACD |= ...
if clustered: Child.ClusterId.Id = New; Child.ClusterGroupIndex = 0
New.InternalStrain += Child.InternalStrain // :273
New.CollisionImpulses = max(...); CollisionGroup = min(...)
New.InternalStrain /= N // :285 (平均)
UpdateClusterMassProperties(New, Children) // 质量/CoM/惯量合并
if ForceMassOrientation: SetX/SetR(它) else MoveClusterToMassOffset(Position)
UpdateKinematicProperties(New) // 锚点/静态传染
UpdateGeometry(New, Children, ProxyGeometry, Parameters) // 碰撞几何
GenerateConnectionGraph(New, Parameters) // 若 bGenerateConnectionGraph
if ClusterGroupIndex > 0: ClusterUnionManager.AddPendingExplicitIndexOperation(idx, AddReleased, {New})- GC 路径:
BuildClusters_Internal传bGenerateConnectionGraph=false、传&ParticleTM作为ForceMassOrientation、传Implicits[簇]作为ProxyGeometry(GeometryCollectionPhysicsProxy.cpp:2154-2180)。簇内部阈值 = 子阈值均值只是默认值,GC 之后会被UpdateDamageThreshold_Internal逐粒子覆盖(:1245-1298)。
3.2 质量/惯量合并
UpdateClusterMassProperties(PBDRigidClusteringAlgo.cpp:46-109):每个子在父空间下取{M, I, CoM, RoM}→Chaos::Combine(ChildMasses)(平行轴合并,结果对角化)→ 设M/InvM/I/InvI/CenterOfMass/RotationOfMass。MoveClusterToMassOffset(Position|Rotation)(:123-144):把 X/R 搬到质心/主轴系并清零 CoM/RoM。- GC 实际用的是离线预计算值:
InitializeSharedCollisionStructures自底向上做同样的合并并把偏移存成MassToLocal(GeometryCollectionPhysicsProxy.cpp:5839-5900);运行时PopulateSimulatedParticle以资产Mass/InertiaTensor覆盖粒子(:2204-2221, 419-507),CoM=0, RoM=Identity(:441-442)。 - 叶子质量:
DesiredDensity = TotalMass/TotalVolume,Mass_i = max(Density × Volume_i, MinimumMassClamp)(:5605-5650);资产默认bMassAsDensity=true, Mass=2500(kg/m³), MinimumMassClamp=0.1(GeometryCollectionObject.cpp:132-134);FSharedSimulationParameters夹紧范围 Mass[0.1, 1e5]、包围盒[0.1, 1e6](GeometryCollectionSimulationCoreTypes.h:93-99)。 - 运行时 internal cluster 每次创建/改动都重算(O(子数)):
CreateClusterParticleFromClusterChildren:654-655;AddParticlesToCluster/RemoveParticlesFromCluster增量维护平均内部应变:347-392, :448-512。
3.3 碰撞近似(谁用哪种形状)
| 对象 | 形状 | 备注 |
|---|---|---|
| GC 叶子 | 资产 SizeSpecificData[档].CollisionShapes[0]:ImplicitType ∈ {Box, Sphere, Capsule, LevelSet, Convex, None} | 默认 Convex + Chaos_Volumetric(Implicit-Implicit),CollisionObjectReduction=0,MaxCollisionParticles=60,CollisionParticlesFraction=1(GeometryCollectionObject.cpp:176-192);ECollisionTypeEnum/EImplicitTypeEnum 见 GeometryCollectionSimulationTypes.h:10-30 |
| GC 原生簇(活跃时) | 离线用 CreateImplicitGeometry 为每个簇生成;Convex 时用该簇 TransformToConvexIndices 的若干凸包(多个→FImplicitObjectUnion),LevelSet 时分辨率由「簇包围盒/最大子包围盒」缩放并夹在 Min/MaxClusterLevelSetResolution | GeometryCollectionPhysicsProxy.cpp:5149-5277, 5958-6008;凸包构建默认 ConvexCount=4、FractionAllowRemove=.3、CanExceedFraction=.5(GeometryCollectionConvexUtility.h:123, 126-135) |
Chaos_Volumetric + LevelSet | 被强制改成 Surface_Volumetric | GeometryCollectionObject.cpp:540-546 |
Surface_Volumetric(Particle-Implicit) | UpdateImplicitFlags:CollisionType 记为 LevelSet;若隐式体本身是 LevelSet 则 SetDoCollide(false)/SetConvex(false),只当 SDF,碰撞由碰撞粒子(simplicial)点采样对 SDF 完成(推断自 DisplayName「Particle-Implicit」) | GeometryCollectionCollisionStructureManager.cpp:174-187;组件仅在此类型下启用 simplicial(GeometryCollectionComponent.cpp:3985-3991) |
| simplicial 生成 | 叶:CleanCollisionParticles(TriMesh,…,Fraction) 按顶点重要度排序取前 Fraction;簇:(可选,p.CollisionParticlesUseImplicitCulling 默认 0,且仅当顶点数 > 上限时)用 SDF 剔除内部点 → 空间网格去重(SnapThreshold=Extent/p.CollisionParticlesSpatialDivision(10)) → 封顶 min(MaximumCollisionParticleCount=60, p.CollisionParticlesMax=2000) | GeometryCollectionCollisionStructureManager.cpp:24-34, 36-127, 129-172 |
| 运行时 internal cluster(破裂后剩余岛) | 无代理形状:UpdateGeometry(…, nullptr, …) → 单子=该子形状,多子=FImplicitObjectUnionClustered(子形状…);bCopyCollisionParticles = p.UnionsHaveCollisionParticles(默认0) ⇒ 无碰撞粒子 | PBDRigidClustering.cpp:546-550, 648-659;PBDRigidClusteringAlgo.cpp:448-460 |
| union(>10 形状) | 外层包 BVH:p.Chaos.Collision.UnionBVH.NumShapes=10、MaxDepth=14 | ImplicitObjectUnion.cpp:22-28 |
| 旧路径 | p.UseLevelsetCollision=0(默认关);p.MinLevelsetDimension=4/MaxLevelsetDimension=20/MinLevelsetSize=50 仅旧路径使用 | PBDRigidClusteringAlgo.cpp:18-31, 406-447 |
资产 LevelSet 默认分辨率 5/10/25/50(叶 min/max,簇 min/max)(GeometryCollectionSimulationCoreTypes.h:18-31;GeometryCollectionObject.cpp:168-174)。
坑/取舍
- 破裂后的 internal cluster 比「原簇」更贵:原簇是离线代理(几块凸包),岛簇是「逐子形状 + BVH」的 union,且无 simplicial。大规模破碎时窄相开销主要来自这些岛簇。
FindAllIntersectingChildren在无 BVH(<10 形状)时返回全部子节点(ImplicitObjectUnion.cpp:588-592;调用点注释PBDRigidClustering.cpp:2392)→ 小簇一次撞击 = 所有子块都吃满冲量。p.Chaos.Convex.SimplifyUnion默认 false,资产bOptimizeConvexes默认取该 CVar(GeometryCollectionObject.cpp:136)。
3.4 初始状态:Static / Kinematic / Dynamic / Sleeping 与锚点
EObjectStateTypeEnum { Sleeping=1, Kinematic=2, Static=3, Dynamic=4, UserDefined=100 }(GeometryCollectionSimulationTypes.h:33-43)。- 组件
ObjectType默认Dynamic(GeometryCollectionComponent.cpp:604)。填充DynamicState[]:非 UserDefined 时,Dynamic 且资产有InitialDynamicState就用资产值,其余全体统一赋 ObjectType(:3862-3884)。 - 簇的状态由子孙传染(
UpdateKinematicProperties,PBDRigidClusteringAlgo.cpp:147-228):GC 在if 簇 IsAnchored → Kinematic else BFS 子孙: 遇 IsAnchored 或 Kinematic → Kinematic; 遇 Static → Static; 否则 Dynamic if 原本 Sleeping 且结果 Dynamic → Sleeping 非 Kinematic 时清 KinematicTargetBuildClusters_Internal里检测InvM==0把DynamicState[簇]回写为 Static/Kinematic(GeometryCollectionPhysicsProxy.cpp:2188-2198)。 - Anchored 是「可被掰掉的锚点贡献者」(
PBDRigidClusteredParticles.h:112-114):叶子被释放后自己是 Dynamic(锚点只在挂在父簇上时使父簇变 Kinematic)。API:SetAnchoredByIndex/ByTransformedBox/RemoveAllAnchors(GeometryCollectionPhysicsProxy.cpp:2640-2768),均通过EnqueueCommandImmediate投递到 PT,并立即UpdateKinematicProperties(TopParent)。 - 坑(推断):
ObjectType=Static/Kinematic会把所有 transform 的DynamicState设成同一状态(:3877-3883),释放后的叶子依旧是该状态(UpdateKinematicProperties对叶子不做事)→ 需要Field_DynamicState或改用 Anchored 路径才能「破开后落下」。 - 起始睡眠:
ObjectType==Sleeping或(Dynamic 且!StartAwake)→SetSleepingState只把顶层活跃簇设为 Sleeping,睡眠不从叶子向上传播(:1733-1739、:1219-1232)。
4. 连接图(Connection Graph)
4.1 连接方法 EConnectionMethod 与实现
枚举定义:ClusterCreationParameters.h:15-22(ChaosSolverConfiguration.h:10-18 的 EClusterUnionMethod 同序镜像;ChaosSolverActor.h:33-43 的 EClusterConnectionTypeEnum 为旧版隐藏枚举,其中 DelaunayTriangulation 与 None 被 UMETA(Hidden))。
| 枚举 | 值 | 实现 | 复杂度/特点 |
|---|---|---|---|
PointImplicit | 0 | UpdateConnectivityGraphUsingPointImplicit(PBDRigidClustering.cpp:2719-2789):对每对 (i<j),把 j 的碰撞粒子变换到 i 空间,若 i.Geometry.SignedDistance(P − P·Δ) < 0 则连边;Δ=CoillisionThicknessPercent(默认 0.3,夹 [0,1]) | O(N²·P),PhysicsParallelFor;依赖碰撞粒子,没有则回退(:2186-2189) |
DelaunayTriangulation | 1 | 取所有子粒子 X 做 VoronoiNeighbors ⇒ 所有邻接对都连(UpdateConnectivityGraphUsingDelaunayTriangulationWithFiltering,:2981-3018);可被 p.UseBoundingBoxForConnectionGraphFiltering 加 AABB 重叠过滤 | 注释 // not thread safe(:2161) |
MinimalSpanningSubsetDelaunayTriangulation | 2 | FixConnectivityGraphUsingDelaunayTriangulation(:2802-2952):并查集;先合并已有边;Delaunay 邻接里 DistSq < 50²×100×ClusterConnectionFactor(≈500cm×√Factor)直接连;其余候选按距离排序,仅当能连通两个不同连通分量才连 | 名为「最小生成子集」,实为「近距全连 + MST 式补桥」 |
PointImplicitAugmentedWithMinimalDelaunay | 3 | 先 PointImplicit,再跑上面的 Fix 补桥 | :2153-2172 |
BoundsOverlapFilteredDelaunayTriangulation | 4 | Delaunay 邻接 + 世界包围盒(WorldSpaceInflatedBounds,加 ConnectionGraphBoundsFilteringMargin)相交才保留(:2969-2978, 3053-3068) | 用于 union 的 intercluster edge |
None | 5 | GenerateConnectionGraph 里被当作 MinimalSpanningSubset 默认方法 | :2146-2151 |
默认值谱系(易混):
FClusterCreationParameters构造默认MinimalSpanningSubset(ClusterCreationParameters.h:31)。FSimulationParameters.ClusterConnectionMethod默认PointImplicit(GeometryCollectionSimulationCoreTypes.h:188),但会被资产ClusterConnectionType(默认MinimalSpanningSubset…,GeometryCollectionObject.cpp:112) 覆盖(GeometryCollectionComponent.cpp:3983, 4004)。- Solver 级
ClusterConnectionFactor=1.0、ClusterUnionConnectionType=DelaunayTriangulation(ChaosSolverConfiguration.cpp:17-18);后者只用于 union 的默认创建参数(ClusterUnionManager.cpp:1360-1365)。 MaxNumConnections=100参数未被执行(相关判断被注释掉,PBDRigidClustering.cpp:2692, 2697)。
4.2 连接图从哪来:离线优先
InitializeBodiesPT(GeometryCollectionPhysicsProxy.cpp:1648-1730):
bGenerate = !Facade.IsValid() || p.GeometryCollection.AlwaysGenerateConnectionGraph || !Facade.HasValidConnections()
if bGenerate: // 运行时现算
for 每个簇: RigidClustering.GenerateConnectionGraph(簇, {ConnectionMethod, BoundsFilteringMargin})
if 材料模型: 每条边.Area = AreaFromBoundingBoxOverlap(两端包围盒) // :1673-1689
else: // 读资产预烘焙
for 每条 (A,B[,ContactArea]): 双向 Emplace 边
v = HasContactAreas ? ContactArea : (A.内部阈值+B.内部阈值)/2
if 无 ContactArea 且 材料模型: v = AreaFromBoundingBoxOverlap(A,B) // :1718-1722资产生成链(编辑器):CreateNonOverlappingConvexHullData→ProximityUtility.RequireProximity+SetVolumeAttributes(GeometryCollectionConvexUtility.cpp:1791-1793)→(若 bUseAsConnectionGraph)CopyProximityToConnectionGraph(GeometryCollectionProximityUtility.cpp:610-717):把叶子级邻接沿父链爬升到「同父兄弟」层级,得到每个簇的兄弟边;ConvexHullContactArea 模式把叶-叶接触面积累加成兄弟边面积。邻接方法 EProximityMethod{Precise(顶点接触/共面反向三角,阈值0.01), ConvexHull(带距离阈值)},可再用 RequireContactAmount+EProximityContactMethod 过滤掉角点/尖锐接触(GeometryCollectionProximityUtility.h:17-36,.cpp:726-872)。
GT 侧另有 FGeometryCollectionConnectionGraphGenerator(GeometryCollectionConnectionGraphUtility.cpp:9-60)复刻 Delaunay/Minimal 逻辑;PointImplicitAugmentedWithMinimalDelaunay 会命中 check(false)(:37-38),而纯 PointImplicit 会先被降级为 Minimal(:28-32)。
4.3 边上存什么
TConnectivityEdge{ Sibling, Strain }(PBDRigidClusteredParticles.h:46-75)。注释明确:「既可能是 strain 也可能是 area,因向后兼容不改名」。
- 默认模型:
ConnectClusteredNodes写(A.内部阈值 + B.内部阈值)/2(PBDRigidClustering.cpp:2689-2701),该值在判定中并不使用,边只提供拓扑与传播路径。 - 材料模型:存面积,用于
ComputeMaterialBasedDamageThreshold_Internal(§5.4)。
4.4 Cluster Union 的跨 GC 边
- 加入 union 时用 union 自己的 BVH 找重叠:
AddParticleToConnectionGraph用kAddThickness = 5cm,LevelSet 只做 AABB 判断,其他形状做OverlapQuery(MTD)(ClusterUnionManager.cpp:68-179)。 - GC 在 union 内破裂/脱离时,用
BoundsOverlapFilteredDelaunayTriangulation、Margin=1.0在两个 GC 的子节点集合之间生成 intercluster edge(GenerateInterclusterEdgesBetweenParticles,:1115-1153,用FromSet/ToSet限定边必须跨集合,PBDRigidClustering.cpp:2677-2686)。
5. 应变/损伤与破碎判定
5.1 阈值(InternalStrain)怎么设
UpdateDamageThreshold_Internal(GeometryCollectionPhysicsProxy.cpp:1245-1298),对每个粒子:
switch DamageModel:
UserDefined_Damage_Threshold:
if !bUsePerClusterOnlyDamageThreshold && (EnableClustering || 是簇):
T = ComputeUserDefinedDamageThreshold_Internal(TransformIndex)
else T = DamageThreshold[0]
Material_Strength_And_Connectivity_DamageThreshold:
T = ComputeMaterialBasedDamageThreshold_Internal(TransformIndex)
RigidClustering.SetInternalStrain(Handle, T)
// 旧兼容模式 PerClusterOnlyDamageThreshold: 簇算出值,下发覆盖直接子节点 (:1281-1296)用户阈值模型(:1946-1992)
Level = clamp(层级 - (bPerClusterOnly ? 0 : 1), 0, ∞) // 子块脱离「它的父簇」所需阈值
if Level >= min(MaxClusterLevel, GlobalMaxSimulatedLevel, MaxSimulatedLevel): T = FLT_MAX
elif bUseSizeSpecificDamageThresholds: T = SizeSpecific[按 Size 选档].DamageThreshold
else: T = (Level < N) ? DamageThreshold[Level] : DamageThreshold[N-1]
if bUseMaterialDamageModifiers: T *= PhysicalMaterial.DamageModifier.DamageThresholdMultiplier| 变换层级 L(root=0) | 取表下标 | 默认 | 含义 |
|---|---|---|---|
| 0 (root) | 0 | 500000 | 无父,不会被用到 |
| 1 | 0 | 500000 | 把一级块从 root 掰下来 |
| 2 | 1 | 50000 | 把二级块从一级簇掰下来 |
| ≥3 | 2…末值 | 5000 | 叶子级 |
- 默认数组:资产与组件都是
{500000, 50000, 5000}(GeometryCollectionObject.cpp:108,GeometryCollectionComponent.cpp:613,FSimulationParameters同值GeometryCollectionSimulationCoreTypes.h:179)。ApplyAssetDefaults把资产值拷到组件(GeometryCollectionComponent.cpp:5126-5146)。 - SizeSpecific 选档:
GetSizeSpecificData优先用Size(相对尺寸) 属性,否则用包围盒体积(GeometryCollectionPhysicsProxy.cpp:5113-5127);FindIndexForVolume在按 MaxSize 升序的数组里取「第一个Volume < MaxSize」的档(GeometryCollectionSizeSpecificUtility.cpp:20-35)。FSharedSimulationSizeSpecificData默认MaxSize=0, DamageThreshold=5000(GeometryCollectionSimulationCoreTypes.h:66-80);资产默认档MaxSize=99999.9,DamageThreshold=5000;GetSharedSimulationParams另补一档MaxSize=FLT_MAX,其DamageThreshold取默认构造值 5000,并非资产里设置的值(GeometryCollectionObject.cpp:96, 194-210, 534-566)。 - (推断,需实测)legacy 模式
bUsePerClusterOnlyDamageThreshold=true时,第一轮循环因if (!bUsePerClusterOnlyDamageThreshold)不调用ComputeUserDefinedDamageThreshold_Internal(GeometryCollectionPhysicsProxy.cpp:1264),所有粒子先取DamageThreshold[0],第二轮再把「父簇值」拷给子节点(:1281-1296)——按代码阅读,逐层递减在该模式下似乎不会生效。 - 5.7 的
PerClusterOnlyDamageThreshold(兼容开关)默认 false:「false 时每个子块独立算阈值,更精确直观」(GeometryCollectionObject.h:583-587)。
材料强度+连通性模型(:1884-1944)
Area = 聚合(该粒子的所有 ConnectivityEdges.Area) // p.GeometryCollection.AreaBasedDamageThresholdMode: 0 sum(默认)/1 max/2 min/3 avg
T = Area > 0 ? Material.Strength.TensileStrength × Area : FLT_MAXTensileStrength默认 2MPa(混凝土下限),换算MPa×10000→kg/(cm·s²)(PhysicalMaterial.cpp:30-35,Defines.h:46-63)。- 该模型下碰撞载荷会再
× 1/Dt,使之成为「力」而不是「冲量」(PBDRigidClustering.cpp:2353-2359)。 - 破裂后因邻居减少会对岛内成员重算阈值(
:1399-1414)。 - 疑点(推断):
ComputedConnectionArea初值为 0,模式 2(min) 的Min(0, area)恒为 0(GeometryCollectionPhysicsProxy.cpp:1905, 1922-1923),会得到FLT_MAX。
5.2 载荷入口 A:碰撞(ComputeStrainFromCollision,PBDRigidClustering.cpp:2226-2426)
Reset CollisionImpulses (若上步 dirty) // :2231, 2429-2438
for 每个碰撞约束 C:
if C.IsOneWayInteraction: continue // 碎渣(debris)不致伤
若两端都不是 clustered: continue
// 过滤「静止接触」:
if p.chaos.UseContactSpeedForStrainEval(默认1):
SpeedAlongNormal = (V0.Pre − V1.Pre)·n // 注意用 PreV
if SpeedAlongNormal > −MinContactSpeedForStrainEval(1.0 cm/s) && 累积冲量>0: continue
else if |AccumulatedImpulse| < MinImpulseForStrainEval(980*2/30≈65.3): continue
for 两端中的每个 clustered 粒子 Cluster:
I = |C.AccumulatedImpulse| // 有限且 > UE_SMALL_NUMBER
if Cluster 是 ClusterUnion proxy: // 把载荷下沉到「接触形状对应的子 GC」
if !proxy.bEnableStrainOnCollision: continue
Cluster = ClusterUnion.ChildParticles[接触形状索引](失败回退最近子)
if GC proxy 且 !bEnableStrainOnCollision: continue
if 材料模型: I *= 1/Dt
if Cluster.NumChildren == 0: Cluster.CollisionImpulses += I // 叶子直接吃
elif bUseDamagePropagation: 只给「离接触点最近的子」 += I // 靠传播扩散
else: 取接触点(簇局部) 的盒子 Thicken(p.ClusterDistanceThreshold=100cm)
for 子 in ChildrenSpatial.FindAllIntersectingChildren(盒子): 子.CollisionImpulses += I // 每个都拿满,不分摊
UpdateTopLevelParticle(子) // 登记其父簇到 Strained 表- 关键默认:
p.ClusterDistanceThreshold=100、p.ComputeClusterCollisionStrains=1、p.chaos.MinContactSpeedForStrainEval=1(:39-40, 51-52, 2216-2223)。 - 脏标记
MCollisionImpulseArrayDirty只在确实累加了冲量时置位,用来跳过无谓的 BreakingModel(:2408-2423, 1835)。 - 残留 CVar:
p.GraphPropagationBasedCollisionImpulseProcessing/Factor已声明但源码内无引用(:63-67)。
5.3 载荷入口 B:Field / BP
Field_ExternalClusterStrain:对每个采样粒子SetExternalStrain(max(旧, 新))(FieldSystemProxyHelper.h:502-521)。Field_InternalClusterStrain:SetInternalStrain(旧阈值 + 场值),仅对ObjectState==Dynamic(:568-583)。- BP/C++:
ApplyExternalStrain(ItemIndex, Location, Radius, PropagationDepth, PropagationFactor, Strain)(GeometryCollectionComponent.h:704-726)→ PT:以FindChildrenWithinRadius(…, bAlwaysReturnClosest=true)选子块,沿ConnectivityEdgesBFS 扩 PropagationDepth 层,每层乘PropagationFactor衰减(GeometryCollectionPhysicsProxy.cpp:2770-2858)。与伤害传播不同:这里一次命令内即时完成。 ApplyInternalStrain:Internal -= Strain×衰减,下限 0(:2860-2879)。阈值 ≤0 会被ProcessClusteredParticle判为「必破」并把CollisionImpulse置FLT_MAX(PBDRigidClustering.cpp:1730-1737)。- 强制破:
BreakCluster/BreakClustersByProxy把所有子的 External/Internal 都设为FLT_MAX,并置CrumbledSinceLastUpdate与发 Crumbling 事件(:2569-2667);ReleaseClusterParticles(list)/ForceReleaseChildParticleAndParents同理(:1578-1650)。
5.4 判定与释放(ReleaseClusterParticlesImpl,PBDRigidClustering.cpp:1109-1349)
if 簇.Unbreakable: return
Children = MChildren[簇]
for Child in Children (倒序):
Applied = max(Child.CollisionImpulses, Child.ExternalStrain)
if Applied >= Child.InternalStrain || bForceRelease:
if 首次释放: TrackBreakingCollision(撞击者动量记录);若簇在 union 里: 先生成 intercluster edges、删节点边、延迟移出 union
if Child 在 union 里: 延迟 union.Remove
else: RemoveChildFromParent(Child, 簇); UpdateTopLevelParticle(Child); Children.RemoveAtSwap
Activated.Add(Child); SendBreakingEvent(Child, bParentCrumbled)
if 开伤害传播: AppliedStrains[Child] = Applied
Child.ExternalStrain = 0 // 载荷清零
传播(见 §5.5)
if Activated 非空:
if UseConnectivity(p.UseConnectivity=1):
for c in Activated: RemoveNodeConnections(c)
Activated += HandleConnectivityOnReleaseClusterParticle(簇, bCreateNewClusters) // §7
for c in Activated(非有父的): UpdateKinematicProperties(c)
if 簇不是 union: DisableCluster(簇) // 原簇退场
ThrottleReleasedParticlesIfNecessary(Activated)- 比较是
>=(:1179)。>=/FLT_MAX的配合使「强制破」与「永不破(FLT_MAX)」都无需特殊分支。 - 每个簇本步只处理一轮,不会在同一次调用里递归评估新释放的子簇(它们的载荷下一步才会结算)。
5.5 伤害传播(FGeometryCollectionDamagePropagationData)
组件/资产字段(GeometryCollectionDamagePropagationData.h:7-24):bEnabled=true、BreakDamagePropagationFactor=1.0、ShockDamagePropagationFactor=0.0。组件构造里把 bEnabled 置 false 以兼容旧资产(GeometryCollectionComponent.cpp:707),而 ApplyAssetDefaults 会从资产拷(:5137)→ 新资产默认启用。
传播逻辑(PBDRigidClustering.cpp:1257-1299):
for (Child, Applied) in AppliedStrains:
if Break > 0 && Child 已破:
Remain = Applied − Child.InternalStrain
if Remain > 0: PerEdge = Break × Remain / Child.连接边数
elif Shock > 0: // 未破块,或 Break==0 时的已破块
PerEdge = Shock × Applied // 注意:不除以边数
for 每条边的 Sibling: Sibling.ExternalStrain = max(Sibling.ExternalStrain, PerEdge) // 并登记 Strained- 「此时图尚未更新」(
:1256);邻居的新载荷在下一步 AdvanceClustering 才被评估 → 每步前进一跳,天然形成「裂纹蔓延」动画。 - 开启后碰撞只给最近子块(
:2370-2383),由传播负责扩散。
5.6 运行时参数一览(见 §10 汇总表)
5.7 没有疲劳、没有累积
CollisionImpulses每步重置(:2231);ExternalStrain评估即清零(:1247)。- 唯一的持久状态是
InternalStrain(被 Field/BP 加减修改)。
5.8 StrainModifier 扩展点
ISimCallbackObject::StrainModification_Internal(FStrainModifierAccessor&) 在 ProcessClusteredParticle 之后、BreakingModel 之前被调用(PBDRigidClustering.cpp:1825-1828, :2470-2480)。StrainModification.h:51-198 提供:GetStrainedProxies、GetMaxBreakStrainRatio(FatigueThresholdPercent, FatigueThresholdMinimum, bRelative, StrainTypes)、AdjustStrainForBreak、ClearStrains、GetNumBreakingStrains —— 即 自定义疲劳/限破裂数 的官方入口。
6. 一步之内发生什么(时序)
6.1 Solver Tick(PBDRigidsSolver.cpp:490-660)
ApplyCallbacks_Internal / FieldParameterUpdateCallback(各 GC proxy) :504-519
RigidClustering.UnionClusterGroups() → ClusterUnionManager.FlushPendingOperations() :545 (每个 Solver tick 一次,在子步循环之外)
IslandManager.UpdateExplicitSleep; CollisionConstraints.BeginFrame()
while 子步:
FieldForcesUpdateCallback(...) :575-583
Evolution.AdvanceOneTimeStep(Dt)
FillProducerData(事件) ; Evolution.EndFrame ; CompleteSceneSimulation :618-651
RigidClustering.ResetAllEvents() // 刻意放在 Complete 之后,保证缓存/复制客户端不丢事件 :653-6606.2 AdvanceOneTimeStep 里的位置(PBDRigidsEvolutionGBF.cpp:748-818)
IslandGroupManager.Solve → CCD → UpdateSleep/UpdateDisable → **Clustering.AdvanceClustering** → CaptureRewindData → ParticleUpdatePosition。即:先把本步碰撞解完,再用 AccumulatedImpulse 判破,释放出的粒子 P/Q/V/W 在 ParticleUpdatePosition 前已就位,当步就会被积分。
6.3 AdvanceClustering(PBDRigidClustering.cpp:1672-1897)
if MChildren 非空:
if p.ComputeClusterCollisionStrains: ComputeStrainFromCollision else Reset
Strained 表按时间戳排序 → 逐个 ProcessClusteredParticle:
// union 内部簇不直接处理,改为展开它的子 GC
for 子 in 簇.Children: 子阈值<=0 ⇒ 置必破;子有 External/Collision 载荷 ⇒ 加入 ParticlesToProcess, bPotentialBreak
ApplyStrainModifiers(ParticlesToProcess)
ClusterUnionManager.HandleDeferredClusterUnionUpdateProperties()
TopLevelClusterParentsStrained.Reset() // 破裂会重新填
if MCollisionImpulseArrayDirty || bPotentialBreak:
if p.chaos.clustering.breakonlystrained(1):
for P in ParticlesToProcess: if BreakingModel({P}): NumBreaks++ ; 达 PerAdvanceBreaksAllowed 则 break
未处理的前 PerAdvanceBreaksRescheduleLimit 个重新登记到下一步
else: BreakingModel() // 遍历所有 GetNonDisabledClusteredView,O(所有簇)
if bCheckForInterclusterEdgesOnRelease: ClusterUnionManager.FlushPendingOperations()BreakingModel(span)(:1970-2001):对每个有子的簇调 ReleaseClusterParticles;只要有释放,则 HandleDeferredClusterUnionUpdateProperties(批量重建 union 几何/连接图/属性)并 RestoreBreakingMomentum。
6.4 预算与限流
| 机制 | 默认 | 说明 | 行 |
|---|---|---|---|
PerAdvanceBreaksAllowed | INT_MAX | 每次 AdvanceClustering 允许的「产生释放的簇」个数;min(Settings, p.Chaos.Clustering.PerAdvanceBreaksAllowed) | ChaosSolverConfiguration.h:25-27; PBDRigidClustering.cpp:1841-1861, 3133 |
PerAdvanceBreaksRescheduleLimit | INT_MAX | 超额簇中最多 N 个顺延到下一步 | :1863-1878 |
ClusteringParticleReleaseThrottlingMin/MaxCount | -1(关) | 需两者都 ≥0 才启用;活跃 GC 粒子数(睡眠+动态)在 [Min,Max] 线性映射为「新释放粒子被立刻 Disable 的比例」,≥Max 则全部 Disable | :141-190, 3094-3107, 3135-3136 |
OneWayInteractionLevel | -1(关) | 层级 ≥N 的块为「碎渣」:不对非碎渣施力、也不产生应变 | GeometryCollectionComponent.h:982-985; PBDRigidClustering.cpp:2240 |
RestoreBreakingMomentumPercent | 0.5 | 撞击者在破裂前被簇「挡住」的动量,破后回补 50% | :69-70, 701-773 |
回补细节:对破裂簇上每个接触,ΔV = InvM · Impulse · 0.5,ΔW = InvI_world · (r × Impulse) · 0.5,在 BreakingModel 末尾累加到撞击者 V/W(TrackBreakingCollision/RestoreBreakingMomentum)。效果:子弹/物体「穿透」脆弱结构而不是被刚体撞停。
被 throttle 的粒子:
DisableParticle后无父 ⇒ GT 视为HasDecayed,不再渲染(GeometryCollectionPhysicsProxy.cpp:4236)。
7. 破裂之后:连通分量、倒塌、速度继承
7.1 HandleConnectivityOnReleaseClusterParticle(PBDRigidClustering.cpp:1352-1575)
Islands = FindIslandsInChildren(簇, bTraverseInterclusterEdges = 簇在 union 内) // DFS + TSet visited :839-908
if 簇不是 union:
for Island:
if 材料模型: 对 Island 内每个成员重算阈值 // 失去邻居→更脆
if bCheckForInterclusterEdgesOnRelease: 岛内若有 intercluster 边连到「bCheckConnectivity 的 union」→ 整岛(或仅直接连到主粒子的那部分)重新并回该 union
elif Island.Num()==1: RemoveParticlesFromCluster(簇,{唯一子}) ; Activated.Add // 单块独立
if bCreateNewClusters: // 多块岛
for Island(Num>1): RemoveParticlesFromCluster(簇, Island) ; DisableParticle(各成员,保留在children map)
NewClusters = CreateClustersFromNewIslands(Islands, 簇)
else (union 内的 GC): 只由「是否含主粒子」决定:全为辅助粒子的岛被移出 unionCreateClustersFromNewIslands(:975-1047):每个多块岛 → CreateClusterParticleFromClusterChildren(:557-668,bInternalCluster=true,复制父的重力/CCD/MACD/OneWay/Drag/睡眠倍率/迭代设置;重算质量;UpdateKinematicProperties;UpdateGeometry(无代理);BuildConvexOptimizer)→ 再设 InternalStrain = 父的、V/W = 父的 V/W、P/Q = X/R、拷贝物理材质。
7.2 速度继承
单块释放(RemoveChildFromParent,:910-958):
PreSolveTM = (父.P, 父.Q)
Child.X/R = Child.ChildToParent * PreSolveTM ; Child.P/Q = X/R
PDiff = Child.P − 父.P
Child.V += 父.V + 父.W × PDiff // 刚体上一点的线速度
Child.W += 父.W
Child.PreV/PreW 同理(用 父.PreV/PreW)坑点:岛簇(CreateClustersFromNewIslands:1013-1016)直接 V=父.V, W=父.W,没有叠加 W × r,而新簇质心已搬到岛质心(推断:岛离父质心越远,初速偏差越大)。
7.3 事件
SendBreakingEvent(Child, bFromCrumble):仅当 proxy 勾选bGenerateBreakingData/GlobalBreakingData才写FBreakingData(位置/朝向/V/W/质量/包围盒/TransformGroupIndex)(:780-804)。SendCrumblingEvent:簇一次性全破时发(BreakCluster);可选带子索引列表(:807-837)。- 事件队列
MAllClusterBreakings/Crumblings,每个 Solver tick 末在Complete之后清空(PBDRigidsSolver.cpp:653-660)。
7.4 internal cluster 的回收
破裂后 DisableCluster(原簇退场,但 GC handle 保留以备重置);internal cluster 变空时先 DisableParticle 并登记到 EmptyInternalClustersPerProxy(:943-955),直到 proxy 销毁才 DestroyClusterParticle(PBDRigidsSolver.cpp:1451-1461)。
8. Cluster Union(ClusterUnionManager,5.4+)
8.1 数据结构(ClusterUnionManager.h:20-144)
FClusterUnion:InternalCluster(负索引的簇粒子)、Geometry(union)、ChildParticles(与 shape 一一对应,删除不能 RemoveAtSwap,.cpp:907-910)、GeometryChildParticles、ChildProperties{bIsAuxiliaryParticle, bEdgesAreGenerated}、InternalIndex/ExplicitIndex、创建参数、bCheckConnectivity(默认 false)、bGenerateConnectivityEdges(默认 true)、PendingConnectivityOperations、PendingGeometryOperations。
操作:EClusterUnionOperation { Add, AddReleased, Remove, UpdateChildToParent };更新标志 EUpdateClusterUnionPropertiesFlags(重算质量朝向 / 全量或增量建图 / 更新运动学 / 全量或增量建几何 / 连通性检查)(:20-49)。
8.2 生命周期
CreateNewClusterUnion(params, uParams) // :295-336
idx = ClaimNextUnionIndex() // 复用已释放索引,否则 +1,从 1 开始
InternalCluster = Clustering.CreateClusterParticle(-idx, {}, params, 空 union 几何)
InternalCluster.SetInternalCluster(true); 先 DisableParticle(无子)
入口(GT→PT): AddPendingClusterIndexOperation(idx, Add/Remove, 粒子) // ClusterUnionPhysicsProxy.cpp:188-224
每 Solver tick 开头 FlushPendingOperations: // :427-498
显式索引→映射到内部索引
Remove 优先于 Add(p.Chaos.ClusterUnion.FixRemoveDuringSort),再 UpdateChildToParent
HandleDeferredClusterUnionUpdateProperties
HandleAddOperation: // :534-777
过滤已在内的;先从别的 union 摘下(HandleRemoveOperationWithClusterLookup)
bReleaseClustersFirst(AddReleased): 先 ReleaseClusterParticles(簇, force) 把簇打散,再加其子 // 旧 ClusterGroupIndex 行为
AddParticlesToCluster(...); 属性合并: OneWay 取 AND, MACD 取 OR, 睡眠倍率取 min, Drag 取 max, Gravity 取 AND, 迭代设置 Merge
新建→RequestDeferredClusterPropertiesUpdate(All) ;已有→只做增量图/增量几何
DeferredClusterUnionUpdate → UpdateAllClusterUnionProperties: // :978-1054
UpdateClusterMassProperties(全部子); 必要时 MoveClusterToMassOffset(Position)
UpdateKinematicProperties (或 bAnchorLock 时直接用 anchored 标志)
InvalidateParticle(清碰撞缓存); 再生/增量更新几何(每子一个 FImplicitObjectTransformed 的 union, SetAllowBVH)
生成/增量更新连接图; 可选 ConnectivityCheck → HandleConnectivityOnReleaseClusterParticle(…, false)
BuildConvexOptimizer8.3 与破碎的交互
- 应变落点:碰撞命中 union 时按接触 shape 索引找到子 GC,再对其孙子施加冲量(
PBDRigidClustering.cpp:2286-2332)。AdvanceClustering对 union 内部簇展开其ChildParticles逐个处理(:1783-1808)。 - GC 在 union 内破裂:被释放子块若本身在 union,则延迟
Remove;原 GC 簇先GenerateInterclusterEdgesForParticle以便后续连通性判定(:1199-1216, 1250-1253)。 bCheckConnectivity:union 内含「主粒子/辅助粒子」,仅由辅助粒子组成的连通岛会被踢出 union(:1525-1571)。p.Chaos.OnlyUseInterclusterEdgesAttachedToMainParticles(默认 true) 限定「只有直连主粒子的才留在 union」(:48-49)。
8.4 旧入口 ClusterGroupIndex
- 组件
ClusterGroupIndex:「同号的 GC 作为一个簇模拟」(GeometryCollectionComponent.h:1004-1006);仅 root 携带(GeometryCollectionPhysicsProxy.cpp:2174)。 - 类内注释:group index 创建后不应再改;
UnionClusterGroups在AdvanceClustering中调用(旧注释,实际在 Solver tick 开头PBDRigidsSolver.cpp:545)。
8.5 工程注意
- union 几何是每子一个 shape 的扁平 union:成员越多,shape 数越多;
ForceRegenerateGeometry是全量重建(ClusterUnionManager.cpp:1175-1198),增量接口仅在已有FImplicitObjectUnion时可用(:1201-1212)。 - 源码注释强调批量:「大 union 一次性破裂,只更新一次属性」(
PBDRigidClustering.cpp:1990-1992)。 - 解算器提示:变更 union 后
RewindData->BlockResim()(.cpp:490-494),resim 不支持动态 union。
9. GT ↔ PT 数据流(FGeometryCollectionPhysicsProxy)
GT UGeometryCollectionComponent::RegisterAndInitializePhysicsProxy (Component.cpp:3957-4070)
→ 填 FSimulationParameters (DamageThreshold/DamageModel/MaxClusterLevel/ClusterConnectionMethod/…)
→ new Proxy(GameThreadCollection = 复制 RestCollection)
GT Proxy::Initialize (Proxy.cpp:688-821)
EffectiveParticles = Level<=MaxSimulatedLevel (CalculateEffectiveParticles)
仅为 root 建 GTParticle(其余视 p.gc.* 标志) ; 若 EnableClustering: Active[i] = (无父) :790-808
PT Proxy::InitializeBodiesPT (Proxy.cpp:1300-1787)
叶子 GeometryCollection 粒子(CreateNonClusteredParticles) → PopulateSimulatedParticle(ParallelFor)
自底向上 RecursiveOrder: BuildClusters_Internal → FRigidClustering::CreateClusterParticle :1535-1646
连接图(§4.2) → 睡眠状态 → 逐粒子特性(重力/CCD/MACD/迭代/OneWay/阻尼…) :1745-1772
UpdateDamageThreshold_Internal() ; DirtyAllParticles() :1775-1778
PT 每 tick: AdvanceClustering → BufferPhysicsResults_Internal (Proxy.cpp:4165-4379)
只对「有变化」的 transform 写 FGeometryCollectionResults: X,R,V,W,
State{DisabledState, HasParent, DynamicState, HasInternalClusterParent, DynamicInternalClusterParent, HasClusterUnionParent},
InternalClusterUniqueIdx;IsRootBroken = (root.Disabled && 无父) :4200-4208
有 internal cluster 父的子:用 ChildToParent*父 重算世界位姿 :4282-4312
GT PullFromPhysicsState (插值: Prev/Next + Alpha, 支持复制误差平滑) (Proxy.cpp:4718-…)
写回 GameThreadCollection (Active/Parent/Transform) ; bAtLeastOneChildActive 时才 CreateChildrenGeometry_External :4825-4828
GT Component::OnPostPhysicsSync (Component.cpp:4412-): 脏变换 → 渲染/导航;root broken 事件(一次性);Removal 计时
GT→PT 命令一律 Solver->EnqueueCommandImmediate:
BreakClusters_External(2605) / SetAnchored*(2670-2768) / ApplyExternalStrain_External(2837) / ApplyInternalStrain_External(2860)
SetDamageThresholds_External(3665) / SetDamageModel / SetDamagePropagationData(3681-3695) / SetPhysicsMaterial(3890) → 均会 UpdateDamageThreshold_Internal
Field 命令: BufferFieldCommand_External(6104) → FieldParameterUpdateCallback(6130) 每 tick 执行- 粒子延迟创建:
BuildGeometryForChildrenOnPT/GT、CreateGTParticleForChildren默认 true(旧 CVar 已标 DEPRECATED,Proxy.cpp:632-663);破裂时ReleaseClusterParticlesImpl调CreateChildrenGeometry_Internal(PBDRigidClustering.cpp:1137-1149)。 bOptimizeForRuntimeMemory(FChaosSolverDestructionSettings,ChaosSolverConfiguration.h:41-47)→ 开启则「首次破裂才创建非必需粒子/几何」。但在Engine/Source内检索不到把它写入FSimulationParameters::bOptimizeForRuntimeMemory的代码(GeometryCollectionSimulationCoreTypes.h:222默认 false;只有Proxy.cpp:669-684读取)——Plugin 侧可能有赋值,Source 内看是未接线(推断)。- 破碎渲染:GT 只相信
Active/Parent/HasInternalClusterParent等状态;internal cluster 子块位姿由ChildToParent推算,因此岛簇移动时子块不单独占用 PT 资源。
10. 异步 / 多线程 / 迭代 / Solver 与 CVar 参数
10.1 线程模型
- 簇逻辑(评估、释放、连通性、union 更新)全部在 PT 单线程的
AdvanceOneTimeStep内串行执行,没有ParallelFor。并行的只有:PointImplicit连接(PBDRigidClustering.cpp:2740)、Proxy 初始化ParallelFor(Proxy.cpp:1373)、资产构建期的质量/形状(:5412, 5613, 5664)。Delaunay 注释为非线程安全(:2161)。 - GC proxy 要求
EMultiBufferMode::TripleGuarded(Proxy.cpp:622);GT 通过插值 + 误差平滑消费 PT 结果(:4753-4773)。 - 事件在
FillProducerData之后由 GT 消费,ResetAllEvents延后到CompleteSceneSimulation之后(PBDRigidsSolver.cpp:618-660)。
10.2 迭代次数
- 没有 cluster 专属迭代;簇是普通刚体,走全局
Position/Velocity/Projection迭代:Solver 默认8 / 2 / 1(PBDRigidsEvolutionGBF.h:62-64),GC 组件覆盖默认8 / 1 / 1(GeometryCollectionComponent.cpp:4031-4036),internal cluster 复制父的IterationSettings(PBDRigidClustering.cpp:590)。 - 破裂的「级联」:同一步内新释放块的载荷不会再次结算;传播每步前进一跳(§5.5);深层级联 = 多步。
10.3 Solver 配置 FChaosSolverConfiguration(ChaosSolverConfiguration.h:105-112)
| 字段 | 默认 | 作用 |
|---|---|---|
ClusterConnectionFactor | 1.0 | 乘到 Minimal/Augmented 方法的「必连距离²」:50²×100×Factor(PBDRigidClustering.cpp:2909);GT 生成器里写死 1.0(ConnectionGraphUtility.cpp:204) |
ClusterUnionConnectionType | DelaunayTriangulation | 仅影响 union 默认创建参数(ClusterUnionManager.cpp:1360-1365) |
DestructionSettings.* | 见 §6.4 | 预算/限流 |
10.4 CVar 汇总(PBDRigidClustering.cpp、ClusterUnionManager.cpp、GeometryCollectionPhysicsProxy.cpp)
| CVar | 默认 | 含义 |
|---|---|---|
p.ClusterDistanceThreshold | 100 | 碰撞点周围多大盒子内的子块吃载荷 :39-40 |
p.UseConnectivity | 1 | 破裂后是否做连通分量处理 :42-43 |
p.ComputeClusterCollisionStrains | 1 | 是否由碰撞产生应变 :51-52 |
p.chaos.clustering.breakonlystrained | 1 | 只处理有载荷簇 :1652-1654 |
p.chaos.UseContactSpeedForStrainEval / MinContactSpeedForStrainEval / MinImpulseForStrainEval | true / 1.0 / ≈65.3 | 静止接触过滤 :2216-2223 |
p.RestoreBreakingMomentumPercent | 0.5 | 动量回补 :69-70 |
p.Chaos.Clustering.PerAdvanceBreaksAllowed / …RescheduleLimit | INT_MAX | 与 Solver 值取 min :1656-1662, 3133-3134 |
p.Clustering.ParticleReleaseThrottlingMin/MaxCount | -1 | 取 Max(Solver,CVar) :72-76, 3135-3136 |
p.Chaos.Clustering.DumpClusterAndReleaseStats | 0 | 每帧输出处理簇数/释放数 :1664-1666, 1683-1699 |
p.Chaos.CheckForInterclusterEdgesOnRelease / OnlyUseInterclusterEdgesAttachedToMainParticles | true / true | union 内 GC 破裂的重新并入 :45-49 |
p.UnionsHaveCollisionParticles | 0 | internal cluster 是否复制碰撞粒子 :546-547 |
p.DeactivateClusterChildren | 0 | 内部簇是否禁用其子(TODO 注释) :54-55, 612-635 |
p.UseBoundingBoxForConnectionGraphFiltering / BoundingBoxMarginForConnectionGraphFiltering | 0 / 0 | Delaunay 加 AABB 过滤 :57-61 |
p.Chaos.ClusterUnion.GenerateInterclusterEdges | true | ClusterUnionManager.cpp:20-25 |
p.GeometryCollection.AlwaysGenerateConnectionGraph | false | 忽略资产图,运行时重建 Proxy.cpp:93-97 |
p.GeometryCollection.AreaBasedDamageThresholdMode | 0 | Proxy.cpp:99-103 |
p.gc.GlobalMaxSimulatedLevel | 100 | 全局层级上限(与 MaxSimulatedLevel 取 min):1877-1878 |
p.ReportTooManyChildrenNum | -1 | 子数超限告警 :1123-1124, 1578-1582 |
p.Chaos.Solver.DebugDraw.Cluster.Constraints | 0 | 画连接图;p.Chaos.DebugDraw.ConnectionGraph.ShowAreas/ShowInternalStrains/ConnectionGraphLevelOffset PBDRigidsSolver.cpp:163, 200, 3522-3525;ChaosDebugDraw.cpp:127-134 |
10.5 组件/资产主要 UPROPERTY(默认)
| 属性 | 默认 | 位置 |
|---|---|---|
EnableClustering | true | Component.h:1001-1002;ctor :609 |
ClusterGroupIndex | 0 | :1004-1006 |
MaxClusterLevel | 100 | :1008-1010(ctor :611) |
MaxSimulatedLevel | 100 | :1019-1020(注释 :1012-1018) |
DamageModel | UserDefined | :1023-1024 |
DamageThreshold[] | 500000/50000/5000 | :1029-1030 |
bUseSizeSpecificDamageThreshold | false | :1039-1040 |
bUseMaterialDamageModifiers | false | :1043-1044 |
DamagePropagationData | 见 §5.5 | :1050-1051 |
bEnableDamageFromCollision | true | :1057-1058 |
ObjectType | Dynamic | :972-974 |
InitialVelocityType | User_Defined;InitialLinear/AngularVelocity=0 | :1093-1102;ctor :622 |
CollisionGroup / CollisionSampleFraction | 0 / 1.0 | :1077-1082 |
OneWayInteractionLevel | -1 | :982-985 |
资产 ClusterConnectionType / ConnectionGraphBoundsFilteringMargin | MinimalSpanningSubsetDelaunay / 0 | GeometryCollectionObject.h:593-598;.cpp:112-113 |
资产 PerClusterOnlyDamageThreshold | false | GeometryCollectionObject.h:583-587 |
| 资产 Removal 组 | bRemoveOnMaxSleep=false、MaximumSleepTime=(5,10)s、RemovalDuration=(2.5,5)s、bSlowMovingAsSleeping=true、SlowMovingVelocityThreshold=1、bScaleOnRemoval=true、bAutomaticCrumblePartialClusters=true | GeometryCollectionObject.h:788-814;.cpp:137-143 |
11. Removal:BreakTime / Sleep Removal / Cluster Crumbling
GeometryCollectionEngineRemoval.h(动态集合 facade)+ 组件 IncrementBreakTimer/IncrementSleepTimer(GeometryCollectionComponent.cpp:6679-6820)。组件开关 bAllowRemovalOnSleep/bAllowRemovalOnBreak 默认 true(:617-618)。
Remove on Break: (数据来自 FRemoveOnBreakData:打包进一个 FVector4f,含 Enabled、ClusterCrumbling、BreakTimer(min,max)、RemovalTimer(min,max);`CollectionRemoveOnBreakFacade.h:13-41`)
每个已脱离的块计时: BreakTimer += dt
t > PostBreakDuration(随机[min,max], 种子=变换数, 服务器/客户端一致) : Decay = clamp((t−PostBreak)/RemovalDuration, 0, 1)
Decay>=1 → ToDisable → PhysicsProxy.DisableParticles_External → DisableParticleWithRemovalEvent
「Cluster Crumbling」: RemovalDuration 设为 -1 的哨兵,Decay 一出现就改为 BreakClusters_External(该簇) —— 把残留簇碎成子块,让子块各自计时 (UpdateDecay :6636-6677)
Remove on Sleep: 仅对已脱离且(Sleeping 或 慢速<阈值)的块累计 SleepTimer ; 同样 Decay→Disable ; 簇块按 bAutomaticCrumblePartialClusters 先崩解大规模意义:回收「已经没意义」的碎片刚体。UpdateBrokenAndDecayedStates 只在 root 破后才开 Tick(Component.cpp:674-675, 4443-4468)。
12. 大规模场景最佳实践(均有源码依据)
- 控制层级深度与分支因子。每次释放要
FindIslandsInChildren(O(子+边)) 和对单块岛逐个RemoveParticlesFromCluster(内含Children.Find线性查找,:461),代价近似 O(被释放块数 × 父簇子数)。p.ReportTooManyChildrenNum就是官方的告警钩子(Proxy.cpp:1578-1582)。 - 用
MaxSimulatedLevel砍叶子:不需要的细碎层级直接不创建粒子(:3190-3221);用MaxClusterLevel让最后几层「永不破」(阈值FLT_MAX,:1952-1955)。 - 阈值逐级递减 + 按尺寸分档(默认
500000/50000/5000;bUseSizeSpecificDamageThreshold以相对Size选档)。 - 簇用少量凸包(
ConvexCount默认 4)而不是 LevelSet;LevelSet 簇分辨率 25~50 且依赖碰撞粒子,成本高;默认Volumetric+Convex无需 simplicial。 - 预算先于画质:
PerAdvanceBreaksAllowed+RescheduleLimit、ParticleReleaseThrottlingMin/MaxCount、OneWayInteractionLevel(碎渣不再致伤/不反推)、Remove on Break/Sleep + Cluster Crumbling。 - 应变过滤:装饰用 GC 关
bEnableDamageFromCollision;保持MinContactSpeedForStrainEval=1cm/s过滤静止堆叠接触(:2258-2271)。 - 图以离线为主:运行时
PointImplicit是 O(N²P),Delaunay 路径非线程安全;资产预烘焙 proximity→connection graph(带ContactArea)更稳(Proxy.cpp:1648-1730)。 - 用 Anchored 表达固定,不要用
ObjectType=Static/Kinematic把整个资产钉死(后者释放后不会自动变 Dynamic,§3.4 推断)。 - 睡眠起步:
StartAwake=false→ 只把顶层簇置 Sleeping(:1733-1739)。 - union 要成批操作:大量成员增删会触发几何/连接图重建;利用
Defer与HandleDeferredClusterUnionUpdateProperties(ClusterUnionManager.cpp:804-831)。 - 事件按需开:
bNotifyBreaks/Crumblings关闭时不产生数据(PBDRigidClustering.cpp:785-787, 812-814)。 - 监控:
p.Chaos.Clustering.DumpClusterAndReleaseStats=1看每帧「处理簇数/释放数」;p.Chaos.Solver.DebugDraw.Cluster.Constraints=1看图。 - 需要疲劳/累计伤害 → 写 StrainModifier(
StrainModification.h),而不是指望默认模型。
13. 设计取舍与疑点清单
| # | 现象 | 位置 | 备注 |
|---|---|---|---|
| 1 | 默认模型下 strain 比较的是冲量(kg·cm/s),不是力;材料模型才 ÷Dt | PBDRigidClustering.cpp:2353-2359 | 阈值与质量、碰撞速度同量级,调参需结合物体质量 |
| 2 | 每子块「吃满」冲量(不分摊),盒子半径 100cm;无 BVH 时全子块 | :2384-2403; ImplicitObjectUnion.cpp:588-592 | 小簇一次撞击可能整片破 |
| 3 | ExternalStrain 评估即清零,需要持续施加或设足够大 | :1247 | Field 脉冲只有一步生效 |
| 4 | 伤害传播每步一跳;Shock 不按边数均摊,Break 按边数均摊 | :1267-1282 | 速度/强度调参依据 |
| 5 | RescheduleLimit 过小会丢待处理簇;且未触顶时 LastProcessedIndex=0 会把 [1..] 重新登记一遍(多一次空评估) | :1841-1878 | 推断;默认 INT_MAX 无功能影响 |
| 6 | 岛簇速度未叠加 W×r | :1013-1016 | 推断 |
| 7 | MaxNumConnections 未执行 | :2692, 2697 | |
| 8 | 材料模型 Area 模式 2(min) 恒为 0→阈值 FLT_MAX | Proxy.cpp:1905, 1922-1923 | 推断 |
| 9 | p.GraphPropagationBased… CVar 无引用 | :63-67 | 遗留 |
| 10 | GT 连接图生成器 PointImplicitAugmentedWithMinimalDelaunay → check(false) | ConnectionGraphUtility.cpp:37-38 | |
| 11 | 内部簇碰撞比原簇贵(union 形状 + 无 simplicial) | PBDRigidClustering.cpp:648-659 | 破碎规模大时窄相主耗 |
| 12 | ReleaseClusterParticles(list) 假定列表同属一簇 | :1586-1609 | 源码注释 shouldn't be here |
| 13 | bOptimizeForRuntimeMemory 在 Source 内未接线 | §9 | 推断 |
| 14 | ClusterUnionStressSolver 无调用点 | ClusterUnionStressSolver.cpp | 推断:实验代码 |
| 15 | legacy bUsePerClusterOnlyDamageThreshold=true 时逐层阈值似乎不生效(见 §5.1 推断) | Proxy.cpp:1264, 1281-1296 | 推断,需实测 |
| 16 | GetSharedSimulationParams 补的 MaxSize=FLT_MAX 档,DamageThreshold 恒为 5000,不取资产值 | GeometryCollectionObject.cpp:534-566 | 超大相对尺寸才会命中 |
14. 演示页素材(可直接拆页)
页 A:一张图讲清「应变管线」碰撞冲量 / Field / 传播 → max() → >= InternalStrain(按层级) → 释放 → ExternalStrain=0。右侧挂 StrainModifier。
页 B:层级 = 懒激活树 画 3 层树:root(活跃)=1 个刚体;L1 子节点 disabled;撞击后 → 2~3 个 L1 块变活跃,其余变 1 个 internal cluster;L1 块落地 → 其下 L2 才被评估。标注阈值 500000 → 50000 → 5000。
页 C:连接图剪断 = 倒塌 同一座桥:①未破:全部 Kinematic(桥墩 Anchored);②切断桥面连接:左半仍连锚点→Kinematic;右半岛无锚点→新 internal cluster→Dynamic 整体坠落;③单块孤岛直接释放。
页 D:数据结构ClusterId / ChildToParent / Strains / ExternalStrains / CollisionImpulses / ConnectivityEdges / Flags(§2.1 表)。
页 E:连接方法对照 六种 EConnectionMethod 表(§4.1),强调默认 MinimalSpanningSubsetDelaunay 的「近距全连 + MST 补桥」与 500cm 常数。
页 F:一步里的时间线Solve → Sleep → AdvanceClustering(碰撞→应变→评估→释放→连通→岛簇) → ParticleUpdatePosition → Buffer→GT(§6、§9)。
页 G:预算与回收PerAdvanceBreaksAllowed、ReleaseThrottling、OneWayInteractionLevel、Remove on Break/Sleep/Crumbling、MaxSimulatedLevel。
页 H:Cluster Union GC A、GC B、任意刚体 → 一个 union 刚体;一一对应 shape;intercluster edge;破裂时按接触 shape → 子 GC → 孙子。
可演示的数字(源码给定):默认阈值 500000/50000/5000;ClusterDistanceThreshold=100cm;静止接触过滤 1cm/s;RestoreBreakingMomentum=50%;Delaunay 必连距离 500cm×√Factor;union BVH 起始 10 形状;LevelSet 簇分辨率 25~50;simplicial 上限 60/2000;GC 迭代 8/1/1。
