Browse Source

Merge pull request #57361 from lawnjelly/occ_poly_only

Rémi Verschelde 3 years ago
parent
commit
b6dbff7621

+ 6 - 1
core/local_vector.h

@@ -38,7 +38,7 @@
 
 template <class T, class U = uint32_t, bool force_trivial = false>
 class LocalVector {
-private:
+protected:
 	U count = 0;
 	U capacity = 0;
 	T *data = nullptr;
@@ -255,4 +255,9 @@ public:
 	}
 };
 
+// Integer default version
+template <class T, class I = int32_t, bool force_trivial = false>
+class LocalVectori : public LocalVector<T, I, force_trivial> {
+};
+
 #endif // LOCAL_VECTOR_H

+ 34 - 0
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<Vector2>
 }
 */
 
+void Geometry::OccluderMeshData::clear() {
+	faces.clear();
+	vertices.clear();
+}
+
+void Geometry::MeshData::clear() {
+	faces.clear();
+	edges.clear();
+	vertices.clear();
+}
+
 void Geometry::MeshData::optimize_vertices() {
 	Map<int, int> vtx_remap;
 
@@ -1363,6 +1375,28 @@ Vector<Geometry::PackRectsResult> Geometry::partial_pack_rects(const Vector<Vect
 	return ret;
 }
 
+// Expects polygon as a triangle fan
+real_t Geometry::find_polygon_area(const Vector3 *p_verts, int p_num_verts) {
+	if (!p_verts || (p_num_verts < 3)) {
+		return 0.0;
+	}
+
+	Face3 f;
+	f.vertex[0] = p_verts[0];
+	f.vertex[1] = p_verts[1];
+	f.vertex[2] = p_verts[1];
+
+	real_t area = 0.0;
+
+	for (int n = 2; n < p_num_verts; n++) {
+		f.vertex[1] = f.vertex[2];
+		f.vertex[2] = p_verts[n];
+		area += Math::sqrt(f.get_twice_area_squared());
+	}
+
+	return area * 0.5;
+}
+
 // adapted from:
 // https://stackoverflow.com/questions/6989100/sort-points-in-clockwise-order
 void Geometry::sort_polygon_winding(Vector<Vector2> &r_verts, bool p_clockwise) {

+ 27 - 2
core/math/geometry.h

@@ -555,11 +555,17 @@ public:
 		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 @@ public:
 		Vector<Vector3> 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<uint32_t> indices;
+		};
+		struct Face {
+			Plane plane;
+			bool two_way = false;
+			LocalVectori<uint32_t> indices;
+			LocalVectori<Hole> holes;
+		};
+		LocalVectori<Face> faces;
+		LocalVectori<Vector3> vertices;
+		void clear();
 	};
 
 	_FORCE_INLINE_ static int get_uv84_normal_bit(const Vector3 &p_vector) {
@@ -1070,6 +1094,7 @@ public:
 	static PoolVector<Plane> build_cylinder_planes(real_t p_radius, real_t p_height, int p_sides, Vector3::Axis p_axis = Vector3::AXIS_Z);
 	static PoolVector<Plane> 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<Vector2> &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<Size2i> &p_rects, Vector<Point2i> &r_result, Size2i &r_size);
 

+ 1 - 0
core/math/math_funcs.h

@@ -181,6 +181,7 @@ public:
 	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);

+ 45 - 0
doc/classes/OccluderShapePolygon.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OccluderShapePolygon" inherits="OccluderShape" version="3.5">
+	<brief_description>
+		Polygon occlusion primitive for use with the [Occluder] node.
+	</brief_description>
+	<description>
+		[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.
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="set_hole_point">
+			<return type="void" />
+			<argument index="0" name="index" type="int" />
+			<argument index="1" name="position" type="Vector2" />
+			<description>
+				Sets an individual hole point position.
+			</description>
+		</method>
+		<method name="set_polygon_point">
+			<return type="void" />
+			<argument index="0" name="index" type="int" />
+			<argument index="1" name="position" type="Vector2" />
+			<description>
+				Sets an individual polygon point position.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="hole_points" type="PoolVector2Array" setter="set_hole_points" getter="get_hole_points" default="PoolVector2Array(  )">
+			Allows changing the hole geometry from code.
+		</member>
+		<member name="polygon_points" type="PoolVector2Array" setter="set_polygon_points" getter="get_polygon_points" default="PoolVector2Array( 1, -1, 1, 1, -1, 1, -1, -1 )">
+			Allows changing the polygon geometry from code.
+		</member>
+		<member name="two_way" type="bool" setter="set_two_way" getter="is_two_way" default="true">
+			Specifies whether the occluder should operate one way only, or from both sides.
+		</member>
+	</members>
+	<constants>
+	</constants>
+</class>

+ 5 - 0
doc/classes/ProjectSettings.xml

@@ -1330,6 +1330,11 @@
 		<member name="rendering/misc/mesh_storage/split_stream" type="bool" setter="" getter="" default="false">
 			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.
 		</member>
+		<member name="rendering/misc/occlusion_culling/max_active_polygons" type="int" setter="" getter="" default="8">
+			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.
+		</member>
 		<member name="rendering/misc/occlusion_culling/max_active_spheres" type="int" setter="" getter="" default="8">
 			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.

+ 4 - 0
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) {

+ 198 - 9
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<Plane> 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<OccluderShapeSphere>(get_occluder_shape());
+	return occ_sphere;
+}
+
+const OccluderShapePolygon *OccluderSpatialGizmo::get_occluder_shape_poly() const {
+	const OccluderShapePolygon *occ_poly = Object::cast_to<OccluderShapePolygon>(get_occluder_shape());
+	return occ_poly;
+}
+
+OccluderShapePolygon *OccluderSpatialGizmo::get_occluder_shape_poly() {
+	OccluderShapePolygon *occ_poly = Object::cast_to<OccluderShapePolygon>(get_occluder_shape());
+	return occ_poly;
+}
+
+const OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() const {
+	const OccluderShapeSphere *occ_sphere = Object::cast_to<OccluderShapeSphere>(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<OccluderShapeSphere>(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<OccluderShapeSphere>(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<Vector2> &p_pts, const PoolVector<Vector2> &p_pts_raw) {
+	PoolVector<Vector3> pts_edge;
+	PoolVector<Color> 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<Vector3> 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<ArrayMesh> 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> material_poly = gizmo_plugin->get_material("occluder_poly", this);
+			add_mesh(mesh, false, Ref<SkinReference>(), material_poly);
+		}
+
+		// handles
+		if (!p_hole) {
+			Ref<Material> material_handle = gizmo_plugin->get_material("occluder_handle", this);
+			add_handles(handles, material_handle);
+		} else {
+			Ref<Material> 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));
 }

+ 13 - 1
editor/spatial_editor_gizmos.h

@@ -505,15 +505,27 @@ public:
 };
 
 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<Vector2> &p_pts, const PoolVector<Vector2> &p_pts_raw);
 
 public:
 	virtual String get_handle_name(int p_idx) const;

+ 39 - 8
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<OccluderShape> 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;
 	}
 }
 

+ 5 - 0
scene/3d/occluder.h

@@ -54,6 +54,11 @@ public:
 
 	String get_configuration_warning() const;
 
+#ifdef TOOLS_ENABLED
+	// for editor gizmo
+	virtual AABB get_fallback_gizmo_aabb() const;
+#endif
+
 	Occluder();
 	~Occluder();
 };

+ 6 - 0
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 {

+ 1 - 0
scene/3d/spatial.h

@@ -167,6 +167,7 @@ public:
 #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);

+ 2 - 0
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<ConcavePolygonShape>();
 	ClassDB::register_virtual_class<OccluderShape>();
 	ClassDB::register_class<OccluderShapeSphere>();
+	ClassDB::register_class<OccluderShapePolygon>();
 
 	OS::get_singleton()->yield(); //may take time to init
 

+ 37 - 0
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<Plane> &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();
 	}
 }

+ 16 - 0
scene/resources/occluder_shape.h

@@ -57,6 +57,12 @@ public:
 	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<Plane> _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 @@ public:
 	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();
 };
 

+ 236 - 0
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<Vector2> &p_from, Vector<Vector2> &r_to) {
+	// remove duplicates? NYI maybe not necessary
+	Vector<Vector2> 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<Vector2> &p_points) {
+	_poly_pts_local_raw = p_points;
+	_sanitize_points();
+	notify_change_to_owners();
+}
+
+void OccluderShapePolygon::set_hole_points(const PoolVector<Vector2> &p_points) {
+	_hole_pts_local_raw = p_points;
+	_sanitize_points();
+	notify_change_to_owners();
+}
+
+PoolVector<Vector2> OccluderShapePolygon::get_polygon_points() const {
+	return _poly_pts_local_raw;
+}
+
+PoolVector<Vector2> 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<Vector2> 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
+}

+ 95 - 0
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<Vector2> _poly_pts_local_raw;
+	PoolVector<Vector2> _hole_pts_local_raw;
+
+	// sanitized
+	Vector<Vector2> _poly_pts_local;
+	Vector<Vector2> _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<Vector2> &p_from, Vector<Vector2> &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<Vector2> &p_points);
+	PoolVector<Vector2> get_polygon_points() const;
+	void set_hole_points(const PoolVector<Vector2> &p_points);
+	PoolVector<Vector2> 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

+ 42 - 0
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

+ 766 - 32
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<uint32_t, uint32_t> &p_occluder_pool_ids, const Vector3 &pt_camera, const LocalVector<Plane> &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

+ 206 - 3
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<Plane> _pts_in;
+		LocalVectori<Plane> _pts_out;
+
+		// after perspective divide
+		LocalVectori<Vector3> _pts_final;
+
+		template <typename T>
+		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<Plane> &p_planes, const Plane *p_near_plane) {
 		if (p_near_plane) {
 			static LocalVector<Plane> local_planes;
@@ -61,16 +103,33 @@ public:
 	}
 
 	void prepare_generic(PortalRenderer &p_portal_renderer, const LocalVector<uint32_t, uint32_t> &p_occluder_pool_ids, const Vector3 &pt_camera, const LocalVector<Plane> &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<Plane> &p_planes) const {
 		for (unsigned int p = 0; p < p_planes.size(); p++) {
@@ -99,10 +158,94 @@ private:
 				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<Plane> &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<Plane> &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 @@ private:
 	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

+ 104 - 1
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<Geometry::OccluderMeshData::Face> &faces = p_mesh_data.faces;
+	const LocalVectori<Vector3> &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<Plane> &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<Plane>());
 		} 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<Vector3> &p_came
 	_gameplay_monitor.update_gameplay(*this, source_rooms, num_source_rooms);
 }
 
-int PortalRenderer::cull_convex_implementation(const Vector3 &p_point, const Vector<Plane> &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<Plane> &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);
 

+ 68 - 9
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 @@ public:
 	// occluders
 	OccluderHandle occluder_create(VSOccluder::Type p_type);
 	void occluder_update_spheres(OccluderHandle p_handle, const Vector<Plane> &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<Plane> &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<Plane> &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<Plane> &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<Plane> &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<Plane> &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<Plane> &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 @@ public:
 	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<uint32_t, uint32_t> &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<uint32_t, uint32_t> &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 @@ private:
 	// occluders
 	TrackedPooledList<VSOccluder> _occluder_pool;
 	TrackedPooledList<VSOccluder_Sphere, uint32_t, true> _occluder_sphere_pool;
+	TrackedPooledList<VSOccluder_Mesh, uint32_t, true> _occluder_mesh_pool;
+	TrackedPooledList<VSOccluder_Hole, uint32_t, true> _occluder_hole_pool;
 
 	PVS _pvs;
 
@@ -327,6 +354,7 @@ public:
 	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

+ 3 - 1
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<Plane> &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<Plane> &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...

+ 5 - 1
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 @@ public:
 
 	// 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<Plane> &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<Plane> &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

+ 54 - 0
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

+ 2 - 0
servers/visual/visual_server_raster.h

@@ -585,9 +585,11 @@ public:
 	BIND0R(RID, occluder_create)
 	BIND3(occluder_set_scenario, RID, RID, OccluderType)
 	BIND2(occluder_spheres_update, RID, const Vector<Plane> &)
+	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)

+ 24 - 6
servers/visual/visual_server_scene.cpp

@@ -1246,12 +1246,28 @@ void VisualServerScene::occluder_spheres_update(RID p_occluder, const Vector<Pla
 	ro->scenario->_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<ObjectID> VisualServerScene::instances_cull_convex(const Vector<Plane> &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<Plane> &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<Plane> &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<Plane> 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<Plane> 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;

+ 5 - 1
servers/visual/visual_server_scene.h

@@ -694,10 +694,14 @@ public:
 	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<Plane> &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 @@ public:
 	virtual Vector<ObjectID> instances_cull_convex(const Vector<Plane> &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<Plane> &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<Plane> &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);

+ 2 - 0
servers/visual/visual_server_wrap_mt.h

@@ -508,9 +508,11 @@ public:
 	FUNCRID(occluder)
 	FUNC3(occluder_set_scenario, RID, RID, OccluderType)
 	FUNC2(occluder_spheres_update, RID, const Vector<Plane> &)
+	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)

+ 2 - 0
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

+ 3 - 0
servers/visual_server.h

@@ -901,15 +901,18 @@ public:
 	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<Plane> &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 {