Flutter 绘制集录 | Shader 让绘制无限强大 - 壹

3,250 阅读4分钟

shader.png

在之前研究 opengl 时,知道 Shader 的强大,我们可以通过着色器完成很多特效。之前在 Android 中写过
《 [ - OpenGLES3.0 - ] 第三集 主线 - shader着色器与图片特效》 一文, 其中详细介绍了 OpenGLEs 的着色器。而

Flutter 本身是支持 glsl 着色器的,也就是说,你可以在全平台使用着色器 shader 实现特效。

image.png

image.png

image.png

image.png


1. 从一个颜色开始说起

先从最简单的一个颜色开始认识 shader 的使用,如下所示在屏幕中展示单一颜色。在项目中创建一个 shaders 文件夹,并创建 color.frag 着色器文件,其中输出一个 vec4 四维向量 fragColor 表示颜色的 rgba 。在 main 函数中为 fragColor 赋值即可:
注意: 需要在 pubspec.yaml 中的 flutter/shaders 节点下配置着色器文件:

image.png

---->[shaders/color.frag]----
#version 460 core

precision mediump float;
out vec4 fragColor;

vec3 blue = vec3(5, 83, 177) / 255;

void main() {
  fragColor = vec4(blue, 1);
}

shader 的使用分为三步:

  • [1]. 加载 frag 着色器文件,得到片元程序 FragmentProgram
  • [2]. 通过 FragmentProgram 获取着色器 FragmentShader,并设置给 Paint 画笔。
  • [3]. 使用画笔绘制内容。

image.png

下面是视图组件,在初始化状态时通过 _loadShader 加载着色器,并通过 CustomPaint 展示绘制内容。为画板传入 shader 对象:

---->[lib/paint/shaders/color_shader_demo.dart]----
class ColorShaderDemo extends StatefulWidget {
  const ColorShaderDemo({super.key});

  @override
  State<ColorShaderDemo> createState() => _ColorShaderDemoState();
}

class _ColorShaderDemoState extends State<ColorShaderDemo> {
  FragmentShader? shader;

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

  @override
  Widget build(BuildContext context) {
    if (shader == null) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
    return Center(
      child: CustomPaint(
        size: const Size(500 * 0.8, 658 * 0.8),
        painter: ShaderPainter(  shader: shader!  ),
      ),
    );
  }

  void _loadShader() async {
    String path = 'shaders/color.frag';
    FragmentProgram program = await FragmentProgram.fromAsset(path);
    shader = program.fragmentShader();
    setState(() {});
  }
}

在 ShaderPainter 中为画笔设置 shader 着色器即可,这样 color.frag 中的主色逻辑就会应用到画笔上:

---->[lib/paint/shaders/color_shader_demo.dart]----
class ShaderPainter extends CustomPainter {
  ShaderPainter({required this.shader});

  FragmentShader shader;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..shader = shader;
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

2. 图片纹理贴图

下面通过展示一张图片,来介绍一下如何通过 shader 展示图片。如下的着色器文件中,定义了两个参数

  • vec2 的二维向量 uSize 表示图片尺寸。
  • sampler2D 类型的 uTexture 表示图片采样数据。

其实本质上就是通过 texture 根据图片数据在纹理坐标上拾取颜色,将颜色值赋值给 fragColor 输出:

---->[shaders/image.frag]----
#version 460 core

precision mediump float;

#include <flutter/runtime_effect.glsl>

uniform vec2 uSize;
uniform sampler2D uTexture;

out vec4 fragColor;

void main() {
    vec2 coo = FlutterFragCoord().xy / uSize;
    vec4 color = texture(uTexture, coo);
    fragColor = color;
}

image.png

由于这里有两个入参,我们可以通过 shader.setFloat 设置。如下所示:

  • 0 表示 uniform 入参的第一个维度,也就是尺寸的宽度;1 表示高度。
  • setImageSampler 方法用于设置着色器的图片资源,入参是 ui.Image 图片。

在状态类中需要加载图片资源着色器资源 ,通过 ShaderPainter 的构造传入这样一张贴图就可以附着在着色器上了。

---->[lib/paint/shaders/image_shader_demo.dart]----
class ShaderPainter extends CustomPainter {
  ShaderPainter({required this.shader, required this.image});

  FragmentShader shader;
  ui.Image image;

  @override
  void paint(Canvas canvas, Size size) {
    shader.setFloat(0, size.width);
    shader.setFloat(1, size.height);
    shader.setImageSampler(0, image);
    final paint = Paint()..shader = shader;
    canvas.drawRect(
      Rect.fromLTWH(0, 0, size.width, size.height),
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

3. 图片纹理贴图的特效

可能有人会问,这有什么用? Canvas 不是一样可以绘制图片吗? 着色器的强大之处在于可以 操作像素 , 从而完成复杂的特效。如下所示,当我们得到颜色的像素之后,可以对像素进行运算再输出:

原图黑白
image.pngimage.png

下面的着色器会将灰度小于 0.5 的像素变成白色,灰度大于 0.5 的像素变成灰色:

#version 460 core

precision mediump float;

#include <flutter/runtime_effect.glsl>

uniform vec2 uSize;
uniform sampler2D uTexture;

out vec4 fragColor;
const float threshold = 0.5;//阈值
void main() {
    vec2 coo = FlutterFragCoord().xy / uSize;
    vec4 color = texture(uTexture,coo);
    
    float r = color.r;
    float g = color.g;
    float b = color.b;
    g = r * 0.3 + g * 0.59 + b * 0.11;
    g = g <= threshold ? 0.0 : 1.0;
    fragColor = vec4(g, g, g, 1.0);
}

原图圆点马赛克
image.pngimage.png

下面通过对纹理坐标的校验决定绘制颜色还是空白,从而达到圆形马赛克的效果。

---->[shaders/mask.frag]----
#version 460 core
#include <flutter/runtime_effect.glsl>

precision mediump float;

out vec4 fragColor;
uniform vec2 uSize;
uniform sampler2D uTexture;

void main() {
    float rate = uSize.x / uSize.y;
    float cellX = 2.0;
    float cellY = 2.0;
    float rowCount = 100.0;
    vec2 coo = FlutterFragCoord().xy / uSize;

    vec2 sizeFmt = vec2(rowCount, rowCount / rate);
    vec2 sizeMsk = vec2(cellX, cellY / rate);
    vec2 posFmt = vec2(coo.x * sizeFmt.x, coo.y * sizeFmt.y);
    float posMskX = floor(posFmt.x / sizeMsk.x) * sizeMsk.x;
    float posMskY = floor(posFmt.y / sizeMsk.y) * sizeMsk.y;
    vec2 posMsk = vec2(posMskX, posMskY) + 0.5 * sizeMsk;

    bool inCircle = length(posMsk - posFmt)<cellX / 2.0;
    
    vec4 result;
    if (inCircle) {
        vec2 UVMosaic = vec2(posMsk.x / sizeFmt.x, posMsk.y / sizeFmt.y);
        result = texture(uTexture, UVMosaic);
    } else {
        result = vec4(1.0, 1.0, 1.0, 0.0);
    }
    fragColor = result;
}

本篇通过几个小案例让大家对 shader 着色器的强大能力有一个简单的认识。之后还会结合图片特效信息地介绍一下着色器的用法,Flutter 有了 Shader 的支持,可谓如虎添翼。那本篇就到这里,谢谢观看~