Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions newton/_src/usd/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,7 @@ def _extract_shader_properties(shader: UsdShade.Shader | None, prim: Usd.Prim) -
shader,
(
"diffuse_color_constant",
"diffuse_tint",
"diffuse_color",
"diffuseColor",
"base_color",
Expand Down Expand Up @@ -1835,6 +1836,18 @@ def resolve_material_properties_for_prim(prim: Usd.Prim) -> dict[str, Any]:
if fallback_props is not None:
return fallback_props

# Last resort: check displayColor primvar even when no material is bound.
# This covers collision-only prims whose colour is set via displayColor
# rather than through a material binding (e.g. ground planes).
if UsdGeom is not None:
display_color = UsdGeom.PrimvarsAPI(prim).GetPrimvar("displayColor")
if display_color:
color = _coerce_color(display_color.Get())
if color is not None:
props = _empty_material_properties()
props["color"] = color
return props

return _empty_material_properties()


Expand Down
110 changes: 97 additions & 13 deletions newton/_src/utils/import_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,14 @@ def _get_mesh_with_visual_material(prim: Usd.Prim, *, path_name: str) -> Mesh:
mesh.texture = texture
if mesh.texture is not None and mesh.uvs is None:
warnings.warn(
f"Warning: mesh {path_name} has a texture but no UVs; texture will be ignored.",
f"Mesh {path_name} has a texture but no UVs; texture will use projected UVs.",
stacklevel=2,
)
mesh.texture = None
if material_props.get("color") is not None and mesh.texture is None:
if mesh.texture is not None:
# Texture provides albedo; use white so it renders at full brightness.
# (diffuse_color_constant is the untextured fallback, not a multiplier.)
mesh.color = (1.0, 1.0, 1.0)
elif material_props.get("color") is not None:
mesh.color = material_props["color"]
if material_props.get("roughness") is not None:
mesh.roughness = material_props["roughness"]
Expand All @@ -461,6 +464,42 @@ def _get_prim_world_mat(prim, articulation_root_xform, incoming_world_xform):
prim_world_mat = incoming_mat @ prim_world_mat
return prim_world_mat

def _cube_to_textured_mesh(hx: float, hy: float, hz: float, material_props: dict) -> Mesh:
"""Convert a cube primitive to a Mesh with box-mapped UVs for texture rendering.

Creates 24 vertices (4 per face) with independent UVs so each face
maps the full texture via simple box projection.
"""
# 6 faces, 4 verts each, 2 tris each — winding is CCW when viewed from outside
verts = np.array([
# +X face
[hx, -hy, -hz], [hx, hy, -hz], [hx, hy, hz], [hx, -hy, hz],
# -X face
[-hx, hy, -hz], [-hx, -hy, -hz], [-hx, -hy, hz], [-hx, hy, hz],
# +Y face
[hx, hy, -hz], [-hx, hy, -hz], [-hx, hy, hz], [hx, hy, hz],
# -Y face
[-hx, -hy, -hz], [hx, -hy, -hz], [hx, -hy, hz], [-hx, -hy, hz],
# +Z face
[-hx, -hy, hz], [hx, -hy, hz], [hx, hy, hz], [-hx, hy, hz],
# -Z face
[-hx, hy, -hz], [hx, hy, -hz], [hx, -hy, -hz], [-hx, -hy, -hz],
], dtype=np.float32)
uvs = np.tile(np.array([[0, 0], [1, 0], [1, 1], [0, 1]], dtype=np.float32), (6, 1))
tris = np.array([[i * 4 + j for j in t] for i in range(6) for t in [[0, 1, 2], [0, 2, 3]]], dtype=np.int32)
mesh = Mesh(verts, tris, uvs=uvs, compute_inertia=False)
mesh.texture = material_props.get("texture")
# When a texture is present, the texture provides the albedo and the
# shape color acts as a multiplier. Use white so the texture renders
# at full brightness (OmniPBR's diffuse_color_constant is the
# untextured fallback and would darken the image if used here).
mesh.color = (1.0, 1.0, 1.0)
if material_props.get("roughness") is not None:
mesh.roughness = material_props["roughness"]
if material_props.get("metallic") is not None:
mesh.metallic = material_props["metallic"]
return mesh

def _load_visual_shapes_impl(
parent_body_id: int,
prim: Usd.Prim,
Expand Down Expand Up @@ -524,19 +563,47 @@ def _load_visual_shapes_impl(
return

if path_name not in path_shape_map:
# Resolve material properties for primitive (non-mesh) visual shapes.
# Walk up to the parent if the prim itself has no material bound,
# since USD materials are often authored on a parent Xform.
_prim_mat_props = {}
if type_name != "mesh":
_prim_mat_props = _get_material_props_cached(prim)
if _prim_mat_props.get("color") is None and _prim_mat_props.get("texture") is None:
parent = prim.GetParent()
if parent and parent.IsValid():
_par_props = _get_material_props_cached(parent)
if _par_props.get("color") is not None or _par_props.get("texture") is not None:
_prim_mat_props = _par_props
_prim_color = _prim_mat_props.get("color")

if type_name == "cube":
size = usd.get_float(prim, "size", 2.0)
side_lengths = scale * size
shape_id = builder.add_shape_box(
parent_body_id,
xform,
hx=side_lengths[0] / 2,
hy=side_lengths[1] / 2,
hz=side_lengths[2] / 2,
cfg=visual_shape_cfg,
as_site=is_site,
label=path_name,
)
hx, hy, hz = side_lengths[0] / 2, side_lengths[1] / 2, side_lengths[2] / 2
# If the material has a texture, convert the cube to a mesh
# with UVs so Newton's texture pipeline can render it.
if _prim_mat_props.get("texture") is not None:
mesh = _cube_to_textured_mesh(hx, hy, hz, _prim_mat_props)
shape_id = builder.add_shape_mesh(
parent_body_id,
xform,
mesh=mesh,
cfg=visual_shape_cfg,
label=path_name,
)
else:
shape_id = builder.add_shape_box(
parent_body_id,
xform,
hx=hx,
hy=hy,
hz=hz,
cfg=visual_shape_cfg,
as_site=is_site,
color=_prim_color,
label=path_name,
)
elif type_name == "sphere":
if not (scale[0] == scale[1] == scale[2]):
print("Warning: Non-uniform scaling of spheres is not supported.")
Expand All @@ -547,6 +614,7 @@ def _load_visual_shapes_impl(
radius,
cfg=visual_shape_cfg,
as_site=is_site,
color=_prim_color,
label=path_name,
)
elif type_name == "plane":
Expand All @@ -562,6 +630,7 @@ def _load_visual_shapes_impl(
width=width,
length=length,
cfg=visual_shape_cfg,
color=_prim_color,
label=path_name,
)
elif type_name == "capsule":
Expand All @@ -577,6 +646,7 @@ def _load_visual_shapes_impl(
half_height,
cfg=visual_shape_cfg,
as_site=is_site,
color=_prim_color,
label=path_name,
)
elif type_name == "cylinder":
Expand All @@ -592,6 +662,7 @@ def _load_visual_shapes_impl(
half_height,
cfg=visual_shape_cfg,
as_site=is_site,
color=_prim_color,
label=path_name,
)
elif type_name == "cone":
Expand Down Expand Up @@ -2151,9 +2222,22 @@ def _build_mass_info_from_shape_geometry(
if shape_kd is None:
shape_kd = builder.default_shape_cfg.kd

# Resolve material color for visible collision shapes so they
# render with the correct colour instead of the palette fallback.
# Walk up to the parent if the prim itself has no material.
_collider_color = None
if collider_is_visible:
_coll_mat = _get_material_props_cached(prim)
_collider_color = _coll_mat.get("color")
if _collider_color is None:
_coll_parent = prim.GetParent()
if _coll_parent and _coll_parent.IsValid():
_collider_color = _get_material_props_cached(_coll_parent).get("color")

shape_params = {
"body": body_id,
"xform": shape_xform,
"color": _collider_color,
Comment on lines +2225 to +2240
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t override textured mesh collider albedo with an explicit shape color.

For visible MeshShape colliders, _get_mesh_with_visual_material() already forces mesh.color to white when a texture exists. ModelBuilder.add_shape() prefers the explicit color argument over src.color, so wiring _collider_color into shape_params re-tints/darkens textured colliders.

💡 Suggested fix
-                _collider_color = None
+                _collider_color = None
+                _collider_texture = None
                 if collider_is_visible:
                     _coll_mat = _get_material_props_cached(prim)
                     _collider_color = _coll_mat.get("color")
+                    _collider_texture = _coll_mat.get("texture")
                     if _collider_color is None:
                         _coll_parent = prim.GetParent()
                         if _coll_parent and _coll_parent.IsValid():
-                            _collider_color = _get_material_props_cached(_coll_parent).get("color")
+                            _coll_parent_props = _get_material_props_cached(_coll_parent)
+                            _collider_color = _coll_parent_props.get("color")
+                            _collider_texture = _collider_texture or _coll_parent_props.get("texture")

                 shape_params = {
                     "body": body_id,
                     "xform": shape_xform,
-                    "color": _collider_color,
+                    "color": None
+                    if key == UsdPhysics.ObjectType.MeshShape and _collider_texture is not None
+                    else _collider_color,
                     "cfg": ModelBuilder.ShapeConfig(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/utils/import_usd.py` around lines 2225 - 2240, The code is
incorrectly forcing an explicit "color" into shape_params which overrides
textured mesh albedo; change the logic around _collider_color so you only add
"color" to shape_params when the resolved material has no texture (i.e., when
_get_material_props_cached(prim) or its parent returns a color and does not
contain a texture entry). In practice, inspect the material props returned by
_get_material_props_cached (used here and by _get_mesh_with_visual_material())
and only set shape_params["color"] when that material has no texture; otherwise
omit the "color" key so ModelBuilder.add_shape() will use src.color (preserving
textures).

"cfg": ModelBuilder.ShapeConfig(
ke=shape_ke,
kd=shape_kd,
Expand Down
Loading