Top

Your first WebGL.NET app

Your first WebGL.NET app

WebGL.NET?

WebGL.NET borns as our WebGL low-level bindings for Wave Engine 3.0. Thanks to Mono’s effort on taking the CLR into WebAssembly (WASM), WebGL.NET serves as a binding to consume the WebGL API directly from C# (or the .NET language you prefer, actually). WebGL.NET covers the official WebGL 1.0 and 2.0 specifications, offering a more suitable scenario for day-to-day .NET developers.

This guide will help you creating a WASM app which draws a triangle with WebGL, all of this programmed in C# and running on top of .NET. Previous knowledge on WebGL will make this even easier; however, we’re thinking here on people which’s coming new to the OpenGL world.

You must have installed a modern .NET Core SDK, and can use Visual Studio Code for editing files, although it’s not mandatory.

Create the project

Start by running the following command, in your preferred console terminal, to create a new console app:

> dotnet new console

Make a local copy of —at the same level where you executed the previous command:
– this packages folder; and
– this NuGet.config file, which adds priority to above when looking for packages

That packages folder contains the sauce needed to enable WASM compilation, and will eventually become available through regular NuGet by the end of 2019.

Open the CSPROJ and modify the Project’s SDK value to “Mono.WebAssembly.Sdk” (if you’re curious: yes, it fits with one of the package names). Now you have a DLL (a library) which, when built, generates a static web site.

Follow adding the WebGL.NET NuGet: you can do it manually by inserting the following snippet into the CSPROJ:

<ItemGroup>
  <PackageReference Include="WebGLDotNET" Version="1.2.0-preview" />
</ItemGroup>

OK, you’re now yes ready to start touching some WebGL.

Draw a triangle

Create the canvas

In order to draw anything with WebGL you need a canvas, the surface. This can be done by manually touching the resulting HTML or, dinamically, creating it on the fly. And, as we all like dynamic stuff, because it can be done, let’s choose this way: the following snippet does just that:

JSObject canvas;

using (var document = (JSObject)Runtime.GetGlobalObject("document"))
using (var body = (JSObject)document.GetObjectProperty("body"))
{
    canvas = (JSObject)document.Invoke("createElement", "canvas");
    body.Invoke("appendChild", canvas);
}

If you come from JavaScript, you’ll quickly notice above is the translation of:

var canvas = document.body.createElement("canvas");
body.appendChild(canvas);

The Runtime and JSObject classes are the trampolines to communicate with JavaScript from C#, and are the underlying glue to call WebGL from C# too.

Set the triangle up

Now we need the context: the object which exposes the drawing API —notice how we’ll use WebGL 2.0 since the ctor.:

var gl = new WebGL2RenderingContext(canvas);

A triangle in OpenGL is the equivalent of “Hello, World!” in whatever programming language we start learning. It’s made of just 3 vertices, which we need to place in the 3D space, although we won’t use Z axe, with the following piece —think of (0, 0) as the middle of the screen:

var vertices = new float[]
{
    -0.5f,  0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f
};
var vertexBuffer = gl.CreateBuffer();
gl.BindBuffer(WebGLRenderingContextBase.ARRAY_BUFFER, vertexBuffer);
gl.BufferData(WebGLRenderingContextBase.ARRAY_BUFFER, vertices, WebGLRenderingContextBase.STATIC_DRAW);
gl.BindBuffer(WebGLRenderingContextBase.ARRAY_BUFFER, null);

The indices are needed to tell WebGL which’s the order to draw the vertices, where we’ll simply follow the clock-wise convention:

var indices = new ushort[] { 0, 1, 2 };
var indexBuffer = gl.CreateBuffer();
gl.BindBuffer(WebGLRenderingContextBase.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.BufferData(WebGLRenderingContextBase.ELEMENT_ARRAY_BUFFER, indices, WebGLRenderingContextBase.STATIC_DRAW);
gl.BindBuffer(WebGLRenderingContextBase.ELEMENT_ARRAY_BUFFER, null);

Now come the shaders, somewhat little pieces of low-level code which execute in the GPU:

var shaderProgram = gl.CreateProgram();

var vertexShader = gl.CreateShader(WebGLRenderingContextBase.VERTEX_SHADER);
gl.ShaderSource(vertexShader, 
@"attribute vec3 position;

void main(void) {
    gl_Position = vec4(position, 1.0);
}");
gl.CompileShader(vertexShader);

var fragmentShader = gl.CreateShader(WebGLRenderingContextBase.FRAGMENT_SHADER);
gl.ShaderSource(fragmentShader, 
@"void main(void) {
    gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}");
gl.CompileShader(fragmentShader);

gl.AttachShader(shaderProgram, vertexShader);
gl.AttachShader(shaderProgram, fragmentShader);

gl.LinkProgram(shaderProgram);

gl.UseProgram(shaderProgram);

And, before drawing, we link our vertices and indices with the shaders, and vice versa:

gl.BindBuffer(WebGLRenderingContextBase.ARRAY_BUFFER, vertexBuffer);
gl.BindBuffer(WebGLRenderingContextBase.ELEMENT_ARRAY_BUFFER, indexBuffer);

var positionAttribute = (uint)gl.GetAttribLocation(shaderProgram, "position");
gl.VertexAttribPointer(positionAttribute, 3, WebGLRenderingContextBase.FLOAT, false, 0, 0);
gl.EnableVertexAttribArray(positionAttribute);

Draw

And, now yes, the final drawing:

gl.Enable(WebGLRenderingContextBase.DEPTH_TEST);

gl.Viewport(0, 0, (int)canvas.GetObjectProperty("width"), (int)canvas.GetObjectProperty("height"));

gl.ClearColor(0, 0, 0, 0);
gl.Clear(WebGLRenderingContextBase.COLOR_BUFFER_BIT);

gl.DrawElements(
    WebGLRenderingContextBase.TRIANGLES, 
    indices.Length, 
    WebGLRenderingContextBase.UNSIGNED_SHORT, 
    0);

Save all the changes and go back to the terminal:

> dotnet build

If everything goes fine, which it should, navigate to bin/Debug/netstandard2.0/. Simply serve this folder locally with Fenix, xsp4 (it’s included in Mono’s SDK) or similar, and navigate to the provided URL.

From now on

Although you haven’t end up with a strong knowledge on WebGL, you have now the basics working and have a white paper from where to start investigating. WebGL.NET can be used for easy learning about OpenGL, as you only need a browser to see the results.

You can share your findings too in any static sites hosting, as GitHub pages, because there’s no backend needed, everything runs locally.

Marcos Cobeña Morián

Software Development Engineer

No Comments

Post a Comment