
Metal Shader Expert
- 1 installs
- 2 repo stars
- Updated February 3, 2026
- afolkest/metal-lic
metal-shader-expert is a Claude Code skill that provides expert guidance on Apple Metal Shading Language shaders, PBR rendering and GPU optimization.
About
metal-shader-expert is a Claude Code skill for writing and optimizing Apple Metal shaders. It covers Metal Shading Language, PBR rendering, tile-based deferred rendering, compute shaders, ray tracing on Apple Silicon and GPU debugging. A developer uses it when building GPU-accelerated iOS or Mac graphics and needs shader code or Apple GPU optimization. It explicitly excludes WebGL, GLSL, CUDA and OpenGL.
- Expert guidance for Metal Shading Language, PBR rendering and Apple tile-based deferred rendering
- Covers compute shaders, ray tracing on Apple Silicon and GPU profiling and debugging
- Ships reference files for Cook-Torrance PBR, noise effects and debug visualization tools
Metal Shader Expert by the numbers
- 1 all-time installs (skills.sh)
- Ranked #218 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Jul 7, 2026 (Skillselion catalog sync)
metal-shader-expert capabilities & compatibility
- Capabilities
- metal shaders · pbr rendering · gpu optimization · ray tracing · gpu debugging
- Use cases
- ui design
- Platforms
- macOS
What metal-shader-expert says it does
Expert in MSL shaders, PBR rendering, tile-based deferred rendering (TBDR), and GPU debugging.
NOT for WebGL/GLSL (different architecture), general OpenGL (deprecated on Apple), CUDA (NVIDIA only), or CPU-side rendering optimization.
npx skills add https://github.com/afolkest/metal-lic --skill metal-shader-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 2 |
| Last updated | February 3, 2026 |
| Repository | afolkest/metal-lic ↗ |
What it does
Write and optimize Metal shaders and PBR rendering pipelines for Apple GPU-accelerated iOS and Mac apps.
Who is it for?
Developers building GPU-accelerated iOS or Mac graphics who need MSL shaders, PBR pipelines or Apple GPU optimization.
Skip if: WebGL/GLSL, CUDA, OpenGL or CPU-side rendering optimization.
When should I use this skill?
You are working on Metal shaders, MSL, compute or fragment shaders, PBR, ray tracing or Apple GPU profiling.
What you get
Performant, correctly-specialized Metal shaders and rendering pipelines tuned for Apple GPU architecture.
By the numbers
- 6-row expert-vs-novice shibboleth table
- 3 reference files (pbr-shaders, noise-effects, debug-tools)
Files
Metal Shader Expert
20+ years Weta/Pixar experience specializing in Metal shaders, real-time rendering, and creative visual effects. Expert in Apple's Tile-Based Deferred Rendering (TBDR) architecture.
When to Use This Skill
Use for:
- Metal Shading Language (MSL) development
- Apple GPU optimization (TBDR architecture)
- PBR rendering pipelines
- Compute shaders and parallel processing
- Ray tracing on Apple Silicon
- GPU profiling and debugging
Do NOT use for:
- WebGL/GLSL → different architecture, browser constraints
- CUDA → NVIDIA-only
- OpenGL → deprecated on Apple since 2018
- CPU-side optimization → use general performance tools
Expert vs Novice Shibboleths
| Topic | Novice | Expert |
|---|---|---|
| Data types | Uses float everywhere | Defaults to half (16-bit), float only when precision needed |
| Specialization | Runtime branching | Function constants for compile-time specialization |
| Memory | Everything in device space | Knows constant/device/threadgroup tradeoffs |
| Architecture | Treats like desktop GPU | Understands TBDR: tile memory is free, bandwidth is expensive |
| Ray tracing | Uses intersection queries | Uses intersector API (hardware-aligned) |
| Debugging | Print debugging | GPU capture, shader profiler, occupancy analysis |
Common Anti-Patterns
32-Bit Everything
| What it looks like | Why it's wrong |
|---|---|
float4 color, float3 normal everywhere | Wastes registers, reduces occupancy, doubles bandwidth |
Instead: Default to half, upgrade to float only for positions/depth |
Ignoring TBDR Architecture
| What it looks like | Why it's wrong |
|---|---|
| Treating Apple GPU like immediate-mode renderer | Tile memory reads are free; bandwidth is not |
Instead: Use [[color(n)]] freely, prefer memoryless targets, avoid unnecessary store |
Runtime Branching for Constants
| What it looks like | Why it's wrong |
|---|---|
if (material.useNormalMap) checked every fragment | Creates divergent warps, wastes ALU |
| Instead: Function constants + pipeline specialization |
Intersection Queries for Ray Tracing
| What it looks like | Why it's wrong |
|---|---|
| Using query-based API | Doesn't align with hardware; less efficient grouping |
| Instead: Use intersector API with explicit result handling |
Evolution Timeline
| Era | Key Development |
|---|---|
| Pre-2020 | Metal 2.x, OpenGL migration, basic compute |
| 2020-2022 | Apple Silicon, unified memory, tile shaders critical |
| 2023-2024 | Metal 3, mesh shaders, ray tracing HW acceleration |
| 2025+ | Neural Engine + GPU cooperation, Vision Pro foveated rendering |
Apple Family 9 Note: Threadgroup memory less advantageous vs direct device access.
Philosophy: Play, Exposition, Tools
Play: The best shaders come from experimentation and happy accidents. Try weird ideas, build beautiful effects.
Exposition: If you can't explain it clearly, you don't understand it yet. Comment generously, show the math visually.
Tools: A good debug tool saves 100 hours of guessing. Build visualization for every complex shader.
Core Competencies
| Area | Skills |
|---|---|
| MSL | Kernel functions, vertex/fragment, tile shaders, ray tracing |
| Production | Asset pipelines, artist-friendly parameters, fast iteration |
| Rendering | PBR, IBL, volumetrics, post-processing, mesh shaders |
| Debug | Heat maps, shader inspection, GPU profiling, custom overlays |
MCP Integrations
| MCP | Purpose |
|---|---|
| Firecrawl | Research SIGGRAPH papers, Apple GPU architecture |
| WebFetch | Fetch Apple Metal documentation |
Reference Files
| File | Contents |
|---|---|
references/pbr-shaders.md | Cook-Torrance BRDF, material structs, lighting calculations |
references/noise-effects.md | Hash functions, FBM, Voronoi, domain warping, animated effects |
references/debug-tools.md | Heat maps, debug modes, overdraw viz, NaN detection, wireframe |
Integration with Other Skills
- physics-rendering-expert - Jacobi solver GPU compute shaders
- native-app-designer - Visualization and debugging UI
---
Craft beautiful, performant Metal shaders with the artistry of film production and the pragmatism of real-time constraints.
Changelog
[2.0.0] - 2024-12-XX
Changed
- SKILL.md restructured for progressive disclosure (406 → ~115 lines)
- Shader code examples extracted to reference files
- Removed duplicate Philosophy section
Added
references/pbr-shaders.md- Complete Cook-Torrance BRDF, Fresnel-Schlick, GGX distribution, Smith geometryreferences/noise-effects.md- Hash functions, smooth noise, FBM, Voronoi, domain warping, animated effectsreferences/debug-tools.md- Heat maps, debug modes, overdraw visualization, NaN detection, wireframe overlay- Shibboleths table (half vs float, TBDR architecture, intersector API)
- Apple Family 9 note on threadgroup memory changes
Migration Guide
- No changes to frontmatter or activation triggers
- Shader code now in reference files for copy-paste use
- Philosophy section deduplicated (single version retained)
value = length(in.world_position) / debug_scale; return float4(heat_map(value), 1.0);
case 3: // Lighting complexity (fake, for demo) value = fbm(in.texcoord * 10.0, 3); return float4(heat_map(value), 1.0);
case 4: // Wireframe (requires geometry shader or clever tricks) // Barycentric coordinates magic float3 bary = in.barycentric; float edge_dist = min(min(bary.x, bary.y), bary.z); float edge = 1.0 - smoothstep(0.0, 0.02, edge_dist); return float4(float3(edge), 1.0);
default: return float4(1.0, 0.0, 1.0, 1.0); // Magenta = error } }
### Live Value Inspector
// Draw numbers on screen (for debugging values) // Uses a simple bitmap font stored in a texture
struct DebugText { float2 screen_pos; // Where to draw (normalized 0-1) float value; // Value to display float3 color; // Text color };
fragment float4 debug_text_overlay_fragment( float2 screen_pos [[position]], constant DebugText* debug_values [[buffer(0)]], constant uint& debug_count [[buffer(1)]], texture2d<float> font_atlas [[texture(0)]], sampler font_sampler [[sampler(0)]] ) { float4 output = float4(0.0); // Transparent background
for (uint i = 0; i < debug_count; i++) { DebugText dt = debug_values[i];
// Convert value to string (simplified - just show as digits) // In real implementation, format as "123.45" etc.
// Check if we're in the text region float2 local_pos = screen_pos - dt.screen_pos;
if (local_pos.x > 0.0 && local_pos.x < 100.0 && local_pos.y > 0.0 && local_pos.y < 20.0) {
// Sample font atlas (simplified) float2 uv = local_pos / float2(100.0, 20.0); float alpha = font_atlas.sample(font_sampler, uv).r;
output.rgb = mix(output.rgb, dt.color, alpha); output.a = max(output.a, alpha); } }
return output; }
### Performance Profiler Overlay
struct GPUMetrics { float frame_time_ms; float vertex_shader_time_ms; float fragment_shader_time_ms; float memory_usage_mb; uint triangle_count; uint draw_call_count; };
// Draw performance overlay (graphs, numbers, bars) kernel void render_performance_overlay( texture2d<float, access::write> output [[texture(0)]], constant GPUMetrics& metrics [[buffer(0)]], constant float* frame_history [[buffer(1)]], // Last 120 frames uint2 gid [[thread_position_in_grid]] ) { float2 uv = float2(gid) / float2(output.get_width(), output.get_height());
float4 color = float4(0.0, 0.0, 0.0, 0.0);
// Draw frame time graph (top left corner) if (uv.x < 0.3 && uv.y < 0.2) { float2 graph_uv = uv / float2(0.3, 0.2);
// Sample frame history uint history_index = uint(graph_uv.x * 120.0); float frame_time = frame_history[history_index];
// Draw line graph float graph_value = 1.0 - (frame_time / 33.0); // 33ms = 30fps float y_threshold = graph_uv.y;
if (abs(graph_value - y_threshold) < 0.01) { // Graph line color = float4(0.0, 1.0, 0.0, 0.8); }
// 60fps line (16.67ms) if (abs((1.0 - 16.67/33.0) - y_threshold) < 0.005) { color = float4(1.0, 1.0, 0.0, 0.5); }
// Background if (color.a == 0.0) { color = float4(0.1, 0.1, 0.1, 0.7); } }
// Draw current metrics (numbers - simplified) // In real version, use the text rendering system
output.write(color, gid); }
## Weta/Pixar Production Techniques
### Shader Authoring for Artists
// Material definition that artists can understand and control
struct ArtistMaterial { // Base properties float3 base_color; float base_color_intensity;
// Surface float metallic; float roughness; float specular_tint; float sheen; float sheen_tint;
// Subsurface float subsurface; float3 subsurface_color; float subsurface_radius;
// Clearcoat (car paint, etc.) float clearcoat; float clearcoat_roughness;
// Emission float3 emission_color; float emission_strength;
// Special FX float iridescence; float anisotropic; float anisotropic_rotation; };
// The key: Make complex physically accurate, but expose artist-friendly controls
### Procedural Variation for Uniqueness
// Add procedural variation so every instance looks unique // (Pixar trick: never have two identical things on screen)
float3 add_surface_variation( float3 base_color, float3 world_pos, float variation_amount ) { // Subtle color variation float color_var = fbm(world_pos 5.0, 3) 0.1; base_color = (1.0 + color_var variation_amount);
// Slight hue shift float hue_shift = (hash(world_pos.xz) - 0.5) 0.05 variation_amount; // Apply hue shift (simplified - real version uses HSV conversion)
return base_color; }
float add_roughness_variation( float base_roughness, float3 world_pos, float variation_amount ) { // Add wear patterns, dirt, micro-scratches float wear = fbm(world_pos 10.0, 4); float dirt = fbm(world_pos 20.0, 3) * 0.5;
float variation = (wear + dirt) variation_amount 0.2;
return saturate(base_roughness + variation); }
## Performance Optimization
### Profiling Mental Model
GPU Performance Bottlenecks (in order of likelihood):
1. Memory Bandwidth
- Texture fetches
- Buffer reads/writes
- Fix: Reduce texture size, compress, use mipmaps
2. ALU (Arithmetic Logic Unit)
- Complex math in shaders
- Too many instructions
- Fix: Simplify math, use lookup tables, reduce precision
3. Occupancy
- Register pressure
- Shared memory usage
- Fix: Reduce register usage, simplify shaders
4. Divergence
- Branching (if/else) in shaders
- Non-uniform control flow
- Fix: Minimize branching, use select() instead of if
### Optimization Examples
// ❌ SLOW: Branch divergence fragment float4 slow_conditional(VertexOut in [[stage_in]]) { if (in.texcoord.x > 0.5) { // Complex calculation A return complex_calc_A(in); } else { // Complex calculation B return complex_calc_B(in); } }
// ✅ FAST: Branchless with select fragment float4 fast_branchless(VertexOut in [[stage_in]]) { float4 result_a = complex_calc_A(in); float4 result_b = complex_calc_B(in);
// select(false_value, true_value, condition) return select(result_b, result_a, in.texcoord.x > 0.5); }
// ❌ SLOW: Texture sampling in loop float calculate_blur(texture2d<float> tex, sampler s, float2 uv) { float sum = 0.0; for (int i = -5; i <= 5; i++) { for (int j = -5; j <= 5; j++) { float2 offset = float2(i, j) / 512.0; sum += tex.sample(s, uv + offset).r; } } return sum / 121.0; // 11x11 = 121 samples }
// ✅ FAST: Separable blur (11x11 -> 11+11 samples) float calculate_blur_fast(texture2d<float> tex, sampler s, float2 uv) { // First pass: horizontal blur (done separately) // Second pass: vertical blur on pre-blurred texture float sum = 0.0; for (int i = -5; i <= 5; i++) { float2 offset = float2(0, i) / 512.0; sum += tex.sample(s, uv + offset).r; } return sum / 11.0; }
## Internal Tools Philosophy
"Build the tool you wish you had yesterday."
### Essential Debug Tools Checklist
- [ ] **Shader Hot Reload**: Edit shader, see changes in <1 second
- [ ] **Value Inspector**: Click any pixel, see all shader variables
- [ ] **Heat Maps**: Visualize complexity, overdraw, bandwidth
- [ ] **Wireframe Toggle**: See geometry structure
- [ ] **Texture Viewer**: Inspect all textures, mipmaps, channels
- [ ] **Performance Overlay**: Frame time, draw calls, triangles
- [ ] **Capture/Replay**: Record frames, step through rendering
- [ ] **Shader Compiler Warnings**: Catch inefficiencies early
- [ ] **GPU Counters**: ALU, bandwidth, cache, occupancy
- [ ] **Diff Tool**: Compare shader versions side-by-side
## The Weta/Pixar Mindset
### Quality Over Everything
"Never let technology limit artistry."
- If it doesn't look right, it's wrong (even if technically correct)
- Artists drive the vision, engineers enable it
- Iterate until it's beautiful, then optimize
- The audience doesn't see the tech, they feel the emotion
### Collaboration
"The best shots come from engineers who understand art and artists who understand tech."
- Learn to speak both languages (technical and artistic)
- Build tools artists love using
- Pair with artists during development
- Take feedback seriously
### Continuous Learning
"The technology changes every 2 years. Stay curious."
- Study new GPU features
- Read papers from SIGGRAPH, GDC
- Experiment with unreleased techniques
- Share knowledge generously
---
**Remember**: Shaders are where art meets mathematics meets engineering. Make them beautiful, make them fast, and make tools that let you iterate quickly. The best shader is the one that makes the artist say "Yes! That's exactly what I imagined."
Now go make something beautiful. 🎨✨
Debug Tools & Visualization
Essential patterns for shader debugging and performance analysis.
Heat Map Visualization
// Visualize scalar values: 0=blue, 0.5=green, 1=red
float3 heat_map(float v) {
v = saturate(v);
return v < 0.5
? mix(float3(0,0,1), float3(0,1,0), v*2)
: mix(float3(0,1,0), float3(1,0,0), (v-0.5)*2);
}
// Extended heat map with purple for overflow
float3 heat_map_extended(float v) {
if (v < 0.0) return float3(0.5, 0, 0.5); // Magenta: negative
if (v > 1.0) return float3(1, 0, 1); // Purple: overflow
return heat_map(v);
}Debug Visualization Modes
fragment float4 debug_fragment(
VertexOut in [[stage_in]],
constant uint& mode [[buffer(0)]]
) {
switch (mode) {
case 0: // World normals
return float4(in.world_normal * 0.5 + 0.5, 1.0);
case 1: // UV coordinates
return float4(in.texcoord, 0.0, 1.0);
case 2: // Depth (linear)
float depth = in.position.z / in.position.w;
return float4(float3(depth), 1.0);
case 3: // Tangent space
return float4(in.tangent * 0.5 + 0.5, 1.0);
case 4: // Bitangent
return float4(in.bitangent * 0.5 + 0.5, 1.0);
case 5: // World position (wrapped)
return float4(fract(in.world_position), 1.0);
default:
return float4(1, 0, 1, 1); // Magenta = error
}
}Overdraw Visualization
// Increment counter per fragment
kernel void overdraw_counter(
texture2d<uint, access::read_write> counter [[texture(0)]],
uint2 gid [[thread_position_in_grid]]
) {
uint current = counter.read(gid).r;
counter.write(uint4(current + 1), gid);
}
// Visualize overdraw
fragment float4 overdraw_visualize(
VertexOut in [[stage_in]],
texture2d<uint> counter [[texture(0)]]
) {
uint2 pos = uint2(in.position.xy);
uint count = counter.read(pos).r;
// Heat map: 1=green, 2=yellow, 3+=red
float normalized = float(count) / 5.0;
return float4(heat_map(normalized), 1.0);
}Mipmap Level Visualization
// Shows which mipmap is being sampled
float3 mip_colors[] = {
float3(1,0,0), // Mip 0 - Red
float3(1,0.5,0), // Mip 1 - Orange
float3(1,1,0), // Mip 2 - Yellow
float3(0,1,0), // Mip 3 - Green
float3(0,1,1), // Mip 4 - Cyan
float3(0,0,1), // Mip 5 - Blue
float3(0.5,0,1), // Mip 6 - Purple
float3(1,0,1), // Mip 7 - Magenta
};
fragment float4 mip_debug(
VertexOut in [[stage_in]],
texture2d<float> tex [[texture(0)]]
) {
// Calculate mip level from UV derivatives
float2 dx = dfdx(in.texcoord);
float2 dy = dfdy(in.texcoord);
float delta = max(dot(dx, dx), dot(dy, dy));
float mip = 0.5 * log2(delta * tex.get_width() * tex.get_width());
int mip_index = clamp(int(mip), 0, 7);
return float4(mip_colors[mip_index], 1.0);
}NaN/Inf Detection
float4 nan_check(float4 color) {
if (any(isnan(color))) return float4(1, 0, 1, 1); // Magenta = NaN
if (any(isinf(color))) return float4(0, 1, 1, 1); // Cyan = Inf
return color;
}Wireframe Overlay
// Barycentric wireframe (requires vertex shader to pass barycentrics)
float wireframe(float3 bary, float thickness) {
float3 d = fwidth(bary);
float3 a = smoothstep(float3(0), d * thickness, bary);
return min(min(a.x, a.y), a.z);
}
fragment float4 wireframe_overlay(
VertexOut in [[stage_in]],
constant float4& base_color [[buffer(0)]],
constant float4& wire_color [[buffer(1)]]
) {
float edge = wireframe(in.barycentrics, 1.5);
return mix(wire_color, base_color, edge);
}Performance Timers
// Measure shader complexity by counting iterations
kernel void complexity_visualize(
texture2d<float, access::write> output [[texture(0)]],
constant uint& max_iterations [[buffer(0)]],
uint2 gid [[thread_position_in_grid]]
) {
uint iterations = 0;
// Your algorithm with iteration counting
while (/* condition */ iterations < max_iterations) {
// Work...
iterations++;
}
float complexity = float(iterations) / float(max_iterations);
output.write(float4(heat_map(complexity), 1.0), gid);
}GPU Capture Integration
Use Xcode GPU Capture for:
- Frame timeline analysis
- Shader profiler
- Memory bandwidth
- Occupancy metrics
- Pipeline state inspection
Best Practices
1. Always have a debug mode: Toggle with function constant 2. Color-code errors: Magenta for NaN, Cyan for Inf 3. Visualize intermediate buffers: G-buffer, shadow maps 4. Add performance overlays: FPS, draw calls, triangles 5. Hot-reload shaders: Metal Library at runtime
Debug Macro Pattern
#if DEBUG_MODE
return float4(heat_map(some_value), 1.0);
#else
return final_color;
#endifUse function constants for runtime toggling without recompilation.
Noise-Based Effects
Organic, procedural effects using noise functions in Metal.
Hash Functions
// Simple 2D hash function
float hash(float2 p) {
p = fract(p * float2(234.34, 435.345));
p += dot(p, p + 34.23);
return fract(p.x * p.y);
}
// 3D hash for volumetric effects
float hash3(float3 p) {
p = fract(p * float3(443.897, 441.423, 437.195));
p += dot(p, p.yzx + 19.19);
return fract((p.x + p.y) * p.z);
}Smooth Noise
float smooth_noise(float2 uv) {
float2 i = floor(uv);
float2 f = fract(uv);
// Smooth interpolation (smoothstep)
f = f * f * (3.0 - 2.0 * f);
// Four corners of grid
float a = hash(i);
float b = hash(i + float2(1.0, 0.0));
float c = hash(i + float2(0.0, 1.0));
float d = hash(i + float2(1.0, 1.0));
// Bilinear interpolation
return mix(mix(a, b, f.x),
mix(c, d, f.x), f.y);
}Fractal Brownian Motion (FBM)
Creates organic, natural-looking patterns by layering noise at different frequencies.
float fbm(float2 uv, int octaves) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 2.0;
for (int i = 0; i < octaves; i++) {
value += amplitude * smooth_noise(uv * frequency);
amplitude *= 0.5; // Persistence
frequency *= 2.0; // Lacunarity
}
return value;
}FBM Parameters
| Parameter | Effect |
|---|---|
| Octaves | More = finer detail, higher cost |
| Persistence (amplitude multiplier) | Lower = smoother, higher = rougher |
| Lacunarity (frequency multiplier) | Controls how fast detail increases |
Typical values:
- Clouds: 6-8 octaves, 0.5 persistence
- Terrain: 8-12 octaves, 0.6 persistence
- Marble: 4-6 octaves, 0.5 persistence
Animated Flowing Marble Effect
fragment float4 flowing_marble_fragment(
VertexOut in [[stage_in]],
constant float& time [[buffer(0)]]
) {
float2 uv = in.texcoord * 5.0;
// Create flowing pattern
float2 flow = float2(
fbm(uv + time * 0.1, 4),
fbm(uv + time * 0.15 + 100.0, 4)
);
// Distort UV with flow
uv += flow * 2.0;
// Create marble veins
float marble = fbm(uv, 6);
marble = abs(sin(marble * 10.0 + time * 0.5));
// Color gradient (purple to gold)
float3 color1 = float3(0.4, 0.1, 0.7); // Purple
float3 color2 = float3(1.0, 0.7, 0.2); // Gold
float3 color = mix(color1, color2, marble);
// Add shimmer
float shimmer = fbm(uv * 10.0 + time, 3) * 0.3;
color += shimmer;
return float4(color, 1.0);
}Domain Warping
Distort UV coordinates with noise for organic effects:
float2 warp_domain(float2 uv, float time) {
float2 q = float2(
fbm(uv, 4),
fbm(uv + float2(5.2, 1.3), 4)
);
float2 r = float2(
fbm(uv + q + float2(1.7, 9.2) + 0.15 * time, 4),
fbm(uv + q + float2(8.3, 2.8) + 0.126 * time, 4)
);
return uv + r * 2.0;
}Voronoi / Cellular Noise
float voronoi(float2 uv) {
float2 i = floor(uv);
float2 f = fract(uv);
float min_dist = 1.0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
float2 neighbor = float2(x, y);
float2 point = float2(hash(i + neighbor),
hash(i + neighbor + 127.0));
float dist = length(neighbor + point - f);
min_dist = min(min_dist, dist);
}
}
return min_dist;
}Performance Tips
1. Unroll small loops: [[unroll]] for octave loops with fixed count 2. Use half precision: half for color calculations 3. Precompute gradients: For Perlin noise, texture-based gradients faster 4. Limit octaves: 4-6 is usually enough for real-time 5. LOD-based detail: Fewer octaves for distant objects
Effect Ideas
| Effect | Technique |
|---|---|
| Fire | FBM + time + color ramp |
| Water caustics | Animated Voronoi |
| Clouds | FBM with domain warping |
| Marble | FBM with sin() banding |
| Wood grain | FBM rings + turbulence |
| Plasma | Multiple sin waves + time |
PBR Shader Implementation
Complete Cook-Torrance BRDF implementation in Metal Shading Language.
Material Properties Structure
struct MaterialProperties {
float3 albedo;
float metallic;
float roughness;
float ao; // Ambient occlusion
float3 emission;
};
struct Light {
float3 position;
float3 color;
float intensity;
};BRDF Components
Fresnel-Schlick Approximation
float3 fresnel_schlick(float cos_theta, float3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - cos_theta, 5.0);
}GGX/Trowbridge-Reitz Normal Distribution
float distribution_ggx(float3 N, float3 H, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = M_PI_F * denom * denom;
return a2 / denom;
}Smith's Schlick-GGX Geometry Function
float geometry_schlick_ggx(float NdotV, float roughness) {
float r = (roughness + 1.0);
float k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
float geometry_smith(float3 N, float3 V, float3 L, float roughness) {
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx1 = geometry_schlick_ggx(NdotV, roughness);
float ggx2 = geometry_schlick_ggx(NdotL, roughness);
return ggx1 * ggx2;
}Complete PBR Lighting Function
float3 calculate_pbr_lighting(
float3 world_pos,
float3 normal,
float3 view_dir,
MaterialProperties material,
Light light
) {
// Calculate light direction
float3 light_dir = normalize(light.position - world_pos);
float3 halfway = normalize(view_dir + light_dir);
// Distance attenuation
float distance = length(light.position - world_pos);
float attenuation = 1.0 / (distance * distance);
float3 radiance = light.color * light.intensity * attenuation;
// Cook-Torrance BRDF
float3 F0 = mix(float3(0.04), material.albedo, material.metallic);
float3 F = fresnel_schlick(max(dot(halfway, view_dir), 0.0), F0);
float NDF = distribution_ggx(normal, halfway, material.roughness);
float G = geometry_smith(normal, view_dir, light_dir, material.roughness);
float3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(normal, view_dir), 0.0) *
max(dot(normal, light_dir), 0.0) + 0.0001;
float3 specular = numerator / denominator;
// Energy conservation
float3 kS = F;
float3 kD = (1.0 - kS) * (1.0 - material.metallic);
float NdotL = max(dot(normal, light_dir), 0.0);
return (kD * material.albedo / M_PI_F + specular) * radiance * NdotL;
}Fragment Shader
fragment float4 pbr_fragment(
VertexOut in [[stage_in]],
constant MaterialProperties& material [[buffer(0)]],
constant Light* lights [[buffer(1)]],
constant uint& light_count [[buffer(2)]],
constant float3& camera_pos [[buffer(3)]]
) {
float3 normal = normalize(in.world_normal);
float3 view_dir = normalize(camera_pos - in.world_position);
// Accumulate lighting from all lights
float3 Lo = float3(0.0);
for (uint i = 0; i < light_count; i++) {
Lo += calculate_pbr_lighting(
in.world_position,
normal,
view_dir,
material,
lights[i]
);
}
// Ambient lighting (simplified IBL)
float3 ambient = float3(0.03) * material.albedo * material.ao;
float3 color = ambient + Lo + material.emission;
// HDR tone mapping (Reinhard)
color = color / (color + float3(1.0));
// Gamma correction
color = pow(color, float3(1.0/2.2));
return float4(color, 1.0);
}Key Concepts
Cook-Torrance BRDF
The specular term: (D * G * F) / (4 * NdotV * NdotL)
- D: Normal Distribution Function (GGX)
- G: Geometry Function (Smith)
- F: Fresnel (Schlick approximation)
Energy Conservation
kD = (1 - kS) * (1 - metallic)
- Metals have no diffuse component
- Total reflected energy never exceeds incoming
F0 Values
- Dielectrics: ~0.04 (plastic, fabric, skin)
- Metals: Use albedo as F0
F0 = mix(0.04, albedo, metallic)
Half-Precision Optimization
For mobile/Apple Silicon, convert to half precision:
half3 fresnel_schlick_half(half cos_theta, half3 F0) {
return F0 + (half3(1.0h) - F0) * pow(1.0h - cos_theta, 5.0h);
}Only use float for:
- World positions
- Depth values
- Cumulative calculations