📚 Documentation
KraftShade is a modern, high-performance OpenGL ES graphics rendering library for Android, designed to provide a type-safe, Kotlin-first abstraction over OpenGL ES 2.0. Built with coroutines support and a focus on developer experience, KraftShade makes complex graphics operations simple while maintaining flexibility and performance.
- Why Another Graphics Library?
- Features & Goals
- Installation
- Core Components
- Pipeline DSL
- Effect Serialization
- Support Status
- Usage
- Roadmap
- Contributing
- License
While GPUImage has been a popular choice for Android graphics processing, it comes with several limitations:
-
Java-centric Design
- Lacks Kotlin idioms and modern language features
- No coroutines support for thread (EGLContext) based operations (using GLThread)
-
Inflexible Pipeline
- Rigid filter chain architecture (GPUImageFilterGroup)
- Limited to single serial pipeline due to Bitmap-based texture inputs
- No support for memory optimizations like ping-pong buffers
-
Development Challenges
- Insufficient error handling and debugging capabilities
- Limited control over resource allocation
- Insufficient development tooling
- Limited View component support
- No active maintenance since 2021
KraftShade addresses these limitations with:
-
Modern Architecture
- Kotlin-first design with coroutines support
- Easy to use DSL for pipeline construction
- Type-safe builder pattern
- Automatic resource cleanup
-
Flexible Pipeline
- Composable effects
- Support for complex multi-pass rendering
- Efficient texture and buffer management
- On/Off-screen rendering support
- Support for parallel processing
- Automatic buffer management
-
Developer Experience
- Flexible View components & Compose integration
- Flexible input system for effect adjustment and re-rendering
- Debugging tools with named buffer references
- Comprehensive error handling
-
Performance
- Minimal overhead
- Smart resource management
- Optimized rendering pipeline
KraftShade is available on Maven Central. You can integrate it into your project using Gradle.
dependencies {
// find the latest version from the badge at the beginning of this README file
// Core library
implementation("com.cardinalblue:kraftshade:latest_version")
// Optional: Jetpack Compose integration
implementation("com.cardinalblue:kraftshade-compose:latest_version")
}
If you're using Gradle's version catalog feature, add the following to your libs.versions.toml
file:
[versions]
# find the latest version from the badge at the beginning of this README file
kraftshade = "..."
[libraries]
kraftshade-core = { group = "com.cardinalblue", name = "kraftshade", version.ref = "kraftshade" }
kraftshade-compose = { group = "com.cardinalblue", name = "kraftshade-compose", version.ref = "kraftshade" }
Then in your module's build.gradle.kts file:
dependencies {
implementation(libs.kraftshade.core)
implementation(libs.kraftshade.compose)
}
- Encapsulates all OpenGL ES environment setup in a single class
- Creates a dedicated thread dispatcher and binds the GLES context to that thread
- Ensures all OpenGL operations run in the correct context
KraftShader
: Base shader implementation with modern Kotlin featuresGlUniformDelegate
: Smart uniform handling- Supports multiple types (Int, Float, FloatArray, GlMat2/3/4)
- Deferred location query and value setting
- Optional uniforms support (useful for base class implementations)
- Automatic GLES API selection for value binding
KraftShaderTextureInput
: Simplified texture input management- Debug mode support with detailed logging and error tracking
- Bypassable shader support for conditional effect application
- Input updates (sample from ViewModel or MutableState in Compose UI)
- Reset
PipelineRunContext
which is a state you can access in the pipeline - Iterate through steps in the pipeline and render to either intermediate buffers or the final target buffer
- Intermediate buffers are TextureBuffers provided by TextureBufferPool
- For the last step, the target buffer is usually the target surface
- Flow for running one step (shader)
- Get values from input and set them as parameters of the shader
- Run the shader to render to the target buffer for the step
- Automatic recycling buffers not referenced anymore back to the pool
Input<T>
: Base input typeSampledInput<T>
: Dynamic input handlingTimeInput
: Time-based animationsMappedInput
: Value transformations
- Supports complex animations and transitions
- Example of mapped input:
fun SampledInput<Float>.bounceBetween(value1: Float, value2: Float): SampledInput<Float> { val min = min(value1, value2) val max = max(value1, value2) val interval = max - min return map { value -> val intervalValue = value % (interval * 2f) if (intervalValue < interval) { intervalValue + value1 } else { 2f * interval - intervalValue } } }
- Android Views:
KraftTextureView
: Base OpenGL rendering viewKraftEffectTextureView
: Effect-enabled viewAnimatedKraftTextureView
: Animation support with Choreographer
- Jetpack Compose Integration:
KraftShadeView
: AndroidView wrapper for KraftTextureViewKraftShadeEffectView
: AndroidView wrapper for KraftEffectTextureViewKraftShadeAnimatedView
: AndroidView wrapper for AnimatedKraftTextureView
KraftShade provides a powerful DSL for setting up rendering pipelines with different levels of complexity:
Simple linear processing chain, similar to GPUImageFilterGroup:
pipeline(windowSurface) {
serialSteps(
inputTexture = bitmap.asTexture(),
targetBuffer = windowSurface,
) {
step(SaturationKraftShader()) {
saturation = sampledInput { saturation }
}
step(HueKraftShader()) {
setHueInDegree(sampledInput { hue })
}
}
}
Complex multi-pass rendering with flexible input/output connections:
pipeline(windowSurface) {
// Create buffer references for intermediate results
val (step1Result, step2Result, blendResult) = createBufferReferences(
"graph_step1",
"graph_step2",
"graph_blend",
)
step(
Shader1(),
targetBuffer = step1Result
) { shader ->
shader.setParameter(input1.get())
}
step(
Shader2(),
targetBuffer = step2Result
) { shader ->
shader.setParameter(input2.get())
}
step(
BlendingShader().apply {
mixRatio = 0.5f
},
targetBuffer = blendResult
) { shader ->
shader.setTexture1(texture1)
shader.setTexture2(texture2)
}
}
Combine serial and graph pipelines for complex effects:
pipeline(windowSurface) {
serialSteps(
...
) {
graphStep() { inputTexture ->
// create buffer references for intermediate results
// graph steps...
// write to graphTargetBuffer which is just one of the ping-pong buffers from serialSteps scope that's provided through the child scope
}
}
}
KraftShade provides a powerful serialization system that allows you to convert complex effect pipelines into JSON format and reconstruct them later. This enables effect sharing, storage.
Important: The serialization system creates a snapshot of your pipeline at the time of serialization, capturing the static structure and parameter values that were active during serialization.
The EffectSerializer
converts pipeline setups into JSON format:
class EffectSerializer(private val context: Context, private val size: GlSize) {
suspend fun serialize(
block: suspend GraphPipelineSetupScope.() -> Unit,
): String
}
The SerializedEffect
reconstructs effects from JSON:
class SerializedEffect(
private val json: String,
private val getTextureProvider: (String) -> TextureProvider?,
) {
suspend fun applyTo(pipeline: Pipeline, targetBuffer: GlBufferProvider)
}
The serialization process follows this workflow:
graph LR
A[Pipeline Setup] --> B[EffectSerializer]
B --> C[JSON String]
C --> D[SerializedEffect]
D --> E[Reconstructed Pipeline]
F[Texture Providers] --> D
- Capture Pipeline Structure: The serializer executes your pipeline setup to collect all shader steps
- Extract Shader Information: For each step, it captures:
- Shader class name and properties
- Input texture references
- Output buffer references
- Generate JSON: Creates a structured JSON representation of the pipeline
- Reconstruction: Uses the JSON and provided texture providers to rebuild the exact same pipeline
Here's a complete example showing how to serialize and deserialize an effect:
// Define your effect pipeline
suspend fun createVintageEffect(
inputImage: Bitmap,
maskImage: Bitmap
): suspend GraphPipelineSetupScope.() -> Unit = {
val pipelineModifier = VintageGlowPipelineModifier(
equalizedImage = sampledBitmapTextureProvider("input") { inputImage },
faceMask = sampledBitmapTextureProvider("mask") { maskImage },
contrast = sampledInput { 1.2f },
brightness = sampledInput { 0.1f },
intensity = sampledInput { 0.8f }
)
pipelineModifier.apply {
addStep(
inputTexture = sampledBitmapTextureProvider("input") { inputImage },
outputBuffer = graphTargetBuffer
)
}
}
// Serialize the effect
val context = LocalContext.current
val serializer = EffectSerializer(context, GlSize(1024, 1024))
val jsonString = serializer.serialize(createVintageEffect(inputBitmap, maskBitmap))
// Deserialize and apply the effect
val serializedEffect = SerializedEffect(json = jsonString) { textureId ->
when (textureId) {
"input" -> sampledBitmapTextureProvider("input") { inputBitmap }
"mask" -> sampledBitmapTextureProvider("mask") { maskBitmap }
else -> null
}
}
// Apply to your view
state.setEffect { targetBuffer ->
createEffectExecutionProvider(serializedEffect)
}
When deserializing, you need to provide a mapping function that resolves texture names to TextureProvider
instances:
val serializedEffect = SerializedEffect(json = jsonString) { textureId ->
when (textureId) {
"input" -> sampledBitmapTextureProvider("input") { inputBitmap }
"foreground_mask" -> sampledBitmapTextureProvider("mask") { maskBitmap }
else -> {
// Handle asset textures or other resources
val bitmap = context.assets.open(textureId).use {
BitmapFactory.decodeStream(it)
}
bitmap.asTexture()
}
}
}
The serialized JSON contains an array of shader nodes, each with:
[
{
"shaderClassName": "com.cardinalblue.kraftshade.shader.SaturationKraftShader",
"shaderProperties": {
"saturation": 1.5
},
"inputs": ["input"],
"output": "BufferReference@12345"
},
{
"shaderClassName": "com.cardinalblue.kraftshade.shader.BrightnessKraftShader",
"shaderProperties": {
"brightness": 0.2
},
"inputs": ["BufferReference@12345"],
"output": "BufferReference@67890"
}
]
- Consistent Naming: Use consistent texture names (like
"input"
,"foreground_mask"
) for better maintainability - Size Considerations: Choose appropriate
GlSize
for serialization based on your target use case - Error Handling: Always provide fallback texture providers for missing resources
- Testing: Compare original and deserialized effects to ensure consistency
- Static Snapshot Only: Serialization captures fixed parameter values at serialization time - dynamic inputs like
SampledInput<T>
, animations, or time-based effects are not preserved - Sub-pipelines are not yet supported in serialization
- Complex input types beyond textures may require custom handling
- Shader properties must be serializable to JSON (primitives, arrays)
- SerializableKraftShader Interface: Implement a more flexible serialization system by defining a
SerializableKraftShader
interface. This would allow shaders to provide their own serialization information instead of the current approach where the serializer needs to recognize specific shader types likeTwoTextureInputKraftShader
. This design would give better control over serialization behavior and makeKraftShader
more flexible.
- TextureInputKraftShader (single texture input)
- ThreeTextureInputKraftShader
- TwoTextureInputKraftShader (GPUImageTwoInputFilter)
- MixBlendKraftShader (GPUImageMixBlendFilter)
- SingleDirectionForTwoPassSamplingKraftShader (GPUImageTwoPassTextureSamplingFilter)
- BypassableTextureInputKraftShader
- BypassableTwoTextureInputKraftShader
- SaturationKraftShader (GPUImageSaturationFilter)
- ContrastKraftShader (GPUImageContrastFilter)
- BrightnessKraftShader (GPUImageBrightnessFilter)
- ExposureKraftShader (GPUImageExposureFilter)
- HueKraftShader (GPUImageHueFilter)
- WhiteBalanceKraftShader (GPUImageWhiteBalanceFilter)
- GammaKraftShader (GPUImageGammaFilter)
- ColorInversionKraftShader
- GrayscaleKraftShader (GPUImageGrayscaleFilter)
- HighlightShadowKraftShader (GPUImageHighlightShadowFilter)
- ColorMatrixKraftShader (GPUImageColorMatrixFilter)
- Additionally supports color offset
- LookUpTableKraftShader (GPUImageLookupFilter)
- ColorMappingKraftShader
- For mapping specific colors to other colors
- At most 8 color mappings. If you need to map more colors, you can use it multiple times.
- RGBKraftShader (GPUImageRGBFilter)
- ColorBalanceKraftShader (GPUImageColorBalanceFilter)
- LevelsKraftShader (GPUImageLevelsFilter)
- SolarizeKraftShader (GPUImageSolarizeFilter)
- FalseColorKraftShader (GPUImageFalseColorFilter)
- MonochromeKraftShader (GPUImageMonochromeFilter)
- OpacityKraftShader (GPUImageOpacityFilter)
- PosterizeKraftShader (GPUImagePosterizeFilter)
- SepiaToneKraftShader (GPUImageSepiaToneFilter)
- ToneCurveKraftShader (GPUImageToneCurveFilter)
- VibranceKraftShader (GPUImageVibranceFilter)
- VignetteKraftShader (GPUImageVignetteFilter)
- HazeKraftShader (GPUImageHazeFilter)
- AlphaInvertKraftShader
- ApplyAlphaMaskKraftShader
- CrosshatchKraftShader (GPUImageCrosshatchFilter)
- PixelationKraftShader (GPUImagePixelationFilter)
- Sample3x3KraftShader (GPUImage3x3TextureSamplingFilter)
- ToonKraftShader (GPUImageToonFilter)
- SobelEdgeDetectionKraftShader (GPUImageSobelEdgeDetectionFilter)
- DirectionalSobelEdgeDetectionKraftShader (GPUImageDirectionalSobelEdgeDetectionFilter)
- LaplacianKraftShader (GPUImageLaplacianFilter)
- LaplacianMagnitudeKraftShader
- Convolution3x3KraftShader (GPUImage3x3ConvolutionFilter)
- Convolution3x3WithColorOffsetKraftShader
- EmbossKraftShader (GPUImageEmbossFilter)
- DilationKraftShader (GPUImageDilationFilter)
- ErosionKraftShader
- MultiplyBlendKraftShader (GPUImageMultiplyBlendFilter)
- ScreenBlendKraftShader (GPUImageScreenBlendFilter)
- NormalBlendKraftShader (GPUImageNormalBlendFilter)
- SourceOverBlendKraftShader (GPUImageSourceOverBlendFilter)
- AlphaBlendKraftShader (GPUImageAlphaBlendFilter)
- HardLightBlendKraftShader (GPUImageHardLightBlendFilter)
- SimpleMixtureBlendKraftShader
- AddBlendKraftShader (GPUImageAddBlendFilter)
- CircularBlurKraftShader
- BilateralBlurKraftShader (GPUImageBilateralBlurFilter)
- BoxBlurKraftShader (GPUImageBoxBlurFilter)
- BulgeDistortionKraftShader (GPUImageBulgeDistortionFilter)
- GaussianBlurKraftShader (GPUImageGaussianBlurFilter)
- GlassSphereKraftShader (GPUImageGlassSphereFilter)
- SphereRefractionKraftShader (GPUImageSphereRefractionFilter)
- SwirlKraftShader (GPUImageSwirlFilter)
- ZoomBlurKraftShader (GPUImageZoomBlurFilter)
- SharpenKraftShader (GPUImageSharpenFilter)
- KuwaharaKraftShader (GPUImageKuwaharaFilter)
- ColorBlendKraftShader (GPUImageColorBlendFilter)
- ColorBurnBlendKraftShader (GPUImageColorBurnBlendFilter)
- ColorDodgeBlendKraftShader (GPUImageColorDodgeBlendFilter)
- DarkenBlendKraftShader (GPUImageDarkenBlendFilter)
- DifferenceBlendKraftShader (GPUImageDifferenceBlendFilter)
- DissolveBlendKraftShader (GPUImageDissolveBlendFilter)
- DivideBlendKraftShader (GPUImageDivideBlendFilter)
- ExclusionBlendKraftShader (GPUImageExclusionBlendFilter)
- HueBlendKraftShader (GPUImageHueBlendFilter)
- LightenBlendKraftShader (GPUImageLightenBlendFilter)
- LinearBurnBlendKraftShader (GPUImageLinearBurnBlendFilter)
- LuminosityBlendKraftShader (GPUImageLuminosityBlendFilter)
- OverlayBlendKraftShader (GPUImageOverlayBlendFilter)
- SaturationBlendKraftShader (GPUImageSaturationBlendFilter)
- SoftLightBlendKraftShader (GPUImageSoftLightBlendFilter)
- SubtractBlendKraftShader (GPUImageSubtractBlendFilter)
- BilateralBlurKraftShader (GPUImageBilateralBlurFilter)
- BoxBlurKraftShader (GPUImageBoxBlurFilter)
- BulgeDistortionKraftShader (GPUImageBulgeDistortionFilter)
- GaussianBlurKraftShader (GPUImageGaussianBlurFilter)
- GlassSphereKraftShader (GPUImageGlassSphereFilter)
- SphereRefractionKraftShader (GPUImageSphereRefractionFilter)
- CGAColorspaceKraftShader (GPUImageCGAColorspaceFilter)
- HalftoneKraftShader (GPUImageHalftoneFilter)
- RGBDilationKraftShader (GPUImageRGBDilationFilter)
- SketchKraftShader (GPUImageSketchFilter)
- SmoothToonKraftShader (GPUImageSmoothToonFilter)
- NonMaximumSuppressionKraftShader (GPUImageNonMaximumSuppressionFilter)
- SobelThresholdKraftShader (GPUImageSobelThresholdFilter)
- ThresholdEdgeDetectionKraftShader (GPUImageThresholdEdgeDetectionFilter)
- WeakPixelInclusionKraftShader (GPUImageWeakPixelInclusionFilter)
- ChromaKeyBlendKraftShader (GPUImageChromaKeyBlendFilter)
- LuminanceKraftShader (GPUImageLuminanceFilter)
- LuminanceThresholdKraftShader (GPUImageLuminanceThresholdFilter)
- TransformKraftShader (GPUImageTransformFilter)
- Add KraftShade to your project (see Installation)
- Initialize logging in your Application class
- Create your first effect in a Compose UI or Android View
// Initialize in Application class
class App : Application() {
override fun onCreate() {
super.onCreate()
KraftLogger.logLevel = KraftLogger.Level.DEBUG
KraftLogger.throwOnError = true
}
}
Here's a simple example of using KraftShade with Jetpack Compose to create an image effect with adjustable saturation and brightness:
@Composable
fun ImageEffectDemo() {
val state = rememberKraftShadeEffectState()
var aspectRatio by remember { mutableFloatStateOf(1f) }
var image by remember { mutableStateOf<Bitmap?>(null) }
var saturation by remember { mutableFloatStateOf(1f) }
var brightness by remember { mutableFloatStateOf(0f) }
val context = LocalContext.current
// Load image and set aspect ratio
LaunchedEffect(Unit) {
val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.sample_image)
image = bitmap
aspectRatio = bitmap.width.toFloat() / bitmap.height.toFloat()
}
// Set effect
LaunchedEffect(Unit) {
state.setEffect { targetBuffer ->
pipeline(targetBuffer) {
serialSteps(
inputTexture = sampledBitmapTextureProvider { image.value },
targetBuffer = targetBuffer,
) {
step(SaturationKraftShader()) {
saturation = sampledInput { saturation }
}
step(BrightnessKraftShader()) {
brightness = sampledInput { brightness }
}
}
}
}
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Image preview with effects
Box(
modifier = Modifier
.fillMaxHeight(0.5f)
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
KraftShadeEffectView(
modifier = Modifier.aspectRatio(aspectRatio),
state = state
)
}
// Effect controls
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Slider(
value = brightness,
onValueChange = {
brightness = it
state.requestRender()
},
valueRange = 0f..1f
)
Slider(
value = saturation,
onValueChange = {
saturation = it
state.requestRender()
},
valueRange = 0f..2f
)
Text("Saturation: ${String.format("%.1f", saturation)}")
}
}
}
- Dynamic Shader Bypass Mechanism
- Implement a mechanism to map GlReference outputs to inputs
- Allow steps to skip shader execution based on inputs
- Optimize buffer recycling mechanism
- Improve performance by avoiding unnecessary shader executions
- Add DSL scope for shader setup
- Implement GlReference mapping state management
We welcome contributions to KraftShade! Here's how you can help:
-
Bug Reports
- Use the GitHub issue tracker
- Include detailed steps to reproduce
- Attach sample code if possible
-
Feature Requests
- Open an issue with the "enhancement" label
- Describe your use case
- Provide example code if possible
-
Pull Requests
- Fork the repository
- Create a feature branch
- Make your changes
- Include tests in the demo app to show the change is working
- Submit a pull request with a clear description
Copyright 2025 Cardinal Blue
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.