Block Land 12

From DaveWiki

Jump to: navigation, search

Last Time we added a variety of improved lighting effects to the world including shadows and we'll continue in this article to look at lighting effects.

MineCraft Lighting Model

Our world is looking pretty good but we'll take a quick side excursion to try and duplicate MineCraft's lighting model in our block world. We probably won't end up using it, at least in full, but it may lead to improvements in our existing lighting model. Building a quick "roof" in a 20x20 section in MineCraft shows us a few things right away:

Exploring MineCraft's Lighting Model
  • There are 16 light levels (as we knew already from MineCraft's documentation).
  • The last level is not completely black but is close.
  • There is no "ambient" light during the day like in our model.
  • The block light is not dependent on how many blocks there are above another block. It only takes one block to cast a shadow. Additional blocks have no further effect.
  • The block light is dependent in the horizontal direction how far away it is from an open lite block.

Trying to us these rules to recreate the lighting model is not too difficult. First we'll create a method to make a test world to demonstrate the lighting model more explicitly:

void TestBlockLandApplication::initWorldBlocksTestLight()
{
	int x, y, z, i;
 
                //Fill in the bottom half of the world with grass/soil
	for (y = 0; y < WORLD_SIZE/2; ++y)
	{
		for (z = 0; z < WORLD_SIZE; ++z)
		{
			for (x = 0; x < WORLD_SIZE; ++x)
			{
				GetBlock(x, y, z) = 1;
			}
		}
	}
 
	int StartX = 1;
	int StartZ = 1;
	int StartY = WORLD_SIZE/2 + 10;
 
                //Create a bunch of square floating planes from 1 to 40 blocks in size
	for (i = 1; i < 40; i += 2)
	{
		if (StartX + i >= WORLD_SIZE)
		{
			StartX = 1;
			StartZ += i + 1;
		}
 
		for (z = 0; z < i; ++z)
		{
			if (StartX + i >= WORLD_SIZE) break;
			if (StartZ + i >= WORLD_SIZE) break;
 
			for (x = 0; x < i; ++x)
			{
				GetBlock(x + StartX, StartY, z + StartZ) = 2;
			}
		}
 
		StartX += i + 1;
	}
 
}

Next we'll implement a method to try and duplicate the lighting model. The basic steps in the algorithm are:

  • Initialize the light levels for all blocks and find the "ground level" for each (X,Z) point. After this stage each block will either be fully lit (light level = 15) or if below ground level have the next lowest light level (14). At this point we'll just consider any non-air block to cast shadows.
  • Start processing blocks one Y-level at a time starting at the top of the world beginning in the +X direction. Starting at the top of the world and working downwards is important as it lets the light "filter" downwards through the air which is necessary for non-trivial block world shapes.
  • For each point if we're above ground the light level is reset to full (15). If underground the light level is reduced by one level for each block iteration.
  • We'll repeat the above two steps for the -X, +Z, and -Z direction with the additional rule of only changing the light level of a block if it makes it lighter than its existing light level.

This sounds more complex than it actually is, although it did take me a few attempts to get the details right in the following code:

void TestBlockLandApplication::initWorldBlocksLightMineCraft()
{
	int x, y, z;
	blocklight_t DeltaLight = 16;
	blocklight_t DeltaLight1 = 16;
	int HeightMap[WORLD_SIZE][WORLD_SIZE];
 
               //Initialize the world's block light levels
	for (z = 0; z < WORLD_SIZE; ++z)
	{
		for (x = 0; x < WORLD_SIZE; ++x)
		{
			for (y = WORLD_SIZE - 1; y >= 0; --y)
			{
				GetBlockLight(x, y, z) = 255;
 
				if (GetBlock(x, y, z) != 0)
				{
					HeightMap[x][z] = y;
					break;
				}
			}
 
			for (--y; y >= 0; --y)
			{
				GetBlockLight(x, y, z) = 255 - DeltaLight1;
			}
		}
	}
 
	block_t LastBlock;
 
                //Start from the top of the world and work downwards
	for (y = WORLD_SIZE - 1; y >= 0; --y)
	{	
		for (z = 0; z < WORLD_SIZE; ++z)
		{
			int LightLevel = 0;
 
                                 //Process blocks in the +X direction
			for (x = 0; x < WORLD_SIZE; ++x)
			{
				int GndHeight = HeightMap[x][z];
				block_t Block = GetBlock(x, y, z);
 
				if (GndHeight <= y) 
				{
					LightLevel = 255 - DeltaLight1;
					continue;
				}
 
				GetBlockLight(x, y, z) = LightLevel;
 
				if (y < WORLD_SIZE - 1 && GetBlock(x, y+1, z) == 0)
				{
					int Light1 = GetBlockLight(x, y+1, z);
					LightLevel = Light1;
					GetBlockLight(x, y, z) = LightLevel;
				}
				else if (LightLevel > 0) 
				{
					LightLevel -= DeltaLight;
					if (LightLevel < 0) LightLevel = 0;
				}
			}
 
			LastBlock = 1;
 
                                 //Process blocks in the -X direction
			for (x = WORLD_SIZE - 1; x >= 0; --x)
			{
				int GndHeight = HeightMap[x][z];
				int BlockLight = GetBlockLight(x, y, z);
				block_t Block = GetBlock(x, y, z);
 
				if (GndHeight <= y) 
				{
					LightLevel = 255 - DeltaLight1;
					continue;
				}
 
				if (BlockLight < LightLevel) 
				{
					GetBlockLight(x, y, z) = LightLevel;
				}
				else if (LastBlock == 0)
				{
					LightLevel = BlockLight;
				}
 
				if (LightLevel > 0) 
				{
					LightLevel -= DeltaLight;
					if (LightLevel < 0) LightLevel = 0;
				}
 
				LastBlock = Block;
			}
		}
 
		LastBlock = 1;
 
		for (x = 0; x < WORLD_SIZE; ++x)
		{
			int LightLevel = 0;
 
                                 //Process blocks in the +Z direction
			for (z = 0; z < WORLD_SIZE; ++z)
			{
				int GndHeight = HeightMap[x][z];
				int BlockLight = GetBlockLight(x, y, z);
				block_t Block = GetBlock(x, y, z);
 
				if (GndHeight <= y) 
				{
					LightLevel = 255 - DeltaLight1;
					continue;
				}
 
				if (BlockLight < LightLevel)
				{
					GetBlockLight(x, y, z) = LightLevel;
				}
				else if (LastBlock == 0)
				{
					LightLevel = BlockLight;
				}
 
				if (LightLevel > 0) 
				{
					LightLevel -= DeltaLight;
					if (LightLevel < 0) LightLevel = 0;
				}
 
				LastBlock = Block;
			}
 
			LastBlock = 1;
 
                                 //Process blocks in the -Z direction
			for (z = WORLD_SIZE - 1; z >= 0; --z)
			{
				int GndHeight = HeightMap[x][z];
				int BlockLight = GetBlockLight(x, y, z);
				block_t Block = GetBlock(x, y, z);
 
				if (GndHeight <= y) 
				{
					LightLevel = 255 - DeltaLight1;
					continue;
				}
 
				if (BlockLight < LightLevel)
				{
					GetBlockLight(x, y, z) = LightLevel;
				}
				else if (LastBlock == 0)
				{
					LightLevel = BlockLight;
				}
 
				if (LightLevel > 0) 
				{
					LightLevel -= DeltaLight;
					if (LightLevel < 0) LightLevel = 0;
				}
 
				LastBlock = Block;
			}
		}
	}
 
}

Before testing this in our block world we have to temporarily disable the existing lighting model which depends on the time of day and also set the ambient light level to 0. Using the new test world and lighting model we get:

Overview of the Test World with MineCraft's Lighting Model
Close Up of the Test World with MineCraft's Lighting Model

This doesn't look too bad but there is one thing noticeably off in our light model. The 16 light levels are not evenly distributed. Looking at the MineCraft screen shot above the light levels as the blocks get darker appear at least roughly the same brightness step. In our model, however, the first several lighting levels have only a small difference in brightness while the darkest few levels have a much larger brightness level. The reason for this is probably due to an incorrect gamma level (for example, see GPU Gems 3 Chapter 24 for a good description of the effect). We can manually adjust the gamma level by using a different distribution of the lighting levels.

To adjust the light level we'll add one class member and two methods:

class TestBlockApplication : public BaseApplication {
....
      float m_LightLevels[16];
};
 
 
void TestBlockLandApplication::MakeLogLightArray (void)
{
	float Light0 = 0.05f;
	float Light14 = 0.85f;
	float Light15 = 1.0f;
 
	for (int i = 1; i < 14; ++i)
	{
		m_LightLevels[i] = pow((Light14 - Light0)/15.0f * i, 2.0f)*1.25 + 0.07f;
	}
 
	m_LightLevels[0] = Light0;
	m_LightLevels[14] = Light14;
	m_LightLevels[15] = Light15;
}
 
 
void TestBlockLandApplication::MakeLinearLightArray (void)
{
	float Light0 = 0.0f;
	float Light15 = 1.0f;
 
	for (int i = 0; i < 15; ++i)
	{
		m_LightLevels[i] = (Light15 - Light0)/16.0f * (i + 1);
	}
 
	m_LightLevels[0] = Light0;
	m_LightLevels[15] = Light15;
}

The methods should be relatively straightforward. The linear method just duplicates what we were doing previously while the perhaps incorrectly named "log" method adjusts the gamma of the lighting levels to try and make them appear more even (the exact numbers used took a bit of playing to get right). To use the new light level array member we'll update the mesh chunking method:

void TestBlockLandApplication::createChunkCombineMat (Ogre::ManualObject* MeshChunk, const int StartX, const int StartY, const int StartZ)
{
...
                        for (int x = StartX; x < CHUNK_SIZE + StartX; ++x)
			{
                                ...
				float BaseLight = m_LightLevels[(int)(GetBlockLight(x, y, z) / 16.0f)];
				float BottomLight = y > 0 ? m_LightLevels[(int)(GetBlockLight(x, y-1, z) / 16.0f)] : BaseLight;
...
}

Running this reveals:

Gamma Adjusted Lighting Levels

While not perfect the lighting levels are definitely much more even than previously. Trying this lighting model now in a complete world results in:

Block World Using a MineCraft-Like Lighting Model
Looking Down a Cave With Our New Lighting Model
Example of a Strangely Lit Block

Some notes and comments on this attempt to recreate MineCraft's lighting model:

  • Caves in general look much better with this lighting model and closer to caves in MineCraft.
  • Performance for this model is worse than the previous model as we have to essentially do 5 completely passes over each block in the world where as before we were doing only 1-2 passes per block. The overall time is ~0.5 seconds on my mid-end computer which is still decent, however, particularly since we haven't attempted to optimize anything yet.
  • There are still some odd-lite blocks in the world indicating we haven't completely duplicated MineCraft's model. For example there are still some odd "black" blocks found in caves and on steep slopes there are oddly-lite blocks.
  • Combining this model with our light angle/shadow model may be difficult (or at least non-intuitive or straight forward). The ultimate method we use has to both look good in all situations and be fast enough to allow for dynamically adjusting the light level/angle in real time.
Personal tools