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 add_frame_information =
False
107 logger.info(f
"> Pre-processing MaterialX file: {input_path}")
108 utils = mxusd_utils.MaterialXUsdUtilities()
109 doc = utils.create_document(input_path)
112 if utils.has_time_frame_nodes(doc):
113 add_frame_information =
True
114 logger.info(
"> Found time or frame nodes in the MaterialX document.")
116 shader_materials_added = utils.add_materials_for_shaders(doc)
117 if shader_materials_added:
118 logger.info(f
"> Added {shader_materials_added} shader materials to the document")
120 doc.setDataLibrary(utils.get_standard_libraries())
121 implicit_nodes_added = utils.add_explicit_geometry_stream(doc)
122 if implicit_nodes_added:
123 logger.info(f
"> Added {implicit_nodes_added} explicit geometry nodes to the document")
124 num_top_level_nodes = utils.encapsulate_top_level_nodes(doc,
'root_graph')
125 if num_top_level_nodes:
126 logger.info(f
"> Encapsulated {num_top_level_nodes} top level nodes.")
128 materials_added = utils.add_downstream_materials(doc)
129 materials_added += utils.add_materials_for_shaders(doc)
131 logger.info(f
'> Added {materials_added} downstream materials.')
134 explicit_outputs_added = utils.add_nodegraph_output_qualifier_on_shaders(doc)
135 if explicit_outputs_added:
136 logger.info(f
"> Added {explicit_outputs_added} explicit outputs to nodegraph outputs for shader connections")
140 resolved_image_paths =
False
141 image_paths = args.imagepaths.split(
',')
if args.imagepaths
else []
142 image_paths.append(os.path.dirname(os.path.abspath(input_path)))
144 beforeDoc = mx.prettyPrint(doc)
145 mx_image_search_path = utils.create_FileSearchPath(image_paths)
146 utils.resolve_image_file_paths(doc, mx_image_search_path)
147 afterDoc = mx.prettyPrint(doc)
148 if beforeDoc != afterDoc:
149 resolved_image_paths =
True
150 logger.info(f
"> Resolved image file paths using search paths: {mx_image_search_path.asString()}")
151 resolved_image_paths =
True
153 if explicit_outputs_added
or resolved_image_paths
or materials_added > 0
or num_top_level_nodes > 0
or implicit_nodes_added > 0:
154 valid, errors = doc.validate()
155 doc.setDataLibrary(
None)
157 logger.warning(f
"> Validation errors: {errors}")
159 new_input_path = input_path.replace(
'.mtlx',
'_converted.mtlx')
160 utils.write_document(doc, new_input_path)
161 logger.info(f
"> Saved converted MaterialX document to: {new_input_path}")
162 input_path = new_input_path
164 material_file_path =
''
166 material_file_path = input_path.replace(
'.mtlx',
'_material.usda')
174 logger.info(f
"> Build tests scene from material scene: {input_path}")
175 abs_geometry_path = os.path.abspath(args.geometry)
176 if not os.path.exists(abs_geometry_path):
177 logger.info(f
"> Error: Geometry file not found at {abs_geometry_path}")
179 abs_environment_path = os.path.abspath(args.environment)
180 if not os.path.exists(abs_environment_path):
181 logger.info(f
"> Error: Environment file not found at {abs_environment_path}")
184 abs_camera_path =
None
185 if args.camera ==
"":
186 logger.info(f
"> Using computer camera from geometry.")
188 abs_camera_path = os.path.abspath(args.camera)
189 if not os.path.exists(abs_camera_path):
190 logger.info(f
"> Camera file not found at {abs_camera_path}")
192 converter = mxusd.MaterialxUSDConverter()
193 custom_conversion = args.custom
194 stage, found_materials, test_geom_prim, dome_light, camera_prim = converter.mtlx_to_usd(input_path,
196 abs_environment_path,
204 if add_frame_information:
207 logger.info(f
"> Add frame range: {start_frame} to {end_frame} to stage.")
208 stage.SetStartTimeCode(0)
209 stage.SetEndTimeCode(100)
211 output_folder, input_file = os.path.split(input_path)
212 output_file = input_file
213 unused, subfolder_file = os.path.split(subfolder_path)
215 if not found_materials:
217 material_count = len(found_materials)
218 multiple_materials = material_count > 1
219 if material_count == 0:
222 found_materials.append(
None)
229 for found_material
in found_materials:
232 if test_geom_prim
and found_material:
233 logger.info(f
"> Bind material to geometry: {found_material.GetName()} to {test_geom_prim.GetPath()}")
234 material_binding_api = UsdShade.MaterialBindingAPI(test_geom_prim)
235 material_binding_api.Bind(UsdShade.Material(found_material))
239 use_material_name = args.useMaterialName
240 if multiple_materials:
241 use_material_name =
True
242 if use_material_name:
244 found_material_name = found_material.GetName()
246 output_file = found_material_name +
".usda"
249 sub_folder = output_folder
250 if args.render
and args.subfolder:
251 subfolder_name = os.path.join(output_folder, subfolder_file.replace(
'.mtlx',
''))
252 if not os.path.exists(subfolder_name):
253 os.makedirs(subfolder_name)
254 sub_folder = subfolder_name
255 logger.info(f
"> Override output folder: {subfolder_name}")
257 output_file = output_file.replace(
'.mtlx',
'.usda')
258 output_path = os.path.join(output_folder, output_file)
261 stage.GetRootLayer().documentation = f
"Combined content from: {input_path}, {abs_geometry_path}, {abs_environment_path}."
262 stage.GetRootLayer().Export(output_path)
263 logger.info(f
"> Save USD file to: {output_path}.")
267 errors, warnings, failed_checks = converter.validate_stage(output_path)
268 print_validation_results(output_path, errors, warnings, failed_checks)
274 if args.render
and found_material:
278 sub_folder_path = os.path.join(sub_folder, output_file)
279 render_path = sub_folder_path.replace(
'.usda', f
'_{args.shadingLanguage}.png')
281 render_path = output_path.replace(
'.usda', f
'_{args.shadingLanguage}.png')
282 render_command = f
'usdrecord "{output_path}" "{render_path}" --disableCameraLight --imageWidth 512'
284 render_command += f
' --camera "{camera_prim.GetName()}"'
285 logger.info(f
"> Rendering using command: {render_command}")
287 render_command += f
' {args.renderargs}'
288 print(
'>'*20, render_command)
290 os.system(f
"{render_command} > nul 2>&1" if os.name ==
"nt" else f
"{render_command} > /dev/null 2>&1")
292 logger.info(
"> Rendering complete.")
299 flattened_layer =
None
300 need_flattening = args.zip
or args.flatten
302 logger.info(
"> Flattening the stage.")
303 flattened_layer = converter.get_flattend_layer(stage)
308 usdz_file_path = input_path.replace(
'.mtlx',
'.usdz')
309 usdz_created, error = converter.create_usdz_package(usdz_file_path, flattened_layer)
311 logger.info(f
"> Error: {error}")
315 flattend_path = converter.save_flattened_layer(flattened_layer, output_path)
316 logger.info(f
"> Flattened USD file saved to: {flattend_path}.")
318 done_message =
"-" * 80 +
"\n> Done."
319 logger.info(done_message)
321if __name__ ==
"__main__":