00c8ed4d577126f68937dae681651043186a8be4
[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
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 ## Mesh 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.file.write("</g>\n")
268         
269         self._printFooter()
270     
271     ##  
272     # Private Methods
273     #
274     
275     def _printHeader(self):
276         """Print SVG header."""
277
278         self.file.write("<?xml version=\"1.0\"?>\n")
279         self.file.write("<svg version=\"1.2\"\n")
280         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
281         self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
282                 self.canvasSize)
283
284     def _printFooter(self):
285         """Print the SVG footer."""
286
287         self.file.write("\n</svg>\n")
288         self.file.close()
289
290     def _printPolygon(self, face):
291         """Print our primitive, finally.
292         """
293         
294         wireframe = False
295         
296         stroke_width=0.5
297         
298         self.file.write("<polygon points=\"")
299
300         for v in face:
301             self.file.write("%g,%g " % (v[0], v[1]))
302         
303         self.file.seek(-1,1) # get rid of the last space
304         self.file.write("\"\n")
305         
306         #take as face color the first vertex color
307         fcol = face.col[0]
308         color = [fcol.r, fcol.g, fcol.b]
309
310         stroke_col = [0, 0, 0]
311         if not wireframe:
312             stroke_col = color
313
314         self.file.write("\tstyle=\"fill:rgb("+str(color[0])+","+str(color[1])+","+str(color[2])+");")
315         self.file.write(" stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
316         self.file.write(" stroke-width:"+str(stroke_width)+";\n")
317         self.file.write(" stroke-linecap:round;stroke-linejoin:round")
318         self.file.write("\"/>\n")
319
320
321 # ---------------------------------------------------------------------
322 #
323 ## Rendering Classes
324 #
325 # ---------------------------------------------------------------------
326
327 def RotatePoint(PX,PY,PZ,AngleX,AngleY,AngleZ):
328     
329     NewPoint = []
330     # Rotate X
331     NewY = (PY * cos(AngleX))-(PZ * sin(AngleX))
332     NewZ = (PZ * cos(AngleX))+(PY * sin(AngleX))
333     # Rotate Y
334     PZ = NewZ
335     PY = NewY
336     NewZ = (PZ * cos(AngleY))-(PX * sin(AngleY))
337     NewX = (PX * cos(AngleY))+(PZ * sin(AngleY))
338     PX = NewX
339     PZ = NewZ
340     # Rotate Z
341     NewX = (PX * cos(AngleZ))-(PY * sin(AngleZ))
342     NewY = (PY * cos(AngleZ))+(PX * sin(AngleZ))
343     NewPoint.append(NewX)
344     NewPoint.append(NewY)
345     NewPoint.append(NewZ)
346     return NewPoint
347
348 class Renderer:
349     """Render a scene viewed from a given camera.
350     
351     This class is responsible of the rendering process, hence transormation
352     and projection of the ojects in the scene are invoked by the renderer.
353
354     The user can optionally provide a specific camera for the rendering, see
355     the #doRendering# method for more informations.
356     """
357
358     def __init__(self):
359         """Set the canvas size to a defaulr value.
360         
361         The only instance attribute here is the canvas size, which can be
362         queryed to the renderer by other entities.
363         """
364         self.canvasSize = (0.0, 0.0)
365
366
367     ##
368     # Public Methods
369     #
370
371     def getCanvasSize(self):
372         """Return the current canvas size read from Blender rendering context"""
373         return self.canvasSize
374         
375     def doRendering(self, scene, cameraObj=None):
376         """Control the rendering process.
377         
378         Here we control the entire rendering process invoking the operation
379         needed to transforma project the 3D scene in two dimensions.
380
381         Parameters:
382         scene --- the Blender Scene to render
383         cameraObj --- the camera object to use for the viewing processing
384         """
385
386         if cameraObj == None:
387             cameraObj = scene.getCurrentCamera()
388         
389         context = scene.getRenderingContext()
390         self.canvasSize = (context.imageSizeX(), context.imageSizeY())
391         
392         Objects = scene.getChildren()
393         
394         # A structure to store the transformed scene
395         newscene = Scene.New("flat"+scene.name)
396         
397         for obj in Objects:
398             
399             if (obj.getType() != "Mesh"):
400                 print "Type:", obj.getType(), "\tSorry, only mesh Object supported!"
401                 continue
402
403             # Get a projector for this object
404             proj = Projector(cameraObj, obj, self.canvasSize)
405
406             # Let's store the transformed data
407             transformed_mesh = NMesh.New("flat"+obj.name)
408             transformed_mesh.hasVertexColours(1)
409
410             # Store the materials
411             materials = obj.getData().getMaterials()
412
413             meshfaces = obj.getData().faces
414
415             for face in meshfaces:
416
417                 # if the face is visible flatten it on the "picture plane"
418                 if self._isFaceVisible_old(face, obj, cameraObj):
419                     
420                     # Store transformed face
421                     newface = NMesh.Face()
422
423                     for vert in face:
424
425                         p = proj.doProjection(vert.co)
426
427                         tmp_vert = NMesh.Vert(p[0], p[1], p[2])
428
429                         # Add the vert to the mesh
430                         transformed_mesh.verts.append(tmp_vert)
431                         
432                         newface.v.append(tmp_vert)
433                         
434                     
435                     # Per-face color calculation
436                     # code taken mostly from the original vrm script
437                     # TODO: understand the code and rewrite it clearly
438                     ambient = -150
439                     
440                     fakelight = Object.Get("Lamp").loc
441                     if fakelight == None:
442                         fakelight = [1.0, 1.0, -0.3]
443
444                     norm = Vector(face.no)
445                     vektori = (norm[0]*fakelight[0]+norm[1]*fakelight[1]+norm[2]*fakelight[2])
446                     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)))
447                     intensity = floor(ambient + 200*acos(vektori/vduzine))/200
448                     if intensity < 0:
449                         intensity = 0
450
451                     if materials:
452                         tmp_col = materials[face.mat].getRGBCol()
453                     else:
454                         tmp_col = [0.5, 0.5, 0.5]
455                         
456                     tmp_col = [ (c>intensity) and int(round((c-intensity)*10)*25.5) for c in tmp_col ]
457
458                     vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2])
459                     newface.col = [vcol, vcol, vcol, 255]
460                     
461                     transformed_mesh.addFace(newface)
462
463             # at the end of the loop on obj
464             
465             transformed_obj = Object.New(obj.getType(), "flat"+obj.name)
466             transformed_obj.link(transformed_mesh)
467             transformed_obj.loc = obj.loc
468             newscene.link(transformed_obj)
469
470         
471         return newscene
472
473
474     ##
475     # Private Methods
476     #
477
478     def _isFaceVisible_old(self, face, obj, cameraObj):
479         """Determine if the face is visible from the current camera.
480
481         The following code is taken basicly from the original vrm script.
482         """
483
484         camera = cameraObj
485
486         numvert = len(face)
487
488         # backface culling
489
490         # translate and rotate according to the object matrix
491         # and then translate according to the camera position
492         #m = obj.getMatrix()
493         #m.transpose()
494         
495         #a = m*Vector(face[0]) - Vector(cameraObj.loc)
496         #b = m*Vector(face[1]) - Vector(cameraObj.loc)
497         #c = m*Vector(face[numvert-1]) - Vector(cameraObj.loc)
498         
499         a = []
500         a.append(face[0][0])
501         a.append(face[0][1])
502         a.append(face[0][2])
503         a = RotatePoint(a[0], a[1], a[2], obj.RotX, obj.RotY, obj.RotZ)
504         a[0] += obj.LocX - camera.LocX
505         a[1] += obj.LocY - camera.LocY
506         a[2] += obj.LocZ - camera.LocZ
507         b = []
508         b.append(face[1][0])
509         b.append(face[1][1])
510         b.append(face[1][2])
511         b = RotatePoint(b[0], b[1], b[2], obj.RotX, obj.RotY, obj.RotZ)
512         b[0] += obj.LocX - camera.LocX
513         b[1] += obj.LocY - camera.LocY
514         b[2] += obj.LocZ - camera.LocZ
515         c = []
516         c.append(face[numvert-1][0])
517         c.append(face[numvert-1][1])
518         c.append(face[numvert-1][2])
519         c = RotatePoint(c[0], c[1], c[2], obj.RotX, obj.RotY, obj.RotZ)
520         c[0] += obj.LocX - camera.LocX
521         c[1] += obj.LocY - camera.LocY
522         c[2] += obj.LocZ - camera.LocZ
523
524         norm = [0, 0, 0]
525         norm[0] = (b[1] - a[1])*(c[2] - a[2]) - (c[1] - a[1])*(b[2] - a[2])
526         norm[1] = -((b[0] - a[0])*(c[2] - a[2]) - (c[0] - a[0])*(b[2] - a[2]))
527         norm[2] = (b[0] - a[0])*(c[1] - a[1]) - (c[0] - a[0])*(b[1] - a[1])
528
529         d = norm[0]*a[0] + norm[1]*a[1] + norm[2]*a[2]
530         #d = DotVecs(Vector(norm), Vector(a))
531
532         return (d<0)
533     
534     def _isFaceVisible(self, face, obj, cameraObj):
535         """Determine if the face is visible from the current camera.
536
537         The following code is taken basicly from the original vrm script.
538         """
539
540         camera = cameraObj
541
542         numvert = len(face)
543
544         # backface culling
545
546         # translate and rotate according to the object matrix
547         # and then translate according to the camera position
548         m = obj.getMatrix()
549         m.transpose()
550         
551         a = m*Vector(face[0]) - Vector(cameraObj.loc)
552         b = m*Vector(face[1]) - Vector(cameraObj.loc)
553         c = m*Vector(face[numvert-1]) - Vector(cameraObj.loc)
554
555         norm = m*Vector(face.no)
556
557         d = DotVecs(norm, a)
558
559         return (d<0)
560
561     def _doClipping(face):
562         return
563
564
565 # ---------------------------------------------------------------------
566 #
567 ## Main Program
568 #
569 # ---------------------------------------------------------------------
570
571
572 # FIXME: really hackish code, just to test if the other parts work
573 def depthSorting(scene):
574
575     cameraObj = Scene.GetCurrent().getCurrentCamera()
576     Objects = scene.getChildren()
577
578     Objects.sort(lambda obj1, obj2: 
579             cmp(Vector(Vector(cameraObj.loc) - Vector(obj1.loc)).length,
580                 Vector(Vector(cameraObj.loc) - Vector(obj2.loc)).length
581                 )
582             )
583     
584     # hackish sorting of faces according to the max z value of a vertex
585     for o in Objects:
586
587         mesh = o.data
588         mesh.faces.sort(
589             lambda f1, f2:
590                 # Sort faces according to the min z coordinate in a face
591                 #cmp(min([v[2] for v in f1]), min([v[2] for v in f2])))
592
593                 # Sort faces according to the max z coordinate in a face
594                 cmp(max([v[2] for v in f1]), max([v[2] for v in f2])))
595                 
596                 # Sort faces according to the avg z coordinate in a face
597                 #cmp(sum([v[2] for v in f1])/len(f1), sum([v[2] for v in f2])/len(f2)))
598         mesh.faces.reverse()
599         mesh.update()
600         
601     # update the scene
602     for o in scene.getChildren():
603         scene.unlink(o)
604     for o in Objects:
605         scene.link(o)
606     
607 def vectorize(filename):
608
609     print "Filename: %s" % filename
610     
611     scene = Scene.GetCurrent()
612     renderer = Renderer()
613
614     flatScene = renderer.doRendering(scene)
615     canvasSize = renderer.getCanvasSize()
616
617     depthSorting(flatScene)
618
619     writer = SVGVectorWriter(filename, canvasSize)
620     writer.printCanvas(flatScene)
621
622     Blender.Scene.unlink(flatScene)
623     del flatScene
624
625 # Here the main
626 if __name__ == "__main__":
627     try:
628         Blender.Window.FileSelector (vectorize, 'Save SVG', "proba.svg")
629     except:
630         vectorize("proba.svg")
631