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