bc44cfd36ad5741715154c469b93f0133db515b2
[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://projects.blender.org/projects/vrm"]
11 __version__ = "0.3.beta"
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 #   - FIX the issue with negative scales in object tranformations!
46 #   - Use a better depth sorting algorithm
47 #   - Implement clipping of primitives and do handle object intersections.
48 #     (for now only clipping away whole objects is supported).
49 #   - Review how selections are made (this script uses selection states of
50 #     primitives to represent visibility infos)
51 #   - Use a data structure other than Mesh to represent the 2D image? 
52 #     Think to a way to merge (adjacent) polygons that have the same color.
53 #     Or a way to use paths for silhouettes and contours.
54 #   - Consider SMIL for animation handling instead of ECMA Script? (Firefox do
55 #     not support SMIL for animations)
56 #   - Switch to the Mesh structure, should be considerably faster
57 #     (partially done, but with Mesh we cannot sort faces, yet)
58 #   - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
59 #   - Implement Shading Styles? (partially done, to make more flexible).
60 #   - Add Vector Writers other than SVG.
61 #   - Check memory use!!
62 #   - Support Indexed palettes!! (Useful for ILDA FILES, for example,
63 #     see http://www.linux-laser.org/download/autotrace/ilda-output.patch)
64 #
65 # ---------------------------------------------------------------------
66 #
67 # Changelog:
68 #
69 #   vrm-0.3.py  - ...
70 #     * First release after code restucturing.
71 #       Now the script offers a useful set of functionalities
72 #       and it can render animations, too.
73 #     * Optimization in Renderer.doEdgeStyle(), build a topology cache
74 #       so to speed up the lookup of adjacent faces of an edge.
75 #       Thanks ideasman42.
76 #     * The SVG output is now SVG 1.0 valid.
77 #       Checked with: http://jiggles.w3.org/svgvalidator/ValidatorURI.html
78 #     * Progress indicator during HSR.
79 #
80 # ---------------------------------------------------------------------
81
82 import Blender
83 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera, Window
84 from Blender.Mathutils import *
85 from math import *
86 import sys, time
87
88
89 # Some global settings
90
91 class config:
92     polygons = dict()
93     polygons['SHOW'] = True
94     polygons['SHADING'] = 'FLAT'
95     polygons['HSR'] = 'PAINTER' # 'PAINTER' or 'NEWELL'
96     #polygons['HSR'] = 'NEWELL'
97     # Hidden to the user for now
98     polygons['EXPANSION_TRICK'] = True
99
100     polygons['TOON_LEVELS'] = 2
101
102     edges = dict()
103     edges['SHOW'] = False
104     edges['SHOW_HIDDEN'] = False
105     edges['STYLE'] = 'MESH'
106     edges['WIDTH'] = 2
107     edges['COLOR'] = [0, 0, 0]
108
109     output = dict()
110     output['FORMAT'] = 'SVG'
111     output['ANIMATION'] = False
112     output['JOIN_OBJECTS'] = True
113
114
115
116 # Debug utility function
117 print_debug = False
118 def debug(msg):
119     if print_debug:
120         sys.stderr.write(msg)
121
122
123 # ---------------------------------------------------------------------
124 #
125 ## Mesh Utility class
126 #
127 # ---------------------------------------------------------------------
128 class MeshUtils:
129
130     def buildEdgeFaceUsersCache(me):
131         ''' 
132         Takes a mesh and returns a list aligned with the meshes edges.
133         Each item is a list of the faces that use the edge
134         would be the equiv for having ed.face_users as a property
135
136         Taken from .blender/scripts/bpymodules/BPyMesh.py,
137         thanks to ideasman_42.
138         '''
139
140         def sorted_edge_indicies(ed):
141             i1= ed.v1.index
142             i2= ed.v2.index
143             if i1>i2:
144                 i1,i2= i2,i1
145             return i1, i2
146
147         
148         face_edges_dict= dict([(sorted_edge_indicies(ed), (ed.index, [])) for ed in me.edges])
149         for f in me.faces:
150             fvi= [v.index for v in f.v]# face vert idx's
151             for i in xrange(len(f)):
152                 i1= fvi[i]
153                 i2= fvi[i-1]
154                 
155                 if i1>i2:
156                     i1,i2= i2,i1
157                 
158                 face_edges_dict[i1,i2][1].append(f)
159         
160         face_edges= [None] * len(me.edges)
161         for ed_index, ed_faces in face_edges_dict.itervalues():
162             face_edges[ed_index]= ed_faces
163         
164         return face_edges
165
166     def isMeshEdge(adjacent_faces):
167         """Mesh edge rule.
168
169         A mesh edge is visible if _at_least_one_ of its adjacent faces is selected.
170         Note: if the edge has no adjacent faces we want to show it as well,
171         useful for "edge only" portion of objects.
172         """
173
174         if len(adjacent_faces) == 0:
175             return True
176
177         selected_faces = [f for f in adjacent_faces if f.sel]
178
179         if len(selected_faces) != 0:
180             return True
181         else:
182             return False
183
184     def isSilhouetteEdge(adjacent_faces):
185         """Silhuette selection rule.
186
187         An edge is a silhuette edge if it is shared by two faces with
188         different selection status or if it is a boundary edge of a selected
189         face.
190         """
191
192         if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
193             (len(adjacent_faces) == 2 and
194                 adjacent_faces[0].sel != adjacent_faces[1].sel)
195             ):
196             return True
197         else:
198             return False
199
200     buildEdgeFaceUsersCache = staticmethod(buildEdgeFaceUsersCache)
201     isMeshEdge = staticmethod(isMeshEdge)
202     isSilhouetteEdge = staticmethod(isSilhouetteEdge)
203
204
205 # ---------------------------------------------------------------------
206 #
207 ## Shading Utility class
208 #
209 # ---------------------------------------------------------------------
210 class ShadingUtils:
211
212     shademap = None
213
214     def toonShadingMapSetup():
215         levels = config.polygons['TOON_LEVELS']
216
217         texels = 2*levels - 1
218         tmp_shademap = [0.0] + [(i)/float(texels-1) for i in xrange(1, texels-1) ] + [1.0]
219
220         return tmp_shademap
221
222     def toonShading(u):
223
224         shademap = ShadingUtils.shademap
225
226         if not shademap:
227             shademap = ShadingUtils.toonShadingMapSetup()
228
229         v = 1.0
230         for i in xrange(0, len(shademap)-1):
231             pivot = (shademap[i]+shademap[i+1])/2.0
232             j = int(u>pivot)
233
234             v = shademap[i+j]
235
236             if v < shademap[i+1]:
237                 return v
238
239         return v
240
241     toonShadingMapSetup = staticmethod(toonShadingMapSetup)
242     toonShading = staticmethod(toonShading)
243
244
245 # ---------------------------------------------------------------------
246 #
247 ## Projections classes
248 #
249 # ---------------------------------------------------------------------
250
251 class Projector:
252     """Calculate the projection of an object given the camera.
253     
254     A projector is useful to so some per-object transformation to obtain the
255     projection of an object given the camera.
256     
257     The main method is #doProjection# see the method description for the
258     parameter list.
259     """
260
261     def __init__(self, cameraObj, canvasRatio):
262         """Calculate the projection matrix.
263
264         The projection matrix depends, in this case, on the camera settings.
265         TAKE CARE: This projector expects vertices in World Coordinates!
266         """
267
268         camera = cameraObj.getData()
269
270         aspect = float(canvasRatio[0])/float(canvasRatio[1])
271         near = camera.clipStart
272         far = camera.clipEnd
273
274         scale = float(camera.scale)
275
276         fovy = atan(0.5/aspect/(camera.lens/32))
277         fovy = fovy * 360.0/pi
278         
279         # What projection do we want?
280         if camera.type == 0:
281             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
282         elif camera.type == 1:
283             mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) 
284         
285         # View transformation
286         cam = Matrix(cameraObj.getInverseMatrix())
287         cam.transpose() 
288         
289         mP = mP * cam
290
291         self.projectionMatrix = mP
292
293     ##
294     # Public methods
295     #
296
297     def doProjection(self, v):
298         """Project the point on the view plane.
299
300         Given a vertex calculate the projection using the current projection
301         matrix.
302         """
303         
304         # Note that we have to work on the vertex using homogeneous coordinates
305         # From blender 2.42+ we don't need to resize the vector to be 4d
306         # when applying a 4x4 matrix, but we do that anyway since we need the
307         # 4th coordinate later
308         p = self.projectionMatrix * Vector(v).resize4D()
309         
310         # Perspective division
311         if p[3] != 0:
312             p[0] = p[0]/p[3]
313             p[1] = p[1]/p[3]
314             p[2] = p[2]/p[3]
315
316         # restore the size
317         p[3] = 1.0
318         p.resize3D()
319
320         return p
321
322
323     ##
324     # Private methods
325     #
326     
327     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
328         """Return a perspective projection matrix.
329         """
330         
331         top = near * tan(fovy * pi / 360.0)
332         bottom = -top
333         left = bottom*aspect
334         right= top*aspect
335         x = (2.0 * near) / (right-left)
336         y = (2.0 * near) / (top-bottom)
337         a = (right+left) / (right-left)
338         b = (top+bottom) / (top - bottom)
339         c = - ((far+near) / (far-near))
340         d = - ((2*far*near)/(far-near))
341         
342         m = Matrix(
343                 [x,   0.0,    a,    0.0],
344                 [0.0,   y,    b,    0.0],
345                 [0.0, 0.0,    c,      d],
346                 [0.0, 0.0, -1.0,    0.0])
347
348         return m
349
350     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
351         """Return an orthogonal projection matrix.
352         """
353         
354         # The 11 in the formula was found emiprically
355         top = near * tan(fovy * pi / 360.0) * (scale * 11)
356         bottom = -top 
357         left = bottom * aspect
358         right= top * aspect
359         rl = right-left
360         tb = top-bottom
361         fn = near-far 
362         tx = -((right+left)/rl)
363         ty = -((top+bottom)/tb)
364         tz = ((far+near)/fn)
365
366         m = Matrix(
367                 [2.0/rl, 0.0,    0.0,     tx],
368                 [0.0,    2.0/tb, 0.0,     ty],
369                 [0.0,    0.0,    2.0/fn,  tz],
370                 [0.0,    0.0,    0.0,    1.0])
371         
372         return m
373
374
375 # ---------------------------------------------------------------------
376 #
377 ## Progress Indicator
378 #
379 # ---------------------------------------------------------------------
380
381 class Progress:
382     """A model for a progress indicator.
383     
384     Do the progress calculation calculation and
385     the view independent stuff of a progress indicator.
386     """
387     def __init__(self, steps=0):
388         self.name = ""
389         self.steps = steps
390         self.completed = 0
391         self.progress = 0
392
393     def setSteps(self, steps):
394         """Set the number of steps of the activity wich we want to track.
395         """
396         self.steps = steps
397
398     def getSteps(self):
399         return self.steps
400
401     def setName(self, name):
402         """Set the name of the activity wich we want to track.
403         """
404         self.name = name
405
406     def getName(self):
407         return self.name
408
409     def getProgress(self):
410         return self.progress
411
412     def reset(self):
413         self.completed = 0
414         self.progress = 0
415
416     def update(self):
417         """Update the model, call this method when one step is completed.
418         """
419         if self.progress == 100:
420             return False
421
422         self.completed += 1
423         self.progress = ( float(self.completed) / float(self.steps) ) * 100
424         self.progress = int(self.progress)
425
426         return True
427
428
429 class ProgressIndicator:
430     """An abstraction of a View for the Progress Model
431     """
432     def __init__(self):
433
434         # Use a refresh rate so we do not show the progress at
435         # every update, but every 'self.refresh_rate' times.
436         self.refresh_rate = 10
437         self.shows_counter = 0
438
439         self.progressModel = None
440
441     def setActivity(self, name, steps):
442         """Initialize the Model.
443
444         In a future version (with subactivities-progress support) this method
445         could only set the current activity.
446         """
447         self.progressModel = Progress()
448         self.progressModel.setName(name)
449         self.progressModel.setSteps(steps)
450
451     def getActivity(self):
452         return self.progressModel
453
454     def update(self):
455         """Update the model and show the actual progress.
456         """
457         assert(self.progressModel)
458
459         if self.progressModel.update():
460             self.show(self.progressModel.getProgress(),
461                     self.progressModel.getName())
462
463         # We return always True here so we can call the update() method also
464         # from lambda funcs (putting the call in logical AND with other ops)
465         return True
466
467     def show(self, progress, name=""):
468         self.shows_counter = (self.shows_counter + 1) % self.refresh_rate
469         if self.shows_counter != 0:
470             return
471
472         if progress == 100:
473             self.shows_counter = -1
474
475
476 class ConsoleProgressIndicator(ProgressIndicator):
477     """Show a progress bar on stderr, a la wget.
478     """
479     def __init__(self):
480         ProgressIndicator.__init__(self)
481
482         self.swirl_chars = ["-", "\\", "|", "/"]
483         self.swirl_count = -1
484
485     def show(self, progress, name):
486         ProgressIndicator.show(self, progress, name)
487         
488         bar_length = 70
489         bar_progress = int( (progress/100.0) * bar_length )
490         bar = ("=" * bar_progress).ljust(bar_length)
491
492         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
493         swirl_char = self.swirl_chars[self.swirl_count]
494
495         progress_bar = "%s |%s| %c %3d%%" % (name, bar, swirl_char, progress)
496
497         sys.stderr.write(progress_bar+"\r")
498         if progress == 100:
499             sys.stderr.write("\n")
500
501
502 class GraphicalProgressIndicator(ProgressIndicator):
503     """Interface to the Blender.Window.DrawProgressBar() method.
504     """
505     def __init__(self):
506         ProgressIndicator.__init__(self)
507
508         #self.swirl_chars = ["-", "\\", "|", "/"]
509         # We have to use letters with the same width, for now!
510         # Blender progress bar considers the font widths when
511         # calculating the progress bar width.
512         self.swirl_chars = ["\\", "/"]
513         self.swirl_count = -1
514
515     def show(self, progress, name):
516         ProgressIndicator.show(self, progress)
517
518         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
519         swirl_char = self.swirl_chars[self.swirl_count]
520
521         progress_text = "%s - %c %3d%%" % (name, swirl_char, progress)
522
523         # Finally draw  the Progress Bar
524         Window.WaitCursor(1) # Maybe we can move that call in the constructor?
525         Window.DrawProgressBar(progress/100.0, progress_text)
526
527         if progress == 100:
528             Window.DrawProgressBar(1, progress_text)
529             Window.WaitCursor(0)
530
531
532
533 # ---------------------------------------------------------------------
534 #
535 ## 2D Object representation class
536 #
537 # ---------------------------------------------------------------------
538
539 # TODO: a class to represent the needed properties of a 2D vector image
540 # For now just using a [N]Mesh structure.
541
542
543 # ---------------------------------------------------------------------
544 #
545 ## Vector Drawing Classes
546 #
547 # ---------------------------------------------------------------------
548
549 ## A generic Writer
550
551 class VectorWriter:
552     """
553     A class for printing output in a vectorial format.
554
555     Given a 2D representation of the 3D scene the class is responsible to
556     write it is a vector format.
557
558     Every subclasses of VectorWriter must have at last the following public
559     methods:
560         - open(self)
561         - close(self)
562         - printCanvas(self, scene,
563             doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
564     """
565     
566     def __init__(self, fileName):
567         """Set the output file name and other properties"""
568
569         self.outputFileName = fileName
570         self.file = None
571         
572         context = Scene.GetCurrent().getRenderingContext()
573         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
574
575         self.startFrame = 1
576         self.endFrame = 1
577         self.animation = False
578
579
580     ##
581     # Public Methods
582     #
583     
584     def open(self, startFrame=1, endFrame=1):
585         if startFrame != endFrame:
586             self.startFrame = startFrame
587             self.endFrame = endFrame
588             self.animation = True
589
590         self.file = open(self.outputFileName, "w")
591         print "Outputting to: ", self.outputFileName
592
593         return
594
595     def close(self):
596         self.file.close()
597         return
598
599     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
600             showHiddenEdges=False):
601         """This is the interface for the needed printing routine.
602         """
603         return
604         
605
606 ## SVG Writer
607
608 class SVGVectorWriter(VectorWriter):
609     """A concrete class for writing SVG output.
610     """
611
612     def __init__(self, fileName):
613         """Simply call the parent Contructor.
614         """
615         VectorWriter.__init__(self, fileName)
616
617
618     ##
619     # Public Methods
620     #
621
622     def open(self, startFrame=1, endFrame=1):
623         """Do some initialization operations.
624         """
625         VectorWriter.open(self, startFrame, endFrame)
626         self._printHeader()
627
628     def close(self):
629         """Do some finalization operation.
630         """
631         self._printFooter()
632
633         # remember to call the close method of the parent
634         VectorWriter.close(self)
635
636         
637     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
638             showHiddenEdges=False):
639         """Convert the scene representation to SVG.
640         """
641
642         Objects = scene.getChildren()
643
644         context = scene.getRenderingContext()
645         framenumber = context.currentFrame()
646
647         if self.animation:
648             framestyle = "display:none"
649         else:
650             framestyle = "display:block"
651         
652         # Assign an id to this group so we can set properties on it using DOM
653         self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
654                 (framenumber, framestyle) )
655
656
657         for obj in Objects:
658
659             if(obj.getType() != 'Mesh'):
660                 continue
661
662             self.file.write("<g id=\"%s\">\n" % obj.getName())
663
664             mesh = obj.getData(mesh=1)
665
666             if doPrintPolygons:
667                 self._printPolygons(mesh)
668
669             if doPrintEdges:
670                 self._printEdges(mesh, showHiddenEdges)
671             
672             self.file.write("</g>\n")
673
674         self.file.write("</g>\n")
675
676     
677     ##  
678     # Private Methods
679     #
680     
681     def _calcCanvasCoord(self, v):
682         """Convert vertex in scene coordinates to canvas coordinates.
683         """
684
685         pt = Vector([0, 0, 0])
686         
687         mW = float(self.canvasSize[0])/2.0
688         mH = float(self.canvasSize[1])/2.0
689
690         # rescale to canvas size
691         pt[0] = v.co[0]*mW + mW
692         pt[1] = v.co[1]*mH + mH
693         pt[2] = v.co[2]
694          
695         # For now we want (0,0) in the top-left corner of the canvas.
696         # Mirror and translate along y
697         pt[1] *= -1
698         pt[1] += self.canvasSize[1]
699         
700         return pt
701
702     def _printHeader(self):
703         """Print SVG header."""
704
705         self.file.write("<?xml version=\"1.0\"?>\n")
706         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n")
707         self.file.write("\t\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
708         self.file.write("<svg version=\"1.0\"\n")
709         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
710         self.file.write("\twidth=\"%d\" height=\"%d\">\n\n" %
711                 self.canvasSize)
712
713         if self.animation:
714
715             self.file.write("""\n<script type="text/javascript"><![CDATA[
716             globalStartFrame=%d;
717             globalEndFrame=%d;
718
719             /* FIXME: Use 1000 as interval as lower values gives problems */
720             timerID = setInterval("NextFrame()", 1000);
721             globalFrameCounter=%d;
722
723             function NextFrame()
724             {
725               currentElement  = document.getElementById('frame'+globalFrameCounter)
726               previousElement = document.getElementById('frame'+(globalFrameCounter-1))
727
728               if (!currentElement)
729               {
730                 return;
731               }
732
733               if (globalFrameCounter > globalEndFrame)
734               {
735                 clearInterval(timerID)
736               }
737               else
738               {
739                 if(previousElement)
740                 {
741                     previousElement.style.display="none";
742                 }
743                 currentElement.style.display="block";
744                 globalFrameCounter++;
745               }
746             }
747             \n]]></script>\n
748             \n""" % (self.startFrame, self.endFrame, self.startFrame) )
749                 
750     def _printFooter(self):
751         """Print the SVG footer."""
752
753         self.file.write("\n</svg>\n")
754
755     def _printPolygons(self, mesh): 
756         """Print the selected (visible) polygons.
757         """
758
759         if len(mesh.faces) == 0:
760             return
761
762         self.file.write("<g>\n")
763
764         for face in mesh.faces:
765             if not face.sel:
766                continue
767
768             self.file.write("<path d=\"")
769
770             p = self._calcCanvasCoord(face.verts[0])
771             self.file.write("M %g,%g L " % (p[0], p[1]))
772
773             for v in face.verts[1:]:
774                 p = self._calcCanvasCoord(v)
775                 self.file.write("%g,%g " % (p[0], p[1]))
776             
777             # get rid of the last blank space, just cosmetics here.
778             self.file.seek(-1, 1) 
779             self.file.write(" z\"\n")
780             
781             # take as face color the first vertex color
782             if face.col:
783                 fcol = face.col[0]
784                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
785             else:
786                 color = [255, 255, 255, 255]
787
788             # Convert the color to the #RRGGBB form
789             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
790
791             # Handle transparent polygons
792             opacity_string = ""
793             if color[3] != 255:
794                 opacity = float(color[3])/255.0
795                 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
796
797             self.file.write("\tstyle=\"fill:" + str_col + ";")
798             self.file.write(opacity_string)
799
800             # use the stroke property to alleviate the "adjacent edges" problem,
801             # we simulate polygon expansion using borders,
802             # see http://www.antigrain.com/svg/index.html for more info
803             stroke_width = 1.0
804
805             if config.polygons['EXPANSION_TRICK']:
806                 self.file.write(" stroke:%s;\n" % str_col)
807                 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
808                 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
809
810             self.file.write("\"/>\n")
811
812         self.file.write("</g>\n")
813
814     def _printEdges(self, mesh, showHiddenEdges=False):
815         """Print the wireframe using mesh edges.
816         """
817
818         stroke_width = config.edges['WIDTH']
819         stroke_col = config.edges['COLOR']
820         
821         self.file.write("<g>\n")
822
823         for e in mesh.edges:
824             
825             hidden_stroke_style = ""
826             
827             if e.sel == 0:
828                 if showHiddenEdges == False:
829                     continue
830                 else:
831                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
832
833             p1 = self._calcCanvasCoord(e.v1)
834             p2 = self._calcCanvasCoord(e.v2)
835             
836             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
837                     % ( p1[0], p1[1], p2[0], p2[1] ) )
838             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
839             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
840             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
841             self.file.write(hidden_stroke_style)
842             self.file.write("\"/>\n")
843
844         self.file.write("</g>\n")
845
846
847 # ---------------------------------------------------------------------
848 #
849 ## Rendering Classes
850 #
851 # ---------------------------------------------------------------------
852
853 # A dictionary to collect different shading style methods
854 shadingStyles = dict()
855 shadingStyles['FLAT'] = None
856 shadingStyles['TOON'] = None
857
858 # A dictionary to collect different edge style methods
859 edgeStyles = dict()
860 edgeStyles['MESH'] = MeshUtils.isMeshEdge
861 edgeStyles['SILHOUETTE'] = MeshUtils.isSilhouetteEdge
862
863 # A dictionary to collect the supported output formats
864 outputWriters = dict()
865 outputWriters['SVG'] = SVGVectorWriter
866
867
868 class Renderer:
869     """Render a scene viewed from the active camera.
870     
871     This class is responsible of the rendering process, transformation and
872     projection of the objects in the scene are invoked by the renderer.
873
874     The rendering is done using the active camera for the current scene.
875     """
876
877     def __init__(self):
878         """Make the rendering process only for the current scene by default.
879
880         We will work on a copy of the scene, to be sure that the current scene do
881         not get modified in any way.
882         """
883
884         # Render the current Scene, this should be a READ-ONLY property
885         self._SCENE = Scene.GetCurrent()
886         
887         # Use the aspect ratio of the scene rendering context
888         context = self._SCENE.getRenderingContext()
889
890         aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
891         self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
892                             float(context.aspectRatioY())
893                             )
894
895         # Render from the currently active camera 
896         self.cameraObj = self._SCENE.getCurrentCamera()
897
898         # Get a projector for this camera.
899         # NOTE: the projector wants object in world coordinates,
900         # so we should remember to apply modelview transformations
901         # _before_ we do projection transformations.
902         self.proj = Projector(self.cameraObj, self.canvasRatio)
903
904         # Get the list of lighting sources
905         obj_lst = self._SCENE.getChildren()
906         self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
907
908         # When there are no lights we use a default lighting source
909         # that have the same position of the camera
910         if len(self.lights) == 0:
911             l = Lamp.New('Lamp')
912             lobj = Object.New('Lamp')
913             lobj.loc = self.cameraObj.loc
914             lobj.link(l) 
915             self.lights.append(lobj)
916
917
918     ##
919     # Public Methods
920     #
921
922     def doRendering(self, outputWriter, animation=False):
923         """Render picture or animation and write it out.
924         
925         The parameters are:
926             - a Vector writer object that will be used to output the result.
927             - a flag to tell if we want to render an animation or only the
928               current frame.
929         """
930         
931         context = self._SCENE.getRenderingContext()
932         origCurrentFrame = context.currentFrame()
933
934         # Handle the animation case
935         if not animation:
936             startFrame = origCurrentFrame
937             endFrame = startFrame
938             outputWriter.open()
939         else:
940             startFrame = context.startFrame()
941             endFrame = context.endFrame()
942             outputWriter.open(startFrame, endFrame)
943         
944         # Do the rendering process frame by frame
945         print "Start Rendering of %d frames" % (endFrame-startFrame)
946         for f in xrange(startFrame, endFrame+1):
947             print "\n\nFrame: %d" % f
948             context.currentFrame(f)
949
950             # Use some temporary workspace, a full copy of the scene
951             inputScene = self._SCENE.copy(2)
952             # And Set our camera accordingly
953             self.cameraObj = inputScene.getCurrentCamera()
954
955             try:
956                 renderedScene = self.doRenderScene(inputScene)
957             except :
958                 print "There was an error! Aborting."
959                 import traceback
960                 print traceback.print_exc()
961
962                 self._SCENE.makeCurrent()
963                 Scene.unlink(inputScene)
964                 del inputScene
965                 return
966
967             outputWriter.printCanvas(renderedScene,
968                     doPrintPolygons = config.polygons['SHOW'],
969                     doPrintEdges    = config.edges['SHOW'],
970                     showHiddenEdges = config.edges['SHOW_HIDDEN'])
971             
972             # delete the rendered scene
973             self._SCENE.makeCurrent()
974             Scene.unlink(renderedScene)
975             del renderedScene
976
977         outputWriter.close()
978         print "Done!"
979         context.currentFrame(origCurrentFrame)
980
981
982     def doRenderScene(self, workScene):
983         """Control the rendering process.
984         
985         Here we control the entire rendering process invoking the operation
986         needed to transform and project the 3D scene in two dimensions.
987         """
988         
989         # global processing of the scene
990
991         self._doSceneClipping(workScene)
992
993         self._doConvertGeometricObjsToMesh(workScene)
994
995         if config.output['JOIN_OBJECTS']:
996             self._joinMeshObjectsInScene(workScene)
997
998         self._doSceneDepthSorting(workScene)
999         
1000         # Per object activities
1001
1002         Objects = workScene.getChildren()
1003         print "Total Objects: %d" % len(Objects)
1004         for i,obj in enumerate(Objects):
1005             print "Rendering Object: %d" % i
1006
1007             if obj.getType() != 'Mesh':
1008                 print "Only Mesh supported! - Skipping type:", obj.getType()
1009                 continue
1010
1011             print "Rendering: ", obj.getName()
1012
1013             mesh = obj.getData(mesh=1)
1014
1015             self._doModelingTransformation(mesh, obj.matrix)
1016
1017             self._doBackFaceCulling(mesh)
1018
1019             self._doLighting(mesh)
1020
1021             # Do "projection" now so we perform further processing
1022             # in Normalized View Coordinates
1023             self._doProjection(mesh, self.proj)
1024
1025             self._doViewFrustumClipping(mesh)
1026
1027             self._doHiddenSurfaceRemoval(mesh)
1028
1029             self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
1030
1031             
1032             # Update the object data, important! :)
1033             mesh.update()
1034
1035         return workScene
1036
1037
1038     ##
1039     # Private Methods
1040     #
1041
1042     # Utility methods
1043
1044     def _getObjPosition(self, obj):
1045         """Return the obj position in World coordinates.
1046         """
1047         return obj.matrix.translationPart()
1048
1049     def _cameraViewVector(self):
1050         """Get the View Direction form the camera matrix.
1051         """
1052         return Vector(self.cameraObj.matrix[2]).resize3D()
1053
1054
1055     # Faces methods
1056
1057     def _isFaceVisible(self, face):
1058         """Determine if a face of an object is visible from the current camera.
1059         
1060         The view vector is calculated from the camera location and one of the
1061         vertices of the face (expressed in World coordinates, after applying
1062         modelview transformations).
1063
1064         After those transformations we determine if a face is visible by
1065         computing the angle between the face normal and the view vector, this
1066         angle has to be between -90 and 90 degrees for the face to be visible.
1067         This corresponds somehow to the dot product between the two, if it
1068         results > 0 then the face is visible.
1069
1070         There is no need to normalize those vectors since we are only interested in
1071         the sign of the cross product and not in the product value.
1072
1073         NOTE: here we assume the face vertices are in WorldCoordinates, so
1074         please transform the object _before_ doing the test.
1075         """
1076
1077         normal = Vector(face.no)
1078         camPos = self._getObjPosition(self.cameraObj)
1079         view_vect = None
1080
1081         # View Vector in orthographics projections is the view Direction of
1082         # the camera
1083         if self.cameraObj.data.getType() == 1:
1084             view_vect = self._cameraViewVector()
1085
1086         # View vector in perspective projections can be considered as
1087         # the difference between the camera position and one point of
1088         # the face, we choose the farthest point from the camera.
1089         if self.cameraObj.data.getType() == 0:
1090             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
1091             view_vect = vv[1]
1092
1093
1094         # if d > 0 the face is visible from the camera
1095         d = view_vect * normal
1096         
1097         if d > 0:
1098             return True
1099         else:
1100             return False
1101
1102
1103     # Scene methods
1104
1105     def _doSceneClipping(self, scene):
1106         """Clip whole objects against the View Frustum.
1107
1108         For now clip away only objects according to their center position.
1109         """
1110
1111         cpos = self._getObjPosition(self.cameraObj)
1112         view_vect = self._cameraViewVector()
1113
1114         near = self.cameraObj.data.clipStart
1115         far  = self.cameraObj.data.clipEnd
1116
1117         aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
1118         fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
1119         fovy = fovy * 360.0/pi
1120
1121         Objects = scene.getChildren()
1122         for o in Objects:
1123             if o.getType() != 'Mesh': continue;
1124
1125             obj_vect = Vector(cpos) - self._getObjPosition(o)
1126
1127             d = obj_vect*view_vect
1128             theta = AngleBetweenVecs(obj_vect, view_vect)
1129             
1130             # if the object is outside the view frustum, clip it away
1131             if (d < near) or (d > far) or (theta > fovy):
1132                 scene.unlink(o)
1133
1134     def _doConvertGeometricObjsToMesh(self, scene):
1135         """Convert all "geometric" objects to mesh ones.
1136         """
1137         geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
1138
1139         Objects = scene.getChildren()
1140         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
1141         for obj in objList:
1142             old_obj = obj
1143             obj = self._convertToRawMeshObj(obj)
1144             scene.link(obj)
1145             scene.unlink(old_obj)
1146
1147
1148             # XXX Workaround for Text and Curve which have some normals
1149             # inverted when they are converted to Mesh, REMOVE that when
1150             # blender will fix that!!
1151             if old_obj.getType() in ['Curve', 'Text']:
1152                 me = obj.getData(mesh=1)
1153                 for f in me.faces: f.sel = 1;
1154                 for v in me.verts: v.sel = 1;
1155                 me.remDoubles(0)
1156                 me.triangleToQuad()
1157                 me.recalcNormals()
1158                 me.update()
1159
1160
1161     def _doSceneDepthSorting(self, scene):
1162         """Sort objects in the scene.
1163
1164         The object sorting is done accordingly to the object centers.
1165         """
1166
1167         c = self._getObjPosition(self.cameraObj)
1168
1169         by_center_pos = (lambda o1, o2:
1170                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
1171                 cmp((self._getObjPosition(o1) - Vector(c)).length,
1172                     (self._getObjPosition(o2) - Vector(c)).length)
1173             )
1174
1175         # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
1176         # then ob1 goes farther than obj2, useful when obj2 has holes
1177         by_bbox = None
1178         
1179         Objects = scene.getChildren()
1180         Objects.sort(by_center_pos)
1181         
1182         # update the scene
1183         for o in Objects:
1184             scene.unlink(o)
1185             scene.link(o)
1186
1187     def _joinMeshObjectsInScene(self, scene):
1188         """Merge all the Mesh Objects in a scene into a single Mesh Object.
1189         """
1190
1191         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
1192
1193         # FIXME: Object.join() do not work if the list contains 1 object
1194         if len(oList) == 1:
1195             return
1196
1197         mesh = Mesh.New('BigOne')
1198         bigObj = Object.New('Mesh', 'BigOne')
1199         bigObj.link(mesh)
1200
1201         scene.link(bigObj)
1202
1203         try:
1204             bigObj.join(oList)
1205         except RuntimeError:
1206             print "\nWarning! - Can't Join Objects\n"
1207             scene.unlink(bigObj)
1208             return
1209         except TypeError:
1210             print "Objects Type error?"
1211         
1212         for o in oList:
1213             scene.unlink(o)
1214
1215         scene.update()
1216
1217  
1218     # Per object/mesh methods
1219
1220     def _convertToRawMeshObj(self, object):
1221         """Convert geometry based object to a mesh object.
1222         """
1223         me = Mesh.New('RawMesh_'+object.name)
1224         me.getFromObject(object.name)
1225
1226         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
1227         newObject.link(me)
1228
1229         # If the object has no materials set a default material
1230         if not me.materials:
1231             me.materials = [Material.New()]
1232             #for f in me.faces: f.mat = 0
1233
1234         newObject.setMatrix(object.getMatrix())
1235
1236         return newObject
1237
1238     def _doModelingTransformation(self, mesh, matrix):
1239         """Transform object coordinates to world coordinates.
1240
1241         This step is done simply applying to the object its tranformation
1242         matrix and recalculating its normals.
1243         """
1244         # XXX FIXME: blender do not transform normals in the right way when
1245         # there are negative scale values
1246         if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
1247             print "WARNING: Negative scales, expect incorrect results!"
1248
1249         mesh.transform(matrix, True)
1250
1251     def _doBackFaceCulling(self, mesh):
1252         """Simple Backface Culling routine.
1253         
1254         At this level we simply do a visibility test face by face and then
1255         select the vertices belonging to visible faces.
1256         """
1257         
1258         # Select all vertices, so edges can be displayed even if there are no
1259         # faces
1260         for v in mesh.verts:
1261             v.sel = 1
1262         
1263         Mesh.Mode(Mesh.SelectModes['FACE'])
1264         # Loop on faces
1265         for f in mesh.faces:
1266             f.sel = 0
1267             if self._isFaceVisible(f):
1268                 f.sel = 1
1269
1270     def _doLighting(self, mesh):
1271         """Apply an Illumination and shading model to the object.
1272
1273         The model used is the Phong one, it may be inefficient,
1274         but I'm just learning about rendering and starting from Phong seemed
1275         the most natural way.
1276         """
1277
1278         # If the mesh has vertex colors already, use them,
1279         # otherwise turn them on and do some calculations
1280         if mesh.vertexColors:
1281             return
1282         mesh.vertexColors = 1
1283
1284         materials = mesh.materials
1285
1286         camPos = self._getObjPosition(self.cameraObj)
1287
1288         # We do per-face color calculation (FLAT Shading), we can easily turn
1289         # to a per-vertex calculation if we want to implement some shading
1290         # technique. For an example see:
1291         # http://www.miralab.unige.ch/papers/368.pdf
1292         for f in mesh.faces:
1293             if not f.sel:
1294                 continue
1295
1296             mat = None
1297             if materials:
1298                 mat = materials[f.mat]
1299                 # Check if it is a shadeless material
1300                 if mat.getMode() & Material.Modes['SHADELESS']:
1301                     I = mat.getRGBCol()
1302                     # Convert to a value between 0 and 255
1303                     tmp_col = [ int(c * 255.0) for c in I]
1304
1305                     for c in f.col:
1306                         c.r = tmp_col[0]
1307                         c.g = tmp_col[1]
1308                         c.b = tmp_col[2]
1309                         #c.a = tmp_col[3]
1310
1311                     continue
1312
1313
1314
1315             # A new default material
1316             if mat == None:
1317                 mat = Material.New('defMat')
1318
1319             # do vertex color calculation
1320
1321             TotDiffSpec = Vector([0.0, 0.0, 0.0])
1322
1323             for l in self.lights:
1324                 light_obj = l
1325                 light_pos = self._getObjPosition(l)
1326                 light = light_obj.data
1327             
1328                 L = Vector(light_pos).normalize()
1329
1330                 V = (Vector(camPos) - Vector(f.cent)).normalize()
1331
1332                 N = Vector(f.no).normalize()
1333
1334                 if config.polygons['SHADING'] == 'TOON':
1335                     NL = ShadingUtils.toonShading(N*L)
1336                 else:
1337                     NL = (N*L)
1338
1339                 # Should we use NL instead of (N*L) here?
1340                 R = 2 * (N*L) * N - L
1341
1342                 Ip = light.getEnergy()
1343
1344                 # Diffuse co-efficient
1345                 kd = mat.getRef() * Vector(mat.getRGBCol())
1346                 for i in [0, 1, 2]:
1347                     kd[i] *= light.col[i]
1348
1349                 Idiff = Ip * kd * max(0, NL)
1350
1351
1352                 # Specular component
1353                 ks = mat.getSpec() * Vector(mat.getSpecCol())
1354                 ns = mat.getHardness()
1355                 Ispec = Ip * ks * pow(max(0, (V*R)), ns)
1356
1357                 TotDiffSpec += (Idiff+Ispec)
1358
1359
1360             # Ambient component
1361             Iamb = Vector(Blender.World.Get()[0].getAmb())
1362             ka = mat.getAmb()
1363
1364             # Emissive component (convert to a triplet)
1365             ki = Vector([mat.getEmit()]*3)
1366
1367             #I = ki + Iamb + (Idiff + Ispec)
1368             I = ki + (ka * Iamb) + TotDiffSpec
1369
1370
1371             # Set Alpha component
1372             I = list(I)
1373             I.append(mat.getAlpha())
1374
1375             # Clamp I values between 0 and 1
1376             I = [ min(c, 1) for c in I]
1377             I = [ max(0, c) for c in I]
1378
1379             # Convert to a value between 0 and 255
1380             tmp_col = [ int(c * 255.0) for c in I]
1381
1382             for c in f.col:
1383                 c.r = tmp_col[0]
1384                 c.g = tmp_col[1]
1385                 c.b = tmp_col[2]
1386                 c.a = tmp_col[3]
1387
1388     def _doProjection(self, mesh, projector):
1389         """Apply Viewing and Projection tranformations.
1390         """
1391
1392         for v in mesh.verts:
1393             p = projector.doProjection(v.co[:])
1394             v.co[0] = p[0]
1395             v.co[1] = p[1]
1396             v.co[2] = p[2]
1397
1398         #mesh.recalcNormals()
1399         #mesh.update()
1400
1401         # We could reeset Camera matrix, since now
1402         # we are in Normalized Viewing Coordinates,
1403         # but doung that would affect World Coordinate
1404         # processing for other objects
1405
1406         #self.cameraObj.data.type = 1
1407         #self.cameraObj.data.scale = 2.0
1408         #m = Matrix().identity()
1409         #self.cameraObj.setMatrix(m)
1410
1411     def _doViewFrustumClipping(self, mesh):
1412         """Clip faces against the View Frustum.
1413         """
1414
1415     # HSR routines
1416     def __simpleDepthSort(self, mesh):
1417         """Sort faces by the furthest vertex.
1418
1419         This simple mesthod is known also as the painter algorithm, and it
1420         solves HSR correctly only for convex meshes.
1421         """
1422
1423         global progress
1424         # The sorting requires circa n*log(n) steps
1425         n = len(mesh.faces)
1426         progress.setActivity("HSR: Painter", n*log(n))
1427         
1428
1429         by_furthest_z = (lambda f1, f2: progress.update() and
1430                 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2]))
1431                 )
1432
1433         # FIXME: using NMesh to sort faces. We should avoid that!
1434         nmesh = NMesh.GetRaw(mesh.name)
1435
1436         # remember that _higher_ z values mean further points
1437         nmesh.faces.sort(by_furthest_z)
1438         nmesh.faces.reverse()
1439
1440         nmesh.update()
1441
1442     def __topologicalDepthSort(self, mesh):
1443         """Occlusion based on topological occlusion.
1444         
1445         Build the occlusion graph of the mesh,
1446         and then do topological sort on that graph
1447         """
1448         return
1449
1450     def __newellDepthSort(self, mesh):
1451         """Newell's depth sorting.
1452
1453         """
1454         by_furthest_z = (lambda f1, f2:
1455                 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2]))
1456                 )
1457
1458         def Distance(point, face):
1459             """ Calculate the distance between a point and a face.
1460
1461             An alternative but more expensive method can be:
1462
1463                 ip = Intersect(Vector(face[0]), Vector(face[1]), Vector(face[2]),
1464                         Vector(face.no), Vector(point), 0)
1465
1466                 d = Vector(ip - point).length
1467             """
1468
1469             plNormal = Vector(face.no)
1470             plVert0 = Vector(face[0])
1471
1472             #d = abs( (point * plNormal ) - (plVert0 * plNormal) )
1473             d = (point * plNormal ) - (plVert0 * plNormal)
1474             debug("d: "+ str(d) + "\n")
1475
1476             return d
1477
1478
1479         # FIXME: using NMesh to sort faces. We should avoid that!
1480         nmesh = NMesh.GetRaw(mesh.name)
1481
1482         # remember that _higher_ z values mean further points
1483         nmesh.faces.sort(by_furthest_z)
1484         nmesh.faces.reverse()
1485
1486         
1487         # Begin depth sort tests
1488
1489         # use the smooth flag to set marked faces
1490         for f in nmesh.faces:
1491             f.smooth = 0
1492
1493         facelist = nmesh.faces[:]
1494         maplist = []
1495
1496         EPS = 10e-7
1497
1498         global progress
1499         progress.setActivity("HSR: Newell", len(facelist))
1500
1501         while len(facelist):
1502             P = facelist[0]
1503
1504             pSign = 1
1505             if P.sel == 0:
1506                 pSign = -1
1507
1508             for Q in facelist[1:]:
1509
1510                 debug("P.smooth: " + str(P.smooth) + "\n")
1511                 debug("Q.smooth: " + str(Q.smooth) + "\n")
1512                 debug("\n")
1513
1514                 qSign = 1
1515                 if Q.sel == 0:
1516                     qSign = -1
1517
1518                 # We need to test only those Qs whose furthest vertex
1519                 # is closer to the observer than the closest vertex of P.
1520
1521                 zP = [v.co[2] for v in P.v]
1522                 zQ = [v.co[2] for v in Q.v]
1523                 ZOverlap = min(zP) < max(zQ)
1524
1525                 if not ZOverlap:
1526                     if not Q.smooth:
1527                         # We can safely print P
1528                         break
1529                     else:
1530                         continue
1531                 
1532                 # Test 1: X extent overlapping
1533                 xP = [v.co[0] for v in P.v]
1534                 xQ = [v.co[0] for v in Q.v]
1535                 notXOverlap = (max(xP) < min(xQ)) or (max(xQ) < min(xP))
1536
1537                 if notXOverlap:
1538                     continue
1539
1540                 # Test 2: Y extent Overlapping
1541                 yP = [v.co[1] for v in P.v]
1542                 yQ = [v.co[1] for v in Q.v]
1543                 notYOverlap = (max(yP) < min(yQ)) or (max(yQ) < min(yP))
1544
1545                 if notYOverlap:
1546                     continue
1547                 
1548
1549                 # Test 3: P vertices are all behind the plane of Q
1550                 n = 0
1551                 for Pi in P:
1552                     d = qSign * Distance(Vector(Pi), Q)
1553                     if d < EPS:
1554                         n += 1
1555                 pVerticesBehindPlaneQ = (n == len(P))
1556
1557                 if pVerticesBehindPlaneQ:
1558                     debug("\nTest 3\n")
1559                     debug("P BEHIND Q!\n")
1560                     continue
1561
1562
1563                 # Test 4: Q vertices in front of the plane of P
1564                 n = 0
1565                 for Qi in Q:
1566                     d = pSign * Distance(Vector(Qi), P)
1567                     if d >= EPS:
1568                         n += 1
1569                 qVerticesInFrontPlaneP = (n == len(Q))
1570
1571                 if qVerticesInFrontPlaneP:
1572                     debug("\nTest 4\n")
1573                     debug("Q IN FRONT OF P!\n")
1574                     continue
1575
1576                 # Test 5: Line Intersections... TODO
1577
1578
1579                 # We do not know if P obscures Q.
1580                 if Q.smooth == 1:
1581                     # Split P or Q, TODO
1582                     debug("Split here!!\n")
1583                     continue
1584
1585
1586                 # The question now is: Does Q obscure P?
1587
1588                 # Test 3bis: Q vertices are all behind the plane of P
1589                 n = 0
1590                 for Qi in Q:
1591                     d = pSign * Distance(Vector(Qi), P)
1592                     if d < EPS:
1593                         n += 1
1594                 qVerticesBehindPlaneP = (n == len(Q))
1595
1596
1597                 # Test 4bis: P vertices in front of the plane of Q
1598                 n = 0
1599                 for Pi in P:
1600                     d = qSign * Distance(Vector(Pi), Q)
1601                     if d >= EPS:
1602                         n += 1
1603                 pVerticesInFrontPlaneQ = (n == len(P))
1604
1605
1606                 """
1607                 import intersection
1608
1609                 if not qVerticesBehindPlaneP and not pVerticesInFrontPlaneQ:
1610                     # Split P or Q, TODO
1611                     print "Test 3bis or 4bis failed"
1612                     print "Split here!!2\n"
1613
1614                     newfaces = intersection.splitOn(nmesh, P, Q, 0)
1615                     facelist.remove(Q)
1616                     for nf in newfaces:
1617                         if nf:
1618                             nf.col = Q.col
1619                             facelist.append(nf)
1620
1621                     break
1622
1623                 # We do not know
1624                 if Q.smooth:
1625                     # split P or Q
1626                     print "Split here!!\n"
1627                     newfaces = intersection.splitOn(nmesh, P, Q, 0)
1628                     facelist.remove(Q)
1629                     for nf in newfaces:
1630                         if nf:
1631                             nf.col = Q.col
1632                             facelist.append(nf)
1633
1634                     break
1635                 """ 
1636
1637                 Q.smooth = 1
1638                 facelist.remove(Q)
1639                 facelist.insert(0, Q)
1640            
1641             # Write P!                     
1642             facelist.remove(P)
1643             maplist.append(P)
1644
1645             progress.update()
1646
1647         
1648         nmesh.faces = maplist
1649
1650         for f in nmesh.faces:
1651             f.sel = 1
1652         nmesh.update()
1653
1654     def _doHiddenSurfaceRemoval(self, mesh):
1655         """Do HSR for the given mesh.
1656         """
1657         if len(mesh.faces) == 0:
1658             return
1659
1660         if config.polygons['HSR'] == 'PAINTER':
1661             print "\n\nUsing the Painter algorithm for HSR.\n"
1662             self.__simpleDepthSort(mesh)
1663
1664         elif config.polygons['HSR'] == 'NEWELL':
1665             print "\n\nUsing the Newell's algorithm for HSR.\n"
1666             self.__newellDepthSort(mesh)
1667
1668
1669     def _doEdgesStyle(self, mesh, edgestyleSelect):
1670         """Process Mesh Edges accroding to a given selection style.
1671
1672         Examples of algorithms:
1673
1674         Contours:
1675             given an edge if its adjacent faces have the same normal (that is
1676             they are complanar), than deselect it.
1677
1678         Silhouettes:
1679             given an edge if one its adjacent faces is frontfacing and the
1680             other is backfacing, than select it, else deselect.
1681         """
1682
1683         Mesh.Mode(Mesh.SelectModes['EDGE'])
1684
1685         edge_cache = MeshUtils.buildEdgeFaceUsersCache(mesh)
1686
1687         for i,edge_faces in enumerate(edge_cache):
1688             mesh.edges[i].sel = 0
1689             if edgestyleSelect(edge_faces):
1690                 mesh.edges[i].sel = 1
1691
1692         """
1693         for e in mesh.edges:
1694
1695             e.sel = 0
1696             if edgestyleSelect(e, mesh):
1697                 e.sel = 1
1698         """
1699                 
1700
1701
1702 # ---------------------------------------------------------------------
1703 #
1704 ## GUI Class and Main Program
1705 #
1706 # ---------------------------------------------------------------------
1707
1708
1709 from Blender import BGL, Draw
1710 from Blender.BGL import *
1711
1712 class GUI:
1713     
1714     def _init():
1715
1716         # Output Format menu 
1717         output_format = config.output['FORMAT']
1718         default_value = outputWriters.keys().index(output_format)+1
1719         GUI.outFormatMenu = Draw.Create(default_value)
1720         GUI.evtOutFormatMenu = 0
1721
1722         # Animation toggle button
1723         GUI.animToggle = Draw.Create(config.output['ANIMATION'])
1724         GUI.evtAnimToggle = 1
1725
1726         # Join Objects toggle button
1727         GUI.joinObjsToggle = Draw.Create(config.output['JOIN_OBJECTS'])
1728         GUI.evtJoinObjsToggle = 2
1729
1730         # Render filled polygons
1731         GUI.polygonsToggle = Draw.Create(config.polygons['SHOW'])
1732
1733         # Shading Style menu 
1734         shading_style = config.polygons['SHADING']
1735         default_value = shadingStyles.keys().index(shading_style)+1
1736         GUI.shadingStyleMenu = Draw.Create(default_value)
1737         GUI.evtShadingStyleMenu = 21
1738
1739         GUI.evtPolygonsToggle = 3
1740         # We hide the config.polygons['EXPANSION_TRICK'], for now
1741
1742         # Render polygon edges
1743         GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
1744         GUI.evtShowEdgesToggle = 4
1745
1746         # Render hidden edges
1747         GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
1748         GUI.evtShowHiddenEdgesToggle = 5
1749
1750         # Edge Style menu 
1751         edge_style = config.edges['STYLE']
1752         default_value = edgeStyles.keys().index(edge_style)+1
1753         GUI.edgeStyleMenu = Draw.Create(default_value)
1754         GUI.evtEdgeStyleMenu = 6
1755
1756         # Edge Width slider
1757         GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
1758         GUI.evtEdgeWidthSlider = 7
1759
1760         # Edge Color Picker
1761         c = config.edges['COLOR']
1762         GUI.edgeColorPicker = Draw.Create(c[0]/255.0, c[1]/255.0, c[2]/255.0)
1763         GUI.evtEdgeColorPicker = 71
1764
1765         # Render Button
1766         GUI.evtRenderButton = 8
1767
1768         # Exit Button
1769         GUI.evtExitButton = 9
1770
1771     def draw():
1772
1773         # initialize static members
1774         GUI._init()
1775
1776         glClear(GL_COLOR_BUFFER_BIT)
1777         glColor3f(0.0, 0.0, 0.0)
1778         glRasterPos2i(10, 350)
1779         Draw.Text("VRM: Vector Rendering Method script. Version %s." %
1780                 __version__)
1781         glRasterPos2i(10, 335)
1782         Draw.Text("Press Q or ESC to quit.")
1783
1784         # Build the output format menu
1785         glRasterPos2i(10, 310)
1786         Draw.Text("Select the output Format:")
1787         outMenuStruct = "Output Format %t"
1788         for t in outputWriters.keys():
1789            outMenuStruct = outMenuStruct + "|%s" % t
1790         GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
1791                 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
1792
1793         # Animation toggle
1794         GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
1795                 10, 260, 160, 18, GUI.animToggle.val,
1796                 "Toggle rendering of animations")
1797
1798         # Join Objects toggle
1799         GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
1800                 10, 235, 160, 18, GUI.joinObjsToggle.val,
1801                 "Join objects in the rendered file")
1802
1803         # Render Button
1804         Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
1805                 "Start Rendering")
1806         Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
1807
1808         # Rendering Styles
1809         glRasterPos2i(200, 310)
1810         Draw.Text("Rendering Style:")
1811
1812         # Render Polygons
1813         GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
1814                 200, 285, 160, 18, GUI.polygonsToggle.val,
1815                 "Render filled polygons")
1816
1817         if GUI.polygonsToggle.val == 1:
1818
1819             # Polygon Shading Style
1820             shadingStyleMenuStruct = "Shading Style %t"
1821             for t in shadingStyles.keys():
1822                 shadingStyleMenuStruct = shadingStyleMenuStruct + "|%s" % t.lower()
1823             GUI.shadingStyleMenu = Draw.Menu(shadingStyleMenuStruct, GUI.evtShadingStyleMenu,
1824                     200, 260, 160, 18, GUI.shadingStyleMenu.val,
1825                     "Choose the shading style")
1826
1827
1828         # Render Edges
1829         GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
1830                 200, 235, 160, 18, GUI.showEdgesToggle.val,
1831                 "Render polygon edges")
1832
1833         if GUI.showEdgesToggle.val == 1:
1834             
1835             # Edge Style
1836             edgeStyleMenuStruct = "Edge Style %t"
1837             for t in edgeStyles.keys():
1838                 edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t.lower()
1839             GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
1840                     200, 210, 160, 18, GUI.edgeStyleMenu.val,
1841                     "Choose the edge style")
1842
1843             # Edge size
1844             GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
1845                     200, 185, 140, 18, GUI.edgeWidthSlider.val,
1846                     0.0, 10.0, 0, "Change Edge Width")
1847
1848             # Edge Color
1849             GUI.edgeColorPicker = Draw.ColorPicker(GUI.evtEdgeColorPicker,
1850                     342, 185, 18, 18, GUI.edgeColorPicker.val, "Choose Edge Color")
1851
1852             # Show Hidden Edges
1853             GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
1854                     GUI.evtShowHiddenEdgesToggle,
1855                     200, 160, 160, 18, GUI.showHiddenEdgesToggle.val,
1856                     "Render hidden edges as dashed lines")
1857
1858         glRasterPos2i(10, 160)
1859         Draw.Text("%s (c) 2006" % __author__)
1860
1861     def event(evt, val):
1862
1863         if evt == Draw.ESCKEY or evt == Draw.QKEY:
1864             Draw.Exit()
1865         else:
1866             return
1867
1868         Draw.Redraw(1)
1869
1870     def button_event(evt):
1871
1872         if evt == GUI.evtExitButton:
1873             Draw.Exit()
1874
1875         elif evt == GUI.evtOutFormatMenu:
1876             i = GUI.outFormatMenu.val - 1
1877             config.output['FORMAT']= outputWriters.keys()[i]
1878
1879         elif evt == GUI.evtAnimToggle:
1880             config.output['ANIMATION'] = bool(GUI.animToggle.val)
1881
1882         elif evt == GUI.evtJoinObjsToggle:
1883             config.output['JOIN_OBJECTS'] = bool(GUI.joinObjsToggle.val)
1884
1885         elif evt == GUI.evtPolygonsToggle:
1886             config.polygons['SHOW'] = bool(GUI.polygonsToggle.val)
1887
1888         elif evt == GUI.evtShadingStyleMenu:
1889             i = GUI.shadingStyleMenu.val - 1
1890             config.polygons['SHADING'] = shadingStyles.keys()[i]
1891
1892         elif evt == GUI.evtShowEdgesToggle:
1893             config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
1894
1895         elif evt == GUI.evtShowHiddenEdgesToggle:
1896             config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
1897
1898         elif evt == GUI.evtEdgeStyleMenu:
1899             i = GUI.edgeStyleMenu.val - 1
1900             config.edges['STYLE'] = edgeStyles.keys()[i]
1901
1902         elif evt == GUI.evtEdgeWidthSlider:
1903             config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
1904
1905         elif evt == GUI.evtEdgeColorPicker:
1906             config.edges['COLOR'] = [int(c*255.0) for c in GUI.edgeColorPicker.val]
1907
1908         elif evt == GUI.evtRenderButton:
1909             label = "Save %s" % config.output['FORMAT']
1910             # Show the File Selector
1911             global outputfile
1912             Blender.Window.FileSelector(vectorize, label, outputfile)
1913
1914         else:
1915             print "Event: %d not handled!" % evt
1916
1917         if evt:
1918             Draw.Redraw(1)
1919             #GUI.conf_debug()
1920
1921     def conf_debug():
1922         from pprint import pprint
1923         print "\nConfig"
1924         pprint(config.output)
1925         pprint(config.polygons)
1926         pprint(config.edges)
1927
1928     _init = staticmethod(_init)
1929     draw = staticmethod(draw)
1930     event = staticmethod(event)
1931     button_event = staticmethod(button_event)
1932     conf_debug = staticmethod(conf_debug)
1933
1934 # A wrapper function for the vectorizing process
1935 def vectorize(filename):
1936     """The vectorizing process is as follows:
1937      
1938      - Instanciate the writer and the renderer
1939      - Render!
1940      """
1941
1942     if filename == "":
1943         print "\nERROR: invalid file name!"
1944         return
1945
1946     from Blender import Window
1947     editmode = Window.EditMode()
1948     if editmode: Window.EditMode(0)
1949
1950     actualWriter = outputWriters[config.output['FORMAT']]
1951     writer = actualWriter(filename)
1952     
1953     renderer = Renderer()
1954     renderer.doRendering(writer, config.output['ANIMATION'])
1955
1956     if editmode: Window.EditMode(1) 
1957
1958 # We use a global progress Indicator Object
1959 progress = None
1960
1961 # Here the main
1962 if __name__ == "__main__":
1963
1964     global progress
1965
1966     outputfile = ""
1967     basename = Blender.sys.basename(Blender.Get('filename'))
1968     if basename != "":
1969         outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
1970
1971     if Blender.mode == 'background':
1972         progress = ConsoleProgressIndicator()
1973         vectorize(outputfile)
1974     else:
1975         progress = GraphicalProgressIndicator()
1976         Draw.Register(GUI.draw, GUI.event, GUI.button_event)