Skip to content

Commit fbff57c

Browse files
committed
Test demo to validate the mesh type abstraction.
1 parent 1c8fcb8 commit fbff57c

1 file changed

Lines changed: 280 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
using System;
2+
using System.Numerics;
3+
using System.Runtime.CompilerServices;
4+
using BepuPhysics;
5+
using BepuPhysics.Collidables;
6+
using BepuPhysics.CollisionDetection;
7+
using BepuPhysics.CollisionDetection.CollisionTasks;
8+
using BepuPhysics.CollisionDetection.SweepTasks;
9+
using BepuPhysics.Constraints;
10+
using BepuPhysics.Trees;
11+
using BepuUtilities;
12+
using BepuUtilities.Collections;
13+
using BepuUtilities.Memory;
14+
using DemoContentLoader;
15+
using DemoRenderer;
16+
using DemoRenderer.UI;
17+
using DemoUtilities;
18+
19+
namespace Demos.SpecializedTests;
20+
21+
/// <summary>
22+
/// Pure forwarding wrapper around <see cref="Mesh"/>. Has its own TypeId so the narrow phase treats it as a distinct shape,
23+
/// which lets us verify that <see cref="MeshReduction"/>'s boundary smoothing works for any <see cref="IHomogeneousCompoundShape{Triangle, TriangleWide}">, not just the built-in Mesh type.
24+
/// </summary>
25+
public struct WrappedMesh : IHomogeneousCompoundShape<Triangle, TriangleWide>
26+
{
27+
public Mesh Inner;
28+
29+
public WrappedMesh(Mesh inner)
30+
{
31+
Inner = inner;
32+
}
33+
34+
public const int Id = 13;
35+
public static int TypeId => Id;
36+
37+
public readonly int ChildCount => Inner.ChildCount;
38+
39+
public static ShapeBatch CreateShapeBatch(BufferPool pool, int initialCapacity, Shapes shapeBatches)
40+
{
41+
return new HomogeneousCompoundShapeBatch<WrappedMesh, Triangle, TriangleWide>(pool, initialCapacity);
42+
}
43+
44+
public readonly void ComputeBounds(Quaternion orientation, out Vector3 min, out Vector3 max)
45+
{
46+
Inner.ComputeBounds(orientation, out min, out max);
47+
}
48+
49+
public readonly void GetLocalChild(int childIndex, out Triangle target)
50+
{
51+
Inner.GetLocalChild(childIndex, out target);
52+
}
53+
54+
public readonly void GetPosedLocalChild(int childIndex, out Triangle target, out RigidPose childPose)
55+
{
56+
Inner.GetPosedLocalChild(childIndex, out target, out childPose);
57+
}
58+
59+
public readonly void GetLocalChild(int childIndex, ref TriangleWide target)
60+
{
61+
Inner.GetLocalChild(childIndex, ref target);
62+
}
63+
64+
public readonly void RayTest<TRayHitHandler>(in RigidPose pose, in RayData ray, ref float maximumT, BufferPool pool, ref TRayHitHandler hitHandler)
65+
where TRayHitHandler : struct, IShapeRayHitHandler
66+
{
67+
Inner.RayTest(pose, ray, ref maximumT, pool, ref hitHandler);
68+
}
69+
70+
public readonly void RayTest<TRayHitHandler>(in RigidPose pose, ref RaySource rays, BufferPool pool, ref TRayHitHandler hitHandler)
71+
where TRayHitHandler : struct, IShapeRayHitHandler
72+
{
73+
Inner.RayTest(pose, ref rays, pool, ref hitHandler);
74+
}
75+
76+
public readonly unsafe void FindLocalOverlaps<TOverlaps, TSubpairOverlaps>(ref Buffer<OverlapQueryForPair> pairs, BufferPool pool, Shapes shapes, ref TOverlaps overlaps)
77+
where TOverlaps : struct, ICollisionTaskOverlaps<TSubpairOverlaps>
78+
where TSubpairOverlaps : struct, ICollisionTaskSubpairOverlaps
79+
{
80+
//Can't forward directly: the Mesh implementation reinterprets each pair.Container as Mesh*, but here the containers point to WrappedMesh instances.
81+
//Replicate the loop and forward each pair's AABB to the inner mesh's single-AABB overload instead.
82+
ShapeTreeOverlapEnumerator<TSubpairOverlaps> enumerator;
83+
enumerator.Pool = pool;
84+
for (int i = 0; i < pairs.Length; ++i)
85+
{
86+
ref var pair = ref pairs[i];
87+
ref var wrapped = ref Unsafe.AsRef<WrappedMesh>(pair.Container);
88+
enumerator.Overlaps = Unsafe.AsPointer(ref overlaps.GetOverlapsForPair(i));
89+
wrapped.Inner.FindLocalOverlaps(pair.Min, pair.Max, pool, shapes, ref enumerator);
90+
}
91+
}
92+
93+
public readonly unsafe void FindLocalOverlaps<TOverlaps>(Vector3 min, Vector3 max, Vector3 sweep, float maximumT, BufferPool pool, Shapes shapes, void* overlaps)
94+
where TOverlaps : ICollisionTaskSubpairOverlaps
95+
{
96+
Inner.FindLocalOverlaps<TOverlaps>(min, max, sweep, maximumT, pool, shapes, overlaps);
97+
}
98+
99+
public readonly void FindLocalOverlaps<TEnumerator>(Vector3 min, Vector3 max, BufferPool pool, Shapes shapes, ref TEnumerator enumerator)
100+
where TEnumerator : IBreakableForEach<int>
101+
{
102+
Inner.FindLocalOverlaps(min, max, pool, shapes, ref enumerator);
103+
}
104+
105+
public void Dispose(BufferPool pool)
106+
{
107+
Inner.Dispose(pool);
108+
}
109+
}
110+
111+
/// <summary>
112+
/// Drops convex shapes onto two WrappedMesh heightfields side by side. The fine mesh (many small triangles) forces MeshReduction into its
113+
/// dictionary-based high-subpair-count path; the coarse mesh (few large triangles) keeps subpair counts under the brute-force threshold.
114+
/// Between them the demo exercises every branch of <see cref="MeshReduction.ReduceManifolds"/> for a non-<see cref="Mesh"/>
115+
/// IHomogeneousCompoundShape so boundary smoothing can be validated on the type-erased path.
116+
/// </summary>
117+
public class CustomMeshSmoothingTestDemo : Demo
118+
{
119+
(StaticHandle Handle, Mesh InnerMesh)[] wrappedMeshes;
120+
121+
public override void Initialize(ContentArchive content, Camera camera)
122+
{
123+
camera.Position = new Vector3(0, 20, 60);
124+
camera.Yaw = 0;
125+
camera.Pitch = -0.3f;
126+
127+
Simulation = Simulation.Create(BufferPool, new DemoNarrowPhaseCallbacks(new SpringSettings(30, 1)), new DemoPoseIntegratorCallbacks(new Vector3(0, -10, 0)), new SolveDescription(8, 1));
128+
129+
//Register collision tasks for every convex shape we're going to drop against the WrappedMesh.
130+
//These are the same tasks DefaultTypes registers for Mesh, just closed over WrappedMesh so MeshReductionThunks<WrappedMesh> is used instead of MeshReductionThunks<Mesh>.
131+
var collisionTasks = Simulation.NarrowPhase.CollisionTaskRegistry;
132+
collisionTasks.Register(new ConvexCompoundCollisionTask<Sphere, WrappedMesh, ConvexCompoundOverlapFinder<Sphere, SphereWide, WrappedMesh>, ConvexMeshContinuations<WrappedMesh>, MeshReduction>());
133+
collisionTasks.Register(new ConvexCompoundCollisionTask<Capsule, WrappedMesh, ConvexCompoundOverlapFinder<Capsule, CapsuleWide, WrappedMesh>, ConvexMeshContinuations<WrappedMesh>, MeshReduction>());
134+
collisionTasks.Register(new ConvexCompoundCollisionTask<Box, WrappedMesh, ConvexCompoundOverlapFinder<Box, BoxWide, WrappedMesh>, ConvexMeshContinuations<WrappedMesh>, MeshReduction>());
135+
collisionTasks.Register(new ConvexCompoundCollisionTask<Triangle, WrappedMesh, ConvexCompoundOverlapFinder<Triangle, TriangleWide, WrappedMesh>, ConvexMeshContinuations<WrappedMesh>, MeshReduction>());
136+
collisionTasks.Register(new ConvexCompoundCollisionTask<Cylinder, WrappedMesh, ConvexCompoundOverlapFinder<Cylinder, CylinderWide, WrappedMesh>, ConvexMeshContinuations<WrappedMesh>, MeshReduction>());
137+
collisionTasks.Register(new ConvexCompoundCollisionTask<ConvexHull, WrappedMesh, ConvexCompoundOverlapFinder<ConvexHull, ConvexHullWide, WrappedMesh>, ConvexMeshContinuations<WrappedMesh>, MeshReduction>());
138+
139+
//Compound-vs-WrappedMesh uses a separate continuation type (CompoundMeshReduction), but it plugs into MeshReductionThunks<WrappedMesh> the same way.
140+
collisionTasks.Register(new CompoundPairCollisionTask<Compound, WrappedMesh, CompoundPairOverlapFinder<Compound, WrappedMesh>, CompoundMeshContinuations<Compound, WrappedMesh>, CompoundMeshReduction>());
141+
142+
//Sweep tasks matching the convex set, so swept queries keep working too.
143+
var sweepTasks = Simulation.NarrowPhase.SweepTaskRegistry;
144+
sweepTasks.Register(new ConvexHomogeneousCompoundSweepTask<Sphere, SphereWide, WrappedMesh, Triangle, TriangleWide, ConvexCompoundSweepOverlapFinder<Sphere, WrappedMesh>>());
145+
sweepTasks.Register(new ConvexHomogeneousCompoundSweepTask<Capsule, CapsuleWide, WrappedMesh, Triangle, TriangleWide, ConvexCompoundSweepOverlapFinder<Capsule, WrappedMesh>>());
146+
sweepTasks.Register(new ConvexHomogeneousCompoundSweepTask<Box, BoxWide, WrappedMesh, Triangle, TriangleWide, ConvexCompoundSweepOverlapFinder<Box, WrappedMesh>>());
147+
sweepTasks.Register(new ConvexHomogeneousCompoundSweepTask<Triangle, TriangleWide, WrappedMesh, Triangle, TriangleWide, ConvexCompoundSweepOverlapFinder<Triangle, WrappedMesh>>());
148+
sweepTasks.Register(new ConvexHomogeneousCompoundSweepTask<Cylinder, CylinderWide, WrappedMesh, Triangle, TriangleWide, ConvexCompoundSweepOverlapFinder<Cylinder, WrappedMesh>>());
149+
sweepTasks.Register(new ConvexHomogeneousCompoundSweepTask<ConvexHull, ConvexHullWide, WrappedMesh, Triangle, TriangleWide, ConvexCompoundSweepOverlapFinder<ConvexHull, WrappedMesh>>());
150+
sweepTasks.Register(new CompoundHomogeneousCompoundSweepTask<Compound, WrappedMesh, Triangle, TriangleWide, CompoundPairSweepOverlapFinder<Compound, WrappedMesh>>());
151+
152+
//Two meshes that share the same world-space terrain shape and footprint, but with wildly different tessellation density.
153+
//The fine mesh pushes subpair counts into the dictionary path; the coarse mesh keeps them in the brute-force path.
154+
wrappedMeshes = new (StaticHandle, Mesh)[2];
155+
var fineOrigin = Vector3.Zero;
156+
var coarseOrigin = new Vector3(0, 0, 160);
157+
AddWrappedTerrain(fineOrigin, planeWidth: 513, xzScale: 0.3f, out wrappedMeshes[0].Handle, out wrappedMeshes[0].InnerMesh);
158+
AddShapesAt(fineOrigin);
159+
AddWrappedTerrain(coarseOrigin, planeWidth: 33, xzScale: 4.8f, out wrappedMeshes[1].Handle, out wrappedMeshes[1].InnerMesh);
160+
AddShapesAt(coarseOrigin);
161+
}
162+
163+
void AddWrappedTerrain(Vector3 staticPosition, int planeWidth, float xzScale, out StaticHandle handle, out Mesh innerMesh)
164+
{
165+
//The noise is evaluated in mesh-local world space so both meshes end up with the same apparent terrain — only triangle density differs.
166+
Vector2 terrainOffset = new Vector2(1 - planeWidth, 1 - planeWidth) * 0.5f;
167+
var scale = new Vector3(xzScale, 0.1f, xzScale);
168+
innerMesh = DemoMeshHelper.CreateDeformedPlane(planeWidth, planeWidth,
169+
(int vX, int vY) =>
170+
{
171+
//vX and vY are vertex indices; multiply by scale after adding the centering offset to get a local-space position in world units.
172+
var localX = (vX + terrainOffset.X) * xzScale;
173+
var localZ = (vY + terrainOffset.Y) * xzScale;
174+
var octave0 = (MathF.Sin((localX + 5f) * 0.133f) + MathF.Sin((localZ + 11) * 0.133f)) * 0.9f;
175+
var octave1 = (MathF.Sin((localX + 17) * 0.367f) + MathF.Sin((localZ + 19) * 0.367f)) * 0.35f;
176+
var octave2 = (MathF.Sin((localX + 37) * 0.767f) + MathF.Sin((localZ + 93) * 0.767f)) * 0.15f;
177+
var terrainHeight = octave0 + octave1 + octave2;
178+
return new Vector3(vX + terrainOffset.X, terrainHeight, vY + terrainOffset.Y);
179+
}, scale, BufferPool);
180+
var wrapped = new WrappedMesh(innerMesh);
181+
handle = Simulation.Statics.Add(new StaticDescription(staticPosition, QuaternionEx.CreateFromAxisAngle(new Vector3(0, 1, 0), MathF.PI / 2), Simulation.Shapes.Add(wrapped)));
182+
}
183+
184+
void AddShapesAt(Vector3 center)
185+
{
186+
//Wide, shallow shapes maximize the number of triangle AABBs intersecting the convex AABB on the fine mesh; on the coarse mesh the same shapes
187+
//keep subpair counts well below MeshReduction's bruteForceThreshold of 128.
188+
189+
//1) Small box: fewer than 128 subpairs on either mesh.
190+
{
191+
var box = new Box(1.2f, 1.2f, 1.2f);
192+
var shape = Simulation.Shapes.Add(box);
193+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(-12, 4, 0), box.ComputeInertia(1), shape, 0.01f));
194+
}
195+
196+
//2) Medium box: ~300-500 subpairs on the fine mesh (dictionary path), a handful on the coarse mesh.
197+
{
198+
var box = new Box(5f, 0.6f, 5f);
199+
var shape = Simulation.Shapes.Add(box);
200+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(-4, 4, 0), box.ComputeInertia(1), shape, 0.01f));
201+
}
202+
203+
//3) Large box: ~800-1000 subpairs on the fine mesh, still close to the skip threshold.
204+
{
205+
var box = new Box(8f, 0.6f, 8f);
206+
var shape = Simulation.Shapes.Add(box);
207+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(6, 4, 0), box.ComputeInertia(1), shape, 0.01f));
208+
}
209+
210+
//4) Oversized box: intentionally exceeds the 1024-subpair skip threshold on the fine mesh to confirm the fall-through doesn't crash.
211+
{
212+
var box = new Box(14f, 0.6f, 14f);
213+
var shape = Simulation.Shapes.Add(box);
214+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(18, 4, 0), box.ComputeInertia(1), shape, 0.01f));
215+
}
216+
217+
//5) A few rounded shapes rolling across the bumpy surface. Boundary smoothing matters most when contacts straddle edges, so rollers are a good stress test.
218+
{
219+
var sphere = new Sphere(1.5f);
220+
var shape = Simulation.Shapes.Add(sphere);
221+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(-12, 6, 6), sphere.ComputeInertia(1), shape, 0.01f));
222+
223+
var cylinder = new Cylinder(2.5f, 1.5f);
224+
var cylinderShape = Simulation.Shapes.Add(cylinder);
225+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(-4, 6, 6), cylinder.ComputeInertia(1), cylinderShape, 0.01f));
226+
227+
var capsule = new Capsule(0.8f, 4f);
228+
var capsuleShape = Simulation.Shapes.Add(capsule);
229+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(6, 6, 6), capsule.ComputeInertia(1), capsuleShape, 0.01f));
230+
}
231+
232+
//6) A Compound of a few boxes. This routes through CompoundMeshContinuations / CompoundMeshReduction instead of the convex-only MeshReduction path,
233+
// but it still feeds MeshReductionThunks<WrappedMesh>, so it's the complementary check that compound-vs-wrapped-mesh boundary smoothing works too.
234+
{
235+
var builder = new CompoundBuilder(BufferPool, Simulation.Shapes, 3);
236+
builder.Add(new Box(3f, 0.5f, 3f), RigidPose.Identity, 1);
237+
builder.Add(new Box(1.5f, 1.5f, 1.5f), new RigidPose(new Vector3(0, 1f, 0)), 1);
238+
builder.Add(new Box(0.75f, 0.75f, 4f), new RigidPose(new Vector3(1.5f, 0.5f, 0)), 1);
239+
builder.BuildDynamicCompound(out var children, out var compoundInertia);
240+
builder.Dispose();
241+
var compound = new Compound(children);
242+
var shape = Simulation.Shapes.Add(compound);
243+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(14, 8, -6), compoundInertia, shape, 0.01f));
244+
}
245+
246+
//7) A wide, low convex hull. Hulls exercise a different convex-triangle tester than boxes, so including one catches regressions specific to hull-triangle manifolds.
247+
{
248+
const int hullPoints = 32;
249+
var points = new QuickList<Vector3>(hullPoints, BufferPool);
250+
var random = new Random(5);
251+
for (int i = 0; i < hullPoints; ++i)
252+
{
253+
var xz = new Vector2(random.NextSingle() * 2 - 1, random.NextSingle() * 2 - 1);
254+
//Flatten the hull so it covers a lot of ground when resting.
255+
points.AllocateUnsafely() = new Vector3(xz.X * 3f, (random.NextSingle() * 2 - 1) * 0.35f, xz.Y * 3f);
256+
}
257+
var hull = new ConvexHull(points.Span.Slice(points.Count), BufferPool, out _);
258+
var shape = Simulation.Shapes.Add(hull);
259+
Simulation.Bodies.Add(BodyDescription.CreateDynamic(center + new Vector3(-4, 8, -6), hull.ComputeInertia(1), shape, 0.01f));
260+
}
261+
}
262+
263+
public override void Render(Renderer renderer, Camera camera, Input input, TextBuilder text, Font font)
264+
{
265+
//The renderer's shape extractor switch doesn't know about WrappedMesh, so add each inner Mesh directly at its static's pose.
266+
//Using AddShape<Mesh> (rather than AddShape<WrappedMesh>) makes AddShape see Mesh.Id and routes to the existing mesh path.
267+
foreach (var (handle, innerMesh) in wrappedMeshes)
268+
{
269+
ref var pose = ref Simulation.Statics[handle].Pose;
270+
renderer.Shapes.AddShape(innerMesh, Simulation.Shapes, pose, new Vector3(0.7f, 0.7f, 0.75f));
271+
}
272+
273+
var resolution = renderer.Surface.Resolution;
274+
renderer.TextBatcher.Write(text.Clear().Append("Two WrappedMesh terrains: fine (near) and coarse (far, +Z). Identical shapes are dropped on each."), new Vector2(16, resolution.Y - 80), 16, Vector3.One, font);
275+
renderer.TextBatcher.Write(text.Clear().Append("Fine mesh pushes MeshReduction into its dictionary path; coarse mesh keeps everything in the brute-force path."), new Vector2(16, resolution.Y - 64), 16, Vector3.One, font);
276+
renderer.TextBatcher.Write(text.Clear().Append("Note: the largest box on the fine mesh overlaps more than 1024 triangles, so MeshReduction.ReduceManifolds early-outs"), new Vector2(16, resolution.Y - 40), 16, Vector3.One, font);
277+
renderer.TextBatcher.Write(text.Clear().Append("and no boundary smoothing is applied to it. Expect visible bumps there; the coarse-mesh counterpart still smooths."), new Vector2(16, resolution.Y - 24), 16, Vector3.One, font);
278+
base.Render(renderer, camera, input, text, font);
279+
}
280+
}

0 commit comments

Comments
 (0)