administrators

Private

Posts

  • RE: MergeDocument() crash C4D

    @chuanzhen said in MergeDocument() crash C4D:

    But I still have a question. If it's a plugin issue ( CopyTo Read Write function is not implemented), then why can the Merge command in C4D merge files normally?

    What I meant was this: Imagine you have an ObjectData plugin Foo which has a class instance attribute called _data. Many of Foo's methods rely on _data, as for example GetVirtualObjects, GetDDescription, etc. When you now copy an instance of Foo, Cinema will copy its data container but not _data when you have not implemented CopyTo. On the copied instance of Foo, the _data attribute will either not exist at all or be in uninitialized state. Cinema can then for example crash or freeze when you raise an AttributeError (because you try to access a non-existing _data; or any other error) in a GetDDescription call in some corner cases. _data being misaligned with the data container of a node could also lead to all sorts of bugs when your code assumes that they are somehow aligned.

    In short, Cinema can always read, write, and copy nodes, no matter what you do. But with CopyTo you can make sure that data outside of the data container of the node (BaseList2D.GetDataInstance) is also correctly copied. The lesson here is that when you have fields such as self._my_data on a node, and that field cannot be reestablished on the fly, you must implement Read, Write, and CopyTo, so that _my_data can be written, read, and copied from/to/between scene files. You therefore also usually implement all three of these methods and not just one of them when you need them.

    Cheers,
    Ferdinand

  • RE: Using SetWeightMap() multiple times can lead to inexplicable changes in weight values

    Hey,

    so I gave this a shot but I cannot reproduce it. Please provide a scene file as asked for when this is still a problem. I did not fully understand your instructions, so I created a simple scene where a cube is being rigged.

    When I print out the weights of the rig while running your script, I cannot see any change of data. Your video shows some change of values, but overlays and GUI values in general can be unreliable, and I am also not quite sure where the "cube_joint" overlay draws its data from.

    Cheers,
    Ferdinand

    Result

    my scene: joints.c4d
    3349d252-8642-4041-a6f3-1b69cddb7e8c-image.png

    import c4d
    
    doc: c4d.documents.BaseDocument  # The currently active document.
    op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
    
    def main() -> None:
        tag = op.GetTag(c4d.Tweights)
        if not tag:
            return
        j_cnt = tag.GetJointCount()
        p_cnt = op.GetPointCount()
        print("="*200)
        for i in range(10):
            weights = [tag.GetWeightMap(j_id,p_cnt) for j_id in range(j_cnt)]
            print(weights)
            for j_id in range(j_cnt):
                tag.SetWeightMap(j_id,weights[j_id])
            tag.WeightDirty()
        
        
        c4d.EventAdd()
    
    
    if __name__ == '__main__':
        main()
    
  • RE: MergeDocument() crash C4D

    Hey @chuanzhen,

    we will need both the scene you are merging and merging into to make here any sensible statement. We will also need more than this one sole line of code.

    And while a crash always indicates that we could do something better, my hunch would be that that fault lies mostly with some plugin. Because Cinema itself uses heavily MergeDocument and if there would be a principal bug with the very common flags c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS, we would have found and fixed it before.

    The first thing that comes to mind with your line of code is that you are off-main thread and then run into problems because you try to let the code run on the MT by passing None for thread. But off-main-thread, merging documents would be a bit unusual (and also illegal if one of the participating documents is a loaded document). You can use c4d.threading.GeGetCurrentThread() to get the current thread and pass it for thread.

    Another cause could be some NodeData plugin which holds data outside of its data container and which does not correctly serialize that extra data (does not implement NodeData::Read, ::Write, and ::CopyTo) which can then lead to crashes when Cinema tries to run the merged scene with only partially copied node data (assuming the plugin in questions also fails to do sanity/existence checks on its internal extra data).

    Cheers,
    Ferdinand

  • RE: List of selected edges

    Hey @Kantronin,

    you might want to look at geometry_polgyon_edges_2024.0.py, it should answer all your questions.

    The TLDR is: full-edge maps as you hint at in your posting do exist in Cinema via helper functions, but they are often also not very useful. Internally, Cinema is operating as any other DCC application with the concept of what is often called a "half-edge" data structure. So, when you have the two polygons A and B:

      a---b---e
      | A | B |
      d---c---f
    

    Which share an edge over the vertices b and c, this is actually two edges and not one, due to how normals work. For both polygons to have a normal facing in the same direction, they must have opposite winding orders, which means that the edge b-c is actually two edges: b->c and c->b. So, A could for example index the edge as bc, and B as cb. The example goes into more detail with this.

    Cheers,
    Ferdinand

  • RE: Disable all animation in document

    Hello @ceen,

    Yes, this is possible. @Anlv is right that ID_CTRACK_ANIMOFF is the toggle you are looking for (thank you for helping out!). You can then either combine it with manual scene traversal code, or use mxutils.RecurseGraph which is for most users probably the simpler option when they do not have intimate knowledge of the Cinema scene graph.

    Cheers,
    Ferdinand

    """Loops over all tracks in the scene and toggles their animation state.
    
    I.e., running this once will disable all tracks, running it again will enable all tracks, and so on.
    """
    
    import c4d
    import mxutils
    
    doc: c4d.documents.BaseDocument  # The currently active document.
    op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
    
    def main() -> None:
        """Called by Cinema 4D when the script is being executed.
        """
        # We walk the document using RecurseGraph, finding all tracks in the scene, no matter where they
        # are hiding. This is a relatively inefficient call as we really walk everything, and experts
        # could fine tune this. But for a simple script like this or any code that is not performance 
        # critical, this is just fine.
        for track in mxutils.RecurseGraph(doc, yieldBranches=True, yieldHierarchy=True, nodeFilter=[c4d.CTbase]):
            track[c4d.ID_CTRACK_ANIMOFF] = not track[c4d.ID_CTRACK_ANIMOFF]
    
        c4d.EventAdd()
    
    
    if __name__ == '__main__':
        main()
    
  • RE: Using SetWeightMap() multiple times can lead to inexplicable changes in weight values

    Thanks I will have a look, but it might take a few days before I find the time.

  • RE: Using SetWeightMap() multiple times can lead to inexplicable changes in weight values

    Hey @chuanzhen,

    thank you for reaching out to us. Please provide a code example and scene file to reproduce the issue, without it is hard to give a concrete answer.

    @Anlv and @ThomasB are correct in bringing forward that floating point precision could be an issue in principle. But IEEE 754 corresponds to roughly 17 significant digits in the interval [0, 1] without loss of precision for the data type Float64, a change in the third significant digit as you report is somewhat unlikely in the interval [0, 1] (but not impossible).

    And more importantly, all integer values, e.g., 1.0, are exactly representable up to 2^53 in IEEE 754-64, so you should never see a loss for the value 1.0. More over, round trips should mean no accumulated loss when you do not do additional computations with the value. So, you first write the value X which might or might not be representable as IEEE 754 -64 because you entered it as some kind of literal in your code or via a GUI. Once written X will be X', i.e., the closest value of X which is representable in IEEE 754-64. Further read and write events will not change that value, unless you do some arithmetic operations with it.

    Overall this sounds all bit like the weight tag is somehow post processing the passed data, and you therefore encounter some error creep. But I can only say anything concrete with code and a scene.

    Cheers,
    Ferdinand

  • RE: After calling BaseContainer.Sort(), the stored values are lost

    Hey @chuanzhen,

    Thank you for reporting this. Yes, you are misunderstanding this function a bit. But I have to admit that I did too first, because the function is very weird in what it does. But when you look at the C++ docs, there is a warning.

    846d8773-a0bf-4ac4-b6b0-6207ca995bf6-image.png

    I have just updated the Python docs for the next release to also contain the warning. So, this function is effectively only useable for containers holding strings. As you can see by my example below, it does not only remove all values but turns them into strings (and then leaves them empty when the source was not a string). The function is badly named.

    Find also a little example below to do what you want: sort a container by values.

    Cheers,
    Ferdinand

    Result

    Root (None , id = -1):
    ├── 4 (DTYPE_LONG): 4
    ├── 5 (DTYPE_LONG): 5
    ├── 6 (DTYPE_LONG): 6
    ├── 7 (DTYPE_LONG): 7
    ├── 8 (DTYPE_LONG): 8
    ├── 9 (DTYPE_LONG): 9
    ├── 10 (DTYPE_LONG): 10
    ├── 11 (DTYPE_LONG): 11
    ├── 0 (DTYPE_LONG): 0
    ├── 1 (DTYPE_LONG): 1
    └── 2 (DTYPE_LONG): 2
    --------
    Root (None , id = -1):
    ├── 1 (DTYPE_STRING):
    ├── 4 (DTYPE_STRING):
    ├── 5 (DTYPE_STRING):
    ├── 6 (DTYPE_STRING):
    ├── 7 (DTYPE_STRING):
    ├── 8 (DTYPE_STRING):
    ├── 9 (DTYPE_STRING):
    ├── 10 (DTYPE_STRING):
    ├── 11 (DTYPE_STRING):
    ├── 0 (DTYPE_STRING):
    └── 2 (DTYPE_STRING):
    

    Code

    import c4d
    import mxutils
    
    doc: c4d.documents.BaseDocument  # The currently active document.
    op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
    
    def main() -> None:
        bc = c4d.BaseContainer()
        for i in range(4,12):
            bc.SetInt32(i,i)
        for i in range(3):
            bc.SetInt32(i,i)
        print(mxutils.GetContainerTreeString(bc))
        print("--------")
        bc.Sort()
        print(mxutils.GetContainerTreeString(bc))
        
        
    if __name__ == '__main__':
        main()
    

    Custom Sorting Function

    Result

    Root (None , id = -1):
    ├── 0 (DTYPE_LONG): 0
    ├── 1 (DTYPE_LONG): 1
    ├── 2 (DTYPE_LONG): 2
    ├── 3 (DTYPE_LONG): 3
    ├── 5 (DTYPE_LONG): 5
    ├── 6 (DTYPE_LONG): 6
    ├── 7 (DTYPE_LONG): 7
    ├── 8 (DTYPE_LONG): 8
    ├── 9 (DTYPE_LONG): 9
    ├── 10 (DTYPE_LONG): 10
    ├── 11 (DTYPE_LONG): 11
    ├── 12 (DTYPE_LONG): 12
    ├── 13 (DTYPE_LONG): 13
    ├── 14 (DTYPE_LONG): 14
    ├── 15 (DTYPE_LONG): 15
    ├── 16 (DTYPE_LONG): 16
    ├── 17 (DTYPE_LONG): 17
    ├── 18 (DTYPE_LONG): 18
    ├── 19 (DTYPE_LONG): 19
    └── 4 (DTYPE_SUBCONTAINER , id = -1):
        ├── 0 (DTYPE_LONG): 0
        ├── 1 (DTYPE_LONG): 1
        ├── 2 (DTYPE_LONG): 2
        ├── 3 (DTYPE_LONG): 3
        ├── 4 (DTYPE_LONG): 4
        ├── 5 (DTYPE_LONG): 5
        ├── 6 (DTYPE_LONG): 6
        ├── 7 (DTYPE_LONG): 7
        ├── 8 (DTYPE_LONG): 8
        ├── 9 (DTYPE_LONG): 9
        ├── 10 (DTYPE_LONG): 10
        ├── 11 (DTYPE_LONG): 11
        ├── 12 (DTYPE_LONG): 12
        ├── 13 (DTYPE_LONG): 13
        ├── 14 (DTYPE_LONG): 14
        ├── 15 (DTYPE_LONG): 15
        ├── 16 (DTYPE_LONG): 16
        ├── 17 (DTYPE_LONG): 17
        ├── 18 (DTYPE_LONG): 18
        └── 19 (DTYPE_LONG): 19
    

    Code

    import c4d
    import mxutils
    
    import random
    
    doc: c4d.documents.BaseDocument  # The currently active document.
    op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
    
    def SortContainer(bc: c4d.BaseContainer, mode: str = "value") -> None:
        """Sorts the given container by ID or value.
    
        This function reallocates sub-containers but modifies the passed in container in place. It would also be
        possible to modify the sub-container in place, but more work to implement and also more complex to run.
        Due to the fact that we have to copy container data, this is also not the cheapest function.
        """
        def copy(bc: c4d.BaseContainer) -> list[tuple[int, any]]:
            """Copies the contents of the given BaseContainer to a list of (ID, value) tuples.
            """
            result = []
            for i, v in bc:
                if isinstance(v, c4d.BaseContainer):
                    v = copy(v)
                result.append((i, v))
            return result
    
        def build(bc: c4d.BaseContainer, items: list[tuple[int, any]]) -> None:
            """Builds a BaseContainer from the given list of (ID, value) tuples.
            """
            bc.FlushAll()
            for i, v in items:
                if isinstance(v, list):
                    sub = c4d.BaseContainer()
                    build(sub, v)
                    v = sub
                bc[i] = v
    
        def sort_items(items: list[tuple[int, any]], mode: str) -> list[tuple[int, any]]:
            """Sorts the given list of (ID, value) tuples by ID or value. Nested containers are sorted recursively.
            """
            def is_nested(item: tuple[int, any]) -> bool:
                return isinstance(item[1], (list, tuple))
    
            def sort_key(item: tuple[int, any]) -> tuple[bool, any]:
                item_id, value = item
    
                # The first key puts nested values after scalar values.
                # The second key is only compared within the same group.
                if is_nested(item):
                    return True, item_id if mode == "id" else 0
    
                return False, item_id if mode == "id" else value
    
            # Sort nested contents recursively before sorting this level.
            for index, (item_id, value) in enumerate(items):
                if is_nested((item_id, value)):
                    items[index] = (item_id, sort_items(value, mode))
    
            return sorted(items, key=sort_key)
    
        items: list[tuple[int, any]] = copy(bc)
        return build(bc, sort_items(items, mode))
    
    def main() -> None:
        """
        """
        bc: c4d.BaseContainer = c4d.BaseContainer()
        data: list[int] = list(range(20))
        random.shuffle(data)
        for v in data:
            bc[v] = v
    
        copy: c4d.BaseContainer = bc.GetClone(0)
        bc[4] = copy
    
        print(mxutils.GetContainerTreeString(bc))
        print("--------")
        SortContainer(bc)
        print(mxutils.GetContainerTreeString(bc))
        
        
    if __name__ == '__main__':
        main()
    
  • RE: Redshift Maya Extension SDK Access Request (Custom Hair Translator)

    There seems to be some solution, please stay tuned until the RS team has sorted out things. I will then update this topic.

  • RE: Alembic Export to Unreal - RootUVs?

    What I meant was to use the Cineware Unreal bindings, but I am not sure if they support it. Going the export route is a big backwards when Cinema has specific bindings.

    https://www.maxon.net/en/cineware

    I will give the Unreal dev a bump when I see him again.