0

I want to extract the three components of a simd_float4x4 transform matrix without engine-specific helpers (such as RealityKit's Transform).

I already found the way to extract translation and scale, but I'm missing rotation.

The closest I found is simd_quatf(matrix), but it doesn't work when the transform matrix has a scale.

let transform = Transform(
    scale: simd_float3(1, 2, 3),
    rotation: simd_quatf(angle: 0.5, axis: .init(0, 1, 0)),
    translation: simd_float3(10, 20, 30)
)
let matrix = transform.matrix


// Translation: OK
let translation = simd_float3(
    matrix.columns.3.x,
    matrix.columns.3.y,
    matrix.columns.3.z
)
print("Expected: \( transform.translation )")
print("Actual:   \( translation )")
// Expected: SIMD3<Float>(10.0, 20.0, 30.0)
// Actual:   SIMD3<Float>(10.0, 20.0, 30.0)


// Scale: OK
let scale = simd_float3(
    simd_length(simd_float3(matrix.columns.0.x, matrix.columns.0.y, matrix.columns.0.z)),
    simd_length(simd_float3(matrix.columns.1.x, matrix.columns.1.y, matrix.columns.1.z)),
    simd_length(simd_float3(matrix.columns.2.x, matrix.columns.2.y, matrix.columns.2.z))
)
print("Expected: \( transform.scale )")
print("Actual:   \( scale )")
// Expected: SIMD3<Float>(1.0, 2.0, 3.0)
// Actual:   SIMD3<Float>(1.0, 2.0, 3.0)


// Rotation: doesn't match
let rotation = simd_quatf(matrix)
print("Expected: \( transform.rotation )")
print("Actual:   \( rotation )")
// Expected: simd_quatf(real: 0.9689124, imag: SIMD3<Float>(0.0, 0.24740396, 0.0))
// Actual:   simd_quatf(real: 1.2757674, imag: SIMD3<Float>(0.0, 0.37579384, 0.0))

Update

I know it's possible to extract a rotation value because Transform is able to do it, I merely don't know how it's calculated internally.

In this example, both Transform extract the same rotation value despite the second Transform only has the matrix:

// Initialize from components
let transform1 = Transform(
    scale: simd_float3(10, 20, 30),
    rotation: simd_quatf(angle: 0.5, axis: .init(0, 1, 0)),
    translation: simd_float3(1, 2, 3)
)

// Initialize from composed matrix
let matrix = simd_float4x4([
    [8.7758255, 0.0, -4.7942553, 0.0],
    [0.0, 20.0, 0.0, 0.0],
    [14.382767, 0.0, 26.327477, 0.0],
    [1.0, 2.0, 3.0, 1.0]
])
let transform2 = Transform(matrix: matrix)

// Both extract the same value:
// simd_quatf(real: 0.9689124, imag: SIMD3<Float>(0.0, 0.24740396, 0.0))
print( transform1.rotation )
print( transform2.rotation )

// This doesn't extract the same value:
// simd_quatf(real: 3.745107, imag: SIMD3<Float>(0.0, 1.2801384, 0.0))
print( simd_quatf(matrix) )

// They don't match visually either
anchor1.orientation = transform1.rotation
anchor2.orientation = transform2.rotation    // same as anchor 1
anchor3.orientation = simd_quatf(matrix)     // different from anchor 1 & 2
wildpeaks
  • 7,273
  • 2
  • 30
  • 35
  • I'm not familiar with swift/swift-simd, but would it be enough to normalize the matrix columns before passing them to `simd_quatf`? You'd still have problems if your input transformation also includes a skew or if it was actually mirroring, but this seems not to be relevant in your case. – chtz Feb 13 '23 at 02:49
  • I already read your post and I'm sorry that you keep misunderstanding the question. – wildpeaks Feb 13 '23 at 16:06
  • For context (because quite a few comments have apparently been deleted), my reply wasn't pointed at chtz. – wildpeaks Feb 13 '23 at 23:24

2 Answers2

2

Matrix decomposition

Use the following scheme when decomposing RealityKit matrix4x4:

┌                   ┐
|   a   b   c   d   |
|   e   f   g   h   |
|   i   j   k   l   |
|   0   0   0   1   |
└                   ┘

This method allows you to decompose a ModelEntity's or AnchorEntity's matrix and get separately scale, orientation and position vectors.

import UIKit
import RealityKit

extension ViewController {
    
    func decomposingMatrixOf(_ entity: Entity) -> (scale: SIMD3<Float>,
                                             orientation: simd_quatf,
                                                position: SIMD3<Float>) {
        // SCALE EXTRACTION 
        // only positive scale is considered in this example
        let a = entity.transform.matrix.columns.0.x
        let e = entity.transform.matrix.columns.0.y
        let i = entity.transform.matrix.columns.0.z
        let b = entity.transform.matrix.columns.1.x
        let f = entity.transform.matrix.columns.1.y
        let j = entity.transform.matrix.columns.1.z
        let c = entity.transform.matrix.columns.2.x
        let g = entity.transform.matrix.columns.2.y
        let k = entity.transform.matrix.columns.2.z
        let xScale = sqrt((a * a) + (e * e) + (i * i))
        let yScale = sqrt((b * b) + (f * f) + (j * j))
        let zScale = sqrt((c * c) + (g * g) + (k * k))
        let scale = SIMD3<Float>(xScale, yScale, zScale)
        
        // ORIENTATION EXTRACTION
        let orientation = entity.transform.rotation
        
        // POSITION EXTRACTION
        let d = entity.transform.matrix.columns.3.x
        let h = entity.transform.matrix.columns.3.y
        let l = entity.transform.matrix.columns.3.z
        let position = SIMD3<Float>(d, h, l)
        
        return (scale, orientation, position)
    }
}

class ViewController: UIViewController {
    
    @IBOutlet var arView: ARView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var matrixS = simd_float4x4()
        matrixS.columns.0.x = 2.0
        matrixS.columns.1.y = 2.5
        matrixS.columns.2.z = 1.5

        let model = ModelEntity(mesh: .generateBox(size: 0.25))

        let rotationTransform = Transform(pitch: 0, yaw: .pi/6, roll: 0)
        print(rotationTransform.rotation)                        // 0

        // your formula is just a part of transform
        let matrixR = Transform(scale: .one,
                             rotation: simd_quatf(rotationTransform.matrix),
                          translation: .zero).matrix
                
        let matrixSR = simd_mul(matrixR, matrixS)
        model.transform.matrix = matrixSR
        model.position.z = -0.5
        
        let anchor = AnchorEntity()
        anchor.addChild(model)
        arView.scene.anchors.append(anchor)
        
        // Decomposing
        print( self.decomposingMatrixOf(model).scale )           // 1
        print( model.scale )                                     // 2
        print( self.decomposingMatrixOf(model).orientation )     // 3
        print( model.transform.rotation )                        // 4
        print( self.decomposingMatrixOf(model).position )        // 5
        print( model.position )                                  // 6
    }
}

Results in Console:

(real and imaginary parts of a constructed quaternion are consistent here)

simd_quatf(real: 0.9659258, imag: SIMD3<Float>(0.0, 0.258819, 0.0))     // 0
SIMD3<Float>(2.0, 2.5, 1.5)                                             // 1
SIMD3<Float>(2.0, 2.5, 1.5)                                             // 2
simd_quatf(real: 0.9659258, imag: SIMD3<Float>(0.0, 0.258819, 0.0))     // 3
simd_quatf(real: 0.9659258, imag: SIMD3<Float>(0.0, 0.258819, 0.0))     // 4
SIMD3<Float>(0.0, 0.0, -0.5)                                            // 5
SIMD3<Float>(0.0, 0.0, -0.5)                                            // 6


Rotation is a combination of scaling and shearing

simd_quatf.init(_ rotationMatrix: simd_float4x4) only takes into account entity's orientation and does not take into account its real scale and position. Thus, in order for the transform matrix to be complete, values of all 16 cells are needed.

The RealityKit's 4x4 matrix, like any other 4x4 simd matrix, does not contain PURE rotation values. Rotation is a product of shear and scale. Therefore, extracting, for example, the Y-rotation values ​​​​from the matrix, you'll get the data located in 4 cells of the matrix.

Moreover, the result of the rotation is calculated with the help of four trigonometric functions. Extracting the rotation values ​​with .init(real:imag:) gives you the following result. All rotations' calculations in RealityKit, ARKit and SceneKit are done in radians.

let transform = Transform(scale: SIMD3<Float>(1, 2, 3),
                       rotation: simd_quatf(angle: .pi/6, axis: [0, 1, 0]),
                    translation: SIMD3<Float>(10, 20, 30))

Resulted matrix:

┌                              ┐
|  0.86   0.00   1.49   10.00  |
|  0.00   2.00   0.00   20.00  |
| -0.49   0.00   2.59   30.00  |
|  0.00   0.00   0.00    1.00  |
└                              ┘

If you print transform.rotation you'll get the following result:

simd_quatf(real: 0.9659258, imag: SIMD3<Float>(0.0, 0.258819, 0.0))

When you print these sub-properties, you'll get initial values:

print(transform.rotation.angle)
print(transform.rotation.axis)

0.52359873                      //  Float.pi/6
SIMD3<Float>(0.0, 1.0, 0.0)     //  Y axis


An addition to the above

Here's what official Apple documentation says about var matrix: float4x4 { get set }

The Transform component can’t represent all transforms that a general 4x4 matrix can represent. Using a 4x4 matrix to set the transform is therefore a lossy event that might result in certain transformations, like shear, being dropped.

That's why your results of rotation-only values are inconsistent. Also, simd_quatf.init(_ rotationMatrix: simd_float4x4) ignores the last row and last column of a 4x4 matrix (i.e. we are passing a 3x3 matrix), hence we lose the Homogeneous Coordinates switch and translation values.

The formula you are trying to infer will always contain an inconsistent result. In RealityKit 2.0, there's no way, using just simd_quatf initializer, to generate a transform 4x4 matrix.

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
  • I appreciate the effort, but the question was about calculating the same value as `Transform.rotation` using only simd functions (in my example, `Transform` is only there to compare that the result matches). I even included the way to calculate `translation` and `scale` to illustrate the kind of code I was looking for, `Rotation: doesn't match` is the part that lacks the right formula. – wildpeaks Feb 13 '23 at 07:46
  • Also, my first reaction before posting the question was "even though the numbers are different, it might still be the same rotation because I know quaternions can express the same transformation multiple ways". Unfortunately it's not the case, the values are truly different: apply the two rotations values on different AnchorEntity and you'll see that "actual" and "expected" values don't match visually. – wildpeaks Feb 13 '23 at 08:18
  • 1
    Thanks for the effort, however the question is about replacing `simd_quatf(matrix)` by another formula (because it's the wrong formula), without using `Transform`. – wildpeaks Feb 13 '23 at 08:23
-2

Extension

This adds Transform-like properties to simd_float4x4.

extension simd_float4x4 {
    var translation: simd_float3 {
        return [columns.3.x, columns.3.y, columns.3.z]
    }

    var rotation: simd_quatf {
        // This would work only when scale is 1:
        // return simd_quatf(self)

        return Transform(matrix: self).rotation
    }

    var scale: simd_float3 {
        return [
            simd_length(simd_float3(columns.0.x, columns.0.y, columns.0.z)),
            simd_length(simd_float3(columns.1.x, columns.1.y, columns.1.z)),
            simd_length(simd_float3(columns.2.x, columns.2.y, columns.2.z)),
        ]
    }
}

Then you can use it directly from a matrix value:

let matrix = simd_float4x4( ... )

print( matrix.translation )
print( matrix.rotation )
print( matrix.scale )
wildpeaks
  • 7,273
  • 2
  • 30
  • 35
  • Because the question, as I stated several times now, was about calculating the translation/rotation/scale values out of a simd_float4x4 value without using Transform. This answer does exactly what I asked, further formatted as an extension to make it easy to use. I wish I could have had a pure SIMD solution for rotation as well, but in the meantime, at least it covers almost all cases. – wildpeaks Feb 13 '23 at 15:59