diff --git a/shaders/fisheye-reproject/shader.json b/shaders/fisheye-reproject/shader.json new file mode 100644 index 0000000..0be320b --- /dev/null +++ b/shaders/fisheye-reproject/shader.json @@ -0,0 +1,100 @@ +{ + "id": "fisheye-reproject", + "name": "Fisheye Reproject", + "description": "Inverse-projects a cropped fisheye source into a virtual rectilinear or cylindrical camera view with pan, tilt, and roll controls.", + "category": "Projection", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "lensFovDegrees", + "label": "Lens FOV", + "type": "float", + "default": 190.0, + "min": 1.0, + "max": 220.0, + "step": 0.1 + }, + { + "id": "center", + "label": "Optical Center", + "type": "vec2", + "default": [0.5, 0.5], + "min": [0.0, 0.0], + "max": [1.0, 1.0], + "step": [0.001, 0.001] + }, + { + "id": "radius", + "label": "Fisheye Radius", + "type": "vec2", + "default": [0.5, 0.885], + "min": [0.001, 0.001], + "max": [2.0, 2.0], + "step": [0.001, 0.001] + }, + { + "id": "virtualFovDegrees", + "label": "Virtual FOV", + "type": "float", + "default": 75.0, + "min": 1.0, + "max": 175.0, + "step": 0.1 + }, + { + "id": "panDegrees", + "label": "Pan", + "type": "float", + "default": 0.0, + "min": -180.0, + "max": 180.0, + "step": 0.1 + }, + { + "id": "tiltDegrees", + "label": "Tilt", + "type": "float", + "default": 0.0, + "min": -120.0, + "max": 120.0, + "step": 0.1 + }, + { + "id": "rollDegrees", + "label": "Roll", + "type": "float", + "default": 0.0, + "min": -180.0, + "max": 180.0, + "step": 0.1 + }, + { + "id": "fisheyeModel", + "label": "Fisheye Model", + "type": "enum", + "default": "equidistant", + "options": [ + { "value": "equidistant", "label": "Equidistant" }, + { "value": "equisolid", "label": "Equisolid" }, + { "value": "stereographic", "label": "Stereographic" }, + { "value": "orthographic", "label": "Orthographic" } + ] + }, + { + "id": "outputProjection", + "label": "Output Projection", + "type": "enum", + "default": "rectilinear", + "options": [ + { "value": "rectilinear", "label": "Rectilinear" }, + { "value": "cylindrical", "label": "Cylindrical" } + ] + }, + { + "id": "outsideColor", + "label": "Outside Color", + "type": "color", + "default": [0.0, 0.0, 0.0, 1.0] + } + ] +} diff --git a/shaders/fisheye-reproject/shader.slang b/shaders/fisheye-reproject/shader.slang new file mode 100644 index 0000000..4147b2e --- /dev/null +++ b/shaders/fisheye-reproject/shader.slang @@ -0,0 +1,95 @@ +static const float PI = 3.14159265358979323846; + +float radiansFromDegrees(float degrees) +{ + return degrees * (PI / 180.0); +} + +float3 rotateX(float3 ray, float angle) +{ + float s = sin(angle); + float c = cos(angle); + return float3(ray.x, c * ray.y - s * ray.z, s * ray.y + c * ray.z); +} + +float3 rotateY(float3 ray, float angle) +{ + float s = sin(angle); + float c = cos(angle); + return float3(c * ray.x + s * ray.z, ray.y, -s * ray.x + c * ray.z); +} + +float3 rotateZ(float3 ray, float angle) +{ + float s = sin(angle); + float c = cos(angle); + return float3(c * ray.x - s * ray.y, s * ray.x + c * ray.y, ray.z); +} + +float3 buildRectilinearRay(float2 screen, float outputAspect, float tanHalfFov) +{ + return normalize(float3(screen.x * outputAspect * tanHalfFov, screen.y * tanHalfFov, 1.0)); +} + +float3 buildCylindricalRay(float2 screen, float outputAspect, float tanHalfFov) +{ + float horizontalFov = 2.0 * atan(outputAspect * tanHalfFov); + float yaw = screen.x * horizontalFov * 0.5; + float vertical = screen.y * tanHalfFov; + return normalize(float3(sin(yaw), vertical, cos(yaw))); +} + +float normalizedFisheyeRadius(float theta, float halfFov) +{ + float safeHalfFov = max(halfFov, 0.0001); + + if (fisheyeModel == 1) + { + return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001); + } + else if (fisheyeModel == 2) + { + return tan(theta * 0.5) / max(tan(safeHalfFov * 0.5), 0.0001); + } + else if (fisheyeModel == 3) + { + return sin(theta) / max(sin(safeHalfFov), 0.0001); + } + + return theta / safeHalfFov; +} + +float4 shadeVideo(ShaderContext context) +{ + float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0); + float outputAspect = context.outputResolution.x / max(context.outputResolution.y, 1.0); + + float virtualFov = radiansFromDegrees(clamp(virtualFovDegrees, 1.0, 175.0)); + float tanHalfFov = tan(virtualFov * 0.5); + + float3 ray = outputProjection == 1 + ? buildCylindricalRay(screen, outputAspect, tanHalfFov) + : buildRectilinearRay(screen, outputAspect, tanHalfFov); + + ray = rotateZ(ray, radiansFromDegrees(rollDegrees)); + ray = rotateX(ray, radiansFromDegrees(-tiltDegrees)); + ray = rotateY(ray, radiansFromDegrees(panDegrees)); + + float halfFov = radiansFromDegrees(clamp(lensFovDegrees, 1.0, 220.0) * 0.5); + float theta = acos(clamp(ray.z, -1.0, 1.0)); + if (theta > halfFov) + return outsideColor; + + float phi = atan2(ray.y, ray.x); + float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov); + + float2 sourceUv = float2( + center.x + cos(phi) * fisheyeRadius * radius.x, + center.y - sin(phi) * fisheyeRadius * radius.y + ); + + if (sourceUv.x < 0.0 || sourceUv.x > 1.0 || sourceUv.y < 0.0 || sourceUv.y > 1.0) + return outsideColor; + + return sampleVideo(sourceUv); +}