Как создать анимированные шейдеры в Jetpack Compose

от автора

Jetpack Compose — молодой, но бурно развивающийся фреймворк для разработки под Android, который обладает множеством не всегда очевидных фичей. Сегодня я хотел бы описать одну из таких встроенных возможностей: речь идет об использовании OpenGL-шейдеров. Они позволяют делать красивые анимированные интерфейсы, как на картинке ниже.

image


Я написал это приложение не просто на Jetpack Compose, а на Compose Multiplatform, поэтому вместе с мобильной версией у нас должна получиться аналогичная десктопная:

image

В качестве основы дизайна для приложения возьмем первый попавшийся проект с Figma. Мне было не важно, как именно будет выглядеть приложение, потому что его основная цель — показать работу шейдеров. По этой же причине я опущу подробности написания простенького кода с текстом и полем для ввода мыла. Полную версию кода всегда можно почитать в репозитории.

Итак, перейдем к шейдерам. Изложенный ниже способ позволяет применить их как фон к любому Composable UI-элементу. Для этого создадим функцию-расширение, применяющую шейдер, и назовем ее Modifier.shaderEffect().

Чтобы функция работала, нужен а) шейдер на OpenGL и б) код на Kotlin, который запускает этот шейдер. Начнем с первого. Я взял уже имеющийся шейдер, классическую старую как мир анимацию с облаками, которая даже на моем Pixel 7 выдает 90 fps. Чтобы было удобно работать и линтер Intellij Idea подсветил код на другом языке в .kt файле, добавим аннотацию @Language перед строкой:

@Language("GLSL") const val compositeSksl = """                // Параметры шейдера                 uniform float3 iResolution;      // Viewport resolution (pixels)                 uniform float  iTime;            // Shader playback time (s)                 // Тело шейдера                 ...             """ 

Полностью код шейдера приводить не буду: в его копипасте с ShaderToy заслуга невелика. Для нас важнее другое. Создадим саму функцию, рисующую шейдер:

 @RequiresApi(Build.VERSION_CODES.TIRAMISU) actual fun Modifier.shaderEffect(): Modifier = composed {     val time by produceState(0f) {         while (true) {             withInfiniteAnimationFrameMillis {                 value = it / 1000f             }         }     }     Modifier.drawWithCache {         val shader = RuntimeShader(compositeSksl)         val shaderBrush = ShaderBrush(shader)         shader.setFloatUniform("iResolution", size.width, size.height)         shader.setFloatUniform("iTime", time)         onDrawBehind {             drawRect(shaderBrush)         }     } } 

Что здесь происходит? produceStateOf запускает отдельную корутину (side-effect), в которой будет отсчитываться текущее время шейдера. withInfiniteAnimationFrameMillis запускает бесконечную покадровую анимацию, результат этой анимации записывается в produceState.

drawWithCache позволяет не только рисовать что-либо, но и кэшировать значения переменных внутри функции. Это дает нам возможность оптимизировать выделение памяти под наши объекты. Параметры в шейдер передаем при помощи setFloatUniform, причем внимательно следим за типами данных: в таком интеропе, к сожалению, нет compile-time проверки, что в float передан один ключ, а в float2 — два ключа.

Что касается десктопной версии, она будет отличаться, но незначительно. Код самого шейдера останется тем же, но за рендер будет отвечать десктопная обертка Skiko (Skia for Kotlin):

 actual fun Modifier.shaderEffect(): Modifier = composed {     val time by produceState(0f) {         while (true) {             withInfiniteAnimationFrameMillis {                 value = it / 1000f             }         }     }     Modifier.drawWithCache {         val effect = RuntimeEffect.makeForShader(compositeSksl)         val compositeShaderBuilder = RuntimeShaderBuilder(effect)         compositeShaderBuilder.uniform(             name = "iResolution",             value1 = size.width,             value2 = size.height         )         compositeShaderBuilder.uniform(             "iTime",             time         )         val shaderBrush = ShaderBrush(compositeShaderBuilder.makeShader())         onDrawBehind {             drawRect(shaderBrush)         }     } } 

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

Я поигрался со своим пет-проектом и запустил еще шейдер с горой Фудзи в стиле киберпанк. Возможно, вам захочется поставить на фон что-то свое, не ограничивайте полет фантазии!

image


ссылка на оригинал статьи https://habr.com/ru/companies/timeweb/articles/736192/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *