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