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 (using ming)
79 # * Fixed a bug in the animation code, now the projection matrix is
80 # recalculated at each frame!
81 # * PDF output (using reportlab)
82 # * Fixed another problem in the animation code the current frame was off
84 # * Use fps as specified in blender when VectorWriter handles animation
85 # * Remove the real file opening in the abstract VectorWriter
87 # ---------------------------------------------------------------------
90 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera, Window
91 from Blender.Mathutils import *
98 # We use a global progress Indicator Object
102 # Some global settings
106 polygons['SHOW'] = True
107 polygons['SHADING'] = 'FLAT' # FLAT or TOON
108 polygons['HSR'] = 'NEWELL' # PAINTER or NEWELL
109 # Hidden to the user for now
110 polygons['EXPANSION_TRICK'] = True
112 polygons['TOON_LEVELS'] = 2
115 edges['SHOW'] = False
116 edges['SHOW_HIDDEN'] = False
117 edges['STYLE'] = 'MESH' # MESH or SILHOUETTE
119 edges['COLOR'] = [0, 0, 0]
122 output['FORMAT'] = 'PDF'
123 #output['ANIMATION'] = False
124 output['ANIMATION'] = True
125 output['JOIN_OBJECTS'] = True
131 def dumpfaces(flist, filename):
132 """Dump a single face to a file.
143 writerobj = SVGVectorWriter(filename)
146 writerobj._printPolygons(m)
152 sys.stderr.write(msg)
155 return (abs(v1[0]-v2[0]) < EPS and
156 abs(v1[1]-v2[1]) < EPS )
157 by_furthest_z = (lambda f1, f2:
158 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
173 # ---------------------------------------------------------------------
177 # ---------------------------------------------------------------------
183 """A utility class for HSR processing.
186 def is_nonplanar_quad(face):
187 """Determine if a quad is non-planar.
189 From: http://mathworld.wolfram.com/Coplanar.html
191 Geometric objects lying in a common plane are said to be coplanar.
192 Three noncollinear points determine a plane and so are trivially coplanar.
193 Four points are coplanar iff the volume of the tetrahedron defined by them is
199 | x_4 y_4 z_4 1 | == 0
201 Coplanarity is equivalent to the statement that the pair of lines
202 determined by the four points are not skew, and can be equivalently stated
203 in vector form as (x_3-x_1).[(x_2-x_1)x(x_4-x_3)]==0.
205 An arbitrary number of n points x_1, ..., x_n can be tested for
206 coplanarity by finding the point-plane distances of the points
207 x_4, ..., x_n from the plane determined by (x_1,x_2,x_3)
208 and checking if they are all zero.
209 If so, the points are all coplanar.
211 We here check only for 4-point complanarity.
217 print "ERROR a mesh in Blender can't have more than 4 vertices or less than 3"
221 # three points must be complanar
224 x1 = Vector(face[0].co)
225 x2 = Vector(face[1].co)
226 x3 = Vector(face[2].co)
227 x4 = Vector(face[3].co)
229 v = (x3-x1) * CrossVecs((x2-x1), (x4-x3))
235 is_nonplanar_quad = staticmethod(is_nonplanar_quad)
237 def pointInPolygon(poly, v):
240 pointInPolygon = staticmethod(pointInPolygon)
242 def edgeIntersection(s1, s2, do_perturbate=False):
244 (x1, y1) = s1[0].co[0], s1[0].co[1]
245 (x2, y2) = s1[1].co[0], s1[1].co[1]
247 (x3, y3) = s2[0].co[0], s2[0].co[1]
248 (x4, y4) = s2[1].co[0], s2[1].co[1]
256 # calculate delta values (vector components)
265 C = dy2 * dx1 - dx2 * dy1 # /* cross product */
266 if C == 0: #/* parallel */
269 dx3 = x1 - x3 # /* combined origin offset vector */
272 a1 = (dy3 * dx2 - dx3 * dy2) / C;
273 a2 = (dy3 * dx1 - dx3 * dy1) / C;
275 # check for degeneracies
277 #print_debug(str(a1)+"\n")
278 #print_debug(str(a2)+"\n\n")
280 if (a1 == 0 or a1 == 1 or a2 == 0 or a2 == 1):
281 # Intersection on boundaries, we consider the point external?
284 elif (a1>0.0 and a1<1.0 and a2>0.0 and a2<1.0): # /* lines cross */
290 return (NMesh.Vert(x, y, z), a1, a2)
293 # lines have intersections but not those segments
296 edgeIntersection = staticmethod(edgeIntersection)
298 def isVertInside(self, v):
302 # Create point at infinity
303 point_at_infinity = NMesh.Vert(-INF, v.co[1], -INF)
305 for i in range(len(self.v)):
306 s1 = (point_at_infinity, v)
307 s2 = (self.v[i-1], self.v[i])
309 if EQ(v.co, s2[0].co) or EQ(v.co, s2[1].co):
312 if HSR.edgeIntersection(s1, s2, do_perturbate=False):
316 if winding_number % 2 == 0 :
323 isVertInside = staticmethod(isVertInside)
327 return ((b[0] - a[0]) * (c[1] - a[1]) -
328 (b[1] - a[1]) * (c[0] - a[0]) )
330 det = staticmethod(det)
332 def pointInPolygon(q, P):
335 point_at_infinity = NMesh.Vert(-INF, q.co[1], -INF)
339 for i in range(len(P.v)):
342 if (det(q.co, point_at_infinity.co, p0.co)<0) != (det(q.co, point_at_infinity.co, p1.co)<0):
343 if det(p0.co, p1.co, q.co) == 0 :
346 elif (det(p0.co, p1.co, q.co)<0) != (det(p0.co, p1.co, point_at_infinity.co)<0):
351 pointInPolygon = staticmethod(pointInPolygon)
353 def projectionsOverlap(f1, f2):
354 """ If you have nonconvex, but still simple polygons, an acceptable method
355 is to iterate over all vertices and perform the Point-in-polygon test[1].
356 The advantage of this method is that you can compute the exact
357 intersection point and collision normal that you will need to simulate
358 collision. When you have the point that lies inside the other polygon, you
359 just iterate over all edges of the second polygon again and look for edge
360 intersections. Note that this method detects collsion when it already
361 happens. This algorithm is fast enough to perform it hundreds of times per
364 for i in range(len(f1.v)):
367 # If a point of f1 in inside f2, there is an overlap!
369 #if HSR.isVertInside(f2, v1):
370 if HSR.pointInPolygon(v1, f2):
373 # If not the polygon can be ovelap as well, so we check for
374 # intersection between an edge of f1 and all the edges of f2
378 for j in range(len(f2.v)):
385 intrs = HSR.edgeIntersection(e1, e2)
387 #print_debug(str(v0.co) + " " + str(v1.co) + " " +
388 # str(v2.co) + " " + str(v3.co) )
389 #print_debug("\nIntersection\n")
395 projectionsOverlap = staticmethod(projectionsOverlap)
397 def midpoint(p1, p2):
398 """Return the midpoint of two vertices.
400 m = MidpointVecs(Vector(p1), Vector(p2))
401 mv = NMesh.Vert(m[0], m[1], m[2])
405 midpoint = staticmethod(midpoint)
407 def facesplit(P, Q, facelist, nmesh):
408 """Split P or Q according to the strategy illustrated in the Newell's
412 by_furthest_z = (lambda f1, f2:
413 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
416 # Choose if split P on Q plane or vice-versa
420 d = HSR.Distance(Vector(Pi), Q)
423 pIntersectQ = (n != len(P))
427 d = HSR.Distance(Vector(Qi), P)
430 qIntersectP = (n != len(Q))
434 # 1. If parts of P lie in both half-spaces of Q
435 # then splice P in two with the plane of Q
441 newfaces = HSR.splitOn(plane, f)
443 # 2. Else if parts of Q lie in both half-space of P
444 # then splice Q in two with the plane of P
445 if qIntersectP and newfaces == None:
450 newfaces = HSR.splitOn(plane, f)
453 # 3. Else slice P in half through the mid-point of
454 # the longest pair of opposite sides
457 print "We ignore P..."
464 # v1 = midpoint(f[0], f[1])
465 # v2 = midpoint(f[1], f[2])
467 # v1 = midpoint(f[0], f[1])
468 # v2 = midpoint(f[2], f[3])
469 #vec3 = (Vector(v2)+10*Vector(f.normal))
471 #v3 = NMesh.Vert(vec3[0], vec3[1], vec3[2])
473 #plane = NMesh.Face([v1, v2, v3])
475 #newfaces = splitOn(plane, f)
479 print "Big FAT problem, we weren't able to split POLYGONS!"
485 # if v not in plane and v in nmesh.verts:
486 # nmesh.verts.remove(v)
491 nf.col = [f.col[0]] * len(nf.v)
496 nmesh.verts.append(v)
497 # insert pieces in the list
502 # and resort the faces
503 facelist.sort(by_furthest_z)
504 facelist.sort(lambda f1, f2: cmp(f1.smooth, f2.smooth))
507 #print [ f.smooth for f in facelist ]
511 facesplit = staticmethod(facesplit)
513 def isOnSegment(v1, v2, p, extremes_internal=False):
514 """Check if point p is in segment v1v2.
520 # Should we consider extreme points as internal ?
522 # if p == v1 or p == v2:
523 if l1 < EPS or l2 < EPS:
524 return extremes_internal
528 # if the sum of l1 and l2 is circa l, then the point is on segment,
529 if abs(l - (l1+l2)) < EPS:
534 isOnSegment = staticmethod(isOnSegment)
536 def Distance(point, face):
537 """ Calculate the distance between a point and a face.
539 An alternative but more expensive method can be:
541 ip = Intersect(Vector(face[0]), Vector(face[1]), Vector(face[2]),
542 Vector(face.no), Vector(point), 0)
544 d = Vector(ip - point).length
546 See: http://mathworld.wolfram.com/Point-PlaneDistance.html
550 plNormal = Vector(face.no)
551 plVert0 = Vector(face.v[0])
553 d = (plVert0 * plNormal) - (p * plNormal)
555 #d = plNormal * (plVert0 - p)
557 #print "\nd: %.10f - sel: %d, %s\n" % (d, face.sel, str(point))
561 Distance = staticmethod(Distance)
565 # make one or two new faces based on a list of vertex-indices
594 makeFaces = staticmethod(makeFaces)
597 """Split P using the plane of Q.
598 Logic taken from the knife.py python script
601 # Check if P and Q are parallel
602 u = CrossVecs(Vector(Q.no),Vector(P.no))
608 print "PARALLEL planes!!"
612 # The final aim is to find the intersection line between P
613 # and the plane of Q, and split P along this line
617 # Calculate point-plane Distance between vertices of P and plane Q
619 for i in range(0, nP):
620 d.append(HSR.Distance(P.v[i], Q))
633 #print "d0:", d0, "d1:", d1
635 # if the vertex lies in the cutplane
637 #print "d1 On cutplane"
638 posVertList.append(V1)
639 negVertList.append(V1)
641 # if the previous vertex lies in cutplane
643 #print "d0 on Cutplane"
645 #print "d1 on positive Halfspace"
646 posVertList.append(V1)
648 #print "d1 on negative Halfspace"
649 negVertList.append(V1)
651 # if they are on the same side of the plane
653 #print "On the same half-space"
655 #print "d1 on positive Halfspace"
656 posVertList.append(V1)
658 #print "d1 on negative Halfspace"
659 negVertList.append(V1)
661 # the vertices are not on the same side of the plane, so we have an intersection
663 #print "Intersection"
665 e = Vector(V0), Vector(V1)
666 tri = Vector(Q[0]), Vector(Q[1]), Vector(Q[2])
668 inters = Intersect(tri[0], tri[1], tri[2], e[1]-e[0], e[0], 0)
673 #print "Intersection", inters
675 nv = NMesh.Vert(inters[0], inters[1], inters[2])
676 newVertList.append(nv)
678 posVertList.append(nv)
679 negVertList.append(nv)
682 posVertList.append(V1)
684 negVertList.append(V1)
688 posVertList = [ u for u in posVertList if u not in locals()['_[1]'] ]
689 negVertList = [ u for u in negVertList if u not in locals()['_[1]'] ]
692 # If vertex are all on the same half-space, return
693 #if len(posVertList) < 3:
694 # print "Problem, we created a face with less that 3 verteices??"
696 #if len(negVertList) < 3:
697 # print "Problem, we created a face with less that 3 verteices??"
700 if len(posVertList) < 3 or len(negVertList) < 3:
701 print "RETURN NONE, SURE???"
705 newfaces = HSR.addNewFaces(posVertList, negVertList)
709 splitOn = staticmethod(splitOn)
711 def addNewFaces(posVertList, negVertList):
712 # Create new faces resulting from the split
714 if len(posVertList) or len(negVertList):
716 #newfaces = [posVertList] + [negVertList]
717 newfaces = ( [[ NMesh.Vert(v[0], v[1], v[2]) for v in posVertList]] +
718 [[ NMesh.Vert(v[0], v[1], v[2]) for v in negVertList]] )
722 outfaces += HSR.makeFaces(nf)
727 addNewFaces = staticmethod(addNewFaces)
730 # ---------------------------------------------------------------------
732 ## Mesh Utility class
734 # ---------------------------------------------------------------------
738 def buildEdgeFaceUsersCache(me):
740 Takes a mesh and returns a list aligned with the meshes edges.
741 Each item is a list of the faces that use the edge
742 would be the equiv for having ed.face_users as a property
744 Taken from .blender/scripts/bpymodules/BPyMesh.py,
745 thanks to ideasman_42.
748 def sorted_edge_indicies(ed):
756 face_edges_dict= dict([(sorted_edge_indicies(ed), (ed.index, [])) for ed in me.edges])
758 fvi= [v.index for v in f.v]# face vert idx's
759 for i in xrange(len(f)):
766 face_edges_dict[i1,i2][1].append(f)
768 face_edges= [None] * len(me.edges)
769 for ed_index, ed_faces in face_edges_dict.itervalues():
770 face_edges[ed_index]= ed_faces
774 def isMeshEdge(adjacent_faces):
777 A mesh edge is visible if _at_least_one_ of its adjacent faces is selected.
778 Note: if the edge has no adjacent faces we want to show it as well,
779 useful for "edge only" portion of objects.
782 if len(adjacent_faces) == 0:
785 selected_faces = [f for f in adjacent_faces if f.sel]
787 if len(selected_faces) != 0:
792 def isSilhouetteEdge(adjacent_faces):
793 """Silhuette selection rule.
795 An edge is a silhuette edge if it is shared by two faces with
796 different selection status or if it is a boundary edge of a selected
800 if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
801 (len(adjacent_faces) == 2 and
802 adjacent_faces[0].sel != adjacent_faces[1].sel)
808 buildEdgeFaceUsersCache = staticmethod(buildEdgeFaceUsersCache)
809 isMeshEdge = staticmethod(isMeshEdge)
810 isSilhouetteEdge = staticmethod(isSilhouetteEdge)
813 # ---------------------------------------------------------------------
815 ## Shading Utility class
817 # ---------------------------------------------------------------------
823 def toonShadingMapSetup():
824 levels = config.polygons['TOON_LEVELS']
826 texels = 2*levels - 1
827 tmp_shademap = [0.0] + [(i)/float(texels-1) for i in xrange(1, texels-1) ] + [1.0]
833 shademap = ShadingUtils.shademap
836 shademap = ShadingUtils.toonShadingMapSetup()
839 for i in xrange(0, len(shademap)-1):
840 pivot = (shademap[i]+shademap[i+1])/2.0
845 if v < shademap[i+1]:
850 toonShadingMapSetup = staticmethod(toonShadingMapSetup)
851 toonShading = staticmethod(toonShading)
854 # ---------------------------------------------------------------------
856 ## Projections classes
858 # ---------------------------------------------------------------------
861 """Calculate the projection of an object given the camera.
863 A projector is useful to so some per-object transformation to obtain the
864 projection of an object given the camera.
866 The main method is #doProjection# see the method description for the
870 def __init__(self, cameraObj, canvasRatio):
871 """Calculate the projection matrix.
873 The projection matrix depends, in this case, on the camera settings.
874 TAKE CARE: This projector expects vertices in World Coordinates!
877 camera = cameraObj.getData()
879 aspect = float(canvasRatio[0])/float(canvasRatio[1])
880 near = camera.clipStart
883 scale = float(camera.scale)
885 fovy = atan(0.5/aspect/(camera.lens/32))
886 fovy = fovy * 360.0/pi
888 # What projection do we want?
890 mP = self._calcPerspectiveMatrix(fovy, aspect, near, far)
891 elif camera.type == 1:
892 mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale)
894 # View transformation
895 cam = Matrix(cameraObj.getInverseMatrix())
900 self.projectionMatrix = mP
906 def doProjection(self, v):
907 """Project the point on the view plane.
909 Given a vertex calculate the projection using the current projection
913 # Note that we have to work on the vertex using homogeneous coordinates
914 # From blender 2.42+ we don't need to resize the vector to be 4d
915 # when applying a 4x4 matrix, but we do that anyway since we need the
916 # 4th coordinate later
917 p = self.projectionMatrix * Vector(v).resize4D()
919 # Perspective division
936 def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
937 """Return a perspective projection matrix.
940 top = near * tan(fovy * pi / 360.0)
944 x = (2.0 * near) / (right-left)
945 y = (2.0 * near) / (top-bottom)
946 a = (right+left) / (right-left)
947 b = (top+bottom) / (top - bottom)
948 c = - ((far+near) / (far-near))
949 d = - ((2*far*near)/(far-near))
955 [0.0, 0.0, -1.0, 0.0])
959 def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
960 """Return an orthogonal projection matrix.
963 # The 11 in the formula was found emiprically
964 top = near * tan(fovy * pi / 360.0) * (scale * 11)
966 left = bottom * aspect
971 tx = -((right+left)/rl)
972 ty = -((top+bottom)/tb)
976 [2.0/rl, 0.0, 0.0, tx],
977 [0.0, 2.0/tb, 0.0, ty],
978 [0.0, 0.0, 2.0/fn, tz],
979 [0.0, 0.0, 0.0, 1.0])
984 # ---------------------------------------------------------------------
986 ## Progress Indicator
988 # ---------------------------------------------------------------------
991 """A model for a progress indicator.
993 Do the progress calculation calculation and
994 the view independent stuff of a progress indicator.
996 def __init__(self, steps=0):
1002 def setSteps(self, steps):
1003 """Set the number of steps of the activity wich we want to track.
1010 def setName(self, name):
1011 """Set the name of the activity wich we want to track.
1018 def getProgress(self):
1019 return self.progress
1026 """Update the model, call this method when one step is completed.
1028 if self.progress == 100:
1032 self.progress = ( float(self.completed) / float(self.steps) ) * 100
1033 self.progress = int(self.progress)
1038 class ProgressIndicator:
1039 """An abstraction of a View for the Progress Model
1043 # Use a refresh rate so we do not show the progress at
1044 # every update, but every 'self.refresh_rate' times.
1045 self.refresh_rate = 10
1046 self.shows_counter = 0
1050 self.progressModel = None
1052 def setQuiet(self, value):
1055 def setActivity(self, name, steps):
1056 """Initialize the Model.
1058 In a future version (with subactivities-progress support) this method
1059 could only set the current activity.
1061 self.progressModel = Progress()
1062 self.progressModel.setName(name)
1063 self.progressModel.setSteps(steps)
1065 def getActivity(self):
1066 return self.progressModel
1069 """Update the model and show the actual progress.
1071 assert(self.progressModel)
1073 if self.progressModel.update():
1077 self.show(self.progressModel.getProgress(),
1078 self.progressModel.getName())
1080 # We return always True here so we can call the update() method also
1081 # from lambda funcs (putting the call in logical AND with other ops)
1084 def show(self, progress, name=""):
1085 self.shows_counter = (self.shows_counter + 1) % self.refresh_rate
1086 if self.shows_counter != 0:
1090 self.shows_counter = -1
1093 class ConsoleProgressIndicator(ProgressIndicator):
1094 """Show a progress bar on stderr, a la wget.
1097 ProgressIndicator.__init__(self)
1099 self.swirl_chars = ["-", "\\", "|", "/"]
1100 self.swirl_count = -1
1102 def show(self, progress, name):
1103 ProgressIndicator.show(self, progress, name)
1106 bar_progress = int( (progress/100.0) * bar_length )
1107 bar = ("=" * bar_progress).ljust(bar_length)
1109 self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1110 swirl_char = self.swirl_chars[self.swirl_count]
1112 progress_bar = "%s |%s| %c %3d%%" % (name, bar, swirl_char, progress)
1114 sys.stderr.write(progress_bar+"\r")
1116 sys.stderr.write("\n")
1119 class GraphicalProgressIndicator(ProgressIndicator):
1120 """Interface to the Blender.Window.DrawProgressBar() method.
1123 ProgressIndicator.__init__(self)
1125 #self.swirl_chars = ["-", "\\", "|", "/"]
1126 # We have to use letters with the same width, for now!
1127 # Blender progress bar considers the font widths when
1128 # calculating the progress bar width.
1129 self.swirl_chars = ["\\", "/"]
1130 self.swirl_count = -1
1132 def show(self, progress, name):
1133 ProgressIndicator.show(self, progress)
1135 self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1136 swirl_char = self.swirl_chars[self.swirl_count]
1138 progress_text = "%s - %c %3d%%" % (name, swirl_char, progress)
1140 # Finally draw the Progress Bar
1141 Window.WaitCursor(1) # Maybe we can move that call in the constructor?
1142 Window.DrawProgressBar(progress/100.0, progress_text)
1145 Window.DrawProgressBar(1, progress_text)
1146 Window.WaitCursor(0)
1150 # ---------------------------------------------------------------------
1152 ## 2D Object representation class
1154 # ---------------------------------------------------------------------
1156 # TODO: a class to represent the needed properties of a 2D vector image
1157 # For now just using a [N]Mesh structure.
1160 # ---------------------------------------------------------------------
1162 ## Vector Drawing Classes
1164 # ---------------------------------------------------------------------
1170 A class for printing output in a vectorial format.
1172 Given a 2D representation of the 3D scene the class is responsible to
1173 write it is a vector format.
1175 Every subclasses of VectorWriter must have at last the following public
1179 - printCanvas(self, scene,
1180 doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
1183 def __init__(self, fileName):
1184 """Set the output file name and other properties"""
1186 self.outputFileName = fileName
1188 context = Scene.GetCurrent().getRenderingContext()
1189 self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
1191 self.fps = context.fps
1195 self.animation = False
1202 def open(self, startFrame=1, endFrame=1):
1203 if startFrame != endFrame:
1204 self.startFrame = startFrame
1205 self.endFrame = endFrame
1206 self.animation = True
1208 print "Outputting to: ", self.outputFileName
1215 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1216 showHiddenEdges=False):
1217 """This is the interface for the needed printing routine.
1224 class SVGVectorWriter(VectorWriter):
1225 """A concrete class for writing SVG output.
1228 def __init__(self, fileName):
1229 """Simply call the parent Contructor.
1231 VectorWriter.__init__(self, fileName)
1240 def open(self, startFrame=1, endFrame=1):
1241 """Do some initialization operations.
1243 VectorWriter.open(self, startFrame, endFrame)
1245 self.file = open(self.outputFileName, "w")
1250 """Do some finalization operation.
1257 # remember to call the close method of the parent as last
1258 VectorWriter.close(self)
1261 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1262 showHiddenEdges=False):
1263 """Convert the scene representation to SVG.
1266 Objects = scene.getChildren()
1268 context = scene.getRenderingContext()
1269 framenumber = context.currentFrame()
1272 framestyle = "display:none"
1274 framestyle = "display:block"
1276 # Assign an id to this group so we can set properties on it using DOM
1277 self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
1278 (framenumber, framestyle) )
1283 if(obj.getType() != 'Mesh'):
1286 self.file.write("<g id=\"%s\">\n" % obj.getName())
1288 mesh = obj.getData(mesh=1)
1291 self._printPolygons(mesh)
1294 self._printEdges(mesh, showHiddenEdges)
1296 self.file.write("</g>\n")
1298 self.file.write("</g>\n")
1305 def _calcCanvasCoord(self, v):
1306 """Convert vertex in scene coordinates to canvas coordinates.
1309 pt = Vector([0, 0, 0])
1311 mW = float(self.canvasSize[0])/2.0
1312 mH = float(self.canvasSize[1])/2.0
1314 # rescale to canvas size
1315 pt[0] = v.co[0]*mW + mW
1316 pt[1] = v.co[1]*mH + mH
1319 # For now we want (0,0) in the top-left corner of the canvas.
1320 # Mirror and translate along y
1322 pt[1] += self.canvasSize[1]
1326 def _printHeader(self):
1327 """Print SVG header."""
1329 self.file.write("<?xml version=\"1.0\"?>\n")
1330 self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n")
1331 self.file.write("\t\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
1332 self.file.write("<svg version=\"1.0\"\n")
1333 self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
1334 self.file.write("\twidth=\"%d\" height=\"%d\">\n\n" %
1338 delay = 1000/self.fps
1340 self.file.write("""\n<script type="text/javascript"><![CDATA[
1341 globalStartFrame=%d;
1344 timerID = setInterval("NextFrame()", %d);
1345 globalFrameCounter=%d;
1346 \n""" % (self.startFrame, self.endFrame, delay, self.startFrame) )
1348 self.file.write("""\n
1349 function NextFrame()
1351 currentElement = document.getElementById('frame'+globalFrameCounter)
1352 previousElement = document.getElementById('frame'+(globalFrameCounter-1))
1354 if (!currentElement)
1359 if (globalFrameCounter > globalEndFrame)
1361 clearInterval(timerID)
1367 previousElement.style.display="none";
1369 currentElement.style.display="block";
1370 globalFrameCounter++;
1376 def _printFooter(self):
1377 """Print the SVG footer."""
1379 self.file.write("\n</svg>\n")
1381 def _printPolygons(self, mesh):
1382 """Print the selected (visible) polygons.
1385 if len(mesh.faces) == 0:
1388 self.file.write("<g>\n")
1390 for face in mesh.faces:
1394 self.file.write("<path d=\"")
1396 #p = self._calcCanvasCoord(face.verts[0])
1397 p = self._calcCanvasCoord(face.v[0])
1398 self.file.write("M %g,%g L " % (p[0], p[1]))
1400 for v in face.v[1:]:
1401 p = self._calcCanvasCoord(v)
1402 self.file.write("%g,%g " % (p[0], p[1]))
1404 # get rid of the last blank space, just cosmetics here.
1405 self.file.seek(-1, 1)
1406 self.file.write(" z\"\n")
1408 # take as face color the first vertex color
1411 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1413 color = [255, 255, 255, 255]
1415 # Convert the color to the #RRGGBB form
1416 str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
1418 # Handle transparent polygons
1421 opacity = float(color[3])/255.0
1422 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
1423 #opacity_string = "opacity: %g;" % (opacity)
1425 self.file.write("\tstyle=\"fill:" + str_col + ";")
1426 self.file.write(opacity_string)
1428 # use the stroke property to alleviate the "adjacent edges" problem,
1429 # we simulate polygon expansion using borders,
1430 # see http://www.antigrain.com/svg/index.html for more info
1433 # EXPANSION TRICK is not that useful where there is transparency
1434 if config.polygons['EXPANSION_TRICK'] and color[3] == 255:
1435 # str_col = "#000000" # For debug
1436 self.file.write(" stroke:%s;\n" % str_col)
1437 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
1438 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
1440 self.file.write("\"/>\n")
1442 self.file.write("</g>\n")
1444 def _printEdges(self, mesh, showHiddenEdges=False):
1445 """Print the wireframe using mesh edges.
1448 stroke_width = config.edges['WIDTH']
1449 stroke_col = config.edges['COLOR']
1451 self.file.write("<g>\n")
1453 for e in mesh.edges:
1455 hidden_stroke_style = ""
1458 if showHiddenEdges == False:
1461 hidden_stroke_style = ";\n stroke-dasharray:3, 3"
1463 p1 = self._calcCanvasCoord(e.v1)
1464 p2 = self._calcCanvasCoord(e.v2)
1466 self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
1467 % ( p1[0], p1[1], p2[0], p2[1] ) )
1468 self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
1469 self.file.write(" stroke-width:"+str(stroke_width)+";\n")
1470 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
1471 self.file.write(hidden_stroke_style)
1472 self.file.write("\"/>\n")
1474 self.file.write("</g>\n")
1483 SWFSupported = False
1485 class SWFVectorWriter(VectorWriter):
1486 """A concrete class for writing SWF output.
1489 def __init__(self, fileName):
1490 """Simply call the parent Contructor.
1492 VectorWriter.__init__(self, fileName)
1502 def open(self, startFrame=1, endFrame=1):
1503 """Do some initialization operations.
1505 VectorWriter.open(self, startFrame, endFrame)
1506 self.movie = SWFMovie()
1507 self.movie.setDimension(self.canvasSize[0], self.canvasSize[1])
1509 self.movie.setRate(self.fps)
1510 numframes = endFrame - startFrame + 1
1511 self.movie.setFrames(numframes)
1514 """Do some finalization operation.
1516 self.movie.save(self.outputFileName)
1518 # remember to call the close method of the parent
1519 VectorWriter.close(self)
1521 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1522 showHiddenEdges=False):
1523 """Convert the scene representation to SVG.
1525 context = scene.getRenderingContext()
1526 framenumber = context.currentFrame()
1528 Objects = scene.getChildren()
1531 self.movie.remove(self.sprite)
1533 sprite = SWFSprite()
1537 if(obj.getType() != 'Mesh'):
1540 mesh = obj.getData(mesh=1)
1543 self._printPolygons(mesh, sprite)
1546 self._printEdges(mesh, sprite, showHiddenEdges)
1549 i = self.movie.add(sprite)
1550 # Remove the instance the next time
1553 self.movie.nextFrame()
1560 def _calcCanvasCoord(self, v):
1561 """Convert vertex in scene coordinates to canvas coordinates.
1564 pt = Vector([0, 0, 0])
1566 mW = float(self.canvasSize[0])/2.0
1567 mH = float(self.canvasSize[1])/2.0
1569 # rescale to canvas size
1570 pt[0] = v.co[0]*mW + mW
1571 pt[1] = v.co[1]*mH + mH
1574 # For now we want (0,0) in the top-left corner of the canvas.
1575 # Mirror and translate along y
1577 pt[1] += self.canvasSize[1]
1581 def _printPolygons(self, mesh, sprite):
1582 """Print the selected (visible) polygons.
1585 if len(mesh.faces) == 0:
1588 for face in mesh.faces:
1594 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1596 color = [255, 255, 255, 255]
1599 f = s.addFill(color[0], color[1], color[2], color[3])
1602 # The starting point of the shape
1603 p0 = self._calcCanvasCoord(face.verts[0])
1604 s.movePenTo(p0[0], p0[1])
1606 for v in face.verts[1:]:
1607 p = self._calcCanvasCoord(v)
1608 s.drawLineTo(p[0], p[1])
1611 s.drawLineTo(p0[0], p0[1])
1617 def _printEdges(self, mesh, sprite, showHiddenEdges=False):
1618 """Print the wireframe using mesh edges.
1621 stroke_width = config.edges['WIDTH']
1622 stroke_col = config.edges['COLOR']
1626 for e in mesh.edges:
1628 # Next, we set the line width and color for our shape.
1629 s.setLine(stroke_width, stroke_col[0], stroke_col[1], stroke_col[2],
1633 if showHiddenEdges == False:
1636 # SWF does not support dashed lines natively, so -for now-
1637 # draw hidden lines thinner and half-trasparent
1638 s.setLine(stroke_width/2, stroke_col[0], stroke_col[1],
1641 p1 = self._calcCanvasCoord(e.v1)
1642 p2 = self._calcCanvasCoord(e.v2)
1644 # FIXME: this is just a qorkaround, remove that after the
1645 # implementation of propoer Viewport clipping
1646 if abs(p1[0]) < 3000 and abs(p2[0]) < 3000 and abs(p1[1]) < 3000 and abs(p1[2]) < 3000:
1647 s.movePenTo(p1[0], p1[1])
1648 s.drawLineTo(p2[0], p2[1])
1658 from reportlab.pdfgen import canvas
1661 PDFSupported = False
1663 class PDFVectorWriter(VectorWriter):
1664 """A concrete class for writing PDF output.
1667 def __init__(self, fileName):
1668 """Simply call the parent Contructor.
1670 VectorWriter.__init__(self, fileName)
1679 def open(self, startFrame=1, endFrame=1):
1680 """Do some initialization operations.
1682 VectorWriter.open(self, startFrame, endFrame)
1683 size = (self.canvasSize[0], self.canvasSize[1])
1684 self.canvas = canvas.Canvas(self.outputFileName, pagesize=size, bottomup=0)
1687 """Do some finalization operation.
1691 # remember to call the close method of the parent
1692 VectorWriter.close(self)
1694 def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1695 showHiddenEdges=False):
1696 """Convert the scene representation to SVG.
1698 context = scene.getRenderingContext()
1699 framenumber = context.currentFrame()
1701 Objects = scene.getChildren()
1705 if(obj.getType() != 'Mesh'):
1708 mesh = obj.getData(mesh=1)
1711 self._printPolygons(mesh)
1714 self._printEdges(mesh, showHiddenEdges)
1716 self.canvas.showPage()
1722 def _calcCanvasCoord(self, v):
1723 """Convert vertex in scene coordinates to canvas coordinates.
1726 pt = Vector([0, 0, 0])
1728 mW = float(self.canvasSize[0])/2.0
1729 mH = float(self.canvasSize[1])/2.0
1731 # rescale to canvas size
1732 pt[0] = v.co[0]*mW + mW
1733 pt[1] = v.co[1]*mH + mH
1736 # For now we want (0,0) in the top-left corner of the canvas.
1737 # Mirror and translate along y
1739 pt[1] += self.canvasSize[1]
1743 def _printPolygons(self, mesh):
1744 """Print the selected (visible) polygons.
1747 if len(mesh.faces) == 0:
1750 for face in mesh.faces:
1756 color = [fcol.r/255.0, fcol.g/255.0, fcol.b/255.0,
1759 color = [1, 1, 1, 1]
1761 self.canvas.setFillColorRGB(color[0], color[1], color[2])
1763 self.canvas.setStrokeColorRGB(0, 0, 0)
1765 path = self.canvas.beginPath()
1767 # The starting point of the path
1768 p0 = self._calcCanvasCoord(face.verts[0])
1769 path.moveTo(p0[0], p0[1])
1771 for v in face.verts[1:]:
1772 p = self._calcCanvasCoord(v)
1773 path.lineTo(p[0], p[1])
1778 self.canvas.drawPath(path, stroke=0, fill=1)
1780 def _printEdges(self, mesh, showHiddenEdges=False):
1781 """Print the wireframe using mesh edges.
1784 stroke_width = config.edges['WIDTH']
1785 stroke_col = config.edges['COLOR']
1787 self.canvas.setLineCap(1)
1788 self.canvas.setLineJoin(1)
1789 self.canvas.setLineWidth(stroke_width)
1790 self.canvas.setStrokeColorRGB(stroke_col[0]/255.0, stroke_col[1]/255.0,
1793 for e in mesh.edges:
1795 self.canvas.setLineWidth(stroke_width)
1798 if showHiddenEdges == False:
1801 # PDF does not support dashed lines natively, so -for now-
1802 # draw hidden lines thinner
1803 self.canvas.setLineWidth(stroke_width/2.0)
1805 p1 = self._calcCanvasCoord(e.v1)
1806 p2 = self._calcCanvasCoord(e.v2)
1808 # FIXME: this is just a workaround, remove that after the
1809 # implementation of propoer Viewport clipping
1810 if abs(p1[0]) < 3000 and abs(p2[0]) < 3000 and abs(p1[1]) < 3000 and abs(p1[2]) < 3000:
1811 self.canvas.line(p1[0], p1[1], p2[0], p2[1])
1815 # ---------------------------------------------------------------------
1817 ## Rendering Classes
1819 # ---------------------------------------------------------------------
1821 # A dictionary to collect different shading style methods
1822 shadingStyles = dict()
1823 shadingStyles['FLAT'] = None
1824 shadingStyles['TOON'] = None
1826 # A dictionary to collect different edge style methods
1828 edgeStyles['MESH'] = MeshUtils.isMeshEdge
1829 edgeStyles['SILHOUETTE'] = MeshUtils.isSilhouetteEdge
1831 # A dictionary to collect the supported output formats
1832 outputWriters = dict()
1833 outputWriters['SVG'] = SVGVectorWriter
1835 outputWriters['SWF'] = SWFVectorWriter
1837 outputWriters['PDF'] = PDFVectorWriter
1841 """Render a scene viewed from the active camera.
1843 This class is responsible of the rendering process, transformation and
1844 projection of the objects in the scene are invoked by the renderer.
1846 The rendering is done using the active camera for the current scene.
1850 """Make the rendering process only for the current scene by default.
1852 We will work on a copy of the scene, to be sure that the current scene do
1853 not get modified in any way.
1856 # Render the current Scene, this should be a READ-ONLY property
1857 self._SCENE = Scene.GetCurrent()
1859 # Use the aspect ratio of the scene rendering context
1860 context = self._SCENE.getRenderingContext()
1862 aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
1863 self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
1864 float(context.aspectRatioY())
1867 # Render from the currently active camera
1868 #self.cameraObj = self._SCENE.getCurrentCamera()
1870 # Get the list of lighting sources
1871 obj_lst = self._SCENE.getChildren()
1872 self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
1874 # When there are no lights we use a default lighting source
1875 # that have the same position of the camera
1876 if len(self.lights) == 0:
1877 l = Lamp.New('Lamp')
1878 lobj = Object.New('Lamp')
1879 lobj.loc = self.cameraObj.loc
1881 self.lights.append(lobj)
1888 def doRendering(self, outputWriter, animation=False):
1889 """Render picture or animation and write it out.
1892 - a Vector writer object that will be used to output the result.
1893 - a flag to tell if we want to render an animation or only the
1897 context = self._SCENE.getRenderingContext()
1898 origCurrentFrame = context.currentFrame()
1900 # Handle the animation case
1902 startFrame = origCurrentFrame
1903 endFrame = startFrame
1906 startFrame = context.startFrame()
1907 endFrame = context.endFrame()
1908 outputWriter.open(startFrame, endFrame)
1910 # Do the rendering process frame by frame
1911 print "Start Rendering of %d frames" % (endFrame-startFrame+1)
1912 for f in xrange(startFrame, endFrame+1):
1913 print "\n\nFrame: %d" % f
1915 # FIXME To get the correct camera position we have to use +1 here.
1916 # Is there a bug somewhere in the Scene module?
1917 context.currentFrame(f+1)
1918 self.cameraObj = self._SCENE.getCurrentCamera()
1920 # Use some temporary workspace, a full copy of the scene
1921 inputScene = self._SCENE.copy(2)
1923 # To get the objects at this frame remove the +1 ...
1924 ctx = inputScene.getRenderingContext()
1928 # Get a projector for this camera.
1929 # NOTE: the projector wants object in world coordinates,
1930 # so we should remember to apply modelview transformations
1931 # _before_ we do projection transformations.
1932 self.proj = Projector(self.cameraObj, self.canvasRatio)
1935 renderedScene = self.doRenderScene(inputScene)
1937 print "There was an error! Aborting."
1939 print traceback.print_exc()
1941 self._SCENE.makeCurrent()
1942 Scene.unlink(inputScene)
1946 outputWriter.printCanvas(renderedScene,
1947 doPrintPolygons = config.polygons['SHOW'],
1948 doPrintEdges = config.edges['SHOW'],
1949 showHiddenEdges = config.edges['SHOW_HIDDEN'])
1951 # delete the rendered scene
1952 self._SCENE.makeCurrent()
1953 Scene.unlink(renderedScene)
1956 outputWriter.close()
1958 context.currentFrame(origCurrentFrame)
1961 def doRenderScene(self, workScene):
1962 """Control the rendering process.
1964 Here we control the entire rendering process invoking the operation
1965 needed to transform and project the 3D scene in two dimensions.
1968 # global processing of the scene
1970 self._doSceneClipping(workScene)
1972 self._doConvertGeometricObjsToMesh(workScene)
1974 if config.output['JOIN_OBJECTS']:
1975 self._joinMeshObjectsInScene(workScene)
1977 self._doSceneDepthSorting(workScene)
1979 # Per object activities
1981 Objects = workScene.getChildren()
1982 print "Total Objects: %d" % len(Objects)
1983 for i,obj in enumerate(Objects):
1985 print "Rendering Object: %d" % i
1987 if obj.getType() != 'Mesh':
1988 print "Only Mesh supported! - Skipping type:", obj.getType()
1991 print "Rendering: ", obj.getName()
1993 mesh = obj.getData(mesh=1)
1995 self._doModelingTransformation(mesh, obj.matrix)
1997 self._doBackFaceCulling(mesh)
2000 # When doing HSR with NEWELL we may want to flip all normals
2002 if config.polygons['HSR'] == "NEWELL":
2003 for f in mesh.faces:
2006 for f in mesh.faces:
2009 self._doLighting(mesh)
2011 # Do "projection" now so we perform further processing
2012 # in Normalized View Coordinates
2013 self._doProjection(mesh, self.proj)
2015 self._doViewFrustumClipping(mesh)
2017 self._doHiddenSurfaceRemoval(mesh)
2019 self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
2021 # Update the object data, important! :)
2033 def _getObjPosition(self, obj):
2034 """Return the obj position in World coordinates.
2036 return obj.matrix.translationPart()
2038 def _cameraViewVector(self):
2039 """Get the View Direction form the camera matrix.
2041 return Vector(self.cameraObj.matrix[2]).resize3D()
2046 def _isFaceVisible(self, face):
2047 """Determine if a face of an object is visible from the current camera.
2049 The view vector is calculated from the camera location and one of the
2050 vertices of the face (expressed in World coordinates, after applying
2051 modelview transformations).
2053 After those transformations we determine if a face is visible by
2054 computing the angle between the face normal and the view vector, this
2055 angle has to be between -90 and 90 degrees for the face to be visible.
2056 This corresponds somehow to the dot product between the two, if it
2057 results > 0 then the face is visible.
2059 There is no need to normalize those vectors since we are only interested in
2060 the sign of the cross product and not in the product value.
2062 NOTE: here we assume the face vertices are in WorldCoordinates, so
2063 please transform the object _before_ doing the test.
2066 normal = Vector(face.no)
2067 camPos = self._getObjPosition(self.cameraObj)
2070 # View Vector in orthographics projections is the view Direction of
2072 if self.cameraObj.data.getType() == 1:
2073 view_vect = self._cameraViewVector()
2075 # View vector in perspective projections can be considered as
2076 # the difference between the camera position and one point of
2077 # the face, we choose the farthest point from the camera.
2078 if self.cameraObj.data.getType() == 0:
2079 vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
2083 # if d > 0 the face is visible from the camera
2084 d = view_vect * normal
2094 def _doSceneClipping(self, scene):
2095 """Clip whole objects against the View Frustum.
2097 For now clip away only objects according to their center position.
2100 cpos = self._getObjPosition(self.cameraObj)
2101 view_vect = self._cameraViewVector()
2103 near = self.cameraObj.data.clipStart
2104 far = self.cameraObj.data.clipEnd
2106 aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
2107 fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
2108 fovy = fovy * 360.0/pi
2110 Objects = scene.getChildren()
2112 if o.getType() != 'Mesh': continue;
2114 obj_vect = Vector(cpos) - self._getObjPosition(o)
2116 d = obj_vect*view_vect
2117 theta = AngleBetweenVecs(obj_vect, view_vect)
2119 # if the object is outside the view frustum, clip it away
2120 if (d < near) or (d > far) or (theta > fovy):
2123 def _doConvertGeometricObjsToMesh(self, scene):
2124 """Convert all "geometric" objects to mesh ones.
2126 geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
2127 #geometricObjTypes = ['Mesh', 'Surf', 'Curve']
2129 Objects = scene.getChildren()
2130 objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
2133 obj = self._convertToRawMeshObj(obj)
2135 scene.unlink(old_obj)
2138 # XXX Workaround for Text and Curve which have some normals
2139 # inverted when they are converted to Mesh, REMOVE that when
2140 # blender will fix that!!
2141 if old_obj.getType() in ['Curve', 'Text']:
2142 me = obj.getData(mesh=1)
2143 for f in me.faces: f.sel = 1;
2144 for v in me.verts: v.sel = 1;
2151 def _doSceneDepthSorting(self, scene):
2152 """Sort objects in the scene.
2154 The object sorting is done accordingly to the object centers.
2157 c = self._getObjPosition(self.cameraObj)
2159 by_center_pos = (lambda o1, o2:
2160 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
2161 cmp((self._getObjPosition(o1) - Vector(c)).length,
2162 (self._getObjPosition(o2) - Vector(c)).length)
2165 # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
2166 # then ob1 goes farther than obj2, useful when obj2 has holes
2169 Objects = scene.getChildren()
2170 Objects.sort(by_center_pos)
2177 def _joinMeshObjectsInScene(self, scene):
2178 """Merge all the Mesh Objects in a scene into a single Mesh Object.
2181 oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
2183 # FIXME: Object.join() do not work if the list contains 1 object
2187 mesh = Mesh.New('BigOne')
2188 bigObj = Object.New('Mesh', 'BigOne')
2195 except RuntimeError:
2196 print "\nWarning! - Can't Join Objects\n"
2197 scene.unlink(bigObj)
2200 print "Objects Type error?"
2208 # Per object/mesh methods
2210 def _convertToRawMeshObj(self, object):
2211 """Convert geometry based object to a mesh object.
2213 me = Mesh.New('RawMesh_'+object.name)
2214 me.getFromObject(object.name)
2216 newObject = Object.New('Mesh', 'RawMesh_'+object.name)
2219 # If the object has no materials set a default material
2220 if not me.materials:
2221 me.materials = [Material.New()]
2222 #for f in me.faces: f.mat = 0
2224 newObject.setMatrix(object.getMatrix())
2228 def _doModelingTransformation(self, mesh, matrix):
2229 """Transform object coordinates to world coordinates.
2231 This step is done simply applying to the object its tranformation
2232 matrix and recalculating its normals.
2234 # XXX FIXME: blender do not transform normals in the right way when
2235 # there are negative scale values
2236 if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
2237 print "WARNING: Negative scales, expect incorrect results!"
2239 mesh.transform(matrix, True)
2241 def _doBackFaceCulling(self, mesh):
2242 """Simple Backface Culling routine.
2244 At this level we simply do a visibility test face by face and then
2245 select the vertices belonging to visible faces.
2248 # Select all vertices, so edges can be displayed even if there are no
2250 for v in mesh.verts:
2253 Mesh.Mode(Mesh.SelectModes['FACE'])
2255 for f in mesh.faces:
2257 if self._isFaceVisible(f):
2260 def _doLighting(self, mesh):
2261 """Apply an Illumination and shading model to the object.
2263 The model used is the Phong one, it may be inefficient,
2264 but I'm just learning about rendering and starting from Phong seemed
2265 the most natural way.
2268 # If the mesh has vertex colors already, use them,
2269 # otherwise turn them on and do some calculations
2270 if mesh.vertexColors:
2272 mesh.vertexColors = 1
2274 materials = mesh.materials
2276 camPos = self._getObjPosition(self.cameraObj)
2278 # We do per-face color calculation (FLAT Shading), we can easily turn
2279 # to a per-vertex calculation if we want to implement some shading
2280 # technique. For an example see:
2281 # http://www.miralab.unige.ch/papers/368.pdf
2282 for f in mesh.faces:
2288 mat = materials[f.mat]
2290 # A new default material
2292 mat = Material.New('defMat')
2294 # Check if it is a shadeless material
2295 elif mat.getMode() & Material.Modes['SHADELESS']:
2297 # Convert to a value between 0 and 255
2298 tmp_col = [ int(c * 255.0) for c in I]
2309 # do vertex color calculation
2311 TotDiffSpec = Vector([0.0, 0.0, 0.0])
2313 for l in self.lights:
2315 light_pos = self._getObjPosition(l)
2316 light = light_obj.getData()
2318 L = Vector(light_pos).normalize()
2320 V = (Vector(camPos) - Vector(f.cent)).normalize()
2322 N = Vector(f.no).normalize()
2324 if config.polygons['SHADING'] == 'TOON':
2325 NL = ShadingUtils.toonShading(N*L)
2329 # Should we use NL instead of (N*L) here?
2330 R = 2 * (N*L) * N - L
2332 Ip = light.getEnergy()
2334 # Diffuse co-efficient
2335 kd = mat.getRef() * Vector(mat.getRGBCol())
2337 kd[i] *= light.col[i]
2339 Idiff = Ip * kd * max(0, NL)
2342 # Specular component
2343 ks = mat.getSpec() * Vector(mat.getSpecCol())
2344 ns = mat.getHardness()
2345 Ispec = Ip * ks * pow(max(0, (V*R)), ns)
2347 TotDiffSpec += (Idiff+Ispec)
2351 Iamb = Vector(Blender.World.Get()[0].getAmb())
2354 # Emissive component (convert to a triplet)
2355 ki = Vector([mat.getEmit()]*3)
2357 #I = ki + Iamb + (Idiff + Ispec)
2358 I = ki + (ka * Iamb) + TotDiffSpec
2361 # Set Alpha component
2363 I.append(mat.getAlpha())
2365 # Clamp I values between 0 and 1
2366 I = [ min(c, 1) for c in I]
2367 I = [ max(0, c) for c in I]
2369 # Convert to a value between 0 and 255
2370 tmp_col = [ int(c * 255.0) for c in I]
2378 def _doProjection(self, mesh, projector):
2379 """Apply Viewing and Projection tranformations.
2382 for v in mesh.verts:
2383 p = projector.doProjection(v.co[:])
2388 #mesh.recalcNormals()
2391 # We could reeset Camera matrix, since now
2392 # we are in Normalized Viewing Coordinates,
2393 # but doung that would affect World Coordinate
2394 # processing for other objects
2396 #self.cameraObj.data.type = 1
2397 #self.cameraObj.data.scale = 2.0
2398 #m = Matrix().identity()
2399 #self.cameraObj.setMatrix(m)
2401 def _doViewFrustumClipping(self, mesh):
2402 """Clip faces against the View Frustum.
2406 def __simpleDepthSort(self, mesh):
2407 """Sort faces by the furthest vertex.
2409 This simple mesthod is known also as the painter algorithm, and it
2410 solves HSR correctly only for convex meshes.
2415 # The sorting requires circa n*log(n) steps
2417 progress.setActivity("HSR: Painter", n*log(n))
2419 by_furthest_z = (lambda f1, f2: progress.update() and
2420 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
2423 # FIXME: using NMesh to sort faces. We should avoid that!
2424 nmesh = NMesh.GetRaw(mesh.name)
2426 # remember that _higher_ z values mean further points
2427 nmesh.faces.sort(by_furthest_z)
2428 nmesh.faces.reverse()
2433 def __newellDepthSort(self, mesh):
2434 """Newell's depth sorting.
2440 # Find non planar quads and convert them to triangle
2441 #for f in mesh.faces:
2443 # if is_nonplanar_quad(f.v):
2444 # print "NON QUAD??"
2448 # Now reselect all faces
2449 for f in mesh.faces:
2451 mesh.quadToTriangle()
2453 # FIXME: using NMesh to sort faces. We should avoid that!
2454 nmesh = NMesh.GetRaw(mesh.name)
2456 # remember that _higher_ z values mean further points
2457 nmesh.faces.sort(by_furthest_z)
2458 nmesh.faces.reverse()
2460 # Begin depth sort tests
2462 # use the smooth flag to set marked faces
2463 for f in nmesh.faces:
2466 facelist = nmesh.faces[:]
2470 # The steps are _at_least_ equal to len(facelist), we do not count the
2471 # feces coming out from splitting!!
2472 progress.setActivity("HSR: Newell", len(facelist))
2473 #progress.setQuiet(True)
2476 while len(facelist):
2477 debug("\n----------------------\n")
2478 debug("len(facelits): %d\n" % len(facelist))
2481 pSign = sign(P.normal[2])
2483 # We can discard faces parallel to the view vector
2484 #if P.normal[2] == 0:
2485 # facelist.remove(P)
2491 for Q in facelist[1:]:
2493 debug("P.smooth: " + str(P.smooth) + "\n")
2494 debug("Q.smooth: " + str(Q.smooth) + "\n")
2497 qSign = sign(Q.normal[2])
2498 # TODO: check also if Q is parallel??
2500 # Test 0: We need to test only those Qs whose furthest vertex
2501 # is closer to the observer than the closest vertex of P.
2503 zP = [v.co[2] for v in P.v]
2504 zQ = [v.co[2] for v in Q.v]
2505 notZOverlap = min(zP) > max(zQ) + EPS
2509 debug("NOT Z OVERLAP!\n")
2511 # If Q is not marked then we can safely print P
2514 debug("met a marked face\n")
2518 # Test 1: X extent overlapping
2519 xP = [v.co[0] for v in P.v]
2520 xQ = [v.co[0] for v in Q.v]
2521 #notXOverlap = (max(xP) <= min(xQ)) or (max(xQ) <= min(xP))
2522 notXOverlap = (min(xQ) >= max(xP)-EPS) or (min(xP) >= max(xQ)-EPS)
2526 debug("NOT X OVERLAP!\n")
2530 # Test 2: Y extent Overlapping
2531 yP = [v.co[1] for v in P.v]
2532 yQ = [v.co[1] for v in Q.v]
2533 #notYOverlap = (max(yP) <= min(yQ)) or (max(yQ) <= min(yP))
2534 notYOverlap = (min(yQ) >= max(yP)-EPS) or (min(yP) >= max(yQ)-EPS)
2538 debug("NOT Y OVERLAP!\n")
2542 # Test 3: P vertices are all behind the plane of Q
2545 d = qSign * HSR.Distance(Vector(Pi), Q)
2548 pVerticesBehindPlaneQ = (n == len(P))
2550 if pVerticesBehindPlaneQ:
2552 debug("P BEHIND Q!\n")
2556 # Test 4: Q vertices in front of the plane of P
2559 d = pSign * HSR.Distance(Vector(Qi), P)
2562 qVerticesInFrontPlaneP = (n == len(Q))
2564 if qVerticesInFrontPlaneP:
2566 debug("Q IN FRONT OF P!\n")
2570 # Test 5: Check if projections of polygons effectively overlap,
2571 # in previous tests we checked only bounding boxes.
2573 #if not projectionsOverlap(P, Q):
2574 if not ( HSR.projectionsOverlap(P, Q) or HSR.projectionsOverlap(Q, P)):
2576 debug("Projections do not overlap!\n")
2579 # We still can't say if P obscures Q.
2581 # But if Q is marked we do a face-split trying to resolve a
2582 # difficulty (maybe a visibility cycle).
2585 debug("Possibly a cycle detected!\n")
2586 debug("Split here!!\n")
2588 facelist = HSR.facesplit(P, Q, facelist, nmesh)
2592 # The question now is: Does Q obscure P?
2595 # Test 3bis: Q vertices are all behind the plane of P
2598 d = pSign * HSR.Distance(Vector(Qi), P)
2601 qVerticesBehindPlaneP = (n == len(Q))
2603 if qVerticesBehindPlaneP:
2604 debug("\nTest 3bis\n")
2605 debug("Q BEHIND P!\n")
2608 # Test 4bis: P vertices in front of the plane of Q
2611 d = qSign * HSR.Distance(Vector(Pi), Q)
2614 pVerticesInFrontPlaneQ = (n == len(P))
2616 if pVerticesInFrontPlaneQ:
2617 debug("\nTest 4bis\n")
2618 debug("P IN FRONT OF Q!\n")
2621 # We don't even know if Q does obscure P, so they should
2622 # intersect each other, split one of them in two parts.
2623 if not qVerticesBehindPlaneP and not pVerticesInFrontPlaneQ:
2624 debug("\nSimple Intersection?\n")
2625 debug("Test 3bis or 4bis failed\n")
2626 debug("Split here!!2\n")
2628 facelist = HSR.facesplit(P, Q, facelist, nmesh)
2633 facelist.insert(0, Q)
2636 debug("Q marked!\n")
2640 if split_done == 0 and face_marked == 0:
2643 dumpfaces(maplist, "dump"+str(len(maplist)).zfill(4)+".svg")
2647 if len(facelist) == 870:
2648 dumpfaces([P, Q], "loopdebug.svg")
2651 #if facelist == None:
2653 # print [v.co for v in P]
2654 # print [v.co for v in Q]
2657 # end of while len(facelist)
2660 nmesh.faces = maplist
2661 #for f in nmesh.faces:
2667 def _doHiddenSurfaceRemoval(self, mesh):
2668 """Do HSR for the given mesh.
2670 if len(mesh.faces) == 0:
2673 if config.polygons['HSR'] == 'PAINTER':
2674 print "\nUsing the Painter algorithm for HSR."
2675 self.__simpleDepthSort(mesh)
2677 elif config.polygons['HSR'] == 'NEWELL':
2678 print "\nUsing the Newell's algorithm for HSR."
2679 self.__newellDepthSort(mesh)
2682 def _doEdgesStyle(self, mesh, edgestyleSelect):
2683 """Process Mesh Edges accroding to a given selection style.
2685 Examples of algorithms:
2688 given an edge if its adjacent faces have the same normal (that is
2689 they are complanar), than deselect it.
2692 given an edge if one its adjacent faces is frontfacing and the
2693 other is backfacing, than select it, else deselect.
2696 Mesh.Mode(Mesh.SelectModes['EDGE'])
2698 edge_cache = MeshUtils.buildEdgeFaceUsersCache(mesh)
2700 for i,edge_faces in enumerate(edge_cache):
2701 mesh.edges[i].sel = 0
2702 if edgestyleSelect(edge_faces):
2703 mesh.edges[i].sel = 1
2706 for e in mesh.edges:
2709 if edgestyleSelect(e, mesh):
2715 # ---------------------------------------------------------------------
2717 ## GUI Class and Main Program
2719 # ---------------------------------------------------------------------
2722 from Blender import BGL, Draw
2723 from Blender.BGL import *
2729 # Output Format menu
2730 output_format = config.output['FORMAT']
2731 default_value = outputWriters.keys().index(output_format)+1
2732 GUI.outFormatMenu = Draw.Create(default_value)
2733 GUI.evtOutFormatMenu = 0
2735 # Animation toggle button
2736 GUI.animToggle = Draw.Create(config.output['ANIMATION'])
2737 GUI.evtAnimToggle = 1
2739 # Join Objects toggle button
2740 GUI.joinObjsToggle = Draw.Create(config.output['JOIN_OBJECTS'])
2741 GUI.evtJoinObjsToggle = 2
2743 # Render filled polygons
2744 GUI.polygonsToggle = Draw.Create(config.polygons['SHOW'])
2746 # Shading Style menu
2747 shading_style = config.polygons['SHADING']
2748 default_value = shadingStyles.keys().index(shading_style)+1
2749 GUI.shadingStyleMenu = Draw.Create(default_value)
2750 GUI.evtShadingStyleMenu = 21
2752 GUI.evtPolygonsToggle = 3
2753 # We hide the config.polygons['EXPANSION_TRICK'], for now
2755 # Render polygon edges
2756 GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
2757 GUI.evtShowEdgesToggle = 4
2759 # Render hidden edges
2760 GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
2761 GUI.evtShowHiddenEdgesToggle = 5
2764 edge_style = config.edges['STYLE']
2765 default_value = edgeStyles.keys().index(edge_style)+1
2766 GUI.edgeStyleMenu = Draw.Create(default_value)
2767 GUI.evtEdgeStyleMenu = 6
2770 GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
2771 GUI.evtEdgeWidthSlider = 7
2774 c = config.edges['COLOR']
2775 GUI.edgeColorPicker = Draw.Create(c[0]/255.0, c[1]/255.0, c[2]/255.0)
2776 GUI.evtEdgeColorPicker = 71
2779 GUI.evtRenderButton = 8
2782 GUI.evtExitButton = 9
2786 # initialize static members
2789 glClear(GL_COLOR_BUFFER_BIT)
2790 glColor3f(0.0, 0.0, 0.0)
2791 glRasterPos2i(10, 350)
2792 Draw.Text("VRM: Vector Rendering Method script. Version %s." %
2794 glRasterPos2i(10, 335)
2795 Draw.Text("Press Q or ESC to quit.")
2797 # Build the output format menu
2798 glRasterPos2i(10, 310)
2799 Draw.Text("Select the output Format:")
2800 outMenuStruct = "Output Format %t"
2801 for t in outputWriters.keys():
2802 outMenuStruct = outMenuStruct + "|%s" % t
2803 GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
2804 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
2807 GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
2808 10, 260, 160, 18, GUI.animToggle.val,
2809 "Toggle rendering of animations")
2811 # Join Objects toggle
2812 GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
2813 10, 235, 160, 18, GUI.joinObjsToggle.val,
2814 "Join objects in the rendered file")
2817 Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
2819 Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
2822 glRasterPos2i(200, 310)
2823 Draw.Text("Rendering Style:")
2826 GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
2827 200, 285, 160, 18, GUI.polygonsToggle.val,
2828 "Render filled polygons")
2830 if GUI.polygonsToggle.val == 1:
2832 # Polygon Shading Style
2833 shadingStyleMenuStruct = "Shading Style %t"
2834 for t in shadingStyles.keys():
2835 shadingStyleMenuStruct = shadingStyleMenuStruct + "|%s" % t.lower()
2836 GUI.shadingStyleMenu = Draw.Menu(shadingStyleMenuStruct, GUI.evtShadingStyleMenu,
2837 200, 260, 160, 18, GUI.shadingStyleMenu.val,
2838 "Choose the shading style")
2842 GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
2843 200, 235, 160, 18, GUI.showEdgesToggle.val,
2844 "Render polygon edges")
2846 if GUI.showEdgesToggle.val == 1:
2849 edgeStyleMenuStruct = "Edge Style %t"
2850 for t in edgeStyles.keys():
2851 edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t.lower()
2852 GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
2853 200, 210, 160, 18, GUI.edgeStyleMenu.val,
2854 "Choose the edge style")
2857 GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
2858 200, 185, 140, 18, GUI.edgeWidthSlider.val,
2859 0.0, 10.0, 0, "Change Edge Width")
2862 GUI.edgeColorPicker = Draw.ColorPicker(GUI.evtEdgeColorPicker,
2863 342, 185, 18, 18, GUI.edgeColorPicker.val, "Choose Edge Color")
2866 GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
2867 GUI.evtShowHiddenEdgesToggle,
2868 200, 160, 160, 18, GUI.showHiddenEdgesToggle.val,
2869 "Render hidden edges as dashed lines")
2871 glRasterPos2i(10, 160)
2872 Draw.Text("%s (c) 2006" % __author__)
2874 def event(evt, val):
2876 if evt == Draw.ESCKEY or evt == Draw.QKEY:
2883 def button_event(evt):
2885 if evt == GUI.evtExitButton:
2888 elif evt == GUI.evtOutFormatMenu:
2889 i = GUI.outFormatMenu.val - 1
2890 config.output['FORMAT']= outputWriters.keys()[i]
2891 # Set the new output file
2893 outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
2895 elif evt == GUI.evtAnimToggle:
2896 config.output['ANIMATION'] = bool(GUI.animToggle.val)
2898 elif evt == GUI.evtJoinObjsToggle:
2899 config.output['JOIN_OBJECTS'] = bool(GUI.joinObjsToggle.val)
2901 elif evt == GUI.evtPolygonsToggle:
2902 config.polygons['SHOW'] = bool(GUI.polygonsToggle.val)
2904 elif evt == GUI.evtShadingStyleMenu:
2905 i = GUI.shadingStyleMenu.val - 1
2906 config.polygons['SHADING'] = shadingStyles.keys()[i]
2908 elif evt == GUI.evtShowEdgesToggle:
2909 config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
2911 elif evt == GUI.evtShowHiddenEdgesToggle:
2912 config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
2914 elif evt == GUI.evtEdgeStyleMenu:
2915 i = GUI.edgeStyleMenu.val - 1
2916 config.edges['STYLE'] = edgeStyles.keys()[i]
2918 elif evt == GUI.evtEdgeWidthSlider:
2919 config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
2921 elif evt == GUI.evtEdgeColorPicker:
2922 config.edges['COLOR'] = [int(c*255.0) for c in GUI.edgeColorPicker.val]
2924 elif evt == GUI.evtRenderButton:
2925 label = "Save %s" % config.output['FORMAT']
2926 # Show the File Selector
2928 Blender.Window.FileSelector(vectorize, label, outputfile)
2931 print "Event: %d not handled!" % evt
2938 from pprint import pprint
2940 pprint(config.output)
2941 pprint(config.polygons)
2942 pprint(config.edges)
2944 _init = staticmethod(_init)
2945 draw = staticmethod(draw)
2946 event = staticmethod(event)
2947 button_event = staticmethod(button_event)
2948 conf_debug = staticmethod(conf_debug)
2950 # A wrapper function for the vectorizing process
2951 def vectorize(filename):
2952 """The vectorizing process is as follows:
2954 - Instanciate the writer and the renderer
2959 print "\nERROR: invalid file name!"
2962 from Blender import Window
2963 editmode = Window.EditMode()
2964 if editmode: Window.EditMode(0)
2966 actualWriter = outputWriters[config.output['FORMAT']]
2967 writer = actualWriter(filename)
2969 renderer = Renderer()
2970 renderer.doRendering(writer, config.output['ANIMATION'])
2972 if editmode: Window.EditMode(1)
2977 if __name__ == "__main__":
2982 basename = Blender.sys.basename(Blender.Get('filename'))
2984 outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
2986 if Blender.mode == 'background':
2987 progress = ConsoleProgressIndicator()
2988 vectorize(outputfile)
2990 progress = GraphicalProgressIndicator()
2991 Draw.Register(GUI.draw, GUI.event, GUI.button_event)