YatoVoid

index / notes / lumen

lumen

A small HolyC program that opens a window, creates an OpenGL 3.3 context, compiles a fragment shader, and runs it every frame with a bloom pass on top. The pictures are drawn on the GPU; everything around them is HolyC.

HolyC (hcc), SDL2, OpenGL 3.3, GLSL ยท MIT source

1 Scenes

There are four. Each one is a single .frag file, and keys 1 to 4 switch between them while the program runs. All of these clips were rendered by lumen itself, not screen-recorded.

raymarch: a signed distance field scene with soft shadows, ambient occlusion and one mirror bounce. Drag to orbit.
fractal: a Mandelbulb whose power moves between 4 and 8, coloured by an orbit trap.
particles: 140 comets on tilted elliptical orbits. Hold the mouse to pull them.
plasma: four summed sine fields through a cosine palette. The smallest scene.

2 What happens in a frame

Scenes render in linear HDR, so a light eight times brighter than white stays eight times brighter until the very end. Then:

  1. Scene pass. The scene shader draws into an RGBA16F texture. Values above 1.0 survive.
  2. Bright pass. The first blur pass keeps only what exceeds 1.0, with a soft knee so the cutoff doesn't show up as an edge.
  3. Blur. A 9-tap Gaussian, done as 5 bilinear taps, runs horizontally into one half-resolution texture and vertically into another. The pair repeats four times, each round blurring the last, so the glow spreads further than one kernel can.
  4. Composite. Scene plus blur, tone mapped with an ACES curve and gamma corrected into the window.

Splitting the 2D Gaussian into two 1D passes turns N×N texture reads per pixel into 2N. Doing it at half resolution divides the cost by four again, and the blur hides the detail that was lost.

Bloom off on the left of the divider, on to the right. Without it, bright pixels clip to white and stop there.

There are no vertex buffers. The vertex stage makes one triangle from gl_VertexID that covers the screen, so every pixel goes through the scene's fragment function.

3 Writing a scene

A scene defines mainImage and returns linear colour. It doesn't do gamma or tone mapping; the composite pass does. This is the whole plasma scene:

void mainImage(out vec4 c, in vec2 fc)
{
  vec2 uv = (fc - 0.5 * iResolution.xy) / iResolution.y;
  float v = sin(uv.x * 8.0 + iTime) + sin(uv.y * 9.0 - iTime * 1.3)
          + sin((uv.x + uv.y) * 7.0 + iTime * 0.7)
          + sin(length(uv) * 14.0 - iTime * 2.0);
  vec3 col = 0.5 + 0.5 * cos(vec3(0.0, 2.1, 4.2) + v);
  float ridge = pow(0.5 + 0.5 * sin(v * 1.5 - iTime), 8.0);
  c = vec4(col * (0.35 + 3.2 * ridge), 1.0);
}

The host adds the #version line and three uniforms in front, and a main that calls mainImage after it:

UniformTypeMeaning
iResolutionvec3framebuffer width and height in pixels
iTimefloatseconds since start, or frame index over fps when rendering offline
iMousevec4x and y in pixels; z is 1.0 while the left button is held

The prelude ends with #line 1, so compile errors point at the right line of the scene file.

4 Hot reload

The running scene's file is read again every 250 ms. Save it and the new version compiles in place. If it doesn't compile, the error goes to the terminal and the old version keeps running, so a typo never blanks the window.

5 Offline rendering

--render draws frames into a hidden window at a fixed time step and writes them out as PPM files. Time comes from the frame index, not the clock, so the output is the same on every run and on every machine. scripts/record.sh wraps this and encodes the mp4 and gif with ffmpeg; --compare makes the bloom off/on clips.

./build/lumen --render <scene> <out-dir> <frames> <fps> <width> <height> <bloom 0|1>
scripts/record.sh [scene...]
scripts/record.sh --compare [scene...]

6 Running it

You need hcc, SDL2, and a driver with OpenGL 3.3.

hcc src/main.HC -o build/lumen
./build/lumen            # raymarch
./build/lumen fractal    # any file in shaders/
KeyAction
1–4switch scene
Btoggle bloom
Rreload the current scene
Escquit
left dragorbit the camera (raymarch, fractal) or pull the particles
FileWhat's in it
src/main.HCwindow, scene loading, hot reload, offline renderer
src/post.HCframebuffers, bloom passes, final composite
src/gl.HCOpenGL 3.3 declarations, shader compile and link
src/sdl2.HCSDL2 declarations and input
shaders/the scenes, plus blur and composite in post/

7 Limits

The SDL2 and OpenGL declarations come from the HolyC Graphics Guide. The distance functions follow Inigo Quilez's articles.

← all notes