diff --git a/doc/classes/VisualServer.xml b/doc/classes/VisualServer.xml index abe44ca3b90..cd0460b974a 100644 --- a/doc/classes/VisualServer.xml +++ b/doc/classes/VisualServer.xml @@ -728,6 +728,14 @@ Modulates all colors in the given canvas. + + + + + Returns the bounding rectangle for a canvas item in local space, as calculated by the renderer. This bound is used internally for culling. + [b]Warning:[/b] This function is intended for debugging in the editor, and will pass through and return a zero [Rect2] in exported projects. + + diff --git a/scene/2d/polygon_2d.cpp b/scene/2d/polygon_2d.cpp index 7d68bb56a5c..669e93fc0f6 100644 --- a/scene/2d/polygon_2d.cpp +++ b/scene/2d/polygon_2d.cpp @@ -111,6 +111,16 @@ void Polygon2D::_notification(int p_what) { if (skeleton_node) { VS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), skeleton_node->get_skeleton()); new_skeleton_id = skeleton_node->get_instance_id(); + + // Sync the offset transform between the Polygon2D and the skeleton. + // This is needed for accurate culling in VisualServer. + Transform2D global_xform_skel = skeleton_node->get_global_transform(); + Transform2D global_xform_poly = get_global_transform(); + + // find the difference + Transform2D global_xform_offset = global_xform_skel.affine_inverse() * global_xform_poly; + VS::get_singleton()->canvas_item_set_skeleton_relative_xform(get_canvas_item(), global_xform_offset); + } else { VS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), RID()); } diff --git a/servers/visual/rasterizer.cpp b/servers/visual/rasterizer.cpp index cc10eb9dc6d..696366537d0 100644 --- a/servers/visual/rasterizer.cpp +++ b/servers/visual/rasterizer.cpp @@ -561,3 +561,173 @@ int RasterizerStorage::multimesh_get_visible_instances(RID p_multimesh) const { AABB RasterizerStorage::multimesh_get_aabb(RID p_multimesh) const { return _multimesh_get_aabb(p_multimesh); } + +// The bone bounds are determined by rigging, +// as such they can be calculated as a one off operation, +// rather than each call to get_rect(). +void RasterizerCanvas::Item::precalculate_polygon_bone_bounds(const Item::CommandPolygon &p_polygon) const { + p_polygon.skinning_data->dirty = false; + p_polygon.skinning_data->untransformed_bound = Rect2(Vector2(), Vector2(-1, -1)); // negative means unused. + + int num_points = p_polygon.points.size(); + const Point2 *pp = &p_polygon.points[0]; + + // Calculate bone AABBs. + int bone_count = RasterizerStorage::base_singleton->skeleton_get_bone_count(skeleton); + + // Get some local aliases + LocalVector &active_bounds = p_polygon.skinning_data->active_bounds; + LocalVector &active_bone_ids = p_polygon.skinning_data->active_bone_ids; + active_bounds.clear(); + active_bone_ids.clear(); + + // Uses dynamic allocation, but shouldn't happen very often. + // If happens more often, use alloca. + LocalVector bone_to_active_bone_mapping; + bone_to_active_bone_mapping.resize(bone_count); + + for (int n = 0; n < bone_count; n++) { + bone_to_active_bone_mapping[n] = -1; + } + + const Transform2D &item_transform = skinning_data->skeleton_relative_xform; + + bool some_were_untransformed = false; + + for (int n = 0; n < num_points; n++) { + Point2 p = pp[n]; + bool bone_space = false; + float total_weight = 0; + + for (int k = 0; k < 4; k++) { + int bone_id = p_polygon.bones[n * 4 + k]; + float w = p_polygon.weights[n * 4 + k]; + if (w == 0) { + continue; + } + total_weight += w; + + // Ensure the point is in "bone space" / rigged space. + if (!bone_space) { + bone_space = true; + p = item_transform.xform(p); + } + + // get the active bone, or create a new active bone + DEV_ASSERT(bone_id < bone_count); + int32_t &active_bone = bone_to_active_bone_mapping[bone_id]; + if (active_bone != -1) { + active_bounds[active_bone].expand_to(p); + } else { + // Increment the number of active bones stored. + active_bone = active_bounds.size(); + active_bounds.resize(active_bone + 1); + active_bone_ids.resize(active_bone + 1); + + // First point for the bone + DEV_ASSERT(bone_id <= UINT16_MAX); + active_bone_ids[active_bone] = bone_id; + active_bounds[active_bone] = Rect2(p, Vector2(0.00001, 0.00001)); + } + } + + // If some points were not rigged, + // we want to add them directly to an "untransformed bound", + // and merge this with the skinned bound later. + // Also do this if a point is not FULLY weighted, + // because the untransformed position is still having an influence. + if (!bone_space || (total_weight < 0.99f)) { + if (some_were_untransformed) { + p_polygon.skinning_data->untransformed_bound.expand_to(pp[n]); + } else { + // First point + some_were_untransformed = true; + p_polygon.skinning_data->untransformed_bound = Rect2(pp[n], Vector2()); + } + } + } +} + +Rect2 RasterizerCanvas::Item::calculate_polygon_bounds(const Item::CommandPolygon &p_polygon) const { + int num_points = p_polygon.points.size(); + + // If there is no skeleton, or the bones data is invalid... + // Note : Can we check the second more efficiently? by checking if polygon.skinning_data is set perhaps? + if (skeleton == RID() || !(num_points && p_polygon.bones.size() == num_points * 4 && p_polygon.weights.size() == p_polygon.bones.size())) { + // With no skeleton, all points are untransformed. + Rect2 r; + const Point2 *pp = &p_polygon.points[0]; + r.position = pp[0]; + + for (int n = 1; n < num_points; n++) { + r.expand_to(pp[n]); + } + + return r; + } + + // Skinned skeleton is present. + ERR_FAIL_COND_V_MSG(!skinning_data, Rect2(), "Skinned Polygon2D must have skeleton_relative_xform set for correct culling."); + + // Ensure the polygon skinning data is created... + // (This isn't stored on every polygon to save memory). + if (!p_polygon.skinning_data) { + p_polygon.skinning_data = memnew(Item::CommandPolygon::SkinningData); + } + + Item::CommandPolygon::SkinningData &pdata = *p_polygon.skinning_data; + + // This should only occur when rigging has changed. + // Usually a one off in games. + if (pdata.dirty) { + precalculate_polygon_bone_bounds(p_polygon); + } + + // We only deal with the precalculated ACTIVE bone AABBs using the skeleton. + // (No need to bother with bones that are unused for this poly.) + int num_active_bones = pdata.active_bounds.size(); + if (!num_active_bones) { + return pdata.untransformed_bound; + } + + // No need to make a dynamic allocation here in 99% of cases. + Rect2 *bptr = nullptr; + LocalVector bone_aabbs; + if (num_active_bones <= 1024) { + bptr = (Rect2 *)alloca(sizeof(Rect2) * num_active_bones); + } else { + bone_aabbs.resize(num_active_bones); + bptr = bone_aabbs.ptr(); + } + + // Copy across the precalculated bone bounds. + memcpy(bptr, pdata.active_bounds.ptr(), sizeof(Rect2) * num_active_bones); + + const Transform2D &item_transform_inv = skinning_data->skeleton_relative_xform_inv; + + Rect2 aabb; + bool first_bone = true; + + for (int n = 0; n < num_active_bones; n++) { + int bone_id = pdata.active_bone_ids[n]; + const Transform2D &mtx = RasterizerStorage::base_singleton->skeleton_bone_get_transform_2d(skeleton, bone_id); + Rect2 baabb = mtx.xform(bptr[n]); + + if (first_bone) { + aabb = baabb; + first_bone = false; + } else { + aabb = aabb.merge(baabb); + } + } + + // Transform the polygon AABB back into local space from bone space. + aabb = item_transform_inv.xform(aabb); + + // If some were untransformed... + if (pdata.untransformed_bound.size.x >= 0) { + return pdata.untransformed_bound.merge(aabb); + } + + return aabb; +} diff --git a/servers/visual/rasterizer.h b/servers/visual/rasterizer.h index f58b2212811..b2471f922a1 100644 --- a/servers/visual/rasterizer.h +++ b/servers/visual/rasterizer.h @@ -892,10 +892,24 @@ public: bool antialiased; bool antialiasing_use_indices; + struct SkinningData { + bool dirty = true; + LocalVector active_bounds; + LocalVector active_bone_ids; + Rect2 untransformed_bound; + }; + mutable SkinningData *skinning_data = nullptr; + CommandPolygon() { type = TYPE_POLYGON; count = 0; } + virtual ~CommandPolygon() { + if (skinning_data) { + memdelete(skinning_data); + skinning_data = nullptr; + } + } }; struct CommandMesh : public Command { @@ -968,6 +982,12 @@ public: Item *next; + struct SkinningData { + Transform2D skeleton_relative_xform; + Transform2D skeleton_relative_xform_inv; + }; + SkinningData *skinning_data = nullptr; + struct CopyBackBuffer { Rect2 rect; Rect2 screen_rect; @@ -984,6 +1004,11 @@ public: Rect2 global_rect_cache; + private: + Rect2 calculate_polygon_bounds(const Item::CommandPolygon &p_polygon) const; + void precalculate_polygon_bone_bounds(const Item::CommandPolygon &p_polygon) const; + + public: const Rect2 &get_rect() const { if (custom_rect) { return rect; @@ -1068,61 +1093,8 @@ public: } break; case Item::Command::TYPE_POLYGON: { const Item::CommandPolygon *polygon = static_cast(c); - int l = polygon->points.size(); - const Point2 *pp = &polygon->points[0]; - r.position = pp[0]; - for (int j = 1; j < l; j++) { - r.expand_to(pp[j]); - } - - if (skeleton != RID()) { - // calculate bone AABBs - int bone_count = RasterizerStorage::base_singleton->skeleton_get_bone_count(skeleton); - - Vector bone_aabbs; - bone_aabbs.resize(bone_count); - Rect2 *bptr = bone_aabbs.ptrw(); - - for (int j = 0; j < bone_count; j++) { - bptr[j].size = Vector2(-1, -1); //negative means unused - } - if (l && polygon->bones.size() == l * 4 && polygon->weights.size() == polygon->bones.size()) { - for (int j = 0; j < l; j++) { - Point2 p = pp[j]; - for (int k = 0; k < 4; k++) { - int idx = polygon->bones[j * 4 + k]; - float w = polygon->weights[j * 4 + k]; - if (w == 0) { - continue; - } - - if (bptr[idx].size.x < 0) { - //first - bptr[idx] = Rect2(p, Vector2(0.00001, 0.00001)); - } else { - bptr[idx].expand_to(p); - } - } - } - - Rect2 aabb; - bool first_bone = true; - for (int j = 0; j < bone_count; j++) { - Transform2D mtx = RasterizerStorage::base_singleton->skeleton_bone_get_transform_2d(skeleton, j); - Rect2 baabb = mtx.xform(bone_aabbs[j]); - - if (first_bone) { - aabb = baabb; - first_bone = false; - } else { - aabb = aabb.merge(baabb); - } - } - - r = r.merge(aabb); - } - } - + DEV_ASSERT(polygon); + r = calculate_polygon_bounds(*polygon); } break; case Item::Command::TYPE_MESH: { const Item::CommandMesh *mesh = static_cast(c); @@ -1188,6 +1160,11 @@ public: final_clip_owner = nullptr; material_owner = nullptr; light_masked = false; + + if (skinning_data) { + memdelete(skinning_data); + skinning_data = nullptr; + } } Item() { light_mask = 1; diff --git a/servers/visual/visual_server_canvas.cpp b/servers/visual/visual_server_canvas.cpp index 7cf80e136d5..ba013ab5c51 100644 --- a/servers/visual/visual_server_canvas.cpp +++ b/servers/visual/visual_server_canvas.cpp @@ -917,6 +917,38 @@ void VisualServerCanvas::canvas_item_set_z_as_relative_to_parent(RID p_item, boo canvas_item->z_relative = p_enable; } +Rect2 VisualServerCanvas::_debug_canvas_item_get_rect(RID p_item) { + Item *canvas_item = canvas_item_owner.getornull(p_item); + ERR_FAIL_COND_V(!canvas_item, Rect2()); + return canvas_item->get_rect(); +} + +void VisualServerCanvas::canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform) { + Item *canvas_item = canvas_item_owner.getornull(p_item); + ERR_FAIL_COND(!canvas_item); + + if (!canvas_item->skinning_data) { + canvas_item->skinning_data = memnew(Item::SkinningData); + } + canvas_item->skinning_data->skeleton_relative_xform = p_relative_xform; + canvas_item->skinning_data->skeleton_relative_xform_inv = p_relative_xform.affine_inverse(); + + // Set any Polygon2Ds pre-calced bone bounds to dirty. + for (int n = 0; n < canvas_item->commands.size(); n++) { + Item::Command *c = canvas_item->commands[n]; + if (c->type == Item::Command::TYPE_POLYGON) { + Item::CommandPolygon *polygon = static_cast(c); + + // Make sure skinning data is present. + if (!polygon->skinning_data) { + polygon->skinning_data = memnew(Item::CommandPolygon::SkinningData); + } + + polygon->skinning_data->dirty = true; + } + } +} + void VisualServerCanvas::canvas_item_attach_skeleton(RID p_item, RID p_skeleton) { Item *canvas_item = canvas_item_owner.getornull(p_item); ERR_FAIL_COND(!canvas_item); diff --git a/servers/visual/visual_server_canvas.h b/servers/visual/visual_server_canvas.h index 774c8fdd7ef..7e25dc55a15 100644 --- a/servers/visual/visual_server_canvas.h +++ b/servers/visual/visual_server_canvas.h @@ -208,6 +208,8 @@ public: void canvas_item_set_z_as_relative_to_parent(RID p_item, bool p_enable); void canvas_item_set_copy_to_backbuffer(RID p_item, bool p_enable, const Rect2 &p_rect); void canvas_item_attach_skeleton(RID p_item, RID p_skeleton); + void canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform); + Rect2 _debug_canvas_item_get_rect(RID p_item); void canvas_item_clear(RID p_item); void canvas_item_set_draw_index(RID p_item, int p_index); diff --git a/servers/visual/visual_server_raster.h b/servers/visual/visual_server_raster.h index c43501e7f4f..4f05d1faf77 100644 --- a/servers/visual/visual_server_raster.h +++ b/servers/visual/visual_server_raster.h @@ -718,6 +718,8 @@ public: BIND2(canvas_item_set_z_as_relative_to_parent, RID, bool) BIND3(canvas_item_set_copy_to_backbuffer, RID, bool, const Rect2 &) BIND2(canvas_item_attach_skeleton, RID, RID) + BIND2(canvas_item_set_skeleton_relative_xform, RID, Transform2D) + BIND1R(Rect2, _debug_canvas_item_get_rect, RID) BIND1(canvas_item_clear, RID) BIND2(canvas_item_set_draw_index, RID, int) diff --git a/servers/visual/visual_server_wrap_mt.h b/servers/visual/visual_server_wrap_mt.h index 3d814c2db39..2cb6adfa3af 100644 --- a/servers/visual/visual_server_wrap_mt.h +++ b/servers/visual/visual_server_wrap_mt.h @@ -619,6 +619,8 @@ public: FUNC2(canvas_item_set_z_as_relative_to_parent, RID, bool) FUNC3(canvas_item_set_copy_to_backbuffer, RID, bool, const Rect2 &) FUNC2(canvas_item_attach_skeleton, RID, RID) + FUNC2(canvas_item_set_skeleton_relative_xform, RID, Transform2D) + FUNC1R(Rect2, _debug_canvas_item_get_rect, RID) FUNC1(canvas_item_clear, RID) FUNC2(canvas_item_set_draw_index, RID, int) diff --git a/servers/visual_server.cpp b/servers/visual_server.cpp index 9cd7ef3bc2c..d3d2b078497 100644 --- a/servers/visual_server.cpp +++ b/servers/visual_server.cpp @@ -2202,6 +2202,7 @@ void VisualServer::_bind_methods() { ClassDB::bind_method(D_METHOD("canvas_item_set_draw_index", "item", "index"), &VisualServer::canvas_item_set_draw_index); ClassDB::bind_method(D_METHOD("canvas_item_set_material", "item", "material"), &VisualServer::canvas_item_set_material); ClassDB::bind_method(D_METHOD("canvas_item_set_use_parent_material", "item", "enabled"), &VisualServer::canvas_item_set_use_parent_material); + ClassDB::bind_method(D_METHOD("debug_canvas_item_get_rect", "item"), &VisualServer::debug_canvas_item_get_rect); ClassDB::bind_method(D_METHOD("canvas_light_create"), &VisualServer::canvas_light_create); ClassDB::bind_method(D_METHOD("canvas_light_attach_to_canvas", "light", "canvas"), &VisualServer::canvas_light_attach_to_canvas); ClassDB::bind_method(D_METHOD("canvas_light_set_enabled", "light", "enabled"), &VisualServer::canvas_light_set_enabled); diff --git a/servers/visual_server.h b/servers/visual_server.h index ba1e0534515..07a2fb553cc 100644 --- a/servers/visual_server.h +++ b/servers/visual_server.h @@ -1052,6 +1052,16 @@ public: virtual void canvas_item_set_copy_to_backbuffer(RID p_item, bool p_enable, const Rect2 &p_rect) = 0; virtual void canvas_item_attach_skeleton(RID p_item, RID p_skeleton) = 0; + virtual void canvas_item_set_skeleton_relative_xform(RID p_item, Transform2D p_relative_xform) = 0; + + Rect2 debug_canvas_item_get_rect(RID p_item) { +#ifdef TOOLS_ENABLED + return _debug_canvas_item_get_rect(p_item); +#else + return Rect2(); +#endif + } + virtual Rect2 _debug_canvas_item_get_rect(RID p_item) = 0; virtual void canvas_item_clear(RID p_item) = 0; virtual void canvas_item_set_draw_index(RID p_item, int p_index) = 0;