b1217659a0489fdbe2cb6fef62a2918739e58825
[vrm.git] / vrm.py
1 #!BPY
2 """
3 Name: 'VRM'
4 Blender: 242
5 Group: 'Render'
6 Tooltip: 'Vector Rendering Method script'
7 """
8
9 __author__ = "Antonio Ospite"
10 __url__ = ["http://projects.blender.org/projects/vrm"]
11 __version__ = "0.3.beta"
12
13 __bpydoc__ = """\
14     Render the scene and save the result in vector format.
15 """
16
17 # ---------------------------------------------------------------------
18 #    Copyright (c) 2006 Antonio Ospite
19 #
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.
24 #
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.
29 #
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
33 #
34 # ---------------------------------------------------------------------
35 #
36 # Additional credits:
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.
41 #
42 # ---------------------------------------------------------------------
43
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!!
63 #
64 # ---------------------------------------------------------------------
65 #
66 # Changelog:
67 #
68 #   vrm-0.3.py  - ...
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.
74 #       Thanks ideasman42.
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!
81 #
82 # ---------------------------------------------------------------------
83
84 import Blender
85 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera, Window
86 from Blender.Mathutils import *
87 from math import *
88 import sys, time
89
90 # Constants
91 EPS = 10e-5
92
93 # We use a global progress Indicator Object
94 progress = None
95
96
97 # Some global settings
98
99 class config:
100     polygons = dict()
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
107
108     polygons['TOON_LEVELS'] = 2
109
110     edges = dict()
111     edges['SHOW'] = False
112     edges['SHOW_HIDDEN'] = False
113     edges['STYLE'] = 'MESH' # or SILHOUETTE
114     edges['WIDTH'] = 2
115     edges['COLOR'] = [0, 0, 0]
116
117     output = dict()
118     output['FORMAT'] = 'SVG'
119     output['ANIMATION'] = False
120     output['JOIN_OBJECTS'] = True
121
122     #output['FORMAT'] = 'SWF'
123     #output['ANIMATION'] = True
124
125
126 # Utility functions
127 print_debug = False
128
129 def dumpfaces(flist, filename):
130     """Dump a single face to a file.
131     """
132     if not print_debug:
133         return
134
135     class tmpmesh:
136         pass
137
138     m = tmpmesh()
139     m.faces = flist
140
141     writerobj = SVGVectorWriter(filename)
142
143     writerobj.open()
144     writerobj._printPolygons(m)
145
146     writerobj.close()
147
148 def debug(msg):
149     if print_debug:
150         sys.stderr.write(msg)
151
152 def EQ(v1, v2):
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)
157     )
158
159 def sign(x):
160
161     if x < -EPS:
162     #if x < 0:
163         return -1
164     elif x > EPS:
165     #elif x > 0:
166         return 1
167     else:
168         return 0
169
170
171 # ---------------------------------------------------------------------
172 #
173 ## HSR Utility class
174 #
175 # ---------------------------------------------------------------------
176
177 EPS = 10e-5
178 INF = 10e5
179
180 class HSR:
181     """A utility class for HSR processing.
182     """
183
184     def is_nonplanar_quad(face):
185         """Determine if a quad is non-planar.
186
187         From: http://mathworld.wolfram.com/Coplanar.html
188
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
192         0, 
193         
194             | x_1 y_1 z_1 1 |
195             | x_2 y_2 z_2 1 |
196             | x_3 y_3 z_3 1 |
197             | x_4 y_4 z_4 1 | == 0
198
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.
202
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.
208
209         We here check only for 4-point complanarity.
210         """
211         n = len(face)
212
213         # assert(n>4)
214         if n < 3 or n > 4:
215             print "ERROR a mesh in Blender can't have more than 4 vertices or less than 3"
216             raise AssertionError
217
218         elif n == 3:
219             # three points must be complanar
220             return False
221         else: # n == 4
222             x1 = Vector(face[0].co)
223             x2 = Vector(face[1].co)
224             x3 = Vector(face[2].co)
225             x4 = Vector(face[3].co)
226
227             v = (x3-x1) * CrossVecs((x2-x1), (x4-x3))
228             if v != 0:
229                 return True
230
231         return False
232
233     is_nonplanar_quad = staticmethod(is_nonplanar_quad)
234
235     def pointInPolygon(poly, v):
236         return False
237
238     pointInPolygon = staticmethod(pointInPolygon)
239
240     def edgeIntersection(s1, s2, do_perturbate=False):
241
242         (x1, y1) = s1[0].co[0], s1[0].co[1]
243         (x2, y2) = s1[1].co[0], s1[1].co[1]
244
245         (x3, y3) = s2[0].co[0], s2[0].co[1]
246         (x4, y4) = s2[1].co[0], s2[1].co[1]
247
248         #z1 = s1[0].co[2]
249         #z2 = s1[1].co[2]
250         #z3 = s2[0].co[2]
251         #z4 = s2[1].co[2]
252
253
254         # calculate delta values (vector components)
255         dx1 = x2 - x1;
256         dx2 = x4 - x3;
257         dy1 = y2 - y1;
258         dy2 = y4 - y3;
259
260         #dz1 = z2 - z1;
261         #dz2 = z4 - z3;
262
263         C = dy2 * dx1 - dx2 * dy1 #  /* cross product */
264         if C == 0:  #/* parallel */
265             return None
266
267         dx3 = x1 - x3 # /* combined origin offset vector */
268         dy3 = y1 - y3
269
270         a1 = (dy3 * dx2 - dx3 * dy2) / C;
271         a2 = (dy3 * dx1 - dx3 * dy1) / C;
272
273         # check for degeneracies
274         #print_debug("\n")
275         #print_debug(str(a1)+"\n")
276         #print_debug(str(a2)+"\n\n")
277
278         if (a1 == 0 or a1 == 1 or a2 == 0 or a2 == 1):
279             # Intersection on boundaries, we consider the point external?
280             return None
281
282         elif (a1>0.0 and a1<1.0 and a2>0.0 and a2<1.0): #  /* lines cross */
283             x = x1 + a1*dx1
284             y = y1 + a1*dy1
285
286             #z = z1 + a1*dz1
287             z = 0
288             return (NMesh.Vert(x, y, z), a1, a2)
289
290         else:
291             # lines have intersections but not those segments
292             return None
293
294     edgeIntersection = staticmethod(edgeIntersection)
295
296     def isVertInside(self, v):
297         winding_number = 0
298         coincidence = False
299
300         # Create point at infinity
301         point_at_infinity = NMesh.Vert(-INF, v.co[1], -INF)
302
303         for i in range(len(self.v)):
304             s1 = (point_at_infinity, v)
305             s2 = (self.v[i-1], self.v[i])
306
307             if EQ(v.co, s2[0].co) or EQ(v.co, s2[1].co):
308                 coincidence = True
309
310             if HSR.edgeIntersection(s1, s2, do_perturbate=False):
311                 winding_number += 1
312
313         # Check even or odd
314         if winding_number % 2 == 0 :
315             return False
316         else:
317             if coincidence:
318                 return False
319             return True
320
321     isVertInside = staticmethod(isVertInside)
322
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
332         sec.  """
333
334         for i in range(len(f1.v)):
335
336
337             # If a point of f1 in inside f2, there is an overlap!
338             v1 = f1.v[i]
339             if HSR.isVertInside(f2, v1):
340                 return True
341
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
344
345             v0 = f1.v[i-1]
346
347             for j in range(len(f2.v)):
348                 v2 = f2.v[j-1]
349                 v3 = f2.v[j]
350
351                 e1 = v0, v1
352                 e2 = v2, v3
353
354                 intrs = HSR.edgeIntersection(e1, e2)
355                 if intrs:
356                     #print_debug(str(v0.co) + " " + str(v1.co) + " " +
357                     #        str(v2.co) + " " + str(v3.co) )
358                     #print_debug("\nIntersection\n")
359
360                     return True
361
362         return False
363
364     projectionsOverlap = staticmethod(projectionsOverlap)
365
366     def midpoint(p1, p2):
367         """Return the midpoint of two vertices.
368         """
369         m = MidpointVecs(Vector(p1), Vector(p2))
370         mv = NMesh.Vert(m[0], m[1], m[2])
371
372         return mv
373
374     midpoint = staticmethod(midpoint)
375
376     def facesplit(P, Q, facelist, nmesh):
377         """Split P or Q according to the strategy illustrated in the Newell's
378         paper.
379         """
380
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)
383                 )
384
385         # Choose if split P on Q plane or vice-versa
386
387         n = 0
388         for Pi in P:
389             d = HSR.Distance(Vector(Pi), Q)
390             if d <= EPS:
391                 n += 1
392         pIntersectQ = (n != len(P))
393
394         n = 0
395         for Qi in Q:
396             d = HSR.Distance(Vector(Qi), P)
397             if d >= -EPS:
398                 n += 1
399         qIntersectP = (n != len(Q))
400
401         newfaces = []
402
403         # 1. If parts of P lie in both half-spaces of Q
404         # then splice P in two with the plane of Q
405         if pIntersectQ:
406             #print "We split P"
407             f = P
408             plane = Q
409
410             newfaces = HSR.splitOn(plane, f)
411
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:
415             #print "We split Q"
416             f = Q
417             plane = P
418
419             newfaces = HSR.splitOn(plane, f)
420             #print "After"
421
422         # 3. Else slice P in half through the mid-point of
423         # the longest pair of opposite sides
424         if newfaces == None:
425
426             print "We ignore P..."
427             facelist.remove(P)
428             return facelist
429
430             #f = P
431
432             #if len(P)==3:
433             #    v1 = midpoint(f[0], f[1])
434             #    v2 = midpoint(f[1], f[2])
435             #if len(P)==4:
436             #    v1 = midpoint(f[0], f[1])
437             #    v2 = midpoint(f[2], f[3])
438             #vec3 = (Vector(v2)+10*Vector(f.normal))
439             #
440             #v3 = NMesh.Vert(vec3[0], vec3[1], vec3[2])
441
442             #plane = NMesh.Face([v1, v2, v3])
443             #
444             #newfaces = splitOn(plane, f)
445
446         
447         if newfaces == None:
448             print "Big FAT problem, we weren't able to split POLYGONS!"
449             raise AssertionError
450
451         #print newfaces
452         if newfaces:
453             #for v in f:
454             #    if v not in plane and v in nmesh.verts:
455             #        nmesh.verts.remove(v)
456             for nf in newfaces:
457
458                 nf.mat = f.mat
459                 nf.sel = f.sel
460                 nf.col = [f.col[0]] * len(nf.v)
461
462                 nf.smooth = 0
463
464                 for v in nf:
465                     nmesh.verts.append(v)
466                 # insert pieces in the list
467                 facelist.append(nf)
468
469             facelist.remove(f)
470
471         # and resort the faces
472         facelist.sort(by_furthest_z)
473         facelist.sort(lambda f1, f2: cmp(f1.smooth, f2.smooth))
474         facelist.reverse()
475
476         #print [ f.smooth for f in facelist ]
477
478         return facelist
479
480     facesplit = staticmethod(facesplit)
481
482     def isOnSegment(v1, v2, p, extremes_internal=False):
483         """Check if point p is in segment v1v2.
484         """
485
486         l1 = (v1-p).length
487         l2 = (v2-p).length
488
489         # Should we consider extreme points as internal ?
490         # The test:
491         # if p == v1 or p == v2:
492         if l1 < EPS or l2 < EPS:
493             return extremes_internal
494
495         l = (v1-v2).length
496
497         # if the sum of l1 and l2 is circa l, then the point is on segment,
498         if abs(l - (l1+l2)) < EPS:
499             return True
500         else:
501             return False
502
503     isOnSegment = staticmethod(isOnSegment)
504
505     def Distance(point, face):
506         """ Calculate the distance between a point and a face.
507
508         An alternative but more expensive method can be:
509
510             ip = Intersect(Vector(face[0]), Vector(face[1]), Vector(face[2]),
511                     Vector(face.no), Vector(point), 0)
512
513             d = Vector(ip - point).length
514
515         See: http://mathworld.wolfram.com/Point-PlaneDistance.html
516         """
517
518         p = Vector(point)
519         plNormal = Vector(face.no)
520         plVert0 = Vector(face.v[0])
521
522         d = (plVert0 * plNormal) - (p * plNormal)
523
524         #d = plNormal * (plVert0 - p)
525
526         #print "\nd: %.10f - sel: %d, %s\n" % (d, face.sel, str(point))
527
528         return d
529
530     Distance = staticmethod(Distance)
531
532     def makeFaces(vl):
533         #
534         # make one or two new faces based on a list of vertex-indices
535         #
536         newfaces = []
537
538         if len(vl) <= 4:
539             nf = NMesh.Face()
540
541             for v in vl:
542                 nf.v.append(v)
543
544             newfaces.append(nf)
545
546         else:
547             nf = NMesh.Face()
548
549             nf.v.append(vl[0])
550             nf.v.append(vl[1])
551             nf.v.append(vl[2])
552             nf.v.append(vl[3])
553             newfaces.append(nf)
554
555             nf = NMesh.Face()
556             nf.v.append(vl[3])
557             nf.v.append(vl[4])
558             nf.v.append(vl[0])
559             newfaces.append(nf)
560
561         return newfaces
562
563     makeFaces = staticmethod(makeFaces)
564
565     def splitOn(Q, P):
566         """Split P using the plane of Q.
567         Logic taken from the knife.py python script
568         """
569
570         # Check if P and Q are parallel
571         u = CrossVecs(Vector(Q.no),Vector(P.no))
572         ax = abs(u[0])
573         ay = abs(u[1])
574         az = abs(u[2])
575
576         if (ax+ay+az) < EPS:
577             print "PARALLEL planes!!"
578             return
579
580
581         # The final aim is to find the intersection line between P
582         # and the plane of Q, and split P along this line
583
584         nP = len(P.v)
585
586         # Calculate point-plane Distance between vertices of P and plane Q
587         d = []
588         for i in range(0, nP):
589             d.append(HSR.Distance(P.v[i], Q))
590
591         newVertList = []
592
593         posVertList = []
594         negVertList = []
595         for i in range(nP):
596             d0 = d[i-1]
597             V0 = P.v[i-1]
598
599             d1 = d[i]
600             V1 = P.v[i]
601
602             #print "d0:", d0, "d1:", d1
603
604             # if the vertex lies in the cutplane                        
605             if abs(d1) < EPS:
606                 #print "d1 On cutplane"
607                 posVertList.append(V1)
608                 negVertList.append(V1)
609             else:
610                 # if the previous vertex lies in cutplane
611                 if abs(d0) < EPS:
612                     #print "d0 on Cutplane"
613                     if d1 > 0:
614                         #print "d1 on positive Halfspace"
615                         posVertList.append(V1)
616                     else:
617                         #print "d1 on negative Halfspace"
618                         negVertList.append(V1)
619                 else:
620                     # if they are on the same side of the plane
621                     if d1*d0 > 0:
622                         #print "On the same half-space"
623                         if d1 > 0:
624                             #print "d1 on positive Halfspace"
625                             posVertList.append(V1)
626                         else:
627                             #print "d1 on negative Halfspace"
628                             negVertList.append(V1)
629
630                     # the vertices are not on the same side of the plane, so we have an intersection
631                     else:
632                         #print "Intersection"
633
634                         e = Vector(V0), Vector(V1)
635                         tri = Vector(Q[0]), Vector(Q[1]), Vector(Q[2])
636
637                         inters = Intersect(tri[0], tri[1], tri[2], e[1]-e[0], e[0], 0)
638                         if inters == None:
639                             print "Split Break"
640                             break
641
642                         #print "Intersection", inters
643
644                         nv = NMesh.Vert(inters[0], inters[1], inters[2])
645                         newVertList.append(nv)
646
647                         posVertList.append(nv)
648                         negVertList.append(nv)
649
650                         if d1 > 0:
651                             posVertList.append(V1)
652                         else:
653                             negVertList.append(V1)
654
655         
656         # uniq
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]'] ]
659
660
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??"
664         #    posVertList = []
665         #if len(negVertList) < 3:
666         #    print "Problem, we created a face with less that 3 verteices??"
667         #    negVertList = []
668
669         if len(posVertList) < 3 or len(negVertList) < 3:
670             print "RETURN NONE, SURE???"
671             return None
672
673
674         newfaces = HSR.addNewFaces(posVertList, negVertList)
675
676         return newfaces
677
678     splitOn = staticmethod(splitOn)
679
680     def addNewFaces(posVertList, negVertList):
681         # Create new faces resulting from the split
682         outfaces = []
683         if len(posVertList) or len(negVertList):
684
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]] )
688
689             for nf in newfaces:
690                 if nf and len(nf)>2:
691                     outfaces += HSR.makeFaces(nf)
692
693         return outfaces
694
695
696     addNewFaces = staticmethod(addNewFaces)
697
698
699 # ---------------------------------------------------------------------
700 #
701 ## Mesh Utility class
702 #
703 # ---------------------------------------------------------------------
704
705 class MeshUtils:
706
707     def buildEdgeFaceUsersCache(me):
708         ''' 
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
712
713         Taken from .blender/scripts/bpymodules/BPyMesh.py,
714         thanks to ideasman_42.
715         '''
716
717         def sorted_edge_indicies(ed):
718             i1= ed.v1.index
719             i2= ed.v2.index
720             if i1>i2:
721                 i1,i2= i2,i1
722             return i1, i2
723
724         
725         face_edges_dict= dict([(sorted_edge_indicies(ed), (ed.index, [])) for ed in me.edges])
726         for f in me.faces:
727             fvi= [v.index for v in f.v]# face vert idx's
728             for i in xrange(len(f)):
729                 i1= fvi[i]
730                 i2= fvi[i-1]
731                 
732                 if i1>i2:
733                     i1,i2= i2,i1
734                 
735                 face_edges_dict[i1,i2][1].append(f)
736         
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
740         
741         return face_edges
742
743     def isMeshEdge(adjacent_faces):
744         """Mesh edge rule.
745
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.
749         """
750
751         if len(adjacent_faces) == 0:
752             return True
753
754         selected_faces = [f for f in adjacent_faces if f.sel]
755
756         if len(selected_faces) != 0:
757             return True
758         else:
759             return False
760
761     def isSilhouetteEdge(adjacent_faces):
762         """Silhuette selection rule.
763
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
766         face.
767         """
768
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)
772             ):
773             return True
774         else:
775             return False
776
777     buildEdgeFaceUsersCache = staticmethod(buildEdgeFaceUsersCache)
778     isMeshEdge = staticmethod(isMeshEdge)
779     isSilhouetteEdge = staticmethod(isSilhouetteEdge)
780
781
782 # ---------------------------------------------------------------------
783 #
784 ## Shading Utility class
785 #
786 # ---------------------------------------------------------------------
787
788 class ShadingUtils:
789
790     shademap = None
791
792     def toonShadingMapSetup():
793         levels = config.polygons['TOON_LEVELS']
794
795         texels = 2*levels - 1
796         tmp_shademap = [0.0] + [(i)/float(texels-1) for i in xrange(1, texels-1) ] + [1.0]
797
798         return tmp_shademap
799
800     def toonShading(u):
801
802         shademap = ShadingUtils.shademap
803
804         if not shademap:
805             shademap = ShadingUtils.toonShadingMapSetup()
806
807         v = 1.0
808         for i in xrange(0, len(shademap)-1):
809             pivot = (shademap[i]+shademap[i+1])/2.0
810             j = int(u>pivot)
811
812             v = shademap[i+j]
813
814             if v < shademap[i+1]:
815                 return v
816
817         return v
818
819     toonShadingMapSetup = staticmethod(toonShadingMapSetup)
820     toonShading = staticmethod(toonShading)
821
822
823 # ---------------------------------------------------------------------
824 #
825 ## Projections classes
826 #
827 # ---------------------------------------------------------------------
828
829 class Projector:
830     """Calculate the projection of an object given the camera.
831     
832     A projector is useful to so some per-object transformation to obtain the
833     projection of an object given the camera.
834     
835     The main method is #doProjection# see the method description for the
836     parameter list.
837     """
838
839     def __init__(self, cameraObj, canvasRatio):
840         """Calculate the projection matrix.
841
842         The projection matrix depends, in this case, on the camera settings.
843         TAKE CARE: This projector expects vertices in World Coordinates!
844         """
845
846         camera = cameraObj.getData()
847
848         aspect = float(canvasRatio[0])/float(canvasRatio[1])
849         near = camera.clipStart
850         far = camera.clipEnd
851
852         scale = float(camera.scale)
853
854         fovy = atan(0.5/aspect/(camera.lens/32))
855         fovy = fovy * 360.0/pi
856         
857         # What projection do we want?
858         if camera.type == 0:
859             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
860         elif camera.type == 1:
861             mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) 
862         
863         # View transformation
864         cam = Matrix(cameraObj.getInverseMatrix())
865         cam.transpose() 
866         
867         mP = mP * cam
868
869         self.projectionMatrix = mP
870
871     ##
872     # Public methods
873     #
874
875     def doProjection(self, v):
876         """Project the point on the view plane.
877
878         Given a vertex calculate the projection using the current projection
879         matrix.
880         """
881         
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()
887         
888         # Perspective division
889         if p[3] != 0:
890             p[0] = p[0]/p[3]
891             p[1] = p[1]/p[3]
892             p[2] = p[2]/p[3]
893
894         # restore the size
895         p[3] = 1.0
896         p.resize3D()
897
898         return p
899
900
901     ##
902     # Private methods
903     #
904     
905     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
906         """Return a perspective projection matrix.
907         """
908         
909         top = near * tan(fovy * pi / 360.0)
910         bottom = -top
911         left = bottom*aspect
912         right= top*aspect
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))
919         
920         m = Matrix(
921                 [x,   0.0,    a,    0.0],
922                 [0.0,   y,    b,    0.0],
923                 [0.0, 0.0,    c,      d],
924                 [0.0, 0.0, -1.0,    0.0])
925
926         return m
927
928     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
929         """Return an orthogonal projection matrix.
930         """
931         
932         # The 11 in the formula was found emiprically
933         top = near * tan(fovy * pi / 360.0) * (scale * 11)
934         bottom = -top 
935         left = bottom * aspect
936         right= top * aspect
937         rl = right-left
938         tb = top-bottom
939         fn = near-far 
940         tx = -((right+left)/rl)
941         ty = -((top+bottom)/tb)
942         tz = ((far+near)/fn)
943
944         m = Matrix(
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])
949         
950         return m
951
952
953 # ---------------------------------------------------------------------
954 #
955 ## Progress Indicator
956 #
957 # ---------------------------------------------------------------------
958
959 class Progress:
960     """A model for a progress indicator.
961     
962     Do the progress calculation calculation and
963     the view independent stuff of a progress indicator.
964     """
965     def __init__(self, steps=0):
966         self.name = ""
967         self.steps = steps
968         self.completed = 0
969         self.progress = 0
970
971     def setSteps(self, steps):
972         """Set the number of steps of the activity wich we want to track.
973         """
974         self.steps = steps
975
976     def getSteps(self):
977         return self.steps
978
979     def setName(self, name):
980         """Set the name of the activity wich we want to track.
981         """
982         self.name = name
983
984     def getName(self):
985         return self.name
986
987     def getProgress(self):
988         return self.progress
989
990     def reset(self):
991         self.completed = 0
992         self.progress = 0
993
994     def update(self):
995         """Update the model, call this method when one step is completed.
996         """
997         if self.progress == 100:
998             return False
999
1000         self.completed += 1
1001         self.progress = ( float(self.completed) / float(self.steps) ) * 100
1002         self.progress = int(self.progress)
1003
1004         return True
1005
1006
1007 class ProgressIndicator:
1008     """An abstraction of a View for the Progress Model
1009     """
1010     def __init__(self):
1011
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
1016
1017         self.quiet = False
1018
1019         self.progressModel = None
1020
1021     def setQuiet(self, value):
1022         self.quiet = value
1023
1024     def setActivity(self, name, steps):
1025         """Initialize the Model.
1026
1027         In a future version (with subactivities-progress support) this method
1028         could only set the current activity.
1029         """
1030         self.progressModel = Progress()
1031         self.progressModel.setName(name)
1032         self.progressModel.setSteps(steps)
1033
1034     def getActivity(self):
1035         return self.progressModel
1036
1037     def update(self):
1038         """Update the model and show the actual progress.
1039         """
1040         assert(self.progressModel)
1041
1042         if self.progressModel.update():
1043             if self.quiet:
1044                 return
1045
1046             self.show(self.progressModel.getProgress(),
1047                     self.progressModel.getName())
1048
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)
1051         return True
1052
1053     def show(self, progress, name=""):
1054         self.shows_counter = (self.shows_counter + 1) % self.refresh_rate
1055         if self.shows_counter != 0:
1056             return
1057
1058         if progress == 100:
1059             self.shows_counter = -1
1060
1061
1062 class ConsoleProgressIndicator(ProgressIndicator):
1063     """Show a progress bar on stderr, a la wget.
1064     """
1065     def __init__(self):
1066         ProgressIndicator.__init__(self)
1067
1068         self.swirl_chars = ["-", "\\", "|", "/"]
1069         self.swirl_count = -1
1070
1071     def show(self, progress, name):
1072         ProgressIndicator.show(self, progress, name)
1073         
1074         bar_length = 70
1075         bar_progress = int( (progress/100.0) * bar_length )
1076         bar = ("=" * bar_progress).ljust(bar_length)
1077
1078         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1079         swirl_char = self.swirl_chars[self.swirl_count]
1080
1081         progress_bar = "%s |%s| %c %3d%%" % (name, bar, swirl_char, progress)
1082
1083         sys.stderr.write(progress_bar+"\r")
1084         if progress == 100:
1085             sys.stderr.write("\n")
1086
1087
1088 class GraphicalProgressIndicator(ProgressIndicator):
1089     """Interface to the Blender.Window.DrawProgressBar() method.
1090     """
1091     def __init__(self):
1092         ProgressIndicator.__init__(self)
1093
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
1100
1101     def show(self, progress, name):
1102         ProgressIndicator.show(self, progress)
1103
1104         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1105         swirl_char = self.swirl_chars[self.swirl_count]
1106
1107         progress_text = "%s - %c %3d%%" % (name, swirl_char, progress)
1108
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)
1112
1113         if progress == 100:
1114             Window.DrawProgressBar(1, progress_text)
1115             Window.WaitCursor(0)
1116
1117
1118
1119 # ---------------------------------------------------------------------
1120 #
1121 ## 2D Object representation class
1122 #
1123 # ---------------------------------------------------------------------
1124
1125 # TODO: a class to represent the needed properties of a 2D vector image
1126 # For now just using a [N]Mesh structure.
1127
1128
1129 # ---------------------------------------------------------------------
1130 #
1131 ## Vector Drawing Classes
1132 #
1133 # ---------------------------------------------------------------------
1134
1135 ## A generic Writer
1136
1137 class VectorWriter:
1138     """
1139     A class for printing output in a vectorial format.
1140
1141     Given a 2D representation of the 3D scene the class is responsible to
1142     write it is a vector format.
1143
1144     Every subclasses of VectorWriter must have at last the following public
1145     methods:
1146         - open(self)
1147         - close(self)
1148         - printCanvas(self, scene,
1149             doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
1150     """
1151     
1152     def __init__(self, fileName):
1153         """Set the output file name and other properties"""
1154
1155         self.outputFileName = fileName
1156         self.file = None
1157         
1158         context = Scene.GetCurrent().getRenderingContext()
1159         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
1160
1161         self.startFrame = 1
1162         self.endFrame = 1
1163         self.animation = False
1164
1165
1166     ##
1167     # Public Methods
1168     #
1169     
1170     def open(self, startFrame=1, endFrame=1):
1171         if startFrame != endFrame:
1172             self.startFrame = startFrame
1173             self.endFrame = endFrame
1174             self.animation = True
1175
1176         self.file = open(self.outputFileName, "w")
1177         print "Outputting to: ", self.outputFileName
1178
1179         return
1180
1181     def close(self):
1182         if self.file:
1183             self.file.close()
1184         return
1185
1186     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1187             showHiddenEdges=False):
1188         """This is the interface for the needed printing routine.
1189         """
1190         return
1191         
1192
1193 ## SVG Writer
1194
1195 class SVGVectorWriter(VectorWriter):
1196     """A concrete class for writing SVG output.
1197     """
1198
1199     def __init__(self, fileName):
1200         """Simply call the parent Contructor.
1201         """
1202         VectorWriter.__init__(self, fileName)
1203
1204
1205     ##
1206     # Public Methods
1207     #
1208
1209     def open(self, startFrame=1, endFrame=1):
1210         """Do some initialization operations.
1211         """
1212         VectorWriter.open(self, startFrame, endFrame)
1213         self._printHeader()
1214
1215     def close(self):
1216         """Do some finalization operation.
1217         """
1218         self._printFooter()
1219
1220         # remember to call the close method of the parent
1221         VectorWriter.close(self)
1222
1223         
1224     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1225             showHiddenEdges=False):
1226         """Convert the scene representation to SVG.
1227         """
1228
1229         Objects = scene.getChildren()
1230
1231         context = scene.getRenderingContext()
1232         framenumber = context.currentFrame()
1233
1234         if self.animation:
1235             framestyle = "display:none"
1236         else:
1237             framestyle = "display:block"
1238         
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) )
1242
1243
1244         for obj in Objects:
1245
1246             if(obj.getType() != 'Mesh'):
1247                 continue
1248
1249             self.file.write("<g id=\"%s\">\n" % obj.getName())
1250
1251             mesh = obj.getData(mesh=1)
1252
1253             if doPrintPolygons:
1254                 self._printPolygons(mesh)
1255
1256             if doPrintEdges:
1257                 self._printEdges(mesh, showHiddenEdges)
1258             
1259             self.file.write("</g>\n")
1260
1261         self.file.write("</g>\n")
1262
1263     
1264     ##  
1265     # Private Methods
1266     #
1267     
1268     def _calcCanvasCoord(self, v):
1269         """Convert vertex in scene coordinates to canvas coordinates.
1270         """
1271
1272         pt = Vector([0, 0, 0])
1273         
1274         mW = float(self.canvasSize[0])/2.0
1275         mH = float(self.canvasSize[1])/2.0
1276
1277         # rescale to canvas size
1278         pt[0] = v.co[0]*mW + mW
1279         pt[1] = v.co[1]*mH + mH
1280         pt[2] = v.co[2]
1281          
1282         # For now we want (0,0) in the top-left corner of the canvas.
1283         # Mirror and translate along y
1284         pt[1] *= -1
1285         pt[1] += self.canvasSize[1]
1286         
1287         return pt
1288
1289     def _printHeader(self):
1290         """Print SVG header."""
1291
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" %
1298                 self.canvasSize)
1299
1300         if self.animation:
1301
1302             self.file.write("""\n<script type="text/javascript"><![CDATA[
1303             globalStartFrame=%d;
1304             globalEndFrame=%d;
1305
1306             /* FIXME: Use 1000 as interval as lower values gives problems */
1307             timerID = setInterval("NextFrame()", 1000);
1308             globalFrameCounter=%d;
1309
1310             function NextFrame()
1311             {
1312               currentElement  = document.getElementById('frame'+globalFrameCounter)
1313               previousElement = document.getElementById('frame'+(globalFrameCounter-1))
1314
1315               if (!currentElement)
1316               {
1317                 return;
1318               }
1319
1320               if (globalFrameCounter > globalEndFrame)
1321               {
1322                 clearInterval(timerID)
1323               }
1324               else
1325               {
1326                 if(previousElement)
1327                 {
1328                     previousElement.style.display="none";
1329                 }
1330                 currentElement.style.display="block";
1331                 globalFrameCounter++;
1332               }
1333             }
1334             \n]]></script>\n
1335             \n""" % (self.startFrame, self.endFrame, self.startFrame) )
1336                 
1337     def _printFooter(self):
1338         """Print the SVG footer."""
1339
1340         self.file.write("\n</svg>\n")
1341
1342     def _printPolygons(self, mesh): 
1343         """Print the selected (visible) polygons.
1344         """
1345
1346         if len(mesh.faces) == 0:
1347             return
1348
1349         self.file.write("<g>\n")
1350
1351         for face in mesh.faces:
1352             if not face.sel:
1353                continue
1354
1355             self.file.write("<path d=\"")
1356
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]))
1360
1361             for v in face.v[1:]:
1362                 p = self._calcCanvasCoord(v)
1363                 self.file.write("%g,%g " % (p[0], p[1]))
1364             
1365             # get rid of the last blank space, just cosmetics here.
1366             self.file.seek(-1, 1) 
1367             self.file.write(" z\"\n")
1368             
1369             # take as face color the first vertex color
1370             if face.col:
1371                 fcol = face.col[0]
1372                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1373             else:
1374                 color = [255, 255, 255, 255]
1375
1376             # Convert the color to the #RRGGBB form
1377             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
1378
1379             # Handle transparent polygons
1380             opacity_string = ""
1381             if color[3] != 255:
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)
1385
1386             self.file.write("\tstyle=\"fill:" + str_col + ";")
1387             self.file.write(opacity_string)
1388
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
1392             stroke_width = 1.0
1393
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")
1400
1401             self.file.write("\"/>\n")
1402
1403         self.file.write("</g>\n")
1404
1405     def _printEdges(self, mesh, showHiddenEdges=False):
1406         """Print the wireframe using mesh edges.
1407         """
1408
1409         stroke_width = config.edges['WIDTH']
1410         stroke_col = config.edges['COLOR']
1411         
1412         self.file.write("<g>\n")
1413
1414         for e in mesh.edges:
1415             
1416             hidden_stroke_style = ""
1417             
1418             if e.sel == 0:
1419                 if showHiddenEdges == False:
1420                     continue
1421                 else:
1422                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
1423
1424             p1 = self._calcCanvasCoord(e.v1)
1425             p2 = self._calcCanvasCoord(e.v2)
1426             
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")
1434
1435         self.file.write("</g>\n")
1436
1437
1438 ## SWF Writer
1439
1440 try:
1441     from ming import *
1442     SWFSupported = True
1443 except:
1444     SWFSupported = False
1445
1446 class SWFVectorWriter(VectorWriter):
1447     """A concrete class for writing SWF output.
1448     """
1449
1450     def __init__(self, fileName):
1451         """Simply call the parent Contructor.
1452         """
1453         VectorWriter.__init__(self, fileName)
1454
1455         self.movie = None
1456         self.sprite = None
1457
1458
1459     ##
1460     # Public Methods
1461     #
1462
1463     def open(self, startFrame=1, endFrame=1):
1464         """Do some initialization operations.
1465         """
1466         VectorWriter.open(self, startFrame, endFrame)
1467         self.movie = SWFMovie()
1468         self.movie.setDimension(self.canvasSize[0], self.canvasSize[1])
1469         # set fps
1470         self.movie.setRate(25)
1471         numframes = endFrame - startFrame + 1
1472         self.movie.setFrames(numframes)
1473
1474     def close(self):
1475         """Do some finalization operation.
1476         """
1477         self.movie.save(self.outputFileName)
1478
1479         # remember to call the close method of the parent
1480         VectorWriter.close(self)
1481
1482     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1483             showHiddenEdges=False):
1484         """Convert the scene representation to SVG.
1485         """
1486         context = scene.getRenderingContext()
1487         framenumber = context.currentFrame()
1488
1489         Objects = scene.getChildren()
1490
1491         if self.sprite:
1492             self.movie.remove(self.sprite)
1493
1494         sprite = SWFSprite()
1495
1496         for obj in Objects:
1497
1498             if(obj.getType() != 'Mesh'):
1499                 continue
1500
1501             mesh = obj.getData(mesh=1)
1502
1503             if doPrintPolygons:
1504                 self._printPolygons(mesh, sprite)
1505
1506             if doPrintEdges:
1507                 self._printEdges(mesh, sprite, showHiddenEdges)
1508             
1509         sprite.nextFrame()
1510         i = self.movie.add(sprite)
1511         # Remove the instance the next time
1512         self.sprite = i
1513         if self.animation:
1514             self.movie.nextFrame()
1515
1516     
1517     ##  
1518     # Private Methods
1519     #
1520     
1521     def _calcCanvasCoord(self, v):
1522         """Convert vertex in scene coordinates to canvas coordinates.
1523         """
1524
1525         pt = Vector([0, 0, 0])
1526         
1527         mW = float(self.canvasSize[0])/2.0
1528         mH = float(self.canvasSize[1])/2.0
1529
1530         # rescale to canvas size
1531         pt[0] = v.co[0]*mW + mW
1532         pt[1] = v.co[1]*mH + mH
1533         pt[2] = v.co[2]
1534          
1535         # For now we want (0,0) in the top-left corner of the canvas.
1536         # Mirror and translate along y
1537         pt[1] *= -1
1538         pt[1] += self.canvasSize[1]
1539         
1540         return pt
1541                 
1542     def _printPolygons(self, mesh, sprite): 
1543         """Print the selected (visible) polygons.
1544         """
1545
1546         if len(mesh.faces) == 0:
1547             return
1548
1549         for face in mesh.faces:
1550             if not face.sel:
1551                continue
1552
1553             if face.col:
1554                 fcol = face.col[0]
1555                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1556             else:
1557                 color = [255, 255, 255, 255]
1558
1559             s = SWFShape()
1560             f = s.addFill(color[0], color[1], color[2], color[3])
1561             s.setRightFill(f)
1562
1563             # The starting point of the shape
1564             p0 = self._calcCanvasCoord(face.verts[0])
1565             s.movePenTo(p0[0], p0[1])
1566
1567
1568             for v in face.verts[1:]:
1569                 p = self._calcCanvasCoord(v)
1570                 s.drawLineTo(p[0], p[1])
1571             
1572             # Closing the shape
1573             s.drawLineTo(p0[0], p0[1])
1574             s.end()
1575             sprite.add(s)
1576
1577
1578             """
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
1582             stroke_width = 1.0
1583
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")
1590
1591             """
1592
1593     def _printEdges(self, mesh, sprite, showHiddenEdges=False):
1594         """Print the wireframe using mesh edges.
1595         """
1596
1597         stroke_width = config.edges['WIDTH']
1598         stroke_col = config.edges['COLOR']
1599
1600         s = SWFShape()
1601
1602         for e in mesh.edges:
1603
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],
1606             255)
1607             
1608             if e.sel == 0:
1609                 if showHiddenEdges == False:
1610                     continue
1611                 else:
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],
1615                             stroke_col[2], 128)
1616
1617             p1 = self._calcCanvasCoord(e.v1)
1618             p2 = self._calcCanvasCoord(e.v2)
1619
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])
1625             
1626
1627         s.end()
1628         sprite.add(s)
1629             
1630
1631
1632 # ---------------------------------------------------------------------
1633 #
1634 ## Rendering Classes
1635 #
1636 # ---------------------------------------------------------------------
1637
1638 # A dictionary to collect different shading style methods
1639 shadingStyles = dict()
1640 shadingStyles['FLAT'] = None
1641 shadingStyles['TOON'] = None
1642
1643 # A dictionary to collect different edge style methods
1644 edgeStyles = dict()
1645 edgeStyles['MESH'] = MeshUtils.isMeshEdge
1646 edgeStyles['SILHOUETTE'] = MeshUtils.isSilhouetteEdge
1647
1648 # A dictionary to collect the supported output formats
1649 outputWriters = dict()
1650 outputWriters['SVG'] = SVGVectorWriter
1651 if SWFSupported:
1652     outputWriters['SWF'] = SWFVectorWriter
1653
1654
1655 class Renderer:
1656     """Render a scene viewed from the active camera.
1657     
1658     This class is responsible of the rendering process, transformation and
1659     projection of the objects in the scene are invoked by the renderer.
1660
1661     The rendering is done using the active camera for the current scene.
1662     """
1663
1664     def __init__(self):
1665         """Make the rendering process only for the current scene by default.
1666
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.
1669         """
1670
1671         # Render the current Scene, this should be a READ-ONLY property
1672         self._SCENE = Scene.GetCurrent()
1673         
1674         # Use the aspect ratio of the scene rendering context
1675         context = self._SCENE.getRenderingContext()
1676
1677         aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
1678         self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
1679                             float(context.aspectRatioY())
1680                             )
1681
1682         # Render from the currently active camera 
1683         self.cameraObj = self._SCENE.getCurrentCamera()
1684
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']
1688
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
1695             lobj.link(l) 
1696             self.lights.append(lobj)
1697
1698
1699     ##
1700     # Public Methods
1701     #
1702
1703     def doRendering(self, outputWriter, animation=False):
1704         """Render picture or animation and write it out.
1705         
1706         The parameters are:
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
1709               current frame.
1710         """
1711         
1712         context = self._SCENE.getRenderingContext()
1713         origCurrentFrame = context.currentFrame()
1714
1715         # Handle the animation case
1716         if not animation:
1717             startFrame = origCurrentFrame
1718             endFrame = startFrame
1719             outputWriter.open()
1720         else:
1721             startFrame = context.startFrame()
1722             endFrame = context.endFrame()
1723             outputWriter.open(startFrame, endFrame)
1724         
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)
1730
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()
1735
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)
1741
1742             try:
1743                 renderedScene = self.doRenderScene(inputScene)
1744             except :
1745                 print "There was an error! Aborting."
1746                 import traceback
1747                 print traceback.print_exc()
1748
1749                 self._SCENE.makeCurrent()
1750                 Scene.unlink(inputScene)
1751                 del inputScene
1752                 return
1753
1754             outputWriter.printCanvas(renderedScene,
1755                     doPrintPolygons = config.polygons['SHOW'],
1756                     doPrintEdges    = config.edges['SHOW'],
1757                     showHiddenEdges = config.edges['SHOW_HIDDEN'])
1758             
1759             # delete the rendered scene
1760             self._SCENE.makeCurrent()
1761             Scene.unlink(renderedScene)
1762             del renderedScene
1763
1764         outputWriter.close()
1765         print "Done!"
1766         context.currentFrame(origCurrentFrame)
1767
1768
1769     def doRenderScene(self, workScene):
1770         """Control the rendering process.
1771         
1772         Here we control the entire rendering process invoking the operation
1773         needed to transform and project the 3D scene in two dimensions.
1774         """
1775         
1776         # global processing of the scene
1777
1778         self._doSceneClipping(workScene)
1779
1780         self._doConvertGeometricObjsToMesh(workScene)
1781
1782         if config.output['JOIN_OBJECTS']:
1783             self._joinMeshObjectsInScene(workScene)
1784
1785         self._doSceneDepthSorting(workScene)
1786         
1787         # Per object activities
1788
1789         Objects = workScene.getChildren()
1790         print "Total Objects: %d" % len(Objects)
1791         for i,obj in enumerate(Objects):
1792             print "\n\n-------"
1793             print "Rendering Object: %d" % i
1794
1795             if obj.getType() != 'Mesh':
1796                 print "Only Mesh supported! - Skipping type:", obj.getType()
1797                 continue
1798
1799             print "Rendering: ", obj.getName()
1800
1801             mesh = obj.getData(mesh=1)
1802
1803             self._doModelingTransformation(mesh, obj.matrix)
1804
1805             self._doBackFaceCulling(mesh)
1806
1807
1808             # When doing HSR with NEWELL we may want to flip all normals
1809             # toward the viewer
1810             if config.polygons['HSR'] == "NEWELL":
1811                 for f in mesh.faces:
1812                     f.sel = 1-f.sel
1813                 mesh.flipNormals()
1814                 for f in mesh.faces:
1815                     f.sel = 1
1816
1817             self._doLighting(mesh)
1818
1819             # Do "projection" now so we perform further processing
1820             # in Normalized View Coordinates
1821             self._doProjection(mesh, self.proj)
1822
1823             self._doViewFrustumClipping(mesh)
1824
1825             self._doHiddenSurfaceRemoval(mesh)
1826
1827             self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
1828
1829             # Update the object data, important! :)
1830             mesh.update()
1831
1832         return workScene
1833
1834
1835     ##
1836     # Private Methods
1837     #
1838
1839     # Utility methods
1840
1841     def _getObjPosition(self, obj):
1842         """Return the obj position in World coordinates.
1843         """
1844         return obj.matrix.translationPart()
1845
1846     def _cameraViewVector(self):
1847         """Get the View Direction form the camera matrix.
1848         """
1849         return Vector(self.cameraObj.matrix[2]).resize3D()
1850
1851
1852     # Faces methods
1853
1854     def _isFaceVisible(self, face):
1855         """Determine if a face of an object is visible from the current camera.
1856         
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).
1860
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.
1866
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.
1869
1870         NOTE: here we assume the face vertices are in WorldCoordinates, so
1871         please transform the object _before_ doing the test.
1872         """
1873
1874         normal = Vector(face.no)
1875         camPos = self._getObjPosition(self.cameraObj)
1876         view_vect = None
1877
1878         # View Vector in orthographics projections is the view Direction of
1879         # the camera
1880         if self.cameraObj.data.getType() == 1:
1881             view_vect = self._cameraViewVector()
1882
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] )
1888             view_vect = vv[1]
1889
1890
1891         # if d > 0 the face is visible from the camera
1892         d = view_vect * normal
1893         
1894         if d > 0:
1895             return True
1896         else:
1897             return False
1898
1899
1900     # Scene methods
1901
1902     def _doSceneClipping(self, scene):
1903         """Clip whole objects against the View Frustum.
1904
1905         For now clip away only objects according to their center position.
1906         """
1907
1908         cpos = self._getObjPosition(self.cameraObj)
1909         view_vect = self._cameraViewVector()
1910
1911         near = self.cameraObj.data.clipStart
1912         far  = self.cameraObj.data.clipEnd
1913
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
1917
1918         Objects = scene.getChildren()
1919         for o in Objects:
1920             if o.getType() != 'Mesh': continue;
1921
1922             obj_vect = Vector(cpos) - self._getObjPosition(o)
1923
1924             d = obj_vect*view_vect
1925             theta = AngleBetweenVecs(obj_vect, view_vect)
1926             
1927             # if the object is outside the view frustum, clip it away
1928             if (d < near) or (d > far) or (theta > fovy):
1929                 scene.unlink(o)
1930
1931     def _doConvertGeometricObjsToMesh(self, scene):
1932         """Convert all "geometric" objects to mesh ones.
1933         """
1934         geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
1935         #geometricObjTypes = ['Mesh', 'Surf', 'Curve']
1936
1937         Objects = scene.getChildren()
1938         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
1939         for obj in objList:
1940             old_obj = obj
1941             obj = self._convertToRawMeshObj(obj)
1942             scene.link(obj)
1943             scene.unlink(old_obj)
1944
1945
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;
1953                 me.remDoubles(0)
1954                 me.triangleToQuad()
1955                 me.recalcNormals()
1956                 me.update()
1957
1958
1959     def _doSceneDepthSorting(self, scene):
1960         """Sort objects in the scene.
1961
1962         The object sorting is done accordingly to the object centers.
1963         """
1964
1965         c = self._getObjPosition(self.cameraObj)
1966
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)
1971             )
1972
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
1975         by_bbox = None
1976         
1977         Objects = scene.getChildren()
1978         Objects.sort(by_center_pos)
1979         
1980         # update the scene
1981         for o in Objects:
1982             scene.unlink(o)
1983             scene.link(o)
1984
1985     def _joinMeshObjectsInScene(self, scene):
1986         """Merge all the Mesh Objects in a scene into a single Mesh Object.
1987         """
1988
1989         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
1990
1991         # FIXME: Object.join() do not work if the list contains 1 object
1992         if len(oList) == 1:
1993             return
1994
1995         mesh = Mesh.New('BigOne')
1996         bigObj = Object.New('Mesh', 'BigOne')
1997         bigObj.link(mesh)
1998
1999         scene.link(bigObj)
2000
2001         try:
2002             bigObj.join(oList)
2003         except RuntimeError:
2004             print "\nWarning! - Can't Join Objects\n"
2005             scene.unlink(bigObj)
2006             return
2007         except TypeError:
2008             print "Objects Type error?"
2009         
2010         for o in oList:
2011             scene.unlink(o)
2012
2013         scene.update()
2014
2015  
2016     # Per object/mesh methods
2017
2018     def _convertToRawMeshObj(self, object):
2019         """Convert geometry based object to a mesh object.
2020         """
2021         me = Mesh.New('RawMesh_'+object.name)
2022         me.getFromObject(object.name)
2023
2024         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
2025         newObject.link(me)
2026
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
2031
2032         newObject.setMatrix(object.getMatrix())
2033
2034         return newObject
2035
2036     def _doModelingTransformation(self, mesh, matrix):
2037         """Transform object coordinates to world coordinates.
2038
2039         This step is done simply applying to the object its tranformation
2040         matrix and recalculating its normals.
2041         """
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!"
2046
2047         mesh.transform(matrix, True)
2048
2049     def _doBackFaceCulling(self, mesh):
2050         """Simple Backface Culling routine.
2051         
2052         At this level we simply do a visibility test face by face and then
2053         select the vertices belonging to visible faces.
2054         """
2055         
2056         # Select all vertices, so edges can be displayed even if there are no
2057         # faces
2058         for v in mesh.verts:
2059             v.sel = 1
2060         
2061         Mesh.Mode(Mesh.SelectModes['FACE'])
2062         # Loop on faces
2063         for f in mesh.faces:
2064             f.sel = 0
2065             if self._isFaceVisible(f):
2066                 f.sel = 1
2067
2068     def _doLighting(self, mesh):
2069         """Apply an Illumination and shading model to the object.
2070
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.
2074         """
2075
2076         # If the mesh has vertex colors already, use them,
2077         # otherwise turn them on and do some calculations
2078         if mesh.vertexColors:
2079             return
2080         mesh.vertexColors = 1
2081
2082         materials = mesh.materials
2083
2084         camPos = self._getObjPosition(self.cameraObj)
2085
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:
2091             if not f.sel:
2092                 continue
2093
2094             mat = None
2095             if materials:
2096                 mat = materials[f.mat]
2097
2098             # A new default material
2099             if mat == None:
2100                 mat = Material.New('defMat')
2101
2102             # Check if it is a shadeless material
2103             elif mat.getMode() & Material.Modes['SHADELESS']:
2104                 I = mat.getRGBCol()
2105                 # Convert to a value between 0 and 255
2106                 tmp_col = [ int(c * 255.0) for c in I]
2107
2108                 for c in f.col:
2109                     c.r = tmp_col[0]
2110                     c.g = tmp_col[1]
2111                     c.b = tmp_col[2]
2112                     #c.a = tmp_col[3]
2113
2114                 continue
2115
2116
2117             # do vertex color calculation
2118
2119             TotDiffSpec = Vector([0.0, 0.0, 0.0])
2120
2121             for l in self.lights:
2122                 light_obj = l
2123                 light_pos = self._getObjPosition(l)
2124                 light = light_obj.getData()
2125             
2126                 L = Vector(light_pos).normalize()
2127
2128                 V = (Vector(camPos) - Vector(f.cent)).normalize()
2129
2130                 N = Vector(f.no).normalize()
2131
2132                 if config.polygons['SHADING'] == 'TOON':
2133                     NL = ShadingUtils.toonShading(N*L)
2134                 else:
2135                     NL = (N*L)
2136
2137                 # Should we use NL instead of (N*L) here?
2138                 R = 2 * (N*L) * N - L
2139
2140                 Ip = light.getEnergy()
2141
2142                 # Diffuse co-efficient
2143                 kd = mat.getRef() * Vector(mat.getRGBCol())
2144                 for i in [0, 1, 2]:
2145                     kd[i] *= light.col[i]
2146
2147                 Idiff = Ip * kd * max(0, NL)
2148
2149
2150                 # Specular component
2151                 ks = mat.getSpec() * Vector(mat.getSpecCol())
2152                 ns = mat.getHardness()
2153                 Ispec = Ip * ks * pow(max(0, (V*R)), ns)
2154
2155                 TotDiffSpec += (Idiff+Ispec)
2156
2157
2158             # Ambient component
2159             Iamb = Vector(Blender.World.Get()[0].getAmb())
2160             ka = mat.getAmb()
2161
2162             # Emissive component (convert to a triplet)
2163             ki = Vector([mat.getEmit()]*3)
2164
2165             #I = ki + Iamb + (Idiff + Ispec)
2166             I = ki + (ka * Iamb) + TotDiffSpec
2167
2168
2169             # Set Alpha component
2170             I = list(I)
2171             I.append(mat.getAlpha())
2172
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]
2176
2177             # Convert to a value between 0 and 255
2178             tmp_col = [ int(c * 255.0) for c in I]
2179
2180             for c in f.col:
2181                 c.r = tmp_col[0]
2182                 c.g = tmp_col[1]
2183                 c.b = tmp_col[2]
2184                 c.a = tmp_col[3]
2185
2186     def _doProjection(self, mesh, projector):
2187         """Apply Viewing and Projection tranformations.
2188         """
2189
2190         for v in mesh.verts:
2191             p = projector.doProjection(v.co[:])
2192             v.co[0] = p[0]
2193             v.co[1] = p[1]
2194             v.co[2] = p[2]
2195
2196         #mesh.recalcNormals()
2197         #mesh.update()
2198
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
2203
2204         #self.cameraObj.data.type = 1
2205         #self.cameraObj.data.scale = 2.0
2206         #m = Matrix().identity()
2207         #self.cameraObj.setMatrix(m)
2208
2209     def _doViewFrustumClipping(self, mesh):
2210         """Clip faces against the View Frustum.
2211         """
2212
2213     # HSR routines
2214     def __simpleDepthSort(self, mesh):
2215         """Sort faces by the furthest vertex.
2216
2217         This simple mesthod is known also as the painter algorithm, and it
2218         solves HSR correctly only for convex meshes.
2219         """
2220
2221         #global progress
2222
2223         # The sorting requires circa n*log(n) steps
2224         n = len(mesh.faces)
2225         progress.setActivity("HSR: Painter", n*log(n))
2226
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)
2229                 )
2230
2231         # FIXME: using NMesh to sort faces. We should avoid that!
2232         nmesh = NMesh.GetRaw(mesh.name)
2233
2234         # remember that _higher_ z values mean further points
2235         nmesh.faces.sort(by_furthest_z)
2236         nmesh.faces.reverse()
2237
2238         nmesh.update()
2239
2240
2241     def __newellDepthSort(self, mesh):
2242         """Newell's depth sorting.
2243
2244         """
2245
2246         #global progress
2247
2248         # Find non planar quads and convert them to triangle
2249         #for f in mesh.faces:
2250         #    f.sel = 0
2251         #    if is_nonplanar_quad(f.v):
2252         #        print "NON QUAD??"
2253         #        f.sel = 1
2254
2255
2256         # Now reselect all faces
2257         for f in mesh.faces:
2258             f.sel = 1
2259         mesh.quadToTriangle()
2260
2261         # FIXME: using NMesh to sort faces. We should avoid that!
2262         nmesh = NMesh.GetRaw(mesh.name)
2263
2264         # remember that _higher_ z values mean further points
2265         nmesh.faces.sort(by_furthest_z)
2266         nmesh.faces.reverse()
2267
2268         # Begin depth sort tests
2269
2270         # use the smooth flag to set marked faces
2271         for f in nmesh.faces:
2272             f.smooth = 0
2273
2274         facelist = nmesh.faces[:]
2275         maplist = []
2276
2277
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)
2282
2283         
2284         while len(facelist):
2285             debug("\n----------------------\n")
2286             debug("len(facelits): %d\n" % len(facelist))
2287             P = facelist[0]
2288
2289             pSign = sign(P.normal[2])
2290
2291             # We can discard faces parallel to the view vector
2292             #if P.normal[2] == 0:
2293             #    facelist.remove(P)
2294             #    continue
2295
2296             split_done = 0
2297             face_marked = 0
2298
2299             for Q in facelist[1:]:
2300
2301                 debug("P.smooth: " + str(P.smooth) + "\n")
2302                 debug("Q.smooth: " + str(Q.smooth) + "\n")
2303                 debug("\n")
2304
2305                 qSign = sign(Q.normal[2])
2306                 # TODO: check also if Q is parallel??
2307  
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.
2310
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
2314
2315                 if notZOverlap:
2316                     debug("\nTest 0\n")
2317                     debug("NOT Z OVERLAP!\n")
2318                     if Q.smooth == 0:
2319                         # If Q is not marked then we can safely print P
2320                         break
2321                     else:
2322                         debug("met a marked face\n")
2323                         continue
2324
2325  
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)
2331
2332                 if notXOverlap:
2333                     debug("\nTest 1\n")
2334                     debug("NOT X OVERLAP!\n")
2335                     continue
2336
2337
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)
2343
2344                 if notYOverlap:
2345                     debug("\nTest 2\n")
2346                     debug("NOT Y OVERLAP!\n")
2347                     continue
2348                 
2349
2350                 # Test 3: P vertices are all behind the plane of Q
2351                 n = 0
2352                 for Pi in P:
2353                     d = qSign * HSR.Distance(Vector(Pi), Q)
2354                     if d <= EPS:
2355                         n += 1
2356                 pVerticesBehindPlaneQ = (n == len(P))
2357
2358                 if pVerticesBehindPlaneQ:
2359                     debug("\nTest 3\n")
2360                     debug("P BEHIND Q!\n")
2361                     continue
2362
2363
2364                 # Test 4: Q vertices in front of the plane of P
2365                 n = 0
2366                 for Qi in Q:
2367                     d = pSign * HSR.Distance(Vector(Qi), P)
2368                     if d >= -EPS:
2369                         n += 1
2370                 qVerticesInFrontPlaneP = (n == len(Q))
2371
2372                 if qVerticesInFrontPlaneP:
2373                     debug("\nTest 4\n")
2374                     debug("Q IN FRONT OF P!\n")
2375                     continue
2376
2377
2378                 # Test 5: Check if projections of polygons effectively overlap,
2379                 # in previous tests we checked only bounding boxes.
2380
2381                 #if not projectionsOverlap(P, Q):
2382                 if not ( HSR.projectionsOverlap(P, Q) or HSR.projectionsOverlap(Q, P)):
2383                     debug("\nTest 5\n")
2384                     debug("Projections do not overlap!\n")
2385                     continue
2386
2387                 # We still can't say if P obscures Q.
2388
2389                 # But if Q is marked we do a face-split trying to resolve a
2390                 # difficulty (maybe a visibility cycle).
2391                 if Q.smooth == 1:
2392                     # Split P or Q
2393                     debug("Possibly a cycle detected!\n")
2394                     debug("Split here!!\n")
2395
2396                     facelist = HSR.facesplit(P, Q, facelist, nmesh)
2397                     split_done = 1
2398                     break 
2399
2400                 # The question now is: Does Q obscure P?
2401
2402
2403                 # Test 3bis: Q vertices are all behind the plane of P
2404                 n = 0
2405                 for Qi in Q:
2406                     d = pSign * HSR.Distance(Vector(Qi), P)
2407                     if d <= EPS:
2408                         n += 1
2409                 qVerticesBehindPlaneP = (n == len(Q))
2410
2411                 if qVerticesBehindPlaneP:
2412                     debug("\nTest 3bis\n")
2413                     debug("Q BEHIND P!\n")
2414
2415
2416                 # Test 4bis: P vertices in front of the plane of Q
2417                 n = 0
2418                 for Pi in P:
2419                     d = qSign * HSR.Distance(Vector(Pi), Q)
2420                     if d >= -EPS:
2421                         n += 1
2422                 pVerticesInFrontPlaneQ = (n == len(P))
2423
2424                 if pVerticesInFrontPlaneQ:
2425                     debug("\nTest 4bis\n")
2426                     debug("P IN FRONT OF Q!\n")
2427
2428                 
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")
2435
2436                     facelist = HSR.facesplit(P, Q, facelist, nmesh)
2437                     split_done = 1
2438                     break 
2439                     
2440                 facelist.remove(Q)
2441                 facelist.insert(0, Q)
2442                 Q.smooth = 1
2443                 face_marked = 1
2444                 debug("Q marked!\n")
2445                 break
2446  
2447             # Write P!                     
2448             if split_done == 0 and face_marked == 0:
2449                 facelist.remove(P)
2450                 maplist.append(P)
2451                 dumpfaces(maplist, "dump"+str(len(maplist)).zfill(4)+".svg")
2452
2453                 progress.update()
2454
2455             if len(facelist) == 870:
2456                 dumpfaces([P, Q], "loopdebug.svg")
2457
2458
2459             #if facelist == None:
2460             #    maplist = [P, Q]
2461             #    print [v.co for v in P]
2462             #    print [v.co for v in Q]
2463             #    break
2464
2465             # end of while len(facelist)
2466          
2467
2468         nmesh.faces = maplist
2469         #for f in nmesh.faces:
2470         #    f.sel = 1
2471
2472         nmesh.update()
2473
2474
2475     def _doHiddenSurfaceRemoval(self, mesh):
2476         """Do HSR for the given mesh.
2477         """
2478         if len(mesh.faces) == 0:
2479             return
2480
2481         if config.polygons['HSR'] == 'PAINTER':
2482             print "\nUsing the Painter algorithm for HSR."
2483             self.__simpleDepthSort(mesh)
2484
2485         elif config.polygons['HSR'] == 'NEWELL':
2486             print "\nUsing the Newell's algorithm for HSR."
2487             self.__newellDepthSort(mesh)
2488
2489
2490     def _doEdgesStyle(self, mesh, edgestyleSelect):
2491         """Process Mesh Edges accroding to a given selection style.
2492
2493         Examples of algorithms:
2494
2495         Contours:
2496             given an edge if its adjacent faces have the same normal (that is
2497             they are complanar), than deselect it.
2498
2499         Silhouettes:
2500             given an edge if one its adjacent faces is frontfacing and the
2501             other is backfacing, than select it, else deselect.
2502         """
2503
2504         Mesh.Mode(Mesh.SelectModes['EDGE'])
2505
2506         edge_cache = MeshUtils.buildEdgeFaceUsersCache(mesh)
2507
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
2512
2513         """
2514         for e in mesh.edges:
2515
2516             e.sel = 0
2517             if edgestyleSelect(e, mesh):
2518                 e.sel = 1
2519         """
2520
2521
2522 # ---------------------------------------------------------------------
2523 #
2524 ## GUI Class and Main Program
2525 #
2526 # ---------------------------------------------------------------------
2527
2528
2529 from Blender import BGL, Draw
2530 from Blender.BGL import *
2531
2532 class GUI:
2533     
2534     def _init():
2535
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
2541
2542         # Animation toggle button
2543         GUI.animToggle = Draw.Create(config.output['ANIMATION'])
2544         GUI.evtAnimToggle = 1
2545
2546         # Join Objects toggle button
2547         GUI.joinObjsToggle = Draw.Create(config.output['JOIN_OBJECTS'])
2548         GUI.evtJoinObjsToggle = 2
2549
2550         # Render filled polygons
2551         GUI.polygonsToggle = Draw.Create(config.polygons['SHOW'])
2552
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
2558
2559         GUI.evtPolygonsToggle = 3
2560         # We hide the config.polygons['EXPANSION_TRICK'], for now
2561
2562         # Render polygon edges
2563         GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
2564         GUI.evtShowEdgesToggle = 4
2565
2566         # Render hidden edges
2567         GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
2568         GUI.evtShowHiddenEdgesToggle = 5
2569
2570         # Edge Style menu 
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
2575
2576         # Edge Width slider
2577         GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
2578         GUI.evtEdgeWidthSlider = 7
2579
2580         # Edge Color Picker
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
2584
2585         # Render Button
2586         GUI.evtRenderButton = 8
2587
2588         # Exit Button
2589         GUI.evtExitButton = 9
2590
2591     def draw():
2592
2593         # initialize static members
2594         GUI._init()
2595
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." %
2600                 __version__)
2601         glRasterPos2i(10, 335)
2602         Draw.Text("Press Q or ESC to quit.")
2603
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")
2612
2613         # Animation toggle
2614         GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
2615                 10, 260, 160, 18, GUI.animToggle.val,
2616                 "Toggle rendering of animations")
2617
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")
2622
2623         # Render Button
2624         Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
2625                 "Start Rendering")
2626         Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
2627
2628         # Rendering Styles
2629         glRasterPos2i(200, 310)
2630         Draw.Text("Rendering Style:")
2631
2632         # Render Polygons
2633         GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
2634                 200, 285, 160, 18, GUI.polygonsToggle.val,
2635                 "Render filled polygons")
2636
2637         if GUI.polygonsToggle.val == 1:
2638
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")
2646
2647
2648         # Render Edges
2649         GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
2650                 200, 235, 160, 18, GUI.showEdgesToggle.val,
2651                 "Render polygon edges")
2652
2653         if GUI.showEdgesToggle.val == 1:
2654             
2655             # Edge Style
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")
2662
2663             # Edge size
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")
2667
2668             # Edge Color
2669             GUI.edgeColorPicker = Draw.ColorPicker(GUI.evtEdgeColorPicker,
2670                     342, 185, 18, 18, GUI.edgeColorPicker.val, "Choose Edge Color")
2671
2672             # Show Hidden Edges
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")
2677
2678         glRasterPos2i(10, 160)
2679         Draw.Text("%s (c) 2006" % __author__)
2680
2681     def event(evt, val):
2682
2683         if evt == Draw.ESCKEY or evt == Draw.QKEY:
2684             Draw.Exit()
2685         else:
2686             return
2687
2688         Draw.Redraw(1)
2689
2690     def button_event(evt):
2691
2692         if evt == GUI.evtExitButton:
2693             Draw.Exit()
2694
2695         elif evt == GUI.evtOutFormatMenu:
2696             i = GUI.outFormatMenu.val - 1
2697             config.output['FORMAT']= outputWriters.keys()[i]
2698
2699         elif evt == GUI.evtAnimToggle:
2700             config.output['ANIMATION'] = bool(GUI.animToggle.val)
2701
2702         elif evt == GUI.evtJoinObjsToggle:
2703             config.output['JOIN_OBJECTS'] = bool(GUI.joinObjsToggle.val)
2704
2705         elif evt == GUI.evtPolygonsToggle:
2706             config.polygons['SHOW'] = bool(GUI.polygonsToggle.val)
2707
2708         elif evt == GUI.evtShadingStyleMenu:
2709             i = GUI.shadingStyleMenu.val - 1
2710             config.polygons['SHADING'] = shadingStyles.keys()[i]
2711
2712         elif evt == GUI.evtShowEdgesToggle:
2713             config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
2714
2715         elif evt == GUI.evtShowHiddenEdgesToggle:
2716             config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
2717
2718         elif evt == GUI.evtEdgeStyleMenu:
2719             i = GUI.edgeStyleMenu.val - 1
2720             config.edges['STYLE'] = edgeStyles.keys()[i]
2721
2722         elif evt == GUI.evtEdgeWidthSlider:
2723             config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
2724
2725         elif evt == GUI.evtEdgeColorPicker:
2726             config.edges['COLOR'] = [int(c*255.0) for c in GUI.edgeColorPicker.val]
2727
2728         elif evt == GUI.evtRenderButton:
2729             label = "Save %s" % config.output['FORMAT']
2730             # Show the File Selector
2731             global outputfile
2732             Blender.Window.FileSelector(vectorize, label, outputfile)
2733
2734         else:
2735             print "Event: %d not handled!" % evt
2736
2737         if evt:
2738             Draw.Redraw(1)
2739             #GUI.conf_debug()
2740
2741     def conf_debug():
2742         from pprint import pprint
2743         print "\nConfig"
2744         pprint(config.output)
2745         pprint(config.polygons)
2746         pprint(config.edges)
2747
2748     _init = staticmethod(_init)
2749     draw = staticmethod(draw)
2750     event = staticmethod(event)
2751     button_event = staticmethod(button_event)
2752     conf_debug = staticmethod(conf_debug)
2753
2754 # A wrapper function for the vectorizing process
2755 def vectorize(filename):
2756     """The vectorizing process is as follows:
2757      
2758      - Instanciate the writer and the renderer
2759      - Render!
2760      """
2761
2762     if filename == "":
2763         print "\nERROR: invalid file name!"
2764         return
2765
2766     from Blender import Window
2767     editmode = Window.EditMode()
2768     if editmode: Window.EditMode(0)
2769
2770     actualWriter = outputWriters[config.output['FORMAT']]
2771     writer = actualWriter(filename)
2772     
2773     renderer = Renderer()
2774     renderer.doRendering(writer, config.output['ANIMATION'])
2775
2776     if editmode: Window.EditMode(1) 
2777
2778
2779
2780 # Here the main
2781 if __name__ == "__main__":
2782
2783     global progress
2784
2785     outputfile = ""
2786     basename = Blender.sys.basename(Blender.Get('filename'))
2787     if basename != "":
2788         outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
2789
2790     if Blender.mode == 'background':
2791         progress = ConsoleProgressIndicator()
2792         vectorize(outputfile)
2793     else:
2794         progress = GraphicalProgressIndicator()
2795         Draw.Register(GUI.draw, GUI.event, GUI.button_event)