In the previous part I promised we will learn more about the Metal shading language
. Before that, first let’s do some code cleaning and structuring since we are already getting into the habit of doing this from previous episodes. Start by downloading the source code from the previous episode. We want to refactor the huge render() function, to start with. So let’s take the vertex buffer and the render pipeline state outside of the function, and also create 3 new smaller functions, so that our old function reduces to this:
var vertex_buffer: MTLBuffer!
var rps: MTLRenderPipelineState! = nil
func render() {
device = MTLCreateSystemDefaultDevice()
createBuffer()
registerShaders()
sendToGPU()
}
For the createBuffer() function we need to first make some changes. Recall from last episode that vertex data was an array of type Float
like this:
let vertex_data:[Float] = [-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0]
Let’s transform it into a better suited format, a struct with two members of vector_float4
type, one for position and the other for color:
struct Vertex {
var position: vector_float4
var color: vector_float4
}
You might wonder what kind of a data type vector_float4 is. From Apple
’s documentation we find that the vector types are clang-based types that are better suited than traditional SIMD
types for vector-vector and vector-scalar arithmetic operations. It is possible to access vector components both via array-style subscripting, and by using the . operator with component names (x
, y
, z
, w
, or combinations thereof). Besides the .xyzw component names, the following sub-vectors can also be easily accessed: .lo / .hi (first half and second half of a vector), as well as the .even / .odd sub-vectors:
vector_float4 x = 1.0f; // x = { 1, 1, 1, 1 }.
vector_float3 y = { 1, 2, 3 }; // y = { 1, 2, 3 }.
x.xyz = y.zyx; // x = { 1/3, 1/2, 1, 1 }.
x.w = 0; // x = { 1/4, 1/3, 1/2, 0 }.
Let’s get back to createBuffer()
so we can change our vertex_data using the new struct
:
func createBuffer() {
let vertex_data = [Vertex(position: [-1.0, -1.0, 0.0, 1.0], color: [1, 0, 0, 1]),
Vertex(position: [ 1.0, -1.0, 0.0, 1.0], color: [0, 1, 0, 1]),
Vertex(position: [ 0.0, 1.0, 0.0, 1.0], color: [0, 0, 1, 1])]
vertex_buffer = device!.newBufferWithBytes(vertex_data, length: sizeof(Vertex) * 3, options:[])
}
You notice how handy is to have it as an array of structs where we can easily create vertexes in place. You also notice we kept the vertex positions as they were last time, and we added separate colors for each vertex (red, green and blue). Next up, is the registerShaders() function. We don’t change anything to the old code other than having it moved to this new place:
func registerShaders() {
let library = device!.newDefaultLibrary()!
let vertex_func = library.newFunctionWithName("vertex_func")
let frag_func = library.newFunctionWithName("fragment_func")
let rpld = MTLRenderPipelineDescriptor()
rpld.vertexFunction = vertex_func
rpld.fragmentFunction = frag_func
rpld.colorAttachments[0].pixelFormat = .BGRA8Unorm
do {
try rps = device!.newRenderPipelineStateWithDescriptor(rpld)
} catch let error {
self.print("\(error)")
}
}
And lastly, we do the same with the sendToGPU() function, nothing being changed to the old code other than having it moved to this new place:
func sendToGPU() {
if let rpd = currentRenderPassDescriptor, drawable = currentDrawable {
rpd.colorAttachments[0].clearColor = MTLClearColorMake(0.5, 0.5, 0.5, 1.0)
let command_buffer = device!.newCommandQueue().commandBuffer()
let command_encoder = command_buffer.renderCommandEncoderWithDescriptor(rpd)
command_encoder.setRenderPipelineState(rps)
command_encoder.setVertexBuffer(vertex_buffer, offset: 0, atIndex: 0)
command_encoder.drawPrimitives(.Triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
command_encoder.endEncoding()
command_buffer.presentDrawable(drawable)
command_buffer.commit()
}
}
Let’s move on to the Shaders.metal file next. We do two modifications here. First, we add a colormember to our Vertex
struct so we can pass it back and forth between the CPU
and the GPU
:
struct Vertex {
float4 position [[position]];
float4 color;
};
Second, we replace the hardcoded color we used last time in the fragment shader:
fragment float4 fragment_func(Vertex vert [[stage_in]]) {
return float4(0.7, 1, 1, 1);
}
with the actual color each vertex carries (sent to GPU
by the vertex_buffer):
fragment float4 fragment_func(Vertex vert [[stage_in]]) {
return vert.color;
}
If you run the app, you should now see a more nicely colored triangle:
You might be wondering why are the colors becoming gradients as we move away from the three vertexes we passed to the shaders? To understand this, it’s important to first understand the difference between the two shaders and their role in the graphics pipeline. Let’s look at the syntax for writing any shader (we choose the vertex shader as example):
vertex Vertex vertex_func(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]])
The first keyword is the function qualifier and can only have the value vertex, fragment or kernel. The next keyword is the return type. Next is the function name followed by the function arguments inside the parentheses. The Metal shading language
restricts the use of pointers unless the arguments are declared with the device, threadgroup, or constant address space qualifier which specifies the region of memory where a function variable or argument is allocated. The [[ … ]] syntax is used to declare attributes such as resource locations, shader inputs, and built-in variables that are passed back and forth between shaders and CPU.
Metal
uses the [[ buffer(index) ]] attribute to identify the location for the device
and constant buffer
argument types. Built-in input and output variables are used to communicate values between the graphics (vertex and fragment) functions and the fixed-function graphics pipeline stages. In our case [[ vertex_id ]] is the per-vertex identifier used in communication. Metal
generates the per-fragment inputs to a fragment function using the output from a vertex function and the fragments generated by the rasterizer. The per-fragment inputs are identified with the [[ stage_in ]] attribute qualifier.
The vertex shader
takes a pointer to the vertex list as the 1st parameter. We will be able to index into vertices using the 2nd parameter vid which is attributed with vertex_id that tells Metal
to insert the vertex index currently being processed as this parameter. We then simply pass along each vertex (with its position and color) for the fragment shader
to consume. All the fragment shader
does is to take the vertex passed from the vertex shader
and pass through the color for each and every pixel without changing anything to the input data. The vertex shader runs infrequently (only 3 times in this case – for each vertex), while the fragment shader
runs thousands of times – for each pixel it needs to draw.
So you might be still asking: “Ok, but what about the color gradients”? Well, now that you understand what each shader does and how often they run, you can think about the color at any given pixel as the average color value of its neighbors. For example, the color halfway between the the red
and the green
pixel will be yellow
simply because the fragment shader
interpolates the two colors by averaging them: 0.5 * red + 0.5 * green. The same happens with the color halfway between red
and blue
resulting magenta
, as well as halfway between blue
and green
resulting cyan
. From here on, the rest of the pixels are interpolated with unequal parts of the primary colors resulting the gradient range you see.
The source code is posted on Github as usual.
Until next time!