MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
PhysicsOperator.hpp
Go to the documentation of this file.
1#pragma once
4
7
9
10/**
11 * @enum PhysicsParameter
12 * @brief Identifiers for physics parameters that can be set via parameter mapping
13 */
27
28/**
29 * @struct PhysicsState
30 * @brief Physics-specific data parallel to PointVertex array
31 *
32 * Stored separately to avoid polluting vertex types with
33 * physics data. Indexed in parallel with PointCollectionNode's
34 * internal vertex array.
35 */
37 glm::vec3 velocity { 0.0F };
38 glm::vec3 force { 0.0F };
39 float mass { 1.0F };
40};
41
42/**
43 * @class PhysicsOperator
44 * @brief N-body physics simulation with point rendering
45 *
46 * Delegates rendering to PointCollectionNode. Physics state
47 * (velocity, force, mass) stored in parallel array. Each frame:
48 * 1. Apply forces
49 * 2. Integrate motion
50 * 3. Update PointCollectionNode vertices
51 * 4. PointCollectionNode handles GPU upload
52 */
53class MAYAFLUX_API PhysicsOperator : public GraphicsOperator {
54public:
56 std::shared_ptr<GpuSync::PointCollectionNode> collection;
57 std::vector<PhysicsState> physics_state;
58 };
59
60 /**
61 * @enum BoundsMode
62 * @brief How particles behave at spatial bounds
63 */
64 enum class BoundsMode : uint8_t {
65 NONE, ///< No bounds checking
66 BOUNCE, ///< Reflect off boundaries with damping
67 WRAP, ///< Teleport to opposite side
68 CLAMP ///< Stop at boundary
69 };
70
72
73 ~PhysicsOperator() override { m_shutdown.store(true, std::memory_order_release); }
74
75 /**
76 * @brief Initialize with a single physics collection
77 * @param vertices PointVertex array with position, color, size
78 */
79 void initialize(const std::vector<PointVertex>& vertices);
80
81 /**
82 * @brief Initialize multiple physics collections
83 * @param collections Vector of PointVertex vectors (one per collection)
84 */
85 void initialize_collections(
86 const std::vector<std::vector<PointVertex>>& collections);
87
88 /**
89 * @brief Add a single physics collection
90 * @param vertices PointVertex array with position, color, size
91 * @param mass_multiplier Mass multiplier for all particles in this collection
92 */
93 void add_collection(
94 const std::vector<PointVertex>& vertices,
95 float mass_multiplier = 1.0F);
96
97 void process(float dt) override;
98
99 [[nodiscard]] std::span<const uint8_t> get_vertex_data_for_collection(uint32_t idx) const override;
100 [[nodiscard]] std::span<const uint8_t> get_vertex_data() const override;
101 [[nodiscard]] Kakshya::VertexLayout get_vertex_layout() const override;
102 [[nodiscard]] size_t get_vertex_count() const override;
103 [[nodiscard]] bool is_vertex_data_dirty() const override;
104 void mark_vertex_data_clean() override;
105
106 /**
107 * @brief Extract current vertex data as PointVertex array
108 * @return Vector of PointVertex with current positions, colors, sizes
109 */
110 [[nodiscard]] std::vector<PointVertex> extract_vertices() const;
111
112 void set_parameter(std::string_view param, double value) override;
113 [[nodiscard]] std::optional<double> query_state(std::string_view query) const override;
114 [[nodiscard]] std::string_view get_type_name() const override { return "Physics"; }
115 [[nodiscard]] size_t get_point_count() const override;
116
117 /**
118 * @brief Set the gravity vector.
119 * @param gravity Gravity vector.
120 */
121 void set_gravity(const glm::vec3& gravity) { m_gravity = gravity; }
122
123 /**
124 * @brief Set the drag coefficient.
125 * @param drag Drag value.
126 */
127 void set_drag(float drag) { m_drag = drag; }
128
129 /**
130 * @brief Set the interaction radius for physics calculations.
131 * @param radius Interaction radius.
132 */
133 void set_interaction_radius(float radius) { m_interaction_radius = radius; }
134
135 /**
136 * @brief Get the interaction radius for physics calculations.
137 * @return Interaction radius.
138 */
139 [[nodiscard]] float get_interaction_radius() const { return m_interaction_radius; }
140
141 /**
142 * @brief Set the spring stiffness for interactions.
143 * @param stiffness Spring stiffness value.
144 */
145 void set_spring_stiffness(float stiffness) { m_spring_stiffness = stiffness; }
146
147 /**
148 * @brief Set the simulation bounds.
149 * @param min Minimum bounds.
150 * @param max Maximum bounds.
151 */
152 void set_bounds(const glm::vec3& min, const glm::vec3& max);
153
154 /**
155 * @brief Set the rendered point size.
156 * @param size Point size.
157 */
158 void set_point_size(float size) { m_point_size = size; }
159
160 /**
161 * @brief Set the current bounds mode.
162 * @param mode Bounds mode to set.
163 */
164 void set_bounds_mode(BoundsMode mode) { m_bounds_mode = mode; }
165
166 /**
167 * @brief Enable or disable spatial interactions between particles.
168 * @param enable True to enable, false to disable.
169 */
170 void enable_spatial_interactions(bool enable) { m_spatial_interactions_enabled = enable; }
171
172 /**
173 * @brief Set the strength of repulsion between particles when spatial interactions are enabled.
174 * @param strength Repulsion strength value.
175 */
176 void set_repulsion_strength(float strength) { m_repulsion_strength = strength; }
177
178 /**
179 * @brief Set the strength of attraction towards the attraction point.
180 * @param strength Attraction strength value.
181 */
182 void set_attraction_strength(float strength) { m_attraction_strength = strength; }
183
184 /**
185 * @brief Set the strength of attraction towards the attraction point.
186 * @param strength Attraction strength value.
187 */
188 void set_turbulence_strength(float strength) { m_turbulence_strength = strength; }
189
190 /**
191 * @brief Get velocity magnitude for specific particle
192 * @param global_index Particle index across all collections
193 */
194 [[nodiscard]] std::optional<double> get_particle_velocity(size_t global_index) const;
195
196 /**
197 * @brief Get the current gravity vector.
198 * @return Gravity vector.
199 */
200 [[nodiscard]] glm::vec3 get_gravity() const { return m_gravity; }
201
202 /**
203 * @brief Get the current drag coefficient.
204 * @return Drag value.
205 */
206 [[nodiscard]] float get_drag() const { return m_drag; }
207
208 /**
209 * @brief Get the current bounds mode.
210 * @return Current bounds mode.
211 */
212 [[nodiscard]] BoundsMode get_bounds_mode() const { return m_bounds_mode; }
213
214 /**
215 * @brief Check if spatial interactions between particles are enabled.
216 * @return True if enabled, false otherwise.
217 */
218 [[nodiscard]] bool spatial_interactions_enabled() const { return m_spatial_interactions_enabled; }
219
220 /**
221 * @brief Get the current repulsion strength for spatial interactions.
222 * @return Repulsion strength value.
223 */
224 [[nodiscard]] float get_repulsion_strength() const { return m_repulsion_strength; }
225
226 void set_attraction_point(const glm::vec3& point);
227
228 void clear_attraction_point() { m_has_attraction_point = false; }
229
230 [[nodiscard]] bool has_attraction_point() const { return m_has_attraction_point; }
231
232 [[nodiscard]] glm::vec3 get_attraction_point() const { return m_attraction_point; }
233
234 /**
235 * @brief Apply impulse to all particles
236 */
237 void apply_global_impulse(const glm::vec3& impulse);
238
239 /**
240 * @brief Apply impulse to specific particle
241 */
242 void apply_impulse(size_t index, const glm::vec3& impulse);
243
244 /**
245 * @brief Add an external force field evaluated per-particle per-frame
246 * @param field VectorField: glm::vec3 (position) -> glm::vec3 (force)
247 *
248 * Fields are evaluated additively alongside existing hardcoded forces
249 * (gravity, attraction, turbulence, spatial interactions). Evaluated
250 * after gravity, before integration.
251 */
252 void add_force_field(Kinesis::VectorField field);
253
254 /**
255 * @brief Remove all external force fields
256 */
257 void clear_force_fields();
258
259 /**
260 * @brief Adopt this cycle's GPU claim/absorption clustering as bonds.
261 * @param claimed_by One entry per particle, global index order: entry i
262 * equals i if i is a cluster root, or the root's index if i was
263 * absorbed into it. Same shape as MutationClaimProcessor's
264 * mutation_claimed_by state field, since it is meant to be called
265 * directly with that field's readback.
266 *
267 * Bonded (non-root) particles are pulled toward their root each cycle by
268 * apply_bond_forces, a spring toward bond_rest_length rather than the
269 * root's own absorb_radius, so the CPU-simulated particle stays close to
270 * its cluster even on a cycle where the GPU claim graph (fully rebuilt
271 * from scratch every cycle, see ClaimInitProcessor) briefly finds no
272 * claim for it. Replaces any previously adopted bonds outright: there is
273 * no incremental merge, the whole table is the current cycle's snapshot.
274 *
275 * @note Entries in claimed_by are validated against m_accreted_mass's
276 * size before use as an index; an out-of-range entry is skipped
277 * rather than trusted, since claimed_by comes from a GPU readback.
278 */
279 void sync_bonds_from_claims(std::span<const uint32_t> claimed_by);
280
281 /**
282 * @brief Drop every adopted bond.
283 *
284 * Called when the claim graph reports nothing claimed this cycle
285 * (see ClaimAccumulateProcessor), so stale bonds from an earlier cycle
286 * don't keep pulling particles together after they've genuinely drifted
287 * apart.
288 */
289 void clear_bonds();
290
291 /**
292 * @brief Spring constant pulling a bonded particle toward its root.
293 * @param stiffness Force per unit distance from bond_rest_length.
294 */
295 void set_bond_stiffness(float stiffness) { m_bond_stiffness = stiffness; }
296 [[nodiscard]] float get_bond_stiffness() const { return m_bond_stiffness; }
297
298 /**
299 * @brief Distance a bond settles at.
300 * @param rest_length Below this distance the bond pushes apart (repel);
301 * above it, the bond pulls together (attract). 0 pulls a bonded
302 * particle fully onto its root, matching the GPU cosmetic swallow.
303 */
304 void set_bond_rest_length(float rest_length) { m_bond_rest_length = rest_length; }
305 [[nodiscard]] float get_bond_rest_length() const { return m_bond_rest_length; }
306
307 /** @brief Number of particles currently bonded to a root other than themselves. */
308 [[nodiscard]] size_t bond_count() const;
309
310 /**
311 * @brief Enable or disable adopting bonds from sync_bonds_from_claims.
312 * @param enable False also clears any bonds currently held, so the
313 * effect is immediate rather than waiting for the next zero-claim
314 * cycle.
315 *
316 * Purely a toggle for comparing bonded vs. unbonded behaviour live: the
317 * GPU claim/swallow cosmetic pass is unaffected either way, since it
318 * never reads this flag. Only apply_bond_forces (a real physics effect)
319 * is gated by it.
320 */
321 void enable_bonds(bool enable);
322
323 [[nodiscard]] bool bonds_enabled() const { return m_bonds_enabled; }
324
325 /**
326 * @brief This particle's currently accreted mass.
327 * @return 1.0 (the seed value every particle starts at) before bonds
328 * have ever synced, or if global_index is out of range.
329 *
330 * Populated by sync_bonds_from_claims, which is the only place mass
331 * moves: a satellite's mass transfers onto its current root every cycle
332 * it remains claimed and is never created or destroyed (see that
333 * method's own doc). Unlike ClaimAccumulateProcessor's GPU-side
334 * swallow_count, which ClaimInitProcessor wipes every cycle, this is a
335 * genuine permanent record of accumulated material, suitable for a
336 * growth effect that actually takes real time rather than saturating as
337 * soon as local density reaches steady state.
338 */
339 [[nodiscard]] float get_accreted_mass(size_t global_index) const;
340
341 /**
342 * @brief Every particle's currently accreted mass, in global index order.
343 * @return Empty if there are no particles yet; otherwise always
344 * get_point_count() elements, lazily seeded to 1.0 each on the
345 * first call, the same seeding sync_bonds_from_claims does, so a
346 * caller can upload this from cycle one without waiting for the
347 * first claim event to size it.
348 *
349 * Not const: may lazily initialise m_accreted_mass on first call.
350 */
351 [[nodiscard]] std::span<const float> get_accreted_mass_span();
352
353 /**
354 * @brief Sum of every particle's accreted mass.
355 * @return particle count before bonds have ever synced (each particle
356 * starts at mass 1.0), and exactly that same value forever
357 * after, since transfer neither creates nor destroys mass.
358 * Useful as a caller's own reference point for what fraction of
359 * the whole system a given body currently represents.
360 */
361 [[nodiscard]] float get_total_mass() const;
362
363 /**
364 * @brief Fixed dt substituted when set_force_internal_dt(true).
365 * @param dt Timestep in seconds. Default 0.016F.
366 */
367 void set_internal_dt(float dt) { m_internal_dt = dt; }
368 [[nodiscard]] float get_internal_dt() const { return m_internal_dt; }
369
370 /**
371 * @brief Get number of active external force fields
372 */
373 [[nodiscard]] size_t force_field_count() const { return m_force_fields.size(); }
374
375 /**
376 * @brief Direct access to collections for advanced per-particle control
377 * @warning Only for ParticleNetwork's ONE_TO_ONE parameter mapping
378 */
379 std::vector<CollectionGroup>& get_collections() { return m_collections; }
380
381 /**
382 * @brief Per-particle collection index, global index order.
383 * @return get_point_count() elements, entry i equal to the index of the
384 * CollectionGroup particle i belongs to in get_collections()
385 * order. All zero when there is at most one collection.
386 *
387 * The single source of truth for "which complete vertex-array pack does
388 * this particle belong to", consumed by any GPU-side stage that needs
389 * to respect collection boundaries in a neighbour query (the spatial
390 * hash's ClaimProcessor/HashDensityColorProcessor, and optionally
391 * GpuFieldOperator's own cluster-scoped bindings): each such consumer
392 * uploads this once, at wiring time, into whatever state field it
393 * declares for the purpose, rather than every consumer reaching into
394 * get_collections() and re-deriving the same prefix sum independently.
395 *
396 * Overrides GraphicsOperator::build_cluster_ids(), whose default (every
397 * entry 0) is exactly what this returns when there is at most one
398 * collection; the override only differs once a second one exists.
399 */
400 [[nodiscard]] std::vector<uint32_t> build_cluster_ids() const override;
401
402 /**
403 * @brief Adds "mass" (PhysicsState::mass) and "accreted_mass"
404 * (get_accreted_mass_span()) as named float attributes, in the
405 * same global vertex-index order get_vertex_data() produces.
406 *
407 * Cluster/collection membership is not repeated here: a consumer
408 * already gets that from build_cluster_ids(), which this class already
409 * overrides.
410 */
411 [[nodiscard]] std::vector<std::pair<std::string, Kakshya::DataVariant>>
412 extract_vertex_attributes() const override;
413
414 /**
415 * @brief Per-particle PhysicsState::velocity, in the same global
416 * vertex-index order get_vertex_data() produces.
417 */
418 [[nodiscard]] std::vector<glm::vec3> extract_vertex_velocities() const override;
419
420 /**
421 * @brief Forwards to the first collection's own get_primitive_topology().
422 * @return nullopt when there is no collection yet; otherwise
423 * m_collections[0].collection->get_primitive_topology()
424 * (PointCollectionNode's default, POINT_LIST). Same
425 * first-element choice get_vertex_layout() already makes for
426 * the aggregate.
427 */
428 [[nodiscard]] std::optional<Portal::Graphics::PrimitiveTopology> declared_topology() const override;
429
430 /**
431 * @brief Coalesces adjacent point collections sharing a concatenable
432 * topology into one run. Strip and fan collections are always
433 * reported separately.
434 */
435 std::vector<DrawRun> topology_runs() const override;
436
437 /**
438 * @brief Apply ONE_TO_ONE parameter for physics-specific properties
439 *
440 * Supports:
441 * - "force_x/y/z": Per-particle force application
442 * - "mass": Per-particle mass
443 * - "color": Per-particle color (delegated to base)
444 * - "size": Per-particle size (delegated to base)
445 */
446 void apply_one_to_one(
447 std::string_view param,
448 const std::shared_ptr<NodeNetwork>& source) override;
449
450 /**
451 * @brief Seed physics state from upstream operator's vertex data
452 *
453 * Extracts positions, colors, sizes from upstream and initializes
454 * physics state (velocity = 0, mass = 1) for each particle. Supports
455 * PointVertex input; other vertex types are ignored with a warning.
456 *
457 * @param upstream Upstream operator to seed from
458 */
459 void seed_from_upstream(const GraphicsOperator* upstream) override;
460
461 const char* get_vertex_type_name() const override { return "PointVertex"; }
462
463protected:
464 void* get_data_at(size_t global_index) override;
465
466private:
467 std::vector<CollectionGroup> m_collections;
468 mutable std::vector<uint8_t> m_vertex_data_aggregate;
469 std::vector<Kinesis::VectorField> m_force_fields;
470
472
473 glm::vec3 m_gravity { 0.0F, -9.81F, 0.0F };
474 float m_drag { 0.01F };
475 float m_interaction_radius { 1.0F };
476 float m_spring_stiffness { 0.5F };
477 float m_point_size { 5.0F };
478 float m_turbulence_strength { 0.0F };
479 Kinesis::SamplerBounds m_bounds { .min = glm::vec3 { -10.0F }, .max = glm::vec3 { 10.0F } };
480 BoundsMode m_bounds_mode { BoundsMode::BOUNCE };
481 bool m_spatial_interactions_enabled {};
482 float m_repulsion_strength { 0.5F };
483
484 glm::vec3 m_attraction_point { 0.0F };
485 bool m_has_attraction_point { false };
486 float m_attraction_strength { 1.0F };
487 float m_internal_dt { 0.016F };
488
489 std::vector<uint32_t> m_bond_root; ///< Empty when no bonds are adopted; see sync_bonds_from_claims.
490 float m_bond_stiffness { 2.0F };
491 float m_bond_rest_length { 0.0F };
492 bool m_bonds_enabled { true };
493
494 std::vector<float> m_accreted_mass; ///< Lazily seeded to 1.0 per particle; see sync_bonds_from_claims.
495
496 static std::optional<PhysicsParameter> string_to_parameter(std::string_view param);
497
498 void apply_forces();
499 void apply_spatial_interactions();
500 void apply_attraction_forces();
501 void apply_turbulence();
502 void apply_bond_forces();
503 void integrate(float dt);
504 void handle_boundary_conditions();
505 void sync_to_point_collection();
506
507 /**
508 * @struct GroupIndex
509 * @brief A global particle index resolved to its owning collection.
510 */
511 struct GroupIndex {
512 size_t group;
513 size_t local;
514 };
515
516 /** @brief Resolve a global index the same way apply_impulse/get_data_at do. */
517 [[nodiscard]] std::optional<GroupIndex> resolve_global_index(size_t global_index) const;
518
519 void apply_per_particle_force(
520 std::string_view param,
521 const std::shared_ptr<NodeNetwork>& source);
522
523 void apply_per_particle_mass(
524 const std::shared_ptr<NodeNetwork>& source);
525
526 mutable std::atomic<uint32_t> m_access_token { 0 };
527 std::atomic<bool> m_shutdown { false };
528};
529
530} // namespace MayaFlux::Nodes::Network
bool size
float radius
uint32_t mode
glm::vec3 value
uint32_t index
Definition VKDevice.cpp:122
Unified generative infrastructure for stochastic and procedural algorithms.
Operator that produces GPU-renderable geometry.
void set_repulsion_strength(float strength)
Set the strength of repulsion between particles when spatial interactions are enabled.
std::vector< CollectionGroup > m_collections
glm::vec3 get_gravity() const
Get the current gravity vector.
void set_turbulence_strength(float strength)
Set the strength of attraction towards the attraction point.
Kinesis::Stochastic::Stochastic m_random_generator
float get_drag() const
Get the current drag coefficient.
BoundsMode
How particles behave at spatial bounds.
void set_interaction_radius(float radius)
Set the interaction radius for physics calculations.
void set_drag(float drag)
Set the drag coefficient.
float get_interaction_radius() const
Get the interaction radius for physics calculations.
std::vector< Kinesis::VectorField > m_force_fields
std::vector< CollectionGroup > & get_collections()
Direct access to collections for advanced per-particle control.
void set_bounds_mode(BoundsMode mode)
Set the current bounds mode.
std::string_view get_type_name() const override
Type name for introspection.
size_t force_field_count() const
Get number of active external force fields.
void set_internal_dt(float dt)
Fixed dt substituted when set_force_internal_dt(true).
void set_gravity(const glm::vec3 &gravity)
Set the gravity vector.
std::vector< float > m_accreted_mass
Lazily seeded to 1.0 per particle; see sync_bonds_from_claims.
const char * get_vertex_type_name() const override
Get human-readable vertex type name (for validation/debugging)
void enable_spatial_interactions(bool enable)
Enable or disable spatial interactions between particles.
float get_repulsion_strength() const
Get the current repulsion strength for spatial interactions.
BoundsMode get_bounds_mode() const
Get the current bounds mode.
bool spatial_interactions_enabled() const
Check if spatial interactions between particles are enabled.
void set_bond_stiffness(float stiffness)
Spring constant pulling a bonded particle toward its root.
void set_point_size(float size)
Set the rendered point size.
void set_spring_stiffness(float stiffness)
Set the spring stiffness for interactions.
void set_bond_rest_length(float rest_length)
Distance a bond settles at.
void set_attraction_strength(float strength)
Set the strength of attraction towards the attraction point.
std::vector< uint32_t > m_bond_root
Empty when no bonds are adopted; see sync_bonds_from_claims.
N-body physics simulation with point rendering.
void initialize()
Definition main.cpp:10
PhysicsParameter
Identifiers for physics parameters that can be set via parameter mapping.
Complete description of vertex data layout in a buffer.
Spatial domain for vertex generation.
Typed, composable, stateless callable from domain D to range R.
Definition Tendency.hpp:22
std::shared_ptr< GpuSync::PointCollectionNode > collection
A global particle index resolved to its owning collection.
Physics-specific data parallel to PointVertex array.