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;