Nebula Engine
Overview
The Nebula Engine is a modern 3D game engine, written in C++ and powered by Vulkan, that is capable of rendering true-to-scale planetary environments. This is an ongoing personal project that I have worked on intermittently between the periods of 2018-2019 and 2022-present. I wrote the engine from scratch initially as a learning exercise that quickly grew into a more extensive hobby project that is very much still a work in progress. Most modern graphics/game engine techniques are implemented in this project such as deferred shading, physically based shading, compute shaders (GPGPU), realtime raytracing, and a framerate-independent game loop. The engine supports various quality-of-life features such as automatic shader compilation (HLSL->SPIR-V) and hot-reloading of shaders. On top of the core engine I have implemented a space-sim that allows the user to explore the major planets of the solar system at 1:1 scale which I initially created in the Unity engine and have been gradually porting over.
There is a lot to discuss in relation to this project so more content, including screenshots, will be coming soon!
Graphics
The Nebula Engine uses the Vulkan API enabling realtime raytracing that will be an essential part of the rendering pipeline. Currently the engine supports deferred shading, physically based shading, atmospheric scattering, compute shaders, and raytraced shadows. Having prior experience developing graphics pipelines, I found the graphics implementation to be fairly straightforward after creating the proper abstractions of Vulkan features/objects; however creating these proper abstractions took some time to get right since I developed most of this part of the engine when Vulkan tutorials/resources were more sparse (2018) than they are today.
Probably the most challenging part of the graphics implementation was the shader compilation/reflection system. My initial system used a simple, custom parser and glslang to compile GLSL shaders into SPIR-V and reflect shader variables and bindings on the CPU-side but lacked the ability hot reloading and was tedious to add features to - not to mention that most of the shaders I had written previously were in HLSL. After returning to the engine in 2022 I rewrote most (eventually all) of the engine's shaders in HLSL and now use ANTLR and DXC for parsing and reflection with manual handling of #include. This setup is much more stable and convenient and supports hot-reloading which is almost a necessity when working on procedural generation shaders that will be tweaked constantly.
As the engine supports realtime raytracing (currently only used for shadows) I decided not to implement raster-based techniques such as cascaded shadow maps, SSR or SSAO and plan to entirely rely on raytracing for these effects as it is better suited to planetary-scale environments.
Planetary-Scale Environments
While the task of representing and rendering planetary-scale environments may be tricky in most modern, commercially-available game engines, the Nebula engine was designed to do exactly this.
An essential part of doing this is the use of a double precision coordinate system. Most game engines use single precision floating point representation for coordinates which only has acceptable precision up to a few kilometers (at a 1 unit = 1 meter scale); beyond that range noticeable artifacts begin to appear such as vertex jitter and possibly physics errors; on a planetary scale, the quantization interval of coordinates at the surface of the planet (assuming the origin is at the center of the planet) becomes too large to represent the geometry of small objects such as a human. In order to get around this one could implement a floating origin - i.e. resetting the coordinates of the game world such that the camera/player is always close to the origin - however this ultimately involves explicitly maintaining two sets of coordinates: "true world" coordinates in double precision and "game world" coordinates in single precision, for each transform. This approach can become cumbersome and uses more memory than the cleaner solution, that is not available in most existing game engines, of directly using doubles. Of course doubles are generally slower on the CPU and much slower on the GPU so vector calculations with doubles are minimized as much as possible by using relative coordinates in single precision as much as possible. For instance, rendering is done by computing the relative coordinates of transforms to the camera (to a similar effect as floating origin) every frame (a simple vec3d difference) then proceeding normally for the calculation of model matrices, etc., therefore there is a minimal performance overhead of doing this.
Another technical detail is that in order to render planetary-scale environments in a single camera frustum the near and far planes of the main camera must have a huge range with the near plane being a few centimeters away and the far plane being ~100k km away from the camera. If not accounted for this depth range will cause Z-testing to fail to due a lack of precision, however, by using either logarithmic depth or reversed-Z this problem can be alleviated. Initially I implemented logarithmic depth but ultimately found reversed-Z to be a simpler and equally effective solution.
Procedural Space Sim
On top of the engine I am building a space simulator that will eventually allow the user to explore a 1:1 procedural, virtual Milky Way that will be similar to Cosmographic Software's Space Engine. Currently I have implemented a procedural planet system that can use a mix of real-world and procedural data to render the major planets of the solar system and entirely procedural worlds.
Currently the space sim features the solar system + the ~100k real-world stars found in the Hipparcos catalog and allows the user to search for stars via either its HIP id, HD number or Bayer designation and warp to them.
Planets with solid surfaces are rendered as quadtrilateralized spherical cubes with a quadtree-LOD system that allows for viewing from space to the surface which is a fairly standard approach. All planetary textures, including heightmaps, are generated at runtime on the GPU via compute shaders and either mix real-world DEM and other data with procedural noise (for finer detail at high resolutions) or fully procedural terrain data and are stored in texture arrays. Currently these textures are uncompressed and take up a substantial amount of VRAM and I will be porting over texture compression system I initially wrote for a procedural virtual texturing system in Unity (will be showcased in a separate project - coming soon!) to reduce memory consumption.
In an effort to generate more physically plausible terrain than the common procedural noise techniques
I implemented an
algorithm for simulating hydraulic and thermal erosion on infinite terrain in realtime via the GPU
that can generate somewhat realistic mountains and multi-scale erosion effects on procedural worlds by
taking advantage of the fractal nature of the quadtree-LOD system.
Additionally I created a planetary system generation algorithm that incorporates some astrophysical principles to generate plausible results (generation of moons hasn't been implemented yet but will follow a similar pattern). A general overview of the algorithm is as follows:
- calculate the total mass of the parent star's protoplanetary disk, 98% of this will be gas, the rest will be dust or ice (that can accrete into solid cores), this will act as a "mass budget" for generating new planets
- sampling from an exponentional distribution, calculate the semi-major axis (SMA) of a new planet core from the parent star (cores are more likely to form closer to the star)
- randomly calculate the mass of the core from a mass range that depends on whether or not the core is within the parent star's frost line (cores will tend to be more massive beyond the frost line as they can accrete both rock and ice instead of just rock) and subtract this from the mass budget
- if the core mass is greater than 10 Earth masses we assume the core can accrete hydrogen gas in the protoplanetary disk in order to become a gas or ice giant, we calculate its new mass based on the core mass and the density of gas in the disk at its SMA
- once the mass budget is depleted and planet generation is finished, some planets may be within each others' Hill spheres, we assume these planets will collide and merge them
-
then for each planet:
- calculate/generate additional intrinsic parameters such as density, surface gravity, etc.
- estimate the core cooling time for rocky planets (this determines how long the planet will be geologically active which affects both terrain formation and atmospheric composition/density from outgassing)
- estimate the atmospheric conditions (i.e. density and temperatue) of the planet's early atmosphere, shortly after formation
- extrapolate from early conditions to current atmospheric conditions based on various factors 3(e.g. would liquid oceans have frozen or evapored, could life have developed and survived?) - planets within the parent star's habitable zone may become unhabitable due to greenhouse effects (e.g. Venus) and planets seemingly outside the habitable zone may be able to support liquid water and life due for the same reason
- generate planetary terrain and atmosphere model for rendering