444 lines
16 KiB
C++
444 lines
16 KiB
C++
/**
|
|
*
|
|
* RenderPipeline
|
|
*
|
|
* Copyright (c) 2014-2016 tobspr <tobias.springer1@gmail.com>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
*/
|
|
|
|
|
|
#include "internalLightManager.h"
|
|
|
|
#include <algorithm>
|
|
|
|
using std::endl;
|
|
|
|
NotifyCategoryDef(lightmgr, "");
|
|
|
|
|
|
/**
|
|
* @brief Constructs the light manager
|
|
* @details This constructs the light manager, initializing the light and shadow
|
|
* storage. You should set a command list and shadow manager before calling
|
|
* InternalLightManager::update. s
|
|
*/
|
|
InternalLightManager::InternalLightManager() {
|
|
_shadow_update_distance = 100.0;
|
|
_cmd_list = nullptr;
|
|
_shadow_manager = nullptr;
|
|
}
|
|
|
|
/**
|
|
* @brief Adds a new light.
|
|
* @details This adds a new light to the list of lights. This will throw an
|
|
* error and return if the light is already attached. You may only call
|
|
* this after the ShadowManager was already set.
|
|
*
|
|
* While the light is attached, the light manager keeps a reference to it, so
|
|
* the light does not get destructed.
|
|
*
|
|
* This also setups the shadows on the light, in case shadows are enabled.
|
|
* While a light is attached, you can not change whether it casts shadows or not.
|
|
* To do so, detach the light, change the setting, and re-add the light.
|
|
*
|
|
* In case no free light slot is available, an error will be printed and no
|
|
* action will be performed.
|
|
*
|
|
* If no shadow manager was set, an assertion will be triggered.
|
|
*
|
|
* @param light The light to add.
|
|
*/
|
|
void InternalLightManager::add_light(PT(RPLight) light) {
|
|
nassertv(_shadow_manager != nullptr); // Shadow manager not set yet!
|
|
|
|
// Don't attach the light in case its already attached
|
|
if (light->has_slot()) {
|
|
lightmgr_cat.error() << "could not add light because it already is attached! "
|
|
<< "Detach the light first, then try it again." << endl;
|
|
return;
|
|
}
|
|
|
|
// Find a free slot
|
|
size_t slot;
|
|
if (!_lights.find_slot(slot)) {
|
|
lightmgr_cat.error() << "Light limit of " << MAX_LIGHT_COUNT << " reached, "
|
|
<< "all light slots used!" << endl;
|
|
return;
|
|
}
|
|
|
|
// Reference the light because we store it, to avoid it getting destructed
|
|
// on the python side while we still work with it. The reference will be
|
|
// removed when the light gets detached.
|
|
light->ref();
|
|
|
|
// Reserve the slot
|
|
light->assign_slot(slot);
|
|
_lights.reserve_slot(slot, light);
|
|
|
|
// Setup the shadows in case the light uses them
|
|
if (light->get_casts_shadows()) {
|
|
setup_shadows(light);
|
|
}
|
|
|
|
// Store the light on the gpu, to make sure the GPU directly knows about it.
|
|
// We could wait until the next update cycle, but then we might be one frame
|
|
// too late already.
|
|
gpu_update_light(light);
|
|
}
|
|
|
|
/**
|
|
* @brief Internal method to setup shadows for a light
|
|
* @details This method gets called by the InternalLightManager::add_light method
|
|
* to setup a lights shadow sources, in case shadows are enabled on that light.
|
|
*
|
|
* It finds a slot for all shadow sources of the ilhgt, and inits the shadow
|
|
* sources as well. If no slot could be found, an error is printed an nothing
|
|
* happens.
|
|
*
|
|
* @param light The light to init the shadow sources for
|
|
*/
|
|
void InternalLightManager::setup_shadows(RPLight* light) {
|
|
|
|
// Init the lights shadow sources, and also call update once to make sure
|
|
// the sources are properly initialized
|
|
light->init_shadow_sources();
|
|
light->update_shadow_sources();
|
|
|
|
// Find consecutive slots, this is important for PointLights so we can just
|
|
// store the first index of the source, and get the other slots by doing
|
|
// first_index + 1, +2 and so on.
|
|
size_t base_slot;
|
|
size_t num_sources = light->get_num_shadow_sources();
|
|
if (!_shadow_sources.find_consecutive_slots(base_slot, num_sources)) {
|
|
lightmgr_cat.error() << "Failed to find slot for shadow sources! "
|
|
<< "Shadow-Source limit of " << MAX_SHADOW_SOURCES
|
|
<< " reached!" << endl;
|
|
return;
|
|
}
|
|
|
|
// Init all sources
|
|
for (size_t i = 0; i < num_sources; ++i) {
|
|
ShadowSource* source = light->get_shadow_source(i);
|
|
|
|
// Set the source as dirty, so it gets updated in the beginning
|
|
source->set_needs_update(true);
|
|
|
|
// Assign the slot to the source. Since we got consecutive slots, we can
|
|
// just do base_slot + N.
|
|
size_t slot = base_slot + i;
|
|
_shadow_sources.reserve_slot(slot, source);
|
|
source->set_slot(slot);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Removes a light
|
|
* @details This detaches a light. This prevents it from being rendered, and also
|
|
* cleans up all resources used by that light. If no reference is kept on the
|
|
* python side, the light will also get destructed.
|
|
*
|
|
* If the light was not previously attached with InternalLightManager::add_light,
|
|
* an error will be triggered and nothing happens.
|
|
*
|
|
* In case the light was set to cast shadows, all shadow sources are cleaned
|
|
* up, and their regions in the shadow atlas are freed.
|
|
*
|
|
* All resources used by the light in the light and shadow storage are also
|
|
* cleaned up, by emitting cleanup GPUCommands.
|
|
*
|
|
* If no shadow manager was set, an assertion will be triggered.
|
|
*
|
|
* @param light [description]
|
|
*/
|
|
void InternalLightManager::remove_light(PT(RPLight) light) {
|
|
nassertv(_shadow_manager != nullptr);
|
|
|
|
if (!light->has_slot()) {
|
|
lightmgr_cat.error() << "Could not detach light, light was not attached!" << endl;
|
|
return;
|
|
}
|
|
|
|
// Free the lights slot in the light storage
|
|
_lights.free_slot(light->get_slot());
|
|
|
|
// Tell the GPU we no longer need the lights data
|
|
gpu_remove_light(light);
|
|
|
|
// Mark the light as detached. After this call, we can not call get_slot
|
|
// anymore, so its important we do this after we unregistered the light
|
|
// from everywhere.
|
|
light->remove_slot();
|
|
|
|
// Clear shadow related stuff, in case the light casts shadows
|
|
if (light->get_casts_shadows()) {
|
|
|
|
// Free the slots of all sources, and also unregister their regions from
|
|
// the shadow atlas.
|
|
for (size_t i = 0; i < light->get_num_shadow_sources(); ++i) {
|
|
ShadowSource* source = light->get_shadow_source(i);
|
|
if (source->has_slot()) {
|
|
_shadow_sources.free_slot(source->get_slot());
|
|
}
|
|
if (source->has_region()) {
|
|
_shadow_manager->get_atlas()->free_region(source->get_region());
|
|
source->clear_region();
|
|
}
|
|
}
|
|
|
|
// Remove all sources of the light by emitting a consecutive remove command
|
|
gpu_remove_consecutive_sources(light->get_shadow_source(0),
|
|
light->get_num_shadow_sources());
|
|
|
|
// Finally remove all shadow sources. This is important in case the light
|
|
// will be re-attached. Otherwise an assertion will get triggered.
|
|
light->clear_shadow_sources();
|
|
}
|
|
|
|
// Since we referenced the light when we stored it, we have to decrease
|
|
// the reference now. In case no reference was kept on the python side,
|
|
// the light will get destructed soon.
|
|
light->unref();
|
|
}
|
|
|
|
/**
|
|
* @brief Internal method to remove consecutive sources from the GPU.
|
|
* @details This emits a GPUCommand to consecutively remove shadow sources from
|
|
* the GPU. This is called when a light gets removed, to free the space its
|
|
* shadow sources took. Its not really required, because as long as the light
|
|
* is not used, there is no reference to the sources. However, it can't hurt to
|
|
* cleanup the memory.
|
|
*
|
|
* All sources starting at first_source->get_slot() until
|
|
* first_source->get_slot() + num_sources will get cleaned up.
|
|
*
|
|
* @param first_source First source of the light
|
|
* @param num_sources Amount of consecutive sources to clear
|
|
*/
|
|
void InternalLightManager::gpu_remove_consecutive_sources(ShadowSource *first_source,
|
|
size_t num_sources) {
|
|
nassertv(_cmd_list != nullptr); // No command list set yet
|
|
nassertv(first_source->has_slot()); // Source has no slot!
|
|
GPUCommand cmd_remove(GPUCommand::CMD_remove_sources);
|
|
cmd_remove.push_int(first_source->get_slot());
|
|
cmd_remove.push_int(num_sources);
|
|
_cmd_list->add_command(cmd_remove);
|
|
}
|
|
|
|
/**
|
|
* @brief Internal method to remove a light from the GPU.
|
|
* @details This emits a GPUCommand to clear a lights data. This sets the data
|
|
* to all zeros, marking that no light is stored anymore.
|
|
*
|
|
* This throws an assertion in case the light is not currently attached. Be
|
|
* sure to call this before detaching the light.
|
|
*
|
|
* @param light The light to remove, must be attached.
|
|
*/
|
|
void InternalLightManager::gpu_remove_light(RPLight* light) {
|
|
nassertv(_cmd_list != nullptr); // No command list set yet
|
|
nassertv(light->has_slot()); // Light has no slot!
|
|
GPUCommand cmd_remove(GPUCommand::CMD_remove_light);
|
|
cmd_remove.push_int(light->get_slot());
|
|
_cmd_list->add_command(cmd_remove);
|
|
}
|
|
|
|
/**
|
|
* @brief Updates a lights data on the GPU
|
|
* @details This method emits a GPUCommand to update a lights data. This can
|
|
* be used to initially store the lights data, or to update the data whenever
|
|
* the light changed.
|
|
*
|
|
* This throws an assertion in case the light is not currently attached. Be
|
|
* sure to call this after attaching the light.
|
|
*
|
|
* @param light The light to update
|
|
*/
|
|
void InternalLightManager::gpu_update_light(RPLight* light) {
|
|
nassertv(_cmd_list != nullptr); // No command list set yet
|
|
nassertv(light->has_slot()); // Light has no slot!
|
|
GPUCommand cmd_update(GPUCommand::CMD_store_light);
|
|
cmd_update.push_int(light->get_slot());
|
|
light->write_to_command(cmd_update);
|
|
light->set_needs_update(false);
|
|
_cmd_list->add_command(cmd_update);
|
|
}
|
|
|
|
/**
|
|
* @brief Updates a shadow source data on the GPU
|
|
* @details This emits a GPUCommand to update a given shadow source, storing all
|
|
* data of the source on the GPU. This can also be used to initially store a
|
|
* ShadowSource, since all data will be overridden.
|
|
*
|
|
* This throws an assertion if the source has no slot yet.
|
|
*
|
|
* @param source The source to update
|
|
*/
|
|
void InternalLightManager::gpu_update_source(ShadowSource* source) {
|
|
nassertv(_cmd_list != nullptr); // No command list set yet
|
|
nassertv(source->has_slot()); // Source has no slot!
|
|
GPUCommand cmd_update(GPUCommand::CMD_store_source);
|
|
cmd_update.push_int(source->get_slot());
|
|
source->write_to_command(cmd_update);
|
|
_cmd_list->add_command(cmd_update);
|
|
}
|
|
|
|
/**
|
|
* @brief Internal method to update all lights
|
|
* @details This is called by the main update method, and iterates over the list
|
|
* of lights. If a light is marked as dirty, it will recieve an update of its
|
|
* data and its shadow sources.
|
|
*/
|
|
void InternalLightManager::update_lights() {
|
|
for (auto iter = _lights.begin(); iter != _lights.end(); ++iter) {
|
|
RPLight* light = *iter;
|
|
if (light && light->get_needs_update()) {
|
|
if (light->get_casts_shadows()) {
|
|
light->update_shadow_sources();
|
|
}
|
|
gpu_update_light(light);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Compares shadow sources by their priority
|
|
* @details Returns if a has a greater priority than b. This depends on the
|
|
* resolution of the source, and also if the source has a region or not.
|
|
* This method can be passed to std::sort.
|
|
*
|
|
* @param a First source
|
|
* @param b Second source
|
|
*
|
|
* @return true if a is more important than b, else false
|
|
*/
|
|
bool InternalLightManager::compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const {
|
|
|
|
// Make sure that sources which already have a region (but maybe outdated)
|
|
// come after sources which have no region at all.
|
|
if (a->has_region() != b->has_region()) {
|
|
return b->has_region();
|
|
}
|
|
|
|
// Compare sources based on their distance to the camera
|
|
PN_stdfloat dist_a = (_camera_pos - a->get_bounds().get_center()).length_squared();
|
|
PN_stdfloat dist_b = (_camera_pos - a->get_bounds().get_center()).length_squared();
|
|
|
|
// XXX: Should also compare based on source size, so that huge sources recieve
|
|
// more updates
|
|
|
|
return dist_b > dist_a;
|
|
}
|
|
|
|
/**
|
|
* @brief Internal method to update all shadow sources
|
|
* @details This updates all shadow sources which are marked dirty. It will sort
|
|
* the list of all dirty shadow sources by their resolution, take the first
|
|
* n entries, and update them. The amount of sources processed depends on the
|
|
* max_updates of the ShadowManager.
|
|
*/
|
|
void InternalLightManager::update_shadow_sources() {
|
|
|
|
// Find all dirty shadow sources and make a list of them
|
|
std::vector<ShadowSource*> sources_to_update;
|
|
for (auto iter = _shadow_sources.begin(); iter != _shadow_sources.end(); ++iter) {
|
|
ShadowSource* source = *iter;
|
|
if (source) {
|
|
const BoundingSphere& bounds = source->get_bounds();
|
|
|
|
// Check if source is in range
|
|
PN_stdfloat distance_to_camera = (_camera_pos - bounds.get_center()).length() - bounds.get_radius();
|
|
if (distance_to_camera < _shadow_update_distance) {
|
|
if (source->get_needs_update()) {
|
|
sources_to_update.push_back(source);
|
|
}
|
|
} else {
|
|
|
|
// Free regions of sources which are out of the update radius,
|
|
// to make space for other regions
|
|
if (source->has_region()) {
|
|
_shadow_manager->get_atlas()->free_region(source->get_region());
|
|
source->clear_region();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Sort the sources based on their importance, so that sources with a bigger
|
|
// priority come first. This helps to get a better packing on the shadow atlas.
|
|
// However, we also need to prioritize sources which have no current region,
|
|
// because no shadows are worse than outdated-shadows.
|
|
std::sort(sources_to_update.begin(), sources_to_update.end(), [this](const ShadowSource* a, const ShadowSource* b) {
|
|
return this->compare_shadow_sources(a, b);
|
|
});
|
|
|
|
// Get a handle to the atlas, will be frequently used
|
|
ShadowAtlas *atlas = _shadow_manager->get_atlas();
|
|
|
|
// Free the regions of all sources which will get updated. We have to take into
|
|
// account that only a limited amount of sources can get updated per frame.
|
|
size_t update_slots = std::min(sources_to_update.size(),
|
|
_shadow_manager->get_num_update_slots_left());
|
|
for(size_t i = 0; i < update_slots; ++i) {
|
|
if (sources_to_update[i]->has_region()) {
|
|
atlas->free_region(sources_to_update[i]->get_region());
|
|
}
|
|
}
|
|
|
|
// Find an atlas spot for all regions which are supposed to get an update
|
|
for (size_t i = 0; i < update_slots; ++i) {
|
|
ShadowSource *source = sources_to_update[i];
|
|
|
|
if(!_shadow_manager->add_update(source)) {
|
|
// In case the ShadowManager lied about the number of updates left
|
|
lightmgr_cat.error() << "ShadowManager ensured update slot, but slot is taken!" << endl;
|
|
break;
|
|
}
|
|
|
|
// We have an update slot, and are guaranteed to get updated as soon
|
|
// as possible, so we can start getting a new atlas position.
|
|
size_t region_size = atlas->get_required_tiles(source->get_resolution());
|
|
LVecBase4i new_region = atlas->find_and_reserve_region(region_size, region_size);
|
|
LVecBase4 new_uv_region = atlas->region_to_uv(new_region);
|
|
source->set_region(new_region, new_uv_region);
|
|
|
|
// Mark the source as updated
|
|
source->set_needs_update(false);
|
|
gpu_update_source(source);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Main update method
|
|
* @details This is the main update method of the InternalLightManager. It
|
|
* processes all lights and shadow sources, updates them, and notifies the
|
|
* GPU about it. This should be called on a per-frame basis.
|
|
*
|
|
* If the InternalLightManager was not initialized yet, an assertion is thrown.
|
|
*/
|
|
void InternalLightManager::update() {
|
|
nassertv(_shadow_manager != nullptr); // Not initialized yet!
|
|
nassertv(_cmd_list != nullptr); // Not initialized yet!
|
|
|
|
update_lights();
|
|
update_shadow_sources();
|
|
}
|