c694b776f4c7cecb20c2609126e4de3c2a4a03e2
[vrm.git] / vrm.py
1 #!BPY
2 """
3 Name: 'VRM'
4 Blender: 241
5 Group: 'Export'
6 Tooltip: 'Vector Rendering Method Export Script 0.3'
7 """
8
9 # ---------------------------------------------------------------------
10 #    Copyright (c) 2006 Antonio Ospite
11 #
12 #    This program is free software; you can redistribute it and/or modify
13 #    it under the terms of the GNU General Public License as published by
14 #    the Free Software Foundation; either version 2 of the License, or
15 #    (at your option) any later version.
16 #
17 #    This program is distributed in the hope that it will be useful,
18 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
19 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 #    GNU General Public License for more details.
21 #
22 #    You should have received a copy of the GNU General Public License
23 #    along with this program; if not, write to the Free Software
24 #    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
25 #
26 # ---------------------------------------------------------------------
27 #
28 #    NOTE: I do not know who is the original author of 'vrm'.
29 #    The present code is almost entirely rewritten from scratch,
30 #    but if I have to give credits to anyone, please let me know,
31 #    so I can update the copyright.
32 #
33 # ---------------------------------------------------------------------
34 #
35 # Additional credits:
36 #   Thanks to Emilio Aguirre for S2flender from which I took inspirations :)
37 #   Thanks to Anthony C. D'Agostino for the original backface.py script   
38 #
39 # ---------------------------------------------------------------------
40
41 import Blender
42 from Blender import Scene, Object, Mesh, NMesh, Lamp, Camera
43 from Blender.Mathutils import *
44 from math import *
45
46
47 # ---------------------------------------------------------------------
48 #
49 ## Projections classes
50 #
51 # ---------------------------------------------------------------------
52
53 class Projector:
54     """Calculate the projection of an object given the camera.
55     
56     A projector is useful to so some per-object transformation to obtain the
57     projection of an object given the camera.
58     
59     The main method is #doProjection# see the method description for the
60     parameter list.
61     """
62
63     def __init__(self, cameraObj, canvasRatio):
64         """Calculate the projection matrix.
65
66         The projection matrix depends, in this case, on the camera settings,
67         and also on object transformation matrix.
68         """
69
70         camera = cameraObj.getData()
71
72         aspect = float(canvasRatio[0])/float(canvasRatio[1])
73         near = camera.clipStart
74         far = camera.clipEnd
75
76         fovy = atan(0.5/aspect/(camera.lens/32))
77         fovy = fovy * 360/pi
78         
79         # What projection do we want?
80         if camera.type:
81             m2 = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale) 
82         else:
83             m2 = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
84         
85
86         # View transformation
87         cam = Matrix(cameraObj.getInverseMatrix())
88         cam.transpose() 
89         
90         # FIXME: remove the commented part, we used to pass object in local
91         # coordinates, but this is not very clean, we should apply modelview
92         # tranformations _before_ (at some other level).
93         #m1 = Matrix(obMesh.getMatrix())
94         #m1.transpose()
95         
96         #mP = cam * m1
97         mP = cam
98         mP = m2  * mP
99
100         self.projectionMatrix = mP
101
102     ##
103     # Public methods
104     #
105
106     def doProjection(self, v):
107         """Project the point on the view plane.
108
109         Given a vertex calculate the projection using the current projection
110         matrix.
111         """
112         
113         # Note that we need the vertex expressed using homogeneous coordinates
114         p = self.projectionMatrix * Vector(v).resize4D()
115
116         if p[3]>0:
117             p[0] = p[0]/p[3]
118             p[1] = p[1]/p[3]
119
120         return p
121
122     ##
123     # Private methods
124     #
125     
126     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
127         """Return a perspective projection matrix."""
128         
129         top = near * tan(fovy * pi / 360.0)
130         bottom = -top
131         left = bottom*aspect
132         right= top*aspect
133         x = (2.0 * near) / (right-left)
134         y = (2.0 * near) / (top-bottom)
135         a = (right+left) / (right-left)
136         b = (top+bottom) / (top - bottom)
137         c = - ((far+near) / (far-near))
138         d = - ((2*far*near)/(far-near))
139         
140         m = Matrix(
141                 [x,   0.0,    a,    0.0],
142                 [0.0,   y,    b,    0.0],
143                 [0.0, 0.0,    c,      d],
144                 [0.0, 0.0, -1.0,    0.0])
145
146         return m
147
148     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
149         """Return an orthogonal projection matrix."""
150         
151         top = near * tan(fovy * pi / 360.0) * (scale * 10)
152         bottom = -top 
153         left = bottom * aspect
154         right= top * aspect
155         rl = right-left
156         tb = top-bottom
157         fn = near-far 
158         tx = -((right+left)/rl)
159         ty = -((top+bottom)/tb)
160         tz = ((far+near)/fn)
161
162         m = Matrix(
163                 [2.0/rl, 0.0,    0.0,     tx],
164                 [0.0,    2.0/tb, 0.0,     ty],
165                 [0.0,    0.0,    2.0/fn,  tz],
166                 [0.0,    0.0,    0.0,    1.0])
167         
168         return m
169
170
171 # ---------------------------------------------------------------------
172 #
173 ## Object representation class
174 #
175 # ---------------------------------------------------------------------
176
177 # TODO: a class to represent the needed properties of a 2D vector image
178 # Just use a NMesh structure?
179
180
181 # ---------------------------------------------------------------------
182 #
183 ## Vector Drawing Classes
184 #
185 # ---------------------------------------------------------------------
186
187 ## A generic Writer
188
189 class VectorWriter:
190     """
191     A class for printing output in a vectorial format.
192
193     Given a 2D representation of the 3D scene the class is responsible to
194     write it is a vector format.
195
196     Every subclasses of VectorWriter must have at last the following public
197     methods:
198         - printCanvas(mesh) --- where mesh is as specified before.
199     """
200     
201     def __init__(self, fileName):
202         """Open the file named #fileName# and set the canvas size."""
203         
204         self.file = open(fileName, "w")
205         print "Outputting to: ", fileName
206
207
208         context = Scene.GetCurrent().getRenderingContext()
209         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
210     
211
212     ##
213     # Public Methods
214     #
215     
216     def printCanvas(mesh):
217         return
218         
219     ##
220     # Private Methods
221     #
222     
223     def _printHeader():
224         return
225
226     def _printFooter():
227         return
228
229
230 ## SVG Writer
231
232 class SVGVectorWriter(VectorWriter):
233     """A concrete class for writing SVG output.
234
235     The class does not support animations, yet.
236     Sorry.
237     """
238
239     def __init__(self, file):
240         """Simply call the parent Contructor."""
241         VectorWriter.__init__(self, file)
242
243
244     ##
245     # Public Methods
246     #
247
248     def open(self):
249         self._printHeader()
250
251     def close(self):
252         self._printFooter()
253
254         
255     
256     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
257         """Convert the scene representation to SVG."""
258
259         Objects = scene.getChildren()
260         for obj in Objects:
261
262             if(obj.getType() != 'Mesh'):
263                 continue
264             #
265
266             self.file.write("<g>\n")
267
268             
269             if doPrintPolygons:
270                 for face in obj.getData().faces:
271                     self._printPolygon(face)
272
273             if doPrintEdges:
274                 self._printEdges(obj.getData(), showHiddenEdges)
275             
276             self.file.write("</g>\n")
277         
278     
279     ##  
280     # Private Methods
281     #
282     
283     def _printHeader(self):
284         """Print SVG header."""
285
286         self.file.write("<?xml version=\"1.0\"?>\n")
287         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n")
288         self.file.write("\t\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
289         self.file.write("<svg version=\"1.1\"\n")
290         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
291         self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
292                 self.canvasSize)
293
294     def _printFooter(self):
295         """Print the SVG footer."""
296
297         self.file.write("\n</svg>\n")
298         self.file.close()
299
300     def _printEdges(self, mesh, showHiddenEdges=False):
301         """Print the wireframe using mesh edges... is this the correct way?
302         """
303
304         stroke_width=0.5
305         stroke_col = [0, 0, 0]
306         
307         self.file.write("<g>\n")
308
309         for e in mesh.edges:
310             
311             hidden_stroke_style = ""
312             
313             # And edge is selected if both vertives are selected
314             if e.v1.sel == 0 or e.v2.sel == 0:
315                 if showHiddenEdges == False:
316                     continue
317                 else:
318                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
319
320             p1 = self._calcCanvasCoord(e.v1)
321             p2 = self._calcCanvasCoord(e.v2)
322             
323             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
324                     % ( p1[0], p1[1], p2[0], p2[1] ) )
325             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
326             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
327             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
328             self.file.write(hidden_stroke_style)
329             self.file.write("\"/>\n")
330
331         self.file.write("</g>\n")
332             
333         
334
335     def _printPolygon(self, face):
336         """Print our primitive, finally.
337         """
338
339         wireframe = False
340         
341         stroke_width=0.5
342         
343         self.file.write("<polygon points=\"")
344
345         for v in face:
346             p = self._calcCanvasCoord(v)
347             self.file.write("%g,%g " % (p[0], p[1]))
348         
349         self.file.seek(-1,1) # get rid of the last space
350         self.file.write("\"\n")
351         
352         #take as face color the first vertex color
353         if face.col:
354             fcol = face.col[0]
355             color = [fcol.r, fcol.g, fcol.b]
356         else:
357             color = [ 255, 255, 255]
358
359         stroke_col = [0, 0, 0]
360         if not wireframe:
361             stroke_col = color
362
363         self.file.write("\tstyle=\"fill:rgb("+str(color[0])+","+str(color[1])+","+str(color[2])+");")
364         self.file.write(" stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
365         self.file.write(" stroke-width:"+str(stroke_width)+";\n")
366         self.file.write(" stroke-linecap:round;stroke-linejoin:round")
367         self.file.write("\"/>\n")
368
369     def _calcCanvasCoord(self, v):
370
371         pt = Vector([0, 0, 0])
372         
373         mW = self.canvasSize[0]/2
374         mH = self.canvasSize[1]/2
375
376         # rescale to canvas size
377         pt[0] = round(v[0]*mW)+mW
378         pt[1] = round(v[1]*mH)+mH
379          
380         # For now we want (0,0) in the top-left corner of the canvas
381         # Mirror and translate along y
382         pt[1] *= -1
383         pt[1] += self.canvasSize[1]
384         
385         return pt
386
387
388 # ---------------------------------------------------------------------
389 #
390 ## Rendering Classes
391 #
392 # ---------------------------------------------------------------------
393
394 class Renderer:
395     """Render a scene viewed from a given camera.
396     
397     This class is responsible of the rendering process, hence transformation
398     and projection of the ojects in the scene are invoked by the renderer.
399
400     The user can optionally provide a specific camera for the rendering, see
401     the #doRendering# method for more informations.
402     """
403
404     def __init__(self):
405         """Make the rendering process only for the current scene by default.
406         """
407
408         # Render the current Scene set as a READ-ONLY property
409         self._SCENE = Scene.GetCurrent()
410         
411         # Use the aspect ratio of the scene rendering context
412         context = self._SCENE.getRenderingContext()
413         self.canvasRatio = (context.aspectRatioX(), context.aspectRatioY())
414
415         # Render from the currently active camera 
416         self.camera = self._SCENE.getCurrentCamera()
417
418
419     ##
420     # Public Methods
421     #
422
423     def doRendering(self, outputWriter, animation=0):
424         """Render picture or animation and write it out.
425         
426         The parameters are:
427             - a Vector writer object than will be used to output the result.
428             - a flag to tell if we want to render an animation or the only
429               current frame.
430         """
431         
432         context = self._SCENE.getRenderingContext()
433         currentFrame = context.currentFrame()
434
435         # Handle the animation case
436         if animation == 0:
437             startFrame = currentFrame
438             endFrame = startFrame
439         else:
440             startFrame = context.startFrame()
441             endFrame = context.endFrame()
442         
443         # Do the rendering process frame by frame
444         print "Start Rendering!"
445         for f in range(startFrame, endFrame+1):
446             context.currentFrame(f)
447             renderedScene = self.doRenderScene(self._SCENE)
448             outputWriter.printCanvas(renderedScene,
449                     doPrintPolygons=False, doPrintEdges=True, showHiddenEdges=True)
450             
451             # clear the rendered scene
452             self._SCENE.makeCurrent()
453             Scene.unlink(renderedScene)
454             del renderedScene
455
456         print "Done!"
457         context.currentFrame(currentFrame)
458
459
460
461     def doRenderScene(self, inputScene):
462         """Control the rendering process.
463         
464         Here we control the entire rendering process invoking the operation
465         needed to transform and project the 3D scene in two dimensions.
466         """
467         
468         # Use some temporary workspace, a full copy of the scene
469         workScene = inputScene.copy(2)
470
471         # Get a projector for this scene.
472         # NOTE: the projector wants object in world coordinates,
473         # so we should apply modelview transformations _before_
474         # projection transformations
475         proj = Projector(self.camera, self.canvasRatio)
476             
477         # global processing of the scene
478         self._doDepthSorting(workScene)
479         
480         # Per object activities
481         Objects = workScene.getChildren()
482         
483         for obj in Objects:
484             
485             if (obj.getType() != 'Mesh'):
486                 print "Type:", obj.getType(), "\tSorry, only mesh Object supported!"
487                 continue
488             #
489
490             self._doModelViewTransformations(obj)
491
492             self._doBackFaceCulling(obj)
493             
494             self._doColorAndLighting(obj)
495
496             # 'style' can be a function that determine
497             # if an edge should be showed?
498             self._doEdgesStyle(obj, style=None)
499            
500             self._doProjection(obj, proj)
501
502         return workScene
503
504
505     def oldRenderScene(scene):
506         
507         # Per object activities
508         Objects = workScene.getChildren()
509         
510         for obj in Objects:
511             
512             if (obj.getType() != 'Mesh'):
513                 print "Type:", obj.getType(), "\tSorry, only mesh Object supported!"
514                 continue
515             
516             # Get a projector for this object
517             proj = Projector(self.camera, obj, self.canvasSize)
518
519             # Let's store the transformed data
520             transformed_mesh = NMesh.New("flat"+obj.name)
521             transformed_mesh.hasVertexColours(1)
522
523             # process Edges
524             self._doProcessEdges(obj)
525             
526             for v in obj.getData().verts:
527                 transformed_mesh.verts.append(v)
528             transformed_mesh.edges = self._processEdges(obj.getData().edges)
529             #print transformed_mesh.edges
530
531             
532             # Store the materials
533             materials = obj.getData().getMaterials()
534
535             meshfaces = obj.getData().faces
536
537             for face in meshfaces:
538
539                 # if the face is visible flatten it on the "picture plane"
540                 if self._isFaceVisible(face, obj, cameraObj):
541                     
542                     # Store transformed face
543                     newface = NMesh.Face()
544
545                     for vert in face:
546
547                         p = proj.doProjection(vert.co)
548
549                         tmp_vert = NMesh.Vert(p[0], p[1], p[2])
550
551                         # Add the vert to the mesh
552                         transformed_mesh.verts.append(tmp_vert)
553                         
554                         newface.v.append(tmp_vert)
555                         
556                     
557                     # Per-face color calculation
558                     # code taken mostly from the original vrm script
559                     # TODO: understand the code and rewrite it clearly
560                     ambient = -150
561                     
562                     fakelight = Object.Get("Lamp").loc
563                     if fakelight == None:
564                         fakelight = [1.0, 1.0, -0.3]
565
566                     norm = Vector(face.no)
567                     vektori = (norm[0]*fakelight[0]+norm[1]*fakelight[1]+norm[2]*fakelight[2])
568                     vduzine = fabs(sqrt(pow(norm[0],2)+pow(norm[1],2)+pow(norm[2],2))*sqrt(pow(fakelight[0],2)+pow(fakelight[1],2)+pow(fakelight[2],2)))
569                     intensity = floor(ambient + 200*acos(vektori/vduzine))/200
570                     if intensity < 0:
571                         intensity = 0
572
573                     if materials:
574                         tmp_col = materials[face.mat].getRGBCol()
575                     else:
576                         tmp_col = [0.5, 0.5, 0.5]
577                         
578                     tmp_col = [ (c>intensity) and int(round((c-intensity)*10)*25.5) for c in tmp_col ]
579
580                     vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2])
581                     newface.col = [vcol, vcol, vcol, 255]
582                     
583                     transformed_mesh.addFace(newface)
584
585             # at the end of the loop on obj
586             
587             transformed_obj = Object.New(obj.getType(), "flat"+obj.name)
588             transformed_obj.link(transformed_mesh)
589             transformed_obj.loc = obj.loc
590             newscene.link(transformed_obj)
591
592         
593         return workScene
594
595
596     ##
597     # Private Methods
598     #
599
600     # Faces methods
601
602     def _isFaceVisible(self, face, obj, camObj):
603         """Determine if a face of an object is visible from a given camera.
604         
605         The normals need to be transformed, but note that we should apply only the
606         rotation part of the tranformation matrix, since the normals are
607         normalized and they can be intended as starting from the origin.
608
609         The view vector is calculated from the camera location and one of the
610         vertices of the face (expressed in World coordinates, after applying
611         modelview transformations).
612
613         After those transformations we determine if a face is visible by computing
614         the angle between the face normal and the view vector, this angle
615         corresponds somehow to the dot product between the two. If the product
616         results <= 0 then the angle between the two vectors is less that 90
617         degrees and then the face is visible.
618
619         There is no need to normalize those vectors since we are only interested in
620         the sign of the cross product and not in the product value.
621         """
622
623         # The transformation matrix of the object
624         mObj = Matrix(obj.getMatrix())
625         mObj.transpose()
626
627         # The normal after applying the current object rotation
628         #normal = mObj.rotationPart() * Vector(face.no)
629         normal = Vector(face.no)
630
631         # View vector in orthographics projections can be considered simply s the
632         # camera position
633         #view_vect = Vector(camObj.loc)
634
635         # View vector as in perspective projections
636         # it is the dofference between the camera position and
637         # one point of the face, we choose the first point,
638         # but maybe a better choice may be the farthest point from the camera.
639         point = Vector(face[0].co)
640         #point = mObj * point.resize4D()
641         #point.resize3D()
642         view_vect = Vector(camObj.loc) - point
643         
644
645         # if d <= 0 the face is visible from the camera
646         d = view_vect * normal
647         
648         if d <= 0:
649             return False
650         else:
651             return True
652
653
654     # Scene methods
655
656     def _doClipping():
657         return
658
659     def _doDepthSorting(self, scene):
660
661         cameraObj = self.camera
662         Objects = scene.getChildren()
663
664         Objects.sort(lambda obj1, obj2: 
665                 cmp(Vector(Vector(cameraObj.loc) - Vector(obj1.loc)).length,
666                     Vector(Vector(cameraObj.loc) - Vector(obj2.loc)).length
667                     )
668                 )
669         
670         # hackish sorting of faces according to the max z value of a vertex
671         for o in Objects:
672
673             if (o.getType() != 'Mesh'):
674                 continue
675             #
676
677             mesh = o.data
678             mesh.faces.sort(
679                 lambda f1, f2:
680                     # Sort faces according to the min z coordinate in a face
681                     #cmp(min([v[2] for v in f1]), min([v[2] for v in f2])))
682
683                     # Sort faces according to the max z coordinate in a face
684                     cmp(max([v[2] for v in f1]), max([v[2] for v in f2])))
685                     
686                     # Sort faces according to the avg z coordinate in a face
687                     #cmp(sum([v[2] for v in f1])/len(f1), sum([v[2] for v in f2])/len(f2)))
688             mesh.faces.reverse()
689             mesh.update()
690             
691         # update the scene
692         # FIXME: check if it is correct
693         scene.update()
694         #for o in scene.getChildren():
695         #     scene.unlink(o)
696         #for o in Objects:
697         #   scene.link(o)
698
699     # Per object methods
700
701     def _doModelViewTransformations(self, object):
702         if(object.getType() != 'Mesh'):
703             return
704         
705         matMV = object.matrix
706         mesh = object.data
707         mesh.transform(matMV, True)
708         mesh.update()
709
710
711     def _doBackFaceCulling(self, object):
712         if(object.getType() != 'Mesh'):
713             return
714         
715         print "doing Backface Culling"
716         mesh = object.data
717         
718         # Select all vertices, so edges without faces can be displayed
719         for v in mesh.verts:
720             v.sel = 1
721         
722         Mesh.Mode(Mesh.SelectModes['FACE'])
723         # Loop on faces
724         for f in mesh.faces:
725             f.sel = 0
726             if self._isFaceVisible(f, object, self.camera):
727                 f.sel = 1
728
729         for f in mesh.faces:
730             if not f.sel:
731                 for v in f:
732                     v.sel = 0
733
734         for f in mesh.faces:
735             if f.sel:
736                 for v in f:
737                     v.sel = 1
738
739         mesh.update()
740
741         
742
743         #Mesh.Mode(Mesh.SelectModes['VERTEX'])
744
745     def _doColorAndLighting(self, object):
746         return
747
748     def _doEdgesStyle(self, object, style):
749         """Process Mesh Edges. (For now copy the edge data, in next version it
750         can be a place where recognize silouhettes and/or contours).
751
752         input: an edge list
753         return: a processed edge list
754         """
755         return
756
757     def _doProjection(self, object, projector):
758
759         if(object.getType() != 'Mesh'):
760             return
761         
762         mesh = object.data
763         for v in mesh.verts:
764             p = projector.doProjection(v.co)
765             v[0] = p[0]
766             v[1] = p[1]
767             v[2] = p[2]
768         mesh.update()
769
770
771
772 # ---------------------------------------------------------------------
773 #
774 ## Main Program
775 #
776 # ---------------------------------------------------------------------
777
778
779 # FIXME: really hackish code, just to test if the other parts work
780     
781 def vectorize(filename):
782     """The vectorizing process is as follows:
783      
784      - Open the writer
785      - Render the scene
786      - Close the writer
787      
788      If you want to render an animation the second pass should be
789      repeated for any frame, and the frame number should be passed to the
790      renderer.
791      """
792     writer = SVGVectorWriter(filename)
793     
794     writer.open()
795     
796     renderer = Renderer()
797     renderer.doRendering(writer)
798
799     writer.close()
800
801
802 # Here the main
803 if __name__ == "__main__":
804     # with this trick we can run the script in batch mode
805     try:
806         Blender.Window.FileSelector (vectorize, 'Save SVG', "proba.svg")
807         Blender.Redraw()
808     except:
809         from Blender import Window
810         editmode = Window.EditMode()
811         if editmode: Window.EditMode(0)
812
813         vectorize("proba.svg")
814         if editmode: Window.EditMode(1) 
815
816
817