0
\$\begingroup\$

I've been trying to get rotations with quaternions to work for a while now, and I feel I am very close to it actually working but I just can't get it to work.

I do not have a view matrix yet and am just working in 2D space to make this slightly easier to set at the moment. So I'm only rotating around the Z axis.

My model matrix is calculated like this

var modelMatrix: matrix_float4x4 {
    var modelMatrix = matrix_identity_float4x4
        
    modelMatrix.translate(_position)
        
    modelMatrix.rotate(_orientation)
        
    modelMatrix.scale(_scale)
        
    return matrix_multiply(matrix_identity_float4x4, modelMatrix)
}

_orientation is defined as

private var _orientation: quaternion = quaternion(angle: toRadians(0), axis: float3(0, 0, 1))

quaternion is just a typedef of simd_quatf

my rotate function is

mutating func rotate(_ quat: quaternion) {
    let quatMatrix = matrix_float4x4(simd_normalize(quat))
        
    self = matrix_multiply(self, quatMatrix)
}

I also tried to do it with the .act method

mutating func rotate(_ quat: quaternion, _ vec: float3) {
    let rotatedVec = quat.act(vec)
        
    self.translate(rotatedVec)
}

This did the seemingly same thing as the first rotate function.

When I initialize _orientation with a rotation of 0, the program acts as normal but when I initialize it with 45, it warps the rectangle: Warped rect

and when I do it with 90, it produces the same result as with 0, which shouldn't happen. I'm assuming the problem lies somewhere in the way I'm applying the rotation to the object, but I have no clue what it is.

Edit:

Also A thought, maybe it is because of the way I am applying the modelMatrix to the shader positions. I'm pretty sure this is the right way but it might be slightly wrong.

vertex RastData vertexShader(const VertexIn vIn [[stage_in]],
                             constant ModelMat &modelMatrix [[buffer(1)]]) {
    RastData rd;
    
    rd.position = modelMatrix.modelMatrix * float4(vIn.position, 1.0);
    rd.color = vIn.color;
    
    return rd;
}

Edit 2: I Have tried to do it with the math described in this answer https://gamedev.stackexchange.com/a/188776/157187 but that did nothing

mutating func rotate(_ angle: Float, _ axis: float3, _ pQuat: quaternion) {
        let newQ = quaternion(vector: float4(axis, cos(angle / 2)))
        
        let newPQ = newQ * pQuat * newQ.conjugate
        
        let newVec = float3(newPQ.vector.x, newPQ.vector.y, newPQ.vector.z)
        
        var mat = matrix_identity_float4x4
        
        mat.columns = (
            float4(1, 0, 0, 0),
            float4(0, 1, 0, 0),
            float4(0, 0, 1, 0),
            float4(newVec.x, newVec.y, newVec.z, 1)
        )
        
        self = matrix_multiply(self, mat)
}

Edit 3: Alright I think the problem lies in my lack of a view and projection matrix, so I have defined some quick ones, not really implemented a camera system, just having the functionality of the view and projection matracies.

I have a function that makes an orthographic projection:

func makeOrthographicMatrix(tooManyArgsLol) { 
return float4x4(
        float4(2 / (right - left), 0, 0, 0),
        float4(0, 2 / (top - bottom), 0, 0),
        float4(0, 0, 1 / (far - near),   0),
        float4((left + right) / (left - right), (top + bottom) / (bottom - top), near / (near - far), 1)
    )
}

This uses the Column constructor so it looks weird but it's correct.

And a view matrix that should be -3 on the Z axis:

var viewMatrix: float4x4 {
        var viewMat = matrix_identity_float4x4
        
        viewMat.translate(float3(0, 0, -3))
        
        return viewMat
}

but now all I get is the clearColor I set.

In my vertex shader I do the multiplication like this

cameraMats.projectionMatrix * cameraMats.viewMatrix * modelMatrix.modelMatrix * float4(vIn.position, 1.0);

When I debug using the Metal debugger, it shows that all the data is going through to the shader correctly, so it's definitely some sort of math issue, I just don't really know where to look now.

\$\endgroup\$
13
  • \$\begingroup\$ Have you tried applying your scale before the rotation? \$\endgroup\$
    – DMGregory
    Commented Nov 4, 2021 at 18:13
  • \$\begingroup\$ Just tried that, same result. \$\endgroup\$ Commented Nov 4, 2021 at 18:19
  • \$\begingroup\$ The way the resulting picture looks, it seems the problem might appear because you apply the translation step before the rotation. If you think about how the operations work, if you translate first then rotate by the coordinate system's Z axis (i.e. not the rectangle's center since it is now translated away from the origin \$O = (0,0)\$) then it will rotate the whole rectangle around the origin \$O\$. It might be possible that this is what you want but I think in your case it's not what you are trying to achieve. \$\endgroup\$
    – PentaKon
    Commented Nov 4, 2021 at 19:22
  • \$\begingroup\$ How would I go about rotating around the center of the rectangle? I was thinking that if I give each object an orientation it would be rotated around itself, but from what you're saying that doesn't seem to be the case. I tried switching the order of the translation and rotation operations but all I got was each vertex moving around in a random pattern independent of eachother like the menu text in Persona 5. \$\endgroup\$ Commented Nov 4, 2021 at 19:46
  • \$\begingroup\$ Would I need a view matrix to do that? Because as of right now all I have is the raw vertices and the model matrix. \$\endgroup\$ Commented Nov 4, 2021 at 19:49

2 Answers 2

1
\$\begingroup\$

Alright so the main problem was actually that I had to projection matrix that changed depending on the aspect ratio of the window that was being rendered to. So with that out of the way I actually did end up getting quaternion rotations to work:

mutating func quatRotate(_ angle: Float, _ axis: float3) {
    var q = quaternion(angle: angle.toRadians, axis: axis)
        
    let qMat = float4x4(q.normalized)
        
    self = qMat * self
}

And use this before I translate like so:

var viewMatrix: float4x4 {
    var vm = matrix_identity_float4x4
        
    vm.quatRotate(_orientation.x * 50, XAXIS)
    vm.quatRotate(_orientation.y * 50, YAXIS)
    vm.quatRotate(_orientation.z * 50, ZAXIS)
        
    vm.translate(_position)
        
    return vm.inverse
}

The * 50 is because just using _orientation.angle was like a 1 pixel per second rotation speed lol.

Thank you to everyone who commented on this despite it being hot mess in the context of me actually knowing what was wrong! @DMGregory the goat I love you.

\$\endgroup\$
3
  • \$\begingroup\$ Two things to note: The orthogonal projection you mention in your question above has issues in the near_far calculations in column 3. It might not be apparent in 2D but it should be problematic in 3D. Second thing, the way you apply rotations in the view matrix seem to me like it might suffer from gimbal lock. These comments don't have anything to do with your specific question but you should keep them in mind if you build upon your current solution. \$\endgroup\$
    – PentaKon
    Commented Nov 9, 2021 at 12:28
  • \$\begingroup\$ How should I be storing my rotations to prevent against gimbal lock? Like one quaternion per axis? Or do it all in one which I haven't been able to figure out tbh. \$\endgroup\$ Commented Nov 9, 2021 at 15:10
  • \$\begingroup\$ When you want to a achieve a specific orientation and apply successive rotations on each basis axis, that's when gimbal lock can occur. What is generally suggested in that case is to do one rotation around an arbitrary axis (not one of the 3 standard axes) that achieves the same orientation. This rotation can be done using a rotation matrix from the link you posted above (gamedev.stackexchange.com/a/188776/157187). In your code you should do vm.quatRotate(θ, rotationAxis). \$\endgroup\$
    – PentaKon
    Commented Nov 9, 2021 at 16:08
0
\$\begingroup\$

I am not familiar with Metal's API so I can't be 100% sure on the usage correctness but mathematically what must happen is the following:

  1. Create a model matrix by applying scaling, rotation then translation, in that order (you are doing the reverse)
  2. Apply the resulting matrix to each point of the model (that is what the vertex shader does)

Code-wise this would look like:

var modelMatrix: matrix_float4x4 {
    var modelMatrix = matrix_identity_float4x4
    modelMatrix.scale(_scale)
    modelMatrix.rotate(_orientation)
    modelMatrix.translate(_position)
    return modelMatrix
}

Also the rotation method might need to change to

mutating func rotate(_ quat: quaternion) {
    let quatMatrix = matrix_float4x4(simd_normalize(quat))
    self = matrix_multiply(quatMatrix, self) // the rotation matrix must be multiplied from the LEFT
}

For the rotation, you must make sure that the quatMatrix value ends up being $$C=\begin{pmatrix} 0 & -1 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}.$$ or something very close to it (i.e. the 1x1 and 2x2 cells should be very small values). This is for clockwise 90 degree rotation.

As mentioned, I am not familiar with the API at all so the fixes I've suggested might be incorrect or not needed but that's the theory at least.

The vertex shader looks correct to me, again with assumptions on Metal's shader language.

Finally, remember that since vertex shaders output normalized device coordinates (that is, all coordinates range from \$[-1, 1]\$) you have to compensate for the viewport aspect ratio distortion. What this means is that if you have a square in the vertex shader for example with the top left corner at \$(-200,-200)\$ and bottom right at \$(200,200)\$, these must not be mapped to something like \$(-0.2, -0.2)\$ and \$(0.2, 0.2)\$. Instead the x coordinate must be divided by the viewport's aspect ratio \$(-0.2 / aspectRatio, -0.2)\$ in order for a square to be rendered on the screen. This can be easily done by applying an orthographic projection matrix after the model matrix (view this page https://en.wikipedia.org/wiki/Orthographic_projection on how to create it).

\$\endgroup\$
6
  • \$\begingroup\$ I made the changes you suggested and it seemed to be going well until the 1x1 and 2x2 cells were both 5.9ish, so I'm guessing I have some strange incorrect function call somewhere that is causing some goofyness. \$\endgroup\$ Commented Nov 5, 2021 at 14:38
  • \$\begingroup\$ I think they are 5.9E-8 which means that they are essentially 5.9 * 10^-8 = 0.000000059 ~ 0 \$\endgroup\$
    – PentaKon
    Commented Nov 5, 2021 at 14:41
  • \$\begingroup\$ ohhhhhhh yeah that makes more sense \$\endgroup\$ Commented Nov 5, 2021 at 14:47
  • \$\begingroup\$ @PentaKon Check the end of the comment thread on the question - it looks to me like this isn't an issue with the rotation at all, but with the projection of the object coordinates into normalized device coordinates. OP is not using a projection matrix to compensate for the aspect ratio of the window, so using a window wider than it is tall stretches the image sideways. Any advice you can offer to OP in that situation? \$\endgroup\$
    – DMGregory
    Commented Nov 6, 2021 at 13:52
  • \$\begingroup\$ @DMGregory Based on the above comment thread and if my understanding is correct, if your viewport's aspect ratio is not 1:1 (which based, on the blue background, it isn't) you must handle it in your projection matrix (which, based on this wiki page: en.wikipedia.org/wiki/Orthographic_projection, seems to have mistakes in its creation). I shall edit my answer tomorrow but the basic premise is that you have to divide the x coordinate of each vertex in your vertex shader by the viewport aspect ratio. This should give you a square in your screen. Hopefully DMGregory can confirm. \$\endgroup\$
    – PentaKon
    Commented Nov 8, 2021 at 0:13

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .