diff --git a/lib/usd/translators/shading/usdFileTextureWriter.cpp b/lib/usd/translators/shading/usdFileTextureWriter.cpp index 66349f9d21..8623c8e79c 100644 --- a/lib/usd/translators/shading/usdFileTextureWriter.cpp +++ b/lib/usd/translators/shading/usdFileTextureWriter.cpp @@ -67,7 +67,8 @@ class PxrUsdTranslators_FileTextureWriter : public UsdMayaShaderWriter const TfToken& mayaAttrName, const SdfValueTypeName& typeName) override; - void WriteTransform2dNode(const UsdTimeCode& usdTime, const UsdShadeShader& texShaderSchema); + void WriteTransform2dNode(const UsdTimeCode& usdTime, const UsdShadeShader& texShaderSchema); + SdfPath getPlace2DTexturePath(const MFnDependencyNode& depNodeFn); }; PXRUSDMAYA_REGISTER_SHADER_WRITER(file, PxrUsdTranslators_FileTextureWriter); @@ -77,7 +78,7 @@ TF_DEFINE_PRIVATE_TOKENS( _tokens, // UsdPrimvarReader_float2 Prim Name - ((PrimvarReaderShaderName, "TexCoordReader")) + ((PrimvarReaderShaderName, "shared_TexCoordReader")) // Usd2dTransform Prim Name ((UsdTransform2dShaderName, "UsdTransform2d")) @@ -125,49 +126,62 @@ PxrUsdTranslators_FileTextureWriter::PxrUsdTranslators_FileTextureWriter( // Now create a UsdPrimvarReader shader that the UsdUvTexture shader will // use. - const SdfPath primvarReaderShaderPath - = texShaderSchema.GetPath().AppendChild(_tokens->PrimvarReaderShaderName); - UsdShadeShader primvarReaderShaderSchema - = UsdShadeShader::Define(GetUsdStage(), primvarReaderShaderPath); - - primvarReaderShaderSchema.CreateIdAttr(VtValue(TrUsdTokens->UsdPrimvarReader_float2)); - - UsdShadeInput varnameInput - = primvarReaderShaderSchema.CreateInput(TrUsdTokens->varname, SdfValueTypeNames->Token); - - // We expose the primvar reader varname attribute to the material to allow - // easy specialization based on UV mappings to geometries: - SdfPath materialPath = GetUsdPath().GetParentPath(); - UsdShadeMaterial materialSchema(GetUsdStage()->GetPrimAtPath(materialPath)); - while (!materialSchema && !materialPath.IsEmpty()) { - materialPath = materialPath.GetParentPath(); - materialSchema = UsdShadeMaterial(GetUsdStage()->GetPrimAtPath(materialPath)); - } - - if (materialSchema) { - TfToken inputName( - TfStringPrintf("%s:%s", depNodeFn.name().asChar(), TrUsdTokens->varname.GetText())); - UsdShadeInput materialInput - = materialSchema.CreateInput(inputName, SdfValueTypeNames->Token); - materialInput.Set(UsdUtilsGetPrimaryUVSetName()); - varnameInput.ConnectToSource(materialInput); - // Note: This needs to be done for all nodes that require UV input. In - // the UsdPreviewSurface case, the file node is the only one, but for - // other Maya nodes like cloth, checker, mandelbrot, we will also need - // to resolve the UV channels. This means traversing UV inputs until we - // find the unconnected one that implicitly connects to uvSet[0] of the - // geometry, or an explicit uvChooser node connecting to alternate uvSets. - } else { - varnameInput.Set(UsdUtilsGetPrimaryUVSetName()); - } + const SdfPath primvarReaderShaderPath = getPlace2DTexturePath(depNodeFn); + + if (!GetUsdStage()->GetPrimAtPath(primvarReaderShaderPath)) { + UsdShadeShader primvarReaderShaderSchema + = UsdShadeShader::Define(GetUsdStage(), primvarReaderShaderPath); + + primvarReaderShaderSchema.CreateIdAttr(VtValue(TrUsdTokens->UsdPrimvarReader_float2)); - UsdShadeOutput primvarReaderOutput - = primvarReaderShaderSchema.CreateOutput(TrUsdTokens->result, SdfValueTypeNames->Float2); + UsdShadeInput varnameInput + = primvarReaderShaderSchema.CreateInput(TrUsdTokens->varname, SdfValueTypeNames->Token); - // Connect the output of the primvar reader to the texture coordinate - // input of the UV texture. - texShaderSchema.CreateInput(TrUsdTokens->st, SdfValueTypeNames->Float2) - .ConnectToSource(primvarReaderOutput); + // We expose the primvar reader varname attribute to the material to allow + // easy specialization based on UV mappings to geometries: + SdfPath materialPath = GetUsdPath().GetParentPath(); + UsdShadeMaterial materialSchema(GetUsdStage()->GetPrimAtPath(materialPath)); + while (!materialSchema && !materialPath.IsEmpty()) { + materialPath = materialPath.GetParentPath(); + materialSchema = UsdShadeMaterial(GetUsdStage()->GetPrimAtPath(materialPath)); + } + + if (materialSchema) { + TfToken inputName( + TfStringPrintf("%s:%s", depNodeFn.name().asChar(), TrUsdTokens->varname.GetText())); + UsdShadeInput materialInput + = materialSchema.CreateInput(inputName, SdfValueTypeNames->Token); + materialInput.Set(UsdUtilsGetPrimaryUVSetName()); + varnameInput.ConnectToSource(materialInput); + // Note: This needs to be done for all nodes that require UV input. In + // the UsdPreviewSurface case, the file node is the only one, but for + // other Maya nodes like cloth, checker, mandelbrot, we will also need + // to resolve the UV channels. This means traversing UV inputs until we + // find the unconnected one that implicitly connects to uvSet[0] of the + // geometry, or an explicit uvChooser node connecting to alternate uvSets. + } else { + varnameInput.Set(UsdUtilsGetPrimaryUVSetName()); + } + + UsdShadeOutput primvarReaderOutput = primvarReaderShaderSchema.CreateOutput( + TrUsdTokens->result, SdfValueTypeNames->Float2); + + // Connect the output of the primvar reader to the texture coordinate + // input of the UV texture. + texShaderSchema.CreateInput(TrUsdTokens->st, SdfValueTypeNames->Float2) + .ConnectToSource(primvarReaderOutput); + } else { + // Re-using an existing primvar reader: + UsdShadeShader primvarReaderShaderSchema( + GetUsdStage()->GetPrimAtPath(primvarReaderShaderPath)); + UsdShadeOutput primvarReaderOutput + = primvarReaderShaderSchema.GetOutput(TrUsdTokens->result); + + // Connect the output of the primvar reader to the texture coordinate + // input of the UV texture. + texShaderSchema.CreateInput(TrUsdTokens->st, SdfValueTypeNames->Float2) + .ConnectToSource(primvarReaderOutput); + } } /* virtual */ @@ -503,82 +517,129 @@ void PxrUsdTranslators_FileTextureWriter::WriteTransform2dNode( } // Get the TexCoordReader node and its output "result" - const SdfPath primvarReaderShaderPath - = texShaderSchema.GetPath().AppendChild(_tokens->PrimvarReaderShaderName); - + const SdfPath primvarReaderShaderPath = getPlace2DTexturePath(depNodeFn); const UsdShadeShader primvarReaderShader = texShaderSchema.Get(GetUsdStage(), primvarReaderShaderPath); const UsdShadeOutput primvarReaderShaderOutput = primvarReaderShader.GetOutput(TrUsdTokens->result); - // Create the Transform2d node as a child of the UsdUVTexture node - const SdfPath transform2dShaderPath - = texShaderSchema.GetPath().AppendChild(_tokens->UsdTransform2dShaderName); - UsdShadeShader transform2dShaderSchema - = UsdShadeShader::Define(GetUsdStage(), transform2dShaderPath); - - transform2dShaderSchema.CreateIdAttr(VtValue(TrUsdTokens->UsdTransform2d)); - - // Create the Transform2d input "in" attribute and connect it - // to the TexCoordReader output "result" - transform2dShaderSchema.CreateInput(TrUsdTokens->in, SdfValueTypeNames->Float2) - .ConnectToSource(primvarReaderShaderOutput); - - // Compute the Transform2d values, converting from Maya's coordinates to USD coordinates - - // Maya's place2dtexture transform order seems to be `in * T * S * R`, where the rotation - // pivot is (0.5, 0.5) and scale pivot is (0,0). USD's Transform2d transform order is `in * - // S * R * T`, where the rotation and scale pivots are (0,0). This conversion translates - // from place2dtexture's UV space to Transform2d's UV space: `in * S * T * Rpivot_inverse * - // R * Rpivot` - GfMatrix4f pivotXform = GfMatrix4f().SetTranslate(GfVec3f(0.5, 0.5, 0)); - GfMatrix4f translateXform - = GfMatrix4f().SetTranslate(GfVec3f(translationValue[0], translationValue[1], 0)); - GfRotation rotation = GfRotation(GfVec3f::ZAxis(), rotationValue); - GfMatrix4f rotationXform = GfMatrix4f().SetRotate(rotation); - GfVec3f scale; - if (fabs(scaleValue[0]) <= std::numeric_limits::epsilon() - || fabs(scaleValue[1]) <= std::numeric_limits::epsilon()) { - TF_WARN( - "At least one of the components of RepeatUV for %s are set to zero. To avoid divide " - "by zero exceptions, these values are changed to the smallest finite float greater " - "than zero.", - UsdMayaUtil::GetMayaNodeName(GetMayaObject()).c_str()); - - scale = GfVec3f( - 1.0 / std::max(scaleValue[0], std::numeric_limits::min()), - 1.0 / std::max(scaleValue[1], std::numeric_limits::min()), - 1.0); + // We have two cases. If the node is connected to a place2DTransform, then the transform data + // was on the placement node. If not, then the transform data was on the file node. + std::string usdUvTransformName; + if (primvarReaderShaderPath.GetName() == _tokens->PrimvarReaderShaderName.GetString()) { + usdUvTransformName = TfStringPrintf( + "%s_%s", depNodeFn.name().asChar(), _tokens->UsdTransform2dShaderName.GetText()); + } else { - scale = GfVec3f(1.0 / scaleValue[0], 1.0 / scaleValue[1], 1.0); - } + usdUvTransformName = TfStringPrintf( + "%s_%s", + primvarReaderShaderPath.GetName().c_str(), + _tokens->UsdTransform2dShaderName.GetText()); + } + + const SdfPath transform2dShaderPath = texShaderSchema.GetPath().GetParentPath().AppendChild( + TfToken(usdUvTransformName.c_str())); + + if (!GetUsdStage()->GetPrimAtPath(transform2dShaderPath)) { + // Create the Transform2d node as a child of the UsdUVTexture node + UsdShadeShader transform2dShaderSchema + = UsdShadeShader::Define(GetUsdStage(), transform2dShaderPath); + + transform2dShaderSchema.CreateIdAttr(VtValue(TrUsdTokens->UsdTransform2d)); + + // Create the Transform2d input "in" attribute and connect it + // to the TexCoordReader output "result" + transform2dShaderSchema.CreateInput(TrUsdTokens->in, SdfValueTypeNames->Float2) + .ConnectToSource(primvarReaderShaderOutput); + + // Compute the Transform2d values, converting from Maya's coordinates to USD coordinates + + // Maya's place2dtexture transform order seems to be `in * T * S * R`, where the rotation + // pivot is (0.5, 0.5) and scale pivot is (0,0). USD's Transform2d transform order is `in * + // S * R * T`, where the rotation and scale pivots are (0,0). This conversion translates + // from place2dtexture's UV space to Transform2d's UV space: `in * S * T * Rpivot_inverse * + // R * Rpivot` + GfMatrix4f pivotXform = GfMatrix4f().SetTranslate(GfVec3f(0.5, 0.5, 0)); + GfMatrix4f translateXform + = GfMatrix4f().SetTranslate(GfVec3f(translationValue[0], translationValue[1], 0)); + GfRotation rotation = GfRotation(GfVec3f::ZAxis(), rotationValue); + GfMatrix4f rotationXform = GfMatrix4f().SetRotate(rotation); + GfVec3f scale; + if (fabs(scaleValue[0]) <= std::numeric_limits::epsilon() + || fabs(scaleValue[1]) <= std::numeric_limits::epsilon()) { + TF_WARN( + "At least one of the components of RepeatUV for %s are set to zero. To avoid " + "divide " + "by zero exceptions, these values are changed to the smallest finite float greater " + "than zero.", + UsdMayaUtil::GetMayaNodeName(GetMayaObject()).c_str()); + + scale = GfVec3f( + 1.0 / std::max(scaleValue[0], std::numeric_limits::min()), + 1.0 / std::max(scaleValue[1], std::numeric_limits::min()), + 1.0); + } else { + scale = GfVec3f(1.0 / scaleValue[0], 1.0 / scaleValue[1], 1.0); + } + + GfMatrix4f scaleXform = GfMatrix4f().SetScale(scale); - GfMatrix4f scaleXform = GfMatrix4f().SetScale(scale); + GfMatrix4f transform + = scaleXform * translateXform * pivotXform.GetInverse() * rotationXform * pivotXform; + GfVec3f translationResult = transform.ExtractTranslation(); + translationValue.Set(translationResult[0], translationResult[1]); - GfMatrix4f transform - = scaleXform * translateXform * pivotXform.GetInverse() * rotationXform * pivotXform; - GfVec3f translationResult = transform.ExtractTranslation(); - translationValue.Set(translationResult[0], translationResult[1]); + // Create and set the Transform2d input attributes + transform2dShaderSchema.CreateInput(TrUsdTokens->translation, SdfValueTypeNames->Float2) + .Set(translationValue); - // Create and set the Transform2d input attributes - transform2dShaderSchema.CreateInput(TrUsdTokens->translation, SdfValueTypeNames->Float2) - .Set(translationValue); + transform2dShaderSchema.CreateInput(TrUsdTokens->rotation, SdfValueTypeNames->Float) + .Set(rotationValue); - transform2dShaderSchema.CreateInput(TrUsdTokens->rotation, SdfValueTypeNames->Float) - .Set(rotationValue); + transform2dShaderSchema.CreateInput(TrUsdTokens->scale, SdfValueTypeNames->Float2) + .Set(scaleValue); - transform2dShaderSchema.CreateInput(TrUsdTokens->scale, SdfValueTypeNames->Float2) - .Set(scaleValue); + // Create the Transform2d output "result" attribute + UsdShadeOutput transform2dOutput + = transform2dShaderSchema.CreateOutput(TrUsdTokens->result, SdfValueTypeNames->Float2); + + // Get and connect the file texture input "st" to the Transform2d output "result" + UsdShadeInput texShaderSchemaInput = texShaderSchema.GetInput(TrUsdTokens->st); + texShaderSchemaInput.ConnectToSource(transform2dOutput); + } else { + // Re-using an existing transform node: + UsdShadeShader transform2dShaderSchema(GetUsdStage()->GetPrimAtPath(transform2dShaderPath)); + UsdShadeOutput transform2dOutput = transform2dShaderSchema.GetOutput(TrUsdTokens->result); + // Get and connect the file texture input "st" to the Transform2d output "result" + UsdShadeInput texShaderSchemaInput = texShaderSchema.GetInput(TrUsdTokens->st); + texShaderSchemaInput.ConnectToSource(transform2dOutput); + } +} - // Create the Transform2d output "result" attribute - UsdShadeOutput transform2dOutput - = transform2dShaderSchema.CreateOutput(TrUsdTokens->result, SdfValueTypeNames->Float2); +SdfPath +PxrUsdTranslators_FileTextureWriter::getPlace2DTexturePath(const MFnDependencyNode& depNodeFn) +{ + MStatus status; + std::string usdUvTextureName; + const MPlug plug = depNodeFn.findPlug( + TrMayaTokens->uvCoord.GetText(), + /* wantNetworkedPlug = */ true, + &status); + if (status == MS::kSuccess && plug.isDestination(&status)) { + MPlug source = plug.source(&status); + if (status == MS::kSuccess && !source.isNull()) { + MFnDependencyNode sourceNode(source.node()); + usdUvTextureName = sourceNode.name().asChar(); + } + } + + if (usdUvTextureName.empty()) { + // We want a single UV reader for all file nodes not connected to a place2DTexture node + usdUvTextureName = _tokens->PrimvarReaderShaderName.GetString(); + } - // Get and connect the TexCoordReader input "st" to the Transform2d - // output "result" - UsdShadeInput texShaderSchemaInput = texShaderSchema.GetInput(TrUsdTokens->st); - texShaderSchemaInput.ConnectToSource(transform2dOutput); + return GetUsdPath().GetParentPath().AppendChild(TfToken(usdUvTextureName.c_str())); } /* virtual */ diff --git a/test/lib/mayaUsd/fileio/testShaderWriter.py b/test/lib/mayaUsd/fileio/testShaderWriter.py index e58ea9318d..222bde4fbb 100644 --- a/test/lib/mayaUsd/fileio/testShaderWriter.py +++ b/test/lib/mayaUsd/fileio/testShaderWriter.py @@ -90,6 +90,8 @@ def testSimpleShaderWriter(self): ".noiseUV", ".vertexUvOne", ".vertexUvTwo", ".vertexUvThree", ".vertexCameraOne"): cmds.connectAttr(uv_node + att_name, file_node + att_name, f=True) + cmds.connectAttr(uv_node + ".outUV", file_node + ".uvCoord", f=True) + cmds.connectAttr(uv_node + ".outUvFilterSize", file_node + ".uvFilterSize", f=True) cmds.connectAttr(file_node + ".outColor", material_node + ".color", f=True) diff --git a/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py b/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py index f89d8ad7c8..7376fd9ca3 100644 --- a/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py +++ b/test/lib/usd/translators/testUsdExportImportRoundtripPreviewSurface.py @@ -31,6 +31,15 @@ except ImportError: pass +def connectUVNode(uv_node, file_node): + for att_name in (".coverage", ".translateFrame", ".rotateFrame", + ".mirrorU", ".mirrorV", ".stagger", ".wrapU", + ".wrapV", ".repeatUV", ".offset", ".rotateUV", + ".noiseUV", ".vertexUvOne", ".vertexUvTwo", + ".vertexUvThree", ".vertexCameraOne"): + cmds.connectAttr(uv_node + att_name, file_node + att_name, f=True) + cmds.connectAttr(uv_node + ".outUV", file_node + ".uvCoord", f=True) + cmds.connectAttr(uv_node + ".outUvFilterSize", file_node + ".uvFilterSize", f=True) class testUsdExportImportRoundtripPreviewSurface(unittest.TestCase): @@ -97,12 +106,7 @@ def __testUsdPreviewSurfaceRoundtrip(self, isColorManaged=True) uv_node = cmds.shadingNode("place2dTexture", asUtility=True) - for att_name in (".coverage", ".translateFrame", ".rotateFrame", - ".mirrorU", ".mirrorV", ".stagger", ".wrapU", - ".wrapV", ".repeatUV", ".offset", ".rotateUV", - ".noiseUV", ".vertexUvOne", ".vertexUvTwo", - ".vertexUvThree", ".vertexCameraOne"): - cmds.connectAttr(uv_node + att_name, file_node + att_name, f=True) + connectUVNode(uv_node, file_node) cmds.connectAttr(file_node + ".outColor", material_node + ".diffuseColor", f=True) @@ -349,6 +353,93 @@ def testDisplayColorLossyRoundtrip(self): self.assertTrue(mark.IsClean()) + def testUVReaderMerging(self): + """ + Test that we produce a minimal number of UV readers + """ + cmds.file(f=True, new=True) + + sphere_xform = cmds.polySphere()[0] + + material_node = cmds.shadingNode("usdPreviewSurface", asShader=True, + name="ss01") + material_sg = cmds.sets(renderable=True, noSurfaceShader=True, + empty=True, name="ss01SG") + cmds.connectAttr(material_node+".outColor", + material_sg+".surfaceShader", force=True) + cmds.sets(sphere_xform, e=True, forceElement=material_sg) + + # One file with UVs connected to diffuse: + file_node = cmds.shadingNode("file", asTexture=True, + isColorManaged=True) + uv_node = cmds.shadingNode("place2dTexture", asUtility=True) + cmds.setAttr(uv_node + ".offsetU", 0.125) + cmds.setAttr(uv_node + ".offsetV", 0.5) + connectUVNode(uv_node, file_node) + cmds.connectAttr(file_node + ".outColor", + material_node + ".diffuseColor", f=True) + + # Another file, same UVs, connected to emissiveColor + file_node = cmds.shadingNode("file", asTexture=True, + isColorManaged=True) + connectUVNode(uv_node, file_node) + cmds.connectAttr(file_node + ".outColor", + material_node + ".emissiveColor", f=True) + + # Another file, no UVs, connected to metallic + file_node = cmds.shadingNode("file", asTexture=True, + isColorManaged=True) + cmds.connectAttr(file_node + ".outColorR", + material_node + ".metallic", f=True) + + # Another file, no UVs, connected to roughness + file_node = cmds.shadingNode("file", asTexture=True, + isColorManaged=True) + cmds.connectAttr(file_node + ".outColorR", + material_node + ".roughness", f=True) + cmds.setAttr(file_node + ".offsetU", 0.25) + cmds.setAttr(file_node + ".offsetV", 0.75) + + # Export to USD: + usd_path = os.path.abspath('MinimalUVReader.usda') + cmds.usdExport(mergeTransformAndShape=True, + file=usd_path, + shadingMode='useRegistry', + exportDisplayColor=True) + + # We expect 2 primvar readers, and 2 st transforms: + stage = Usd.Stage.Open(usd_path) + mat_path = "/pSphere1/Looks/ss01SG/" + + # Here are the expected connections in the produced USD file: + connections = [ + # Source node, input, destination node: + ("ss01", "diffuseColor", "file1"), + ("file1", "st", "place2dTexture1_UsdTransform2d"), + ("place2dTexture1_UsdTransform2d", "in", "place2dTexture1"), + + ("ss01", "emissiveColor", "file2"), + ("file2", "st", "place2dTexture1_UsdTransform2d"), # re-used + # Note that the transform name is derived from place2DTexture name. + + ("ss01", "metallic", "file3"), + ("file3", "st", "shared_TexCoordReader"), # no UV in Maya. + + ("ss01", "roughness", "file4"), + ("file4", "st", "file4_UsdTransform2d"), # xform on file node + ("file4_UsdTransform2d", "in", "shared_TexCoordReader") + # Note that the transform name is derived from file node name. + ] + for src_name, input_name, dst_name in connections: + src_prim = stage.GetPrimAtPath(mat_path + src_name) + self.assertTrue(src_prim) + src_shade = UsdShade.Shader(src_prim) + self.assertTrue(src_shade) + src_input = src_shade.GetInput(input_name) + self.assertTrue(src_input.HasConnectedSource()) + (connect_api, out_name, _) = src_input.GetConnectedSource() + self.assertEqual(connect_api.GetPath(), mat_path + dst_name) + @unittest.skipUnless("mayaUtils" in globals() and mayaUtils.mayaMajorVersion() >= 2020, 'Requires standardSurface node which appeared in 2020.') def testOpacityRoundtrip(self): """