6 Tooltip: 'Vector Rendering Method Export Script'
9 __author__ = "Antonio Ospite"
14 Render the scene and save the result in vector format.
17 # ---------------------------------------------------------------------
18 # Copyright (c) 2006 Antonio Ospite
20 # This program is free software; you can redistribute it and/or modify
21 # it under the terms of the GNU General Public License as published by
22 # the Free Software Foundation; either version 2 of the License, or
23 # (at your option) any later version.
25 # This program is distributed in the hope that it will be useful,
26 # but WITHOUT ANY WARRANTY; without even the implied warranty of
27 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 # GNU General Public License for more details.
30 # You should have received a copy of the GNU General Public License
31 # along with this program; if not, write to the Free Software
32 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
34 # ---------------------------------------------------------------------
37 # Thanks to Emilio Aguirre for S2flender from which I took inspirations :)
38 # Thanks to Nikola Radovanovic, the author of the original VRM script,
39 # the code you read here has been rewritten _almost_ entirely
40 # from scratch but Nikola gave me the idea, so I thank him publicly.
42 # ---------------------------------------------------------------------
44 # Things TODO for a next release:
45 # - Switch to the Mesh structure, should be considerably faster
46 # (partially done, but cannot sort faces, yet)
47 # - Use a better depth sorting algorithm
48 # - Review how selections are made (this script uses selection states of
49 # primitives to represent visibility infos)
50 # - Implement Clipping and do handle object intersections
51 # - Implement Edge Styles (silhouettes, contours, etc.)
52 # - Implement Edge coloring
53 # - Use multiple lighting sources in color calculation
54 # - Implement Shading Styles?
55 # - Use another representation for the 2D projection?
56 # Think to a way to merge adjacent polygons that have the same color.
57 # - Add other Vector Writers.
59 # ---------------------------------------------------------------------
63 # vrm-0.3.py - 2006-05-19
64 # * First release after code restucturing.
65 # Now the script offers a useful set of functionalities
66 # and it can render animations, too.
68 # ---------------------------------------------------------------------
71 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera
72 from Blender.Mathutils import *
76 # Some global settings
79 SHOW_HIDDEN_EDGES = False
83 POLYGON_EXPANSION_TRICK = True
85 RENDER_ANIMATION = False
87 # Do not work for now!
88 OPTIMIZE_FOR_SPACE = False
91 # ---------------------------------------------------------------------
93 ## Projections classes
95 # ---------------------------------------------------------------------
98 """Calculate the projection of an object given the camera.
100 A projector is useful to so some per-object transformation to obtain the
101 projection of an object given the camera.
103 The main method is #doProjection# see the method description for the
107 def __init__(self, cameraObj, canvasRatio):
108 """Calculate the projection matrix.
110 The projection matrix depends, in this case, on the camera settings.
111 TAKE CARE: This projector expects vertices in World Coordinates!
114 camera = cameraObj.getData()
116 aspect = float(canvasRatio[0])/float(canvasRatio[1])
117 near = camera.clipStart
120 scale = float(camera.scale)
122 fovy = atan(0.5/aspect/(camera.lens/32))
123 fovy = fovy * 360.0/pi
125 # What projection do we want?
127 #mP = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale)
128 mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale)
130 mP = self._calcPerspectiveMatrix(fovy, aspect, near, far)
133 # View transformation
134 cam = Matrix(cameraObj.getInverseMatrix())
139 self.projectionMatrix = mP
145 def doProjection(self, v):
146 """Project the point on the view plane.
148 Given a vertex calculate the projection using the current projection
152 # Note that we have to work on the vertex using homogeneous coordinates
153 p = self.projectionMatrix * Vector(v).resize4D()
169 def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
170 """Return a perspective projection matrix.
173 top = near * tan(fovy * pi / 360.0)
177 x = (2.0 * near) / (right-left)
178 y = (2.0 * near) / (top-bottom)
179 a = (right+left) / (right-left)
180 b = (top+bottom) / (top - bottom)
181 c = - ((far+near) / (far-near))
182 d = - ((2*far*near)/(far-near))
188 [0.0, 0.0, -1.0, 0.0])
192 def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
193 """Return an orthogonal projection matrix.
196 # The 11 in the formula was found emiprically
197 top = near * tan(fovy * pi / 360.0) * (scale * 11)
199 left = bottom * aspect
204 tx = -((right+left)/rl)
205 ty = -((top+bottom)/tb)
209 [2.0/rl, 0.0, 0.0, tx],
210 [0.0, 2.0/tb, 0.0, ty],
211 [0.0, 0.0, 2.0/fn, tz],
212 [0.0, 0.0, 0.0, 1.0])
217 # ---------------------------------------------------------------------
219 ## 2DObject representation class
221 # ---------------------------------------------------------------------
223 # TODO: a class to represent the needed properties of a 2D vector image
224 # For now just using a [N]Mesh structure.
227 # ---------------------------------------------------------------------
229 ## Vector Drawing Classes
231 # ---------------------------------------------------------------------
237 A class for printing output in a vectorial format.
239 Given a 2D representation of the 3D scene the class is responsible to
240 write it is a vector format.
242 Every subclasses of VectorWriter must have at last the following public
246 - printCanvas(self, scene,
247 doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
250 def __init__(self, fileName):
251 """Set the output file name and other properties"""
253 self.outputFileName = fileName
256 context = Scene.GetCurrent().getRenderingContext()
257 self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
261 self.animation = False
268 def open(self, startFrame=1, endFrame=1):
269 if startFrame != endFrame:
270 self.startFrame = startFrame
271 self.endFrame = endFrame
272 self.animation = True
274 self.file = open(self.outputFileName, "w")
275 print "Outputting to: ", self.outputFileName
283 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
284 showHiddenEdges=False):
285 """This is the interface for the needed printing routine.
292 class SVGVectorWriter(VectorWriter):
293 """A concrete class for writing SVG output.
296 def __init__(self, file):
297 """Simply call the parent Contructor.
299 VectorWriter.__init__(self, file)
306 def open(self, startFrame=1, endFrame=1):
307 """Do some initialization operations.
309 VectorWriter.open(self, startFrame, endFrame)
313 """Do some finalization operation.
318 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
319 showHiddenEdges=False):
320 """Convert the scene representation to SVG.
323 Objects = scene.getChildren()
325 context = scene.getRenderingContext()
326 framenumber = context.currentFrame()
329 framestyle = "display:none"
331 framestyle = "display:block"
333 # Assign an id to this group so we can set properties on it using DOM
334 self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
335 (framenumber, framestyle) )
339 if(obj.getType() != 'Mesh'):
342 self.file.write("<g id=\"%s\">\n" % obj.getName())
344 mesh = obj.getData(mesh=1)
347 self._printPolygons(mesh)
350 self._printEdges(mesh, showHiddenEdges)
352 self.file.write("</g>\n")
354 self.file.write("</g>\n")
361 def _calcCanvasCoord(self, v):
362 """Convert vertex in scene coordinates to canvas coordinates.
365 pt = Vector([0, 0, 0])
367 mW = float(self.canvasSize[0])/2.0
368 mH = float(self.canvasSize[1])/2.0
370 # rescale to canvas size
371 pt[0] = v.co[0]*mW + mW
372 pt[1] = v.co[1]*mH + mH
375 # For now we want (0,0) in the top-left corner of the canvas.
376 # Mirror and translate along y
378 pt[1] += self.canvasSize[1]
382 def _printHeader(self):
383 """Print SVG header."""
385 self.file.write("<?xml version=\"1.0\"?>\n")
386 self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n")
387 self.file.write("\t\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
388 self.file.write("<svg version=\"1.1\"\n")
389 self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
390 self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
395 self.file.write("""\n<script><![CDATA[
399 /* FIXME: Use 1000 as interval as lower values gives problems */
400 timerID = setInterval("NextFrame()", 1000);
401 globalFrameCounter=%d;
405 currentElement = document.getElementById('frame'+globalFrameCounter)
406 previousElement = document.getElementById('frame'+(globalFrameCounter-1))
413 if (globalFrameCounter > globalEndFrame)
415 clearInterval(timerID)
421 previousElement.style.display="none";
423 currentElement.style.display="block";
424 globalFrameCounter++;
428 \n""" % (self.startFrame, self.endFrame, self.startFrame) )
430 def _printFooter(self):
431 """Print the SVG footer."""
433 self.file.write("\n</svg>\n")
435 def _printPolygons(self, mesh):
436 """Print the selected (visible) polygons.
439 if len(mesh.faces) == 0:
442 self.file.write("<g>\n")
444 for face in mesh.faces:
448 self.file.write("<polygon points=\"")
451 p = self._calcCanvasCoord(v)
452 self.file.write("%g,%g " % (p[0], p[1]))
454 # get rid of the last blank space, just cosmetics here.
455 self.file.seek(-1, 1)
456 self.file.write("\"\n")
458 # take as face color the first vertex color
459 # TODO: the average of vetrex colors?
462 color = [fcol.r, fcol.g, fcol.b]
464 color = [255, 255, 255]
466 # use the stroke property to alleviate the "adjacent edges" problem,
467 # we simulate polygon expansion using borders,
468 # see http://www.antigrain.com/svg/index.html for more info
472 self.file.write("\tstyle=\"fill:rgb("+str(color[0])+","+str(color[1])+","+str(color[2])+");")
473 if POLYGON_EXPANSION_TRICK:
474 self.file.write(" stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
475 self.file.write(" stroke-width:"+str(stroke_width)+";\n")
476 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
477 self.file.write("\"/>\n")
479 self.file.write("</g>\n")
481 def _printEdges(self, mesh, showHiddenEdges=False):
482 """Print the wireframe using mesh edges.
485 stroke_width=EDGES_WIDTH
486 stroke_col = [0, 0, 0]
488 self.file.write("<g>\n")
492 hidden_stroke_style = ""
494 # Consider an edge selected if both vertices are selected
495 if e.v1.sel == 0 or e.v2.sel == 0:
496 if showHiddenEdges == False:
499 hidden_stroke_style = ";\n stroke-dasharray:3, 3"
501 p1 = self._calcCanvasCoord(e.v1)
502 p2 = self._calcCanvasCoord(e.v2)
504 self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
505 % ( p1[0], p1[1], p2[0], p2[1] ) )
506 self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
507 self.file.write(" stroke-width:"+str(stroke_width)+";\n")
508 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
509 self.file.write(hidden_stroke_style)
510 self.file.write("\"/>\n")
512 self.file.write("</g>\n")
516 # ---------------------------------------------------------------------
520 # ---------------------------------------------------------------------
523 """Render a scene viewed from a given camera.
525 This class is responsible of the rendering process, transformation and
526 projection of the objects in the scene are invoked by the renderer.
528 The rendering is done using the active camera for the current scene.
532 """Make the rendering process only for the current scene by default.
534 We will work on a copy of the scene, be sure that the current scene do
535 not get modified in any way.
538 # Render the current Scene, this should be a READ-ONLY property
539 self._SCENE = Scene.GetCurrent()
541 # Use the aspect ratio of the scene rendering context
542 context = self._SCENE.getRenderingContext()
544 aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
545 self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
546 float(context.aspectRatioY())
549 # Render from the currently active camera
550 self.cameraObj = self._SCENE.getCurrentCamera()
552 # Get the list of lighting sources
553 obj_lst = self._SCENE.getChildren()
554 self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
556 if len(self.lights) == 0:
558 lobj = Object.New('Lamp')
560 self.lights.append(lobj)
567 def doRendering(self, outputWriter, animation=False):
568 """Render picture or animation and write it out.
571 - a Vector writer object than will be used to output the result.
572 - a flag to tell if we want to render an animation or only the
576 context = self._SCENE.getRenderingContext()
577 currentFrame = context.currentFrame()
579 # Handle the animation case
581 startFrame = currentFrame
582 endFrame = startFrame
585 startFrame = context.startFrame()
586 endFrame = context.endFrame()
587 outputWriter.open(startFrame, endFrame)
589 # Do the rendering process frame by frame
590 print "Start Rendering!"
591 for f in range(startFrame, endFrame+1):
592 context.currentFrame(f)
594 renderedScene = self.doRenderScene(self._SCENE)
595 outputWriter.printCanvas(renderedScene,
596 doPrintPolygons = PRINT_POLYGONS,
597 doPrintEdges = PRINT_EDGES,
598 showHiddenEdges = SHOW_HIDDEN_EDGES)
600 # clear the rendered scene
601 self._SCENE.makeCurrent()
602 Scene.unlink(renderedScene)
607 context.currentFrame(currentFrame)
610 def doRenderScene(self, inputScene):
611 """Control the rendering process.
613 Here we control the entire rendering process invoking the operation
614 needed to transform and project the 3D scene in two dimensions.
617 # Use some temporary workspace, a full copy of the scene
618 workScene = inputScene.copy(2)
620 # Get a projector for this scene.
621 # NOTE: the projector wants object in world coordinates,
622 # so we should apply modelview transformations _before_
623 # projection transformations
624 proj = Projector(self.cameraObj, self.canvasRatio)
627 # Convert geometric object types to mesh Objects
628 geometricObjTypes = ['Mesh', 'Surf', 'Curve'] # TODO: add the Text type
629 Objects = workScene.getChildren()
630 objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
633 obj = self._convertToRawMeshObj(obj)
635 workScene.unlink(old_obj)
638 # FIXME: does not work!!, Blender segfaults on joins
639 if OPTIMIZE_FOR_SPACE:
640 self._joinMeshObjectsInScene(workScene)
643 # global processing of the scene
646 self._doSceneDepthSorting(workScene)
648 # Per object activities
649 Objects = workScene.getChildren()
653 if obj.getType() not in geometricObjTypes:
654 print "Only geometric Objects supported! - Skipping type:", obj.getType()
657 print "Rendering: ", obj.getName()
661 self._doModelToWorldCoordinates(mesh, obj.matrix)
663 self._doObjectDepthSorting(mesh)
665 self._doBackFaceCulling(mesh)
667 self._doColorAndLighting(mesh)
669 # TODO: 'style' can be a function that determine
670 # if an edge should be showed?
671 self._doEdgesStyle(mesh, style=None)
673 self._doProjection(mesh, proj)
675 # Update the object data, important! :)
687 def _worldPosition(self, obj):
688 """Return the obj position in World coordinates.
690 return obj.matrix.translationPart()
692 def _cameraWorldPosition(self):
693 """Return the camera position in World coordinates.
695 This trick is needed when the camera follows a path and then
696 camera.loc does not correspond to the current real position of the
699 return self._worldPosition(self.cameraObj)
704 def _isFaceVisible(self, face):
705 """Determine if a face of an object is visible from the current camera.
707 The view vector is calculated from the camera location and one of the
708 vertices of the face (expressed in World coordinates, after applying
709 modelview transformations).
711 After those transformations we determine if a face is visible by
712 computing the angle between the face normal and the view vector, this
713 angle has to be between -90 and 90 degrees for the face to be visible.
714 This corresponds somehow to the dot product between the two, if it
715 results > 0 then the face is visible.
717 There is no need to normalize those vectors since we are only interested in
718 the sign of the cross product and not in the product value.
720 NOTE: here we assume the face vertices are in WorldCoordinates, so
721 please transform the object _before_ doing the test.
724 normal = Vector(face.no)
725 c = self._cameraWorldPosition()
727 # View vector in orthographics projections can be considered simply as the
729 view_vect = Vector(c)
730 #if self.cameraObj.data.getType() == 1:
731 # view_vect = Vector(c)
733 # View vector as in perspective projections
734 # it is the difference between the camera position and one point of
735 # the face, we choose the farthest point.
736 # TODO: make the code more pythonic :)
737 if self.cameraObj.data.getType() == 0:
740 vv = Vector(c) - Vector(vect.co)
741 if vv.length > max_len:
745 # if d > 0 the face is visible from the camera
746 d = view_vect * normal
756 def _doClipping(self):
757 """Clip object against the View Frustum.
759 print "TODO: _doClipping()"
762 def _doSceneDepthSorting(self, scene):
763 """Sort objects in the scene.
765 The object sorting is done accordingly to the object centers.
768 c = self._cameraWorldPosition()
770 Objects = scene.getChildren()
772 #Objects.sort(lambda obj1, obj2:
773 # cmp((Vector(obj1.loc) - Vector(c)).length,
774 # (Vector(obj2.loc) - Vector(c)).length
778 Objects.sort(lambda obj1, obj2:
779 cmp((self._worldPosition(obj1) - Vector(c)).length,
780 (self._worldPosition(obj2) - Vector(c)).length
790 def _joinMeshObjectsInScene(self, scene):
791 """Merge all the Mesh Objects in a scene into a single Mesh Object.
793 bigObj = Object.New('Mesh', 'BigOne')
794 oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
795 print "Before join", oList
805 def _convertToRawMeshObj(self, object):
806 """Convert geometry based object to a mesh object.
808 me = Mesh.New('RawMesh_'+object.name)
809 me.getFromObject(object.name)
811 newObject = Object.New('Mesh', 'RawMesh_'+object.name)
814 newObject.setMatrix(object.getMatrix())
818 def _doModelToWorldCoordinates(self, mesh, matrix):
819 """Transform object coordinates to world coordinates.
821 This step is done simply applying to the object its tranformation
822 matrix and recalculating its normals.
824 mesh.transform(matrix, True)
826 def _doObjectDepthSorting(self, mesh):
827 """Sort faces in an object.
829 The faces in the object are sorted following the distance of the
830 vertices from the camera position.
832 c = self._cameraWorldPosition()
834 # hackish sorting of faces
837 # Sort faces according to the min distance from the camera
838 #cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]),
839 # min([(Vector(v.co)-Vector(c)).length for v in f2])))
841 # Sort faces according to the max distance from the camera
842 cmp(max([(Vector(v.co)-Vector(c)).length for v in f1]),
843 max([(Vector(v.co)-Vector(c)).length for v in f2])))
845 # Sort faces according to the avg distance from the camera
846 #cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1),
847 # sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2)))
851 def _doBackFaceCulling(self, mesh):
852 """Simple Backface Culling routine.
854 At this level we simply do a visibility test face by face and then
855 select the vertices belonging to visible faces.
858 # Select all vertices, so edges without faces can be displayed
862 Mesh.Mode(Mesh.SelectModes['FACE'])
866 if self._isFaceVisible(f):
869 # Is this the correct way to propagate the face selection info to the
870 # vertices belonging to a face ??
871 # TODO: Using the Mesh class this should come for free. Right?
872 Mesh.Mode(Mesh.SelectModes['VERTEX'])
883 def _doColorAndLighting(self, mesh):
884 """Apply an Illumination model to the object.
886 The Illumination model used is the Phong one, it may be inefficient,
887 but I'm just learning about rendering and starting from Phong seemed
888 the most natural way.
891 # If the mesh has vertex colors already, use them,
892 # otherwise turn them on and do some calculations
893 if mesh.hasVertexColours():
895 mesh.hasVertexColours(True)
897 materials = mesh.materials
899 # TODO: use multiple lighting sources
900 light_obj = self.lights[0]
901 light_pos = self._worldPosition(light_obj)
902 light = light_obj.data
904 camPos = self._cameraWorldPosition()
906 # We do per-face color calculation (FLAT Shading), we can easily turn
907 # to a per-vertex calculation if we want to implement some shading
908 # technique. For an example see:
909 # http://www.miralab.unige.ch/papers/368.pdf
916 mat = materials[f.mat]
918 # A new default material
920 mat = Material.New('defMat')
922 L = Vector(light_pos).normalize()
924 V = (Vector(camPos) - Vector(f.v[0].co)).normalize()
926 N = Vector(f.no).normalize()
928 R = 2 * (N*L) * N - L
930 # TODO: Attenuation factor (not used for now)
931 a0 = 1; a1 = 0.0; a2 = 0.0
932 d = (Vector(f.v[0].co) - Vector(light_pos)).length
933 fd = min(1, 1.0/(a0 + a1*d + a2*d*d))
937 ka = mat.getAmb() * Vector([0.1, 0.1, 0.1])
940 # Diffuse component (add light.col for kd)
941 kd = mat.getRef() * Vector(mat.getRGBCol())
942 Ip = light.getEnergy()
943 Idiff = Ip * kd * (N*L)
946 ks = mat.getSpec() * Vector(mat.getSpecCol())
947 ns = mat.getHardness()
948 Ispec = Ip * ks * pow((V * R), ns)
951 ki = Vector([mat.getEmit()]*3)
953 I = ki + Iamb + Idiff + Ispec
955 # Clamp I values between 0 and 1
956 I = [ min(c, 1) for c in I]
957 I = [ max(0, c) for c in I]
958 tmp_col = [ int(c * 255.0) for c in I]
960 vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2], 255)
965 def _doEdgesStyle(self, mesh, style):
966 """Process Mesh Edges. (For now copy the edge data, in next version it
967 can be a place where recognize silouhettes and/or contours).
970 return: a processed edge list
972 #print "\tTODO: _doEdgeStyle()"
975 def _doProjection(self, mesh, projector):
976 """Calculate the Projection for the object.
978 # TODO: maybe using the object.transform() can be faster?
981 p = projector.doProjection(v.co)
988 # ---------------------------------------------------------------------
992 # ---------------------------------------------------------------------
994 def vectorize(filename):
995 """The vectorizing process is as follows:
997 - Instanciate the writer and the renderer
1000 from Blender import Window
1001 editmode = Window.EditMode()
1002 if editmode: Window.EditMode(0)
1004 writer = SVGVectorWriter(filename)
1006 renderer = Renderer()
1007 renderer.doRendering(writer, RENDER_ANIMATION)
1009 if editmode: Window.EditMode(1)
1011 def vectorize_gui(filename):
1014 I would like to keep that simple, really.
1016 Blender.Window.FileSelector (vectorize, 'Save SVG', filename)
1021 if __name__ == "__main__":
1024 outputfile = os.path.splitext(Blender.Get('filename'))[0]+".svg"
1026 # with this trick we can run the script in batch mode
1028 vectorize_gui(outputfile)
1030 vectorize(outputfile)