From c71b2b3970d0bdcd1df68026aa3315141b42ded0 Mon Sep 17 00:00:00 2001 From: Antonio Ospite Date: Sun, 4 Jun 2006 20:29:58 +0200 Subject: [PATCH] Handle non-mesh objects * Prepare for alpha component handling in output * Convert gometric objects (curves, text) to mesh before render * Make the isibility routine more pythonic * Add some basic View frustum clipping * Experiment with different face depth sort strategies Signed-off-by: Antonio Ospite --- vrm.py | 251 ++++++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 149 insertions(+), 102 deletions(-) diff --git a/vrm.py b/vrm.py index 69099c4..312d8af 100755 --- a/vrm.py +++ b/vrm.py @@ -47,7 +47,8 @@ __bpydoc__ = """\ # - Use a better depth sorting algorithm # - Review how selections are made (this script uses selection states of # primitives to represent visibility infos) -# - Implement Clipping and do handle object intersections +# - Implement clipping of primitives and do handle object intersections. +# (for now only clipping for whole objects is supported). # - Implement Edge Styles (silhouettes, contours, etc.) # - Implement Edge coloring # - Use multiple lighting sources in color calculation @@ -84,8 +85,8 @@ POLYGON_EXPANSION_TRICK = True RENDER_ANIMATION = False -# Do not work for now! -OPTIMIZE_FOR_SPACE = False +# Does not work in batch mode!! +#OPTIMIZE_FOR_SPACE = True # --------------------------------------------------------------------- @@ -129,7 +130,6 @@ class Projector: else: mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) - # View transformation cam = Matrix(cameraObj.getInverseMatrix()) cam.transpose() @@ -293,10 +293,10 @@ class SVGVectorWriter(VectorWriter): """A concrete class for writing SVG output. """ - def __init__(self, file): + def __init__(self, fileName): """Simply call the parent Contructor. """ - VectorWriter.__init__(self, file) + VectorWriter.__init__(self, fileName) ## @@ -314,6 +314,9 @@ class SVGVectorWriter(VectorWriter): """ self._printFooter() + # remember to call the close method of the parent + VectorWriter.close(self) + def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False): @@ -459,9 +462,9 @@ class SVGVectorWriter(VectorWriter): # TODO: the average of vetrex colors? if face.col: fcol = face.col[0] - color = [fcol.r, fcol.g, fcol.b] + color = [fcol.r, fcol.g, fcol.b, fcol.a] else: - color = [255, 255, 255] + color = [255, 255, 255, 255] # use the stroke property to alleviate the "adjacent edges" problem, # we simulate polygon expansion using borders, @@ -469,10 +472,13 @@ class SVGVectorWriter(VectorWriter): stroke_col = color stroke_width = 0.5 - self.file.write("\tstyle=\"fill:rgb("+str(color[0])+","+str(color[1])+","+str(color[2])+");") + # Convert the color to the #RRGGBB form + str_col = "#%02X%02X%02X" % (color[0], color[1], color[2]) + + self.file.write("\tstyle=\"fill:" + str_col + ";") if POLYGON_EXPANSION_TRICK: - self.file.write(" stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");") - self.file.write(" stroke-width:"+str(stroke_width)+";\n") + self.file.write(" stroke:" + str_col + ";") + self.file.write(" stroke-width:" + str(stroke_width) + ";\n") self.file.write(" stroke-linecap:round;stroke-linejoin:round") self.file.write("\"/>\n") @@ -548,6 +554,7 @@ class Renderer: # Render from the currently active camera self.cameraObj = self._SCENE.getCurrentCamera() + print dir(self._SCENE) # Get the list of lighting sources obj_lst = self._SCENE.getChildren() @@ -568,7 +575,7 @@ class Renderer: """Render picture or animation and write it out. The parameters are: - - a Vector writer object than will be used to output the result. + - a Vector writer object that will be used to output the result. - a flag to tell if we want to render an animation or only the current frame. """ @@ -623,35 +630,25 @@ class Renderer: # projection transformations proj = Projector(self.cameraObj, self.canvasRatio) + # global processing of the scene - # Convert geometric object types to mesh Objects - geometricObjTypes = ['Mesh', 'Surf', 'Curve'] # TODO: add the Text type - Objects = workScene.getChildren() - objList = [ o for o in Objects if o.getType() in geometricObjTypes ] - for obj in objList: - old_obj = obj - obj = self._convertToRawMeshObj(obj) - workScene.link(obj) - workScene.unlink(old_obj) - + self._doConvertGeometricObjToMesh(workScene) - # FIXME: does not work!!, Blender segfaults on joins - if OPTIMIZE_FOR_SPACE: - self._joinMeshObjectsInScene(workScene) + self._doSceneClipping(workScene) - - # global processing of the scene - self._doClipping() + # FIXME: does not work in batch mode! + #if OPTIMIZE_FOR_SPACE: + # self._joinMeshObjectsInScene(workScene) self._doSceneDepthSorting(workScene) # Per object activities - Objects = workScene.getChildren() + Objects = workScene.getChildren() for obj in Objects: - if obj.getType() not in geometricObjTypes: - print "Only geometric Objects supported! - Skipping type:", obj.getType() + if obj.getType() != 'Mesh': + print "Only Mesh supported! - Skipping type:", obj.getType() continue print "Rendering: ", obj.getName() @@ -684,19 +681,15 @@ class Renderer: # Utility methods - def _worldPosition(self, obj): + def _getObjPosition(self, obj): """Return the obj position in World coordinates. """ return obj.matrix.translationPart() - def _cameraWorldPosition(self): - """Return the camera position in World coordinates. - - This trick is needed when the camera follows a path and then - camera.loc does not correspond to the current real position of the - camera in the world. + def _cameraViewDirection(self): + """Get the View Direction form the camera matrix. """ - return self._worldPosition(self.cameraObj) + return Vector(self.cameraObj.matrix[2]).resize3D() # Faces methods @@ -722,25 +715,20 @@ class Renderer: """ normal = Vector(face.no) - c = self._cameraWorldPosition() - - # View vector in orthographics projections can be considered simply as the - # camera position - view_vect = Vector(c) - #if self.cameraObj.data.getType() == 1: - # view_vect = Vector(c) - - # View vector as in perspective projections - # it is the difference between the camera position and one point of - # the face, we choose the farthest point. - # TODO: make the code more pythonic :) + camPos = self._getObjPosition(self.cameraObj) + view_vect = None + + # View Vector in orthographics projections is the view Direction of + # the camera + if self.cameraObj.data.getType() == 1: + view_vect = self._cameraViewDirection() + + # View vector in perspective projections can be considered as + # the difference between the camera position and one point of + # the face, we choose the farthest point from the camera. if self.cameraObj.data.getType() == 0: - max_len = 0 - for vect in face: - vv = Vector(c) - Vector(vect.co) - if vv.length > max_len: - max_len = vv.length - view_vect = vv + vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] ) + view_vect = vv[1] # if d > 0 the face is visible from the camera d = view_vect * normal @@ -753,11 +741,56 @@ class Renderer: # Scene methods - def _doClipping(self): - """Clip object against the View Frustum. + def _doConvertGeometricObjToMesh(self, scene): + """Convert all "geometric" objects to mesh ones. """ - print "TODO: _doClipping()" - return + geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text'] + + Objects = scene.getChildren() + objList = [ o for o in Objects if o.getType() in geometricObjTypes ] + for obj in objList: + old_obj = obj + obj = self._convertToRawMeshObj(obj) + scene.link(obj) + scene.unlink(old_obj) + + # Mesh Cleanup + me = obj.getData(mesh=1) + for f in me.faces: f.sel = 1; + for v in me.verts: v.sel = 1; + me.remDoubles(0) + me.triangleToQuad() + me.recalcNormals() + me.update() + + def _doSceneClipping(self, scene): + """Clip objects against the View Frustum. + + For now clip away only objects according to their center position. + """ + + cpos = self._getObjPosition(self.cameraObj) + view_vect = self._cameraViewDirection() + + near = self.cameraObj.data.clipStart + far = self.cameraObj.data.clipEnd + + aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1]) + fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32)) + fovy = fovy * 360.0/pi + + Objects = scene.getChildren() + for o in Objects: + if o.getType() != 'Mesh': continue; + + obj_vect = Vector(cpos) - self._getObjPosition(o) + + d = obj_vect*view_vect + theta = AngleBetweenVecs(obj_vect, view_vect) + + # if the object is outside the view frustum, clip it away + if (d < near) or (d > far) or (theta > fovy): + scene.unlink(o) def _doSceneDepthSorting(self, scene): """Sort objects in the scene. @@ -765,40 +798,41 @@ class Renderer: The object sorting is done accordingly to the object centers. """ - c = self._cameraWorldPosition() + c = self._getObjPosition(self.cameraObj) - Objects = scene.getChildren() + by_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) + ) - #Objects.sort(lambda obj1, obj2: - # cmp((Vector(obj1.loc) - Vector(c)).length, - # (Vector(obj2.loc) - 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 - Objects.sort(lambda obj1, obj2: - cmp((self._worldPosition(obj1) - Vector(c)).length, - (self._worldPosition(obj2) - Vector(c)).length - ) - ) + Objects = scene.getChildren() + Objects.sort(by_center_pos) # update the scene for o in Objects: scene.unlink(o) scene.link(o) - def _joinMeshObjectsInScene(self, scene): """Merge all the Mesh Objects in a scene into a single Mesh Object. """ + mesh = Mesh.New() bigObj = Object.New('Mesh', 'BigOne') + bigObj.link(mesh) + oList = [o for o in scene.getChildren() if o.getType()=='Mesh'] - print "Before join", oList bigObj.join(oList) - print "After join" scene.link(bigObj) for o in oList: scene.unlink(o) + scene.update() + # Per object methods @@ -811,6 +845,11 @@ class Renderer: newObject = Object.New('Mesh', 'RawMesh_'+object.name) newObject.link(me) + # If the object has no materials set a default material + if not me.materials: + me.materials = [Material.New()] + #for f in me.faces: f.mat = 0 + newObject.setMatrix(object.getMatrix()) return newObject @@ -829,23 +868,26 @@ class Renderer: The faces in the object are sorted following the distance of the vertices from the camera position. """ - c = self._cameraWorldPosition() + c = self._getObjPosition(self.cameraObj) # hackish sorting of faces - mesh.faces.sort( - lambda f1, f2: - # Sort faces according to the min distance from the camera - #cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]), - # min([(Vector(v.co)-Vector(c)).length for v in f2]))) - # Sort faces according to the max distance from the camera + # Sort faces according to the max distance from the camera + by_max_vert_dist = (lambda f1, f2: cmp(max([(Vector(v.co)-Vector(c)).length for v in f1]), max([(Vector(v.co)-Vector(c)).length for v in f2]))) - - # Sort faces according to the avg distance from the camera - #cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1), - # sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2))) + + # Sort faces according to the min distance from the camera + by_min_vert_dist = (lambda f1, f2: + cmp(min([(Vector(v.co)-Vector(c)).length for v in f1]), + min([(Vector(v.co)-Vector(c)).length for v in f2]))) + + # Sort faces according to the avg distance from the camera + by_avg_vert_dist = (lambda f1, f2: + cmp(sum([(Vector(v.co)-Vector(c)).length for v in f1])/len(f1), + sum([(Vector(v.co)-Vector(c)).length for v in f2])/len(f2))) + mesh.faces.sort(by_max_vert_dist) mesh.faces.reverse() def _doBackFaceCulling(self, mesh): @@ -855,7 +897,8 @@ class Renderer: select the vertices belonging to visible faces. """ - # Select all vertices, so edges without faces can be displayed + # Select all vertices, so edges can be displayed even if there are no + # faces for v in mesh.verts: v.sel = 1 @@ -868,17 +911,15 @@ class Renderer: # Is this the correct way to propagate the face selection info to the # vertices belonging to a face ?? - # TODO: Using the Mesh class this should come for free. Right? + # TODO: Using the Mesh module this should come for free. Right? Mesh.Mode(Mesh.SelectModes['VERTEX']) for f in mesh.faces: if not f.sel: - for v in f: - v.sel = 0 + for v in f: v.sel = 0; for f in mesh.faces: if f.sel: - for v in f: - v.sel = 1 + for v in f: v.sel = 1; def _doColorAndLighting(self, mesh): """Apply an Illumination model to the object. @@ -898,10 +939,10 @@ class Renderer: # TODO: use multiple lighting sources light_obj = self.lights[0] - light_pos = self._worldPosition(light_obj) + light_pos = self._getObjPosition(light_obj) light = light_obj.data - camPos = self._cameraWorldPosition() + camPos = self._getObjPosition(self.cameraObj) # We do per-face color calculation (FLAT Shading), we can easily turn # to a per-vertex calculation if we want to implement some shading @@ -916,7 +957,7 @@ class Renderer: mat = materials[f.mat] # A new default material - if not mat: + if mat == None: mat = Material.New('defMat') L = Vector(light_pos).normalize() @@ -963,11 +1004,17 @@ class Renderer: f.col.append(vcol) def _doEdgesStyle(self, mesh, style): - """Process Mesh Edges. (For now copy the edge data, in next version it - can be a place where recognize silouhettes and/or contours). + """Process Mesh Edges. + + Examples of algorithms: + + Contours: + given an edge if its adjacent faces have the same normal (that is + they are complanar), than deselect it. - input: an edge list - return: a processed edge list + Silhouettes: + given an edge if one its adjacent faces is frontfacing and the + other is backfacing, than select it, else deselect. """ #print "\tTODO: _doEdgeStyle()" return @@ -1020,8 +1067,8 @@ def vectorize_gui(filename): # Here the main if __name__ == "__main__": - import os - outputfile = os.path.splitext(Blender.Get('filename'))[0]+".svg" + basename = Blender.sys.basename(Blender.Get('filename')) + outputfile = Blender.sys.splitext(basename)[0]+".svg" # with this trick we can run the script in batch mode try: -- 2.1.4