Chapters

Hide chapters

Metal by Tutorials

Fourth Edition · macOS 14, iOS 17 · Swift 5.9 · Xcode 15

Section I: Beginning Metal

Section 1: 10 chapters
Show chapters Hide chapters

Section II: Intermediate Metal

Section 2: 8 chapters
Show chapters Hide chapters

Section III: Advanced Metal

Section 3: 8 chapters
Show chapters Hide chapters

7. The Fragment Function
Written by Caroline Begbie & Marius Horga

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Knowing how to render triangles, lines and points by sending vertex data to the vertex function is a pretty neat skill to have — especially since you’re able to color the shapes using simple, one-line fragment functions. However, fragment shaders are capable of doing a lot more.

➤ Open the website https://shadertoy.com, where you’ll find a dizzying number of brilliant community-created shaders.

shadertoy.com examples
shadertoy.com examples

These examples may look like renderings of complex 3D models, but looks are deceiving! Every “model” you see here is entirely generated using mathematics, written in a GLSL fragment shader. GLSL is the Graphics Library Shading Language for OpenGL — and in this chapter, you’ll begin to understand the principles that all shading masters use.

Note: Every graphics API uses its own shader language. The principles are the same, so if you find a GLSL shader you like, you can recreate it in Metal’s MSL.

The Starter Project

The starter project shows an example of using multiple pipeline states with different vertex functions, depending on whether you render the rotating train or the full-screen quad.

➤ Open the starter project for this chapter.

➤ Build and run the project. (You can choose to render the train or the quad. You’ll start with the quad first.)

The starter project
The starter project

Let’s have a closer look at the code.

➤ Open Vertex.metal in the Shaders group, and you’ll see two vertex functions:

  • vertex_main: This function renders the train, just as it did in the previous chapter.
  • vertex_quad: This function renders the full-screen quad using an array defined in the shader.

Both functions output a VertexOut, containing only the vertex’s position.

➤ Open Renderer.swift.

In init(metalView:options:), you’ll see two pipeline state objects (PSOs). The only difference between the two PSOs is the vertex function the GPU will call when drawing.

Depending on the value of options.renderChoice, draw(in:) renders either the train model or the quad, swapping in the correct pipeline state. The SwiftUI views handle updating Options and MetalViewRepresentable passes the current option to Renderer.

➤ Ensure you understand how this project works before you continue.

Screen Space

One of the many things a fragment function can do is create complex patterns that fill the screen on a rendered quad. At the moment, the fragment function has only the interpolated position output from the vertex function available to it. So first, you’ll learn what you can do with this position and what its limitations are.

float color;
in.position.x < 200 ? color = 0 : color = 1;
return float4(color, color, color, 1);
MacBook Pro vs iPhone 15 Pro Max
XokTuak Mhe wl oXcege 67 Cfe Rap

typedef struct {
  uint width;
  uint height;
} Params;
var params = Params()
params.width = UInt32(size.width)
params.height = UInt32(size.height)
renderEncoder.setFragmentBytes(
  &params,
  length: MemoryLayout<Params>.stride,
  index: 12)
fragment float4 fragment_main(
  constant Params &params [[buffer(12)]],
  VertexOut in [[stage_in]])
in.position.x < params.width * 0.5 ? color = 0 : color = 1;
Corrected for retina devices
Tesyazliy waf soguri soririx

Metal Standard Library Functions

In addition to standard mathematical functions such as sin, abs and length, there are a few other useful functions. Let’s have a look.

step

step(edge, x) returns 0 if x is less than edge. Otherwise, it returns 1. This evaluation is exactly what you’re doing with your current fragment function.

float color = step(params.width * 0.5, in.position.x);
return float4(color, color, color, 1);
step
ztat

uint checks = 8;
// 1
float2 uv = in.position.xy / params.width;
// 2
uv = fract(uv * checks * 0.5) - 0.5;
// 3
float3 color = step(uv.x * uv.y, 0.0);
return float4(color, 1.0);
float2 uv = (550, 50) / 800;     // uv = (0.6875, 0.0625)
uv = fract(uv * checks * 0.5);   // uv = (0.75, 0.25)
uv -= 0.5; // uv = (0.25, -0.25)
float3 color = step(uv.x * uv.y, 0.0); // x > -0.0625, so color is 1
Checker board
Fnoljup kuisv

length

Creating squares is a lot of fun, but let’s create some circles using a length function.

float center = 0.5;
float radius = 0.2;
float2 uv = in.position.xy / params.width - center;
float3 color = step(length(uv), radius);
return float4(color, 1.0);
Circle
Kenwzo

smoothstep

smoothstep(edge0, edge1, x) returns a smooth Hermite interpolation between 0 and 1.

float color = smoothstep(0, params.width, in.position.x);
return float4(color, color, color, 1);
smoothstep gradient
sdoozsvbob kseruijw

mix

mix(x, y, a) produces the same result as x + (y - x) * a.

float3 red = float3(1, 0, 0);
float3 blue = float3(0, 0, 1);
float3 color = mix(red, blue, 0.6);
return float4(color, 1);
A blend between red and blue
I sbity poymooq fon uss cboe

float3 red = float3(1, 0, 0);
float3 blue = float3(0, 0, 1);
float result = smoothstep(0, params.width, in.position.x);
float3 color = mix(red, blue, result);
return float4(color, 1);
Combining smoothstep and mix
Nonjegewv jkiuczzded ozj qaj

normalize

The process of normalization means to rescale data to use a standard range. For example, a vector has both direction and magnitude. In the following image, vector A has a length of 2.12132 and a direction of 45 degrees. Vector B has the same length but a different direction. Vector C has a different length but the same direction.

Vectors
Hojnojk

return in.position;
Visualizing positions
Gugoufobaqr hibeqoasy

float3 color = normalize(in.position.xyz);
return float4(color, 1);
Normalized positions
Jadbepufuj pixutiuhm

Normals

Although visualizing positions is helpful for debugging, it’s not generally helpful in creating a 3D render. But, finding the direction a triangle faces is useful for shading, which is where normals come into play. Normals are vectors that represent the direction a vertex or surface is facing. In the next chapter, you’ll learn how to light your models. But first, you need to understand normals.

Vertex normals
Rirvav munlabf

var renderChoice = RenderChoice.train
Train render
Mdauf majjef

Loading the Train Model With Normals

3D model files generally contain surface normal values, and you can load these values with your model. If your file doesn’t contain surface normals, Model I/O can generate them on import using MDLMesh’s addNormals(withAttributeNamed:creaseThreshold:).

Adding Normals to the Vertex Descriptor

➤ Open VertexDescriptor.swift.

vertexDescriptor.attributes[1] = MDLVertexAttribute(
  name: MDLVertexAttributeNormal,
  format: .float3,
  offset: offset,
  bufferIndex: 0)
offset += MemoryLayout<float3>.stride

Updating the Shader Functions

➤ Open Vertex.metal.

float3 normal [[attribute(1)]];
float3 normal;
VertexOut out {
  .position = position,
  .normal = in.normal
};
return float4(in.normal, 1);

Adding a Header

It’s common to require structures and functions in multiple shader files. So, just as you did with the bridging header Common.h between Swift and Metal, you can add other header files and import them in the shader files.

#include <metal_stdlib>
using namespace metal;

struct VertexOut {
  float4 position [[position]];
  float3 normal;
};
#import "ShaderDefs.h"
#import "ShaderDefs.h"
Normals with rendering weirdness
Wossoxd curc gizcawoxl waejqhekk

Depth

The rasterizer doesn’t process depth order by default, so you need to give the rasterizer the information it needs with a depth stencil state.

metalView.depthStencilPixelFormat = .depth32Float
pipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
let depthStencilState: MTLDepthStencilState?
static func buildDepthStencilState() -> MTLDepthStencilState? {
// 1
  let descriptor = MTLDepthStencilDescriptor()
// 2
  descriptor.depthCompareFunction = .less
// 3
  descriptor.isDepthWriteEnabled = true
  return Renderer.device.makeDepthStencilState(
    descriptor: descriptor)
}
depthStencilState = Renderer.buildDepthStencilState()
renderEncoder.setDepthStencilState(depthStencilState)
Normals
Mulqirk

Normal colors along axes
Netgow gurutc igaqd evah

Hemispheric Lighting

Hemispheric lighting uses ambient light. With this type of lighting, half of the scene is lit with one color and the other half with another color. For example, the sphere in the following image uses Hemispheric lighting.

Hemispheric lighting
Cisakmbosin wagjkopz

float4 sky = float4(0.34, 0.9, 1.0, 1.0);
float4 earth = float4(0.29, 0.58, 0.2, 1.0);
float intensity = in.normal.y * 0.5 + 0.5;
return mix(earth, sky, intensity);
Hemispheric lighting
Kulufhsisat hoswseyr

Challenge

Currently, you’re using hard-coded magic numbers for all of the buffer indices and attributes. As your app grows, it’ll get increasingly difficult to keep track of these numbers. So, your challenge for this chapter is to hunt down all of those magic numbers and give them memorable names. For this challenge, you’ll create an enum in Common.h.

typedef enum {
  VertexBuffer = 0,
  UniformsBuffer = 11,
  ParamsBuffer = 12
} BufferIndices;
//Swift
encoder.setVertexBytes(
  &uniforms,
  length: MemoryLayout<Uniforms>.stride,
  index: Int(UniformsBuffer.rawValue))
// Shader Function
vertex VertexOut vertex_main(
  const VertexIn in [[stage_in]],
  constant Uniforms &uniforms [[buffer(UniformsBuffer)]])
extension BufferIndices {
  var index: Int {
    return Int(self.rawValue)
  }
}

Key Points

  • The fragment function is responsible for returning a color for each fragment that successfully passes through the rasterizer and the Stencil Test unit.
  • You have complete control over the color and can perform any math you choose.
  • You can pass parameters to the fragment function, such as current drawable size, camera position or vertex color.
  • You can use header files to define structures common to multiple Metal shader files.
  • Check the Metal Shading Language Specification at https://apple.co/3jDLQn4 for all of the MSL functions available in shader functions.
  • It’s easy to make the mistake of using a different buffer index in the vertex function than what you use in the renderer. Use descriptive enumerations for buffer indices.

Where to Go From Here?

This chapter touched the surface of what you can create in a fragment shader. A great place to start experimenting is using ideas from The Book of Shaders by Patricio Gonzalez.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now