0
votes

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.

2

2 Answers

1
votes

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);
}
1
votes

A little more futzing around and this seems to work

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);
  float2 uv = float2(cos(theta), sin(theta)) * r * 0.5 + 0.5;
  return float2(uv.x * 0.5, uv.y);
}