Process only object that are on visible layers
[vrm.git] / vrm.py
diff --git a/vrm.py b/vrm.py
index 0338065..6a69a66 100755 (executable)
--- 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? 
@@ -80,9 +78,14 @@ __bpydoc__ = """\
 #       recalculated at each frame!
 #     * PDF output (using reportlab)
 #     * Fixed another problem in the animation code the current frame was off
-#       by one
+#       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
+#     * Process only object that are on visible layers.
 #
 # ---------------------------------------------------------------------
 
@@ -92,6 +95,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
 
@@ -683,9 +693,13 @@ 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
@@ -887,13 +901,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() 
@@ -1644,12 +1667,8 @@ 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)
@@ -1808,10 +1827,7 @@ class PDFVectorWriter(VectorWriter):
             p1 = self._calcCanvasCoord(e.v1)
             p2 = self._calcCanvasCoord(e.v2)
 
-            # FIXME: this is just a workaround, 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:
-                self.canvas.line(p1[0], p1[1], p2[0], p2[1])
+            self.canvas.line(p1[0], p1[1], p2[0], p2[1])
 
 
 
@@ -1870,18 +1886,7 @@ class Renderer:
         # Render from the currently active camera 
         #self.cameraObj = self._SCENE.getCurrentCamera()
 
-        # Get the list of lighting sources
-        obj_lst = self._SCENE.getChildren()
-        self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
-
-        # When there are no lights we use a default lighting source
-        # that have the same position of the camera
-        if len(self.lights) == 0:
-            l = Lamp.New('Lamp')
-            lobj = Object.New('Lamp')
-            lobj.loc = self.cameraObj.loc
-            lobj.link(l) 
-            self.lights.append(lobj)
+        self.lights = []
 
 
     ##
@@ -1970,6 +1975,10 @@ class Renderer:
         
         # global processing of the scene
 
+        self._filterHiddenObjects(workScene)
+
+        self._buildLightSetup(workScene)
+
         self._doSceneClipping(workScene)
 
         self._doConvertGeometricObjsToMesh(workScene)
@@ -1982,6 +1991,7 @@ class Renderer:
         # Per object activities
 
         Objects = workScene.getChildren()
+
         print "Total Objects: %d" % len(Objects)
         for i,obj in enumerate(Objects):
             print "\n\n-------"
@@ -2094,6 +2104,38 @@ class Renderer:
 
     # Scene methods
 
+    def _filterHiddenObjects(self, scene):
+        """Discard object that are on hidden layers in the scene.
+        """
+
+        Objects = scene.getChildren()
+
+        visible_obj_list = [ obj for obj in Objects if
+                set(obj.layers).intersection(set(scene.getLayers())) ]
+
+        for o in Objects:
+            if o not in visible_obj_list:
+                scene.unlink(o)
+
+        scene.update()
+
+
+
+    def _buildLightSetup(self, scene):
+        # Get the list of lighting sources
+        obj_lst = scene.getChildren()
+        self.lights = [ o for o in obj_lst if o.getType() == 'Lamp' ]
+
+        # When there are no lights we use a default lighting source
+        # that have the same position of the camera
+        if len(self.lights) == 0:
+            l = Lamp.New('Lamp')
+            lobj = Object.New('Lamp')
+            lobj.loc = self.cameraObj.loc
+            lobj.link(l) 
+            self.lights.append(lobj)
+
+
     def _doSceneClipping(self, scene):
         """Clip whole objects against the View Frustum.
 
@@ -2111,12 +2153,11 @@ class Renderer:
         fovy = fovy * 360.0/pi
 
         Objects = scene.getChildren()
+
         for o in Objects:
             if o.getType() != 'Mesh': continue;
 
-            # TODO: use the object bounding box (that is already in WorldSpace)
-            # bb = o.getBoundBox() and then: for point in bb: ...
-
+            """
             obj_vect = Vector(cam_pos) - self._getObjPosition(o)
 
             d = obj_vect*view_vect
@@ -2125,6 +2166,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.
@@ -2133,6 +2198,7 @@ class Renderer:
         #geometricObjTypes = ['Mesh', 'Surf', 'Curve']
 
         Objects = scene.getChildren()
+
         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
         for obj in objList:
             old_obj = obj
@@ -2162,18 +2228,26 @@ 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:
@@ -2441,16 +2515,17 @@ class Renderer:
         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 inside the view rectangle
+                    # 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:
+                        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):
@@ -2465,9 +2540,9 @@ class Renderer:
                         nf.col = [f.col[0]] * len(nf.v)
 
                         clippedfaces.append(nf)
-
             facelist = clippedfaces[:]
 
+
         nmesh.faces = facelist
         nmesh.update()