FBX IO: Add attributes utility functions #104645
@ -49,6 +49,9 @@ from .fbx_utils import (
|
||||
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,
|
||||
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.
|
||||
vcos_transformed, nors_transformed,
|
||||
# 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)
|
||||
|
||||
attributes = me.attributes
|
||||
|
||||
# Vertex cos.
|
||||
co_bl_dtype = np.single
|
||||
co_fbx_dtype = np.float64
|
||||
|
@ -9,6 +9,8 @@ import time
|
||||
from collections import namedtuple
|
||||
from collections.abc import Iterable
|
||||
from itertools import zip_longest, chain
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable
|
||||
import numpy as np
|
||||
|
||||
import bpy
|
||||
@ -592,6 +594,147 @@ def ensure_object_not_in_edit_mode(context, obj):
|
||||
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. #####
|
||||
|
||||
# ID class (mere int).
|
||||
|
@ -41,6 +41,12 @@ from .fbx_utils import (
|
||||
nors_transformed,
|
||||
parray_as_ndarray,
|
||||
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
|
||||
@ -1458,6 +1464,7 @@ def blen_read_geom(fbx_tmpl, fbx_obj, settings):
|
||||
tot_edges = len(fbx_edges)
|
||||
|
||||
mesh = bpy.data.meshes.new(name=elem_name_utf8)
|
||||
attributes = mesh.attributes
|
||||
|
||||
if tot_verts:
|
||||
if geom_mat_co is not None:
|
||||
|
Loading…
Reference in New Issue
Block a user