forked from bartvdbraak/blender
453 lines
12 KiB
Python
453 lines
12 KiB
Python
|
#!BPY
|
||
|
|
||
|
"""
|
||
|
Name: 'Triangles to Quads'
|
||
|
Blender: 240
|
||
|
Group: 'Mesh'
|
||
|
Tooltip: 'Triangles to Quads for all selected mesh objects.'
|
||
|
"""
|
||
|
|
||
|
__author__ = "Campbell Barton AKA Ideasman"
|
||
|
__url__ = ["http://members.iinet.net.au/~cpbarton/ideasman/", "blender", "elysiun"]
|
||
|
|
||
|
__bpydoc__ = """\
|
||
|
This script joins any triangles into quads for all selected mesh objects.
|
||
|
|
||
|
Usage:
|
||
|
|
||
|
Select the mesh(es) and run this script. Mesh data will be edited in place.
|
||
|
so make a backup copy first if your not sure of the results
|
||
|
|
||
|
The limit value allows you to choose how pedantic the algorithum is when detecting errors between 2 triangles.
|
||
|
Over 50 could result in quads that are not correct.
|
||
|
|
||
|
The joining of quads takes into account UV mapping, UV Images and Vertex colours
|
||
|
and will not join faces that have mis-matching data.
|
||
|
"""
|
||
|
|
||
|
|
||
|
from Blender import Object, Mathutils, Draw, Window, sys
|
||
|
import math
|
||
|
|
||
|
TRI_LIST = (0,1,2)
|
||
|
|
||
|
vecAngle = Mathutils.AngleBetweenVecs
|
||
|
TriangleNormal = Mathutils.TriangleNormal
|
||
|
|
||
|
#=============================================================================#
|
||
|
# All measurement algorithums for face compatibility when joining into quads #
|
||
|
# every function returns a value between 0.0 and 1.0 #
|
||
|
#=============================================================================#
|
||
|
# total diff is 1.0, no diff is 0.0
|
||
|
# measure accross 2 possible triangles in the imagined quad.
|
||
|
def isfaceNoDiff(imagQuag):
|
||
|
# Divide the quad one way and measure normals
|
||
|
noA1 = TriangleNormal(imagQuag[0].co, imagQuag[1].co, imagQuag[2].co)
|
||
|
noA2 = TriangleNormal(imagQuag[0].co, imagQuag[2].co, imagQuag[3].co)
|
||
|
|
||
|
if noA1 == noA2:
|
||
|
normalADiff = 0.0
|
||
|
else:
|
||
|
try:
|
||
|
normalADiff = vecAngle(noA1, noA2)
|
||
|
except:
|
||
|
#print noA1, noA2
|
||
|
normalADiff = 179
|
||
|
|
||
|
#print normalADiff, noA1, noA2
|
||
|
|
||
|
# Alternade division of the quad
|
||
|
noB1 = TriangleNormal(imagQuag[1].co, imagQuag[2].co, imagQuag[3].co)
|
||
|
noB2 = TriangleNormal(imagQuag[3].co, imagQuag[0].co, imagQuag[1].co)
|
||
|
if noB1 == noB2:
|
||
|
normalBDiff = 0.0
|
||
|
else:
|
||
|
try:
|
||
|
normalBDiff = vecAngle(noB1, noB2)
|
||
|
except:
|
||
|
# print noB1, noB2
|
||
|
normalBDiff = 179
|
||
|
|
||
|
# Should never be NAN anymore.
|
||
|
'''
|
||
|
if normalBDiff != normalBDiff or normalADiff != normalADiff:
|
||
|
raise "NAN"
|
||
|
'''
|
||
|
# The greatest possible difference is 180 for each
|
||
|
return (normalADiff/360) + (normalBDiff/360)
|
||
|
|
||
|
|
||
|
# 4 90d angles == 0, each corner diff from 90 is added together.
|
||
|
# 360 is total possible difference,
|
||
|
def isfaceCoLin(imagQuag):
|
||
|
|
||
|
edgeVec1 = imagQuag[0].co - imagQuag[1].co
|
||
|
edgeVec2 = imagQuag[1].co - imagQuag[2].co
|
||
|
edgeVec3 = imagQuag[2].co - imagQuag[3].co
|
||
|
edgeVec4 = imagQuag[3].co - imagQuag[0].co
|
||
|
|
||
|
# Work out how different from 90 each edge is.
|
||
|
diff = 0
|
||
|
try:
|
||
|
diff += abs(vecAngle(edgeVec1, edgeVec2) - 90)
|
||
|
diff += abs(vecAngle(edgeVec2, edgeVec3) - 90)
|
||
|
diff += abs(vecAngle(edgeVec3, edgeVec4) - 90)
|
||
|
diff += abs(vecAngle(edgeVec4, edgeVec1) - 90)
|
||
|
|
||
|
except:
|
||
|
return 1.0
|
||
|
|
||
|
# Avoid devide by 0
|
||
|
if not diff:
|
||
|
return 0.0
|
||
|
|
||
|
return diff/360
|
||
|
|
||
|
|
||
|
# Meause the areas of the 2 possible ways of subdividing the imagined quad.
|
||
|
# if 1 is very different then the quad is concave.
|
||
|
# We should probably throw out any pairs that are at all concave,
|
||
|
# since even a slightly concacve paor will definetly be co linear.
|
||
|
# though even virging on this can be a recipe for a bad join.
|
||
|
def isfaceConcave(imagQuag):
|
||
|
# Add the 2 areas the deviding one way
|
||
|
areaA =\
|
||
|
Mathutils.TriangleArea(imagQuag[0].co, imagQuag[1].co, imagQuag[2].co) +\
|
||
|
Mathutils.TriangleArea(imagQuag[0].co, imagQuag[2].co, imagQuag[3].co)
|
||
|
|
||
|
# Add the tri's triangulated the alternate way.
|
||
|
areaB =\
|
||
|
Mathutils.TriangleArea(imagQuag[1].co, imagQuag[2].co, imagQuag[3].co) +\
|
||
|
Mathutils.TriangleArea(imagQuag[3].co, imagQuag[0].co, imagQuag[1].co)
|
||
|
|
||
|
# Make a ratio of difference so they are between 1 and 0
|
||
|
# Need to invert the value so 1.0 is 0
|
||
|
minarea = min(areaA, areaB)
|
||
|
maxarea = max(areaA, areaB)
|
||
|
|
||
|
# Aviod devide by 0
|
||
|
if maxarea == 0.0:
|
||
|
return 1
|
||
|
else:
|
||
|
return 1 - (minarea / maxarea)
|
||
|
|
||
|
|
||
|
|
||
|
# This returns a list of verts, to test
|
||
|
# dosent modify the actual faces.
|
||
|
def meshJoinFacesTest(f1, f2, V1FREE, V2FREE):
|
||
|
# pretend face
|
||
|
dummyFace = f1.v[:]
|
||
|
|
||
|
# We know the 2 free verts. insert the f2 free vert in
|
||
|
if V1FREE is 0:
|
||
|
dummyFace.insert(2, f2.v[V2FREE])
|
||
|
elif V1FREE is 1:
|
||
|
dummyFace.append(f2.v[V2FREE])
|
||
|
elif V1FREE is 2:
|
||
|
dummyFace.insert(1, f2.v[V2FREE])
|
||
|
|
||
|
return dummyFace
|
||
|
|
||
|
|
||
|
# Measure how good a quad the 2 tris will make,
|
||
|
def measureFacePair(f1, f2, f1free, f2free):
|
||
|
# Make a imaginary quad. from 2 tris into 4 verts
|
||
|
imagFace = meshJoinFacesTest(f1, f2, f1free, f2free)
|
||
|
if imagFace is False:
|
||
|
return False
|
||
|
|
||
|
measure = 0 # start at 0, a lower value is better.
|
||
|
|
||
|
# Do a series of tests,
|
||
|
# each value will add to the measure value
|
||
|
# and be between 0 and 1.0
|
||
|
|
||
|
measure+= isfaceNoDiff(imagFace)
|
||
|
measure+= isfaceCoLin(imagFace)
|
||
|
measure+= isfaceConcave(imagFace)
|
||
|
|
||
|
# For debugging.
|
||
|
'''
|
||
|
a1= isfaceNoDiff(imagFace)
|
||
|
a2= isfaceCoLin(imagFace)
|
||
|
a3= isfaceConcave(imagFace)
|
||
|
|
||
|
print 'a1 %f' % a1
|
||
|
print 'a2 %f' % a2
|
||
|
print 'a3 %f' % a3
|
||
|
|
||
|
measure = a1+a2+a3
|
||
|
'''
|
||
|
|
||
|
return [f1,f2, measure/3, f1free, f2free]
|
||
|
|
||
|
|
||
|
# We know the faces are good to join, simply execute the join
|
||
|
# by making f1 into a quad and f2 inde an edge (2 vert face.)
|
||
|
INSERT_LOOKUP = (2,3,1)
|
||
|
OTHER_LOOKUP = ((1,2),(0,2),(0,1))
|
||
|
def meshJoinFaces(f1, f2, V1FREE, V2FREE, mesh):
|
||
|
|
||
|
# Only used if we have edges.
|
||
|
# DEBUG
|
||
|
edgeVert1, edgeVert2 = OTHER_LOOKUP[V1FREE]
|
||
|
edgeVert1, edgeVert2 = f1[edgeVert1], f1[edgeVert2]
|
||
|
|
||
|
|
||
|
fverts = f1.v[:]
|
||
|
if mesh.hasFaceUV():
|
||
|
fuvs = f1.uv[:]
|
||
|
if f1.col:
|
||
|
fcols = f1.col[:]
|
||
|
|
||
|
|
||
|
# We know the 2 free verts. insert the f2 free vert in
|
||
|
# Work out which vert to insert
|
||
|
i = INSERT_LOOKUP[V1FREE]
|
||
|
|
||
|
# Insert the vert in the desired location.
|
||
|
fverts.insert(i, f2.v[V2FREE])
|
||
|
if mesh.hasFaceUV():
|
||
|
fuvs.insert(i, f2.uv[V2FREE])
|
||
|
if f1.col:
|
||
|
fcols.insert(i, f2.col[V2FREE])
|
||
|
|
||
|
# Assign the data to the faces.
|
||
|
f1.v = fverts
|
||
|
if mesh.hasFaceUV():
|
||
|
f1.uv = fuvs
|
||
|
if f1.col:
|
||
|
f1.col = fcols
|
||
|
|
||
|
# Make an edge from the 2nd vert.
|
||
|
# removing anything other then the free vert will
|
||
|
# remove the edge from accress the new quad
|
||
|
f2.v.pop(not V2FREE)
|
||
|
|
||
|
|
||
|
# DEBUG
|
||
|
if mesh.edges:
|
||
|
mesh.removeEdge(edgeVert1, edgeVert2)
|
||
|
|
||
|
#return f2
|
||
|
|
||
|
|
||
|
|
||
|
def compare2(v1, v2, limit):
|
||
|
if v1[0] + limit > v2[0] and v1[0] - limit < v2[0] and\
|
||
|
v1[1] + limit > v2[1] and v1[1] - limit < v2[1]:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
def compare3(v1, v2, limit):
|
||
|
if v1[0] + limit > v2[0] and v1[0] - limit < v2[0] and\
|
||
|
v1[1] + limit > v2[1] and v1[1] - limit < v2[1] and\
|
||
|
v1[2] + limit > v2[2] and v1[2] - limit < v2[2]:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
UV_LIMIT = 0.005 # 0.0 to 1.0, can be greater then these bounds tho
|
||
|
def compareFaceUV(f1, f2):
|
||
|
if f1.image == None and f1.image == None:
|
||
|
# No Image, ignore uv's
|
||
|
return True
|
||
|
elif f1.image != f2.image:
|
||
|
# Images differ, dont join faces.
|
||
|
return False
|
||
|
|
||
|
# We know 2 of these will match.
|
||
|
for v1i in TRI_LIST:
|
||
|
for v2i in TRI_LIST:
|
||
|
if f1[v1i] is f2[v2i]:
|
||
|
# We have a vertex index match.
|
||
|
# now match the UV's
|
||
|
if not compare2(f1.uv[v1i], f2.uv[v2i], UV_LIMIT):
|
||
|
# UV's are different
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
COL_LIMIT = 3 # 0 to 255
|
||
|
def compareFaceCol(f1, f2):
|
||
|
# We know 2 of these will match.
|
||
|
for v1i in TRI_LIST:
|
||
|
for v2i in TRI_LIST:
|
||
|
if f1[v1i] is f2[v2i]:
|
||
|
# We have a vertex index match.
|
||
|
# now match the UV's
|
||
|
if not compare3(f1.col[v1i], f2.col[v2i], COL_LIMIT):
|
||
|
# UV's are different
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
def sortPair(a,b):
|
||
|
return min(a,b), max(a,b)
|
||
|
|
||
|
def tri2quad(mesh, limit, selectedFacesOnly):
|
||
|
print '\nStarting tri2quad for mesh: %s' % mesh.name
|
||
|
print '\t...finding pairs'
|
||
|
time1 = sys.time() # Time the function
|
||
|
pairCount = 0
|
||
|
|
||
|
# each item in this list will be a list
|
||
|
# [face1, face2, measureFacePairValue]
|
||
|
facePairLs = []
|
||
|
|
||
|
if selectedFacesOnly:
|
||
|
faceList = [f for f in mesh.faces if f.sel if len(f) is 3 if not f.hide]
|
||
|
else:
|
||
|
faceList = [f for f in mesh.faces if len(f) == 3]
|
||
|
|
||
|
has_face_uv = mesh.hasFaceUV()
|
||
|
has_vert_col = mesh.hasVertexColours()
|
||
|
|
||
|
|
||
|
# Build a list of edges and tris that use those edges.
|
||
|
edgeFaceUsers = {}
|
||
|
for f in faceList:
|
||
|
i1,i2,i3 = f.v[0].index, f.v[1].index, f.v[2].index
|
||
|
ed1, ed2, ed3 = sortPair(i1, i2), sortPair(i2, i3), sortPair(i3, i1)
|
||
|
|
||
|
# The second int in the tuple is the free vert, its easier to store then to work it out again.
|
||
|
try: edgeFaceUsers[ed1].append((f, 2))
|
||
|
except: edgeFaceUsers[ed1] = [(f, 2)]
|
||
|
|
||
|
try: edgeFaceUsers[ed2].append((f, 0))
|
||
|
except: edgeFaceUsers[ed2] = [(f, 0)]
|
||
|
|
||
|
try: edgeFaceUsers[ed3].append((f, 1))
|
||
|
except: edgeFaceUsers[ed3] = [(f, 1)]
|
||
|
|
||
|
|
||
|
edgeDoneCount = 0
|
||
|
for faceListEdgeShared in edgeFaceUsers.itervalues():
|
||
|
if len(faceListEdgeShared) == 2:
|
||
|
f1, f1free = faceListEdgeShared[0]
|
||
|
f2, f2free = faceListEdgeShared[1]
|
||
|
|
||
|
if f1.mat != f2.mat:
|
||
|
pass # faces have different materials.
|
||
|
elif has_face_uv and (not compareFaceUV(f1, f2)):
|
||
|
pass # UV's are there but dont match.
|
||
|
elif has_vert_col and not compareFaceCol(f1, f2):
|
||
|
pass # Colours are there but dont match.
|
||
|
else:
|
||
|
# We can now store the qpair and measure
|
||
|
# there eligability for becoming 1 quad.
|
||
|
pair = measureFacePair(f1, f2, f1free, f2free)
|
||
|
if pair is not False and pair[2] < limit: # Some terraible error
|
||
|
facePairLs.append(pair)
|
||
|
pairCount += 1
|
||
|
|
||
|
edgeDoneCount += 1
|
||
|
if not edgeDoneCount % 20:
|
||
|
p = float(edgeDoneCount) / len(edgeFaceUsers)
|
||
|
Window.DrawProgressBar(p*0.5, 'Found pairs: %i' % pairCount)
|
||
|
|
||
|
|
||
|
# Sort, best options first :)
|
||
|
facePairLs.sort(lambda a,b: cmp(a[2], b[2]))
|
||
|
draws = 0
|
||
|
print '\t...joining pairs'
|
||
|
joinCount = 0
|
||
|
len_facePairLs = len(facePairLs)
|
||
|
|
||
|
#faces_to_remove = []
|
||
|
|
||
|
for pIdx, pair in enumerate(facePairLs):
|
||
|
# We know the last item is the best option, and no other face pairs will get in the way.
|
||
|
# now join the faces.
|
||
|
|
||
|
# If any of the faces have alredy been used then they will not have a lengh of 3 verts
|
||
|
if len(pair[0]) is 3 and len(pair[1]) is 3:
|
||
|
joinCount +=1
|
||
|
# print 'joining faces', joinCount, 'Limit:', facePairLs[-1][2]
|
||
|
#faces_to_remove.append( meshJoinFaces(pair[0], pair[1], mesh) )
|
||
|
meshJoinFaces(pair[0], pair[1], pair[3], pair[4], mesh)
|
||
|
|
||
|
if not pIdx % 20:
|
||
|
p = (0.5 + ((float((len_facePairLs - (len_facePairLs - pIdx))))/len_facePairLs*0.5)) * 0.99
|
||
|
Window.DrawProgressBar(p, 'Joining Face count: %i' % joinCount)
|
||
|
draws +=1
|
||
|
|
||
|
# print 'Draws', draws
|
||
|
|
||
|
# Remove faces, due to a bug in ZR's new BF-Blender CVS
|
||
|
|
||
|
fIdx = len(mesh.faces)
|
||
|
while fIdx:
|
||
|
fIdx -=1
|
||
|
if len(mesh.faces[fIdx]) < 3:
|
||
|
mesh.faces.pop(fIdx)
|
||
|
|
||
|
# Was buggy in 2.4.alpha fixed now I think
|
||
|
#mesh.faces[0].sel = 0
|
||
|
|
||
|
|
||
|
if joinCount:
|
||
|
print "tri2quad time for %s: %s joined %s tri's into quads" % (mesh.name, sys.time()-time1, joinCount)
|
||
|
|
||
|
#mesh.update(0, (mesh.edges != []), 0)
|
||
|
mesh.update(0, 0, 0)
|
||
|
|
||
|
else:
|
||
|
print "tri2quad nothing done %s: %s joined none" % (mesh.name, sys.time()-time1)
|
||
|
|
||
|
|
||
|
|
||
|
#====================================#
|
||
|
# Sanity checks #
|
||
|
#====================================#
|
||
|
def error(str):
|
||
|
Draw.PupMenu('ERROR%t|'+str)
|
||
|
|
||
|
def main():
|
||
|
#selection = Object.Get()
|
||
|
selection = Object.GetSelected()
|
||
|
if len(selection) is 0:
|
||
|
error('No object selected')
|
||
|
return
|
||
|
|
||
|
# GET UNIQUE MESHES.
|
||
|
meshDict = {}
|
||
|
# Mesh names
|
||
|
for ob in selection:
|
||
|
if ob.getType() == 'Mesh':
|
||
|
meshDict[ob.getData(1)] = ob # dont do doubles.
|
||
|
|
||
|
# Create the variables.
|
||
|
selectedFacesOnly = Draw.Create(1)
|
||
|
limit = Draw.Create(25)
|
||
|
|
||
|
|
||
|
pup_block = [\
|
||
|
('Selected Faces Only', selectedFacesOnly, 'Use only selected faces from all selected meshes.'),\
|
||
|
('Limit: ', limit, 1, 100, 'A higher value will join more tris to quads, even if the quads are not perfect.'),\
|
||
|
]
|
||
|
selectedFacesOnly = selectedFacesOnly.val
|
||
|
limit = limit.val
|
||
|
|
||
|
if not Draw.PupBlock('Tri2Quad for %i mesh object(s)' % len(meshDict), pup_block):
|
||
|
return
|
||
|
|
||
|
# We now know we can execute
|
||
|
is_editmode = Window.EditMode()
|
||
|
if is_editmode: Window.EditMode(0)
|
||
|
|
||
|
limit = limit/100.0 # Make between 1 and 0
|
||
|
|
||
|
for ob in meshDict.itervalues():
|
||
|
mesh = ob.getData()
|
||
|
tri2quad(mesh, limit, selectedFacesOnly)
|
||
|
if is_editmode: Window.EditMode(1)
|
||
|
|
||
|
# Dont run when importing
|
||
|
if __name__ == '__main__':
|
||
|
Window.DrawProgressBar(0.0, 'Triangles to Quads 1.1 ')
|
||
|
main()
|
||
|
Window.DrawProgressBar(1.0, '')
|