Monday, 13 April 2020

Lighting [1.14.4+]

Minecraft uses three different lighting models depending on what it is rendering:

  1. No lighting - typically for GUI screens.
  2. Item Lighting - uses OpenGL light source models, which are used when rendering items in GUI screens such as inventory slots
  3. World lighting,  which is used for objects which become brighter or darker depending on the amount of light in the world (light from the sky and/or nearby glowing objects).

Item Lighting

Item lighting models two light sources chosen to ensure the rendered items look three-dimensional.  It uses vertex normal information to perform shading, when rendered using an appropriate RenderType.

World Lighting 

The basic world lighting model in Minecraft is made up of two components; Sky Light and Block Light.  Each grid location in the world has a value between 0 - 15 for Sky Light and also a value between 0 - 15 for Block Light.
  1. Sky Light is the light that this block is receiving from the sky, either directly (can see the sky) or indirectly (is near blocks that can directly see the sky).
  2. Block Light is the light that this block is receiving from other light sources nearby (torches, glowstone, etc). 
See here for some more background information.

Vanilla calculates the Sky Light and Block Light for each block.  The basic concepts are:
  1. Each block has an Opacity value which determines how much light it receives from its six neighbours.  A low opacity (eg 1) means the light level decreases by 1 from its brightest neighbour, higher opacity (eg 3) leads to a correspondingly larger decrease.  This calculation is performed separately for both the Sky Light and the Block Light.
  2. Some blocks emit light (eg glowstone) - if this is more than the Block Light coming from its neighbours, the Block Light is set to this emitted value.
  3. If the block has only open sky or transparent blocks above it, its Sky Light is 15.
Two-dimensional example for propagation of Block Light, with opacity = 2.  Sky Light is propagated in the same way.
When the block is rendered, the Block Light and Sky Light values for the block are used to set the brightness.  This is done as follows:
  1. The Block Light and Sky Light values are combined into a single 32-bit integer.  The upper 16 bits contain the Sky Light multiplied by 16, and the lower 16 bits contain the Block Light multiplied by 16.
  2.  this combined "mixed brightness" value, which is interpreted by the renderer as two 16 bit coordinates (i.e. [BlockLight, SkyLight]) for a texture called the lightMap texture - see picture below.  
  3.  When the block face is renderered, it first draws the icon texture then "modulates" it using the appropriate texel from the lightMap, i.e. as given by the [BlockLight, SkyLight] coordinate.  (This process is called Multitexturing).
The lightMap texture (OpenGlHelper.lightmapTexUnit), which has 16x16 texels .  Each texel corresponds to a combination of Block Light and Sky Light.
Why does Minecraft bother to do it this way?  There are at least two good reasons
  1. Treating Block Light and Sky Light separately allows more realistic lighting effects.  Looking at the lightMap texture you can see that Block light imparts a reddish/brownish colour whereas Sky Light does not.  This simulates the yellowy light produced by (eg) a torch.
  2. The lightMap can be updated very easily to change the lighting of the entire scene- for example depending on the time of day (eg full sun, sunset); to create the effect of a flickering torch; or when the player drinks a night vision potion.  This updating is performed by EntityRenderer.updateLightMap().

Face-dependent lighting effects

In addition to the per-block lighting described above, Minecraft can also perform some extra lighting calculations for each block face to increase the realism of the lighting:

1-Direction-dependent face brightness

This method renders the six faces with a different lighting intensity depending on which direction the faces points:
  1. Top face (Ypos) is at full intensity
  2. Bottom face (Yneg) is 50% intensity
  3. Z-side faces (Zpos, Zneg = South, North) are 80% intensity
  4. X-side faces (Xpos, Xneg = East, West) are 60% intensity.
Direction-dependent face brightness: off (left) vs on (right)

This lighting is applied to each face; when the texture is rendered it is multiplied by the "colour" value (in this case a shade of grey).

In some cases an RGB colour multiplier is also passed in by the caller (for example underwater gives blocks a blue tint) which is multiplied by the % intensity described above.

The lighting of the face is also affected by the [BlockLight, SkyLight] mixed-brightness in the usual way.  The diagram below summarises these steps.

Schematic showing how Minecraft applies lighting effects:
(1a) face-dependent intensity and (1b) colour multiplier, then (2) [Blocklight,Skylight] lightmap

2- Ambient Occlusion

Ambient Occlusion is a method for calculating the amount of light shining on a surface based on nearby blocks.  A brief introduction on the approach used by minecraft is here.   Briefly, for each vertex, an average light intensity is calculated from the three adjacent blocks plus the block which touches the face.  In the example below for the red circled vertex this is the three blocks in white plus the block resting on top of the face (not shown).  If all these four blocks are transparent, the "Ambient Occlusion" light intensity will be 1.0.  Each opaque block reduces light received by the vertex - if all four blocks are opaque the intensity will be 0.2.  This "Ambient Occlusion" value is then multiplied by the colour multiplier for that vertex.
The Mixed Brightness for the vertex is calculated as the average of these four adjacent blocks.
 (There are some extra details about whether the corner block is included or not, but you get the basic idea).
 OpenGL applies smoothing when drawing the face, so that the lighting effect is graded smoothly over the face between the vertices.

Ambient Occlusion algorithm.  Diagram adapted from 0fps.


Further Details

When a quad is rendered in the world, a number of different types of lighting effects may be applied:
· A ‘diffuse lighting’ (“shade”) where the brightness is changed depending on which direction the quad is facing.
· A multitexturing brightness: BlockLight+SkyLight
· Ambient occlusion (if enabled): the four corners of the face will be modified to
o    Be darker at vertices where opaque blocks are next to the vertex
o    Use the skylight+blocklight at that corner instead of a uniform value for the entire face
 
The blocklight+skylight is calculated depending on where the face is located:
· Inside the cube = the block itself
· At the edge of the cube: from the neighbour in the direction that the face is pointed (eg east)
The blocklight+skylight is generally calculated as:
· For blocks with isEmissiveRendering() == true, maximum
· For others: the calculated skylight, plus the calculated blocklight (from nearby blocks) or blockState.getLightValue(), whichever is higher.

Interesting vanilla classes to explore

BlockModelRenderer::renderModelSmooth (ambient occlusion rendering)
BlockModelRenderer::renderModelFlat (no ambient occlusion)
ILightReader
LightTexture (the multitexture used for blocklight+skylight)
WorldRenderer::getPackedLightmapCoords
 

Further Information

Wiki about Light
Ambient Occlusion

1 comment:

  1. Hey man, I'd just like to let you know that you have been a lifesaver. Your site explains everything so well, I was looking for an explanation like this for so long. Thanks again. And please put a donate button I would love to support genuinely helpful people like you man.

    ReplyDelete