Add round-trip tests for *_to_direction functions and fix some of them

The function `direction_to_<some projection model>` computes the inverse of `<some projection model>_to_direction`.
Some of these functions had a bug where they mirror the x-axis, and some of them could be simplified.
I added round-trip tests for all of them.
This MR might change the behavior of the renderer when using equiangular_cubemap_face_to_direction:
I normalized the result vector. I looked at the usages and I think it's normalized later anyways, but someone else should probably verify that this doesn't cause issues.

Pull Request: https://projects.blender.org/blender/blender/pulls/123932
This commit is contained in:
Alexander Brock 2024-07-04 09:54:50 +02:00 • committed by Sergey Sharybin
parent d8675a3f6a
commit 717c970297
2 changed files with 214 additions and 25 deletions

View file

@ -25,7 +25,7 @@ ccl_device float2 direction_to_spherical(float3 dir)
ccl_device float3 spherical_to_direction(float theta, float phi)
{
float sin_theta = sinf(theta);
return make_float3(sin_theta * cosf(phi), sin_theta * sinf(phi), cosf(theta));
return make_float3(sin_theta * sinf(phi), sin_theta * cosf(phi), cosf(theta));
}
/* Equirectangular coordinates <-> Cartesian direction */
@ -63,13 +63,9 @@ ccl_device float3 equirectangular_to_direction(float u, float v)
ccl_device float2 direction_to_fisheye(float3 dir, float fov)
{
float r = atan2f(sqrtf(dir.y * dir.y + dir.z * dir.z), dir.x) / fov;
float phi = atan2f(dir.z, dir.y);
float u = r * cosf(phi) + 0.5f;
float v = r * sinf(phi) + 0.5f;
return make_float2(u, v);
const float r = atan2f(len(make_float2(dir.y, dir.z)), dir.x) / fov;
const float2 uv = r * safe_normalize(make_float2(dir.y, dir.z));
return make_float2(0.5f - uv.x, uv.y + 0.5f);
}
ccl_device float3 fisheye_to_direction(float u, float v, float fov)
@ -93,14 +89,11 @@ ccl_device float3 fisheye_to_direction(float u, float v, float fov)
ccl_device float2 direction_to_fisheye_equisolid(float3 dir, float lens, float width, float height)
{
float theta = safe_acosf(dir.x);
float r = 2.0f * lens * sinf(theta * 0.5f);
float phi = atan2f(dir.z, dir.y);
const float theta = safe_acosf(dir.x);
const float r = 2.0f * lens * sinf(theta * 0.5f);
float u = r * cosf(phi) / width + 0.5f;
float v = r * sinf(phi) / height + 0.5f;
return make_float2(u, v);
const float2 uv = r * safe_normalize(make_float2(dir.y, dir.z));
return make_float2(0.5f - uv.x / width, uv.y / height + 0.5f);
}
ccl_device_inline float3
@ -225,21 +218,17 @@ ccl_device float2 direction_to_mirrorball(float3 dir)
* https://blog.google/products/google-ar-vr/bringing-pixels-front-and-center-vr-video/ */
ccl_device float3 equiangular_cubemap_face_to_direction(float u, float v)
{
u = (1.0f - u);
u = tanf((0.5f - u) * M_PI_2_F);
v = tanf((v - 0.5f) * M_PI_2_F);
u = tanf(u * M_PI_2_F - M_PI_4_F);
v = tanf(v * M_PI_2_F - M_PI_4_F);
return make_float3(1.0f, u, v);
return normalize(make_float3(1.0f, u, v));
}
ccl_device float2 direction_to_equiangular_cubemap_face(float3 dir)
{
float u = atan2f(dir.y, dir.x) * 2.0f / M_PI_F + 0.5f;
float u = 0.5f - atan2f(dir.y, dir.x) * 2.0f / M_PI_F;
float v = atan2f(dir.z, dir.x) * 2.0f / M_PI_F + 0.5f;
u = 1.0f - u;
return make_float2(u, v);
}

View file

@ -10,16 +10,17 @@
#include "kernel/device/cpu/compat.h"
#include "kernel/device/cpu/globals.h"
#include "kernel/types.h"
#include "kernel/camera/camera.h"
#include "kernel/camera/projection.h"
#include "kernel/types.h"
CCL_NAMESPACE_BEGIN
/**
* @brief Test #fisheye_lens_polynomial_to_direction and its inverse
* #direction_to_fisheye_lens_polynomial by checking if sensor position equals
* direction_to_fisheye_lens_polynomial(fisheye_lens_polynomial_to_direction/sensor position))
* direction_to_fisheye_lens_polynomial(fisheye_lens_polynomial_to_direction(sensor position))
* for a couple of sensor positions and a couple of different sets of parameters.
*/
TEST(KernelCamera, FisheyeLensPolynomialRoundtrip)
@ -217,4 +218,203 @@ TEST(KernelCamera, FisheyeLensPolynomialToDirection)
}
}
/**
* @brief The CommonValues struct contains information about the tests
* which is common across the different tests.
* Derived classes may override functions to make tests less strict
* if necessary.
*/
struct CommonValues {
/**
* @brief Threshold for the reprojection error.
* @return
*/
virtual double threshold() const
{
return 2e-6;
}
/**
* @brief If skip_invalid returns true, invalid unprojections are ignored in the test.
* @return
*/
virtual bool skip_invalid() const
{
return false;
}
};
struct Spherical : public CommonValues {
static float2 direction_to_sensor(float3 const &dir,
float const fov,
float const width,
float const height)
{
return direction_to_spherical(dir);
}
static float3 sensor_to_direction(float2 const &sensor,
float const fov,
float const width,
float const height)
{
return spherical_to_direction(sensor.x, sensor.y);
}
};
struct Equirectangular : public CommonValues {
static float2 direction_to_sensor(float3 const &dir,
float const fov,
float const width,
float const height)
{
return direction_to_equirectangular(dir);
}
static float3 sensor_to_direction(float2 const &sensor,
float const fov,
float const width,
float const height)
{
return equirectangular_to_direction(sensor.x, sensor.y);
}
};
struct FisheyeEquidistant : public CommonValues {
static float2 direction_to_sensor(float3 const &dir,
float const fov,
float const width,
float const height)
{
return direction_to_fisheye(dir, fov);
}
static float3 sensor_to_direction(float2 const &sensor,
float const fov,
float const width,
float const height)
{
return fisheye_to_direction(sensor.x, sensor.y, fov);
}
};
struct FisheyeEquisolid : public CommonValues {
bool skip_invalid() const
{
return true;
}
static constexpr float lens = 15.0f;
static float2 direction_to_sensor(float3 const &dir,
float const fov,
float const width,
float const height)
{
return direction_to_fisheye_equisolid(dir, lens, width, height);
}
static float3 sensor_to_direction(float2 const &sensor,
float const fov,
float const width,
float const height)
{
return fisheye_equisolid_to_direction(sensor.x, sensor.y, lens, fov, width, height);
}
};
struct MirrorBall : public CommonValues {
static float2 direction_to_sensor(float3 const &dir,
float const fov,
float const width,
float const height)
{
return direction_to_mirrorball(dir);
}
static float3 sensor_to_direction(float2 const &sensor,
float const fov,
float const width,
float const height)
{
return mirrorball_to_direction(sensor.x, sensor.y);
}
};
struct EquiangularCubemapFace : public CommonValues {
static float2 direction_to_sensor(float3 const &dir,
float const fov,
float const width,
float const height)
{
return direction_to_equiangular_cubemap_face(dir);
}
static float3 sensor_to_direction(float2 const &sensor,
float const fov,
float const width,
float const height)
{
return equiangular_cubemap_face_to_direction(sensor.x, sensor.y);
}
};
template<typename T> class PanoramaProjection : public testing::Test {};
using MyTypes = ::testing::Types<Spherical,
Equirectangular,
FisheyeEquidistant,
FisheyeEquisolid,
MirrorBall,
EquiangularCubemapFace>;
TYPED_TEST_SUITE(PanoramaProjection, MyTypes);
/**
* @brief Test <projection>_to_direction and its inverse
* direction_to_<projection> by checking if sensor position equals
* direction_to_<projection>(<projection>_to_direction(sensor position))
* for a couple of sensor positions and a couple of different sets of parameters.
*/
TYPED_TEST(PanoramaProjection, round_trip)
{
TypeParam test;
const float2 sensors[]{{0.5f, 0.5f},
{0.4f, 0.4f},
{0.3f, 0.3f},
{0.4f, 0.6f},
{0.3f, 0.7f},
{0.2f, 0.8f},
{0.5f, 0.9f},
{0.5f, 0.1f},
{0.1f, 0.5f},
{0.9f, 0.5f}};
for (float const size : {36.0f, 24.0f, 6.0f * M_PI_F}) {
float const width = size;
float const height = size;
for (const float fov : {2.0f * M_PI_F, M_PI_F, M_PI_2_F, M_PI_4_F, 1.0f, 2.0f}) {
size_t test_count = 0;
for (const float2 &sensor : sensors) {
const float3 direction = TypeParam::sensor_to_direction(sensor, fov, width, height);
if (test.skip_invalid() && len(direction) < 0.9f) {
continue;
}
test_count++;
EXPECT_NEAR(len(direction), 1.0, 1e-6)
<< "dir: (" << direction.x << ", " << direction.y << ", " << direction.z << ")"
<< std::endl
<< "fov: " << fov << std::endl
<< "sensor: (" << sensor.x << ", " << sensor.y << ")" << std::endl;
const float2 projection = TypeParam::direction_to_sensor(direction, fov, width, height);
EXPECT_NEAR(sensor.x, projection.x, test.threshold())
<< "dir: (" << direction.x << ", " << direction.y << ", " << direction.z << ")"
<< std::endl
<< "fov: " << fov << std::endl
<< "sensor: (" << sensor.x << ", " << sensor.y << ")" << std::endl;
EXPECT_NEAR(sensor.y, projection.y, test.threshold())
<< "dir: (" << direction.x << ", " << direction.y << ", " << direction.z << ")"
<< std::endl
<< "fov: " << fov << std::endl
<< "sensor: (" << sensor.x << ", " << sensor.y << ")" << std::endl;
}
EXPECT_GE(test_count, 2) << "fov: " << fov << std::endl << "size: " << size << std::endl;
}
}
}
CCL_NAMESPACE_END