# -*- indent-tabs-mode: t -*- # $Id: bcobject.py 426 2006-10-03 15:04:31Z cubicool $ import Blender import blendercal import bcconf import bcgui # We use immutable sets in the LOD algorithm # to identify unique edges and faces. Immutable # sets are handy for identification because they're # hashable and unordered. from sets import ImmutableSet CONCAT = lambda s, j="": j.join(str(v) for v in s) STRFLT = lambda f: "%%.%df" % bcconf.FLOATPRE % f class Cal3DObject(object): # The base class for all of the bcobject classes. Children of this class must # define a method called XML which should build and return an XML representation # of the object. Furthermore, children that pass a value in the constructor # for the magic parameter are treated as top-level XML files and preprend # the appropriate headers. def __init__(self, magic=None): self.__magic = magic def __repr__(self): ret = "" xml = self.XML().replace("\t", "").replace("#", " " * bcconf.XMLINDENT) if self.__magic: ret += """\n""" ret += """
\n""" % self.__magic return ret + xml def __str__(self): return self.__repr__() def XML(self): raise AttributeError, "Children must define this method!" class Material(Cal3DObject): MATERIALS = {} def __init__(self, name, ambient=[255]*4, diffuse=[255]*4, specular=[255]*4, mapnames=None): Cal3DObject.__init__(self, "XRF") self.name = name self.ambient = ambient self.diffuse = diffuse self.specular = specular self.shininess = 1.0 self.mapnames = [] self.id = len(Material.MATERIALS) mapnames and self.mapnames.extend(mapnames) Material.MATERIALS[self.name] = self def XML(self): mapXML = "" for mapname in self.mapnames: mapXML += "#" + mapname + "\n" return """\ #%s %s %s %s #%s %s %s %s #%s %s %s %s #%s %s """ % ( len(self.mapnames), self.ambient[0], self.ambient[1], self.ambient[2], self.ambient[3], self.diffuse[0], self.diffuse[1], self.diffuse[2], self.diffuse[3], self.specular[0], self.specular[1], self.specular[2], self.specular[3], self.shininess, mapXML ) class Mesh(Cal3DObject): def __init__(self, name): Cal3DObject.__init__(self, "XMF") self.name = name.replace(".", "_") self.submeshes = [] def XML(self): return """\ %s """ % ( len(self.submeshes), CONCAT(self.submeshes) ) class SubMesh(Cal3DObject): def __init__(self, mesh, material): Cal3DObject.__init__(self) self.material = material self.vertices = [] self.faces = [] self.springs = [] self.mesh = mesh self.num_lodsteps = 0 mesh.submeshes.append(self) if not material: self.material = Material("Default") # These are all small classes for the creation of a very specific, # temporary data structure for LOD calculations. The entire submesh # will be temporarily copied into this structure, to allow more freedom # in on-the-fly refactorizations and manipulations. class LODVertex: """We need to factor in some other information, compared to standard vertices, like edges and faces. On the other hand, we don't really need stuff like UVs when we do this. Doing another small, inner Vertex class for this, will hopefully not be seen as a total waste.""" def __init__(self, origindex, loc, cloned): self.id = origindex self.loc = Blender.Mathutils.Vector(loc) self.edges = {} self.faces = {} self.cloned = cloned self.col_to = None self.col_from = None self.face_collapses = 0 self.deathmarked = False def colto(self): if self.col_to: cvert = self.col_to while cvert.col_to: cvert = cvert.col_to return cvert else: return self def colfrom(self): if self.col_from: cvert = self.col_from while cvert.col_from: cvert = cvert.col_from return cvert else: return self def getid(self): return self.colto().id def getloc(self): return self.colto().loc def getfaces(self, facel = None): if not facel: facelist = [] else: facelist = facel for face in self.faces.values(): if (not face.dead) and (not facelist.__contains__(face)): facelist.append(face) if self.col_from: facelist = self.col_from.getfaces(facelist) return facelist def getedges(self, edgel = None): if not edgel: edgelist = [] else: edgelist = edgel for edge in self.edges.values(): if (not edge.dead) and (not edgelist.__contains__(edge)): edgelist.append(edge) if self.col_from: edgelist = self.col_from.getedges(edgelist) return edgelist class LODFace: def __init__(self, verts, fid): self.verts = verts vertset = ImmutableSet((self.verts[0].id, self.verts[1].id, self.verts[2].id)) for vert in self.verts: vert.faces[self.getHashableSet()] = self self.id = fid self.edges = [] self.RefactorArea() self.dead = False def replaceVert(self, replacev, withv): i = self.verts.index(replacev) self.verts[i] = withv # def Refactor(self): # self.RefactorArea() def RefactorArea(self): crossp = Blender.Mathutils.CrossVecs(self.verts[1].getloc() - self.verts[2].getloc(), self.verts[0].getloc() - self.verts[2].getloc()) self.area = (1./2.)*((crossp.x**2 + crossp.y**2 + crossp.z**2)**(1./2.)) def getHashableSet(self): return ImmutableSet((self.verts[0].id, self.verts[1].id, self.verts[2].id)) class LODEdge: """Extra, inner class used for the temporary LOD datastructure""" def __init__(self, v1, v2): self.v1 = v1 self.v2 = v2 vertset = ImmutableSet((self.v1.id, self.v2.id)) self.v1.edges[vertset] = self self.v2.edges[vertset] = self # Get faces common for both v1 and v2 self.faces = [] #for key in filter(self.v1.faces.__contains__, self.v2.faces): # face = self.v1.faces[key] #self.faces[ImmutableSet((face.verts[0].id, face.verts[1].id, face.verts[2].id))] = face # self.faces.append(face) self.collapsed_faces = {} self.RefactorLength() self.dead = False def getOtherVert(self, vertex): if vertex == self.v1: return self.v2 elif vertex == self.v2: return self.v1 def Refactor(self): self.RefactorLength() self.RefactorWeight() def RefactorLength(self): self.length = (self.v2.getloc() - self.v1.getloc()).length def RefactorWeight(self): # Determine which vert to collapse to which, # using jiba's method of basing this decision on # The number of edges connected to each vertex # I.e.: Collapse the edge with least amount of edges. # The order of the vertices in v1, v2 do not matter in # any other respect, so we simply use this order, and # say we collapse v1 into v2. if len(self.v1.getedges()) > len(self.v2.getedges()): self.v1, self.v2 = self.v2, self.v1 # Get total area of faces surrounding edge area = 0 for face in self.faces: area += face.area proportional_area = area / avg_area proportional_length = self.length / avg_length # Get dot products (angle sharpness) of edges connected to v1 edgeverts_factor = 0 self_vec = self.v2.getloc() - self.v1.getloc() self_vec.normalize() for edge in self.v1.edges.values(): if edge != self: edgevert = edge.getOtherVert(self.v1) edge_vec = edgevert.getloc() - self.v1.getloc() edge_vec.normalize() edgeverts_factor += (1 - Blender.Mathutils.DotVecs(self_vec, edge_vec))/2 # Get dot products of edges connected to v2. Wohoo, copy-paste! self_vec = self.v1.getloc() - self.v2.getloc() self_vec.normalize() for edge in self.v2.edges.values(): if edge != self: edgevert = edge.getOtherVert(self.v2) edge_vec = edgevert.getloc() - self.v2.getloc() edge_vec.normalize() edgeverts_factor += (1 - Blender.Mathutils.DotVecs(self_vec, edge_vec))/2 # Error metric, or magic formula. Whatever you like to call it. # This calculates the weight of the edge, based on the # information we have now gathered. We can change this at # any time to try and get better results. self.weight = proportional_area * proportional_length * edgeverts_factor #self.weight = proportional_length return self.weight def getHashableSet(self): return ImmutableSet((self.v1.id, self.v2.id)) def collapse(self): if self.v1.col_to or self.v2.col_to: return False if self.v1.cloned or self.v2.cloned: return False if len(self.faces) < 2: return False self.dead = True # Mark all faces as dead and the two # collapsed edges as dead for face in filter(self.v1.getfaces().__contains__, self.v2.getfaces()): # If not dead, add to dict of faces to collapse with this edge if not face.dead: self.collapsed_faces[face.getHashableSet()] = face self.v1.face_collapses += 1 face.dead = True # Mark collapsed edges as dead. Edges that don't share # a vertex with this edge's v2 dies. for edge in face.edges: if (edge.v1 != self.v2) and (edge.v2 != self.v2): edge.dead = True # for face in self.faces: # face.dead = True # for edge in face.edges: # if (edge.v1 != self.v2) and (edge.v2 != self.v2): # edge.dead = True # self.v1.face_collapses += 1 # Refactor area of all non-dead faces on vertex 1 for face in self.v1.getfaces(): if not face.dead: face.RefactorArea() # Refactor lengths and weights of all non-dead # edges on vertex 1 for edge in self.v1.getedges(): if not edge.dead: edge.Refactor() self.v2.colfrom().col_from = self.v1 self.v1.col_to = self.v2 return True def LOD(self): global avg_area, avg_length progressbar = bcgui.Progress(10) # Step one. Build temporary data structure suited for weight calculations. # Vertices are the only ones that can be/needs to be ordered. # Faces and edges are dicts, with Immutable Sets (containing Vertex indices) as keys. LODverts = [] LODfaces = {} LODedges = {} # Create vertices progressbar.setup(len(self.vertices), "Creating LODverts") for vertex in self.vertices: progressbar.increment() LODverts.append(self.LODVertex(vertex.id, vertex.loc, vertex.cloned)) # Create faces num_faces = 0 avg_area = 0 total_area = 0 progressbar.setup(len(self.faces), "Creating LODfaces") for face in self.faces: progressbar.increment() lface = self.LODFace([LODverts[face.vertices[0].id], LODverts[face.vertices[1].id], LODverts[face.vertices[2].id]], num_faces) LODfaces[lface.getHashableSet()] = lface total_area += lface.area num_faces += 1 if num_faces: avg_area = total_area / float(num_faces) # Create edges num_edges = 0 avg_length = 0 total_length = 0 progressbar.setup(len(LODfaces), "Creating LODedges") for lodface in LODfaces.values(): progressbar.increment() #Create the three edges from this face for e in [(0, 1), (0, 2), (1, 2)]: imset = ImmutableSet((lodface.verts[e[0]].id, lodface.verts[e[1]].id)) if not LODedges.has_key(imset): #Create edge lodedge = self.LODEdge(lodface.verts[e[0]], lodface.verts[e[1]]) LODedges[imset] = lodedge lodface.edges.append(lodedge) lodedge.faces.append(lodface) total_length += lodedge.length num_edges += 1 else: lodedge = LODedges[imset] lodface.edges.append(lodedge) lodedge.faces.append(lodface) if num_edges: avg_length = total_length / float(num_edges) # print total_length # print avg_length # Step two. Calculate initial weights of all edges. progressbar.setup(len(LODedges), "Calculating weights") for edge in LODedges.values(): progressbar.increment() edge.RefactorWeight() # print edge.weight # Order edges in list after weights LODedgelist = LODedges.values() LODedgelist.sort(self.compareweights) weight = LODedgelist[0].weight percentage = len(LODedgelist) * 0.6 count = 0 collapse_list = [] progressbar.setup(percentage, "Calculating LOD") while count < percentage: edge = LODedgelist.pop(0) if not edge.dead: if edge.collapse(): LODedgelist.sort(self.compareweights) collapse_list.append((edge.v1, edge.collapsed_faces)) count += 1 progressbar.increment() self.num_lodsteps = len(collapse_list) newvertlist = [] newfacelist = [] # The list should be in reverse order, with the most # important ones first. collapse_list.reverse() for vertex, faces in collapse_list: vertex.col_to = self.vertices[vertex.col_to.id] for vertex in LODverts: if not vertex.col_to: cvert = self.vertices[vertex.id] cvert.id = len(newvertlist) newvertlist.append(cvert) for face in LODfaces.values(): if not face.dead: newfacelist.append(self.faces[face.id]) for vertex, faces in collapse_list: for face in faces.values(): newfacelist.append(self.faces[face.id]) cvert = self.vertices[vertex.id] cvert.id = len(newvertlist) cvert.collapse_to = vertex.col_to cvert.num_faces = vertex.face_collapses newvertlist.append(cvert) self.vertices = newvertlist self.faces = newfacelist def compareweights(self, x, y): result = x.weight - y.weight if result < 0: return -1 elif result > 0: return 1 else: return 0 def XML(self): return """\ # %s%s%s# """ % ( len(self.vertices), len(self.faces), self.material.id, self.num_lodsteps, len(self.springs), len(self.material.mapnames), CONCAT(self.vertices), CONCAT(self.springs), CONCAT(self.faces) ) #class Progress: # self.progress = 0.0 # self. class Map(Cal3DObject): def __init__(self, uv): Cal3DObject.__init__(self) self.uv = Blender.Mathutils.Vector(uv) def XML(self): return "###%s %s\n" % ( STRFLT(self.uv.x), STRFLT(self.uv.y) ) class Vertex(Cal3DObject): # An interesting note about this class is that we keep Blender objects # as self.loc and self.normal. Of note is how we "wrap" the existing # instances with our own copies, since I was experiencing bugs where # the existing ones would go out of scope. def __init__(self, submesh, loc, normal, cloned, uvs): Cal3DObject.__init__(self) self.loc = Blender.Mathutils.Vector(loc) self.normal = Blender.Mathutils.Vector(normal) self.maps = [] self.influences = [] self.submesh = submesh self.id = len(submesh.vertices) self.cloned = cloned self.collapse_to = None self.num_faces = 0 # If one UV is None, the rest will also be None. if len(uvs) and (uvs[0][0] != None): self.maps.extend(Map(uv) for uv in uvs) submesh.vertices.append(self) def XML(self): loc = blendercal.VECTOR2GL(self.loc) normal = blendercal.VECTOR2GL(self.normal) unset = lambda t: "###\n" % t collapse = "" # Note: collapse_to is an index, and _can_ be 0 if self.collapse_to != None: collapse = """\ ###%s ###%s """ % ( str(self.collapse_to.id), str(self.num_faces) ) loc = loc * bcconf.SCALE normal = normal * bcconf.SCALE return """\ ## ###%s %s %s ###%s %s %s %s%s%s## """ % ( self.id, len(self.influences), STRFLT(loc.x), STRFLT(loc.y), STRFLT(loc.z), STRFLT(normal.x), STRFLT(normal.y), STRFLT(normal.z), collapse, len(self.maps) and CONCAT(self.maps) or unset("UV Coords"), self.influences and CONCAT(self.influences) or unset("Influences") ) class Influence(Cal3DObject): def __init__(self, bone, weight): Cal3DObject.__init__(self) self.bone = bone self.weight = weight def XML(self): return """###%s\n""" % ( self.bone.id, STRFLT(self.weight) ) class Face(Cal3DObject): def __init__(self, submesh, v1, v2, v3): Cal3DObject.__init__(self) self.vertices = (v1, v2, v3) self.submesh = submesh submesh.faces.append(self) def XML(self): return """##\n""" % (self.vertices[0].id, self.vertices[1].id, self.vertices[2].id) class Skeleton(Cal3DObject): ARMATURE = None def __init__(self): Cal3DObject.__init__(self, "XSF") self.bones = [] def XML(self): return """\ %s """ % ( len(self.bones), CONCAT(self.bones) ) class Bone(Cal3DObject): BONES = {} def __init__(self, skeleton, parent, bone, armamat): Cal3DObject.__init__(self) absmat = bone.matrix["ARMATURESPACE"] * armamat self.parent = parent self.name = bone.name.replace(".", "_") self.invert = Blender.Mathutils.Matrix(absmat).invert() self.local = (parent and (absmat * self.parent.invert)) or absmat self.children = [] self.skeleton = skeleton self.id = len(skeleton.bones) if self.parent: self.parent.children.append(self) skeleton.bones.append(self) Bone.BONES[self.name] = self def XML(self): # TRANSLATION and ROTATION are relative to the parent bone. # They are virtually useless since the animations (.XAF .CAF) # will always override them. # # LOCALTRANSLATION and LOCALROTATION are the invert of the cumulated # TRANSLATION and ROTATION (see above). It is used to calculate the # delta between an animated bone and the original non animated bone. # This delta will be applied to the influenced vertexes. # # Negate the rotation because blender rotations are clockwise # and cal3d rotations are counterclockwise local = blendercal.MATRIX2GL(self.local) local = local * bcconf.SCALE localloc = local.translationPart() localrot = local.toQuat() invert = blendercal.MATRIX2GL(self.invert) invertloc = invert.translationPart() invertloc = invertloc * bcconf.SCALE invertrot = invert.toQuat() return """\ # ##%s %s %s ##%s %s %s -%s ##%s %s %s ##%s %s %s -%s ##%s %s# """ % ( self.id, self.name, len(self.children), STRFLT(localloc.x), STRFLT(localloc.y), STRFLT(localloc.z), STRFLT(localrot.x), STRFLT(localrot.y), STRFLT(localrot.z), STRFLT(localrot.w), STRFLT(invertloc.x), STRFLT(invertloc.y), STRFLT(invertloc.z), STRFLT(invertrot.x), STRFLT(invertrot.y), STRFLT(invertrot.z), STRFLT(invertrot.w), self.parent and "%d" % self.parent.id or "-1", "".join("##%s\n" % c.id for c in self.children) ) class Animation(Cal3DObject): def __init__(self, name, duration=0.0): Cal3DObject.__init__(self, "XAF") self.name = name.replace(".", "_") self.duration = duration self.tracks = {} def XML(self): return """\ %s """ % ( self.duration, len(self.tracks), CONCAT(self.tracks.values()) ) class Track(Cal3DObject): def __init__(self, animation, bone): Cal3DObject.__init__(self) self.bone = bone self.keyframes = [] self.animation = animation animation.tracks[bone.name] = self def XML(self): return """\ # %s# """ % ( self.bone.id, len(self.keyframes), CONCAT(self.keyframes) ) class KeyFrame(Cal3DObject): def __init__(self, track, time, loc, rot): Cal3DObject.__init__(self) self.time = time self.loc = Blender.Mathutils.Vector(loc) self.rot = Blender.Mathutils.Quaternion(rot) self.track = track track.keyframes.append(self) def XML(self): self.loc = self.loc * bcconf.SCALE return """\ ## ###%s %s %s ###%s %s %s -%s ## """ % ( STRFLT(self.time), STRFLT(self.loc.x), STRFLT(self.loc.y), STRFLT(self.loc.z), STRFLT(self.rot.x), STRFLT(self.rot.y), STRFLT(self.rot.z), STRFLT(self.rot.w) )