Mirroring with Matching or Different Axes



  • Hi,

    I just had a quick look at both the online documentation and your file. The online documentation for the mirror tool has this image (I hope it is okay that I hotlink it here) for the dropdown in question.

    alt text

    The option None would be what we discussed under reflecting the vertices, only that they here also shove the frame a bit around so that it sits where the reflected frame would sit, they do not touch the orientation at all and simply reflect the internal point data. The option Rotate is something that does not really matter here I think. And the options XY, XZ and YZ are exactly that what we did discuss regarding left-handed matrices. In the XY example you can see that for a truly mirrored frame, the z-axis should point downwards on the right side or upwards on the left side. But since Cinema only has left handed matrices, one axis has to be flipped and Cinema leaves the decision which axis to flip to the user via that option.

    But in your file in the not working part of the hierarchy you have an target object with a different frame orientation than the source object. This is a different problem than addressed by the mirror tool, I think. We could differentiate two types here:

    1. The frame orientations are different, but each component appears in the other frame as another axis or the inverse of another axis (e.g. the x axis in one frame is the inverse of the y axis in the other) . The other frame would be here the result of rotating the primary frame by 90° around one of its axis or multiple of these operations. This could be relatively easily solved by swizzling the components of the reflected frame before assigning the result to the target object. The most straight forward option would be to leave the decision which planes/axis to swizzle to the user via the interface, because trying to detect the swizzle relation automatically would let you face the same problem as described in the next point under the hard way.
    2. The frame orientations are truly arbitrary and in no hidden predefined relation to each other. This seems to be the case in your file, at least the parent null-object of target object is in such orientation. This is either an annoying or a hard problem to solve and if I am not mistaken, Cinema offers no solution for the hard way in its toolset.

    The annoying way would be reflecting the internal point data of the object. While this is trivial for a bunch of vertices, it can become tedious for more complex structures, because if you reflect vertices that have polygons, you will also have to flip the vertex index order in each polygon, i.e. flip the normals of the polygons, because reflecting the vertices flipped the point normals. And the list goes on: you might have to handle uvw and weight information, untangle hierarchical information like in your example, etc. pp.

    The hard way would be analysing the topology of the object so that one can place the target object in a way that it looks like it is reflected, but its frame is no reflection relation to the source object and you also do not touch the internal (point) data. I will leave it at that for now.

    So knowing the problem in all its nasty requirements: I would say there is no easy way, or at least I do not see one.

    Cheers,
    zipit



  • Hi,

    I have slightly updated my previous example for the case when the two frames are in a simple rotational relation around one of their standard basis vectors. As already stated, a general solution for this problem is much harder to accomplish and also not really a 'math' problem, but more a conceptual and algorithmic problem.

    There are also other ways to calculate the delta between your two frames if you are in the non-general case, which will have different advantages and disadvantages to the solution provided by me, but will also require certain guarantees regarding the source and target object and their frames to work properly (like for example having their vertices occupy the same points in world space).

    testing-the-script.c4d

    Cheers,
    zipit



  • Hi @blastframe and @zipit, thanks for the thoughtful discussion on the topic.
    With regard to the Mirror Tool's implementation it's not rocket science because the foundation of the tool have been properly explained by @zipit with regard to reflecting a transformation matrix on main axes, in case of XY, XZ, YZ mirroring and with the vertexes being actually displaced in the "mirrored" position in case None mirroring.
    That said a very simplified version in Python could be based on the following matrix changes:

    ...
    ...
        
        if flip == "XY":
    
            targetMG.v1 = c4d.Vector( sourceMG.v1.x,  sourceMG.v1.y, -sourceMG.v1.z)
            targetMG.v2 = c4d.Vector( sourceMG.v2.x,  sourceMG.v2.y, -sourceMG.v2.z)
            targetMG.v3 = c4d.Vector(-sourceMG.v3.x, -sourceMG.v3.y,  sourceMG.v3.z)
            
            targetMG.off = c4d.Vector(sourceMG.off.x, sourceMG.off.y, -sourceMG.off.z)
            
        elif flip == "YZ":
            targetMG.v1 = c4d.Vector( sourceMG.v1.x, -sourceMG.v1.y, -sourceMG.v1.z)
            targetMG.v2 = c4d.Vector(-sourceMG.v2.x,  sourceMG.v2.y,  sourceMG.v2.z)
            targetMG.v3 = c4d.Vector(-sourceMG.v3.x,  sourceMG.v3.y,  sourceMG.v3.z)
            
            targetMG.off = c4d.Vector(-sourceMG.off.x, sourceMG.off.y, sourceMG.off.z)
        
        elif flip == "ZX":
            targetMG.v1 = c4d.Vector(-sourceMG.v1.x,  sourceMG.v1.y, -sourceMG.v1.z)
            targetMG.v2 = c4d.Vector( sourceMG.v2.x, -sourceMG.v2.y,  sourceMG.v2.z)
            targetMG.v3 = c4d.Vector( sourceMG.v3.x, -sourceMG.v3.y,  sourceMG.v3.z)
            
            targetMG.off = c4d.Vector(sourceMG.off.x, -sourceMG.off.y, sourceMG.off.z)
    ...
    ...
    

    Cheers



  • @r_gigante Hi Riccardo! I'm very happy to get your input into this matter and thank you for the mirroring code at its essence.

    There is a major part of my issue that I'm still unsure how to resolve, however: even though it is useful to switch the mirroring plane to ZY, XY, XZ, I'm not able to mirror objects whose axes are inverted. This is common with character rigs so I need to account for it. I mention the Mirror Tool because it has Axes controls. I don't know how to do the matrix transformations for the differing axes along with the Mirror Plane and could really use your help.

    While the original code works when the axes are the same, using the mirroring technique with the provided code flips the objects in Y and continually adds to their P rotation values. The rig is using X+ controls with XYZ rotation order. I've included these controls as Scenario 2 in the attached scene file, along with other axis orientations & rotation orders one might see in a rig:
    Mirroring Different Axes.c4d

    How can I account for the rotated axes? Thank you.



  • Hi @blastframe, sorry for getting back with a bit of a delay but summer time is usually pretty busy.

    With regard on how to take in account the different axis, you can immediately see that for scenario 1 asking for Ml and Mg for both source and target object will return

    ----------------------------------------------------------------------------------------------------
    L_Leg_con+ | L_Leg_zero | Scenario 1 - HPB Z- (works) (global): 
    Matrix(v1: (1, 0, 0); v2: (0, 1, 0); v3: (0, 0, 1); off: (10, 15.171, 3.922))
    
    R_leg_con+ | R_leg_zero | Scenario 1 - HPB Z- (works) (global): 
    Matrix(v1: (1, 0, 0); v2: (0, 1, 0); v3: (0, 0, 1); off: (-10, 15.171, 3.922))
    ----------------------------------------------------------------------------------------------------
    

    Given this numbers you can immediately see that the frames are aligned and no changes in the mirroring code should be done

    Repeating the same with scenario 2 will return:

    ----------------------------------------------------------------------------------------------------
    L_Leg_con+ | L_Leg_zero | Scenario 2 - XYZ X+ (global): 
    Matrix(v1: (0, 0, -1); v2: (0, -1, 0); v3: (-1, 0, 0); off: (10, 5.171, 3.922))
    
    R_Leg_con+ | R_Leg_zero | Scenario 2 - XYZ X+ (global): 
    Matrix(v1: (0, 0, 1); v2: (0, 1, 0); v3: (-1, 0, 0); off: (-10, 5.171, 3.922))
    ----------------------------------------------------------------------------------------------------
    

    and here you see, comparing the global matrices that Z and Y components are inverted. Thus inverting the Z and Y components of the mirroring matrices will provide you with the correct transformation.

    Finally in scenario 3 checking again the frames will result in:

    ----------------------------------------------------------------------------------------------------
    L_Leg_con+ | L_Leg_zero | Scenario 3 - XYZ Z+/Z- (global): 
    Matrix(v1: (1, 0, 0); v2: (0, 1, 0); v3: (0, 0, 1); off: (10, -4.829, 3.922))
    
    R_leg_con+ | R_leg_zero | Scenario 3 - XYZ Z+/Z- (global): 
    Matrix(v1: (-1, 0, 0); v2: (0, 1, 0); v3: (0, 0, -1); off: (-10, -4.829, 3.922))
    ----------------------------------------------------------------------------------------------------
    

    where the X and Z component are inverted. Thus inverting the X and Z components of the mirroring matrices will provide you again with the correct transformation.

    Again due to time constraint i can't come with full code, but once you got the logic won't be hard to implement it in the code.

    Finally consider that in the test project you've shared in the previous post in the scenario 3 , L_Leg_con+ and R_Leg_con+ both had rotation mode set to HPB instead of XYZ

    Best, R

    mirroring-different-axes_correct.c4d



  • @r_gigante Hi Riccardo! Thank you for your reply and for taking a look at the file. I understand about this being a busy time of year. I appreciate the support you and your team gives year-round. It's a special and well-appreciated service that you all provide, thank you!

    I think I have the problem narrowed down to one issue: can you offer any code help for determining & applying the axes' rotational differences to the mirrored values when they are at 0 values in position & rotation please?



  • Hi @r_gigante ,
    I am still working on this problem every day and could really use some help before the weekend if it's possible.

    I think I could just use help knowing how to get the difference between the axes in their default state. Could you please give me some advice on how to go about this? I am trying to apply the different axes' rotations to the mirrored matrices from @r_gigante 's code.

    Can you help me to get the right axis rotation values to apply to the mirrored values? Any help would be huge so I don't spin my wheels on this problem for another weekend. Thank you!



  • Hi @blastframe , I understand the need of help but actually the SDK Team needs to balance the support efforts among different topics/services. In this case, the support you're looking for goes behind the scope of the APIs and it's mostly related to the math behind it.
    When time has permitted we've always tried to be supportive even beyond the scope of the API providing snippets, scripts if not whole plugins. Unfortunately, this is not possible at this time but we'll try to keep this thread on our radars and hopefully to get back on it later.

    Best, Riccardo



  • @r_gigante Okay, thank you for the reply.



  • Hi,

    I have slightly updated my previous example for the case when the two frames are in a simple rotational relation around one of their standard basis vectors. As already stated, a general solution for this problem is much harder to accomplish and also not really a 'math' problem, but more a conceptual and algorithmic problem.

    There are also other ways to calculate the delta between your two frames if you are in the non-general case, which will have different advantages and disadvantages to the solution provided by me, but will also require certain guarantees regarding the source and target object and their frames to work properly (like for example having their vertices occupy the same points in world space).

    testing-the-script.c4d

    Cheers,
    zipit



  • @zipit That is very kind of you to revisit this topic, thank you! Also, very cool gizmos 👍

    I have considered allowing the user to choose how the axes are different themselves as you have in your example, but I really want to see if calculating the delta is possible first. The reason is that there could be many left & right object pairs to be mirrored that have different axis orientations. Can you think of any way to do this?

    For example, this is what I have tried:

    • L_Cube's, local rotation is (45°, 0°, 0°). Save this matrix, reset the rotation to (0°,0°,0°) locally, and get the global rotation (-90°, 180°, 0°). We save this matrix and reset back to (45°, 0°, 0°).
    • R_Cube's, local rotation is (0°, 45°, 0°). Save this matrix, reset the rotation to (0°,0°,0°) locally, and get the global rotation (90°, 0°, 0°). We save this matrix and reset back to (0°, 45°, 0°).
    • Mirror the values using your code
    • How can we then apply our global rotations that we got when the objects were reset: (-90°, 180°, 0°) and (90°, 0°, 0°) as corrections respectively?
    • In other words, how do we determine the inverted_result_axis and target_adjustment_rotation from your code based on these values?

    Here is what I have tried:

        l_diff_x = utils.Rad((-90+90)) #difference between x-axes
        l_diff_y = utils.Rad((180-0)) #difference between y-axes
        l_diff_z = utils.Rad((0-0)) #difference between z-axes
    
        r_diff_x = utils.Rad((90-90)) #difference between x-axes
        r_diff_y = utils.Rad((0-180)) #difference between y-axes
        r_diff_z = utils.Rad((0-0)) #difference between z-axes
    
        l_correction = utils.MatrixRotX(l_diff_x)
        l_correction = utils.MatrixRotY(l_diff_y)
        l_correction = utils.MatrixRotZ(l_diff_z)
    
        r_correction = utils.MatrixRotX(r_diff_x)
        r_correction = utils.MatrixRotY(r_diff_y)
        r_correction = utils.MatrixRotZ(r_diff_z)
        
        l_cube.SetMg(l_reflection * l_correction)
        r_cube.SetMg(r_reflection * r_correction)
    

    This makes sense to me but it doesn't work. There is something missing...putting the corrections into the objects' frames, or inverting them. I don't know; I have tried them to no avail.

    Thank you!



  • Hi,

    there are some fundamental problems with your code, but when I am trying to understand the intention of that code, you also seem to have overlooked the major prerequisite of your approach - It would require both objects to be topologically aligned. That was not the case in any of your example files.

    Imagine an object "source" that you have just duplicated ("target"), so that target "sits in the same place" as source. If you now would rotate the frame of target with Cinema's axis-mode thingy, then its points would occupy the same coordinates in world space as before, but their local coordinates would be different. You could simply compute the transform between the two frames then by correction = ~source.GetMg() * target.GetMg(). If however also target itself had been rotated (like it was the case in your files), then you cannot do this anymore, because there are now two sources of information that have been mixed: The orientation of the frame in respect to its vertices and the rotation of the frame in respect to the source. We do not have any clue how to untangle that (or more precisely - it is not so easy).

    If you do not want to to dial in an angle, but also cannot guarantee that the objects are topologically aligned, you could also compute the correction transform by letting the user choose a from-to-axis pair. In pseudo code (i.e. I have written this on my iPad):

    frm_source, frm_target = source.GetMg(), target.GetMg()
    # Rotate the source x-axis to the y-target axis.
    if user_choice is "x_source to y_target":
        # The two axis in question
        a, b = frm_source.v1, frm_target.v2
        # The normal to both axis, which is the axis of rotation. We take
        # the cross product and normalize it (normalization is technically 
        # not necessary, but better safe than sorry ;)
        nrm = ~(a % b)
        # The angle between both axis.
        theta = math.acos(~a * ~b)
        # We could construct the transform/matrix with that ourselves, but
        # why should we when Cinema has Quaternions for us.
        quat = c4d.Quaternion()
        quat.SetAxis(nrm, theta)
        # Get the rotation matrix for that Quaternion.
        transform = quat.GetMatrix()
    

    Cheers,
    zipit



  • @zipit Hello, thank you again for your help!

    You're right: the objects wouldn't be topologically aligned.

    I will have a look into the math you provided here, but I don't understand what the other user choice options would be or what determines an object as being "x_source" or "y_target." I couldn't get it working.

    In the meantime, I'm going to look into the solutions you proposed in your testing-the-script.c4d file again. Thank you 😃



  • @zipit I am finally making progress, thanks to your help! I have implemented the inverted_result_axis,
    target_adjustment_axis,target_adjustment_rotation and it's working. 😃 My concern now is that it's confusing to the user on how to get the desired output. I don't understand it all myself.

    Could you please help me to understand what Inverted Result Axis means? In your code it says:

    #inverted_result_axis (int): The axis to adjust in a reflected frame to make it conform with Cinema's left-handed matrices.
    

    What would cause the need to change the Inverted Result Axis value? If there's a way to determine this value based on reflection axis, I'd rather not surface this as an option to the user. I noticed when the axes were the same, on opposite sides of the ZY plane, Inverted Result Axis was X, and in my examples where Target Adjustment Axis was Y, the Inverted Result Axis was Y.

    Finally, if I'm able to reduce these options with the from-to-axis pair option, I'd be very interested if you would explain. As mentioned above, the pseudo code did not make sense to me enough to where I could build out the other user_choice options. Could you please explain the from-to-axis pair options and how this would work with your testing-the-script example?



  • Hi,

    inverted_result_axis is similar to the xy, xz, and yz option in Cinema's tool and related to what we disused regarding left-handed and right-handed matrices. Reflecting a left-handed matrix (i.e. a Cinema matrix) will always give you a right-handed matrix. To make that result conform with Cinema's matrix orientation again, you will have to flip one axis of the result.

    Actually you do not ;) Because like I have shown in my first script and mentioned in the post before, you can feed a right-handed frame into a c4d.Matrix constructor (or modify an existing matrix in such way). Cinema will then just silently flip some random axis in your matrix to make it conformant again (which is both a terrible workflow and API design IMHO).

    And I agree, the whole process is rather bloated regarding its options. You could technically remove the flipping option and either flip a fixed axis or leave the choice to Cinema, if you do not care about the orientation of the object. The from-to approach would not reduce the number of options, but replace the degree field by another drop-down selection menu. The idea behind this approach is to let the user select an axis in the source and then select an axis in the target, to which the axis in source should be rotated. This would imply the transform between both frames.

    The only way to cut down on options is , like it is almost always the case, to make your code smarter, i.e. go the route of what I did refer to as the hard way. One way to do this could be to try to choose or compute a characteristic vector for your internal point data (i.e. the vertices attached to it) for both objects and then construct a quaternion for each object with these vectors and common arbitrary vector (does not really matter what vector). With that you could rotate the objects in and out an identical neutral orientation (for the lack of a better description). But that is only a rough outline, I would have actually try this myself and I am not even sure if this will work.

    Cheers,
    zipit



  • @zipit Thank you for all of the the replies and explanations!

    I think I'm going to have to go the options way. The idea I had was the one I explained above: getting the axes' differences and applying them to the target_adjustment_rotation. I might be able to make it work...let's see.

    The Quaternion method you described sounded promising but I wouldn't know how to do it based on your explanation.

    Can I connect with you somehow off of the forum? I'd like to send you something as a token of my gratitude for helping to get this feature working.



  • Hi,

    I am happy that I could help. It is very kind of you that you want to express your gratitude, but not necessary.

    Happy coding and rendering,
    zipit



  • @zipit I also want to say that the amount of time you contribute here to help out developers is very generous of you. You are doing an amazing job. I would hope that Maxon would actually pay you some retainer fee for your time or at least provide you with a free subscription for all the help you have given everyone here.


Log in to reply