peclet-dem
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
19namespace peclet::dem {
20
29struct ShapeDesc {
30 int type; // peclet::dem::ShapeKind
31 F4 params; // analytic parameters (see sdf_analytic); grid: params.x = bounding radius
32 int shellOffset; // start index into the flat shell-points View
33 int numPoints; // shell size; 0 => analytic single-probe (sphere center)
34 // --- grid-SDF fields (type == SHAPE_GRID_SDF); zero for analytic shapes ---
35 int gridOffset = 0; // start index into the shared sdfGrid View
36 int nx = 0, ny = 0, nz = 0; // lattice dimensions
37 F3 gridOrigin{0, 0, 0}; // canonical coord of node (0,0,0)
38 F3 gridInvSpacing{0, 0, 0}; // 1 / node spacing, per axis
39};
40
41struct PlaneP {
44};
45
59struct WallSdf {
60 // world-space grid SDF samples live in Particles::wallGrid at [gridOffset, gridOffset+nx*ny*nz),
61 // x-fastest (idx = x + y*nx + z*nx*ny), at nodes q = origin + (x,y,z)/invSpacing.
62 int nx = 0, ny = 0, nz = 0;
63 int gridOffset = 0;
64 F3 origin{0, 0, 0};
65 F3 invSpacing{0, 0, 0};
66 // rigid-body surface velocity field v(x) = linVel + angVel × (x − center) (set from the host).
67 F3 linVel{0, 0, 0};
68 F3 angVel{0, 0, 0};
69 F3 center{0, 0, 0};
70 // binary particle–wall material (independent of the body-body material).
71 float restitution = 0.0f;
72 float friction = 0.0f;
73};
74
75// Convenience aliases for the SoA particle views the narrow-phase reads.
76using PosView = Kokkos::View<const float* [3], CpMem>;
77using QuatView = Kokkos::View<const float* [4], CpMem>;
78using ScalarF = Kokkos::View<const float*, CpMem>;
79using ScalarI = Kokkos::View<const int*, CpMem>;
80using ShellView = Kokkos::View<const float* [3], CpMem>;
81using GridView = Kokkos::View<const float*, CpMem>;
82
83KOKKOS_INLINE_FUNCTION F3 loadF3(PosView v, int i) {
84 return F3{v(i, 0), v(i, 1), v(i, 2)};
85}
86KOKKOS_INLINE_FUNCTION F4 loadF4(QuatView v, int i) {
87 return F4{v(i, 0), v(i, 1), v(i, 2), v(i, 3)};
88}
89
96KOKKOS_INLINE_FUNCTION float sampleGridSdf(F3 p, const ShapeDesc& d, GridView grid) {
97 const float fx = (p.x - d.gridOrigin.x) * d.gridInvSpacing.x;
98 const float fy = (p.y - d.gridOrigin.y) * d.gridInvSpacing.y;
99 const float fz = (p.z - d.gridOrigin.z) * d.gridInvSpacing.z;
100 const float cx = Kokkos::fmin(Kokkos::fmax(fx, 0.0f), (float)(d.nx - 1));
101 const float cy = Kokkos::fmin(Kokkos::fmax(fy, 0.0f), (float)(d.ny - 1));
102 const float cz = Kokkos::fmin(Kokkos::fmax(fz, 0.0f), (float)(d.nz - 1));
103 const int ix = (int)cx, iy = (int)cy, iz = (int)cz;
104 const int ix1 = ix < d.nx - 1 ? ix + 1 : ix;
105 const int iy1 = iy < d.ny - 1 ? iy + 1 : iy;
106 const int iz1 = iz < d.nz - 1 ? iz + 1 : iz;
107 const float tx = cx - ix, ty = cy - iy, tz = cz - iz;
108 const long nxny = (long)d.nx * d.ny;
109 const int off = d.gridOffset;
110 auto at = [&](int x, int y, int z) { return grid(off + (long)z * nxny + (long)y * d.nx + x); };
111 const float c00 = at(ix, iy, iz) * (1 - tx) + at(ix1, iy, iz) * tx;
112 const float c10 = at(ix, iy1, iz) * (1 - tx) + at(ix1, iy1, iz) * tx;
113 const float c01 = at(ix, iy, iz1) * (1 - tx) + at(ix1, iy, iz1) * tx;
114 const float c11 = at(ix, iy1, iz1) * (1 - tx) + at(ix1, iy1, iz1) * tx;
115 const float c0 = c00 * (1 - ty) + c10 * ty;
116 const float c1 = c01 * (1 - ty) + c11 * ty;
117 const float val = c0 * (1 - tz) + c1 * tz;
118 // residual distance from p to the clamped lattice point (voxel units -> canonical units).
119 const float rx = (d.gridInvSpacing.x > 0.0f) ? (fx - cx) / d.gridInvSpacing.x : 0.0f;
120 const float ry = (d.gridInvSpacing.y > 0.0f) ? (fy - cy) / d.gridInvSpacing.y : 0.0f;
121 const float rz = (d.gridInvSpacing.z > 0.0f) ? (fz - cz) / d.gridInvSpacing.z : 0.0f;
122 return val + Kokkos::sqrt(rx * rx + ry * ry + rz * rz);
123}
124
128KOKKOS_INLINE_FUNCTION float sampleWallSdf(F3 p, const WallSdf& w, GridView grid) {
129 const float fx = (p.x - w.origin.x) * w.invSpacing.x;
130 const float fy = (p.y - w.origin.y) * w.invSpacing.y;
131 const float fz = (p.z - w.origin.z) * w.invSpacing.z;
132 const float cx = Kokkos::fmin(Kokkos::fmax(fx, 0.0f), (float)(w.nx - 1));
133 const float cy = Kokkos::fmin(Kokkos::fmax(fy, 0.0f), (float)(w.ny - 1));
134 const float cz = Kokkos::fmin(Kokkos::fmax(fz, 0.0f), (float)(w.nz - 1));
135 const int ix = (int)cx, iy = (int)cy, iz = (int)cz;
136 const int ix1 = ix < w.nx - 1 ? ix + 1 : ix;
137 const int iy1 = iy < w.ny - 1 ? iy + 1 : iy;
138 const int iz1 = iz < w.nz - 1 ? iz + 1 : iz;
139 const float tx = cx - ix, ty = cy - iy, tz = cz - iz;
140 const long nxny = (long)w.nx * w.ny;
141 const int off = w.gridOffset;
142 auto at = [&](int x, int y, int z) { return grid(off + (long)z * nxny + (long)y * w.nx + x); };
143 const float c00 = at(ix, iy, iz) * (1 - tx) + at(ix1, iy, iz) * tx;
144 const float c10 = at(ix, iy1, iz) * (1 - tx) + at(ix1, iy1, iz) * tx;
145 const float c01 = at(ix, iy, iz1) * (1 - tx) + at(ix1, iy, iz1) * tx;
146 const float c11 = at(ix, iy1, iz1) * (1 - tx) + at(ix1, iy1, iz1) * tx;
147 const float c0 = c00 * (1 - ty) + c10 * ty;
148 const float c1 = c01 * (1 - ty) + c11 * ty;
149 const float val = c0 * (1 - tz) + c1 * tz;
150 const float rx = (w.invSpacing.x > 0.0f) ? (fx - cx) / w.invSpacing.x : 0.0f;
151 const float ry = (w.invSpacing.y > 0.0f) ? (fy - cy) / w.invSpacing.y : 0.0f;
152 const float rz = (w.invSpacing.z > 0.0f) ? (fz - cz) / w.invSpacing.z : 0.0f;
153 return val + Kokkos::sqrt(rx * rx + ry * ry + rz * rz);
154}
155
158KOKKOS_INLINE_FUNCTION float sdfEvalShape(F3 p, const ShapeDesc& d, GridView grid) {
159 if (d.type == SHAPE_GRID_SDF)
160 return sampleGridSdf(p, d, grid);
161 return sdfEval(p, d.type, d.params);
162}
163
166inline void detectContactsKokkos(Kokkos::View<const int* [2], CpMem> pairs, int numPairs,
167 PosView pos, QuatView quat, ScalarF scale, ScalarI shapeId,
168 Kokkos::View<const ShapeDesc*, CpMem> shapes, ShellView shell,
169 float globalScale, float margin,
170 Kokkos::View<ContactC*, CpMem> outContacts,
171 Kokkos::View<int, CpMem> outCount,
172 Kokkos::View<float, CpMem> maxOverlap, GridView sdfGrid = GridView{}) {
173 CpExec space;
174 const int maxContacts = static_cast<int>(outContacts.extent(0));
175 Kokkos::parallel_for(
176 "peclet::dem::np::contacts", Kokkos::RangePolicy<CpExec>(space, 0, numPairs),
177 KOKKOS_LAMBDA(int idx) {
178 const int idA = pairs(idx, 0), idB = pairs(idx, 1);
179 const ShapeDesc dA = shapes(shapeId(idA));
180 const ShapeDesc dB = shapes(shapeId(idB));
181 const F3 posA = loadF3(pos, idA), posB = loadF3(pos, idB);
182 const F4 qA = loadF4(quat, idA), qB = loadF4(quat, idB);
183 // Canonical->world size = per-particle scale * global scale. globalScale must appear in the
184 // shell placement, the canonical remap of B, and the distance rescale exactly as it does in
185 // the sphere probe radius below — else a non-unit global_scale makes A's and B's radii
186 // disagree (grains a real diameter apart read a huge penetration and the solver explodes).
187 // The plane/wall boundary kernels already fold globalScale into their `s = scale*globalScale`.
188 const float effScaleA = scale(idA) * globalScale, effScaleB = scale(idB) * globalScale;
189
190 const int countA = dA.numPoints;
191 const bool sphereA = (dA.type == SPHERE);
192 const int iter = (countA > 0) ? countA : 1;
193
194 for (int k = 0; k < iter; ++k) {
195 F3 pLocalA{0, 0, 0};
196 float pointRadius = 0.0f;
197 if (countA > 0) {
198 const int s = dA.shellOffset + k;
199 pLocalA = F3{shell(s, 0), shell(s, 1), shell(s, 2)};
200 } else if (sphereA) {
201 pointRadius = dA.params.x * effScaleA;
202 }
203
204 const F3 pWorld = add3(posA, rotateVector(qA, scale3(pLocalA, effScaleA)));
205 const F3 pLocalB = invRotateVector(qB, sub3(pWorld, posB));
206 const F3 pCanB = scale3(pLocalB, 1.0f / effScaleB);
207
208 const float dist = sdfEvalShape(pCanB, dB, sdfGrid) * effScaleB;
209 const float effDist = dist - pointRadius;
210 if (effDist >= margin)
211 continue;
212
213 if (effDist < 0.0f)
214 Kokkos::atomic_max(&maxOverlap(), -effDist);
215 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
216 if (slot >= maxContacts) {
217 Kokkos::atomic_add(&outCount(), -1);
218 continue;
219 }
220
221 const float eps = 1e-4f;
222 F3 nLoc{sdfEvalShape(F3{pCanB.x + eps, pCanB.y, pCanB.z}, dB, sdfGrid) -
223 sdfEvalShape(F3{pCanB.x - eps, pCanB.y, pCanB.z}, dB, sdfGrid),
224 sdfEvalShape(F3{pCanB.x, pCanB.y + eps, pCanB.z}, dB, sdfGrid) -
225 sdfEvalShape(F3{pCanB.x, pCanB.y - eps, pCanB.z}, dB, sdfGrid),
226 sdfEvalShape(F3{pCanB.x, pCanB.y, pCanB.z + eps}, dB, sdfGrid) -
227 sdfEvalShape(F3{pCanB.x, pCanB.y, pCanB.z - eps}, dB, sdfGrid)};
228 const float len = len3(nLoc);
229 nLoc = (len > 1e-9f) ? scale3(nLoc, 1.0f / len) : F3{0, 1, 0};
230
231 const F3 nWorld = rotateVector(qB, nLoc);
232 const F3 pSurfA = sub3(pWorld, scale3(nWorld, pointRadius));
233 const F3 rA = sub3(pSurfA, posA);
234 const F3 rB = sub3(sub3(pSurfA, scale3(nWorld, effDist)), posB);
235
236 ContactC c{};
237 c.bodyA = idA;
238 c.bodyB = idB;
239 c.normal = F4{nWorld.x, nWorld.y, nWorld.z, 0.0f};
240 c.rA = F4{rA.x, rA.y, rA.z, 0.0f};
241 c.rB = F4{rB.x, rB.y, rB.z, 0.0f};
242 c.dist = effDist;
243 c.friction_lambda_n = 0.0f;
244 c.weight = 0.0f;
245 outContacts(slot) = c;
246 }
247 });
248 space.fence();
249}
250
259inline void detectWallSdfKokkos(int numReal, int numWalls, PosView pos, QuatView quat, ScalarF scale,
260 ScalarI shapeId, Kokkos::View<const ShapeDesc*, CpMem> shapes,
261 ShellView shell, Kokkos::View<const WallSdf*, CpMem> walls,
262 GridView wallGrid, float globalScale, float margin,
263 Kokkos::View<ContactC*, CpMem> outContacts,
264 Kokkos::View<int, CpMem> outCount,
265 Kokkos::View<float, CpMem> maxOverlap) {
266 CpExec space;
267 const int maxContacts = static_cast<int>(outContacts.extent(0));
268 Kokkos::parallel_for(
269 "peclet::dem::np::wallsdf", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
270 KOKKOS_LAMBDA(int i) {
271 const F3 posA = loadF3(pos, i);
272 const float s = scale(i) * globalScale;
273 const ShapeDesc d = shapes(shapeId(i));
274 float baseR = d.params.x;
275 if (baseR == 0.0f)
276 baseR = 1.0f;
277 const float radius = baseR * s; // analytic sphere radius (numPts == 0)
278 const int numPts = d.numPoints;
279 const F4 qA = loadF4(quat, i);
280 const float eps = 1e-4f;
281
282 for (int wi = 0; wi < numWalls; ++wi) {
283 const WallSdf w = walls(wi);
284 const int iter = (numPts > 0) ? numPts : 1;
285 for (int k = 0; k < iter; ++k) {
286 // World surface probe: a shell point (rotated + scaled) or, for a sphere, the centre.
287 F3 rA{0, 0, 0};
288 if (numPts > 0) {
289 const int si = d.shellOffset + k;
290 rA = rotateVector(qA, scale3(F3{shell(si, 0), shell(si, 1), shell(si, 2)}, s));
291 }
292 const F3 pw = add3(posA, rA);
293 const float sdf = sampleWallSdf(pw, w, wallGrid);
294 // Outward SDF gradient (central difference) = push-out normal (wall -> void).
295 F3 n{sampleWallSdf(F3{pw.x + eps, pw.y, pw.z}, w, wallGrid) -
296 sampleWallSdf(F3{pw.x - eps, pw.y, pw.z}, w, wallGrid),
297 sampleWallSdf(F3{pw.x, pw.y + eps, pw.z}, w, wallGrid) -
298 sampleWallSdf(F3{pw.x, pw.y - eps, pw.z}, w, wallGrid),
299 sampleWallSdf(F3{pw.x, pw.y, pw.z + eps}, w, wallGrid) -
300 sampleWallSdf(F3{pw.x, pw.y, pw.z - eps}, w, wallGrid)};
301 const float ln = len3(n);
302 n = (ln > 1e-9f) ? scale3(n, 1.0f / ln) : F3{0, 1, 0};
303
304 // For a sphere the nearest surface point is one radius toward the wall (−normal); its
305 // signed gap to the wall is sdf − radius.
306 const float dist = (numPts > 0) ? sdf : sdf - radius;
307 if (dist >= margin)
308 continue;
309 const F3 rAeff = (numPts > 0) ? rA : scale3(n, -radius);
310 const F3 pSurfA = add3(posA, rAeff); // particle surface point
311 const F3 pWall = sub3(pSurfA, scale3(n, dist)); // point on the wall along the normal
312
313 if (dist < 0.0f)
314 Kokkos::atomic_max(&maxOverlap(), -dist);
315 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
316 if (slot >= maxContacts) {
317 Kokkos::atomic_add(&outCount(), -1);
318 continue;
319 }
320 // Rigid-body wall surface velocity at the contact point: linVel + angVel × (r − center).
321 const F3 r = sub3(pWall, w.center);
322 const F3 vWall = add3(w.linVel, cross3v(w.angVel, r));
323
324 ContactC c{};
325 c.bodyA = i;
326 c.bodyB = -1;
327 c.normal = F4{n.x, n.y, n.z, 0.0f};
328 c.rA = F4{rAeff.x, rAeff.y, rAeff.z, 0.0f};
329 c.rB = F4{pWall.x, pWall.y, pWall.z, 0.0f};
330 c.dist = dist;
331 c.friction_lambda_n = 0.0f;
332 c.weight = 0.0f;
333 c.boundaryVel = F4{vWall.x, vWall.y, vWall.z, 0.0f};
334 c.boundaryRestitution = w.restitution;
335 c.boundaryFriction = w.friction;
336 outContacts(slot) = c;
337 }
338 }
339 });
340 space.fence();
341}
342
345inline void detectBoundaryKokkos(int numReal, int numPlanes, PosView pos, QuatView quat,
346 ScalarF scale, ScalarI shapeId,
347 Kokkos::View<const ShapeDesc*, CpMem> shapes, ShellView shell,
348 Kokkos::View<const PlaneP*, CpMem> planes, float globalScale,
349 float margin, Kokkos::View<ContactC*, CpMem> outContacts,
350 Kokkos::View<int, CpMem> outCount,
351 Kokkos::View<float, CpMem> maxOverlap) {
352 CpExec space;
353 const int maxContacts = static_cast<int>(outContacts.extent(0));
354 Kokkos::parallel_for(
355 "peclet::dem::np::boundary", Kokkos::RangePolicy<CpExec>(space, 0, numReal),
356 KOKKOS_LAMBDA(int i) {
357 const F3 posA = loadF3(pos, i);
358 const float s = scale(i) * globalScale;
359 const ShapeDesc d = shapes(shapeId(i));
360 float baseR = d.params.x;
361 if (baseR == 0.0f)
362 baseR = 1.0f;
363 const float radius = baseR * s;
364 const int numPts = d.numPoints;
365 const F4 qA = loadF4(quat, i);
366
367 for (int pi = 0; pi < numPlanes; ++pi) {
368 const PlaneP pl = planes(pi);
369 // emit helper, inlined (no lambda capture of mutable counters across backends).
370 if (numPts > 0) {
371 for (int k = 0; k < numPts; ++k) {
372 const int si = d.shellOffset + k;
373 const F3 rA =
374 rotateVector(qA, scale3(F3{shell(si, 0), shell(si, 1), shell(si, 2)}, s));
375 const F3 pwk = add3(posA, rA);
376 const float dist = dot3(sub3(pwk, pl.point), pl.normal);
377 if (dist >= margin)
378 continue;
379 if (dist < 0.0f)
380 Kokkos::atomic_max(&maxOverlap(), -dist);
381 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
382 if (slot >= maxContacts) {
383 Kokkos::atomic_add(&outCount(), -1);
384 continue;
385 }
386 ContactC c{};
387 c.bodyA = i;
388 c.bodyB = -1;
389 c.normal = F4{pl.normal.x, pl.normal.y, pl.normal.z, 0.0f};
390 c.rA = F4{rA.x, rA.y, rA.z, 0.0f};
391 c.rB = F4{pl.point.x, pl.point.y, pl.point.z, 0.0f};
392 c.dist = dist;
393 c.friction_lambda_n = 0.0f;
394 c.weight = 0.0f;
395 outContacts(slot) = c;
396 }
397 } else {
398 const float dist = dot3(sub3(posA, pl.point), pl.normal) - radius;
399 if (dist >= margin)
400 continue;
401 if (dist < 0.0f)
402 Kokkos::atomic_max(&maxOverlap(), -dist);
403 const int slot = Kokkos::atomic_fetch_add(&outCount(), 1);
404 if (slot >= maxContacts) {
405 Kokkos::atomic_add(&outCount(), -1);
406 continue;
407 }
408 const F3 rA = scale3(pl.normal, -radius);
409 ContactC c{};
410 c.bodyA = i;
411 c.bodyB = -1;
412 c.normal = F4{pl.normal.x, pl.normal.y, pl.normal.z, 0.0f};
413 c.rA = F4{rA.x, rA.y, rA.z, 0.0f};
414 c.rB = F4{pl.point.x, pl.point.y, pl.point.z, 0.0f};
415 c.dist = dist;
416 c.friction_lambda_n = 0.0f;
417 c.weight = 0.0f;
418 outContacts(slot) = c;
419 }
420 }
421 });
422 space.fence();
423}
424
425} // namespace peclet::dem
426
427#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.
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)
Per-real-particle contacts against a static world-space wall SDF set (a drum barrel,...
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
float sampleWallSdf(F3 p, const WallSdf &w, GridView grid)
Trilinearly sample a static world-space wall SDF at world point p (same "clamp + residual" extension ...
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{})
Pair point-shell vs SDF contacts.
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)
F3 sub3(F3 a, F3 b)
CpExec::memory_space CpMem
F4 loadF4(QuatView v, int i)
F3 scale3(F3 a, float s)
float len3(F3 v)
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
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).
Static, world-space SDF container/geometry the particles collide against (a drum barrel,...