Asset Pipeline v2 #145

Closed
Nick Alberelli wants to merge 431 commits from (deleted):feature/asset-pipeline-v2 into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
Showing only changes of commit de611d4012 - Show all commits

View File

@ -478,20 +478,14 @@ def transfer_material_slots(target_obj: bpy.types.Object, source_obj):
# SHAPE KEYS
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_key_closest_face_to_point(bm_source, p_target, bvh_tree=None):
def closest_face_to_point(bm_source, p_target, bvh_tree=None):
if not bvh_tree:
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(bm_source)
(loc, norm, index, distance) = bvh_tree.find_nearest(p_target)
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_dict = dict()
for face in bm_source.faces:
@ -503,7 +497,7 @@ def shape_key_tris_per_face(bm_source):
return tris_dict
def shape_key_closest_tri_on_face(tris_dict, face, p):
def closest_tri_on_face(tris_dict, face, p):
points = []
dist = []
tris = []
@ -520,6 +514,203 @@ def shape_key_closest_tri_on_face(tris_dict, face, p):
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):
if obj.type != "MESH" or obj.data.shape_keys is None:
return
@ -618,12 +809,12 @@ def transfer_shape_key(
bm_source.faces.ensure_lookup_table()
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):
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:
continue
weights = mathutils.interpolate.poly_3d_calc(
@ -730,8 +921,8 @@ def transfer_attribute(
source_attributes = source_obj.data.attributes
target_attributes = target_obj.data.attributes
source_attribute = source_attributes.get(attribute_name)
target_attribute = target_attributes.get(attribute_name)
if target_attribute:
target_attributes.remove(target_attribute)
@ -740,7 +931,13 @@ def transfer_attribute(
type=source_attribute.data_type,
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():
index = source_data_item[0]
source_data = source_data_item[1]
@ -752,6 +949,134 @@ def transfer_attribute(
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):
matches = check_transfer_data_entry(
obj.transfer_data_ownership,