Skip to content

[Impeller] ImageFilter.compose doesn't work with ImageFilter.shader in all cases #170973

@itspectre

Description

@itspectre

Steps to reproduce

  1. Create a project and paste the dart code below into main.dart
  2. Create the shaders folder and the one.frag file
  3. Paste the GLSL code below into one.frag
  4. Add the shader path to pubspec.yaml
  5. Run using the device with impeller

I have tested it on the main branch channel (3.33.0-1.0.pre.619) using Samsung Galaxy A51 (Android 12)

Expected results

ImageFilter.shader in ImageFilter.compose (with ImageFiltered and BackdropFilter widgets) should work for the inner and outer attributes.

Actual results

ImageFilter.shader in ImageFilter.compose only works for the outer parameter, not for the inner one.
For example, this case will work:

ImageFiltered(
  imageFilter: ImageFilter.compose(
    inner: ImageFilter.blur(sigmaX: 1.0),
    outer: ImageFilter.shader(shader),
  ),
  child: child
)

but in this case, nothing is displayed:

ImageFiltered(
  imageFilter: ImageFilter.compose(
    outer: ImageFilter.blur(sigmaX: 1.0),
    inner: ImageFilter.shader(shader),
  ),
  child: child
)

In addition, even when using Image.shader as the outer parameter with ImageFilter.matrix as the inner one, and the matrix uses scaling, the resulting texture is too small to represent the full new image (shown in the picture below).

Code sample

Code sample
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show Ticker;

enum ImageFilterMode {
  outerShaderInnerBlurWorks,
  innerShaderOuterBlurNotWorks,
  outerShaderInnerShaderNotWorks,
  outerShaderInnerMatrixWorksWithProblem,
  innerShaderOuterMatrixNotWorks,
}

void main() {
  runApp(App());
}

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(debugShowCheckedModeBanner: false, home: Home());
  }
}

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> with TickerProviderStateMixin {
  late FragmentProgram program;
  FragmentShader? shader;

  Ticker? ticker;

  ImageFilterMode mode = ImageFilterMode.outerShaderInnerBlurWorks;

  @override
  void initState() {
    super.initState();

    FragmentProgram.fromAsset('shaders/one.frag').then((program) {
      this.program = program;
      setState(() {
        shader = program.fragmentShader();
      });
      ticker = createTicker((Duration elapsed) {
        shader!.setFloat(2, elapsed.inMilliseconds.toDouble());
        setState(() {});
      })..start();
    });
  }

  ImageFilter _getImageFilter() {
    return switch (mode) {
      ImageFilterMode.outerShaderInnerBlurWorks => ImageFilter.compose(
        outer: ImageFilter.shader(shader!),
        inner: ImageFilter.blur(sigmaX: 1, sigmaY: 1),
      ),
      ImageFilterMode.innerShaderOuterBlurNotWorks => ImageFilter.compose(
        inner: ImageFilter.shader(shader!),
        outer: ImageFilter.blur(sigmaX: 1, sigmaY: 1),
      ),
      ImageFilterMode.outerShaderInnerShaderNotWorks => ImageFilter.compose(
        inner: ImageFilter.shader(shader!),
        outer: ImageFilter.shader(shader!),
      ),
      ImageFilterMode.outerShaderInnerMatrixWorksWithProblem =>
        ImageFilter.compose(
          outer: ImageFilter.shader(shader!),
          inner: ImageFilter.matrix(
            (Matrix4.identity()
                  ..translate(100.0, 50.0)
                  ..scale(0.5)
                  ..translate(-100.0, -50.0))
                .storage,
          ),
        ),
      ImageFilterMode.innerShaderOuterMatrixNotWorks => ImageFilter.compose(
        inner: ImageFilter.shader(shader!),
        outer: ImageFilter.matrix(
          (Matrix4.identity()
                ..translate(100.0, 50.0)
                ..scale(0.5)
                ..translate(-100.0, -50.0))
              .storage,
        ),
      ),
    };
  }

  @override
  void dispose() {
    ticker?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (shader == null) {
      return const SizedBox();
    }
    return SafeArea(
      child: Scaffold(
        body: Center(
          child: Column(
            spacing: 100,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              DropdownButton<ImageFilterMode>(
                items: ImageFilterMode.values
                    .map(
                      (item) =>
                          DropdownMenuItem(value: item, child: Text(item.name)),
                    )
                    .toList(),
                value: mode,
                onChanged: (value) {
                  if (value != null && value != mode) {
                    setState(() {
                      mode = value;
                    });
                  }
                },
              ),
              SizedBox(
                width: 200,
                height: 100,
                child: ColoredBox(
                  color: Colors.blue.withAlpha(50),
                  child: ImageFiltered(
                    imageFilter: _getImageFilter(),
                    child: Column(
                      spacing: 20,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(Icons.flutter_dash),
                        Text('Hello, world'),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
#include <flutter/runtime_effect.glsl>

uniform vec2 uSize;
uniform float uTime;

uniform sampler2D uTexture;

out vec4 fragColor;

void main() {
  vec2 uv = FlutterFragCoord().xy / uSize;
  #ifdef IMPELLER_TARGET_OPENGLES
    uv.y = 1.0 - uv.y;
  #endif
  fragColor = texture(uTexture, uv + vec2(0.2 * sin(uv.x + uv.y + 0.0025*uTime), 0.09 * sin(10*uv.x + 0.0025*uTime)));
  fragColor += vec4(1.0, 0.0, 0.0, 0.25);
}

Screenshots or Video

Screenshots / Video demonstration

outer - ImageFilter.shader, inner - ImageFilter.blur - no problems:
Image

outer - ImageFilter.blur, inner - ImageFilter.shader - nothing is shown:
Image

outer - ImageFilter.shader , inner - ImageFilter.shader - nothing is shown:
Image

outer - ImageFilter.shader , inner - ImageFilter.matrix - we can see a small texture that clips our widget:
Image

outer - ImageFilter.matrix, inner - ImageFilter.shader - nothing is shown:
Image

Flutter Doctor output

Doctor output
[√] Flutter (Channel main, 3.33.0-1.0.pre.619, on Microsoft Windows [Version 10.0.19045.5965], locale ru-RU)
[√] Windows Version (Њ ©Єа®б®дв Windows 10 Pro 64-а §ап¤­ п, 22H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Build Tools 2022 17.6.5)
[√] Android Studio (version 2022.2)
[√] VS Code (version 1.101.1)
[√] Connected device (4 available)
[√] Network resources

• No issues found!

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work liste: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.found in release: 3.32Found to occur in 3.32found in release: 3.33Found to occur in 3.33has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions