diff --git a/core/engine.h b/core/engine.h index 72f3118eb48..2a2f67508d3 100644 --- a/core/engine.h +++ b/core/engine.h @@ -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; diff --git a/core/pooled_list.h b/core/pooled_list.h index 24dadeb7656..19ee4081b89 100644 --- a/core/pooled_list.h +++ b/core/pooled_list.h @@ -159,6 +159,8 @@ public: } } + const LocalVector &get_active_list() const { return _active_list; } + private: PooledList _pool; LocalVector _active_map; diff --git a/doc/classes/EditorSpatialGizmoPlugin.xml b/doc/classes/EditorSpatialGizmoPlugin.xml index fa063e53014..db00d904873 100644 --- a/doc/classes/EditorSpatialGizmoPlugin.xml +++ b/doc/classes/EditorSpatialGizmoPlugin.xml @@ -45,8 +45,10 @@ + 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. diff --git a/doc/classes/Occluder.xml b/doc/classes/Occluder.xml new file mode 100644 index 00000000000..5dca954c684 --- /dev/null +++ b/doc/classes/Occluder.xml @@ -0,0 +1,29 @@ + + + + Allows [OccluderShape]s to be used for occlusion culling. + + + [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. + + + + + + + + + + + + + + + + + + diff --git a/doc/classes/OccluderShape.xml b/doc/classes/OccluderShape.xml new file mode 100644 index 00000000000..4fa27aca677 --- /dev/null +++ b/doc/classes/OccluderShape.xml @@ -0,0 +1,15 @@ + + + + Base class for shapes used for occlusion culling by the [Occluder] node. + + + [Occluder]s can use any primitive shape derived from [OccluderShape]. + + + + + + + + diff --git a/doc/classes/OccluderShapeSphere.xml b/doc/classes/OccluderShapeSphere.xml new file mode 100644 index 00000000000..1ca99ac7b8a --- /dev/null +++ b/doc/classes/OccluderShapeSphere.xml @@ -0,0 +1,37 @@ + + + + Spherical occlusion primitive for use with the [Occluder] node. + + + [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. + + + + + + + + + + Sets an individual sphere's position. + + + + + + + + Sets an individual sphere's radius. + + + + + + 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. + + + + + diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 77080561bb6..e55a605378c 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1242,6 +1242,10 @@ On import, mesh vertex data will be split into two streams within a single vertex buffer, one for position data and the other for interleaved attributes data. Recommended to be enabled if targeting mobile devices. Requires manual reimport of meshes after toggling. + + Determines the maximum number of 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. + 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. diff --git a/doc/classes/VisualServer.xml b/doc/classes/VisualServer.xml index ae7308c860b..5d1dc919c0b 100644 --- a/doc/classes/VisualServer.xml +++ b/doc/classes/VisualServer.xml @@ -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. + + + + + Enables or disables occlusion culling. + + diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index bb3083b91d2..0db0e383827 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -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))); diff --git a/editor/icons/icon_occluder.svg b/editor/icons/icon_occluder.svg new file mode 100644 index 00000000000..e204b7fba80 --- /dev/null +++ b/editor/icons/icon_occluder.svg @@ -0,0 +1 @@ + diff --git a/editor/icons/icon_occluder_shape_sphere.svg b/editor/icons/icon_occluder_shape_sphere.svg new file mode 100644 index 00000000000..8f871edda8b --- /dev/null +++ b/editor/icons/icon_occluder_shape_sphere.svg @@ -0,0 +1 @@ + diff --git a/editor/plugins/room_manager_editor_plugin.cpp b/editor/plugins/room_manager_editor_plugin.cpp index ea152135b7a..9d4eca10250 100644 --- a/editor/plugins/room_manager_editor_plugin.cpp +++ b/editor/plugins/room_manager_editor_plugin.cpp @@ -82,6 +82,9 @@ RoomManagerEditorPlugin::RoomManagerEditorPlugin(EditorNode *p_node) { Ref portal_gizmo_plugin = Ref(memnew(PortalGizmoPlugin)); SpatialEditor::get_singleton()->add_gizmo_plugin(portal_gizmo_plugin); + + Ref occluder_gizmo_plugin = Ref(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 ref = _occluder->get_shape(); + + if (ref.is_valid()) { + Spatial *parent = Object::cast_to(_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(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() { +} diff --git a/editor/plugins/room_manager_editor_plugin.h b/editor/plugins/room_manager_editor_plugin.h index d865661a0a4..370b41684e1 100644 --- a/editor/plugins/room_manager_editor_plugin.h +++ b/editor/plugins/room_manager_editor_plugin.h @@ -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 diff --git a/editor/plugins/spatial_editor_plugin.cpp b/editor/plugins/spatial_editor_plugin.cpp index 18c62bc6695..c2bcdaa96d5 100644 --- a/editor/plugins/spatial_editor_plugin.cpp +++ b/editor/plugins/spatial_editor_plugin.cpp @@ -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 &p_icon) { Ref handle_material = Ref(memnew(SpatialMaterial)); handle_material->set_flag(SpatialMaterial::FLAG_UNSHADED, true); handle_material->set_flag(SpatialMaterial::FLAG_USE_POINT_SIZE, true); - Ref handle_t = SpatialEditor::get_singleton()->get_icon("Editor3DHandle", "EditorIcons"); + + Ref 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())); diff --git a/editor/plugins/spatial_editor_plugin.h b/editor/plugins/spatial_editor_plugin.h index 0f662898b7b..d5bcdc7e54d 100644 --- a/editor/plugins/spatial_editor_plugin.h +++ b/editor/plugins/spatial_editor_plugin.h @@ -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 &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 &p_icon = nullptr); void add_material(const String &p_name, Ref p_material); Ref get_material(const String &p_name, const Ref &p_gizmo = Ref()); diff --git a/editor/spatial_editor_gizmos.cpp b/editor/spatial_editor_gizmos.cpp index 66358db9783..cce56ac88f6 100644 --- a/editor/spatial_editor_gizmos.cpp +++ b/editor/spatial_editor_gizmos.cpp @@ -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 OccluderGizmoPlugin::create_gizmo(Spatial *p_spatial) { + Ref ref; + + Occluder *occluder = Object::cast_to(p_spatial); + if (occluder) { + ref = Ref(memnew(OccluderSpatialGizmo(occluder))); + } + + return ref; +} + +bool OccluderGizmoPlugin::has_gizmo(Spatial *p_spatial) { + if (Object::cast_to(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 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 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 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 rshape = _occluder->get_shape(); + if (rshape.is_null() || !rshape.is_valid()) { + return nullptr; + } + + OccluderShape *shape = rshape.ptr(); + OccluderShapeSphere *occ_sphere = Object::cast_to(shape); + return occ_sphere; +} + +const OccluderShapeSphere *OccluderSpatialGizmo::get_occluder_shape_sphere() const { + if (!_occluder) { + return nullptr; + } + + Ref 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(shape); + return occ_sphere; +} + +void OccluderSpatialGizmo::redraw() { + clear(); + + if (!_occluder) { + return; + } + + Ref 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 spheres = occ_sphere->get_spheres(); + if (!spheres.size()) { + return; + } + + Vector points; + Vector handles; + Vector 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_handle = gizmo_plugin->get_material("occluder_handle", this); + Ref 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); +} diff --git a/editor/spatial_editor_gizmos.h b/editor/spatial_editor_gizmos.h index 4f6f9eae1cb..37c2e671a34 100644 --- a/editor/spatial_editor_gizmos.h +++ b/editor/spatial_editor_gizmos.h @@ -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 create_gizmo(Spatial *p_spatial); + +public: + OccluderGizmoPlugin(); +}; + #endif // SPATIAL_EDITOR_GIZMOS_H diff --git a/scene/3d/occluder.cpp b/scene/3d/occluder.cpp new file mode 100644 index 00000000000..69993e48ace --- /dev/null +++ b/scene/3d/occluder.cpp @@ -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 &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 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); + } +} diff --git a/scene/3d/occluder.h b/scene/3d/occluder.h new file mode 100644 index 00000000000..c5b3bac62c4 --- /dev/null +++ b/scene/3d/occluder.h @@ -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 _shape; + + void resource_changed(RES res); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void set_shape(const Ref &p_shape); + Ref get_shape() const; + + String get_configuration_warning() const; + + Occluder(); + ~Occluder(); +}; + +#endif // OCCLUDER_H diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index e3394059696..0ae77a0b4c9 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -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 resource_saver_text; @@ -438,6 +440,7 @@ void register_scene_types() { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); @@ -649,6 +652,8 @@ void register_scene_types() { ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_virtual_class(); + ClassDB::register_class(); OS::get_singleton()->yield(); //may take time to init diff --git a/scene/resources/occluder_shape.cpp b/scene/resources/occluder_shape.cpp new file mode 100644 index 00000000000..888ac4e0f65 --- /dev/null +++ b/scene/resources/occluder_shape.cpp @@ -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 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 &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()) { +} diff --git a/scene/resources/occluder_shape.h b/scene/resources/occluder_shape.h new file mode 100644 index 00000000000..486a58e7236 --- /dev/null +++ b/scene/resources/occluder_shape.h @@ -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 _spheres; + const real_t _min_radius = 0.1; + +protected: + static void _bind_methods(); + +public: + void set_spheres(const Vector &p_spheres); + Vector 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 diff --git a/servers/visual/portals/portal_occlusion_culler.cpp b/servers/visual/portals/portal_occlusion_culler.cpp new file mode 100644 index 00000000000..d379b8ac7b3 --- /dev/null +++ b/servers/visual/portals/portal_occlusion_culler.cpp @@ -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 &p_occluder_pool_ids, const Vector3 &pt_camera, const LocalVector &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"); +} diff --git a/servers/visual/portals/portal_occlusion_culler.h b/servers/visual/portals/portal_occlusion_culler.h new file mode 100644 index 00000000000..a7688dc4eb9 --- /dev/null +++ b/servers/visual/portals/portal_occlusion_culler.h @@ -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 &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 &p_occluder_pool_ids, const Vector3 &pt_camera, const LocalVector &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 &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 diff --git a/servers/visual/portals/portal_renderer.cpp b/servers/visual/portals/portal_renderer.cpp index 513fb94bd33..275d246221d 100644 --- a/servers/visual/portals/portal_renderer.cpp +++ b/servers/visual/portals/portal_renderer.cpp @@ -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 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 &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()); + } 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) { diff --git a/servers/visual/portals/portal_renderer.h b/servers/visual/portals/portal_renderer.h index 60892648f65..9fd08b3f890 100644 --- a/servers/visual/portals/portal_renderer.h +++ b/servers/visual/portals/portal_renderer.h @@ -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 &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 &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 &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 &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 &p_planes, const Plane &p); @@ -259,6 +286,10 @@ private: LocalVector _moving_list_global; LocalVector _moving_list_roaming; + // occluders + TrackedPooledList _occluder_pool; + TrackedPooledList _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 diff --git a/servers/visual/portals/portal_tracer.cpp b/servers/visual/portals/portal_tracer.cpp index 8770bde2cfa..95e75f450ec 100644 --- a/servers/visual/portals/portal_tracer.cpp +++ b/servers/visual/portals/portal_tracer.cpp @@ -168,10 +168,12 @@ void PortalTracer::cull_roamers(const VSRoom &p_room, const LocalVector & } 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 & // 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 &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 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; +} diff --git a/servers/visual/portals/portal_tracer.h b/servers/visual/portals/portal_tracer.h index 5670606ec75..4b6af191ff6 100644 --- a/servers/visual/portals/portal_tracer.h +++ b/servers/visual/portals/portal_tracer.h @@ -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 &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 &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 diff --git a/servers/visual/portals/portal_types.h b/servers/visual/portals/portal_types.h index 5021f3b8795..881583d2045 100644 --- a/servers/visual/portals/portal_types.h +++ b/servers/visual/portals/portal_types.h @@ -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 _static_ids; @@ -349,9 +369,87 @@ struct VSRoom { LocalVector _roamer_pool_ids; LocalVector _rghost_pool_ids; + // only using uint here for compatibility with TrackedPoolList, + // as we will use either this or TrackedPoolList for occlusion testing + LocalVector _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 _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 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 diff --git a/servers/visual/visual_server_raster.h b/servers/visual/visual_server_raster.h index a35e64ffaf7..a8a0baf0eea 100644 --- a/servers/visual/visual_server_raster.h +++ b/servers/visual/visual_server_raster.h @@ -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 &) + 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) diff --git a/servers/visual/visual_server_scene.cpp b/servers/visual/visual_server_scene.cpp index 59a83518f5c..b3a7fc6070e 100644 --- a/servers/visual/visual_server_scene.cpp +++ b/servers/visual/visual_server_scene.cpp @@ -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 &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; } diff --git a/servers/visual/visual_server_scene.h b/servers/visual/visual_server_scene.h index c36ab6cc77c..1761bbb5e52 100644 --- a/servers/visual/visual_server_scene.h +++ b/servers/visual/visual_server_scene.h @@ -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_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 &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 diff --git a/servers/visual/visual_server_wrap_mt.cpp b/servers/visual/visual_server_wrap_mt.cpp index 1eb3c0f26fd..11b157c2fd3 100644 --- a/servers/visual/visual_server_wrap_mt.cpp +++ b/servers/visual/visual_server_wrap_mt.cpp @@ -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) { diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h index a4cd6cd444d..5f412f14306 100644 --- a/servers/visual/visual_server_wrap_mt.h +++ b/servers/visual/visual_server_wrap_mt.h @@ -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 &) + 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) diff --git a/servers/visual_server.cpp b/servers/visual_server.cpp index 0ac6c61f045..2eb0e5b130d 100644 --- a/servers/visual_server.cpp +++ b/servers/visual_server.cpp @@ -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() { diff --git a/servers/visual_server.h b/servers/visual_server.h index 6d2c3f9826e..f6512d16c0c 100644 --- a/servers/visual_server.h +++ b/servers/visual_server.h @@ -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 &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,