Solved Nesting Pivot object causes parent to shoot up into space

Hello guys,

I have a python tag that sits on a plane old camera object. The tag has some user data controls for position, rotation etc. along with the option to use a custom pivot object (e.g. c4d.BaseObject(c4d.Onull) ) to rotate the camera around.

If no custom pivot object is supllied I use the world origin i.e. a unit matrix c4d.Matrix() as the pivot.

While this all works I noticed some odd behaviour when I nest the custom pivot object as a child object of the camera. Things start to get really weird then. I move the nested pivot object and the camera object starts flying all over the place.

I suspect that I either ran into some kind of logical limitation or that my math is not correct.

While I know that my question is probably way beyond support I still wonder if someone could be so kind and spare some time to help me out here.

Please find below the code I'm using along with the scene file.

Cheers,
Sebastian

Python_OrbitCamera.c4d

from typing import Optional
import c4d

doc: c4d.documents.BaseDocument # The document evaluating this tag
op: c4d.BaseTag # The Python scripting tag
flags: int # c4d.EXECUTIONFLAGS
priority: int # c4d.EXECUTIONPRIORITY
tp: Optional[c4d.modules.thinkingparticles.TP_MasterSystem] # Particle system
thread: Optional[c4d.threading.BaseThread] # The thread executing this tag

def main() -> None:
    # Called when the tag is executed. It can be called multiple time per frame. Similar to TagData.Execute.
    # Write your code here

    pivot = op[c4d.ID_USERDATA,1]

    pivot_rotation_heading = op[c4d.ID_USERDATA,3]
    pivot_rotation_pitch = op[c4d.ID_USERDATA,4]
    pivot_rotation_banking = op[c4d.ID_USERDATA,5]

    camera_position_x = op[c4d.ID_USERDATA,13]
    camera_position_y = op[c4d.ID_USERDATA,17]
    camera_position_z = op[c4d.ID_USERDATA,18]
    camera_rotation_heading = op[c4d.ID_USERDATA,6]
    camera_rotation_pitch = op[c4d.ID_USERDATA,7]
    camera_rotation_banking = op[c4d.ID_USERDATA,8]

    camera = op.GetObject()

    if not camera or not camera.CheckType(c4d.Ocamera):
        return

    # Get the global matrix of the pivot.
    # If there is no pivot object supplied use the unit matrix.
    pivot_matrix = pivot.GetMg() if pivot is not None else c4d.Matrix()

    # Construct a matrix to set our camera object to.
    camera_matrix = (

        pivot_matrix *
        c4d.utils.MatrixRotY(pivot_rotation_heading) *
        c4d.utils.MatrixRotX(pivot_rotation_pitch) *
        c4d.utils.MatrixRotZ(pivot_rotation_banking) *

        c4d.utils.MatrixMove(
            c4d.Vector(camera_position_x, camera_position_y, camera_position_z)
        ) *

        c4d.utils.MatrixRotY(camera_rotation_heading) *
        c4d.utils.MatrixRotX(camera_rotation_pitch) *
        c4d.utils.MatrixRotZ(camera_rotation_banking)
    )

    camera.SetMg(camera_matrix)

Hello @HerrMay,

What to do here depends a bit on what you want to achieve, if your first setup works for you, you could just use that.

I do not really understand why the pivot should be a child of the camera in the first place, because the camera is meant to be set to the transform of the pivot anyways (plus some extra steps I am ignoring here). So, they are already effectively in a parent-child relation.

In the end you must decide: Should the pivot lead the camera or vice versa? There is no technicality which can save you from that choice :)

If you want these two just to be bundled up, you could simply inverse the relation, which won't be a problem, because there you would simply enforce the parent child relation and would not have two contradicting operations, this feedback loop.

8f421a81-e36a-4f34-bcea-dc671d46f399-image.png

And in your code, you would then completely ignore the pivot thing, you would not even need the link field anymore, and simply operate in the local space of the parent to do your spiny thing:

Cheers,
Ferdinand

File: python_orbitcamera.c4d
Code:

from typing import Optional
import c4d

doc: c4d.documents.BaseDocument # The document evaluating this tag
op: c4d.BaseTag # The Python scripting tag
flags: int # c4d.EXECUTIONFLAGS
priority: int # c4d.EXECUTIONPRIORITY
tp: Optional[c4d.modules.thinkingparticles.TP_MasterSystem] # Particle system
thread: Optional[c4d.threading.BaseThread] # The thread executing this tag

def main() -> None:
    # Called when the tag is executed. It can be called multiple time per frame. Similar to TagData.Execute.
    # Write your code here

    pivot_rotation_heading = op[c4d.ID_USERDATA,3]
    pivot_rotation_pitch = op[c4d.ID_USERDATA,4]
    pivot_rotation_banking = op[c4d.ID_USERDATA,5]

    camera_position_x = op[c4d.ID_USERDATA,13]
    camera_position_y = op[c4d.ID_USERDATA,17]
    camera_position_z = op[c4d.ID_USERDATA,18]
    camera_rotation_heading = op[c4d.ID_USERDATA,6]
    camera_rotation_pitch = op[c4d.ID_USERDATA,7]
    camera_rotation_banking = op[c4d.ID_USERDATA,8]

    camera = op.GetObject()

    if not camera or not camera.CheckType(c4d.Ocamera):
        return

    # Construct a matrix to set our camera object to.
    camera_matrix = (
        c4d.utils.MatrixRotY(pivot_rotation_heading) *
        c4d.utils.MatrixRotX(pivot_rotation_pitch) *
        c4d.utils.MatrixRotZ(pivot_rotation_banking) *

        c4d.utils.MatrixMove(
            c4d.Vector(camera_position_x, camera_position_y, camera_position_z)
        ) *

        c4d.utils.MatrixRotY(camera_rotation_heading) *
        c4d.utils.MatrixRotX(camera_rotation_pitch) *
        c4d.utils.MatrixRotZ(camera_rotation_banking)
    )
    
    # Set the new camera mattrix in relation to its parent so that we follow the transform of the pivot.
    camera.SetMl(camera_matrix)

MAXON SDK Specialist
developers.maxon.net

Hello @HerrMay,

Thank you for reaching out to us. Let us look at your two setups:

3215fb76-a25e-4a79-970e-2518dac02af9-image.png

  • Setup that works fine: The transform of the pivot object is disjunct from the transform of the camera object.
  • Setup that does not work: The transform of the pivot object is not disjunct from the transform of the camera object.

In your code you then do this:

 pivot_matrix = pivot.GetMg() if pivot is not None else c4d.Matrix()
 camera_matrix = (
        pivot_matrix *
        ...
    )
    camera.SetMg(camera_matrix)

So, above we established that in your second setup the transform of pivot depends on the transform of camera. And here you make the transform of camera dependent on pivot, creating the feedback loop you consider weird.

The "working" solution is basically doing this:

pivot := {USER_VALUE}
camera = pivot + 42 

Output sequence for USER_VALUE: {0, 1, 2, 3} -> [42, 43, 44, 45]

While the other one is doing this:

camera := 0 
pivot  := camera + {USER_VALUE}
camera  = pivot + 42 

Output sequence for USER_VALUE {0, 1, 2, 3} -> [42, 85, 129, 174]

pivot is here dependent on camera and you have therefore this recursive quality, what you describe as weird. The second step computes for example as 42 (value from the last step) + 1(USER_VALUE) + 42 (the constant in the camera equation).

You can never use the value of a parameter x in an equation to drive the parameter x with an expression (i.e., tag), as you will give up the interpolation qualities of that parameter with that.

For some things like for example particle simulations, plugin authors make the deliberate choice to give up the interpolateability of a parameter, i.e., you cannot scrub the timeline to jump back and forth between particle states (without caching them) and instead must build a value one after the other based on its previous value.

That you consider your result weird (due to the unintended feedback loop) is only the minor problem. Worse is that you give up interpolateability, you cannot rewind things with this approach.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net

Hi @ferdinand,

as always - thanks for your explanation. Thats what I thought, I got myself in a race condition there. I feel more and more stupid with these questions I ask. :D

However, I wonder what could be a viable solution then? I mean, there must be a better way, right? As one simply can not stop a user from nesting the pivot object under the camera.

I hacked that rig together because I thought it would be nice to have a tag based solution for an orbit camera. Instead of always nesting various null objects to drive rotations and what not.

I thought about cloning that pivot into a virtual document and reading its matrix from there and use that as a pivot matrix. Didn't make much difference. The race condition was still there, though not as hefty.

Cheers,
Sebastian

Hello @HerrMay,

What to do here depends a bit on what you want to achieve, if your first setup works for you, you could just use that.

I do not really understand why the pivot should be a child of the camera in the first place, because the camera is meant to be set to the transform of the pivot anyways (plus some extra steps I am ignoring here). So, they are already effectively in a parent-child relation.

In the end you must decide: Should the pivot lead the camera or vice versa? There is no technicality which can save you from that choice :)

If you want these two just to be bundled up, you could simply inverse the relation, which won't be a problem, because there you would simply enforce the parent child relation and would not have two contradicting operations, this feedback loop.

8f421a81-e36a-4f34-bcea-dc671d46f399-image.png

And in your code, you would then completely ignore the pivot thing, you would not even need the link field anymore, and simply operate in the local space of the parent to do your spiny thing:

Cheers,
Ferdinand

File: python_orbitcamera.c4d
Code:

from typing import Optional
import c4d

doc: c4d.documents.BaseDocument # The document evaluating this tag
op: c4d.BaseTag # The Python scripting tag
flags: int # c4d.EXECUTIONFLAGS
priority: int # c4d.EXECUTIONPRIORITY
tp: Optional[c4d.modules.thinkingparticles.TP_MasterSystem] # Particle system
thread: Optional[c4d.threading.BaseThread] # The thread executing this tag

def main() -> None:
    # Called when the tag is executed. It can be called multiple time per frame. Similar to TagData.Execute.
    # Write your code here

    pivot_rotation_heading = op[c4d.ID_USERDATA,3]
    pivot_rotation_pitch = op[c4d.ID_USERDATA,4]
    pivot_rotation_banking = op[c4d.ID_USERDATA,5]

    camera_position_x = op[c4d.ID_USERDATA,13]
    camera_position_y = op[c4d.ID_USERDATA,17]
    camera_position_z = op[c4d.ID_USERDATA,18]
    camera_rotation_heading = op[c4d.ID_USERDATA,6]
    camera_rotation_pitch = op[c4d.ID_USERDATA,7]
    camera_rotation_banking = op[c4d.ID_USERDATA,8]

    camera = op.GetObject()

    if not camera or not camera.CheckType(c4d.Ocamera):
        return

    # Construct a matrix to set our camera object to.
    camera_matrix = (
        c4d.utils.MatrixRotY(pivot_rotation_heading) *
        c4d.utils.MatrixRotX(pivot_rotation_pitch) *
        c4d.utils.MatrixRotZ(pivot_rotation_banking) *

        c4d.utils.MatrixMove(
            c4d.Vector(camera_position_x, camera_position_y, camera_position_z)
        ) *

        c4d.utils.MatrixRotY(camera_rotation_heading) *
        c4d.utils.MatrixRotX(camera_rotation_pitch) *
        c4d.utils.MatrixRotZ(camera_rotation_banking)
    )
    
    # Set the new camera mattrix in relation to its parent so that we follow the transform of the pivot.
    camera.SetMl(camera_matrix)

MAXON SDK Specialist
developers.maxon.net

Hi @ferdinand,

Eureka! Of course, how simple. :D
This bevaiour is just what I wanted all long. Man, sometimes you don't see the hand in front of your face.

Thanks for helping me @ferdinand. You're simply golden.

P. S. I should really internalize these damn matrices already. :D

Cheers,
Sebastian

Hey @HerrMay,

Great to hear that you solved your problem.

Cheers,
Ferdinand

MAXON SDK Specialist
developers.maxon.net