warfactoryracine/docs/02-systems/rendering-system.md
StillHammer f393b28d73 Migrate core engine interfaces to GroveEngine repository
Removed core engine infrastructure from warfactoryracine:
- Core interfaces: IEngine, IModule, IModuleSystem, IIO, ITaskScheduler, ICoordinationModule
- Configuration system: IDataTree, IDataNode, DataTreeFactory
- UI system: IUI, IUI_Enums, ImGuiUI (header + implementation)
- Resource management: Resource, ResourceRegistry, SerializationRegistry
- Serialization: ASerializable, ISerializable
- World generation: IWorldGenerationStep (replaced by IWorldGenerationPhase)

These components now live in the GroveEngine repository and are included
via CMake add_subdirectory(../GroveEngine) for reusability across projects.

warfactoryracine remains focused on game-specific logic and content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 00:22:36 +08:00

14 KiB

Rendering System - Sprite Composition

Vue d'ensemble

Le système de rendu graphique de Warfactory utilise une approche de composition de sprites inspirée de Rimworld, optimisée pour minimiser le nombre de sprites à dessiner tout en permettant une grande flexibilité visuelle.

Principe fondamental : Chaque entité est composée de sprites superposés avec des orientations indépendantes, permettant un rendu dynamique efficace.

Architecture de composition

Personnages (Humains)

Un personnage est composé de 3 sprites maximum :

Personnage
├── Corps (sprite de base + vêtements composés)
├── Tête (orientation indépendante - turret behavior)
└── Arme (orientation indépendante - turret behavior)

Optimisation clé : Les vêtements sont pré-composés avec le corps pour éviter de dessiner chaque couche de vêtement séparément.

Exemple de composition

Rendu final = Corps_Base + Veste + Pantalon + Tête + Arme
              └─────────────────────┘
                   1 sprite composé

Au lieu de dessiner 5 sprites par personnage, on en dessine 3 :

  • 1 sprite corps composé (avec tous les vêtements)
  • 1 sprite tête
  • 1 sprite arme

Véhicules

Un véhicule suit le même principe avec un nombre variable de tourelles :

Véhicule
├── Corps (sprite fixe avec orientation principale)
└── Tourelles[] (orientations indépendantes)
    ├── Tourelle principale
    ├── Tourelle secondaire (optionnelle)
    └── Tourelle tertiaire (optionnelle)

Exemple :

  • Tank léger : Corps + 1 tourelle = 2 sprites
  • APC : Corps + 1 tourelle principale + 1 mitrailleuse = 3 sprites
  • Véhicule lourd : Corps + 3 tourelles = 4 sprites

Pipeline unifiée : Concept Fixe/Mobile

Le système distingue deux types de composants :

1. Composants Fixes

Définition : Composants dont l'orientation est liée à l'orientation principale de l'entité.

Caractéristiques :

  • Rotation synchronisée avec l'entité parente
  • Pas de calcul d'orientation indépendant
  • Exemples : corps de personnage, châssis de véhicule
struct FixedComponent {
    SpriteID sprite;
    Vector2 offset;        // Position relative au centre de l'entité
    float rotationOffset;  // Rotation additionnelle par rapport à l'orientation principale
};

2. Composants Mobiles (Turret Behavior)

Définition : Composants avec orientation indépendante suivant une cible.

Caractéristiques :

  • Orientation calculée indépendamment
  • Cible une position XY (target)
  • Contraintes de rotation optionnelles (angle min/max, vitesse de rotation)
  • Exemples : tête de personnage, arme, tourelle de véhicule
struct MobileComponent {
    SpriteID sprite;
    Vector2 offset;        // Position relative au centre de l'entité

    // Turret behavior
    Vector2 currentTarget; // Position XY actuelle de la cible
    float currentAngle;    // Angle actuel du composant
    float rotationSpeed;   // Vitesse de rotation max (rad/s)

    // Contraintes optionnelles
    float minAngle;        // Angle minimum (par rapport au fixe)
    float maxAngle;        // Angle maximum (par rapport au fixe)
    bool hasConstraints;   // Si true, applique les contraintes min/max
};

Pipeline de rendu unifiée

La même pipeline gère les têtes, armes et tourelles :

class EntityRenderer {
    // Composants fixes (corps)
    std::vector<FixedComponent> fixedComponents;

    // Composants mobiles (têtes, armes, tourelles)
    std::vector<MobileComponent> mobileComponents;

    void render(Vector2 position, float mainOrientation) {
        // 1. Dessiner les composants fixes
        for (const FixedComponent& comp : fixedComponents) {
            float finalAngle = mainOrientation + comp.rotationOffset;
            drawSprite(comp.sprite, position + rotate(comp.offset, mainOrientation), finalAngle);
        }

        // 2. Dessiner les composants mobiles
        for (MobileComponent& comp : mobileComponents) {
            // Calculer l'orientation vers la cible
            float targetAngle = calculateAngleToTarget(position, comp.offset, comp.currentTarget);

            // Appliquer la vitesse de rotation (smooth rotation)
            comp.currentAngle = lerpAngle(comp.currentAngle, targetAngle, comp.rotationSpeed * deltaTime);

            // Appliquer les contraintes si nécessaire
            if (comp.hasConstraints) {
                float relativeAngle = comp.currentAngle - mainOrientation;
                comp.currentAngle = mainOrientation + clamp(relativeAngle, comp.minAngle, comp.maxAngle);
            }

            drawSprite(comp.sprite, position + rotate(comp.offset, mainOrientation), comp.currentAngle);
        }
    }
};

Exemples concrets

Personnage avec arme

EntityRenderer soldier;

// Corps (fixe)
soldier.fixedComponents.push_back({
    .sprite = SPRITE_SOLDIER_BODY_COMPOSED,
    .offset = {0, 0},
    .rotationOffset = 0
});

// Tête (mobile - turret behavior)
soldier.mobileComponents.push_back({
    .sprite = SPRITE_HEAD,
    .offset = {0, 8},  // 8 pixels au-dessus du centre
    .currentTarget = enemyPosition,
    .currentAngle = 0,
    .rotationSpeed = 5.0f,  // rad/s
    .minAngle = -PI/2,      // Peut regarder 90° à gauche
    .maxAngle = PI/2,       // Peut regarder 90° à droite
    .hasConstraints = true
});

// Arme (mobile - turret behavior)
soldier.mobileComponents.push_back({
    .sprite = SPRITE_RIFLE,
    .offset = {4, 2},   // Décalage pour la main
    .currentTarget = enemyPosition,
    .currentAngle = 0,
    .rotationSpeed = 3.0f,  // Plus lent que la tête
    .minAngle = -PI/4,      // Contraintes plus strictes
    .maxAngle = PI/4,
    .hasConstraints = true
});

// Rendu
soldier.render(soldierPosition, soldierOrientation);

Véhicule avec tourelle

EntityRenderer tank;

// Châssis (fixe)
tank.fixedComponents.push_back({
    .sprite = SPRITE_TANK_HULL,
    .offset = {0, 0},
    .rotationOffset = 0
});

// Tourelle principale (mobile - rotation 360°)
tank.mobileComponents.push_back({
    .sprite = SPRITE_TANK_TURRET,
    .offset = {0, 0},
    .currentTarget = targetPosition,
    .currentAngle = 0,
    .rotationSpeed = 2.0f,  // Rotation lente (réaliste)
    .hasConstraints = false  // Rotation complète
});

// Rendu
tank.render(tankPosition, tankOrientation);

Optimisations

1. Composition de sprites (Layering)

Problème : Dessiner tous les vêtements séparément = beaucoup de draw calls.

Solution : Pré-composer les sprites en cache.

class SpriteComposer {
    std::unordered_map<CompositionKey, SpriteID> compositionCache;

    SpriteID getComposedSprite(const std::vector<LayerID>& layers) {
        CompositionKey key = hashLayers(layers);

        if (compositionCache.contains(key)) {
            return compositionCache[key];
        }

        // Composer les sprites
        SpriteID composed = renderToTexture(layers);
        compositionCache[key] = composed;
        return composed;
    }
};

Exemple :

// Composition d'un soldat
std::vector<LayerID> soldierLayers = {
    SPRITE_BODY_BASE,
    SPRITE_PANTS_CAMO,
    SPRITE_JACKET_TACTICAL,
    SPRITE_VEST_ARMOR
};

SpriteID composedBody = composer.getComposedSprite(soldierLayers);

Avantages :

  • Réduction massive des draw calls
  • Cache persistant entre frames
  • Mise à jour uniquement quand les vêtements changent

2. Sprite Batching

Grouper les sprites par texture pour minimiser les changements d'état GPU :

// Trier par texture avant de dessiner
std::sort(entities.begin(), entities.end(), [](const Entity& a, const Entity& b) {
    return a.getTextureID() < b.getTextureID();
});

// Dessiner tous les sprites de la même texture ensemble
for (const Entity& entity : entities) {
    entity.render();
}

3. Culling

Ne dessiner que les entités visibles à l'écran :

void renderEntities(const Camera& camera) {
    for (Entity& entity : entities) {
        if (camera.isVisible(entity.getBounds())) {
            entity.render();
        }
    }
}

Système de couches (Layering)

Ordre de rendu (Z-order)

Les sprites doivent être dessinés dans un ordre précis pour éviter les problèmes de superposition :

Z-Order (du plus bas au plus haut)
├── 0: Corps/Châssis (fixe)
├── 1: Tête/Tourelle (mobile)
└── 2: Arme (mobile)

Implémentation :

struct RenderableComponent {
    SpriteID sprite;
    Vector2 position;
    float rotation;
    int zOrder;  // Ordre de rendu
};

void renderEntity(const Entity& entity) {
    std::vector<RenderableComponent> renderables;

    // Collecter tous les composants
    collectFixedComponents(entity, renderables);
    collectMobileComponents(entity, renderables);

    // Trier par Z-order
    std::sort(renderables.begin(), renderables.end(),
        [](const RenderableComponent& a, const RenderableComponent& b) {
            return a.zOrder < b.zOrder;
        });

    // Dessiner dans l'ordre
    for (const RenderableComponent& comp : renderables) {
        drawSprite(comp.sprite, comp.position, comp.rotation);
    }
}

Gestion des orientations

Angles et conventions

Convention : Angle 0 = direction droite (East), rotation anti-horaire.

        N (π/2)
        |
W (π) --+-- E (0)
        |
      S (3π/2)

Calcul de l'angle vers une cible

float calculateAngleToTarget(Vector2 entityPos, Vector2 componentOffset, Vector2 target) {
    // Position mondiale du composant
    Vector2 worldPos = entityPos + componentOffset;

    // Direction vers la cible
    Vector2 direction = target - worldPos;

    // Angle en radians
    return std::atan2(direction.y, direction.x);
}

Interpolation d'angle (smooth rotation)

Pour éviter les rotations brusques, on interpole vers l'angle cible :

float lerpAngle(float current, float target, float speed, float deltaTime) {
    // Normaliser les angles entre -π et π
    float diff = normalizeAngle(target - current);

    // Calculer le changement maximum pour cette frame
    float maxChange = speed * deltaTime;

    // Appliquer le changement (limité par la vitesse)
    float change = std::clamp(diff, -maxChange, maxChange);

    return current + change;
}

float normalizeAngle(float angle) {
    while (angle > PI) angle -= 2 * PI;
    while (angle < -PI) angle += 2 * PI;
    return angle;
}

Extensions futures

1. Animations

Ajouter un système d'animation pour les sprites :

struct AnimatedComponent {
    std::vector<SpriteID> frames;
    float frameDuration;
    float currentTime;

    SpriteID getCurrentFrame() {
        int frameIndex = (int)(currentTime / frameDuration) % frames.size();
        return frames[frameIndex];
    }

    void update(float deltaTime) {
        currentTime += deltaTime;
    }
};

2. Effets visuels

Ajouter des effets visuels aux composants :

struct VisualEffect {
    Color tint;          // Couleur de teinte
    float opacity;       // Transparence
    Vector2 scale;       // Échelle
    bool flipX, flipY;   // Miroirs
};

3. Attachments dynamiques

Support pour des composants attachés dynamiquement :

class AttachmentSystem {
    void attachComponent(EntityID entity, MobileComponent component, const std::string& attachPoint);
    void detachComponent(EntityID entity, const std::string& attachPoint);
};

// Exemple : attacher une tourelle sur un point de montage
attachments.attachComponent(vehicleID, turretComponent, "mount_point_1");

Performances

Métriques cibles

  • Entités à l'écran : 500-1000 personnages/véhicules simultanés
  • Draw calls : < 100 par frame (via batching)
  • FPS cible : 60 fps (16.67ms par frame)
  • Temps de rendu : < 8ms par frame (50% du budget)

Profiling

Points de mesure critiques :

void renderAll() {
    PROFILE_SCOPE("Entity Rendering");

    {
        PROFILE_SCOPE("Sprite Composition");
        updateComposedSprites();
    }

    {
        PROFILE_SCOPE("Culling");
        cullEntities(camera);
    }

    {
        PROFILE_SCOPE("Sorting");
        sortByTexture(visibleEntities);
    }

    {
        PROFILE_SCOPE("Drawing");
        drawEntities(visibleEntities);
    }
}

Intégration avec les autres systèmes

Lien avec le système militaire

Le système de rendu est découplé des systèmes de combat :

  • Combat System calcule les positions, rotations, cibles
  • Rendering System lit ces données et dessine les sprites
// Dans le module Combat
void updateCombat(Entity& entity, float deltaTime) {
    // Calculer la cible
    Vector2 target = findClosestEnemy(entity.position);
    entity.setTarget(target);

    // Le rendering system lira cette cible plus tard
}

// Dans le module Rendering
void render(const Entity& entity) {
    // Lire la cible depuis l'entité
    Vector2 target = entity.getTarget();

    // Dessiner avec la cible
    entityRenderer.render(entity.position, entity.orientation, target);
}

Lien avec le système de pathfinding

Le système de rendu utilise les positions calculées par le pathfinding :

// Pathfinding calcule la position
Vector2 newPosition = pathfinding.getNextPosition(entity);
entity.setPosition(newPosition);

// Rendering dessine à cette position
render(entity);

Statut : DESIGN COMPLETE Prochaines étapes :

  1. Implémenter EntityRenderer avec la pipeline fixe/mobile
  2. Créer SpriteComposer pour la composition de sprites
  3. Intégrer avec le système de combat pour les cibles
  4. Optimiser avec batching et culling