USD: Add support for UsdPrimvarReader_TYPE in materials
Add support for the UsdPrimvarReader_TYPE templates for both import and export. These are used by several USD test assets and support here represents the last major piece of the UsdPreviewSurface spec to be implemented. On import these become `Attribute` nodes and on export the `Attribute` nodes will become `UsdPrimvarReader_TYPE`'s accordingly. Import: - `UsdPrimvarReader_float` and `UsdPrimvarReader_int` will use the `Fac` output - `UsdPrimvarReader_float3` and `UsdPrimvarReader_float4` will use the `Color` output - `UsdPrimvarReader_vector`, `UsdPrimvarReader_normal`, and `UsdPrimvarReader_point` will use the `Vector` output Export (only `Geometry` Attribute types are considered): - `Fac` will use `UsdPrimvarReader_float` - `Color` will use `UsdPrimvarReader_float3` - `Vector` will use `UsdPrimvarReader_vector` - `Alpha` is not considered MaterialX note: Hydra-native support is a bit more involved and will have to be done separately. Hydra w/USD sync is trivial to implement but those changes have been left out here. Pull Request: https://projects.blender.org/blender/blender/pulls/135143
This commit is contained in:
committed by
Jesse Yurkovich
parent
dc213cd79e
commit
46ec277713
@@ -1082,6 +1082,18 @@ bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input,
|
||||
else if (shader_id == usdtokens::UsdTransform2d) {
|
||||
convert_usd_transform_2d(source_shader, dest_node, dest_socket_name, ntree, column + 1, ctx);
|
||||
}
|
||||
else {
|
||||
/* Handle any remaining "generic" primvar readers. */
|
||||
StringRef shader_id_name(shader_id.GetString());
|
||||
if (shader_id_name.startswith("UsdPrimvarReader_")) {
|
||||
int64_t type_offset = shader_id_name.rfind('_');
|
||||
if (type_offset >= 0) {
|
||||
StringRef output_type = shader_id_name.drop_prefix(type_offset + 1);
|
||||
convert_usd_primvar_reader_generic(
|
||||
source_shader, output_type, dest_node, dest_socket_name, ntree, column + 1, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1418,6 +1430,73 @@ void USDMaterialReader::convert_usd_primvar_reader_float2(const pxr::UsdShadeSha
|
||||
link_nodes(ntree, uv_map, "UV", dest_node, dest_socket_name);
|
||||
}
|
||||
|
||||
void USDMaterialReader::convert_usd_primvar_reader_generic(const pxr::UsdShadeShader &usd_shader,
|
||||
const StringRef output_type,
|
||||
bNode *dest_node,
|
||||
const StringRefNull dest_socket_name,
|
||||
bNodeTree *ntree,
|
||||
const int column,
|
||||
NodePlacementContext &ctx) const
|
||||
{
|
||||
if (!usd_shader || !dest_node || !ntree) {
|
||||
return;
|
||||
}
|
||||
|
||||
bNode *attribute = ctx.get_cached_node(usd_shader);
|
||||
|
||||
if (attribute == nullptr) {
|
||||
const float2 loc = ctx.compute_node_loc(column);
|
||||
|
||||
/* Create the attribute node. */
|
||||
attribute = add_node(ntree, SH_NODE_ATTRIBUTE, loc);
|
||||
|
||||
/* Cache newly created node. */
|
||||
ctx.cache_node(usd_shader, attribute);
|
||||
|
||||
/* Set the attribute name. */
|
||||
pxr::UsdShadeInput varname_input = usd_shader.GetInput(usdtokens::varname);
|
||||
|
||||
/* First check if the shader's "varname" input is connected to another source,
|
||||
* and use that instead if so. */
|
||||
if (varname_input) {
|
||||
for (const pxr::UsdShadeConnectionSourceInfo &source_info :
|
||||
varname_input.GetConnectedSources())
|
||||
{
|
||||
pxr::UsdShadeShader shader = pxr::UsdShadeShader(source_info.source.GetPrim());
|
||||
pxr::UsdShadeInput secondary_varname_input = shader.GetInput(source_info.sourceName);
|
||||
if (secondary_varname_input) {
|
||||
varname_input = secondary_varname_input;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (varname_input) {
|
||||
pxr::VtValue varname_val;
|
||||
/* The varname input may be a string or TfToken, so just cast it to a string.
|
||||
* The Cast function is defined to provide an empty result if it fails. */
|
||||
if (varname_input.Get(&varname_val) && varname_val.CanCastToTypeid(typeid(std::string))) {
|
||||
std::string varname = varname_val.Cast<std::string>().Get<std::string>();
|
||||
if (!varname.empty()) {
|
||||
NodeShaderAttribute *storage = (NodeShaderAttribute *)attribute->storage;
|
||||
STRNCPY(storage->name, varname.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Connect to destination node input. */
|
||||
if (ELEM(output_type, "float", "int")) {
|
||||
link_nodes(ntree, attribute, "Fac", dest_node, dest_socket_name);
|
||||
}
|
||||
else if (ELEM(output_type, "float3", "float4")) {
|
||||
link_nodes(ntree, attribute, "Color", dest_node, dest_socket_name);
|
||||
}
|
||||
else if (ELEM(output_type, "vector", "normal", "point")) {
|
||||
link_nodes(ntree, attribute, "Vector", dest_node, dest_socket_name);
|
||||
}
|
||||
}
|
||||
|
||||
void build_material_map(const Main *bmain, blender::Map<std::string, Material *> &r_mat_map)
|
||||
{
|
||||
BLI_assert_msg(r_mat_map.is_empty(), "The incoming material map should be empty");
|
||||
|
||||
@@ -188,8 +188,6 @@ class USDMaterialReader {
|
||||
/**
|
||||
* This function creates a Blender UV Map node, under the simplifying assumption that
|
||||
* UsdPrimvarReader_float2 shaders output UV coordinates.
|
||||
* TODO(makowalski): investigate supporting conversion to other Blender node types
|
||||
* (e.g., Attribute Nodes) if needed.
|
||||
*/
|
||||
void convert_usd_primvar_reader_float2(const pxr::UsdShadeShader &usd_shader,
|
||||
const pxr::TfToken &usd_source_name,
|
||||
@@ -198,6 +196,13 @@ class USDMaterialReader {
|
||||
bNodeTree *ntree,
|
||||
int column,
|
||||
NodePlacementContext &ctx) const;
|
||||
void convert_usd_primvar_reader_generic(const pxr::UsdShadeShader &usd_shader,
|
||||
StringRef output_type,
|
||||
bNode *dest_node,
|
||||
const StringRefNull dest_socket_name,
|
||||
bNodeTree *ntree,
|
||||
int column,
|
||||
NodePlacementContext &ctx) const;
|
||||
};
|
||||
|
||||
/* Utility functions. */
|
||||
|
||||
@@ -61,7 +61,10 @@ static const pxr::TfToken preview_shader("previewShader", pxr::TfToken::Immortal
|
||||
static const pxr::TfToken preview_surface("UsdPreviewSurface", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken UsdTransform2d("UsdTransform2d", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken uv_texture("UsdUVTexture", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken primvar_float("UsdPrimvarReader_float", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken primvar_float2("UsdPrimvarReader_float2", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken primvar_float3("UsdPrimvarReader_float3", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken primvar_vector("UsdPrimvarReader_vector", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken roughness("roughness", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken specular("specular", pxr::TfToken::Immortal);
|
||||
static const pxr::TfToken opacity("opacity", pxr::TfToken::Immortal);
|
||||
@@ -119,6 +122,11 @@ static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &u
|
||||
static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &usd_export_context,
|
||||
const pxr::UsdShadeMaterial &material,
|
||||
bNode *node);
|
||||
static pxr::UsdShadeShader create_primvar_reader_shader(
|
||||
const USDExporterContext &usd_export_context,
|
||||
const pxr::UsdShadeMaterial &material,
|
||||
const pxr::TfToken &primvar_type,
|
||||
const bNode *node);
|
||||
static void create_uv_input(const USDExporterContext &usd_export_context,
|
||||
bNodeSocket *input_socket,
|
||||
pxr::UsdShadeMaterial &usd_material,
|
||||
@@ -183,14 +191,13 @@ static void process_inputs(const USDExporterContext &usd_export_context,
|
||||
continue;
|
||||
}
|
||||
|
||||
const InputSpec &input_spec = *spec;
|
||||
|
||||
/* Allow scaling inputs. */
|
||||
float input_scale = 1.0;
|
||||
|
||||
const InputSpec &input_spec = *spec;
|
||||
bNodeLink *input_link = traverse_channel(sock, SH_NODE_TEX_IMAGE);
|
||||
|
||||
/* Don't export emission color if strength is zero. */
|
||||
if (input_spec.input_name == usdtokens::emissive_color) {
|
||||
/* Don't export emission color if strength is zero. */
|
||||
const bNodeSocket *emission_strength_sock = bke::node_find_socket(
|
||||
*node, SOCK_IN, "Emission Strength");
|
||||
if (!emission_strength_sock) {
|
||||
@@ -203,6 +210,10 @@ static void process_inputs(const USDExporterContext &usd_export_context,
|
||||
}
|
||||
}
|
||||
|
||||
bool processed = false;
|
||||
|
||||
/* Check for an upstream Image node. */
|
||||
const bNodeLink *input_link = traverse_channel(sock, SH_NODE_TEX_IMAGE);
|
||||
if (input_link) {
|
||||
/* Convert the texture image node connected to this input. */
|
||||
bNode *input_node = input_link->fromnode;
|
||||
@@ -347,10 +358,60 @@ static void process_inputs(const USDExporterContext &usd_export_context,
|
||||
opacity_threshold_input.GetAttr().Set(pxr::VtValue(threshold));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (input_spec.set_default_value) {
|
||||
/* Set hardcoded value. */
|
||||
|
||||
processed = true;
|
||||
}
|
||||
|
||||
if (processed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* No upstream Image was found. Check for an Attribute node instead */
|
||||
input_link = traverse_channel(sock, SH_NODE_ATTRIBUTE);
|
||||
if (input_link) {
|
||||
const bNode *attr_node = input_link->fromnode;
|
||||
const NodeShaderAttribute *storage = (NodeShaderAttribute *)attr_node->storage;
|
||||
|
||||
if (storage->type == SHD_ATTRIBUTE_GEOMETRY) {
|
||||
pxr::SdfValueTypeName output_type;
|
||||
pxr::UsdShadeShader usd_shader;
|
||||
if (STREQ(input_link->fromsock->identifier, "Color")) {
|
||||
output_type = pxr::SdfValueTypeNames->Float3;
|
||||
usd_shader = create_primvar_reader_shader(
|
||||
usd_export_context, usd_material, usdtokens::primvar_float3, attr_node);
|
||||
}
|
||||
else if (STREQ(input_link->fromsock->identifier, "Vector")) {
|
||||
output_type = pxr::SdfValueTypeNames->Float3;
|
||||
usd_shader = create_primvar_reader_shader(
|
||||
usd_export_context, usd_material, usdtokens::primvar_vector, attr_node);
|
||||
}
|
||||
else if (STREQ(input_link->fromsock->identifier, "Fac")) {
|
||||
output_type = pxr::SdfValueTypeNames->Float;
|
||||
usd_shader = create_primvar_reader_shader(
|
||||
usd_export_context, usd_material, usdtokens::primvar_float, attr_node);
|
||||
}
|
||||
|
||||
std::string attr_name = make_safe_name(storage->name,
|
||||
usd_export_context.export_params.allow_unicode);
|
||||
usd_shader.CreateInput(usdtokens::varname, pxr::SdfValueTypeNames->String).Set(attr_name);
|
||||
|
||||
pxr::UsdShadeConnectionSourceInfo source_info(usd_shader.ConnectableAPI(),
|
||||
usdtokens::result,
|
||||
pxr::UsdShadeAttributeType::Output,
|
||||
output_type);
|
||||
shader.CreateInput(input_spec.input_name, input_spec.input_type)
|
||||
.ConnectToSource(source_info);
|
||||
|
||||
processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (processed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* No upstream nodes, just set a default constant. */
|
||||
if (input_spec.set_default_value) {
|
||||
switch (sock->type) {
|
||||
case SOCK_FLOAT: {
|
||||
create_input<bNodeSocketValueFloat, float>(
|
||||
@@ -1085,6 +1146,20 @@ static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &u
|
||||
return shader;
|
||||
}
|
||||
|
||||
static pxr::UsdShadeShader create_primvar_reader_shader(
|
||||
const USDExporterContext &usd_export_context,
|
||||
const pxr::UsdShadeMaterial &material,
|
||||
const pxr::TfToken &primvar_type,
|
||||
const bNode *node)
|
||||
{
|
||||
pxr::SdfPath shader_path = material.GetPath().AppendChild(
|
||||
pxr::TfToken(make_safe_name(node->name, usd_export_context.export_params.allow_unicode)));
|
||||
pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(usd_export_context.stage, shader_path);
|
||||
|
||||
shader.CreateIdAttr(pxr::VtValue(primvar_type));
|
||||
return shader;
|
||||
}
|
||||
|
||||
static std::string get_tex_image_asset_filepath(const Image *ima)
|
||||
{
|
||||
char filepath[FILE_MAX];
|
||||
|
||||
BIN
tests/files/usd/usd_materials_attributes.blend
(Stored with Git LFS)
Normal file
BIN
tests/files/usd/usd_materials_attributes.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -461,6 +461,32 @@ class USDExportTest(AbstractUSDTest):
|
||||
input_displacement = shader_surface.GetInput('displacement')
|
||||
self.assertTrue(input_displacement.Get() is None)
|
||||
|
||||
def test_export_material_attributes(self):
|
||||
"""Validate correct export of Attribute information to UsdPrimvarReaders"""
|
||||
|
||||
# Use the common materials .blend file
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_attributes.blend"))
|
||||
export_path = self.tempdir / "usd_materials_attributes.usda"
|
||||
self.export_and_validate(filepath=str(export_path), export_materials=True)
|
||||
|
||||
stage = Usd.Stage.Open(str(export_path))
|
||||
|
||||
shader_attr = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/Material/Attribute"))
|
||||
shader_attr1 = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/Material/Attribute_001"))
|
||||
shader_attr2 = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/Material/Attribute_002"))
|
||||
|
||||
self.assertEqual(shader_attr.GetIdAttr().Get(), "UsdPrimvarReader_float3")
|
||||
self.assertEqual(shader_attr1.GetIdAttr().Get(), "UsdPrimvarReader_float")
|
||||
self.assertEqual(shader_attr2.GetIdAttr().Get(), "UsdPrimvarReader_vector")
|
||||
|
||||
self.assertEqual(shader_attr.GetInput("varname").Get(), "displayColor")
|
||||
self.assertEqual(shader_attr1.GetInput("varname").Get(), "f_float")
|
||||
self.assertEqual(shader_attr2.GetInput("varname").Get(), "f_vec")
|
||||
|
||||
self.assertEqual(shader_attr.GetOutput("result").GetTypeName().type.typeName, "GfVec3f")
|
||||
self.assertEqual(shader_attr1.GetOutput("result").GetTypeName().type.typeName, "float")
|
||||
self.assertEqual(shader_attr2.GetOutput("result").GetTypeName().type.typeName, "GfVec3f")
|
||||
|
||||
def test_export_metaballs(self):
|
||||
"""Validate correct export of Metaball objects. These are written out as Meshes."""
|
||||
|
||||
|
||||
@@ -471,6 +471,42 @@ class USDImportTest(AbstractUSDTest):
|
||||
mat = bpy.data.materials["mid_0_0_scale_0_3"]
|
||||
assert_displacement(mat, None, 0.0, 0.3)
|
||||
|
||||
def test_import_material_attributes(self):
|
||||
"""Validate correct import of Attribute information from UsdPrimvarReaders"""
|
||||
|
||||
# Use the existing materials test file to create the USD file
|
||||
# for import. It is validated as part of the bl_usd_export test.
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_attributes.blend"))
|
||||
testfile = str(self.tempdir / "usd_materials_attributes.usda")
|
||||
res = bpy.ops.wm.usd_export(filepath=str(testfile), export_materials=True)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to export to {testfile}")
|
||||
|
||||
# Reload the empty file and import back in
|
||||
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
||||
res = bpy.ops.wm.usd_import(filepath=testfile)
|
||||
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}")
|
||||
|
||||
# Most shader graph validation should occur through the Hydra render test suite. Here we
|
||||
# will only check some high-level criteria for each expected node graph.
|
||||
|
||||
def assert_attribute(mat, attribute_name, from_socket, to_socket):
|
||||
nodes = [n for n in mat.node_tree.nodes if n.type == 'ATTRIBUTE' and n.attribute_name == attribute_name]
|
||||
self.assertTrue(len(nodes) == 1)
|
||||
outputs = [o for o in nodes[0].outputs if o.identifier == from_socket]
|
||||
self.assertTrue(len(outputs) == 1)
|
||||
self.assertTrue(len(outputs[0].links) == 1)
|
||||
link = outputs[0].links[0]
|
||||
self.assertEqual(link.from_socket.identifier, from_socket)
|
||||
self.assertEqual(link.to_socket.identifier, to_socket)
|
||||
|
||||
mat = bpy.data.materials["Material"]
|
||||
self.assert_all_nodes_present(
|
||||
mat, ["Principled BSDF", "Attribute", "Attribute.001", "Attribute.002", "Material Output"])
|
||||
|
||||
assert_attribute(mat, "displayColor", "Color", "Base Color")
|
||||
assert_attribute(mat, "f_vec", "Vector", "Normal")
|
||||
assert_attribute(mat, "f_float", "Fac", "Roughness")
|
||||
|
||||
def test_import_shader_varname_with_connection(self):
|
||||
"""Test importing USD shader where uv primvar is a connection"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user