Matrix and Vector access
-
I could've sworn I have seen some kind of comment on this some time ago, but I can't find it any more, so maybe it was just in my perverted fantasy...
Access to the elements of a vector in a matrix doesn't seem to work as expected. Compare the following two attempts:
import c4d from c4d import gui class abc: def __init__(self): self.v = c4d.Vector(1.0) def main(): # works fine: abc1 = abc() print (abc1.v, abc1.v.x) abc1.v.x = 9.9 print (abc1.v, abc1.v.x) # does not work fine: mat = c4d.Matrix() print (mat.off, mat.off.x) mat.off.x = 9.9 print (mat.off, mat.off.x) if __name__=='__main__': main()
The test class
abc
contains ac4d.Vector()
. Changing the vector's attributes (herex
) works fine.Then I try the same with a
c4d.Matrix()
. Here, it doesn't work. The linemat.off.x = 9.9
does not change the vector'sx
attribute. It doesn't raise an error either - it just ignores the value completely. To make changes tooff
, I have to swap the complete vector against another one:mat.off = c4d.Vector(9.9, mat.off.y, mat.off.z)
or something like it.I assume that this behavior results from
Matrix
not being a true Python class, but a wrapper object for a C++ object. But I can't find any comment on that. I believe I even saw that kind of deep reference being used in one sample in the documentation, although I may have hallucinated that (chanterelles, I swear it was chanterelles).(While I'm at it: could someone amend the documentation on the Vector operators
*
and^
when the other operand is a matrix? The text does not mention that one is the equivalent to Matrix'sMul
, while the other isMulV
. Even the code samples are the same! Which is technically correct as the matrix used in that sample is a scale matrix with no translational component, but it's quite misleading.) -
Hi,
jeah, that is a bit odd about matrices. The reason for this happening (and why you might have seen the mentioned example) is probably that the classic API matrices are gone and have been replaced by just a
typedef
around themaxon
matrices which among other things actually handle the frame/basis of the transform differently by separating it into its own 3x3 matrix. The Python wrapper does not reflect that properly.Similar things apply to vectors and
Vector.Mul
andVector.MulV
are technically gone. The "proper current" methods overloading the operators are documented correctly in the Python docs (at least mostly). I was never a big fan of the fact that they did cram scalar multiplication, the scalar product and matrix multiplication into a single operator though. It can make code quite hard to read.Cheers,
zipit -
Hi @Cairyn thanks for the question, I would say here is due to the implementation.
In Python, everything is a reference so in your first example this is normal this is working fine since in
abc.v.x
the v component is a reference ofabc.v
instance.Now when it comes to CPython this up to the API designer (us) to decide what to do and more important what the referenced Python Object will really represent and how the real data will be held. In our implementation, we make the choice to implement vector and Matrix (and generally POD datatype) as a complete data allocated on the stack and not storing a pointer to the data.
We made the choice because overall it's safer and the only drawback is the one you are experiencing. But let me explain what happens behind the scene in your case when doingmat.off.x = 9.9
.- The
off
getter function is called. - This will create a new Python Vector object, but as we saw previously PODs are created on the stack, so it means it's also a totally new C++ Vector, not the real Vector hold by the Matrix, but a copy of the one hold by the Matrix.
- Then you assign a value to this copy of the original Vector without any luck to assign it back to the original holder (aka the matrix)
But you could tell me then, why returning Python Object that holds a new object and not a Python object that points to an existing object (as we do for BaseList2D). The question is correct and here we choose the safer approach because what will happen
- If you delete the Matrix but still want to access the vector? Like in the next example
v = c4d.Vector() if True: v = obj.GetMg().off v.x # This will not be possible
Most problematic even the next code will not work
v = obj.GetMg().off v.x # This will not be possible
Because GetMg (also in C++) returns a new Matrix allocated on the stack, then if we retrieve the
off
component that we store as a Vector* in our Python object. This pointed vector will be alive as long as the Matrix is alive, which in this case will live only the time of the line. Andv.x
will simply be not possible.Regarding the doc of
*
and^
it's true, I will add a note:*
doesMatrix * Vector
.^
doesMatrix.sqmat * Vector
.
Hope this answers your questions,
Cheers,
Maxime. - The
-
@zipit @m_adam Thank you for the confirmation. It is a bit unexpected that a dot operator returns a copy (when all involved objects are mutable).
Because GetMg (also in C++) returns a new Matrix allocated on the stack, then if we retrieve the off component that we store as a Vector* in our Python object. This pointed vector will be alive as long as the Matrix is alive, which in this case will live only the time of the line. And v.x will simply be not possible.
This is apparently a reasoning that works only because the object in question (the matrix) is referencing its subobjects (the vectors) with a pointer in a C++ data structure that does not provide a reference counter. In pure Python, the vector would be marked as referenced by the variable v and could not be garbage collected. In C++, the destructor of the matrix would also destroy the vectors, leaving no object that v could refer to. But this is happening on the C++ side - we're practically inheriting C++ issues indirectly
I probably haven't encountered these issues before because most access to inner values of a structure happen through functions instead of chained dot operators. ...But now you actually make me wonder how the Python/C++ interface avoids situations where the C++ side deletes objects that are referenced in a Python wrapper without notifying these, creating invalid pointers. The whole Python interface is, after all, a layer on top, not as deeply integrated as would be necessary to make use of Python abilities like ref counters. Or is it?
-
@Cairyn said in Matrix and Vector access:
But this is happening on the C++ side - we're practically inheriting C++ issues indirectly
Correct our Python API is just a layer on top of our C++ API so it of course has its same limitation.
But now you actually make me wonder how the Python/C++ interface avoids situations where the C++ side deletes objects that are referenced in a Python wrapper without notifying these, creating invalid pointers.
This is exactly the purpose of C4DAton.IsAlive and internally for each type that may be null each call do perform a check to see if the referenced c++ object is still alive.
The whole Python interface is, after all, a layer on top, not as deeply integrated as would be necessary to make use of Python abilities like ref counters. Or is it?
It depends, because in the case of
obj = c4d.BaseObject(c4d.Ocube)
then the owner is the Python object, so we have mechanisms to ensure that this obj is held by the PythonObject, and in many cases, we manage python object ref-count properly, so the GC doesn't delete thing people may not expect to be deleted or don't let alive object tool long (aka memory leak). But this is just the Python Object that represents a C++ object, so we have to be careful because if a Python object is deleted, the C++ object is not necessary also cleaned, it depends on who is the owner of the C++ object.Hope this answers your questions.
Cheers,
Maxime. -
hi @m_adam,
hope it's ok that i reply in this old thread.so the fastest way of doing
mg.off.x = 9.9
would be something like
off = mg.off off.x = 9.9 mg.off = off
or might there be a shorter/cleaner way?
with longer variable names and more variables the code can become a bit unclear ... -
Hey @datamilch,
Thank you for reaching out to us. In general we prefer if users open new threads for their questions. You are of course welcome to link to an existing topic and say that your question relates to that. The reason for that is that even when the question of another user is right on-topic as it is here the case with your question (which in the most other cases will not be true), it still tends to derail a thread. I have not forked your question since you are on-topic.
About your Question
I think this thread can be a bit opaque for beginners as @Cairyn and @m_adam have approached this topic on a very technical level.
The important thing to understand, is that both
BaseObject.GetMg()
and the fields of ac4d.Matrix
, i.e.,off
,v1
,v2
, andv3
return a copy of the requested object, not an instance. In plain terms this means that the matrix/vector one retrieves is not the one used by the object/matrix but a copy of it.So, we can totally do this:
>>> mg Matrix(v1: (1, 0, 0); v2: (0, 1, 0); v3: (0, 0, 1); off: (0, 0, 0)) >>> mg.off.x = 5
but it does not do what we think it would do, we changed the x-value of the returned copy, not the
off
vector used by themg
instance.>>> mg.off.x 0.0 >>> mg Matrix(v1: (1, 0, 0); v2: (0, 1, 0); v3: (0, 0, 1); off: (0, 0, 0))
But we can certainly set the x-component of the global offset of an object in one line.
import c4d cube: c4d.BaseObject = c4d.BaseObject(c4d.Ocube) # ONE: Using parameter access # We set the the x component of the global position and the global rotation using parameter access. # There are also parameters for the relative, aka local, transform values, as well as special things # like a frozen transform. There are also functions on BaseObject which do the same thing but at # least I never use them. # See: https://developers.maxon.net/docs/py/2024_3_0/classic_resource/base_list/obase.html cube[c4d.ID_BASEOBJECT_GLOBAL_POSITION,c4d.VECTOR_X] = 9.9 cube[c4d.ID_BASEOBJECT_GLOBAL_ROTATION] = c4d.Vector(0, 0, c4d.utils.DegToRad(45)) # TWO: Use matrix and vector constructors # In three steps ... mg: c4d.Matrix = cube.GetMg() mg.off = c4d.Vector(9.9, mg.off.y, mg.off.z) cube.SetMg(mg) # ... or in two steps, although this is a bit stupid. mg: c4d.Matrix = cube.GetMg() cube.SetMg(c4d.Matrix(c4d.Vector(9.9, mg.off.y, mg.off.z), mg.v1, mg.v2, mg.v3)) # THREE: Using transforms # This is not exactly the same as the two other cases, but it is the most common operation. Here we # do not set the global x-position of #cube to 9.9, but we move it 9.9 units in the x-direction. # In code we often operate on objects with the identity transform/matrix, i.e., which "sit at origin # with the standard size and orientation". In that case transforms act the same as if we would just # set these values. The other case is that you have an object at (1, 2, 3) and then move it (3, 2, 1) # units, transforms also work for this. For the case where the object is at (1, 2, 3) and you want # to _set_ it to (9.9, 2, 3) a transform won't work unless you do the math. # Freshly instantiated objects have the identity transform, we can just pile transforms on top of # that to "set" the values. sphere: c4d.BaseObject = c4d.BaseObject(c4d.Osphere) # Just move it 9.9 units in the x-direction. sphere.SetMg(sphere.GetMg() * c4d.utils.MatrixMove(c4d.Vector(9.9, 0, 0))) # Or first move it 9.9 units in the x-direction and then rotate it 45 degrees around the z-axis. This # would also "pile" onto the previous transform we have already applied. Note that matrix # multiplication is also not commutative, i.e., first moving something and then rotating it (might) # yield something different than first rotating and then moving. sphere.SetMg(sphere.GetMg() * c4d.utils.MatrixMove(c4d.Vector(9.9, 0, 0)) * c4d.utils.MatrixRotZ(c4d.utils.DegToRad(45)))
Cheers,
Ferdinand