Damien Picard
8db31f5cb9
Using hardcoded names to retrieve modifiers can result in errors if the user has enabled UI translation, because the new modifiers may not have the expected English names if the UI is translated. So, use the data API to create the modifiers instead of the ops API, and assign nodes to variables instead of getting them by name.
476 lines
18 KiB
Python
476 lines
18 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
# --------------------------- LATTICE ALONG SURFACE -------------------------- #
|
|
# -------------------------------- version 0.3 ------------------------------- #
|
|
# #
|
|
# Automatically generate and assign a lattice that follows the active surface. #
|
|
# #
|
|
# (c) Alessandro Zomparelli #
|
|
# (2017) #
|
|
# #
|
|
# http://www.co-de-it.com/ #
|
|
# #
|
|
# ############################################################################ #
|
|
|
|
import bpy
|
|
import bmesh
|
|
from bpy.types import Operator
|
|
from bpy.props import (BoolProperty, StringProperty, FloatProperty)
|
|
from mathutils import Vector
|
|
|
|
from .utils import *
|
|
|
|
|
|
def not_in(element, grid):
|
|
output = True
|
|
for loop in grid:
|
|
if element in loop:
|
|
output = False
|
|
break
|
|
return output
|
|
|
|
|
|
def grid_from_mesh(mesh, swap_uv):
|
|
bm = bmesh.new()
|
|
bm.from_mesh(mesh)
|
|
verts_grid = []
|
|
edges_grid = []
|
|
faces_grid = []
|
|
|
|
running_grid = True
|
|
while running_grid:
|
|
verts_loop = []
|
|
edges_loop = []
|
|
faces_loop = []
|
|
|
|
# storing first point
|
|
verts_candidates = []
|
|
if len(faces_grid) == 0:
|
|
# for first loop check all vertices
|
|
verts_candidates = bm.verts
|
|
else:
|
|
# for other loops start form the vertices of the first face
|
|
# the last loop, skipping already used vertices
|
|
verts_candidates = [v for v in bm.faces[faces_grid[-1][0]].verts if not_in(v.index, verts_grid)]
|
|
|
|
# check for last loop
|
|
is_last = False
|
|
for vert in verts_candidates:
|
|
if len(vert.link_faces) == 1: # check if corner vertex
|
|
vert.select = True
|
|
verts_loop.append(vert.index)
|
|
is_last = True
|
|
break
|
|
|
|
if not is_last:
|
|
for vert in verts_candidates:
|
|
new_link_faces = [f for f in vert.link_faces if not_in(f.index, faces_grid)]
|
|
if len(new_link_faces) < 2: # check if corner vertex
|
|
vert.select = True
|
|
verts_loop.append(vert.index)
|
|
break
|
|
|
|
running_loop = len(verts_loop) > 0
|
|
|
|
while running_loop:
|
|
bm.verts.ensure_lookup_table()
|
|
id = verts_loop[-1]
|
|
link_edges = bm.verts[id].link_edges
|
|
# storing second point
|
|
if len(verts_loop) == 1: # only one vertex stored in the loop
|
|
if len(faces_grid) == 0: # first loop #
|
|
edge = link_edges[swap_uv] # chose direction
|
|
for vert in edge.verts:
|
|
if vert.index != id:
|
|
vert.select = True
|
|
verts_loop.append(vert.index) # new vertex
|
|
edges_loop.append(edge.index) # chosen edge
|
|
faces_loop.append(edge.link_faces[0].index) # only one face
|
|
# edge.link_faces[0].select = True
|
|
else: # other loops #
|
|
# start from the edges of the first face of the last loop
|
|
for edge in bm.faces[faces_grid[-1][0]].edges:
|
|
# chose an edge starting from the first vertex that is not returning back
|
|
if bm.verts[verts_loop[0]] in edge.verts and \
|
|
bm.verts[verts_grid[-1][0]] not in edge.verts:
|
|
for vert in edge.verts:
|
|
if vert.index != id:
|
|
vert.select = True
|
|
verts_loop.append(vert.index)
|
|
edges_loop.append(edge.index)
|
|
|
|
for face in edge.link_faces:
|
|
if not_in(face.index, faces_grid):
|
|
faces_loop.append(face.index)
|
|
# continuing the loop
|
|
else:
|
|
for edge in link_edges:
|
|
for vert in edge.verts:
|
|
store_data = False
|
|
if not_in(vert.index, verts_grid) and vert.index not in verts_loop:
|
|
if len(faces_loop) > 0:
|
|
bm.faces.ensure_lookup_table()
|
|
if vert not in bm.faces[faces_loop[-1]].verts:
|
|
store_data = True
|
|
else:
|
|
store_data = True
|
|
if store_data:
|
|
vert.select = True
|
|
verts_loop.append(vert.index)
|
|
edges_loop.append(edge.index)
|
|
for face in edge.link_faces:
|
|
if not_in(face.index, faces_grid):
|
|
faces_loop.append(face.index)
|
|
break
|
|
# ending condition
|
|
if verts_loop[-1] == id or verts_loop[-1] == verts_loop[0]:
|
|
running_loop = False
|
|
|
|
verts_grid.append(verts_loop)
|
|
edges_grid.append(edges_loop)
|
|
faces_grid.append(faces_loop)
|
|
|
|
if len(faces_loop) == 0:
|
|
running_grid = False
|
|
bm.free()
|
|
return verts_grid, edges_grid, faces_grid
|
|
|
|
|
|
class lattice_along_surface(Operator):
|
|
bl_idname = "object.lattice_along_surface"
|
|
bl_label = "Lattice along Surface"
|
|
bl_description = ("Automatically add a Lattice modifier to the selected "
|
|
"object, adapting it to the active one.\nThe active "
|
|
"object must be a rectangular grid compatible with the "
|
|
"Lattice's topology")
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
set_parent : BoolProperty(
|
|
name="Set Parent",
|
|
default=True,
|
|
description="Automatically set the Lattice as parent"
|
|
)
|
|
flipNormals : BoolProperty(
|
|
name="Flip Normals",
|
|
default=False,
|
|
description="Flip normals direction"
|
|
)
|
|
swapUV : BoolProperty(
|
|
name="Swap UV",
|
|
default=False,
|
|
description="Flip grid's U and V"
|
|
)
|
|
flipU : BoolProperty(
|
|
name="Flip U",
|
|
default=False,
|
|
description="Flip grid's U")
|
|
|
|
flipV : BoolProperty(
|
|
name="Flip V",
|
|
default=False,
|
|
description="Flip grid's V"
|
|
)
|
|
flipW : BoolProperty(
|
|
name="Flip W",
|
|
default=False,
|
|
description="Flip grid's W"
|
|
)
|
|
use_groups : BoolProperty(
|
|
name="Vertex Group",
|
|
default=False,
|
|
description="Use active Vertex Group for lattice's thickness"
|
|
)
|
|
high_quality_lattice : BoolProperty(
|
|
name="High quality",
|
|
default=True,
|
|
description="Increase the the subdivisions in normal direction for a "
|
|
"more correct result"
|
|
)
|
|
hide_lattice : BoolProperty(
|
|
name="Hide Lattice",
|
|
default=True,
|
|
description="Automatically hide the Lattice object"
|
|
)
|
|
scale_x : FloatProperty(
|
|
name="Scale X",
|
|
default=1,
|
|
min=0.001,
|
|
max=1,
|
|
description="Object scale"
|
|
)
|
|
scale_y : FloatProperty(
|
|
name="Scale Y", default=1,
|
|
min=0.001,
|
|
max=1,
|
|
description="Object scale"
|
|
)
|
|
scale_z : FloatProperty(
|
|
name="Scale Z",
|
|
default=1,
|
|
min=0.001,
|
|
max=1,
|
|
description="Object scale"
|
|
)
|
|
thickness : FloatProperty(
|
|
name="Thickness",
|
|
default=1,
|
|
soft_min=0,
|
|
soft_max=5,
|
|
description="Lattice thickness"
|
|
)
|
|
displace : FloatProperty(
|
|
name="Displace",
|
|
default=0,
|
|
soft_min=-1,
|
|
soft_max=1,
|
|
description="Lattice displace"
|
|
)
|
|
weight_factor : FloatProperty(
|
|
name="Factor",
|
|
default=0,
|
|
min=0.000,
|
|
max=1.000,
|
|
precision=3,
|
|
description="Thickness factor to use for zero vertex group influence"
|
|
)
|
|
grid_object = ""
|
|
source_object = ""
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
try: return context.object.mode == 'OBJECT'
|
|
except: return False
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
col = layout.column(align=True)
|
|
col.label(text="Thickness:")
|
|
col.prop(
|
|
self, "thickness", text="Thickness", icon='NONE', expand=False,
|
|
slider=True, toggle=False, icon_only=False, event=False,
|
|
full_event=False, emboss=True, index=-1
|
|
)
|
|
col.prop(
|
|
self, "displace", text="Offset", icon='NONE', expand=False,
|
|
slider=True, toggle=False, icon_only=False, event=False,
|
|
full_event=False, emboss=True, index=-1
|
|
)
|
|
row = col.row()
|
|
row.prop(self, "use_groups")
|
|
if self.use_groups:
|
|
row = col.row()
|
|
row.prop(self, "weight_factor")
|
|
col.separator()
|
|
col.label(text="Scale:")
|
|
col.prop(
|
|
self, "scale_x", text="U", icon='NONE', expand=False,
|
|
slider=True, toggle=False, icon_only=False, event=False,
|
|
full_event=False, emboss=True, index=-1
|
|
)
|
|
col.prop(
|
|
self, "scale_y", text="V", icon='NONE', expand=False,
|
|
slider=True, toggle=False, icon_only=False, event=False,
|
|
full_event=False, emboss=True, index=-1
|
|
)
|
|
col.separator()
|
|
col.label(text="Flip:")
|
|
row = col.row()
|
|
row.prop(self, "flipU", text="U")
|
|
row.prop(self, "flipV", text="V")
|
|
row.prop(self, "flipW", text="W")
|
|
col.prop(self, "swapUV")
|
|
col.prop(self, "flipNormals")
|
|
col.separator()
|
|
col.label(text="Lattice Options:")
|
|
col.prop(self, "high_quality_lattice")
|
|
col.prop(self, "hide_lattice")
|
|
col.prop(self, "set_parent")
|
|
|
|
def execute(self, context):
|
|
if self.source_object == self.grid_object == "" or True:
|
|
if len(context.selected_objects) != 2:
|
|
self.report({'ERROR'}, "Please, select two objects")
|
|
return {'CANCELLED'}
|
|
grid_obj = context.object
|
|
if grid_obj.type not in ('MESH', 'CURVE', 'SURFACE'):
|
|
self.report({'ERROR'}, "The surface object is not valid. Only Mesh,"
|
|
"Curve and Surface objects are allowed.")
|
|
return {'CANCELLED'}
|
|
obj = None
|
|
for o in context.selected_objects:
|
|
if o.name != grid_obj.name and o.type in \
|
|
('MESH', 'CURVE', 'SURFACE', 'FONT'):
|
|
obj = o
|
|
o.select_set(False)
|
|
break
|
|
try:
|
|
obj_dim = obj.dimensions
|
|
obj_me = simple_to_mesh(obj)#obj.to_mesh(bpy.context.depsgraph, apply_modifiers=True)
|
|
except:
|
|
self.report({'ERROR'}, "The object to deform is not valid. Only "
|
|
"Mesh, Curve, Surface and Font objects are allowed.")
|
|
return {'CANCELLED'}
|
|
self.grid_object = grid_obj.name
|
|
self.source_object = obj.name
|
|
else:
|
|
grid_obj = bpy.data.objects[self.grid_object]
|
|
obj = bpy.data.objects[self.source_object]
|
|
obj_me = simple_to_mesh(obj)# obj.to_mesh(bpy.context.depsgraph, apply_modifiers=True)
|
|
for o in context.selected_objects: o.select_set(False)
|
|
grid_obj.select_set(True)
|
|
context.view_layer.objects.active = grid_obj
|
|
|
|
temp_grid_obj = grid_obj.copy()
|
|
temp_grid_obj.data = simple_to_mesh(grid_obj)
|
|
grid_mesh = temp_grid_obj.data
|
|
for v in grid_mesh.vertices:
|
|
v.co = grid_obj.matrix_world @ v.co
|
|
grid_mesh.calc_normals()
|
|
|
|
if len(grid_mesh.polygons) > 64 * 64:
|
|
bpy.data.objects.remove(temp_grid_obj)
|
|
context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64")
|
|
return {'CANCELLED'}
|
|
|
|
# CREATING LATTICE
|
|
min = Vector((0, 0, 0))
|
|
max = Vector((0, 0, 0))
|
|
first = True
|
|
for v in obj_me.vertices:
|
|
v0 = v.co.copy()
|
|
vert = obj.matrix_world @ v0
|
|
if vert[0] < min[0] or first:
|
|
min[0] = vert[0]
|
|
if vert[1] < min[1] or first:
|
|
min[1] = vert[1]
|
|
if vert[2] < min[2] or first:
|
|
min[2] = vert[2]
|
|
if vert[0] > max[0] or first:
|
|
max[0] = vert[0]
|
|
if vert[1] > max[1] or first:
|
|
max[1] = vert[1]
|
|
if vert[2] > max[2] or first:
|
|
max[2] = vert[2]
|
|
first = False
|
|
|
|
bb = max - min
|
|
lattice_loc = (max + min) / 2
|
|
bpy.ops.object.add(type='LATTICE')
|
|
lattice = context.active_object
|
|
lattice.location = lattice_loc
|
|
lattice.scale = Vector((bb.x / self.scale_x, bb.y / self.scale_y,
|
|
bb.z / self.scale_z))
|
|
|
|
if bb.x == 0:
|
|
lattice.scale.x = 1
|
|
if bb.y == 0:
|
|
lattice.scale.y = 1
|
|
if bb.z == 0:
|
|
lattice.scale.z = 1
|
|
|
|
context.view_layer.objects.active = obj
|
|
lattice_modifier = context.object.modifiers.new("", 'LATTICE')
|
|
lattice_modifier.object = lattice
|
|
|
|
# set as parent
|
|
if self.set_parent:
|
|
override = {'active_object': obj, 'selected_objects' : [lattice,obj]}
|
|
bpy.ops.object.parent_set(override, type='OBJECT', keep_transform=False)
|
|
|
|
# reading grid structure
|
|
verts_grid, edges_grid, faces_grid = grid_from_mesh(
|
|
grid_mesh,
|
|
swap_uv=self.swapUV
|
|
)
|
|
nu = len(verts_grid)
|
|
nv = len(verts_grid[0])
|
|
nw = 2
|
|
scale_normal = self.thickness
|
|
|
|
try:
|
|
lattice.data.points_u = nu
|
|
lattice.data.points_v = nv
|
|
lattice.data.points_w = nw
|
|
if self.use_groups:
|
|
vg = temp_grid_obj.vertex_groups.active
|
|
weight_factor = self.weight_factor
|
|
for i in range(nu):
|
|
for j in range(nv):
|
|
for w in range(nw):
|
|
if self.use_groups:
|
|
try:
|
|
weight_influence = vg.weight(verts_grid[i][j])
|
|
except:
|
|
weight_influence = 0
|
|
weight_influence = weight_influence * (1 - weight_factor) + weight_factor
|
|
displace = weight_influence * scale_normal * bb.z
|
|
else:
|
|
displace = scale_normal * bb.z
|
|
target_point = (grid_mesh.vertices[verts_grid[i][j]].co +
|
|
grid_mesh.vertices[verts_grid[i][j]].normal *
|
|
(w + self.displace / 2 - 0.5) * displace) - lattice.location
|
|
if self.flipW:
|
|
w = 1 - w
|
|
if self.flipU:
|
|
i = nu - i - 1
|
|
if self.flipV:
|
|
j = nv - j - 1
|
|
|
|
lattice.data.points[i + j * nu + w * nu * nv].co_deform.x = \
|
|
target_point.x / bpy.data.objects[lattice.name].scale.x
|
|
lattice.data.points[i + j * nu + w * nu * nv].co_deform.y = \
|
|
target_point.y / bpy.data.objects[lattice.name].scale.y
|
|
lattice.data.points[i + j * nu + w * nu * nv].co_deform.z = \
|
|
target_point.z / bpy.data.objects[lattice.name].scale.z
|
|
|
|
except:
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
temp_grid_obj.select_set(True)
|
|
lattice.select_set(True)
|
|
obj.select_set(False)
|
|
bpy.ops.object.delete(use_global=False)
|
|
context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
bpy.ops.object.modifier_remove(modifier=lattice_modifier.name)
|
|
if nu > 64 or nv > 64:
|
|
self.report({'ERROR'}, "Maximum resolution allowed for Lattice is 64")
|
|
return {'CANCELLED'}
|
|
else:
|
|
self.report({'ERROR'}, "The grid mesh is not correct")
|
|
return {'CANCELLED'}
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
#grid_obj.select_set(True)
|
|
#lattice.select_set(False)
|
|
obj.select_set(False)
|
|
#bpy.ops.object.delete(use_global=False)
|
|
context.view_layer.objects.active = lattice
|
|
lattice.select_set(True)
|
|
|
|
if self.high_quality_lattice:
|
|
context.object.data.points_w = 8
|
|
else:
|
|
context.object.data.use_outside = True
|
|
|
|
if self.hide_lattice:
|
|
bpy.ops.object.hide_view_set(unselected=False)
|
|
|
|
context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
lattice.select_set(False)
|
|
|
|
if self.flipNormals:
|
|
try:
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
bpy.ops.mesh.flip_normals()
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
except:
|
|
pass
|
|
bpy.data.meshes.remove(grid_mesh)
|
|
bpy.data.meshes.remove(obj_me)
|
|
|
|
return {'FINISHED'}
|