summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2025-06-08 17:51:43 -0700
committeryum <yum.food.vr@gmail.com>2025-06-08 17:51:43 -0700
commitb481bfc4e2fcabc670253766adb3c4a00d84bbfc (patch)
tree10864b5036f26a718f6320f31050fbb5e0e2be51
parentfac50a639aecf425a9d29d9948ea147f88ce3984 (diff)
Add caching to baker
-rw-r--r--BakeVertexData.py575
1 files changed, 214 insertions, 361 deletions
diff --git a/BakeVertexData.py b/BakeVertexData.py
index d90e78d..f5ac764 100644
--- a/BakeVertexData.py
+++ b/BakeVertexData.py
@@ -1248,6 +1248,12 @@ class MESH_OT_bake_vertex_and_rotation_combined(Operator):
precision=3
)
+ use_cache: BoolProperty(
+ name="Cache Identical Submeshes",
+ description="Optimize by caching calculations for identical submeshes (recommended for meshes with many repeated elements)",
+ default=True
+ )
+
@classmethod
def poll(cls, context):
obj = context.active_object
@@ -1256,429 +1262,276 @@ class MESH_OT_bake_vertex_and_rotation_combined(Operator):
context.mode == 'EDIT_MESH')
def get_vertex_islands(self, mesh, selected_indices):
- """Find contiguous groups of vertices using edge connectivity"""
- adjacency = {idx: set() for idx in selected_indices}
-
+ """Find contiguous groups of vertices"""
+ from collections import deque
+
+ # Build adjacency
+ adjacency = {idx: [] for idx in selected_indices}
for edge in mesh.edges:
v0, v1 = edge.vertices
- if v0 in selected_indices and v1 in selected_indices:
- adjacency[v0].add(v1)
- adjacency[v1].add(v0)
-
+ if v0 in adjacency and v1 in adjacency:
+ adjacency[v0].append(v1)
+ adjacency[v1].append(v0)
+
+ # Find islands
islands = []
visited = set()
-
+
for start_idx in selected_indices:
if start_idx in visited:
continue
-
+
island = set()
- queue = [start_idx]
-
+ queue = deque([start_idx])
+
while queue:
- current = queue.pop(0)
+ current = queue.popleft()
if current in visited:
continue
-
visited.add(current)
island.add(current)
-
- for neighbor in adjacency[current]:
+
+ for neighbor in adjacency.get(current, []):
if neighbor not in visited:
queue.append(neighbor)
-
+
islands.append(island)
-
+
return islands
- def get_submesh_faces_and_normals(self, mesh, island_indices):
- """Get all faces and their normals for vertices in the island"""
- faces_data = []
-
- # Build vertex to faces mapping for efficiency
- vertex_to_faces = {}
- for face_idx, face in enumerate(mesh.polygons):
- for vertex_idx in face.vertices:
- if vertex_idx in island_indices:
- if vertex_idx not in vertex_to_faces:
- vertex_to_faces[vertex_idx] = []
- vertex_to_faces[vertex_idx].append(face_idx)
-
- # Collect unique faces that belong to this island
- island_faces = set()
- for vertex_idx in island_indices:
- if vertex_idx in vertex_to_faces:
- for face_idx in vertex_to_faces[vertex_idx]:
- face = mesh.polygons[face_idx]
- # Check if all vertices of this face are in the island
- if all(v in island_indices for v in face.vertices):
- island_faces.add(face_idx)
-
- # Calculate face data
- for face_idx in island_faces:
- face = mesh.polygons[face_idx]
- faces_data.append({
- 'normal': face.normal.copy(),
- 'area': face.area,
- 'index': face_idx
- })
-
- return faces_data
+ def calculate_island_data(self, mesh, island_indices):
+ """Calculate center and scale for an island"""
+ if not island_indices:
+ return None, 1.0
+
+ # Calculate center
+ center = mathutils.Vector((0.0, 0.0, 0.0))
+ for idx in island_indices:
+ center += mesh.vertices[idx].co
+ center /= len(island_indices)
+
+ # Calculate scale (1 / max distance from center)
+ max_dist = 0.0
+ for idx in island_indices:
+ dist = max(abs(c - center[i]) for i, c in enumerate(mesh.vertices[idx].co))
+ max_dist = max(max_dist, dist)
+
+ scale = 1.0 / max_dist if max_dist > 0 else 1.0
+ return center, scale
- def build_orthonormal_basis_from_faces(self, faces_data, epsilon):
+ def build_basis_from_faces(self, mesh, face_indices, epsilon):
"""Build orthonormal basis from face normals"""
- if not faces_data:
- # Default to standard basis if no faces
+ if not face_indices:
return mathutils.Matrix.Identity(3)
-
- # Sort faces by area (largest first)
- faces_data.sort(key=lambda x: x['area'], reverse=True)
-
- # Find up to 3 non-parallel normals
- basis_normals = []
+
+ # Get largest face normal as primary axis
+ faces = sorted(face_indices, key=lambda i: mesh.polygons[i].area, reverse=True)
+ x_axis = mesh.polygons[faces[0]].normal.normalized()
+
+ # Find a second normal that's different enough
+ y_axis = None
epsilon_cos = math.cos(epsilon)
-
- for face_data in faces_data:
- normal = face_data['normal'].normalized()
-
- # Check if this normal is sufficiently different from existing ones
- is_unique = True
- for existing_normal in basis_normals:
- dot_product = abs(normal.dot(existing_normal))
- if dot_product > epsilon_cos:
- is_unique = False
- break
-
- if is_unique:
- basis_normals.append(normal)
- if len(basis_normals) >= 3:
- break
-
- # Build orthonormal basis
- if len(basis_normals) == 0:
- # No faces, use default
- return mathutils.Matrix.Identity(3)
-
- # First basis vector is the largest face normal
- x_axis = basis_normals[0].normalized()
-
- if len(basis_normals) >= 2:
- # Use second normal to define y-axis
- y_candidate = basis_normals[1]
- # Make it orthogonal to x
- y_axis = (y_candidate - y_candidate.project(x_axis)).normalized()
-
- # If y_axis is too small (normals were almost parallel), find a different vector
- if y_axis.length < 0.1:
- # Create arbitrary perpendicular vector
- if abs(x_axis.z) < 0.9:
- y_axis = mathutils.Vector((0, 0, 1)).cross(x_axis).normalized()
- else:
- y_axis = mathutils.Vector((1, 0, 0)).cross(x_axis).normalized()
- else:
- # Only one unique normal, create arbitrary perpendicular
+
+ for face_idx in faces[1:]:
+ normal = mesh.polygons[face_idx].normal.normalized()
+ if abs(normal.dot(x_axis)) < epsilon_cos:
+ y_axis = normal - normal.dot(x_axis) * x_axis
+ y_axis.normalize()
+ break
+
+ # If no good second normal, create perpendicular
+ if not y_axis:
if abs(x_axis.z) < 0.9:
- y_axis = mathutils.Vector((0, 0, 1)).cross(x_axis).normalized()
+ y_axis = mathutils.Vector((-x_axis.y, x_axis.x, 0))
else:
- y_axis = mathutils.Vector((1, 0, 0)).cross(x_axis).normalized()
-
- # Third axis from cross product
- z_axis = x_axis.cross(y_axis).normalized()
-
- # Build rotation matrix (columns are the basis vectors)
- matrix = mathutils.Matrix((
- (x_axis.x, y_axis.x, z_axis.x),
- (x_axis.y, y_axis.y, z_axis.y),
- (x_axis.z, y_axis.z, z_axis.z)
- ))
-
+ y_axis = mathutils.Vector((0, -x_axis.z, x_axis.y))
+ y_axis.normalize()
+
+ # Complete the basis
+ z_axis = x_axis.cross(y_axis)
+
# Ensure right-handed coordinate system
+ matrix = mathutils.Matrix((x_axis, y_axis, z_axis)).transposed()
if matrix.determinant() < 0:
- z_axis = -z_axis
- matrix = mathutils.Matrix((
- (x_axis.x, y_axis.x, z_axis.x),
- (x_axis.y, y_axis.y, z_axis.y),
- (x_axis.z, y_axis.z, z_axis.z)
- ))
-
+ matrix[2] = -matrix[2]
+
return matrix
- def get_island_info(self, mesh, island_indices):
- """Calculate center and scale for a single island"""
- verts = []
+ def create_submesh_signature(self, mesh, island_indices, center, scale, tolerance=0.0001):
+ """Create a hash signature for a submesh based on its local geometry"""
+ # Collect local vertex positions (relative to center and normalized by scale)
+ local_positions = []
for idx in island_indices:
- verts.append(mesh.vertices[idx].co)
-
- if not verts:
- return None, None, None
-
- center = mathutils.Vector((0, 0, 0))
- for co in verts:
- center += co
- center /= len(verts)
-
- max_dist = 0.0
- for co in verts:
- vec = co - center
- max_component = max(abs(vec.x), abs(vec.y), abs(vec.z))
- max_dist = max(max_dist, max_component)
-
- if max_dist > 0:
- scale = 1.0 / max_dist
- else:
- scale = 1.0
-
- return center, max_dist, scale
+ local_pos = (mesh.vertices[idx].co - center) * scale
+ # Round to tolerance to handle floating point differences
+ rounded = (
+ round(local_pos.x / tolerance) * tolerance,
+ round(local_pos.y / tolerance) * tolerance,
+ round(local_pos.z / tolerance) * tolerance
+ )
+ local_positions.append(rounded)
+
+ # Sort for consistent ordering
+ local_positions.sort()
+
+ # Create signature from vertex count and positions
+ signature = (len(island_indices), tuple(local_positions))
+ return signature
+
+ def calculate_submesh_data(self, mesh, island, selected_faces, scale, correction):
+ """Calculate basis, quaternion, and inverted basis for a submesh"""
+ # Get faces for this island
+ island_faces = [f for f in selected_faces
+ if all(v in island for v in mesh.polygons[f].vertices)]
+
+ # Build basis matrix
+ basis = self.build_basis_from_faces(mesh, island_faces, self.normal_epsilon)
+
+ # Calculate quaternion
+ quat = basis.to_quaternion()
+ quat.normalize()
+ if quat.w < 0:
+ quat.negate()
+ quat = correction @ quat
+
+ # Cache inverted basis
+ basis_inv = basis.inverted()
+
+ return scale, basis, quat, basis_inv
def execute(self, context):
obj = context.active_object
mesh = obj.data
- try:
- bpy.ops.object.mode_set(mode='OBJECT')
- except Exception as e:
- self.report({'ERROR'}, f"Failed to switch to object mode: {str(e)}")
- return {'CANCELLED'}
+ # Switch to object mode
+ bpy.ops.object.mode_set(mode='OBJECT')
+ # Get selected vertices
selected_indices = {v.index for v in mesh.vertices if v.select}
-
if not selected_indices:
self.report({'WARNING'}, "No vertices selected")
- try:
- bpy.ops.object.mode_set(mode='EDIT')
- except:
- pass
+ bpy.ops.object.mode_set(mode='EDIT')
return {'CANCELLED'}
- # Check if mesh has faces
+ # Ensure mesh has faces
if not mesh.polygons:
- self.report({'ERROR'}, "Mesh has no faces. Both vertex colors and quaternion baking require faces.")
- try:
- bpy.ops.object.mode_set(mode='EDIT')
- except:
- pass
+ self.report({'ERROR'}, "Mesh has no faces")
+ bpy.ops.object.mode_set(mode='EDIT')
return {'CANCELLED'}
- # Create vertex color layer
+ # Create/get data layers
if not mesh.vertex_colors:
mesh.vertex_colors.new(name="BakedVectors")
color_layer = mesh.vertex_colors.active
- if not color_layer:
- self.report({'ERROR'}, "Failed to create vertex color layer")
- try:
- bpy.ops.object.mode_set(mode='EDIT')
- except:
- pass
- return {'CANCELLED'}
- # Remove existing UV maps and create new ones for quaternions
- uv_names = ["BakedOriginAngle0", "BakedOriginAngle1"]
- for uv_name in uv_names:
- if uv_name in mesh.uv_layers:
- mesh.uv_layers.remove(mesh.uv_layers[uv_name])
-
- uv_layer0 = mesh.uv_layers.new(name="BakedOriginAngle0")
- uv_layer1 = mesh.uv_layers.new(name="BakedOriginAngle1")
-
- if not uv_layer0 or not uv_layer1:
- self.report({'ERROR'}, "Failed to create UV layers")
- try:
- bpy.ops.object.mode_set(mode='EDIT')
- except:
- pass
- return {'CANCELLED'}
+ uv_layer0 = mesh.uv_layers.get("BakedOriginAngle0")
+ uv_layer1 = mesh.uv_layers.get("BakedOriginAngle1")
+ if not uv_layer0:
+ uv_layer0 = mesh.uv_layers.new(name="BakedOriginAngle0")
+ if not uv_layer1:
+ uv_layer1 = mesh.uv_layers.new(name="BakedOriginAngle1")
- source_matrix = obj.matrix_world
+ # Get correction quaternion
+ settings = context.scene.bake_vertex_settings
+ correction = mathutils.Euler(
+ (settings.correction_angle_x, settings.correction_angle_y, settings.correction_angle_z), 'XYZ'
+ ).to_quaternion()
+
+ # Build vertex to loops mapping
+ vertex_loops = {}
+ selected_faces = []
+
+ for poly in mesh.polygons:
+ face_verts = set(poly.vertices)
+ if face_verts & selected_indices: # Face has selected vertices
+ if face_verts <= selected_indices: # All vertices selected
+ selected_faces.append(poly.index)
+
+ for loop_idx in poly.loop_indices:
+ vert_idx = mesh.loops[loop_idx].vertex_index
+ if vert_idx in selected_indices:
+ vertex_loops.setdefault(vert_idx, []).append(loop_idx)
+ # Get islands to process
if self.contiguous_mode:
- # Build vertex to polygon mapping for efficiency
- vertex_to_polys = {}
- for poly_idx, poly in enumerate(mesh.polygons):
- for vertex_idx in poly.vertices:
- if vertex_idx in selected_indices:
- if vertex_idx not in vertex_to_polys:
- vertex_to_polys[vertex_idx] = []
- vertex_to_polys[vertex_idx].append(poly_idx)
-
islands = self.get_vertex_islands(mesh, selected_indices)
- total_updated = 0
+ else:
+ islands = [selected_indices] # Single island with all selected
- # Process each island separately
- for island_indices in islands:
- # Get vector data for this island
- center, max_dist, scale = self.get_island_info(mesh, island_indices)
- if center is None:
- continue
+ # Process each island
+ world_matrix = obj.matrix_world
+ world_inv = world_matrix.inverted()
+
+ # Cache for identical submeshes - stores (scale, basis, quaternion, basis_inv)
+ submesh_cache = {} if self.use_cache else None
+ cache_hits = 0
+
+ for island in islands:
+ # Calculate island data
+ center, scale = self.calculate_island_data(mesh, island)
+ if center is None:
+ continue
- # Get face data and build orthonormal basis for this island
- faces_data = self.get_submesh_faces_and_normals(mesh, island_indices)
- basis_matrix = self.build_orthonormal_basis_from_faces(faces_data, self.normal_epsilon)
+ # Check if we should use caching
+ if self.use_cache:
+ # Create signature for this submesh
+ signature = self.create_submesh_signature(mesh, island, center, scale)
- # Convert basis to quaternion
- quaternion = basis_matrix.to_quaternion()
- quaternion.normalize()
-
- if quaternion.w < 0:
- quaternion.negate()
-
- # Apply user-defined rotation correction from scene properties
- settings = context.scene.bake_vertex_settings
- correction_euler = mathutils.Euler(
- (settings.correction_angle_x, settings.correction_angle_y, settings.correction_angle_z), 'XYZ'
- )
- unity_correction = correction_euler.to_quaternion()
- quaternion = unity_correction @ quaternion
-
- center_world = source_matrix @ center
-
- # Collect all polygons that contain vertices from this island
- relevant_polys = set()
- for vertex_idx in island_indices:
- if vertex_idx in vertex_to_polys:
- relevant_polys.update(vertex_to_polys[vertex_idx])
-
- # Only process relevant polygons
- for poly_idx in relevant_polys:
- poly = mesh.polygons[poly_idx]
- for loop_idx in poly.loop_indices:
- vertex_idx = mesh.loops[loop_idx].vertex_index
-
- if vertex_idx in island_indices:
- vertex = mesh.vertices[vertex_idx]
-
- # Calculate vector from vertex to center in world space
- vertex_world = source_matrix @ vertex.co
- vector_world = vertex_world - center_world
-
- # Transform to object space
- vector_object = source_matrix.inverted_safe().to_3x3() @ vector_world
-
- # Transform vector to local orthonormal basis coordinates
- # Use transpose of basis matrix to transform from object to local
- vector_local = basis_matrix.transposed() @ vector_object
-
- # Apply scale
- vector_scaled = vector_local * scale
-
- # Store transformed vector in vertex colors
- color = mathutils.Vector((
- (vector_scaled.x + 1.0) * 0.5,
- (vector_scaled.y + 1.0) * 0.5,
- (vector_scaled.z + 1.0) * 0.5,
- scale
- ))
- color_layer.data[loop_idx].color = color
-
- # Store quaternion in UV coordinates
- uv_layer0.data[loop_idx].uv = mathutils.Vector((quaternion.x, quaternion.y))
- uv_layer1.data[loop_idx].uv = mathutils.Vector((quaternion.z, quaternion.w))
-
- total_updated += 1
-
- mesh.update()
-
- try:
- bpy.ops.object.mode_set(mode='EDIT')
- except Exception as e:
- self.report({'WARNING'}, f"Could not return to edit mode: {str(e)}")
-
- self.report({'INFO'}, f"Baked vectors & quaternions for {len(islands)} contiguous groups ({len(selected_indices)} vertices)")
- return {'FINISHED'}
-
- else:
- # Single group mode - process all selected vertices as one group
-
- # Get vector data
- verts = []
- for idx in selected_indices:
- verts.append(mesh.vertices[idx].co)
-
- center = mathutils.Vector((0, 0, 0))
- for co in verts:
- center += co
- center /= len(verts)
-
- max_dist = 0.0
- for co in verts:
- vec = co - center
- max_component = max(abs(vec.x), abs(vec.y), abs(vec.z))
- max_dist = max(max_dist, max_component)
-
- if max_dist > 0:
- scale = 1.0 / max_dist
+ # Check cache for identical submesh
+ if signature in submesh_cache:
+ # Reuse cached data
+ scale, basis, quat, basis_inv = submesh_cache[signature]
+ cache_hits += 1
+ else:
+ # Calculate new data and store in cache
+ data = self.calculate_submesh_data(mesh, island, selected_faces, scale, correction)
+ scale, basis, quat, basis_inv = data
+ submesh_cache[signature] = data
else:
- scale = 1.0
-
- # Get face data and build orthonormal basis for all selected vertices
- faces_data = self.get_submesh_faces_and_normals(mesh, selected_indices)
- basis_matrix = self.build_orthonormal_basis_from_faces(faces_data, self.normal_epsilon)
+ # No caching - calculate data directly
+ scale, basis, quat, basis_inv = self.calculate_submesh_data(mesh, island, selected_faces, scale, correction)
- # Convert basis to quaternion
- quaternion = basis_matrix.to_quaternion()
- quaternion.normalize()
-
- if quaternion.w < 0:
- quaternion.negate()
-
- # Apply user-defined rotation correction from scene properties
- settings = context.scene.bake_vertex_settings
- correction_euler = mathutils.Euler(
- (settings.correction_angle_x, settings.correction_angle_y, settings.correction_angle_z), 'XYZ'
- )
- unity_correction = correction_euler.to_quaternion()
- quaternion = unity_correction @ quaternion
+ # Transform vertices
+ center_world = world_matrix @ center
+
+ for vert_idx in island:
+ # Calculate local position
+ vert_world = world_matrix @ mesh.vertices[vert_idx].co
+ offset = world_inv.to_3x3() @ (vert_world - center_world)
+ local_pos = basis_inv @ offset
+
+ # Scale and convert to color
+ color = mathutils.Vector((
+ (local_pos.x * scale + 1.0) * 0.5,
+ (local_pos.y * scale + 1.0) * 0.5,
+ (local_pos.z * scale + 1.0) * 0.5,
+ scale
+ ))
+
+ # Apply to all loops of this vertex
+ for loop_idx in vertex_loops.get(vert_idx, []):
+ color_layer.data[loop_idx].color = color
+ uv_layer0.data[loop_idx].uv = (quat.x, quat.y)
+ uv_layer1.data[loop_idx].uv = (quat.z, quat.w)
- center_world = source_matrix @ center
+ mesh.update()
+ bpy.ops.object.mode_set(mode='EDIT')
- updated_count = 0
- for poly in mesh.polygons:
- for loop_idx in poly.loop_indices:
- vertex_idx = mesh.loops[loop_idx].vertex_index
-
- if vertex_idx in selected_indices:
- vertex = mesh.vertices[vertex_idx]
-
- # Calculate vector from vertex to center in world space
- vertex_world = source_matrix @ vertex.co
- vector_world = vertex_world - center_world
-
- # Transform to object space
- vector_object = source_matrix.inverted_safe().to_3x3() @ vector_world
-
- # Transform vector to local orthonormal basis coordinates
- vector_local = basis_matrix.transposed() @ vector_object
-
- # Apply scale
- vector_scaled = vector_local * scale
-
- # Store transformed vector in vertex colors
- color = mathutils.Vector((
- (vector_scaled.x + 1.0) * 0.5,
- (vector_scaled.y + 1.0) * 0.5,
- (vector_scaled.z + 1.0) * 0.5,
- scale
- ))
- color_layer.data[loop_idx].color = color
-
- # Store quaternion in UV coordinates
- uv_layer0.data[loop_idx].uv = mathutils.Vector((quaternion.x, quaternion.y))
- uv_layer1.data[loop_idx].uv = mathutils.Vector((quaternion.z, quaternion.w))
-
- updated_count += 1
-
- mesh.update()
-
- try:
- bpy.ops.object.mode_set(mode='EDIT')
- except Exception as e:
- self.report({'WARNING'}, f"Could not return to edit mode: {str(e)}")
-
- self.report({'INFO'}, f"Baked vectors & quaternion for {len(selected_indices)} vertices with scale {scale:.3f}")
- return {'FINISHED'}
+ # Report with cache information
+ if self.use_cache and submesh_cache:
+ unique_submeshes = len(submesh_cache)
+ if cache_hits > 0:
+ self.report({'INFO'}, f"Baked {len(islands)} islands ({unique_submeshes} unique, {cache_hits} cache hits) with {len(selected_indices)} vertices")
+ else:
+ self.report({'INFO'}, f"Baked {len(islands)} islands ({unique_submeshes} unique shapes) with {len(selected_indices)} vertices")
+ else:
+ self.report({'INFO'}, f"Baked {len(islands)} island(s) with {len(selected_indices)} vertices")
+ return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.prop(self, "contiguous_mode")
+ layout.prop(self, "use_cache")
layout.prop(self, "normal_epsilon")
settings = context.scene.bake_vertex_settings