PhysX: get particle positions from inflatable simulation demo

Hi everyone, was redirected here from the PhysX Github discussion forum.
In USD Composer, there is an inflatable simulation tutorial. I would like to edit the simulation script (shown below) so that at each timestep of the simulation, the vertices and edges (if there are any) of the cloth/mesh are written to a file - this way I can reconstruct the mesh of the inflatable.
I am aware there exists PxParticleBuffer::getPositionInvMasses()/ PxParticleBuffer::getVelocities() to get the position and velocities of particles in C++, but I’m unsure on how this would be called using the Python API. Specifically, how would I obtain these in Python and obtain them at every time step that the simulation runs?

Does anyone, perhaps someone from the Physics team, know how to do so? I imagine it would be like a callback function that executes with each timestep but unsure.
Thanks!

import carb
import math
from omni.physx.scripts import physicsUtils, particleUtils
from pxr import UsdGeom, Sdf, Gf, PhysxSchema, UsdPhysics
import omni.usd
import omni.physxdemos as demo
from .ParticleDemoBaseDemo import ParticleDemoBase


class ParticleInflatableDemo(demo.Base, ParticleDemoBase):
    title = "Particle Inflatable"
    category = demo.Categories.PARTICLES
    short_description = "Basic demo showing setup of a sphere as a particle inflatable"
    description = "A basic sphere mesh is setup as a particle simulation inflatable cloth mesh."

    def create(self, stage):
        default_prim_path = self.setup_base_scene(stage, zoomAmount = 0.8)

        # configure and create particle system
        # we use all defaults, so the particle contact offset will be 5cm / 0.05m
        # so the simulation determines the other offsets from the particle contact offset
        particle_system_path = default_prim_path.AppendChild("particleSystem")
        particle_system = PhysxSchema.PhysxParticleSystem.Define(stage, particle_system_path)
        particle_system.CreateSimulationOwnerRel().SetTargets([self._scene.GetPath()])

        # create material and assign it to the system:
        particle_material_path = default_prim_path.AppendChild("particleMaterial")
        particleUtils.add_pbd_particle_material(stage, particle_material_path)
        physicsUtils.add_physics_material_to_prim(
            stage, stage.GetPrimAtPath(particle_system_path), particle_material_path
        )

        # create a mesh that is turned into an inflatable
        cube_u_resolution = 20
        cube_v_resolution = 20
        cube_w_resolution = 4

        success, tmp_path = omni.kit.commands.execute(
            "CreateMeshPrimWithDefaultXform",
            prim_type="Cube",
            u_patches=cube_u_resolution,
            v_patches=cube_v_resolution,
            w_patches=cube_w_resolution,
            u_verts_scale=1,
            v_verts_scale=1,
            w_verts_scale=1,
            half_scale=60.0,
        )

        cloth_mesh_path = Sdf.Path(omni.usd.get_stage_next_free_path(stage, "/Inflatable", True))
        omni.kit.commands.execute("MovePrim", path_from=tmp_path, path_to=cloth_mesh_path)

        cloth_mesh = UsdGeom.Mesh.Get(stage, cloth_mesh_path)
        physicsUtils.setup_transform_as_scale_orient_translate(cloth_mesh)
        physicsUtils.set_or_add_translate_op(cloth_mesh, Gf.Vec3f(0.0, 200.0, -20.0))
        physicsUtils.set_or_add_orient_op(cloth_mesh, Gf.Quatf(Gf.Rotation(Gf.Vec3d(1,0,0), 10.0).GetQuat()))
        physicsUtils.set_or_add_scale_op(cloth_mesh, Gf.Vec3f(1.0, 1.0, 0.04))

        # configure as cloth
        pressure = 8.0
        stretchStiffness = 20000.0
        bendStiffness = 100.0
        shearStiffness = 100.0
        damping = 0.5
        particleUtils.add_physx_particle_cloth(
            stage=stage,
            path=cloth_mesh_path,
            dynamic_mesh_path=None,
            particle_system_path=particle_system_path,
            spring_stretch_stiffness=stretchStiffness,
            spring_bend_stiffness=bendStiffness,
            spring_shear_stiffness=shearStiffness,
            spring_damping=damping,
            pressure=pressure,
        )

        # configure mass:
        particle_mass = 0.1
        num_verts = len(cloth_mesh.GetPointsAttr().Get())
        mass = particle_mass * num_verts
        massApi = UsdPhysics.MassAPI.Apply(cloth_mesh.GetPrim())
        massApi.GetMassAttr().Set(mass)

        # add render material:
        material_path = self.create_pbd_material("OmniPBR")
        omni.kit.commands.execute(
            "BindMaterialCommand", prim_path=cloth_mesh_path, material_path=material_path, strength=None
        )

Hi and thanks for posting. Let me get one of our physics experts to look into this code for you.

1 Like

Hi @u7013004 ,

You could just read the positions from USD.

Implement def update(self, stage, dt, viewport, physxIFace):

read the UsdGeom.Mesh from “/Cloth” and use GetPointsAttr().Get, and transform as needed with ComputeLocalToWorldTransform.

Super helpful Simon, thanks! Could you elaborate how to read the UsdGeom.Mesh from “/Cloth”? Is that a directory within the project that already exists?

Also, do I assume that I don’t have to call my update() function anyway, as long as I define it, it will automatically get called when the code executes?

“/Cloth” is the stage path of the cloth mesh.
so something like this
mesh = UsdGeom.Mesh(stage.GetPrimAtPath(“/Cloth”))
if mesh:
points = mesh.GetPointsAttr().Get()

Thanks, Simon, that clarifies things! I tested defining update(self, stage, dt, viewport, physxIFace), and I noticed that it runs every dt (1/100th of a second for FPS 100) while the application is open, rather than only when the simulation is running.

From what I understand, this is because the update function is tied to the main UI thread’s update loop, which runs at the same rate as the FPS, rather than being directly synced to the physics steps. The physics engine performs substepping independently based on the set time step per second, rather than using dt from the update function as a single step.

Does that sound correct? If so, is there a recommended way to ensure that update() only runs when the simulation is active and have access to the USD stage?

I’ve tried creating the function within the class

class ParticleInflatableDemo(demo.Base, ParticleDemoBase):
    ...
    def _on_physics_step(self,dt):
        if dt > 0:
            stage = self.stage
            cloth_mesh_path = Sdf.Path(omni.usd.get_stage_next_free_path(stage, "/Inflatable", True))
            cloth_mesh = UsdGeom.Mesh.Get(stage, cloth_mesh_path)
            print("step!", dt, stage, cloth_mesh_path)

    def create(self, stage):
        ...
        self.stage = stage
        self._stepping_sub = get_physx_interface().subscribe_physics_step_events(self._on_physics_step)

and initialising this in the create function by running self._stepping_sub = get_physx_interface().subscribe_physics_step_events(self._on_physics_step) however I am not able to access the USD stage, even if I set it to be a class variable like self.stage = stage in create().

Found the answer and fixed my bug! The issue I believe was the way I was obtaining the cloth_mesh_path. I directly found the path of the mesh at “/World/Inflatable” and accessed it in the _on_physics_step function by setting the stage to be a class variable. Worked like a charm. Code and console output below:


import carb
import math
from omni.physx.scripts import physicsUtils, particleUtils
from pxr import UsdGeom, Sdf, Gf, PhysxSchema, UsdPhysics
import omni.usd
import omni.physxdemos as demo
from .ParticleDemoBaseDemo import ParticleDemoBase
from omni.physx import get_physx_interface


class ParticleInflatableDemo(demo.Base, ParticleDemoBase):
    title = "Particle Inflatable"
    category = demo.Categories.PARTICLES
    short_description = "Basic demo showing setup of a sphere as a particle inflatable"
    description = "A basic sphere mesh is setup as a particle simulation inflatable cloth mesh."


    def create(self, stage):
        default_prim_path = self.setup_base_scene(stage, zoomAmount = 0.8)
        # configure and create particle system
        # we use all defaults, so the particle contact offset will be 5cm / 0.05m
        # so the simulation determines the other offsets from the particle contact offset
        particle_system_path = default_prim_path.AppendChild("particleSystem")
        particle_system = PhysxSchema.PhysxParticleSystem.Define(stage, particle_system_path)
        particle_system.CreateSimulationOwnerRel().SetTargets([self._scene.GetPath()])

        # create material and assign it to the system:
        particle_material_path = default_prim_path.AppendChild("particleMaterial")
        particleUtils.add_pbd_particle_material(stage, particle_material_path)
        physicsUtils.add_physics_material_to_prim(
            stage, stage.GetPrimAtPath(particle_system_path), particle_material_path
        )

        # create a mesh that is turned into an inflatable
        cube_u_resolution = 20
        cube_v_resolution = 20
        cube_w_resolution = 4

        success, tmp_path = omni.kit.commands.execute(
            "CreateMeshPrimWithDefaultXform",
            prim_type="Cube",
            u_patches=cube_u_resolution,
            v_patches=cube_v_resolution,
            w_patches=cube_w_resolution,
            u_verts_scale=1,
            v_verts_scale=1,
            w_verts_scale=1,
            half_scale=60.0,
        )

        cloth_mesh_path = Sdf.Path(omni.usd.get_stage_next_free_path(stage, "/Inflatable", True))
        omni.kit.commands.execute("MovePrim", path_from=tmp_path, path_to=cloth_mesh_path)

        cloth_mesh = UsdGeom.Mesh.Get(stage, cloth_mesh_path)

        physicsUtils.setup_transform_as_scale_orient_translate(cloth_mesh)
        physicsUtils.set_or_add_translate_op(cloth_mesh, Gf.Vec3f(0.0, 200.0, -20.0))
        physicsUtils.set_or_add_orient_op(cloth_mesh, Gf.Quatf(Gf.Rotation(Gf.Vec3d(1,0,0), 10.0).GetQuat()))
        physicsUtils.set_or_add_scale_op(cloth_mesh, Gf.Vec3f(1.0, 1.0, 0.04))

        # configure as cloth
        pressure = 8.0
        stretchStiffness = 20000.0
        bendStiffness = 100.0
        shearStiffness = 100.0
        damping = 0.5
        particleUtils.add_physx_particle_cloth(
            stage=stage,
            path=cloth_mesh_path,
            dynamic_mesh_path=None,
            particle_system_path=particle_system_path,
            spring_stretch_stiffness=stretchStiffness,
            spring_bend_stiffness=bendStiffness,
            spring_shear_stiffness=shearStiffness,
            spring_damping=damping,
            pressure=pressure,
        )

        # configure mass:
        particle_mass = 0.1
        num_verts = len(cloth_mesh.GetPointsAttr().Get())
        mass = particle_mass * num_verts
        massApi = UsdPhysics.MassAPI.Apply(cloth_mesh.GetPrim())
        massApi.GetMassAttr().Set(mass)

        # add render material:
        material_path = self.create_pbd_material("OmniPBR")
        omni.kit.commands.execute(
            "BindMaterialCommand", prim_path=cloth_mesh_path, material_path=material_path, strength=None
        )
        self.stage = stage

        self._stepping_sub = get_physx_interface().subscribe_physics_step_events(self._on_physics_step)

        points = cloth_mesh.GetPointsAttr().Get()
        print("create: points", points)
        print("create: faces", cloth_mesh.GetFaceVertexIndicesAttr().Get())
        print("create: facevtxcount", cloth_mesh.GetFaceVertexCountsAttr().Get())

    def _on_physics_step(self,dt):
        if dt > 0:
            stage = self.stage
            cloth_mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/World/Inflatable"))
            if cloth_mesh:
                points = cloth_mesh.GetPointsAttr().Get()
                print("points", points)
1 Like

Great !

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.