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