Misc fixes and improvements
authorAntonio Ospite <ospite@studenti.unina.it>
Sat, 2 Sep 2006 23:38:42 +0000 (01:38 +0200)
committerAntonio Ospite <ospite@studenti.unina.it>
Thu, 24 Sep 2009 16:05:15 +0000 (18:05 +0200)
 * Support orthogonal camera
 * Fix perspective division
 * Use the path element to draw faces in SVG output
 * Do scene clipping before object transformations
 * Refactor mesh depth sorting

Signed-off-by: Antonio Ospite <ospite@studenti.unina.it>
vrm.py

diff --git a/vrm.py b/vrm.py
index 0d35415..f7382a7 100755 (executable)
--- a/vrm.py
+++ b/vrm.py
@@ -7,7 +7,7 @@ Tooltip: 'Vector Rendering Method script'
 """
 
 __author__ = "Antonio Ospite"
 """
 
 __author__ = "Antonio Ospite"
-__url__ = ["http://vrm.projects.blender.org"]
+__url__ = ["http://projects.blender.org/projects/vrm"]
 __version__ = "0.3"
 
 __bpydoc__ = """\
 __version__ = "0.3"
 
 __bpydoc__ = """\
@@ -42,23 +42,24 @@ __bpydoc__ = """\
 # ---------------------------------------------------------------------
 # 
 # Things TODO for a next release:
 # ---------------------------------------------------------------------
 # 
 # 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)
+#   - Use multiple lighting sources in color calculation,
+#     (this is part of the "shading refactor") and use light color!
+#   - FIX the issue with negative scales in object tranformations!
 #   - Use a better depth sorting algorithm
 #   - 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)
 #   - 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).
-#   - 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? 
 #   - 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.
+#     Think to a way to merge (adjacent) polygons that have the same color.
 #     Or a way to use paths for silhouettes and contours.
 #     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)
 #   - 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!
+#   - 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? (for now we use Flat Shading) (partially done).
+#   - Add Vector Writers other than SVG.
 #
 # ---------------------------------------------------------------------
 #
 #
 # ---------------------------------------------------------------------
 #
@@ -219,11 +220,10 @@ class Projector:
         fovy = fovy * 360.0/pi
         
         # What projection do we want?
         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) 
-        else:
+        if camera.type == 0:
             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
+        elif camera.type == 1:
+            mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) 
         
         # View transformation
         cam = Matrix(cameraObj.getInverseMatrix())
         
         # View transformation
         cam = Matrix(cameraObj.getInverseMatrix())
@@ -247,9 +247,11 @@ class Projector:
         # Note that we have to work on the vertex using homogeneous coordinates
         p = self.projectionMatrix * Vector(v).resize4D()
 
         # Note that we have to work on the vertex using homogeneous coordinates
         p = self.projectionMatrix * Vector(v).resize4D()
 
-        if p[3]>0:
+        # Perspective division
+        if p[3] != 0:
             p[0] = p[0]/p[3]
             p[1] = p[1]/p[3]
             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
 
         # restore the size
         p[3] = 1.0
@@ -546,9 +548,12 @@ class SVGVectorWriter(VectorWriter):
             if not face.sel:
                continue
 
             if not face.sel:
                continue
 
-            self.file.write("<polygon points=\"")
+            self.file.write("<path d=\"")
+
+            p = self._calcCanvasCoord(face.verts[0])
+            self.file.write("M %g,%g L " % (p[0], p[1]))
 
 
-            for v in face:
+            for v in face.verts[1:]:
                 p = self._calcCanvasCoord(v)
                 self.file.write("%g,%g " % (p[0], p[1]))
             
                 p = self._calcCanvasCoord(v)
                 self.file.write("%g,%g " % (p[0], p[1]))
             
@@ -581,7 +586,6 @@ class SVGVectorWriter(VectorWriter):
             self.file.write("\tstyle=\"fill:" + str_col + ";")
             self.file.write(opacity_string)
             if config.polygons['EXPANSION_TRICK']:
             self.file.write("\tstyle=\"fill:" + str_col + ";")
             self.file.write(opacity_string)
             if config.polygons['EXPANSION_TRICK']:
-                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-width:" + str(stroke_width) + ";\n")
                 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
             self.file.write("\"/>\n")
@@ -726,6 +730,8 @@ class Renderer:
 
             # Use some temporary workspace, a full copy of the scene
             inputScene = self._SCENE.copy(2)
 
             # Use some temporary workspace, a full copy of the scene
             inputScene = self._SCENE.copy(2)
+            # And Set our camera accordingly
+            self.cameraObj = inputScene.getCurrentCamera()
 
             try:
                 renderedScene = self.doRenderScene(inputScene)
 
             try:
                 renderedScene = self.doRenderScene(inputScene)
@@ -746,8 +752,8 @@ class Renderer:
             
             # clear the rendered scene
             self._SCENE.makeCurrent()
             
             # clear the rendered scene
             self._SCENE.makeCurrent()
-            Scene.unlink(renderedScene)
-            del renderedScene
+            #Scene.unlink(renderedScene)
+            #del renderedScene
 
         outputWriter.close()
         print "Done!"
 
         outputWriter.close()
         print "Done!"
@@ -763,9 +769,9 @@ class Renderer:
         
         # global processing of the scene
 
         
         # global processing of the scene
 
-        self._doConvertGeometricObjToMesh(workScene)
+        self._doSceneClipping(workScene)
 
 
-        #self._doSceneClipping(workScene)
+        self._doConvertGeometricObjToMesh(workScene)
 
         if config.output['JOIN_OBJECTS']:
             self._joinMeshObjectsInScene(workScene)
 
         if config.output['JOIN_OBJECTS']:
             self._joinMeshObjectsInScene(workScene)
@@ -777,7 +783,6 @@ class Renderer:
         Objects = workScene.getChildren()
         for obj in Objects:
 
         Objects = workScene.getChildren()
         for obj in Objects:
 
-            
             if obj.getType() != 'Mesh':
                 print "Only Mesh supported! - Skipping type:", obj.getType()
                 continue
             if obj.getType() != 'Mesh':
                 print "Only Mesh supported! - Skipping type:", obj.getType()
                 continue
@@ -786,17 +791,22 @@ class Renderer:
 
             mesh = obj.getData(mesh=1)
 
 
             mesh = obj.getData(mesh=1)
 
-            self._doModelToWorldCoordinates(mesh, obj.matrix)
+            self._doModelingTransformation(mesh, obj.matrix)
 
             self._doBackFaceCulling(mesh)
 
             self._doBackFaceCulling(mesh)
-            
-            self._doObjectDepthSorting(mesh)
-            
-            self._doColorAndLighting(mesh)
 
 
-            self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
+            self._doPerVertexLighting(mesh)
 
 
+            # Do "projection" now so we perform further processing
+            # in Normalized View Coordinates
             self._doProjection(mesh, self.proj)
             self._doProjection(mesh, self.proj)
+
+            self._doViewFrustumClipping(mesh)
+
+            self._doMeshDepthSorting(mesh)
+
+            self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
+
             
             # Update the object data, important! :)
             mesh.update()
             
             # Update the object data, important! :)
             mesh.update()
@@ -815,7 +825,7 @@ class Renderer:
         """
         return obj.matrix.translationPart()
 
         """
         return obj.matrix.translationPart()
 
-    def _cameraViewDirection(self):
+    def _cameraViewVector(self):
         """Get the View Direction form the camera matrix.
         """
         return Vector(self.cameraObj.matrix[2]).resize3D()
         """Get the View Direction form the camera matrix.
         """
         return Vector(self.cameraObj.matrix[2]).resize3D()
@@ -850,7 +860,7 @@ class Renderer:
         # View Vector in orthographics projections is the view Direction of
         # the camera
         if self.cameraObj.data.getType() == 1:
         # View Vector in orthographics projections is the view Direction of
         # the camera
         if self.cameraObj.data.getType() == 1:
-            view_vect = self._cameraViewDirection()
+            view_vect = self._cameraViewVector()
 
         # View vector in perspective projections can be considered as
         # the difference between the camera position and one point of
 
         # View vector in perspective projections can be considered as
         # the difference between the camera position and one point of
@@ -859,6 +869,7 @@ class Renderer:
             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
             view_vect = vv[1]
 
             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
         
@@ -870,40 +881,14 @@ class Renderer:
 
     # Scene methods
 
 
     # Scene methods
 
-    def _doConvertGeometricObjToMesh(self, scene):
-        """Convert all "geometric" objects to mesh ones.
-        """
-        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)
-
-
-            # XXX Workaround for Text and Curve which have some normals
-            # inverted when they are converted to Mesh, REMOVE that when
-            # blender will fix that!!
-            if old_obj.getType() in ['Curve', 'Text']:
-                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):
     def _doSceneClipping(self, scene):
-        """Clip objects against the View Frustum.
+        """Clip whole objects against the View Frustum.
 
         For now clip away only objects according to their center position.
         """
 
         cpos = self._getObjPosition(self.cameraObj)
 
         For now clip away only objects according to their center position.
         """
 
         cpos = self._getObjPosition(self.cameraObj)
-        view_vect = self._cameraViewDirection()
+        view_vect = self._cameraViewVector()
 
         near = self.cameraObj.data.clipStart
         far  = self.cameraObj.data.clipEnd
 
         near = self.cameraObj.data.clipStart
         far  = self.cameraObj.data.clipEnd
@@ -925,6 +910,33 @@ class Renderer:
             if (d < near) or (d > far) or (theta > fovy):
                 scene.unlink(o)
 
             if (d < near) or (d > far) or (theta > fovy):
                 scene.unlink(o)
 
+    def _doConvertGeometricObjToMesh(self, scene):
+        """Convert all "geometric" objects to mesh ones.
+        """
+        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)
+
+
+            # XXX Workaround for Text and Curve which have some normals
+            # inverted when they are converted to Mesh, REMOVE that when
+            # blender will fix that!!
+            if old_obj.getType() in ['Curve', 'Text']:
+                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 _doSceneDepthSorting(self, scene):
         """Sort objects in the scene.
 
     def _doSceneDepthSorting(self, scene):
         """Sort objects in the scene.
 
@@ -955,10 +967,6 @@ class Renderer:
         """Merge all the Mesh Objects in a scene into a single Mesh Object.
         """
 
         """Merge all the Mesh Objects in a scene into a single Mesh Object.
         """
 
-        if Blender.mode == 'background':
-            print "\nWARNING! Joining objects not supported in background mode!\n"
-            return
-
         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
 
         # FIXME: Object.join() do not work if the list contains 1 object
         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
 
         # FIXME: Object.join() do not work if the list contains 1 object
@@ -974,7 +982,7 @@ class Renderer:
         try:
             bigObj.join(oList)
         except RuntimeError:
         try:
             bigObj.join(oList)
         except RuntimeError:
-            print "Can't Join Objects"
+            print "\nCan't Join Objects\n"
             scene.unlink(bigObj)
             return
         except TypeError:
             scene.unlink(bigObj)
             return
         except TypeError:
@@ -1006,7 +1014,7 @@ class Renderer:
 
         return newObject
 
 
         return newObject
 
-    def _doModelToWorldCoordinates(self, mesh, matrix):
+    def _doModelingTransformation(self, mesh, matrix):
         """Transform object coordinates to world coordinates.
 
         This step is done simply applying to the object its tranformation
         """Transform object coordinates to world coordinates.
 
         This step is done simply applying to the object its tranformation
@@ -1019,56 +1027,6 @@ class Renderer:
 
         mesh.transform(matrix, True)
 
 
         mesh.transform(matrix, True)
 
-    def _doObjectDepthSorting(self, mesh):
-        """Sort faces in an object.
-
-        The faces in the object are sorted following the distance of the
-        vertices from the camera position.
-        """
-        if len(mesh.faces) == 0:
-            return
-
-        c = self._getObjPosition(self.cameraObj)
-
-        # hackish sorting of faces
-
-        # 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 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)))
-
-
-        # FIXME: using NMesh to sort faces. We should avoid that!
-        nmesh = NMesh.GetRaw(mesh.name)
-        nmesh.faces.sort(by_max_vert_dist)
-        nmesh.faces.reverse()
-
-        # Get visible faces
-        #vf = nmesh.faces
-        #while len(vf)>1:
-        #    p1 = vf[0]
-        #    insideList = 
-
-        
-        mesh.faces.delete(1, range(0, len(mesh.faces)))
-
-        for i,f in enumerate(nmesh.faces):
-            fv = [v.index for v in f.v] 
-            mesh.faces.extend(fv)
-            mesh.faces[i].mat = f.mat
-            mesh.faces[i].sel = f.sel
-
-
     def _doBackFaceCulling(self, mesh):
         """Simple Backface Culling routine.
         
     def _doBackFaceCulling(self, mesh):
         """Simple Backface Culling routine.
         
@@ -1088,7 +1046,7 @@ class Renderer:
             if self._isFaceVisible(f):
                 f.sel = 1
 
             if self._isFaceVisible(f):
                 f.sel = 1
 
-    def _doColorAndLighting(self, mesh):
+    def _doPerVertexLighting(self, mesh):
         """Apply an Illumination ans shading model to the object.
 
         The model used is the Phong one, it may be inefficient,
         """Apply an Illumination ans shading model to the object.
 
         The model used is the Phong one, it may be inefficient,
@@ -1110,7 +1068,7 @@ class Renderer:
         light = light_obj.data
 
         camPos = self._getObjPosition(self.cameraObj)
         light = light_obj.data
 
         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
         # technique. For an example see:
         # We do per-face color calculation (FLAT Shading), we can easily turn
         # to a per-vertex calculation if we want to implement some shading
         # technique. For an example see:
@@ -1150,14 +1108,14 @@ class Renderer:
             Ip = light.getEnergy()
             
             if config.polygons['SHADING'] == 'FLAT':
             Ip = light.getEnergy()
             
             if config.polygons['SHADING'] == 'FLAT':
-                Idiff = Ip * kd * (N*L)
+                Idiff = Ip * kd * max(0, (N*L))
             elif config.polygons['SHADING'] == 'TOON':
                 Idiff = Ip * kd * MeshUtils.toonShading(N*L)
 
             # Specular component
             ks = mat.getSpec() * Vector(mat.getSpecCol())
             ns = mat.getHardness()
             elif config.polygons['SHADING'] == 'TOON':
                 Idiff = Ip * kd * MeshUtils.toonShading(N*L)
 
             # Specular component
             ks = mat.getSpec() * Vector(mat.getSpecCol())
             ns = mat.getHardness()
-            Ispec = Ip * ks * pow((V*R), ns)
+            Ispec = Ip * ks * pow(max(0, (V*R)), ns)
 
             # Emissive component
             ki = Vector([mat.getEmit()]*3)
 
             # Emissive component
             ki = Vector([mat.getEmit()]*3)
@@ -1182,6 +1140,93 @@ class Renderer:
                 c.b = tmp_col[2]
                 c.a = tmp_col[3]
 
                 c.b = tmp_col[2]
                 c.a = tmp_col[3]
 
+    def _doProjection(self, mesh, projector):
+        """Apply Viewing and Projection tranformations.
+        """
+
+        for v in mesh.verts:
+            p = projector.doProjection(v.co)
+            v.co[0] = p[0]
+            v.co[1] = p[1]
+            v.co[2] = p[2]
+
+        # We could reeset Camera matrix, since now
+        # we are in Normalized Viewing Coordinates,
+        # but doung that would affect World Coordinate
+        # processing for other objects
+
+        #self.cameraObj.data.type = 1
+        #self.cameraObj.data.scale = 2.0
+        #m = Matrix().identity()
+        #self.cameraObj.setMatrix(m)
+
+    def _doViewFrustumClipping(self, mesh):
+        """Clip faces against the View Frustum.
+        """
+
+    def test_extensions(self, f1, f2):
+        for v1, v2 in [ (v1, v2) for v1 in f1 for v2 in f2 ]:
+            pass
+
+    def depth_sort(self, faces):
+        return
+            
+
+    def _doMeshDepthSorting(self, mesh):
+        """Sort faces in an object.
+
+        The faces in the object are sorted following the distance of the
+        vertices from the camera position.
+        """
+        if len(mesh.faces) == 0:
+            return
+
+        #c = self._getObjPosition(self.cameraObj)
+
+        # In NVC
+        c = [0, 0, 1]
+
+        # hackish sorting of faces
+
+        # 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 f2]),
+                    max([(Vector(v.co)-Vector(c)).length for v in f1])))
+        
+        # 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)))
+
+
+        # FIXME: using NMesh to sort faces. We should avoid that!
+        nmesh = NMesh.GetRaw(mesh.name)
+        nmesh.faces.sort(by_max_vert_dist)
+        #nmesh.faces.reverse()
+
+        # Depth sort tests
+
+        self.depth_sort(nmesh.faces)
+
+        
+        mesh.faces.delete(1, range(0, len(mesh.faces)))
+
+        for i,f in enumerate(nmesh.faces):
+            fv = [v.index for v in f.v] 
+            mesh.faces.extend(fv)
+            mesh.faces[i].mat = f.mat
+            mesh.faces[i].sel = f.sel
+            for i,c in enumerate(mesh.faces[i].col):
+                c.r = f.col[i].r
+                c.g = f.col[i].g
+                c.b = f.col[i].b
+                c.a = f.col[i].a
+
     def _doEdgesStyle(self, mesh, edgestyleSelect):
         """Process Mesh Edges accroding to a given selection style.
 
     def _doEdgesStyle(self, mesh, edgestyleSelect):
         """Process Mesh Edges accroding to a given selection style.
 
@@ -1204,17 +1249,6 @@ class Renderer:
             if edgestyleSelect(e, mesh):
                 e.sel = 1
                 
             if edgestyleSelect(e, mesh):
                 e.sel = 1
                 
-    def _doProjection(self, mesh, projector):
-        """Calculate the Projection for the object.
-        """
-        # TODO: maybe using the object.transform() can be faster?
-
-        for v in mesh.verts:
-            p = projector.doProjection(v.co)
-            v.co[0] = p[0]
-            v.co[1] = p[1]
-            v.co[2] = p[2]
-
 
 
 # ---------------------------------------------------------------------
 
 
 # ---------------------------------------------------------------------
@@ -1433,7 +1467,7 @@ class GUI:
 
         if evt:
             Draw.Redraw(1)
 
         if evt:
             Draw.Redraw(1)
-            GUI.conf_debug()
+            #GUI.conf_debug()
 
     def conf_debug():
         from pprint import pprint
 
     def conf_debug():
         from pprint import pprint