• Series
  • Blazor
  • BabylonJS
  • C#
  • .NET

BabylonJS and Blazor - Getting Set Up

For this article we will go over creating a ASP.NET Core Blazor web application integrated with BabylonJS. The Guided Learning tutorial uses TypeScript and Webpack for the development, but we will be using C#/.NET and Web Assembly for our development.

Checkout BabylonJS and Blazor - Setting Up A State Machine, the next step in the series!

Demo and Source Code

A running Demo of what should be seen when the application is Set Up and running. You can open the below demo in a new tab by going to the BabylonJS Blazor Step 01 website.

You can also see the full source code in GitHub here: canhorn/BabylonJS.Blazor.Game.Tutorial at step/01_Getting-Set-Up

Compare the original step in the Series here: Getting Set Up | Babylon.js Documentation (babylonjs.com)

Set Up

Below is the list of software and tools we will need during the re-implementation of the game into Blazor. The project will be focused on .NET6, but the project should work equally well in .NET5.

Install the .NET6+ SDK

Here you can get the .NET SDK below, I have included two links, one for the download page and another for the .NET6 download page. At the time of writing this .NET6 was still in Preview, but the code will still work with .NET5.

Create new .NET Project

Creating a new project can be done from the command line, but an IDE with the blazorwasm template will also work, below is a code snippet for the command line to create a new Blazor Wasm Host project in the current directory. We want to make sure the Blazor is hosted, this will give us a full solution to work against.

dotnet new blazorwasm -ho -o .

Install Interop Proxy generation tool

Here is where the magic starts, we are going to install a tool that can be used to create a simple abstraction around the BabylonJS JavaScript API that will allow for us to interact with it by using just C# syntax. This tool works by reading a TypeScript definition file and introspecting the Abstract Syntax Tree to create the abstraction for Wasm Interop for the library. I packaged up the tool so it can be installed using the dotnet tool, for easy usage in multiple projects and re-generation processes.

# Install the Tool Globally
dotnet tool install -g EventHorizon.Blazor.TypeScript.Interop.Tool

Generate Proxy from BabylonJS

Here we will generate the Blazor.BabylonJS.WASM project, the tool takes in a list of classes you want to generate against, it will also generate any referenced classes and interfaces. A list of sources are also includes, we use the TypeScript definition files for our version we will be working against.

ehz-generate -c Scene -c Engine -c DebugLayer -c HemisphericLight -c ArcRotateCamera -c MeshBuilder -a Blazor.BabylonJS.WASM -s https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/babylon.d.ts -s https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/gui/babylon.gui.d.ts

After the project is generated open the solution and add a reference to the _generated/Blazor.BabylonJS.WASM project, this will give us access to the BabylonJS abstraction from our new Blazor application.

Main Files

Here we will go over the files we will need and point out the structure of the project we will use. Below are a list of files and the expected contents, I will not go over all the cleanup but at the end of this article you will find a link to the source branch for this step with all the cleanup and updates necessary to get started with a BabylonJS in Blazor Game!

wwwroot/index.html is updated with the necessary JavaScript files necessary for BabylonJS to run, also including the interop-bridge file that is used by the generated interop abstraction.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BabylonJS.Blazor.Game.Tutorial</title>
    <base href="/" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BabylonJS.Blazor.Game.Tutorial.Client.styles.css" rel="stylesheet" />

    <script src="https://code.jquery.com/pep/0.4.2/pep.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script>

    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/ammo.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/cannon.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/Oimo.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/libktx.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/earcut.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/babylon.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/materialsLibrary/babylonjs.materials.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/loaders/babylonjs.loaders.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/serializers/babylonjs.serializers.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/gui/babylon.gui.min.js"></script>
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="_content/EventHorizon.Blazor.Interop/interop-bridge.js"></script>
</body>

</html>

App.razor adds the code necessary to give the generated interop abstraction access to the JSRuntime, the JSRuntime is used to call into the JavaScript layer with the logic provided by the interop-bridge.js from the wwwroot/index.html.

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    [Inject]
    public IJSRuntime JSRuntime { get; set; }

    protected override void OnInitialized()
    {
        EventHorizon.Blazor.Interop.EventHorizonBlazorInterop.JSRuntime = JSRuntime;
    }
}

Shared/MainLayout.razor will have most of the code removed and just a full screen div added so the game takes up the whole page.

@inherits LayoutComponentBase

<div style="width: 100vw; height: 100vh;">
	@Body
</div>

HTML/Canvas.cs is a custom class that will gives us a proxied instance to the DOM canvas element. This class relies on the EventHorizonBlazorInterop functionality to call into the JavaScript and gets the element by id. The class is a wrapper to the DOM element, and the Interop layer will use this reference, and replace it with the actual element in JavaScript, when it is passed through the abstraction.

namespace BabylonJS.Blazor.Game.Tutorial.Client.HTML
{
    using EventHorizon.Blazor.Interop;
    using System.Text.Json.Serialization;

    [JsonConverter(typeof(CachedEntityConverter<Canvas>))]
    public class Canvas : HTMLCanvasElementCachedEntity
    {
        public static Canvas GetElementById(
            string elementId
        ) => EventHorizonBlazorInterop.FuncClass(
            entity => new Canvas(entity),
            new string[] { "document", "getElementById" },
            elementId
        );

        private Canvas(
            ICachedEntity entity
        )
        {
            ___guid = entity.___guid;
        }
    }
}

BabylonJSExtensions/BabylonJSExtensions.cs is an extended BabylonJS Scene, adding access to the DebugLayer. This class is an example of how the generated code can be extended with custom functionality, be it missing features from the TypeScript definition or custom functionality added by a developer.

namespace BabylonJS.Blazor.Game.Tutorial.Client.BabylonJSExtensions
{
    using System.Text.Json.Serialization;
    using BABYLON;
    using EventHorizon.Blazor.Interop;

    [JsonConverter(typeof(CachedEntityConverter<DebugLayerScene>))]
    public class DebugLayerScene : Scene
    {
        private DebugLayer __debugLayer;

        public DebugLayerScene(Engine engine, SceneOptions options = null) 
            : base(engine, options)
        {
        }

        public DebugLayer debugLayer
        {
            get
            {
                if (__debugLayer == null)
                {
                    __debugLayer = EventHorizonBlazorInterop.GetClass<DebugLayer>(
                        this.___guid,
                        "debugLayer",
                        (entity) =>
                        {
                            return new DebugLayer() { ___guid = entity.___guid };
                        }
                    );
                }
                return __debugLayer;
            }
            set
            {
                __debugLayer = null;
                EventHorizonBlazorInterop.Set(
                    this.___guid,
                    "debugLayer",
                    value
                );
            }
        }
    }
}

Pages/Index.razor contains our standard HTML canvas tag, with an id we will use as a reference passed to the BabylonJS Engine creation. Also includes styles to fill the page with the canvas and a Blazor event handler hook to the onkeydown callback.

@page "/"

<canvas id="game-window" style="width:100%; height: 100%;"
		@onkeydown="HandleKeyDown"></canvas>

Pages/Index.razor.cs is the backing code provided to our index page, includes standard component lifecycle hooks, but also creates the scene. What you might notice is that the code is very similar to standard BabylonJS setup.

namespace BabylonJS.Blazor.Game.Tutorial.Client.Pages
{
    using System;
    using System.Threading.Tasks;
    using BABYLON;
    using BabylonJS.Blazor.Game.Tutorial.Client.BabylonJSExtensions;
    using BabylonJS.Blazor.Game.Tutorial.Client.HTML;
    using EventHorizon.Blazor.Interop.Callbacks;
    using Microsoft.AspNetCore.Components.Web;

    public partial class Index : IDisposable
    {
        private Engine _engine;
        private DebugLayerScene _scene;

        protected override void OnAfterRender(bool firstRender)
        {
            if (firstRender)
            {
                CreateScene();
            }
        }

        public void Dispose()
        {
            _engine?.dispose();
        }

        public void CreateScene()
        {
            var canvas = Canvas.GetElementById(
                "game-window"
            );
            var engine = new Engine(
                canvas,
                true
            );
            // We extend the standard Scene with the DebugLayer getter in the DebugLayerScene
            _scene = new DebugLayerScene(
                engine
            );
            var light1 = new HemisphericLight(
                "light1",
                new Vector3(
                    0,
                    2,
                    8
                ),
                _scene
            );
            var camera = new ArcRotateCamera(
                "Camera",
                (decimal)(Math.PI / 2),
                (decimal)(Math.PI / 4),
                2,
                Vector3.Zero(),
                _scene
            );
            _scene.activeCamera = camera;
            camera.attachControl(
                false
            );
            var sphere = MeshBuilder.CreateSphere(
                "sphere",
                new
                {
                    diameter = 1
                },
                _scene
            );

            engine.runRenderLoop(new ActionCallback(
                () => Task.Run(() => _scene.render(true, false))
            ));

            _engine = engine;
        }

        protected void HandleKeyDown(
            KeyboardEventArgs args
        )
        {
            Console.WriteLine(args.Key);
            if (args.ShiftKey && args.CtrlKey && args.AltKey && args.Key.ToLower() == "i")
            {
                if (_scene.debugLayer.isVisible())
                {
                    Console.WriteLine("Hello");
                    _scene.debugLayer.hide();
                }
                else
                {
                    _scene.debugLayer.show();
                }
            }
        }
    }
}

Running the Project

To run the application all we have to do is run a standard .NET run command from the Command Line, or if your using an IDE the standard run actions will also work. If you are using Visual Studio 2019 Preview, as of writing this, you can Debug the application, and even introspect the BabylonJS generated instances, it will even pull the JavaScript values as if they were standard C#/.NET objects!

dotnet run

You should see something similar to the image below when the application is running successfully!

A Sphere on a blue background, showing a working BabylonJS Scene deployed on Blazor.

Cody's logo image, it is an abstract of a black hole with a white Event Horizon.

Cody Merritt Anhorn

A Engineer with a passion for Platform Architecture and Tool Development.