Fisheye effect vertex shader fail - asking for help - video and code included

Video here


Hi people!

Sorry for the probably noobish question, but I’d greatly appreciate if you could help me out. I’m a somewhat experienced programmer, but this is my first time trying my hands at shader programming. I figured a basic fisheye effect vertex shader would be a fun first program to write. I am aware that there are better ways to do this, in fact there are multiple existing solutions available, but I want to make this work anyway, because I need to learn it.


My idea was to simply calculate the distance of the vertices from the camera, and take the square root of that. E.g. if the camera is at (0,0) and point P is at (15,20) its original distance would be 25 units, so it should be transformed into (3,4) making the new distance exactly 5 units, which is the square root of the original 25.


Should be simple enough, but as you can see on the video, I failed, almost completely. That rainbow background is 2 Planes on top of each other, to make the distortion visible. The most obvious issues:

00:01.00 - There is a hole in the middle of the Plane. Wtf. It disappears later, and doesn’t come back.

00:04.72 - The other spaceship on the right appears out of nowhere. It’s supposed to appear as a 1-pixel wide line first, but it doesn’t. It’s completely invisible until it’s about 20 pixels wide.

00:05.17 - The nicely distorted Plane suddenly becomes completely straight as the camera moves away from it. And not just that, but somehow it’s taller than it’s wide. It’s supposed to be a square. Also, the Z-order is somehow reversed now, the Plane partially covering the main spaceship.

00:13.34 - If I got too far away from the plane, it completely and instantly disappears.

00:47.00 - Deleting the smaller plane breaks the effect on the remaining bigger plane. No fisheye distortion anymore, no matter where the camera is.


I think that’s all. Here’s the shader code:

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

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

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

			sampler2D _MainTex;
			float4 _MainTex_ST;

			v2f vert(appdata v)
			{
				v2f o;
				float2 ObjSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1.0)).xy;
				float2 offset = v.vertex.xy - ObjSpaceCameraPos;
				float2 l = length(offset);
				v.vertex.xy = ObjSpaceCameraPos + offset / sqrt(l);

				o.vertex = UnityObjectToClipPos(v.vertex);

				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex,  i.uv);
				return col;
			}
			ENDCG
		}
	}
}

I asked the same question on reddit
and fortunately some did help me out.
See the thread for details, but tldr; I had to change the code to do the transformation in screen space instead of object space, then create a custom culling matrix (essentially disabling frustum culling).
Also translated the background, so that there are no vertices at (0,0) which is important, because apparently square root is calculated from the inverse square root.