Edge styles (silhouettes, contours)
[vrm.git] / vrm.py
diff --git a/vrm.py b/vrm.py
index 8914045..b3aac25 100755 (executable)
--- a/vrm.py
+++ b/vrm.py
@@ -2,12 +2,12 @@
 """
 Name: 'VRM'
 Blender: 241
-Group: 'Export'
-Tooltip: 'Vector Rendering Method Export Script'
+Group: 'Render'
+Tooltip: 'Vector Rendering Method script'
 """
 
 __author__ = "Antonio Ospite"
-__url__ = ["blender"]
+__url__ = ["http://vrm.projects.blender.org"]
 __version__ = "0.3"
 
 __bpydoc__ = """\
@@ -43,19 +43,21 @@ __bpydoc__ = """\
 # 
 # Things TODO for a next release:
 #   - Switch to the Mesh structure, should be considerably faster
-#    (partially done, but cannot sort faces, yet)
+#    (partially done, but with Mesh we cannot sort faces, yet)
 #   - 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.)
+#   - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
 #   - Implement Edge coloring
 #   - Use multiple lighting sources in color calculation
-#   - Implement Shading Styles?
-#   - Use another representation for the 2D projection
+#   - 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.
-#   - Add other Vector Writers.
+#     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?
 #
 # ---------------------------------------------------------------------
 #
@@ -76,17 +78,81 @@ from math import *
 
 # Some global settings
 PRINT_POLYGONS     = True
-PRINT_EDGES        = False
-SHOW_HIDDEN_EDGES  = False
+POLYGON_EXPANSION_TRICK = True
 
+PRINT_EDGES        = True
+SHOW_HIDDEN_EDGES  = False
+#EDGE_STYLE = 'normal'
+EDGE_STYLE = 'silhouette'
 EDGES_WIDTH = 0.5
 
-POLYGON_EXPANSION_TRICK = True
-
 RENDER_ANIMATION = False
 
-# Does not work in batch mode!!
-#OPTIMIZE_FOR_SPACE = True
+OPTIMIZE_FOR_SPACE = True
+
+OUTPUT_FORMAT = 'SVG'
+
+
+# ---------------------------------------------------------------------
+#
+## Utility Mesh class
+#
+# ---------------------------------------------------------------------
+class MeshUtils:
+    def __init__(self):
+        return
+
+    def getEdgeAdjacentFaces(self, edge, mesh):
+        """Get the faces adjacent to a given edge.
+
+        There can be 0, 1 or more (usually 2) faces adjacent to an edge.
+        """
+        adjface_list = []
+
+        for f in mesh.faces:
+            if (edge.v1 in f.v) and (edge.v2 in f.v):
+                adjface_list.append(f)
+
+        return adjface_list
+
+    def isVisibleEdge(self, e, mesh):
+        """Normal edge selection rule.
+
+        An edge is visible if _any_ 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 = self.getEdgeAdjacentFaces(e, mesh)
+
+        if len(adjacent_faces) == 0:
+            return True
+
+        selected_faces = [f for f in adjacent_faces if f.sel]
+
+        if len(selected_faces) != 0:
+            return True
+        else:
+            return False
+
+    def isSilhouetteEdge(self, e, mesh):
+        """Silhuette selection rule.
+
+        An edge is a silhuette edge if it is shared by two faces with
+        different selection status or if it is a boundary edge of a selected
+        face.
+        """
+
+        adjacent_faces = self.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)
+            ):
+            return True
+        else:
+            return False
+
 
 
 # ---------------------------------------------------------------------
@@ -214,9 +280,10 @@ class Projector:
         return m
 
 
+
 # ---------------------------------------------------------------------
 #
-## 2DObject representation class
+## 2D Object representation class
 #
 # ---------------------------------------------------------------------
 
@@ -446,7 +513,7 @@ class SVGVectorWriter(VectorWriter):
 
         for face in mesh.faces:
             if not face.sel:
-                continue
+               continue
 
             self.file.write("<polygon points=\"")
 
@@ -475,7 +542,14 @@ class SVGVectorWriter(VectorWriter):
             # Convert the color to the #RRGGBB form
             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
 
+            # Handle transparent polygons
+            opacity_string = ""
+            if color[3] != 255:
+                opacity = float(color[3])/255.0
+                opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
+
             self.file.write("\tstyle=\"fill:" + str_col + ";")
+            self.file.write(opacity_string)
             if POLYGON_EXPANSION_TRICK:
                 self.file.write(" stroke:" + str_col + ";")
                 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
@@ -497,8 +571,9 @@ class SVGVectorWriter(VectorWriter):
             
             hidden_stroke_style = ""
             
-            # Consider an edge selected if both vertices are selected
-            if e.v1.sel == 0 or e.v2.sel == 0:
+            # We consider an edge visible if _both_ its vertices are selected,
+            # hence an edge is hidden if _any_ of its vertices is deselected.
+            if e.sel == 0:
                 if showHiddenEdges == False:
                     continue
                 else:
@@ -635,9 +710,18 @@ class Renderer:
 
         self._doSceneClipping(workScene)
 
-        # FIXME: does not work in batch mode!
-        #if OPTIMIZE_FOR_SPACE:
-        #    self._joinMeshObjectsInScene(workScene)
+
+        # XXX: Joining objects does not work in batch mode!!
+        # Do not touch the following if, please :)
+
+        global OPTIMIZE_FOR_SPACE
+        if Blender.mode == 'background':
+            print "\nWARNING! Joining objects not supported in background mode!\n"
+            OPTIMIZE_FOR_SPACE = False
+
+        if OPTIMIZE_FOR_SPACE:
+            self._joinMeshObjectsInScene(workScene)
+
 
         self._doSceneDepthSorting(workScene)
         
@@ -652,19 +736,22 @@ class Renderer:
 
             print "Rendering: ", obj.getName()
 
-            mesh = obj.data
+            mesh = obj.getData()
 
             self._doModelToWorldCoordinates(mesh, obj.matrix)
 
             self._doObjectDepthSorting(mesh)
             
+            # We use both Mesh and NMesh because for depth sorting we change
+            # face order and Mesh class don't let us to do that.
+            mesh.update()
+            mesh = obj.getData(mesh=1)
+            
             self._doBackFaceCulling(mesh)
             
             self._doColorAndLighting(mesh)
 
-            # TODO: 'style' can be a function that determine
-            # if an edge should be showed?
-            self._doEdgesStyle(mesh, style=None)
+            self._doEdgesStyle(mesh, edgeSelectionStyles[EDGE_STYLE])
 
             self._doProjection(mesh, proj)
             
@@ -916,14 +1003,14 @@ class Renderer:
         # Is this the correct way to propagate the face selection info to the
         # vertices belonging to a face ??
         # 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;
+        #Mesh.Mode(Mesh.SelectModes['VERTEX'])
+        #for f in mesh.faces:
+        #    if not f.sel:
+        #        for v in f: v.sel = 0;
 
-        for f in mesh.faces:
-            if f.sel:
-                for v in f: v.sel = 1;
+        #for f in mesh.faces:
+        #    if f.sel:
+        #        for v in f: v.sel = 1;
 
     def _doColorAndLighting(self, mesh):
         """Apply an Illumination model to the object.
@@ -935,9 +1022,9 @@ class Renderer:
 
         # If the mesh has vertex colors already, use them,
         # otherwise turn them on and do some calculations
-        if mesh.hasVertexColours():
+        if mesh.vertexColors:
             return
-        mesh.hasVertexColours(True)
+        mesh.vertexColors = 1
 
         materials = mesh.materials
         
@@ -997,18 +1084,23 @@ class Renderer:
 
             I = ki + Iamb + Idiff + Ispec
 
+            # Set Alpha component
+            I = list(I)
+            I.append(mat.getAlpha())
+
             # Clamp I values between 0 and 1
             I = [ min(c, 1) for c in I]
             I = [ max(0, c) for c in I]
             tmp_col = [ int(c * 255.0) for c in I]
 
-            vcol = NMesh.Col(tmp_col[0], tmp_col[1], tmp_col[2], 255)
-            f.col = []
-            for v in f.v:
-                f.col.append(vcol)
+            for c in f.col:
+                c.r = tmp_col[0]
+                c.g = tmp_col[1]
+                c.b = tmp_col[2]
+                c.a = tmp_col[3]
 
-    def _doEdgesStyle(self, mesh, style):
-        """Process Mesh Edges.
+    def _doEdgesStyle(self, mesh, edgestyleSelect):
+        """Process Mesh Edges accroding to a given selection style.
 
         Examples of algorithms:
 
@@ -1020,9 +1112,16 @@ class Renderer:
             given an edge if one its adjacent faces is frontfacing and the
             other is backfacing, than select it, else deselect.
         """
-        #print "\tTODO: _doEdgeStyle()"
-        return
 
+        Mesh.Mode(Mesh.SelectModes['EDGE'])
+
+        for e in mesh.edges:
+
+            if edgestyleSelect(e, mesh):
+                e.sel = 1
+            else:
+                e.sel = 0
+                
     def _doProjection(self, mesh, projector):
         """Calculate the Projection for the object.
         """
@@ -1042,6 +1141,20 @@ class Renderer:
 #
 # ---------------------------------------------------------------------
 
+# A dictionary to collect all the different edge styles and their edge
+# selection criteria
+edgeSelectionStyles = {
+        'normal': MeshUtils().isVisibleEdge,
+        'silhouette': MeshUtils().isSilhouetteEdge
+        }
+
+# A dictionary to collect the supported output formats
+outputWriters = {
+        'SVG': SVGVectorWriter,
+        }
+
+
+# A wrapper function for the vectorizing process
 def vectorize(filename):
     """The vectorizing process is as follows:
      
@@ -1052,21 +1165,13 @@ def vectorize(filename):
     editmode = Window.EditMode()
     if editmode: Window.EditMode(0)
 
-    writer = SVGVectorWriter(filename)
+    writer = outputWriters[OUTPUT_FORMAT](filename)
     
     renderer = Renderer()
     renderer.doRendering(writer, RENDER_ANIMATION)
 
     if editmode: Window.EditMode(1) 
 
-def vectorize_gui(filename):
-    """Draw the gui.
-
-    I would like to keep that simple, really.
-    """
-    Blender.Window.FileSelector (vectorize, 'Save SVG', filename)
-    Blender.Redraw()
-
 
 # Here the main
 if __name__ == "__main__":
@@ -1074,8 +1179,9 @@ if __name__ == "__main__":
     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:
-        vectorize_gui(outputfile)
-    except:
+    if Blender.mode == 'background':
         vectorize(outputfile)
+    else:
+        label = "Save %s" % OUTPUT_FORMAT
+        Blender.Window.FileSelector(vectorize, label, outputfile)
+        Blender.Redraw()