16 February 2019

Procedural Chunk-Based Universe Part 3 - Software Development Buzzwords

In previous parts I explained how I set out to make a chunk system for huge 3D game worlds, how I experimented with using it to generate a universe using 3D noise to produce a cosmic web, and how I expanded it to be able to use chunks and visible radii whose values were independent. At the end of Part 2 I recounted how a need arose to be able to keep chunks in the world persistent and unchanged when they didn't really need to be updated.

The architecture of my chunk system had to be pervasively rekerjiggered once again, so much so that  I decided I wanted to be a pro and do it "properly" this time with inheritance and encapsulation and other software development buzzwords. I synergized logistical paradigms and leveraged strategical methodologies for performant serviceability throughout the product lifespan to maximize utility and market penetration.
In other words I got out a text editor and typed out a new vision for what would be the fifth version of the project... and then a mere four days of derping around later, went back to the drawing board... typing window... whatever, and I wrote down the sixth version:
VertexArrayMesh script
    updates MeshFilter with procedural mesh based on Vector3[]
    includes vertex colors, with alpha channel for luminosity / scale

DummyMesh script
    renders mesh (e.g. from MeshFilter) at Vector3 location

Starfield shader
    takes in starfield mesh with vertex positions and colors
    draws textured quad at each vertex
    colors quad with vertex color rgb
    scales quad with vertex color a

Procedural Universe script
    creates and manages chunks
        chunks exist in WORLD space
    collects points from live chunks and pushes to VertexArrayMesh or other scripts as necessary (e.g. for unpacking)
    Chunk class
        points, random seed, Vector3 position (not ints! not worth it)
        persists at world space position, holding points
        Refresh()
            pushes position, size (localScale Vector3), random seed, and density to NoiseProvider
            obtains points Vector3[]
    Refresh(force, updateAll)
        clear refresh queue and wait for RefreshQueue to stop
        if !force, check position and compare with oldPosition
        edgeSize Vector3 = min(3, ceil(visibleRadius / localScale.xyz)).xyz
        triple loop edgeSize.x, y, z
            generate Vector3 xyz and skip if not touching sphere
            store as linked list
        loop chunks
            if !updateAll and chunk touches visibleRadius sphere (double check is faster than extra looping), loop possible positions
                match with chunk position
                if match found, remove position, break back into chunks loop, and continue to next chunk
            if loop completes (no match or updateAll), chunk is dead; add to deadPool linked list   
        if positions left, loop leftover positions
            pop dead chunk if available, or spawn new chunk
            position chunk at position
            if player is within chunk, refresh chunk now
            else add chunk to refresh queue
        if dead chunks left, delete
    RefreshQueue coroutine or thread
        refresh chunks one at a time

NoiseProvider script/function
    Takes in random seed and point count
    Outputs points Vector3[] within AABB with center and size Vector3
    Multiple variants:
        Voronoi - generates seeds Vector3[] once when called and then generates points based on these
        Spiral Galaxy - generates points based on spiral algorithm
            sphere -> ellipsoid -> spiral
            sphere -> spheroid -> disk
Please don't copy this - not because it's my special valuable IP but because I later, as I mean to explain later on, found all the ways this is wrong and bad and made a different and much better one. Also it mentions some "deadPool" entity or person and I feel like there should only ever be one of whoever that is. I've just included it here so you can see where I was at this point.
Also it's my personal notes for myself copied verbatim, so I have no expectations it'll be digestible what with all the references to variables I had bouncing around my head, abbreviations, jargon, shorthand, etc. Cooooooode...
Time for a pretty picture.
Oh hey! This is where we were before (but with the GUI not hidden this time). It was during this phase that I actually implemented the outline gizmo thing I kept mentioning.
Now that I have a visual, I can explain what all that stuff above means a bit better.
Each blue cube is the outline of a single chunk. Chunks don't actually have to be cubes any more - they can be varying amounts of tall, wide, and long for various purposes like maybe making a big wide flat galaxy or whatever. Since the algorithm generates the universe by comparing the chunk dimensions and visible radius during runtime, it works fine even if the chunks are oblong.
Each of these chunks is a GameObject, attached to which is the actual chunk script, a script for providing procedural noise to the chunk, and scripts for using that procedural noise to render starfields. My specific implementation led me to craft a script for rendering meshes that didn't bother with rotation and scaling and a geometry shader that could take in a single mesh and simply use its vertices to draw star sprites so I didn't need to have tons of objects or particles eating up computing resources.
Because the starfield and noise generation features had moved out to their own scripts, I was free to replace these with specially tailored versions that lent themselves to more effective diagnostics and debugging, for instance the Debug Noise script I teased earlier, which inherits from NoiseProvider. This allowed me to put serious work into improving the stability and performance of the system; one by one bugs were exposed and overcome, and efficiency was added, until I was able to produce screenshots like this:
Another example is the current background image for this blog. I not only had a cosmic web working, but I tailored the galaxy sprite colors to produce an imitation of "cosmic latte", the average color of all light sources in the known universe, and I used the power of my geometry shader to add redshift based on the distances to the various galaxies (note in the second image how some galaxies remain bright and nearly white while others, dim due to distance, also have an increasingly strong reddish orange tint). I was quite proud of myself at this point.
Also because the starfield and noise generation features had moved out to their own scripts, I was no longer constrained to using my procedural universe for starfields - any large volume that could be represented as a collection of chunks could be recreated using this system. I continued to iterate on my starfield version, gradually adding improvements, but I began to also poke around at using it for things like flattened disk-shaped starfields as one might find in galaxies, asteroid belts, or ring systems and for things that weren't starfields at all. In theory this could be used for such things as cellular automata or terrains, although I still didn't have much interest in being yet another developer who's made a terrain generator by using Perlin noise to add a heightmap to a plane. But there was another application that was tempting me...
In Part 4 I aim to explain how pursuing that idea brought me to my current design for the system and led to much, much better performance for all implementations of it including the existing starfield generator.

No comments:

Post a Comment

Sorry this isn't a real post :C

I've entered graduate school and, as I expected, suddenly become far busier than I had been before, leaving no time to maintain my blog,...