Creating a basic planet shader for mobile

I have a need to update a standard unity shader, in this case the Mobile/Diffuse shader. Essentially I want to add in some extra effects such as clouds and waves etc. The code for the built in shader is:

Shader "Mobile/Diffuse" {
Properties {
    _MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 150

CGPROGRAM
#pragma surface surf Lambert noforwardadd

sampler2D _MainTex;

struct Input {
    float2 uv_MainTex;
};

void surf (Input IN, inout SurfaceOutput o) {
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}
ENDCG
}

I have implemented these effects already in my own shader, where I used vertex and frag functions to create the effects. My shader can be seen here:

Shader "Unlit/MyShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Clouds ("Clouds", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct MeshData
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
            };

            sampler2D _MainTex;
            sampler2D _Clouds;
            float4 _MainTex_ST; // optional - Scaling and Tiling options in the editor

            v2f vert (MeshData v)
            {
                v2f o;
                o.worldPos = mul(UNITY_MATRIX_M, v.vertex); // object to world
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.uv += _Time.y * 0.03;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 topDownProjection = i.worldPos.xz; // shade according to xz world position
                fixed4 moss = tex2D(_MainTex, topDownProjection); // read the moss texture from top down

                float clouds = tex2D(_Clouds, i.uv); // sample the clouds texture
               
                // Create a lerp effect between the moss and a white color, using the clouds
                float4 finalColor = lerp(float4(1,1,1,1), moss, clouds);
                return finalColor;
            }
            ENDCG
        }
    }
}

The problem is that I don’t know how to adapt my code into the standard shader, since it does not have vert or frag functions. I’ve tried hacking together a solution but I always get errors. I’m new to shaders so if anyone could point me in the right direction that would be really appreciated. Can I somehow adapt my code into the “Input” or “surf” functions? I never saw these when doing shader tutorials so I’m not sure what they mean.

Is anybody able to help please? I managed to rewrite the default mobile/diffuse shader code with a vert and frag function. This is below:

Shader "Custom/CloudedSurface"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Clouds ("Clouds", 2D) = "white" {}
    }
    SubShader
    {
        Pass {
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"

                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                    float3 normal : NORMAL;
                };
                struct v2f
                {
                    float4 pos : SV_POSITION;
                    float2 uv : TEXCOORD0;
                    float3 normal : NORMAL;
                    float3 worldPos : TEXCOORD1;
                };
                sampler2D _MainTex;
                sampler2D _Clouds;
                float4 _MainTex_ST;
                float4 _Clouds_ST;
                v2f vert(appdata v)
                {
                    v2f o;
                    o.pos = UnityObjectToClipPos(v.vertex);
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                    o.normal = normalize(mul(v.normal, unity_WorldToObject).xyz);
                    return o;
                }
                fixed4 _LightColor0;
                fixed4 frag(v2f i) : SV_Target
                {
                    float dif = max(0.05, dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
                    fixed4 col = tex2D(_MainTex, i.uv);
                    fixed4 result = fixed4(col.rgb * dif * _LightColor0.rgb, 1);
                    return result;
                }
            ENDCG
        }
    }
}

You can see the result below, which is a very nicely lit planet:

What I really need to do though is apply a second texture, called “clouds” and be able to move this around the surface, so it looks like clouds are passing around the planet. Essentially it is just a sphere with two textures, as I need a simple shader that runs smoothly on mobile. No need for fancy details etc.

Any help would be really appreciated - I am still struggling to apply and displace the second texture, whilst keeping it under the influence of the lighting.

Hi, what does the cloud texture looks like?

If the cloud texture is seamless (depends on the planet’s UV), you can animate the cloud by changing its offset. (Texture’s Tiling & Offset)

To apply cloud texture:

  1. Consider cloud texture’s Tiling & Offset.

Skip step1 & 2 if you don’t use Tiling & Offset (1 Tiling, 0 Offset) for clouds. (use MainTex’s UV to sample cloud texture)

struct v2f
{
    float4 pos : SV_POSITION;
    float2 uv : TEXCOORD0;
    float3 normal : NORMAL;
    // Seems that you don't use world position in fragment shader.
    float3 worldPos : TEXCOORD1;
    float2 cloudUV : TEXCOORD2;
};
  1. Apply Cloud Texture’s Tiling & Offset to original UV, then output it from vertex to fragment.
v2f vert(appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.normal = normalize(mul(v.normal, unity_WorldToObject).xyz);
    // Apply Cloud Texture's Tiling & Offset to original UV.
    o.cloudUV = TRANSFORM_TEX(v.uv, _Clouds);
    return o;
}
  1. Sample cloud texture using cloudUV. (“i.uv” if skipping step1 & 2)
fixed4 frag(v2f i) : SV_Target
{
    float dif = max(0.05, dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
    fixed4 col = tex2D(_MainTex, i.uv);

    // Sample the cloud texture.
    fixed4 clouds = tex2D(_Clouds, i.cloudUV);

    // Don't know what cloud texture looks like.
    // Assume: black & white texture with black background and no transparency.
    // output "col" if "clouds" is black (0) for this pixel.
    col = lerp(clouds, col, clouds.r);
    // If not black & white, use Luminance() to get the lerp factor.
    // Luminance(fixed3 color) is in "UnityCG.cginc".
    col = lerp(clouds, col, Luminance(clouds));
                 
    fixed4 result = fixed4(col.rgb * dif * _LightColor0.rgb, 1);
    return result;
}

Please let me know if the cloud texture is not as I thought.

1 Like

Standard shader’s vert & frag functions (vertBase, fragBase) are inside the “*.cginc” files.

Standard shader:
8482256--1127879--Unity_StandardShader.png

I think you’ll need to copy this include file and standard shader before modifying them.

1 Like

Oops, if you are talking about Mobile/Diffuse shader, it is a Unity surface shader.

Surface shader is not a real shader, it’s more like a predefined shader which allows you to modify some of the shading steps. (In this case, modify SurfaceOutput structure)

// From Unity surface shader docs.

struct SurfaceOutput
{
    fixed3 Albedo;  // diffuse color
    fixed3 Normal;  // tangent space normal, if written
    fixed3 Emission;
    half Specular;  // specular power in 0..1 range
    fixed Gloss;    // specular intensity
    fixed Alpha;    // alpha for transparencies
};

Surface shaders are easy to use (not suggested, it’s not Unity’s future), for example:

sampler2D _MainTex;
sampler2D _Clouds;

struct Input
{
    float2 uv_MainTex;
    float2 uv_Clouds;
};

void surf (Input IN, inout SurfaceOutput o)
{
    fixed4 col = tex2D(_MainTex, IN.uv_MainTex);
    // Sample cloud texture.
    fixed4 clouds = tex2D(_Clouds, i.cloudUV);
    // Lerp colors.
    o.Albedo = lerp(clouds.rgb, col.rgb, clouds.r);
    o.Alpha = c.a;
}
1 Like

I cannot thank you enough! I just tried it and it works! I don’t know why I never thought to have a separate cloudsUV variable assigned to the clouds texture… d’oh!

The texture I am using is just a generic coloured texture so right now it puts an ugly screen over the planet. I now need to create a texture that has a transparent background with some cloud-like artefacts in it. That’s another issue though… the question here was applying it over the top of the main texture… and you have solved that! Thanks so much!

Okay so I have tried experimenting with a transparent cloud texture and here are the results.

Without the cloud:

8482490--1127903--Screenshot 2022-10-01 at 19.22.15.png

With the cloud applied:

8482490--1127906--ezgif-4-57e0013fcd.gif

You can see the cloud texture is very nicely overlaid and the lighting is retained… awesome! But… it appears to place a screen or fogginess over the original texture, i.e. the colours look duller. Do you know why this is? I’ve attached both the main texture and clouds texture here, and I have “Alpha is Transparency” ticked in Unity for the cloud texture.

Note: This is using the code in Post #3


Hi, seems that the cloud texture has alpha.

In this case, we don’t need to use Luminance() to “approximate the alpha” (lerp factor).

Change step 3 to:

fixed4 frag(v2f i) : SV_Target
{
    float dif = max(0.05, dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
    fixed4 col = tex2D(_MainTex, i.uv);
    // Sample the cloud texture.
    fixed4 clouds = tex2D(_Clouds, i.cloudUV);
 
    // Cloud Texture has alpha, so we can use alpha to lerp colors.
    col = lerp(col, clouds, clouds.a);
 
    fixed4 result = fixed4(col.rgb * dif * _LightColor0.rgb, 1);
    return result;
}
1 Like

Thank you so much for helping me, I really appreciate it. I have tried this and there is a massive improvement:

8482547--1127930--image.gif 8482547--1127933--ezgif-4-c2582891cd.gif

You can see the clouds look so much better! It does still seem to reduce the colour of the main texture though… it looks a bit less green. Do you think this is because of the clouds texture? Ideally I’d like to retain the look of the main texture as much as possible.

I have just confirmed that it is indeed the texture that is causing this reduced vibrancy of the original colours. So I just needed to edit my textures. Thanks again @[wwWwwwW1]( Creating a basic planet shader for mobile members/wwwwwww1.8413720/)!

1 Like

It is possible to add a new parameter to adjust the strength of cloud.

Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Clouds ("Clouds", 2D) = "white" {}
        _CloudStrength ("Cloud Strength", Range(0.0, 1.0)) = 1.0
    }
sampler2D _MainTex;
sampler2D _Clouds;
float4 _MainTex_ST;
float4 _Clouds_ST;
// half precision should be enough here.
half _CloudStrength;

//... vert()

fixed4 frag(v2f i) : SV_Target
{
    float dif = max(0.05, dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
    fixed4 col = tex2D(_MainTex, i.uv);
    // Sample the cloud texture.
    fixed4 clouds = tex2D(_Clouds, i.cloudUV);
    // Cloud Texture has alpha, so we can use alpha to lerp colors.
    col = lerp(col, clouds, clouds.a * _CloudStrength);
    fixed4 result = fixed4(col.rgb * dif * _LightColor0.rgb, 1);
    return result;
}

Or you can try additive color blending?

fixed4 frag(v2f i) : SV_Target
{
    float dif = max(0.05, dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
    fixed4 col = tex2D(_MainTex, i.uv);
    // Sample the cloud texture.
    fixed4 clouds = tex2D(_Clouds, i.cloudUV);
    // Use cloud texture's alpha as cloud color. (assume white this time)
    col += clouds.aaaa;
    fixed4 result = fixed4(col.rgb * dif * _LightColor0.rgb, 1);
    return result;
}
1 Like

Adjusting the strength of the cloud as you recommend works really well… very nice! I think this completes the effect, you sir are awesome.

This is probably quite cheeky… but do you have any advice how I might be able to add an “atmospheric” effect to my planets? Could it be done inside the same shader? It would essentially need to add a subtle outline that looks like a “glow” around the planet. Something like this:

8482598--1127936--icon.png

It wouldn’t need to influence the sphere itself, just a subtle glow around the sphere. If you had any advice how to achieve this that would be amazing.

Hi, I think Fresnel Effect is something you’re looking for. (Link to Unity’s Shader Graph Docs)

------------------------------Part 1---------------------------------
Shader Graph Node Preview from Google:

We’ll add a helper function (below) to the shader so that it’ll still be easy-to-read.

Code from Shader Graph Node:

// float precision may improve Fresnel's attenuation quality.
float FresnelEffect(float3 normalWS, float3 viewDirWS, half power)
{
    return pow((1.0 - saturate(dot(normalize(normalWS), normalize(viewDirWS)))), power);
}

The fragment shader cannot render pixels outside the mesh (actually vertex shader can move vertices)

Luckily, we can apply this effect at the edge of the planet but not outside. (so that we don’t need to render twice)

Fresnel Effect requires ViewDirection and SurfaceNormal (we already have).

To get the ViewDirection, we can:

normalize(_WorldSpaceCameraPos - i.worldPos)

In the next part, we’ll add this effect to the shader.
-------------------------See part 2----------------------------

Any chance we could avoid using Shader Graph though? I am using Unity 2020 with the built-in render pipeline, so Shader Graph is not an option for me. I think the Fresnel effect looks really nice though! Can it be done in the usual shader code like we did above?

Yes we won’t use shader graph.

It’s just for us to have a reference.

Ah okay… thanks so much!

------------------------------Part 2---------------------------------

“worldPos” already exists in the “v2f” structure.

  1. Add the Fresnel Effect helper function to the shader.

Such as on top of vert() function:

float FresnelEffect(float3 normalWS, float3 viewDirWS, half power)
{
    // Remove "One Minus" as needed.
    return pow((saturate(dot(normalize(normalWS), normalize(viewDirWS)))), power);
}

v2f vert(appdata v)
{
    //...
}

And add “_Power” & “_FresnelColor” parameters to better control Fresnel Effect.

Properties
{
    //...
    _Power("Rim Power", Range(1.0, 20.0)) = 10.0
    _FresnelColor("Rim Color", Color) = (1.0, 1.0, 1.0, 1.0)
}

//...
float4 _Clouds_ST;
half _Power;
fixed4 _FresnelColor;
  1. We need to pass “worldPos” from vert() to frag().
v2f vert(appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.normal = normalize(mul(v.normal, unity_WorldToObject).xyz);

    // We need to convert Vertices' Position from Object Space to World Space.
    // "Built-In RP" doesn't have a helper function like UnityObjectToWorldPos(posOS).
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

    // Apply Cloud Texture's Tiling & Offset to original UV.
    o.cloudUV = TRANSFORM_TEX(v.uv, _Clouds);
    return o;
}
  1. In frag(), we need to do:
  • Calculate View Direction (World Space)
  • Calculate Fresnel factor. (will be 0 on edge, the attenuation range shrinks when “power” increases)
  • Adjust the final color according to Fresnel factor.
fixed4 frag(v2f i) : SV_Target
{
    float dif = max(0.05, dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
    fixed4 col = tex2D(_MainTex, i.uv);
    // Sample the cloud texture.
    fixed4 clouds = tex2D(_Clouds, i.cloudUV);
    // Cloud Texture has alpha, so we can use alpha to lerp colors.
    col = lerp(col, clouds, clouds.a);
    // Calculate world space view direction.
    float3 viewDirWS = normalize(_WorldSpaceCameraPos - i.worldPos);
    // Fresnel Effect
    float fresnel = FresnelEffect(i.normal, viewDirWS, _Power);
    // Apply before lighting, so that darkness will affect Fresnel.
    col = lerp(_FresnelColor, col, fresnel);
    fixed4 result = fixed4(col.rgb * dif * _LightColor0.rgb, 1);
    return result;
}

Edited:
Fresnel will be 0 on the edge, not 1.
Fix worldPos calculation, it should be something like this.

float3 TransformObjectToWorld(float3 positionOS)
{
    return mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)).xyz;
}
1 Like

Untested, maybe need more adjustments.

Please let me know if it’s not expected.

Edited:
If you want to use Bloom post-processing for better glowing, you can try enabling HDR color for “_FresnelColor” and adjust it:

[HDR] _FresnelColor (“Rim Color”, Color) = = (1.0, 1.0, 1.0, 1.0)
1 Like

@[wwWwwwW1]( Creating a basic planet shader for mobile members/wwwwwww1.8413720/) you are awesome man seriously… I’d send you 1000 likes if I could!

It looks superb:

8482769--1127978--ezgif-4-717540f4c1.gif
It actually looks like a real planet, amazing! Can I trust that this is not too taxing to render? Will it run smooth on mobile devices?

There is only one issue - If I set the rim power to zero, i.e. no atmosphere, then it creates an unsightly black outline around the sphere:

8482769--1127981--Screenshot 2022-10-01 at 22.35.09.png

Any idea how to remove this? I suppose I could just add an if statement to check if power equals 0, but this adds conditional logic and I thought it was bad to do this inside a shader?

Hi, this is because of pow(0, 0).


We should try not to set “_Power” to 0 (try something like 0.001), to avoid the worst case (NaN, can cause big problem when using post-processing).

To support disabling atmosphere (and even runtime), we can add a toggle.

you may try this:

Properties:

Properties
{
    //...
    [Toggle(_RIM_ENABLED)] _RimEnabled("Enable Rim", Float) = 1.0
}

Pragma:

#pragma vertex vert
#pragma fragment frag
// 2 shader variants, (_RIM_ENABLED) and !(_RIM_ENABLED)
#pragma multi_compile _ _RIM_ENABLED
#include "UnityCG.cginc"

Vertex:

v2f vert(appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.normal = normalize(mul(v.normal, unity_WorldToObject).xyz);
#if defined(_RIM_ENABLED)
    // We need to convert Vertices' Position from Object Space to World Space.
    // "Built-In RP" doesn't have a helper function like UnityObjectToWorldPos(posOS).
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
#endif
    // Apply Cloud Texture's Tiling & Offset to original UV.
    o.cloudUV = TRANSFORM_TEX(v.uv, _Clouds);
    return o;
}

Fragment:

fixed4 frag(v2f i) : SV_Target
{
    float dif = max(0.05, dot(i.normal, normalize(_WorldSpaceLightPos0.xyz)));
    fixed4 col = tex2D(_MainTex, i.uv);
    // Sample the cloud texture.
    fixed4 clouds = tex2D(_Clouds, i.cloudUV);
    // Cloud Texture has alpha, so we can use alpha to lerp colors.
    col = lerp(col, clouds, clouds.a);
#if defined(_RIM_ENABLED)
    // Calculate world space view direction.
    float3 viewDirWS = normalize(_WorldSpaceCameraPos - i.worldPos);
    // Fresnel Effect
    float fresnel = FresnelEffect(i.normal, viewDirWS, _Power);
    // Apply before lighting, so that darkness will affect Fresnel.
    col = lerp(_FresnelColor, col, fresnel);
#endif
    fixed4 result = fixed4(col.rgb * dif * _LightColor0.rgb, 1);
    return result;
}

Edited: use “multi_compile” instead of “multi_compile_fragment”.

1 Like