Chapters

Hide chapters

Metal by Tutorials

Fifth Edition · macOS 26, iOS 26 · Swift 6, Metal 3 · Xcode 26

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

28. Geometry Creation with Mesh Shaders
Written by Marius Horga & Caroline Begbie

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

Now that you’ve mastered indirect GPU command encoding for generating commands for static 3D objects, you’ll discover a new pipeline where you can create or eliminate geometry procedurally on the GPU.

Grass is an ideal example. Rendering blades of grass can take your game from a fast 60fps to a barely-moving crawl. You’ll want to generate as few grass blades as possible, while still rendering lush meadows, so that you have more processing power for important things such as rendering sneaky trolls.

The Mesh Shader Pipeline

While the traditional vertex shader render pipeline is still good for standard 3D rendering, the newer mesh shader pipeline allows more fine-grained procedural geometry creation and geometry culling.

Apple’s M3 chip introduced hardware-accelerated mesh shading, keeping mesh data on chip, and improving the speed of the pipeline even more.

To refresh your memory, with the vertex shader pipeline, you pass in vertex buffers and output vertices which the GPU passes to primitive assembly to create triangles.

Blue indicates programmable functions, and pink indicates fixed GPU functions:

The vertex shader pipeline
The vertex shader pipeline

The rasterizer takes the triangles output from the vertex function and passes fragments to the fragment shader. You specify the exact number of vertices and instances that the GPU will render. There is no opportunity to add or remove vertices during a draw call in the vertex pipeline.

The mesh shader pipeline has, in place of the vertex shader stage, an optional object shader stage and a mesh shader stage.

The mesh shader pipeline
The mesh shader pipeline

You can pass in any data to the object function, such as camera data, scene data or textures. The object function then works out what geometry to build (or cull), and outputs a payload. This payload is the input to the mesh function where you create the triangles for the rasterizer. From there, the pipeline is the same as the vertex pipeline.

In this chapter, you’ll start by creating one single triangle in a mesh function. After that, you’ll generate grass blades in a tiled grid. This is where mesh shading shines. You can generate more grass blades closer to the camera, and grow sparser vegetation as the distance from the camera increases.

The Starter Project

The starter project for this chapter simply renders a triangle with three vertices using the standard vertex shader pipeline. There are three user options that load different render passes:

The starter project
Fqo crarwaz whutelw

Render a Triangle With a Mesh Shader

Rendering a triangle using a mesh shader is quite similar to using a compute shader. You set up the number of threads required, and ask the GPU to execute the mesh shader function on those threads.

The Mesh Shader Render Pass

➤ In the Render Passes folder, open MeshRenderPass.swift.

static func createMeshPSO()
  -> MTLRenderPipelineState {
  // 1
  let objectFunction: MTLFunction? = nil
  let meshFunction = Renderer.library.makeFunction(name: "mesh_main")
  let fragmentFunction = 
    Renderer.library.makeFunction(name: "fragment_main")
  // 2
  let pipelineDescriptor = MTLMeshRenderPipelineDescriptor()
  pipelineDescriptor.objectFunction = objectFunction
  pipelineDescriptor.meshFunction = meshFunction
  pipelineDescriptor.fragmentFunction = fragmentFunction
  pipelineDescriptor.colorAttachments[0].pixelFormat
    = Renderer.viewColorPixelFormat
  pipelineDescriptor.depthAttachmentPixelFormat = .depth32Float
  // 3
  let meshPSO: MTLRenderPipelineState
  do {
    (meshPSO, _) = try Renderer.device.makeRenderPipelineState(
    descriptor: pipelineDescriptor, options: [])
  } catch {
    fatalError("Mesh PSO not created \(error.localizedDescription)")
  }
  return meshPSO
}
pipelineState = PipelineStates.createMeshPSO()
// 1
let threadgroupsPerGrid = 
  MTLSize(width: 1, height: 1, depth: 1)
// 2
let threadsPerObjectThreadgroup = 
  MTLSize(width: 1, height: 1, depth: 1)
// 3
let threadsPerMeshThreadgroup = 
  MTLSize(width: 3, height: 1, depth: 1)
renderEncoder.drawMeshThreadgroups(
  threadgroupsPerGrid,
  threadsPerObjectThreadgroup: threadsPerObjectThreadgroup,
  threadsPerMeshThreadgroup: threadsPerMeshThreadgroup)

The Mesh Shader

➤ In the Shaders folder, open Shaders.metal.

using MeshTriangle = metal::mesh<
  VertexOut, void, 3, 1,
  metal::topology::triangle>;
[[mesh]] void mesh_main(
    MeshTriangle triangle,
    uint threadID[[thread_index_in_threadgroup]])
{
}
float4 positions[3] = {
    float4( 0.0,  0.5, 0.0, 1),
    float4(-0.5, -0.5, 0.0, 1),
    float4( 0.5, -0.5, 0.0, 1)
};

float4 colors[3] = {
    float4(1.0, 0.0, 0.0, 1.0),
    float4(0.0, 1.0, 0.0, 1.0),
    float4(0.0, 0.0, 1.0, 1.0)
};
// 1
if (threadID < 3) {
  // 2
  triangle.set_vertex(threadID, VertexOut {
    .position = positions[threadID],
    .color = colors[threadID]
  });
  // 3
  triangle.set_index(threadID, threadID);
}
thread 0: index 0: vertex: 0
thread 1: index 1: vertex: 1
thread 2: index 2: vertex: 2
if (threadID == 0) {
  triangle.set_primitive_count(1);
}
Mesh shader output
Bucy mmocoh uaznot

Procedural Grass Generation

Now that you know how to render one triangle, the next step is to procedurally generate many triangles for grass.

Rendering grass
Zaqseletf wdepl

Object to mesh threadgroups
Ujwuhy ma gekj lrvuezgbuadf

1. Set up the Pipeline State Object

➤ In the Render Passes folder, open GrassRenderPass.swift and check out the code.

let objectFunction = 
  Renderer.library.makeFunction(name: "object_grass")
pipelineState = PipelineStates.createGrassPSO()

2. Set up the Grass Input Data

➤ In the Shaders folder, open Common.h, and add these new structures before #endif /* Common_h */:

#define MaxBladesPerTile 8

typedef struct {
  float maxDistance;
  float bladeVertices;
  float gridSize;
  float tileSize;
} GrassSettings;

typedef struct {
  matrix_float4x4 viewProjectionMatrix;
  simd_float3 cameraPosition;
  simd_float3 cameraForward;
} GrassUniforms;

typedef struct {
  simd_float3 bladePositions[MaxBladesPerTile];
  uint32_t bladeCount;
  simd_uint3 tileID;
} GrassPayload;
let grassSettings = GrassSettings(
  maxDistance: 15,
  bladeVertices: 3,
  gridSize: 3,
  tileSize: 3)

3. The Draw

➤ In draw(commandBuffer:scene:uniforms:), replace // add code here, and bind the data to the object stage:

var grassUniforms = GrassUniforms()
let viewProjectionMatrix =
  scene.camera.projectionMatrix * scene.camera.viewMatrix
grassUniforms.viewProjectionMatrix = viewProjectionMatrix
grassUniforms.cameraPosition = scene.camera.position
grassUniforms.cameraForward = scene.camera.forwardVector
renderEncoder.setObjectBytes(
  &grassUniforms,
  length: MemoryLayout<GrassUniforms>.stride,
  index: GrassUniformsBuffer.index)

renderEncoder.setMeshBytes(
  &grassUniforms,
  length: MemoryLayout<GrassUniforms>.stride,
  index: GrassUniformsBuffer.index)

var grassSettings = grassSettings
renderEncoder.setObjectBytes(
  &grassSettings,
  length: MemoryLayout<GrassSettings>.stride,
  index: GrassSettingsBuffer.index)
// 1
let threadgroupsPerGrid = MTLSize(
  width: Int(grassSettings.gridSize),
  height: 1,
  depth: Int(grassSettings.gridSize))
// 2
let threadsPerTile =
  MTLSize(width: 1, height: 1, depth: 1)
// 3
let threadsPerBlade = MTLSize(
  width: Int(grassSettings.bladeVertices),
  height: 1,
  depth: 1)
renderEncoder.drawMeshThreadgroups(
  threadgroupsPerGrid,
  threadsPerObjectThreadgroup: threadsPerTile,
  threadsPerMeshThreadgroup: threadsPerBlade)

4. The Object Shader Function

➤ Create a new file in the Shaders folder, using the Metal File template, named GrassShaders.metal.

#import "Common.h"
#import "ShaderDefs.h"

[[object]]
void object_grass(
  uint3 objectID [[threadgroup_position_in_grid]],
  constant GrassUniforms& uniforms [[buffer(GrassUniformsBuffer)]],
  constant GrassSettings& settings [[buffer(GrassSettingsBuffer)]],
  object_data GrassPayload& payload [[payload]],
  mesh_grid_properties meshGridProperties)
{
}
pipelineDescriptor.payloadMemoryLength = 
  MemoryLayout<GrassPayload>.stride
float halfGrid = (settings.gridSize - 1.0) * 0.5;
float3 tileCenter = float3(
  (float(objectID.x) - halfGrid) * settings.tileSize,
  0.0,
  (float(objectID.z) - halfGrid) * settings.tileSize
);
float3 cameraToTile = normalize(tileCenter - uniforms.cameraPosition);
float3 cameraForward = normalize(uniforms.cameraForward);
if (dot(cameraForward, cameraToTile) < -0.4) {
  meshGridProperties.set_threadgroups_per_grid(0);
  return;
}
float distanceToCamera =
  length(uniforms.cameraPosition - tileCenter);
uint bladesInTile;
if (distanceToCamera < settings.maxDistance * 0.3) {
  bladesInTile = MaxBladesPerTile;
} else if (distanceToCamera < settings.maxDistance * 0.6) {
  bladesInTile = MaxBladesPerTile / 2;
} else if (distanceToCamera < settings.maxDistance) {
  bladesInTile = MaxBladesPerTile / 4;
} else {
  bladesInTile = 0;
}
if (bladesInTile == 0) {
  meshGridProperties.set_threadgroups_per_grid(0);
  return;
}
payload.bladeCount = bladesInTile;

for (uint i = 0; i < bladesInTile; i++) {
    float t = float(i) / float(bladesInTile - 1);
    float x = (t - 0.5) * settings.tileSize * 0.5;
    payload.bladePositions[i] = tileCenter + float3(x, 0.0, 0.0);
}

payload.tileID = objectID;
meshGridProperties.set_threadgroups_per_grid(
  uint3(bladesInTile, 1, 1));

5. The Mesh Shader Function

With the object shader function outputting a payload of grass blades, you can create the mesh shader.

using GrassMesh = metal::mesh<VertexOut, void, 3, 1, topology::triangle>;

[[mesh]]
void mesh_grass(
  uint meshID [[threadgroup_position_in_grid]],
  uint threadID [[thread_index_in_threadgroup]],
  const object_data GrassPayload& payload [[payload]],
  GrassMesh outputMesh,
  constant GrassUniforms& uniforms [[buffer(GrassUniformsBuffer)]])
{
}
float4 position;
float4 color = { 0, 0.5, 0, 1 };
switch (threadID) {
  case 0:   // top vertex
    position = { 0, 1, 0, 1 };
    color = { 0.5, 0.8, 0, 1};
    break;
  case 1:   // bottom left vertex
    position = { -0.2, 0, 0, 1 };
    break;
  case 2:   // bottom right vertex
    position = { 0.2, 0, 0, 1 };
    break;
}
position += float4(payload.bladePositions[meshID], 0);
if (threadID < 3) {
  outputMesh.set_vertex(threadID, VertexOut {
    .position = uniforms.viewProjectionMatrix * position,
    .color = color
  });
  
  outputMesh.set_index(threadID, threadID);
}
if (threadID == 0) {
  outputMesh.set_primitive_count(1);
}
First grass render
Pinjr xyenn fohtin

Culled tiles
Jotxuc cubit

Creating Randomness

The grass is standing around like soldiers in a row. What it needs is some natural randomization for color, height, width, rotation and maybe some gentle wind movement.

float simpleHash(float2 coords) {
  float h = dot(coords, float2(127.1, 311.7));
  return fract(sin(h) * 43758.5453);
}
for (uint i = 0; i < bladesInTile; i++) {
  float randX = simpleHash(float2(objectID.xz) + float2(i, 0));
  float randY = simpleHash(float2(objectID.xz) + float2(0, i));
  float offsetX = (randX - 0.5) * settings.tileSize;
  float offsetZ = (randY - 0.5) * settings.tileSize;
  float3 offset = float3(offsetX, 0, offsetZ);
  payload.bladePositions[i] = tileCenter + offset;
}
Scattered grass
Mtolfakub pbahs

let grassSettings = GrassSettings(
  maxDistance: 30,
  bladeVertices: 3,
  gridSize: 25,
  tileSize: 3)
Lush grass
Rabz pyerr

Meshlet Culling

An important use case for mesh shaders is rendering levels of detail. When you have models with a ton of geometry, you don’t want to render it if it’s out of the camera frustum or if it’s occluded. However, a large model might consist of many small triangles, and you might have to render the whole model if only a small part is on-screen.

Stanford Dragon showing colored meshlets
Wcejzuyv Qbunif frukepd pepewuw nerjlabw

Challenge

For your challenge, you’ll make the grass look more grassy and less triangular.

float randomRange(
  float2 coords, 
  float offset, 
  float minValue, 
  float maxValue) {
  return simpleHash(coords + offset) 
    * (maxValue - minValue) + minValue;
}
Completed grass
Bakwlohin tnayk

Key Points

  • Use the standard vertex pipeline for most rendering. You can use GPU indirect command encoding for camera frustum culling. Mesh shaders are useful for generating and culling simple geometry.
  • There are two stages in the mesh shader pipeline. Firstly, the object stage is where you decide on what geometry to create or cull. Secondly, the mesh stage is where you create actual vertices.
  • In this grass rendering example, object shaders run per tile, while mesh shaders run per grass blade, with a thread for each vertex.
  • Metal Shading Language doesn’t provide a random number function. There are many different kinds of random number algorithms. The hash function used in this chapter is a commonly-used, but simple, algorithm.

Where to Go From Here?

The grass you created in this chapter is highly stylized. For more natural grass, you’ll create more vertices than just a triangle. Wind blowing across the surface will enhance the effect enormously.

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.
© 2025 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