I want to get the same results mirroring two objects with either matching or different axes. For my examples, I have two objects, L_Cone & R_Cone, on opposite sizes of the ZY plane. Both of their local transformations are being reset by a parent Null (positions & rotations are 0, scale is 1)
In the first example, I have two objects whose axes are oriented differently (e.g. +X & -X on Z). I'm going to manipulate the transformations of R_Cone. I want to transfer this rotation to L_Cone so that it's mirrored on the ZY plane.
How can I code this transfer so that it gets the same result, regardless of the axis offset? In the first example with the different orientations, the position values are negated and the scale/rotation values are the same so it'd be as simple as:
lCone[c4d.ID_BASEOBJECT_ABS_POSITION] = -rCone[c4d.ID_BASEOBJECT_ABS_POSITION]
lCone[c4d.ID_BASEOBJECT_ABS_SCALE] = rCone[c4d.ID_BASEOBJECT_ABS_SCALE]
lcone[c4d.ID_BASEOBJECT_ABS_ROTATION] = rCone[c4d.ID_BASEOBJECT_ABS_ROTATION]
In the second example, however, that code wouldn't work because pos.X, rot.H, & rot.B are negated.
I thought I could get the difference between the Cones' Parents' Matrices and add that to the new values but it didn't work.
lMat = coneL.GetUpMg() # L_Cone's Parent Matrix
rMat = coneR.GetUpMg() # R_Cone's Parent Matrix
offsetMat = lMat-rMat # the offset between the two parents
coneL.SetMg(coneR.GetMg()+offsetMat) # the offset Matrix added to the R_Cone's Matrix
Could anyone please shed light on why this isn't working or how to do this another way?
if I understand you correctly, you simply want to reflect a frame (i.e. a c4d.Matrix) on an arbitrary plane of reflection. To do that you could:
The problem with that approach is that Cinema has a fixed frame orientation (matrices are always left-handed in Cinema) and the operation described above will invert the frame orientation of the input frame (i.e. in the case of Cinema produce a right-handed matrix). When constructing a c4d.Matrix in Cinema with axis components that form a right-handed frame, Cinema will silently rectify these inputs into a left-handed frame, changing the orientation of the frame on at least one axis. In some cases you might not mind, for example in the case of the y-axis in your picture, but in other cases this will result in an incorrect result, when the decision is left to Cinema.
The bigger insight here is that there is no solution for the reflection of a frame while preserving its frame orientation and scale. What you could do, is analyse your input matrix to find the axis that is "most parallel" to the plane of reflection to take control over which axis has to be inverted. This "most parallel" axis would be more likely less important and could therefor be inverted. You could also work with a negative scalings instead. But this is a hack in the broader sense of the term, because there is no guarantee that this assumption holds true.
The only way to faithfully reflect a point object, is to reflect each point individually.
Hi @zipit ,
Thank you very much for the reply. My apologies, but I could use some help with where to start on your advice. In the first approach you mentioned (the enumerated one), which picture did you mean when you said 'in the case of the y-axis in your picture'? You said the first approach is unreliable because of Cinema 4D's left-handed matrices. If this approach wouldn't work for both scenarios and will product incorrect results in some cases, I do not wish to pursue it.
For the second approach, you mentioned determining a most-parallel plane of reflection by the input matrix. Which matrix did you mean by the input matrix: the target's matrix (R_Cone in my example)? How would I go about analyzing the input matrix?
Finally, you mentioned the only faithful way to reflect a point object is to reflect each point individually. If that's the case, I'd be most interested in pursuing this approach. How would I go about this with the scenarios above (with R_Cone for L_Cone)? Not all of my objects will be point objects and, even if the points are flipped correctly, returning the offset to the axes would be necessary as well.
For a little more clarity, which of these approaches would you choose for this task?
sorry, I was probably a bit too abstract.
Hi @zipit ,
I'm sure it's more of a case that I don't know my linear algebra well enough I've taken some online courses in it this weekend to get a better understanding of the dot & cross product. I do understand the difference between left & right-handed matrices after reading your description and looking more into them, thank you.
You were right about the result of my second example image being off: there was an error in my the animation I created. L_Cone was using the axis of the first example. I have fixed it and replaced it in the original post. Sorry about that and very perceptive of you to notice! I appreciate your attention to my issue.
Regarding your last message's #7, I do care about the axes because I am using this with animation controllers that drive joints.
I came up with some code based on what I think you're describing with 'evaluate the angle between each axis and a normal of the reflection plane'. Am I right in using the objects' position in relation to the mirror plane's normal value? I got the same result for both axis orientation scenarios, so I'm not sure this is correct. Perhaps I should be evaluating their Matrices' v1, v2, & v3 vectors this way as well?
import c4d, math
from c4d import utils
mirrorPlane = c4d.Vector(1,0,0) #ZY's normal
rObj = doc.SearchObject("R_Cone").GetMg().off
lObj = doc.SearchObject("L_Cone").GetMg().off
mirNormMag = mirrorPlane.GetLength() #magnitude of the mirror plane's vector
rDotProd = mirrorPlane.Dot(rObj) #dot product of the mirror plane & right object
rMag = rObj.GetLength() #magnitude of the right object's position vector
rTheta = math.acos( rDotProd / (mirNormMag * rMag) ) #angle between two the two vectors
lDotProd = mirrorPlane.Dot(lObj) #dot product of the mirror plane & left object
lMag = lObj.GetLength() #magnitude of the left object's position vector
lTheta = math.acos( lDotProd / (mirNormMag * lMag) ) #angle between two the two vectors
# This is the result for both examples.
print "rTheta, rad: %s, deg: %s"%(rTheta,utils.RadToDeg(rTheta))
# rTheta, rad: 3.14159265359, deg: 180.0
print "lTheta, rad: %s, deg: %s"%(lTheta,utils.RadToDeg(lTheta))
# lTheta, rad: 0.0, deg: 0.0
I probably should have not mentioned that stuff, because it obviously side tracked you, what was not my intention. I suppose you wrote this code just for fun, which is cool, Trigonometry and Linear Algebra can be fun, but it has little to do with your problem. So I wrote a little snippet which should bring you back on track. I also created a little file  which demonstrates said snippet, because the setup is a bit complicated. Below you will also find the code in case you run into problems with the file (which was created with an educational license).
And finally two cents about your code. You got the major gist of the dot product, but normally you simply compute the unit vectors before computing the dot product if you are interested in the angle these vectors span. Your solution of adjusting the dot product afterwards is not wrong, but a bit convoluted in my opinion. You usually also do not compute the inverse cosine of a dot product if you are only interested in their angular relation and not the specific angle they span (it just is an unnecessary and relatively expensive computation) . For unit vectors the dot product already has very nice properties. Parallel unit vectors will have a dot product of 1, anti-parallel (parallel, but pointing in different directions) unit vectors a dot product of -1 and orthogonal unit vectors a dot product of 0.
""" Example for reflecting a frame (matrix).
This example will roughly do what you want (I am still not quite sure what
you exactly want to do). For this example to work, you will need.
1. A reflection plane. Any object will do, we just need the matrix.
2. An object to reflect, referred to in this script as "source".
3. An "reflected object", i.e. something to write the result to, referred
to in this script as "target".
4. A Python tag to put this script into.
The Python tag will need four user data elements:
ID 1 - A link to the reflection plane
ID 2 - A link to the source object
ID 3 - A link to the target object
ID 4 - The reflection axis vector. This should be a vector clamped to
the interval [-1, 1] with a step of 2 (i.e. it can only take
on the values -1 and 1 on each component).
I did not address any shenanigans regarding how Cinema interprets the
computed frame when constructing a matrix. This would be another topic.
def reflect_frame(source, plane, axis):
""" Reflects a frame on a reflection plane.
Reflects the frame "source" on the reflection plane defined by the
frame "plane" on the axis defined by the vector "axis."
source (c4d.Matrix): The frame to reflect.
plane (c4d.Matrix): The frame of the reflection plane.
axis (c4d.Vector): The reflection axis. See the code for details.
c4d.Matrix: The reflected frame.
# We do not want the reflection plane to apply any translation
# or scaling, i.e. we are only interested in its normalized
# orientation. So we remove anything except the orientation information.
# I took care in the Cinema file that this can not happen in the first
# place, so this is kind of redundant here.
plane.off = c4d.Vector()
plane.v1 = plane.v1.GetNormalized()
plane.v2 = plane.v2.GetNormalized()
plane.v3 = plane.v3.GetNormalized()
# Now we express the offset in the frame of the reflection plane.
pl = source.off * ~plane
# Now we express the three axis of our source as global points (e.g.
# source.off + source.v1) and then express these global points in the
# frame of our reflection plane.
xl = (source.off + source.v1) * ~plane
yl = (source.off + source.v2) * ~plane
zl = (source.off + source.v3) * ~plane
# Now we invert the component(s) we want to reflect by multiplying our
# points expressed in the plane of reflection componentwise by the
# reflection axis - which I did express as a signed vector
# (e.g.: (10, 10, 10) ^ (-1, 1, 1) = (-10, 10, 10) - a reflection on
# the x-axis). After that we express these points in the world frame
# again. These four components are now almost our finished frame.
# The offset, this is the final value.
off = (pl ^ axis) * plane
# The three axis, which are still points in world space.
x = (xl ^ axis) * plane
y = (yl ^ axis) * plane
z = (zl ^ axis) * plane
# Finally we localize our axis components again in respect to their
# origin and normalize them and reinstate their original scaling.
x = (x - off).GetNormalized() * source.v1.GetLength()
y = (y - off).GetNormalized() * source.v2.GetLength()
z = (z - off).GetNormalized() * source.v3.GetLength()
# The final frame converted into a Cinema matrix.
mg = c4d.Matrix(off, x, y, z)
# The input user data.
plane = op[c4d.ID_USERDATA, 1]
source = op[c4d.ID_USERDATA, 2]
target = op[c4d.ID_USERDATA, 3]
axis = op[c4d.ID_USERDATA, 4]
# The user data are not fully populated.
if None in (plane, source, target, axis):
raise ValueError("User data input values are not fully populated.")
# Compute the reflected frame.
mg = reflect_frame(source.GetMg(), plane.GetMg(), axis)
# Set the world matrix of the target object.
 The file: frame_reflection.c4d
Hi @zipit ,
I am so grateful for you taking the time to put this together, thank you! It is really cool that you can rotate the mirror plane. I never expected that, but it's a cool feature. The script I wrote wasn't for fun, but in response to when you wrote 'evaluate the angle between each axis and a normal of the reflection plane to determine which axis would be the least painful to invert.' I didn't fully understand the math, I was just trying to follow my understanding of your advice.
Your example works great with the second scenario from my original post where the axes are the same, but when I rotate the axis of the "source (transform me)" cone (mimicking the first scenario from my original post), it doesn't work anymore because, I believe, it's rotating the axis of the target. I do not wish to affect the axes in any way, but rather position & rotate the objects into the mirrored state. Being able to do this with different axes orientations is essentially what I am seeking.
In response to the comment in your code "(I am still not quite sure what you exactly want to do)", I am sorry if I have not made my intentions clear enough. I will try to explain once more in different terms, and please let me know if anything is unclear. Thank you for your patience and perseverance:
Here is the demo file you sent me with the other scenario for which I need to account (the objects are also parented under nulls):
I need to create a programmatic solution for this as I cannot use the Mirror Tool for multiple objects. It only accepts one Source and Target object at a time and I will eventually use this for multiple control pairs. Right now, in the case of this forum example, I'd just like to get this behavior working with one pair of objects at a time (once with matching axes and once with different axes).
Please let me know if you need me to explain anything any more.
I am truly grateful for your help!
I am not at home anymore, so I cannot look at your file right now, but your explanation was indeed a bit enlightening especially regarding "In some cases, their axes will match. In others they might differ, so I want to account for this." part. As I already stated in my first post, it confused the heck out of me that the axes were all over the place in your pictures in the first posting.
If you have two objects which are topologically identical, but have a different frame and you want to "reflect" one object in respect to the other and a reflection plane, you would first have to compute a transform which transforms the frame of the one object to the other in respect to their topology. Cannot say much more without having a better look at the problem and your file myself.
I will probably take a look at your file tomorrow and I will probably also have to take a look at the character mirror tool, because I am completely clueless when it comes to rigging. Maybe MAXON will chime in and tell you what the character tool does under the hood or someone else already knows this specific problem.
@zipit Thank you for the replies and I'm happy that has explained things a little better. Yes, hopefully the MAXON SDK team can speak to getting the mirroring working with different axes in Python similar to their Mirror Tool. Have a good rest of your weekend and thank you again.
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.
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:
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.
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)
@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
@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.
@r_gigante Okay, thank you for the reply.
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).
@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:
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.
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).
correction = ~source.GetMg() * target.GetMg()
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()
# Get the rotation matrix for that Quaternion.
transform = quat.GetMatrix()