X-Git-Url: https://git.ao2.it/vrm.git/blobdiff_plain/2a59bfdb7dfdc76e8a5f25fa74971aae6627e643..ae133d0af9d635758bb88c6fc6383d42b207d02f:/vrm.py?ds=inline diff --git a/vrm.py b/vrm.py index b121765..84e4588 100755 --- a/vrm.py +++ b/vrm.py @@ -44,8 +44,6 @@ __bpydoc__ = """\ # Things TODO for a next release: # - FIX the issue with negative scales in object tranformations! # - Use a better depth sorting algorithm -# - Implement clipping of primitives and do handle object intersections. -# (for now only clipping away whole objects is supported). # - Review how selections are made (this script uses selection states of # primitives to represent visibility infos) # - Use a data structure other than Mesh to represent the 2D image? @@ -75,9 +73,18 @@ __bpydoc__ = """\ # * The SVG output is now SVG 1.0 valid. # Checked with: http://jiggles.w3.org/svgvalidator/ValidatorURI.html # * Progress indicator during HSR. -# * Initial SWF output support +# * Initial SWF output support (using ming) # * Fixed a bug in the animation code, now the projection matrix is # recalculated at each frame! +# * PDF output (using reportlab) +# * Fixed another problem in the animation code the current frame was off +# by one in the case of camera movement. +# * Use fps as specified in blender when VectorWriter handles animation +# * Remove the real file opening in the abstract VectorWriter +# * View frustum clipping +# * Scene clipping done using bounding box instead of object center +# * Fix camera type selection for blender>2.43 (Thanks to Thomas Lachmann) +# * Compatibility with python 2.3 # # --------------------------------------------------------------------- @@ -87,6 +94,13 @@ from Blender.Mathutils import * from math import * import sys, time +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 @@ -99,9 +113,8 @@ progress = None class config: polygons = dict() polygons['SHOW'] = True - polygons['SHADING'] = 'FLAT' - #polygons['HSR'] = 'PAINTER' # 'PAINTER' or 'NEWELL' - polygons['HSR'] = 'NEWELL' + polygons['SHADING'] = 'FLAT' # FLAT or TOON + polygons['HSR'] = 'NEWELL' # PAINTER or NEWELL # Hidden to the user for now polygons['EXPANSION_TRICK'] = True @@ -110,7 +123,7 @@ class config: edges = dict() edges['SHOW'] = False edges['SHOW_HIDDEN'] = False - edges['STYLE'] = 'MESH' # or SILHOUETTE + edges['STYLE'] = 'MESH' # MESH or SILHOUETTE edges['WIDTH'] = 2 edges['COLOR'] = [0, 0, 0] @@ -119,9 +132,6 @@ class config: output['ANIMATION'] = False output['JOIN_OBJECTS'] = True - #output['FORMAT'] = 'SWF' - #output['ANIMATION'] = True - # Utility functions print_debug = False @@ -320,6 +330,34 @@ class HSR: 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]. @@ -336,7 +374,8 @@ class HSR: # If a point of f1 in inside f2, there is an overlap! v1 = f1.v[i] - if HSR.isVertInside(f2, v1): + #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 @@ -562,7 +601,7 @@ class HSR: makeFaces = staticmethod(makeFaces) - def splitOn(Q, P): + 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 """ @@ -653,23 +692,31 @@ class HSR: negVertList.append(V1) - # uniq - posVertList = [ u for u in posVertList if u not in locals()['_[1]'] ] - negVertList = [ u for u in negVertList if u not in locals()['_[1]'] ] + # 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 verteices??" + # 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 verteices??" + # print "Problem, we created a face with less that 3 vertices??" # negVertList = [] if len(posVertList) < 3 or len(negVertList) < 3: - print "RETURN NONE, SURE???" + #print "RETURN NONE, SURE???" return None + if not return_positive_faces: + posVertList = [] + if not return_negative_faces: + negVertList = [] newfaces = HSR.addNewFaces(posVertList, negVertList) @@ -853,13 +900,22 @@ class Projector: fovy = atan(0.5/aspect/(camera.lens/32)) fovy = fovy * 360.0/pi - + + + if Blender.Get('version') < 243: + camPersp = 0 + camOrtho = 1 + else: + camPersp = 'persp' + camOrtho = 'ortho' + # What projection do we want? - if camera.type == 0: + if camera.type == camPersp: mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) - elif camera.type == 1: - mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) + elif camera.type == camOrtho: + mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) + # View transformation cam = Matrix(cameraObj.getInverseMatrix()) cam.transpose() @@ -1153,11 +1209,12 @@ class VectorWriter: """Set the output file name and other properties""" self.outputFileName = fileName - self.file = None context = Scene.GetCurrent().getRenderingContext() self.canvasSize = ( context.imageSizeX(), context.imageSizeY() ) + self.fps = context.fps + self.startFrame = 1 self.endFrame = 1 self.animation = False @@ -1173,14 +1230,11 @@ class VectorWriter: self.endFrame = endFrame self.animation = True - self.file = open(self.outputFileName, "w") print "Outputting to: ", self.outputFileName return def close(self): - if self.file: - self.file.close() return def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False, @@ -1201,6 +1255,8 @@ class SVGVectorWriter(VectorWriter): """ VectorWriter.__init__(self, fileName) + self.file = None + ## # Public Methods @@ -1210,6 +1266,9 @@ class SVGVectorWriter(VectorWriter): """Do some initialization operations. """ VectorWriter.open(self, startFrame, endFrame) + + self.file = open(self.outputFileName, "w") + self._printHeader() def close(self): @@ -1217,7 +1276,10 @@ 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) @@ -1298,15 +1360,17 @@ class SVGVectorWriter(VectorWriter): self.canvasSize) if self.animation: + delay = 1000/self.fps self.file.write("""\n\n - \n""" % (self.startFrame, self.endFrame, self.startFrame) ) + \n""") def _printFooter(self): """Print the SVG footer.""" @@ -1466,10 +1530,10 @@ class SWFVectorWriter(VectorWriter): VectorWriter.open(self, startFrame, endFrame) self.movie = SWFMovie() self.movie.setDimension(self.canvasSize[0], self.canvasSize[1]) - # set fps - self.movie.setRate(25) - numframes = endFrame - startFrame + 1 - self.movie.setFrames(numframes) + if self.animation: + self.movie.setRate(self.fps) + numframes = endFrame - startFrame + 1 + self.movie.setFrames(numframes) def close(self): """Do some finalization operation. @@ -1564,32 +1628,17 @@ class SWFVectorWriter(VectorWriter): p0 = self._calcCanvasCoord(face.verts[0]) s.movePenTo(p0[0], p0[1]) - for v in face.verts[1:]: p = self._calcCanvasCoord(v) s.drawLineTo(p[0], p[1]) # Closing the shape s.drawLineTo(p0[0], p0[1]) + s.end() sprite.add(s) - """ - # use the stroke property to alleviate the "adjacent edges" problem, - # we simulate polygon expansion using borders, - # see http://www.antigrain.com/svg/index.html for more info - stroke_width = 1.0 - - # EXPANSION TRICK is not that useful where there is transparency - if config.polygons['EXPANSION_TRICK'] and color[3] == 255: - # str_col = "#000000" # For debug - self.file.write(" stroke:%s;\n" % str_col) - self.file.write(" stroke-width:" + str(stroke_width) + ";\n") - self.file.write(" stroke-linecap:round;stroke-linejoin:round") - - """ - def _printEdges(self, mesh, sprite, showHiddenEdges=False): """Print the wireframe using mesh edges. """ @@ -1601,7 +1650,7 @@ class SWFVectorWriter(VectorWriter): for e in mesh.edges: - #Next, we set the line width and color for our shape. + # Next, we set the line width and color for our shape. s.setLine(stroke_width, stroke_col[0], stroke_col[1], stroke_col[2], 255) @@ -1617,17 +1666,169 @@ class SWFVectorWriter(VectorWriter): p1 = self._calcCanvasCoord(e.v1) p2 = self._calcCanvasCoord(e.v2) - # FIXME: this is just a qorkaround, remove that after the - # implementation of propoer Viewport clipping - if abs(p1[0]) < 3000 and abs(p2[0]) < 3000 and abs(p1[1]) < 3000 and abs(p1[2]) < 3000: - s.movePenTo(p1[0], p1[1]) - s.drawLineTo(p2[0], p2[1]) - + s.movePenTo(p1[0], p1[1]) + s.drawLineTo(p2[0], p2[1]) s.end() sprite.add(s) +## PDF Writer + +try: + from reportlab.pdfgen import canvas + PDFSupported = True +except: + PDFSupported = False + +class PDFVectorWriter(VectorWriter): + """A concrete class for writing PDF output. + """ + + def __init__(self, fileName): + """Simply call the parent Contructor. + """ + VectorWriter.__init__(self, fileName) + + self.canvas = None + + + ## + # Public Methods + # + + def open(self, startFrame=1, endFrame=1): + """Do some initialization operations. + """ + VectorWriter.open(self, startFrame, endFrame) + size = (self.canvasSize[0], self.canvasSize[1]) + self.canvas = canvas.Canvas(self.outputFileName, pagesize=size, bottomup=0) + + def close(self): + """Do some finalization operation. + """ + self.canvas.save() + + # remember to call the close method of the parent + VectorWriter.close(self) + + def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False, + showHiddenEdges=False): + """Convert the scene representation to SVG. + """ + context = scene.getRenderingContext() + framenumber = context.currentFrame() + + Objects = scene.getChildren() + + for obj in Objects: + + if(obj.getType() != 'Mesh'): + continue + + mesh = obj.getData(mesh=1) + + if doPrintPolygons: + self._printPolygons(mesh) + + if doPrintEdges: + self._printEdges(mesh, showHiddenEdges) + + self.canvas.showPage() + + ## + # 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 + + # rescale to canvas size + 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 _printPolygons(self, mesh): + """Print the selected (visible) polygons. + """ + + if len(mesh.faces) == 0: + return + + for face in mesh.faces: + if not face.sel: + continue + + if face.col: + fcol = face.col[0] + color = [fcol.r/255.0, fcol.g/255.0, fcol.b/255.0, + fcol.a/255.0] + else: + color = [1, 1, 1, 1] + + self.canvas.setFillColorRGB(color[0], color[1], color[2]) + # For debug + self.canvas.setStrokeColorRGB(0, 0, 0) + + path = self.canvas.beginPath() + + # The starting point of the path + p0 = self._calcCanvasCoord(face.verts[0]) + path.moveTo(p0[0], p0[1]) + + for v in face.verts[1:]: + p = self._calcCanvasCoord(v) + path.lineTo(p[0], p[1]) + + # Closing the shape + path.close() + + self.canvas.drawPath(path, stroke=0, fill=1) + + def _printEdges(self, mesh, showHiddenEdges=False): + """Print the wireframe using mesh edges. + """ + + stroke_width = config.edges['WIDTH'] + stroke_col = config.edges['COLOR'] + + self.canvas.setLineCap(1) + self.canvas.setLineJoin(1) + self.canvas.setLineWidth(stroke_width) + self.canvas.setStrokeColorRGB(stroke_col[0]/255.0, stroke_col[1]/255.0, + stroke_col[2]/255) + + for e in mesh.edges: + + self.canvas.setLineWidth(stroke_width) + + if e.sel == 0: + if showHiddenEdges == False: + continue + else: + # PDF does not support dashed lines natively, so -for now- + # draw hidden lines thinner + self.canvas.setLineWidth(stroke_width/2.0) + + p1 = self._calcCanvasCoord(e.v1) + p2 = self._calcCanvasCoord(e.v2) + + self.canvas.line(p1[0], p1[1], p2[0], p2[1]) + + # --------------------------------------------------------------------- # @@ -1650,6 +1851,8 @@ outputWriters = dict() outputWriters['SVG'] = SVGVectorWriter if SWFSupported: outputWriters['SWF'] = SWFVectorWriter +if PDFSupported: + outputWriters['PDF'] = PDFVectorWriter class Renderer: @@ -1680,7 +1883,7 @@ class Renderer: ) # Render from the currently active camera - self.cameraObj = self._SCENE.getCurrentCamera() + #self.cameraObj = self._SCENE.getCurrentCamera() # Get the list of lighting sources obj_lst = self._SCENE.getChildren() @@ -1723,15 +1926,22 @@ class Renderer: outputWriter.open(startFrame, endFrame) # Do the rendering process frame by frame - print "Start Rendering of %d frames" % (endFrame-startFrame) + print "Start Rendering of %d frames" % (endFrame-startFrame+1) for f in xrange(startFrame, endFrame+1): print "\n\nFrame: %d" % f - context.currentFrame(f) + + # FIXME To get the correct camera position we have to use +1 here. + # Is there a bug somewhere in the Scene module? + context.currentFrame(f+1) + self.cameraObj = self._SCENE.getCurrentCamera() # Use some temporary workspace, a full copy of the scene inputScene = self._SCENE.copy(2) - # And Set our camera accordingly - self.cameraObj = inputScene.getCurrentCamera() + + # To get the objects at this frame remove the +1 ... + ctx = inputScene.getRenderingContext() + ctx.currentFrame(f) + # Get a projector for this camera. # NOTE: the projector wants object in world coordinates, @@ -1905,7 +2115,7 @@ class Renderer: For now clip away only objects according to their center position. """ - cpos = self._getObjPosition(self.cameraObj) + cam_pos = self._getObjPosition(self.cameraObj) view_vect = self._cameraViewVector() near = self.cameraObj.data.clipStart @@ -1919,7 +2129,8 @@ class Renderer: for o in Objects: if o.getType() != 'Mesh': continue; - obj_vect = Vector(cpos) - self._getObjPosition(o) + """ + obj_vect = Vector(cam_pos) - self._getObjPosition(o) d = obj_vect*view_vect theta = AngleBetweenVecs(obj_vect, view_vect) @@ -1927,6 +2138,30 @@ class Renderer: # if the object is outside the view frustum, clip it away if (d < near) or (d > far) or (theta > fovy): scene.unlink(o) + """ + + # Use the object bounding box + # (whose points are already in WorldSpace Coordinate) + + bb = o.getBoundBox() + + points_outside = 0 + for p in bb: + p_vect = Vector(cam_pos) - Vector(p) + + d = p_vect * view_vect + theta = AngleBetweenVecs(p_vect, view_vect) + + # Is this point outside the view frustum? + if (d < near) or (d > far) or (theta > fovy): + points_outside += 1 + + # If the bb is all outside the view frustum we clip the whole + # object away + if points_outside == len(bb): + scene.unlink(o) + + def _doConvertGeometricObjsToMesh(self, scene): """Convert all "geometric" objects to mesh ones. @@ -1964,18 +2199,25 @@ class Renderer: c = self._getObjPosition(self.cameraObj) - by_center_pos = (lambda o1, o2: + by_obj_center_pos = (lambda o1, o2: (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and cmp((self._getObjPosition(o1) - Vector(c)).length, (self._getObjPosition(o2) - Vector(c)).length) ) - # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb, - # then ob1 goes farther than obj2, useful when obj2 has holes - by_bbox = None + # Implement sorting by bounding box, the object with the bb + # nearest to the camera should be drawn as last. + by_nearest_bbox_point = (lambda o1, o2: + (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and + cmp( min( [(Vector(p) - Vector(c)).length for p in o1.getBoundBox()] ), + min( [(Vector(p) - Vector(c)).length for p in o2.getBoundBox()] ) + ) + ) + Objects = scene.getChildren() - Objects.sort(by_center_pos) + #Objects.sort(by_obj_center_pos) + Objects.sort(by_nearest_bbox_point) # update the scene for o in Objects: @@ -2210,6 +2452,71 @@ class Renderer: """Clip faces against the View Frustum. """ + # The Canonical View Volume, 8 vertices, and 6 faces, + # We consider its face normals pointing outside + + v1 = NMesh.Vert(1, 1, -1) + v2 = NMesh.Vert(1, -1, -1) + v3 = NMesh.Vert(-1, -1, -1) + v4 = NMesh.Vert(-1, 1, -1) + v5 = NMesh.Vert(1, 1, 1) + v6 = NMesh.Vert(1, -1, 1) + v7 = NMesh.Vert(-1, -1, 1) + v8 = NMesh.Vert(-1, 1, 1) + + cvv = [] + f1 = NMesh.Face([v1, v4, v3, v2]) + cvv.append(f1) + f2 = NMesh.Face([v5, v6, v7, v8]) + cvv.append(f2) + f3 = NMesh.Face([v1, v2, v6, v5]) + cvv.append(f3) + f4 = NMesh.Face([v2, v3, v7, v6]) + cvv.append(f4) + f5 = NMesh.Face([v3, v4, v8, v7]) + cvv.append(f5) + f6 = NMesh.Face([v4, v1, v5, v8]) + cvv.append(f6) + + nmesh = NMesh.GetRaw(mesh.name) + clippedfaces = nmesh.faces[:] + facelist = clippedfaces[:] + + for clipface in cvv: + + clippedfaces = [] + + for f in facelist: + + newfaces = HSR.splitOn(clipface, f, return_positive_faces=False) + + if not newfaces: + # Check if the face is all outside the view frustum + # TODO: Do this test before, it is more efficient + points_outside = 0 + for v in f: + if abs(v[0]) > 1-EPS or abs(v[1]) > 1-EPS or abs(v[2]) > 1-EPS: + points_outside += 1 + + if points_outside != len(f): + clippedfaces.append(f) + else: + for nf in newfaces: + for v in nf: + nmesh.verts.append(v) + + nf.mat = f.mat + nf.sel = f.sel + nf.col = [f.col[0]] * len(nf.v) + + clippedfaces.append(nf) + facelist = clippedfaces[:] + + + nmesh.faces = facelist + nmesh.update() + + # HSR routines def __simpleDepthSort(self, mesh): """Sort faces by the furthest vertex. @@ -2517,6 +2824,7 @@ class Renderer: if edgestyleSelect(e, mesh): e.sel = 1 """ + # # --------------------------------------------------------------------- @@ -2695,6 +3003,9 @@ class GUI: elif evt == GUI.evtOutFormatMenu: i = GUI.outFormatMenu.val - 1 config.output['FORMAT']= outputWriters.keys()[i] + # Set the new output file + global outputfile + outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower() elif evt == GUI.evtAnimToggle: config.output['ANIMATION'] = bool(GUI.animToggle.val)