From ae877415fdde4ae0b392e3310a12945a3b182f1d Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 7 Jun 2025 21:11:28 +0200 Subject: [PATCH 1/4] Fix rect rotation --- .../system/shaders/shapes/rectangle/filled_unbuffered_vs.glsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/resources/system/shaders/shapes/rectangle/filled_unbuffered_vs.glsl b/arcade/resources/system/shaders/shapes/rectangle/filled_unbuffered_vs.glsl index 6560d426c9..fbeb22178c 100644 --- a/arcade/resources/system/shaders/shapes/rectangle/filled_unbuffered_vs.glsl +++ b/arcade/resources/system/shaders/shapes/rectangle/filled_unbuffered_vs.glsl @@ -19,6 +19,6 @@ void main() { ); // vec2 size = shape.xy / 2.0; mat4 mvp = window.projection * window.view; - vec2 pos = in_instance_pos + (in_vert * shape.xy); - gl_Position = mvp * vec4(rot * pos, 0.0, 1.0); + vec2 pos = in_instance_pos + (rot * (in_vert * shape.xy)); + gl_Position = mvp * vec4(pos, 0.0, 1.0); } From 90a203c8718c8cfaae03454ba83ca183e9d31feb Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 7 Jun 2025 22:11:30 +0200 Subject: [PATCH 2/4] unbuffered ellipse shader: remove geometry shader --- arcade/context.py | 8 +-- arcade/draw/circle.py | 20 ++++-- .../shapes/ellipse/filled_unbuffered_geo.glsl | 68 ------------------- .../shapes/ellipse/filled_unbuffered_vs.glsl | 34 +++++++++- 4 files changed, 49 insertions(+), 81 deletions(-) delete mode 100644 arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_geo.glsl diff --git a/arcade/context.py b/arcade/context.py index 2cbbc33d7b..fbd886d16f 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -159,7 +159,6 @@ def __init__( self.shape_ellipse_filled_unbuffered_program: Program = self.load_program( vertex_shader=":system:shaders/shapes/ellipse/filled_unbuffered_vs.glsl", fragment_shader=":system:shaders/shapes/ellipse/filled_unbuffered_fs.glsl", - geometry_shader=":system:shaders/shapes/ellipse/filled_unbuffered_geo.glsl", ) self.shape_ellipse_outline_unbuffered_program: Program = self.load_program( vertex_shader=":system:shaders/shapes/ellipse/outline_unbuffered_vs.glsl", @@ -258,11 +257,8 @@ def __init__( ], mode=self.TRIANGLE_STRIP, ) - # ellipse/circle filled - self.shape_ellipse_unbuffered_buffer = self.buffer(reserve=8) - self.shape_ellipse_unbuffered_geometry: Geometry = self.geometry( - [BufferDescription(self.shape_ellipse_unbuffered_buffer, "2f", ["in_vert"])] - ) + # ellipse/circle filled. Empty geometry. We generate it on the fly in the vertex shader. + self.shape_ellipse_unbuffered_geometry: Geometry = self.geometry() # ellipse/circle outline self.shape_ellipse_outline_unbuffered_buffer = self.buffer(reserve=8) self.shape_ellipse_outline_unbuffered_geometry: Geometry = self.geometry( diff --git a/arcade/draw/circle.py b/arcade/draw/circle.py index 729859c55f..3028e3075f 100644 --- a/arcade/draw/circle.py +++ b/arcade/draw/circle.py @@ -129,23 +129,31 @@ def draw_ellipse_filled( # Fail immediately if we have no window or context window = get_window() ctx = window.ctx - ctx.enable(ctx.BLEND) program = ctx.shape_ellipse_filled_unbuffered_program geometry = ctx.shape_ellipse_unbuffered_geometry - buffer = ctx.shape_ellipse_unbuffered_buffer # type: ignore # Normalize the color because this shader takes a float uniform color_normalized = Color.from_iterable(color).normalized + # Auto select number of segments if not specified + if num_segments == -1: + size = max(width, height) + if size <= 12: + num_segments = 6 + else: + num_segments = int(size) // 2 + + if num_segments < 3: + num_segments = 3 + # Pass data to the shader + program["center"] = center_x, center_y program["color"] = color_normalized program["shape"] = width / 2, height / 2, tilt_angle program["segments"] = num_segments - buffer.orphan() - buffer.write(data=array.array("f", (center_x, center_y))) - - geometry.render(program, mode=gl.POINTS, vertices=1) + ctx.enable(ctx.BLEND) + geometry.render(program, mode=ctx.TRIANGLES, vertices=num_segments * 3) ctx.disable(ctx.BLEND) diff --git a/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_geo.glsl b/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_geo.glsl deleted file mode 100644 index e1c9a7f377..0000000000 --- a/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_geo.glsl +++ /dev/null @@ -1,68 +0,0 @@ -#version 330 - -// 3 points per segment, max of 256 points, so 85 * 3 = 255 -const int MIN_SEGMENTS = 3; -const int MAX_SEGMENTS = 112; -const float PI = 3.141592; - -layout (points) in; -// TODO: We might want to increase the number of emitted vertices, but core 3.3 says 256 is min requirement. -// TODO: Normally 4096 is supported, but let's stay on the safe side -layout (triangle_strip, max_vertices = 256) out; - -uniform WindowBlock { - mat4 projection; - mat4 view; -} window; - -uniform int segments; -// [w, h, tilt] -uniform vec3 shape; - -void main() { - // Get center of the circle - vec2 center = gl_in[0].gl_Position.xy; - int segments_selected = segments; - - // Calculate rotation/tilt - float angle = radians(shape.z); - mat2 rot = mat2( - cos(angle), -sin(angle), - sin(angle), cos(angle) - ); - - if (segments_selected < 0) { - // Estimate the number of segments needed based on size - float size = max(shape.x, shape.y); - if (size <= 4.0) - segments_selected = 4; - else if (size <= 16.0) - segments_selected = 16; - else - segments_selected = 32; - } - // Clamp number of segments - segments_selected = clamp(segments_selected, MIN_SEGMENTS, MAX_SEGMENTS); - - // sin(v), cos(v) travels clockwise around the circle starting at 0, 1 (top of circle) - float st = PI * 2.0 / float(segments_selected); - - for (int i = 0; i < segments_selected; i++) { - gl_Position = window.projection * window.view * vec4(center, 0.0, 1.0); - EmitVertex(); - - // Calculate the ellipse/circle using 0, 0 as origin - vec2 p1 = vec2(sin((float(i) + 1.0) * st), cos((float(i) + 1.0) * st)) * shape.xy; - // Rotate the circle and then add translation to get the right origin - gl_Position = window.projection * window.view * vec4((rot * p1) + center, 0.0, 1.0); - EmitVertex(); - - // Calculate the ellipse/circle using 0, 0 as origin - vec2 p2 = vec2(sin(float(i) * st), cos(float(i) * st)) * shape.xy; - // Rotate the circle and then add translation to get the right origin - gl_Position = window.projection * window.view * vec4((rot * p2) + center, 0.0, 1.0); - EmitVertex(); - - EndPrimitive(); - } -} diff --git a/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl b/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl index 02be2f2654..e75bd4b9ec 100644 --- a/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl +++ b/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl @@ -1,7 +1,39 @@ #version 330 +uniform WindowBlock { + mat4 projection; + mat4 view; +} window; + +uniform vec2 center; +uniform int segments; +// [w, h, tilt] +uniform vec3 shape; + in vec2 in_vert; +const float PI = 3.141592; + void main() { - gl_Position = vec4(in_vert, 0.0, 1.0); + int triangle_id = gl_VertexID / 3; + int vertex_id = gl_VertexID % 3; + + // Calculate rotation/tilt + float angle = radians(shape.z); + mat2 rot = mat2( + cos(angle), -sin(angle), + sin(angle), cos(angle) + ); + // Calculate the positions for the full triangle in the current segment + vec2 positions[3] = vec2[3]( + vec2(0.0, 0.0), + vec2(sin((float(triangle_id) + 1.0) * (PI * 2.0 / float(segments))), + cos((float(triangle_id) + 1.0) * (PI * 2.0 / float(segments)))) * shape.xy, + vec2(sin(float(triangle_id) * (PI * 2.0 / float(segments))), + cos(float(triangle_id) * (PI * 2.0 / float(segments)))) * shape.xy + ); + + mat4 mvp = window.projection * window.view; + vec4 pos = vec4(rot * positions[vertex_id] + center, 0.0, 1.0); + gl_Position = mvp * pos; } From 218b97199ddd7fef8815c0e27176fb6adad83086 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 7 Jun 2025 22:16:13 +0200 Subject: [PATCH 3/4] Remove unused in attribute --- .../system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl | 2 -- 1 file changed, 2 deletions(-) diff --git a/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl b/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl index e75bd4b9ec..efd5f7414d 100644 --- a/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl +++ b/arcade/resources/system/shaders/shapes/ellipse/filled_unbuffered_vs.glsl @@ -10,8 +10,6 @@ uniform int segments; // [w, h, tilt] uniform vec3 shape; -in vec2 in_vert; - const float PI = 3.141592; void main() { From 0eb546096784f174f4195c750ded7933e90c57ae Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 7 Jun 2025 23:39:41 +0200 Subject: [PATCH 4/4] ellipse outline no geometry shader --- arcade/context.py | 8 +- arcade/draw/circle.py | 22 +++--- .../ellipse/outline_unbuffered_geo.glsl | 76 ------------------- .../shapes/ellipse/outline_unbuffered_vs.glsl | 42 +++++++++- 4 files changed, 55 insertions(+), 93 deletions(-) delete mode 100644 arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_geo.glsl diff --git a/arcade/context.py b/arcade/context.py index fbd886d16f..b727fa1795 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -163,7 +163,6 @@ def __init__( self.shape_ellipse_outline_unbuffered_program: Program = self.load_program( vertex_shader=":system:shaders/shapes/ellipse/outline_unbuffered_vs.glsl", fragment_shader=":system:shaders/shapes/ellipse/outline_unbuffered_fs.glsl", - geometry_shader=":system:shaders/shapes/ellipse/outline_unbuffered_geo.glsl", ) self.shape_rectangle_filled_unbuffered_program = self.load_program( vertex_shader=":system:shaders/shapes/rectangle/filled_unbuffered_vs.glsl", @@ -259,11 +258,8 @@ def __init__( ) # ellipse/circle filled. Empty geometry. We generate it on the fly in the vertex shader. self.shape_ellipse_unbuffered_geometry: Geometry = self.geometry() - # ellipse/circle outline - self.shape_ellipse_outline_unbuffered_buffer = self.buffer(reserve=8) - self.shape_ellipse_outline_unbuffered_geometry: Geometry = self.geometry( - [BufferDescription(self.shape_ellipse_outline_unbuffered_buffer, "2f", ["in_vert"])] - ) + # ellipse/circle outline. Empty geometry. We generate it on the fly in the vertex shader. + self.shape_ellipse_outline_unbuffered_geometry: Geometry = self.geometry() # rectangle filled self.shape_rectangle_filled_unbuffered_buffer = self.buffer(reserve=8) # fmt: off diff --git a/arcade/draw/circle.py b/arcade/draw/circle.py index 3028e3075f..c5bc4e98c2 100644 --- a/arcade/draw/circle.py +++ b/arcade/draw/circle.py @@ -1,6 +1,3 @@ -import array - -from arcade import gl from arcade.types import Color, RGBOrA255 from arcade.window_commands import get_window @@ -198,20 +195,27 @@ def draw_ellipse_outline( ctx = window.ctx program = ctx.shape_ellipse_outline_unbuffered_program geometry = ctx.shape_ellipse_outline_unbuffered_geometry - buffer = ctx.shape_ellipse_outline_unbuffered_buffer # type: ignore # Normalize the color because this shader takes a float uniform color_normalized = Color.from_iterable(color).normalized - ctx.enable(ctx.BLEND) + # Auto select number of segments if not specified + if num_segments == -1: + size = max(width, height) + if size <= 12: + num_segments = 6 + else: + num_segments = int(size) // 2 + + if num_segments < 3: + num_segments = 3 # Pass data to shader + program["center"] = center_x, center_y program["color"] = color_normalized program["shape"] = width / 2, height / 2, tilt_angle, border_width program["segments"] = num_segments - buffer.orphan() - buffer.write(data=array.array("f", (center_x, center_y))) - - geometry.render(program, mode=gl.POINTS, vertices=1) + ctx.enable(ctx.BLEND) + geometry.render(program, mode=ctx.TRIANGLES, vertices=num_segments * 6) ctx.disable(ctx.BLEND) diff --git a/arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_geo.glsl b/arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_geo.glsl deleted file mode 100644 index 2b9668ab1b..0000000000 --- a/arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_geo.glsl +++ /dev/null @@ -1,76 +0,0 @@ -#version 330 - -// 3 points per segment, max of 256 points, so 85 * 3 = 255 -const int MIN_SEGMENTS = 3; -const int MAX_SEGMENTS = 112; -const float PI = 3.141592; - -layout (points) in; -// TODO: We might want to increase the number of emitted vertices, but core 3.3 says 256 is min requirement. -// TODO: Normally 4096 is supported, but let's stay on the safe side -layout (triangle_strip, max_vertices = 256) out; - -uniform WindowBlock { - mat4 projection; - mat4 view; -} window; - -uniform int segments; -// [w, h, tilt, thickness] -uniform vec4 shape; - -void main() { - // Get center of the circle - vec2 center = gl_in[0].gl_Position.xy; - int segments_selected = segments; - - // Calculate rotation/tilt - float angle = radians(shape.z); - mat2 rot = mat2( - cos(angle), -sin(angle), - sin(angle), cos(angle) - ); - - if (segments_selected < 0) { - // Estimate the number of segments needed based on size - float size = max(shape.x, shape.y); - if (size <= 4.0) - segments_selected = 4; - else if (size <= 16.0) - segments_selected = 16; - else - segments_selected = 32; - } - // Clamp number of segments - segments_selected = clamp(segments_selected, MIN_SEGMENTS, MAX_SEGMENTS); - - // sin(v), cos(v) travels clockwise around the circle starting at 0, 1 (top of circle) - float st = PI * 2.0 / float(segments_selected); - - // Draw thick circle with triangle strip. This can be handled as a single primitive by the gpu. - // Number of vertices is segments * 2 + 2, so we need to emit the initial vertex first - - // First outer vertex - vec2 p_start = vec2(sin(0.0), cos(0.0)) * shape.xy; - gl_Position = window.projection * window.view * vec4((rot * p_start) + center, 0.0, 1.0); - EmitVertex(); - - // Draw cross segments from inner to outer - for (int i = 0; i < segments_selected; i++) { - // Inner vertex - vec2 p1 = vec2(sin(float(i) * st), cos(float(i) * st)) * (shape.xy - vec2(shape.w)); - gl_Position = window.projection * window.view * vec4((rot * p1) + center, 0.0, 1.0); - EmitVertex(); - - // Outer vertex - vec2 p2 = vec2(sin((float(i) + 1.0) * st), cos((float(i) + 1.0) * st)) * shape.xy; - gl_Position = window.projection * window.view * vec4((rot * p2) + center, 0.0, 1.0); - EmitVertex(); - } - // Last inner vertex to wrap up - vec2 p_end = vec2(sin(0.0), cos(0.0)) * (shape.xy - vec2(shape.w)); - gl_Position = window.projection * window.view * vec4((rot * p_end) + center, 0.0, 1.0); - EmitVertex(); - - EndPrimitive(); -} diff --git a/arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_vs.glsl b/arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_vs.glsl index 02be2f2654..8c0aea9da3 100644 --- a/arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_vs.glsl +++ b/arcade/resources/system/shaders/shapes/ellipse/outline_unbuffered_vs.glsl @@ -1,7 +1,45 @@ #version 330 -in vec2 in_vert; +uniform WindowBlock { + mat4 projection; + mat4 view; +} window; + +uniform vec2 center; +uniform int segments; +// [w, h, tilt, thickness] +uniform vec4 shape; + +const float PI = 3.141592; void main() { - gl_Position = vec4(in_vert, 0.0, 1.0); + // Two triangles per line segment of the outline + int segment_id = gl_VertexID / 6; + int vertex_id = gl_VertexID % 6; + + // Calculate rotation/tilt + float angle = radians(shape.z); + mat2 rot = mat2( + cos(angle), -sin(angle), + sin(angle), cos(angle) + ); + + // sin(v), cos(v) travels clockwise around the circle starting at 0, 1 (top of circle) + float st = PI * 2.0 / float(segments); + + // calculate the four points of the line segment + // Inner and outer points for the start of line segment + vec2 p0 = vec2(sin(float(segment_id) * st), cos(float(segment_id) * st)) * shape.xy; + vec2 p1 = vec2(sin(float(segment_id) * st), cos(float(segment_id) * st)) * (shape.xy - vec2(shape.w)); + + // Inner and outer points for the end of line segment + vec2 p2 = vec2(sin((float(segment_id) + 1.0) * st), cos((float(segment_id) + 1.0) * st)) * shape.xy; + vec2 p3 = vec2(sin((float(segment_id) + 1.0) * st), cos((float(segment_id) + 1.0) * st)) * (shape.xy - vec2(shape.w)); + + vec2 position[6] = vec2[6]( + p1, p0, p2, // first triangle + p1, p2, p3 // second triangle + ); + mat4 mvp = window.projection * window.view; + gl_Position = mvp * vec4(rot * position[vertex_id] + center, 0.0, 1.0); }