Real-Time Camera Filters with Hardware-Accelerated Shaders in .NET MAUI
Building a cross-platform camera app with real-time SKSL shader filters using DrawnUI's SkiaCamera control for .NET MAUI.
Real-Time Camera Filters with Hardware-Accelerated Shaders in .NET MAUI
Meet SkiaCamera: A Drawn Control for .NET MAUI
If you’ve been following my previous articles you might recall that a SkiaCamera
control was already used for an Android ML photo recognition app. Today I’m happy to announce that this control now supports Android, iOS, Mac Catalyst and Windows. You could use it inside a classic .NET MAUI app by wrapping it into a DrawnUI Canvas.
SkiaCamera provides camera preview that seamlessly integrates with DrawnUI’s hardware-accelerated visual effects system, allowing you to do whatever you please with live camera feed and to process still captured photos with all the power of SkiaSharp.
It looks perfect for any camera data processing, from sending cropped thumbnails to ML/AI to displaying previews in any imaginable way, including any drawn UI around. We will use it for creating that nice combination of Skia SKSL shaders with camera feed.
Along with the app we will discuss you could also see another SkiaCamera
usage example inside the DrawnUI Demo app.
Use-Case: Real-Time Camera Effects
Why a Drawn App?
Since I had a lot of fun playing with SKSL shaders previously I definitely had to try them with the drawn camera feed! Would it lag? Can Skia render a fluid video flow with shaders applied?
SkiaSharp running with hardware acceleration did indeed provide smooth rendering. On low-range Android devices the FPS is fine in Release builds. On mid-range Android phones, performance is good even with Debug builds.
I have created a cross-platform app to make this technology available for an easy demonstration. You could install Filters Camera
from AppStore and GooglePlay as we would further discuss the development process behind it. The app also runs on Windows and on Mac, but you would need to compile it yourself. Link to complete source code is at the end of this article.
Camera Display
Basically camera control contains a customizable SkiaImage
that displays the current camera frame. It is accessed via Display
property. This image could be scaled to Fit or Fill the viewport, or transformed, for example, to flip horizontally as you see in a lot of Mirror apps. The control receives image data from native code and sets as source to Display
. The whole rendered by the usual DrawnUI pipeline with layout, transforms and effects.
In this app we have subclassed SkiaCamera
, to create a CameraWithEffects
that simply auto-adds shader effect to Display
.
1
2
3
4
5
6
7
8
9
10
11
12
13
// create a specific shader processing visual effect
_shader = new ClippedShaderEffect(Display)
{
ShaderSource = shader.Filename,
};
//apply to self
if (_shader != null && !VisualEffects.Contains(_shader))
{
VisualEffects.Add(_shader); //that's it, the rendering pipeline
// will apply the shader to camera control display
}
ClippedShaderEffect
is a subclassed “standard” SkiaShaderEffect
with some additional clipping applied. When playing with Fit and Fill modes I realized we needed to clip the rendering area of the camera display it to the actual image area to avoid applying shader to black borders that appear in case of Fit. SkiaImage
provides us with DisplayRect
property that returns the real rect taken by the scaled image source on the canvas. While the whole image control would take its DrawingRect
area with borders included.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ClippedShaderEffect : SkiaShaderEffect
{
// _image is a reference to camera Display property
// that holds the current frame unprocessed preview image
public override void Render(DrawingContext ctx)
{
if (_image != null)
{
var clipped = ctx.Destination;
clipped.Intersect(new ((int)Math.Round(_image.DisplayRect.Left),
(int)Math.Round(_image.DisplayRect.Top),
(int)Math.Round(_image.DisplayRect.Right),
(int)Math.Round(_image.DisplayRect.Bottom)));
base.Render(ctx.WithDestination(clipped));
}
}
}
Captured Photo
Previewing is one thing, but we also need to save processed full-size captured still photo with same effects applied!
The trick is to use RenderCapturedPhotoAsync
helper method provided by SkiaCamera. It runs the passed as Action
logic on the rendering thread this allows us to use GPU acceleration even for “offscreen” rendering.
1
2
3
4
5
6
7
8
9
10
11
12
13
var finalImage = await Camera.RenderCapturedPhotoAsync(captured, null, image =>
{
if (SelectedShader != null)
{
var shaderEffect = new SkiaShaderEffect()
{
ShaderSource = SelectedShader.Filename
};
image.VisualEffects.Add(shaderEffect);
}
}, true); //true to use GPU acceleration
...
We are also injecting custom EXIF data to indicate camera model and name as our app name.
1
2
3
4
5
captured.Image = finalImage;
captured.Meta.Vendor = "DrawnUI";
captured.Meta.Model = "Shaders Camera";
//SkiaCamera helper method does all the magic for us
await Camera.SaveToGalleryAsync(captured, false);
For future works I’m thinking to add the selected filter name into some of the additional fields. There is a lot of possible improvements to be made, current state is a starting point: please feel free to contribute!
SKSL Shaders
What is SKSL?
SKSL (Skia Shading Language) is a close cousin to GLSL. GLSL shaders can be easily ported to SKSL, especially with the help of LLMs. I follow the concept of using standard uniforms you would see as Shader Inputs at https://shaders.skia.org almost same are used at https://www.shadertoy.com.
I have created all of the shaders used in the app with the help of LLMs. Interestingly Claude Sonnet 4 was able to really replicate the specifics of several well known black and white films, what was confirmed by professional photographers. I didn’t put exact film names into effects titles though not to overwhelm an average user.
Some complex shaders required several days of work and tuning, like those that create “drawn” effects.
If you dig inside included shaders code you would see a base for combining reusable functions across different shaders. For example few of them apply a zoom lens effect: in theory our app could apply them to every shader, we could let users select a lens to be applied on top of any of color effects.
Desktop SKSL Editor
The question arose very rapidly on how to develop shaders for the app, to be able to make small tuning changes and instantly see the result, without recompiling the app while shaders are shipped as files.
We have this wonderful MAUI feature for desktop as to open new app pages in stand-alone windows and this has brought the SKSL Editor to life: on desktop you need to long-press the shader preview in the drawer menu to make the editor appear in a new window.
Working inside shader editor on Windows PC and a webcam with my daughter helping me much.
The editor allows you to edit and to apply the code to the running camera preview, so that you can see the result in real-time. It would also show you compilation errors so you can copy and paste them to LLM if unsure about how to fix.
Of course to reuse the final code you would need to paste it to the embedded file within the Raw
folder. If we were developing a dedicated editor app, we could go with exporting all shaders to app cache folder once and then editor would modify them at run-time. Another limitation of the current quick implementation is that the app wouldn’t use the edited code for saving the photo, only for the live preview: enough for our development needs.
The Development Experience
Initially all the UI was created in XAML making use of .NET MAUI XAML HotReload, and the developing workflow was very smooth. Just before publishing I was playing with speed optimizations and ended up re-creating the main page in code-behind with fluent extensions looking for a faster app startup.
So the source code actually has both XAML and code-behind versions of the main page with camera, could be quite interesting and maybe useful for some developers to compare.
I used MAUI popups for Settings and the Help screen. For that matter I went with the FastPopups library created not so long ago. The reason for this was to be confident with the popup full-screen behaviour on all platforms, as well as the HotReload support.
Final Thoughts
SkiaSharp strikes again with some impressive features it provides. Our app demonstrated that .NET MAUI, combined with SkiaSharp, can deliver quality applications with complex rendering.
This experiment ended up showing incredible opportunities shaders provide to data processing. I’m saying data and not image, because we could work with any data using shaders GPU-accelerated processing. Already existing complex data/image processing C# code could be replaced with SKSL hardware-accelerated shaders all around the .NET ecosystem. Thanks to SkiaSharp v3.
The current codebase opens visible possibilities for creating GPU-accelerated image and video editors, on desktop and mobile, with .NET MAUI. Video recording is the next step on the SkiaCamera
roadmap.
I hope this article and its codebase would inspire you to use the drawn approach in creating NET MAUI apps and maybe help create another canvas-drawn camera app of your own!
Source Code
The complete source code is released under MIT license, you can clone, build, and start experimenting. It includes all the SKSL shaders and the desktop editor.
Please feel free to contribute your own shaders and any improvements!
Links and Resources
- ShadersCamera Repository - Complete source code and setup instructions
- AppStore - install on iPhone and iPad
- GooglePlay - Install on Android
- SkiaCamera Control - The camera preview control used in the app
- DrawnUI Demo - Another example of using SkiaCamera
- DrawnUI for .NET MAUI - The rendering engine powering the camera effects
- FastPopups for .NET MAUI - High-performance popup library
- SKSL Documentation - Official Skia Shading Language reference
- SkiaSharp - The foundation that makes it all possible
The author is available for consulting on drawn applications and custom controls for .NET MAUI. If you need help creating custom UI experiences, optimizing performance, or building entirely drawn apps, feel free to reach out.