diff options
author | Campbell Barton <ideasman42@gmail.com> | 2009-11-04 17:40:35 +0300 |
---|---|---|
committer | Campbell Barton <ideasman42@gmail.com> | 2009-11-04 17:40:35 +0300 |
commit | 4033aba579e4fc44a89bf04c90cbbf299f3de1f8 (patch) | |
tree | 2fbb432b31d6e987589d47a2077098bb9d34b115 /release/scripts/op | |
parent | 69aaa43c2484de5570fa7e50a7764cc1df8589f1 (diff) |
new operator directory, move some scripts from io
Diffstat (limited to 'release/scripts/op')
-rw-r--r-- | release/scripts/op/add_mesh_torus.py | 139 | ||||
-rw-r--r-- | release/scripts/op/mesh_skin.py | 664 | ||||
-rw-r--r-- | release/scripts/op/uvcalc_smart_project.py | 1163 | ||||
-rw-r--r-- | release/scripts/op/vertexpaint_dirt.py | 178 |
4 files changed, 2144 insertions, 0 deletions
diff --git a/release/scripts/op/add_mesh_torus.py b/release/scripts/op/add_mesh_torus.py new file mode 100644 index 00000000000..ddc4f0b9770 --- /dev/null +++ b/release/scripts/op/add_mesh_torus.py @@ -0,0 +1,139 @@ +# ##### 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-80 compliant> +import bpy +import Mathutils +from math import cos, sin, pi + + +def add_torus(major_rad, minor_rad, major_seg, minor_seg): + Vector = Mathutils.Vector + Quaternion = Mathutils.Quaternion + + PI_2 = pi * 2 + z_axis = (0, 0, 1) + + verts = [] + faces = [] + i1 = 0 + tot_verts = major_seg * minor_seg + for major_index in range(major_seg): + quat = Quaternion(z_axis, (major_index / major_seg) * PI_2) + + for minor_index in range(minor_seg): + angle = 2 * pi * minor_index / minor_seg + + vec = Vector(major_rad + (cos(angle) * minor_rad), 0.0, + (sin(angle) * minor_rad)) * quat + + verts.extend([vec.x, vec.y, vec.z]) + + if minor_index + 1 == minor_seg: + i2 = (major_index) * minor_seg + i3 = i1 + minor_seg + i4 = i2 + minor_seg + + else: + i2 = i1 + 1 + i3 = i1 + minor_seg + i4 = i3 + 1 + + if i2 >= tot_verts: + i2 = i2 - tot_verts + if i3 >= tot_verts: + i3 = i3 - tot_verts + if i4 >= tot_verts: + i4 = i4 - tot_verts + + # stupid eekadoodle + if i2: + faces.extend([i1, i3, i4, i2]) + else: + faces.extend([i2, i1, i3, i4]) + + i1 += 1 + + return verts, faces + +from bpy.props import * + + +class AddTorus(bpy.types.Operator): + '''Add a torus mesh.''' + bl_idname = "mesh.primitive_torus_add" + bl_label = "Add Torus" + bl_register = True + bl_undo = True + + major_radius = FloatProperty(name="Major Radius", + description="Number of segments for the main ring of the torus", + default=1.0, min=0.01, max=100.0) + minor_radius = FloatProperty(name="Minor Radius", + description="Number of segments for the minor ring of the torus", + default=0.25, min=0.01, max=100.0) + major_segments = IntProperty(name="Major Segments", + description="Number of segments for the main ring of the torus", + default=48, min=3, max=256) + minor_segments = IntProperty(name="Minor Segments", + description="Number of segments for the minor ring of the torus", + default=16, min=3, max=256) + + def execute(self, context): + + verts_loc, faces = add_torus(self.major_radius, + self.minor_radius, + self.major_segments, + self.minor_segments) + + mesh = bpy.data.add_mesh("Torus") + + mesh.add_geometry(int(len(verts_loc) / 3), 0, int(len(faces) / 4)) + mesh.verts.foreach_set("co", verts_loc) + mesh.faces.foreach_set("verts_raw", faces) + + scene = context.scene + + # ugh + for ob in scene.objects: + ob.selected = False + + mesh.update() + ob_new = bpy.data.add_object('MESH', "Torus") + ob_new.data = mesh + scene.add_object(ob_new) + scene.objects.active = ob_new + ob_new.selected = True + + ob_new.location = tuple(context.scene.cursor_location) + + return ('FINISHED',) + +# Register the operator +bpy.ops.add(AddTorus) + +# Add to a menu +import dynamic_menu + +menu_func = (lambda self, context: self.layout.itemO(AddTorus.bl_idname, + text="Torus", icon='ICON_MESH_DONUT')) + +menu_item = dynamic_menu.add(bpy.types.INFO_MT_mesh_add, menu_func) + +if __name__ == "__main__": + bpy.ops.mesh.primitive_torus_add() diff --git a/release/scripts/op/mesh_skin.py b/release/scripts/op/mesh_skin.py new file mode 100644 index 00000000000..04543cbef3a --- /dev/null +++ b/release/scripts/op/mesh_skin.py @@ -0,0 +1,664 @@ +# ##### 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 ##### + +# import Blender +import time, functools +import bpy +# from Blender import Window +from Mathutils import MidpointVecs, Vector +from Mathutils import AngleBetweenVecs as _AngleBetweenVecs_ +# import BPyMessages + +# from Blender.Draw import PupMenu + +BIG_NUM = 1<<30 + +global CULL_METHOD +CULL_METHOD = 0 + +def AngleBetweenVecs(a1,a2): + import math + try: + return math.degrees(_AngleBetweenVecs_(a1,a2)) + except: + return 180.0 + +class edge(object): + __slots__ = 'v1', 'v2', 'co1', 'co2', 'length', 'removed', 'match', 'cent', 'angle', 'next', 'prev', 'normal', 'fake' + def __init__(self, v1,v2): + self.v1 = v1 + self.v2 = v2 + co1, co2= v1.co, v2.co + self.co1= co1 + self.co2= co2 + + # uv1 uv2 vcol1 vcol2 # Add later + self.length = (co1 - co2).length + self.removed = 0 # Have we been culled from the eloop + self.match = None # The other edge were making a face with + + self.cent= MidpointVecs(co1, co2) + self.angle= 0.0 + self.fake= False + +class edgeLoop(object): + __slots__ = 'centre', 'edges', 'normal', 'closed', 'backup_edges' + def __init__(self, loop, me, closed): # Vert loop + # Use next and prev, nextDist, prevDist + + # Get Loops centre. + fac= len(loop) + verts = me.verts + self.centre= functools.reduce(lambda a,b: a+verts[b].co/fac, loop, Vector()) + + # Convert Vert loop to Edges. + self.edges = [edge(verts[loop[vIdx-1]], verts[loop[vIdx]]) for vIdx in range(len(loop))] + + if not closed: + self.edges[0].fake = True # fake edge option + + self.closed = closed + + + # Assign linked list + for eIdx in range(len(self.edges)-1): + self.edges[eIdx].next = self.edges[eIdx+1] + self.edges[eIdx].prev = self.edges[eIdx-1] + # Now last + self.edges[-1].next = self.edges[0] + self.edges[-1].prev = self.edges[-2] + + + + # GENERATE AN AVERAGE NORMAL FOR THE WHOLE LOOP. + self.normal = Vector() + for e in self.edges: + n = (self.centre-e.co1).cross(self.centre-e.co2) + # Do we realy need tot normalize? + n.normalize() + self.normal += n + + # Generate the angle + va= e.cent - e.prev.cent + vb= e.next.cent - e.cent + + e.angle= AngleBetweenVecs(va, vb) + + # Blur the angles + #for e in self.edges: + # e.angle= (e.angle+e.next.angle)/2 + + # Blur the angles + #for e in self.edges: + # e.angle= (e.angle+e.prev.angle)/2 + + self.normal.normalize() + + # Generate a normal for each edge. + for e in self.edges: + + n1 = e.co1 + n2 = e.co2 + n3 = e.prev.co1 + + a = n1-n2 + b = n1-n3 + normal1 = a.cross(b) + normal1.normalize() + + n1 = e.co2 + n3 = e.next.co2 + n2 = e.co1 + + a = n1-n2 + b = n1-n3 + + normal2 = a.cross(b) + normal2.normalize() + + # Reuse normal1 var + normal1 += normal1 + normal2 + normal1.normalize() + + e.normal = normal1 + #print e.normal + + + + def backup(self): + # Keep a backup of the edges + self.backup_edges = self.edges[:] + + def restore(self): + self.edges = self.backup_edges[:] + for e in self.edges: + e.removed = 0 + + def reverse(self): + self.edges.reverse() + self.normal.negate() + + for e in self.edges: + e.normal.negate() + e.v1, e.v2 = e.v2, e.v1 + e.co1, e.co2 = e.co2, e.co1 + e.next, e.prev = e.prev, e.next + + + def removeSmallest(self, cullNum, otherLoopLen): + ''' + Removes N Smallest edges and backs up the loop, + this is so we can loop between 2 loops as if they are the same length, + backing up and restoring incase the loop needs to be skinned with another loop of a different length. + ''' + global CULL_METHOD + if CULL_METHOD == 1: # Shortest edge + eloopCopy = self.edges[:] + + # Length sort, smallest first + try: eloopCopy.sort(key = lambda e1: e1.length) + except: eloopCopy.sort(lambda e1, e2: cmp(e1.length, e2.length )) + + # Dont use atm + #eloopCopy.sort(lambda e1, e2: cmp(e1.angle*e1.length, e2.angle*e2.length)) # Length sort, smallest first + #eloopCopy.sort(lambda e1, e2: cmp(e1.angle, e2.angle)) # Length sort, smallest first + + remNum = 0 + for i, e in enumerate(eloopCopy): + if not e.fake: + e.removed = 1 + self.edges.remove( e ) # Remove from own list, still in linked list. + remNum += 1 + + if not remNum < cullNum: + break + + else: # CULL METHOD is even + + culled = 0 + + step = int(otherLoopLen / float(cullNum)) * 2 + + currentEdge = self.edges[0] + while culled < cullNum: + + # Get the shortest face in the next STEP + step_count= 0 + bestAng= 360.0 + smallestEdge= None + while step_count<=step or smallestEdge==None: + step_count+=1 + if not currentEdge.removed: # 0 or -1 will not be accepted + if currentEdge.angle<bestAng and not currentEdge.fake: + smallestEdge= currentEdge + bestAng= currentEdge.angle + + currentEdge = currentEdge.next + + # In that stepping length we have the smallest edge.remove it + smallestEdge.removed = 1 + self.edges.remove(smallestEdge) + + # Start scanning from the edge we found? - result is over fanning- no good. + #currentEdge= smallestEdge.next + + culled+=1 + + +# Returns face edges. +# face must have edge data. + +# Utility funcs for 2.5, make into a module?? +def ord_ind(i1,i2): + if i1<i2: return i1,i2 + return i2,i1 + +def edge_key(ed): + v1,v2 = tuple(ed.verts) + return ord_ind(v1, v2) + +def face_edge_keys(f): + verts = tuple(f.verts) + if len(verts)==3: + return ord_ind(verts[0], verts[1]), ord_ind(verts[1], verts[2]), ord_ind(verts[2], verts[0]) + + return ord_ind(verts[0], verts[1]), ord_ind(verts[1], verts[2]), ord_ind(verts[2], verts[3]), ord_ind(verts[3], verts[0]) + +def mesh_faces_extend(me, faces, mat_idx = 0): + orig_facetot = len(me.faces) + new_facetot = len(faces) + me.add_geometry(0, 0, new_facetot) + tot = orig_facetot+new_facetot + me_faces = me.faces + i= 0 + while i < new_facetot: + + f = [v.index for v in faces[i]] + if len(f)==4 and f[3]==0: + f = f[1], f[2], f[3], f[0] + + mf = me_faces[orig_facetot+i] + mf.verts_raw = f + mf.material_index = mat_idx + i+=1 +# end utils + + +def getSelectedEdges(context, me, ob): + MESH_MODE= context.scene.tool_settings.mesh_selection_mode + + if MESH_MODE in ('EDGE', 'VERTEX'): + context.scene.tool_settings.mesh_selection_mode = 'EDGE' + edges= [ ed for ed in me.edges if ed.selected ] + # print len(edges), len(me.edges) + context.scene.tool_settings.mesh_selection_mode = MESH_MODE + return edges + + if MESH_MODE == 'FACE': + context.scene.tool_settings.mesh_selection_mode = 'EDGE' + # value is [edge, face_sel_user_in] + edge_dict= dict((edge_key(ed), [ed, 0]) for ed in me.edges) + + for f in me.faces: + if f.sel: + for edkey in face_edge_keys(f): + edge_dict[edkey][1] += 1 + + context.scene.tool_settings.mesh_selection_mode = MESH_MODE + return [ ed_data[0] for ed_data in edge_dict.itervalues() if ed_data[1] == 1 ] + + + +def getVertLoops(selEdges, me): + ''' + return a list of vert loops, closed and open [(loop, closed)...] + ''' + + mainVertLoops = [] + # second method + tot = len(me.verts) + vert_siblings = [[] for i in range(tot)] + vert_used = [False] * tot + + for ed in selEdges: + i1, i2 = edge_key(ed) + vert_siblings[i1].append(i2) + vert_siblings[i2].append(i1) + + # find the first used vert and keep looping. + for i in range(tot): + if vert_siblings[i] and not vert_used[i]: + sbl = vert_siblings[i] # siblings + + if len(sbl) > 2: + return None + + vert_used[i] = True + + # do an edgeloop seek + if len(sbl) == 2: + contextVertLoop= [sbl[0], i, sbl[1]] # start the vert loop + vert_used[contextVertLoop[ 0]] = True + vert_used[contextVertLoop[-1]] = True + else: + contextVertLoop= [i, sbl[0]] + vert_used[contextVertLoop[ 1]] = True + + # Always seek up + ok = True + while ok: + ok = False + closed = False + sbl = vert_siblings[contextVertLoop[-1]] + if len(sbl) == 2: + next = sbl[not sbl.index( contextVertLoop[-2] )] + if vert_used[next]: + closed = True + # break + else: + contextVertLoop.append( next ) # get the vert that isnt the second last + vert_used[next] = True + ok = True + + # Seek down as long as the starting vert was not at the edge. + if not closed and len(vert_siblings[i]) == 2: + + ok = True + while ok: + ok = False + sbl = vert_siblings[contextVertLoop[0]] + if len(sbl) == 2: + next = sbl[not sbl.index( contextVertLoop[1] )] + if vert_used[next]: + closed = True + else: + contextVertLoop.insert(0, next) # get the vert that isnt the second last + vert_used[next] = True + ok = True + + mainVertLoops.append((contextVertLoop, closed)) + + + verts = me.verts + # convert from indicies to verts + # mainVertLoops = [([verts[i] for i in contextVertLoop], closed) for contextVertLoop, closed in mainVertLoops] + # print len(mainVertLoops) + return mainVertLoops + + + +def skin2EdgeLoops(eloop1, eloop2, me, ob, MODE): + + new_faces= [] # + + # Make sure e1 loops is bigger then e2 + if len(eloop1.edges) != len(eloop2.edges): + if len(eloop1.edges) < len(eloop2.edges): + eloop1, eloop2 = eloop2, eloop1 + + eloop1.backup() # were about to cull faces + CULL_FACES = len(eloop1.edges) - len(eloop2.edges) + eloop1.removeSmallest(CULL_FACES, len(eloop1.edges)) + else: + CULL_FACES = 0 + # First make sure poly vert loops are in sync with eachother. + + # The vector allong which we are skinning. + skinVector = eloop1.centre - eloop2.centre + + loopDist = skinVector.length + + # IS THE LOOP FLIPPED, IF SO FLIP BACK. we keep it flipped, its ok, + if eloop1.closed or eloop2.closed: + angleBetweenLoopNormals = AngleBetweenVecs(eloop1.normal, eloop2.normal) + if angleBetweenLoopNormals > 90: + eloop2.reverse() + + + DIR= eloop1.centre - eloop2.centre + + # if eloop2.closed: + bestEloopDist = BIG_NUM + bestOffset = 0 + # Loop rotation offset to test.1 + eLoopIdxs = list(range(len(eloop1.edges))) + for offset in range(len(eloop1.edges)): + totEloopDist = 0 # Measure this total distance for thsi loop. + + offsetIndexLs = eLoopIdxs[offset:] + eLoopIdxs[:offset] # Make offset index list + + + # e1Idx is always from 0uu to N, e2Idx is offset. + for e1Idx, e2Idx in enumerate(offsetIndexLs): + e1= eloop1.edges[e1Idx] + e2= eloop2.edges[e2Idx] + + + # Include fan connections in the measurement. + OK= True + while OK or e1.removed: + OK= False + + # Measure the vloop distance =============== + diff= ((e1.cent - e2.cent).length) #/ nangle1 + + ed_dir= e1.cent-e2.cent + a_diff= AngleBetweenVecs(DIR, ed_dir)/18 # 0 t0 18 + + totEloopDist += (diff * (1+a_diff)) / (1+loopDist) + + # Premeture break if where no better off + if totEloopDist > bestEloopDist: + break + + e1=e1.next + + if totEloopDist < bestEloopDist: + bestOffset = offset + bestEloopDist = totEloopDist + + # Modify V2 LS for Best offset + eloop2.edges = eloop2.edges[bestOffset:] + eloop2.edges[:bestOffset] + + else: + # Both are open loops, easier to calculate. + + + # Make sure the fake edges are at the start. + for i, edloop in enumerate((eloop1, eloop2)): + # print "LOOPO" + if edloop.edges[0].fake: + # alredy at the start + #print "A" + pass + elif edloop.edges[-1].fake: + # put the end at the start + edloop.edges.insert(0, edloop.edges.pop()) + #print "B" + + else: + for j, ed in enumerate(edloop.edges): + if ed.fake: + #print "C" + edloop.edges = edloop.edges = edloop.edges[j:] + edloop.edges[:j] + break + # print "DONE" + ed1, ed2 = eloop1.edges[0], eloop2.edges[0] + + if not ed1.fake or not ed2.fake: + raise "Error" + + # Find the join that isnt flipped (juts like detecting a bow-tie face) + a1 = (ed1.co1 - ed2.co1).length + (ed1.co2 - ed2.co2).length + a2 = (ed1.co1 - ed2.co2).length + (ed1.co2 - ed2.co1).length + + if a1 > a2: + eloop2.reverse() + # make the first edge the start edge still + eloop2.edges.insert(0, eloop2.edges.pop()) + + + + + for loopIdx in range(len(eloop2.edges)): + e1 = eloop1.edges[loopIdx] + e2 = eloop2.edges[loopIdx] + + # Remember the pairs for fan filling culled edges. + e1.match = e2; e2.match = e1 + + if not (e1.fake or e2.fake): + new_faces.append([e1.v1, e1.v2, e2.v2, e2.v1]) + + # FAN FILL MISSING FACES. + if CULL_FACES: + # Culled edges will be in eloop1. + FAN_FILLED_FACES = 0 + + contextEdge = eloop1.edges[0] # The larger of teh 2 + while FAN_FILLED_FACES < CULL_FACES: + while contextEdge.next.removed == 0: + contextEdge = contextEdge.next + + vertFanPivot = contextEdge.match.v2 + + while contextEdge.next.removed == 1: + #if not contextEdge.next.fake: + new_faces.append([contextEdge.next.v1, contextEdge.next.v2, vertFanPivot]) + + # Should we use another var?, this will work for now. + contextEdge.next.removed = 1 + + contextEdge = contextEdge.next + FAN_FILLED_FACES += 1 + + # may need to fan fill backwards 1 for non closed loops. + + eloop1.restore() # Add culled back into the list. + + return new_faces + +def main(context): + global CULL_METHOD + + ob = context.object + + is_editmode = (ob.mode=='EDIT') + if is_editmode: bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + if ob == None or ob.type != 'MESH': + raise Exception("BPyMessages.Error_NoMeshActive()") + return + + me = ob.data + + time1 = time.time() + selEdges = getSelectedEdges(context, me, ob) + vertLoops = getVertLoops(selEdges, me) # list of lists of edges. + if vertLoops == None: + raise Exception('Error%t|Selection includes verts that are a part of more then 1 loop') + if is_editmode: bpy.ops.object.mode_set(mode='EDIT', toggle=False) + return + # print len(vertLoops) + + + if len(vertLoops) > 2: + choice = PupMenu('Loft '+str(len(vertLoops))+' edge loops%t|loop|segment') + if choice == -1: + if is_editmode: bpy.ops.object.mode_set(mode='EDIT', toggle=False) + return + + elif len(vertLoops) < 2: + raise Exception('Error%t|No Vertloops found!') + if is_editmode: bpy.ops.object.mode_set(mode='EDIT', toggle=False) + return + else: + choice = 2 + + + # The line below checks if any of the vert loops are differenyt in length. + if False in [len(v[0]) == len(vertLoops[0][0]) for v in vertLoops]: + CULL_METHOD = PupMenu('Small to large edge loop distrobution method%t|remove edges evenly|remove smallest edges') + if CULL_METHOD == -1: + if is_editmode: Window.EditMode(1) + return + + if CULL_METHOD ==1: # RESET CULL_METHOD + CULL_METHOD = 0 # shortest + else: + CULL_METHOD = 1 # even + + + time1 = time.time() + # Convert to special edge data. + edgeLoops = [] + for vloop, closed in vertLoops: + edgeLoops.append(edgeLoop(vloop, me, closed)) + + + # VERT LOOP ORDERING CODE + # "Build a worm" list - grow from Both ends + edgeOrderedList = [edgeLoops.pop()] + + # Find the closest. + bestSoFar = BIG_NUM + bestIdxSoFar = None + for edLoopIdx, edLoop in enumerate(edgeLoops): + l =(edgeOrderedList[-1].centre - edLoop.centre).length + if l < bestSoFar: + bestIdxSoFar = edLoopIdx + bestSoFar = l + + edgeOrderedList.append( edgeLoops.pop(bestIdxSoFar) ) + + # Now we have the 2 closest, append to either end- + # Find the closest. + while edgeLoops: + bestSoFar = BIG_NUM + bestIdxSoFar = None + first_or_last = 0 # Zero is first + for edLoopIdx, edLoop in enumerate(edgeLoops): + l1 =(edgeOrderedList[-1].centre - edLoop.centre).length + + if l1 < bestSoFar: + bestIdxSoFar = edLoopIdx + bestSoFar = l1 + first_or_last = 1 # last + + l2 =(edgeOrderedList[0].centre - edLoop.centre).length + if l2 < bestSoFar: + bestIdxSoFar = edLoopIdx + bestSoFar = l2 + first_or_last = 0 # last + + if first_or_last: # add closest Last + edgeOrderedList.append( edgeLoops.pop(bestIdxSoFar) ) + else: # Add closest First + edgeOrderedList.insert(0, edgeLoops.pop(bestIdxSoFar) ) # First + + faces = [] + + for i in range(len(edgeOrderedList)-1): + faces.extend( skin2EdgeLoops(edgeOrderedList[i], edgeOrderedList[i+1], me, ob, 0) ) + if choice == 1 and len(edgeOrderedList) > 2: # Loop + faces.extend( skin2EdgeLoops(edgeOrderedList[0], edgeOrderedList[-1], me, ob, 0) ) + + # REMOVE SELECTED FACES. + MESH_MODE= ob.mode + if MESH_MODE == 'EDGE' or MESH_MODE == 'VERTEX': pass + elif MESH_MODE == 'FACE': + try: me.faces.delete(1, [ f for f in me.faces if f.sel ]) + except: pass + + if 1: # 2.5 + mesh_faces_extend(me, faces, ob.active_material_index) + me.update(calc_edges=True) + else: + me.faces.extend(faces, smooth = True) + + print('\nSkin done in %.4f sec.' % (time.time()-time1)) + + if is_editmode: bpy.ops.object.mode_set(mode='EDIT', toggle=False) + + +class MESH_OT_skin(bpy.types.Operator): + '''Bridge face loops.''' + + bl_idname = "mesh.skin" + bl_label = "Add Torus" + bl_register = True + bl_undo = True + + ''' + loft_method = EnumProperty(attr="loft_method", items=[(), ()], description="", default= True) + + ''' + + def execute(self, context): + main(context) + return ('FINISHED',) + + +# Register the operator +bpy.ops.add(MESH_OT_skin) + +# Add to a menu +import dynamic_menu +menu_item = dynamic_menu.add(bpy.types.VIEW3D_MT_edit_mesh_faces, (lambda self, context: self.layout.itemO("mesh.skin", text="Bridge Faces")) ) + +if __name__ == "__main__": + bpy.ops.mesh.skin() diff --git a/release/scripts/op/uvcalc_smart_project.py b/release/scripts/op/uvcalc_smart_project.py new file mode 100644 index 00000000000..328470a5ec5 --- /dev/null +++ b/release/scripts/op/uvcalc_smart_project.py @@ -0,0 +1,1163 @@ +# -------------------------------------------------------------------------- +# Smart Projection UV Projection Unwrapper v1.2 by Campbell Barton (AKA Ideasman) +# -------------------------------------------------------------------------- +# ***** 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 LICENCE BLOCK ***** +# -------------------------------------------------------------------------- + + +#from Blender import Object, Draw, Window, sys, Mesh, Geometry +from Mathutils import Matrix, Vector, RotationMatrix +import time +import Geometry +import bpy +from math import cos, degrees, radians + +DEG_TO_RAD = 0.017453292519943295 # pi/180.0 +SMALL_NUM = 0.000000001 +BIG_NUM = 1e15 + +global USER_FILL_HOLES +global USER_FILL_HOLES_QUALITY +USER_FILL_HOLES = None +USER_FILL_HOLES_QUALITY = None + +dict_matrix = {} + +def pointInTri2D(v, v1, v2, v3): + global dict_matrix + + key = v1.x, v1.y, v2.x, v2.y, v3.x, v3.y + + # Commented because its slower to do teh bounds check, we should realy cache the bounds info for each face. + ''' + # BOUNDS CHECK + xmin= 1000000 + ymin= 1000000 + + xmax= -1000000 + ymax= -1000000 + + for i in (0,2,4): + x= key[i] + y= key[i+1] + + if xmax<x: xmax= x + if ymax<y: ymax= y + if xmin>x: xmin= x + if ymin>y: ymin= y + + x= v.x + y= v.y + + if x<xmin or x>xmax or y < ymin or y > ymax: + return False + # Done with bounds check + ''' + try: + mtx = dict_matrix[key] + if not mtx: + return False + except: + side1 = v2 - v1 + side2 = v3 - v1 + + nor = side1.cross(side2) + + l1 = [side1[0], side1[1], side1[2]] + l2 = [side2[0], side2[1], side2[2]] + l3 = [nor[0], nor[1], nor[2]] + + mtx = Matrix(l1, l2, l3) + + # Zero area 2d tri, even tho we throw away zerop area faces + # the projection UV can result in a zero area UV. + if not mtx.determinant(): + dict_matrix[key] = None + return False + + mtx.invert() + + dict_matrix[key] = mtx + + uvw = (v - v1) * mtx + return 0 <= uvw[0] and 0 <= uvw[1] and uvw[0] + uvw[1] <= 1 + + +def boundsIsland(faces): + minx = maxx = faces[0].uv[0][0] # Set initial bounds. + miny = maxy = faces[0].uv[0][1] + # print len(faces), minx, maxx, miny , maxy + for f in faces: + for uv in f.uv: + x= uv.x + y= uv.y + if x<minx: minx= x + if y<miny: miny= y + if x>maxx: maxx= x + if y>maxy: maxy= y + + return minx, miny, maxx, maxy + +""" +def boundsEdgeLoop(edges): + minx = maxx = edges[0][0] # Set initial bounds. + miny = maxy = edges[0][1] + # print len(faces), minx, maxx, miny , maxy + for ed in edges: + for pt in ed: + print 'ass' + x= pt[0] + y= pt[1] + if x<minx: x= minx + if y<miny: y= miny + if x>maxx: x= maxx + if y>maxy: y= maxy + + return minx, miny, maxx, maxy +""" + +# Turns the islands into a list of unpordered edges (Non internal) +# Onlt for UV's +# only returns outline edges for intersection tests. and unique points. + +def island2Edge(island): + + # Vert index edges + edges = {} + + unique_points= {} + + for f in island: + f_uvkey= map(tuple, f.uv) + + + for vIdx, edkey in enumerate(f.edge_keys): + unique_points[f_uvkey[vIdx]] = f.uv[vIdx] + + if f.v[vIdx].index > f.v[vIdx-1].index: + i1= vIdx-1; i2= vIdx + else: + i1= vIdx; i2= vIdx-1 + + try: edges[ f_uvkey[i1], f_uvkey[i2] ] *= 0 # sets eny edge with more then 1 user to 0 are not returned. + except: edges[ f_uvkey[i1], f_uvkey[i2] ] = (f.uv[i1] - f.uv[i2]).length, + + # If 2 are the same then they will be together, but full [a,b] order is not correct. + + # Sort by length + + + length_sorted_edges = [(Vector(key[0]), Vector(key[1]), value) for key, value in edges.items() if value != 0] + + try: length_sorted_edges.sort(key = lambda A: -A[2]) # largest first + except: length_sorted_edges.sort(lambda A, B: cmp(B[2], A[2])) + + # Its okay to leave the length in there. + #for e in length_sorted_edges: + # e.pop(2) + + # return edges and unique points + return length_sorted_edges, [v.__copy__().resize3D() for v in unique_points.values()] + +# ========================= NOT WORKING???? +# Find if a points inside an edge loop, un-orderd. +# pt is and x/y +# edges are a non ordered loop of edges. +# #offsets are the edge x and y offset. +""" +def pointInEdges(pt, edges): + # + x1 = pt[0] + y1 = pt[1] + + # Point to the left of this line. + x2 = -100000 + y2 = -10000 + intersectCount = 0 + for ed in edges: + xi, yi = lineIntersection2D(x1,y1, x2,y2, ed[0][0], ed[0][1], ed[1][0], ed[1][1]) + if xi != None: # Is there an intersection. + intersectCount+=1 + + return intersectCount % 2 +""" + +def pointInIsland(pt, island): + vec1 = Vector(); vec2 = Vector(); vec3 = Vector() + for f in island: + vec1.x, vec1.y = f.uv[0] + vec2.x, vec2.y = f.uv[1] + vec3.x, vec3.y = f.uv[2] + + if pointInTri2D(pt, vec1, vec2, vec3): + return True + + if len(f.v) == 4: + vec1.x, vec1.y = f.uv[0] + vec2.x, vec2.y = f.uv[2] + vec3.x, vec3.y = f.uv[3] + if pointInTri2D(pt, vec1, vec2, vec3): + return True + return False + + +# box is (left,bottom, right, top) +def islandIntersectUvIsland(source, target, SourceOffset): + # Is 1 point in the box, inside the vertLoops + edgeLoopsSource = source[6] # Pretend this is offset + edgeLoopsTarget = target[6] + + # Edge intersect test + for ed in edgeLoopsSource: + for seg in edgeLoopsTarget: + i = Geometry.LineIntersect2D(\ + seg[0], seg[1], SourceOffset+ed[0], SourceOffset+ed[1]) + if i: + return 1 # LINE INTERSECTION + + # 1 test for source being totally inside target + SourceOffset.resize3D() + for pv in source[7]: + if pointInIsland(pv+SourceOffset, target[0]): + return 2 # SOURCE INSIDE TARGET + + # 2 test for a part of the target being totaly inside the source. + for pv in target[7]: + if pointInIsland(pv-SourceOffset, source[0]): + return 3 # PART OF TARGET INSIDE SOURCE. + + return 0 # NO INTERSECTION + + + + +# Returns the X/y Bounds of a list of vectors. +def testNewVecLs2DRotIsBetter(vecs, mat=-1, bestAreaSoFar = -1): + + # UV's will never extend this far. + minx = miny = BIG_NUM + maxx = maxy = -BIG_NUM + + for i, v in enumerate(vecs): + + # Do this allong the way + if mat != -1: + v = vecs[i] = v*mat + x= v.x + y= v.y + if x<minx: minx= x + if y<miny: miny= y + if x>maxx: maxx= x + if y>maxy: maxy= y + + # Spesific to this algo, bail out if we get bigger then the current area + if bestAreaSoFar != -1 and (maxx-minx) * (maxy-miny) > bestAreaSoFar: + return (BIG_NUM, None), None + w = maxx-minx + h = maxy-miny + return (w*h, w,h), vecs # Area, vecs + +# Takes a list of faces that make up a UV island and rotate +# until they optimally fit inside a square. +ROTMAT_2D_POS_90D = RotationMatrix( radians(90.0), 2) +ROTMAT_2D_POS_45D = RotationMatrix( radians(45.0), 2) + +RotMatStepRotation = [] +rot_angle = 22.5 #45.0/2 +while rot_angle > 0.1: + RotMatStepRotation.append([\ + RotationMatrix( radians(rot_angle), 2),\ + RotationMatrix( radians(-rot_angle), 2)]) + + rot_angle = rot_angle/2.0 + + +def optiRotateUvIsland(faces): + global currentArea + + # Bestfit Rotation + def best2dRotation(uvVecs, MAT1, MAT2): + global currentArea + + newAreaPos, newfaceProjectionGroupListPos =\ + testNewVecLs2DRotIsBetter(uvVecs[:], MAT1, currentArea[0]) + + + # Why do I use newpos here? May as well give the best area to date for an early bailout + # some slight speed increase in this. + # If the new rotation is smaller then the existing, we can + # avoid copying a list and overwrite the old, crappy one. + + if newAreaPos[0] < currentArea[0]: + newAreaNeg, newfaceProjectionGroupListNeg =\ + testNewVecLs2DRotIsBetter(uvVecs, MAT2, newAreaPos[0]) # Reuse the old bigger list. + else: + newAreaNeg, newfaceProjectionGroupListNeg =\ + testNewVecLs2DRotIsBetter(uvVecs[:], MAT2, currentArea[0]) # Cant reuse, make a copy. + + + # Now from the 3 options we need to discover which to use + # we have cerrentArea/newAreaPos/newAreaNeg + bestArea = min(currentArea[0], newAreaPos[0], newAreaNeg[0]) + + if currentArea[0] == bestArea: + return uvVecs + elif newAreaPos[0] == bestArea: + uvVecs = newfaceProjectionGroupListPos + currentArea = newAreaPos + elif newAreaNeg[0] == bestArea: + uvVecs = newfaceProjectionGroupListNeg + currentArea = newAreaNeg + + return uvVecs + + + # Serialized UV coords to Vectors + uvVecs = [uv for f in faces for uv in f.uv] + + # Theres a small enough number of these to hard code it + # rather then a loop. + + # Will not modify anything + currentArea, dummy =\ + testNewVecLs2DRotIsBetter(uvVecs) + + + # Try a 45d rotation + newAreaPos, newfaceProjectionGroupListPos = testNewVecLs2DRotIsBetter(uvVecs[:], ROTMAT_2D_POS_45D, currentArea[0]) + + if newAreaPos[0] < currentArea[0]: + uvVecs = newfaceProjectionGroupListPos + currentArea = newAreaPos + # 45d done + + # Testcase different rotations and find the onfe that best fits in a square + for ROTMAT in RotMatStepRotation: + uvVecs = best2dRotation(uvVecs, ROTMAT[0], ROTMAT[1]) + + # Only if you want it, make faces verticle! + if currentArea[1] > currentArea[2]: + # Rotate 90d + # Work directly on the list, no need to return a value. + testNewVecLs2DRotIsBetter(uvVecs, ROTMAT_2D_POS_90D) + + + # Now write the vectors back to the face UV's + i = 0 # count the serialized uv/vectors + for f in faces: + #f.uv = [uv for uv in uvVecs[i:len(f)+i] ] + for j, k in enumerate(range(i, len(f.v)+i)): + f.uv[j][:] = uvVecs[k] + i += len(f.v) + + +# Takes an island list and tries to find concave, hollow areas to pack smaller islands into. +def mergeUvIslands(islandList): + global USER_FILL_HOLES + global USER_FILL_HOLES_QUALITY + + + # Pack islands to bottom LHS + # Sync with island + + #islandTotFaceArea = [] # A list of floats, each island area + #islandArea = [] # a list of tuples ( area, w,h) + + + decoratedIslandList = [] + + islandIdx = len(islandList) + while islandIdx: + islandIdx-=1 + minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx]) + w, h = maxx-minx, maxy-miny + + totFaceArea = 0 + offset= Vector(minx, miny) + for f in islandList[islandIdx]: + for uv in f.uv: + uv -= offset + + totFaceArea += f.area + + islandBoundsArea = w*h + efficiency = abs(islandBoundsArea - totFaceArea) + + # UV Edge list used for intersections as well as unique points. + edges, uniqueEdgePoints = island2Edge(islandList[islandIdx]) + + decoratedIslandList.append([islandList[islandIdx], totFaceArea, efficiency, islandBoundsArea, w,h, edges, uniqueEdgePoints]) + + + # Sort by island bounding box area, smallest face area first. + # no.. chance that to most simple edge loop first. + decoratedIslandListAreaSort =decoratedIslandList[:] + + decoratedIslandListAreaSort.sort(key = lambda A: A[3]) + + # sort by efficiency, Least Efficient first. + decoratedIslandListEfficSort = decoratedIslandList[:] + # decoratedIslandListEfficSort.sort(lambda A, B: cmp(B[2], A[2])) + + decoratedIslandListEfficSort.sort(key = lambda A: -A[2]) + + # ================================================== THESE CAN BE TWEAKED. + # This is a quality value for the number of tests. + # from 1 to 4, generic quality value is from 1 to 100 + USER_STEP_QUALITY = ((USER_FILL_HOLES_QUALITY - 1) / 25.0) + 1 + + # If 100 will test as long as there is enough free space. + # this is rarely enough, and testing takes a while, so lower quality speeds this up. + + # 1 means they have the same quality + USER_FREE_SPACE_TO_TEST_QUALITY = 1 + (((100 - USER_FILL_HOLES_QUALITY)/100.0) *5) + + #print 'USER_STEP_QUALITY', USER_STEP_QUALITY + #print 'USER_FREE_SPACE_TO_TEST_QUALITY', USER_FREE_SPACE_TO_TEST_QUALITY + + removedCount = 0 + + areaIslandIdx = 0 + ctrl = Window.Qual.CTRL + BREAK= False + while areaIslandIdx < len(decoratedIslandListAreaSort) and not BREAK: + sourceIsland = decoratedIslandListAreaSort[areaIslandIdx] + # Alredy packed? + if not sourceIsland[0]: + areaIslandIdx+=1 + else: + efficIslandIdx = 0 + while efficIslandIdx < len(decoratedIslandListEfficSort) and not BREAK: + + if Window.GetKeyQualifiers() & ctrl: + BREAK= True + break + + # Now we have 2 islands, is the efficience of the islands lowers theres an + # increasing likely hood that we can fit merge into the bigger UV island. + # this ensures a tight fit. + + # Just use figures we have about user/unused area to see if they might fit. + + targetIsland = decoratedIslandListEfficSort[efficIslandIdx] + + + if sourceIsland[0] == targetIsland[0] or\ + not targetIsland[0] or\ + not sourceIsland[0]: + pass + else: + + # ([island, totFaceArea, efficiency, islandArea, w,h]) + # Waisted space on target is greater then UV bounding island area. + + + # if targetIsland[3] > (sourceIsland[2]) and\ # + # print USER_FREE_SPACE_TO_TEST_QUALITY, 'ass' + if targetIsland[2] > (sourceIsland[1] * USER_FREE_SPACE_TO_TEST_QUALITY) and\ + targetIsland[4] > sourceIsland[4] and\ + targetIsland[5] > sourceIsland[5]: + + # DEBUG # print '%.10f %.10f' % (targetIsland[3], sourceIsland[1]) + + # These enough spare space lets move the box until it fits + + # How many times does the source fit into the target x/y + blockTestXUnit = targetIsland[4]/sourceIsland[4] + blockTestYUnit = targetIsland[5]/sourceIsland[5] + + boxLeft = 0 + + + # Distllllance we can move between whilst staying inside the targets bounds. + testWidth = targetIsland[4] - sourceIsland[4] + testHeight = targetIsland[5] - sourceIsland[5] + + # Increment we move each test. x/y + xIncrement = (testWidth / (blockTestXUnit * ((USER_STEP_QUALITY/50)+0.1))) + yIncrement = (testHeight / (blockTestYUnit * ((USER_STEP_QUALITY/50)+0.1))) + + # Make sure were not moving less then a 3rg of our width/height + if xIncrement<sourceIsland[4]/3: + xIncrement= sourceIsland[4] + if yIncrement<sourceIsland[5]/3: + yIncrement= sourceIsland[5] + + + boxLeft = 0 # Start 1 back so we can jump into the loop. + boxBottom= 0 #-yIncrement + + ##testcount= 0 + + while boxBottom <= testHeight: + # Should we use this? - not needed for now. + #if Window.GetKeyQualifiers() & ctrl: + # BREAK= True + # break + + ##testcount+=1 + #print 'Testing intersect' + Intersect = islandIntersectUvIsland(sourceIsland, targetIsland, Vector(boxLeft, boxBottom)) + #print 'Done', Intersect + if Intersect == 1: # Line intersect, dont bother with this any more + pass + + if Intersect == 2: # Source inside target + ''' + We have an intersection, if we are inside the target + then move us 1 whole width accross, + Its possible this is a bad idea since 2 skinny Angular faces + could join without 1 whole move, but its a lot more optimal to speed this up + since we have alredy tested for it. + + It gives about 10% speedup with minimal errors. + ''' + #print 'ass' + # Move the test allong its width + SMALL_NUM + #boxLeft += sourceIsland[4] + SMALL_NUM + boxLeft += sourceIsland[4] + elif Intersect == 0: # No intersection?? Place it. + # Progress + removedCount +=1 +#XXX Window.DrawProgressBar(0.0, 'Merged: %i islands, Ctrl to finish early.' % removedCount) + + # Move faces into new island and offset + targetIsland[0].extend(sourceIsland[0]) + offset= Vector(boxLeft, boxBottom) + + for f in sourceIsland[0]: + for uv in f.uv: + uv+= offset + + sourceIsland[0][:] = [] # Empty + + + # Move edge loop into new and offset. + # targetIsland[6].extend(sourceIsland[6]) + #while sourceIsland[6]: + targetIsland[6].extend( [ (\ + (e[0]+offset, e[1]+offset, e[2])\ + ) for e in sourceIsland[6] ] ) + + sourceIsland[6][:] = [] # Empty + + # Sort by edge length, reverse so biggest are first. + + try: targetIsland[6].sort(key = lambda A: A[2]) + except: targetIsland[6].sort(lambda B,A: cmp(A[2], B[2] )) + + + targetIsland[7].extend(sourceIsland[7]) + offset= Vector(boxLeft, boxBottom, 0) + for p in sourceIsland[7]: + p+= offset + + sourceIsland[7][:] = [] + + + # Decrement the efficiency + targetIsland[1]+=sourceIsland[1] # Increment totFaceArea + targetIsland[2]-=sourceIsland[1] # Decrement efficiency + # IF we ever used these again, should set to 0, eg + sourceIsland[2] = 0 # No area if anyone wants to know + + break + + + # INCREMENR NEXT LOCATION + if boxLeft > testWidth: + boxBottom += yIncrement + boxLeft = 0.0 + else: + boxLeft += xIncrement + ##print testcount + + efficIslandIdx+=1 + areaIslandIdx+=1 + + # Remove empty islands + i = len(islandList) + while i: + i-=1 + if not islandList[i]: + del islandList[i] # Can increment islands removed here. + +# Takes groups of faces. assumes face groups are UV groups. +def getUvIslands(faceGroups, me): + + # Get seams so we dont cross over seams + edge_seams = {} # shoudl be a set + for ed in me.edges: + if ed.seam: + edge_seams[ed.key] = None # dummy var- use sets! + # Done finding seams + + + islandList = [] + +#XXX Window.DrawProgressBar(0.0, 'Splitting %d projection groups into UV islands:' % len(faceGroups)) + #print '\tSplitting %d projection groups into UV islands:' % len(faceGroups), + # Find grouped faces + + faceGroupIdx = len(faceGroups) + + while faceGroupIdx: + faceGroupIdx-=1 + faces = faceGroups[faceGroupIdx] + + if not faces: + continue + + # Build edge dict + edge_users = {} + + for i, f in enumerate(faces): + for ed_key in f.edge_keys: + if ed_key in edge_seams: # DELIMIT SEAMS! ;) + edge_users[ed_key] = [] # so as not to raise an error + else: + try: edge_users[ed_key].append(i) + except: edge_users[ed_key] = [i] + + # Modes + # 0 - face not yet touched. + # 1 - added to island list, and need to search + # 2 - touched and searched - dont touch again. + face_modes = [0] * len(faces) # initialize zero - untested. + + face_modes[0] = 1 # start the search with face 1 + + newIsland = [] + + newIsland.append(faces[0]) + + + ok = True + while ok: + + ok = True + while ok: + ok= False + for i in range(len(faces)): + if face_modes[i] == 1: # search + for ed_key in faces[i].edge_keys: + for ii in edge_users[ed_key]: + if i != ii and face_modes[ii] == 0: + face_modes[ii] = ok = 1 # mark as searched + newIsland.append(faces[ii]) + + # mark as searched, dont look again. + face_modes[i] = 2 + + islandList.append(newIsland) + + ok = False + for i in range(len(faces)): + if face_modes[i] == 0: + newIsland = [] + newIsland.append(faces[i]) + + face_modes[i] = ok = 1 + break + # if not ok will stop looping + +#XXX Window.DrawProgressBar(0.1, 'Optimizing Rotation for %i UV Islands' % len(islandList)) + + for island in islandList: + optiRotateUvIsland(island) + + return islandList + + +def packIslands(islandList): + if USER_FILL_HOLES: +#XXX Window.DrawProgressBar(0.1, 'Merging Islands (Ctrl: skip merge)...') + mergeUvIslands(islandList) # Modify in place + + + # Now we have UV islands, we need to pack them. + + # Make a synchronised list with the islands + # so we can box pak the islands. + packBoxes = [] + + # Keep a list of X/Y offset so we can save time by writing the + # uv's and packed data in one pass. + islandOffsetList = [] + + islandIdx = 0 + + while islandIdx < len(islandList): + minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx]) + + w, h = maxx-minx, maxy-miny + + if USER_ISLAND_MARGIN: + minx -= USER_ISLAND_MARGIN# *w + miny -= USER_ISLAND_MARGIN# *h + maxx += USER_ISLAND_MARGIN# *w + maxy += USER_ISLAND_MARGIN# *h + + # recalc width and height + w, h = maxx-minx, maxy-miny + + if w < 0.00001 or h < 0.00001: + del islandList[islandIdx] + islandIdx -=1 + continue + + '''Save the offset to be applied later, + we could apply to the UVs now and allign them to the bottom left hand area + of the UV coords like the box packer imagines they are + but, its quicker just to remember their offset and + apply the packing and offset in 1 pass ''' + islandOffsetList.append((minx, miny)) + + # Add to boxList. use the island idx for the BOX id. + packBoxes.append([0, 0, w, h]) + islandIdx+=1 + + # Now we have a list of boxes to pack that syncs + # with the islands. + + #print '\tPacking UV Islands...' +#XXX Window.DrawProgressBar(0.7, 'Packing %i UV Islands...' % len(packBoxes) ) + + time1 = time.time() + packWidth, packHeight = Geometry.BoxPack2D(packBoxes) + + # print 'Box Packing Time:', time.time() - time1 + + #if len(pa ckedLs) != len(islandList): + # raise "Error packed boxes differes from original length" + + #print '\tWriting Packed Data to faces' +#XXX Window.DrawProgressBar(0.8, 'Writing Packed Data to faces') + + # Sort by ID, so there in sync again + islandIdx = len(islandList) + # Having these here avoids devide by 0 + if islandIdx: + + if USER_STRETCH_ASPECT: + # Maximize to uv area?? Will write a normalize function. + xfactor = 1.0 / packWidth + yfactor = 1.0 / packHeight + else: + # Keep proportions. + xfactor = yfactor = 1.0 / max(packWidth, packHeight) + + while islandIdx: + islandIdx -=1 + # Write the packed values to the UV's + + xoffset = packBoxes[islandIdx][0] - islandOffsetList[islandIdx][0] + yoffset = packBoxes[islandIdx][1] - islandOffsetList[islandIdx][1] + + for f in islandList[islandIdx]: # Offsetting the UV's so they fit in there packed box + for uv in f.uv: + uv.x= (uv.x+xoffset) * xfactor + uv.y= (uv.y+yoffset) * yfactor + + + +def VectoMat(vec): + a3 = vec.__copy__().normalize() + + up = Vector(0,0,1) + if abs(a3.dot(up)) == 1.0: + up = Vector(0,1,0) + + a1 = a3.cross(up).normalize() + a2 = a3.cross(a1) + return Matrix([a1[0], a1[1], a1[2]], [a2[0], a2[1], a2[2]], [a3[0], a3[1], a3[2]]) + + +# Utility funcs for 2.5, make into a module?? +def ord_ind(i1,i2): + if i1<i2: return i1,i2 + return i2,i1 + +def edge_key(ed): + v1,v2 = tuple(ed.verts) + return ord_ind(v1, v2) + +def face_edge_keys(f): + verts = tuple(f.verts) + if len(verts)==3: + return ord_ind(verts[0], verts[1]), ord_ind(verts[1], verts[2]), ord_ind(verts[2], verts[0]) + + return ord_ind(verts[0], verts[1]), ord_ind(verts[1], verts[2]), ord_ind(verts[2], verts[3]), ord_ind(verts[3], verts[0]) + + +class thickface(object): + __slost__= 'v', 'uv', 'no', 'area', 'edge_keys' + def __init__(self, face, uvface, mesh_verts): + self.v = [mesh_verts[i] for i in face.verts] + if len(self.v)==4: + self.uv = uvface.uv1, uvface.uv2, uvface.uv3, uvface.uv4 + else: + self.uv = uvface.uv1, uvface.uv2, uvface.uv3 + + self.no = face.normal + self.area = face.area + self.edge_keys = face_edge_keys(face) + +global ob +ob = None +def main(context, island_margin, projection_limit): + global USER_FILL_HOLES + global USER_FILL_HOLES_QUALITY + global USER_STRETCH_ASPECT + global USER_ISLAND_MARGIN + +#XXX objects= bpy.data.scenes.active.objects + objects = context.selected_editable_objects + + + # we can will tag them later. + obList = [ob for ob in objects if ob.type == 'MESH'] + + # Face select object may not be selected. +#XXX ob = objects.active + ob= objects[0] + + if ob and ob.selected == 0 and ob.type == 'MESH': + # Add to the list + obList =[ob] + del objects + + if not obList: + raise('error, no selected mesh objects') + + # Create the variables. + USER_PROJECTION_LIMIT = projection_limit + USER_ONLY_SELECTED_FACES = (1) + USER_SHARE_SPACE = (1) # Only for hole filling. + USER_STRETCH_ASPECT = (1) # Only for hole filling. + USER_ISLAND_MARGIN = island_margin # Only for hole filling. + USER_FILL_HOLES = (0) + USER_FILL_HOLES_QUALITY = (50) # Only for hole filling. + USER_VIEW_INIT = (0) # Only for hole filling. + USER_AREA_WEIGHT = (1) # Only for hole filling. + + # Reuse variable + if len(obList) == 1: + ob = "Unwrap %i Selected Mesh" + else: + ob = "Unwrap %i Selected Meshes" + + # HACK, loop until mouse is lifted. + ''' + while Window.GetMouseButtons() != 0: + time.sleep(10) + ''' + +#XXX if not Draw.PupBlock(ob % len(obList), pup_block): +#XXX return +#XXX del ob + + # Convert from being button types + + USER_PROJECTION_LIMIT_CONVERTED = cos(USER_PROJECTION_LIMIT * DEG_TO_RAD) + USER_PROJECTION_LIMIT_HALF_CONVERTED = cos((USER_PROJECTION_LIMIT/2) * DEG_TO_RAD) + + + # Toggle Edit mode + is_editmode = (context.active_object.mode == 'EDIT') + if is_editmode: + bpy.ops.object.mode_set(mode='OBJECT') + # Assume face select mode! an annoying hack to toggle face select mode because Mesh dosent like faceSelectMode. + + if USER_SHARE_SPACE: + # Sort by data name so we get consistant results + obList.sort(key = lambda ob: ob.data.name) + collected_islandList= [] + +#XXX Window.WaitCursor(1) + + time1 = time.time() + + # Tag as False se we dont operate on teh same mesh twice. +#XXX bpy.data.meshes.tag = False + for me in bpy.data.meshes: + me.tag = False + + + for ob in obList: + me = ob.data + + if me.tag or me.library: + continue + + # Tag as used + me.tag = True + + if len(me.uv_textures)==0: # Mesh has no UV Coords, dont bother. + me.add_uv_texture() + + uv_layer = me.active_uv_texture.data + me_verts = list(me.verts) + + if USER_ONLY_SELECTED_FACES: + meshFaces = [thickface(f, uv_layer[i], me_verts) for i, f in enumerate(me.faces) if f.selected] + #else: + # meshFaces = map(thickface, me.faces) + + if not meshFaces: + continue + +#XXX Window.DrawProgressBar(0.1, 'SmartProj UV Unwrapper, mapping "%s", %i faces.' % (me.name, len(meshFaces))) + + # ======= + # Generate a projection list from face normals, this is ment to be smart :) + + # make a list of face props that are in sync with meshFaces + # Make a Face List that is sorted by area. + # meshFaces = [] + + # meshFaces.sort( lambda a, b: cmp(b.area , a.area) ) # Biggest first. + meshFaces.sort( key = lambda a: -a.area ) + + # remove all zero area faces + while meshFaces and meshFaces[-1].area <= SMALL_NUM: + # Set their UV's to 0,0 + for uv in meshFaces[-1].uv: + uv.zero() + meshFaces.pop() + + # Smallest first is slightly more efficient, but if the user cancels early then its better we work on the larger data. + + # Generate Projection Vecs + # 0d is 1.0 + # 180 IS -0.59846 + + + # Initialize projectVecs + if USER_VIEW_INIT: + # Generate Projection + projectVecs = [Vector(Window.GetViewVector()) * ob.matrixWorld.copy().invert().rotationPart()] # We add to this allong the way + else: + projectVecs = [] + + newProjectVec = meshFaces[0].no + newProjectMeshFaces = [] # Popping stuffs it up. + + + # Predent that the most unique angke is ages away to start the loop off + mostUniqueAngle = -1.0 + + # This is popped + tempMeshFaces = meshFaces[:] + + + + # This while only gathers projection vecs, faces are assigned later on. + while 1: + # If theres none there then start with the largest face + + # add all the faces that are close. + for fIdx in range(len(tempMeshFaces)-1, -1, -1): + # Use half the angle limit so we dont overweight faces towards this + # normal and hog all the faces. + if newProjectVec.dot(tempMeshFaces[fIdx].no) > USER_PROJECTION_LIMIT_HALF_CONVERTED: + newProjectMeshFaces.append(tempMeshFaces.pop(fIdx)) + + # Add the average of all these faces normals as a projectionVec + averageVec = Vector(0,0,0) + if USER_AREA_WEIGHT: + for fprop in newProjectMeshFaces: + averageVec += (fprop.no * fprop.area) + else: + for fprop in newProjectMeshFaces: + averageVec += fprop.no + + if averageVec.x != 0 or averageVec.y != 0 or averageVec.z != 0: # Avoid NAN + projectVecs.append(averageVec.normalize()) + + + # Get the next vec! + # Pick the face thats most different to all existing angles :) + mostUniqueAngle = 1.0 # 1.0 is 0d. no difference. + mostUniqueIndex = 0 # dummy + + for fIdx in range(len(tempMeshFaces)-1, -1, -1): + angleDifference = -1.0 # 180d difference. + + # Get the closest vec angle we are to. + for p in projectVecs: + temp_angle_diff= p.dot(tempMeshFaces[fIdx].no) + + if angleDifference < temp_angle_diff: + angleDifference= temp_angle_diff + + if angleDifference < mostUniqueAngle: + # We have a new most different angle + mostUniqueIndex = fIdx + mostUniqueAngle = angleDifference + + if mostUniqueAngle < USER_PROJECTION_LIMIT_CONVERTED: + #print 'adding', mostUniqueAngle, USER_PROJECTION_LIMIT, len(newProjectMeshFaces) + # Now weight the vector to all its faces, will give a more direct projection + # if the face its self was not representive of the normal from surrounding faces. + + newProjectVec = tempMeshFaces[mostUniqueIndex].no + newProjectMeshFaces = [tempMeshFaces.pop(mostUniqueIndex)] + + + else: + if len(projectVecs) >= 1: # Must have at least 2 projections + break + + + # If there are only zero area faces then its possible + # there are no projectionVecs + if not len(projectVecs): + Draw.PupMenu('error, no projection vecs where generated, 0 area faces can cause this.') + return + + faceProjectionGroupList =[[] for i in range(len(projectVecs)) ] + + # MAP and Arrange # We know there are 3 or 4 faces here + + for fIdx in range(len(meshFaces)-1, -1, -1): + fvec = meshFaces[fIdx].no + i = len(projectVecs) + + # Initialize first + bestAng = fvec.dot(projectVecs[0]) + bestAngIdx = 0 + + # Cycle through the remaining, first alredy done + while i-1: + i-=1 + + newAng = fvec.dot(projectVecs[i]) + if newAng > bestAng: # Reverse logic for dotvecs + bestAng = newAng + bestAngIdx = i + + # Store the area for later use. + faceProjectionGroupList[bestAngIdx].append(meshFaces[fIdx]) + + # Cull faceProjectionGroupList, + + + # Now faceProjectionGroupList is full of faces that face match the project Vecs list + for i in range(len(projectVecs)): + # Account for projectVecs having no faces. + if not faceProjectionGroupList[i]: + continue + + # Make a projection matrix from a unit length vector. + MatProj = VectoMat(projectVecs[i]) + + # Get the faces UV's from the projected vertex. + for f in faceProjectionGroupList[i]: + f_uv = f.uv + for j, v in enumerate(f.v): + # XXX - note, between Mathutils in 2.4 and 2.5 the order changed. + f_uv[j][:] = (v.co * MatProj)[:2] + + + if USER_SHARE_SPACE: + # Should we collect and pack later? + islandList = getUvIslands(faceProjectionGroupList, me) + collected_islandList.extend(islandList) + + else: + # Should we pack the islands for this 1 object? + islandList = getUvIslands(faceProjectionGroupList, me) + packIslands(islandList) + + + # update the mesh here if we need to. + + # We want to pack all in 1 go, so pack now + if USER_SHARE_SPACE: +#XXX Window.DrawProgressBar(0.9, "Box Packing for all objects...") + packIslands(collected_islandList) + + print("Smart Projection time: %.2f" % (time.time() - time1)) + # Window.DrawProgressBar(0.9, "Smart Projections done, time: %.2f sec." % (time.time() - time1)) + + if is_editmode: + bpy.ops.object.mode_set(mode='EDIT') + +#XXX Window.DrawProgressBar(1.0, "") +#XXX Window.WaitCursor(0) +#XXX Window.RedrawAll() + +""" + pup_block = [\ + 'Projection',\ +* ('Angle Limit:', USER_PROJECTION_LIMIT, 1, 89, ''),\ + ('Selected Faces Only', USER_ONLY_SELECTED_FACES, 'Use only selected faces from all selected meshes.'),\ + ('Init from view', USER_VIEW_INIT, 'The first projection will be from the view vector.'),\ + ('Area Weight', USER_AREA_WEIGHT, 'Weight projections vector by face area.'),\ + '',\ + '',\ + '',\ + 'UV Layout',\ + ('Share Tex Space', USER_SHARE_SPACE, 'Objects Share texture space, map all objects into 1 uvmap.'),\ + ('Stretch to bounds', USER_STRETCH_ASPECT, 'Stretch the final output to texture bounds.'),\ +* ('Island Margin:', USER_ISLAND_MARGIN, 0.0, 0.5, ''),\ + 'Fill in empty areas',\ + ('Fill Holes', USER_FILL_HOLES, 'Fill in empty areas reduced texture waistage (slow).'),\ + ('Fill Quality:', USER_FILL_HOLES_QUALITY, 1, 100, 'Depends on fill holes, how tightly to fill UV holes, (higher is slower)'),\ + ] +""" + +from bpy.props import * +class SmartProject(bpy.types.Operator): + '''This script projection unwraps the selected faces of a mesh. it operates on all selected mesh objects, and can be used unwrap selected faces, or all faces.''' + bl_idname = "uv.smart_project" + bl_label = "Smart UV Project" + + bl_register = True + bl_undo = True + + angle_limit = FloatProperty(name="Angle Limit", + description="lower for more projection groups, higher for less distortion.", + default=66.0, min=1.0, max=89.0) + + island_margin = FloatProperty(name="Island Margin", + description="Margin to reduce bleed from adjacent islands.", + default=0.0, min=0.0, max=1.0) + + def poll(self, context): + return context.active_object != None + + def execute(self, context): + main(context, self.island_margin, self.angle_limit) + return ('FINISHED',) + +bpy.ops.add(SmartProject) + +# Add to a menu +import dynamic_menu + +menu_func = (lambda self, context: self.layout.itemO(SmartProject.bl_idname, + text="Smart Project")) + +menu_item = dynamic_menu.add(bpy.types.VIEW3D_MT_uv_map, menu_func) + +if __name__ == '__main__': + bpy.ops.uv.smart_project() + diff --git a/release/scripts/op/vertexpaint_dirt.py b/release/scripts/op/vertexpaint_dirt.py new file mode 100644 index 00000000000..7d32c153d04 --- /dev/null +++ b/release/scripts/op/vertexpaint_dirt.py @@ -0,0 +1,178 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# Script copyright (C) Campbell J Barton +# +# 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 LICENCE BLOCK ***** +# -------------------------------------------------------------------------- + +# History +# +# Originally written by Campbell Barton aka ideasman42 +# +# 2009-11-01: * 2.5 port by Keith "Wahooney" Boshoff +# * Replaced old method with my own, speed is similar (about 0.001 sec on Suzanne) +# but results are far more accurate +# + +import bpy +import Mathutils +import math +import time + +from Mathutils import Vector +from bpy.props import * + +def applyVertexDirt(me, blur_iterations, blur_strength, clamp_dirt, clamp_clean, dirt_only): +## Window.WaitCursor(1) + + #BPyMesh.meshCalcNormals(me) + + vert_tone = [0.0] * len(me.verts) + + min_tone =180.0 + max_tone =0.0 + + # create lookup table for each vertex's connected vertices (via edges) + con = [] + + con = [[] for i in range(len(me.verts))] + + # add connected verts + for e in me.edges: + con[e.verts[0]].append(e.verts[1]) + con[e.verts[1]].append(e.verts[0]) + + for v in me.verts: + vec = Vector() + no = v.normal + co = v.co + + # get the direction of the vectors between the vertex and it's connected vertices + for c in con[v.index]: + vec += Vector(me.verts[c].co - co).normalize() + + # normalize the vector by dividing by the number of connected verts + vec /= len(con[v.index]) + + # angle is the acos of the dot product between vert and connected verts normals + ang = math.acos(no.dot(vec)) + + # enforce min/max + ang = max(clamp_dirt, ang) + + if not dirt_only: + ang = min(clamp_clean, ang) + + vert_tone[v.index] = ang + + # blur tones + for i in range(blur_iterations): + # backup the original tones + orig_vert_tone = list(vert_tone) + + # use connected verts look up for blurring + for j, c in enumerate(con): + for v in c: + vert_tone[j] += blur_strength * orig_vert_tone[v] + + vert_tone[j] /= len(c) * blur_strength + 1 + + min_tone = min(vert_tone) + max_tone = max(vert_tone) + + # debug information + # print(min_tone * 2 * math.pi) + # print(max_tone * 2 * math.pi) + # print(clamp_clean) + # print(clamp_dirt) + + tone_range = max_tone-min_tone + + if not tone_range: + return + + active_col_layer = None + + if len(me.vertex_colors): + for lay in me.vertex_colors: + if lay.active: + active_col_layer = lay.data + else: + bpy.ops.mesh.vertex_color_add() + me.vertex_colors[0].active = True + active_col_layer = me.vertex_colors[0].data + + if not active_col_layer: + return("CANCELLED", ) + + for i, f in enumerate(me.faces): + if not me.use_paint_mask or f.selected: + + f_col = active_col_layer[i] + + f_col = [f_col.color1, f_col.color2, f_col.color3, f_col.color4] + + for j, v in enumerate(f.verts): + col = f_col[j] + tone = vert_tone[me.verts[v].index] + tone = (tone-min_tone)/tone_range + + if dirt_only: + tone = min(tone, 0.5) + tone *= 2 + + col[0] = tone*col[0] + col[1] = tone*col[1] + col[2] = tone*col[2] + +## Window.WaitCursor(0) + +class VertexPaintDirt(bpy.types.Operator): + + bl_idname = "mesh.vertex_paint_dirt" + bl_label = "Dirty Vertex Colors" + bl_register = True + bl_undo = True + + blur_strength = FloatProperty(name = "Blur Strength", description = "Blur strength per iteration", default = 1.0, min = 0.01, max = 1.0) + blur_iterations = IntProperty(name = "Blur Iterations", description = "Number times to blur the colors. (higher blurs more)", default = 1, min = 0, max = 40) + clean_angle = FloatProperty(name = "Highlight Angle", description = "Less then 90 limits the angle used in the tonal range", default = 180.0, min = 0.0, max = 180.0) + dirt_angle = FloatProperty(name = "Dirt Angle", description = "Less then 90 limits the angle used in the tonal range", default = 0.0, min = 0.0, max = 180.0) + dirt_only = BoolProperty(name= "Dirt Only", description = "Dont calculate cleans for convex areas", default = False) + + def execute(self, context): + sce = context.scene + ob = context.object + + if not ob or ob.type != 'MESH': + print('Error, no active mesh object, aborting.') + return('CANCELLED',) + + me = ob.data + + t = time.time() + + applyVertexDirt(me, self.blur_iterations, self.blur_strength, math.radians(self.dirt_angle), math.radians(self.clean_angle), self.dirt_only) + + print('Dirt calculated in %.6f' % (time.time()-t)) + + return('FINISHED',) + +bpy.ops.add(VertexPaintDirt) + +if __name__ == "__main__": + bpy.ops.mesh.vertex_paint_dirt() |