[BUG? Report]+[Fix] _create_mesh_shell in OgnCreateProjectionMaterial does not copy essential target_prim mesh primvars and properties

Hello NVIDIA Community,

Version isaac_sim-2023.1.1

During development encountered strange behavior of materials applied to “shell” which is created by _create_mesh_shell in extscache/omni.replicator.core-1.10.20+105.1.lx64.r.cp310/omni/replicator/core/ogn/python/_impl/nodes/OgnCreateProjectionMaterial.py.

Using the following script in console you can check that “shell” mesh with applied material doesn’t look normal.

from pathlib import Path

import numpy as np
import omni
import omni.kit.material.library as mat_lib
import omni.replicator.core as rep
import omni.usd
from pxr import Gf, Sdf, Semantics, Usd, UsdGeom, UsdShade


def _create_mesh_shell(target_prim, path):
    """Create a mesh shell from input mesh(es)

    Concatenate points, polygons and normals and create a new mesh at the specified path.
    """
    stage = omni.usd.get_context().get_stage()
    projection_prim = stage.DefinePrim(path, "Mesh")
    vert_combined = []
    face_vert_counts_combined = []
    face_vert_idx_combined = []
    normals_combined = []
    cur_time = 0.0
    target_prim_to_world = UsdGeom.Xformable(target_prim).ComputeLocalToWorldTransform(
        cur_time
    )
    vert_idx_offset = 0
    descendants = [target_prim]
    prototype_tfs = {}
    while descendants:
        cur_prim = descendants.pop()
        if cur_prim.HasAttribute("replicatorProjection"):
            continue
        if cur_prim.IsInstanceable():
            prototype_tfs[cur_prim.GetPrototype().GetName()] = UsdGeom.Xformable(
                cur_prim
            ).ComputeLocalToWorldTransform(cur_time)
            descendants.append(cur_prim.GetPrototype())
        elif cur_prim.GetTypeName() != "Mesh":
            descendants.extend(cur_prim.GetChildren())
        else:
            mesh_to_world = UsdGeom.Xformable(cur_prim).ComputeLocalToWorldTransform(
                cur_time
            )
            root_path = str(cur_prim.GetPrimPath()).split("/", 2)[1]
            if cur_prim.IsInPrototype() and root_path in prototype_tfs:
                mesh_to_world = mesh_to_world * prototype_tfs.get(root_path)
            mesh_to_target = mesh_to_world * target_prim_to_world.GetInverse()
            points_raw = np.array(cur_prim.GetAttribute("points").Get())
            points_transformed = (
                np.pad(points_raw, ((0, 0), (0, 1)), constant_values=1) @ mesh_to_target
            )[..., :3]
            vert_combined.append(points_transformed)
            face_vert_counts_combined.extend(
                cur_prim.GetAttribute("faceVertexCounts").Get()
            )
            face_vert_idx_raw = np.array(
                cur_prim.GetAttribute("faceVertexIndices").Get()
            )
            face_vert_idx = face_vert_idx_raw + vert_idx_offset
            face_vert_idx_combined.append(face_vert_idx)
            normals_combined.extend(cur_prim.GetAttribute("normals").Get())
            vert_idx_offset += len(points_transformed)

    projection_prim.GetAttribute("points").Set(np.vstack(vert_combined))
    projection_prim.GetAttribute("faceVertexCounts").Set(
        np.array(face_vert_counts_combined)
    )
    projection_prim.GetAttribute("faceVertexIndices").Set(
        np.hstack(face_vert_idx_combined)
    )
    projection_prim.GetAttribute("normals").Set(np.array(normals_combined))
    return projection_prim


def _create_mesh_shell_mod(target_prim, path):
    """Create a mesh shell from input mesh(es)

    Concatenate points, polygons and normals and create a new mesh at the specified path.
    """
    stage = omni.usd.get_context().get_stage()
    projection_prim = stage.DefinePrim(path, "Mesh")
    vert_combined = []
    face_vert_counts_combined = []
    face_vert_idx_combined = []
    normals_combined = []
    cur_time = 0.0
    target_prim_to_world = UsdGeom.Xformable(target_prim).ComputeLocalToWorldTransform(
        cur_time
    )
    vert_idx_offset = 0
    descendants = [target_prim]
    prototype_tfs = {}
    while descendants:
        cur_prim = descendants.pop()
        if cur_prim.HasAttribute("replicatorProjection"):
            continue
        if cur_prim.IsInstanceable():
            prototype_tfs[cur_prim.GetPrototype().GetName()] = UsdGeom.Xformable(
                cur_prim
            ).ComputeLocalToWorldTransform(cur_time)
            descendants.append(cur_prim.GetPrototype())
        elif cur_prim.GetTypeName() != "Mesh":
            descendants.extend(cur_prim.GetChildren())
        else:
            mesh_to_world = UsdGeom.Xformable(cur_prim).ComputeLocalToWorldTransform(
                cur_time
            )
            root_path = str(cur_prim.GetPrimPath()).split("/", 2)[1]
            if cur_prim.IsInPrototype() and root_path in prototype_tfs:
                mesh_to_world = mesh_to_world * prototype_tfs.get(root_path)
            mesh_to_target = mesh_to_world * target_prim_to_world.GetInverse()
            points_raw = np.array(cur_prim.GetAttribute("points").Get())
            points_transformed = (
                np.pad(points_raw, ((0, 0), (0, 1)), constant_values=1) @ mesh_to_target
            )[..., :3]
            vert_combined.append(points_transformed)
            face_vert_counts_combined.extend(
                cur_prim.GetAttribute("faceVertexCounts").Get()
            )
            face_vert_idx_raw = np.array(
                cur_prim.GetAttribute("faceVertexIndices").Get()
            )
            face_vert_idx = face_vert_idx_raw + vert_idx_offset
            face_vert_idx_combined.append(face_vert_idx)
            normals_combined.extend(cur_prim.GetAttribute("normals").Get())
            vert_idx_offset += len(points_transformed)

    projection_prim.GetAttribute("points").Set(np.vstack(vert_combined))
    projection_prim.GetAttribute("faceVertexCounts").Set(
        np.array(face_vert_counts_combined)
    )
    projection_prim.GetAttribute("faceVertexIndices").Set(
        np.hstack(face_vert_idx_combined)
    )
    projection_prim.GetAttribute("normals").Set(np.array(normals_combined))

    # Copy any primvar data from the src to the dst mesh, as they can contain
    # essential data for render engine
    for pv in UsdGeom.PrimvarsAPI(target_prim).GetPrimvars():
        _primvar = UsdGeom.PrimvarsAPI(projection_prim).CreatePrimvar(
            pv.GetName(), pv.GetTypeName()
        )
        _primvar.SetInterpolation(pv.GetInterpolation())
        if pv.HasValue():
            _primvar.Set(pv.Get())

    projection_prim_mesh = UsdGeom.Mesh(projection_prim)
    target_prim_mesh = UsdGeom.Mesh(target_prim)
    # set mesh normals interpolation and subdivision
    projection_prim_mesh.SetNormalsInterpolation(
        target_prim_mesh.GetNormalsInterpolation()
    )
    projection_prim_mesh.CreateSubdivisionSchemeAttr("none")
    return projection_prim


stage = omni.usd.get_context().get_stage()

# Create parent prim
omni.kit.commands.create("CreateMeshPrimCommand", prim_type="Cylinder").do()
parent_prim = stage.GetPrimAtPath("/World/Cylinder")

# Create Light
# rep.create.plane(scale=(10, 10, 1))
rep.create.light(rotation=(0, 45, 0), intensity=3000, light_type="distant")
rep.create.light(position=(1, 0, 1), intensity=10000, scale=0.1, light_type="sphere")

# Assign material
_mdl = "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Materials/Base/Masonry/Brick_Pavers.mdl"
on_mat = lambda mat_prim: UsdShade.MaterialBindingAPI(parent_prim).Bind(
    UsdShade.Material(mat_prim), UsdShade.Tokens.weakerThanDescendants
)
mat_lib.create_mdl_material(stage, _mdl, "Brick_Pavers", on_mat)
UsdShade.MaterialBindingAPI.Apply(parent_prim)

# Make mesh copy
copy_prim = _create_mesh_shell(parent_prim, "/World/Copy")
# Fixed version
# copy_prim = _create_mesh_shell_mod(parent_prim, "/World/Copy")

on_mat = lambda mat_prim: UsdShade.MaterialBindingAPI(copy_prim).Bind(
    UsdShade.Material(mat_prim), UsdShade.Tokens.weakerThanDescendants
)
mat_lib.create_mdl_material(stage, _mdl, "Brick_Pavers", on_mat)
UsdShade.MaterialBindingAPI.Apply(copy_prim)

Example:

If you enable debug mode view for Tangent U or Texture Coordinates you will see that on “shell” mesh those are flipped/missing.


I cannot say that this lack of Parent primvars on “shell” mesh is correct, because depending on material you want to use with this node in your project you can get very strange behavior like on example.

The fix is provided within above code in _create_mesh_shell_mod. Solution is similar to _copy_base_mesh included in omni.kit.tools module /ov/pkg/isaac_sim-2023.1.1/extscache/omni.kit.tools.mergemesh-0.1.6/omni/kit/tools/mergemesh/merge_mesh.py.

And the result is 1:1 copy of target_prim mesh with all primvars and some additional mesh settings:

@pcallender
Could you please check this with devs also.

This is an excellent catch. Unfortunately this isn’t directly a Replicator code issue, so this needs to go through another team’s priorities. I’ll update when I hear anything worth reporting.