Important

This functionality is only available on WinAppSDK and Skia Desktop (netX.0-desktop) targets that are running on platforms with support for hardware acceleration. On Windows and Linux, OpenGL is used directly and on macOS, Metal is used through the ANGLE library.

GLCanvasElement is a control for drawing 3D graphics with OpenGL. It can be enabled by adding the GLCanvas UnoFeature. The OpenGL APIs provided are provided by Silk.NET.

Using the GLCanvasElement

To use GLCanvasElement, create a subclass of GLCanvasElement and override the abstract methods Init, RenderOverride and OnDestroy.

protected GLCanvasElement(Func<Window> getWindowFunc);

protected abstract void Init(GL gl);
protected abstract void RenderOverride(GL gl);
protected abstract void OnDestroy(GL gl);

These three abstract methods take a Silk.NET.OpenGL.GL parameter that can be used to make OpenGL calls.

The GLCanvasElement constructor

The protected constructor requires a Func<Window> argument that fetches the Microsoft.UI.Xaml.Window object that the GLCanvasElement belongs to. This function is required because WinUI doesn't yet provide a way to get the Window of a FrameworkElement. This parameter is ignored on Uno Platform and must be set to null. This function is only called while the GLCanvasElement is still in the visual tree.

The Init method

The Init method is a regular OpenGL setup method that you can use to set up the needed OpenGL objects, like textures, Vertex Array Buffers (VAOs), Element Array Buffers (EBOs), etc. The OnDestroy method is the complement of Init and is used to clean up any allocated resources. Init and OnDestroy might be called multiple times alternatingly. In other words, 2 OnDestroy calls are guaranteed to have an Init call in between and vice versa.

The RenderOverride method

The RenderOverride is the main render-loop function. When adding your drawing logic in RenderOverride, you can assume that the OpenGL viewport rectangle is already set and its dimensions are equal to the RenderSize of the GLCanvasElement.

macOS Specifics

On MacOS, since OpenGL support is not natively present, we use ANGLE to provide OpenGL ES support. This means that we're actually using OpenGL ES 3.00, not OpenGL. Due to the similarity between desktop OpenGL and OpenGL ES, (almost) all the OpenGL ES functions are present in the Silk.NET.OpenGL.GL API surface and therefore we can use the same class to represent both the OpenGL and OpenGL ES APIs. To run the same GLCanvasElement subclasses on all supported platforms, make sure to use a subset of functions that are present in both APIs (which is almost all of OpenGL ES).

Invalidating the canvas

Additionally, GLCanvasElement has an Invalidate method that requests a redrawing of the GLCanvasElement, calling RenderOverride in the process. Note that RenderOverride will only be called once per Invalidate call and the output will be saved to be used in future frames. To update the output, you must call Invalidate. If you need to continuously update the output (e.g. in an animation), you can add an Invalidate call inside RenderOverride.

How to use Silk.NET

To learn more about using Silk.NET as a C# binding for OpenGL, see the examples in the Silk.NET repository here. Note that the windowing and inputs APIs in Silk.NET are not relevant to GLCanvasElement, since we only use Silk.NET as an OpenGL binding library, not a windowing library.

Full example

To see this in action, here's a complete sample that uses GLCanvasElement to draw a triangle. Note how you have to be careful with surrounding all the OpenGL-related logic in platform-specific guards. This is the case for both the XAML and the code-behind. For complete C# projects, visit our GLCanvasElement Samples in the Uno.Samples repository.

XAML:

<!-- GLCanvasElementExample.xaml -->
<UserControl x:Class="BlankApp.GLCanvasElementExample"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:BlankApp"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:skia="http://uno.ui/skia#using:UITests.Shared.Windows_UI_Composition"
             xmlns:not_skia="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:not_win="http://uno.ui/not_win"
             mc:Ignorable="skia not_win">

    <Grid>
        <skia:SimpleTriangleGlCanvasElement />
        <win:Grid>
            <local:SimpleTriangleGlCanvasElement />
        </win:Grid>
        <not_win:Grid>
            <not_skia:TextBlock Text="This sample is only supported on skia targets and WinUI." />
        </not_win:Grid>
    </Grid>
</UserControl>

Code-behind:

// GLCanvasElementExample.xaml.cs
public partial class GLCanvasElementExample : UserControl
{
    public GLCanvasElementExample()
    {
        this.InitializeComponent();
    }
}
// GLTriangleElement.cs
#if DESKTOP || WINDOWS
// https://learnopengl.com/Getting-started/Hello-Triangle
public class SimpleTriangleGlCanvasElement()
    // Assuming that App.xaml.cs has a static property named MainWindow
    : GLCanvasElement(() => App.MainWindow)
{
    private uint _vao;
    private uint _vbo;
    private uint _program;

    unsafe protected override void Init(GL gl)
    {
        _vao = gl.GenVertexArray();
        gl.BindVertexArray(_vao);

        float[] vertices =
        {
            0.5f, -0.5f, 0.0f,  // bottom right
            -0.5f, -0.5f, 0.0f,  // bottom left
            0.0f,  0.5f, 0.0f   // top
        };

        _vbo = gl.GenBuffer();
        gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
        gl.BufferData(BufferTargetARB.ArrayBuffer, new ReadOnlySpan<float>(vertices), BufferUsageARB.StaticDraw);

        gl.VertexAttribPointer(0, 3, GLEnum.Float, false, 3 * sizeof(float), (void*)0);
        gl.EnableVertexAttribArray(0);

        var slVersion = gl.GetStringS(StringName.ShadingLanguageVersion);
        var versionDef = slVersion.Contains("OpenGL ES", StringComparison.InvariantCultureIgnoreCase)
            ? "#version 300 es"
            : "#version 330";
        var vertexCode =
        $$"""
        {{versionDef}}
        precision highp float; // for OpenGL ES compatibility

        layout (location = 0) in vec3 aPosition;
        out vec4 vertexColor;

        void main()
        {
         gl_Position = vec4(aPosition, 1.0);
         vertexColor = vec4(aPosition.x + 0.5, aPosition.y + 0.5, aPosition.z + 0.5, 1.0);
        }
        """;

        var fragmentCode =
        $$"""
        {{versionDef}}
        precision highp float; // for OpenGL ES compatibility

        out vec4 out_color;
        in vec4 vertexColor;

        void main()
        {
            out_color = vertexColor;
        }
        """;

        uint vertexShader = gl.CreateShader(ShaderType.VertexShader);
        gl.ShaderSource(vertexShader, vertexCode);
        gl.CompileShader(vertexShader);

        gl.GetShader(vertexShader, ShaderParameterName.CompileStatus, out int vStatus);
        if (vStatus != (int)GLEnum.True)
        {
            throw new Exception("Vertex shader failed to compile: " + gl.GetShaderInfoLog(vertexShader));
        }

        uint fragmentShader = gl.CreateShader(ShaderType.FragmentShader);
        gl.ShaderSource(fragmentShader, fragmentCode);
        gl.CompileShader(fragmentShader);

        gl.GetShader(fragmentShader, ShaderParameterName.CompileStatus, out int fStatus);
        if (fStatus != (int)GLEnum.True)
        {
            throw new Exception("Fragment shader failed to compile: " + gl.GetShaderInfoLog(fragmentShader));
        }

        _program = gl.CreateProgram();
        gl.AttachShader(_program, vertexShader);
        gl.AttachShader(_program, fragmentShader);
        gl.LinkProgram(_program);

        gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int lStatus);
        if (lStatus != (int)GLEnum.True)
        {
            throw new Exception("Program failed to link: " + gl.GetProgramInfoLog(_program));
        }

        gl.DetachShader(_program, vertexShader);
        gl.DetachShader(_program, fragmentShader);
        gl.DeleteShader(vertexShader);
        gl.DeleteShader(fragmentShader);
    }

    protected override void OnDestroy(GL gl)
    {
        gl.DeleteVertexArray(_vao);
        gl.DeleteBuffer(_vbo);
        gl.DeleteProgram(_program);
    }

    protected override void RenderOverride(GL gl)
    {
        gl.ClearColor(Color.Black);
        gl.Clear(ClearBufferMask.ColorBufferBit);

        gl.UseProgram(_program);

        gl.BindVertexArray(_vao);
        gl.DrawArrays(PrimitiveType.Triangles, 0, 3);
    }
}
#endif