15 "PhysicsOperator created");
24 if (vertices.empty()) {
26 "Cannot initialize PhysicsOperator with zero vertices");
36 "PhysicsOperator initialized with {} points", vertices.size());
44 const std::vector<std::vector<PointVertex>>& collections)
46 for (
const auto& collection : collections) {
51 "PhysicsOperator initialized with {} collections",
56 const std::vector<PointVertex>& vertices,
57 float mass_multiplier)
59 if (vertices.empty()) {
61 "Cannot add collection with zero vertices");
66 group.
collection = std::make_shared<GpuSync::PointCollectionNode>();
70 for (
size_t i = 0; i < vertices.size(); ++i) {
73 .force = glm::vec3(0.0F),
74 .mass = mass_multiplier
81 uint32_t expected = 0;
83 std::memory_order_acquire, std::memory_order_relaxed)) {
84 if (
m_shutdown.load(std::memory_order_relaxed))
94 "Added collection #{} with {} points (mass_mult={:.2f})",
111 std::vector<PointVertex> vertices(
count);
112 std::memcpy(vertices.data(), data.data(), data.size());
116 "PhysicsOperator seeded {} vertices from upstream '{}'",
126 uint32_t expected = 0;
128 std::memory_order_acquire, std::memory_order_relaxed)) {
129 if (
m_shutdown.load(std::memory_order_relaxed))
146 group.collection->compute_frame();
158 return Reflect::string_to_enum_case_insensitive<PhysicsParameter>(param);
167 Reflect::string_to_enum_or_throw_case_insensitive<PhysicsParameter>(
168 param,
"PhysicsOperator parameter");
169 }
catch (
const std::invalid_argument& e) {
174 "Unknown physics parameter: '{}'", param);
178 switch (*param_enum) {
189 m_drag = glm::clamp(
static_cast<float>(
value), 0.0F, 1.0F);
206 auto&
points = group.collection->get_points();
223 if (query ==
"point_count") {
226 if (query ==
"collection_count") {
229 if (query ==
"avg_velocity") {
231 size_t total_points = 0;
234 for (
const auto& state : group.physics_state) {
235 avg += state.velocity;
240 if (total_points > 0) {
241 avg /=
static_cast<float>(total_points);
243 return static_cast<double>(glm::length(avg));
258 const auto&
points = group.collection->get_points();
259 for (
const auto& pt :
points) {
279 auto span = group.collection->get_vertex_data();
282 span.begin(), span.end());
289 size_t current_offset = 0;
292 size_t group_size = group.physics_state.size();
294 if (global_index < current_offset + group_size) {
295 size_t local_index = global_index - current_offset;
296 return static_cast<double>(glm::length(group.physics_state[local_index].velocity));
299 current_offset += group_size;
311 auto layout_opt =
m_collections[0].collection->get_vertex_layout();
312 if (!layout_opt.has_value()) {
316 auto layout = *layout_opt;
326 return m_collections[0].collection->get_primitive_topology();
331 std::vector<DrawRun> runs;
335 const auto count =
static_cast<uint32_t
>(
points.collection->get_vertex_count());
336 const auto topo =
points.collection->get_primitive_topology();
338 if (!runs.empty() && runs.back().topology == topo && is_concatenable_topology(topo)) {
339 runs.back().vertex_count +=
count;
354 total += group.collection->get_vertex_count();
361 return std::ranges::any_of(
363 [](
const auto& group) {
return group.collection->needs_gpu_update(); });
369 group.collection->clear_gpu_update_flag();
377 total += group.collection->get_point_count();
399 std::string_view param,
400 const std::shared_ptr<NodeNetwork>& source)
404 if (source->get_node_count() != point_count) {
406 "ONE_TO_ONE size mismatch: {} particles vs {} source nodes",
407 point_count, source->get_node_count());
411 if (param ==
"force_x" || param ==
"force_y" || param ==
"force_z") {
413 }
else if (param ==
"mass") {
421 std::string_view param,
422 const std::shared_ptr<NodeNetwork>& source)
424 size_t global_index = 0;
427 for (
auto& i : group.physics_state) {
428 auto val = source->get_node_output(global_index++);
432 auto force =
static_cast<float>(*val);
434 if (param ==
"force_x") {
436 }
else if (param ==
"force_y") {
438 }
else if (param ==
"force_z") {
446 const std::shared_ptr<NodeNetwork>& source)
448 size_t global_index = 0;
451 for (
auto& i : group.physics_state) {
452 auto val = source->get_node_output(global_index++);
456 i.mass = std::max(0.1F,
static_cast<float>(*val));
468 for (
auto& state : group.physics_state) {
491 auto&
points = group.collection->get_points();
493 for (
size_t i = 0; i <
points.size(); ++i) {
507 for (
auto& state : group.physics_state) {
508 state.force += field(glm::vec3(0.0F));
517 auto& points1 = group1.collection->get_points();
519 for (
size_t i = 0; i < points1.size(); ++i) {
520 const auto& pos_i = points1[i].position;
521 auto& state_i = group1.physics_state[i];
525 auto& points2 = group2.collection->get_points();
527 size_t start_j = (g1 == g2) ? i + 1 : 0;
529 for (
size_t j = start_j; j < points2.size(); ++j) {
530 const auto& pos_j = points2[j].position;
531 auto& state_j = group2.physics_state[j];
533 glm::vec3 delta = pos_j - pos_i;
534 float distance = glm::length(delta);
536 if (distance < m_interaction_radius && distance > 0.001F) {
537 glm::vec3 direction = delta / distance;
541 float repulsion_force = 0.0F;
546 glm::vec3 force = direction * (spring_force - repulsion_force);
548 state_i.force += force;
549 state_j.force -= force;
588 if (!self_idx || !root_idx) {
592 auto& self_state =
m_collections[self_idx->group].physics_state[self_idx->local];
593 const auto& self_pos =
m_collections[self_idx->group].collection->get_points()[self_idx->local].position;
594 const auto& root_pos =
m_collections[root_idx->group].collection->get_points()[root_idx->local].position;
596 glm::vec3 delta = root_pos - self_pos;
597 float distance = glm::length(delta);
598 if (distance < 0.001F) {
604 glm::vec3 direction = delta / distance;
605 self_state.force += direction * (
m_bond_stiffness * (distance - rest_length));
619 for (
size_t i = 0; i < claimed_by.size(); ++i) {
620 uint32_t root = claimed_by[i];
628 m_bond_root.assign(claimed_by.begin(), claimed_by.end());
666 count +=
static_cast<float>(group.collection->get_point_count());
694 auto&
points = group.collection->get_points();
696 for (
size_t i = 0; i <
points.size(); ++i) {
697 group.physics_state[i].force += field(
points[i].
position) * group.physics_state[i].mass;
705 auto&
points = group.collection->get_points();
707 for (
size_t i = 0; i <
points.size(); ++i) {
708 auto& state = group.physics_state[i];
711 glm::vec3 acceleration = state.force / state.mass;
712 state.velocity += acceleration * dt;
713 state.velocity *= (1.0F -
m_drag);
714 vertex.position += state.velocity * dt;
716 state.force = glm::vec3(0.0F);
727 constexpr float damping = 0.8F;
730 auto&
points = group.collection->get_points();
732 for (
size_t i = 0; i <
points.size(); ++i) {
734 auto& state = group.physics_state[i];
736 for (
int axis = 0; axis < 3; ++axis) {
741 state.velocity[axis] *= -damping;
748 state.velocity[axis] = 0.0F;
753 }
else if (vertex.position[axis] >
m_bounds.
max[axis]) {
757 state.velocity[axis] *= -damping;
764 state.velocity[axis] = 0.0F;
778 group.collection->mark_vertex_data_dirty(
true);
787 size_t local_index = global_index -
offset;
788 return &group.collection->get_points()[local_index];
790 offset += group.collection->get_point_count();
798 for (
auto& state : group.physics_state) {
799 state.velocity += impulse / state.mass;
810 group.physics_state[local_index].velocity += impulse / group.physics_state[local_index].mass;
813 offset += group.collection->get_point_count();
824std::vector<std::pair<std::string, Kakshya::DataVariant>>
827 std::vector<float> mass;
830 for (
const auto& state : group.physics_state) {
831 mass.push_back(state.mass);
835 std::vector<std::pair<std::string, Kakshya::DataVariant>>
attributes;
836 attributes.emplace_back(
"mass", std::move(mass));
842 attributes.emplace_back(
"accreted_mass", std::move(accreted));
852 for (
const auto& state : group.physics_state) {
868 uint32_t cluster = 0;
870 const size_t count = group.collection->get_vertex_count();
894 "Cleared all force fields");
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
std::optional< glm::vec3 > position
std::vector< glm::vec2 > * points
std::vector< uint64_t > ids
std::vector< glm::vec3 > positions
std::vector< SpatialAttribute > attributes
std::vector< glm::vec3 > velocities
void apply_one_to_one(std::string_view param, const std::shared_ptr< NodeNetwork > &source) override
Apply ONE_TO_ONE parameter mapping.
virtual std::span< const uint8_t > get_vertex_data() const =0
Get vertex data for GPU upload.
Operator that produces GPU-renderable geometry.
virtual std::string_view get_type_name() const =0
Type name for introspection.
Kinesis::SamplerBounds m_bounds
size_t bond_count() const
Number of particles currently bonded to a root other than themselves.
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.
std::span< const float > get_accreted_mass_span()
Every particle's currently accreted mass, in global index order.
std::optional< GroupIndex > resolve_global_index(size_t global_index) const
Resolve a global index the same way apply_impulse/get_data_at do.
size_t get_point_count() const override
Get source point count (before topology expansion)
std::vector< CollectionGroup > m_collections
std::vector< glm::vec3 > extract_vertex_velocities() const override
Per-particle PhysicsState::velocity, in the same global vertex-index order get_vertex_data() produces...
std::atomic< bool > m_shutdown
void enable_bonds(bool enable)
Enable or disable adopting bonds from sync_bonds_from_claims.
Kinesis::Stochastic::Stochastic m_random_generator
void mark_vertex_data_clean() override
Clear dirty flag after GPU upload.
std::vector< DrawRun > topology_runs() const override
Coalesces adjacent point collections sharing a concatenable topology into one run.
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)
void sync_bonds_from_claims(std::span< const uint32_t > claimed_by)
Adopt this cycle's GPU claim/absorption clustering as bonds.
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
std::vector< Kinesis::VectorField > m_force_fields
std::vector< std::pair< std::string, Kakshya::DataVariant > > extract_vertex_attributes() const override
Adds "mass" (PhysicsState::mass) and "accreted_mass" (get_accreted_mass_span()) as named float attrib...
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.
void seed_from_upstream(const GraphicsOperator *upstream) override
Seed physics state from upstream operator's vertex data.
std::vector< float > m_accreted_mass
Lazily seeded to 1.0 per particle; see sync_bonds_from_claims.
std::span< const uint8_t > get_vertex_data_for_collection(uint32_t idx) const override
Get vertex data for specific collection (if multiple)
void clear_force_fields()
Remove all external force fields.
std::vector< uint32_t > build_cluster_ids() const override
Per-particle collection index, global index order.
float m_repulsion_strength
float get_accreted_mass(size_t global_index) const
This particle's currently accreted mass.
float get_total_mass() const
Sum of every particle's accreted mass.
float m_interaction_radius
void add_force_field(Kinesis::VectorField field)
Add an external force field evaluated per-particle per-frame.
void apply_attraction_forces()
void clear_bonds()
Drop every adopted bond.
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.
std::vector< uint32_t > m_bond_root
Empty when no bonds are adopted; see sync_bonds_from_claims.
std::optional< Portal::Graphics::PrimitiveTopology > declared_topology() const override
Forwards to the first collection's own get_primitive_topology().
void apply_global_impulse(const glm::vec3 &impulse)
Apply impulse to all particles.
static std::optional< PhysicsParameter > string_to_parameter(std::string_view param)
std::atomic< uint32_t > m_access_token
void apply_bond_forces()
Pull each bonded particle toward its root with a spring.
@ NodeProcessing
Node graph processing (Nodes::NodeGraphManager)
@ Nodes
DSP Generator and Filter Nodes, graph pipeline, node management.
VectorField turbulence(float strength, Stochastic::Stochastic rng=Stochastic::Stochastic())
Uniform random force field using Stochastic infrastructure.
VectorField point_attractor(const glm::vec3 &anchor, float strength)
Radial attraction/repulsion toward an anchor point.
Kakshya::PointVertex PointVertex
uint32_t vertex_count
Total number of vertices in this buffer.
Complete description of vertex data layout in a buffer.
Typed, composable, stateless callable from domain D to range R.
std::shared_ptr< GpuSync::PointCollectionNode > collection
std::vector< PhysicsState > physics_state
A global particle index resolved to its owning collection.
Physics-specific data parallel to PointVertex array.
PrimitiveTopology topology
One contiguous span of vertices sharing a primitive topology.