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:
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’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:
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 (
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:
Let’s get back to
createBuffer() so we can change our vertex_data using the new
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:
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:
Let’s move on to the Shaders.metal file next. We do two modifications here. First, we add a color member to our
Vertex struct so we can pass it back and forth between the
CPU and the
Second, we replace the hardcoded color we used last time in the fragment shader:
with the actual color each vertex carries (sent to
GPU by the vertex_buffer):
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):
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
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.
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
magenta, as well as halfway between
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!