6 Tooltip: 'Vector Rendering Method script'
9 __author__ = "Antonio Ospite"
10 __url__ = ["http://projects.blender.org/projects/vrm"]
11 __version__ = "0.3.beta"
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 # - FIX the issue with negative scales in object tranformations!
46 # - Use a better depth sorting algorithm
47 # - Implement clipping of primitives and do handle object intersections.
48 # (for now only clipping away whole objects is supported).
49 # - Review how selections are made (this script uses selection states of
50 # primitives to represent visibility infos)
51 # - Use a data structure other than Mesh to represent the 2D image?
52 # Think to a way to merge (adjacent) polygons that have the same color.
53 # Or a way to use paths for silhouettes and contours.
54 # - Consider SMIL for animation handling instead of ECMA Script? (Firefox do
55 # not support SMIL for animations)
56 # - Switch to the Mesh structure, should be considerably faster
57 # (partially done, but with Mesh we cannot sort faces, yet)
58 # - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
59 # - Implement Shading Styles? (partially done, to make more flexible).
60 # - Add Vector Writers other than SVG.
61 # - set the background color!
62 # - Check memory use!!
64 # ---------------------------------------------------------------------
69 # * First release after code restucturing.
70 # Now the script offers a useful set of functionalities
71 # and it can render animations, too.
72 # * Optimization in Renderer.doEdgeStyle(), build a topology cache
73 # so to speed up the lookup of adjacent faces of an edge.
75 # * The SVG output is now SVG 1.0 valid.
76 # Checked with: http://jiggles.w3.org/svgvalidator/ValidatorURI.html
77 # * Progress indicator during HSR.
78 # * Initial SWF output support
79 # * Fixed a bug in the animation code, now the projection matrix is
80 # recalculated at each frame!
82 # ---------------------------------------------------------------------
85 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera, Window
86 from Blender.Mathutils import *
93 # We use a global progress Indicator Object
97 # Some global settings
101 polygons['SHOW'] = True
102 polygons['SHADING'] = 'FLAT'
103 #polygons['HSR'] = 'PAINTER' # 'PAINTER' or 'NEWELL'
104 polygons['HSR'] = 'NEWELL'
105 # Hidden to the user for now
106 polygons['EXPANSION_TRICK'] = True
108 polygons['TOON_LEVELS'] = 2
111 edges['SHOW'] = False
112 edges['SHOW_HIDDEN'] = False
113 edges['STYLE'] = 'MESH' # or SILHOUETTE
115 edges['COLOR'] = [0, 0, 0]
118 output['FORMAT'] = 'SVG'
119 output['ANIMATION'] = False
120 output['JOIN_OBJECTS'] = True
122 #output['FORMAT'] = 'SWF'
123 #output['ANIMATION'] = True
129 def dumpfaces(flist, filename):
130 """Dump a single face to a file.
141 writerobj = SVGVectorWriter(filename)
144 writerobj._printPolygons(m)
150 sys.stderr.write(msg)
153 return (abs(v1[0]-v2[0]) < EPS and
154 abs(v1[1]-v2[1]) < EPS )
155 by_furthest_z = (lambda f1, f2:
156 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
171 # ---------------------------------------------------------------------
175 # ---------------------------------------------------------------------
181 """A utility class for HSR processing.
184 def is_nonplanar_quad(face):
185 """Determine if a quad is non-planar.
187 From: http://mathworld.wolfram.com/Coplanar.html
189 Geometric objects lying in a common plane are said to be coplanar.
190 Three noncollinear points determine a plane and so are trivially coplanar.
191 Four points are coplanar iff the volume of the tetrahedron defined by them is
197 | x_4 y_4 z_4 1 | == 0
199 Coplanarity is equivalent to the statement that the pair of lines
200 determined by the four points are not skew, and can be equivalently stated
201 in vector form as (x_3-x_1).[(x_2-x_1)x(x_4-x_3)]==0.
203 An arbitrary number of n points x_1, ..., x_n can be tested for
204 coplanarity by finding the point-plane distances of the points
205 x_4, ..., x_n from the plane determined by (x_1,x_2,x_3)
206 and checking if they are all zero.
207 If so, the points are all coplanar.
209 We here check only for 4-point complanarity.
215 print "ERROR a mesh in Blender can't have more than 4 vertices or less than 3"
219 # three points must be complanar
222 x1 = Vector(face[0].co)
223 x2 = Vector(face[1].co)
224 x3 = Vector(face[2].co)
225 x4 = Vector(face[3].co)
227 v = (x3-x1) * CrossVecs((x2-x1), (x4-x3))
233 is_nonplanar_quad = staticmethod(is_nonplanar_quad)
235 def pointInPolygon(poly, v):
238 pointInPolygon = staticmethod(pointInPolygon)
240 def edgeIntersection(s1, s2, do_perturbate=False):
242 (x1, y1) = s1[0].co[0], s1[0].co[1]
243 (x2, y2) = s1[1].co[0], s1[1].co[1]
245 (x3, y3) = s2[0].co[0], s2[0].co[1]
246 (x4, y4) = s2[1].co[0], s2[1].co[1]
254 # calculate delta values (vector components)
263 C = dy2 * dx1 - dx2 * dy1 # /* cross product */
264 if C == 0: #/* parallel */
267 dx3 = x1 - x3 # /* combined origin offset vector */
270 a1 = (dy3 * dx2 - dx3 * dy2) / C;
271 a2 = (dy3 * dx1 - dx3 * dy1) / C;
273 # check for degeneracies
275 #print_debug(str(a1)+"\n")
276 #print_debug(str(a2)+"\n\n")
278 if (a1 == 0 or a1 == 1 or a2 == 0 or a2 == 1):
279 # Intersection on boundaries, we consider the point external?
282 elif (a1>0.0 and a1<1.0 and a2>0.0 and a2<1.0): # /* lines cross */
288 return (NMesh.Vert(x, y, z), a1, a2)
291 # lines have intersections but not those segments
294 edgeIntersection = staticmethod(edgeIntersection)
296 def isVertInside(self, v):
300 # Create point at infinity
301 point_at_infinity = NMesh.Vert(-INF, v.co[1], -INF)
303 for i in range(len(self.v)):
304 s1 = (point_at_infinity, v)
305 s2 = (self.v[i-1], self.v[i])
307 if EQ(v.co, s2[0].co) or EQ(v.co, s2[1].co):
310 if HSR.edgeIntersection(s1, s2, do_perturbate=False):
314 if winding_number % 2 == 0 :
321 isVertInside = staticmethod(isVertInside)
323 def projectionsOverlap(f1, f2):
324 """ If you have nonconvex, but still simple polygons, an acceptable method
325 is to iterate over all vertices and perform the Point-in-polygon test[1].
326 The advantage of this method is that you can compute the exact
327 intersection point and collision normal that you will need to simulate
328 collision. When you have the point that lies inside the other polygon, you
329 just iterate over all edges of the second polygon again and look for edge
330 intersections. Note that this method detects collsion when it already
331 happens. This algorithm is fast enough to perform it hundreds of times per
334 for i in range(len(f1.v)):
337 # If a point of f1 in inside f2, there is an overlap!
339 if HSR.isVertInside(f2, v1):
342 # If not the polygon can be ovelap as well, so we check for
343 # intersection between an edge of f1 and all the edges of f2
347 for j in range(len(f2.v)):
354 intrs = HSR.edgeIntersection(e1, e2)
356 #print_debug(str(v0.co) + " " + str(v1.co) + " " +
357 # str(v2.co) + " " + str(v3.co) )
358 #print_debug("\nIntersection\n")
364 projectionsOverlap = staticmethod(projectionsOverlap)
366 def midpoint(p1, p2):
367 """Return the midpoint of two vertices.
369 m = MidpointVecs(Vector(p1), Vector(p2))
370 mv = NMesh.Vert(m[0], m[1], m[2])
374 midpoint = staticmethod(midpoint)
376 def facesplit(P, Q, facelist, nmesh):
377 """Split P or Q according to the strategy illustrated in the Newell's
381 by_furthest_z = (lambda f1, f2:
382 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
385 # Choose if split P on Q plane or vice-versa
389 d = HSR.Distance(Vector(Pi), Q)
392 pIntersectQ = (n != len(P))
396 d = HSR.Distance(Vector(Qi), P)
399 qIntersectP = (n != len(Q))
403 # 1. If parts of P lie in both half-spaces of Q
404 # then splice P in two with the plane of Q
410 newfaces = HSR.splitOn(plane, f)
412 # 2. Else if parts of Q lie in both half-space of P
413 # then splice Q in two with the plane of P
414 if qIntersectP and newfaces == None:
419 newfaces = HSR.splitOn(plane, f)
422 # 3. Else slice P in half through the mid-point of
423 # the longest pair of opposite sides
426 print "We ignore P..."
433 # v1 = midpoint(f[0], f[1])
434 # v2 = midpoint(f[1], f[2])
436 # v1 = midpoint(f[0], f[1])
437 # v2 = midpoint(f[2], f[3])
438 #vec3 = (Vector(v2)+10*Vector(f.normal))
440 #v3 = NMesh.Vert(vec3[0], vec3[1], vec3[2])
442 #plane = NMesh.Face([v1, v2, v3])
444 #newfaces = splitOn(plane, f)
448 print "Big FAT problem, we weren't able to split POLYGONS!"
454 # if v not in plane and v in nmesh.verts:
455 # nmesh.verts.remove(v)
460 nf.col = [f.col[0]] * len(nf.v)
465 nmesh.verts.append(v)
466 # insert pieces in the list
471 # and resort the faces
472 facelist.sort(by_furthest_z)
473 facelist.sort(lambda f1, f2: cmp(f1.smooth, f2.smooth))
476 #print [ f.smooth for f in facelist ]
480 facesplit = staticmethod(facesplit)
482 def isOnSegment(v1, v2, p, extremes_internal=False):
483 """Check if point p is in segment v1v2.
489 # Should we consider extreme points as internal ?
491 # if p == v1 or p == v2:
492 if l1 < EPS or l2 < EPS:
493 return extremes_internal
497 # if the sum of l1 and l2 is circa l, then the point is on segment,
498 if abs(l - (l1+l2)) < EPS:
503 isOnSegment = staticmethod(isOnSegment)
505 def Distance(point, face):
506 """ Calculate the distance between a point and a face.
508 An alternative but more expensive method can be:
510 ip = Intersect(Vector(face[0]), Vector(face[1]), Vector(face[2]),
511 Vector(face.no), Vector(point), 0)
513 d = Vector(ip - point).length
515 See: http://mathworld.wolfram.com/Point-PlaneDistance.html
519 plNormal = Vector(face.no)
520 plVert0 = Vector(face.v[0])
522 d = (plVert0 * plNormal) - (p * plNormal)
524 #d = plNormal * (plVert0 - p)
526 #print "\nd: %.10f - sel: %d, %s\n" % (d, face.sel, str(point))
530 Distance = staticmethod(Distance)
534 # make one or two new faces based on a list of vertex-indices
563 makeFaces = staticmethod(makeFaces)
566 """Split P using the plane of Q.
567 Logic taken from the knife.py python script
570 # Check if P and Q are parallel
571 u = CrossVecs(Vector(Q.no),Vector(P.no))
577 print "PARALLEL planes!!"
581 # The final aim is to find the intersection line between P
582 # and the plane of Q, and split P along this line
586 # Calculate point-plane Distance between vertices of P and plane Q
588 for i in range(0, nP):
589 d.append(HSR.Distance(P.v[i], Q))
602 #print "d0:", d0, "d1:", d1
604 # if the vertex lies in the cutplane
606 #print "d1 On cutplane"
607 posVertList.append(V1)
608 negVertList.append(V1)
610 # if the previous vertex lies in cutplane
612 #print "d0 on Cutplane"
614 #print "d1 on positive Halfspace"
615 posVertList.append(V1)
617 #print "d1 on negative Halfspace"
618 negVertList.append(V1)
620 # if they are on the same side of the plane
622 #print "On the same half-space"
624 #print "d1 on positive Halfspace"
625 posVertList.append(V1)
627 #print "d1 on negative Halfspace"
628 negVertList.append(V1)
630 # the vertices are not on the same side of the plane, so we have an intersection
632 #print "Intersection"
634 e = Vector(V0), Vector(V1)
635 tri = Vector(Q[0]), Vector(Q[1]), Vector(Q[2])
637 inters = Intersect(tri[0], tri[1], tri[2], e[1]-e[0], e[0], 0)
642 #print "Intersection", inters
644 nv = NMesh.Vert(inters[0], inters[1], inters[2])
645 newVertList.append(nv)
647 posVertList.append(nv)
648 negVertList.append(nv)
651 posVertList.append(V1)
653 negVertList.append(V1)
657 posVertList = [ u for u in posVertList if u not in locals()['_[1]'] ]
658 negVertList = [ u for u in negVertList if u not in locals()['_[1]'] ]
661 # If vertex are all on the same half-space, return
662 #if len(posVertList) < 3:
663 # print "Problem, we created a face with less that 3 verteices??"
665 #if len(negVertList) < 3:
666 # print "Problem, we created a face with less that 3 verteices??"
669 if len(posVertList) < 3 or len(negVertList) < 3:
670 print "RETURN NONE, SURE???"
674 newfaces = HSR.addNewFaces(posVertList, negVertList)
678 splitOn = staticmethod(splitOn)
680 def addNewFaces(posVertList, negVertList):
681 # Create new faces resulting from the split
683 if len(posVertList) or len(negVertList):
685 #newfaces = [posVertList] + [negVertList]
686 newfaces = ( [[ NMesh.Vert(v[0], v[1], v[2]) for v in posVertList]] +
687 [[ NMesh.Vert(v[0], v[1], v[2]) for v in negVertList]] )
691 outfaces += HSR.makeFaces(nf)
696 addNewFaces = staticmethod(addNewFaces)
699 # ---------------------------------------------------------------------
701 ## Mesh Utility class
703 # ---------------------------------------------------------------------
707 def buildEdgeFaceUsersCache(me):
709 Takes a mesh and returns a list aligned with the meshes edges.
710 Each item is a list of the faces that use the edge
711 would be the equiv for having ed.face_users as a property
713 Taken from .blender/scripts/bpymodules/BPyMesh.py,
714 thanks to ideasman_42.
717 def sorted_edge_indicies(ed):
725 face_edges_dict= dict([(sorted_edge_indicies(ed), (ed.index, [])) for ed in me.edges])
727 fvi= [v.index for v in f.v]# face vert idx's
728 for i in xrange(len(f)):
735 face_edges_dict[i1,i2][1].append(f)
737 face_edges= [None] * len(me.edges)
738 for ed_index, ed_faces in face_edges_dict.itervalues():
739 face_edges[ed_index]= ed_faces
743 def isMeshEdge(adjacent_faces):
746 A mesh edge is visible if _at_least_one_ of its adjacent faces is selected.
747 Note: if the edge has no adjacent faces we want to show it as well,
748 useful for "edge only" portion of objects.
751 if len(adjacent_faces) == 0:
754 selected_faces = [f for f in adjacent_faces if f.sel]
756 if len(selected_faces) != 0:
761 def isSilhouetteEdge(adjacent_faces):
762 """Silhuette selection rule.
764 An edge is a silhuette edge if it is shared by two faces with
765 different selection status or if it is a boundary edge of a selected
769 if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
770 (len(adjacent_faces) == 2 and
771 adjacent_faces[0].sel != adjacent_faces[1].sel)
777 buildEdgeFaceUsersCache = staticmethod(buildEdgeFaceUsersCache)
778 isMeshEdge = staticmethod(isMeshEdge)
779 isSilhouetteEdge = staticmethod(isSilhouetteEdge)
782 # ---------------------------------------------------------------------
784 ## Shading Utility class
786 # ---------------------------------------------------------------------
792 def toonShadingMapSetup():
793 levels = config.polygons['TOON_LEVELS']
795 texels = 2*levels - 1
796 tmp_shademap = [0.0] + [(i)/float(texels-1) for i in xrange(1, texels-1) ] + [1.0]
802 shademap = ShadingUtils.shademap
805 shademap = ShadingUtils.toonShadingMapSetup()
808 for i in xrange(0, len(shademap)-1):
809 pivot = (shademap[i]+shademap[i+1])/2.0
814 if v < shademap[i+1]:
819 toonShadingMapSetup = staticmethod(toonShadingMapSetup)
820 toonShading = staticmethod(toonShading)
823 # ---------------------------------------------------------------------
825 ## Projections classes
827 # ---------------------------------------------------------------------
830 """Calculate the projection of an object given the camera.
832 A projector is useful to so some per-object transformation to obtain the
833 projection of an object given the camera.
835 The main method is #doProjection# see the method description for the
839 def __init__(self, cameraObj, canvasRatio):
840 """Calculate the projection matrix.
842 The projection matrix depends, in this case, on the camera settings.
843 TAKE CARE: This projector expects vertices in World Coordinates!
846 camera = cameraObj.getData()
848 aspect = float(canvasRatio[0])/float(canvasRatio[1])
849 near = camera.clipStart
852 scale = float(camera.scale)
854 fovy = atan(0.5/aspect/(camera.lens/32))
855 fovy = fovy * 360.0/pi
857 # What projection do we want?
859 mP = self._calcPerspectiveMatrix(fovy, aspect, near, far)
860 elif camera.type == 1:
861 mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale)
863 # View transformation
864 cam = Matrix(cameraObj.getInverseMatrix())
869 self.projectionMatrix = mP
875 def doProjection(self, v):
876 """Project the point on the view plane.
878 Given a vertex calculate the projection using the current projection
882 # Note that we have to work on the vertex using homogeneous coordinates
883 # From blender 2.42+ we don't need to resize the vector to be 4d
884 # when applying a 4x4 matrix, but we do that anyway since we need the
885 # 4th coordinate later
886 p = self.projectionMatrix * Vector(v).resize4D()
888 # Perspective division
905 def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
906 """Return a perspective projection matrix.
909 top = near * tan(fovy * pi / 360.0)
913 x = (2.0 * near) / (right-left)
914 y = (2.0 * near) / (top-bottom)
915 a = (right+left) / (right-left)
916 b = (top+bottom) / (top - bottom)
917 c = - ((far+near) / (far-near))
918 d = - ((2*far*near)/(far-near))
924 [0.0, 0.0, -1.0, 0.0])
928 def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
929 """Return an orthogonal projection matrix.
932 # The 11 in the formula was found emiprically
933 top = near * tan(fovy * pi / 360.0) * (scale * 11)
935 left = bottom * aspect
940 tx = -((right+left)/rl)
941 ty = -((top+bottom)/tb)
945 [2.0/rl, 0.0, 0.0, tx],
946 [0.0, 2.0/tb, 0.0, ty],
947 [0.0, 0.0, 2.0/fn, tz],
948 [0.0, 0.0, 0.0, 1.0])
953 # ---------------------------------------------------------------------
955 ## Progress Indicator
957 # ---------------------------------------------------------------------
960 """A model for a progress indicator.
962 Do the progress calculation calculation and
963 the view independent stuff of a progress indicator.
965 def __init__(self, steps=0):
971 def setSteps(self, steps):
972 """Set the number of steps of the activity wich we want to track.
979 def setName(self, name):
980 """Set the name of the activity wich we want to track.
987 def getProgress(self):
995 """Update the model, call this method when one step is completed.
997 if self.progress == 100:
1001 self.progress = ( float(self.completed) / float(self.steps) ) * 100
1002 self.progress = int(self.progress)
1007 class ProgressIndicator:
1008 """An abstraction of a View for the Progress Model
1012 # Use a refresh rate so we do not show the progress at
1013 # every update, but every 'self.refresh_rate' times.
1014 self.refresh_rate = 10
1015 self.shows_counter = 0
1019 self.progressModel = None
1021 def setQuiet(self, value):
1024 def setActivity(self, name, steps):
1025 """Initialize the Model.
1027 In a future version (with subactivities-progress support) this method
1028 could only set the current activity.
1030 self.progressModel = Progress()
1031 self.progressModel.setName(name)
1032 self.progressModel.setSteps(steps)
1034 def getActivity(self):
1035 return self.progressModel
1038 """Update the model and show the actual progress.
1040 assert(self.progressModel)
1042 if self.progressModel.update():
1046 self.show(self.progressModel.getProgress(),
1047 self.progressModel.getName())
1049 # We return always True here so we can call the update() method also
1050 # from lambda funcs (putting the call in logical AND with other ops)
1053 def show(self, progress, name=""):
1054 self.shows_counter = (self.shows_counter + 1) % self.refresh_rate
1055 if self.shows_counter != 0:
1059 self.shows_counter = -1
1062 class ConsoleProgressIndicator(ProgressIndicator):
1063 """Show a progress bar on stderr, a la wget.
1066 ProgressIndicator.__init__(self)
1068 self.swirl_chars = ["-", "\\", "|", "/"]
1069 self.swirl_count = -1
1071 def show(self, progress, name):
1072 ProgressIndicator.show(self, progress, name)
1075 bar_progress = int( (progress/100.0) * bar_length )
1076 bar = ("=" * bar_progress).ljust(bar_length)
1078 self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1079 swirl_char = self.swirl_chars[self.swirl_count]
1081 progress_bar = "%s |%s| %c %3d%%" % (name, bar, swirl_char, progress)
1083 sys.stderr.write(progress_bar+"\r")
1085 sys.stderr.write("\n")
1088 class GraphicalProgressIndicator(ProgressIndicator):
1089 """Interface to the Blender.Window.DrawProgressBar() method.
1092 ProgressIndicator.__init__(self)
1094 #self.swirl_chars = ["-", "\\", "|", "/"]
1095 # We have to use letters with the same width, for now!
1096 # Blender progress bar considers the font widths when
1097 # calculating the progress bar width.
1098 self.swirl_chars = ["\\", "/"]
1099 self.swirl_count = -1
1101 def show(self, progress, name):
1102 ProgressIndicator.show(self, progress)
1104 self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1105 swirl_char = self.swirl_chars[self.swirl_count]
1107 progress_text = "%s - %c %3d%%" % (name, swirl_char, progress)
1109 # Finally draw the Progress Bar
1110 Window.WaitCursor(1) # Maybe we can move that call in the constructor?
1111 Window.DrawProgressBar(progress/100.0, progress_text)
1114 Window.DrawProgressBar(1, progress_text)
1115 Window.WaitCursor(0)
1119 # ---------------------------------------------------------------------
1121 ## 2D Object representation class
1123 # ---------------------------------------------------------------------
1125 # TODO: a class to represent the needed properties of a 2D vector image
1126 # For now just using a [N]Mesh structure.
1129 # ---------------------------------------------------------------------
1131 ## Vector Drawing Classes
1133 # ---------------------------------------------------------------------
1139 A class for printing output in a vectorial format.
1141 Given a 2D representation of the 3D scene the class is responsible to
1142 write it is a vector format.
1144 Every subclasses of VectorWriter must have at last the following public
1148 - printCanvas(self, scene,
1149 doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
1152 def __init__(self, fileName):
1153 """Set the output file name and other properties"""
1155 self.outputFileName = fileName
1158 context = Scene.GetCurrent().getRenderingContext()
1159 self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
1163 self.animation = False
1170 def open(self, startFrame=1, endFrame=1):
1171 if startFrame != endFrame:
1172 self.startFrame = startFrame
1173 self.endFrame = endFrame
1174 self.animation = True
1176 self.file = open(self.outputFileName, "w")
1177 print "Outputting to: ", self.outputFileName
1186 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1187 showHiddenEdges=False):
1188 """This is the interface for the needed printing routine.
1195 class SVGVectorWriter(VectorWriter):
1196 """A concrete class for writing SVG output.
1199 def __init__(self, fileName):
1200 """Simply call the parent Contructor.
1202 VectorWriter.__init__(self, fileName)
1209 def open(self, startFrame=1, endFrame=1):
1210 """Do some initialization operations.
1212 VectorWriter.open(self, startFrame, endFrame)
1216 """Do some finalization operation.
1220 # remember to call the close method of the parent
1221 VectorWriter.close(self)
1224 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1225 showHiddenEdges=False):
1226 """Convert the scene representation to SVG.
1229 Objects = scene.getChildren()
1231 context = scene.getRenderingContext()
1232 framenumber = context.currentFrame()
1235 framestyle = "display:none"
1237 framestyle = "display:block"
1239 # Assign an id to this group so we can set properties on it using DOM
1240 self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
1241 (framenumber, framestyle) )
1246 if(obj.getType() != 'Mesh'):
1249 self.file.write("<g id=\"%s\">\n" % obj.getName())
1251 mesh = obj.getData(mesh=1)
1254 self._printPolygons(mesh)
1257 self._printEdges(mesh, showHiddenEdges)
1259 self.file.write("</g>\n")
1261 self.file.write("</g>\n")
1268 def _calcCanvasCoord(self, v):
1269 """Convert vertex in scene coordinates to canvas coordinates.
1272 pt = Vector([0, 0, 0])
1274 mW = float(self.canvasSize[0])/2.0
1275 mH = float(self.canvasSize[1])/2.0
1277 # rescale to canvas size
1278 pt[0] = v.co[0]*mW + mW
1279 pt[1] = v.co[1]*mH + mH
1282 # For now we want (0,0) in the top-left corner of the canvas.
1283 # Mirror and translate along y
1285 pt[1] += self.canvasSize[1]
1289 def _printHeader(self):
1290 """Print SVG header."""
1292 self.file.write("<?xml version=\"1.0\"?>\n")
1293 self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n")
1294 self.file.write("\t\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
1295 self.file.write("<svg version=\"1.0\"\n")
1296 self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
1297 self.file.write("\twidth=\"%d\" height=\"%d\">\n\n" %
1302 self.file.write("""\n<script type="text/javascript"><![CDATA[
1303 globalStartFrame=%d;
1306 /* FIXME: Use 1000 as interval as lower values gives problems */
1307 timerID = setInterval("NextFrame()", 1000);
1308 globalFrameCounter=%d;
1310 function NextFrame()
1312 currentElement = document.getElementById('frame'+globalFrameCounter)
1313 previousElement = document.getElementById('frame'+(globalFrameCounter-1))
1315 if (!currentElement)
1320 if (globalFrameCounter > globalEndFrame)
1322 clearInterval(timerID)
1328 previousElement.style.display="none";
1330 currentElement.style.display="block";
1331 globalFrameCounter++;
1335 \n""" % (self.startFrame, self.endFrame, self.startFrame) )
1337 def _printFooter(self):
1338 """Print the SVG footer."""
1340 self.file.write("\n</svg>\n")
1342 def _printPolygons(self, mesh):
1343 """Print the selected (visible) polygons.
1346 if len(mesh.faces) == 0:
1349 self.file.write("<g>\n")
1351 for face in mesh.faces:
1355 self.file.write("<path d=\"")
1357 #p = self._calcCanvasCoord(face.verts[0])
1358 p = self._calcCanvasCoord(face.v[0])
1359 self.file.write("M %g,%g L " % (p[0], p[1]))
1361 for v in face.v[1:]:
1362 p = self._calcCanvasCoord(v)
1363 self.file.write("%g,%g " % (p[0], p[1]))
1365 # get rid of the last blank space, just cosmetics here.
1366 self.file.seek(-1, 1)
1367 self.file.write(" z\"\n")
1369 # take as face color the first vertex color
1372 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1374 color = [255, 255, 255, 255]
1376 # Convert the color to the #RRGGBB form
1377 str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
1379 # Handle transparent polygons
1382 opacity = float(color[3])/255.0
1383 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
1384 #opacity_string = "opacity: %g;" % (opacity)
1386 self.file.write("\tstyle=\"fill:" + str_col + ";")
1387 self.file.write(opacity_string)
1389 # use the stroke property to alleviate the "adjacent edges" problem,
1390 # we simulate polygon expansion using borders,
1391 # see http://www.antigrain.com/svg/index.html for more info
1394 # EXPANSION TRICK is not that useful where there is transparency
1395 if config.polygons['EXPANSION_TRICK'] and color[3] == 255:
1396 # str_col = "#000000" # For debug
1397 self.file.write(" stroke:%s;\n" % str_col)
1398 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
1399 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
1401 self.file.write("\"/>\n")
1403 self.file.write("</g>\n")
1405 def _printEdges(self, mesh, showHiddenEdges=False):
1406 """Print the wireframe using mesh edges.
1409 stroke_width = config.edges['WIDTH']
1410 stroke_col = config.edges['COLOR']
1412 self.file.write("<g>\n")
1414 for e in mesh.edges:
1416 hidden_stroke_style = ""
1419 if showHiddenEdges == False:
1422 hidden_stroke_style = ";\n stroke-dasharray:3, 3"
1424 p1 = self._calcCanvasCoord(e.v1)
1425 p2 = self._calcCanvasCoord(e.v2)
1427 self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
1428 % ( p1[0], p1[1], p2[0], p2[1] ) )
1429 self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
1430 self.file.write(" stroke-width:"+str(stroke_width)+";\n")
1431 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
1432 self.file.write(hidden_stroke_style)
1433 self.file.write("\"/>\n")
1435 self.file.write("</g>\n")
1444 SWFSupported = False
1446 class SWFVectorWriter(VectorWriter):
1447 """A concrete class for writing SWF output.
1450 def __init__(self, fileName):
1451 """Simply call the parent Contructor.
1453 VectorWriter.__init__(self, fileName)
1463 def open(self, startFrame=1, endFrame=1):
1464 """Do some initialization operations.
1466 VectorWriter.open(self, startFrame, endFrame)
1467 self.movie = SWFMovie()
1468 self.movie.setDimension(self.canvasSize[0], self.canvasSize[1])
1470 self.movie.setRate(25)
1471 numframes = endFrame - startFrame + 1
1472 self.movie.setFrames(numframes)
1475 """Do some finalization operation.
1477 self.movie.save(self.outputFileName)
1479 # remember to call the close method of the parent
1480 VectorWriter.close(self)
1482 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1483 showHiddenEdges=False):
1484 """Convert the scene representation to SVG.
1486 context = scene.getRenderingContext()
1487 framenumber = context.currentFrame()
1489 Objects = scene.getChildren()
1492 self.movie.remove(self.sprite)
1494 sprite = SWFSprite()
1498 if(obj.getType() != 'Mesh'):
1501 mesh = obj.getData(mesh=1)
1504 self._printPolygons(mesh, sprite)
1507 self._printEdges(mesh, sprite, showHiddenEdges)
1510 i = self.movie.add(sprite)
1511 # Remove the instance the next time
1514 self.movie.nextFrame()
1521 def _calcCanvasCoord(self, v):
1522 """Convert vertex in scene coordinates to canvas coordinates.
1525 pt = Vector([0, 0, 0])
1527 mW = float(self.canvasSize[0])/2.0
1528 mH = float(self.canvasSize[1])/2.0
1530 # rescale to canvas size
1531 pt[0] = v.co[0]*mW + mW
1532 pt[1] = v.co[1]*mH + mH
1535 # For now we want (0,0) in the top-left corner of the canvas.
1536 # Mirror and translate along y
1538 pt[1] += self.canvasSize[1]
1542 def _printPolygons(self, mesh, sprite):
1543 """Print the selected (visible) polygons.
1546 if len(mesh.faces) == 0:
1549 for face in mesh.faces:
1555 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1557 color = [255, 255, 255, 255]
1560 f = s.addFill(color[0], color[1], color[2], color[3])
1563 # The starting point of the shape
1564 p0 = self._calcCanvasCoord(face.verts[0])
1565 s.movePenTo(p0[0], p0[1])
1568 for v in face.verts[1:]:
1569 p = self._calcCanvasCoord(v)
1570 s.drawLineTo(p[0], p[1])
1573 s.drawLineTo(p0[0], p0[1])
1579 # use the stroke property to alleviate the "adjacent edges" problem,
1580 # we simulate polygon expansion using borders,
1581 # see http://www.antigrain.com/svg/index.html for more info
1584 # EXPANSION TRICK is not that useful where there is transparency
1585 if config.polygons['EXPANSION_TRICK'] and color[3] == 255:
1586 # str_col = "#000000" # For debug
1587 self.file.write(" stroke:%s;\n" % str_col)
1588 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
1589 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
1593 def _printEdges(self, mesh, sprite, showHiddenEdges=False):
1594 """Print the wireframe using mesh edges.
1597 stroke_width = config.edges['WIDTH']
1598 stroke_col = config.edges['COLOR']
1602 for e in mesh.edges:
1604 #Next, we set the line width and color for our shape.
1605 s.setLine(stroke_width, stroke_col[0], stroke_col[1], stroke_col[2],
1609 if showHiddenEdges == False:
1612 # SWF does not support dashed lines natively, so -for now-
1613 # draw hidden lines thinner and half-trasparent
1614 s.setLine(stroke_width/2, stroke_col[0], stroke_col[1],
1617 p1 = self._calcCanvasCoord(e.v1)
1618 p2 = self._calcCanvasCoord(e.v2)
1620 # FIXME: this is just a qorkaround, remove that after the
1621 # implementation of propoer Viewport clipping
1622 if abs(p1[0]) < 3000 and abs(p2[0]) < 3000 and abs(p1[1]) < 3000 and abs(p1[2]) < 3000:
1623 s.movePenTo(p1[0], p1[1])
1624 s.drawLineTo(p2[0], p2[1])
1632 # ---------------------------------------------------------------------
1634 ## Rendering Classes
1636 # ---------------------------------------------------------------------
1638 # A dictionary to collect different shading style methods
1639 shadingStyles = dict()
1640 shadingStyles['FLAT'] = None
1641 shadingStyles['TOON'] = None
1643 # A dictionary to collect different edge style methods
1645 edgeStyles['MESH'] = MeshUtils.isMeshEdge
1646 edgeStyles['SILHOUETTE'] = MeshUtils.isSilhouetteEdge
1648 # A dictionary to collect the supported output formats
1649 outputWriters = dict()
1650 outputWriters['SVG'] = SVGVectorWriter
1652 outputWriters['SWF'] = SWFVectorWriter
1656 """Render a scene viewed from the active camera.
1658 This class is responsible of the rendering process, transformation and
1659 projection of the objects in the scene are invoked by the renderer.
1661 The rendering is done using the active camera for the current scene.
1665 """Make the rendering process only for the current scene by default.
1667 We will work on a copy of the scene, to be sure that the current scene do
1668 not get modified in any way.
1671 # Render the current Scene, this should be a READ-ONLY property
1672 self._SCENE = Scene.GetCurrent()
1674 # Use the aspect ratio of the scene rendering context
1675 context = self._SCENE.getRenderingContext()
1677 aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
1678 self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
1679 float(context.aspectRatioY())
1682 # Render from the currently active camera
1683 self.cameraObj = self._SCENE.getCurrentCamera()
1685 # Get the list of lighting sources
1686 obj_lst = self._SCENE.getChildren()
1687 self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
1689 # When there are no lights we use a default lighting source
1690 # that have the same position of the camera
1691 if len(self.lights) == 0:
1692 l = Lamp.New('Lamp')
1693 lobj = Object.New('Lamp')
1694 lobj.loc = self.cameraObj.loc
1696 self.lights.append(lobj)
1703 def doRendering(self, outputWriter, animation=False):
1704 """Render picture or animation and write it out.
1707 - a Vector writer object that will be used to output the result.
1708 - a flag to tell if we want to render an animation or only the
1712 context = self._SCENE.getRenderingContext()
1713 origCurrentFrame = context.currentFrame()
1715 # Handle the animation case
1717 startFrame = origCurrentFrame
1718 endFrame = startFrame
1721 startFrame = context.startFrame()
1722 endFrame = context.endFrame()
1723 outputWriter.open(startFrame, endFrame)
1725 # Do the rendering process frame by frame
1726 print "Start Rendering of %d frames" % (endFrame-startFrame)
1727 for f in xrange(startFrame, endFrame+1):
1728 print "\n\nFrame: %d" % f
1729 context.currentFrame(f)
1731 # Use some temporary workspace, a full copy of the scene
1732 inputScene = self._SCENE.copy(2)
1733 # And Set our camera accordingly
1734 self.cameraObj = inputScene.getCurrentCamera()
1736 # Get a projector for this camera.
1737 # NOTE: the projector wants object in world coordinates,
1738 # so we should remember to apply modelview transformations
1739 # _before_ we do projection transformations.
1740 self.proj = Projector(self.cameraObj, self.canvasRatio)
1743 renderedScene = self.doRenderScene(inputScene)
1745 print "There was an error! Aborting."
1747 print traceback.print_exc()
1749 self._SCENE.makeCurrent()
1750 Scene.unlink(inputScene)
1754 outputWriter.printCanvas(renderedScene,
1755 doPrintPolygons = config.polygons['SHOW'],
1756 doPrintEdges = config.edges['SHOW'],
1757 showHiddenEdges = config.edges['SHOW_HIDDEN'])
1759 # delete the rendered scene
1760 self._SCENE.makeCurrent()
1761 Scene.unlink(renderedScene)
1764 outputWriter.close()
1766 context.currentFrame(origCurrentFrame)
1769 def doRenderScene(self, workScene):
1770 """Control the rendering process.
1772 Here we control the entire rendering process invoking the operation
1773 needed to transform and project the 3D scene in two dimensions.
1776 # global processing of the scene
1778 self._doSceneClipping(workScene)
1780 self._doConvertGeometricObjsToMesh(workScene)
1782 if config.output['JOIN_OBJECTS']:
1783 self._joinMeshObjectsInScene(workScene)
1785 self._doSceneDepthSorting(workScene)
1787 # Per object activities
1789 Objects = workScene.getChildren()
1790 print "Total Objects: %d" % len(Objects)
1791 for i,obj in enumerate(Objects):
1793 print "Rendering Object: %d" % i
1795 if obj.getType() != 'Mesh':
1796 print "Only Mesh supported! - Skipping type:", obj.getType()
1799 print "Rendering: ", obj.getName()
1801 mesh = obj.getData(mesh=1)
1803 self._doModelingTransformation(mesh, obj.matrix)
1805 self._doBackFaceCulling(mesh)
1808 # When doing HSR with NEWELL we may want to flip all normals
1810 if config.polygons['HSR'] == "NEWELL":
1811 for f in mesh.faces:
1814 for f in mesh.faces:
1817 self._doLighting(mesh)
1819 # Do "projection" now so we perform further processing
1820 # in Normalized View Coordinates
1821 self._doProjection(mesh, self.proj)
1823 self._doViewFrustumClipping(mesh)
1825 self._doHiddenSurfaceRemoval(mesh)
1827 self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
1829 # Update the object data, important! :)
1841 def _getObjPosition(self, obj):
1842 """Return the obj position in World coordinates.
1844 return obj.matrix.translationPart()
1846 def _cameraViewVector(self):
1847 """Get the View Direction form the camera matrix.
1849 return Vector(self.cameraObj.matrix[2]).resize3D()
1854 def _isFaceVisible(self, face):
1855 """Determine if a face of an object is visible from the current camera.
1857 The view vector is calculated from the camera location and one of the
1858 vertices of the face (expressed in World coordinates, after applying
1859 modelview transformations).
1861 After those transformations we determine if a face is visible by
1862 computing the angle between the face normal and the view vector, this
1863 angle has to be between -90 and 90 degrees for the face to be visible.
1864 This corresponds somehow to the dot product between the two, if it
1865 results > 0 then the face is visible.
1867 There is no need to normalize those vectors since we are only interested in
1868 the sign of the cross product and not in the product value.
1870 NOTE: here we assume the face vertices are in WorldCoordinates, so
1871 please transform the object _before_ doing the test.
1874 normal = Vector(face.no)
1875 camPos = self._getObjPosition(self.cameraObj)
1878 # View Vector in orthographics projections is the view Direction of
1880 if self.cameraObj.data.getType() == 1:
1881 view_vect = self._cameraViewVector()
1883 # View vector in perspective projections can be considered as
1884 # the difference between the camera position and one point of
1885 # the face, we choose the farthest point from the camera.
1886 if self.cameraObj.data.getType() == 0:
1887 vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
1891 # if d > 0 the face is visible from the camera
1892 d = view_vect * normal
1902 def _doSceneClipping(self, scene):
1903 """Clip whole objects against the View Frustum.
1905 For now clip away only objects according to their center position.
1908 cpos = self._getObjPosition(self.cameraObj)
1909 view_vect = self._cameraViewVector()
1911 near = self.cameraObj.data.clipStart
1912 far = self.cameraObj.data.clipEnd
1914 aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
1915 fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
1916 fovy = fovy * 360.0/pi
1918 Objects = scene.getChildren()
1920 if o.getType() != 'Mesh': continue;
1922 obj_vect = Vector(cpos) - self._getObjPosition(o)
1924 d = obj_vect*view_vect
1925 theta = AngleBetweenVecs(obj_vect, view_vect)
1927 # if the object is outside the view frustum, clip it away
1928 if (d < near) or (d > far) or (theta > fovy):
1931 def _doConvertGeometricObjsToMesh(self, scene):
1932 """Convert all "geometric" objects to mesh ones.
1934 geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
1935 #geometricObjTypes = ['Mesh', 'Surf', 'Curve']
1937 Objects = scene.getChildren()
1938 objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
1941 obj = self._convertToRawMeshObj(obj)
1943 scene.unlink(old_obj)
1946 # XXX Workaround for Text and Curve which have some normals
1947 # inverted when they are converted to Mesh, REMOVE that when
1948 # blender will fix that!!
1949 if old_obj.getType() in ['Curve', 'Text']:
1950 me = obj.getData(mesh=1)
1951 for f in me.faces: f.sel = 1;
1952 for v in me.verts: v.sel = 1;
1959 def _doSceneDepthSorting(self, scene):
1960 """Sort objects in the scene.
1962 The object sorting is done accordingly to the object centers.
1965 c = self._getObjPosition(self.cameraObj)
1967 by_center_pos = (lambda o1, o2:
1968 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
1969 cmp((self._getObjPosition(o1) - Vector(c)).length,
1970 (self._getObjPosition(o2) - Vector(c)).length)
1973 # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
1974 # then ob1 goes farther than obj2, useful when obj2 has holes
1977 Objects = scene.getChildren()
1978 Objects.sort(by_center_pos)
1985 def _joinMeshObjectsInScene(self, scene):
1986 """Merge all the Mesh Objects in a scene into a single Mesh Object.
1989 oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
1991 # FIXME: Object.join() do not work if the list contains 1 object
1995 mesh = Mesh.New('BigOne')
1996 bigObj = Object.New('Mesh', 'BigOne')
2003 except RuntimeError:
2004 print "\nWarning! - Can't Join Objects\n"
2005 scene.unlink(bigObj)
2008 print "Objects Type error?"
2016 # Per object/mesh methods
2018 def _convertToRawMeshObj(self, object):
2019 """Convert geometry based object to a mesh object.
2021 me = Mesh.New('RawMesh_'+object.name)
2022 me.getFromObject(object.name)
2024 newObject = Object.New('Mesh', 'RawMesh_'+object.name)
2027 # If the object has no materials set a default material
2028 if not me.materials:
2029 me.materials = [Material.New()]
2030 #for f in me.faces: f.mat = 0
2032 newObject.setMatrix(object.getMatrix())
2036 def _doModelingTransformation(self, mesh, matrix):
2037 """Transform object coordinates to world coordinates.
2039 This step is done simply applying to the object its tranformation
2040 matrix and recalculating its normals.
2042 # XXX FIXME: blender do not transform normals in the right way when
2043 # there are negative scale values
2044 if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
2045 print "WARNING: Negative scales, expect incorrect results!"
2047 mesh.transform(matrix, True)
2049 def _doBackFaceCulling(self, mesh):
2050 """Simple Backface Culling routine.
2052 At this level we simply do a visibility test face by face and then
2053 select the vertices belonging to visible faces.
2056 # Select all vertices, so edges can be displayed even if there are no
2058 for v in mesh.verts:
2061 Mesh.Mode(Mesh.SelectModes['FACE'])
2063 for f in mesh.faces:
2065 if self._isFaceVisible(f):
2068 def _doLighting(self, mesh):
2069 """Apply an Illumination and shading model to the object.
2071 The model used is the Phong one, it may be inefficient,
2072 but I'm just learning about rendering and starting from Phong seemed
2073 the most natural way.
2076 # If the mesh has vertex colors already, use them,
2077 # otherwise turn them on and do some calculations
2078 if mesh.vertexColors:
2080 mesh.vertexColors = 1
2082 materials = mesh.materials
2084 camPos = self._getObjPosition(self.cameraObj)
2086 # We do per-face color calculation (FLAT Shading), we can easily turn
2087 # to a per-vertex calculation if we want to implement some shading
2088 # technique. For an example see:
2089 # http://www.miralab.unige.ch/papers/368.pdf
2090 for f in mesh.faces:
2096 mat = materials[f.mat]
2098 # A new default material
2100 mat = Material.New('defMat')
2102 # Check if it is a shadeless material
2103 elif mat.getMode() & Material.Modes['SHADELESS']:
2105 # Convert to a value between 0 and 255
2106 tmp_col = [ int(c * 255.0) for c in I]
2117 # do vertex color calculation
2119 TotDiffSpec = Vector([0.0, 0.0, 0.0])
2121 for l in self.lights:
2123 light_pos = self._getObjPosition(l)
2124 light = light_obj.getData()
2126 L = Vector(light_pos).normalize()
2128 V = (Vector(camPos) - Vector(f.cent)).normalize()
2130 N = Vector(f.no).normalize()
2132 if config.polygons['SHADING'] == 'TOON':
2133 NL = ShadingUtils.toonShading(N*L)
2137 # Should we use NL instead of (N*L) here?
2138 R = 2 * (N*L) * N - L
2140 Ip = light.getEnergy()
2142 # Diffuse co-efficient
2143 kd = mat.getRef() * Vector(mat.getRGBCol())
2145 kd[i] *= light.col[i]
2147 Idiff = Ip * kd * max(0, NL)
2150 # Specular component
2151 ks = mat.getSpec() * Vector(mat.getSpecCol())
2152 ns = mat.getHardness()
2153 Ispec = Ip * ks * pow(max(0, (V*R)), ns)
2155 TotDiffSpec += (Idiff+Ispec)
2159 Iamb = Vector(Blender.World.Get()[0].getAmb())
2162 # Emissive component (convert to a triplet)
2163 ki = Vector([mat.getEmit()]*3)
2165 #I = ki + Iamb + (Idiff + Ispec)
2166 I = ki + (ka * Iamb) + TotDiffSpec
2169 # Set Alpha component
2171 I.append(mat.getAlpha())
2173 # Clamp I values between 0 and 1
2174 I = [ min(c, 1) for c in I]
2175 I = [ max(0, c) for c in I]
2177 # Convert to a value between 0 and 255
2178 tmp_col = [ int(c * 255.0) for c in I]
2186 def _doProjection(self, mesh, projector):
2187 """Apply Viewing and Projection tranformations.
2190 for v in mesh.verts:
2191 p = projector.doProjection(v.co[:])
2196 #mesh.recalcNormals()
2199 # We could reeset Camera matrix, since now
2200 # we are in Normalized Viewing Coordinates,
2201 # but doung that would affect World Coordinate
2202 # processing for other objects
2204 #self.cameraObj.data.type = 1
2205 #self.cameraObj.data.scale = 2.0
2206 #m = Matrix().identity()
2207 #self.cameraObj.setMatrix(m)
2209 def _doViewFrustumClipping(self, mesh):
2210 """Clip faces against the View Frustum.
2214 def __simpleDepthSort(self, mesh):
2215 """Sort faces by the furthest vertex.
2217 This simple mesthod is known also as the painter algorithm, and it
2218 solves HSR correctly only for convex meshes.
2223 # The sorting requires circa n*log(n) steps
2225 progress.setActivity("HSR: Painter", n*log(n))
2227 by_furthest_z = (lambda f1, f2: progress.update() and
2228 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
2231 # FIXME: using NMesh to sort faces. We should avoid that!
2232 nmesh = NMesh.GetRaw(mesh.name)
2234 # remember that _higher_ z values mean further points
2235 nmesh.faces.sort(by_furthest_z)
2236 nmesh.faces.reverse()
2241 def __newellDepthSort(self, mesh):
2242 """Newell's depth sorting.
2248 # Find non planar quads and convert them to triangle
2249 #for f in mesh.faces:
2251 # if is_nonplanar_quad(f.v):
2252 # print "NON QUAD??"
2256 # Now reselect all faces
2257 for f in mesh.faces:
2259 mesh.quadToTriangle()
2261 # FIXME: using NMesh to sort faces. We should avoid that!
2262 nmesh = NMesh.GetRaw(mesh.name)
2264 # remember that _higher_ z values mean further points
2265 nmesh.faces.sort(by_furthest_z)
2266 nmesh.faces.reverse()
2268 # Begin depth sort tests
2270 # use the smooth flag to set marked faces
2271 for f in nmesh.faces:
2274 facelist = nmesh.faces[:]
2278 # The steps are _at_least_ equal to len(facelist), we do not count the
2279 # feces coming out from splitting!!
2280 progress.setActivity("HSR: Newell", len(facelist))
2281 #progress.setQuiet(True)
2284 while len(facelist):
2285 debug("\n----------------------\n")
2286 debug("len(facelits): %d\n" % len(facelist))
2289 pSign = sign(P.normal[2])
2291 # We can discard faces parallel to the view vector
2292 #if P.normal[2] == 0:
2293 # facelist.remove(P)
2299 for Q in facelist[1:]:
2301 debug("P.smooth: " + str(P.smooth) + "\n")
2302 debug("Q.smooth: " + str(Q.smooth) + "\n")
2305 qSign = sign(Q.normal[2])
2306 # TODO: check also if Q is parallel??
2308 # Test 0: We need to test only those Qs whose furthest vertex
2309 # is closer to the observer than the closest vertex of P.
2311 zP = [v.co[2] for v in P.v]
2312 zQ = [v.co[2] for v in Q.v]
2313 notZOverlap = min(zP) > max(zQ) + EPS
2317 debug("NOT Z OVERLAP!\n")
2319 # If Q is not marked then we can safely print P
2322 debug("met a marked face\n")
2326 # Test 1: X extent overlapping
2327 xP = [v.co[0] for v in P.v]
2328 xQ = [v.co[0] for v in Q.v]
2329 #notXOverlap = (max(xP) <= min(xQ)) or (max(xQ) <= min(xP))
2330 notXOverlap = (min(xQ) >= max(xP)-EPS) or (min(xP) >= max(xQ)-EPS)
2334 debug("NOT X OVERLAP!\n")
2338 # Test 2: Y extent Overlapping
2339 yP = [v.co[1] for v in P.v]
2340 yQ = [v.co[1] for v in Q.v]
2341 #notYOverlap = (max(yP) <= min(yQ)) or (max(yQ) <= min(yP))
2342 notYOverlap = (min(yQ) >= max(yP)-EPS) or (min(yP) >= max(yQ)-EPS)
2346 debug("NOT Y OVERLAP!\n")
2350 # Test 3: P vertices are all behind the plane of Q
2353 d = qSign * HSR.Distance(Vector(Pi), Q)
2356 pVerticesBehindPlaneQ = (n == len(P))
2358 if pVerticesBehindPlaneQ:
2360 debug("P BEHIND Q!\n")
2364 # Test 4: Q vertices in front of the plane of P
2367 d = pSign * HSR.Distance(Vector(Qi), P)
2370 qVerticesInFrontPlaneP = (n == len(Q))
2372 if qVerticesInFrontPlaneP:
2374 debug("Q IN FRONT OF P!\n")
2378 # Test 5: Check if projections of polygons effectively overlap,
2379 # in previous tests we checked only bounding boxes.
2381 #if not projectionsOverlap(P, Q):
2382 if not ( HSR.projectionsOverlap(P, Q) or HSR.projectionsOverlap(Q, P)):
2384 debug("Projections do not overlap!\n")
2387 # We still can't say if P obscures Q.
2389 # But if Q is marked we do a face-split trying to resolve a
2390 # difficulty (maybe a visibility cycle).
2393 debug("Possibly a cycle detected!\n")
2394 debug("Split here!!\n")
2396 facelist = HSR.facesplit(P, Q, facelist, nmesh)
2400 # The question now is: Does Q obscure P?
2403 # Test 3bis: Q vertices are all behind the plane of P
2406 d = pSign * HSR.Distance(Vector(Qi), P)
2409 qVerticesBehindPlaneP = (n == len(Q))
2411 if qVerticesBehindPlaneP:
2412 debug("\nTest 3bis\n")
2413 debug("Q BEHIND P!\n")
2416 # Test 4bis: P vertices in front of the plane of Q
2419 d = qSign * HSR.Distance(Vector(Pi), Q)
2422 pVerticesInFrontPlaneQ = (n == len(P))
2424 if pVerticesInFrontPlaneQ:
2425 debug("\nTest 4bis\n")
2426 debug("P IN FRONT OF Q!\n")
2429 # We don't even know if Q does obscure P, so they should
2430 # intersect each other, split one of them in two parts.
2431 if not qVerticesBehindPlaneP and not pVerticesInFrontPlaneQ:
2432 debug("\nSimple Intersection?\n")
2433 debug("Test 3bis or 4bis failed\n")
2434 debug("Split here!!2\n")
2436 facelist = HSR.facesplit(P, Q, facelist, nmesh)
2441 facelist.insert(0, Q)
2444 debug("Q marked!\n")
2448 if split_done == 0 and face_marked == 0:
2451 dumpfaces(maplist, "dump"+str(len(maplist)).zfill(4)+".svg")
2455 if len(facelist) == 870:
2456 dumpfaces([P, Q], "loopdebug.svg")
2459 #if facelist == None:
2461 # print [v.co for v in P]
2462 # print [v.co for v in Q]
2465 # end of while len(facelist)
2468 nmesh.faces = maplist
2469 #for f in nmesh.faces:
2475 def _doHiddenSurfaceRemoval(self, mesh):
2476 """Do HSR for the given mesh.
2478 if len(mesh.faces) == 0:
2481 if config.polygons['HSR'] == 'PAINTER':
2482 print "\nUsing the Painter algorithm for HSR."
2483 self.__simpleDepthSort(mesh)
2485 elif config.polygons['HSR'] == 'NEWELL':
2486 print "\nUsing the Newell's algorithm for HSR."
2487 self.__newellDepthSort(mesh)
2490 def _doEdgesStyle(self, mesh, edgestyleSelect):
2491 """Process Mesh Edges accroding to a given selection style.
2493 Examples of algorithms:
2496 given an edge if its adjacent faces have the same normal (that is
2497 they are complanar), than deselect it.
2500 given an edge if one its adjacent faces is frontfacing and the
2501 other is backfacing, than select it, else deselect.
2504 Mesh.Mode(Mesh.SelectModes['EDGE'])
2506 edge_cache = MeshUtils.buildEdgeFaceUsersCache(mesh)
2508 for i,edge_faces in enumerate(edge_cache):
2509 mesh.edges[i].sel = 0
2510 if edgestyleSelect(edge_faces):
2511 mesh.edges[i].sel = 1
2514 for e in mesh.edges:
2517 if edgestyleSelect(e, mesh):
2522 # ---------------------------------------------------------------------
2524 ## GUI Class and Main Program
2526 # ---------------------------------------------------------------------
2529 from Blender import BGL, Draw
2530 from Blender.BGL import *
2536 # Output Format menu
2537 output_format = config.output['FORMAT']
2538 default_value = outputWriters.keys().index(output_format)+1
2539 GUI.outFormatMenu = Draw.Create(default_value)
2540 GUI.evtOutFormatMenu = 0
2542 # Animation toggle button
2543 GUI.animToggle = Draw.Create(config.output['ANIMATION'])
2544 GUI.evtAnimToggle = 1
2546 # Join Objects toggle button
2547 GUI.joinObjsToggle = Draw.Create(config.output['JOIN_OBJECTS'])
2548 GUI.evtJoinObjsToggle = 2
2550 # Render filled polygons
2551 GUI.polygonsToggle = Draw.Create(config.polygons['SHOW'])
2553 # Shading Style menu
2554 shading_style = config.polygons['SHADING']
2555 default_value = shadingStyles.keys().index(shading_style)+1
2556 GUI.shadingStyleMenu = Draw.Create(default_value)
2557 GUI.evtShadingStyleMenu = 21
2559 GUI.evtPolygonsToggle = 3
2560 # We hide the config.polygons['EXPANSION_TRICK'], for now
2562 # Render polygon edges
2563 GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
2564 GUI.evtShowEdgesToggle = 4
2566 # Render hidden edges
2567 GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
2568 GUI.evtShowHiddenEdgesToggle = 5
2571 edge_style = config.edges['STYLE']
2572 default_value = edgeStyles.keys().index(edge_style)+1
2573 GUI.edgeStyleMenu = Draw.Create(default_value)
2574 GUI.evtEdgeStyleMenu = 6
2577 GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
2578 GUI.evtEdgeWidthSlider = 7
2581 c = config.edges['COLOR']
2582 GUI.edgeColorPicker = Draw.Create(c[0]/255.0, c[1]/255.0, c[2]/255.0)
2583 GUI.evtEdgeColorPicker = 71
2586 GUI.evtRenderButton = 8
2589 GUI.evtExitButton = 9
2593 # initialize static members
2596 glClear(GL_COLOR_BUFFER_BIT)
2597 glColor3f(0.0, 0.0, 0.0)
2598 glRasterPos2i(10, 350)
2599 Draw.Text("VRM: Vector Rendering Method script. Version %s." %
2601 glRasterPos2i(10, 335)
2602 Draw.Text("Press Q or ESC to quit.")
2604 # Build the output format menu
2605 glRasterPos2i(10, 310)
2606 Draw.Text("Select the output Format:")
2607 outMenuStruct = "Output Format %t"
2608 for t in outputWriters.keys():
2609 outMenuStruct = outMenuStruct + "|%s" % t
2610 GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
2611 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
2614 GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
2615 10, 260, 160, 18, GUI.animToggle.val,
2616 "Toggle rendering of animations")
2618 # Join Objects toggle
2619 GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
2620 10, 235, 160, 18, GUI.joinObjsToggle.val,
2621 "Join objects in the rendered file")
2624 Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
2626 Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
2629 glRasterPos2i(200, 310)
2630 Draw.Text("Rendering Style:")
2633 GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
2634 200, 285, 160, 18, GUI.polygonsToggle.val,
2635 "Render filled polygons")
2637 if GUI.polygonsToggle.val == 1:
2639 # Polygon Shading Style
2640 shadingStyleMenuStruct = "Shading Style %t"
2641 for t in shadingStyles.keys():
2642 shadingStyleMenuStruct = shadingStyleMenuStruct + "|%s" % t.lower()
2643 GUI.shadingStyleMenu = Draw.Menu(shadingStyleMenuStruct, GUI.evtShadingStyleMenu,
2644 200, 260, 160, 18, GUI.shadingStyleMenu.val,
2645 "Choose the shading style")
2649 GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
2650 200, 235, 160, 18, GUI.showEdgesToggle.val,
2651 "Render polygon edges")
2653 if GUI.showEdgesToggle.val == 1:
2656 edgeStyleMenuStruct = "Edge Style %t"
2657 for t in edgeStyles.keys():
2658 edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t.lower()
2659 GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
2660 200, 210, 160, 18, GUI.edgeStyleMenu.val,
2661 "Choose the edge style")
2664 GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
2665 200, 185, 140, 18, GUI.edgeWidthSlider.val,
2666 0.0, 10.0, 0, "Change Edge Width")
2669 GUI.edgeColorPicker = Draw.ColorPicker(GUI.evtEdgeColorPicker,
2670 342, 185, 18, 18, GUI.edgeColorPicker.val, "Choose Edge Color")
2673 GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
2674 GUI.evtShowHiddenEdgesToggle,
2675 200, 160, 160, 18, GUI.showHiddenEdgesToggle.val,
2676 "Render hidden edges as dashed lines")
2678 glRasterPos2i(10, 160)
2679 Draw.Text("%s (c) 2006" % __author__)
2681 def event(evt, val):
2683 if evt == Draw.ESCKEY or evt == Draw.QKEY:
2690 def button_event(evt):
2692 if evt == GUI.evtExitButton:
2695 elif evt == GUI.evtOutFormatMenu:
2696 i = GUI.outFormatMenu.val - 1
2697 config.output['FORMAT']= outputWriters.keys()[i]
2699 elif evt == GUI.evtAnimToggle:
2700 config.output['ANIMATION'] = bool(GUI.animToggle.val)
2702 elif evt == GUI.evtJoinObjsToggle:
2703 config.output['JOIN_OBJECTS'] = bool(GUI.joinObjsToggle.val)
2705 elif evt == GUI.evtPolygonsToggle:
2706 config.polygons['SHOW'] = bool(GUI.polygonsToggle.val)
2708 elif evt == GUI.evtShadingStyleMenu:
2709 i = GUI.shadingStyleMenu.val - 1
2710 config.polygons['SHADING'] = shadingStyles.keys()[i]
2712 elif evt == GUI.evtShowEdgesToggle:
2713 config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
2715 elif evt == GUI.evtShowHiddenEdgesToggle:
2716 config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
2718 elif evt == GUI.evtEdgeStyleMenu:
2719 i = GUI.edgeStyleMenu.val - 1
2720 config.edges['STYLE'] = edgeStyles.keys()[i]
2722 elif evt == GUI.evtEdgeWidthSlider:
2723 config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
2725 elif evt == GUI.evtEdgeColorPicker:
2726 config.edges['COLOR'] = [int(c*255.0) for c in GUI.edgeColorPicker.val]
2728 elif evt == GUI.evtRenderButton:
2729 label = "Save %s" % config.output['FORMAT']
2730 # Show the File Selector
2732 Blender.Window.FileSelector(vectorize, label, outputfile)
2735 print "Event: %d not handled!" % evt
2742 from pprint import pprint
2744 pprint(config.output)
2745 pprint(config.polygons)
2746 pprint(config.edges)
2748 _init = staticmethod(_init)
2749 draw = staticmethod(draw)
2750 event = staticmethod(event)
2751 button_event = staticmethod(button_event)
2752 conf_debug = staticmethod(conf_debug)
2754 # A wrapper function for the vectorizing process
2755 def vectorize(filename):
2756 """The vectorizing process is as follows:
2758 - Instanciate the writer and the renderer
2763 print "\nERROR: invalid file name!"
2766 from Blender import Window
2767 editmode = Window.EditMode()
2768 if editmode: Window.EditMode(0)
2770 actualWriter = outputWriters[config.output['FORMAT']]
2771 writer = actualWriter(filename)
2773 renderer = Renderer()
2774 renderer.doRendering(writer, config.output['ANIMATION'])
2776 if editmode: Window.EditMode(1)
2781 if __name__ == "__main__":
2786 basename = Blender.sys.basename(Blender.Get('filename'))
2788 outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
2790 if Blender.mode == 'background':
2791 progress = ConsoleProgressIndicator()
2792 vectorize(outputfile)
2794 progress = GraphicalProgressIndicator()
2795 Draw.Register(GUI.draw, GUI.event, GUI.button_event)