Block Land 11

From DaveWiki

Jump to: navigation, search

Last Time we added the effect of light angle and color to the block world to implement a basic day/night cycle. This time we'll look at more lighting effects.

Trees

Trees may be an odd way to start adding lighting effects but my plan will hopefully become apparent later on. MineCraft trees are composed of two block types, logs and leaves, and we'll follow the same approach. In our current world implementation to add more block types we have to:

  • Update the texture atlas to add the block textures.
  • Update the mesh creation method to properly calculate the texture UV coordinates.
  • Update the world's block information array.

This leaves us with the following simple changes:

                //New block information
	{ 6, "Log", "", false },
	{ 7, "Leaves", "", true },
...
                //Use the 7 block texture atlas
        createTexture("Combine7", "combine_7.tga");
...
                //Compute the correct block texture coordinates
        V1 = 1.0f/7.0f * (float)(Block - 1);
	V2 = V1 + 1.0f/7.0f;

Now to add some trees to the world we'll create two new methods:

       //Return the ground height at the specific spot or -1
int TestBlockLandApplication::FindWorldGroundHeight (const int x, const int z)
{
	if (x < 0 || z < 0 || x >= WORLD_SIZE || z >= WORLD_SIZE) return -1;
 
	for (int y = WORLD_SIZE - 1; y >= 0; --y)
	{
		block_t Block = GetBlock(x, y, z);
		if (Block > 0 && Block < 5) return (y);
	}
 
	return -1;	
}
 
void TestBlockLandApplication::initWorldTrees (void)
{
	int NumTrees = 100;    //Change to add more or less trees to the world
 
	for (int i = 0; i < NumTrees; ++i)
	{
			//Choose the tree position
		int x = rand() / 32768.0f * WORLD_SIZE;
		int z = rand() / 32768.0f * WORLD_SIZE;
		int y = FindWorldGroundHeight(x, z);
 
		if (y <= 0) continue;
 
		int TrunkHeight = rand() % 5 + 4;
 
			//Create the tree trunk
		for (int y1 = y + 1; y1 < y + 1 + TrunkHeight && y1 < WORLD_SIZE; ++y1)
		{
			block_t& Block = GetBlock(x, y1, z);
			if (Block == 0) Block = 6;
		}
 
                        //Create the leaves
		for (int j = 0; j < 64; ++j)
		{
			int x1 = rand() % 5 + x - 2;
			int y1 = rand() % 5 + y + TrunkHeight - 2;
			int z1 = rand() % 5 + z - 2;
 
			if (x1 < 0 || y1 < 0 || z1 < 0) continue;
			if (x1 >= WORLD_SIZE || y1 >= WORLD_SIZE || z1 >= WORLD_SIZE) continue;			
 
			block_t& Block = GetBlock(x1, y1, z1);
			if (Block == 0) Block = 7;
		}
	}
}

The tree creation is pretty basic:

  • Choose a random spot in the world.
  • Find the ground height at that spot.
  • Create a tree trunk 4 to 8 blocks high.
  • Create a random number of leaves in a 5x5x5 block area around the top of the trunk.

We would like our leaf texture to have some transparent areas like in MineCraft. On the texture side this is easy enough to do by creating an image with an alpha layer (which is why we've changed from PNG to TGA texture image as our image program can't put an alpha layer in a PNG). We also need some minor changes to our material definition to make our leaves see-through:

	pass->setSceneBlending(Ogre::SBT_REPLACE);
	pass->setAlphaRejectSettings (Ogre::CMPF_GREATER_EQUAL, 128);
	pass->setCullingMode(Ogre::CULL_NONE);
	pass->setManualCullingMode (Ogre::MANUAL_CULL_NONE);

The culling mode options are optional and affect whether the back side of the leaves are seen or not (with culling on the back side is not shown and performance may be better). We'll also make a change to the block light model:

void TestBlockLandApplication::initWorldBlocksLight()
{
	int x, y, z;
	blocklight_t Light;
	blocklight_t DeltaLight = 16;
 
	for (z = 0; z < WORLD_SIZE; ++z)
	{
		for (x = 0; x < WORLD_SIZE; ++x)
		{
			Light = 255;
 
			for (y = WORLD_SIZE - 1; y >= 0; --y)
			{
				GetBlockLight(x, y, z) = Light;
 
				if (GetBlock(x, y, z) > 0) 
				{
					if (Light == 255)
						Light -= 128;
					else if (Light >= DeltaLight)
						Light -= DeltaLight;
					else
						Light = 0;
				}
			}
		}
	}
}

The effect of the change is to make a "shadow" underneath the trees more apparent and to make the leaves generate a shadow as well. The leafy result of all these changes is:

Basic Trees in Our Block World

Just the simple addition of some trees makes our world look a lot better and surprisingly MineCraft like (I've resisted the urge to use the MineCraft texture images so far). One thing to note is that we didn't have any issue with the transparent leaf texture like we did with water a few articles ago because we're not dealing with partial transparency in the material. By using full transparency in the leaves we can use different material options and avoid the whole mess we encountered with the water material.

Shadows

We mentioned shadows in passing a while ago and now it is time to reexamine them. Our current model for determining the light level of each block is to assume a noon time sun directly overhead and compute the light levels of blocks vertically starting at the top. This makes the light model very simple and easy to perform. Now, however, that we have a nice light angle/color implementation it would be nice to see if we can incorporate that into the light model as well.

Since we're just dealing with blocks (at the moment anyways) this is easier than it sounds. Instead of always coming in from the top in the -Y direction in the light model we just have to come in on the current sunlight angle. The fact that the sun travels in the Y-X plane also makes it easier as it turns a 3D problem in essentially a 2D one. For a simple example see the following figure:

Calculating Light At Different Angles

Our original light model just used the pink line with a light angle of 90 degrees which corresponds to a delta y of 1 (in the down direction) and a delta x of 0. If we want our light model to use an arbitrary angle, such as the blue or red lines, we just have to use the light angle to compute new delta x/y values with which we'll use to traverse the block world. We'll normalize the largest of delta x/y to 1 to let us always move one block at a time (which is why the red line has delta x set to one while the blue line has delta y set to one).

Lets run through the red line in the above figure as an explicit example:

  • Start at X=6 and Y=5
  • DeltaX=-1 and DeltaY=-0.34
  • First block is (6, 5)
  • Add (-1, -0.34) to get (5, 4.66)
  • Next block (4, 4.32)
  • Next block (3, 3.98)
  • Next block (2, 3.64)
  • Next block (1, 3.30)

An important note here is that we have to use floats to keep track of the position as we move from block to block. If you look carefully you'll see our algorithm didn't check every single block that the red line touches (we missed 6,4 and 3,4). This should be fine and brings up the next step in the lighting model. In our original vertical light model (pink line) we simply started at each X/Z block in world and followed the "arrow" down. With our change to include the light angle we'll do the same thing with the addition of also starting on each face in the Y direction as well. Take our red line as an example again:

  • Start light modeling at (1,6)
  • Start at (2,6)
  • etc...
  • Start at (6,6)
  • Start at (6,1)
  • Start at (6,2)
  • etc...
  • Start at (6,6)
  • Repeat the above for all Z-coordinates

Basically we're just starting our red arrow at each block around the edge of the world. We can eliminate the faces pointing "away" from our light angle as a simple optimization. There will be some block overlapping, particularly at very sharp angles (close to 0, 90 and 180 degrees), but we can live with that for now.

Moving this algorithm into code brings us two new methods:

           //Update all block light levels in world
void TestBlockLandApplication::initWorldBlocksLightAngle()
{
 
		/* Night time light model */
	if (m_LightAngle < 0 || m_LightAngle > 180)
	{
		initWorldBlocksLight();
		return;
	}
 
	for (int z = 0; z < WORLD_SIZE; ++z)
	{
		initWorldBlocksLightAngle(z);
	}
}
 
           //Update block lights in one z-column of blocks
void TestBlockLandApplication::initWorldBlocksLightAngle(const int z)
{
                 //Compute the delta x/y values
	float DeltaX = cos(m_LightAngle * 3.1415926 / 180.0);
	float DeltaY = -sin(m_LightAngle * 3.1415926 / 180.0);
 
	if (fabs(DeltaX) >= fabs(DeltaY))
	{
		DeltaY *= fabs(1.0f/DeltaX);
		DeltaX *= fabs(1.0f/DeltaX);
	}
	else
	{
		DeltaX *= fabs(1.0f/DeltaY);
		DeltaY *= fabs(1.0f/DeltaY);
	}
 
             //Determines effect of shadows
	blocklight_t DeltaLight = 16;
	blocklight_t DeltaLight1 = 128;
 
                //Start in each block on the X side of world
	for (int y = 0; y < WORLD_SIZE; ++y)
	{
		float x1 = (DeltaX > 0) ? 0 : WORLD_SIZE - 1;
		float y1 = y;
		blocklight_t Light = 255;
 
                         //Iterate through blocks until we've reached the other side
		while (y1 >= 0 && x1 >= 0 && x1 < WORLD_SIZE && y1 < WORLD_SIZE)
		{
			GetBlockLight(x1, y1, z) = Light;
			block_t Block = GetBlock(x1, y1, z);
 
			if (Block != 0)
			{
				if (Light == 255)
					Light -= DeltaLight1;
				else if (Light > DeltaLight)
					Light -= DeltaLight;
				else
					Light = 0;
			}
 
			x1 += DeltaX;
			y1 += DeltaY;
		}
 
	}
 
                //Start on each block on the top of the world
	for (int x = 0; x < WORLD_SIZE; ++x)
	{
		float x1 = x;
		float y1 = WORLD_SIZE - 1;
		blocklight_t Light = 255;
 
		while (y1 >= 0 && x1 >= 0 && x1 < WORLD_SIZE && y1 < WORLD_SIZE)
		{
			GetBlockLight(x1, y1, z) = Light;
			block_t Block = GetBlock(x1, y1, z);
 
			if (Block != 0)
			{
				if (Light == 255)
					Light -= DeltaLight1;
				else if (Light > DeltaLight)
					Light -= DeltaLight;
				else
					Light = 0;
			}
 
			x1 += DeltaX;
			y1 += DeltaY;
		}
	}
}

We've split the light modeling into two methods because with a little foresight we should realize that we're going to have be updating the block lights in between frames and the world is too big to update all at once...the same problem we had with mesh updating. By having a method that just does one Z-column at a time it makes it easy to do this.

To update our block lights using this new method we'll edit our mesh updating method that we call each frame already:

void TestBlockLandApplication::updateChunksFrame (void)
{
        int NUM_CHUNKS = WORLD_SIZE/CHUNK_SIZE;
	if (m_UpdateChunksCount <= 0) return;
 
	for (int i = 0; i < NUM_UPDATE_CHUNKS; ++i)
	{
				/* Ignore chunks with nothing in it */
		if (m_BlockVertexCount[m_UpdateChunkX][m_UpdateChunkY][m_UpdateChunkZ] > 0) 
		{
			if (m_pBlockChunks[m_UpdateChunkX][m_UpdateChunkY][m_UpdateChunkZ] != nullptr)
			{
				Ogre::ManualObject* pMeshChunk = m_pBlockChunks[m_UpdateChunkX][m_UpdateChunkY][m_UpdateChunkZ];
				pMeshChunk->beginUpdate(0);
				createChunkCombineMat(pMeshChunk, m_UpdateChunkX*CHUNK_SIZE, m_UpdateChunkY*CHUNK_SIZE, m_UpdateChunkZ*CHUNK_SIZE);
			}
		}
 
		++m_UpdateChunkX;
 
		if (m_UpdateChunkX >= NUM_CHUNKS)
		{
			m_UpdateChunkX = 0;
			++m_UpdateChunkZ;
 
			if (m_UpdateChunkZ >= NUM_CHUNKS)
			{
				m_UpdateChunkZ = 0;
				++m_UpdateChunkY;
 
				if (m_UpdateChunkY >= NUM_CHUNKS)
				{
					m_UpdateChunkY = 0;
					--m_UpdateChunksCount;
				}
			}
 
		}
	}
 
               //Only update as many columns as needed to match mesh updating
        const int UpdateCount = NUM_UPDATE_CHUNKS / 16;
 
	for (int i = 0; i < UpdateCount; ++i)
	{
		initWorldBlocksLightAngle(m_UpdateLightZ);
 
		++m_UpdateLightZ;
		if (m_UpdateLightZ >= WORLD_SIZE) m_UpdateLightZ = 0;
	}
 
}

We've also made a small change that has a significant performance improvement. Instead of completely recreating the mesh of each chunk we'll simply update the mesh. We can use the update method because we aren't adding or deleting any of the mesh faces, only modifying the vertex colors to reflect the change in lighting. For some reason this wasn't working previously (it would crash) but appears to work fine now and increases the mesh update speed by a factor of 10 or so.

The result of all this are some of the best looking views of our world so far (also see this YouTube video):

Sunrise With Block World Shadows
Sunrise Showing Tree Shadows Against the Hills
Looking North West in the Mid Afternoon
Sunset Casting Hill Shadows Across the World

Some comments on our world shadow system so far:

  • With some simple optimization performance is very good with FPS in the 250ish range (the above screen shots were taken with an old unoptimized version). With NUM_UPDATE_CHUNKS=16 we update 16 mesh chunks each frame which takes around 0-5ms depending on the number of blocks in the chunk. We also update the lighting in one Z-column of blocks (so 256x256 = 65536 blocks) each frame which takes a consistent ~1 ms.
  • The lighting in our entire 256x256x256 block world updates around once a second which is more than fast enough and could be slowed down to increase performance. The lighting only changes noticeably during sun rise and set and world clock is still operating a relatively quick 10 minutes of real time per world day.
  • The shadows created are particularly harsh partially due to choosing the settings to make the shadows more obvious and part due to the simple model. Adjusting the light model is easy to do and there are various additional steps we can look at as well to make softer shadows (like a blur pass on the block light values).
  • There are noticeable shadow artifacts present at certain times (lines across the face of the world). This is really due to how our block light level is computed by not considering whether a block is underground or exposed to the sun. Right now we're assuming all "shadowed" blocks are underground which worked fine for our initial vertical light model but causes issues now with varying light angle.
  • Another issue with the new light model is how will light up the sides of the world when the light is on an angle (this could be seen by enabling caves). This is again due to not considering what blocks are underground or exposed to the sun.
  • Once again we haven't put any effort into the night time lighting so it is a little on the dark side at the moment.

Next Time

Next Time we'll look at more improvements to the world's light model.

Personal tools