stephen-hqxu / superterrainplus

SuperTerrain+: A real-time procedural 3D infinite terrain engine with geographical features and photorealistic rendering.
MIT License
12 stars 1 forks source link

Water rendering with global illumination #41

Open stephen-hqxu opened 2 years ago

stephen-hqxu commented 2 years ago

What's wrong

Currently water is rendered with SSR. As has been mentioned in #40, due to its simplicity the quality of reflection and refraction is underwhelming in general. As the ray leaves the screen domain no colour nor depth information is available, leaving black pixels. In modern real-time water rendering there are a few ways of tackling this problem.

For procedural generation, environment mapping is not applicable due to the degree of randomness of the scene. For reflection probe, it can also be difficult to determine the location where the probe should be placed; it can also be difficult to handle a large reflection body; updating a large number of probe can also be expensive.

Ray march based GI

As the terrain is heightmap based, the heightmap can be used to construct a signed distance texture for ray marching. When rendering water reflection and refraction the renderer only needs to ray march from the point where view ray hits the water, to the intersection of the terrain, based on the heightmap.

The same principle can be applied to sky rendering because our sky is already generated with ray marching.

Terrain generation and rendering with ray marching is nothing new (see shader toy) and it is fairly efficient to do so on a heightmap, especially when we don't need a superior quality for anything displayed in the water. However it can be limited if I want to extend the renderer for other rendering components that are difficult to have a SDF defined, such as pre-generated vegetations (trees, grass, flowers...).

Maybe just generate vegetations and other geometries with ray marching in the future... :thinking:

Ray trace based GI

Ray tracing, as a more futuristic and emerging technique, however, can be applied to arbitrary geometry. For the terrain, the model with fixed LoD can be generated before rendering, followed by the generation of an acceleration structure, and use this low-quality model for the reflected scene (again, we don't need a superior quality for stuff rendered in the water). Things like vegetations can also be added for ray tracing easily.

Since the sky is rendered with ray marching, if the ray miss the geometry, it needs to feed back to the rendering pipeline and invoke the traditional sky renderer to fill in the missing pixels.

There is no point to implement a ray tracer from scratch, especially when there exists ray tracing API nowadays that exploits the power of GPU. I might end up consider either Vulkan or OptiX for this purpose.

So which one

I don't know, it is a very difficult task to add a global illumination path to a rasterisation pipeline, and handle all sorts of texture, buffer, state sharing and synchronisation. But it has to be done at some point.

Ray marching may give better performance but can be quite difficult for complex geometries..

Ray tracing should be more flexible but can be slow even with hardware acceleration, and regenerate the acceleration structure every time the terrain updates; also consumes a tons of GPU memory space and bandwidth. In addition, this will increase the hardware requirement to run the engine.

stephen-hqxu commented 2 years ago

Decision

After some research and experiments, it is definitely a good idea to implement global illumination using an existing ray tracing API rather than doing it myself with ray marching. As long as the ray does not bounce all over the place, a simple ray-primitive intersection test is surprisingly fast thanks to hardware acceleration, with only around 1ms under 1080p on a RTX 2080.

For API usage, we plan to use OptiX because 1) we don't have much experience with Vulkan and 2) OptiX is based on CUDA for which we are already familiar with when working with terrain generation. We will not consider DirectX Ray Tracing as it is only available on Windows.

Implementation plan

To implement a ray tracer for water reflection and transmission, we need to set up a global illumination pipeline. The function takes a ray origin, a ray direction and a stencil texture as inputs, and output a GI G-Buffer consist of position, UV and normal of the geometry where the ray intersects. The GI G-Buffer can be simplified at the beginning to contain only position because the engine currently only has a heightfield terrain and we can compute the UV and normal based on world position; if we need to add more geometries in the future it can be extended easily. The stencil texture is used to identify which pixel needs to be ray-traced, or if the ray misses the geometry.

Using the GI G-Buffer, we can compute the rasterisation G-Buffer by traversing through the STPScenePipeline but bypassing all vertex shaders. For example to get the G-Buffer for heightfield terrain we can simplify passing the point of intersection buffer and apply texture splatting techniques to texture the terrain and output the G-Buffer. Lighting step can be carried on as before with G-Buffer. Our current implementation of ambient occlusion only works on screen-space therefore for the GI pass we should skip AO calculation. For missed rays we invoke the environment renderers.

Finally this pipeline should returns a GI colour back to the original rasterisation scene pipeline; for water effect we should obtain a reflection and refraction colour textures and blend then based on some magic equations. This process is concluded by post processing before writing to the default framebuffer.

Evaluation

This idea takes a reasonable amount of additional memory for storing GI G-Buffer and the final colour, but for a hybrid rendering system this is yet the best approach to be used in modern real-time applications like games. It separates the ray tracing and rasterisation pipeline allowing asynchronous rendering and avoid hazards of data sharing because each pipeline only processes data as indicated by the corresponding G-Buffer.