diff --git a/core/local_vector.h b/core/local_vector.h index e83d23f52cfc..b5fe6999f75f 100644 --- a/core/local_vector.h +++ b/core/local_vector.h @@ -38,7 +38,7 @@ template class LocalVector { -private: +protected: U count = 0; U capacity = 0; T *data = nullptr; @@ -255,4 +255,9 @@ class LocalVector { } }; +// Integer default version +template +class LocalVectori : public LocalVector { +}; + #endif // LOCAL_VECTOR_H diff --git a/core/math/geometry.cpp b/core/math/geometry.cpp index 8707b9e91cc0..6fc64428f96a 100644 --- a/core/math/geometry.cpp +++ b/core/math/geometry.cpp @@ -30,6 +30,7 @@ #include "geometry.h" +#include "core/local_vector.h" #include "core/print_string.h" #include "thirdparty/misc/clipper.hpp" @@ -53,6 +54,17 @@ bool Geometry::is_point_in_polygon(const Vector2 &p_point, const Vector } */ +void Geometry::OccluderMeshData::clear() { + faces.clear(); + vertices.clear(); +} + +void Geometry::MeshData::clear() { + faces.clear(); + edges.clear(); + vertices.clear(); +} + void Geometry::MeshData::optimize_vertices() { Map vtx_remap; @@ -1363,6 +1375,28 @@ Vector Geometry::partial_pack_rects(const Vector &r_verts, bool p_clockwise) { diff --git a/core/math/geometry.h b/core/math/geometry.h index c9ae9f20076f..6739f02a7b54 100644 --- a/core/math/geometry.h +++ b/core/math/geometry.h @@ -555,11 +555,17 @@ class Geometry { double dot11 = v1.dot(v1); double dot12 = v1.dot(v2); + // Check for divide by zero + double denom = dot00 * dot11 - dot01 * dot01; + if (denom == 0.0) { + return Vector3(0.0, 0.0, 0.0); + } + // Compute barycentric coordinates - double invDenom = 1.0f / (dot00 * dot11 - dot01 * dot01); + double invDenom = 1.0 / denom; double b2 = (dot11 * dot02 - dot01 * dot12) * invDenom; double b1 = (dot00 * dot12 - dot01 * dot02) * invDenom; - double b0 = 1.0f - b2 - b1; + double b0 = 1.0 - b2 - b1; return Vector3(b0, b1, b2); } @@ -978,6 +984,24 @@ class Geometry { Vector vertices; void optimize_vertices(); + void clear(); + }; + + // Occluder Meshes contain convex faces which may contain 0 to many convex holes. + // (holes are analogous to portals) + struct OccluderMeshData { + struct Hole { + LocalVectori indices; + }; + struct Face { + Plane plane; + bool two_way = false; + LocalVectori indices; + LocalVectori holes; + }; + LocalVectori faces; + LocalVectori vertices; + void clear(); }; _FORCE_INLINE_ static int get_uv84_normal_bit(const Vector3 &p_vector) { @@ -1070,6 +1094,7 @@ class Geometry { static PoolVector build_cylinder_planes(real_t p_radius, real_t p_height, int p_sides, Vector3::Axis p_axis = Vector3::AXIS_Z); static PoolVector build_capsule_planes(real_t p_radius, real_t p_height, int p_sides, int p_lats, Vector3::Axis p_axis = Vector3::AXIS_Z); static void sort_polygon_winding(Vector &r_verts, bool p_clockwise = true); + static real_t find_polygon_area(const Vector3 *p_verts, int p_num_verts); static void make_atlas(const Vector &p_rects, Vector &r_result, Size2i &r_size); diff --git a/core/math/math_funcs.h b/core/math/math_funcs.h index 54ad852163da..5edae8f3e2d3 100644 --- a/core/math/math_funcs.h +++ b/core/math/math_funcs.h @@ -181,6 +181,7 @@ class Math { static _ALWAYS_INLINE_ double abs(double g) { return absd(g); } static _ALWAYS_INLINE_ float abs(float g) { return absf(g); } static _ALWAYS_INLINE_ int abs(int g) { return g > 0 ? g : -g; } + static _ALWAYS_INLINE_ int64_t abs(int64_t g) { return g > 0 ? g : -g; } static _ALWAYS_INLINE_ double fposmod(double p_x, double p_y) { double value = Math::fmod(p_x, p_y); diff --git a/doc/classes/OccluderShapePolygon.xml b/doc/classes/OccluderShapePolygon.xml new file mode 100644 index 000000000000..ad903a559805 --- /dev/null +++ b/doc/classes/OccluderShapePolygon.xml @@ -0,0 +1,45 @@ + + + + Polygon occlusion primitive for use with the [Occluder] node. + + + [OccluderShape]s are resources used by [Occluder] nodes, allowing geometric occlusion culling. + The polygon must be a convex polygon. The polygon points can be created and deleted either in the Editor inspector or by calling [code]set_polygon_points[/code]. The points of the edges can be set by dragging the handles in the Editor viewport. + Additionally each polygon occluder can optionally support a single hole. If you add at least three points in the Editor inspector to the hole, you can drag the edge points of the hole in the Editor viewport. + In general, the lower the number of edges in polygons and holes, the faster the system will operate at runtime, so in most cases you will want to use 4 points for each. + + + + + + + + + + Sets an individual hole point position. + + + + + + + + Sets an individual polygon point position. + + + + + + Allows changing the hole geometry from code. + + + Allows changing the polygon geometry from code. + + + Specifies whether the occluder should operate one way only, or from both sides. + + + + + diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 0e5b8d38bf76..3cbaedd5616c 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1330,6 +1330,11 @@ On import, mesh vertex data will be split into two streams within a single vertex buffer, one for position data and the other for interleaved attributes data. Recommended to be enabled if targeting mobile devices. Requires manual reimport of meshes after toggling. + + Determines the maximum number of polygon occluders that will be used at any one time. + Although you can have many occluders in a scene, each frame the system will choose from these the most relevant based on a screen space metric, in order to give the best overall performance. + A greater number of polygons can potentially cull more objects, however the cost of culling calculations scales with the number of occluders. + Determines the maximum number of sphere occluders that will be used at any one time. Although you can have many occluders in a scene, each frame the system will choose from these the most relevant based on a screen space metric, in order to give the best overall performance. diff --git a/editor/plugins/spatial_editor_plugin.cpp b/editor/plugins/spatial_editor_plugin.cpp index ad8987ed2f1d..0605b7f3b993 100644 --- a/editor/plugins/spatial_editor_plugin.cpp +++ b/editor/plugins/spatial_editor_plugin.cpp @@ -3674,7 +3674,11 @@ AABB SpatialEditorViewport::_calculate_spatial_bounds(const Spatial *p_parent, b } if (bounds.size == Vector3() && p_parent->get_class_name() != StringName("Spatial")) { +#ifdef TOOLS_ENABLED + bounds = p_parent->get_fallback_gizmo_aabb(); +#else bounds = AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4)); +#endif } if (!p_exclude_toplevel_transform) { diff --git a/editor/spatial_editor_gizmos.cpp b/editor/spatial_editor_gizmos.cpp index 9e131dcc5680..44fb4e9a4ac1 100644 --- a/editor/spatial_editor_gizmos.cpp +++ b/editor/spatial_editor_gizmos.cpp @@ -62,6 +62,7 @@ #include "scene/resources/cylinder_shape.h" #include "scene/resources/height_map_shape.h" #include "scene/resources/occluder_shape.h" +#include "scene/resources/occluder_shape_polygon.h" #include "scene/resources/plane_shape.h" #include "scene/resources/primitive_meshes.h" #include "scene/resources/ray_shape.h" @@ -5000,6 +5001,8 @@ OccluderGizmoPlugin::OccluderGizmoPlugin() { Color color_occluder = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/occluder", Color(1.0, 0.0, 1.0)); create_material("occluder", color_occluder, false, true, false); + create_material("occluder_poly", Color(1, 1, 1, 1), false, false, true); + create_handle_material("occluder_handle"); create_handle_material("extra_handle", false, SpatialEditor::get_singleton()->get_icon("EditorInternalHandle", "EditorIcons")); } @@ -5046,6 +5049,15 @@ String OccluderSpatialGizmo::get_handle_name(int p_idx) const { } } + const OccluderShapePolygon *occ_poly = get_occluder_shape_poly(); + if (occ_poly) { + if (p_idx < occ_poly->_poly_pts_local_raw.size()) { + return "Poly Point " + itos(p_idx); + } else { + return "Hole Point " + itos(p_idx - occ_poly->_poly_pts_local_raw.size()); + } + } + return "Unknown"; } @@ -5063,6 +5075,19 @@ Variant OccluderSpatialGizmo::get_handle_value(int p_idx) { } } + const OccluderShapePolygon *occ_poly = get_occluder_shape_poly(); + if (occ_poly) { + if (p_idx < occ_poly->_poly_pts_local_raw.size()) { + return occ_poly->_poly_pts_local_raw[p_idx]; + } else { + p_idx -= occ_poly->_poly_pts_local_raw.size(); + if (p_idx < occ_poly->_hole_pts_local_raw.size()) { + return occ_poly->_hole_pts_local_raw[p_idx]; + } + return Vector2(0, 0); + } + } + return 0; } @@ -5145,16 +5170,63 @@ void OccluderSpatialGizmo::set_handle(int p_idx, Camera *p_camera, const Point2 return; } } + + OccluderShapePolygon *occ_poly = get_occluder_shape_poly(); + if (occ_poly) { + Vector3 pt_local; + + bool hole = p_idx >= occ_poly->_poly_pts_local_raw.size(); + if (hole) { + p_idx -= occ_poly->_poly_pts_local_raw.size(); + if (p_idx >= occ_poly->_hole_pts_local_raw.size()) { + return; + } + pt_local = OccluderShapePolygon::_vec2to3(occ_poly->_hole_pts_local_raw[p_idx]); + } else { + pt_local = OccluderShapePolygon::_vec2to3(occ_poly->_poly_pts_local_raw[p_idx]); + } + + Vector3 pt_world = tr.xform(pt_local); + + // get a normal from the global transform + Plane plane(Vector3(0, 0, 0), Vector3(0, 0, 1)); + plane = tr.xform(plane); + + // construct the plane that the 2d portal is defined in + plane = Plane(pt_world, plane.normal); + + Vector3 inters; + + if (plane.intersects_ray(ray_from, ray_dir, &inters)) { + // back calculate from the 3d intersection to the 2d portal plane + inters = tr_inv.xform(inters); + + // snapping will be in 2d for portals, and the scale may make less sense, + // but better to offer at least some functionality + if (SpatialEditor::get_singleton()->is_snap_enabled()) { + float snap = SpatialEditor::get_singleton()->get_translate_snap(); + inters.snap(Vector3(snap, snap, snap)); + } + + if (hole) { + occ_poly->set_hole_point(p_idx, Vector2(inters.x, inters.y)); + } else { + occ_poly->set_polygon_point(p_idx, Vector2(inters.x, inters.y)); + } + + return; + } + } } void OccluderSpatialGizmo::commit_handle(int p_idx, const Variant &p_restore, bool p_cancel) { + UndoRedo *ur = SpatialEditor::get_singleton()->get_undo_redo(); + OccluderShapeSphere *occ_sphere = get_occluder_shape_sphere(); if (occ_sphere) { Vector spheres = occ_sphere->get_spheres(); int num_spheres = spheres.size(); - UndoRedo *ur = SpatialEditor::get_singleton()->get_undo_redo(); - if (p_idx >= num_spheres) { p_idx -= num_spheres; @@ -5170,9 +5242,49 @@ void OccluderSpatialGizmo::commit_handle(int p_idx, const Variant &p_restore, bo ur->commit_action(); _occluder->property_list_changed_notify(); } + + OccluderShapePolygon *occ_poly = get_occluder_shape_poly(); + if (occ_poly) { + if (p_idx < occ_poly->_poly_pts_local_raw.size()) { + ur->create_action(TTR("Set Occluder Polygon Point Position")); + ur->add_do_method(occ_poly, "set_polygon_point", p_idx, occ_poly->_poly_pts_local_raw[p_idx]); + ur->add_undo_method(occ_poly, "set_polygon_point", p_idx, p_restore); + ur->commit_action(); + _occluder->property_list_changed_notify(); + } else { + p_idx -= occ_poly->_poly_pts_local_raw.size(); + if (p_idx < occ_poly->_hole_pts_local_raw.size()) { + ur->create_action(TTR("Set Occluder Hole Point Position")); + ur->add_do_method(occ_poly, "set_hole_point", p_idx, occ_poly->_hole_pts_local_raw[p_idx]); + ur->add_undo_method(occ_poly, "set_hole_point", p_idx, p_restore); + ur->commit_action(); + _occluder->property_list_changed_notify(); + } + } + } } OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() { + OccluderShapeSphere *occ_sphere = Object::cast_to(get_occluder_shape()); + return occ_sphere; +} + +const OccluderShapePolygon *OccluderSpatialGizmo::get_occluder_shape_poly() const { + const OccluderShapePolygon *occ_poly = Object::cast_to(get_occluder_shape()); + return occ_poly; +} + +OccluderShapePolygon *OccluderSpatialGizmo::get_occluder_shape_poly() { + OccluderShapePolygon *occ_poly = Object::cast_to(get_occluder_shape()); + return occ_poly; +} + +const OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() const { + const OccluderShapeSphere *occ_sphere = Object::cast_to(get_occluder_shape()); + return occ_sphere; +} + +const OccluderShape *OccluderSpatialGizmo::get_occluder_shape() const { if (!_occluder) { return nullptr; } @@ -5182,12 +5294,10 @@ OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() { return nullptr; } - OccluderShape *shape = rshape.ptr(); - OccluderShapeSphere *occ_sphere = Object::cast_to(shape); - return occ_sphere; + return rshape.ptr(); } -const OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() const { +OccluderShape *OccluderSpatialGizmo::get_occluder_shape() { if (!_occluder) { return nullptr; } @@ -5197,9 +5307,7 @@ const OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() con return nullptr; } - const OccluderShape *shape = rshape.ptr(); - const OccluderShapeSphere *occ_sphere = Object::cast_to(shape); - return occ_sphere; + return rshape.ptr(); } void OccluderSpatialGizmo::redraw() { @@ -5258,9 +5366,90 @@ void OccluderSpatialGizmo::redraw() { add_handles(handles, material_handle); add_handles(radius_handles, material_extra_handle, false, true); } + + const OccluderShapePolygon *occ_poly = get_occluder_shape_poly(); + if (occ_poly) { + // main poly + _redraw_poly(false, occ_poly->_poly_pts_local, occ_poly->_poly_pts_local_raw); + + // hole + _redraw_poly(true, occ_poly->_hole_pts_local, occ_poly->_hole_pts_local_raw); + } +} + +void OccluderSpatialGizmo::_redraw_poly(bool p_hole, const Vector &p_pts, const PoolVector &p_pts_raw) { + PoolVector pts_edge; + PoolVector cols; + + Color col_front = _color_poly_front; + Color col_back = _color_poly_back; + + if (p_hole) { + col_front = _color_hole; + col_back = _color_hole; + } + + if (p_pts.size() > 2) { + Vector3 pt_first = OccluderShapePolygon::_vec2to3(p_pts[0]); + Vector3 pt_prev = OccluderShapePolygon::_vec2to3(p_pts[p_pts.size() - 1]); + for (int n = 0; n < p_pts.size(); n++) { + Vector3 pt_curr = OccluderShapePolygon::_vec2to3(p_pts[n]); + pts_edge.push_back(pt_first); + pts_edge.push_back(pt_prev); + pts_edge.push_back(pt_curr); + cols.push_back(col_front); + cols.push_back(col_front); + cols.push_back(col_front); + + pts_edge.push_back(pt_first); + pts_edge.push_back(pt_curr); + pts_edge.push_back(pt_prev); + cols.push_back(col_back); + cols.push_back(col_back); + cols.push_back(col_back); + + pt_prev = pt_curr; + } + } + + // draw the handles separately because these must correspond to the raw points + // for editing + Vector handles; + for (int n = 0; n < p_pts_raw.size(); n++) { + Vector3 pt = OccluderShapePolygon::_vec2to3(p_pts_raw[n]); + handles.push_back(pt); + } + + // poly itself + { + if (pts_edge.size() > 2) { + Ref mesh = memnew(ArrayMesh); + Array array; + array.resize(Mesh::ARRAY_MAX); + array[Mesh::ARRAY_VERTEX] = pts_edge; + array[Mesh::ARRAY_COLOR] = cols; + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, array); + + Ref material_poly = gizmo_plugin->get_material("occluder_poly", this); + add_mesh(mesh, false, Ref(), material_poly); + } + + // handles + if (!p_hole) { + Ref material_handle = gizmo_plugin->get_material("occluder_handle", this); + add_handles(handles, material_handle); + } else { + Ref material_extra_handle = gizmo_plugin->get_material("extra_handle", this); + add_handles(handles, material_extra_handle, false, true); + } + } } OccluderSpatialGizmo::OccluderSpatialGizmo(Occluder *p_occluder) { _occluder = p_occluder; set_spatial_node(p_occluder); + + _color_poly_front = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/occluder_polygon_front", Color(1.0, 0.25, 0.8, 0.3)); + _color_poly_back = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/occluder_polygon_back", Color(0.85, 0.1, 1.0, 0.3)); + _color_hole = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/occluder_hole", Color(0.0, 1.0, 1.0, 0.3)); } diff --git a/editor/spatial_editor_gizmos.h b/editor/spatial_editor_gizmos.h index 323b085fe68a..0eeef772cc6e 100644 --- a/editor/spatial_editor_gizmos.h +++ b/editor/spatial_editor_gizmos.h @@ -505,15 +505,27 @@ class PortalGizmoPlugin : public EditorSpatialGizmoPlugin { }; class Occluder; +class OccluderShape; class OccluderShapeSphere; +class OccluderShapePolygon; class OccluderSpatialGizmo : public EditorSpatialGizmo { GDCLASS(OccluderSpatialGizmo, EditorSpatialGizmo); Occluder *_occluder = nullptr; - OccluderShapeSphere *get_occluder_shape_sphere(); + const OccluderShape *get_occluder_shape() const; const OccluderShapeSphere *get_occluder_shape_sphere() const; + const OccluderShapePolygon *get_occluder_shape_poly() const; + OccluderShape *get_occluder_shape(); + OccluderShapeSphere *get_occluder_shape_sphere(); + OccluderShapePolygon *get_occluder_shape_poly(); + + Color _color_poly_front; + Color _color_poly_back; + Color _color_hole; + + void _redraw_poly(bool p_hole, const Vector &p_pts, const PoolVector &p_pts_raw); public: virtual String get_handle_name(int p_idx) const; diff --git a/scene/3d/occluder.cpp b/scene/3d/occluder.cpp index efd8ddd15c8b..148033ea4ecd 100644 --- a/scene/3d/occluder.cpp +++ b/scene/3d/occluder.cpp @@ -31,6 +31,7 @@ #include "occluder.h" #include "core/engine.h" +#include "servers/visual/portals/portal_occlusion_culler.h" void Occluder::resource_changed(RES res) { update_gizmo(); @@ -72,6 +73,15 @@ Ref Occluder::get_shape() const { return _shape; } +#ifdef TOOLS_ENABLED +AABB Occluder::get_fallback_gizmo_aabb() const { + if (_shape.is_valid()) { + return _shape->get_fallback_gizmo_aabb(); + } + return Spatial::get_fallback_gizmo_aabb(); +} +#endif + String Occluder::get_configuration_warning() const { String warning = Spatial::get_configuration_warning(); @@ -80,18 +90,23 @@ String Occluder::get_configuration_warning() const { warning += "\n\n"; } warning += TTR("No shape is set."); + return warning; } - Transform tr = get_global_transform(); - Vector3 scale = tr.basis.get_scale(); - - if ((!Math::is_equal_approx(scale.x, scale.y, 0.01f)) || - (!Math::is_equal_approx(scale.x, scale.z, 0.01f))) { - if (!warning.empty()) { - warning += "\n\n"; +#ifdef TOOLS_ENABLED + if (_shape.ptr()->requires_uniform_scale()) { + Transform tr = get_global_transform(); + Vector3 scale = tr.basis.get_scale(); + + if ((!Math::is_equal_approx(scale.x, scale.y, 0.01f)) || + (!Math::is_equal_approx(scale.x, scale.z, 0.01f))) { + if (!warning.empty()) { + warning += "\n\n"; + } + warning += TTR("Only uniform scales are supported."); } - warning += TTR("Only uniform scales are supported."); } +#endif return warning; } @@ -106,11 +121,21 @@ void Occluder::_notification(int p_what) { _shape->update_shape_to_visual_server(); _shape->update_transform_to_visual_server(get_global_transform()); } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + set_process_internal(true); + } +#endif } break; case NOTIFICATION_EXIT_WORLD: { if (_shape.is_valid()) { _shape->notification_exit_world(); } +#ifdef TOOLS_ENABLED + if (Engine::get_singleton()->is_editor_hint()) { + set_process_internal(false); + } +#endif } break; case NOTIFICATION_VISIBILITY_CHANGED: { if (_shape.is_valid() && is_inside_tree()) { @@ -128,6 +153,12 @@ void Occluder::_notification(int p_what) { #endif } } break; + case NOTIFICATION_INTERNAL_PROCESS: { + if (PortalOcclusionCuller::_redraw_gizmo) { + PortalOcclusionCuller::_redraw_gizmo = false; + update_gizmo(); + } + } break; } } diff --git a/scene/3d/occluder.h b/scene/3d/occluder.h index 6a0a08eac7aa..6b0d06522d76 100644 --- a/scene/3d/occluder.h +++ b/scene/3d/occluder.h @@ -54,6 +54,11 @@ class Occluder : public Spatial { String get_configuration_warning() const; +#ifdef TOOLS_ENABLED + // for editor gizmo + virtual AABB get_fallback_gizmo_aabb() const; +#endif + Occluder(); ~Occluder(); }; diff --git a/scene/3d/spatial.cpp b/scene/3d/spatial.cpp index 1fa6403e73e9..c3e4bc23b3cb 100644 --- a/scene/3d/spatial.cpp +++ b/scene/3d/spatial.cpp @@ -300,6 +300,12 @@ Transform Spatial::get_global_gizmo_transform() const { Transform Spatial::get_local_gizmo_transform() const { return get_transform(); } + +// If not a VisualInstance, use this AABB for the orange box in the editor +AABB Spatial::get_fallback_gizmo_aabb() const { + return AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4)); +} + #endif Spatial *Spatial::get_parent_spatial() const { diff --git a/scene/3d/spatial.h b/scene/3d/spatial.h index 381bf6ec3cf0..ff0cf645a6af 100644 --- a/scene/3d/spatial.h +++ b/scene/3d/spatial.h @@ -167,6 +167,7 @@ class Spatial : public Node { #ifdef TOOLS_ENABLED virtual Transform get_global_gizmo_transform() const; virtual Transform get_local_gizmo_transform() const; + virtual AABB get_fallback_gizmo_aabb() const; #endif void set_as_toplevel(bool p_enabled); diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index a6ab56d22fea..45311c326e05 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -220,6 +220,7 @@ #include "scene/resources/environment.h" #include "scene/resources/mesh_library.h" #include "scene/resources/occluder_shape.h" +#include "scene/resources/occluder_shape_polygon.h" #endif #include "modules/modules_enabled.gen.h" // For freetype. @@ -668,6 +669,7 @@ void register_scene_types() { ClassDB::register_class(); ClassDB::register_virtual_class(); ClassDB::register_class(); + ClassDB::register_class(); OS::get_singleton()->yield(); //may take time to init diff --git a/scene/resources/occluder_shape.cpp b/scene/resources/occluder_shape.cpp index e72c7ec08be1..60f8474bd814 100644 --- a/scene/resources/occluder_shape.cpp +++ b/scene/resources/occluder_shape.cpp @@ -63,6 +63,12 @@ void OccluderShape::notification_exit_world() { VisualServer::get_singleton()->occluder_set_scenario(_shape, RID(), VisualServer::OCCLUDER_TYPE_UNDEFINED); } +#ifdef TOOLS_ENABLED +AABB OccluderShape::get_fallback_gizmo_aabb() const { + return AABB(Vector3(-0.5, -0.5, -0.5), Vector3(1, 1, 1)); +} +#endif + ////////////////////////////////////////////// void OccluderShapeSphere::_bind_methods() { @@ -75,6 +81,29 @@ void OccluderShapeSphere::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "spheres", PROPERTY_HINT_NONE, itos(Variant::PLANE) + ":"), "set_spheres", "get_spheres"); } +#ifdef TOOLS_ENABLED +void OccluderShapeSphere::_update_aabb() { + _aabb_local = AABB(); + + if (!_spheres.size()) { + return; + } + + _aabb_local.position = _spheres[0].normal; + + for (int n = 0; n < _spheres.size(); n++) { + AABB bb(_spheres[n].normal, Vector3(0, 0, 0)); + bb.grow_by(_spheres[n].d); + _aabb_local.merge_with(bb); + } +} + +AABB OccluderShapeSphere::get_fallback_gizmo_aabb() const { + return _aabb_local; +} + +#endif + void OccluderShapeSphere::update_shape_to_visual_server() { VisualServer::get_singleton()->occluder_spheres_update(get_shape(), _spheres); } @@ -188,6 +217,8 @@ void OccluderShapeSphere::set_spheres(const Vector &p_spheres) { if (adding_in_editor) { _spheres.set(_spheres.size() - 1, Plane(Vector3(), 1.0)); } + + _update_aabb(); #endif notify_change_to_owners(); @@ -198,6 +229,9 @@ void OccluderShapeSphere::set_sphere_position(int p_idx, const Vector3 &p_positi Plane p = _spheres[p_idx]; p.normal = p_position; _spheres.set(p_idx, p); +#ifdef TOOLS_ENABLED + _update_aabb(); +#endif notify_change_to_owners(); } } @@ -207,6 +241,9 @@ void OccluderShapeSphere::set_sphere_radius(int p_idx, real_t p_radius) { Plane p = _spheres[p_idx]; p.d = MAX(p_radius, _min_radius); _spheres.set(p_idx, p); +#ifdef TOOLS_ENABLED + _update_aabb(); +#endif notify_change_to_owners(); } } diff --git a/scene/resources/occluder_shape.h b/scene/resources/occluder_shape.h index 217925acf2b2..e98fb1f3eff2 100644 --- a/scene/resources/occluder_shape.h +++ b/scene/resources/occluder_shape.h @@ -57,6 +57,12 @@ class OccluderShape : public Resource { void update_active_to_visual_server(bool p_active); void notification_exit_world(); virtual Transform center_node(const Transform &p_global_xform, const Transform &p_parent_xform, real_t p_snap) = 0; + +#ifdef TOOLS_ENABLED + // for editor gizmo + virtual AABB get_fallback_gizmo_aabb() const; + virtual bool requires_uniform_scale() const { return false; } +#endif }; class OccluderShapeSphere : public OccluderShape { @@ -66,6 +72,11 @@ class OccluderShapeSphere : public OccluderShape { Vector _spheres; const real_t _min_radius = 0.1; +#ifdef TOOLS_ENABLED + AABB _aabb_local; + void _update_aabb(); +#endif + protected: static void _bind_methods(); @@ -80,6 +91,11 @@ class OccluderShapeSphere : public OccluderShape { virtual void update_shape_to_visual_server(); virtual Transform center_node(const Transform &p_global_xform, const Transform &p_parent_xform, real_t p_snap); +#ifdef TOOLS_ENABLED + virtual AABB get_fallback_gizmo_aabb() const; + virtual bool requires_uniform_scale() const { return false; } +#endif + OccluderShapeSphere(); }; diff --git a/scene/resources/occluder_shape_polygon.cpp b/scene/resources/occluder_shape_polygon.cpp new file mode 100644 index 000000000000..4533c9d811de --- /dev/null +++ b/scene/resources/occluder_shape_polygon.cpp @@ -0,0 +1,236 @@ +/*************************************************************************/ +/* occluder_shape_polygon.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "occluder_shape_polygon.h" + +#include "servers/visual_server.h" + +#ifdef TOOLS_ENABLED +void OccluderShapePolygon::_update_aabb() { + _aabb_local = AABB(); + + if (_poly_pts_local.size()) { + Vector3 begin = _vec2to3(_poly_pts_local[0]); + Vector3 end = begin; + + for (int n = 1; n < _poly_pts_local.size(); n++) { + Vector3 pt = _vec2to3(_poly_pts_local[n]); + begin.x = MIN(begin.x, pt.x); + begin.y = MIN(begin.y, pt.y); + begin.z = MIN(begin.z, pt.z); + end.x = MAX(end.x, pt.x); + end.y = MAX(end.y, pt.y); + end.z = MAX(end.z, pt.z); + } + + for (int n = 0; n < _hole_pts_local.size(); n++) { + Vector3 pt = _vec2to3(_hole_pts_local[n]); + begin.x = MIN(begin.x, pt.x); + begin.y = MIN(begin.y, pt.y); + begin.z = MIN(begin.z, pt.z); + end.x = MAX(end.x, pt.x); + end.y = MAX(end.y, pt.y); + end.z = MAX(end.z, pt.z); + } + + _aabb_local.position = begin; + _aabb_local.size = end - begin; + } +} + +AABB OccluderShapePolygon::get_fallback_gizmo_aabb() const { + return _aabb_local; +} + +#endif + +void OccluderShapePolygon::_sanitize_points_internal(const PoolVector &p_from, Vector &r_to) { + // remove duplicates? NYI maybe not necessary + Vector raw; + raw.resize(p_from.size()); + for (int n = 0; n < p_from.size(); n++) { + raw.set(n, p_from[n]); + } + + // this function may get rid of some concave points due to user editing .. + // may not be necessary, no idea how fast it is + r_to = Geometry::convex_hull_2d(raw); + + // some peculiarity of convex_hull_2d function, it duplicates the last point for some reason + if (r_to.size() > 1) { + r_to.resize(r_to.size() - 1); + } + + // sort winding, the system expects counter clockwise polys + Geometry::sort_polygon_winding(r_to, false); +} + +void OccluderShapePolygon::_sanitize_points() { + _sanitize_points_internal(_poly_pts_local_raw, _poly_pts_local); + _sanitize_points_internal(_hole_pts_local_raw, _hole_pts_local); + +#ifdef TOOLS_ENABLED + _update_aabb(); +#endif +} + +void OccluderShapePolygon::set_polygon_point(int p_idx, const Vector2 &p_point) { + if (p_idx >= _poly_pts_local_raw.size()) { + return; + } + + _poly_pts_local_raw.set(p_idx, p_point); + _sanitize_points(); + notify_change_to_owners(); +} + +void OccluderShapePolygon::set_hole_point(int p_idx, const Vector2 &p_point) { + if (p_idx >= _hole_pts_local_raw.size()) { + return; + } + + _hole_pts_local_raw.set(p_idx, p_point); + _sanitize_points(); + notify_change_to_owners(); +} + +void OccluderShapePolygon::set_polygon_points(const PoolVector &p_points) { + _poly_pts_local_raw = p_points; + _sanitize_points(); + notify_change_to_owners(); +} + +void OccluderShapePolygon::set_hole_points(const PoolVector &p_points) { + _hole_pts_local_raw = p_points; + _sanitize_points(); + notify_change_to_owners(); +} + +PoolVector OccluderShapePolygon::get_polygon_points() const { + return _poly_pts_local_raw; +} + +PoolVector OccluderShapePolygon::get_hole_points() const { + return _hole_pts_local_raw; +} + +void OccluderShapePolygon::notification_enter_world(RID p_scenario) { + VisualServer::get_singleton()->occluder_set_scenario(get_shape(), p_scenario, VisualServer::OCCLUDER_TYPE_MESH); +} + +void OccluderShapePolygon::update_shape_to_visual_server() { + if (_poly_pts_local.size() < 3) + return; + + Geometry::OccluderMeshData md; + md.faces.resize(1); + + Geometry::OccluderMeshData::Face &face = md.faces[0]; + face.two_way = is_two_way(); + + md.vertices.resize(_poly_pts_local.size() + _hole_pts_local.size()); + face.indices.resize(_poly_pts_local.size()); + + for (int n = 0; n < _poly_pts_local.size(); n++) { + md.vertices[n] = _vec2to3(_poly_pts_local[n]); + face.indices[n] = n; + } + + // hole points + if (_hole_pts_local.size()) { + face.holes.resize(1); + Geometry::OccluderMeshData::Hole &hole = face.holes[0]; + hole.indices.resize(_hole_pts_local.size()); + + for (int n = 0; n < _hole_pts_local.size(); n++) { + int dest_idx = n + _poly_pts_local.size(); + + hole.indices[n] = dest_idx; + md.vertices[dest_idx] = _vec2to3(_hole_pts_local[n]); + } + } + + face.plane = Plane(Vector3(0, 0, 0), Vector3(0, 0, -1)); + + VisualServer::get_singleton()->occluder_mesh_update(get_shape(), md); +} + +void OccluderShapePolygon::set_two_way(bool p_two_way) { + _settings_two_way = p_two_way; + notify_change_to_owners(); +} + +Transform OccluderShapePolygon::center_node(const Transform &p_global_xform, const Transform &p_parent_xform, real_t p_snap) { + return Transform(); +} + +void OccluderShapePolygon::clear() { + _poly_pts_local.clear(); + _poly_pts_local_raw.resize(0); + _hole_pts_local.clear(); + _hole_pts_local_raw.resize(0); +#ifdef TOOLS_ENABLED + _aabb_local = AABB(); +#endif +} + +void OccluderShapePolygon::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_two_way", "two_way"), &OccluderShapePolygon::set_two_way); + ClassDB::bind_method(D_METHOD("is_two_way"), &OccluderShapePolygon::is_two_way); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "two_way"), "set_two_way", "is_two_way"); + + ClassDB::bind_method(D_METHOD("set_polygon_points", "points"), &OccluderShapePolygon::set_polygon_points); + ClassDB::bind_method(D_METHOD("get_polygon_points"), &OccluderShapePolygon::get_polygon_points); + + ClassDB::bind_method(D_METHOD("set_polygon_point", "index", "position"), &OccluderShapePolygon::set_polygon_point); + + ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR2_ARRAY, "polygon_points"), "set_polygon_points", "get_polygon_points"); + + ClassDB::bind_method(D_METHOD("set_hole_points", "points"), &OccluderShapePolygon::set_hole_points); + ClassDB::bind_method(D_METHOD("get_hole_points"), &OccluderShapePolygon::get_hole_points); + ClassDB::bind_method(D_METHOD("set_hole_point", "index", "position"), &OccluderShapePolygon::set_hole_point); + + ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR2_ARRAY, "hole_points"), "set_hole_points", "get_hole_points"); +} + +OccluderShapePolygon::OccluderShapePolygon() : + OccluderShape(RID_PRIME(VisualServer::get_singleton()->occluder_create())) { + clear(); + + PoolVector points; + points.resize(4); + points.set(0, Vector2(1, -1)); + points.set(1, Vector2(1, 1)); + points.set(2, Vector2(-1, 1)); + points.set(3, Vector2(-1, -1)); + + set_polygon_points(points); // default shape +} diff --git a/scene/resources/occluder_shape_polygon.h b/scene/resources/occluder_shape_polygon.h new file mode 100644 index 000000000000..6d04b09c6f24 --- /dev/null +++ b/scene/resources/occluder_shape_polygon.h @@ -0,0 +1,95 @@ +/*************************************************************************/ +/* occluder_shape_polygon.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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. */ +/*************************************************************************/ + +#ifndef OCCLUDER_SHAPE_POLYGON_H +#define OCCLUDER_SHAPE_POLYGON_H + +#include "occluder_shape.h" + +class OccluderShapePolygon : public OccluderShape { + GDCLASS(OccluderShapePolygon, OccluderShape); + OBJ_SAVE_TYPE(OccluderShapePolygon); + + friend class OccluderSpatialGizmo; + + // points in local space of the plane, + // not necessary in correct winding order + // (as they can be edited by the user) + // Note: these are saved by the IDE + PoolVector _poly_pts_local_raw; + PoolVector _hole_pts_local_raw; + + // sanitized + Vector _poly_pts_local; + Vector _hole_pts_local; + bool _settings_two_way = true; + +#ifdef TOOLS_ENABLED + AABB _aabb_local; + void _update_aabb(); +#endif + + // mem funcs + void _sanitize_points(); + void _sanitize_points_internal(const PoolVector &p_from, Vector &r_to); + static Vector3 _vec2to3(const Vector2 &p_pt) { return Vector3(p_pt.x, p_pt.y, 0.0); } + +protected: + static void _bind_methods(); + +public: + // the raw points are used for the IDE Inspector, and also to allow the user + // to edit the geometry of the poly at runtime (they can also just change the node transform) + void set_polygon_points(const PoolVector &p_points); + PoolVector get_polygon_points() const; + void set_hole_points(const PoolVector &p_points); + PoolVector get_hole_points() const; + + // primarily for the gizmo + void set_polygon_point(int p_idx, const Vector2 &p_point); + void set_hole_point(int p_idx, const Vector2 &p_point); + + void set_two_way(bool p_two_way); + bool is_two_way() const { return _settings_two_way; } + + void clear(); + + virtual void notification_enter_world(RID p_scenario); + virtual void update_shape_to_visual_server(); + virtual Transform center_node(const Transform &p_global_xform, const Transform &p_parent_xform, real_t p_snap); + +#ifdef TOOLS_ENABLED + virtual AABB get_fallback_gizmo_aabb() const; +#endif + + OccluderShapePolygon(); +}; + +#endif // OCCLUDER_SHAPE_POLYGON_H diff --git a/servers/visual/portals/portal_defines.h b/servers/visual/portals/portal_defines.h new file mode 100644 index 000000000000..f406f8115ee9 --- /dev/null +++ b/servers/visual/portals/portal_defines.h @@ -0,0 +1,42 @@ +/*************************************************************************/ +/* portal_defines.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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. */ +/*************************************************************************/ + +#ifndef PORTAL_DEFINES_H +#define PORTAL_DEFINES_H + +// This file is to allow constants etc to be accessible from outside the visual server, +// while keeping the dependencies to an absolute minimum. + +struct PortalDefines { + static const int OCCLUSION_POLY_MAX_VERTS = 8; + static const int OCCLUSION_POLY_MAX_HOLES = 4; +}; + +#endif // PORTAL_DEFINES_H diff --git a/servers/visual/portals/portal_occlusion_culler.cpp b/servers/visual/portals/portal_occlusion_culler.cpp index 2684a1f391cd..9ab212698b71 100644 --- a/servers/visual/portals/portal_occlusion_culler.cpp +++ b/servers/visual/portals/portal_occlusion_culler.cpp @@ -30,26 +30,252 @@ #include "portal_occlusion_culler.h" +#include "core/engine.h" +#include "core/math/aabb.h" #include "core/project_settings.h" #include "portal_renderer.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; - real_t goodness_of_fit[MAX_SPHERES]; + // spheres + _num_spheres = 0; + real_t goodness_of_fit_sphere[MAX_SPHERES]; for (int n = 0; n < _max_spheres; n++) { - goodness_of_fit[n] = 0.0; + goodness_of_fit_sphere[n] = 0.0f; } - real_t weakest_fit = FLT_MAX; + real_t weakest_fit_sphere = FLT_MAX; int weakest_sphere = 0; _sphere_closest_dist = FLT_MAX; - // TODO : occlusion cull spheres AGAINST themselves. - // i.e. a sphere that is occluded by another occluder is no - // use as an occluder... + // 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 - // find sphere occluders + // find occluders for (unsigned int o = 0; o < p_occluder_pool_ids.size(); o++) { int id = p_occluder_pool_ids[o]; VSOccluder &occ = p_portal_renderer.get_pool_occluder(id); @@ -61,6 +287,9 @@ void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, c 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::OT_SPHERE) { // make sure world space spheres are up to date p_portal_renderer.occluder_ensure_up_to_date_sphere(occ); @@ -83,7 +312,7 @@ void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, c // calculate the goodness of fit .. smaller distance better, and larger radius // calculate adjusted radius at 100.0 - real_t fit = 100 / MAX(dist, 0.01); + real_t fit = 100 / MAX(dist, 0.01f); fit *= occluder_sphere.radius; // until we reach the max, just keep recording, and keep track @@ -91,10 +320,10 @@ void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, c if (_num_spheres < _max_spheres) { _spheres[_num_spheres] = occluder_sphere; _sphere_distances[_num_spheres] = dist; - goodness_of_fit[_num_spheres] = fit; + goodness_of_fit_sphere[_num_spheres] = fit; - if (fit < weakest_fit) { - weakest_fit = fit; + if (fit < weakest_fit_sphere) { + weakest_fit_sphere = fit; weakest_sphere = _num_spheres; } @@ -106,10 +335,10 @@ void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, c _num_spheres++; } else { // must beat the weakest - if (fit > weakest_fit) { + if (fit > weakest_fit_sphere) { _spheres[weakest_sphere] = occluder_sphere; _sphere_distances[weakest_sphere] = dist; - goodness_of_fit[weakest_sphere] = fit; + goodness_of_fit_sphere[weakest_sphere] = fit; // keep a record of the closest sphere for quick rejects if (dist < _sphere_closest_dist) { @@ -117,10 +346,10 @@ void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, c } // the weakest may have changed (this could be done more efficiently) - weakest_fit = FLT_MAX; + weakest_fit_sphere = FLT_MAX; for (int s = 0; s < _max_spheres; s++) { - if (goodness_of_fit[s] < weakest_fit) { - weakest_fit = goodness_of_fit[s]; + if (goodness_of_fit_sphere[s] < weakest_fit_sphere) { + weakest_fit_sphere = goodness_of_fit_sphere[s]; weakest_sphere = s; } } @@ -128,8 +357,109 @@ void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, c } } } // sphere + + if (occ.type == VSOccluder::OT_MESH) { + // make sure world space spheres are up to date + p_portal_renderer.occluder_ensure_up_to_date_polys(occ); + + // multiple polys + for (int n = 0; n < occ.list_ids.size(); n++) { + const VSOccluder_Mesh &opoly = p_portal_renderer.get_pool_occluder_mesh(occ.list_ids[n]); + const Occlusion::PolyPlane &poly = opoly.poly_world; + + // 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); @@ -150,41 +480,400 @@ void PortalOcclusionCuller::prepare_generic(PortalRenderer &p_portal_renderer, c n--; } } + + // record whether to do any occlusion culling at all.. + _occluders_present = _num_spheres || _num_polys; } -bool PortalOcclusionCuller::cull_sphere(const Vector3 &p_occludee_center, real_t p_occludee_radius, int p_ignore_sphere) const { - if (!_num_spheres) { +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_Mesh &mesh = _portal_renderer->get_pool_occluder_mesh(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_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.poly_world; + + int hole_num_verts = hole.poly_world.num_verts; + const Vector3 *hverts = hole.poly_world.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 GODOT_OCCLUSION_FLASH_POLYS +#ifdef GODOT_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)); + + // 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); + + // 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_Mesh &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_world.num_verts; + + for (int n = 0; n < num_verts; n++) { + // source and dest in homogeneous coords + Plane source(p_opoly.poly_world.verts[n], 1.0f); + Plane &dest = xpoints[n]; + + dest = _matrix_camera.xform4(source); + } + + // find screen space area + real_t area = _clipper.clip_and_find_poly_area(xpoints, num_verts); + if (area <= 0.0f) { 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(); + r_fit = area; - // account for occludee radius - real_t dist_to_occludee = dist_to_occludee_raw - p_occludee_radius; + 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 (dist_to_occludee < _sphere_closest_dist) { + if (p_dist_to_occludee < _sphere_closest_dist) { 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; - // 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 (dist_to_occludee < occluder_dist_to_cam) { + 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 / dist_to_occludee); + 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; @@ -195,8 +884,8 @@ bool PortalOcclusionCuller::cull_sphere(const Vector3 &p_occludee_center, real_t // distance to hit real_t dist; - if (occluder_sphere.intersect_ray(_pt_camera, ray_dir, dist, occluder_radius)) { - if ((dist < dist_to_occludee) && (s != p_ignore_sphere)) { + 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; } @@ -207,6 +896,51 @@ bool PortalOcclusionCuller::cull_sphere(const Vector3 &p_occludee_center, real_t 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 diff --git a/servers/visual/portals/portal_occlusion_culler.h b/servers/visual/portals/portal_occlusion_culler.h index caa019e89240..40e13f2342ae 100644 --- a/servers/visual/portals/portal_occlusion_culler.h +++ b/servers/visual/portals/portal_occlusion_culler.h @@ -32,15 +32,57 @@ #define PORTAL_OCCLUSION_CULLER_H class PortalRenderer; +#include "core/math/camera_matrix.h" +#include "core/math/geometry.h" #include "portal_types.h" class PortalOcclusionCuller { enum { MAX_SPHERES = 64, + MAX_POLYS = 64, + }; + + class Clipper { + public: + real_t clip_and_find_poly_area(const Plane *p_verts, int p_num_verts); + + private: + enum Boundary { + B_LEFT, + B_RIGHT, + B_TOP, + B_BOTTOM, + B_NEAR, + B_FAR, + }; + + bool is_inside(const Plane &p_pt, Boundary p_boundary); + Plane intersect(const Plane &p_a, const Plane &p_b, Boundary p_boundary); + void debug_print_points(String p_string); + + Plane interpolate(const Plane &p_a, const Plane &p_b, real_t p_t) const; + bool clip_to_plane(real_t a, real_t b, real_t c, real_t d); + + LocalVectori _pts_in; + LocalVectori _pts_out; + + // after perspective divide + LocalVectori _pts_final; + + template + int sgn(T val) { + return (T(0) < val) - (val < T(0)); + } }; public: PortalOcclusionCuller(); + + void prepare_camera(const CameraMatrix &p_cam_matrix, const Vector3 &p_cam_dir) { + _matrix_camera = p_cam_matrix; + _pt_cam_dir = p_cam_dir; + } + void prepare(PortalRenderer &p_portal_renderer, const VSRoom &p_room, const Vector3 &pt_camera, const LocalVector &p_planes, const Plane *p_near_plane) { if (p_near_plane) { static LocalVector local_planes; @@ -61,16 +103,33 @@ class PortalOcclusionCuller { } void prepare_generic(PortalRenderer &p_portal_renderer, const LocalVector &p_occluder_pool_ids, const Vector3 &pt_camera, const LocalVector &p_planes); + bool cull_aabb(const AABB &p_aabb) const { - if (!_num_spheres) { + if (!_occluders_present) { return false; } + if (cull_aabb_to_polys(p_aabb)) { + return true; + } - return cull_sphere(p_aabb.get_center(), p_aabb.size.length() * 0.5); + return cull_sphere(p_aabb.get_center(), p_aabb.size.length() * 0.5, -1, false); } - bool cull_sphere(const Vector3 &p_occludee_center, real_t p_occludee_radius, int p_ignore_sphere = -1) const; + + bool cull_sphere(const Vector3 &p_occludee_center, real_t p_occludee_radius, int p_ignore_sphere = -1, bool p_cull_to_polys = true) const; + + Geometry::MeshData debug_get_current_polys() const; + + static bool _redraw_gizmo; private: + bool 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; + bool cull_sphere_to_polys(const Vector3 &p_occludee_center, real_t p_occludee_radius) const; + bool cull_aabb_to_polys(const AABB &p_aabb) const; + + // experimental + bool cull_aabb_to_polys_ex(const AABB &p_aabb) const; + bool _is_poly_of_interest_to_split_plane(const Plane *p_poly_split_plane, int p_poly_id) const; + // if a sphere is entirely in front of any of the culling planes, it can't be seen so returns false bool is_sphere_culled(const Vector3 &p_pos, real_t p_radius, const LocalVector &p_planes) const { for (unsigned int p = 0; p < p_planes.size(); p++) { @@ -99,10 +158,94 @@ class PortalOcclusionCuller { return true; } } + return false; + } + + bool calculate_poly_goodness_of_fit(const VSOccluder_Mesh &p_opoly, real_t &r_fit); + void whittle_polys(); + void precalc_poly_edge_planes(const Vector3 &p_pt_camera); + + bool is_vso_poly_culled(const VSOccluder_Mesh &p_opoly, const LocalVector &p_planes) const { + return is_poly_culled(p_opoly.poly_world, p_planes); + } + + // If all the points of the poly are beyond one of the planes (e.g. frustum), it is completely culled. + bool is_poly_culled(const Occlusion::PolyPlane &p_opoly, const LocalVector &p_planes) const { + for (unsigned int p = 0; p < p_planes.size(); p++) { + const Plane &plane = p_planes[p]; + int points_outside = 0; + for (int n = 0; n < p_opoly.num_verts; n++) { + const Vector3 &pt = p_opoly.verts[n]; + if (!plane.is_point_over(pt)) { + break; + } else { + points_outside++; + } + } + + if (points_outside == p_opoly.num_verts) { + return true; + } + } return false; } + // All the points of the poly must be within ALL the planes to return true. + struct PlaneSet; + bool is_poly_inside_occlusion_volume(const Occlusion::Poly &p_test_poly, const Plane &p_occluder_plane, const PlaneSet &p_planeset) const { + // first test against the occluder poly plane + for (int n = 0; n < p_test_poly.num_verts; n++) { + const Vector3 &pt = p_test_poly.verts[n]; + if (p_occluder_plane.is_point_over(pt)) { + return false; + } + } + + for (int p = 0; p < p_planeset.num_planes; p++) { + const Plane &plane = p_planeset.planes[p]; + + for (int n = 0; n < p_test_poly.num_verts; n++) { + const Vector3 &pt = p_test_poly.verts[n]; + if (plane.is_point_over(pt)) { + return false; + } + } + } + return true; + } + + bool is_poly_touching_hole(const Occlusion::Poly &p_opoly, const PlaneSet &p_planeset) const { + if (!p_opoly.num_verts) { + // should not happen? + return false; + } + // find aabb + AABB bb; + bb.position = p_opoly.verts[0]; + for (int n = 1; n < p_opoly.num_verts; n++) { + bb.expand_to(p_opoly.verts[n]); + } + + // if the AABB is totally outside any edge, it is safe for a hit + real_t omin, omax; + + for (int e = 0; e < p_planeset.num_planes; e++) { + // edge plane to camera + const Plane &plane = p_planeset.planes[e]; + bb.project_range_in_plane(plane, omin, omax); + + // if inside the hole, no longer a hit on this poly + if (omin > 0.0) { + return false; + } + } // for e + + return true; + } + + void log(String p_string, int p_depth = 0) const; + // only a number of the spheres in the scene will be chosen to be // active based on their distance to the camera, screen space etc. Occlusion::Sphere _spheres[MAX_SPHERES]; @@ -111,7 +254,67 @@ class PortalOcclusionCuller { int _num_spheres = 0; int _max_spheres = 8; + struct SortPoly { + enum SortPolyFlags { + SPF_FACES_CAMERA = 1, + SPF_DONE = 2, + SPF_TESTED_AS_OCCLUDER = 4, + SPF_HAS_HOLES = 8, + }; + + Occlusion::PolyPlane poly; + uint32_t flags; +#ifdef TOOLS_ENABLED + uint32_t poly_source_id; +#endif + uint32_t mesh_source_id; + real_t goodness_of_fit; + }; + + struct PlaneSet { + void flip() { + for (int n = 0; n < num_planes; n++) { + planes[n] = -planes[n]; + } + } + // pre-calculated edge planes to the camera + int num_planes = 0; + Plane planes[PortalDefines::OCCLUSION_POLY_MAX_VERTS]; + }; + + struct PreCalcedPoly { + void flip() { + edge_planes.flip(); + for (int n = 0; n < num_holes; n++) { + hole_edge_planes[n].flip(); + } + } + int num_holes = 0; + PlaneSet edge_planes; + PlaneSet hole_edge_planes[PortalDefines::OCCLUSION_POLY_MAX_HOLES]; + Occlusion::Poly hole_polys[PortalDefines::OCCLUSION_POLY_MAX_HOLES]; + }; + + SortPoly _polys[MAX_POLYS]; + PreCalcedPoly _precalced_poly[MAX_POLYS]; + int _num_polys = 0; + int _max_polys = 8; + +#ifdef TOOLS_ENABLED + uint32_t _poly_checksum = 0; +#endif + Vector3 _pt_camera; + Vector3 _pt_cam_dir; + + CameraMatrix _matrix_camera; + PortalRenderer *_portal_renderer = nullptr; + + Clipper _clipper; + + bool _occluders_present = false; + + static bool _debug_log; }; #endif // PORTAL_OCCLUSION_CULLER_H diff --git a/servers/visual/portals/portal_renderer.cpp b/servers/visual/portals/portal_renderer.cpp index 39cd37e52061..031f35719518 100644 --- a/servers/visual/portals/portal_renderer.cpp +++ b/servers/visual/portals/portal_renderer.cpp @@ -547,6 +547,103 @@ void PortalRenderer::occluder_refresh_room_within(uint32_t p_occluder_pool_id) { } } +void PortalRenderer::occluder_update_mesh(OccluderHandle p_handle, const Geometry::OccluderMeshData &p_mesh_data) { + p_handle--; + VSOccluder &occ = _occluder_pool[p_handle]; + ERR_FAIL_COND(occ.type != VSOccluder::OT_MESH); + + // needs world points updating next time + occ.dirty = true; + + const LocalVectori &faces = p_mesh_data.faces; + const LocalVectori &vertices = p_mesh_data.vertices; + + // first deal with the situation where the number of polys has changed (rare) + if (occ.list_ids.size() != faces.size()) { + // not the most efficient, but works... + // remove existing + for (int n = 0; n < occ.list_ids.size(); n++) { + uint32_t id = occ.list_ids[n]; + _occluder_mesh_pool.free(id); + } + + occ.list_ids.clear(); + // create new + for (int n = 0; n < faces.size(); n++) { + uint32_t id; + VSOccluder_Mesh *poly = _occluder_mesh_pool.request(id); + poly->create(); + occ.list_ids.push_back(id); + } + } + + // new data + for (int n = 0; n < occ.list_ids.size(); n++) { + uint32_t id = occ.list_ids[n]; + + VSOccluder_Mesh &opoly = _occluder_mesh_pool[id]; + Occlusion::PolyPlane &poly = opoly.poly_local; + + // source face + const Geometry::OccluderMeshData::Face &face = faces[n]; + opoly.two_way = face.two_way; + + // make sure the number of holes is correct + if (face.holes.size() != opoly.num_holes) { + // slow but hey ho + // delete existing holes + for (int i = 0; i < opoly.num_holes; i++) { + _occluder_hole_pool.free(opoly.hole_pool_ids[i]); + opoly.hole_pool_ids[i] = UINT32_MAX; + } + // create any new holes + opoly.num_holes = face.holes.size(); + for (int i = 0; i < opoly.num_holes; i++) { + uint32_t hole_id; + VSOccluder_Hole *hole = _occluder_hole_pool.request(hole_id); + opoly.hole_pool_ids[i] = hole_id; + hole->create(); + } + } + + poly.plane = face.plane; + + poly.num_verts = MIN(face.indices.size(), Occlusion::PolyPlane::MAX_POLY_VERTS); + + // make sure the world poly also has the correct num verts + opoly.poly_world.num_verts = poly.num_verts; + + for (int c = 0; c < poly.num_verts; c++) { + int vert_index = face.indices[c]; + + if (vert_index < vertices.size()) { + poly.verts[c] = vertices[vert_index]; + } else { + WARN_PRINT_ONCE("occluder_update_mesh : poly index out of range"); + } + } + + // holes + for (int h = 0; h < opoly.num_holes; h++) { + VSOccluder_Hole &dhole = get_pool_occluder_hole(opoly.hole_pool_ids[h]); + const Geometry::OccluderMeshData::Hole &shole = face.holes[h]; + + dhole.poly_local.num_verts = shole.indices.size(); + dhole.poly_local.num_verts = MIN(dhole.poly_local.num_verts, Occlusion::Poly::MAX_POLY_VERTS); + dhole.poly_world.num_verts = dhole.poly_local.num_verts; + + for (int c = 0; c < dhole.poly_local.num_verts; c++) { + int vert_index = shole.indices[c]; + if (vert_index < vertices.size()) { + dhole.poly_local.verts[c] = vertices[vert_index]; + } else { + WARN_PRINT_ONCE("occluder_update_mesh : hole index out of range"); + } + } + } + } +} + void PortalRenderer::occluder_update_spheres(OccluderHandle p_handle, const Vector &p_spheres) { p_handle--; VSOccluder &occ = _occluder_pool[p_handle]; @@ -591,6 +688,9 @@ void PortalRenderer::occluder_destroy(OccluderHandle p_handle) { case VSOccluder::OT_SPHERE: { occluder_update_spheres(p_handle + 1, Vector()); } break; + case VSOccluder::OT_MESH: { + occluder_update_mesh(p_handle + 1, Geometry::OccluderMeshData()); + } break; default: { } break; } @@ -1100,7 +1200,7 @@ void PortalRenderer::rooms_update_gameplay_monitor(const Vector &p_came _gameplay_monitor.update_gameplay(*this, source_rooms, num_source_rooms); } -int PortalRenderer::cull_convex_implementation(const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint) { +int PortalRenderer::cull_convex_implementation(const Vector3 &p_point, const Vector3 &p_cam_dir, const CameraMatrix &p_cam_matrix, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint) { // start room int start_room_id = find_room_within(p_point, r_previous_room_id_hint); @@ -1111,6 +1211,9 @@ int PortalRenderer::cull_convex_implementation(const Vector3 &p_point, const Vec return -1; } + // set up the occlusion culler once off .. this is a prepare before the prepare is done PER room + _tracer.get_occlusion_culler().prepare_camera(p_cam_matrix, p_cam_dir); + // planes must be in CameraMatrix order DEV_ASSERT(p_convex.size() == 6); diff --git a/servers/visual/portals/portal_renderer.h b/servers/visual/portals/portal_renderer.h index 21fd782868a1..449227a286d1 100644 --- a/servers/visual/portals/portal_renderer.h +++ b/servers/visual/portals/portal_renderer.h @@ -31,6 +31,8 @@ #ifndef PORTAL_RENDERER_H #define PORTAL_RENDERER_H +#include "core/math/camera_matrix.h" +#include "core/math/geometry.h" #include "core/math/plane.h" #include "core/pooled_list.h" #include "core/vector.h" @@ -187,30 +189,50 @@ class PortalRenderer { // occluders OccluderHandle occluder_create(VSOccluder::Type p_type); void occluder_update_spheres(OccluderHandle p_handle, const Vector &p_spheres); + void occluder_update_mesh(OccluderHandle p_handle, const Geometry::OccluderMeshData &p_mesh_data); void occluder_set_transform(OccluderHandle p_handle, const Transform &p_xform); void occluder_set_active(OccluderHandle p_handle, bool p_active); void occluder_destroy(OccluderHandle p_handle); + // editor only .. slow + Geometry::MeshData occlusion_debug_get_current_polys() const { return _tracer.get_occlusion_culler().debug_get_current_polys(); } + // note that this relies on a 'frustum' type cull, from a point, and that the planes are specified as in // CameraMatrix, i.e. // order PLANE_NEAR,PLANE_FAR,PLANE_LEFT,PLANE_TOP,PLANE_RIGHT,PLANE_BOTTOM - int cull_convex(const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint) { + int cull_convex(const Transform &p_cam_transform, const CameraMatrix &p_cam_projection, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint) { + // combined camera matrix + CameraMatrix cm = CameraMatrix(p_cam_transform.affine_inverse()); + cm = p_cam_projection * cm; + Vector3 point = p_cam_transform.origin; + Vector3 cam_dir = -p_cam_transform.basis.get_axis(2).normalized(); + if (!_override_camera) - return cull_convex_implementation(p_point, p_convex, p_result_array, p_result_max, p_mask, r_previous_room_id_hint); + return cull_convex_implementation(point, cam_dir, cm, p_convex, p_result_array, p_result_max, p_mask, r_previous_room_id_hint); - return cull_convex_implementation(_override_camera_pos, _override_camera_planes, p_result_array, p_result_max, p_mask, r_previous_room_id_hint); + // override camera matrix NYI + return cull_convex_implementation(_override_camera_pos, cam_dir, cm, _override_camera_planes, p_result_array, p_result_max, p_mask, r_previous_room_id_hint); } - int cull_convex_implementation(const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint); + int cull_convex_implementation(const Vector3 &p_point, const Vector3 &p_cam_dir, const CameraMatrix &p_cam_matrix, const Vector &p_convex, VSInstance **p_result_array, int p_result_max, uint32_t p_mask, int32_t &r_previous_room_id_hint); + + bool occlusion_is_active() const { return _occluder_pool.active_size() && use_occlusion_culling; } // special function for occlusion culling only that does not use portals / rooms, // but allows using occluders with the main scene - int occlusion_cull(const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_num_results) { + int occlusion_cull(const Transform &p_cam_transform, const CameraMatrix &p_cam_projection, const Vector &p_convex, VSInstance **p_result_array, int p_num_results) { // inactive? if (!_occluder_pool.active_size() || !use_occlusion_culling) { return p_num_results; } - return _tracer.occlusion_cull(*this, p_point, p_convex, p_result_array, p_num_results); + + // combined camera matrix + CameraMatrix cm = CameraMatrix(p_cam_transform.affine_inverse()); + cm = p_cam_projection * cm; + Vector3 point = p_cam_transform.origin; + Vector3 cam_dir = -p_cam_transform.basis.get_axis(2).normalized(); + + return _tracer.occlusion_cull(*this, point, cam_dir, cm, p_convex, p_result_array, p_num_results); } bool is_active() const { return _active && _loaded; } @@ -235,10 +257,13 @@ class PortalRenderer { RGhost &get_pool_rghost(uint32_t p_pool_id) { return _rghost_pool[p_pool_id]; } const RGhost &get_pool_rghost(uint32_t p_pool_id) const { return _rghost_pool[p_pool_id]; } + const LocalVector &get_occluders_active_list() const { return _occluder_pool.get_active_list(); } const VSOccluder &get_pool_occluder(uint32_t p_pool_id) const { return _occluder_pool[p_pool_id]; } VSOccluder &get_pool_occluder(uint32_t p_pool_id) { return _occluder_pool[p_pool_id]; } const VSOccluder_Sphere &get_pool_occluder_sphere(uint32_t p_pool_id) const { return _occluder_sphere_pool[p_pool_id]; } - const LocalVector &get_occluders_active_list() const { return _occluder_pool.get_active_list(); } + const VSOccluder_Mesh &get_pool_occluder_mesh(uint32_t p_pool_id) const { return _occluder_mesh_pool[p_pool_id]; } + const VSOccluder_Hole &get_pool_occluder_hole(uint32_t p_pool_id) const { return _occluder_hole_pool[p_pool_id]; } + VSOccluder_Hole &get_pool_occluder_hole(uint32_t p_pool_id) { return _occluder_hole_pool[p_pool_id]; } VSStaticGhost &get_static_ghost(uint32_t p_id) { return _static_ghosts[p_id]; } @@ -295,6 +320,8 @@ class PortalRenderer { // occluders TrackedPooledList _occluder_pool; TrackedPooledList _occluder_sphere_pool; + TrackedPooledList _occluder_mesh_pool; + TrackedPooledList _occluder_hole_pool; PVS _pvs; @@ -327,6 +354,7 @@ class PortalRenderer { static String _addr_to_string(const void *p_addr); void occluder_ensure_up_to_date_sphere(VSOccluder &r_occluder); + void occluder_ensure_up_to_date_polys(VSOccluder &r_occluder); void occluder_refresh_room_within(uint32_t p_occluder_pool_id); }; @@ -350,7 +378,6 @@ inline void PortalRenderer::occluder_ensure_up_to_date_sphere(VSOccluder &r_occl uint32_t pool_id = r_occluder.list_ids[n]; VSOccluder_Sphere &osphere = _occluder_sphere_pool[pool_id]; - // transform position and radius osphere.world.pos = tr.xform(osphere.local.pos); osphere.world.radius = osphere.local.radius * scale; @@ -370,4 +397,36 @@ inline void PortalRenderer::occluder_ensure_up_to_date_sphere(VSOccluder &r_occl r_occluder.aabb.size = bb_max - bb_min; } -#endif +inline void PortalRenderer::occluder_ensure_up_to_date_polys(VSOccluder &r_occluder) { + if (!r_occluder.dirty) { + return; + } + r_occluder.dirty = false; + + const Transform &tr = r_occluder.xform; + + for (int n = 0; n < r_occluder.list_ids.size(); n++) { + uint32_t pool_id = r_occluder.list_ids[n]; + + VSOccluder_Mesh &opoly = _occluder_mesh_pool[pool_id]; + + for (int i = 0; i < opoly.poly_local.num_verts; i++) { + opoly.poly_world.verts[i] = tr.xform(opoly.poly_local.verts[i]); + } + + opoly.poly_world.plane = tr.xform(opoly.poly_local.plane); + + // holes + for (int h = 0; h < opoly.num_holes; h++) { + uint32_t hid = opoly.hole_pool_ids[h]; + + VSOccluder_Hole &hole = _occluder_hole_pool[hid]; + + for (int i = 0; i < hole.poly_local.num_verts; i++) { + hole.poly_world.verts[i] = tr.xform(hole.poly_local.verts[i]); + } + } + } +} + +#endif // PORTAL_RENDERER_H diff --git a/servers/visual/portals/portal_tracer.cpp b/servers/visual/portals/portal_tracer.cpp index 50f9a4b32ecb..a89a8294c451 100644 --- a/servers/visual/portals/portal_tracer.cpp +++ b/servers/visual/portals/portal_tracer.cpp @@ -532,7 +532,9 @@ void PortalTracer::trace_recursive(const TraceParams &p_params, int p_depth, int } // for p through portals } -int PortalTracer::occlusion_cull(PortalRenderer &p_portal_renderer, const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_num_results) { +int PortalTracer::occlusion_cull(PortalRenderer &p_portal_renderer, const Vector3 &p_point, const Vector3 &p_cam_dir, const CameraMatrix &p_cam_matrix, const Vector &p_convex, VSInstance **p_result_array, int p_num_results) { + _occlusion_culler.prepare_camera(p_cam_matrix, p_cam_dir); + // silly conversion of vector to local vector // can this be avoided? NYI // pretty cheap anyway as it will just copy 6 planes, max a few times per frame... diff --git a/servers/visual/portals/portal_tracer.h b/servers/visual/portals/portal_tracer.h index 091d57498424..d5f85558d431 100644 --- a/servers/visual/portals/portal_tracer.h +++ b/servers/visual/portals/portal_tracer.h @@ -41,6 +41,7 @@ //#define PORTAL_RENDERER_STORE_MOVING_RIDS #endif +struct CameraMatrix; class PortalRenderer; struct VSRoom; @@ -113,7 +114,10 @@ class PortalTracer { // special function for occlusion culling only that does not use portals / rooms, // but allows using occluders with the main scene - int occlusion_cull(PortalRenderer &p_portal_renderer, const Vector3 &p_point, const Vector &p_convex, VSInstance **p_result_array, int p_num_results); + int occlusion_cull(PortalRenderer &p_portal_renderer, const Vector3 &p_point, const Vector3 &p_cam_dir, const CameraMatrix &p_cam_matrix, const Vector &p_convex, VSInstance **p_result_array, int p_num_results); + + PortalOcclusionCuller &get_occlusion_culler() { return _occlusion_culler; } + const PortalOcclusionCuller &get_occlusion_culler() const { return _occlusion_culler; } private: // main tracing function is recursive diff --git a/servers/visual/portals/portal_types.h b/servers/visual/portals/portal_types.h index 5944ff04e8b6..0907988d654e 100644 --- a/servers/visual/portals/portal_types.h +++ b/servers/visual/portals/portal_types.h @@ -39,6 +39,7 @@ #include "core/math/vector3.h" #include "core/object_id.h" #include "core/rid.h" +#include "portal_defines.h" // visual server scene instance. // we can't have a pointer to nested class outside of visual server scene... @@ -391,6 +392,7 @@ struct VSOccluder { enum Type : uint32_t { OT_UNDEFINED, OT_SPHERE, + OT_MESH, OT_NUM_TYPES, } type; @@ -444,6 +446,30 @@ struct Sphere { return true; } }; + +struct Poly { + static const int MAX_POLY_VERTS = PortalDefines::OCCLUSION_POLY_MAX_VERTS; + void create() { + num_verts = 0; + } + void flip() { + for (int n = 0; n < num_verts / 2; n++) { + SWAP(verts[n], verts[num_verts - n - 1]); + } + } + + int num_verts; + Vector3 verts[MAX_POLY_VERTS]; +}; + +struct PolyPlane : public Poly { + void flip() { + plane = -plane; + Poly::flip(); + } + Plane plane; +}; + } // namespace Occlusion struct VSOccluder_Sphere { @@ -456,4 +482,32 @@ struct VSOccluder_Sphere { Occlusion::Sphere world; }; +struct VSOccluder_Mesh { + static const int MAX_POLY_HOLES = PortalDefines::OCCLUSION_POLY_MAX_HOLES; + void create() { + poly_local.create(); + poly_world.create(); + num_holes = 0; + two_way = false; + for (int n = 0; n < MAX_POLY_HOLES; n++) { + hole_pool_ids[n] = UINT32_MAX; + } + } + Occlusion::PolyPlane poly_local; + Occlusion::PolyPlane poly_world; + bool two_way; + + int num_holes; + uint32_t hole_pool_ids[MAX_POLY_HOLES]; +}; + +struct VSOccluder_Hole { + void create() { + poly_local.create(); + poly_world.create(); + } + Occlusion::Poly poly_local; + Occlusion::Poly poly_world; +}; + #endif diff --git a/servers/visual/visual_server_raster.h b/servers/visual/visual_server_raster.h index aedc17852ed1..27e2ba6f1c94 100644 --- a/servers/visual/visual_server_raster.h +++ b/servers/visual/visual_server_raster.h @@ -585,9 +585,11 @@ class VisualServerRaster : public VisualServer { BIND0R(RID, occluder_create) BIND3(occluder_set_scenario, RID, RID, OccluderType) BIND2(occluder_spheres_update, RID, const Vector &) + BIND2(occluder_mesh_update, RID, const Geometry::OccluderMeshData &) BIND2(occluder_set_transform, RID, const Transform &) BIND2(occluder_set_active, RID, bool) BIND1(set_use_occlusion_culling, bool) + BIND1RC(Geometry::MeshData, occlusion_debug_get_current_polys, RID) // Rooms BIND0R(RID, room_create) diff --git a/servers/visual/visual_server_scene.cpp b/servers/visual/visual_server_scene.cpp index f6cf5ee31131..fb384f8eccbd 100644 --- a/servers/visual/visual_server_scene.cpp +++ b/servers/visual/visual_server_scene.cpp @@ -1246,12 +1246,28 @@ void VisualServerScene::occluder_spheres_update(RID p_occluder, const Vectorscenario->_portal_renderer.occluder_update_spheres(ro->scenario_occluder_id, p_spheres); } +void VisualServerScene::occluder_mesh_update(RID p_occluder, const Geometry::OccluderMeshData &p_mesh_data) { + Occluder *ro = occluder_owner.getornull(p_occluder); + ERR_FAIL_COND(!ro); + ERR_FAIL_COND(!ro->scenario); + ro->scenario->_portal_renderer.occluder_update_mesh(ro->scenario_occluder_id, p_mesh_data); +} + void VisualServerScene::set_use_occlusion_culling(bool p_enable) { // this is not scenario specific, and is global // (mainly for debugging) PortalRenderer::use_occlusion_culling = p_enable; } +Geometry::MeshData VisualServerScene::occlusion_debug_get_current_polys(RID p_scenario) const { + Scenario *scenario = scenario_owner.getornull(p_scenario); + if (!scenario) { + return Geometry::MeshData(); + } + + return scenario->_portal_renderer.occlusion_debug_get_current_polys(); +} + // Rooms void VisualServerScene::callbacks_register(VisualServerCallbacks *p_callbacks) { _visual_server_callbacks = p_callbacks; @@ -1480,13 +1496,13 @@ Vector VisualServerScene::instances_cull_convex(const Vector &p } // thin wrapper to allow rooms / portals to take over culling if active -int VisualServerScene::_cull_convex_from_point(Scenario *p_scenario, const Vector3 &p_point, const Vector &p_convex, Instance **p_result_array, int p_result_max, int32_t &r_previous_room_id_hint, uint32_t p_mask) { +int VisualServerScene::_cull_convex_from_point(Scenario *p_scenario, const Transform &p_cam_transform, const CameraMatrix &p_cam_projection, const Vector &p_convex, Instance **p_result_array, int p_result_max, int32_t &r_previous_room_id_hint, uint32_t p_mask) { int res = -1; if (p_scenario->_portal_renderer.is_active()) { // Note that the portal renderer ASSUMES that the planes exactly match the convention in // CameraMatrix of enum Planes (6 planes, in order, near, far etc) // If this is not the case, it should not be used. - res = p_scenario->_portal_renderer.cull_convex(p_point, p_convex, (VSInstance **)p_result_array, p_result_max, p_mask, r_previous_room_id_hint); + res = p_scenario->_portal_renderer.cull_convex(p_cam_transform, p_cam_projection, p_convex, (VSInstance **)p_result_array, p_result_max, p_mask, r_previous_room_id_hint); } // fallback to BVH / octree if portals not active @@ -1494,7 +1510,9 @@ int VisualServerScene::_cull_convex_from_point(Scenario *p_scenario, const Vecto res = p_scenario->sps->cull_convex(p_convex, p_result_array, p_result_max, p_mask); // Opportunity for occlusion culling on the main scene. This will be a noop if no occluders. - res = p_scenario->_portal_renderer.occlusion_cull(p_point, p_convex, (VSInstance **)p_result_array, res); + if (p_scenario->_portal_renderer.occlusion_is_active()) { + res = p_scenario->_portal_renderer.occlusion_cull(p_cam_transform, p_cam_projection, p_convex, (VSInstance **)p_result_array, res); + } } return res; } @@ -2321,7 +2339,7 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons Vector planes = cm.get_projection_planes(xform); - int cull_count = _cull_convex_from_point(p_scenario, light_transform.origin, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK); + int cull_count = _cull_convex_from_point(p_scenario, light_transform, cm, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK); Plane near_plane(xform.origin, -xform.basis.get_axis(2)); for (int j = 0; j < cull_count; j++) { @@ -2356,7 +2374,7 @@ bool VisualServerScene::_light_instance_update_shadow(Instance *p_instance, cons cm.set_perspective(angle * 2.0, 1.0, 0.01, radius); Vector planes = cm.get_projection_planes(light_transform); - int cull_count = _cull_convex_from_point(p_scenario, light_transform.origin, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK); + int cull_count = _cull_convex_from_point(p_scenario, light_transform, cm, planes, instance_shadow_cull_result, MAX_INSTANCE_CULL, light->previous_room_id_hint, VS::INSTANCE_GEOMETRY_MASK); Plane near_plane(light_transform.origin, -light_transform.basis.get_axis(2)); for (int j = 0; j < cull_count; j++) { @@ -2535,7 +2553,7 @@ void VisualServerScene::_prepare_scene(const Transform p_cam_transform, const Ca float z_far = p_cam_projection.get_z_far(); /* STEP 2 - CULL */ - instance_cull_count = _cull_convex_from_point(scenario, p_cam_transform.origin, planes, instance_cull_result, MAX_INSTANCE_CULL, r_previous_room_id_hint); + instance_cull_count = _cull_convex_from_point(scenario, p_cam_transform, p_cam_projection, planes, instance_cull_result, MAX_INSTANCE_CULL, r_previous_room_id_hint); light_cull_count = 0; reflection_probe_cull_count = 0; diff --git a/servers/visual/visual_server_scene.h b/servers/visual/visual_server_scene.h index 4337b4564e7e..a1f97e83afc0 100644 --- a/servers/visual/visual_server_scene.h +++ b/servers/visual/visual_server_scene.h @@ -694,10 +694,14 @@ class VisualServerScene { virtual RID occluder_create(); virtual void occluder_set_scenario(RID p_occluder, RID p_scenario, VisualServer::OccluderType p_type); virtual void occluder_spheres_update(RID p_occluder, const Vector &p_spheres); + virtual void occluder_mesh_update(RID p_occluder, const Geometry::OccluderMeshData &p_mesh_data); virtual void occluder_set_transform(RID p_occluder, const Transform &p_xform); virtual void occluder_set_active(RID p_occluder, bool p_active); virtual void set_use_occlusion_culling(bool p_enable); + // editor only .. slow + virtual Geometry::MeshData occlusion_debug_get_current_polys(RID p_scenario) const; + // Rooms struct Room : RID_Data { // all interations with actual rooms are indirect, as the room is part of the scenario @@ -740,7 +744,7 @@ class VisualServerScene { virtual Vector instances_cull_convex(const Vector &p_convex, RID p_scenario = RID()) const; // internal (uses portals when available) - int _cull_convex_from_point(Scenario *p_scenario, const Vector3 &p_point, const Vector &p_convex, Instance **p_result_array, int p_result_max, int32_t &r_previous_room_id_hint, uint32_t p_mask = 0xFFFFFFFF); + int _cull_convex_from_point(Scenario *p_scenario, const Transform &p_cam_transform, const CameraMatrix &p_cam_projection, const Vector &p_convex, Instance **p_result_array, int p_result_max, int32_t &r_previous_room_id_hint, uint32_t p_mask = 0xFFFFFFFF); void _rooms_instance_update(Instance *p_instance, const AABB &p_aabb); virtual void instance_geometry_set_flag(RID p_instance, VS::InstanceFlags p_flags, bool p_enabled); diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h index 7e04336e161c..328f4d2afd86 100644 --- a/servers/visual/visual_server_wrap_mt.h +++ b/servers/visual/visual_server_wrap_mt.h @@ -508,9 +508,11 @@ class VisualServerWrapMT : public VisualServer { FUNCRID(occluder) FUNC3(occluder_set_scenario, RID, RID, OccluderType) FUNC2(occluder_spheres_update, RID, const Vector &) + FUNC2(occluder_mesh_update, RID, const Geometry::OccluderMeshData &) FUNC2(occluder_set_transform, RID, const Transform &) FUNC2(occluder_set_active, RID, bool) FUNC1(set_use_occlusion_culling, bool) + FUNC1RC(Geometry::MeshData, occlusion_debug_get_current_polys, RID) // Rooms FUNCRID(room) diff --git a/servers/visual_server.cpp b/servers/visual_server.cpp index c96c6377370e..1eadd55cea62 100644 --- a/servers/visual_server.cpp +++ b/servers/visual_server.cpp @@ -2718,6 +2718,8 @@ VisualServer::VisualServer() { // Occlusion culling GLOBAL_DEF("rendering/misc/occlusion_culling/max_active_spheres", 8); ProjectSettings::get_singleton()->set_custom_property_info("rendering/misc/occlusion_culling/max_active_spheres", PropertyInfo(Variant::INT, "rendering/misc/occlusion_culling/max_active_spheres", PROPERTY_HINT_RANGE, "0,64")); + GLOBAL_DEF("rendering/misc/occlusion_culling/max_active_polygons", 8); + ProjectSettings::get_singleton()->set_custom_property_info("rendering/misc/occlusion_culling/max_active_polygons", PropertyInfo(Variant::INT, "rendering/misc/occlusion_culling/max_active_polygons", PROPERTY_HINT_RANGE, "0,64")); // Async. compilation and caching #ifdef DEBUG_ENABLED diff --git a/servers/visual_server.h b/servers/visual_server.h index 06036a7f8991..d04940f5d0d3 100644 --- a/servers/visual_server.h +++ b/servers/visual_server.h @@ -901,15 +901,18 @@ class VisualServer : public Object { enum OccluderType { OCCLUDER_TYPE_UNDEFINED, OCCLUDER_TYPE_SPHERE, + OCCLUDER_TYPE_MESH, OCCLUDER_TYPE_NUM_TYPES, }; virtual RID occluder_create() = 0; virtual void occluder_set_scenario(RID p_occluder, RID p_scenario, VisualServer::OccluderType p_type) = 0; virtual void occluder_spheres_update(RID p_occluder, const Vector &p_spheres) = 0; + virtual void occluder_mesh_update(RID p_occluder, const Geometry::OccluderMeshData &p_mesh_data) = 0; virtual void occluder_set_transform(RID p_occluder, const Transform &p_xform) = 0; virtual void occluder_set_active(RID p_occluder, bool p_active) = 0; virtual void set_use_occlusion_culling(bool p_enable) = 0; + virtual Geometry::MeshData occlusion_debug_get_current_polys(RID p_scenario) const = 0; // Rooms enum RoomsDebugFeature {