MaterialXUSD 0.0.1
Utilities for using MaterialX with USD
Loading...
Searching...
No Matches
mtlx2usd.py
1# @brief: This script converts MaterialX file to usda file and adds inscene elements which
2# use the material. Currently only the first material is bound to a single geometry
3import argparse
4import os
5import sys
6import zipfile
7import logging
8
9import MaterialX as mx
10import materialxusd as mxusd
11import materialxusd_utils as mxusd_utils
12
13logging.basicConfig(level=logging.INFO)
14logger = logging.getLogger('mltx2usd')
15try:
16 from pxr import Usd, Sdf, UsdShade, UsdGeom, Gf, UsdLux, UsdUtils
17except ImportError:
18 logger.info("Error: Python module 'pxr' not found. Please ensure that the USD Python bindings are installed.")
19 exit(1)
20
21
22def get_mtlx_files(input_path: str):
23 mtlx_files = []
24
25 if not os.path.exists(input_path):
26 logger.info('Error: Input path does not exist.')
27 return mtlx_files
28
29 if os.path.isdir(input_path):
30 for root, dirs, files in os.walk(input_path):
31 for file in files:
32 if file.endswith(".mtlx") and not file.endswith("_converted.mtlx"):
33 mtlx_files.append(os.path.join(root, file))
34
35 else:
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"):
39 # Unzip the file and get all mtlx files
40 # Get zip file name w/o extension
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):
46 for file in files:
47 if file.endswith(".mtlx") and not file.endswith("_converted.mtlx"):
48 mtlx_files.append(os.path.join(root, file))
49 return mtlx_files
50
51def print_validation_results(output_path:str, errors:str, warnings:str, failed_checks:str):
52 if errors or warnings or failed_checks:
53 if errors:
54 logger.info(f"> Errors: {errors}")
55 if warnings:
56 logger.info(f"> Warnings: {warnings}")
57 if failed_checks:
58 logger.info(f"> Failed checks: {failed_checks}")
59 else:
60 logger.info(f'> Document "{output_path}" is valid.')
61
62def main():
63 # Set up command-line argument parsing
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.")
82
83 # Parse arguments
84 args = parser.parse_args()
85
86 # if input is a folder then get all .mtlx files under the folder recursively
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}")
90 return
91
92 validate_output = args.validate
93
94 separator = "-" * 80
95
96 # Create usd file for each mtlx file
97 for input_path in input_paths:
98
99 logger.info(separator)
100
101 # Cache this as we don't want to use the modified MaterialX document
102 # for the subfolder path to render to
103 subfolder_path = input_path
104 doc = None
105 add_frame_information = False
106 if args.preprocess:
107 logger.info(f"> Pre-processing MaterialX file: {input_path}")
108 utils = mxusd_utils.MaterialXUsdUtilities()
109 doc = utils.create_document(input_path)
110
111 # Check for time or frame nodes.
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.")
115
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")
119
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.")
127
128 materials_added = utils.add_downstream_materials(doc)
129 materials_added += utils.add_materials_for_shaders(doc)
130 if materials_added:
131 logger.info(f'> Added {materials_added} downstream materials.')
132
133 # Add explicit outputs to nodegraph outputs for shader connections
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")
137
138 # Resolve image file paths
139 # Include absolute path of the input file's folder
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)))
143 if image_paths:
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
152
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)
156 if not valid:
157 logger.warning(f"> Validation errors: {errors}")
158
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
163
164 material_file_path = ''
165 if args.material:
166 material_file_path = input_path.replace('.mtlx', '_material.usda')
167
168 # Not required as done in Python
169 #logger.info('> Converting MaterialX file to USDA file: ', input_path, material_file_path)
170 #os.system(f"usdcat {input_path} -o {material_file_path}")
171 #input_path = material_file_path
172
173 # Translate MaterialX to USD document
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}")
178 return
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}")
182 return
183
184 abs_camera_path = None
185 if args.camera == "":
186 logger.info(f"> Using computer camera from geometry.")
187 else:
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}")
191
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,
195 abs_geometry_path,
196 abs_environment_path,
197 material_file_path,
198 abs_camera_path,
199 custom_conversion)
200
201 if stage:
202 # Add start and end time by default.
203 # TODO: Try and figure out frame range required
204 if add_frame_information:
205 start_frame = 0
206 end_frame = 100
207 logger.info(f"> Add frame range: {start_frame} to {end_frame} to stage.")
208 stage.SetStartTimeCode(0)
209 stage.SetEndTimeCode(100)
210
211 output_folder, input_file = os.path.split(input_path)
212 output_file = input_file
213 unused, subfolder_file = os.path.split(subfolder_path)
214
215 if not found_materials:
216 found_materials = []
217 material_count = len(found_materials)
218 multiple_materials = material_count > 1
219 if material_count == 0:
220 # Append a dummy so that the stage will still be saved
221 # and validated, even if no materials are found.
222 found_materials.append(None)
223
224 # Iterate through all materials replacing the bound
225 # material in the test geometry. Note that we do not
226 # create a new stage for each material, but rather
227 # bind the material to the existing stage and save it
228 # to new files.
229 for found_material in found_materials:
230
231 # Replace the bound material in the test geometry
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))
236
237 # Override: Use material name as output file name instead
238 # Also use material name if multiple materials are found
239 use_material_name = args.useMaterialName
240 if multiple_materials:
241 use_material_name = True
242 if use_material_name:
243 if found_material:
244 found_material_name = found_material.GetName()
245 # Split output_path into folder and file names
246 output_file = found_material_name + ".usda"
247
248 # Append input file name (w/o extension) to output folder
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}")
256
257 output_file = output_file.replace('.mtlx', '.usda')
258 output_path = os.path.join(output_folder, output_file)
259
260 # Save the modified stage to the output USDA 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}.")
264
265 if validate_output:
266 #logger.info(f"> Validating document: {output_path}")
267 errors, warnings, failed_checks = converter.validate_stage(output_path)
268 print_validation_results(output_path, errors, warnings, failed_checks)
269
270 #if not found_material:
271 # logger.info("> Warning: No materials found in the MaterialX document. Continuing to next file.")
272 # continue
273
274 if args.render and found_material:
275
276 render_path = ''
277 if sub_folder:
278 sub_folder_path = os.path.join(sub_folder, output_file)
279 render_path = sub_folder_path.replace('.usda', f'_{args.shadingLanguage}.png')
280 else:
281 render_path = output_path.replace('.usda', f'_{args.shadingLanguage}.png')
282 render_command = f'usdrecord "{output_path}" "{render_path}" --disableCameraLight --imageWidth 512'
283 if camera_prim:
284 render_command += f' --camera "{camera_prim.GetName()}"'
285 logger.info(f"> Rendering using command: {render_command}")
286 if args.renderargs:
287 render_command += f' {args.renderargs}'
288 print('>'*20, render_command)
289 sys.stdout.flush()
290 os.system(f"{render_command} > nul 2>&1" if os.name == "nt" else f"{render_command} > /dev/null 2>&1")
291 #os.system(render_command)
292 logger.info("> Rendering complete.")
293
294 # TODO: Currently USDZ conversion is not working propertly yet
295 usdz_working = False
296 if not usdz_working:
297 args.zip = False
298
299 flattened_layer = None
300 need_flattening = args.zip or args.flatten
301 if need_flattening:
302 logger.info("> Flattening the stage.")
303 flattened_layer = converter.get_flattend_layer(stage)
304
305 if flattened_layer:
306 if args.zip:
307 # Save the flattened stage to a new USDZ package
308 usdz_file_path = input_path.replace('.mtlx', '.usdz')
309 usdz_created, error = converter.create_usdz_package(usdz_file_path, flattened_layer)
310 if not usdz_created:
311 logger.info(f"> Error: {error}")
312
313 if args.flatten:
314 # Save the flattened stage to a new USD file
315 flattend_path = converter.save_flattened_layer(flattened_layer, output_path)
316 logger.info(f"> Flattened USD file saved to: {flattend_path}.")
317
318 done_message = "-" * 80 + "\n> Done."
319 logger.info(done_message)
320
321if __name__ == "__main__":
322 main()