Spaces:
Sleeping
Sleeping
GarmentCode / NvidiaWarp-GarmentCode /exts /omni.warp /omni /warp /nodes /_impl /OgnParticlesFromMesh.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 generating particles inside a mesh.""" | |
| import traceback | |
| from typing import Tuple | |
| import numpy as np | |
| import omni.graph.core as og | |
| import warp as wp | |
| import omni.warp.nodes | |
| from omni.warp.nodes.ogn.OgnParticlesFromMeshDatabase import OgnParticlesFromMeshDatabase | |
| PROFILING = False | |
| # Kernels | |
| # ------------------------------------------------------------------------------ | |
| 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 sample_mesh_kernel( | |
| mesh: wp.uint64, | |
| grid_lower_bound: wp.vec3, | |
| max_points: int, | |
| min_sdf: float, | |
| max_sdf: float, | |
| spacing: float, | |
| spacing_jitter: float, | |
| seed: int, | |
| out_point_count: wp.array(dtype=int), | |
| out_points: wp.array(dtype=wp.vec3), | |
| ): | |
| tid = wp.tid() | |
| x, y, z = wp.tid() | |
| # Retrieve the cell's center position. | |
| cell_pos = ( | |
| grid_lower_bound | |
| + wp.vec3( | |
| float(x) + 0.5, | |
| float(y) + 0.5, | |
| float(z) + 0.5, | |
| ) | |
| * spacing | |
| ) | |
| # Query the closest location on the mesh. | |
| max_dist = 1000.0 | |
| sign = float(0.0) | |
| face_index = int(0) | |
| face_u = float(0.0) | |
| face_v = float(0.0) | |
| if not wp.mesh_query_point( | |
| mesh, | |
| cell_pos, | |
| max_dist, | |
| sign, | |
| face_index, | |
| face_u, | |
| face_v, | |
| ): | |
| return | |
| # Evaluates the position of the closest mesh location found. | |
| mesh_pos = wp.mesh_eval_position(mesh, face_index, face_u, face_v) | |
| # Check that the cell's distance to the mesh location is within | |
| # the desired range. | |
| dist = wp.length(cell_pos - mesh_pos) * sign | |
| if dist < min_sdf or dist > max_sdf: | |
| return | |
| # Increment the counter of valid point locations found. | |
| point_index = wp.atomic_add(out_point_count, 0, 1) | |
| if point_index > max_points: | |
| return | |
| # Compute the spacing jitter value while making sure it's normalized | |
| # in a range [-1, 1]. | |
| rng = wp.rand_init(seed, tid) | |
| jitter = wp.vec3( | |
| wp.randf(rng) * 2.0 - 1.0, | |
| wp.randf(rng) * 2.0 - 1.0, | |
| wp.randf(rng) * 2.0 - 1.0, | |
| ) | |
| # Store the point position. | |
| out_points[point_index] = cell_pos + jitter * spacing_jitter | |
| # Internal State | |
| # ------------------------------------------------------------------------------ | |
| class InternalState: | |
| """Internal state for the node.""" | |
| def __init__(self) -> None: | |
| self.mesh = None | |
| self.is_valid = False | |
| self.attr_tracking = omni.warp.nodes.AttrTracking( | |
| ( | |
| "transform", | |
| "seed", | |
| "minSdf", | |
| "maxSdf", | |
| "radius", | |
| "spacing", | |
| "spacingJitter", | |
| "mass", | |
| "velocityDir", | |
| "velocityAmount", | |
| "maxPoints", | |
| ), | |
| ) | |
| def needs_initialization(self, db: OgnParticlesFromMeshDatabase) -> bool: | |
| """Checks if the internal state needs to be (re)initialized.""" | |
| if not self.is_valid: | |
| return True | |
| if omni.warp.nodes.bundle_has_changed(db.inputs.mesh): | |
| return True | |
| return False | |
| def initialize(self, db: OgnParticlesFromMeshDatabase) -> bool: | |
| """Initializes the internal state.""" | |
| point_count = omni.warp.nodes.mesh_get_point_count(db.inputs.mesh) | |
| xform = omni.warp.nodes.bundle_get_world_xform(db.inputs.mesh) | |
| # Transform the mesh's point positions into world space. | |
| world_point_positions = wp.empty(point_count, dtype=wp.vec3) | |
| wp.launch( | |
| kernel=transform_points_kernel, | |
| dim=point_count, | |
| inputs=[ | |
| omni.warp.nodes.mesh_get_points(db.inputs.mesh), | |
| xform.T, | |
| ], | |
| outputs=[ | |
| world_point_positions, | |
| ], | |
| ) | |
| # Initialize Warp's mesh instance, which requires | |
| # a triangulated topology. | |
| face_vertex_indices = omni.warp.nodes.mesh_triangulate(db.inputs.mesh) | |
| mesh = wp.Mesh( | |
| points=world_point_positions, | |
| velocities=wp.zeros(point_count, dtype=wp.vec3), | |
| indices=face_vertex_indices, | |
| ) | |
| # Store the class members. | |
| self.mesh = mesh | |
| return True | |
| # Compute | |
| # ------------------------------------------------------------------------------ | |
| def spawn_particles(db: OgnParticlesFromMeshDatabase) -> Tuple[wp.array, int]: | |
| """Spawns the particles by filling the given point positions array.""" | |
| # Initialize an empty array that will hold the particle positions. | |
| points = wp.empty(db.inputs.maxPoints, dtype=wp.vec3) | |
| # Retrieve the mesh's aligned bounding box. | |
| extent = omni.warp.nodes.mesh_get_world_extent( | |
| db.inputs.mesh, | |
| axis_aligned=True, | |
| ) | |
| # Compute the emitter's bounding box size. | |
| extent_size = extent[1] - extent[0] | |
| # Infer the emitter's grid dimensions from its bounding box size and | |
| # the requested spacing. | |
| spacing = max(db.inputs.spacing, 1e-6) | |
| dims = (extent_size / spacing).astype(int) + 1 | |
| dims = np.maximum(dims, 1) | |
| # Add one particle per grid cell located within the mesh geometry. | |
| point_count = wp.zeros(1, dtype=int) | |
| wp.launch( | |
| kernel=sample_mesh_kernel, | |
| dim=dims, | |
| inputs=[ | |
| db.internal_state.mesh.id, | |
| extent[0], | |
| db.inputs.maxPoints, | |
| db.inputs.minSdf, | |
| db.inputs.maxSdf, | |
| spacing, | |
| db.inputs.spacingJitter, | |
| db.inputs.seed, | |
| ], | |
| outputs=[ | |
| point_count, | |
| points, | |
| ], | |
| ) | |
| # Retrieve the actual number of particles created. | |
| point_count = min(int(point_count.numpy()[0]), db.inputs.maxPoints) | |
| return (points, point_count) | |
| def compute(db: OgnParticlesFromMeshDatabase) -> None: | |
| """Evaluates the node.""" | |
| db.outputs.particles.changes().activate() | |
| if not db.inputs.mesh.valid or not db.outputs.particles.valid: | |
| return | |
| state = db.internal_state | |
| # Initialize the internal state if it hasn't been already. | |
| if state.needs_initialization(db): | |
| if not state.initialize(db): | |
| return | |
| elif not state.attr_tracking.have_attrs_changed(db): | |
| return | |
| with omni.warp.nodes.NodeTimer("spawn_particles", db, active=PROFILING): | |
| # Spawn new particles inside the mesh. | |
| (points, point_count) = spawn_particles(db) | |
| # Create a new geometry points within the output bundle. | |
| omni.warp.nodes.points_create_bundle( | |
| db.outputs.particles, | |
| point_count, | |
| xform=db.inputs.transform, | |
| create_masses=True, | |
| create_velocities=True, | |
| create_widths=True, | |
| ) | |
| # Copy the point positions onto the output bundle. | |
| wp.copy( | |
| omni.warp.nodes.points_get_points(db.outputs.particles), | |
| points, | |
| count=point_count, | |
| ) | |
| if point_count: | |
| velocities = omni.warp.nodes.points_get_velocities(db.outputs.particles) | |
| if db.inputs.velocityAmount < 1e-6: | |
| velocities.fill_(0.0) | |
| else: | |
| # Retrieve the mesh's world transformation. | |
| xform = omni.warp.nodes.bundle_get_world_xform(db.inputs.mesh) | |
| # Retrieve the normalized velocity direction. | |
| vel = db.inputs.velocityDir | |
| vel /= np.linalg.norm(vel) | |
| # Transform the velocity local direction with the mesh's world | |
| # rotation matrix to get the velocity direction in world space. | |
| vel = np.dot(xform[:3, :3].T, vel) | |
| # Scale the result to get the velocity's magnitude. | |
| vel *= db.inputs.velocityAmount | |
| # Store the velocities in the output bundle. | |
| velocities.fill_(wp.vec3(vel)) | |
| # Store the radius in the output bundle. | |
| widths = omni.warp.nodes.points_get_widths(db.outputs.particles) | |
| widths.fill_(db.inputs.radius * 2.0) | |
| # Store the mass in the output bundle. | |
| masses = omni.warp.nodes.points_get_masses(db.outputs.particles) | |
| masses.fill_(db.inputs.mass) | |
| state.attr_tracking.update_state(db) | |
| # Node Entry Point | |
| # ------------------------------------------------------------------------------ | |
| class OgnParticlesFromMesh: | |
| """Node.""" | |
| def internal_state() -> InternalState: | |
| return InternalState() | |
| def compute(db: OgnParticlesFromMeshDatabase) -> None: | |
| device = wp.get_device("cuda:0") | |
| try: | |
| with wp.ScopedDevice(device): | |
| compute(db) | |
| 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 | |