Flutter自定义渲染插件shader的使用

发布于 1周前 作者 gougou168 来自 Flutter

Flutter自定义渲染插件shader的使用

shader

shader 管理将您的 GLSL shaders 编译为 SPIR-V 字节码和 Dart 代码。

快速开始

# 安装命令行工具
dart pub global activate shader

# 编译项目中的所有 glsl 文件
shader --use-remote --to-dart

# 查看所有功能
shader --help

使用方法

编译到Dart

最简单的使用着色器的方法如下:

shader --use-remote --to-dart

它会扫描项目中的 *.glsl 文件并使用托管的云服务进行编译。Flutter 在运行时需要 SPIR-V 字节码

一个非常简单的着色器 red-shader.glsl 可以是这样的:

#version 320 es

precision highp float;

layout(location = 0) out vec4 fragColor;

void main() {
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

这个着色器没有 uniforms(输入参数),并且将每个像素绘制为红色。

编译器创建了一个 Dart 文件 red_shader_sprv.dart,其中包含一个函数 Future<FragmentProgram> redShaderFragmentProgram(),在运行时初始化着色器。

我们可以利用 FutureBuilder 来加载它:

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

/// 导入由 CLI 工具生成的文件
import 'package:flutter_app/shader/red_shader_sprv.dart';

class RedShaderWidget extends StatelessWidget {
  const RedShaderWidget({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return FutureBuilder<FragmentProgram>(
      /// 使用生成的加载器函数
      future: redShaderFragmentProgram(),
      builder: ((context, snapshot) {
        if (!snapshot.hasData) {
          /// 着色器正在加载
          return const CircularProgressIndicator(); // 替换为实际的加载组件
        }

        /// 着色器已准备好使用
        return CustomPaint(
          painter: RedShaderPainter(snapshot.data!),
        );
      }),
    );
  }
}

/// 自定义画笔类,使用着色器
class RedShaderPainter extends CustomPainter {
  RedShaderPainter(this.fragmentProgram);

  final FragmentProgram fragmentProgram;

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    /// 创建带有着色器的画笔
    final paint = Paint()..shader = fragmentProgram.shader();

    /// 使用带有着色器的画笔绘制矩形
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
  }

  [@override](/user/override)
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    if (oldDelegate is RedShaderPainter &&
        oldDelegate.fragmentProgram == fragmentProgram) {
      /// 当画笔具有相同的属性集时不重绘
      return false;
    }
    return true;
  }
}
Uniforms 的使用

Uniforms 是 GLSL 中的术语,表示输入参数。Uniforms 可以在每一帧中更改,但在单个帧中对于每个像素来说是常量。

给定以下 GLSL 文件:

#version 320 es

precision highp float;

layout(location = 0) out vec4 fragColor;

// 定义 uniforms:
layout(location = 0) uniform vec3 color1;
layout(location = 1) uniform vec3 color2;
layout(location = 2) uniform float someValue;
layout(location = 3) uniform vec2 size;

void main() {
    // ...
}

这可以在 FragmentProgramshader() 方法中处理:

[@override](/user/override)
void paint(Canvas canvas, Size size) {
  /// 输入参数
  Color color1 = Colors.blue;
  Color color2 = Colors.green;
  double someValue = 0.5;

  /// 创建带有着色器的画笔
  final paint = Paint()
    ..shader = fragmentProgram.shader(

        /// 指定输入参数 (uniforms)
        floatUniforms: Float32List.fromList([
      /// color1 需要 3 个浮点数,并将映射到 `vec3`
      color1.red / 255.0,
      color1.green / 255.0,
      color1.blue / 255.0,

      /// color2 同样需要 3 个浮点数,并将映射到 `vec3`
      color2.red / 255.0,
      color2.green / 255.0,
      color2.blue / 255.0,

      /// someValue 需要 1 个浮点数,并将映射到 `float`
      someValue,

      /// size 需要 2 个浮点数,并将映射到 `vec2`
      size.width,
      size.height,
    ]));

  /// 使用带有着色器的画笔绘制矩形
  canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
}
使用 sampler uniform

图像纹理可以通过 sampler2d uniform 访问:

layout(location = 0) uniform sampler2D image;

为此,您需要创建一个 ImageShader

final asset = await rootBundle.load("assets/image.jpg");
final image = await decodeImageFromList(asset.buffer.asUint8List());

/// 创建 ImageShader 提供 GLSL sampler
final ImageShader imageShader = ImageShader(
  image,
  // 指定 x 和 y 维度的图像重复处理方式
  TileMode.repeated,
  TileMode.repeated,
  // 变换矩阵(单位矩阵 = 无变换)
  Matrix4.identity().storage,
);

ImageShader 可以作为 samplerUniform 传递给 FragmentProgramshader() 方法:

final paint = Paint()
  ..shader = fragmentProgram.shader(
    samplerUniforms: [
      imageShader,
    ],
  );
使用本地编译器

如果您不想依赖托管的云服务或云服务不可用,可以使用本地编译器。

您可以从 这里 下载编译器。

--use-local 选项接受指向编译器二进制文件的路径:

shader --use-local $HOME/sdk/shaderc --to-dart
改进开发周期

为了更快地迭代并使用热重载,可以使用 --watch 标志:

shader --use-remote --to-dart --watch

shader --use-local $HOME/sdk/shaderc --to-dart --watch

编写着色器

Flutter 中的约束条件

着色器目前不支持 Flutter Web,但 Flutter 引擎开发人员有一个 计划 来启用它。

GLSL 语言功能的能力也受到限制。请查看 SPIR-V 转换器规范

此包将 GLSL 代码编译为 SPIR-V 代码,在运行时 SPIR-V 转换器将其转换为本机 API(例如 OpenGL、Vulkan)。因此,可能 shader 编译成功了,但在运行时失败。

学习 GLSL

有许多资源可以学习 GLSL:

示例代码

红色着色器

red-shader.glsl 非常简单,将所有像素变为红色:

#version 320 es

precision highp float;

layout(location = 0) out vec4 fragColor;

void main() {
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

可以通过 shader 执行程序编译:

shader --use-remote --to-dart

编译后应用程序可以使用它:

import 'dart:ui';

import 'package:flutter/material.dart';

/// 导入由 CLI 工具生成的文件
import 'package:flutter_app/shader/red_shader_sprv.dart';

void main() {
  runApp(const MaterialApp(home: Page()));
}

class Page extends StatelessWidget {
  const Page({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<FragmentProgram>(
          /// 使用生成的加载器函数
          future: redShaderFragmentProgram(),
          builder: ((context, snapshot) {
            if (!snapshot.hasData) {
              /// 着色器正在加载
              return const CircularProgressIndicator();
            }

            /// 着色器已准备好使用
            return SizedBox.expand(
              child: CustomPaint(
                painter: RedShaderPainter(snapshot.data!),
              ),
            );
          })),
    );
  }
}

/// 自定义画笔类,使用着色器
class RedShaderPainter extends CustomPainter {
  RedShaderPainter(this.fragmentProgram);

  final FragmentProgram fragmentProgram;

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    /// 创建带有着色器的画笔
    final paint = Paint()..shader = fragmentProgram.shader();

    /// 使用带有着色器的画笔绘制矩形
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
  }

  [@override](/user/override)
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    if (oldDelegate is RedShaderPainter &&
        oldDelegate.fragmentProgram == fragmentProgram) {
      /// 当画笔具有相同的属性集时不重绘
      return false;
    }
    return true;
  }
}
颜色着色器

color-shader.glsl 使用输入参数(uniforms)并将所有像素变为该颜色:

#version 320 es

precision highp float;

layout(location = 0) out vec4 fragColor;

layout(location = 0) uniform vec3 color;

void main() {
    fragColor = vec4(color.rgb, 1.0);
}

可以通过 shader 执行程序编译:

shader --use-remote --to-dart

编译后应用程序可以使用它:

import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/material.dart';

/// 导入由 CLI 工具生成的文件
import 'package:flutter_app/shader/color_shader_sprv.dart';

void main() {
  runApp(const MaterialApp(home: Page()));
}

class Page extends StatelessWidget {
  const Page({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<FragmentProgram>(
          /// 使用生成的加载器函数
          future: colorShaderFragmentProgram(),
          builder: ((context, snapshot) {
            if (!snapshot.hasData) {
              /// 着色器正在加载
              return const CircularProgressIndicator();
            }

            /// 着色器已准备好使用
            return TweenAnimationBuilder<Color?>(
              /// Flutter 动画:在 10 秒内渐变颜色
              tween: ColorTween(begin: Colors.green, end: Colors.blue),
              duration: const Duration(seconds: 10),
              builder: (context, color, _) {
                return SizedBox.expand(
                  child: CustomPaint(
                    /// 将颜色作为输入参数传递到着色器中
                    painter: RedShaderPainter(snapshot.data!, color!),
                  ),
                );
              },
            );
          })),
    );
  }
}

/// 自定义画笔类,使用着色器
class RedShaderPainter extends CustomPainter {
  RedShaderPainter(this.fragmentProgram, this.color);

  final FragmentProgram fragmentProgram;
  final Color color;

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    /// 创建带有着色器的画笔
    final paint = Paint()
      ..shader = fragmentProgram.shader(

          /// 指定输入参数 (uniforms)
          floatUniforms: Float32List.fromList([
        // 设置第一个浮点数为相对红色
        color.red / 255.0,
        // 设置第二个浮点数为相对绿色
        color.green / 255.0,
        // 设置第三个浮点数为相对蓝色
        color.blue / 255.0,
      ]));

    /// 使用带有着色器的画笔绘制矩形
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
  }

  [@override](/user/override)
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    if (oldDelegate is RedShaderPainter &&
        oldDelegate.fragmentProgram == fragmentProgram &&
        oldDelegate.color == color) {
      /// 当画笔具有相同的属性集时不重绘
      return false;
    }
    return true;
  }
}
图像缩放着色器

image-scale-shader.glsl 使用图像纹理作为采样器均匀变量以及一个浮点均匀变量:

#version 320 es

precision highp float;

layout(location = 0) out vec4 fragColor;

layout(location = 0) uniform float scale;
layout(location = 1) uniform sampler2D image;

void main() {
  vec2 coords = (0.0015 / scale) * (gl_FragCoord.xy);
  vec4 textureColor = texture(image, coords);
  fragColor = textureColor;
}

可以通过 shader 执行程序编译:

shader --use-remote --to-dart

编译后应用程序可以使用它:

import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_app/shader/image_scale_shader_sprv.dart';

void main() {
  runApp(const MaterialApp(home: Page()));
}

/// 将多个内容组合在一起
class PainterNeeds {
  final ImageShader imageShader;
  final FragmentProgram fragmentProgram;

  PainterNeeds(this.imageShader, this.fragmentProgram);
}

/// 加载 JPEG 图像和 [FragmentProgram]
Future<PainterNeeds> loadPainterNeeds() async {
  final asset = await rootBundle.load("assets/image.jpg");
  final image = await decodeImageFromList(asset.buffer.asUint8List());

  /// 创建 ImageShader 提供 GLSL sampler
  final ImageShader imageShader = ImageShader(
    image,
    // 指定 x 和 y 维度的图像重复处理方式
    TileMode.repeated,
    TileMode.repeated,
    // 变换矩阵(单位矩阵 = 无变换)
    Matrix4.identity().storage,
  );

  return PainterNeeds(imageShader, await imageScaleShaderFragmentProgram());
}

class Page extends StatelessWidget {
  const Page({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<PainterNeeds>(
          /// 使用生成的加载器函数
          future: loadPainterNeeds(),
          builder: ((context, snapshot) {
            if (!snapshot.hasData) {
              /// 着色器正在加载
              return const CircularProgressIndicator();
            }

            /// 着色器已准备好使用
            return SizedBox.expand(
              child: CustomPaint(
                painter: ImageScaleShaderPainter(snapshot.data!),
              ),
            );
          })),
    );
  }
}

/// 自定义画笔类,使用着色器
class ImageScaleShaderPainter extends CustomPainter {
  ImageScaleShaderPainter(this.painterNeeds);

  final PainterNeeds painterNeeds;

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    /// 创建带有着色器的画笔
    final paint = Paint()
      ..shader = painterNeeds.fragmentProgram.shader(
        floatUniforms: Float32List.fromList([
          // scale 均匀变量
          0.1,
        ]),
        samplerUniforms: [
          painterNeeds.imageShader,
        ],
      );

    /// 使用带有着色器的画笔绘制矩形
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
  }

  [@override](/user/override)
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    if (oldDelegate is ImageScaleShaderPainter &&
        oldDelegate.painterNeeds == painterNeeds) {
      /// 当画笔具有相同的属性集时不重绘
      return false;
    }
    return true;
  }
}

更多关于Flutter自定义渲染插件shader的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter自定义渲染插件shader的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,自定义渲染插件通常涉及到使用平台通道(Platform Channels)与原生代码(iOS/Android)进行交互,以及利用Flutter的渲染引擎来创建自定义的渲染效果。对于使用Shader来自定义渲染效果,通常需要在Dart代码中创建自定义的CustomPainter,并在其中使用ui.Shader来定义渲染逻辑。

下面是一个简单的示例,展示了如何在Flutter中使用自定义Shader来实现一个基本的渐变效果。这个示例不会涉及到平台通道,而是直接在Dart代码中实现。

Dart 代码

首先,我们定义一个自定义的CustomPainter,在其中使用ui.Gradientui.LinearGradient来创建一个线性渐变效果,并将其应用到Canvas上。

import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class GradientShaderPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()
      ..shader = ui.LinearGradient(
        colors: [Colors.blue, Colors.red],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      );

    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
  }

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Gradient Shader Example'),
        ),
        body: CustomPaint(
          painter: GradientShaderPainter(),
          size: Size.infinite,
        ),
      ),
    );
  }
}

解释

  1. GradientShaderPainter

    • 继承自CustomPainter
    • paint方法中,我们创建了一个Paint对象,并使用ui.LinearGradient为其设置了一个线性渐变Shader。
    • ui.LinearGradient接受一个颜色列表colors,以及渐变的起始和结束点beginend
    • 使用canvas.drawRect方法将渐变效果绘制到整个画布上。
  2. shouldRepaint 方法

    • 返回false,因为我们不需要在Widget状态改变时重绘这个自定义Painter。
  3. MyApp

    • 创建一个简单的Flutter应用,包含一个Scaffold和一个CustomPaint组件。
    • CustomPaint组件使用我们自定义的GradientShaderPainter来绘制内容。

注意事项

  • 在这个例子中,我们直接在Dart代码中使用了Flutter提供的ui.Shader。如果你需要更复杂的自定义渲染逻辑,可能需要结合原生平台代码(如使用OpenGL ES在Android或iOS上绘制),这通常涉及到平台通道的使用。
  • 对于更高级的自定义渲染需求,例如3D渲染或复杂的图形处理,可以考虑使用Flutter的impeller渲染引擎(如果可用)或者与第三方图形库集成。

这个示例展示了如何在Flutter中使用自定义Shader进行基本的渲染。对于更复杂的需求,你可能需要深入研究Flutter的渲染引擎和平台通道的使用。

回到顶部