Procedural Terrain Rendering How-To

Question to those of you who implemented a procedural terrain, maybe even in Unity3D.

In my Unity implementation I got the circular artefacts eleminated, and also a problem that the planet was culled away when the center was not in the camera frustum. Basically things go very well. I can now start work to optimize it for being close to the surface. Hurray :grinning:

While things go pretty well when being in orbit, problems occur as expected :worried: when going down to the terrain. The earth is right now rendered in full scale, so 6371000 meter sphere radius.

There are two major issues I try to get under control

Problem a) Keep the number of triangles under control. Right now I render 32x32 vertices-planes and a quadtree until depth 20. I use a LODSphere approach by NavyFish to define the required LOD while recursively going down the quadtrees. During parsing the quadtree down I check if something can be splitted or merged (I put these nodes in a split or merge queue then that is worked on after the quadtree traversing has finished) I do a

  1. Plane Volume check (check if the camera is within the spaned volume of a plane
  2. Horizon Culling check
  3. Frustum Culling check (well, very roughly, lacking a good strategy in Unity, it only checks if an object is behind the camera, but not if its within the frustum)

I do each check for the current quadtree node and its parent. If for example both nodes are occluded, I merge to the parent node, until the parent is not occluded or the root node is reached. So there is always a full sphere rendered, but if a section is totally occluded (e.g. the backside of the planet), its rendered at 32x32 vertices. Unfortunately I have no chance to get below 7-16M triangles.

What culling strategies do you use to keep the number of triangles low? One problem of mine could be that the level of detail currenty fully depends on the number of vertices rendered. Probably texturing or moving away from per-vertex-normals (need to review some of your posts) could help to detail out the terrain while keeping the number of triangles low?

PS: Culling checks on CPU is really bad if you dont have the terrain height information (the noise) on the GPU :sleepy:

Problem b) Light see-through/Z-Fighting when close to the surface. I get heaviest Z-Fighting when I go close to the surface, there is bad “see-through” going on.

EDIT 2:
Problem b) Light seethrough solved. For everyone having the same problem, you need to make sure that you have shadowing enabled for your light source. Otherwise it can pass objects and you can see structures behind the foreground. Now as that this (shadowing enabled) can lead to heavy problems when using a point light in a certain distance (Unity will warn you, or throw the evil “notNormalized(normal)”), it is better to switch to a “Directional Light” lightsource. Which should be fairly similar to a sun lightsource in space, as the light rays would reach the earth close to parallel due to the distance.

I recorded a video of the current state. While leaving the planet again, I noiced there is still a bug as some part of the planet didnt merge back to low LOD. But it looked so beautiful that I needed to keep recording and take a separate picture :joy:

Anyway, as now culling is crucial, any hint on your strategies to increase performance (with culling or anything else) and thus allow additional terraindetail is very much appreciated!

2 Likes

Just a quick note on this, you are hitting the float precision barrier at this point, a float is (-3.4 × 10^38 to +3.4 × 10^38 [7 digits]). That means that you will get precision issues at around 2m level, if you were to add a moon on that same coordinate system, the resolution would be lowered further.

I think there are a few ways to deal with this, one is to use logarithmic depth buffering and maybe even logarithmic positions. Another way could be to setup your scene with different cameras, with different clipping planes, for instance on the ship camera you might even need it to go down to millimeter scale while the terrain scale might be adequate at meter scale from space or high atmosphere. And of course you can rewrite the inner workings of the engine to support doubles on lowest level, something that is probably not possible with Unity.

I posted a logarithmic depth buffer implementation for a Unity shader above, if you need it.

IMO you might want to consider moving your Normal data into a texture, you will gain resolution on the terrain and can lower your tris count considerably, while still achieving the same resolution, going with 512 or 1024 or 2048 Normal texture input you might be able to cut the tris by more than half.

In the sense of archiving as I PMed this with cybercritics, just in case someone needs to pick this problem up also shortly here. I did workaroung this in Unity by setting up multiple cameras and “spaces”, basically the approach by Kerbal Space Program. It is described in post #42 with the required piece of code in case anyone needs it. There was a suggestion by Ryan Dean on google to use colliders instead of checking distances, which I still find a nice approach. I described my thoughs in thread #94. I haven’t been able to fully implement this, as planets are too large to fit into a reasonable camera collider-box and there was quite a bit of effort needed to determine when to scale up or down.

Currently I research a third idea to scale objects continuously (and not only at certain distance or collider-events) by using the angular diameter formula at https://en.wikipedia.org/wiki/Angular_diameter

Formulas: g = 2rtan(α/2) || r = g/(2tan(α/2)) || α = 2*arctan(g/(2r))

I think continues scaling at moving could be possible using that formula (as the FOW of the camera and the (theoretical) distance of the object is known at any frame). I havent been able to get my hand around how to use that formular right in a per-frame-check&rescale implementation, so that still requires a proof-of-concept.
But I am sure combined with floating origin this could be a third approach to continously fit large scales into float limitations.

Hi,

as I am moving away from per-vertex-normal to a NormalMap (created entirely on GPU with a Compute Shader), so I need a new strategy on my Compute Shader layout. Additionally I want to create a higher resolution Surface-Color-Texture (to move away from per-vertex-color only).

My current structure is (in the example I assume we want to create a 32x32 plane mesh):

Kernel1 (34x34x1 threads):

  • Create Vertex Positions to Stage1-Buffer (34x34). Here I do Noise calls based on each Vertex-position.
  • Create Noise-Values to Stage1-Buffer (34x34).

Kernel2 (32x32x1 threads):

  • Create per-vertex-normals (32x32) (with vertex-information from Stage1-Buffer) to Final-Buffer (32x32).
  • Create slope-values (32x32) (with vertex-information from Stage1-Buffer) to Final-Buffer (32x32).
  • Create TerrainType-Value (that identifies the terrain at each vertex) to Final-Buffer (32x32).
  • Copy from Stage1-Buffer (34x34) to Final-Buffer (32x32) the Vertex-positions and Noise-values.
  • Additional: Create 32x32 NormalMap RenderTexture.

-> I did the normal calculcation in Kernel2 as I then have all neighbouring information available (including all at the edges), thats why I didnt calulcate the normals in stage1 and thats why I do 34x34 threads (for the additional edge information) in stage1.
-> Result is: 32x32 Compute Buffer with Vertex-Positions, Noise, Normals (per Vertex). Additionally a normalmap in 32x32.

–

Now I want to enlarge the normalmap to gain more detail at no cost of a higher number of triangles. I’d be interested at your thoughs of my current idea how to do it. Any advice very much appreciated.

Assuming we want to create a 32x32 mesh with a 512x512 normalmap and surface texture, the new layout could be:

Kernel1 (32x32x1 threads):

  • Create Vertex Positions to Final-Buffer (32x32). Here I do Noise calls based on each Vertex-Position.
  • Create TerrainType-Value (that identifies the terrain at each vertex) to Final-Buffer (32x32).

Kernel2 (514x514x1 threads):

  • Create Noise-Values to Stage2-Buffer (514x514). Here I do Noise calls based on temporary created positions (same used for vertex position calculation).

Kernel3 (512x512x1 threads):

  • Create NormalMap RenderTexture (512x512) (with noise-information from Stage2-Buffer)
  • Create Surface-Color-Texture RenderTexture (512x512) (with noise-information from Stage2-Buffer) (NEW)

Questions:

  • Generally, do you see bigger flaws in that layout (e.g. the encrease to three kernels)?
  • I am unsure what I should store in Kernel2 to be able to calculcate the normalmap in Kernel3? Here I assume storing the noise-information, but, is there an easy way to calculcate normals on a sphere when just having the noise (height) information? Should I store the temporary positions (used to do the noise calls) instead?
  • Where should I create the slope values? I need them to define the terraintype for texturing (so that is not only related to height), so logically I would need that information (temporary) in kernel3 when I do the new Surface-Color-Texture.
  • I assume storing the TerrainType (32x32) should be sufficient when I want to do additional texturing, which in my understanding is done later in the Surface-Shader and per Vertex e.g. by using an AtlasTexture?
  • Would you store anything else created in the Compute Shader for later?

Hey guys, apologies for being a bit absent lately. Life’s been busy. Glad @cybercritic brought up the issue of precision.

@JoergZdarsky - what was the root cause of your circular artifacts? Also, how did you end up fixing the planet culling? I know we (in PM) discussed setting the mesh’s bounds explicitly in Unity, but you weren’t initially having success with that.

I think you’re producing tons more triangles than necessary, so am glad you’re going towards a Normal Map. I came up with a formula a couple years ago to calculate the LOD distances needed to maintain a worst-case ‘screen-space density’ of vertices, based upon the client’s screen resolution and camera’s field of view. It makes a lot of assumptions, but is what I use for a starting point:

Terms:

Pmax = Target maximum # of pixels between vertices (regardless of LOD)
S = Vertex Spacing in World Units at highest LOD (smallest spacing)
W = Screen Width
H = Screen Height
FOVh = Horizontal Field of View
N = Level of Detail, runs from 0 to LODMax, with LOD 0 being the highest detail
D = Distance at which to transition to LOD N in order to maintain PMax

D = 2 * H *S / ( 2 * tan(.5 * FOVh*(H / W)) * Pmax * (2 ^ N))

So, as an example, if you wanted to maintain a resolution of less than 10 pixels between each vertex, and at LOD 0 your vertices were .1 world units apart (i.e. 10 cm):
(W = 1920, H = 1080, FOVh = 90):

LOD 0:  D = 22.83468146
LOD 1:  D = 45.66936292
LOD 2:  D = 91.33872585
LOD 3:  D = 182.6774517
LOD 4:  D = 365.3549034
LOD 5:  D = 730.7098068
LOD 6:  D = 1461.419614
LOD 7:  D = 2922.839227
LOD 8:  D = 5845.678454
...

Please note there are tons of assumptions in this formula. i.e. you’re always facing perpendicular to the patch’s normal direction, slope is 0, etc. But the assumptions are made to cover the worst case scenario, so it will error on the side of giving better resolution.


The problem with calculating the vertex positions to an extremely fine detail, calculating their normals, and then “baking” that into the normal map is that you’re running a massive number of noise computations - say 8 octaves of noise at a 512x512 resolution - that’s 16x the density of your terrain mesh! This will be incredibly slow to calculate.

You might want to explore using a tiled higher-frequency noise (i.e. perhaps 16x higher) to generate a normal map which ‘perturbs’ the underlying normal data you’ve already calculated in Kernel 2. Your Normal Map will still be coupled to the terrain (due to using position as input to the noise generator), but will only need ~3 octaves to calculate each texel. You may need 2 calculations per texel for this perturbation vector (i.e. x perturbation and y perturbation).

Alternatively, you could generate several noise-based tileable normal map textures and perturb the calculated vertex normals with these. This would be even cheaper.

To be honest though, I’m still struggling with a good solution for this myself. Would be curious to hear from others in this thread!

I needed to create a bounding box for the protoype mesh itself, not a separate one for the GameObject.
So its been this additional line of code in the start routine applied to the once created prototype mesh.

using UnityEngine;

[RequireComponent(typeof(GameObject))]
public class SpaceObjectProceduralPlanet : MonoBehaviour {

    public Mesh prototypeMesh;
    public float planetRadius;

    // Initial call. We setup the shaders and prototype meshes here.
    void Awake () {
        // Mesh prottype (used for the vertex shader combined with compute shader output)
        this.prototypeMesh = MeshServiceProvider.setupDummyMesh(nVertsPerEdge); // MeshServiceProvider provides the Unity mesh (in this case a plane)
    }

    // We initialize the buffers and the material used to draw.
    void Start()
    {
        // Bounds (To avoid culling when using rendering in the shader)
        this.prototypeMesh.bounds = new Bounds(new Vector3(0, 0, 0), new Vector3(this.planetRadius * 2.0f, this.planetRadius * 2.0f, this.planetRadius * 2.0f));
    }
}

Hello everyone!
So, after i get dx11 on board GPU i ran in to the compute shaders.
Results are awesome!
20ms vs 0.015ms!!!

So. I have only one question:

Why i have that “angled output”. If you go closer you can see, that vertices on bottom have smaller distance between others.

Sorry for my English. My bad.

Oh. My bad. Vertices problem fixed


1 Like

Congrats, great first steps!

Some results of my custom noise engine work.
Thx for all on this thread.
After reading this thread i know something about Compute Shaders and dx11 power! :smiley:

1 Like

Very beautiful
 nice noise function! Great craters.

Glad this thread has been helpful to you! That was the whole point of starting it :smile:

First of all this thread helpful for me in case of Compute Shaders, cuz before i have only dx9 gpu
 you know.

And i’am new in CS.

But i know a lot of aspects about planetary rendering from CPU side. In case of cooperate with you, guys, i can share helpful infoprmation, algorithms, examples, and other tricky stuff. Would be great!
And again - Sorry for my English :smile:

HI zameran, welcome!! And congrats for the nice results!

Your asteroid selena looks beautiful. What kind of noise is that? I’ve tried to produce asteroids using Voronoi noise. In my opinion Voronoi it is very suitable when you try to create smaller asteroids in the kind of rock splinters (hope that is the right english word?) of asteroids of a few meters to kilometer.
But you algorithm seems to be pretty suitable when it comes to planets been hit by asteroids, leaving craters on the surface.

Hmm I havent had this issue until now. Please note that I only set this GenerationConstantsBuffer only once, but for each plane (=each quadtree node). So there are many instances (as much as there are planes) where each Compute Buffer only uses the first index. The Compute Shader only needs to read from the constants buffer, there was no need yet to write to it in the shader. Maybe it helps, here is my code how I set the Compute Buffer up. But I guess you’ve already come that far. I guess you are using multiple indexes of the ComputeBuffer array, I’ve found it easier to create one array (with one index) per plane (as I store all buffers in the corresponding quadtree node object).

// We create the buffers in that function (per plane / quadtree)
    void CreateBuffers(QuadtreeTerrain quadtreeTerrain)
    {
        if (quadtreeTerrain == null) Debug.LogWarning(this.name + "Trying to do CreateBuffers(..) of a quadtree that is null)");
        // Input Buffer: Buffer Patch Generation Constants
        quadtreeTerrain.generationConstantsBuffer = new ComputeBuffer(4, // 1x int (4 bytes) for one index, index = 0
            4 +     // nVertsPerEdge (int = 4 bytes), 
            4 +     // nVertsPerEdgeWithSkirt (int = 4 bytes), 
            4 +     // nPixelsPerEdge (int = 4 bytes), 
            4 +     // nPixelsPerEdgeWithSkirt (int = 4 bytes), 
            4 +     // scale (float = 4 bytes), 
            4 +     // spacingVerts (float = 4 bytes),
            4 +     // spacingPixels (float = 4 bytes),
            4 +     // LOD (int = 4 bytes),
            12 +    // patchCubeCenter (float3 = 12 bytes),
            12 +    // cubeFaceEastDirection (float3 = 12 bytes),
            12 +    // cubeFaceNorthDirection (float3 = 12 bytes),
            4 +     // planetRadius (float = 4 bytes),
            4 +     // terrainMaxHeight (float = 4 bytes),
            4 +     // noiseSeaLevel (float = 4 bytes),
            4);     // noiseSnowLevel (float = 4 bytes),
        PatchGenerationConstantsStruct[] generationConstants = new PatchGenerationConstantsStruct[1];
        generationConstants[0].nVertsPerEdge = quadtreeTerrain.parameter.nVertsPerEdge;
        generationConstants[0].nVertsPerEdgeWithSkirt = quadtreeTerrain.parameter.nVertsPerEdgeWithSkirt;
        generationConstants[0].nPixelsPerEdge = quadtreeTerrain.parameter.nPixelsPerEdge;
        generationConstants[0].nPixelsPerEdgeWithSkirt = quadtreeTerrain.parameter.nPixelsPerEdgeWithSkirt;
        generationConstants[0].scale = quadtreeTerrain.scale;
        generationConstants[0].spacingVerts = quadtreeTerrain.spacingVerts;
        generationConstants[0].spacingPixels = quadtreeTerrain.spacingPixels;
        generationConstants[0].nodeLevel = quadtreeTerrain.nodeLevel;
        generationConstants[0].patchCubeCenter = quadtreeTerrain.centerVector;
        generationConstants[0].cubeFaceEastDirection = quadtreeTerrain.parameter.cubeFaceEastDirection;
        generationConstants[0].cubeFaceNorthDirection = quadtreeTerrain.parameter.cubeFaceNorthDirection;
        generationConstants[0].planetRadius = quadtreeTerrain.parameter.planetRadius;
        generationConstants[0].terrainMaxHeight = quadtreeTerrain.parameter.terrainMaxHeight;
        generationConstants[0].noiseSeaLevel = quadtreeTerrain.parameter.noiseSeaLevel;
        generationConstants[0].noiseSnowLevel = quadtreeTerrain.parameter.noiseSnowLevel;
        quadtreeTerrain.generationConstantsBuffer.SetData(generationConstants);
        // Output Buffer(s)
        quadtreeTerrain.patchGeneratedStage1DataBuffer = new ComputeBuffer(nVertsWithSkirt, 16 + 12 + 4 + 4 + 4 + 12, ComputeBufferType.Default);   // Output buffer contains 
                                                                                                                                                              // vertex position (float4 = 16 bytes), 
                                                                                                                                                              // normals (float3 = 12 bytes), 
                                                                                                                                                              // noise (float = 4 bytes)
                                                                                                                                                              // slope (float = 4 bytes)
                                                                                                                                                              // terrainType (float = 4 bytes)
                                                                                                                                                              // patchCenter (float3 = 12 bytes)
        quadtreeTerrain.patchGeneratedFinalDataBuffer = new ComputeBuffer(nVerts, 16 + 12 + 4 + 4 + 4 + 12, ComputeBufferType.Default);   // Output buffer contains  
                                                                                                                                          // vertex position (float4 = 16 bytes), 
                                                                                                                                          // normals (float3 = 12 bytes), 
                                                                                                                                          // noise (float = 4 bytes)
                                                                                                                                          // slope (float = 4 bytes)
                                                                                                                                          // terrainType (float = 4 bytes
                                                                                                                                          // patchCenter (float3 = 12 bytes)
        quadtreeTerrain.patchGeneratedSurfaceMapTexture = new RenderTexture(nPixelsPerEdge, nPixelsPerEdge, 0);
        quadtreeTerrain.patchGeneratedSurfaceMapTexture.enableRandomWrite = true;
        quadtreeTerrain.patchGeneratedSurfaceMapTexture.Create();
        quadtreeTerrain.patchGeneratedNormalMapTexture = new RenderTexture(nVertsPerEdge, nVertsPerEdge, 0);
        quadtreeTerrain.patchGeneratedNormalMapTexture.enableRandomWrite = true;
        quadtreeTerrain.patchGeneratedNormalMapTexture.Create();
    }

Hello dear @JoergZdarsky. Thank you!

Asteroid alg. is pretty simple - specific voronoi noise distorted with some fbm. Also some hiils and cracs will be added later. After “Crater Noise calculation” output value passed in to simple Height Control function. So i can control borders of craters, depth, and some other parameters.

To say in tuth - a have big noise libriary in .cginc file
 Years of coding and porting from other apps :flushed: Like a practise in shader coding under Unity.
It contains a lot of “Space Dimension” sheet for procedural planets (and not only!). From “Stones” to “Egg-like binary stars”. :smile:
It also contains some cool code that provides cool coloring (now WIP)

Some output of that is the Space Engine like planets. Realy.

What about release it for free in that thread? Would be cool, yeah?

About buffer
 Thank you, i gonna try immediately!!!

You used Voronoi with FBM. Interesting. I wasnt aware that you can create those craters with it, spreaded on the spherical surface, but only applying holes like that. I only created more celular surfaces with it, to give simplex noise a more natural structure. Or like I did to push out certain “crack”-lines for an asteroid.

In the pure form (Voronoi and FBM, no simplex noise) I have cells spreaded all over the planet and mastered to create the larged golfball in the universe :joy:

Well done to concentrate Voronoi that nice so that it only applies to certain sphere areas to form a crater. :clap:

Wow well thats the best you can achieve :grin: Space Engine is really upfront, nothing gets over it when it comes to procedural space content generation (not even I:B).

1 Like

@JoergZdarsky can we cooperate closer? I mean skype or messager?
Anyway i can try to explain you the noise “magic” :smile:
And i have some questions to you too

Think about it. :wink:

P.s I’am obsessed with space and coding.

The PMs were actually mostly me moaning about how Normal textures are a good thing, at around midnight, while trying to figure out how it’s possible that there are so many muscles in the human body that can hurt after a proper day’s work (decided to extend my patio). :wink:

And somewhat on topic, a Pizza Planet from Frontier Developments.

/edit
@zameran, some pretty vids would be very nice.

2 Likes

@cybercritic, Hello!

A lot things need to be done on my noise library
 You should understand that i alone with TONN of code. Shader code, in which i’am not a master :smile:

But i realy wanna release dat thing, for you guys!

Hallelujah!

First attempt to several kernel CS and texture hookup!

Edit : And normals too! Awesome!

Awesome. That planet made my day!!:grinning:

1 Like