Skip to content

Commit

Permalink
GRAPHICS: LightManager static tree
Browse files Browse the repository at this point in the history
Implement a static tree for static lights, to increase performance
of determining mesh/light intersections.

Signed-off-by: mirv <mirv.sillyfish@gmail.com>
  • Loading branch information
mirv-sillyfish committed Oct 13, 2023
1 parent 8183a41 commit 981c7f6
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 56 deletions.
292 changes: 242 additions & 50 deletions src/graphics/lightman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,25 @@
#include "src/graphics/lightman.h"
#include "src/graphics/renderable.h"

#include <limits.h>

DECLARE_SINGLETON(Graphics::LightManager)

namespace Graphics {

LightManager::LightManager() : _maxLights(8), _activeLights(0) {
_lights.resize(_maxLights);
LightManager::LightManager() : _staticRootNode(nullptr), _staticDirty(true), _maxLights(8), _activeLights(0) {
_lightsGL.resize(_maxLights);
}

LightManager::~LightManager() {
deleteStaticTree();
}

void LightManager::registerLight(const LightNode *light) {
if (!isLightRegistered(light)) {
_registered.push_back(light);
}
_staticDirty = true;
}

void LightManager::deregisterLight(const LightNode *light) {
Expand All @@ -54,6 +58,7 @@ void LightManager::deregisterLight(const LightNode *light) {
break;
}
}
_staticDirty = true;
}

bool LightManager::isLightRegistered(const LightNode *light) {
Expand All @@ -67,68 +72,255 @@ bool LightManager::isLightRegistered(const LightNode *light) {

void LightManager::buildActiveLights(const glm::vec3 &pos, float radius) {
/**
* This is about the most inefficient way possible to build a list of lights
* that are applied to a mesh. There's no static/dynamic difference: this
* is done every single frame, for every single active modelnode with a mesh.
*
* There's no sorting of lights by distance, no priority checking when the
* list is filled.
* Building a list of active lights is still very much a work in progress.
* Only static lights are currently supported - but dynamic lights are
* unlikely to be put into a tree structure just yet.
*
* This gets much slower on larger levels with hundreds of lights. A lot of
* time can be wasted on just this step. There is however one glimmer (pun
* intended) of hope: levels fade in out of view around the player character.
* This means lights can be activated when a tile comes into view, and
* deactivated when it goes out of view. Not every mesh is rendered every
* frame, and not every light is in the global list to check against. That's
* the theory anyway, but this kind of behaviour is not yet implemented.
* Light priorities are also currently completely ignored. This is something
* to fix later, or see if it's even required at all: increasing the number
* of supported active lights is also an option that might simply solve it
* anyway.
*
* Another approach to build on the above, if needed, is to store active
* lighting in a quadtree or similar. That will allow lists of lights to be
* built very quickly, especially if the lights are mostly static. Or keep
* a combination of the two: hierarchy for static lights (e.g tile main lights)
* and just a normal list for dynamic lights (e.g torches held by characters).
* It might be feasible to pre-build static light lists for static geometry as
* well, which would remove an awful lot per-frame overhead. There would still
* be a need for checking against dynamic lighting, but that's expected to be
* at least an order of magnitude less.
*/

glm::mat4 modelview = CameraMan.getModelview();
if (_staticDirty) {
buildStaticTree();
}
_activeLights = 0;
for (const auto *licht : _registered) {

if (_staticRootNode) {
searchStaticTree(pos, radius);
}
}

void LightManager::buildStaticTree() {
/**
* Note that any node in the hierarchy could potentially contain a light if the radius of
* that light entirely encompasses another one or more lights. That's not considered to be
* the most likely of circumstances however, so for now the algorithm to build a tree is
* a little more focused on populating only leaf nodes.
*/

// Clear the tree if it's already been built, or else memory leakage ensues.
deleteStaticTree();

if (_registered.size() == 0) {
return;
}

std::vector<StaticLightNode *> initial;
for (size_t i = 0; i < _registered.size(); ++i) {
StaticLightNode *slight = new StaticLightNode();
const LightNode *light = _registered[i];
float radius = light->radius;
slight->light = light;
slight->centre = light->position;
slight->radius = radius;
slight->left = nullptr;
slight->right = nullptr;
slight->aabb_min = slight->centre + glm::vec3(-radius, -radius, -radius);
slight->aabb_max = slight->centre + glm::vec3(radius, radius, radius);
initial.push_back(slight);
}

while (initial.size() > 1) {
std::vector<StaticLightNode *> working;
/**
* Need to iterate over all registered lights to determine which are closest to each other.
* Once a pair is found, it's removed from the list and a static light node is created. This
* will be put onto a "working list", and then go around the initial list again. Once the
* initial list is depleted, refill it with the new nodes in the working list.
* Keep going round and round until only one element in the initial list remains after the
* refill - this element will be the root node.
*/
while (initial.size() > 1) {
/**
* This inner loop first grabs the last element, and then calculates which other node
* is closest to it.
*/
StaticLightNode *light_a = initial.back();
StaticLightNode *light_b = nullptr;
initial.pop_back();
float min_distance = FLT_MAX;
int index = -1;
for (size_t i = 0; i < initial.size(); ++i) {
StaticLightNode *test_light = initial[i];
float test_distance = glm::distance(light_a->centre, test_light->centre);
if (test_distance < min_distance) {
min_distance = test_distance;
index = static_cast<int>(i);
}
}
if ((index >= 0)) {
// The nearest to light_a has been found, assign to light_b and remove from initial.
light_b = initial[index];
initial[index] = initial.back();
initial.pop_back();
}

if (light_a && light_b) {
StaticLightNode *slight = new StaticLightNode();
///< @todo: populate slight.
slight->left = light_a;
slight->right = light_b;
slight->light = nullptr;
// Now get the extents to determine the centre point of the new node.
glm::vec3 diff = light_b->centre - light_a->centre;
float distance = glm::length(diff);
if (distance <= light_a->radius) {
slight->centre = light_a->centre;
slight->radius = light_a->radius;
} else if (distance <= light_b->radius) {
slight->centre = light_b->centre;
slight->radius = light_b->radius;
} else {
glm::vec3 unit = glm::normalize(diff);
glm::vec3 max = diff + (unit * light_b->radius);
glm::vec3 min = -(unit * light_a->radius);
slight->radius = glm::length(max - min) * 0.5f;
glm::vec3 relativeCentre = (max + min) * 0.5f;
slight->centre = light_a->centre + relativeCentre;
}
slight->aabb_min = glm::min(light_a->aabb_min, light_b->aabb_min);
slight->aabb_max = glm::max(light_a->aabb_max, light_b->aabb_max);
working.push_back(slight);
} else {
working.push_back(light_a);
}
}

/**
* For comparing distances, it's just as easy to compare distance squared
* instead, which will avoid the use of sqrt.
* After going through the initial list a few times to pair up as many nodes as possible,
* replenish the list with the newly created nodes before another round.
*/
glm::vec3 d = pos - licht->position;
for (size_t i = 0; i < working.size(); ++i) {
initial.push_back(working[i]);
}
working.empty();
// Then go back over it all again until the root node is reached, i.e initial.size() == 1.
}

assert(initial.size() == 1);

// The complete tree should now be built, but don't forget to track the root node.
_staticRootNode = initial[0];

_staticDirty = false;
}

void LightManager::deleteStaticTree() {
deleteStaticTree(_staticRootNode);
_staticRootNode = nullptr;
}

void LightManager::deleteStaticTree(StaticLightNode *node) {
if (node) {
deleteStaticTree(node->left);
deleteStaticTree(node->right);
delete node;
}
}

void LightManager::searchStaticTree(const glm::vec3 &pos, const float radius) {
if (!_staticRootNode) {
return;
}

_staticSearchStack.push_back(_staticRootNode);
do {
const StaticLightNode *node = _staticSearchStack.back();
_staticSearchStack.pop_back();

glm::vec3 d = pos - node->centre;
float dsquared = glm::dot(d, d);
float rsquared = (licht->radius + radius);
float rsquared = (node->radius + radius);
rsquared *= rsquared;
if (dsquared < rsquared) {
auto &light = _lights[_activeLights];
glm::vec4 modified = modelview * glm::vec4(licht->position, 1.0f);
light.position[0] = modified.x;
light.position[1] = modified.y;
light.position[2] = modified.z;
light.position[3] = 1.0f;

light.colour[0] = licht->colour[0];
light.colour[1] = licht->colour[1];
light.colour[2] = licht->colour[2];
light.colour[3] = 1.0f;
if (node->light) {
activateLight(node->light);
if (_activeLights >= _maxLights) {
/**
* Number of lights has reached maximum, don't bother searching more just yet.
* In theory lights do have priorities and there might be a higher priority
* found later, but that's for another time.
*/
break;
}
}
if (node->left) {
_staticSearchStack.push_back(node->left);
}
if (node->right) {
_staticSearchStack.push_back(node->right);
}
}
} while (_staticSearchStack.size() > 0);
_staticSearchStack.empty();
}

/**
* @TODO: Some of the coefficients here could be pre-calculated as a performance
* benefit. How much benefit is a matter up for debate, but either way it's
* recommended to wait until later down the line to implement: get it all
* working first, then optimise and increase performance.
*/
light.coefficients[0] = licht->multiplier;
light.coefficients[1] = 1.0f / (licht->radius * licht->radius * 0.25f);
light.coefficients[2] = licht->ambient ? 1.0f : 0.0f;
light.coefficients[3] = licht->ambient ? 0.0f : 1.0f;
void LightManager::searchStaticTree(const glm::vec3 &pos, const float radius, const StaticLightNode *node) {
if (_activeLights >= _maxLights) {
// Early-out if the maximum number of lights has been reached.
return;
}

glm::vec3 d = pos - node->centre;
float dsquared = glm::dot(d, d);
float rsquared = (node->radius + radius);
rsquared *= rsquared;
if (dsquared < rsquared) {
// There is intersection between the node and provided bounding sphere.

/**
* @TODO: check against an AABB to trim out branches. This is not guaranteed to be
* optimal, and will definitely depend on how distributed lights are, and generally
* how close the AABB is to a bounding sphere. Basically need to test and see which
* is better: just the bounding sphere test, or also with the AABB.
*/

if (++_activeLights >= _maxLights) {
break;
if (node->light) {
activateLight(node->light);
if (_activeLights >= _maxLights) {
return;
}
}
if (node->left) {
searchStaticTree(pos, radius, node->left);
}
if (node->right) {
searchStaticTree(pos, radius, node->right);
}
}
}

void LightManager::activateLight(const LightNode *light) {
LightGL &lightGL = _lightsGL[_activeLights];
glm::vec4 modified = CameraMan.getModelview() * glm::vec4(light->position, 1.0f);
lightGL.position[0] = modified.x;
lightGL.position[1] = modified.y;
lightGL.position[2] = modified.z;
lightGL.position[3] = 1.0f;

lightGL.colour[0] = light->colour[0];
lightGL.colour[1] = light->colour[1];
lightGL.colour[2] = light->colour[2];
lightGL.colour[3] = 1.0f;

/**
* @TODO: Some of the coefficients here could be pre-calculated as a performance
* benefit. How much benefit is a matter up for debate, but either way it's
* recommended to wait until later down the line to implement: get it all
* working first, then optimise and increase performance.
*/
lightGL.coefficients[0] = light->multiplier;
lightGL.coefficients[1] = 1.0f / (light->radius * light->radius * 0.25f);
lightGL.coefficients[2] = light->ambient ? 1.0f : 0.0f;
lightGL.coefficients[3] = light->ambient ? 0.0f : 1.0f;

++_activeLights;
}

} // End of namespace Graphics
Loading

0 comments on commit 981c7f6

Please sign in to comment.