Skip to content

ImageFilter.blur breaks ImageFilter.shader in a BackdropFilter together #170820

@timcreatedit

Description

@timcreatedit

Steps to reproduce

When trying to find a workaround for #170792 I stumbled on another roadblock.

In an Impeller build, do the following:

  1. create a BackdropFilterLayer with its filter set to ImageFilter.combined, with inner being a blur and outer being a shader
  2. When you change sigmaX and sigmaY, the size of the texture that's passed to the shader seems to change, but not in a uniform way

Check the code sample for a reliable repro.

Expected results

The red circle stays in the same position with the same size the entire time.

Actual results

See video

Code sample

Code sample
// main.dart
import 'dart:ui';

import 'package:flutter/material.dart';

void main() async {
  final shader = await FragmentProgram.fromAsset('shaders/circle.frag');

  runApp(MainApp(shader: shader));
}

class MainApp extends StatefulWidget {
  const MainApp({super.key, required this.shader});

  final FragmentProgram shader;

  @override
  State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
  late final animationController = AnimationController(
    vsync: this,
    lowerBound: 0,
    upperBound: 10,
    duration: Duration(seconds: 2),
  )..repeat(reverse: true);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ListenableBuilder(
        listenable: animationController,
        builder: (context, child) {
          return Scaffold(
            body: Stack(
              children: [
                const Center(child: FlutterLogo(size: 100)),
                Positioned.fill(
                  child: LayoutBuilder(
                    builder: (context, constraints) {
                      return BackdropFilter(
                        filter: ImageFilter.compose(
                          inner: ImageFilter.blur(
                            sigmaX: animationController.value,
                            sigmaY: animationController.value,
                            tileMode: TileMode.clamp,
                          ),
                          outer: ImageFilter.shader(
                            widget.shader.fragmentShader()
                              ..setFloat(2, 100) // uCirclePos.x (center)
                              ..setFloat(3, 100) // uCirclePos.y (center)
                              ..setFloat(4, 100.0), // uCircleRadius
                          ),
                        ),
                        child: Center(
                          child: Text(
                            animationController.value.toStringAsFixed(2),
                          ),
                        ),
                      );
                    },
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

With this fragment shader:

#version 320 es
precision mediump float;

uniform vec2 uResolution;
uniform vec2 uCirclePos;
uniform float uCircleRadius;
uniform sampler2D uTexture;

out vec4 fragColor;

#include <flutter/runtime_effect.glsl>

float sdfCircle(vec2 p, float radius) {
    return length(p) - radius;
}

void main() {
    vec2 fragCoord = FlutterFragCoord().xy;
    vec2 screenUV = fragCoord / uResolution;
    
    vec2 normalizedCoord = (fragCoord - uResolution * 0.5) / min(uResolution.x, uResolution.y);
    vec2 normalizedCirclePos = (uCirclePos - uResolution * 0.5) / min(uResolution.x, uResolution.y);
    
    float normalizedRadius = uCircleRadius / min(uResolution.x, uResolution.y);
    float distance = sdfCircle(normalizedCoord - normalizedCirclePos, normalizedRadius);
    float circleAlpha = 1.0 - smoothstep(-0.01, 0.01, distance);
    vec4 backgroundColor = texture(uTexture, screenUV);
    vec4 circleColor = vec4(1.0, 0.0, 0.0, circleAlpha);
    fragColor = mix(backgroundColor, circleColor, circleAlpha * 0.8);
}

Screenshots or Video

Screenshots / Video demonstration
CleanShot.2025-06-18.at.19.51.11.mp4

Logs

No response

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.101.0)
[✓] Connected device (2 available)
    ! Error: Browsing on the local area network for iPhone von mir. Ensure the device is
      unlocked and attached with a cable or associated with the same local area network
      as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
    ! Error: Browsing on the local area network for Tim’s iPad. Ensure the device is
      unlocked and attached with a cable or associated with the same local area network
      as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources

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