Lighting System

I created a lighting system for my game engine MOABE. The core principles behind the design of the system were to simplify the process of adding and removing lights from a scene. In addition, I wanted to update the process of updating lights.

Light Management

At the core of the light system, is the idea that lights are intrinsically tied to scenes. This is already a departure from how the graphics engine, jAzul, treats lights. In the graphics engine, all the ShaderContextManager is responsible for for tracking all the lights that are set to be sent to the shaders in a static array, since DirectX 11 does not support dynamic arrays. That is why an essential function for my Light System is the ability to Register and Deregister lights.

UML Diagram

// Registration Methods
void LightManager::RegisterLight(MOABEDirectionalLight* pDirLight)
{
	if (this->dirList.size() < MAX_DIR_LIGHTS) {
		this->dirList.push_back(pDirLight);
	}
	else {
		Trace::out("MOABE WARNING: Directional Light At Mem: [%p] was not registered as you have exceeded the maximum number of directional lights. Please deregister one before trying again.");
	}
}

void LightManager::RegisterLight(MOABEPointLight* pPointLight)
{
	this->pointList.push_back(pPointLight);
}

void LightManager::RegisterLight(MOABESpotLight* pSpotLight)
{
	this->spotList.push_back(pSpotLight);
}

// Deregistration Methods
void LightManager::DeregisterLight(MOABEDirectionalLight* pDirLight)
{
	auto it = std::find(this->dirList.begin(), this->dirList.end(), pDirLight);

	assert(it != this->dirList.end());
	this->dirList.erase(it);
}

void LightManager::DeregisterLight(MOABEPointLight* pPointLight)
{
	auto it = std::find(this->pointList.begin(), this->pointList.end(), pPointLight);
	
	assert(it != this->pointList.end());
	this->pointList.erase(it);
}

void LightManager::DeregisterLight(MOABESpotLight* pSpotLight)
{
	auto it = std::find(this->spotList.begin(), this->spotList.end(), pSpotLight);

	assert(it != this->spotList.end());
	this->spotList.erase(it);
}

These methods are allowed to be as simple as they are because instead of processing the light at the moment of registration we process them every frame. While it may seem like this it may cost us on performance, we still leave the option to skip the processing all together if they have less than the maximum number of lights enabled. Having lights be processed every frame however gives users a great deal more flexibility when registering lights.

There is still the question of how exactly lights are processed. The Light Manager has three modes/stratgeies:

static NormalProcessor NormalMode;
static CullDistProcessor CullDistMode;
static CullViewProcessor CullViewMode;

  • NormalProcessor performs no processing on the lights UNLESS the number of lights surpasses the maximum. In this case it culls the lights FURTHEST from the main scene camera.
  • CullDistProcessor tests and only renders the lights within a certain distance from the camera.
  • CullViewProcessor tests and only renders the lights within a certain distance AND in front of the main scene camera.

From a performance point of view, this system is extremely flexible. If a developer knows their scene will not have many lights they can save on performance and stay in normal mode. If they do intend on having many lights they can set the LightManager to cull those lights not relevant to the current frame.

Note: This light culling is SEPARATE from the light culling performed on the graphics engine side. All the hlsl shaders in jAzul all perform light culling internally. This means they do NOT perform the PHONG calculations on lights outside of the camera’s view.

Light culling in action. Here, the LightManager is set to CullView meaning as you can see, only the lights in front of the camera are rendered.

As mentioned before, the lights must be processed every frame, at the end of the scene’s Update loop.

void Scene::Update()
{
	this->alarmMan.ProcessAlarms();
	this->cmdBroker.ProcessCommands();
	this->colMan.ProcessCollisions();
	this->updateMan.ProcessElements();
	this->keyMan.ProcessInputs();
	this->lightMan.ProcessLights();    // Lights are updated every frame
}

// ...

void LightManager::ProcessLights() const
{
        // Since Directional lights are visible no matter what, no processing is required
	unsigned int i = 0;
	for (MOABELight* pLight : this->dirList) {
		LightAttorney::Light::UpdateLight(pLight);
		ShaderContextManager::SetDirectionalLightParameters(LIGHTNUM(i), &((MOABEDirectionalLight*)pLight)->GetLight());
		i++;
	}

	this->ProcessMode->ProcessLights(this->pointList, this->spotList, this->minRenderDistanceSqr);
}

Moving on to the tests themselves, here is what those look like:

	for (MOABELight* pLight : pointList) {
		LightAttorney::Light::UpdateLight(pLight);

		const Vect toLight = ((MOABEPointLight&)*pLight).GetPosition() - CamPos;
		const float distToLight = toLight.magSqr();

		// First Check if The Light Is Within The Render Distance
		if (distToLight <= distSqr) {
			// Then we check if the lights is "in view" aka in front of the camera
			// We do that by checking the dot product to see if it is greater than 0 aka the angle between them is greater than 90 degrees
			if (toLight.dot(CamDir) <= 0.0f) {
				pointDists.push_back(LightDistPair(distToLight, pLight));
			}
		}
	}

	unsigned int i = 0;
	while (i < MAX_POINT_LIGHTS && i < pointDists.size()) {
		ShaderContextManager::SetPointLightParameters(LIGHTNUM(i), &((MOABEPointLight*)pointDists[i].second)->GetLight());
		i++;
	}

	while (i < MAX_POINT_LIGHTS) {
		ShaderContextManager::SetPointLightParameters(LIGHTNUM(i), &LightManager::emptyPointLight);
		i++;
	}

Note: This is what the CullVIew test looks like. The only unique math operation here is the dot product to check the lights angle to the camera. The distance tests are the same in the other strategies.

As you can see they are relativity un-intense, being only comprised of multiplication and addition. However, since these calculations are performed every frame, it would obviously be preferable to avoid them unless necessary.

Light Behaviors

UML Diagram