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 if args.preprocess:
105 logger.info(f"> Pre-processing MaterialX file: {input_path}")
106 utils = mxusd_utils.MaterialXUsdUtilities()
107 doc = utils.create_document(input_path)
108
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")
112
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.")
120
121 materials_added = utils.add_downstream_materials(doc)
122 materials_added += utils.add_materials_for_shaders(doc)
123 if materials_added:
124 logger.info(f'> Added {materials_added} downstream materials.')
125
126 # Add explicit outputs to nodegraph outputs for shader connections
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")
130
131 # Resolve image file paths
132 # Include absolute path of the input file's folder
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)))
136 if image_paths:
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
145
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)
149 if not valid:
150 logger.warning(f"> Validation errors: {errors}")
151
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
156
157 material_file_path = ''
158 if args.material:
159 material_file_path = input_path.replace('.mtlx', '_material.usda')
160
161 # Not required as done in Python
162 #logger.info('> Converting MaterialX file to USDA file: ', input_path, material_file_path)
163 #os.system(f"usdcat {input_path} -o {material_file_path}")
164 #input_path = material_file_path
165
166 # Translate MaterialX to USD document
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}")
171 return
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}")
175 return
176
177 abs_camera_path = None
178 if args.camera == "":
179 logger.info(f"> Using computer camera from geometry.")
180 else:
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}")
184
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,
188 abs_geometry_path,
189 abs_environment_path,
190 material_file_path,
191 abs_camera_path,
192 custom_conversion)
193
194 if stage:
195 output_folder, input_file = os.path.split(input_path)
196 output_file = input_file
197 unused, subfolder_file = os.path.split(subfolder_path)
198
199 if not found_materials:
200 found_materials = []
201 material_count = len(found_materials)
202 multiple_materials = material_count > 1
203 if material_count == 0:
204 # Append a dummy so that the stage will still be saved
205 # and validated, even if no materials are found.
206 found_materials.append(None)
207
208 # Iterate through all materials replacing the bound
209 # material in the test geometry. Note that we do not
210 # create a new stage for each material, but rather
211 # bind the material to the existing stage and save it
212 # to new files.
213 for found_material in found_materials:
214
215 # Replace the bound material in the test geometry
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))
220
221 # Override: Use material name as output file name instead
222 # Also use material name if multiple materials are found
223 use_material_name = args.useMaterialName
224 if multiple_materials:
225 use_material_name = True
226 if use_material_name:
227 if found_material:
228 found_material_name = found_material.GetName()
229 # Split output_path into folder and file names
230 output_file = found_material_name + ".usda"
231
232 # Append input file name (w/o extension) to output folder
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}")
240
241 output_file = output_file.replace('.mtlx', '.usda')
242 output_path = os.path.join(output_folder, output_file)
243
244 # Save the modified stage to the output USDA 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}.")
248
249 if validate_output:
250 #logger.info(f"> Validating document: {output_path}")
251 errors, warnings, failed_checks = converter.validate_stage(output_path)
252 print_validation_results(output_path, errors, warnings, failed_checks)
253
254 #if not found_material:
255 # logger.info("> Warning: No materials found in the MaterialX document. Continuing to next file.")
256 # continue
257
258 if args.render and found_material:
259
260 render_path = ''
261 if sub_folder:
262 sub_folder_path = os.path.join(sub_folder, output_file)
263 render_path = sub_folder_path.replace('.usda', f'_{args.shadingLanguage}.png')
264 else:
265 render_path = output_path.replace('.usda', f'_{args.shadingLanguage}.png')
266 render_command = f'usdrecord "{output_path}" "{render_path}" --disableCameraLight --imageWidth 512'
267 if camera_prim:
268 render_command += f' --camera "{camera_prim.GetName()}"'
269 logger.info(f"> Rendering using command: {render_command}")
270 if args.renderargs:
271 render_command += f' {args.renderargs}'
272 print('>'*20, render_command)
273 sys.stdout.flush()
274 os.system(f"{render_command} > nul 2>&1" if os.name == "nt" else f"{render_command} > /dev/null 2>&1")
275 #os.system(render_command)
276 logger.info("> Rendering complete.")
277
278 # TODO: Currently USDZ conversion is not working propertly yet
279 usdz_working = False
280 if not usdz_working:
281 args.zip = False
282
283 flattened_layer = None
284 need_flattening = args.zip or args.flatten
285 if need_flattening:
286 logger.info("> Flattening the stage.")
287 flattened_layer = converter.get_flattend_layer(stage)
288
289 if flattened_layer:
290 if args.zip:
291 # Save the flattened stage to a new USDZ package
292 usdz_file_path = input_path.replace('.mtlx', '.usdz')
293 usdz_created, error = converter.create_usdz_package(usdz_file_path, flattened_layer)
294 if not usdz_created:
295 logger.info(f"> Error: {error}")
296
297 if args.flatten:
298 # Save the flattened stage to a new USD file
299 flattend_path = converter.save_flattened_layer(flattened_layer, output_path)
300 logger.info(f"> Flattened USD file saved to: {flattend_path}.")
301
302 done_message = "-" * 80 + "\n> Done."
303 logger.info(done_message)
304
305if __name__ == "__main__":
306 main()