-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Autodesk: Optimize MaterialX shader generation in Storm by comparing topologies #3073
Autodesk: Optimize MaterialX shader generation in Storm by comparing topologies #3073
Conversation
Fixes: - MaterialX: Storm fails to reuse GLSL shaders for networks differing only on node names PixarAnimationStudios#2330 - Storm shader compilation errors when using MaterialX triplanarprojection node. PixarAnimationStudios#3004 - Improve transparency detection and fix generation Using a topo-trimmed network resulted in all shaders being generated with opaque semantics. The shaders are now created with the right transparent hardware settings. Also improved transparency detection at the _GetMaterialTag level by adding more checks for known surfaces and delegating to MaterialX when dealing with custom shaders, but without generating the full MaterialX document.
Primary goal was to only reconstruct the MaterialX document if we are really going all the way to compiling the OpenGL shader. This means we prioritize gathering the Storm requirements from: To do this, we reduce the HdMaterialNetwork2 to a topology equivalent skeleton network that will produce the exact same shader code. We generate a hash of this skeleton network and use it to fetch a previously compiled MaterialX shader. With this we can generate the uniform block required by Storm using the default values found in the shader, modified by the values found in the original network. Some extra work was required to make sure the final HdMaterialNetwork had all the requirements for correctly findings the textures to load. Testing shows that for the scene submitted in #2330 we now only generate a single OpenGL shader. Time to first frame is 10X faster. Also, a scene with a simple animated material (animated |
(transmission) | ||
|
||
(open_pbr_surface) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added programmatic detection for both OpenPBR and glTf surfaces to be able to get the materialTag without having to peek into the MaterialX NodeGraph implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, just merged #3069 ... Any quick opinions on how to merge those changes vs these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR also includes detection for glTf textures via a _GetGlTFSurfaceMaterialTag() function that properly differentiate between translucent and masked mode. The merge resolution strategy would be to let this PR fully win over #3069, equivalent to reverting it before merge.
I have opened Pablo's asset with my local build and it renders correctly.
_topologicalTokens, | ||
// This represents living knowledge of the internals of the MaterialX shader generator | ||
// for both GLSL and Metal. Such knowledge should reside inside the generator class | ||
// provided by MaterialX. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be submitted to MaterialX for 1.39.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the intent here to have bool MaterialXIsNodeTopological(std::string name) or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd definitely be interested in this code moving into MX, for the record :D.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the information being dependent on ShaderGen target, it should probably be static bool MaterialX::GlslShaderGenerator::isNodeDefTopological(MaterialX::NodeDefPtr const&)
I do love the fact that we can get all the required info from Sdr and compare via TfTokens without having to use the MaterialX library, but in this case correctness trumps convenience, so this will definitely be added to MaterialX. If performance matters, we can always cache the result since the return value will always be the same for a given nodeId.
{ | ||
bool hasTransparency = mx::isTransparentSurface(mxElem, | ||
mxContext.getShaderGenerator().getTarget()); | ||
bool hasTransparency = mxContext.getOptions().hwTransparency; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should have been correctly set in mxContext
.
outNode.parameters.insert(param); | ||
// Need an empty asset as well: | ||
outNode.parameters.emplace(TfToken(colorManagedFile.first), | ||
VtValue{SdfAssetPath{}}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be incorrect if there are plans to color-correct color3 values on nodes that are not file based.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're planning to, in some form, but I think we can update this bit when we get to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We actually already support that for MaterialX - there is an existing test case but it looks like the opensource thresholds were not tight enough to catch this. Let me know if you are good with this change to catch color corrected values
const auto colorManagedInput =
SdfPath::StripPrefixNamespace(param.first.GetString(), SdfFieldKeys->ColorSpace);
// If this parameter indicates a colorspace on a color
// managed input, find and add that corresponding input
if (colorManagedInput.second) {
outNode.parameters.insert(param);
// Get the parameter value for the input
const TfToken colorInputParam(colorManagedInput.first);
const auto colorInputIt = inNode.parameters.find(colorInputParam);
if (colorInputIt != inNode.parameters.end()) {
VtValue colorInputValue = colorInputIt->second;
// Use an empty asset for color managed files
if (colorInputValue.IsHolding<SdfAssetPath>()) {
colorInputValue = VtValue(SdfAssetPath());
}
outNode.parameters.emplace(colorInputParam, colorInputValue);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly there will not be a value to be found in the data since it all gets pruned to create a pure "default" shader. But the answer we are looking for can be found in the Sdr registry with code pretty close to this:
const auto colorManagedInput =
SdfPath::StripPrefixNamespace(param.first.GetString(), SdfFieldKeys->ColorSpace);
// If this parameter indicates a colorspace on a color
// managed input, we need to find out if it manages an asset.
if (colorManagedInput.second) {
outNode.parameters.insert(param);
// Get the parameter type for the input
const TfToken colorInputParam(colorManagedInput.first);
SdrRegistry &sdrRegistry = SdrRegistry::GetInstance();
const SdrShaderNodeConstPtr sdrNode =
sdrRegistry.GetShaderNodeByIdentifierAndType(inNode.nodeTypeId, _tokens->mtlx);
auto sdrInput = sdrNode->GetShaderInput(colorInputParam);
if (sdrInput->GetTypeAsSdfType().first == SdfValueTypeNames->Asset) {
// Use an empty asset for color managed files
colorInputValue = VtValue(SdfAssetPath());
outNode.parameters.emplace(colorInputParam, colorInputValue);
}
}
The other solution would be to fix _AddMaterialXNode()
in usd\pxr\imaging\hdMtlx\hdMtlx.cpp
to change this block:
// Skip Colorspace parameter, this is already captured in the paramData.
// Note: Colorspace inputNames are of the form 'colorSpace:inputName'
const std::pair<std::string, bool> result =
SdfPath::StripPrefixNamespace(mxInputName, SdfFieldKeys->ColorSpace);
if (result.second) {
continue;
}
to:
// Colorspace parameter: create an empty input if it does not already
// exist to set colorspace metadata.
// Note: Colorspace inputNames are of the form 'colorSpace:inputName'
const std::pair<std::string, bool> result =
SdfPath::StripPrefixNamespace(mxInputName, SdfFieldKeys->ColorSpace);
if (result.second) {
mx::InputPtr mxInput = mxNode->addInputFromNodeDef(result .first);
mxInput->setColorSpace(HdMtlxConvertToString(paramData.value));
continue;
}
Which would allow not having to create a dummy value in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually had to fix this on our side since it impacted our internal unit tests. The following function was added:
// MaterialX requires a value for color-spaced attributes. Fetch the default from Sdr shader:
void _AddMaterialXDefaultValue(HdMaterialNode2& node, std::string const& parameterName) {
SdrRegistry &sdrRegistry = SdrRegistry::GetInstance();
const SdrShaderNodeConstPtr sdrNode = sdrRegistry.GetShaderNodeByIdentifierAndType(
node.nodeTypeId, _tokens->mtlx);
if (sdrNode) {
SdrShaderPropertyConstPtr sdrInput = sdrNode->GetShaderInput(TfToken(parameterName));
if (sdrInput) {
if (sdrInput->IsAssetIdentifier()) {
// Replace empty filename with a placeholder ignored by MaterialX shadergen:
node.parameters.emplace(TfToken(parameterName),
VtValue{SdfAssetPath{"placeholder"}});
} else {
node.parameters.emplace(TfToken(parameterName),
sdrInput->GetDefaultValueAsSdfType());
}
}
}
}
and was called:
@@ -282,9 +303,8 @@ size_t _BuildEquivalentMaterialNetwork(
SdfFieldKeys->ColorSpace);
if (colorManagedFile.second) {
outNode.parameters.insert(param);
- // Need an empty asset as well:
- outNode.parameters.emplace(TfToken(colorManagedFile.first),
- VtValue{SdfAssetPath{}});
+ // We need a value as well:
+ _AddMaterialXDefaultValue(outNode, colorManagedFile.first);
}
}
}
And this made sure there was a correct value for the MaterialX code.
|
||
// Replace the filenameInput parameter with a connection to a new texture node | ||
static void | ||
_ReplaceFilenameInput( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved below at line 1158. Heavily modified to only require the Hydra network.
} | ||
|
||
static std::string const& | ||
_GetMaterialTag(HdMaterialNode2 const& terminal) | ||
_GetUsdPreviewSurfaceMaterialTag(HdMaterialNode2 const& terminal) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a per nodetype function since generic code does not work all the time. Especially true for UsdPreviewSurface and gfTF since they both support masked
mode and this prevents automatic detection from properly deducing the material tag.
} | ||
auto const& lineSeparator = | ||
(i == block.size() - 1) ? mx::EMPTY_STRING : separator; | ||
line += _EmitMxVertexDataLine(block[i], lineSeparator); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found while testing some assets. Make sure we do not add the commas in the first place because there might be more than one to remove afterwards.
" #else\n" | ||
" %s(0.0),\n" | ||
" %s(0.0)%s\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where the unremovable extra commas were added.
@@ -70,7 +70,6 @@ float displacement; | |||
float occlusion; | |||
|
|||
// Uniform block: PrivateUniforms | |||
float u_alphaThreshold = 0.001000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were incorrectly detected as transparent by MaterialX. The new code that enforces transparency using the materialTag is correct so I had to fix the baseline.
Filed as internal issue #USD-9646 |
/AzurePipelines run |
Azure Pipelines successfully started running 1 pipeline(s). |
// Everything boils down to an <image> node. We might have to dig it out of | ||
// the nodegraph. Unsure about triplanar that has 3 image nodes. Does Storm | ||
// require per-image texture params? How does one specify that using a single | ||
// token? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you talking about sampler parameters (e.g. wrap mode)? Hydra takes them specified per-texture, and in the texture metadata we're gathering we should have a per-texture entry for everything. If the triplanar node only has one set of params, we can just splat that to all of the referenced textures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sample parameters could theoretically vary from one texture to the other. This is not the case with triplanar, but a custom NodeDef/NodeGraph node with multiple texture could try having one texture repeat while the other clamps. This is a bit of a corner case though, so unsure how much energy we should invest into covering it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to leave it until we have a concrete asset we're trying to get working.
{ | ||
_AddDefaultMtlxTextureValues(hdTextureParams); | ||
|
||
if (nodeDef->getCategory() == mx::ShaderNode::IMAGE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the category for nodeDef's 'nodeDef' should we be checking the category on the mtlx Node itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Checking on the node would indeed work. The other option is to use nodeDef->getNodeString()
instead. Do you want me to update the PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, unit tests passing, as well as my test scenes rendering correctly, give a strong indication that this code block should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree I think it should be removed. My next question was going to be how is it different from the other _AddDefaultMtlxTextureValues()
above, which seems to be the reason the tests were ok.
Am I understanding this function correctly in that the first _AddDefaultMtlxTextureValues()
call handles the <image>
and <tiledimage>
nodes and the rest is intended to handle any other custom texture based nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are looking for default values and indeed those default are correct for mx::ShaderNode::IMAGE
. One more reason to remove the if (nodeDef->getCategory() == mx::ShaderNode::IMAGE)
block of code.
The first _AddDefaultMtlxTextureValues()
can probably be merged into the second one since it is the only place where it is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just browsed the MaterialX library, and all image nodes used in NodeGraphs are repeat
, which mean the defaults are sufficient for all cases I tested.
The only way to insure this code actually does something useful would be to create a custom untiledimage
NodeDef that does not have repeat
as the default on its embedded image node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to verify my understanding of this function. Since the first _AddDefaultMtlxTextureValues()
correctly gets the default values for both of the image based stdlib nodes, is the rest of the function (after that if block we agree should be removed) handling the case that we have a custom texture node that uses different default values for the wrap mode or filterType than the stdlib nodes?
} | ||
|
||
auto renderableNode = renderableElem->asA<mx::Node>(); | ||
if (renderableNode && renderableNode->getCategory() == "UsdPreviewSurface") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need special handling for UsdPreviewSurface materials? Would we not get the correct result by calling mx::isTransparentSurface()
below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, mx::isTransparentSurface
is only able to handle the most basic cases. It digs inside layers of NodeGraph until it finds either the opacity input of the ND_surface, or the weight input of a transparent Schlick node.
Then is has to traverse back up the connections to the calling node interface to check what was the actual value.
The problem is when there are intermediate nodes in that code path. MaterialX is able to deal with the luminance found in standard_surface that converts the color3 interface opacity input down to the float opacity input of the ND_surface node, but when the graph is more complex, as is the case with PreviewSurface and glTf, it becomes very hard to evaluate the expression in C++ to know or not if the surface is transparent.
In these cases, the mx::isTransparentSurface
will always return transparent, and this is why we need a custom test. This is also why I had to fix the output of a few PreviewSurface unit tests because the surface was not transparent anymore.
Description of Change(s)
Fixes:
Using a topo-trimmed network resulted in all shaders being generated with opaque semantics. The shaders are now created with the right transparent hardware settings.
Also improved transparency detection at the
_GetMaterialTag
level by adding more checks for known surfaces and delegating to MaterialX when dealing with custom shaders, but without generating the full MaterialX document.Fixes Issue(s)