Rendering

Back to Lambent

This page explains what Lambent does in a frame. You need it to work on Lambent itself, to draw something in step with its lights, or to work out why a light looks wrong.

When it draws

Lambent hooks the game renderer right after the level is drawn and before the hand:

  1. A mixin on GameRenderer.renderLevel, right after it has the level drawn, keeps the view the level was drawn with: model view, projection (with view bobbing and the nausea warp in it), camera and partial tick.
  2. Then (and, under a shader pack, after the pack’s last pass) LightRenderer.afterLevel runs:
    • every source is asked for its lights (Frame);
    • the spot lights’ shadow maps are drawn;
    • the light pass lights the scene;
    • every AfterLights registered with LambentClient.afterLights draws.

So everything Lambent draws, and everything drawn after it, sits over the finished level, a shader pack’s included.

One frame

Step Class What it does
Collect Frame Sources hand over their lights, up to the room for each kind.
Shut-in air Enclosure A few short block probes along each beam, eased toward, say how shut in the air is: more dust in a barn than in a field.
Shadow blocks Occluders The blocks round each beam as meshes, built a little each frame and kept by key. See below.
Shadow maps ShadowMaps Each spot light’s depth from just behind its lamps, one tile of a shared atlas each: the block meshes, then every entity in the beam but the holder.
Point shadows Solids, Skyline Only while a point light is lit: which blocks round the eye stop light, as a 3D texture and a height map.
Bloom Bloom The lenses at half size where the scene shows them, blurred twice into a glow and a halo.
Dust Dust Motes placed and lit on the GPU, drawn as specks over the scene behind its depth.
Light pass LightPass, lamps.fsh The light in the air at half size, then the lit scene.
After AfterLights Whatever mods draw over the lights.

Shadow blocks

The shadow map does not draw the level’s chunk meshes. It draws meshes of its own, built from the blocks round each beam:

The meshes cover a margin round the beam, longer in the direction the holder is moving, so they last a while. They are rebuilt a little each frame: when the beam nears the edge, at once when a block in them changes (a block change mixin reports it), and every so often in case one changed unseen. The old mesh is drawn until the new one is ready, so a rebuild never holds up a frame.

The light pass

lamps.fsh runs twice:

  1. The air, at half size, because it is soft and the costliest part: how much light each beam and point light scatters toward the eye along the view ray, through the shadow maps, with the haze thicker where the air is shut in.
  2. The lit scene: each pixel’s world position from the depth buffer, lit by every beam that reaches it (shadow-mapped, tinted through glass), every point light inside its box and not blocked, plus the air from step 1, the glows and their glare, and the bloom.

The added light is weighed against ambient, the lightmap color the scene was drawn with round each light, so a lamp brightens a dark surface by about what it should and never turns it past white.

Where the hardware lets a pass read the pixel it writes (a texture barrier), the lit scene goes straight over the scene and leaves untouched what no light reaches. Elsewhere it is written to a target of its own and copied back.

Drawing after the lights

LambentClient.afterLights((modelView, projection, camera, partialTick) -> {
    // draw over the lit level, with the view it was drawn with
});

Veltorque draws its smoke and fire here, so they sit over the lit level and in front of the beams’ haze.

While the shadow maps draw

Entities in a beam are drawn into its shadow map with their own renderers. During that, LambentClient.drawingShadows() returns true, and:

Shader packs

Lambent checks net.irisshaders.iris.api.v0.IrisApi.isShaderPackInUse() by reflection, so Iris and Oculus are optional. Under a pack, entity shadows in the beams are off (the pack’s programs replace the entity renderers’), and Lambent’s own programs, buffers and meshes keep the pack from standing in for them.

Lambent’s GL state

Lambent links its own GL programs and binds its own vertex arrays and buffers on every version. It puts back the state Minecraft tracks (the bound program, textures, vertex array, blend and depth) afterwards. On 1.20.1 and 1.21.1, two helpers are public for mods that draw with their own programs after the lights:

int program = LambentClient.program(mc, vertexId, fragmentId, "Position");   // throws IOException
LambentClient.texture(unit, textureId);                                       // leaves GL_TEXTURE0 active

On 26.x the helpers are gone. Lambent sets state through GlStateManager wherever Minecraft keeps its own copy, unbinds Minecraft’s sampler objects from the units it uses, and from 26.2 handles Minecraft’s reversed depth. It needs the OpenGL backend: on Vulkan it draws no lights. See Version differences.