More voxels
Working a bit on terrain generation. Turns out it can be a lot more fun working on this in native desktop with Rust rather than WebGL given the runtime limitations of the latter.
Working a bit on terrain generation. Turns out it can be a lot more fun working on this in native desktop with Rust rather than WebGL given the runtime limitations of the latter.
Still working on voxel rendering in Bevy. Restarted from fresh as I've learned a bit more since last time...
Been working on a lot of different experiments lately. Merging my recent interest in learning Bevy, Rust, and WASM with my long-time interest in voxel rendering, I've been working on an experimental program to convert a Quake 2 BSP38 map file into voxelized representation.
The above is slow and inefficient, but it's an interesting starting point as it's all running in WASM using Bevy.
Added an experiment of rendering with Bevy using WASM. It's nothing too exciting as I'm new to Bevy, WASM, and still relatively inexperienced with Rust!
A few notes on the development:
I wanted to host this experiment (and future ones) within the context of pages within the Docusaurus app. I certainly do not know the "best" way to do this yet, but one definite constraint to pass the canvas id
to the WASM module at startup rather than hard-coding it in the WASM and browser.
As far as I can tell, the WASM init
function does not take arguments, therefore the startup is exposed via a separate wasm_bindgen
exported function called start
.
In the Rust snippet below, you can see we do nothing in main
and explicitly pass in an id
to the WindowPlugin
in a separate start
function.
fn main() {
// The run() entrypoint does the work as it can be called
// from the browser with parameters.
}
#[wasm_bindgen]
pub fn start(canvas_id: &str) {
let id = format!("#{}", canvas_id);
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
canvas: Some(id.into()),
..default()
}),
..default()
}))
.add_systems(Startup, setup)
.add_systems(
Update,
(
move_ball, //
update_transforms,
),
)
.run();
}
The JavaScript code to bootstrap this looks like this:
const go = async () => {
let mod = await import(moduleName);
await mod.default();
await mod.start(canvasID);
};
go();
But admittedly the above is not the actual JavaScript code for hosting the WASM module...
This is workaround code. It "works" but I'm sure there is a correct way to handle this that I was not able to discover!
There's something I don't understand about WASM module loading and, more importantly, reloading/reuse. This is problematic in the context of a Single Page Application (SPA) like Docusaurus where if you navigate to page ABC, then to page XYZ, then back to ABC, any initialization that happened on the first visit to page ABC will happen again on the second visit. In other words, I'm not sure how to make the WASM initalization idempotent.
If there's a correct way to...
...I'd enjoy learning how!
Docusaurus also has logic for renaming, bundling, rewriting, etc. JavaScript code used on the pages. I'm not sure what the exact logic of what it does, but end of the day, I did not want Docusaurus manipulating the JS generated by the WASM build process.
Admittedly this is a bit of laziness on my part for not really understanding what Docusaurus does and how best to circumvent it.
I worked around the reload problems and script mangling with a custom MDX Component in Docusaurus that:
<script>
element so Docusaurus can't modify what it doesexport function CanvasWASM({
id,
module,
width,
height,
style,
}: {
id: string,
module: string,
width: number,
height: number,
style?: React.CSSProperties,
}) {
React.useEffect(() => {
// We create a a DOM element since Docusaurus ends up renaming /
// changing the JS file to load the WASM which breaks the import
// in production. This is pretty hacky but it works (for now).
const script = document.createElement('script');
script.type = 'module';
script.text = `
let key = 'wasm-retry-count-${module}-${id}';
let success = setTimeout(function() {
localStorage.setItem(key, "0");
}, 3500);
const go = async () => {
try {
let mod = await import('${module}');
await mod.default();
await mod.start('${id}');
localStorage.setItem(key, "0");
} catch (e) {
if (e.message == "unreachable") {
clearTimeout(success);
let value = parseInt(localStorage.getItem(key) || "0", 10);
if (value < 10) {
console.log("WASM error, retry attempt: ", value);
setTimeout(function() {
localStorage.setItem(key, (value + 1).toString());
window.location.reload();
}, 20 + 100 * value);
} else {
throw e;
}
}
}
};
go();
`.trim();
document.body.appendChild(script);
}, []);
return <canvas id={id} width={width} height={height} style={style}></canvas>;
}
In case anyone on the internet runs into this and stumbles upon this page...
I also wasted quite a bit of time on this problem:
.gitattributes
file in all my repos to store generated files using git LFS*.wasm
to store those in LFSThis meant my code was working locally but when I was trying to load the WASM files on the published site, the LFS "pointer" text file was being served rather than the binary WASM file itself. It took me a while to figure this out. Ultimately the fix was to remove the .gitattributes
from the GitHub Pages repo so LFS is not used on the published site. (Aside: this might be a good reason to consider hosting this site via another platform, but I'll leave that for another day!)
Added the ability to write Rust "scripts" (basically crates that the engine will run to get model output from) and added a voxel terrain with some basic procedural noise. I think of it as a "script" since it uses a blanket important use scriptlib::*;
which brings in a lot of utilities to keep the code itself concise.
Also switched the WGPU to prefer linear RGB color until I progress a bit further and want to handle sRGB correctly.
Updated the code base to run Rust code to generate models. The below heightmap is a combination of sin functions and noise functions, generated in Rust code.