diff --git a/tensionmap.py b/tensionmap.py
index a5aee8c..9254f9b 100644
--- a/tensionmap.py
+++ b/tensionmap.py
@@ -16,11 +16,13 @@
# along with this program. If not, see .
import bpy
+import bmesh
+from dataclasses import dataclass
bl_info = {
"name": "Tension Map Script",
"author": "Scott Winkelmann , Jean-Francois Gallant (PyroEvil)",
- "version": (2, 2, 0),
+ "version": (2, 3, 0),
"blender": (2, 80, 72),
"location": "Properties Panel > Data Tab",
"description": "This add-on adds stretch and squeeze information to desired meshes",
@@ -30,8 +32,9 @@
"category": "Object"
}
+geometry_cache = dict()
last_processed_frame = None
-number_of_tm_channels = 2
+number_of_color_channels = 4
# list of modifiers that we will keep to compute the deformation
# TODO: update based on list in docs
# https://docs.blender.org/api/blender2.8/bpy.types.Modifier.html#bpy.types.Modifier.type
@@ -44,6 +47,58 @@
tm_update_modes = ["OBJECT", "WEIGHT_PAINT", "VERTEX_PAINT"]
+@dataclass
+class GeometryData:
+ original_edge_lengths: list
+ vertex_color_index_mapping: dict
+
+
+def calculate_original_edge_lengths(obj):
+ """
+ Calculates the edge length of an object, without modifiers
+ :param obj: the object to operate on
+ :return: returns a list of edge lengths
+ """
+ number_of_edges = len(obj.data.edges)
+ edge_lengths = [0] * number_of_edges
+ bmesh_orig = bmesh.new()
+ bmesh_orig.from_mesh(obj.data)
+ bmesh_orig.edges.ensure_lookup_table()
+ for i in range(number_of_edges):
+ edge_lengths[i] = bmesh_orig.edges[i].calc_length()
+ return edge_lengths
+
+
+def calculate_vertex_color_index_mapping(obj):
+ """
+ Calculates an dict for mapping regular vertex indices to loop-based vertex indices
+ :param obj: the object to operate on
+ :return: returns the mapping dict
+ """
+ index_mapping = dict()
+ index = 0
+ for polygon in obj.data.polygons:
+ for loop_vertex_idx, loop_idx in enumerate(polygon.loop_indices):
+ vertex_idx = polygon.vertices[loop_vertex_idx]
+ index_mapping[index] = vertex_idx
+ index = index + 1
+ return index_mapping
+
+
+def update_geometry_cache_for_object(obj):
+ """
+ Updates the geometry cache for an object by filling it with new values or deleting it
+ :param obj: the object to operate on
+ :return: nothing
+ """
+ if obj.data.tm_enable_geometry_cache:
+ geometry_cache[obj] = GeometryData(calculate_original_edge_lengths(obj),
+ calculate_vertex_color_index_mapping(obj))
+ else:
+ if obj in geometry_cache:
+ del geometry_cache[obj]
+
+
def get_or_create_vertex_group(obj, group_name):
"""
Creates a new vertex group and initializes it only if it doesn't exist then returns it
@@ -130,30 +185,47 @@ def tm_update(obj, context):
# array to store new weight for each vertices
weights = [0.0] * num_vertices
+ # referencing the cache data
+ # it would be simpler to just generate the needed GeometryData for any non-cached object here,
+ # but then we would go through the edge-loop twice, when calculating the edge lengths
+ if obj.data.tm_enable_geometry_cache:
+ if obj not in geometry_cache:
+ update_geometry_cache_for_object(obj)
+ geometry_data = geometry_cache[obj]
+ else:
+ original_bmesh = bmesh.new()
+ original_bmesh.from_mesh(obj.data)
+ original_bmesh.edges.ensure_lookup_table()
+ deformed_bmesh = bmesh.new()
+ deformed_bmesh.from_mesh(deformed_mesh)
+ deformed_bmesh.edges.ensure_lookup_table()
+ num_edges = len(obj.data.edges)
+
# calculate the new weights
- for i in range(len(obj.data.edges)):
- edge = obj.data.edges[i]
- first_vertex = edge.vertices[0]
- second_vertex = edge.vertices[1]
-
- original_edge_length = (obj.data.vertices[first_vertex].co -
- obj.data.vertices[second_vertex].co).length
- deformed_edge_length = (
- deformed_mesh.vertices[first_vertex].co - deformed_mesh.vertices[second_vertex].co).length
-
- deformation_factor = (original_edge_length -
+ for i in range(num_edges):
+ if obj.data.tm_enable_geometry_cache:
+ original_edge_length = geometry_data.original_edge_lengths[i]
+ else:
+ original_edge_length = original_bmesh.edges[i].calc_length()
+ deformed_edge_length = deformed_bmesh.edges[i].calc_length()
+ deformation_factor = (original_edge_length -
deformed_edge_length) * obj.data.tm_multiply
+ first_vertex, second_vertex = deformed_bmesh.edges[i].verts
# store the weights by subtracting to overlay all the factors for each vertex
- weights[first_vertex] -= deformation_factor
- weights[second_vertex] -= deformation_factor
+ weights[first_vertex.index] -= deformation_factor
+ weights[second_vertex.index] -= deformation_factor
+
# delete the temporary deformed mesh
object_eval.to_mesh_clear()
# create vertex color list for faster access only if vertex color is activated
if obj.data.tm_enable_vertex_colors:
- vertex_colors = [0.0] * (number_of_tm_channels * num_vertices)
+ vertex_colors = [[0.0] * number_of_color_channels] * num_vertices
+
+ # lambda for clamping between min and max
+ clamp = lambda value, lower, upper: lower if value < lower else upper if value > upper else value
# calculate the new values
# store them in the vertex_colors array if the feature is active
@@ -164,13 +236,11 @@ def tm_update(obj, context):
if weights[i] >= 0:
# positive: stretched
- stretch_value = max(obj.data.tm_minimum, min(
- obj.data.tm_maximum, weights[i]))
+ stretch_value = clamp(weights[i], obj.data.tm_minimum, obj.data.tm_maximum)
else:
# negative: squeezed
# invert weights to keep only positive values
- squeeze_value = max(obj.data.tm_minimum, min(
- obj.data.tm_maximum, -weights[i]))
+ squeeze_value = clamp(-weights[i], obj.data.tm_minimum, obj.data.tm_maximum)
if obj.data.tm_enable_vertex_groups:
add_index = [i]
@@ -178,24 +248,19 @@ def tm_update(obj, context):
group_stretch.add(add_index, stretch_value, "REPLACE")
if obj.data.tm_enable_vertex_colors:
- # red
- vertex_colors[i * number_of_tm_channels] = stretch_value
- # green
- vertex_colors[i * number_of_tm_channels + 1] = squeeze_value
+ # red, green, blue, alpha
+ vertex_colors[i] = (stretch_value, squeeze_value, 0.0, 1.0)
+ if obj.data.tm_enable_geometry_cache:
+ vertex_color_index_mapping = geometry_data.vertex_color_index_mapping
+ else:
+ vertex_color_index_mapping = calculate_vertex_color_index_mapping(obj)
# store the calculated vertex colors if the feature is active
if obj.data.tm_enable_vertex_colors:
- colors_tension = get_or_create_vertex_colors(obj, "tm_tension")
- # this is heavy, but vertex colors are stored by vertex loop
- # and there is no simpler way to do it (it would seem)
- for poly_idx in range(len(obj.data.polygons)):
- polygon = obj.data.polygons[poly_idx]
- for loop_vertex_idx, loop_idx in enumerate(polygon.loop_indices):
- vertex_color = colors_tension.data[loop_idx]
- vertex_idx = polygon.vertices[loop_vertex_idx]
- # replace the color by a 4D vector, using 0 for blue and 1 for alpha
- vertex_color.color = (vertex_colors[vertex_idx * number_of_tm_channels],
- vertex_colors[vertex_idx * number_of_tm_channels + 1], 0, 1)
+ colors_tension_data = get_or_create_vertex_colors(obj, "tm_tension").data
+ tension_color_size = len(colors_tension_data)
+ for i in range(tension_color_size):
+ colors_tension_data[i].color = vertex_colors[vertex_color_index_mapping[i]]
def tm_update_handler(scene):
@@ -227,6 +292,17 @@ def tm_update_selected(self, context):
tm_update(context.object, context)
+def tm_update_geometry_cache(self, context):
+ """
+ Updates the geometry cache for the selected object, then updates the tension map
+ :param context: the context in which the selected object is
+ :return: nothing
+ """
+ update_geometry_cache_for_object(context.object)
+
+ tm_update(context.object, context)
+
+
class TmUpdateSelected(bpy.types.Operator):
"""Update tension map for selected object"""
@@ -243,6 +319,22 @@ def invoke(self, context, event):
return self.execute(context)
+class TmUpdateGeometryCache(bpy.types.Operator):
+ """Update geometry cache for selected object. \nHas to be done manually when the object changes."""
+
+ # this operator is simply a wrapper for the update_geometry_cache function
+ bl_label = "Update cache"
+ bl_idname = "tm.update_geometry_cache"
+ bl_options = {"REGISTER"}
+
+ def execute(self, context):
+ tm_update_geometry_cache(self, context)
+ return {"FINISHED"}
+
+ def invoke(self, context, event):
+ return self.execute(context)
+
+
class TmPanel(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
@@ -263,27 +355,33 @@ def draw(self, context):
if context.object.type != "MESH":
return
- flow = self.layout.column()
-
- row1 = flow.column()
- row1.active = context.object.data.tm_active
- row1.operator("tm.update_selected")
- row1.prop(context.object.data, "tm_enable_vertex_groups",
- text="Enable Vertex Groups")
- row1.prop(context.object.data, "tm_enable_vertex_colors",
- text="Enable Vertex Colors")
- row1.prop(context.object.data, "tm_multiply", text="Multiplier")
- row1.prop(context.object.data, "tm_minimum", text="Minimum")
- row1.prop(context.object.data, "tm_maximum", text="Maximum")
+ obj = context.object.data
+
+ col1 = self.layout.column()
+ col1.active = obj.tm_active
+ col1.operator("tm.update_selected", icon="FILE_REFRESH")
+ row1 = col1.row()
+ row1Col1 = row1.column()
+ row1Col2 = row1.column()
+ row1Col1.prop(obj, "tm_enable_geometry_cache", toggle=True,
+ icon='CHECKBOX_HLT' if obj.tm_enable_geometry_cache else 'CHECKBOX_DEHLT', text="Geometry Cache")
+ row1Col2.operator("tm.update_geometry_cache", icon="FILE_REFRESH")
+ row1Col2.active = obj.tm_enable_geometry_cache
+
+ col1.prop(obj, "tm_enable_vertex_groups")
+ col1.prop(obj, "tm_enable_vertex_colors")
+ col1.prop(obj, "tm_multiply")
+ col1.prop(obj, "tm_minimum")
+ col1.prop(obj, "tm_maximum")
'''
# TODO: finish implementing interface for choosing modifiers
flow.separator()
- row2 = flow.column()
- row2.enabled = context.object.data.tm_active
- row2.label(text="Modifiers to use when computing tension")
- list = row2.box()
+ col2 = flow.column()
+ col2.enabled = context.object.data.tm_active
+ col2.label(text="Modifiers to use when computing tension")
+ list = col2.box()
modifiers = context.object.modifiers
@@ -304,36 +402,42 @@ def add_props():
default=False,
update=tm_update_selected)
bpy.types.Mesh.tm_multiply = bpy.props.FloatProperty(
- name="tm_multiply",
+ name="Multiplier",
description="Tension map intensity multiplier",
min=0.0,
max=9999.0,
default=1.0,
update=tm_update_selected)
bpy.types.Mesh.tm_minimum = bpy.props.FloatProperty(
- name="tm_minimum",
+ name="Minimum",
description="Tension map minimum value",
min=0.0,
max=1.0,
default=0.0,
update=tm_update_selected)
bpy.types.Mesh.tm_maximum = bpy.props.FloatProperty(
- name="tm_maximum",
+ name="Maximum",
description="Tension map maximum value",
min=0.0,
max=1.0,
default=1.0,
update=tm_update_selected)
bpy.types.Mesh.tm_enable_vertex_groups = bpy.props.BoolProperty(
- name="tm_enable_vertex_groups",
+ name="Enable Vertex Group Output",
description="Whether to enable vertex groups",
default=False,
update=tm_update_selected)
bpy.types.Mesh.tm_enable_vertex_colors = bpy.props.BoolProperty(
- name="tm_enable_vertex_colors",
+ name="Enable Vertex Color Output",
description="Whether to enable vertex colors",
default=False,
update=tm_update_selected)
+ bpy.types.Mesh.tm_enable_geometry_cache = bpy.props.BoolProperty(
+ name="Enable Geometry Cache",
+ description="Improve realtime performance by pre-calculating some geometry data.\n"
+ "An update to the mesh will require a manual cache update",
+ default=False,
+ update=tm_update_geometry_cache)
def remove_props():
@@ -347,6 +451,7 @@ def remove_props():
del bpy.types.Mesh.tm_maximum
del bpy.types.Mesh.tm_enable_vertex_groups
del bpy.types.Mesh.tm_enable_vertex_colors
+ del bpy.types.Mesh.tm_enable_geometry_cache
def add_handlers():
@@ -372,6 +477,7 @@ def register():
:return: nothing
"""
add_props()
+ bpy.utils.register_class(TmUpdateGeometryCache)
bpy.utils.register_class(TmUpdateSelected)
bpy.utils.register_class(TmPanel)
add_handlers()
@@ -385,6 +491,7 @@ def unregister():
remove_handlers()
bpy.utils.unregister_class(TmPanel)
bpy.utils.unregister_class(TmUpdateSelected)
+ bpy.utils.unregister_class(TmUpdateGeometryCache)
remove_props()