3D Affine Transformation Matrices (Metal Part 7)
Implementing transformation matrices to convert vertex positions from object space to world space, using the model matrix for transformations. Passing the matrix to the CPU using a buffer, and then doing the matrix multiplication for each vertex in the vertex shader function
Source Code
References
Table of Content
Translation
The Affine translation matrix allows to transform the position of the game object.
1mutating func translate(position: float3) { 2 var result = matrix_identity_float4x4 3 4 let x = position.x 5 let y = position.y 6 let z = position.z 7 8 result.columns = ( 9 float4(1, 0, 0, 0), 10 float4(0, 1, 0, 0), 11 float4(0, 0, 1, 0), 12 float4(x, y, z, 1) 13 ) 14 15 self = matrix_multiply(self, result) 16}
Scaling
The Affine scaling matrix allows to transform the scale of the game object.
1mutating func scale(scale: float3) { 2 var result = matrix_identity_float4x4 3 4 let x = scale.x 5 let y = scale.y 6 let z = scale.z 7 8 result.columns = ( 9 float4(x, 0, 0, 0), 10 float4(0, y, 0, 0), 11 float4(0, 0, z, 0), 12 float4(0, 0, 0, 1) 13 ) 14 15 self = matrix_multiply(self, result) 16}
Rotation
The rotation matrix depends on the axis of rotation and the angle of rotation.
These matrices introduce the gimbal lock problem, which is solved by using Quaternions for rotations, but for now this is enough.
1mutating func rotateX(angle: Float) { 2 var result = matrix_identity_float4x4 3 4 let s = sin(angle) 5 let c = cos(angle) 6 7 result.columns = ( 8 float4(1, 0, 0, 0), 9 float4(0, c, s, 0), 10 float4(0, -s, c, 0), 11 float4(0, 0, 0, 1) 12 ) 13 14 self = matrix_multiply(self, result) 15} 16 17mutating func rotateY(angle: Float) { 18 var result = matrix_identity_float4x4 19 20 let s = sin(angle) 21 let c = cos(angle) 22 23 result.columns = ( 24 float4(c, 0, -s, 0), 25 float4(0, 1, 0, 0), 26 float4(s, 0, c, 0), 27 float4(0, 0, 0, 1) 28 ) 29 30 self = matrix_multiply(self, result) 31} 32 33mutating func rotateZ(angle: Float) { 34 var result = matrix_identity_float4x4 35 36 let s = sin(angle) 37 let c = cos(angle) 38 39 result.columns = ( 40 float4(c, s, 0, 0), 41 float4(-s, c, 0, 0), 42 float4(0, 0, 1, 0), 43 float4(0, 0, 0, 1) 44 ) 45 46 self = matrix_multiply(self, result) 47}
Shader
The MeshRenderer now also keeps a transformation matrix, called the model matrix, to represent the transformations for the vertices, given the position, scale and rotation of the transform.
It also passes it down to the CPU so it can use it to transform the vertices.
The struct holding the model constants is passed by value, as bytes, in a buffer for the CPU.
Here we do updateModelConstants() as part of the rendering, it should be part of the update() stage for optimization purposes. For simplicity we will do it here, to avoid issues with order of execution of the components in the game object.
We will also need to take into consideration parent transforms in the future.
1class MeshRenderer : Component, Renderable { 2 3 private var _modelConstants: ModelConstants! = ModelConstants() 4 5 ... 6 7 func updateModelConstants() { 8 var modelMatrix: float4x4 = matrix_identity_float4x4 9 10 modelMatrix.translate(position: gameObject.position) 11 modelMatrix.scale(scale: gameObject.scale) 12 13 modelMatrix.rotateX(angle: gameObject.rotation.x) 14 modelMatrix.rotateY(angle: gameObject.rotation.y) 15 modelMatrix.rotateZ(angle: gameObject.rotation.z) 16 17 _modelConstants.modelMatrix = modelMatrix 18 } 19 20 func doRender(renderCommandEncoder: MTLRenderCommandEncoder) { 21 22 updateModelConstants() 23 24 // set the transformation matrix 25 renderCommandEncoder.setVertexBytes(&_modelConstants, length: ModelConstants.stride, index: 1) 26 27 renderCommandEncoder.setRenderPipelineState(RenderPipelineStateCache.getPipelineState(.Basic)) 28 renderCommandEncoder.setVertexBuffer(_vertexBuffer, offset: 0, index: 0) 29 renderCommandEncoder.drawPrimitives(type: MTLPrimitiveType.triangle, vertexStart: 0, vertexCount: _mesh.vertices.count) 30 } 31}
In the Shader code, we define the same structure, and we add a new parameter to the vertex function.
By doing a matrix multiplication between the model matrix and the vertex position in object space, we get the new transformed position of the vertex.
1struct ModelConstants { 2 float4x4 modelMatrix; 3}; 4 5vertex FragmentData basic_vertex_shader( 6 // metal can infer the data because we are describing it using the vertex descriptor 7 const VertexData IN [[ stage_in ]], 8 constant ModelConstants &modelConstants [[ buffer(1) ]] 9){ 10 FragmentData OUT; 11 12 // return the vertex position in homogeneous screen space 13 OUT.position = modelConstants.modelMatrix * float4(IN.position, 1); 14 15 OUT.color = IN.color; 16 17 return OUT; 18}
Result
Now the mesh translates, scales and rotates.