Geometry Nodes: refactor instances component

The main goal of this refactor is to not store Object/Collection
pointers for every individual instance. Instead instances now
store a handle for the referenced data. The actual Object/Collection
pointers are stored in a new `InstanceReference` class.

This refactor also allows for some better optimizations further down
the line, because one does not have to search through all instances
anymore to find what data is instanced.

Furthermore, this refactor makes it easier to support instancing
`GeometrySet` or any other data that has to be owned by the
`InstancesComponent`.

Differential Revision: https://developer.blender.org/D11125
This commit is contained in:
Jacques Lucke 2021-05-04 10:16:24 +02:00
parent e6bf272abd
commit 4599cea15d
Notes: blender-bot 2023-02-14 07:30:31 +01:00
Referenced by commit 9569a522f2, Geometry Nodes: Refactor point instance node
11 changed files with 269 additions and 168 deletions

View File

@ -43,19 +43,6 @@ void BKE_geometry_set_free(struct GeometrySet *geometry_set);
bool BKE_geometry_set_has_instances(const struct GeometrySet *geometry_set);
typedef enum InstancedDataType {
INSTANCE_DATA_TYPE_OBJECT = 0,
INSTANCE_DATA_TYPE_COLLECTION = 1,
} InstancedDataType;
typedef struct InstancedData {
InstancedDataType type;
union {
struct Object *object;
struct Collection *collection;
} data;
} InstancedData;
#ifdef __cplusplus
}
#endif

View File

@ -30,6 +30,7 @@
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_user_counter.hh"
#include "BLI_vector_set.hh"
#include "BKE_attribute_access.hh"
#include "BKE_geometry_set.h"
@ -505,12 +506,81 @@ class CurveComponent : public GeometryComponent {
const blender::bke::ComponentAttributeProviders *get_attribute_providers() const final;
};
class InstanceReference {
public:
enum class Type {
/**
* An empty instance. This allows an `InstanceReference` to be default constructed without
* being in an invalid state. There might also be other use cases that we haven't explored much
* yet (such as changing the instance later on, and "disabling" some instances).
*/
None,
Object,
Collection,
};
private:
Type type_ = Type::None;
/** Depending on the type this is either null, an Object or Collection pointer. */
void *data_ = nullptr;
public:
InstanceReference() = default;
InstanceReference(Object &object) : type_(Type::Object), data_(&object)
{
}
InstanceReference(Collection &collection) : type_(Type::Collection), data_(&collection)
{
}
Type type() const
{
return type_;
}
Object &object() const
{
BLI_assert(type_ == Type::Object);
return *(Object *)data_;
}
Collection &collection() const
{
BLI_assert(type_ == Type::Collection);
return *(Collection *)data_;
}
uint64_t hash() const
{
return blender::get_default_hash(data_);
}
friend bool operator==(const InstanceReference &a, const InstanceReference &b)
{
return a.data_ == b.data_;
}
};
/** A geometry component that stores instances. */
class InstancesComponent : public GeometryComponent {
private:
blender::Vector<blender::float4x4> transforms_;
blender::Vector<int> ids_;
blender::Vector<InstancedData> instanced_data_;
/**
* Indexed set containing information about the data that is instanced.
* Actual instances store an index ("handle") into this set.
*/
blender::VectorSet<InstanceReference> references_;
/** Index into `references_`. Determines what data is instanced. */
blender::Vector<int> instance_reference_handles_;
/** Transformation of the instances. */
blender::Vector<blender::float4x4> instance_transforms_;
/**
* IDs of the instances. They are used for consistency over multiple frames for things like
* motion blur.
*/
blender::Vector<int> instance_ids_;
/* These almost unique ids are generated based on `ids_`, which might not contain unique ids at
* all. They are *almost* unique, because under certain very unlikely circumstances, they are not
@ -525,14 +595,20 @@ class InstancesComponent : public GeometryComponent {
GeometryComponent *copy() const override;
void clear();
void add_instance(Object *object, blender::float4x4 transform, const int id = -1);
void add_instance(Collection *collection, blender::float4x4 transform, const int id = -1);
void add_instance(InstancedData data, blender::float4x4 transform, const int id = -1);
blender::Span<InstancedData> instanced_data() const;
blender::Span<blender::float4x4> transforms() const;
blender::Span<int> ids() const;
blender::MutableSpan<blender::float4x4> transforms();
void reserve(int min_capacity);
int add_reference(InstanceReference reference);
void add_instance(int instance_handle, const blender::float4x4 &transform, const int id = -1);
blender::Span<InstanceReference> references() const;
blender::Span<int> instance_reference_handles() const;
blender::MutableSpan<blender::float4x4> instance_transforms();
blender::Span<blender::float4x4> instance_transforms() const;
blender::MutableSpan<int> instance_ids();
blender::Span<int> instance_ids() const;
int instances_amount() const;
blender::Span<int> almost_unique_ids() const;

View File

@ -42,72 +42,86 @@ InstancesComponent::InstancesComponent() : GeometryComponent(GEO_COMPONENT_TYPE_
GeometryComponent *InstancesComponent::copy() const
{
InstancesComponent *new_component = new InstancesComponent();
new_component->transforms_ = transforms_;
new_component->instanced_data_ = instanced_data_;
new_component->ids_ = ids_;
new_component->instance_reference_handles_ = instance_reference_handles_;
new_component->instance_transforms_ = instance_transforms_;
new_component->instance_ids_ = instance_ids_;
new_component->references_ = references_;
return new_component;
}
void InstancesComponent::reserve(int min_capacity)
{
instance_reference_handles_.reserve(min_capacity);
instance_transforms_.reserve(min_capacity);
instance_ids_.reserve(min_capacity);
}
void InstancesComponent::clear()
{
instanced_data_.clear();
transforms_.clear();
ids_.clear();
instance_reference_handles_.clear();
instance_transforms_.clear();
instance_ids_.clear();
references_.clear();
}
void InstancesComponent::add_instance(Object *object, float4x4 transform, const int id)
void InstancesComponent::add_instance(const int instance_handle,
const float4x4 &transform,
const int id)
{
InstancedData data;
data.type = INSTANCE_DATA_TYPE_OBJECT;
data.data.object = object;
this->add_instance(data, transform, id);
BLI_assert(instance_handle >= 0);
BLI_assert(instance_handle < references_.size());
instance_reference_handles_.append(instance_handle);
instance_transforms_.append(transform);
instance_ids_.append(id);
}
void InstancesComponent::add_instance(Collection *collection, float4x4 transform, const int id)
blender::Span<int> InstancesComponent::instance_reference_handles() const
{
InstancedData data;
data.type = INSTANCE_DATA_TYPE_COLLECTION;
data.data.collection = collection;
this->add_instance(data, transform, id);
return instance_reference_handles_;
}
void InstancesComponent::add_instance(InstancedData data, float4x4 transform, const int id)
blender::MutableSpan<blender::float4x4> InstancesComponent::instance_transforms()
{
instanced_data_.append(data);
transforms_.append(transform);
ids_.append(id);
return instance_transforms_;
}
blender::Span<blender::float4x4> InstancesComponent::instance_transforms() const
{
return instance_transforms_;
}
Span<InstancedData> InstancesComponent::instanced_data() const
blender::MutableSpan<int> InstancesComponent::instance_ids()
{
return instanced_data_;
return instance_ids_;
}
blender::Span<int> InstancesComponent::instance_ids() const
{
return instance_ids_;
}
Span<float4x4> InstancesComponent::transforms() const
/**
* Returns a handle for the given reference.
* If the reference exists already, the handle of the existing reference is returned.
* Otherwise a new handle is added.
*/
int InstancesComponent::add_reference(InstanceReference reference)
{
return transforms_;
return references_.index_of_or_add_as(reference);
}
Span<int> InstancesComponent::ids() const
blender::Span<InstanceReference> InstancesComponent::references() const
{
return ids_;
}
MutableSpan<float4x4> InstancesComponent::transforms()
{
return transforms_;
return references_;
}
int InstancesComponent::instances_amount() const
{
const int size = instanced_data_.size();
BLI_assert(transforms_.size() == size);
return size;
return instance_transforms_.size();
}
bool InstancesComponent::is_empty() const
{
return transforms_.size() == 0;
return this->instance_reference_handles_.size() == 0;
}
bool InstancesComponent::owns_direct_data() const
@ -178,8 +192,8 @@ static blender::Array<int> generate_unique_instance_ids(Span<int> original_ids)
blender::Span<int> InstancesComponent::almost_unique_ids() const
{
std::lock_guard lock(almost_unique_ids_mutex_);
if (almost_unique_ids_.size() != ids_.size()) {
almost_unique_ids_ = generate_unique_instance_ids(ids_);
if (almost_unique_ids_.size() != instance_ids_.size()) {
almost_unique_ids_ = generate_unique_instance_ids(instance_ids_);
}
return almost_unique_ids_;
}

View File

@ -149,21 +149,28 @@ static void geometry_set_collect_recursive(const GeometrySet &geometry_set,
const InstancesComponent &instances_component =
*geometry_set.get_component_for_read<InstancesComponent>();
Span<float4x4> transforms = instances_component.transforms();
Span<InstancedData> instances = instances_component.instanced_data();
for (const int i : instances.index_range()) {
const InstancedData &data = instances[i];
Span<float4x4> transforms = instances_component.instance_transforms();
Span<int> handles = instances_component.instance_reference_handles();
Span<InstanceReference> references = instances_component.references();
for (const int i : transforms.index_range()) {
const InstanceReference &reference = references[handles[i]];
const float4x4 instance_transform = transform * transforms[i];
if (data.type == INSTANCE_DATA_TYPE_OBJECT) {
BLI_assert(data.data.object != nullptr);
const Object &object = *data.data.object;
geometry_set_collect_recursive_object(object, instance_transform, r_sets);
}
else if (data.type == INSTANCE_DATA_TYPE_COLLECTION) {
BLI_assert(data.data.collection != nullptr);
const Collection &collection = *data.data.collection;
geometry_set_collect_recursive_collection_instance(collection, instance_transform, r_sets);
switch (reference.type()) {
case InstanceReference::Type::Object: {
Object &object = reference.object();
geometry_set_collect_recursive_object(object, instance_transform, r_sets);
break;
}
case InstanceReference::Type::Collection: {
Collection &collection = reference.collection();
geometry_set_collect_recursive_collection_instance(
collection, instance_transform, r_sets);
break;
}
case InstanceReference::Type::None: {
break;
}
}
}
}
@ -267,19 +274,24 @@ static bool instances_attribute_foreach_recursive(const GeometrySet &geometry_se
return true;
}
for (const InstancedData &data : instances_component->instanced_data()) {
if (data.type == INSTANCE_DATA_TYPE_OBJECT) {
BLI_assert(data.data.object != nullptr);
const Object &object = *data.data.object;
if (!object_instance_attribute_foreach(object, callback, limit, count)) {
return false;
for (const InstanceReference &reference : instances_component->references()) {
switch (reference.type()) {
case InstanceReference::Type::Object: {
const Object &object = reference.object();
if (!object_instance_attribute_foreach(object, callback, limit, count)) {
return false;
}
break;
}
}
else if (data.type == INSTANCE_DATA_TYPE_COLLECTION) {
BLI_assert(data.data.collection != nullptr);
const Collection &collection = *data.data.collection;
if (!collection_instance_attribute_foreach(collection, callback, limit, count)) {
return false;
case InstanceReference::Type::Collection: {
const Collection &collection = reference.collection();
if (!collection_instance_attribute_foreach(collection, callback, limit, count)) {
return false;
}
break;
}
case InstanceReference::Type::None: {
break;
}
}
}

View File

@ -842,38 +842,38 @@ static void make_duplis_instances_component(const DupliContext *ctx)
return;
}
Span<float4x4> instance_offset_matrices = component->transforms();
Span<float4x4> instance_offset_matrices = component->instance_transforms();
Span<int> instance_reference_handles = component->instance_reference_handles();
Span<int> almost_unique_ids = component->almost_unique_ids();
Span<InstancedData> instanced_data = component->instanced_data();
Span<InstanceReference> references = component->references();
for (int i = 0; i < component->instances_amount(); i++) {
const InstancedData &data = instanced_data[i];
for (int64_t i : instance_offset_matrices.index_range()) {
const InstanceReference &reference = references[instance_reference_handles[i]];
const int id = almost_unique_ids[i];
if (data.type == INSTANCE_DATA_TYPE_OBJECT) {
Object *object = data.data.object;
if (object != nullptr) {
switch (reference.type()) {
case InstanceReference::Type::Object: {
Object &object = reference.object();
float matrix[4][4];
mul_m4_m4m4(matrix, ctx->object->obmat, instance_offset_matrices[i].values);
make_dupli(ctx, object, matrix, id);
make_dupli(ctx, &object, matrix, id);
float space_matrix[4][4];
mul_m4_m4m4(space_matrix, instance_offset_matrices[i].values, object->imat);
mul_m4_m4m4(space_matrix, instance_offset_matrices[i].values, object.imat);
mul_m4_m4_pre(space_matrix, ctx->object->obmat);
make_recursive_duplis(ctx, object, space_matrix, id);
make_recursive_duplis(ctx, &object, space_matrix, id);
break;
}
}
else if (data.type == INSTANCE_DATA_TYPE_COLLECTION) {
Collection *collection = data.data.collection;
if (collection != nullptr) {
case InstanceReference::Type::Collection: {
Collection &collection = reference.collection();
float collection_matrix[4][4];
unit_m4(collection_matrix);
sub_v3_v3(collection_matrix[3], collection->instance_offset);
sub_v3_v3(collection_matrix[3], collection.instance_offset);
mul_m4_m4_pre(collection_matrix, instance_offset_matrices[i].values);
mul_m4_m4_pre(collection_matrix, ctx->object->obmat);
eEvaluationMode mode = DEG_get_mode(ctx->depsgraph);
FOREACH_COLLECTION_VISIBLE_OBJECT_RECURSIVE_BEGIN (collection, object, mode) {
FOREACH_COLLECTION_VISIBLE_OBJECT_RECURSIVE_BEGIN (&collection, object, mode) {
if (object == ctx->object) {
continue;
}
@ -885,6 +885,10 @@ static void make_duplis_instances_component(const DupliContext *ctx)
make_recursive_duplis(ctx, object, collection_matrix, id);
}
FOREACH_COLLECTION_VISIBLE_OBJECT_RECURSIVE_END;
break;
}
case InstanceReference::Type::None: {
break;
}
}
}

View File

@ -276,25 +276,31 @@ std::unique_ptr<ColumnValues> InstancesDataSource::get_column_values(
const int size = this->tot_rows();
if (STREQ(column_id.name, "Name")) {
Span<InstancedData> instance_data = component_->instanced_data();
Span<int> reference_handles = component_->instance_reference_handles();
Span<InstanceReference> references = component_->references();
std::unique_ptr<ColumnValues> values = column_values_from_function(
"Name", size, [instance_data](int index, CellValue &r_cell_value) {
const InstancedData &data = instance_data[index];
if (data.type == INSTANCE_DATA_TYPE_OBJECT) {
if (data.data.object != nullptr) {
r_cell_value.value_object = ObjectCellValue{data.data.object};
"Name", size, [reference_handles, references](int index, CellValue &r_cell_value) {
const InstanceReference &reference = references[reference_handles[index]];
switch (reference.type()) {
case InstanceReference::Type::Object: {
Object &object = reference.object();
r_cell_value.value_object = ObjectCellValue{&object};
break;
}
}
else if (data.type == INSTANCE_DATA_TYPE_COLLECTION) {
if (data.data.collection != nullptr) {
r_cell_value.value_collection = CollectionCellValue{data.data.collection};
case InstanceReference::Type::Collection: {
Collection &collection = reference.collection();
r_cell_value.value_collection = CollectionCellValue{&collection};
break;
}
case InstanceReference::Type::None: {
break;
}
}
});
values->default_width = 8.0f;
return values;
}
Span<float4x4> transforms = component_->transforms();
Span<float4x4> transforms = component_->instance_transforms();
if (STREQ(column_id.name, "Position")) {
return column_values_from_function(
column_id.name,
@ -322,7 +328,7 @@ std::unique_ptr<ColumnValues> InstancesDataSource::get_column_values(
},
default_float3_column_width);
}
Span<int> ids = component_->ids();
Span<int> ids = component_->instance_ids();
if (STREQ(column_id.name, "ID")) {
/* Make the column a bit wider by default, since the IDs tend to be large numbers. */
return column_values_from_function(

View File

@ -58,10 +58,6 @@ static void geo_node_collection_info_exec(GeoNodeExecParams params)
const bool transform_space_relative = (node_storage->transform_space ==
GEO_NODE_TRANSFORM_SPACE_RELATIVE);
InstancedData instance;
instance.type = INSTANCE_DATA_TYPE_COLLECTION;
instance.data.collection = collection;
InstancesComponent &instances = geometry_set_out.get_component_for_write<InstancesComponent>();
float transform_mat[4][4];
@ -73,7 +69,9 @@ static void geo_node_collection_info_exec(GeoNodeExecParams params)
mul_m4_m4_pre(transform_mat, self_object->imat);
}
instances.add_instance(instance, transform_mat, -1);
const int handle = instances.add_reference(*collection);
instances.add_instance(handle, transform_mat, -1);
params.set_output("Geometry", geometry_set_out);
}

View File

@ -243,13 +243,30 @@ static void join_components(Span<const PointCloudComponent *> src_components, Ge
static void join_components(Span<const InstancesComponent *> src_components, GeometrySet &result)
{
InstancesComponent &dst_component = result.get_component_for_write<InstancesComponent>();
for (const InstancesComponent *component : src_components) {
const int size = component->instances_amount();
Span<InstancedData> instanced_data = component->instanced_data();
Span<float4x4> transforms = component->transforms();
Span<int> ids = component->ids();
for (const int i : IndexRange(size)) {
dst_component.add_instance(instanced_data[i], transforms[i], ids[i]);
int tot_instances = 0;
for (const InstancesComponent *src_component : src_components) {
tot_instances += src_component->instances_amount();
}
dst_component.reserve(tot_instances);
for (const InstancesComponent *src_component : src_components) {
Span<InstanceReference> src_references = src_component->references();
Array<int> handle_map(src_references.size());
for (const int src_handle : src_references.index_range()) {
handle_map[src_handle] = dst_component.add_reference(src_references[src_handle]);
}
Span<float4x4> src_transforms = src_component->instance_transforms();
Span<int> src_ids = src_component->instance_ids();
Span<int> src_reference_handles = src_component->instance_reference_handles();
for (const int i : src_transforms.index_range()) {
const int src_handle = src_reference_handles[i];
const int dst_handle = handle_map[src_handle];
const float4x4 &transform = src_transforms[i];
const int id = src_ids[i];
dst_component.add_instance(dst_handle, transform, id);
}
}
}

View File

@ -73,14 +73,15 @@ static void geo_node_object_info_exec(GeoNodeExecParams params)
if (object != self_object) {
InstancesComponent &instances = geometry_set.get_component_for_write<InstancesComponent>();
const int handle = instances.add_reference(*object);
if (transform_space_relative) {
instances.add_instance(object, transform);
instances.add_instance(handle, transform);
}
else {
float unit_transform[4][4];
unit_m4(unit_transform);
instances.add_instance(object, unit_transform);
instances.add_instance(handle, unit_transform);
}
}
}

View File

@ -65,8 +65,8 @@ static void geo_node_point_instance_update(bNodeTree *UNUSED(tree), bNode *node)
seed_socket, type == GEO_NODE_POINT_INSTANCE_TYPE_COLLECTION && !use_whole_collection);
}
static void get_instanced_data__object(const GeoNodeExecParams &params,
MutableSpan<std::optional<InstancedData>> r_instances_data)
static void get_instance_references__object(const GeoNodeExecParams &params,
MutableSpan<InstanceReference> r_references)
{
bke::PersistentObjectHandle object_handle = params.get_input<bke::PersistentObjectHandle>(
"Object");
@ -75,17 +75,13 @@ static void get_instanced_data__object(const GeoNodeExecParams &params,
object = nullptr;
}
if (object != nullptr) {
InstancedData instance;
instance.type = INSTANCE_DATA_TYPE_OBJECT;
instance.data.object = object;
r_instances_data.fill(instance);
r_references.fill(*object);
}
}
static void get_instanced_data__collection(
const GeoNodeExecParams &params,
const GeometryComponent &component,
MutableSpan<std::optional<InstancedData>> r_instances_data)
static void get_instance_references__collection(const GeoNodeExecParams &params,
const GeometryComponent &component,
MutableSpan<InstanceReference> r_references)
{
const bNode &node = params.node();
NodeGeometryPointInstance *node_storage = (NodeGeometryPointInstance *)node.storage;
@ -106,62 +102,51 @@ static void get_instanced_data__collection(
const bool use_whole_collection = (node_storage->flag &
GEO_NODE_POINT_INSTANCE_WHOLE_COLLECTION) != 0;
if (use_whole_collection) {
InstancedData instance;
instance.type = INSTANCE_DATA_TYPE_COLLECTION;
instance.data.collection = collection;
r_instances_data.fill(instance);
r_references.fill(*collection);
}
else {
Vector<InstancedData> possible_instances;
Vector<InstanceReference> possible_references;
/* Direct child objects are instanced as objects. */
LISTBASE_FOREACH (CollectionObject *, cob, &collection->gobject) {
Object *object = cob->ob;
InstancedData instance;
instance.type = INSTANCE_DATA_TYPE_OBJECT;
instance.data.object = object;
possible_instances.append(instance);
possible_references.append(*cob->ob);
}
/* Direct child collections are instanced as collections. */
LISTBASE_FOREACH (CollectionChild *, child, &collection->children) {
Collection *child_collection = child->collection;
InstancedData instance;
instance.type = INSTANCE_DATA_TYPE_COLLECTION;
instance.data.collection = child_collection;
possible_instances.append(instance);
possible_references.append(*child->collection);
}
if (!possible_instances.is_empty()) {
if (!possible_references.is_empty()) {
const int seed = params.get_input<int>("Seed");
Array<uint32_t> ids = get_geometry_element_ids_as_uints(component, ATTR_DOMAIN_POINT);
for (const int i : r_instances_data.index_range()) {
const int index = BLI_hash_int_2d(ids[i], seed) % possible_instances.size();
r_instances_data[i] = possible_instances[index];
for (const int i : r_references.index_range()) {
const int index = BLI_hash_int_2d(ids[i], seed) % possible_references.size();
r_references[i] = possible_references[index];
}
}
}
}
static Array<std::optional<InstancedData>> get_instanced_data(const GeoNodeExecParams &params,
const GeometryComponent &component,
const int amount)
static Array<InstanceReference> get_instance_references(const GeoNodeExecParams &params,
const GeometryComponent &component,
const int amount)
{
const bNode &node = params.node();
NodeGeometryPointInstance *node_storage = (NodeGeometryPointInstance *)node.storage;
const GeometryNodePointInstanceType type = (GeometryNodePointInstanceType)
node_storage->instance_type;
Array<std::optional<InstancedData>> instances_data(amount);
Array<InstanceReference> references(amount);
switch (type) {
case GEO_NODE_POINT_INSTANCE_TYPE_OBJECT: {
get_instanced_data__object(params, instances_data);
get_instance_references__object(params, references);
break;
}
case GEO_NODE_POINT_INSTANCE_TYPE_COLLECTION: {
get_instanced_data__collection(params, component, instances_data);
get_instance_references__collection(params, component, references);
break;
}
}
return instances_data;
return references;
}
static void add_instances_from_geometry_component(InstancesComponent &instances,
@ -171,8 +156,7 @@ static void add_instances_from_geometry_component(InstancesComponent &instances,
const AttributeDomain domain = ATTR_DOMAIN_POINT;
const int domain_size = src_geometry.attribute_domain_size(domain);
Array<std::optional<InstancedData>> instances_data = get_instanced_data(
params, src_geometry, domain_size);
Array<InstanceReference> references = get_instance_references(params, src_geometry, domain_size);
GVArray_Typed<float3> positions = src_geometry.attribute_get_for_read<float3>(
"position", domain, {0, 0, 0});
@ -183,9 +167,11 @@ static void add_instances_from_geometry_component(InstancesComponent &instances,
GVArray_Typed<int> ids = src_geometry.attribute_get_for_read<int>("id", domain, -1);
for (const int i : IndexRange(domain_size)) {
if (instances_data[i].has_value()) {
const InstanceReference &reference = references[i];
if (reference.type() != InstanceReference::Type::None) {
const float4x4 matrix = float4x4::from_loc_eul_scale(positions[i], rotations[i], scales[i]);
instances.add_instance(*instances_data[i], matrix, ids[i]);
const int handle = instances.add_reference(reference);
instances.add_instance(handle, matrix, ids[i]);
}
}
}

View File

@ -101,7 +101,7 @@ static void transform_instances(InstancesComponent &instances,
const float3 rotation,
const float3 scale)
{
MutableSpan<float4x4> transforms = instances.transforms();
MutableSpan<float4x4> transforms = instances.instance_transforms();
/* Use only translation if rotation and scale don't apply. */
if (use_translate(rotation, scale)) {