/*************************************************************************/ /* portal_occlusion_culler.cpp */ /*************************************************************************/ /* This file is part of: */ /* PANDEMONIUM ENGINE */ /* https://github.com/Relintai/pandemonium_engine */ /*************************************************************************/ /* Copyright (c) 2022-present Péter Magyar. */ /* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* */ /* 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_occlusion_culler.h" #include "core/config/engine.h" #include "core/config/project_settings.h" #include "core/math/aabb.h" #include "portal_renderer.h" #include "servers/rendering/rendering_server_globals.h" #include "servers/rendering/rendering_server_scene.h" #define _log(a, b) ; //#define _log_prepare(a) log(a, 0) #define _log_prepare(a) ; bool PortalOcclusionCuller::_debug_log = true; bool PortalOcclusionCuller::_redraw_gizmo = false; void PortalOcclusionCuller::Clipper::debug_print_points(String p_string) { print_line(p_string); for (int n = 0; n < _pts_in.size(); n++) { print_line("\t" + itos(n) + " : " + String(Variant(_pts_in[n]))); } } Plane PortalOcclusionCuller::Clipper::interpolate(const Plane &p_a, const Plane &p_b, real_t p_t) const { Vector3 diff = p_b.normal - p_a.normal; real_t d = p_b.d - p_a.d; diff *= p_t; d *= p_t; return Plane(p_a.normal + diff, p_a.d + d); } real_t PortalOcclusionCuller::Clipper::clip_and_find_poly_area(const Plane *p_verts, int p_num_verts) { _pts_in.clear(); _pts_out.clear(); // seed for (int n = 0; n < p_num_verts; n++) { _pts_in.push_back(p_verts[n]); } if (!clip_to_plane(-1, 0, 0, 1)) { return 0.0; } if (!clip_to_plane(1, 0, 0, 1)) { return 0.0; } if (!clip_to_plane(0, -1, 0, 1)) { return 0.0; } if (!clip_to_plane(0, 1, 0, 1)) { return 0.0; } if (!clip_to_plane(0, 0, -1, 1)) { return 0.0; } if (!clip_to_plane(0, 0, 1, 1)) { return 0.0; } // perspective divide _pts_final.resize(_pts_in.size()); for (int n = 0; n < _pts_in.size(); n++) { _pts_final[n] = _pts_in[n].normal / _pts_in[n].d; } return Geometry::find_polygon_area(&_pts_final[0], _pts_final.size()); } bool PortalOcclusionCuller::Clipper::is_inside(const Plane &p_pt, Boundary p_boundary) { real_t w = p_pt.d; switch (p_boundary) { case B_LEFT: { return p_pt.normal.x > -w; } break; case B_RIGHT: { return p_pt.normal.x < w; } break; case B_TOP: { return p_pt.normal.y < w; } break; case B_BOTTOM: { return p_pt.normal.y > -w; } break; case B_NEAR: { return p_pt.normal.z < w; } break; case B_FAR: { return p_pt.normal.z > -w; } break; default: break; } return false; } // a is out, b is in Plane PortalOcclusionCuller::Clipper::intersect(const Plane &p_a, const Plane &p_b, Boundary p_boundary) { Plane diff_plane(p_b.normal - p_a.normal, p_b.d - p_a.d); const Vector3 &diff = diff_plane.normal; real_t t = 0.0; const real_t epsilon = 0.001f; // prevent divide by zero switch (p_boundary) { case B_LEFT: { if (diff.x > epsilon) { t = (-1.0f - p_a.normal.x) / diff.x; } } break; case B_RIGHT: { if (-diff.x > epsilon) { t = (p_a.normal.x - 1.0f) / -diff.x; } } break; case B_TOP: { if (-diff.y > epsilon) { t = (p_a.normal.y - 1.0f) / -diff.y; } } break; case B_BOTTOM: { if (diff.y > epsilon) { t = (-1.0f - p_a.normal.y) / diff.y; } } break; case B_NEAR: { if (-diff.z > epsilon) { t = (p_a.normal.z - 1.0f) / -diff.z; } } break; case B_FAR: { if (diff.z > epsilon) { t = (-1.0f - p_a.normal.z) / diff.z; } } break; default: break; } diff_plane.normal *= t; diff_plane.d *= t; return Plane(p_a.normal + diff_plane.normal, p_a.d + diff_plane.d); } // Clip the poly to the plane given by the formula a * x + b * y + c * z + d * w. bool PortalOcclusionCuller::Clipper::clip_to_plane(real_t a, real_t b, real_t c, real_t d) { _pts_out.clear(); // repeat the first _pts_in.push_back(_pts_in[0]); Plane vPrev = _pts_in[0]; real_t dpPrev = a * vPrev.normal.x + b * vPrev.normal.y + c * vPrev.normal.z + d * vPrev.d; for (int i = 1; i < _pts_in.size(); ++i) { Plane v = _pts_in[i]; real_t dp = a * v.normal.x + b * v.normal.y + c * v.normal.z + d * v.d; if (dpPrev >= 0) { _pts_out.push_back(vPrev); } if (sgn(dp) != sgn(dpPrev)) { real_t t = dp < 0 ? dpPrev / (dpPrev - dp) : -dpPrev / (dp - dpPrev); Plane vOut = interpolate(vPrev, v, t); _pts_out.push_back(vOut); } vPrev = v; dpPrev = dp; } // start again from the output points next time _pts_in = _pts_out; return _pts_in.size() > 2; } Geometry::MeshData PortalOcclusionCuller::debug_get_current_polys() const { Geometry::MeshData md; for (int n = 0; n < _num_polys; n++) { const Occlusion::PolyPlane &p = _polys[n].poly; int first_index = md.vertices.size(); Vector3 normal_push = p.plane.normal * 0.001f; // copy verts for (int c = 0; c < p.num_verts; c++) { md.vertices.push_back(p.verts[c] + normal_push); } // indices Geometry::MeshData::Face face; // triangle fan face.indices.resize(p.num_verts); for (int c = 0; c < p.num_verts; c++) { face.indices.set(c, first_index + c); } md.faces.push_back(face); } return md; } void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, const LocalVector &p_occluder_pool_ids, const Vector3 &pt_camera, const LocalVector &p_planes) { _portal_renderer = &p_portal_renderer; // Bodge to keep settings up to date, until the project settings PR is merged #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint() && ((Engine::get_singleton()->get_frames_drawn() % 16) == 0)) { _max_polys = GLOBAL_GET("rendering/misc/occlusion_culling/max_active_polygons"); } #endif _num_spheres = 0; _pt_camera = pt_camera; // spheres _num_spheres = 0; real_t goodness_of_fit_sphere[MAX_SPHERES]; for (int n = 0; n < _max_spheres; n++) { goodness_of_fit_sphere[n] = 0.0f; } real_t weakest_fit_sphere = FLT_MAX; int weakest_sphere = 0; _sphere_closest_dist = FLT_MAX; // polys _num_polys = 0; for (int n = 0; n < _max_polys; n++) { _polys[n].goodness_of_fit = 0.0f; } real_t weakest_fit_poly = FLT_MAX; int weakest_poly_id = 0; #ifdef TOOLS_ENABLED uint32_t polycount = 0; #endif const PortalResources &resources = RSG::scene->get_portal_resources(); // find occluders for (unsigned int o = 0; o < p_occluder_pool_ids.size(); o++) { int id = p_occluder_pool_ids[o]; VSOccluder_Instance &occ = p_portal_renderer.get_pool_occluder_instance(id); // is it active? // in the case of rooms, they will always be active, as inactive // are removed from rooms. But for whole scene mode, some may be inactive. if (!occ.active) { continue; } // TODO : occlusion cull spheres AGAINST themselves. // i.e. a sphere that is occluded by another occluder is no // use as an occluder... if (occ.type == VSOccluder_Instance::OT_SPHERE) { // make sure world space spheres are up to date p_portal_renderer.occluder_ensure_up_to_date_sphere(resources, occ); // cull entire AABB if (is_aabb_culled(occ.aabb, p_planes)) { continue; } // multiple spheres for (int n = 0; n < occ.list_ids.size(); n++) { const Occlusion::Sphere &occluder_sphere = p_portal_renderer.get_pool_occluder_world_sphere(occ.list_ids[n]); // is the occluder sphere culled? if (is_sphere_culled(occluder_sphere.pos, occluder_sphere.radius, p_planes)) { continue; } real_t dist = (occluder_sphere.pos - pt_camera).length(); // calculate the goodness of fit .. smaller distance better, and larger radius // calculate adjusted radius at 100.0 real_t fit = 100 / MAX(dist, 0.01f); fit *= occluder_sphere.radius; // until we reach the max, just keep recording, and keep track // of the worst fit if (_num_spheres < _max_spheres) { _spheres[_num_spheres] = occluder_sphere; _sphere_distances[_num_spheres] = dist; goodness_of_fit_sphere[_num_spheres] = fit; if (fit < weakest_fit_sphere) { weakest_fit_sphere = fit; weakest_sphere = _num_spheres; } // keep a record of the closest sphere for quick rejects if (dist < _sphere_closest_dist) { _sphere_closest_dist = dist; } _num_spheres++; } else { // must beat the weakest if (fit > weakest_fit_sphere) { _spheres[weakest_sphere] = occluder_sphere; _sphere_distances[weakest_sphere] = dist; goodness_of_fit_sphere[weakest_sphere] = fit; // keep a record of the closest sphere for quick rejects if (dist < _sphere_closest_dist) { _sphere_closest_dist = dist; } // the weakest may have changed (this could be done more efficiently) weakest_fit_sphere = FLT_MAX; for (int s = 0; s < _max_spheres; s++) { if (goodness_of_fit_sphere[s] < weakest_fit_sphere) { weakest_fit_sphere = goodness_of_fit_sphere[s]; weakest_sphere = s; } } } } } } // sphere if (occ.type == VSOccluder_Instance::OT_MESH) { // make sure world space spheres are up to date p_portal_renderer.occluder_ensure_up_to_date_polys(resources, occ); // multiple polys for (int n = 0; n < occ.list_ids.size(); n++) { const VSOccluder_Poly &opoly = p_portal_renderer.get_pool_occluder_world_poly(occ.list_ids[n]); const Occlusion::PolyPlane &poly = opoly.poly; // backface cull bool faces_camera = poly.plane.is_point_over(pt_camera); if (!faces_camera && !opoly.two_way) { continue; } real_t fit; if (!calculate_poly_goodness_of_fit(opoly, fit)) { continue; } if (_num_polys < _max_polys) { SortPoly &dest = _polys[_num_polys]; dest.poly = poly; dest.flags = faces_camera ? SortPoly::SPF_FACES_CAMERA : 0; if (opoly.num_holes) { dest.flags |= SortPoly::SPF_HAS_HOLES; } #ifdef TOOLS_ENABLED dest.poly_source_id = polycount++; #endif dest.mesh_source_id = occ.list_ids[n]; dest.goodness_of_fit = fit; if (fit < weakest_fit_poly) { weakest_fit_poly = fit; weakest_poly_id = _num_polys; } _num_polys++; } else { // must beat the weakest if (fit > weakest_fit_poly) { SortPoly &dest = _polys[weakest_poly_id]; dest.poly = poly; //dest.faces_camera = faces_camera; dest.flags = faces_camera ? SortPoly::SPF_FACES_CAMERA : 0; if (opoly.num_holes) { dest.flags |= SortPoly::SPF_HAS_HOLES; } #ifdef TOOLS_ENABLED dest.poly_source_id = polycount++; #endif dest.mesh_source_id = occ.list_ids[n]; dest.goodness_of_fit = fit; // the weakest may have changed (this could be done more efficiently) weakest_fit_poly = FLT_MAX; for (int p = 0; p < _max_polys; p++) { real_t goodness_of_fit = _polys[p].goodness_of_fit; if (goodness_of_fit < weakest_fit_poly) { weakest_fit_poly = goodness_of_fit; weakest_poly_id = p; } } } } // polys full up, replace } } } // for o precalc_poly_edge_planes(pt_camera); // flip polys so always facing camera for (int n = 0; n < _num_polys; n++) { if (!(_polys[n].flags & SortPoly::SPF_FACES_CAMERA)) { _polys[n].poly.flip(); // must flip holes and planes too _precalced_poly[n].flip(); } } // cull polys against each other. whittle_polys(); // checksum is used only in the editor, to decide // whether to redraw the gizmo of active polys #ifdef TOOLS_ENABLED uint32_t last_checksum = _poly_checksum; _poly_checksum = 0; for (int n = 0; n < _num_polys; n++) { _poly_checksum += _polys[n].poly_source_id; //_log_prepare("prepfinal : " + itos(_polys[n].poly_source_id) + " fit : " + rtos(_polys[n].goodness_of_fit)); } if (_poly_checksum != last_checksum) { _redraw_gizmo = true; } #endif // force the sphere closest distance to above zero to prevent // divide by zero in the quick reject _sphere_closest_dist = MAX(_sphere_closest_dist, 0.001); // sphere self occlusion. // we could avoid testing the closest sphere, but the complexity isn't worth any speed benefit for (int n = 0; n < _num_spheres; n++) { const Occlusion::Sphere &sphere = _spheres[n]; // is it occluded by another sphere? if (cull_sphere(sphere.pos, sphere.radius, n)) { // yes, unordered remove _num_spheres--; _spheres[n] = _spheres[_num_spheres]; _sphere_distances[n] = _sphere_distances[_num_spheres]; // repeat this n n--; } } // record whether to do any occlusion culling at all.. _occluders_present = _num_spheres || _num_polys; } void PortalOcclusionCuller::precalc_poly_edge_planes(const Vector3 &p_pt_camera) { for (int n = 0; n < _num_polys; n++) { const SortPoly &sortpoly = _polys[n]; const Occlusion::PolyPlane &spoly = sortpoly.poly; PreCalcedPoly &dpoly = _precalced_poly[n]; dpoly.edge_planes.num_planes = spoly.num_verts; for (int e = 0; e < spoly.num_verts; e++) { // point a and b of the edge const Vector3 &pt_a = spoly.verts[e]; const Vector3 &pt_b = spoly.verts[(e + 1) % spoly.num_verts]; // edge plane to camera dpoly.edge_planes.planes[e] = Plane(p_pt_camera, pt_a, pt_b); } dpoly.num_holes = 0; // holes if (sortpoly.flags & SortPoly::SPF_HAS_HOLES) { // get the mesh poly and the holes const VSOccluder_Poly &mesh = _portal_renderer->get_pool_occluder_world_poly(sortpoly.mesh_source_id); dpoly.num_holes = mesh.num_holes; for (int h = 0; h < mesh.num_holes; h++) { uint32_t hid = mesh.hole_pool_ids[h]; const VSOccluder_Hole &hole = _portal_renderer->get_pool_occluder_world_hole(hid); // copy the verts to the precalced poly, // we will need these later for whittling polys. // We could alternatively link back to the original verts, but that gets messy. dpoly.hole_polys[h] = hole; int hole_num_verts = hole.num_verts; const Vector3 *hverts = hole.verts; // number of planes equals number of verts forming edges dpoly.hole_edge_planes[h].num_planes = hole_num_verts; for (int e = 0; e < hole_num_verts; e++) { const Vector3 &pt_a = hverts[e]; const Vector3 &pt_b = hverts[(e + 1) % hole_num_verts]; dpoly.hole_edge_planes[h].planes[e] = Plane(p_pt_camera, pt_a, pt_b); } // for e } // for h } // if has holes } } void PortalOcclusionCuller::whittle_polys() { //#define PANDEMONIUM_OCCLUSION_FLASH_POLYS #ifdef PANDEMONIUM_OCCLUSION_FLASH_POLYS if (((Engine::get_singleton()->get_frames_drawn() / 4) % 2) == 0) { return; } #endif bool repeat = true; while (repeat) { repeat = false; // Check for complete occlusion of polys by a closer poly. // Such polys can be completely removed from checks. for (int n = 0; n < _num_polys; n++) { // ensure we test each occluder once and only once // (as this routine will repeat each time an occluded poly is found) SortPoly &sort_poly = _polys[n]; if (!(sort_poly.flags & SortPoly::SPF_TESTED_AS_OCCLUDER)) { sort_poly.flags |= SortPoly::SPF_TESTED_AS_OCCLUDER; } else { continue; } const Occlusion::PolyPlane &poly = _polys[n].poly; const Plane &occluder_plane = poly.plane; const PreCalcedPoly &pcp = _precalced_poly[n]; // the goodness of fit is the screen space area at the moment, // so we can use it as a quick reject .. polys behind occluders will always // be smaller area than the occluder. real_t occluder_area = _polys[n].goodness_of_fit; // check each other poly as an occludee for (int t = 0; t < _num_polys; t++) { if (n == t) { continue; } // quick reject based on screen space area. // if the area of the test poly is larger, it can't be completely behind // the occluder. bool quick_reject_entire_occludee = _polys[t].goodness_of_fit > occluder_area; const Occlusion::PolyPlane &test_poly = _polys[t].poly; PreCalcedPoly &pcp_test = _precalced_poly[t]; // We have two considerations: // (1) Entire poly is occluded // (2) If not (1), then maybe a hole is occluded bool completely_reject = false; if (!quick_reject_entire_occludee && is_poly_inside_occlusion_volume(test_poly, occluder_plane, pcp.edge_planes)) { completely_reject = true; // we must also test against all holes if some are present for (int h = 0; h < pcp.num_holes; h++) { if (is_poly_touching_hole(test_poly, pcp.hole_edge_planes[h])) { completely_reject = false; break; } } if (completely_reject) { // yes .. we can remove this poly .. but do not muck up the iteration of the list //print_line("poly is occluded " + itos(t)); #ifdef TOOLS_ENABLED // this condition should never happen, we should never be checking occludee against itself DEV_ASSERT(_polys[t].poly_source_id != _polys[n].poly_source_id); #endif // unordered remove _polys[t] = _polys[_num_polys - 1]; _precalced_poly[t] = _precalced_poly[_num_polys - 1]; _num_polys--; // no NOT repeat the test poly if it was copied from n, i.e. the occludee would // be the same as the occluder if (_num_polys != n) { // repeat this test poly as it will be the next t--; } // If we end up removing a poly BEFORE n, the replacement poly (from the unordered remove) // will never get tested as an occluder. So we have to account for this by rerunning the routine. repeat = true; } // allow due to holes } // if poly inside occlusion volume // if we did not completely reject, there could be holes that could be rejected if (!completely_reject) { if (pcp_test.num_holes) { for (int h = 0; h < pcp_test.num_holes; h++) { const Occlusion::Poly &hole_poly = pcp_test.hole_polys[h]; // is the hole within the occluder? if (is_poly_inside_occlusion_volume(hole_poly, occluder_plane, pcp.edge_planes)) { // if the hole touching a hole in the occluder? if so we can't eliminate it bool allow = true; for (int oh = 0; oh < pcp.num_holes; oh++) { if (is_poly_touching_hole(hole_poly, pcp.hole_edge_planes[oh])) { allow = false; break; } } if (allow) { // Unordered remove the hole. No need to repeat the whole while loop I don't think? // As this just makes it more efficient at runtime, it doesn't make the further whittling more accurate. pcp_test.num_holes--; pcp_test.hole_edge_planes[h] = pcp_test.hole_edge_planes[pcp_test.num_holes]; pcp_test.hole_polys[h] = pcp_test.hole_polys[pcp_test.num_holes]; h--; // repeat this as the unordered remove has placed a new member into h slot } // allow } // hole is within } } // has holes } // did not completely reject } // for t through occludees } // for n through occluders } // while repeat // order polys by distance to camera / area? NYI } bool PortalOcclusionCuller::calculate_poly_goodness_of_fit(const VSOccluder_Poly &p_opoly, real_t &r_fit) { // transform each of the poly points, find the area in screen space // The points must be homogeneous coordinates, i.e. BEFORE // the perspective divide, in clip space. They will have the perspective // divide applied after clipping, to calculate the area. // We therefore store them as planes to store the w coordinate as d. Plane xpoints[Occlusion::PolyPlane::MAX_POLY_VERTS]; int num_verts = p_opoly.poly.num_verts; for (int n = 0; n < num_verts; n++) { // source and dest in homogeneous coords Plane source(p_opoly.poly.verts[n], 1.0f); Plane &dest = xpoints[n]; dest = _matrix_camera.xform(source); } // find screen space area real_t area = _clipper.clip_and_find_poly_area(xpoints, num_verts); if (area <= 0.0f) { return false; } r_fit = area; return true; } bool PortalOcclusionCuller::_is_poly_of_interest_to_split_plane(const Plane *p_poly_split_plane, int p_poly_id) const { const Occlusion::PolyPlane &poly = _polys[p_poly_id].poly; int over = 0; int under = 0; // we need an epsilon because adjacent polys that just // join with a wall may have small floating point error ahead // of the splitting plane. const real_t epsilon = 0.005f; for (int n = 0; n < poly.num_verts; n++) { // point a and b of the edge const Vector3 &pt = poly.verts[n]; real_t dist = p_poly_split_plane->distance_to(pt); if (dist > epsilon) { over++; } else { under++; } } // return whether straddles the plane return over && under; } bool PortalOcclusionCuller::cull_aabb_to_polys_ex(const AABB &p_aabb) const { _log("\n", 0); _log("* cull_aabb_to_polys_ex " + String(Variant(p_aabb)), 0); Plane plane; for (int n = 0; n < _num_polys; n++) { _log("\tchecking poly " + itos(n), 0); const SortPoly &sortpoly = _polys[n]; const Occlusion::PolyPlane &poly = sortpoly.poly; // occludee must be on opposite side to camera real_t omin, omax; p_aabb.project_range_in_plane(poly.plane, omin, omax); if (omax > -0.2f) { _log("\t\tAABB is in front of occluder, ignoring", 0); continue; } // test against each edge of the poly, and expand the edge bool hit = true; const PreCalcedPoly &pcp = _precalced_poly[n]; for (int e = 0; e < pcp.edge_planes.num_planes; e++) { // edge plane to camera plane = pcp.edge_planes.planes[e]; p_aabb.project_range_in_plane(plane, omin, omax); if (omax > 0.0f) { hit = false; break; } } // if it hit, check against holes if (hit && pcp.num_holes) { for (int h = 0; h < pcp.num_holes; h++) { const PlaneSet &hole = pcp.hole_edge_planes[h]; // if the AABB is totally outside any edge, it is safe for a hit bool safe = false; for (int e = 0; e < hole.num_planes; e++) { // edge plane to camera plane = hole.planes[e]; p_aabb.project_range_in_plane(plane, omin, omax); // if inside the hole, no longer a hit on this poly if (omin > 0.0f) { safe = true; break; } } // for e if (!safe) { hit = false; } if (!hit) { break; } } // for h } // if has holes // hit? if (hit) { return true; } } _log("\tno hit", 0); return false; } bool PortalOcclusionCuller::cull_aabb_to_polys(const AABB &p_aabb) const { if (!_num_polys) { return false; } return cull_aabb_to_polys_ex(p_aabb); } bool PortalOcclusionCuller::cull_sphere_to_polys(const Vector3 &p_occludee_center, real_t p_occludee_radius) const { if (!_num_polys) { return false; } Plane plane; for (int n = 0; n < _num_polys; n++) { const Occlusion::PolyPlane &poly = _polys[n].poly; // test against each edge of the poly, and expand the edge bool hit = true; // occludee must be on opposite side to camera real_t dist = poly.plane.distance_to(p_occludee_center); if (dist > -p_occludee_radius) { continue; } for (int e = 0; e < poly.num_verts; e++) { plane = Plane(_pt_camera, poly.verts[e], poly.verts[(e + 1) % poly.num_verts]); // de-expand plane.d -= p_occludee_radius; if (plane.is_point_over(p_occludee_center)) { hit = false; break; } } // hit? if (hit) { return true; } } return false; } bool PortalOcclusionCuller::cull_sphere_to_spheres(const Vector3 &p_occludee_center, real_t p_occludee_radius, const Vector3 &p_ray_dir, real_t p_dist_to_occludee, int p_ignore_sphere) const { // maybe not required if (!_num_spheres) { return false; } // prevent divide by zero, and the occludee cannot be occluded if we are WITHIN // its bounding sphere... so no need to check if (p_dist_to_occludee < _sphere_closest_dist) { return false; } // this can probably be done cheaper with dot products but the math might be a bit fiddly to get right for (int s = 0; s < _num_spheres; s++) { // first get the sphere distance real_t occluder_dist_to_cam = _sphere_distances[s]; if (p_dist_to_occludee < occluder_dist_to_cam) { // can't occlude continue; } // the perspective adjusted occludee radius real_t adjusted_occludee_radius = p_occludee_radius * (occluder_dist_to_cam / p_dist_to_occludee); const Occlusion::Sphere &occluder_sphere = _spheres[s]; real_t occluder_radius = occluder_sphere.radius - adjusted_occludee_radius; if (occluder_radius > 0.0) { occluder_radius = occluder_radius * occluder_radius; // distance to hit real_t dist; if (occluder_sphere.intersect_ray(_pt_camera, p_ray_dir, dist, occluder_radius)) { if ((dist < p_dist_to_occludee) && (s != p_ignore_sphere)) { // occluded return true; } } } // expanded occluder radius is more than 0 } return false; } bool PortalOcclusionCuller::cull_sphere(const Vector3 &p_occludee_center, real_t p_occludee_radius, int p_ignore_sphere, bool p_cull_to_polys) const { if (!_occluders_present) { return false; } // ray from origin to the occludee Vector3 ray_dir = p_occludee_center - _pt_camera; real_t dist_to_occludee_raw = ray_dir.length(); // account for occludee radius real_t dist_to_occludee = dist_to_occludee_raw - p_occludee_radius; // ignore occlusion for closeup, and avoid divide by zero if (dist_to_occludee_raw < 0.1) { return false; } // normalize ray // hopefully by this point, dist_to_occludee_raw cannot possibly be zero due to above check ray_dir *= 1.0 / dist_to_occludee_raw; if (cull_sphere_to_spheres(p_occludee_center, p_occludee_radius, ray_dir, dist_to_occludee, p_ignore_sphere)) { return true; } if (p_cull_to_polys && cull_sphere_to_polys(p_occludee_center, p_occludee_radius)) { return true; } return false; } PortalOcclusionCuller::PortalOcclusionCuller() { _max_spheres = GLOBAL_GET("rendering/misc/occlusion_culling/max_active_spheres"); _max_polys = GLOBAL_GET("rendering/misc/occlusion_culling/max_active_polygons"); } void PortalOcclusionCuller::log(String p_string, int p_depth) const { if (_debug_log) { for (int n = 0; n < p_depth; n++) { p_string = "\t\t\t" + p_string; } print_line(p_string); } } #undef _log #undef _log_prepare