Getting the UV coordinates at a specific location on a sphere

Hi there.
I’m making a space exploration game that features planets with atmospheres, rendered on a single sphere. I’m pretty happy with how things are looking but I’d like to give the illusion of depth to the cloud layer by simulating shadows on the surface ‘below’.

Just to be clear, both the surface and the cloud layer are rendered on one sphere, using a surface shader. (I did try rendering the cloud layer on a second, slightly larger, ‘transparent’ sphere, but I ran into all kinds of depth sorting issues)

So my idea is, in the shader, to draw the cloud shadows over the surface, by subtracting the Luminance of the cloud layer, then adding the cloud layer in. This I have working; The next part is what I’m struggling with:

To simulate distance between the cloud layer and the planet surface I’d like to ‘offset’ the shadow at areas on the sphere where the normals are perpendicular to the sun direction, ie where the shadows should be ‘long’:

  • float offset = 1 - saturate(dot(normalize(_sunDirection), o.Normal));

I’d like to use that inversed dot product value as a multiplier for sampling the cloud layer at a UV location ‘toward’ the direction of the sun. That way the shadow will ‘spread’ more at the areas near sunset.

So my question (finally!) is: How do I get the UVs from a calculated vertex fragment that isn’t the current vertex fragment?

And if I can explain this better, please let me know!

You need the light direction in tangent (aka UV) space to know what direction to offset the UVs. There are two ways to do that, though only one Surface Shaders really like.

  1. Use a custom vertex function to calculate the tangent space light position and pass the vector in as a custom Input variable.
struct Input
{
  // usual stuff you want, UVs etc
  float3 tangentSpaceLightPos;
}

void vert(inout appdata_full, out Input o)
{
  UNITY_INITIALIZE_OUTPUT(Input,o);
  float3 worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0)).xyz;
  TANGENT_SPACE_ROTATION;
  o.tangentSpaceLightPos = mul(rotation, worldPos - _SunDirection);
}

// in the surf function
float2 cloudShadowUV = IN.uv_CloudTex.xy + normalize(IN.tangentSpaceLightPos).xy * _CloudShadowOffset;
  1. Do the transform in the surf function. This is harder than it sounds since Surface shaders, while they have the data, purposefully hide it from the author. The macro I use above can only be used in the vertex function. It’s also only possible if you’re using normal maps. Just use the method above.

Wow. Thanks for your answer. I’ve tried including your code (altered to suit my actual shader) and it’s certainly doing SOMEthing to the position of the cloud shadow, but I can’t tell what. Also I don’t understand what you mean my ‘the macro I use above…’ I should probably google this, but what is a ‘macro’ in the context of a shader? Also my shader doesn’t use normal maps. You seem to say that that would prevent your solution working; Could you tell me why that would that be, if so? Thanks again…

Option 1 is using the macro TANGENT_SPACE_ROTATION which constructs a world to tangent matrix called rotation that I use on the next line. It works there in the vert function because it has access to the vertex normal and tangent from the appdata_full struct.

For option 2, you could pass that rotation matrix as a custom Input variable, but there’s not really a benefit over the per vertex method.

Option 1 should work though, or least it should do something roughly like what you want in terms of offsetting the UVs based on the light direction. It’s a big fake though, and really only works properly at the equator (assuming you’re using a sphere mesh with a simple sphere wrapped UV like the default Unity sphere). If you have clouds at the poles it’ll look increasingly “off”. There are some minor changes to the code that would be a little more shadow like, producing stretched out shadows rather than just a small offset, but it would still only work properly around the equator and get more and more obviously wrong as you got to the poles.

The more correct way would require some more complicated math: in shader sphere tracing and analytical UVs.

Ah, I see. Most of the planets rotate on a non-vertical axis so I would need it to work at the poles. I’m using a kind of geode sphere, but the UVs are standard x: 0-1 rotates laterally around the sphere, y: 0-1 goes from pole to pole.
The effect I’m getting with the cloud shadows, using option 1, bears only a passing resemblance to the cloud layer. There are quite extreme distortions at the poles and the seam where the UV x value passes from 1 back to 0 is clearly visible. The fact that I can’t tell what it’s doing means I don’t have a clue as to make it look closer to what I want. I probably need to do some more research, I guess.
The other solution - if you care to offer an opinion - would be to go back to having the cloud layer on a separate, outer sphere. Any idea how to get around depth sorting issues when the two spheres’ centres are at the same point?

So my code had two bugs in it.

First is this: worldPos ``-`` _SunDirection

That should just be _SunDirection by itself. The world position isn’t needed at all. I’d originally written it out to take a light position, then replaced it your _SunDirection direction parameter.

The second bug is the rotation transform matrix is an object to tangent transform, not a world space to tangent transform, so the full line should be:

o.tangentSpaceLightPos = mul([rotation](http://unity3d.com/support/documentation/ScriptReference/30_search.html?q=rotation), mul(unity_WorldToObject, _SunDirection));

I wrote up this proof of concept:

Shader "Custom/PlanetCloudShadows" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB) Smoothness (A)", 2D) = "white" {}
        _CloudTex ("Clouds (A)", 2D) = "black" {}
        _CloudColor ("Cloud Color", Color) = (1,1,1,1)
        _CloudShadowColor ("Cloud Shadow Color", Color) = (0.25,0.25,0.25,1)
        _Glossiness ("Smoothness", Range(0,1)) = 0.0
        _CloudShadowOffset ("Cloud Shadow Offset", Range(0,0.015)) = 0.005
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf StandardSpecular fullforwardshadows vertex:vert
        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _CloudTex;

        struct Input {
            float2 uv_MainTex;
            float2 uv_CloudTex;
            float3 tangentSpaceLightPos;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color, _CloudColor, _CloudShadowColor;
        half _CloudShadowOffset;

        void vert(inout appdata_full v, out Input o)
        {
            UNITY_INITIALIZE_OUTPUT(Input,o);
            TANGENT_SPACE_ROTATION;
            o.tangentSpaceLightPos = mul(rotation, mul((float3x3)unity_WorldToObject, _WorldSpaceLightPos0.xyz));
        }

        void surf (Input IN, inout SurfaceOutputStandardSpecular o) {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            fixed4 cloud = tex2D(_CloudTex, IN.uv_CloudTex);

            half3 shadowVec = normalize(IN.tangentSpaceLightPos);

            float2 cloudShadowUV = IN.uv_CloudTex + shadowVec.xy * _CloudShadowOffset;
            fixed cloudShadow = tex2D(_CloudTex, cloudShadowUV).a;

            fixed shadowFade = saturate(shadowVec.z * 2.0 + 0.5);

            c.rgb = lerp(c.rgb, c.rgb * _CloudShadowColor, cloudShadow * _CloudShadowColor.a * _CloudColor.a * shadowFade);
            c.rgb = lerp(c.rgb, _CloudColor.rgb, cloud.a * _CloudColor.a);

            o.Albedo = c.rgb;
            o.Specular = unity_ColorSpaceDielectricSpec * saturate(1.0 - cloudShadow);
            o.Smoothness = _Glossiness * c.a * (1.0 - saturate(cloud.a + cloudShadow * _CloudShadowColor.a));
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Left: No shadows (Cloud Shadow Color A = 0), Right: Shadows


Note, this is still a little bit of a kludge. Really this will only work with directional lights. If you have point lights the shadows will be broken as those are in world space and need that _WorldSpaceLightPos.xyz - worldPos line. I’m being a little lazy and not fixing the shader to handle that properly.