blender/release/scripts/modules/retopo.py
Campbell Barton 08c9ecb3b0 RNA/Py API
change how data is added. eg.
 bpy.data.add_mesh(name) --> bpy.data.meshes.new(name)
 bpy.data.remove_lamp(lamp) --> bpy.data.lamps.remove(lamp)


image and texture stil use add_* funcs
2010-01-09 23:44:01 +00:00

507 lines
14 KiB
Python

# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
import bpy
EPS_SPLINE_DIV = 15.0 # remove doubles is ~15th the length of the spline
ANGLE_JOIN_LIMIT = 25.0 # limit for joining splines into 1.
def get_hub(co, _hubs, EPS_SPLINE):
if 1:
for hub in _hubs.values():
if (hub.co - co).length < EPS_SPLINE:
return hub
key = co.toTuple(3)
hub = _hubs[key] = Hub(co, key, len(_hubs))
return hub
else:
pass
'''
key = co.toTuple(3)
try:
return _hubs[key]
except:
hub = _hubs[key] = Hub(co, key, len(_hubs))
return hub
'''
class Hub(object):
__slots__ = "co", "key", "index", "links"
def __init__(self, co, key, index):
self.co = co.copy()
self.key = key
self.index = index
self.links = []
def get_weight(self):
f = 0.0
for hub_other in self.links:
f += (self.co - hub_other.co).length
def replace(self, other):
for hub in self.links:
try:
hub.links.remove(self)
except:
pass
if other not in hub.links:
hub.links.append(other)
def dist(self, other):
return (self.co - other.co).length
def calc_faces(self, hub_ls):
faces = []
# first tris
for l_a in self.links:
for l_b in l_a.links:
if l_b is not self and l_b in self.links:
# will give duplicates
faces.append((self.index, l_a.index, l_b.index))
# now quads, check which links share 2 different verts directly
def validate_quad(face):
if len(set(face)) != len(face):
return False
if hub_ls[face[0]] in hub_ls[face[2]].links:
return False
if hub_ls[face[2]] in hub_ls[face[0]].links:
return False
if hub_ls[face[1]] in hub_ls[face[3]].links:
return False
if hub_ls[face[3]] in hub_ls[face[1]].links:
return False
return True
for i, l_a in enumerate(self.links):
links_a = {l.index for l in l_a.links}
for j in range(i):
l_b = self.links[j]
links_b = {l.index for l in l_b.links}
isect = links_a.intersection(links_b)
if len(isect) == 2:
isect = list(isect)
# check there are no diagonal lines
face = (isect[0], l_a.index, isect[1], l_b.index)
if validate_quad(face):
faces.append(face)
return faces
class BBox(object):
__slots__ = "xmin", "ymin", "zmin", "xmax", "ymax", "zmax"
def __init__(self):
self.xmin = self.ymin = self.zmin = 100000000.0
self.xmax = self.ymax = self.zmax = -100000000.0
@property
def xdim(self):
return self.xmax - self.xmin
@property
def ydim(self):
return self.ymax - self.ymin
@property
def zdim(self):
return self.zmax - self.zmin
def calc(self, points):
xmin = ymin = zmin = 100000000.0
xmax = ymax = zmax = -100000000.0
for pt in points:
x, y, z = pt
if x < xmin:
xmin = x
if y < ymin:
ymin = y
if z < zmin:
zmin = z
if x > xmax:
xmax = x
if y > ymax:
ymax = y
if z > zmax:
zmax = z
self.xmin, self.ymin, self.zmin = xmin, ymin, zmin
self.xmax, self.ymax, self.zmax = xmax, ymax, zmax
def xsect(self, other, margin=0.0):
if margin == 0.0:
if self.xmax < other.xmin:
return False
if self.ymax < other.ymin:
return False
if self.zmax < other.zmin:
return False
if self.xmin > other.xmax:
return False
if self.ymin > other.ymax:
return False
if self.zmin > other.zmax:
return False
else:
xmargin = ((self.xdim + other.xdim) / 2.0) * margin
ymargin = ((self.ydim + other.ydim) / 2.0) * margin
zmargin = ((self.zdim + other.zdim) / 2.0) * margin
if self.xmax < other.xmin - xmargin:
return False
if self.ymax < other.ymin - ymargin:
return False
if self.zmax < other.zmin - zmargin:
return False
if self.xmin > other.xmax + xmargin:
return False
if self.ymin > other.ymax + ymargin:
return False
if self.zmin > other.zmax + zmargin:
return False
return True
def __iadd__(self, other):
self.xmin = min(self.xmin, other.xmin)
self.ymin = min(self.ymin, other.ymin)
self.zmin = min(self.zmin, other.zmin)
self.xmax = max(self.xmax, other.xmax)
self.ymax = max(self.ymax, other.ymax)
self.zmax = max(self.zmax, other.zmax)
return self
class Spline(object):
__slots__ = "points", "hubs", "length", "bb"
def __init__(self, points):
self.points = points
self.hubs = []
self.calc_length()
self.bb = BBox()
self.bb.calc(points)
def calc_length(self):
# calc length
f = 0.0
co_prev = self.points[0]
for co in self.points[1:]:
f += (co - co_prev).length
co_prev = co
self.length = f
def link(self):
if len(self.hubs) < 2:
return
edges = list(set([i for i, hub in self.hubs]))
edges.sort()
edges_order = {}
for i in edges:
edges_order[i] = []
# self.hubs.sort()
for i, hub in self.hubs:
edges_order[i].append(hub)
hubs_order = []
for i in edges:
ls = edges_order[i]
edge_start = self.points[i]
ls.sort(key=lambda hub: (hub.co - edge_start).length)
hubs_order.extend(ls)
# Now we have the order, connect the hubs
hub_prev = hubs_order[0]
for hub in hubs_order[1:]:
hub.links.append(hub_prev)
hub_prev.links.append(hub)
hub_prev = hub
def get_points(stroke):
return [point.co.copy() for point in stroke.points]
def get_splines(gp):
l = None
for l in gp.layers:
if l.active: # XXX - should be layers.active
break
if l:
frame = l.active_frame
return [Spline(get_points(stroke)) for stroke in frame.strokes]
else:
return []
def xsect_spline(sp_a, sp_b, _hubs):
from Mathutils import LineIntersect
from Mathutils import MidpointVecs
from Geometry import ClosestPointOnLine
pt_a_prev = pt_b_prev = None
EPS_SPLINE = (sp_a.length + sp_b.length) / (EPS_SPLINE_DIV * 2)
pt_a_prev = sp_a.points[0]
for a, pt_a in enumerate(sp_a.points[1:]):
pt_b_prev = sp_b.points[0]
for b, pt_b in enumerate(sp_b.points[1:]):
# Now we have 2 edges
# print(pt_a, pt_a_prev, pt_b, pt_b_prev)
xsect = LineIntersect(pt_a, pt_a_prev, pt_b, pt_b_prev)
if xsect is not None:
if (xsect[0] - xsect[1]).length <= EPS_SPLINE:
f = ClosestPointOnLine(xsect[1], pt_a, pt_a_prev)[1]
# if f >= 0.0-EPS_SPLINE and f <= 1.0+EPS_SPLINE: # for some reason doesnt work so well, same below
if f >= 0.0 and f <= 1.0:
f = ClosestPointOnLine(xsect[0], pt_b, pt_b_prev)[1]
# if f >= 0.0-EPS_SPLINE and f <= 1.0+EPS_SPLINE:
if f >= 0.0 and f <= 1.0:
# This wont happen often
co = MidpointVecs(xsect[0], xsect[1])
hub = get_hub(co, _hubs, EPS_SPLINE)
sp_a.hubs.append((a, hub))
sp_b.hubs.append((b, hub))
pt_b_prev = pt_b
pt_a_prev = pt_a
def connect_splines(splines):
HASH_PREC = 8
from Mathutils import AngleBetweenVecs
from math import radians
ANG_LIMIT = radians(ANGLE_JOIN_LIMIT)
def sort_pair(a, b):
if a < b:
return a, b
else:
return b, a
#def test_join(p1a, p1b, p2a, p2b, length_average):
def test_join(s1, s2, dir1, dir2, length_average):
if dir1 is False:
p1a = s1.points[0]
p1b = s1.points[1]
else:
p1a = s1.points[-1]
p1b = s1.points[-2]
if dir2 is False:
p2a = s2.points[0]
p2b = s2.points[1]
else:
p2a = s2.points[-1]
p2b = s2.points[-2]
# compare length between tips
if (p1a - p2a).length > (length_average / EPS_SPLINE_DIV):
return False
v1 = p1a - p1b
v2 = p2b - p2a
if AngleBetweenVecs(v1, v2) > ANG_LIMIT:
return False
# print("joining!")
return True
# lazy, hash the points that have been compared.
comparisons = set()
do_join = True
while do_join:
do_join = False
for i, s1 in enumerate(splines):
key1a = s1.points[0].toTuple(HASH_PREC)
key1b = s1.points[-1].toTuple(HASH_PREC)
for j, s2 in enumerate(splines):
if s1 is s2:
continue
length_average = min(s1.length, s2.length)
key2a = s2.points[0].toTuple(HASH_PREC)
key2b = s2.points[-1].toTuple(HASH_PREC)
# there are 4 ways this may be joined
key_pair = sort_pair(key1a, key2a)
if key_pair not in comparisons:
comparisons.add(key_pair)
if test_join(s1, s2, False, False, length_average):
s1.points[:0] = reversed(s2.points)
s1.bb += s2.bb
s1.calc_length()
del splines[j]
do_join = True
break
key_pair = sort_pair(key1a, key2b)
if key_pair not in comparisons:
comparisons.add(key_pair)
if test_join(s1, s2, False, True, length_average):
s1.points[:0] = s2.points
s1.bb += s2.bb
s1.calc_length()
del splines[j]
do_join = True
break
key_pair = sort_pair(key1b, key2b)
if key_pair not in comparisons:
comparisons.add(key_pair)
if test_join(s1, s2, True, True, length_average):
s1.points += list(reversed(s2.points))
s1.bb += s2.bb
s1.calc_length()
del splines[j]
do_join = True
break
key_pair = sort_pair(key1b, key2a)
if key_pair not in comparisons:
comparisons.add(key_pair)
if test_join(s1, s2, True, False, length_average):
s1.points += s2.points
s1.bb += s2.bb
s1.calc_length()
del splines[j]
do_join = True
break
if do_join:
break
def calculate(gp):
splines = get_splines(gp)
# spline endpoints may be co-linear, join these into single splines
connect_splines(splines)
_hubs = {}
for i, sp in enumerate(splines):
for j, sp_other in enumerate(splines):
if j <= i:
continue
if sp.bb.xsect(sp_other.bb, margin=0.1):
xsect_spline(sp, sp_other, _hubs)
for sp in splines:
sp.link()
# remove these
hubs_ls = [hub for hub in _hubs.values() if hub.index != -1]
_hubs.clear()
_hubs = None
for i, hub in enumerate(hubs_ls):
hub.index = i
# Now we have connected hubs, write all edges!
def order(i1, i2):
if i1 > i2:
return i2, i1
return i1, i2
edges = {}
for hub in hubs_ls:
i1 = hub.index
for hub_other in hub.links:
i2 = hub_other.index
edges[order(i1, i2)] = None
verts = []
edges = edges.keys()
faces = []
for hub in hubs_ls:
verts.append(hub.co)
faces.extend(hub.calc_faces(hubs_ls))
# remove double faces
faces = dict([(tuple(sorted(f)), f) for f in faces]).values()
mesh = bpy.data.meshes.new("Retopo")
mesh.from_pydata(verts, [], faces)
scene = bpy.context.scene
mesh.update()
obj_new = bpy.data.objects.new("Torus", 'MESH')
obj_new.data = mesh
scene.objects.link(obj_new)
return obj_new
def main():
scene = bpy.context.scene
obj = bpy.context.object
gp = None
if obj:
gp = obj.grease_pencil
if not gp:
gp = scene.grease_pencil
if not gp:
raise Exception("no active grease pencil")
obj_new = calculate(gp)
scene.objects.active = obj_new
obj_new.selected = True
# nasty, recalc normals
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
bpy.ops.mesh.normals_make_consistent(inside=False)
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)