13 "PhysicsOperator created");
22 if (vertices.empty()) {
24 "Cannot initialize PhysicsOperator with zero vertices");
32 "PhysicsOperator initialized with {} points", vertices.size());
40 const std::vector<std::vector<PointVertex>>& collections)
42 for (
const auto& collection : collections) {
47 "PhysicsOperator initialized with {} collections",
52 const std::vector<PointVertex>& vertices,
53 float mass_multiplier)
55 if (vertices.empty()) {
57 "Cannot add collection with zero vertices");
62 group.
collection = std::make_shared<GpuSync::PointCollectionNode>();
66 for (
size_t i = 0; i < vertices.size(); ++i) {
69 .force = glm::vec3(0.0F),
70 .mass = mass_multiplier
80 "Added collection #{} with {} points (mass_mult={:.2f})",
100 group.collection->compute_frame();
110 return Reflect::string_to_enum_case_insensitive<PhysicsParameter>(param);
119 Reflect::string_to_enum_or_throw_case_insensitive<PhysicsParameter>(
120 param,
"PhysicsOperator parameter");
121 }
catch (
const std::invalid_argument& e) {
126 "Unknown physics parameter: '{}'", param);
130 switch (*param_enum) {
141 m_drag = glm::clamp(
static_cast<float>(value), 0.0F, 1.0F);
158 auto& points = group.collection->get_points();
159 for (
auto& pt : points) {
175 if (query ==
"point_count") {
178 if (query ==
"collection_count") {
181 if (query ==
"avg_velocity") {
183 size_t total_points = 0;
186 for (
const auto& state : group.physics_state) {
187 avg += state.velocity;
192 if (total_points > 0) {
193 avg /=
static_cast<float>(total_points);
195 return static_cast<double>(glm::length(avg));
207 std::vector<PointVertex> positions;
210 const auto& points = group.collection->get_points();
211 for (
const auto& pt : points) {
212 positions.push_back(pt);
231 auto span = group.collection->get_vertex_data();
234 span.begin(), span.end());
241 size_t current_offset = 0;
244 size_t group_size = group.physics_state.size();
246 if (global_index < current_offset + group_size) {
247 size_t local_index = global_index - current_offset;
248 return static_cast<double>(glm::length(group.physics_state[local_index].velocity));
251 current_offset += group_size;
263 auto layout_opt =
m_collections[0].collection->get_vertex_layout();
264 if (!layout_opt.has_value()) {
275 total += group.collection->get_vertex_count();
282 return std::ranges::any_of(
284 [](
const auto& group) {
return group.collection->needs_gpu_update(); });
290 group.collection->mark_vertex_data_dirty(
false);
298 total += group.collection->get_point_count();
320 std::string_view param,
321 const std::shared_ptr<NodeNetwork>& source)
325 if (source->get_node_count() != point_count) {
327 "ONE_TO_ONE size mismatch: {} particles vs {} source nodes",
328 point_count, source->get_node_count());
332 if (param ==
"force_x" || param ==
"force_y" || param ==
"force_z") {
334 }
else if (param ==
"mass") {
342 std::string_view param,
343 const std::shared_ptr<NodeNetwork>& source)
345 size_t global_index = 0;
348 for (
auto& i : group.physics_state) {
349 auto val = source->get_node_output(global_index++);
353 auto force =
static_cast<float>(*val);
355 if (param ==
"force_x") {
357 }
else if (param ==
"force_y") {
359 }
else if (param ==
"force_z") {
367 const std::shared_ptr<NodeNetwork>& source)
369 size_t global_index = 0;
372 for (
auto& i : group.physics_state) {
373 auto val = source->get_node_output(global_index++);
377 i.mass = std::max(0.1F,
static_cast<float>(*val));
389 for (
auto& state : group.physics_state) {
410 for (
auto& state : group.physics_state) {
411 glm::vec3 random_force(
424 auto& points1 = group1.collection->get_points();
426 for (
size_t i = 0; i < points1.size(); ++i) {
427 const auto& pos_i = points1[i].position;
428 auto& state_i = group1.physics_state[i];
432 auto& points2 = group2.collection->get_points();
434 size_t start_j = (g1 == g2) ? i + 1 : 0;
436 for (
size_t j = start_j; j < points2.size(); ++j) {
437 const auto& pos_j = points2[j].position;
438 auto& state_j = group2.physics_state[j];
440 glm::vec3 delta = pos_j - pos_i;
441 float distance = glm::length(delta);
443 if (distance < m_interaction_radius && distance > 0.001F) {
444 glm::vec3 direction = delta / distance;
448 float repulsion_force = 0.0F;
453 glm::vec3 force = direction * (spring_force - repulsion_force);
455 state_i.force += force;
456 state_j.force -= force;
467 auto& points = group.collection->get_points();
469 for (
size_t i = 0; i < points.size(); ++i) {
471 float distance = glm::length(to_attractor);
473 if (distance > 0.001F) {
474 glm::vec3 direction = to_attractor / distance;
476 group.physics_state[i].force += direction * force_magnitude * group.physics_state[i].mass;
485 auto& points = group.collection->get_points();
487 for (
size_t i = 0; i < points.size(); ++i) {
488 auto& state = group.physics_state[i];
489 auto& vertex = points[i];
491 glm::vec3 acceleration = state.force / state.mass;
492 state.velocity += acceleration * dt;
493 state.velocity *= (1.0F -
m_drag);
494 vertex.position += state.velocity * dt;
496 state.force = glm::vec3(0.0F);
507 constexpr float damping = 0.8F;
510 auto& points = group.collection->get_points();
512 for (
size_t i = 0; i < points.size(); ++i) {
513 auto& vertex = points[i];
514 auto& state = group.physics_state[i];
516 for (
int axis = 0; axis < 3; ++axis) {
521 state.velocity[axis] *= -damping;
528 state.velocity[axis] = 0.0F;
533 }
else if (vertex.position[axis] >
m_bounds.
max[axis]) {
537 state.velocity[axis] *= -damping;
544 state.velocity[axis] = 0.0F;
558 group.collection->mark_vertex_data_dirty(
true);
567 size_t local_index = global_index - offset;
568 return &group.collection->get_points()[local_index];
570 offset += group.collection->get_point_count();
578 for (
auto& state : group.physics_state) {
579 state.velocity += impulse / state.mass;
589 size_t local_index = index - offset;
590 group.physics_state[local_index].velocity += impulse / group.physics_state[local_index].mass;
593 offset += group.collection->get_point_count();
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
void apply_one_to_one(std::string_view param, const std::shared_ptr< NodeNetwork > &source) override
Apply ONE_TO_ONE parameter mapping.
Kinesis::SamplerBounds m_bounds
std::vector< PointVertex > extract_vertices() const
Extract current vertex data as PointVertex array.
size_t get_vertex_count() const override
Get number of vertices (may differ from point count for topology/path)
void apply_impulse(size_t index, const glm::vec3 &impulse)
Apply impulse to specific particle.
size_t get_point_count() const override
Get source point count (before topology expansion)
std::vector< CollectionGroup > m_collections
Kinesis::Stochastic::Stochastic m_random_generator
void mark_vertex_data_clean() override
Clear dirty flag after GPU upload.
void apply_per_particle_force(std::string_view param, const std::shared_ptr< NodeNetwork > &source)
std::optional< double > get_particle_velocity(size_t global_index) const
Get velocity magnitude for specific particle.
Kakshya::VertexLayout get_vertex_layout() const override
Get vertex layout describing vertex structure.
void set_attraction_point(const glm::vec3 &point)
std::vector< uint8_t > m_vertex_data_aggregate
std::span< const uint8_t > get_vertex_data() const override
Get vertex data for GPU upload.
void initialize_collections(const std::vector< std::vector< PointVertex > > &collections)
Initialize multiple physics collections.
bool is_vertex_data_dirty() const override
Check if geometry changed this frame.
@ BOUNCE
Reflect off boundaries with damping.
@ NONE
No bounds checking.
@ WRAP
Teleport to opposite side.
bool m_has_attraction_point
void apply_per_particle_mass(const std::shared_ptr< NodeNetwork > &source)
void add_collection(const std::vector< PointVertex > &vertices, float mass_multiplier=1.0F)
Add a single physics collection.
void set_bounds(const glm::vec3 &min, const glm::vec3 &max)
Set the simulation bounds.
float m_attraction_strength
float m_turbulence_strength
bool m_spatial_interactions_enabled
void * get_data_at(size_t global_index) override
Get mutable access to point at global index.
void sync_to_point_collection()
void handle_boundary_conditions()
std::optional< double > query_state(std::string_view query) const override
Query operator internal state.
void process(float dt) override
Process for one batch cycle.
std::span< const uint8_t > get_vertex_data_for_collection(uint32_t idx) const override
Get vertex data for specific collection (if multiple)
float m_repulsion_strength
float m_interaction_radius
void apply_attraction_forces()
void apply_spatial_interactions()
glm::vec3 m_attraction_point
void set_parameter(std::string_view param, double value) override
Set operator parameter.
void apply_one_to_one(std::string_view param, const std::shared_ptr< NodeNetwork > &source) override
Apply ONE_TO_ONE parameter for physics-specific properties.
void initialize(const std::vector< PointVertex > &vertices)
Initialize with a single physics collection.
void apply_global_impulse(const glm::vec3 &impulse)
Apply impulse to all particles.
static std::optional< PhysicsParameter > string_to_parameter(std::string_view param)
@ NodeProcessing
Node graph processing (Nodes::NodeGraphManager)
@ Nodes
DSP Generator and Filter Nodes, graph pipeline, node management.
Complete description of vertex data layout in a buffer.
std::shared_ptr< GpuSync::PointCollectionNode > collection
std::vector< PhysicsState > physics_state
Physics-specific data parallel to PointVertex array.