
Grain sphere
a shader study
Overview
A GLSL shader generating animated grain over spherical geometry, layering noise at different rates to read as film.
The idea
Film grain does not belong to what is being filmed. It belongs to the stock. That is why, when the camera moves, the grain does not move with the scene: it stays where it is, and the image travels behind it.
Almost every grain shader does the opposite — noise over surface coordinates, travelling glued to the object — and that is exactly why they read as a texture rather than as film.
The grain lives on the screen, not on the object
The decision everything else follows from is a single line: the noise is
evaluated over gl_FragCoord, the pixel coordinate, and not over UVs or the
normal.
float g = hash12(gl_FragCoord.xy);
float on = step(g, uParticleDensity);Which pixels are particles is settled once and never changes. The sphere rotates, breathes and travels underneath a skin that stays still.
This has a consequence I had not planned for, and which ended up being the reason to keep it: the sphere and the background particle field share one skin. They are two different objects, with different geometry, evaluating the same function over the same screen coordinate. Where they overlap, the grain lines up exactly.
The hash is Dave Hoskins', without sin(). The sine version is the one everyone
copies, and it produces grid artefacts as soon as resolution goes up, because
its periodicity aligns with the pixel's.
/media/projects/grain-sphere-shader/01-screen-space.webpThe wave travels through the body
If every lit pixel were equally bright this would be static. What gives it shape is a planar wave travelling through the volume:
float w1 = 0.5 + 0.5 * sin(
dot(vWorldPos, normalize(vec3(0.8, 0.45, 0.35))) * uWaveFreq
- uTime * uWaveSpeed);The dot product of world position and a fixed direction collapses space into a single figure: distance along that direction. The wavefronts are planes sweeping the sphere, so the lit band wraps the body as it advances rather than sliding across its surface. A second, much fainter wave in another direction breaks up the regularity of the first.
/media/projects/grain-sphere-shader/02-wavefront.webpThe limb
Volume is still missing. Without it the sphere is a disc: a round silhouette filled with grain, with no sign that a face is turned toward the camera.
float ndv = clamp(dot(normalize(vNormal), viewDir), 0.0, 1.0);
float volume = 0.25 + 0.75 * smoothstep(0.0, 0.9, ndv);
float W = pow(wave, 1.8) * volume;What faces the camera lights up; what turns away melts to 25%, not to zero — an edge that reaches zero clips against the background and reads as a disc again, this time with a hard outline.
The pow(wave, 1.8) is what separates the wave from the ground. Without it a
sine's minimum is still light, and the whole body stays faintly lit all the
time.
Breathing
The geometry does exactly one thing, in the vertex shader: the whole sphere swells and settles, multiplying position by an oscillating factor. It is not a per-vertex deformation, it is one mass changing size.
It is the only movement the sphere owns. Everything else — the route poses, the scale on navigation — is written from outside by GSAP, and that separation is the same rule the rest of the site follows: one writer per property.


