Author Topic: Apr 10, 2016 - Planetary terrain  (Read 5586 times)


  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Apr 10, 2016 - Planetary terrain
« on: April 10, 2016, 12:28:44 PM »
With the atmospheric shaders in solid shape I decided it was time to go back to planetary terrain generation. In the previous post on this topic I explained how I succeeded in reducing memory usage down to a fraction of what it used to be, but I still had a long way to go before the planetary terrain is anything close to being usable.

First, the seams. Due to how graphics work, when vertices on one mesh don't perfectly overlap vertices on an adjacent mesh, seams occur which are easily visible when in the game. In the last screenshot of the abovementioned post I show exactly that issue -- there are vertices on one mesh that don't have a corresponding vertex on an adjacent mesh. Fortunately it's easy enough to fix by trimming edge triangles on the denser of the two meshes if it's adjacent to a mesh with a lower subdivision level:

With that out of the way moved the entire subdivision process onto worker threads using the handy WorkerThread class I created a while back that's capable of spawning threads up to the limit of 2*CPU cores, after which point it queues up functions and executes them as threads free up. The only thing that still needed to be done on the main thread was the actual setting of mesh geometry and collider mesh. Unfortunately the latter was always the performance hog, taking up twice as long as all the other operations executed in the same frame combined:

How to speed this up? Well, first, I don't actually need colliders on any but the deepest-most subdivision patches. This eliminated the majority of them right away. The rest? I simply staggered them out -- rather than creating all colliders when the data becomes available, I made it so that only one collider can be created per frame. This only takes about 2 milliseconds per frame, which is perfectly acceptable. This effectively makes the entire planet generate seamlessly and without any hiccups all the way down to the 10th subdivision:

The next step was to actually generate a terrain based on some useful data. Naturally, since I already had a high-res height map of Earth (8192x4096), I simply wrote a script that samples its height values:

  1. using UnityEngine;
  2. using TNet;
  4. [RequireComponent(typeof(QuadSphere))]
  5. public class EquirectangularHeightmap : MonoBehaviour
  6. {
  7.         public Texture2D texture;
  9.         public double lowestPoint = 0d;
  10.         public double highestPoint = 8848d;
  11.         public double planetRadius = 6371000d;
  13.         const float invPi = 1f / Mathf.PI;
  14.         const float invTwoPi = 0.5f / Mathf.PI;
  16.         void Awake ()
  17.         {
  18.                 if (texture != null)
  19.                 {
  20.                         float[] data;
  21.                         {
  22.                                 Color[] cols = texture.GetPixels();
  23.                                 data = new float[cols.Length];
  24.                                 for (int i = 0, imax = cols.Length; i < imax; ++i) data[i] = cols[i].r;
  25.                         }
  27.                         int width = texture.width;
  28.                         int height = texture.height;
  29.                         var sphere = GetComponent<QuadSphere>();
  30.                         var min = (float)(sphere.radius * lowestPoint / planetRadius);
  31.                         var max = (float)(sphere.radius * highestPoint / planetRadius);
  33.                         sphere.onSampleHeight = delegate(ref Vector3d normal)
  34.                         {
  35.                                 var longtitude = Mathf.Atan2((float)normal.x, (float)normal.z);
  36.                                 var latitude = Mathf.Asin((float)normal.y);
  37.                                 longtitude = Mathf.Repeat(0.5f - longtitude * invTwoPi, 1f);
  38.                                 latitude = latitude * invPi + 0.5f;
  39.                                 return Mathf.Lerp(min, max, Interpolation.BicubicClamp(data, width, height, longtitude, latitude));
  40.                         };
  41.                 }
  42.         }
  43. }

It looked alright from high orbit, but what about zooming in? Not so much:

Fortunately I was working on a game prototype a couple of years ago for which I wrote various interpolation techniques. The screenshot above uses the basic Bilinear filtering. Switching the code to use Bicubic filtering proves a much more pleasant result:

Hermite spline filtering is even better:

Still very blurry though, and increasing texture resolution is not an option. Solution? Add some noise! I opted to go with a pair of noises: a 5 octave ridged multifractal, and a 5 octave Perlin noise that results in this terrain:

Combining the hermite filtered sampled texture with the noise results in this:

It looks excellent all the way down to ground level with 16 subdivions:

At that subdivision level the vertex resolution is 19.2 meters. Since I'll most likely go with a planetary scale of 1/10th of their actual size for gameplay reasons, that will give me a resolution of under 2 meters per vertex, which should make it possible to have a pretty detailed terrain.

The downside of this approach right now is having to sample that massive texture... 8192*4096*4 = 134.2 megabytes needs to be allocated just to parse it via texture.GetColors32(). Another 134.2 MB is needed because I have to convert Color32 to float before it can be used for interpolation. Not nice... I wish there was a way to texture.GetColor() on a specific channel... The obvious way around it would be to use smaller textures, but that would cause even more details to be lost. I'm thinking of simply caching it by saving the result in a file after parsing the texture once.

In case you're wondering, it takes 50 milliseconds to generate a planet going down to 16th subdivision level in the Unity Editor:

The memory usage goes up by 300 MB when generating the planet, and since 134*2=268 MB of that is due to sampling the epic heightmap texture, that means the entire planet only takes ~30 MB in memory. Not bad!

I'm looking forward to seeing how it will look when I put some textures on it -- but that's going to be my focus next week.

Speaking of textures, I also tweaked the atmospheric shader from the previous post a little, making it more vibrant and improving the day/night transition to make it more realistic:

I achieved the improved fidelity by splitting up the scattering from just one color to two. One color is used for atmospheric scattering, and another is used for scattering close to the ground (terrain, clouds). The reason I had to do it was because when using just one color it doesn't seem possible to have a vibrant blue sky and a reddish tint night time cloud transition. Blue sky causes the night time transition to look yellow. To get a reddish transition I had to make the sky light turquoise colored which obviously looks pretty terrible. Using two scattering colors gave me the best of both worlds:

« Last Edit: April 10, 2016, 01:06:46 PM by ArenMook »