Shader support #106

Closed
opened 2025-07-06 02:49:59 +00:00 by john · 4 comments
Owner

concept:

sprite.shader = "glow.frag"

Will shaders be available from Python (string -> shader code) or is this a "compiled-only" feature?

concept: ``` sprite.shader = "glow.frag" ``` Will shaders be available from Python (string -> shader code) or is this a "compiled-only" feature?
Author
Owner

SFML 2.6 Shader Research (January 2026)

Executive Summary

Shaders can be loaded at runtime from strings - no compilation into a library needed.

SFML's sf::Shader class wraps OpenGL GLSL shaders with a simple API:

sf::Shader shader;
// Load from file
shader.loadFromFile("glow.frag", sf::Shader::Fragment);

// OR load from string at runtime
std::string fragCode = R"(
    uniform sampler2D texture;
    uniform float intensity;
    void main() {
        vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
        gl_FragColor = pixel * intensity;
    }
)";
shader.loadFromMemory(fragCode, sf::Shader::Fragment);

Apply when drawing:

window.draw(sprite, &shader);  // Simple!

What Effects Are Possible

Effect Complexity How It Works
Glow/Bloom Medium Multi-pass: extract bright pixels → blur → blend back
Color Effects Simple Single fragment shader modifying pixel colors
Outline Simple Sample neighboring pixels, detect edges
Distortion Simple Modify texture coordinates (ripple, wave, heat)
Pixelation Simple Quantize texture coordinates
Lighting Medium Per-pixel light calculations with normals
Reflection/Refraction Advanced Environment mapping, ray calculations
Scintillation/Sparkle Medium Time-based noise patterns on bright pixels

Current Architecture Status

The codebase already has infrastructure shaders need:

  1. RenderTexture usage - UIFrame, UIGrid, and GridLayers already render to textures (required for post-processing)
  2. Dirty flags - Cache invalidation system means shaders won't cause unnecessary re-renders
  3. Draw pipeline - All drawables use target.draw() which accepts an optional shader parameter

Proposed Python API

Option 1: Per-Drawable Shaders

sprite.shader = mcrfpy.Shader("glow.frag")  # Load from file
sprite.shader = mcrfpy.Shader(source=glsl_string)  # Load from string

# Set shader parameters
sprite.shader.set_uniform("intensity", 1.5)
sprite.shader.set_uniform("glow_color", (1.0, 0.8, 0.0, 1.0))

Option 2: Scene Post-Processing

scene.post_shader = mcrfpy.Shader(source=bloom_shader_code)

Implementation Components

Component Effort Notes
PyShader class with Python bindings Medium Wrap sf::Shader
Integration with UISprite Simple Add shader member, modify render()
Integration with UICaption Simple Same pattern
Integration with UIFrame Medium Children need consideration
Integration with UIGrid/Layers Medium-High Chunk system complexity
Scene-level post-processing Medium Render scene to texture first

Key sf::Shader API Methods

Method Purpose
loadFromMemory(string, Type) Load shader from Python string
loadFromFile(path, Type) Load shader from file
setUniform(name, value) Pass parameters (float, vec2, vec3, vec4, texture, color, transform)
sf::Shader::isAvailable() Check if GPU supports shaders

Caveats

  1. GPU Compatibility: Must check sf::Shader::isAvailable() - some old GPUs don't support shaders
  2. Performance: Complex shaders or many shader switches per frame can hurt FPS
  3. Multi-pass effects (like proper bloom) require rendering to intermediate textures
  4. GLSL version: SFML uses GLSL 1.2 syntax by default (compatible with older hardware)

Prerequisite: Harden RenderTexture Systems

Before relying on RenderTexture for shader post-processing, the existing RenderTexture infrastructure should be hardened:

  • UIFrame's render_texture / cache_subtree system
  • UIGrid's renderTexture and chunk textures in GridLayers
  • SceneTransition's texture capture

These systems work but haven't been stress-tested for the kind of multi-pass rendering that advanced shader effects require.


References

## SFML 2.6 Shader Research (January 2026) ### Executive Summary **Shaders can be loaded at runtime from strings - no compilation into a library needed.** SFML's `sf::Shader` class wraps OpenGL GLSL shaders with a simple API: ```cpp sf::Shader shader; // Load from file shader.loadFromFile("glow.frag", sf::Shader::Fragment); // OR load from string at runtime std::string fragCode = R"( uniform sampler2D texture; uniform float intensity; void main() { vec4 pixel = texture2D(texture, gl_TexCoord[0].xy); gl_FragColor = pixel * intensity; } )"; shader.loadFromMemory(fragCode, sf::Shader::Fragment); ``` Apply when drawing: ```cpp window.draw(sprite, &shader); // Simple! ``` --- ### What Effects Are Possible | Effect | Complexity | How It Works | |--------|------------|--------------| | **Glow/Bloom** | Medium | Multi-pass: extract bright pixels → blur → blend back | | **Color Effects** | Simple | Single fragment shader modifying pixel colors | | **Outline** | Simple | Sample neighboring pixels, detect edges | | **Distortion** | Simple | Modify texture coordinates (ripple, wave, heat) | | **Pixelation** | Simple | Quantize texture coordinates | | **Lighting** | Medium | Per-pixel light calculations with normals | | **Reflection/Refraction** | Advanced | Environment mapping, ray calculations | | **Scintillation/Sparkle** | Medium | Time-based noise patterns on bright pixels | --- ### Current Architecture Status The codebase already has infrastructure shaders need: 1. **RenderTexture usage** - UIFrame, UIGrid, and GridLayers already render to textures (required for post-processing) 2. **Dirty flags** - Cache invalidation system means shaders won't cause unnecessary re-renders 3. **Draw pipeline** - All drawables use `target.draw()` which accepts an optional shader parameter --- ### Proposed Python API #### Option 1: Per-Drawable Shaders ```python sprite.shader = mcrfpy.Shader("glow.frag") # Load from file sprite.shader = mcrfpy.Shader(source=glsl_string) # Load from string # Set shader parameters sprite.shader.set_uniform("intensity", 1.5) sprite.shader.set_uniform("glow_color", (1.0, 0.8, 0.0, 1.0)) ``` #### Option 2: Scene Post-Processing ```python scene.post_shader = mcrfpy.Shader(source=bloom_shader_code) ``` --- ### Implementation Components | Component | Effort | Notes | |-----------|--------|-------| | PyShader class with Python bindings | Medium | Wrap sf::Shader | | Integration with UISprite | Simple | Add shader member, modify render() | | Integration with UICaption | Simple | Same pattern | | Integration with UIFrame | Medium | Children need consideration | | Integration with UIGrid/Layers | Medium-High | Chunk system complexity | | Scene-level post-processing | Medium | Render scene to texture first | --- ### Key sf::Shader API Methods | Method | Purpose | |--------|---------| | `loadFromMemory(string, Type)` | Load shader from Python string | | `loadFromFile(path, Type)` | Load shader from file | | `setUniform(name, value)` | Pass parameters (float, vec2, vec3, vec4, texture, color, transform) | | `sf::Shader::isAvailable()` | Check if GPU supports shaders | --- ### Caveats 1. **GPU Compatibility**: Must check `sf::Shader::isAvailable()` - some old GPUs don't support shaders 2. **Performance**: Complex shaders or many shader switches per frame can hurt FPS 3. **Multi-pass effects** (like proper bloom) require rendering to intermediate textures 4. **GLSL version**: SFML uses GLSL 1.2 syntax by default (compatible with older hardware) --- ### Prerequisite: Harden RenderTexture Systems Before relying on RenderTexture for shader post-processing, the existing RenderTexture infrastructure should be hardened: - UIFrame's `render_texture` / `cache_subtree` system - UIGrid's `renderTexture` and chunk textures in GridLayers - SceneTransition's texture capture These systems work but haven't been stress-tested for the kind of multi-pass rendering that advanced shader effects require. --- ### References - [SFML 2.6 Shader Tutorial](https://www.sfml-dev.org/tutorials/2.6/graphics-shader.php) - [sf::Shader Class Reference](https://www.sfml-dev.org/documentation/2.6.0/classsf_1_1Shader.php) - [LearnOpenGL Bloom Effect](https://learnopengl.com/Advanced-Lighting/Bloom) - [Basic Fragment Shader with SFML](https://duerrenberger.dev/blog/2021/08/08/basic-fragment-shader-with-sfml/)
Author
Owner

POC Complete - Design Decisions for Uniform Binding

Commit 41d551e adds a working shader_enabled property to UIFrame with a hardcoded test shader. Shaders work with all RenderTexture modes (basic, clip_children, cache_subtree).

Answer to original question: Yes, shaders can be loaded from strings at runtime via SFML's sf::Shader::loadFromMemory(). No compilation into the binary required.


Uniform Binding System Design

1. Engine-Provided Uniforms (Auto-Managed)

Applied FIRST every frame to all shaders:

Uniform Type Description
time float Seconds since program start
delta_time float Frame delta time
resolution vec2 Texture/drawable dimensions
mouse vec2 Mouse position (TBD: coordinate space)
  • If shader doesn't use them, GLSL silently ignores
  • User can override by setting same name (applied AFTER engine uniforms)
  • Warning issued if user sets a reserved name (but allowed)

2. Static Uniforms (User-Defined Constants)

shader.set_uniform("intensity", 0.8)
shader.set_uniform("glow_color", (1.0, 0.5, 0.0, 1.0))  # vec4
  • Stored per-drawable
  • Applied after engine uniforms
  • Types: float, int, vec2, vec3, vec4, Color

3. Dynamic Uniforms

Option A: Property Binding (C++ path, fast)

shader.bind_property("alpha", other_frame, "opacity")
  • Uses existing getProperty() infrastructure
  • No Python per frame
  • Limited to UIDrawable properties
  • Aligns with animation system data model

Option B: Callable Binding (Python path, explicit)

shader.bind_callable("health_pct", lambda: player.health / 100.0)
  • Explicit opt-in to "Python every frame"
  • Most flexible for arbitrary game state
  • Closure captures handle object lifetime naturally

4. Animation Integration

Extend existing animation system to target shader uniforms:

frame.animate("shader.intensity", 1.0, 2.0, "easeOut")
frame.animate("shader.glow_color.r", 255, 1.0, "linear")
  • C++ handles interpolation, no Python per frame
  • Consistent with existing property animation

Dynamic Flag for Dirty System Integration

Critical: Shaders using time or delta_time defeat RenderTexture caching.

shader = mcrfpy.Shader(source, dynamic=True)
# OR
shader.dynamic = True

When dynamic=True:

  • Drawable is re-rendered every frame (ignores dirty flag optimization)
  • Propagates up UI tree - parent caches are invalidated
  • Could potentially auto-detect via shader introspection for time/delta_time uniforms

Remaining Work

  1. PyShader class - Python-exposed shader object with uniform storage
  2. Extend RenderTexture to UISprite, UICaption - Currently only UIFrame uses it
  3. UIEntity shader support - Entities rendered on Grid
  4. GridLayer shader support - Apply shaders to entire tile layers
  5. Shader introspection - glGetActiveUniform for validation and auto-detection
  6. Error handling - Capture sf::err() for Python exceptions on compile failure
## POC Complete - Design Decisions for Uniform Binding Commit 41d551e adds a working `shader_enabled` property to UIFrame with a hardcoded test shader. Shaders work with all RenderTexture modes (basic, clip_children, cache_subtree). **Answer to original question:** Yes, shaders can be loaded from strings at runtime via SFML's `sf::Shader::loadFromMemory()`. No compilation into the binary required. --- ## Uniform Binding System Design ### 1. Engine-Provided Uniforms (Auto-Managed) Applied FIRST every frame to all shaders: | Uniform | Type | Description | |---------|------|-------------| | `time` | float | Seconds since program start | | `delta_time` | float | Frame delta time | | `resolution` | vec2 | Texture/drawable dimensions | | `mouse` | vec2 | Mouse position (TBD: coordinate space) | - If shader doesn't use them, GLSL silently ignores - User can override by setting same name (applied AFTER engine uniforms) - **Warning issued** if user sets a reserved name (but allowed) ### 2. Static Uniforms (User-Defined Constants) ```python shader.set_uniform("intensity", 0.8) shader.set_uniform("glow_color", (1.0, 0.5, 0.0, 1.0)) # vec4 ``` - Stored per-drawable - Applied after engine uniforms - Types: float, int, vec2, vec3, vec4, Color ### 3. Dynamic Uniforms **Option A: Property Binding (C++ path, fast)** ```python shader.bind_property("alpha", other_frame, "opacity") ``` - Uses existing `getProperty()` infrastructure - No Python per frame - Limited to UIDrawable properties - Aligns with animation system data model **Option B: Callable Binding (Python path, explicit)** ```python shader.bind_callable("health_pct", lambda: player.health / 100.0) ``` - Explicit opt-in to "Python every frame" - Most flexible for arbitrary game state - Closure captures handle object lifetime naturally ### 4. Animation Integration Extend existing animation system to target shader uniforms: ```python frame.animate("shader.intensity", 1.0, 2.0, "easeOut") frame.animate("shader.glow_color.r", 255, 1.0, "linear") ``` - C++ handles interpolation, no Python per frame - Consistent with existing property animation --- ## Dynamic Flag for Dirty System Integration **Critical:** Shaders using `time` or `delta_time` defeat RenderTexture caching. ```python shader = mcrfpy.Shader(source, dynamic=True) # OR shader.dynamic = True ``` When `dynamic=True`: - Drawable is re-rendered every frame (ignores dirty flag optimization) - Propagates up UI tree - parent caches are invalidated - Could potentially auto-detect via shader introspection for `time`/`delta_time` uniforms --- ## Remaining Work 1. **PyShader class** - Python-exposed shader object with uniform storage 2. **Extend RenderTexture to UISprite, UICaption** - Currently only UIFrame uses it 3. **UIEntity shader support** - Entities rendered on Grid 4. **GridLayer shader support** - Apply shaders to entire tile layers 5. **Shader introspection** - `glGetActiveUniform` for validation and auto-detection 6. **Error handling** - Capture `sf::err()` for Python exceptions on compile failure
Author
Owner

Architecture Design - Finalized

Shared Intermediate RenderTexture

All shader-enabled drawables will use a shared screen-resolution RenderTexture for shader processing:

// Static in UIDrawable or GameEngine
static sf::RenderTexture shader_intermediate;
// Size: mcrfpy.window.game_resolution (not window size)

Rationale:

  • Shaders may produce effects outside object bounds (glow, blur, distortion)
  • Rendering at screen coordinates prevents clipping at object edges
  • Only clips at display edges (acceptable)
  • Single allocation, reused across all drawables
  • Cleared per-drawable, no state leakage

User Optimization Pattern:
For performance-critical scenarios, users wrap shader content in caching Frames:

# Shader runs once, result cached until manually invalidated
cache_frame = Frame(size=(120, 120), cache_subtree=True)
sprite = Sprite(pos=(10, 10), shader=glow_shader)
cache_frame.children.append(sprite)

Dirty Flag Propagation

Critical: Shader dynamic flag must propagate through the entire UI tree.

Entity (dynamic shader)
  └─► Grid (marks dirty)
       └─► Frame with cache_subtree=True (must re-composite)
            └─► Scene (schedules redraw)

Even though Grid rebuilds its composite from cached layers each frame, if Grid is a child of a caching Frame, that parent cache is invalidated by any dynamic shader in the subtree.

Implementation:

  • UIDrawable::markDynamic() - sets flag and propagates to parent
  • UIEntity participates in dirty propagation through its parent Grid
  • Grid propagates to its parent (if any)

Shader / Uniforms Separation

mcrfpy.Shader - Compiled program (shareable, immutable)

glow = mcrfpy.Shader(fragment_source, dynamic=True)
glow.active_uniforms  # Dict of uniform names -> types (introspection)

drawable.shader - Program reference only

frame.shader = glow  # Just sets which program to use

drawable.uniforms - Per-drawable UniformCollection

frame.uniforms["intensity"] = 0.5                    # Static float
frame.uniforms["color"] = (1.0, 0.5, 0.0, 1.0)      # Static vec4
frame.uniforms["alpha"] = PropertyBinding(other, "opacity")  # C++ path
frame.uniforms["hp"] = CallableBinding(lambda: p.health/100) # Python path

a.shader = b.shader copies program reference only. Uniforms are independent per-drawable.


Binding Objects with Immediate Validation

# PropertyBinding validates at creation
binding = mcrfpy.PropertyBinding(target_drawable, "opacity")
# Raises ValueError if "opacity" not found on target_drawable

# CallableBinding validates callable
binding = mcrfpy.CallableBinding(my_func)
# Raises TypeError if my_func not callable

# Assignment validates type compatibility with shader uniform
frame.uniforms["intensity"] = binding
# Could warn if shader expects float but binding returns vec2

UniformCollection Implementation

class UniformCollection:
    """Dict-like container for shader uniform values and bindings."""
    
    def __setitem__(self, name: str, value):
        """Set uniform value or binding with validation."""
        if isinstance(value, (int, float)):
            self._static[name] = value
        elif isinstance(value, (tuple, list)) and len(value) in (2,3,4):
            self._static[name] = value  # vec2/3/4
        elif isinstance(value, Color):
            self._static[name] = value
        elif isinstance(value, PropertyBinding):
            self._property_bindings[name] = value
        elif isinstance(value, CallableBinding):
            self._callable_bindings[name] = value
        else:
            raise TypeError(f"Invalid uniform type: {type(value)}")
        
        # Warn if name matches engine-reserved uniform
        if name in ('time', 'delta_time', 'resolution', 'mouse'):
            warnings.warn(f"Uniform '{name}' overrides engine-provided value")
    
    def __getitem__(self, name: str):
        """Get current value (evaluates bindings)."""
        ...
    
    def __delitem__(self, name: str):
        """Remove uniform (reverts to engine default if applicable)."""
        ...

Render Pipeline with Shaders

void UIDrawable::renderWithShader(sf::Vector2f offset, sf::RenderTarget& target) {
    // 1. Clear shared intermediate
    shader_intermediate.clear(sf::Color::Transparent);
    
    // 2. Render self to intermediate at SCREEN position
    sf::Vector2f screen_pos = offset + position;
    render_content(shader_intermediate, screen_pos);
    shader_intermediate.display();
    
    // 3. Apply uniforms in order:
    //    a) Engine uniforms (time, resolution, etc.)
    //    b) Static uniforms from UniformCollection
    //    c) Evaluate PropertyBindings
    //    d) Evaluate CallableBindings
    apply_uniforms();
    
    // 4. Draw intermediate with shader to final target
    sf::Sprite result(shader_intermediate.getTexture());
    target.draw(result, shader.get());
}

Remaining Implementation Tasks

  1. PyShader class - src/PyShader.h/.cpp
  2. UniformCollection class - src/PyUniformCollection.h/.cpp
  3. PropertyBinding / CallableBinding - validation, storage, evaluation
  4. Shared intermediate texture - initialization, lifecycle
  5. UISprite/UICaption - adapt render() for shader path
  6. UIEntity - shader support in Grid rendering
  7. GridLayer - shader support for layer compositing
  8. Dirty propagation - dynamic flag bubbles up through Grid → Frame → Scene
  9. Engine uniforms - time, delta_time, resolution, mouse
  10. Shader introspection - glGetActiveUniform for validation
## Architecture Design - Finalized ### Shared Intermediate RenderTexture All shader-enabled drawables will use a **shared screen-resolution RenderTexture** for shader processing: ```cpp // Static in UIDrawable or GameEngine static sf::RenderTexture shader_intermediate; // Size: mcrfpy.window.game_resolution (not window size) ``` **Rationale:** - Shaders may produce effects outside object bounds (glow, blur, distortion) - Rendering at screen coordinates prevents clipping at object edges - Only clips at display edges (acceptable) - Single allocation, reused across all drawables - Cleared per-drawable, no state leakage **User Optimization Pattern:** For performance-critical scenarios, users wrap shader content in caching Frames: ```python # Shader runs once, result cached until manually invalidated cache_frame = Frame(size=(120, 120), cache_subtree=True) sprite = Sprite(pos=(10, 10), shader=glow_shader) cache_frame.children.append(sprite) ``` --- ### Dirty Flag Propagation **Critical:** Shader `dynamic` flag must propagate through the entire UI tree. ``` Entity (dynamic shader) └─► Grid (marks dirty) └─► Frame with cache_subtree=True (must re-composite) └─► Scene (schedules redraw) ``` Even though Grid rebuilds its composite from cached layers each frame, if Grid is a child of a caching Frame, that parent cache is invalidated by any dynamic shader in the subtree. **Implementation:** - `UIDrawable::markDynamic()` - sets flag and propagates to parent - `UIEntity` participates in dirty propagation through its parent Grid - Grid propagates to its parent (if any) --- ### Shader / Uniforms Separation **`mcrfpy.Shader`** - Compiled program (shareable, immutable) ```python glow = mcrfpy.Shader(fragment_source, dynamic=True) glow.active_uniforms # Dict of uniform names -> types (introspection) ``` **`drawable.shader`** - Program reference only ```python frame.shader = glow # Just sets which program to use ``` **`drawable.uniforms`** - Per-drawable `UniformCollection` ```python frame.uniforms["intensity"] = 0.5 # Static float frame.uniforms["color"] = (1.0, 0.5, 0.0, 1.0) # Static vec4 frame.uniforms["alpha"] = PropertyBinding(other, "opacity") # C++ path frame.uniforms["hp"] = CallableBinding(lambda: p.health/100) # Python path ``` **`a.shader = b.shader`** copies program reference only. Uniforms are independent per-drawable. --- ### Binding Objects with Immediate Validation ```python # PropertyBinding validates at creation binding = mcrfpy.PropertyBinding(target_drawable, "opacity") # Raises ValueError if "opacity" not found on target_drawable # CallableBinding validates callable binding = mcrfpy.CallableBinding(my_func) # Raises TypeError if my_func not callable # Assignment validates type compatibility with shader uniform frame.uniforms["intensity"] = binding # Could warn if shader expects float but binding returns vec2 ``` --- ### UniformCollection Implementation ```python class UniformCollection: """Dict-like container for shader uniform values and bindings.""" def __setitem__(self, name: str, value): """Set uniform value or binding with validation.""" if isinstance(value, (int, float)): self._static[name] = value elif isinstance(value, (tuple, list)) and len(value) in (2,3,4): self._static[name] = value # vec2/3/4 elif isinstance(value, Color): self._static[name] = value elif isinstance(value, PropertyBinding): self._property_bindings[name] = value elif isinstance(value, CallableBinding): self._callable_bindings[name] = value else: raise TypeError(f"Invalid uniform type: {type(value)}") # Warn if name matches engine-reserved uniform if name in ('time', 'delta_time', 'resolution', 'mouse'): warnings.warn(f"Uniform '{name}' overrides engine-provided value") def __getitem__(self, name: str): """Get current value (evaluates bindings).""" ... def __delitem__(self, name: str): """Remove uniform (reverts to engine default if applicable).""" ... ``` --- ### Render Pipeline with Shaders ```cpp void UIDrawable::renderWithShader(sf::Vector2f offset, sf::RenderTarget& target) { // 1. Clear shared intermediate shader_intermediate.clear(sf::Color::Transparent); // 2. Render self to intermediate at SCREEN position sf::Vector2f screen_pos = offset + position; render_content(shader_intermediate, screen_pos); shader_intermediate.display(); // 3. Apply uniforms in order: // a) Engine uniforms (time, resolution, etc.) // b) Static uniforms from UniformCollection // c) Evaluate PropertyBindings // d) Evaluate CallableBindings apply_uniforms(); // 4. Draw intermediate with shader to final target sf::Sprite result(shader_intermediate.getTexture()); target.draw(result, shader.get()); } ``` --- ### Remaining Implementation Tasks 1. **PyShader class** - `src/PyShader.h/.cpp` 2. **UniformCollection class** - `src/PyUniformCollection.h/.cpp` 3. **PropertyBinding / CallableBinding** - validation, storage, evaluation 4. **Shared intermediate texture** - initialization, lifecycle 5. **UISprite/UICaption** - adapt render() for shader path 6. **UIEntity** - shader support in Grid rendering 7. **GridLayer** - shader support for layer compositing 8. **Dirty propagation** - dynamic flag bubbles up through Grid → Frame → Scene 9. **Engine uniforms** - time, delta_time, resolution, mouse 10. **Shader introspection** - glGetActiveUniform for validation
Author
Owner
486087b9cb
john closed this issue 2026-02-07 19:15:47 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#106
No description provided.