Block Land 10

From DaveWiki

Jump to: navigation, search

Last Time we played around with adding a water block and basic water modeling to the world. This time we'll go back to world lighting to add some special effects.

Light Color

Our block lighting model we've used has been simple: use the vertex color of our blocks as the amount of light that block receives. This is split up into two parts: the light modeling which computes how much light each block receives and the mesh chunking which assigns the block light level to the mesh's vertex. We subtly modify the light level on the blocks X/Y/Z faces to give each face a slightly varying light level which makes the scene less plain.

The result of this model, whether we realized it or not initially, gives us a number of powerful methods to improve the lighting of our world without too much additional effort. Firstly, and easiest, is to give our sunlight some color. Currently we're assuming a perfect white light source which works fine for regular day light but at sunset and sunrise the sun's color is much redder. Secondly, and more importantly, we can look at varying the angle of the sunlight and use that to vary the amount of light that falls on each face of the block.

We'll start by adding a few members to the world class which should be self descriptive:

class TestBlockLandApplication : public BaseApplication
{
...
	Ogre::ColourValue       m_LightColor;
        Ogre::ColourValue	m_AmbientColor;
	Ogre::ColourValue	m_FogColor;
	float			m_LightAngle;
};

The light angle will be in degrees with 0 being sunrise, 90 being noon, and 180 being sunset. The exact angle values and orientation are arbitrary but it affects how we calculate the face light levels later on. We'll initialize these values next:

TestBlockLandApplication::TestBlockLandApplication(void)
{
...
              //Noon light levels to start with
	m_LightAngle = 90;
	m_LightColor = Ogre::ColourValue(1, 1, 1);
        m_FogColor = m_LightColor * 0.8f;
	m_AmbientColor = m_LightColor / 3.0f;
}

To use these values we have to update our mesh chunking method (we'll also update the separate water mesh chunking method which is not shown here):

void TestBlockLandApplication::createChunkCombineMat (Ogre::ManualObject* MeshChunk, const int StartX, const int StartY, const int StartZ)
{
...
	Ogre::ColourValue BlockColory1;    //Top face
	Ogre::ColourValue BlockColory2;    //Bottom face
	Ogre::ColourValue BlockColorx1;    //Sunset face
	Ogre::ColourValue BlockColorx2;    //Sunrise face
	Ogre::ColourValue BlockColorz;     //Front/back faces
 
        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)
			{
...
                                float BaseLight = GetBlockLight(x, y, z) / 255.0f;
                                float Factor;
 
                                if (m_LightAngle >= 0 && m_LightAngle <= 180)
                                {
                                    Factor = sin(m_LightAngle * 3.1415926f / 180.0f);
                                }
                                else
                                {
                                    Factor = 0;
                                }
 
                                if (Factor < 0.1f) Factor = 0.1f;
                                BlockColory1 = m_LightColor * (Factor * BaseLight) + m_AmbientColor;
                                BlockColory1.saturate();
                                BlockColory2 = m_LightColor * (Factor / 2.0f * BaseLight) + m_AmbientColor;
                                BlockColory2.saturate();
                                BlockColorz  = m_LightColor * (Factor * 0.70f * BaseLight) + m_AmbientColor;
                                BlockColorz.saturate();
                                BlockColorz *= 0.80f;
 
                                if (m_LightAngle >= 315 || m_LightAngle <= 45)
                                {
                                    Factor = fabs(cos(m_LightAngle * 3.1415926f / 180.0f));
                                }
                                else
                                {
                                    Factor = fabs(sin(m_LightAngle * 3.1415926f / 180.0f));
                                }
 
                                if (Factor < 0.1f) Factor = 0.1f;
                                BlockColorx1 = m_LightColor * (Factor * 0.80f * BaseLight) + m_AmbientColor;
                                BlockColorx1.saturate();
                                BlockColorx1 *= 0.95f;
 
                                if (m_LightAngle >= 135 && m_LightAngle <= 225)
                                {
                                    Factor = fabs(cos(m_LightAngle * 3.1415926f / 180.0f));
                                }
                                else
                                {
                                    Factor = fabs(sin(m_LightAngle * 3.1415926f / 180.0f));
                                }
 
                                if (Factor < 0.1f) Factor = 0.1f;
                                BlockColorx2 = m_LightColor * (Factor * 0.80f * BaseLight) + m_AmbientColor;
                                BlockColorx2.saturate();
                                BlockColorx2 *= 0.95f;
 
                                	//x-1
				Block1 = DefaultBlock;
				if (x > SX) Block1 = GetBlock(x-1,y,z);
 
				if (g_BlockInfo[Block1].Transparent)
				{
					MeshChunk->position(x, y,   z+1);	MeshChunk->normal(-1,0,0);	MeshChunk->textureCoord(0, V2); MeshChunk->colour(BlockColorx1);
...
//etc... for each of the faces using the appropriate BlockColor### value
...

The light calculation for each face based on the light angle is pretty simple but is explained below in more detail:

  • Only sun light is currently modeled. At night everything gets a minimum, directionless light value.
  • The light of the top face is just the sin(LightAngle) from sun up to sun down.
  • The light of the bottom face is set to be half that of the top face.
  • The light of the sun rise face is set to be cos(LightAngle) from about 3am to 9am (assuming a 6am sun rise). After that the light is set to be 90% of the top face's light level.
  • The light of the sun set face is the same as the sun rise face except at the opposite times (afternoon instead of morning).
  • The light of the two Z faces is set to be a percentage of the top face.

The exact parameter values were just a result of a lot of tests and tweaks to get it looking decent. Testing this with a noon light yields:

New Light Model With a Noon Light

The fact that the world looks just about the same as it did before is a good sign meaning we haven't broken anything too bad. With a sunrise light level (angle of 10 and a color of [1,0.6,0.04]) yields:

New Light Model With a Sun Rise Light

You may be wondering how I got the skydome texture to be modified based on the light level. To do this I created two methods to manually create the sky dome texture material (just a copy of what was used from the Examples.material file) and modify the texture's light level based on the world's light level:

void TestBlockLandApplication::createSkyTexture (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();
 
	pass->setLightingEnabled(false);
	pass->setDepthCheckEnabled(false);
	pass->setDepthWriteEnabled(false);
	pass->setFog(true);
 
	tex->setTextureName("clouds.jpg");
	tex->setScrollAnimation(0.05, 0);
 
                //This is a new texture state to simulate lighting
	tex = pass->createTextureUnitState();
	tex->setColourOperationEx(Ogre::LBX_MODULATE, Ogre::LBS_MANUAL, Ogre::LBS_CURRENT, Ogre::ColourValue(1, 1, 1));
 
	m_SkyMaterial = mat;
	updateSkyTextureLight();
}
 
 
void TestBlockLandApplication::updateSkyTextureLight (void)
{
	if (m_SkyMaterial.isNull()) return;
 
	Ogre::Technique* tech = m_SkyMaterial->getTechnique(0);
	if (tech == nullptr) return;
	Ogre::Pass* pass = tech->getPass(0);
	if (pass == nullptr) return;
 
	Ogre::TextureUnitState* tex = pass->getTextureUnitState(1);
	if (tex == nullptr) return;
 
                //Update the texture unit's color operation with the world light level
	tex->setColourOperationEx(Ogre::LBX_MODULATE, Ogre::LBS_MANUAL, Ogre::LBS_CURRENT, m_LightColor);
}

Using this in the scene creation is as simple as:

        createSkyTexture("SkyDome1");
	mSceneMgr->setSkyDome(true, "SkyDome1", 2, 8, 100);

Day/Night Cycle

Many games, including MineCraft, have a day/night cycle which would be nice to try in our block world. The basis of the cycle will be the world time:

        float	m_WorldTime;	//24 hour world time

This will just be a value from 0-24 representing the world time in hours. Next we have to create a method that will compute the light angle and colors based on the current world time:

void TestBlockLandApplication::ComputeWorldLightValues (const float WorldTime)
{
	//6am = SunRise
	//Light is symmetric about noon
	//4am-8am = dawn
	//4am color = (0.1, 0.1, 0.1)
	//6am color = (1, 0.6, 0.04)
	//8am color = (1, 1, 1)
 
	float BaseWorldTime = std::fmod(WorldTime, 24);
 
	m_LightAngle = BaseWorldTime / 24.0f * 360.0f - 90.0f;
	if (m_LightAngle < 0) m_LightAngle += 360.0f;
 
	if (BaseWorldTime <= 4 || BaseWorldTime >= 20)
	{
		m_LightColor = Ogre::ColourValue(0.1f, 0.1f, 0.1f);
	}
	else if (BaseWorldTime >= 8 && BaseWorldTime <= 16)
	{
		m_LightColor = Ogre::ColourValue(1, 1, 1);
	}
	else if (BaseWorldTime >= 4 && BaseWorldTime <= 6)
	{
		m_LightColor.r = (BaseWorldTime - 4.0f)/2.0f * 0.9f + 0.1f;
		m_LightColor.g = (BaseWorldTime - 4.0f)/2.0f * 0.5f + 0.1f;
		m_LightColor.b = (BaseWorldTime - 4.0f)/2.0f * -0.06f + 0.1f;
	}
	else if (BaseWorldTime >= 6 && BaseWorldTime <= 8)
	{
		m_LightColor.r = 1.0f;
		m_LightColor.g = (BaseWorldTime - 6.0f)/2.0f * 0.4f + 0.6f;
		m_LightColor.b = (BaseWorldTime - 6.0f)/2.0f * 0.96f + 0.04f;
	}
	else if (BaseWorldTime >= 16 && BaseWorldTime <= 18)
	{
		m_LightColor.r = 1.0f;
		m_LightColor.g = (18.0f - BaseWorldTime)/2.0f * 0.4f + 0.6f;
		m_LightColor.b = (18.0f - BaseWorldTime)/2.0f * 0.96f + 0.04f;
	}
	else if (BaseWorldTime >= 18 && BaseWorldTime <= 20)
	{
		m_LightColor.r = (20.0f - BaseWorldTime)/2.0f * 0.9f + 0.1f;
		m_LightColor.g = (20.0f - BaseWorldTime)/2.0f * 0.5f + 0.1f;
		m_LightColor.b = (20.0f - BaseWorldTime)/2.0f * -0.06f + 0.1f;
	}
	else	//Shouldn't get here
	{	
		m_LightColor = Ogre::ColourValue(1, 1, 1);
	}
 
	m_AmbientColor = m_LightColor / 3.0f;
	m_FogColor = m_LightColor * 0.80f;
}

For most of the day the light color will be constant and just the light angle will be changed. During the hours of sunrise and sunset we just do a simple linear interpolation between the three set colors (night, sun rise/set, day). The exact values come from a little experimentation and are far from perfect but are more than good enough for now.

Unfortunately, this is the easy part. Now that we can compute the sunlight angle and color at any time of day we have to actually update the world to display the changes. To do this we have to rebuild all our mesh chunks since the lighting is built into their vertex colors. We can't simply update the entire world at once since it takes over 3 secs to do so after the recent changes in the lighting model. Instead, we'll do like MineCraft does and just update a few chunks at a time every frame (you can see this effect in MineCraft during sun rise and set).

We'll add a few class members:

        int		 m_UpdateChunkX;              //The current chunk index we're updating
	int		 m_UpdateChunkY;
	int		 m_UpdateChunkZ;
	int		 m_UpdateChunksCount;        //Keeps track of whether to update chunks or not
	static const int NUM_UPDATE_CHUNKS = 16;     //Number of chunks to update each frame
 
               //Stores the mesh object for each chunk
        Ogre::ManualObject* m_pBlockChunks[WORLD_SIZE/CHUNK_SIZE][WORLD_SIZE/CHUNK_SIZE][WORLD_SIZE/CHUNK_SIZE];
 
               //The number of vertices in each mesh chunk
        int m_BlockVertexCount[WORLD_SIZE/CHUNK_SIZE][WORLD_SIZE/CHUNK_SIZE][WORLD_SIZE/CHUNK_SIZE];

The last line is an optimization to only update chunks that have an non-empty mesh. Currently with a 16 block chunk many of our chunks are completely empty (especially with no caves) so there is no point in updating these. To use it we just have to add:

         m_pBlockChunks[StartX/CHUNK_SIZE][StartY/CHUNK_SIZE][StartZ/CHUNK_SIZE] = MeshChunk;
         m_BlockVertexCount[StartX/CHUNK_SIZE][StartY/CHUNK_SIZE][StartZ/CHUNK_SIZE] = iVertex;

at the end of our mesh chunking method. Our chunk updating method looks like:

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) 
		{
                                // Remove and delete the existing mesh from the scene
			if (m_pBlockChunks[m_UpdateChunkX][m_UpdateChunkY][m_UpdateChunkZ] != nullptr)
			{
				Ogre::ManualObject* pMeshChunk = m_pBlockChunks[m_UpdateChunkX][m_UpdateChunkY][m_UpdateChunkZ];
				pMeshChunk->detachFromParent();
				delete pMeshChunk;
				m_pBlockChunks[m_UpdateChunkX][m_UpdateChunkY][m_UpdateChunkZ] = nullptr;
			}
 
                                 // Create a new mesh for the chunk
			Ogre::ManualObject* MeshChunk = new Ogre::ManualObject("MeshMatChunk" + Ogre::StringConverter::toString(m_ChunkID));
			MeshChunk->setDynamic(true);
			MeshChunk->begin("Combine4");
			createChunkCombineMat(MeshChunk, m_UpdateChunkX*CHUNK_SIZE, m_UpdateChunkY*CHUNK_SIZE, m_UpdateChunkZ*CHUNK_SIZE);
			m_pBlockChunks[m_UpdateChunkX][m_UpdateChunkY][m_UpdateChunkZ] = MeshChunk;
			mSceneMgr->getRootSceneNode()->createChildSceneNode()->attachObject(MeshChunk);
			++m_ChunkID;
		}
 
                         // Find the next chunk to update
		++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;
				}
			}
		}
	}
}

We'll simply recreate a new mesh for each update but we could also just update the existing mesh object (that is, if it wouldn't crash on me). There are a variety of ways to make this chunk updating better but for a first attempt this will do fine.

There are a few ways we can actually add the day/night cycle to the scene. The first, and simplest, is to add control to manually increase/decrease the world time which is great for testing our cycle:

bool TestBlockLandApplication::keyPressed( const OIS::KeyEvent &arg )
{
 
    if (arg.key == OIS::KC_K)
    {
        m_WorldTime += 0.1f;   //Increase world time a bit
	if (m_WorldTime > 24.0f) m_WorldTime -= 24.0f;
 
	ComputeWorldLightValues(m_WorldTime);
	UpdateSceneLighting();
 
	++m_UpdateChunksCount;
    }
    else if (arg.key == OIS::KC_J)
    {
        m_WorldTime -= 0.1f; //Decrease world time a bit
	if (m_WorldTime < 0) m_WorldTime += 24.0f;
 
	ComputeWorldLightValues(m_WorldTime);
	UpdateSceneLighting();
 
	++m_UpdateChunksCount;
    }
 
  ...
}

The next method is to add an automatic cycle to one of the frame events. We also have to make sure to call our chunk update method on each frame:

bool TestBlockLandApplication::frameEnded (const Ogre::FrameEvent &evt)
{
 
               //This makes the world day last about 5 minutes of real time
	m_WorldTime += evt.timeSinceLastFrame * 0.04;  
        m_WorldTime = fmod(m_WorldTime, 24.0f);
 
	ComputeWorldLightValues(m_WorldTime);
	UpdateSceneLighting();
 
	if (m_UpdateChunksCount == 0) ++m_UpdateChunksCount;
 
               //Update a few chunks of the world each frame
	updateChunksFrame();
...
}

We've adjusted the automatic cycle to take roughly 10 minutes of real time for each block world day. This is faster than MineCraft which has a 20 minute real time day but makes for a slightly less boring video:

Sample of Basic Day Night Cycle (click to see YouTube video)

For relatively little effort we have a complete and not half bad looking day/night cycle (minus the terrible video taking...FRAPs doesn't work well with our test application).

  • During the day the lighting is very even as it should be with little noticeable variation.
  • During sun rise and set there the lighting changes fast enough to be very noticeable.
  • The real time length of the day in our world was set to about 10 minutes which is on the fast side. This would slow down the sun rise/set lighting changes some.
  • Currently we're basically just updating the chunks as fast as we can (16 chunks each frame) dropping our FPS significantly. This would seem to indicate that we'll have to be careful with performance in order to have a decently short time between chunk updates. There are multiple optimizations we can, and probably will have to, look at eventually.
  • No effort has been made yet for the night time lighting or to add the effect of moon light but its effect will be very similar to what we've done with sunlight (just a different period and light colors).

Next Time

Next Time we'll look at some more lighting effects.

Personal tools