Add a config class
[vrm.git] / vrm.py
1 #!BPY
2 """
3 Name: 'VRM'
4 Blender: 242
5 Group: 'Render'
6 Tooltip: 'Vector Rendering Method script'
7 """
8
9 __author__ = "Antonio Ospite"
10 __url__ = ["http://vrm.projects.blender.org"]
11 __version__ = "0.3"
12
13 __bpydoc__ = """\
14     Render the scene and save the result in vector format.
15 """
16
17 # ---------------------------------------------------------------------
18 #    Copyright (c) 2006 Antonio Ospite
19 #
20 #    This program is free software; you can redistribute it and/or modify
21 #    it under the terms of the GNU General Public License as published by
22 #    the Free Software Foundation; either version 2 of the License, or
23 #    (at your option) any later version.
24 #
25 #    This program is distributed in the hope that it will be useful,
26 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
27 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28 #    GNU General Public License for more details.
29 #
30 #    You should have received a copy of the GNU General Public License
31 #    along with this program; if not, write to the Free Software
32 #    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
33 #
34 # ---------------------------------------------------------------------
35 #
36 # Additional credits:
37 #   Thanks to Emilio Aguirre for S2flender from which I took inspirations :)
38 #   Thanks to Nikola Radovanovic, the author of the original VRM script,
39 #       the code you read here has been rewritten _almost_ entirely
40 #       from scratch but Nikola gave me the idea, so I thank him publicly.
41 #
42 # ---------------------------------------------------------------------
43
44 # Things TODO for a next release:
45 #   - Switch to the Mesh structure, should be considerably faster
46 #    (partially done, but with Mesh we cannot sort faces, yet)
47 #   - Use a better depth sorting algorithm
48 #   - Review how selections are made (this script uses selection states of
49 #     primitives to represent visibility infos)
50 #   - Implement clipping of primitives and do handle object intersections.
51 #     (for now only clipping for whole objects is supported).
52 #   - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
53 #   - Implement Edge coloring
54 #   - Use multiple lighting sources in color calculation
55 #   - Implement Shading Styles? (for now we use Flat Shading).
56 #   - Use a data structure other than Mesh to represent the 2D image? 
57 #     Think to a way to merge adjacent polygons that have the same color.
58 #     Or a way to use paths for silhouettes and contours.
59 #   - Add Vector Writers other that SVG.
60 #   - Consider SMIL for animation handling instead of ECMA Script? (Firefox do
61 #     not support SMIL for animations)
62 #   - FIX the issue with negative scales in object tranformations!
63 #
64 # ---------------------------------------------------------------------
65 #
66 # Changelog:
67 #
68 #   vrm-0.3.py  -   2006-05-19
69 #    * First release after code restucturing.
70 #      Now the script offers a useful set of functionalities
71 #      and it can render animations, too.
72 #
73 # ---------------------------------------------------------------------
74
75 import Blender
76 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera
77 from Blender.Mathutils import *
78 from math import *
79
80
81 # Some global settings
82
83 class config:
84     polygons = dict()
85     polygons['FILL'] = True
86     polygons['STYLE'] = None
87     # Hidden to the user for now
88     polygons['EXPANSION_TRICK'] = True
89
90     edges = dict()
91     edges['SHOW'] = True
92     edges['SHOW_HIDDEN'] = False
93     edges['STYLE'] = 'silhouette'
94     edges['WIDTH'] = 2
95
96     output = dict()
97     output['FORMAT'] = 'SVG'
98     output['ANIMATION'] = False
99     output['MERGED_OBJECTS'] = True
100
101
102
103 # ---------------------------------------------------------------------
104 #
105 ## Utility Mesh class
106 #
107 # ---------------------------------------------------------------------
108 class MeshUtils:
109
110     def getEdgeAdjacentFaces(edge, mesh):
111         """Get the faces adjacent to a given edge.
112
113         There can be 0, 1 or more (usually 2) faces adjacent to an edge.
114         """
115         adjface_list = []
116
117         for f in mesh.faces:
118             if (edge.v1 in f.v) and (edge.v2 in f.v):
119                 adjface_list.append(f)
120
121         return adjface_list
122
123     def isVisibleEdge(e, mesh):
124         """Normal edge selection rule.
125
126         An edge is visible if _any_ of its adjacent faces is selected.
127         Note: if the edge has no adjacent faces we want to show it as well,
128         useful for "edge only" portion of objects.
129         """
130
131         adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
132
133         if len(adjacent_faces) == 0:
134             return True
135
136         selected_faces = [f for f in adjacent_faces if f.sel]
137
138         if len(selected_faces) != 0:
139             return True
140         else:
141             return False
142
143     def isSilhouetteEdge(e, mesh):
144         """Silhuette selection rule.
145
146         An edge is a silhuette edge if it is shared by two faces with
147         different selection status or if it is a boundary edge of a selected
148         face.
149         """
150
151         adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
152
153         if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
154             (len(adjacent_faces) == 2 and
155                 adjacent_faces[0].sel != adjacent_faces[1].sel)
156             ):
157             return True
158         else:
159             return False
160     
161     def toonShading(u):
162
163         levels = 2
164         texels = 2*levels - 1
165         map = [0.0] + [(i)/float(texels-1) for i in range(1, texels-1) ] + [1.0]
166         
167         v = 1.0
168         for i in range(0, len(map)-1):
169             pivot = (map[i]+map[i+1])/2.0
170             j = int(u>pivot)
171
172             v = map[i+j]
173
174             if v<map[i+1]:
175                 return v
176
177         return v
178
179
180     getEdgeAdjacentFaces = staticmethod(getEdgeAdjacentFaces)
181     isVisibleEdge = staticmethod(isVisibleEdge)
182     isSilhouetteEdge = staticmethod(isSilhouetteEdge)
183     toonShading = staticmethod(toonShading)
184
185
186
187 # ---------------------------------------------------------------------
188 #
189 ## Projections classes
190 #
191 # ---------------------------------------------------------------------
192
193 class Projector:
194     """Calculate the projection of an object given the camera.
195     
196     A projector is useful to so some per-object transformation to obtain the
197     projection of an object given the camera.
198     
199     The main method is #doProjection# see the method description for the
200     parameter list.
201     """
202
203     def __init__(self, cameraObj, canvasRatio):
204         """Calculate the projection matrix.
205
206         The projection matrix depends, in this case, on the camera settings.
207         TAKE CARE: This projector expects vertices in World Coordinates!
208         """
209
210         camera = cameraObj.getData()
211
212         aspect = float(canvasRatio[0])/float(canvasRatio[1])
213         near = camera.clipStart
214         far = camera.clipEnd
215
216         scale = float(camera.scale)
217
218         fovy = atan(0.5/aspect/(camera.lens/32))
219         fovy = fovy * 360.0/pi
220         
221         # What projection do we want?
222         if camera.type:
223             #mP = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale) 
224             mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) 
225         else:
226             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
227         
228         # View transformation
229         cam = Matrix(cameraObj.getInverseMatrix())
230         cam.transpose() 
231         
232         mP = mP * cam
233
234         self.projectionMatrix = mP
235
236     ##
237     # Public methods
238     #
239
240     def doProjection(self, v):
241         """Project the point on the view plane.
242
243         Given a vertex calculate the projection using the current projection
244         matrix.
245         """
246         
247         # Note that we have to work on the vertex using homogeneous coordinates
248         p = self.projectionMatrix * Vector(v).resize4D()
249
250         if p[3]>0:
251             p[0] = p[0]/p[3]
252             p[1] = p[1]/p[3]
253
254         # restore the size
255         p[3] = 1.0
256         p.resize3D()
257
258         return p
259
260     ##
261     # Private methods
262     #
263     
264     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
265         """Return a perspective projection matrix.
266         """
267         
268         top = near * tan(fovy * pi / 360.0)
269         bottom = -top
270         left = bottom*aspect
271         right= top*aspect
272         x = (2.0 * near) / (right-left)
273         y = (2.0 * near) / (top-bottom)
274         a = (right+left) / (right-left)
275         b = (top+bottom) / (top - bottom)
276         c = - ((far+near) / (far-near))
277         d = - ((2*far*near)/(far-near))
278         
279         m = Matrix(
280                 [x,   0.0,    a,    0.0],
281                 [0.0,   y,    b,    0.0],
282                 [0.0, 0.0,    c,      d],
283                 [0.0, 0.0, -1.0,    0.0])
284
285         return m
286
287     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
288         """Return an orthogonal projection matrix.
289         """
290         
291         # The 11 in the formula was found emiprically
292         top = near * tan(fovy * pi / 360.0) * (scale * 11)
293         bottom = -top 
294         left = bottom * aspect
295         right= top * aspect
296         rl = right-left
297         tb = top-bottom
298         fn = near-far 
299         tx = -((right+left)/rl)
300         ty = -((top+bottom)/tb)
301         tz = ((far+near)/fn)
302
303         m = Matrix(
304                 [2.0/rl, 0.0,    0.0,     tx],
305                 [0.0,    2.0/tb, 0.0,     ty],
306                 [0.0,    0.0,    2.0/fn,  tz],
307                 [0.0,    0.0,    0.0,    1.0])
308         
309         return m
310
311
312
313 # ---------------------------------------------------------------------
314 #
315 ## 2D Object representation class
316 #
317 # ---------------------------------------------------------------------
318
319 # TODO: a class to represent the needed properties of a 2D vector image
320 # For now just using a [N]Mesh structure.
321
322
323 # ---------------------------------------------------------------------
324 #
325 ## Vector Drawing Classes
326 #
327 # ---------------------------------------------------------------------
328
329 ## A generic Writer
330
331 class VectorWriter:
332     """
333     A class for printing output in a vectorial format.
334
335     Given a 2D representation of the 3D scene the class is responsible to
336     write it is a vector format.
337
338     Every subclasses of VectorWriter must have at last the following public
339     methods:
340         - open(self)
341         - close(self)
342         - printCanvas(self, scene,
343             doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
344     """
345     
346     def __init__(self, fileName):
347         """Set the output file name and other properties"""
348
349         self.outputFileName = fileName
350         self.file = None
351         
352         context = Scene.GetCurrent().getRenderingContext()
353         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
354
355         self.startFrame = 1
356         self.endFrame = 1
357         self.animation = False
358
359
360     ##
361     # Public Methods
362     #
363     
364     def open(self, startFrame=1, endFrame=1):
365         if startFrame != endFrame:
366             self.startFrame = startFrame
367             self.endFrame = endFrame
368             self.animation = True
369
370         self.file = open(self.outputFileName, "w")
371         print "Outputting to: ", self.outputFileName
372
373         return
374
375     def close(self):
376         self.file.close()
377         return
378
379     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
380             showHiddenEdges=False):
381         """This is the interface for the needed printing routine.
382         """
383         return
384         
385
386 ## SVG Writer
387
388 class SVGVectorWriter(VectorWriter):
389     """A concrete class for writing SVG output.
390     """
391
392     def __init__(self, fileName):
393         """Simply call the parent Contructor.
394         """
395         VectorWriter.__init__(self, fileName)
396
397
398     ##
399     # Public Methods
400     #
401
402     def open(self, startFrame=1, endFrame=1):
403         """Do some initialization operations.
404         """
405         VectorWriter.open(self, startFrame, endFrame)
406         self._printHeader()
407
408     def close(self):
409         """Do some finalization operation.
410         """
411         self._printFooter()
412
413         # remember to call the close method of the parent
414         VectorWriter.close(self)
415
416         
417     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
418             showHiddenEdges=False):
419         """Convert the scene representation to SVG.
420         """
421
422         Objects = scene.getChildren()
423
424         context = scene.getRenderingContext()
425         framenumber = context.currentFrame()
426
427         if self.animation:
428             framestyle = "display:none"
429         else:
430             framestyle = "display:block"
431         
432         # Assign an id to this group so we can set properties on it using DOM
433         self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
434                 (framenumber, framestyle) )
435
436
437         for obj in Objects:
438
439             if(obj.getType() != 'Mesh'):
440                 continue
441
442             self.file.write("<g id=\"%s\">\n" % obj.getName())
443
444             mesh = obj.getData(mesh=1)
445
446             if doPrintPolygons:
447                 self._printPolygons(mesh)
448
449             if doPrintEdges:
450                 self._printEdges(mesh, showHiddenEdges)
451             
452             self.file.write("</g>\n")
453
454         self.file.write("</g>\n")
455
456     
457     ##  
458     # Private Methods
459     #
460     
461     def _calcCanvasCoord(self, v):
462         """Convert vertex in scene coordinates to canvas coordinates.
463         """
464
465         pt = Vector([0, 0, 0])
466         
467         mW = float(self.canvasSize[0])/2.0
468         mH = float(self.canvasSize[1])/2.0
469
470         # rescale to canvas size
471         pt[0] = v.co[0]*mW + mW
472         pt[1] = v.co[1]*mH + mH
473         pt[2] = v.co[2]
474          
475         # For now we want (0,0) in the top-left corner of the canvas.
476         # Mirror and translate along y
477         pt[1] *= -1
478         pt[1] += self.canvasSize[1]
479         
480         return pt
481
482     def _printHeader(self):
483         """Print SVG header."""
484
485         self.file.write("<?xml version=\"1.0\"?>\n")
486         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n")
487         self.file.write("\t\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
488         self.file.write("<svg version=\"1.1\"\n")
489         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
490         self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
491                 self.canvasSize)
492
493         if self.animation:
494
495             self.file.write("""\n<script><![CDATA[
496             globalStartFrame=%d;
497             globalEndFrame=%d;
498
499             /* FIXME: Use 1000 as interval as lower values gives problems */
500             timerID = setInterval("NextFrame()", 1000);
501             globalFrameCounter=%d;
502
503             function NextFrame()
504             {
505               currentElement  = document.getElementById('frame'+globalFrameCounter)
506               previousElement = document.getElementById('frame'+(globalFrameCounter-1))
507
508               if (!currentElement)
509               {
510                 return;
511               }
512
513               if (globalFrameCounter > globalEndFrame)
514               {
515                 clearInterval(timerID)
516               }
517               else
518               {
519                 if(previousElement)
520                 {
521                     previousElement.style.display="none";
522                 }
523                 currentElement.style.display="block";
524                 globalFrameCounter++;
525               }
526             }
527             \n]]></script>\n
528             \n""" % (self.startFrame, self.endFrame, self.startFrame) )
529                 
530     def _printFooter(self):
531         """Print the SVG footer."""
532
533         self.file.write("\n</svg>\n")
534
535     def _printPolygons(self, mesh):
536         """Print the selected (visible) polygons.
537         """
538
539         if len(mesh.faces) == 0:
540             return
541
542         self.file.write("<g>\n")
543
544         for face in mesh.faces:
545             if not face.sel:
546                continue
547
548             self.file.write("<polygon points=\"")
549
550             for v in face:
551                 p = self._calcCanvasCoord(v)
552                 self.file.write("%g,%g " % (p[0], p[1]))
553             
554             # get rid of the last blank space, just cosmetics here.
555             self.file.seek(-1, 1) 
556             self.file.write("\"\n")
557             
558             # take as face color the first vertex color
559             # TODO: the average of vetrex colors?
560             if face.col:
561                 fcol = face.col[0]
562                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
563             else:
564                 color = [255, 255, 255, 255]
565
566             # use the stroke property to alleviate the "adjacent edges" problem,
567             # we simulate polygon expansion using borders,
568             # see http://www.antigrain.com/svg/index.html for more info
569             stroke_col = color
570             stroke_width = 0.5
571
572             # Convert the color to the #RRGGBB form
573             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
574
575             # Handle transparent polygons
576             opacity_string = ""
577             if color[3] != 255:
578                 opacity = float(color[3])/255.0
579                 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
580
581             self.file.write("\tstyle=\"fill:" + str_col + ";")
582             self.file.write(opacity_string)
583             if config.polygons['EXPANSION_TRICK']:
584                 self.file.write(" stroke:" + str_col + ";")
585                 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
586                 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
587             self.file.write("\"/>\n")
588
589         self.file.write("</g>\n")
590
591     def _printEdges(self, mesh, showHiddenEdges=False):
592         """Print the wireframe using mesh edges.
593         """
594
595         stroke_width=EDGES_WIDTH
596         stroke_col = [0, 0, 0]
597         
598         self.file.write("<g>\n")
599
600         for e in mesh.edges:
601             
602             hidden_stroke_style = ""
603             
604             if e.sel == 0:
605                 if showHiddenEdges == False:
606                     continue
607                 else:
608                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
609
610             p1 = self._calcCanvasCoord(e.v1)
611             p2 = self._calcCanvasCoord(e.v2)
612             
613             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
614                     % ( p1[0], p1[1], p2[0], p2[1] ) )
615             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
616             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
617             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
618             self.file.write(hidden_stroke_style)
619             self.file.write("\"/>\n")
620
621         self.file.write("</g>\n")
622
623
624
625 # ---------------------------------------------------------------------
626 #
627 ## Rendering Classes
628 #
629 # ---------------------------------------------------------------------
630
631 # A dictionary to collect all the different edge styles and their edge
632 # selection criteria
633 edgeSelectionStyles = {
634         'normal': MeshUtils.isVisibleEdge,
635         'silhouette': MeshUtils.isSilhouetteEdge
636         }
637
638 # A dictionary to collect the supported output formats
639 outputWriters = {
640         'SVG': SVGVectorWriter,
641         }
642
643
644 class Renderer:
645     """Render a scene viewed from a given camera.
646     
647     This class is responsible of the rendering process, transformation and
648     projection of the objects in the scene are invoked by the renderer.
649
650     The rendering is done using the active camera for the current scene.
651     """
652
653     def __init__(self):
654         """Make the rendering process only for the current scene by default.
655
656         We will work on a copy of the scene, be sure that the current scene do
657         not get modified in any way.
658         """
659
660         # Render the current Scene, this should be a READ-ONLY property
661         self._SCENE = Scene.GetCurrent()
662         
663         # Use the aspect ratio of the scene rendering context
664         context = self._SCENE.getRenderingContext()
665
666         aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
667         self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
668                             float(context.aspectRatioY())
669                             )
670
671         # Render from the currently active camera 
672         self.cameraObj = self._SCENE.getCurrentCamera()
673
674         # Get a projector for this camera.
675         # NOTE: the projector wants object in world coordinates,
676         # so we should remember to apply modelview transformations
677         # _before_ we do projection transformations.
678         self.proj = Projector(self.cameraObj, self.canvasRatio)
679
680         # Get the list of lighting sources
681         obj_lst = self._SCENE.getChildren()
682         self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
683
684         # When there are no lights we use a default lighting source
685         # that have the same position of the camera
686         if len(self.lights) == 0:
687             l = Lamp.New('Lamp')
688             lobj = Object.New('Lamp')
689             lobj.loc = self.cameraObj.loc
690             lobj.link(l) 
691             self.lights.append(lobj)
692
693
694     ##
695     # Public Methods
696     #
697
698     def doRendering(self, outputWriter, animation=False):
699         """Render picture or animation and write it out.
700         
701         The parameters are:
702             - a Vector writer object that will be used to output the result.
703             - a flag to tell if we want to render an animation or only the
704               current frame.
705         """
706         
707         context = self._SCENE.getRenderingContext()
708         currentFrame = context.currentFrame()
709
710         # Handle the animation case
711         if not animation:
712             startFrame = currentFrame
713             endFrame = startFrame
714             outputWriter.open()
715         else:
716             startFrame = context.startFrame()
717             endFrame = context.endFrame()
718             outputWriter.open(startFrame, endFrame)
719         
720         # Do the rendering process frame by frame
721         print "Start Rendering!"
722         for f in range(startFrame, endFrame+1):
723             context.currentFrame(f)
724
725             # Use some temporary workspace, a full copy of the scene
726             inputScene = self._SCENE.copy(2)
727
728             print "Before"
729
730             try:
731                 renderedScene = self.doRenderScene(inputScene)
732             except:
733                 self._SCENE.makeCurrent()
734                 Scene.unlink(inputScene)
735                 del inputScene
736
737             outputWriter.printCanvas(renderedScene,
738                     doPrintPolygons = config.polygons['FILL'],
739                     doPrintEdges    = config.edges['SHOW'],
740                     showHiddenEdges = config.edges['SHOW_HIDDEN'])
741             
742             print "After"
743
744             # clear the rendered scene
745             self._SCENE.makeCurrent()
746             Scene.unlink(renderedScene)
747             del renderedScene
748
749         outputWriter.close()
750         print "Done!"
751         context.currentFrame(currentFrame)
752
753
754     def doRenderScene(self, workScene):
755         """Control the rendering process.
756         
757         Here we control the entire rendering process invoking the operation
758         needed to transform and project the 3D scene in two dimensions.
759         """
760         
761         # global processing of the scene
762
763         self._doConvertGeometricObjToMesh(workScene)
764
765         self._doSceneClipping(workScene)
766
767
768         # XXX: Joining objects does not work in batch mode!!
769         # Do not touch the following if, please :)
770
771         global OPTIMIZE_FOR_SPACE
772         if Blender.mode == 'background':
773             print "\nWARNING! Joining objects not supported in background mode!\n"
774             OPTIMIZE_FOR_SPACE = False
775
776         if OPTIMIZE_FOR_SPACE:
777             self._joinMeshObjectsInScene(workScene)
778
779
780         self._doSceneDepthSorting(workScene)
781         
782         # Per object activities
783
784         Objects = workScene.getChildren()
785         for obj in Objects:
786             
787             if obj.getType() != 'Mesh':
788                 print "Only Mesh supported! - Skipping type:", obj.getType()
789                 continue
790
791             print "Rendering: ", obj.getName()
792
793             mesh = obj.getData()
794
795             self._doModelToWorldCoordinates(mesh, obj.matrix)
796
797             self._doObjectDepthSorting(mesh)
798             
799             # We use both Mesh and NMesh because for depth sorting we change
800             # face order and Mesh class don't let us to do that.
801             mesh.update()
802             mesh = obj.getData(mesh=1)
803             
804             self._doBackFaceCulling(mesh)
805             
806             self._doColorAndLighting(mesh)
807
808             self._doEdgesStyle(mesh, edgeSelectionStyles[EDGE_STYLE])
809
810             self._doProjection(mesh, self.proj)
811             
812             # Update the object data, important! :)
813             mesh.update()
814
815         return workScene
816
817
818     ##
819     # Private Methods
820     #
821
822     # Utility methods
823
824     def _getObjPosition(self, obj):
825         """Return the obj position in World coordinates.
826         """
827         return obj.matrix.translationPart()
828
829     def _cameraViewDirection(self):
830         """Get the View Direction form the camera matrix.
831         """
832         return Vector(self.cameraObj.matrix[2]).resize3D()
833
834
835     # Faces methods
836
837     def _isFaceVisible(self, face):
838         """Determine if a face of an object is visible from the current camera.
839         
840         The view vector is calculated from the camera location and one of the
841         vertices of the face (expressed in World coordinates, after applying
842         modelview transformations).
843
844         After those transformations we determine if a face is visible by
845         computing the angle between the face normal and the view vector, this
846         angle has to be between -90 and 90 degrees for the face to be visible.
847         This corresponds somehow to the dot product between the two, if it
848         results > 0 then the face is visible.
849
850         There is no need to normalize those vectors since we are only interested in
851         the sign of the cross product and not in the product value.
852
853         NOTE: here we assume the face vertices are in WorldCoordinates, so
854         please transform the object _before_ doing the test.
855         """
856
857         normal = Vector(face.no)
858         camPos = self._getObjPosition(self.cameraObj)
859         view_vect = None
860
861         # View Vector in orthographics projections is the view Direction of
862         # the camera
863         if self.cameraObj.data.getType() == 1:
864             view_vect = self._cameraViewDirection()
865
866         # View vector in perspective projections can be considered as
867         # the difference between the camera position and one point of
868         # the face, we choose the farthest point from the camera.
869         if self.cameraObj.data.getType() == 0:
870             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
871             view_vect = vv[1]
872
873         # if d > 0 the face is visible from the camera
874         d = view_vect * normal
875         
876         if d > 0:
877             return True
878         else:
879             return False
880
881
882     # Scene methods
883
884     def _doConvertGeometricObjToMesh(self, scene):
885         """Convert all "geometric" objects to mesh ones.
886         """
887         geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
888
889         Objects = scene.getChildren()
890         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
891         for obj in objList:
892             old_obj = obj
893             obj = self._convertToRawMeshObj(obj)
894             scene.link(obj)
895             scene.unlink(old_obj)
896
897
898             # XXX Workaround for Text and Curve which have some normals
899             # inverted when they are converted to Mesh, REMOVE that when
900             # blender will fix that!!
901             if old_obj.getType() in ['Curve', 'Text']:
902                 me = obj.getData(mesh=1)
903                 for f in me.faces: f.sel = 1;
904                 for v in me.verts: v.sel = 1;
905                 me.remDoubles(0)
906                 me.triangleToQuad()
907                 me.recalcNormals()
908                 me.update()
909
910
911     def _doSceneClipping(self, scene):
912         """Clip objects against the View Frustum.
913
914         For now clip away only objects according to their center position.
915         """
916
917         cpos = self._getObjPosition(self.cameraObj)
918         view_vect = self._cameraViewDirection()
919
920         near = self.cameraObj.data.clipStart
921         far  = self.cameraObj.data.clipEnd
922
923         aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
924         fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
925         fovy = fovy * 360.0/pi
926
927         Objects = scene.getChildren()
928         for o in Objects:
929             if o.getType() != 'Mesh': continue;
930
931             obj_vect = Vector(cpos) - self._getObjPosition(o)
932
933             d = obj_vect*view_vect
934             theta = AngleBetweenVecs(obj_vect, view_vect)
935             
936             # if the object is outside the view frustum, clip it away
937             if (d < near) or (d > far) or (theta > fovy):
938                 scene.unlink(o)
939
940     def _doSceneDepthSorting(self, scene):
941         """Sort objects in the scene.
942
943         The object sorting is done accordingly to the object centers.
944         """
945
946         c = self._getObjPosition(self.cameraObj)
947
948         by_center_pos = (lambda o1, o2:
949                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
950                 cmp((self._getObjPosition(o1) - Vector(c)).length,
951                     (self._getObjPosition(o2) - Vector(c)).length)
952             )
953
954         # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
955         # then ob1 goes farther than obj2, useful when obj2 has holes
956         by_bbox = None
957         
958         Objects = scene.getChildren()
959         Objects.sort(by_center_pos)
960         
961         # update the scene
962         for o in Objects:
963             scene.unlink(o)
964             scene.link(o)
965
966     def _joinMeshObjectsInScene(self, scene):
967         """Merge all the Mesh Objects in a scene into a single Mesh Object.
968         """
969
970         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
971
972         # FIXME: Object.join() do not work if the list contains 1 object
973         if len(oList) == 1:
974             return
975
976         mesh = Mesh.New()
977         bigObj = Object.New('Mesh', 'BigOne')
978         bigObj.link(mesh)
979
980         bigObj.join(oList)
981         scene.link(bigObj)
982         for o in oList:
983             scene.unlink(o)
984
985         scene.update()
986
987  
988     # Per object methods
989
990     def _convertToRawMeshObj(self, object):
991         """Convert geometry based object to a mesh object.
992         """
993         me = Mesh.New('RawMesh_'+object.name)
994         me.getFromObject(object.name)
995
996         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
997         newObject.link(me)
998
999         # If the object has no materials set a default material
1000         if not me.materials:
1001             me.materials = [Material.New()]
1002             #for f in me.faces: f.mat = 0
1003
1004         newObject.setMatrix(object.getMatrix())
1005
1006         return newObject
1007
1008     def _doModelToWorldCoordinates(self, mesh, matrix):
1009         """Transform object coordinates to world coordinates.
1010
1011         This step is done simply applying to the object its tranformation
1012         matrix and recalculating its normals.
1013         """
1014         # XXX FIXME: blender do not transform normals in the right way when
1015         # there are negative scale values
1016         if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
1017             print "WARNING: Negative scales, expect incorrect results!"
1018
1019         mesh.transform(matrix, True)
1020
1021     def _doObjectDepthSorting(self, mesh):
1022         """Sort faces in an object.
1023
1024         The faces in the object are sorted following the distance of the
1025         vertices from the camera position.
1026         """
1027         c = self._getObjPosition(self.cameraObj)
1028
1029         # hackish sorting of faces
1030
1031         # Sort faces according to the max distance from the camera
1032         by_max_vert_dist = (lambda f1, f2:
1033                 cmp(max([(Vector(v.co)-Vector(c)).length for v in f1]),
1034                     max([(Vector(v.co)-Vector(c)).length for v in f2])))
1035         
1036         # Sort faces according to the min distance from the camera
1037         by_min_vert_dist = (lambda f1, f2:
1038                 cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]),
1039                     min([(Vector(v.co)-Vector(c)).length for v in f2])))
1040         
1041         # Sort faces according to the avg distance from the camera
1042         by_avg_vert_dist = (lambda f1, f2:
1043                 cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1),
1044                     sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2)))
1045
1046         mesh.faces.sort(by_max_vert_dist)
1047         mesh.faces.reverse()
1048
1049     def _doBackFaceCulling(self, mesh):
1050         """Simple Backface Culling routine.
1051         
1052         At this level we simply do a visibility test face by face and then
1053         select the vertices belonging to visible faces.
1054         """
1055         
1056         # Select all vertices, so edges can be displayed even if there are no
1057         # faces
1058         for v in mesh.verts:
1059             v.sel = 1
1060         
1061         Mesh.Mode(Mesh.SelectModes['FACE'])
1062         # Loop on faces
1063         for f in mesh.faces:
1064             f.sel = 0
1065             if self._isFaceVisible(f):
1066                 f.sel = 1
1067
1068         # Is this the correct way to propagate the face selection info to the
1069         # vertices belonging to a face ??
1070         # TODO: Using the Mesh module this should come for free. Right?
1071         #Mesh.Mode(Mesh.SelectModes['VERTEX'])
1072         #for f in mesh.faces:
1073         #    if not f.sel:
1074         #        for v in f: v.sel = 0;
1075
1076         #for f in mesh.faces:
1077         #    if f.sel:
1078         #        for v in f: v.sel = 1;
1079
1080     def _doColorAndLighting(self, mesh):
1081         """Apply an Illumination model to the object.
1082
1083         The Illumination model used is the Phong one, it may be inefficient,
1084         but I'm just learning about rendering and starting from Phong seemed
1085         the most natural way.
1086         """
1087
1088         # If the mesh has vertex colors already, use them,
1089         # otherwise turn them on and do some calculations
1090         if mesh.vertexColors:
1091             return
1092         mesh.vertexColors = 1
1093
1094         materials = mesh.materials
1095         
1096         # TODO: use multiple lighting sources
1097         light_obj = self.lights[0]
1098         light_pos = self._getObjPosition(light_obj)
1099         light = light_obj.data
1100
1101         camPos = self._getObjPosition(self.cameraObj)
1102         
1103         # We do per-face color calculation (FLAT Shading), we can easily turn
1104         # to a per-vertex calculation if we want to implement some shading
1105         # technique. For an example see:
1106         # http://www.miralab.unige.ch/papers/368.pdf
1107         for f in mesh.faces:
1108             if not f.sel:
1109                 continue
1110
1111             mat = None
1112             if materials:
1113                 mat = materials[f.mat]
1114
1115             # A new default material
1116             if mat == None:
1117                 mat = Material.New('defMat')
1118             
1119             L = Vector(light_pos).normalize()
1120
1121             V = (Vector(camPos) - Vector(f.v[0].co)).normalize()
1122
1123             N = Vector(f.no).normalize()
1124
1125             R = 2 * (N*L) * N - L
1126
1127             # TODO: Attenuation factor (not used for now)
1128             a0 = 1; a1 = 0.0; a2 = 0.0
1129             d = (Vector(f.v[0].co) - Vector(light_pos)).length
1130             fd = min(1, 1.0/(a0 + a1*d + a2*d*d))
1131
1132             # Ambient component
1133             Ia = 1.0
1134             ka = mat.getAmb() * Vector([0.1, 0.1, 0.1])
1135             Iamb = Ia * ka
1136             
1137             # Diffuse component (add light.col for kd)
1138             kd = mat.getRef() * Vector(mat.getRGBCol())
1139             Ip = light.getEnergy()
1140             #Idiff = Ip * kd * int(N*L > 0.5)
1141             Idiff = Ip * kd * MeshUtils.toonShading(N*L)
1142
1143             # Specular component
1144             ks = mat.getSpec() * Vector(mat.getSpecCol())
1145             ns = mat.getHardness()
1146             Ispec = Ip * ks * pow((V*R), ns)
1147
1148             # Emissive component
1149             ki = Vector([mat.getEmit()]*3)
1150
1151             I = ki + Iamb + Idiff + Ispec
1152
1153
1154             # Set Alpha component
1155             I = list(I)
1156             I.append(mat.getAlpha())
1157
1158             # Toon shading
1159             #I = [MeshUtils.toonShading(c) for c in I] 
1160
1161             # Clamp I values between 0 and 1
1162             I = [ min(c, 1) for c in I]
1163             I = [ max(0, c) for c in I]
1164             tmp_col = [ int(c * 255.0) for c in I]
1165
1166             for c in f.col:
1167                 c.r = tmp_col[0]
1168                 c.g = tmp_col[1]
1169                 c.b = tmp_col[2]
1170                 c.a = tmp_col[3]
1171
1172     def _doEdgesStyle(self, mesh, edgestyleSelect):
1173         """Process Mesh Edges accroding to a given selection style.
1174
1175         Examples of algorithms:
1176
1177         Contours:
1178             given an edge if its adjacent faces have the same normal (that is
1179             they are complanar), than deselect it.
1180
1181         Silhouettes:
1182             given an edge if one its adjacent faces is frontfacing and the
1183             other is backfacing, than select it, else deselect.
1184         """
1185
1186         Mesh.Mode(Mesh.SelectModes['EDGE'])
1187
1188         for e in mesh.edges:
1189
1190             e.sel = 0
1191             if edgestyleSelect(e, mesh):
1192                 e.sel = 1
1193                 
1194     def _doProjection(self, mesh, projector):
1195         """Calculate the Projection for the object.
1196         """
1197         # TODO: maybe using the object.transform() can be faster?
1198
1199         for v in mesh.verts:
1200             p = projector.doProjection(v.co)
1201             v.co[0] = p[0]
1202             v.co[1] = p[1]
1203             v.co[2] = p[2]
1204
1205
1206
1207 # ---------------------------------------------------------------------
1208 #
1209 ## GUI Class and Main Program
1210 #
1211 # ---------------------------------------------------------------------
1212
1213
1214 from Blender import BGL, Draw
1215 from Blender.BGL import *
1216
1217 class GUI:
1218     
1219     def _init():
1220
1221         # Output Format menu 
1222         output_format = config.output['FORMAT']
1223         default_value = outputWriters.keys().index(output_format)+1
1224         GUI.outFormatMenu = Draw.Create(default_value)
1225         GUI.evtOutFormatMenu = 0
1226
1227         # Animation toggle button
1228         GUI.animToggle = Draw.Create(config.output['ANIMATION'])
1229         GUI.evtAnimToggle = 1
1230
1231         # Join Objects toggle button
1232         GUI.joinObjsToggle = Draw.Create(config.output['MERGED_OBJECTS'])
1233         GUI.evtJoinObjsToggle = 2
1234
1235         # Render filled polygons
1236         GUI.polygonsToggle = Draw.Create(config.polygons['FILL'])
1237
1238         GUI.evtPolygonsToggle = 3
1239         # We hide the config.polygons['EXPANSION_TRICK'], for now
1240
1241         # Render polygon edges
1242         GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
1243         GUI.evtShowEdgesToggle = 4
1244
1245         # Render hidden edges
1246         GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
1247         GUI.evtShowHiddenEdgesToggle = 5
1248
1249         # Edge Style menu 
1250         edge_style = config.edges['STYLE']
1251         default_value = edgeSelectionStyles.keys().index(edge_style)+1
1252         GUI.edgeStyleMenu = Draw.Create(default_value)
1253         GUI.evtEdgeStyleMenu = 6
1254
1255         # Edge Width slider
1256         GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
1257         GUI.evtEdgeWidthSlider = 7
1258
1259         # Render Button
1260         GUI.evtRenderButton = 8
1261
1262         # Exit Button
1263         GUI.evtExitButton = 9
1264
1265     def draw():
1266
1267         # initialize static members
1268         GUI._init()
1269
1270         glClear(GL_COLOR_BUFFER_BIT)
1271         glColor3f(0.0, 0.0, 0.0)
1272         glRasterPos2i(10, 350)
1273         Draw.Text("VRM: Vector Rendering Method script.")
1274         glRasterPos2i(10, 335)
1275         Draw.Text("Press Q or ESC to quit.")
1276
1277         # Build the output format menu
1278         glRasterPos2i(10, 310)
1279         Draw.Text("Select the output Format:")
1280         outMenuStruct = "Output Format %t"
1281         for t in outputWriters.keys():
1282            outMenuStruct = outMenuStruct + "|%s" % t
1283         GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
1284                 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
1285
1286         # Animation toggle
1287         GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
1288                 10, 260, 160, 18, GUI.animToggle.val,
1289                 "Toggle rendering of animations")
1290
1291         # Join Objects toggle
1292         GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
1293                 10, 235, 160, 18, GUI.joinObjsToggle.val,
1294                 "Join objects in the rendered file")
1295
1296         # Render Button
1297         Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
1298                 "Start Rendering")
1299         Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
1300
1301         # Rendering Styles
1302         glRasterPos2i(200, 310)
1303         Draw.Text("Rendering Style:")
1304
1305         # Render Polygons
1306         GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
1307                 200, 285, 160, 18, GUI.polygonsToggle.val,
1308                 "Render filled polygons")
1309
1310         # Render Edges
1311         GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
1312                 200, 260, 160, 18, GUI.showEdgesToggle.val,
1313                 "Render polygon edges")
1314
1315         if GUI.showEdgesToggle.val == 1:
1316             
1317             # Edge Style
1318             edgeStyleMenuStruct = "Edge Style %t"
1319             for t in edgeSelectionStyles.keys():
1320                edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t
1321             GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
1322                     200, 235, 160, 18, GUI.edgeStyleMenu.val,
1323                     "Choose the edge style")
1324
1325             # Edge size
1326             GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
1327                     200, 210, 160, 18, GUI.edgeWidthSlider.val,
1328                     0.0, 10.0, 0, "Change Edge Width")
1329
1330             # Show Hidden Edges
1331             GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
1332                     GUI.evtShowHiddenEdgesToggle,
1333                     200, 185, 160, 18, GUI.showHiddenEdgesToggle.val,
1334                     "Render hidden edges as dashed lines")
1335
1336         glRasterPos2i(10, 160)
1337         Draw.Text("Antonio Ospite (c) 2006")
1338
1339     def event(evt, val):
1340
1341         if evt == Draw.ESCKEY or evt == Draw.QKEY:
1342             Draw.Exit()
1343         else:
1344             return
1345
1346         Draw.Redraw(1)
1347
1348     def button_event(evt):
1349
1350         if evt == GUI.evtExitButton:
1351             Draw.Exit()
1352
1353         elif evt == GUI.evtOutFormatMenu:
1354             i = GUI.outFormatMenu.val - 1
1355             config.output['FORMAT']= outputWriters.keys()[i]
1356
1357         elif evt == GUI.evtAnimToggle:
1358             config.outpur['ANIMATION'] = bool(GUI.animToggle.val)
1359
1360         elif evt == GUI.evtJoinObjsToggle:
1361             config.output['MERGED_OBJECTS'] = bool(GUI.joinObjsToggle.val)
1362
1363         elif evt == GUI.evtPolygonsToggle:
1364             config.polygons['FILL'] = bool(GUI.polygonsToggle.val)
1365
1366         elif evt == GUI.evtShowEdgesToggle:
1367             config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
1368
1369         elif evt == GUI.evtShowHiddenEdgesToggle:
1370             config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
1371
1372         elif evt == GUI.evtEdgeStyleMenu:
1373             i = GUI.edgeStyleMenu.val - 1
1374             config.edges['STYLE'] = edgeSelectionStyles.keys()[i]
1375
1376         elif evt == GUI.evtEdgeWidthSlider:
1377             config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
1378
1379         elif evt == GUI.evtRenderButton:
1380             label = "Save %s" % config.output['FORMAT']
1381             # Show the File Selector
1382             global outputfile
1383             Blender.Window.FileSelector(vectorize, label, outputfile)
1384
1385         else:
1386             print "Event: %d not handled!" % evt
1387
1388         if evt:
1389             Draw.Redraw(1)
1390             GUI.conf_debug()
1391
1392     def conf_debug():
1393         from pprint import pprint
1394         pprint(config)
1395
1396     _init = staticmethod(_init)
1397     draw = staticmethod(draw)
1398     event = staticmethod(event)
1399     button_event = staticmethod(button_event)
1400     conf_debug = staticmethod(conf_debug)
1401
1402 # A wrapper function for the vectorizing process
1403 def vectorize(filename):
1404     """The vectorizing process is as follows:
1405      
1406      - Instanciate the writer and the renderer
1407      - Render!
1408      """
1409
1410     if filename == "":
1411         print "\nERROR: invalid file name!"
1412         return
1413
1414     from Blender import Window
1415     editmode = Window.EditMode()
1416     if editmode: Window.EditMode(0)
1417
1418     actualWriter = outputWriters[config.output['FORMAT']]
1419     writer = actualWriter(filename)
1420     
1421     renderer = Renderer()
1422     renderer.doRendering(writer, config.output['ANIMATION'])
1423
1424     if editmode: Window.EditMode(1) 
1425
1426
1427 # Here the main
1428 if __name__ == "__main__":
1429     
1430     outputfile = ""
1431     basename = Blender.sys.basename(Blender.Get('filename'))
1432     if basename != "":
1433         outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
1434
1435     if Blender.mode == 'background':
1436         vectorize(outputfile)
1437     else:
1438         Draw.Register(GUI.draw, GUI.event, GUI.button_event)