eb6f98ec55
Adds support for occlusion culling via rooms and portals.
475 lines
16 KiB
C++
475 lines
16 KiB
C++
/*************************************************************************/
|
|
/* portal_tracer.cpp */
|
|
/*************************************************************************/
|
|
/* This file is part of: */
|
|
/* GODOT ENGINE */
|
|
/* https://godotengine.org */
|
|
/*************************************************************************/
|
|
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
|
|
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
|
|
/* */
|
|
/* 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 "portal_tracer.h"
|
|
|
|
#include "portal_renderer.h"
|
|
#include "servers/visual/visual_server_globals.h"
|
|
#include "servers/visual/visual_server_scene.h"
|
|
|
|
PortalTracer::PlanesPool::PlanesPool() {
|
|
reset();
|
|
|
|
// preallocate the vectors to a reasonable size
|
|
for (int n = 0; n < POOL_MAX; n++) {
|
|
_planes[n].resize(32);
|
|
}
|
|
}
|
|
|
|
void PortalTracer::PlanesPool::reset() {
|
|
for (int n = 0; n < POOL_MAX; n++) {
|
|
_freelist[n] = POOL_MAX - n - 1;
|
|
}
|
|
|
|
_num_free = POOL_MAX;
|
|
}
|
|
|
|
unsigned int PortalTracer::PlanesPool::request() {
|
|
if (!_num_free) {
|
|
return -1;
|
|
}
|
|
|
|
_num_free--;
|
|
return _freelist[_num_free];
|
|
}
|
|
|
|
void PortalTracer::PlanesPool::free(unsigned int ui) {
|
|
DEV_ASSERT(ui < POOL_MAX);
|
|
DEV_ASSERT(_num_free < POOL_MAX);
|
|
|
|
_freelist[_num_free] = ui;
|
|
_num_free++;
|
|
}
|
|
|
|
void PortalTracer::trace_debug_sprawl(PortalRenderer &p_portal_renderer, const Vector3 &p_pos, int p_start_room_id, TraceResult &r_result) {
|
|
_portal_renderer = &p_portal_renderer;
|
|
_trace_start_point = p_pos;
|
|
_result = &r_result;
|
|
|
|
// all the statics should be not hit to start with
|
|
_result->clear();
|
|
|
|
// new test, new tick, to prevent hitting objects more than once
|
|
// on a test.
|
|
_tick++;
|
|
|
|
// if the camera is not in a room do nothing
|
|
if (p_start_room_id == -1) {
|
|
return;
|
|
}
|
|
|
|
trace_debug_sprawl_recursive(0, p_start_room_id);
|
|
}
|
|
|
|
void PortalTracer::trace(PortalRenderer &p_portal_renderer, const Vector3 &p_pos, const LocalVector<Plane> &p_planes, int p_start_room_id, TraceResult &r_result) {
|
|
// store local versions to prevent passing around recursive functions
|
|
_portal_renderer = &p_portal_renderer;
|
|
_trace_start_point = p_pos;
|
|
_result = &r_result;
|
|
|
|
// The near and far clipping planes needs special treatment. The problem is, if it is
|
|
// say a metre from the camera, it will clip out a portal immediately in front of the camera.
|
|
// as a result we want to use the near clipping plane for objects, but construct a fake
|
|
// near plane at exactly the position of the camera, to clip out portals that are behind us.
|
|
_near_and_far_planes[0] = p_planes[0];
|
|
_near_and_far_planes[1] = p_planes[1];
|
|
|
|
// all the statics should be not hit to start with
|
|
_result->clear();
|
|
|
|
// new test, new tick, to prevent hitting objects more than once
|
|
// on a test.
|
|
_tick++;
|
|
|
|
// if the camera is not in a room do nothing
|
|
// (this will return no hits, but is unlikely because the find_rooms lookup will return the nearest
|
|
// room even if not inside)
|
|
if (p_start_room_id == -1) {
|
|
return;
|
|
}
|
|
|
|
// start off the trace with the planes from the camera
|
|
LocalVector<Plane> cam_planes;
|
|
cam_planes = p_planes;
|
|
|
|
if (p_portal_renderer.get_cull_using_pvs()) {
|
|
trace_pvs(p_start_room_id, cam_planes);
|
|
} else {
|
|
// alternative : instead of copying straight, we create the first (near) clipping
|
|
// plane manually, at 0 distance from the camera. This ensures that portals will not be
|
|
// missed, while still culling portals and objects behind us. If we use the actual near clipping plane
|
|
// then a portal in front of the camera may not be seen through, giving glitches
|
|
cam_planes[0] = Plane(p_pos, cam_planes[0].normal);
|
|
|
|
TraceParams params;
|
|
params.use_pvs = p_portal_renderer.get_pvs().is_loaded();
|
|
|
|
// create bitfield
|
|
if (params.use_pvs) {
|
|
const PVS &pvs = _portal_renderer->get_pvs();
|
|
if (!pvs.get_pvs_size()) {
|
|
params.use_pvs = false;
|
|
} else {
|
|
// decompress a simple to read roomlist bitfield (could use bits maybe but bytes ok for now)
|
|
params.decompressed_room_pvs = nullptr;
|
|
params.decompressed_room_pvs = (uint8_t *)alloca(sizeof(uint8_t) * pvs.get_pvs_size());
|
|
memset(params.decompressed_room_pvs, 0, sizeof(uint8_t) * pvs.get_pvs_size());
|
|
const VSRoom &source_room = _portal_renderer->get_room(p_start_room_id);
|
|
|
|
for (int n = 0; n < source_room._pvs_size; n++) {
|
|
int room_id = pvs.get_pvs_room_id(source_room._pvs_first + n);
|
|
params.decompressed_room_pvs[room_id] = 255;
|
|
}
|
|
}
|
|
}
|
|
|
|
trace_recursive(params, 0, p_start_room_id, cam_planes);
|
|
}
|
|
}
|
|
|
|
void PortalTracer::cull_roamers(const VSRoom &p_room, const LocalVector<Plane> &p_planes) {
|
|
int num_roamers = p_room._roamer_pool_ids.size();
|
|
|
|
for (int n = 0; n < num_roamers; n++) {
|
|
uint32_t pool_id = p_room._roamer_pool_ids[n];
|
|
|
|
PortalRenderer::Moving &moving = _portal_renderer->get_pool_moving(pool_id);
|
|
|
|
// done already?
|
|
if (moving.last_tick_hit == _tick) {
|
|
continue;
|
|
}
|
|
|
|
// mark as done
|
|
moving.last_tick_hit = _tick;
|
|
|
|
if (test_cull_inside(moving.exact_aabb, p_planes)) {
|
|
_result->visible_roamer_pool_ids.push_back(pool_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PortalTracer::cull_statics_debug_sprawl(const VSRoom &p_room) {
|
|
int num_statics = p_room._static_ids.size();
|
|
|
|
for (int n = 0; n < num_statics; n++) {
|
|
uint32_t static_id = p_room._static_ids[n];
|
|
|
|
// VSStatic &stat = _portal_renderer->get_static(static_id);
|
|
|
|
// deal with dynamic stats
|
|
// if (stat.dynamic) {
|
|
// VSG::scene->_instance_get_transformed_aabb(stat.instance, stat.aabb);
|
|
// }
|
|
|
|
// set the visible bit if not set
|
|
if (!_result->bf_visible_statics.check_and_set(static_id)) {
|
|
_result->visible_static_ids.push_back(static_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PortalTracer::cull_statics(const VSRoom &p_room, const LocalVector<Plane> &p_planes) {
|
|
int num_statics = p_room._static_ids.size();
|
|
|
|
for (int n = 0; n < num_statics; n++) {
|
|
uint32_t static_id = p_room._static_ids[n];
|
|
|
|
VSStatic &stat = _portal_renderer->get_static(static_id);
|
|
|
|
// deal with dynamic stats
|
|
if (stat.dynamic) {
|
|
VSG::scene->_instance_get_transformed_aabb(stat.instance, stat.aabb);
|
|
}
|
|
|
|
// estimate the radius .. for now
|
|
const AABB &bb = stat.aabb;
|
|
|
|
// print("\t\t\tculling object " + pObj->get_name());
|
|
|
|
if (test_cull_inside(bb, p_planes)) {
|
|
// bypass the bitfield for now and just show / hide
|
|
//stat.show(bShow);
|
|
|
|
// set the visible bit if not set
|
|
if (_result->bf_visible_statics.check_and_set(static_id)) {
|
|
// if wasn't previously set, add to the visible list
|
|
_result->visible_static_ids.push_back(static_id);
|
|
}
|
|
}
|
|
|
|
} // for n through statics
|
|
}
|
|
|
|
int PortalTracer::trace_globals(const LocalVector<Plane> &p_planes, VSInstance **p_result_array, int first_result, int p_result_max, uint32_t p_mask) {
|
|
uint32_t num_globals = _portal_renderer->get_num_moving_globals();
|
|
int current_result = first_result;
|
|
|
|
for (uint32_t n = 0; n < num_globals; n++) {
|
|
const PortalRenderer::Moving &moving = _portal_renderer->get_moving_global(n);
|
|
|
|
#ifdef PORTAL_RENDERER_STORE_MOVING_RIDS
|
|
// debug check the instance is valid
|
|
void *vss_instance = VSG::scene->_instance_get_from_rid(moving.instance_rid);
|
|
|
|
if (vss_instance) {
|
|
#endif
|
|
if (test_cull_inside(moving.exact_aabb, p_planes, false)) {
|
|
if (VSG::scene->_instance_cull_check(moving.instance, p_mask)) {
|
|
p_result_array[current_result++] = moving.instance;
|
|
|
|
// full up?
|
|
if (current_result >= p_result_max) {
|
|
return current_result;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef PORTAL_RENDERER_STORE_MOVING_RIDS
|
|
} else {
|
|
WARN_PRINT("vss instance is null " + PortalRenderer::_addr_to_string(moving.instance));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return current_result;
|
|
}
|
|
|
|
void PortalTracer::trace_debug_sprawl_recursive(int p_depth, int p_room_id) {
|
|
if (p_depth > 1) {
|
|
return;
|
|
}
|
|
|
|
// prevent too much depth
|
|
ERR_FAIL_COND_MSG(p_depth > 8, "Portal Depth Limit reached");
|
|
|
|
// get the room
|
|
const VSRoom &room = _portal_renderer->get_room(p_room_id);
|
|
|
|
int num_portals = room._portal_ids.size();
|
|
|
|
for (int p = 0; p < num_portals; p++) {
|
|
const VSPortal &portal = _portal_renderer->get_portal(room._portal_ids[p]);
|
|
|
|
if (!portal._active) {
|
|
continue;
|
|
}
|
|
|
|
cull_statics_debug_sprawl(room);
|
|
|
|
// everything depends on whether the portal is incoming or outgoing.
|
|
int outgoing = 1;
|
|
|
|
int room_a_id = portal._linkedroom_ID[0];
|
|
if (room_a_id != p_room_id) {
|
|
outgoing = 0;
|
|
DEV_ASSERT(portal._linkedroom_ID[1] == p_room_id);
|
|
}
|
|
|
|
// trace through this portal to the next room
|
|
int linked_room_id = portal._linkedroom_ID[outgoing];
|
|
|
|
if (linked_room_id != -1) {
|
|
trace_debug_sprawl_recursive(p_depth + 1, linked_room_id);
|
|
} // if a linked room exists
|
|
|
|
} // for p through portals
|
|
}
|
|
|
|
void PortalTracer::trace_pvs(int p_source_room_id, const LocalVector<Plane> &p_planes) {
|
|
const PVS &pvs = _portal_renderer->get_pvs();
|
|
const VSRoom &source_room = _portal_renderer->get_room(p_source_room_id);
|
|
|
|
for (int r = 0; r < source_room._pvs_size; r++) {
|
|
int room_id = pvs.get_pvs_room_id(source_room._pvs_first + r);
|
|
|
|
// get the room
|
|
const VSRoom &room = _portal_renderer->get_room(room_id);
|
|
|
|
cull_statics(room, p_planes);
|
|
cull_roamers(room, p_planes);
|
|
}
|
|
}
|
|
|
|
void PortalTracer::trace_recursive(const TraceParams &p_params, int p_depth, int p_room_id, const LocalVector<Plane> &p_planes) {
|
|
// prevent too much depth
|
|
if (p_depth > _depth_limit) {
|
|
WARN_PRINT_ONCE("Portal Depth Limit reached (seeing through too many portals)");
|
|
return;
|
|
}
|
|
|
|
// get the room
|
|
const VSRoom &room = _portal_renderer->get_room(p_room_id);
|
|
|
|
cull_statics(room, p_planes);
|
|
cull_roamers(room, p_planes);
|
|
|
|
int num_portals = room._portal_ids.size();
|
|
|
|
for (int p = 0; p < num_portals; p++) {
|
|
const VSPortal &portal = _portal_renderer->get_portal(room._portal_ids[p]);
|
|
|
|
// portals can be switched on and off at runtime, like opening and closing a door
|
|
if (!portal._active) {
|
|
continue;
|
|
}
|
|
|
|
// everything depends on whether the portal is incoming or outgoing.
|
|
// if incoming we reverse the logic.
|
|
int outgoing = 1;
|
|
|
|
int room_a_id = portal._linkedroom_ID[0];
|
|
if (room_a_id != p_room_id) {
|
|
outgoing = 0;
|
|
DEV_ASSERT(portal._linkedroom_ID[1] == p_room_id);
|
|
}
|
|
|
|
// trace through this portal to the next room
|
|
int linked_room_id = portal._linkedroom_ID[outgoing];
|
|
|
|
// cull by PVS
|
|
if (p_params.use_pvs && (!p_params.decompressed_room_pvs[linked_room_id])) {
|
|
continue;
|
|
}
|
|
|
|
// cull by portal angle to camera.
|
|
|
|
// much better way of culling portals by direction to camera...
|
|
// instead of using dot product with a varying view direction, we simply find which side of the portal
|
|
// plane the camera is on! If it is behind, the portal can be seen through, if in front, it can't
|
|
real_t dist_cam = portal._plane.distance_to(_trace_start_point);
|
|
|
|
if (!outgoing) {
|
|
dist_cam = -dist_cam;
|
|
}
|
|
|
|
if (dist_cam >= 0.0) {
|
|
continue;
|
|
}
|
|
|
|
// is it culled by the planes?
|
|
VSPortal::ClipResult overall_res = VSPortal::ClipResult::CLIP_INSIDE;
|
|
|
|
// while clipping to the planes we maintain a list of partial planes, so we can add them to the
|
|
// recursive next iteration of planes to check
|
|
static LocalVector<int> partial_planes;
|
|
partial_planes.clear();
|
|
|
|
// for portals, we want to ignore the near clipping plane, as we might be right on the edge of a doorway
|
|
// and still want to look through the portal.
|
|
// so earlier we have set it that the first plane (ASSUMING that plane zero is the near clipping plane)
|
|
// starts from the camera position, and NOT the actual near clipping plane.
|
|
// if we need quite a distant near plane, we may need a different strategy.
|
|
for (uint32_t l = 0; l < p_planes.size(); l++) {
|
|
VSPortal::ClipResult res = portal.clip_with_plane(p_planes[l]);
|
|
|
|
switch (res) {
|
|
case VSPortal::ClipResult::CLIP_OUTSIDE: {
|
|
overall_res = res;
|
|
} break;
|
|
case VSPortal::ClipResult::CLIP_PARTIAL: {
|
|
// if the portal intersects one of the planes, we should take this plane into account
|
|
// in the next call of this recursive trace, because it can be used to cull out more objects
|
|
overall_res = res;
|
|
partial_planes.push_back(l);
|
|
} break;
|
|
default: // suppress warning
|
|
break;
|
|
}
|
|
|
|
// if the portal was totally outside the 'frustum' then we can ignore it
|
|
if (overall_res == VSPortal::ClipResult::CLIP_OUTSIDE)
|
|
break;
|
|
}
|
|
|
|
// this portal is culled
|
|
if (overall_res == VSPortal::ClipResult::CLIP_OUTSIDE) {
|
|
continue;
|
|
}
|
|
|
|
// hopefully the portal actually leads somewhere...
|
|
if (linked_room_id != -1) {
|
|
// we need some new planes
|
|
unsigned int pool_mem = _planes_pool.request();
|
|
|
|
// if the planes pool is not empty, we got some planes, and can recurse
|
|
if (pool_mem != (unsigned int)-1) {
|
|
// get a new vector of planes from the pool
|
|
LocalVector<Plane> &new_planes = _planes_pool.get(pool_mem);
|
|
|
|
// makes sure there are none left over (as the pool may not clear them)
|
|
new_planes.clear();
|
|
|
|
// if portal is totally inside the planes, don't copy the old planes ..
|
|
// i.e. we can now cull using the portal and forget about the rest of the frustum (yay)
|
|
// note that this loses the far clipping plane .. but that shouldn't be important usually?
|
|
// (maybe we might need to account for this in future .. look for issues)
|
|
if (overall_res != VSPortal::ClipResult::CLIP_INSIDE) {
|
|
// if it WASNT totally inside the existing frustum, we also need to add any existing planes
|
|
// that cut the portal.
|
|
for (uint32_t n = 0; n < partial_planes.size(); n++) {
|
|
new_planes.push_back(p_planes[partial_planes[n]]);
|
|
}
|
|
}
|
|
|
|
// we will always add the portals planes. This could probably be optimized, as some
|
|
// portal planes may be culled out by partial planes... NYI
|
|
portal.add_planes(_trace_start_point, new_planes, outgoing != 0);
|
|
|
|
// always add the far plane. It is likely the portal is inside the far plane,
|
|
// but it is still needed in future for culling portals and objects.
|
|
// note that there is a small possibility of far plane being added twice here
|
|
// in some situations, but I don't think it should be a problem.
|
|
// The fake near plane BTW is almost never added (otherwise it would prematurely
|
|
// break traversal through the portals), so near clipping must be done
|
|
// explicitly on objects.
|
|
new_planes.push_back(_near_and_far_planes[1]);
|
|
|
|
// go and do the whole lot again in the next room
|
|
trace_recursive(p_params, p_depth + 1, linked_room_id, new_planes);
|
|
|
|
// no longer need these planes, return them to the pool
|
|
_planes_pool.free(pool_mem);
|
|
|
|
} // pool mem allocated
|
|
else {
|
|
// planes pool is empty!
|
|
// This will happen if the view goes through shedloads of portals
|
|
// The solution is either to increase the plane pool size, or not build levels
|
|
// with views through multiple portals. Looking through multiple portals is likely to be
|
|
// slow anyway because of the number of planes to test.
|
|
WARN_PRINT_ONCE("planes pool is empty");
|
|
// note we also have a depth check at the top of this function. Which will probably get hit
|
|
// before the pool gets empty.
|
|
}
|
|
|
|
} // if a linked room exists
|
|
} // for p through portals
|
|
}
|