Aller au contenu

Shaders et Post-Processing

Effets visuels et filtres d'accessibilité.

Architecture

flowchart LR
    subgraph Render Pipeline
        Scene[Scene Render] --> FB[Framebuffer]
        FB --> PP[Post-Process]
        PP --> Screen[Display]
    end

    subgraph Shaders
        Bloom[Bloom]
        CB[Colorblind]
        Vignette[Vignette]
    end

    PP --> Bloom
    PP --> CB
    PP --> Vignette

    style PP fill:#7c3aed,color:#fff

Post-Processing Manager

class PostProcessor {
    GLuint framebuffer_;
    GLuint colorTexture_;
    GLuint quadVAO_;

    std::vector<Shader*> effects_;
    bool colorblindEnabled_ = false;
    ColorblindMode colorblindMode_ = ColorblindMode::Normal;

public:
    void init(int width, int height) {
        // Create framebuffer
        glGenFramebuffers(1, &framebuffer_);
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);

        // Color attachment
        glGenTextures(1, &colorTexture_);
        glBindTexture(GL_TEXTURE_2D, colorTexture_);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height,
                     0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                               GL_TEXTURE_2D, colorTexture_, 0);

        createQuad();
    }

    void beginScene() {
        glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
        glClear(GL_COLOR_BUFFER_BIT);
    }

    void endScene() {
        glBindFramebuffer(GL_FRAMEBUFFER, 0);

        // Apply effects chain
        for (auto* effect : effects_) {
            applyEffect(effect);
        }

        // Final render to screen
        renderToScreen();
    }
};

Filtres Daltonisme

Modes Supportés

Mode Description Prévalence
Protanopia Rouge absent 1% hommes
Deuteranopia Vert absent 1% hommes
Tritanopia Bleu absent 0.01%
Achromatopsia Noir/blanc Rare
HighContrast Contraste élevé -

Shader Daltonisme

#version 330 core

in vec2 TexCoords;
out vec4 FragColor;

uniform sampler2D screenTexture;
uniform int colorblindMode;

// Matrices de transformation
const mat3 PROTANOPIA = mat3(
    0.567, 0.433, 0.000,
    0.558, 0.442, 0.000,
    0.000, 0.242, 0.758
);

const mat3 DEUTERANOPIA = mat3(
    0.625, 0.375, 0.000,
    0.700, 0.300, 0.000,
    0.000, 0.300, 0.700
);

const mat3 TRITANOPIA = mat3(
    0.950, 0.050, 0.000,
    0.000, 0.433, 0.567,
    0.000, 0.475, 0.525
);

void main() {
    vec3 color = texture(screenTexture, TexCoords).rgb;

    switch (colorblindMode) {
        case 1: // Protanopia
            color = PROTANOPIA * color;
            break;
        case 2: // Deuteranopia
            color = DEUTERANOPIA * color;
            break;
        case 3: // Tritanopia
            color = TRITANOPIA * color;
            break;
        case 4: // Achromatopsia
            float gray = dot(color, vec3(0.299, 0.587, 0.114));
            color = vec3(gray);
            break;
        case 5: // High Contrast
            color = pow(color, vec3(0.7)) * 1.2;
            color = clamp(color, 0.0, 1.0);
            break;
    }

    FragColor = vec4(color, 1.0);
}

Effets Visuels

Bloom (Lueur)

// bloom_extract.frag - Extraction des zones lumineuses
#version 330 core

in vec2 TexCoords;
out vec4 FragColor;

uniform sampler2D scene;
uniform float threshold;

void main() {
    vec3 color = texture(scene, TexCoords).rgb;
    float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722));

    if (brightness > threshold) {
        FragColor = vec4(color, 1.0);
    } else {
        FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}

// bloom_blur.frag - Flou gaussien
#version 330 core

in vec2 TexCoords;
out vec4 FragColor;

uniform sampler2D image;
uniform bool horizontal;

const float weights[5] = float[](
    0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216
);

void main() {
    vec2 texOffset = 1.0 / textureSize(image, 0);
    vec3 result = texture(image, TexCoords).rgb * weights[0];

    for (int i = 1; i < 5; i++) {
        vec2 offset = horizontal
            ? vec2(texOffset.x * i, 0.0)
            : vec2(0.0, texOffset.y * i);

        result += texture(image, TexCoords + offset).rgb * weights[i];
        result += texture(image, TexCoords - offset).rgb * weights[i];
    }

    FragColor = vec4(result, 1.0);
}

Vignette

#version 330 core

in vec2 TexCoords;
out vec4 FragColor;

uniform sampler2D scene;
uniform float intensity;
uniform float radius;

void main() {
    vec3 color = texture(scene, TexCoords).rgb;

    vec2 center = TexCoords - vec2(0.5);
    float dist = length(center);
    float vignette = smoothstep(radius, radius - 0.2, dist);

    color *= mix(1.0, vignette, intensity);

    FragColor = vec4(color, 1.0);
}

Support par Backend

SFML uniquement

Les shaders post-processing ne sont supportés que par le backend SFML. Le backend SDL2 utilise une approche différente (voir ci-dessous).

SFML : Shaders GLSL

class SFMLWindow : public IWindow {
    sf::Shader _colorblindShader;
    sf::RenderTexture _renderTexture;
    std::string _activeShader;

public:
    bool loadShader(const std::string& key,
                    const std::string& vertPath,
                    const std::string& fragPath) override {
        return _shaders[key].loadFromFile(vertPath, fragPath);
    }

    void setPostProcessShader(const std::string& key) override {
        _activeShader = key;
    }

    void beginFrame() override {
        _renderTexture.clear();
        // Render to texture instead of window
    }

    void endFrame() override {
        // Apply shader and draw to window
        sf::Sprite sprite(_renderTexture.getTexture());
        _window.draw(sprite, &_shaders[_activeShader]);
    }

    bool supportsShaders() const override { return true; }
};

SDL2 : Pas de Shaders

SDL2 ne supporte pas les shaders dans cette implémentation.

class SDL2Window : public IWindow {
    bool loadShader(...) override { return false; }
    void setPostProcessShader(...) override { /* no-op */ }
    bool supportsShaders() const override { return false; }
};

Pour le mode daltonien, SDL2 utilise AccessibilityConfig qui fournit des palettes de couleurs alternatives au lieu de transformer l'image :

// Le code de rendu utilise les couleurs de AccessibilityConfig
auto color = AccessibilityConfig::getInstance().getPlayerColor();
window.drawRect(x, y, w, h, {color.r, color.g, color.b, color.a});

Voir Accessibilité pour plus de détails.


Configuration

{
  "graphics": {
    "postProcessing": {
      "enabled": true,
      "bloom": {
        "enabled": true,
        "threshold": 0.8,
        "intensity": 1.0
      },
      "vignette": {
        "enabled": false,
        "intensity": 0.3,
        "radius": 0.8
      }
    },
    "accessibility": {
      "colorblindMode": "none"
    }
  }
}

Pipeline Complet

sequenceDiagram
    participant Game as Game Loop
    participant PP as PostProcessor
    participant FB as Framebuffer
    participant Shader as Shaders
    participant Screen as Display

    Game->>PP: beginScene()
    PP->>FB: Bind framebuffer

    Note over Game: Render scene...

    Game->>PP: endScene()
    PP->>Shader: Extract bright pixels
    PP->>Shader: Blur (horizontal)
    PP->>Shader: Blur (vertical)
    PP->>Shader: Combine bloom
    PP->>Shader: Apply colorblind
    PP->>Screen: Final render

Paramètres

Paramètre Défaut Description
bloom.threshold 0.8 Seuil luminosité
bloom.intensity 1.0 Force du bloom
vignette.intensity 0.3 Assombrissement bords
vignette.radius 0.8 Rayon central