Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix VP2 rendering of UsdPreviewSurface with opacityThreshold #3947

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
58 changes: 36 additions & 22 deletions lib/usd/pxrUsdPreviewSurface/usdPreviewSurface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@ MStatus PxrMayaUsdPreviewSurface::initialize()

status = attributeAffects(opacityAttr, outTransparencyOnAttr);
CHECK_MSTATUS_AND_RETURN_IT(status);
status = attributeAffects(opacityThresholdAttr, outTransparencyOnAttr);
CHECK_MSTATUS_AND_RETURN_IT(status);

return status;
}
Expand Down Expand Up @@ -461,32 +463,44 @@ MStatus PxrMayaUsdPreviewSurface::compute(const MPlug& plug, MDataBlock& dataBlo
// details. We don't use the user-visible "outTransparency" attribute for transparency test
// because its value depends on upstream nodes and thus error-prone when the "opacity" plug
// is connected to certain textures. In that case, we should enable transparency.
bool opacityConnected = false;

const MObject opacityAttr
= depNodeFn.attribute(PxrMayaUsdPreviewSurfaceTokens->OpacityAttrName.GetText());
const MPlug opacityPlug(thisMObject(), opacityAttr);
if (opacityPlug.isConnected()) {
const MPlug sourcePlug = opacityPlug.source(&status);
CHECK_MSTATUS(status);
const MObject sourceNode = sourcePlug.node(&status);
CHECK_MSTATUS(status);

// Anim curve output will be evaluated to determine if transparency should be enabled.
if (!sourceNode.hasFn(MFn::kAnimCurve)) {
opacityConnected = true;
}
}
MObject opacityThresholdAttr = depNodeFn.attribute(
PxrMayaUsdPreviewSurfaceTokens->OpacityThresholdAttrName.GetText());
const MDataHandle opacityThresholdData
= dataBlock.inputValue(opacityThresholdAttr, &status);
CHECK_MSTATUS(status);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you replicate the opacityConnected code you see for opacity, but on opacityThreshold? A connected opacityThreshold should also activate masked mode.

Copy link
Contributor Author

@jufrantz jufrantz Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @JGamache-autodesk for your feedback,

Interesting point, but I think I disagree with this solution.

As you know, marking the whole material as transparent informs the rasterizer to use alpha blending for proper rendering (e.g. GL_BLEND and object sorting). Conversely, opaque material allows the rasterizer to perform simple depth testing and overwrite fragments. Moreover for masked materials, some fragments will be discarded without writing to the depth or color buffers, which still works with simple depth testing.

OpacityThreshold functions similarly to a combination of glTF alphaMode and alphaCutoff. For a material with authored opacity:

  • A zero opacityThreshold is equivalent to glTF.alphaMode = BLEND
  • Positive values would translate to glTF.alphaMode=MASK and glTF.alphaCutoff=opacityThreshold.

Texturing opacityThreshold is akin to texturing both alphaCutoff AND alphaMode: since you can get zero-valued threshold texels, you would expect a partially masked and partially translucent material. In this scenario, to ensure proper rendering of the semi-transparent regions, the only viable solution to me is to render these objects in the transparent pass (that would also still work with alpha cut/discarded regions).

For reference, HdStorm’s implementation of masked detection for GLSLFX UsdPreviewSurface only tests for a constant positive opacityThreshold value, while for translucent it tests opacity input connections and decides that a varying opacity differs from plain one, like the current VP2 shading node implementation.

https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/imaging/hdSt/materialNetwork.cpp#L75-L83

    // Next check for authored terminal.opacityThreshold value > 0
    for (auto const& paramIt : terminal.parameters) {
        if (paramIt.first != _tokens->opacityThreshold) continue;


        VtValue const& vtOpacityThreshold = paramIt.second;
        if (vtOpacityThreshold.Get<float>() > 0.0f) {
            return HdStMaterialTagTokens->masked;
        }
    }

Moreover, I may have over-interpreted it but - as I understand it - the USDPreviewSurface Specification discourages a material with a texture used for translucency and mask.

Thus,` the opacityThreshold serves as a switch for how the opacity input is interpreted; this 'translucent or masked' behavior is common in engines and renderers, and makes the UsdPreviewSurface easier to interchange. It does imply, however, that it is not possible to faithfully recreate a glassy/translucent material that also provides an opacity-based mask… so no single-polygon glass leaves.

If we want to improve this, an alternative could be to disallow connections to the opacityThreshold parameter during VP2 shader translation. This restriction can be implemented with MPxShadingNodeOverride.

void PxrMayaUsdPreviewSurfaceShadingNodeOverride::getCustomMappings(

Please let me know if this approach meets your requirements,
Best,
Julien

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good explanation. I agree with you. This would mean the implementation of _GetUsdPreviewSurfaceMaterialTag() I added to materialXFilter.cpp is buggy since it considers textured opacityThreshold as masked.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably no need to forbid connections since the result is not fatal and will render correctly, albeit slowly, in a transparency pass.


float transparencyOn = false;
if (opacityConnected) {
transparencyOn = true;
} else {
const MDataHandle opacityData = dataBlock.inputValue(opacityAttr, &status);
CHECK_MSTATUS(status);
const float opacity = opacityData.asFloat();
if (opacity < 1.0f - std::numeric_limits<float>::epsilon()) {

// For masked transparency, we want VP2 to treat the shader as opaque.
if (opacityThresholdData.asFloat() <= 0.0f) {
bool opacityConnected = false;

const MObject opacityAttr
= depNodeFn.attribute(PxrMayaUsdPreviewSurfaceTokens->OpacityAttrName.GetText());
const MPlug opacityPlug(thisMObject(), opacityAttr);
if (opacityPlug.isConnected()) {
const MPlug sourcePlug = opacityPlug.source(&status);
CHECK_MSTATUS(status);
const MObject sourceNode = sourcePlug.node(&status);
CHECK_MSTATUS(status);

// Anim curve output will be evaluated to determine if transparency should be
// enabled.
if (!sourceNode.hasFn(MFn::kAnimCurve)) {
opacityConnected = true;
}
}

if (opacityConnected) {
transparencyOn = true;
} else {
const MDataHandle opacityData = dataBlock.inputValue(opacityAttr, &status);
CHECK_MSTATUS(status);
const float opacity = opacityData.asFloat();
if (opacity < 1.0f - std::numeric_limits<float>::epsilon()) {
transparencyOn = true;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# limitations under the License.
#

import unittest

import fixturesUtils
import imageUtils

Expand Down Expand Up @@ -271,6 +273,7 @@ def testDoubleSided(self):
mayaUtils.setBasicCamera(3)
self.assertSnapshotClose('doubleSided_disabled_back.png')

@unittest.skipUnless(int(os.getenv("MAYA_LIGHTAPI_VERSION")) >= 2, 'UsdPreviewSurface fragment graph for light API v1 does not support opacityThreshold')
def testOpacityThreshold(self):
'''
Test UsdPreviewSurface transparency cut-out. The surface fragments should be
Expand Down