Как создать анимированные шейдеры в Jetpack Compose
Jetpack Compose — молодой, но бурно развивающийся фреймворк для разработки под Android, который обладает множеством не всегда очевидных фичей. Сегодня я хотел бы описать одну из таких встроенных возможностей: речь идет об использовании OpenGL-шейдеров. Они позволяют делать красивые анимированные интерфейсы, как на картинке ниже.
Я написал это приложение не просто на Jetpack Compose, а на Compose Multiplatform, поэтому вместе с мобильной версией у нас должна получиться аналогичная десктопная:
В качестве основы дизайна для приложения возьмем первый попавшийся проект с 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 скажется на всем интерфейсе приложения.
Я поигрался со своим пет-проектом и запустил еще шейдер с горой Фудзи в стиле киберпанк. Возможно, вам захочется поставить на фон что-то свое, не ограничивайте полет фантазии!
UPD: в приложении появилась возможность выбирать один шейдер из трех прямо из интерфейса: