89140458519db6360975dae030eb4c1611d5082c
[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
558         # Get the list of lighting sources
559         obj_lst = self._SCENE.getChildren()
560         self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
561
562         if len(self.lights) == 0:
563             l = Lamp.New('Lamp')
564             lobj = Object.New('Lamp')
565             lobj.link(l) 
566             self.lights.append(lobj)
567
568
569     ##
570     # Public Methods
571     #
572
573     def doRendering(self, outputWriter, animation=False):
574         """Render picture or animation and write it out.
575         
576         The parameters are:
577             - a Vector writer object that will be used to output the result.
578             - a flag to tell if we want to render an animation or only the
579               current frame.
580         """
581         
582         context = self._SCENE.getRenderingContext()
583         currentFrame = context.currentFrame()
584
585         # Handle the animation case
586         if not animation:
587             startFrame = currentFrame
588             endFrame = startFrame
589             outputWriter.open()
590         else:
591             startFrame = context.startFrame()
592             endFrame = context.endFrame()
593             outputWriter.open(startFrame, endFrame)
594         
595         # Do the rendering process frame by frame
596         print "Start Rendering!"
597         for f in range(startFrame, endFrame+1):
598             context.currentFrame(f)
599
600             renderedScene = self.doRenderScene(self._SCENE)
601             outputWriter.printCanvas(renderedScene,
602                     doPrintPolygons = PRINT_POLYGONS,
603                     doPrintEdges    = PRINT_EDGES,
604                     showHiddenEdges = SHOW_HIDDEN_EDGES)
605             
606             # clear the rendered scene
607             self._SCENE.makeCurrent()
608             Scene.unlink(renderedScene)
609             del renderedScene
610
611         outputWriter.close()
612         print "Done!"
613         context.currentFrame(currentFrame)
614
615
616     def doRenderScene(self, inputScene):
617         """Control the rendering process.
618         
619         Here we control the entire rendering process invoking the operation
620         needed to transform and project the 3D scene in two dimensions.
621         """
622         
623         # Use some temporary workspace, a full copy of the scene
624         workScene = inputScene.copy(2)
625
626         # Get a projector for this scene.
627         # NOTE: the projector wants object in world coordinates,
628         # so we should apply modelview transformations _before_
629         # projection transformations
630         proj = Projector(self.cameraObj, self.canvasRatio)
631
632         # global processing of the scene
633
634         self._doConvertGeometricObjToMesh(workScene)
635
636         self._doSceneClipping(workScene)
637
638         # FIXME: does not work in batch mode!
639         #if OPTIMIZE_FOR_SPACE:
640         #    self._joinMeshObjectsInScene(workScene)
641
642         self._doSceneDepthSorting(workScene)
643         
644         # Per object activities
645
646         Objects = workScene.getChildren()
647         for obj in Objects:
648             
649             if obj.getType() != 'Mesh':
650                 print "Only Mesh supported! - Skipping type:", obj.getType()
651                 continue
652
653             print "Rendering: ", obj.getName()
654
655             mesh = obj.data
656
657             self._doModelToWorldCoordinates(mesh, obj.matrix)
658
659             self._doObjectDepthSorting(mesh)
660             
661             self._doBackFaceCulling(mesh)
662             
663             self._doColorAndLighting(mesh)
664
665             # TODO: 'style' can be a function that determine
666             # if an edge should be showed?
667             self._doEdgesStyle(mesh, style=None)
668
669             self._doProjection(mesh, proj)
670             
671             # Update the object data, important! :)
672             mesh.update()
673
674         return workScene
675
676
677     ##
678     # Private Methods
679     #
680
681     # Utility methods
682
683     def _getObjPosition(self, obj):
684         """Return the obj position in World coordinates.
685         """
686         return obj.matrix.translationPart()
687
688     def _cameraViewDirection(self):
689         """Get the View Direction form the camera matrix.
690         """
691         return Vector(self.cameraObj.matrix[2]).resize3D()
692
693
694     # Faces methods
695
696     def _isFaceVisible(self, face):
697         """Determine if a face of an object is visible from the current camera.
698         
699         The view vector is calculated from the camera location and one of the
700         vertices of the face (expressed in World coordinates, after applying
701         modelview transformations).
702
703         After those transformations we determine if a face is visible by
704         computing the angle between the face normal and the view vector, this
705         angle has to be between -90 and 90 degrees for the face to be visible.
706         This corresponds somehow to the dot product between the two, if it
707         results > 0 then the face is visible.
708
709         There is no need to normalize those vectors since we are only interested in
710         the sign of the cross product and not in the product value.
711
712         NOTE: here we assume the face vertices are in WorldCoordinates, so
713         please transform the object _before_ doing the test.
714         """
715
716         normal = Vector(face.no)
717         camPos = self._getObjPosition(self.cameraObj)
718         view_vect = None
719
720         # View Vector in orthographics projections is the view Direction of
721         # the camera
722         if self.cameraObj.data.getType() == 1:
723             view_vect = self._cameraViewDirection()
724
725         # View vector in perspective projections can be considered as
726         # the difference between the camera position and one point of
727         # the face, we choose the farthest point from the camera.
728         if self.cameraObj.data.getType() == 0:
729             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
730             view_vect = vv[1]
731
732         # if d > 0 the face is visible from the camera
733         d = view_vect * normal
734         
735         if d > 0:
736             return True
737         else:
738             return False
739
740
741     # Scene methods
742
743     def _doConvertGeometricObjToMesh(self, scene):
744         """Convert all "geometric" objects to mesh ones.
745         """
746         geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
747
748         Objects = scene.getChildren()
749         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
750         for obj in objList:
751             old_obj = obj
752             obj = self._convertToRawMeshObj(obj)
753             scene.link(obj)
754             scene.unlink(old_obj)
755
756
757             # XXX Workaround for Text and Curve which have some normals
758             # inverted when they are converted to Mesh, REMOVE that when
759             # blender will fix that!!
760             if old_obj.getType() in ['Curve', 'Text']:
761                 me = obj.getData(mesh=1)
762                 for f in me.faces: f.sel = 1;
763                 for v in me.verts: v.sel = 1;
764                 me.remDoubles(0)
765                 me.triangleToQuad()
766                 me.recalcNormals()
767                 me.update()
768
769
770     def _doSceneClipping(self, scene):
771         """Clip objects against the View Frustum.
772
773         For now clip away only objects according to their center position.
774         """
775
776         cpos = self._getObjPosition(self.cameraObj)
777         view_vect = self._cameraViewDirection()
778
779         near = self.cameraObj.data.clipStart
780         far  = self.cameraObj.data.clipEnd
781
782         aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
783         fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
784         fovy = fovy * 360.0/pi
785
786         Objects = scene.getChildren()
787         for o in Objects:
788             if o.getType() != 'Mesh': continue;
789
790             obj_vect = Vector(cpos) - self._getObjPosition(o)
791
792             d = obj_vect*view_vect
793             theta = AngleBetweenVecs(obj_vect, view_vect)
794             
795             # if the object is outside the view frustum, clip it away
796             if (d < near) or (d > far) or (theta > fovy):
797                 scene.unlink(o)
798
799     def _doSceneDepthSorting(self, scene):
800         """Sort objects in the scene.
801
802         The object sorting is done accordingly to the object centers.
803         """
804
805         c = self._getObjPosition(self.cameraObj)
806
807         by_center_pos = (lambda o1, o2:
808                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
809                 cmp((self._getObjPosition(o1) - Vector(c)).length,
810                     (self._getObjPosition(o2) - Vector(c)).length)
811             )
812
813         # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
814         # then ob1 goes farther than obj2, useful when obj2 has holes
815         by_bbox = None
816         
817         Objects = scene.getChildren()
818         Objects.sort(by_center_pos)
819         
820         # update the scene
821         for o in Objects:
822             scene.unlink(o)
823             scene.link(o)
824
825     def _joinMeshObjectsInScene(self, scene):
826         """Merge all the Mesh Objects in a scene into a single Mesh Object.
827         """
828         mesh = Mesh.New()
829         bigObj = Object.New('Mesh', 'BigOne')
830         bigObj.link(mesh)
831
832         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
833         bigObj.join(oList)
834         scene.link(bigObj)
835         for o in oList:
836             scene.unlink(o)
837
838         scene.update()
839
840  
841     # Per object methods
842
843     def _convertToRawMeshObj(self, object):
844         """Convert geometry based object to a mesh object.
845         """
846         me = Mesh.New('RawMesh_'+object.name)
847         me.getFromObject(object.name)
848
849         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
850         newObject.link(me)
851
852         # If the object has no materials set a default material
853         if not me.materials:
854             me.materials = [Material.New()]
855             #for f in me.faces: f.mat = 0
856
857         newObject.setMatrix(object.getMatrix())
858
859         return newObject
860
861     def _doModelToWorldCoordinates(self, mesh, matrix):
862         """Transform object coordinates to world coordinates.
863
864         This step is done simply applying to the object its tranformation
865         matrix and recalculating its normals.
866         """
867         mesh.transform(matrix, True)
868
869     def _doObjectDepthSorting(self, mesh):
870         """Sort faces in an object.
871
872         The faces in the object are sorted following the distance of the
873         vertices from the camera position.
874         """
875         c = self._getObjPosition(self.cameraObj)
876
877         # hackish sorting of faces
878
879         # Sort faces according to the max distance from the camera
880         by_max_vert_dist = (lambda f1, f2:
881                 cmp(max([(Vector(v.co)-Vector(c)).length for v in f1]),
882                     max([(Vector(v.co)-Vector(c)).length for v in f2])))
883         
884         # Sort faces according to the min distance from the camera
885         by_min_vert_dist = (lambda f1, f2:
886                 cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]),
887                     min([(Vector(v.co)-Vector(c)).length for v in f2])))
888         
889         # Sort faces according to the avg distance from the camera
890         by_avg_vert_dist = (lambda f1, f2:
891                 cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1),
892                     sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2)))
893
894         mesh.faces.sort(by_max_vert_dist)
895         mesh.faces.reverse()
896
897     def _doBackFaceCulling(self, mesh):
898         """Simple Backface Culling routine.
899         
900         At this level we simply do a visibility test face by face and then
901         select the vertices belonging to visible faces.
902         """
903         
904         # Select all vertices, so edges can be displayed even if there are no
905         # faces
906         for v in mesh.verts:
907             v.sel = 1
908         
909         Mesh.Mode(Mesh.SelectModes['FACE'])
910         # Loop on faces
911         for f in mesh.faces:
912             f.sel = 0
913             if self._isFaceVisible(f):
914                 f.sel = 1
915
916         # Is this the correct way to propagate the face selection info to the
917         # vertices belonging to a face ??
918         # TODO: Using the Mesh module this should come for free. Right?
919         Mesh.Mode(Mesh.SelectModes['VERTEX'])
920         for f in mesh.faces:
921             if not f.sel:
922                 for v in f: v.sel = 0;
923
924         for f in mesh.faces:
925             if f.sel:
926                 for v in f: v.sel = 1;
927
928     def _doColorAndLighting(self, mesh):
929         """Apply an Illumination model to the object.
930
931         The Illumination model used is the Phong one, it may be inefficient,
932         but I'm just learning about rendering and starting from Phong seemed
933         the most natural way.
934         """
935
936         # If the mesh has vertex colors already, use them,
937         # otherwise turn them on and do some calculations
938         if mesh.hasVertexColours():
939             return
940         mesh.hasVertexColours(True)
941
942         materials = mesh.materials
943         
944         # TODO: use multiple lighting sources
945         light_obj = self.lights[0]
946         light_pos = self._getObjPosition(light_obj)
947         light = light_obj.data
948
949         camPos = self._getObjPosition(self.cameraObj)
950         
951         # We do per-face color calculation (FLAT Shading), we can easily turn
952         # to a per-vertex calculation if we want to implement some shading
953         # technique. For an example see:
954         # http://www.miralab.unige.ch/papers/368.pdf
955         for f in mesh.faces:
956             if not f.sel:
957                 continue
958
959             mat = None
960             if materials:
961                 mat = materials[f.mat]
962
963             # A new default material
964             if mat == None:
965                 mat = Material.New('defMat')
966             
967             L = Vector(light_pos).normalize()
968
969             V = (Vector(camPos) - Vector(f.v[0].co)).normalize()
970
971             N = Vector(f.no).normalize()
972
973             R = 2 * (N*L) * N - L
974
975             # TODO: Attenuation factor (not used for now)
976             a0 = 1; a1 = 0.0; a2 = 0.0
977             d = (Vector(f.v[0].co) - Vector(light_pos)).length
978             fd = min(1, 1.0/(a0 + a1*d + a2*d*d))
979
980             # Ambient component
981             Ia = 1.0
982             ka = mat.getAmb() * Vector([0.1, 0.1, 0.1])
983             Iamb = Ia * ka
984             
985             # Diffuse component (add light.col for kd)
986             kd = mat.getRef() * Vector(mat.getRGBCol())
987             Ip = light.getEnergy()
988             Idiff = Ip * kd * (N*L)
989             
990             # Specular component
991             ks = mat.getSpec() * Vector(mat.getSpecCol())
992             ns = mat.getHardness()
993             Ispec = Ip * ks * pow((V * R), ns)
994
995             # Emissive component
996             ki = Vector([mat.getEmit()]*3)
997
998             I = ki + Iamb + Idiff + Ispec
999
1000             # Clamp I values between 0 and 1
1001             I = [ min(c, 1) for c in I]
1002             I = [ max(0, c) for c in I]
1003             tmp_col = [ int(c * 255.0) for c in I]
1004
1005             vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2], 255)
1006             f.col = []
1007             for v in f.v:
1008                 f.col.append(vcol)
1009
1010     def _doEdgesStyle(self, mesh, style):
1011         """Process Mesh Edges.
1012
1013         Examples of algorithms:
1014
1015         Contours:
1016             given an edge if its adjacent faces have the same normal (that is
1017             they are complanar), than deselect it.
1018
1019         Silhouettes:
1020             given an edge if one its adjacent faces is frontfacing and the
1021             other is backfacing, than select it, else deselect.
1022         """
1023         #print "\tTODO: _doEdgeStyle()"
1024         return
1025
1026     def _doProjection(self, mesh, projector):
1027         """Calculate the Projection for the object.
1028         """
1029         # TODO: maybe using the object.transform() can be faster?
1030
1031         for v in mesh.verts:
1032             p = projector.doProjection(v.co)
1033             v.co[0] = p[0]
1034             v.co[1] = p[1]
1035             v.co[2] = p[2]
1036
1037
1038
1039 # ---------------------------------------------------------------------
1040 #
1041 ## Main Program
1042 #
1043 # ---------------------------------------------------------------------
1044
1045 def vectorize(filename):
1046     """The vectorizing process is as follows:
1047      
1048      - Instanciate the writer and the renderer
1049      - Render!
1050      """
1051     from Blender import Window
1052     editmode = Window.EditMode()
1053     if editmode: Window.EditMode(0)
1054
1055     writer = SVGVectorWriter(filename)
1056     
1057     renderer = Renderer()
1058     renderer.doRendering(writer, RENDER_ANIMATION)
1059
1060     if editmode: Window.EditMode(1) 
1061
1062 def vectorize_gui(filename):
1063     """Draw the gui.
1064
1065     I would like to keep that simple, really.
1066     """
1067     Blender.Window.FileSelector (vectorize, 'Save SVG', filename)
1068     Blender.Redraw()
1069
1070
1071 # Here the main
1072 if __name__ == "__main__":
1073     
1074     basename = Blender.sys.basename(Blender.Get('filename'))
1075     outputfile = Blender.sys.splitext(basename)[0]+".svg"
1076
1077     # with this trick we can run the script in batch mode
1078     try:
1079         vectorize_gui(outputfile)
1080     except:
1081         vectorize(outputfile)