Spaces:
Sleeping
Sleeping
GarmentCode / NvidiaWarp-GarmentCode /exts /omni.warp /omni /warp /nodes /_impl /OgnParticlesSimulate.py
| # Copyright (c) 2023 NVIDIA CORPORATION. All rights reserved. | |
| # NVIDIA CORPORATION and its licensors retain all intellectual property | |
| # and proprietary rights in and to this software, related documentation | |
| # and any modifications thereto. Any use, reproduction, disclosure or | |
| # distribution of this software and related documentation without an express | |
| # license agreement from NVIDIA CORPORATION is strictly prohibited. | |
| """Node simulating particles.""" | |
| from math import inf | |
| import traceback | |
| import numpy as np | |
| import omni.graph.core as og | |
| import omni.timeline | |
| import warp as wp | |
| import omni.warp.nodes | |
| from omni.warp.nodes.ogn.OgnParticlesSimulateDatabase import OgnParticlesSimulateDatabase | |
| USE_GRAPH = True | |
| PROFILING = False | |
| # Kernels | |
| # ------------------------------------------------------------------------------ | |
| def query_max_value_kernel( | |
| values: wp.array(dtype=float), | |
| out_max: wp.array(dtype=float), | |
| ): | |
| wp.atomic_max(out_max, 0, values[wp.tid()]) | |
| def compute_particles_inv_mass_kernel( | |
| masses: wp.array(dtype=float), | |
| out_inv_masses: wp.array(dtype=float), | |
| ): | |
| tid = wp.tid() | |
| out_inv_masses[tid] = 1.0 / masses[tid] | |
| def compute_particles_radius_kernel( | |
| widths: wp.array(dtype=float), | |
| out_radii: wp.array(dtype=float), | |
| ): | |
| tid = wp.tid() | |
| out_radii[tid] = widths[tid] * 0.5 | |
| def transform_points_kernel( | |
| points: wp.array(dtype=wp.vec3), | |
| xform: wp.mat44, | |
| out_points: wp.array(dtype=wp.vec3), | |
| ): | |
| tid = wp.tid() | |
| out_points[tid] = wp.transform_point(xform, points[tid]) | |
| def update_collider_kernel( | |
| points_0: wp.array(dtype=wp.vec3), | |
| points_1: wp.array(dtype=wp.vec3), | |
| xform_0: wp.mat44, | |
| xform_1: wp.mat44, | |
| sim_dt: float, | |
| out_points: wp.array(dtype=wp.vec3), | |
| out_velocities: wp.array(dtype=wp.vec3), | |
| ): | |
| tid = wp.tid() | |
| point_0 = wp.transform_point(xform_0, points_0[tid]) | |
| point_1 = wp.transform_point(xform_1, points_1[tid]) | |
| out_points[tid] = point_0 | |
| out_velocities[tid] = (point_1 - point_0) / sim_dt | |
| def update_particles_kernel( | |
| points_0: wp.array(dtype=wp.vec3), | |
| xform: wp.mat44, | |
| out_points: wp.array(dtype=wp.vec3), | |
| out_velocities: wp.array(dtype=wp.vec3), | |
| ): | |
| tid = wp.tid() | |
| point = wp.transform_point(xform, points_0[tid]) | |
| diff = point - points_0[tid] | |
| out_points[tid] = point | |
| out_velocities[tid] = out_velocities[tid] + diff | |
| # Internal State | |
| # ------------------------------------------------------------------------------ | |
| class InternalState: | |
| """Internal state for the node.""" | |
| def __init__(self) -> None: | |
| self.sim_dt = None | |
| self.sim_tick = None | |
| self.model = None | |
| self.integrator = None | |
| self.state_0 = None | |
| self.state_1 = None | |
| self.xform = None | |
| self.collider_xform = None | |
| self.collider_mesh = None | |
| self.collider_points_0 = None | |
| self.collider_points_1 = None | |
| self.graph = None | |
| self.sim_enabled = True | |
| self.time = 0.0 | |
| self.is_valid = False | |
| self.attr_tracking = omni.warp.nodes.AttrTracking( | |
| ( | |
| "substepCount", | |
| "gravity", | |
| "globalScale", | |
| "contactElasticStiffness", | |
| "contactFrictionStiffness", | |
| "contactFrictionCoeff", | |
| "contactDampingStiffness", | |
| "particlesQueryRange", | |
| "particlesContactAdhesion", | |
| "particlesContactCohesion", | |
| "colliderContactDistance", | |
| "colliderContactQueryRange", | |
| "groundEnabled", | |
| "groundAltitude", | |
| ), | |
| ) | |
| def needs_initialization(self, db: OgnParticlesSimulateDatabase) -> bool: | |
| """Checks if the internal state needs to be (re)initialized.""" | |
| if not self.is_valid or not db.inputs.enabled or not self.sim_enabled: | |
| return True | |
| if self.attr_tracking.have_attrs_changed(db): | |
| return True | |
| if db.inputs.time < self.time: | |
| # Reset the simulation when we're rewinding. | |
| return True | |
| return False | |
| def initialize( | |
| self, | |
| db: OgnParticlesSimulateDatabase, | |
| device: wp.context.Device, | |
| ) -> bool: | |
| """Initializes the internal state.""" | |
| # Lazy load warp.sim here to not slow down extension loading. | |
| import warp.sim | |
| # Compute the simulation time step. | |
| timeline = omni.timeline.get_timeline_interface() | |
| sim_rate = timeline.get_ticks_per_second() | |
| sim_dt = 1.0 / sim_rate | |
| # Initialize Warp's simulation model builder. | |
| builder = wp.sim.ModelBuilder() | |
| # Retrieve some data from the particles points. | |
| points = omni.warp.nodes.points_get_points(db.inputs.particles) | |
| xform = omni.warp.nodes.bundle_get_world_xform(db.inputs.particles) | |
| # Transform the particles point positions into world space. | |
| world_points = wp.empty(len(points), dtype=wp.vec3) | |
| wp.launch( | |
| kernel=transform_points_kernel, | |
| dim=len(points), | |
| inputs=[ | |
| points, | |
| xform.T, | |
| ], | |
| outputs=[ | |
| world_points, | |
| ], | |
| ) | |
| if db.inputs.collider.valid: | |
| # Retrieve some data from the collider mesh. | |
| collider_points = omni.warp.nodes.mesh_get_points(db.inputs.collider) | |
| collider_xform = omni.warp.nodes.bundle_get_world_xform(db.inputs.collider) | |
| # Transform the collider point positions into world space. | |
| collider_world_points = wp.empty( | |
| len(collider_points), | |
| dtype=wp.vec3, | |
| ) | |
| wp.launch( | |
| kernel=transform_points_kernel, | |
| dim=len(collider_points), | |
| inputs=[ | |
| collider_points, | |
| collider_xform.T, | |
| ], | |
| outputs=[ | |
| collider_world_points, | |
| ], | |
| ) | |
| # Initialize Warp's mesh instance, which requires | |
| # triangulated meshes. | |
| collider_face_vertex_indices = omni.warp.nodes.mesh_triangulate( | |
| db.inputs.collider, | |
| ) | |
| collider_mesh = wp.sim.Mesh( | |
| collider_world_points.numpy(), | |
| collider_face_vertex_indices.numpy(), | |
| compute_inertia=False, | |
| ) | |
| # Register the collider geometry mesh into Warp's simulation model | |
| # builder. | |
| builder.add_shape_mesh( | |
| body=-1, | |
| mesh=collider_mesh, | |
| pos=(0.0, 0.0, 0.0), | |
| rot=(0.0, 0.0, 0.0, 1.0), | |
| scale=(1.0, 1.0, 1.0), | |
| ) | |
| # Store the collider's point positions as internal state. | |
| collider_points_0 = wp.empty_like(collider_points) | |
| collider_points_1 = wp.empty_like(collider_points) | |
| wp.copy(collider_points_0, collider_points) | |
| wp.copy(collider_points_1, collider_points) | |
| # Store the class members. | |
| self.collider_xform = collider_xform.copy() | |
| self.collider_mesh = collider_mesh | |
| self.collider_points_0 = collider_points_0 | |
| self.collider_points_1 = collider_points_1 | |
| else: | |
| self.collider_mesh = None | |
| # Register the ground. | |
| builder.set_ground_plane( | |
| offset=-db.inputs.groundAltitude, | |
| ke=db.inputs.contactElasticStiffness * db.inputs.globalScale, | |
| kd=db.inputs.contactDampingStiffness * db.inputs.globalScale, | |
| kf=db.inputs.contactFrictionStiffness * db.inputs.globalScale, | |
| mu=db.inputs.contactFrictionCoeff, | |
| ) | |
| # Build the simulation model. | |
| model = builder.finalize() | |
| # Register the input particles into the system. | |
| model.particle_count = omni.warp.nodes.points_get_point_count(db.inputs.particles) | |
| model.particle_q = world_points | |
| model.particle_qd = omni.warp.nodes.points_get_velocities(db.inputs.particles) | |
| model.particle_mass = omni.warp.nodes.points_get_masses(db.inputs.particles) | |
| model.particle_inv_mass = wp.empty_like(model.particle_mass) | |
| wp.launch( | |
| compute_particles_inv_mass_kernel, | |
| dim=model.particle_count, | |
| inputs=[model.particle_mass], | |
| outputs=[model.particle_inv_mass], | |
| ) | |
| widths = omni.warp.nodes.points_get_widths(db.inputs.particles) | |
| model._particle_radius = wp.empty_like(widths) | |
| wp.launch( | |
| compute_particles_radius_kernel, | |
| dim=model.particle_count, | |
| inputs=[widths], | |
| outputs=[model._particle_radius], | |
| ) | |
| model.particle_flags = wp.empty(model.particle_count, dtype=wp.uint32) | |
| model.particle_flags.fill_(warp.sim.model.PARTICLE_FLAG_ACTIVE.value) | |
| max_width = wp.array((-inf,), dtype=float) | |
| wp.launch( | |
| query_max_value_kernel, | |
| dim=model.particle_count, | |
| inputs=[widths], | |
| outputs=[max_width], | |
| ) | |
| model.particle_max_radius = float(max_width.numpy()[0]) * 0.5 | |
| # Allocate a single contact per particle. | |
| model.allocate_soft_contacts(model.particle_count) | |
| # Initialize the integrator. | |
| integrator = wp.sim.SemiImplicitIntegrator() | |
| # Set the model properties. | |
| model.ground = db.inputs.groundEnabled | |
| model.gravity = db.inputs.gravity | |
| model.particle_adhesion = db.inputs.particlesContactAdhesion | |
| model.particle_cohesion = db.inputs.particlesContactCohesion | |
| model.particle_ke = db.inputs.contactElasticStiffness * db.inputs.globalScale | |
| model.particle_kf = db.inputs.contactFrictionStiffness * db.inputs.globalScale | |
| model.particle_mu = db.inputs.contactFrictionCoeff | |
| model.particle_kd = db.inputs.contactDampingStiffness * db.inputs.globalScale | |
| model.soft_contact_ke = db.inputs.contactElasticStiffness * db.inputs.globalScale | |
| model.soft_contact_kf = db.inputs.contactFrictionStiffness * db.inputs.globalScale | |
| model.soft_contact_mu = db.inputs.contactFrictionCoeff | |
| model.soft_contact_kd = db.inputs.contactDampingStiffness * db.inputs.globalScale | |
| model.soft_contact_margin = db.inputs.colliderContactDistance * db.inputs.colliderContactQueryRange | |
| # Store the class members. | |
| self.sim_dt = sim_dt | |
| self.sim_tick = 0 | |
| self.model = model | |
| self.integrator = integrator | |
| self.state_0 = model.state() | |
| self.state_1 = model.state() | |
| self.xform = xform.copy() | |
| if USE_GRAPH: | |
| # Create the CUDA graph. We first manually load the necessary | |
| # modules to avoid the capture to load all the modules that are | |
| # registered and possibly not relevant. | |
| wp.load_module(device=device) | |
| wp.load_module(module=warp.sim, device=device, recursive=True) | |
| wp.capture_begin(force_module_load=False) | |
| step(db) | |
| self.graph = wp.capture_end() | |
| else: | |
| self.graph = None | |
| self.attr_tracking.update_state(db) | |
| return True | |
| # Compute | |
| # ------------------------------------------------------------------------------ | |
| def update_collider( | |
| db: OgnParticlesSimulateDatabase, | |
| ) -> None: | |
| """Updates the collider state.""" | |
| state = db.internal_state | |
| points = omni.warp.nodes.mesh_get_points(db.inputs.collider) | |
| xform = omni.warp.nodes.bundle_get_world_xform(db.inputs.collider) | |
| # Swap the previous and current collider point positions. | |
| (state.collider_points_0, state.collider_points_1) = ( | |
| state.collider_points_1, | |
| state.collider_points_0, | |
| ) | |
| # Store the current point positions. | |
| wp.copy(state.collider_points_1, points) | |
| # Retrieve the previous and current world transformations. | |
| xform_0 = state.collider_xform | |
| xform_1 = xform | |
| # Update the internal point positions and velocities. | |
| wp.launch( | |
| kernel=update_collider_kernel, | |
| dim=len(state.collider_mesh.vertices), | |
| inputs=[ | |
| state.collider_points_0, | |
| state.collider_points_1, | |
| xform_0.T, | |
| xform_1.T, | |
| state.sim_dt, | |
| ], | |
| outputs=[ | |
| state.collider_mesh.mesh.points, | |
| state.collider_mesh.mesh.velocities, | |
| ], | |
| ) | |
| # Refit the BVH. | |
| state.collider_mesh.mesh.refit() | |
| # Update the state members. | |
| state.collider_xform = xform.copy() | |
| def update_particles( | |
| db: OgnParticlesSimulateDatabase, | |
| ) -> None: | |
| """Updates the particles state.""" | |
| state = db.internal_state | |
| xform = omni.warp.nodes.bundle_get_world_xform(db.inputs.particles) | |
| # Retrieve the previous and current world transformations. | |
| xform_0 = state.xform | |
| xform_1 = xform | |
| # Update the internal point positions and velocities. | |
| wp.launch( | |
| kernel=update_particles_kernel, | |
| dim=len(state.state_0.particle_q), | |
| inputs=[ | |
| state.state_0.particle_q, | |
| np.matmul(np.linalg.inv(xform_0), xform_1).T, | |
| ], | |
| outputs=[ | |
| state.state_0.particle_q, | |
| state.state_0.particle_qd, | |
| ], | |
| ) | |
| # Update the state members. | |
| state.xform = xform.copy() | |
| def step(db: OgnParticlesSimulateDatabase) -> None: | |
| """Steps through the simulation.""" | |
| state = db.internal_state | |
| sim_dt = state.sim_dt / db.inputs.substepCount | |
| # Run the collision detection once per frame. | |
| wp.sim.collide(state.model, state.state_0) | |
| for _ in range(db.inputs.substepCount): | |
| state.state_0.clear_forces() | |
| state.integrator.simulate( | |
| state.model, | |
| state.state_0, | |
| state.state_1, | |
| sim_dt, | |
| ) | |
| # Swap the previous and current states. | |
| (state.state_0, state.state_1) = (state.state_1, state.state_0) | |
| def simulate(db: OgnParticlesSimulateDatabase) -> None: | |
| """Simulates the particles at the current time.""" | |
| state = db.internal_state | |
| state.model.particle_grid.build( | |
| state.state_0.particle_q, | |
| state.model.particle_max_radius * db.inputs.particlesQueryRange, | |
| ) | |
| if USE_GRAPH: | |
| wp.capture_launch(state.graph) | |
| else: | |
| step(db) | |
| def compute(db: OgnParticlesSimulateDatabase, device: wp.context.Device) -> None: | |
| """Evaluates the node.""" | |
| if not db.inputs.particles.valid or not db.outputs.particles.valid: | |
| return | |
| state = db.internal_state | |
| if not db.inputs.enabled: | |
| # Pass through the data. | |
| db.outputs.particles = db.inputs.particles | |
| # Store whether the simulation was last enabled. | |
| state.sim_enabled = False | |
| return | |
| if state.needs_initialization(db): | |
| # Initialize the internal state if it hasn't been already. | |
| # We want to use the input particles geometry as the initial state | |
| # of the simulation so we copy its bundle to the output one. | |
| db.outputs.particles = db.inputs.particles | |
| if not state.initialize(db, device): | |
| return | |
| else: | |
| # We skip the simulation if it has just been initialized. | |
| if state.sim_tick == 0 and omni.warp.nodes.bundle_has_changed(db.inputs.particles): | |
| if not state.initialize(db, device): | |
| return | |
| if ( | |
| db.inputs.collider.valid | |
| and state.collider_mesh is not None | |
| and omni.warp.nodes.bundle_has_changed(db.inputs.collider) | |
| ): | |
| # The collider might be animated so we need to update its state. | |
| update_collider(db) | |
| if omni.warp.nodes.bundle_have_attrs_changed(db.inputs.particles, ("worldMatrix",)): | |
| update_particles(db) | |
| with omni.warp.nodes.NodeTimer("simulate", db, active=PROFILING): | |
| # Run the particles simulation at the current time. | |
| simulate(db) | |
| with omni.warp.nodes.NodeTimer("transform_points_to_local_space", db, active=PROFILING): | |
| # Retrieve some data from the particles points. | |
| xform = omni.warp.nodes.bundle_get_world_xform(db.inputs.particles) | |
| # Transform the particles point positions back into local space | |
| # and store them into the bundle. | |
| out_points = omni.warp.nodes.points_get_points(db.outputs.particles) | |
| wp.launch( | |
| kernel=transform_points_kernel, | |
| dim=len(out_points), | |
| inputs=[ | |
| state.state_0.particle_q, | |
| np.linalg.inv(xform).T, | |
| ], | |
| outputs=[ | |
| out_points, | |
| ], | |
| ) | |
| # Increment the simulation tick. | |
| state.sim_tick += 1 | |
| # Store whether the simulation was last enabled. | |
| state.sim_enabled = True | |
| # Store the current time. | |
| state.time = db.inputs.time | |
| # Node Entry Point | |
| # ------------------------------------------------------------------------------ | |
| class OgnParticlesSimulate: | |
| """Node.""" | |
| def internal_state() -> InternalState: | |
| return InternalState() | |
| def compute(db: OgnParticlesSimulateDatabase) -> None: | |
| device = wp.get_device("cuda:0") | |
| try: | |
| with wp.ScopedDevice(device): | |
| compute(db, device) | |
| except Exception: | |
| db.log_error(traceback.format_exc()) | |
| db.internal_state.is_valid = False | |
| return | |
| db.internal_state.is_valid = True | |
| # Fire the execution for the downstream nodes. | |
| db.outputs.execOut = og.ExecutionAttributeState.ENABLED | |