Skip to content

Commit 2751f17

Browse files
committed
Implemented transfer with deltas relative to vertex normals. Closes issue #4.
1 parent f5c9e20 commit 2751f17

File tree

2 files changed

+76
-18
lines changed

2 files changed

+76
-18
lines changed

BlenderScripts/addons/sktransfer/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ class OBJECT_OT_transfer_shapekey_via_uv(Operator):
4040
"Increase this size if you have a high density of pixels in the UV and see distortions in the transferred ShapeKey."
4141
)
4242

43-
#relative_to_normals: BoolProperty(
44-
# name="relative_to_normals",
45-
# default=False,
46-
# description="If True, vertex offsets will be considered as relative to the vertex normals,"
47-
# " otherwise, each offset is in object space."
48-
#)
43+
relative_to_normals: BoolProperty(
44+
name="relative_to_normals",
45+
default=False,
46+
description="If True, vertex offsets will be considered as relative to the vertex normals,"
47+
" otherwise, each offset is in object space."
48+
)
4949

5050
save_debug_images: BoolProperty(
5151
name="save_debug_images",
@@ -134,6 +134,7 @@ def execute(self, context):
134134
transfer_shapekey_via_uv(src_obj=src_obj, src_sk_idx=src_active_sk_idx, src_uv_idx=src_active_uv_idx,
135135
dst_obj=dst_obj, dst_uv_idx=dst_active_uv_idx,
136136
resolution=(self.buffer_size, self.buffer_size),
137+
use_normals=self.relative_to_normals,
137138
save_debug_images=self.save_debug_images)
138139

139140
return {'FINISHED'}

BlenderScripts/addons/sktransfer/shapekeys.py

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

55
import bpy
6-
from mathutils import Vector
6+
from mathutils import Vector, Quaternion
77

88
import numpy as np
99

@@ -16,7 +16,43 @@
1616
from typing import Tuple
1717

1818

19-
def export_shapekey_info(mesh: bpy.types.Mesh, shape_key_idx: int, uv_layer_idx: int, resolution: Tuple[int, int] = (256, 256)) -> np.ndarray:
19+
def _quaternion_between(v1: Vector, v2: Vector) -> Quaternion:
20+
"""
21+
Computes the Quaternion aligning the direction of v1 to v2
22+
:param v1:
23+
:param v2:
24+
:return: The Quaternion that can rotate v1 to v2
25+
"""
26+
27+
# From https://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another
28+
# One can rotate a vector u to vector v with
29+
#
30+
# function fromVectors(u, v) {
31+
#
32+
# d = dot(u, v)
33+
# w = cross(u, v)
34+
#
35+
# return Quaternion(d + sqrt(d * d + dot(w, w)), w).normalize()
36+
# }
37+
# If it is known that the vectors u to vector v are unit vectors, the function reduces to
38+
#
39+
# function fromUnitVectors(u, v) {
40+
# return Quaternion(1 + dot(u, v), cross(u, v)).normalize()
41+
# }
42+
43+
v1n = v1.normalized()
44+
v2n = v2.normalized()
45+
46+
d = v1n.dot(v2n)
47+
w = v1n.cross(v2n)
48+
49+
out = Quaternion((1 + d, *w)).normalized()
50+
51+
return out
52+
53+
54+
def export_shapekey_info(mesh: bpy.types.Mesh, shape_key_idx: int, uv_layer_idx: int,
55+
resolution: Tuple[int, int] = (256, 256), use_normals: bool = False) -> np.ndarray:
2056
"""
2157
Given a mesh, a ShapeKey index and a UV layer, uses the UV vertex coordinates to produce a 3D map storing the offset of each vertex with respect to the base ShapeKey.
2258
The function returns a "ShapeKey Info" 4D array storing the ShapeKey offset and a counter info.
@@ -75,6 +111,13 @@ def export_shapekey_info(mesh: bpy.types.Mesh, shape_key_idx: int, uv_layer_idx:
75111
delta = sk_vertex_coords - sk_base_vertex_coords
76112
# print(sk_vertex_coords, sk_base_vertex_coords, delta)
77113

114+
# Convert the delta into a vector relative to the vertex normal
115+
if use_normals:
116+
v_normal = mesh.vertices[v_idx].normal
117+
canonical_normal = Vector((0, 1, 0))
118+
rot_to_canonical = _quaternion_between(v_normal, canonical_normal)
119+
delta = rot_to_canonical @ delta
120+
78121
# Accumulate the delta into the output picture
79122
if out[out_y, out_x, 3] == 0.0:
80123
out[out_y, out_x, :] = delta.x, delta.y, delta.z, 1
@@ -88,7 +131,10 @@ def export_shapekey_info(mesh: bpy.types.Mesh, shape_key_idx: int, uv_layer_idx:
88131
return out
89132

90133

91-
def create_shapekey(obj: bpy.types.Object, uv_layer_idx: int, xyz_map: np.ndarray, src_sk: str) -> None:
134+
def create_shapekey(obj: bpy.types.Object, uv_layer_idx: int, xyz_map: np.ndarray, src_sk: bpy.types.ShapeKey,
135+
use_normals: bool = False) -> None:
136+
"""Analyses the xyz delta map and creates a new ShapeKey onto the specified object using the specified
137+
UV layer. The reference to the source ShapeKey is used to copy name and other parameters."""
92138

93139
if obj.type != 'MESH':
94140
raise Exception("obj must be of type MESH")
@@ -133,6 +179,13 @@ def create_shapekey(obj: bpy.types.Object, uv_layer_idx: int, xyz_map: np.ndarra
133179
delta = Vector(xyz_map[uv_y, uv_x])
134180
# print(f"For poly {poly.index} coords {uv_x},{uv_y} --> delta {delta}")
135181

182+
# Convert the delta into a vector relative to the vertex normal
183+
if use_normals:
184+
v_normal = mesh.vertices[v_idx].normal
185+
canonical_normal = Vector((0, 1, 0))
186+
rot_to_canonical = _quaternion_between(v_normal, canonical_normal)
187+
delta = rot_to_canonical.inverted() @ delta
188+
136189
# Adds the delta to the vertex position in the reference ShapeKey
137190
reference_coords: Vector = reference_shape_key.data[v_idx].co
138191
absolute_coords = reference_coords + delta
@@ -180,21 +233,23 @@ def _save_buffer_as_image(buffer: np.ndarray, save_name: str) -> None:
180233
def transfer_shapekey_via_uv(src_obj: bpy.types.Object, src_sk_idx: int, src_uv_idx: int,
181234
dst_obj: bpy.types.Object, dst_uv_idx: int,
182235
resolution: Tuple[int, int],
236+
use_normals: bool = False,
183237
save_debug_images: bool = False) -> None:
184238
"""
185239
The main function that: i) retrieves the ShapeKey deltas from the source mesh,
186240
ii) triangulates the delta points and fills the gaps between the points, and
187241
iii) creates the ShapeKey on the destination object.
188242
189-
:param src_obj:
190-
:param src_sk_idx:
191-
:param src_uv_idx:
192-
:param dst_obj:
193-
:param dst_uv_idx:
194-
:param resolution:
195-
:param save_debug_images: If True, the intermediate buffers (counts, deltas and triangulates deltas)
243+
:param src_obj: The source object, containing ShapeKeys and UV layers
244+
:param src_sk_idx: The index of the ShapeKey to copy
245+
:param src_uv_idx: The index of the UV layer to be used for creating the delta map
246+
:param dst_obj: The destination object on which a new ShapeKey will be created
247+
:param dst_uv_idx: The UV layer of the destination object that should be used to find locations on the delta map
248+
:param resolution: The resolution of the intermediate delta map
249+
:param use_normals: If True, the vertex deltas will be saved and loaded as relative to the vertex normal
250+
:param save_debug_images: If True, the intermediate buffers (counts, deltas, and triangulates deltas)
196251
will be saved as PNG images for visual debugging.
197-
:return:
252+
:return: Nothing
198253
"""
199254

200255
map_width, map_height = resolution
@@ -205,6 +260,7 @@ def transfer_shapekey_via_uv(src_obj: bpy.types.Object, src_sk_idx: int, src_uv_
205260
#
206261
# Extract ShapeKey info
207262
sk_info = export_shapekey_info(mesh=src_mesh, shape_key_idx=src_sk_idx, uv_layer_idx=src_uv_idx,
263+
use_normals=use_normals,
208264
resolution=resolution)
209265

210266
# Convert counts ShapeKey Info into a grey image and save
@@ -263,4 +319,5 @@ def transfer_shapekey_via_uv(src_obj: bpy.types.Object, src_sk_idx: int, src_uv_
263319
#
264320
# Rebuild the ShapeKey on the destination object
265321
print(f"Creating ShapeKey '{src_active_sk.name}' on object '{dst_obj.name}' using UV layer {dst_uv_idx} ...")
266-
create_shapekey(obj=dst_obj, uv_layer_idx=dst_uv_idx, xyz_map=filled_xyz_map, src_sk=src_active_sk)
322+
create_shapekey(obj=dst_obj, uv_layer_idx=dst_uv_idx, xyz_map=filled_xyz_map, src_sk=src_active_sk,
323+
use_normals=use_normals)

0 commit comments

Comments
 (0)