Back to Home

Kotlin DSL for AGSL shaders Android

Kotlin DSL simplifies AGSL shader development for Android, replacing string literals with typed constructs. Examples of effects wave, signal, radar using stdlib. Native performance and integration with Compose preserved.

DSL for AGSL shaders: from strings to types in Android
Advertisement 728x90

Kotlin DSL for AGSL Shaders in Android: A Typed Approach

Developing AGSL shaders for Android with RuntimeShader and RenderEffect often hits roadblocks like string literals, manual uniform binding, and poor IDE support. Kotlin DSL fixes these issues by generating native AGSL code without performance hits. The library turns typed constructs into ready-to-use shaders while keeping access to raw AGSL via agslSource().

This lets you focus on effect logic instead of string syntax. Supports API 33+, integrates with Compose, and includes a standard library of procedural primitives.

Pain Points of Raw AGSL in Kotlin

Shaders as strings lose IDE perks like autocomplete, refactoring, and type checking. Uniforms are declared manually—name mismatches crash your app. Complex effects with smoothstep, fract, and mix balloon into unreadable messes. In Compose, you add extra boilerplate for time and updates.

Google AdInline article slot

DSL removes these hurdles: uniforms are typed, intermediate calculations get named via let(), and standard functions are called declaratively.

Adding the Library

Add these dependencies to your 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")
}

Comparison: Wave Displacement

Raw 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)
}

The generated AGSL matches the logic exactly, but your source code is typed and readable.

Google AdInline article slot

Effect Examples by Complexity

1. Wave: Basic Effect

Simple Y-offset with sin. Perfect for learning fragCoord, sample(), uniforms, and let() to name steps without extra complexity.

2. Signal: Functions and Masks

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() extracts custom functions, gridMask() and scanlines() from the stdlib simplify masks. ifElse() keeps things declarative.

3. Radar: Polar Coordinates

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(), and ringMask() from stdlib replace manual smoothstep chains. maskedScreen() and maskedOverlay() streamline blending.

Google AdInline article slot

4. Metaballs: Procedural Shapes

This effect builds SDF circles with smoothed min for neon blobs. DSL handles full procedural effects while keeping complex geometry readable.

Key Takeaways

  • Kotlin DSL generates native AGSL with zero overhead—verify via agslSource().
  • Typed uniforms and let() fix string-based code and refactoring woes.
  • Standard library (gridMask, polarCoordinates, arcMask) saves time on primitives.
  • fn() and ifElse() enable modularity without going imperative.
  • Compose integration via autoUniformTime() and parameter binding.

— Editorial Team

Advertisement 728x90

Read Next