MayaFlux 0.5.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 Apply ONE_TO_ONE parameter for physics-specific properties
404 *
405 * Supports:
406 * - "force_x/y/z": Per-particle force application
407 * - "mass": Per-particle mass
408 * - "color": Per-particle color (delegated to base)
409 * - "size": Per-particle size (delegated to base)
410 */
411 void apply_one_to_one(
412 std::string_view param,
413 const std::shared_ptr<NodeNetwork>& source) override;
414
415 /**
416 * @brief Seed physics state from upstream operator's vertex data
417 *
418 * Extracts positions, colors, sizes from upstream and initializes
419 * physics state (velocity = 0, mass = 1) for each particle. Supports
420 * PointVertex input; other vertex types are ignored with a warning.
421 *
422 * @param upstream Upstream operator to seed from
423 */
424 void seed_from_upstream(const GraphicsOperator* upstream) override;
425
426 const char* get_vertex_type_name() const override { return "PointVertex"; }
427
428protected:
429 void* get_data_at(size_t global_index) override;
430
431private:
432 std::vector<CollectionGroup> m_collections;
433 mutable std::vector<uint8_t> m_vertex_data_aggregate;
434 std::vector<Kinesis::VectorField> m_force_fields;
435
437
438 glm::vec3 m_gravity { 0.0F, -9.81F, 0.0F };
439 float m_drag { 0.01F };
440 float m_interaction_radius { 1.0F };
441 float m_spring_stiffness { 0.5F };
442 float m_point_size { 5.0F };
443 float m_turbulence_strength { 0.0F };
444 Kinesis::SamplerBounds m_bounds { .min = glm::vec3 { -10.0F }, .max = glm::vec3 { 10.0F } };
445 BoundsMode m_bounds_mode { BoundsMode::BOUNCE };
446 bool m_spatial_interactions_enabled {};
447 float m_repulsion_strength { 0.5F };
448
449 glm::vec3 m_attraction_point { 0.0F };
450 bool m_has_attraction_point { false };
451 float m_attraction_strength { 1.0F };
452 float m_internal_dt { 0.016F };
453
454 std::vector<uint32_t> m_bond_root; ///< Empty when no bonds are adopted; see sync_bonds_from_claims.
455 float m_bond_stiffness { 2.0F };
456 float m_bond_rest_length { 0.0F };
457 bool m_bonds_enabled { true };
458
459 std::vector<float> m_accreted_mass; ///< Lazily seeded to 1.0 per particle; see sync_bonds_from_claims.
460
461 static std::optional<PhysicsParameter> string_to_parameter(std::string_view param);
462
463 void apply_forces();
464 void apply_spatial_interactions();
465 void apply_attraction_forces();
466 void apply_turbulence();
467 void apply_bond_forces();
468 void integrate(float dt);
469 void handle_boundary_conditions();
470 void sync_to_point_collection();
471
472 /**
473 * @struct GroupIndex
474 * @brief A global particle index resolved to its owning collection.
475 */
476 struct GroupIndex {
477 size_t group;
478 size_t local;
479 };
480
481 /** @brief Resolve a global index the same way apply_impulse/get_data_at do. */
482 [[nodiscard]] std::optional<GroupIndex> resolve_global_index(size_t global_index) const;
483
484 void apply_per_particle_force(
485 std::string_view param,
486 const std::shared_ptr<NodeNetwork>& source);
487
488 void apply_per_particle_mass(
489 const std::shared_ptr<NodeNetwork>& source);
490
491 mutable std::atomic<uint32_t> m_access_token { 0 };
492 std::atomic<bool> m_shutdown { false };
493};
494
495} // namespace MayaFlux::Nodes::Network
float radius
uint32_t index
Definition VKDevice.cpp:142
float value
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:11
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.