Metal shaders for iOS and macOS

Metal Shading Language is based on C++, and it is what you use for graphics on iPhone, iPad and Mac. The pixel code reads almost exactly like HLSL. The export gives you a complete .metal file with a full-screen vertex function and a fragment function that calls yours.

Things to know about Metal Shading Language

  • Types look like HLSL: float2, float3, float4; blend with mix.
  • Fragment functions are marked fragment and get their inputs through [[stage_in]] and buffer bindings.
  • Your helper functions go inside a struct in the export, so they can read time and aspect.
  • Pass a small uniforms struct with setFragmentBytes in your render encoder.

Cheat sheet

IdeaWrite
Vectorsfloat2 float3 float4
Declare a valuefloat d = length(p);
A functionfloat circle(float2 p, float r) { ... }
Matricesfloat2x2, multiply with M * v (this site writes v * M for lesson rotations)
Blendmix(a, b, t)
Fractional partfract(x)
Clamp to 0..1saturate(x)
Remainderfmod(a, b) (keeps the sign of a)
Choosec ? a : b
Angle of a vectoratan2(y, x)
Time in secondstime
Pixel positionuv (0..1, bottom-left)
Width / heightaspect
Entry pointfloat4 frag(float2 uv) { return …; }

Handy built-ins: sin cos abs sign floor ceil fract step smoothstep clamp min max pow exp sqrt length distance dot cross normalize reflect

Gotchas

  • Every pixel runs on its own, with no memory of other pixels.
  • Declare a function above the place you use it.
  • Prefer 2.0 over 2 in maths. This site fixes most cases for you; real engines may not.
  • Loops need a small fixed count: for (int i = 0; i < 5; i++).
  • Metal is C++ based. The export puts your functions in a struct so they can read time and aspect.

Example: a glowing ring

The same shader as it is written for Apple (iOS, macOS):

float4 frag(float2 uv)
{
    float2 p = (uv - 0.5) * float2(aspect, 1.0);
    float d = length(p);
    float m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
    float3 bg = float3(0.08, 0.08, 0.12);
    float3 fg = float3(0.4, 0.9, 1.0);
    return float4(mix(bg, fg, m), 1.0);
}

Exporting to Metal

Add the file to your Xcode project. Draw 3 vertices with sb_vertex / sb_fragment and pass a Uniforms struct in buffer 0 (setFragmentBytes). Your functions live inside a struct so they can read time and aspect, the same trick as the Unreal export.

// ShaderBattle: Glowing ring
#include <metal_stdlib>
using namespace metal;

struct ShaderBattle
{
    float time;
    float aspect;

    float4 frag(float2 uv)
    {
        float2 p = (uv - 0.5) * float2(aspect, 1.0);
        float d = length(p);
        float m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
        float3 bg = float3(0.08, 0.08, 0.12);
        float3 fg = float3(0.4, 0.9, 1.0);
        return float4(mix(bg, fg, m), 1.0);
    }
};

struct Uniforms { float time; float aspect; };
struct VertexOut { float4 position [[position]]; float2 uv; };

// one triangle that covers the screen
vertex VertexOut sb_vertex(uint vid [[vertex_id]])
{
    float2 p = float2((vid << 1) & 2, vid & 2);
    VertexOut o;
    o.position = float4(p * 2.0 - 1.0, 0.0, 1.0);
    o.uv = p;                     // (0,0) bottom-left
    return o;
}

fragment float4 sb_fragment(VertexOut v [[stage_in]], constant Uniforms& u [[buffer(0)]])
{
    ShaderBattle sb { u.time, u.aspect };
    return sb.frag(v.uv);
}

Questions

Do I need a Mac to learn Metal shading?

Not to practise the shader code: it runs in your browser. You will need Xcode to build and run the exported file.

Other languages

Unity (Built-in) · Unity (URP) · Unreal · Shadertoy / WebGL · OpenGL (LWJGL, raylib) · three.js · GameMaker · LÖVE (Lua) · Godot · WebGPU / Bevy