フラグメントシェーダーとは
フラグメントシェーダーは主にGPUで画像を描画するときに使われる画素単位での色の計算を行うものです。3DCGなどでは頂点の位置を計算するためのバーテックスシェーダーの後にフラグメントシェーダーによる画素単位での画素値の計算を行うことで画像を生成しています。
Flutterでは今のところ3Dの描画機能は備わっていないのでバーテックスシェーダーは存在していませんが、フラグメントシェーダーは使用することができます。ただし、Web版では使用することが出来ないのでこの記事の例はすべてLinuxビルドのスクリーンショットとなります。もちろんAndroidやiOSでも使用可能です。
ベースとなるコード
まずは本記事でのベースとなるコードを示します。このコードの時点ではフラグメントシェーダーを使用していません。単にcanvas.drawRect
関数により矩形を描画しているだけです。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: MainView());
}
}
class MainView extends StatefulWidget {
const MainView({super.key});
@override
MainViewState createState() => MainViewState();
}
class MainViewState extends State<MainView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: CustomPaint(painter: ShaderPainter())));
}
}
class ShaderPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Offset.zero & size, Paint());
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Linuxのアプリケーションとして実行した結果は下記のようになります。
フラグメントシェーダーによる描画
ここからフラグメントシェーダーを使用した描画について見ていきます。まず、最初に示したコードの中のShaderPainterをフラグメントシェーダーを使用して描画するように書き換えます。
class ShaderPainter extends CustomPainter {
final FragmentShader shader;
ShaderPainter(this.shader);
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Offset.zero & size, Paint()..shader = shader);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
ここではメンバ変数にFragmentShader
型のshader
を追加し、コンストラクタで受け取るようにしました。また、paint
関数内のcanvas.drawRectでshader
を使用するようにしました。Paint()..shader = shader
としている部分がshader
を使用して描画するように指定しているところです。
ShaderPainter
がコンストラクタにFragmentShader
型の引数を取るようになったため、MainViewState
クラスの方でFragmentShader
を用意する必要があります。後述しますが、Flutterではアセットとしてフラグメントシェーダーをあらかじめ用意しておき、それを読み込んで使用します。その処理を行うために、MainViewState
クラスにFragmentShader
型のメンバ変数とinitState
関数を追加します。また、build
関数の中でshader
をShaderPainter
に渡すようにします。
class MainViewState extends State<MainView> {
FragmentShader? shader;
@override
void initState() {
super.initState();
FragmentProgram.fromAsset('shaders/shader1.frag').then((program) {
setState(() {
shader = program.fragmentShader();
});
});
}
@override
Widget build(BuildContext context) {
if (shader == null) {
return Container();
} else {
return Scaffold(
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: CustomPaint(painter: ShaderPainter(shader!))));
}
}
}
最後にフラグメントシェーダーのコードを用意します。まずは非常にシンプルな例として、画素値を単に赤色にするだけのものを用意しました。
out vec4 color;
void main() {
color = vec4(1.0, 0.0, 0.0, 1.0);
}
このコードはFlutterのプロジェクトルートからの相対パスでshaders/shader1.frag
に置いてあるものとします。このコードをアセットに追加するため、pubspec.yamlのflutter:
と書かれているところに下記の記述を追加します。
flutter:
# 下2行を追加
shaders:
- shaders/shader1.frag
これでコードの修正は完了です。実行してみると下記のようになります。
座標に応じた計算を行う
FlutterのフラグメントシェーダーはGLSLで記述できるので、gl_FragCoord
を使って座標に応じた計算をすることが出来ます。例えば、さきほどの例で用意したshaders/shader1.fragを次のように変更してみます。
out vec4 color;
void main() {
float n = mod(floor(gl_FragCoord.x / 100.0) + floor(gl_FragCoord.y / 100.0), 2.0);
float c = step(float(n), 0.5);
color = vec4(c, c, c, 1.0);
}
これで実行すると下記のように市松模様を描くことが出来ます。
作ってみてから思いましたが、このような規則的な図形を描く場合はdrawRect
などの用意された描画関数を使うよりもフラグメントシェーダーを使う方がコード量は少なくて済むかもしれませんね。
画像を使う
画像をテクスチャとして使用して描画することも出来ます。ここでは別記事で生成した以下の画像を使います。
この画像をぼかしつつ引き伸ばして描画することにします。
まず、この画像をShaderPainter
が受け取る必要があるので、ShaderPainter
を以下のように変更します。なお、dart:ui
に定義されているImage
がウィジェットのImage
と重複してしまうので、dart:ui
をインポートしているところをimport 'dart:ui' as ui;
と書き換えています。
class ShaderPainter extends CustomPainter {
final ui.FragmentShader shader;
final ui.Image image;
ShaderPainter(this.shader, this.image);
@override
void paint(Canvas canvas, Size size) {
shader.setImageSampler(0, image);
shader.setFloat(0, size.width);
shader.setFloat(1, size.height);
shader.setFloat(2, image.width.toDouble());
shader.setFloat(3, image.height.toDouble());
canvas.drawRect(Offset.zero & size, Paint()..shader = shader);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
新たにimage
がメンバ変数として追加されていますが、これをshader.setImageSampler(0, image)
とすることで、画像をテクスチャとしてフラグメントシェーダーに渡すことができます。また、shader.setFloat(0, size.width)
とすることで描画領域の幅をフラグメントシェーダーに渡しています。その下の行で同様に高さも渡しています。また、画像の幅と高さも渡しています。これらの数値をフラグメントシェーダーの中で使用します。
次にShaderPainter
に画像を渡すためにMainViewState
側で画像を用意します。MainViewState
クラスは以下のように書き換えました。
class MainViewState extends State<MainView> {
ui.FragmentShader? shader;
ui.Image? image;
@override
void initState() {
super.initState();
ui.FragmentProgram.fromAsset('shaders/shader1.frag').then((program) {
setState(() {
shader = program.fragmentShader();
});
});
rootBundle.load('assets/image.png').then((data) {
ui.decodeImageFromList(data.buffer.asUint8List(), (image) {
setState(() {
this.image = image;
});
});
});
}
@override
Widget build(BuildContext context) {
if (shader == null || image == null) {
return Container();
} else {
return Scaffold(
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: CustomPaint(painter: ShaderPainter(shader!, image!))));
}
}
}
フラグメントシェーダーと同様にアセットから画像を読み込んでいて、フラグメントシェーダーと画像の両方を読み込み終わったらShaderPainter
の描画を行うようになっています。アセットから画像を読み込むため、pubspec.yamlも以下のように記述を追加します。
flutter:
# 下2行を追加
shaders:
- shaders/shader1.frag
# さらに追加
assets:
- assets/image.png
もちろんFlutterのプロジェクトルートからの相対パスassets/image.pngに画像は置いておく必要があります。
最後にフラグメントシェーダーを以下のように書き換えます。
out vec4 color;
uniform sampler2D imageSampler;
uniform vec2 viewSize, samplerSize;
void main() {
vec3 rgb = vec3(0.0, 0.0, 0.0);
const int N = 10;
for(int i = -N; i <= N; i++) {
for(int j = -N; j <= N; j++) {
rgb += texture(imageSampler, gl_FragCoord.xy / viewSize + vec2(j, i) / samplerSize).xyz;
}
}
rgb /= (2.0 * N + 1.0) * (2.0 * N + 1.0);
color = vec4(rgb, 1.0);
}
ここでは、forループを使用して入力画像の41×41ピクセルの画素値を平均して画素値を計算しています。gl_FragCoord
は描画領域基準で座標が格納されていますが、texture
関数は画像に対して0.0~1.0の正規化された座標でアクセスします。そのため、gl_FragCoord
を描画領域の幅と高さで割ったものでアクセスしています。
以上のコードを実行してみると下記のようになります。
以上、ここまでで画像を使用した描画を行うことが出来ました。
まとめ
本記事では、Flutterでフラグメントシェーダーを使用した描画の方法について確認しました。フラグメントシェーダーを使うことで座標に応じて色を付ける処理や入力画像に対して画像処理した結果を描画することなどが出来ました。
コメント