Earthican Shader

In my previous post, I described a process for generating terrain on a spherical mesh. As the screenshot showed, the algorithm worked pretty well to give you a lumpy looking planet or asteroid. But-- what if you want it to look like Earth? Green continents, blue water? You know the Earth I'm talking about.

Well here's my first attempt:

Not bad!
Not bad!

The caption says it all.

How does he do it?

In order to get this Earthy feel I really only needed a proper surface shader, since the mesh geometry is uniform across the surface regardless of the bumps and whatnot. First, my properties:

_Center ("Center Point", Vector) = (0, 0, 0, 0)
_OceanColor ("Ocean Color", Color) = (0, 0.1, 0.7, 1)
_LandColor ("Land Color", Color) = (0, 0.7, 0.1, 1)
_BlendDistance ("Blend Distance", Float) = 1
_BlendThreshold ("Blend Threshold", Float) = 0.001

_Center defines the center point of the sphere. This is so that I can calculate how high a vertex is, i.e. its distance from the center of the planet. Then there are colors for the ocean and water. _BlendDistance essentially defines sea level, and _BlendThreshold defines the distance to blend across. These default values are what you see in the screenshot.

Then we define a vertex program:

void vert (inout appdata_full v) {
	float dist = distance(_Center.xyz, v.vertex.xyz);

	// flatten the oceans
	if (dist <= _BlendDistance) {
		v.normal = normalize(v.vertex.xyz);
		v.vertex.xyz = normalize(v.vertex.xyz);
		v.vertex.xyz *= (1 - _BlendThreshold);
	}
}

Stupid simple. Essentially you normalize any vertices that are below sea level, thereby smoothing out the oceans. You also want to correct the normals. Finally, we define a surface shader:

struct Input {
	float3 worldPos;
};

void surf (Input IN, inout SurfaceOutput o) {
	float dist = distance(_Center.xyz, IN.worldPos);
	float startBlending = _BlendDistance - _BlendThreshold;
	float blendFactor = saturate((dist - startBlending) / _BlendThreshold);

	half4 color = lerp(_OceanColor, _LandColor, blendFactor);

	o.Albedo = color.rgb;
	o.Alpha = color.a;
}

I've included the Input struct to show that the world position needs sent in as well. This is so that I can apply the correct colors at the correct distances. I use a simple lerp and apply it to the albedo which colors the earth appropriately.

Simple as that!

Further Thoughts

The only problem with this method, is that it doesn't give a ton of definition unless you have an extremely high resolution mesh. The mesh above has over ten thousand vertices. For oceans, which only need to look spherical, that's probably enough, but if you look at the areas where the continents meet the sea, it would be nice if there was more geometry to make the transitions smoother.

An interesting study I may pursue later is iterating over the edges of the land and adding more geometry.

Another idea, which dawns on me now that I'm flattening out oceans in the vertex program, is putting the procedural transformations of vertices actually in the shader, rather than transforming in an iterative fashion on the CPU.

Anyway, just a couple ideas.

5 Comments

  1. Marionette

    given that you know the radius, worldPos and add in the worldNormal to get direction, couldn't you solve for _Center.xyz instead of passing it in, thus removing the cpu bound part of it? and if so, what would that formula look like. it's been driving me crazy for a day now...

    1. thegoldenmule

      You are correct, I definitely do not need to pass in _Center. This would probably work with something like:

      float4 center = mul(UNITY_MATRIX_MVP, float4(0,0,0,0));

      The center in model space is the zero vector, which you multiply by the MVP matrix to find the center in world space.

      Nice observation!

  2. Marionette

    first, thank you very much for the fast response 😉

    to be honest, I didn't know if you still checked this article or not.

    the above equation doesn't seem to work... hehe I don't have much hair left lol

    I was thinking something along the lines of: (worldPos * -worldNormal) - (radius /2).. or something like that, but it doesn't work either

    1. thegoldenmule

      Hmmm, I'm not sure why that wouldn't be correct. I don't have the code on this computer anymore, or I'd do some more digging.

      Regardless, uploading a single float4 uniform is very unlikely to cause any sort of bottleneck. This is not CPU bound once the mesh is built, it's more probably vertex bound.

Leave a Reply to thegoldenmule Cancel reply