Skip to content
Moritz Roetner edited this page Jun 27, 2022 · 7 revisions

Blazor/WebAssembly

Introduction

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications. (see: webassembly.org).

The Wave Engine (waveengine.net) developers are providing their implementation of .NET bindings for WebGL through WebAssembly WebGL.NET to the public.

These tools enable Fusee to compile web applications without relying on JSIL cross compilation anymore (from v0.8 onward).

Fusee hereby relies upon Blazor for compiling and running.

Usage

A minimal Fusee Wasm project needs, besides the actual Fusee core application, at least, an Microsoft.NET.Sdk.BlazorWebAssembly project.

Project file

Add the following lines to the Blazor project file inside the first <PropertyGroup>:

<BlazorEnableCompression>false</BlazorEnableCompression>
<BlazorCacheBootResources>false</BlazorCacheBootResources>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>

At the end of the file, paste these lines. This handles the Asset management during build.

<Target Name="MovingAssetsToServerRoot" BeforeTargets="PostBuildEvent">
		<ItemGroup>
			<AssetsDir Include="$(OutputPath)$(TargetFramework)\Assets\**\*.*" />
		</ItemGroup>
		<Message Text="Moving 'Assets' folder to http server root folder" Importance="high" />
		<Move SourceFiles="@(AssetsDir)" DestinationFolder="$(OutputPath)$(TargetFramework)\wwwroot\Assets\%(RecursiveDir)" />
		<RemoveDir Directories="$(OutputPath)$(TargetFramework)\Assets" />
</Target>

This modifies the build process and removes some pitfalls while publishing and running a Fusee Blazor project from Visual Studio.

_Imports.razor

After creation, one needs to add the following lines to _Imports.razor:

@using Fusee.Base.Imp.Blazor
@using Fusee.Engine.Imp.Graphics.Blazor
@using Fusee.Engine.Imp.Blazor

Main.cs

Create a file called Main.cs (if not already present) and paste the typical Fusee application code, alter paths and names accordingly.

using Fusee.Base.Common;
using Fusee.Base.Core;
using Fusee.Base.Imp.Blazor;
using Fusee.Engine.Core;
using Fusee.Engine.Core.Scene;
using Fusee.Engine.Imp.Graphics.Blazor;
using Fusee.Serialization;
using Microsoft.JSInterop;
using ProtoBuf;
using System;
using System.Threading.Tasks;
using Path = System.IO.Path;
using Stream = System.IO.Stream;

namespace Fusee.[NAMESPACE].Blazor
{
    public class Main : BlazorBase
    {
        private RenderCanvasImp _canvasImp;
        private Core.Simple _app;

        public override void Run()
        {
            Console.WriteLine("Starting Blazor program");

            // Disable colored console ouput, not supported
            Diagnostics.UseConsoleColor(false);
            Diagnostics.SetMinDebugOutputLoggingSeverityLevel(Diagnostics.SeverityLevel.Verbose);

            base.Run();

            // Inject Fusee.Engine.Base InjectMe dependencies
            Base.Core.IO.IOImp = new Fusee.Base.Imp.Blazor.IO();

            #region FAP

            var fap = new Fusee.Base.Imp.Blazor.AssetProvider(Runtime);
            fap.RegisterTypeHandler(
                new AssetHandler
                {
                    ReturnedType = typeof(Base.Core.Font),
                    Decoder = (_, __) => throw new System.NotImplementedException("Non-async decoder isn't supported in Blazor builds"),
                    DecoderAsync = async (string id, object storage) =>
                    {
                        if (Path.GetExtension(id).Contains("ttf", System.StringComparison.OrdinalIgnoreCase))
                        {
                            var font = new Base.Core.Font
                            {
                                _fontImp = new FontImp((Stream)storage)
                            };

                            return await Task.FromResult(font);
                        }

                        return null;
                    },
                    Checker = (string id) =>
                    {
                        return Path.GetExtension(id).Contains("ttf", System.StringComparison.OrdinalIgnoreCase);
                    }
                });

            fap.RegisterTypeHandler(
                new AssetHandler
                {
                    ReturnedType = typeof(SceneContainer),
                    Decoder = (_, __) => throw new System.NotImplementedException("Non-async decoder isn't supported in Blazor builds"),
                    DecoderAsync = async (string id, object storage) =>
                    {
                        if (Path.GetExtension(id).IndexOf("fus", System.StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            return await FusSceneConverter.ConvertFromAsync(Serializer.Deserialize<FusFile>((System.IO.Stream)storage));
                        }
                        return null;
                    },
                    Checker = (string id) =>
                    {
                        return Path.GetExtension(id).Contains("fus", System.StringComparison.OrdinalIgnoreCase);
                    }
                });
            AssetStorage.RegisterProvider(fap);

            #endregion

            _app = new Core.Simple();

            // Inject Fusee.Engine InjectMe dependencies (hard coded)
            _canvasImp = new RenderCanvasImp(canvas, Runtime, gl, canvasWidth, canvasHeight);
            _app.CanvasImplementor = _canvasImp;
            _app.ContextImplementor = new RenderContextImp(_app.CanvasImplementor);
            Input.AddDriverImp(new RenderCanvasInputDriverImp(_app.CanvasImplementor, Runtime));

            _app.LoadingCompleted += (s, e) =>
            {
                Console.WriteLine("Loading finished");
                ((IJSInProcessRuntime)Runtime).InvokeVoid("LoadingFinished");
            };

            _app.InitApp();

            // Start the app
            _app.Run();
        }

        public override void Update(double elapsedMilliseconds)
        {
            if (_canvasImp != null)
                _canvasImp.DeltaTimeUpdate = (float)(elapsedMilliseconds / 1000.0);

            _canvasImp?.DoUpdate();
        }

        public override void Draw(double elapsedMilliseconds)
        {
            if (_canvasImp != null)
                _canvasImp.DeltaTime = (float)(elapsedMilliseconds / 1000.0);

            _canvasImp?.DoRender();
        }

        public override void Resize(int width, int height)
        {
            base.Resize(width, height);
            _canvasImp?.DoResize(width, height);
        }
    }
}

Note that non async file handler are not supported within Blazor. These block the whole application. Therefore, be extra careful to use AssetStorage.GetAsync<T> everywhere!

Pages/Index.razor

Replace the content inside Pages/Index.razor with the following lines:

@page "/"
@using System.Runtime.CompilerServices
@using System.Diagnostics.CodeAnalysis
@inject IJSRuntime JS


@code
{

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var program = new Fusee.Base.Imp.Blazor.BlazorProgramm();
            var main = new Main();
            program.Start(main, (IJSInProcessRuntime)JS);
        }



        await base.OnAfterRenderAsync(firstRender);
    }
}

This takes care of injecting the Javascript Runtime and creates all necessary instances for Fusee to run.

wwwroot/index.html

Replace the content of wwwroot/index.html with the following lines:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Fusee.Examples.Simple.Blazor</title>
    <base href="/" />
    <link href="manifest.json" rel="manifest" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Lato:wght@700&display=swap" rel="stylesheet">

    <link rel="stylesheet" href="style/style.css" type="text/css" />

</head>

<body>

    <div id="LoadingOverlay">
        <div id="center">
            <p>
                <img src="style/FuseeSpinning.gif" alt="Loading Animation" />
            </p>
            <p>Loading</p>
        </div>
    </div>

    <div id="LoadingFinishedOverlay">
        <div id="center">
            <p>
                <img src="style/FuseeAnim.gif" alt="Startup animation" />
            </p>
            <p>Made with Fusee</p>
        </div>
    </div>

    <div id="app">
    </div>

    <script src="./_framework/blazor.webassembly.js"></script>
    <script>navigator.serviceWorker.register('service-worker.js');</script>
    <script src="./_content/Fusee.Base.Imp.Blazor/Fusee.Base.Imp.Blazor.Native.js"></script>
    <script src="./_content/Fusee.Engine.Imp.Graphics.Blazor/Fusee.Engine.Imp.Graphics.Blazor.Native.js"></script>
</body>
</html>

Final touches

Copy the style folder from any existing Blazor project to wwwroot.

👷 Engine Developer

Short implementation overview of WebAssembly in Fusee

As written above, Fusee utilizes Blazor for its Wasm implementation on the one hand, on the other hand, the WebGL.NET bindings generated by the WaveEngine team.

The architecture of Fusee allows separate implementations for arbitrary renderer and build targets. To implement the new WebGL Wasm backend, one needs to implement the RenderContextImp which implements the IRenderContextImp interface, which anon represents the methods within the non platform specific RenderContext. This implementation (and the WebGL.NET bindings) can be found within Base.Engine.Imp.Graphics.WebAsm.

Manual Javascript implementation

Blazor serializes variables to json while crossing the barrier between managed (C#) and "unmanged" (javascript, Webassembly) code. This works well for primitive data types (POD, struct, etc.), however sometimes it fails as the compiler is unable to work out how to serialize certain types. Furthermore, sometimes we do not want to serialize our data, think of large Textures or Meshes. A serialization of one Mesh to json every frame is something we certainly do not want to happen. To prevent this behavior Blazor provides the method

IJSUnmarshalledRuntime.InvokeUnmarshalled<T, T, T, T>({MethodName}, T, T, T);

with a fixed maximum of three {T} arguments. The first three are the arguments, the last T represents the return value of the method.

This method searches for a Javascript method named {MethodName} and calls it while passing the given parameters to the managed memory. The passed parameters can be used directly inside the Javascript method, as long as one passes PODs.

However, Blazor is unable to pass arrays as datatypes, therefore, on the javscript side some IntPtrunpacking needs to be done. Please note, that it's possible to pass more than three {T} arguments with a workaround. Just pass {T}[] or a struct as one of the arguments.

For the unpacking, Blazor provides the following methods inside any Javascript code of the current project.

 // extract values from array
const pointerToValue = Blazor.platform.getArrayEntryPtr(value, 0, 4); // value, startIdx, bytes per entry
const lengthOfArray = Blazor.platform.getArrayLength(value);
var result = new Float32Array(Module.HEAPU8.buffer, pointerToValue, lengthOfArray); // Make sure to use matching type to `getArrayEntryPtr` 

Fusee relies heavily upon unmarshalled invocation for OpenGL buffers and texture binding, as we do not have the time and resources to wait for Json serialization. When implementing a new feature, be certain to use InvokeUnmarshalled where appropriate.

Sometimes, as mentioned above, one needs to use this feature for certain OpenGL functions because the serialization fails. When you encounter an Exception inside the browser that reads something along the lines: Unable to serialize [datatype] ..., replace the function call with InvokeUnmarshalled and implement the Javascript functionality by hand inside Fusee.Engine.Imp.Graphics.Blazor/wwwroot/Fusee.Engine.Imp.Graphics.Blazor.Native.js for any OpenGL or graphic related stuff. Any engine related implementation goes into Fusee.Base.Imp.Blazor/wwwroot/Fusee.Base.Imp.Blazor.Native.js.

Fusee.Engine.Player.Blazor

Any Fusee.Core.dll can be played with our pre-built Fusee.Engine.Player.Blazor. This project provides the possibility to load and present those and is used for our VSCode Fusee template where a user can create a Fusee application inside a lightweight environment.

The implementation is nearly the same as inside Fusee.Engine.Player.Desktop. Try to load the Fusee.App.dll, if not found, load the standard Fusee.Engine.Player.Core.dll. For more information consult the source code file.

Clone this wiki locally