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