I thought it might be fun to convert my F# version of Wolfenstein 3D (code here, playable in the browser here) to use modern C# and use Blazor to run it in the browser hopefully ending up with something like this below (screenshot grabbed in Edge on a Mac):
I've often been asked to document my work on these fun projects and so here's part 1. There's not much there yet as today was a "voyage of discovery" to figure out how to handle the basics of rendering with a byte array - Wolfenstein 3D, and my F# version, are raycasters that essentially work with the individual pixels rather than using hardware acceleration and I needed an approach that would work with Blazor from WASM land.
My initial attempt at this was to use the Blazor.Extensions.Canvas package to do this much the same way I had with F# - I expected their might be excessive interop cost around putImageData (how you update the underlying data) but it seemed a good place to start. Unfortunately the authors of that package have not exposed that method so that quickly stymied that.
Attempt two was to use the WebGL context on the canvas to pretty much do the same. Write to a byte array, load that up as a texture, and then render and texture map the image onto a rectangular surface. That approach was also stymied as the somewhat critical texImage2D method doesn't work. The C# version is bound incorrectly to JS and the package hasn't been updated for a good while.
Which brings me to a massive bugbear I have with Blazor - Microsoft seemed to have shrugged their shoulders when it comes to providing access to browser APIs. Literally living in their own little world and relying on "the community" (i.e. free labour) to plug the gaps. Its ludicrous. If you're going to implement a browser framework in WASM you have, have, have to provide access to the browser APIs or people are just going to run into surprising and unexpected roadblocks. To me that seems like table stakes.
I nearly stopped at this point thinking oh well I'll just do it on the desktop using OpenGL like the F# desktop version. This is supposed to be fun and not teeth pulling after all. Then I remembered SkiaSharp - turns out some brave soul has got that working with Blazor and WebAssembly and had another go.
After a bit of experimenting with what would be the most optimal way of rendering essentially a byte array of pixels I found a method that worked acceptably and, well, here we are. End of part 1 - I have some scrappy code that can render a byte array to a canvas element in the browser and you can find the source for this part here and an exciting screenshot of this below:
We're not quite at the "wow it looks like Wolfenstein" stage just yet.
The key piece of code to getting this performant, which I can take no credit for - SkiaSharp has excellent documentation, is this:
unsafe
{
fixed (uint* ptr = _renderer.UpdateFrameBuffer())
{
_bitmap.SetPixels((IntPtr)ptr);
}
}
canvas.DrawBitmap(_bitmap, new SKRect(0, 0, WolfViewportWidth*CanvasZoom, WolfViewportHeight*CanvasZoom));
This basically allows my renderer to work in an abstract manner on an array of bytes (well uint's actually but we'll come back to that) and then create an SKBitmap from them and draw that onto the canvas. Interestingly using a 2D canvas was much faster than a WebGL canvas - I assume because with the latter somewhere or other with the dynamic nature of my texture I'm incurring yet another JS interop hop with a large array.
Back to the array. Why a uint and not a byte array? Each pixel is made up of a red, green, blue and alpha component with each component being a byte long. Using a uint simply allows me to treat a pixel as an element in the array rather than grappling with 4 elements per pixel. Shorthand really and you end up with something a bit like this:
public uint[,] UpdateFrameBuffer()
{
for (int row = 0; row < _height; row++)
{
for (int col = 0; col < _width; col++)
{
_buffer[row, col] = MakePixel((byte) col, 0, (byte) row, 0xFF);
}
}
return _buffer;
}
What I'll be doing now is walking towards replacing that simple placeholder with, well, Wolfenstein. Next step will be loading the maps and graphics.
And an aside: was quite impressed to find that unsafe code is supported in the .NET WASM compiler. I mean there's no reason it shouldn't be but it had the feel of one of those things I was going to try and get an unpleasant error message back from.
Going forwards my plan is to do this in a fairly functional style - probably go back and forth a bit as I've not written much C# for a couple of language versions. I had some more functional stuff in the WebGL version (stupidly deleted) and my thoughts so far are that its a bit weird. Having records but not free standing functions is just very strange to an F#er, and good lord their is so very much typing even with Rider / Intellisense / CoPilot.
Hopefully once I've wrapped my head around current C# the code will end up more elegant than my F# version - after all in that version I was grappling with how to build a raycaster and how to make it play like Wolfenstein. This time they are both solved problems.