1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-GLSL.md 19 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Небольшие хитрости Flutter для создания уникальной 3D анимации складывания страниц

Сегодня мы поговорим об интересной реализации анимации в Flutter. Предположим, вам нужно создать эффект 3D складывания страниц, как показано ниже.

анимация 3D складывания страниц

Многие могут сразу подумать, что это можно сделать с помощью матричных преобразований и Canvas в Dart.

Этот эффект довольно распространен, особенно в приложениях для чтения книг, где аналогичные эффекты обычно создаются именно таким образом. Я тоже использовал этот подход раньше, например, в статьях "Крутой 3D карточки и стильный 360° поворот" и "Использование чистого кода для создания 3D Dash и логотипа".

анимация 3D поворота

Однако сегодня мы рассмотрим проект под названием riveo_page_curl, который предлагает альтернативный способ реализации этого эффекта — через использование пользовательских шейдеров отдельных фрагментов (Fragment Shaders). Это позволяет использовать язык программирования GLSL для написания кода шейдеров, что приводит к более сложным графическим эффектам, генерируемым GPU.

анимация 3D складывания страниц через шейдерыПеред тем как объяснять этот проект, давайте поговорим о Fragment Shaders. Flutter начиная с версии 3.7 предоставляет API для работы с Fragment Shaders, то есть это шейдер, действующий на уровне отдельных фрагментов, позволяющий разработчику напрямую влиять на процесс рендера в Flutter.

API для работы с Fragment Shaders в Flutter

А теперь вопрос: почему стоит использовать Fragment Shaders вместо матричных преобразований в Dart? Ответ прост: это снижает нагрузку на CPU, так как вы можете напрямую отправлять команды GPU с помощью языка GLSL, что значительно увеличивает производительность и упрощает реализацию.

Однако компиляция шейдеров может занять некоторое время, поэтому важно компилировать их во время выполнения в специфические платформенные шейдеры.

Конечно, использование Fragment Shaders в Flutter имеет свои ограничения, такие как необходимость использования заголовочного файла #include <flutter/runtime_effect.glsl>. В этом файле содержится функционал доступа к локальным координатам текущего фрагмента, такой как FlutterFragCoord().xy;, что не является стандартным API для GLSL. Кроме того, Fragment Shader поддерживает только файлы в формате .frag, а также имеет следующие ограничения:- Поддержка UBO и SSBO отсутствует

  • sampler2D является единственным поддерживаемым типом sampler'а
  • texture поддерживает только версию с двумя параметрами (sampler и uv)
  • Нельзя объявлять дополнительные переменные входных данных
  • Поддержка беззнаковых целых чисел и булевых значений отсутствуетПоэтому при необходимости переноса существующих эффектов на основе GLSL, таких как код с сайта shadertoy, некоторые изменения в коде всё же потребуются. Например, ниже приведён пример шейдера для плавной анимации градиента:
void mainImage(out vec4 fragColor, in vec2 fragCoord){
    float strength = 0.4;
    float t = iTime / 3.0;
    
    vec3 col = vec3(0);
    vec2 fC = fragCoord;

    for(int i = -1; i <= 1; i++){
        for(int j = -1; j <= 1; j++){
            
            fC = fragCoord + vec2(i, j) / 3.0;
            vec2 pos = fC / iResolution.xy;
            pos.y /= iResolution.x / iResolution.y;
            pos = 4.0 * (vec2(0.5) - pos);
            for(float k = 1.0; k < 7.0; k += 1.0){
                pos.x += strength * sin(2.0 * t + k * 1.5 * pos.y) + t * 0.5;
                pos.y += strength * cos(2.0 * t + k * 1.5 * pos.x);
            }
            col += 0.5 + 0.5 * cos(iTime + pos.xyx + vec3(0, 2, 4));
        }
    }
    col /= 9.0;
    col = pow(col, vec3(0.4545));
    fragColor = vec4(col, 1.0);
}

image

А в Flutter это должно быть преобразовано в следующий код:

  • В первую очередь необходимый файл flutter/runtime_effect.glsl
  • Затем определяется функция main()
  • Также нужно переместить объявление out vec4 fragColor; в глобальную область видимости
  • Поскольку в GLSL iResolution используется для представления высоты и ширины экрана в пикселях, а iTime — для времени выполнения программы, здесь эти значения определяются через uniform как resolution и time соответственно для получения входных данных из Dart
  • Соответствие fragCoord можно получить в Flutter через FlutterFragCoord
#version 460 core
#include <flutter/runtime_effect.glsl>

out vec4 fragColor;
```uniform vec2 resolution;
uniform float iTime;

```markdown
void main() {
    float power = 0.25;
    float t = iTime / 8.0;
    vec3 color = vec3(0);
    vec2 position = FlutterFragCoord().xy / resolution.xy;
    position = 4.0 * (vec2(0.5) - position);
    for (float k = 1.0; k < 7.0; k += 1.0) {
        position.x += power * sin(2.0 * t + k * 1.5 * position.y) + t * 0.5;
        position.y += power * cos(2.0 * t + k * 1.5 * position.x);
    }
    color += 0.5 + 0.5 * cos(iTime + position.xyx + vec3(0, 2, 4));
    color = pow(color, vec3(0.4545));
    fragColor = vec4(color, 1.0);
}
``````markdown
> Первый ряд `#version 460 core` указывает используемую версию языка OpenGL.

Как можно заметить, преобразование одной части кода на GLSL не представляет особой сложности, за исключением изменения координат и входных параметров. Тем не менее, с помощью этих существующих фрагментных шейдеров мы можем получить очень богатые визуальные эффекты, как показано ниже:

- В `pubspec.yaml` импортируйте вышестоящие шейдеры
- Используйте `ШейдерСтроитель`, чтобы загрузить файл `'shaders/warp.frag'` и получить `ФрагментныйШейдер`
- Используйте метод `setFloat` шейдера для передачи данных
- Добавьте шейдер через `Paint().shader` для отрисовки, что позволит завершить рендеринг

```dart
flutter:
  shaders:
    - shaders/warp.frag

...

late Ticker _тиккер;

Duration _elapsed = Duration.zero;

@override
void initState() {
  super.initState();
  _тиккер = createTicker((elapsed) {
    setState(() {
      _elapsed = elapsed;
    });
  });
  _тиккер.start();
}

@override
Widget build(BuildContext context) => ШейдерСтроитель(
      assetKey: 'shaders/warp.frag',
      (BuildContext context, ФрагментныйШейдер шейдер, _) => Scaffold(
        appBar: AppBar(
          title: const Text('Warp'),
        ),
        body: CustomPaint(
          size: MediaQuery.of(context).size,
          painter: ShaderCustomPainter(шейдер, _elapsed),
        ),
      ),
    );

class ShaderCustomPainter extends CustomPainter {
  final ФрагментныйШейдер шейдер;
  final Duration currentTime;
```  ShaderCustomPainter(this.shader, this.currentTime);

  @Override
  void paint(Canvas canvas, Size size) {
    shader.setFloat(0, size.width);
    shader.setFloat(1, size.height);
    shader.setFloat(2, currentTime.inSeconds.toDouble());
    final Paint paint = new Paint()..shader = shader;
    canvas.drawRect(new Offset.zero & size, paint);
  }

  @Override
  boolean shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

Единственное, что требует объяснения — это процесс shader.setFloat. Это происходит потому, что он фактически связывается с переменной в файле .frag. Проще говоря:

Мы определили uniform vec2 resolution; и uniform float iTime; в GLSL. Таким образом, vec2 resolution занимает индексы 0 и 1, а float iTime — индекс 2.

Примерно так понять это можно: vec2 представляет собой два значения типа float, объединённых вместе. Поэтому vec2 resolution занимает индексы 0 и 1. Например, если бы были объявлены vec2 и vec3, они заняли бы индексы 0–4 соответственно.


Аналогично, используя `uniform` в шейдерах GLSL, можно определить значения, а затем передать соответствующие данные с помощью индекса `setFloat` в Dart, что обеспечивает полный цикл взаимодействия данных.

> Полный код для этого градиентного анимационного эффекта в Flutter можно найти на GitHub по адресу https://github.com/tbuczkowski/flutter_shaders в файле [warp.frag](https://github.com/tbuczkowski/flutter_shaders/blob/master/shaders/warp.frag).

Автор также предоставил аналогичную реализацию на чистом Dart для сравнения с использованием шейдеров, благодаря чему удалось значительно повысить производительность.![image-20231031175152699](http://img.cdn.guoshuyu.cn/20231031_GLSL/image7.png)

Просмотрев проект [riveo_page_curl](https://github.com/Rahiche/riveo_page_curl), можно заметить, что шейдеры используют множество непонятных матричных преобразований, таких как масштабирование (`scale`), перемещение (`translate`) и проекция (`project`). Также присутствуют различные тригонометрические вычисления, основной идеей которых является расчет радиуса кривой при матричных преобразованиях и добавление тени для повышения визуального качества.

```glsl
#include <flutter/runtime_effect.glsl>

uniform vec2 resolution;
uniform float pointer;
uniform float origin;
uniform vec4 container;
uniform float cornerRadius;
uniform sampler2D image;

const float r = 150.0;
const float scaleFactor = 0.2;

#define PI 3.14159265359
#define TRANSPARENT vec4(0.0, 0.0, 0.0, 0.0)

mat3 translate(vec2 p) {
    return mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, p.x, p.y, 1.0);
}

mat3 scale(vec2 s, vec2 p) {
    return translate(p) * mat3(s.x, 0.0, 0.0, 0.0, s.y, 0.0, 0.0, 0.0, 1.0) * translate(-p);
}

vec2 project(vec2 p, mat3 m) {
    return (inverse(m) * vec3(p, 1.0)).xy;
}

struct Paint {
    vec4 color;
    bool stroke;
    float strokeWidth;
    int blendMode;
};
``````cpp
struct Context {
    vec4 color;
    vec2 p;
    vec2 resolution;
};
bool внутриКвадрата(vec2 p, vec4 квадрат) {
    bool внутри = p.x > квадрат.x && p.x < квадрат.z && p.y > квадрат.y && p.y < квадрат.w;
    if (!внутри) {
        return false;
    }
    // Верхний левый угол
    if (p.x < квадрат.x + радиус_угла && p.y < квадрат.y + радиус_угла) {
        return длина(p - vec2(квадрат.x + радиус_угла, квадрат.y + радиус_угла)) < радиус_угла;
    }
    // Верхний правый угол
    if (p.x > квадрат.z - радиус_угла && p.y < квадрат.y + радиус_угла) {
        return длина(p - vec2(квадрат.z - радиус_угла, квадрат.y + радиус_угла)) < радиус_угла;
    }
    // Нижний левый угол
    if (p.x < квадрат.x + радиус_угла && p.y > квадрат.w - радиус_угла) {
        return длина(p - vec2(квадрат.x + радиус_угла, квадрат.w - радиус_угла)) < радиус_угла;
    }
    // Нижний правый угол
    if (p.x > квадрат.z - радиус_угла && p.y > квадрат.w - радиус_угла) {
        return длина(p - vec2(квадрат.z - радиус_угла, квадрат.w - радиус_угла)) < радиус_угла;
    }
    return true;
}
out vec4 fragColor;
``````swift
void main() {
    vec2 xy = FlutterFragCoord().xy;
    vec2 center = resolution * 0.5;
    float dx = origin - pointer;
    float x = container.z - dx;
    float d = xy.x - x;

    if (d > r) {
        fragColor = TRANSPARENT;
        if (inRect(xy, container)) {
            fragColor.a = mix(0.5, 0.0, (d - r) / r);
        }
    }

    else
    if (d > 0.0) {
        float theta = asin(d / r);
        float d1 = theta * r;
        float d2 = (3.14159265 - theta) * r;

        vec2 s = vec2(1.0 + (1.0 - sin(3.14159265 / 2.0 + theta)) * 0.1);
        mat3 transform = scale(s, center);
        vec2 uv = project(xy, transform);
        vec2 p1 = vec2(x + d1, uv.y);

        s = vec2(1.1 + sin(3.14159265 / 2.0 + theta) * 0.1);
        transform = scale(s, center);
        uv = project(xy, transform);
        vec2 p2 = vec2(x + d2, uv.y);

        if (inRect(p2, container)) {
            fragColor = texture(image, p2 / resolution);
        } else if (inRect(p1, container)) {
            fragColor = texture(image, p1 / resolution);
            fragColor.rgb *= pow(clamp((r - d) / r, 0.0, 1.0), 0.2);
        } else if (inRect(xy, container)) {
            fragColor = vec4(0.0, 0.0, 0.0, 0.5);
        }
    }
    else {
        vec2 s = vec2(1.2);
        mat3 transform = scale(s, center);
        vec2 uv = project(xy, transform);

        vec2 p = vec2(x + abs(d) + 3.14159265 * r, uv.y);
        if (inRect(p, container)) {
            fragColor = texture(image, p / resolution);
        } else {
            fragColor = texture(image, xy / resolution);
        }
    }

}

изображение

На самом деле, мне кажется, что вас больше интересует не сама реализация логики, а то, как ее использовать. Ключевым моментом здесь является объявление uniform sampler2D image. Благодаря этому объявленному типу данных, можно передать объект типа ui.Image через метод setImageSampler(0, image) в Dart, чтобы реализовать вышеописанный складывающийся анимационный эффект для компонентов Flutter.В контексте Dart это означает использование не только ShaderBuilder, но также библиотеки flutter_shaders для более простого использования shader, image и canvas. Основной функцией AnimatedSampler является то, что он позволяет сделать снимок всего потомка через PictureRecorder, преобразовать его в ui.Image и передать в GLSL, тем самым обеспечивая взаимодействие между UI и эффектами. Полный проект доступен по адресу: https://github.com/Rahiche/riveo_page_curl

Как можно заметить, в отличие от реализации такого 3D эффекта вращения страниц с помощью Dart, использование FragmentShader позволяет сделать код более лаконичным и обеспечивает лучшую производительность. Более того, подобные шейдеры, как те, что используются в ShaderToy, могут быть легко адаптированы и использованы в Flutter, что делает его особенно удобным для использования в игровых сценариях.

Наконец, начиная с версии Flutter 3.10, Flutter Web также поддерживает fragment shaders, поэтому сейчас реализация шейдеров в Flutter достаточно зрелая. Если бы я ранее реализовал логику "эффекта поломки" для текста с помощью Flutter, то было бы более эффективно и проще использовать fragment shaders для этой задачи.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master