6 Tooltip: 'Vector Rendering Method Export Script 0.3'
9 # ---------------------------------------------------------------------
10 # Copyright (c) 2006 Antonio Ospite
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.
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.
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
26 # ---------------------------------------------------------------------
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.
33 # ---------------------------------------------------------------------
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
39 # ---------------------------------------------------------------------
42 from Blender import Scene, Object, Mesh, NMesh, Lamp, Camera
43 from Blender.Mathutils import *
47 # ---------------------------------------------------------------------
49 ## Projections classes
51 # ---------------------------------------------------------------------
54 """Calculate the projection of an object given the camera.
56 A projector is useful to so some per-object transformation to obtain the
57 projection of an object given the camera.
59 The main method is #doProjection# see the method description for the
63 def __init__(self, cameraObj, canvasRatio):
64 """Calculate the projection matrix.
66 The projection matrix depends, in this case, on the camera settings,
67 and also on object transformation matrix.
70 camera = cameraObj.getData()
72 aspect = float(canvasRatio[0])/float(canvasRatio[1])
73 near = camera.clipStart
76 fovy = atan(0.5/aspect/(camera.lens/32))
79 # What projection do we want?
81 m2 = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale)
83 m2 = self._calcPerspectiveMatrix(fovy, aspect, near, far)
87 cam = Matrix(cameraObj.getInverseMatrix())
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())
100 self.projectionMatrix = mP
106 def doProjection(self, v):
107 """Project the point on the view plane.
109 Given a vertex calculate the projection using the current projection
113 # Note that we need the vertex expressed using homogeneous coordinates
114 p = self.projectionMatrix * Vector(v).resize4D()
126 def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
127 """Return a perspective projection matrix."""
129 top = near * tan(fovy * pi / 360.0)
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))
144 [0.0, 0.0, -1.0, 0.0])
148 def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
149 """Return an orthogonal projection matrix."""
151 top = near * tan(fovy * pi / 360.0) * (scale * 10)
153 left = bottom * aspect
158 tx = -((right+left)/rl)
159 ty = -((top+bottom)/tb)
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])
171 # ---------------------------------------------------------------------
173 ## Object representation class
175 # ---------------------------------------------------------------------
177 # TODO: a class to represent the needed properties of a 2D vector image
178 # Just use a NMesh structure?
181 # ---------------------------------------------------------------------
183 ## Vector Drawing Classes
185 # ---------------------------------------------------------------------
191 A class for printing output in a vectorial format.
193 Given a 2D representation of the 3D scene the class is responsible to
194 write it is a vector format.
196 Every subclasses of VectorWriter must have at last the following public
198 - printCanvas(mesh) --- where mesh is as specified before.
201 def __init__(self, fileName):
202 """Open the file named #fileName# and set the canvas size."""
204 self.file = open(fileName, "w")
205 print "Outputting to: ", fileName
208 context = Scene.GetCurrent().getRenderingContext()
209 self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
216 def printCanvas(mesh):
232 class SVGVectorWriter(VectorWriter):
233 """A concrete class for writing SVG output.
235 The class does not support animations, yet.
239 def __init__(self, file):
240 """Simply call the parent Contructor."""
241 VectorWriter.__init__(self, file)
256 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
257 """Convert the scene representation to SVG."""
259 Objects = scene.getChildren()
262 if(obj.getType() != 'Mesh'):
266 self.file.write("<g>\n")
270 for face in obj.getData().faces:
271 self._printPolygon(face)
274 self._printEdges(obj.getData(), showHiddenEdges)
276 self.file.write("</g>\n")
283 def _printHeader(self):
284 """Print SVG header."""
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" %
294 def _printFooter(self):
295 """Print the SVG footer."""
297 self.file.write("\n</svg>\n")
300 def _printEdges(self, mesh, showHiddenEdges=False):
301 """Print the wireframe using mesh edges... is this the correct way?
305 stroke_col = [0, 0, 0]
307 self.file.write("<g>\n")
311 hidden_stroke_style = ""
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:
318 hidden_stroke_style = ";\n stroke-dasharray:3, 3"
320 p1 = self._calcCanvasCoord(e.v1)
321 p2 = self._calcCanvasCoord(e.v2)
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")
331 self.file.write("</g>\n")
335 def _printPolygon(self, face):
336 """Print our primitive, finally.
343 self.file.write("<polygon points=\"")
346 p = self._calcCanvasCoord(v)
347 self.file.write("%g,%g " % (p[0], p[1]))
349 self.file.seek(-1,1) # get rid of the last space
350 self.file.write("\"\n")
352 #take as face color the first vertex color
355 color = [fcol.r, fcol.g, fcol.b]
357 color = [ 255, 255, 255]
359 stroke_col = [0, 0, 0]
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")
369 def _calcCanvasCoord(self, v):
371 pt = Vector([0, 0, 0])
373 mW = self.canvasSize[0]/2
374 mH = self.canvasSize[1]/2
376 # rescale to canvas size
377 pt[0] = round(v[0]*mW)+mW
378 pt[1] = round(v[1]*mH)+mH
380 # For now we want (0,0) in the top-left corner of the canvas
381 # Mirror and translate along y
383 pt[1] += self.canvasSize[1]
388 # ---------------------------------------------------------------------
392 # ---------------------------------------------------------------------
395 """Render a scene viewed from a given camera.
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.
400 The user can optionally provide a specific camera for the rendering, see
401 the #doRendering# method for more informations.
405 """Make the rendering process only for the current scene by default.
408 # Render the current Scene set as a READ-ONLY property
409 self._SCENE = Scene.GetCurrent()
411 # Use the aspect ratio of the scene rendering context
412 context = self._SCENE.getRenderingContext()
413 self.canvasRatio = (context.aspectRatioX(), context.aspectRatioY())
415 # Render from the currently active camera
416 self.camera = self._SCENE.getCurrentCamera()
423 def doRendering(self, outputWriter, animation=0):
424 """Render picture or animation and write it out.
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
432 context = self._SCENE.getRenderingContext()
433 currentFrame = context.currentFrame()
435 # Handle the animation case
437 startFrame = currentFrame
438 endFrame = startFrame
440 startFrame = context.startFrame()
441 endFrame = context.endFrame()
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)
451 # clear the rendered scene
452 self._SCENE.makeCurrent()
453 Scene.unlink(renderedScene)
457 context.currentFrame(currentFrame)
461 def doRenderScene(self, inputScene):
462 """Control the rendering process.
464 Here we control the entire rendering process invoking the operation
465 needed to transform and project the 3D scene in two dimensions.
468 # Use some temporary workspace, a full copy of the scene
469 workScene = inputScene.copy(2)
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)
477 # global processing of the scene
478 self._doDepthSorting(workScene)
480 # Per object activities
481 Objects = workScene.getChildren()
485 if (obj.getType() != 'Mesh'):
486 print "Type:", obj.getType(), "\tSorry, only mesh Object supported!"
490 self._doModelViewTransformations(obj)
492 self._doBackFaceCulling(obj)
494 self._doColorAndLighting(obj)
496 # 'style' can be a function that determine
497 # if an edge should be showed?
498 self._doEdgesStyle(obj, style=None)
500 self._doProjection(obj, proj)
505 def oldRenderScene(scene):
507 # Per object activities
508 Objects = workScene.getChildren()
512 if (obj.getType() != 'Mesh'):
513 print "Type:", obj.getType(), "\tSorry, only mesh Object supported!"
516 # Get a projector for this object
517 proj = Projector(self.camera, obj, self.canvasSize)
519 # Let's store the transformed data
520 transformed_mesh = NMesh.New("flat"+obj.name)
521 transformed_mesh.hasVertexColours(1)
524 self._doProcessEdges(obj)
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
532 # Store the materials
533 materials = obj.getData().getMaterials()
535 meshfaces = obj.getData().faces
537 for face in meshfaces:
539 # if the face is visible flatten it on the "picture plane"
540 if self._isFaceVisible(face, obj, cameraObj):
542 # Store transformed face
543 newface = NMesh.Face()
547 p = proj.doProjection(vert.co)
549 tmp_vert = NMesh.Vert(p[0], p[1], p[2])
551 # Add the vert to the mesh
552 transformed_mesh.verts.append(tmp_vert)
554 newface.v.append(tmp_vert)
557 # Per-face color calculation
558 # code taken mostly from the original vrm script
559 # TODO: understand the code and rewrite it clearly
562 fakelight = Object.Get("Lamp").loc
563 if fakelight == None:
564 fakelight = [1.0, 1.0, -0.3]
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
574 tmp_col = materials[face.mat].getRGBCol()
576 tmp_col = [0.5, 0.5, 0.5]
578 tmp_col = [ (c>intensity) and int(round((c-intensity)*10)*25.5) for c in tmp_col ]
580 vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2])
581 newface.col = [vcol, vcol, vcol, 255]
583 transformed_mesh.addFace(newface)
585 # at the end of the loop on obj
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)
602 def _isFaceVisible(self, face, obj, camObj):
603 """Determine if a face of an object is visible from a given camera.
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.
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).
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.
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.
623 # The transformation matrix of the object
624 mObj = Matrix(obj.getMatrix())
627 # The normal after applying the current object rotation
628 #normal = mObj.rotationPart() * Vector(face.no)
629 normal = Vector(face.no)
631 # View vector in orthographics projections can be considered simply s the
633 #view_vect = Vector(camObj.loc)
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()
642 view_vect = Vector(camObj.loc) - point
645 # if d <= 0 the face is visible from the camera
646 d = view_vect * normal
659 def _doDepthSorting(self, scene):
661 cameraObj = self.camera
662 Objects = scene.getChildren()
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
670 # hackish sorting of faces according to the max z value of a vertex
673 if (o.getType() != 'Mesh'):
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])))
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])))
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)))
692 # FIXME: check if it is correct
694 #for o in scene.getChildren():
701 def _doModelViewTransformations(self, object):
702 if(object.getType() != 'Mesh'):
705 matMV = object.matrix
707 mesh.transform(matMV, True)
711 def _doBackFaceCulling(self, object):
712 if(object.getType() != 'Mesh'):
715 print "doing Backface Culling"
718 # Select all vertices, so edges without faces can be displayed
722 Mesh.Mode(Mesh.SelectModes['FACE'])
726 if self._isFaceVisible(f, object, self.camera):
743 #Mesh.Mode(Mesh.SelectModes['VERTEX'])
745 def _doColorAndLighting(self, object):
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).
753 return: a processed edge list
757 def _doProjection(self, object, projector):
759 if(object.getType() != 'Mesh'):
764 p = projector.doProjection(v.co)
772 # ---------------------------------------------------------------------
776 # ---------------------------------------------------------------------
779 # FIXME: really hackish code, just to test if the other parts work
781 def vectorize(filename):
782 """The vectorizing process is as follows:
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
792 writer = SVGVectorWriter(filename)
796 renderer = Renderer()
797 renderer.doRendering(writer)
803 if __name__ == "__main__":
804 # with this trick we can run the script in batch mode
806 Blender.Window.FileSelector (vectorize, 'Save SVG', "proba.svg")
809 from Blender import Window
810 editmode = Window.EditMode()
811 if editmode: Window.EditMode(0)
813 vectorize("proba.svg")
814 if editmode: Window.EditMode(1)