Search code examples
unity-game-engineshaderfragment-shader

Making a Fisheye Skybox Shader in Unity


Unity has a built in Skybox shader that takes either a cubemap texture or an equirectangular texture like this

Loading it in and following the instructions to use it as a skybox works

skybox from equirectangular image in unity

I want to extend it to handle a fisheye image like this

fisheye 180 degress

The code for the shader is available from the built in shaders and a beta version (which at a glance seems the same) is available here

Looking through the shader code a 3D direction is computed in the vertex shader and the passed to the fragment shader. The fragment shader is then supposed to take that 3D direction and generate a texture coordinate.

Here's the code for equirectangular images

inline float2 ToRadialCoords(float3 coords)
{
  float3 normalizedCoords = normalize(coords);
  float latitude = acos(normalizedCoords.y);
  float longitude = atan2(normalizedCoords.z, normalizedCoords.x);
  float2 sphereCoords = float2(longitude, latitude) * float2(0.5/UNITY_PI, 1.0/UNITY_PI);
  return float2(0.5,1.0) - sphereCoords;
}

Here's the code I tried to change it to for the fisheye image

inline float2 ToFisheyeCoords(float3 coords)
  float3 normalizedCoords = normalize(coords);

  float r = 2.0 * atan2(length(normalizedCoords.xy), abs(normalizedCoords.z)) / UNITY_PI;
  float theta = atan2(normalizedCoords.y, normalizedCoords.x * sign(normalizedCoords.z));
  float2 uv = float2(cos(theta), sin(theta)) * r * 0.5 + 0.5;
  return frac(uv * float2(-1, 1));
}

But it's not working.

enter image description here

I feel like I'm overlooking something obvious.

The entire project is here. To switch between the fisheye example and the equirectangular example you need to open Window->Rendering->Light Settings and then drag the SkyboxMaterialEquirectangular into the Skybox Material slot in the Lighting window.


Solution

  • I played around with this a bit and I figured I'd post it. The only thing to add to your answer is, in the 360 case, choosing which half of the image to sample.

    inline float2 ToFisheyeCoords(float3 coords)
    {
        float3 n = normalize(coords);        
    
        // u = r cos(phi) + 0.5
        // v = r sin(phi) + 0.5
        //where
        // r = atan2(sqrt(x * x + y * y), p.z) / pi
        // phi = atan2(y, x)
    
        float r = atan2(length(n.xy), abs(n.z)) / UNITY_PI;
        float phi = atan2(n.y, n.x * sign(n.z));
        float2 uv = float2(cos(phi), sin(phi)) * r + .5;
    
        uv.x *= .5;   
        //Choose image half to sample depending on sign of normal.z            
        uv.x += .25*(1 - sign(n.z));
    
        return uv;
    }
    

    A more general solution for a single (not split) image the seems to be:

    inline float2 ToFisheyeCoords(float3 coords)
    {
        float3 n = normalize(coords);
        //float FOV = UNITY_PI; // 180 degrees
        float FOV = UNITY_PI*2; // 360 degrees
        float r = atan2(length(n.xy), n.z) / FOV;
        float phi = atan2(n.y, n.x);
        float2 uv = float2(cos(phi), sin(phi)) * r + .5;
        return saturate(uv);
    }