This is a mobile-friendly shader for displaying an atmospheric light scattering effect around a sphere. I developed this graphical effect as two shaders – one shader for the planet’s surface and one transparent shader for the atmosphere around the planet. To reduce the number of vertices and object, I combined them into one shader for a single 3D sphere. You can use the two separate shaders or alternatively a combined shader. These are HLSL shaders for Unity wrapped in the Unity-specific shader property language ShaderLab.
Planet surface/ground shader
Shader "Planet/Planet shader" { Properties { [NoScaleOffset] _MainTex("Texture", 2D) = "white" {} _Shininess("Shininess", Float) = 8 _SpecColor("Specular color", Color) = (1.0, 1.0, 1.0, 0.7) _TransitionWidth("Transition width", Range(0.1, 0.5)) = 0.15 } SubShader { Tags { "RenderType" = "Opaque" } Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM // pragmas and includes #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" // user-defined variables sampler2D _MainTex; float4 _MainTex_ST; // Needed for scale and offset in TRANSFORM_TEX uv-mapping float _Shininess; float4 _SpecColor; uniform float _TransitionWidth; // unity-defined variables uniform float4 _LightColor0; // constants static const float PI = 3.14159265f; // base input structs struct vertexInput { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct vertexOutput { float4 posProjection : SV_POSITION; float2 uv : TEXCOORD0; float3 diffuse : BRIGHTNESS; float3 normalDirection : TEXCOORD1; float3 viewDirection : TEXCOORD2; float3 lightDirection : TEXCOORD3; float4 posWorld : TEXCOORD4; }; // vertex program vertexOutput vert(vertexInput v) { vertexOutput o; o.normalDirection = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz); o.viewDirection = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz); o.lightDirection = normalize(_WorldSpaceLightPos0.xyz); o.posWorld = mul(unity_ObjectToWorld, v.vertex); // diffuse calculation according to ramp function assuming the object is a sphere lit up on one side float angleIncidence = acos(dot(o.lightDirection, o.normalDirection)) / PI; float shadeFactor = 0.1 * (1 - angleIncidence) + 0.9 * (1 - (clamp(angleIncidence, 0.5, 0.5 + _TransitionWidth) - 0.5) / _TransitionWidth); o.diffuse = shadeFactor * (1 - UNITY_LIGHTMODEL_AMBIENT) + UNITY_LIGHTMODEL_AMBIENT; o.posProjection = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } // fragment program fixed3 frag(vertexOutput i) : SV_Target { // specular reflection calculation float3 specularReflection = _SpecColor.a * _SpecColor.rgb * max(0.0, dot(i.lightDirection, i.normalDirection)) * pow(max(0.0, dot(reflect(-i.lightDirection, i.normalDirection), i.viewDirection)), _Shininess); // sample the texture fixed3 col = _LightColor0.rgb * (tex2D(_MainTex, i.uv).rgb * i.diffuse + specularReflection * tex2D(_MainTex, i.uv).a); return col; } ENDCG } } }
“UNITY_LIGHTMODEL_AMBIENT” can be replaced or set to 0 for more realism.
Atmosphere shader
Shader "Planet/Atmosphere shader" { Properties { [NoScaleOffset] _Gradient("Diffraction ramp", 2D) = "white" {} _FresnelExponent("Fresnel exponent", Float) = 5 _TransitionWidth("Transition width", Range(0.1, 0.5)) = 0.15 } SubShader { Tags{ Queue = Transparent } Blend SrcAlpha OneMinusSrcAlpha Pass { Tags {"LightMode" = "ForwardBase"} CGPROGRAM // pragmas and includes #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" // user-defined variables uniform sampler2D _Gradient; uniform float _FresnelExponent; uniform float _TransitionWidth; // unity-defined variables uniform float4 _LightColor0; // constants static const float PI = 3.14159265f; // base input structs struct vertexInput { float4 vertex : POSITION; float3 normal : NORMAL; }; struct vertexOutput { float4 posProjection : SV_POSITION; float angleIncidence : ANGLE; float4 col : COLOR; }; // vertex program vertexOutput vert(vertexInput v) { vertexOutput o; float3 normalDirection = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz); float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz); float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz); // assuming the object is a sphere, the angles between normals and light determines the positions on the sphere o.angleIncidence = acos(dot(lightDirection, normalDirection)) / PI; // shade atmosphere according to this ramp function from 0 to 180 degrees float shadeFactor = 0.1 * (1 - o.angleIncidence) + 0.9 * (1 - (clamp(o.angleIncidence, 0.5, 0.5 + _TransitionWidth) - 0.5) / _TransitionWidth); // the viewer should be able to see further distance through atmosphere towards edges of the sphere float angleToViewer = sin(acos(dot(normalDirection, viewDirection))); // this ramp funtion lights up edges, especially the very edges of the sphere contour float perspectiveFactor = 0.3 + 0.2 * pow(angleToViewer, _FresnelExponent) + 0.5 * pow(angleToViewer, _FresnelExponent * 20); o.col = _LightColor0 * perspectiveFactor * shadeFactor; o.posProjection = mul(UNITY_MATRIX_MVP, v.vertex); return o; } // fragment program fixed4 frag (vertexOutput i) : SV_Target { // tint with gradient texture ramp of 70% brightness value and multiply by 1.4 to re-adjust brightness level float2 gradientLevel = float2(i.angleIncidence, 0); fixed4 col = i.col * tex2D(_Gradient, gradientLevel) * 1.4; return col; } ENDCG } } //Fallback "Diffuse" }
Combined Planet surface and atmosphere shader
Shader "Planet/Planet Surface and Atmosphere Shader" { Properties { // Atmosphere variables [NoScaleOffset] _Gradient("Diffraction ramp", 2D) = "white" {} _FresnelExponent("Fresnel exponent", Float) = 5 _TransitionWidth("Transition width", Range(0.1, 0.5)) = 0.15 // Planet surface variables [NoScaleOffset] _MainTex("Texture", 2D) = "white" {} _Shininess("Shininess", Float) = 8 _SpecColor("Specular color", Color) = (1.0, 1.0, 1.0, 0.7) } SubShader { Tags { "RenderType" = "Opaque" } Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM // pragmas and includes #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" // user-defined variables uniform sampler2D _Gradient; uniform float _FresnelExponent; uniform float _TransitionWidth; sampler2D _MainTex; float4 _MainTex_ST; // Needed for scale and offset in TRANSFORM_TEX uv-mapping float _Shininess; float4 _SpecColor; // unity-defined variables uniform float4 _LightColor0; // constants static const float PI = 3.14159265f; // base input structs struct vertexInput { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal : NORMAL; }; struct vertexOutput { float4 posProjection : SV_POSITION; float angleIncidence : ANGLE; float4 colAtmosphere : COLOR; float2 uv : TEXCOORD0; float3 diffuse : BRIGHTNESS; float3 normalDirection : TEXCOORD1; float3 viewDirection : TEXCOORD2; float3 lightDirection : TEXCOORD3; float4 posWorld : TEXCOORD4; }; // vertex program vertexOutput vert(vertexInput v) { vertexOutput o; o.normalDirection = normalize(mul(float4(v.normal, 0.0), unity_WorldToObject).xyz); o.viewDirection = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz); o.lightDirection = normalize(_WorldSpaceLightPos0.xyz); o.posWorld = mul(unity_ObjectToWorld, v.vertex); // assuming the object is a sphere, the angles between normals and light determines the positions on the sphere o.angleIncidence = acos(dot(o.lightDirection, o.normalDirection)) / PI; // shade surface and atmosphere according to this ramp function from 0 to 180 degrees float shadeFactor = 0.1 * (1 - o.angleIncidence) + 0.9 * (1 - (clamp(o.angleIncidence, 0.5, 0.5 + _TransitionWidth) - 0.5) / _TransitionWidth); // surface diffuse calculation according to ramp function assuming the object is a sphere lit up on one side o.diffuse = shadeFactor * (1 - UNITY_LIGHTMODEL_AMBIENT) + UNITY_LIGHTMODEL_AMBIENT; // the viewer should be able to see further distance through atmosphere towards edges of the sphere float angleToViewer = sin(acos(dot(o.normalDirection, o.viewDirection))); // this ramp funtion lights up edges, especially the very edges of the sphere contour float perspectiveFactor = 0.3 + 0.2 * pow(angleToViewer, _FresnelExponent) + 0.5 * pow(angleToViewer, _FresnelExponent * 20); o.colAtmosphere = _LightColor0 * perspectiveFactor * shadeFactor; o.posProjection = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } // fragment program fixed3 frag(vertexOutput i) : SV_Target { // surface specular reflection calculation float3 specularReflection = _SpecColor.a * _SpecColor.rgb * max(0.0, dot(i.lightDirection, i.normalDirection)) * pow(max(0.0, dot(reflect(-i.lightDirection, i.normalDirection), i.viewDirection)), _Shininess); // tint with gradient texture ramp of 70% brightness value and multiply by 1.4 to re-adjust brightness level float2 gradientLevel = float2(i.angleIncidence, 0); fixed4 colAtmosphere = i.colAtmosphere * tex2D(_Gradient, gradientLevel) * 1.4; // sample the texture and calculate colour for the planet surface fixed3 col = _LightColor0.rgb * (tex2D(_MainTex, i.uv).rgb * i.diffuse + specularReflection * tex2D(_MainTex, i.uv).a); // blend the planet surface colour and atmosphere with traditional transparency col = colAtmosphere.a * colAtmosphere.rgb + (float3(1.0, 1.0, 1.0) - colAtmosphere.a) * col; return col; } ENDCG } } }
“UNITY_LIGHTMODEL_AMBIENT” can be replaced or set to 0 for more realism.
I distribute these shaders under CC0 1.0 licence for free use “as is”. I do not take any responsibility for their use or provide any kind of support.