Android / Jetpack Compose port of
paper-design/shaders, implemented
with android.graphics.RuntimeShader and AGSL.
This repository is the Android sibling of:
- paper-shaders-swift — Swift/Metal package.
- paper-shaders-flutter — Flutter runtime-shader package.
The Android port currently includes:
simplex-noisemesh-gradientdot-orbitgod-raysvoronoiwavesspiralswirlneuro-noisestatic-mesh-gradientdot-gridstatic-radial-gradientperlin-noisecolor-panelsditheringimage-ditheringhalftone-dotshalftone-cmykpaper-texturewaterfluted-glassheatmapgem-smokeliquid-metalsmoke-ringwarpmetaballspulsing-bordergrain-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.
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.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun PreviewShader() {
SimplexNoiseView(
params = SimplexNoiseParams(
colors = listOf("#4449cf", "#ffd1e0", "#f94446"),
speed = 0.5f,
),
modifier = Modifier.fillMaxSize(),
)
}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.
./gradlew :paper-shaders-compose:testDebugUnitTest
./gradlew :sample:testDebugUnitTest
./gradlew :sample:assembleDebug
./gradlew :sample:assembleDebugAndroidTestOn a connected Android 13+ device:
ANDROID_SERIAL=adb-32091FDH3002DX-0cGmxo._adb-tls-connect._tcp \
./gradlew :paper-shaders-compose:connectedDebugAndroidTestThe 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-onlyAll 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.
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.sampler2Dbecomesuniform shaderand.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 = 41500speed = 0pixelRatio = 1- render size
512x512