3

For reasons that are more complex than this minimal testcase, I need to have a entity (childEntity, the magenta box) child of another entity (parentEntity, the cyan box), but childEntity should be independent of parentEntity's transform.

Therefore I add this handler:

QtQuick.Connections {
    target: parentTransform
    onMatrixChanged: {
        // cancel parent's transform
        var m = parentTransform.matrix
        var i = m.inverted()
        childTransform.matrix = i

        // debug:
        console.log(parentTransform.matrix.times(i))
    }
}

which works well for cancelling out parent's translation and rotation, but not for scale.

When parent's scale3D is not [1,1,1] and rotation is also set, then childEntity appears distorted, despite the product of parentTransform.matrix times childTransform.matrix gives the 4x4 identity. Why?

Screenshot

Minimal testcase: (load into a QQuickView)

import QtQml 2.12 as QtQml
import QtQuick 2.12 as QtQuick
import QtQuick.Controls 2.12 as QtQuickControls
import QtQuick.Scene3D 2.0

import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0

Scene3D {
    function change_translation_and_rotation() {
        parentTransform.translation.x = 0.1
        parentTransform.translation.y = 0.5
        parentTransform.translation.z = 2
        parentTransform.rotationX = 30
        parentTransform.rotationY = 60
        parentTransform.rotationZ = 10
    }

    function change_rotation_and_scale() {
        parentTransform.rotationX = 30
        parentTransform.rotationY = 60
        parentTransform.rotationZ = 10
        parentTransform.scale3D.x = 0.1
        parentTransform.scale3D.y = 0.5
        parentTransform.scale3D.z = 2
    }

    function reset_transform() {
        parentTransform.translation.x = -0.5
        parentTransform.translation.y = 0
        parentTransform.translation.z = 0.5
        parentTransform.rotationX = 0
        parentTransform.rotationY = 0
        parentTransform.rotationZ = 0
        parentTransform.scale3D.x = 1
        parentTransform.scale3D.y = 1
        parentTransform.scale3D.z = 1
    }

    data: [
        QtQml.Connections {
            target: parentTransform
            onMatrixChanged: {
                // cancel parent's transform
                var m = parentTransform.matrix
                var i = m.inverted()
                childTransform.matrix = i

                // debug:
                console.log(parentTransform.matrix.times(i))
            }
        },

        QtQuick.Column {
            spacing: 5
            QtQuick.Repeater {
                id: buttons
                model: ["change_translation_and_rotation", "change_rotation_and_scale", "reset_transform"]
                delegate: QtQuickControls.Button {
                    text: modelData.replace(/_/g, ' ')
                    font.bold: focus
                    onClicked: {focus = true; scene3d[modelData]()}
                }
            }
        }
    ]

    id: scene3d
    anchors.fill: parent
    aspects: ["render", "logic", "input"]

    Entity {
        id: root
        components: [RenderSettings {activeFrameGraph: ForwardRenderer {camera: mainCamera}}, InputSettings {}]

        Camera {
            id: mainCamera
            projectionType: CameraLens.PerspectiveProjection
            fieldOfView: 45
            aspectRatio: 16/9
            nearPlane : 0.1
            farPlane : 1000.0
            position: Qt.vector3d(-3.46902, 4.49373, -3.78577)
            upVector: Qt.vector3d(0.41477, 0.789346, 0.452641)
            viewCenter: Qt.vector3d(0.0, 0.5, 0.0)
        }

        OrbitCameraController {
            camera: mainCamera
        }

        Entity {
            id: parentEntity
            components: [
                CuboidMesh {
                    xExtent: 1
                    yExtent: 1
                    zExtent: 1
                },
                PhongMaterial {
                    ambient: "#6cc"
                },
                Transform {
                    id: parentTransform
                    translation: Qt.vector3d(-0.5, 0, 0.5)
                }
            ]

            Entity {
                id: childEntity
                components: [
                    CuboidMesh {
                        xExtent: 0.5
                        yExtent: 0.5
                        zExtent: 0.5
                    },
                    PhongMaterial {
                        ambient: "#c6c"
                    },
                    Transform {
                        id: childTransform
                        translation: Qt.vector3d(-0.5, 0, 0.5)
                    }
                ]
            }
        }

        QtQuick.Component.onCompleted: reset_transform()
    }
}
fferri
  • 18,285
  • 5
  • 46
  • 95

1 Answers1

3

The problem is that the QTransform node does not store the transformation as a general 4x4 matrix. Rather is decomposes the matrix into a 3 transformations that are applied in fixed order:

  • S - a diagonal scaling matrix
  • R - the rotation matrix
  • T - translation

and then applies it in the order T * R * S * X to a point X.

The documentation for the matrix property writes about this decomposition step https://doc.qt.io/qt-5/qt3dcore-qtransform.html#matrix-prop

So when the transformation on the parent is M = T * R * S, then the inverse on the child will be M^-1 = S^-1 * R^-1 * T^-1. Setting the inverse on the child QTransform will attempt to decompose it in the same way:

M^-1 = T_i * R_i * S_i = S^-1 * R^-1 * T^-1

That doesn't work, because particularly S and R don't commute like this.

You can test this in your code by comparing the value of childTransform.matrix and i after you set the childTransform.matrix.

I think the only solution is to rather have 3 QTransforms on entities nested above the child to implement the correct order of the inverses as S^-1 * R^-1 * T^-1.

It is pretty simple to compute the inverse S^-1, R^-1 T^-1 from the corresponding parameters in the parent QTransform.

Gerhard
  • 41
  • 4
  • If I have 3 extra nested entities like this: `e0`, `e1`, `e2` from parent to child, then which one should have `S^-1`? Which `R^-1`? Which `T^-1`? Just trying to double-check the correct order =) – Megidd Feb 26 '21 at 18:55
  • 1
    the entities need to nest, so e0 contains e1 contains e2. I think the right order would then be: e0 has S^-1 e1 has R^-1 = R^T e2 has T^-1 = -T – Gerhard Mar 09 '21 at 00:50