Fix Polygon2D skinned bounds (for culling)

The bound Rect2 was previously incorrect because bone transforms need to be applied to verts in bone space, rather than local space. This was previously resulting in skinned Polygon2Ds being incorrectly culled.
This commit is contained in:
lawnjelly 2023-04-10 16:19:32 +01:00
parent 632a544c6e
commit dd6c213dac
10 changed files with 269 additions and 55 deletions

View file

@ -728,6 +728,14 @@
Modulates all colors in the given canvas.
</description>
</method>
<method name="debug_canvas_item_get_rect">
<return type="Rect2" />
<argument index="0" name="item" type="RID" />
<description>
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.
</description>
</method>
<method name="directional_light_create">
<return type="RID" />
<description>

View file

@ -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());
}

View file

@ -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<Rect2> &active_bounds = p_polygon.skinning_data->active_bounds;
LocalVector<uint16_t> &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<int32_t> 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<Rect2> 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;
}

View file

@ -892,10 +892,24 @@ public:
bool antialiased;
bool antialiasing_use_indices;
struct SkinningData {
bool dirty = true;
LocalVector<Rect2> active_bounds;
LocalVector<uint16_t> 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<const Item::CommandPolygon *>(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<Rect2> 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<const Item::CommandMesh *>(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;

View file

@ -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<Item::CommandPolygon *>(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);

View file

@ -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);

View file

@ -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)

View file

@ -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)

View file

@ -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);

View file

@ -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;