Rendering Terrain Part 12 – Lighting, Lights and a Simple Day/Night Cycle

I had intended to talk about Shadow Mapping today, but I wound up having a bunch of trouble getting it working and I haven’t figured out what’s wrong yet. Instead, I’m going to talk a little bit about how I’m doing a day/night cycle. It’s still a pretty basic system, but I think I’ll be able to extend it fairly easily to something a little more powerful down the road. Before we talk about that, however, we should talk about lighting and lights.

Lighting

In Part 5, I very briefly described the lighting we’ve been using to date. It was pretty basic. I just defined a colour and the direction the light was going in. I multiplied the color by an ambient term to estimate the amount of light hitting surfaces not facing the light source. Then I added to that a diffuse term that was defined as the colour times the cosine of the angle between the light vector and the normal at the point.
That was ok and made for some decent static pictures, but it was pretty basic and was completely hard-coded. Now that we’re beginning to move away from hard-coded values, we should also move towards a better lighting model. To that end, I’ve begun to implement the Phong Reflection Model for specular reflections. There’s a good explanation here.
So we’re going to modify our lighting model a bit. For each of the ambient, diffuse, and specular terms, we’re going to define a colour for the light, and for the object. To get the final colour, we’ll need to multiply the two colours together. For diffuse and specular, we’ll then need to do some extra calculations. For ambient, we’ll assume that the final intensity of the ambient term will be included in how we set the ambient colours.

float4 color = float4(0.22f, 0.72f, 0.31f, 1.0f);
float4 ambient = color * light.amb;
float4 diffuse = color * light.dif * dot(-light.dir, norm);
float3 V = reflect(light.dir, norm);
float3 toEye = normalize(eye.xyz - input.worldpos);
float4 specular = color * 0.1f * light.spec * pow(max(dot(V, toEye), 0.0f), 2.0f);
return ambient + diffuse + specular;

There’s some hard-coded values in there that we’ll need to fix when we actually define a material for the terrain. I’m not planning to do this until after I get shadows working, so we’ll just leave this for now. We’re currently using the same colour for the terrain for all three terms, plus we’ve set our specular exponent to 2.0 and our specular strength to 0.1, resulting in a very weak and broad dim specular highlight. I figure this is reasonable for grass.

I turned off the ambient and diffuse terms for this image and used a specular strength of 0.5 and an exponent of 32. It kind of looks like plastic as the light moves over it.

I turned off the ambient and diffuse terms for this image and used a specular strength of 0.5 and an exponent of 32. It kind of looks like plastic as the light moves over it.

Lights

So now that we’ve got a new lighting model and we know what data we need for our lights, we can define those lights in our application.
While I currently only have a single directional light, I wanted to make a Light class that would encompass point and spot lights as well. The intention being that I could potentially add them down the road or use the class in a different project.
For directional lights, we really only need a direction and the three colour terms for the light. You could get away with just one colour, but we get some flexibility with three separate colours.
Point lights add a position in space for the light, as well as an attenuation factor and a range that describe how the light falls off as you get further from it.
A spot light adds another term, a spotlight exponent value that is used in calculating the cone of the spotlight. We won’t worry about the calculation as we’re not currently using it, but we’ll add the term to our light object so it’s there when we need it.

struct LightSource {
	LightSource() { ZeroMemory(this, sizeof(this)); }

	XMFLOAT4	pos;
	XMFLOAT4	intensityAmbient;
	XMFLOAT4	intensityDiffuse;
	XMFLOAT4	intensitySpecular;
	XMFLOAT3	attenuation;
	float		range;
	XMFLOAT3	direction;
	float		spotlightConeExponent;
};

We’ll make an instance of this structure a member of our light class. Then we can easily pass it to our shader in the constant buffer. So we currently have a pretty simple base class.

class Light {
public:
	Light() {}
	Light(XMFLOAT4 p, XMFLOAT4 amb, XMFLOAT4 dif, XMFLOAT4 spec, XMFLOAT3 att, float r, XMFLOAT3 dir, float sexp);
	~Light() {}

	LightSource GetLight() { return mlsLightData; }
	void SetLightDirection(XMFLOAT3 dir) { mlsLightData.direction = dir; }

protected:
	LightSource mlsLightData;
};

We can then create child classes for each of our light types to simplify creating and managing them. We’ve just made a directional light so far.

class DirectionalLight : public Light {
public:
	DirectionalLight() : Light() {}
	DirectionalLight(XMFLOAT4 amb, XMFLOAT4 dif, XMFLOAT4 spec, XMFLOAT3 dir) : Light(XMFLOAT4(0.0f, 0.0f, 0.0f, 0.0f), amb, dif, spec, XMFLOAT3(0.0f, 0.0f, 0.0f), 0.0f, dir, 0.0f) {}
	~DirectionalLight() {}
};

As you can see, the directional light’s constructor only sets the terms it cares about and zeros out the rest.
That’s it for now. Eventually, I’ll add the code for calculating the light’s view projection matrix as well, but I have not yet added that as I’m not sure if it is working. Right now that code still lives in the DayNightCycle class. Once I’ve got shadows figured out, I’ll move it to where it should be.

The Day/Night Cycle

My ultimate goal for the DayNightCycle class is to encapsulate both the Sun and the Moon. Perhaps I could even make it more flexible by allowing the user to define how many Suns and Moons to use. Probably not right away, though. For now, I’m only taking the Sun into account. We define the Sun as a directional light and give it a starting direction and define the colour terms. Currently the colour is set once and doesn’t change, but I’d like to make it eventually change based on the direction the light is coming from, so we can get those crazy dawn and dusk colours.

static const double DEG_PER_MILLI = 360.0 / 86400000.0; // the number of degrees per millisecond, assuming you rotate 360 degrees in 24 hours.

class DayNightCycle {
public:
	DayNightCycle(UINT period);
	~DayNightCycle();

	void Update();

	LightSource GetLight() { return mdlSun.GetLight(); }

private:
	UINT						mPeriod;	// the number of game milliseconds that each real time millisecond should count as.
	DirectionalLight			mdlSun;		// light source representing the sun. 
	time_point<system_clock>	mtLast;		// the last point in time that we updated our cycle.
};

DayNightCycle::DayNightCycle(UINT period) : mdlSun(XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f), XMFLOAT4(0.91f, 0.74f, 0.08f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f), XMFLOAT3(-1.0f, 0.0f, 0.0f)),
								mdlMoon(XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f), XMFLOAT4(0.4f, 0.4f, 0.4f, 1.0f), XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f), XMFLOAT3(1.0f, 0.0f, 0.0f)),
								mPeriod(period) {
	mtLast = system_clock::now();
}

For the moment, I’ve defined the Sun’s light as shining down the x-axis (-1.0, 0.0, 0.0), from positive to negative. Effectively, I’ve made the positive x-axis East and the negative x-axis West. I chose this for now because it makes revolving the Sun easy. We just have to rotate the direction vector about (0,0,0) and it will spin just like if the light were revolving about the terrain.

void DayNightCycle::Update() {
	time_point<system_clock> now = system_clock::now();

	// get the amount of time in ms since the last time we updated.
	milliseconds elapsed = duration_cast<milliseconds>(now - mtLast);

	// calculate how far to rotate.
	double angletorotate = elapsed.count() * mPeriod * XMConvertToRadians(DEG_PER_MILLI);

	// rotate the sun's direction vector.
	XMFLOAT3 tmp = mdlSun.GetLight().direction;
	XMVECTOR dir = XMLoadFloat3(&tmp);
	XMVECTOR rot = XMQuaternionRotationRollPitchYaw(0.0f, -(float)angletorotate, 0.0f);
	dir = XMVector3Normalize(XMVector3Rotate(dir, rot));
	XMStoreFloat3(&tmp, dir);
	mdlSun.SetLightDirection(tmp);

	// update the time for the next pass.
	mtLast = now;
}

We use the std::chrono::system_clock library to keep time and calculate how far to rotate the Sun’s direction each frame.
I haven’t thought about how to move the Sun off the direct East-West track yet. This is good enough for getting Shadows working. Later, we’ll work on that. We’ll also work on changing the Sun’s light based on it’s direction/the time of day. We’ll also add the Moon so we can have some light at night. I won’t be taking into account Moon phases or the possibility of multiple Suns or Moons in this project, though.

Hopefully by next post I’ll have figured out what’s wrong with my Shadow Map code and fixed it. If not, I may still post about it. The basic algorithm is straight forward enough. I really can’t figure out what I’m doing wrong. I broke down and ordered Introduction to 3D Game Programming with DirectX 12 by Frank Luna. I’ve mentioned this book and its predecessor a few times as I have the DirectX 11 version and have been following it for a few aspects of this project. However, I’m finding the differences between DirectX 11 and 12 are really stumping me with the Shadow Mapping. I couldn’t find the answer in any example code or tutorial I could find online, so I ordered the book to see if it will make the reason I’m stuck clear. It’s supposed to arrive tomorrow.
I may also decide to refactor my Graphics class as well. I’ve been thinking a lot about what I want to do with it, mostly based on what I’ve seen going though the Microsoft Samples. We’ll see.

For the latest version of the code, go to GitHub.

Traagen