Skip to content

Commit

Permalink
Draw outline ellipse and circle with geometry shader
Browse files Browse the repository at this point in the history
  • Loading branch information
einarf committed Apr 16, 2020
1 parent c9fe324 commit 201d5e8
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 73 deletions.
158 changes: 93 additions & 65 deletions arcade/draw_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def draw_circle_filled(center_x: float, center_y: float, radius: float,

def draw_circle_outline(center_x: float, center_y: float, radius: float,
color: Color, border_width: float = 1,
num_segments: int = 128):
num_segments: int = -1):
"""
Draw the outline of a circle.
Expand All @@ -234,14 +234,16 @@ def draw_circle_outline(center_x: float, center_y: float, radius: float,
:param Color color: color, specified in a list of 3 or 4 bytes in RGB or
RGBA format.
:param float border_width: Width of the circle outline in pixels.
:param int num_segments: Int of triangle segments that make up this
:param int num_segments: Number of triangle segments that make up this
circle. Higher is better quality, but slower render time.
The default value of -1 means arcade will try to calulate a reasonable
amount of segments based on the size of the circle.
"""
width = radius * 2
height = radius * 2
draw_ellipse_outline(center_x, center_y, width, height,
color, border_width, num_segments=num_segments)

# width = radius * 2
# height = radius * 2
# draw_ellipse_outline(center_x, center_y, width, height,
# color, border_width, num_segments=num_segments)
draw_ellipse_outline(center_x, center_y, radius * 2, radius * 2, color, border_width, num_segments)

# --- END CIRCLE FUNCTIONS # # #

Expand Down Expand Up @@ -271,7 +273,7 @@ def draw_ellipse_filled(center_x: float, center_y: float,
raise RuntimeError("No window found")

ctx = window.ctx
program = ctx.shape_ellipse_unbuffered_program
program = ctx.shape_ellipse_filled_unbuffered_program
geometry = ctx.shape_ellipse_unbuffered_geometry
buffer = ctx.shape_ellipse_unbuffered_buffer
# We need to normalize the color because we are setting it as a float uniform
Expand Down Expand Up @@ -317,7 +319,7 @@ def draw_ellipse_filled(center_x: float, center_y: float,
def draw_ellipse_outline(center_x: float, center_y: float, width: float,
height: float, color: Color,
border_width: float = 1, tilt_angle: float = 0,
num_segments: int = 128):
num_segments: int = -1):
"""
Draw the outline of an ellipse.
Expand All @@ -329,68 +331,94 @@ def draw_ellipse_outline(center_x: float, center_y: float, width: float,
RGBA format.
:param float border_width: Width of the circle outline in pixels.
:param float tilt_angle: Angle in degrees to tilt the ellipse.
:param int num_segments: Number of line segments used to make the ellipse
:param int num_segments: Number of triangle segments that make up this
circle. Higher is better quality, but slower render time.
The default value of -1 means arcade will try to calulate a reasonable
amount of segments based on the size of the circle.
"""
window = get_window()
if not window:
raise RuntimeError("No window found")

if border_width == 1:
unrotated_point_list = []

for segment in range(num_segments):
theta = 2.0 * 3.1415926 * segment / num_segments

x = (width / 2) * math.cos(theta)
y = (height / 2) * math.sin(theta)

unrotated_point_list.append([x, y])

if tilt_angle == 0:
uncentered_point_list = unrotated_point_list
else:
uncentered_point_list = []
for point in unrotated_point_list:
uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))

point_list = []
for point in uncentered_point_list:
point_list.append((point[0] + center_x, point[1] + center_y))

_generic_draw_line_strip(point_list, color, gl.GL_LINE_LOOP)
ctx = window.ctx
program = ctx.shape_ellipse_outline_unbuffered_program
geometry = ctx.shape_ellipse_outline_unbuffered_geometry
buffer = ctx.shape_ellipse_outline_unbuffered_buffer
# We need to normalize the color because we are setting it as a float uniform
if len(color) == 3:
color_normalized = (color[0] / 255, color[1] / 255, color[2] / 255, 1.0)
elif len(color) == 4:
color_normalized = (color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255) # type: ignore
else:
raise ValueError("Invalid color format. Use a 3 or 4 component tuple")

unrotated_point_list = []

start_segment = 0
end_segment = num_segments

inside_width = (width / 2) - border_width / 2
outside_width = (width / 2) + border_width / 2
inside_height = (height / 2) - border_width / 2
outside_height = (height / 2) + border_width / 2

for segment in range(start_segment, end_segment + 1):
theta = 2.0 * math.pi * segment / num_segments

x1 = inside_width * math.cos(theta)
y1 = inside_height * math.sin(theta)

x2 = outside_width * math.cos(theta)
y2 = outside_height * math.sin(theta)

unrotated_point_list.append([x1, y1])
unrotated_point_list.append([x2, y2])

if tilt_angle == 0:
uncentered_point_list = unrotated_point_list
else:
uncentered_point_list = []
for point in unrotated_point_list:
uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
program['Projection'] = get_projection().flatten()
program['color'] = color_normalized
program['shape'] = width / 2, height / 2, tilt_angle, border_width
program['segments'] = num_segments
buffer.write(data=array.array('f', (center_x, center_y)).tobytes())

point_list = []
for point in uncentered_point_list:
point_list.append((point[0] + center_x, point[1] + center_y))
geometry.render(program, mode=gl.GL_POINTS, vertices=1)

_generic_draw_line_strip(point_list, color, gl.GL_TRIANGLE_STRIP)
# if border_width == 1:
# unrotated_point_list = []
#
# for segment in range(num_segments):
# theta = 2.0 * 3.1415926 * segment / num_segments
#
# x = (width / 2) * math.cos(theta)
# y = (height / 2) * math.sin(theta)
#
# unrotated_point_list.append([x, y])
#
# if tilt_angle == 0:
# uncentered_point_list = unrotated_point_list
# else:
# uncentered_point_list = []
# for point in unrotated_point_list:
# uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
#
# point_list = []
# for point in uncentered_point_list:
# point_list.append((point[0] + center_x, point[1] + center_y))
#
# _generic_draw_line_strip(point_list, color, gl.GL_LINE_LOOP)
# else:
#
# unrotated_point_list = []
#
# start_segment = 0
# end_segment = num_segments
#
# inside_width = (width / 2) - border_width / 2
# outside_width = (width / 2) + border_width / 2
# inside_height = (height / 2) - border_width / 2
# outside_height = (height / 2) + border_width / 2
#
# for segment in range(start_segment, end_segment + 1):
# theta = 2.0 * math.pi * segment / num_segments
#
# x1 = inside_width * math.cos(theta)
# y1 = inside_height * math.sin(theta)
#
# x2 = outside_width * math.cos(theta)
# y2 = outside_height * math.sin(theta)
#
# unrotated_point_list.append([x1, y1])
# unrotated_point_list.append([x2, y2])
#
# if tilt_angle == 0:
# uncentered_point_list = unrotated_point_list
# else:
# uncentered_point_list = []
# for point in unrotated_point_list:
# uncentered_point_list.append(rotate_point(point[0], point[1], 0, 0, tilt_angle))
#
# point_list = []
# for point in uncentered_point_list:
# point_list.append((point[0] + center_x, point[1] + center_y))
#
# _generic_draw_line_strip(point_list, color, gl.GL_TRIANGLE_STRIP)


# --- END ELLIPSE FUNCTIONS # # #
Expand Down
16 changes: 10 additions & 6 deletions arcade/experimental/examples/shapes_perf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def random_radius(start=5, end=25):
class TestWindow(arcade.Window):

def __init__(self, width, height, title):
super().__init__(width, height, title, antialiasing=False, resizable=True)
super().__init__(width, height, title, antialiasing=True, resizable=True)
# Single lines
self.single_lines_calls = [(*random_pos(), *random_pos(), random_color()) for _ in range(600)]
# Line list
Expand All @@ -51,15 +51,17 @@ def do_draw_lines(self):
arcade.draw_lines(self.line_list, (255, 0, 0, 10))

def do_draw_circle_filled(self):
# for c in self.single_circle_calls:
# arcade.draw_circle_filled(c[0], c[1], c[2], c[3])
arcade.draw_circle_filled(400, 300, 300, arcade.color.AZURE)
for c in self.single_circle_calls:
arcade.draw_circle_filled(c[0], c[1], c[2], c[3])

def do_draw_ellipse_filled(self):
arcade.draw_ellipse_filled(400, 300, 100, 200, arcade.color.AZURE, self.elapsed * 10)

def do_draw_circle_outline(self):
pass
arcade.draw_circle_outline(400, 300, 200, arcade.color.AZURE, 10)

def do_draw_ellipse_outline(self):
arcade.draw_ellipse_outline(400, 300, 230, 100, arcade.color.AZURE, 10, 45)

def on_draw(self):
try:
Expand All @@ -70,8 +72,10 @@ def on_draw(self):
# Toggle what to test here
# self.do_draw_line()
# self.do_draw_lines()
self.do_draw_circle_filled()
# self.do_draw_circle_filled()
# self.do_draw_ellipse_filled()
self.do_draw_circle_outline()
self.do_draw_ellipse_outline()

self.execution_time += time.time() - start
self.frames += 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ layout (triangle_strip, max_vertices = 256) out;

uniform mat4 Projection;
uniform int segments;
// [x, y, tilt]
// [w, h, tilt]
uniform vec3 shape;

void main() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#version 330

uniform vec4 color;

out vec4 f_color;

void main() {
f_color = color;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#version 330

#define PI 3.1415926535897932384626433832795
#define MIN_SEGMENTS 16
#define MAX_SEGMENTS 112

layout (points) in;
// TODO: We might want to increase the number of emitted verties, 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 mat4 Projection;
uniform int segments;
// [w, h, tilt, thinkness]
uniform vec4 shape;

void main() {
// Get center of the circle
vec2 center = gl_in[0].gl_Position.xy;
int segments_selected = 0;

// Calculate rotation/tilt
float angle = radians(shape.z);
mat2 rot = mat2(
cos(angle), -sin(angle),
sin(angle), cos(angle)
);

if (segments > 0) {
// The user defined number of segments. Clamp it.
segments_selected = segments;
} else {
// Estimate the number of segments needed based on size
segments_selected = int(2.0 * PI * max(shape.x, shape.y) / 10.0);
}
// 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 step = PI * 2 / segments_selected;

// Draw think circle with triangle strip. This can be handled as a single primive by the gpu.
// Number of vertices is segments * 2 + 2, so we need to emit the inital vertex first

// First outer vertex
vec2 p_start = vec2(sin(0), cos(0)) * shape.xy;
gl_Position = Projection * 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((i) * step), cos((i) * step)) * (shape.xy - vec2(shape.w));
gl_Position = Projection * vec4((rot * p1) + center, 0.0, 1.0);
EmitVertex();

// Outer vertex
vec2 p2 = vec2(sin((i + 1) * step), cos((i + 1) * step)) * shape.xy;
gl_Position = Projection * vec4((rot * p2) + center, 0.0, 1.0);
EmitVertex();
}
// Last inner vertex to wrap up
vec2 p_end = vec2(sin(0), cos(0)) * (shape.xy - vec2(shape.w));
gl_Position = Projection * vec4((rot * p_end) + center, 0.0, 1.0);
EmitVertex();

EndPrimitive();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#version 330

in vec2 in_vert;

void main() {
gl_Position = vec4(in_vert, 0.0, 1.0);
}
11 changes: 10 additions & 1 deletion arcade/shader.py
Original file line number Diff line number Diff line change
Expand Up @@ -1856,11 +1856,16 @@ def __init__(self, window):
fragment_shader=":resources:/shaders/shapes/line/unbuffered_fs.glsl",
geometry_shader=":resources:/shaders/shapes/line/unbuffered_geo.glsl",
)
self.shape_ellipse_unbuffered_program = self.load_program(
self.shape_ellipse_filled_unbuffered_program = self.load_program(
vertex_shader=":resources:/shaders/shapes/ellipse/filled_unbuffered_vs.glsl",
fragment_shader=":resources:/shaders/shapes/ellipse/filled_unbuffered_fs.glsl",
geometry_shader=":resources:/shaders/shapes/ellipse/filled_unbuffered_geo.glsl",
)
self.shape_ellipse_outline_unbuffered_program = self.load_program(
vertex_shader=":resources:/shaders/shapes/ellipse/outline_unbuffered_vs.glsl",
fragment_shader=":resources:/shaders/shapes/ellipse/outline_unbuffered_fs.glsl",
geometry_shader=":resources:/shaders/shapes/ellipse/outline_unbuffered_geo.glsl",
)

# --- Pre-created geometry and buffers for unbuffered draw calls ----
# FIXME: These pre-created resources needs to be packaged nicely
Expand All @@ -1884,6 +1889,10 @@ def __init__(self, window):
self.shape_ellipse_unbuffered_buffer = self.buffer(reserve=8)
self.shape_ellipse_unbuffered_geometry = self.geometry([
BufferDescription(self.shape_ellipse_unbuffered_buffer, '2f', ['in_vert'])])
# ellipse/circle outline
self.shape_ellipse_outline_unbuffered_buffer = self.buffer(reserve=8)
self.shape_ellipse_outline_unbuffered_geometry = self.geometry([
BufferDescription(self.shape_ellipse_outline_unbuffered_buffer, '2f', ['in_vert'])])

@property
def window(self):
Expand Down

0 comments on commit 201d5e8

Please sign in to comment.