FBX IO: Add attributes utility functions #104645

Merged
Bastien Montagne merged 1 commits from Mysteryem/blender-addons:attribute_access_base_pr into main 2023-06-26 13:01:26 +02:00
3 changed files with 155 additions and 0 deletions

View File

@ -49,6 +49,9 @@ from .fbx_utils import (
units_blender_to_fbx_factor, units_convertor, units_convertor_iter, units_blender_to_fbx_factor, units_convertor, units_convertor_iter,
matrix4_to_array, similar_values, shape_difference_exclude_similar, astype_view_signedness, fast_first_axis_unique, matrix4_to_array, similar_values, shape_difference_exclude_similar, astype_view_signedness, fast_first_axis_unique,
fast_first_axis_flat, fast_first_axis_flat,
# Attribute helpers.
MESH_ATTRIBUTE_CORNER_EDGE, MESH_ATTRIBUTE_SHARP_EDGE, MESH_ATTRIBUTE_EDGE_VERTS, MESH_ATTRIBUTE_CORNER_VERT,
MESH_ATTRIBUTE_SHARP_FACE, MESH_ATTRIBUTE_POSITION, MESH_ATTRIBUTE_MATERIAL_INDEX,
# Mesh transform helpers. # Mesh transform helpers.
vcos_transformed, nors_transformed, vcos_transformed, nors_transformed,
# UUID from key. # UUID from key.
@ -888,6 +891,8 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION) elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION)
attributes = me.attributes
# Vertex cos. # Vertex cos.
co_bl_dtype = np.single co_bl_dtype = np.single
co_fbx_dtype = np.float64 co_fbx_dtype = np.float64

View File

@ -9,6 +9,8 @@ import time
from collections import namedtuple from collections import namedtuple
from collections.abc import Iterable from collections.abc import Iterable
from itertools import zip_longest, chain from itertools import zip_longest, chain
from dataclasses import dataclass, field
from typing import Callable
import numpy as np import numpy as np
import bpy import bpy
@ -592,6 +594,147 @@ def ensure_object_not_in_edit_mode(context, obj):
return True return True
# ##### Attribute utils. #####
AttributeDataTypeInfo = namedtuple("AttributeDataTypeInfo", ["dtype", "foreach_attribute", "item_size"])
_attribute_data_type_info_lookup = {
'FLOAT': AttributeDataTypeInfo(np.single, "value", 1),
'INT': AttributeDataTypeInfo(np.intc, "value", 1),
'FLOAT_VECTOR': AttributeDataTypeInfo(np.single, "vector", 3),
'FLOAT_COLOR': AttributeDataTypeInfo(np.single, "color", 4), # color_srgb is an alternative
'BYTE_COLOR': AttributeDataTypeInfo(np.single, "color", 4), # color_srgb is an alternative
'STRING': AttributeDataTypeInfo(None, "value", 1), # Not usable with foreach_get/set
'BOOLEAN': AttributeDataTypeInfo(bool, "value", 1),
'FLOAT2': AttributeDataTypeInfo(np.single, "vector", 2),
'INT8': AttributeDataTypeInfo(np.intc, "value", 1),
'INT32_2D': AttributeDataTypeInfo(np.intc, "value", 2),
}
def attribute_get(attributes, name, data_type, domain):
"""Get an attribute by its name, data_type and domain.
Returns None if no attribute with this name, data_type and domain exists."""
attr = attributes.get(name)
if not attr:
return None
if attr.data_type == data_type and attr.domain == domain:
return attr
# It shouldn't normally happen, but it's possible there are multiple attributes with the same name, but different
# data_types or domains.
for attr in attributes:
if attr.name == name and attr.data_type == data_type and attr.domain == domain:
return attr
return None
def attribute_foreach_set(attribute, array_or_list, foreach_attribute=None):
"""Set every value of an attribute with foreach_set."""
if foreach_attribute is None:
foreach_attribute = _attribute_data_type_info_lookup[attribute.data_type].foreach_attribute
attribute.data.foreach_set(foreach_attribute, array_or_list)
def attribute_to_ndarray(attribute, foreach_attribute=None):
"""Create a NumPy ndarray from an attribute."""
data = attribute.data
data_type_info = _attribute_data_type_info_lookup[attribute.data_type]
ndarray = np.empty(len(data) * data_type_info.item_size, dtype=data_type_info.dtype)
if foreach_attribute is None:
foreach_attribute = data_type_info.foreach_attribute
data.foreach_get(foreach_attribute, ndarray)
return ndarray
@dataclass
class AttributeDescription:
"""Helper class to reduce duplicate code for handling built-in Blender attributes."""
name: str
# Valid identifiers can be found in bpy.types.Attribute.bl_rna.properties["data_type"].enum_items
data_type: str
# Valid identifiers can be found in bpy.types.Attribute.bl_rna.properties["domain"].enum_items
domain: str
# Some attributes are required to exist if certain conditions are met. If a required attribute does not exist when
# attempting to get it, an AssertionError is raised.
is_required_check: Callable[[bpy.types.AttributeGroup], bool] = None
# NumPy dtype that matches the internal C data of this attribute.
dtype: np.dtype = field(init=False)
# The default attribute name to use with foreach_get and foreach_set.
foreach_attribute: str = field(init=False)
# The number of elements per value of the attribute when flattened into a 1-dimensional list/array.
item_size: int = field(init=False)
def __post_init__(self):
data_type_info = _attribute_data_type_info_lookup[self.data_type]
self.dtype = data_type_info.dtype
self.foreach_attribute = data_type_info.foreach_attribute
self.item_size = data_type_info.item_size
def is_required(self, attributes):
"""Check if the attribute is required to exist in the provided attributes."""
is_required_check = self.is_required_check
return is_required_check and is_required_check(attributes)
def get(self, attributes):
"""Get the attribute.
If the attribute is required, but does not exist, an AssertionError is raised, otherwise None is returned."""
attr = attribute_get(attributes, self.name, self.data_type, self.domain)
if not attr and self.is_required(attributes):
raise AssertionError("Required attribute '%s' with type '%s' and domain '%s' not found in %r"
% (self.name, self.data_type, self.domain, attributes))
return attr
def ensure(self, attributes):
"""Get the attribute, creating it if it does not exist.
Raises a RuntimeError if the attribute could not be created, which should only happen when attempting to create
an attribute with a reserved name, but with the wrong data_type or domain. See usage of
BuiltinCustomDataLayerProvider in Blender source for most reserved names.
There is no guarantee that the returned attribute has the desired name because the name could already be in use
by another attribute with a different data_type and/or domain."""
attr = self.get(attributes)
if attr:
return attr
attr = attributes.new(self.name, self.data_type, self.domain)
if not attr:
raise RuntimeError("Could not create attribute '%s' with type '%s' and domain '%s' in %r"
% (self.name, self.data_type, self.domain, attributes))
return attr
def foreach_set(self, attributes, array_or_list, foreach_attribute=None):
"""Get the attribute, creating it if it does not exist, and then set every value in the attribute."""
attribute_foreach_set(self.ensure(attributes), array_or_list, foreach_attribute)
def get_ndarray(self, attributes, foreach_attribute=None):
"""Get the attribute and if it exists, return a NumPy ndarray containing its data, otherwise return None."""
attr = self.get(attributes)
return attribute_to_ndarray(attr, foreach_attribute) if attr else None
def to_ndarray(self, attributes, foreach_attribute=None):
"""Get the attribute and if it exists, return a NumPy ndarray containing its data, otherwise return a
zero-length ndarray."""
ndarray = self.get_ndarray(attributes, foreach_attribute)
return ndarray if ndarray is not None else np.empty(0, dtype=self.dtype)
# Built-in Blender attributes
# Only attributes used by the importer/exporter are included here.
# See usage of BuiltinCustomDataLayerProvider in Blender source to find most built-in attributes.
MESH_ATTRIBUTE_MATERIAL_INDEX = AttributeDescription("material_index", 'INT', 'FACE')
MESH_ATTRIBUTE_POSITION = AttributeDescription("position", 'FLOAT_VECTOR', 'POINT',
is_required_check=lambda attributes: bool(attributes.id_data.vertices))
MESH_ATTRIBUTE_SHARP_EDGE = AttributeDescription("sharp_edge", 'BOOLEAN', 'EDGE')
MESH_ATTRIBUTE_EDGE_VERTS = AttributeDescription(".edge_verts", 'INT32_2D', 'EDGE',
is_required_check=lambda attributes: bool(attributes.id_data.edges))
MESH_ATTRIBUTE_CORNER_VERT = AttributeDescription(".corner_vert", 'INT', 'CORNER',
is_required_check=lambda attributes: bool(attributes.id_data.loops))
MESH_ATTRIBUTE_CORNER_EDGE = AttributeDescription(".corner_edge", 'INT', 'CORNER',
is_required_check=lambda attributes: bool(attributes.id_data.loops))
MESH_ATTRIBUTE_SHARP_FACE = AttributeDescription("sharp_face", 'BOOLEAN', 'FACE')
# ##### UIDs code. ##### # ##### UIDs code. #####
# ID class (mere int). # ID class (mere int).

View File

@ -41,6 +41,12 @@ from .fbx_utils import (
nors_transformed, nors_transformed,
parray_as_ndarray, parray_as_ndarray,
astype_view_signedness, astype_view_signedness,
MESH_ATTRIBUTE_MATERIAL_INDEX,
MESH_ATTRIBUTE_POSITION,
MESH_ATTRIBUTE_EDGE_VERTS,
MESH_ATTRIBUTE_CORNER_VERT,
MESH_ATTRIBUTE_SHARP_FACE,
MESH_ATTRIBUTE_SHARP_EDGE,
) )
# global singleton, assign on execution # global singleton, assign on execution
@ -1458,6 +1464,7 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings):
tot_edges = len(fbx_edges) tot_edges = len(fbx_edges)
mesh = bpy.data.meshes.new(name=elem_name_utf8) mesh = bpy.data.meshes.new(name=elem_name_utf8)
attributes = mesh.attributes
if tot_verts: if tot_verts:
if geom_mat_co is not None: if geom_mat_co is not None: