X-Git-Url: https://git.ao2.it/vrm.git/blobdiff_plain/dcc666c5b495072d477e85b544d6eb967fb6acc0..e9fc019655d1dc322bcd845e667079ea2503fc7d:/vrm.py
diff --git a/vrm.py b/vrm.py
index 35df00c..267b239 100755
--- a/vrm.py
+++ b/vrm.py
@@ -1,21 +1,21 @@
#!BPY
"""
Name: 'VRM'
-Blender: 241
+Blender: 245
Group: 'Render'
Tooltip: 'Vector Rendering Method script'
"""
__author__ = "Antonio Ospite"
-__url__ = ["http://vrm.projects.blender.org"]
-__version__ = "0.3"
+__url__ = ["http://vrm.ao2.it"]
+__version__ = "0.3.beta"
__bpydoc__ = """\
Render the scene and save the result in vector format.
"""
# ---------------------------------------------------------------------
-# Copyright (c) 2006 Antonio Ospite
+# Copyright (c) 2006, 2007, 2008, 2009 Antonio Ospite
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -40,93 +40,773 @@ __bpydoc__ = """\
# from scratch but Nikola gave me the idea, so I thank him publicly.
#
# ---------------------------------------------------------------------
-#
+#
# Things TODO for a next release:
-# - Switch to the Mesh structure, should be considerably faster
-# (partially done, but with Mesh we cannot sort faces, yet)
+# - Shadeless shader
+# - FIX the issue with negative scales in object tranformations!
# - Use a better depth sorting algorithm
# - Review how selections are made (this script uses selection states of
# primitives to represent visibility infos)
-# - Implement clipping of primitives and do handle object intersections.
-# (for now only clipping for whole objects is supported).
-# - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
-# - Implement Edge coloring
-# - Use multiple lighting sources in color calculation
-# - Implement Shading Styles? (for now we use Flat Shading).
-# - Use a data structure other than Mesh to represent the 2D image?
-# Think to a way to merge adjacent polygons that have the same color.
+# - Use a data structure other than Mesh to represent the 2D image?
+# Think to a way to merge (adjacent) polygons that have the same color.
# Or a way to use paths for silhouettes and contours.
-# - Add Vector Writers other that SVG.
# - Consider SMIL for animation handling instead of ECMA Script? (Firefox do
# not support SMIL for animations)
-# - FIX the issue with negative scales in object tranformations!
-#
-# ---------------------------------------------------------------------
-#
-# Changelog:
-#
-# vrm-0.3.py - 2006-05-19
-# * First release after code restucturing.
-# Now the script offers a useful set of functionalities
-# and it can render animations, too.
+# - Switch to the Mesh structure, should be considerably faster
+# (partially done, but with Mesh we cannot sort faces, yet)
+# - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
+# - Implement Shading Styles? (partially done, to make more flexible).
+# - Add Vector Writers other than SVG.
+# - set the background color!
+# - Check memory use!!
#
# ---------------------------------------------------------------------
import Blender
-from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera
+from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera, Window
from Blender.Mathutils import *
from math import *
+import sys
+import time
+
+try:
+ set()
+except NameError:
+ from sets import Set as set
+
+
+def uniq(alist):
+ tmpdict = dict()
+ return [tmpdict.setdefault(e, e) for e in alist if e not in tmpdict]
+ # in python > 2.4 we ca use the following
+ #return [ u for u in alist if u not in locals()['_[1]'] ]
+
+
+# Constants
+EPS = 10e-5
+
+# We use a global progress Indicator Object
+progress = None
+
+
+# Config class for global settings
+
+class config:
+ polygons = dict()
+ polygons['SHOW'] = True
+ polygons['SHADING'] = 'FLAT' # FLAT or TOON
+ polygons['HSR'] = 'PAINTER' # PAINTER or NEWELL
+ # Hidden to the user for now
+ polygons['EXPANSION_TRICK'] = True
+
+ polygons['TOON_LEVELS'] = 2
+
+ edges = dict()
+ edges['SHOW'] = False
+ edges['SHOW_HIDDEN'] = False
+ edges['STYLE'] = 'MESH' # MESH or SILHOUETTE
+ edges['WIDTH'] = 2
+ edges['COLOR'] = [0, 0, 0]
+
+ output = dict()
+ output['FORMAT'] = 'SVG'
+ output['ANIMATION'] = False
+ output['JOIN_OBJECTS'] = True
+
+ def saveToRegistry():
+ registry = {}
+
+ for k, v in config.__dict__.iteritems():
+
+ # config class store settings in dictionaries
+ if v.__class__ == dict().__class__:
+
+ regkey_prefix = k.upper() + "_"
+
+ for opt_k, opt_v in v.iteritems():
+ regkey = regkey_prefix + opt_k
+
+ registry[regkey] = opt_v
+
+ Blender.Registry.SetKey('VRM', registry, True)
+
+ saveToRegistry = staticmethod(saveToRegistry)
+
+ def loadFromRegistry():
+ registry = Blender.Registry.GetKey('VRM', True)
+ if not registry:
+ return
+
+ for k, v in registry.iteritems():
+ k_tmp = k.split('_')
+ conf_attr = k_tmp[0].lower()
+ conf_key = str.join("_", k_tmp[1:])
+ conf_val = v
+
+ if conf_attr in config.__dict__:
+ config.__dict__[conf_attr][conf_key] = conf_val
+
+ loadFromRegistry = staticmethod(loadFromRegistry)
+
+
+# Utility functions
+print_debug = False
+
+
+def dumpfaces(flist, filename):
+ """Dump a single face to a file.
+ """
+ if not print_debug:
+ return
+
+ class tmpmesh:
+ pass
+
+ m = tmpmesh()
+ m.faces = flist
+ writerobj = SVGVectorWriter(filename)
-# Some global settings
+ writerobj.open()
+ writerobj._printPolygons(m)
-PRINT_POLYGONS = True
+ writerobj.close()
-POLYGON_EXPANSION_TRICK = True # Hidden to the user for now
-PRINT_EDGES = False
-SHOW_HIDDEN_EDGES = False
-EDGE_STYLE = 'silhouette'
-EDGES_WIDTH = 0.5
+def debug(msg):
+ if print_debug:
+ sys.stderr.write(msg)
-RENDER_ANIMATION = False
-OPTIMIZE_FOR_SPACE = True
+def EQ(v1, v2):
+ return (abs(v1[0] - v2[0]) < EPS and
+ abs(v1[1] - v2[1]) < EPS)
+by_furthest_z = (lambda f1, f2:
+ cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2]) + EPS)
+ )
-OUTPUT_FORMAT = 'SVG'
+def sign(x):
+
+ if x < -EPS:
+ #if x < 0:
+ return -1
+ elif x > EPS:
+ #elif x > 0:
+ return 1
+ else:
+ return 0
# ---------------------------------------------------------------------
#
-## Utility Mesh class
+## HSR Utility class
#
# ---------------------------------------------------------------------
-class MeshUtils:
- def getEdgeAdjacentFaces(edge, mesh):
- """Get the faces adjacent to a given edge.
+EPS = 10e-5
+INF = 10e5
+
+
+class HSR:
+ """A utility class for HSR processing.
+ """
+
+ def is_nonplanar_quad(face):
+ """Determine if a quad is non-planar.
+
+ From: http://mathworld.wolfram.com/Coplanar.html
- There can be 0, 1 or more (usually 2) faces adjacent to an edge.
+ Geometric objects lying in a common plane are said to be coplanar.
+ Three noncollinear points determine a plane and so are trivially
+ coplanar. Four points are coplanar iff the volume of the tetrahedron
+ defined by them is 0,
+
+ | x_1 y_1 z_1 1 |
+ | x_2 y_2 z_2 1 |
+ | x_3 y_3 z_3 1 |
+ | x_4 y_4 z_4 1 | == 0
+
+ Coplanarity is equivalent to the statement that the pair of lines
+ determined by the four points are not skew, and can be equivalently
+ stated in vector form as (x_3-x_1).[(x_2-x_1)x(x_4-x_3)]==0.
+
+ An arbitrary number of n points x_1, ..., x_n can be tested for
+ coplanarity by finding the point-plane distances of the points
+ x_4, ..., x_n from the plane determined by (x_1,x_2,x_3)
+ and checking if they are all zero.
+ If so, the points are all coplanar.
+
+ We here check only for 4-point complanarity.
"""
- adjface_list = []
+ n = len(face)
- for f in mesh.faces:
- if (edge.v1 in f.v) and (edge.v2 in f.v):
- adjface_list.append(f)
+ # assert(n>4)
+ if n < 3 or n > 4:
+ print "ERROR a mesh in Blender can't have more than 4 vertices or less than 3"
+ raise AssertionError
+
+ elif n == 3:
+ # three points must be complanar
+ return False
+ else: # n == 4
+ x1 = Vector(face[0].co)
+ x2 = Vector(face[1].co)
+ x3 = Vector(face[2].co)
+ x4 = Vector(face[3].co)
+
+ v = (x3 - x1) * CrossVecs((x2 - x1), (x4 - x3))
+ if v != 0:
+ return True
+
+ return False
+
+ is_nonplanar_quad = staticmethod(is_nonplanar_quad)
+
+ def pointInPolygon(poly, v):
+ return False
+
+ pointInPolygon = staticmethod(pointInPolygon)
+
+ def edgeIntersection(s1, s2, do_perturbate=False):
+
+ (x1, y1) = s1[0].co[0], s1[0].co[1]
+ (x2, y2) = s1[1].co[0], s1[1].co[1]
+
+ (x3, y3) = s2[0].co[0], s2[0].co[1]
+ (x4, y4) = s2[1].co[0], s2[1].co[1]
+
+ #z1 = s1[0].co[2]
+ #z2 = s1[1].co[2]
+ #z3 = s2[0].co[2]
+ #z4 = s2[1].co[2]
+
+ # calculate delta values (vector components)
+ dx1 = x2 - x1
+ dx2 = x4 - x3
+ dy1 = y2 - y1
+ dy2 = y4 - y3
+
+ #dz1 = z2 - z1
+ #dz2 = z4 - z3
+
+ C = dy2 * dx1 - dx2 * dy1 # cross product
+ if C == 0: # parallel
+ return None
+
+ dx3 = x1 - x3 # combined origin offset vector
+ dy3 = y1 - y3
+
+ a1 = (dy3 * dx2 - dx3 * dy2) / C
+ a2 = (dy3 * dx1 - dx3 * dy1) / C
+
+ # check for degeneracies
+ #print_debug("\n")
+ #print_debug(str(a1)+"\n")
+ #print_debug(str(a2)+"\n\n")
+
+ if (a1 == 0 or a1 == 1 or a2 == 0 or a2 == 1):
+ # Intersection on boundaries, we consider the point external?
+ return None
+
+ elif (a1 > 0.0 and a1 < 1.0 and a2 > 0.0 and a2 < 1.0): # lines cross
+ x = x1 + a1 * dx1
+ y = y1 + a1 * dy1
+
+ #z = z1 + a1 * dz1
+ z = 0
+ return (NMesh.Vert(x, y, z), a1, a2)
+
+ else:
+ # lines have intersections but not those segments
+ return None
+
+ edgeIntersection = staticmethod(edgeIntersection)
+
+ def isVertInside(self, v):
+ winding_number = 0
+ coincidence = False
+
+ # Create point at infinity
+ point_at_infinity = NMesh.Vert(-INF, v.co[1], -INF)
+
+ for i in range(len(self.v)):
+ s1 = (point_at_infinity, v)
+ s2 = (self.v[i - 1], self.v[i])
+
+ if EQ(v.co, s2[0].co) or EQ(v.co, s2[1].co):
+ coincidence = True
+
+ if HSR.edgeIntersection(s1, s2, do_perturbate=False):
+ winding_number += 1
+
+ # Check even or odd
+ if (winding_number % 2) == 0:
+ return False
+ else:
+ if coincidence:
+ return False
+ return True
+
+ isVertInside = staticmethod(isVertInside)
+
+ def det(a, b, c):
+ return ((b[0] - a[0]) * (c[1] - a[1]) -
+ (b[1] - a[1]) * (c[0] - a[0]))
+
+ det = staticmethod(det)
+
+ def pointInPolygon(q, P):
+ is_in = False
+
+ point_at_infinity = NMesh.Vert(-INF, q.co[1], -INF)
+
+ det = HSR.det
+
+ for i in range(len(P.v)):
+ p0 = P.v[i - 1]
+ p1 = P.v[i]
+ if (det(q.co, point_at_infinity.co, p0.co) < 0) != (det(q.co, point_at_infinity.co, p1.co) < 0):
+ if det(p0.co, p1.co, q.co) == 0:
+ #print "On Boundary"
+ return False
+ elif (det(p0.co, p1.co, q.co) < 0) != (det(p0.co, p1.co, point_at_infinity.co) < 0):
+ is_in = not is_in
+
+ return is_in
+
+ pointInPolygon = staticmethod(pointInPolygon)
+
+ def projectionsOverlap(f1, f2):
+ """ If you have nonconvex, but still simple polygons, an acceptable method
+ is to iterate over all vertices and perform the Point-in-polygon test[1].
+ The advantage of this method is that you can compute the exact
+ intersection point and collision normal that you will need to simulate
+ collision. When you have the point that lies inside the other polygon, you
+ just iterate over all edges of the second polygon again and look for edge
+ intersections. Note that this method detects collsion when it already
+ happens. This algorithm is fast enough to perform it hundreds of times per
+ sec. """
+
+ for i in range(len(f1.v)):
+
+ # If a point of f1 in inside f2, there is an overlap!
+ v1 = f1.v[i]
+ #if HSR.isVertInside(f2, v1):
+ if HSR.pointInPolygon(v1, f2):
+ return True
+
+ # If not the polygon can be ovelap as well, so we check for
+ # intersection between an edge of f1 and all the edges of f2
+
+ v0 = f1.v[i - 1]
+
+ for j in range(len(f2.v)):
+ v2 = f2.v[j - 1]
+ v3 = f2.v[j]
+
+ e1 = v0, v1
+ e2 = v2, v3
+
+ intrs = HSR.edgeIntersection(e1, e2)
+ if intrs:
+ #print_debug(str(v0.co) + " " + str(v1.co) + " " +
+ # str(v2.co) + " " + str(v3.co) )
+ #print_debug("\nIntersection\n")
+
+ return True
+
+ return False
+
+ projectionsOverlap = staticmethod(projectionsOverlap)
+
+ def midpoint(p1, p2):
+ """Return the midpoint of two vertices.
+ """
+ m = MidpointVecs(Vector(p1), Vector(p2))
+ mv = NMesh.Vert(m[0], m[1], m[2])
+
+ return mv
+
+ midpoint = staticmethod(midpoint)
+
+ def facesplit(P, Q, facelist, nmesh):
+ """Split P or Q according to the strategy illustrated in the Newell's
+ paper.
+ """
+
+ by_furthest_z = (lambda f1, f2:
+ cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2]) + EPS)
+ )
+
+ # Choose if split P on Q plane or vice-versa
+
+ n = 0
+ for Pi in P:
+ d = HSR.Distance(Vector(Pi), Q)
+ if d <= EPS:
+ n += 1
+ pIntersectQ = (n != len(P))
+
+ n = 0
+ for Qi in Q:
+ d = HSR.Distance(Vector(Qi), P)
+ if d >= -EPS:
+ n += 1
+ qIntersectP = (n != len(Q))
+
+ newfaces = []
+
+ # 1. If parts of P lie in both half-spaces of Q
+ # then splice P in two with the plane of Q
+ if pIntersectQ:
+ #print "We split P"
+ f = P
+ plane = Q
+
+ newfaces = HSR.splitOn(plane, f)
+
+ # 2. Else if parts of Q lie in both half-space of P
+ # then splice Q in two with the plane of P
+ if qIntersectP and newfaces == None:
+ #print "We split Q"
+ f = Q
+ plane = P
+
+ newfaces = HSR.splitOn(plane, f)
+ #print "After"
+
+ # 3. Else slice P in half through the mid-point of
+ # the longest pair of opposite sides
+ if newfaces == None:
+
+ print "We ignore P..."
+ facelist.remove(P)
+ return facelist
+
+ #f = P
+
+ #if len(P)==3:
+ # v1 = midpoint(f[0], f[1])
+ # v2 = midpoint(f[1], f[2])
+ #if len(P)==4:
+ # v1 = midpoint(f[0], f[1])
+ # v2 = midpoint(f[2], f[3])
+ #vec3 = (Vector(v2)+10*Vector(f.normal))
+ #
+ #v3 = NMesh.Vert(vec3[0], vec3[1], vec3[2])
+
+ #plane = NMesh.Face([v1, v2, v3])
+ #
+ #newfaces = splitOn(plane, f)
+
+ if newfaces == None:
+ print "Big FAT problem, we weren't able to split POLYGONS!"
+ raise AssertionError
+
+ #print newfaces
+ if newfaces:
+ #for v in f:
+ # if v not in plane and v in nmesh.verts:
+ # nmesh.verts.remove(v)
+ for nf in newfaces:
+
+ nf.mat = f.mat
+ nf.sel = f.sel
+ nf.col = [f.col[0]] * len(nf.v)
+
+ nf.smooth = 0
+
+ for v in nf:
+ nmesh.verts.append(v)
+ # insert pieces in the list
+ facelist.append(nf)
+
+ facelist.remove(f)
+
+ # and resort the faces
+ facelist.sort(by_furthest_z)
+ facelist.sort(lambda f1, f2: cmp(f1.smooth, f2.smooth))
+ facelist.reverse()
+
+ #print [ f.smooth for f in facelist ]
+
+ return facelist
+
+ facesplit = staticmethod(facesplit)
+
+ def isOnSegment(v1, v2, p, extremes_internal=False):
+ """Check if point p is in segment v1v2.
+ """
+
+ l1 = (v1 - p).length
+ l2 = (v2 - p).length
+
+ # Should we consider extreme points as internal ?
+ # The test:
+ # if p == v1 or p == v2:
+ if l1 < EPS or l2 < EPS:
+ return extremes_internal
+
+ l = (v1 - v2).length
+
+ # if the sum of l1 and l2 is circa l, then the point is on segment,
+ if abs(l - (l1 + l2)) < EPS:
+ return True
+ else:
+ return False
+
+ isOnSegment = staticmethod(isOnSegment)
+
+ def Distance(point, face):
+ """ Calculate the distance between a point and a face.
+
+ An alternative but more expensive method can be:
+
+ ip = Intersect(Vector(face[0]), Vector(face[1]), Vector(face[2]),
+ Vector(face.no), Vector(point), 0)
+
+ d = Vector(ip - point).length
+
+ See: http://mathworld.wolfram.com/Point-PlaneDistance.html
+ """
+
+ p = Vector(point)
+ plNormal = Vector(face.no)
+ plVert0 = Vector(face.v[0])
+
+ d = (plVert0 * plNormal) - (p * plNormal)
+
+ #d = plNormal * (plVert0 - p)
+
+ #print "\nd: %.10f - sel: %d, %s\n" % (d, face.sel, str(point))
+
+ return d
+
+ Distance = staticmethod(Distance)
+
+ def makeFaces(vl):
+ #
+ # make one or two new faces based on a list of vertex-indices
+ #
+ newfaces = []
+
+ if len(vl) <= 4:
+ nf = NMesh.Face()
- return adjface_list
+ for v in vl:
+ nf.v.append(v)
- def isVisibleEdge(e, mesh):
- """Normal edge selection rule.
+ newfaces.append(nf)
- An edge is visible if _any_ of its adjacent faces is selected.
+ else:
+ nf = NMesh.Face()
+
+ nf.v.append(vl[0])
+ nf.v.append(vl[1])
+ nf.v.append(vl[2])
+ nf.v.append(vl[3])
+ newfaces.append(nf)
+
+ nf = NMesh.Face()
+ nf.v.append(vl[3])
+ nf.v.append(vl[4])
+ nf.v.append(vl[0])
+ newfaces.append(nf)
+
+ return newfaces
+
+ makeFaces = staticmethod(makeFaces)
+
+ def splitOn(Q, P, return_positive_faces=True, return_negative_faces=True):
+ """Split P using the plane of Q.
+ Logic taken from the knife.py python script
+ """
+
+ # Check if P and Q are parallel
+ u = CrossVecs(Vector(Q.no), Vector(P.no))
+ ax = abs(u[0])
+ ay = abs(u[1])
+ az = abs(u[2])
+
+ if (ax + ay + az) < EPS:
+ print "PARALLEL planes!!"
+ return
+
+ # The final aim is to find the intersection line between P
+ # and the plane of Q, and split P along this line
+
+ nP = len(P.v)
+
+ # Calculate point-plane Distance between vertices of P and plane Q
+ d = []
+ for i in range(0, nP):
+ d.append(HSR.Distance(P.v[i], Q))
+
+ newVertList = []
+
+ posVertList = []
+ negVertList = []
+ for i in range(nP):
+ d0 = d[i - 1]
+ V0 = P.v[i - 1]
+
+ d1 = d[i]
+ V1 = P.v[i]
+
+ #print "d0:", d0, "d1:", d1
+
+ # if the vertex lies in the cutplane
+ if abs(d1) < EPS:
+ #print "d1 On cutplane"
+ posVertList.append(V1)
+ negVertList.append(V1)
+ else:
+ # if the previous vertex lies in cutplane
+ if abs(d0) < EPS:
+ #print "d0 on Cutplane"
+ if d1 > 0:
+ #print "d1 on positive Halfspace"
+ posVertList.append(V1)
+ else:
+ #print "d1 on negative Halfspace"
+ negVertList.append(V1)
+ else:
+ # if they are on the same side of the plane
+ if (d1 * d0) > 0:
+ #print "On the same half-space"
+ if d1 > 0:
+ #print "d1 on positive Halfspace"
+ posVertList.append(V1)
+ else:
+ #print "d1 on negative Halfspace"
+ negVertList.append(V1)
+
+ # the vertices are not on the same side of the plane, so we have an intersection
+ else:
+ #print "Intersection"
+
+ e = Vector(V0), Vector(V1)
+ tri = Vector(Q[0]), Vector(Q[1]), Vector(Q[2])
+
+ inters = Intersect(tri[0], tri[1], tri[2], e[1] - e[0], e[0], 0)
+ if inters == None:
+ print "Split Break"
+ break
+
+ #print "Intersection", inters
+
+ nv = NMesh.Vert(inters[0], inters[1], inters[2])
+ newVertList.append(nv)
+
+ posVertList.append(nv)
+ negVertList.append(nv)
+
+ if d1 > 0:
+ posVertList.append(V1)
+ else:
+ negVertList.append(V1)
+
+ # uniq for python > 2.4
+ #posVertList = [ u for u in posVertList if u not in locals()['_[1]'] ]
+ #negVertList = [ u for u in negVertList if u not in locals()['_[1]'] ]
+
+ # a more portable way
+ posVertList = uniq(posVertList)
+ negVertList = uniq(negVertList)
+
+ # If vertex are all on the same half-space, return
+ #if len(posVertList) < 3:
+ # print "Problem, we created a face with less that 3 vertices??"
+ # posVertList = []
+ #if len(negVertList) < 3:
+ # print "Problem, we created a face with less that 3 vertices??"
+ # negVertList = []
+
+ if len(posVertList) < 3 or len(negVertList) < 3:
+ #print "RETURN NONE, SURE???"
+ return None
+
+ if not return_positive_faces:
+ posVertList = []
+ if not return_negative_faces:
+ negVertList = []
+
+ newfaces = HSR.addNewFaces(posVertList, negVertList)
+
+ return newfaces
+
+ splitOn = staticmethod(splitOn)
+
+ def addNewFaces(posVertList, negVertList):
+ # Create new faces resulting from the split
+ outfaces = []
+ if len(posVertList) or len(negVertList):
+
+ #newfaces = [posVertList] + [negVertList]
+ newfaces = ([[NMesh.Vert(v[0], v[1], v[2]) for v in posVertList]] +
+ [[NMesh.Vert(v[0], v[1], v[2]) for v in negVertList]])
+
+ for nf in newfaces:
+ if nf and len(nf) > 2:
+ outfaces += HSR.makeFaces(nf)
+
+ return outfaces
+
+ addNewFaces = staticmethod(addNewFaces)
+
+
+# ---------------------------------------------------------------------
+#
+## Mesh Utility class
+#
+# ---------------------------------------------------------------------
+
+class MeshUtils:
+
+ def buildEdgeFaceUsersCache(me):
+ '''
+ Takes a mesh and returns a list aligned with the meshes edges.
+ Each item is a list of the faces that use the edge
+ would be the equiv for having ed.face_users as a property
+
+ Taken from .blender/scripts/bpymodules/BPyMesh.py,
+ thanks to ideasman_42.
+ '''
+
+ def sorted_edge_indicies(ed):
+ i1 = ed.v1.index
+ i2 = ed.v2.index
+ if i1 > i2:
+ i1, i2 = i2, i1
+ return i1, i2
+
+ face_edges_dict = dict([(sorted_edge_indicies(ed), (ed.index, [])) for ed in me.edges])
+ for f in me.faces:
+ fvi = [v.index for v in f.v] # face vert idx's
+ for i in xrange(len(f)):
+ i1 = fvi[i]
+ i2 = fvi[i - 1]
+
+ if i1 > i2:
+ i1, i2 = i2, i1
+
+ face_edges_dict[i1, i2][1].append(f)
+
+ face_edges = [None] * len(me.edges)
+ for ed_index, ed_faces in face_edges_dict.itervalues():
+ face_edges[ed_index] = ed_faces
+
+ return face_edges
+
+ def isMeshEdge(adjacent_faces):
+ """Mesh edge rule.
+
+ A mesh edge is visible if _at_least_one_ of its adjacent faces is selected.
Note: if the edge has no adjacent faces we want to show it as well,
useful for "edge only" portion of objects.
"""
- adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
-
if len(adjacent_faces) == 0:
return True
@@ -137,7 +817,7 @@ class MeshUtils:
else:
return False
- def isSilhouetteEdge(e, mesh):
+ def isSilhouetteEdge(adjacent_faces):
"""Silhuette selection rule.
An edge is a silhuette edge if it is shared by two faces with
@@ -145,8 +825,6 @@ class MeshUtils:
face.
"""
- adjacent_faces = MeshUtils.getEdgeAdjacentFaces(e, mesh)
-
if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
(len(adjacent_faces) == 2 and
adjacent_faces[0].sel != adjacent_faces[1].sel)
@@ -154,12 +832,52 @@ class MeshUtils:
return True
else:
return False
-
- getEdgeAdjacentFaces = staticmethod(getEdgeAdjacentFaces)
- isVisibleEdge = staticmethod(isVisibleEdge)
+
+ buildEdgeFaceUsersCache = staticmethod(buildEdgeFaceUsersCache)
+ isMeshEdge = staticmethod(isMeshEdge)
isSilhouetteEdge = staticmethod(isSilhouetteEdge)
+# ---------------------------------------------------------------------
+#
+## Shading Utility class
+#
+# ---------------------------------------------------------------------
+
+class ShadingUtils:
+
+ shademap = None
+
+ def toonShadingMapSetup():
+ levels = config.polygons['TOON_LEVELS']
+
+ texels = 2 * levels - 1
+ tmp_shademap = [0.0] + [(i) / float(texels - 1) for i in xrange(1, texels - 1)] + [1.0]
+
+ return tmp_shademap
+
+ def toonShading(u):
+
+ shademap = ShadingUtils.shademap
+
+ if not shademap:
+ shademap = ShadingUtils.toonShadingMapSetup()
+
+ v = 1.0
+ for i in xrange(0, len(shademap) - 1):
+ pivot = (shademap[i] + shademap[i + 1]) / 2.0
+ j = int(u > pivot)
+
+ v = shademap[i + j]
+
+ if v < shademap[i + 1]:
+ return v
+
+ return v
+
+ toonShadingMapSetup = staticmethod(toonShadingMapSetup)
+ toonShading = staticmethod(toonShading)
+
# ---------------------------------------------------------------------
#
@@ -169,10 +887,10 @@ class MeshUtils:
class Projector:
"""Calculate the projection of an object given the camera.
-
+
A projector is useful to so some per-object transformation to obtain the
projection of an object given the camera.
-
+
The main method is #doProjection# see the method description for the
parameter list.
"""
@@ -186,26 +904,32 @@ class Projector:
camera = cameraObj.getData()
- aspect = float(canvasRatio[0])/float(canvasRatio[1])
+ aspect = float(canvasRatio[0]) / float(canvasRatio[1])
near = camera.clipStart
far = camera.clipEnd
scale = float(camera.scale)
- fovy = atan(0.5/aspect/(camera.lens/32))
- fovy = fovy * 360.0/pi
-
- # What projection do we want?
- if camera.type:
- #mP = self._calcOrthoMatrix(fovy, aspect, near, far, 17) #camera.scale)
- mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale)
+ fovy = atan(0.5 / aspect / (camera.lens / 32))
+ fovy = fovy * 360.0 / pi
+
+ if Blender.Get('version') < 243:
+ camPersp = 0
+ camOrtho = 1
else:
- mP = self._calcPerspectiveMatrix(fovy, aspect, near, far)
-
+ camPersp = 'persp'
+ camOrtho = 'ortho'
+
+ # What projection do we want?
+ if camera.type == camPersp:
+ mP = self._calcPerspectiveMatrix(fovy, aspect, near, far)
+ elif camera.type == camOrtho:
+ mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale)
+
# View transformation
cam = Matrix(cameraObj.getInverseMatrix())
- cam.transpose()
-
+ cam.transpose()
+
mP = mP * cam
self.projectionMatrix = mP
@@ -220,13 +944,18 @@ class Projector:
Given a vertex calculate the projection using the current projection
matrix.
"""
-
+
# Note that we have to work on the vertex using homogeneous coordinates
+ # From blender 2.42+ we don't need to resize the vector to be 4d
+ # when applying a 4x4 matrix, but we do that anyway since we need the
+ # 4th coordinate later
p = self.projectionMatrix * Vector(v).resize4D()
- if p[3]>0:
- p[0] = p[0]/p[3]
- p[1] = p[1]/p[3]
+ # Perspective division
+ if p[3] != 0:
+ p[0] = p[0] / p[3]
+ p[1] = p[1] / p[3]
+ p[2] = p[2] / p[3]
# restore the size
p[3] = 1.0
@@ -237,22 +966,22 @@ class Projector:
##
# Private methods
#
-
+
def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
"""Return a perspective projection matrix.
"""
-
+
top = near * tan(fovy * pi / 360.0)
bottom = -top
- left = bottom*aspect
- right= top*aspect
- x = (2.0 * near) / (right-left)
- y = (2.0 * near) / (top-bottom)
- a = (right+left) / (right-left)
- b = (top+bottom) / (top - bottom)
- c = - ((far+near) / (far-near))
- d = - ((2*far*near)/(far-near))
-
+ left = bottom * aspect
+ right = top * aspect
+ x = (2.0 * near) / (right - left)
+ y = (2.0 * near) / (top - bottom)
+ a = (right + left) / (right - left)
+ b = (top + bottom) / (top - bottom)
+ c = - ((far + near) / (far - near))
+ d = - ((2 * far * near) / (far - near))
+
m = Matrix(
[x, 0.0, a, 0.0],
[0.0, y, b, 0.0],
@@ -261,31 +990,194 @@ class Projector:
return m
- def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
+ def _calcOrthoMatrix(self, fovy, aspect, near, far, scale):
"""Return an orthogonal projection matrix.
"""
-
+
# The 11 in the formula was found emiprically
top = near * tan(fovy * pi / 360.0) * (scale * 11)
- bottom = -top
+ bottom = -top
left = bottom * aspect
- right= top * aspect
- rl = right-left
- tb = top-bottom
- fn = near-far
- tx = -((right+left)/rl)
- ty = -((top+bottom)/tb)
- tz = ((far+near)/fn)
+ right = top * aspect
+ rl = right - left
+ tb = top - bottom
+ fn = near - far
+ tx = -((right + left) / rl)
+ ty = -((top + bottom) / tb)
+ tz = ((far + near) / fn)
m = Matrix(
- [2.0/rl, 0.0, 0.0, tx],
- [0.0, 2.0/tb, 0.0, ty],
- [0.0, 0.0, 2.0/fn, tz],
- [0.0, 0.0, 0.0, 1.0])
-
+ [2.0 / rl, 0.0, 0.0, tx],
+ [0.0, 2.0 / tb, 0.0, ty],
+ [0.0, 0.0, 2.0 / fn, tz],
+ [0.0, 0.0, 0.0, 1.0])
+
return m
+# ---------------------------------------------------------------------
+#
+## Progress Indicator
+#
+# ---------------------------------------------------------------------
+
+class Progress:
+ """A model for a progress indicator.
+
+ Do the progress calculation calculation and
+ the view independent stuff of a progress indicator.
+ """
+ def __init__(self, steps=0):
+ self.name = ""
+ self.steps = steps
+ self.completed = 0
+ self.progress = 0
+
+ def setSteps(self, steps):
+ """Set the number of steps of the activity wich we want to track.
+ """
+ self.steps = steps
+
+ def getSteps(self):
+ return self.steps
+
+ def setName(self, name):
+ """Set the name of the activity wich we want to track.
+ """
+ self.name = name
+
+ def getName(self):
+ return self.name
+
+ def getProgress(self):
+ return self.progress
+
+ def reset(self):
+ self.completed = 0
+ self.progress = 0
+
+ def update(self):
+ """Update the model, call this method when one step is completed.
+ """
+ if self.progress == 100:
+ return False
+
+ self.completed += 1
+ self.progress = (float(self.completed) / float(self.steps)) * 100
+ self.progress = int(self.progress)
+
+ return True
+
+
+class ProgressIndicator:
+ """An abstraction of a View for the Progress Model
+ """
+ def __init__(self):
+
+ # Use a refresh rate so we do not show the progress at
+ # every update, but every 'self.refresh_rate' times.
+ self.refresh_rate = 10
+ self.shows_counter = 0
+
+ self.quiet = False
+
+ self.progressModel = None
+
+ def setQuiet(self, value):
+ self.quiet = value
+
+ def setActivity(self, name, steps):
+ """Initialize the Model.
+
+ In a future version (with subactivities-progress support) this method
+ could only set the current activity.
+ """
+ self.progressModel = Progress()
+ self.progressModel.setName(name)
+ self.progressModel.setSteps(steps)
+
+ def getActivity(self):
+ return self.progressModel
+
+ def update(self):
+ """Update the model and show the actual progress.
+ """
+ assert(self.progressModel)
+
+ if self.progressModel.update():
+ if self.quiet:
+ return
+
+ self.show(self.progressModel.getProgress(),
+ self.progressModel.getName())
+
+ # We return always True here so we can call the update() method also
+ # from lambda funcs (putting the call in logical AND with other ops)
+ return True
+
+ def show(self, progress, name=""):
+ self.shows_counter = (self.shows_counter + 1) % self.refresh_rate
+ if self.shows_counter != 0:
+ return
+
+ if progress == 100:
+ self.shows_counter = -1
+
+
+class ConsoleProgressIndicator(ProgressIndicator):
+ """Show a progress bar on stderr, a la wget.
+ """
+ def __init__(self):
+ ProgressIndicator.__init__(self)
+
+ self.swirl_chars = ["-", "\\", "|", "/"]
+ self.swirl_count = -1
+
+ def show(self, progress, name):
+ ProgressIndicator.show(self, progress, name)
+
+ bar_length = 70
+ bar_progress = int((progress / 100.0) * bar_length)
+ bar = ("=" * bar_progress).ljust(bar_length)
+
+ self.swirl_count = (self.swirl_count + 1) % len(self.swirl_chars)
+ swirl_char = self.swirl_chars[self.swirl_count]
+
+ progress_bar = "%s |%s| %c %3d%%" % (name, bar, swirl_char, progress)
+
+ sys.stderr.write(progress_bar + "\r")
+ if progress == 100:
+ sys.stderr.write("\n")
+
+
+class GraphicalProgressIndicator(ProgressIndicator):
+ """Interface to the Blender.Window.DrawProgressBar() method.
+ """
+ def __init__(self):
+ ProgressIndicator.__init__(self)
+
+ #self.swirl_chars = ["-", "\\", "|", "/"]
+ # We have to use letters with the same width, for now!
+ # Blender progress bar considers the font widths when
+ # calculating the progress bar width.
+ self.swirl_chars = ["\\", "/"]
+ self.swirl_count = -1
+
+ def show(self, progress, name):
+ ProgressIndicator.show(self, progress)
+
+ self.swirl_count = (self.swirl_count + 1) % len(self.swirl_chars)
+ swirl_char = self.swirl_chars[self.swirl_count]
+
+ progress_text = "%s - %c %3d%%" % (name, swirl_char, progress)
+
+ # Finally draw the Progress Bar
+ Window.WaitCursor(1) # Maybe we can move that call in the constructor?
+ Window.DrawProgressBar(progress / 100.0, progress_text)
+
+ if progress == 100:
+ Window.DrawProgressBar(1, progress_text)
+ Window.WaitCursor(0)
# ---------------------------------------------------------------------
#
@@ -319,38 +1211,42 @@ class VectorWriter:
- printCanvas(self, scene,
doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
"""
-
+
def __init__(self, fileName):
"""Set the output file name and other properties"""
+ try:
+ config.writer
+ except:
+ config.writer = dict()
+ config.writer['SETTING'] = True
+
self.outputFileName = fileName
- self.file = None
-
+
context = Scene.GetCurrent().getRenderingContext()
- self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
+ self.canvasSize = (context.imageSizeX(), context.imageSizeY())
+
+ self.fps = context.fps
self.startFrame = 1
self.endFrame = 1
self.animation = False
-
##
# Public Methods
#
-
+
def open(self, startFrame=1, endFrame=1):
if startFrame != endFrame:
self.startFrame = startFrame
self.endFrame = endFrame
self.animation = True
- self.file = open(self.outputFileName, "w")
print "Outputting to: ", self.outputFileName
return
def close(self):
- self.file.close()
return
def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
@@ -358,7 +1254,7 @@ class VectorWriter:
"""This is the interface for the needed printing routine.
"""
return
-
+
## SVG Writer
@@ -371,6 +1267,7 @@ class SVGVectorWriter(VectorWriter):
"""
VectorWriter.__init__(self, fileName)
+ self.file = None
##
# Public Methods
@@ -380,6 +1277,9 @@ class SVGVectorWriter(VectorWriter):
"""Do some initialization operations.
"""
VectorWriter.open(self, startFrame, endFrame)
+
+ self.file = open(self.outputFileName, "w")
+
self._printHeader()
def close(self):
@@ -387,16 +1287,18 @@ class SVGVectorWriter(VectorWriter):
"""
self._printFooter()
- # remember to call the close method of the parent
+ if self.file:
+ self.file.close()
+
+ # remember to call the close method of the parent as last
VectorWriter.close(self)
-
def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
showHiddenEdges=False):
"""Convert the scene representation to SVG.
"""
- Objects = scene.getChildren()
+ Objects = scene.objects
context = scene.getRenderingContext()
framenumber = context.currentFrame()
@@ -405,15 +1307,14 @@ class SVGVectorWriter(VectorWriter):
framestyle = "display:none"
else:
framestyle = "display:block"
-
+
# Assign an id to this group so we can set properties on it using DOM
self.file.write("\n" %
- (framenumber, framestyle) )
-
+ (framenumber, framestyle))
for obj in Objects:
- if(obj.getType() != 'Mesh'):
+ if obj.getType() != 'Mesh':
continue
self.file.write("\n" % obj.getName())
@@ -425,58 +1326,59 @@ class SVGVectorWriter(VectorWriter):
if doPrintEdges:
self._printEdges(mesh, showHiddenEdges)
-
+
self.file.write("\n")
self.file.write("\n")
-
- ##
+ ##
# Private Methods
#
-
+
def _calcCanvasCoord(self, v):
"""Convert vertex in scene coordinates to canvas coordinates.
"""
pt = Vector([0, 0, 0])
-
- mW = float(self.canvasSize[0])/2.0
- mH = float(self.canvasSize[1])/2.0
+
+ mW = float(self.canvasSize[0]) / 2.0
+ mH = float(self.canvasSize[1]) / 2.0
# rescale to canvas size
- pt[0] = v.co[0]*mW + mW
- pt[1] = v.co[1]*mH + mH
+ pt[0] = v.co[0] * mW + mW
+ pt[1] = v.co[1] * mH + mH
pt[2] = v.co[2]
-
+
# For now we want (0,0) in the top-left corner of the canvas.
# Mirror and translate along y
pt[1] *= -1
pt[1] += self.canvasSize[1]
-
+
return pt
def _printHeader(self):
"""Print SVG header."""
self.file.write("\n")
- self.file.write("\n")
- self.file.write("