From 07342407d35c2dd4072bf6bc2f5920444a2b4606 Mon Sep 17 00:00:00 2001 From: Michael B Johnson Date: Sat, 14 Jun 2025 01:10:55 +0200 Subject: [PATCH] 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 --- .../common/IO_abstract_hierarchy_iterator.h | 15 + .../intern/abstract_hierarchy_iterator.cc | 34 +- source/blender/io/usd/CMakeLists.txt | 2 + .../blender/io/usd/intern/usd_capi_export.cc | 83 +++ .../io/usd/intern/usd_exporter_context.hh | 3 + .../io/usd/intern/usd_hierarchy_iterator.cc | 174 ++++- .../io/usd/intern/usd_hierarchy_iterator.hh | 7 + source/blender/io/usd/intern/usd_utils.hh | 1 - .../usd/intern/usd_writer_pointinstancer.cc | 624 ++++++++++++++++++ .../usd/intern/usd_writer_pointinstancer.hh | 62 ++ .../io/usd/intern/usd_writer_transform.cc | 4 + .../usd_point_instancer_collection_ref.blend | 3 + .../usd/usd_point_instancer_nested.blend | 3 + .../usd/usd_point_instancer_object_ref.blend | 3 + tests/python/bl_usd_export_test.py | 89 +++ 15 files changed, 1097 insertions(+), 10 deletions(-) create mode 100644 source/blender/io/usd/intern/usd_writer_pointinstancer.cc create mode 100644 source/blender/io/usd/intern/usd_writer_pointinstancer.hh create mode 100644 tests/files/usd/usd_point_instancer_collection_ref.blend create mode 100644 tests/files/usd/usd_point_instancer_nested.blend create mode 100644 tests/files/usd/usd_point_instancer_object_ref.blend diff --git a/source/blender/io/common/IO_abstract_hierarchy_iterator.h b/source/blender/io/common/IO_abstract_hierarchy_iterator.h index dade895d2af..4e2cb32122b 100644 --- a/source/blender/io/common/IO_abstract_hierarchy_iterator.h +++ b/source/blender/io/common/IO_abstract_hierarchy_iterator.h @@ -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; }; diff --git a/source/blender/io/common/intern/abstract_hierarchy_iterator.cc b/source/blender/io/common/intern/abstract_hierarchy_iterator.cc index ac0ecc2ef88..c82adef847d 100644 --- a/source/blender/io/common/intern/abstract_hierarchy_iterator.cc +++ b/source/blender/io/common/intern/abstract_hierarchy_iterator.cc @@ -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); } diff --git a/source/blender/io/usd/CMakeLists.txt b/source/blender/io/usd/CMakeLists.txt index e401aa5785c..86bef88da4f 100644 --- a/source/blender/io/usd/CMakeLists.txt +++ b/source/blender/io/usd/CMakeLists.txt @@ -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 diff --git a/source/blender/io/usd/intern/usd_capi_export.cc b/source/blender/io/usd/intern/usd_capi_export.cc index 20a36fa1368..8e679d386c4 100644 --- a/source/blender/io/usd/intern/usd_capi_export.cc +++ b/source/blender/io/usd/intern/usd_capi_export.cc @@ -15,9 +15,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -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 &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 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 visited; + for (const pxr::UsdPrim &prim : usd_stage->Traverse()) { + if (!prim.IsA()) { + 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; diff --git a/source/blender/io/usd/intern/usd_exporter_context.hh b/source/blender/io/usd/intern/usd_exporter_context.hh index d7266a0b6b6..886cda6733e 100644 --- a/source/blender/io/usd/intern/usd_exporter_context.hh +++ b/source/blender/io/usd/intern/usd_exporter_context.hh @@ -35,6 +35,9 @@ struct USDExporterContext { const USDExportParams &export_params; std::string export_file_path; std::function export_image_fn; + + /** Optional callback for skel/shapekey path registration (used by USDPointInstancerWriter) */ + std::function add_skel_mapping_fn; }; } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_hierarchy_iterator.cc b/source/blender/io/usd/intern/usd_hierarchy_iterator.cc index 054bad8576f..85478913091 100644 --- a/source/blender/io/usd/intern/usd_hierarchy_iterator.cc +++ b/source/blender/io/usd/intern/usd_hierarchy_iterator.cc @@ -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 +#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> 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 mesh_writer = std::make_unique( + 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 curves_writer = std::make_unique( + 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 point_cloud_writer = std::make_unique( + 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(&new_context.usd_path) = base_path; + + return new_context; +} + } // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_hierarchy_iterator.hh b/source/blender/io/usd/intern/usd_hierarchy_iterator.hh index c101737999c..e0ad89b9f91 100644 --- a/source/blender/io/usd/intern/usd_hierarchy_iterator.hh +++ b/source/blender/io/usd/intern/usd_hierarchy_iterator.hh @@ -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>> 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); }; diff --git a/source/blender/io/usd/intern/usd_utils.hh b/source/blender/io/usd/intern/usd_utils.hh index d9095f1f306..173c1869ad7 100644 --- a/source/blender/io/usd/intern/usd_utils.hh +++ b/source/blender/io/usd/intern/usd_utils.hh @@ -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 diff --git a/source/blender/io/usd/intern/usd_writer_pointinstancer.cc b/source/blender/io/usd/intern/usd_writer_pointinstancer.cc new file mode 100644 index 00000000000..00c55e65dbc --- /dev/null +++ b/source/blender/io/usd/intern/usd_writer_pointinstancer.cc @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DEG_depsgraph_query.hh" +#include "IO_abstract_hierarchy_iterator.h" +#include + +namespace blender::io::usd { + +USDPointInstancerWriter::USDPointInstancerWriter( + const USDExporterContext &ctx, + std::set> &prototype_paths, + std::unique_ptr 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(*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 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 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 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 scales(instance_num); + for (int i = 0; i < instance_num; i++) { + const MatBase &mat = transforms[i]; + blender::float3 scale_vec = math::to_scale(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 proto_index_map; + std::map proto_path_map; + + if (!prototype_paths_.empty() && usd_instancer) { + int iter = 0; + + for (const std::pair &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 proto_indices; + std::vector> collection_instance_object_count_map; + + Span reference_handles = instances->reference_handles(); + Span references = instances->references(); + std::map 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 &proto_index_map, + std::map &final_proto_index_map, + std::map &proto_path_map, + pxr::UsdStageRefPtr stage, + pxr::VtArray &proto_indices, + std::vector> &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 components = geometry_set.get_components(); + for (const bke::GeometryComponent *comp : components) { + if (const bke::Instances *instances = + static_cast(*comp).get()) + { + Span ref_handles = instances->reference_handles(); + Span 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 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 proto_indices; + if (!proto_indices_attr.Get(&proto_indices, timecode)) { + return; + } + + ///* Find actually used prototype indices */ + std::set used_proto_indices(proto_indices.begin(), proto_indices.end()); + + std::map 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(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 +static pxr::VtArray DuplicateArray(const pxr::VtArray &original, size_t copies) +{ + pxr::VtArray 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 +static void DuplicatePerInstanceAttribute(const GetterFunc &getter, + const CreatorFunc &creator, + size_t copies, + const pxr::UsdTimeCode &timecode) +{ + pxr::VtArray values; + if (getter().Get(&values, timecode) && !values.empty()) { + auto newValues = DuplicateArray(values, copies); + creator().Set(newValues, timecode); + } +} + +template +static void ExpandAttributePerInstance(const GetterFunc &getter, + const CreatorFunc &creator, + const std::vector> &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 original_values; + if (!getter().Get(&original_values, timecode) || original_values.empty()) { + return; + } + + pxr::VtArray expanded_values; + for (const auto &[instance_index, object_count] : instance_object_map) { + if (instance_index < static_cast(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> &collection_instance_object_count_map) +{ + // Duplicate attributes + if (usd_instancer.GetPositionsAttr().HasAuthoredValue()) { + ExpandAttributePerInstance([&]() { return usd_instancer.GetPositionsAttr(); }, + [&]() { return usd_instancer.CreatePositionsAttr(); }, + collection_instance_object_count_map, + timecode); + } + if (usd_instancer.GetOrientationsAttr().HasAuthoredValue()) { + ExpandAttributePerInstance( + [&]() { return usd_instancer.GetOrientationsAttr(); }, + [&]() { return usd_instancer.CreateOrientationsAttr(); }, + collection_instance_object_count_map, + timecode); + } + if (usd_instancer.GetScalesAttr().HasAuthoredValue()) { + ExpandAttributePerInstance([&]() { return usd_instancer.GetScalesAttr(); }, + [&]() { return usd_instancer.CreateScalesAttr(); }, + collection_instance_object_count_map, + timecode); + } + if (usd_instancer.GetVelocitiesAttr().HasAuthoredValue()) { + ExpandAttributePerInstance( + [&]() { return usd_instancer.GetVelocitiesAttr(); }, + [&]() { return usd_instancer.CreateVelocitiesAttr(); }, + collection_instance_object_count_map, + timecode); + } + if (usd_instancer.GetAngularVelocitiesAttr().HasAuthoredValue()) { + ExpandAttributePerInstance( + [&]() { 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 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( + [&]() { return primvar; }, create, collection_instance_object_count_map, timecode); + } + else if (type == pxr::SdfValueTypeNames->IntArray) { + ExpandAttributePerInstance( + [&]() { return primvar; }, create, collection_instance_object_count_map, timecode); + } + else if (type == pxr::SdfValueTypeNames->UCharArray) { + ExpandAttributePerInstance( + [&]() { return primvar; }, create, collection_instance_object_count_map, timecode); + } + else if (type == pxr::SdfValueTypeNames->Float2Array) { + ExpandAttributePerInstance( + [&]() { return primvar; }, create, collection_instance_object_count_map, timecode); + } + else if (type == pxr::SdfValueTypeNames->Float3Array || + type == pxr::SdfValueTypeNames->Color3fArray || + type == pxr::SdfValueTypeNames->Color4fArray) + { + ExpandAttributePerInstance( + [&]() { return primvar; }, create, collection_instance_object_count_map, timecode); + } + else if (type == pxr::SdfValueTypeNames->QuatfArray) { + ExpandAttributePerInstance( + [&]() { return primvar; }, create, collection_instance_object_count_map, timecode); + } + else if (type == pxr::SdfValueTypeNames->BoolArray) { + ExpandAttributePerInstance( + [&]() { return primvar; }, create, collection_instance_object_count_map, timecode); + } + else if (type == pxr::SdfValueTypeNames->StringArray) { + ExpandAttributePerInstance( + [&]() { 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 index; + for (int i = 0; i < prototype_paths_.size(); i++) { + std::vector current_proto_index(instance_num, i); + index.insert(index.end(), current_proto_index.begin(), current_proto_index.end()); + } + + proto_indices_attr.Set(pxr::VtArray(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 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.data() issues + std::vector mask_values(attribute.size()); + attribute.materialize(IndexMask(attribute.size()), mask_values.data()); + + pxr::VtArray ids; + pxr::VtArray invisibleIds; + ids.reserve(mask_values.size()); + + for (int64_t i = 0; i < static_cast(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 diff --git a/source/blender/io/usd/intern/usd_writer_pointinstancer.hh b/source/blender/io/usd/intern/usd_writer_pointinstancer.hh new file mode 100644 index 00000000000..c56b6a05d15 --- /dev/null +++ b/source/blender/io/usd/intern/usd_writer_pointinstancer.hh @@ -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 +#include +#include + +struct USDExporterContext; + +namespace blender::io::usd { + +class USDPointInstancerWriter final : public USDAbstractWriter { + public: + USDPointInstancerWriter(const USDExporterContext &ctx, + std::set> &prototype_paths, + std::unique_ptr base_writer); + ~USDPointInstancerWriter() final = default; + + protected: + virtual void do_write(HierarchyContext &context) override; + + private: + std::unique_ptr base_writer_; + std::set> 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 &proto_index_map, + std::map &final_proto_index_map, + std::map &proto_path_map, + pxr::UsdStageRefPtr stage, + pxr::VtArray &proto_indices, + std::vector> &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> &collection_instance_object_count_map); +}; + +} // namespace blender::io::usd diff --git a/source/blender/io/usd/intern/usd_writer_transform.cc b/source/blender/io/usd/intern/usd_writer_transform.cc index cb8f0bb30fe..4e22a3e9bbc 100644 --- a/source/blender/io/usd/intern/usd_writer_transform.cc +++ b/source/blender/io/usd/intern/usd_writer_transform.cc @@ -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}, diff --git a/tests/files/usd/usd_point_instancer_collection_ref.blend b/tests/files/usd/usd_point_instancer_collection_ref.blend new file mode 100644 index 00000000000..3b5a0b38909 --- /dev/null +++ b/tests/files/usd/usd_point_instancer_collection_ref.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a620e2716e36c39147e5e9cc76e01d1fcb55fcf76d20c3aa43482e35c678789 +size 600667 diff --git a/tests/files/usd/usd_point_instancer_nested.blend b/tests/files/usd/usd_point_instancer_nested.blend new file mode 100644 index 00000000000..9e669da71d5 --- /dev/null +++ b/tests/files/usd/usd_point_instancer_nested.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67bf475410db1d173ba4aa176326fddf0538cc6026fbd97368761062783682f1 +size 107645 diff --git a/tests/files/usd/usd_point_instancer_object_ref.blend b/tests/files/usd/usd_point_instancer_object_ref.blend new file mode 100644 index 00000000000..ba6ee3d1f0c --- /dev/null +++ b/tests/files/usd/usd_point_instancer_object_ref.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23d78bb171113ac8135a3fd855aa1c3ddb3ce235f87c636658d119f6bcd730e2 +size 529455 diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 84eeb455290..ccda6f3760c 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -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 = {}