TextureCanvas
Introduction
The TextureCanvas
is a utility class allowing dynamic creation of textures at runtime. Acting as a wrapper around the Texture
class, it provides a simpler API for simple texture manipulation.
Motivation
Stride is a very versatile and powerful engine which uses textures for a lot of data processing. Sometimes it is necessary to create these textures on the fly for example for simple tasks like scaling, applying image effects or to build new textures based on existing ones (texture atlas).
TextureCanvas
The TextureCanvas abstracts away a lot of complexity behind custom buffers, textures, memory allocation and instead provides you with a simpler builder API to define your texture dynamically.
var renderContext = RenderContext.GetShared(game.Services);
using var canvas = new TextureCanvas(renderContext);
Note
Notice the using
at the front. This part is crucial as you should always dispose of your texture canvas when it's no longer needed. The canvas utilizes multiple image buffers behind the scenes, and failing to dispose of it prevents the release of these resources, which can lead to memory leaks.
Initialization
Most operations start with a single texture as a source so we need to load the texture first using the traditional texture api.
var directory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!;
var filePath = Path.Combine(directory, "input.png");
using var input = File.Open(filePath, FileMode.Open);
var texture = Texture.Load(game.GraphicsDevice, input);
This code loads an input image from the local file system and stores it as a temporary texture
Loading
In case you just want to manipulate a texture directly you can also use the similar api from the TextureCanvas
directly.
var directory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!;
var filePath = Path.Combine(directory, "input.png");
canvas.Load(filePath)
This would load the same texture directly as a buffer for further processing. Similar methods also exist for file streams or data buffers accordingly.
Storing
To get the resulting image you have the option to save the image to a file, stream or array depending on your requirements.
var directory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!;
var filePath = Path.Combine(directory, "input.png");
using var output = File.Open(filePath, FileMode.Open);
canvas.Store(output, ImageFileType.Png);
The most common approach however would be to get the result as a texture you can use for your UI or Materials.
var output = canvas.ToTexture();
Note
Notice that you shouldn't use these methods every frame as it involves GPU < - > CPU transfers which can block your game.
Manipulation
The TextureCanvas
supports various image manipulations as well as image effects.
Resample
The Resample method changes the current size of the texture or pixel format while preserving the current content. The content will be resized to match the new size if neccesary.
canvas.Resample(size: new Size2(512, 512), pixelFormat: PixelFormat.R8G8B8A8_UNorm)
BrightFilter
The BrightFilter method a BrightFilter filter to the image
canvas.BrightFilter(threshold: 0.2f, steepness: 1.0f)
GaussianBlur
The GaussianBlur method applies a GaussianBlur effect to the image
canvas.GaussianBlur(radius: 4, sigmaRatio: 2.0f)
CoCMapBlur
The CoCMapBlur method applies a CoCMapBlur effect to the image
canvas.CoCMapBlur(radius: 4)
Transform
The Transform method applies a ColorTransformGroup effect to the image
canvas.Transform(transforms: new[] { new LuminanceToChannelTransform{ ColorChannel = ColorChannel.R } })
Combine
The Combine method applies a ColorCombiner effect to the image
canvas.Combine(
textures: new [] { sourceTexture1, sourceTexture2, null /* = Canvas Content */ },
factors: new [] { .2f, .2f, .6f }
);
Colorize
The Colorize method multiplies the current canvas content by a color to create a modulated image.
canvas.Colorize(colorMultiplier: new Color4(1f, 0, 0));
Recolorize
The Recolorize method works the same way as colorize but only uses the red-channel as an input. The color multiplier is optional, omitting it would change the image to a gray-scale image.
canvas.Recolorize(colorMultiplier: new Color4(1f, 0, 0));
Apply
The Apply method applies a custom ImageEffect to the image
using var effect = new GaussianBlur();
canvas.Apply(effect);
Drawing
Sometimes you also want to customize which parts of a source texture is applied to which part of the canvas. You can also compose multiple texture into a single one using these methods.
Stretch
This enum controls the stretch mode when resampling the source rect to the target rect
- None: The texture preserves its original size. Overflowing content is cropped.
- Stretch: The texture is resized to fill the destination dimensions. The aspect ratio is not preserved.
- Contain: The texture is resized to fit in the destination dimensions while it preserves its native aspect ratio.
- Cover: The texture is resized to fill the destination dimensions while it preserves its native aspect ratio. If the aspect ratio of the destination rectangle differs from the source, the source texture is clipped to fit in the destination dimensions.
Anchors
This enum controls the alignment mode if clipping or padding is necessary. How the anchors behave is dependant on the selected stretch mode.
- TopLeft: Adjust the position so the top-left corner of the source and target rect are aligned.
- Top: Adjust the position so the top-edge center of the source and target rect are aligned.
- TopRight: Adjust the position so the top-right corner of the source and target rect are aligned.
- Left: Adjust the position so the left-edge center of the source and target rect are aligned.
- Center: Adjust the position so the center of the source and target rect are aligned.
- Right: Adjust the position so the right-edge center of the source and target rect are aligned.
- BottomLeft: Adjust the position so the bottom-left corner of the source and target rect are aligned.
- Bottom: Adjust the position so the bottom-edge center of the source and target rect are aligned.
- BottomRight: Adjust the position so the bottom-right corner of the source and target rect are aligned.
Draw
There are many overloads to the draw method but all of them follow this basic structure.
canvas.DrawTexture(
texture,
[SourceRect],
[TargetRect],
[ColorMultiplier],
[Stretch],
[Anchor],
[SamplingPattern]
);
Note
Notice the options for stretch and anchors these control how the texture is converted from the source rectangle to the target one.
Examples
Stretch | Anchor | Source Rect | Target Rect | Result |
---|---|---|---|---|
None | TopLeft | |||
None | Top | |||
None | TopRight | |||
None | Left | |||
None | Center | |||
None | Right | |||
None | BottomLeft | |||
None | Bottom | |||
None | BottomRight | |||
Stretch | Any | |||
Stretch | Any | |||
Contain | AnyLeft | |||
Contain | AnyRight | |||
Contain | AnyTop | |||
Contain | AnyBottom | |||
Contain | AnyCenter | |||
Cover | AnyLeft | |||
Cover | AnyRight | |||
Cover | AnyTop | |||
Cover | AnyBottom | |||
Cover | AnyCenter |
View on GitHub.
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Extensions;
using Stride.CommunityToolkit.Rendering.Utilities;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Graphics;
using Stride.Rendering.Images;
using Stride.Rendering.Sprites;
using Stride.UI;
using Stride.UI.Controls;
using Stride.UI.Panels;
using System.Reflection;
using var game = new Game();
game.Run(start: Start);
static void Start(Game game)
{
game.Window.SetSize(new Int2(1000, 1080));
game.SetupBase();
var directory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!;
var filePath = Path.Combine(directory, "input.png");
using var input = File.Open(filePath, FileMode.Open);
var texture = Texture.Load(game.GraphicsDevice, input);
var grid = new UniformGrid
{
Width = 1000,
Height = 1000,
Columns = 9,
Rows = 9,
Margin = new Thickness(8, 8, 8, 8)
};
grid.Children.Add(CreateCard(texture));
for (var a = 0; a < 9; a++)
{
var anchor = (TextureCanvas.Anchor)a;
for (var s = 0; s < 4; s++)
{
var stretch = (TextureCanvas.Stretch)s;
using (var canvas = game.CreateTextureCanvas(new Size2(1024, 1024)))
{
canvas.DrawTexture(texture, new Rectangle(0, 128, 256, 256), new Rectangle(128, 256, 768, 512), null, stretch, anchor, SamplingPattern.Expanded);
var card = CreateCard(canvas.ToTexture());
card.SetGridColumn(a);
card.SetGridRow(s * 2 + 1);
grid.Children.Add(card);
}
using (var canvas = game.CreateTextureCanvas(new Size2(1024, 1024)))
{
canvas.DrawTexture(texture, new Rectangle(0, 128, 256, 256), new Rectangle(256, 128, 512, 768), null, stretch, anchor);
var card = CreateCard(canvas.ToTexture());
card.SetGridColumn(a);
card.SetGridRow(s * 2 + 2);
grid.Children.Add(card);
}
}
}
var entity = new Entity { Scene = game.SceneSystem.SceneInstance.RootScene };
entity.Add(new UIComponent { Page = new UIPage { RootElement = grid } });
}
static Border CreateCard(Texture texture)
{
var card = new Border
{
BorderColor = new Color(25, 25, 25),
BackgroundColor = new Color(120, 120, 120),
BorderThickness = new Thickness(2, 2, 2, 2),
Padding = new Thickness(8, 8, 8, 8),
Margin = new Thickness(4, 4, 4, 4),
Content = new StackPanel
{
Orientation = Orientation.Vertical,
Children =
{
new ImageElement
{
Source = new SpriteFromTexture { Texture = texture }
}
}
}
};
return card;
}