Post

Simple procedural skybox with a moon

We’re making a game and were looking for some references. This one by Mark Kirkpatrick seemed very interesting:

Mark Kirkpatrick art

I tried to recreate that in Unity and here’s what we got:

Unity art

I used standard procedural skybox shader and this set of shaders (https://github.com/keijiro/UnitySkyboxShaders) as a reference, but simplified it a lot. Here’s how it works.

Gradient

We have top and bottom colors and we need to blend them together. Skybox shaders are interesting, there’s a set of vertices that are rendered using texture coordinates and positions. We will use texture coordinates in our case. Here’s the vertex shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct appdata
{
    float4 position : POSITION;
    float3 texcoord : TEXCOORD0;
};

struct v2f
{
    float4 position : SV_POSITION;
    float3 texcoord : TEXCOORD0;
};

v2f vert(appdata v)
{
    v2f o;
    o.position = UnityObjectToClipPos(v.position);
    o.texcoord = v.texcoord;
    return o;
}

We just pass texture coordinates without any changes. As you can see, those texture coordinates are stored in a 3D vector. That vector represents a unit vector of direction towards the point on a skybox (much like a directional light rotation _WorldSpaceLightPos0 used in shaders. So Y component of that vector represents vertical direction of that point and we can use that to blend two colors together. All components change from -1 to 1 (Y component is -1 at the bottom and 1 at the top). If we normalize the Y component and blend our two colors together, we get a very smooth transition:

1
2
3
4
5
6
7
8
9
alf4 frag(v2f i) : COLOR
{
    half p = i.texcoord.y;
    
    // Moving [-1, 1] range to [0, 1]
    half normP = p * 0.5 + 0.5;
    half3 color = lerp(_GroundColor,_SkyTint, p);;
    return half4(color, 1.0);
}

Unity art

So we need a way to move the border closer to the center and squish it a bit.

1
2
3
4
5
6
7
8
half4 frag(v2f i) : COLOR
{
    half p = i.texcoord.y;

    float p1 = pow(min(1.0f, 1.0f - p), _Exponent);

    half3 color = lerp(_GroundColor, _SkyTint, p1);
}

We subtract current Y component from 1 and take a minimum of the subtraction and 1.0. As values are less than 0 after we cross a horizontal plane, subtraction absolute values will be more than 1, and this is where we will blend the Ground color. _Exponent parameter allows to tweak “hardness” of the gradient transition. This is how it looks with _Exponent = 15:

Unity art

Moon

The moon was a bit tricky and got me digging a lot of Unity shader sources. The idea itself is simple though.

  1. Set our moon center as a directional unit vector (just like the directional _WorldSpaceLightPos0 is stored)
  2. Find distance between the moon center directional vector and a skybox directional vector (our float3 textureCoords)
  3. Use that distance to calculate falloff of the moon circle using some function that allows to tweak the hardness of the moon edges.

Here’s a modified calcSunSpot function from the standard procedural skybox:

1
2
3
4
5
6
7
8
9
#define HARDNESS_EXPONENT_BASE 0.125

half calcSunSpot(half3 sunDirPos, half3 skyDirPos)
{
    half3 delta = sunDirPos - skyDirPos;
    half dist = length(delta);
    half spot = 1.0 - smoothstep(0.0, _SunSize, dist);
    return 1.0 - pow(HARDNESS_EXPONENT_BASE, spot * _SunHardness);
}

5 and 6 lines calculate distance between two directional vectors (how far is the fragment from the center of the moon)

7 line calculates the circular form of our moon. We take advantage of the smoothstep function clamping abilities outside of the interpolation range. Subtracting it from 1 gives us bigger values in the center (whites)

Unity art Smoothstepped moon without exponential squishing logic

The 8 line squishes our gradient transition. You can imitate it’s mathematical logic in Photoshop using Curves adjustments:

Unity art Unity art Moon inside Unity

All together

We need to blend our gradient with the moon now. We will also need our ground fog to affect the moon.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
half4 frag(v2f i) : COLOR
{
    half p = i.texcoord.y;

    float p1 = pow(min(1.0f, 1.0f - p), _Exponent);
    float p2 = 1.0f - p1;

    // Our moon circle
    half3 mie = calcSunSpot(_SunPosition.xyz, i.texcoord.xyz) * _SunColor;
    // Blend ground fog with the moon
    half3 col = _GroundColor * p1 + mie * p2;
    // Add sky color
    col += _SkyTint * p2;

    return half4(col, 1.0);
}

Line 9: calculating and coloring our moon (sun) spot Line 11: blending the ground color with the moon Line 13: adding the top (sky) color

This is what it looks like in the end (I changed the color of the moon from white to light cyan):

Unity art

One thing to mention — _SunPosition should be a unit vector (with a magnitude of 1). It’s tricky to set it by hand, so you can either use some helper methods to calculate a unit vector from rotation, or use one directional light on the scene just for its rotation (in this case either your lighting will depend on it’s rotation or you’ll need to use unlit shaders everywhere).

Here’s a full shader source. Create a new material and set it as skybox inside Unity’s lighting preferences window — https://github.com/KumoKairo/Procedural-Skybox/blob/master/GradientMoonSkybox.shader

Cheers.

This post is licensed under CC BY 4.0 by the author.