core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
scene_builder.hpp
Go to the documentation of this file.
1// core — host-side scene assembly + the flat encoding the Python bindings speak.
2//
3// Layer 0 rung 5 of suite/docs/ANALYTIC_SDF_GEOMETRY.md. Two things live here:
4//
5// SceneBuilder<Real> owns the node / grid / sample / instance arrays while a scene is being
6// composed, and hands out a non-owning SceneView over them. Host-only
7// (std::vector); device consumers copy the arrays into Views and build their
8// own SceneView over .data().
9//
10// THE FLAT ENCODING a fixed, documented layout of plain int and Real arrays. This is the
11// contract a nanobind layer implements: numpy arrays in, POD records out,
12// no per-field binding code and no reflection. Strides are compile-time
13// constants so a binding can size its arrays without guessing.
14//
15// FLAT NODE ENCODING — per node, kNodeIntStride ints and kNodeRealStride reals:
16// ints [0] kind (ShapeKind) [1] aux0 [2] aux1
17// reals [0..7] params[8]
18// [8..10] transform.translation (x, y, z)
19// [11..14] transform.rotation (x, y, z, w) -- dem's quaternion order, NOT w-first
20// [15] transform.scale (isotropic; contract 1)
21//
22// FLAT INSTANCE ENCODING — per instance, kInstanceIntStride ints and kInstanceRealStride reals:
23// ints [0] shapeRoot [1] materialId
24// reals [0..2] transform.translation [3..6] transform.rotation [7] transform.scale
25// [8..10] linVel [11..13] angVel [14..16] center
26//
27// GRIDS are encoded by their descriptor fields plus one shared sample pool; addGrid() appends to
28// the pool and returns the node index, so a caller never computes an offset by hand.
29//
30// C++20 host code (std::vector); the POD types it fills are the C++17-clean ones from scene.hpp.
31#ifndef PECLET_CORE_GEOM_SCENE_BUILDER_HPP
32#define PECLET_CORE_GEOM_SCENE_BUILDER_HPP
33
34#include <cmath>
35#include <cstddef>
36#include <initializer_list>
37#include <limits>
38#include <set>
39#include <stdexcept>
40#include <string>
41#include <tuple>
42#include <vector>
43
45
46namespace peclet::core::geom {
47
48inline constexpr int kNodeIntStride = 3;
49inline constexpr int kNodeRealStride = 16;
50inline constexpr int kInstanceIntStride = 2;
51inline constexpr int kInstanceRealStride = 18; // reals[17] = centerPinned flag (2026-09-02)
52inline constexpr int kInstanceRealStrideLegacy =
53 17; // pre-flag records: an all-zero centre = follows the body
54
55// --- flat <-> POD conversion (the binding boundary) --------------------------------------------
56
57template <class Real>
58void encodeNode(const ShapeNode<Real>& n, int* ints, Real* reals) {
59 ints[0] = n.kind;
60 ints[1] = n.aux0;
61 ints[2] = n.aux1;
62 for (int i = 0; i < 8; ++i)
63 reals[i] = n.params[i];
64 reals[8] = n.transform.translation.x;
65 reals[9] = n.transform.translation.y;
66 reals[10] = n.transform.translation.z;
67 reals[11] = n.transform.rotation.x;
68 reals[12] = n.transform.rotation.y;
69 reals[13] = n.transform.rotation.z;
70 reals[14] = n.transform.rotation.w;
71 reals[15] = n.transform.scale;
72}
73
74template <class Real>
77 n.kind = ints[0];
78 n.aux0 = ints[1];
79 n.aux1 = ints[2];
80 for (int i = 0; i < 8; ++i)
81 n.params[i] = reals[i];
82 n.transform.translation = Vec3<Real>{reals[8], reals[9], reals[10]};
83 n.transform.rotation = Quat<Real>{reals[11], reals[12], reals[13], reals[14]};
84 n.transform.scale = reals[15];
85 return n;
86}
87
88template <class Real>
90 ints[0] = v.shapeRoot;
91 ints[1] = v.materialId;
92 reals[0] = v.transform.translation.x;
93 reals[1] = v.transform.translation.y;
94 reals[2] = v.transform.translation.z;
95 reals[3] = v.transform.rotation.x;
96 reals[4] = v.transform.rotation.y;
97 reals[5] = v.transform.rotation.z;
98 reals[6] = v.transform.rotation.w;
99 reals[7] = v.transform.scale;
100 reals[8] = v.linVel.x;
101 reals[9] = v.linVel.y;
102 reals[10] = v.linVel.z;
103 reals[11] = v.angVel.x;
104 reals[12] = v.angVel.y;
105 reals[13] = v.angVel.z;
106 reals[14] = v.center.x;
107 reals[15] = v.center.y;
108 reals[16] = v.center.z;
109 reals[17] = v.centerPinned ? Real(1) : Real(0);
110}
111
112template <class Real>
116 v.shapeRoot = ints[0];
117 v.materialId = ints[1];
118 v.transform.translation = Vec3<Real>{reals[0], reals[1], reals[2]};
119 v.transform.rotation = Quat<Real>{reals[3], reals[4], reals[5], reals[6]};
120 v.transform.scale = reals[7];
121 v.linVel = Vec3<Real>{reals[8], reals[9], reals[10]};
122 v.angVel = Vec3<Real>{reals[11], reals[12], reals[13]};
123 v.center = Vec3<Real>{reals[14], reals[15], reals[16]};
124 // Legacy 17-real records carry no flag: a finite, non-zero centre was the only way to pin one,
125 // and an all-zero centre meant "follows the body" (every producer wrote zeros). Consumers keep
126 // that reading for legacy records; the 18-real record says it explicitly.
127 if (stride >= 18)
128 v.centerPinned = reals[17] > Real(0.5);
129 else
130 v.centerPinned = (v.center.x != 0 || v.center.y != 0 || v.center.z != 0) &&
131 std::isfinite(v.center.x) && std::isfinite(v.center.y) &&
132 std::isfinite(v.center.z);
133 return v;
134}
135
136// --- host assembly -----------------------------------------------------------------------------
137
145template <class Real>
147 public:
149 int addLeaf(int kind, std::initializer_list<Real> params, Transform<Real> tr = {}) {
150 if (kind >= kCsgBase)
151 throw std::invalid_argument("SceneBuilder::addLeaf: kind is a CSG operator, not a leaf");
152 if (params.size() > 8)
153 throw std::invalid_argument("SceneBuilder::addLeaf: at most 8 parameters");
155 n.kind = kind;
156 int i = 0;
157 for (Real v : params)
158 n.params[i++] = v;
159 n.transform = tr;
160 nodes_.push_back(n);
161 return static_cast<int>(nodes_.size()) - 1;
162 }
163
166 int addNode(const ShapeNode<Real>& n) {
167 if (n.kind >= kCsgBase)
168 throw std::invalid_argument("SceneBuilder::addNode: CSG kinds go through addUnion/...");
169 if (n.kind == kGrid)
170 throw std::invalid_argument("SceneBuilder::addNode: grid leaves go through addGrid");
171 nodes_.push_back(n);
172 return static_cast<int>(nodes_.size()) - 1;
173 }
174
177 int addGrid(const std::vector<float>& samples, int nx, int ny, int nz, Vec3<Real> origin,
179 if (nx < 2 || ny < 2 || nz < 2)
180 throw std::invalid_argument("SceneBuilder::addGrid: dims must be >= 2 on each axis");
181 if (static_cast<std::size_t>(nx) * ny * nz != samples.size())
182 throw std::invalid_argument("SceneBuilder::addGrid: samples.size() must equal nx*ny*nz");
184 d.nx = nx;
185 d.ny = ny;
186 d.nz = nz;
187 d.offset = static_cast<int>(pool_.size());
188 d.origin = origin;
189 d.invSpacing = Vec3<Real>{spacing.x != Real(0) ? Real(1) / spacing.x : Real(0),
190 spacing.y != Real(0) ? Real(1) / spacing.y : Real(0),
191 spacing.z != Real(0) ? Real(1) / spacing.z : Real(0)};
192 d.extension = ext;
193 pool_.insert(pool_.end(), samples.begin(), samples.end());
194 grids_.push_back(d);
196 n.kind = kGrid;
197 n.aux0 = static_cast<int>(grids_.size()) - 1;
198 n.transform = tr;
199 nodes_.push_back(n);
200 return static_cast<int>(nodes_.size()) - 1;
201 }
202
203 int addUnion(int a, int b, Transform<Real> tr = {}) { return addCsg(kUnion, a, b, tr); }
204 int addIntersection(int a, int b, Transform<Real> tr = {}) {
205 return addCsg(kIntersection, a, b, tr);
206 }
208 int addDifference(int a, int b, Transform<Real> tr = {}) { return addCsg(kDifference, a, b, tr); }
209
214 int addInstance(int shapeRoot, Transform<Real> tr = {}, Vec3<Real> linVel = {0, 0, 0},
215 Vec3<Real> angVel = {0, 0, 0},
216 Vec3<Real> center = {std::numeric_limits<Real>::quiet_NaN(),
217 std::numeric_limits<Real>::quiet_NaN(),
218 std::numeric_limits<Real>::quiet_NaN()},
219 int materialId = -1) {
220 requireNode(shapeRoot, "addInstance");
222 v.shapeRoot = shapeRoot;
223 v.materialId = materialId;
224 v.transform = tr;
225 v.linVel = linVel;
226 v.angVel = angVel;
227 v.center = center;
228 v.centerPinned = std::isfinite(center.x) && std::isfinite(center.y) && std::isfinite(center.z);
229 instances_.push_back(v);
230 return static_cast<int>(instances_.size()) - 1;
231 }
232
236 s.nodes = nodes_.data();
237 s.nodeCount = static_cast<int>(nodes_.size());
238 s.grids = grids_.data();
239 s.gridCount = static_cast<int>(grids_.size());
240 s.samples = pool_.data();
241 s.sampleCount = static_cast<long>(pool_.size());
242 s.instances = instances_.data();
243 s.instanceCount = static_cast<int>(instances_.size());
244 return s;
245 }
246
247 const std::vector<ShapeNode<Real>>& nodes() const { return nodes_; }
248 const std::vector<GridDesc<Real>>& grids() const { return grids_; }
249 const std::vector<float>& samples() const { return pool_; }
250 const std::vector<Instance<Real>>& instances() const { return instances_; }
251
257 C.rotation = mulQuat(A.rotation, B.rotation);
258 C.scale = A.scale * B.scale;
259 const Vec3<Real> rb =
260 rotate(A.rotation, Vec3<Real>{B.translation.x * A.scale, B.translation.y * A.scale,
261 B.translation.z * A.scale});
262 C.translation =
263 Vec3<Real>{A.translation.x + rb.x, A.translation.y + rb.y, A.translation.z + rb.z};
264 return C;
265 }
266
273 requireNode(root, "addReframed");
274 // copy the subtree (post-order via explicit stack), remapping child indices
275 std::vector<int> order;
276 {
277 std::vector<int> stack{root};
278 std::vector<char> seen(nodes_.size(), 0);
279 while (!stack.empty()) {
280 const int nidx = stack.back();
281 stack.pop_back();
282 if (nidx < 0 || seen[(std::size_t)nidx])
283 continue;
284 seen[(std::size_t)nidx] = 1;
285 order.push_back(nidx);
286 const ShapeNode<Real>& nd = nodes_[(std::size_t)nidx];
287 if (nd.kind >= kCsgBase) {
288 stack.push_back(nd.aux0);
289 stack.push_back(nd.aux1);
290 }
291 }
292 }
293 std::vector<int> remap(nodes_.size(), -1);
294 int newRoot = -1;
295 // copy in reverse discovery order so children exist before parents remap to them
296 for (auto it = order.rbegin(); it != order.rend(); ++it) {
297 ShapeNode<Real> nd = nodes_[(std::size_t)*it];
298 if (nd.kind >= kCsgBase) {
299 nd.aux0 = remap[(std::size_t)nd.aux0];
300 nd.aux1 = remap[(std::size_t)nd.aux1];
301 }
302 nodes_.push_back(nd);
303 remap[(std::size_t)*it] = static_cast<int>(nodes_.size()) - 1;
304 }
305 newRoot = remap[(std::size_t)root];
306 nodes_[(std::size_t)newRoot].transform =
307 composeTransform(W, nodes_[(std::size_t)newRoot].transform);
308 return newRoot;
309 }
310
316 if (i < 0 || i >= static_cast<int>(instances_.size()))
317 throw std::out_of_range("SceneBuilder::instanceRef: index out of range");
318 return instances_[static_cast<std::size_t>(i)];
319 }
320
321 // --- flat encoding out / in (the binding boundary) -------------------------------------------
322
323 void encode(std::vector<int>& nodeInts, std::vector<Real>& nodeReals, std::vector<int>& instInts,
324 std::vector<Real>& instReals) const {
325 nodeInts.resize(nodes_.size() * kNodeIntStride);
326 nodeReals.resize(nodes_.size() * kNodeRealStride);
327 for (std::size_t i = 0; i < nodes_.size(); ++i)
329 instInts.resize(instances_.size() * kInstanceIntStride);
330 instReals.resize(instances_.size() * kInstanceRealStride);
331 for (std::size_t i = 0; i < instances_.size(); ++i)
334 }
335
337 static SceneBuilder decode(const std::vector<int>& nodeInts, const std::vector<Real>& nodeReals,
338 const std::vector<int>& instInts, const std::vector<Real>& instReals,
339 const std::vector<GridDesc<Real>>& grids,
340 const std::vector<float>& pool) {
341 if (nodeInts.size() % kNodeIntStride || nodeReals.size() % kNodeRealStride)
342 throw std::invalid_argument(
343 "SceneBuilder::decode: node arrays are not a whole number of "
344 "records");
345 const std::size_t nn = nodeInts.size() / kNodeIntStride;
346 if (nodeReals.size() / kNodeRealStride != nn)
347 throw std::invalid_argument("SceneBuilder::decode: node int/real counts disagree");
348 SceneBuilder b;
349 b.grids_ = grids;
350 b.pool_ = pool;
351 b.nodes_.reserve(nn);
352 for (std::size_t i = 0; i < nn; ++i)
353 b.nodes_.push_back(
355 const std::size_t ni = instInts.size() / kInstanceIntStride;
356 // Accept both record widths: 18 (with the centre-pinned flag) and the legacy 17.
358 if (ni > 0) {
359 if (instReals.size() == ni * (std::size_t)kInstanceRealStride)
361 else if (instReals.size() == ni * (std::size_t)kInstanceRealStrideLegacy)
363 else
364 throw std::invalid_argument(
365 "SceneBuilder::decode: instance reals are neither 18 nor 17 per instance record");
366 }
367 b.instances_.reserve(ni);
368 for (std::size_t i = 0; i < ni; ++i)
369 b.instances_.push_back(decodeInstance<Real>(&instInts[i * kInstanceIntStride],
370 &instReals[i * (std::size_t)stride], stride));
371 return b;
372 }
373
374 private:
375 int addCsg(int kind, int a, int b, Transform<Real> tr) {
376 requireNode(a, "addCsg");
377 requireNode(b, "addCsg");
379 n.kind = kind;
380 n.aux0 = a;
381 n.aux1 = b;
382 n.transform = tr;
383 nodes_.push_back(n);
384 return static_cast<int>(nodes_.size()) - 1;
385 }
386 void requireNode(int i, const char* who) const {
387 if (i < 0 || i >= static_cast<int>(nodes_.size()))
388 throw std::invalid_argument(std::string("SceneBuilder::") + who +
389 ": node index out of range");
390 }
391
392 std::vector<ShapeNode<Real>> nodes_;
393 std::vector<GridDesc<Real>> grids_;
394 std::vector<float> pool_;
395 std::vector<Instance<Real>> instances_;
396};
397
414template <class Real>
415std::vector<Vec3<Real>> surfacePoints(const SceneView<Real>& sc, int root, Real spacing,
417 std::vector<Vec3<Real>> out;
418 if (spacing <= Real(0) || root < 0 || root >= sc.nodeCount)
419 return out;
420 const TablePtr<ShapeNode<Real>> nodes{sc.nodes};
421 const TablePtr<GridDesc<Real>> grids{sc.grids};
422 const PoolPtr<float> pool{sc.samples};
423 const auto f = [&](Vec3<Real> p) {
424 return evalTree<Real>(nodes, sc.nodeCount, root, p, grids, pool);
425 };
426 // Central-difference normal: generic, so CSG seams and sampled grids are handled too.
427 const Real h = spacing * Real(1e-3);
428 const auto n = [&](Vec3<Real> p) {
429 Vec3<Real> g{f(Vec3<Real>{p.x + h, p.y, p.z}) - f(Vec3<Real>{p.x - h, p.y, p.z}),
430 f(Vec3<Real>{p.x, p.y + h, p.z}) - f(Vec3<Real>{p.x, p.y - h, p.z}),
431 f(Vec3<Real>{p.x, p.y, p.z + h}) - f(Vec3<Real>{p.x, p.y, p.z - h})};
432 const Real l = std::sqrt(g.x * g.x + g.y * g.y + g.z * g.z);
433 return (l > Real(0)) ? Vec3<Real>{g.x / l, g.y / l, g.z / l} : Vec3<Real>{1, 0, 0};
434 };
435
436 // A lattice cell can touch the surface if |f| is within its half-diagonal.
437 const Real band = spacing * Real(0.87); // sqrt(3)/2, rounded up
438 const Real snap = spacing * Real(0.5);
439 std::set<std::tuple<long, long, long>> seen;
440 const int nx = static_cast<int>((hi.x - lo.x) / spacing) + 1;
441 const int ny = static_cast<int>((hi.y - lo.y) / spacing) + 1;
442 const int nz = static_cast<int>((hi.z - lo.z) / spacing) + 1;
443 for (int k = 0; k <= nz; ++k)
444 for (int j = 0; j <= ny; ++j)
445 for (int i = 0; i <= nx; ++i) {
446 Vec3<Real> p{lo.x + spacing * i, lo.y + spacing * j, lo.z + spacing * k};
447 if (std::fabs(f(p)) > band)
448 continue;
449 for (int it = 0; it < projectIters; ++it) {
450 const Real d = f(p);
451 const Vec3<Real> g = n(p);
452 p = Vec3<Real>{p.x - d * g.x, p.y - d * g.y, p.z - d * g.z};
453 }
454 if (std::fabs(f(p)) > spacing * Real(0.05))
455 continue; // failed to land on the surface (a bound-only leaf far from its zero set)
456 const std::tuple<long, long, long> key{static_cast<long>(std::llround(p.x / snap)),
457 static_cast<long>(std::llround(p.y / snap)),
458 static_cast<long>(std::llround(p.z / snap))};
459 if (seen.insert(key).second)
460 out.push_back(p);
461 }
462 return out;
463}
464
465} // namespace peclet::core::geom
466
467#endif // PECLET_CORE_GEOM_SCENE_BUILDER_HPP
Owns a scene under construction and hands out a non-owning SceneView over it.
const std::vector< ShapeNode< Real > > & nodes() const
int addUnion(int a, int b, Transform< Real > tr={})
const std::vector< Instance< Real > > & instances() const
Instance< Real > & instanceRef(int i)
Mutable access to one placed instance – how a MOVING scene advances: a driver updates the transform (...
int addNode(const ShapeNode< Real > &n)
Analytic leaf from a prebuilt node (the runtime-parameter sibling of addLeaf, for callers whose param...
SceneView< Real > view() const
Non-owning view over this builder's arrays. Invalidated by any further add*.
const std::vector< float > & samples() const
int addLeaf(int kind, std::initializer_list< Real > params, Transform< Real > tr={})
Analytic leaf. params follows the per-kind layout documented on ShapeKind.
int addIntersection(int a, int b, Transform< Real > tr={})
int addDifference(int a, int b, Transform< Real > tr={})
a MINUS b.
void encode(std::vector< int > &nodeInts, std::vector< Real > &nodeReals, std::vector< int > &instInts, std::vector< Real > &instReals) const
const std::vector< GridDesc< Real > > & grids() const
int addReframed(int root, const Transform< Real > &W)
Deep-copy the subtree rooted at root and pre-compose transform W onto the COPIED root,...
int addInstance(int shapeRoot, Transform< Real > tr={}, Vec3< Real > linVel={0, 0, 0}, Vec3< Real > angVel={0, 0, 0}, Vec3< Real > center={std::numeric_limits< Real >::quiet_NaN(), std::numeric_limits< Real >::quiet_NaN(), std::numeric_limits< Real >::quiet_NaN()}, int materialId=-1)
Place a tree in the world.
int addGrid(const std::vector< float > &samples, int nx, int ny, int nz, Vec3< Real > origin, Vec3< Real > spacing, GridExtension ext, Transform< Real > tr={})
Sampled field: copies samples into the shared pool, registers a descriptor, returns the node index.
static Transform< Real > composeTransform(const Transform< Real > &A, const Transform< Real > &B)
Compose two transforms so that toLocal(C, p) == toLocal(B, toLocal(A, p)) – i.e.
static SceneBuilder decode(const std::vector< int > &nodeInts, const std::vector< Real > &nodeReals, const std::vector< int > &instInts, const std::vector< Real > &instReals, const std::vector< GridDesc< Real > > &grids, const std::vector< float > &pool)
Rebuild from the flat encoding (grids and the sample pool are carried across verbatim).
constexpr int kInstanceRealStrideLegacy
constexpr int kInstanceRealStride
constexpr int kNodeIntStride
Instance< Real > decodeInstance(const int *ints, const Real *reals, int stride=kInstanceRealStride)
void encodeNode(const ShapeNode< Real > &n, int *ints, Real *reals)
@ kDifference
max(L, -R) — L minus R
Definition scene.hpp:50
@ kIntersection
max(L, R)
Definition scene.hpp:49
@ kGrid
sampled field; aux0 = index into the GridDesc table
Definition scene.hpp:36
@ kUnion
min(L, R) — aux0/aux1 = child node indices
Definition scene.hpp:48
void encodeInstance(const Instance< Real > &v, int *ints, Real *reals)
std::vector< Vec3< Real > > surfacePoints(const SceneView< Real > &sc, int root, Real spacing, Vec3< Real > lo, Vec3< Real > hi, int projectIters=4)
Sample points on the zero level set of a shape tree — the GENERAL surface-point generator.
ShapeNode< Real > decodeNode(const int *ints, const Real *reals)
GridExtension
NOT YET A NODE KIND: prim::Rounded (offset/rounding) stays a compile-time wrapper for now.
Definition scene.hpp:71
constexpr int kInstanceIntStride
constexpr int kNodeRealStride
PECLET_HD Quat< Real > mulQuat(Quat< Real > a, Quat< Real > b)
Hamilton product a ⊗ b in the (x, y, z, w) storage convention rotate uses, so rotate(mulQuat(a,...
Definition portable.hpp:96
PECLET_HD Vec3< Real > rotate(Quat< Real > q, Vec3< Real > v)
Rotate v by q.
Definition portable.hpp:81
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
double Real
Default host floating type. Device kernels may use float; conversions happen at the boundary.
Definition types.hpp:18
int materialId
consumer-defined; -1 = unset
Definition scene.hpp:201
bool centerPinned
true: center is an explicit world point (the origin included); false: the centre follows the body (co...
Definition scene.hpp:206
Vec3< Real > center
reference point of the rotation, in world coordinates
Definition scene.hpp:205
Transform< Real > transform
Definition scene.hpp:202
int shapeRoot
index of the root ShapeNode of this instance's tree
Definition scene.hpp:200
const Instance< Real > * instances
Definition scene.hpp:236
const GridDesc< Real > * grids
Definition scene.hpp:232
const ShapeNode< Real > * nodes
Definition scene.hpp:230
int aux0
CSG: left child index. kGrid: GridDesc table index.
Definition scene.hpp:103
Transform< Real > transform
Definition scene.hpp:106
int aux1
CSG: right child index.
Definition scene.hpp:104