Handle non-mesh objects
authorAntonio Ospite <ospite@studenti.unina.it>
Sun, 4 Jun 2006 18:29:58 +0000 (20:29 +0200)
committerAntonio Ospite <ospite@studenti.unina.it>
Thu, 24 Sep 2009 15:19:51 +0000 (17:19 +0200)
 * 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 <ospite@studenti.unina.it>
vrm.py

diff --git a/vrm.py b/vrm.py
index 69099c4..312d8af 100755 (executable)
--- 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)
 #   - 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
 #   - 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
 
 
 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) 
         
         else:
             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
         
-
         # View transformation
         cam = Matrix(cameraObj.getInverseMatrix())
         cam.transpose() 
         # View transformation
         cam = Matrix(cameraObj.getInverseMatrix())
         cam.transpose() 
@@ -293,10 +293,10 @@ class SVGVectorWriter(VectorWriter):
     """A concrete class for writing SVG output.
     """
 
     """A concrete class for writing SVG output.
     """
 
-    def __init__(self, file):
+    def __init__(self, fileName):
         """Simply call the parent Contructor.
         """
         """Simply call the parent Contructor.
         """
-        VectorWriter.__init__(self, file)
+        VectorWriter.__init__(self, fileName)
 
 
     ##
 
 
     ##
@@ -314,6 +314,9 @@ class SVGVectorWriter(VectorWriter):
         """
         self._printFooter()
 
         """
         self._printFooter()
 
+        # remember to call the close method of the parent
+        VectorWriter.close(self)
+
         
     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
             showHiddenEdges=False):
         
     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]
             # 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:
             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,
 
             # 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
 
             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:
             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")
 
                 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()
 
         # 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()
 
         # 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:
         """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.
         """
             - 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)
 
         # 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
 
         self._doSceneDepthSorting(workScene)
         
         # Per object activities
-        Objects = workScene.getChildren()
 
 
+        Objects = workScene.getChildren()
         for obj in Objects:
             
         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()
                 continue
 
             print "Rendering: ", obj.getName()
@@ -684,19 +681,15 @@ class Renderer:
 
     # Utility methods
 
 
     # Utility methods
 
-    def _worldPosition(self, obj):
+    def _getObjPosition(self, obj):
         """Return the obj position in World coordinates.
         """
         return obj.matrix.translationPart()
 
         """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
 
 
     # Faces methods
@@ -722,25 +715,20 @@ class Renderer:
         """
 
         normal = Vector(face.no)
         """
 
         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:
         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
 
         # if d > 0 the face is visible from the camera
         d = view_vect * normal
@@ -753,11 +741,56 @@ class Renderer:
 
     # Scene methods
 
 
     # 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.
 
     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.
         """
 
         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)
         
         # 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.
         """
 
     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 = Object.New('Mesh', 'BigOne')
+        bigObj.link(mesh)
+
         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
-        print "Before join", oList
         bigObj.join(oList)
         bigObj.join(oList)
-        print "After join"
         scene.link(bigObj)
         for o in oList:
             scene.unlink(o)
 
         scene.link(bigObj)
         for o in oList:
             scene.unlink(o)
 
+        scene.update()
+
  
     # Per object methods
 
  
     # Per object methods
 
@@ -811,6 +845,11 @@ class Renderer:
         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
         newObject.link(me)
 
         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
         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.
         """
         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
 
         # 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])))
                 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):
         mesh.faces.reverse()
 
     def _doBackFaceCulling(self, mesh):
@@ -855,7 +897,8 @@ class Renderer:
         select the vertices belonging to visible faces.
         """
         
         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
         
         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 ??
 
         # 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:
         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 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.
 
     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]
         
         # 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
 
         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
         
         # 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
                 mat = materials[f.mat]
 
             # A new default material
-            if not mat:
+            if mat == None:
                 mat = Material.New('defMat')
             
             L = Vector(light_pos).normalize()
                 mat = Material.New('defMat')
             
             L = Vector(light_pos).normalize()
@@ -963,11 +1004,17 @@ class Renderer:
                 f.col.append(vcol)
 
     def _doEdgesStyle(self, mesh, style):
                 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
         """
         #print "\tTODO: _doEdgeStyle()"
         return
@@ -1020,8 +1067,8 @@ def vectorize_gui(filename):
 # Here the main
 if __name__ == "__main__":
     
 # 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:
 
     # with this trick we can run the script in batch mode
     try: