Creating ultra‑fine textures on display surfaces---whether for UI mock‑ups, visual effects in games, or experimental art installations---often hinges on one deceptively simple trick: variable‑angle scratches . By controlling the direction, depth, and density of micro‑scratches across a screen, you can generate textures that feel tactile, realistic, and visually rich without resorting to heavyweight bitmap assets.
Below is a step‑by‑step guide that walks you through the theory, the tools, and the practical workflow for mastering this technique.
Why Variable‑Angle Scratches Work
| Property | Conventional Approach | Variable‑Angle Scratches |
|---|---|---|
| Perceived Roughness | Achieved with noise maps or grain layers. | Created through anisotropic light scattering caused by oriented micro‑grooves. |
| Performance | Often requires large texture files or shader loops. | Can be generated procedurally with simple math, reducing memory overhead. |
| Scalability | Fixed resolution; zooming reveals pixelation. | Controlled by parameters; remains crisp at any scale. |
| Artistic Control | Limited to pre‑made patterns. | Adjustable in real‑time for dynamic effects (e.g., scratches that follow cursor movement). |
The key insight is that human vision interprets directional micro‑features as surface texture. When light hits those features at varying angles, the reflected intensity changes, giving the illusion of depth. By varying the scratch orientation across the screen, you break up uniformity and produce a natural, ultra‑fine appearance.
Core Mathematics
At the heart of the effect is a directional noise field that dictates the local scratch angle. The most common formulation uses a 2‑D vector field V(x, y) obtained from a noise function:
// Pseudocode (GLSL/HLSL)
float2 uv = screenCoord / resolution;
float2 angle = normalize( noise2D(uv * https://www.amazon.com/s?k=Frequency&tag=organizationtip101-20) );
float2 tangent = float2(-angle.y, angle.x); // Perpendicular direction
// Compute a thin https://www.amazon.com/s?k=line&tag=organizationtip101-20 profile (e.g., Gaussian)
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 https://www.amazon.com/s?k=line&tag=organizationtip101-20 = exp( -pow(dot(uv - center, tangent), 2) / (2.0 * sigma * sigma) );
- frequency controls the scratch density.
sigmadetermines the scratch width (lower = finer).anglevaries smoothly across the surface, giving each scratch its own orientation.
By compositing many such line profiles---often via a sum of absolute values or a max operation---you achieve a layered scratch texture.
Tip: Use Simplex or Perlin noise with a rotation matrix to quickly change the dominant direction without regenerating the whole noise field.
Toolchain Overview
| Stage | Recommended Tools | What It Does |
|---|---|---|
| Noise Generation | ShaderToy, Unity Shader Graph, Unreal Material Editor | Produce a tiled, seamless directional noise map. |
| Scratch Synthesis | Custom GLSL/HLSL fragment shader or GPU Compute | Convert noise into line profiles, apply blur for realism. |
| Post‑Processing | Adobe After Effects (Effects → Stylize → Strobe Light) , Nuke | Add subtle color shifts, grain, or dynamic highlights. |
| Integration | Unity URP/HDRP, Unreal Engine, WebGL (Three.js) | Feed the procedural texture into the material pipeline. |
All steps can be performed at runtime , enabling interactive texture changes (e.g., a user swiping across a screen modifies scratch angles in real time).
Step‑by‑Step Implementation
4.1. Generate a Directional Noise Map
// SimplexNoise2D returns a https://www.amazon.com/s?k=Float&tag=organizationtip101-20 in [-1, 1]
float2 uv = fragCoord.xy / iResolution.xy;
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 n = simplexNoise2D(uv * 12.0); // 12 = https://www.amazon.com/s?k=Frequency&tag=organizationtip101-20
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 angle = n * PI; // Map to [−π, π]
float2 dir = float2(cos(angle), sin(angle));
Higher frequency yields tighter scratch clusters.
4.2. Build the Scratch Profile
float2 offset = uv - 0.5; // Center relative coordinates
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 perpendicular = dot(offset, float2(-dir.y, dir.x));
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 width = 0.002; // 0.2% of screen width = ultra-fine
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 https://www.amazon.com/s?k=line&tag=organizationtip101-20 = exp(- (perpendicular * perpendicular) / (2.0 * width * width));
The exp function creates a Gaussian ridge, which looks like a thin scratch when rendered.
4.3. Accumulate Multiple Layers
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 intensity = 0.0;
for (int i = 0; i < 4; ++i) { // Four overlapping https://www.amazon.com/s?k=layers&tag=organizationtip101-20
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 https://www.amazon.com/s?k=scale&tag=organizationtip101-20 = pow(2.0, https://www.amazon.com/s?k=Float&tag=organizationtip101-20(i));
float2 uvScaled = uv * https://www.amazon.com/s?k=scale&tag=organizationtip101-20;
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 n = simplexNoise2D(uvScaled);
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 angle = n * PI;
float2 dir = float2(cos(angle), sin(angle));
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 perp = dot(offset, float2(-dir.y, dir.x));
intensity += exp(- (perp * perp) / (2.0 * width * width));
}
intensity = saturate(intensity / 4.0);
Stacking layers at different frequencies prevents repetitive patterns and mimics real‑world abrasion where scratches exist at multiple scales.
4.4. Light Interaction
To make the texture feel three‑dimensional, modulate it with a simple Lambertian term based on a fake light direction L:
float3 L = normalize(float3(0.5, 0.8, 1.0)); // Top‑right light
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 ndotl = max(dot(float3(dir, 0.0), L), 0.0);
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 shading = lerp(0.6, 1.0, ndotl); // Darker where light hits shallowly
float3 color = lerp(float3(0.15,0.15,0.15), float3(0.9,0.85,0.8), shading);
color *= intensity;
Now the scratches highlight and shadow realistically as the light moves.
4.5. Optional Motion
For a dynamic feel, animate the noise seed over time:
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 time = iTime * 0.1;
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 n = simplexNoise2D(uv * 12.0 + time);
You'll see the scratch pattern slowly drift , perfect for loading screens or ambient backgrounds.
Practical Tips & Common Pitfalls
| Issue | Cause | Fix |
|---|---|---|
| Banding on low‑end GPUs | Discrete step size in noise → visible stripes. | Dither the final output or use higher‑precision noise (e.g., 16‑bit float). |
| Over‑saturation | Too many overlapping layers amplify intensity. | Normalize by the number of layers; apply a gentle pow(intensity, 0.8) curve. |
| Performance drop | Running many loops in a fragment shader on mobile. | Pre‑bake a low‑frequency directional map to a texture and sample it; only compute fine layers on‑the‑fly. |
| Pattern repetition | Using a too‑small noise tile. | Tile the noise with a prime‑number offset or use a blue‑noise texture for less perceptible repeats. |
| Unrealistic lighting | Light direction not orthogonal to screen space. | Transform both the scratch direction and light direction into the same view space before dot product. |
Real‑World Use Cases
| Domain | Implementation Example | Visual Impact |
|---|---|---|
| Mobile UI | A "paper‑like" scroll view with subtle micro‑scratches that react to finger movement. | Gives a tactile feel without extra assets. |
| Game Environments | Procedural rusted metal panels in a sci‑fi corridor, where the scratches change orientation based on local surface normals. | Adds depth, reduces texture memory. |
| Data Visualization | Overlaying a fine‑scratch grid on a heatmap to help differentiate close values. | Improves readability while keeping the chart clean. |
| Art Installations | Projection-mapped surfaces that display a live‑generated scratch field responding to ambient sound frequencies. | Creates an immersive, ever‑changing texture. |
Extending the Technique
- Combine with Height Maps -- Encode the scratch intensity as a height map and run a screen‑space ambient occlusion (SSAO) pass for extra shadow detail.
- Multi‑Material Blending -- Blend the scratch layer with other procedural textures (e.g., noise‑based grime) using a lerp driven by a curvature map.
- Temporal Coherence -- Store the previous frame's noise seed and interpolate slowly to avoid sudden jumps, useful for cinematic shots.
- Non‑Uniform Width -- Modulate
sigma(scratch width) with another noise channel to simulate wear patterns that are thicker in high‑stress zones.
Quick Reference Code Snippet (GLSL)
// Ultra‑Fine Variable‑Angle Scratch Shader
// -------------------------------------------------
uniform vec2 iResolution;
uniform https://www.amazon.com/s?k=Float&tag=organizationtip101-20 iTime;
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 simplexNoise2D(vec2 p);
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord / iResolution;
vec2 center = uv - 0.5;
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 intensity = 0.0;
const int https://www.amazon.com/s?k=layers&tag=organizationtip101-20 = 4;
const https://www.amazon.com/s?k=Float&tag=organizationtip101-20 BASE_WIDTH = 0.0015; // ultra‑fine
for (int i = 0; i < https://www.amazon.com/s?k=layers&tag=organizationtip101-20; ++i) {
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 https://www.amazon.com/s?k=scale&tag=organizationtip101-20 = pow(2.0, https://www.amazon.com/s?k=Float&tag=organizationtip101-20(i));
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 n = simplexNoise2D(uv * https://www.amazon.com/s?k=scale&tag=organizationtip101-20 + iTime * 0.05);
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 a = n * 3.1415926;
vec2 dir = vec2(cos(a), sin(a));
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 perp = dot(center, vec2(-dir.y, dir.x));
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 w = BASE_WIDTH / https://www.amazon.com/s?k=scale&tag=organizationtip101-20; // thinner for higher frequencies
intensity += exp(- (perp * perp) / (2.0 * w * w));
}
intensity = https://www.amazon.com/s?k=clamp&tag=organizationtip101-20(intensity / https://www.amazon.com/s?k=Float&tag=organizationtip101-20(https://www.amazon.com/s?k=layers&tag=organizationtip101-20), 0.0, 1.0);
// Simple https://www.amazon.com/s?k=lighting&tag=organizationtip101-20
vec3 L = normalize(vec3(0.6, 0.7, 1.0));
vec3 N = vec3(dir, 0.0); // pseudo normal from scratch direction
https://www.amazon.com/s?k=Float&tag=organizationtip101-20 ndotl = max(dot(N, L), 0.0);
vec3 baseColor = mix(vec3(0.12,0.12,0.12), vec3(0.9,0.85,0.78), ndotl);
fragColor = vec4(baseColor * intensity, 1.0);
}
Simply paste this into a ShaderToy or integrate it into your engine's material system, tweak BASE_WIDTH, LAYERS, and iTime speed to suit the visual style you desire.
Closing Thoughts
Variable‑angle scratches turn a simple mathematical noise into a high‑fidelity texture that scales, animates, and reacts to light---all without bloating your asset pipeline. By mastering the steps above, you can give any screen surface---whether a UI component, a game asset, or an artistic canvas---a tactile, ultra‑fine presence that feels both organic and efficient.
Happy scratching! 🎨🚀