Sphere occluders (portals and general use)

Add framework for supporting geometrical occluders within rooms, and add support for sphere occluders.
Includes gizmos for editing.

They also work outside the portal system.
This commit is contained in:
lawnjelly 2021-08-05 15:44:43 +01:00
parent 450f7fdc39
commit 115f4dce55
36 changed files with 1825 additions and 7 deletions

View file

@ -61,6 +61,7 @@ private:
uint64_t _physics_frames;
float _physics_interpolation_fraction;
bool _portals_active;
bool _occlusion_culling_active;
uint64_t _idle_frames;
bool _in_physics;

View file

@ -159,6 +159,8 @@ public:
}
}
const LocalVector<uint32_t, uint32_t> &get_active_list() const { return _active_list; }
private:
PooledList<T, force_trivial> _pool;
LocalVector<uint32_t, uint32_t> _active_map;

View file

@ -45,8 +45,10 @@
<return type="void" />
<argument index="0" name="name" type="String" />
<argument index="1" name="billboard" type="bool" default="false" />
<argument index="2" name="texture" type="Texture" default="null" />
<description>
Creates a handle material with its variants (selected and/or editable) and adds them to the internal material list. They can then be accessed with [method get_material] and used in [method EditorSpatialGizmo.add_handles]. Should not be overridden.
You can optionally provide a texture to use instead of the default icon.
</description>
</method>
<method name="create_icon_material">

29
doc/classes/Occluder.xml Normal file
View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="Occluder" inherits="Spatial" version="3.4">
<brief_description>
Allows [OccluderShape]s to be used for occlusion culling.
</brief_description>
<description>
[Occluder]s that are placed within your scene will automatically cull objects that are hidden from view by the occluder. This can increase performance by decreasing the amount of objects drawn.
[Occluder]s are totally dynamic, you can move them as you wish. This means you can for example, place occluders on a moving spaceship, and have it occlude objects as it flies past.
You can place a large number of [Occluder]s within a scene. As it would be counterproductive to cull against hundreds of occluders, the system will automatically choose a selection of these for active use during any given frame, based a screen space metric. Larger occluders are favored, as well as those close to the camera. Note that a small occluder close to the camera may be a better occluder in terms of screen space than a large occluder far in the distance.
The type of occlusion primitive is determined by the [OccluderShape] that you add to the [Occluder]. Some [OccluderShape]s may allow more than one primitive in a single, node, for greater efficiency.
Although [Occluder]s work in general use, they also become even more powerful when used in conjunction with the portal system. Occluders are placed in rooms (based on their origin), and can block portals (and thus entire rooms) as well as objects from rendering.
</description>
<tutorials>
</tutorials>
<methods>
<method name="resource_changed">
<return type="void" />
<argument index="0" name="resource" type="Resource" />
<description>
</description>
</method>
</methods>
<members>
<member name="shape" type="OccluderShape" setter="set_shape" getter="get_shape">
</member>
</members>
<constants>
</constants>
</class>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="OccluderShape" inherits="Resource" version="3.4">
<brief_description>
Base class for shapes used for occlusion culling by the [Occluder] node.
</brief_description>
<description>
[Occluder]s can use any primitive shape derived from [OccluderShape].
</description>
<tutorials>
</tutorials>
<methods>
</methods>
<constants>
</constants>
</class>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="OccluderShapeSphere" inherits="OccluderShape" version="3.4">
<brief_description>
Spherical occlusion primitive for use with the [Occluder] node.
</brief_description>
<description>
[OccluderShape]s are resources used by [Occluder] nodes, allowing geometric occlusion culling.
This shape can include multiple spheres. These can be created and deleted either in the Editor inspector or by calling [code]set_spheres[/code]. The sphere positions can be set by dragging the handle in the Editor viewport. The radius can be set with the smaller handle.
</description>
<tutorials>
</tutorials>
<methods>
<method name="set_sphere_position">
<return type="void" />
<argument index="0" name="index" type="int" />
<argument index="1" name="position" type="Vector3" />
<description>
Sets an individual sphere's position.
</description>
</method>
<method name="set_sphere_radius">
<return type="void" />
<argument index="0" name="index" type="int" />
<argument index="1" name="radius" type="float" />
<description>
Sets an individual sphere's radius.
</description>
</method>
</methods>
<members>
<member name="spheres" type="Array" setter="set_spheres" getter="get_spheres" default="[ ]">
The sphere data can be accessed as an array of [Plane]s. The position of each sphere is stored in the [code]normal[/code], and the radius is stored in the [code]d[/code] value of the plane.
</member>
</members>
<constants>
</constants>
</class>

View file

@ -1242,6 +1242,10 @@
<member name="rendering/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_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.
</member>
<member name="rendering/portals/advanced/flip_imported_portals" type="bool" setter="" getter="" default="false">
The default convention is for portal normals to point outward (face outward) from the source room.
If you accidentally build your level with portals facing the wrong way, this setting can fix the problem.

View file

@ -2531,6 +2531,13 @@
The default value is [code]1.0[/code], which means [code]TIME[/code] will count the real time as it goes by, without narrowing or stretching it.
</description>
</method>
<method name="set_use_occlusion_culling">
<return type="void" />
<argument index="0" name="enable" type="bool" />
<description>
Enables or disables occlusion culling.
</description>
</method>
<method name="shader_create">
<return type="RID" />
<description>

View file

@ -6850,6 +6850,7 @@ EditorNode::EditorNode() {
add_editor_plugin(memnew(BakedLightmapEditorPlugin(this)));
add_editor_plugin(memnew(RoomManagerEditorPlugin(this)));
add_editor_plugin(memnew(RoomEditorPlugin(this)));
add_editor_plugin(memnew(OccluderEditorPlugin(this)));
add_editor_plugin(memnew(PortalEditorPlugin(this)));
add_editor_plugin(memnew(Path2DEditorPlugin(this)));
add_editor_plugin(memnew(PathEditorPlugin(this)));

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7.90625 1a7 7 0 0 0 -1.2988281.1386719 4.5 4.5 0 0 1 3.3925781 4.3613281 4.5 4.5 0 0 1 -4.5 4.5 4.5 4.5 0 0 1 -4.359375-3.3886719 7 7 0 0 0 -.140625 1.3886719 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0 -7-7 7 7 0 0 0 -.09375 0z" fill="#fc9c9c" stroke-width=".365215"/></svg>

After

Width:  |  Height:  |  Size: 363 B

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7.90625 1a7 7 0 0 0 -1.2988281.1386719 4.5 4.5 0 0 1 3.3925781 4.3613281 4.5 4.5 0 0 1 -4.5 4.5 4.5 4.5 0 0 1 -4.359375-3.3886719 7 7 0 0 0 -.140625 1.3886719 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0 -7-7 7 7 0 0 0 -.09375 0z" fill="#ffd684" stroke-width=".365215"/></svg>

After

Width:  |  Height:  |  Size: 363 B

View file

@ -82,6 +82,9 @@ RoomManagerEditorPlugin::RoomManagerEditorPlugin(EditorNode *p_node) {
Ref<PortalGizmoPlugin> portal_gizmo_plugin = Ref<PortalGizmoPlugin>(memnew(PortalGizmoPlugin));
SpatialEditor::get_singleton()->add_gizmo_plugin(portal_gizmo_plugin);
Ref<OccluderGizmoPlugin> occluder_gizmo_plugin = Ref<OccluderGizmoPlugin>(memnew(OccluderGizmoPlugin));
SpatialEditor::get_singleton()->add_gizmo_plugin(occluder_gizmo_plugin);
}
RoomManagerEditorPlugin::~RoomManagerEditorPlugin() {
@ -206,3 +209,78 @@ PortalEditorPlugin::PortalEditorPlugin(EditorNode *p_node) {
PortalEditorPlugin::~PortalEditorPlugin() {
}
///////////////////////
void OccluderEditorPlugin::_center() {
if (_occluder && _occluder->is_inside_tree()) {
Ref<OccluderShape> ref = _occluder->get_shape();
if (ref.is_valid()) {
Spatial *parent = Object::cast_to<Spatial>(_occluder->get_parent());
if (parent) {
real_t snap = 0.0;
if (Engine::get_singleton()->is_editor_hint()) {
if (SpatialEditor::get_singleton() && SpatialEditor::get_singleton()->is_snap_enabled()) {
snap = SpatialEditor::get_singleton()->get_translate_snap();
}
}
Transform old_local_xform = _occluder->get_transform();
Transform new_local_xform = ref->center_node(_occluder->get_global_transform(), parent->get_global_transform(), snap);
_occluder->property_list_changed_notify();
undo_redo->create_action(TTR("Occluder Set Transform"));
undo_redo->add_do_method(_occluder, "set_transform", new_local_xform);
undo_redo->add_undo_method(_occluder, "set_transform", old_local_xform);
undo_redo->commit_action();
_occluder->update_gizmo();
}
}
}
}
void OccluderEditorPlugin::edit(Object *p_object) {
Occluder *p = Object::cast_to<Occluder>(p_object);
if (!p) {
return;
}
_occluder = p;
}
bool OccluderEditorPlugin::handles(Object *p_object) const {
return p_object->is_class("Occluder");
}
void OccluderEditorPlugin::make_visible(bool p_visible) {
if (p_visible) {
button_center->show();
} else {
button_center->hide();
}
}
void OccluderEditorPlugin::_bind_methods() {
ClassDB::bind_method("_center", &OccluderEditorPlugin::_center);
}
OccluderEditorPlugin::OccluderEditorPlugin(EditorNode *p_node) {
editor = p_node;
button_center = memnew(ToolButton);
button_center->set_icon(editor->get_gui_base()->get_icon("EditorPosition", "EditorIcons"));
button_center->set_text(TTR("Center Node"));
button_center->hide();
button_center->connect("pressed", this, "_center");
add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, button_center);
undo_redo = EditorNode::get_undo_redo();
_occluder = nullptr;
}
OccluderEditorPlugin::~OccluderEditorPlugin() {
}

View file

@ -33,6 +33,7 @@
#include "editor/editor_node.h"
#include "editor/editor_plugin.h"
#include "scene/3d/occluder.h"
#include "scene/3d/portal.h"
#include "scene/3d/room.h"
#include "scene/3d/room_manager.h"
@ -113,4 +114,30 @@ public:
~PortalEditorPlugin();
};
///////////////////////
class OccluderEditorPlugin : public EditorPlugin {
GDCLASS(OccluderEditorPlugin, EditorPlugin);
Occluder *_occluder;
ToolButton *button_center;
EditorNode *editor;
UndoRedo *undo_redo;
void _center();
protected:
static void _bind_methods();
public:
virtual String get_name() const { return "Occluder"; }
bool has_main_screen() const { return false; }
virtual void edit(Object *p_object);
virtual bool handles(Object *p_object) const;
virtual void make_visible(bool p_visible);
OccluderEditorPlugin(EditorNode *p_node);
~OccluderEditorPlugin();
};
#endif

View file

@ -5001,6 +5001,13 @@ void SpatialEditor::_menu_item_pressed(int p_option) {
RoomManager::static_rooms_set_active(!is_checked);
update_portal_tools();
} break;
case MENU_VIEW_OCCLUSION_CULLING: {
int checkbox_id = view_menu->get_popup()->get_item_index(p_option);
bool is_checked = view_menu->get_popup()->is_item_checked(checkbox_id);
VisualServer::get_singleton()->set_use_occlusion_culling(!is_checked);
view_menu->get_popup()->set_item_checked(checkbox_id, !is_checked);
} break;
case MENU_VIEW_CAMERA_SETTINGS: {
settings_dialog->popup_centered(settings_vbc->get_combined_minimum_size() + Size2(50, 50));
} break;
@ -6480,12 +6487,14 @@ SpatialEditor::SpatialEditor(EditorNode *p_editor) {
p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_origin", TTR("View Origin")), MENU_VIEW_ORIGIN);
p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid", TTR("View Grid"), KEY_MASK_CMD + KEY_G), MENU_VIEW_GRID);
p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_portal_culling", TTR("View Portal Culling"), KEY_MASK_ALT | KEY_P), MENU_VIEW_PORTAL_CULLING);
p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_occlusion_culling", TTR("View Occlusion Culling")), MENU_VIEW_OCCLUSION_CULLING);
p->add_separator();
p->add_shortcut(ED_SHORTCUT("spatial_editor/settings", TTR("Settings...")), MENU_VIEW_CAMERA_SETTINGS);
p->set_item_checked(p->get_item_index(MENU_VIEW_ORIGIN), true);
p->set_item_checked(p->get_item_index(MENU_VIEW_GRID), true);
p->set_item_checked(p->get_item_index(MENU_VIEW_OCCLUSION_CULLING), true);
p->connect("id_pressed", this, "_menu_item_pressed");
@ -6867,12 +6876,13 @@ void EditorSpatialGizmoPlugin::create_icon_material(const String &p_name, const
materials[p_name] = icons;
}
void EditorSpatialGizmoPlugin::create_handle_material(const String &p_name, bool p_billboard) {
void EditorSpatialGizmoPlugin::create_handle_material(const String &p_name, bool p_billboard, const Ref<Texture> &p_icon) {
Ref<SpatialMaterial> handle_material = Ref<SpatialMaterial>(memnew(SpatialMaterial));
handle_material->set_flag(SpatialMaterial::FLAG_UNSHADED, true);
handle_material->set_flag(SpatialMaterial::FLAG_USE_POINT_SIZE, true);
Ref<Texture> handle_t = SpatialEditor::get_singleton()->get_icon("Editor3DHandle", "EditorIcons");
Ref<Texture> handle_t = p_icon != nullptr ? p_icon : SpatialEditor::get_singleton()->get_icon("Editor3DHandle", "EditorIcons");
handle_material->set_point_size(handle_t->get_width());
handle_material->set_texture(SpatialMaterial::TEXTURE_ALBEDO, handle_t);
handle_material->set_albedo(Color(1, 1, 1));
@ -6956,7 +6966,7 @@ void EditorSpatialGizmoPlugin::_bind_methods() {
ClassDB::bind_method(D_METHOD("create_material", "name", "color", "billboard", "on_top", "use_vertex_color"), &EditorSpatialGizmoPlugin::create_material, DEFVAL(false), DEFVAL(false), DEFVAL(false));
ClassDB::bind_method(D_METHOD("create_icon_material", "name", "texture", "on_top", "color"), &EditorSpatialGizmoPlugin::create_icon_material, DEFVAL(false), DEFVAL(Color(1, 1, 1, 1)));
ClassDB::bind_method(D_METHOD("create_handle_material", "name", "billboard"), &EditorSpatialGizmoPlugin::create_handle_material, DEFVAL(false));
ClassDB::bind_method(D_METHOD("create_handle_material", "name", "billboard", "texture"), &EditorSpatialGizmoPlugin::create_handle_material, DEFVAL(false), DEFVAL(Variant()));
ClassDB::bind_method(D_METHOD("add_material", "name", "material"), &EditorSpatialGizmoPlugin::add_material);
ClassDB::bind_method(D_METHOD("get_material", "name", "gizmo"), &EditorSpatialGizmoPlugin::get_material, DEFVAL(Ref<EditorSpatialGizmo>()));

View file

@ -648,6 +648,7 @@ private:
MENU_VIEW_ORIGIN,
MENU_VIEW_GRID,
MENU_VIEW_PORTAL_CULLING,
MENU_VIEW_OCCLUSION_CULLING,
MENU_VIEW_GIZMOS_3D_ICONS,
MENU_VIEW_CAMERA_SETTINGS,
MENU_LOCK_SELECTED,
@ -869,7 +870,7 @@ protected:
public:
void create_material(const String &p_name, const Color &p_color, bool p_billboard = false, bool p_on_top = false, bool p_use_vertex_color = false);
void create_icon_material(const String &p_name, const Ref<Texture> &p_texture, bool p_on_top = false, const Color &p_albedo = Color(1, 1, 1, 1));
void create_handle_material(const String &p_name, bool p_billboard = false);
void create_handle_material(const String &p_name, bool p_billboard = false, const Ref<Texture> &p_icon = nullptr);
void add_material(const String &p_name, Ref<SpatialMaterial> p_material);
Ref<SpatialMaterial> get_material(const String &p_name, const Ref<EditorSpatialGizmo> &p_gizmo = Ref<EditorSpatialGizmo>());

View file

@ -42,6 +42,7 @@
#include "scene/3d/listener.h"
#include "scene/3d/mesh_instance.h"
#include "scene/3d/navigation_mesh.h"
#include "scene/3d/occluder.h"
#include "scene/3d/particles.h"
#include "scene/3d/physics_joint.h"
#include "scene/3d/portal.h"
@ -60,6 +61,7 @@
#include "scene/resources/convex_polygon_shape.h"
#include "scene/resources/cylinder_shape.h"
#include "scene/resources/height_map_shape.h"
#include "scene/resources/occluder_shape.h"
#include "scene/resources/plane_shape.h"
#include "scene/resources/primitive_meshes.h"
#include "scene/resources/ray_shape.h"
@ -4938,3 +4940,274 @@ PortalSpatialGizmo::PortalSpatialGizmo(Portal *p_portal) {
_color_portal_front = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_front", Color(0.05, 0.05, 1.0, 0.3));
_color_portal_back = EDITOR_DEF("editors/3d_gizmos/gizmo_colors/portal_back", Color(1.0, 1.0, 0.0, 0.15));
}
/////////////////////
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_handle_material("occluder_handle");
create_handle_material("extra_handle", false, SpatialEditor::get_singleton()->get_icon("EditorInternalHandle", "EditorIcons"));
}
Ref<EditorSpatialGizmo> OccluderGizmoPlugin::create_gizmo(Spatial *p_spatial) {
Ref<OccluderSpatialGizmo> ref;
Occluder *occluder = Object::cast_to<Occluder>(p_spatial);
if (occluder) {
ref = Ref<OccluderSpatialGizmo>(memnew(OccluderSpatialGizmo(occluder)));
}
return ref;
}
bool OccluderGizmoPlugin::has_gizmo(Spatial *p_spatial) {
if (Object::cast_to<Occluder>(p_spatial)) {
return true;
}
return false;
}
String OccluderGizmoPlugin::get_name() const {
return "Occluder";
}
int OccluderGizmoPlugin::get_priority() const {
return -1;
}
//////////////////////
String OccluderSpatialGizmo::get_handle_name(int p_idx) const {
const OccluderShapeSphere *occ_sphere = get_occluder_shape_sphere();
if (occ_sphere) {
int num_spheres = occ_sphere->get_spheres().size();
if (p_idx >= num_spheres) {
p_idx -= num_spheres;
return "Radius " + itos(p_idx);
} else {
return "Sphere " + itos(p_idx);
}
}
return "Unknown";
}
Variant OccluderSpatialGizmo::get_handle_value(int p_idx) {
const OccluderShapeSphere *occ_sphere = get_occluder_shape_sphere();
if (occ_sphere) {
Vector<Plane> spheres = occ_sphere->get_spheres();
int num_spheres = spheres.size();
if (p_idx >= num_spheres) {
p_idx -= num_spheres;
return spheres[p_idx].d;
} else {
return spheres[p_idx].normal;
}
}
return 0;
}
void OccluderSpatialGizmo::set_handle(int p_idx, Camera *p_camera, const Point2 &p_point) {
if (!_occluder) {
return;
}
Transform tr = _occluder->get_global_transform();
Transform tr_inv = tr.affine_inverse();
// selection ray
Vector3 ray_from = p_camera->project_ray_origin(p_point);
Vector3 ray_dir = p_camera->project_ray_normal(p_point);
Vector3 camera_dir = p_camera->get_transform().basis.get_axis(2);
// find the smallest camera axis, we will only transform the handles on 2 axes max,
// to try and make things more user friendly (it is confusing trying to change 3d position
// from a 2d view)
int biggest_axis = 0;
real_t biggest = 0.0;
for (int n = 0; n < 3; n++) {
real_t val = Math::abs(camera_dir.get_axis(n));
if (val > biggest) {
biggest = val;
biggest_axis = n;
}
}
// find world space of selected point
OccluderShapeSphere *occ_sphere = get_occluder_shape_sphere();
if (occ_sphere) {
Vector<Plane> spheres = occ_sphere->get_spheres();
int num_spheres = spheres.size();
// radius?
bool is_radius = false;
if (p_idx >= num_spheres) {
p_idx -= num_spheres;
is_radius = true;
}
Vector3 pt_world = spheres[p_idx].normal;
pt_world = tr.xform(pt_world);
Vector3 pt_world_center = pt_world;
// a plane between the radius point and the centre
Plane plane;
if (is_radius) {
plane = Plane(Vector3(0, 0, 1), pt_world.z);
} else {
plane = Plane(pt_world, camera_dir);
}
Vector3 inters;
if (plane.intersects_ray(ray_from, ray_dir, &inters)) {
if (SpatialEditor::get_singleton()->is_snap_enabled()) {
float snap = SpatialEditor::get_singleton()->get_translate_snap();
inters.snap(Vector3(snap, snap, snap));
}
if (is_radius) {
pt_world = inters;
// new radius is simply the dist between this point and the centre of the sphere
real_t radius = (pt_world - pt_world_center).length();
occ_sphere->set_sphere_radius(p_idx, radius);
} else {
for (int n = 0; n < 3; n++) {
if (n != biggest_axis) {
pt_world.set_axis(n, inters.get_axis(n));
}
}
Vector3 pt_local = tr_inv.xform(pt_world);
occ_sphere->set_sphere_position(p_idx, pt_local);
}
return;
}
}
}
void OccluderSpatialGizmo::commit_handle(int p_idx, const Variant &p_restore, bool p_cancel) {
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;
ur->create_action(TTR("Set Occluder Sphere Radius"));
ur->add_do_method(occ_sphere, "set_sphere_radius", p_idx, spheres[p_idx].d);
ur->add_undo_method(occ_sphere, "set_sphere_radius", p_idx, p_restore);
} else {
ur->create_action(TTR("Set Occluder Sphere Position"));
ur->add_do_method(occ_sphere, "set_sphere_position", p_idx, spheres[p_idx].normal);
ur->add_undo_method(occ_sphere, "set_sphere_position", p_idx, p_restore);
}
ur->commit_action();
_occluder->property_list_changed_notify();
}
}
OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() {
if (!_occluder) {
return nullptr;
}
Ref<OccluderShape> rshape = _occluder->get_shape();
if (rshape.is_null() || !rshape.is_valid()) {
return nullptr;
}
OccluderShape *shape = rshape.ptr();
OccluderShapeSphere *occ_sphere = Object::cast_to<OccluderShapeSphere>(shape);
return occ_sphere;
}
const OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() const {
if (!_occluder) {
return nullptr;
}
Ref<OccluderShape> rshape = _occluder->get_shape();
if (rshape.is_null() || !rshape.is_valid()) {
return nullptr;
}
const OccluderShape *shape = rshape.ptr();
const OccluderShapeSphere *occ_sphere = Object::cast_to<OccluderShapeSphere>(shape);
return occ_sphere;
}
void OccluderSpatialGizmo::redraw() {
clear();
if (!_occluder) {
return;
}
Ref<Material> material_occluder = gizmo_plugin->get_material("occluder", this);
Color color(1, 1, 1, 1);
const OccluderShapeSphere *occ_sphere = get_occluder_shape_sphere();
if (occ_sphere) {
Vector<Plane> spheres = occ_sphere->get_spheres();
if (!spheres.size()) {
return;
}
Vector<Vector3> points;
Vector<Vector3> handles;
Vector<Vector3> radius_handles;
for (int n = 0; n < spheres.size(); n++) {
const Plane &p = spheres[n];
real_t r = p.d;
Vector3 offset = p.normal;
handles.push_back(offset);
// add a handle for the radius
radius_handles.push_back(offset + Vector3(r, 0, 0));
const int deg_change = 4;
for (int i = 0; i <= 360; i += deg_change) {
real_t ra = Math::deg2rad((real_t)i);
real_t rb = Math::deg2rad((real_t)i + deg_change);
Point2 a = Vector2(Math::sin(ra), Math::cos(ra)) * r;
Point2 b = Vector2(Math::sin(rb), Math::cos(rb)) * r;
points.push_back(offset + Vector3(a.x, 0, a.y));
points.push_back(offset + Vector3(b.x, 0, b.y));
points.push_back(offset + Vector3(0, a.x, a.y));
points.push_back(offset + Vector3(0, b.x, b.y));
points.push_back(offset + Vector3(a.x, a.y, 0));
points.push_back(offset + Vector3(b.x, b.y, 0));
}
} // for n through spheres
add_lines(points, material_occluder, false, color);
// handles
Ref<Material> material_handle = gizmo_plugin->get_material("occluder_handle", this);
Ref<Material> material_extra_handle = gizmo_plugin->get_material("extra_handle", this);
add_handles(handles, material_handle);
add_handles(radius_handles, material_extra_handle, false, true);
}
}
OccluderSpatialGizmo::OccluderSpatialGizmo(Occluder *p_occluder) {
_occluder = p_occluder;
set_spatial_node(p_occluder);
}

View file

@ -491,4 +491,38 @@ public:
PortalGizmoPlugin();
};
class Occluder;
class OccluderShapeSphere;
class OccluderSpatialGizmo : public EditorSpatialGizmo {
GDCLASS(OccluderSpatialGizmo, EditorSpatialGizmo);
Occluder *_occluder = nullptr;
OccluderShapeSphere *get_occluder_shape_sphere();
const OccluderShapeSphere *get_occluder_shape_sphere() const;
public:
virtual String get_handle_name(int p_idx) const;
virtual Variant get_handle_value(int p_idx);
virtual void set_handle(int p_idx, Camera *p_camera, const Point2 &p_point);
virtual void commit_handle(int p_idx, const Variant &p_restore, bool p_cancel = false);
virtual void redraw();
OccluderSpatialGizmo(Occluder *p_occluder = nullptr);
};
class OccluderGizmoPlugin : public EditorSpatialGizmoPlugin {
GDCLASS(OccluderGizmoPlugin, EditorSpatialGizmoPlugin);
protected:
virtual bool has_gizmo(Spatial *p_spatial);
String get_name() const;
int get_priority() const;
Ref<EditorSpatialGizmo> create_gizmo(Spatial *p_spatial);
public:
OccluderGizmoPlugin();
};
#endif // SPATIAL_EDITOR_GIZMOS_H

150
scene/3d/occluder.cpp Normal file
View file

@ -0,0 +1,150 @@
/*************************************************************************/
/* occluder.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "occluder.h"
#include "core/engine.h"
void Occluder::resource_changed(RES res) {
update_gizmo();
if (_shape.is_valid()) {
_shape->update_shape_to_visual_server();
}
}
void Occluder::set_shape(const Ref<OccluderShape> &p_shape) {
if (p_shape == _shape) {
return;
}
if (!_shape.is_null()) {
_shape->unregister_owner(this);
}
_shape = p_shape;
if (_shape.is_valid()) {
_shape->register_owner(this);
// Especially in the editor, the shape can be added AFTER the occluder
// is added to the world. This means the shape scenario will not be set.
// To remedy this we make sure the scenario etc is refreshed as soon as
// a shape is set on the occluder.
if (is_inside_world() && get_world().is_valid()) {
_shape->notification_enter_world(get_world()->get_scenario());
_shape->update_shape_to_visual_server();
if (is_inside_tree()) {
_shape->update_active_to_visual_server(is_visible_in_tree());
_shape->update_transform_to_visual_server(get_global_transform());
}
}
}
update_gizmo();
update_configuration_warning();
}
Ref<OccluderShape> Occluder::get_shape() const {
return _shape;
}
String Occluder::get_configuration_warning() const {
String warning = Spatial::get_configuration_warning();
if (!_shape.is_valid()) {
if (!warning.empty()) {
warning += "\n\n";
}
warning += TTR("No shape is set.");
}
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.");
}
return warning;
}
void Occluder::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_WORLD: {
ERR_FAIL_COND(get_world().is_null());
if (_shape.is_valid()) {
_shape->notification_enter_world(get_world()->get_scenario());
_shape->update_active_to_visual_server(is_visible_in_tree());
_shape->update_shape_to_visual_server();
_shape->update_transform_to_visual_server(get_global_transform());
}
} break;
case NOTIFICATION_EXIT_WORLD: {
if (_shape.is_valid()) {
_shape->notification_exit_world();
}
} break;
case NOTIFICATION_VISIBILITY_CHANGED: {
if (_shape.is_valid() && is_inside_tree()) {
_shape->update_active_to_visual_server(is_visible_in_tree());
}
} break;
case NOTIFICATION_TRANSFORM_CHANGED: {
if (_shape.is_valid()) {
_shape->update_transform_to_visual_server(get_global_transform());
#ifdef TOOLS_ENABLED
if (Engine::get_singleton()->is_editor_hint()) {
update_configuration_warning();
}
#endif
}
} break;
}
}
void Occluder::_bind_methods() {
ClassDB::bind_method(D_METHOD("resource_changed", "resource"), &Occluder::resource_changed);
ClassDB::bind_method(D_METHOD("set_shape", "shape"), &Occluder::set_shape);
ClassDB::bind_method(D_METHOD("get_shape"), &Occluder::get_shape);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shape", PROPERTY_HINT_RESOURCE_TYPE, "OccluderShape"), "set_shape", "get_shape");
}
Occluder::Occluder() {
set_notify_transform(true);
}
Occluder::~Occluder() {
if (!_shape.is_null()) {
_shape->unregister_owner(this);
}
}

61
scene/3d/occluder.h Normal file
View file

@ -0,0 +1,61 @@
/*************************************************************************/
/* occluder.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#ifndef OCCLUDER_H
#define OCCLUDER_H
#include "scene/3d/spatial.h"
#include "scene/resources/occluder_shape.h"
class Occluder : public Spatial {
GDCLASS(Occluder, Spatial);
friend class OccluderSpatialGizmo;
friend class OccluderEditorPlugin;
Ref<OccluderShape> _shape;
void resource_changed(RES res);
protected:
void _notification(int p_what);
static void _bind_methods();
public:
void set_shape(const Ref<OccluderShape> &p_shape);
Ref<OccluderShape> get_shape() const;
String get_configuration_warning() const;
Occluder();
~Occluder();
};
#endif // OCCLUDER_H

View file

@ -191,6 +191,7 @@
#include "scene/3d/multimesh_instance.h"
#include "scene/3d/navigation.h"
#include "scene/3d/navigation_mesh.h"
#include "scene/3d/occluder.h"
#include "scene/3d/particles.h"
#include "scene/3d/path.h"
#include "scene/3d/physics_body.h"
@ -213,6 +214,7 @@
#include "scene/animation/skeleton_ik.h"
#include "scene/resources/environment.h"
#include "scene/resources/mesh_library.h"
#include "scene/resources/occluder_shape.h"
#endif
static Ref<ResourceFormatSaverText> resource_saver_text;
@ -438,6 +440,7 @@ void register_scene_types() {
ClassDB::register_class<Room>();
ClassDB::register_class<RoomGroup>();
ClassDB::register_class<RoomManager>();
ClassDB::register_class<Occluder>();
ClassDB::register_class<Portal>();
ClassDB::register_class<RootMotionView>();
@ -649,6 +652,8 @@ void register_scene_types() {
ClassDB::register_class<PlaneShape>();
ClassDB::register_class<ConvexPolygonShape>();
ClassDB::register_class<ConcavePolygonShape>();
ClassDB::register_virtual_class<OccluderShape>();
ClassDB::register_class<OccluderShapeSphere>();
OS::get_singleton()->yield(); //may take time to init

View file

@ -0,0 +1,216 @@
/*************************************************************************/
/* occluder_shape.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "occluder_shape.h"
#include "core/engine.h"
#include "core/math/transform.h"
#include "servers/visual_server.h"
#ifdef TOOLS_ENABLED
#include "editor/editor_node.h"
#endif
void OccluderShape::_bind_methods() {
}
OccluderShape::OccluderShape(RID p_shape) {
_shape = p_shape;
}
OccluderShape::~OccluderShape() {
if (_shape != RID()) {
VisualServer::get_singleton()->free(_shape);
}
}
void OccluderShape::update_transform_to_visual_server(const Transform &p_global_xform) {
VisualServer::get_singleton()->occluder_set_transform(get_shape(), p_global_xform);
}
void OccluderShape::update_active_to_visual_server(bool p_active) {
VisualServer::get_singleton()->occluder_set_active(get_shape(), p_active);
}
void OccluderShape::notification_exit_world() {
VisualServer::get_singleton()->occluder_set_scenario(_shape, RID(), VisualServer::OCCLUDER_TYPE_UNDEFINED);
}
//////////////////////////////////////////////
void OccluderShapeSphere::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_spheres", "spheres"), &OccluderShapeSphere::set_spheres);
ClassDB::bind_method(D_METHOD("get_spheres"), &OccluderShapeSphere::get_spheres);
ClassDB::bind_method(D_METHOD("set_sphere_position", "index", "position"), &OccluderShapeSphere::set_sphere_position);
ClassDB::bind_method(D_METHOD("set_sphere_radius", "index", "radius"), &OccluderShapeSphere::set_sphere_radius);
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "spheres", PROPERTY_HINT_NONE, itos(Variant::PLANE) + ":"), "set_spheres", "get_spheres");
}
void OccluderShapeSphere::update_shape_to_visual_server() {
VisualServer::get_singleton()->occluder_spheres_update(get_shape(), _spheres);
}
Transform OccluderShapeSphere::center_node(const Transform &p_global_xform, const Transform &p_parent_xform, real_t p_snap) {
if (!_spheres.size()) {
return Transform();
}
// make sure world spheres correct
Vector<Plane> spheres_world_space;
if (spheres_world_space.size() != _spheres.size()) {
spheres_world_space.resize(_spheres.size());
}
Vector3 scale3 = p_global_xform.basis.get_scale_abs();
real_t scale = (scale3.x + scale3.y + scale3.z) / 3.0;
for (int n = 0; n < _spheres.size(); n++) {
Plane p;
p.normal = p_global_xform.xform(_spheres[n].normal);
p.d = _spheres[n].d * scale;
spheres_world_space.set(n, p);
}
// first find the center
AABB bb;
bb.set_position(spheres_world_space[0].normal);
// new positions
for (int n = 0; n < spheres_world_space.size(); n++) {
const Plane &sphere = spheres_world_space[n];
// update aabb
AABB sphere_bb(sphere.normal, Vector3());
sphere_bb.grow_by(sphere.d);
bb.merge_with(sphere_bb);
}
Vector3 center = bb.get_center();
// snapping
if (p_snap > 0.0001) {
center.snap(Vector3(p_snap, p_snap, p_snap));
}
// new transform with no rotate or scale, centered
Transform new_local_xform = Transform();
new_local_xform.translate(center.x, center.y, center.z);
Transform inv_xform = new_local_xform.affine_inverse();
// back calculate the new spheres
for (int n = 0; n < spheres_world_space.size(); n++) {
Plane p = spheres_world_space[n];
p.normal = inv_xform.xform(p.normal);
// assuming uniform scale, otherwise this will go wrong
Vector3 inv_scale = inv_xform.basis.get_scale_abs();
p.d *= inv_scale.x;
spheres_world_space.set(n, p);
}
#ifdef TOOLS_ENABLED
if (Engine::get_singleton()->is_editor_hint()) {
UndoRedo *undo_redo = EditorNode::get_undo_redo();
undo_redo->create_action(TTR("OccluderShapeSphere Set Spheres"));
undo_redo->add_do_method(this, "set_spheres", spheres_world_space);
undo_redo->add_undo_method(this, "set_spheres", _spheres);
undo_redo->commit_action();
} else {
set_spheres(spheres_world_space);
}
#else
set_spheres(spheres_world_space);
#endif
notify_change_to_owners();
return new_local_xform;
}
void OccluderShapeSphere::notification_enter_world(RID p_scenario) {
VisualServer::get_singleton()->occluder_set_scenario(get_shape(), p_scenario, VisualServer::OCCLUDER_TYPE_SPHERE);
}
void OccluderShapeSphere::set_spheres(const Vector<Plane> &p_spheres) {
#ifdef TOOLS_ENABLED
// try and detect special circumstance of adding a new sphere in the editor
bool adding_in_editor = false;
if ((p_spheres.size() == _spheres.size() + 1) && (p_spheres[p_spheres.size() - 1] == Plane())) {
adding_in_editor = true;
}
#endif
_spheres = p_spheres;
// sanitize radii
for (int n = 0; n < _spheres.size(); n++) {
if (_spheres[n].d < _min_radius) {
Plane p = _spheres[n];
p.d = _min_radius;
_spheres.set(n, p);
}
}
#ifdef TOOLS_ENABLED
if (adding_in_editor) {
_spheres.set(_spheres.size() - 1, Plane(Vector3(), 1.0));
}
#endif
notify_change_to_owners();
}
void OccluderShapeSphere::set_sphere_position(int p_idx, const Vector3 &p_position) {
if ((p_idx >= 0) && (p_idx < _spheres.size())) {
Plane p = _spheres[p_idx];
p.normal = p_position;
_spheres.set(p_idx, p);
notify_change_to_owners();
}
}
void OccluderShapeSphere::set_sphere_radius(int p_idx, real_t p_radius) {
if ((p_idx >= 0) && (p_idx < _spheres.size())) {
Plane p = _spheres[p_idx];
p.d = MAX(p_radius, _min_radius);
_spheres.set(p_idx, p);
notify_change_to_owners();
}
}
OccluderShapeSphere::OccluderShapeSphere() :
OccluderShape(VisualServer::get_singleton()->occluder_create()) {
}

View file

@ -0,0 +1,86 @@
/*************************************************************************/
/* occluder_shape.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#ifndef OCCLUDER_SHAPE_H
#define OCCLUDER_SHAPE_H
#include "core/math/plane.h"
#include "core/resource.h"
#include "core/vector.h"
class OccluderShape : public Resource {
GDCLASS(OccluderShape, Resource);
OBJ_SAVE_TYPE(OccluderShape);
RES_BASE_EXTENSION("occ");
RID _shape;
protected:
static void _bind_methods();
RID get_shape() const { return _shape; }
OccluderShape(RID p_shape);
public:
virtual RID get_rid() const { return _shape; }
~OccluderShape();
virtual void notification_enter_world(RID p_scenario) = 0;
virtual void update_shape_to_visual_server() = 0;
void update_transform_to_visual_server(const Transform &p_global_xform);
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;
};
class OccluderShapeSphere : public OccluderShape {
GDCLASS(OccluderShapeSphere, OccluderShape);
// We bandit a plane to store position / radius
Vector<Plane> _spheres;
const real_t _min_radius = 0.1;
protected:
static void _bind_methods();
public:
void set_spheres(const Vector<Plane> &p_spheres);
Vector<Plane> get_spheres() const { return _spheres; }
void set_sphere_position(int p_idx, const Vector3 &p_position);
void set_sphere_radius(int p_idx, real_t p_radius);
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);
OccluderShapeSphere();
};
#endif // OCCLUDER_SHAPE_H

View file

@ -0,0 +1,185 @@
/*************************************************************************/
/* portal_occlusion_culler.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "portal_occlusion_culler.h"
#include "core/project_settings.h"
#include "portal_renderer.h"
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, const Plane *p_near_plane) {
_num_spheres = 0;
_pt_camera = pt_camera;
real_t goodness_of_fit[MAX_SPHERES];
for (int n = 0; n < _max_spheres; n++) {
goodness_of_fit[n] = 0.0;
}
real_t weakest_fit = 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...
// find sphere 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);
// is it active?
// in the case of rooms, they will always be active, as inactive
// are removed from rooms. But for whole scene mode, some may be inactive.
if (!occ.active) {
continue;
}
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);
// multiple spheres
for (int n = 0; n < occ.list_ids.size(); n++) {
const Occlusion::Sphere &occluder_sphere = p_portal_renderer.get_pool_occluder_sphere(occ.list_ids[n]).world;
// is the occluder sphere culled?
if (is_sphere_culled(occluder_sphere.pos, occluder_sphere.radius, p_planes, p_near_plane)) {
continue;
}
real_t dist = (occluder_sphere.pos - pt_camera).length();
// keep a record of the closest sphere for quick rejects
if (dist < _sphere_closest_dist) {
_sphere_closest_dist = dist;
}
// 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);
fit *= occluder_sphere.radius;
// until we reach the max, just keep recording, and keep track
// of the worst fit
if (_num_spheres < _max_spheres) {
_spheres[_num_spheres] = occluder_sphere;
_sphere_distances[_num_spheres] = dist;
goodness_of_fit[_num_spheres] = fit;
if (fit < weakest_fit) {
weakest_fit = fit;
weakest_sphere = _num_spheres;
}
_num_spheres++;
} else {
// must beat the weakest
if (fit > weakest_fit) {
_spheres[weakest_sphere] = occluder_sphere;
_sphere_distances[weakest_sphere] = dist;
goodness_of_fit[weakest_sphere] = fit;
// the weakest may have changed (this could be done more efficiently)
weakest_fit = FLT_MAX;
for (int s = 0; s < _max_spheres; s++) {
if (goodness_of_fit[s] < weakest_fit) {
weakest_fit = goodness_of_fit[s];
weakest_sphere = s;
}
}
}
}
}
} // sphere
} // for o
// 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);
}
bool PortalOcclusionCuller::cull_sphere(const Vector3 &p_occludee_center, real_t p_occludee_radius) const {
if (!_num_spheres) {
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;
// 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) {
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) {
// can't occlude
continue;
}
// the perspective adjusted occludee radius
real_t adjusted_occludee_radius = p_occludee_radius * (occluder_dist_to_cam / dist_to_occludee);
const Occlusion::Sphere &occluder_sphere = _spheres[s];
real_t occluder_radius = occluder_sphere.radius - adjusted_occludee_radius;
if (occluder_radius > 0.0) {
occluder_radius = occluder_radius * occluder_radius;
// distance to hit
real_t dist;
if (occluder_sphere.intersect_ray(_pt_camera, ray_dir, dist, occluder_radius)) {
if (dist < dist_to_occludee) {
// occluded
return true;
}
}
} // expanded occluder radius is more than 0
}
return false;
}
PortalOcclusionCuller::PortalOcclusionCuller() {
_max_spheres = GLOBAL_GET("rendering/misc/occlusion_culling/max_active_spheres");
}

View file

@ -0,0 +1,89 @@
/*************************************************************************/
/* portal_occlusion_culler.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#ifndef PORTAL_OCCLUSION_CULLER_H
#define PORTAL_OCCLUSION_CULLER_H
class PortalRenderer;
#include "portal_types.h"
class PortalOcclusionCuller {
enum {
MAX_SPHERES = 64,
};
public:
PortalOcclusionCuller();
void prepare(PortalRenderer &p_portal_renderer, const VSRoom &p_room, const Vector3 &pt_camera, const LocalVector<Plane> &p_planes, const Plane *p_near_plane) {
prepare_generic(p_portal_renderer, p_room._occluder_pool_ids, pt_camera, p_planes, p_near_plane);
}
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, const Plane *p_near_plane);
bool cull_aabb(const AABB &p_aabb) const {
if (!_num_spheres) {
return false;
}
return cull_sphere(p_aabb.get_center(), p_aabb.size.length() * 0.5);
}
bool cull_sphere(const Vector3 &p_occludee_center, real_t p_occludee_radius) const;
private:
// 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 Plane *p_near_plane) const {
if (p_near_plane) {
real_t dist = p_near_plane->distance_to(p_pos);
if (dist > p_radius) {
return true;
}
}
for (unsigned int p = 0; p < p_planes.size(); p++) {
real_t dist = p_planes[p].distance_to(p_pos);
if (dist > p_radius) {
return true;
}
}
return false;
}
// 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];
real_t _sphere_distances[MAX_SPHERES];
real_t _sphere_closest_dist = 0.0;
int _num_spheres = 0;
int _max_spheres = 8;
Vector3 _pt_camera;
};
#endif // PORTAL_OCCLUSION_CULLER_H

View file

@ -34,6 +34,8 @@
#include "servers/visual/visual_server_globals.h"
#include "servers/visual/visual_server_scene.h"
bool PortalRenderer::use_occlusion_culling = true;
OcclusionHandle PortalRenderer::instance_moving_create(VSInstance *p_instance, RID p_instance_rid, bool p_global, AABB p_aabb) {
uint32_t pool_id = 0;
Moving *moving = _moving_pool.request(pool_id);
@ -114,6 +116,14 @@ void PortalRenderer::_rghost_remove_from_rooms(uint32_t p_pool_id) {
moving._rooms.clear();
}
void PortalRenderer::_occluder_remove_from_rooms(uint32_t p_pool_id) {
VSOccluder &occ = _occluder_pool[p_pool_id];
if (_loaded && (occ.room_id != -1)) {
VSRoom &room = get_room(occ.room_id);
room.remove_occluder(p_pool_id);
}
}
void PortalRenderer::_moving_remove_from_rooms(uint32_t p_moving_pool_id) {
Moving &moving = _moving_pool[p_moving_pool_id];
@ -281,6 +291,16 @@ void PortalRenderer::portal_set_geometry(PortalHandle p_portal, const Vector<Vec
// record the center for use in PVS
portal._pt_center = average_pt;
// calculate bounding sphere radius
portal._bounding_sphere_radius = 0.0;
for (unsigned int n = 0; n < portal._pts_world.size(); n++) {
real_t sl = (portal._pts_world[n] - average_pt).length_squared();
if (sl > portal._bounding_sphere_radius) {
portal._bounding_sphere_radius = sl;
}
}
portal._bounding_sphere_radius = Math::sqrt(portal._bounding_sphere_radius);
// use the average point and normal to derive the plane
portal._plane = Plane(average_pt, average_normal);
@ -447,6 +467,138 @@ void PortalRenderer::rghost_destroy(RGhostHandle p_handle) {
_rghost_pool.free(p_handle);
}
OccluderHandle PortalRenderer::occluder_create(VSOccluder::Type p_type) {
uint32_t pool_id = 0;
VSOccluder *occ = _occluder_pool.request(pool_id);
occ->create();
// specific type
occ->type = p_type;
CRASH_COND(p_type == VSOccluder::OT_UNDEFINED);
OccluderHandle handle = pool_id + 1;
return handle;
}
void PortalRenderer::occluder_set_active(OccluderHandle p_handle, bool p_active) {
p_handle--;
VSOccluder &occ = _occluder_pool[p_handle];
if (occ.active == p_active) {
return;
}
occ.active = p_active;
// this will take care of adding or removing from rooms
occluder_refresh_room_within(p_handle);
}
void PortalRenderer::occluder_set_transform(OccluderHandle p_handle, const Transform &p_xform) {
p_handle--;
VSOccluder &occ = _occluder_pool[p_handle];
occ.xform = p_xform;
// mark as dirty as the world space spheres will be out of date
occ.dirty = true;
occluder_refresh_room_within(p_handle);
}
void PortalRenderer::occluder_refresh_room_within(uint32_t p_occluder_pool_id) {
VSOccluder &occ = _occluder_pool[p_occluder_pool_id];
// if we aren't loaded, the room within can't be valid
if (!_loaded) {
occ.room_id = -1;
return;
}
// inactive?
if (!occ.active) {
// remove from any rooms present in
if (occ.room_id != -1) {
_occluder_remove_from_rooms(p_occluder_pool_id);
occ.room_id = -1;
}
return;
}
// prevent checks with no significant changes
Vector3 offset = occ.xform.origin - occ.pt_center;
// could possibly make this epsilon editable?
// is highly world size dependent.
if ((offset.length_squared() < 0.01) && (occ.room_id != -1)) {
return;
}
// standardize on the node origin for now
occ.pt_center = occ.xform.origin;
int new_room = find_room_within(occ.pt_center, occ.room_id);
if (new_room != occ.room_id) {
_occluder_remove_from_rooms(p_occluder_pool_id);
occ.room_id = new_room;
if (new_room != -1) {
VSRoom &room = get_room(new_room);
room.add_occluder(p_occluder_pool_id);
}
}
}
void PortalRenderer::occluder_update_spheres(OccluderHandle p_handle, const Vector<Plane> &p_spheres) {
p_handle--;
VSOccluder &occ = _occluder_pool[p_handle];
ERR_FAIL_COND(occ.type != VSOccluder::OT_SPHERE);
// first deal with the situation where the number of spheres has changed (rare)
if (occ.list_ids.size() != p_spheres.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_sphere_pool.free(id);
}
occ.list_ids.clear();
// create new
for (int n = 0; n < p_spheres.size(); n++) {
uint32_t id;
VSOccluder_Sphere *sphere = _occluder_sphere_pool.request(id);
sphere->create();
occ.list_ids.push_back(id);
}
}
// new positions
for (int n = 0; n < occ.list_ids.size(); n++) {
uint32_t id = occ.list_ids[n];
VSOccluder_Sphere &sphere = _occluder_sphere_pool[id];
sphere.local.from_plane(p_spheres[n]);
}
// mark as dirty as the world space spheres will be out of date
occ.dirty = true;
}
void PortalRenderer::occluder_destroy(OccluderHandle p_handle) {
p_handle--;
// depending on the occluder type, remove the spheres etc
VSOccluder &occ = _occluder_pool[p_handle];
switch (occ.type) {
case VSOccluder::OT_SPHERE: {
occluder_update_spheres(p_handle + 1, Vector<Plane>());
} break;
default: {
} break;
}
_occluder_remove_from_rooms(p_handle);
_occluder_pool.free(p_handle);
}
// Rooms
RoomHandle PortalRenderer::room_create() {
uint32_t pool_id = 0;
@ -808,6 +960,19 @@ void PortalRenderer::_load_finalize_roaming() {
rghost_update(_rghost_pool.get_active_id(n) + 1, aabb, true);
}
for (int n = 0; n < _occluder_pool.active_size(); n++) {
VSOccluder &occ = _occluder_pool.get_active(n);
int occluder_id = _occluder_pool.get_active_id(n);
// make sure occluder is in the correct room
occ.room_id = find_room_within(occ.pt_center, -1);
if (occ.room_id != -1) {
VSRoom &room = get_room(occ.room_id);
room.add_occluder(occluder_id);
}
}
}
void PortalRenderer::sprawl_roaming(uint32_t p_mover_pool_id, MovingBase &r_moving, int p_room_id, bool p_moving_or_ghost) {

View file

@ -31,6 +31,7 @@
#ifndef PORTAL_RENDERER_H
#define PORTAL_RENDERER_H
#include "core/math/plane.h"
#include "core/pooled_list.h"
#include "core/vector.h"
#include "portal_gameplay_monitor.h"
@ -39,6 +40,8 @@
#include "portal_tracer.h"
#include "portal_types.h"
class Transform;
struct VSStatic {
// the lifetime of statics is not strictly monitored like moving objects
// therefore we store a RID which could return NULL if the object has been deleted
@ -74,6 +77,7 @@ public:
// in which case, deleting such an instance should deactivate the portal system to prevent
// crashes due to dangling references to instances.
static const uint32_t OCCLUSION_HANDLE_ROOM_BIT = 1 << 31;
static bool use_occlusion_culling;
struct MovingBase {
// when the rooms_and_portals_clear message is sent,
@ -174,6 +178,13 @@ public:
void rghost_update(RGhostHandle p_handle, const AABB &p_aabb, bool p_force_reinsert = false);
void rghost_destroy(RGhostHandle p_handle);
// occluders
OccluderHandle occluder_create(VSOccluder::Type p_type);
void occluder_update_spheres(OccluderHandle p_handle, const Vector<Plane> &p_spheres);
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);
// 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
@ -186,6 +197,16 @@ public:
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);
// 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) {
// 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);
}
bool is_active() const { return _active && _loaded; }
VSStatic &get_static(int p_id) { return _statics[p_id]; }
@ -208,6 +229,11 @@ 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 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(); }
VSStaticGhost &get_static_ghost(uint32_t p_id) { return _static_ghosts[p_id]; }
VSRoomGroup &get_roomgroup(uint32_t p_pool_id) { return _roomgroup_pool[p_pool_id]; }
@ -230,6 +256,7 @@ private:
void sprawl_roaming(uint32_t p_mover_pool_id, MovingBase &r_moving, int p_room_id, bool p_moving_or_ghost);
void _moving_remove_from_rooms(uint32_t p_moving_pool_id);
void _rghost_remove_from_rooms(uint32_t p_pool_id);
void _occluder_remove_from_rooms(uint32_t p_pool_id);
void _ensure_unloaded();
void _rooms_add_portals_to_convex_hulls();
void _add_portal_to_convex_hull(LocalVector<Plane, int32_t> &p_planes, const Plane &p);
@ -259,6 +286,10 @@ private:
LocalVector<uint32_t, int32_t> _moving_list_global;
LocalVector<uint32_t, int32_t> _moving_list_roaming;
// occluders
TrackedPooledList<VSOccluder> _occluder_pool;
TrackedPooledList<VSOccluder_Sphere> _occluder_sphere_pool;
PVS _pvs;
bool _active = true;
@ -288,6 +319,31 @@ private:
public:
static String _rid_to_string(RID p_rid);
static String _addr_to_string(const void *p_addr);
void occluder_ensure_up_to_date_sphere(VSOccluder &r_occluder);
void occluder_refresh_room_within(uint32_t p_occluder_pool_id);
};
inline void PortalRenderer::occluder_ensure_up_to_date_sphere(VSOccluder &r_occluder) {
if (!r_occluder.dirty) {
return;
}
r_occluder.dirty = false;
const Transform &tr = r_occluder.xform;
Vector3 scale3 = tr.basis.get_scale_abs();
real_t scale = (scale3.x + scale3.y + scale3.z) / 3.0;
// transform spheres
for (int n = 0; n < r_occluder.list_ids.size(); n++) {
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;
}
}
#endif

View file

@ -168,10 +168,12 @@ void PortalTracer::cull_roamers(const VSRoom &p_room, const LocalVector<Plane> &
}
if (test_cull_inside(moving.exact_aabb, p_planes)) {
// mark as done (and on visible list)
moving.last_tick_hit = _tick;
if (!_occlusion_culler.cull_aabb(moving.exact_aabb)) {
// mark as done (and on visible list)
moving.last_tick_hit = _tick;
_result->visible_roamer_pool_ids.push_back(pool_id);
_result->visible_roamer_pool_ids.push_back(pool_id);
}
}
}
}
@ -215,6 +217,10 @@ void PortalTracer::cull_statics(const VSRoom &p_room, const LocalVector<Plane> &
// print("\t\t\tculling object " + pObj->get_name());
if (test_cull_inside(bb, p_planes)) {
if (_occlusion_culler.cull_aabb(bb)) {
continue;
}
// bypass the bitfield for now and just show / hide
//stat.show(bShow);
@ -348,6 +354,9 @@ void PortalTracer::trace_recursive(const TraceParams &p_params, int p_depth, int
// get the room
const VSRoom &room = _portal_renderer->get_room(p_room_id);
// set up the occlusion culler as a one off
_occlusion_culler.prepare(*_portal_renderer, room, _trace_start_point, p_planes, &_near_and_far_planes[0]);
cull_statics(room, p_planes);
cull_roamers(room, p_planes);
@ -458,6 +467,11 @@ void PortalTracer::trace_recursive(const TraceParams &p_params, int p_depth, int
}
}
// occlusion culling of portals
if (_occlusion_culler.cull_sphere(portal._pt_center, portal._bounding_sphere_radius)) {
continue;
}
// hopefully the portal actually leads somewhere...
if (linked_room_id != -1) {
// we need some new planes
@ -517,3 +531,38 @@ void PortalTracer::trace_recursive(const TraceParams &p_params, int p_depth, int
} // if a linked room exists
} // 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) {
// 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...
static LocalVector<Plane> local_planes;
if ((int)local_planes.size() != p_convex.size()) {
local_planes.resize(p_convex.size());
}
for (int n = 0; n < p_convex.size(); n++) {
local_planes[n] = p_convex[n];
}
_occlusion_culler.prepare_generic(p_portal_renderer, p_portal_renderer.get_occluders_active_list(), p_point, local_planes, nullptr);
// cull each instance
int count = p_num_results;
AABB bb;
for (int n = 0; n < count; n++) {
VSInstance *instance = p_result_array[n];
// this will return false for GLOBAL instances, so we don't occlusion cull gizmos
if (VSG::scene->_instance_get_transformed_aabb_for_occlusion(instance, bb)) {
if (_occlusion_culler.cull_aabb(bb)) {
// remove from list with unordered swap from the end of list
p_result_array[n] = p_result_array[count - 1];
count--;
n--; // repeat this element, as it will have changed
}
}
}
return count;
}

View file

@ -33,6 +33,7 @@
#include "core/bitfield_dynamic.h"
#include "core/local_vector.h"
#include "portal_occlusion_culler.h"
#include "portal_types.h"
#ifdef TOOLS_ENABLED
@ -110,6 +111,10 @@ public:
void set_depth_limit(int p_limit) { _depth_limit = p_limit; }
int get_depth_limit() const { return _depth_limit; }
// 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);
private:
// main tracing function is recursive
void trace_recursive(const TraceParams &p_params, int p_depth, int p_room_id, const LocalVector<Plane> &p_planes, int p_from_external_room_id = -1);
@ -156,6 +161,7 @@ private:
PlanesPool _planes_pool;
int _depth_limit = 16;
PortalOcclusionCuller _occlusion_culler;
// keep a tick count for each trace, to avoid adding a visible
// object to the hit list more than once per tick

View file

@ -34,6 +34,8 @@
#include "core/local_vector.h"
#include "core/math/aabb.h"
#include "core/math/plane.h"
#include "core/math/quat.h"
#include "core/math/transform.h"
#include "core/math/vector3.h"
#include "core/object_id.h"
#include "core/rid.h"
@ -52,6 +54,7 @@ typedef uint32_t RoomHandle;
typedef uint32_t RoomGroupHandle;
typedef uint32_t OcclusionHandle;
typedef uint32_t RGhostHandle;
typedef uint32_t OccluderHandle;
struct VSPortal {
enum ClipResult {
@ -184,6 +187,9 @@ public:
// used in PVS calculation
Vector3 _pt_center;
// used for occlusion culling with occluders
real_t _bounding_sphere_radius = 0.0;
// portal plane
Plane _plane;
@ -300,6 +306,16 @@ struct VSRoom {
return false;
}
bool remove_occluder(uint32_t p_pool_id) {
for (unsigned int n = 0; n < _occluder_pool_ids.size(); n++) {
if (_occluder_pool_ids[n] == p_pool_id) {
_occluder_pool_ids.remove_unordered(n);
return true;
}
}
return false;
}
void add_roamer(uint32_t p_pool_id) {
_roamer_pool_ids.push_back(p_pool_id);
}
@ -308,6 +324,10 @@ struct VSRoom {
_rghost_pool_ids.push_back(p_pool_id);
}
void add_occluder(uint32_t p_pool_id) {
_occluder_pool_ids.push_back(p_pool_id);
}
// keep a list of statics in the room .. statics may appear
// in more than one room due to sprawling!
LocalVector<uint32_t, int32_t> _static_ids;
@ -349,9 +369,87 @@ struct VSRoom {
LocalVector<uint32_t, int32_t> _roamer_pool_ids;
LocalVector<uint32_t, int32_t> _rghost_pool_ids;
// only using uint here for compatibility with TrackedPoolList,
// as we will use either this or TrackedPoolList for occlusion testing
LocalVector<uint32_t, uint32_t> _occluder_pool_ids;
// keep track of which roomgroups the room is in, that
// way we can switch on and off roomgroups as they enter / exit view
LocalVector<uint32_t, int32_t> _roomgroup_ids;
};
struct VSOccluder {
void create() {
type = OT_UNDEFINED;
room_id = -1;
dirty = false;
active = true;
}
// these should match the values in VisualServer::OccluderType
enum Type : uint32_t {
OT_UNDEFINED,
OT_SPHERE,
OT_NUM_TYPES,
} type;
// which is the primary room this group of occluders is in
// (it may sprawl into multiple rooms)
int32_t room_id;
// location for finding the room
Vector3 pt_center;
// global xform
Transform xform;
// whether world space need calculating
bool dirty;
// controlled by the visible flag on the occluder
bool active;
// ids of multiple objects in the appropriate occluder pool
LocalVector<uint32_t, int32_t> list_ids;
};
namespace Occlusion {
struct Sphere {
Vector3 pos;
real_t radius;
void create() { radius = 0.0; }
void from_plane(const Plane &p_plane) {
pos = p_plane.normal;
// Disallow negative radius. Even zero radius should not really be sent.
radius = MAX(p_plane.d, 0.0);
}
bool intersect_ray(const Vector3 &p_ray_origin, const Vector3 &p_ray_dir, real_t &r_dist, real_t radius_squared) const {
Vector3 offset = pos - p_ray_origin;
real_t c2 = offset.length_squared();
real_t v = offset.dot(p_ray_dir);
real_t d = radius_squared - (c2 - (v * v));
if (d < 0.0) {
return false;
}
r_dist = (v - Math::sqrt(d));
return true;
}
};
} // namespace Occlusion
struct VSOccluder_Sphere {
void create() {
local.create();
world.create();
}
Occlusion::Sphere local;
Occlusion::Sphere world;
};
#endif

View file

@ -578,6 +578,14 @@ public:
BIND2(roomgroup_set_scenario, RID, RID)
BIND2(roomgroup_add_room, RID, RID)
// Occluders
BIND0R(RID, occluder_create)
BIND3(occluder_set_scenario, RID, RID, OccluderType)
BIND2(occluder_spheres_update, RID, const Vector<Plane> &)
BIND2(occluder_set_transform, RID, const Transform &)
BIND2(occluder_set_active, RID, bool)
BIND1(set_use_occlusion_culling, bool)
// Rooms
BIND0R(RID, room_create)
BIND2(room_set_scenario, RID, RID)

View file

@ -1156,6 +1156,67 @@ void VisualServerScene::roomgroup_add_room(RID p_roomgroup, RID p_room) {
roomgroup->scenario->_portal_renderer.roomgroup_add_room(roomgroup->scenario_roomgroup_id, room->scenario_room_id);
}
// Occluders
RID VisualServerScene::occluder_create() {
Occluder *ro = memnew(Occluder);
ERR_FAIL_COND_V(!ro, RID());
RID occluder_rid = occluder_owner.make_rid(ro);
return occluder_rid;
}
void VisualServerScene::occluder_set_scenario(RID p_occluder, RID p_scenario, VisualServer::OccluderType p_type) {
Occluder *ro = occluder_owner.getornull(p_occluder);
ERR_FAIL_COND(!ro);
Scenario *scenario = scenario_owner.getornull(p_scenario);
// noop?
if (ro->scenario == scenario) {
return;
}
// if the portal is in a scenario already, remove it
if (ro->scenario) {
ro->scenario->_portal_renderer.occluder_destroy(ro->scenario_occluder_id);
ro->scenario = nullptr;
ro->scenario_occluder_id = 0;
}
// create when entering the world
if (scenario) {
ro->scenario = scenario;
// defer the actual creation to here
ro->scenario_occluder_id = scenario->_portal_renderer.occluder_create((VSOccluder::Type)p_type);
}
}
void VisualServerScene::occluder_set_active(RID p_occluder, bool p_active) {
Occluder *ro = occluder_owner.getornull(p_occluder);
ERR_FAIL_COND(!ro);
ERR_FAIL_COND(!ro->scenario);
ro->scenario->_portal_renderer.occluder_set_active(ro->scenario_occluder_id, p_active);
}
void VisualServerScene::occluder_set_transform(RID p_occluder, const Transform &p_xform) {
Occluder *ro = occluder_owner.getornull(p_occluder);
ERR_FAIL_COND(!ro);
ERR_FAIL_COND(!ro->scenario);
ro->scenario->_portal_renderer.occluder_set_transform(ro->scenario_occluder_id, p_xform);
}
void VisualServerScene::occluder_spheres_update(RID p_occluder, const Vector<Plane> &p_spheres) {
Occluder *ro = occluder_owner.getornull(p_occluder);
ERR_FAIL_COND(!ro);
ERR_FAIL_COND(!ro->scenario);
ro->scenario->_portal_renderer.occluder_update_spheres(ro->scenario_occluder_id, p_spheres);
}
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;
}
// Rooms
void VisualServerScene::callbacks_register(VisualServerCallbacks *p_callbacks) {
_visual_server_callbacks = p_callbacks;
@ -1396,6 +1457,9 @@ int VisualServerScene::_cull_convex_from_point(Scenario *p_scenario, const Vecto
// fallback to BVH / octree if portals not active
if (res == -1) {
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);
}
return res;
}
@ -4010,6 +4074,10 @@ bool VisualServerScene::free(RID p_rid) {
RoomGroup *roomgroup = roomgroup_owner.get(p_rid);
roomgroup_owner.free(p_rid);
memdelete(roomgroup);
} else if (occluder_owner.owns(p_rid)) {
Occluder *ro = occluder_owner.get(p_rid);
occluder_owner.free(p_rid);
memdelete(ro);
} else {
return false;
}

View file

@ -533,6 +533,10 @@ public:
// Portals
virtual void instance_set_portal_mode(RID p_instance, VisualServer::InstancePortalMode p_mode);
bool _instance_get_transformed_aabb(RID p_instance, AABB &r_aabb);
bool _instance_get_transformed_aabb_for_occlusion(VSInstance *p_instance, AABB &r_aabb) const {
r_aabb = ((Instance *)p_instance)->transformed_aabb;
return ((Instance *)p_instance)->portal_mode != VisualServer::INSTANCE_PORTAL_MODE_GLOBAL;
}
void *_instance_get_from_rid(RID p_instance);
bool _instance_cull_check(VSInstance *p_instance, uint32_t p_cull_mask) const {
uint32_t pairable_type = 1 << ((Instance *)p_instance)->base_type;
@ -617,6 +621,27 @@ public:
virtual void roomgroup_set_scenario(RID p_roomgroup, RID p_scenario);
virtual void roomgroup_add_room(RID p_roomgroup, RID p_room);
// Occluders
struct Occluder : RID_Data {
uint32_t scenario_occluder_id = 0;
Scenario *scenario = nullptr;
virtual ~Occluder() {
if (scenario) {
scenario->_portal_renderer.occluder_destroy(scenario_occluder_id);
scenario = nullptr;
scenario_occluder_id = 0;
}
}
};
RID_Owner<Occluder> occluder_owner;
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_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);
// Rooms
struct Room : RID_Data {
// all interations with actual rooms are indirect, as the room is part of the scenario

View file

@ -144,6 +144,7 @@ void VisualServerWrapMT::finish() {
roomgroup_free_cached_ids();
portal_free_cached_ids();
ghost_free_cached_ids();
occluder_free_cached_ids();
}
void VisualServerWrapMT::set_use_vsync_callback(bool p_enable) {

View file

@ -501,6 +501,14 @@ public:
FUNC2(roomgroup_set_scenario, RID, RID)
FUNC2(roomgroup_add_room, RID, RID)
// Occluders
FUNCRID(occluder)
FUNC3(occluder_set_scenario, RID, RID, OccluderType)
FUNC2(occluder_spheres_update, RID, const Vector<Plane> &)
FUNC2(occluder_set_transform, RID, const Transform &)
FUNC2(occluder_set_active, RID, bool)
FUNC1(set_use_occlusion_culling, bool)
// Rooms
FUNCRID(room)
FUNC2(room_set_scenario, RID, RID)

View file

@ -2164,6 +2164,7 @@ void VisualServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("has_feature", "feature"), &VisualServer::has_feature);
ClassDB::bind_method(D_METHOD("has_os_feature", "feature"), &VisualServer::has_os_feature);
ClassDB::bind_method(D_METHOD("set_debug_generate_wireframes", "generate"), &VisualServer::set_debug_generate_wireframes);
ClassDB::bind_method(D_METHOD("set_use_occlusion_culling", "enable"), &VisualServer::set_use_occlusion_culling);
ClassDB::bind_method(D_METHOD("is_render_loop_enabled"), &VisualServer::is_render_loop_enabled);
ClassDB::bind_method(D_METHOD("set_render_loop_enabled", "enabled"), &VisualServer::set_render_loop_enabled);
@ -2612,6 +2613,10 @@ VisualServer::VisualServer() {
GLOBAL_DEF("rendering/portals/optimize/remove_danglers", true);
GLOBAL_DEF("rendering/portals/debug/logging", true);
GLOBAL_DEF("rendering/portals/advanced/flip_imported_portals", false);
// 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"));
}
VisualServer::~VisualServer() {

View file

@ -890,6 +890,20 @@ public:
virtual void roomgroup_set_scenario(RID p_roomgroup, RID p_scenario) = 0;
virtual void roomgroup_add_room(RID p_roomgroup, RID p_room) = 0;
// Occluders
enum OccluderType {
OCCLUDER_TYPE_UNDEFINED,
OCCLUDER_TYPE_SPHERE,
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_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;
// Rooms
enum RoomsDebugFeature {
ROOMS_DEBUG_SPRAWL,