Я написал это приложение не просто на 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: в приложении появилась возможность выбирать один шейдер из трех прямо из интерфейса:
Комментарии (10)
quaer
19.05.2023 14:20Я написал это приложение не просто на Jetpack Compose, а на Compose Multiplatform
Совсем запутали... Как понять что есть что?
glider_skobb Автор
19.05.2023 14:20Jetpack Compose - фреймворк для разработки под Андроид от Google. Вскоре после его релиза Jetbrains выпустили на его основе Compose Multiplatform - фреймворк, позволяющий запускать интерфейсы Jetpack Compose дополнительно в десктопных приложениях, iOS и в браузере. Управление состоянием, StateFlow, рендерер Skia те же. Подпробнее можно почитать тут: https://github.com/JetBrains/compose-multiplatform и в моей статье: https://habr.com/ru/companies/timeweb/articles/734818/
Rusrst
19.05.2023 14:20-1Они даже пару слов в язык добавили, все ради него.
glider_skobb Автор
19.05.2023 14:20Если вы про expect и actual - они доступны только при наличии KCP kotlin("multiplatform"). Любой желающий может добавить в свой проект на котлине новые ключевые слова, если напишет собственный плагин компиляции.
Rusrst
19.05.2023 14:20Да ладно, а чего же они аж в compose для android лежат без всяких плагинов? А давно у нас модификаторы языка без плагинов не собираются?
https://kotlinlang.org/docs/keyword-reference.html#modifier-keywords
nikita_dol
19.05.2023 14:20+1Я так понимаю, что от
Modifier.drawWithCache
нету смысла, так как происходит постоянное изменение времени, а значит, что шейдеры пересоздаются на каждую перерисовкуglider_skobb Автор
19.05.2023 14:20Когда люди пробуют рисовать такое без кэширования, fps дропается до 5 или ниже:
nikita_dol
19.05.2023 14:20+1(Промахнулся веткой, ответ на этот комментарий)
Согласно документации,
drawWithCache
вызывает лямбду когда изменился размер рисуемой области и/или состояние, которое читается из лямбдыВ вашем случае, это сработает только между кадрами, если кадры будут рисоваться быстрее, чем вы изменяете переменную
time
Можете
print
в консоль сделать и убедится, что у вас постоянно создаются новые шейдеры
Rusrst
Шейдеры только в 13 Андроиде завезли, ниже их нет, так что конечно красиво, но ветвления логики что-то не заметно, кто соберёт на устройство ниже, поймает краш.
А у вас все хорошо работает, glsl вместо agls подхватился без проблем?
glider_skobb Автор
Да, работает хорошо, запускал как в эмуляторе, так и на физическом устройстве.