Kotlin DSL dla shaderów AGSL w Androidzie: podejście typizowane
Tworzenie shaderów AGSL dla Androida z RuntimeShader i RenderEffect często napotyka problemy ze stałymi łańcuchami znaków, ręcznym wiązaniem uniformów i słabym wsparciem IDE. Kotlin DSL rozwiązuje te kwestie, generując natywny kod AGSL bez utraty wydajności. Biblioteka przekształca typizowane konstrukcje w gotowe shadery, zachowując dostęp do oryginalnego AGSL poprzez metodę agslSource().
Dzięki temu możesz skupić się na logice efektów, a nie na składni ciągów znaków. Wsparcie dla API 33+, integracja z Compose i standardowa biblioteka prymitywów proceduralnych.
Problemy czystego AGSL w Kotlinie
Shadery zapisane jako ciągi znaków tracą zalety IDE: autouzupełnianie, refaktoryzację, sprawdzanie typów. Uniformy deklaruje się ręcznie, błędy w nazwach powodują awarie. Złożone efekty z smoothstep, fract, mix rozrastają się w nieczytelny kod. W Compose dochodzi jeszcze obsługa czasu i aktualizacji.
DSL eliminuje te bariery: uniformy są typizowane, pośrednie obliczenia nazywane przez let(), standardowe funkcje wywoływane deklaratywnie.
Podłączenie biblioteki
Dodaj zależności w build.gradle:
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")
}
Porównanie: przesunięcie falowe
Czysty 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);
}
W Kotlin DSL
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)
}
Wygenerowany AGSL jest identyczny logicznie, ale kod źródłowy typowany i czytelny.
Przykłady efektów według złożoności
1. Wave: podstawowy efekt
Proste przesunięcie po Y z sin. Idealne do nauki fragCoord, sample(), uniform. let() nazywa kroki bez komplikowania.
2. Signal: funkcje i maski
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() wydziela funkcje, gridMask() i scanlines() ze stdlib upraszczają maski. ifElse() zachowuje deklaratywność.
3. Radar: współrzędne polarne
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)
}
polarCoordinates(), arcMask(), ringMask() ze stdlib zastępują ręczne smoothstep. maskedScreen() i maskedOverlay() upraszczają kompozycję.
4. Metaballs: proceduralne kształty
Efekt buduje SDF-kręgi ze smoothed min dla neonowych skupisk. DSL nadaje się do pełnoprawnych efektów proceduralnych, zachowując czytelność złożonej geometrii.
Co ważne
- Kotlin DSL generuje natywny AGSL bez narzutu, weryfikowalny przez agslSource().
- Typizowane uniformy i let() rozwiązują problemy kodu stringowego i refaktoryzacji.
- Standardowa biblioteka (gridMask, polarCoordinates, arcMask) oszczędza czas na prymitywy.
- fn() i ifElse() wspierają modułowość bez imperatywizmu.
- Integracja z Compose przez autoUniformTime() i wiązanie parametrów.
— Editorial Team
Brak komentarzy.