6 Tooltip: 'Vector Rendering Method script'
 
   9 __author__ = "Antonio Ospite"
 
  10 __url__ = ["http://vrm.projects.blender.org"]
 
  14     Render the scene and save the result in vector format.
 
  17 # ---------------------------------------------------------------------
 
  18 #    Copyright (c) 2006 Antonio Ospite
 
  20 #    This program is free software; you can redistribute it and/or modify
 
  21 #    it under the terms of the GNU General Public License as published by
 
  22 #    the Free Software Foundation; either version 2 of the License, or
 
  23 #    (at your option) any later version.
 
  25 #    This program is distributed in the hope that it will be useful,
 
  26 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  27 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  28 #    GNU General Public License for more details.
 
  30 #    You should have received a copy of the GNU General Public License
 
  31 #    along with this program; if not, write to the Free Software
 
  32 #    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
  34 # ---------------------------------------------------------------------
 
  37 #   Thanks to Emilio Aguirre for S2flender from which I took inspirations :)
 
  38 #   Thanks to Nikola Radovanovic, the author of the original VRM script,
 
  39 #       the code you read here has been rewritten _almost_ entirely
 
  40 #       from scratch but Nikola gave me the idea, so I thank him publicly.
 
  42 # ---------------------------------------------------------------------
 
  44 # Things TODO for a next release:
 
  45 #   - Switch to the Mesh structure, should be considerably faster
 
  46 #    (partially done, but with Mesh we cannot sort faces, yet)
 
  47 #   - Use a better depth sorting algorithm
 
  48 #   - Review how selections are made (this script uses selection states of
 
  49 #     primitives to represent visibility infos)
 
  50 #   - Implement clipping of primitives and do handle object intersections.
 
  51 #     (for now only clipping for whole objects is supported).
 
  52 #   - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
 
  53 #   - Implement Edge coloring
 
  54 #   - Use multiple lighting sources in color calculation
 
  55 #   - Implement Shading Styles? (for now we use Flat Shading).
 
  56 #   - Use a data structure other than Mesh to represent the 2D image? 
 
  57 #     Think to a way to merge adjacent polygons that have the same color.
 
  58 #     Or a way to use paths for silhouettes and contours.
 
  59 #   - Add Vector Writers other that SVG.
 
  60 #   - Consider SMIL for animation handling instead of ECMA Script? (Firefox do
 
  61 #     not support SMIL for animations)
 
  62 #   - FIX the issue with negative scales in object tranformations!
 
  64 # ---------------------------------------------------------------------
 
  68 #   vrm-0.3.py  -   2006-05-19
 
  69 #    * First release after code restucturing.
 
  70 #      Now the script offers a useful set of functionalities
 
  71 #      and it can render animations, too.
 
  73 # ---------------------------------------------------------------------
 
  76 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera
 
  77 from Blender.Mathutils import *
 
  81 # Some global settings
 
  85 POLYGON_EXPANSION_TRICK = True # Hidden to the user for now
 
  88 SHOW_HIDDEN_EDGES  = False
 
  89 EDGE_STYLE = 'silhouette'
 
  92 RENDER_ANIMATION = False
 
  94 OPTIMIZE_FOR_SPACE = True
 
 100 # ---------------------------------------------------------------------
 
 102 ## Utility Mesh class
 
 104 # ---------------------------------------------------------------------
 
 107     def getEdgeAdjacentFaces(edge, mesh):
 
 108         """Get the faces adjacent to a given edge.
 
 110         There can be 0, 1 or more (usually 2) faces adjacent to an edge.
 
 115             if (edge.v1 in f.v) and (edge.v2 in f.v):
 
 116                 adjface_list.append(f)
 
 120     def isVisibleEdge(e, mesh):
 
 121         """Normal edge selection rule.
 
 123         An edge is visible if _any_ of its adjacent faces is selected.
 
 124         Note: if the edge has no adjacent faces we want to show it as well,
 
 125         useful for "edge only" portion of objects.
 
 128         adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
 
 130         if len(adjacent_faces) == 0:
 
 133         selected_faces = [f for f in adjacent_faces if f.sel]
 
 135         if len(selected_faces) != 0:
 
 140     def isSilhouetteEdge(e, mesh):
 
 141         """Silhuette selection rule.
 
 143         An edge is a silhuette edge if it is shared by two faces with
 
 144         different selection status or if it is a boundary edge of a selected
 
 148         adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
 
 150         if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
 
 151             (len(adjacent_faces) == 2 and
 
 152                 adjacent_faces[0].sel != adjacent_faces[1].sel)
 
 158     getEdgeAdjacentFaces = staticmethod(getEdgeAdjacentFaces)
 
 159     isVisibleEdge = staticmethod(isVisibleEdge)
 
 160     isSilhouetteEdge = staticmethod(isSilhouetteEdge)
 
 164 # ---------------------------------------------------------------------
 
 166 ## Projections classes
 
 168 # ---------------------------------------------------------------------
 
 171     """Calculate the projection of an object given the camera.
 
 173     A projector is useful to so some per-object transformation to obtain the
 
 174     projection of an object given the camera.
 
 176     The main method is #doProjection# see the method description for the
 
 180     def __init__(self, cameraObj, canvasRatio):
 
 181         """Calculate the projection matrix.
 
 183         The projection matrix depends, in this case, on the camera settings.
 
 184         TAKE CARE: This projector expects vertices in World Coordinates!
 
 187         camera = cameraObj.getData()
 
 189         aspect = float(canvasRatio[0])/float(canvasRatio[1])
 
 190         near = camera.clipStart
 
 193         scale = float(camera.scale)
 
 195         fovy = atan(0.5/aspect/(camera.lens/32))
 
 196         fovy = fovy * 360.0/pi
 
 198         # What projection do we want?
 
 200             #mP = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale) 
 
 201             mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) 
 
 203             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
 
 205         # View transformation
 
 206         cam = Matrix(cameraObj.getInverseMatrix())
 
 211         self.projectionMatrix = mP
 
 217     def doProjection(self, v):
 
 218         """Project the point on the view plane.
 
 220         Given a vertex calculate the projection using the current projection
 
 224         # Note that we have to work on the vertex using homogeneous coordinates
 
 225         p = self.projectionMatrix * Vector(v).resize4D()
 
 241     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
 
 242         """Return a perspective projection matrix.
 
 245         top = near * tan(fovy * pi / 360.0)
 
 249         x = (2.0 * near) / (right-left)
 
 250         y = (2.0 * near) / (top-bottom)
 
 251         a = (right+left) / (right-left)
 
 252         b = (top+bottom) / (top - bottom)
 
 253         c = - ((far+near) / (far-near))
 
 254         d = - ((2*far*near)/(far-near))
 
 260                 [0.0, 0.0, -1.0,    0.0])
 
 264     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
 
 265         """Return an orthogonal projection matrix.
 
 268         # The 11 in the formula was found emiprically
 
 269         top = near * tan(fovy * pi / 360.0) * (scale * 11)
 
 271         left = bottom * aspect
 
 276         tx = -((right+left)/rl)
 
 277         ty = -((top+bottom)/tb)
 
 281                 [2.0/rl, 0.0,    0.0,     tx],
 
 282                 [0.0,    2.0/tb, 0.0,     ty],
 
 283                 [0.0,    0.0,    2.0/fn,  tz],
 
 284                 [0.0,    0.0,    0.0,    1.0])
 
 290 # ---------------------------------------------------------------------
 
 292 ## 2D Object representation class
 
 294 # ---------------------------------------------------------------------
 
 296 # TODO: a class to represent the needed properties of a 2D vector image
 
 297 # For now just using a [N]Mesh structure.
 
 300 # ---------------------------------------------------------------------
 
 302 ## Vector Drawing Classes
 
 304 # ---------------------------------------------------------------------
 
 310     A class for printing output in a vectorial format.
 
 312     Given a 2D representation of the 3D scene the class is responsible to
 
 313     write it is a vector format.
 
 315     Every subclasses of VectorWriter must have at last the following public
 
 319         - printCanvas(self, scene,
 
 320             doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
 
 323     def __init__(self, fileName):
 
 324         """Set the output file name and other properties"""
 
 326         self.outputFileName = fileName
 
 329         context = Scene.GetCurrent().getRenderingContext()
 
 330         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
 
 334         self.animation = False
 
 341     def open(self, startFrame=1, endFrame=1):
 
 342         if startFrame != endFrame:
 
 343             self.startFrame = startFrame
 
 344             self.endFrame = endFrame
 
 345             self.animation = True
 
 347         self.file = open(self.outputFileName, "w")
 
 348         print "Outputting to: ", self.outputFileName
 
 356     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
 
 357             showHiddenEdges=False):
 
 358         """This is the interface for the needed printing routine.
 
 365 class SVGVectorWriter(VectorWriter):
 
 366     """A concrete class for writing SVG output.
 
 369     def __init__(self, fileName):
 
 370         """Simply call the parent Contructor.
 
 372         VectorWriter.__init__(self, fileName)
 
 379     def open(self, startFrame=1, endFrame=1):
 
 380         """Do some initialization operations.
 
 382         VectorWriter.open(self, startFrame, endFrame)
 
 386         """Do some finalization operation.
 
 390         # remember to call the close method of the parent
 
 391         VectorWriter.close(self)
 
 394     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
 
 395             showHiddenEdges=False):
 
 396         """Convert the scene representation to SVG.
 
 399         Objects = scene.getChildren()
 
 401         context = scene.getRenderingContext()
 
 402         framenumber = context.currentFrame()
 
 405             framestyle = "display:none"
 
 407             framestyle = "display:block"
 
 409         # Assign an id to this group so we can set properties on it using DOM
 
 410         self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
 
 411                 (framenumber, framestyle) )
 
 416             if(obj.getType() != 'Mesh'):
 
 419             self.file.write("<g id=\"%s\">\n" % obj.getName())
 
 421             mesh = obj.getData(mesh=1)
 
 424                 self._printPolygons(mesh)
 
 427                 self._printEdges(mesh, showHiddenEdges)
 
 429             self.file.write("</g>\n")
 
 431         self.file.write("</g>\n")
 
 438     def _calcCanvasCoord(self, v):
 
 439         """Convert vertex in scene coordinates to canvas coordinates.
 
 442         pt = Vector([0, 0, 0])
 
 444         mW = float(self.canvasSize[0])/2.0
 
 445         mH = float(self.canvasSize[1])/2.0
 
 447         # rescale to canvas size
 
 448         pt[0] = v.co[0]*mW + mW
 
 449         pt[1] = v.co[1]*mH + mH
 
 452         # For now we want (0,0) in the top-left corner of the canvas.
 
 453         # Mirror and translate along y
 
 455         pt[1] += self.canvasSize[1]
 
 459     def _printHeader(self):
 
 460         """Print SVG header."""
 
 462         self.file.write("<?xml version=\"1.0\"?>\n")
 
 463         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n")
 
 464         self.file.write("\t\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
 
 465         self.file.write("<svg version=\"1.1\"\n")
 
 466         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
 
 467         self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
 
 472             self.file.write("""\n<script><![CDATA[
 
 476             /* FIXME: Use 1000 as interval as lower values gives problems */
 
 477             timerID = setInterval("NextFrame()", 1000);
 
 478             globalFrameCounter=%d;
 
 482               currentElement  = document.getElementById('frame'+globalFrameCounter)
 
 483               previousElement = document.getElementById('frame'+(globalFrameCounter-1))
 
 490               if (globalFrameCounter > globalEndFrame)
 
 492                 clearInterval(timerID)
 
 498                     previousElement.style.display="none";
 
 500                 currentElement.style.display="block";
 
 501                 globalFrameCounter++;
 
 505             \n""" % (self.startFrame, self.endFrame, self.startFrame) )
 
 507     def _printFooter(self):
 
 508         """Print the SVG footer."""
 
 510         self.file.write("\n</svg>\n")
 
 512     def _printPolygons(self, mesh):
 
 513         """Print the selected (visible) polygons.
 
 516         if len(mesh.faces) == 0:
 
 519         self.file.write("<g>\n")
 
 521         for face in mesh.faces:
 
 525             self.file.write("<polygon points=\"")
 
 528                 p = self._calcCanvasCoord(v)
 
 529                 self.file.write("%g,%g " % (p[0], p[1]))
 
 531             # get rid of the last blank space, just cosmetics here.
 
 532             self.file.seek(-1, 1) 
 
 533             self.file.write("\"\n")
 
 535             # take as face color the first vertex color
 
 536             # TODO: the average of vetrex colors?
 
 539                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
 
 541                 color = [255, 255, 255, 255]
 
 543             # use the stroke property to alleviate the "adjacent edges" problem,
 
 544             # we simulate polygon expansion using borders,
 
 545             # see http://www.antigrain.com/svg/index.html for more info
 
 549             # Convert the color to the #RRGGBB form
 
 550             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
 
 552             # Handle transparent polygons
 
 555                 opacity = float(color[3])/255.0
 
 556                 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
 
 558             self.file.write("\tstyle=\"fill:" + str_col + ";")
 
 559             self.file.write(opacity_string)
 
 560             if POLYGON_EXPANSION_TRICK:
 
 561                 self.file.write(" stroke:" + str_col + ";")
 
 562                 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
 
 563                 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
 
 564             self.file.write("\"/>\n")
 
 566         self.file.write("</g>\n")
 
 568     def _printEdges(self, mesh, showHiddenEdges=False):
 
 569         """Print the wireframe using mesh edges.
 
 572         stroke_width=EDGES_WIDTH
 
 573         stroke_col = [0, 0, 0]
 
 575         self.file.write("<g>\n")
 
 579             hidden_stroke_style = ""
 
 582                 if showHiddenEdges == False:
 
 585                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
 
 587             p1 = self._calcCanvasCoord(e.v1)
 
 588             p2 = self._calcCanvasCoord(e.v2)
 
 590             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
 
 591                     % ( p1[0], p1[1], p2[0], p2[1] ) )
 
 592             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
 
 593             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
 
 594             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
 
 595             self.file.write(hidden_stroke_style)
 
 596             self.file.write("\"/>\n")
 
 598         self.file.write("</g>\n")
 
 602 # ---------------------------------------------------------------------
 
 606 # ---------------------------------------------------------------------
 
 608 # A dictionary to collect all the different edge styles and their edge
 
 610 edgeSelectionStyles = {
 
 611         'normal': MeshUtils.isVisibleEdge,
 
 612         'silhouette': MeshUtils.isSilhouetteEdge
 
 615 # A dictionary to collect the supported output formats
 
 617         'SVG': SVGVectorWriter,
 
 622     """Render a scene viewed from a given camera.
 
 624     This class is responsible of the rendering process, transformation and
 
 625     projection of the objects in the scene are invoked by the renderer.
 
 627     The rendering is done using the active camera for the current scene.
 
 631         """Make the rendering process only for the current scene by default.
 
 633         We will work on a copy of the scene, be sure that the current scene do
 
 634         not get modified in any way.
 
 637         # Render the current Scene, this should be a READ-ONLY property
 
 638         self._SCENE = Scene.GetCurrent()
 
 640         # Use the aspect ratio of the scene rendering context
 
 641         context = self._SCENE.getRenderingContext()
 
 643         aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
 
 644         self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
 
 645                             float(context.aspectRatioY())
 
 648         # Render from the currently active camera 
 
 649         self.cameraObj = self._SCENE.getCurrentCamera()
 
 651         # Get a projector for this camera.
 
 652         # NOTE: the projector wants object in world coordinates,
 
 653         # so we should remember to apply modelview transformations
 
 654         # _before_ we do projection transformations.
 
 655         self.proj = Projector(self.cameraObj, self.canvasRatio)
 
 657         # Get the list of lighting sources
 
 658         obj_lst = self._SCENE.getChildren()
 
 659         self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
 
 661         # When there are no lights we use a default lighting source
 
 662         # that have the same position of the camera
 
 663         if len(self.lights) == 0:
 
 665             lobj = Object.New('Lamp')
 
 666             lobj.loc = self.cameraObj.loc
 
 668             self.lights.append(lobj)
 
 675     def doRendering(self, outputWriter, animation=False):
 
 676         """Render picture or animation and write it out.
 
 679             - a Vector writer object that will be used to output the result.
 
 680             - a flag to tell if we want to render an animation or only the
 
 684         context = self._SCENE.getRenderingContext()
 
 685         currentFrame = context.currentFrame()
 
 687         # Handle the animation case
 
 689             startFrame = currentFrame
 
 690             endFrame = startFrame
 
 693             startFrame = context.startFrame()
 
 694             endFrame = context.endFrame()
 
 695             outputWriter.open(startFrame, endFrame)
 
 697         # Do the rendering process frame by frame
 
 698         print "Start Rendering!"
 
 699         for f in range(startFrame, endFrame+1):
 
 700             context.currentFrame(f)
 
 702             # Use some temporary workspace, a full copy of the scene
 
 703             inputScene = self._SCENE.copy(2)
 
 706                 renderedScene = self.doRenderScene(inputScene)
 
 708                 self._SCENE.makeCurrent()
 
 709                 Scene.unlink(inputScene)
 
 712             outputWriter.printCanvas(renderedScene,
 
 713                     doPrintPolygons = PRINT_POLYGONS,
 
 714                     doPrintEdges    = PRINT_EDGES,
 
 715                     showHiddenEdges = SHOW_HIDDEN_EDGES)
 
 717             # clear the rendered scene
 
 718             self._SCENE.makeCurrent()
 
 719             Scene.unlink(renderedScene)
 
 724         context.currentFrame(currentFrame)
 
 727     def doRenderScene(self, workScene):
 
 728         """Control the rendering process.
 
 730         Here we control the entire rendering process invoking the operation
 
 731         needed to transform and project the 3D scene in two dimensions.
 
 734         # global processing of the scene
 
 736         self._doConvertGeometricObjToMesh(workScene)
 
 738         self._doSceneClipping(workScene)
 
 741         # XXX: Joining objects does not work in batch mode!!
 
 742         # Do not touch the following if, please :)
 
 744         global OPTIMIZE_FOR_SPACE
 
 745         if Blender.mode == 'background':
 
 746             print "\nWARNING! Joining objects not supported in background mode!\n"
 
 747             OPTIMIZE_FOR_SPACE = False
 
 749         if OPTIMIZE_FOR_SPACE:
 
 750             self._joinMeshObjectsInScene(workScene)
 
 753         self._doSceneDepthSorting(workScene)
 
 755         # Per object activities
 
 757         Objects = workScene.getChildren()
 
 760             if obj.getType() != 'Mesh':
 
 761                 print "Only Mesh supported! - Skipping type:", obj.getType()
 
 764             print "Rendering: ", obj.getName()
 
 768             self._doModelToWorldCoordinates(mesh, obj.matrix)
 
 770             self._doObjectDepthSorting(mesh)
 
 772             # We use both Mesh and NMesh because for depth sorting we change
 
 773             # face order and Mesh class don't let us to do that.
 
 775             mesh = obj.getData(mesh=1)
 
 777             self._doBackFaceCulling(mesh)
 
 779             self._doColorAndLighting(mesh)
 
 781             self._doEdgesStyle(mesh, edgeSelectionStyles[EDGE_STYLE])
 
 783             self._doProjection(mesh, self.proj)
 
 785             # Update the object data, important! :)
 
 797     def _getObjPosition(self, obj):
 
 798         """Return the obj position in World coordinates.
 
 800         return obj.matrix.translationPart()
 
 802     def _cameraViewDirection(self):
 
 803         """Get the View Direction form the camera matrix.
 
 805         return Vector(self.cameraObj.matrix[2]).resize3D()
 
 810     def _isFaceVisible(self, face):
 
 811         """Determine if a face of an object is visible from the current camera.
 
 813         The view vector is calculated from the camera location and one of the
 
 814         vertices of the face (expressed in World coordinates, after applying
 
 815         modelview transformations).
 
 817         After those transformations we determine if a face is visible by
 
 818         computing the angle between the face normal and the view vector, this
 
 819         angle has to be between -90 and 90 degrees for the face to be visible.
 
 820         This corresponds somehow to the dot product between the two, if it
 
 821         results > 0 then the face is visible.
 
 823         There is no need to normalize those vectors since we are only interested in
 
 824         the sign of the cross product and not in the product value.
 
 826         NOTE: here we assume the face vertices are in WorldCoordinates, so
 
 827         please transform the object _before_ doing the test.
 
 830         normal = Vector(face.no)
 
 831         camPos = self._getObjPosition(self.cameraObj)
 
 834         # View Vector in orthographics projections is the view Direction of
 
 836         if self.cameraObj.data.getType() == 1:
 
 837             view_vect = self._cameraViewDirection()
 
 839         # View vector in perspective projections can be considered as
 
 840         # the difference between the camera position and one point of
 
 841         # the face, we choose the farthest point from the camera.
 
 842         if self.cameraObj.data.getType() == 0:
 
 843             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
 
 846         # if d > 0 the face is visible from the camera
 
 847         d = view_vect * normal
 
 857     def _doConvertGeometricObjToMesh(self, scene):
 
 858         """Convert all "geometric" objects to mesh ones.
 
 860         geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
 
 862         Objects = scene.getChildren()
 
 863         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
 
 866             obj = self._convertToRawMeshObj(obj)
 
 868             scene.unlink(old_obj)
 
 871             # XXX Workaround for Text and Curve which have some normals
 
 872             # inverted when they are converted to Mesh, REMOVE that when
 
 873             # blender will fix that!!
 
 874             if old_obj.getType() in ['Curve', 'Text']:
 
 875                 me = obj.getData(mesh=1)
 
 876                 for f in me.faces: f.sel = 1;
 
 877                 for v in me.verts: v.sel = 1;
 
 884     def _doSceneClipping(self, scene):
 
 885         """Clip objects against the View Frustum.
 
 887         For now clip away only objects according to their center position.
 
 890         cpos = self._getObjPosition(self.cameraObj)
 
 891         view_vect = self._cameraViewDirection()
 
 893         near = self.cameraObj.data.clipStart
 
 894         far  = self.cameraObj.data.clipEnd
 
 896         aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
 
 897         fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
 
 898         fovy = fovy * 360.0/pi
 
 900         Objects = scene.getChildren()
 
 902             if o.getType() != 'Mesh': continue;
 
 904             obj_vect = Vector(cpos) - self._getObjPosition(o)
 
 906             d = obj_vect*view_vect
 
 907             theta = AngleBetweenVecs(obj_vect, view_vect)
 
 909             # if the object is outside the view frustum, clip it away
 
 910             if (d < near) or (d > far) or (theta > fovy):
 
 913     def _doSceneDepthSorting(self, scene):
 
 914         """Sort objects in the scene.
 
 916         The object sorting is done accordingly to the object centers.
 
 919         c = self._getObjPosition(self.cameraObj)
 
 921         by_center_pos = (lambda o1, o2:
 
 922                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
 
 923                 cmp((self._getObjPosition(o1) - Vector(c)).length,
 
 924                     (self._getObjPosition(o2) - Vector(c)).length)
 
 927         # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
 
 928         # then ob1 goes farther than obj2, useful when obj2 has holes
 
 931         Objects = scene.getChildren()
 
 932         Objects.sort(by_center_pos)
 
 939     def _joinMeshObjectsInScene(self, scene):
 
 940         """Merge all the Mesh Objects in a scene into a single Mesh Object.
 
 943         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
 
 945         # FIXME: Object.join() do not work if the list contains 1 object
 
 950         bigObj = Object.New('Mesh', 'BigOne')
 
 963     def _convertToRawMeshObj(self, object):
 
 964         """Convert geometry based object to a mesh object.
 
 966         me = Mesh.New('RawMesh_'+object.name)
 
 967         me.getFromObject(object.name)
 
 969         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
 
 972         # If the object has no materials set a default material
 
 974             me.materials = [Material.New()]
 
 975             #for f in me.faces: f.mat = 0
 
 977         newObject.setMatrix(object.getMatrix())
 
 981     def _doModelToWorldCoordinates(self, mesh, matrix):
 
 982         """Transform object coordinates to world coordinates.
 
 984         This step is done simply applying to the object its tranformation
 
 985         matrix and recalculating its normals.
 
 987         # XXX FIXME: blender do not transform normals in the right way when
 
 988         # there are negative scale values
 
 989         if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
 
 990             print "WARNING: Negative scales, expect incorrect results!"
 
 992         mesh.transform(matrix, True)
 
 994     def _doObjectDepthSorting(self, mesh):
 
 995         """Sort faces in an object.
 
 997         The faces in the object are sorted following the distance of the
 
 998         vertices from the camera position.
 
1000         c = self._getObjPosition(self.cameraObj)
 
1002         # hackish sorting of faces
 
1004         # Sort faces according to the max distance from the camera
 
1005         by_max_vert_dist = (lambda f1, f2:
 
1006                 cmp(max([(Vector(v.co)-Vector(c)).length for v in f1]),
 
1007                     max([(Vector(v.co)-Vector(c)).length for v in f2])))
 
1009         # Sort faces according to the min distance from the camera
 
1010         by_min_vert_dist = (lambda f1, f2:
 
1011                 cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]),
 
1012                     min([(Vector(v.co)-Vector(c)).length for v in f2])))
 
1014         # Sort faces according to the avg distance from the camera
 
1015         by_avg_vert_dist = (lambda f1, f2:
 
1016                 cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1),
 
1017                     sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2)))
 
1019         mesh.faces.sort(by_max_vert_dist)
 
1020         mesh.faces.reverse()
 
1022     def _doBackFaceCulling(self, mesh):
 
1023         """Simple Backface Culling routine.
 
1025         At this level we simply do a visibility test face by face and then
 
1026         select the vertices belonging to visible faces.
 
1029         # Select all vertices, so edges can be displayed even if there are no
 
1031         for v in mesh.verts:
 
1034         Mesh.Mode(Mesh.SelectModes['FACE'])
 
1036         for f in mesh.faces:
 
1038             if self._isFaceVisible(f):
 
1041         # Is this the correct way to propagate the face selection info to the
 
1042         # vertices belonging to a face ??
 
1043         # TODO: Using the Mesh module this should come for free. Right?
 
1044         #Mesh.Mode(Mesh.SelectModes['VERTEX'])
 
1045         #for f in mesh.faces:
 
1047         #        for v in f: v.sel = 0;
 
1049         #for f in mesh.faces:
 
1051         #        for v in f: v.sel = 1;
 
1053     def _doColorAndLighting(self, mesh):
 
1054         """Apply an Illumination model to the object.
 
1056         The Illumination model used is the Phong one, it may be inefficient,
 
1057         but I'm just learning about rendering and starting from Phong seemed
 
1058         the most natural way.
 
1061         # If the mesh has vertex colors already, use them,
 
1062         # otherwise turn them on and do some calculations
 
1063         if mesh.vertexColors:
 
1065         mesh.vertexColors = 1
 
1067         materials = mesh.materials
 
1069         # TODO: use multiple lighting sources
 
1070         light_obj = self.lights[0]
 
1071         light_pos = self._getObjPosition(light_obj)
 
1072         light = light_obj.data
 
1074         camPos = self._getObjPosition(self.cameraObj)
 
1076         # We do per-face color calculation (FLAT Shading), we can easily turn
 
1077         # to a per-vertex calculation if we want to implement some shading
 
1078         # technique. For an example see:
 
1079         # http://www.miralab.unige.ch/papers/368.pdf
 
1080         for f in mesh.faces:
 
1086                 mat = materials[f.mat]
 
1088             # A new default material
 
1090                 mat = Material.New('defMat')
 
1092             L = Vector(light_pos).normalize()
 
1094             V = (Vector(camPos) - Vector(f.v[0].co)).normalize()
 
1096             N = Vector(f.no).normalize()
 
1098             R = 2 * (N*L) * N - L
 
1100             # TODO: Attenuation factor (not used for now)
 
1101             a0 = 1; a1 = 0.0; a2 = 0.0
 
1102             d = (Vector(f.v[0].co) - Vector(light_pos)).length
 
1103             fd = min(1, 1.0/(a0 + a1*d + a2*d*d))
 
1107             ka = mat.getAmb() * Vector([0.1, 0.1, 0.1])
 
1110             # Diffuse component (add light.col for kd)
 
1111             kd = mat.getRef() * Vector(mat.getRGBCol())
 
1112             Ip = light.getEnergy()
 
1113             Idiff = Ip * kd * (N*L)
 
1115             # Specular component
 
1116             ks = mat.getSpec() * Vector(mat.getSpecCol())
 
1117             ns = mat.getHardness()
 
1118             Ispec = Ip * ks * pow((V * R), ns)
 
1120             # Emissive component
 
1121             ki = Vector([mat.getEmit()]*3)
 
1123             I = ki + Iamb + Idiff + Ispec
 
1125             # Set Alpha component
 
1127             I.append(mat.getAlpha())
 
1129             # Clamp I values between 0 and 1
 
1130             I = [ min(c, 1) for c in I]
 
1131             I = [ max(0, c) for c in I]
 
1132             tmp_col = [ int(c * 255.0) for c in I]
 
1140     def _doEdgesStyle(self, mesh, edgestyleSelect):
 
1141         """Process Mesh Edges accroding to a given selection style.
 
1143         Examples of algorithms:
 
1146             given an edge if its adjacent faces have the same normal (that is
 
1147             they are complanar), than deselect it.
 
1150             given an edge if one its adjacent faces is frontfacing and the
 
1151             other is backfacing, than select it, else deselect.
 
1154         Mesh.Mode(Mesh.SelectModes['EDGE'])
 
1156         for e in mesh.edges:
 
1159             if edgestyleSelect(e, mesh):
 
1162     def _doProjection(self, mesh, projector):
 
1163         """Calculate the Projection for the object.
 
1165         # TODO: maybe using the object.transform() can be faster?
 
1167         for v in mesh.verts:
 
1168             p = projector.doProjection(v.co)
 
1175 # ---------------------------------------------------------------------
 
1177 ## GUI Class and Main Program
 
1179 # ---------------------------------------------------------------------
 
1182 from Blender import BGL, Draw
 
1183 from Blender.BGL import *
 
1189         # Output Format menu 
 
1190         default_value = outputWriters.keys().index(OUTPUT_FORMAT)+1
 
1191         GUI.outFormatMenu = Draw.Create(default_value)
 
1192         GUI.evtOutFormatMenu = 0
 
1194         # Animation toggle button
 
1195         GUI.animToggle = Draw.Create(RENDER_ANIMATION)
 
1196         GUI.evtAnimToggle = 1
 
1198         # Join Objects toggle button
 
1199         GUI.joinObjsToggle = Draw.Create(OPTIMIZE_FOR_SPACE)
 
1200         GUI.evtJoinObjsToggle = 2
 
1202         # Render filled polygons
 
1203         GUI.polygonsToggle = Draw.Create(PRINT_POLYGONS)
 
1204         GUI.evtPolygonsToggle = 3
 
1205         # We hide the POLYGON_EXPANSION_TRICK, for now
 
1207         # Render polygon edges
 
1208         GUI.showEdgesToggle = Draw.Create(PRINT_EDGES)
 
1209         GUI.evtShowEdgesToggle = 4
 
1211         # Render hidden edges
 
1212         GUI.showHiddenEdgesToggle = Draw.Create(SHOW_HIDDEN_EDGES)
 
1213         GUI.evtShowHiddenEdgesToggle = 5
 
1216         default_value = edgeSelectionStyles.keys().index(EDGE_STYLE)+1
 
1217         GUI.edgeStyleMenu = Draw.Create(default_value)
 
1218         GUI.evtEdgeStyleMenu = 6
 
1221         GUI.edgeWidthSlider = Draw.Create(EDGES_WIDTH)
 
1222         GUI.evtEdgeWidthSlider = 7
 
1225         GUI.evtRenderButton = 8
 
1228         GUI.evtExitButton = 9
 
1232         # initialize static members
 
1235         glClear(GL_COLOR_BUFFER_BIT)
 
1236         glColor3f(0.0, 0.0, 0.0)
 
1237         glRasterPos2i(10, 350)
 
1238         Draw.Text("VRM: Vector Rendering Method script.")
 
1239         glRasterPos2i(10, 335)
 
1240         Draw.Text("Press Q or ESC to quit.")
 
1242         # Build the output format menu
 
1243         glRasterPos2i(10, 310)
 
1244         Draw.Text("Select the output Format:")
 
1245         outMenuStruct = "Output Format %t"
 
1246         for t in outputWriters.keys():
 
1247            outMenuStruct = outMenuStruct + "|%s" % t
 
1248         GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
 
1249                 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
 
1252         GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
 
1253                 10, 260, 160, 18, GUI.animToggle.val,
 
1254                 "Toggle rendering of animations")
 
1256         # Join Objects toggle
 
1257         GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
 
1258                 10, 235, 160, 18, GUI.joinObjsToggle.val,
 
1259                 "Join objects in the rendered file")
 
1262         Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
 
1264         Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
 
1267         glRasterPos2i(200, 310)
 
1268         Draw.Text("Rendering Style:")
 
1271         GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
 
1272                 200, 285, 160, 18, GUI.polygonsToggle.val,
 
1273                 "Render filled polygons")
 
1276         GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
 
1277                 200, 260, 160, 18, GUI.showEdgesToggle.val,
 
1278                 "Render polygon edges")
 
1280         if GUI.showEdgesToggle.val == 1:
 
1283             edgeStyleMenuStruct = "Edge Style %t"
 
1284             for t in edgeSelectionStyles.keys():
 
1285                edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t
 
1286             GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
 
1287                     200, 235, 160, 18, GUI.edgeStyleMenu.val,
 
1288                     "Choose the edge style")
 
1291             GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
 
1292                     200, 210, 160, 18, GUI.edgeWidthSlider.val,
 
1293                     0.0, 10.0, 0, "Change Edge Width")
 
1296             GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
 
1297                     GUI.evtShowHiddenEdgesToggle,
 
1298                     200, 185, 160, 18, GUI.showHiddenEdgesToggle.val,
 
1299                     "Render hidden edges as dashed lines")
 
1301         glRasterPos2i(10, 160)
 
1302         Draw.Text("Antonio Ospite (c) 2006")
 
1304     def event(evt, val):
 
1306         if evt == Draw.ESCKEY or evt == Draw.QKEY:
 
1313     def button_event(evt):
 
1314         global PRINT_POLYGONS
 
1315         global POLYGON_EXPANSION_TRICK
 
1317         global SHOW_HIDDEN_EDGES
 
1320         global RENDER_ANIMATION
 
1321         global OPTIMIZE_FOR_SPACE
 
1322         global OUTPUT_FORMAT
 
1324         if evt == GUI.evtExitButton:
 
1326         elif evt == GUI.evtOutFormatMenu:
 
1327             i = GUI.outFormatMenu.val - 1
 
1328             OUTPUT_FORMAT = outputWriters.keys()[i]
 
1329         elif evt == GUI.evtAnimToggle:
 
1330             RENDER_ANIMATION = bool(GUI.animToggle.val)
 
1331         elif evt == GUI.evtJoinObjsToggle:
 
1332             OPTIMIZE_FOR_SPACE = bool(GUI.joinObjsToggle.val)
 
1333         elif evt == GUI.evtPolygonsToggle:
 
1334             PRINT_POLYGONS = bool(GUI.polygonsToggle.val)
 
1335         elif evt == GUI.evtShowEdgesToggle:
 
1336             PRINT_EDGES = bool(GUI.showEdgesToggle.val)
 
1337         elif evt == GUI.evtShowHiddenEdgesToggle:
 
1338             SHOW_HIDDEN_EDGES = bool(GUI.showHiddenEdgesToggle.val)
 
1339         elif evt == GUI.evtEdgeStyleMenu:
 
1340             i = GUI.edgeStyleMenu.val - 1
 
1341             EDGE_STYLE = edgeSelectionStyles.keys()[i]
 
1342         elif evt == GUI.evtEdgeWidthSlider:
 
1343             EDGES_WIDTH = float(GUI.edgeWidthSlider.val)
 
1344         elif evt == GUI.evtRenderButton:
 
1345             label = "Save %s" % OUTPUT_FORMAT
 
1346             # Show the File Selector
 
1348             Blender.Window.FileSelector(vectorize, label, outputfile)
 
1351             print "Event: %d not handled!" % evt
 
1359         print "PRINT_POLYGONS:", PRINT_POLYGONS
 
1360         print "POLYGON_EXPANSION_TRICK:", POLYGON_EXPANSION_TRICK
 
1361         print "PRINT_EDGES:", PRINT_EDGES
 
1362         print "SHOW_HIDDEN_EDGES:", SHOW_HIDDEN_EDGES
 
1363         print "EDGE_STYLE:", EDGE_STYLE
 
1364         print "EDGES_WIDTH:", EDGES_WIDTH
 
1365         print "RENDER_ANIMATION:", RENDER_ANIMATION
 
1366         print "OPTIMIZE_FOR_SPACE:", OPTIMIZE_FOR_SPACE
 
1367         print "OUTPUT_FORMAT:", OUTPUT_FORMAT
 
1369     _init = staticmethod(_init)
 
1370     draw = staticmethod(draw)
 
1371     event = staticmethod(event)
 
1372     button_event = staticmethod(button_event)
 
1373     conf_debug = staticmethod(conf_debug)
 
1375 # A wrapper function for the vectorizing process
 
1376 def vectorize(filename):
 
1377     """The vectorizing process is as follows:
 
1379      - Instanciate the writer and the renderer
 
1384         print "\nERROR: invalid file name!"
 
1387     from Blender import Window
 
1388     editmode = Window.EditMode()
 
1389     if editmode: Window.EditMode(0)
 
1391     actualWriter = outputWriters[OUTPUT_FORMAT]
 
1392     writer = actualWriter(filename)
 
1394     renderer = Renderer()
 
1395     renderer.doRendering(writer, RENDER_ANIMATION)
 
1397     if editmode: Window.EditMode(1) 
 
1401 if __name__ == "__main__":
 
1404     basename = Blender.sys.basename(Blender.Get('filename'))
 
1406         outputfile = Blender.sys.splitext(basename)[0] + "." + str(OUTPUT_FORMAT).lower()
 
1408     if Blender.mode == 'background':
 
1409         vectorize(outputfile)
 
1411         Draw.Register(GUI.draw, GUI.event, GUI.button_event)