-
Notifications
You must be signed in to change notification settings - Fork 36
WebAssembly
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.
A minimal Fusee Wasm project needs, besides the actual Fusee core application, at least, an Microsoft.NET.Sdk.BlazorWebAssembly
project.
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.
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
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!
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.
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>
Copy the style
folder from any existing Blazor
project to wwwroot
.
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
.
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 POD
s.
However, Blazor
is unable to pass arrays as datatypes, therefore, on the javscript
side some IntPtr
unpacking 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
.
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.
- Using FUSEE
- Tutorials
- Examples
- In-Depth Topics
- Input and Input Devices
- The Rendering Pipeline
- Render Layer
- Camera
- Textures
- FUSEE Exporter Blender Add on
- Assets
- Lighting & Materials
- Serialization and protobuf-net
- ImGui
- Blazor/WebAssembly
- Miscellaneous
- Developing FUSEE