10import materialxusd
as mxusd
11import materialxusd_utils
as mxusd_utils
13logging.basicConfig(level=logging.INFO)
14logger = logging.getLogger(
'mltx2usd')
16 from pxr
import Usd, Sdf, UsdShade, UsdGeom, Gf, UsdLux, UsdUtils
18 logger.info(
"Error: Python module 'pxr' not found. Please ensure that the USD Python bindings are installed.")
22def get_mtlx_files(input_path: str):
25 if not os.path.exists(input_path):
26 logger.info(
'Error: Input path does not exist.')
29 if os.path.isdir(input_path):
30 for root, dirs, files
in os.walk(input_path):
32 if file.endswith(
".mtlx")
and not file.endswith(
"_converted.mtlx"):
33 mtlx_files.append(os.path.join(root, file))
36 if input_path.endswith(
".mtlx")
and not input_path.endswith(
"_converted.mtlx"):
37 mtlx_files.append(input_path)
38 elif input_path.endswith(
".zip"):
41 output_path = input_path.replace(
'.zip',
'')
42 with zipfile.ZipFile(input_path,
'r')
as zip_ref:
43 zip_ref.extractall(output_path)
44 logger.info(
'> Extracted zip file to: {output_path}')
45 for root, dirs, files
in os.walk(output_path):
47 if file.endswith(
".mtlx")
and not file.endswith(
"_converted.mtlx"):
48 mtlx_files.append(os.path.join(root, file))
51def print_validation_results(output_path:str, errors:str, warnings:str, failed_checks:str):
52 if errors
or warnings
or failed_checks:
54 logger.info(f
"> Errors: {errors}")
56 logger.info(f
"> Warnings: {warnings}")
58 logger.info(f
"> Failed checks: {failed_checks}")
60 logger.info(f
'> Document "{output_path}" is valid.')
64 parser = argparse.ArgumentParser(description=
"Convert MaterialX file to usda file with references to scene elements.")
65 parser.add_argument(
"input_file", help=
"Path to the input MaterialX file. If a folder is ")
66 parser.add_argument(
"-o",
"--output_file", default=
None, help=
"Path to the output USDA file.")
67 parser.add_argument(
"-c",
"--camera", default=
"./data/camera.usda", help=
"Path to the camera USD file (default: ./data/camera.usda).")
68 parser.add_argument(
"-g",
"--geometry", default=
"./data/shaderball.usd", help=
"Path to the geometry USD file (default: ./data/shaderball.usda).")
69 parser.add_argument(
"-e",
"--environment", default=
"./data/san_giuseppe_bridge.hdr", help=
"Path to the environment USD file (default: ./data/san_giuseppe_bridge.hdr).")
70 parser.add_argument(
"-f",
"--flatten", action=
"store_true", help=
"Flatten the final USD file.")
71 parser.add_argument(
"-m",
"--material", action=
"store_true", help=
"Save USD file with just MaterialX content.")
72 parser.add_argument(
"-z",
"--zip", action=
"store_true", help=
"Create a USDZ final file.")
73 parser.add_argument(
"-v",
"--validate", action=
"store_true", help=
"Validate output documents.")
74 parser.add_argument(
"-r",
"--render", action=
"store_true", help=
"Render the final stage.")
75 parser.add_argument(
"-sl",
"--shadingLanguage", help=
"Shading language string.", default=
"glslfx")
76 parser.add_argument(
"-mn",
"--useMaterialName", action=
"store_true", help=
"Set output file to material name.")
77 parser.add_argument(
"-sf",
"--subfolder", action=
"store_true", help=
"Save output to subfolder named <input materialx file> w/o extension.")
78 parser.add_argument(
"-pp",
"--preprocess", action=
"store_true", help=
"Attempt to pre-process the MaterialX file.")
79 parser.add_argument(
"-ip",
"--imagepaths", default=
"", help=
"Comma separated list of search paths for image path resolving. ")
80 parser.add_argument(
"-ra",
"--renderargs", default=
"", help=
"Additional render arguments.")
81 parser.add_argument(
"-cst",
"--custom", action=
"store_true", help=
"Use custom MaterialX USD conversion.")
84 args = parser.parse_args()
87 input_paths = get_mtlx_files(args.input_file)
88 if len(input_paths) == 0:
89 logger.info(f
"Error: No MaterialX files found in {args.input_file}")
92 validate_output = args.validate
97 for input_path
in input_paths:
99 logger.info(separator)
103 subfolder_path = input_path
105 logger.info(f
"> Pre-processing MaterialX file: {input_path}")
106 utils = mxusd_utils.MaterialXUsdUtilities()
107 doc = utils.create_document(input_path)
109 shader_materials_added = utils.add_materials_for_shaders(doc)
110 if shader_materials_added:
111 logger.info(f
"> Added {shader_materials_added} shader materials to the document")
113 doc.setDataLibrary(utils.get_standard_libraries())
114 implicit_nodes_added = utils.add_explicit_geometry_stream(doc)
115 if implicit_nodes_added:
116 logger.info(f
"> Added {implicit_nodes_added} explicit geometry nodes to the document")
117 num_top_level_nodes = utils.encapsulate_top_level_nodes(doc,
'root_graph')
118 if num_top_level_nodes:
119 logger.info(f
"> Encapsulated {num_top_level_nodes} top level nodes.")
121 materials_added = utils.add_downstream_materials(doc)
122 materials_added += utils.add_materials_for_shaders(doc)
124 logger.info(f
'> Added {materials_added} downstream materials.')
127 explicit_outputs_added = utils.add_nodegraph_output_qualifier_on_shaders(doc)
128 if explicit_outputs_added:
129 logger.info(f
"> Added {explicit_outputs_added} explicit outputs to nodegraph outputs for shader connections")
133 resolved_image_paths =
False
134 image_paths = args.imagepaths.split(
',')
if args.imagepaths
else []
135 image_paths.append(os.path.dirname(os.path.abspath(input_path)))
137 beforeDoc = mx.prettyPrint(doc)
138 mx_image_search_path = utils.create_FileSearchPath(image_paths)
139 utils.resolve_image_file_paths(doc, mx_image_search_path)
140 afterDoc = mx.prettyPrint(doc)
141 if beforeDoc != afterDoc:
142 resolved_image_paths =
True
143 logger.info(f
"> Resolved image file paths using search paths: {mx_image_search_path.asString()}")
144 resolved_image_paths =
True
146 if explicit_outputs_added
or resolved_image_paths
or materials_added > 0
or num_top_level_nodes > 0
or implicit_nodes_added > 0:
147 valid, errors = doc.validate()
148 doc.setDataLibrary(
None)
150 logger.warning(f
"> Validation errors: {errors}")
152 new_input_path = input_path.replace(
'.mtlx',
'_converted.mtlx')
153 utils.write_document(doc, new_input_path)
154 logger.info(f
"> Saved converted MaterialX document to: {new_input_path}")
155 input_path = new_input_path
157 material_file_path =
''
159 material_file_path = input_path.replace(
'.mtlx',
'_material.usda')
167 logger.info(f
"> Build tests scene from material scene: {input_path}")
168 abs_geometry_path = os.path.abspath(args.geometry)
169 if not os.path.exists(abs_geometry_path):
170 logger.info(f
"> Error: Geometry file not found at {abs_geometry_path}")
172 abs_environment_path = os.path.abspath(args.environment)
173 if not os.path.exists(abs_environment_path):
174 logger.info(f
"> Error: Environment file not found at {abs_environment_path}")
177 abs_camera_path =
None
178 if args.camera ==
"":
179 logger.info(f
"> Using computer camera from geometry.")
181 abs_camera_path = os.path.abspath(args.camera)
182 if not os.path.exists(abs_camera_path):
183 logger.info(f
"> Camera file not found at {abs_camera_path}")
185 converter = mxusd.MaterialxUSDConverter()
186 custom_conversion = args.custom
187 stage, found_materials, test_geom_prim, dome_light, camera_prim = converter.mtlx_to_usd(input_path,
189 abs_environment_path,
195 output_folder, input_file = os.path.split(input_path)
196 output_file = input_file
197 unused, subfolder_file = os.path.split(subfolder_path)
199 if not found_materials:
201 material_count = len(found_materials)
202 multiple_materials = material_count > 1
203 if material_count == 0:
206 found_materials.append(
None)
213 for found_material
in found_materials:
216 if test_geom_prim
and found_material:
217 logger.info(f
"> Bind material to geometry: {found_material.GetName()} to {test_geom_prim.GetPath()}")
218 material_binding_api = UsdShade.MaterialBindingAPI(test_geom_prim)
219 material_binding_api.Bind(UsdShade.Material(found_material))
223 use_material_name = args.useMaterialName
224 if multiple_materials:
225 use_material_name =
True
226 if use_material_name:
228 found_material_name = found_material.GetName()
230 output_file = found_material_name +
".usda"
233 sub_folder = output_folder
234 if args.render
and args.subfolder:
235 subfolder_name = os.path.join(output_folder, subfolder_file.replace(
'.mtlx',
''))
236 if not os.path.exists(subfolder_name):
237 os.makedirs(subfolder_name)
238 sub_folder = subfolder_name
239 logger.info(f
"> Override output folder: {subfolder_name}")
241 output_file = output_file.replace(
'.mtlx',
'.usda')
242 output_path = os.path.join(output_folder, output_file)
245 stage.GetRootLayer().documentation = f
"Combined content from: {input_path}, {abs_geometry_path}, {abs_environment_path}."
246 stage.GetRootLayer().Export(output_path)
247 logger.info(f
"> Save USD file to: {output_path}.")
251 errors, warnings, failed_checks = converter.validate_stage(output_path)
252 print_validation_results(output_path, errors, warnings, failed_checks)
258 if args.render
and found_material:
262 sub_folder_path = os.path.join(sub_folder, output_file)
263 render_path = sub_folder_path.replace(
'.usda', f
'_{args.shadingLanguage}.png')
265 render_path = output_path.replace(
'.usda', f
'_{args.shadingLanguage}.png')
266 render_command = f
'usdrecord "{output_path}" "{render_path}" --disableCameraLight --imageWidth 512'
268 render_command += f
' --camera "{camera_prim.GetName()}"'
269 logger.info(f
"> Rendering using command: {render_command}")
271 render_command += f
' {args.renderargs}'
272 print(
'>'*20, render_command)
274 os.system(f
"{render_command} > nul 2>&1" if os.name ==
"nt" else f
"{render_command} > /dev/null 2>&1")
276 logger.info(
"> Rendering complete.")
283 flattened_layer =
None
284 need_flattening = args.zip
or args.flatten
286 logger.info(
"> Flattening the stage.")
287 flattened_layer = converter.get_flattend_layer(stage)
292 usdz_file_path = input_path.replace(
'.mtlx',
'.usdz')
293 usdz_created, error = converter.create_usdz_package(usdz_file_path, flattened_layer)
295 logger.info(f
"> Error: {error}")
299 flattend_path = converter.save_flattened_layer(flattened_layer, output_path)
300 logger.info(f
"> Flattened USD file saved to: {flattend_path}.")
302 done_message =
"-" * 80 +
"\n> Done."
303 logger.info(done_message)
305if __name__ ==
"__main__":