Unity ShaderGraph Procedural Skybox Tutorial Pt.1

Procedural skybox demo picture showing the skybox used in a demo game scene.

Unity ShaderGraph Procedural Skybox (Tutorial)

Create the sun and the sky,
And the stars and the clouds,
Add them all up,
And turn day into night

0.Introduction

Turns out.. it’s surprisingly easy to start creating your own custom skybox shaders with Unity + ShaderGraph…

In this tutorial we’ll build up a fully customizable skybox by creating functions in ShaderGraph for each different layer the skybox consists of, starting with the colors of the sky, then we’ll add the sun and a layer for the clouds and the stars and blend between day and night by the rotation of the directional light in the scene.

This tutorial was written using Unity 2019.2.4f1 using the LWRP template and later upgraded to work with Unity2019.3.1f1 and URP.

The only real difference between the LWRP and the URP shader graph is that you have to enable ‘Two Sided’ on the Unlit Master output node.. See step 1.5.

1. Creating the Skybox Material

1.1 Create a new unlit ShaderGraph

Right click in the Project window and select Create > Shader > Unlit Graph

1.2 Create a new material

Create a new material in the Project window and drag the ShaderGraph on top of it to assign it to the material.

1.3 Assign the material to the skybox

Open Window > Rendering > Lighting Settings and drag the material into the Skybox Material slot in the Environment settings.

1.4 Open the ShaderGraph

Double click on the Skybox ShaderGraph file in the project window to open the graph…

1.5 Unlit Master Node Settings URP

If you are using Unity 2019.3 and the URP then you have to enable ‘Two Sided’ in the unlit master node settings by clicking on the cogwheel icon:

ShaderGraph master node settings for the Universal Render Pipeline (URP).

1.6 Simple gradient sky Test

Just to make sure everything works we can create a very simple gradient skybox by using the green channel (Y-axis) of the normalised world position and feeding that into a sample gradient node. The normalised world position ranges from -1 at the bottom of the world to 1 at the top center of the world so in order to plug that value into the time input of the gradient it needs to be remapped to a 0 to 1 range first:

Image of a shader graph skybox node setup that creates a gradient in the sky.
Image displaying the shader graph skybox material added to the scene's lighting settings.

It would be really great if we could use gradients throughout the entire skybox but at the moment gradients in ShaderGraph cannot be turned into exposed properties so for the sake of not having to open the graph to make adjustments and so that we can use the same shader for different materials we’ll use three separate color properties instead and blend between those..

2. Adding a Sky Layer

This ShaderGraph nodes setup blends three exposed HDR color properties for the sky, horizon and ground. The softness of the gradient towards the horizon from the ground and from the sky can be adjusted with the Exponent1 and Exponent2 properties, the overall brightness with the Intensity property. By using HDR colors we can make the sky emit light coming from the middle horizon color for instance. Using HDR colors combined with a Bloom post-processing effect on the camera can make it look even better!:

Image of a shader graph skybox node setup that creates a gradient in the sky from three color properties.
Image of the shader graph skybox node setup that creates a gradient in the sky from three color properties used in the scene.

3. Adding the Sun

To add a sun to a skybox that has its position based on the direction of a directional light in your scene, a custom ‘Main Light’ node that gets the direction of the Main Light in the scene has to be created first..
You can find detailed instructions on how to create this custom main light node (and custom nodes in general) in this Unity blog post (opens in a new tab).: blogs.unity3d.com/2019/07/31/custom-lighting-in-shader-graph…

The best way to create a custom node that we can reuse in other graphs as well is to create a Custom Function node first and then convert it into a Subgraph.
Both the custom node function .hlsl file and the SubGraph can be downloaded from here (see the Subgraphs and CustomNodes folders): github.com/Timanious/MyShaderGraphs…

Or if you want to create this custom node yourself, you can follow along with these instructions to create the Main Light node…

3.1 Create a Custom Function node

Right click in the ShaderGraph working area and create a new Custom Function node, you can find it under ‘Utility’:

Image showing a newly created shader graph custom function node.

3.2 Add input and output parameters

Click on the settings cogwheel from the Custom Function node and add the following input and output parameters:

Image of a shader graph custom function node with input and output properties added.

3.3 Function name

The name of the function inside of the .hlsl file that this node is going to use is called MainLight, type that into the Name field:

Image of a shader graph custom function node showing the name of the node which is MainLight with capital M and L.

3.4 Adding an HLSL include file

As you can see the Custom Function node has the option to use a inline function if you switch the type to string, (see the Unity blogpost for more information about that) but using a separate HLSL include file gives us more flexibility, it can contain more complicated functions and you can keep them all organised in one place.

At the time of writing this, Unity doesn’t have a convenient menu item for creating an HLSL include file asset, so you’ll have to create it yourself, for example by creating a normal unlit .shader or .cs file and changing the file extension to .hlsl and removing the code from it..
Create a file named CustomLighting.hlsl, paste the code from below into it and save it : 

#ifndef CUSTOM_LIGHTING_INCLUDED
#define CUSTOM_LIGHTING_INCLUDED

void MainLight_float(float3 WorldPos, out float3 Direction, out float3 Color, out float DistanceAtten, out float ShadowAtten)
{
#if SHADERGRAPH_PREVIEW
    Direction = float3(0.5, 0.5, 0);
    Color = 1;
    DistanceAtten = 1;
    ShadowAtten = 1;
#else
#if SHADOWS_SCREEN
    float4 clipPos = TransformWorldToHClip(WorldPos);
    float4 shadowCoord = ComputeScreenPos(clipPos);
#else
    float4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
#endif
    Light mainLight = GetMainLight(shadowCoord);
    Direction = mainLight.direction;
    Color = mainLight.color;
    DistanceAtten = mainLight.distanceAttenuation;
    ShadowAtten = mainLight.shadowAttenuation;
#endif
}

void MainLight_half(float3 WorldPos, out half3 Direction, out half3 Color, out half DistanceAtten, out half ShadowAtten)
{
#if SHADERGRAPH_PREVIEW
    Direction = half3(0.5, 0.5, 0);
    Color = 1;
    DistanceAtten = 1;
    ShadowAtten = 1;
#else
#if SHADOWS_SCREEN
    half4 clipPos = TransformWorldToHClip(WorldPos);
    half4 shadowCoord = ComputeScreenPos(clipPos);
#else
    half4 shadowCoord = TransformWorldToShadowCoord(WorldPos);
#endif
    Light mainLight = GetMainLight(shadowCoord);
    Direction = mainLight.direction;
    Color = mainLight.color;
    DistanceAtten = mainLight.distanceAttenuation;
    ShadowAtten = mainLight.shadowAttenuation;
#endif
}

void DirectSpecular_float(float3 Specular, float Smoothness, float3 Direction, float3 Color, float3 WorldNormal, float3 WorldView, out float3 Out)
{
#if SHADERGRAPH_PREVIEW
    Out = 0;
#else
    Smoothness = exp2(10 * Smoothness + 1);
    WorldNormal = normalize(WorldNormal);
    WorldView = SafeNormalize(WorldView);
    Out = LightingSpecular(Color, Direction, WorldNormal, WorldView, float4(Specular, 0), Smoothness);
#endif
}

void DirectSpecular_half(half3 Specular, half Smoothness, half3 Direction, half3 Color, half3 WorldNormal, half3 WorldView, out half3 Out)
{
#if SHADERGRAPH_PREVIEW
    Out = 0;
#else
    Smoothness = exp2(10 * Smoothness + 1);
    WorldNormal = normalize(WorldNormal);
    WorldView = SafeNormalize(WorldView);
    Out = LightingSpecular(Color, Direction, WorldNormal, WorldView,half4(Specular, 0), Smoothness);
#endif
}

void AdditionalLights_float(float3 SpecColor, float Smoothness, float3 WorldPosition, float3 WorldNormal, float3 WorldView, out float3 Diffuse, out float3 Specular)
{
    float3 diffuseColor = 0;
    float3 specularColor = 0;

#ifndef SHADERGRAPH_PREVIEW
    Smoothness = exp2(10 * Smoothness + 1);
    WorldNormal = normalize(WorldNormal);
    WorldView = SafeNormalize(WorldView);
    int pixelLightCount = GetAdditionalLightsCount();
    for (int i = 0; i < pixelLightCount; ++i)
    {
        Light light = GetAdditionalLight(i, WorldPosition);
        half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
        diffuseColor += LightingLambert(attenuatedLightColor, light.direction, WorldNormal);
        specularColor += LightingSpecular(attenuatedLightColor, light.direction, WorldNormal, WorldView, float4(SpecColor, 0), Smoothness);
    }
#endif

    Diffuse = diffuseColor;
    Specular = specularColor;
}

void AdditionalLights_half(half3 SpecColor, half Smoothness, half3 WorldPosition, half3 WorldNormal, half3 WorldView, out half3 Diffuse, out half3 Specular)
{
    half3 diffuseColor = 0;
    half3 specularColor = 0;

#ifndef SHADERGRAPH_PREVIEW
    Smoothness = exp2(10 * Smoothness + 1);
    WorldNormal = normalize(WorldNormal);
    WorldView = SafeNormalize(WorldView);
    int pixelLightCount = GetAdditionalLightsCount();
    for (int i = 0; i < pixelLightCount; ++i)
    {
        Light light = GetAdditionalLight(i, WorldPosition);
        half3 attenuatedLightColor = light.color * (light.distanceAttenuation * light.shadowAttenuation);
        diffuseColor += LightingLambert(attenuatedLightColor, light.direction, WorldNormal);
        specularColor += LightingSpecular(attenuatedLightColor, light.direction, WorldNormal, WorldView, half4(SpecColor, 0), Smoothness);
    }
#endif

    Diffuse = diffuseColor;
    Specular = specularColor;
}

#endif

3.5 Assigning the .hlsl file

Drag the CustomLighting.hlsl file into the text asset Source slot to assign it to the custom function node:

Image of a shader graph custom function node with with the CustomLighting.hlsl file added to the Source slot.

3.6 Convert to Sub-graph

Right-click on the Custom Function node and select ‘Convert to Sub-graph’. Name the sub-graph MainLight:

Image displaying the right click menu pointed at the Convert To Sub-graph option of the Custom Function node.

3.7 Add Sub-graph Outputs

Open the new Subgraph and Connect the WorldPos input from the Custom Function node to a Position node. Also create 4 new inputs on the Output node corresponding with the outputs from the Custom Function node:Screenshot 2019-09-03 at 14.22.20.png

3.8 Organising the Sub-graph node

If you look at the Blackboard, you’ll see that underneath the name of the Subgraph in a darker tint grey you can specify where the subgraph is organised in the node creation menu. If you change it to Input/Lighting it will be sorted with the other Input/Lighting nodes, so it will be easy to find:

Image displaying the shader graph name on the blackboard and the organisation name beneath.
Screenshot 2019-09-03 at 14.31.38.png

3.9 Positioning the Sun

Now that we can get the direction of the Main Light in the scene, we can use it for positioning a sun:

Screenshot 2019-09-03 at 14.47.30.png

This graph works by getting the dot product from the view direction and the inverse direction of the main light. The size, intensity and softness of the sun can be adjusted with the radius, intensity and exponent variables. The color of the Main Light in the scene is also used to give the sun color.

3.10 Add the Sun to the Sky

The last step is to simply add the output from the Sun to the sky color with a Add node:

Image displaying the shader graph with the sun and the sky layers added with an add node.
Image displaying the shader graph sun used in the game scene.

4.Adding a clouds layer

Building up a clouds layer has a lot of steps, but it starts simple, so for this I think it’s best if we build it up step by step, then you can decide how much information your clouds layer needs.

For the purpose of better showing you what’s going on this tutorial uses an RGB circles testing image created with Photoshop for most of the examples as well as a noise/clouds texture + normal map created with CatlikeCoding’s NumberFlow Unity plugin (which is also a node based application but for creating procedural textures).
You can download the textures used in this tutorial from this blogpost: https://timcoster.wordpress.com/2019/09/09/tileable-clouds-texture/

Or if you are going to use your own textures, then what probably works best is using an actual transparent .PNG file instead of a black and white one. This way you won’t get clouds with grey edges but with transparent white edges instead. Make sure to set the Alpha Source to Input Texture Aplha in the import settings and also make sure the texture has an Alpha channel, you can see if it has one in the preview window if you see a white A after RGB :

Screenshot 2019-09-06 at 12.43.27

4.1 Adding a clouds layer

Image displaying the clouds layer for the skybox shader with a debug color circles image used.

The node setup below generates an infinitely large flat layer for a clouds texture:

Image displaying the clouds layer shader graph nodes setup.

We can blend the transparent clouds texture with the sky colors and the sun, using a blend node. By using the Alpha channel from the texture for the opacity of the Blend node and setting the Blend node to overwrite, we can get a nice clean result:

Screenshot 2019-09-06 at 13.02.41.pngScreenshot 2019-09-06 at 12.57.38

4.2 Adding far tiling

By adding a property for far tiling we can make it look like the horizon is a little bit closer.
for the RGB circles example it was set to 0.1 and for the clouds to 0.25:Screenshot 2019-09-06 at 13.08.31.png

4.3 Adding Texture Tiling

Adding a texture tiling property gives us control over the size of the texture, here it was set to .05:

Image displaying the shader graph node setup to add texture tiling to the clouds layer.

4.4 Adding Texture Offset

Adding in a Vector2 clouds texture offset property will be useful for animating the clouds later on:

Image displaying the shader graph node setup to add texture offset to the clouds layer.

4.5 Adding Cutoff

Adding Distance and Cutoff properties to control fading out the clouds towards the horizon. For the examples below the distance was set to 0.8 and the cutoff to 25:

Image displaying the shader graph node setup for how to add distance and cutoff to the clouds layer.

4.6 Adding Opacity

Opacity can be added by Multiplying the output of the Alpha channel of the clouds texture with a new Vector1 clouds opacity property. The example shows the opacity at 0.5:

Image displaying the shader graph node setup to add texture opacity to the clouds layer.

4.7 Adding a Normal Map

Adding support for a normal map texture to suggest depth based on the direction of the light. This Makes it look like shadows are at the bottom of the clouds when the directional light shines straight down. When adding the Sample Texture node for the normal map, make sure to set the node type to Normal. Also make sure to set the clouds normal map texture property to ‘Bump’ mode on the blackboard.
The custom Main Light function node is used outside of the subgraph here because it uses the tangent position instead of the world position. You can simply copy the custom node from the Main Light subgraph.

Normal map strength was set to 0.8 for the example:

Image displaying the shader graph node setup to add a normal map texture to the clouds layer.

4.8 Adding Brightness

Add a new Vector1 property for clouds brightness, and a new add, subtract and multiply node.
Subtract 1 from the brightness value and then add it to the output of the blend node.
Subtracting one from the brightness will make it so that a brightness value of 1 will be normal and a value of 0 will be no brightness at all, instead of zero being the normal brightness.
Also multiply the normal map strength by the brightness property before plugging it back into the blend node, to make it dependent on the brightness value.
Left and right examples show brightness values of 0.1 and 1.5:

Image displaying the shader graph node setup to add brightness to the clouds layer.

4.9 Adding Directional Light Color

Multiplying the texture by the Main Light color output will make the color of the clouds dependant on the color of the directional light in the scene. For the examples the light color was set to light orange and light blue:

Image displaying the shader graph node setup to add directional light color the clouds.

4.10 Adding Clouds Color

Now that the hard bits for the clouds layer are done, let’s add in something a bit more easy, which is the basic clouds color. For this, make a new Color property called Clouds Color and drag it onto the graph, also make a new Blend node. Blend the Main Light color with the Clouds Color before plugging it into the multiply node at the end.
For the example below the sun color was set to light orange and the Clouds Color to light blue HDR with a high intensity:

Image displaying the shader graph node setup to add a clouds color property to the graph.

4.11 Reorganise Layers

To make the adding of the different layers make more sense we can rearrange the graph so the order will be Sun + Sky + Clouds.
This way what is closest by will be added last:

5. Adding a Night-Sky

Now that we have a sun sky and some basic clouds we can start reaching for the stars!…(yeah, yeah..)…
But in order to see those stars better when we add them it will be handy if we can automatically transition from day-sky colors to night-sky colors using the direction of the sun for the blending between them.
We can do this by using almost the same position input that we use for the sun to Lerp from color a to b, and while we’re at it we can do the same with the sky exponent 1 and 2 properties to make the horizon look thinner at night.

So before we add the stars we’ll have to revisit the sky colors layer..

5.1 Adding night-sky properties

Add three new Color properties for the night-sky colors and add two new Vector1 properties for the night-sky exponents:

Screenshot 2019-09-08 at 15.05.59.png

5.2 Lerping Colors

Swap the three color and two exponent property nodes with Lerp nodes and connect them in the way that you see in the diagram below.
First image is the old sky and the second is the new sky layer with all the new nodes selected so they show a blue line around them:

Screenshot 2019-09-08 at 15.03.46
Screenshot 2019-09-08 at 14.57.17.png
ProceduralSkyBoxDayNightTrans

6. Adding Stars

Now that we can transition to dark night colors we can start adding the stars layer to the skybox. This tutorial uses a very simple black and white stars texture, but you can go as fancy as you want of course:

StarsBlack.jpg

6.1 Sphere Mapping Space

The following node setup gives us a nice spherical mapping for a stars texture. There are some weird visual artefacts from the texture not being made to wrap around a sphere but that’s not really a problem because the texture will be faded out towards the horizon:

Screenshot 2019-09-08 at 16.36.06.png

6.3 Add Intensity

Multiply the RGB output from the stars texture by an intensity value to control how bright they shine:

Screenshot 2019-09-08 at 16.43.05.png
Screenshot 2019-09-08 at 17.21.23

6.4 Add Texture Offset

Adding a Vector2 offset value to the UV coordinates before plugging it in the texture gives us control the position of the stars texture, useful for animating later on:

Screenshot 2019-09-08 at 16.44.28.png

6.5 Add Horizon Fade-out

To gently fade out the stars towards the horizon we can multiply the texture again by using a power node and a fadeout property:

Screenshot 2019-09-08 at 16.46.04.png
Screenshot 2019-09-08 at 17.23.53

6.6 Add Day/Night Transition

Finally add a day to night transition by multiplying again by a Lerp node using the dot product of the light direction for the Lerp node’s time input. Now the stars will be completely visible when the directional light is shining straight up and completely invisible when shining straight down:

Image displaying the shader graph node setup to add a day night transition to the stars layer.

6.7 Adding the Stars Layer

Rearrange the graph so the stars come on top. They are farthest away so the stars layer is drawn first.

The complete Procedural Skybox ShaderGraph:

Large image displaying the complete procedural skybox shader graph with all the layers.

Adding everything up and using it on a small testing scene with a floor, exponential fog with a very low value of about 0.0006 and some post processing effects enabled it already starts to look pretty game-ee!:

Animated GIF displaying the changes of the sky color etc. of the day night cycle while the sun is rotating.

7.0 Finally

Want to learn how we can access the properties of this Skybox ShaderGraph from a C# script to create a Day/Night Cycle for a game next?? Then click on the button below to go to part 2!:

BUY ME A COFFEE

Donate $5 to buy me a coffee so I have the fuel I need to keep producing great tutorials!

$5.00

Website Powered by WordPress.com.