Block Land 9

From DaveWiki

Jump to: navigation, search

Last Time we played around with improving the block world's light model. In this article we'll look at adding some simple water to the world.

Contents

Water Blocks

At first you'd think to add water it would be a simple matter to just add another block type and texture but things will turn out to be not that simple. For one, water will be our first transparent block, not counting air which is a special case. We'll begin by updating the block info structure:

struct blockinfo_t
{
	block_t ID;
	TCHAR	Name[64];
	TCHAR   Texture[64];
	bool	Transparent;
};
 
blockinfo_t g_BlockInfo[] =
{
	{ 0, "Air", "", true },
	{ 1, "Grass", "grass01.png", false },
	{ 2, "Soil", "soil01.png", false },
	{ 3, "Rock", "rock01.png", false },
	{ 4, "Lava", "lava01.png", false },
	{ 5, "Water", "water01.png", true },
	{ 255, "null", "", false }
};

Since we're not using individual texture files we'll also have to update our 1x4 texture atlas into a 1x5 with the 16x16 pixel water texture part having some alpha transparency. In order to display our texture with transparency we'll have to change its material definition:

	pass->setDepthWriteEnabled(false);
	pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
	pass->setCullingMode(Ogre::CULL_NONE);

We have to update our mesh chunking algorithm to take into account that some blocks can be seen through:

...
	//x-1
	Block1 = DefaultBlock;
	if (x > SX) Block1 = GetBlock(x-1,y,z);
 
	if (g_BlockInfo[Block1].Transparent)
	{
		//Create face for block
	}
...

Finally, we'll update our world layering to add some water to the world:

layer_t g_Layers[] = 
{
	{ 5, 0, 2, 5 },		//Water
	{ 1, 0, 2, 1 },		//Grass
	{ 2, 0, 10, 2 },	//Soil
	{ 3, 20, 200, 3 },	//Rock
	{ 4, 100, 300, 4 },     //Lava
	{ 255, 0, 0, 0 }
};

Unfortunately, the result of all this isn't exactly what we'd expect or want:

First Attempt at Water in Our Block World

Playing around with the material properties (like depth write and culling) results in different levels of failure. While I don't fully understand the nature of the issue it has to do with how the 3D engine sorts and displays triangles through transparent textures. While it may be possible to handle transparent blocks in this manner it will likely be complex and beyond the skills of this amateur Ogre programmer.

Separate Water Meshes

If we cannot easily combine the water blocks in the overall mesh chunks the next option would be to create separate mesh chunks of just the water blocks. This should get around our transparency issue and also has a benefit when we consider than water will be much more dynamic than the other types of blocks and will require more frequency updating of its mesh. By creating a separate water mesh we avoid having to completely rebuild the entire block mesh when we wish to update the water.

In our current mesh chunking method we'll change it to ignore all water blocks (like they were air):

...
	Block = GetBlock(x,y,z);
	if (Block == 0) continue;
	if (Block == 5) continue;       //Ignore water blocks
...

We'll then add a method to create the water chunks which will be essentially a direct copy of the mesh chunking method:

void TestBlockLandApplication::createChunkWater (const int StartX, const int StartY, const int StartZ)
{
	block_t LastBlock = 0;
 
	int iVertex = 0;
	block_t Block;
	block_t Block1;
 
		/* Only create visible faces of chunk */
	block_t DefaultBlock = 1;
	int SX = 0;
	int SY = 0;
	int SZ = 0;
	int MaxSize = WORLD_SIZE;
 
	float BlockLight;
	float BlockLight1;
	float BlockLight2;
 
	float V1, V2;
 
	Ogre::ManualObject* MeshChunk = new Ogre::ManualObject("MeshWaterChunk" + Ogre::StringConverter::toString(m_ChunkID));
	MeshChunk->setDynamic(true);
	MeshChunk->begin("WaterTest");
 
	for (int z = StartZ; z < CHUNK_SIZE + StartZ; ++z)
	{
		for (int y = StartY; y < CHUNK_SIZE + StartY; ++y)
		{
			for (int x = StartX; x < CHUNK_SIZE + StartX; ++x)
			{
				Block = GetBlock(x,y,z);
				if (Block != 5) continue;   //Only create water meshes
 
				BlockLight  = GetBlockLight(x, y, z) / 255.0f;
				BlockLight1 = BlockLight * 0.9f;
				BlockLight2 = BlockLight * 0.8f;
 
				V1 = 1.0f/5.0f * (float)(Block - 1);
				V2 = V1 + 1.0f/5.0f;
 
					//x-1
				Block1 = DefaultBlock;
				if (x > SX) Block1 = GetBlock(x-1,y,z);
 
				if (Block1 != 5)
				{
                                        //create face of block
                                }
	...

For the water blocks we'll create a new material (blue with 50% transparency):

void TestBlockLandApplication::createWaterTexture (const TCHAR* pName)
{
	Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create(pName, "General", true );
	Ogre::Technique* tech = mat->getTechnique(0);
	Ogre::Pass* pass = tech->getPass(0);
	Ogre::TextureUnitState* tex = pass->createTextureUnitState();
 
	tech->setLightingEnabled(false);
 
	pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
	pass->setDepthWriteEnabled(false);
 
	tex->setColourOperationEx(Ogre::LBX_SOURCE1, Ogre::LBS_MANUAL, Ogre::LBS_CURRENT, Ogre::ColourValue(0, 0, 1));
	tex->setAlphaOperation(Ogre::LBX_SOURCE1, Ogre::LBS_MANUAL, Ogre::LBS_CURRENT, 0.5);
}

The world chunking method has to be updated to include the creation of water meshes:

void TestBlockLandApplication::createWorldChunks (void)
{
	createTexture("BoxColor", "Grass01.png");
	createWaterTexture("WaterTest");
 
	for (int z = 0; z < WORLD_SIZE; z += CHUNK_SIZE)
	{
		for (int y = 0; y < WORLD_SIZE; y += CHUNK_SIZE)
		{
			for (int x = 0; x < WORLD_SIZE; x += CHUNK_SIZE)
			{
				createChunkCombineMat(x,y,z);
				createChunkWater(x,y,z);
			}
		}
	}
 
}

Disabling caves for now to make the landscape generation faster now results in:

Separate Water Mesh Chunks

Fortunately, our previous issues of transparency have disappeared and performance is still reasonable.

Water Modeling

One thing we would like to do with water is some very simple flow modeling like is done in MineCraft or Dwarf Fortress. To start with we'll add a water depth value to each block and other members we'll need:

class TestBlockLandApplication : public BaseApplication 
{
...
	int ModelWaterX;
	int ModelWaterY;
	int ModelWaterZ;
 
	Ogre::ManualObject* m_pWaterChunks[WORLD_SIZE/CHUNK_SIZE][WORLD_SIZE/CHUNK_SIZE][WORLD_SIZE/CHUNK_SIZE];
 
	waterdepth_t* m_WaterDepth;
 
	waterdepth_t& GetWaterDepth (const int x, const int y, const int z)
	{
		return m_WaterDepth[x + y * WORLD_SIZE + z * WORLD_SIZE2];
	}
};
 
TestBlockLandApplication::TestBlockLandApplication(void)
{
...
	ModelWaterX = 0;
	ModelWaterY = 0;
	ModelWaterZ = 0;
 
	m_WaterDepth = new waterdepth_t[WORLD_SIZE3 + 16000];
	memset(m_WaterDepth, 0, sizeof(waterdepth_t) * WORLD_SIZE3);
}

Like our lighting model we'll just use a byte value for each block although eventually we may reduce the water depth bit size to 4 or even 3 bits to save space. Our water depth values will range from 0 (no water) to 10 (full water). The pWaterChunks array member is to save the water mesh object for each chunk. Since we'll have to update the water mesh we need to be able to remove and delete the old water mesh from the scene. The following method is used to update a specific water mesh chunk:

void TestBlockLandApplication::updateChunkWater (const int StartX, const int StartY, const int StartZ)
{
	Ogre::ManualObject* pMeshChunk = m_pWaterChunks[StartX/CHUNK_SIZE][StartY/CHUNK_SIZE][StartZ/CHUNK_SIZE];
 
	if (pMeshChunk)
	{
		pMeshChunk->detachFromParent();
		delete pMeshChunk;
		m_pWaterChunks[StartX/CHUNK_SIZE][StartY/CHUNK_SIZE][StartZ/CHUNK_SIZE] = NULL;
	}
 
	createChunkWater(StartX, StartY, StartZ);
}

A number of changes also have to be made to the water mesh chunking method to get to display the water depth properly and save the mesh chunk object to the array:

void TestBlockLandApplication::createChunkWater (const int StartX, const int StartY, const int StartZ)
{
	block_t LastBlock = 0;
	int iVertex = 0;
	block_t Block;
	block_t Block1;
 
 
		/* Only create visible faces of chunk */
	block_t DefaultBlock = 1;
	int SX = 0;
	int SY = 0;
	int SZ = 0;
	int MaxSize = WORLD_SIZE;
 
	float BlockLight;
	float BlockLight1;
	float BlockLight2;
 
	float V1, V2;
	waterdepth_t WaterDepth;
	waterdepth_t WaterDepth1;
 
	Ogre::ManualObject* MeshChunk = new Ogre::ManualObject("MeshWaterChunk" + Ogre::StringConverter::toString(m_ChunkID));
 
	m_pWaterChunks[StartX/CHUNK_SIZE][StartY/CHUNK_SIZE][StartZ/CHUNK_SIZE] = MeshChunk;
 
	m_pMeshChunk = MeshChunk;
	MeshChunk->setDynamic(true);
	MeshChunk->begin("WaterTest");
 
	for (int z = StartZ; z < CHUNK_SIZE + StartZ; ++z)
	{
		for (int y = StartY; y < CHUNK_SIZE + StartY; ++y)
		{
			for (int x = StartX; x < CHUNK_SIZE + StartX; ++x)
			{
				Block = GetBlock(x,y,z);
				if (Block != 5) continue;
 
				WaterDepth = GetWaterDepth(x,y,z);
				if (WaterDepth <= 0) continue;
 
				float yDepth = WaterDepth / 10.0f;
 
				BlockLight  = GetBlockLight(x, y, z) / 255.0f;
				BlockLight1 = BlockLight * 0.9f;
				BlockLight2 = BlockLight * 0.8f;
 
				V1 = 1.0f/5.0f * (float)(Block - 1);
				V2 = V1 + 1.0f/5.0f;
 
					//x-1
				Block1 = DefaultBlock;
				WaterDepth1 = 0;
 
				if (x > SX) 
				{
					Block1 = GetBlock(x-1,y,z);
					WaterDepth1 = GetWaterDepth(x-1,y,z);
				}
 
				if (Block1 != 5)
				{
					MeshChunk->position(x, y,   z+1);	...
					MeshChunk->position(x, y+yDepth, z+1);  ...
					MeshChunk->position(x, y+yDepth, z);	...
					MeshChunk->position(x, y,   z);		...
 
					MeshChunk->triangle(iVertex, iVertex+1, iVertex+2);
					MeshChunk->triangle(iVertex+2, iVertex+3, iVertex);
 
					iVertex += 4;
				}
        ...
}

So far the changes have been just simple things to get the a block of water to have and be displayed with a depth. The more interesting part is how to model the water flow. We'll start with these very basic rules:

  • If a block has water continue.
  • Water can move into a block that is air or is not completely filled with water (depth of 9 or less).
  • If there is space underneath a water block as much water as possible will fall into that block.
  • If there is space in an adjacent block with less water one unit of water will be transferred.

Our modeling will occur on a chunk of blocks at a time:

void TestBlockLandApplication::modelChunkWater (const int StartX, const int StartY, const int StartZ)
{
 
	for (int y = StartY; y < CHUNK_SIZE + StartY; ++y)
	{
		for (int z = StartZ; z < CHUNK_SIZE + StartZ; ++z)
		{
			for (int x = StartX; x < CHUNK_SIZE + StartX; ++x)
			{
				block_t& Block = GetBlock(x, y, z);
				if (Block != 5) continue;
 
				waterdepth_t& WaterDepth = GetWaterDepth(x, y, z);
				if (WaterDepth <= 0) continue;
 
				if (y > 0)
				{
					waterdepth_t& WaterDepth1 = GetWaterDepth(x, y-1, z);
					block_t& Block1 = GetBlock(x, y-1, z);
 
					if (Block1 == 0)
					{
						WaterDepth1 = WaterDepth;
						Block1 = 5;
						WaterDepth = 0;
						if (WaterDepth == 0) Block = 0;
					}
					else if (Block1 == 5 && WaterDepth1 < 10)
					{
						WaterDepth1 += WaterDepth;
						WaterDepth = 0;
 
						if (WaterDepth1 > 10) 
						{
							WaterDepth = WaterDepth1 - 10;
							WaterDepth1 = 10;
						}
 
						if (WaterDepth == 0) Block = 0;
					}
 
				}
 
				if (WaterDepth == 0) continue;
 
				if (x > 0)
				{
					waterdepth_t& WaterDepth1 = GetWaterDepth(x-1, y, z);
					block_t& Block1 = GetBlock(x-1, y, z);
 
					if (Block1 == 0)
					{
						--WaterDepth;
						if (WaterDepth == 0) Block = 0;
						WaterDepth1 = 1;
						Block1 = 5;
					}
					else if (Block1 == 5 && WaterDepth1 < WaterDepth)
					{
						--WaterDepth;
						++WaterDepth1;
						if (WaterDepth == 0) Block = 0;
					}
				}
 
				if (WaterDepth == 0) continue;
 
				if (x < WORLD_SIZE - 1)
				{
					waterdepth_t& WaterDepth1 = GetWaterDepth(x+1, y, z);
					block_t& Block1 = GetBlock(x+1, y, z);
 
					if (Block1 == 0)
					{
						--WaterDepth;
						if (WaterDepth == 0) Block = 0;
						WaterDepth1 = 1;
						Block1 = 5;
					}
					else if (Block1 == 5 && WaterDepth1 < WaterDepth)
					{
						--WaterDepth;
						++WaterDepth1;
						if (WaterDepth == 0) Block = 0;
					}
				}
 
				if (WaterDepth == 0) continue;
 
				if (z > 0)
				{
					waterdepth_t& WaterDepth1 = GetWaterDepth(x, y, z-1);
					block_t& Block1 = GetBlock(x, y, z-1);
 
					if (Block1 == 0)
					{
						--WaterDepth;
						if (WaterDepth == 0) Block = 0;
						WaterDepth1 = 1;
						Block1 = 5;
					}
					else if (Block1 == 5 && WaterDepth1 < WaterDepth)
					{
						--WaterDepth;
						++WaterDepth1;
						if (WaterDepth == 0) Block = 0;
					}
				}
 
				if (WaterDepth == 0) continue;
 
				if (z < WORLD_SIZE - 1)
				{
					waterdepth_t& WaterDepth1 = GetWaterDepth(x, y, z+1);
					block_t& Block1 = GetBlock(x, y, z+1);
 
					if (Block1 == 0)
					{
						--WaterDepth;
						if (WaterDepth == 0) Block = 0;
						WaterDepth1 = 1;
						Block1 = 5;
					}
					else if (Block1 == 5 && WaterDepth1 < WaterDepth)
					{
						--WaterDepth;
						++WaterDepth1;
						if (WaterDepth == 0) Block = 0;
					}
				}
			}
		}
	}
 
}

To use this water modeling code we will use Ogre's frameEnded method and model/update one chunk of water at a time. Depending on the performance we can update more than one chunk at a time. We'll also add an infinite water source at the top of the world for testing purposes:

bool TestBlockLandApplication::frameEnded (const Ogre::FrameEvent &evt)
{
 
                //Form a source of water in the top of the world for testing 
	GetWaterDepth(WORLD_SIZE/2 + 8, WORLD_SIZE - 2, WORLD_SIZE/2 + 8) = 10;
	GetBlock(WORLD_SIZE/2 + 8, WORLD_SIZE - 2, WORLD_SIZE/2 + 8) = 5;
 
                //Model and update one or more chunks of water
	for (int i = 0; i < 1; ++i) 
	{
		modelChunkWater(ModelWaterX, ModelWaterY, ModelWaterZ);
		updateChunkWater(ModelWaterX, ModelWaterY, ModelWaterZ);
 
		ModelWaterX += CHUNK_SIZE;
 
		if (ModelWaterX >= WORLD_SIZE)
		{
			ModelWaterX = 0;
			ModelWaterZ += CHUNK_SIZE;
 
			if (ModelWaterZ >= WORLD_SIZE)
			{
				ModelWaterZ = 0;
				ModelWaterY += CHUNK_SIZE;
 
				if (ModelWaterY >= WORLD_SIZE) ModelWaterY = 0;
			}
		}
	}
 
	return BaseApplication::frameEnded(evt);
}

For performance reasons we'll reduce our world size to 64x64x64 and model all 64 chunks each frame and see what we get (click image to see a short YouTube video):

Basic Water Modeling in a 64x64x64 Block World

A few points regarding the water modeling so far:

  • To completely model our 64x64x64 block world takes an average of 2ms with an additional 20ms to update the water meshes. This varies depending on the amount of water in each chunk. This is decent performance but it suggests that since mesh updating is much more expensive we may need a more complex model/update loop (for example, update mesh chunks closer to the player more frequently and chunks farther away less frequently).
  • If we wished to model and update a larger 256x256x256 block world would require 130ms to model and 1300ms to update. This is obviously far too much indicating that we'll have to model/update on a chunk by chunk basis. Note that on my mid-end desktop system these times are reduced to 40ms to model and 300ms to update which are better but still too high to simply model/update everything each frame.
  • We haven't added any real-time checks into the modeling which means the water will flow faster on higher end systems. There is also no viscosity modeling of the fluid which will come into effect when/if we consider lava (or other liquids).
  • The current water display method is simple and not very good looking. There are large gaps in the water between blocks and water is left "hanging" in places. Part of this can be improved by the water mesh creation and part from the water model itself.

Next Time

Next Time we'll go back and look at lighting again to add some more effects.

Personal tools