Skip to content

Repository files navigation

Paper Shaders Android

Android / Jetpack Compose port of paper-design/shaders, implemented with android.graphics.RuntimeShader and AGSL.

This repository is the Android sibling of:

Current Status

The Android port currently includes:

  • simplex-noise
  • mesh-gradient
  • dot-orbit
  • god-rays
  • voronoi
  • waves
  • spiral
  • swirl
  • neuro-noise
  • static-mesh-gradient
  • dot-grid
  • static-radial-gradient
  • perlin-noise
  • color-panels
  • dithering
  • image-dithering
  • halftone-dots
  • halftone-cmyk
  • paper-texture
  • water
  • fluted-glass
  • heatmap
  • gem-smoke
  • liquid-metal
  • smoke-ring
  • warp
  • metaballs
  • pulsing-border
  • grain-gradient

The runtime targets Android 13 / API 33+ for shader rendering. The library can compile with a lower minSdk, but the composables that instantiate RuntimeShader are annotated with @RequiresApi(33).

The deterministic default capture passes the golden comparator for all 29 upstream shaders on Pixel 7 Pro. Noise-backed shaders bind their 128x128 data texture with explicit bilinear filtering to match the upstream WebGL and Metal samplers.

ImageDitheringView accepts an Android Bitmap as its source image. The runtime binds it as a color-managed, bilinearly filtered BitmapShader and derives u_imageAspectRatio from its dimensions.

WaterView uses the same image binding and sizing contract for its caustic distortion effect.

HalftoneDotsView uses that image binding for its source photograph. Its default gooey hex-grid preset has a documented 0.05 per-shader golden threshold because WebGL and AGSL evaluate the final derivative smoothing with different fragment neighborhoods; the global threshold remains 0.02.

HalftoneCmykView combines the image and embedded noise bindings. Its default ink preset has a 0.06 per-shader threshold for the same WebGL derivative limitation; the global threshold remains 0.02.

PaperTextureView combines the same two inputs with high-frequency paper fibers. Its 0.025 per-shader allowance covers backend sampling variance.

FlutedGlassView, HeatmapView, GemSmokeView, and LiquidMetalView cover the remaining image-backed shaders. fluted-glass and gem-smoke use narrow 0.03 thresholds for blur and mask sampling variance; heatmap and liquid-metal pass the global threshold.

Install From GitHub

The Android port is distributed as source from tagged GitHub releases. Check out the repository at the desired tag, then include its library module from your application's settings.gradle.kts:

include(":paper-shaders-compose")
project(":paper-shaders-compose").projectDir =
    file("../paper-shaders-android/paper-shaders-compose")

Add the module dependency to the application:

dependencies {
    implementation(project(":paper-shaders-compose"))
}

Releases follow semantic version tags. The current release is 0.0.3.

Minimal Usage

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun PreviewShader() {
    SimplexNoiseView(
        params = SimplexNoiseParams(
            colors = listOf("#4449cf", "#ffd1e0", "#f94446"),
            speed = 0.5f,
        ),
        modifier = Modifier.fillMaxSize(),
    )
}

Render quality and metrics

Wrap any typed shader view in PaperShaderRenderHost to cap its intermediate pixel count, enable adaptive resolution, and receive throttled render metrics:

var metrics by remember { mutableStateOf(ShaderRenderMetrics.Empty) }

PaperShaderRenderHost(
    options = ShaderRenderOptions.adaptive(targetFrameRate = 60f),
    onMetricsChanged = { metrics = it },
) {
    SimplexNoiseView(modifier = Modifier.fillMaxSize())
}

Adaptive rendering follows the Metal port's conservative controller: it lowers the offscreen render scale quickly after sustained missed frames and raises it more slowly when headroom returns. maxPixelCount remains a hard cap. Fixed quality is the default, so existing integrations keep their current behavior.

AGSL does not expose Metal-style compiler switches for fast versus precise math. Android documents float and vec* as high-precision single precision, while half is medium precision. Paper Shaders uses float/vec* throughout; the runtime reports this as float/highp and does not offer a misleading math mode toggle.

Local Checks

./gradlew :paper-shaders-compose:testDebugUnitTest
./gradlew :sample:testDebugUnitTest
./gradlew :sample:assembleDebug
./gradlew :sample:assembleDebugAndroidTest

On a connected Android 13+ device:

ANDROID_SERIAL=adb-32091FDH3002DX-0cGmxo._adb-tls-connect._tcp \
  ./gradlew :paper-shaders-compose:connectedDebugAndroidTest

The instrumented test writes deterministic 512x512 captures to /sdcard/Pictures/PaperShadersAndroid/run-<timestamp>/. Pull one run and compare it against the workbench goldens:

adb -s adb-32091FDH3002DX-0cGmxo._adb-tls-connect._tcp \
  pull /sdcard/Pictures/PaperShadersAndroid/run-<timestamp> captures/device/run-<timestamp>

python3 tools/compare_android_captures.py \
  --captures captures/device/run-<timestamp> \
  --goldens ../paper-shaders-prd/golden \
  --warn-only

All upstream shaders pass the Pixel 7 Pro comparator using the default 0.02 mean-difference threshold, except for the narrow per-shader allowances documented in docs/AGSL-PARITY.md. Use --warn-only only while investigating a new port or a regression.

Design Notes

AGSL is close to GLSL, but it runs inside Android's Skia graphics pipeline. Important differences from the WebGL source:

  • main(float2 fragCoord) receives local Canvas coordinates.
  • sampler2D becomes uniform shader and .eval(...).
  • Bitmap shader coordinates are pixel coordinates, not normalized UVs.
  • Returned colors must be premultiplied alpha.
  • The upstream vertex-shader sizing logic is inlined into AGSL helpers.

Golden parity should be checked against ../paper-shaders-prd/golden with the same deterministic values used by the Swift and Flutter ports:

  • frame = 41500
  • speed = 0
  • pixelRatio = 1
  • render size 512x512

Releases

Packages

Contributors

Languages