# 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 # ------------------------------------------------------------------------------ @wp.kernel(enable_backward=False) 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]) @wp.kernel(enable_backward=False) 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.""" @staticmethod def internal_state() -> InternalState: return InternalState() @staticmethod 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