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