skip to content

In Flutter, how do you use a custom GLSL fragment shader in a CustomPainter, from pubspec declaration to setting uniforms each frame?

level: seniorimportance: nice to knowfreq 15%

answer

  1. flutter: shaders: in pubspec
  2. FragmentProgram.fromAsset, load once
  3. fragmentShader(), reuse per frame
  4. setFloat by declaration order
  5. FlutterFragCoord, not gl_FragCoord

basics

~20 s

List the .frag file under flutter: shaders: in pubspec.yaml, load it once with FragmentProgram.fromAsset, create a FragmentShader, set uniforms with setFloat in declaration order, and draw with Paint()..shader = shader in paint. The tool compiles the shader at build time.

solid answer

~40 s

Declare the GLSL file (`.frag`) under `flutter:` → `shaders:` in `pubspec.yaml`; the Flutter tool compiles it to each backend's format at build time and bundles it like an asset. At runtime load it once with `await FragmentProgram.fromAsset('shaders/ripple.frag')`, then call `program.fragmentShader()` to get a `FragmentShader`, and reuse that shader across frames. In `paint`, set float uniforms with `shader.setFloat(index, value)`, where indices follow the order of declaration and a `vec2` takes two consecutive indices; set images with `setImageSampler`, whose indices count separately. Draw with `canvas.drawRect(Offset.zero & size, Paint()..shader = shader)`. In GLSL, include `<flutter/runtime_effect.glsl>` and use `FlutterFragCoord()` for local coordinates. Output premultiplied colour in `0.0`–`1.0`. Drive time or other uniforms through the painter's `repaint` listenable.

code

yaml · 3 lines
yaml
flutter:
  shaders:
    - shaders/ripple.frag

go deeper

for a junior

Recall that custom shaders are .frag files declared under shaders in pubspec and loaded with FragmentProgram.fromAsset.

for a middle

Explain program versus shader, uniform indexing by declaration order with separate sampler indices, and drawing through Paint.shader.

for a senior

Load programs once, reuse FragmentShader objects, animate uniforms through the repaint listenable, and write GLSL that is portable across Impeller backends.

for a principal

Judge when a custom shader is worth its GPU cost, testing burden and team skill requirements compared with composing built-in effects.

## What a fragment shader gives you A **fragment shader** is a small GLSL program the GPU runs for every pixel of a shape you draw. Flutter lets you supply your own, which is the way to build effects no built-in widget offers: animated noise backgrounds, ripples, custom gradients, dissolves. Both Impeller and Skia support custom fragment shaders; vertex shaders are not supported. ## Step 1: declare it Shaders are declared in `pubspec.yaml` under the `flutter` section, in their own `shaders` list (not under `assets`): ```yaml flutter: shaders: - shaders/ripple.frag ``` The Flutter tool compiles each `.frag` file to the formats the backends need and generates the metadata the runtime uses to find uniforms. In debug mode, editing the shader recompiles it on hot reload or hot restart. Shaders from a package are referenced with the `packages/<name>/` prefix. ## Step 2: load it once ```dart final program = await FragmentProgram.fromAsset('shaders/ripple.frag'); final shader = program.fragmentShader(); ``` - `FragmentProgram.fromAsset` is asynchronous; call it once, for example in `initState` of the owning `State` or behind a `FutureBuilder`, not in `paint`. - A `FragmentProgram` can create many `FragmentShader` objects; each holds one set of **uniform** values. - The docs recommend **reusing** a `FragmentShader` across frames rather than creating one per frame, and precaching programs before an animation starts where Skia still compiles at runtime. ## Step 3: set uniforms and draw Uniforms are the shader's parameters, declared in GLSL with `uniform`. From Dart: 1. **Floats** (`float`, `vec2`, `vec3`, `vec4`) are set with `shader.setFloat(index, value)`. The index follows the **declaration order**, and each component of a vector takes its own index: a `vec2 uSize` declared first uses indices 0 and 1. 2. **Images** (`sampler2D`, the only sampler type) are set with `shader.setImageSampler(index, image)`. Sampler indices are counted **separately**, starting at 0, and are not included in the float indices. 3. Name-based helpers, `getUniformFloat(name, [index])` returning a `UniformFloatSlot`, avoid hand-counting indices. 4. Unset float uniforms default to `0.0`. Then draw with the shader as the paint's colour source: ```dart canvas.drawRect(Offset.zero & size, Paint()..shader = shader); ``` Most canvas calls honour `Paint.shader`; a stroked path applies it only inside the stroke, and `drawImage` ignores it. ## Writing the GLSL for Flutter - Include `<flutter/runtime_effect.glsl>` and use **`FlutterFragCoord()`** for the fragment's local position. Avoid `gl_FragCoord`, which gives screen coordinates and is only rewritten to local coordinates on Skia. - Write a single `out vec4` colour, **premultiplied**, in the range `0.0`–`1.0`, unlike Flutter's usual 0–255 unpremultiplied `Color` values. - Limits: no UBOs or SSBOs, only `sampler2D`, only the two-argument `texture`, no extra varyings, no unsigned integers or booleans. ## Animating it Time is just another uniform. Keep the elapsed time in a listenable (an animation value or a `ValueNotifier<double>`), pass it as the painter's `repaint` argument, and set it with `setFloat` inside `paint`. The shader animates with no widget rebuilds. How the time value itself is produced belongs to animation controllers. ## Beyond CustomPainter With Impeller, a shader can also be used as an image filter via `ImageFilter.shader`, applied through `ImageFiltered` or `BackdropFilter`; the engine then supplies the input image and its size as the first uniforms. This API is Impeller-only and throws on other backends. ## Debugging a blank or black output 1. Confirm the painter's `size` is not zero and that `paint` runs. 2. Temporarily write a constant colour such as `fragColor = vec4(1.0, 0.0, 0.0, 1.0);` to prove the draw call and the `Paint.shader` wiring work. 3. Output the normalised coordinate, `FlutterFragCoord().xy / uSize`, as red and green; a gradient proves `uSize` arrived at the right indices. 4. Only then re-enable the real effect, one uniform at a time. ## Pitfalls | Pitfall | Effect | |---|---| | listing the shader under `assets:` | `fromAsset` cannot find a compiled shader | | calling `fromAsset` inside `paint` | async load every frame; nothing to draw on the first frames | | off-by-one uniform index | wrong parameters, often a black or garbled output | | returning non-premultiplied colour | halos and wrong blending at edges | | using `gl_FragCoord` | output shifts or breaks under Impeller |

  • A shader declares uniform vec2 uSize; uniform sampler2D uTex; uniform float uTime;. Which setFloat index is uTime?
    Index 2. Float indices count only float components in declaration order: `uSize` takes 0 and 1, the sampler is skipped because samplers are indexed separately via `setImageSampler(0, image)`, and `uTime` follows at 2.
  • Why should the shader use FlutterFragCoord() rather than gl_FragCoord?
    `FlutterFragCoord()` returns the fragment's local coordinates consistently across backends. `gl_FragCoord` gives screen-space coordinates; the Skia path rewrites it to local coordinates, but that rewriting is not possible with Impeller, so an effect using it can shift or break on Impeller devices.

saying these in an interview costs you the question

  • Shaders are listed under flutter: assets: like images.
  • FragmentProgram.fromAsset should be called inside paint each frame.
  • Flutter supports custom vertex shaders as well as fragment shaders.
  • setFloat indices count sampler uniforms along with floats.
  • A shader's output colour should be unpremultiplied 0 to 255 values.
  • gl_FragCoord gives local coordinates on every backend.