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.
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.
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:
- Scene pass. The scene shader draws into an
RGBA16Ftexture. Values above 1.0 survive. - 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.
- 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.
- 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.
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:
| Uniform | Type | Meaning |
|---|---|---|
iResolution | vec3 | framebuffer width and height in pixels |
iTime | float | seconds since start, or frame index over fps when rendering offline |
iMouse | vec4 | x 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/
| Key | Action |
|---|---|
| 1–4 | switch scene |
| B | toggle bloom |
| R | reload the current scene |
| Esc | quit |
| left drag | orbit the camera (raymarch, fractal) or pull the particles |
| File | What's in it |
|---|---|
src/main.HC | window, scene loading, hot reload, offline renderer |
src/post.HC | framebuffers, bloom passes, final composite |
src/gl.HC | OpenGL 3.3 declarations, shader compile and link |
src/sdl2.HC | SDL2 declarations and input |
shaders/ | the scenes, plus blur and composite in post/ |
7 Limits
- Bloom threshold, strength and number of rounds are constants at the top of
src/post.HC, not uniforms yet. - Tested on Linux with Mesa on an Intel GPU. macOS should need
-clibs "-framework OpenGL"and hasn't been tried.
The SDL2 and OpenGL declarations come from the HolyC Graphics Guide. The distance functions follow Inigo Quilez's articles.