The piece of software I've mostly been working on recently is still All is Cubes (posts). However, this one project has sprawled out into a lot of things to do with Rust.

For example, something about my coding style (or maybe my attention to error messages) seems to turn up compiler bugs — such as #80772, #88630, #95324, #95873, #97205, #104025, #105645, #108481, and #109067. (Which is not to say that Rust is dreadfully buggy — on average, the experience is reliable and pleasant.) I'm also taking some steps into contributing to the compiler, so I can get some bugs (and pet peeves) fixed myself. (The first step, though, was simply running my own code against rustc nightly builds, so I can help spot these kinds of bugs before they get released.)

I've also taken some code from All is Cubes and split it out into libraries that might be useful for others.

  • The first one was exhaust, a trait-and-macro library that provides the ability to take a type (that implements the Exhaust trait) and generate all possible values of that type. The original motivation was to improve on (for my purposes) strum::IntoEnumIterator (which does this for enums but always leaves the enum’s fields with the Default value) by generating enums and structs arbitrarily recursively.

    (Exhaustive iteration is perhaps surprisingly feasible even in bigger domains than a single enum; if you have a simple piece of arithmetic, for example, it only takes a few seconds to run it on every 32-bit integer or floating-point value, and look for specific outcomes or build a histogram of the results.)

  • The second one, published just yesterday, is rendiff, a (slightly) novel image diffing algorithm which I invented to compare the output of All is Cubes’ renderer test cases. Its value is that it is able to compensate for the results of rounding errors on the positions of the edges of objects in the scene — instead of such errors counting against a budget of allowable wrong pixels, they're just counted as correct, by observing that they exist at a neighboring position in the other image.

I woke up last night with a great feature idea which on further examination was totally inapplicable to All is Cubes in its current state. (I was for some dream-ish reason imagining top-down tile- and turn-based movement, and had the idea to change the cursor depending on whether a click was a normal move or an only-allowed-because-we're-debugging teleport. This is totally unlike anything I've done yet and makes no sense for a first-person free movement game. But I might think about cursor/crosshair changes to signal what clicking on a block would do.)

That seems like a good excuse to write a status update. Since my last post I've made significant progress, but there are still large missing pieces compared to the original JavaScript version.

(The hazard in any rewrite, of course, is second-system effect — “we know what mistakes we made last time, so let's make zero of them this time”, with the result that you add both constraints and features, and overengineer the second version until you have a complex system that doesn't work. I'm trying to pay close attention to signs of overconstraint.)

[screenshot]

Now done:

  • There's a web server you can run (aic-server) that will serve the web app version; right now it's just static files with no client-server features.

  • Recursive blocks exist, and they can be rendered both in the WebGL and raytracing modes.

  • There's an actual camera/character component, so we can have perspective projection, WASD movement (but not yet mouselook), and collision.

    For collision, right now the body is considered a point, but I'm in the middle of adding axis-aligned box collisions. I've improved on the original implementation in that I'm using the raycasting algorithm rather than making three separate axis-aligned moves, so we have true “continuous collision detection” and fast objects will never pass through walls or collide with things that aren't actually in their path.

  • You can click on blocks to remove them (but not place new ones).

  • Most of the lighting algorithm from the original, with the addition of RGB color.

    Also new in this implementation, Space has an explicit field for the “sky color” which is used both for rendering and for illuminating blocks from outside the bounds. This actually reduces the number of constants used in the code, but also gets us closer to “physically based rendering”, and allows having “night” scenes without needing to put a roof over everything. (I expect to eventually generalize from a single color to a skybox of some sort, for outdoor directional lighting and having a visible horizon, sun, or other decorative elements.)

  • Rendering space in chunks instead of a single list of vertices that has to be recomputed for every change.

  • Added a data structure (EvaluatedBlock) for caching computed details of blocks like whether their faces are opaque, and used it to correctly implement interior surface removal and lighting. This will also be critical for efficiently supporting things like rotated variants of blocks. (In the JS version, the Block type was a JS object which memoized this information, but here, Block is designed to be lightweight and copiable (because I've replaced having a Blockset defining numeric IDs with passing around the actual Block and letting the Space handle allocating IDs), so it's less desirable to be storing computed values in Block.)

  • Made nearly all of the GL/luminance rendering code not wasm-specific. That way, we can support "desktop application" as an option if we want to (I might do this solely for purposes of being able to graphically debug physics tests) and there is less code that can only be compiled with the wasm cross-compilation target.

  • Integrated embedded_graphics to allow us to draw text (and other 2D graphics) into voxels. (That library was convenient because it came with fonts and because it allows implementing new drawing targets as the minimal interface "write this color (whatever you mean by color) to the pixel at these coordinates".) I plan to use this for building possibly the entire user interface out of voxels — but for now it's also an additional tool for test content generation.

Still to do that original Cubes had:

  • Mouselook/pointer lock.
  • Block selection UI and placement.
  • Any UI at all other than movement and targeting blocks. I've got ambitious plans to build the UI itself out of blocks, which both fits the "recursive self-defining blocks" theme and means I can do less platform-specific UI code (while running headlong down the path of problematically from-scratch inaccessible video game UI).
  • Collision with recursive subcubes rather than whole cubes (so slopes/stairs and other smaller-than-an-entire-cube blocks work as expected).
  • Persistence (saving to disk).
  • Lots and lots of currently unhandled edge cases and "reallocate this buffer bigger" cases.

Stuff I want to do that's entirely new:

  • Networking; if not multiplayer, at least the web client saves its world data to a server. I've probably already gone a bit too far down the path of writing a data model without consideration for networking.

I've now been programming in Rust for over a month (since the end of July). Some thoughts:

  • It feels a lot like Haskell. Of course, Rust has no mechanism for enforcing/preferring lack of side effects, but the memory management, which avoids using a garbage collection algorithm in favor of statically analyzable object lifetimes, gives a very similar feeling of being a force which shapes every aspect of your program. Instead of having to figure out how to, at any given code location, fit all the information you want to preserve for the future into a return value, you instead get to store it somewhere with a plain old side effect, but you have to prove that that side effect won't conflict with anything else.

    And, of course, there are algebraic data types and type classes, er, traits.

  • It's nice to be, for once, living in a world where there's a library for everything and you can just use them by declaring a dependency on them and recompiling. Of course, there's risks here (unvetted code, library might be doing unsound unsafe, unmaintained libraries you get entangled with), but I haven't had a chance to have this experience at all before.

  • The standard library design sure is a fan of short names like we're back in the age of “linker only recognizes 8 characters of symbol name”. I don't mind too much, and if it helps win over C programmers, I'm all in favor.

  • They (mostly) solved the method chaining problem! (This got long, so it's another post.)

I've been getting back into playing Minecraft recently, and getting back into that frame of mind caused me to take another look at my block-game-engine project titled "Cubes" (previous posts).

I've fixed some API-change bitrot in Cubes so that it's runnable on current browsers; unfortunately the GitHub Pages build is broken so the running version that I'd otherwise link to isn't updated. (I intend to fix that.)

The bigger news is that I've decided to rewrite it. Why?

  • There's some inconsistency in the design of how block rotation works, and the way I've thought of to fix it is to start with a completely different strategy: instead of rotation being a feature of a block's behavior, there will be the general notion of blocks derived from other block definitions, so “this block but rotated” is such a derivation.

  • I'd like to start with a client-server architecture from the beginning, to support the options of both multiplayer and ✨ saving to the cloud!✨ — I mean, having a server which stores the world data instead of fitting it all into browser local storage.

  • I've been looking for an excuse to learn Rust. And, if it works as I hope, I'll be able to program with a much better tradeoff between performance and high-level code.

The new version is already on GitHub. I've given it the name “All is Cubes”, because “Cubes” really was a placeholder from the beginning and it's too generic.

I'm currently working on porting (with improvements) various core data structures and algorithms from the original version — the first one being the voxel raycasting algorithm, which I then used to implement a raytracer that outputs to the terminal. (Conveniently, "ASCII art" is low-resolution and thus doesn't require too many rays.) And after getting that solid, I set up compiling the Rust code into WebAssembly to run in web browsers and render with WebGL.

[console screenshot] [WebGL screenshot]

(In the unlikely event that anyone cares, I haven't quite decided what to do with the post tags; I think that I will switch to tagging them all with all is cubes, but I might or might not go back and apply that to the old posts on the grounds that having a tag that gets everything is good and I'm not really giving the rewrite a different name so much as taking the opportunity to replace the placeholder at a convenient time.)