MetalGraph Documentation

MetalGraph is a visual shader editor for creating Metal fragment shaders that integrate directly with SwiftUI. Build shaders by connecting nodes, see live previews, and export production-ready code.

Understanding Shader Inputs

Every shader needs data to work with. MetalGraph provides several input nodes that give you access to essential information about each pixel being rendered.

UV Coordinates

half2 (x, y)

Normalized coordinates from 0 to 1. Range: (0, 0) at top-left to (1, 1) at bottom-right. Use for resolution-independent effects — the same UV value represents the same relative position regardless of view size.

metal
// What UV Coordinates generates:
half2 uv = half2(position / size);

Position

half2 (x, y)

Actual pixel coordinates in points. Range: (0, 0) to (width, height) of the view. Use for effects that need exact pixel positions, like pixel-perfect patterns.

Time

half

Elapsed time in seconds since the shader started. Multiply by values to control speed, use with sin/cos for oscillations.

metal
// Example: Create a pulsing effect
half pulse = sin(time * 2.0);  // Oscillates between -1 and 1

Size

half2 (width, height)

View dimensions in points. Use for aspect ratio calculations or converting between normalized and pixel coordinates.

Touch Position

half2 (x, y)

Current touch or pointer location. Default: (0, 0) when not touching. Use for interactive effects that respond to user input.

Source Color

half4 (r, g, b, a)Color Effect only

The original color at the current pixel. Use for modifying existing content (tinting, color grading, filters).

Sample Layer

half4 (color at position)Layer Effect only

Samples the source layer at any position you specify. Use for blur, displacement, edge detection — effects that read from multiple positions.

Constant Values

MetalGraph provides nodes for constant values:

NodeOutput TypeUse Case
FloathalfNumbers (scale, threshold, etc.)
Vec2half22D vectors (offset, direction)
Vec3half33D vectors (RGB color without alpha)
Vec4half44D vectors (RGBA, arbitrary data)
Colorhalf4RGBA colors with color picker

Understanding Shader Outputs

The Output node is the endpoint of every shader graph. It determines what type of shader you're creating and how it integrates with SwiftUI.

Color Effect

.colorEffect()

Transforms the color of each pixel independently. You receive the source color and return a new color.

metal
[[ stitchable ]] half4 myShader(
    float2 position,    // Pixel position in points
    half4 color,        // Original color at this pixel
    float2 size,        // View dimensions
    float time,         // Elapsed time
    float2 touch        // Touch position
)

Use cases: Color grading, tinting, grayscale, sepia, invert, gradient overlays

Layer Effect

.layerEffect()

Can sample pixels from any position in the source layer. Enables effects that combine information from multiple pixels.

metal
[[ stitchable ]] half4 myShader(
    float2 position,        // Pixel position
    SwiftUI::Layer layer,   // Layer to sample from
    float2 size,            // View dimensions
    float time,             // Elapsed time
    float2 touch            // Touch position
)

Use cases: Blur, displacement, distortion, edge detection, ripple effects

Distortion Effect

.distortionEffect()

Returns a position to sample from instead of a color. The GPU then reads from that position.

metal
[[ stitchable ]] float2 myShader(
    float2 position,    // Current pixel position
    float2 size,        // View dimensions
    float time,         // Elapsed time
    float2 touch        // Touch position
)

Use cases: Lens distortion, wave effects, magnification, geometric transformations

Example: Building a Gradient Shader

Let's walk through creating a simple horizontal gradient from red to blue.

1

Add Input Nodes

Open MetalGraph and drag a UV Coordinates node from the Input category in the left sidebar.

This gives us normalized coordinates where x goes from 0 (left) to 1 (right).

2

Extract the X Component

From the Vector category, add a Split Vec2 node and connect the UV output to it.

This separates UV into x and y components. We only need x for a horizontal gradient.

3

Create the Colors

Add two Color nodes from the Input category.

Set one to red (1, 0, 0, 1) and one to blue (0, 0, 1, 1).

4

Blend the Colors

Add a Mix node from the Color category and connect:

  • Red color to input A
  • Blue color to input B
  • X component to the T (blend factor) input

The Mix node interpolates: when T=0 you get A (red), when T=1 you get B (blue).

5

Connect to Output

Connect the Mix output to the Output node and ensure "Effect Type" is set to "Color Effect".

6

Preview and Export

The preview panel shows your gradient in real-time. Click the code tab to see the generated Metal shader.

Using Your Shader in SwiftUI

MetalGraph generates two files: the Metal shader and SwiftUI integration code.

Step 1: Add the Metal Shader

  1. Copy the Metal code from the code panel
  2. In Xcode, create a new file: File → New → File
  3. Choose "Metal File" and name it (e.g., Shaders.metal)
  4. Paste the Metal code
metal
#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]] half4 myShader(
    float2 position,
    half4 color,
    float2 size,
    float time,
    float2 touch
) {
    half2 uv = half2(position / size);
    half x = uv.x;
    half4 colorA = half4(1.0h, 0.0h, 0.0h, 1.0h);
    half4 colorB = half4(0.0h, 0.0h, 1.0h, 1.0h);
    half4 result = mix(colorA, colorB, x);
    return result;
}

Step 2: Add the SwiftUI View

Copy the SwiftUI code and create a new Swift file:

swift
import SwiftUI

struct GradientShaderView: View {
    @State private var touch: CGPoint = .zero
    private let startDate = Date()

    var body: some View {
        TimelineView(.animation) { context in
            let time = context.date.timeIntervalSince(startDate)

            GeometryReader { geometry in
                let size = geometry.size

                Rectangle()
                    .fill(.white)
                    .colorEffect(
                        ShaderLibrary.myShader(
                            .float2(size),
                            .float(time),
                            .float2(touch)
                        )
                    )
            }
        }
        .gesture(
            DragGesture(minimumDistance: 0)
                .onChanged { touch = $0.location }
        )
    }
}

Understanding the Integration

ComponentPurpose
TimelineView(.animation)Continuously updates to animate time-based effects
GeometryReaderProvides view dimensions to the shader
ShaderLibrary.myShader()Accesses your compiled Metal function
.colorEffect()Applies the shader as a color transformation
DragGestureCaptures touch position for interactive effects

Effect-Specific Modifiers

For Layer Effects, use .layerEffect() with a max sample offset:

swift
.layerEffect(
    ShaderLibrary.myBlur(
        .float2(size),
        .float(time),
        .float2(touch)
    ),
    maxSampleOffset: CGSize(width: 50, height: 50)
)

For Distortion Effects, use .distortionEffect():

swift
.distortionEffect(
    ShaderLibrary.myDistortion(
        .float2(size),
        .float(time),
        .float2(touch)
    ),
    maxSampleOffset: CGSize(width: 100, height: 100)
)

The maxSampleOffset tells SwiftUI how far outside the view bounds the shader might read, ensuring proper rendering.

Quick Reference: SwiftUI Shader Modifiers

ModifierShader ReturnsInput AvailableUse Case
.colorEffect()`half4` colorSource colorPer-pixel color changes
.layerEffect()`half4` colorLayer to sampleMulti-pixel effects (blur, edge)
.distortionEffect()`float2` positionNoneGeometric warping

Simplifying the Generated Code

MetalGraph generates SwiftUI code that works with any shader graph. This means it includes boilerplate for features you might not be using. Once you understand what each part does, you can simplify the code for your specific shader.

Metal Shader Parameters

The SwiftUI shader system only requires position and color (for color effects) or layer (for layer effects). The size, time, and touch parameters are conveniences that MetalGraph adds — you can remove them entirely from both the Metal function signature and the SwiftUI call if your shader doesn't use them:

metal
// Minimal color effect shader (no size, time, or touch)
[[ stitchable ]] half4 myShader(float2 position, half4 color) {
    // Your shader code
}
swift
// Minimal SwiftUI usage
Rectangle()
    .colorEffect(ShaderLibrary.myShader())

Current Limitations

MetalGraph is designed as a learning tool and shader prototyping environment. Here are some limitations to be aware of:

No Texture Sampling

MetalGraph currently doesn't support loading external textures or images. You can sample from the source content (via Source Color or Sample Layer) or generate procedural patterns with noise and math nodes, but you cannot load a custom image file to use as a texture map.

No Vertex Shaders

MetalGraph focuses on fragment shaders only. You cannot modify vertex positions or create geometry. All effects operate on a flat 2D surface.

No Render Targets / Multi-Pass

Each shader is a single pass. You cannot render to an intermediate texture, chain multiple shader passes, or create feedback loops (reading from previous frame output). For multi-pass effects, you'd need to apply multiple SwiftUI shader modifiers in sequence.

No Custom Uniforms Beyond Built-ins

The shader parameters are fixed: position, size, time, and touch. You cannot add custom uniform variables that update from Swift code at runtime (beyond what the constant nodes provide at compile time).

Half Precision Only

MetalGraph uses half (16-bit float) for all calculations. This is efficient on mobile GPUs but has limited precision: range of approximately -65,504 to +65,504 with about 3 decimal digits of precision. For most visual effects this is fine, but complex mathematical operations may accumulate error.

No Branching Optimization

The generated code doesn't optimize conditional branches. Complex Select node chains generate straightforward code that evaluates all branches. Hand-written Metal could use early-exit patterns for better performance in some cases.

SwiftUI Integration Only

The generated code targets SwiftUI's shader system specifically. It won't work directly with UIKit, AppKit, SceneKit, RealityKit materials, or raw Metal rendering pipelines. You'd need to adapt the core shader logic for other contexts.

MetalGraph — The companion tool for metal.graphics

Created by Victor Baro