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'}
|