rework how transformations work to allow rotations around a point of origin

Sun, 13 Jul 2025 15:09:04 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 13 Jul 2025 15:09:04 +0200
changeset 204
be5cf64b5c29
parent 203
1883bdc4fb20
child 205
d1e44c861426

rework how transformations work to allow rotations around a point of origin

src/2d.c file | annotate | diff | comparison | revisions
src/ascension/datatypes.h file | annotate | diff | comparison | revisions
src/ascension/scene_node.h file | annotate | diff | comparison | revisions
src/ascension/sprite.h file | annotate | diff | comparison | revisions
src/ascension/text.h file | annotate | diff | comparison | revisions
src/ascension/transform.h file | annotate | diff | comparison | revisions
src/scene.c file | annotate | diff | comparison | revisions
src/scene_node.c file | annotate | diff | comparison | revisions
src/sprite.c file | annotate | diff | comparison | revisions
src/text.c file | annotate | diff | comparison | revisions
test/snake/snake.c file | annotate | diff | comparison | revisions
--- a/src/2d.c	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/2d.c	Sun Jul 13 15:09:04 2025 +0200
@@ -34,6 +34,8 @@
 
 #include <assert.h>
 
+// TODO: add "origin" arguments to 2D primitives
+
 typedef struct asc_rectangle_shader_s {
     AscShaderProgram program;
     asc_uniform_loc color;
@@ -188,9 +190,9 @@
     }
 
     AscSceneNode *node = &rectangle->node;
-    asc_transform_identity(node->transform);
-    asc_transform_translate3f(node->transform,
-        ASC_VEC3F(pos_x, pos_y, ASC_SCENE_2D_DEPTH_OFFSET));
+    node->position = ASC_VEC3F(pos_x, pos_y, ASC_SCENE_2D_DEPTH_OFFSET);
+    node->scale = ASC_VEC3F_1;
+    asc_mat4f_unit(node->rotation);
     node->render_group = asc_context.ink.alpha < 255
                              ? ASC_RENDER_GROUP_2D_BLEND
                              : ASC_RENDER_GROUP_2D_OPAQUE;
@@ -344,9 +346,9 @@
     }
 
     AscSceneNode *node = &ellipsis->node;
-    asc_transform_identity(node->transform);
-    asc_transform_translate3f(node->transform,
-        ASC_VEC3F(pos_x, pos_y, ASC_SCENE_2D_DEPTH_OFFSET));
+    node->position = ASC_VEC3F(pos_x, pos_y, ASC_SCENE_2D_DEPTH_OFFSET);
+    node->scale = ASC_VEC3F_1;
+    asc_mat4f_unit(node->rotation);
     node->render_group = asc_context.ink.alpha < 255
                              ? ASC_RENDER_GROUP_2D_BLEND
                              : ASC_RENDER_GROUP_2D_OPAQUE;
--- a/src/ascension/datatypes.h	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/ascension/datatypes.h	Sun Jul 13 15:09:04 2025 +0200
@@ -107,7 +107,6 @@
 typedef union asc_vec3f {
     struct { float x, y, z; };
     struct { float width, height, depth; };
-    struct { float pitch, yaw, roll; };
     float data[3];
 } asc_vec3f;
 #define ASC_VEC3F(x, y, z) (asc_vec3f){{(float)x, (float)y, (float)(z)}}
@@ -307,6 +306,14 @@
     return ASC_VEC3F(v.x*s, v.y*s, v.z*s);
 }
 
+static inline asc_vec2f asc_vec2f_neg(asc_vec2f v) {
+    return ASC_VEC2F(-v.x, -v.y);
+}
+
+static inline asc_vec3f asc_vec3f_neg(asc_vec3f v) {
+    return ASC_VEC3F(-v.x, -v.y, -v.z);
+}
+
 static inline unsigned asc_vec2u_sqrlen(asc_vec2u v) {
     return v.x*v.x + v.y*v.y;
 }
--- a/src/ascension/scene_node.h	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/ascension/scene_node.h	Sun Jul 13 15:09:04 2025 +0200
@@ -66,6 +66,10 @@
     asc_scene_node_draw_func draw_func;
     asc_transform transform;
     asc_transform world_transform;
+    asc_vec3f position;
+    asc_vec3f scale;
+    asc_vec3f origin;
+    asc_transform rotation;
     enum AscRenderGroup render_group;
     /**
      * Custom flags for this node.
@@ -100,6 +104,7 @@
  */
 #define ASC_SCENE_NODE_HIDDEN                 0x80000000
 
+// TODO: some functions are prefixed asc_scene_node_ and others just asc_node_
 
 /**
  * Creates an empty node that may serve as a container for other nodes.
@@ -123,6 +128,17 @@
 void asc_scene_node_free(AscSceneNode *node);
 
 /**
+ * Calculates the transformation matrix from components.
+ *
+ * Used internally, usually you never need to call this.
+ * Use asc_node_update_transform() to trigger a recalculation.
+ *
+ * @param node the node
+ * @see asc_node_update_transform()
+ */
+void asc_scene_node_calculate_transform(AscSceneNode *node);
+
+/**
  * Sets the name of a node.
  *
  * @param node the node
@@ -172,39 +188,50 @@
  */
 #define ASC_SCENE_2D_DEPTH_OFFSET 0.0078125f
 
-/**
- * Applies an affine transformation to a scene node.
- *
- * @param node the node to modify
- * @param matrix the matrix to multiply with the current transformation matrix
- */
-ASC_TRANFORM_FUNC void asc_node_transform_apply(AscSceneNode *node, const asc_transform matrix) {
-    asc_mat4f_mul(node->transform, node->transform, matrix);
+static inline void asc_node_set_position(AscSceneNode *node, asc_vec3f position) {
+    node->position = position;
+    asc_node_update_transform(node);
+}
+
+static inline void asc_node_set_scale(AscSceneNode *node, asc_vec3f scale) {
+    node->scale = scale;
+    asc_node_update_transform(node);
+}
+
+static inline void asc_node_set_origin(AscSceneNode *node, asc_vec3f origin) {
+    node->origin = origin;
+    asc_node_update_transform(node);
+}
+
+static inline void asc_node_set_position2f(AscSceneNode *node, asc_vec2f position) {
+    node->position.x = position.x;
+    node->position.y = position.y;
     asc_node_update_transform(node);
 }
 
-/**
- * Overwrites the current local transformation matrix.
- *
- * @param node the node to modify
- * @param matrix the matrix to set as the new local transformation matrix
- */
-ASC_TRANFORM_FUNC void asc_node_transform_set(AscSceneNode *node, const asc_transform matrix) {
-    asc_transform_copy(node->transform, matrix);
+static inline void asc_node_set_scale2f(AscSceneNode *node, asc_vec2f scale) {
+    node->scale.width = scale.width;
+    node->scale.height = scale.height;
+    asc_node_update_transform(node);
+}
+
+static inline void asc_node_set_origin2f(AscSceneNode *node, asc_vec2f origin) {
+    node->origin.x = origin.x;
+    node->origin.y = origin.y;
     asc_node_update_transform(node);
 }
 
-/**
- * Resets the node to use an identity transformation matrix.
- *
- * This is, for example, useful when you want to recalculate a chain of transformations from scratch.
- *
- * @param node the node for which to reset the local transformation
- */
-ASC_TRANFORM_FUNC void asc_node_transform_reset(AscSceneNode *node) {
-    asc_transform_identity(node->transform);
+static inline void asc_node_set_rotation(AscSceneNode *node, asc_transform rotation) {
+    memcpy(node->rotation, rotation, ASC_TRANSFORM_SIZE);
     asc_node_update_transform(node);
 }
 
+static inline void asc_node_roll_deg(AscSceneNode *node, float angle) {
+    asc_transform r, d;
+    asc_transform_roll(r, asc_rad(angle));
+    asc_transform_apply(d, r, node->rotation);
+    asc_transform_copy(node->rotation, d);
+    asc_node_update_transform(node);
+}
 
 #endif
--- a/src/ascension/sprite.h	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/ascension/sprite.h	Sun Jul 13 15:09:04 2025 +0200
@@ -48,6 +48,8 @@
     AscTexture *texture;
     int x;
     int y;
+    int origin_x;
+    int origin_y;
     /**
      * Optional width for re-scaling.
      * If zero, the texture width will be used.
--- a/src/ascension/text.h	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/ascension/text.h	Sun Jul 13 15:09:04 2025 +0200
@@ -69,6 +69,7 @@
     const char *text;
     enum asc_text_alignment alignment;
     unsigned short max_width;
+    bool centered;
 };
 
 /**
--- a/src/ascension/transform.h	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/ascension/transform.h	Sun Jul 13 15:09:04 2025 +0200
@@ -40,148 +40,138 @@
 #define ASC_TRANFORM_FUNC static inline
 #endif
 
+ASC_TRANFORM_FUNC void asc_transform_copy(asc_transform dest, const asc_transform src) {
+    memcpy(dest, src, ASC_TRANSFORM_SIZE);
+}
+
+ASC_TRANFORM_FUNC void asc_transform_apply(asc_transform dest, const asc_transform left, const asc_transform right) {
+    asc_mat4f_mul(dest, left, right);
+}
 
 ASC_TRANFORM_FUNC void asc_transform_identity(asc_transform transform) {
     asc_mat4f_unit(transform);
 }
 
-ASC_TRANFORM_FUNC void asc_transform_copy(asc_transform dest, const asc_transform src) {
-    memcpy(dest, src, ASC_TRANSFORM_SIZE);
-}
-
+/**
+ * Makes the transformation matrix a translation matrix.
+ *
+ * @param transform the matrix to initialize
+ * @param vec the translation vector
+ */
 ASC_TRANFORM_FUNC void asc_transform_translate3f(
         asc_transform transform,
         asc_vec3f vec
 ) {
-    transform[asc_mat4_index(3, 0)] += vec.x;
-    transform[asc_mat4_index(3, 1)] += vec.y;
-    transform[asc_mat4_index(3, 2)] += vec.z;
+    asc_mat4f_unit(transform);
+    transform[asc_mat4_index(3, 0)] = vec.x;
+    transform[asc_mat4_index(3, 1)] = vec.y;
+    transform[asc_mat4_index(3, 2)] = vec.z;
 }
 
+/**
+ * Makes the transformation matrix a scale matrix.
+ *
+ * @param transform the matrix to initialize
+ * @param vec the scale vector
+ */
 ASC_TRANFORM_FUNC void asc_transform_scale3f(
         asc_transform transform,
         asc_vec3f vec
 ) {
-    for (unsigned i = 0 ; i < 3 ; i++) {
-        transform[asc_mat4_index(0, i)] *= vec.width;
-        transform[asc_mat4_index(1, i)] *= vec.height;
-        transform[asc_mat4_index(2, i)] *= vec.depth;
-    }
+    memset(transform, 0, ASC_TRANSFORM_SIZE);
+    transform[asc_mat4_index(0, 0)] = vec.width;
+    transform[asc_mat4_index(1, 1)] = vec.height;
+    transform[asc_mat4_index(2, 2)] = vec.depth;
+    transform[asc_mat4_index(3, 3)] = 1.f;
 }
 
+/**
+ * Makes the transformation matrix a translation matrix.
+ *
+ * @param transform the matrix to initialize
+ * @param vec the translation vector
+ */
 ASC_TRANFORM_FUNC void asc_transform_translate2f(
         asc_transform transform,
         asc_vec2f vec
 ) {
-    transform[asc_mat4_index(3, 0)] += vec.x;
-    transform[asc_mat4_index(3, 1)] += vec.y;
+    asc_transform_translate3f(transform, ASC_VEC3F(vec.x, vec.y, 0.f));
 }
 
+/**
+ * Makes the transformation matrix a scale matrix.
+ *
+ * @param transform the matrix to initialize
+ * @param vec the scale vector
+ */
 ASC_TRANFORM_FUNC void asc_transform_scale2f(
         asc_transform transform,
         asc_vec2f vec
 ) {
-    for (unsigned i = 0 ; i < 3 ; i++) {
-        transform[asc_mat4_index(0, i)] *= vec.width;
-        transform[asc_mat4_index(1, i)] *= vec.height;
-    }
+    asc_transform_scale3f(transform, ASC_VEC3F(vec.width, vec.height, 1.f));
 }
 
+/**
+ * Makes the transformation matrix a rotation matrix around the y-axis.
+ *
+ * @param transform the matrix to initialize
+ * @param angle the angle in radians
+ */
 ASC_TRANFORM_FUNC void asc_transform_yaw(
         asc_transform transform,
         float angle
 ) {
-    float s = sinf(angle);
-    float c = cosf(angle);
-
-    float m00 = transform[asc_mat4_index(0, 0)];
-    float m02 = transform[asc_mat4_index(0, 2)];
-    float m10 = transform[asc_mat4_index(1, 0)];
-    float m12 = transform[asc_mat4_index(1, 2)];
-    float m20 = transform[asc_mat4_index(2, 0)];
-    float m22 = transform[asc_mat4_index(2, 2)];
-
-    transform[asc_mat4_index(0, 0)] = m00 * c + m02 * s;
-    transform[asc_mat4_index(0, 2)] = -m00 * s + m02 * c;
-    transform[asc_mat4_index(1, 0)] = m10 * c + m12 * s;
-    transform[asc_mat4_index(1, 2)] = -m10 * s + m12 * c;
-    transform[asc_mat4_index(2, 0)] = m20 * c + m22 * s;
-    transform[asc_mat4_index(2, 2)] = -m20 * s + m22 * c;
+    memset(transform, 0, ASC_TRANSFORM_SIZE);
+    const float s = asc_sin(angle);
+    const float c = asc_cos(angle);
+    transform[asc_mat4_index(0, 0)] = c;
+    transform[asc_mat4_index(0, 2)] = -s;
+    transform[asc_mat4_index(1, 1)] = 1;
+    transform[asc_mat4_index(2, 0)] = s;
+    transform[asc_mat4_index(2, 2)] = c;
+    transform[asc_mat4_index(3, 3)] = 1;
 }
 
+/**
+ * Makes the transformation matrix a rotation matrix around the x-axis.
+ *
+ * @param transform the matrix to initialize
+ * @param angle the angle in radians
+ */
 ASC_TRANFORM_FUNC void asc_transform_pitch(
     asc_transform transform,
     float angle
 ) {
-    float s = sinf(angle);
-    float c = cosf(angle);
-
-    float m01 = transform[asc_mat4_index(0, 1)];
-    float m02 = transform[asc_mat4_index(0, 2)];
-    float m11 = transform[asc_mat4_index(1, 1)];
-    float m12 = transform[asc_mat4_index(1, 2)];
-    float m21 = transform[asc_mat4_index(2, 1)];
-    float m22 = transform[asc_mat4_index(2, 2)];
-
-    transform[asc_mat4_index(0, 1)] = m01 * c - m02 * s;
-    transform[asc_mat4_index(0, 2)] = m01 * s + m02 * c;
-    transform[asc_mat4_index(1, 1)] = m11 * c - m12 * s;
-    transform[asc_mat4_index(1, 2)] = m11 * s + m12 * c;
-    transform[asc_mat4_index(2, 1)] = m21 * c - m22 * s;
-    transform[asc_mat4_index(2, 2)] = m21 * s + m22 * c;
+    memset(transform, 0, ASC_TRANSFORM_SIZE);
+    const float s = asc_sin(angle);
+    const float c = asc_cos(angle);
+    transform[asc_mat4_index(0, 0)] = 1;
+    transform[asc_mat4_index(1, 1)] = c;
+    transform[asc_mat4_index(1, 2)] = s;
+    transform[asc_mat4_index(2, 1)] = -s;
+    transform[asc_mat4_index(2, 2)] = c;
+    transform[asc_mat4_index(3, 3)] = 1;
 }
 
+/**
+ * Makes the transformation matrix a rotation matrix around the z-axis.
+ *
+ * @param transform the matrix to initialize
+ * @param angle the angle in radians
+ */
 ASC_TRANFORM_FUNC void asc_transform_roll(
     asc_transform transform,
     float angle
 ) {
-    float s = sinf(angle);
-    float c = cosf(angle);
-
-    float m00 = transform[asc_mat4_index(0, 0)];
-    float m01 = transform[asc_mat4_index(0, 1)];
-    float m10 = transform[asc_mat4_index(1, 0)];
-    float m11 = transform[asc_mat4_index(1, 1)];
-    float m20 = transform[asc_mat4_index(2, 0)];
-    float m21 = transform[asc_mat4_index(2, 1)];
-
-    transform[asc_mat4_index(0, 0)] = m00 * c - m01 * s;
-    transform[asc_mat4_index(0, 1)] = m00 * s + m01 * c;
-    transform[asc_mat4_index(1, 0)] = m10 * c - m11 * s;
-    transform[asc_mat4_index(1, 1)] = m10 * s + m11 * c;
-    transform[asc_mat4_index(2, 0)] = m20 * c - m21 * s;
-    transform[asc_mat4_index(2, 1)] = m20 * s + m21 * c;
-}
-
-ASC_TRANFORM_FUNC void asc_transform_roll_origin(
-    asc_transform transform,
-    float angle,
-    asc_vec3f point_of_origin
-) {
-    // TODO: this is somehow still broken - find out why
-    asc_transform translate;
-    asc_transform_identity(translate);
-    translate[asc_mat4_index(3, 0)] = -point_of_origin.x;
-    translate[asc_mat4_index(3, 1)] = -point_of_origin.y;
-    translate[asc_mat4_index(3, 2)] = -point_of_origin.z;
-
-    asc_transform rotate;
-    asc_transform_identity(rotate);
-    float s = sinf(angle);
-    float c = cosf(angle);
-    rotate[asc_mat4_index(0, 0)] = c;
-    rotate[asc_mat4_index(0, 1)] = s;
-    rotate[asc_mat4_index(1, 0)] = -s;
-    rotate[asc_mat4_index(1, 1)] = c;
-
-    asc_transform result;
-    asc_mat4f_mul(result, transform, translate);
-    asc_mat4f_mul(transform, result, rotate);
-    translate[asc_mat4_index(3, 0)] *= -1.f;
-    translate[asc_mat4_index(3, 1)] *= -1.f;
-    translate[asc_mat4_index(3, 2)] *= -1.f;
-    asc_mat4f_mul(result, transform, translate);
-    asc_transform_copy(transform, result);
+    memset(transform, 0, ASC_TRANSFORM_SIZE);
+    const float s = asc_sin(angle);
+    const float c = asc_cos(angle);
+    transform[asc_mat4_index(0, 0)] = c;
+    transform[asc_mat4_index(0, 1)] = s;
+    transform[asc_mat4_index(1, 0)] = -s;
+    transform[asc_mat4_index(1, 1)] = c;
+    transform[asc_mat4_index(2, 2)] = 1;
+    transform[asc_mat4_index(3, 3)] = 1;
 }
 
 
--- a/src/scene.c	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/scene.c	Sun Jul 13 15:09:04 2025 +0200
@@ -164,6 +164,9 @@
         if (asc_test_flag(node->flags, ASC_SCENE_NODE_UPDATE_TRANSFORM)) {
             asc_set_flag(node->flags, ASC_SCENE_NODE_TRANSFORM_UPDATED);
             asc_clear_flag(node->flags, ASC_SCENE_NODE_UPDATE_TRANSFORM);
+
+            asc_scene_node_calculate_transform(node);
+
             if (node->parent == scene->root) {
                 // skip unnecessary multiplication with unity matrix
                 asc_transform_copy(node->world_transform, node->transform);
--- a/src/scene_node.c	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/scene_node.c	Sun Jul 13 15:09:04 2025 +0200
@@ -79,6 +79,24 @@
     }
 }
 
+void asc_scene_node_calculate_transform(AscSceneNode *node) {
+    asc_transform temp, temp2;
+
+    // move the point of origin
+    asc_transform_translate3f(temp, asc_vec3f_neg(node->origin));
+
+    // apply the rotation
+    asc_transform_apply(node->transform, node->rotation, temp);
+
+    // apply the scale
+    asc_transform_scale3f(temp, node->scale);
+    asc_transform_apply(temp2, temp, node->transform);
+
+    // apply the translation
+    asc_transform_translate3f(temp, node->position);
+    asc_transform_apply(node->transform, temp, temp2);
+}
+
 void asc_scene_node_name(AscSceneNode *node, const char *name) {
     cx_strfree(&node->name);
     if (name == NULL) {
--- a/src/sprite.c	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/sprite.c	Sun Jul 13 15:09:04 2025 +0200
@@ -133,9 +133,10 @@
     node->destroy_func = asc_sprite_destroy;
     node->draw_func = asc_sprite_draw;
 
-    asc_transform_identity(node->transform);
-    asc_transform_translate3f(node->transform,
-        ASC_VEC3F(args.x, args.y, ASC_SCENE_2D_DEPTH_OFFSET));
+    node->position = ASC_VEC3F(args.x, args.y, ASC_SCENE_2D_DEPTH_OFFSET);
+    node->origin = ASC_VEC3F(args.origin_x, args.origin_y, 0);
+    node->scale = ASC_VEC3F_1;
+    asc_mat4f_unit(node->rotation);
 
     asc_node_update(node);
     return node;
--- a/src/text.c	Sun Jul 13 14:22:40 2025 +0200
+++ b/src/text.c	Sun Jul 13 15:09:04 2025 +0200
@@ -86,9 +86,7 @@
         return;
     }
     if (asc_test_flag(text->base.flags, ASC_TEXT_CENTERED_FLAG)) {
-        unsigned short newoffx = surface->w / 2;
-        asc_transform_translate2f(node->transform, ASC_VEC2F(text->offx - newoffx, 0));
-        text->offx = newoffx;
+        asc_node_set_origin(node, ASC_VEC3F(surface->w / 2, 0, 0));
     }
 
     // Transfer Image Data
@@ -151,9 +149,9 @@
     node->destroy_func = asc_text_destroy;
     node->update_func = asc_text_update;
     node->draw_func = asc_text_draw;
-    asc_transform_identity(node->transform);
-    asc_transform_translate3f(node->transform,
-        ASC_VEC3F(args.x, args.y, ASC_SCENE_2D_DEPTH_OFFSET));
+    node->position = ASC_VEC3F(args.x, args.y, ASC_SCENE_2D_DEPTH_OFFSET);
+    node->scale = ASC_VEC3F_1;
+    asc_mat4f_unit(node->rotation);
 
     // text properties
     node->flags = args.alignment; // use flags variable to save some space
@@ -165,6 +163,9 @@
     } else {
         text->text = cx_mutstr(strdup(args.text));
     }
+    if (args.centered) {
+        asc_set_flag(node->flags, ASC_TEXT_CENTERED_FLAG);
+    }
 
     // initialize texture
     // mesh will be created in the update func
--- a/test/snake/snake.c	Sun Jul 13 14:22:40 2025 +0200
+++ b/test/snake/snake.c	Sun Jul 13 15:09:04 2025 +0200
@@ -70,13 +70,10 @@
     if (asc_test_flag(node->flags, ASC_SCENE_NODE_GRAPHICS_UPDATED) || asc_active_window->resized) {
         asc_vec2u bottom_right = asc_active_window->dimensions;
         asc_vec2u text_size = ((AscText*)node)->dimension;
-        asc_transform new_transform;
-        asc_transform_identity(new_transform);
-        asc_transform_translate2f(new_transform, ASC_VEC2F(
+        asc_node_set_position2f(node, ASC_VEC2F(
                 (int) bottom_right.x - (int) text_size.width - 10,
                 (int) bottom_right.y - (int) text_size.height - 10
         ));
-        asc_node_transform_set(node, new_transform);
     }
 }
 
@@ -125,7 +122,9 @@
         .x = 250,
         .y = 300,
         .width = 64,
-        .height = 64
+        .height = 64,
+        .origin_x = 32,
+        .origin_y = 32,
     );
     asc_scene_add_node(MAIN_SCENE, sprite);
     return sprite;
@@ -208,7 +207,7 @@
 
         // player rotation
         if (asc_key_pressed(ASC_KEY(LEFT))) {
-            asc_transform_roll_origin(spaceship->transform, asc_rad(-90), ASC_VEC3F(32, 32, 0));
+            asc_node_roll_deg(spaceship, -90);
             asc_node_update_transform(spaceship);
         }
 

mercurial