Configure Hydroelastic Contact for a Nut-and-Bolt Assembly#

Tool Reference

Hydroelastic contact represents interaction over a contact patch rather than only at a small set of points. This contact model is useful for conforming geometry, where the shape of the interface affects the forces, torques, and relative motion between bodies.

This walkthrough uses a threaded nut-and-bolt assembly to demonstrate how to prepare existing USD collision meshes for Newton hydroelastic contact in the Isaac Sim UI. It also distinguishes the contact properties authored on the USD assets from the collision-pipeline settings configured at runtime.

By the end of this walkthrough you will know how to:

  • Prepare existing collision meshes for hydroelastic contact from the Property panel.

  • Configure SDF detail and contact properties for conforming geometry.

  • Distinguish asset-level USD properties from runtime NewtonConfig settings.

  • Validate the authored contact pair in simulation.

For the contact model and parameter reference, see Hydroelastic Contact.

Prerequisites#

  • Isaac Sim with the Newton backend available (isaacsim.physics.newton).

Start with Newton#

Hydroelastic contact is available only with the Newton physics engine. Launch Isaac Sim with Newton enabled (for example isaacsim.newton.sh / isaacsim.newton.bat, or set the default engine to Newton), then confirm that the viewport indicates Newton is active.

The Newton schema entries under Add > Physics > Newton appear only when Newton is the active engine. Without them, you cannot apply NewtonSDFCollisionAPI from the UI.

Load the nut and bolt#

The Factory assets provide collision meshes for the example nut and bolt. Open Window > Script Editor, paste the following script, and run it. The script creates a new stage, loads both assets, and places the nut on the bolt tip with a small initial thread engagement.

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Load and position the Factory nut-and-bolt assets."""

import asyncio
import math

import isaacsim.core.experimental.utils.stage as stage_utils
import omni.kit.actions.core
import omni.kit.app
from isaacsim.core.experimental.prims import XformPrim
from isaacsim.storage.native import get_assets_root_path_async
from pxr import UsdPhysics

BOLT_PATH = "/World/bolt"
NUT_PATH = "/World/nut"
BOLT_BODY_PATH = f"{BOLT_PATH}/factory_bolt_loose"
BOLT_ROOT_JOINT_PATH = f"{BOLT_PATH}/root_joint"

BOLT_TIP_HEIGHT = 0.035
NUT_MESH_BASE_OFFSET = 0.010
NUT_ENGAGEMENT = 0.0005
NUT_YAW = math.pi / 8.0


async def load_factory_fasteners_async() -> None:
    """Create a stage and place the Factory nut on the bolt tip."""
    assets_root_path = await get_assets_root_path_async()
    if assets_root_path is None:
        raise RuntimeError("Could not find the Isaac Sim assets root.")

    await stage_utils.create_new_stage_async()
    action_registry = omni.kit.actions.core.get_action_registry()
    action_registry.get_action("omni.kit.viewport.menubar.lighting", "set_lighting_mode_camera").execute()

    factory_directory = f"{assets_root_path}/Isaac/IsaacLab/Factory"

    stage_utils.add_reference_to_stage(
        usd_path=f"{factory_directory}/factory_bolt_m16.usd",
        path=BOLT_PATH,
    )
    XformPrim(BOLT_PATH, reset_xform_op_properties=True).set_local_poses(
        translations=[[0.0, 0.0, 0.0]],
        orientations=[[1.0, 0.0, 0.0, 0.0]],
    )

    stage = stage_utils.get_current_stage(backend="usd")
    bolt_body = stage.GetPrimAtPath(BOLT_BODY_PATH)
    # Keep the bolt fixed as a static collider instead of a one-joint articulation.
    bolt_body.RemoveAPI(UsdPhysics.RigidBodyAPI)
    bolt_body.RemoveAPI(UsdPhysics.ArticulationRootAPI)
    stage.GetPrimAtPath(BOLT_ROOT_JOINT_PATH).SetActive(False)

    stage_utils.add_reference_to_stage(
        usd_path=f"{factory_directory}/factory_nut_m16.usd",
        path=NUT_PATH,
    )
    XformPrim(NUT_PATH, reset_xform_op_properties=True).set_local_poses(
        translations=[[0.0, 0.0, BOLT_TIP_HEIGHT - NUT_MESH_BASE_OFFSET - NUT_ENGAGEMENT]],
        orientations=[[math.cos(NUT_YAW * 0.5), 0.0, 0.0, math.sin(NUT_YAW * 0.5)]],
    )

    await omni.kit.app.get_app().next_update_async()


asyncio.ensure_future(load_factory_fasteners_async())

This setup uses an asynchronous function because asset-root lookup, stage creation, and the final Kit update must be awaited. The remaining Script Editor snippets on this page perform synchronous USD authoring and do not require an asynchronous wrapper.

The script also converts the bolt from a fixed-base articulation to a static collider and enables Camera Light. The nut remains dynamic so gravity can engage the threads. To enable the same lighting mode manually, open the viewport Lighting menu and select Camera Light.

Apply Newton SDF Collider#

Hydroelastic contact needs a signed distance field (SDF) on each participating collider. Applying Newton SDF Collider is the UI step that opts a mesh into that path: it adds NewtonSDFCollisionAPI, enables SDF generation for the shape, and inherits NewtonCollisionAPI and PhysicsCollisionAPI.

  1. In the Stage panel, select the bolt collision mesh at /World/bolt/factory_bolt_loose/collisions.

  2. In the Property panel, click Add > Physics > Newton > Newton SDF Collider.

  3. Repeat for the nut collision mesh at /World/nut/factory_nut_loose/collisions.

Both shapes in a pair must carry this API. Applying it on only one mesh does not produce hydroelastic contacts.

Enable hydroelastic contact#

NewtonSDFCollisionAPI alone prepares the SDF. You still have to enable hydroelastic contact on each shape and size the SDF so the helical thread is resolved. With each collider selected after applying Newton SDF Collider, set the attributes in the Property panel:

  1. Set Hydroelastic Enabled (newton:hydroelasticEnabled) to true on both the nut and the bolt colliders.

  2. Optionally set Hydroelastic Stiffness (newton:hydroelasticStiffness). Higher values produce stiffer contacts. The default is 1e10.

  3. Set SDF Max Resolution (newton:sdfMaxResolution) high enough for the thread detail. 128 is a reasonable starting value for these Factory meshes.

  4. Optionally tighten SDF Narrow Band Inner / Outer for fine features (for example -0.005 and 0.005).

  5. Set Contact Margin (newton:contactMargin) to 0.0 and Contact Gap (newton:contactGap) to 0.005 so contacts are detected before the threads interpenetrate without inflating the collision surface.

Both shapes must have newton:hydroelasticEnabled = true. Enabling the attribute on only one shape of a pair does not produce hydroelastic contacts.

The Factory assets also carry the PhysX SDF collider setup. NewtonSDFCollisionAPI supersedes it, and Newton warns that physics:approximation is ignored. Setting Approximation to none on the collider clears the warning.

After completing the UI steps, you can perform the same authoring through the Script Editor. The following script applies NewtonSDFCollisionAPI and the hydroelastic settings described above to both Factory collision meshes:

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Configure the Factory colliders for Newton hydroelastic contact."""

import omni.usd
from pxr import Sdf

COLLIDER_PATHS = (
    "/World/bolt/factory_bolt_loose/collisions",
    "/World/nut/factory_nut_loose/collisions",
)


def configure_hydroelastic_colliders() -> None:
    """Configure both fastener colliders for hydroelastic contact."""
    stage = omni.usd.get_context().get_stage()

    for collider_path in COLLIDER_PATHS:
        prim = stage.GetPrimAtPath(collider_path)
        if not prim.IsValid():
            raise RuntimeError(f"Collider prim does not exist: {collider_path}")
        if not prim.HasAPI("NewtonSDFCollisionAPI"):
            prim.ApplyAPI("NewtonSDFCollisionAPI")

        prim.GetAttribute("newton:hydroelasticEnabled").Set(True)
        prim.GetAttribute("newton:hydroelasticStiffness").Set(1.0e10)
        prim.GetAttribute("newton:sdfMaxResolution").Set(128)
        prim.GetAttribute("newton:sdfNarrowBandInner").Set(-0.005)
        prim.GetAttribute("newton:sdfNarrowBandOuter").Set(0.005)
        prim.GetAttribute("newton:contactMargin").Set(0.0)
        prim.GetAttribute("newton:contactGap").Set(0.005)
        approximation_attr = prim.GetAttribute("physics:approximation")
        if not approximation_attr.IsValid():
            approximation_attr = prim.CreateAttribute("physics:approximation", Sdf.ValueTypeNames.Token)
        approximation_attr.Set("none")


configure_hydroelastic_colliders()

Bind a contact material#

NewtonSDFCollisionAPI configures the collision representation, but it does not define friction or other material response. In this example, friction determines whether the nut turns along the thread or locks on the first engagement. With the default friction of 1.0, the nut wedges after a few millimeters.

  1. Select Create > Physics > Physics Material, choose Rigid Body Material, and create the material at /World/PhysicsMaterials/fastener.

  2. On the material, set Static Friction and Dynamic Friction to 0.01 and Restitution to 0.0.

  3. With the material selected, choose Add > Physics > Newton > Newton Material. Set Torsional Friction and Rolling Friction to 0.0, Contact Stiffness to 1e7, and Contact Damping to 1e4.

  4. Select the bolt collision mesh. In Physics Materials on Selected Models, bind /World/PhysicsMaterials/fastener.

  5. Repeat the material binding for the nut collision mesh.

The following Script Editor snippet performs the same setup. It creates the material, applies NewtonMaterialAPI, authors the USD Physics and Newton material properties, and binds the material to both collision meshes for the physics purpose.

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Author and bind a low-friction Newton contact material."""

import omni.usd
from pxr import UsdPhysics, UsdShade

COLLIDER_PATHS = (
    "/World/bolt/factory_bolt_loose/collisions",
    "/World/nut/factory_nut_loose/collisions",
)


def bind_fastener_material() -> None:
    """Create a Newton material and bind it to both fastener colliders."""
    stage = omni.usd.get_context().get_stage()

    material = UsdShade.Material.Define(stage, "/World/PhysicsMaterials/fastener")
    material_api = UsdPhysics.MaterialAPI.Apply(material.GetPrim())
    material_api.CreateStaticFrictionAttr().Set(0.01)
    material_api.CreateDynamicFrictionAttr().Set(0.01)
    material_api.CreateRestitutionAttr().Set(0.0)

    material_prim = material.GetPrim()
    if not material_prim.HasAPI("NewtonMaterialAPI"):
        material_prim.ApplyAPI("NewtonMaterialAPI")
    material_prim.GetAttribute("newton:torsionalFriction").Set(0.0)
    material_prim.GetAttribute("newton:rollingFriction").Set(0.0)
    material_prim.GetAttribute("newton:contactStiffness").Set(1.0e7)
    material_prim.GetAttribute("newton:contactDamping").Set(1.0e4)

    for collider_path in COLLIDER_PATHS:
        collider_prim = stage.GetPrimAtPath(collider_path)
        if not collider_prim.IsValid():
            raise RuntimeError(f"Collider prim does not exist: {collider_path}")
        UsdShade.MaterialBindingAPI.Apply(collider_prim).Bind(
            material,
            UsdShade.Tokens.weakerThanDescendants,
            materialPurpose="physics",
        )


bind_fastener_material()

Configure solver and pipeline settings#

The USD work above makes the shapes hydroelastic-ready. Threaded contact also depends on the simulation rate, solver settings, and runtime collision-pipeline capacity. NewtonConfig is not stored in USD, so apply this configuration after loading the stage and before pressing Play:

  • A physics rate of 240 Hz with num_substeps set to 2, so contact is resolved at 480 Hz for the 2 mm thread pitch.

  • rigid_contact_max and the MuJoCo nconmax / njmax raised well above their defaults. A hydroelastic patch on a helical thread produces hundreds of contacts per step, and the defaults (1000 pipeline contacts, 200 solver contacts) drop them, which lets the nut pass through the bolt.

  • MuJoCo iterations set to 15 and ls_iterations set to 100 to match the solver configuration used by the standalone example.

  • mc_edge_clamp_min of 0.0. The marching-cubes edge clamp biases contact surface vertices, and threading is sensitive to that bias.

  • buffer_mult_iso of 2. The iso-surface buffers must hold the full thread patch; the default size overflows and drops contacts.

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Configure solver and pipeline settings that have no USD attribute equivalent."""

import omni.kit.app
from isaacsim.core.simulation_manager import SimulationManager

ext_mgr = omni.kit.app.get_app().get_extension_manager()
ext_mgr.set_extension_enabled_immediate("isaacsim.physics.newton", True)

from isaacsim.physics.newton import (
    CollisionConfig,
    HydroelasticConfig,
    MuJoCoSolverConfig,
    NewtonConfig,
    configure_newton,
)

MAX_CONTACTS = 40_000

SimulationManager.setup_simulation(dt=1.0 / 240.0, device="cuda")

configure_newton(
    NewtonConfig(
        num_substeps=2,
        solver_cfg=MuJoCoSolverConfig(
            njmax=MAX_CONTACTS,
            nconmax=MAX_CONTACTS,
        ),
        collision_cfg=CollisionConfig(
            rigid_contact_max=MAX_CONTACTS,
            hydroelastic=HydroelasticConfig(mc_edge_clamp_min=0.0, buffer_mult_iso=2),
        ),
    )
)

The snippet sets the 240 Hz physics timestep with SimulationManager.setup_simulation() and then calls configure_newton(). Rerun it whenever you open or reload the stage. Defaults for other fields (enabled, reduce_contacts, and so on) are listed in Hydroelastic Contact and the isaacsim.physics.newton Python API.

The resulting motion is shown below.

Standalone example#

If you prefer not to author in the UI, the same scene can be built and run from a standalone script:

./python.sh standalone_examples/api/isaacsim.physics.newton/nut_bolt_hydroelastic.py

That script loads the Factory USDs, applies NewtonSDFCollisionAPI and the contact material on the colliders, configures the solver and pipeline, and then waits for you to press Play.

Additional resources#