Rust generator "scripts"
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.
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.
Added support for loading MagicaVoxel VOX models. This relies almost entirely on the work of others as the model below was created by Mike Judge and the VOX loading code comes from jgraef's vox-format crate. The only real addition to the Snowfall code was to add a YAML descriptor for VOX models and to write a quick translation from the vox-format
format to the existing internal voxel format.
Note that the shader is randomly darkening each voxel a bit based on it's world coordinate to make for an (intentionally) less uniformly colored look to the final render.
Note that the array look-ups need to be expanded out as if-else
branches as WGSL rejects attempts to index using a non-const value (i.e. return shade_x[xi] + shade_y[yi] + shade_z[zi];
does not compile).
const shade_x = array<f32, 7>(0.05, 0.18, 0.10, 0.96, 0.46, 0.75, 0.55);
const shade_y = array<f32, 7>(0.52, 0.52, 0.34, 0.03, 0.38, 0.01, 0.66);
const shade_z = array<f32, 7>(0.33, 0.60, 0.80, 0.30, 0.16, 0.85, 0.13);
// Shade the color based on the world coordinate grid position
//
// This gives a subtle variation to each voxel based on world position
// so that there's less uniformity to everything. Subjectively produces
// a better looking result.
//
// Returns a [0-1] value.
//
fn shade_world_coord(world_coord : vec3<f32>) -> f32 {
var grid_wc = vec3<i32>(floor(world_coord));
var ix = (1 * grid_wc.x + 13 * grid_wc.y + 31 * grid_wc.z) % 7;
var iy = (1 * grid_wc.y + 17 * grid_wc.z + 43 * grid_wc.x) % 7;
var iz = (1 * grid_wc.z + 37 * grid_wc.x + 3 * grid_wc.y) % 7;
// The shade arrays are [0-1] in range, so shade is [0-3]
var shade = 0.0;
if ix == 0 {
shade += shade_x[0];
} else if ix == 1 {
shade += shade_x[1];
} else if ix == 2 {
shade += shade_x[2];
} else if ix == 3 {
shade += shade_x[3];
} else if ix == 4 {
shade += shade_x[4];
} else if ix == 5 {
shade += shade_x[5];
} else if ix == 6 {
shade += shade_x[6];
}
if iy == 0 {
shade += shade_y[0];
} else if iy == 1 {
shade += shade_y[1];
} else if iy == 2 {
shade += shade_y[2];
} else if iy == 3 {
shade += shade_y[3];
} else if iy == 4 {
shade += shade_y[4];
} else if iy == 5 {
shade += shade_y[5];
} else if iy == 6 {
shade += shade_y[6];
}
if iz == 0 {
shade += shade_z[0];
} else if iz == 1 {
shade += shade_z[1];
} else if iz == 2 {
shade += shade_z[2];
} else if iz == 3 {
shade += shade_z[3];
} else if iz == 4 {
shade += shade_z[4];
} else if iz == 5 {
shade += shade_z[5];
} else if iz == 6 {
shade += shade_z[6];
}
return shade/3.0;
}
Not much visually different, but improved the code base a bit. Most notably having the engine automatically run model generation scripts (rather than manually running scripts to create static assets) and improved logging.
➕ Split model definition YAML from generator script JS
➕ Landscape generator takes grid size as an optional parameter
➕ Improved logging output
➕ Default camera now set based on scene bounds
➕ Added id
and generation
to models to detect cache staleness
➕ Added BoundingBox
🗄️ Split files for better organization
➕ git status alias not provides links to diffs on GitHub so summary changelogs like this are easier to create
🔮 Hot reloading of the module when the script or model file changes
The above is a bit tricky as it creates a dependency between the loading code, an async file watcher, and the main render loop. I don't yet have a clear vision for how to connect those in a non-intrusive manner, but otherwise would like to add "hot reloading" as a core piece of functionality to make development easier and faster.
Added shading based on world coordinate positions. This was mostly a test of adding new uniforms and vertex shader outputs to the shader program.
Added a voxel model. Very inefficient at this point, but "correct."
Tried increasing the resolution and found bugs :)
Guess: missing array data?
Manually counting the input data matches the generated positions array and checking against the runtime data...
4416 voxels * 6 faces/voxel * 8 positions/face => 105984 positiions
4416 voxels * 6 faces/voxel * 6 indices/face => 158976 indices
...seems correct.
Guess: too many draw calls?
Seems impossible as this is a single mesh.
Investigation: is it always the same voxels that are missing?
Flipping the order of the voxels during generation changes the missing voxels. Not sure what to conclude from this yet.
Drawing just the Z+ face on the voxels seems to work correctly. The same seems to be true when rendering any one of the six faces.
...but if as soon as 3 faces are rendered together, voxels seem to be lost.
Guess: too many indicies
Yup. The code is using u16
which has a max representable value of 256*256 = 65,536
. There are 158,976
indices.
Changing to a u32
index buffer addresses the problem.
Resolution
Bumping the render code to always use u32
seems fine for this stage of development. Using the larger buffer size affords more flexibility, which is a higher priority right now than optimization for memory or performance.
Voxel chunking and optimizing away invisible faces need to be done regardless. After that is done, it should be easier to move back to a u16
index buffer.
With the bug fixed, here's a sine + cosine generated voxel heightmap of 128x128x32
resolution.
Progress update: have some vertices making it from disk to the screen. That's a cube sitting behind of a "2D" pentagon (not necessarily obvious without lights or shading yet).
The general task I've been working on is taking the "catch-all" State
object from the Learn Wgpu tutorial and refactoring into a rendering architecture. This requires wrangling with lifetimes and data sharing, both in my learning how Rust works as well as how objects work in wgpu.
A bit more progress, getting basic semi-harcoded face shading working. Also changed the camera axes to a Z+ => UP
system.
A bit more progress: depth buffering finally enabled and rendering multiple objects.
Ramping up on the custom rendering engine by going through the excellent Learn Wgpu tutorial. There's not much to say that's insightful here, but I find it's nice to create "early days" blog posts and images to highlight progress over time.
Though pedantic, I'd argue wgpu has a lot of configuration. I tend to consider boilerplate code common text that must be repeated to properly structure or specify another piece of non-common configuration or otherwise unique code. (Note: my personal definition is very different from AWS' definition which portrays "boilerplate" as a positive). By "must be repeated", I'm alluding to the kind of code that cannot be encapsulated easily into a reusable function, library, or other standard language primitive -- let's ignore macros certainly blur that line and just run with this hand-wavy definition!
In the context of creating many different wgpu programs, I can see how repeating the exact same configuration would constitue a good deal of "boilerplate code." However, that commonality could be easily wrapped into a reusable library, which -- if we're willing to run with my definition of boilerplate code! -- means it is not boilerplate code as the code does not have the quality that it "must be" repeated.
In the context of a single program, wgpu strikes me simply as a very low-level library with detailed, highly structured configuration.