Shaders & Effects

Ply lets you apply fragment shaders to individual elements or groups of elements. Write standard GLSL ES 1.00 (compatible with both WebGL 1 and 2), pass uniforms, and get per-element post-processing without boilerplate.

🔗Per-element effects

Use .effect() to apply a shader to a single element. The shader receives the element's rendered content as sampler2D Texture:

const TINT: ShaderAsset = ShaderAsset::Path("shaders/tint.frag");

ui.element()
  .width(fixed!(200.0))
  .height(fixed!(100.0))
  .background_color(0xE8E0DC)
  .effect(&TINT, |s| s
    .uniform("u_time", get_time() as f32)
    .uniform("u_tint", [1.0f32, 0.3, 0.2, 0.5])
  )
  .empty();

Every shader automatically gets two uniforms:

  • u_resolution: element width and height in pixels (vec2)
  • u_position: element position in screen space (vec2)

🔗Group shaders

Use .shader() to capture an element and all its children into an offscreen buffer, then apply the shader as post-processing:

ui.element()
  .shader(&FOIL, |s| s
    .uniform("u_time", get_time() as f32)
    .uniform("u_speed", 1.5f32)
  )
  .children(|ui| {
    ui.text("Shiny card", |t| t.font_size(18).color(0xFFFFFF));
    ui.element()
      .width(grow!())
      .height(fixed!(60.0))
      .background_color(0x3A3533)
      .empty();
  });

Multiple .shader() calls nest, the first is innermost (closest to the content), later ones wrap previous:

ui.element()
  .shader(&BLUR_SHADER, |s| s.uniform("u_radius", 4.0f32))
  .shader(&COLOR_GRADE, |s| s.uniform("u_contrast", 1.2f32))
  .children(|ui| { /* ... */ });

🔗ShaderAsset

Three ways to provide shader source:

VariantUse case
ShaderAsset::Path("shaders/fx.frag")File on disk
ShaderAsset::Source { file_name, fragment }Embedded in binary
ShaderAsset::Stored("name")Runtime-updateable

🔗Path

const WAVE: ShaderAsset = ShaderAsset::Path("shaders/wave.frag");

Reads the file each time the material is created.

🔗Source

const WAVE: ShaderAsset = ShaderAsset::Source {
    file_name: "wave",
    fragment: include_str!("../shaders/wave.frag"),
};

Baked into the binary. No file fetch at runtime.

🔗Stored (live-editable)

use ply_engine::renderer::set_shader_source;

// Update the source at any time:
set_shader_source("live_shader", &editor_text);

// Reference it:
const LIVE: ShaderAsset = ShaderAsset::Stored("live_shader");

ui.element()
  .effect(&LIVE, |s| s.uniform("u_time", get_time() as f32))
  .empty();

When the source changes, the old compiled material is evicted automatically. The next render pass recompiles.

🔗Uniform types

.uniform() accepts anything that implements Into<ShaderUniformValue>:

Rust typeGLSL type
f32float
[f32; 2]vec2
[f32; 3]vec3
[f32; 4]vec4
i32int
[[f32; 4]; 4]mat4

🔗Built-in shaders

Enable with the built-in-shaders feature:

[dependencies]
ply-engine = { version = "1.0", features = ["built-in-shaders"] }

Or add it with the CLI:

plyx add built-in-shaders

Built-in shaders are automatically included in the prelude.

🔗FOIL

.effect(&FOIL, |s| s
  .uniform("u_time", get_time() as f32)
  .uniform("u_speed", 1.0f32)
  .uniform("u_intensity", 0.3f32)
)

🔗HOLOGRAPHIC

.effect(&HOLOGRAPHIC, |s| s
  .uniform("u_time", get_time() as f32)
  .uniform("u_speed", 1.0f32)
  .uniform("u_saturation", 0.7f32)
)

🔗DISSOLVE

.effect(&DISSOLVE, |s| s
  .uniform("u_threshold", progress)
  .uniform("u_edge_color", [1.0f32, 0.5, 0.0, 1.0])
  .uniform("u_edge_width", 0.05f32)
  .uniform("u_seed", 42.0f32)
)

🔗GLOW

.effect(&GLOW, |s| s
  .uniform("u_glow_color", [0.0f32, 0.5, 1.0, 1.0])
  .uniform("u_glow_radius", 0.05f32)
  .uniform("u_glow_intensity", 1.0f32)
)

🔗CRT

.effect(&CRT, |s| s
  .uniform("u_line_count", 100.0f32)
  .uniform("u_intensity", 0.3f32)
  .uniform("u_time", get_time() as f32)
)

🔗GRADIENT_LINEAR

.effect(&GRADIENT_LINEAR, |s| s
  .uniform("u_color_a", [0.73f32, 0.08, 0.08, 1.0])
  .uniform("u_color_b", [1.0f32, 0.76, 0.17, 1.0])
  .uniform("u_angle", 0.0f32)
)

🔗GRADIENT_RADIAL

.effect(&GRADIENT_RADIAL, |s| s
  .uniform("u_color_a", [1.0f32, 0.4, 0.3, 1.0])
  .uniform("u_color_b", [0.15f32, 0.13, 0.12, 1.0])
  .uniform("u_center", [0.5f32, 0.5])
  .uniform("u_radius", 0.5f32)
)

🔗GRADIENT_CONIC

.effect(&GRADIENT_CONIC, |s| s
  .uniform("u_color_a", [1.0f32, 0.76, 0.17, 1.0])
  .uniform("u_color_b", [0.73f32, 0.08, 0.08, 1.0])
  .uniform("u_center", [0.5f32, 0.5])
  .uniform("u_offset", 0.0f32)
  .uniform("u_hardness", 0.0f32)
)

🔗Writing a shader

Shaders are GLSL ES 1.00 fragment shaders. Ply's default vertex shader is #version 100, so your fragment shaders must also be #version 100. The shaded content is available as sampler2D Texture and the UV coordinates come from the bounding box:

#version 100
precision highp float;

varying lowp vec2 uv;

uniform sampler2D Texture;
uniform vec2 u_resolution;
uniform vec2 u_position;
uniform float u_time;

void main() {
    vec4 color = texture2D(Texture, uv);
    // Tint red based on time
    color.r = mix(color.r, 1.0, sin(u_time) * 0.5 + 0.5);
    gl_FragColor = color;
}

🔗Shader build pipeline

For larger projects, use the shader-build feature to compile .frag and .slang files via a build.rs script:

[build-dependencies]
ply-engine = { version = "1.0", default_features = false, features = ["shader-build"] }
use ply_engine::shader_build::ShaderBuild;

fn main() {
  ShaderBuild::new()
    .source_dir("shaders/")
    .output_dir("assets/build/shaders/")
    .build();
}

Or initialize it with the CLI:

plyx add shader-pipeline

This compiles all .hlsl and .slang files to GLSL ES 1.00, with hash-based incremental rebuilds. Files go through SPIR-V cross-compilation.

Custom file types can be handled too:

ShaderBuild::new()
  .override_file_type_handler(".wgsl", |file_path, output_dir| {
    // Custom compilation logic
    vec!["shaders/includes/**/*.wgsl".to_string()]
  })
  .build();

🔗Next steps

Rotation