DEFORMCACHE AND MATRIX ISSUE



  • Hi,
    I'm trying to write a Tag plugin to visualise data on objects.
    Everything's work fine until I put a deformer under the object and get the deformed Cache.
    In that case, the matrix of the generated Lines are translated ( I think they are calculated twice or something similar ).

    this is the code:

    import random
    import os
    import sys
    import c4d
    from c4d import utils as u
    from c4d import Vector as vc
    
    PLUGIN_ID = 1099997
    # VIS_SETTINGS
    # POLYCENTERINFO
    # POLYNORMALINFO
    
    
    class PolyVisualiserHelper(object):
        def polysCenter(self, ob, local=True):
            polys = ob.GetAllPolygons()
            pts = ob.GetAllPoints()
            nbPolys = ob.GetPolygonCount()
            center = vc()
    
            for i, poly in enumerate(polys):
                if poly.c != poly.d:
                    center = (pts[poly.a] + pts[poly.b] + pts[poly.c] + pts[poly.d]) / 4
                else:
                    center = (pts[poly.a] + pts[poly.b] + pts[poly.c]) / 3
    
                yield center
    
    
        def polysNormal(self, ob, local=True):
            polys = ob.GetAllPolygons()
            pts = ob.GetAllPoints()
            nbPolys = ob.GetPolygonCount()
    
            norPolys = [vc()] * nbPolys
            nor = vc()
    
            for i, poly in enumerate(polys):
                nor = (pts[poly.a] - pts[poly.c]).Cross(pts[poly.b] - pts[poly.d])
                nor.Normalize()
    
                yield nor
    
    
        def DrawPolyNormals(self, bd, obj):
            ob = obj
            mx = ob.GetMg()
            for p, n in zip(self.polysCenter(ob), self.polysNormal(ob)):
                normalDistance = n * 50
                destV = p + normalDistance
                bd.DrawLine(p * mx, destV * mx, 0)
    
    
        def AccessCache(self, bd, op) :
            temp = op.GetDeformCache()
            if temp is not None:
              # recurse over the deformed cache
              self.AccessCache(bd, temp)
            else:
              # verify existance of the cache
              temp = op.GetCache()
              if temp is not None:
                  # recurve over the cache
                  self.AccessCache(bd, temp)
              else:
                  # the relevant data has been found
                  # and it should not be a generator
                  if not op.GetBit(c4d.BIT_CONTROLOBJECT) :
                      # check the cache is an instance of Opoint
                      if op.IsInstanceOf(c4d.Opoint) :
                          self.DrawPolyNormals(bd, op)
    
    
    class PolyVisualiser(c4d.plugins.TagData, PolyVisualiserHelper):
        """Look at Camera"""
    
        def Init(self, node):
            pd = c4d.PriorityData()
            if pd is None:
                raise MemoryError("Failed to create a priority data.")
    
            pd.SetPriorityValue(c4d.PRIORITYVALUE_CAMERADEPENDENT, True)
            node[c4d.EXPRESSION_PRIORITY] = pd
    
            return True
    
    
        def Draw(self, tag, op, bd, bh):
            self.AccessCache(bd, op)
    
            return c4d.DRAWRESULT_OK
    
    
    if __name__ == "__main__":
        # Retrieves the icon path
        directory, _ = os.path.split(__file__)
        fn = os.path.join(directory, "res", "polyNormal.png")
    
        # Creates a BaseBitmap
        bmp = c4d.bitmaps.BaseBitmap()
        if bmp is None:
            raise MemoryError("Failed to create a BaseBitmap.")
    
        # Init the BaseBitmap with the icon
        if bmp.InitWith(fn)[0] != c4d.IMAGERESULT_OK:
            raise MemoryError("Failed to initialize the BaseBitmap.")
    
        c4d.plugins.RegisterTagPlugin(id=PLUGIN_ID,
                                      str="Draw Test 03",
                                      info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE | c4d.TAG_IMPLEMENTS_DRAW_FUNCTION,
                                      g=PolyVisualiser,
                                      description="drawtest03",
                                      icon=bmp)
    
    

    I already tried to disable the Matrix multiplication in the DrawPolyNormals() method when deformCache is generated but the matrix were still not correct

    thanks for future help



  • Hi @mdr74,

    thank you for reaching out to us. If possible, we would like to ask you to provide the following, as it would be then easier for us to answer your question:

    • The whole Python plugin (i.e. a folder including the resources for the plugin)
    • An example file which makes use of your plugin and also demonstrates the problem.
    • When you are not at liberty to share things publicly, you can also send your data to sdk_support(at)maxon.net.

    A few things that stand out to me regarding your code without me being able to run it.

    • You never set the matrix for the drawing operations in the BaseDraw. Since you draw in global space, you should be fine, since the BaseDraw should normally does come initialized for that. But it feels a bit dicey. Have a look at BaseDraw.SetMatrix_Matrix() on how to set up a drawing matrix in world or object coordinates (link).
    • You do not fully traverse the cache of an object. Caches can contain children and since you never go down in your cache evaluation, you might miss crucial data. I have talked here a bit about caches.
    • There is also the fact that your polysCenter is assuming all polygons, or more precisely their internal triangulation, to be coplanar. Which is not guaranteed and subsequently will cause your normal indicators not be attached to non-planar polygons. There is not really a performant fix for that in Python AFIAK, I am just mentioning it for completeness.
    • And finally, you are transforming your normals by the global matrix of the object (destV * mx). Which is most likely not what you want to do.
    # This will also fully transform the normal, because matrix multiplication is
    # distributive, i.e.: M * (u + v) = Mu + Mv
    destV = p + normalDistance
    bd.DrawLine(p * mx, destV * mx, 0)
    
    # Instead you should first transform the point and then transform the
    # normal by the normalized frame of your first transform, i.e. apply the 
    # transform without the offset and scale.
    q = p * transform
    r = q + normal * c4d.Matrix(v1=~transform.v1, 
                                v2=~transform.v2, 
                                v3=~transform.v3)
    bd.DrawLine(q, r, 0)
    

    Cheers,
    Ferdinand



  • @zipit
    Thanks

    I corrected the matrix calculation as suggested but the problem persist.
    I'll try to check the cache issue thread you linked

    for now I upload the plugin...it's very messy I think, but I use it also for testing other things ;):
    PLUGIN

    I also attach 2 screenshots of the problem

    when there is no deformer everything's correct (with base object and polygonal object):

    image.jpg

    as soon as I put a deformer below the lines shift away:

    image.jpg

    the same problem occurs when I change the deformer alignment to other planes and fit to parent:

    image.jpg

    thanks for the support



  • Hi @mdr74,

    jeah, it looks a bit like you not grabbing the right part of the cache. The link I have posted above shows that deform caches can be imbedded within the GetCache() cache - for example in the case of parametric objects, as they will hold a PolygonObject cache which then will be deformed by modifiers. I will have a look at your file on Monday, as I today probably won't have time to do it anymore. Feel free to look at the mentioned example in the mean time, in case you are impatient ;)

    Cheers,
    Ferdinand



  • @zipit
    I can wait, no hurry ;)

    Just to let you know...the same problem occurs with polygonal object and deformer
    everything's correct in world center but as soon as I move the object it seems that the matrix are calculated 2 times.
    It seems a cache issue

    I'm checking the link provided

    cheers



  • I don't know exactly how and why, but I found the solution.
    setting the basedraw matrix relative to the object when deformer is selected

    It's not a clean solution, but I cannot understand the hierarchy cache problem

    the modified method is:

    def DrawPolyNormals(self, obj):
            for p, n in zip(self.objPolysCenter, self.objPolysNormal):
                mx = self.mx
                if obj.GetDown():
                    if obj.GetDown().GetBit(c4d.BIT_ACTIVE):
                        self.basedraw.SetMatrix_Matrix(None, mx)
                    else:
                        self.basedraw.SetMatrix_Matrix(None, c4d.Matrix())
                normalDistance = n * self.normalLength
                q = p
                r = q + normalDistance * c4d.Matrix(v1=~mx.v1, v2=~mx.v2, v3=~mx.v3)
                self.basedraw.DrawLine(q, r, 0)
    

    the object points global position are calculated in self.objPolysCenter:

    def polysCenter(self, local=False):
            polys = self.polyPlgList
            pts = self.polyPtsList
            nbPolys = self.polyPlgCount
            center = vc()
    
            for i, poly in enumerate(self.polyPlgList):
                if poly.c != poly.d:
                    center = (pts[poly.a] + pts[poly.b] + pts[poly.c] + pts[poly.d]) / 4
                else:
                    center = (pts[poly.a] + pts[poly.b] + pts[poly.c]) / 3
                if not local:
                    yield center * self.mx
                else:
                    yield center
    


  • Hi @mdr74,

    I see you were busy on the weekend ;) Yes, your major mistake was that you did mix up drawing spaces in your code. There were also other problems in your code, most importantly the incomplete cache access. Below you will find a snippet which addresses these problems and scene file which I did test your code on.

    edit: Something I forgot to mention in the file and here is that _leaf_node_walk() should be implemented iteratively (currently it is recursive). But things would have gotten more bloated if I had done this. So I kept it this way. There are multiple examples here on the forum on how to walk a scene graph iteratively, which is a very similar problem. The major difference is that here you do not have a monohierarchical structure, but a polyhierarchical (child, cache and deform cache) which can make the implementation a bit cumbersome, as you do not have an easy way to get from a cache to its parent (GeListNode.GetUp() or BaseList2D.GetMain() won't work). You will have to build your own hierarchy lookup table to traverse the graph iteratively. The current implementation will fail at caches of roughly 500 objects or more, since Python sets its recursion limit to 1000 to prevent stack overflows. You could also increase that recursion limit with sys, but at some point Python will then crash spectacularly when you exhaust all the stack memory for that process ;)

    Cheers,
    Ferdinand

    pc13027_scene.c4d

    """Example for a TagData plugin that draws stuff into the viewport based on 
    the cache of the object it is attached to.
    
    Your problems did not really stem from adding deformers, but the fact that you
    did mix local and global coordinates. But instead of sifting through all your 
    code, I just quickly wrote a clean version, since also your cache access code 
    was faulty. I also added the "correct" draw pass in BaseTag.Draw(). You can of
    course choose another pass, but you should not draw into all passes.
    
    Your code also did suffer a bit from being overly complex or quite lenient 
    with system resources. You should be more conservative when writing code that
    draws stuff onto the screen, or things might get quite slow. 
    
    For example functions like zip() are quite expensive to run in such context.
    And there is also the fact that DrawLine() is not the fastest thing in the 
    world, which should make you even more conservative ;)
    
    Cheers,
    Ferdinand
    """
    
    import c4d
    import os
    
    PLUGIN_ID = 1099997
    
    
    class DrawPolyInfo(object):
        """Draws stuff for polygon objects.
        """
    
        def __init__(self, bd, data, node):
            """DrawPolyInfo initializer.
    
            Args:
                bd (c4d.BaseDraw): The view port to draw in.
                data (c4d.BaseContainer): The parameters of the tag node.
                node (c4d.BaseObject): The object to draw for.
    
            Raises:
                TypeError: When arguments are not of expected type.
            """
            if not isinstance(bd, c4d.BaseDraw):
                raise TypeError(f"Expected BaseDraw. Received: {type(bd)}")
            if not isinstance(data, c4d.BaseContainer):
                raise TypeError(f"Expected BaseContainer. Received: {type(data)}")
            if not isinstance(node, c4d.BaseObject):
                raise TypeError(f"Expected BaseObject. Received: {type(node)}")
            # Run the draw loop.
            DrawPolyInfo._draw_information(bd, data, node)
    
        @staticmethod
        def _draw_information(bd, data, node):
            """The drawing loop.
    
            Args:
                bd (c4d.BaseDraw): The view port to draw in.
                data (c4d.BaseContainer): The parameters of the tag node.
                node (c4d.BaseObject): The host of the tag.
            """
            # Go over all polygonal leaf nodes in cache of node.
            for leaf_node in DrawPolyInfo._leaf_node_walk(node, is_cache=False):
                # Draw the normals
                if data[c4d.VIS_SETTINGS_NORMAL_ACTIVE]:
                    DrawPolyInfo._draw_normals(bd, data, leaf_node)
                # Do other stuff you want to do here.
                # ...
    
        @staticmethod
        def _leaf_node_walk(node, is_cache=True):
            """Yields recursively the the polygonal leaf nodes of a cache.
    
            Args:
                node (c4d.BaseObject): The node to walk the cache for.
                is_cache (bool, optional): If passed node is a virtual object,
                 i.e. part of a cache, or not. Defaults to `True`. 
    
            Yields:
                c4d.PolygonObject: The polygonal leaf nodes in the cache of 
                 `node`.
            """
            if node is None:
                return
    
            # Walk the deformed cache or the static cache when the former is
            # not present.
            cache = node.GetDeformCache() or node.GetCache()
            if cache is not None:
                # Yield the leaf nodes in that cache.
                for leaf in DrawPolyInfo._leaf_node_walk(cache):
                    yield leaf
            # A node with no caches is a terminal/leaf node. When it is a
            # polygon object not marked as an input object, we yield it.
            elif (isinstance(node, c4d.PolygonObject) and
                  not node.GetBit(c4d.BIT_CONTROLOBJECT)):
                yield node
    
            # Break out for the start node so that we do not include its children
            # (i.e. non-virtual objects).
            if is_cache is False:
                return
    
            # We have also to go over the children for composed caches.
            for child in node.GetChildren():
                for leaf in DrawPolyInfo._leaf_node_walk(child):
                    yield leaf
    
        @staticmethod
        def _draw_normals(bd, data, node):
            """Draws fancy normal indicators.
    
            Args:
                bd (c4d.BaseDraw): The view port to draw into.
                data (c4d.BaseContainer): The parameters of the tag node.
                node (c4d.PolygonObject): A polygonal node to draw stuff for.
            """
            # We set the drawing space to the local space of the node we want
            # to draw stuff for, making our life much more easier. I.e. we do
            # not have to juggle global coordinates anymore.
            bd.SetMatrix_Matrix(node, node.GetMg(), 4)
            bd.SetPen(c4d.Vector(1., 1., 0.))
    
            # Now we go over all polygons.
            points = node.GetAllPoints()
            for cpoly in node.GetAllPolygons():
                a, b, c, d = (points[cpoly.a], points[cpoly.b],
                              points[cpoly.c], points[cpoly.d])
                # Just as in your computations, these two are assuming that the
                # polygons are coplanar. Which is not always the case, but there
                # is no easy way to get hold of Cinema's internal triangulation.
                # Your best bet is to triangulate the node manually beforehand
                # (which is slow and therefore rather bad for drawing stuff). The
                # best design would be probably to cache the triangulation in some
                # way.
                mid_point = (a + b + c + d) * .25
                normal = ~((b - a) % (c - a))
                # Note: DrawLine() will not perform very well when you have have
                # any mildly heavy geometry as the input(s) here.
                length = data[c4d.VIS_SETTINGS_NORMAL_LENGTH]
                bd.DrawLine(mid_point, mid_point + normal * length, c4d.NOCLIP_D)
    
    
    class PolyVisualiser(c4d.plugins.TagData):
        """Your plugin class.
    
        I pruned some stuff to make this very straight forward.
        """
    
        def Init(self, node):
            """
            Called when Cinema 4D Initialize the TagData (used to define, default 
            values).
    
            :param node: The instance of the TagData.
            :type node: c4d.GeListNode
            :return: True on success, otherwise False.
            """
            self.InitAttr(node, bool, c4d.VIS_SETTINGS_CENTER_ACTIVE)
            node[c4d.VIS_SETTINGS_CENTER_ACTIVE] = True
            self.InitAttr(node, bool, c4d.VIS_SETTINGS_NORMAL_ACTIVE)
            node[c4d.VIS_SETTINGS_NORMAL_ACTIVE] = False
    
            pd = c4d.PriorityData()
            if pd is None:
                raise MemoryError("Failed to create a priority data.")
    
            pd.SetPriorityValue(c4d.PRIORITYVALUE_CAMERADEPENDENT, True)
            node[c4d.EXPRESSION_PRIORITY] = pd
            return True
    
        def Draw(self, tag, op, bd, bh):
            """
            Called by Cinema 4d when the display is updated to display some v
            isual element of your object in the 3D view. This is also the place 
            to draw Handle.
    
            :param op: The instance of the ObjectData.
            :type op: c4d.BaseObject
            :param drawpass:
            :param bd: The editor's view.
            :type bd: c4d.BaseDraw
            :param bh: The BaseDrawHelp editor's view.
            :type bh: c4d.plugins.BaseDrawHelp
            :return: The result of the drawing (most likely c4d.DRAWRESULT_OK)
            """
            # We usually only want to draw in the object draw pass. The reason
            # why your version did *flicker* was that you did draw into all draw
            # passes at once.
            if (not isinstance(bd, c4d.BaseDraw) or
                    bd.GetDrawPass() != c4d.DRAWPASS_OBJECT):
                return c4d.DRAWRESULT_OK
    
            # Set off our little drawing helper.
            DrawPolyInfo(bd, tag.GetData(), op)
            return c4d.DRAWRESULT_OK
    
    
    if __name__ == "__main__":
        # Retrieves the icon path
        directory, _ = os.path.split(__file__)
        fn = os.path.join(directory, "res", "polyNormal.png")
    
        # Creates a BaseBitmap
        bmp = c4d.bitmaps.BaseBitmap()
        if bmp is None:
            raise MemoryError("Failed to create a BaseBitmap.")
    
        # Init the BaseBitmap with the icon
        if bmp.InitWith(fn)[0] != c4d.IMAGERESULT_OK:
            raise MemoryError("Failed to initialize the BaseBitmap.")
    
        c4d.plugins.RegisterTagPlugin(id=PLUGIN_ID,
                                      str="Draw Test 03",
                                      info=(c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE |
                                            c4d.TAG_IMPLEMENTS_DRAW_FUNCTION),
                                      g=PolyVisualiser,
                                      description="drawtest03",
                                      icon=bmp)
    


  • Thanks a lot !!!.
    I will try it in the next days.
    I'm free for my personal projects only during weekends


Log in to reply