d85b085e842c1d830f89a05caadd14257c06d324
[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, 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, obMesh, canvasSize):
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         self.size = canvasSize
71
72         camera = cameraObj.getData()
73
74         aspect = float(canvasSize[0])/float(canvasSize[1])
75         near = camera.clipStart
76         far = camera.clipEnd
77
78         fovy = atan(0.5/aspect/(camera.lens/32))
79         fovy = fovy * 360/pi
80         
81         # What projection do we want?
82         if camera.type:
83             m2 = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale) 
84         else:
85             m2 = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
86         
87
88         # View transformation
89         cam = Matrix(cameraObj.getInverseMatrix())
90         cam.transpose() 
91
92         m1 = Matrix(obMesh.getMatrix())
93         m1.transpose()
94         
95         mP = cam * m1
96         mP = m2  * mP
97
98         self.projectionMatrix = mP
99
100     ##
101     # Public methods
102     #
103
104     def doProjection(self, v):
105         """Project the point on the view plane.
106
107         Given a vertex calculate the projection using the current projection
108         matrix.
109         """
110         
111         # Note that we need the vertex expressed using homogeneous coordinates
112         p = self.projectionMatrix * Vector([v[0], v[1], v[2], 1.0])
113         
114         mW = self.size[0]/2
115         mH = self.size[1]/2
116         
117         if p[3]<=0:
118             p[0] = round(p[0]*mW)+mW
119             p[1] = round(p[1]*mH)+mH
120         else:
121             p[0] = round((p[0]/p[3])*mW)+mW
122             p[1] = round((p[1]/p[3])*mH)+mH
123             
124         # For now we want (0,0) in the top-left corner of the canvas
125         # Mirror and translate along y
126         p[1] *= -1
127         p[1] += self.size[1]
128     
129         return p
130
131     ##
132     # Private methods
133     #
134     
135     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
136         """Return a perspective projection matrix."""
137         
138         top = near * tan(fovy * pi / 360.0)
139         bottom = -top
140         left = bottom*aspect
141         right= top*aspect
142         x = (2.0 * near) / (right-left)
143         y = (2.0 * near) / (top-bottom)
144         a = (right+left) / (right-left)
145         b = (top+bottom) / (top - bottom)
146         c = - ((far+near) / (far-near))
147         d = - ((2*far*near)/(far-near))
148         
149         m = Matrix(
150                 [x,   0.0,    a,    0.0],
151                 [0.0,   y,    b,    0.0],
152                 [0.0, 0.0,    c,      d],
153                 [0.0, 0.0, -1.0,    0.0])
154
155         return m
156
157     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
158         """Return an orthogonal projection matrix."""
159         
160         top = near * tan(fovy * pi / 360.0) * (scale * 10)
161         bottom = -top 
162         left = bottom * aspect
163         right= top * aspect
164         rl = right-left
165         tb = top-bottom
166         fn = near-far 
167         tx = -((right+left)/rl)
168         ty = -((top+bottom)/tb)
169         tz = ((far+near)/fn)
170
171         m = Matrix(
172                 [2.0/rl, 0.0,    0.0,     tx],
173                 [0.0,    2.0/tb, 0.0,     ty],
174                 [0.0,    0.0,    2.0/fn,  tz],
175                 [0.0,    0.0,    0.0,    1.0])
176         
177         return m
178
179
180 # ---------------------------------------------------------------------
181 #
182 ## Object representation class
183 #
184 # ---------------------------------------------------------------------
185
186 # TODO: a class to represent the needed properties of a 2D vector image
187 # Just use a NMesh structure?
188
189
190 # ---------------------------------------------------------------------
191 #
192 ## Vector Drawing Classes
193 #
194 # ---------------------------------------------------------------------
195
196 ## A generic Writer
197
198 class VectorWriter:
199     """
200     A class for printing output in a vectorial format.
201
202     Given a 2D representation of the 3D scene the class is responsible to
203     write it is a vector format.
204
205     Every subclasses of VectorWriter must have at last the following public
206     methods:
207         - printCanvas(mesh) --- where mesh is as specified before.
208     """
209     
210     def __init__(self, fileName, canvasSize):
211         """Open the file named #fileName# and set the canvas size."""
212         
213         self.file = open(fileName, "w")
214         print "Outputting to: ", fileName
215
216         self.canvasSize = canvasSize
217     
218
219     ##
220     # Public Methods
221     #
222     
223     def printCanvas(mesh):
224         return
225         
226     ##
227     # Private Methods
228     #
229     
230     def _printHeader():
231         return
232
233     def _printFooter():
234         return
235
236
237 ## SVG Writer
238
239 class SVGVectorWriter(VectorWriter):
240     """A concrete class for writing SVG output.
241
242     The class does not support animations, yet.
243     Sorry.
244     """
245
246     def __init__(self, file, canvasSize):
247         """Simply call the parent Contructor."""
248         VectorWriter.__init__(self, file, canvasSize)
249
250
251     ##
252     # Public Methods
253     #
254     
255     def printCanvas(self, scene):
256         """Convert the scene representation to SVG."""
257
258         self._printHeader()
259         
260         Objects = scene.getChildren()
261         for obj in Objects:
262             self.file.write("<g>\n")
263             
264             for face in obj.getData().faces:
265                 self._printPolygon(face)
266
267             self._printWireframe(obj.getData())
268             
269             self.file.write("</g>\n")
270         
271         self._printFooter()
272     
273     ##  
274     # Private Methods
275     #
276     
277     def _printHeader(self):
278         """Print SVG header."""
279
280         self.file.write("<?xml version=\"1.0\"?>\n")
281         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n")
282         self.file.write("\t\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
283         self.file.write("<svg version=\"1.1\"\n")
284         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
285         self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
286                 self.canvasSize)
287
288     def _printFooter(self):
289         """Print the SVG footer."""
290
291         self.file.write("\n</svg>\n")
292         self.file.close()
293
294     def _printWireframe(self, mesh):
295         """Print the wireframe using mesh edges... is this the correct way?
296         """
297
298         print mesh.edges
299         print
300         print mesh.verts
301         
302         stroke_width=0.5
303         stroke_col = [0, 0, 0]
304         
305         self.file.write("<g>\n")
306
307         for e in mesh.edges:
308             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
309                     % ( e.v1[0], e.v1[1], e.v2[0], e.v2[1] ) )
310             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
311             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
312             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
313             self.file.write("\"/>\n")
314
315         self.file.write("</g>\n")
316             
317         
318
319     def _printPolygon(self, face):
320         """Print our primitive, finally.
321         """
322         
323         wireframe = False
324         
325         stroke_width=0.5
326         
327         self.file.write("<polygon points=\"")
328
329         for v in face:
330             self.file.write("%g,%g " % (v[0], v[1]))
331         
332         self.file.seek(-1,1) # get rid of the last space
333         self.file.write("\"\n")
334         
335         #take as face color the first vertex color
336         fcol = face.col[0]
337         color = [fcol.r, fcol.g, fcol.b]
338
339         stroke_col = [0, 0, 0]
340         if not wireframe:
341             stroke_col = color
342
343         self.file.write("\tstyle=\"fill:rgb("+str(color[0])+","+str(color[1])+","+str(color[2])+");")
344         self.file.write(" stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
345         self.file.write(" stroke-width:"+str(stroke_width)+";\n")
346         self.file.write(" stroke-linecap:round;stroke-linejoin:round")
347         self.file.write("\"/>\n")
348
349
350 # ---------------------------------------------------------------------
351 #
352 ## Rendering Classes
353 #
354 # ---------------------------------------------------------------------
355
356 def RotatePoint(PX,PY,PZ,AngleX,AngleY,AngleZ):
357     
358     NewPoint = []
359     # Rotate X
360     NewY = (PY * cos(AngleX))-(PZ * sin(AngleX))
361     NewZ = (PZ * cos(AngleX))+(PY * sin(AngleX))
362     # Rotate Y
363     PZ = NewZ
364     PY = NewY
365     NewZ = (PZ * cos(AngleY))-(PX * sin(AngleY))
366     NewX = (PX * cos(AngleY))+(PZ * sin(AngleY))
367     PX = NewX
368     PZ = NewZ
369     # Rotate Z
370     NewX = (PX * cos(AngleZ))-(PY * sin(AngleZ))
371     NewY = (PY * cos(AngleZ))+(PX * sin(AngleZ))
372     NewPoint.append(NewX)
373     NewPoint.append(NewY)
374     NewPoint.append(NewZ)
375     return NewPoint
376
377 class Renderer:
378     """Render a scene viewed from a given camera.
379     
380     This class is responsible of the rendering process, hence transormation
381     and projection of the ojects in the scene are invoked by the renderer.
382
383     The user can optionally provide a specific camera for the rendering, see
384     the #doRendering# method for more informations.
385     """
386
387     def __init__(self):
388         """Set the canvas size to a defaulr value.
389         
390         The only instance attribute here is the canvas size, which can be
391         queryed to the renderer by other entities.
392         """
393         self.canvasSize = (0.0, 0.0)
394
395
396     ##
397     # Public Methods
398     #
399
400     def getCanvasSize(self):
401         """Return the current canvas size read from Blender rendering context"""
402         return self.canvasSize
403         
404     def doRendering(self, scene, cameraObj=None):
405         """Control the rendering process.
406         
407         Here we control the entire rendering process invoking the operation
408         needed to transforma project the 3D scene in two dimensions.
409
410         Parameters:
411         scene --- the Blender Scene to render
412         cameraObj --- the camera object to use for the viewing processing
413         """
414
415         if cameraObj == None:
416             cameraObj = scene.getCurrentCamera()
417         
418         context = scene.getRenderingContext()
419         self.canvasSize = (context.imageSizeX(), context.imageSizeY())
420         
421         Objects = scene.getChildren()
422         
423         # A structure to store the transformed scene
424         newscene = Scene.New("flat"+scene.name)
425         
426         for obj in Objects:
427             
428             if (obj.getType() != "Mesh"):
429                 print "Type:", obj.getType(), "\tSorry, only mesh Object supported!"
430                 continue
431
432             # Get a projector for this object
433             proj = Projector(cameraObj, obj, self.canvasSize)
434
435             # Let's store the transformed data
436             transformed_mesh = NMesh.New("flat"+obj.name)
437             transformed_mesh.hasVertexColours(1)
438
439             # process Edges
440             for v in obj.getData().verts:
441                 transformed_mesh.verts.append(v)
442             transformed_mesh.edges = self._processEdges(obj.getData().edges)
443             print transformed_mesh.edges
444
445             
446             # Store the materials
447             materials = obj.getData().getMaterials()
448
449             meshfaces = obj.getData().faces
450
451             for face in meshfaces:
452
453                 # if the face is visible flatten it on the "picture plane"
454                 if self._isFaceVisible_old(face, obj, cameraObj):
455                     
456                     # Store transformed face
457                     newface = NMesh.Face()
458
459                     for vert in face:
460
461                         p = proj.doProjection(vert.co)
462
463                         tmp_vert = NMesh.Vert(p[0], p[1], p[2])
464
465                         # Add the vert to the mesh
466                         transformed_mesh.verts.append(tmp_vert)
467                         
468                         newface.v.append(tmp_vert)
469                         
470                     
471                     # Per-face color calculation
472                     # code taken mostly from the original vrm script
473                     # TODO: understand the code and rewrite it clearly
474                     ambient = -150
475                     
476                     fakelight = Object.Get("Lamp").loc
477                     if fakelight == None:
478                         fakelight = [1.0, 1.0, -0.3]
479
480                     norm = Vector(face.no)
481                     vektori = (norm[0]*fakelight[0]+norm[1]*fakelight[1]+norm[2]*fakelight[2])
482                     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)))
483                     intensity = floor(ambient + 200*acos(vektori/vduzine))/200
484                     if intensity < 0:
485                         intensity = 0
486
487                     if materials:
488                         tmp_col = materials[face.mat].getRGBCol()
489                     else:
490                         tmp_col = [0.5, 0.5, 0.5]
491                         
492                     tmp_col = [ (c>intensity) and int(round((c-intensity)*10)*25.5) for c in tmp_col ]
493
494                     vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2])
495                     newface.col = [vcol, vcol, vcol, 255]
496                     
497                     transformed_mesh.addFace(newface)
498
499             # at the end of the loop on obj
500             
501             transformed_obj = Object.New(obj.getType(), "flat"+obj.name)
502             transformed_obj.link(transformed_mesh)
503             transformed_obj.loc = obj.loc
504             newscene.link(transformed_obj)
505
506         
507         return newscene
508
509
510     ##
511     # Private Methods
512     #
513
514     def _isFaceVisible_old(self, face, obj, cameraObj):
515         """Determine if the face is visible from the current camera.
516
517         The following code is taken basicly from the original vrm script.
518         """
519
520         camera = cameraObj
521
522         numvert = len(face)
523
524         # backface culling
525
526         # translate and rotate according to the object matrix
527         # and then translate according to the camera position
528         #m = obj.getMatrix()
529         #m.transpose()
530         
531         #a = m*Vector(face[0]) - Vector(cameraObj.loc)
532         #b = m*Vector(face[1]) - Vector(cameraObj.loc)
533         #c = m*Vector(face[numvert-1]) - Vector(cameraObj.loc)
534         
535         a = []
536         a.append(face[0][0])
537         a.append(face[0][1])
538         a.append(face[0][2])
539         a = RotatePoint(a[0], a[1], a[2], obj.RotX, obj.RotY, obj.RotZ)
540         a[0] += obj.LocX - camera.LocX
541         a[1] += obj.LocY - camera.LocY
542         a[2] += obj.LocZ - camera.LocZ
543         b = []
544         b.append(face[1][0])
545         b.append(face[1][1])
546         b.append(face[1][2])
547         b = RotatePoint(b[0], b[1], b[2], obj.RotX, obj.RotY, obj.RotZ)
548         b[0] += obj.LocX - camera.LocX
549         b[1] += obj.LocY - camera.LocY
550         b[2] += obj.LocZ - camera.LocZ
551         c = []
552         c.append(face[numvert-1][0])
553         c.append(face[numvert-1][1])
554         c.append(face[numvert-1][2])
555         c = RotatePoint(c[0], c[1], c[2], obj.RotX, obj.RotY, obj.RotZ)
556         c[0] += obj.LocX - camera.LocX
557         c[1] += obj.LocY - camera.LocY
558         c[2] += obj.LocZ - camera.LocZ
559
560         norm = [0, 0, 0]
561         norm[0] = (b[1] - a[1])*(c[2] - a[2]) - (c[1] - a[1])*(b[2] - a[2])
562         norm[1] = -((b[0] - a[0])*(c[2] - a[2]) - (c[0] - a[0])*(b[2] - a[2]))
563         norm[2] = (b[0] - a[0])*(c[1] - a[1]) - (c[0] - a[0])*(b[1] - a[1])
564
565         d = norm[0]*a[0] + norm[1]*a[1] + norm[2]*a[2]
566         #d = DotVecs(Vector(norm), Vector(a))
567
568         return (d<0)
569     
570     def _isFaceVisible(self, face, obj, cameraObj):
571         """Determine if the face is visible from the current camera.
572
573         The following code is taken basicly from the original vrm script.
574         """
575
576         camera = cameraObj
577
578         numvert = len(face)
579
580         # backface culling
581
582         # translate and rotate according to the object matrix
583         # and then translate according to the camera position
584         m = obj.getMatrix()
585         m.transpose()
586         
587         a = m*Vector(face[0]) - Vector(cameraObj.loc)
588         b = m*Vector(face[1]) - Vector(cameraObj.loc)
589         c = m*Vector(face[numvert-1]) - Vector(cameraObj.loc)
590
591         norm = m*Vector(face.no)
592
593         d = DotVecs(norm, a)
594
595         return (d<0)
596
597
598     def _doClipping():
599         return
600
601
602     # Per object methods
603
604     def _doVisibleSurfaceDetermination(object):
605         return
606
607     def _doColorizing(object):
608         return
609
610     def _doStylizingEdges(self, object, style):
611         """Process Mesh Edges. (For now copy the edge data, in next version it
612         can be a place where recognize silouhettes and/or contours).
613
614         input: an edge list
615         return: a processed edge list
616         """
617         return
618
619
620
621 # ---------------------------------------------------------------------
622 #
623 ## Main Program
624 #
625 # ---------------------------------------------------------------------
626
627
628 # FIXME: really hackish code, just to test if the other parts work
629 def depthSorting(scene):
630
631     cameraObj = Scene.GetCurrent().getCurrentCamera()
632     Objects = scene.getChildren()
633
634     Objects.sort(lambda obj1, obj2: 
635             cmp(Vector(Vector(cameraObj.loc) - Vector(obj1.loc)).length,
636                 Vector(Vector(cameraObj.loc) - Vector(obj2.loc)).length
637                 )
638             )
639     
640     # hackish sorting of faces according to the max z value of a vertex
641     for o in Objects:
642
643         mesh = o.data
644         mesh.faces.sort(
645             lambda f1, f2:
646                 # Sort faces according to the min z coordinate in a face
647                 #cmp(min([v[2] for v in f1]), min([v[2] for v in f2])))
648
649                 # Sort faces according to the max z coordinate in a face
650                 cmp(max([v[2] for v in f1]), max([v[2] for v in f2])))
651                 
652                 # Sort faces according to the avg z coordinate in a face
653                 #cmp(sum([v[2] for v in f1])/len(f1), sum([v[2] for v in f2])/len(f2)))
654         mesh.faces.reverse()
655         mesh.update()
656         
657     # update the scene
658     for o in scene.getChildren():
659         scene.unlink(o)
660     for o in Objects:
661         scene.link(o)
662     
663 def vectorize(filename):
664     """The vectorizing process is as follows:
665      
666      - Open the writer
667      - Render the scene
668      - Close the writer
669      
670      If you want to render an animation the second pass should be
671      repeated for any frame, and the frame number should be passed to the
672      renderer.
673      """
674
675     print "Filename: %s" % filename
676     
677     scene = Scene.GetCurrent()
678     renderer = Renderer()
679     
680     flatScene = renderer.doRendering(scene)
681     canvasSize = renderer.getCanvasSize()
682
683     depthSorting(flatScene)
684
685     writer = SVGVectorWriter(filename, canvasSize)
686     writer.printCanvas(flatScene)
687
688     Blender.Scene.unlink(flatScene)
689     del flatScene
690
691 # Here the main
692 if __name__ == "__main__":
693     # with this trick we can run the script in batch mode
694     try:
695         Blender.Window.FileSelector (vectorize, 'Save SVG', "proba.svg")
696     except:
697         vectorize("proba.svg")
698