Alembic: remember default and active color attributes

Prior to version 3.5, when importing an Alembic file the default and
active color attributes were set to the first color layer. Since then,
as the default and active color attributes are stored as strings on the
Mesh object, importing an Alembic file leaves those uninitialized.

Note that this only worked prior to 3.5 as the indices for the active
and default layers were initialized by default to zero.

Remembering those default and active attributes is necessary after
an import for e.g. being able to visualize the colors using viewport
shading overlays.

As Alembic does not have a concept of default or active color
attribute, this writes the name of those attributes in the mesh user
properties during export.

During import, we lookup those properties and use them to set the
default and active color layers. If the properties do not exist, we
default to the first color layer.

This adds a test for the new export and import of the default and
active names, and updates old tests to check that the fallback to the
first layer works.

Pull Request: https://projects.blender.org/blender/blender/pulls/164075
This commit is contained in:
Kévin Dietrich 2026-09-21 21:11:36 +02:00 • committed by Kévin Dietrich
parent c6447e69c6
commit cc44ebbd24
7 changed files with 138 additions and 2 deletions

View file

@ -51,6 +51,7 @@ using Alembic::AbcGeom::OInt32Property;
using Alembic::AbcGeom::ON3fGeomParam;
using Alembic::AbcGeom::OPolyMesh;
using Alembic::AbcGeom::OPolyMeshSchema;
using Alembic::AbcGeom::OStringProperty;
using Alembic::AbcGeom::OSubD;
using Alembic::AbcGeom::OSubDSchema;
using Alembic::AbcGeom::OV2fGeomParam;
@ -137,10 +138,23 @@ void ABCGenericMeshWriter::create_alembic_objects(const HierarchyContext *contex
abc_poly_mesh_ = OPolyMesh(args_.abc_parent, args_.abc_name, timesample_index_);
abc_poly_mesh_schema_ = abc_poly_mesh_.getSchema();
OCompoundProperty typeContainer = abc_poly_mesh_.getSchema().getUserProperties();
OBoolProperty type(typeContainer, "meshtype");
abc_custom_data_container_ = abc_poly_mesh_.getSchema().getUserProperties();
OBoolProperty type(abc_custom_data_container_, "meshtype");
type.set(subsurf_modifier_ == nullptr);
}
if (context->object->data->id_type() == ID_ME) {
Mesh *mesh = id_cast<Mesh *>(context->object->data);
if (mesh->active_color_attribute && mesh->default_color_attribute) {
OStringProperty active_color_attribute(abc_custom_data_container_,
ABC_ACTIVE_COLOR_ATTRIBUTE_PROPNAME);
active_color_attribute.set(mesh->active_color_attribute);
OStringProperty default_color_attribute(abc_custom_data_container_,
ABC_DEFAULT_COLOR_ATTRIBUTE_PROPNAME);
default_color_attribute.set(mesh->default_color_attribute);
}
}
}
Alembic::Abc::OObject ABCGenericMeshWriter::get_alembic_object() const

View file

@ -559,5 +559,70 @@ AbcUvScope get_uv_scope(const Alembic::AbcGeom::GeometryScope scope,
return ABC_UV_SCOPE_NONE;
}
static std::string get_string_property(const ICompoundProperty &prop,
const StringRefNull name,
const Alembic::AbcGeom::ISampleSelector &sample_sel)
{
if (!prop.valid()) {
return "";
}
const PropertyHeader *header = prop.getPropertyHeader(name);
if (header && header->isScalar() && Alembic::AbcGeom::IStringProperty::matches(*header)) {
Alembic::AbcGeom::IStringProperty active_color_attribute(prop, header->getName());
return active_color_attribute.getValue(sample_sel);
}
return "";
}
void read_active_and_default_color_attributes(Mesh &mesh,
const ICompoundProperty &user_props,
const Alembic::AbcGeom::ISampleSelector &sample_sel)
{
std::string active_color_attribute = get_string_property(
user_props, ABC_ACTIVE_COLOR_ATTRIBUTE_PROPNAME, sample_sel);
std::string default_color_attribute = get_string_property(
user_props, ABC_DEFAULT_COLOR_ATTRIBUTE_PROPNAME, sample_sel);
StringRefNull first_color_attribute;
StringRefNull final_active_color_attribute;
StringRefNull final_default_color_attribute;
mesh.attributes().foreach_attribute([&](const bke::AttributeIter &iter) {
if (iter.domain != bke::AttrDomain::Corner && iter.domain != bke::AttrDomain::Point) {
return;
}
if (iter.data_type != bke::AttrType::ColorByte && iter.data_type != bke::AttrType::ColorFloat)
{
return;
}
if (first_color_attribute.is_empty()) {
first_color_attribute = iter.name;
}
if (iter.name == active_color_attribute) {
final_active_color_attribute = active_color_attribute;
}
if (iter.name == default_color_attribute) {
final_default_color_attribute = default_color_attribute;
}
});
if (final_active_color_attribute.is_empty()) {
final_active_color_attribute = first_color_attribute;
}
if (final_default_color_attribute.is_empty()) {
final_default_color_attribute = first_color_attribute;
}
if (!final_default_color_attribute.is_empty()) {
BKE_id_attributes_default_color_set(&mesh.id, final_default_color_attribute);
}
if (!final_active_color_attribute.is_empty()) {
BKE_id_attributes_active_color_set(&mesh.id, final_active_color_attribute);
}
}
} // namespace io::alembic
} // namespace blender

View file

@ -123,5 +123,14 @@ AbcUvScope get_uv_scope(const Alembic::AbcGeom::GeometryScope scope,
const CDStreamConfig &config,
const Alembic::AbcGeom::UInt32ArraySamplePtr &indices);
/**
* Sets the active and default color attribute names on the mesh. Defaults to the first color
* attribute layer if the #user_props do not have the Blender-specific properties which are set
* by the exporter.
*/
void read_active_and_default_color_attributes(Mesh &mesh,
const ICompoundProperty &user_props,
const Alembic::AbcGeom::ISampleSelector &sample_sel);
} // namespace io::alembic
} // namespace blender

View file

@ -811,6 +811,9 @@ void AbcMeshReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelec
if (m_settings->always_add_cache_reader || has_animations(m_schema, m_settings)) {
addCacheModifier();
}
ICompoundProperty user_props = m_schema.getUserProperties();
read_active_and_default_color_attributes(*mesh, user_props, sample_sel);
}
bool AbcMeshReader::accepts_object_type(
@ -1153,6 +1156,9 @@ void AbcSubDReader::readObjectData(Main *bmain, const Alembic::Abc::ISampleSelec
}
add_subdiv_modifier();
ICompoundProperty user_props = m_schema.getUserProperties();
read_active_and_default_color_attributes(*mesh, user_props, sample_sel);
}
bool AbcSubDReader::topology_changed(const Mesh *existing_mesh, const ISampleSelector &sample_sel)

View file

@ -36,6 +36,10 @@ class AbcObjectReader;
struct AbcReaderConstructorArgs;
static inline const std::string ABC_CURVE_RESOLUTION_U_PROPNAME = "blender:resolution";
static inline const std::string ABC_ACTIVE_COLOR_ATTRIBUTE_PROPNAME =
"blender:active_color_attribute";
static inline const std::string ABC_DEFAULT_COLOR_ATTRIBUTE_PROPNAME =
"blender:default_color_attribute";
std::string get_valid_abc_name(const char *name);

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e8c4f4e4ca4559330562bc6283cb14e4b1985935cb516b40d04545e37208217
size 98444

View file

@ -220,6 +220,10 @@ class VertexColourImportTest(AbstractAlembicTest):
self.assertAlmostEqualFloatArray(layer.data[98].color, (0.9019607, 0.4745098, 0.2666666, 1.0))
self.assertAlmostEqualFloatArray(layer.data[99].color, (0.8941176, 0.4705882, 0.2627451, 1.0))
mesh = ob.data
self.assertEqual(mesh.color_attributes.active_color_name, 'Cf')
self.assertEqual(mesh.color_attributes.default_color_name, 'Cf')
def test_import_from_blender(self):
# Blender saved per-vertex, and as RGBA.
res = bpy.ops.wm.alembic_import(
@ -235,6 +239,37 @@ class VertexColourImportTest(AbstractAlembicTest):
self.assertAlmostEqualFloatArray(layer.data[98].color, (0.0941176, 0.1215686, 0.9137254, 1.0))
self.assertAlmostEqualFloatArray(layer.data[99].color, (0.1294117, 0.3529411, 0.7529411, 1.0))
mesh = ob.data
self.assertEqual(mesh.color_attributes.active_color_name, 'Cf')
self.assertEqual(mesh.color_attributes.default_color_name, 'Cf')
class VertexColourImportExportTest(AbstractAlembicTest):
def setUp(self):
self._tempdir = tempfile.TemporaryDirectory()
self.tempdir = pathlib.Path(self._tempdir.name)
def test_export_import_default_active(self):
"""Test export and import of the default and active color attribute names"""
abc_path = str(self.tempdir / "vertex-colours-default-active.abc")
# Export
bpy.ops.wm.open_mainfile(filepath=str(args.testdir / "vertex-colours-default-active.blend"))
self.assertIn('FINISHED', bpy.ops.wm.alembic_export(
filepath=abc_path,
vcolors=True
))
# Import back
res = bpy.ops.wm.alembic_import(filepath=abc_path, as_background_job=False)
self.assertEqual({'FINISHED'}, res)
ob = bpy.context.active_object
mesh = ob.data
self.assertEqual(mesh.color_attributes.active_color_name, 'Active')
self.assertEqual(mesh.color_attributes.default_color_name, 'Default')
class CameraExportImportTest(unittest.TestCase):
names = [