6 Tooltip: 'Vector Rendering Method script'
9 __author__ = "Antonio Ospite"
10 __url__ = ["http://projects.blender.org/projects/vrm"]
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 # - Use multiple lighting sources in color calculation,
46 # (this is part of the "shading refactor") and use light color!
47 # - FIX the issue with negative scales in object tranformations!
48 # - Use a better depth sorting algorithm
49 # - Implement clipping of primitives and do handle object intersections.
50 # (for now only clipping away whole objects is supported).
51 # - Review how selections are made (this script uses selection states of
52 # primitives to represent visibility infos)
53 # - Use a data structure other than Mesh to represent the 2D image?
54 # Think to a way to merge (adjacent) polygons that have the same color.
55 # Or a way to use paths for silhouettes and contours.
56 # - Consider SMIL for animation handling instead of ECMA Script? (Firefox do
57 # not support SMIL for animations)
58 # - Switch to the Mesh structure, should be considerably faster
59 # (partially done, but with Mesh we cannot sort faces, yet)
60 # - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
61 # - Implement Shading Styles? (for now we use Flat Shading) (partially done).
62 # - Add Vector Writers other than SVG.
64 # ---------------------------------------------------------------------
68 # vrm-0.3.py - 2006-05-19
69 # * First release after code restucturing.
70 # Now the script offers a useful set of functionalities
71 # and it can render animations, too.
73 # ---------------------------------------------------------------------
76 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera
77 from Blender.Mathutils import *
81 # Some global settings
85 polygons['SHOW'] = True
86 polygons['SHADING'] = 'TOON'
87 # Hidden to the user for now
88 polygons['EXPANSION_TRICK'] = True
92 edges['SHOW_HIDDEN'] = False
93 edges['STYLE'] = 'SILHOUETTE'
95 edges['COLOR'] = [0, 0, 0]
98 output['FORMAT'] = 'SVG'
99 output['ANIMATION'] = False
100 output['JOIN_OBJECTS'] = True
104 # ---------------------------------------------------------------------
106 ## Utility Mesh class
108 # ---------------------------------------------------------------------
111 def getEdgeAdjacentFaces(edge, mesh):
112 """Get the faces adjacent to a given edge.
114 There can be 0, 1 or more (usually 2) faces adjacent to an edge.
119 if (edge.v1 in f.v) and (edge.v2 in f.v):
120 adjface_list.append(f)
124 def isMeshEdge(e, mesh):
127 A mesh edge is visible if _any_ of its adjacent faces is selected.
128 Note: if the edge has no adjacent faces we want to show it as well,
129 useful for "edge only" portion of objects.
132 adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
134 if len(adjacent_faces) == 0:
137 selected_faces = [f for f in adjacent_faces if f.sel]
139 if len(selected_faces) != 0:
144 def isSilhouetteEdge(e, mesh):
145 """Silhuette selection rule.
147 An edge is a silhuette edge if it is shared by two faces with
148 different selection status or if it is a boundary edge of a selected
152 adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
154 if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
155 (len(adjacent_faces) == 2 and
156 adjacent_faces[0].sel != adjacent_faces[1].sel)
165 texels = 2*levels - 1
166 map = [0.0] + [(i)/float(texels-1) for i in range(1, texels-1) ] + [1.0]
169 for i in range(0, len(map)-1):
170 pivot = (map[i]+map[i+1])/2.0
181 getEdgeAdjacentFaces = staticmethod(getEdgeAdjacentFaces)
182 isMeshEdge = staticmethod(isMeshEdge)
183 isSilhouetteEdge = staticmethod(isSilhouetteEdge)
184 toonShading = staticmethod(toonShading)
188 # ---------------------------------------------------------------------
190 ## Projections classes
192 # ---------------------------------------------------------------------
195 """Calculate the projection of an object given the camera.
197 A projector is useful to so some per-object transformation to obtain the
198 projection of an object given the camera.
200 The main method is #doProjection# see the method description for the
204 def __init__(self, cameraObj, canvasRatio):
205 """Calculate the projection matrix.
207 The projection matrix depends, in this case, on the camera settings.
208 TAKE CARE: This projector expects vertices in World Coordinates!
211 camera = cameraObj.getData()
213 aspect = float(canvasRatio[0])/float(canvasRatio[1])
214 near = camera.clipStart
217 scale = float(camera.scale)
219 fovy = atan(0.5/aspect/(camera.lens/32))
220 fovy = fovy * 360.0/pi
222 # What projection do we want?
224 mP = self._calcPerspectiveMatrix(fovy, aspect, near, far)
225 elif camera.type == 1:
226 mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale)
228 # View transformation
229 cam = Matrix(cameraObj.getInverseMatrix())
234 self.projectionMatrix = mP
240 def doProjection(self, v):
241 """Project the point on the view plane.
243 Given a vertex calculate the projection using the current projection
247 # Note that we have to work on the vertex using homogeneous coordinates
248 p = self.projectionMatrix * Vector(v).resize4D()
250 # Perspective division
267 def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
268 """Return a perspective projection matrix.
271 top = near * tan(fovy * pi / 360.0)
275 x = (2.0 * near) / (right-left)
276 y = (2.0 * near) / (top-bottom)
277 a = (right+left) / (right-left)
278 b = (top+bottom) / (top - bottom)
279 c = - ((far+near) / (far-near))
280 d = - ((2*far*near)/(far-near))
286 [0.0, 0.0, -1.0, 0.0])
290 def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
291 """Return an orthogonal projection matrix.
294 # The 11 in the formula was found emiprically
295 top = near * tan(fovy * pi / 360.0) * (scale * 11)
297 left = bottom * aspect
302 tx = -((right+left)/rl)
303 ty = -((top+bottom)/tb)
307 [2.0/rl, 0.0, 0.0, tx],
308 [0.0, 2.0/tb, 0.0, ty],
309 [0.0, 0.0, 2.0/fn, tz],
310 [0.0, 0.0, 0.0, 1.0])
316 # ---------------------------------------------------------------------
318 ## 2D Object representation class
320 # ---------------------------------------------------------------------
322 # TODO: a class to represent the needed properties of a 2D vector image
323 # For now just using a [N]Mesh structure.
326 # ---------------------------------------------------------------------
328 ## Vector Drawing Classes
330 # ---------------------------------------------------------------------
336 A class for printing output in a vectorial format.
338 Given a 2D representation of the 3D scene the class is responsible to
339 write it is a vector format.
341 Every subclasses of VectorWriter must have at last the following public
345 - printCanvas(self, scene,
346 doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
349 def __init__(self, fileName):
350 """Set the output file name and other properties"""
352 self.outputFileName = fileName
355 context = Scene.GetCurrent().getRenderingContext()
356 self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
360 self.animation = False
367 def open(self, startFrame=1, endFrame=1):
368 if startFrame != endFrame:
369 self.startFrame = startFrame
370 self.endFrame = endFrame
371 self.animation = True
373 self.file = open(self.outputFileName, "w")
374 print "Outputting to: ", self.outputFileName
382 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
383 showHiddenEdges=False):
384 """This is the interface for the needed printing routine.
391 class SVGVectorWriter(VectorWriter):
392 """A concrete class for writing SVG output.
395 def __init__(self, fileName):
396 """Simply call the parent Contructor.
398 VectorWriter.__init__(self, fileName)
405 def open(self, startFrame=1, endFrame=1):
406 """Do some initialization operations.
408 VectorWriter.open(self, startFrame, endFrame)
412 """Do some finalization operation.
416 # remember to call the close method of the parent
417 VectorWriter.close(self)
420 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
421 showHiddenEdges=False):
422 """Convert the scene representation to SVG.
425 Objects = scene.getChildren()
427 context = scene.getRenderingContext()
428 framenumber = context.currentFrame()
431 framestyle = "display:none"
433 framestyle = "display:block"
435 # Assign an id to this group so we can set properties on it using DOM
436 self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
437 (framenumber, framestyle) )
442 if(obj.getType() != 'Mesh'):
445 self.file.write("<g id=\"%s\">\n" % obj.getName())
447 mesh = obj.getData(mesh=1)
450 self._printPolygons(mesh)
453 self._printEdges(mesh, showHiddenEdges)
455 self.file.write("</g>\n")
457 self.file.write("</g>\n")
464 def _calcCanvasCoord(self, v):
465 """Convert vertex in scene coordinates to canvas coordinates.
468 pt = Vector([0, 0, 0])
470 mW = float(self.canvasSize[0])/2.0
471 mH = float(self.canvasSize[1])/2.0
473 # rescale to canvas size
474 pt[0] = v.co[0]*mW + mW
475 pt[1] = v.co[1]*mH + mH
478 # For now we want (0,0) in the top-left corner of the canvas.
479 # Mirror and translate along y
481 pt[1] += self.canvasSize[1]
485 def _printHeader(self):
486 """Print SVG header."""
488 self.file.write("<?xml version=\"1.0\"?>\n")
489 self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n")
490 self.file.write("\t\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
491 self.file.write("<svg version=\"1.1\"\n")
492 self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
493 self.file.write("\twidth=\"%d\" height=\"%d\" streamable=\"true\">\n\n" %
498 self.file.write("""\n<script><![CDATA[
502 /* FIXME: Use 1000 as interval as lower values gives problems */
503 timerID = setInterval("NextFrame()", 1000);
504 globalFrameCounter=%d;
508 currentElement = document.getElementById('frame'+globalFrameCounter)
509 previousElement = document.getElementById('frame'+(globalFrameCounter-1))
516 if (globalFrameCounter > globalEndFrame)
518 clearInterval(timerID)
524 previousElement.style.display="none";
526 currentElement.style.display="block";
527 globalFrameCounter++;
531 \n""" % (self.startFrame, self.endFrame, self.startFrame) )
533 def _printFooter(self):
534 """Print the SVG footer."""
536 self.file.write("\n</svg>\n")
538 def _printPolygons(self, mesh):
539 """Print the selected (visible) polygons.
542 if len(mesh.faces) == 0:
545 self.file.write("<g>\n")
547 for face in mesh.faces:
551 self.file.write("<path d=\"")
553 p = self._calcCanvasCoord(face.verts[0])
554 self.file.write("M %g,%g L " % (p[0], p[1]))
556 for v in face.verts[1:]:
557 p = self._calcCanvasCoord(v)
558 self.file.write("%g,%g " % (p[0], p[1]))
560 # get rid of the last blank space, just cosmetics here.
561 self.file.seek(-1, 1)
562 self.file.write("\"\n")
564 # take as face color the first vertex color
565 # TODO: the average of vetrex colors?
568 color = [fcol.r, fcol.g, fcol.b, fcol.a]
570 color = [255, 255, 255, 255]
572 # Convert the color to the #RRGGBB form
573 str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
575 # use the stroke property to alleviate the "adjacent edges" problem,
576 # we simulate polygon expansion using borders,
577 # see http://www.antigrain.com/svg/index.html for more info
580 # Handle transparent polygons
583 opacity = float(color[3])/255.0
584 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
586 self.file.write("\tstyle=\"fill:" + str_col + ";")
587 self.file.write(opacity_string)
588 if config.polygons['EXPANSION_TRICK']:
589 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
590 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
591 self.file.write("\"/>\n")
593 self.file.write("</g>\n")
595 def _printEdges(self, mesh, showHiddenEdges=False):
596 """Print the wireframe using mesh edges.
599 stroke_width = config.edges['WIDTH']
600 stroke_col = config.edges['COLOR']
602 self.file.write("<g>\n")
606 hidden_stroke_style = ""
609 if showHiddenEdges == False:
612 hidden_stroke_style = ";\n stroke-dasharray:3, 3"
614 p1 = self._calcCanvasCoord(e.v1)
615 p2 = self._calcCanvasCoord(e.v2)
617 self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
618 % ( p1[0], p1[1], p2[0], p2[1] ) )
619 self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
620 self.file.write(" stroke-width:"+str(stroke_width)+";\n")
621 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
622 self.file.write(hidden_stroke_style)
623 self.file.write("\"/>\n")
625 self.file.write("</g>\n")
629 # ---------------------------------------------------------------------
633 # ---------------------------------------------------------------------
635 # A dictionary to collect different shading style methods
636 shadingStyles = dict()
637 shadingStyles['FLAT'] = None
638 shadingStyles['TOON'] = None
640 # A dictionary to collect different edge style methods
642 edgeStyles['MESH'] = MeshUtils.isMeshEdge
643 edgeStyles['SILHOUETTE'] = MeshUtils.isSilhouetteEdge
645 # A dictionary to collect the supported output formats
646 outputWriters = dict()
647 outputWriters['SVG'] = SVGVectorWriter
651 """Render a scene viewed from a given camera.
653 This class is responsible of the rendering process, transformation and
654 projection of the objects in the scene are invoked by the renderer.
656 The rendering is done using the active camera for the current scene.
660 """Make the rendering process only for the current scene by default.
662 We will work on a copy of the scene, be sure that the current scene do
663 not get modified in any way.
666 # Render the current Scene, this should be a READ-ONLY property
667 self._SCENE = Scene.GetCurrent()
669 # Use the aspect ratio of the scene rendering context
670 context = self._SCENE.getRenderingContext()
672 aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
673 self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
674 float(context.aspectRatioY())
677 # Render from the currently active camera
678 self.cameraObj = self._SCENE.getCurrentCamera()
680 # Get a projector for this camera.
681 # NOTE: the projector wants object in world coordinates,
682 # so we should remember to apply modelview transformations
683 # _before_ we do projection transformations.
684 self.proj = Projector(self.cameraObj, self.canvasRatio)
686 # Get the list of lighting sources
687 obj_lst = self._SCENE.getChildren()
688 self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
690 # When there are no lights we use a default lighting source
691 # that have the same position of the camera
692 if len(self.lights) == 0:
694 lobj = Object.New('Lamp')
695 lobj.loc = self.cameraObj.loc
697 self.lights.append(lobj)
704 def doRendering(self, outputWriter, animation=False):
705 """Render picture or animation and write it out.
708 - a Vector writer object that will be used to output the result.
709 - a flag to tell if we want to render an animation or only the
713 context = self._SCENE.getRenderingContext()
714 origCurrentFrame = context.currentFrame()
716 # Handle the animation case
718 startFrame = origCurrentFrame
719 endFrame = startFrame
722 startFrame = context.startFrame()
723 endFrame = context.endFrame()
724 outputWriter.open(startFrame, endFrame)
726 # Do the rendering process frame by frame
727 print "Start Rendering!"
728 for f in range(startFrame, endFrame+1):
729 context.currentFrame(f)
731 # Use some temporary workspace, a full copy of the scene
732 inputScene = self._SCENE.copy(2)
733 # And Set our camera accordingly
734 self.cameraObj = inputScene.getCurrentCamera()
737 renderedScene = self.doRenderScene(inputScene)
739 print "There was an error! Aborting."
741 print traceback.print_exc()
743 self._SCENE.makeCurrent()
744 Scene.unlink(inputScene)
748 outputWriter.printCanvas(renderedScene,
749 doPrintPolygons = config.polygons['SHOW'],
750 doPrintEdges = config.edges['SHOW'],
751 showHiddenEdges = config.edges['SHOW_HIDDEN'])
753 # clear the rendered scene
754 self._SCENE.makeCurrent()
755 #Scene.unlink(renderedScene)
760 context.currentFrame(origCurrentFrame)
763 def doRenderScene(self, workScene):
764 """Control the rendering process.
766 Here we control the entire rendering process invoking the operation
767 needed to transform and project the 3D scene in two dimensions.
770 # global processing of the scene
772 self._doSceneClipping(workScene)
774 self._doConvertGeometricObjToMesh(workScene)
776 if config.output['JOIN_OBJECTS']:
777 self._joinMeshObjectsInScene(workScene)
779 self._doSceneDepthSorting(workScene)
781 # Per object activities
783 Objects = workScene.getChildren()
786 if obj.getType() != 'Mesh':
787 print "Only Mesh supported! - Skipping type:", obj.getType()
790 print "Rendering: ", obj.getName()
792 mesh = obj.getData(mesh=1)
794 self._doModelingTransformation(mesh, obj.matrix)
796 self._doBackFaceCulling(mesh)
798 self._doPerVertexLighting(mesh)
800 # Do "projection" now so we perform further processing
801 # in Normalized View Coordinates
802 self._doProjection(mesh, self.proj)
804 self._doViewFrustumClipping(mesh)
806 self._doMeshDepthSorting(mesh)
808 self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
811 # Update the object data, important! :)
823 def _getObjPosition(self, obj):
824 """Return the obj position in World coordinates.
826 return obj.matrix.translationPart()
828 def _cameraViewVector(self):
829 """Get the View Direction form the camera matrix.
831 return Vector(self.cameraObj.matrix[2]).resize3D()
836 def _isFaceVisible(self, face):
837 """Determine if a face of an object is visible from the current camera.
839 The view vector is calculated from the camera location and one of the
840 vertices of the face (expressed in World coordinates, after applying
841 modelview transformations).
843 After those transformations we determine if a face is visible by
844 computing the angle between the face normal and the view vector, this
845 angle has to be between -90 and 90 degrees for the face to be visible.
846 This corresponds somehow to the dot product between the two, if it
847 results > 0 then the face is visible.
849 There is no need to normalize those vectors since we are only interested in
850 the sign of the cross product and not in the product value.
852 NOTE: here we assume the face vertices are in WorldCoordinates, so
853 please transform the object _before_ doing the test.
856 normal = Vector(face.no)
857 camPos = self._getObjPosition(self.cameraObj)
860 # View Vector in orthographics projections is the view Direction of
862 if self.cameraObj.data.getType() == 1:
863 view_vect = self._cameraViewVector()
865 # View vector in perspective projections can be considered as
866 # the difference between the camera position and one point of
867 # the face, we choose the farthest point from the camera.
868 if self.cameraObj.data.getType() == 0:
869 vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
873 # if d > 0 the face is visible from the camera
874 d = view_vect * normal
884 def _doSceneClipping(self, scene):
885 """Clip whole objects against the View Frustum.
887 For now clip away only objects according to their center position.
890 cpos = self._getObjPosition(self.cameraObj)
891 view_vect = self._cameraViewVector()
893 near = self.cameraObj.data.clipStart
894 far = self.cameraObj.data.clipEnd
896 aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
897 fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
898 fovy = fovy * 360.0/pi
900 Objects = scene.getChildren()
902 if o.getType() != 'Mesh': continue;
904 obj_vect = Vector(cpos) - self._getObjPosition(o)
906 d = obj_vect*view_vect
907 theta = AngleBetweenVecs(obj_vect, view_vect)
909 # if the object is outside the view frustum, clip it away
910 if (d < near) or (d > far) or (theta > fovy):
913 def _doConvertGeometricObjToMesh(self, scene):
914 """Convert all "geometric" objects to mesh ones.
916 geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
918 Objects = scene.getChildren()
919 objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
922 obj = self._convertToRawMeshObj(obj)
924 scene.unlink(old_obj)
927 # XXX Workaround for Text and Curve which have some normals
928 # inverted when they are converted to Mesh, REMOVE that when
929 # blender will fix that!!
930 if old_obj.getType() in ['Curve', 'Text']:
931 me = obj.getData(mesh=1)
932 for f in me.faces: f.sel = 1;
933 for v in me.verts: v.sel = 1;
940 def _doSceneDepthSorting(self, scene):
941 """Sort objects in the scene.
943 The object sorting is done accordingly to the object centers.
946 c = self._getObjPosition(self.cameraObj)
948 by_center_pos = (lambda o1, o2:
949 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
950 cmp((self._getObjPosition(o1) - Vector(c)).length,
951 (self._getObjPosition(o2) - Vector(c)).length)
954 # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
955 # then ob1 goes farther than obj2, useful when obj2 has holes
958 Objects = scene.getChildren()
959 Objects.sort(by_center_pos)
966 def _joinMeshObjectsInScene(self, scene):
967 """Merge all the Mesh Objects in a scene into a single Mesh Object.
970 oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
972 # FIXME: Object.join() do not work if the list contains 1 object
976 mesh = Mesh.New('BigOne')
977 bigObj = Object.New('Mesh', 'BigOne')
985 print "\nCan't Join Objects\n"
989 print "Objects Type error?"
999 def _convertToRawMeshObj(self, object):
1000 """Convert geometry based object to a mesh object.
1002 me = Mesh.New('RawMesh_'+object.name)
1003 me.getFromObject(object.name)
1005 newObject = Object.New('Mesh', 'RawMesh_'+object.name)
1008 # If the object has no materials set a default material
1009 if not me.materials:
1010 me.materials = [Material.New()]
1011 #for f in me.faces: f.mat = 0
1013 newObject.setMatrix(object.getMatrix())
1017 def _doModelingTransformation(self, mesh, matrix):
1018 """Transform object coordinates to world coordinates.
1020 This step is done simply applying to the object its tranformation
1021 matrix and recalculating its normals.
1023 # XXX FIXME: blender do not transform normals in the right way when
1024 # there are negative scale values
1025 if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
1026 print "WARNING: Negative scales, expect incorrect results!"
1028 mesh.transform(matrix, True)
1030 def _doBackFaceCulling(self, mesh):
1031 """Simple Backface Culling routine.
1033 At this level we simply do a visibility test face by face and then
1034 select the vertices belonging to visible faces.
1037 # Select all vertices, so edges can be displayed even if there are no
1039 for v in mesh.verts:
1042 Mesh.Mode(Mesh.SelectModes['FACE'])
1044 for f in mesh.faces:
1046 if self._isFaceVisible(f):
1049 def _doPerVertexLighting(self, mesh):
1050 """Apply an Illumination ans shading model to the object.
1052 The model used is the Phong one, it may be inefficient,
1053 but I'm just learning about rendering and starting from Phong seemed
1054 the most natural way.
1057 # If the mesh has vertex colors already, use them,
1058 # otherwise turn them on and do some calculations
1059 if mesh.vertexColors:
1061 mesh.vertexColors = 1
1063 materials = mesh.materials
1065 # TODO: use multiple lighting sources
1066 light_obj = self.lights[0]
1067 light_pos = self._getObjPosition(light_obj)
1068 light = light_obj.data
1070 camPos = self._getObjPosition(self.cameraObj)
1072 # We do per-face color calculation (FLAT Shading), we can easily turn
1073 # to a per-vertex calculation if we want to implement some shading
1074 # technique. For an example see:
1075 # http://www.miralab.unige.ch/papers/368.pdf
1076 for f in mesh.faces:
1082 mat = materials[f.mat]
1084 # A new default material
1086 mat = Material.New('defMat')
1088 L = Vector(light_pos).normalize()
1090 V = (Vector(camPos) - Vector(f.cent)).normalize()
1092 N = Vector(f.no).normalize()
1094 R = 2 * (N*L) * N - L
1096 # TODO: Attenuation factor (not used for now)
1097 a0 = 1.0; a1 = 0.0; a2 = 1.0
1098 d = (Vector(f.v[0].co) - Vector(light_pos)).length
1099 fd = min(1, 1.0/(a0 + a1*d + a2*(d*d)))
1103 ka = mat.getAmb() * Vector([0.1, 0.1, 0.1])
1106 # Diffuse component (add light.col for kd)
1107 kd = mat.getRef() * Vector(mat.getRGBCol())
1108 Ip = light.getEnergy()
1110 if config.polygons['SHADING'] == 'FLAT':
1111 Idiff = Ip * kd * max(0, (N*L))
1112 elif config.polygons['SHADING'] == 'TOON':
1113 Idiff = Ip * kd * MeshUtils.toonShading(N*L)
1115 # Specular component
1116 ks = mat.getSpec() * Vector(mat.getSpecCol())
1117 ns = mat.getHardness()
1118 Ispec = Ip * ks * pow(max(0, (V*R)), ns)
1120 # Emissive component
1121 ki = Vector([mat.getEmit()]*3)
1123 I = ki + Iamb + (Idiff + Ispec)
1126 # Set Alpha component
1128 I.append(mat.getAlpha())
1130 # Clamp I values between 0 and 1
1131 I = [ min(c, 1) for c in I]
1132 I = [ max(0, c) for c in I]
1134 # Convert to a value between 0 and 255
1135 tmp_col = [ int(c * 255.0) for c in I]
1143 def _doProjection(self, mesh, projector):
1144 """Apply Viewing and Projection tranformations.
1147 for v in mesh.verts:
1148 p = projector.doProjection(v.co)
1153 # We could reeset Camera matrix, since now
1154 # we are in Normalized Viewing Coordinates,
1155 # but doung that would affect World Coordinate
1156 # processing for other objects
1158 #self.cameraObj.data.type = 1
1159 #self.cameraObj.data.scale = 2.0
1160 #m = Matrix().identity()
1161 #self.cameraObj.setMatrix(m)
1163 def _doViewFrustumClipping(self, mesh):
1164 """Clip faces against the View Frustum.
1167 def test_extensions(self, f1, f2):
1168 for v1, v2 in [ (v1, v2) for v1 in f1 for v2 in f2 ]:
1171 def depth_sort(self, faces):
1175 def _doMeshDepthSorting(self, mesh):
1176 """Sort faces in an object.
1178 The faces in the object are sorted following the distance of the
1179 vertices from the camera position.
1181 if len(mesh.faces) == 0:
1184 #c = self._getObjPosition(self.cameraObj)
1189 # hackish sorting of faces
1191 # Sort faces according to the max distance from the camera
1192 by_max_vert_dist = (lambda f1, f2:
1193 cmp(max([(Vector(v.co)-Vector(c)).length for v in f2]),
1194 max([(Vector(v.co)-Vector(c)).length for v in f1])))
1196 # Sort faces according to the min distance from the camera
1197 by_min_vert_dist = (lambda f1, f2:
1198 cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]),
1199 min([(Vector(v.co)-Vector(c)).length for v in f2])))
1201 # Sort faces according to the avg distance from the camera
1202 by_avg_vert_dist = (lambda f1, f2:
1203 cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1),
1204 sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2)))
1207 # FIXME: using NMesh to sort faces. We should avoid that!
1208 nmesh = NMesh.GetRaw(mesh.name)
1209 nmesh.faces.sort(by_max_vert_dist)
1210 #nmesh.faces.reverse()
1214 self.depth_sort(nmesh.faces)
1217 mesh.faces.delete(1, range(0, len(mesh.faces)))
1219 for i,f in enumerate(nmesh.faces):
1220 fv = [v.index for v in f.v]
1221 mesh.faces.extend(fv)
1222 mesh.faces[i].mat = f.mat
1223 mesh.faces[i].sel = f.sel
1224 for i,c in enumerate(mesh.faces[i].col):
1230 def _doEdgesStyle(self, mesh, edgestyleSelect):
1231 """Process Mesh Edges accroding to a given selection style.
1233 Examples of algorithms:
1236 given an edge if its adjacent faces have the same normal (that is
1237 they are complanar), than deselect it.
1240 given an edge if one its adjacent faces is frontfacing and the
1241 other is backfacing, than select it, else deselect.
1244 Mesh.Mode(Mesh.SelectModes['EDGE'])
1246 for e in mesh.edges:
1249 if edgestyleSelect(e, mesh):
1254 # ---------------------------------------------------------------------
1256 ## GUI Class and Main Program
1258 # ---------------------------------------------------------------------
1261 from Blender import BGL, Draw
1262 from Blender.BGL import *
1268 # Output Format menu
1269 output_format = config.output['FORMAT']
1270 default_value = outputWriters.keys().index(output_format)+1
1271 GUI.outFormatMenu = Draw.Create(default_value)
1272 GUI.evtOutFormatMenu = 0
1274 # Animation toggle button
1275 GUI.animToggle = Draw.Create(config.output['ANIMATION'])
1276 GUI.evtAnimToggle = 1
1278 # Join Objects toggle button
1279 GUI.joinObjsToggle = Draw.Create(config.output['JOIN_OBJECTS'])
1280 GUI.evtJoinObjsToggle = 2
1282 # Render filled polygons
1283 GUI.polygonsToggle = Draw.Create(config.polygons['SHOW'])
1285 # Shading Style menu
1286 shading_style = config.polygons['SHADING']
1287 default_value = shadingStyles.keys().index(shading_style)+1
1288 GUI.shadingStyleMenu = Draw.Create(default_value)
1289 GUI.evtShadingStyleMenu = 21
1291 GUI.evtPolygonsToggle = 3
1292 # We hide the config.polygons['EXPANSION_TRICK'], for now
1294 # Render polygon edges
1295 GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
1296 GUI.evtShowEdgesToggle = 4
1298 # Render hidden edges
1299 GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
1300 GUI.evtShowHiddenEdgesToggle = 5
1303 edge_style = config.edges['STYLE']
1304 default_value = edgeStyles.keys().index(edge_style)+1
1305 GUI.edgeStyleMenu = Draw.Create(default_value)
1306 GUI.evtEdgeStyleMenu = 6
1309 GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
1310 GUI.evtEdgeWidthSlider = 7
1313 c = config.edges['COLOR']
1314 GUI.edgeColorPicker = Draw.Create(c[0]/255.0, c[1]/255.0, c[2]/255.0)
1315 GUI.evtEdgeColorPicker = 71
1318 GUI.evtRenderButton = 8
1321 GUI.evtExitButton = 9
1325 # initialize static members
1328 glClear(GL_COLOR_BUFFER_BIT)
1329 glColor3f(0.0, 0.0, 0.0)
1330 glRasterPos2i(10, 350)
1331 Draw.Text("VRM: Vector Rendering Method script.")
1332 glRasterPos2i(10, 335)
1333 Draw.Text("Press Q or ESC to quit.")
1335 # Build the output format menu
1336 glRasterPos2i(10, 310)
1337 Draw.Text("Select the output Format:")
1338 outMenuStruct = "Output Format %t"
1339 for t in outputWriters.keys():
1340 outMenuStruct = outMenuStruct + "|%s" % t
1341 GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
1342 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
1345 GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
1346 10, 260, 160, 18, GUI.animToggle.val,
1347 "Toggle rendering of animations")
1349 # Join Objects toggle
1350 GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
1351 10, 235, 160, 18, GUI.joinObjsToggle.val,
1352 "Join objects in the rendered file")
1355 Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
1357 Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
1360 glRasterPos2i(200, 310)
1361 Draw.Text("Rendering Style:")
1364 GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
1365 200, 285, 160, 18, GUI.polygonsToggle.val,
1366 "Render filled polygons")
1368 if GUI.polygonsToggle.val == 1:
1370 # Polygon Shading Style
1371 shadingStyleMenuStruct = "Shading Style %t"
1372 for t in shadingStyles.keys():
1373 shadingStyleMenuStruct = shadingStyleMenuStruct + "|%s" % t.lower()
1374 GUI.shadingStyleMenu = Draw.Menu(shadingStyleMenuStruct, GUI.evtShadingStyleMenu,
1375 200, 260, 160, 18, GUI.shadingStyleMenu.val,
1376 "Choose the shading style")
1380 GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
1381 200, 235, 160, 18, GUI.showEdgesToggle.val,
1382 "Render polygon edges")
1384 if GUI.showEdgesToggle.val == 1:
1387 edgeStyleMenuStruct = "Edge Style %t"
1388 for t in edgeStyles.keys():
1389 edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t.lower()
1390 GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
1391 200, 210, 160, 18, GUI.edgeStyleMenu.val,
1392 "Choose the edge style")
1395 GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
1396 200, 185, 140, 18, GUI.edgeWidthSlider.val,
1397 0.0, 10.0, 0, "Change Edge Width")
1400 GUI.edgeColorPicker = Draw.ColorPicker(GUI.evtEdgeColorPicker,
1401 342, 185, 18, 18, GUI.edgeColorPicker.val, "Choose Edge Color")
1404 GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
1405 GUI.evtShowHiddenEdgesToggle,
1406 200, 160, 160, 18, GUI.showHiddenEdgesToggle.val,
1407 "Render hidden edges as dashed lines")
1409 glRasterPos2i(10, 160)
1410 Draw.Text("Antonio Ospite (c) 2006")
1412 def event(evt, val):
1414 if evt == Draw.ESCKEY or evt == Draw.QKEY:
1421 def button_event(evt):
1423 if evt == GUI.evtExitButton:
1426 elif evt == GUI.evtOutFormatMenu:
1427 i = GUI.outFormatMenu.val - 1
1428 config.output['FORMAT']= outputWriters.keys()[i]
1430 elif evt == GUI.evtAnimToggle:
1431 config.outpur['ANIMATION'] = bool(GUI.animToggle.val)
1433 elif evt == GUI.evtJoinObjsToggle:
1434 config.output['JOIN_OBJECTS'] = bool(GUI.joinObjsToggle.val)
1436 elif evt == GUI.evtPolygonsToggle:
1437 config.polygons['SHOW'] = bool(GUI.polygonsToggle.val)
1439 elif evt == GUI.evtShadingStyleMenu:
1440 i = GUI.shadingStyleMenu.val - 1
1441 config.polygons['SHADING'] = shadingStyles.keys()[i]
1443 elif evt == GUI.evtShowEdgesToggle:
1444 config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
1446 elif evt == GUI.evtShowHiddenEdgesToggle:
1447 config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
1449 elif evt == GUI.evtEdgeStyleMenu:
1450 i = GUI.edgeStyleMenu.val - 1
1451 config.edges['STYLE'] = edgeStyles.keys()[i]
1453 elif evt == GUI.evtEdgeWidthSlider:
1454 config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
1456 elif evt == GUI.evtEdgeColorPicker:
1457 config.edges['COLOR'] = [int(c*255.0) for c in GUI.edgeColorPicker.val]
1459 elif evt == GUI.evtRenderButton:
1460 label = "Save %s" % config.output['FORMAT']
1461 # Show the File Selector
1463 Blender.Window.FileSelector(vectorize, label, outputfile)
1466 print "Event: %d not handled!" % evt
1473 from pprint import pprint
1475 pprint(config.output)
1476 pprint(config.polygons)
1477 pprint(config.edges)
1479 _init = staticmethod(_init)
1480 draw = staticmethod(draw)
1481 event = staticmethod(event)
1482 button_event = staticmethod(button_event)
1483 conf_debug = staticmethod(conf_debug)
1485 # A wrapper function for the vectorizing process
1486 def vectorize(filename):
1487 """The vectorizing process is as follows:
1489 - Instanciate the writer and the renderer
1494 print "\nERROR: invalid file name!"
1497 from Blender import Window
1498 editmode = Window.EditMode()
1499 if editmode: Window.EditMode(0)
1501 actualWriter = outputWriters[config.output['FORMAT']]
1502 writer = actualWriter(filename)
1504 renderer = Renderer()
1505 renderer.doRendering(writer, config.output['ANIMATION'])
1507 if editmode: Window.EditMode(1)
1511 if __name__ == "__main__":
1514 basename = Blender.sys.basename(Blender.Get('filename'))
1516 outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
1518 if Blender.mode == 'background':
1519 vectorize(outputfile)
1521 Draw.Register(GUI.draw, GUI.event, GUI.button_event)