USD PointInstancer import support #113107

Merged
Michael Kowalski merged 29 commits from expenses/blender:usd-read-pointinstancer-2 into main 2024-02-01 15:37:54 +01:00
9 changed files with 617 additions and 48 deletions

View File

@ -113,6 +113,7 @@ set(SRC
intern/usd_reader_stage.cc
intern/usd_reader_volume.cc
intern/usd_reader_xform.cc
intern/usd_reader_pointinstancer.cc
intern/usd_skel_convert.cc
intern/usd_skel_root_utils.cc
@ -152,6 +153,7 @@ set(SRC
intern/usd_reader_stage.h
intern/usd_reader_volume.h
intern/usd_reader_xform.h
intern/usd_reader_pointinstancer.h
intern/usd_skel_convert.h
intern/usd_skel_root_utils.h
)

View File

@ -304,7 +304,7 @@ static void import_startjob(void *customdata, wmJobWorkerStatus *worker_status)
data->archive = archive;
archive->collect_readers(data->bmain);
archive->collect_readers();
if (data->params.import_materials && data->params.import_all_materials) {
archive->import_all_materials(data->bmain);
@ -412,7 +412,7 @@ static void import_endjob(void *customdata)
if (!reader) {
continue;
}
if (reader->prim().IsInPrototype()) {
if (reader->is_in_proto()) {
/* Skip prototype prims, as these are added to prototype collections. */
continue;
}
@ -429,6 +429,7 @@ static void import_endjob(void *customdata)
if (!reader) {
continue;
}
Object *ob = reader->object();
if (!ob) {
continue;

View File

@ -0,0 +1,310 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "usd_reader_pointinstancer.h"
#include "BKE_attribute.hh"
#include "BKE_lib_id.hh"
#include "BKE_modifier.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_tree_update.hh"
#include "BKE_object.hh"
#include "BKE_pointcloud.hh"
#include "BLI_math_quaternion.hh"
#include "BLI_string.h"
#include "DNA_collection_types.h"
#include <iostream>
#include <pxr/usd/usdGeom/pointInstancer.h>
namespace blender::io::usd {
/**
* Create a node to read a geometry attribute of the given name and type.
*/
static bNode *add_input_named_attrib_node(bNodeTree *ntree, const char *name, int8_t prop_type)
{
bNode *node = nodeAddStaticNode(nullptr, ntree, GEO_NODE_INPUT_NAMED_ATTRIBUTE);
auto *storage = reinterpret_cast<NodeGeometryInputNamedAttribute *>(node->storage);
storage->data_type = prop_type;
bNodeSocket *socket = nodeFindSocket(node, SOCK_IN, "Name");
bNodeSocketValueString *str_value = static_cast<bNodeSocketValueString *>(socket->default_value);
BLI_strncpy(str_value->value, name, MAX_NAME);
return node;
}
USDPointInstancerReader::USDPointInstancerReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
makowalski marked this conversation as resolved
Review

Might as well put this in the blender::io::usd namespace too. I'd suggest a few tweaks too, just for general cleanup.

static bNode *add_input_named_attrib_node(bNodeTree *ntree, const StringRef name, int8_t prop_type)
{
  bNode *node = nodeAddStaticNode(NULL, ntree, GEO_NODE_INPUT_NAMED_ATTRIBUTE);
  auto *storage = reinterpret_cast<NodeGeometryInputNamedAttribute *>(node->storage);
  storage->data_type = prop_type;

  bNodeSocket *socket = nodeFindSocket(node, SOCK_IN, "Name");
  bNodeSocketValueString *str_value = static_cast<bNodeSocketValueString *>(socket->default_value);
  BLI_strncpy(str_value->value, name.data(), name.size());
  return node;
}

I thought the asserts weren't really necessary since the pointers are de-referenced on the following line, which will generally give the same effect and show the same thing to the reader. Also, auto can be used here since the type name is already written in the cast. It's nice in that case when it doesn't wrap to two lines.

Might as well put this in the `blender::io::usd` namespace too. I'd suggest a few tweaks too, just for general cleanup. ```cpp static bNode *add_input_named_attrib_node(bNodeTree *ntree, const StringRef name, int8_t prop_type) { bNode *node = nodeAddStaticNode(NULL, ntree, GEO_NODE_INPUT_NAMED_ATTRIBUTE); auto *storage = reinterpret_cast<NodeGeometryInputNamedAttribute *>(node->storage); storage->data_type = prop_type; bNodeSocket *socket = nodeFindSocket(node, SOCK_IN, "Name"); bNodeSocketValueString *str_value = static_cast<bNodeSocketValueString *>(socket->default_value); BLI_strncpy(str_value->value, name.data(), name.size()); return node; } ``` I thought the asserts weren't really necessary since the pointers are de-referenced on the following line, which will generally give the same effect and show the same thing to the reader. Also, `auto` can be used here since the type name is already written in the cast. It's nice in that case when it doesn't wrap to two lines.
const ImportSettings &settings)
: USDXformReader(prim, import_params, settings)
{
}
bool USDPointInstancerReader::valid() const
{
return prim_.IsValid() && prim_.IsA<pxr::UsdGeomPointInstancer>();
}
void USDPointInstancerReader::create_object(Main *bmain, const double /* motionSampleTime */)
{
void *point_cloud = BKE_pointcloud_add(bmain, name_.c_str());
this->object_ = BKE_object_add_only_object(bmain, OB_POINTCLOUD, name_.c_str());
this->object_->data = point_cloud;
}
void USDPointInstancerReader::read_object_data(Main *bmain, const double motionSampleTime)
{
PointCloud *base_point_cloud = static_cast<PointCloud *>(object_->data);
pxr::UsdGeomPointInstancer point_instancer_prim(prim_);
if (!point_instancer_prim) {
return;
}
pxr::VtArray<pxr::GfVec3f> positions;
pxr::VtArray<pxr::GfVec3f> scales;
pxr::VtArray<pxr::GfQuath> orientations;
pxr::VtArray<int> proto_indices;
std::vector<double> time_samples;
point_instancer_prim.GetPositionsAttr().GetTimeSamples(&time_samples);
double sample_time = motionSampleTime;
if (!time_samples.empty()) {
sample_time = time_samples[0];
}

The orientations and scales are not required properties, so it should skip those if they don't exist.

The orientations and scales are not required properties, so it should skip those if they don't exist.

So I think the attributes need to be created even if the properties aren't set in the usd file as they are used in the geometry node setup.

So I think the attributes need to be created even if the properties aren't set in the usd file as they are used in the geometry node setup.
point_instancer_prim.GetPositionsAttr().Get(&positions, sample_time);
point_instancer_prim.GetScalesAttr().Get(&scales, sample_time);
point_instancer_prim.GetOrientationsAttr().Get(&orientations, sample_time);
point_instancer_prim.GetProtoIndicesAttr().Get(&proto_indices, sample_time);
std::vector<bool> mask = point_instancer_prim.ComputeMaskAtTime(sample_time);
PointCloud *point_cloud = BKE_pointcloud_new_nomain(positions.size());
MutableSpan<float3> positions_span = point_cloud->positions_for_write();
for (int i = 0; i < positions.size(); ++i) {
positions_span[i] = float3(positions[i][0], positions[i][1], positions[i][2]);
}
bke::MutableAttributeAccessor attributes = point_cloud->attributes_for_write();
bke::SpanAttributeWriter<float3> scales_attribute =
attributes.lookup_or_add_for_write_only_span<float3>("scale", bke::AttrDomain::Point);
/* Here and below, handle the case where instancing attributes are empty or
* not of the expected size. */
if (scales.size() < positions.size()) {
scales_attribute.span.fill(float3(1.0f));
}
for (const int i : IndexRange(std::min(scales.size(), positions.size()))) {
scales_attribute.span[i] = float3(scales[i][0], scales[i][1], scales[i][2]);
makowalski marked this conversation as resolved Outdated

auto -> MutableSpan<float3>. Same with auto elsewhere here. The type names are generally helpful to the reader, might as well include them.

`auto` -> `MutableSpan<float3>`. Same with `auto` elsewhere here. The type names are generally helpful to the reader, might as well include them.
}
scales_attribute.finish();
bke::SpanAttributeWriter<math::Quaternion> orientations_attribute =
attributes.lookup_or_add_for_write_only_span<math::Quaternion>("orientation",
bke::AttrDomain::Point);
makowalski marked this conversation as resolved Outdated

A temporary variable would make this look a bit nicer.

bke::MutableAttributeAccessor attributes = point_cloud->attributes_for_write();
A temporary variable would make this look a bit nicer. ``` bke::MutableAttributeAccessor attributes = point_cloud->attributes_for_write(); ```
if (orientations.size() < positions.size()) {
orientations_attribute.span.fill(math::Quaternion::identity());
makowalski marked this conversation as resolved Outdated

The check can be removed from inside the loop

for (const int i : IndexRange(std::min(scales.size(), positions.size())) {

The check can be removed from inside the loop `for (const int i : IndexRange(std::min(scales.size(), positions.size())) {`
}
for (const int i : IndexRange(std::min(orientations.size(), positions.size()))) {
orientations_attribute.span[i] = math::Quaternion(orientations[i].GetReal(),
orientations[i].GetImaginary()[0],
orientations[i].GetImaginary()[1],
orientations[i].GetImaginary()[2]);
}
orientations_attribute.finish();
bke::SpanAttributeWriter<int> proto_indices_attribute =
attributes.lookup_or_add_for_write_only_span<int>("proto_index", bke::AttrDomain::Point);
if (proto_indices.size() < positions.size()) {
proto_indices_attribute.span.fill(0);
}
for (const int i : IndexRange(std::min(proto_indices.size(), positions.size()))) {
proto_indices_attribute.span[i] = proto_indices[i];
}
proto_indices_attribute.finish();
makowalski marked this conversation as resolved Outdated

math::Quaternion::identity() I think

`math::Quaternion::identity()` I think
bke::SpanAttributeWriter<bool> mask_attribute =
attributes.lookup_or_add_for_write_only_span<bool>("mask", bke::AttrDomain::Point);
if (mask.size() < positions.size()) {
mask_attribute.span.fill(true);
}
for (const int i : IndexRange(std::min(mask.size(), positions.size()))) {
mask_attribute.span[i] = mask[i];
}
mask_attribute.finish();
makowalski marked this conversation as resolved Outdated

FYI, I uncommented the node creation code for testing, and it looks great!

However this line was causing a crash. For one thing, this is assigning a pointer to the local variable separate_children, which goes out of scope at the end of the function.

More importantly, I believe you need to perform an additional cast of bNodeSocket::default_value so you can assign the bool to bNodeSocketValueBoolean::value:

bNodeSocket *separate_children_sock = nodeFindSocket(
    collection_info_node, SOCK_IN, "Separate Children");
((bNodeSocketValueBoolean *)separate_children_sock->default_value)->value = true;

I realize this code was commented out, so this might not have been intended for review, but I just wanted to mention this in case you weren't aware of the issue.

FYI, I uncommented the node creation code for testing, and it looks great! However this line was causing a crash. For one thing, this is assigning a pointer to the local variable `separate_children`, which goes out of scope at the end of the function. More importantly, I believe you need to perform an additional cast of `bNodeSocket::default_value` so you can assign the bool to `bNodeSocketValueBoolean::value`: ``` bNodeSocket *separate_children_sock = nodeFindSocket( collection_info_node, SOCK_IN, "Separate Children"); ((bNodeSocketValueBoolean *)separate_children_sock->default_value)->value = true; ``` I realize this code was commented out, so this might not have been intended for review, but I just wanted to mention this in case you weren't aware of the issue.
BKE_pointcloud_nomain_to_pointcloud(point_cloud, base_point_cloud);
ModifierData *md = BKE_modifier_new(eModifierType_Nodes);
BLI_addtail(&object_->modifiers, md);
NodesModifierData &nmd = *reinterpret_cast<NodesModifierData *>(md);
nmd.node_group = ntreeAddTree(bmain, "Instances", "GeometryNodeTree");
makowalski marked this conversation as resolved
Review

NULL -> nullptr

`NULL` -> `nullptr`
bNodeTree *ntree = nmd.node_group;
makowalski marked this conversation as resolved
Review

This can use socket->default_value_typed<bNodeSocketValueBoolean>()->value = true; to avoid the C style cast

This can use `socket->default_value_typed<bNodeSocketValueBoolean>()->value = true;` to avoid the C style cast
ntree->tree_interface.add_socket("Geometry",
"",
"NodeSocketGeometry",
NODE_INTERFACE_SOCKET_INPUT | NODE_INTERFACE_SOCKET_OUTPUT,
nullptr);
bNode *group_input = nodeAddStaticNode(nullptr, ntree, NODE_GROUP_INPUT);
group_input->locx = -400.0f;
bNode *group_output = nodeAddStaticNode(nullptr, ntree, NODE_GROUP_OUTPUT);
group_output->locx = 500.0f;
group_output->flag |= NODE_DO_OUTPUT;
bNode *instance_on_points_node = nodeAddStaticNode(nullptr, ntree, GEO_NODE_INSTANCE_ON_POINTS);
instance_on_points_node->locx = 300.0f;
bNodeSocket *socket = nodeFindSocket(instance_on_points_node, SOCK_IN, "Pick Instance");
socket->default_value_typed<bNodeSocketValueBoolean>()->value = true;
bNode *mask_attrib_node = add_input_named_attrib_node(ntree, "mask", CD_PROP_BOOL);
mask_attrib_node->locx = 100.0f;
mask_attrib_node->locy = -100.0f;
bNode *collection_info_node = nodeAddStaticNode(nullptr, ntree, GEO_NODE_COLLECTION_INFO);
collection_info_node->locx = 100.0f;
collection_info_node->locy = -300.0f;
socket = nodeFindSocket(collection_info_node, SOCK_IN, "Separate Children");
socket->default_value_typed<bNodeSocketValueBoolean>()->value = true;
bNode *indices_attrib_node = add_input_named_attrib_node(ntree, "proto_index", CD_PROP_INT32);
indices_attrib_node->locx = 100.0f;
makowalski marked this conversation as resolved
Review

The rotation to Euler node should be unnecessary here.

The rotation to Euler node should be unnecessary here.
indices_attrib_node->locy = -500.0f;
bNode *rotation_attrib_node = add_input_named_attrib_node(
ntree, "orientation", CD_PROP_QUATERNION);
rotation_attrib_node->locx = 100.0f;
rotation_attrib_node->locy = -700.0f;
bNode *scale_attrib_node = add_input_named_attrib_node(ntree, "scale", CD_PROP_FLOAT3);
scale_attrib_node->locx = 100.0f;
scale_attrib_node->locy = -900.0f;
nodeAddLink(ntree,
group_input,
static_cast<bNodeSocket *>(group_input->outputs.first),
instance_on_points_node,
nodeFindSocket(instance_on_points_node, SOCK_IN, "Points"));
nodeAddLink(ntree,
mask_attrib_node,
nodeFindSocket(mask_attrib_node, SOCK_OUT, "Attribute"),
instance_on_points_node,
nodeFindSocket(instance_on_points_node, SOCK_IN, "Selection"));
nodeAddLink(ntree,
indices_attrib_node,
nodeFindSocket(indices_attrib_node, SOCK_OUT, "Attribute"),
instance_on_points_node,
nodeFindSocket(instance_on_points_node, SOCK_IN, "Instance Index"));
nodeAddLink(ntree,
scale_attrib_node,
nodeFindSocket(scale_attrib_node, SOCK_OUT, "Attribute"),
instance_on_points_node,
nodeFindSocket(instance_on_points_node, SOCK_IN, "Scale"));
nodeAddLink(ntree,
rotation_attrib_node,
nodeFindSocket(rotation_attrib_node, SOCK_OUT, "Attribute"),
instance_on_points_node,
nodeFindSocket(instance_on_points_node, SOCK_IN, "Rotation"));
nodeAddLink(ntree,
collection_info_node,
nodeFindSocket(collection_info_node, SOCK_OUT, "Instances"),
instance_on_points_node,
nodeFindSocket(instance_on_points_node, SOCK_IN, "Instance"));
nodeAddLink(ntree,
instance_on_points_node,
nodeFindSocket(instance_on_points_node, SOCK_OUT, "Instances"),
group_output,
static_cast<bNodeSocket *>(group_output->inputs.first));
BKE_ntree_update_main_tree(bmain, ntree, nullptr);
makowalski marked this conversation as resolved
Review

A call to BKE_object_modifier_set_active(object_, md); can be done after the ntree_update to ensure the modifier is selected/active. It's fine without it but it means the user has to select the modifier themselves before they can view it in the node editor.

A call to `BKE_object_modifier_set_active(object_, md);` can be done after the ntree_update to ensure the modifier is selected/active. It's fine without it but it means the user has to select the modifier themselves before they can view it in the node editor.
BKE_object_modifier_set_active(object_, md);
USDXformReader::read_object_data(bmain, motionSampleTime);
}
pxr::SdfPathVector USDPointInstancerReader::proto_paths() const
{
pxr::SdfPathVector paths;
pxr::UsdGeomPointInstancer point_instancer_prim(prim_);
if (!point_instancer_prim) {
return paths;
}
point_instancer_prim.GetPrototypesRel().GetTargets(&paths);
return paths;
}
void USDPointInstancerReader::set_collection(Main *bmain, Collection &coll)
{
makowalski marked this conversation as resolved Outdated

Collection *coll can become a reference, then the assert can be removed. Also not sure here if !object_ is an overly defensive check

`Collection *coll` can become a reference, then the assert can be removed. Also not sure here if `!object_` is an overly defensive check
/* create_object() should have been called already. */
BLI_assert(object_);
ModifierData *md = BKE_modifiers_findby_type(this->object_, eModifierType_Nodes);
if (!md) {
BLI_assert_unreachable();
makowalski marked this conversation as resolved Outdated

Typical name for mod_data is just md

Typical name for `mod_data` is just `md`
return;
}
NodesModifierData *nmd = reinterpret_cast<NodesModifierData *>(md);
bNodeTree *ntree = nmd->node_group;
if (!ntree) {
makowalski marked this conversation as resolved Outdated

NodesModifierData will not be null if ModifierData isn't null, and that was already checked.

`NodesModifierData` will not be null if `ModifierData` isn't null, and that was already checked.
BLI_assert_unreachable();
return;
}
bNode *collection_node = nodeFindNodebyName(ntree, "Collection Info");
if (!collection_node) {
BLI_assert_unreachable();
return;
}
bNodeSocket *sock = nodeFindSocket(collection_node, SOCK_IN, "Collection");
if (!sock) {
BLI_assert_unreachable();
return;
}
bNodeSocketValueCollection *socket_data = static_cast<bNodeSocketValueCollection *>(
sock->default_value);
if (socket_data->value != &coll) {
socket_data->value = &coll;
BKE_ntree_update_main_tree(bmain, ntree, nullptr);
}
makowalski marked this conversation as resolved Outdated

static_cast here

`static_cast` here
}
} // namespace blender::io::usd

View File

@ -0,0 +1,42 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
makowalski marked this conversation as resolved Outdated

Looks like this can use the new copyright and license format

Looks like this can use the new copyright and license format
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "usd_reader_xform.h"
struct Collection;
namespace blender::io::usd {
/* Wraps the UsdGeomPointInstancer schema. Creates a Blender point cloud object. */
class USDPointInstancerReader : public USDXformReader {
public:
USDPointInstancerReader(const pxr::UsdPrim &prim,
const USDImportParams &import_params,
const ImportSettings &settings);
bool valid() const override;
void create_object(Main *bmain, double motionSampleTime) override;
void read_object_data(Main *bmain, double motionSampleTime) override;
pxr::SdfPathVector proto_paths() const;
/**
* Set the given collection on the Collection Info
* node referenced by the geometry nodes modifier
* on the object created by the reader. This assumes
* create_object() and read_object_data() have already
* been called.
*
* \param bmain: Pointer to Main
* \param coll: The collection to set
*/
void set_collection(Main *bmain, Collection &coll);
};
} // namespace blender::io::usd

View File

@ -21,7 +21,8 @@ USDPrimReader::USDPrimReader(const pxr::UsdPrim &prim,
import_params_(import_params),
parent_reader_(nullptr),
settings_(&settings),
refcount_(0)
refcount_(0),
is_in_instancer_proto_(false)
{
}
@ -63,4 +64,9 @@ void USDPrimReader::decref()
BLI_assert(refcount_ >= 0);
}
bool USDPrimReader::is_in_proto() const
{
return prim_ && (prim_.IsInPrototype() || is_in_instancer_proto_);
}
} // namespace blender::io::usd

View File

@ -94,6 +94,7 @@ class USDPrimReader {
USDPrimReader *parent_reader_;
const ImportSettings *settings_;
int refcount_;
bool is_in_instancer_proto_;
public:
USDPrimReader(const pxr::UsdPrim &prim,
@ -147,6 +148,18 @@ class USDPrimReader {
{
return prim_path_;
}
void set_is_in_instancer_proto(bool flag)
{
is_in_instancer_proto_ = flag;
}
bool is_in_instancer_proto() const
{
return is_in_instancer_proto_;
}
bool is_in_proto() const;
};
} // namespace blender::io::usd

View File

@ -10,6 +10,7 @@
#include "usd_reader_material.h"
#include "usd_reader_mesh.h"
#include "usd_reader_nurbs.h"
#include "usd_reader_pointinstancer.h"
#include "usd_reader_prim.h"
#include "usd_reader_shape.h"
#include "usd_reader_skeleton.h"
@ -26,6 +27,7 @@
#include <pxr/usd/usdGeom/cylinder.h>
#include <pxr/usd/usdGeom/mesh.h>
#include <pxr/usd/usdGeom/nurbsCurves.h>
#include <pxr/usd/usdGeom/pointInstancer.h>
#include <pxr/usd/usdGeom/scope.h>
#include <pxr/usd/usdGeom/sphere.h>
#include <pxr/usd/usdGeom/xform.h>
@ -39,6 +41,7 @@
#endif
#include "BLI_map.hh"
#include "BLI_math_base.h"
#include "BLI_sort.hh"
#include "BLI_string.h"
@ -52,12 +55,25 @@
#include "DNA_collection_types.h"
#include "DNA_material_types.h"
#include "WM_api.hh"
#include <iomanip>
makowalski marked this conversation as resolved Outdated

Not clear that this include is used (sorry if I'm just missing it)

Not clear that this include is used (sorry if I'm just missing it)
static CLG_LogRef LOG = {"io.usd"};
namespace blender::io::usd {
static void decref(USDPrimReader *reader)
{
if (!reader) {
return;
}
reader->decref();
if (reader->refcount() == 0) {
delete reader;
}
}
/**
* Create a collection with the given parent and name.
*/
@ -95,6 +111,29 @@ static void set_instance_collection(
}
}
static void collect_point_instancer_proto_paths(const pxr::UsdPrim &prim, UsdPathSet &r_paths)
{
/* Note that we use custom filter flags to allow traversing undefined prims,
* because prototype prims may be defined as overs which are skipped by the
* default predicate. */
pxr::Usd_PrimFlagsConjunction filter_flags = pxr::UsdPrimIsActive && pxr::UsdPrimIsLoaded &&
!pxr::UsdPrimIsAbstract;
pxr::UsdPrimSiblingRange children = prim.GetFilteredChildren(filter_flags);
for (const auto &child_prim : children) {
if (pxr::UsdGeomPointInstancer instancer = pxr::UsdGeomPointInstancer(child_prim)) {
pxr::SdfPathVector paths;
instancer.GetPrototypesRel().GetTargets(&paths);
for (const pxr::SdfPath &path : paths) {
r_paths.add(path);
}
}
collect_point_instancer_proto_paths(child_prim, r_paths);
}
}
USDStageReader::USDStageReader(pxr::UsdStageRefPtr stage,
const USDImportParams &params,
const ImportSettings &settings)
@ -104,7 +143,6 @@ USDStageReader::USDStageReader(pxr::UsdStageRefPtr stage,
USDStageReader::~USDStageReader()
{
clear_proto_readers();
clear_readers();
}
@ -128,6 +166,9 @@ USDPrimReader *USDStageReader::create_reader_if_allowed(const pxr::UsdPrim &prim
if (params_.import_shapes && is_primitive_prim(prim)) {
return new USDShapeReader(prim, params_, settings_);
}
if (prim.IsA<pxr::UsdGeomPointInstancer>()) {
return new USDPointInstancerReader(prim, params_, settings_);
}
if (params_.import_cameras && prim.IsA<pxr::UsdGeomCamera>()) {
return new USDCameraReader(prim, params_, settings_);
}
@ -300,8 +341,9 @@ static bool merge_with_parent(USDPrimReader *reader)
return true;
}
USDPrimReader *USDStageReader::collect_readers(Main *bmain,
const pxr::UsdPrim &prim,
USDPrimReader *USDStageReader::collect_readers(const pxr::UsdPrim &prim,
const UsdPathSet &pruned_prims,
const bool defined_prims_only,
blender::Vector<USDPrimReader *> &r_readers)
{
if (prim.IsA<pxr::UsdGeomImageable>()) {
@ -316,18 +358,29 @@ USDPrimReader *USDStageReader::collect_readers(Main *bmain,
}
}
pxr::Usd_PrimFlagsPredicate filter_predicate = pxr::UsdPrimDefaultPredicate;
pxr::Usd_PrimFlagsConjunction filter_flags = pxr::UsdPrimIsActive && pxr::UsdPrimIsLoaded &&
!pxr::UsdPrimIsAbstract;
if (defined_prims_only) {
filter_flags &= pxr::UsdPrimIsDefined;
}
pxr::Usd_PrimFlagsPredicate filter_predicate(filter_flags);
if (!params_.support_scene_instancing) {
filter_predicate = pxr::UsdTraverseInstanceProxies(filter_predicate);
}
pxr::UsdPrimSiblingRange children = prim.GetFilteredChildren(filter_predicate);
blender::Vector<USDPrimReader *> child_readers;
for (const auto &childPrim : children) {
if (USDPrimReader *child_reader = collect_readers(bmain, childPrim, r_readers)) {
pxr::UsdPrimSiblingRange children = prim.GetFilteredChildren(filter_predicate);
for (const auto &child_prim : children) {
if (pruned_prims.contains(child_prim.GetPath())) {
continue;
}
if (USDPrimReader *child_reader = collect_readers(
child_prim, pruned_prims, defined_prims_only, r_readers))
{
child_readers.append(child_reader);
}
}
@ -380,20 +433,25 @@ USDPrimReader *USDStageReader::collect_readers(Main *bmain,
return reader;
}
void USDStageReader::collect_readers(Main *bmain)
void USDStageReader::collect_readers()
{
if (!valid()) {
return;
}
clear_readers();
clear_proto_readers();
/* Identify paths to point instancer prototypes, as these will be converted
* in a separate pass over the stage. */
UsdPathSet instancer_proto_paths = collect_point_instancer_proto_paths();
/* Iterate through the stage. */
pxr::UsdPrim root = stage_->GetPseudoRoot();
stage_->SetInterpolationType(pxr::UsdInterpolationType::UsdInterpolationTypeHeld);
collect_readers(bmain, root, readers_);
/* Create readers, skipping over prototype prims in this pass. */
collect_readers(root, instancer_proto_paths, true, readers_);
if (params_.support_scene_instancing) {
/* Collect the scene-graph instance prototypes. */
@ -401,7 +459,7 @@ void USDStageReader::collect_readers(Main *bmain)
for (const pxr::UsdPrim &proto_prim : protos) {
blender::Vector<USDPrimReader *> proto_readers;
collect_readers(bmain, proto_prim, proto_readers);
collect_readers(proto_prim, instancer_proto_paths, true, proto_readers);
proto_readers_.add(proto_prim.GetPath(), proto_readers);
for (USDPrimReader *reader : proto_readers) {
@ -410,6 +468,10 @@ void USDStageReader::collect_readers(Main *bmain)
}
}
}
if (!instancer_proto_paths.is_empty()) {
create_point_instancer_proto_readers(instancer_proto_paths);
}
}
void USDStageReader::process_armature_modifiers() const
@ -517,39 +579,23 @@ void USDStageReader::fake_users_for_unused_materials()
void USDStageReader::clear_readers()
{
for (USDPrimReader *reader : readers_) {
if (!reader) {
continue;
}
reader->decref();
if (reader->refcount() == 0) {
delete reader;
}
decref(reader);
}
readers_.clear();
}
void USDStageReader::clear_proto_readers()
{
for (const auto item : proto_readers_.items()) {
for (USDPrimReader *reader : item.value) {
if (!reader) {
continue;
}
reader->decref();
if (reader->refcount() == 0) {
delete reader;
}
decref(reader);
}
}
proto_readers_.clear();
for (const auto item : instancer_proto_readers_.items()) {
for (USDPrimReader *reader : item.value) {
decref(reader);
}
}
instancer_proto_readers_.clear();
}
void USDStageReader::sort_readers()
@ -564,7 +610,7 @@ void USDStageReader::sort_readers()
void USDStageReader::create_proto_collections(Main *bmain, Collection *parent_collection)
{
if (proto_readers_.is_empty()) {
if (proto_readers_.is_empty() && instancer_proto_readers_.is_empty()) {
return;
}
@ -615,6 +661,112 @@ void USDStageReader::create_proto_collections(Main *bmain, Collection *parent_co
BKE_collection_object_add(bmain, collection, ob);
}
}
/* Create collections for the point instancer prototypes. */
/* For every point instancer reader, create a "prototypes" collection and set it
* on the Collection Info node referenced by the geometry nodes modifier created by
* the reader. We also create collections containing prototype geometry as children
* of the "prototypes" collection. These child collections will be indexed for
* instancing by the Instance on Points geometry node.
*
* Note that the prototype collections will be ordered alphabetically by the Collection
* Info node. We must therefore take care to generate collection names that will maintain
* the original prototype order, so that the prototype indices will remain valid. We use
* the naming convention proto_<index>, where the index suffix may be zero padded (e.g.,
* "proto_00", "proto_01", "proto_02", etc.).
*/
for (USDPrimReader *reader : readers_) {
USDPointInstancerReader *instancer_reader = dynamic_cast<USDPointInstancerReader *>(reader);
if (!instancer_reader) {
continue;
}
const pxr::SdfPath &instancer_path = reader->prim().GetPath();
Collection *instancer_protos_coll = create_collection(
bmain, all_protos_collection, instancer_path.GetName().c_str());
/* Determine the max number of digits we will need for the possibly zero-padded
* string representing the prototype index. */
int max_index_digits = integer_digits_i(instancer_reader->proto_paths().size());
int proto_index = 0;
for (const pxr::SdfPath &proto_path : instancer_reader->proto_paths()) {
BLI_assert(max_index_digits > 0);
/* Format the collection name to follow the proto_<index> pattern. */
std::ostringstream ss;
ss << std::setw(max_index_digits) << std::setfill('0') << proto_index;
std::string coll_name = "proto_" + ss.str();
Review

After this is checked in I'll probably move this to using fmtlib to remove the dependency on iostreams. It requires a bit of cmake changes so I wouldn't bother to do it in this checkin.

After this is checked in I'll probably move this to using `fmtlib` to remove the dependency on iostreams. It requires a bit of cmake changes so I wouldn't bother to do it in this checkin.
/* Create the collection and populate it with the prototype objects. */
Collection *proto_coll = create_collection(bmain, instancer_protos_coll, coll_name.c_str());
blender::Vector<USDPrimReader *> proto_readers = instancer_proto_readers_.lookup_default(
proto_path, {});
for (USDPrimReader *reader : proto_readers) {
makowalski marked this conversation as resolved
Review

blender::Vector -> Vector

`blender::Vector` -> `Vector`
Object *ob = reader->object();
if (!ob) {
continue;
}
BKE_collection_object_add(bmain, proto_coll, ob);
}
++proto_index;
}
instancer_reader->set_collection(bmain, *instancer_protos_coll);
}
}
makowalski marked this conversation as resolved Outdated

The use of an optional here isn't really buying much since it basically means the same thing as an empty Set. It's also causing additional copy ctor calls for the Set since optional can only hold values and not references. I'd say just return the UsdPathSet.

The use of an optional here isn't really buying much since it basically means the same thing as an empty Set. It's also causing additional copy ctor calls for the Set since optional can only hold values and not references. I'd say just return the UsdPathSet.
void USDStageReader::create_point_instancer_proto_readers(const UsdPathSet &proto_paths)
{
if (proto_paths.is_empty()) {
return;
}
for (const pxr::SdfPath &path : proto_paths) {
pxr::UsdPrim proto_prim = stage_->GetPrimAtPath(path);
if (!proto_prim) {
continue;
}
Vector<USDPrimReader *> proto_readers;
/* Note that point instancer prototypes may be defined as overs, so
* we must call collect readers with argument defined_prims_only = false. */
collect_readers(proto_prim, proto_paths, false /* include undefined prims */, proto_readers);
instancer_proto_readers_.add(path, proto_readers);
for (USDPrimReader *reader : proto_readers) {
reader->set_is_in_instancer_proto(true);
readers_.append(reader);
reader->incref();
}
}
}
UsdPathSet USDStageReader::collect_point_instancer_proto_paths() const
{
UsdPathSet result;
if (!stage_) {
return result;
}
io::usd::collect_point_instancer_proto_paths(stage_->GetPseudoRoot(), result);
std::vector<pxr::UsdPrim> protos = stage_->GetPrototypes();
for (const pxr::UsdPrim &proto_prim : protos) {
io::usd::collect_point_instancer_proto_paths(proto_prim, result);
}
return result;
}
} // Namespace blender::io::usd

View File

@ -7,6 +7,8 @@ struct Main;
#include "WM_types.hh"
#include "BLI_set.hh"
#include "usd.h"
#include "usd_hash_types.h"
#include "usd_reader_prim.h"
@ -20,12 +22,16 @@ struct ImportSettings;
namespace blender::io::usd {
class USDPointInstancerReader;
/**
* Map a USD prototype prim path to the list of readers that convert
* the prototype data.
*/
using ProtoReaderMap = blender::Map<pxr::SdfPath, blender::Vector<USDPrimReader *>>;
using UsdPathSet = blender::Set<pxr::SdfPath>;
class USDStageReader {
protected:
@ -42,6 +48,9 @@ class USDStageReader {
/* Readers for scene-graph instance prototypes. */
ProtoReaderMap proto_readers_;
/* Readers for point instancer prototypes. */
ProtoReaderMap instancer_proto_readers_;
public:
USDStageReader(pxr::UsdStageRefPtr stage,
const USDImportParams &params,
@ -53,7 +62,7 @@ class USDStageReader {
USDPrimReader *create_reader(const pxr::UsdPrim &prim);
void collect_readers(struct Main *bmain);
void collect_readers();
/**
* Complete setting up the armature modifiers that
@ -95,10 +104,9 @@ class USDStageReader {
return params_.worker_status ? params_.worker_status->reports : nullptr;
}
/** Clear all cached reader collections. */
void clear_readers();
void clear_proto_readers();
const blender::Vector<USDPrimReader *> &readers() const
{
return readers_;
@ -112,8 +120,26 @@ class USDStageReader {
void create_proto_collections(Main *bmain, Collection *parent_collection);
private:
USDPrimReader *collect_readers(Main *bmain,
const pxr::UsdPrim &prim,
/**
* Create readers for the subtree rooted at the given prim and append the
* new readers in r_readers.
*
* \param prim: Root of the subtree to convert to readers
* \param pruned_prims: Set of paths to prune when iterating over the
* stage during conversion. I.e., these prims
* and their descendents will not be converted to
* readers.
* \param defined_prims_only: If true, only defined prims will be converted,
* skipping abstract and over prims. This should
* be set to false when converting point instancer
* prototype prims, which can be declared as overs.
* \param r_readers: Readers created for the prims in the converted subtree.
* \return: A pointer to the reader created for the given prim or null if
* the prim cannot be converted.
*/
USDPrimReader *collect_readers(const pxr::UsdPrim &prim,
const UsdPathSet &pruned_prims,
bool defined_prims_only,
makowalski marked this conversation as resolved Outdated

Is there really a difference between null and an empty set to have this be a pointer? Or are you worried about performance of checking the empty set perhaps?

Is there really a difference between null and an empty set to have this be a pointer? Or are you worried about performance of checking the empty set perhaps?

My thought was that there might be use cases in the future where we call collect_readers() without needing the option to prune anything, so it might be nice to avoid constructing an empty set just to call the function. But I don't feel strongly about this at all. I can change this to a reference, if you feel it's cleaner to do so. Let me know.

My thought was that there might be use cases in the future where we call collect_readers() without needing the option to prune anything, so it might be nice to avoid constructing an empty set just to call the function. But I don't feel strongly about this at all. I can change this to a reference, if you feel it's cleaner to do so. Let me know.

I'd personally only change the API as-needed when necessary. Though I think creating an empty set in those cases would be fine really.

I'd personally only change the API as-needed when necessary. Though I think creating an empty set in those cases would be fine really.

Thanks for the feedback! I updated the code to pass the set as a reference.

Thanks for the feedback! I updated the code to pass the set as a reference.
blender::Vector<USDPrimReader *> &r_readers);
/**
@ -139,6 +165,23 @@ class USDStageReader {
* procedural shape, such as UsdGeomCube.
*/
bool is_primitive_prim(const pxr::UsdPrim &prim) const;
/**
* Iterate over the stage and return the paths of all prototype
* primitives references by point instancers.
*
* \return: The prototype paths, or an empty path set if the scene
* does not contain any point instancers.
*/
UsdPathSet collect_point_instancer_proto_paths() const;
/**
* Populate the instancer_proto_readers_ map for the prototype prims
* in the given set. For each prototype path, this function will
* create readers for the prims in the subtree rooted at the prototype
* prim.
*/
void create_point_instancer_proto_readers(const UsdPathSet &proto_paths);
};
}; // namespace blender::io::usd

View File

@ -118,7 +118,7 @@ bool USDXformReader::is_root_xform_prim() const
return false;
}
if (prim_.IsInPrototype()) {
if (is_in_proto()) {
/* We don't consider prototypes to be root prims,
* because we never want to apply global scaling
* or rotations to the prototypes themselves. */