Когда я впервые всерьёз сел писать AGSL под Android, ощущение было очень странное. С одной стороны - современный графический конвейер, RuntimeShaderRenderEffect, красивые эффекты и весь этот техно-киберпанк. С другой - шейдерный код живёт внутри строковых литералов, uniform-ы приходится объявлять и привязывать вручную, а отладка периодически начинается с философского вопроса: «в какой именно строке я сейчас всё сломал?»

В какой-то момент я поймал очень «кин-дза-дзовское» настроение: вроде перед тобой космическая технология, а инструменты ощущаются так, будто тебе выдали гравицапу без инструкции и сказали: «ну ты это... лети». Можно, конечно. Но хочется, чтобы летели не только самые упорные.

Собственно, так и появился RedByteFX. Я сделал его не только ради собственного удобства. Мне хотелось решить ещё одну задачу: сделать AGSL более массовой и понятной технологией для Android-разработчиков. Потому что в нативном виде AGSL мощный, но психологически для многих выглядит как «не трогай, это на Новый год». А мне хотелось, чтобы к шейдерам можно было подойти без дрожи в коленях и без ощущения, что сейчас придётся разговаривать с господином ПЖ на древнем пацакском наречии.

У библиотеки очень простой принцип:

Kotlin DSL → сгенерированный AGSL → RuntimeShader / RenderEffect

То есть на устройстве по-прежнему исполняется нативный AGSL. Я не подменяю исполнение каким-то своим движком и не прячу магию под ковёр. Я меняю только то, как шейдер пишется и собирается: вместо хрупких строк с шейдером мы получаем типизированный Kotlin DSL, а если хочется убедиться, что всё честно, всегда можно посмотреть итоговую строку через метод agslSource().

Принцип библиотеки RedByteFX
Принцип библиотеки RedByteFX

В статье я покажу:

  • почему голый AGSL в Android-коде быстро начинает утомлять;

  • как выглядит тот же шейдер в RedByteFX;

  • четыре учебных примера - от простого к сложным эффектам;

  • как устроен DSL: координаты, uniform-ы, let(...)fn(...)sample()sampleUv(), стандартная библиотека и интеграция с Compose;

  • где библиотека реально выигрывает, а где у неё есть честные ограничения.

RedByteFx demo
RedByteFx demo

Почему голый AGSL в Android-коде так часто ощущается как наказание

Сразу важная оговорка: сам AGSL не плохой. Наоборот, штука мощная и очень полезная. Проблема не в самом шейдерном языке, а в том, как именно мы обычно пишем и сопровождаем его из Kotlin.

Вот где чаще всего начинает болеть:

  • код шейдера живёт внутри строки, а значит IDE помогает сильно меньше, чем могла бы;

  • имена uniform-ов нужно держать в голове и не ошибаться в строковых ключах;

  • рефакторинг становится нервным: переименовал что-то в Kotlin, а строку не обновил - привет;

  • даже средний эффект быстро превращается в суп из smoothstepfractmix и локальных временных переменных;

  • в Compose поверх этого ещё появляется обвязка времени выполнения: контроллеры, привязка времени, обновление размеров, инвалидирование;

  • разработчик, который просто хотел «лёгкое красивое свечение», внезапно оказывается в песках Плюка.

Я хотел сохранить силу AGSL, но убрать ощущение, что ты каждый раз ковыряешься отвёрткой в тёмном отсеке корабля.

Подключение

RedByteFX опубликован в Maven Central. Библиотека рассчитана на Android API 33+, потому что опирается на современный стек AGSL / RuntimeShader / RenderEffect.

dependencies {
    implementation("io.github.i-redbyte:redbytefx-core:1.0.0")
    implementation("io.github.i-redbyte:redbytefx-compose:1.0.0")
    implementation("io.github.i-redbyte:redbytefx-stdlib:1.0.0")
}

Репозиторий проекта: github.com/i-redbyte/redbytefx

Два разных подхода к одному шейдеру

Возьмём самый простой и честный пример: волновое смещение по оси Y. Это демо есть в приложении с примерами как DemoWave.kt.

Вручную на AGSL

uniform shader content;
uniform float wave_amplitude;
uniform float wave_frequency;

half4 main(float2 fragCoord) {
  float2 offset = float2(
    0.0,
    sin(fragCoord.x * wave_frequency) * wave_amplitude
  );
  return content.eval(fragCoord + offset);
}

Формально всё нормально. Но как только вы начинаете расширять эффект, добавлять локальные вычисления, привязку параметров во время работы, элементы интерфейса и несколько вариантов логики - строка быстро перестаёт быть уютным местом.

То же самое на RedByteFX

val effect = redbytefx {
    val amplitudeUniform = uniformFloat(0f, "wave_amplitude")
    val frequencyUniform = uniformFloat(0.08f, "wave_frequency")

    val x = let(fragCoord.x, "x")
    val waveOffset = let(
        float2(0f, sin(x * frequencyUniform) * amplitudeUniform),
        "wave_offset"
    )

    sample(fragCoord + waveOffset)
}

Разница тут не в том, что математика «стала другой». Она как раз осталась той же. Но изменился опыт разработки:

  • uniform-ы теперь типизированы и одновременно служат дескрипторами для привязки во время работы;

  • let(...) позволяет осмысленно именовать промежуточные шаги;

  • финальное чтение входного контента выглядит как sample(...), а не как ручная возня со строкой;

  • код живёт в Kotlin, а значит IDE снова ваш союзник, а не сторонний наблюдатель.

Самое приятное: если хочется доказательств, что DSL не занимается шаманством, можно посмотреть сгенерированный AGSL. Для волны он остаётся почти один в один:

uniform shader uContent;
uniform float2 uResolution;
uniform float u_amp;
uniform float u_freq;

half4 main(float2 fragCoord) {
  return rb_sample(
    fragCoord + float2(0.0, sin(fragCoord.x * u_freq) * u_amp)
  );
}

И вот это, на мой взгляд, ключевой момент. RedByteFX не прячет AGSL. Он делает так, чтобы до AGSL было приятно дойти живым человеком.

От первого шейдера к эффектам, которые хочется повторить

Ниже четыре реальных примера из демонстрационного приложения. Я специально иду по возрастающей: простой, чуть более сложный, средний и, на мой взгляд, витринный.

1. Wave: самый честный старт

Почему я советую начинать именно с него:

  • он очень близок к чистому AGSL по форме;

  • на нём легко понять, что такое fragCoordsample() и пользовательские uniform-ы;

  • здесь уже видна польза let(...), но ещё нет ощущения, что вам дали швейцарский нож размером с трактор.

Если бы я объяснял RedByteFX одной фразой, я бы сказал так: это AGSL, которому наконец-то разрешили жить в нормальном Kotlin-коде.

2. Signal: когда появляются функции, маски и процедурная логика

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

val effect = redbytefx {
    val densityUniform = uniformFloat(8f, "signal_density")
    val lineWidthUniform = uniformFloat(0.08f, "signal_line_width")
    val amountUniform = uniformFloat(0.85f, "signal_amount")

    val pulseBand = fn(
        name = "pulse_band",
        arg1 = FloatType,
        arg2 = FloatType,
        returns = FloatType
    ) { phase, threshold ->
        step(threshold, smoothstep(0.08f, 0.92f, fract(phase)))
    }

    val base = let(sample(), "base")
    val uv = let(normalizedUv(), "uv")
    val grid = let(gridMask(uv, densityUniform, lineWidthUniform), "grid")
    val scan = let(scanlines(fragCoord.y, 14f, 3f), "scan")
    val pulse = let(pulseBand(uv.y * densityUniform * 0.5f + grid * 0.35f, 0.55f), "pulse")
    val hardMask = let(step(0.45f, scan * pulse), "hard_mask")
    val active = let((grid gt 0.05f) or (hardMask gt 0.5f), "active")
    val accent = let(color(float3(0.05f, 0.95f, 0.82f), base.a), "accent")
    val mixed = let(mix(base, accent, min(grid * 0.85f + hardMask * 0.35f, 1f)), "mixed")

    ifElse(active, mix(base, mixed, amountUniform), base)
}

Что здесь важно:

  • fn(...) позволяет вынести вспомогательную AGSL-функцию в отдельный именованный блок;

  • normalizedUv() сразу переводит нас в нормализованное пространство координат;

  • gridMask(...) и scanlines(...) из стандартной библиотеки убирают процедурный шум и делают код намерения читаемым;

  • ifElse(...) оставляет шейдер выражением, а не ломает его на императивные костыли.

В сыром AGSL такой эффект тоже можно написать. Но довольно быстро код превращается в что-то вида «сорок строк математики, из которых через три дня понятна только первая и случайно последняя». А здесь видно историю эффекта: база, UV, сетка, скан, импульс, маска, смешивание. Не просто формулы, а сюжет.

3. Radar: когда стандартная библиотека начинает экономить вам дни жизни

Радар - отличный пример того, где чистый AGSL ещё не ужасен, но уже начинает напоминать технический долг в реальном времени. Полярные координаты, вращающийся луч, дуга, кольца, радиальный градиент, смешивание слоёв - всё это очень любит разрастаться.

val effect = redbytefx {
    val time by autoUniformTime()
    val speed by autoUniformFloat(0.72f)
    val radius by autoUniformFloat(0.34f)
    val amount by autoUniformFloat(0.86f)

    val base = let(sample(), "base")
    val uv = let(fragCoord / resolution, "uv")
    val polar = let(polarCoordinates(uv), "polar")
    val sweepAngle = let(fract(time * speed * 0.08f), "sweep_angle")
    val sweep = let(angularSweep(uv = uv, angle = sweepAngle, width = 0.12f, feather = 0.03f), "sweep")
    val arc = let(
        arcMask(
            uv = uv,
            radius = radius,
            ringWidth = 0.09f,
            angle = sweepAngle,
            arcWidth = 0.18f,
            feather = 0.03f
        ),
        "arc"
    )
    val outerRing = let(ringMask(uv, radius = radius, width = 0.016f, feather = 0.012f), "outer_ring")
    val innerRing = let(ringMask(uv, radius = max(radius * 0.58f, 0.08f), width = 0.014f, feather = 0.012f), "inner_ring")
    val beam = let(radialRamp(uv = uv, innerRadius = float(0.06f), outerRadius = radius + 0.18f), "beam")
    val mask = let(max(max(sweep * beam, arc), max(outerRing, innerRing)), "mask")

    val tint = let(
        color(
            mix(0.05f, 0.18f, polar.x * 1.4f),
            mix(0.24f, 1f, sweep + arc * 0.55f),
            mix(0.10f, 0.62f, polar.y * 0.45f + outerRing * 0.35f),
            base.a
        ),
        "tint"
    )

    val screened = let(maskedScreen(base, tint, mask, amount), "screened")
    maskedOverlay(screened, color(float3(0.82f, 1f, 0.72f), base.a), arc, amount * 0.32f)
}

Вот здесь стандартная библиотека RedByteFX раскрывается во весь рост:

  • polarCoordinates(...) и angularSweep(...) делают полярную логику декларативной;

  • arcMask(...)ringMask(...)radialRamp(...) избавляют от копипасты из smoothstep и ручных кривых затухания;

  • maskedScreen(...) и maskedOverlay(...) позволяют говорить языком композиции, а не языком случайно перемноженных коэффициентов.

Если коротко: "сырой" AGSL тут уже начинает требовать дисциплины уровня «пацак сказал - пацак сделал». RedByteFX позволяет всё ещё думать про эффект, а не про то, сколько раз вы сегодня вручную собрали кольцевую маску.

4. Metaballs: когда шейдер уже начинает выглядеть как живая материя

Здесь библиотека показывает уже совсем другой класс задач. Это не постобработка, не вращающийся луч и не маски поверх готового контента, а вполне процедурная «живая» форма: три SDF-круга сливаются с помощью сглаженного минимума в мягкие неоновые сгустки. И именно в этот момент особенно хорошо видно, что RedByteFX годится не только для аккуратной обвязки AGSL, но и для написания выразительной математики шейдера как нормального Kotlin-кода.

val effect = redbytefx {
    val timeUniform = uniformTime(name = "meta_time")
    val blendK = uniformFloat(0.1f, "meta_blend")

    val uv = let(fragCoord / resolution, "uv")
    val c1 = let(float2(0.35f + sin(timeUniform * 0.7f) * 0.11f, 0.42f + cos(timeUniform * 0.52f) * 0.09f), "c1")
    val c2 = let(float2(0.64f + cos(timeUniform * 0.58f) * 0.1f, 0.54f + sin(timeUniform * 0.63f) * 0.08f), "c2")
    val c3 = let(float2(0.48f + sin(timeUniform * 0.33f) * 0.13f, 0.74f + cos(timeUniform * 0.41f) * 0.07f), "c3")

    val d1 = let(sdCircle(uv - c1, 0.11f), "d1")
    val d2 = let(sdCircle(uv - c2, 0.1155f), "d2")
    val d3 = let(sdCircle(uv - c3, 0.1045f), "d3")

    val m12 = let(sminPoly(d1, d2, 0.085f), "m12")
    val field = let(sminPoly(m12, d3, blendK), "field")
    val blob = softFill(field, feather = 0.035f)

    val bg = color(float3(0.03f, 0.04f, 0.07f), 1f)
    val fill = color(float3(0.15f, 0.95f, 0.82f), 1f)
    val rim = color(float3(0.95f, 0.35f, 0.85f), 1f)
    val shaded = mix(fill, rim, saturate(blob * 1.15f - 0.35f))

    mix(bg, shaded, blob)
}

Здесь особенно приятно то, что sminPoly(...) - не встроенная магия, а небольшая вспомогательная функция, объявленная прямо рядом в том же примере. То есть DSL позволяет не только комбинировать готовые кубики, но и спокойно дописывать свою математику шейдера, когда она действительно нужна.

Чем мне нравится этот пример:

  • он резко отличается от Radar и визуально, и по математике: вместо масок вращающегося луча здесь SDF и слияние полей;

  • на нём очень хорошо видно, что DSL годится не только для обвязки AGSL, но и для написания собственных вспомогательных шейдерных функций вроде sminPoly(...);

  • sdCircle(...)softFill(...) и сглаженный минимум превращают сложную процедурную форму в набор читаемых строительных блоков;

  • это один из тех примеров, которые отлично смотрятся в анимации: сгустки двигаются, слипаются и, как мне кажется, вызывают желания «поиграться с этим».

Именно на таких примерах особенно видно, почему библиотечный подход выигрывает. На чистом AGSL такие меташары тоже собрать можно, но очень быстро код превращается в липкую субстанцию не только на экране, но и в редакторе. А тут история остаётся прозрачной: центры кругов, поля расстояний, плавное слияние, мягкая заливка, цвет. Как говорится: "Ку!".

Бонус: что ещё посмотреть

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

Cамое вкусное: как устроен DSL RedByteFX

Если воспринимать RedByteFX как «набор готовых заготовок», можно недооценить библиотеку. На самом деле это именно DSL для написания AGSL в Kotlin. Ниже - краткая, но максимально практическая roadmap.

1. Корневые координаты: fragCoord и resolution

Здесь всё максимально похоже на AGSL:

  • fragCoord - текущая координата фрагмента в пикселях;

  • resolution - размер области рендера в пикселях.

val uv = fragCoord / resolution
val center = resolution * 0.5f

Если вы переносите существующий AGSL почти один в один, это очень помогает: мозг не делает лишний кульбит.

2. Чтение входного контента: sample() и sampleUv()

Правило простое:

  • sample() и sample(coord) работают в пиксельном пространстве;

  • sampleUv(uv) из stdlib работает в нормализованном UV-пространстве [0, 1];

  • sampleUnclamped(...) нужен для осознанных экспериментов с выходом за границы.

Это важный момент, потому что значительная часть ошибок в AGSL-портировании - это именно путаница пространств координат.

val base = sample()                  // читаем по fragCoord
val uv = normalizedUv()             // переходим в UV
val reread = sampleUv(uv + drift)   // читаем повторно уже в UV-пространстве

3. Uniform-ы: типизированные и пригодные для работы эффекта

Вместо строковой магии вы пишете обычный код на DSL:

val amount = uniformFloat(0.5f, "amount")
val shift = uniformFloat2(0f, 0f, "shift")
val time = uniformTime(name = "time")

Или ещё приятнее - через auto-делегаты:

val amount by autoUniformFloat(0.5f)
val time by autoUniformTime()
val shift by autoUniformFloat2(0f, 0f)

Почему это круто:

  • имя можно не таскать строкой по всему коду;

  • возвращаемый объект FxParam - это одновременно выражение внутри DSL и дескриптор для привязки во время работы;

  • имя свойства в Kotlin превращается в читаемое имя uniform-а в сгенерированном AGSL.

Важно: дескриптор uniform-а привязан к конкретному скомпилированному эффекту. Нельзя взять FxParam из одного redbytefx { ... } и безопасно использовать его с другим эффектом, даже если имена похожи.

4. Типы выражений

Внутри DSL всё представлено типизированными узлами выражений:

  • FloatExpr - скалярный float;

  • BoolExpr - булево выражение;

  • Float2ExprFloat3ExprFloat4Expr - векторы;

  • ColorExpr - цветовой результат.

Это не значения времени выполнения, а строительные блоки будущего AGSL. Проще говоря, вы пишете не «программу, которая считает прямо сейчас», а «дерево выражений, которое потом честно превратится в AGSL».

5. Конструкторы значений

val scalar = float(0.5f)
val offset = float2(0f, 12f)
val rgb = float3(0.2f, 0.8f, 1f)
val rgba = float4(rgb, 1f)
val tint = color(0.2f, 0.8f, 1f, 1f)

Это вещи, которые в чистом AGSL вы и так делаете постоянно. В RedByteFX просто появляется типобезопасная форма записи.

6. Операторы, сравнения и условия

Обычная математика поддерживается привычно: +-*/. Для сравнений есть ltltegtgteeqneq. Для булевой логики - andor!.

val active = (amount gt 0.5f) and (edge lt 0.9f)
val mask = ifElse(active, 1f, 0f)

Почему не обычный if? Потому что внутри шейдера нам нужно строить выражение, а не выполнять Kotlin-ветвление на CPU.

7. Базовые встроенные функции

В core уже есть всё, что обычно нужно для AGSL-подобного мышления:

  • mixclampsmoothstepstepsaturate;

  • sincosatanpowsqrtabsfloorceilfract;

  • minmaxmod;

  • dotlengthdistancenormalize;

  • luminancegrayscale.

Мне было важно сохранить близость к AGSL-лексике, чтобы перенос старого шейдера не превращался в перевод с одного языка на совершенно другой.

8. Локальные переменные через let(...)

Это один из моих любимых инструментов в библиотеке. В чистом AGSL вы и так всё время заводите временные переменные. Так почему бы не делать это так же удобно и в DSL?

val base = let(sample(), "base")
val luma = let(luminance(base), "luma")
val mono = let(grayscale(base), "mono")
mix(base, mono, luma)

let(...) сохраняет выражение как локальную переменную в сгенерированном AGSL. Это резко улучшает читаемость больших эффектов. А ещё это очень помогает при отладке через agslSource(): вы видите не мешанину, а именованные шаги.

9. Переиспользуемые функции через fn(...) и fnN(...)

Если в AGSL, написанном вручную, вы бы вынесли вспомогательную функцию, здесь нужно делать ровно то же самое.

val palette = fn(
    name = "palette_rgb",
    arg1 = FloatType,
    arg2 = FloatType,
    returns = Float3Type
) { tone, warmth ->
    val phase = let(tone * 6.2831855f, "phase")
    float3(
        0.24f + 0.45f * sin(phase + warmth * 0.90f + 0.10f),
        0.30f + 0.42f * sin(phase + warmth * 1.50f + 2.10f),
        0.42f + 0.36f * sin(phase + warmth * 2.10f + 4.20f)
    )
}

Этот стиль можно посмотреть вживую в DemoDuotone.kt. Если параметров больше четырёх, есть fnN(...).

10. core и stdlib: где заканчивается «чистый DSL» и начинается «удобная библиотека рецептов»

Я бы сформулировал так:

  • redbytefx-core - это язык и минимальный инструментарий. Идеален, когда вы переводите чистый AGSL почти один в один.

  • redbytefx-stdlib - это набор высокоуровневых рецептов поверх языка. Он нужен, когда у вас начинают повторяться маски, переходы, световые приёмы, маршруты, полярная логика, SDF и композитинг.

Полезные группы хелперов из stdlib:

  • координаты: normalizedUvsampleUvcenteredUvaspectCenteredUv;

  • маски: circleMaskrectMaskringMaskarcMask;

  • переходы и градиентные маски: horizontalRevealverticalRevealradialRevealradialRampangularSweep;

  • смешивание слоёв: maskedMixalphaMaskmaskedScreenmaskedOverlayblendMultiplyblendScreenblendOverlay;

  • свет и форма: rimLight, SDF-хелперы, хелперы маршрутов, шумы и искажения вроде domainWarp.

Практическое правило очень простое:

  1. Если вы портируете чистый AGSL, сначала оставайтесь ближе к core.

  2. Когда видите повторяющиеся паттерны, переходите к stdlib.

  3. После каждого такого шага смотрите agslSource(), чтобы сохранить ощущение прозрачности.

Важные элементы тут такие:

  • rememberFxController(effect) создаёт удобный для Compose контроллер для экземпляра эффекта во время работы;

  • bindFloat(...)bindFloat2(...)bindTime(...) обновляют параметры без ручной возни;

  • Modifier.redbyteFx(fx) применяет эффект к composable.

Если что-то выглядит странно, самый короткий путь диагностики обычно такой: сначала смотрим effect.agslSource(), потом проверяем пространство координат, затем убеждаемся, что привязываем именно те FxParam, которые были созданы этим эффектом.

Небольшая шпаргалка: как мыслить при портировании AGSL в RedByteFX

Что было в AGSL

Чем это становится в RedByteFX

Комментарий

main(float2 fragCoord)

тело redbytefx { ... }

Возвращаем финальный ColorExpr.

uniform float u_amount;

val amount by autoUniformFloat(0.5f)

И выражение в шейдере, и дескриптор для привязки во время работы сразу.

content.eval(fragCoord)

sample()

Для UV-повторного чтения - sampleUv(...).

float2 uv = fragCoord / resolution;

val uv = let(normalizedUv(), "uv")

Можно и напрямую fragCoord / resolution.

float foo(float x) { ... }

val foo = fn(...)

Функция остаётся видимой и в сгенерированном AGSL.

float tmp = ...;

val tmp = let(..., "tmp")

Очень помогает держать большие эффекты читаемыми.

if (cond) a else b

ifElse(cond, a, b)

Потому что внутри DSL мы строим выражения, а не ветвим Kotlin-код.

Почему библиотечный подход здесь реально выигрывает

Ниже - честная табличка. Не рекламная мантра, а мой практический опыт.

Критерий

Голый AGSL

RedByteFX

Скорость старта

Порог входа высокий, особенно для Android-разработчика без опыта в шейдерах.

Заметно ниже: Kotlin-код, типы, демонстрационные примеры и стандартная библиотека.

Читаемость среднего эффекта

Быстро превращается в строковый техно-борщ.

let(...)fn(...) и именованные вспомогательные блоки делают код сюжетным.

Работа с uniform-ами

Ручные строки и ручная привязка во время работы.

Типизированные FxParam, auto-uniform-ы, нормальный биндинг.

Compose-интеграция

Можно, но требует собственной аккуратной обвязки.

FxControllerbindTimeModifier.redbyteFx уже есть.

Прозрачность исполнения

Максимальная, вы пишете AGSL напрямую.

Тоже высокая: всегда можно посмотреть agslSource().

Рефакторинг

Нервный: строковые имена, ручные замены.

Гораздо спокойнее: это обычный Kotlin-код.

Повторное использование паттернов

Копипаста или ручная поддержка вспомогательных AGSL-функций.

fn(...) и stdlib позволяют переиспользовать смысловые блоки.

Цена за удобство

Ноль абстракций, но много рутины.

Нужно освоить DSL, зато дальше работа идёт сильно быстрее.

Итог

Хорош для точечного низкоуровневого ручного контроля.

Побеждает почти во всех сценариях реальной разработки.

Где у библиотеки есть честные ограничения

  • это Android API 33+; если вам нужен старый стек, чудес не будет;

  • если вам важен полностью ручной контроль и вы предпочитаете писать шейдеры прямо на AGSL, этот путь по-прежнему открыт;

  • как и любой DSL, RedByteFX требует один раз освоить модель мышления: выражения, пространства координат, сгенерированный AGSL и параметры, привязанные к конкретному эффекту.

Но, на мой взгляд, это честная цена. Особенно если сравнить её с тем, сколько боли экономится уже на втором или третьем эффекте.

Что я в итоге хотел получить и что получилось

Мне хотелось, чтобы AGSL в Android перестал быть технологией «для тех, кто уже пережил три ритуала посвящения». Чтобы разработчик мог открыть sample, посмотреть на разные эффекты - и не подумал «ой нет, это не для меня», а подумал: «О, а я же могу это попробовать сегодня вечером!».

Если это ощущение у вас сейчас появилось, значит я всё делал не зря.

Финал

RedByteFX - библиотека свежая. Это не «всё, высечено в камне», а живой проект, который я продолжаю развивать. Поэтому мне особенно интересны люди, которым хочется не просто посмотреть, а попробовать, поругать по делу, предложить идеи, завести issue или принести PR.

Если вам близка идея сделать AGSL в Android более массовым, дружелюбным и при этом не потерять нативность исполнения - добро пожаловать. Репозиторий здесь: github.com/i-redbyte/redbytefx.

Если после знакомства с этой библиотекой у вас появилось ощущение, что эцилоп перестал бить вас по ночам, значит всё было не зря.