From 7127a8cf0d8dcd5043ecd9e6ae655626cfd8258c Mon Sep 17 00:00:00 2001 From: Colin O'Rourke Date: Wed, 7 Jan 2026 20:43:44 -0800 Subject: [PATCH] Fix GPU & CPU Particle Gizmos deforming based on CanvasItem Adds a redundant Gizmo_Canvas_Item to GPU & CPU Particles2D, which is used to draw Visibility and Emission Gizmos independently of the original canvas item (resolving the issue where these gizmos would deform based on CanvasItem Particle_Anim) Also moves Circle & Rect Outline Calculation to Geometry2D for Code simplification. --- core/math/geometry_2d.h | 32 ++++++++++++++++++++++++++ scene/2d/cpu_particles_2d.cpp | 35 +++++++++++++++++++--------- scene/2d/cpu_particles_2d.h | 1 + scene/2d/gpu_particles_2d.cpp | 43 +++++++++++++++++++++++++++-------- scene/2d/gpu_particles_2d.h | 1 + scene/main/canvas_item.cpp | 27 +++------------------- 6 files changed, 94 insertions(+), 45 deletions(-) diff --git a/core/math/geometry_2d.h b/core/math/geometry_2d.h index fa5b0f061bda..03b5101fe5a5 100644 --- a/core/math/geometry_2d.h +++ b/core/math/geometry_2d.h @@ -431,6 +431,38 @@ class Geometry2D { return (real_t)(A.x - O.x) * (B.y - O.y) - (real_t)(A.y - O.y) * (B.x - O.x); } + static Vector get_rect_outline(const Rect2 &p_rect) { + Vector points; + points.resize(5); + points.write[0] = p_rect.position; + points.write[1] = p_rect.position + Vector2(p_rect.size.x, 0); + points.write[2] = p_rect.position + p_rect.size; + points.write[3] = p_rect.position + Vector2(0, p_rect.size.y); + points.write[4] = p_rect.position; + return points; + } + + static Vector get_ellipse_outline(const Point2 &p_point, real_t p_major, real_t p_minor) { + // Tessellation count is hardcoded. Keep in sync with the same variable in `RendererCanvasCull::canvas_item_add_circle()`. + const int circle_segments = 64; + + Vector points; + points.resize(circle_segments + 1); + + Vector2 *points_ptr = points.ptrw(); + const real_t circle_point_step = Math::TAU / circle_segments; + + for (int i = 0; i < circle_segments; i++) { + float angle = i * circle_point_step; + points_ptr[i].x = Math::cos(angle) * p_major; + points_ptr[i].y = Math::sin(angle) * p_minor; + points_ptr[i] += p_point; + } + points_ptr[circle_segments] = points_ptr[0]; + + return points; + } + // Returns a list of points on the convex hull in counter-clockwise order. // Note: the last point in the returned list is the same as the first one. static Vector convex_hull(Vector P) { diff --git a/scene/2d/cpu_particles_2d.cpp b/scene/2d/cpu_particles_2d.cpp index c4e7f0389449..f509aa7b06a1 100644 --- a/scene/2d/cpu_particles_2d.cpp +++ b/scene/2d/cpu_particles_2d.cpp @@ -31,6 +31,7 @@ #include "cpu_particles_2d.h" #include "cpu_particles_2d.compat.inc" +#include "core/math/geometry_2d.h" #include "core/math/random_number_generator.h" #include "core/math/transform_interpolator.h" #include "scene/2d/gpu_particles_2d.h" @@ -602,6 +603,9 @@ void CPUParticles2D::set_show_gizmos(bool p_show_gizmos) { return; } show_gizmos = p_show_gizmos; + if (!show_gizmos) { + RS::get_singleton()->canvas_item_clear(gizmo_canvas_item); + } queue_redraw(); } #endif @@ -1277,6 +1281,7 @@ void CPUParticles2D::_notification(int p_what) { #ifdef TOOLS_ENABLED if (show_gizmos) { + RS::get_singleton()->canvas_item_clear(gizmo_canvas_item); _draw_emission_gizmo(); } #endif @@ -1320,23 +1325,22 @@ void CPUParticles2D::_notification(int p_what) { #ifdef TOOLS_ENABLED void CPUParticles2D::_draw_emission_gizmo() { Color emission_ring_color = Color(0.8, 0.7, 0.4, 0.4); - Transform2D gizmo_transform; - if (!local_coords) { - gizmo_transform = get_global_transform(); - } - - draw_set_transform_matrix(gizmo_transform); switch (emission_shape) { - case CPUParticles2D::EMISSION_SHAPE_RECTANGLE: - draw_rect(Rect2(-emission_rect_extents, emission_rect_extents * 2.0), emission_ring_color, false); + case CPUParticles2D::EMISSION_SHAPE_RECTANGLE: { + Vector pos = Geometry2D::get_rect_outline(Rect2(-emission_rect_extents, emission_rect_extents * 2.0)); + RS::get_singleton()->canvas_item_add_polyline(gizmo_canvas_item, pos, { emission_ring_color }); break; + } case CPUParticles2D::EMISSION_SHAPE_SPHERE: - case CPUParticles2D::EMISSION_SHAPE_SPHERE_SURFACE: - draw_circle(Vector2(), emission_sphere_radius, emission_ring_color, false); + case CPUParticles2D::EMISSION_SHAPE_SPHERE_SURFACE: { + Vector pos = Geometry2D::get_ellipse_outline(Vector2(), emission_sphere_radius, emission_sphere_radius); + RS::get_singleton()->canvas_item_add_polyline(gizmo_canvas_item, pos, { emission_ring_color }); break; - default: + } + default: { break; + } } } #endif @@ -1671,6 +1675,12 @@ CPUParticles2D::CPUParticles2D() { multimesh = RenderingServer::get_singleton()->multimesh_create(); RenderingServer::get_singleton()->multimesh_set_mesh(multimesh, mesh); +#ifdef TOOLS_ENABLED + gizmo_canvas_item = RS::get_singleton()->canvas_item_create(); + RS::get_singleton()->canvas_item_set_z_index(gizmo_canvas_item, RS::CANVAS_ITEM_Z_MAX - 1); + RS::get_singleton()->canvas_item_set_parent(gizmo_canvas_item, get_canvas_item()); +#endif + set_emitting(true); set_amount(8); set_use_local_coordinates(false); @@ -1723,4 +1733,7 @@ CPUParticles2D::~CPUParticles2D() { ERR_FAIL_NULL(RenderingServer::get_singleton()); RS::get_singleton()->free_rid(multimesh); RS::get_singleton()->free_rid(mesh); +#ifdef TOOLS_ENABLED + RS::get_singleton()->free_rid(gizmo_canvas_item); +#endif } diff --git a/scene/2d/cpu_particles_2d.h b/scene/2d/cpu_particles_2d.h index dc838622abca..1a990073b24b 100644 --- a/scene/2d/cpu_particles_2d.h +++ b/scene/2d/cpu_particles_2d.h @@ -142,6 +142,7 @@ class CPUParticles2D : public Node2D { Transform2D inv_emission_transform; #ifdef TOOLS_ENABLED + RID gizmo_canvas_item; bool show_gizmos = false; #endif diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 924cfd2acac0..c165add89e95 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -31,6 +31,7 @@ #include "gpu_particles_2d.h" #include "gpu_particles_2d.compat.inc" +#include "core/math/geometry_2d.h" #include "scene/2d/cpu_particles_2d.h" #include "scene/resources/atlas_texture.h" #include "scene/resources/canvas_item_material.h" @@ -218,6 +219,9 @@ void GPUParticles2D::set_show_gizmos(bool p_show_gizmos) { return; } show_gizmos = p_show_gizmos; + if (!show_gizmos) { + RS::get_singleton()->canvas_item_clear(gizmo_canvas_item); + } queue_redraw(); } #endif @@ -729,7 +733,9 @@ void GPUParticles2D::_notification(int p_what) { #ifdef TOOLS_ENABLED if (show_gizmos) { - draw_rect(visibility_rect, Color(0, 0.7, 0.9, 0.4), false); + RS::get_singleton()->canvas_item_clear(gizmo_canvas_item); + Vector pos = Geometry2D::get_rect_outline(visibility_rect); + RS::get_singleton()->canvas_item_add_polyline(gizmo_canvas_item, pos, { Color(0, 0.7, 0.9, 0.4) }); _draw_emission_gizmo(); } #endif @@ -813,27 +819,35 @@ void GPUParticles2D::_draw_emission_gizmo() { if (pm.is_null()) { return; } - draw_set_transform( - Vector2(pm->get_emission_shape_offset().x, pm->get_emission_shape_offset().y), + + Transform2D xform( 0.0, - Vector2(pm->get_emission_shape_scale().x, pm->get_emission_shape_scale().y)); + Vector2(pm->get_emission_shape_scale().x, pm->get_emission_shape_scale().y), + 0.0, + Vector2(pm->get_emission_shape_offset().x, pm->get_emission_shape_offset().y)); + RS::get_singleton()->canvas_item_add_set_transform(gizmo_canvas_item, xform); + Vector colors = { emission_ring_color }; switch (pm->get_emission_shape()) { case ParticleProcessMaterial::EmissionShape::EMISSION_SHAPE_BOX: { Vector2 extents2d = Vector2(pm->get_emission_box_extents().x, pm->get_emission_box_extents().y); - draw_rect(Rect2(-extents2d, extents2d * 2.0), emission_ring_color, false); + Vector pos = Geometry2D::get_rect_outline(Rect2(-extents2d, extents2d * 2.0)); + RS::get_singleton()->canvas_item_add_polyline(gizmo_canvas_item, pos, colors); break; } case ParticleProcessMaterial::EmissionShape::EMISSION_SHAPE_SPHERE: case ParticleProcessMaterial::EmissionShape::EMISSION_SHAPE_SPHERE_SURFACE: { - draw_circle(Vector2(), pm->get_emission_sphere_radius(), emission_ring_color, false); + Vector pos = Geometry2D::get_ellipse_outline(Vector2(), pm->get_emission_sphere_radius(), pm->get_emission_sphere_radius()); + RS::get_singleton()->canvas_item_add_polyline(gizmo_canvas_item, pos, colors); break; } case ParticleProcessMaterial::EmissionShape::EMISSION_SHAPE_RING: { Vector3 ring_axis = pm->get_emission_ring_axis(); if (ring_axis.is_equal_approx(Vector3(0.0, 0.0, 1.0)) || ring_axis.is_zero_approx()) { - draw_circle(Vector2(), pm->get_emission_ring_inner_radius(), emission_ring_color, false); - draw_circle(Vector2(), pm->get_emission_ring_radius(), emission_ring_color, false); + Vector pos_inner = Geometry2D::get_ellipse_outline(Vector2(), pm->get_emission_ring_inner_radius(), pm->get_emission_ring_inner_radius()); + Vector pos_outer = Geometry2D::get_ellipse_outline(Vector2(), pm->get_emission_ring_radius(), pm->get_emission_ring_radius()); + RS::get_singleton()->canvas_item_add_polyline(gizmo_canvas_item, pos_inner, colors); + RS::get_singleton()->canvas_item_add_polyline(gizmo_canvas_item, pos_outer, colors); } else { Vector2 a = Vector2(pm->get_emission_ring_height() / -2.0, pm->get_emission_ring_radius() / -1.0); Vector2 b = Vector2(-a.x, MIN(a.y + std::tan((90.0 - pm->get_emission_ring_cone_angle()) * 0.01745329) * pm->get_emission_ring_height(), 0.0)); @@ -841,14 +855,14 @@ void GPUParticles2D::_draw_emission_gizmo() { Vector2 d = Vector2(a.x, -a.y); if (ring_axis.is_equal_approx(Vector3(1.0, 0.0, 0.0))) { Vector pos = { a, b, b, c, c, d, d, a }; - draw_multiline(pos, emission_ring_color); + RS::get_singleton()->canvas_item_add_multiline(gizmo_canvas_item, pos, colors); } else if (ring_axis.is_equal_approx(Vector3(0.0, 1.0, 0.0))) { a = Vector2(a.y, a.x); b = Vector2(b.y, b.x); c = Vector2(c.y, c.x); d = Vector2(d.y, d.x); Vector pos = { a, b, b, c, c, d, d, a }; - draw_multiline(pos, emission_ring_color); + RS::get_singleton()->canvas_item_add_multiline(gizmo_canvas_item, pos, colors); } } break; @@ -990,6 +1004,12 @@ GPUParticles2D::GPUParticles2D() { RS::get_singleton()->particles_set_draw_passes(particles, 1); RS::get_singleton()->particles_set_draw_pass_mesh(particles, 0, mesh); +#ifdef TOOLS_ENABLED + gizmo_canvas_item = RS::get_singleton()->canvas_item_create(); + RS::get_singleton()->canvas_item_set_z_index(gizmo_canvas_item, RS::CANVAS_ITEM_Z_MAX - 1); + RS::get_singleton()->canvas_item_set_parent(gizmo_canvas_item, get_canvas_item()); +#endif + one_shot = false; // Needed so that set_emitting doesn't access uninitialized values set_emitting(true); set_one_shot(false); @@ -1016,4 +1036,7 @@ GPUParticles2D::~GPUParticles2D() { ERR_FAIL_NULL(RenderingServer::get_singleton()); RS::get_singleton()->free_rid(particles); RS::get_singleton()->free_rid(mesh); +#ifdef TOOLS_ENABLED + RS::get_singleton()->free_rid(gizmo_canvas_item); +#endif } diff --git a/scene/2d/gpu_particles_2d.h b/scene/2d/gpu_particles_2d.h index db3286a013c0..ecb614eebcc1 100644 --- a/scene/2d/gpu_particles_2d.h +++ b/scene/2d/gpu_particles_2d.h @@ -68,6 +68,7 @@ class GPUParticles2D : public Node2D { uint32_t seed = 0; bool use_fixed_seed = false; #ifdef TOOLS_ENABLED + RID gizmo_canvas_item; bool show_gizmos = false; #endif Ref process_material; diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index ffbe9dc21d37..28f0fcbdc9ef 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -31,6 +31,7 @@ #include "canvas_item.h" #include "canvas_item.compat.inc" +#include "core/math/geometry_2d.h" #include "scene/2d/canvas_group.h" #include "scene/main/canvas_layer.h" #include "scene/main/window.h" @@ -827,13 +828,7 @@ void CanvasItem::draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_fil } else if (p_width >= rect.size.width || p_width >= rect.size.height) { RenderingServer::get_singleton()->canvas_item_add_rect(canvas_item, rect.grow(0.5f * p_width), p_color, p_antialiased); } else { - Vector points; - points.resize(5); - points.write[0] = rect.position; - points.write[1] = rect.position + Vector2(rect.size.x, 0); - points.write[2] = rect.position + rect.size; - points.write[3] = rect.position + Vector2(0, rect.size.y); - points.write[4] = rect.position; + Vector points = Geometry2D::get_rect_outline(rect); Vector colors = { p_color }; @@ -844,7 +839,6 @@ void CanvasItem::draw_rect(const Rect2 &p_rect, const Color &p_color, bool p_fil void CanvasItem::draw_ellipse(const Point2 &p_pos, real_t p_major, real_t p_minor, const Color &p_color, bool p_filled, real_t p_width, bool p_antialiased) { ERR_THREAD_GUARD; ERR_DRAW_GUARD; - if (p_filled) { if (p_width != -1.0) { WARN_PRINT("The \"width\" argument has no effect when \"filled\" is \"true\"."); @@ -854,22 +848,7 @@ void CanvasItem::draw_ellipse(const Point2 &p_pos, real_t p_major, real_t p_mino } else if (p_width >= 2.0 * MAX(p_major, p_minor)) { RenderingServer::get_singleton()->canvas_item_add_ellipse(canvas_item, p_pos, p_major + 0.5 * p_width, p_minor + 0.5 * p_width, p_color, p_antialiased); } else { - // Tessellation count is hardcoded. Keep in sync with the same variable in `RendererCanvasCull::canvas_item_add_circle()`. - const int circle_segments = 64; - - Vector points; - points.resize(circle_segments + 1); - - Vector2 *points_ptr = points.ptrw(); - const real_t circle_point_step = Math::TAU / circle_segments; - - for (int i = 0; i < circle_segments; i++) { - float angle = i * circle_point_step; - points_ptr[i].x = Math::cos(angle) * p_major; - points_ptr[i].y = Math::sin(angle) * p_minor; - points_ptr[i] += p_pos; - } - points_ptr[circle_segments] = points_ptr[0]; + Vector points = Geometry2D::get_ellipse_outline(p_pos, p_major, p_minor); Vector colors = { p_color };