Skip to content

imagefilter.shader broken when there's a rotation on ctm #179918

Description

@gaaclarke

reproduction

Details
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter_shaders/flutter_shaders.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Simple Shader Demo',
      theme: ThemeData(colorSchemeSeed: Colors.blue),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool useImageFilter = false;
  double rotation = 0.0;
  ui.Image? image;

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

  Future<void> _createImage() async {
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);
    final paint = Paint();

    // Draw a checkers pattern
    for (int i = 0; i < 2; i++) {
      for (int j = 0; j < 2; j++) {
        paint.color = (i + j) % 2 == 0 ? Colors.red : Colors.blue;
        canvas.drawRect(Rect.fromLTWH(i * 50.0, j * 50.0, 50.0, 50.0), paint);
      }
    }

    final picture = recorder.endRecording();
    final img = await picture.toImage(100, 100);
    setState(() {
      image = img;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Simple Shader Demo'),
        actions: [
          Row(
            children: [
              const Text('Use Paint.imageFilter'),
            ],
          ),
        ],
      ),
      body: image == null
          ? const Center(child: CircularProgressIndicator())
          : Column(
              children: [
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Row(
                        children: [
                          const Text('Filter:'),
                          Switch(
                            value: useImageFilter,
                            onChanged: (v) =>
                                setState(() => useImageFilter = v),
                          ),
                        ],
                      ),
                      Expanded(
                        child: Row(
                          children: [
                            const Text('Rot:'),
                            Expanded(
                              child: Slider(
                                value: rotation,
                                min: 0.0,
                                max: 6.28, // 2*PI approx
                                onChanged: (v) => setState(() => rotation = v),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
                Expanded(
                  child: ShaderBuilder(
                    assetKey: 'shaders/gradient.frag',
                    (context, shader, child) => CustomPaint(
                      size: MediaQuery.of(context).size,
                      painter: ShaderPainter(
                        shader: shader,
                        useImageFilter: useImageFilter,
                        image: image!,
                        rotation: rotation,
                      ),
                    ),
                    child: const Center(child: CircularProgressIndicator()),
                  ),
                ),
              ],
            ),
    );
  }
}

class ShaderPainter extends CustomPainter {
  ShaderPainter({
    required this.shader,
    required this.useImageFilter,
    required this.image,
    required this.rotation,
  });

  final ui.FragmentShader shader;
  final bool useImageFilter;
  final ui.Image image;
  final double rotation;

  @override
  void paint(Canvas canvas, Size size) {
    shader.setFloat(0, 100); // resolution.x
    shader.setFloat(1, 100); // resolution.y

    final paint = Paint();

    canvas.translate(50, 50);
    canvas.rotate(rotation);
    canvas.translate(-50, -50);

    if (useImageFilter) {
      // Pass the shader as an ImageFilter.
      // The content drawn (the image) becomes the input texture for the shader.
      paint.imageFilter = ui.ImageFilter.shader(shader);
      canvas.drawImage(image, Offset.zero, paint);
    } else {
      // Bind the image explicitly to the shader's sampler 0
      shader.setImageSampler(0, image);
      paint.shader = shader;
      // Draw a rect filled with the shader (which samples the image)
      canvas.drawRect(Rect.fromLTWH(0, 0, 100, 100), paint);
    }
  }

  @override
  bool shouldRepaint(covariant ShaderPainter oldDelegate) {
    return oldDelegate.shader != shader ||
        oldDelegate.useImageFilter != useImageFilter ||
        oldDelegate.image != image ||
        oldDelegate.rotation != rotation;
  }
}

gradient

#version 460 core

#include <flutter/runtime_effect.glsl>

precision mediump float;

uniform vec2 resolution;
uniform sampler2D uTexture;

out vec4 fragColor;

void main() {
  vec2 uv = FlutterFragCoord().xy / resolution;
  vec4 color = texture(uTexture, uv);
  
  // Transition to black based on X coordinate
  // 0.0 (left) = original color, 1.0 (right) = black
  vec3 finalColor = mix(color.rgb, vec3(0.0), uv.x);
  
  fragColor = vec4(finalColor, color.a);
}

simple

#version 460 core

#include <flutter/runtime_effect.glsl>

precision mediump float;

uniform vec2 resolution;
out vec4 fragColor;

vec3 flutterBlue = vec3(5, 83, 177) / 255;
vec3 flutterNavy = vec3(4, 43, 89) / 255;
vec3 flutterSky = vec3(2, 125, 253) / 255;

void main() {
  vec2 st = FlutterFragCoord().xy / resolution.xy;

  vec3 color = vec3(0.0);
  vec3 percent = vec3((st.x + st.y) / 2);

  color =
      mix(mix(flutterSky, flutterBlue, percent * 2),
          mix(flutterBlue, flutterNavy, percent * 2 - 1), step(0.5, percent));

  fragColor = vec4(color, 1);
}

observed (stable)

f6ff152

imagefilterbefore.mov

observed (main)

acdb284

imagefilterafter.mov

expected

The imagefilter effect should rotate like it does on stable, but the rect should be sized to 100,100 at all times, like after when there is no rotation.

Metadata

Metadata

Assignees

Labels

e: impellerImpeller rendering backend issues and features requeststeam-engineOwned by Engine team

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions