Solved 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 a c4d.Vector(). Changing the vector's attributes (here x) works fine.

Then I try the same with a c4d.Matrix(). Here, it doesn't work. The line mat.off.x = 9.9 does not change the vector's x attribute. It doesn't raise an error either - it just ignores the value completely. To make changes to off, 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's Mul, while the other is MulV. 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 the maxon 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 and Vector.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

MAXON SDK Specialist
developers.maxon.net

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 of abc.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 doing mat.off.x = 9.9.

  1. The off getter function is called.
  2. 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.
  3. 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

  1. 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. And v.x will simply be not possible.

Regarding the doc of * and ^ it's true, I will add a note:

  • * does Matrix * Vector.
  • ^ does Matrix.sqmat * Vector.

Hope this answers your questions,
Cheers,
Maxime.

@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 :grin:

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 :grin:

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.