#!BPY """ Name: 'VRM' Blender: 241 Group: 'Export' Tooltip: 'Vector Rendering Method Export Script 0.3' """ # --------------------------------------------------------------------- # Copyright (c) 2006 Antonio Ospite # # 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # --------------------------------------------------------------------- # # NOTE: I do not know who is the original author of 'vrm'. # The present code is almost entirely rewritten from scratch, # but if I have to give credits to anyone, please let me know, # so I can update the copyright. # # --------------------------------------------------------------------- # # Additional credits: # Thanks to Emilio Aguirre for S2flender from which I took inspirations :) # Thanks to Anthony C. D'Agostino for the original backface.py script # # --------------------------------------------------------------------- import Blender from Blender import Scene, Object, NMesh, Lamp, Camera from Blender.Mathutils import * from math import * # --------------------------------------------------------------------- # ## Projections classes # # --------------------------------------------------------------------- class Projector: """Calculate the projection of an object given the camera. A projector is useful to so some per-object transformation to obtain the projection of an object given the camera. The main method is #doProjection# see the method description for the parameter list. """ def __init__(self, cameraObj, obMesh, canvasSize): """Calculate the projection matrix. The projection matrix depends, in this case, on the camera settings, and also on object transformation matrix. """ self.size = canvasSize camera = cameraObj.getData() aspect = float(canvasSize[0])/float(canvasSize[1]) near = camera.clipStart far = camera.clipEnd fovy = atan(0.5/aspect/(camera.lens/32)) fovy = fovy * 360/pi # What projection do we want? if camera.type: m2 = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale) else: m2 = self._calcPerspectiveMatrix(fovy, aspect, near, far) # View transformation cam = Matrix(cameraObj.getInverseMatrix()) cam.transpose() m1 = Matrix(obMesh.getMatrix()) m1.transpose() mP = cam * m1 mP = m2 * mP self.projectionMatrix = mP ## # Public methods # def doProjection(self, v): """Project the point on the view plane. Given a vertex calculate the projection using the current projection matrix. """ # Note that we need the vertex expressed using homogeneous coordinates p = self.projectionMatrix * Vector([v[0], v[1], v[2], 1.0]) mW = self.size[0]/2 mH = self.size[1]/2 if p[3]<=0: p[0] = round(p[0]*mW)+mW p[1] = round(p[1]*mH)+mH else: p[0] = round((p[0]/p[3])*mW)+mW p[1] = round((p[1]/p[3])*mH)+mH # For now we want (0,0) in the top-left corner of the canvas # Mirror and translate along y p[1] *= -1 p[1] += self.size[1] return p ## # Private methods # def _calcPerspectiveMatrix(self, fovy, aspect, near, far): """Return a perspective projection matrix.""" top = near * tan(fovy * pi / 360.0) bottom = -top left = bottom*aspect right= top*aspect x = (2.0 * near) / (right-left) y = (2.0 * near) / (top-bottom) a = (right+left) / (right-left) b = (top+bottom) / (top - bottom) c = - ((far+near) / (far-near)) d = - ((2*far*near)/(far-near)) m = Matrix( [x, 0.0, a, 0.0], [0.0, y, b, 0.0], [0.0, 0.0, c, d], [0.0, 0.0, -1.0, 0.0]) return m def _calcOrthoMatrix(self, fovy, aspect , near, far, scale): """Return an orthogonal projection matrix.""" top = near * tan(fovy * pi / 360.0) * (scale * 10) bottom = -top left = bottom * aspect right= top * aspect rl = right-left tb = top-bottom fn = near-far tx = -((right+left)/rl) ty = -((top+bottom)/tb) tz = ((far+near)/fn) m = Matrix( [2.0/rl, 0.0, 0.0, tx], [0.0, 2.0/tb, 0.0, ty], [0.0, 0.0, 2.0/fn, tz], [0.0, 0.0, 0.0, 1.0]) return m # --------------------------------------------------------------------- # ## Object representation class # # --------------------------------------------------------------------- # TODO: a class to represent the needed properties of a 2D vector image # Just use a NMesh structure? # --------------------------------------------------------------------- # ## Vector Drawing Classes # # --------------------------------------------------------------------- ## A generic Writer class VectorWriter: """ A class for printing output in a vectorial format. Given a 2D representation of the 3D scene the class is responsible to write it is a vector format. Every subclasses of VectorWriter must have at last the following public methods: - printCanvas(mesh) --- where mesh is as specified before. """ def __init__(self, fileName, canvasSize): """Open the file named #fileName# and set the canvas size.""" self.file = open(fileName, "w") print "Outputting to: ", fileName self.canvasSize = canvasSize ## # Public Methods # def printCanvas(mesh): return ## # Private Methods # def _printHeader(): return def _printFooter(): return ## SVG Writer class SVGVectorWriter(VectorWriter): """A concrete class for writing SVG output. The class does not support animations, yet. Sorry. """ def __init__(self, file, canvasSize): """Simply call the parent Contructor.""" VectorWriter.__init__(self, file, canvasSize) ## # Public Methods # def printCanvas(self, scene): """Convert the scene representation to SVG.""" self._printHeader() Objects = scene.getChildren() for obj in Objects: self.file.write("\n") for face in obj.getData().faces: self._printPolygon(face) self._printWireframe(obj.getData()) self.file.write("\n") self._printFooter() ## # Private Methods # def _printHeader(self): """Print SVG header.""" self.file.write("\n") self.file.write("\n") self.file.write("\n\n" % self.canvasSize) def _printFooter(self): """Print the SVG footer.""" self.file.write("\n\n") self.file.close() def _printWireframe(self, mesh): """Print the wireframe using mesh edges... is this the correct way? """ print mesh.edges print print mesh.verts stroke_width=0.5 stroke_col = [0, 0, 0] self.file.write("\n") for e in mesh.edges: self.file.write("\n") self.file.write("\n") def _printPolygon(self, face): """Print our primitive, finally. """ wireframe = False stroke_width=0.5 self.file.write("\n") # --------------------------------------------------------------------- # ## Rendering Classes # # --------------------------------------------------------------------- def RotatePoint(PX,PY,PZ,AngleX,AngleY,AngleZ): NewPoint = [] # Rotate X NewY = (PY * cos(AngleX))-(PZ * sin(AngleX)) NewZ = (PZ * cos(AngleX))+(PY * sin(AngleX)) # Rotate Y PZ = NewZ PY = NewY NewZ = (PZ * cos(AngleY))-(PX * sin(AngleY)) NewX = (PX * cos(AngleY))+(PZ * sin(AngleY)) PX = NewX PZ = NewZ # Rotate Z NewX = (PX * cos(AngleZ))-(PY * sin(AngleZ)) NewY = (PY * cos(AngleZ))+(PX * sin(AngleZ)) NewPoint.append(NewX) NewPoint.append(NewY) NewPoint.append(NewZ) return NewPoint class Renderer: """Render a scene viewed from a given camera. This class is responsible of the rendering process, hence transormation and projection of the ojects in the scene are invoked by the renderer. The user can optionally provide a specific camera for the rendering, see the #doRendering# method for more informations. """ def __init__(self): """Set the canvas size to a defaulr value. The only instance attribute here is the canvas size, which can be queryed to the renderer by other entities. """ self.canvasSize = (0.0, 0.0) ## # Public Methods # def getCanvasSize(self): """Return the current canvas size read from Blender rendering context""" return self.canvasSize def doRendering(self, scene, cameraObj=None): """Control the rendering process. Here we control the entire rendering process invoking the operation needed to transforma project the 3D scene in two dimensions. Parameters: scene --- the Blender Scene to render cameraObj --- the camera object to use for the viewing processing """ if cameraObj == None: cameraObj = scene.getCurrentCamera() context = scene.getRenderingContext() self.canvasSize = (context.imageSizeX(), context.imageSizeY()) Objects = scene.getChildren() # A structure to store the transformed scene newscene = Scene.New("flat"+scene.name) for obj in Objects: if (obj.getType() != "Mesh"): print "Type:", obj.getType(), "\tSorry, only mesh Object supported!" continue # Get a projector for this object proj = Projector(cameraObj, obj, self.canvasSize) # Let's store the transformed data transformed_mesh = NMesh.New("flat"+obj.name) transformed_mesh.hasVertexColours(1) # process Edges for v in obj.getData().verts: transformed_mesh.verts.append(v) transformed_mesh.edges = self._processEdges(obj.getData().edges) print transformed_mesh.edges # Store the materials materials = obj.getData().getMaterials() meshfaces = obj.getData().faces for face in meshfaces: # if the face is visible flatten it on the "picture plane" if self._isFaceVisible_old(face, obj, cameraObj): # Store transformed face newface = NMesh.Face() for vert in face: p = proj.doProjection(vert.co) tmp_vert = NMesh.Vert(p[0], p[1], p[2]) # Add the vert to the mesh transformed_mesh.verts.append(tmp_vert) newface.v.append(tmp_vert) # Per-face color calculation # code taken mostly from the original vrm script # TODO: understand the code and rewrite it clearly ambient = -150 fakelight = Object.Get("Lamp").loc if fakelight == None: fakelight = [1.0, 1.0, -0.3] norm = Vector(face.no) vektori = (norm[0]*fakelight[0]+norm[1]*fakelight[1]+norm[2]*fakelight[2]) vduzine = fabs(sqrt(pow(norm[0],2)+pow(norm[1],2)+pow(norm[2],2))*sqrt(pow(fakelight[0],2)+pow(fakelight[1],2)+pow(fakelight[2],2))) intensity = floor(ambient + 200*acos(vektori/vduzine))/200 if intensity < 0: intensity = 0 if materials: tmp_col = materials[face.mat].getRGBCol() else: tmp_col = [0.5, 0.5, 0.5] tmp_col = [ (c>intensity) and int(round((c-intensity)*10)*25.5) for c in tmp_col ] vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2]) newface.col = [vcol, vcol, vcol, 255] transformed_mesh.addFace(newface) # at the end of the loop on obj transformed_obj = Object.New(obj.getType(), "flat"+obj.name) transformed_obj.link(transformed_mesh) transformed_obj.loc = obj.loc newscene.link(transformed_obj) return newscene ## # Private Methods # def _isFaceVisible_old(self, face, obj, cameraObj): """Determine if the face is visible from the current camera. The following code is taken basicly from the original vrm script. """ camera = cameraObj numvert = len(face) # backface culling # translate and rotate according to the object matrix # and then translate according to the camera position #m = obj.getMatrix() #m.transpose() #a = m*Vector(face[0]) - Vector(cameraObj.loc) #b = m*Vector(face[1]) - Vector(cameraObj.loc) #c = m*Vector(face[numvert-1]) - Vector(cameraObj.loc) a = [] a.append(face[0][0]) a.append(face[0][1]) a.append(face[0][2]) a = RotatePoint(a[0], a[1], a[2], obj.RotX, obj.RotY, obj.RotZ) a[0] += obj.LocX - camera.LocX a[1] += obj.LocY - camera.LocY a[2] += obj.LocZ - camera.LocZ b = [] b.append(face[1][0]) b.append(face[1][1]) b.append(face[1][2]) b = RotatePoint(b[0], b[1], b[2], obj.RotX, obj.RotY, obj.RotZ) b[0] += obj.LocX - camera.LocX b[1] += obj.LocY - camera.LocY b[2] += obj.LocZ - camera.LocZ c = [] c.append(face[numvert-1][0]) c.append(face[numvert-1][1]) c.append(face[numvert-1][2]) c = RotatePoint(c[0], c[1], c[2], obj.RotX, obj.RotY, obj.RotZ) c[0] += obj.LocX - camera.LocX c[1] += obj.LocY - camera.LocY c[2] += obj.LocZ - camera.LocZ norm = [0, 0, 0] norm[0] = (b[1] - a[1])*(c[2] - a[2]) - (c[1] - a[1])*(b[2] - a[2]) norm[1] = -((b[0] - a[0])*(c[2] - a[2]) - (c[0] - a[0])*(b[2] - a[2])) norm[2] = (b[0] - a[0])*(c[1] - a[1]) - (c[0] - a[0])*(b[1] - a[1]) d = norm[0]*a[0] + norm[1]*a[1] + norm[2]*a[2] #d = DotVecs(Vector(norm), Vector(a)) return (d<0) def _isFaceVisible(self, face, obj, cameraObj): """Determine if the face is visible from the current camera. The following code is taken basicly from the original vrm script. """ camera = cameraObj numvert = len(face) # backface culling # translate and rotate according to the object matrix # and then translate according to the camera position m = obj.getMatrix() m.transpose() a = m*Vector(face[0]) - Vector(cameraObj.loc) b = m*Vector(face[1]) - Vector(cameraObj.loc) c = m*Vector(face[numvert-1]) - Vector(cameraObj.loc) norm = m*Vector(face.no) d = DotVecs(norm, a) return (d<0) def _doClipping(): return # Per object methods def _doVisibleSurfaceDetermination(object): return def _doColorizing(object): return def _doStylizingEdges(self, object, style): """Process Mesh Edges. (For now copy the edge data, in next version it can be a place where recognize silouhettes and/or contours). input: an edge list return: a processed edge list """ return # --------------------------------------------------------------------- # ## Main Program # # --------------------------------------------------------------------- # FIXME: really hackish code, just to test if the other parts work def depthSorting(scene): cameraObj = Scene.GetCurrent().getCurrentCamera() Objects = scene.getChildren() Objects.sort(lambda obj1, obj2: cmp(Vector(Vector(cameraObj.loc) - Vector(obj1.loc)).length, Vector(Vector(cameraObj.loc) - Vector(obj2.loc)).length ) ) # hackish sorting of faces according to the max z value of a vertex for o in Objects: mesh = o.data mesh.faces.sort( lambda f1, f2: # Sort faces according to the min z coordinate in a face #cmp(min([v[2] for v in f1]), min([v[2] for v in f2]))) # Sort faces according to the max z coordinate in a face cmp(max([v[2] for v in f1]), max([v[2] for v in f2]))) # Sort faces according to the avg z coordinate in a face #cmp(sum([v[2] for v in f1])/len(f1), sum([v[2] for v in f2])/len(f2))) mesh.faces.reverse() mesh.update() # update the scene for o in scene.getChildren(): scene.unlink(o) for o in Objects: scene.link(o) def vectorize(filename): """The vectorizing process is as follows: - Open the writer - Render the scene - Close the writer If you want to render an animation the second pass should be repeated for any frame, and the frame number should be passed to the renderer. """ print "Filename: %s" % filename scene = Scene.GetCurrent() renderer = Renderer() flatScene = renderer.doRendering(scene) canvasSize = renderer.getCanvasSize() depthSorting(flatScene) writer = SVGVectorWriter(filename, canvasSize) writer.printCanvas(flatScene) Blender.Scene.unlink(flatScene) del flatScene # Here the main if __name__ == "__main__": # with this trick we can run the script in batch mode try: Blender.Window.FileSelector (vectorize, 'Save SVG', "proba.svg") except: vectorize("proba.svg")