Asset Pipeline v2 #145
@ -478,20 +478,14 @@ def transfer_material_slots(target_obj: bpy.types.Object, source_obj):
|
|||||||
# SHAPE KEYS
|
# SHAPE KEYS
|
||||||
|
|
||||||
|
|
||||||
def shape_key_set_active(obj, shape_key_name):
|
def closest_face_to_point(bm_source, p_target, bvh_tree=None):
|
||||||
for index, shape_key in enumerate(obj.data.shape_keys.key_blocks):
|
|
||||||
if shape_key.name == shape_key_name:
|
|
||||||
obj.active_shape_key_index = index
|
|
||||||
|
|
||||||
|
|
||||||
def shape_key_closest_face_to_point(bm_source, p_target, bvh_tree=None):
|
|
||||||
if not bvh_tree:
|
if not bvh_tree:
|
||||||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||||
(loc, norm, index, distance) = bvh_tree.find_nearest(p_target)
|
(loc, norm, index, distance) = bvh_tree.find_nearest(p_target)
|
||||||
return bm_source.faces[index]
|
return bm_source.faces[index]
|
||||||
|
|
||||||
|
|
||||||
def shape_key_tris_per_face(bm_source):
|
def tris_per_face(bm_source):
|
||||||
tris_source = bm_source.calc_loop_triangles()
|
tris_source = bm_source.calc_loop_triangles()
|
||||||
tris_dict = dict()
|
tris_dict = dict()
|
||||||
for face in bm_source.faces:
|
for face in bm_source.faces:
|
||||||
@ -503,7 +497,7 @@ def shape_key_tris_per_face(bm_source):
|
|||||||
return tris_dict
|
return tris_dict
|
||||||
|
|
||||||
|
|
||||||
def shape_key_closest_tri_on_face(tris_dict, face, p):
|
def closest_tri_on_face(tris_dict, face, p):
|
||||||
points = []
|
points = []
|
||||||
dist = []
|
dist = []
|
||||||
tris = []
|
tris = []
|
||||||
@ -520,6 +514,203 @@ def shape_key_closest_tri_on_face(tris_dict, face, p):
|
|||||||
return (tri, point)
|
return (tri, point)
|
||||||
|
|
||||||
|
|
||||||
|
def closest_edge_on_face_to_line(face, p1, p2, skip_edges=None):
|
||||||
|
"""Returns edge of a face which is closest to line."""
|
||||||
|
for edge in face.edges:
|
||||||
|
if skip_edges:
|
||||||
|
if edge in skip_edges:
|
||||||
|
continue
|
||||||
|
res = mathutils.geometry.intersect_line_line(
|
||||||
|
p1, p2, *[edge.verts[i].co for i in range(2)]
|
||||||
|
)
|
||||||
|
if not res:
|
||||||
|
continue
|
||||||
|
(p_traversal, p_edge) = res
|
||||||
|
frac_1 = (edge.verts[1].co - edge.verts[0].co).dot(
|
||||||
|
p_edge - edge.verts[0].co
|
||||||
|
) / (edge.verts[1].co - edge.verts[0].co).length ** 2.0
|
||||||
|
frac_2 = (p2 - p1).dot(p_traversal - p1) / (p2 - p1).length ** 2.0
|
||||||
|
if (frac_1 >= 0 and frac_1 <= 1) and (frac_2 >= 0 and frac_2 <= 1):
|
||||||
|
return edge
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def edge_data_split(edge, data_layer, data_suffix: str):
|
||||||
|
for vert in edge.verts:
|
||||||
|
vals = []
|
||||||
|
for loop in vert.link_loops:
|
||||||
|
loops_edge_vert = set([loop for f in edge.link_faces for loop in f.loops])
|
||||||
|
if loop not in loops_edge_vert:
|
||||||
|
continue
|
||||||
|
dat = data_layer[loop.index]
|
||||||
|
element = list(getattr(dat, data_suffix))
|
||||||
|
if not vals:
|
||||||
|
vals.append(element)
|
||||||
|
elif not vals[0] == element:
|
||||||
|
vals.append(element)
|
||||||
|
if len(vals) > 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_data_from_face(
|
||||||
|
bm_source, tris_dict, face, p, data_layer_source, data_suffix=''
|
||||||
|
):
|
||||||
|
"""Returns interpolated value of a data layer within a face closest to a point."""
|
||||||
|
|
||||||
|
(tri, point) = closest_tri_on_face(tris_dict, face, p)
|
||||||
|
if not tri:
|
||||||
|
return None
|
||||||
|
weights = mathutils.interpolate.poly_3d_calc(
|
||||||
|
[tri[i].vert.co for i in range(3)], point
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data_suffix:
|
||||||
|
cols_weighted = [
|
||||||
|
weights[i] * np.array(data_layer_source[tri[i].index]) for i in range(3)
|
||||||
|
]
|
||||||
|
col = sum(np.array(cols_weighted))
|
||||||
|
else:
|
||||||
|
cols_weighted = [
|
||||||
|
weights[i] * np.array(getattr(data_layer_source[tri[i].index], data_suffix))
|
||||||
|
for i in range(3)
|
||||||
|
]
|
||||||
|
col = sum(np.array(cols_weighted))
|
||||||
|
return col
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_corner_data(
|
||||||
|
obj_source, obj_target, data_layer_source, data_layer_target, data_suffix=''
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Transfers interpolated face corner data from data layer of a source object to data layer of a
|
||||||
|
target object, while approximately preserving data seams (e.g. necessary for UV Maps).
|
||||||
|
The transfer is face interpolated per target corner within the source face that is closest
|
||||||
|
to the target corner point and does not have any data seams on the way back to the
|
||||||
|
source face that is closest to the target face's center.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bm_source = bmesh.new()
|
||||||
|
bm_source.from_mesh(obj_source.data)
|
||||||
|
bm_source.faces.ensure_lookup_table()
|
||||||
|
bm_target = bmesh.new()
|
||||||
|
bm_target.from_mesh(obj_target.data)
|
||||||
|
bm_target.faces.ensure_lookup_table()
|
||||||
|
|
||||||
|
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||||
|
|
||||||
|
tris_dict = tris_per_face(bm_source)
|
||||||
|
|
||||||
|
for face_target in bm_target.faces:
|
||||||
|
face_target_center = face_target.calc_center_median()
|
||||||
|
|
||||||
|
face_source = closest_face_to_point(bm_source, face_target_center, bvh_tree)
|
||||||
|
|
||||||
|
for corner_target in face_target.loops:
|
||||||
|
# find nearest face on target compared to face that loop belongs to
|
||||||
|
p = corner_target.vert.co
|
||||||
|
|
||||||
|
face_source_closest = closest_face_to_point(bm_source, p, bvh_tree)
|
||||||
|
enclosed = face_source_closest is face_source
|
||||||
|
face_source_int = face_source
|
||||||
|
if not enclosed:
|
||||||
|
# traverse faces between point and face center
|
||||||
|
traversed_faces = set()
|
||||||
|
traversed_edges = set()
|
||||||
|
while face_source_int is not face_source_closest:
|
||||||
|
traversed_faces.add(face_source_int)
|
||||||
|
edge = closest_edge_on_face_to_line(
|
||||||
|
face_source_int,
|
||||||
|
face_target_center,
|
||||||
|
p,
|
||||||
|
skip_edges=traversed_edges,
|
||||||
|
)
|
||||||
|
if edge == None:
|
||||||
|
break
|
||||||
|
if len(edge.link_faces) != 2:
|
||||||
|
break
|
||||||
|
traversed_edges.add(edge)
|
||||||
|
|
||||||
|
split = edge_data_split(edge, data_layer_source, data_suffix)
|
||||||
|
if split:
|
||||||
|
break
|
||||||
|
|
||||||
|
# set new source face to other face belonging to edge
|
||||||
|
face_source_int = (
|
||||||
|
edge.link_faces[1]
|
||||||
|
if edge.link_faces[1] is not face_source_int
|
||||||
|
else edge.link_faces[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
# avoid looping behaviour
|
||||||
|
if face_source_int in traversed_faces:
|
||||||
|
face_source_int = face_source
|
||||||
|
break
|
||||||
|
|
||||||
|
# interpolate data from selected face
|
||||||
|
col = interpolate_data_from_face(
|
||||||
|
bm_source, tris_dict, face_source_int, p, data_layer_source, data_suffix
|
||||||
|
)
|
||||||
|
if col is None:
|
||||||
|
continue
|
||||||
|
if not data_suffix:
|
||||||
|
data_layer_target.data[corner_target.index] = col
|
||||||
|
else:
|
||||||
|
setattr(data_layer_target[corner_target.index], data_suffix, list(col))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def is_mesh_identical(mesh_a, mesh_b) -> bool:
|
||||||
|
if len(mesh_a.vertices) != len(mesh_b.vertices):
|
||||||
|
return False
|
||||||
|
if len(mesh_a.edges) != len(mesh_b.edges):
|
||||||
|
return False
|
||||||
|
if len(mesh_a.polygons) != len(mesh_b.polygons):
|
||||||
|
return False
|
||||||
|
for e1, e2 in zip(mesh_a.edges, mesh_b.edges):
|
||||||
|
for v1, v2 in zip(e1.vertices, e2.vertices):
|
||||||
|
if v1 != v2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_curve_identical(curve_a: bpy.types.Curve, curve_b: bpy.types.Curve) -> bool:
|
||||||
|
if len(curve_a.splines) != len(curve_b.splines):
|
||||||
|
return False
|
||||||
|
for spline1, spline2 in zip(curve_a.splines, curve_b.splines):
|
||||||
|
if len(spline1.points) != len(spline2.points):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_obdata_identical(
|
||||||
|
a: bpy.types.Object or bpy.types.Mesh, b: bpy.types.Object or bpy.types.Mesh
|
||||||
|
) -> bool:
|
||||||
|
"""Checks if two objects have matching topology (efficiency over exactness)"""
|
||||||
|
if type(a) == bpy.types.Object:
|
||||||
|
a = a.data
|
||||||
|
if type(b) == bpy.types.Object:
|
||||||
|
b = b.data
|
||||||
|
|
||||||
|
if type(a) != type(b):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if type(a) == bpy.types.Mesh:
|
||||||
|
return is_mesh_identical(a, b)
|
||||||
|
elif type(a) == bpy.types.Curve:
|
||||||
|
return is_curve_identical(a, b)
|
||||||
|
else:
|
||||||
|
# TODO: Support geometry types other than mesh or curve.
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def shape_key_set_active(obj, shape_key_name):
|
||||||
|
for index, shape_key in enumerate(obj.data.shape_keys.key_blocks):
|
||||||
|
if shape_key.name == shape_key_name:
|
||||||
|
obj.active_shape_key_index = index
|
||||||
|
|
||||||
|
|
||||||
def shape_keys_clean(obj):
|
def shape_keys_clean(obj):
|
||||||
if obj.type != "MESH" or obj.data.shape_keys is None:
|
if obj.type != "MESH" or obj.data.shape_keys is None:
|
||||||
return
|
return
|
||||||
@ -618,12 +809,12 @@ def transfer_shape_key(
|
|||||||
bm_source.faces.ensure_lookup_table()
|
bm_source.faces.ensure_lookup_table()
|
||||||
|
|
||||||
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||||
tris_dict = shape_key_tris_per_face(bm_source)
|
tris_dict = tris_per_face(bm_source)
|
||||||
for i, vert in enumerate(target_obj.data.vertices):
|
for i, vert in enumerate(target_obj.data.vertices):
|
||||||
p = vert.co
|
p = vert.co
|
||||||
face = shape_key_closest_face_to_point(bm_source, p, bvh_tree)
|
face = closest_face_to_point(bm_source, p, bvh_tree)
|
||||||
|
|
||||||
(tri, point) = shape_key_closest_tri_on_face(tris_dict, face, p)
|
(tri, point) = closest_tri_on_face(tris_dict, face, p)
|
||||||
if not tri:
|
if not tri:
|
||||||
continue
|
continue
|
||||||
weights = mathutils.interpolate.poly_3d_calc(
|
weights = mathutils.interpolate.poly_3d_calc(
|
||||||
@ -730,8 +921,8 @@ def transfer_attribute(
|
|||||||
source_attributes = source_obj.data.attributes
|
source_attributes = source_obj.data.attributes
|
||||||
target_attributes = target_obj.data.attributes
|
target_attributes = target_obj.data.attributes
|
||||||
source_attribute = source_attributes.get(attribute_name)
|
source_attribute = source_attributes.get(attribute_name)
|
||||||
|
|
||||||
target_attribute = target_attributes.get(attribute_name)
|
target_attribute = target_attributes.get(attribute_name)
|
||||||
|
|
||||||
if target_attribute:
|
if target_attribute:
|
||||||
target_attributes.remove(target_attribute)
|
target_attributes.remove(target_attribute)
|
||||||
|
|
||||||
@ -740,7 +931,13 @@ def transfer_attribute(
|
|||||||
type=source_attribute.data_type,
|
type=source_attribute.data_type,
|
||||||
domain=source_attribute.domain,
|
domain=source_attribute.domain,
|
||||||
)
|
)
|
||||||
# print(f"Transfering Attribute {attribute_name}")
|
|
||||||
|
if not is_obdata_identical(source_obj, target_obj):
|
||||||
|
proximity_transfer_single_attribute(
|
||||||
|
source_obj, target_obj, source_attribute, target_attribute
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
for source_data_item in source_attribute.data.items():
|
for source_data_item in source_attribute.data.items():
|
||||||
index = source_data_item[0]
|
index = source_data_item[0]
|
||||||
source_data = source_data_item[1]
|
source_data = source_data_item[1]
|
||||||
@ -752,6 +949,134 @@ def transfer_attribute(
|
|||||||
setattr(target_data, key, getattr(source_data, key))
|
setattr(target_data, key, getattr(source_data, key))
|
||||||
|
|
||||||
|
|
||||||
|
def proximity_transfer_single_attribute(
|
||||||
|
source_obj: bpy.types.Object,
|
||||||
|
target_obj: bpy.types.Object,
|
||||||
|
source_attribute: bpy.types.Attribute,
|
||||||
|
target_attribute: bpy.types.Attribute,
|
||||||
|
):
|
||||||
|
# src_dat = source_obj.data
|
||||||
|
# tgt_dat = target_obj.data
|
||||||
|
# if type(src_dat) is not type(tgt_dat) or not (src_dat or tgt_dat):
|
||||||
|
# return False
|
||||||
|
# if type(tgt_dat) is not bpy.types.Mesh: # TODO: support more types
|
||||||
|
# return False
|
||||||
|
|
||||||
|
# If target attribute already exists, remove it.
|
||||||
|
# tgt_attr = tgt_dat.attributes.get(source_attribute.name)
|
||||||
|
# if tgt_attr is not None:
|
||||||
|
# try:
|
||||||
|
# tgt_dat.attributes.remove(tgt_attr)
|
||||||
|
# except RuntimeError:
|
||||||
|
# # Built-ins like "position" cannot be removed, and should be skipped.
|
||||||
|
# return
|
||||||
|
|
||||||
|
# Create target attribute.
|
||||||
|
# target_attribute = tgt_dat.attributes.new(
|
||||||
|
# source_attribute.name, source_attribute.data_type, source_attribute.domain
|
||||||
|
# )
|
||||||
|
|
||||||
|
data_sfx = {
|
||||||
|
'INT8': 'value',
|
||||||
|
'INT': 'value',
|
||||||
|
'FLOAT': 'value',
|
||||||
|
'FLOAT2': 'vector',
|
||||||
|
'BOOLEAN': 'value',
|
||||||
|
'STRING': 'value',
|
||||||
|
'BYTE_COLOR': 'color',
|
||||||
|
'FLOAT_COLOR': 'color',
|
||||||
|
'FLOAT_VECTOR': 'vector',
|
||||||
|
}
|
||||||
|
|
||||||
|
data_sfx = data_sfx[source_attribute.data_type]
|
||||||
|
|
||||||
|
# if topo_match:
|
||||||
|
# # TODO: optimize using foreach_get/set rather than loop
|
||||||
|
# for i in range(len(source_attribute.data)):
|
||||||
|
# setattr(tgt_attr.data[i], data_sfx, getattr(source_attribute.data[i], data_sfx))
|
||||||
|
# return
|
||||||
|
|
||||||
|
# proximity fallback
|
||||||
|
if source_attribute.data_type == 'STRING':
|
||||||
|
# TODO: add NEAREST transfer fallback for attributes without interpolation
|
||||||
|
print(
|
||||||
|
f'Proximity based transfer for generic attributes of type STRING not supported yet. Skipping attribute {source_attribute.name} on {target_obj}.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
domain = source_attribute.domain
|
||||||
|
if (
|
||||||
|
domain == 'POINT'
|
||||||
|
): # TODO: deduplicate interpolated point domain proximity transfer
|
||||||
|
bm_source = bmesh.new()
|
||||||
|
bm_source.from_mesh(source_obj.data)
|
||||||
|
bm_source.faces.ensure_lookup_table()
|
||||||
|
|
||||||
|
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||||
|
|
||||||
|
tris_dict = tris_per_face(bm_source)
|
||||||
|
|
||||||
|
for i, vert in enumerate(target_obj.data.vertices):
|
||||||
|
p = vert.co
|
||||||
|
face = closest_face_to_point(bm_source, p, bvh_tree)
|
||||||
|
|
||||||
|
(tri, point) = closest_tri_on_face(tris_dict, face, p)
|
||||||
|
if not tri:
|
||||||
|
continue
|
||||||
|
weights = mathutils.interpolate.poly_3d_calc(
|
||||||
|
[tri[i].vert.co for i in range(3)], point
|
||||||
|
)
|
||||||
|
|
||||||
|
if data_sfx in ['color']:
|
||||||
|
vals_weighted = [
|
||||||
|
weights[i]
|
||||||
|
* (
|
||||||
|
np.array(
|
||||||
|
getattr(source_attribute.data[tri[i].vert.index], data_sfx)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for i in range(3)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
vals_weighted = [
|
||||||
|
weights[i]
|
||||||
|
* (getattr(source_attribute.data[tri[i].vert.index], data_sfx))
|
||||||
|
for i in range(3)
|
||||||
|
]
|
||||||
|
setattr(target_attribute.data[i], data_sfx, sum(np.array(vals_weighted)))
|
||||||
|
return
|
||||||
|
elif domain == 'EDGE':
|
||||||
|
# TODO support proximity fallback for generic edge attributes
|
||||||
|
print(
|
||||||
|
f'Proximity based transfer of generic edge attributes not supported yet. Skipping attribute {source_attribute.name} on {target_obj}.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
elif domain == 'FACE':
|
||||||
|
bm_source = bmesh.new()
|
||||||
|
bm_source.from_mesh(source_obj.data)
|
||||||
|
bm_source.faces.ensure_lookup_table()
|
||||||
|
|
||||||
|
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
|
||||||
|
for i, face in enumerate(target_obj.data.polygons):
|
||||||
|
p_target = face.center
|
||||||
|
closest_face = closest_face_to_point(bm_source, p_target, bvh_tree)
|
||||||
|
setattr(
|
||||||
|
target_attribute.data[i],
|
||||||
|
data_sfx,
|
||||||
|
getattr(source_attribute.data[closest_face.index], data_sfx),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
elif domain == 'CORNER':
|
||||||
|
transfer_corner_data(
|
||||||
|
source_obj,
|
||||||
|
target_obj,
|
||||||
|
source_attribute.data,
|
||||||
|
target_attribute.data,
|
||||||
|
data_suffix=data_sfx,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def parent_clean(obj):
|
def parent_clean(obj):
|
||||||
matches = check_transfer_data_entry(
|
matches = check_transfer_data_entry(
|
||||||
obj.transfer_data_ownership,
|
obj.transfer_data_ownership,
|
||||||
|
Loading…
Reference in New Issue
Block a user