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