Skip to content

Commit

Permalink
Merge pull request godotengine#20 from lawnjelly/fix_skele2d_bounds2
Browse files Browse the repository at this point in the history
Fix Polygon2D skinned bounds (for culling)
  • Loading branch information
lawnjelly authored Apr 10, 2023
2 parents 7d36cae + 1c13a31 commit 030b3c9
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 61 deletions.
8 changes: 8 additions & 0 deletions doc/classes/VisualServer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,14 @@
Modulates all colors in the given canvas.
</description>
</method>
<method name="debug_canvas_item_get_bounding_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>
Expand Down
10 changes: 10 additions & 0 deletions scene/2d/polygon_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
214 changes: 153 additions & 61 deletions servers/visual/rasterizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -562,80 +562,172 @@ AABB RasterizerStorage::multimesh_get_aabb(RID p_multimesh) const {
return _multimesh_get_aabb(p_multimesh);
}

Rect2 RasterizerCanvas::Item::calculate_polygon_bounds(const Item::CommandPolygon &p_polygon) const {
Rect2 r;
int l = p_polygon.points.size();
// 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];

// First add all untransformed points to the bound.
// This will be final version if no skeleton.
r.position = pp[0];
for (int j = 1; j < l; j++) {
r.expand_to(pp[j]);
// 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;
}

// If there is a skeleton, some or all of the points may be transformed
// by bones (depending on the weights).
// If all points are fully weighted by bones, this means the original point is not needed for the bound,
// so this routine will OVER-ESTIMATE the bound with skeletons. Perhaps this can be improved.
if (skeleton != RID()) {
// calculate bone AABBs
int bone_count = RasterizerStorage::base_singleton->skeleton_get_bone_count(skeleton);
const Transform2D &item_transform = skinning_data->skeleton_relative_xform;

Vector<Rect2> bone_aabbs;
bone_aabbs.resize(bone_count);
Rect2 *bptr = bone_aabbs.ptrw();
bool some_were_untransformed = false;

for (int j = 0; j < bone_count; j++) {
bptr[j].size = Vector2(-1, -1); //negative means unused
}
if (l && p_polygon.bones.size() == l * 4 && p_polygon.weights.size() == p_polygon.bones.size()) {
for (int j = 0; j < l; j++) {
Point2 p = pp[j];
bool bone_space = false;

for (int k = 0; k < 4; k++) {
int idx = p_polygon.bones[j * 4 + k];
float w = p_polygon.weights[j * 4 + k];
if (w == 0) {
continue;
}
for (int n = 0; n < num_points; n++) {
Point2 p = pp[n];
bool bone_space = false;
float total_weight = 0;

// Ensure the point is in "bone space", i.e. around the origin,
// before the bone transform is applied.
if (!bone_space) {
bone_space = true;
p = xform.xform(p);
}
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;

if (bptr[idx].size.x < 0) {
//first
bptr[idx] = Rect2(p, Vector2(0.00001, 0.00001));
} else {
bptr[idx].expand_to(p);
}
}
// Ensure the point is in "bone space" / rigged space.
if (!bone_space) {
bone_space = true;
p = item_transform.xform(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);
}
// 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.");

// Transform the polygon AABB back into local space from bone space.
aabb = xform.affine_inverse().xform(aabb);
// 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;
}

r = r.merge(aabb);
// 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);
}
}
return r;

// 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;
}
26 changes: 26 additions & 0 deletions servers/visual/rasterizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -907,10 +907,24 @@ class RasterizerCanvas {
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 {
Expand Down Expand Up @@ -985,6 +999,12 @@ class RasterizerCanvas {

Item *next;

struct SkinningData {
Transform2D skeleton_relative_xform;
Transform2D skeleton_relative_xform_inv;
};
SkinningData *skinning_data = nullptr;

struct CopyBackBuffer {
Rect2 rect;
Rect2 screen_rect;
Expand All @@ -1007,6 +1027,7 @@ class RasterizerCanvas {

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 {
Expand Down Expand Up @@ -1171,6 +1192,11 @@ class RasterizerCanvas {
final_clip_owner = nullptr;
material_owner = nullptr;
light_masked = false;

if (skinning_data) {
memdelete(skinning_data);
skinning_data = nullptr;
}
}
Item() {
light_mask = 1;
Expand Down
32 changes: 32 additions & 0 deletions servers/visual/visual_server_canvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +1523,38 @@ void VisualServerCanvas::canvas_item_set_z_as_relative_to_parent(RID p_item, boo
_check_bound_integrity(canvas_item);
}

Rect2 VisualServerCanvas::_debug_canvas_item_get_bounding_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);
Expand Down
2 changes: 2 additions & 0 deletions servers/visual/visual_server_canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ class VisualServerCanvas {
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_bounding_rect(RID p_item);

void canvas_item_clear(RID p_item);
void canvas_item_set_draw_index(RID p_item, int p_index);
Expand Down
2 changes: 2 additions & 0 deletions servers/visual/visual_server_raster.h
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,8 @@ class VisualServerRaster : public VisualServer {
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_bounding_rect, RID)

BIND1(canvas_item_clear, RID)
BIND2(canvas_item_set_draw_index, RID, int)
Expand Down
2 changes: 2 additions & 0 deletions servers/visual/visual_server_wrap_mt.h
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,8 @@ class VisualServerWrapMT : public VisualServer {
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_bounding_rect, RID)

FUNC1(canvas_item_clear, RID)
FUNC2(canvas_item_set_draw_index, RID, int)
Expand Down
1 change: 1 addition & 0 deletions servers/visual_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_bounding_rect", "item"), &VisualServer::debug_canvas_item_get_bounding_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);
Expand Down
Loading

0 comments on commit 030b3c9

Please sign in to comment.