PNG writer
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m35s
CI / Windows Release Package (push) Successful in 2m17s

This commit is contained in:
2026-05-08 15:33:40 +10:00
parent 6ea70d9497
commit 05d0bcbedd
18 changed files with 509 additions and 14 deletions

View File

@@ -0,0 +1,90 @@
{
"id": "fisheye-equirectangular-mirror",
"name": "Fisheye Equirectangular Mirror",
"description": "Unwraps a single fisheye lens into a 360x180 equirectangular map by mirroring the rear hemisphere into the same fisheye source.",
"category": "Projection",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "lensFovDegrees",
"label": "Lens FOV",
"type": "float",
"default": 180.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.5],
"min": [0.001, 0.001],
"max": [2.0, 2.0],
"step": [0.001, 0.001]
},
{
"id": "yawDegrees",
"label": "Yaw",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
},
{
"id": "pitchDegrees",
"label": "Pitch",
"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": "seamAngleDegrees",
"label": "Seam Angle",
"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": "outsideColor",
"label": "Outside Color",
"type": "color",
"default": [0.0, 0.0, 0.0, 1.0]
}
]
}

View File

@@ -0,0 +1,93 @@
static const float PI = 3.14159265358979323846;
static const float TWO_PI = 6.28318530717958647692;
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);
}
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;
}
float3 equirectangularRay(float2 uv)
{
float longitude = (uv.x - 0.5) * TWO_PI + radiansFromDegrees(seamAngleDegrees);
float latitude = (0.5 - uv.y) * PI;
float latitudeCos = cos(latitude);
return normalize(float3(
sin(longitude) * latitudeCos,
sin(latitude),
cos(longitude) * latitudeCos
));
}
float4 shadeVideo(ShaderContext context)
{
float3 ray = equirectangularRay(context.uv);
ray = rotateZ(ray, radiansFromDegrees(rollDegrees));
ray = rotateX(ray, radiansFromDegrees(-pitchDegrees));
ray = rotateY(ray, radiansFromDegrees(yawDegrees));
// Mirror the rear hemisphere into the front-facing fisheye image so one
// circular lens source fills both halves of the equirectangular output.
ray.z = abs(ray.z);
ray = normalize(ray);
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);
}