peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
narrowphase.hpp
Go to the documentation of this file.
1
11#ifndef DEM_NARROWPHASE_HPP
12#define DEM_NARROWPHASE_HPP
13
14#include <Kokkos_Core.hpp>
15
16#include "contact_preprocessing.hpp" // ContactC, CpExec/CpMem
17#include "dem_portable.hpp"
18#include "peclet/core/geom/scene.hpp"
19
20namespace peclet::dem {
21
30struct ShapeDesc {
31 int type; // peclet::dem::ShapeKind
32 F4 params; // analytic parameters (see sdf_analytic); grid: params.x = bounding radius
33 int shellOffset; // start index into the flat shell-points View
34 int numPoints; // shell size; 0 => analytic single-probe (sphere center)
35 // --- grid-SDF descriptor (type == SHAPE_GRID_SDF); default/zero for analytic shapes ---
36 // Carries core's descriptor DIRECTLY rather than loose fields the sampler marshals per call.
37 // That is not cosmetic: building a GridDesc inside the sampler shifted nvcc's FMA contraction in
38 // the trilinear chain and moved 185/4000 CUDA results by ~1 ULP (Layer 0 rung 3). Passing a
39 // prebuilt descriptor through is bit-identical to the pre-port code on every backend.
40 peclet::core::geom::GridDesc<float> grid; // extension = kObject (a body)
41 // --- composed-analytic descriptor (type == SHAPE_SCENE); null/-1 for everything else ---
42 // A raw pointer into the Simulation's pooled device node View (same pattern as WallSdf.nodes:
43 // the owning View lives on the Simulation so ShapeDesc stays POD and device-copyable). Child
44 // indices are ABSOLUTE into the pool (rebased at add_scene_shape, like addAnalyticWall).
45 const peclet::core::geom::ShapeNode<float>* nodes = nullptr;
46 int nodeCount = 0;
47 int shapeRoot = -1;
48};
49
50struct PlaneP {
53};
54
70struct WallSdf {
71 // world-space grid SDF samples live in Particles::wallGrid at [offset, offset + nx*ny*nz),
72 // x-fastest (idx = x + y*nx + z*nx*ny), at nodes q = origin + (x,y,z)/invSpacing.
73 // extension = kContainer -- see the sign discussion on sampleWallSdf below.
74 peclet::core::geom::GridDesc<float> grid;
75 // ANALYTIC alternative (Layer 1): when shapeRoot >= 0 the wall is a core shape tree evaluated
76 // exactly, not a sampled field -- a stirrer becomes a CSG expression instead of a voxel grid,
77 // with no resolution limit and no per-rank replicated sample pool. Raw pointer (like
78 // core::geom::SceneView) so WallSdf stays POD and device-copyable with no signature churn at the
79 // call sites. `sign` is -1 for the usual case of a CONTAINER built from an object tree: core's
80 // leaves are negative-inside-solid, while a wall must read positive in the void where the grains
81 // live.
82 const peclet::core::geom::ShapeNode<float>* nodes = nullptr;
83 int nodeCount = 0;
84 int shapeRoot = -1;
85 float sign = 1.0f;
86 // rigid-body surface velocity field v(x) = linVel + angVel × (x − center) (set from the host).
87 F3 linVel{0, 0, 0};
88 F3 angVel{0, 0, 0};
89 F3 center{0, 0, 0};
90 // binary particle–wall material (independent of the body-body material).
91 float restitution = 0.0f;
92 float friction = 0.0f;
93 // Optional material id: >= 0 and a pair table set -> particle-wall (e, mu) comes from the
94 // pair table row (materialId(particle), materialId) instead of the binary values above.
95 int materialId = -1;
96};
97
99constexpr int kMaxMaterials = 8;
100using PairTableView = Kokkos::View<const float*, CpMem>;
101using MatIdView = Kokkos::View<const unsigned char*, CpMem>;
102
103// Convenience aliases for the SoA particle views the narrow-phase reads.
104using PosView = Kokkos::View<const float* [3], CpMem>;
105using QuatView = Kokkos::View<const float* [4], CpMem>;
106using ScalarF = Kokkos::View<const float*, CpMem>;
107using ScalarI = Kokkos::View<const int*, CpMem>;
108using ShellView = Kokkos::View<const float* [3], CpMem>;
109using GridView = Kokkos::View<const float*, CpMem>;
110
111KOKKOS_INLINE_FUNCTION F3 loadF3(PosView v, int i) {
112 return F3{v(i, 0), v(i, 1), v(i, 2)};
113}
114KOKKOS_INLINE_FUNCTION F4 loadF4(QuatView v, int i) {
115 return F4{v(i, 0), v(i, 1), v(i, 2), v(i, 3)};
116}
117
129KOKKOS_INLINE_FUNCTION float sampleGridSdf(F3 p, const ShapeDesc& d, GridView grid) {
130 return peclet::core::geom::sampleGrid(toCoreVec(p), d.grid, grid);
131}
132
143KOKKOS_INLINE_FUNCTION float sampleWallSdf(F3 p, const WallSdf& w, GridView grid) {
144 if (w.shapeRoot >= 0) // analytic wall: exact, no lattice
145 return w.sign * peclet::core::geom::evalTree<float>(
146 peclet::core::geom::TablePtr<peclet::core::geom::ShapeNode<float>>{w.nodes},
148 peclet::core::geom::TablePtr<peclet::core::geom::GridDesc<float>>{nullptr},
149 peclet::core::geom::PoolPtr<float>{nullptr});
150 return peclet::core::geom::sampleGrid(toCoreVec(p), w.grid, grid);
151}
152
155KOKKOS_INLINE_FUNCTION float sdfEvalShape(F3 p, const ShapeDesc& d, GridView grid) {
156 if (d.type == SHAPE_GRID_SDF)
157 return sampleGridSdf(p, d, grid);
158 if (d.type == SHAPE_SCENE) // composed analytic tree, exact in canonical body space
159 return peclet::core::geom::evalTree<float>(
160 peclet::core::geom::TablePtr<peclet::core::geom::ShapeNode<float>>{d.nodes}, d.nodeCount,
161 d.shapeRoot, toCoreVec(p),
162 peclet::core::geom::TablePtr<peclet::core::geom::GridDesc<float>>{nullptr},
163 peclet::core::geom::PoolPtr<float>{nullptr});
164 return sdfEval(p, d.type, d.params);
165}
166
175KOKKOS_INLINE_FUNCTION F3 sdfGradShape(F3 p, const ShapeDesc& d, GridView grid) {
176 namespace prim = peclet::core::geom::prim;
177 const peclet::core::Vec3<float> q = toCoreVec(p);
178 peclet::core::Vec3<float> g;
179 switch (d.type) {
180 case SPHERE:
181 g = prim::Sphere<float>{d.params.x}.grad(q);
182 break;
183 case BOX:
184 g = prim::Box<float>{d.params.x, d.params.y, d.params.z}.grad(q);
185 break;
186 case HOLLOW_CYLINDER:
187 g = prim::HollowCylinder<float>{d.params.x, d.params.y, d.params.z}.grad(q);
188 break;
189 case SHAPE_SCENE: { // composed tree: ANALYTIC gradient (core evalTreeGrad -- the chain rule
190 // costs one rotation per node; at CSG ridges this is the active face's
191 // EXACT normal where a central difference smears the two faces into an
192 // average that belongs to neither. Measured 3.2x faster than eval+FD too.)
193 peclet::core::Vec3<float> gt;
194 (void)peclet::core::geom::evalTreeGrad<float>(
195 peclet::core::geom::TablePtr<peclet::core::geom::ShapeNode<float>>{d.nodes}, d.nodeCount,
196 d.shapeRoot, toCoreVec(p),
197 peclet::core::geom::TablePtr<peclet::core::geom::GridDesc<float>>{nullptr},
198 peclet::core::geom::PoolPtr<float>{nullptr}, gt);
199 return F3{gt.x, gt.y, gt.z};
200 }
201 default: { // SHAPE_GRID_SDF: sampled field, central difference (fixed-eps caveat, Layer 1)
202 const float eps = 1e-4f;
203 return F3{sdfEvalShape(F3{p.x + eps, p.y, p.z}, d, grid) -
204 sdfEvalShape(F3{p.x - eps, p.y, p.z}, d, grid),
205 sdfEvalShape(F3{p.x, p.y + eps, p.z}, d, grid) -
206 sdfEvalShape(F3{p.x, p.y - eps, p.z}, d, grid),
207 sdfEvalShape(F3{p.x, p.y, p.z + eps}, d, grid) -
208 sdfEvalShape(F3{p.x, p.y, p.z - eps}, d, grid)};
209 }
210 }
211 return F3{g.x, g.y, g.z};
212}
213
235inline void detectContactsKokkos(Kokkos::View<const int* [2], CpMem> pairs, int numPairs,
236 PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId,
237 Kokkos::View<const ShapeDesc*, CpMem> shapes, ShellView shell,
238 float globalScale, float margin,
239 Kokkos::View<ContactC*, CpMem> outContacts,
240 Kokkos::View<int, CpMem> outCount,
241 Kokkos::View<float, CpMem> maxOverlap,
242 GridView sdfGrid = GridView{}, MatIdView matId = MatIdView{},
243 PairTableView pairTable = PairTableView{}, bool twoWay = false) {
244 CpExec space;
245 const int maxContacts = static_cast<int>(outContacts.extent(0));
246 Kokkos::parallel_for(
247 "peclet::dem::np::contacts", Kokkos::RangePolicy<CpExec>(space, 0, numPairs),
248 KOKKOS_LAMBDA(int idx) {
249 const int idA = pairs(idx, 0), idB = pairs(idx, 1);
250 const ShapeDesc dA = shapes(shapeId(idA));
251 const ShapeDesc dB = shapes(shapeId(idB));
252 const F3 posA = loadF3(pos, idA), posB = loadF3(pos, idB);
253 const F4 qA = loadF4(quat, idA), qB = loadF4(quat, idB);
254 // Canonical->world size = per-particle scale * global scale. globalScale must appear in the
255 // shell placement, the canonical remap of B, and the distance rescale exactly as it does in
256 // the sphere probe radius below — else a non-unit global_scale makes A's and B's radii
257 // disagree (grains a real diameter apart read a huge penetration and the solver explodes).
258 // The plane/wall boundary kernels already fold globalScale into their `s =
259 // scale*globalScale`.
260 const float effScaleA = scale(idA) * globalScale, effScaleB = scale(idB) * globalScale;
261
262 const int countA = dA.numPoints;
263 const bool sphereA = (dA.type == SPHERE);
264 const int iter = (countA > 0) ? countA : 1;
265 // Two-way only: B's probes against A's SDF. A shell A against a sphere B swaps to B's
266 // exact sphere probe (and drops A's forward probes below); a shell pair adds B's shell.
267 const int countB = dB.numPoints;
268 const bool sphereProbeB = twoWay && countA > 0 && countB == 0 && dB.type == SPHERE;
269 const bool shellProbeB = twoWay && countA > 0 && countB > 0;
270
271 for (int k = 0; k < (sphereProbeB ? 0 : iter); ++k) {
272 F3 pLocalA{0, 0, 0};
273 float pointRadius = 0.0f;
274 if (countA > 0) {
275 const int s = dA.shellOffset + k;
276 pLocalA = F3{shell(s, 0), shell(s, 1), shell(s, 2)};
277 } else if (sphereA) {
278 pointRadius = dA.params.x * effScaleA;
279 }
280
281 const F3 pWorld = add3(posA, rotateVector(qA, scale3(pLocalA, effScaleA)));
282 const F3 pLocalB = invRotateVector(qB, sub3(pWorld, posB));
283 const F3 pCanB = scale3(pLocalB, 1.0f / effScaleB);
284
285 const float dist = sdfEvalShape(pCanB, dB, sdfGrid) * effScaleB;
286 const float effDist = dist - pointRadius;
287 if (effDist >= margin)
288 continue;
289
290 if (effDist < 0.0f)
291 Kokkos::atomic_max(&maxOverlap(), -effDist);
292 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
293 if (slot >= maxContacts) {
294 Kokkos::atomic_add(&outCount(), -1);
295 continue;
296 }
297
298 F3 nLoc = sdfGradShape(pCanB, dB, sdfGrid);
299 const float len = len3(nLoc);
300 nLoc = (len > 1e-9f) ? scale3(nLoc, 1.0f / len) : F3{0, 1, 0};
301
302 const F3 nWorld = rotateVector(qB, nLoc);
303 const F3 pSurfA = sub3(pWorld, scale3(nWorld, pointRadius));
304 const F3 rA = sub3(pSurfA, posA);
305 const F3 rB = sub3(sub3(pSurfA, scale3(nWorld, effDist)), posB);
306
307 ContactC c{};
308 c.bodyA = idA;
309 c.bodyB = idB;
310 c.normal = F4{nWorld.x, nWorld.y, nWorld.z, 0.0f};
311 c.rA = F4{rA.x, rA.y, rA.z, 0.0f};
312 c.rB = F4{rB.x, rB.y, rB.z, 0.0f};
313 c.dist = effDist;
314 c.friction_lambda_n = 0.0f;
315 c.weight = 0.0f;
316 if (pairTable.extent(0) > 0) { // per-pair body-body material
317 const int t = (int(matId(idA)) * kMaxMaterials + int(matId(idB))) * 2;
318 c.boundaryRestitution = pairTable(t);
319 c.boundaryFriction = pairTable(t + 1);
320 }
321 outContacts(slot) = c;
322 }
323
324 // Reverse direction: B's probe points against A's SDF, emitted in the canonical (A, B)
325 // orientation. The geometry mirrors the forward block with the roles of A and B exchanged;
326 // only the emission differs (normal = -gradient of A, the surface points swap arms).
327 const int iterB = sphereProbeB ? 1 : (shellProbeB ? countB : 0);
328 for (int k = 0; k < iterB; ++k) {
329 F3 pLocalB{0, 0, 0};
330 float pointRadius = 0.0f;
331 if (shellProbeB) {
332 const int s = dB.shellOffset + k;
333 pLocalB = F3{shell(s, 0), shell(s, 1), shell(s, 2)};
334 } else {
335 pointRadius = dB.params.x * effScaleB;
336 }
337
338 const F3 pWorld = add3(posB, rotateVector(qB, scale3(pLocalB, effScaleB)));
339 const F3 pLocalA = invRotateVector(qA, sub3(pWorld, posA));
340 const F3 pCanA = scale3(pLocalA, 1.0f / effScaleA);
341
342 const float dist = sdfEvalShape(pCanA, dA, sdfGrid) * effScaleA;
343 const float effDist = dist - pointRadius;
344 if (effDist >= margin)
345 continue;
346
347 if (effDist < 0.0f)
348 Kokkos::atomic_max(&maxOverlap(), -effDist);
349 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
350 if (slot >= maxContacts) {
351 Kokkos::atomic_add(&outCount(), -1);
352 continue;
353 }
354
355 F3 mLoc = sdfGradShape(pCanA, dA, sdfGrid);
356 const float len = len3(mLoc);
357 mLoc = (len > 1e-9f) ? scale3(mLoc, 1.0f / len) : F3{0, 1, 0};
358
359 const F3 mWorld = rotateVector(qA, mLoc); // outward normal of A at B's probe
360 const F3 pSurfB = sub3(pWorld, scale3(mWorld, pointRadius)); // on B's surface
361 const F3 pSurfA = sub3(pSurfB, scale3(mWorld, effDist)); // on A's surface
362 const F3 rA = sub3(pSurfA, posA);
363 const F3 rB = sub3(pSurfB, posB);
364
365 // Canonical orientation: the normal is B's outward direction, -mWorld, so that
366 // pSurfA = pSurfB + normal * dist as for a forward contact. It is A's surface normal,
367 // so normal.w = 1 marks it (normalOfA): the position phase rotates it with A's spin.
368 ContactC c{};
369 c.bodyA = idA;
370 c.bodyB = idB;
371 c.normal = F4{-mWorld.x, -mWorld.y, -mWorld.z, 1.0f};
372 c.rA = F4{rA.x, rA.y, rA.z, 0.0f};
373 c.rB = F4{rB.x, rB.y, rB.z, 0.0f};
374 c.dist = effDist;
375 c.friction_lambda_n = 0.0f;
376 c.weight = 0.0f;
377 if (pairTable.extent(0) > 0) { // per-pair body-body material (the pair's own row)
378 const int t = (int(matId(idA)) * kMaxMaterials + int(matId(idB))) * 2;
379 c.boundaryRestitution = pairTable(t);
380 c.boundaryFriction = pairTable(t + 1);
381 }
382 outContacts(slot) = c;
383 }
384 });
385 space.fence();
386}
387
398 int numReal, int numWalls, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId,
399 Kokkos::View<const ShapeDesc*, CpMem> shapes, ShellView shell,
400 Kokkos::View<const WallSdf*, CpMem> walls, GridView wallGrid, float globalScale, float margin,
401 Kokkos::View<ContactC*, CpMem> outContacts, Kokkos::View<int, CpMem> outCount,
402 Kokkos::View<float, CpMem> maxOverlap, MatIdView matId = MatIdView{},
403 PairTableView pairTable = PairTableView{}, int wallIdBase = 0) {
404 CpExec space;
405 const int maxContacts = static_cast<int>(outContacts.extent(0));
406 Kokkos::parallel_for(
407 "peclet::dem::np::wallsdf", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
408 KOKKOS_LAMBDA(int i) {
409 const F3 posA = loadF3(pos, i);
410 const float s = scale(i) * globalScale;
411 const ShapeDesc d = shapes(shapeId(i));
412 float baseR = d.params.x;
413 if (baseR == 0.0f)
414 baseR = 1.0f;
415 const float radius = baseR * s; // analytic sphere radius (numPts == 0)
416 const int numPts = d.numPoints;
417 const F4 qA = loadF4(quat, i);
418 const float eps = 1e-4f;
419
420 for (int wi = 0; wi < numWalls; ++wi) {
421 const WallSdf w = walls(wi);
422 const int iter = (numPts > 0) ? numPts : 1;
423 for (int k = 0; k < iter; ++k) {
424 // World surface probe: a shell point (rotated + scaled) or, for a sphere, the centre.
425 F3 rA{0, 0, 0};
426 if (numPts > 0) {
427 const int si = d.shellOffset + k;
428 rA = rotateVector(qA, scale3(F3{shell(si, 0), shell(si, 1), shell(si, 2)}, s));
429 }
430 const F3 pw = add3(posA, rA);
431 const float sdf = sampleWallSdf(pw, w, wallGrid);
432 // Outward SDF gradient = push-out normal (wall -> void). ANALYTIC for a tree wall
433 // (evalTreeGrad; walls are CSG containers whose EDGES are exactly where the old
434 // central difference smeared the normal -- the deliberate numerics change of this
435 // commit, mirroring Layer 1's exact particle normals). Grid walls keep the FD.
436 F3 n;
437 if (w.shapeRoot >= 0) {
438 peclet::core::Vec3<float> gt;
439 (void)peclet::core::geom::evalTreeGrad<float>(
440 peclet::core::geom::TablePtr<peclet::core::geom::ShapeNode<float>>{w.nodes},
441 w.nodeCount, w.shapeRoot, toCoreVec(pw),
442 peclet::core::geom::TablePtr<peclet::core::geom::GridDesc<float>>{nullptr},
443 peclet::core::geom::PoolPtr<float>{nullptr}, gt);
444 n = F3{w.sign * gt.x, w.sign * gt.y, w.sign * gt.z};
445 } else {
446 n = F3{sampleWallSdf(F3{pw.x + eps, pw.y, pw.z}, w, wallGrid) -
447 sampleWallSdf(F3{pw.x - eps, pw.y, pw.z}, w, wallGrid),
448 sampleWallSdf(F3{pw.x, pw.y + eps, pw.z}, w, wallGrid) -
449 sampleWallSdf(F3{pw.x, pw.y - eps, pw.z}, w, wallGrid),
450 sampleWallSdf(F3{pw.x, pw.y, pw.z + eps}, w, wallGrid) -
451 sampleWallSdf(F3{pw.x, pw.y, pw.z - eps}, w, wallGrid)};
452 }
453 const float ln = len3(n);
454 n = (ln > 1e-9f) ? scale3(n, 1.0f / ln) : F3{0, 1, 0};
455
456 // For a sphere the nearest surface point is one radius toward the wall (−normal); its
457 // signed gap to the wall is sdf − radius.
458 const float dist = (numPts > 0) ? sdf : sdf - radius;
459 if (dist >= margin)
460 continue;
461 const F3 rAeff = (numPts > 0) ? rA : scale3(n, -radius);
462 const F3 pSurfA = add3(posA, rAeff); // particle surface point
463 const F3 pWall = sub3(pSurfA, scale3(n, dist)); // point on the wall along the normal
464
465 if (dist < 0.0f)
466 Kokkos::atomic_max(&maxOverlap(), -dist);
467 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
468 if (slot >= maxContacts) {
469 Kokkos::atomic_add(&outCount(), -1);
470 continue;
471 }
472 // Rigid-body wall surface velocity at the contact point: linVel + angVel × (r −
473 // center).
474 const F3 r = sub3(pWall, w.center);
475 const F3 vWall = add3(w.linVel, cross3v(w.angVel, r));
476
477 ContactC c{};
478 c.bodyA = i;
479 c.bodyB = wallContactId(wallIdBase + wi); // SDF walls follow the planes
480 c.normal = F4{n.x, n.y, n.z, 0.0f};
481 c.rA = F4{rAeff.x, rAeff.y, rAeff.z, 0.0f};
482 c.rB = F4{pWall.x, pWall.y, pWall.z, 0.0f};
483 c.dist = dist;
484 c.friction_lambda_n = 0.0f;
485 c.weight = 0.0f;
486 c.boundaryVel = F4{vWall.x, vWall.y, vWall.z, 0.0f};
487 if (w.materialId >= 0 && pairTable.extent(0) > 0) {
488 const int t = (int(matId(i)) * kMaxMaterials + w.materialId) * 2;
489 c.boundaryRestitution = pairTable(t);
490 c.boundaryFriction = pairTable(t + 1);
491 } else {
492 c.boundaryRestitution = w.restitution;
493 c.boundaryFriction = w.friction;
494 }
495 outContacts(slot) = c;
496 }
497 }
498 });
499 space.fence();
500}
501
505inline void detectBoundaryKokkos(int numReal, int numPlanes, PosView pos, QuatView quat,
506 ScalarF scale, ScalarI shapeId,
507 Kokkos::View<const ShapeDesc*, CpMem> shapes, ShellView shell,
508 Kokkos::View<const PlaneP*, CpMem> planes, float globalScale,
509 float margin, Kokkos::View<ContactC*, CpMem> outContacts,
510 Kokkos::View<int, CpMem> outCount,
511 Kokkos::View<float, CpMem> maxOverlap) {
512 CpExec space;
513 const int maxContacts = static_cast<int>(outContacts.extent(0));
514 Kokkos::parallel_for(
515 "peclet::dem::np::boundary", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
516 KOKKOS_LAMBDA(int i) {
517 const F3 posA = loadF3(pos, i);
518 const float s = scale(i) * globalScale;
519 const ShapeDesc d = shapes(shapeId(i));
520 float baseR = d.params.x;
521 if (baseR == 0.0f)
522 baseR = 1.0f;
523 const float radius = baseR * s;
524 const int numPts = d.numPoints;
525 const F4 qA = loadF4(quat, i);
526
527 for (int pi = 0; pi < numPlanes; ++pi) {
528 const PlaneP pl = planes(pi);
529 // emit helper, inlined (no lambda capture of mutable counters across backends).
530 if (numPts > 0) {
531 for (int k = 0; k < numPts; ++k) {
532 const int si = d.shellOffset + k;
533 const F3 rA =
534 rotateVector(qA, scale3(F3{shell(si, 0), shell(si, 1), shell(si, 2)}, s));
535 const F3 pwk = add3(posA, rA);
536 const float dist = dot3(sub3(pwk, pl.point), pl.normal);
537 if (dist >= margin)
538 continue;
539 if (dist < 0.0f)
540 Kokkos::atomic_max(&maxOverlap(), -dist);
541 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
542 if (slot >= maxContacts) {
543 Kokkos::atomic_add(&outCount(), -1);
544 continue;
545 }
546 ContactC c{};
547 c.bodyA = i;
548 c.bodyB = wallContactId(pi);
549 c.normal = F4{pl.normal.x, pl.normal.y, pl.normal.z, 0.0f};
550 c.rA = F4{rA.x, rA.y, rA.z, 0.0f};
551 c.rB = F4{pl.point.x, pl.point.y, pl.point.z, 0.0f};
552 c.dist = dist;
553 c.friction_lambda_n = 0.0f;
554 c.weight = 0.0f;
555 outContacts(slot) = c;
556 }
557 } else {
558 const float dist = dot3(sub3(posA, pl.point), pl.normal) - radius;
559 if (dist >= margin)
560 continue;
561 if (dist < 0.0f)
562 Kokkos::atomic_max(&maxOverlap(), -dist);
563 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
564 if (slot >= maxContacts) {
565 Kokkos::atomic_add(&outCount(), -1);
566 continue;
567 }
568 const F3 rA = scale3(pl.normal, -radius);
569 ContactC c{};
570 c.bodyA = i;
571 c.bodyB = wallContactId(pi);
572 c.normal = F4{pl.normal.x, pl.normal.y, pl.normal.z, 0.0f};
573 c.rA = F4{rA.x, rA.y, rA.z, 0.0f};
574 c.rB = F4{pl.point.x, pl.point.y, pl.point.z, 0.0f};
575 c.dist = dist;
576 c.friction_lambda_n = 0.0f;
577 c.weight = 0.0f;
578 outContacts(slot) = c;
579 }
580 }
581 });
582 space.fence();
583}
584
585} // namespace peclet::dem
586
587#endif // DEM_NARROWPHASE_HPP
dem — portable (Kokkos) contact->manifold reduction, replacing the thrust-based reduce_contacts_to_ma...
dem — portable POD types + math + analytic SDFs shared by the Kokkos kernel ports.
F3 sdfGradShape(F3 p, const ShapeDesc &d, GridView grid)
Outward gradient of a shape's SDF at canonical point p, NOT normalised (callers normalise,...
float sdfEval(F3 p, int type, F4 params)
Kokkos::View< const float *[3], CpMem > ShellView
F3 cross3v(F3 a, F3 b)
Kokkos::View< const float *, CpMem > GridView
constexpr int wallContactId(int wallIndex)
Wall identity of a boundary contact (docs/contact_solve_framework.md §12 S6): a contact with a wall c...
Kokkos::View< const unsigned char *, CpMem > MatIdView
float sampleWallSdf(F3 p, const WallSdf &w, GridView grid)
Trilinearly sample a static world-space wall SDF at world point p.
Kokkos::View< const float *, CpMem > PairTableView
F3 invRotateVector(F4 q, F3 v)
Kokkos::View< const int *, CpMem > ScalarI
void detectBoundaryKokkos(int numReal, int numPlanes, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, ShellView shell, Kokkos::View< const PlaneP *, CpMem > planes, float globalScale, float margin, Kokkos::View< ContactC *, CpMem > outContacts, Kokkos::View< int, CpMem > outCount, Kokkos::View< float, CpMem > maxOverlap)
Per-real-particle contacts against explicit planes (point-shell shapes test each surface point; analy...
F3 loadF3(PosView v, int i)
float sdfEvalShape(F3 p, const ShapeDesc &d, GridView grid)
Canonical-space SDF of a shape: analytic dispatch, or a trilinear grid sample for an imported grid SD...
F3 rotateVector(F4 q, F3 v)
float dot3(F3 a, F3 b)
peclet::core::Vec3< float > toCoreVec(F3 p)
F3 -> core's Real-templated point type. Layout-identical; this is a type change, not a copy.
F3 sub3(F3 a, F3 b)
CpExec::memory_space CpMem
void detectWallSdfKokkos(int numReal, int numWalls, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, ShellView shell, Kokkos::View< const WallSdf *, CpMem > walls, GridView wallGrid, float globalScale, float margin, Kokkos::View< ContactC *, CpMem > outContacts, Kokkos::View< int, CpMem > outCount, Kokkos::View< float, CpMem > maxOverlap, MatIdView matId=MatIdView{}, PairTableView pairTable=PairTableView{}, int wallIdBase=0)
Per-real-particle contacts against a static world-space wall SDF set (a drum barrel,...
F4 loadF4(QuatView v, int i)
F3 scale3(F3 a, float s)
float len3(F3 v)
void detectContactsKokkos(Kokkos::View< const int *[2], CpMem > pairs, int numPairs, PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId, Kokkos::View< const ShapeDesc *, CpMem > shapes, ShellView shell, float globalScale, float margin, Kokkos::View< ContactC *, CpMem > outContacts, Kokkos::View< int, CpMem > outCount, Kokkos::View< float, CpMem > maxOverlap, GridView sdfGrid=GridView{}, MatIdView matId=MatIdView{}, PairTableView pairTable=PairTableView{}, bool twoWay=false)
Pair point-shell vs SDF contacts.
F3 add3(F3 a, F3 b)
Kokkos::View< const float *[4], CpMem > QuatView
float sampleGridSdf(F3 p, const ShapeDesc &d, GridView grid)
Trilinearly sample an imported grid SDF at canonical point p.
Kokkos::View< const float *[3], CpMem > PosView
constexpr int kMaxMaterials
Pair-material lookup: flat [K*K*2] table, entry ((a*K + b)*2) = restitution, +1 = friction.
Kokkos::View< const float *, CpMem > ScalarF
Kokkos::DefaultExecutionSpace CpExec
Portable mirror of ParticleSystem.cuh ContactConstraint (the fields this reduction touches).
Portable mirror of ShapeDescriptor (analytic fields + a flat-array point shell).
const peclet::core::geom::ShapeNode< float > * nodes
peclet::core::geom::GridDesc< float > grid
Static, world-space SDF container/geometry the particles collide against (a drum barrel,...
const peclet::core::geom::ShapeNode< float > * nodes
peclet::core::geom::GridDesc< float > grid