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:
Jesse Yurkovich
2025-05-19 19:47:22 +02:00
committed by Jesse Yurkovich
parent dc213cd79e
commit 46ec277713
6 changed files with 233 additions and 9 deletions

View File

@@ -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");

View File

@@ -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. */

View File

@@ -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

Binary file not shown.

View File

@@ -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."""

View File

@@ -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"""