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
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.
// What UV Coordinates generates:
half2 uv = half2(position / size);Position
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
Elapsed time in seconds since the shader started. Multiply by values to control speed, use with sin/cos for oscillations.
// Example: Create a pulsing effect
half pulse = sin(time * 2.0); // Oscillates between -1 and 1Size
View dimensions in points. Use for aspect ratio calculations or converting between normalized and pixel coordinates.
Touch Position
Current touch or pointer location. Default: (0, 0) when not touching. Use for interactive effects that respond to user input.
Source Color
The original color at the current pixel. Use for modifying existing content (tinting, color grading, filters).
Sample Layer
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:
| Node | Output Type | Use Case |
|---|---|---|
| Float | half | Numbers (scale, threshold, etc.) |
| Vec2 | half2 | 2D vectors (offset, direction) |
| Vec3 | half3 | 3D vectors (RGB color without alpha) |
| Vec4 | half4 | 4D vectors (RGBA, arbitrary data) |
| Color | half4 | RGBA 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
Transforms the color of each pixel independently. You receive the source color and return a new color.
[[ 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
Can sample pixels from any position in the source layer. Enables effects that combine information from multiple pixels.
[[ 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
Returns a position to sample from instead of a color. The GPU then reads from that position.
[[ 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.
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).
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.
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).
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).
Connect to Output
Connect the Mix output to the Output node and ensure "Effect Type" is set to "Color Effect".
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
- Copy the Metal code from the code panel
- In Xcode, create a new file: File → New → File
- Choose "Metal File" and name it (e.g.,
Shaders.metal) - Paste the Metal code
#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:
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
| Component | Purpose |
|---|---|
| TimelineView(.animation) | Continuously updates to animate time-based effects |
| GeometryReader | Provides view dimensions to the shader |
| ShaderLibrary.myShader() | Accesses your compiled Metal function |
| .colorEffect() | Applies the shader as a color transformation |
| DragGesture | Captures touch position for interactive effects |
Effect-Specific Modifiers
For Layer Effects, use .layerEffect() with a max sample offset:
.layerEffect(
ShaderLibrary.myBlur(
.float2(size),
.float(time),
.float2(touch)
),
maxSampleOffset: CGSize(width: 50, height: 50)
)For Distortion Effects, use .distortionEffect():
.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
| Modifier | Shader Returns | Input Available | Use Case |
|---|---|---|---|
| .colorEffect() | `half4` color | Source color | Per-pixel color changes |
| .layerEffect() | `half4` color | Layer to sample | Multi-pixel effects (blur, edge) |
| .distortionEffect() | `float2` position | None | Geometric 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:
// Minimal color effect shader (no size, time, or touch)
[[ stitchable ]] half4 myShader(float2 position, half4 color) {
// Your shader code
}// 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.