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