From 02428e104c908832e76b2ea5bae8f7956ca9459b Mon Sep 17 00:00:00 2001 From: Ahmed Hindy Date: Sun, 31 May 2026 02:07:21 +0300 Subject: [PATCH] Add Blender texture graph fidelity --- .../dcc/blender/traverser.py | 54 ++++++-- src/materials_processor/mappings.py | 35 +++++ src/materials_processor/usd/graph_builder.py | 45 +++++- src/materials_processor/usd/mappings.py | 17 +++ tests/test_blender_runtime.py | 71 ++++++++++ tests/test_blender_support.py | 125 +++++++++++++++++ tests/test_usd_json_conversion.py | 131 +++++++++++++++++- 7 files changed, 460 insertions(+), 18 deletions(-) diff --git a/src/materials_processor/dcc/blender/traverser.py b/src/materials_processor/dcc/blender/traverser.py index 218a23d..1ab7bdf 100644 --- a/src/materials_processor/dcc/blender/traverser.py +++ b/src/materials_processor/dcc/blender/traverser.py @@ -12,6 +12,36 @@ bpy = None +def _socket_generic_type(socket, *, is_output=False, node=None): + """Return the closest generic parameter type for a Blender socket.""" + socket_type = socket.type.lower() + if socket_type == "value": + return "float1" + if socket_type == "vector": + if is_output and getattr(node, "bl_idname", None) == "ShaderNodeUVMap": + return "vector2" + return "vector3" + if socket_type == "rgba": + return "color3" if is_output else "color4" + if socket_type == "shader": + return "shader" + return "float1" + + +def _resolve_blender_image_path(image): + """Resolve Blender image paths relative to the current blend file when possible.""" + filepath = getattr(image, "filepath", "") + if not filepath: + return filepath + if bpy is None: + return filepath + try: + return bpy.path.abspath(filepath) + except Exception as exc: + logger.warning("Failed to resolve Blender image path '%s': %s", filepath, exc) + return filepath + + class BlenderNodeTraverser: """Class for traversing Blender material node trees to extract their connections and output nodes.""" @@ -165,21 +195,10 @@ def _convert_parms_to_dict(node): elif not isinstance(val, str) and hasattr(val, "__iter__"): val = list(val) - # Map Blender socket type names to generic types - socket_type = socket.type.lower() - if socket_type == "value": - generic_type = "float1" - elif socket_type == "vector": - generic_type = "vector3" - elif socket_type == "rgba": - generic_type = "color4" - else: - generic_type = "float1" - parms["input"].append({ "generic_name": socket.name, "value": val, - "type": generic_type, + "type": _socket_generic_type(socket, node=node), "direction": "input", }) @@ -187,7 +206,14 @@ def _convert_parms_to_dict(node): if node.bl_idname == "ShaderNodeTexImage" and getattr(node, "image", None): parms["input"].append({ "generic_name": "image", - "value": node.image.filepath, + "value": _resolve_blender_image_path(node.image), + "type": "string1", + "direction": "input" + }) + elif node.bl_idname == "ShaderNodeUVMap": + parms["input"].append({ + "generic_name": "uv_map", + "value": getattr(node, "uv_map", ""), "type": "string1", "direction": "input" }) @@ -207,7 +233,7 @@ def _convert_parms_to_dict(node): parms["output"].append({ "generic_name": socket.name, "value": None, - "type": "color3" if socket.type == "RGBA" else "float1", + "type": _socket_generic_type(socket, is_output=True, node=node), "direction": "output", }) diff --git a/src/materials_processor/mappings.py b/src/materials_processor/mappings.py index 94de526..f474cee 100644 --- a/src/materials_processor/mappings.py +++ b/src/materials_processor/mappings.py @@ -258,6 +258,8 @@ 'ND_image_float': 'GENERIC::image', 'ND_image_color3': 'GENERIC::image', 'ND_normalmap_vector3': 'GENERIC::normalmap', + 'ND_geompropvalue_vector2': 'GENERIC::uvmap', + 'ND_separate3_color3': 'GENERIC::separate_color', 'ND_colorcorrect_color3': 'GENERIC::color_correct', 'ND_range_float': 'GENERIC::range', 'ND_bump_vector3': 'GENERIC::displacement', @@ -282,6 +284,8 @@ 'ND_image_float': 'GENERIC::image', 'ND_image_color3': 'GENERIC::image', 'ND_normalmap_vector3': 'GENERIC::normalmap', + 'ND_geompropvalue_vector2': 'GENERIC::uvmap', + 'ND_separate3_color3': 'GENERIC::separate_color', 'ND_colorcorrect_color3': 'GENERIC::color_correct', 'ND_range_float': 'GENERIC::range', 'ND_bump_vector3': 'GENERIC::displacement', @@ -309,6 +313,8 @@ 'blender_shader_nodes': { 'ShaderNodeBsdfPrincipled': 'GENERIC::standard_surface', 'ShaderNodeTexImage': 'GENERIC::image', + 'ShaderNodeUVMap': 'GENERIC::uvmap', + 'ShaderNodeSeparateColor': 'GENERIC::separate_color', 'ShaderNodeNormalMap': 'GENERIC::normalmap', 'ShaderNodeBump': 'GENERIC::displacement', 'ShaderNodeOutputMaterial': 'GENERIC::output_node', @@ -377,6 +383,7 @@ def convert_generic(node_type: str, 'mtlximage': { 'signature': 'signature', 'file': 'filename', + 'texcoord': 'texcoord', 'out': 'rgb', }, 'mtlxcolorcorrect': { @@ -450,6 +457,8 @@ def convert_generic(node_type: str, 'ND_image_float': { 'signature': 'signature', 'file': 'filename', + 'texcoord': 'texcoord', + 'out': 'rgb', }, 'ND_range_float': { 'in': 'in', @@ -470,6 +479,19 @@ def convert_generic(node_type: str, 'ND_image_color3': { 'signature': 'signature', 'file': 'filename', + 'texcoord': 'texcoord', + 'out': 'rgb', + }, + 'ND_geompropvalue_vector2': { + 'geomprop': 'uv_map', + 'default': 'default', + 'out': 'vector', + }, + 'ND_separate3_color3': { + 'in': 'rgb', + 'outr': 'r', + 'outg': 'g', + 'outb': 'b', }, 'ND_normalmap_vector3': { 'in': 'in', @@ -685,7 +707,20 @@ def convert_generic(node_type: str, }, 'ShaderNodeTexImage': { 'image': 'filename', + 'Vector': 'texcoord', + 'Color': 'rgb', + 'Alpha': 'alpha', + }, + 'ShaderNodeUVMap': { + 'uv_map': 'uv_map', + 'UV': 'vector', + }, + 'ShaderNodeSeparateColor': { 'Color': 'rgb', + 'Red': 'r', + 'Green': 'g', + 'Blue': 'b', + 'Alpha': 'alpha', }, 'ShaderNodeNormalMap': { 'Color': 'in', diff --git a/src/materials_processor/usd/graph_builder.py b/src/materials_processor/usd/graph_builder.py index ebbb7b1..a26f309 100644 --- a/src/materials_processor/usd/graph_builder.py +++ b/src/materials_processor/usd/graph_builder.py @@ -76,6 +76,43 @@ def _create_shader_id(self, shader, generic_type): return True return False + def _nodeinfo_by_path(self): + """Return a flat lookup of source node paths to generic node infos.""" + lookup = {} + + def visit(nodes): + for nodeinfo in nodes: + lookup[nodeinfo.node_path] = nodeinfo + if nodeinfo.children_list: + visit(nodeinfo.children_list) + + visit(self.nodeinfo_list) + return lookup + + def _renderer_parm_name(self, generic_node_type, generic_parm_name): + """Translate a generic socket name to the target USD renderer socket name.""" + if not generic_node_type or not generic_parm_name: + return generic_parm_name + + renderer_node_type = convert_generic( + node_type=generic_node_type, + target_renderer=self.target_renderer, + profile='usd_prims', + ) + if not renderer_node_type: + return generic_parm_name + + std_parm_map = REGULAR_PARAM_NAMES_TO_GENERIC.get(renderer_node_type.replace('::', ':'), {}) + for renderer_name, mapped_generic_name in std_parm_map.items(): + if mapped_generic_name == generic_parm_name: + return renderer_name + return generic_parm_name + + def _connection_parm_name(self, endpoint, nodeinfo_by_path): + nodeinfo = nodeinfo_by_path.get(endpoint.node_path) + generic_node_type = nodeinfo.node_type if nodeinfo else endpoint.node_type + return self._renderer_parm_name(generic_node_type, endpoint.parm_name) + def _apply_parameters(self, shader, node_type, parameters): """ Map generic parameters over to renderer-specific USD inputs. @@ -259,12 +296,13 @@ def set_shader_connections(self, nodeinfo_list, parent_node=None): """ Connect child shader prims based on stored connection_tasks. """ + nodeinfo_by_path = self._nodeinfo_by_path() for nodeinfo in nodeinfo_list: for conn_index, conn in nodeinfo.connection_info.items(): src_path = self.old_new_map.get(conn.input.node_path) dst_path = self.old_new_map.get(conn.output.node_path) - src_parm = conn.input.parm_name - dst_parm = conn.output.parm_name + src_parm = self._connection_parm_name(conn.input, nodeinfo_by_path) + dst_parm = self._connection_parm_name(conn.output, nodeinfo_by_path) src_prim = self.stage.GetPrimAtPath(Sdf.Path(src_path)) if src_path else None dst_prim = self.stage.GetPrimAtPath(Sdf.Path(dst_path)) if dst_path else None @@ -288,7 +326,8 @@ def set_shader_connections(self, nodeinfo_list, parent_node=None): logger.debug("new_src_prim=%s", new_src_prim) logger.debug("new_conn: %s", pprint.pformat(new_conn, sort_dicts=False)) - self._connect_pair(new_src_prim, dst_prim, new_conn.input.parm_name, dst_parm) + new_src_parm = self._connection_parm_name(new_conn.input, nodeinfo_by_path) + self._connect_pair(new_src_prim, dst_prim, new_src_parm, dst_parm) continue diff --git a/src/materials_processor/usd/mappings.py b/src/materials_processor/usd/mappings.py index 67356ac..62b5dd1 100644 --- a/src/materials_processor/usd/mappings.py +++ b/src/materials_processor/usd/mappings.py @@ -52,6 +52,20 @@ 'rs_usd_material_builder': 'redshift::BumpMap', }, }, + 'GENERIC::uvmap': { + 'prim_type': 'Shader', + 'info_id': { + 'mtlx': 'ND_geompropvalue_vector2', + 'openpbr': 'ND_geompropvalue_vector2', + }, + }, + 'GENERIC::separate_color': { + 'prim_type': 'Shader', + 'info_id': { + 'mtlx': 'ND_separate3_color3', + 'openpbr': 'ND_separate3_color3', + }, + }, 'GENERIC::range': { 'prim_type': 'Shader', 'info_id': { @@ -199,6 +213,9 @@ 'float2': Sdf.ValueTypeNames.Float2, 'float3': Sdf.ValueTypeNames.Float3, 'float4': Sdf.ValueTypeNames.Float4, + 'vector2': Sdf.ValueTypeNames.TexCoord2f, + 'vector3': Sdf.ValueTypeNames.Vector3f, + 'vector4': Sdf.ValueTypeNames.Float4, 'bool': Sdf.ValueTypeNames.Bool, 'bool1': Sdf.ValueTypeNames.Bool, 'str': Sdf.ValueTypeNames.String, diff --git a/tests/test_blender_runtime.py b/tests/test_blender_runtime.py index b3d4179..09dcb87 100644 --- a/tests/test_blender_runtime.py +++ b/tests/test_blender_runtime.py @@ -13,12 +13,18 @@ BlenderRuntime, _default_package_src, _default_blender_root, + _parse_prefixed_output, + _run_blender_python, resolve_blender_runtime, validate_blender_material_smoke, validate_blender_runtime, ) ROOT = Path(__file__).resolve().parents[1] +LOCAL_TIE_DEFENDER_BLEND = Path( + r"F:\Users\Ahmed Hindy\Downloads\vfx_tmp\X-Ripper Stuff\Tie Defender\Tie Defender.blend" +) +TIE_DEFENDER_RESULT_PREFIX = "MATERIALS_PROCESSOR_TIE_DEFENDER=" def _fake_blender_root(tmp_path): @@ -162,3 +168,68 @@ def test_validate_local_blender_runtime_when_available(): assert validated.python_version assert smoke["recreated"] is True assert "ShaderNodeBsdfPrincipled" in smoke["target_node_types"] + + +@pytest.mark.blender +def test_ingests_local_tie_defender_packed_material_when_available(): + if not LOCAL_TIE_DEFENDER_BLEND.is_file(): + pytest.skip(f"Tie Defender blend file is missing: {LOCAL_TIE_DEFENDER_BLEND}") + try: + runtime = resolve_blender_runtime(version=None) + except FileNotFoundError as exc: + pytest.skip(str(exc)) + + code = f""" +import json + +import bpy + +from materials_processor.dcc.blender.adapters import BlenderMaterialReader + + +def iter_nodes(nodes): + for node in nodes: + yield node + yield from iter_nodes(node.children_list) + + +bpy.ops.wm.open_mainfile(filepath={str(LOCAL_TIE_DEFENDER_BLEND)!r}) +material = bpy.data.materials["T_TieDefender_01_CS_Mat"] +graph = BlenderMaterialReader().read(material) +nodes = list(iter_nodes(graph.nodeinfo_list)) +texture_paths = [ + parameter.value + for node in nodes + for parameter in (node.parameters or []) + if parameter.generic_name == "filename" +] +result = {{ + "material_name": graph.material_name, + "node_types": sorted(set(node.node_type for node in nodes)), + "output_keys": sorted(graph.output_connections), + "texture_paths": sorted(texture_paths), +}} +print({TIE_DEFENDER_RESULT_PREFIX!r} + json.dumps(result, sort_keys=True)) +""".strip() + + completed = _run_blender_python(runtime, code, ROOT / "src", timeout=180) + if completed.returncode != 0: + raise RuntimeError( + "Tie Defender Blender ingest failed with exit code " + f"{completed.returncode}.\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}" + ) + + result = _parse_prefixed_output( + completed.stdout, + completed.stderr, + TIE_DEFENDER_RESULT_PREFIX, + "Tie Defender ingest", + ) + assert result["material_name"] == "T_TieDefender_01_CS_Mat" + assert "GENERIC::standard_surface" in result["node_types"] + assert "GENERIC::image" in result["node_types"] + assert "GENERIC::uvmap" in result["node_types"] + assert "GENERIC::separate_color" in result["node_types"] + assert "GENERIC::output_surface" in result["output_keys"] + assert result["texture_paths"] + assert all(not texture_path.startswith("//") for texture_path in result["texture_paths"]) diff --git a/tests/test_blender_support.py b/tests/test_blender_support.py index 34f73e6..5f4584d 100644 --- a/tests/test_blender_support.py +++ b/tests/test_blender_support.py @@ -23,6 +23,16 @@ def test_blender_profile_maps_generic_nodes_without_becoming_houdini_target(): "blender", profile="blender_shader_nodes", ) == "ShaderNodeBsdfPrincipled" + assert mappings.convert_generic( + "GENERIC::uvmap", + "blender", + profile="blender_shader_nodes", + ) == "ShaderNodeUVMap" + assert mappings.convert_generic( + "GENERIC::separate_color", + "blender", + profile="blender_shader_nodes", + ) == "ShaderNodeSeparateColor" assert "blender" not in mappings.FORMAT_CHOICES @@ -61,6 +71,8 @@ def __init__(self, bl_idname, name, inputs=None, outputs=None): # Mock Image Texture image field if bl_idname == "ShaderNodeTexImage": self.image = type("Image", (), {"filepath": "C:/textures/diffuse.png"})() + elif bl_idname == "ShaderNodeUVMap": + self.uv_map = "UVMap" class FakeNodeTree: @@ -162,6 +174,68 @@ def _make_simple_fake_blender_material(name="test_mat"): return FakeMaterial(name, node_tree) +def _link(from_node, from_socket, to_node, to_socket): + from_socket.node = from_node + to_socket.node = to_node + from_socket.is_linked = True + to_socket.is_linked = True + link = FakeLink(from_node, from_socket, to_node, to_socket) + from_socket.links.append(link) + to_socket.links.append(link) + return link + + +def _iter_nodeinfos(nodeinfos): + for nodeinfo in nodeinfos: + yield nodeinfo + yield from _iter_nodeinfos(nodeinfo.children_list) + + +def _make_packed_texture_fake_blender_material(name="packed_mat"): + out_node = FakeNode("ShaderNodeOutputMaterial", "Material Output") + bsdf_node = FakeNode("ShaderNodeBsdfPrincipled", "Principled BSDF") + tex_node = FakeNode("ShaderNodeTexImage", "Packed Texture") + uv_node = FakeNode("ShaderNodeUVMap", "UV Map") + separate_node = FakeNode("ShaderNodeSeparateColor", "Separate Color") + + out_surf_socket = FakeSocket("Surface", "SHADER") + bsdf_out_socket = FakeSocket("BSDF", "SHADER") + bsdf_base_socket = FakeSocket("Base Color", "RGBA") + bsdf_metallic_socket = FakeSocket("Metallic", "VALUE") + bsdf_roughness_socket = FakeSocket("Roughness", "VALUE") + tex_vector_socket = FakeSocket("Vector", "VECTOR") + tex_color_socket = FakeSocket("Color", "RGBA") + tex_alpha_socket = FakeSocket("Alpha", "VALUE") + uv_socket = FakeSocket("UV", "VECTOR") + separate_color_socket = FakeSocket("Color", "RGBA") + separate_green_socket = FakeSocket("Green", "VALUE") + separate_blue_socket = FakeSocket("Blue", "VALUE") + + out_node.inputs = [out_surf_socket] + bsdf_node.outputs = [bsdf_out_socket] + bsdf_node.inputs = [bsdf_base_socket, bsdf_metallic_socket, bsdf_roughness_socket] + tex_node.inputs = [tex_vector_socket] + tex_node.outputs = [tex_color_socket, tex_alpha_socket] + uv_node.outputs = [uv_socket] + separate_node.inputs = [separate_color_socket] + separate_node.outputs = [separate_green_socket, separate_blue_socket] + + links = [ + _link(bsdf_node, bsdf_out_socket, out_node, out_surf_socket), + _link(tex_node, tex_color_socket, bsdf_node, bsdf_base_socket), + _link(tex_node, tex_color_socket, separate_node, separate_color_socket), + _link(uv_node, uv_socket, tex_node, tex_vector_socket), + _link(separate_node, separate_blue_socket, bsdf_node, bsdf_metallic_socket), + _link(separate_node, separate_green_socket, bsdf_node, bsdf_roughness_socket), + ] + + node_tree = FakeNodeTree( + nodes=[out_node, bsdf_node, tex_node, uv_node, separate_node], + links=links, + ) + return FakeMaterial(name, node_tree) + + def test_blender_traverser_simple(): """Test that BlenderNodeTraverser processes Cycles material trees correctly.""" material = _make_simple_fake_blender_material() @@ -187,6 +261,57 @@ def test_blender_traverser_simple(): assert output_connections["GENERIC::output_surface"].connected_node_name == "Principled BSDF" +def test_blender_traverser_preserves_packed_texture_graph(caplog): + """Test that texture coordinates and packed channel splits survive standardization.""" + material = _make_packed_texture_fake_blender_material() + + nodes_dict, output_dict = BlenderNodeTraverser(material).run() + nodeinfo_list, _ = standardizer.NodeStandardizer( + traversed_nodes_dict=nodes_dict, + output_nodes_dict=output_dict, + material_type="blender", + source_type="blender_shader_nodes", + ).run() + + all_nodes = list(_iter_nodeinfos(nodeinfo_list)) + assert {node.node_type for node in all_nodes} >= { + "GENERIC::standard_surface", + "GENERIC::image", + "GENERIC::uvmap", + "GENERIC::separate_color", + } + + image_node = next(node for node in all_nodes if node.node_type == "GENERIC::image") + image_params = {param.generic_name: param for param in image_node.parameters} + assert image_params["filename"].value == "C:/textures/diffuse.png" + assert {"rgb", "alpha"} <= {param.generic_name for param in image_node.parameters} + + uv_node = next(node for node in all_nodes if node.node_type == "GENERIC::uvmap") + uv_params = {param.generic_name: param for param in uv_node.parameters} + assert uv_params["uv_map"].value == "UVMap" + assert uv_params["vector"].generic_type == "vector2" + + connections = [ + connection + for node in all_nodes + for connection in node.connection_info.values() + ] + assert any( + connection.input.parm_name == "vector" and connection.output.parm_name == "texcoord" + for connection in connections + ) + assert any( + connection.input.parm_name == "b" and connection.output.parm_name == "metalness" + for connection in connections + ) + assert any( + connection.input.parm_name == "g" and connection.output.parm_name == "specular_roughness" + for connection in connections + ) + assert "No generic type was found for node type: 'ShaderNodeUVMap'" not in caplog.text + assert "No generic type was found for node type: 'ShaderNodeSeparateColor'" not in caplog.text + + def test_blender_recreator_simple(): """Test that BlenderNodeRecreator successfully reconstructs material nodes.""" surface_param = NodeParameter( diff --git a/tests/test_usd_json_conversion.py b/tests/test_usd_json_conversion.py index 1a7250a..8f106a2 100644 --- a/tests/test_usd_json_conversion.py +++ b/tests/test_usd_json_conversion.py @@ -8,7 +8,7 @@ from pxr import Sdf, Usd, UsdShade from materials_processor import io as material_io -from materials_processor.core.graph import NodeInfo, OutputConnection +from materials_processor.core.graph import ConnectionEndpoint, NodeConnection, NodeInfo, NodeParameter, OutputConnection from materials_processor.standardizer import NodeStandardizer from materials_processor.usd.recreator import USDMaterialRecreator from materials_processor.usd.traverser import USDTraverser @@ -279,6 +279,135 @@ def test_usd_recreator_sanitizes_material_and_shader_prim_names(): assert "outputs:mtlx:surface" in {output.GetFullName() for output in material.GetOutputs()} +def test_usd_recreator_maps_blender_texture_coordinate_and_channel_sockets(): + stage = Usd.Stage.CreateInMemory() + surface = NodeInfo( + node_type="GENERIC::standard_surface", + node_name="Principled BSDF", + node_path="/mat/packed/Principled BSDF", + parameters=[], + connection_info={}, + children_list=[], + ) + separate = NodeInfo( + node_type="GENERIC::separate_color", + node_name="Separate Color", + node_path="/mat/packed/Separate Color", + parameters=[], + connection_info={ + "connection_0": NodeConnection( + input=ConnectionEndpoint( + node_name="Separate Color", + node_path="/mat/packed/Separate Color", + node_type="ShaderNodeSeparateColor", + node_index=0, + parm_name="b", + ), + output=ConnectionEndpoint( + node_name="Principled BSDF", + node_path="/mat/packed/Principled BSDF", + node_type="ShaderNodeBsdfPrincipled", + node_index=0, + parm_name="metalness", + ), + ) + }, + children_list=[], + ) + image = NodeInfo( + node_type="GENERIC::image", + node_name="Packed Texture", + node_path="/mat/packed/Packed Texture", + parameters=[ + NodeParameter("filename", "string1", "input", "C:/textures/packed.png"), + NodeParameter("signature", "string1", "input", "color3"), + ], + connection_info={ + "connection_0": NodeConnection( + input=ConnectionEndpoint( + node_name="Packed Texture", + node_path="/mat/packed/Packed Texture", + node_type="ShaderNodeTexImage", + node_index=0, + parm_name="rgb", + ), + output=ConnectionEndpoint( + node_name="Separate Color", + node_path="/mat/packed/Separate Color", + node_type="ShaderNodeSeparateColor", + node_index=0, + parm_name="rgb", + ), + ) + }, + children_list=[], + ) + uvmap = NodeInfo( + node_type="GENERIC::uvmap", + node_name="UV Map", + node_path="/mat/packed/UV Map", + parameters=[NodeParameter("uv_map", "string1", "input", "UVMap")], + connection_info={ + "connection_0": NodeConnection( + input=ConnectionEndpoint( + node_name="UV Map", + node_path="/mat/packed/UV Map", + node_type="ShaderNodeUVMap", + node_index=0, + parm_name="vector", + ), + output=ConnectionEndpoint( + node_name="Packed Texture", + node_path="/mat/packed/Packed Texture", + node_type="ShaderNodeTexImage", + node_index=0, + parm_name="texcoord", + ), + ) + }, + children_list=[], + ) + image.children_list.append(uvmap) + separate.children_list.append(image) + surface.children_list.append(separate) + output_connection = OutputConnection( + node_name="Material Output", + node_path="/mat/packed/Material Output", + connected_node_name="Principled BSDF", + connected_node_path="/mat/packed/Principled BSDF", + connected_input_index=0, + connected_input_name="Surface", + connected_output_name="surface", + ) + + USDMaterialRecreator( + stage=stage, + material_name="packed", + nodeinfo_list=[surface], + output_connections={"GENERIC::output_surface": output_connection}, + target_renderer="mtlx", + ).run() + + assert { + "ND_standard_surface_surfaceshader", + "ND_image_color3", + "ND_geompropvalue_vector2", + "ND_separate3_color3", + } <= _shader_ids(stage) + + image_shader = UsdShade.Shader.Get(stage, Sdf.Path("/materials/packed/Packed_Texture")) + separate_shader = UsdShade.Shader.Get(stage, Sdf.Path("/materials/packed/Separate_Color")) + surface_shader = UsdShade.Shader.Get(stage, Sdf.Path("/materials/packed/Principled_BSDF")) + + assert image_shader.GetInput("texcoord").GetAttr().GetConnections()[0].pathString.endswith("/UV_Map.outputs:out") + assert separate_shader.GetInput("in").GetAttr().GetConnections()[0].pathString.endswith( + "/Packed_Texture.outputs:out" + ) + assert surface_shader.GetInput("metalness").GetAttr().GetConnections()[0].pathString.endswith( + "/Separate_Color.outputs:outb" + ) + + def test_usd_recreator_legacy_texture_collect_builder_smoke(): stage = Usd.Stage.CreateInMemory() recreator = USDMaterialRecreator(