1# 2# Copyright 2018 Pixar 3# 4# Licensed under the Apache License, Version 2.0 (the "Apache License") 5# with the following modification; you may not use this file except in 6# compliance with the Apache License and the following modification to it: 7# Section 6. Trademarks. is deleted and replaced with: 8# 9# 6. Trademarks. This License does not grant permission to use the trade 10# names, trademarks, service marks, or product names of the Licensor 11# and its affiliates, except as required to comply with Section 4(c) of 12# the License and to reproduce the content of the NOTICE file. 13# 14# You may obtain a copy of the Apache License at 15# 16# http://www.apache.org/licenses/LICENSE-2.0 17# 18# Unless required by applicable law or agreed to in writing, software 19# distributed under the Apache License with the above modification is 20# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 21# KIND, either express or implied. See the Apache License for the specific 22# language governing permissions and limitations under the Apache License. 23# 24 25from __future__ import print_function 26 27from pxr import Ar 28 29from pxr.UsdUtils.constantsGroup import ConstantsGroup 30 31class NodeTypes(ConstantsGroup): 32 UsdPreviewSurface = "UsdPreviewSurface" 33 UsdUVTexture = "UsdUVTexture" 34 UsdTransform2d = "UsdTransform2d" 35 UsdPrimvarReader = "UsdPrimvarReader" 36 37class ShaderProps(ConstantsGroup): 38 Bias = "bias" 39 Scale = "scale" 40 SourceColorSpace = "sourceColorSpace" 41 Normal = "normal" 42 File = "file" 43 44 45def _IsPackageOrPackagedLayer(layer): 46 return layer.GetFileFormat().IsPackage() or \ 47 Ar.IsPackageRelativePath(layer.identifier) 48 49class BaseRuleChecker(object): 50 """This is Base class for all the rule-checkers.""" 51 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 52 self._verbose = verbose 53 self._consumerLevelChecks = consumerLevelChecks 54 self._assetLevelChecks = assetLevelChecks 55 self._failedChecks = [] 56 self._errors = [] 57 self._warnings = [] 58 59 def _AddFailedCheck(self, msg): 60 self._failedChecks.append(msg) 61 62 def _AddError(self, msg): 63 self._errors.append(msg) 64 65 def _AddWarning(self, msg): 66 self._warnings.append(msg) 67 68 def _Msg(self, msg): 69 if self._verbose: 70 print(msg) 71 72 def GetFailedChecks(self): 73 return self._failedChecks 74 75 def GetErrors(self): 76 return self._errors 77 78 def GetWarnings(self): 79 return self._warnings 80 81 # ------------------------------------------------------------------------- 82 # Virtual methods that any derived rule-checker may want to override. 83 # Default implementations do nothing. 84 # 85 # A rule-checker may choose to override one or more of the virtual methods. 86 # The callbacks are invoked in the order they are defined here (i.e. 87 # CheckStage is invoked first, followed by CheckDiagnostics, followed by 88 # CheckUnresolvedPaths and so on until CheckPrim). Some of the callbacks may 89 # be invoked multiple times per-rule with different parameters, for example, 90 # CheckLayer, CheckPrim and CheckZipFile. 91 92 def CheckStage(self, usdStage): 93 """ Check the given usdStage. """ 94 pass 95 96 def CheckDiagnostics(self, diagnostics): 97 """ Check the diagnostic messages that were generated when opening the 98 USD stage. The diagnostic messages are collected using a 99 UsdUtilsCoalescingDiagnosticDelegate. 100 """ 101 pass 102 103 def CheckUnresolvedPaths(self, unresolvedPaths): 104 """ Check or process any unresolved asset paths that were found when 105 analysing the dependencies. 106 """ 107 pass 108 109 def CheckDependencies(self, usdStage, layerDeps, assetDeps): 110 """ Check usdStage's layer and asset dependencies that were gathered 111 using UsdUtils.ComputeAllDependencies(). 112 """ 113 pass 114 115 def CheckLayer(self, layer): 116 """ Check the given SdfLayer. """ 117 pass 118 119 def CheckZipFile(self, zipFile, packagePath): 120 """ Check the zipFile object created by opening the package at path 121 packagePath. 122 """ 123 pass 124 125 def CheckPrim(self, prim): 126 """ Check the given prim, which may only exist is a specific combination 127 of variant selections on the UsdStage. 128 """ 129 pass 130 131 def ResetCaches(self): 132 """ Reset any caches the rule owns. Called whenever stage authoring 133 occurs, such as when we iterate through VariantSet combinations. 134 """ 135 pass 136 137 # ------------------------------------------------------------------------- 138 139class ByteAlignmentChecker(BaseRuleChecker): 140 @staticmethod 141 def GetDescription(): 142 return "Files within a usdz package must be laid out properly, "\ 143 "i.e. they should be aligned to 64 bytes." 144 145 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 146 super(ByteAlignmentChecker, self).__init__(verbose, 147 consumerLevelChecks, 148 assetLevelChecks) 149 150 def CheckZipFile(self, zipFile, packagePath): 151 fileNames = zipFile.GetFileNames() 152 for fileName in fileNames: 153 fileExt = Ar.GetResolver().GetExtension(fileName) 154 fileInfo = zipFile.GetFileInfo(fileName) 155 offset = fileInfo.dataOffset 156 if offset % 64 != 0: 157 self._AddFailedCheck("File '%s' in package '%s' has an " 158 "invalid offset %s." % 159 (fileName, packagePath, offset)) 160 161class CompressionChecker(BaseRuleChecker): 162 @staticmethod 163 def GetDescription(): 164 return "Files within a usdz package should not be compressed or "\ 165 "encrypted." 166 167 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 168 super(CompressionChecker, self).__init__(verbose, 169 consumerLevelChecks, 170 assetLevelChecks) 171 172 def CheckZipFile(self, zipFile, packagePath): 173 fileNames = zipFile.GetFileNames() 174 for fileName in fileNames: 175 fileExt = Ar.GetResolver().GetExtension(fileName) 176 fileInfo = zipFile.GetFileInfo(fileName) 177 if fileInfo.compressionMethod != 0: 178 self._AddFailedCheck("File '%s' in package '%s' has " 179 "compression. Compression method is '%s', actual size " 180 "is %s. Uncompressed size is %s." % ( 181 fileName, packagePath, fileInfo.compressionMethod, 182 fileInfo.size, fileInfo.uncompressedSize)) 183 184class MissingReferenceChecker(BaseRuleChecker): 185 @staticmethod 186 def GetDescription(): 187 return "The composed USD stage should not contain any unresolvable"\ 188 " asset dependencies (in every possible variation of the "\ 189 "asset), when using the default asset resolver. " 190 191 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 192 super(MissingReferenceChecker, self).__init__(verbose, 193 consumerLevelChecks, 194 assetLevelChecks) 195 196 def CheckDiagnostics(self, diagnostics): 197 for diag in diagnostics: 198 # "_ReportErrors" is the name of the function that issues 199 # warnings about unresolved references, sublayers and other 200 # composition arcs. 201 if '_ReportErrors' in diag.sourceFunction and \ 202 'usd/stage.cpp' in diag.sourceFileName: 203 self._AddFailedCheck(diag.commentary) 204 205 def CheckUnresolvedPaths(self, unresolvedPaths): 206 for unresolvedPath in unresolvedPaths: 207 self._AddFailedCheck("Found unresolvable external dependency " 208 "'%s'." % unresolvedPath) 209 210class StageMetadataChecker(BaseRuleChecker): 211 @staticmethod 212 def GetDescription(): 213 return """All stages should declare their 'upAxis' and 'metersPerUnit'. 214Stages that can be consumed as referencable assets should furthermore have 215a valid 'defaultPrim' declared, and stages meant for consumer-level packaging 216should always have upAxis set to 'Y'""" 217 218 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 219 super(StageMetadataChecker, self).__init__(verbose, 220 consumerLevelChecks, 221 assetLevelChecks) 222 223 def CheckStage(self, usdStage): 224 from pxr import UsdGeom 225 226 if not usdStage.HasAuthoredMetadata(UsdGeom.Tokens.upAxis): 227 self._AddFailedCheck("Stage does not specify an upAxis.") 228 elif self._consumerLevelChecks: 229 upAxis = UsdGeom.GetStageUpAxis(usdStage) 230 if upAxis != UsdGeom.Tokens.y: 231 self._AddFailedCheck("Stage specifies upAxis '%s'. upAxis should" 232 " be '%s'." % (upAxis, UsdGeom.Tokens.y)) 233 234 if not usdStage.HasAuthoredMetadata(UsdGeom.Tokens.metersPerUnit): 235 self._AddFailedCheck("Stage does not specify its linear scale " 236 "in metersPerUnit.") 237 238 if self._assetLevelChecks: 239 defaultPrim = usdStage.GetDefaultPrim() 240 if not defaultPrim: 241 self._AddFailedCheck("Stage has missing or invalid defaultPrim.") 242 243 244 245class TextureChecker(BaseRuleChecker): 246 # The most basic formats are those published in the USDZ spec 247 _basicUSDZImageFormats = ("jpg", "png") 248 249 # In non-consumer-content (arkit) mode, OIIO can allow us to 250 # additionaly read other formats from USDZ packages 251 _extraUSDZ_OIIOImageFormats = (".exr") 252 253 # Include a list of "unsupported" image formats to provide better error 254 # messages when we find one of these. Our builtin image decoder 255 # _can_ decode these, but they are not considered portable consumer-level 256 _unsupportedImageFormats = ["bmp", "tga", "hdr", "exr", "tif", "zfile", 257 "tx"] 258 259 @staticmethod 260 def GetDescription(): 261 return """Texture files should be readable by intended client 262(only .jpg or .png for consumer-level USDZ).""" 263 264 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 265 # Check if the prim has an allowed type. 266 super(TextureChecker, self).__init__(verbose, consumerLevelChecks, assetLevelChecks) 267 # a None value for _allowedFormats indicates all formats are allowed 268 self._allowedFormats = None 269 270 def CheckStage(self, usdStage): 271 # This is the point at which we can determine whether we have a USDZ 272 # archive, and so have enough information to set the allowed formats. 273 rootLayer = usdStage.GetRootLayer() 274 if rootLayer.GetFileFormat().IsPackage() or self._consumerLevelChecks: 275 self._allowedFormats = list(TextureChecker._basicUSDZImageFormats) 276 if not self._consumerLevelChecks: 277 self._allowedFormats.append(TextureChecker._extraUSDZ_OIIOImageFormats) 278 else: 279 self._Msg("Not performing texture format checks for general " 280 "USD asset") 281 282 283 def _CheckTexture(self, texAssetPath, inputPath): 284 self._Msg("Checking texture <%s>." % texAssetPath) 285 texFileExt = Ar.GetResolver().GetExtension(texAssetPath).lower() 286 if (self._consumerLevelChecks and 287 texFileExt in TextureChecker._unsupportedImageFormats): 288 self._AddFailedCheck("Texture <%s> with asset @%s@ has non-portable " 289 "file format." % (inputPath, texAssetPath)) 290 elif texFileExt not in self._allowedFormats: 291 self._AddFailedCheck("Texture <%s> with asset @%s@ has unknown " 292 "file format." % (inputPath, texAssetPath)) 293 294 def CheckPrim(self, prim): 295 # Right now, we find texture referenced by looking at the asset-valued 296 # inputs on Connectable prims. 297 from pxr import Sdf, Usd, UsdShade 298 299 # Nothing to do if we are allowing all formats, or if 300 # we are an untyped prim 301 if self._allowedFormats is None or not prim.GetTypeName(): 302 return 303 304 # Check if the prim is Connectable. 305 connectable = UsdShade.ConnectableAPI(prim) 306 if not connectable: 307 return 308 309 shaderInputs = connectable.GetInputs() 310 for ip in shaderInputs: 311 attrPath = ip.GetAttr().GetPath() 312 if ip.GetTypeName() == Sdf.ValueTypeNames.Asset: 313 texFilePath = ip.Get(Usd.TimeCode.EarliestTime()) 314 # ip may be unauthored and/or connected 315 if texFilePath: 316 self._CheckTexture(texFilePath.path, attrPath) 317 elif ip.GetTypeName() == Sdf.ValueTypeNames.AssetArray: 318 texPathArray = ip.Get(Usd.TimeCode.EarliestTime()) 319 if texPathArray: 320 for texPath in texPathArray: 321 self._CheckTexture(texPath, attrPath) 322 323class PrimEncapsulationChecker(BaseRuleChecker): 324 @staticmethod 325 def GetDescription(): 326 return """Check for basic prim encapsulation rules: 327 - Boundables may not be nested under Gprims 328 - Connectable prims (e.g. Shader, Material, etc) can only be nested 329 inside other Container-like Connectable prims. Container-like prims 330 include Material, NodeGraph, Light, LightFilter, and *exclude Shader*""" 331 332 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 333 super(PrimEncapsulationChecker, self).__init__(verbose, 334 consumerLevelChecks, 335 assetLevelChecks) 336 self.ResetCaches() 337 338 def _HasGprimAncestor(self, prim): 339 from pxr import Sdf, UsdGeom 340 path = prim.GetPath() 341 if path in self._hasGprimInPathMap: 342 return self._hasGprimInPathMap[path] 343 elif path == Sdf.Path.absoluteRootPath: 344 self._hasGprimInPathMap[path] = False 345 return False 346 else: 347 val = (self._HasGprimAncestor(prim.GetParent()) or 348 prim.IsA(UsdGeom.Gprim)) 349 self._hasGprimInPathMap[path] = val 350 return val 351 352 def _FindConnectableAncestor(self, prim): 353 from pxr import Sdf, UsdShade 354 path = prim.GetPath() 355 if path in self._connectableAncestorMap: 356 return self._connectableAncestorMap[path] 357 elif path == Sdf.Path.absoluteRootPath: 358 self._connectableAncestorMap[path] = None 359 return None 360 else: 361 val = self._FindConnectableAncestor(prim.GetParent()) 362 # The GetTypeName() check is to work around a bug in 363 # ConnectableAPIBehavior registry. 364 if prim.GetTypeName() and not val: 365 conn = UsdShade.ConnectableAPI(prim) 366 if conn: 367 val = prim 368 self._connectableAncestorMap[path] = val 369 return val 370 371 372 def CheckPrim(self, prim): 373 from pxr import UsdGeom, UsdShade 374 375 parent = prim.GetParent() 376 377 # Of course we must allow Boundables under other Boundables, so that 378 # schemas like UsdGeom.Pointinstancer can nest their prototypes. But 379 # we disallow a PointInstancer under a Mesh just as we disallow a Mesh 380 # under a Mesh, for the same reason: we cannot then independently 381 # adjust visibility for the two objects, nor can we reasonably compute 382 # the parent Mesh's extent. 383 if prim.IsA(UsdGeom.Boundable): 384 if parent: 385 if self._HasGprimAncestor(parent): 386 self._AddFailedCheck("Gprim <%s> has an ancestor prim that " 387 "is also a Gprim, which is not allowed." 388 % prim.GetPath()) 389 390 connectable = UsdShade.ConnectableAPI(prim) 391 # The GetTypeName() check is to work around a bug in 392 # ConnectableAPIBehavior registry. 393 if prim.GetTypeName() and connectable: 394 if parent: 395 pConnectable = UsdShade.ConnectableAPI(parent) 396 if not parent.GetTypeName(): 397 pConnectable = None 398 if pConnectable and not pConnectable.IsContainer(): 399 # XXX This should be a failure as it is a violation of the 400 # UsdShade OM. But pragmatically, there are many 401 # authoring tools currently producing this structure, which 402 # does not _currently_ perturb Hydra, so we need to start 403 # with a warning 404 self._AddWarning("Connectable %s <%s> cannot reside " 405 "under a non-Container Connectable %s" 406 % (prim.GetTypeName(), 407 prim.GetPath(), 408 parent.GetTypeName())) 409 elif not pConnectable: 410 # it's only OK to have a non-connectable parent if all 411 # the rest of your ancestors are also non-connectable. The 412 # error message we give is targeted at the most common 413 # infraction, using Scope or other grouping prims inside 414 # a Container like a Material 415 connAnstr = self._FindConnectableAncestor(parent) 416 if connAnstr is not None: 417 self.AddFailedCheck("Connectable %s <%s> can only have " 418 "Connectable Container ancestors up " 419 "to %s ancestor <%s>, but its parent" 420 "%s is a %s." % 421 (prim.GetTypeName(), 422 prim.GetPath(), 423 connAnstr.GetTypeName(), 424 connAnstr.GetPath(), 425 parent.GetName(), 426 parent.GetTypeName())) 427 428 429 def ResetCaches(self): 430 self._connectableAncestorMap = dict() 431 self._hasGprimInPathMap = dict() 432 433 434 435class NormalMapTextureChecker(BaseRuleChecker): 436 @staticmethod 437 def GetDescription(): 438 return """UsdUVTexture nodes that feed the _inputs:normals_ of a 439UsdPreviewSurface must ensure that the data is encoded and scaled properly. 440Specifically: 441 - Since normals are expected to be in the range [(-1,-1,-1), (1,1,1)], 442 the Texture node must transform 8-bit textures from their [0..1] range by 443 setting its _inputs:scale_ to [2, 2, 2, 1] and 444 _inputs:bias_ to [-1, -1, -1, 0] 445 - Normal map data is commonly expected to be linearly encoded. However, many 446 image-writing tools automatically set the profile of three-channel, 8-bit 447 images to SRGB. To prevent an unwanted transformation, the UsdUVTexture's 448 _inputs:sourceColorSpace_ must be set to "raw". This program cannot 449 currently read the texture metadata itself, so for now we emit warnings 450 about this potential infraction for all 8 bit image formats. 451""" 452 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 453 super(NormalMapTextureChecker, self).__init__(verbose, 454 consumerLevelChecks, 455 assetLevelChecks) 456 457 def _GetShaderId(self, shader): 458 # We might someday try harder to find an identifier... 459 return shader.GetShaderId() 460 461 def _TextureIs8Bit(self, asset): 462 # Eventually we hope to leverage HioImage through a plugin system, 463 # when Imaging is present, to answer this and other image queries 464 # more definitively 465 from pxr import Ar 466 ext = Ar.GetResolver().GetExtension(asset.resolvedPath) 467 # not an exhaustive list, but ones we typically can read 468 return ext in ["bmp", "tga", "jpg", "png", "tif"] 469 470 def _GetInputValue(self, shader, inputName): 471 from pxr import Usd 472 input = shader.GetInput(inputName) 473 if not input: 474 return None 475 return input.Get(Usd.TimeCode.EarliestTime()) 476 477 def CheckPrim(self, prim): 478 from pxr import UsdShade, Gf 479 from pxr.UsdShade import Utils as ShadeUtils 480 481 if not prim.IsA(UsdShade.Shader): 482 return 483 484 shader = UsdShade.Shader(prim) 485 if not shader: 486 self._AddError("Invalid shader prim <%s>." % prim.GetPath()) 487 return 488 489 shaderId = self._GetShaderId(shader) 490 491 # We may have failed to fetch an identifier for asset/source-based 492 # nodes. We are only interested in UsdPreviewSurface nodes identified via 493 # info:id, so it's not an error 494 if not shaderId or shaderId != NodeTypes.UsdPreviewSurface: 495 return 496 497 normalInput = shader.GetInput(ShaderProps.Normal) 498 if not normalInput: 499 return 500 valueProducingAttrs = ShadeUtils.GetValueProducingAttributes(normalInput) 501 if not valueProducingAttrs or valueProducingAttrs[0].GetPrim() == prim: 502 return 503 504 sourcePrim = valueProducingAttrs[0].GetPrim() 505 506 sourceShader = UsdShade.Shader(sourcePrim) 507 if not sourceShader: 508 # In theory, could be connected to an interface attribute of a 509 # parent connectable... not useful, but not an error 510 if UsdShade.ConnectableAPI(sourcePrim): 511 return 512 self._AddFailedCheck("%s.%s on prim <%s> is connected to a" 513 " non-Shader prim." % \ 514 (NodeTypes.UsdPreviewSurface, 515 ShaderProps.Normal)) 516 return 517 518 sourceId = self._GetShaderId(sourceShader) 519 520 # We may have failed to fetch an identifier for asset/source-based 521 # nodes. OR, we could potentially be driven by a UsdPrimvarReader, 522 # in which case we'd have nothing to validate 523 if not sourceId or sourceId != NodeTypes.UsdUVTexture: 524 return 525 526 texAsset = self._GetInputValue(sourceShader, ShaderProps.File) 527 528 if not texAsset or not texAsset.resolvedPath: 529 self._AddFailedCheck("%s prim <%s> has invalid or unresolvable " 530 "inputs:file of @%s@" % \ 531 (NodeTypes.UsdUVTexture, 532 sourcePrim.GetPath(), 533 texAsset.path if texAsset else "")) 534 return 535 536 if not self._TextureIs8Bit(texAsset): 537 # really nothing more is required for image depths > 8 bits, 538 # which we assume FOR NOW, are floating point 539 return 540 541 if not self._GetInputValue(sourceShader, ShaderProps.SourceColorSpace): 542 self._AddWarning("%s prim <%s> that reads Normal Map @%s@ may need " 543 "to set inputs:sourceColorSpace to 'raw' as some " 544 "8-bit image writers always indicate an SRGB " 545 "encoding." % \ 546 (NodeTypes.UsdUVTexture, 547 sourcePrim.GetPath(), 548 texAsset.path)) 549 550 bias = self._GetInputValue(sourceShader, ShaderProps.Bias) 551 552 scale = self._GetInputValue(sourceShader, ShaderProps.Scale) 553 554 if not (bias and scale and 555 isinstance(bias, Gf.Vec4f) and isinstance(scale, Gf.Vec4f)): 556 # XXX This should be a failure, as it results in broken normal 557 # maps in Storm and hdPrman, at least. But for the same reason 558 # as the shader-under-shader check, we cannot fail until at least 559 # the major authoring tools have been updated. 560 self._AddWarning("%s prim <%s> reads 8 bit Normal Map @%s@, " 561 "which requires that inputs:scale be set to " 562 "[2, 2, 2, 1] and inputs:bias be set to " 563 "[-1, -1, -1, 0] for proper interpretation." %\ 564 (NodeTypes.UsdUVTexture, 565 sourcePrim.GetPath(), 566 texAsset.path)) 567 return 568 569 # don't really care about fourth components... 570 if (bias[0] != -1 or bias[1] != -1 or bias[2] != -1 or 571 scale[0] != 2 or scale[1] != 2 or scale[2] != 2): 572 self._AddWarning("%s prim <%s> reads an 8 bit Normal Map, " 573 "but has non-standard inputs:scale and " 574 "inputs:bias values of %s and %s" %\ 575 (NodeTypes.UsdUVTexture, 576 sourcePrim.GetPath(), 577 str(scale), str(bias))) 578 579 580class ARKitPackageEncapsulationChecker(BaseRuleChecker): 581 @staticmethod 582 def GetDescription(): 583 return "If the root layer is a package, then the composed stage "\ 584 "should not contain references to files outside the package. "\ 585 "In other words, the package should be entirely self-contained." 586 587 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 588 super(ARKitPackageEncapsulationChecker, self).\ 589 __init__(verbose, consumerLevelChecks, assetLevelChecks) 590 591 def CheckDependencies(self, usdStage, layerDeps, assetDeps): 592 rootLayer = usdStage.GetRootLayer() 593 if not _IsPackageOrPackagedLayer(rootLayer): 594 return 595 596 packagePath = usdStage.GetRootLayer().realPath 597 if packagePath: 598 if Ar.IsPackageRelativePath(packagePath): 599 packagePath = Ar.SplitPackageRelativePathOuter( 600 packagePath)[0] 601 for layer in layerDeps: 602 # In-memory layers like session layers (which we must skip when 603 # doing this check) won't have a real path. 604 if layer.realPath: 605 if not layer.realPath.startswith(packagePath): 606 self._AddFailedCheck("Found loaded layer '%s' that " 607 "does not belong to the package '%s'." % 608 (layer.identifier, packagePath)) 609 for asset in assetDeps: 610 if not asset.startswith(packagePath): 611 self._AddFailedCheck("Found asset reference '%s' that " 612 "does not belong to the package '%s'." % 613 (asset, packagePath)) 614 615class ARKitLayerChecker(BaseRuleChecker): 616 # Only core USD file formats are allowed. 617 _allowedLayerFormatIds = ('usd', 'usda', 'usdc', 'usdz') 618 619 @staticmethod 620 def GetDescription(): 621 return "All included layers that participate in composition should"\ 622 " have one of the core supported file formats." 623 624 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 625 # Check if the prim has an allowed type. 626 super(ARKitLayerChecker, self).__init__(verbose, 627 consumerLevelChecks, 628 assetLevelChecks) 629 630 def CheckLayer(self, layer): 631 self._Msg("Checking layer <%s>." % layer.identifier) 632 633 formatId = layer.GetFileFormat().formatId 634 if not formatId in \ 635 ARKitLayerChecker._allowedLayerFormatIds: 636 self._AddFailedCheck("Layer '%s' has unsupported formatId " 637 "'%s'." % (layer.identifier, formatId)) 638 639class ARKitPrimTypeChecker(BaseRuleChecker): 640 # All core prim types other than UsdGeomPointInstancers, Curve types, Nurbs, 641 # and the types in UsdLux are allowed. 642 _allowedPrimTypeNames = ('', 'Scope', 'Xform', 'Camera', 643 'Shader', 'Material', 644 'Mesh', 'Sphere', 'Cube', 'Cylinder', 'Cone', 645 'Capsule', 'GeomSubset', 'Points', 646 'SkelRoot', 'Skeleton', 'SkelAnimation', 647 'BlendShape', 'SpatialAudio') 648 649 @staticmethod 650 def GetDescription(): 651 return "UsdGeomPointInstancers and custom schemas not provided by "\ 652 "core USD are not allowed." 653 654 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 655 # Check if the prim has an allowed type. 656 super(ARKitPrimTypeChecker, self).__init__(verbose, 657 consumerLevelChecks, 658 assetLevelChecks) 659 660 def CheckPrim(self, prim): 661 self._Msg("Checking prim <%s>." % prim.GetPath()) 662 if prim.GetTypeName() not in \ 663 ARKitPrimTypeChecker._allowedPrimTypeNames: 664 self._AddFailedCheck("Prim <%s> has unsupported type '%s'." % 665 (prim.GetPath(), prim.GetTypeName())) 666 667class ARKitShaderChecker(BaseRuleChecker): 668 @staticmethod 669 def GetDescription(): 670 return "Shader nodes must have \"id\" as the implementationSource, " \ 671 "with id values that begin with \"Usd*\". Also, shader inputs "\ 672 "with connections must each have a single, valid connection " \ 673 "source." 674 675 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 676 super(ARKitShaderChecker, self).__init__(verbose, consumerLevelChecks, 677 assetLevelChecks) 678 679 def CheckPrim(self, prim): 680 from pxr import UsdShade 681 682 if not prim.IsA(UsdShade.Shader): 683 return 684 685 shader = UsdShade.Shader(prim) 686 if not shader: 687 # Error has already been issued by a Base-level checker 688 return 689 690 self._Msg("Checking shader <%s>." % prim.GetPath()) 691 692 implSource = shader.GetImplementationSource() 693 if implSource != UsdShade.Tokens.id: 694 self._AddFailedCheck("Shader <%s> has non-id implementation " 695 "source '%s'." % (prim.GetPath(), implSource)) 696 697 shaderId = shader.GetShaderId() 698 if not shaderId or \ 699 not (shaderId in [NodeTypes.UsdPreviewSurface, 700 NodeTypes.UsdUVTexture, 701 NodeTypes.UsdTransform2d] or 702 shaderId.startswith(NodeTypes.UsdPrimvarReader)) : 703 self._AddFailedCheck("Shader <%s> has unsupported info:id '%s'." 704 % (prim.GetPath(), shaderId)) 705 706 # Check shader input connections 707 shaderInputs = shader.GetInputs() 708 for shdInput in shaderInputs: 709 connections = shdInput.GetAttr().GetConnections() 710 # If an input has one or more connections, ensure that the 711 # connections are valid. 712 if len(connections) > 0: 713 if len(connections) > 1: 714 self._AddFailedCheck("Shader input <%s> has %s connection " 715 "sources, but only one is allowed." % 716 (shdInput.GetAttr.GetPath(), len(connections))) 717 connectedSource = shdInput.GetConnectedSource() 718 if connectedSource is None: 719 self._AddFailedCheck("Connection source <%s> for shader " 720 "input <%s> is missing." % (connections[0], 721 shdInput.GetAttr().GetPath())) 722 else: 723 # The source must be a valid shader or material prim. 724 source = connectedSource[0] 725 if not source.GetPrim().IsA(UsdShade.Shader) and \ 726 not source.GetPrim().IsA(UsdShade.Material): 727 self._AddFailedCheck("Shader input <%s> has an invalid " 728 "connection source prim of type '%s'." % 729 (shdInput.GetAttr().GetPath(), 730 source.GetPrim().GetTypeName())) 731 732class ARKitMaterialBindingChecker(BaseRuleChecker): 733 @staticmethod 734 def GetDescription(): 735 return "All material binding relationships must have valid targets." 736 737 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 738 super(ARKitMaterialBindingChecker, self).__init__(verbose, 739 consumerLevelChecks, 740 assetLevelChecks) 741 742 def CheckPrim(self, prim): 743 from pxr import UsdShade 744 relationships = prim.GetRelationships() 745 bindingRels = [rel for rel in relationships if 746 rel.GetName().startswith(UsdShade.Tokens.materialBinding)] 747 748 for bindingRel in bindingRels: 749 targets = bindingRel.GetTargets() 750 if len(targets) == 1: 751 directBinding = UsdShade.MaterialBindingAPI.DirectBinding( 752 bindingRel) 753 if not directBinding.GetMaterial(): 754 self._AddFailedCheck("Direct material binding <%s> targets " 755 "an invalid material <%s>." % (bindingRel.GetPath(), 756 directBinding.GetMaterialPath())) 757 elif len(targets) == 2: 758 collBinding = UsdShade.MaterialBindingAPI.CollectionBinding( 759 bindingRel) 760 if not collBinding.GetMaterial(): 761 self._AddFailedCheck("Collection-based material binding " 762 "<%s> targets an invalid material <%s>." % 763 (bindingRel.GetPath(), collBinding.GetMaterialPath())) 764 if not collBinding.GetCollection(): 765 self._AddFailedCheck("Collection-based material binding " 766 "<%s> targets an invalid collection <%s>." % 767 (bindingRel.GetPath(), collBinding.GetCollectionPath())) 768 769class ARKitFileExtensionChecker(BaseRuleChecker): 770 _allowedFileExtensions = \ 771 ARKitLayerChecker._allowedLayerFormatIds + \ 772 TextureChecker._basicUSDZImageFormats 773 774 @staticmethod 775 def GetDescription(): 776 return "Only layer files and textures are allowed in a package." 777 778 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 779 super(ARKitFileExtensionChecker, self).__init__(verbose, 780 consumerLevelChecks, 781 assetLevelChecks) 782 783 def CheckZipFile(self, zipFile, packagePath): 784 fileNames = zipFile.GetFileNames() 785 for fileName in fileNames: 786 fileExt = Ar.GetResolver().GetExtension(fileName) 787 if fileExt not in ARKitFileExtensionChecker._allowedFileExtensions: 788 self._AddFailedCheck("File '%s' in package '%s' has an " 789 "unknown or unsupported extension '%s'." % 790 (fileName, packagePath, fileExt)) 791 792class ARKitRootLayerChecker(BaseRuleChecker): 793 @staticmethod 794 def GetDescription(): 795 return "The root layer of the package must be a usdc file and " \ 796 "must not include any external dependencies that participate in "\ 797 "stage composition." 798 799 def __init__(self, verbose, consumerLevelChecks, assetLevelChecks): 800 super(ARKitRootLayerChecker, self).__init__(verbose, 801 consumerLevelChecks, 802 assetLevelChecks) 803 804 def CheckStage(self, usdStage): 805 usedLayers = usdStage.GetUsedLayers() 806 # This list excludes any session layers. 807 usedLayersOnDisk = [i for i in usedLayers if i.realPath] 808 if len(usedLayersOnDisk) > 1: 809 self._AddFailedCheck("The stage uses %s layers. It should " 810 "contain a single usdc layer to be compatible with ARKit's " 811 "implementation of usdz." % len(usedLayersOnDisk)) 812 813 rootLayerRealPath = usdStage.GetRootLayer().realPath 814 if rootLayerRealPath.endswith(".usdz"): 815 # Check if the root layer in the package is a usdc. 816 from pxr import Usd 817 zipFile = Usd.ZipFile.Open(rootLayerRealPath) 818 if not zipFile: 819 self._AddError("Could not open package at path '%s'." % 820 resolvedPath) 821 return 822 fileNames = zipFile.GetFileNames() 823 if not fileNames[0].endswith(".usdc"): 824 self._AddFailedCheck("First file (%s) in usdz package '%s' " 825 "does not have the .usdc extension." % (fileNames[0], 826 rootLayerRealPath)) 827 elif not rootLayerRealPath.endswith(".usdc"): 828 self._AddFailedCheck("Root layer of the stage '%s' does not " 829 "have the '.usdc' extension." % (rootLayerRealPath)) 830 831class ComplianceChecker(object): 832 """ A utility class for checking compliance of a given USD asset or a USDZ 833 package. 834 835 Since usdz files are zip files, someone could use generic zip tools to 836 create an archive and just change the extension, producing a .usdz file that 837 does not honor the additional constraints that usdz files require. Even if 838 someone does use our official archive creation tools, though, we 839 intentionally allow creation of usdz files that can be very "permissive" in 840 their contents for internal studio uses, where portability outside the 841 studio is not a concern. For content meant to be delivered over the web 842 (eg. ARKit assets), however, we must be much more restrictive. 843 844 This class provides two levels of compliance checking: 845 * "structural" validation that is represented by a set of base rules. 846 * "ARKit" compatibility validation, which includes many more restrictions. 847 848 Calling ComplianceChecker.DumpAllRules() will print an enumeration of the 849 various rules in the two categories of compliance checking. 850 """ 851 852 @staticmethod 853 def GetBaseRules(): 854 return [ByteAlignmentChecker, CompressionChecker, 855 MissingReferenceChecker, StageMetadataChecker, TextureChecker, 856 PrimEncapsulationChecker, NormalMapTextureChecker] 857 858 @staticmethod 859 def GetARKitRules(skipARKitRootLayerCheck=False): 860 arkitRules = [ARKitLayerChecker, ARKitPrimTypeChecker, 861 ARKitShaderChecker, 862 ARKitMaterialBindingChecker, 863 ARKitFileExtensionChecker, 864 ARKitPackageEncapsulationChecker] 865 if not skipARKitRootLayerCheck: 866 arkitRules.append(ARKitRootLayerChecker) 867 return arkitRules 868 869 @staticmethod 870 def GetRules(arkit=False, skipARKitRootLayerCheck=False): 871 allRules = ComplianceChecker.GetBaseRules() 872 if arkit: 873 arkitRules = ComplianceChecker.GetARKitRules( 874 skipARKitRootLayerCheck=skipARKitRootLayerCheck) 875 allRules += arkitRules 876 return allRules 877 878 @staticmethod 879 def DumpAllRules(): 880 print('Base rules:') 881 for ruleNum, rule in enumerate(GetBaseRules()): 882 print('[%s] %s' % (ruleNum + 1, rule.GetDescription())) 883 print('-' * 30) 884 print('ARKit rules: ') 885 for ruleNum, rule in enumerate(GetBaseRules()): 886 print('[%s] %s' % (ruleNum + 1, rule.GetDescription())) 887 print('-' * 30) 888 889 def __init__(self, arkit=False, skipARKitRootLayerCheck=False, 890 rootPackageOnly=False, skipVariants=False, verbose=False, 891 assetLevelChecks=True): 892 self._rootPackageOnly = rootPackageOnly 893 self._doVariants = not skipVariants 894 self._verbose = verbose 895 self._errors = [] 896 self._warnings = [] 897 898 # Once a package has been checked, it goes into this set. 899 self._checkedPackages = set() 900 901 # Instantiate an instance of every rule checker and store in a list. 902 self._rules = [Rule(self._verbose, arkit, assetLevelChecks) for Rule in 903 ComplianceChecker.GetRules(arkit, skipARKitRootLayerCheck)] 904 905 def _Msg(self, msg): 906 if self._verbose: 907 print(msg) 908 909 def _AddError(self, errMsg): 910 self._errors.append(errMsg) 911 912 def _AddWarning(self, errMsg): 913 self._warnings.append(errMsg) 914 915 def GetErrors(self): 916 errors = self._errors 917 for rule in self._rules: 918 errs = rule.GetErrors() 919 for err in errs: 920 errors.append("Error checking rule '%s': %s" % 921 (type(rule).__name__, err)) 922 return errors 923 924 def GetWarnings(self): 925 warnings = self._warnings 926 for rule in self._rules: 927 advisories = rule.GetWarnings() 928 for ad in advisories: 929 warnings.append("%s (may violate '%s')" % (ad, 930 type(rule).__name__)) 931 return warnings 932 933 def DumpRules(self): 934 print('Checking rules: ') 935 for rule in self._rules: 936 print('-' * 30) 937 print('[%s]:\n %s' % (type(rule).__name__, rule.GetDescription())) 938 print('-' * 30) 939 940 def GetFailedChecks(self): 941 failedChecks = [] 942 for rule in self._rules: 943 fcs = rule.GetFailedChecks() 944 for fc in fcs: 945 failedChecks.append("%s (fails '%s')" % (fc, 946 type(rule).__name__)) 947 return failedChecks 948 949 def CheckCompliance(self, inputFile): 950 from pxr import Sdf, Usd, UsdUtils 951 if not Usd.Stage.IsSupportedFile(inputFile): 952 _AddError("Cannot open file '%s' on a USD stage." % args.inputFile) 953 return 954 955 # Collect all warnings using a diagnostic delegate. 956 delegate = UsdUtils.CoalescingDiagnosticDelegate() 957 usdStage = Usd.Stage.Open(inputFile) 958 stageOpenDiagnostics = delegate.TakeUncoalescedDiagnostics() 959 960 for rule in self._rules: 961 rule.CheckStage(usdStage) 962 rule.CheckDiagnostics(stageOpenDiagnostics) 963 964 with Ar.ResolverContextBinder(usdStage.GetPathResolverContext()): 965 # This recursively computes all of inputFiles's external 966 # dependencies. 967 (allLayers, allAssets, unresolvedPaths) = \ 968 UsdUtils.ComputeAllDependencies(Sdf.AssetPath(inputFile)) 969 for rule in self._rules: 970 rule.CheckUnresolvedPaths(unresolvedPaths) 971 rule.CheckDependencies(usdStage, allLayers, allAssets) 972 973 if self._rootPackageOnly: 974 rootLayer = usdStage.GetRootLayer() 975 if rootLayer.GetFileFormat().IsPackage(): 976 packagePath = Ar.SplitPackageRelativePathInner( 977 rootLayer.identifier)[0] 978 self._CheckPackage(packagePath) 979 else: 980 self._AddError("Root layer of the USD stage (%s) doesn't belong to " 981 "a package, but 'rootPackageOnly' is True!" % 982 Usd.Describe(usdStage)) 983 else: 984 # Process every package just once by storing them all in a set. 985 packages = set() 986 for layer in allLayers: 987 if _IsPackageOrPackagedLayer(layer): 988 packagePath = Ar.SplitPackageRelativePathInner( 989 layer.identifier)[0] 990 packages.add(packagePath) 991 self._CheckLayer(layer) 992 for package in packages: 993 self._CheckPackage(package) 994 995 # Traverse the entire stage and check every prim. 996 from pxr import Usd 997 # Author all variant switches in the session layer. 998 usdStage.SetEditTarget(usdStage.GetSessionLayer()) 999 allPrimsIt = iter(Usd.PrimRange.Stage(usdStage, 1000 Usd.TraverseInstanceProxies())) 1001 self._TraverseRange(allPrimsIt, isStageRoot=True) 1002 1003 def _CheckPackage(self, packagePath): 1004 self._Msg("Checking package <%s>." % packagePath) 1005 1006 # XXX: Should we open the package on a stage to ensure that it is valid 1007 # and entirely self-contained. 1008 1009 from pxr import Usd 1010 pkgExt = Ar.GetResolver().GetExtension(packagePath) 1011 if pkgExt != "usdz": 1012 self._AddError("Package at path %s has an invalid extension." 1013 % packagePath) 1014 return 1015 1016 # Check the parent package first. 1017 if Ar.IsPackageRelativePath(packagePath): 1018 parentPackagePath = Ar.SplitPackageRelativePathInner(packagePath)[0] 1019 self._CheckPackage(parentPackagePath) 1020 1021 # Avoid checking the same parent package multiple times. 1022 if packagePath in self._checkedPackages: 1023 return 1024 self._checkedPackages.add(packagePath) 1025 1026 resolvedPath = Ar.GetResolver().Resolve(packagePath) 1027 if not resolvedPath: 1028 self._AddError("Failed to resolve package path '%s'." % packagePath) 1029 return 1030 1031 zipFile = Usd.ZipFile.Open(resolvedPath) 1032 if not zipFile: 1033 self._AddError("Could not open package at path '%s'." % 1034 resolvedPath) 1035 return 1036 for rule in self._rules: 1037 rule.CheckZipFile(zipFile, packagePath) 1038 1039 def _CheckLayer(self, layer): 1040 for rule in self._rules: 1041 rule.CheckLayer(layer) 1042 1043 def _CheckPrim(self, prim): 1044 for rule in self._rules: 1045 rule.CheckPrim(prim) 1046 1047 def _TraverseRange(self, primRangeIt, isStageRoot): 1048 primsWithVariants = [] 1049 rootPrim = primRangeIt.GetCurrentPrim() 1050 for prim in primRangeIt: 1051 # Skip variant set check on the root prim if it is the stage'. 1052 if not self._doVariants or (not isStageRoot and prim == rootPrim): 1053 self._CheckPrim(prim) 1054 continue 1055 1056 vSets = prim.GetVariantSets() 1057 vSetNames = vSets.GetNames() 1058 if len(vSetNames) == 0: 1059 self._CheckPrim(prim) 1060 else: 1061 primsWithVariants.append(prim) 1062 primRangeIt.PruneChildren() 1063 1064 for prim in primsWithVariants: 1065 self._TraverseVariants(prim) 1066 1067 def _TraverseVariants(self, prim): 1068 from pxr import Usd 1069 if prim.IsInstanceProxy(): 1070 return True 1071 1072 vSets = prim.GetVariantSets() 1073 vSetNames = vSets.GetNames() 1074 allVariantNames = [] 1075 for vSetName in vSetNames: 1076 vSet = vSets.GetVariantSet(vSetName) 1077 vNames = vSet.GetVariantNames() 1078 allVariantNames.append(vNames) 1079 1080 import itertools 1081 allVariations = itertools.product(*allVariantNames) 1082 1083 for variation in allVariations: 1084 self._Msg("Testing variation %s of prim <%s>" % 1085 (variation, prim.GetPath())) 1086 for (idx, sel) in enumerate(variation): 1087 vSets.SetSelection(vSetNames[idx], sel) 1088 for rule in self._rules: 1089 rule.ResetCaches() 1090 primRangeIt = iter(Usd.PrimRange(prim, 1091 Usd.TraverseInstanceProxies())) 1092 self._TraverseRange(primRangeIt, isStageRoot=False) 1093