Home / Blog / Shader Programming: Custom Effects for Magical Worlds

Shader Programming: Custom Effects for Magical Worlds

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.