Appendix: Common Types Reference

This appendix provides authoritative, consolidated definitions for the shared types and interfaces used throughout the Advanced glTF series.

Core Scene Graph Types

static constexpr uint32_t INVALID_NODE_INDEX = 0xFFFFFFFF;

enum TransformStatus : uint8_t {
    Clean      = 0,
    LocalDirty = 1 << 0,  // This node's SRT components changed
    WorldDirty = 1 << 1   // This node's world matrix needs recalculation
};

struct ColliderDef {
    enum class Shape { CAPSULE, BOX, NONE };
    Shape shape     = Shape::NONE;
    float radius    = 0.0f;
    float half_height = 0.0f;
    glm::vec3 box_half_extents = {0,0,0};
    float mass      = 1.0f;
    std::string collision_group;
    std::string collision_mask;
};

struct ConstraintDef {
    enum class Type { NONE, BALL_SOCKET, HINGE };
    Type        type              = Type::NONE;
    float       swing_limit_deg   = 180.0f;
    float       twist_limit_deg   = 180.0f;
    float       hinge_min_deg     = -180.0f;
    float       hinge_max_deg     =  180.0f;
    glm::vec3   hinge_axis        = {0,0,1};
    std::string parent_bone;
};

struct Node {
    uint32_t node_index;
    uint32_t parent_index = INVALID_NODE_INDEX;
    std::vector<uint32_t> child_indices;
    std::string name;

    // Local transform data (SRT: Scale, Rotation, Translation)
    glm::vec3 translation    = {0,0,0};
    glm::quat local_rotation = glm::identity<glm::quat>();
    glm::vec3 scale          = {1,1,1};

    // Cached world matrix
    glm::mat4 world_matrix = glm::mat4(1.0f);

    uint8_t status = TransformStatus::Clean;
    bool is_joint = false;

    // Physics metadata (extracted from glTF extras)
    ColliderDef collider_def;
    ConstraintDef constraint_def;

    // Call this whenever you change translation, rotation, or scale
    void mark_dirty() {
        status |= TransformStatus::LocalDirty | TransformStatus::WorldDirty;
    }

    // Computes the local transform matrix from SRT components (TRS order)
    glm::mat4 get_local_matrix() const {
        return glm::translate(glm::mat4(1.0f), translation) *
               glm::mat4_cast(local_rotation) *
               glm::scale(glm::mat4(1.0f), scale);
    }

    // Safely extracts rotation from the world matrix, stripping any scale.
    glm::quat get_world_rotation() const {
        glm::mat3 rot_scale = glm::mat3(world_matrix);
        glm::mat3 rotation;
        rotation[0] = glm::normalize(rot_scale[0]);
        rotation[1] = glm::normalize(rot_scale[1]);
        rotation[2] = glm::normalize(rot_scale[2]);
        return glm::quat_cast(rotation);
    }
};

class SceneGraph {
public:
    std::vector<Node> nodes;

    // Linear update: Only works if nodes are topologically sorted
    void update_transforms() {
        for (auto& node : nodes) {
            if (node.status & TransformStatus::WorldDirty) {
                if (node.parent_index != INVALID_NODE_INDEX) {
                    node.world_matrix = nodes[node.parent_index].world_matrix * node.get_local_matrix();
                } else {
                    node.world_matrix = node.get_local_matrix();
                }

                for (uint32_t child_index : node.child_indices) {
                    nodes[child_index].status |= TransformStatus::WorldDirty;
                }
                node.status = TransformStatus::Clean;
            }
        }
    }

    // Recursive update: Handles any node order and sub-tree updates
    void update_world_matrices_subtree(uint32_t index) {
        Node& node = nodes[index];
        if (node.parent_index != INVALID_NODE_INDEX) {
            node.world_matrix = nodes[node.parent_index].world_matrix * node.get_local_matrix();
        } else {
            node.world_matrix = node.get_local_matrix();
        }
        for (uint32_t child_idx : node.child_indices) {
            nodes[child_idx].status |= TransformStatus::WorldDirty;
            update_world_matrices_subtree(child_idx);
        }
        node.status = TransformStatus::Clean;
    }
};

// Free-function helper for recursive subtree updates (matches IK chapter usage)
inline void update_world_matrices_subtree(std::vector<Node>& nodes, uint32_t index) {
    Node& node = nodes[index];
    if (node.parent_index != INVALID_NODE_INDEX) {
        node.world_matrix = nodes[node.parent_index].world_matrix * node.get_local_matrix();
    } else {
        node.world_matrix = node.get_local_matrix();
    }
    for (uint32_t child_idx : node.child_indices) {
        nodes[child_idx].status |= TransformStatus::WorldDirty;
        update_world_matrices_subtree(nodes, child_idx);
    }
    node.status = TransformStatus::Clean;
}

Animation & Skinning Types

enum InterpolationMode { STEP, LINEAR, CUBICSPLINE };

struct AnimationSampler {
    InterpolationMode interpolation = LINEAR;
    std::vector<float> inputs;              // Timestamps in seconds
    std::vector<glm::vec4> outputs_raw;     // Packed: for CUBICSPLINE stores in_tan/value/out_tan triples

    // For CUBICSPLINE, we split the raw data for easier interpolation
    std::vector<glm::vec4> in_tangents;
    std::vector<glm::vec4> values;
    std::vector<glm::vec4> out_tangents;
};

struct AnimationChannel {
    enum PathType { TRANSLATION, ROTATION, SCALE, WEIGHTS };
    PathType path;
    uint32_t node_index;
    uint32_t sampler_index;
};

struct Pose {
    std::vector<glm::vec3> translations;
    std::vector<glm::quat> rotations;
    std::vector<glm::vec3> scales;
};

// Matches the glTF skin object.
struct Skin {
    std::string              name;
    std::vector<uint32_t>    joints;                // Node indices, one per joint
    std::vector<glm::mat4>   inverse_bind_matrices; // One per joint
    uint32_t                 skeleton_root = INVALID_NODE_INDEX;
};

// Binary search for the keyframe index corresponding to time
uint32_t find_keyframe(const AnimationSampler& sampler, float time) {
    if (sampler.inputs.size() < 2) return 0;
    auto it = std::lower_bound(sampler.inputs.begin(), sampler.inputs.end(), time);
    uint32_t idx = static_cast<uint32_t>(std::distance(sampler.inputs.begin(), it));
    return (idx > 0) ? idx - 1 : 0;
}

// Pre-computes joint matrices (JointWorldMatrix * InverseBindMatrix) for GPU upload.
// Call after animation update and scene graph update, once per frame.
void compute_joint_matrices(
    const Skin&              skin,
    const std::vector<Node>& nodes,
    std::vector<glm::mat4>&  joint_matrices_out)
{
    joint_matrices_out.resize(skin.joints.size());
    for (size_t i = 0; i < skin.joints.size(); ++i) {
        const Node& joint_node = nodes[skin.joints[i]];
        joint_matrices_out[i] = joint_node.world_matrix * skin.inverse_bind_matrices[i];
    }
}

// Bridges the Animation output (Pose) back to the Scene Graph Nodes
void apply_pose_to_scene_graph(std::vector<Node>& nodes, const Pose& pose, const std::vector<uint32_t>& joint_indices) {
    for (size_t i = 0; i < joint_indices.size(); ++i) {
        Node& node = nodes[joint_indices[i]];
        node.translation    = pose.translations[i];
        node.local_rotation = pose.rotations[i];
        node.scale          = pose.scales[i];
        node.mark_dirty();
    }
}

Physics Types

struct PhysicsPose {
    glm::vec3 position;
    glm::quat orientation;

    glm::mat4 to_matrix() const {
        return glm::translate(glm::mat4(1.0f), position) * glm::mat4_cast(orientation);
    }
};

class PhysicsWorld {
public:
    virtual ~PhysicsWorld() = default;

    // Global lifecycle
    static void global_init();
    static void global_shutdown();
    static std::unique_ptr<PhysicsWorld> create();

    // Body management
    virtual JPH::BodyID create_body(const JPH::BodyCreationSettings& settings) = 0;
    virtual void        destroy_body(JPH::BodyID body_id) = 0;
    virtual void        set_motion_type(JPH::BodyID body_id, JPH::EMotionType type) = 0;
    virtual void        set_object_layer(JPH::BodyID body_id, uint16_t layer) = 0;
    virtual void        activate_body(JPH::BodyID body_id) = 0;

    // Kinematic/dynamic sync
    virtual void        move_kinematic(JPH::BodyID body_id, const PhysicsPose& pose) = 0;
    virtual PhysicsPose get_body_pose(JPH::BodyID body_id) const = 0;
    virtual glm::vec3   get_linear_velocity(JPH::BodyID body_id) const = 0;
    virtual void        set_linear_velocity(JPH::BodyID body_id, const glm::vec3& velocity) = 0;

    // Constraints (angles in radians)
    virtual void create_ball_socket_constraint(JPH::BodyID p1, JPH::BodyID p2,
                                               float swing_rad, float twist_rad) = 0;
    virtual void create_hinge_constraint(JPH::BodyID p1, JPH::BodyID p2,
                                         const glm::vec3& axis,
                                         float min_angle_rad, float max_angle_rad) = 0;

    // Simulation
    virtual void step(float delta_seconds) = 0;

    // Queries
    virtual bool raycast(const glm::vec3& origin, const glm::vec3& direction, float max_distance,
                         float& out_distance, glm::vec3& out_normal,
                         JPH::BodyID& out_body_id) const = 0;
};

IK & Procedural Types

struct IKChain {
    std::vector<uint32_t> joints;  // Ordered from root to end effector
    uint32_t effector_node;        // The node whose position we are trying to place
    float    threshold;            // Convergence threshold in world-space units
    int      max_iterations;       // Safety cap
    glm::vec3 target_world;        // Target position
    glm::vec3 pole_vector;         // For algorithms like FABRIK or constrained CCD
};