Всем привет! На связи Юрий Петров, Flutter Team Lead в Friflex. В предыдущей статье мы познакомились с работой шейдеров во Flutter, а также рассмотрели, как написать свой собственный шейдер на языке GLSL. В этой части разберемся, как импортировать готовые шейдеры и управлять ими из Flutter.
![](https://habrastorage.org/getpro/habr/upload_files/598/db4/6fa/598db46fad7ab2466060c71dae95e1dd.gif)
Содержание:
Использование шейдеров во Flutter. Часть 2
Если, вы еще не читали первую часть, рекомендую сначала ознакомиться с ней, а потом перейти ко второй части.
Шейдеры — это маленькие программы, написанные на языке GLSL. Соответственно, существуют ресурсы, где пользователи делятся уже готовыми шейдерами. Один из них —https://glslsandbox.com/. В данном хранилище можно прямо в браузере пробовать изменять код шейдера. Это очень удобно для отладки шейдера. В последнее время на этом ресурсе есть некоторые проблемы с модерацией контента. Но нас интересует очень красивый шейдер https://glslsandbox.com/e#94097.0 — полет в облаках.
Шейдер опубликованный на сайте https://glslsandbox.com/
![](https://habrastorage.org/getpro/habr/upload_files/d08/357/598/d08357598585f9d7fa08927033067ebe.png)
При просмотре данного шейдера с помощью кнопки Show/Hide Code мы можем показать/скрыть исходный код. Именно этот исходный код нам нужен.
Если такую анимацию попробовать реализовать стандартными средствами Flutter, то это будет очень сложная задача. А вот написать такой шейдер на GLSL не составит большого труда. Если, конечно, у вас есть практика написания шейдеров на GLSL.
Импортирование шейдера
Копируем исходный код со страницы шейдера и вставляем в наш ранее написанный файл shader.glsl.
Исходный код шейдера
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
#define iTime time
#define iMouse mouse
#define iResolution resolution
// referred https://www.shadertoy.com/view/4sXGRM
vec3 skytop = vec3(0.05, 0.2, 0.5);
vec3 light = normalize(vec3(0.1, 0.25, 0.9));
vec2 cloudrange = vec2(0.0, 10000.0);
mat3 m = mat3(0.00, 1.60, 1.20, -1.60, 0.72, -0.96, -1.20, -0.96, 1.28);
// hash function
float hash(float n)
{
return fract(cos(n) * 114514.1919);
}
// 3d noise function
float noise(in vec3 x)
{
vec3 p = floor(x);
vec3 f = smoothstep(0.0, 1.0, fract(x));
float n = p.x + p.y * 10.0 + p.z * 100.0;
return mix(
mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
mix(hash(n + 10.0), hash(n + 11.0), f.x), f.y),
mix(mix(hash(n + 100.0), hash(n + 101.0), f.x),
mix(hash(n + 110.0), hash(n + 111.0), f.x), f.y), f.z);
}
// Fractional Brownian motion
float fbm(vec3 p)
{
float f = 0.5000 * noise(p);
p = m * p;
f += 0.2500 * noise(p);
p = m * p;
f += 0.1666 * noise(p);
p = m * p;
f += 0.0834 * noise(p);
return f;
}
vec3 camera(float time)
{
return vec3(5000.0 * sin(1.0 * time), 5000. + 1500. * sin(0.5 * time), 6000.0 * time);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
vec2 uv = 2. * fragCoord.xy / iResolution.xy - 1.0;
uv.x *= iResolution.x / iResolution.y;
float time = (iTime + 13.5 + 44.) * 1.0;
vec3 campos = camera(time);
vec3 camtar = camera(time + 0.4);
vec3 front = normalize(camtar - campos);
vec3 right = normalize(cross(front, vec3(0.0, 1.0, 0.0)));
vec3 up = normalize(cross(right, front));
vec3 fragAt = normalize(uv.x * right + uv.y * up + front);
// clouds
vec4 sum = vec4(0, 0, 0, 0);
for (float depth = 0.0; depth < 100000.0; depth += 200.0)
{
vec3 ray = campos + fragAt * depth;
if (cloudrange.x < ray.y && ray.y < cloudrange.y)
{
float alpha = smoothstep(0.5, 1.0, fbm(ray * 0.00025));
vec3 localcolor = mix(vec3(1.1, 1.05, 1.0), vec3(0.3, 0.3, 0.2), alpha);
alpha = (1.0 - sum.a) * alpha;
sum += vec4(localcolor * alpha, alpha);
}
}
float alpha = smoothstep(0.7, 1.0, sum.a);
sum.rgb /= sum.a + 0.0001;
float sundot = clamp(dot(fragAt, light), 0.0, 1.0);
vec3 col = 0.8 * (skytop);
col += 0.47 * vec3(1.6, 1.4, 1.0) * pow(sundot, 350.0);
col += 0.4 * vec3(0.8, 0.9, 1.0) * pow(sundot, 2.0);
sum.rgb -= 0.6 * vec3(0.8, 0.75, 0.7) * pow(sundot, 13.0) * alpha;
sum.rgb += 0.2 * vec3(1.3, 1.2, 1.0) * pow(sundot, 5.0) * (1.0 - alpha);
col = mix(col, sum.rgb, sum.a);
fragColor = vec4(col, 1.0);
}
void main( void ) {
mainImage(gl_FragColor,gl_FragCoord.xy);
}
Теперь нам необходимо произвести небольшую адаптацию данного шейдера для использования в Flutter. При компиляции шейдера, скорее всего, вы получите ошибку данного типа.
![Это означает, что при компиляции GLSL в SPIRV произошла ошибка. Это означает, что при компиляции GLSL в SPIRV произошла ошибка.](https://habrastorage.org/getpro/habr/upload_files/69d/e6b/e34/69de6be3489d4f3e33414d13065d88d8.png)
Давайте попробуем это исправить. Для этого произведем несколько манипуляций с кодом.
В строке 62 необходимо изменить точку входа в шейдер:
Изменяем:
void mainImage(out vec4 fragColor, in vec2 fragCoord)
на:
void main()
Таким образом, мы убираем вспомогательную функцию mainImage() и делаем из нее точку входа в шейдер.
В строке 64, так как теперь мы не получаем аргумент fragCoord, необходимо использовать gl_FragCoord.
Меняем:
vec2 uv = 2. * fragCoord.xy / iResolution.xy - 1.0;
на:
vec2 uv = 2. * gl_FragCoord.xy / iResolution.xy - 1.0;
Так как мы уже объявили точку входа в шейдер, то весь код, начиная со строки 107, нужно удалить:
void main( void ) {
mainImage(gl_FragColor,gl_FragCoord.xy);
}
Далее нам необходимо добавить выходную переменную, которая будет, как вектор из четырех чисел + альфа канал. Эту переменную мы будем возвращать как результат работы шейдера во фреймворк Flutter.
out vec4 fragColor;
Уменьшаем глубину отрисовки облаков в строке 78.
Меняем:
for (float depth = 0.0; depth < 100000.0; depth += 200.0)
на:
for (float depth = 0.0; depth < 10000.0; depth += 200.0)
Последний штрих — удалить переменную mouse. Удаляем полностью шестую строку.
В итоге у нас должен получиться вот такой исходный код.
Адаптированный исходный код шейдера
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform float time;
uniform vec2 resolution;
out vec4 fragColor;
#define iTime time
#define iMouse mouse
#define iResolution resolution
// referred https://www.shadertoy.com/view/4sXGRM
vec3 skytop = vec3(0.05, 0.2, 0.5);
vec3 light = normalize(vec3(0.1, 0.25, 0.9));
vec2 cloudrange = vec2(0.0, 10000.0);
mat3 m = mat3(0.00, 1.60, 1.20, -1.60, 0.72, -0.96, -1.20, -0.96, 1.28);
// hash function
float hash(float n)
{
return fract(cos(n) * 114514.1919);
}
// 3d noise function
float noise(in vec3 x)
{
vec3 p = floor(x);
vec3 f = smoothstep(0.0, 1.0, fract(x));
float n = p.x + p.y * 10.0 + p.z * 100.0;
return mix(
mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
mix(hash(n + 10.0), hash(n + 11.0), f.x), f.y),
mix(mix(hash(n + 100.0), hash(n + 101.0), f.x),
mix(hash(n + 110.0), hash(n + 111.0), f.x), f.y), f.z);
}
// Fractional Brownian motion
float fbm(vec3 p)
{
float f = 0.5000 * noise(p);
p = m * p;
f += 0.2500 * noise(p);
p = m * p;
f += 0.1666 * noise(p);
p = m * p;
f += 0.0834 * noise(p);
return f;
}
vec3 camera(float time)
{
return vec3(5000.0 * sin(1.0 * time), 5000. + 1500. * sin(0.5 * time), 6000.0 * time);
}
void main()
{
vec2 uv = 2. * gl_FragCoord.xy / iResolution.xy - 1.0;
uv.x *= iResolution.x / iResolution.y;
float time = (iTime + 13.5 + 44.) * 1.0;
vec3 campos = camera(time);
vec3 camtar = camera(time + 0.4);
vec3 front = normalize(camtar - campos);
vec3 right = normalize(cross(front, vec3(0.0, 1.0, 0.0)));
vec3 up = normalize(cross(right, front));
vec3 fragAt = normalize(uv.x * right + uv.y * up + front);
// clouds
vec4 sum = vec4(0, 0, 0, 0);
for (float depth = 0.0; depth < 10000.0; depth += 200.0)
{
vec3 ray = campos + fragAt * depth;
if (cloudrange.x < ray.y && ray.y < cloudrange.y)
{
float alpha = smoothstep(0.5, 1.0, fbm(ray * 0.00025));
vec3 localcolor = mix(vec3(1.1, 1.05, 1.0), vec3(0.3, 0.3, 0.2), alpha);
alpha = (1.0 - sum.a) * alpha;
sum += vec4(localcolor * alpha, alpha);
}
}
float alpha = smoothstep(0.7, 1.0, sum.a);
sum.rgb /= sum.a + 0.0001;
float sundot = clamp(dot(fragAt, light), 0.0, 1.0);
vec3 col = 0.8 * (skytop);
col += 0.47 * vec3(1.6, 1.4, 1.0) * pow(sundot, 350.0);
col += 0.4 * vec3(0.8, 0.9, 1.0) * pow(sundot, 2.0);
sum.rgb -= 0.6 * vec3(0.8, 0.75, 0.7) * pow(sundot, 13.0) * alpha;
sum.rgb += 0.2 * vec3(1.3, 1.2, 1.0) * pow(sundot, 5.0) * (1.0 - alpha);
col = mix(col, sum.rgb, sum.a);
fragColor = vec4(col, 1.0);
}
Запускаем проект и любуемся полетом в облаках на вашем смартфоне. А что если вы хотите управлять шейдерами из Flutter?
Результат работы шейдера
Управляем шейдером из Flutter
Для удобного управления шейдером, реализуем следующее:
Добавим в класс _MyAppState две переменные, их мы будем передавать в шейдер.
var _move = 0.0;
var _stop = 0.0;
Добавим две кнопки. Одна кнопка будет менять направление полета, а другая — останавливать работу шейдера. Но, по сути, они будут менять состояние переменных _move и _stop.
Добавили две кнопки в метод build
return Stack(
children: [
CustomPaint(painter: AnimRect(shader)),
Align(
alignment: Alignment.bottomLeft,
child: FloatingActionButton(
onPressed: () {
if (_move == 1) {
_move = 0.0;
} else {
_move = 1;
}
setState(() {});
},
),
),
Align(
alignment: Alignment.bottomRight,
child: FloatingActionButton(
onPressed: () {
if (_stop == 1) {
_stop = 0.0;
} else {
_stop = 1;
}
setState(() {});
},
),
),
],
);
Передадим в шейдер эти два параметра.
Передаем _move и _stop в шейдер
...
body: FutureBuilder<FragmentProgram>(
future: _initShader(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final shader = snapshot.data!.fragmentShader()
..setFloat(0, updateTime)
..setFloat(1, 300)
..setFloat(2, 300)
..setFloat(3, _move)
..setFloat(4, _stop);
return Stack(
children: [
...
В итоге файл main должен выглядеть так:
main.dart
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
var updateTime = 0.0;
var _move = 0.0;
var _stop = 0.0;
@override
void initState() {
super.initState();
createTicker((elapsed) {
updateTime = elapsed.inMilliseconds / 1000;
setState(() {});
}).start();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
body: FutureBuilder<FragmentProgram>(
future: _initShader(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final shader = snapshot.data!.fragmentShader()
..setFloat(0, updateTime)
..setFloat(1, 300)
..setFloat(2, 300)
..setFloat(3, _move)
..setFloat(4, _stop);
return Stack(
children: [
CustomPaint(painter: _MySweepPainter(shader)),
Align(
alignment: Alignment.bottomLeft,
child: FloatingActionButton(
onPressed: () {
if (_move == 1) {
_move = 0.0;
} else {
_move = 1;
}
setState(() {});
},
),
),
Align(
alignment: Alignment.bottomRight,
child: FloatingActionButton(
onPressed: () {
if (_stop == 1) {
_stop = 0.0;
} else {
_stop = 1;
}
setState(() {});
},
),
),
],
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
),
);
}
Future<FragmentProgram> _initShader() {
return FragmentProgram.fromAsset("shader.glsl");
}
}
class _MySweepPainter extends CustomPainter {
_MySweepPainter(this.shader);
final Shader shader;
@override
void paint(Canvas canvas, Size size) {
const Rect rect = Rect.largest;
final Paint paint = Paint()..shader = shader;
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Переходим в исходный код шейдера shader.glsl.
Добавляем две входные переменные iMovie и iStop
uniform float time;
uniform vec2 resolution;
uniform float iMove;
uniform float iStop;
out vec4 fragColor;
Находим функцию vec3 camera(float time). Данная функция возвращает вектор из трех чисел, рассчитанных с помощью входного параметра time. Добавляем условие, если iMove равна 1, то мы будем возвращать первым числом вектора как константу единицу. Соответственно, не будет происходить смещение камеры. И нам будет казаться, что мы летим только прямо.
vec3 camera(float time)
{
if(iMove == 1){
return vec3(1, 5000. + 1500. * sin(0.5 * time), 6000.0 * time);
}
return vec3(5000.0 * sin(1.0 * time), 5000. + 1500. * sin(0.5 * time), 6000.0 * time);
}
И последнее, нам необходимо с помощью входного параметра iStop останавливать анимацию. Найдем функцию main(), в ней добавим следующее условие:
...
vec3 campos = camera(time);
if(iStop == 1){
campos = camera(1);
} else {
campos = camera(time);
}
vec3 camtar = camera(time + 0.4);
...
Таким образом, мы будем проверять, если iStop равна единице, то мы вместо переменной time в функцию camera будем передавать константу.
В итоге наш шейдер должен быть таким:
shader.glsl
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform float time;
uniform vec2 resolution;
uniform float iMove;
uniform float iStop;
out vec4 fragColor;
#define iTime time
#define iMouse mouse
#define iResolution resolution
// referred https://www.shadertoy.com/view/4sXGRM
vec3 skytop = vec3(0.05, 0.2, 0.5);
vec3 light = normalize(vec3(0.1, 0.25, 0.9));
vec2 cloudrange = vec2(0.0, 10000.0);
mat3 m = mat3(0.00, 1.60, 1.20, -1.60, 0.72, -0.96, -1.20, -0.96, 1.28);
// hash function
float hash(float n)
{
return fract(cos(n) * 114514.1919);
}
// 3d noise function
float noise(in vec3 x)
{
vec3 p = floor(x);
vec3 f = smoothstep(0.0, 1.0, fract(x));
float n = p.x + p.y * 10.0 + p.z * 100.0;
return mix(
mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
mix(hash(n + 10.0), hash(n + 11.0), f.x), f.y),
mix(mix(hash(n + 100.0), hash(n + 101.0), f.x),
mix(hash(n + 110.0), hash(n + 111.0), f.x), f.y), f.z);
}
// Fractional Brownian motion
float fbm(vec3 p)
{
float f = 0.5000 * noise(p);
p = m * p;
f += 0.2500 * noise(p);
p = m * p;
f += 0.1666 * noise(p);
p = m * p;
f += 0.0834 * noise(p);
return f;
}
vec3 camera(float time)
{
if(iMove == 1){
return vec3(1, 5000. + 1500. * sin(0.5 * time), 6000.0 * time);
}
return vec3(5000.0 * sin(1.0 * time), 5000. + 1500. * sin(0.5 * time), 6000.0 * time);
}
void main()
{
vec2 uv = 2. * gl_FragCoord.xy / iResolution.xy - 1.0;
uv.x *= iResolution.x / iResolution.y;
float time = (iTime + 13.5 + 44.) * 1.0;
vec3 campos = camera(time);
if(iStop == 1){
campos = camera(1);
} else {
campos = camera(time);
}
vec3 camtar = camera(time + 0.4);
vec3 front = normalize(camtar - campos);
vec3 right = normalize(cross(front, vec3(0.0, 1.0, 0.0)));
vec3 up = normalize(cross(right, front));
vec3 fragAt = normalize(uv.x * right + uv.y * up + front);
// clouds
vec4 sum = vec4(0, 0, 0, 0);
for (float depth = 0.0; depth < 10000.0; depth += 200.0)
{
vec3 ray = campos + fragAt * depth;
if (cloudrange.x < ray.y && ray.y < cloudrange.y)
{
float alpha = smoothstep(0.5, 1.0, fbm(ray * 0.00025));
vec3 localcolor = mix(vec3(1.1, 1.05, 1.0), vec3(0.3, 0.3, 0.2), alpha);
alpha = (1.0 - sum.a) * alpha;
sum += vec4(localcolor * alpha, alpha);
}
}
float alpha = smoothstep(0.7, 1.0, sum.a);
sum.rgb /= sum.a + 0.0001;
float sundot = clamp(dot(fragAt, light), 0.0, 1.0);
vec3 col = 0.8 * (skytop);
col += 0.47 * vec3(1.6, 1.4, 1.0) * pow(sundot, 350.0);
col += 0.4 * vec3(0.8, 0.9, 1.0) * pow(sundot, 2.0);
sum.rgb -= 0.6 * vec3(0.8, 0.75, 0.7) * pow(sundot, 13.0) * alpha;
sum.rgb += 0.2 * vec3(1.3, 1.2, 1.0) * pow(sundot, 5.0) * (1.0 - alpha);
col = mix(col, sum.rgb, sum.a);
fragColor = vec4(col, 1.0);
}
Вот так выглядит управление шейдером из Flutter.
Исходный код можно посмотреть здесь.
Результат
Используем ShaderMask
Мне бы хотелось попробовать наложить шейдер на виджет Text. Для этого нам необходимо изменить метод build виджета MyApp, таким образом чтобы он работал с виджетом ShaderMask.
Измененный метод build()
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.black,
body: FutureBuilder<FragmentProgram>(
future: _initShader(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ShaderMask(
shaderCallback: (bounds) {
return snapshot.data!.fragmentShader()
..setFloat(0, updateTime)
..setFloat(1, bounds.height)
..setFloat(2, bounds.width);
},
child: const Center(
child: Text(
"TEST",
style: TextStyle(
fontSize: 150,
fontWeight: FontWeight.w900,
color: Colors.white,
),
)),
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
),
);
}
Разберем код построчно:
на четвертую и пятую строки для красоты добавим MaterialApp и Scaffold.
на одиннадцатую строку возвращаем виджет ShaderMask. У него есть обратный вызов shaderCallback, который в свою очередь возвращает bounds (прямоугольник размером дочернего элемента виджета ShaderMask). А дочерним элементом является виджет Text. Таким образом, мы накладываем шейдер на текст.
Запускаем проект и любуемся работой ShaderMask.
Результат работы виджета ShaderMask
Хотите поделиться своим опытом работы с шейдерами? Жду ваши вопросы в комментариях!
Исходный код урока можно посмотреть по ссылке.