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