Vertex Maps on Generators



  • I'm pretty sure I know the answer to this, but I just need to ask to make sure I'm not missing something.

    I'm trying to do something similar to how the Voronoi Fracture can generate selections and vertex maps. Selections have been going swell, its these pesky vertex maps! My plugin happens to be a tag, not a generator... not sure if that distinction matters in this context.

    I don't know how c4d updates vertex maps when their host object's geometry changes, but it certainly doesn't happen when that host object is parametric. Once the geometry changes a replacement tag needs to be generated and the old vertex map's goals transferred to it. While I've been able to get this much of it working, once I attempt to delete the original tag it all falls apart. Even if I could get this working, I still don't know where I could safely call the following function in a NodeData plugin:

    def RefreshVertexMapTag(vmap, obj, point_cnt):
        # Make a brand new vertex map tag
        replacement_tag = obj.MakeVariableTag(c4d.Tvertexmap, point_count, vmap)
        # Disable the tag so that c4d doesn't try to update it
        replacement_tag[c4d.EXPRESSION_ENABLE] = False
        # transfer all goals to the new tag
        vmap.TransferGoal(replacement_tag, False)
        # Remove the original tag
        vmap.Remove()
    

    This very simplified function is based on a workaround posted by Maxime last year, simply to illustrate what it would entail to expose a vertex map on a generator who's data is of a variable size, noting that it is not suitable for production because it does some cross-threaded things that could crash c4d.

    I've even tried updating the byte sequence manually using GetAllLowlevelDataW(), but since that's dealing with a memoryview it naturally doesn't work.

    So, I've finally resigned to giving up on this effort, but not without at least asking if it's possible. Can a vertex map be updated to respond to the changing geometry of a parametric object/generator in a way that is safe to use in a NodeData plugin?

    To recap:

    • SetAllHighlevelData(): throws an error when the passed data's size differs

    • GetAllLowlevelDataW(): basically same result as above, except quietly (no error, just never applies any of the changes to the byte sequence)

    • Fully replace tag: Works until you try to delete the old tag, then none of it works... but even if it did it's prone to crashing c4d.

    Thanks in advance for either confirming the bad news... or for swooping in to save the day (if that's possible!)

    Cheers!
    Kevin



  • Hi @kvb,

    thank you for reaching out to us. Unfortunately it still is a bit unclear to me, what you are trying to do. It would be beneficial to give us a more concrete example for the context in which you want to rebuild a vertex map. Some points:

    • As you already figured out, c4d.VariableTag is immutable regarding its size, so you cannot change the size of an existing instance and subsequently all write attempts, be it byte sequences or the high level data, will fail, when the size of the written data does not match the size of the tag. But even if it would not be, that won't help when the new geometry assigns a new topological meaning to its vertex order, as vertex maps reference the vertex order for the point object they are created for.
    • In a NodeData plugin NodeData.Message() would be a relatively safe place to modify the scene graph (i.e. add and remove tags). Since it is being called most of the time from the main thread. However, there is guarantee for this to hold true, so you should check yourself via c4d.threading.GeIsMainThread() or c4d.threading.GeIsMainThreadAndNoDrawThread() (depending on your goals).

    Regarding: "Can a vertex map be updated to respond to the changing geometry"? It mostly depends. If you for example just have a mesh with n vertices as geometry and then just append one or multiple vertices, it is relatively easy to that, since the first n vertices in the new mesh are going to have the same topological meaning as in the old one. So as a non-code, but structural example:

    # Some vector tuples making up our vertices. Order matters here!
    old_points = [(0, 0, 0), (0, 1, 1), (1, 1, 1)]
    # Weights for each vertex.
    old_weights = {0: 1.0, 1; 0.5, 2: 0.25}
    
    # The "new" geometry just appends a point to the end.
    new_points = [(0, 0, 0), (0, 1, 1), (1, 1, 1), (-1, -1, -1)]
    # Which allows us to infer that this holds true.
    new_weights = old_weights + {3: SOME_DEFAULT_VALUE}
    

    This obviously only rarely holds true. Its not impossible to do this for a general case - e.g. when the new mesh still only adds one vertex, but also rearranges the order of the vertices -, but you have to choose an algorithm/approach on how you want to map your new topology to the old one.

    This can be done relatively naïvely, by for example just computing the Euclidian distances between the two point sets and mapping the shortest pairs to each other. This will work pretty well when the meshes are very similar. But when you have to think about topological similarity in a more abstract sense, things can get pretty complicated. We cannot really offer support on that topic, as this would be support on designing an algorithm. You are of course more than welcome to post any developed solutions here and we might be able to point out smaller problems.

    Thank you for your understanding,
    Ferdinand



  • Thanks @zipit!
    While being diligent in my response I actually started to notice a pattern to what I was seeing with my "recreate the tag" workaround. I believe it's related to python's "call by assignment" nature coupled with how TransferGoal() works.
    When I would call TransferGoal() how the sdk instructs me to do (always passing False for the second argument) it would result in the variable that holds the old vertex map tag updating to now hold the newly created tag (since it is now in the baselink from which the old tag was referenced). If I pass True to that second argument, my old tag variable continues to reference the old tag and everything works as expected.

    The question now is, what's the reasoning behind the sdk marking that argument as private and instructing us to always set it to False?

    https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/BaseList2D/index.html?highlight=transfergoal#BaseList2D.TransferGoal

    Cheers!
    Kevin



  • Hi @kvb,

    I am still not quite sure if I understand you and your problem correctly. Given your first post, I was under the impression that your major question was how to transfer a mesh attribute (i.e. a vertex map weighting) from on geometry to another, while maintaining some sense of intentionality in this operation, i.e. make it look right. Feel free to point out when I was wrong.

    BaseList2D.TransferGoal() has nothing to do with that topic. It is only meant to replace all BaseLinks that point to the BaseList2D it is being invoked on, with links to the BaseList2D dst passed as its first argument. Things to be marked as private in Cinema's documentation can have multiple reasons. The major one is that these symbols or functions might have unintended side-effects for public users and/or require an insight into the non-public API of Cinema in order to be used properly. Having a look at the code for BaseList2D::TransferGoal, it seems that passing true for undolink will change how certain things are handled in the private API, regarding the AtomGoals representing the linked nodes in the interface. It seems advisable to follow the recommendation of the developer who wrote this documentation of always passing false, as otherwise the BaseLink will not be updated properly.

    I am not quite sure what you mean by tag variable, but I assume you mean a BaseLink attribute of your tag? Or do you actually mean a symbol/variable in your code? Your BaseLink not being updated properly after passing True for undolink would actually align with my findings. But I am not quite sure, why you would consider this to be a desirable or "expected" behavior.

    Sorry, I am really confused :D It might be helpful if you would explain from scratch what you are trying to do. Are you only after transferring BaseLinks? And if so, what is not working out for you when passing the recommended false for undolink?

    Cheers,
    Ferdinand



  • Apologies, I thought my mention of the Voronoi Fracture's ability to generate vertex map tags (itself being a generator that deals with variable geometry) would be clear enough, but I suppose a casual mention doesn't suffice for an app this complex, haha!

    Yeah, it's really all about overcoming that immutable nature of the vertex map tag. I'm generating them on parametric objects and I need them to be able to adapt to changing point counts. Just like they're able to do on editable polygon objects. Which means I have to create a new tag each time the host object's geometry changes and update anywhere else it's linked. That's where TransferGoal() comes in.

    Where my function was failing was a result of TransferGoal() not only updating the BaseLinks, but also the variables within my code. So in my function, once TransferGoal()is called, both vmap and replacement_tag variables end up referencing the same tag (the new one), leaving me without a variable filled with the old tag to delete.

    Passing True as the second argument left my variables alone and only updated the links, essentially making it work perfectly. So passing True or False to TransferGoal() didn't make any difference as far as the BaseLinks were concerned. Those always updated correctly. If I had to guess I would say it has to do with the memory addresses of the objects being transferred... which is just more reason to avoid ignoring that little instruction in the sdk;) I'll trust the devs on matters of memory handling lol.

    Turns out it's quite easy to work around. Since I insert the new tag directly after the old tag I can simply use GetPred() to find the old tag and successfully delete it.

    As far as your question as to whether I would consider this desirable or expected behavior, I would say no. I would much prefer that my in-code variables remain as they were before calling TransferGoal, because as it currently is I'm left with two distinct variables that hold the same object. In other words, I'm left with redundancy and it ends up preventing me from doing more to the old BaseList2D object. In my case, it happens to be relatively easy to re-find and re-reference that BaseList2D object, but that might not always be the case.

    But yeah, now I have a working skeleton function (that doesn't break the rules lol). Just gotta bulk it up with appropriate checks and I should be good to go!

    Thanks for your help @zipit!

    Cheers!
    Kevin