Unity's built-in shaders weren't magical enough. Here's how we wrote custom shaders for impossible visuals.
Why Custom Shaders Matter
Unity provides Standard shader (PBR), Unlit shader (performance), and Shader Graph (node-based visual programming). These cover 80% of use cases but don't create distinctive visuals. Every Unity game looks similar because they use the same shaders. We wanted impossible colors, non-Euclidean geometry, accessibility-reactive rendering, and magical effects that don't exist in reality. That requires writing HLSL/GLSL shader code directly—intimidating but incredibly rewarding.
Color Manipulation for Accessibility Modes
We wrote fragment shaders that transform colors in real-time based on accessibility settings. Deuteranopia mode shifts green wavelengths to blue using matrix transformations in CIE LAB color space (more perceptually accurate than RGB). Protanopia and Tritanopia use similar matrices. Each mode also reveals hidden objects: we sample a mask texture in UV space and discard fragments (pixels) conditionally based on the active mode. One shader handles all three colorblind modes plus the default view, improving performance compared to swapping materials.
Procedural Patterns and Noise Functions
Sigils use procedurally generated patterns rendered in custom shaders. We use Perlin noise, Voronoi diagrams, and fractal Brownian motion (FBM) to create organic magical patterns. The shader takes player behavior hashes as seed values and generates deterministic patterns—same player ID always produces the same sigil. We layer multiple noise octaves at different frequencies and amplitudes, then apply domain warping (displacing UV coordinates based on noise) to create flowing, organic shapes impossible to replicate with traditional textures.
Performance Optimization for Mobile GPUs
Mobile GPUs are heavily fragment shader-limited—complex per-pixel calculations kill framerate. We use vertex shaders to pre-compute values when possible (calculate once per vertex, interpolate across triangles). Expensive operations like noise generation are baked into textures when patterns don't need to be dynamic. We use shader LOD (level of detail): close objects get full shader complexity, distant objects use simplified versions. Conditional compilation (#if UNITY_ANDROID) generates mobile-optimized variants. Result: magical visuals at 60fps on mobile.