Edge styles (silhouettes, contours)
[vrm.git] / vrm.py
1 #!BPY
2 """
3 Name: 'VRM'
4 Blender: 241
5 Group: 'Render'
6 Tooltip: 'Vector Rendering Method script'
7 """
8
9 __author__ = "Antonio Ospite"
10 __url__ = ["http://vrm.projects.blender.org"]
11 __version__ = "0.3"
12
13 __bpydoc__ = """\
14     Render the scene and save the result in vector format.
15 """
16
17 # ---------------------------------------------------------------------
18 #    Copyright (c) 2006 Antonio Ospite
19 #
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.
24 #
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.
29 #
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
33 #
34 # ---------------------------------------------------------------------
35 #
36 # Additional credits:
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.
41 #
42 # ---------------------------------------------------------------------
43
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?
61 #
62 # ---------------------------------------------------------------------
63 #
64 # Changelog:
65 #
66 #   vrm-0.3.py  -   2006-05-19
67 #    * First release after code restucturing.
68 #      Now the script offers a useful set of functionalities
69 #      and it can render animations, too.
70 #
71 # ---------------------------------------------------------------------
72
73 import Blender
74 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera
75 from Blender.Mathutils import *
76 from math import *
77
78
79 # Some global settings
80 PRINT_POLYGONS     = True
81 POLYGON_EXPANSION_TRICK = True
82
83 PRINT_EDGES        = True
84 SHOW_HIDDEN_EDGES  = False
85 #EDGE_STYLE = 'normal'
86 EDGE_STYLE = 'silhouette'
87 EDGES_WIDTH = 0.5
88
89 RENDER_ANIMATION = False
90
91 OPTIMIZE_FOR_SPACE = True
92
93 OUTPUT_FORMAT = 'SVG'
94
95
96 # ---------------------------------------------------------------------
97 #
98 ## Utility Mesh class
99 #
100 # ---------------------------------------------------------------------
101 class MeshUtils:
102     def __init__(self):
103         return
104
105     def getEdgeAdjacentFaces(self, edge, mesh):
106         """Get the faces adjacent to a given edge.
107
108         There can be 0, 1 or more (usually 2) faces adjacent to an edge.
109         """
110         adjface_list = []
111
112         for f in mesh.faces:
113             if (edge.v1 in f.v) and (edge.v2 in f.v):
114                 adjface_list.append(f)
115
116         return adjface_list
117
118     def isVisibleEdge(self, e, mesh):
119         """Normal edge selection rule.
120
121         An edge is visible if _any_ of its adjacent faces is selected.
122         Note: if the edge has no adjacent faces we want to show it as well,
123         useful for "edge only" portion of objects.
124         """
125
126         adjacent_faces = self.getEdgeAdjacentFaces(e, mesh)
127
128         if len(adjacent_faces) == 0:
129             return True
130
131         selected_faces = [f for f in adjacent_faces if f.sel]
132
133         if len(selected_faces) != 0:
134             return True
135         else:
136             return False
137
138     def isSilhouetteEdge(self, e, mesh):
139         """Silhuette selection rule.
140
141         An edge is a silhuette edge if it is shared by two faces with
142         different selection status or if it is a boundary edge of a selected
143         face.
144         """
145
146         adjacent_faces = self.getEdgeAdjacentFaces(e, mesh)
147
148         if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
149             (len(adjacent_faces) == 2 and
150                 adjacent_faces[0].sel != adjacent_faces[1].sel)
151             ):
152             return True
153         else:
154             return False
155
156
157
158 # ---------------------------------------------------------------------
159 #
160 ## Projections classes
161 #
162 # ---------------------------------------------------------------------
163
164 class Projector:
165     """Calculate the projection of an object given the camera.
166     
167     A projector is useful to so some per-object transformation to obtain the
168     projection of an object given the camera.
169     
170     The main method is #doProjection# see the method description for the
171     parameter list.
172     """
173
174     def __init__(self, cameraObj, canvasRatio):
175         """Calculate the projection matrix.
176
177         The projection matrix depends, in this case, on the camera settings.
178         TAKE CARE: This projector expects vertices in World Coordinates!
179         """
180
181         camera = cameraObj.getData()
182
183         aspect = float(canvasRatio[0])/float(canvasRatio[1])
184         near = camera.clipStart
185         far = camera.clipEnd
186
187         scale = float(camera.scale)
188
189         fovy = atan(0.5/aspect/(camera.lens/32))
190         fovy = fovy * 360.0/pi
191         
192         # What projection do we want?
193         if camera.type:
194             #mP = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale) 
195             mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) 
196         else:
197             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
198         
199         # View transformation
200         cam = Matrix(cameraObj.getInverseMatrix())
201         cam.transpose() 
202         
203         mP = mP * cam
204
205         self.projectionMatrix = mP
206
207     ##
208     # Public methods
209     #
210
211     def doProjection(self, v):
212         """Project the point on the view plane.
213
214         Given a vertex calculate the projection using the current projection
215         matrix.
216         """
217         
218         # Note that we have to work on the vertex using homogeneous coordinates
219         p = self.projectionMatrix * Vector(v).resize4D()
220
221         if p[3]>0:
222             p[0] = p[0]/p[3]
223             p[1] = p[1]/p[3]
224
225         # restore the size
226         p[3] = 1.0
227         p.resize3D()
228
229         return p
230
231     ##
232     # Private methods
233     #
234     
235     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
236         """Return a perspective projection matrix.
237         """
238         
239         top = near * tan(fovy * pi / 360.0)
240         bottom = -top
241         left = bottom*aspect
242         right= top*aspect
243         x = (2.0 * near) / (right-left)
244         y = (2.0 * near) / (top-bottom)
245         a = (right+left) / (right-left)
246         b = (top+bottom) / (top - bottom)
247         c = - ((far+near) / (far-near))
248         d = - ((2*far*near)/(far-near))
249         
250         m = Matrix(
251                 [x,   0.0,    a,    0.0],
252                 [0.0,   y,    b,    0.0],
253                 [0.0, 0.0,    c,      d],
254                 [0.0, 0.0, -1.0,    0.0])
255
256         return m
257
258     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
259         """Return an orthogonal projection matrix.
260         """
261         
262         # The 11 in the formula was found emiprically
263         top = near * tan(fovy * pi / 360.0) * (scale * 11)
264         bottom = -top 
265         left = bottom * aspect
266         right= top * aspect
267         rl = right-left
268         tb = top-bottom
269         fn = near-far 
270         tx = -((right+left)/rl)
271         ty = -((top+bottom)/tb)
272         tz = ((far+near)/fn)
273
274         m = Matrix(
275                 [2.0/rl, 0.0,    0.0,     tx],
276                 [0.0,    2.0/tb, 0.0,     ty],
277                 [0.0,    0.0,    2.0/fn,  tz],
278                 [0.0,    0.0,    0.0,    1.0])
279         
280         return m
281
282
283
284 # ---------------------------------------------------------------------
285 #
286 ## 2D Object representation class
287 #
288 # ---------------------------------------------------------------------
289
290 # TODO: a class to represent the needed properties of a 2D vector image
291 # For now just using a [N]Mesh structure.
292
293
294 # ---------------------------------------------------------------------
295 #
296 ## Vector Drawing Classes
297 #
298 # ---------------------------------------------------------------------
299
300 ## A generic Writer
301
302 class VectorWriter:
303     """
304     A class for printing output in a vectorial format.
305
306     Given a 2D representation of the 3D scene the class is responsible to
307     write it is a vector format.
308
309     Every subclasses of VectorWriter must have at last the following public
310     methods:
311         - open(self)
312         - close(self)
313         - printCanvas(self, scene,
314             doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
315     """
316     
317     def __init__(self, fileName):
318         """Set the output file name and other properties"""
319
320         self.outputFileName = fileName
321         self.file = None
322         
323         context = Scene.GetCurrent().getRenderingContext()
324         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
325
326         self.startFrame = 1
327         self.endFrame = 1
328         self.animation = False
329
330
331     ##
332     # Public Methods
333     #
334     
335     def open(self, startFrame=1, endFrame=1):
336         if startFrame != endFrame:
337             self.startFrame = startFrame
338             self.endFrame = endFrame
339             self.animation = True
340
341         self.file = open(self.outputFileName, "w")
342         print "Outputting to: ", self.outputFileName
343
344         return
345
346     def close(self):
347         self.file.close()
348         return
349
350     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
351             showHiddenEdges=False):
352         """This is the interface for the needed printing routine.
353         """
354         return
355         
356
357 ## SVG Writer
358
359 class SVGVectorWriter(VectorWriter):
360     """A concrete class for writing SVG output.
361     """
362
363     def __init__(self, fileName):
364         """Simply call the parent Contructor.
365         """
366         VectorWriter.__init__(self, fileName)
367
368
369     ##
370     # Public Methods
371     #
372
373     def open(self, startFrame=1, endFrame=1):
374         """Do some initialization operations.
375         """
376         VectorWriter.open(self, startFrame, endFrame)
377         self._printHeader()
378
379     def close(self):
380         """Do some finalization operation.
381         """
382         self._printFooter()
383
384         # remember to call the close method of the parent
385         VectorWriter.close(self)
386
387         
388     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
389             showHiddenEdges=False):
390         """Convert the scene representation to SVG.
391         """
392
393         Objects = scene.getChildren()
394
395         context = scene.getRenderingContext()
396         framenumber = context.currentFrame()
397
398         if self.animation:
399             framestyle = "display:none"
400         else:
401             framestyle = "display:block"
402         
403         # Assign an id to this group so we can set properties on it using DOM
404         self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
405                 (framenumber, framestyle) )
406
407         for obj in Objects:
408
409             if(obj.getType() != 'Mesh'):
410                 continue
411
412             self.file.write("<g id=\"%s\">\n" % obj.getName())
413
414             mesh = obj.getData(mesh=1)
415
416             if doPrintPolygons:
417                 self._printPolygons(mesh)
418
419             if doPrintEdges:
420                 self._printEdges(mesh, showHiddenEdges)
421             
422             self.file.write("</g>\n")
423
424         self.file.write("</g>\n")
425
426     
427     ##  
428     # Private Methods
429     #
430     
431     def _calcCanvasCoord(self, v):
432         """Convert vertex in scene coordinates to canvas coordinates.
433         """
434
435         pt = Vector([0, 0, 0])
436         
437         mW = float(self.canvasSize[0])/2.0
438         mH = float(self.canvasSize[1])/2.0
439
440         # rescale to canvas size
441         pt[0] = v.co[0]*mW + mW
442         pt[1] = v.co[1]*mH + mH
443         pt[2] = v.co[2]
444          
445         # For now we want (0,0) in the top-left corner of the canvas.
446         # Mirror and translate along y
447         pt[1] *= -1
448         pt[1] += self.canvasSize[1]
449         
450         return pt
451
452     def _printHeader(self):
453         """Print SVG header."""
454
455         self.file.write("<?xml version=\"1.0\"?>\n")
456         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n")
457         self.file.write("\t\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
458         self.file.write("<svg version=\"1.1\"\n")
459         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
460         self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
461                 self.canvasSize)
462
463         if self.animation:
464
465             self.file.write("""\n<script><![CDATA[
466             globalStartFrame=%d;
467             globalEndFrame=%d;
468
469             /* FIXME: Use 1000 as interval as lower values gives problems */
470             timerID = setInterval("NextFrame()", 1000);
471             globalFrameCounter=%d;
472
473             function NextFrame()
474             {
475               currentElement  = document.getElementById('frame'+globalFrameCounter)
476               previousElement = document.getElementById('frame'+(globalFrameCounter-1))
477
478               if (!currentElement)
479               {
480                 return;
481               }
482
483               if (globalFrameCounter > globalEndFrame)
484               {
485                 clearInterval(timerID)
486               }
487               else
488               {
489                 if(previousElement)
490                 {
491                     previousElement.style.display="none";
492                 }
493                 currentElement.style.display="block";
494                 globalFrameCounter++;
495               }
496             }
497             \n]]></script>\n
498             \n""" % (self.startFrame, self.endFrame, self.startFrame) )
499                 
500     def _printFooter(self):
501         """Print the SVG footer."""
502
503         self.file.write("\n</svg>\n")
504
505     def _printPolygons(self, mesh):
506         """Print the selected (visible) polygons.
507         """
508
509         if len(mesh.faces) == 0:
510             return
511
512         self.file.write("<g>\n")
513
514         for face in mesh.faces:
515             if not face.sel:
516                continue
517
518             self.file.write("<polygon points=\"")
519
520             for v in face:
521                 p = self._calcCanvasCoord(v)
522                 self.file.write("%g,%g " % (p[0], p[1]))
523             
524             # get rid of the last blank space, just cosmetics here.
525             self.file.seek(-1, 1) 
526             self.file.write("\"\n")
527             
528             # take as face color the first vertex color
529             # TODO: the average of vetrex colors?
530             if face.col:
531                 fcol = face.col[0]
532                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
533             else:
534                 color = [255, 255, 255, 255]
535
536             # use the stroke property to alleviate the "adjacent edges" problem,
537             # we simulate polygon expansion using borders,
538             # see http://www.antigrain.com/svg/index.html for more info
539             stroke_col = color
540             stroke_width = 0.5
541
542             # Convert the color to the #RRGGBB form
543             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
544
545             # Handle transparent polygons
546             opacity_string = ""
547             if color[3] != 255:
548                 opacity = float(color[3])/255.0
549                 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
550
551             self.file.write("\tstyle=\"fill:" + str_col + ";")
552             self.file.write(opacity_string)
553             if POLYGON_EXPANSION_TRICK:
554                 self.file.write(" stroke:" + str_col + ";")
555                 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
556                 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
557             self.file.write("\"/>\n")
558
559         self.file.write("</g>\n")
560
561     def _printEdges(self, mesh, showHiddenEdges=False):
562         """Print the wireframe using mesh edges.
563         """
564
565         stroke_width=EDGES_WIDTH
566         stroke_col = [0, 0, 0]
567         
568         self.file.write("<g>\n")
569
570         for e in mesh.edges:
571             
572             hidden_stroke_style = ""
573             
574             # We consider an edge visible if _both_ its vertices are selected,
575             # hence an edge is hidden if _any_ of its vertices is deselected.
576             if e.sel == 0:
577                 if showHiddenEdges == False:
578                     continue
579                 else:
580                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
581
582             p1 = self._calcCanvasCoord(e.v1)
583             p2 = self._calcCanvasCoord(e.v2)
584             
585             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
586                     % ( p1[0], p1[1], p2[0], p2[1] ) )
587             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
588             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
589             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
590             self.file.write(hidden_stroke_style)
591             self.file.write("\"/>\n")
592
593         self.file.write("</g>\n")
594
595
596
597 # ---------------------------------------------------------------------
598 #
599 ## Rendering Classes
600 #
601 # ---------------------------------------------------------------------
602
603 class Renderer:
604     """Render a scene viewed from a given camera.
605     
606     This class is responsible of the rendering process, transformation and
607     projection of the objects in the scene are invoked by the renderer.
608
609     The rendering is done using the active camera for the current scene.
610     """
611
612     def __init__(self):
613         """Make the rendering process only for the current scene by default.
614
615         We will work on a copy of the scene, be sure that the current scene do
616         not get modified in any way.
617         """
618
619         # Render the current Scene, this should be a READ-ONLY property
620         self._SCENE = Scene.GetCurrent()
621         
622         # Use the aspect ratio of the scene rendering context
623         context = self._SCENE.getRenderingContext()
624
625         aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
626         self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
627                             float(context.aspectRatioY())
628                             )
629
630         # Render from the currently active camera 
631         self.cameraObj = self._SCENE.getCurrentCamera()
632
633         # Get the list of lighting sources
634         obj_lst = self._SCENE.getChildren()
635         self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
636
637         if len(self.lights) == 0:
638             l = Lamp.New('Lamp')
639             lobj = Object.New('Lamp')
640             lobj.link(l) 
641             self.lights.append(lobj)
642
643
644     ##
645     # Public Methods
646     #
647
648     def doRendering(self, outputWriter, animation=False):
649         """Render picture or animation and write it out.
650         
651         The parameters are:
652             - a Vector writer object that will be used to output the result.
653             - a flag to tell if we want to render an animation or only the
654               current frame.
655         """
656         
657         context = self._SCENE.getRenderingContext()
658         currentFrame = context.currentFrame()
659
660         # Handle the animation case
661         if not animation:
662             startFrame = currentFrame
663             endFrame = startFrame
664             outputWriter.open()
665         else:
666             startFrame = context.startFrame()
667             endFrame = context.endFrame()
668             outputWriter.open(startFrame, endFrame)
669         
670         # Do the rendering process frame by frame
671         print "Start Rendering!"
672         for f in range(startFrame, endFrame+1):
673             context.currentFrame(f)
674
675             renderedScene = self.doRenderScene(self._SCENE)
676             outputWriter.printCanvas(renderedScene,
677                     doPrintPolygons = PRINT_POLYGONS,
678                     doPrintEdges    = PRINT_EDGES,
679                     showHiddenEdges = SHOW_HIDDEN_EDGES)
680             
681             # clear the rendered scene
682             self._SCENE.makeCurrent()
683             Scene.unlink(renderedScene)
684             del renderedScene
685
686         outputWriter.close()
687         print "Done!"
688         context.currentFrame(currentFrame)
689
690
691     def doRenderScene(self, inputScene):
692         """Control the rendering process.
693         
694         Here we control the entire rendering process invoking the operation
695         needed to transform and project the 3D scene in two dimensions.
696         """
697         
698         # Use some temporary workspace, a full copy of the scene
699         workScene = inputScene.copy(2)
700
701         # Get a projector for this scene.
702         # NOTE: the projector wants object in world coordinates,
703         # so we should apply modelview transformations _before_
704         # projection transformations
705         proj = Projector(self.cameraObj, self.canvasRatio)
706
707         # global processing of the scene
708
709         self._doConvertGeometricObjToMesh(workScene)
710
711         self._doSceneClipping(workScene)
712
713
714         # XXX: Joining objects does not work in batch mode!!
715         # Do not touch the following if, please :)
716
717         global OPTIMIZE_FOR_SPACE
718         if Blender.mode == 'background':
719             print "\nWARNING! Joining objects not supported in background mode!\n"
720             OPTIMIZE_FOR_SPACE = False
721
722         if OPTIMIZE_FOR_SPACE:
723             self._joinMeshObjectsInScene(workScene)
724
725
726         self._doSceneDepthSorting(workScene)
727         
728         # Per object activities
729
730         Objects = workScene.getChildren()
731         for obj in Objects:
732             
733             if obj.getType() != 'Mesh':
734                 print "Only Mesh supported! - Skipping type:", obj.getType()
735                 continue
736
737             print "Rendering: ", obj.getName()
738
739             mesh = obj.getData()
740
741             self._doModelToWorldCoordinates(mesh, obj.matrix)
742
743             self._doObjectDepthSorting(mesh)
744             
745             # We use both Mesh and NMesh because for depth sorting we change
746             # face order and Mesh class don't let us to do that.
747             mesh.update()
748             mesh = obj.getData(mesh=1)
749             
750             self._doBackFaceCulling(mesh)
751             
752             self._doColorAndLighting(mesh)
753
754             self._doEdgesStyle(mesh, edgeSelectionStyles[EDGE_STYLE])
755
756             self._doProjection(mesh, proj)
757             
758             # Update the object data, important! :)
759             mesh.update()
760
761         return workScene
762
763
764     ##
765     # Private Methods
766     #
767
768     # Utility methods
769
770     def _getObjPosition(self, obj):
771         """Return the obj position in World coordinates.
772         """
773         return obj.matrix.translationPart()
774
775     def _cameraViewDirection(self):
776         """Get the View Direction form the camera matrix.
777         """
778         return Vector(self.cameraObj.matrix[2]).resize3D()
779
780
781     # Faces methods
782
783     def _isFaceVisible(self, face):
784         """Determine if a face of an object is visible from the current camera.
785         
786         The view vector is calculated from the camera location and one of the
787         vertices of the face (expressed in World coordinates, after applying
788         modelview transformations).
789
790         After those transformations we determine if a face is visible by
791         computing the angle between the face normal and the view vector, this
792         angle has to be between -90 and 90 degrees for the face to be visible.
793         This corresponds somehow to the dot product between the two, if it
794         results > 0 then the face is visible.
795
796         There is no need to normalize those vectors since we are only interested in
797         the sign of the cross product and not in the product value.
798
799         NOTE: here we assume the face vertices are in WorldCoordinates, so
800         please transform the object _before_ doing the test.
801         """
802
803         normal = Vector(face.no)
804         camPos = self._getObjPosition(self.cameraObj)
805         view_vect = None
806
807         # View Vector in orthographics projections is the view Direction of
808         # the camera
809         if self.cameraObj.data.getType() == 1:
810             view_vect = self._cameraViewDirection()
811
812         # View vector in perspective projections can be considered as
813         # the difference between the camera position and one point of
814         # the face, we choose the farthest point from the camera.
815         if self.cameraObj.data.getType() == 0:
816             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
817             view_vect = vv[1]
818
819         # if d > 0 the face is visible from the camera
820         d = view_vect * normal
821         
822         if d > 0:
823             return True
824         else:
825             return False
826
827
828     # Scene methods
829
830     def _doConvertGeometricObjToMesh(self, scene):
831         """Convert all "geometric" objects to mesh ones.
832         """
833         geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
834
835         Objects = scene.getChildren()
836         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
837         for obj in objList:
838             old_obj = obj
839             obj = self._convertToRawMeshObj(obj)
840             scene.link(obj)
841             scene.unlink(old_obj)
842
843
844             # XXX Workaround for Text and Curve which have some normals
845             # inverted when they are converted to Mesh, REMOVE that when
846             # blender will fix that!!
847             if old_obj.getType() in ['Curve', 'Text']:
848                 me = obj.getData(mesh=1)
849                 for f in me.faces: f.sel = 1;
850                 for v in me.verts: v.sel = 1;
851                 me.remDoubles(0)
852                 me.triangleToQuad()
853                 me.recalcNormals()
854                 me.update()
855
856
857     def _doSceneClipping(self, scene):
858         """Clip objects against the View Frustum.
859
860         For now clip away only objects according to their center position.
861         """
862
863         cpos = self._getObjPosition(self.cameraObj)
864         view_vect = self._cameraViewDirection()
865
866         near = self.cameraObj.data.clipStart
867         far  = self.cameraObj.data.clipEnd
868
869         aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
870         fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
871         fovy = fovy * 360.0/pi
872
873         Objects = scene.getChildren()
874         for o in Objects:
875             if o.getType() != 'Mesh': continue;
876
877             obj_vect = Vector(cpos) - self._getObjPosition(o)
878
879             d = obj_vect*view_vect
880             theta = AngleBetweenVecs(obj_vect, view_vect)
881             
882             # if the object is outside the view frustum, clip it away
883             if (d < near) or (d > far) or (theta > fovy):
884                 scene.unlink(o)
885
886     def _doSceneDepthSorting(self, scene):
887         """Sort objects in the scene.
888
889         The object sorting is done accordingly to the object centers.
890         """
891
892         c = self._getObjPosition(self.cameraObj)
893
894         by_center_pos = (lambda o1, o2:
895                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
896                 cmp((self._getObjPosition(o1) - Vector(c)).length,
897                     (self._getObjPosition(o2) - Vector(c)).length)
898             )
899
900         # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
901         # then ob1 goes farther than obj2, useful when obj2 has holes
902         by_bbox = None
903         
904         Objects = scene.getChildren()
905         Objects.sort(by_center_pos)
906         
907         # update the scene
908         for o in Objects:
909             scene.unlink(o)
910             scene.link(o)
911
912     def _joinMeshObjectsInScene(self, scene):
913         """Merge all the Mesh Objects in a scene into a single Mesh Object.
914         """
915         mesh = Mesh.New()
916         bigObj = Object.New('Mesh', 'BigOne')
917         bigObj.link(mesh)
918
919         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
920         bigObj.join(oList)
921         scene.link(bigObj)
922         for o in oList:
923             scene.unlink(o)
924
925         scene.update()
926
927  
928     # Per object methods
929
930     def _convertToRawMeshObj(self, object):
931         """Convert geometry based object to a mesh object.
932         """
933         me = Mesh.New('RawMesh_'+object.name)
934         me.getFromObject(object.name)
935
936         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
937         newObject.link(me)
938
939         # If the object has no materials set a default material
940         if not me.materials:
941             me.materials = [Material.New()]
942             #for f in me.faces: f.mat = 0
943
944         newObject.setMatrix(object.getMatrix())
945
946         return newObject
947
948     def _doModelToWorldCoordinates(self, mesh, matrix):
949         """Transform object coordinates to world coordinates.
950
951         This step is done simply applying to the object its tranformation
952         matrix and recalculating its normals.
953         """
954         mesh.transform(matrix, True)
955
956     def _doObjectDepthSorting(self, mesh):
957         """Sort faces in an object.
958
959         The faces in the object are sorted following the distance of the
960         vertices from the camera position.
961         """
962         c = self._getObjPosition(self.cameraObj)
963
964         # hackish sorting of faces
965
966         # Sort faces according to the max distance from the camera
967         by_max_vert_dist = (lambda f1, f2:
968                 cmp(max([(Vector(v.co)-Vector(c)).length for v in f1]),
969                     max([(Vector(v.co)-Vector(c)).length for v in f2])))
970         
971         # Sort faces according to the min distance from the camera
972         by_min_vert_dist = (lambda f1, f2:
973                 cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]),
974                     min([(Vector(v.co)-Vector(c)).length for v in f2])))
975         
976         # Sort faces according to the avg distance from the camera
977         by_avg_vert_dist = (lambda f1, f2:
978                 cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1),
979                     sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2)))
980
981         mesh.faces.sort(by_max_vert_dist)
982         mesh.faces.reverse()
983
984     def _doBackFaceCulling(self, mesh):
985         """Simple Backface Culling routine.
986         
987         At this level we simply do a visibility test face by face and then
988         select the vertices belonging to visible faces.
989         """
990         
991         # Select all vertices, so edges can be displayed even if there are no
992         # faces
993         for v in mesh.verts:
994             v.sel = 1
995         
996         Mesh.Mode(Mesh.SelectModes['FACE'])
997         # Loop on faces
998         for f in mesh.faces:
999             f.sel = 0
1000             if self._isFaceVisible(f):
1001                 f.sel = 1
1002
1003         # Is this the correct way to propagate the face selection info to the
1004         # vertices belonging to a face ??
1005         # TODO: Using the Mesh module this should come for free. Right?
1006         #Mesh.Mode(Mesh.SelectModes['VERTEX'])
1007         #for f in mesh.faces:
1008         #    if not f.sel:
1009         #        for v in f: v.sel = 0;
1010
1011         #for f in mesh.faces:
1012         #    if f.sel:
1013         #        for v in f: v.sel = 1;
1014
1015     def _doColorAndLighting(self, mesh):
1016         """Apply an Illumination model to the object.
1017
1018         The Illumination model used is the Phong one, it may be inefficient,
1019         but I'm just learning about rendering and starting from Phong seemed
1020         the most natural way.
1021         """
1022
1023         # If the mesh has vertex colors already, use them,
1024         # otherwise turn them on and do some calculations
1025         if mesh.vertexColors:
1026             return
1027         mesh.vertexColors = 1
1028
1029         materials = mesh.materials
1030         
1031         # TODO: use multiple lighting sources
1032         light_obj = self.lights[0]
1033         light_pos = self._getObjPosition(light_obj)
1034         light = light_obj.data
1035
1036         camPos = self._getObjPosition(self.cameraObj)
1037         
1038         # We do per-face color calculation (FLAT Shading), we can easily turn
1039         # to a per-vertex calculation if we want to implement some shading
1040         # technique. For an example see:
1041         # http://www.miralab.unige.ch/papers/368.pdf
1042         for f in mesh.faces:
1043             if not f.sel:
1044                 continue
1045
1046             mat = None
1047             if materials:
1048                 mat = materials[f.mat]
1049
1050             # A new default material
1051             if mat == None:
1052                 mat = Material.New('defMat')
1053             
1054             L = Vector(light_pos).normalize()
1055
1056             V = (Vector(camPos) - Vector(f.v[0].co)).normalize()
1057
1058             N = Vector(f.no).normalize()
1059
1060             R = 2 * (N*L) * N - L
1061
1062             # TODO: Attenuation factor (not used for now)
1063             a0 = 1; a1 = 0.0; a2 = 0.0
1064             d = (Vector(f.v[0].co) - Vector(light_pos)).length
1065             fd = min(1, 1.0/(a0 + a1*d + a2*d*d))
1066
1067             # Ambient component
1068             Ia = 1.0
1069             ka = mat.getAmb() * Vector([0.1, 0.1, 0.1])
1070             Iamb = Ia * ka
1071             
1072             # Diffuse component (add light.col for kd)
1073             kd = mat.getRef() * Vector(mat.getRGBCol())
1074             Ip = light.getEnergy()
1075             Idiff = Ip * kd * (N*L)
1076             
1077             # Specular component
1078             ks = mat.getSpec() * Vector(mat.getSpecCol())
1079             ns = mat.getHardness()
1080             Ispec = Ip * ks * pow((V * R), ns)
1081
1082             # Emissive component
1083             ki = Vector([mat.getEmit()]*3)
1084
1085             I = ki + Iamb + Idiff + Ispec
1086
1087             # Set Alpha component
1088             I = list(I)
1089             I.append(mat.getAlpha())
1090
1091             # Clamp I values between 0 and 1
1092             I = [ min(c, 1) for c in I]
1093             I = [ max(0, c) for c in I]
1094             tmp_col = [ int(c * 255.0) for c in I]
1095
1096             for c in f.col:
1097                 c.r = tmp_col[0]
1098                 c.g = tmp_col[1]
1099                 c.b = tmp_col[2]
1100                 c.a = tmp_col[3]
1101
1102     def _doEdgesStyle(self, mesh, edgestyleSelect):
1103         """Process Mesh Edges accroding to a given selection style.
1104
1105         Examples of algorithms:
1106
1107         Contours:
1108             given an edge if its adjacent faces have the same normal (that is
1109             they are complanar), than deselect it.
1110
1111         Silhouettes:
1112             given an edge if one its adjacent faces is frontfacing and the
1113             other is backfacing, than select it, else deselect.
1114         """
1115
1116         Mesh.Mode(Mesh.SelectModes['EDGE'])
1117
1118         for e in mesh.edges:
1119
1120             if edgestyleSelect(e, mesh):
1121                 e.sel = 1
1122             else:
1123                 e.sel = 0
1124                 
1125     def _doProjection(self, mesh, projector):
1126         """Calculate the Projection for the object.
1127         """
1128         # TODO: maybe using the object.transform() can be faster?
1129
1130         for v in mesh.verts:
1131             p = projector.doProjection(v.co)
1132             v.co[0] = p[0]
1133             v.co[1] = p[1]
1134             v.co[2] = p[2]
1135
1136
1137
1138 # ---------------------------------------------------------------------
1139 #
1140 ## Main Program
1141 #
1142 # ---------------------------------------------------------------------
1143
1144 # A dictionary to collect all the different edge styles and their edge
1145 # selection criteria
1146 edgeSelectionStyles = {
1147         'normal': MeshUtils().isVisibleEdge,
1148         'silhouette': MeshUtils().isSilhouetteEdge
1149         }
1150
1151 # A dictionary to collect the supported output formats
1152 outputWriters = {
1153         'SVG': SVGVectorWriter,
1154         }
1155
1156
1157 # A wrapper function for the vectorizing process
1158 def vectorize(filename):
1159     """The vectorizing process is as follows:
1160      
1161      - Instanciate the writer and the renderer
1162      - Render!
1163      """
1164     from Blender import Window
1165     editmode = Window.EditMode()
1166     if editmode: Window.EditMode(0)
1167
1168     writer = outputWriters[OUTPUT_FORMAT](filename)
1169     
1170     renderer = Renderer()
1171     renderer.doRendering(writer, RENDER_ANIMATION)
1172
1173     if editmode: Window.EditMode(1) 
1174
1175
1176 # Here the main
1177 if __name__ == "__main__":
1178     
1179     basename = Blender.sys.basename(Blender.Get('filename'))
1180     outputfile = Blender.sys.splitext(basename)[0]+".svg"
1181
1182     if Blender.mode == 'background':
1183         vectorize(outputfile)
1184     else:
1185         label = "Save %s" % OUTPUT_FORMAT
1186         Blender.Window.FileSelector(vectorize, label, outputfile)
1187         Blender.Redraw()