MaterialXUSD 0.0.1
Utilities for using MaterialX with USD
Loading...
Searching...
No Matches
materialxusd.py
1import logging
2try:
3 from pxr import Usd, Sdf, UsdShade, UsdGeom, Gf, UsdLux, UsdUtils
4except ImportError:
5 self.logger.info("Error: Python module 'pxr' not found. Please ensure that the USD Python bindings are installed.")
6 exit(1)
7
8import materialxusd_custom as mxcust
9
11 '''
12 @brief Class that converts a MaterialX file to a USD file with an appropriate scene.
13 '''
14 def __init__(self):
15 '''
16 @brief Constructor for the MaterialxUSDConverter class.
17 '''
18 logging.basicConfig(level=logging.INFO)
19 self.logger = logging.getLogger('MX2USD')
20
21
22 def validate_stage(self, file:str, verboseOutput:bool=False):
23 '''
24 @brief This function validates a USD file using the ComplianceChecker.
25 @param file: The path to the USD file to validate.
26 @param verboseOutput: If True, the compliance check will output verbose information. Default is False.
27 @return: A tuple containing the errors, warnings, and failed checks.
28 '''
29 # Set up a ComplianceChecker
30 compliance_checker = UsdUtils.ComplianceChecker(
31 rootPackageOnly=False,
32 skipVariants=False,
33 verbose=verboseOutput
34 )
35
36 # Run the compliance check
37 compliance_checker.CheckCompliance(file)
38
39 # Get the results of the compliance check
40 errors = compliance_checker.GetErrors()
41 warnings = compliance_checker.GetWarnings()
42 failed_checks = compliance_checker.GetFailedChecks()
43 return errors, warnings, failed_checks
44
45 def find_first_valid_prim(self, stage):
46 '''
47 @brief This function finds the first valid prim in root layer of a stage.
48 @param stage: The stage to search for the first valid prim.
49 @return: The first valid prim found in the stage. If no valid prim is found, None is returned.
50 '''
51 # Get the root layer of the stage
52 root_layer = stage.GetRootLayer()
53
54 # Find first valid prim
55 first_prim = None
56 for prim in stage.Traverse():
57 if prim.IsValid():
58 first_prim = prim
59 break
60
61 return first_prim
62
64 '''
65 @brief This function sets the required validation attributes for the stage.
66 For now this function sets the upAxis and metersPerUnit. to Y and 1.0 respectively.
67 @param stage: The stage to set the required validation attributes.
68 '''
69 # Set the upAxis and metersPerUnit for validation
70 UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)
71 UsdGeom.SetStageMetersPerUnit(stage, 1.0)
72
73 def find_materials(self, stage, find_first:bool=True):
74 '''
75 @brief This function finds the first material in the stage. Assumes MaterialX
76 materials are stored under the "/MaterialX/Materials" scope.
77 @param stage: The stage to search for the first material.
78 @param find_first: If True, only the first material found is returned. Default is True.
79 @return: The first material found in the stage. If no material is found, None is returned.
80 '''
81 found_materials = []
82
83 # Find the first material under the "MaterialX/Materials" scope
84 materialx_prim = stage.GetPrimAtPath("/MaterialX")
85 if not materialx_prim:
86 self.logger.info("> Warning: Could not find /MaterialX scope in the USDA file.")
87 return found_materials
88
89 materials_prim = materialx_prim.GetPrimAtPath("Materials")
90 if not materials_prim:
91 self.logger.info("> Warning: Could not find /MaterialX/Materials scope in the USDA file.")
92 return found_materials
93
94 for child_prim in materials_prim.GetAllChildren():
95 if child_prim.GetTypeName() == "Material":
96 found_materials.append(child_prim)
97 if find_first:
98 break
99
100 return found_materials
101
102 def add_skydome_light(self, stage: Usd.Stage, environment_path:str, root_path:str = "/TestScene/Lights", light_name:str = "EnvironmentLight", xform_scale=Gf.Vec3f(1.3, 1.3, 1.3), xform_rotate=Gf.Vec3f(0, 0, 0)):
103 '''
104 @brief This function adds a skydome light to the stage.
105 @param stage: The stage to add the skydome light.
106 @param environment_path: The path to the environment light file.
107 @param root_path: The root path to add the skydome light.
108 @param light_name: The name of the skydome light.
109 @param xform_scale: The scale of the skydome light.
110 @param xform_rotate: The rotation of the skydome light.
111 @return: The skydome light added to the stage.
112 '''
113 skydome_prim = stage.DefinePrim(root_path, "Xform")
114 # Make the skydome prim Xformable
115 xformable = UsdGeom.Xformable(skydome_prim)
116
117 # Scale drawing of skydome
118 scale_op = xformable.GetScaleOp()
119 if not scale_op:
120 scale_op = xformable.AddScaleOp(UsdGeom.XformOp.PrecisionFloat)
121 scale_value = xform_scale
122 scale_op.Set(scale_value)
123
124 dome_light = UsdLux.DomeLight.Define(stage, f"{root_path}/{light_name}")
125
126 # Set the attributes for the DomeLight
127 dome_light.CreateIntensityAttr().Set(1.0)
128 dome_light.CreateTextureFileAttr().Set(environment_path)
129
130 # Set guideRadius
131 dome_light.CreateGuideRadiusAttr().Set(1.0)
132
133 # Rotate the light as needed.
134 xformable = UsdGeom.Xformable(dome_light)
135 xform_op = xformable.GetXformOp(UsdGeom.XformOp.TypeRotateXYZ)
136 if not xform_op:
137 xform_op = xformable.AddXformOp(UsdGeom.XformOp.TypeRotateXYZ, UsdGeom.XformOp.PrecisionFloat)
138 xform_op.Set(xform_rotate)
139
140 # Set the xformOpOrder
141 xformable.SetXformOpOrder([xform_op])
142
143 return dome_light
144
145 def add_geometry_reference(self, stage: Usd.Stage, geometry_path : str, root_path : str="/TestScene/Geometry"):
146 '''
147 @brief This function adds a geometry reference to the stage.
148 @param stage: The stage to add the geometry reference.
149 @param geometry_path: The path to the geometry file.
150 @param root_path: The root path to add the geometry reference.
151 '''
152 geom_prim = stage.DefinePrim(root_path, "Xform")
153 geom_prim.GetReferences().AddReference(geometry_path)
154 return geom_prim
155
156 def find_first_camera(self, stage : Usd.Stage):
157 '''
158 @brief This function finds the first camera in the stage.
159 @param stage: The stage to search for the first camera.
160 @return: The first camera found in the stage. If no camera is found, None is returned.
161 '''
162 # Traverse the stage's prims
163 for prim in stage.Traverse():
164 # Check if the prim is a UsdGeomCamera
165 if prim.IsA(UsdGeom.Camera):
166 return prim
167 return None
168
169 def add_camera(self, stage : Usd.Stage, camera_path : str, root_path : str="/TestScene/Camera", geometry_path : str="/TestScene/Geometry"):
170 '''
171 @brief This function adds a camera to the stage.
172 @param stage: The stage to add the camera.
173 @param camera_path: The path to the camera file.
174 @param root_path: The root path to add the camera.
175 @param geometry_path: The path to the geometry file.
176 '''
177 if camera_path:
178 camera = stage.DefinePrim(root_path, "Xform")
179 camera.GetReferences().AddReference(camera_path)
180 return camera
181
182 # Define the geometry path (e.g., a cube or any other geometry)
183 geometry_path = Sdf.Path(geometry_path)
184
185 # Get the UsdPrim for the geometry
186 geometry_prim = stage.GetPrimAtPath(geometry_path)
187
188 camera_path = Sdf.Path(root_path)
189 camera = UsdGeom.Camera.Define(stage, camera_path)
190
191 # Compute the world space bounding box of the geometry
192 bbox_cache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), [UsdGeom.Tokens.default_])
193 bbox = bbox_cache.ComputeWorldBound(geometry_prim)
194 bbox_range = bbox.ComputeAlignedRange()
195
196 # Get the center and size of the bounding box
197 bbox_center = bbox_range.GetMidpoint() # This is a Gf.Vec3d
198 bbox_size = bbox_range.GetSize() # This is a Gf.Vec3d
199
200 # Position the camera to frame the bounding box
201 # Move the camera back along the Z-axis to fit the bounding box
202 distance = max(bbox_size) * 1.5 # Adjust the multiplier as needed
203
204 # Convert bbox_center to Gf.Vec3f for compatibility with Gf.Vec3f(0, 0, distance)
205 camera_position = Gf.Vec3f(bbox_center) + Gf.Vec3f(0, 0, distance)
206 camera.AddTranslateOp().Set(camera_position)
207
208 # Orient the camera to look at the center of the bounding box
209 camera.AddRotateYOp().Set(0) # Rotate to look along the -Z axis
210 look_at = UsdGeom.XformCommonAPI(camera)
211 look_at.SetRotate((0, 0, 0)) # Ensure the camera is aligned
212
213 # Adjust the camera's field of view to fit the bounding box
214 focal_length = 35.0 # Default focal length
215 camera.GetFocalLengthAttr().Set(focal_length)
216
217 # Set the horizontal and vertical aperture to ensure proper framing
218 horizontal_aperture = bbox_size[0] / bbox_size[1] * 20.0 # Adjust based on aspect ratio
219 vertical_aperture = horizontal_aperture
220 camera.GetHorizontalApertureAttr().Set(horizontal_aperture)
221 camera.GetVerticalApertureAttr().Set(vertical_aperture)
222
223 # Set the clipping range to include the bounding box
224 near_clip = distance - max(bbox_size) * 0.5
225 far_clip = distance + max(bbox_size) * 0.5
226 camera.GetClippingRangeAttr().Set(Gf.Vec2f(near_clip, far_clip))
227
228 #camera.SetActive(True)
229
230 # Save the USD stage
231 return camera
232
233 def mtlx_to_usd(self, input_usd_path : str, shaderball_path : str, environment_path : str, material_file_path : str, camera_path : str,
234 use_custom=False):
235 '''
236 @brief This function reads the input usd file and adds the shaderball geometry and environment light
237 to the scene. It also binds the first material to the shaderball geometry. The final stage is returned.
238 @param input_usd_path: Path to the input usd file
239 @param shaderball_path: Path to the shaderball geometry file
240 @param environment_path: Path to the environment light file
241 @param material_file_path: Path to the material file. If specified will save the material file.
242 @param camera_path: Path to the camera file
243 @return: The final stage with all the elements added
244 '''
245 # Open the input USDA file
246 stage = None
247
248 if use_custom:
249 mtlx_to_usd = mxcust.MtlxToUsd(self.logger)
250 stage = mtlx_to_usd.emit(input_usd_path, False)
251 else:
252 try:
253 stage = Usd.Stage.Open(input_usd_path)
254 except Exception as e:
255 self.logger.info(f"> Error: Could not open file at {input_usd_path}. Error: {e}")
256 return stage, None, None, None, None
257
258 if not stage:
259 self.logger.info(f"> Error: Could not open file at {input_usd_path}")
260 return stage, None, None, None, None
261
262 # Set the required validation attributes
264
265 if material_file_path:
266 # Save the material file
267 self.logger.info(f"> Saving MaterialX content to: {material_file_path}")
268 stage.GetRootLayer().documentation = f"MaterialX content from {input_usd_path}"
269 stage.GetRootLayer().Export(material_file_path)
270
271 found_materials = self.find_materials(stage, False)
272 #if not found_materials:
273 # self.logger.info("Warning: No materials found under /MaterialX/Materials.")
274 #return stage, found_materials, None, None, None
275 first_material = found_materials[0] if found_materials else None
276
277 # TODO: Make this a user option...
278 SCENE_ROOT = "/TestScene"
279 GEOM_ROOT = "/TestScene/Geometry"
280 LIGHTS_ROOT = "/TestScene/Lights"
281 SKYDOME_LIGHT_NAME = "EnvironmentLight"
282
283 # Define the scene prim
284 test_scene_prim = stage.DefinePrim(SCENE_ROOT, "Xform")
285 # - Specify a default prim for validation
286 stage.SetDefaultPrim(stage.GetPrimAtPath(SCENE_ROOT))
287
288 # - Add geometry reference
289 test_geom_prim = None
290 if shaderball_path:
291 test_geom_prim = self.add_geometry_reference(stage, shaderball_path, GEOM_ROOT)
292 if test_geom_prim and first_material:
293 material_binding_api = UsdShade.MaterialBindingAPI.Apply(test_geom_prim)
294 material_binding_api.Bind(UsdShade.Material(first_material))
295 self.logger.info(f"> Geometry reference '{shaderball_path} added under: {test_scene_prim.GetPath()}.")
296
297 # Add lighting with reference to light environment file
298 # -----------------------------------------
299 dome_light = None
300 if environment_path:
301 dome_light = self.add_skydome_light(stage, environment_path, LIGHTS_ROOT, SKYDOME_LIGHT_NAME)
302 if dome_light:
303 self.logger.info(f"> Light '{environment_path}' added at path: {dome_light.GetPath()}.")
304
305 # Add camera reference
306 # -----------------------------------------
307 camera_prim = self.add_camera(stage, camera_path)
308 if camera_prim:
309 if camera_path:
310 self.logger.info(f"> Camera '{camera_path}' added at path: {camera_prim.GetPath()}.")
311 else:
312 self.logger.info(f"> Camera added at path: {camera_prim.GetPath()}.")
313
314 return stage, found_materials, test_geom_prim, dome_light, camera_prim
315
316 def get_flattend_layer(self, stage):
317 '''
318 @brief This function flattens the stage and returns the flattened layer.
319 @param stage: The stage to flatten.
320 @return: The flattened layer.
321 '''
322 return stage.Flatten()
323
324 def save_flattened_layer(self, flattened_layer, output_path:str):
325 '''
326 @brief This function saves the flattened stage to a new USD file.
327 @param flattened_layer: The flattened layer to save.
328 @param output_path: The path to save the flattened stage.
329 '''
330 flatten_path = output_path.replace(".usda", "_flattened.usda")
331 flattened_layer.documentation = f"Flattened USD file for {output_path}"
332 flattened_layer.Export(flatten_path)
333 return flatten_path
334
335 def create_usdz_package(self, usdz_file_path:str, flattened_layer):
336 '''
337 @brief This function creates a new USDZ package from a flattened layer.
338 @param usdz_file_path: The path to the USDZ package to create.
339 @param flattened_layer: The flattened layer to save to the USDZ package.
340 @return: True if the USDZ package was successfully created, False otherwise.
341 '''
342 success = False
343 error = ""
344 try:
345 success = UsdUtils.CreateNewUsdzPackage(flattened_layer.identifier, usdz_file_path)
346 if not success:
347 error = ("Failed to create USDZ package.")
348 except Exception as e:
349 error = (f"An exception occurred while creating the USDZ file: {e}")
350
351 return success, error
352
Class that converts a MaterialX file to a USD file with an appropriate scene.
create_usdz_package(self, str usdz_file_path, flattened_layer)
This function creates a new USDZ package from a flattened layer.
validate_stage(self, str file, bool verboseOutput=False)
This function validates a USD file using the ComplianceChecker.
set_required_validation_attributes(self, stage)
This function sets the required validation attributes for the stage.
find_first_valid_prim(self, stage)
This function finds the first valid prim in root layer of a stage.
__init__(self)
Constructor for the MaterialxUSDConverter class.
get_flattend_layer(self, stage)
This function flattens the stage and returns the flattened layer.
add_skydome_light(self, Usd.Stage stage, str environment_path, str root_path="/TestScene/Lights", str light_name="EnvironmentLight", xform_scale=Gf.Vec3f(1.3, 1.3, 1.3), xform_rotate=Gf.Vec3f(0, 0, 0))
This function adds a skydome light to the stage.
mtlx_to_usd(self, str input_usd_path, str shaderball_path, str environment_path, str material_file_path, str camera_path, use_custom=False)
This function reads the input usd file and adds the shaderball geometry and environment light to the ...
find_materials(self, stage, bool find_first=True)
This function finds the first material in the stage.
find_first_camera(self, Usd.Stage stage)
This function finds the first camera in the stage.
add_camera(self, Usd.Stage stage, str camera_path, str root_path="/TestScene/Camera", str geometry_path="/TestScene/Geometry")
This function adds a camera to the stage.
save_flattened_layer(self, flattened_layer, str output_path)
This function saves the flattened stage to a new USD file.
add_geometry_reference(self, Usd.Stage stage, str geometry_path, str root_path="/TestScene/Geometry")
This function adds a geometry reference to the stage.