Harold Serrano

View Original

Rotating a 2D object using Metal

Now that you know the basics of Computer Graphics and how to set up the Metal API, it is time to learn about matrix transformations.

In this article, you are going to learn how to rotate a 2D object using the Metal API. In the process, you will learn about transformations, attributes, uniforms and their interaction with Metal Functions (Shaders).

Prerequisite:

Let's start off by reviewing Attributes, Uniforms, Transformations and Shaders.

Attributes

Elements that describe a mesh, such as vertices, are known as Attributes. For example, a square has four vertex attributes.

Attributes behave as inputs to a Vertex Function (Shader).

Uniforms

A Vertex Shader deals with constant and non-constant data. An Attribute is data that changes per-vertex, thus non-constant. Uniforms are data that are constant during a render pass.

Unlike attributes, uniforms can be received by both the Vertex and the Fragment functions (Shaders).

Transformations

In computer graphics, Matrices are used to transform the coordinate system of a mesh.

For example, to rotate a square 45 degrees about an axis requires a rotational matrix to transform the coordinate system of each vertex of the square.

In computer graphics, transformation matrices are usually sent as uniform data to the GPU.

Functions (Shaders)

The rendering pipeline contains two Shaders known as Vertex Shader and Fragment Shader. In simple terms, the Vertex Shader is in charge of transforming the space of the vertices. Whereas, the Fragment Shader is responsible for providing color to rasterized pixels. In Metal, Shaders are referred to as Functions.

To rotate an object; you provide the attributes of the model, along with the transformation uniform, to the vertex shader. With these two set of data, the Vertex Shader can transform the space of the square during a render pass.

Setting Up the Project

By now, you should know how to set up a Metal Application. If you do not, please read the prerequisite articles mentioned above. In this article, I will focus on setting up the attributes, updating the transformation uniform and the Shaders.

For your convenience, the project can be found here. Download the project so you can follow along.

Note: The project is found in the "rotatingASquare" git branch. The link should take you directly to that branch.

Let's start,

Open up the "ViewController.m" file.

Setting up the square vertices

The vertices representing our square object is defined as follows:

static float quadVertexData[] =
{
    0.5, -0.5, 0.0, 1.0,
    -0.5, -0.5, 0.0, 1.0,
    -0.5,  0.5, 0.0, 1.0,

    0.5,  0.5, 0.0, 1.0,
    0.5, -0.5, 0.0, 1.0,
    -0.5,  0.5, 0.0, 1.0
};

We will use these vertices as attribute input to the GPU.

Declaring Attribute and Uniform data

In Metal, An MTLBuffer represents an allocation of memory that can contain any type of data. We will use an MTLBuffer to represent attribute and uniform data as shown below:

//Attribute
id<MTLBuffer> vertexAttribute;

//Uniform
id<MTLBuffer> transformationUniform;

We will also declare a matrix variable representing our rotation matrix and a float variable representing the rotation angle.

//Matrix transformation
matrix_float4x4 rotationMatrix;

//rotation angle
float rotationAngle;

Loading attribute data into an MTLBuffer

To load data into an MTLBuffer, Metal provides a method called "newBufferWithBytes." We are going to load the square vertices into the vertexAttribute MTLBuffer as shown below. This is shown in the "viewDidLoad" method, Line 6.

//load the data attribute into the buffer
vertexAttribute=[mtlDevice newBufferWithBytes:quadVertexData length:sizeof(quadVertexData) options:MTLResourceOptionCPUCacheModeDefault];

Updating the rotation matrix

Now that we have the attributes stored in the buffer, we need to load the uniform data into its respective buffer. However, unlike the attribute data which is loaded once, we will continuously update the uniform buffer.

The project contains a method called "updateTransformation". The project calls the "updateTransformation" method before each render pass, and its purpose is to create a new rotation matrix depending on the value of "rotationAngle."

To make it a bit interactive, I set up the project to detect touch inputs. On every touch, the "rotationAngle" value is set to the touch x-coordinate value. Thus, as you touch your device, a new rotation matrix is created.

In the "updateTransformation" method, we are going to compute a new matrix rotation and load the rotation matrix into the uniform MTLBuffer as shown below:

//Update the rotation Transformation Matrix
rotationMatrix=matrix_from_rotation(rotationAngle*M_PI/180, 0.0, 0.0, 1.0);

//Update the Transformation Uniform
transformationUniform=[mtlDevice newBufferWithBytes:(void*)&rotationMatrix length:sizeof(rotationMatrix) options:MTLResourceOptionCPUCacheModeDefault];

Linking Resources to Shaders

The next step is to link the attribute and uniform MTLBuffer data to the shaders.

Metal provides a clean way to link resource data to the shaders by specifying an index value in the Render Command Encoder argument table and linking that value to the argument of shader function.

For example, the snippet below shows the attribute data being set at index 0.

//set the vertex buffer object and the index for the data
[renderEncoder setVertexBuffer:vertexAttribute offset:0 atIndex:0];

The shader vertex function, shown below, receives the attribute data by specifying in its argument the keyword "buffer()." In this instance, since we want to link the attribute data to index 0, we set the argument to "buffer(0)."

vertex float4 vertexShader(device float4 *vertices [[buffer(0)]], uint vid [[vertex_id]]){};

Ok, let's go back to our project.

We need to set the index value for our attribute and transformation uniform. To do so, we are going to use the "setVertexBuffer" method and provide an index value for each item. This is shown in the "renderPass" method starting at line 10.

//set the vertex buffer object and the index for the data
[renderEncoder setVertexBuffer:vertexAttribute offset:0 atIndex:0];

//set the uniform buffer and the index for the data
[renderEncoder setVertexBuffer:transformationUniform offset:0 atIndex:1];

Setting up the Shaders (Functions)

Open the "MyShader.metal" file.

Take a look at the function "vertexShader." Its argument receives the vertices at buffer 0 and the transformation at buffer 1. Note that the "transformation" parameter is a 4x4 matrix of type "float4x4."

vertex float4 vertexShader(device float4 *vertices [[buffer(0)]], constant float4x4 &transformation [[buffer(1)]], uint vid [[vertex_id]]){

    //Transform the vertices of the rectangle
    return transformation*vertices[vid];

}

Within the function, the vertices of the square get transformed by the "transformation" matrix.

The "fragmentShader" function is simple. It sets the rectangle to the color red.

fragment float4 fragmentShader(float4 in [[stage_in]]){

    //set color fragment to red
    return float4(1.0,0.0,0.0,1.0);

}

And that is it. Build the project and swipe your finger across the iOS device. The rectangle mesh should rotate as shown below:

Note: As of today, the iOS simulator does not support Metal. You need to connect your iPad or iPhone to run the project.

Next, I will teach you how to render 3D objects using Metal.

Hope this helps.