Fix dir separator in output file
[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 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         m1 = Matrix()
88         mP = Matrix()
89
90         # View transformation
91         cam = cameraObj.getInverseMatrix()
92         cam.transpose() 
93
94         m1 = obMesh.getMatrix()
95         m1.transpose()
96         
97         mP = cam * m1
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[0], v[1], v[2], 1.0])
115         
116         mW = self.size[0]/2
117         mH = self.size[1]/2
118         
119         if p[3]<=0:
120             p[0] = int(p[0]*mW)+mW
121             p[1] = int(p[1]*mH)+mH
122         else:
123             p[0] = int((p[0]/p[3])*mW)+mW
124             p[1] = int((p[1]/p[3])*mH)+mH
125             
126         # For now we want (0,0) in the top-left corner of the canvas
127         # Mirror and translate along y
128         p[1] *= -1
129         p[1] += self.size[1]
130     
131         return p
132
133     ##
134     # Private methods
135     #
136     
137     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
138         """Return a perspective projection matrix."""
139         
140         top = near * tan(fovy * pi / 360.0)
141         bottom = -top
142         left = bottom*aspect
143         right= top*aspect
144         x = (2.0 * near) / (right-left)
145         y = (2.0 * near) / (top-bottom)
146         a = (right+left) / (right-left)
147         b = (top+bottom) / (top - bottom)
148         c = - ((far+near) / (far-near))
149         d = - ((2*far*near)/(far-near))
150         
151         m = Matrix(
152                 [x,   0.0,    a,    0.0],
153                 [0.0,   y,    b,    0.0],
154                 [0.0, 0.0,    c,      d],
155                 [0.0, 0.0, -1.0,    0.0])
156
157         return m
158
159     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
160         """Return an orthogonal projection matrix."""
161         
162         top = near * tan(fovy * pi / 360.0) * (scale * 10)
163         bottom = -top 
164         left = bottom * aspect
165         right= top * aspect
166         rl = right-left
167         tb = top-bottom
168         fn = near-far 
169         tx = -((right+left)/rl)
170         ty = -((top+bottom)/tb)
171         tz = ((far+near)/fn)
172
173         m = Matrix(
174                 [2.0/rl, 0.0,    0.0,     tx],
175                 [0.0,    2.0/tb, 0.0,     ty],
176                 [0.0,    0.0,    2.0/fn,  tz],
177                 [0.0,    0.0,    0.0,    1.0])
178         
179         return m
180
181
182 # ---------------------------------------------------------------------
183 #
184 ## Mesh representation class
185 #
186 # ---------------------------------------------------------------------
187
188 # TODO: a class to represent the needed properties of a 2D vector image
189 # Just use a NMesh structure?
190
191
192 # ---------------------------------------------------------------------
193 #
194 ## Vector Drawing Classes
195 #
196 # ---------------------------------------------------------------------
197
198 ## A generic Writer
199
200 class VectorWriter:
201     """
202     A class for printing output in a vectorial format.
203
204     Given a 2D representation of the 3D scene the class is responsible to
205     write it is a vector format.
206
207     Every subclasses of VectorWriter must have at last the following public
208     methods:
209         - printCanvas(mesh) --- where mesh is as specified before.
210     """
211     
212     def __init__(self, fileName, canvasSize):
213         """Open the file named #fileName# and set the canvas size."""
214         
215         self.file = open(fileName, "w")
216         print "Outputting to: ", fileName
217
218         self.canvasSize = canvasSize
219     
220
221     ##
222     # Public Methods
223     #
224     
225     def printCanvas(mesh):
226         return
227         
228     ##
229     # Private Methods
230     #
231     
232     def _printHeader():
233         return
234
235     def _printFooter():
236         return
237
238
239 ## SVG Writer
240
241 class SVGVectorWriter(VectorWriter):
242     """A concrete class for writing SVG output.
243
244     The class does not support animations, yet.
245     Sorry.
246     """
247
248     def __init__(self, file, canvasSize):
249         """Simply call the parent Contructor."""
250         VectorWriter.__init__(self, file, canvasSize)
251
252
253     ##
254     # Public Methods
255     #
256     
257     def printCanvas(self, scene):
258         """Convert the scene representation to SVG."""
259
260         self._printHeader()
261         
262         for obj in scene:
263             self.file.write("<g>\n")
264             
265             for face in obj.faces:
266                 self._printPolygon(face)
267
268             self.file.write("</g>\n")
269         
270         self._printFooter()
271     
272     ##  
273     # Private Methods
274     #
275     
276     def _printHeader(self):
277         """Print SVG header."""
278
279         self.file.write("<?xml version=\"1.0\"?>\n")
280         self.file.write("<svg version=\"1.2\"\n")
281         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
282         self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
283                 self.canvasSize)
284
285     def _printFooter(self):
286         """Print the SVG footer."""
287
288         self.file.write("\n</svg>\n")
289         self.file.close()
290
291     def _printPolygon(self, face):
292         """Print our primitive, finally.
293
294         There is no color Handling for now, *FIX!*
295         """
296
297         stroke_width=1
298         
299         self.file.write("<polygon points=\"")
300
301         i = 0
302         for v in face:
303             if i != 0:
304                 self.file.write(", ")
305
306             i+=1
307             
308             self.file.write("%g, %g" % (v[0], v[1]))
309         
310         color = [ int(c*255) for c in face.col]
311
312         self.file.write("\"\n")
313         self.file.write("\tstyle=\"fill:rgb("+str(color[0])+","+str(color[1])+","+str(color[2])+");")
314         self.file.write(" stroke:rgb(0,0,0);")
315         self.file.write(" stroke-width:"+str(stroke_width)+";\n")
316         self.file.write(" stroke-linecap:round;stroke-linejoin:round\"/>\n")
317
318
319 # ---------------------------------------------------------------------
320 #
321 ## Rendering Classes
322 #
323 # ---------------------------------------------------------------------
324
325 def RotatePoint(PX,PY,PZ,AngleX,AngleY,AngleZ):
326     
327     NewPoint = []
328     # Rotate X
329     NewY = (PY * cos(AngleX))-(PZ * sin(AngleX))
330     NewZ = (PZ * cos(AngleX))+(PY * sin(AngleX))
331     # Rotate Y
332     PZ = NewZ
333     PY = NewY
334     NewZ = (PZ * cos(AngleY))-(PX * sin(AngleY))
335     NewX = (PX * cos(AngleY))+(PZ * sin(AngleY))
336     PX = NewX
337     PZ = NewZ
338     # Rotate Z
339     NewX = (PX * cos(AngleZ))-(PY * sin(AngleZ))
340     NewY = (PY * cos(AngleZ))+(PX * sin(AngleZ))
341     NewPoint.append(NewX)
342     NewPoint.append(NewY)
343     NewPoint.append(NewZ)
344     return NewPoint
345
346 class Renderer:
347     """Render a scene viewed from a given camera.
348     
349     This class is responsible of the rendering process, hence transormation
350     and projection of the ojects in the scene are invoked by the renderer.
351
352     The user can optionally provide a specific camera for the rendering, see
353     the #doRendering# method for more informations.
354     """
355
356     def __init__(self):
357         """Set the canvas size to a defaulr value.
358         
359         The only instance attribute here is the canvas size, which can be
360         queryed to the renderer by other entities.
361         """
362         self.canvasSize = (0.0, 0.0)
363
364
365     ##
366     # Public Methods
367     #
368
369     def getCanvasSize(self):
370         """Return the current canvas size read from Blender rendering context"""
371         return self.canvasSize
372         
373     def doRendering(self, scene, cameraObj=None):
374         """Control the rendering process.
375         
376         Here we control the entire rendering process invoking the operation
377         needed to transforma project the 3D scene in two dimensions.
378
379         Parameters:
380         scene --- the Blender Scene to render
381         cameraObj --- the camera object to use for the viewing processing
382         """
383
384         if cameraObj == None:
385             cameraObj = scene.getCurrentCamera()
386         
387         context = scene.getRenderingContext()
388         self.canvasSize = (context.imageSizeX(), context.imageSizeY())
389         
390         Objects = scene.getChildren()
391         
392         # A structure to store the transformed scene
393         newscene = []
394         
395         for obj in Objects:
396             
397             if (obj.getType() != "Mesh"):
398                 print "Type:", obj.getType(), "\tSorry, only mesh Object supported!"
399                 continue
400
401             # Get a projector for this object
402             proj = Projector(cameraObj, obj, self.canvasSize)
403
404             # Let's store the transformed data
405             transformed_mesh = NMesh.New(obj.name)
406
407             # Store the materials
408             materials = obj.getData().getMaterials()
409
410             meshfaces = obj.getData().faces
411
412             for face in meshfaces:
413
414                 # if the face is visible flatten it on the "picture plane"
415                 if self._isFaceVisible(face, obj, cameraObj):
416                     
417                     # Store transformed face
418                     transformed_face = []
419
420                     for vert in face:
421
422                         p = proj.doProjection(vert.co)
423
424                         transformed_vert = NMesh.Vert(p[0], p[1], p[2])
425                         transformed_face.append(transformed_vert)
426
427                     newface = NMesh.Face(transformed_face)
428                     
429                     # Per-face color calculation
430                     # code taken mostly from the original vrm script
431                     # TODO: understand the code and rewrite it clearly
432                     ambient = -250
433                     fakelight = [10, 10, 15]
434                     norm = face.normal
435                     vektori = (norm[0]*fakelight[0]+norm[1]*fakelight[1]+norm[2]*fakelight[2])
436                     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)))
437                     intensity = floor(ambient + 200*acos(vektori/vduzine))/200
438                     if intensity < 0:
439                         intensity = 0
440
441                     if materials:
442                         newface.col = materials[face.mat].getRGBCol()
443                     else:
444                         newface.col = [0.5, 0.5, 0.5]
445                         
446                     newface.col = [ (c>0) and (c-intensity) for c in newface.col]
447                     
448                     transformed_mesh.addFace(newface)
449
450             # at the end of the loop on obj
451             
452             #transformed_object = NMesh.PutRaw(transformed_mesh)
453             newscene.append(transformed_mesh)
454
455         # reverse the order (TODO: See how is the object order in NMesh)
456         #newscene.reverse()
457         
458         return newscene
459
460
461     ##
462     # Private Methods
463     #
464
465     def _isFaceVisible(self, face, obj, cameraObj):
466         """Determine if the face is visible from the current camera.
467
468         The following code is taken basicly from the original vrm script.
469         """
470
471         camera = cameraObj
472
473         numvert = len(face)
474
475         # backface culling
476
477         # translate and rotate according to the object matrix
478         # and then translate according to the camera position
479         #m = obj.getMatrix()
480         #m.transpose()
481         
482         #a = m*Vector(face[0]) - Vector(cameraObj.loc)
483         #b = m*Vector(face[1]) - Vector(cameraObj.loc)
484         #c = m*Vector(face[numvert-1]) - Vector(cameraObj.loc)
485         
486         a = []
487         a.append(face[0][0])
488         a.append(face[0][1])
489         a.append(face[0][2])
490         a = RotatePoint(a[0], a[1], a[2], obj.RotX, obj.RotY, obj.RotZ)
491         a[0] += obj.LocX - camera.LocX
492         a[1] += obj.LocY - camera.LocY
493         a[2] += obj.LocZ - camera.LocZ
494         b = []
495         b.append(face[1][0])
496         b.append(face[1][1])
497         b.append(face[1][2])
498         b = RotatePoint(b[0], b[1], b[2], obj.RotX, obj.RotY, obj.RotZ)
499         b[0] += obj.LocX - camera.LocX
500         b[1] += obj.LocY - camera.LocY
501         b[2] += obj.LocZ - camera.LocZ
502         c = []
503         c.append(face[numvert-1][0])
504         c.append(face[numvert-1][1])
505         c.append(face[numvert-1][2])
506         c = RotatePoint(c[0], c[1], c[2], obj.RotX, obj.RotY, obj.RotZ)
507         c[0] += obj.LocX - camera.LocX
508         c[1] += obj.LocY - camera.LocY
509         c[2] += obj.LocZ - camera.LocZ
510
511         norm = Vector([0,0,0])
512         norm[0] = (b[1] - a[1])*(c[2] - a[2]) - (c[1] - a[1])*(b[2] - a[2])
513         norm[1] = -((b[0] - a[0])*(c[2] - a[2]) - (c[0] - a[0])*(b[2] - a[2]))
514         norm[2] = (b[0] - a[0])*(c[1] - a[1]) - (c[0] - a[0])*(b[1] - a[1])
515
516         d = norm[0]*a[0] + norm[1]*a[1] + norm[2]*a[2]
517         # d = DotVecs(norm, Vector(a))
518
519         return (d<0)
520
521     def _doClipping(face):
522         return
523
524
525 # ---------------------------------------------------------------------
526 #
527 ## Main Program
528 #
529 # ---------------------------------------------------------------------
530
531
532 # hackish sorting of faces according to the max z value of a vertex
533 def zSorting(scene):
534     for o in scene:
535         o.faces.sort(lambda f1, f2:
536                 # Sort faces according to the min z coordinate in a face
537                 #cmp(min([v[2] for v in f1]), min([v[2] for v in f2])))
538
539                 # Sort faces according to the max z coordinate in a face
540                 cmp(max([v[2] for v in f1]), max([v[2] for v in f2])))
541                 
542                 # Sort faces according to the avg z coordinate in a face
543                 #cmp(sum([v[2] for v in f1])/len(f1), sum([v[2] for v in f2])/len(f2)))
544         o.faces.reverse()
545     
546 from Blender import sys
547 def vectorize(filename):
548
549     print "Filename: %s" % filename
550     print
551     filename = filename.replace('/', sys.sep)
552     print filename
553     print
554     
555     scene   = Scene.GetCurrent()
556     renderer = Renderer()
557
558     flatScene = renderer.doRendering(scene)
559     canvasSize = renderer.getCanvasSize()
560
561     zSorting(flatScene)
562
563     writer = SVGVectorWriter(filename, canvasSize)
564     writer.printCanvas(flatScene)
565     
566 try:
567     Blender.Window.FileSelector (vectorize, 'Save SVG', "proba.svg")
568 except:
569     vectorize("proba.svg")
570