Kotlin DSL für AGSL-Shader in Android: Typensichere Umsetzung
Bei der Entwicklung von AGSL-Shadern für Android mit RuntimeShader und RenderEffect stoßen Sie oft auf Probleme wie String-Literale, manuelle Uniform-Bindings und schwache IDE-Unterstützung. Kotlin DSL löst diese Probleme, indem es natives AGSL-Code generiert – ohne Performanceeinbußen. Die Bibliothek wandelt typisierte Konstrukte in einsatzbereite Shader um und bietet über agslSource() Zugriff auf rohen AGSL-Code.
So können Sie sich auf die Effektlogik konzentrieren, statt auf String-Syntax. Unterstützt API 33+, integriert sich nahtlos in Compose und enthält eine Standardbibliothek mit prozeduralen Primitiven.
Pain Points beim rohen AGSL in Kotlin
Shader als Strings verlieren IDE-Vorteile wie Autovervollständigung, Refactoring und Typprüfung. Uniforms müssen manuell deklariert werden – Namensfehler lassen Ihre App abstürzen. Komplexe Effekte mit smoothstep, fract und mix werden zu unlesbarem Chaos. In Compose kommt noch Boilerplate für Zeit und Updates hinzu.
Die DSL beseitigt diese Hürden: Uniforms sind typisiert, Zwischenergebnisse lassen sich mit let() benennen, und Standardfunktionen werden deklarativ aufgerufen.
Bibliothek hinzufügen
Fügen Sie diese Dependencies in Ihre build.gradle ein:
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")
}
Vergleich: Wellenverdrängung
Roh-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);
}
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)
}
Der generierte AGSL-Code entspricht exakt der Logik, aber Ihr Quellcode ist typisiert und lesbar.
Effektbeispiele nach Komplexität
1. Welle: Basis-Effekt
Einfache Y-Versetzung mit sin. Ideal zum Lernen von fragCoord, sample(), Uniforms und let() zur Benennung von Schritten ohne unnötige Komplexität.
2. Signal: Funktionen und Masken
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() extrahiert benutzerdefinierte Funktionen, gridMask() und scanlines() aus der Stdlib vereinfachen Masken. ifElse() hält alles deklarativ.
3. Radar: Polarkoordinaten
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() und ringMask() aus der Stdlib ersetzen manuelle smoothstep-Ketten. maskedScreen() und maskedOverlay() vereinfachen Blending.
4. Metaballs: Prozedurale Formen
Dieser Effekt baut SDF-Kreise mit geglättetem min für Neon-Kleckse. Die DSL meistert vollständige prozedurale Effekte und hält komplexe Geometrie lesbar.
Wichtige Erkenntnisse
- Kotlin DSL generiert natives AGSL ohne Overhead – prüfen Sie mit
agslSource(). - Typisierte Uniforms und
let()beheben String-Probleme und Refactoring-Schwierigkeiten. - Standardbibliothek (
gridMask,polarCoordinates,arcMask) spart Zeit bei Primitiven. fn()undifElse()ermöglichen Modularität ohne imperativen Stil.- Compose-Integration über
autoUniformTime()und Parameter-Binding.
— Editorial Team
Noch keine Kommentare.