peclet-dem 1.1.0
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
shape_registry.hpp
Go to the documentation of this file.
1
8#ifndef DEM_SHAPE_REGISTRY_HPP
9#define DEM_SHAPE_REGISTRY_HPP
10
11#include <algorithm>
12#include <cmath>
13#include <cstddef>
14#include <Kokkos_Core.hpp>
15#include <stdexcept>
16#include <string>
17#include <vector>
18
19#include "narrowphase.hpp" // ShapeDesc, SHAPE_GRID_SDF / SHAPE_SCENE
20#include "particles.hpp"
21#include "peclet/core/geom/scene_builder.hpp"
22#include "shapes_portable.hpp" // genCylinderShell / genBoxShell
23#include "step_solve.hpp" // growContactBuffers
24
25namespace peclet::dem {
26
33 public:
34 void setSphereShape(float radius) { initializeShape(SPHERE, radius, 0.0f, 0.0f); }
35
36 // Mirror of CUDA Simulation::initialize(shape_type, radius, height, thickness): builds shape 0's
37 // descriptor + surface point shell (cylinder/box) and records the per-shape base radius and
38 // (uniform-mass=1) inverse inertia applied to every particle by setPositions. shape_type uses the
39 // peclet::dem::ShapeKind values (SPHERE=1, HOLLOW_CYLINDER=2, BOX=3).
40 //
41 // The registered baseRadius is the shape's CIRCUMSCRIBED (bounding) radius, the reach every
42 // reader of baseRadius_ / P.baseRadius / P.rad sizes by (broad-phase box, margin, ghost band,
43 // drift vote, Verlet/Hertz skin): sqrt(R^2 + (H/2)^2) for a hollow cylinder, sqrt(3) R for a
44 // box, R for a sphere (docs/contact_physics_followups.md F1, WO-B0). The geometric radius
45 // registered before missed every end and corner contact (coaxial tubes overlapping by 0.3 gave
46 // 0 contacts). The inertia below keeps the geometric `radius`.
47 void initializeShape(int shape_type, float radius, float height, float thickness) {
48 F4 params{radius, 0, 0, 0};
49 std::vector<F3> shell;
50 float bound = radius; // circumscribed radius (sphere: the radius itself)
51
52 if (shape_type == HOLLOW_CYLINDER) {
53 params = F4{radius, height, thickness, 0};
54 // Dynamic spacing (faithful to CUDA): >=4 pts across thickness, >=20 around circumference.
55 float min_dim = std::min(radius, thickness);
56 if (min_dim < 1e-4f)
57 min_dim = radius; // safety if thickness 0
58 float spacing = std::min(radius * 0.3f, min_dim * 0.5f);
59 if (spacing < 1e-3f)
60 spacing = 1e-3f;
61 shell = genCylinderShell(radius, height, thickness, spacing);
62 bound = std::sqrt(radius * radius + 0.25f * height * height);
63 } else if (shape_type == BOX) {
64 // Cube with half-extent = radius (side = 2*radius).
65 params = F4{radius, radius, radius, 0};
66 float spacing = std::max(radius * 0.5f, 1e-3f);
67 shell = genBoxShell(radius, radius, radius, spacing);
68 bound = std::sqrt(3.0f) * radius;
69 } else {
70 shape_type = SPHERE;
71 params = F4{radius, 0, 0, 0}; // sphere: analytic single-probe, no shell
72 }
73
74 const int nPts = static_cast<int>(shell.size());
75
76 // Per-shape inverse inertia (mass=1), faithful to CUDA Simulation::initialize.
77 float ix = 1.0f, iy = 1.0f, iz = 1.0f;
78 if (shape_type == SPHERE) {
79 if (radius > 0.0f) {
80 float v = 2.5f / (radius * radius);
81 ix = iy = iz = v;
82 }
83 } else if (shape_type == HOLLOW_CYLINDER) {
84 float r_out = radius, r_in = radius - thickness;
85 if (r_in < 0)
86 r_in = 0;
87 float term_r = r_out * r_out + r_in * r_in;
88 float I_zz = 0.5f * term_r;
89 float I_xx = (1.0f / 12.0f) * (3.0f * term_r + height * height);
90 if (I_xx > 1e-6f) {
91 ix = 1.0f / I_xx;
92 iy = 1.0f / I_xx;
93 }
94 if (I_zz > 1e-6f)
95 iz = 1.0f / I_zz;
96 } else if (shape_type == BOX) {
97 float L = 2.0f * radius;
98 float I = (1.0f / 6.0f) * L * L;
99 if (I > 1e-6f) {
100 ix = iy = iz = 1.0f / I;
101 }
102 }
103 // RESET the registry to this one shape -- initializeShape's documented behaviour is "shape 0
104 // becomes this". Use addShape() to build a mixture instead.
105 clearShapes();
106 ShapeDesc sd{shape_type, params, 0, nPts};
107 appendShape(sd, shell, F3{ix, iy, iz}, bound);
108 uploadShapes(); // sets baseRadius_ = P_.baseRadius = the registry's largest bounding radius
110 }
111
115 int addShape(int shape_type, float radius, float height, float thickness) {
116 const std::size_t before = shapesHost_.size();
117 if (before == 0)
118 throw std::runtime_error("addShape: call initialize_shape/set_sphere_shape first");
119 // Reuse initializeShape's descriptor + shell construction by running it into a scratch
120 // registry, then splice the result back on top of the existing shapes.
121 std::vector<ShapeDesc> keepS = shapesHost_;
122 std::vector<F3> keepShell = shellHost_;
123 std::vector<F3> keepInvI = invIHost_;
124 std::vector<float> keepBase = baseRadiusHost_;
125 std::vector<float> keepSamples = sdfSamplesHost_; // initializeShape clears these too
126 initializeShape(shape_type, radius, height, thickness); // leaves exactly one shape
127 ShapeDesc added = shapesHost_[0];
128 const std::vector<F3> addedShell = shellHost_;
129 const F3 addedInvI = invIHost_[0];
130 const float addedBase = baseRadiusHost_[0];
131 shapesHost_ = keepS;
132 shellHost_ = keepShell;
133 invIHost_ = keepInvI;
134 baseRadiusHost_ = keepBase;
135 sdfSamplesHost_ = keepSamples;
136 appendShape(added, addedShell, addedInvI, addedBase);
137 uploadShapes();
139 return static_cast<int>(shapesHost_.size()) - 1;
140 }
141
144 void setShapeIds(const std::vector<int>& ids) {
145 if (static_cast<int>(ids.size()) != P_.numReal)
146 throw std::runtime_error("set_shape_ids: expected one id per particle");
147 const int nShapes = static_cast<int>(shapesHost_.size());
148 for (int v : ids)
149 if (v < 0 || v >= nShapes)
150 throw std::runtime_error("set_shape_ids: shape id out of range");
151 auto sid = Kokkos::create_mirror_view(P_.shapeId);
152 auto ii = Kokkos::create_mirror_view(P_.invInertia);
153 Kokkos::deep_copy(sid, P_.shapeId);
154 Kokkos::deep_copy(ii, P_.invInertia);
155 for (int i = 0; i < P_.numReal; ++i) {
156 sid(i) = ids[i];
157 ii(i, 0) = invIHost_[ids[i]].x;
158 ii(i, 1) = invIHost_[ids[i]].y;
159 ii(i, 2) = invIHost_[ids[i]].z;
160 }
161 Kokkos::deep_copy(P_.shapeId, sid);
162 Kokkos::deep_copy(P_.invInertia, ii);
163 }
164
168 int addSdfShape(const std::vector<float>& grid, int nx, int ny, int nz, F3 origin, F3 spacing,
169 const std::vector<float>& shellFlat, F3 invInertia, float boundingRadius) {
170 if (shapesHost_.empty())
171 throw std::runtime_error("add_sdf_shape: call initialize_shape/set_sdf_shape first");
172 if (nx < 2 || ny < 2 || nz < 2)
173 throw std::runtime_error("add_sdf_shape: grid dims must be >= 2 on each axis");
174 if (static_cast<long>(nx) * ny * nz != static_cast<long>(grid.size()))
175 throw std::runtime_error("add_sdf_shape: grid.size() must equal nx*ny*nz");
176 const std::vector<float> shellUse =
177 shellFlat.empty() ? autoShell(grid, nx, ny, nz, origin, spacing, boundingRadius)
178 : shellFlat;
179 const int nPts = static_cast<int>(shellUse.size() / 3);
180 if (nPts <= 0)
181 throw std::runtime_error(
182 "add_sdf_shape: surface point shell is empty and could not be "
183 "generated from the field");
184 ShapeDesc sd{};
185 sd.type = SHAPE_GRID_SDF;
186 sd.params = F4{boundingRadius, 0, 0, 0};
187 sd.grid.nx = nx;
188 sd.grid.ny = ny;
189 sd.grid.nz = nz;
190 sd.grid.offset = static_cast<int>(sdfSamplesHost_.size()); // append to the shared pool
191 sd.grid.origin = toCoreVec(origin);
192 sd.grid.invSpacing = peclet::core::Vec3<float>{spacing.x > 0 ? 1.0f / spacing.x : 0.0f,
193 spacing.y > 0 ? 1.0f / spacing.y : 0.0f,
194 spacing.z > 0 ? 1.0f / spacing.z : 0.0f};
195 sd.grid.extension = peclet::core::geom::GridExtension::kObject;
196 sdfSamplesHost_.insert(sdfSamplesHost_.end(), grid.begin(), grid.end());
197 std::vector<F3> shellPts(nPts);
198 for (int i = 0; i < nPts; ++i)
199 shellPts[i] = F3{shellUse[3 * i], shellUse[3 * i + 1], shellUse[3 * i + 2]};
200 appendShape(sd, shellPts, invInertia, boundingRadius);
201 uploadShapes();
203 return static_cast<int>(shapesHost_.size()) - 1;
204 }
205
206 int numShapes() const { return static_cast<int>(shapesHost_.size()); }
207
208 // Import a general particle as a grid SDF (canonical, unit-scale space) + a surface point shell +
209 // its unit-mass principal-frame diagonal inverse inertia. Replaces shape 0, so every particle
210 // becomes an instance of this shape (per-particle position/quaternion/scale still apply). `grid`
211 // holds nx*ny*nz signed-distance samples, x-fastest (idx = x + y*nx + z*nx*ny), at lattice nodes
212 // q = origin + (x,y,z)*spacing (negative inside). `shellFlat` is the flat [nPts*3] surface point
213 // set the collision probes body A against. boundingRadius is the canonical radius enclosing the
214 // surface (broad-phase + VTI splat bound). invInertia is the per-unit-mass diagonal inverse
215 // inertia in the body (principal) frame; like the analytic shapes it becomes the default applied
216 // to every particle by setPositions (override afterwards with set_inv_inertia / set_inv_mass for
217 // a real density). See peclet.dem.particle_builder for the Python side that produces these arrays
218 // from an implicit-solid SDF (marching-cubes shell + voxel-integrated mass properties).
219 void setSdfShape(const std::vector<float>& grid, int nx, int ny, int nz, F3 origin, F3 spacing,
220 const std::vector<float>& shellFlat, F3 invInertia, float boundingRadius) {
221 if (nx < 2 || ny < 2 || nz < 2)
222 throw std::runtime_error("setSdfShape: grid dims must be >= 2 on each axis");
223 if (static_cast<long>(nx) * ny * nz != static_cast<long>(grid.size()))
224 throw std::runtime_error("setSdfShape: grid.size() must equal nx*ny*nz");
225 // An EMPTY shell means "generate one from the field itself" (Layer 1).
226 const std::vector<float> shellUse =
227 shellFlat.empty() ? autoShell(grid, nx, ny, nz, origin, spacing, boundingRadius)
228 : shellFlat;
229 const int nPts = static_cast<int>(shellUse.size() / 3);
230 if (nPts <= 0)
231 throw std::runtime_error(
232 "setSdfShape: surface point shell is empty and could not be "
233 "generated from the field");
234
235 // Upload the signed-distance samples.
236 P_.sdfGrid = Kokkos::View<float*, CpMem>("sdfGrid", grid.size());
237 auto hg = Kokkos::create_mirror_view(P_.sdfGrid);
238 for (size_t i = 0; i < grid.size(); ++i)
239 hg(i) = grid[i];
240 Kokkos::deep_copy(P_.sdfGrid, hg);
241
242 // Upload the surface point shell.
243 P_.shell = Kokkos::View<float* [3], CpMem>("shell", nPts);
244 auto hs = Kokkos::create_mirror_view(P_.shell);
245 for (int i = 0; i < nPts; ++i) {
246 hs(i, 0) = shellFlat[3 * i];
247 hs(i, 1) = shellFlat[3 * i + 1];
248 hs(i, 2) = shellFlat[3 * i + 2];
249 }
250 Kokkos::deep_copy(P_.shell, hs);
251
252 // Shape descriptor: grid SDF for the field, shell points for the probes.
253 ShapeDesc sd{};
254 sd.type = SHAPE_GRID_SDF;
255 sd.params = F4{boundingRadius, 0, 0, 0};
256 sd.shellOffset = 0;
257 sd.numPoints = nPts;
258 sd.grid.offset = 0;
259 sd.grid.nx = nx;
260 sd.grid.ny = ny;
261 sd.grid.nz = nz;
262 sd.grid.origin = toCoreVec(origin);
263 sd.grid.invSpacing = peclet::core::Vec3<float>{spacing.x > 0 ? 1.0f / spacing.x : 0.0f,
264 spacing.y > 0 ? 1.0f / spacing.y : 0.0f,
265 spacing.z > 0 ? 1.0f / spacing.z : 0.0f};
266 sd.grid.extension = peclet::core::geom::GridExtension::kObject; // a body
267
268 std::vector<F3> shellPts(nPts);
269 for (int i = 0; i < nPts; ++i)
270 shellPts[i] = F3{shellUse[3 * i], shellUse[3 * i + 1], shellUse[3 * i + 2]};
271 clearShapes();
272 sdfSamplesHost_ = grid; // this shape owns the whole pool when it is the only shape
273 sd.grid.offset = 0;
274 appendShape(sd, shellPts, invInertia, boundingRadius);
275 uploadShapes();
277 }
278
279 // Composed-analytic particle shape (SHAPE_SCENE): decode core's flat node encoding into the
280 // shared per-Simulation pool (child indices rebased to ABSOLUTE pool indices, exactly like
281 // addAnalyticWall), register a ShapeDesc whose tree pointer is patched at uploadShapes. The
282 // canonical frame is trusted to be the PRINCIPAL frame -- SceneBuilder.principal_frame emits
283 // that; passing a non-principal tree runs the diagonal-inertia rotational update on the wrong
284 // frame, silently, which is why the docstring shouts about it.
285 int addSceneShape(const std::vector<int>& nodeInts, const std::vector<float>& nodeReals, int root,
286 const std::vector<float>& shellFlat, F3 invInertia, float boundingRadius) {
287 namespace g = peclet::core::geom;
288 if (nodeInts.size() % g::kNodeIntStride || nodeReals.size() % g::kNodeRealStride)
289 throw std::runtime_error("add_scene_shape: node arrays are not a whole number of records");
290 const int n = static_cast<int>(nodeInts.size() / g::kNodeIntStride);
291 if (n == 0 || static_cast<int>(nodeReals.size() / g::kNodeRealStride) != n)
292 throw std::runtime_error("add_scene_shape: node int/real counts disagree");
293 if (root < 0 || root >= n)
294 throw std::runtime_error("add_scene_shape: root out of range");
295 const int base = static_cast<int>(shapeNodesHost_.size());
296 for (int i = 0; i < n; ++i) {
297 g::ShapeNode<float> nd = g::decodeNode<float>(&nodeInts[i * g::kNodeIntStride],
298 &nodeReals[i * g::kNodeRealStride]);
299 if (nd.kind == g::kGrid)
300 throw std::runtime_error(
301 "add_scene_shape: grid leaves are not supported in particle trees (use set_sdf_shape "
302 "for sampled shapes)");
303 if (nd.kind >= g::kCsgBase) {
304 nd.aux0 += base;
305 nd.aux1 += base;
306 }
307 shapeNodesHost_.push_back(nd);
308 }
309 std::vector<F3> shellPts;
310 if (shellFlat.size() % 3)
311 throw std::runtime_error("add_scene_shape: shell must be (M,3)");
312 shellPts.reserve(shellFlat.size() / 3);
313 for (std::size_t i = 0; i + 2 < shellFlat.size(); i += 3)
314 shellPts.push_back(F3{shellFlat[i], shellFlat[i + 1], shellFlat[i + 2]});
315 if (shellPts.empty())
316 throw std::runtime_error(
317 "add_scene_shape: an empty shell would make the body invisible to contacts -- bake the "
318 "tree and generate one (SceneBuilder.bake + the marching-cubes shell path)");
319 ShapeDesc sd{};
320 sd.type = SHAPE_SCENE;
321 sd.params = F4{boundingRadius, 0, 0, 0};
322 sd.nodeCount = 0; // patched at uploadShapes (whole-pool count; root is absolute)
323 sd.shapeRoot = base + root;
324 appendShape(sd, shellPts, invInertia, boundingRadius);
325 uploadShapes();
326 ensureContactCapacity(); // every other shape adder does this; its absence dropped contacts
327 return static_cast<int>(shapesHost_.size()) - 1;
328 }
329
334 std::vector<float> autoShell(const std::vector<float>& grid, int nx, int ny, int nz, F3 origin,
335 F3 spacing, float boundingRadius) const {
336 namespace g = peclet::core::geom;
337 g::SceneBuilder<float> b;
338 const int node = b.addGrid(
339 grid, nx, ny, nz, peclet::core::Vec3<float>{origin.x, origin.y, origin.z},
340 peclet::core::Vec3<float>{spacing.x, spacing.y, spacing.z}, g::GridExtension::kObject);
341 // Pitch: fine enough that the shell resolves the body, coarse enough not to explode the
342 // contact buffers -- ~1/12 of the bounding radius matches the density of the hand-written
343 // cylinder/box shells.
344 const float pitch = std::max(boundingRadius / 12.0f, 1e-4f);
345 const float r = boundingRadius * 1.05f;
346 const std::vector<peclet::core::Vec3<float>> pts =
347 g::surfacePoints<float>(b.view(), node, pitch, peclet::core::Vec3<float>{-r, -r, -r},
348 peclet::core::Vec3<float>{r, r, r});
349 std::vector<float> flat;
350 flat.reserve(pts.size() * 3);
351 for (const auto& q : pts) {
352 flat.push_back(q.x);
353 flat.push_back(q.y);
354 flat.push_back(q.z);
355 }
356 return flat;
357 }
358
359 protected:
360 // Size the contact/manifold buffers so no contact is dropped: a shell point sits inside at most
361 // one neighbour (body-body ~ capacity*shellPoints) plus one per wall it touches
362 // (capacity*shellPoints per wall). Boundary/wall contacts are appended AFTER body-body ones, so
363 // an undersized buffer silently drops them and grains tunnel through walls. Floored at the
364 // analytic default; grows only.
369
371 float baseRadius_ = 1.0f;
372 // --- host-side shape registry (Layer 1) -------------------------------------------------
373 // The device Views (P_.shapes / P_.shell / P_.sdfGrid) are rebuilt from these by uploadShapes(),
374 // which is what assigns each shape its shellOffset and grid offset into the shared pools. Keeping
375 // the authoritative copy on the host is what makes a MIXTURE of shapes possible at all: the old
376 // code wrote descriptor slot 0 directly and had nowhere to put a second shell.
377 std::vector<ShapeDesc> shapesHost_;
378 std::vector<F3> shellHost_; // concatenated shells; ShapeDesc::shellOffset indexes it
379 std::vector<F3> invIHost_; // per-shape unit-mass inverse inertia
380 std::vector<float> baseRadiusHost_; // per-shape canonical bounding radius
381 std::vector<float> sdfSamplesHost_; // concatenated grid-SDF samples
382
383 void clearShapes() {
384 shapesHost_.clear();
385 shellHost_.clear();
386 invIHost_.clear();
387 baseRadiusHost_.clear();
388 sdfSamplesHost_.clear();
389 }
390
393 void appendShape(ShapeDesc sd, const std::vector<F3>& shell, F3 invI, float baseRadius) {
394 sd.shellOffset = static_cast<int>(shellHost_.size());
395 sd.numPoints = static_cast<int>(shell.size());
396 shellHost_.insert(shellHost_.end(), shell.begin(), shell.end());
397 shapesHost_.push_back(sd);
398 invIHost_.push_back(invI);
399 baseRadiusHost_.push_back(baseRadius);
400 }
401
404 const int nShapes = static_cast<int>(shapesHost_.size());
405 if (nShapes == 0)
406 return;
407 // Composed-analytic node pool first, so the ShapeDesc pointers below can refer to it. Rebuilt
408 // whole (KBs); every SHAPE_SCENE descriptor points at the pool BASE with an absolute root, so
409 // one View serves every tree (the wallNodes pattern).
410 if (!shapeNodesHost_.empty()) {
411 P_.shapeNodes = Kokkos::View<peclet::core::geom::ShapeNode<float>*, CpMem>(
412 "shapeNodes", shapeNodesHost_.size());
413 auto hn = Kokkos::create_mirror_view(P_.shapeNodes);
414 for (std::size_t i = 0; i < shapeNodesHost_.size(); ++i)
415 hn(i) = shapeNodesHost_[i];
416 Kokkos::deep_copy(P_.shapeNodes, hn);
417 }
418 if (static_cast<int>(P_.shapes.extent(0)) < nShapes)
419 P_.shapes = Kokkos::View<ShapeDesc*, CpMem>("shapes", nShapes);
420 auto hs = Kokkos::create_mirror_view(P_.shapes);
421 for (int i = 0; i < nShapes; ++i) {
422 hs(i) = shapesHost_[i];
423 if (hs(i).type == SHAPE_SCENE) {
424 hs(i).nodes = P_.shapeNodes.data();
425 hs(i).nodeCount = static_cast<int>(P_.shapeNodes.extent(0));
426 }
427 }
428 Kokkos::deep_copy(P_.shapes, hs);
429
430 const int nPts = static_cast<int>(shellHost_.size());
431 if (nPts > 0) {
432 P_.shell = Kokkos::View<float* [3], CpMem>("shell", nPts);
433 auto hsh = Kokkos::create_mirror_view(P_.shell);
434 for (int i = 0; i < nPts; ++i) {
435 hsh(i, 0) = shellHost_[i].x;
436 hsh(i, 1) = shellHost_[i].y;
437 hsh(i, 2) = shellHost_[i].z;
438 }
439 Kokkos::deep_copy(P_.shell, hsh);
440 }
441
442 if (!sdfSamplesHost_.empty()) {
443 P_.sdfGrid = Kokkos::View<float*, CpMem>("sdfGrid", sdfSamplesHost_.size());
444 auto hg = Kokkos::create_mirror_view(P_.sdfGrid);
445 for (std::size_t i = 0; i < sdfSamplesHost_.size(); ++i)
446 hg(i) = sdfSamplesHost_[i];
447 Kokkos::deep_copy(P_.sdfGrid, hg);
448 }
449
450 // Broad-phase band and contact-buffer sizing must cover the LARGEST shape present, not the
451 // most recently added one.
452 float rmax = 0.0f;
453 int shellMax = 0;
454 for (int i = 0; i < nShapes; ++i) {
455 rmax = std::max(rmax, baseRadiusHost_[i]);
456 shellMax = std::max(shellMax, shapesHost_[i].numPoints);
457 }
458 baseRadius_ = rmax;
459 P_.baseRadius = rmax;
461 shellPoints_ = shellMax;
462 }
463
464 int shellPoints_ = 0; // LARGEST surface-shell size across shapes (contact-buffer sizing)
465 std::vector<peclet::core::geom::ShapeNode<float>> shapeNodesHost_; // SHAPE_SCENE pool
466 F3 defaultInvI_{2.5f, 2.5f, 2.5f};
467};
468
469} // namespace peclet::dem
470
471#endif // DEM_SHAPE_REGISTRY_HPP
Host-side shape registry (Layer 1).
void appendShape(ShapeDesc sd, const std::vector< F3 > &shell, F3 invI, float baseRadius)
Append one shape, taking ownership of its shell.
std::vector< float > baseRadiusHost_
void setSphereShape(float radius)
std::vector< ShapeDesc > shapesHost_
int addSdfShape(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, const std::vector< float > &shellFlat, F3 invInertia, float boundingRadius)
Append a grid-SDF shape (the general non-spherical particle) to the mixture and return its index.
void uploadShapes()
Rebuild the device Views from the host registry.
std::vector< peclet::core::geom::ShapeNode< float > > shapeNodesHost_
int addShape(int shape_type, float radius, float height, float thickness)
Append an analytic shape and return its index, for simulations with a MIXTURE of shapes.
int addSceneShape(const std::vector< int > &nodeInts, const std::vector< float > &nodeReals, int root, const std::vector< float > &shellFlat, F3 invInertia, float boundingRadius)
void setSdfShape(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, const std::vector< float > &shellFlat, F3 invInertia, float boundingRadius)
std::vector< float > autoShell(const std::vector< float > &grid, int nx, int ny, int nz, F3 origin, F3 spacing, float boundingRadius) const
Generate a collision shell for a grid SDF by sampling its own zero level set, so a caller does not ha...
std::vector< float > sdfSamplesHost_
void initializeShape(int shape_type, float radius, float height, float thickness)
void setShapeIds(const std::vector< int > &ids)
Per-particle shape assignment.
std::vector< F3 > genCylinderShell(float radius, float height, float thickness, float spacing)
peclet::core::Vec3< float > toCoreVec(F3 p)
F3 -> core's Real-templated point type. Layout-identical; this is a type change, not a copy.
CpExec::memory_space CpMem
void growContactBuffers(Particles &P, long nBodies, long floorWant=0)
Grow every maxContacts-sized collision buffer to match the CURRENT particle capacity.
std::vector< F3 > genBoxShell(float hx, float hy, float hz, float spacing)
dem — portable (Kokkos) narrow-phase: SDF point-shell collision + boundary planes.
dem — portable (Kokkos) particle SoA container: the storage the dem flip pivots on.
dem — portable (host) surface-shell point generators for the analytic shapes.
dem — the single-rank step drivers over the particle SoA: the XPBD substep (demStep),...
Kokkos::View< ShapeDesc *, CpMem > shapes
Kokkos::View< peclet::core::geom::ShapeNode< float > *, CpMem > shapeNodes
Kokkos::View< float *, CpMem > sdfGrid
Kokkos::View< float *[3], CpMem > shell
Portable mirror of ShapeDescriptor (analytic fields + a flat-array point shell).