add ellipsis shader - resolves #385 default tip

Tue, 01 Jul 2025 20:28:49 +0200

author
Mike Becker <universe@uap-core.de>
date
Tue, 01 Jul 2025 20:28:49 +0200
changeset 173
bd57fe3f6360
parent 172
8178eee19656

add ellipsis shader - resolves #385

shader/ellipsis_frag.glsl file | annotate | diff | comparison | revisions
src/2d.c file | annotate | diff | comparison | revisions
src/ascension/2d.h file | annotate | diff | comparison | revisions
src/ascension/constants.h file | annotate | diff | comparison | revisions
src/ascension/datatypes.h file | annotate | diff | comparison | revisions
src/ascension/transform.h file | annotate | diff | comparison | revisions
test/snake/snake.c file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/shader/ellipsis_frag.glsl	Tue Jul 01 20:28:49 2025 +0200
@@ -0,0 +1,60 @@
+
+layout(location = 0) out vec4 diffuse;
+in vec2 uvcoord;
+
+uniform vec2 radii;
+#ifdef FILL
+uniform vec4 color;
+#endif
+#ifdef BORDER
+uniform float thickness;
+uniform vec4 border_color;
+#endif
+
+void main(void) {
+    // Calculate center of the ellipse
+    vec2 center = radii;
+
+    // Calculate position relative to center
+    vec2 pos = uvcoord - center;
+
+    // For an ellipse, we need to calculate normalized distance based on the formula:
+    // (x/a)² + (y/b)² = 1 where a and b are the semi-axes
+    vec2 normalized = pos / center;
+
+    // Calculate squared distance in ellipse space
+    // Value = 1.0 exactly at the ellipse boundary
+    float dist_squared = dot(normalized, normalized);
+
+    #ifndef BORDER
+    // For filled ellipse
+    if (dist_squared > 1.0) {
+        discard; // Outside the ellipse
+    }
+    diffuse = color;
+    #else // BORDER
+    // For outlined ellipse
+    // Calculate inner border threshold based on thickness
+    // We need to determine the inner ellipse boundary
+    float border_ratio = thickness / min(center.x, center.y);
+
+    // The inner ellipse has a smaller radius but follows the same equation
+    // We need to calculate what normalized value corresponds to the inner ellipse
+    float inner_threshold = 1.0 - border_ratio;
+    inner_threshold = inner_threshold * inner_threshold; // Square it because we're comparing to dist_squared
+
+    if (dist_squared > 1.0) {
+        discard; // Outside the ellipse
+    } else if (dist_squared > inner_threshold) {
+        // In the border region
+        diffuse = border_color;
+    } else {
+        // Inside the outline
+        #ifdef FILL
+        diffuse = color;
+        #else
+        discard;
+        #endif
+    }
+    #endif // BORDER
+}
\ No newline at end of file
--- a/src/2d.c	Tue Jul 01 00:00:50 2025 +0200
+++ b/src/2d.c	Tue Jul 01 20:28:49 2025 +0200
@@ -198,3 +198,157 @@
     asc_node_update(node);
     return node;
 }
+
+static void asc_ellipsis_destroy(AscSceneNode *node) {
+    asc_ptr_cast(AscEllipsis, ellipsis, node);
+    asc_mesh_destroy(&ellipsis->mesh);
+}
+
+static void asc_ellipsis_update(AscSceneNode *node) {
+    asc_ptr_cast(AscEllipsis, ellipsis, node);
+    asc_vec2f size = asc_vec2f_scale(ellipsis->radii, 2);
+    asc_mesh_plane_2d(&ellipsis->mesh, .size = size, .uv_scale = size);
+}
+
+
+typedef struct asc_ellipsis_shader_s {
+    AscShaderProgram program;
+    asc_uniform_loc color;
+    asc_uniform_loc border_color;
+    asc_uniform_loc radii;
+    asc_uniform_loc thickness;
+} AscEllipsisShader;
+
+#define ASC_ELLIPSIS_SHADER_FLAG_FILL          1
+#define ASC_ELLIPSIS_SHADER_FLAG_BORDER        2
+
+static AscShaderProgram *asc_ellipsis_shader_create(int flags) {
+    AscShaderCodes codes;
+    const char * const defines[] = {
+        "#define FILL\n",
+        "#define BORDER\n"
+    };
+    if (asc_shader_load_code_files((AscShaderCodeInfo){
+        .files.vtx = "sprite_vtx.glsl",
+        .files.frag = "ellipsis_frag.glsl",
+        .defines.frag_list = defines,
+        .defines.frag_list_select = flags
+    }, &codes)) {
+        asc_error("Loading ellipsis shader failed.");
+        return NULL;
+    }
+    AscShaderProgram *shader = asc_shader_create(codes, sizeof(AscRectangleShader));
+    if (asc_shader_invalid(shader)) {
+        asc_shader_free_codes(codes);
+        return shader;
+    }
+    asc_ptr_cast(AscEllipsisShader, ellipsis_shader, shader);
+    ellipsis_shader->radii = asc_shader_get_uniform_loc(shader, "radii");
+    if (asc_test_flag(flags, ASC_ELLIPSIS_SHADER_FLAG_FILL)) {
+        ellipsis_shader->color = asc_shader_get_uniform_loc(shader, "color");
+    } else {
+        ellipsis_shader->color = -1;
+    }
+    if (asc_test_flag(flags, ASC_ELLIPSIS_SHADER_FLAG_BORDER)) {
+        ellipsis_shader->thickness = asc_shader_get_uniform_loc(shader, "thickness");
+        ellipsis_shader->border_color = asc_shader_get_uniform_loc(shader, "border_color");
+    } else {
+        ellipsis_shader->thickness = -1;
+        ellipsis_shader->border_color = -1;
+    }
+    asc_shader_free_codes(codes);
+
+    asc_error_catch_all_gl();
+
+    return shader;
+}
+
+static void asc_ellipsis_draw(const AscCamera *camera, const AscSceneNode *node) {
+    asc_cptr_cast(AscEllipsis, ellipsis, node);
+
+    const bool filled = ellipsis->filled;
+    const bool border = ellipsis->thickness > 0;
+
+    // Compute shader flags
+    int shader_flags = 0;
+    if (filled) shader_flags |= ASC_ELLIPSIS_SHADER_FLAG_FILL;
+    if (border) shader_flags |= ASC_ELLIPSIS_SHADER_FLAG_BORDER;
+
+    // Lookup table for the shader ID
+    const int shader_ids[] = {
+        -1, // unused
+        ASC_SHADER_ELLIPSIS_FILL,
+        ASC_SHADER_ELLIPSIS_DRAW,
+        ASC_SHADER_ELLIPSIS_FILL_BORDER,
+    };
+
+    // Look up and activate shader
+    const AscShaderProgram *shader = asc_shader_lookup_or_create(
+        shader_ids[shader_flags], asc_ellipsis_shader_create, shader_flags);
+    if (asc_shader_use(shader, camera)) return;
+    asc_cptr_cast(AscEllipsisShader, ellipsis_shader, shader);
+
+    // Upload uniforms
+    asc_shader_upload_model_matrix(shader, node);
+
+    if (filled) {
+        asc_shader_upload_col4f(ellipsis_shader->color, ellipsis->color);
+    }
+    asc_shader_upload_vec2f(ellipsis_shader->radii, ellipsis->radii);
+
+    if (border) {
+        asc_shader_upload_float(ellipsis_shader->thickness, ellipsis->thickness);
+        asc_shader_upload_col4f(ellipsis_shader->border_color, ellipsis->border_color);
+    }
+
+    // Draw mesh
+    asc_mesh_draw_triangle_strip(&ellipsis->mesh);
+}
+
+AscSceneNode *asc_ellipsis_create(struct asc_ellipsis_create_args args) {
+    AscEllipsis *ellipsis = cxZallocDefault(sizeof(AscEllipsis));
+
+    if (args.bounds.size.width + args.bounds.size.height > 0) {
+        ellipsis->node.position.x = (float) args.bounds.pos.x;
+        ellipsis->node.position.y = (float) args.bounds.pos.y;
+        ellipsis->radii.x = (float) args.bounds.size.width / 2.f;
+        ellipsis->radii.y = (float) args.bounds.size.height / 2.f;
+    } else {
+        const unsigned cx = ASC_NONZERO_OR(args.x, args.center.x);
+        const unsigned cy = ASC_NONZERO_OR(args.y, args.center.y);
+        const unsigned rx = ASC_NONZERO_OR(args.radius, args.radius_x);
+        const unsigned ry = ASC_NONZERO_OR(args.radius, args.radius_y);
+        ellipsis->node.position.x = (float) (cx-rx);
+        ellipsis->node.position.y = (float) (cy-ry);
+        ellipsis->radii.x = (float) rx;
+        ellipsis->radii.y = (float) ry;
+    }
+
+    ellipsis->color = asc_col_itof(asc_context.ink);
+    ellipsis->border_color = asc_col_itof(args.border_color);
+    ellipsis->filled = args.filled;
+    if (!args.filled && args.thickness == 0) {
+        // when we do not fill the ellipsis, we need a border
+        ellipsis->thickness = 1;
+    } else {
+        ellipsis->thickness = args.thickness;
+    }
+    if (!args.filled && asc_col4_test_zero(args.border_color)) {
+        // convenience fallback:
+        // when we are drawing an outline but have no explicit border color,
+        // use the active ink
+        ellipsis->border_color = ellipsis->color;
+    }
+
+    AscSceneNode *node = &ellipsis->node;
+    node->position.z = ASC_SCENE_2D_DEPTH_OFFSET;
+    node->scale = asc_vec3f_one;
+    node->render_group = asc_context.ink.alpha < 255
+                             ? ASC_RENDER_GROUP_2D_BLEND
+                             : ASC_RENDER_GROUP_2D_OPAQUE;
+    node->update_func = asc_ellipsis_update;
+    node->destroy_func = asc_ellipsis_destroy;
+    node->draw_func = asc_ellipsis_draw;
+    asc_node_update(node);
+    return node;
+}
--- a/src/ascension/2d.h	Tue Jul 01 00:00:50 2025 +0200
+++ b/src/ascension/2d.h	Tue Jul 01 20:28:49 2025 +0200
@@ -72,12 +72,47 @@
 
 #define asc_rectangle(...) asc_rectangle_create((struct asc_rectangle_create_args) { __VA_ARGS__ })
 
+typedef struct asc_ellipsis_s {
+    AscSceneNode node;
+    AscMesh mesh;
+    asc_col4f color;
+    asc_col4f border_color;
+    asc_vec2f radii;
+    float thickness;
+    bool filled;
+} AscEllipsis;
 
 struct asc_ellipsis_create_args {
+    /**
+     * The bounds of the ellipsis.
+     * Preferred over all other settings.
+     * When you specify bounds, you cannot specify a center and radiuses.
+     */
+    asc_recti bounds;
+    /**
+     * The center point of the ellipsis.
+     * Preferred over x and y.
+     */
     asc_vec2i center;
+    /**
+     * The x coordinate of the center, if center is not specified.
+     */
     int x;
+    /**
+     * The y coordinate of the center, if center is not specified.
+     */
     int y;
+    /**
+     * The radius in both directions (use for circles).
+     */
+    unsigned int radius;
+    /**
+     * The radius in x-direction.
+     */
     unsigned int radius_x;
+    /**
+     * The radius in y-direction.
+     */
     unsigned int radius_y;
     /**
      * Border thickness
--- a/src/ascension/constants.h	Tue Jul 01 00:00:50 2025 +0200
+++ b/src/ascension/constants.h	Tue Jul 01 20:28:49 2025 +0200
@@ -42,6 +42,9 @@
 #define ASC_SHADER_RECTANGLE_FILL_ROUND         ASC_SHADER_INTERNAL_ID(6)
 #define ASC_SHADER_RECTANGLE_FILL_BORDER        ASC_SHADER_INTERNAL_ID(7)
 #define ASC_SHADER_RECTANGLE_FILL_BORDER_ROUND  ASC_SHADER_INTERNAL_ID(8)
+#define ASC_SHADER_ELLIPSIS_DRAW                ASC_SHADER_INTERNAL_ID(10)
+#define ASC_SHADER_ELLIPSIS_FILL                ASC_SHADER_INTERNAL_ID(11)
+#define ASC_SHADER_ELLIPSIS_FILL_BORDER         ASC_SHADER_INTERNAL_ID(12)
 
 
 #endif // ASC_CONSTANTS_H
--- a/src/ascension/datatypes.h	Tue Jul 01 00:00:50 2025 +0200
+++ b/src/ascension/datatypes.h	Tue Jul 01 20:28:49 2025 +0200
@@ -120,6 +120,9 @@
 } asc_col4f;
 #define asc_col4f_new(r, g, b, a) (asc_col4f){(float)r, (float)g, (float)b, (float)a}
 
+#define asc_rgb(r,g,b) asc_col4i_new(r,g,b,255)
+#define asc_rgba(r,g,b,a) asc_col4i_new(r,g,b,a)
+
 #define asc_col4_test_zero(v) (v.red == 0 && v.green == 0 && v.blue == 0 && v.alpha == 0)
 
 typedef float asc_mat4f[16];
--- a/src/ascension/transform.h	Tue Jul 01 00:00:50 2025 +0200
+++ b/src/ascension/transform.h	Tue Jul 01 20:28:49 2025 +0200
@@ -114,7 +114,7 @@
         __attribute__((__unused__)) asc_transform transform,
         __attribute__((__unused__)) asc_vec2f vec
 ) {
-    // TODO: implement
+    // TODO: implement - and think about the point of origin!
 }
 
 ASC_TRANFORM_FUNC void asc_transform_from_vec2f(
--- a/test/snake/snake.c	Tue Jul 01 00:00:50 2025 +0200
+++ b/test/snake/snake.c	Tue Jul 01 20:28:49 2025 +0200
@@ -194,41 +194,6 @@
     // create spaceship
     create_spaceship();
 
-    // TODO: play around with the test rectangles
-    asc_ink_rgb(255, 0, 0);
-    asc_scene_add_node(MAIN_SCENE,
-        asc_rectangle(.x = 200, .y = 250, .width = 100, .height = 75,
-            .thickness = 4, .radius = 15, .filled = true, .border_color = asc_col4i_new(0, 255, 0, 255))
-    );
-    asc_scene_add_node(MAIN_SCENE,
-    asc_rectangle(.x = 50, .y = 150, .width = 100, .height = 75,
-        .thickness = 4, .radius = 15, .filled = false, .border_color = asc_col4i_new(0, 255, 0, 255))
-    );
-    asc_scene_add_node(MAIN_SCENE,
-    asc_rectangle(.x = 50, .y = 25, .width = 100, .height = 75,
-        .thickness = 4, .filled = false, .border_color = asc_col4i_new(0, 255, 0, 255))
-    );
-    asc_scene_add_node(MAIN_SCENE,
-    asc_rectangle(.x = 250, .y = 25, .width = 100, .height = 75,
-        .thickness = 4, .filled = true, .border_color = asc_col4i_new(0, 255, 0, 255))
-    );
-    asc_scene_add_node(MAIN_SCENE,
-    asc_rectangle(.x = 350, .y = 250, .width = 100, .height = 75,
-        .radius = 15, .filled = true)
-    );
-    asc_scene_add_node(MAIN_SCENE,
-    asc_rectangle(.x = 350, .y = 50, .width = 100, .height = 75,
-        .filled = true)
-    );
-    asc_scene_add_node(MAIN_SCENE,
-    asc_rectangle(.x = 50, .y = 350, .width = 100, .height = 75,
-        .radius = 15)
-    );
-    asc_scene_add_node(MAIN_SCENE,
-    asc_rectangle(.x = 350, .y = 350, .width = 100, .height = 75)
-    );
-
-
     // Main Loop
     do {
         // quit application on any error

mercurial