浏览代码

Merge pull request #75612 from lawnjelly/fix_skele2d_bounds2

[3.x] Fix Polygon2D skinned bounds (for culling)
Rémi Verschelde 2 年之前
父节点
当前提交
bfb6877b3c

+ 8 - 0
doc/classes/VisualServer.xml

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

+ 10 - 0
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());
 			}

+ 170 - 0
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<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;
+}

+ 32 - 55
servers/visual/rasterizer.h

@@ -907,10 +907,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 {
@@ -983,6 +997,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;
@@ -999,6 +1019,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;
@@ -1093,61 +1118,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);
@@ -1213,6 +1185,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;

+ 32 - 0
servers/visual/visual_server_canvas.cpp

@@ -943,6 +943,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);

+ 2 - 0
servers/visual/visual_server_canvas.h

@@ -209,6 +209,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);

+ 2 - 0
servers/visual/visual_server_raster.h

@@ -719,6 +719,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)

+ 2 - 0
servers/visual/visual_server_wrap_mt.h

@@ -620,6 +620,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)

+ 1 - 0
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);

+ 10 - 0
servers/visual_server.h

@@ -1053,6 +1053,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;