USD: Add support for Point Instancing during Export
Adds a Point Instancing exporter based on the existing USDPointInstancerReader. Covers both round-trip and Blender-native workflows. Exports 'Instance on Points' setups as USDGeomPointInstancer, supporting objects, collections, and nested prototypes. A warning is shown during export if invalid prototype references are detected. These would occur if an instancer attempts to instance itself. This feature is currently gated behind an off-by-default export option (`use_instancing`) as there are still a few cases which can yield incorrect results. Further details in the PR. Ref: #139758 Authored by Apple: Zili (Liz) Zhou Pull Request: https://projects.blender.org/blender/blender/pulls/139760
This commit is contained in:
committed by
Jesse Yurkovich
parent
fc0b659066
commit
07342407d3
@@ -85,6 +85,18 @@ struct HierarchyContext {
|
||||
/* When true this is duplisource object. This flag is used to identify instance prototypes. */
|
||||
bool is_duplisource;
|
||||
|
||||
/* This flag tells whether an object is a valid point instance of other objects.
|
||||
* If true, it means the object has a valid reference path and its value can be included
|
||||
* in the instances data of UsdGeomPointInstancer. */
|
||||
bool is_point_instance;
|
||||
|
||||
/* This flag tells if an object is a valid prototype of a point instancer. */
|
||||
bool is_point_proto;
|
||||
|
||||
/* True if this context is a descendant of any context with is_point_instance set to true.
|
||||
* This helps skip redundant instancing data during export. */
|
||||
bool has_point_instance_ancestor;
|
||||
|
||||
/*********** Determined during writer creation: ***************/
|
||||
float parent_matrix_inv_world[4][4]; /* Inverse of the parent's world matrix. */
|
||||
std::string export_path; /* Hierarchical path, such as "/grandparent/parent/object_name". */
|
||||
@@ -112,6 +124,9 @@ struct HierarchyContext {
|
||||
void mark_as_not_instanced();
|
||||
bool is_prototype() const;
|
||||
|
||||
/* For handling point instancing (Instance on Points geo node). */
|
||||
bool is_point_instancer() const;
|
||||
|
||||
bool is_object_visible(enum eEvaluationMode evaluation_mode) const;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_duplilist.hh"
|
||||
#include "BKE_geometry_set_instances.hh"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_modifier.hh"
|
||||
#include "BKE_node_legacy_types.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_particle.h"
|
||||
|
||||
@@ -23,6 +26,7 @@
|
||||
#include "DNA_ID.h"
|
||||
#include "DNA_layer_types.h"
|
||||
#include "DNA_modifier_types.h"
|
||||
#include "DNA_node_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
#include "DNA_particle_types.h"
|
||||
#include "DNA_rigidbody_types.h"
|
||||
@@ -155,6 +159,21 @@ bool AbstractHierarchyWriter::check_has_deforming_physics(const HierarchyContext
|
||||
return rbo != nullptr && rbo->type == RBO_TYPE_ACTIVE && (rbo->flag & RBO_FLAG_USE_DEFORM) != 0;
|
||||
}
|
||||
|
||||
bool HierarchyContext::is_point_instancer() const
|
||||
{
|
||||
if (!object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Collection instancers are handled elsewhere as part of Scene instancing. */
|
||||
if (object->type == OB_EMPTY && object->instance_collection != nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bke::GeometrySet geometry_set = bke::object_get_evaluated_geometry_set(*object);
|
||||
return geometry_set.has_instances();
|
||||
}
|
||||
|
||||
AbstractHierarchyIterator::AbstractHierarchyIterator(Main *bmain, Depsgraph *depsgraph)
|
||||
: bmain_(bmain), depsgraph_(depsgraph), export_subset_({true, true})
|
||||
{
|
||||
@@ -629,6 +648,12 @@ void AbstractHierarchyIterator::make_writers(const HierarchyContext *parent_cont
|
||||
}
|
||||
|
||||
for (HierarchyContext *context : *children) {
|
||||
if (parent_context) {
|
||||
if (parent_context->is_point_instance || parent_context->has_point_instance_ancestor) {
|
||||
context->has_point_instance_ancestor = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Update the context so that it is correct for this parent-child relation. */
|
||||
copy_m4_m4(context->parent_matrix_inv_world, parent_matrix_inv_world);
|
||||
if (parent_context != nullptr) {
|
||||
@@ -645,15 +670,18 @@ void AbstractHierarchyIterator::make_writers(const HierarchyContext *parent_cont
|
||||
return;
|
||||
}
|
||||
|
||||
BLI_assert(DEG_is_evaluated(context->object));
|
||||
if (transform_writer.is_newly_created() || export_subset_.transforms) {
|
||||
const bool need_writers = context->is_point_proto || (!context->is_point_instance &&
|
||||
!context->has_point_instance_ancestor);
|
||||
|
||||
BLI_assert(DEG_is_evaluated_id(&context->object->id));
|
||||
if ((transform_writer.is_newly_created() || export_subset_.transforms) && need_writers) {
|
||||
/* XXX This can lead to too many XForms being written. For example, a camera writer can
|
||||
* refuse to write an orthographic camera. By the time that this is known, the XForm has
|
||||
* already been written. */
|
||||
transform_writer->write(*context);
|
||||
}
|
||||
|
||||
if (!context->weak_export && include_data_writers(context)) {
|
||||
if (!context->weak_export && include_data_writers(context) && need_writers) {
|
||||
make_writers_particle_systems(context);
|
||||
make_writer_object_data(context);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ set(SRC
|
||||
intern/usd_writer_material.cc
|
||||
intern/usd_writer_mesh.cc
|
||||
intern/usd_writer_metaball.cc
|
||||
intern/usd_writer_pointinstancer.cc
|
||||
intern/usd_writer_points.cc
|
||||
intern/usd_writer_text.cc
|
||||
intern/usd_writer_transform.cc
|
||||
@@ -134,6 +135,7 @@ set(SRC
|
||||
intern/usd_writer_material.hh
|
||||
intern/usd_writer_mesh.hh
|
||||
intern/usd_writer_metaball.hh
|
||||
intern/usd_writer_pointinstancer.hh
|
||||
intern/usd_writer_points.hh
|
||||
intern/usd_writer_text.hh
|
||||
intern/usd_writer_transform.hh
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
#include <pxr/base/tf/token.h>
|
||||
#include <pxr/pxr.h>
|
||||
#include <pxr/usd/sdf/assetPath.h>
|
||||
#include <pxr/usd/sdf/path.h>
|
||||
#include <pxr/usd/usd/primRange.h>
|
||||
#include <pxr/usd/usd/stage.h>
|
||||
#include <pxr/usd/usdGeom/metrics.h>
|
||||
#include <pxr/usd/usdGeom/pointInstancer.h>
|
||||
#include <pxr/usd/usdGeom/tokens.h>
|
||||
#include <pxr/usd/usdGeom/xform.h>
|
||||
#include <pxr/usd/usdGeom/xformCommonAPI.h>
|
||||
@@ -386,6 +388,71 @@ std::string cache_image_color(const float color[4])
|
||||
return file_path;
|
||||
}
|
||||
|
||||
static void mark_point_instancer_prototypes_as_over(const pxr::UsdStageRefPtr &stage,
|
||||
const pxr::SdfPath &wrapper_path,
|
||||
std::set<pxr::SdfPath> &visited)
|
||||
{
|
||||
pxr::UsdPrim wrapper_prim = stage->GetPrimAtPath(wrapper_path);
|
||||
if (!wrapper_prim || !wrapper_prim.IsValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string real_path_str;
|
||||
|
||||
for (const pxr::SdfPrimSpecHandle &primSpec : wrapper_prim.GetPrimStack()) {
|
||||
if (!primSpec || !primSpec->HasReferences()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const pxr::SdfReference &ref : primSpec->GetReferenceList().GetPrependedItems()) {
|
||||
if (ref.GetAssetPath().empty() && !ref.GetPrimPath().IsEmpty()) {
|
||||
real_path_str = ref.GetPrimPath().GetString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!real_path_str.empty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (real_path_str.empty()) {
|
||||
CLOG_WARN(&LOG, "No prototype reference found for: %s", wrapper_path.GetText());
|
||||
return;
|
||||
}
|
||||
|
||||
const pxr::SdfPath real_path(real_path_str);
|
||||
pxr::UsdPrim proto_prim = stage->GetPrimAtPath(real_path);
|
||||
|
||||
if (visited.count(real_path)) {
|
||||
return;
|
||||
}
|
||||
visited.insert(real_path);
|
||||
|
||||
if (!proto_prim || !proto_prim.IsValid()) {
|
||||
CLOG_WARN(&LOG, "Referenced prototype not found at: %s", real_path.GetText());
|
||||
return;
|
||||
}
|
||||
|
||||
proto_prim.SetSpecifier(pxr::SdfSpecifierOver);
|
||||
|
||||
std::string doc_message = fmt::format(
|
||||
"This prim is used as a prototype by the PointInstancer \"{}\" so we override the def "
|
||||
"with an \"over\" so that it isn't imaged in the scene, but is available as a prototype "
|
||||
"that can be referenced.",
|
||||
wrapper_prim.GetName().GetString());
|
||||
proto_prim.SetDocumentation(doc_message);
|
||||
|
||||
if (wrapper_prim.IsA<pxr::UsdGeomPointInstancer>()) {
|
||||
pxr::UsdGeomPointInstancer nested_instancer(wrapper_prim);
|
||||
pxr::SdfPathVector nested_targets;
|
||||
if (nested_instancer.GetPrototypesRel().GetTargets(&nested_targets)) {
|
||||
for (const pxr::SdfPath &nested_wrapper_path : nested_targets) {
|
||||
mark_point_instancer_prototypes_as_over(stage, nested_wrapper_path, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pxr::UsdStageRefPtr export_to_stage(const USDExportParams ¶ms,
|
||||
Depsgraph *depsgraph,
|
||||
const char *filepath)
|
||||
@@ -566,6 +633,22 @@ static void export_startjob(void *customdata, wmJobWorkerStatus *worker_status)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Traverse the point instancer to make sure the prototype referenced by nested point instancers
|
||||
* are also marked as over. */
|
||||
std::set<pxr::SdfPath> visited;
|
||||
for (const pxr::UsdPrim &prim : usd_stage->Traverse()) {
|
||||
if (!prim.IsA<pxr::UsdGeomPointInstancer>()) {
|
||||
continue;
|
||||
}
|
||||
pxr::UsdGeomPointInstancer instancer(prim);
|
||||
pxr::SdfPathVector targets;
|
||||
if (instancer.GetPrototypesRel().GetTargets(&targets)) {
|
||||
for (const pxr::SdfPath &wrapper_path : targets) {
|
||||
mark_point_instancer_prototypes_as_over(usd_stage, wrapper_path, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usd_stage->GetRootLayer()->Save();
|
||||
|
||||
data->export_ok = true;
|
||||
|
||||
@@ -35,6 +35,9 @@ struct USDExporterContext {
|
||||
const USDExportParams &export_params;
|
||||
std::string export_file_path;
|
||||
std::function<std::string(Main *, Scene *, Image *, ImageUser *)> export_image_fn;
|
||||
|
||||
/** Optional callback for skel/shapekey path registration (used by USDPointInstancerWriter) */
|
||||
std::function<void(const Object *, const pxr::SdfPath &)> add_skel_mapping_fn;
|
||||
};
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "usd_writer_light.hh"
|
||||
#include "usd_writer_mesh.hh"
|
||||
#include "usd_writer_metaball.hh"
|
||||
#include "usd_writer_pointinstancer.hh"
|
||||
#include "usd_writer_points.hh"
|
||||
#include "usd_writer_text.hh"
|
||||
#include "usd_writer_transform.hh"
|
||||
@@ -24,7 +25,9 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "BLI_assert.h"
|
||||
|
||||
@@ -133,13 +136,120 @@ USDExporterContext USDHierarchyIterator::create_usd_export_context(const Hierarc
|
||||
const std::string export_file_path = root_layer->GetRealPath();
|
||||
auto get_time_code = [this]() { return this->export_time_; };
|
||||
|
||||
return USDExporterContext{
|
||||
bmain_, depsgraph_, stage_, path, get_time_code, params_, export_file_path};
|
||||
USDExporterContext exporter_context = USDExporterContext{
|
||||
bmain_, depsgraph_, stage_, path, get_time_code, params_, export_file_path, nullptr};
|
||||
|
||||
/* Provides optional skel mapping hook. Now it's been used in USDPointInstancerWriter for write
|
||||
* base layer. */
|
||||
exporter_context.add_skel_mapping_fn = [this](const Object *obj, const pxr::SdfPath &path) {
|
||||
this->add_usd_skel_export_mapping(obj, path);
|
||||
};
|
||||
|
||||
return exporter_context;
|
||||
}
|
||||
|
||||
void USDHierarchyIterator::determine_point_instancers(const HierarchyContext *context)
|
||||
{
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context->object->type == OB_ARMATURE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context->is_point_instancer()) {
|
||||
/* Mark the point instancer's children as a point instance. */
|
||||
USDExporterContext usd_export_context = create_usd_export_context(context);
|
||||
ExportChildren *children = graph_children(context);
|
||||
|
||||
bool is_referencing_self = false;
|
||||
|
||||
pxr::SdfPath instancer_path;
|
||||
if (strlen(params_.root_prim_path) != 0) {
|
||||
instancer_path = pxr::SdfPath(std::string(params_.root_prim_path) + context->export_path);
|
||||
}
|
||||
else {
|
||||
instancer_path = pxr::SdfPath(context->export_path);
|
||||
}
|
||||
|
||||
if (children != nullptr) {
|
||||
for (HierarchyContext *child_context : *children) {
|
||||
if (!child_context->original_export_path.empty()) {
|
||||
const pxr::SdfPath parent_export_path(context->export_path);
|
||||
const pxr::SdfPath children_original_export_path(child_context->original_export_path);
|
||||
|
||||
/* Detect if the parent is referencing itself via a prototype. */
|
||||
if (parent_export_path.HasPrefix(children_original_export_path)) {
|
||||
is_referencing_self = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pxr::SdfPath prototype_path;
|
||||
if (child_context->is_instance() && child_context->duplicator != nullptr) {
|
||||
/* When the current child context is point instancer's instance, use reference path
|
||||
* (original_export_path) as the prototype path. */
|
||||
if (strlen(params_.root_prim_path) != 0) {
|
||||
prototype_path = pxr::SdfPath(std::string(params_.root_prim_path) +
|
||||
child_context->original_export_path);
|
||||
}
|
||||
else {
|
||||
prototype_path = pxr::SdfPath(child_context->original_export_path);
|
||||
}
|
||||
|
||||
prototype_paths[instancer_path].insert(
|
||||
std::make_pair(prototype_path, child_context->object));
|
||||
child_context->is_point_instance = true;
|
||||
}
|
||||
else {
|
||||
/* When the current child context is point instancer's prototype, use its own export path
|
||||
* (export_path) as the prototype path. */
|
||||
if (strlen(params_.root_prim_path) != 0) {
|
||||
prototype_path = pxr::SdfPath(std::string(params_.root_prim_path) +
|
||||
child_context->export_path);
|
||||
}
|
||||
else {
|
||||
prototype_path = pxr::SdfPath(child_context->export_path);
|
||||
}
|
||||
|
||||
prototype_paths[instancer_path].insert(
|
||||
std::make_pair(prototype_path, child_context->object));
|
||||
child_context->is_point_proto = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* MARK: If the "Instance on Points" node uses an Object as a prototype,
|
||||
* but the "Object Info" node has not enabled the "As Instance" option,
|
||||
* then the generated reference path is incorrect and refers to itself. */
|
||||
if (is_referencing_self) {
|
||||
BKE_reportf(
|
||||
params_.worker_status->reports,
|
||||
RPT_WARNING,
|
||||
"One or more objects used as prototypes in 'Instance on Points' nodes either do not "
|
||||
"have 'As Instance' enabled in their 'Object Info' nodes, or the prototype is the "
|
||||
"base geometry input itself. Both cases prevent valid point instancer export. If it's "
|
||||
"the former, enable 'As Instance' to avoid incorrect self-referencing.");
|
||||
|
||||
prototype_paths[instancer_path].clear();
|
||||
for (HierarchyContext *child_context : *children) {
|
||||
child_context->is_point_instance = false;
|
||||
child_context->is_point_proto = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AbstractHierarchyWriter *USDHierarchyIterator::create_transform_writer(
|
||||
const HierarchyContext *context)
|
||||
{
|
||||
/* transform writer is always called before data writers, so determin if the Xform's children is
|
||||
* a point instancer before writing data */
|
||||
if (params_.use_instancing) {
|
||||
determine_point_instancers(context);
|
||||
}
|
||||
|
||||
return new USDTransformWriter(create_usd_export_context(context));
|
||||
}
|
||||
|
||||
@@ -147,11 +257,24 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
|
||||
{
|
||||
USDExporterContext usd_export_context = create_usd_export_context(context);
|
||||
USDAbstractWriter *data_writer = nullptr;
|
||||
std::set<std::pair<pxr::SdfPath, Object *>> proto_paths =
|
||||
prototype_paths[usd_export_context.usd_path.GetParentPath()];
|
||||
|
||||
switch (context->object->type) {
|
||||
case OB_MESH:
|
||||
if (usd_export_context.export_params.export_meshes) {
|
||||
data_writer = new USDMeshWriter(usd_export_context);
|
||||
if (params_.use_instancing && context->is_point_instancer() && !proto_paths.empty()) {
|
||||
USDExporterContext mesh_context = create_point_instancer_context(context,
|
||||
usd_export_context);
|
||||
std::unique_ptr<USDMeshWriter> mesh_writer = std::make_unique<USDMeshWriter>(
|
||||
mesh_context);
|
||||
|
||||
data_writer = new USDPointInstancerWriter(
|
||||
usd_export_context, proto_paths, std::move(mesh_writer));
|
||||
}
|
||||
else {
|
||||
data_writer = new USDMeshWriter(usd_export_context);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return nullptr;
|
||||
@@ -182,7 +305,18 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
|
||||
case OB_CURVES_LEGACY:
|
||||
case OB_CURVES:
|
||||
if (usd_export_context.export_params.export_curves) {
|
||||
data_writer = new USDCurvesWriter(usd_export_context);
|
||||
if (params_.use_instancing && context->is_point_instancer() && !proto_paths.empty()) {
|
||||
USDExporterContext curves_context = create_point_instancer_context(context,
|
||||
usd_export_context);
|
||||
std::unique_ptr<USDCurvesWriter> curves_writer = std::make_unique<USDCurvesWriter>(
|
||||
curves_context);
|
||||
|
||||
data_writer = new USDPointInstancerWriter(
|
||||
usd_export_context, proto_paths, std::move(curves_writer));
|
||||
}
|
||||
else {
|
||||
data_writer = new USDCurvesWriter(usd_export_context);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return nullptr;
|
||||
@@ -206,7 +340,18 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
|
||||
break;
|
||||
case OB_POINTCLOUD:
|
||||
if (usd_export_context.export_params.export_points) {
|
||||
data_writer = new USDPointsWriter(usd_export_context);
|
||||
if (params_.use_instancing && context->is_point_instancer() && !proto_paths.empty()) {
|
||||
USDExporterContext point_cloud_context = create_point_instancer_context(
|
||||
context, usd_export_context);
|
||||
std::unique_ptr<USDPointsWriter> point_cloud_writer = std::make_unique<USDPointsWriter>(
|
||||
point_cloud_context);
|
||||
|
||||
data_writer = new USDPointInstancerWriter(
|
||||
usd_export_context, proto_paths, std::move(point_cloud_writer));
|
||||
}
|
||||
else {
|
||||
data_writer = new USDPointsWriter(usd_export_context);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return nullptr;
|
||||
@@ -229,7 +374,7 @@ AbstractHierarchyWriter *USDHierarchyIterator::create_data_writer(const Hierarch
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!data_writer->is_supported(context)) {
|
||||
if (data_writer && !data_writer->is_supported(context)) {
|
||||
delete data_writer;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -286,4 +431,21 @@ void USDHierarchyIterator::add_usd_skel_export_mapping(const Object *obj, const
|
||||
}
|
||||
}
|
||||
|
||||
USDExporterContext USDHierarchyIterator::create_point_instancer_context(
|
||||
const HierarchyContext *context, const USDExporterContext &usd_export_context)
|
||||
{
|
||||
BLI_assert(context && context->object);
|
||||
std::string base_name = std::string(BKE_id_name(context->object->id)).append("_base");
|
||||
std::string safe_name = make_safe_name(base_name,
|
||||
usd_export_context.export_params.allow_unicode);
|
||||
|
||||
pxr::SdfPath base_path = usd_export_context.usd_path.GetParentPath().AppendChild(
|
||||
pxr::TfToken(safe_name));
|
||||
|
||||
USDExporterContext new_context = usd_export_context;
|
||||
*const_cast<pxr::SdfPath *>(&new_context.usd_path) = base_path;
|
||||
|
||||
return new_context;
|
||||
}
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
||||
@@ -33,6 +33,10 @@ class USDHierarchyIterator : public AbstractHierarchyIterator {
|
||||
ObjExportMap skinned_mesh_export_map_;
|
||||
ObjExportMap shape_key_mesh_export_map_;
|
||||
|
||||
/* prototype_paths[instancer path] = [(proto_path_1, proto_object_1), (proto_path_2,
|
||||
* proto_object_2)...] */
|
||||
std::map<pxr::SdfPath, std::set<std::pair<pxr::SdfPath, Object *>>> prototype_paths;
|
||||
|
||||
public:
|
||||
USDHierarchyIterator(Main *bmain,
|
||||
Depsgraph *depsgraph,
|
||||
@@ -47,6 +51,7 @@ class USDHierarchyIterator : public AbstractHierarchyIterator {
|
||||
|
||||
protected:
|
||||
bool mark_as_weak_export(const Object *object) const override;
|
||||
void determine_point_instancers(const HierarchyContext *context);
|
||||
|
||||
AbstractHierarchyWriter *create_transform_writer(const HierarchyContext *context) override;
|
||||
AbstractHierarchyWriter *create_data_writer(const HierarchyContext *context) override;
|
||||
@@ -60,6 +65,8 @@ class USDHierarchyIterator : public AbstractHierarchyIterator {
|
||||
|
||||
private:
|
||||
USDExporterContext create_usd_export_context(const HierarchyContext *context);
|
||||
USDExporterContext create_point_instancer_context(const HierarchyContext *context,
|
||||
const USDExporterContext &usd_export_context);
|
||||
|
||||
void add_usd_skel_export_mapping(const Object *obj, const pxr::SdfPath &usd_path);
|
||||
};
|
||||
|
||||
@@ -30,5 +30,4 @@ std::string make_safe_name(StringRef name, bool allow_unicode);
|
||||
* \return A valid, and unique, USD `SdfPath`
|
||||
*/
|
||||
pxr::SdfPath get_unique_path(pxr::UsdStageRefPtr stage, const std::string &path);
|
||||
|
||||
} // namespace blender::io::usd
|
||||
|
||||
624
source/blender/io/usd/intern/usd_writer_pointinstancer.cc
Normal file
624
source/blender/io/usd/intern/usd_writer_pointinstancer.cc
Normal file
@@ -0,0 +1,624 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "usd_writer_pointinstancer.hh"
|
||||
#include "usd_attribute_utils.hh"
|
||||
#include "usd_utils.hh"
|
||||
#include "usd_writer_curves.hh"
|
||||
#include "usd_writer_mesh.hh"
|
||||
#include "usd_writer_points.hh"
|
||||
|
||||
#include "BKE_anonymous_attribute_id.hh"
|
||||
#include "BKE_collection.hh"
|
||||
#include "BKE_geometry_set.hh"
|
||||
#include "BKE_geometry_set_instances.hh"
|
||||
#include "BKE_instances.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_node.hh"
|
||||
#include "BKE_node_legacy_types.hh"
|
||||
#include "BKE_node_runtime.hh"
|
||||
#include "BKE_object.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "BLI_math_euler.hh"
|
||||
#include "BLI_math_matrix.hh"
|
||||
|
||||
#include "DNA_collection_types.h"
|
||||
#include "DNA_layer_types.h"
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_node_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
#include "DNA_pointcloud_types.h"
|
||||
|
||||
#include <pxr/base/gf/math.h>
|
||||
#include <pxr/base/gf/matrix3f.h>
|
||||
#include <pxr/base/gf/quatd.h>
|
||||
#include <pxr/base/gf/quatf.h>
|
||||
#include <pxr/base/gf/range3f.h>
|
||||
#include <pxr/base/gf/rotation.h>
|
||||
#include <pxr/base/gf/vec3d.h>
|
||||
#include <pxr/base/gf/vec3f.h>
|
||||
#include <pxr/base/vt/array.h>
|
||||
#include <pxr/usd/usdGeom/pointInstancer.h>
|
||||
#include <pxr/usd/usdGeom/primvarsAPI.h>
|
||||
#include <pxr/usd/usdGeom/scope.h>
|
||||
#include <pxr/usd/usdGeom/xform.h>
|
||||
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
#include "IO_abstract_hierarchy_iterator.h"
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace blender::io::usd {
|
||||
|
||||
USDPointInstancerWriter::USDPointInstancerWriter(
|
||||
const USDExporterContext &ctx,
|
||||
std::set<std::pair<pxr::SdfPath, Object *>> &prototype_paths,
|
||||
std::unique_ptr<USDAbstractWriter> base_writer)
|
||||
: USDAbstractWriter(ctx),
|
||||
base_writer_(std::move(base_writer)),
|
||||
prototype_paths_(prototype_paths)
|
||||
{
|
||||
}
|
||||
|
||||
void USDPointInstancerWriter::do_write(HierarchyContext &context)
|
||||
{
|
||||
/* Write the base data first (e.g., mesh, curves, points) */
|
||||
if (base_writer_) {
|
||||
base_writer_->write(context);
|
||||
|
||||
if (usd_export_context_.add_skel_mapping_fn &&
|
||||
(usd_export_context_.export_params.export_armatures ||
|
||||
usd_export_context_.export_params.export_shapekeys))
|
||||
{
|
||||
usd_export_context_.add_skel_mapping_fn(context.object, base_writer_->usd_path());
|
||||
}
|
||||
}
|
||||
|
||||
const pxr::UsdStageRefPtr stage = usd_export_context_.stage;
|
||||
Object *object_eval = context.object;
|
||||
bke::GeometrySet instance_geometry_set = bke::object_get_evaluated_geometry_set(*object_eval);
|
||||
|
||||
const bke::GeometryComponent *component = instance_geometry_set.get_component(
|
||||
bke::GeometryComponent::Type::Instance);
|
||||
|
||||
const bke::Instances *instances = static_cast<const bke::InstancesComponent &>(*component).get();
|
||||
|
||||
int instance_num = instances->instances_num();
|
||||
const pxr::SdfPath &usd_path = usd_export_context_.usd_path;
|
||||
const pxr::UsdGeomPointInstancer usd_instancer = pxr::UsdGeomPointInstancer::Define(stage,
|
||||
usd_path);
|
||||
const pxr::UsdTimeCode timecode = get_export_time_code();
|
||||
|
||||
Span<float4x4> transforms = instances->transforms();
|
||||
BLI_assert(transforms.size() >= instance_num);
|
||||
|
||||
if (transforms.size() != instance_num) {
|
||||
BKE_reportf(this->reports(),
|
||||
RPT_ERROR,
|
||||
"Instances number '%d' doesn't match transforms size '%d'",
|
||||
instance_num,
|
||||
int(transforms.size()));
|
||||
return;
|
||||
}
|
||||
|
||||
/* evaluated positions */
|
||||
pxr::UsdAttribute position_attr = usd_instancer.CreatePositionsAttr();
|
||||
pxr::VtArray<pxr::GfVec3f> positions(instance_num);
|
||||
for (int i = 0; i < instance_num; i++) {
|
||||
const float3 &pos = transforms[i].location();
|
||||
positions[i] = pxr::GfVec3f(pos.x, pos.y, pos.z);
|
||||
}
|
||||
blender::io::usd::set_attribute(position_attr, positions, timecode, usd_value_writer_);
|
||||
|
||||
/* orientations */
|
||||
pxr::UsdAttribute orientations_attr = usd_instancer.CreateOrientationsAttr();
|
||||
pxr::VtArray<pxr::GfQuath> orientation(instance_num);
|
||||
for (int i = 0; i < instance_num; i++) {
|
||||
const float3 euler = float3(math::to_euler(math::normalize(transforms[i])));
|
||||
const math::Quaternion quat = math::to_quaternion(math::EulerXYZ(euler));
|
||||
orientation[i] = pxr::GfQuath(quat.w, pxr::GfVec3h(quat.x, quat.y, quat.z));
|
||||
}
|
||||
blender::io::usd::set_attribute(orientations_attr, orientation, timecode, usd_value_writer_);
|
||||
|
||||
/* scales */
|
||||
pxr::UsdAttribute scales_attr = usd_instancer.CreateScalesAttr();
|
||||
pxr::VtArray<pxr::GfVec3f> scales(instance_num);
|
||||
for (int i = 0; i < instance_num; i++) {
|
||||
const MatBase<float, 4, 4> &mat = transforms[i];
|
||||
blender::float3 scale_vec = math::to_scale<true>(mat);
|
||||
scales[i] = pxr::GfVec3f(scale_vec.x, scale_vec.y, scale_vec.z);
|
||||
}
|
||||
blender::io::usd::set_attribute(scales_attr, scales, timecode, usd_value_writer_);
|
||||
|
||||
/* other attr */
|
||||
bke::AttributeAccessor attributes_eval = *component->attributes();
|
||||
attributes_eval.foreach_attribute([&](const bke::AttributeIter &iter) {
|
||||
if (iter.name[0] == '.' || blender::bke::attribute_name_is_anonymous(iter.name) ||
|
||||
ELEM(iter.name, "instance_transform") || ELEM(iter.name, "scale") ||
|
||||
ELEM(iter.name, "orientation") || ELEM(iter.name, "mask") ||
|
||||
ELEM(iter.name, "proto_index") || ELEM(iter.name, "id"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->write_attribute_data(iter, usd_instancer, timecode);
|
||||
});
|
||||
|
||||
/* prototypes relations */
|
||||
const pxr::SdfPath protoParentPath = usd_path.AppendChild(pxr::TfToken("Prototypes"));
|
||||
pxr::UsdPrim prototypesOver = stage->DefinePrim(protoParentPath);
|
||||
pxr::SdfPathVector proto_wrapper_paths;
|
||||
|
||||
std::map<std::string, int> proto_index_map;
|
||||
std::map<std::string, pxr::SdfPath> proto_path_map;
|
||||
|
||||
if (!prototype_paths_.empty() && usd_instancer) {
|
||||
int iter = 0;
|
||||
|
||||
for (const std::pair<pxr::SdfPath, Object *> &entry : prototype_paths_) {
|
||||
const pxr::SdfPath &source_path = entry.first;
|
||||
Object *obj = entry.second;
|
||||
|
||||
if (source_path.IsEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pxr::SdfPath proto_path = protoParentPath.AppendChild(
|
||||
pxr::TfToken(proto_name_ + "_" + std::to_string(iter)));
|
||||
|
||||
pxr::UsdPrim prim = stage->DefinePrim(proto_path);
|
||||
|
||||
/* To avoid USD error of Unresolved reference prim path, make sure the referenced path
|
||||
* exists. */
|
||||
stage->DefinePrim(source_path);
|
||||
prim.GetReferences().AddReference(pxr::SdfReference("", source_path));
|
||||
proto_wrapper_paths.push_back(proto_path);
|
||||
|
||||
std::string ob_name = BKE_id_name(obj->id);
|
||||
proto_index_map[ob_name] = iter;
|
||||
proto_path_map[ob_name] = proto_path;
|
||||
|
||||
++iter;
|
||||
}
|
||||
usd_instancer.GetPrototypesRel().SetTargets(proto_wrapper_paths);
|
||||
prototypesOver.GetPrim().SetSpecifier(pxr::SdfSpecifierOver);
|
||||
stage->GetRootLayer()->Save();
|
||||
}
|
||||
|
||||
/* proto indices */
|
||||
/* must be the last to populate */
|
||||
pxr::UsdAttribute proto_indices_attr = usd_instancer.CreateProtoIndicesAttr();
|
||||
pxr::VtArray<int> proto_indices;
|
||||
std::vector<std::pair<int, int>> collection_instance_object_count_map;
|
||||
|
||||
Span<int> reference_handles = instances->reference_handles();
|
||||
Span<bke::InstanceReference> references = instances->references();
|
||||
std::map<std::string, int> final_proto_index_map;
|
||||
|
||||
for (int i = 0; i < instance_num; i++) {
|
||||
bke::InstanceReference reference = references[reference_handles[i]];
|
||||
|
||||
process_instance_reference(reference,
|
||||
i,
|
||||
proto_index_map,
|
||||
final_proto_index_map,
|
||||
proto_path_map,
|
||||
stage,
|
||||
proto_indices,
|
||||
collection_instance_object_count_map);
|
||||
}
|
||||
|
||||
blender::io::usd::set_attribute(proto_indices_attr, proto_indices, timecode, usd_value_writer_);
|
||||
|
||||
/* Handle Collection Prototypes */
|
||||
if (!collection_instance_object_count_map.empty()) {
|
||||
handle_collection_prototypes(
|
||||
usd_instancer, timecode, instance_num, collection_instance_object_count_map);
|
||||
}
|
||||
|
||||
/* Clean unused prototype. When finding prototype paths under the context of a point instancer,
|
||||
* all the prototypes are collected, even those used by lower-level nested child PointInstancers.
|
||||
* It can happen that different levels in nested PointInstancers share the same prototypes, but
|
||||
* if not, we need to clean the extra prototypes from the prototype relationship for a cleaner
|
||||
* USD export. */
|
||||
compact_prototypes(usd_instancer, timecode, proto_wrapper_paths);
|
||||
|
||||
stage->GetRootLayer()->Save();
|
||||
}
|
||||
|
||||
void USDPointInstancerWriter::process_instance_reference(
|
||||
const bke::InstanceReference &reference,
|
||||
int instance_index,
|
||||
std::map<std::string, int> &proto_index_map,
|
||||
std::map<std::string, int> &final_proto_index_map,
|
||||
std::map<std::string, pxr::SdfPath> &proto_path_map,
|
||||
pxr::UsdStageRefPtr stage,
|
||||
pxr::VtArray<int> &proto_indices,
|
||||
std::vector<std::pair<int, int>> &collection_instance_object_count_map)
|
||||
{
|
||||
switch (reference.type()) {
|
||||
case bke::InstanceReference::Type::Object: {
|
||||
Object &object = reference.object();
|
||||
std::string ob_name = BKE_id_name(object.id);
|
||||
|
||||
if (proto_index_map.find(ob_name) != proto_index_map.end()) {
|
||||
proto_indices.push_back(proto_index_map[ob_name]);
|
||||
|
||||
final_proto_index_map[ob_name] = proto_index_map[ob_name];
|
||||
|
||||
/* If the reference is Object, clear prototype's local transform to identity to avoid
|
||||
* double transforms. The PointInstancer will fully control instance placement. */
|
||||
override_transform(stage, proto_path_map[ob_name], float4x4::identity());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case bke::InstanceReference::Type::Collection: {
|
||||
Collection &collection = reference.collection();
|
||||
int object_num = 0;
|
||||
FOREACH_COLLECTION_OBJECT_RECURSIVE_BEGIN (&collection, object) {
|
||||
std::string ob_name = BKE_id_name(object->id);
|
||||
|
||||
if (proto_index_map.find(ob_name) != proto_index_map.end()) {
|
||||
object_num += 1;
|
||||
proto_indices.push_back(proto_index_map[ob_name]);
|
||||
|
||||
final_proto_index_map[ob_name] = proto_index_map[ob_name];
|
||||
}
|
||||
}
|
||||
FOREACH_COLLECTION_OBJECT_RECURSIVE_END;
|
||||
collection_instance_object_count_map.push_back(std::make_pair(instance_index, object_num));
|
||||
break;
|
||||
}
|
||||
|
||||
case bke::InstanceReference::Type::GeometrySet: {
|
||||
bke::GeometrySet geometry_set = reference.geometry_set();
|
||||
std::string set_name = geometry_set.name;
|
||||
|
||||
if (proto_index_map.find(set_name) != proto_index_map.end()) {
|
||||
proto_indices.push_back(proto_index_map[set_name]);
|
||||
|
||||
final_proto_index_map[set_name] = proto_index_map[set_name];
|
||||
}
|
||||
|
||||
Vector<const bke::GeometryComponent *> components = geometry_set.get_components();
|
||||
for (const bke::GeometryComponent *comp : components) {
|
||||
if (const bke::Instances *instances =
|
||||
static_cast<const bke::InstancesComponent &>(*comp).get())
|
||||
{
|
||||
Span<int> ref_handles = instances->reference_handles();
|
||||
Span<bke::InstanceReference> refs = instances->references();
|
||||
|
||||
/* If the top-level GeometrySet is not in proto_index_map, recursively traverse child
|
||||
* InstanceReferences to resolve prototype indices. If the name matches proto_index_map,
|
||||
* skip traversal to avoid duplicates, since GeometrySet names may overlap with object
|
||||
* names. */
|
||||
if (proto_index_map.find(set_name) == proto_index_map.end()) {
|
||||
for (int index = 0; index < ref_handles.size(); ++index) {
|
||||
const bke::InstanceReference &child_ref = refs[ref_handles[index]];
|
||||
|
||||
/* Recursively traverse nested GeometrySets to resolve prototype indices for all
|
||||
* instances. */
|
||||
process_instance_reference(child_ref,
|
||||
instance_index,
|
||||
proto_index_map,
|
||||
final_proto_index_map,
|
||||
proto_path_map,
|
||||
stage,
|
||||
proto_indices,
|
||||
collection_instance_object_count_map);
|
||||
}
|
||||
}
|
||||
|
||||
/* If the reference is GeometrySet, then override the transform with the transform of the
|
||||
* Instance inside this Geometryset. */
|
||||
Span<float4x4> transforms = instances->transforms();
|
||||
if (transforms.size() == 1) {
|
||||
if (proto_path_map.find(set_name) != proto_path_map.end()) {
|
||||
override_transform(stage, proto_path_map[set_name], transforms[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case bke::InstanceReference::Type::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void USDPointInstancerWriter::compact_prototypes(const pxr::UsdGeomPointInstancer &usd_instancer,
|
||||
const pxr::UsdTimeCode timecode,
|
||||
const pxr::SdfPathVector &proto_paths)
|
||||
{
|
||||
pxr::UsdAttribute proto_indices_attr = usd_instancer.GetProtoIndicesAttr();
|
||||
pxr::VtArray<int> proto_indices;
|
||||
if (!proto_indices_attr.Get(&proto_indices, timecode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
///* Find actually used prototype indices */
|
||||
std::set<int> used_proto_indices(proto_indices.begin(), proto_indices.end());
|
||||
|
||||
std::map<int, int> remap;
|
||||
int new_index = 0;
|
||||
for (int i = 0; i < proto_paths.size(); ++i) {
|
||||
if (used_proto_indices.count(i)) {
|
||||
remap[i] = new_index++;
|
||||
}
|
||||
}
|
||||
|
||||
///* Remap protoIndices */
|
||||
for (int &idx : proto_indices) {
|
||||
idx = remap[idx];
|
||||
}
|
||||
proto_indices_attr.Set(proto_indices, timecode);
|
||||
|
||||
pxr::SdfPathVector compact_proto_paths;
|
||||
for (int i = 0; i < proto_paths.size(); ++i) {
|
||||
if (used_proto_indices.count(i)) {
|
||||
compact_proto_paths.push_back(proto_paths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
usd_instancer.GetPrototypesRel().SetTargets(compact_proto_paths);
|
||||
}
|
||||
|
||||
void USDPointInstancerWriter::override_transform(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath &proto_path,
|
||||
const float4x4 &transform)
|
||||
{
|
||||
// Extract translation
|
||||
const float3 &pos = transform.location();
|
||||
pxr::GfVec3d override_position(pos.x, pos.y, pos.z);
|
||||
|
||||
// Extract rotation
|
||||
const float3 euler = float3(math::to_euler(math::normalize(transform)));
|
||||
pxr::GfVec3f override_rotation(euler.x, euler.y, euler.z);
|
||||
|
||||
// Extract scale
|
||||
const float3 scale_vec = math::to_scale<true>(transform);
|
||||
pxr::GfVec3f override_scale(scale_vec.x, scale_vec.y, scale_vec.z);
|
||||
|
||||
pxr::UsdPrim prim = stage->GetPrimAtPath(proto_path);
|
||||
if (!prim) {
|
||||
return;
|
||||
}
|
||||
|
||||
pxr::UsdGeomXformable xformable(prim);
|
||||
xformable.ClearXformOpOrder();
|
||||
xformable.AddTranslateOp().Set(override_position);
|
||||
xformable.AddRotateXYZOp().Set(override_rotation);
|
||||
xformable.AddScaleOp().Set(override_scale);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static pxr::VtArray<T> DuplicateArray(const pxr::VtArray<T> &original, size_t copies)
|
||||
{
|
||||
pxr::VtArray<T> newArray;
|
||||
size_t originalSize = original.size();
|
||||
newArray.resize(originalSize * copies);
|
||||
for (size_t i = 0; i < copies; ++i) {
|
||||
std::copy(original.begin(), original.end(), newArray.begin() + i * originalSize);
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
template<typename T, typename GetterFunc, typename CreatorFunc>
|
||||
static void DuplicatePerInstanceAttribute(const GetterFunc &getter,
|
||||
const CreatorFunc &creator,
|
||||
size_t copies,
|
||||
const pxr::UsdTimeCode &timecode)
|
||||
{
|
||||
pxr::VtArray<T> values;
|
||||
if (getter().Get(&values, timecode) && !values.empty()) {
|
||||
auto newValues = DuplicateArray(values, copies);
|
||||
creator().Set(newValues, timecode);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T, typename GetterFunc, typename CreatorFunc>
|
||||
static void ExpandAttributePerInstance(const GetterFunc &getter,
|
||||
const CreatorFunc &creator,
|
||||
const std::vector<std::pair<int, int>> &instance_object_map,
|
||||
const pxr::UsdTimeCode &timecode)
|
||||
{
|
||||
// MARK: Handle Collection Prototypes
|
||||
// -----------------------------------------------------------------------------
|
||||
// In Blender, a Collection is not an actual Object type. When exporting, the iterator
|
||||
// flattens the Collection hierarchy, treating each object inside the Collection as an
|
||||
// individual prototype. However, all these prototypes share the same instance attributes
|
||||
// (e.g., positions, orientations, scales).
|
||||
//
|
||||
// To ensure correct arrangement, reading, and drawing in OpenUSD, we need to explicitly
|
||||
// duplicate the instance attributes across all prototypes derived from the Collection.
|
||||
pxr::VtArray<T> original_values;
|
||||
if (!getter().Get(&original_values, timecode) || original_values.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
pxr::VtArray<T> expanded_values;
|
||||
for (const auto &[instance_index, object_count] : instance_object_map) {
|
||||
if (instance_index < static_cast<int>(original_values.size())) {
|
||||
for (int i = 0; i < object_count; ++i) {
|
||||
expanded_values.push_back(original_values[instance_index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
creator().Set(expanded_values, timecode);
|
||||
}
|
||||
|
||||
void USDPointInstancerWriter::handle_collection_prototypes(
|
||||
const pxr::UsdGeomPointInstancer &usd_instancer,
|
||||
const pxr::UsdTimeCode timecode,
|
||||
int instance_num,
|
||||
const std::vector<std::pair<int, int>> &collection_instance_object_count_map)
|
||||
{
|
||||
// Duplicate attributes
|
||||
if (usd_instancer.GetPositionsAttr().HasAuthoredValue()) {
|
||||
ExpandAttributePerInstance<pxr::GfVec3f>([&]() { return usd_instancer.GetPositionsAttr(); },
|
||||
[&]() { return usd_instancer.CreatePositionsAttr(); },
|
||||
collection_instance_object_count_map,
|
||||
timecode);
|
||||
}
|
||||
if (usd_instancer.GetOrientationsAttr().HasAuthoredValue()) {
|
||||
ExpandAttributePerInstance<pxr::GfQuath>(
|
||||
[&]() { return usd_instancer.GetOrientationsAttr(); },
|
||||
[&]() { return usd_instancer.CreateOrientationsAttr(); },
|
||||
collection_instance_object_count_map,
|
||||
timecode);
|
||||
}
|
||||
if (usd_instancer.GetScalesAttr().HasAuthoredValue()) {
|
||||
ExpandAttributePerInstance<pxr::GfVec3f>([&]() { return usd_instancer.GetScalesAttr(); },
|
||||
[&]() { return usd_instancer.CreateScalesAttr(); },
|
||||
collection_instance_object_count_map,
|
||||
timecode);
|
||||
}
|
||||
if (usd_instancer.GetVelocitiesAttr().HasAuthoredValue()) {
|
||||
ExpandAttributePerInstance<pxr::GfVec3f>(
|
||||
[&]() { return usd_instancer.GetVelocitiesAttr(); },
|
||||
[&]() { return usd_instancer.CreateVelocitiesAttr(); },
|
||||
collection_instance_object_count_map,
|
||||
timecode);
|
||||
}
|
||||
if (usd_instancer.GetAngularVelocitiesAttr().HasAuthoredValue()) {
|
||||
ExpandAttributePerInstance<pxr::GfVec3f>(
|
||||
[&]() { return usd_instancer.GetAngularVelocitiesAttr(); },
|
||||
[&]() { return usd_instancer.CreateAngularVelocitiesAttr(); },
|
||||
collection_instance_object_count_map,
|
||||
timecode);
|
||||
}
|
||||
|
||||
// Duplicate Primvars
|
||||
const pxr::UsdGeomPrimvarsAPI primvars_api(usd_instancer);
|
||||
std::vector<pxr::UsdGeomPrimvar> primvars = primvars_api.GetPrimvars();
|
||||
for (const pxr::UsdGeomPrimvar &primvar : primvars) {
|
||||
if (!primvar.HasAuthoredValue()) {
|
||||
continue;
|
||||
}
|
||||
const pxr::TfToken name = primvar.GetPrimvarName();
|
||||
const pxr::SdfValueTypeName type = primvar.GetTypeName();
|
||||
const pxr::TfToken interp = primvar.GetInterpolation();
|
||||
auto create = [&]() { return primvars_api.CreatePrimvar(name, type, interp); };
|
||||
|
||||
if (type == pxr::SdfValueTypeNames->FloatArray) {
|
||||
ExpandAttributePerInstance<float>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
else if (type == pxr::SdfValueTypeNames->IntArray) {
|
||||
ExpandAttributePerInstance<int>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
else if (type == pxr::SdfValueTypeNames->UCharArray) {
|
||||
ExpandAttributePerInstance<unsigned char>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
else if (type == pxr::SdfValueTypeNames->Float2Array) {
|
||||
ExpandAttributePerInstance<pxr::GfVec2f>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
else if (type == pxr::SdfValueTypeNames->Float3Array ||
|
||||
type == pxr::SdfValueTypeNames->Color3fArray ||
|
||||
type == pxr::SdfValueTypeNames->Color4fArray)
|
||||
{
|
||||
ExpandAttributePerInstance<pxr::GfVec3f>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
else if (type == pxr::SdfValueTypeNames->QuatfArray) {
|
||||
ExpandAttributePerInstance<pxr::GfQuatf>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
else if (type == pxr::SdfValueTypeNames->BoolArray) {
|
||||
ExpandAttributePerInstance<bool>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
else if (type == pxr::SdfValueTypeNames->StringArray) {
|
||||
ExpandAttributePerInstance<std::string>(
|
||||
[&]() { return primvar; }, create, collection_instance_object_count_map, timecode);
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Ensure Instance Indices Exist
|
||||
// -----------------------------------------------------------------------------
|
||||
// If the PointInstancer has no authored instance indices, manually generate a default
|
||||
// sequence of indices to ensure the PointInstancer functions correctly in OpenUSD.
|
||||
// This guarantees that each instance can correctly reference its prototype.
|
||||
pxr::UsdAttribute proto_indices_attr = usd_instancer.GetProtoIndicesAttr();
|
||||
if (!proto_indices_attr.HasAuthoredValue()) {
|
||||
std::vector<int> index;
|
||||
for (int i = 0; i < prototype_paths_.size(); i++) {
|
||||
std::vector<int> current_proto_index(instance_num, i);
|
||||
index.insert(index.end(), current_proto_index.begin(), current_proto_index.end());
|
||||
}
|
||||
|
||||
proto_indices_attr.Set(pxr::VtArray<int>(index.begin(), index.end()));
|
||||
}
|
||||
}
|
||||
|
||||
void USDPointInstancerWriter::write_attribute_data(const bke::AttributeIter &attr,
|
||||
const pxr::UsdGeomPointInstancer &usd_instancer,
|
||||
const pxr::UsdTimeCode timecode)
|
||||
{
|
||||
const std::optional<pxr::SdfValueTypeName> pv_type = convert_blender_type_to_usd(attr.data_type);
|
||||
|
||||
if (!pv_type) {
|
||||
BKE_reportf(this->reports(),
|
||||
RPT_WARNING,
|
||||
"Attribute '%s' (Blender domain %d, type %d) cannot be converted to USD",
|
||||
attr.name.c_str(),
|
||||
int(attr.domain),
|
||||
attr.data_type);
|
||||
return;
|
||||
}
|
||||
|
||||
const GVArray attribute = *attr.get();
|
||||
if (attribute.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attr.name == "mask") {
|
||||
pxr::UsdAttribute idsAttr = usd_instancer.GetIdsAttr();
|
||||
if (!idsAttr) {
|
||||
idsAttr = usd_instancer.CreateIdsAttr();
|
||||
}
|
||||
|
||||
pxr::UsdAttribute invisibleIdsAttr = usd_instancer.GetInvisibleIdsAttr();
|
||||
if (!invisibleIdsAttr) {
|
||||
invisibleIdsAttr = usd_instancer.CreateInvisibleIdsAttr();
|
||||
}
|
||||
|
||||
const GVArray attribute = *attr.get();
|
||||
/// Retrieve mask values, store as int8_t to avoid std::vector<bool>.data() issues
|
||||
std::vector<int8_t> mask_values(attribute.size());
|
||||
attribute.materialize(IndexMask(attribute.size()), mask_values.data());
|
||||
|
||||
pxr::VtArray<int64_t> ids;
|
||||
pxr::VtArray<int64_t> invisibleIds;
|
||||
ids.reserve(mask_values.size());
|
||||
|
||||
for (int64_t i = 0; i < static_cast<int64_t>(mask_values.size()); ++i) {
|
||||
ids.push_back(i);
|
||||
if (mask_values[i] == 0) {
|
||||
invisibleIds.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
blender::io::usd::set_attribute(idsAttr, ids, timecode, usd_value_writer_);
|
||||
blender::io::usd::set_attribute(invisibleIdsAttr, invisibleIds, timecode, usd_value_writer_);
|
||||
}
|
||||
|
||||
const pxr::TfToken pv_name(
|
||||
make_safe_name(attr.name, usd_export_context_.export_params.allow_unicode));
|
||||
const pxr::UsdGeomPrimvarsAPI pv_api = pxr::UsdGeomPrimvarsAPI(usd_instancer);
|
||||
|
||||
pxr::UsdGeomPrimvar pv_attr = pv_api.CreatePrimvar(pv_name, *pv_type);
|
||||
|
||||
copy_blender_attribute_to_primvar(
|
||||
attribute, attr.data_type, timecode, pv_attr, usd_value_writer_);
|
||||
}
|
||||
|
||||
} // namespace blender::io::usd
|
||||
62
source/blender/io/usd/intern/usd_writer_pointinstancer.hh
Normal file
62
source/blender/io/usd/intern/usd_writer_pointinstancer.hh
Normal file
@@ -0,0 +1,62 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_math_quaternion_types.hh"
|
||||
#include "usd_attribute_utils.hh"
|
||||
#include "usd_writer_abstract.hh"
|
||||
#include <pxr/usd/usdGeom/pointInstancer.h>
|
||||
#include <pxr/usd/usdGeom/points.h>
|
||||
#include <vector>
|
||||
|
||||
struct USDExporterContext;
|
||||
|
||||
namespace blender::io::usd {
|
||||
|
||||
class USDPointInstancerWriter final : public USDAbstractWriter {
|
||||
public:
|
||||
USDPointInstancerWriter(const USDExporterContext &ctx,
|
||||
std::set<std::pair<pxr::SdfPath, Object *>> &prototype_paths,
|
||||
std::unique_ptr<USDAbstractWriter> base_writer);
|
||||
~USDPointInstancerWriter() final = default;
|
||||
|
||||
protected:
|
||||
virtual void do_write(HierarchyContext &context) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<USDAbstractWriter> base_writer_;
|
||||
std::set<std::pair<pxr::SdfPath, Object *>> prototype_paths_;
|
||||
const std::string proto_name_ = "Prototype";
|
||||
|
||||
void write_attribute_data(const bke::AttributeIter &attr,
|
||||
const pxr::UsdGeomPointInstancer &usd_instancer,
|
||||
const pxr::UsdTimeCode timecode);
|
||||
|
||||
void process_instance_reference(
|
||||
const bke::InstanceReference &reference,
|
||||
int instance_index,
|
||||
std::map<std::string, int> &proto_index_map,
|
||||
std::map<std::string, int> &final_proto_index_map,
|
||||
std::map<std::string, pxr::SdfPath> &proto_path_map,
|
||||
pxr::UsdStageRefPtr stage,
|
||||
pxr::VtArray<int> &proto_indices,
|
||||
std::vector<std::pair<int, int>> &collection_instance_object_count_map);
|
||||
|
||||
void compact_prototypes(const pxr::UsdGeomPointInstancer &usd_instancer,
|
||||
const pxr::UsdTimeCode timecode,
|
||||
const pxr::SdfPathVector &proto_paths);
|
||||
|
||||
void override_transform(pxr::UsdStageRefPtr stage,
|
||||
const pxr::SdfPath &proto_path,
|
||||
const float4x4 &transform);
|
||||
|
||||
void handle_collection_prototypes(
|
||||
const pxr::UsdGeomPointInstancer &usd_instancer,
|
||||
const pxr::UsdTimeCode timecode,
|
||||
int instance_num,
|
||||
const std::vector<std::pair<int, int>> &collection_instance_object_count_map);
|
||||
};
|
||||
|
||||
} // namespace blender::io::usd
|
||||
@@ -61,6 +61,10 @@ bool USDTransformWriter::should_apply_root_xform(const HierarchyContext &context
|
||||
|
||||
void USDTransformWriter::do_write(HierarchyContext &context)
|
||||
{
|
||||
if (context.is_point_proto || context.is_point_instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float UNIT_M4[4][4] = {
|
||||
{1, 0, 0, 0},
|
||||
{0, 1, 0, 0},
|
||||
|
||||
BIN
tests/files/usd/usd_point_instancer_collection_ref.blend
(Stored with Git LFS)
Normal file
BIN
tests/files/usd/usd_point_instancer_collection_ref.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
tests/files/usd/usd_point_instancer_nested.blend
(Stored with Git LFS)
Normal file
BIN
tests/files/usd/usd_point_instancer_nested.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
tests/files/usd/usd_point_instancer_object_ref.blend
(Stored with Git LFS)
Normal file
BIN
tests/files/usd/usd_point_instancer_object_ref.blend
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -1689,6 +1689,95 @@ class USDExportTest(AbstractUSDTest):
|
||||
|
||||
self.assertTupleEqual(expected, actual)
|
||||
|
||||
def test_point_instancing_export(self):
|
||||
"""Test exporting scenes that use point instancing."""
|
||||
|
||||
def confirm_point_instancing_stats(stage, num_meshes, num_instancers, num_instances, num_prototypes):
|
||||
mesh_count = 0
|
||||
instancer_count = 0
|
||||
instance_count = 0
|
||||
prototype_count = 0
|
||||
|
||||
for prim in stage.TraverseAll():
|
||||
prim_path = prim.GetPath()
|
||||
prim_type_name = prim.GetTypeName()
|
||||
|
||||
if prim_type_name == "PointInstancer":
|
||||
point_instancer = UsdGeom.PointInstancer(prim)
|
||||
if point_instancer:
|
||||
|
||||
# get instance count
|
||||
positions_attr = point_instancer.GetPositionsAttr()
|
||||
if positions_attr:
|
||||
positions = positions_attr.Get()
|
||||
if positions:
|
||||
instance_count += len(positions)
|
||||
|
||||
# get prototype count
|
||||
prototypes_rel = point_instancer.GetPrototypesRel()
|
||||
if prototypes_rel:
|
||||
target_prims = prototypes_rel.GetTargets()
|
||||
prototype_count += len(target_prims)
|
||||
|
||||
# show all prims and types
|
||||
# output_string = f" Path: {prim_path}, Type: {prim_type_name}"
|
||||
# print(output_string)
|
||||
|
||||
stats = UsdUtils.ComputeUsdStageStats(stage)
|
||||
mesh_count = stats['primary']['primCountsByType']['Mesh']
|
||||
instancer_count = stats['primary']['primCountsByType']['PointInstancer']
|
||||
|
||||
return mesh_count, instancer_count, instance_count, prototype_count
|
||||
|
||||
point_instance_test_scenarios = [
|
||||
# object reference treated as geometry set
|
||||
{'input_file': str(self.testdir / "usd_point_instancer_object_ref.blend"),
|
||||
'output_file': self.tempdir / "usd_export_point_instancer_object_ref.usda",
|
||||
'mesh_count': 3,
|
||||
'instancer_count': 1,
|
||||
'total_instances': 16,
|
||||
'total_prototypes': 1},
|
||||
# collection reference from single point instancer
|
||||
{'input_file': str(self.testdir / "usd_point_instancer_collection_ref.blend"),
|
||||
'output_file': self.tempdir / "usd_export_point_instancer_collection_ref.usda",
|
||||
'mesh_count': 5,
|
||||
'instancer_count': 1,
|
||||
'total_instances': 32,
|
||||
'total_prototypes': 2},
|
||||
# collection references in nested point instancer
|
||||
{'input_file': str(self.testdir / "usd_point_instancer_nested.blend"),
|
||||
'output_file': self.tempdir / "usd_export_point_instancer_nested.usda",
|
||||
'mesh_count': 9,
|
||||
'instancer_count': 3,
|
||||
'total_instances': 14,
|
||||
'total_prototypes': 4},
|
||||
# object reference coming from a collection with separate children
|
||||
{'input_file': str(self.testdir / "../render/shader/texture_coordinate_camera.blend"),
|
||||
'output_file': self.tempdir / "usd_export_point_instancer_separate_children.usda",
|
||||
'mesh_count': 9,
|
||||
'instancer_count': 1,
|
||||
'total_instances': 4,
|
||||
'total_prototypes': 2}
|
||||
]
|
||||
|
||||
for scenario in point_instance_test_scenarios:
|
||||
bpy.ops.wm.open_mainfile(filepath=scenario['input_file'])
|
||||
|
||||
export_path = scenario['output_file']
|
||||
self.export_and_validate(
|
||||
filepath=str(export_path),
|
||||
use_instancing=True
|
||||
)
|
||||
|
||||
stage = Usd.Stage.Open(str(export_path))
|
||||
|
||||
mesh_count, instancer_count, instance_count, proto_count = confirm_point_instancing_stats(
|
||||
stage, scenario['mesh_count'], scenario['instancer_count'], scenario['total_instances'], scenario['total_prototypes'])
|
||||
self.assertEqual(scenario['mesh_count'], mesh_count, "Unexpected number of primary meshes")
|
||||
self.assertEqual(scenario['instancer_count'], instancer_count, "Unexpected number of point instancers")
|
||||
self.assertEqual(scenario['total_instances'], instance_count, "Unexpected number of total instances")
|
||||
self.assertEqual(scenario['total_prototypes'], proto_count, "Unexpected number of total prototypes")
|
||||
|
||||
|
||||
class USDHookBase:
|
||||
instructions = {}
|
||||
|
||||
Reference in New Issue
Block a user