Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Lightnet/c9e076fa3b0748aabcbcef69a6f3a2d8 to your computer and use it in GitHub Desktop.
Save Lightnet/c9e076fa3b0748aabcbcef69a6f3a2d8 to your computer and use it in GitHub Desktop.
Flecs 4.x Documentation for Hierarchical 3D Transformation Systems

Flecs 4.x Documentation for Hierarchical 3D Transformation Systems

This guide outlines how to set up a hierarchical 3D transformation system using Flecs 4.x, focusing on correct entity-component-system (ECS) configuration, transformation order, and parent-child relationships. It is tailored for AI agents and developers building 3D applications (e.g., with OpenGL or other rendering APIs) where entities have position, rotation, and scale, and transformations are propagated through a parent-child hierarchy.Key Concepts1. Flecs Basics

  • Entities: Unique identifiers representing objects (e.g., a cube in a 3D scene).
  • Components: Data structures attached to entities (e.g., Transform3D for position, rotation, scale).
  • Systems: Functions that operate on entities with specific components, executed in a defined order (e.g., EcsPreUpdate, EcsOnUpdate).
  • Pairs: Relationships between entities, used for hierarchies (e.g., (EcsChildOf, ParentEntity) to denote a child-parent relationship).
  1. Hierarchical Transformations
  • Transform3D Component: Stores local and world transformation matrices, typically including:
    • position (vec3): Local position relative to the parent.
    • rotation (vec4 quaternion): Local rotation.
    • scale (vec3): Local scale.
    • local (mat4): Local transformation matrix (relative to parent).
    • world (mat4): World transformation matrix (absolute, after applying parent transforms).
    • isDirty (bool): Flag to indicate if the transform needs recalculation.
    • parent (ecs_entity_t): Reference to the parent entity (optional, for clarity).
  • Transformation Order: Use Translate, Rotate, Scale (TRS) order for local transformations to ensure child entities rotate around their own centers (not orbiting the parent).
    • Incorrect Order (e.g., SRT): Applying translation last causes orbiting because rotation is applied around the parent's origin.
  • Parent-Child Hierarchy: Children inherit their parent's world transform, computed by multiplying the parent's world matrix with the child's local matrix.
  1. Flecs 4.x Specifics
  • Query Syntax: Use ecs_query with .terms for defining queries, replacing Flecs 3.x's ecs_query_new.
  • System Definition: Use ecs_system with .query.terms, .query.flags, and .phase for system setup.
  • Cascading: Use EcsQueryCascade with EcsChildOf to process parent entities before children, critical for hierarchical transforms.
  • Pairs: Use ecs_pair(EcsChildOf, parent) to establish parent-child relationships.

Setup Steps

Step 1: Define Components

Define a Transform3D component to store transformation data. Include fields for position, rotation, scale, matrices, and a dirty flag.

typedef struct {
    vec3 position; // Local position (x, y, z)
    vec4 rotation; // Local rotation (quaternion: x, y, z, w)
    vec3 scale;    // Local scale (x, y, z)
    mat4 local;    // Local transformation matrix
    mat4 world;    // World transformation matrix
    bool isDirty;  // Flag to indicate if transform needs recalculation
    ecs_entity_t parent; // Parent entity (optional, for clarity)
} Transform3D;
ECS_COMPONENT_DECLARE(Transform3D);

In main or initialization:

c

ecs_world_t *world = ecs_init();
ECS_COMPONENT(world, Transform3D);

Step 2: Create Entities with Hierarchy

Create entities with Transform3D components and establish parent-child relationships using ecs_add_pair.

c

// Create parent entity
ecs_entity_t parent = ecs_entity(world, { .name = "ParentCube" });
ecs_set(world, parent, Transform3D, {
    .position = {0.0f, 0.0f, 0.0f},
    .rotation = {0.0f, 0.0f, 0.0f, 1.0f}, // Identity quaternion
    .scale = {1.0f, 1.0f, 1.0f},
    .isDirty = true,
    .parent = 0
});

// Create child entity
ecs_entity_t child = ecs_entity(world, { .name = "ChildCube" });
ecs_set(world, child, Transform3D, {
    .position = {1.0f, 0.0f, 0.0f}, // Offset from parent
    .rotation = {0.0f, sinf(glm_rad(22.5f)), 0.0f, cosf(glm_rad(22.5f))}, // 22.5-degree Y rotation
    .scale = {1.0f, 1.0f, 1.0f},
    .isDirty = true,
    .parent = parent
});
ecs_add_pair(world, child, EcsChildOf, parent);
  • Note: The ecs_add_pair(world, child, EcsChildOf, parent) establishes the hierarchy. The .parent field in Transform3D is optional but can help with debugging or explicit parent tracking.

Step 3: Define the Transformation System

Create a system to update Transform3D components, computing local and world matrices in TRS order.

c

void update_transform_system(ecs_iter_t *it) {
    Transform3D *transforms = ecs_field(it, Transform3D, 0);

    for (int i = 0; i < it->count; i++) {
        Transform3D *transform = &transforms[i];
        if (!transform->isDirty) continue;

        // Calculate local matrix: TRS order
        mat4 local;
        glm_mat4_identity(local);

        // Translate
        glm_translate(local, transform->position);

        // Rotate
        mat4 rot;
        glm_quat_mat4(transform->rotation, rot);
        glm_mat4_mul(local, rot, local);

        // Scale
        glm_scale(local, transform->scale);

        glm_mat4_copy(local, transform->local);

        // Compute world matrix
        ecs_entity_t parent = ecs_get_parent(it->world, it->entities[i]);
        if (parent && ecs_is_valid(it->world, parent) && ecs_has(it->world, parent, Transform3D)) {
            const Transform3D *parent_transform = ecs_get(it->world, parent, Transform3D);
            if (parent_transform) {
                mat4 world;
                glm_mat4_mul((float (*)[4])parent_transform->world, transform->local, world); // Cast to avoid const warning
                glm_mat4_copy(world, transform->world);
            }
        } else {
            glm_mat4_copy(local, transform->world);
        }

        transform->isDirty = false;

        // Mark children as dirty
        ecs_query_t *query_child = ecs_query(it->world, {
            .terms = {
                { .id = ecs_pair(EcsChildOf, it->entities[i]) }
            }
        });
        ecs_iter_t child_it = ecs_query_iter(it->world, query_child);

        while (ecs_query_next(&child_it)) {
            for (int j = 0; j < child_it.count; j++) {
                if (ecs_has(child_it.world, child_it.entities[j], Transform3D)) {
                    Transform3D *child_transform = ecs_get_mut(child_it.world, child_it.entities[j], Transform3D);
                    if (child_transform) {
                        child_transform->isDirty = true;
                        ecs_modified(child_it.world, child_it.entities[j], Transform3D);
                    }
                }
            }
        }
        ecs_query_fini(query_child);
    }
}
  • Transformation Order: The TRS order (glm_translate, glm_mat4_mul for rotation, glm_scale) ensures local rotations. Using Scale, Rotate, Translate (SRT) would cause orbiting.
  • Parent-Child: The world matrix is computed as parent_transform->world * transform->local to propagate the parent's transform.
  • Cascading: The system must process parents before children (handled in Step 4).
  • Const Cast: The (float (*)[4]) cast on parent_transform->world avoids a const qualifier warning, as glm_mat4_mul expects non-const matrices but is read-only for inputs.

Step 4: Define Systems with Cascading

Register the system with cascading to ensure parents are processed before children.

c

ecs_system(world, {
    .entity = ecs_entity(world, { .name = "update_transform_system" }),
    .query.terms = {{ .id = ecs_id(Transform3D) }},
    .query.flags = EcsQueryCascade,
    .query.order_by = EcsChildOf,
    .callback = update_transform_system,
    .phase = EcsPreUpdate
});
  • .query.flags = EcsQueryCascade: Ensures entities are processed in hierarchical order (parents first).
  • .query.order_by = EcsChildOf: Specifies the hierarchy relationship.
  • .phase = EcsPreUpdate: Runs the system before rendering to update transforms.

Step 5: Rendering System

Create a system to render entities using their world matrices.

c

void render_3d_cube_system(ecs_iter_t *it) {
    Transform3D *transforms = ecs_field(it, Transform3D, 0);
    CubeContext *cube = (CubeContext *)ecs_get_ctx(it->world);

    if (!cube) return;

    glUseProgram(cube->shaderProgram);
    glEnable(GL_DEPTH_TEST);

    mat4 view, projection;
    glm_mat4_identity(view);
    glm_lookat((vec3){0.0f, 0.0f, 5.0f}, (vec3){0.0f, 0.0f, 0.0f}, (vec3){0.0f, 1.0f, 0.0f}, view);

    int ww, hh;
    SDL_GetWindowSize(SDL_GL_GetCurrentWindow(), &ww, &hh);
    glm_perspective(glm_rad(45.0f), (float)ww / hh, 0.1f, 100.0f, projection);

    GLint modelLoc = glGetUniformLocation(cube->shaderProgram, "model");
    GLint viewLoc = glGetUniformLocation(cube->shaderProgram, "view");
    GLint projLoc = glGetUniformLocation(cube->shaderProgram, "projection");

    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, (float*)view);
    glUniformMatrix4fv(projLoc, 1, GL_FALSE, (float*)projection);

    glBindVertexArray(cube->vao);
    for (int i = 0; i < it->count; i++) {
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, (float*)transforms[i].world);
        glDrawElements(GL_TRIANGLES, cube->indexCount, GL_UNSIGNED_INT, 0);
    }
    glBindVertexArray(0);
}

Register the rendering system:

c

ecs_system(world, {
    .entity = ecs_entity(world, { .name = "render_3d_cube_system" }),
    .query.terms = {{ .id = ecs_id(Transform3D) }},
    .query.flags = EcsQueryCascade,
    .query.order_by = EcsChildOf,
    .callback = render_3d_cube_system,
    .phase = EcsOnUpdate
});
  • Context: The CubeContext (containing OpenGL VAO, VBO, etc.) is stored in the world context via ecs_set_ctx(world, cube, NULL).
  • World Matrix: The world matrix from Transform3D is used for rendering.

Step 6: Handle User Input (Optional)

To allow runtime modification (e.g., via ImGui), query entities and update their Transform3D components.

c

// In main loop
static int context_count = 0;
static ecs_entity_t selected_id = 0;
Transform3DContext* contexts = NULL;

igBegin("transform3d", NULL, 0);
if (igButton("query Transform3Ds", (ImVec2){0, 0})) {
    ecs_query_t *query = ecs_query(world, {
        .terms = {{ .id = ecs_id(Transform3D) }}
    });
    ecs_iter_t it = ecs_query_iter(world, query);
    int total_count = 0;
    while (ecs_query_next(&it)) {
        total_count += it.count;
    }
    if (total_count != context_count) {
        contexts = realloc(contexts, total_count * sizeof(Transform3DContext));
        context_count = total_count;
    }
    it = ecs_query_iter(world, query);
    int index = 0;
    while (ecs_query_next(&it)) {
        for (int i = 0; i < it.count; i++) {
            const char* name = ecs_get_name(world, it.entities[i]);
            contexts[index].name = name ? name : "Unnamed Entity";
            contexts[index].id = it.entities[i];
            index++;
        }
    }
    ecs_query_fini(query);
}

// Display buttons for each entity
for (int i = 0; i < context_count; i++) {
    char button_label[128];
    snprintf(button_label, sizeof(button_label), "%s (ID: %llu)", contexts[i].name, contexts[i].id);
    if (igButton(button_label, (ImVec2){0, 0})) {
        selected_id = contexts[i].id;
    }
}

// Edit selected entity's transform
if (selected_id != 0) {
    Transform3D* transform = ecs_get_mut(world, selected_id, Transform3D);
    if (transform) {
        bool changed = false;
        igText("Position");
        changed |= igInputFloat("X##pos", &transform->position[0], 0.1f, 1.0f, "%.3f", 0);
        changed |= igInputFloat("Y##pos", &transform->position[1], 0.1f, 1.0f, "%.3f", 0);
        changed |= igInputFloat("Z##pos", &transform->position[2], 0.1f, 1.0f, "%.3f", 0);
        igText("Rotation");
        changed |= igSliderFloat("X##rot", &transform->rotation[0], -1.0f, 1.0f, "%.3f", 0);
        changed |= igSliderFloat("Y##rot", &transform->rotation[1], -1.0f, 1.0f, "%.3f", 0);
        changed |= igSliderFloat("Z##rot", &transform->rotation[2], -1.0f, 1.0f, "%.3f", 0);
        changed |= igSliderFloat("W##rot", &transform->rotation[3], -1.0f, 1.0f, "%.3f", 0);
        igText("Scale");
        changed |= igInputFloat("X##scale", &transform->scale[0], 0.1f, 1.0f, "%.3f", 0);
        changed |= igInputFloat("Y##scale", &transform->scale[1], 0.1f, 1.0f, "%.3f", 0);
        changed |= igInputFloat("Z##scale", &transform->scale[2], 0.1f, 1.0f, "%.3f", 0);
        if (changed) {
            glm_quat_normalize(transform->rotation);
            transform->isDirty = true;
            ecs_modified(world, selected_id, Transform3D);
        }
    }
}
igEnd();
  • Normalization: glm_quat_normalize ensures valid quaternions after user input.
  • Dirty Flag: Setting isDirty = true triggers transform updates.

Common Pitfalls and Fixes

  1. Incorrect Transformation Order:

    • Problem: Using Scale, Rotate, Translate (SRT) causes children to orbit parents.
    • Fix: Use TRS order (glm_translate, glm_mat4_mul for rotation, glm_scale).
    • Why: Translation last applies rotation around the parent's origin.
  2. Flecs 3.x vs. 4.x Syntax:

    • Problem: Using ecs_query_new or ECS_SYSTEM causes compilation errors.
    • Fix: Use ecs_query and ecs_system with .terms, .flags, and .phase.
  3. Const Warnings:

    • Problem: ecs_get returns const pointers, causing warnings with non-const APIs (e.g., glm_mat4_mul).

    • Fix: Cast to float (*)[4] for read-only operations:

      c

      glm_mat4_mul((float (*)[4])parent_transform->world, transform->local, world);
  4. Missing Cascading:

    • Problem: Without EcsQueryCascade, children may be processed before parents, causing incorrect transforms.
    • Fix: Set .query.flags = EcsQueryCascade and .query.order_by = EcsChildOf.

Example: Full Working Code

The code provided in the previous conversation (with the corrected update_transform_system) is a complete example. Key points:

  • Components: Transform3D, Position, Velocity (unused in rendering).
  • Systems: start_up_system, update_transform_system, render_3d_cube_system.
  • Hierarchy: ParentCube, ChildCube, GrandchildCube with EcsChildOf pairs.
  • Rendering: Uses OpenGL with CGLM for matrix operations and SDL3 for windowing.

Example: Simplified Parent-Child SetupFor clarity, here's a minimal example without rendering:

c

#include "flecs.h"
#include <cglm/cglm.h>

typedef struct {
    vec3 position;
    vec4 rotation;
    vec3 scale;
    mat4 local;
    mat4 world;
    bool isDirty;
} Transform3D;
ECS_COMPONENT_DECLARE(Transform3D);

void update_transform(ecs_iter_t *it) {
    Transform3D *transforms = ecs_field(it, Transform3D, 0);
    for (int i = 0; i < it->count; i++) {
        Transform3D *t = &transforms[i];
        if (!t->isDirty) continue;
        mat4 local;
        glm_mat4_identity(local);
        glm_translate(local, t->position);
        mat4 rot;
        glm_quat_mat4(t->rotation, rot);
        glm_mat4_mul(local, rot, local);
        glm_scale(local, t->scale);
        glm_mat4_copy(local, t->local);
        ecs_entity_t parent = ecs_get_parent(it->world, it->entities[i]);
        if (parent && ecs_has(it->world, parent, Transform3D)) {
            const Transform3D *pt = ecs_get(it->world, parent, Transform3D);
            mat4 world;
            glm_mat4_mul((float (*)[4])pt->world, t->local, world);
            glm_mat4_copy(world, t->world);
        } else {
            glm_mat4_copy(local, t->world);
        }
        t->isDirty = false;
    }
}

int main() {
    ecs_world_t *world = ecs_init();
    ECS_COMPONENT(world, Transform3D);

    ecs_system(world, {
        .entity = ecs_entity(world, { .name = "update_transform" }),
        .query.terms = {{ .id = ecs_id(Transform3D) }},
        .query.flags = EcsQueryCascade,
        .query.order_by = EcsChildOf,
        .callback = update_transform,
        .phase = EcsPreUpdate
    });

    ecs_entity_t parent = ecs_entity(world, { .name = "Parent" });
    ecs_set(world, parent, Transform3D, {
        .position = {0.0f, 0.0f, 0.0f},
        .rotation = {0.0f, 0.0f, 0.0f, 1.0f},
        .scale = {1.0f, 1.0f, 1.0f},
        .isDirty = true
    });

    ecs_entity_t child = ecs_entity(world, { .name = "Child" });
    ecs_set(world, child, Transform3D, {
        .position = {1.0f, 0.0f, 0.0f},
        .rotation = {0.0f, sinf(glm_rad(45.0f)), 0.0f, cosf(glm_rad(45.0f))},
        .scale = {0.5f, 0.5f, 0.5f},
        .isDirty = true
    });
    ecs_add_pair(world, child, EcsChildOf, parent);

    ecs_progress(world, 0); // Run systems
    ecs_fini(world);
    return 0;
}

This example demonstrates the core hierarchy and transformation logic without rendering dependencies.

Guidelines for AI Agents

  1. Always Use TRS Order: For local transformations, apply translation, rotation, then scale to avoid orbiting.
  2. Enable Cascading: Use EcsQueryCascade and EcsChildOf in systems to ensure correct hierarchical processing.
  3. Use Flecs 4.x Syntax: Avoid ecs_query_new or ECS_SYSTEM. Use ecs_query and ecs_system with proper query terms.
  4. Handle Const Correctly: Cast const pointers when passing to non-const APIs like glm_mat4_mul, but verify the API is read-only for inputs.
  5. Validate Parent-Child: Use ecs_get_parent and ecs_is_valid to safely access parent transforms.
  6. Mark Dirty: Use a dirty flag to optimize updates, and propagate it to children using queries.

By following this guide, AI agents can correctly implement hierarchical transformation systems in Flecs 4.x, ensuring robust and predictable 3D transformations.

Credits:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment