From cdf867d71095743f37c1198aad7cafd8aaa29082 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 08:38:52 +0200 Subject: [PATCH 001/161] add operator= for glm:: matrices corresponding to our at home matrices - we were never be able to do ourMatrixT = glmMatrixT and its important since a lot of utils create glmMatrixT actually --- include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl b/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl index b1d33f097b..9a8ce9e51f 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl @@ -28,6 +28,12 @@ struct matrix final : private glm::mat return *this; } + matrix& operator=(Base const& rhs) + { + Base::operator=(rhs); + return *this; + } + friend matrix operator+(matrix const& lhs, matrix const& rhs){ return matrix(reinterpret_cast(lhs) + reinterpret_cast(rhs)); } friend matrix operator-(matrix const& lhs, matrix const& rhs){ return matrix(reinterpret_cast(lhs) - reinterpret_cast(rhs)); } From 6ef4f89383e012e8309e52f2cd7ade7fddf26599 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 09:04:26 +0200 Subject: [PATCH 002/161] I'm stupid, I have explicit at home matrix constructor taking glm:: matrix xD remove the = operator glm -> to ours --- include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl b/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl index 9a8ce9e51f..cc89f9d003 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/matrix.hlsl @@ -27,13 +27,7 @@ struct matrix final : private glm::mat Base::operator=(rhs); return *this; } - - matrix& operator=(Base const& rhs) - { - Base::operator=(rhs); - return *this; - } - + friend matrix operator+(matrix const& lhs, matrix const& rhs){ return matrix(reinterpret_cast(lhs) + reinterpret_cast(rhs)); } friend matrix operator-(matrix const& lhs, matrix const& rhs){ return matrix(reinterpret_cast(lhs) - reinterpret_cast(rhs)); } From 22f505c8c7a976aa1b47e90e20bfca71fb2571ca Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 10:33:28 +0200 Subject: [PATCH 003/161] steal include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl from https://github.com/Devsh-Graphics-Programming/Nabla/pull/760 --- .../transformation_matrix_utils.hlsl | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl new file mode 100644 index 0000000000..d50bc16869 --- /dev/null +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -0,0 +1,112 @@ +#ifndef _NBL_BUILTIN_HLSL_TRANSFORMATION_MATRIX_UTILS_INCLUDED_ +#define _NBL_BUILTIN_HLSL_TRANSFORMATION_MATRIX_UTILS_INCLUDED_ + +#include + +namespace nbl +{ +namespace hlsl +{ + +template +matrix getMatrix3x4As4x4(const matrix& mat) +{ + matrix output; + for (int i = 0; i < 3; ++i) + output[i] = mat[i]; + output[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + + return output; +} + +// TODO: use portable_float when merged +//! multiplies matrices a and b, 3x4 matrices are treated as 4x4 matrices with 4th row set to (0, 0, 0 ,1) +template +inline matrix concatenateBFollowedByA(const matrix& a, const matrix& b) +{ + const matrix a4x4 = getMatrix3x4As4x4(a); + const matrix b4x4 = getMatrix3x4As4x4(b); + return matrix(mul(a4x4, b4x4)); +} + +template +inline matrix buildCameraLookAtMatrixLH( + const vector& position, + const vector& target, + const vector& upVector) +{ + const vector zaxis = core::normalize(target - position); + const vector xaxis = core::normalize(core::cross(upVector, zaxis)); + const vector yaxis = core::cross(zaxis, xaxis); + + matrix r; + r[0] = vector(xaxis, -dot(xaxis, position)); + r[1] = vector(yaxis, -dot(yaxis, position)); + r[2] = vector(zaxis, -dot(zaxis, position)); + + return r; +} + +float32_t3x4 buildCameraLookAtMatrixRH( + const float32_t3& position, + const float32_t3& target, + const float32_t3& upVector) +{ + const float32_t3 zaxis = core::normalize(position - target); + const float32_t3 xaxis = core::normalize(core::cross(upVector, zaxis)); + const float32_t3 yaxis = core::cross(zaxis, xaxis); + + float32_t3x4 r; + r[0] = float32_t4(xaxis, -dot(xaxis, position)); + r[1] = float32_t4(yaxis, -dot(yaxis, position)); + r[2] = float32_t4(zaxis, -dot(zaxis, position)); + + return r; +} + +// TODO: test, check if there is better implementation +// TODO: move quaternion to nbl::hlsl +// TODO: why NBL_REF_ARG(MatType) doesn't work????? + +//! Replaces curent rocation and scale by rotation represented by quaternion `quat`, leaves 4th row and 4th colum unchanged +template +inline void setRotation(matrix& outMat, NBL_CONST_REF_ARG(core::quaternion) quat) +{ + static_assert(N == 3 || N == 4); + + outMat[0] = vector( + 1 - 2 * (quat.y * quat.y + quat.z * quat.z), + 2 * (quat.x * quat.y - quat.z * quat.w), + 2 * (quat.x * quat.z + quat.y * quat.w), + outMat[0][3] + ); + + outMat[1] = vector( + 2 * (quat.x * quat.y + quat.z * quat.w), + 1 - 2 * (quat.x * quat.x + quat.z * quat.z), + 2 * (quat.y * quat.z - quat.x * quat.w), + outMat[1][3] + ); + + outMat[2] = vector( + 2 * (quat.x * quat.z - quat.y * quat.w), + 2 * (quat.y * quat.z + quat.x * quat.w), + 1 - 2 * (quat.x * quat.x + quat.y * quat.y), + outMat[2][3] + ); +} + +template +inline void setTranslation(matrix& outMat, NBL_CONST_REF_ARG(vector) translation) +{ + static_assert(N == 3 || N == 4); + + outMat[0].w = translation.x; + outMat[1].w = translation.y; + outMat[2].w = translation.z; +} + +} +} + +#endif \ No newline at end of file From 51f4cedff116b7777c6bcb922f30ed00e88c77b4 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 11:06:23 +0200 Subject: [PATCH 004/161] fix getMatrix3x4As4x4 (return type issues) & buildCameraLookAtMatrixRH (ambiguity dependent type issues), reference https://github.com/Devsh-Graphics-Programming/Nabla/pull/760 --- .../transformation_matrix_utils.hlsl | 31 ++++++++++--------- include/nbl/core/math/glslFunctions.h | 1 + 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl index d50bc16869..3c5b2adcd0 100644 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -9,7 +9,7 @@ namespace hlsl { template -matrix getMatrix3x4As4x4(const matrix& mat) +matrix getMatrix3x4As4x4(const matrix& mat) { matrix output; for (int i = 0; i < 3; ++i) @@ -24,8 +24,8 @@ matrix getMatrix3x4As4x4(const matrix& mat) template inline matrix concatenateBFollowedByA(const matrix& a, const matrix& b) { - const matrix a4x4 = getMatrix3x4As4x4(a); - const matrix b4x4 = getMatrix3x4As4x4(b); + const auto a4x4 = getMatrix3x4As4x4(a); + const auto b4x4 = getMatrix3x4As4x4(b); return matrix(mul(a4x4, b4x4)); } @@ -47,19 +47,20 @@ inline matrix buildCameraLookAtMatrixLH( return r; } -float32_t3x4 buildCameraLookAtMatrixRH( - const float32_t3& position, - const float32_t3& target, - const float32_t3& upVector) +template +inline matrix buildCameraLookAtMatrixRH( + const vector& position, + const vector& target, + const vector& upVector) { - const float32_t3 zaxis = core::normalize(position - target); - const float32_t3 xaxis = core::normalize(core::cross(upVector, zaxis)); - const float32_t3 yaxis = core::cross(zaxis, xaxis); - - float32_t3x4 r; - r[0] = float32_t4(xaxis, -dot(xaxis, position)); - r[1] = float32_t4(yaxis, -dot(yaxis, position)); - r[2] = float32_t4(zaxis, -dot(zaxis, position)); + const vector zaxis = core::normalize(position - target); + const vector xaxis = core::normalize(core::cross(upVector, zaxis)); + const vector yaxis = core::cross(zaxis, xaxis); + + matrix r; + r[0] = vector(xaxis, -dot(xaxis, position)); + r[1] = vector(yaxis, -dot(yaxis, position)); + r[2] = vector(zaxis, -dot(zaxis, position)); return r; } diff --git a/include/nbl/core/math/glslFunctions.h b/include/nbl/core/math/glslFunctions.h index 2bd17cd642..3c9cc98850 100644 --- a/include/nbl/core/math/glslFunctions.h +++ b/include/nbl/core/math/glslFunctions.h @@ -362,6 +362,7 @@ NBL_FORCE_INLINE vectorSIMDf cross(const vectorSIMDf& a, const vect template NBL_FORCE_INLINE T normalize(const T& v) { + // TODO: THIS CREATES AMGIGUITY WITH GLM:: NAMESPACE! auto d = dot(v, v); #ifdef __NBL_FAST_MATH return v * core::inversesqrt(d); From 7f2c0857be1555d591971ec84639576bcb895926 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 14:38:27 +0200 Subject: [PATCH 005/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c5f12f0075..038c5d7992 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c5f12f0075f34e5dca36079b09b75aef19f07681 +Subproject commit 038c5d799269a026ffa71e48f5963013632e4634 From f1daa257e1d2d097a77c986ef15065acb874332c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 16:24:30 +0200 Subject: [PATCH 006/161] actually *this* addresses https://github.com/Devsh-Graphics-Programming/Nabla/pull/760/files#r1816728485 for https://github.com/Devsh-Graphics-Programming/Nabla/pull/760 PR, update examples_tests submodule --- examples_tests | 2 +- .../transformation_matrix_utils.hlsl | 26 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/examples_tests b/examples_tests index 038c5d7992..0415999a70 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 038c5d799269a026ffa71e48f5963013632e4634 +Subproject commit 0415999a7035f56f3d9cca7255329916c697ab36 diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl index 3c5b2adcd0..b2534f9ac8 100644 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -29,20 +29,22 @@ inline matrix concatenateBFollowedByA(const matrix& a, const m return matrix(mul(a4x4, b4x4)); } +// /Arek: glm:: for normalize till dot product is fixed (ambiguity with glm namespace + linker issues) + template inline matrix buildCameraLookAtMatrixLH( const vector& position, const vector& target, const vector& upVector) { - const vector zaxis = core::normalize(target - position); - const vector xaxis = core::normalize(core::cross(upVector, zaxis)); - const vector yaxis = core::cross(zaxis, xaxis); + const vector zaxis = glm::normalize(target - position); + const vector xaxis = glm::normalize(hlsl::cross(upVector, zaxis)); + const vector yaxis = hlsl::cross(zaxis, xaxis); matrix r; - r[0] = vector(xaxis, -dot(xaxis, position)); - r[1] = vector(yaxis, -dot(yaxis, position)); - r[2] = vector(zaxis, -dot(zaxis, position)); + r[0] = vector(xaxis, -hlsl::dot(xaxis, position)); + r[1] = vector(yaxis, -hlsl::dot(yaxis, position)); + r[2] = vector(zaxis, -hlsl::dot(zaxis, position)); return r; } @@ -53,14 +55,14 @@ inline matrix buildCameraLookAtMatrixRH( const vector& target, const vector& upVector) { - const vector zaxis = core::normalize(position - target); - const vector xaxis = core::normalize(core::cross(upVector, zaxis)); - const vector yaxis = core::cross(zaxis, xaxis); + const vector zaxis = glm::normalize(position - target); + const vector xaxis = glm::normalize(hlsl::cross(upVector, zaxis)); + const vector yaxis = hlsl::cross(zaxis, xaxis); matrix r; - r[0] = vector(xaxis, -dot(xaxis, position)); - r[1] = vector(yaxis, -dot(yaxis, position)); - r[2] = vector(zaxis, -dot(zaxis, position)); + r[0] = vector(xaxis, -hlsl::dot(xaxis, position)); + r[1] = vector(yaxis, -hlsl::dot(yaxis, position)); + r[2] = vector(zaxis, -hlsl::dot(zaxis, position)); return r; } From 2192e99e91d359bf7f8c544990d03fdef56a7b4f Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 25 Oct 2024 16:56:07 +0200 Subject: [PATCH 007/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 0415999a70..84f35e5d62 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 0415999a7035f56f3d9cca7255329916c697ab36 +Subproject commit 84f35e5d6259d0c814af72ccefcba0d00818668e From 520b1f5d8702098505bfc111f481d07814158a97 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 28 Oct 2024 11:44:40 +0100 Subject: [PATCH 008/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 84f35e5d62..0574d725b2 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 84f35e5d6259d0c814af72ccefcba0d00818668e +Subproject commit 0574d725b2d0ae02a0d0e409e642a14981bc1e97 From a0890f0df957e0d989d956ec41c41fd3be72791a Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 2 Nov 2024 08:52:21 +0100 Subject: [PATCH 009/161] forgot to commit projection build methods, update examples_tests submodule --- examples_tests | 2 +- .../transformation_matrix_utils.hlsl | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 0574d725b2..ccbb37cf2b 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 0574d725b2d0ae02a0d0e409e642a14981bc1e97 +Subproject commit ccbb37cf2bf150dec0c9ad8ea5d328fbb3806745 diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl index b2534f9ac8..aee84405a2 100644 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -109,6 +109,74 @@ inline void setTranslation(matrix& outMat, NBL_CONST_REF_ARG(vector +inline matrix buildProjectionMatrixPerspectiveFovRH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) +{ + const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); + _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero + const float w = h / aspectRatio; + + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(w, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -h, 0.f, 0.f); + m[2] = vector(0.f, 0.f, -zFar / (zFar - zNear), -zNear * zFar / (zFar - zNear)); + m[3] = vector(0.f, 0.f, -1.f, 0.f); + + return m; +} +template +inline matrix buildProjectionMatrixPerspectiveFovLH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) +{ + const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); + _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero + const float w = h / aspectRatio; + + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(w, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -h, 0.f, 0.f); + m[2] = vector(0.f, 0.f, zFar / (zFar - zNear), -zNear * zFar / (zFar - zNear)); + m[3] = vector(0.f, 0.f, 1.f, 0.f); + + return m; +} + +template +inline matrix buildProjectionMatrixOrthoRH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) +{ + _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(heightOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(2.f / widthOfViewVolume, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -2.f / heightOfViewVolume, 0.f, 0.f); + m[2] = vector(0.f, 0.f, -1.f / (zFar - zNear), -zNear / (zFar - zNear)); + m[3] = vector(0.f, 0.f, 0.f, 1.f); + + return m; +} + +template +inline matrix buildProjectionMatrixOrthoLH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) +{ + _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(heightOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(2.f / widthOfViewVolume, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -2.f / heightOfViewVolume, 0.f, 0.f); + m[2] = vector(0.f, 0.f, 1.f / (zFar - zNear), -zNear / (zFar - zNear)); + m[3] = vector(0.f, 0.f, 0.f, 1.f); + + return m; +} + } } From 0109b22b397ef99053e7b87ff028ca49a8ab8e2d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 2 Nov 2024 15:50:37 +0100 Subject: [PATCH 010/161] add is_smart_refctd_ptr_v, update examples_tests submodule --- examples_tests | 2 +- .../hlsl/matrix_utils/transformation_matrix_utils.hlsl | 8 ++++---- include/nbl/core/decl/smart_refctd_ptr.h | 9 +++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/examples_tests b/examples_tests index ccbb37cf2b..d3f325d6d2 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit ccbb37cf2bf150dec0c9ad8ea5d328fbb3806745 +Subproject commit d3f325d6d2b4c9c623b8e70a0882a1bae17471fb diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl index aee84405a2..22bc82ea13 100644 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -110,7 +110,7 @@ inline void setTranslation(matrix& outMat, NBL_CONST_REF_ARG(vector +template inline matrix buildProjectionMatrixPerspectiveFovRH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) { const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); @@ -127,7 +127,7 @@ inline matrix buildProjectionMatrixPerspectiveFovRH(float fieldOfViewRa return m; } -template +template inline matrix buildProjectionMatrixPerspectiveFovLH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) { const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); @@ -145,7 +145,7 @@ inline matrix buildProjectionMatrixPerspectiveFovLH(float fieldOfViewRa return m; } -template +template inline matrix buildProjectionMatrixOrthoRH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) { _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero @@ -161,7 +161,7 @@ inline matrix buildProjectionMatrixOrthoRH(float widthOfViewVolume, flo return m; } -template +template inline matrix buildProjectionMatrixOrthoLH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) { _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero diff --git a/include/nbl/core/decl/smart_refctd_ptr.h b/include/nbl/core/decl/smart_refctd_ptr.h index 7c231fea4b..74fdf61693 100644 --- a/include/nbl/core/decl/smart_refctd_ptr.h +++ b/include/nbl/core/decl/smart_refctd_ptr.h @@ -144,6 +144,15 @@ smart_refctd_ptr move_and_dynamic_cast(smart_refctd_ptr& smart_ptr); template< class U, class T > smart_refctd_ptr move_and_dynamic_cast(smart_refctd_ptr&& smart_ptr) {return move_and_dynamic_cast(smart_ptr);} +template +struct is_smart_refctd_ptr : std::false_type {}; + +template +struct is_smart_refctd_ptr> : std::true_type {}; + +template +inline constexpr bool is_smart_refctd_ptr_v = is_smart_refctd_ptr::value; + } // end namespace nbl::core /* From 2a29a2f970ea114476cec1fa741075773ad1dbf0 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 3 Nov 2024 19:30:28 +0100 Subject: [PATCH 011/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index d3f325d6d2..f9fce56971 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d3f325d6d2b4c9c623b8e70a0882a1bae17471fb +Subproject commit f9fce56971dd80264eb107c413ba0b038f4ebf96 From 7673769d878a6a12034d2a97fd51a49abb077a6c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 3 Nov 2024 20:12:10 +0100 Subject: [PATCH 012/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index f9fce56971..2f44640ec4 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit f9fce56971dd80264eb107c413ba0b038f4ebf96 +Subproject commit 2f44640ec4e823c5d97b427562b291fd0e3eb62f From 9a139df50bbf14c01a1b6e45bd7c9370f5ea2670 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 6 Nov 2024 17:58:29 +0100 Subject: [PATCH 013/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 2f44640ec4..4428dbb6c4 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 2f44640ec4e823c5d97b427562b291fd0e3eb62f +Subproject commit 4428dbb6c4b03ea80aae746b3f7ba51ed040f027 From 24a80c98802a4c418bc1e63db0ff623458bea892 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 8 Nov 2024 16:28:39 +0100 Subject: [PATCH 014/161] create ui::E_MOUSE_CODE & constexpr ui::mouseCodeToString - we really were missing an equivalent of E_KEY_CODE. Update examples_tests submodule --- examples_tests | 2 +- include/nbl/ui/KeyCodes.h | 51 ++++++++++++++++++++++++++++++++++++ include/nbl/ui/SInputEvent.h | 1 - 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/examples_tests b/examples_tests index 4428dbb6c4..4d32863a11 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 4428dbb6c4b03ea80aae746b3f7ba51ed040f027 +Subproject commit 4d32863a11a727b255d9dfaa8592336edb4151b9 diff --git a/include/nbl/ui/KeyCodes.h b/include/nbl/ui/KeyCodes.h index b6d05aed36..eae8edaf5f 100644 --- a/include/nbl/ui/KeyCodes.h +++ b/include/nbl/ui/KeyCodes.h @@ -276,5 +276,56 @@ enum E_MOUSE_BUTTON : uint8_t EMB_COUNT, }; +// Unambiguous set of "codes" to represent various mouse actions we support with Nabla - equivalent of E_KEY_CODE +enum E_MOUSE_CODE : uint8_t +{ + EMC_NONE = 0, + + // I know its E_MOUSE_BUTTON, this enum *must* be more abstract to standardize mouse + EMC_LEFT_BUTTON, + EMC_RIGHT_BUTTON, + EMC_MIDDLE_BUTTON, + EMC_BUTTON_4, + EMC_BUTTON_5, + + // and this is kinda SMouseEvent::E_EVENT_TYPE::EET_SCROLL + EMC_VERTICAL_POSITIVE_SCROLL, + EMC_VERTICAL_NEGATIVE_SCROLL, + EMC_HORIZONTAL_POSITIVE_SCROLL, + EMC_HORIZONTAL_NEGATIVE_SCROLL, + + // SMouseEvent::E_EVENT_TYPE::EET_MOVEMENT + EMC_RELATIVE_POSITIVE_MOVEMENT_X, + EMC_RELATIVE_POSITIVE_MOVEMENT_Y, + EMC_RELATIVE_NEGATIVE_MOVEMENT_X, + EMC_RELATIVE_NEGATIVE_MOVEMENT_Y, + + EMC_COUNT, +}; + +constexpr std::string_view mouseCodeToString(E_MOUSE_CODE code) +{ + switch (code) + { + case EMC_LEFT_BUTTON: return "LEFT_BUTTON"; + case EMC_RIGHT_BUTTON: return "RIGHT_BUTTON"; + case EMC_MIDDLE_BUTTON: return "MIDDLE_BUTTON"; + case EMC_BUTTON_4: return "BUTTON_4"; + case EMC_BUTTON_5: return "BUTTON_5"; + + case EMC_VERTICAL_POSITIVE_SCROLL: return "VERTICAL_POSITIVE_SCROLL"; + case EMC_VERTICAL_NEGATIVE_SCROLL: return "VERTICAL_NEGATIVE_SCROLL"; + case EMC_HORIZONTAL_POSITIVE_SCROLL: return "HORIZONTAL_POSITIVE_SCROLL"; + case EMC_HORIZONTAL_NEGATIVE_SCROLL: return "HORIZONTAL_NEGATIVE_SCROLL"; + + case EMC_RELATIVE_POSITIVE_MOVEMENT_X: return "RELATIVE_POSITIVE_MOVEMENT_X"; + case EMC_RELATIVE_POSITIVE_MOVEMENT_Y: return "RELATIVE_POSITIVE_MOVEMENT_Y"; + case EMC_RELATIVE_NEGATIVE_MOVEMENT_X: return "RELATIVE_NEGATIVE_MOVEMENT_X"; + case EMC_RELATIVE_NEGATIVE_MOVEMENT_Y: return "RELATIVE_NEGATIVE_MOVEMENT_Y"; + + default: return "NONE"; + } +} + } #endif diff --git a/include/nbl/ui/SInputEvent.h b/include/nbl/ui/SInputEvent.h index d791d08e53..9575f626d4 100644 --- a/include/nbl/ui/SInputEvent.h +++ b/include/nbl/ui/SInputEvent.h @@ -55,7 +55,6 @@ struct SMouseEvent : SEventBase IWindow* window; }; - struct SKeyboardEvent : SEventBase { inline SKeyboardEvent(std::chrono::microseconds ts) : SEventBase(ts) { } From 21b89b48d1d0574ddadd34eca64cac36848aa6f5 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 12 Nov 2024 00:58:02 +0100 Subject: [PATCH 015/161] @alichraghi small homework - read https://en.cppreference.com/w/cpp/language/inline and afterwards https://gudok.xyz/inline/ please --- src/nbl/asset/utils/IShaderCompiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nbl/asset/utils/IShaderCompiler.cpp b/src/nbl/asset/utils/IShaderCompiler.cpp index bde89557ec..01cd360389 100644 --- a/src/nbl/asset/utils/IShaderCompiler.cpp +++ b/src/nbl/asset/utils/IShaderCompiler.cpp @@ -24,7 +24,7 @@ IShaderCompiler::IShaderCompiler(core::smart_refctd_ptr&& syste m_defaultIncludeFinder = core::make_smart_refctd_ptr(core::smart_refctd_ptr(m_system)); } -inline core::smart_refctd_ptr nbl::asset::IShaderCompiler::compileToSPIRV(const std::string_view code, const SCompilerOptions& options) const +core::smart_refctd_ptr nbl::asset::IShaderCompiler::compileToSPIRV(const std::string_view code, const SCompilerOptions& options) const { CCache::SEntry entry; std::vector dependencies; From 68bc7be7837318fd60658d2b0a1b703babee7732 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 12 Nov 2024 08:23:11 +0100 Subject: [PATCH 016/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c991edf6fd..797ba40eac 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c991edf6fdee8a721285cc8940e6b4111fd6427b +Subproject commit 797ba40eacaf8c03417d9b20df8d051edc80413d From 866e84409a0e69ba91069de4b7733974c1d09b29 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 15 Nov 2024 14:15:34 +0100 Subject: [PATCH 017/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 797ba40eac..65efa99add 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 797ba40eacaf8c03417d9b20df8d051edc80413d +Subproject commit 65efa99addb6e968a923f5f646023b0c0823f156 From 8bd64367c8f5454273f7849ee48ae4e22016816d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 15 Nov 2024 14:40:19 +0100 Subject: [PATCH 018/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 65efa99add..2c428694d4 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 65efa99addb6e968a923f5f646023b0c0823f156 +Subproject commit 2c428694d4a6e71a61e969ad3cac263b072f7fe3 From de69afe3ed04a3a4c221358f688ec43a87040f4f Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 18 Nov 2024 14:55:56 +0100 Subject: [PATCH 019/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 2c428694d4..c1063e29ed 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 2c428694d4a6e71a61e969ad3cac263b072f7fe3 +Subproject commit c1063e29ed38f85410f97232b9372cb43a7a551e From a55bd390d189900ed05058838f92f0c4f950b71b Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 18 Nov 2024 15:29:01 +0100 Subject: [PATCH 020/161] update imguizmo with its upstream --- 3rdparty/CMakeLists.txt | 13 +++++++++++-- 3rdparty/imguizmo | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 0165b912e7..5357588043 100755 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -382,8 +382,17 @@ if(NBL_BUILD_IMGUI) target_link_libraries(imtestengine PUBLIC imtestsuite) - set(IMGUIZMO_BUILD_EXAMPLE OFF) - add_subdirectory(imguizmo EXCLUDE_FROM_ALL) + # imguizmo + add_library(imguizmo + "${CMAKE_CURRENT_SOURCE_DIR}/imguizmo/GraphEditor.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/imguizmo/ImCurveEdit.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/imguizmo/ImGradient.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/imguizmo/ImGuizmo.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/imguizmo/ImSequencer.cpp" + ) + + target_include_directories(imguizmo PUBLIC $) + target_link_libraries(imguizmo PUBLIC imgui) # note we override imgui config with our own set(NBL_IMGUI_USER_CONFIG_FILEPATH "${NBL_IMGUI_ROOT}/nabla_imconfig.h") diff --git a/3rdparty/imguizmo b/3rdparty/imguizmo index 6f4b2197ef..b10e91756d 160000 --- a/3rdparty/imguizmo +++ b/3rdparty/imguizmo @@ -1 +1 @@ -Subproject commit 6f4b2197efd715d16b19775b00f36c6c6f5aacb6 +Subproject commit b10e91756d32395f5c1fefd417899b657ed7cb88 From 6f3c5fc7a581c35922c0749076e8300647460bf0 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 18 Nov 2024 16:35:14 +0100 Subject: [PATCH 021/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c1063e29ed..d6db4a7f0e 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c1063e29ed38f85410f97232b9372cb43a7a551e +Subproject commit d6db4a7f0e10535d44e925e7237a6d4fa56cc740 From 0d126dfbb9530fd0a9f574b3cf6f42bbf03fe977 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 2 Dec 2024 09:48:37 +0100 Subject: [PATCH 022/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index d6db4a7f0e..c170c5d687 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d6db4a7f0e10535d44e925e7237a6d4fa56cc740 +Subproject commit c170c5d68719977a9000af4ad5eed35a517a8d1d From 31e85e78cda8a6d76dce4d11b23e7b9e5f8c6bd8 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 2 Dec 2024 19:32:40 +0100 Subject: [PATCH 023/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c170c5d687..df0d70210a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c170c5d68719977a9000af4ad5eed35a517a8d1d +Subproject commit df0d70210a02f8bbc47aaaab750d4b06c505a248 From 08f11f61d37a22f090d5543c19db604026ced80e Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 3 Dec 2024 16:45:27 +0100 Subject: [PATCH 024/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index df0d70210a..6cd5e18f6a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit df0d70210a02f8bbc47aaaab750d4b06c505a248 +Subproject commit 6cd5e18f6a62f6387d40a4137ac577ca42140613 From 5bcd6ef7a80815748db0a36dfbb63099abb7bbc5 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 12 Dec 2024 11:08:03 +0100 Subject: [PATCH 025/161] add getCastedMatrix & getCastedVector, update examples_tests submodule --- examples_tests | 2 +- .../transformation_matrix_utils.hlsl | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 6cd5e18f6a..11585e2355 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 6cd5e18f6a62f6387d40a4137ac577ca42140613 +Subproject commit 11585e2355b15b6c22cdbdde81c785ed9b8e6b01 diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl index 22bc82ea13..926398354f 100644 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -19,6 +19,28 @@ matrix getMatrix3x4As4x4(const matrix& mat) return output; } +template +inline vector getCastedVector(const vector& in) +{ + vector out; + + for (int i = 0; i < N; ++i) + out[i] = (Tout)(in[i]); + + return out; +} + +template +inline matrix getCastedMatrix(const matrix& in) +{ + matrix out; + + for (int i = 0; i < N; ++i) + out[i] = getCastedVector(in[i]); + + return out; +} + // TODO: use portable_float when merged //! multiplies matrices a and b, 3x4 matrices are treated as 4x4 matrices with 4th row set to (0, 0, 0 ,1) template From 5a07fe7141474ccb44f68fc9e43db13c9bf85f96 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 08:26:46 +0100 Subject: [PATCH 026/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 11585e2355..ded92d8f53 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 11585e2355b15b6c22cdbdde81c785ed9b8e6b01 +Subproject commit ded92d8f53b10c9ab87dde982cf19e37a0cc29df From f55b268bf602497c48852451dabdf92f60f039f8 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 10:59:43 +0100 Subject: [PATCH 027/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index ded92d8f53..5e5c6814c1 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit ded92d8f53b10c9ab87dde982cf19e37a0cc29df +Subproject commit 5e5c6814c1446073387b76296f7565afbb415134 From 01f909ae696aa84f993fae31b425029ce07132aa Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 16:06:14 +0100 Subject: [PATCH 028/161] add examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 5e5c6814c1..1d799e1af6 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 5e5c6814c1446073387b76296f7565afbb415134 +Subproject commit 1d799e1af6319f5989721cd38108ac8e3b12e93f From ab90d33d6bfc13ec4028169ef9f5629ce12eafed Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 13 Dec 2024 17:21:10 +0100 Subject: [PATCH 029/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 1d799e1af6..73c2a6a696 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 1d799e1af6319f5989721cd38108ac8e3b12e93f +Subproject commit 73c2a6a696c7c49ef444e0006c3901a8b36b19be From c9f09608fcb81cac8f149bb147be78d92df24afb Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 14 Dec 2024 14:51:37 +0100 Subject: [PATCH 030/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 73c2a6a696..6ad873ee20 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 73c2a6a696c7c49ef444e0006c3901a8b36b19be +Subproject commit 6ad873ee20c8bbf15f7e497907b9936f2f7f74e8 From 3727ae3e38dc9d6a653528d2e5b7fd34e9e5f2f1 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 14 Dec 2024 16:03:41 +0100 Subject: [PATCH 031/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 6ad873ee20..c615d58beb 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 6ad873ee20c8bbf15f7e497907b9936f2f7f74e8 +Subproject commit c615d58bebb71e5ddb27f48f8a09425a7ba0b33d From e9f78a2fbd995540e2ede9be445d5a6492142567 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 14 Dec 2024 17:04:01 +0100 Subject: [PATCH 032/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c615d58beb..10fe3b2972 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c615d58bebb71e5ddb27f48f8a09425a7ba0b33d +Subproject commit 10fe3b297287b576b6689b959f46f57362410ab9 From 33f8d01e10cbf1f6ee1e5c51b7ec74d26dc65986 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 16 Dec 2024 15:58:27 +0100 Subject: [PATCH 033/161] add constexpr stringToKeyCode & stringToMouseCode --- include/nbl/ui/KeyCodes.h | 149 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/include/nbl/ui/KeyCodes.h b/include/nbl/ui/KeyCodes.h index eae8edaf5f..fb749cb801 100644 --- a/include/nbl/ui/KeyCodes.h +++ b/include/nbl/ui/KeyCodes.h @@ -266,6 +266,136 @@ constexpr char keyCodeToChar(E_KEY_CODE code, bool shiftPressed) return result; } +constexpr E_KEY_CODE stringToKeyCode(std::string_view str) +{ + if (str == "BACKSPACE") return EKC_BACKSPACE; + if (str == "TAB") return EKC_TAB; + if (str == "CLEAR") return EKC_CLEAR; + if (str == "ENTER") return EKC_ENTER; + if (str == "LEFT_SHIFT") return EKC_LEFT_SHIFT; + if (str == "RIGHT_SHIFT") return EKC_RIGHT_SHIFT; + if (str == "LEFT_CONTROL") return EKC_LEFT_CONTROL; + if (str == "RIGHT_CONTROL") return EKC_RIGHT_CONTROL; + if (str == "LEFT_ALT") return EKC_LEFT_ALT; + if (str == "RIGHT_ALT") return EKC_RIGHT_ALT; + if (str == "PAUSE") return EKC_PAUSE; + if (str == "CAPS_LOCK") return EKC_CAPS_LOCK; + if (str == "ESCAPE") return EKC_ESCAPE; + if (str == "SPACE") return EKC_SPACE; + if (str == "PAGE_UP") return EKC_PAGE_UP; + if (str == "PAGE_DOWN") return EKC_PAGE_DOWN; + if (str == "END") return EKC_END; + if (str == "HOME") return EKC_HOME; + if (str == "LEFT_ARROW") return EKC_LEFT_ARROW; + if (str == "RIGHT_ARROW") return EKC_RIGHT_ARROW; + if (str == "DOWN_ARROW") return EKC_DOWN_ARROW; + if (str == "UP_ARROW") return EKC_UP_ARROW; + if (str == "SELECT") return EKC_SELECT; + if (str == "PRINT") return EKC_PRINT; + if (str == "EXECUTE") return EKC_EXECUTE; + if (str == "PRINT_SCREEN") return EKC_PRINT_SCREEN; + if (str == "INSERT") return EKC_INSERT; + if (str == "DELETE") return EKC_DELETE; + if (str == "HELP") return EKC_HELP; + if (str == "LEFT_WIN") return EKC_LEFT_WIN; + if (str == "RIGHT_WIN") return EKC_RIGHT_WIN; + if (str == "APPS") return EKC_APPS; + if (str == "COMMA") return EKC_COMMA; + if (str == "PERIOD") return EKC_PERIOD; + if (str == "SEMICOLON") return EKC_SEMICOLON; + if (str == "OPEN_BRACKET") return EKC_OPEN_BRACKET; + if (str == "CLOSE_BRACKET") return EKC_CLOSE_BRACKET; + if (str == "BACKSLASH") return EKC_BACKSLASH; + if (str == "APOSTROPHE") return EKC_APOSTROPHE; + if (str == "ADD") return EKC_ADD; + if (str == "SUBTRACT") return EKC_SUBTRACT; + if (str == "MULTIPLY") return EKC_MULTIPLY; + if (str == "DIVIDE") return EKC_DIVIDE; + + if (str == "A" || str == "a") return EKC_A; + if (str == "B" || str == "b") return EKC_B; + if (str == "C" || str == "c") return EKC_C; + if (str == "D" || str == "d") return EKC_D; + if (str == "E" || str == "e") return EKC_E; + if (str == "F" || str == "f") return EKC_F; + if (str == "G" || str == "g") return EKC_G; + if (str == "H" || str == "h") return EKC_H; + if (str == "I" || str == "i") return EKC_I; + if (str == "J" || str == "j") return EKC_J; + if (str == "K" || str == "k") return EKC_K; + if (str == "L" || str == "l") return EKC_L; + if (str == "M" || str == "m") return EKC_M; + if (str == "N" || str == "n") return EKC_N; + if (str == "O" || str == "o") return EKC_O; + if (str == "P" || str == "p") return EKC_P; + if (str == "Q" || str == "q") return EKC_Q; + if (str == "R" || str == "r") return EKC_R; + if (str == "S" || str == "s") return EKC_S; + if (str == "T" || str == "t") return EKC_T; + if (str == "U" || str == "u") return EKC_U; + if (str == "V" || str == "v") return EKC_V; + if (str == "W" || str == "w") return EKC_W; + if (str == "X" || str == "x") return EKC_X; + if (str == "Y" || str == "y") return EKC_Y; + if (str == "Z" || str == "z") return EKC_Z; + + if (str == "0") return EKC_0; + if (str == "1") return EKC_1; + if (str == "2") return EKC_2; + if (str == "3") return EKC_3; + if (str == "4") return EKC_4; + if (str == "5") return EKC_5; + if (str == "6") return EKC_6; + if (str == "7") return EKC_7; + if (str == "8") return EKC_8; + if (str == "9") return EKC_9; + + if (str == "F1") return EKC_F1; + if (str == "F2") return EKC_F2; + if (str == "F3") return EKC_F3; + if (str == "F4") return EKC_F4; + if (str == "F5") return EKC_F5; + if (str == "F6") return EKC_F6; + if (str == "F7") return EKC_F7; + if (str == "F8") return EKC_F8; + if (str == "F9") return EKC_F9; + if (str == "F10") return EKC_F10; + if (str == "F11") return EKC_F11; + if (str == "F12") return EKC_F12; + if (str == "F13") return EKC_F13; + if (str == "F14") return EKC_F14; + if (str == "F15") return EKC_F15; + if (str == "F16") return EKC_F16; + if (str == "F17") return EKC_F17; + if (str == "F18") return EKC_F18; + if (str == "F19") return EKC_F19; + if (str == "F20") return EKC_F20; + if (str == "F21") return EKC_F21; + if (str == "F22") return EKC_F22; + if (str == "F23") return EKC_F23; + if (str == "F24") return EKC_F24; + + if (str == "NUMPAD_0") return EKC_NUMPAD_0; + if (str == "NUMPAD_1") return EKC_NUMPAD_1; + if (str == "NUMPAD_2") return EKC_NUMPAD_2; + if (str == "NUMPAD_3") return EKC_NUMPAD_3; + if (str == "NUMPAD_4") return EKC_NUMPAD_4; + if (str == "NUMPAD_5") return EKC_NUMPAD_5; + if (str == "NUMPAD_6") return EKC_NUMPAD_6; + if (str == "NUMPAD_7") return EKC_NUMPAD_7; + if (str == "NUMPAD_8") return EKC_NUMPAD_8; + if (str == "NUMPAD_9") return EKC_NUMPAD_9; + + if (str == "NUM_LOCK") return EKC_NUM_LOCK; + if (str == "SCROLL_LOCK") return EKC_SCROLL_LOCK; + + if (str == "VOLUME_MUTE") return EKC_VOLUME_MUTE; + if (str == "VOLUME_UP") return EKC_VOLUME_UP; + if (str == "VOLUME_DOWN") return EKC_VOLUME_DOWN; + + return EKC_NONE; +} + enum E_MOUSE_BUTTON : uint8_t { EMB_LEFT_BUTTON, @@ -327,5 +457,24 @@ constexpr std::string_view mouseCodeToString(E_MOUSE_CODE code) } } +constexpr E_MOUSE_CODE stringToMouseCode(std::string_view str) +{ + if (str == "LEFT_BUTTON") return EMC_LEFT_BUTTON; + if (str == "RIGHT_BUTTON") return EMC_RIGHT_BUTTON; + if (str == "MIDDLE_BUTTON") return EMC_MIDDLE_BUTTON; + if (str == "BUTTON_4") return EMC_BUTTON_4; + if (str == "BUTTON_5") return EMC_BUTTON_5; + if (str == "VERTICAL_POSITIVE_SCROLL") return EMC_VERTICAL_POSITIVE_SCROLL; + if (str == "VERTICAL_NEGATIVE_SCROLL") return EMC_VERTICAL_NEGATIVE_SCROLL; + if (str == "HORIZONTAL_POSITIVE_SCROLL") return EMC_HORIZONTAL_POSITIVE_SCROLL; + if (str == "HORIZONTAL_NEGATIVE_SCROLL") return EMC_HORIZONTAL_NEGATIVE_SCROLL; + if (str == "RELATIVE_POSITIVE_MOVEMENT_X") return EMC_RELATIVE_POSITIVE_MOVEMENT_X; + if (str == "RELATIVE_POSITIVE_MOVEMENT_Y") return EMC_RELATIVE_POSITIVE_MOVEMENT_Y; + if (str == "RELATIVE_NEGATIVE_MOVEMENT_X") return EMC_RELATIVE_NEGATIVE_MOVEMENT_X; + if (str == "RELATIVE_NEGATIVE_MOVEMENT_Y") return EMC_RELATIVE_NEGATIVE_MOVEMENT_Y; + + return EMC_NONE; +} + } #endif From b3d7710149ee40b3e41ce274e3edd437bf4c6052 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Tue, 17 Dec 2024 20:51:50 +0100 Subject: [PATCH 034/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 10fe3b2972..3ea910c6b5 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 10fe3b297287b576b6689b959f46f57362410ab9 +Subproject commit 3ea910c6b56110eee49547328396c9edcc0fda7e From 53f72ecfd4c3789c70d3c7c1bc891f364e4f2d66 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Wed, 18 Dec 2024 16:40:07 +0100 Subject: [PATCH 035/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 3ea910c6b5..e34cb66487 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 3ea910c6b56110eee49547328396c9edcc0fda7e +Subproject commit e34cb66487eea0e847a7c6a607978c14a806a752 From e3d282b13bac34f4aca6bd8aaad5dfe766d2b34f Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 19 Dec 2024 14:35:29 +0100 Subject: [PATCH 036/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index e34cb66487..1332a87201 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit e34cb66487eea0e847a7c6a607978c14a806a752 +Subproject commit 1332a872013c3e90528d8ddb3c1dbb24fd9ce328 From 5124136a09af43f0c8b1801cf096ae8cbf6a1a5d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 19 Dec 2024 16:37:40 +0100 Subject: [PATCH 037/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 1332a87201..a5e77b05f7 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 1332a872013c3e90528d8ddb3c1dbb24fd9ce328 +Subproject commit a5e77b05f7b4d11bd8defe62d5b7f76cbe28bfbe From fb2ce01c838f97f6be6192e75e6eef455f64e7ca Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 20 Dec 2024 10:42:38 +0100 Subject: [PATCH 038/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index a5e77b05f7..e89b1c1033 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit a5e77b05f7b4d11bd8defe62d5b7f76cbe28bfbe +Subproject commit e89b1c1033fe6c68db4c9b2b0af2aa5791c048ff From 7ce37ba3d18cb5e6068a5d41aac13344ed64c046 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 20 Dec 2024 16:51:08 +0100 Subject: [PATCH 039/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index e89b1c1033..237dd4394e 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit e89b1c1033fe6c68db4c9b2b0af2aa5791c048ff +Subproject commit 237dd4394e8a4268ee0290cd597e6f6d7318abde From 406fef84d11fb7feb36b403449d3895f7832b9be Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 21 Dec 2024 11:34:31 +0100 Subject: [PATCH 040/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 237dd4394e..4f0e0e8c60 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 237dd4394e8a4268ee0290cd597e6f6d7318abde +Subproject commit 4f0e0e8c603724217637ce2fdbd8389713764515 From 661dae9398c8810c90bb3de36f8d351ff817ea17 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sat, 21 Dec 2024 13:01:36 +0100 Subject: [PATCH 041/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 4f0e0e8c60..ae342b53e8 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 4f0e0e8c603724217637ce2fdbd8389713764515 +Subproject commit ae342b53e80745bef42eec89f078365f7f7739d2 From 183a8fd7eb243ef4ea47a277efb462e5a171f66e Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 22 Dec 2024 12:49:07 +0100 Subject: [PATCH 042/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index ae342b53e8..cb06b7105c 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit ae342b53e80745bef42eec89f078365f7f7739d2 +Subproject commit cb06b7105c4543a2a829c789521d912da3606106 From a30c409e155ef0d0fe7411ce3e21a33f999e32b6 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 22 Dec 2024 13:23:57 +0100 Subject: [PATCH 043/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index cb06b7105c..7a96b2b433 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit cb06b7105c4543a2a829c789521d912da3606106 +Subproject commit 7a96b2b4339641f3a1d2ceeeae9e7d3bf9c5c249 From 5f033af8f0fc4031ba5a99d8cd218022d5be9c66 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 08:55:55 +0100 Subject: [PATCH 044/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 7a96b2b433..374f5cf2a8 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 7a96b2b4339641f3a1d2ceeeae9e7d3bf9c5c249 +Subproject commit 374f5cf2a8aec7e6ed95fb2920f7b68afb28f4e7 From 0529f8aa9314a1c5fbb2d71eb07b0f559560ef8d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 09:15:29 +0100 Subject: [PATCH 045/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 374f5cf2a8..6ed9686297 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 374f5cf2a8aec7e6ed95fb2920f7b68afb28f4e7 +Subproject commit 6ed9686297804ba5df97a5d3cb5fbf89c3ddbae0 From 74b8c11f4c71df61f8c919021c5d4f1dac8192c0 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 12:16:42 +0100 Subject: [PATCH 046/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 6ed9686297..6231737b62 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 6ed9686297804ba5df97a5d3cb5fbf89c3ddbae0 +Subproject commit 6231737b62576453c0ef3b5e9cb2e33831aceb77 From 2fdf296917e93c63d7536f0575e339e0ebab7a75 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 23 Dec 2024 15:04:49 +0100 Subject: [PATCH 047/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 6231737b62..38168d3a1b 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 6231737b62576453c0ef3b5e9cb2e33831aceb77 +Subproject commit 38168d3a1b461b45a3861762a96516f5f2b1f134 From 3b6844464be03d9477add3bb630ef086de04861c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Mon, 30 Dec 2024 17:02:36 +0100 Subject: [PATCH 048/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 38168d3a1b..9f9c77eda5 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 38168d3a1b461b45a3861762a96516f5f2b1f134 +Subproject commit 9f9c77eda5539650b9f6d0974581e989f980886b From 2693caf4c02d68233665a9c1e802fba4fe21a3e7 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 24 Jan 2025 12:40:29 +0100 Subject: [PATCH 049/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index b44c1f9f20..0bebf6cf03 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit b44c1f9f20d8ad1b212fb6edb28b3eb28d517357 +Subproject commit 0bebf6cf033a13495bf9d59e5d36c30694a668eb From ae315a0b494897fc9919881d6211961f91f29409 Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 24 Jan 2025 14:00:17 +0100 Subject: [PATCH 050/161] a comment for glslFunctions, update examples_tests submodule --- examples_tests | 2 +- include/nbl/core/math/glslFunctions.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 0bebf6cf03..45a576252a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 0bebf6cf033a13495bf9d59e5d36c30694a668eb +Subproject commit 45a576252a698f4acd516726e3c6ac122753c174 diff --git a/include/nbl/core/math/glslFunctions.h b/include/nbl/core/math/glslFunctions.h index 3c9cc98850..1412be95d5 100644 --- a/include/nbl/core/math/glslFunctions.h +++ b/include/nbl/core/math/glslFunctions.h @@ -372,6 +372,7 @@ NBL_FORCE_INLINE T normalize(const T& v) } // TODO : matrixCompMult, outerProduct, inverse +// Arek: old and to be killed (missing .tcc include?), no definition in Nabla causing linker errors template NBL_FORCE_INLINE T transpose(const T& m); template<> From 07eeef771f1b36b035deecc3144d8db403c22a0f Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Fri, 31 Jan 2025 17:16:22 +0100 Subject: [PATCH 051/161] small temporary updates to transformation_matrix_utils.hlsl, update examples_tests submodule --- examples_tests | 2 +- .../transformation_matrix_utils.hlsl | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 45a576252a..578fc0cf5c 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 45a576252a698f4acd516726e3c6ac122753c174 +Subproject commit 578fc0cf5c0d74c37aef6e7c99c2f1cae68e911d diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl index 926398354f..6ad1e636df 100644 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -8,6 +8,25 @@ namespace nbl namespace hlsl { +// TODO: -> move somewhere else and nbl:: to implement it +template +bool isOrthoBase(const T& x, const T& y, const T& z, const E epsilon = 1e-6) +{ + auto isNormalized = [](const auto& v, const auto& epsilon) -> bool + { + return glm::epsilonEqual(glm::length(v), 1.0, epsilon); + }; + + auto isOrthogonal = [](const auto& a, const auto& b, const auto& epsilon) -> bool + { + return glm::epsilonEqual(glm::dot(a, b), 0.0, epsilon); + }; + + return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && + isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); +} +// <- + template matrix getMatrix3x4As4x4(const matrix& mat) { @@ -19,6 +38,17 @@ matrix getMatrix3x4As4x4(const matrix& mat) return output; } +template +matrix getMatrix3x3As4x4(const matrix& mat) +{ + matrix output; + for (int i = 0; i < 3; ++i) + output[i] = float32_t4(mat[i], 1.0f); + output[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + + return output; +} + template inline vector getCastedVector(const vector& in) { From 15aa72f5a81fdeb92f95fe6a733b0de64036a96d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 2 Feb 2025 20:46:51 +0100 Subject: [PATCH 052/161] introduce reference frame concept to imguizmo - commit submodule update --- 3rdparty/imguizmo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/imguizmo b/3rdparty/imguizmo index b10e91756d..4d3445248b 160000 --- a/3rdparty/imguizmo +++ b/3rdparty/imguizmo @@ -1 +1 @@ -Subproject commit b10e91756d32395f5c1fefd417899b657ed7cb88 +Subproject commit 4d3445248b9d598b92e877d752fd1f7fe6c1f134 From 661f999b8fa1f58807ac325fc72467a5b765a76d Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Sun, 2 Feb 2025 20:52:24 +0100 Subject: [PATCH 053/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 578fc0cf5c..61b5db004a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 578fc0cf5c0d74c37aef6e7c99c2f1cae68e911d +Subproject commit 61b5db004af20259dd18b65c994f7d807194333f From db1102c4fa7c0892c2aa493cf0b59b988717282c Mon Sep 17 00:00:00 2001 From: AnastaZIuk Date: Thu, 6 Feb 2025 16:37:16 +0100 Subject: [PATCH 054/161] update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 61b5db004a..cb450ba85c 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 61b5db004af20259dd18b65c994f7d807194333f +Subproject commit cb450ba85c7aafa5767268251ea3971b31abfd68 From 7d81d9ce58e0a209bf485b389dff9c30d27a3654 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 5 Feb 2026 13:26:02 +0100 Subject: [PATCH 055/161] imgui log polish --- examples_tests | 2 +- src/nbl/ext/ImGui/ImGui.cpp | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/examples_tests b/examples_tests index d9dfdd9eb4..b721b3c64d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d9dfdd9eb42540e8c036ea43a4466942b4b3d3a3 +Subproject commit b721b3c64d9a9076e4cd0111dac9f09a0c29b775 diff --git a/src/nbl/ext/ImGui/ImGui.cpp b/src/nbl/ext/ImGui/ImGui.cpp index 8da3f6c481..7b63530919 100644 --- a/src/nbl/ext/ImGui/ImGui.cpp +++ b/src/nbl/ext/ImGui/ImGui.cpp @@ -739,16 +739,22 @@ void UI::handleKeyEvents(const SUpdateParameters& params) const const auto& bind = keyMap[e.keyCode]; const auto& iCharacter = useBigLetters ? bind.physicalBig : bind.physicalSmall; - if(bind.target == ImGuiKey_None) - m_cachedCreationParams.utilities->getLogger()->log(std::string("Requested physical Nabla key \"") + iCharacter + std::string("\" has yet no mapping to IMGUI key!"), ILogger::ELL_ERROR); - else - if (e.action == SKeyboardEvent::ECA_PRESSED) - { - io.AddKeyEvent(bind.target, true); + if (bind.target == ImGuiKey_None) + { + if (e.action == SKeyboardEvent::ECA_PRESSED && iCharacter != 0) io.AddInputCharacter(iCharacter); - } - else if (e.action == SKeyboardEvent::ECA_RELEASED) - io.AddKeyEvent(bind.target, false); + + continue; + } + + if (e.action == SKeyboardEvent::ECA_PRESSED) + { + io.AddKeyEvent(bind.target, true); + if (iCharacter != 0) + io.AddInputCharacter(iCharacter); + } + else if (e.action == SKeyboardEvent::ECA_RELEASED) + io.AddKeyEvent(bind.target, false); } } From 04ce78468784d43882f9f49c46631d7d8128ae27 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 7 Feb 2026 10:37:31 +0100 Subject: [PATCH 056/161] Update camera module and async dispatcher exit --- examples_tests | 2 +- include/nbl/macros.h | 2 +- include/nbl/system/IAsyncQueueDispatcher.h | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/examples_tests b/examples_tests index b721b3c64d..4869976155 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit b721b3c64d9a9076e4cd0111dac9f09a0c29b775 +Subproject commit 48699761553f12176d439cb9561f4445d54303a3 diff --git a/include/nbl/macros.h b/include/nbl/macros.h index fe93201a11..c785b10112 100644 --- a/include/nbl/macros.h +++ b/include/nbl/macros.h @@ -62,7 +62,7 @@ // define a break macro for debugging. #if defined(_NBL_WINDOWS_API_) && defined(_MSC_VER) #include - #define _NBL_BREAK_IF( _CONDITION_ ) if (_CONDITION_) {_CrtDbgBreak();} + #define _NBL_BREAK_IF( _CONDITION_ ) if (_CONDITION_) {_CrtDbgBreak();} #else #include "signal.h" #define _NBL_BREAK_IF( _CONDITION_ ) if ( (_CONDITION_) ) raise(SIGTRAP); diff --git a/include/nbl/system/IAsyncQueueDispatcher.h b/include/nbl/system/IAsyncQueueDispatcher.h index d5b0cb8a1a..fa7e6a1b8d 100644 --- a/include/nbl/system/IAsyncQueueDispatcher.h +++ b/include/nbl/system/IAsyncQueueDispatcher.h @@ -490,6 +490,26 @@ class IAsyncQueueDispatcher : public IThreadHandler, pro protected: inline ~IAsyncQueueDispatcher() {} inline void background_work() {} + inline void exit(internal_state_t* optional_internal_state=nullptr) + { + while (cb_begin!=cb_end) + { + uint64_t r_id = cb_begin; + r_id = wrapAround(r_id); + + request_t& req = request_pool[r_id]; + if (future_base_t* future=req.wait()) + { + if constexpr (base_t::has_internal_state) + static_cast(this)->process_request(future,req.m_metadata,*optional_internal_state); + else + static_cast(this)->process_request(future,req.m_metadata); + req.notify(); + } + cb_begin++; + cb_begin.notify_one(); + } + } private: template From 40c1fdba2931aa743aa7089d0ccfbe086ded9e5a Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 7 Feb 2026 10:52:25 +0100 Subject: [PATCH 057/161] Restore transformation matrix utils compatibility --- .../transformation_matrix_utils.hlsl | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl new file mode 100644 index 0000000000..918cdc7e60 --- /dev/null +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -0,0 +1,222 @@ +#ifndef _NBL_BUILTIN_HLSL_TRANSFORMATION_MATRIX_UTILS_INCLUDED_ +#define _NBL_BUILTIN_HLSL_TRANSFORMATION_MATRIX_UTILS_INCLUDED_ + +#include +#include + +namespace nbl +{ +namespace hlsl +{ + +// TODO: -> move somewhere else and nbl:: to implement it +template +bool isOrthoBase(const T& x, const T& y, const T& z, const E epsilon = 1e-6) +{ + auto isNormalized = [](const auto& v, const auto& epsilon) -> bool + { + return glm::epsilonEqual(glm::length(v), 1.0, epsilon); + }; + + auto isOrthogonal = [](const auto& a, const auto& b, const auto& epsilon) -> bool + { + return glm::epsilonEqual(glm::dot(a, b), 0.0, epsilon); + }; + + return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && + isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); +} +// <- + +template +matrix getMatrix3x4As4x4(const matrix& mat) +{ + matrix output; + for (int i = 0; i < 3; ++i) + output[i] = mat[i]; + output[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + + return output; +} + +template +matrix getMatrix3x3As4x4(const matrix& mat) +{ + matrix output; + for (int i = 0; i < 3; ++i) + output[i] = float32_t4(mat[i], 1.0f); + output[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + + return output; +} + +template +inline vector getCastedVector(const vector& in) +{ + vector out; + + for (int i = 0; i < N; ++i) + out[i] = (Tout)(in[i]); + + return out; +} + +template +inline matrix getCastedMatrix(const matrix& in) +{ + matrix out; + + for (int i = 0; i < N; ++i) + out[i] = getCastedVector(in[i]); + + return out; +} + +// TODO: remove +//! multiplies matrices a and b, 3x4 matrices are treated as 4x4 matrices with 4th row set to (0, 0, 0 ,1) +template +inline matrix concatenateBFollowedByA(const matrix& a, const matrix& b) +{ + const auto a4x4 = getMatrix3x4As4x4(a); + const auto b4x4 = getMatrix3x4As4x4(b); + return matrix(mul(a4x4, b4x4)); +} + +// /Arek: glm:: for normalize till dot product is fixed (ambiguity with glm namespace + linker issues) + +template +inline matrix buildCameraLookAtMatrixLH( + const vector& position, + const vector& target, + const vector& upVector) +{ + const vector zaxis = glm::normalize(target - position); + const vector xaxis = glm::normalize(hlsl::cross(upVector, zaxis)); + const vector yaxis = hlsl::cross(zaxis, xaxis); + + matrix r; + r[0] = vector(xaxis, -hlsl::dot(xaxis, position)); + r[1] = vector(yaxis, -hlsl::dot(yaxis, position)); + r[2] = vector(zaxis, -hlsl::dot(zaxis, position)); + + return r; +} + +template +inline matrix buildCameraLookAtMatrixRH( + const vector& position, + const vector& target, + const vector& upVector) +{ + const vector zaxis = glm::normalize(position - target); + const vector xaxis = glm::normalize(hlsl::cross(upVector, zaxis)); + const vector yaxis = hlsl::cross(zaxis, xaxis); + + matrix r; + r[0] = vector(xaxis, -hlsl::dot(xaxis, position)); + r[1] = vector(yaxis, -hlsl::dot(yaxis, position)); + r[2] = vector(zaxis, -hlsl::dot(zaxis, position)); + + return r; +} + +// TODO: test, check if there is better implementation +// TODO: move quaternion to nbl::hlsl +// TODO: why NBL_REF_ARG(MatType) doesn't work????? + +//! Replaces curent rocation and scale by rotation represented by quaternion `quat`, leaves 4th row and 4th colum unchanged +template +inline void setRotation(matrix& outMat, NBL_CONST_REF_ARG(math::quaternion) quat) +{ + static_assert(N == 3 || N == 4); + matrix mat = _static_cast>(quat); + + outMat[0] = mat[0]; + + outMat[1] = mat[1]; + + outMat[2] = mat[2]; +} + +template +inline void setTranslation(matrix& outMat, NBL_CONST_REF_ARG(vector) translation) +{ + static_assert(N == 3 || N == 4); + + outMat[0].w = translation.x; + outMat[1].w = translation.y; + outMat[2].w = translation.z; +} + + +template +inline matrix buildProjectionMatrixPerspectiveFovRH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) +{ + const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); + _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero + const float w = h / aspectRatio; + + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(w, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -h, 0.f, 0.f); + m[2] = vector(0.f, 0.f, -zFar / (zFar - zNear), -zNear * zFar / (zFar - zNear)); + m[3] = vector(0.f, 0.f, -1.f, 0.f); + + return m; +} +template +inline matrix buildProjectionMatrixPerspectiveFovLH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) +{ + const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); + _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero + const float w = h / aspectRatio; + + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(w, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -h, 0.f, 0.f); + m[2] = vector(0.f, 0.f, zFar / (zFar - zNear), -zNear * zFar / (zFar - zNear)); + m[3] = vector(0.f, 0.f, 1.f, 0.f); + + return m; +} + +template +inline matrix buildProjectionMatrixOrthoRH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) +{ + _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(heightOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(2.f / widthOfViewVolume, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -2.f / heightOfViewVolume, 0.f, 0.f); + m[2] = vector(0.f, 0.f, -1.f / (zFar - zNear), -zNear / (zFar - zNear)); + m[3] = vector(0.f, 0.f, 0.f, 1.f); + + return m; +} + +template +inline matrix buildProjectionMatrixOrthoLH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) +{ + _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(heightOfViewVolume == 0.f); //division by zero + _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero + + matrix m; + m[0] = vector(2.f / widthOfViewVolume, 0.f, 0.f, 0.f); + m[1] = vector(0.f, -2.f / heightOfViewVolume, 0.f, 0.f); + m[2] = vector(0.f, 0.f, 1.f / (zFar - zNear), -zNear / (zFar - zNear)); + m[3] = vector(0.f, 0.f, 0.f, 1.f); + + return m; +} + +} +} + +#endif \ No newline at end of file From 749175c2705604dee1f4964e1b42ff2bc04530c7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 14 Feb 2026 20:23:41 +0100 Subject: [PATCH 058/161] Update examples_tests submodule pointer --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index acafa67c2f..9ef343b797 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit acafa67c2f4e104c653afbc834cbdf2163570736 +Subproject commit 9ef343b797dcc59a6281222100195028a36c7bc6 From 037fc085f2a24ec845a71876a9eb9a35bc96c42d Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 13:19:56 +0100 Subject: [PATCH 059/161] Update examples_tests submodule pointer --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 9ef343b797..430f63032e 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 9ef343b797dcc59a6281222100195028a36c7bc6 +Subproject commit 430f63032ea1bc67eb6da730f456715abad8f3e9 From 4b7cbe8b52f861494da2745ba30f5d19fb176b81 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 13:26:27 +0100 Subject: [PATCH 060/161] Update examples_tests pointer for world axis fix --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 430f63032e..dc663a191c 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 430f63032ea1bc67eb6da730f456715abad8f3e9 +Subproject commit dc663a191c631ae0d3e7c5e0b80d159c9851e352 From 8e76424f9bf0eea9a4fe040555d2ff493067b168 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 20:18:00 +0100 Subject: [PATCH 061/161] Update examples tests camera smoke integration --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index dc663a191c..374ce04f96 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit dc663a191c631ae0d3e7c5e0b80d159c9851e352 +Subproject commit 374ce04f96abbc0106b797f47048b2e083134ec2 From 69d49fdd2da3fcde8b5325dd67d54bacc6bfa3e2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 20:48:47 +0100 Subject: [PATCH 062/161] Add frustum extension and camera smoke updates --- examples_tests | 2 +- include/nbl/ext/Frustum/CDrawFrustum.h | 119 +++++ .../nbl/ext/Frustum/builtin/hlsl/common.hlsl | 61 +++ .../builtin/hlsl/draw_frustum.unified.hlsl | 43 ++ src/nbl/ext/CMakeLists.txt | 10 + src/nbl/ext/frustum/CDrawFrustum.cpp | 464 ++++++++++++++++++ src/nbl/ext/frustum/CMakeLists.txt | 75 +++ 7 files changed, 773 insertions(+), 1 deletion(-) create mode 100644 include/nbl/ext/Frustum/CDrawFrustum.h create mode 100644 include/nbl/ext/Frustum/builtin/hlsl/common.hlsl create mode 100644 include/nbl/ext/Frustum/builtin/hlsl/draw_frustum.unified.hlsl create mode 100644 src/nbl/ext/frustum/CDrawFrustum.cpp create mode 100644 src/nbl/ext/frustum/CMakeLists.txt diff --git a/examples_tests b/examples_tests index 374ce04f96..1fede0b5f6 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 374ce04f96abbc0106b797f47048b2e083134ec2 +Subproject commit 1fede0b5f67f11fb042b0d6e23f8d743bb517a45 diff --git a/include/nbl/ext/Frustum/CDrawFrustum.h b/include/nbl/ext/Frustum/CDrawFrustum.h new file mode 100644 index 0000000000..9ad7c5c764 --- /dev/null +++ b/include/nbl/ext/Frustum/CDrawFrustum.h @@ -0,0 +1,119 @@ +// Copyright (C) 2018-2026 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _NBL_EXT_FRUSTUM_DRAW_FRUSTUM_H_ +#define _NBL_EXT_FRUSTUM_DRAW_FRUSTUM_H_ + +#include "nbl/video/declarations.h" +#include "nbl/builtin/hlsl/cpp_compat.hlsl" +#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" +#include "nbl/ext/Frustum/builtin/hlsl/common.hlsl" + +namespace nbl::ext::frustum +{ + class CDrawFrustum final : public core::IReferenceCounted + { + public: + static constexpr inline uint32_t IndicesCount = 24u; + + enum DrawMode : uint16_t + { + DM_SINGLE = 0b01, + DM_BATCH = 0b10, + DM_BOTH = 0b11 + }; + + struct SCachedCreationParameters + { + using streaming_buffer_t = video::StreamingTransientDataBufferST>; + static constexpr inline auto RequiredAllocateFlags = core::bitflag(video::IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT); + static constexpr inline auto RequiredUsageFlags = core::bitflag(asset::IBuffer::EUF_STORAGE_BUFFER_BIT) | asset::IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT; + DrawMode drawMode = DM_BOTH; + core::smart_refctd_ptr utilities; + core::smart_refctd_ptr streamingBuffer = nullptr; + }; + + struct SCreationParameters : SCachedCreationParameters + { + video::IQueue* transfer = nullptr; + core::smart_refctd_ptr assetManager = nullptr; + + core::smart_refctd_ptr singlePipelineLayout = nullptr; + core::smart_refctd_ptr batchPipelineLayout = nullptr; + core::smart_refctd_ptr renderpass = nullptr; + + inline bool validate() const + { + const auto validation = std::to_array + ({ + std::make_pair(bool(assetManager), "Invalid `creationParams.assetManager` is nullptr!"), + std::make_pair(bool(utilities), "Invalid `creationParams.utilities` is nullptr!"), + std::make_pair(bool(transfer), "Invalid `creationParams.transfer` is nullptr!"), + std::make_pair(bool(renderpass), "Invalid `creationParams.renderpass` is nullptr!"), + std::make_pair(bool(utilities->getLogicalDevice()->getPhysicalDevice()->getQueueFamilyProperties()[transfer->getFamilyIndex()].queueFlags.hasFlags(video::IQueue::FAMILY_FLAGS::TRANSFER_BIT)), "Invalid `creationParams.transfer` is not capable of transfer operations!") + }); + system::logger_opt_ptr logger = utilities->getLogger(); + for (const auto& [ok, error] : validation) + if (!ok) + { + logger.log(error, system::ILogger::ELL_ERROR); + return false; + } + + assert(bool(assetManager->getSystem())); + + return true; + } + }; + + struct DrawParameters + { + video::IGPUCommandBuffer* commandBuffer = nullptr; + hlsl::float32_t4x4 viewProjectionMatrix; + float lineWidth = 2.f; + }; + + static core::smart_refctd_ptr create(SCreationParameters&& params); + + static core::smart_refctd_ptr createPipelineLayoutFromPCRange(video::ILogicalDevice* device, const asset::SPushConstantRange& pcRange); + + static core::smart_refctd_ptr createDefaultPipelineLayout(video::ILogicalDevice* device, DrawMode mode = DM_BATCH); + + static const core::smart_refctd_ptr mount(core::smart_refctd_ptr logger, system::ISystem* system, video::ILogicalDevice* device, const std::string_view archiveAlias = ""); + + inline const SCachedCreationParameters& getCreationParameters() const { return m_cachedCreationParams; } + + bool renderSingle(const DrawParameters& params, const hlsl::float32_t4x4& frustumTransform,const hlsl::float32_t4 & color); + bool render(const DrawParameters& params, video::ISemaphore::SWaitInfo waitInfo, std::span frustumInstances); + protected: + + struct ConstructorParams + { + SCachedCreationParameters creationParams; + core::smart_refctd_ptr singlePipeline = nullptr; + core::smart_refctd_ptr batchPipeline = nullptr; + core::smart_refctd_ptr indicesBuffer = nullptr; + }; + + CDrawFrustum(ConstructorParams&& params) : + m_cachedCreationParams(std::move(params.creationParams)), + m_singlePipeline(std::move(params.singlePipeline)), + m_batchPipeline(std::move(params.batchPipeline)), + m_indicesBuffer(std::move(params.indicesBuffer)) + {} + ~CDrawFrustum() override {}; + private: + static core::smart_refctd_ptr createPipeline(SCreationParameters& params, const video::IGPUPipelineLayout* pipelineLayout, const DrawMode mode); + static bool createStreamingBuffer(SCreationParameters& params); + static core::smart_refctd_ptr createIndicesBuffer(SCreationParameters& params); + + core::smart_refctd_ptr m_indicesBuffer; + + SCachedCreationParameters m_cachedCreationParams; + + core::smart_refctd_ptr m_singlePipeline; + core::smart_refctd_ptr m_batchPipeline; + }; +} +#endif diff --git a/include/nbl/ext/Frustum/builtin/hlsl/common.hlsl b/include/nbl/ext/Frustum/builtin/hlsl/common.hlsl new file mode 100644 index 0000000000..52a6454df3 --- /dev/null +++ b/include/nbl/ext/Frustum/builtin/hlsl/common.hlsl @@ -0,0 +1,61 @@ +// Copyright (C) 2018-2026 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _NBL_EXT_FRUSTUM_BUILTIN_HLSL_COMMON_INCLUDED_ +#define _NBL_EXT_FRUSTUM_BUILTIN_HLSL_COMMON_INCLUDED_ + +#include "nbl/builtin/hlsl/cpp_compat.hlsl" +#ifdef __HLSL_VERSION +#include "nbl/builtin/hlsl/math/linalg/fast_affine.hlsl" +#include "nbl/builtin/hlsl/glsl_compat/core.hlsl" +#include "nbl/builtin/hlsl/bda/__ptr.hlsl" +#endif + +namespace nbl +{ +namespace ext +{ +namespace frustum +{ + +struct InstanceData +{ + hlsl::float32_t4x4 transform; + hlsl::float32_t4 color; +}; + +struct SSinglePC +{ + InstanceData instance; +}; + +struct SInstancedPC +{ + uint64_t pInstanceBuffer; +}; + +struct PushConstants +{ + SSinglePC spc; + SInstancedPC ipc; +}; +#ifdef __HLSL_VERSION +struct PSInput +{ + float32_t4 position : SV_Position; + nointerpolation float32_t4 color : TEXCOORD0; +}; + +float32_t3 getNDCCubeVertex() +{ + float32_t3 v = (hlsl::promote(hlsl::glsl::gl_VertexIndex()) >> uint32_t3(0,2,1)) & 0x1u; + return v * float32_t3(2,2,1) + float32_t3(-1,-1,0); +} +#endif + +} +} +} + +#endif \ No newline at end of file diff --git a/include/nbl/ext/Frustum/builtin/hlsl/draw_frustum.unified.hlsl b/include/nbl/ext/Frustum/builtin/hlsl/draw_frustum.unified.hlsl new file mode 100644 index 0000000000..1b063bc2d4 --- /dev/null +++ b/include/nbl/ext/Frustum/builtin/hlsl/draw_frustum.unified.hlsl @@ -0,0 +1,43 @@ +// Copyright (C) 2018-2026 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Frustum/builtin/hlsl/common.hlsl" + +using namespace nbl::hlsl; +using namespace nbl::ext::frustum; +// Push constants +[[vk::push_constant]] PushConstants pc; + +[shader("vertex")] +PSInput frustum_vertex_single() +{ + PSInput output; + float32_t3 vertex = getNDCCubeVertex(); + + output.position = math::linalg::promoted_mul(pc.spc.instance.transform, vertex); + output.color = pc.spc.instance.color; + + return output; +} + // Vertex shader - batch mode (instanced) +[shader("vertex")] +PSInput frustum_vertex_instances() +{ + PSInput output; + const float32_t3 vertex = getNDCCubeVertex(); + InstanceData instance = vk::BufferPointer(pc.ipc.pInstanceBuffer + sizeof(InstanceData) * glsl::gl_InstanceIndex()).Get(); + + output.position = math::linalg::promoted_mul(instance.transform, vertex); + output.color = instance.color; + + return output; +} + +[shader("pixel")] +float32_t4 frustum_fragment(PSInput input) : SV_TARGET +{ + float32_t4 outColor = input.color; + + return outColor; +} \ No newline at end of file diff --git a/src/nbl/ext/CMakeLists.txt b/src/nbl/ext/CMakeLists.txt index b4c6cf2b64..f3b55531c2 100644 --- a/src/nbl/ext/CMakeLists.txt +++ b/src/nbl/ext/CMakeLists.txt @@ -66,6 +66,16 @@ if(NBL_BUILD_DEBUG_DRAW) ) endif() +add_subdirectory(frustum) +set(NBL_EXT_FRUSTUM_INCLUDE_DIRS + ${NBL_EXT_FRUSTUM_INCLUDE_DIRS} + PARENT_SCOPE +) +set(NBL_EXT_FRUSTUM_LIB + ${NBL_EXT_FRUSTUM_LIB} + PARENT_SCOPE +) + add_subdirectory(FullScreenTriangle) set(NBL_EXT_FULL_SCREEN_TRIANGLE_INCLUDE_DIRS ${NBL_EXT_FULL_SCREEN_TRIANGLE_INCLUDE_DIRS} diff --git a/src/nbl/ext/frustum/CDrawFrustum.cpp b/src/nbl/ext/frustum/CDrawFrustum.cpp new file mode 100644 index 0000000000..7874498757 --- /dev/null +++ b/src/nbl/ext/frustum/CDrawFrustum.cpp @@ -0,0 +1,464 @@ +// Copyright (C) 2018-2026 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h +#include "nbl/ext/Frustum/CDrawFrustum.h" + +#ifdef NBL_EMBED_BUILTIN_RESOURCES +#include "nbl/ext/frustum/builtin/build/CArchive.h" +#endif + +#include "nbl/ext/Frustum/builtin/build/spirv/keys.hpp" + +using namespace nbl; +using namespace core; +using namespace video; +using namespace system; +using namespace asset; +using namespace hlsl; + +namespace nbl::ext::frustum +{ + + core::smart_refctd_ptr CDrawFrustum::create(SCreationParameters&& params) + { + auto* const logger = params.utilities->getLogger(); + + if (!params.validate()) + { + logger->log("Failed creation parameters validation!", ILogger::ELL_ERROR); + return nullptr; + } + + ConstructorParams constructorParams; + + if (params.drawMode & DM_SINGLE) + { + auto pipelineLayout = params.singlePipelineLayout; + if (!pipelineLayout) + pipelineLayout = createDefaultPipelineLayout(params.utilities->getLogicalDevice(), DM_SINGLE); + constructorParams.singlePipeline = createPipeline(params, pipelineLayout.get(), DM_SINGLE); + if (!constructorParams.singlePipeline) + { + logger->log("Failed to create pipeline!", ILogger::ELL_ERROR); + return nullptr; + } + } + + if (params.drawMode & DM_BATCH) + { + auto pipelineLayout = params.batchPipelineLayout; + if (!pipelineLayout) + pipelineLayout = createDefaultPipelineLayout(params.utilities->getLogicalDevice(), DM_BATCH); + constructorParams.batchPipeline = createPipeline(params, pipelineLayout.get(), DM_BATCH); + if (!constructorParams.batchPipeline) + { + logger->log("Failed to create pipeline!", ILogger::ELL_ERROR); + return nullptr; + } + } + + if (!createStreamingBuffer(params)) + { + logger->log("Failed to create streaming buffer!", ILogger::ELL_ERROR); + return nullptr; + } + + constructorParams.indicesBuffer = createIndicesBuffer(params); + if (!constructorParams.indicesBuffer) + { + logger->log("Failed to create indices buffer!", ILogger::ELL_ERROR); + return nullptr; + } + + constructorParams.creationParams = std::move(params); + return core::smart_refctd_ptr(new CDrawFrustum(std::move(constructorParams))); + } + + constexpr std::string_view NBL_EXT_MOUNT_ENTRY = "nbl/ext/Frustum"; + + const smart_refctd_ptr CDrawFrustum::mount(smart_refctd_ptr logger, ISystem* system, video::ILogicalDevice* device, const std::string_view archiveAlias) + { + assert(system); + + if (!system) + return nullptr; + + const auto composed = path(archiveAlias.data()) / nbl::ext::frustum::builtin::build::get_spirv_key<"draw_frustum">(device); + if (system->exists(composed, {})) + return nullptr; + +#ifdef NBL_EMBED_BUILTIN_RESOURCES + auto archive = make_smart_refctd_ptr(smart_refctd_ptr(logger)); +#else + auto archive = make_smart_refctd_ptr(std::string_view(NBL_FRUSTUM_HLSL_MOUNT_POINT), smart_refctd_ptr(logger), system); +#endif + + system->mount(smart_refctd_ptr(archive), archiveAlias.data()); + return smart_refctd_ptr(archive); + } + + smart_refctd_ptr CDrawFrustum::createPipeline(SCreationParameters& params, const IGPUPipelineLayout* pipelineLayout, DrawMode mode) + { + system::logger_opt_ptr logger = params.utilities->getLogger(); + auto system = smart_refctd_ptr(params.assetManager->getSystem()); + auto* device = params.utilities->getLogicalDevice(); + mount(smart_refctd_ptr(params.utilities->getLogger()), system.get(), params.utilities->getLogicalDevice(), NBL_EXT_MOUNT_ENTRY); + + auto getShader = [&](const core::string& key)->smart_refctd_ptr { + IAssetLoader::SAssetLoadParams lp = {}; + lp.logger = params.utilities->getLogger(); + lp.workingDirectory = NBL_EXT_MOUNT_ENTRY; + auto bundle = params.assetManager->getAsset(key.c_str(), lp); + + const auto contents = bundle.getContents(); + + if (contents.empty()) + { + logger.log("Failed to load shader %s from disk", ILogger::ELL_ERROR, key.c_str()); + return nullptr; + } + + if (bundle.getAssetType() != IAsset::ET_SHADER) + { + logger.log("Loaded asset has wrong type!", ILogger::ELL_ERROR); + return nullptr; + } + + return IAsset::castDown(contents[0]); + }; + + const auto key = nbl::ext::frustum::builtin::build::get_spirv_key<"draw_frustum">(device); + smart_refctd_ptr unifiedShader = getShader(key); + if (!unifiedShader) + { + params.utilities->getLogger()->log("Could not compile shaders!", ILogger::ELL_ERROR); + return nullptr; + } + + video::IGPUGraphicsPipeline::SCreationParams pipelineParams[1] = {}; + pipelineParams[0].layout = pipelineLayout; + pipelineParams[0].vertexShader = { .shader = unifiedShader.get(), .entryPoint = (mode & DM_SINGLE) ? "frustum_vertex_single" : "frustum_vertex_instances" }; + pipelineParams[0].fragmentShader = { .shader = unifiedShader.get(), .entryPoint = "frustum_fragment" }; + asset::SRasterizationParams rasterParams; + rasterParams.depthWriteEnable = true; + rasterParams.depthCompareOp = asset::ECO_GREATER; + rasterParams.faceCullingMode = asset::EFCM_NONE; + + pipelineParams[0].cached = { + .primitiveAssembly = { + .primitiveType = asset::E_PRIMITIVE_TOPOLOGY::EPT_LINE_LIST, + }, + .rasterization = rasterParams, + }; + pipelineParams[0].renderpass = params.renderpass.get(); + + smart_refctd_ptr pipeline; + params.utilities->getLogicalDevice()->createGraphicsPipelines(nullptr, pipelineParams, &pipeline); + if (!pipeline) + { + params.utilities->getLogger()->log("Could not create streaming pipeline!", ILogger::ELL_ERROR); + return nullptr; + } + + return pipeline; + } + + bool CDrawFrustum::createStreamingBuffer(SCreationParameters& params) + { + const uint32_t minStreamingBufferAllocationSize = 128u, maxStreamingBufferAllocationAlignment = 4096u, mdiBufferDefaultSize = /* 2MB */ 1024u * 1024u * 2u; + + auto getRequiredAccessFlags = [&](const bitflag& properties) + { + bitflag flags(IDeviceMemoryAllocation::EMCAF_NO_MAPPING_ACCESS); + + if (properties.hasFlags(IDeviceMemoryAllocation::EMPF_HOST_WRITABLE_BIT)) + flags |= IDeviceMemoryAllocation::EMCAF_WRITE; + + return flags; + }; + + if (!params.streamingBuffer) + { + IGPUBuffer::SCreationParams mdiCreationParams = {}; + mdiCreationParams.usage = SCachedCreationParameters::RequiredUsageFlags; + mdiCreationParams.size = mdiBufferDefaultSize; + + auto buffer = params.utilities->getLogicalDevice()->createBuffer(std::move(mdiCreationParams)); + buffer->setObjectDebugName("Frustum Streaming Buffer"); + + auto memoryReqs = buffer->getMemoryReqs(); + memoryReqs.memoryTypeBits &= params.utilities->getLogicalDevice()->getPhysicalDevice()->getUpStreamingMemoryTypeBits(); + + auto allocation = params.utilities->getLogicalDevice()->allocate(memoryReqs, buffer.get(), SCachedCreationParameters::RequiredAllocateFlags); + { + const bool allocated = allocation.isValid(); + assert(allocated); + } + auto memory = allocation.memory; + + if (!memory->map({ 0ull, memoryReqs.size }, getRequiredAccessFlags(memory->getMemoryPropertyFlags()))) + params.utilities->getLogger()->log("Could not map device memory!", ILogger::ELL_ERROR); + + params.streamingBuffer = make_smart_refctd_ptr(SBufferRange{0ull, mdiCreationParams.size, std::move(buffer)}, maxStreamingBufferAllocationAlignment, minStreamingBufferAllocationSize); + } + + auto buffer = params.streamingBuffer->getBuffer(); + auto binding = buffer->getBoundMemory(); + + const auto validation = std::to_array + ({ + std::make_pair(buffer->getCreationParams().usage.hasFlags(SCachedCreationParameters::RequiredUsageFlags), "Streaming buffer must be created with IBuffer::EUF_STORAGE_BUFFER_BIT | IBuffer::EUF_SHADER_DEVICE_ADDRESS_BIT enabled!"), + std::make_pair(binding.memory->getAllocateFlags().hasFlags(SCachedCreationParameters::RequiredAllocateFlags), "Streaming buffer's memory must be allocated with IDeviceMemoryAllocation::EMAF_DEVICE_ADDRESS_BIT enabled!"), + std::make_pair(binding.memory->isCurrentlyMapped(), "Streaming buffer's memory must be mapped!"), + std::make_pair(binding.memory->getCurrentMappingAccess().hasFlags(getRequiredAccessFlags(binding.memory->getMemoryPropertyFlags())), "Streaming buffer's memory current mapping access flags don't meet requirements!") + }); + + for (const auto& [ok, error] : validation) + if (!ok) + { + params.utilities->getLogger()->log(error, ILogger::ELL_ERROR); + return false; + } + + return true; + } + + smart_refctd_ptr CDrawFrustum::createIndicesBuffer(SCreationParameters& params) + { + std::array unitFrustumIndices; + unitFrustumIndices[0] = 0; + unitFrustumIndices[1] = 1; + unitFrustumIndices[2] = 0; + unitFrustumIndices[3] = 2; + + unitFrustumIndices[4] = 3; + unitFrustumIndices[5] = 1; + unitFrustumIndices[6] = 3; + unitFrustumIndices[7] = 2; + + unitFrustumIndices[8] = 4; + unitFrustumIndices[9] = 5; + unitFrustumIndices[10] = 4; + unitFrustumIndices[11] = 6; + + unitFrustumIndices[12] = 7; + unitFrustumIndices[13] = 5; + unitFrustumIndices[14] = 7; + unitFrustumIndices[15] = 6; + + unitFrustumIndices[16] = 0; + unitFrustumIndices[17] = 4; + unitFrustumIndices[18] = 1; + unitFrustumIndices[19] = 5; + + unitFrustumIndices[20] = 2; + unitFrustumIndices[21] = 6; + unitFrustumIndices[22] = 3; + unitFrustumIndices[23] = 7; + + auto* device = params.utilities->getLogicalDevice(); + smart_refctd_ptr cmdbuf; + { + smart_refctd_ptr cmdpool = device->createCommandPool(params.transfer->getFamilyIndex(), IGPUCommandPool::CREATE_FLAGS::TRANSIENT_BIT); + if (!cmdpool->createCommandBuffers(IGPUCommandPool::BUFFER_LEVEL::PRIMARY, { &cmdbuf, 1 })) + { + params.utilities->getLogger()->log("Failed to create Command Buffer for index buffer!\n"); + return nullptr; + } + } + + IGPUBuffer::SCreationParams bufparams; + bufparams.size = sizeof(uint32_t) * unitFrustumIndices.size(); + bufparams.usage = IGPUBuffer::EUF_INDEX_BUFFER_BIT | IGPUBuffer::EUF_TRANSFER_DST_BIT | IGPUBuffer::EUF_INLINE_UPDATE_VIA_CMDBUF; + + smart_refctd_ptr indicesBuffer; + { + indicesBuffer = device->createBuffer(std::move(bufparams)); + if (!indicesBuffer) + { + params.utilities->getLogger()->log("Failed to create index buffer!\n"); + return nullptr; + } + + video::IDeviceMemoryBacked::SDeviceMemoryRequirements reqs = indicesBuffer->getMemoryReqs(); + reqs.memoryTypeBits &= device->getPhysicalDevice()->getDeviceLocalMemoryTypeBits(); + + auto bufMem = device->allocate(reqs, indicesBuffer.get()); + if (!bufMem.isValid()) + { + params.utilities->getLogger()->log("Failed to allocate device memory compatible with index buffer!\n"); + return nullptr; + } + } + + { + cmdbuf->begin(IGPUCommandBuffer::USAGE::ONE_TIME_SUBMIT_BIT); + cmdbuf->beginDebugMarker("Fill indices buffer begin"); + + SBufferRange bufRange = { .offset = 0, .size = indicesBuffer->getSize(), .buffer = indicesBuffer }; + cmdbuf->updateBuffer(bufRange, unitFrustumIndices.data()); + + cmdbuf->endDebugMarker(); + cmdbuf->end(); + } + + smart_refctd_ptr idxBufProgress; + constexpr auto FinishedValue = 25; + { + constexpr auto StartedValue = 0; + idxBufProgress = device->createSemaphore(StartedValue); + + IQueue::SSubmitInfo submitInfos[1] = {}; + const IQueue::SSubmitInfo::SCommandBufferInfo cmdbufs[] = { {.cmdbuf = cmdbuf.get()} }; + submitInfos[0].commandBuffers = cmdbufs; + const IQueue::SSubmitInfo::SSemaphoreInfo signals[] = { {.semaphore = idxBufProgress.get(),.value = FinishedValue,.stageMask = asset::PIPELINE_STAGE_FLAGS::ALL_TRANSFER_BITS} }; + submitInfos[0].signalSemaphores = signals; + + params.transfer->submit(submitInfos); + } + + const ISemaphore::SWaitInfo waitInfos[] = { { + .semaphore = idxBufProgress.get(), + .value = FinishedValue + } }; + device->blockForSemaphores(waitInfos); + + return indicesBuffer; + } + + core::smart_refctd_ptr CDrawFrustum::createPipelineLayoutFromPCRange(video::ILogicalDevice* device, const asset::SPushConstantRange& pcRange) + { + return device->createPipelineLayout({ &pcRange , 1 }, nullptr, nullptr, nullptr, nullptr); + } + + core::smart_refctd_ptr CDrawFrustum::createDefaultPipelineLayout(video::ILogicalDevice* device, DrawMode mode) + { + const uint32_t offset = (mode & DM_BATCH) ? offsetof(ext::frustum::PushConstants, ipc) : offsetof(ext::frustum::PushConstants, spc); + const uint32_t pcSize = (mode & DM_BATCH) ? sizeof(SInstancedPC) : sizeof(SSinglePC); + SPushConstantRange pcRange = { + .stageFlags = IShader::E_SHADER_STAGE::ESS_VERTEX, + .offset = offset, + .size = pcSize + }; + return createPipelineLayoutFromPCRange(device, pcRange); + } + + bool CDrawFrustum::renderSingle(const DrawParameters& params, const hlsl::float32_t4x4& frustumTransform, const hlsl::float32_t4& color) + { + if (!(m_cachedCreationParams.drawMode & DM_SINGLE)) + { + m_cachedCreationParams.utilities->getLogger()->log("CDrawFrustum has not been enabled for draw single!", ILogger::ELL_ERROR); + return false; + } + + auto& commandBuffer = params.commandBuffer; + commandBuffer->bindGraphicsPipeline(m_singlePipeline.get()); + commandBuffer->setLineWidth(params.lineWidth); + asset::SBufferBinding indexBinding = { .offset = 0, .buffer = m_indicesBuffer }; + commandBuffer->bindIndexBuffer(indexBinding, asset::EIT_32BIT); + + SSinglePC pc; + pc.instance.transform = hlsl::mul(params.viewProjectionMatrix, frustumTransform); + pc.instance.color = color; + + commandBuffer->pushConstants(m_singlePipeline->getLayout(), ESS_VERTEX, offsetof(ext::frustum::PushConstants, spc), sizeof(SSinglePC), &pc); + commandBuffer->drawIndexed(IndicesCount, 1, 0, 0, 0); + + return true; + } + + bool CDrawFrustum::render(const DrawParameters& params, video::ISemaphore::SWaitInfo waitInfo, std::span frustumInstances) + { + system::logger_opt_ptr logger = m_cachedCreationParams.utilities->getLogger(); + if (!(m_cachedCreationParams.drawMode & DM_BATCH)) + { + logger.log("CDrawFrustum has not been enabled for draw batches!", system::ILogger::ELL_ERROR); + return false; + } + + using offset_t = SCachedCreationParameters::streaming_buffer_t::size_type; + constexpr offset_t MaxAlignment = sizeof(InstanceData); + const auto MaxPOTAlignment = hlsl::roundUpToPoT(MaxAlignment); + auto* streaming = m_cachedCreationParams.streamingBuffer.get(); + if (streaming->getAddressAllocator().max_alignment() < MaxPOTAlignment) + { + logger.log("Draw Frustum Streaming Buffer cannot guarantee the alignments we require!"); + return false; + } + + auto* const streamingPtr = reinterpret_cast(streaming->getBufferPointer()); + assert(streamingPtr); + + auto& commandBuffer = params.commandBuffer; + commandBuffer->bindGraphicsPipeline(m_batchPipeline.get()); + commandBuffer->setLineWidth(params.lineWidth); + asset::SBufferBinding indexBinding = { .offset = 0, .buffer = m_indicesBuffer }; + commandBuffer->bindIndexBuffer(indexBinding, asset::EIT_32BIT); + + auto srcIt = frustumInstances.begin(); + auto setInstancesRange = [&](InstanceData* data, uint32_t count) -> void { + for (uint32_t i = 0; i < count; i++) + { + auto inst = data + i; + *inst = *srcIt; + inst->transform = hlsl::mul(params.viewProjectionMatrix, inst->transform); + srcIt++; + + if (srcIt == frustumInstances.end()) + break; + } + }; + + const uint32_t numInstances = frustumInstances.size(); + uint32_t remainingInstancesBytes = numInstances * sizeof(InstanceData); + while (srcIt != frustumInstances.end()) + { + uint32_t blockByteSize = core::alignUp(remainingInstancesBytes, MaxAlignment); + bool allocated = false; + + offset_t blockOffset = SCachedCreationParameters::streaming_buffer_t::invalid_value; + const uint32_t smallestAlloc = hlsl::max(core::alignUp(sizeof(InstanceData), MaxAlignment), streaming->getAddressAllocator().min_size()); + while (blockByteSize >= smallestAlloc) + { + std::chrono::steady_clock::time_point waitTill = std::chrono::steady_clock::now() + std::chrono::milliseconds(1u); + if (streaming->multi_allocate(waitTill, 1, &blockOffset, &blockByteSize, &MaxAlignment) == 0u) + { + allocated = true; + break; + } + + streaming->cull_frees(); + blockByteSize >>= 1; + } + + if (!allocated) + { + logger.log("Failed to allocate a chunk from streaming buffer for the next drawcall batch.", system::ILogger::ELL_ERROR); + return false; + } + + const uint32_t instanceCount = blockByteSize / sizeof(InstanceData); + auto* const streamingInstancesPtr = reinterpret_cast(streamingPtr + blockOffset); + setInstancesRange(streamingInstancesPtr, instanceCount); + + if (streaming->needsManualFlushOrInvalidate()) + { + const video::ILogicalDevice::MappedMemoryRange flushRange(streaming->getBuffer()->getBoundMemory().memory, blockOffset, blockByteSize); + m_cachedCreationParams.utilities->getLogicalDevice()->flushMappedMemoryRanges(1, &flushRange); + } + + remainingInstancesBytes -= instanceCount * sizeof(InstanceData); + + SInstancedPC pc; + pc.pInstanceBuffer = m_cachedCreationParams.streamingBuffer->getBuffer()->getDeviceAddress() + blockOffset; + + commandBuffer->pushConstants(m_batchPipeline->getLayout(), asset::IShader::E_SHADER_STAGE::ESS_VERTEX, offsetof(ext::frustum::PushConstants, ipc), sizeof(SInstancedPC), &pc); + commandBuffer->drawIndexed(IndicesCount, instanceCount, 0, 0, 0); + + streaming->multi_deallocate(1, &blockOffset, &blockByteSize, waitInfo); + } + + return true; + } + +} diff --git a/src/nbl/ext/frustum/CMakeLists.txt b/src/nbl/ext/frustum/CMakeLists.txt new file mode 100644 index 0000000000..3da53c0002 --- /dev/null +++ b/src/nbl/ext/frustum/CMakeLists.txt @@ -0,0 +1,75 @@ +# Copyright (C) 2018-2026 - DevSH Graphics Programming Sp. z O.O. +# This file is part of the "Nabla Engine". +# For conditions of distribution and use, see copyright notice in nabla.h + +include(${NBL_ROOT_PATH}/cmake/common.cmake) + +set(NBL_EXT_INTERNAL_INCLUDE_DIR "${NBL_ROOT_PATH}/include") + +set(NBL_EXT_FRUSTUM_H + ${NBL_EXT_INTERNAL_INCLUDE_DIR}/nbl/ext/Frustum/CDrawFrustum.h +) + +set(NBL_EXT_FRUSTUM_SRC + "${CMAKE_CURRENT_SOURCE_DIR}/CDrawFrustum.cpp" +) + +nbl_create_ext_library_project( + FRUSTUM + "${NBL_EXT_FRUSTUM_H}" + "${NBL_EXT_FRUSTUM_SRC}" + "${NBL_EXT_FRUSTUM_EXTERNAL_INCLUDE}" + "" + "" +) + +get_filename_component(_ARCHIVE_ABSOLUTE_ENTRY_PATH_ "${NBL_EXT_INTERNAL_INCLUDE_DIR}" ABSOLUTE) + +set(NBL_FRUSTUM_HLSL_MOUNT_POINT "${_ARCHIVE_ABSOLUTE_ENTRY_PATH_}/nbl/ext/Frustum/builtin/hlsl") +set(OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/auto-gen") +set(DEPENDS + ${NBL_FRUSTUM_HLSL_MOUNT_POINT}/common.hlsl + ${NBL_FRUSTUM_HLSL_MOUNT_POINT}/draw_frustum.unified.hlsl +) +target_sources(${LIB_NAME} PRIVATE ${DEPENDS}) +set_source_files_properties(${DEPENDS} PROPERTIES HEADER_FILE_ONLY ON) + +set(SM 6_8) +set(JSON [=[ +[ + { + "INPUT": "${NBL_FRUSTUM_HLSL_MOUNT_POINT}/draw_frustum.unified.hlsl", + "KEY": "draw_frustum", + } +] +]=]) +string(CONFIGURE "${JSON}" JSON) + +set(COMPILE_OPTIONS + -I "${NBL_ROOT_PATH}/include" # workaround for frustum ext common header which is not part of Nabla builtin archive + -I "${CMAKE_CURRENT_SOURCE_DIR}" + -T lib_${SM} +) + +NBL_CREATE_NSC_COMPILE_RULES( + TARGET ${LIB_NAME}SPIRV + LINK_TO ${LIB_NAME} + DEPENDS ${DEPENDS} + BINARY_DIR ${OUTPUT_DIRECTORY} + MOUNT_POINT_DEFINE NBL_FRUSTUM_HLSL_MOUNT_POINT + COMMON_OPTIONS ${COMPILE_OPTIONS} + OUTPUT_VAR KEYS + INCLUDE nbl/ext/Frustum/builtin/build/spirv/keys.hpp + NAMESPACE nbl::ext::frustum::builtin::build + INPUTS ${JSON} +) + +NBL_CREATE_RESOURCE_ARCHIVE( + NAMESPACE nbl::ext::frustum::builtin::build + TARGET ${LIB_NAME}_builtinsBuild + LINK_TO ${LIB_NAME} + BIND ${OUTPUT_DIRECTORY} + BUILTINS ${KEYS} +) + +add_library(Nabla::ext::Frustum ALIAS ${LIB_NAME}) From 11085840922bdb1cd4cdd0a392d4ebe556a49f0b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 18 Feb 2026 20:53:00 +0100 Subject: [PATCH 063/161] Update examples tests continuity frustum script --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 1fede0b5f6..3269cfad62 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 1fede0b5f67f11fb042b0d6e23f8d743bb517a45 +Subproject commit 3269cfad628ebd7341629d179495f833c6cf6a7a From 848aabee8e999a7e9c8038bf84bc616906fc13db Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 19 Feb 2026 21:27:47 +0100 Subject: [PATCH 064/161] Update camera test submodule revision --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index f6a5a151d3..713984c2ac 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit f6a5a151d3ae4b73bddcfc1e6a510a7501f42527 +Subproject commit 713984c2ac5a1f8194a96078e414213a343ce6b4 From 7d97b8b21e1902720ee98bff9713bfa10688d2c3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Thu, 19 Feb 2026 21:52:22 +0100 Subject: [PATCH 065/161] Update examples_tests submodule revision --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 713984c2ac..4a699bc82f 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 713984c2ac5a1f8194a96078e414213a343ce6b4 +Subproject commit 4a699bc82f1a59105d90de18b576deb5121d3d8a From cadefbb6eacddabfd5fe0a40712d9ab70cc3c83f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 15:21:31 +0200 Subject: [PATCH 066/161] Update examples_tests for cameraz smoke fixes --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 4a699bc82f..7530d25f44 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 4a699bc82f1a59105d90de18b576deb5121d3d8a +Subproject commit 7530d25f4465248737ebe85ddcd82267ac03587f From d97f18d7c80afafe1c18445233fea3540c16313e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 15:34:14 +0200 Subject: [PATCH 067/161] Update examples_tests after master merge --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 7530d25f44..63cea4d374 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 7530d25f4465248737ebe85ddcd82267ac03587f +Subproject commit 63cea4d3743f9290301121216729600b964c483e From 3c6fd0e1933bf060d754992360278face6355ed1 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 16:35:09 +0200 Subject: [PATCH 068/161] Update examples_tests after json alias fix --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 63cea4d374..98f664c844 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 63cea4d3743f9290301121216729600b964c483e +Subproject commit 98f664c844c932b66c2ca570b94710d57753702e From 530883e71ab87cb2488c4c82f9144eac93ac9305 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 19:26:19 +0200 Subject: [PATCH 069/161] Update examples_tests checkpoint --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 98f664c844..880415c423 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 98f664c844c932b66c2ca570b94710d57753702e +Subproject commit 880415c4237f8c5d19e1b9b350731013572d62b5 From abbcfe7a3495269a6afd1ad2733b2e86ef7c0552 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 19:53:46 +0200 Subject: [PATCH 070/161] Update examples_tests after goal solver rename --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 880415c423..e0f93216ad 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 880415c4237f8c5d19e1b9b350731013572d62b5 +Subproject commit e0f93216ad15d6f759b97b7520e07c694ac46dd4 From 38d92c229253f447ccebbe07a62771a09aca5ef6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 3 Apr 2026 20:19:56 +0200 Subject: [PATCH 071/161] Update examples_tests after camera goal cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index e0f93216ad..9194109420 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit e0f93216ad15d6f759b97b7520e07c694ac46dd4 +Subproject commit 9194109420b77766406627ee79e142bbc6bac082 From e7b8b4f568245aa59345e842ad03a84b328dbad6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 09:14:11 +0200 Subject: [PATCH 072/161] Update examples_tests after camera goal solver changes --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 9194109420..931648f20d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 9194109420b77766406627ee79e142bbc6bac082 +Subproject commit 931648f20d1db7ce142680a40578cd2d5443e84f From 5cae47eb46409b605fabb3e246a1fb5d8234cb84 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 12:13:16 +0200 Subject: [PATCH 073/161] Update examples_tests after preset compatibility changes --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 931648f20d..bf0d991a85 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 931648f20d1db7ce142680a40578cd2d5443e84f +Subproject commit bf0d991a85cb41afb034e4a0c5059578c0e1da4c From 23fa6b66eb0e38e8fa0586312535c63f0b58019a Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:10:48 +0200 Subject: [PATCH 074/161] Update examples_tests after preset policy changes --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index bf0d991a85..dcaeef4411 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit bf0d991a85cb41afb034e4a0c5059578c0e1da4c +Subproject commit dcaeef441149bd80e5e902f4370fcd71cfd6e776 From 0fb9e902b8ba1d7c9170573def06646950e23d6f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:26:03 +0200 Subject: [PATCH 075/161] Update examples_tests after Chase and Dolly preset flow checks --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index dcaeef4411..16c7ba835d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit dcaeef441149bd80e5e902f4370fcd71cfd6e776 +Subproject commit 16c7ba835d06bdfd67472499ba4b4af0e024c99c From dac6e9723a1c820fe30fb92451316a03c716dfad Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:35:59 +0200 Subject: [PATCH 076/161] Update examples_tests after planar binding cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 16c7ba835d..fb653e7ac7 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 16c7ba835d06bdfd67472499ba4b4af0e024c99c +Subproject commit fb653e7ac7f095ef6301592d588b8e696a8bf017 From 53f960ad83ca49c51a7a3cc9fd58640b31eba55c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:41:02 +0200 Subject: [PATCH 077/161] Update examples_tests after camera contract cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index fb653e7ac7..012220dfd7 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit fb653e7ac7f095ef6301592d588b8e696a8bf017 +Subproject commit 012220dfd7b1dbbc34200105f490828b23d3a56d From 944f5cb53879a73a0cfa36de104dd6ceb895b060 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:44:48 +0200 Subject: [PATCH 078/161] Update examples_tests after input binding helper cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 012220dfd7..064db7b623 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 012220dfd7b1dbbc34200105f490828b23d3a56d +Subproject commit 064db7b623834c90af7ec67b67572e011c24dbbf From 630b7bdda747cb9c73c5fe2669637cca5d30548a Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:47:28 +0200 Subject: [PATCH 079/161] Update examples_tests after transform editor cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 064db7b623..47af5b0e41 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 064db7b623834c90af7ec67b67572e011c24dbbf +Subproject commit 47af5b0e415c7520a3a8881f529631095cb74d03 From 760acaa0eff7ef91930cea9b21f9d10726beb0b6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 13:52:29 +0200 Subject: [PATCH 080/161] Update examples_tests after binding layout rename --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 47af5b0e41..286667eeb3 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 47af5b0e415c7520a3a8881f529631095cb74d03 +Subproject commit 286667eeb3756fc41859896a6b6dedf307c93620 From 7eb2a91ea95403dd1e81725fa3121d30e508fac1 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:00:54 +0200 Subject: [PATCH 081/161] Update examples_tests after binding storage split --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 286667eeb3..c50e9fe9bb 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 286667eeb3756fc41859896a6b6dedf307c93620 +Subproject commit c50e9fe9bb8c151462c684265d0ddbb82c2cd38b From 0b50aea8ab49add11d55e11a3f932c8ab2ec5246 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:08:32 +0200 Subject: [PATCH 082/161] Update examples_tests after binding layout header split --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c50e9fe9bb..310d79309e 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c50e9fe9bb8c151462c684265d0ddbb82c2cd38b +Subproject commit 310d79309e8bab537cd51d2a85858814168cb255 From 5de70ac18f34106cc3dcf5f6779bc7a2b2832a33 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:12:39 +0200 Subject: [PATCH 083/161] Update examples_tests after JSON binding rename --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 310d79309e..ea09ece252 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 310d79309e8bab537cd51d2a85858814168cb255 +Subproject commit ea09ece252609bcdd88fa2ae3ad69c3cdc8c96c3 From 181714630ea0093c2865545274f3371e06d795a1 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:18:02 +0200 Subject: [PATCH 084/161] Update examples_tests after input processor rename --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index ea09ece252..54b75a5245 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit ea09ece252609bcdd88fa2ae3ad69c3cdc8c96c3 +Subproject commit 54b75a524508e608b1f34447e9d4c1041da8fd40 From 405b4345412c20bbeb092278f655f47aca0af923 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:24:22 +0200 Subject: [PATCH 085/161] Update examples_tests after camera rig setup cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 54b75a5245..5fb8acb60b 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 54b75a524508e608b1f34447e9d4c1041da8fd40 +Subproject commit 5fb8acb60bd786601db6431a2f61b7d48faec384 From 37b707597e2c8edfdab4e19bf98aad20aefe4266 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:26:58 +0200 Subject: [PATCH 086/161] Update examples_tests after rig config routing --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 5fb8acb60b..d82509f473 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 5fb8acb60bd786601db6431a2f61b7d48faec384 +Subproject commit d82509f4736f175b7a238486a1b39747bc2ca2c4 From 0314dbfd49af5c8ace43a7ecd773a5f2b868b33e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:29:57 +0200 Subject: [PATCH 087/161] Update examples_tests after rig preset helper cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index d82509f473..419a58faaf 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d82509f4736f175b7a238486a1b39747bc2ca2c4 +Subproject commit 419a58faaf5af88badf0d0f7ed84e9983bad51c8 From 4a244ca505584d964379cc5fafc2dbdbf91be744 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:46:55 +0200 Subject: [PATCH 088/161] Update examples_tests after camera binding layout decoupling --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 419a58faaf..5c5c8fdcfd 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 419a58faaf5af88badf0d0f7ed84e9983bad51c8 +Subproject commit 5c5c8fdcfd913992f8a0daf5e4a7a91980cc85b4 From f12b5b6681823e8398b8a155899be9d32062669b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:54:43 +0200 Subject: [PATCH 089/161] Update examples_tests after binding naming cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 5c5c8fdcfd..9bc2da8594 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 5c5c8fdcfd913992f8a0daf5e4a7a91980cc85b4 +Subproject commit 9bc2da85949f218cf7321b05c4b614ed5049a6b5 From 3393bbd6a34f806520408d995a6716c2922b2538 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 14:57:31 +0200 Subject: [PATCH 090/161] Update examples_tests after camera binding cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 9bc2da8594..757999edcc 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 9bc2da85949f218cf7321b05c4b614ed5049a6b5 +Subproject commit 757999edcc4477474a4273cf5fec67e3b2460d3b From fa953f18c66e77113baaef3055bc00e2dba276d7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:03:09 +0200 Subject: [PATCH 091/161] Update examples_tests after motion config split --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 757999edcc..f3e4107f66 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 757999edcc4477474a4273cf5fec67e3b2460d3b +Subproject commit f3e4107f66187cf4410e88af89966f6390651cb5 From 90b7c191037c83dabf03434ffee423e243a43318 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:08:01 +0200 Subject: [PATCH 092/161] Update examples_tests after scoped motion overrides --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index f3e4107f66..cb4c749ae5 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit f3e4107f66187cf4410e88af89966f6390651cb5 +Subproject commit cb4c749ae524b9e9b5984edbba7c129138eeb870 From 1b7a8d89c23c6b7bd0a132ba5e11f3bd6b8f8dcb Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:10:41 +0200 Subject: [PATCH 093/161] Update examples_tests after unit motion wrapper --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index cb4c749ae5..38a792da8e 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit cb4c749ae524b9e9b5984edbba7c129138eeb870 +Subproject commit 38a792da8eb77a6721f7280a53c8e00384283728 From 9cd1fc83a9fb4828d6639b23a4a48ac63250b73f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:14:14 +0200 Subject: [PATCH 094/161] Update examples_tests after goal alias cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 38a792da8e..a93971bf5d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 38a792da8eb77a6721f7280a53c8e00384283728 +Subproject commit a93971bf5de1d6c5aaaa618c8ee599aa47a013eb From d8776fd29358b20345d2719cd7a09cb5c14c9dd9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:29:14 +0200 Subject: [PATCH 095/161] Update examples_tests after preset UI cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index a93971bf5d..6d8e3bd238 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit a93971bf5de1d6c5aaaa618c8ee599aa47a013eb +Subproject commit 6d8e3bd238c29b86783a3ddfa72b700bcb76e6a3 From 5add016980fcef79dea7b0e8f3a39c01fa8d7343 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:37:17 +0200 Subject: [PATCH 096/161] Update examples_tests after playback summary changes --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 6d8e3bd238..0481eca85d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 6d8e3bd238c29b86783a3ddfa72b700bcb76e6a3 +Subproject commit 0481eca85dcfe86bdbbed4d6a5c02f9fe437c6d2 From 70e2ce9e4992d5a0387d0ffe4f52125db9936e4e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:40:42 +0200 Subject: [PATCH 097/161] Update examples_tests after preset banner cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 0481eca85d..db147c0cd1 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 0481eca85dcfe86bdbbed4d6a5c02f9fe437c6d2 +Subproject commit db147c0cd166e6399661e6c5d8f37bb0443214f4 From e80ef0cc3704e49af7ed8f314004b68596b2a334 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 15:44:02 +0200 Subject: [PATCH 098/161] Update examples_tests after playback scrub preview --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index db147c0cd1..cadce4926f 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit db147c0cd166e6399661e6c5d8f37bb0443214f4 +Subproject commit cadce4926f55acd5ff1dd809222b5b038468ea0d From ee188ff03c51942021f750ef387aab22eb691096 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 16:06:39 +0200 Subject: [PATCH 099/161] Update examples_tests after capture gating --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index cadce4926f..23af7d385a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit cadce4926f55acd5ff1dd809222b5b038468ea0d +Subproject commit 23af7d385add8113fa2700e486b83f9f105821ee From da755796a983cfdfc78d798e07e57ee34a19f49e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 16:23:56 +0200 Subject: [PATCH 100/161] Update examples_tests after shared goal API cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 23af7d385a..49bf35360c 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 23af7d385add8113fa2700e486b83f9f105821ee +Subproject commit 49bf35360cb24025058adf808815f130b919fd2e From 0b3ec405267fe7ebde3d1155c939867c79e1efb7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 16:33:10 +0200 Subject: [PATCH 101/161] Update examples_tests after shared preset API cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 49bf35360c..f6f91d4584 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 49bf35360cb24025058adf808815f130b919fd2e +Subproject commit f6f91d4584ca9dca74fca21004e4d0cd9f0dd71f From 123405979286def8e905b9974318ed30e160d9e4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:04:20 +0200 Subject: [PATCH 102/161] Update examples_tests after playback keyframe authoring cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index f6f91d4584..5d4278db7a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit f6f91d4584ca9dca74fca21004e4d0cd9f0dd71f +Subproject commit 5d4278db7ae9c1537d3d551e93a079d819ea4d57 From f8219473a43721331b1f8c4a0c64b8b7560cf225 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:09:07 +0200 Subject: [PATCH 103/161] Update examples_tests after keyframe storage changes --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 5d4278db7a..220796ec1d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 5d4278db7ae9c1537d3d551e93a079d819ea4d57 +Subproject commit 220796ec1df02cf7d6b74699f67c4644364c64ef From 0bab32055b66824e1dade553a41077d21ca383b4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:21:44 +0200 Subject: [PATCH 104/161] Update examples_tests after shared analysis cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 220796ec1d..635dc7a0b2 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 220796ec1df02cf7d6b74699f67c4644364c64ef +Subproject commit 635dc7a0b23a8125055aa7620c0f2cad267b1ccb From 7f78cc21a308c3358847f7ac0a73420faa86c2bc Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:32:38 +0200 Subject: [PATCH 105/161] Update examples_tests after shared keyframe track cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 635dc7a0b2..9090b59d25 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 635dc7a0b23a8125055aa7620c0f2cad267b1ccb +Subproject commit 9090b59d259f839d953abfc641240037e26d1d71 From 9a32427cb5383e8dab915d32565cca4ccc201312 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:37:08 +0200 Subject: [PATCH 106/161] Update examples_tests after camera docs --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 9090b59d25..920d640b6d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 9090b59d259f839d953abfc641240037e26d1d71 +Subproject commit 920d640b6de5ac1746e625da6483a4ca6862e176 From 1ac6209431dfb13bf7a49b2995452a940adae486 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:38:52 +0200 Subject: [PATCH 107/161] Update examples_tests after header docs --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 920d640b6d..c218197989 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 920d640b6de5ac1746e625da6483a4ca6862e176 +Subproject commit c218197989b5946abee720bda3b2de27f83860a0 From bfae93ad13d9ecd793864427f4f9dda92e0f92d9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 17:59:09 +0200 Subject: [PATCH 108/161] Update examples_tests after playback timeline cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c218197989..b639fdc82e 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c218197989b5946abee720bda3b2de27f83860a0 +Subproject commit b639fdc82e47a654749aa4f50dc8ec50dfc184a1 From c6fa314c24bfd3673d79e0a1d5e5ef4266a33751 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:06:47 +0200 Subject: [PATCH 109/161] Update examples_tests after camera persistence cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index b639fdc82e..0e018ef76b 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit b639fdc82e47a654749aa4f50dc8ec50dfc184a1 +Subproject commit 0e018ef76b714c1cef5fbdc454677397e1ed4114 From fd191f166491e173ea0ccbd9ec2c7ca2a56347d7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:11:37 +0200 Subject: [PATCH 110/161] Update examples_tests after preset flow cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 0e018ef76b..824feed11f 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 0e018ef76b714c1cef5fbdc454677397e1ed4114 +Subproject commit 824feed11f28b02ee7764a950e78d41e7c7f3b52 From 19ebf8b2b0d2bc6ddaab734979c64c1a68e2c866 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:16:38 +0200 Subject: [PATCH 111/161] Update examples_tests after preset apply summary cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 824feed11f..45cfa1a9af 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 824feed11f28b02ee7764a950e78d41e7c7f3b52 +Subproject commit 45cfa1a9afbebcd7f5a551b22a86c2c3f98a7df4 From 11d934718061c51571b47956c1bffc751ea7901e Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:34:08 +0200 Subject: [PATCH 112/161] Update examples_tests after camera helper smoke hardening --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 45cfa1a9af..42f753fd7a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 45cfa1a9afbebcd7f5a551b22a86c2c3f98a7df4 +Subproject commit 42f753fd7ab8b84cd5c1eb516824af1038d35deb From 9cfdb18d39d642a2b8cd7bcd303042f82299bc27 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 18:37:57 +0200 Subject: [PATCH 113/161] Update examples_tests after camera helper polish --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 42f753fd7a..2084822756 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 42f753fd7ab8b84cd5c1eb516824af1038d35deb +Subproject commit 2084822756fb94932a25c542eb8ac2d9ed78cd9f From ca1d35b17b64bda9e9d992609b1a337eb9d4ab94 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:05:16 +0200 Subject: [PATCH 114/161] Update examples_tests after camera manipulation cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 2084822756..09bb36e6ed 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 2084822756fb94932a25c542eb8ac2d9ed78cd9f +Subproject commit 09bb36e6ed8a050f6bacc117141a72d1d44cdf15 From eada28c95fbeaa9404113b653bc9f521d0b2ad70 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:10:47 +0200 Subject: [PATCH 115/161] Update examples_tests after camera text cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 09bb36e6ed..6a29a7cfb8 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 09bb36e6ed8a050f6bacc117141a72d1d44cdf15 +Subproject commit 6a29a7cfb8d2ab3b2881c8ad164f6f9159f4ebbd From 2bf45901ae9ed324ad416571efe4a41ad8196d29 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:36:14 +0200 Subject: [PATCH 116/161] Update examples_tests after camera projection cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 6a29a7cfb8..4016ac4ad0 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 6a29a7cfb8d2ab3b2881c8ad164f6f9159f4ebbd +Subproject commit 4016ac4ad0e9ad8acb4654aadd4fad814199c645 From 6289e65a1b6bc10d1de8aef7682dfe8defc72c4f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 19:40:14 +0200 Subject: [PATCH 117/161] Update examples_tests after analysis text cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 4016ac4ad0..7f5c50075a 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 4016ac4ad0e9ad8acb4654aadd4fad814199c645 +Subproject commit 7f5c50075a09b303fe1b2985aa167aec1d8e5401 From 6c0d131563414f1e27a1a0a1a129b838c3b7a62f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:03:47 +0200 Subject: [PATCH 118/161] Update examples_tests after helper smoke hardening --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 7f5c50075a..9bf4a0e906 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 7f5c50075a09b303fe1b2985aa167aec1d8e5401 +Subproject commit 9bf4a0e906c9faf1b3b218dca314718c2561c302 From 777117d53c99e910b82070f4fe52f5256d248741 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:10:15 +0200 Subject: [PATCH 119/161] Update examples_tests after presentation cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 9bf4a0e906..c5c6bdb1ab 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 9bf4a0e906c9faf1b3b218dca314718c2561c302 +Subproject commit c5c6bdb1abcf3df91464bb024d8ba6be29a2264e From b88598a14f580f4cb8d8f22b5dcd3bfdf080475d Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:17:10 +0200 Subject: [PATCH 120/161] Update examples_tests after presentation badge cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c5c6bdb1ab..c698e0348c 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c5c6bdb1abcf3df91464bb024d8ba6be29a2264e +Subproject commit c698e0348c231e94e537ddf62087f028fde59b56 From ea56b5348b99220a02932376420bddf9f76b162f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:23:25 +0200 Subject: [PATCH 121/161] Update examples_tests after preset comparison cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index c698e0348c..89790c2af6 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit c698e0348c231e94e537ddf62087f028fde59b56 +Subproject commit 89790c2af67f1afe34caa67b3ae7fabb5f34ccd9 From e21be7eb3682fd61b50b958f3e276d91379494b8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:27:30 +0200 Subject: [PATCH 122/161] Update examples_tests after presentation label cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 89790c2af6..95ab5d660e 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 89790c2af67f1afe34caa67b3ae7fabb5f34ccd9 +Subproject commit 95ab5d660ed54aaa1ee504dc195197534ef9b204 From b52527ee04d3eeac1d59a328b70d5dc2c085be26 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 4 Apr 2026 20:30:51 +0200 Subject: [PATCH 123/161] Update examples_tests after preset collection cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 95ab5d660e..6b414f5581 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 95ab5d660ed54aaa1ee504dc195197534ef9b204 +Subproject commit 6b414f5581acb53456b714ef3530c6135130f98b From 36f266821eae3030db5350698cbb745de63ecd71 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 15:30:12 +0200 Subject: [PATCH 124/161] Update examples_tests after continuity scripting cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 6b414f5581..ed0562f2bc 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 6b414f5581acb53456b714ef3530c6135130f98b +Subproject commit ed0562f2bc1d4e023a10ca8af505bf41f54a03e7 From 2cb9950b12c691029891869b061312e1d350f2e0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 15:50:04 +0200 Subject: [PATCH 125/161] Update examples_tests after follow integration --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index ed0562f2bc..10aaab8e47 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit ed0562f2bc1d4e023a10ca8af505bf41f54a03e7 +Subproject commit 10aaab8e47e4b22081d574901408de6925c73f15 From c257396be8aa141e8897421d1487163f5b98b9ba Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 18:04:24 +0200 Subject: [PATCH 126/161] Update examples_tests after follow integration --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 10aaab8e47..fadc116041 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 10aaab8e47e4b22081d574901408de6925c73f15 +Subproject commit fadc1160415d7caf9b6752ce37de1e8292f1275e From 7a7172fad4d00dc3f22b46e794a9950af1b6c25f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Mon, 6 Apr 2026 19:01:02 +0200 Subject: [PATCH 127/161] Update examples_tests after sequence policy cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index fadc116041..18668aa2dd 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit fadc1160415d7caf9b6752ce37de1e8292f1275e +Subproject commit 18668aa2dd80f8abf768985f5695fcdef54b1631 From d2fe6b175ac36f9d3190e8aa5ecbffae778b3aa5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 07:02:16 +0200 Subject: [PATCH 128/161] Update examples_tests after scripted runtime cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 18668aa2dd..79c2fa11ce 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 18668aa2dd80f8abf768985f5695fcdef54b1631 +Subproject commit 79c2fa11ce8b8719acb0d75aec159d2504cb931a From 016c2bf5fa747495613fc2f21e6c6724da10582d Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 07:04:48 +0200 Subject: [PATCH 129/161] Update examples_tests after camera docs rewrite --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 79c2fa11ce..74cb9093e1 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 79c2fa11ce8b8719acb0d75aec159d2504cb931a +Subproject commit 74cb9093e19e3da8da6198548b8553943c94f5a9 From f783968afbb0ce35205caaa390e5f20ae15b1a71 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 07:26:24 +0200 Subject: [PATCH 130/161] Update examples_tests after camera API cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 74cb9093e1..9cd8a4fb93 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 74cb9093e19e3da8da6198548b8553943c94f5a9 +Subproject commit 9cd8a4fb9324f25b10021995aac76c24ddcf78d2 From c9373156a053ece7ff0b23579f3a27081a8ee103 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 12:38:31 +0200 Subject: [PATCH 131/161] Update examples_tests after camera cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 9cd8a4fb93..1747f30f04 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 9cd8a4fb9324f25b10021995aac76c24ddcf78d2 +Subproject commit 1747f30f04fb20b13d8e091f2dd4fda4e797da10 From 24fc3fb5273073f01a0348e72f9fdc442fb86bb3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 12:59:04 +0200 Subject: [PATCH 132/161] Update examples_tests after camera contract cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 1747f30f04..3d3c7c2ae8 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 1747f30f04fb20b13d8e091f2dd4fda4e797da10 +Subproject commit 3d3c7c2ae8982052ca73d6b7d1d12d95d82431e4 From 3cac4700363e85da072fcbfc3c78acb3dcecca5c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:11:05 +0200 Subject: [PATCH 133/161] Update examples_tests after ui presentation cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 3d3c7c2ae8..34ff741300 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 3d3c7c2ae8982052ca73d6b7d1d12d95d82431e4 +Subproject commit 34ff74130084d52e99f1d43083f036227bc33d9a From 885e330e36aa9bae6bd5cf34d5a6380ace4d584f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:16:41 +0200 Subject: [PATCH 134/161] Update examples_tests after follow label cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 34ff741300..42fc8a6c7b 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 34ff74130084d52e99f1d43083f036227bc33d9a +Subproject commit 42fc8a6c7bf583f5e5528066c138a6ad161f544a From d74490fd6e8cb881c8488d66bcef7841dadd4fee Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:24:56 +0200 Subject: [PATCH 135/161] Update examples_tests after system follow cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 42fc8a6c7b..fca07444aa 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 42fc8a6c7bf583f5e5528066c138a6ad161f544a +Subproject commit fca07444aa2159cd759f491050bb2859ad40ec57 From f56a8fb2fef1d687100aec325fdaca9705fe95ad Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:44:12 +0200 Subject: [PATCH 136/161] Update examples_tests after hlsl cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index fca07444aa..e80a113290 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit fca07444aa2159cd759f491050bb2859ad40ec57 +Subproject commit e80a113290e0b08cc9641e233fbb4409f02af588 From 0068d0990d11cab0dbbe742b65dc1126ee1b0dbf Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:47:37 +0200 Subject: [PATCH 137/161] Update examples_tests after docs cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index e80a113290..d9f6f3db26 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit e80a113290e0b08cc9641e233fbb4409f02af588 +Subproject commit d9f6f3db26cac5ceec386833c27a9dd453f80e7b From 52548300c2035922a8ec457c509b60093decd49b Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 13:51:04 +0200 Subject: [PATCH 138/161] Update examples_tests after header docs --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index d9f6f3db26..537978bac4 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d9f6f3db26cac5ceec386833c27a9dd453f80e7b +Subproject commit 537978bac41a6caac16bc6bac5d228c91abb2fe3 From 8c2b795ed009ab498bf98045dc2d08b4430645d7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Tue, 7 Apr 2026 14:16:08 +0200 Subject: [PATCH 139/161] Polish hlsl transform utility cleanup --- .../transformation_matrix_utils.hlsl | 101 +++--------------- include/nbl/core/math/glslFunctions.h | 2 - 2 files changed, 14 insertions(+), 89 deletions(-) diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl index 918cdc7e60..f4668769be 100644 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl @@ -3,30 +3,30 @@ #include #include +#include namespace nbl { namespace hlsl { -// TODO: -> move somewhere else and nbl:: to implement it +//! Return true when the three basis vectors form an orthonormal basis within `epsilon`. template bool isOrthoBase(const T& x, const T& y, const T& z, const E epsilon = 1e-6) { auto isNormalized = [](const auto& v, const auto& epsilon) -> bool { - return glm::epsilonEqual(glm::length(v), 1.0, epsilon); + return hlsl::abs(hlsl::length(v) - static_cast(1.0)) <= epsilon; }; auto isOrthogonal = [](const auto& a, const auto& b, const auto& epsilon) -> bool { - return glm::epsilonEqual(glm::dot(a, b), 0.0, epsilon); + return hlsl::abs(hlsl::dot(a, b)) <= epsilon; }; return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); } -// <- template matrix getMatrix3x4As4x4(const matrix& mat) @@ -34,7 +34,7 @@ matrix getMatrix3x4As4x4(const matrix& mat) matrix output; for (int i = 0; i < 3; ++i) output[i] = mat[i]; - output[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + output[3] = vector(T(0), T(0), T(0), T(1)); return output; } @@ -44,8 +44,8 @@ matrix getMatrix3x3As4x4(const matrix& mat) { matrix output; for (int i = 0; i < 3; ++i) - output[i] = float32_t4(mat[i], 1.0f); - output[3] = float32_t4(0.0f, 0.0f, 0.0f, 1.0f); + output[i] = vector(mat[i], T(1)); + output[3] = vector(T(0), T(0), T(0), T(1)); return output; } @@ -72,7 +72,6 @@ inline matrix getCastedMatrix(const matrix& in) return out; } -// TODO: remove //! multiplies matrices a and b, 3x4 matrices are treated as 4x4 matrices with 4th row set to (0, 0, 0 ,1) template inline matrix concatenateBFollowedByA(const matrix& a, const matrix& b) @@ -82,16 +81,14 @@ inline matrix concatenateBFollowedByA(const matrix& a, const m return matrix(mul(a4x4, b4x4)); } -// /Arek: glm:: for normalize till dot product is fixed (ambiguity with glm namespace + linker issues) - template inline matrix buildCameraLookAtMatrixLH( const vector& position, const vector& target, const vector& upVector) { - const vector zaxis = glm::normalize(target - position); - const vector xaxis = glm::normalize(hlsl::cross(upVector, zaxis)); + const vector zaxis = hlsl::normalize(target - position); + const vector xaxis = hlsl::normalize(hlsl::cross(upVector, zaxis)); const vector yaxis = hlsl::cross(zaxis, xaxis); matrix r; @@ -108,8 +105,8 @@ inline matrix buildCameraLookAtMatrixRH( const vector& target, const vector& upVector) { - const vector zaxis = glm::normalize(position - target); - const vector xaxis = glm::normalize(hlsl::cross(upVector, zaxis)); + const vector zaxis = hlsl::normalize(position - target); + const vector xaxis = hlsl::normalize(hlsl::cross(upVector, zaxis)); const vector yaxis = hlsl::cross(zaxis, xaxis); matrix r; @@ -120,11 +117,7 @@ inline matrix buildCameraLookAtMatrixRH( return r; } -// TODO: test, check if there is better implementation -// TODO: move quaternion to nbl::hlsl -// TODO: why NBL_REF_ARG(MatType) doesn't work????? - -//! Replaces curent rocation and scale by rotation represented by quaternion `quat`, leaves 4th row and 4th colum unchanged +//! Replace the current rotation and scale by `quat`, leaving translation unchanged. template inline void setRotation(matrix& outMat, NBL_CONST_REF_ARG(math::quaternion) quat) { @@ -138,6 +131,7 @@ inline void setRotation(matrix& outMat, NBL_CONST_REF_ARG(math::quatern outMat[2] = mat[2]; } +//! Replace the current translation, leaving the linear part unchanged. template inline void setTranslation(matrix& outMat, NBL_CONST_REF_ARG(vector) translation) { @@ -149,74 +143,7 @@ inline void setTranslation(matrix& outMat, NBL_CONST_REF_ARG(vector -inline matrix buildProjectionMatrixPerspectiveFovRH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) -{ - const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); - _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero - const float w = h / aspectRatio; - - _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero - - matrix m; - m[0] = vector(w, 0.f, 0.f, 0.f); - m[1] = vector(0.f, -h, 0.f, 0.f); - m[2] = vector(0.f, 0.f, -zFar / (zFar - zNear), -zNear * zFar / (zFar - zNear)); - m[3] = vector(0.f, 0.f, -1.f, 0.f); - - return m; -} -template -inline matrix buildProjectionMatrixPerspectiveFovLH(float fieldOfViewRadians, float aspectRatio, float zNear, float zFar) -{ - const float h = core::reciprocal(tanf(fieldOfViewRadians * 0.5f)); - _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero - const float w = h / aspectRatio; - - _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero - - matrix m; - m[0] = vector(w, 0.f, 0.f, 0.f); - m[1] = vector(0.f, -h, 0.f, 0.f); - m[2] = vector(0.f, 0.f, zFar / (zFar - zNear), -zNear * zFar / (zFar - zNear)); - m[3] = vector(0.f, 0.f, 1.f, 0.f); - - return m; -} - -template -inline matrix buildProjectionMatrixOrthoRH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) -{ - _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero - _NBL_DEBUG_BREAK_IF(heightOfViewVolume == 0.f); //division by zero - _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero - - matrix m; - m[0] = vector(2.f / widthOfViewVolume, 0.f, 0.f, 0.f); - m[1] = vector(0.f, -2.f / heightOfViewVolume, 0.f, 0.f); - m[2] = vector(0.f, 0.f, -1.f / (zFar - zNear), -zNear / (zFar - zNear)); - m[3] = vector(0.f, 0.f, 0.f, 1.f); - - return m; -} - -template -inline matrix buildProjectionMatrixOrthoLH(float widthOfViewVolume, float heightOfViewVolume, float zNear, float zFar) -{ - _NBL_DEBUG_BREAK_IF(widthOfViewVolume == 0.f); //division by zero - _NBL_DEBUG_BREAK_IF(heightOfViewVolume == 0.f); //division by zero - _NBL_DEBUG_BREAK_IF(zNear == zFar); //division by zero - - matrix m; - m[0] = vector(2.f / widthOfViewVolume, 0.f, 0.f, 0.f); - m[1] = vector(0.f, -2.f / heightOfViewVolume, 0.f, 0.f); - m[2] = vector(0.f, 0.f, 1.f / (zFar - zNear), -zNear / (zFar - zNear)); - m[3] = vector(0.f, 0.f, 0.f, 1.f); - - return m; -} - } } -#endif \ No newline at end of file +#endif diff --git a/include/nbl/core/math/glslFunctions.h b/include/nbl/core/math/glslFunctions.h index 1412be95d5..2bd17cd642 100644 --- a/include/nbl/core/math/glslFunctions.h +++ b/include/nbl/core/math/glslFunctions.h @@ -362,7 +362,6 @@ NBL_FORCE_INLINE vectorSIMDf cross(const vectorSIMDf& a, const vect template NBL_FORCE_INLINE T normalize(const T& v) { - // TODO: THIS CREATES AMGIGUITY WITH GLM:: NAMESPACE! auto d = dot(v, v); #ifdef __NBL_FAST_MATH return v * core::inversesqrt(d); @@ -372,7 +371,6 @@ NBL_FORCE_INLINE T normalize(const T& v) } // TODO : matrixCompMult, outerProduct, inverse -// Arek: old and to be killed (missing .tcc include?), no definition in Nabla causing linker errors template NBL_FORCE_INLINE T transpose(const T& m); template<> From 4182b2d5f7715ad9517f9b9366256d7de80d63cc Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 8 Apr 2026 07:19:06 +0200 Subject: [PATCH 140/161] Update examples_tests after camera cleanup --- examples_tests | 2 +- include/nbl/ui/KeyCodes.h | 293 +++++++++++++++++++------------------- 2 files changed, 149 insertions(+), 146 deletions(-) diff --git a/examples_tests b/examples_tests index 537978bac4..61c24c01d7 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 537978bac41a6caac16bc6bac5d228c91abb2fe3 +Subproject commit 61c24c01d7631d972b81d08289b8a8b94f96b565 diff --git a/include/nbl/ui/KeyCodes.h b/include/nbl/ui/KeyCodes.h index fb749cb801..40f6726512 100644 --- a/include/nbl/ui/KeyCodes.h +++ b/include/nbl/ui/KeyCodes.h @@ -1,6 +1,9 @@ #ifndef _NBL_UI_KEYCODES_H_INCLUDED_ #define _NBL_UI_KEYCODES_H_INCLUDED_ +#include +#include + namespace nbl::ui { @@ -266,136 +269,6 @@ constexpr char keyCodeToChar(E_KEY_CODE code, bool shiftPressed) return result; } -constexpr E_KEY_CODE stringToKeyCode(std::string_view str) -{ - if (str == "BACKSPACE") return EKC_BACKSPACE; - if (str == "TAB") return EKC_TAB; - if (str == "CLEAR") return EKC_CLEAR; - if (str == "ENTER") return EKC_ENTER; - if (str == "LEFT_SHIFT") return EKC_LEFT_SHIFT; - if (str == "RIGHT_SHIFT") return EKC_RIGHT_SHIFT; - if (str == "LEFT_CONTROL") return EKC_LEFT_CONTROL; - if (str == "RIGHT_CONTROL") return EKC_RIGHT_CONTROL; - if (str == "LEFT_ALT") return EKC_LEFT_ALT; - if (str == "RIGHT_ALT") return EKC_RIGHT_ALT; - if (str == "PAUSE") return EKC_PAUSE; - if (str == "CAPS_LOCK") return EKC_CAPS_LOCK; - if (str == "ESCAPE") return EKC_ESCAPE; - if (str == "SPACE") return EKC_SPACE; - if (str == "PAGE_UP") return EKC_PAGE_UP; - if (str == "PAGE_DOWN") return EKC_PAGE_DOWN; - if (str == "END") return EKC_END; - if (str == "HOME") return EKC_HOME; - if (str == "LEFT_ARROW") return EKC_LEFT_ARROW; - if (str == "RIGHT_ARROW") return EKC_RIGHT_ARROW; - if (str == "DOWN_ARROW") return EKC_DOWN_ARROW; - if (str == "UP_ARROW") return EKC_UP_ARROW; - if (str == "SELECT") return EKC_SELECT; - if (str == "PRINT") return EKC_PRINT; - if (str == "EXECUTE") return EKC_EXECUTE; - if (str == "PRINT_SCREEN") return EKC_PRINT_SCREEN; - if (str == "INSERT") return EKC_INSERT; - if (str == "DELETE") return EKC_DELETE; - if (str == "HELP") return EKC_HELP; - if (str == "LEFT_WIN") return EKC_LEFT_WIN; - if (str == "RIGHT_WIN") return EKC_RIGHT_WIN; - if (str == "APPS") return EKC_APPS; - if (str == "COMMA") return EKC_COMMA; - if (str == "PERIOD") return EKC_PERIOD; - if (str == "SEMICOLON") return EKC_SEMICOLON; - if (str == "OPEN_BRACKET") return EKC_OPEN_BRACKET; - if (str == "CLOSE_BRACKET") return EKC_CLOSE_BRACKET; - if (str == "BACKSLASH") return EKC_BACKSLASH; - if (str == "APOSTROPHE") return EKC_APOSTROPHE; - if (str == "ADD") return EKC_ADD; - if (str == "SUBTRACT") return EKC_SUBTRACT; - if (str == "MULTIPLY") return EKC_MULTIPLY; - if (str == "DIVIDE") return EKC_DIVIDE; - - if (str == "A" || str == "a") return EKC_A; - if (str == "B" || str == "b") return EKC_B; - if (str == "C" || str == "c") return EKC_C; - if (str == "D" || str == "d") return EKC_D; - if (str == "E" || str == "e") return EKC_E; - if (str == "F" || str == "f") return EKC_F; - if (str == "G" || str == "g") return EKC_G; - if (str == "H" || str == "h") return EKC_H; - if (str == "I" || str == "i") return EKC_I; - if (str == "J" || str == "j") return EKC_J; - if (str == "K" || str == "k") return EKC_K; - if (str == "L" || str == "l") return EKC_L; - if (str == "M" || str == "m") return EKC_M; - if (str == "N" || str == "n") return EKC_N; - if (str == "O" || str == "o") return EKC_O; - if (str == "P" || str == "p") return EKC_P; - if (str == "Q" || str == "q") return EKC_Q; - if (str == "R" || str == "r") return EKC_R; - if (str == "S" || str == "s") return EKC_S; - if (str == "T" || str == "t") return EKC_T; - if (str == "U" || str == "u") return EKC_U; - if (str == "V" || str == "v") return EKC_V; - if (str == "W" || str == "w") return EKC_W; - if (str == "X" || str == "x") return EKC_X; - if (str == "Y" || str == "y") return EKC_Y; - if (str == "Z" || str == "z") return EKC_Z; - - if (str == "0") return EKC_0; - if (str == "1") return EKC_1; - if (str == "2") return EKC_2; - if (str == "3") return EKC_3; - if (str == "4") return EKC_4; - if (str == "5") return EKC_5; - if (str == "6") return EKC_6; - if (str == "7") return EKC_7; - if (str == "8") return EKC_8; - if (str == "9") return EKC_9; - - if (str == "F1") return EKC_F1; - if (str == "F2") return EKC_F2; - if (str == "F3") return EKC_F3; - if (str == "F4") return EKC_F4; - if (str == "F5") return EKC_F5; - if (str == "F6") return EKC_F6; - if (str == "F7") return EKC_F7; - if (str == "F8") return EKC_F8; - if (str == "F9") return EKC_F9; - if (str == "F10") return EKC_F10; - if (str == "F11") return EKC_F11; - if (str == "F12") return EKC_F12; - if (str == "F13") return EKC_F13; - if (str == "F14") return EKC_F14; - if (str == "F15") return EKC_F15; - if (str == "F16") return EKC_F16; - if (str == "F17") return EKC_F17; - if (str == "F18") return EKC_F18; - if (str == "F19") return EKC_F19; - if (str == "F20") return EKC_F20; - if (str == "F21") return EKC_F21; - if (str == "F22") return EKC_F22; - if (str == "F23") return EKC_F23; - if (str == "F24") return EKC_F24; - - if (str == "NUMPAD_0") return EKC_NUMPAD_0; - if (str == "NUMPAD_1") return EKC_NUMPAD_1; - if (str == "NUMPAD_2") return EKC_NUMPAD_2; - if (str == "NUMPAD_3") return EKC_NUMPAD_3; - if (str == "NUMPAD_4") return EKC_NUMPAD_4; - if (str == "NUMPAD_5") return EKC_NUMPAD_5; - if (str == "NUMPAD_6") return EKC_NUMPAD_6; - if (str == "NUMPAD_7") return EKC_NUMPAD_7; - if (str == "NUMPAD_8") return EKC_NUMPAD_8; - if (str == "NUMPAD_9") return EKC_NUMPAD_9; - - if (str == "NUM_LOCK") return EKC_NUM_LOCK; - if (str == "SCROLL_LOCK") return EKC_SCROLL_LOCK; - - if (str == "VOLUME_MUTE") return EKC_VOLUME_MUTE; - if (str == "VOLUME_UP") return EKC_VOLUME_UP; - if (str == "VOLUME_DOWN") return EKC_VOLUME_DOWN; - - return EKC_NONE; -} - enum E_MOUSE_BUTTON : uint8_t { EMB_LEFT_BUTTON, @@ -433,6 +306,150 @@ enum E_MOUSE_CODE : uint8_t EMC_COUNT, }; +namespace impl +{ + +template +struct SNamedCode final +{ + std::string_view name; + Code code; +}; + +template +constexpr Code lookupNamedCode(std::string_view str, const std::array, N>& table, const Code fallback) +{ + for (const auto& entry : table) + { + if (str == entry.name) + return entry.code; + } + + return fallback; +} + +constexpr char asciiToUpper(const char c) +{ + return (c >= 'a' && c <= 'z') ? static_cast(c - ('a' - 'A')) : c; +} + +static constexpr auto NamedKeyCodes = std::to_array>({ + { "BACKSPACE", E_KEY_CODE::EKC_BACKSPACE }, + { "TAB", E_KEY_CODE::EKC_TAB }, + { "CLEAR", E_KEY_CODE::EKC_CLEAR }, + { "ENTER", E_KEY_CODE::EKC_ENTER }, + { "LEFT_SHIFT", E_KEY_CODE::EKC_LEFT_SHIFT }, + { "RIGHT_SHIFT", E_KEY_CODE::EKC_RIGHT_SHIFT }, + { "LEFT_CONTROL", E_KEY_CODE::EKC_LEFT_CONTROL }, + { "RIGHT_CONTROL", E_KEY_CODE::EKC_RIGHT_CONTROL }, + { "LEFT_ALT", E_KEY_CODE::EKC_LEFT_ALT }, + { "RIGHT_ALT", E_KEY_CODE::EKC_RIGHT_ALT }, + { "PAUSE", E_KEY_CODE::EKC_PAUSE }, + { "CAPS_LOCK", E_KEY_CODE::EKC_CAPS_LOCK }, + { "ESCAPE", E_KEY_CODE::EKC_ESCAPE }, + { "SPACE", E_KEY_CODE::EKC_SPACE }, + { "PAGE_UP", E_KEY_CODE::EKC_PAGE_UP }, + { "PAGE_DOWN", E_KEY_CODE::EKC_PAGE_DOWN }, + { "END", E_KEY_CODE::EKC_END }, + { "HOME", E_KEY_CODE::EKC_HOME }, + { "LEFT_ARROW", E_KEY_CODE::EKC_LEFT_ARROW }, + { "RIGHT_ARROW", E_KEY_CODE::EKC_RIGHT_ARROW }, + { "DOWN_ARROW", E_KEY_CODE::EKC_DOWN_ARROW }, + { "UP_ARROW", E_KEY_CODE::EKC_UP_ARROW }, + { "SELECT", E_KEY_CODE::EKC_SELECT }, + { "PRINT", E_KEY_CODE::EKC_PRINT }, + { "EXECUTE", E_KEY_CODE::EKC_EXECUTE }, + { "PRINT_SCREEN", E_KEY_CODE::EKC_PRINT_SCREEN }, + { "INSERT", E_KEY_CODE::EKC_INSERT }, + { "DELETE", E_KEY_CODE::EKC_DELETE }, + { "HELP", E_KEY_CODE::EKC_HELP }, + { "LEFT_WIN", E_KEY_CODE::EKC_LEFT_WIN }, + { "RIGHT_WIN", E_KEY_CODE::EKC_RIGHT_WIN }, + { "APPS", E_KEY_CODE::EKC_APPS }, + { "COMMA", E_KEY_CODE::EKC_COMMA }, + { "PERIOD", E_KEY_CODE::EKC_PERIOD }, + { "SEMICOLON", E_KEY_CODE::EKC_SEMICOLON }, + { "OPEN_BRACKET", E_KEY_CODE::EKC_OPEN_BRACKET }, + { "CLOSE_BRACKET", E_KEY_CODE::EKC_CLOSE_BRACKET }, + { "BACKSLASH", E_KEY_CODE::EKC_BACKSLASH }, + { "APOSTROPHE", E_KEY_CODE::EKC_APOSTROPHE }, + { "ADD", E_KEY_CODE::EKC_ADD }, + { "SUBTRACT", E_KEY_CODE::EKC_SUBTRACT }, + { "MULTIPLY", E_KEY_CODE::EKC_MULTIPLY }, + { "DIVIDE", E_KEY_CODE::EKC_DIVIDE }, + { "F1", E_KEY_CODE::EKC_F1 }, + { "F2", E_KEY_CODE::EKC_F2 }, + { "F3", E_KEY_CODE::EKC_F3 }, + { "F4", E_KEY_CODE::EKC_F4 }, + { "F5", E_KEY_CODE::EKC_F5 }, + { "F6", E_KEY_CODE::EKC_F6 }, + { "F7", E_KEY_CODE::EKC_F7 }, + { "F8", E_KEY_CODE::EKC_F8 }, + { "F9", E_KEY_CODE::EKC_F9 }, + { "F10", E_KEY_CODE::EKC_F10 }, + { "F11", E_KEY_CODE::EKC_F11 }, + { "F12", E_KEY_CODE::EKC_F12 }, + { "F13", E_KEY_CODE::EKC_F13 }, + { "F14", E_KEY_CODE::EKC_F14 }, + { "F15", E_KEY_CODE::EKC_F15 }, + { "F16", E_KEY_CODE::EKC_F16 }, + { "F17", E_KEY_CODE::EKC_F17 }, + { "F18", E_KEY_CODE::EKC_F18 }, + { "F19", E_KEY_CODE::EKC_F19 }, + { "F20", E_KEY_CODE::EKC_F20 }, + { "F21", E_KEY_CODE::EKC_F21 }, + { "F22", E_KEY_CODE::EKC_F22 }, + { "F23", E_KEY_CODE::EKC_F23 }, + { "F24", E_KEY_CODE::EKC_F24 }, + { "NUMPAD_0", E_KEY_CODE::EKC_NUMPAD_0 }, + { "NUMPAD_1", E_KEY_CODE::EKC_NUMPAD_1 }, + { "NUMPAD_2", E_KEY_CODE::EKC_NUMPAD_2 }, + { "NUMPAD_3", E_KEY_CODE::EKC_NUMPAD_3 }, + { "NUMPAD_4", E_KEY_CODE::EKC_NUMPAD_4 }, + { "NUMPAD_5", E_KEY_CODE::EKC_NUMPAD_5 }, + { "NUMPAD_6", E_KEY_CODE::EKC_NUMPAD_6 }, + { "NUMPAD_7", E_KEY_CODE::EKC_NUMPAD_7 }, + { "NUMPAD_8", E_KEY_CODE::EKC_NUMPAD_8 }, + { "NUMPAD_9", E_KEY_CODE::EKC_NUMPAD_9 }, + { "NUM_LOCK", E_KEY_CODE::EKC_NUM_LOCK }, + { "SCROLL_LOCK", E_KEY_CODE::EKC_SCROLL_LOCK }, + { "VOLUME_MUTE", E_KEY_CODE::EKC_VOLUME_MUTE }, + { "VOLUME_UP", E_KEY_CODE::EKC_VOLUME_UP }, + { "VOLUME_DOWN", E_KEY_CODE::EKC_VOLUME_DOWN } +}); + +static constexpr auto NamedMouseCodes = std::to_array>({ + { "LEFT_BUTTON", E_MOUSE_CODE::EMC_LEFT_BUTTON }, + { "RIGHT_BUTTON", E_MOUSE_CODE::EMC_RIGHT_BUTTON }, + { "MIDDLE_BUTTON", E_MOUSE_CODE::EMC_MIDDLE_BUTTON }, + { "BUTTON_4", E_MOUSE_CODE::EMC_BUTTON_4 }, + { "BUTTON_5", E_MOUSE_CODE::EMC_BUTTON_5 }, + { "VERTICAL_POSITIVE_SCROLL", E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL }, + { "VERTICAL_NEGATIVE_SCROLL", E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL }, + { "HORIZONTAL_POSITIVE_SCROLL", E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL }, + { "HORIZONTAL_NEGATIVE_SCROLL", E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL }, + { "RELATIVE_POSITIVE_MOVEMENT_X", E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X }, + { "RELATIVE_POSITIVE_MOVEMENT_Y", E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y }, + { "RELATIVE_NEGATIVE_MOVEMENT_X", E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X }, + { "RELATIVE_NEGATIVE_MOVEMENT_Y", E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y } +}); + +} // namespace impl + +constexpr E_KEY_CODE stringToKeyCode(std::string_view str) +{ + if (str.size() == 1u) + { + const char upper = impl::asciiToUpper(str.front()); + if (upper >= 'A' && upper <= 'Z') + return static_cast(upper); + if (upper >= '0' && upper <= '9') + return static_cast(upper); + } + + return impl::lookupNamedCode(str, impl::NamedKeyCodes, E_KEY_CODE::EKC_NONE); +} + constexpr std::string_view mouseCodeToString(E_MOUSE_CODE code) { switch (code) @@ -459,21 +476,7 @@ constexpr std::string_view mouseCodeToString(E_MOUSE_CODE code) constexpr E_MOUSE_CODE stringToMouseCode(std::string_view str) { - if (str == "LEFT_BUTTON") return EMC_LEFT_BUTTON; - if (str == "RIGHT_BUTTON") return EMC_RIGHT_BUTTON; - if (str == "MIDDLE_BUTTON") return EMC_MIDDLE_BUTTON; - if (str == "BUTTON_4") return EMC_BUTTON_4; - if (str == "BUTTON_5") return EMC_BUTTON_5; - if (str == "VERTICAL_POSITIVE_SCROLL") return EMC_VERTICAL_POSITIVE_SCROLL; - if (str == "VERTICAL_NEGATIVE_SCROLL") return EMC_VERTICAL_NEGATIVE_SCROLL; - if (str == "HORIZONTAL_POSITIVE_SCROLL") return EMC_HORIZONTAL_POSITIVE_SCROLL; - if (str == "HORIZONTAL_NEGATIVE_SCROLL") return EMC_HORIZONTAL_NEGATIVE_SCROLL; - if (str == "RELATIVE_POSITIVE_MOVEMENT_X") return EMC_RELATIVE_POSITIVE_MOVEMENT_X; - if (str == "RELATIVE_POSITIVE_MOVEMENT_Y") return EMC_RELATIVE_POSITIVE_MOVEMENT_Y; - if (str == "RELATIVE_NEGATIVE_MOVEMENT_X") return EMC_RELATIVE_NEGATIVE_MOVEMENT_X; - if (str == "RELATIVE_NEGATIVE_MOVEMENT_Y") return EMC_RELATIVE_NEGATIVE_MOVEMENT_Y; - - return EMC_NONE; + return impl::lookupNamedCode(str, impl::NamedMouseCodes, E_MOUSE_CODE::EMC_NONE); } } From cc9521ff97deb402163235397eca18566412c7ec Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Wed, 8 Apr 2026 07:47:54 +0200 Subject: [PATCH 141/161] Update examples_tests after camera cleanup --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 61c24c01d7..001fc79355 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 61c24c01d7631d972b81d08289b8a8b94f96b565 +Subproject commit 001fc7935521db7be6917365ed9d53f8f44856ab From 3a88506f85070897166c45c9ad85e4b01abf6f29 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 10:01:29 +0200 Subject: [PATCH 142/161] Move cameras module into ext --- examples_tests | 2 +- include/nbl/asset/IDescriptorSet.h | 2 +- .../hlsl/cpp_compat/impl/intrinsics_impl.hlsl | 8 +- .../builtin/hlsl/cpp_compat/intrinsics.hlsl | 3 +- .../hlsl/math/thin_lens_projection.hlsl | 5 +- include/nbl/builtin/hlsl/numbers.hlsl | 2 +- include/nbl/core/math/intutil.h | 12 +- include/nbl/ext/Cameras/CArcballCamera.hpp | 89 ++ .../nbl/ext/Cameras/CCameraFileUtilities.hpp | 94 ++ .../CCameraFollowRegressionUtilities.hpp | 377 ++++++ .../ext/Cameras/CCameraFollowUtilities.hpp | 391 ++++++ include/nbl/ext/Cameras/CCameraGoal.hpp | 409 ++++++ .../nbl/ext/Cameras/CCameraGoalAnalysis.hpp | 89 ++ include/nbl/ext/Cameras/CCameraGoalSolver.hpp | 646 +++++++++ .../Cameras/CCameraInputBindingUtilities.hpp | 561 ++++++++ .../nbl/ext/Cameras/CCameraKeyframeTrack.hpp | 175 +++ .../CCameraKeyframeTrackPersistence.hpp | 30 + .../nbl/ext/Cameras/CCameraKindUtilities.hpp | 140 ++ .../Cameras/CCameraManipulationUtilities.hpp | 156 +++ .../nbl/ext/Cameras/CCameraMathUtilities.hpp | 949 ++++++++++++++ .../nbl/ext/Cameras/CCameraPathMetadata.hpp | 30 + .../nbl/ext/Cameras/CCameraPathUtilities.hpp | 575 ++++++++ .../nbl/ext/Cameras/CCameraPersistence.hpp | 33 + .../ext/Cameras/CCameraPlaybackTimeline.hpp | 103 ++ .../Cameras/CCameraPresentationUtilities.hpp | 125 ++ include/nbl/ext/Cameras/CCameraPreset.hpp | 71 + include/nbl/ext/Cameras/CCameraPresetFlow.hpp | 150 +++ .../ext/Cameras/CCameraPresetPersistence.hpp | 40 + .../Cameras/CCameraProjectionUtilities.hpp | 36 + .../Cameras/CCameraScriptedCheckRunner.hpp | 564 ++++++++ .../ext/Cameras/CCameraScriptedRuntime.hpp | 392 ++++++ .../CCameraScriptedRuntimePersistence.hpp | 74 ++ .../CCameraScriptedUiInputUtilities.hpp | 98 ++ .../nbl/ext/Cameras/CCameraSequenceScript.hpp | 792 +++++++++++ .../CCameraSequenceScriptPersistence.hpp | 29 + .../CCameraSequenceScriptedBuilder.hpp | 128 ++ .../CCameraSmokeRegressionUtilities.hpp | 122 ++ .../CCameraTargetRelativeUtilities.hpp | 270 ++++ .../nbl/ext/Cameras/CCameraTextUtilities.hpp | 210 +++ include/nbl/ext/Cameras/CCameraTraits.hpp | 41 + .../Cameras/CCameraVirtualEventUtilities.hpp | 188 +++ include/nbl/ext/Cameras/CChaseCamera.hpp | 89 ++ include/nbl/ext/Cameras/CCubeProjection.hpp | 97 ++ include/nbl/ext/Cameras/CDollyCamera.hpp | 80 ++ include/nbl/ext/Cameras/CDollyZoomCamera.hpp | 122 ++ include/nbl/ext/Cameras/CFPSCamera.hpp | 125 ++ include/nbl/ext/Cameras/CFreeLockCamera.hpp | 83 ++ .../nbl/ext/Cameras/CGeneralPurposeGimbal.hpp | 24 + .../nbl/ext/Cameras/CGimbalInputBinder.hpp | 131 ++ include/nbl/ext/Cameras/CIsometricCamera.hpp | 77 ++ include/nbl/ext/Cameras/CLinearProjection.hpp | 59 + include/nbl/ext/Cameras/COrbitCamera.hpp | 83 ++ include/nbl/ext/Cameras/CPathCamera.hpp | 342 +++++ include/nbl/ext/Cameras/CPlanarProjection.hpp | 56 + .../ext/Cameras/CSphericalTargetCamera.hpp | 243 ++++ include/nbl/ext/Cameras/CTopDownCamera.hpp | 78 ++ include/nbl/ext/Cameras/CTurntableCamera.hpp | 85 ++ .../nbl/ext/Cameras/CVirtualGimbalEvent.hpp | 159 +++ include/nbl/ext/Cameras/ICamera.hpp | 494 +++++++ include/nbl/ext/Cameras/IGimbal.hpp | 369 ++++++ .../nbl/ext/Cameras/IGimbalBindingLayout.hpp | 131 ++ .../nbl/ext/Cameras/IGimbalInputProcessor.hpp | 438 +++++++ include/nbl/ext/Cameras/ILinearProjection.hpp | 185 +++ .../ext/Cameras/IPerspectiveProjection.hpp | 64 + include/nbl/ext/Cameras/IPlanarProjection.hpp | 137 ++ include/nbl/ext/Cameras/IProjection.hpp | 71 + include/nbl/ext/Cameras/IRange.hpp | 21 + include/nbl/ext/Cameras/README.md | 841 ++++++++++++ include/nbl/ext/Cameras/SCameraRigPose.hpp | 26 + src/nbl/ext/CMakeLists.txt | 10 + .../CCameraJsonPersistenceUtilities.hpp | 98 ++ src/nbl/ext/Cameras/CCameraPersistence.cpp | 278 ++++ .../CCameraScriptedRuntimePersistence.cpp | 1156 +++++++++++++++++ src/nbl/ext/Cameras/CMakeLists.txt | 33 + src/nbl/ext/ImGui/CMakeLists.txt | 6 +- 75 files changed, 14487 insertions(+), 15 deletions(-) create mode 100644 include/nbl/ext/Cameras/CArcballCamera.hpp create mode 100644 include/nbl/ext/Cameras/CCameraFileUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraFollowUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraGoal.hpp create mode 100644 include/nbl/ext/Cameras/CCameraGoalAnalysis.hpp create mode 100644 include/nbl/ext/Cameras/CCameraGoalSolver.hpp create mode 100644 include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp create mode 100644 include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp create mode 100644 include/nbl/ext/Cameras/CCameraKindUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraMathUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPathMetadata.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPathUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPersistence.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPlaybackTimeline.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPresentationUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPreset.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPresetFlow.hpp create mode 100644 include/nbl/ext/Cameras/CCameraPresetPersistence.hpp create mode 100644 include/nbl/ext/Cameras/CCameraProjectionUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp create mode 100644 include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp create mode 100644 include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp create mode 100644 include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraSequenceScript.hpp create mode 100644 include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp create mode 100644 include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp create mode 100644 include/nbl/ext/Cameras/CCameraSmokeRegressionUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraTextUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CCameraTraits.hpp create mode 100644 include/nbl/ext/Cameras/CCameraVirtualEventUtilities.hpp create mode 100644 include/nbl/ext/Cameras/CChaseCamera.hpp create mode 100644 include/nbl/ext/Cameras/CCubeProjection.hpp create mode 100644 include/nbl/ext/Cameras/CDollyCamera.hpp create mode 100644 include/nbl/ext/Cameras/CDollyZoomCamera.hpp create mode 100644 include/nbl/ext/Cameras/CFPSCamera.hpp create mode 100644 include/nbl/ext/Cameras/CFreeLockCamera.hpp create mode 100644 include/nbl/ext/Cameras/CGeneralPurposeGimbal.hpp create mode 100644 include/nbl/ext/Cameras/CGimbalInputBinder.hpp create mode 100644 include/nbl/ext/Cameras/CIsometricCamera.hpp create mode 100644 include/nbl/ext/Cameras/CLinearProjection.hpp create mode 100644 include/nbl/ext/Cameras/COrbitCamera.hpp create mode 100644 include/nbl/ext/Cameras/CPathCamera.hpp create mode 100644 include/nbl/ext/Cameras/CPlanarProjection.hpp create mode 100644 include/nbl/ext/Cameras/CSphericalTargetCamera.hpp create mode 100644 include/nbl/ext/Cameras/CTopDownCamera.hpp create mode 100644 include/nbl/ext/Cameras/CTurntableCamera.hpp create mode 100644 include/nbl/ext/Cameras/CVirtualGimbalEvent.hpp create mode 100644 include/nbl/ext/Cameras/ICamera.hpp create mode 100644 include/nbl/ext/Cameras/IGimbal.hpp create mode 100644 include/nbl/ext/Cameras/IGimbalBindingLayout.hpp create mode 100644 include/nbl/ext/Cameras/IGimbalInputProcessor.hpp create mode 100644 include/nbl/ext/Cameras/ILinearProjection.hpp create mode 100644 include/nbl/ext/Cameras/IPerspectiveProjection.hpp create mode 100644 include/nbl/ext/Cameras/IPlanarProjection.hpp create mode 100644 include/nbl/ext/Cameras/IProjection.hpp create mode 100644 include/nbl/ext/Cameras/IRange.hpp create mode 100644 include/nbl/ext/Cameras/README.md create mode 100644 include/nbl/ext/Cameras/SCameraRigPose.hpp create mode 100644 src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp create mode 100644 src/nbl/ext/Cameras/CCameraPersistence.cpp create mode 100644 src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp create mode 100644 src/nbl/ext/Cameras/CMakeLists.txt diff --git a/examples_tests b/examples_tests index 001fc79355..87f5310134 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 001fc7935521db7be6917365ed9d53f8f44856ab +Subproject commit 87f531013491f4385ac967ff43a7bb61c480f1e8 diff --git a/include/nbl/asset/IDescriptorSet.h b/include/nbl/asset/IDescriptorSet.h index 34195cd5c1..e5502d2033 100644 --- a/include/nbl/asset/IDescriptorSet.h +++ b/include/nbl/asset/IDescriptorSet.h @@ -69,7 +69,7 @@ class IDescriptorSet : public virtual core::IReferenceCounted // TODO: try to re SCombinedImageSamplerInfo combinedImageSampler; } info; - SDescriptorInfo() {} + SDescriptorInfo() : desc(), info() {} template SDescriptorInfo(const SBufferBinding& binding) : desc() diff --git a/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl b/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl index 61081ea327..4e4fcf1672 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl @@ -2,6 +2,7 @@ #define _NBL_BUILTIN_HLSL_CPP_COMPAT_IMPL_INTRINSICS_IMPL_INCLUDED_ #include +#include #include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -509,7 +509,8 @@ struct radians_helper using return_t = T; static inline return_t __call(const T degrees) { - return degrees * (bit_cast(numbers::pi) / static_cast(180.0)); + constexpr T Pi = ::nbl::hlsl::numbers::template pi; + return degrees * (Pi / static_cast(180.0)); } }; @@ -520,7 +521,8 @@ struct degrees_helper using return_t = T; static inline return_t __call(const T radians) { - return radians * (static_cast(180.0) / bit_cast(numbers::pi)); + constexpr T Pi = ::nbl::hlsl::numbers::template pi; + return radians * (static_cast(180.0) / Pi); } }; diff --git a/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl b/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl index 78367f7924..45dfb6d909 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -331,4 +332,4 @@ inline T fma(NBL_CONST_REF_ARG(T) x, NBL_CONST_REF_ARG(T) y, NBL_CONST_REF_ARG(T } } -#endif \ No newline at end of file +#endif diff --git a/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl b/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl index 985ec8a6a3..dd1077f9b7 100644 --- a/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl +++ b/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl @@ -1,6 +1,7 @@ #ifndef _NBL_BUILTIN_HLSL_MATH_THIN_LENS_PROJECTION_INCLUDED_ #define _NBL_BUILTIN_HLSL_MATH_THIN_LENS_PROJECTION_INCLUDED_ +#include #include #include @@ -16,7 +17,7 @@ namespace thin_lens template) inline matrix rhPerspectiveFovMatrix(FloatingPoint fieldOfViewRadians, FloatingPoint aspectRatio, FloatingPoint zNear, FloatingPoint zFar) { - const FloatingPoint h = core::reciprocal(tan(fieldOfViewRadians * 0.5f)); + const FloatingPoint h = ::nbl::core::reciprocal(tan(fieldOfViewRadians * 0.5f)); _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero const float w = h / aspectRatio; @@ -33,7 +34,7 @@ inline matrix rhPerspectiveFovMatrix(FloatingPoint fieldOfV template) inline matrix lhPerspectiveFovMatrix(FloatingPoint fieldOfViewRadians, FloatingPoint aspectRatio, FloatingPoint zNear, FloatingPoint zFar) { - const FloatingPoint h = core::reciprocal(tan(fieldOfViewRadians * 0.5f)); + const FloatingPoint h = ::nbl::core::reciprocal(tan(fieldOfViewRadians * 0.5f)); _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero const float w = h / aspectRatio; diff --git a/include/nbl/builtin/hlsl/numbers.hlsl b/include/nbl/builtin/hlsl/numbers.hlsl index 4594596590..e55d9794af 100644 --- a/include/nbl/builtin/hlsl/numbers.hlsl +++ b/include/nbl/builtin/hlsl/numbers.hlsl @@ -1,7 +1,7 @@ #ifndef _NBL_BUILTIN_HLSL_MATH_NUMBERS_INCLUDED_ #define _NBL_BUILTIN_HLSL_MATH_NUMBERS_INCLUDED_ -#include "nbl/builtin/hlsl/cpp_compat.hlsl" +#include "nbl/builtin/hlsl/cpp_compat/basic.h" namespace nbl { diff --git a/include/nbl/core/math/intutil.h b/include/nbl/core/math/intutil.h index 7a94844258..d799f7f885 100644 --- a/include/nbl/core/math/intutil.h +++ b/include/nbl/core/math/intutil.h @@ -3,10 +3,14 @@ // For conditions of distribution and use, see copyright notice in nabla.h // TODO: kill this file -#ifndef __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ -#define __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ - -#include "nbl/builtin/hlsl/math/intutil.hlsl" +#ifndef __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ +#define __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ + +#include +#include + +#include "nbl/macros.h" +#include "nbl/builtin/hlsl/math/intutil.hlsl" namespace nbl diff --git a/include/nbl/ext/Cameras/CArcballCamera.hpp b/include/nbl/ext/Cameras/CArcballCamera.hpp new file mode 100644 index 0000000000..d88d3743f3 --- /dev/null +++ b/include/nbl/ext/Cameras/CArcballCamera.hpp @@ -0,0 +1,89 @@ +// Copyright (C) 2018-2024 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_ARCBALL_CAMERA_HPP_ +#define _C_ARCBALL_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera with planar target translation and bounded arcball orbiting. +/// +/// The runtime state is inherited from `CSphericalTargetCamera`. Translation +/// moves the target in the current view plane. Rotation updates orbit yaw and +/// pitch under a symmetric pitch limit. +class CArcballCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CArcballCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(position, target) + { + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); + applyPose(); + } + ~CArcballCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Apply one frame of semantic translation and rotation input to the arcball rig. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); + + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + const auto basis = computeBasis(m_orbitUv, m_distance); + applyPlanarTargetTranslation(deltaTranslation, basis); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Arcball; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. + virtual std::string_view getIdentifier() const override { return "Arcball Camera"; } + + static inline constexpr float MinDistance = base_t::MinDistance; + static inline constexpr float MaxDistance = base_t::MaxDistance; + +private: + + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad; + static inline constexpr double MinPitch = -MaxPitch; +}; + +} + +#endif diff --git a/include/nbl/ext/Cameras/CCameraFileUtilities.hpp b/include/nbl/ext/Cameras/CCameraFileUtilities.hpp new file mode 100644 index 0000000000..cd381bdacd --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraFileUtilities.hpp @@ -0,0 +1,94 @@ +#ifndef _C_CAMERA_FILE_UTILITIES_HPP_ +#define _C_CAMERA_FILE_UTILITIES_HPP_ + +#include +#include +#include + +#include "nbl/system/IFile.h" +#include "nbl/system/ISystem.h" + +namespace nbl::system +{ + +/// @brief Shared file I/O helpers used by camera persistence and scripted-runtime loaders. +/// +/// The helpers keep camera-facing persistence code independent from ad-hoc file +/// handling and provide one place for consistent error propagation. +struct CCameraFileUtilities final +{ +public: + /// @brief Read a whole file into a byte buffer. + static inline bool readBinaryFile( + ISystem& system, + const path& filePath, + std::vector& outPayload, + std::string* error = nullptr, + const std::string_view openError = {}) + { + ISystem::future_t> future; + system.createFile(future, filePath, IFile::ECF_READ | IFile::ECF_MAPPABLE); + auto file = future.acquire(); + if (!file || !file->get()) + { + if (error && !openError.empty()) + *error = std::string(openError); + return false; + } + + auto& input = *file->get(); + const auto fileSize = input.getSize(); + outPayload.resize(fileSize); + if (outPayload.empty()) + return true; + + IFile::success_t readResult; + input.read(readResult, outPayload.data(), 0, fileSize); + if (!static_cast(readResult)) + { + if (error && !openError.empty()) + *error = std::string(openError); + return false; + } + return true; + } + + /// @brief Read a whole file and interpret its payload as UTF-8 text. + static inline bool readTextFile( + ISystem& system, + const path& filePath, + std::string& outText, + std::string* error = nullptr, + const std::string_view openError = {}) + { + std::vector payload; + if (!readBinaryFile(system, filePath, payload, error, openError)) + return false; + + outText.assign(reinterpret_cast(payload.data()), payload.size()); + return true; + } + + /// @brief Overwrite a file with the provided text payload. + static inline bool writeTextFile( + ISystem& system, + const path& filePath, + const std::string_view text) + { + ISystem::future_t> future; + system.createFile(future, filePath, IFile::ECF_WRITE); + auto file = future.acquire(); + if (!file || !file->get()) + return false; + if (text.empty()) + return true; + + IFile::success_t writeResult; + (*file)->write(writeResult, text.data(), 0, text.size()); + return static_cast(writeResult); + } +}; + +} // namespace nbl::system + +#endif // _C_CAMERA_FILE_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp b/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp new file mode 100644 index 0000000000..7ea74ae844 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp @@ -0,0 +1,377 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ +#define _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ + +#include + +#include "CCameraFollowUtilities.hpp" + +namespace nbl::system +{ + +struct SCameraProjectedTargetMetrics final +{ + hlsl::float32_t2 ndc = hlsl::float32_t2(0.0f); + float radius = 0.0f; +}; + +/// @brief Reusable follow validation helpers. +/// +/// The checks stay camera-domain: +/// +/// - camera-to-target direction must match the camera forward axis for locking modes +/// - target distance must be finite and internally consistent +/// - spherical cameras must write the tracked target back into spherical target state +/// - spherical distance must match the goal-derived distance when present +struct SCameraFollowRegressionResult +{ + bool passed = false; + bool hasLockMetrics = false; + float lockAngleDeg = 0.0f; + double targetDistance = 0.0; + bool hasProjectedMetrics = false; + SCameraProjectedTargetMetrics projectedTarget = {}; + bool hasSphericalState = false; + hlsl::float64_t3 sphericalTarget = hlsl::float64_t3(0.0); + float sphericalDistance = 0.0f; +}; + +/// @brief Reusable visual/debug metrics for one active follow configuration. +struct SCameraFollowVisualMetrics +{ + bool active = false; + core::ECameraFollowMode mode = core::ECameraFollowMode::Disabled; + bool lockValid = false; + float lockAngleDeg = 0.0f; + float targetDistance = 0.0f; + bool projectedValid = false; + SCameraProjectedTargetMetrics projectedTarget = {}; +}; + +/// @brief Shared view/projection bundle for CPU-side projected target metrics. +struct SCameraProjectionContext +{ + hlsl::float32_t4x4 viewMatrix = hlsl::float32_t4x4(1.0f); + hlsl::float32_t4x4 projectionMatrix = hlsl::float32_t4x4(1.0f); +}; + +/// @brief Shared tolerances for follow target lock, writeback, and projected-center checks. +struct SCameraFollowRegressionThresholds +{ + static inline constexpr float DefaultClipWEpsilon = 1e-5f; + static inline constexpr float DefaultProjectedNdcTolerance = 0.03f; + static inline constexpr float DefaultLockAngleToleranceDeg = static_cast(core::SCameraToolingThresholds::DefaultAngularToleranceDeg); + static inline constexpr double DefaultDistanceTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static inline constexpr double DefaultTargetTolerance = core::SCameraToolingThresholds::TinyScalarEpsilon; + static inline constexpr double DefaultPositionTolerance = core::SCameraToolingThresholds::DefaultPositionTolerance; + static inline constexpr double DefaultRotationToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; + static inline constexpr double DefaultScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; + + float clipWEpsilon = DefaultClipWEpsilon; + float projectedNdcTolerance = DefaultProjectedNdcTolerance; + float lockAngleToleranceDeg = DefaultLockAngleToleranceDeg; + double distanceTolerance = DefaultDistanceTolerance; + double targetTolerance = DefaultTargetTolerance; + double positionTolerance = DefaultPositionTolerance; + double rotationToleranceDeg = DefaultRotationToleranceDeg; + double scalarTolerance = DefaultScalarTolerance; +}; + +/// @brief Bundled reusable follow regression flow. +/// The helper builds a follow goal, applies it, verifies the resulting camera state, +/// and then checks lock/writeback follow consistency. +struct SCameraFollowApplyValidationResult +{ + bool hasGoal = false; + core::CCameraGoal goal = {}; + core::CCameraGoalSolver::SApplyResult applyResult = {}; + bool hasCapturedGoal = false; + core::CCameraGoal capturedGoal = {}; + SCameraFollowRegressionResult regression = {}; +}; + +struct CCameraFollowRegressionUtilities final +{ +public: + static inline SCameraFollowRegressionThresholds makeFollowRegressionThresholds( + const float projectedNdcTolerance = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance, + const float lockAngleToleranceDeg = SCameraFollowRegressionThresholds::DefaultLockAngleToleranceDeg) + { + auto thresholds = SCameraFollowRegressionThresholds{}; + thresholds.projectedNdcTolerance = projectedNdcTolerance; + thresholds.lockAngleToleranceDeg = lockAngleToleranceDeg; + return thresholds; + } + + static inline bool tryComputeProjectedFollowTargetMetrics( + const SCameraProjectionContext& projectionContext, + const core::CTrackedTarget& trackedTarget, + SCameraProjectedTargetMetrics& outMetrics, + const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) + { + outMetrics = {}; + const hlsl::float32_t3 target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); + const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); + const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); + const auto clip = hlsl::mul(clipProjection, viewSpace); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(clip.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.z) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.w)) + return false; + + const auto absW = hlsl::abs(clip.w); + if (absW < clipWEpsilon) + return false; + + const float invW = 1.0f / clip.w; + outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; + if (!hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.y)) + return false; + + outMetrics.radius = hlsl::length(outMetrics.ndc); + + return true; + } + + static inline bool validateProjectedFollowTargetContract( + const SCameraProjectionContext& projectionContext, + const core::CTrackedTarget& trackedTarget, + SCameraProjectedTargetMetrics& outMetrics, + std::string* error = nullptr, + const SCameraFollowRegressionThresholds& thresholds = {}) + { + if (!tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, outMetrics, thresholds.clipWEpsilon)) + { + if (error) + *error = "failed to project follow target"; + return false; + } + + if (outMetrics.radius > thresholds.projectedNdcTolerance) + { + if (error) + { + *error = "projected target mismatch ndc=(" + std::to_string(outMetrics.ndc.x) + + "," + std::to_string(outMetrics.ndc.y) + ") radius=" + std::to_string(outMetrics.radius); + } + return false; + } + + return true; + } + + static inline SCameraFollowVisualMetrics buildFollowVisualMetrics( + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig* followConfig, + const SCameraProjectionContext* projectionContext = nullptr) + { + SCameraFollowVisualMetrics out = {}; + if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) + return out; + + out.active = true; + out.mode = followConfig->mode; + + double targetDistance = 0.0; + out.lockValid = core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig->mode) && + core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); + if (out.lockValid) + out.targetDistance = static_cast(targetDistance); + + if (out.lockValid && projectionContext) + { + out.projectedValid = tryComputeProjectedFollowTargetMetrics(*projectionContext, trackedTarget, out.projectedTarget); + } + + return out; + } + + static inline bool validateFollowTargetContract( + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, + const core::CCameraGoal& followGoal, + SCameraFollowRegressionResult& out, + std::string* error = nullptr, + const SCameraProjectionContext* projectionContext = nullptr, + const SCameraFollowRegressionThresholds& thresholds = {}) + { + out = {}; + if (!camera) + { + if (error) + *error = "missing camera"; + return false; + } + + if (core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig.mode)) + { + out.hasLockMetrics = core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); + if (!out.hasLockMetrics) + { + if (error) + *error = "failed to compute follow lock metrics"; + return false; + } + + const auto& trackedTargetGimbal = trackedTarget.getGimbal(); + const auto& cameraGimbal = camera->getGimbal(); + const hlsl::float64_t3 trackedTargetPosition = trackedTargetGimbal.getPosition(); + const hlsl::float64_t3 cameraPosition = cameraGimbal.getPosition(); + const double expectedTargetDistance = hlsl::length(trackedTargetPosition - cameraPosition); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) + { + if (error) + { + *error = "target distance mismatch actual=" + std::to_string(out.targetDistance) + + " expected=" + std::to_string(expectedTargetDistance); + } + return false; + } + + if (out.lockAngleDeg > thresholds.lockAngleToleranceDeg) + { + if (error) + *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); + return false; + } + + if (projectionContext) + { + out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( + *projectionContext, + trackedTarget, + out.projectedTarget, + thresholds.clipWEpsilon); + if (!out.hasProjectedMetrics) + { + if (error) + *error = "failed to compute projected follow target metrics"; + return false; + } + + if (out.projectedTarget.radius > thresholds.projectedNdcTolerance) + { + if (error) + *error = "projected target mismatch ndc=(" + std::to_string(out.projectedTarget.ndc.x) + + "," + std::to_string(out.projectedTarget.ndc.y) + ") radius=" + std::to_string(out.projectedTarget.radius); + return false; + } + } + } + + if (camera->supportsGoalState(core::ICamera::GoalStateSphericalTarget)) + { + core::ICamera::SphericalTargetState state; + if (!camera->tryGetSphericalTargetState(state)) + { + if (error) + *error = "missing spherical target state"; + return false; + } + + out.hasSphericalState = true; + out.sphericalTarget = state.target; + out.sphericalDistance = state.distance; + + const auto& trackedTargetGimbal = trackedTarget.getGimbal(); + const auto& cameraGimbal = camera->getGimbal(); + const hlsl::float64_t3 trackedTargetPosition = trackedTargetGimbal.getPosition(); + const hlsl::float64_t3 targetDelta = state.target - trackedTargetPosition; + const double targetDeltaLen = hlsl::length(targetDelta); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) + { + if (error) + *error = "spherical target writeback mismatch"; + return false; + } + + const double actualDistance = hlsl::length(cameraGimbal.getPosition() - trackedTargetPosition); + const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : + (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(actualDistance) || !hlsl::CCameraMathUtilities::isFiniteScalar(expectedDistance) || + hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || + hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) + { + if (error) + { + *error = "spherical distance mismatch actual=" + std::to_string(actualDistance) + + " state=" + std::to_string(state.distance) + + " expected=" + std::to_string(expectedDistance); + } + return false; + } + } + + out.passed = true; + return true; + } + + static inline bool buildApplyAndValidateFollowTargetContract( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, + SCameraFollowApplyValidationResult& out, + std::string* error = nullptr, + const SCameraProjectionContext* projectionContext = nullptr, + const SCameraFollowRegressionThresholds& thresholds = {}) + { + out = {}; + + if (!core::CCameraFollowUtilities::tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) + { + if (error) + *error = "failed to build follow goal"; + return false; + } + out.hasGoal = true; + + out.applyResult = core::CCameraFollowUtilities::applyFollowToCamera(solver, camera, trackedTarget, followConfig); + if (!out.applyResult.succeeded()) + { + if (error) + *error = "failed to apply follow goal"; + return false; + } + + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + { + if (error) + *error = "failed to capture camera state after follow apply"; + return false; + } + + out.hasCapturedGoal = true; + out.capturedGoal = capture.goal; + if (!core::CCameraGoalUtilities::compareGoals(out.capturedGoal, out.goal, thresholds.positionTolerance, thresholds.rotationToleranceDeg, thresholds.scalarTolerance)) + { + if (error) + *error = std::string("follow goal mismatch. ") + core::CCameraGoalUtilities::describeGoalMismatch(out.capturedGoal, out.goal); + return false; + } + + if (!validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + out.goal, + out.regression, + error, + projectionContext, + thresholds)) + { + return false; + } + + return true; + } +}; + +} // namespace nbl::system + +#endif // _C_CAMERA_FOLLOW_REGRESSION_UTILITIES_HPP_ + diff --git a/include/nbl/ext/Cameras/CCameraFollowUtilities.hpp b/include/nbl/ext/Cameras/CCameraFollowUtilities.hpp new file mode 100644 index 0000000000..df687458b4 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraFollowUtilities.hpp @@ -0,0 +1,391 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_FOLLOW_UTILITIES_HPP_ +#define _C_CAMERA_FOLLOW_UTILITIES_HPP_ + +#include +#include + +#include "CCameraGoalSolver.hpp" +#include "CCameraTargetRelativeUtilities.hpp" +#include "CCameraKindUtilities.hpp" + +namespace nbl::core +{ + +/// @brief Reusable tracked-target and follow helpers. +/// +/// The tracked subject owns its own gimbal. Follow code reads that pose and +/// maps one camera plus one tracked target into a `CCameraGoal`. +class CTrackedTarget +{ +public: + using gimbal_t = ICamera::CGimbal; + + /// @brief Construct a tracked target from an initial pose and optional identifier. + CTrackedTarget( + const hlsl::float64_t3& position = hlsl::float64_t3(0.0), + const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(), + std::string identifier = "Follow Target") + : m_identifier(std::move(identifier)), + m_gimbal(gimbal_t::base_t::SCreationParameters{ .position = position, .orientation = orientation }) + { + m_gimbal.updateView(); + } + + /// @brief Return the stable human-readable identifier of the tracked target. + inline const std::string& getIdentifier() const { return m_identifier; } + /// @brief Return read-only access to the tracked target gimbal. + inline const gimbal_t& getGimbal() const { return m_gimbal; } + /// @brief Return mutable access to the tracked target gimbal. + inline gimbal_t& getGimbal() { return m_gimbal; } + + /// @brief Replace the tracked target pose in world space. + inline void setPose(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation) + { + m_gimbal.begin(); + m_gimbal.setPosition(position); + m_gimbal.setOrientation(orientation); + m_gimbal.end(); + m_gimbal.updateView(); + } + + /// @brief Replace only the tracked target position. + inline void setPosition(const hlsl::float64_t3& position) + { + setPose(position, m_gimbal.getOrientation()); + } + + /// @brief Replace only the tracked target orientation. + inline void setOrientation(const hlsl::camera_quaternion_t& orientation) + { + setPose(m_gimbal.getPosition(), orientation); + } + + /// @brief Replace the tracked target pose from a rigid transform matrix when possible. + inline bool trySetFromTransform(const hlsl::float64_t4x4& transform) + { + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + if (!hlsl::CCameraMathUtilities::tryExtractRigidPoseFromTransform(transform, position, orientation)) + return false; + + setPose(position, orientation); + return true; + } + +private: + std::string m_identifier; + gimbal_t m_gimbal; +}; + +/// @brief Follow policy layered on top of a tracked target gimbal. +/// +/// Each mode defines how tracked-target motion updates the camera: +/// +/// - `OrbitTarget` rewrites target-relative camera state so the tracked target becomes the camera target +/// - `LookAtTarget` preserves camera position and rebuilds orientation toward the tracked target +/// - `KeepWorldOffset` places the camera at `trackedTarget.position + worldOffset` and looks at the target +/// - `KeepLocalOffset` transforms `localOffset` by the tracked-target local frame and looks at the target +/// +/// The tracked target provides pose data. The camera reads that data and does +/// not own the tracked subject. +enum class ECameraFollowMode : uint8_t +{ + Disabled, + OrbitTarget, + LookAtTarget, + KeepWorldOffset, + KeepLocalOffset +}; + +/// @brief Reusable follow configuration interpreted against a tracked target gimbal. +/// `worldOffset` and `localOffset` are only meaningful for their matching offset-based modes. +struct SCameraFollowConfig +{ + /// @brief Whether follow should be applied at all. + bool enabled = false; + /// @brief Follow policy used when the configuration is enabled. + ECameraFollowMode mode = ECameraFollowMode::OrbitTarget; + /// @brief World-space offset preserved by `KeepWorldOffset`. + hlsl::float64_t3 worldOffset = hlsl::float64_t3(0.0); + /// @brief Target-local offset preserved by `KeepLocalOffset`. + hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); +}; + +/// @brief Shared policy helpers for tracked-target follow. +/// +/// The helpers decide which follow modes lock the view, which ones move the +/// camera, how offsets are captured, and how a tracked target is translated into +/// a `CCameraGoal` that can then be applied through the shared goal solver. +struct CCameraFollowUtilities final +{ + /// @brief Return whether the follow mode rebuilds camera orientation toward the tracked target. + static inline constexpr bool cameraFollowModeLocksViewToTarget(const ECameraFollowMode mode) + { + switch (mode) + { + case ECameraFollowMode::OrbitTarget: + case ECameraFollowMode::LookAtTarget: + case ECameraFollowMode::KeepWorldOffset: + case ECameraFollowMode::KeepLocalOffset: + return true; + default: + return false; + } + } + + /// @brief Return whether the follow mode moves the camera world position together with the target. + static inline constexpr bool cameraFollowModeMovesCameraPosition(const ECameraFollowMode mode) + { + switch (mode) + { + case ECameraFollowMode::OrbitTarget: + case ECameraFollowMode::KeepWorldOffset: + case ECameraFollowMode::KeepLocalOffset: + return true; + default: + return false; + } + } + + /// @brief Return whether the follow mode preserves the current camera world position. + static inline constexpr bool cameraFollowModeKeepsCameraWorldPosition(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::LookAtTarget; + } + + /// @brief Return whether the follow mode interprets `worldOffset`. + static inline constexpr bool cameraFollowModeUsesWorldOffset(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::KeepWorldOffset; + } + + /// @brief Return whether the follow mode interprets `localOffset`. + static inline constexpr bool cameraFollowModeUsesLocalOffset(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::KeepLocalOffset; + } + + /// @brief Return whether the follow mode needs the tracked target local frame. + static inline constexpr bool cameraFollowModeUsesTrackedTargetLocalFrame(const ECameraFollowMode mode) + { + return mode == ECameraFollowMode::KeepLocalOffset; + } + + /// @brief Return whether the follow mode requires a captured offset before it can be replayed. + static inline constexpr bool cameraFollowModeUsesCapturedOffset(const ECameraFollowMode mode) + { + return cameraFollowModeUsesWorldOffset(mode) || cameraFollowModeUsesLocalOffset(mode); + } + + /// @brief Return the shared default follow mode for one camera kind. + static inline constexpr ECameraFollowMode getDefaultCameraFollowMode(const ICamera::CameraKind kind) + { + switch (kind) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::Arcball: + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::TopDown: + case ICamera::CameraKind::Isometric: + case ICamera::CameraKind::DollyZoom: + case ICamera::CameraKind::Path: + return ECameraFollowMode::OrbitTarget; + case ICamera::CameraKind::Chase: + case ICamera::CameraKind::Dolly: + return ECameraFollowMode::KeepLocalOffset; + default: + return ECameraFollowMode::Disabled; + } + } + + /// @brief Build the shared default follow configuration for one camera kind. + static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera::CameraKind kind) + { + const auto mode = getDefaultCameraFollowMode(kind); + return { + .enabled = mode != ECameraFollowMode::Disabled, + .mode = mode + }; + } + + /// @brief Build the shared default follow configuration for one concrete camera instance. + static inline constexpr SCameraFollowConfig makeDefaultFollowConfig(const ICamera* const camera) + { + return camera ? makeDefaultFollowConfig(camera->getKind()) : SCameraFollowConfig{}; + } + + /// @brief Transform a tracked-target local offset into world space. + static inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) + { + return hlsl::CCameraMathUtilities::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); + } + + /// @brief Project a world-space offset into the tracked target local frame. + static inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) + { + return hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); + } + + /// @brief Build a look-at orientation that points from `position` toward the tracked target. + static inline bool buildFollowLookAtOrientation( + const hlsl::float64_t3& position, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& preferredUp, + hlsl::camera_quaternion_t& outOrientation) + { + return hlsl::CCameraMathUtilities::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); + } + + /// @brief Capture world-space and target-local follow offsets from the current camera pose. + static inline bool captureFollowOffsetsFromCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + SCameraFollowConfig& ioConfig) + { + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + const auto& targetGimbal = trackedTarget.getGimbal(); + ioConfig.worldOffset = capture.goal.position - targetGimbal.getPosition(); + ioConfig.localOffset = projectFollowWorldOffsetToLocal(targetGimbal, ioConfig.worldOffset); + return true; + } + + /// @brief Measure the angular lock error between a camera forward axis and a tracked target. + static inline bool tryComputeFollowTargetLockMetrics( + const ICamera::CGimbal& cameraGimbal, + const CTrackedTarget& trackedTarget, + float& outAngleDeg, + double* outDistance = nullptr) + { + const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); + const auto targetDistance = hlsl::length(toTarget); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDistance) || targetDistance <= SCameraToolingThresholds::TinyScalarEpsilon) + return false; + + const auto forward = cameraGimbal.getZAxis(); + const auto forwardLength = hlsl::length(forward); + if (!hlsl::CCameraMathUtilities::isFiniteVec3(forward) || !hlsl::CCameraMathUtilities::isFiniteScalar(forwardLength) || forwardLength <= SCameraToolingThresholds::TinyScalarEpsilon) + return false; + + const auto forwardDirection = forward / forwardLength; + const auto targetDir = toTarget / targetDistance; + const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); + outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(outAngleDeg)) + return false; + + if (outDistance) + *outDistance = targetDistance; + return true; + } + + static inline bool tryBuildFollowPositionGoal( + ICamera* camera, + CCameraGoal& outGoal, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const hlsl::float64_t3& preferredUp) + { + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return CCameraGoalUtilities::buildCanonicalTargetRelativeGoalFromPosition(outGoal, targetPosition, position); + + outGoal.position = position; + return buildFollowLookAtOrientation(outGoal.position, targetPosition, preferredUp, outGoal.orientation) && CCameraGoalUtilities::isGoalFinite(outGoal); + } + + static inline bool tryBuildFollowGoal( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal& outGoal) + { + if (!camera || !config.enabled || config.mode == ECameraFollowMode::Disabled) + return false; + + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + outGoal = capture.goal; + + const auto& targetGimbal = trackedTarget.getGimbal(); + const auto targetPosition = targetGimbal.getPosition(); + + switch (config.mode) + { + case ECameraFollowMode::OrbitTarget: + { + if (!camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return false; + + if (outGoal.hasPathState) + { + return CCameraGoalUtilities::applyCanonicalPathGoalFields(outGoal, targetPosition, outGoal.pathState) && CCameraGoalUtilities::isGoalFinite(outGoal); + } + + const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; + if (!hasSphericalState) + return false; + + const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; + return CCameraGoalUtilities::applyCanonicalTargetRelativeGoal( + outGoal, + { + .target = targetPosition, + .orbitUv = outGoal.orbitUv, + .distance = orbitDistance + }); + } + + case ECameraFollowMode::LookAtTarget: + { + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, capture.goal.position, targetGimbal.getYAxis()); + } + + case ECameraFollowMode::KeepWorldOffset: + { + const auto position = targetPosition + config.worldOffset; + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); + } + + case ECameraFollowMode::KeepLocalOffset: + { + const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); + } + + default: + return false; + } + } + + static inline CCameraGoalSolver::SApplyResult applyFollowToCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal* outGoal = nullptr) + { + CCameraGoal goal = {}; + if (!tryBuildFollowGoal(solver, camera, trackedTarget, config, goal)) + return {}; + + if (outGoal) + *outGoal = goal; + + return solver.applyDetailed(camera, goal); + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_FOLLOW_UTILITIES_HPP_ + diff --git a/include/nbl/ext/Cameras/CCameraGoal.hpp b/include/nbl/ext/Cameras/CCameraGoal.hpp new file mode 100644 index 0000000000..bbf1eae3fb --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraGoal.hpp @@ -0,0 +1,409 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_GOAL_HPP_ +#define _C_CAMERA_GOAL_HPP_ + +#include +#include +#include +#include +#include + +#include "CCameraPathUtilities.hpp" +#include "CCameraTargetRelativeUtilities.hpp" +#include "ICamera.hpp" + +namespace nbl::core +{ + +/// @brief Typed transport object for camera state used by capture, comparison, presets, and playback. +struct CCameraGoal : SCameraRigPose +{ + /// @brief Camera kind that originally produced this goal. + ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; + /// @brief Capability mask captured from the source camera. + uint32_t sourceCapabilities = ICamera::None; + /// @brief Goal-state fragments that were valid on the source camera. + uint32_t sourceGoalStateMask = ICamera::GoalStateNone; + /// @brief Whether `targetPosition` is present in this goal. + bool hasTargetPosition = false; + /// @brief Tracked target position in world space. + hlsl::float64_t3 targetPosition = hlsl::float64_t3(0.0); + /// @brief Whether `distance` is present in this goal. + bool hasDistance = false; + /// @brief Explicit target-relative distance when present. + float distance = 0.f; + /// @brief Whether the canonical orbit state is present in this goal. + bool hasOrbitState = false; + /// @brief Canonical orbit yaw and pitch, expressed in radians. + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); + /// @brief Distance associated with `orbitUv` when the orbit state is present. + float orbitDistance = 0.f; + /// @brief Whether a typed path state is present in this goal. + bool hasPathState = false; + /// @brief Typed path state captured from or authored for a `Path Rig` camera. + ICamera::PathState pathState = {}; + /// @brief Whether a dynamic perspective state is present in this goal. + bool hasDynamicPerspectiveState = false; + /// @brief Typed dynamic perspective state captured from or authored for the source camera. + ICamera::DynamicPerspectiveState dynamicPerspectiveState = {}; +}; + +/// @brief Shared canonicalization, comparison, and conversion helpers for `CCameraGoal`. +struct CCameraGoalUtilities final +{ +public: + /// @brief Compute which typed goal-state fragments are required by the current goal payload. + static inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) + { + uint32_t mask = ICamera::GoalStateNone; + if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) + mask |= ICamera::GoalStateSphericalTarget; + if (target.hasDynamicPerspectiveState) + mask |= ICamera::GoalStateDynamicPerspective; + if (target.hasPathState) + mask |= ICamera::GoalStatePath; + return mask; + } + + /// @brief Overwrite the canonical target-relative fields of a goal from prebuilt state and pose data. + static inline void applyCanonicalTargetRelativeGoalFields( + CCameraGoal& goal, + const SCameraTargetRelativeState& state, + const SCameraTargetRelativePose& pose) + { + goal.position = pose.position; + goal.orientation = pose.orientation; + goal.hasTargetPosition = true; + goal.targetPosition = state.target; + goal.hasDistance = true; + goal.distance = static_cast(pose.appliedDistance); + goal.hasOrbitState = true; + goal.orbitUv = state.orbitUv; + goal.orbitDistance = static_cast(pose.appliedDistance); + } + + /// @brief Rebuild the canonical target-relative portion of a goal from typed target-relative state. + static inline bool applyCanonicalTargetRelativeGoal(CCameraGoal& goal, const SCameraTargetRelativeState& state) + { + SCameraTargetRelativePose pose = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, SCameraTargetRelativeTraits::MinDistance, SCameraTargetRelativeTraits::DefaultMaxDistance, pose)) + return false; + + applyCanonicalTargetRelativeGoalFields(goal, state, pose); + return true; + } + + /// @brief Rebuild the canonical pose and orbit fields of a goal from typed path state. + static inline bool applyCanonicalPathGoalFields( + CCameraGoal& goal, + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& pathState, + const SCameraPathLimits& limits = CCameraPathUtilities::makeDefaultPathLimits()) + { + SCameraCanonicalPathState canonicalPathState = {}; + if (!CCameraPathUtilities::tryBuildCanonicalPathState(targetPosition, pathState, limits, canonicalPathState)) + return false; + + goal.hasTargetPosition = true; + goal.targetPosition = targetPosition; + goal.hasPathState = true; + goal.pathState = pathState; + SCameraTargetRelativePose canonicalPose = {}; + canonicalPose.position = canonicalPathState.pose.position; + canonicalPose.orientation = canonicalPathState.pose.orientation; + canonicalPose.appliedDistance = canonicalPathState.pose.appliedDistance; + applyCanonicalTargetRelativeGoalFields( + goal, + canonicalPathState.targetRelative, + canonicalPose); + return true; + } + + /// @brief Rebuild the canonical pose fields from the goal's current spherical-target payload. + static inline bool applyCanonicalSphericalGoal(CCameraGoal& goal) + { + if (!(goal.hasTargetPosition && goal.hasOrbitState)) + return false; + if (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitDistance)) + return false; + + return applyCanonicalTargetRelativeGoal( + goal, + { + .target = goal.targetPosition, + .orbitUv = goal.orbitUv, + .distance = goal.orbitDistance + }); + } + + /// @brief Infer a target-relative goal from a target position and a desired camera position. + static inline bool buildCanonicalTargetRelativeGoalFromPosition( + CCameraGoal& goal, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position) + { + SCameraTargetRelativeState state = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( + targetPosition, + position, + SCameraTargetRelativeTraits::MinDistance, + SCameraTargetRelativeTraits::DefaultMaxDistance, + state)) + { + return false; + } + + return applyCanonicalTargetRelativeGoal(goal, state); + } + + /// @brief Resolve the effective target-relative state of a goal against the current camera state. + static inline bool tryResolveCanonicalTargetRelativeState( + const CCameraGoal& goal, + const ICamera::SphericalTargetState& currentState, + SCameraTargetRelativeState& outState) + { + outState.target = goal.hasTargetPosition ? goal.targetPosition : currentState.target; + outState.orbitUv = currentState.orbitUv; + outState.distance = currentState.distance; + + if (goal.hasOrbitState) + { + outState.orbitUv = goal.orbitUv; + outState.distance = goal.orbitDistance; + } + else + { + SCameraTargetRelativeState resolvedState = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( + outState.target, + goal.position, + currentState.minDistance, + currentState.maxDistance, + resolvedState)) + { + return false; + } + + outState.orbitUv = resolvedState.orbitUv; + outState.distance = resolvedState.distance; + } + + if (goal.hasDistance && !goal.hasOrbitState) + outState.distance = goal.distance; + + outState.distance = std::clamp(outState.distance, currentState.minDistance, currentState.maxDistance); + return true; + } + + /// @brief Rebuild the canonical pose fields from the goal's current path payload. + static inline bool applyCanonicalPathGoal(CCameraGoal& goal) + { + if (!(goal.hasPathState && goal.hasTargetPosition)) + return false; + if (!CCameraPathUtilities::isPathStateFinite(goal.pathState)) + return false; + return applyCanonicalPathGoalFields(goal, goal.targetPosition, goal.pathState); + } + + /// @brief Canonicalize whichever typed state fragments are currently present on the goal. + static inline bool applyCanonicalGoalState(CCameraGoal& goal) + { + if (goal.hasPathState) + return applyCanonicalPathGoal(goal); + + if (goal.hasTargetPosition && goal.hasOrbitState) + return applyCanonicalSphericalGoal(goal); + + return true; + } + + /// @brief Return a value-copied goal after canonicalizing its typed state. + static inline CCameraGoal canonicalizeGoal(CCameraGoal goal) + { + applyCanonicalGoalState(goal); + return goal; + } + + /// @brief Check whether every populated scalar and vector stored by the goal is finite. + static inline bool isGoalFinite(const CCameraGoal& goal) + { + if (!hlsl::CCameraMathUtilities::isFiniteVec3(goal.position) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(goal.orientation)) + return false; + if (goal.hasTargetPosition && !hlsl::CCameraMathUtilities::isFiniteVec3(goal.targetPosition)) + return false; + if (goal.hasDistance && !hlsl::CCameraMathUtilities::isFiniteScalar(goal.distance)) + return false; + if (goal.hasOrbitState && (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitUv.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.orbitDistance))) + return false; + if (goal.hasPathState && !CCameraPathUtilities::isPathStateFinite(goal.pathState)) + return false; + if (goal.hasDynamicPerspectiveState && + (!hlsl::CCameraMathUtilities::isFiniteScalar(goal.dynamicPerspectiveState.baseFov) || !hlsl::CCameraMathUtilities::isFiniteScalar(goal.dynamicPerspectiveState.referenceDistance))) + return false; + return true; + } + + /// @brief Compare two goals using caller-provided pose and scalar tolerances. + static inline bool compareGoals(const CCameraGoal& actual, const CCameraGoal& expected, + const double posEps, const double rotEpsDeg, const double scalarEps) + { + hlsl::SCameraPoseDelta poseDelta = {}; + if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta)) + return false; + if (poseDelta.position > posEps || poseDelta.rotationDeg > rotEpsDeg) + return false; + + if (expected.hasTargetPosition) + { + if (!actual.hasTargetPosition || !hlsl::CCameraMathUtilities::nearlyEqualVec3(actual.targetPosition, expected.targetPosition, scalarEps)) + return false; + } + if (expected.hasDistance) + { + if (!actual.hasDistance || !hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.distance), static_cast(expected.distance), scalarEps)) + return false; + } + if (expected.hasOrbitState) + { + if (!actual.hasOrbitState) + return false; + if (hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(expected.orbitUv.x, actual.orbitUv.x) > rotEpsDeg) + return false; + if (hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(expected.orbitUv.y, actual.orbitUv.y) > rotEpsDeg) + return false; + if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.orbitDistance), static_cast(expected.orbitDistance), scalarEps)) + return false; + } + if (expected.hasPathState) + { + if (!actual.hasPathState) + return false; + if (!CCameraPathUtilities::pathStatesNearlyEqual(actual.pathState, expected.pathState, CCameraPathUtilities::makePathComparisonThresholds(rotEpsDeg, scalarEps))) + return false; + } + if (expected.hasDynamicPerspectiveState) + { + if (!actual.hasDynamicPerspectiveState) + return false; + if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.baseFov), static_cast(expected.dynamicPerspectiveState.baseFov), scalarEps)) + return false; + if (!hlsl::CCameraMathUtilities::nearlyEqualScalar(static_cast(actual.dynamicPerspectiveState.referenceDistance), static_cast(expected.dynamicPerspectiveState.referenceDistance), scalarEps)) + return false; + } + + return true; + } + + static inline std::string describeGoalMismatch(const CCameraGoal& actual, const CCameraGoal& expected) + { + std::ostringstream oss; + hlsl::SCameraPoseDelta poseDelta = {}; + const bool hasPoseDelta = hlsl::CCameraMathUtilities::tryComputePoseDelta(actual.position, actual.orientation, expected.position, expected.orientation, poseDelta); + const auto currentOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(actual.orientation); + const auto expectedOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(expected.orientation); + oss << "pos_delta=" << (hasPoseDelta ? poseDelta.position : std::numeric_limits::quiet_NaN()) + << " rot_delta_deg=" << (hasPoseDelta ? poseDelta.rotationDeg : std::numeric_limits::quiet_NaN()) + << " current_pos=(" << actual.position.x << "," << actual.position.y << "," << actual.position.z << ")" + << " expected_pos=(" << expected.position.x << "," << expected.position.y << "," << expected.position.z << ")" + << " current_quat=(" << currentOrientation.data.x << "," << currentOrientation.data.y << "," << currentOrientation.data.z << "," << currentOrientation.data.w << ")" + << " expected_quat=(" << expectedOrientation.data.x << "," << expectedOrientation.data.y << "," << expectedOrientation.data.z << "," << expectedOrientation.data.w << ")"; + + if (actual.hasTargetPosition) + { + oss << " target=(" << actual.targetPosition.x << "," << actual.targetPosition.y << "," << actual.targetPosition.z << ")"; + if (actual.hasDistance) + oss << " distance=" << actual.distance; + if (actual.hasOrbitState) + oss << " orbit_u=" << actual.orbitUv.x << " orbit_v=" << actual.orbitUv.y; + } + else if (expected.hasTargetPosition || expected.hasDistance || expected.hasOrbitState) + { + oss << " spherical_state=unavailable"; + } + if (actual.hasPathState) + { + oss << " path_s=" << actual.pathState.s + << " path_u=" << actual.pathState.u + << " path_v=" << actual.pathState.v + << " path_roll=" << actual.pathState.roll; + } + else if (expected.hasPathState) + { + oss << " path_state=unavailable"; + } + + if (actual.hasDynamicPerspectiveState) + { + oss << " dynamic_base_fov=" << actual.dynamicPerspectiveState.baseFov + << " dynamic_reference_distance=" << actual.dynamicPerspectiveState.referenceDistance; + } + else if (expected.hasDynamicPerspectiveState) + { + oss << " dynamic_perspective_state=unavailable"; + } + + return oss.str(); + } + + static inline CCameraGoal blendGoals(const CCameraGoal& a, const CCameraGoal& b, double alpha) + { + CCameraGoal blended; + blended.position = a.position + (b.position - a.position) * alpha; + blended.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(a.orientation, b.orientation, static_cast(alpha)); + blended.sourceKind = (a.sourceKind == b.sourceKind) ? a.sourceKind : ICamera::CameraKind::Unknown; + blended.sourceCapabilities = a.sourceCapabilities & b.sourceCapabilities; + blended.sourceGoalStateMask = a.sourceGoalStateMask | b.sourceGoalStateMask; + blended.hasTargetPosition = a.hasTargetPosition || b.hasTargetPosition; + if (blended.hasTargetPosition) + { + const auto ta = a.hasTargetPosition ? a.targetPosition : b.targetPosition; + const auto tb = b.hasTargetPosition ? b.targetPosition : a.targetPosition; + blended.targetPosition = ta + (tb - ta) * alpha; + } + blended.hasDistance = a.hasDistance || b.hasDistance; + if (blended.hasDistance) + { + const float da = a.hasDistance ? a.distance : b.distance; + const float db = b.hasDistance ? b.distance : a.distance; + blended.distance = da + (db - da) * static_cast(alpha); + } + blended.hasOrbitState = a.hasOrbitState || b.hasOrbitState; + if (blended.hasOrbitState) + { + const auto orbitUvA = a.hasOrbitState ? a.orbitUv : b.orbitUv; + const auto orbitUvB = b.hasOrbitState ? b.orbitUv : a.orbitUv; + const float da = a.hasOrbitState ? a.orbitDistance : b.orbitDistance; + const float db = b.hasOrbitState ? b.orbitDistance : a.orbitDistance; + + blended.orbitUv = hlsl::float64_t2( + hlsl::CCameraMathUtilities::lerpWrappedAngleRad(orbitUvA.x, orbitUvB.x, alpha), + hlsl::CCameraMathUtilities::lerpWrappedAngleRad(orbitUvA.y, orbitUvB.y, alpha)); + blended.orbitDistance = da + (db - da) * static_cast(alpha); + } + blended.hasDynamicPerspectiveState = a.hasDynamicPerspectiveState || b.hasDynamicPerspectiveState; + if (blended.hasDynamicPerspectiveState) + { + const auto dynamicA = a.hasDynamicPerspectiveState ? a.dynamicPerspectiveState : b.dynamicPerspectiveState; + const auto dynamicB = b.hasDynamicPerspectiveState ? b.dynamicPerspectiveState : a.dynamicPerspectiveState; + blended.dynamicPerspectiveState.baseFov = dynamicA.baseFov + (dynamicB.baseFov - dynamicA.baseFov) * static_cast(alpha); + blended.dynamicPerspectiveState.referenceDistance = + dynamicA.referenceDistance + (dynamicB.referenceDistance - dynamicA.referenceDistance) * static_cast(alpha); + } + blended.hasPathState = a.hasPathState || b.hasPathState; + if (blended.hasPathState) + { + const auto pathA = a.hasPathState ? a.pathState : b.pathState; + const auto pathB = b.hasPathState ? b.pathState : a.pathState; + blended.pathState = CCameraPathUtilities::blendPathStates(pathA, pathB, alpha); + } + return canonicalizeGoal(blended); + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_GOAL_HPP_ + diff --git a/include/nbl/ext/Cameras/CCameraGoalAnalysis.hpp b/include/nbl/ext/Cameras/CCameraGoalAnalysis.hpp new file mode 100644 index 0000000000..4653cc3181 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraGoalAnalysis.hpp @@ -0,0 +1,89 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_GOAL_ANALYSIS_HPP_ +#define _C_CAMERA_GOAL_ANALYSIS_HPP_ + +#include "CCameraPreset.hpp" +#include "CCameraGoalSolver.hpp" + +namespace nbl::core +{ + +/// @brief Reusable typed answer for `goal/preset -> camera` compatibility checks. +struct SCameraGoalApplyAnalysis +{ + CCameraGoal goal = {}; + CCameraGoalSolver::SCompatibilityResult compatibility = {}; + bool hasCamera = false; + bool finiteGoal = false; + bool canApply = false; + + inline bool exact() const + { + return compatibility.exact; + } + + inline bool dropsGoalState() const + { + return compatibility.missingGoalStateMask != ICamera::GoalStateNone; + } + + inline bool usesSharedStateOnly() const + { + return !compatibility.sameKind && goal.sourceKind != ICamera::CameraKind::Unknown && !dropsGoalState(); + } + + inline bool isMeaningfulApply() const + { + return canApply; + } +}; + +/// @brief Reusable typed answer for `camera -> goal` capture viability. +struct SCameraCaptureAnalysis +{ + CCameraGoal goal = {}; + bool hasCamera = false; + bool capturedGoal = false; + bool finiteGoal = false; + bool canCapture = false; +}; + +struct CCameraGoalAnalysisUtilities final +{ +public: + static inline SCameraGoalApplyAnalysis analyzeGoalApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraGoal& goal) + { + SCameraGoalApplyAnalysis analysis; + analysis.goal = CCameraGoalUtilities::canonicalizeGoal(goal); + analysis.hasCamera = camera != nullptr; + analysis.finiteGoal = CCameraGoalUtilities::isGoalFinite(analysis.goal); + analysis.canApply = analysis.hasCamera && analysis.finiteGoal; + if (analysis.hasCamera) + analysis.compatibility = solver.analyzeCompatibility(camera, analysis.goal); + return analysis; + } + + static inline SCameraGoalApplyAnalysis analyzePresetApply(const CCameraGoalSolver& solver, const ICamera* camera, const CCameraPreset& preset) + { + return analyzeGoalApply(solver, camera, CCameraPresetUtilities::makeGoalFromPreset(preset)); + } + + static inline SCameraCaptureAnalysis analyzeCameraCapture(const CCameraGoalSolver& solver, ICamera* camera) + { + SCameraCaptureAnalysis analysis; + const auto capture = solver.captureDetailed(camera); + analysis.goal = capture.goal; + analysis.hasCamera = capture.hasCamera; + analysis.capturedGoal = capture.captured; + analysis.finiteGoal = capture.finiteGoal; + analysis.canCapture = capture.canUseGoal(); + return analysis; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_GOAL_ANALYSIS_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraGoalSolver.hpp b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp new file mode 100644 index 0000000000..91dad92191 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp @@ -0,0 +1,646 @@ +#ifndef _C_CAMERA_GOAL_SOLVER_HPP_ +#define _C_CAMERA_GOAL_SOLVER_HPP_ + +#include +#include +#include +#include +#include + +#include "CCameraGoal.hpp" +#include "CCameraTargetRelativeUtilities.hpp" +#include "CCameraVirtualEventUtilities.hpp" + +namespace nbl::core +{ + +/// @brief Goal capture, compatibility analysis, and goal application helper. +/// +/// The solver captures canonical state into `CCameraGoal`, compares a goal +/// against one target camera, applies typed fragments directly when the camera +/// exposes them, and builds virtual-event replay when a typed fragment must be +/// approximated through `manipulate(...)`. +class CCameraGoalSolver +{ +public: + /// @brief Detailed result returned by one goal-capture attempt. + struct SCaptureResult + { + bool hasCamera = false; + bool captured = false; + bool finiteGoal = false; + CCameraGoal goal = {}; + + inline bool canUseGoal() const + { + return hasCamera && captured && finiteGoal; + } + }; + + /// @brief Compatibility of a goal with a target camera kind and state mask. + struct SCompatibilityResult + { + bool sameKind = false; + bool exact = false; + uint32_t requiredGoalStateMask = ICamera::GoalStateNone; + uint32_t supportedGoalStateMask = ICamera::GoalStateNone; + uint32_t missingGoalStateMask = ICamera::GoalStateNone; + }; + + /// @brief Outcome of one goal-application attempt. + struct SApplyResult + { + enum class EStatus : uint8_t + { + Unsupported, + Failed, + AlreadySatisfied, + AppliedAbsoluteOnly, + AppliedVirtualEvents, + AppliedAbsoluteAndVirtualEvents + }; + + enum EIssue : uint32_t + { + NoIssue = 0u, + UsedAbsolutePoseFallback = 1u << 0, + MissingSphericalTargetState = 1u << 1, + MissingPathState = 1u << 2, + MissingDynamicPerspectiveState = 1u << 3, + VirtualEventReplayFailed = 1u << 4 + }; + + EStatus status = EStatus::Unsupported; + bool exact = false; + uint32_t eventCount = 0u; + uint32_t issues = NoIssue; + + inline bool succeeded() const + { + return status != EStatus::Unsupported && status != EStatus::Failed; + } + + inline bool changed() const + { + return status == EStatus::AppliedAbsoluteOnly || + status == EStatus::AppliedVirtualEvents || + status == EStatus::AppliedAbsoluteAndVirtualEvents; + } + + inline bool approximate() const + { + return succeeded() && !exact; + } + + inline bool hasIssue(EIssue issue) const + { + return (issues & issue) == issue; + } + }; + + bool buildEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const + { + out.clear(); + if (!camera) + return false; + + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); + + if (camera->hasCapability(ICamera::SphericalTarget)) + return buildSphericalEvents(camera, canonicalTarget, out); + + return buildFreeEvents(camera, canonicalTarget, out); + } + + bool capture(ICamera* camera, CCameraGoal& out) const + { + out = {}; + if (!camera) + return false; + + const ICamera::CGimbal& gimbal = camera->getGimbal(); + out.position = hlsl::float64_t3(gimbal.getPosition()); + out.orientation = gimbal.getOrientation(); + out.sourceKind = camera->getKind(); + out.sourceCapabilities = camera->getCapabilities(); + out.sourceGoalStateMask = camera->getGoalStateMask(); + + ICamera::SphericalTargetState sphericalState; + if (camera->tryGetSphericalTargetState(sphericalState)) + { + out.targetPosition = sphericalState.target; + out.hasTargetPosition = true; + out.distance = sphericalState.distance; + out.hasDistance = true; + out.orbitDistance = sphericalState.distance; + out.orbitUv = sphericalState.orbitUv; + out.hasOrbitState = true; + } + + ICamera::DynamicPerspectiveState dynamicState; + if (camera->tryGetDynamicPerspectiveState(dynamicState)) + { + out.hasDynamicPerspectiveState = true; + out.dynamicPerspectiveState = dynamicState; + } + + ICamera::PathState pathState; + if (camera->tryGetPathState(pathState)) + { + out.hasPathState = true; + out.pathState = pathState; + } + + out = CCameraGoalUtilities::canonicalizeGoal(out); + return true; + } + + SCaptureResult captureDetailed(ICamera* camera) const + { + SCaptureResult result; + result.hasCamera = camera != nullptr; + if (!result.hasCamera) + return result; + + result.captured = capture(camera, result.goal); + result.finiteGoal = result.captured && CCameraGoalUtilities::isGoalFinite(result.goal); + return result; + } + + SCompatibilityResult analyzeCompatibility(const ICamera* camera, const CCameraGoal& target) const + { + SCompatibilityResult result; + if (!camera) + return result; + + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); + result.sameKind = canonicalTarget.sourceKind == ICamera::CameraKind::Unknown || canonicalTarget.sourceKind == camera->getKind(); + result.supportedGoalStateMask = camera->getGoalStateMask(); + result.requiredGoalStateMask = CCameraGoalUtilities::getRequiredGoalStateMask(canonicalTarget); + result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; + result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; + return result; + } + + SApplyResult applyDetailed(ICamera* camera, const CCameraGoal& target) const + { + SApplyResult result; + if (!camera) + return result; + + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); + + bool exact = true; + bool absoluteChanged = false; + + if (!camera->hasCapability(ICamera::SphericalTarget)) + { + bool poseChanged = false; + bool poseExact = false; + if (tryApplyAbsoluteReferencePose(camera, canonicalTarget, poseChanged, poseExact)) + { + result.issues |= SApplyResult::UsedAbsolutePoseFallback; + absoluteChanged = absoluteChanged || poseChanged; + if (poseExact && !canonicalTarget.hasDynamicPerspectiveState) + { + result.status = poseChanged ? + SApplyResult::EStatus::AppliedAbsoluteOnly : + SApplyResult::EStatus::AlreadySatisfied; + result.exact = true; + return result; + } + } + } + + if (canonicalTarget.hasTargetPosition) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + result.issues |= SApplyResult::MissingSphericalTargetState; + exact = false; + } + else + { + const auto beforeTarget = beforeState.target; + if (!camera->trySetSphericalTarget(canonicalTarget.targetPosition)) + { + result.issues |= SApplyResult::MissingSphericalTargetState; + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + result.issues |= SApplyResult::MissingSphericalTargetState; + exact = false; + } + else + { + absoluteChanged = afterState.target != beforeTarget; + exact = exact && afterState.target == canonicalTarget.targetPosition; + } + } + } + } + + if (canonicalTarget.hasDistance || canonicalTarget.hasOrbitState) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + result.issues |= SApplyResult::MissingSphericalTargetState; + exact = false; + } + else + { + const float desiredDistance = canonicalTarget.hasOrbitState ? canonicalTarget.orbitDistance : canonicalTarget.distance; + const float beforeDistance = beforeState.distance; + if (!camera->trySetSphericalDistance(desiredDistance)) + { + result.issues |= SApplyResult::MissingSphericalTargetState; + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + result.issues |= SApplyResult::MissingSphericalTargetState; + exact = false; + } + else + { + absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; + exact = exact && hlsl::abs(static_cast(afterState.distance - desiredDistance)) <= SCameraToolingThresholds::ScalarTolerance; + } + } + } + } + + if (canonicalTarget.hasPathState) + { + ICamera::PathState beforeState; + if (!camera->tryGetPathState(beforeState)) + { + result.issues |= SApplyResult::MissingPathState; + exact = false; + } + else if (!camera->trySetPathState(canonicalTarget.pathState)) + { + result.issues |= SApplyResult::MissingPathState; + exact = false; + } + else + { + ICamera::PathState afterState; + if (!camera->tryGetPathState(afterState)) + { + result.issues |= SApplyResult::MissingPathState; + exact = false; + } + else + { + const auto thresholds = SCameraPathDefaults::ComparisonThresholds; + const bool pathChanged = CCameraPathUtilities::pathStatesChanged(beforeState, afterState, thresholds); + const bool pathExact = CCameraPathUtilities::pathStatesNearlyEqual(afterState, canonicalTarget.pathState, thresholds); + + absoluteChanged = absoluteChanged || pathChanged; + exact = exact && pathExact; + } + } + } + + if (canonicalTarget.hasDynamicPerspectiveState) + { + ICamera::DynamicPerspectiveState beforeState; + if (!camera->tryGetDynamicPerspectiveState(beforeState)) + { + result.issues |= SApplyResult::MissingDynamicPerspectiveState; + exact = false; + } + else if (!camera->trySetDynamicPerspectiveState(canonicalTarget.dynamicPerspectiveState)) + { + result.issues |= SApplyResult::MissingDynamicPerspectiveState; + exact = false; + } + else + { + ICamera::DynamicPerspectiveState afterState; + if (!camera->tryGetDynamicPerspectiveState(afterState)) + { + result.issues |= SApplyResult::MissingDynamicPerspectiveState; + exact = false; + } + else + { + const bool dynamicChanged = !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) || + !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); + const bool dynamicExact = hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); + + absoluteChanged = absoluteChanged || dynamicChanged; + exact = exact && dynamicExact; + } + } + } + + std::vector events; + buildEvents(camera, canonicalTarget, events); + result.eventCount = static_cast(events.size()); + result.exact = exact; + + if (events.empty()) + { + if (absoluteChanged) + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + else if (exact) + result.status = SApplyResult::EStatus::AlreadySatisfied; + return result; + } + + if (camera->manipulate({ events.data(), events.size() })) + { + result.status = absoluteChanged ? + SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents : + SApplyResult::EStatus::AppliedVirtualEvents; + return result; + } + + if (absoluteChanged) + { + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + result.exact = false; + return result; + } + + result.issues |= SApplyResult::VirtualEventReplayFailed; + result.status = SApplyResult::EStatus::Failed; + result.exact = false; + return result; + } + + bool apply(ICamera* camera, const CCameraGoal& target) const + { + return applyDetailed(camera, target).succeeded(); + } + +private: + struct SGoalSolverDefaults final + { + static constexpr double UnitScale = 1.0; + static inline const hlsl::float64_t3 UnitAxisDenominator = hlsl::float64_t3(UnitScale); + static inline const hlsl::float64_t3 ScalarToleranceVec = hlsl::float64_t3(SCameraToolingThresholds::ScalarTolerance); + static inline const hlsl::float64_t3 AngularToleranceDegVec = hlsl::float64_t3(SCameraToolingThresholds::DefaultAngularToleranceDeg); + }; + + inline void appendYawPitchRollEvents( + std::vector& events, + const hlsl::float64_t3& eulerRadians, + const double denominator, + const bool includeRoll = true) const + { + static constexpr std::array RotationBindings = {{ + { CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown }, + { CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft }, + { CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft } + }}; + + auto tolerances = SGoalSolverDefaults::AngularToleranceDegVec; + if (!includeRoll) + tolerances.z = std::numeric_limits::infinity(); + + CCameraVirtualEventUtilities::appendAngularAxisEvents( + events, + eulerRadians, + hlsl::float64_t3(denominator), + tolerances, + RotationBindings); + } + + inline void appendPathDeltaEvents( + std::vector& events, + const SCameraPathDelta& delta, + const double moveDenominator, + const double rotationDenominator) const + { + CCameraPathUtilities::appendPathDeltaEvents( + events, + delta, + moveDenominator, + rotationDenominator, + SCameraPathDefaults::ExactComparisonThresholds); + } + + inline double getMoveMagnitudeDenominator(const ICamera* camera) const + { + const double moveScale = camera->getMoveSpeedScale(); + return camera->getUnscaledVirtualTranslationMagnitude() * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); + } + + inline double getRotationMagnitudeDenominator(const ICamera* camera) const + { + const double rotationScale = camera->getRotationSpeedScale(); + return rotationScale == 0.0 ? SGoalSolverDefaults::UnitScale : rotationScale; + } + + inline bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const + { + outPositionDelta = 0.0; + outRotationDeltaDeg = 0.0; + if (!camera) + return false; + + const ICamera::CGimbal& gimbal = camera->getGimbal(); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(gimbal.getPosition(), gimbal.getOrientation(), target.position, target.orientation, poseDelta)) + return false; + + outPositionDelta = poseDelta.position; + outRotationDeltaDeg = poseDelta.rotationDeg; + return true; + } + + inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CCameraGoal& target, bool& outChanged, bool& outExact) const + { + outChanged = false; + outExact = false; + if (!camera) + return false; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Free: + case ICamera::CameraKind::FPS: + break; + default: + return false; + } + + double beforePosDelta = 0.0; + double beforeRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) + return false; + + if (beforePosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && beforeRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg) + { + outExact = true; + return true; + } + + const auto targetFrame = hlsl::CCameraMathUtilities::composeTransformMatrix(target.position, target.orientation); + + camera->manipulate({}, &targetFrame); + + double afterPosDelta = 0.0; + double afterRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) + return false; + + outChanged = !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) || + !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)); + outExact = afterPosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && afterRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg; + return true; + } + + inline bool buildTargetRelativeEvents( + ICamera* camera, + const ICamera::SphericalTargetState& sphericalState, + const SCameraTargetRelativeState& goal, + std::vector& out, + const SCameraTargetRelativeEventPolicy& policy) const + { + const auto delta = CCameraTargetRelativeUtilities::buildTargetRelativeDelta(sphericalState, goal); + CCameraTargetRelativeUtilities::appendTargetRelativeDeltaEvents( + out, + delta, + policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), + SCameraToolingThresholds::DefaultAngularToleranceDeg, + camera->getUnscaledVirtualTranslationMagnitude(), + SCameraToolingThresholds::ScalarTolerance, + policy); + return !out.empty(); + } + + inline bool buildPathEvents(ICamera* camera, const CCameraGoal& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const + { + if (!camera) + return false; + + const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; + ICamera::PathState currentState = {}; + const ICamera::PathState* currentStateOverride = camera->tryGetPathState(currentState) ? ¤tState : nullptr; + ICamera::PathStateLimits pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); + camera->tryGetPathStateLimits(pathLimits); + SCameraPathStateTransition transition = {}; + if (!CCameraPathUtilities::tryBuildPathStateTransition( + effectiveTarget, + camera->getGimbal().getPosition(), + target.position, + pathLimits, + currentStateOverride, + target.hasPathState ? &target.pathState : nullptr, + transition)) + { + return false; + } + + const auto moveDenom = getMoveMagnitudeDenominator(camera); + const auto rotationDenom = getRotationMagnitudeDenominator(camera); + appendPathDeltaEvents(out, transition.delta, moveDenom, rotationDenom); + return !out.empty(); + } + + inline bool buildSphericalEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const + { + ICamera::SphericalTargetState sphericalState; + if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) + return false; + + if (camera->getKind() == ICamera::CameraKind::Path) + return buildPathEvents(camera, target, sphericalState, out); + + SCameraTargetRelativeState goal; + if (!CCameraGoalUtilities::tryResolveCanonicalTargetRelativeState(target, sphericalState, goal)) + return false; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::DollyZoom: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); + + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::Arcball: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::RotateDistancePolicy); + + case ICamera::CameraKind::TopDown: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::TopDownPolicy); + + case ICamera::CameraKind::Isometric: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::IsometricPolicy); + + case ICamera::CameraKind::Dolly: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::DollyPolicy); + + case ICamera::CameraKind::Chase: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::ChasePolicy); + + default: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); + } + } + + inline bool buildFreeEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const + { + const ICamera::CGimbal& gimbal = camera->getGimbal(); + const hlsl::float64_t3 currentPos = gimbal.getPosition(); + const hlsl::float64_t3 deltaWorld = target.position - currentPos; + CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents( + out, + gimbal.getOrientation(), + deltaWorld, + SGoalSolverDefaults::UnitAxisDenominator, + SGoalSolverDefaults::ScalarToleranceVec); + + switch (camera->getKind()) + { + case ICamera::CameraKind::FPS: + { + const hlsl::float64_t2 currentPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(gimbal.getOrientation()); + const hlsl::float64_t2 targetPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(target.orientation); + + const double rotScale = camera->getRotationSpeedScale(); + const double invScale = rotScale == 0.0 ? SGoalSolverDefaults::UnitScale : (SGoalSolverDefaults::UnitScale / rotScale); + + appendYawPitchRollEvents( + out, + hlsl::float64_t3( + hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.x - currentPitchYaw.x) * invScale, + hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.y - currentPitchYaw.y) * invScale, + 0.0), + SGoalSolverDefaults::UnitScale, + false); + } break; + + case ICamera::CameraKind::Free: + { + appendYawPitchRollEvents( + out, + hlsl::CCameraMathUtilities::getOrientationDeltaEulerRadiansYXZ(gimbal.getOrientation(), target.orientation), + SGoalSolverDefaults::UnitScale); + } break; + + default: + break; + } + + return !out.empty(); + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_GOAL_SOLVER_HPP_ + diff --git a/include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp b/include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp new file mode 100644 index 0000000000..3529925e6d --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp @@ -0,0 +1,561 @@ +#ifndef _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ +#define _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ + +#include +#include + +#include "CCameraKindUtilities.hpp" +#include "ICamera.hpp" +#include "IGimbalBindingLayout.hpp" + +namespace nbl::ui +{ + +/// @brief Reusable keyboard, mouse, and ImGuizmo binding preset grouped for one camera kind. +struct SCameraInputBindingPreset +{ + IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; + IGimbalBindingLayout::mouse_to_virtual_events_t mouse; + IGimbalBindingLayout::imguizmo_to_virtual_events_t imguizmo; +}; + +/// @brief Shared physical input bundles reused by default presets and smoke probing. +struct SCameraInputBindingPhysicalGroups final +{ + static inline constexpr std::array KeyboardWasdCodes = { + ui::E_KEY_CODE::EKC_W, + ui::E_KEY_CODE::EKC_S, + ui::E_KEY_CODE::EKC_A, + ui::E_KEY_CODE::EKC_D + }; + static inline constexpr std::array KeyboardQeCodes = { + ui::E_KEY_CODE::EKC_Q, + ui::E_KEY_CODE::EKC_E + }; + static inline constexpr std::array KeyboardIjklCodes = { + ui::E_KEY_CODE::EKC_I, + ui::E_KEY_CODE::EKC_K, + ui::E_KEY_CODE::EKC_J, + ui::E_KEY_CODE::EKC_L + }; + static inline constexpr auto KeyboardProbeMoveCodes = KeyboardWasdCodes; + static inline constexpr auto KeyboardProbeLookCodes = KeyboardIjklCodes; + static inline constexpr std::array KeyboardProbeExtraCodes = { + ui::E_KEY_CODE::EKC_U, + ui::E_KEY_CODE::EKC_O + }; + static inline constexpr std::array KeyboardProbeCodes = { + ui::E_KEY_CODE::EKC_W, + ui::E_KEY_CODE::EKC_S, + ui::E_KEY_CODE::EKC_A, + ui::E_KEY_CODE::EKC_D, + ui::E_KEY_CODE::EKC_Q, + ui::E_KEY_CODE::EKC_E, + ui::E_KEY_CODE::EKC_I, + ui::E_KEY_CODE::EKC_K, + ui::E_KEY_CODE::EKC_J, + ui::E_KEY_CODE::EKC_L, + ui::E_KEY_CODE::EKC_U, + ui::E_KEY_CODE::EKC_O + }; + static inline constexpr std::array RelativeMouseCodes = { + ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_X, + ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, + ui::E_MOUSE_CODE::EMC_RELATIVE_POSITIVE_MOVEMENT_Y, + ui::E_MOUSE_CODE::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y + }; + static inline constexpr std::array PositiveScrollCodes = { + ui::E_MOUSE_CODE::EMC_VERTICAL_POSITIVE_SCROLL, + ui::E_MOUSE_CODE::EMC_HORIZONTAL_POSITIVE_SCROLL + }; + static inline constexpr std::array NegativeScrollCodes = { + ui::E_MOUSE_CODE::EMC_VERTICAL_NEGATIVE_SCROLL, + ui::E_MOUSE_CODE::EMC_HORIZONTAL_NEGATIVE_SCROLL + }; +}; + +struct CCameraInputBindingUtilities final +{ +public: + /// @brief Default gains used by the shared binding presets. + /// + /// These gains convert producer-local units into virtual-event magnitudes: + /// held keys into units per second, mouse deltas into units per mouse + /// step, scroll into units per scroll step, and ImGuizmo deltas into + /// virtual translation, rotation, or scale magnitudes. + struct SInputMagnitudeDefaults final + { + static inline constexpr double KeyboardHeldUnitsPerSecond = 1000.0; + static inline constexpr double RelativeMouseUnitsPerStep = 1.0; + static inline constexpr double ScrollUnitsPerStep = 1.0; + static inline constexpr double ImguizmoTranslationUnitsPerWorldUnit = 1.0; + static inline constexpr double ImguizmoRotationUnitsPerRadian = 1.0; + static inline constexpr double ImguizmoScaleUnitsPerFactor = 1.0; + }; + + static inline bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) + { + return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); + } + + static inline bool hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) + { + return containsBindingForAnyCodeGroups( + mousePreset, + SCameraInputBindingPhysicalGroups::PositiveScrollCodes, + SCameraInputBindingPhysicalGroups::NegativeScrollCodes); + } + + static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera::CameraKind kind) + { + return interactionBindingPresetForKind(kind).keyboard; + } + + static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) + { + return getDefaultCameraKeyboardMappingPreset(camera.getKind()); + } + + static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera::CameraKind kind) + { + return interactionBindingPresetForKind(kind).mouse; + } + + static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) + { + return getDefaultCameraMouseMappingPreset(camera.getKind()); + } + + static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const uint32_t allowedVirtualEvents) + { + return makeImguizmoPreset(allowedVirtualEvents); + } + + static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) + { + return buildDefaultCameraImguizmoMappingPreset(camera.getAllowedVirtualEvents()); + } + + static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera::CameraKind kind, const uint32_t allowedVirtualEvents) + { + SCameraInputBindingPreset preset; + preset.keyboard = getDefaultCameraKeyboardMappingPreset(kind); + preset.mouse = getDefaultCameraMouseMappingPreset(kind); + preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(allowedVirtualEvents); + return preset; + } + + static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) + { + return buildDefaultCameraInputBindingPreset(camera.getKind(), camera.getAllowedVirtualEvents()); + } + + static inline void applyDefaultCameraInputBindingPreset( + IGimbalBindingLayout& layout, + const core::ICamera::CameraKind kind, + const uint32_t allowedVirtualEvents) + { + const auto preset = buildDefaultCameraInputBindingPreset(kind, allowedVirtualEvents); + layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); + layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); + layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); + } + + static inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) + { + applyDefaultCameraInputBindingPreset(layout, camera.getKind(), camera.getAllowedVirtualEvents()); + } + +private: + using virtual_event_t = core::CVirtualGimbalEvent::VirtualEventType; + using keyboard_axis_group_t = std::array; + using mouse_axis_group_t = std::array; + using scalar_axis_pair_t = std::array; + + struct SKeyboardPresetSpec final + { + keyboard_axis_group_t wasd = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double wasdScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + scalar_axis_pair_t qe = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double qeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + keyboard_axis_group_t ijkl = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double ijklScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + }; + + struct SMousePresetSpec final + { + mouse_axis_group_t relative = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double relativeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + scalar_axis_pair_t scroll = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double scrollScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + }; + + /// @brief Shared virtual-event bundles reused across interaction families. + struct SCameraInputBindingEventGroups final + { + static inline constexpr std::array FpsMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitTranslate = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitZoom = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward + }; + static inline constexpr std::array VerticalMove = { + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveUp + }; + static inline constexpr std::array PathRigProgressAndU = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array PathRigV = VerticalMove; + static inline constexpr std::array TurntableMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array LookYawPitch = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array Roll = { + core::CVirtualGimbalEvent::RollLeft, + core::CVirtualGimbalEvent::RollRight + }; + static inline constexpr std::array PanOnly = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array RelativeLook = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown + }; + static inline constexpr std::array RelativeOrbitTranslate = { + core::CVirtualGimbalEvent::MoveRight, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; + static inline constexpr std::array RelativeTopDown = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; + }; + + struct SCameraInteractionBindingSpec + { + SKeyboardPresetSpec keyboard = {}; + SMousePresetSpec mouse = {}; + }; + + struct SCameraMappedInteractionBindingSpec + { + IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; + IGimbalBindingLayout::mouse_to_virtual_events_t mouse; + }; + + template + static inline bool containsBindingForAnyCode(const Map& preset, const Codes& codes) + { + for (const auto code : codes) + { + if (preset.find(code) != preset.end()) + return true; + } + return false; + } + + template + static inline bool containsBindingForAnyCodeGroups(const Map& preset, const Codes&... codes) + { + return (containsBindingForAnyCode(preset, codes) || ...); + } + + static inline constexpr size_t interactionFamilyIndex(const core::ECameraInteractionFamily family) + { + return static_cast(family); + } + + template + static inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events, const double magnitudeScale) + { + for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) + { + const auto event = events[i]; + if (event == core::CVirtualGimbalEvent::None) + continue; + preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event, magnitudeScale)); + } + } + + template + static inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event, const double magnitudeScale) + { + if (event == core::CVirtualGimbalEvent::None) + return; + + std::array> duplicatedEvents = {}; + duplicatedEvents.fill(event); + appendBindingSpec(preset, codes, duplicatedEvents, magnitudeScale); + } + + static inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) + { + IGimbalBindingLayout::keyboard_to_virtual_events_t preset; + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd, spec.wasdScale); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe, spec.qeScale); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl, spec.ijklScale); + return preset; + } + + static inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) + { + IGimbalBindingLayout::mouse_to_virtual_events_t preset; + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative, spec.relativeScale); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0], spec.scrollScale); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1], spec.scrollScale); + return preset; + } + + static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) + { + IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; + for (const auto event : core::CVirtualGimbalEvent::VirtualEventsTypeTable) + { + if (event == core::CVirtualGimbalEvent::None) + continue; + if ((allowedVirtualEvents & event) != event) + continue; + preset.emplace(event, IGimbalBindingLayout::CHashInfo(event, getDefaultImguizmoMagnitudeScale(event))); + } + return preset; + } + + static inline double getDefaultImguizmoMagnitudeScale(const virtual_event_t event) + { + if (core::CVirtualGimbalEvent::isTranslationEvent(event)) + return SInputMagnitudeDefaults::ImguizmoTranslationUnitsPerWorldUnit; + if (core::CVirtualGimbalEvent::isRotationEvent(event)) + return SInputMagnitudeDefaults::ImguizmoRotationUnitsPerRadian; + if (core::CVirtualGimbalEvent::isScaleEvent(event)) + return SInputMagnitudeDefaults::ImguizmoScaleUnitsPerFactor; + return IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + } + + static inline constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = {}; + + static inline constexpr SKeyboardPresetSpec FpsKeyboardSpec = { + SCameraInputBindingEventGroups::FpsMove, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, + SCameraInputBindingEventGroups::LookYawPitch, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond + }; + + static inline constexpr SKeyboardPresetSpec FreeKeyboardSpec = { + FpsKeyboardSpec.wasd, + FpsKeyboardSpec.wasdScale, + SCameraInputBindingEventGroups::Roll, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale + }; + + static inline constexpr SKeyboardPresetSpec OrbitKeyboardSpec = { + SCameraInputBindingEventGroups::OrbitTranslate, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + SCameraInputBindingEventGroups::OrbitZoom, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale + }; + + static inline constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = { + FpsKeyboardSpec.wasd, + FpsKeyboardSpec.wasdScale, + SCameraInputBindingEventGroups::VerticalMove, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale + }; + + static inline constexpr SKeyboardPresetSpec TurntableKeyboardSpec = { + SCameraInputBindingEventGroups::TurntableMove, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale + }; + + static inline constexpr SKeyboardPresetSpec TopDownKeyboardSpec = { + OrbitKeyboardSpec.wasd, + OrbitKeyboardSpec.wasdScale, + OrbitKeyboardSpec.qe, + OrbitKeyboardSpec.qeScale, + SCameraInputBindingEventGroups::PanOnly, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond + }; + + static inline constexpr SKeyboardPresetSpec PathKeyboardSpec = { + SCameraInputBindingEventGroups::PathRigProgressAndU, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + SCameraInputBindingEventGroups::PathRigV, + SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale + }; + + static inline constexpr SMousePresetSpec FpsMouseSpec = { + SCameraInputBindingEventGroups::RelativeLook, + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale + }; + + static inline constexpr SMousePresetSpec OrbitMouseSpec = { + SCameraInputBindingEventGroups::RelativeOrbitTranslate, + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + SCameraInputBindingEventGroups::OrbitZoom, + SInputMagnitudeDefaults::ScrollUnitsPerStep + }; + + static inline constexpr SMousePresetSpec TargetRigMouseSpec = { + FpsMouseSpec.relative, + FpsMouseSpec.relativeScale, + OrbitMouseSpec.scroll, + OrbitMouseSpec.scrollScale + }; + + static inline constexpr SMousePresetSpec TopDownMouseSpec = { + SCameraInputBindingEventGroups::RelativeTopDown, + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + OrbitMouseSpec.scroll, + OrbitMouseSpec.scrollScale + }; + + static inline constexpr SMousePresetSpec PathMouseSpec = { + SCameraInputBindingEventGroups::RelativeOrbitTranslate, + SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + SCameraInputBindingEventGroups::OrbitZoom, + SInputMagnitudeDefaults::ScrollUnitsPerStep + }; + + static inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = { + FpsKeyboardSpec, + FpsMouseSpec + }; + + static inline constexpr SCameraInteractionBindingSpec FreeInteractionBindingSpec = { + FreeKeyboardSpec, + FpsMouseSpec + }; + + static inline constexpr SCameraInteractionBindingSpec OrbitInteractionBindingSpec = { + OrbitKeyboardSpec, + OrbitMouseSpec + }; + + static inline constexpr SCameraInteractionBindingSpec TargetRigInteractionBindingSpec = { + TargetRigKeyboardSpec, + TargetRigMouseSpec + }; + + static inline constexpr SCameraInteractionBindingSpec TurntableInteractionBindingSpec = { + TurntableKeyboardSpec, + TargetRigMouseSpec + }; + + static inline constexpr SCameraInteractionBindingSpec TopDownInteractionBindingSpec = { + TopDownKeyboardSpec, + TopDownMouseSpec + }; + + static inline constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = { + PathKeyboardSpec, + PathMouseSpec + }; + + template + static inline auto makePresetCache(const SpecArray& specs, Builder&& builder) + { + std::array> cache = {}; + for (size_t i = 0u; i < specs.size(); ++i) + cache[i] = builder(specs[i]); + return cache; + } + + static inline SCameraMappedInteractionBindingSpec mapInteractionBindingSpec(const SCameraInteractionBindingSpec& spec) + { + return { + .keyboard = buildKeyboardPreset(spec.keyboard), + .mouse = buildMousePreset(spec.mouse) + }; + } + + static inline constexpr std::array InteractionFamilyPresetSpecs = {{ + EmptyInteractionBindingSpec, + FpsInteractionBindingSpec, + FreeInteractionBindingSpec, + OrbitInteractionBindingSpec, + TargetRigInteractionBindingSpec, + TurntableInteractionBindingSpec, + TopDownInteractionBindingSpec, + PathInteractionBindingSpec + }}; + + static inline const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) + { + const auto familyIx = interactionFamilyIndex(core::CCameraKindUtilities::getCameraInteractionFamily(kind)); + static const auto cache = makePresetCache( + InteractionFamilyPresetSpecs, + [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); + return cache[familyIx < cache.size() ? familyIx : 0u]; + } +}; + +} // namespace nbl::ui + +#endif // _NBL_C_CAMERA_INPUT_BINDING_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp b/include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp new file mode 100644 index 0000000000..44894889c7 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp @@ -0,0 +1,175 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_KEYFRAME_TRACK_HPP_ +#define _C_CAMERA_KEYFRAME_TRACK_HPP_ + +#include +#include +#include + +#include "CCameraPreset.hpp" + +namespace nbl::core +{ + +/// @brief Reusable keyframe container plus selection state for playback tooling. +struct CCameraKeyframeTrack +{ + std::vector keyframes; + int selectedKeyframeIx = -1; +}; + +struct CCameraKeyframeTrackUtilities final +{ +public: + /// @brief Compare two keyframes by authored time and shared preset state. + static inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) + { + return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && + CCameraPresetUtilities::comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); + } + + /// @brief Compare two authored keyframe tracks with optional selection-state checking. + static inline bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection = true) + { + if ((compareSelection && lhs.selectedKeyframeIx != rhs.selectedKeyframeIx) || lhs.keyframes.size() != rhs.keyframes.size()) + return false; + + for (size_t i = 0u; i < lhs.keyframes.size(); ++i) + { + if (!compareKeyframes(lhs.keyframes[i], rhs.keyframes[i], timeEps, posEps, rotEpsDeg, scalarEps)) + return false; + } + + return true; + } + + /// @brief Compare only the serialized/authored content of two tracks and ignore transient UI selection state. + static inline bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) + { + return compareKeyframeTracks(lhs, rhs, timeEps, posEps, rotEpsDeg, scalarEps, false); + } + + static inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) + { + if (track.keyframes.empty()) + return false; + + if (track.keyframes.size() == 1u) + { + preset = track.keyframes.front().preset; + return true; + } + + const auto clampedTime = std::clamp(time, 0.f, track.keyframes.back().time); + size_t idx = 0u; + while (idx + 1u < track.keyframes.size() && track.keyframes[idx + 1u].time < clampedTime) + ++idx; + + const auto& a = track.keyframes[idx]; + const auto& b = track.keyframes[std::min(idx + 1u, track.keyframes.size() - 1u)]; + if (b.time <= a.time) + { + preset = a.preset; + return true; + } + + const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); + preset = a.preset; + CCameraPresetUtilities::assignGoalToPreset( + preset, + CCameraGoalUtilities::blendGoals( + CCameraPresetUtilities::makeGoalFromPreset(a.preset), + CCameraPresetUtilities::makeGoalFromPreset(b.preset), + alpha)); + return true; + } + + static inline void sortKeyframeTrackByTime(CCameraKeyframeTrack& track) + { + std::sort(track.keyframes.begin(), track.keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); + } + + static inline void clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time) + { + if (track.keyframes.empty()) + { + time = 0.f; + return; + } + + time = std::clamp(time, 0.f, track.keyframes.back().time); + } + + static inline int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const float time) + { + if (track.keyframes.empty()) + { + track.selectedKeyframeIx = -1; + return track.selectedKeyframeIx; + } + + size_t bestIx = 0u; + float bestDelta = hlsl::abs(track.keyframes.front().time - time); + for (size_t i = 1u; i < track.keyframes.size(); ++i) + { + const float delta = hlsl::abs(track.keyframes[i].time - time); + if (delta < bestDelta) + { + bestDelta = delta; + bestIx = i; + } + } + + track.selectedKeyframeIx = static_cast(bestIx); + return track.selectedKeyframeIx; + } + + static inline void normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track) + { + if (track.keyframes.empty()) + { + track.selectedKeyframeIx = -1; + return; + } + + if (track.selectedKeyframeIx < 0) + track.selectedKeyframeIx = 0; + else if (track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + track.selectedKeyframeIx = static_cast(track.keyframes.size()) - 1; + } + + static inline CCameraKeyframe* getSelectedKeyframe(CCameraKeyframeTrack& track) + { + normalizeSelectedKeyframeTrack(track); + if (track.selectedKeyframeIx < 0) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; + } + + static inline const CCameraKeyframe* getSelectedKeyframe(const CCameraKeyframeTrack& track) + { + if (track.selectedKeyframeIx < 0 || track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; + } + + static inline bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset) + { + auto* selected = getSelectedKeyframe(track); + if (!selected) + return false; + + selected->preset = std::move(preset); + return true; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_KEYFRAME_TRACK_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp b/include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp new file mode 100644 index 0000000000..7ce8a45cb4 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp @@ -0,0 +1,30 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ +#define _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ + +#include + +#include "CCameraKeyframeTrack.hpp" +#include "nbl/system/path.h" + +namespace nbl::system +{ + +class ISystem; + +/// @brief Serialize one camera keyframe track into an existing stream. +bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, int indent = 2); +/// @brief Deserialize one camera keyframe track from an existing stream. +bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track); + +/// @brief Save one camera keyframe track to a file. +bool saveKeyframeTrackToFile(ISystem& system, const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); +/// @brief Load one camera keyframe track from a file. +bool loadKeyframeTrackFromFile(ISystem& system, const path& path, core::CCameraKeyframeTrack& track); + +} // namespace nbl::system + +#endif // _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraKindUtilities.hpp b/include/nbl/ext/Cameras/CCameraKindUtilities.hpp new file mode 100644 index 0000000000..e09fb6ce52 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraKindUtilities.hpp @@ -0,0 +1,140 @@ +#ifndef _C_CAMERA_KIND_UTILITIES_HPP_ +#define _C_CAMERA_KIND_UTILITIES_HPP_ + +#include +#include + +#include "CCameraPathMetadata.hpp" +#include "ICamera.hpp" + +namespace nbl::core +{ + +/// @brief Interaction family used to group camera kinds with matching control semantics. +enum class ECameraInteractionFamily : uint8_t +{ + None, + Fps, + Free, + Orbit, + TargetRig, + Turntable, + TopDown, + Path +}; + +/// @brief Shared metadata for one concrete `CameraKind`. +struct SCameraKindTraits final +{ + ICamera::CameraKind kind = ICamera::CameraKind::Unknown; + std::string_view label = "Unknown"; + std::string_view description = "Unspecified camera behavior"; + ECameraInteractionFamily interactionFamily = ECameraInteractionFamily::None; +}; + +struct CCameraKindUtilities final +{ +public: + static inline constexpr const SCameraKindTraits& getCameraKindTraits(const ICamera::CameraKind kind) + { + const auto ix = static_cast(kind); + if (ix >= CameraKindTraitsTable.size()) + return CameraKindTraitsTable[0u]; + return CameraKindTraitsTable[ix]; + } + + static inline constexpr std::string_view getCameraKindLabel(const ICamera::CameraKind kind) + { + return getCameraKindTraits(kind).label; + } + + static inline constexpr std::string_view getCameraKindDescription(const ICamera::CameraKind kind) + { + return getCameraKindTraits(kind).description; + } + + static inline constexpr ECameraInteractionFamily getCameraInteractionFamily(const ICamera::CameraKind kind) + { + return getCameraKindTraits(kind).interactionFamily; + } + +private: + static inline constexpr std::array(ICamera::CameraKind::Path) + 1u> CameraKindTraitsTable = {{ + { + .kind = ICamera::CameraKind::Unknown, + .label = "Unknown", + .description = "Unspecified camera behavior", + .interactionFamily = ECameraInteractionFamily::None + }, + { + .kind = ICamera::CameraKind::FPS, + .label = "FPS", + .description = "First-person WASD + mouse look", + .interactionFamily = ECameraInteractionFamily::Fps + }, + { + .kind = ICamera::CameraKind::Free, + .label = "Free", + .description = "Free-fly 6DOF with full rotation", + .interactionFamily = ECameraInteractionFamily::Free + }, + { + .kind = ICamera::CameraKind::Orbit, + .label = "Orbit", + .description = "Orbit around target with dolly", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Arcball, + .label = "Arcball", + .description = "Arcball trackball around target", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Turntable, + .label = "Turntable", + .description = "Turntable yaw/pitch around target", + .interactionFamily = ECameraInteractionFamily::Turntable + }, + { + .kind = ICamera::CameraKind::TopDown, + .label = "TopDown", + .description = "Fixed pitch top-down pan", + .interactionFamily = ECameraInteractionFamily::TopDown + }, + { + .kind = ICamera::CameraKind::Isometric, + .label = "Isometric", + .description = "Fixed isometric view with pan", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Chase, + .label = "Chase", + .description = "Target follow with chase controls", + .interactionFamily = ECameraInteractionFamily::TargetRig + }, + { + .kind = ICamera::CameraKind::Dolly, + .label = "Dolly", + .description = "Rig truck/dolly with look-at", + .interactionFamily = ECameraInteractionFamily::TargetRig + }, + { + .kind = ICamera::CameraKind::DollyZoom, + .label = "Dolly Zoom", + .description = "Orbit with dolly-zoom FOV", + .interactionFamily = ECameraInteractionFamily::Orbit + }, + { + .kind = ICamera::CameraKind::Path, + .label = SCameraPathRigMetadata::KindLabel, + .description = SCameraPathRigMetadata::KindDescription, + .interactionFamily = ECameraInteractionFamily::Path + } + }}; +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_KIND_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp b/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp new file mode 100644 index 0000000000..9ca865a0d1 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp @@ -0,0 +1,156 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_MANIPULATION_UTILITIES_HPP_ +#define _C_CAMERA_MANIPULATION_UTILITIES_HPP_ + +#include +#include + +#include "CCameraPresetFlow.hpp" +#include "CCameraVirtualEventUtilities.hpp" + +namespace nbl::core +{ + +struct SCameraConstraintDefaults final +{ + static constexpr float PitchMinDeg = -80.0f; + static constexpr float PitchMaxDeg = 80.0f; + static constexpr float YawMinDeg = -180.0f; + static constexpr float YawMaxDeg = 180.0f; + static constexpr float RollMinDeg = -180.0f; + static constexpr float RollMaxDeg = 180.0f; + static constexpr float MinDistance = SCameraTargetRelativeTraits::MinDistance; + static constexpr float MaxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; +}; + +/// @brief Reusable constraint settings for post-manipulation camera clamping. +struct SCameraConstraintSettings +{ + bool enabled = false; + bool clampPitch = false; + bool clampYaw = false; + bool clampRoll = false; + bool clampDistance = false; + float pitchMinDeg = SCameraConstraintDefaults::PitchMinDeg; + float pitchMaxDeg = SCameraConstraintDefaults::PitchMaxDeg; + float yawMinDeg = SCameraConstraintDefaults::YawMinDeg; + float yawMaxDeg = SCameraConstraintDefaults::YawMaxDeg; + float rollMinDeg = SCameraConstraintDefaults::RollMinDeg; + float rollMaxDeg = SCameraConstraintDefaults::RollMaxDeg; + float minDistance = SCameraConstraintDefaults::MinDistance; + float maxDistance = SCameraConstraintDefaults::MaxDistance; +}; + +struct CCameraManipulationUtilities final +{ +public: + /// @brief Apply an authored world-space reference frame through the shared camera runtime entry point. + static inline bool applyReferenceFrameToCamera(ICamera* camera, const hlsl::float64_t4x4& referenceFrame) + { + if (!camera) + return false; + + return camera->manipulateWithUnitMotionScales({}, &referenceFrame); + } + + /// @brief Scale translation and rotation event magnitudes without touching unrelated event types. + static inline void scaleVirtualEvents(std::vector& events, const uint32_t count, const float translationScale, const float rotationScale) + { + for (uint32_t i = 0u; i < count; ++i) + { + auto& ev = events[i]; + if (CVirtualGimbalEvent::isTranslationEvent(ev.type)) + { + ev.magnitude *= translationScale; + } + else if (CVirtualGimbalEvent::isRotationEvent(ev.type)) + { + ev.magnitude *= rotationScale; + } + } + } + + /// @brief Reinterpret world-space translation intents as local camera-space movement events. + static inline void remapTranslationEventsFromWorldToCameraLocal(ICamera* camera, std::vector& events, uint32_t& count) + { + if (!camera) + return; + + std::vector filtered; + filtered.reserve(events.size()); + + for (uint32_t i = 0u; i < count; ++i) + { + const auto& ev = events[i]; + if (!CVirtualGimbalEvent::isTranslationEvent(ev.type)) + filtered.emplace_back(ev); + } + + const auto worldDelta = CCameraVirtualEventUtilities::collectSignedTranslationDelta({ events.data(), count }); + if (hlsl::CCameraMathUtilities::isNearlyZeroVector(worldDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) + { + events = std::move(filtered); + count = static_cast(events.size()); + return; + } + + CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); + + events = std::move(filtered); + count = static_cast(events.size()); + } + + /// @brief Apply shared distance and Euler-angle constraints after manipulation. + static inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* camera, const SCameraConstraintSettings& constraints) + { + if (!constraints.enabled || !camera) + return false; + + if (camera->hasCapability(ICamera::SphericalTarget)) + { + if (!constraints.clampDistance) + return false; + + ICamera::SphericalTargetState sphericalState; + if (!camera->tryGetSphericalTargetState(sphericalState)) + return false; + + const float clamped = std::clamp(sphericalState.distance, constraints.minDistance, constraints.maxDistance); + if (clamped == sphericalState.distance) + return false; + + return camera->trySetSphericalDistance(clamped); + } + + if (!(constraints.clampPitch || constraints.clampYaw || constraints.clampRoll)) + return false; + + const auto& gimbal = camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto eulerDeg = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); + + auto clamped = eulerDeg; + if (constraints.clampPitch) + clamped.x = std::clamp(clamped.x, static_cast(constraints.pitchMinDeg), static_cast(constraints.pitchMaxDeg)); + if (constraints.clampYaw) + clamped.y = std::clamp(clamped.y, static_cast(constraints.yawMinDeg), static_cast(constraints.yawMaxDeg)); + if (constraints.clampRoll) + clamped.z = std::clamp(clamped.z, static_cast(constraints.rollMinDeg), static_cast(constraints.rollMaxDeg)); + + if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) + return false; + + CCameraPreset preset; + preset.goal.position = pos; + preset.goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(clamped); + return CCameraPresetFlowUtilities::applyPreset(solver, camera, preset); + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_MANIPULATION_UTILITIES_HPP_ + diff --git a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp new file mode 100644 index 0000000000..3d8e270787 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp @@ -0,0 +1,949 @@ +#ifndef _C_CAMERA_MATH_UTILITIES_HPP_ +#define _C_CAMERA_MATH_UTILITIES_HPP_ + +#include +#include +#include +#include + +#include "nbl/builtin/hlsl/cpp_compat/vector.hlsl" +#include "nbl/builtin/hlsl/numbers.hlsl" +#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" + +namespace nbl::hlsl +{ + +/// @brief Camera-oriented math aliases and helpers built on top of Nabla `nbl::hlsl` types. +template +using camera_vector_t = vector; + +template +using camera_matrix_t = matrix; + +template +using camera_quaternion_t = math::quaternion; + +template +struct SRigidTransformComponents +{ + camera_vector_t translation = camera_vector_t(T(0)); + camera_quaternion_t orientation = camera_quaternion_t::create(); + camera_vector_t scale = camera_vector_t(T(1)); +}; + +template +struct SCameraPoseDelta +{ + T position = T(0); + T rotationDeg = T(0); +}; + +struct SCameraViewRigDefaults final +{ + static constexpr double DegreesToRadians = numbers::pi / 180.0; + static constexpr double ArcballPitchLimitDeg = 89.0; + static constexpr double TurntablePitchLimitDeg = ArcballPitchLimitDeg; + static constexpr double ChaseMaxPitchDeg = 70.0; + static constexpr double ChaseMinPitchDeg = -60.0; + static constexpr double DollyPitchLimitDeg = 85.0; + static constexpr double FpsVerticalPitchLimitDeg = 88.0; + static constexpr double TopDownPitchDeg = -90.0; + static constexpr double IsometricYawDeg = 45.0; + static constexpr double IsometricPitchDeg = 35.264389682754654; + + static inline constexpr double ArcballPitchLimitRad = ArcballPitchLimitDeg * DegreesToRadians; + static inline constexpr double TurntablePitchLimitRad = TurntablePitchLimitDeg * DegreesToRadians; + static inline constexpr double ChaseMaxPitchRad = ChaseMaxPitchDeg * DegreesToRadians; + static inline constexpr double ChaseMinPitchRad = ChaseMinPitchDeg * DegreesToRadians; + static inline constexpr double DollyPitchLimitRad = DollyPitchLimitDeg * DegreesToRadians; + static inline constexpr double FpsVerticalPitchLimitRad = FpsVerticalPitchLimitDeg * DegreesToRadians; + static inline constexpr double TopDownPitchRad = TopDownPitchDeg * DegreesToRadians; + static inline constexpr double IsometricYawRad = IsometricYawDeg * DegreesToRadians; + static inline constexpr double IsometricPitchRad = IsometricPitchDeg * DegreesToRadians; +}; + +struct SCameraRigidMathDefaults final +{ + static constexpr double LookAtParallelThreshold = 0.99; +}; + +struct CCameraMathUtilities final +{ + template + static inline T wrapAngleRad(T angle) + { + constexpr T Pi = numbers::pi; + constexpr T TwoPi = Pi * static_cast(2); + + angle = std::fmod(angle + Pi, TwoPi); + if (angle < static_cast(0)) + angle += TwoPi; + return angle - Pi; + } + + template + static inline T getWrappedAngleDistanceRadians(const T a, const T b) + { + return hlsl::abs(wrapAngleRad(a - b)); + } + + template + static inline T getWrappedAngleDistanceDegrees(const T a, const T b) + { + constexpr T HalfTurn = static_cast(180); + constexpr T FullTurn = static_cast(360); + + T angle = std::fmod(a - b + HalfTurn, FullTurn); + if (angle < static_cast(0)) + angle += FullTurn; + return hlsl::abs(angle - HalfTurn); + } + + template + static inline T lerpWrappedAngleRad(const T a, const T b, const T alpha) + { + return a + wrapAngleRad(b - a) * alpha; + } + + template + static inline bool isFiniteScalar(const T value) + { + return std::isfinite(value); + } + + template + static inline constexpr T getCameraMathEpsilon() + { + return std::numeric_limits::epsilon(); + } + + template + static inline bool nearlyEqualScalar(const T a, const T b, const T epsilon) + { + return hlsl::abs(a - b) <= epsilon; + } + + template + static inline bool isNearlyZeroScalar(const T value, const T epsilon = getCameraMathEpsilon()) + { + return hlsl::abs(value) <= epsilon; + } + + template + static inline bool isNearlyZeroVector(const camera_vector_t& value, const T epsilon = getCameraMathEpsilon()) + { + return length(value) <= epsilon; + } + + template + static inline bool hasPlanarDeltaXY(const camera_vector_t& value, const T epsilon = std::numeric_limits::epsilon()) + { + return !isNearlyZeroVector(camera_vector_t(value.x, value.y), epsilon); + } + + template + static inline bool nearlyEqualVec3(const VecA& a, const VecB& b, const T epsilon) + { + const camera_vector_t delta( + static_cast(a.x - b.x), + static_cast(a.y - b.y), + static_cast(a.z - b.z)); + return length(delta) <= epsilon; + } + + template + static inline constexpr camera_vector_t getCameraWorldRight() + { + return camera_vector_t(T(1), T(0), T(0)); + } + + template + static inline constexpr camera_vector_t getCameraWorldUp() + { + return camera_vector_t(T(0), T(1), T(0)); + } + + template + static inline constexpr camera_vector_t getCameraWorldForward() + { + return camera_vector_t(T(0), T(0), T(1)); + } + + template + static inline constexpr T getCameraLookAtParallelThreshold() + { + return static_cast(SCameraRigidMathDefaults::LookAtParallelThreshold); + } + + template + static inline camera_quaternion_t makeIdentityQuaternion() + { + return camera_quaternion_t::create(); + } + + template + static inline camera_quaternion_t makeQuaternionFromComponents(const T x, const T y, const T z, const T w) + { + camera_quaternion_t output; + output.data = camera_vector_t(x, y, z, w); + return output; + } + + template + static inline camera_quaternion_t normalizeQuaternion(const camera_quaternion_t& q) + { + return normalize(q); + } + + template + static inline bool isFiniteQuaternion(const camera_quaternion_t& q) + { + return isFiniteScalar(q.data.x) && + isFiniteScalar(q.data.y) && + isFiniteScalar(q.data.z) && + isFiniteScalar(q.data.w); + } + + template + static inline bool isFiniteVec3(const camera_vector_t& value) + { + return isFiniteScalar(value.x) && + isFiniteScalar(value.y) && + isFiniteScalar(value.z); + } + + template + static inline camera_vector_t safeNormalizeVec3(const camera_vector_t& value, const camera_vector_t& fallback) + { + const auto len = length(value); + if (!isFiniteScalar(len) || len <= getCameraMathEpsilon()) + return fallback; + return value / len; + } + + template + static inline camera_quaternion_t makeQuaternionFromAxisAngle(const camera_vector_t& axis, const T radians) + { + return camera_quaternion_t::create(axis, radians); + } + + template + static inline camera_quaternion_t makeQuaternionFromEulerRadians(const camera_vector_t& eulerRadians) + { + return camera_quaternion_t::create(eulerRadians.x, eulerRadians.y, eulerRadians.z); + } + + template + static inline camera_quaternion_t makeQuaternionFromEulerDegrees(const camera_vector_t& eulerDegrees) + { + return makeQuaternionFromEulerRadians(camera_vector_t( + radians(eulerDegrees.x), + radians(eulerDegrees.y), + radians(eulerDegrees.z))); + } + + template + static inline camera_quaternion_t makeQuaternionFromEulerRadiansYXZ(const camera_vector_t& eulerRadians) + { + const auto pitch = makeQuaternionFromAxisAngle(getCameraWorldRight(), eulerRadians.x); + const auto yaw = makeQuaternionFromAxisAngle(getCameraWorldUp(), eulerRadians.y); + const auto roll = makeQuaternionFromAxisAngle(getCameraWorldForward(), eulerRadians.z); + return normalizeQuaternion(yaw * pitch * roll); + } + + template + static inline camera_quaternion_t makeQuaternionFromEulerDegreesYXZ(const camera_vector_t& eulerDegrees) + { + return makeQuaternionFromEulerRadiansYXZ(camera_vector_t( + radians(eulerDegrees.x), + radians(eulerDegrees.y), + radians(eulerDegrees.z))); + } + + template + static inline camera_quaternion_t makeQuaternionFromBasis( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) + { + const auto canonicalForward = safeNormalizeVec3(forward, getCameraWorldForward()); + + auto canonicalRight = right - canonicalForward * dot(right, canonicalForward); + canonicalRight = safeNormalizeVec3( + canonicalRight, + safeNormalizeVec3(cross(up, canonicalForward), getCameraWorldRight())); + + auto canonicalUp = cross(canonicalForward, canonicalRight); + canonicalUp = safeNormalizeVec3( + canonicalUp, + safeNormalizeVec3(up - canonicalForward * dot(up, canonicalForward), getCameraWorldUp())); + + canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); + canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); + + const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; + const auto desiredRight = canonicalRight; + const auto desiredUp = canonicalUp; + const auto desiredForward = canonicalForward; + + const auto scoreCandidate = [&](const camera_quaternion_t& candidate) + { + if (!isFiniteQuaternion(candidate)) + return std::numeric_limits::infinity(); + + const auto normalizedCandidate = normalizeQuaternion(candidate); + const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); + const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); + const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); + + const T rightError = length(rebuiltRight - desiredRight); + const T upError = length(rebuiltUp - desiredUp); + const T forwardError = length(rebuiltForward - desiredForward); + return rightError + upError + forwardError; + }; + + const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) + { + const T m00 = m[0][0]; + const T m11 = m[1][1]; + const T m22 = m[2][2]; + const T trace = m00 + m11 + m22; + + camera_quaternion_t output = makeIdentityQuaternion(); + if (trace > T(0)) + { + const T scale = hlsl::sqrt(trace + T(1)); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][1] - m[1][2]) * invScale; + output.data.y = (m[0][2] - m[2][0]) * invScale; + output.data.z = (m[1][0] - m[0][1]) * invScale; + output.data.w = scale * T(0.5); + } + else if (m00 >= m11 && m00 >= m22) + { + const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); + const T invScale = T(0.5) / scale; + output.data.x = scale * T(0.5); + output.data.y = (m[0][1] + m[1][0]) * invScale; + output.data.z = (m[2][0] + m[0][2]) * invScale; + output.data.w = (m[2][1] - m[1][2]) * invScale; + } + else if (m11 >= m22) + { + const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); + const T invScale = T(0.5) / scale; + output.data.x = (m[0][1] + m[1][0]) * invScale; + output.data.y = scale * T(0.5); + output.data.z = (m[1][2] + m[2][1]) * invScale; + output.data.w = (m[0][2] - m[2][0]) * invScale; + } + else + { + const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][0] + m[0][2]) * invScale; + output.data.y = (m[1][2] + m[2][1]) * invScale; + output.data.z = scale * T(0.5); + output.data.w = (m[1][0] - m[0][1]) * invScale; + } + return normalizeQuaternion(output); + }; + + const camera_matrix_t transposedBasis = hlsl::transpose(basis); + const camera_quaternion_t candidates[] = { + camera_quaternion_t::create(basis, true), + camera_quaternion_t::create(transposedBasis, true), + quaternionFromMatrixFallback(basis), + quaternionFromMatrixFallback(transposedBasis) + }; + + camera_quaternion_t bestCandidate = makeIdentityQuaternion(); + T bestScore = std::numeric_limits::infinity(); + bool foundFiniteCandidate = false; + const auto considerCandidate = [&](const camera_quaternion_t& candidate) + { + const T score = scoreCandidate(candidate); + if (score < bestScore) + { + bestScore = score; + bestCandidate = candidate; + foundFiniteCandidate = true; + } + }; + + for (const auto& candidate : candidates) + considerCandidate(candidate); + + if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) + return makeIdentityQuaternion(); + + return normalizeQuaternion(bestCandidate); + } + + template + static inline bool tryBuildCameraBasisFromForwardUpHint( + const camera_vector_t& forwardHint, + const camera_vector_t& upHint, + camera_vector_t& outRight, + camera_vector_t& outUp, + camera_vector_t& outForward) + { + const auto forward = safeNormalizeVec3(forwardHint, getCameraWorldForward()); + if (!isFiniteVec3(forward) || isNearlyZeroVector(forward)) + return false; + + const auto preferredUp = safeNormalizeVec3(upHint, getCameraWorldForward()); + auto right = cross(preferredUp, forward); + if (!isFiniteVec3(right) || isNearlyZeroVector(right)) + { + const auto fallbackUp = hlsl::abs(forward.z) < getCameraLookAtParallelThreshold() ? + getCameraWorldForward() : + getCameraWorldUp(); + right = cross(fallbackUp, forward); + if (!isFiniteVec3(right) || isNearlyZeroVector(right)) + return false; + } + + right = safeNormalizeVec3(right, getCameraWorldRight()); + auto up = safeNormalizeVec3(cross(forward, right), preferredUp); + right = safeNormalizeVec3(cross(up, forward), right); + if (!isOrthoBase(right, up, forward)) + return false; + + outRight = right; + outUp = up; + outForward = forward; + return true; + } + + template + static inline camera_vector_t makeSphericalOffsetFromOrbit(const camera_vector_t& orbitUv, const T distance) + { + return camera_vector_t( + hlsl::cos(orbitUv.y) * hlsl::cos(orbitUv.x) * distance, + hlsl::cos(orbitUv.y) * hlsl::sin(orbitUv.x) * distance, + hlsl::sin(orbitUv.y) * distance); + } + + template + static inline T getPlanarRadiusXZ(const camera_vector_t& offset) + { + return length(camera_vector_t(offset.x, offset.z)); + } + + template + static inline T getPathDistance(const T pathU, const T pathV) + { + return length(camera_vector_t(pathU, pathV)); + } + + template + static inline camera_vector_t makePathOffsetFromState(const T pathS, const T pathU, const T pathV) + { + return camera_vector_t(hlsl::cos(pathS) * pathU, pathV, hlsl::sin(pathS) * pathU); + } + + template + static inline bool sanitizePathState(T& pathS, T& pathU, T& pathV, T& pathRoll, const T minU) + { + if (!isFiniteScalar(pathS) || !isFiniteScalar(pathU) || !isFiniteScalar(pathV) || !isFiniteScalar(pathRoll)) + return false; + + pathS = wrapAngleRad(pathS); + pathU = std::max(minU, pathU); + pathRoll = wrapAngleRad(pathRoll); + return isFiniteScalar(pathS) && + isFiniteScalar(pathU) && + isFiniteScalar(pathV) && + isFiniteScalar(pathRoll); + } + + template + static inline bool tryScalePathStateDistance( + const T desiredDistance, + const T minU, + T& pathU, + T& pathV, + T* outAppliedDistance = nullptr) + { + if (!isFiniteScalar(desiredDistance) || + !isFiniteScalar(pathU) || + !isFiniteScalar(pathV)) + return false; + + const T currentDistance = getPathDistance(pathU, pathV); + constexpr T Epsilon = std::numeric_limits::epsilon(); + if (currentDistance > Epsilon) + { + const T scale = desiredDistance / currentDistance; + pathU = std::max(minU, pathU * scale); + pathV *= scale; + } + else + { + pathU = std::max(minU, desiredDistance); + pathV = T(0); + } + + if (outAppliedDistance) + *outAppliedDistance = getPathDistance(pathU, pathV); + return isFiniteScalar(pathU) && isFiniteScalar(pathV); + } + + template + static inline bool tryBuildPathStateFromPosition( + const camera_vector_t& targetPosition, + const camera_vector_t& position, + const T minRadius, + T& outS, + T& outU, + T& outV) + { + const auto offset = position - targetPosition; + const auto radius = getPlanarRadiusXZ(offset); + if (!isFiniteScalar(radius) || !isFiniteScalar(offset.y)) + return false; + + outS = wrapAngleRad(hlsl::atan2(offset.z, offset.x)); + outU = std::max(minRadius, radius); + outV = offset.y; + return isFiniteScalar(outS) && + isFiniteScalar(outU) && + isFiniteScalar(outV); + } + + template + static inline bool tryBuildLookAtOrientation( + const camera_vector_t& position, + const camera_vector_t& targetPosition, + const camera_vector_t& preferredUp, + camera_quaternion_t& outOrientation) + { + const auto toTarget = targetPosition - position; + camera_vector_t right = camera_vector_t(T(0)); + camera_vector_t up = camera_vector_t(T(0)); + camera_vector_t forward = camera_vector_t(T(0)); + if (!tryBuildCameraBasisFromForwardUpHint(toTarget, preferredUp, right, up, forward)) + return false; + + outOrientation = makeQuaternionFromBasis(right, up, forward); + return true; + } + + template + static inline bool tryExtractRigidPoseFromTransform( + const camera_matrix_t& transform, + camera_vector_t& outTranslation, + camera_quaternion_t& outOrientation) + { + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) + return false; + + outTranslation = components.translation; + outOrientation = components.orientation; + return true; + } + + template + static inline bool tryBuildSphericalPoseFromOrbit( + const camera_vector_t& targetPosition, + const camera_vector_t& orbitUv, + const T distance, + const T minDistance, + const T maxDistance, + camera_vector_t& outPosition, + camera_quaternion_t& outOrientation, + T* outAppliedDistance = nullptr) + { + if (!isFiniteScalar(orbitUv.x) || + !isFiniteScalar(orbitUv.y) || + !isFiniteScalar(distance)) + return false; + + const T appliedDistance = std::clamp(distance, minDistance, maxDistance); + const auto spherePosition = makeSphericalOffsetFromOrbit(orbitUv, appliedDistance); + const auto upHint = safeNormalizeVec3( + camera_vector_t( + -hlsl::sin(orbitUv.y) * hlsl::cos(orbitUv.x), + -hlsl::sin(orbitUv.y) * hlsl::sin(orbitUv.x), + hlsl::cos(orbitUv.y)), + getCameraWorldForward()); + camera_vector_t right = camera_vector_t(T(0)); + camera_vector_t up = camera_vector_t(T(0)); + camera_vector_t forward = camera_vector_t(T(0)); + if (!tryBuildCameraBasisFromForwardUpHint(-spherePosition, upHint, right, up, forward)) + return false; + + outPosition = targetPosition + spherePosition; + outOrientation = makeQuaternionFromBasis(right, up, forward); + if (outAppliedDistance) + *outAppliedDistance = appliedDistance; + return true; + } + + template + static inline bool tryBuildOrbitFromPosition( + const camera_vector_t& targetPosition, + const camera_vector_t& position, + const T minDistance, + const T maxDistance, + camera_vector_t& outOrbitUv, + T& outDistance) + { + const auto offset = position - targetPosition; + const auto distance = length(offset); + if (!isFiniteScalar(distance) || distance <= getCameraMathEpsilon()) + return false; + + outDistance = std::clamp(distance, minDistance, maxDistance); + const auto local = offset / outDistance; + outOrbitUv = camera_vector_t( + hlsl::atan2(local.y, local.x), + hlsl::asin(std::clamp(local.z, T(-1), T(1)))); + return isFiniteScalar(outOrbitUv.x) && + isFiniteScalar(outOrbitUv.y) && + isFiniteScalar(outDistance); + } + + template + static inline camera_vector_t getPitchYawFromForwardVector(const camera_vector_t& forward) + { + const T planarLength = length(camera_vector_t(forward.x, forward.z)); + return camera_vector_t( + hlsl::atan2(planarLength, forward.y) - numbers::pi * T(0.5), + hlsl::atan2(forward.x, forward.z)); + } + + template + static inline camera_vector_t getPitchYawFromOrientation(const camera_quaternion_t& orientation) + { + const auto forward = normalizeQuaternion(orientation).transformVector(camera_vector_t(T(0), T(0), T(1)), true); + return getPitchYawFromForwardVector(forward); + } + + template + static inline bool tryBuildPathPoseFromState( + const camera_vector_t& targetPosition, + const T pathS, + const T pathU, + const T pathV, + const T pathRoll, + const T minRadius, + const T minDistance, + const T maxDistance, + camera_vector_t& outPosition, + camera_quaternion_t& outOrientation, + T* outAppliedDistance = nullptr, + camera_vector_t* outOrbitUv = nullptr) + { + if (!isFiniteScalar(pathS) || + !isFiniteScalar(pathU) || + !isFiniteScalar(pathV) || + !isFiniteScalar(pathRoll)) + return false; + + const T appliedU = std::max(minRadius, pathU); + const auto offset = makePathOffsetFromState(pathS, appliedU, pathV); + + camera_vector_t orbitUv = camera_vector_t(T(0)); + T distance = T(0); + if (!tryBuildOrbitFromPosition(targetPosition, targetPosition + offset, minDistance, maxDistance, orbitUv, distance)) + return false; + if (!tryBuildSphericalPoseFromOrbit(targetPosition, orbitUv, distance, minDistance, maxDistance, outPosition, outOrientation, &distance)) + return false; + + if (!isNearlyZeroScalar(pathRoll, std::numeric_limits::epsilon())) + { + const auto basis = getQuaternionBasisMatrix(outOrientation); + const T rollCos = hlsl::cos(pathRoll); + const T rollSin = hlsl::sin(pathRoll); + const auto right = basis[0u] * rollCos + basis[1u] * rollSin; + const auto up = basis[1u] * rollCos - basis[0u] * rollSin; + outOrientation = makeQuaternionFromBasis(right, up, basis[2u]); + } + + if (outAppliedDistance) + *outAppliedDistance = distance; + if (outOrbitUv) + *outOrbitUv = orbitUv; + return true; + } + + template + static inline camera_vector_t rotateVectorByQuaternion(const camera_quaternion_t& orientation, const camera_vector_t& vectorToRotate) + { + return normalizeQuaternion(orientation).transformVector(vectorToRotate, true); + } + + template + static inline camera_vector_t projectWorldVectorToLocalBasis( + const camera_vector_t& worldVector, + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) + { + return camera_vector_t( + dot(worldVector, right), + dot(worldVector, up), + dot(worldVector, forward)); + } + + template + static inline camera_vector_t transformLocalVectorToWorldBasis( + const camera_vector_t& localVector, + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) + { + return right * localVector.x + up * localVector.y + forward * localVector.z; + } + + template + static inline camera_vector_t getQuaternionEulerRadians(const camera_quaternion_t& orientation) + { + const auto q = normalizeQuaternion(orientation); + const T x = q.data.x; + const T y = q.data.y; + const T z = q.data.z; + const T w = q.data.w; + + const T pitch = hlsl::atan2( + T(2) * (y * z + w * x), + w * w - x * x - y * y + z * z); + const T yaw = hlsl::asin(std::clamp( + T(-2) * (x * z - w * y), + T(-1), + T(1))); + const T roll = hlsl::atan2( + T(2) * (x * y + w * z), + w * w + x * x - y * y - z * z); + + return camera_vector_t(pitch, yaw, roll); + } + + template + static inline camera_vector_t getQuaternionEulerDegrees(const camera_quaternion_t& orientation) + { + const auto eulerRadians = getQuaternionEulerRadians(orientation); + return camera_vector_t( + degrees(eulerRadians.x), + degrees(eulerRadians.y), + degrees(eulerRadians.z)); + } + + template + static inline T getQuaternionAngularDistanceRadians(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) + { + const auto lhsNormalized = normalizeQuaternion(lhs); + const auto rhsNormalized = normalizeQuaternion(rhs); + const T orientationDot = std::clamp( + static_cast(hlsl::abs(dot(lhsNormalized.data, rhsNormalized.data))), + T(0), + T(1)); + return T(2) * hlsl::acos(orientationDot); + } + + template + static inline T getQuaternionAngularDistanceDegrees(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs) + { + return degrees(getQuaternionAngularDistanceRadians(lhs, rhs)); + } + + template + static inline bool tryComputePoseDelta( + const camera_vector_t& lhsPosition, + const camera_quaternion_t& lhsOrientation, + const camera_vector_t& rhsPosition, + const camera_quaternion_t& rhsOrientation, + SCameraPoseDelta& outDelta) + { + outDelta = {}; + + const auto lhsNormalized = normalizeQuaternion(lhsOrientation); + const auto rhsNormalized = normalizeQuaternion(rhsOrientation); + if (!isFiniteVec3(lhsPosition) || !isFiniteVec3(rhsPosition) || + !isFiniteQuaternion(lhsNormalized) || !isFiniteQuaternion(rhsNormalized)) + { + return false; + } + + outDelta.position = length(lhsPosition - rhsPosition); + outDelta.rotationDeg = getQuaternionAngularDistanceDegrees(lhsNormalized, rhsNormalized); + return isFiniteScalar(outDelta.position) && isFiniteScalar(outDelta.rotationDeg); + } + + template + static inline camera_quaternion_t slerpQuaternion(const camera_quaternion_t& lhs, const camera_quaternion_t& rhs, const T alpha) + { + return camera_quaternion_t::slerp(normalizeQuaternion(lhs), normalizeQuaternion(rhs), alpha); + } + + template + static inline camera_quaternion_t inverseQuaternion(const camera_quaternion_t& q) + { + return inverse(q); + } + + template + static inline camera_vector_t projectWorldVectorToLocalQuaternionFrame( + const camera_quaternion_t& orientation, + const camera_vector_t& worldVector) + { + return rotateVectorByQuaternion(inverseQuaternion(orientation), worldVector); + } + + template + static inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion_t& orientation) + { + const auto q = normalizeQuaternion(orientation); + return camera_matrix_t( + q.transformVector(camera_vector_t(T(1), T(0), T(0)), true), + q.transformVector(camera_vector_t(T(0), T(1), T(0)), true), + q.transformVector(camera_vector_t(T(0), T(0), T(1)), true)); + } + + template + static inline camera_vector_t getQuaternionEulerRadiansYXZ(const camera_quaternion_t& orientation) + { + const auto basis = getQuaternionBasisMatrix(orientation); + const T yaw = hlsl::atan2(basis[2][0], basis[2][2]); + const T c2 = hlsl::length(camera_vector_t(basis[0][1], basis[1][1])); + const T pitch = hlsl::atan2(-basis[2][1], c2); + const T s1 = hlsl::sin(yaw); + const T c1 = hlsl::cos(yaw); + const T roll = hlsl::atan2( + s1 * basis[1][2] - c1 * basis[1][0], + c1 * basis[0][0] - s1 * basis[0][2]); + return camera_vector_t(pitch, yaw, roll); + } + + template + static inline camera_vector_t getQuaternionEulerDegreesYXZ(const camera_quaternion_t& orientation) + { + const auto eulerRadians = getQuaternionEulerRadiansYXZ(orientation); + return camera_vector_t( + degrees(eulerRadians.x), + degrees(eulerRadians.y), + degrees(eulerRadians.z)); + } + + template + static inline camera_vector_t getCameraOrientationEulerRadians(const camera_quaternion_t& orientation) + { + return getQuaternionEulerRadiansYXZ(orientation); + } + + template + static inline camera_vector_t getCameraOrientationEulerDegrees(const camera_quaternion_t& orientation) + { + return getQuaternionEulerDegreesYXZ(orientation); + } + + template + static inline camera_vector_t getOrientationDeltaEulerRadiansYXZ( + const camera_quaternion_t& from, + const camera_quaternion_t& to) + { + const auto deltaQuat = inverseQuaternion(from) * normalizeQuaternion(to); + return getQuaternionEulerRadiansYXZ(deltaQuat); + } + + template + static inline camera_vector_t getWrappedEulerDistanceDegrees( + const camera_vector_t& a, + const camera_vector_t& b) + { + return camera_vector_t( + getWrappedAngleDistanceDegrees(a.x, b.x), + getWrappedAngleDistanceDegrees(a.y, b.y), + getWrappedAngleDistanceDegrees(a.z, b.z)); + } + + template + static inline T getMaxVectorComponent(const camera_vector_t& value) + { + return std::max(value.x, std::max(value.y, value.z)); + } + + template + static inline camera_matrix_t composeTransformMatrix( + const camera_vector_t& translation, + const camera_quaternion_t& orientation, + const camera_vector_t& scale = camera_vector_t(T(1))) + { + camera_matrix_t output = camera_matrix_t(1); + const auto basis = getQuaternionBasisMatrix(orientation); + output[0] = camera_vector_t(basis[0] * scale.x, T(0)); + output[1] = camera_vector_t(basis[1] * scale.y, T(0)); + output[2] = camera_vector_t(basis[2] * scale.z, T(0)); + output[3] = camera_vector_t(translation, T(1)); + return output; + } + + template + static inline bool tryExtractRigidTransformComponents( + const camera_matrix_t& transform, + SRigidTransformComponents& outComponents) + { + outComponents.translation = camera_vector_t(transform[3].x, transform[3].y, transform[3].z); + + auto right = camera_vector_t(transform[0].x, transform[0].y, transform[0].z); + auto up = camera_vector_t(transform[1].x, transform[1].y, transform[1].z); + auto forward = camera_vector_t(transform[2].x, transform[2].y, transform[2].z); + + outComponents.scale = camera_vector_t(length(right), length(up), length(forward)); + + if (!isFiniteVec3(outComponents.translation) || !isFiniteVec3(outComponents.scale)) + return false; + + constexpr T Epsilon = std::numeric_limits::epsilon(); + if (outComponents.scale.x <= Epsilon || outComponents.scale.y <= Epsilon || outComponents.scale.z <= Epsilon) + return false; + + right /= outComponents.scale.x; + up /= outComponents.scale.y; + forward /= outComponents.scale.z; + if (!isOrthoBase(right, up, forward)) + return false; + + outComponents.orientation = makeQuaternionFromBasis(right, up, forward); + return isFiniteQuaternion(outComponents.orientation); + } + + template + static inline bool tryBuildRigidFrameFromTransform( + const camera_matrix_t& transform, + camera_matrix_t& outFrame, + camera_quaternion_t& outOrientation) + { + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) + return false; + + outOrientation = components.orientation; + outFrame = composeTransformMatrix(components.translation, components.orientation); + return true; + } + + template + static inline bool decomposeTransformMatrix( + const camera_matrix_t& transform, + camera_vector_t& outTranslation, + camera_vector_t& outRotationEulerDegrees, + camera_vector_t& outScale) + { + SRigidTransformComponents components; + if (!tryExtractRigidTransformComponents(transform, components)) + return false; + + outTranslation = components.translation; + outScale = components.scale; + outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); + return isFiniteVec3(outRotationEulerDegrees); + } +}; + +} // namespace nbl::hlsl + +#endif // _C_CAMERA_MATH_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPathMetadata.hpp b/include/nbl/ext/Cameras/CCameraPathMetadata.hpp new file mode 100644 index 0000000000..0ccf2ac6ed --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPathMetadata.hpp @@ -0,0 +1,30 @@ +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PATH_METADATA_HPP_ +#define _C_CAMERA_PATH_METADATA_HPP_ + +#include + +namespace nbl::core +{ + +/// @brief Stable descriptive strings used by the reusable `Path Rig` camera kind. +/// +/// This metadata lives in a lightweight header so code that only needs labels +/// or identifiers does not have to include the full path-model implementation. +struct SCameraPathRigMetadata final +{ + /// @brief User-facing camera kind label. + static inline constexpr std::string_view KindLabel = "Path Rig"; + /// @brief Short user-facing description of the camera kind. + static inline constexpr std::string_view KindDescription = "Path-model camera with typed s/u/v/roll state"; + /// @brief Default runtime identifier used by the concrete camera instance. + static inline constexpr std::string_view Identifier = "Target-relative Path Rig"; + /// @brief Default description of the built-in path model shipped by the shared API. + static inline constexpr std::string_view DefaultModelDescription = "Adjust a target-relative path rig with s/u/v/roll state"; +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_PATH_METADATA_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPathUtilities.hpp b/include/nbl/ext/Cameras/CCameraPathUtilities.hpp new file mode 100644 index 0000000000..736b944f56 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPathUtilities.hpp @@ -0,0 +1,575 @@ +#ifndef _C_CAMERA_PATH_UTILITIES_HPP_ +#define _C_CAMERA_PATH_UTILITIES_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "CCameraPathMetadata.hpp" +#include "CCameraTargetRelativeUtilities.hpp" +#include "CCameraVirtualEventUtilities.hpp" +#include "ICamera.hpp" + +namespace nbl::core +{ + +/// @brief Shared helpers for the reusable `PathRig` camera kind. +struct SCameraPathPose final : SCameraRigPose +{ + /// @brief Final radial distance actually applied after clamping and path-state sanitization. + hlsl::float64_t appliedDistance = 0.0; + /// @brief Canonical orbit yaw/pitch derived from the evaluated path state. + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); +}; + +/// @brief Typed delta applied to `ICamera::PathState`. +struct SCameraPathDelta final : ICamera::PathState +{ + /// @brief Pack the delta into one four-component vector. + inline hlsl::float64_t4 asVector() const + { + return ICamera::PathState::asVector(); + } + + /// @brief Reinterpret the delta as the translation-style helper representation. + inline hlsl::float64_t3 translationVector() const + { + return ICamera::PathState::asTranslationVector(); + } + + /// @brief Rebuild the delta from the packed vector representation. + static inline SCameraPathDelta fromVector(const hlsl::float64_t4& value) + { + SCameraPathDelta delta = {}; + delta.s = value.x; + delta.u = value.y; + delta.v = value.z; + delta.roll = value.w; + return delta; + } + + /// @brief Rebuild the delta from a translation-style helper vector and optional roll value. + static inline SCameraPathDelta fromMotion(const hlsl::float64_t3& translation, const double pathRoll = 0.0) + { + SCameraPathDelta delta = {}; + delta.s = translation.z; + delta.u = translation.x; + delta.v = translation.y; + delta.roll = pathRoll; + return delta; + } +}; + +/// @brief One desired path-state change expressed as current state, desired state, and their delta. +struct SCameraPathStateTransition final +{ + ICamera::PathState current = {}; + ICamera::PathState desired = {}; + SCameraPathDelta delta = {}; +}; + +/// @brief Canonical evaluated path state combining a final pose and target-relative view of that pose. +struct SCameraCanonicalPathState final +{ + SCameraPathPose pose = {}; + SCameraTargetRelativeState targetRelative = {}; +}; + +/// @brief Comparison tolerances used when matching two path states. +struct SCameraPathComparisonThresholds final +{ + double sToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; + double rollToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; + double scalarTolerance = SCameraToolingThresholds::ScalarTolerance; +}; + +/// @brief Result of updating the path distance while preserving the rest of the path state. +struct SCameraPathDistanceUpdateResult final +{ + bool exact = false; + hlsl::float64_t appliedDistance = 0.0; +}; + +/// @brief Default constants used by the built-in `Path Rig` model. +struct SCameraPathDefaults final +{ + static constexpr double MinU = static_cast(SCameraTargetRelativeTraits::MinDistance); + static constexpr double ScalarTolerance = SCameraToolingThresholds::ScalarTolerance; + static constexpr double ExactStateTolerance = SCameraToolingThresholds::TinyScalarEpsilon; + static constexpr double ExactAngleToleranceDeg = ExactStateTolerance * 180.0 / hlsl::numbers::pi; + static constexpr double AngleToleranceDeg = SCameraToolingThresholds::DefaultAngularToleranceDeg; + static inline constexpr std::string_view Identifier = SCameraPathRigMetadata::Identifier; + static inline constexpr std::string_view Description = SCameraPathRigMetadata::DefaultModelDescription; + static inline constexpr ICamera::PathStateLimits Limits = {}; + static inline constexpr SCameraPathComparisonThresholds ComparisonThresholds = { + .sToleranceDeg = AngleToleranceDeg, + .rollToleranceDeg = AngleToleranceDeg, + .scalarTolerance = ScalarTolerance + }; + static inline constexpr SCameraPathComparisonThresholds ExactComparisonThresholds = { + .sToleranceDeg = ExactAngleToleranceDeg, + .rollToleranceDeg = ExactAngleToleranceDeg, + .scalarTolerance = ExactStateTolerance + }; +}; + +using SCameraPathLimits = ICamera::PathStateLimits; + +/// @brief Evaluation context passed into the active path-model control law. +struct SCameraPathControlContext final +{ + ICamera::PathState currentState = {}; + hlsl::float64_t3 translation = hlsl::float64_t3(0.0); + hlsl::float64_t3 rotation = hlsl::float64_t3(0.0); + hlsl::float64_t3 targetPosition = hlsl::float64_t3(0.0); + const CReferenceTransform* reference = nullptr; + SCameraPathLimits limits = SCameraPathDefaults::Limits; +}; + +/// @brief Callback bundle defining path-state resolution, input response, evaluation, and distance updates. +/// +/// A concrete `Path Rig` model provides: +/// - state resolution from target position, world position, and optional typed input +/// - one control law turning accumulated runtime motion into `SCameraPathDelta` +/// - one state integrator +/// - one canonical evaluator producing pose and target-relative view data +/// - one distance-update rule for typed helpers that adjust distance directly +struct SCameraPathModel final +{ + using resolve_state_t = std::function; + using control_law_t = std::function; + using integrate_t = std::function; + using evaluate_t = std::function; + using update_distance_t = std::function; + + resolve_state_t resolveState; + control_law_t controlLaw; + integrate_t integrate; + evaluate_t evaluate; + update_distance_t updateDistance; +}; + +/// @brief Shared state, comparison, and model-building helpers for `Path Rig`. +struct CCameraPathUtilities final +{ + /// @brief Build the default path state used by the built-in model. + static inline ICamera::PathState makeDefaultPathState(const double minU = SCameraPathDefaults::MinU) + { + return { + .s = 0.0, + .u = minU, + .v = 0.0, + .roll = 0.0 + }; + } + + /// @brief Build path-state comparison tolerances from caller-provided angular and scalar thresholds. + static inline SCameraPathComparisonThresholds makePathComparisonThresholds( + const double angularToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, + const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) + { + return { + .sToleranceDeg = angularToleranceDeg, + .rollToleranceDeg = angularToleranceDeg, + .scalarTolerance = scalarTolerance + }; + } + + /// @brief Return the default path-state limits used when a camera does not expose custom ones. + static inline constexpr SCameraPathLimits makeDefaultPathLimits() + { + return SCameraPathDefaults::Limits; + } + + /// @brief Check whether every scalar stored in the path state is finite. + static inline bool isPathStateFinite(const ICamera::PathState& state) + { + return hlsl::CCameraMathUtilities::isFiniteScalar(state.s) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.u) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.v) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.roll); + } + + /// @brief Check whether the path limits can be sanitized into a valid numeric domain. + static inline bool isPathLimitsWellFormed(const SCameraPathLimits& limits) + { + return hlsl::CCameraMathUtilities::isFiniteScalar(limits.minU) && + hlsl::CCameraMathUtilities::isFiniteScalar(limits.minDistance) && + !std::isnan(static_cast(limits.maxDistance)); + } + + /// @brief Clamp and normalize path-state limits into a valid numeric domain. + static inline bool sanitizePathLimits(SCameraPathLimits& limits) + { + if (!isPathLimitsWellFormed(limits)) + return false; + + limits.minU = std::max(limits.minU, 0.0); + limits.minDistance = std::max( + std::max(limits.minDistance, static_cast(limits.minU)), + static_cast(SCameraTargetRelativeTraits::MinDistance)); + + if (!std::isfinite(static_cast(limits.maxDistance))) + limits.maxDistance = std::numeric_limits::infinity(); + else + limits.maxDistance = std::max(limits.maxDistance, limits.minDistance); + return true; + } + + /// @brief Sanitize a path state against a caller-provided `minU` lower bound. + static inline bool sanitizePathState(ICamera::PathState& state, const double minU) + { + return hlsl::CCameraMathUtilities::sanitizePathState(state.s, state.u, state.v, state.roll, minU); + } + + /// @brief Sanitize a path state against a full limit bundle and optionally report the applied distance. + static inline bool sanitizePathState(ICamera::PathState& state, const SCameraPathLimits& limits, double* outAppliedDistance = nullptr) + { + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + + if (!sanitizePathState(state, sanitizedLimits.minU)) + return false; + + const auto desiredDistance = std::clamp( + hlsl::CCameraMathUtilities::getPathDistance(state.u, state.v), + sanitizedLimits.minDistance, + sanitizedLimits.maxDistance); + return tryScalePathStateDistance(desiredDistance, sanitizedLimits.minU, state, outAppliedDistance); + } + + /// @brief Rescale the `(u, v)` pair so the path state reaches the requested radial distance. + static inline bool tryScalePathStateDistance( + const double desiredDistance, + const double minU, + ICamera::PathState& ioState, + double* outAppliedDistance = nullptr) + { + return hlsl::CCameraMathUtilities::tryScalePathStateDistance( + desiredDistance, + minU, + ioState.u, + ioState.v, + outAppliedDistance); + } + + /// @brief Update the distance encoded by a path state while respecting the provided limits. + static inline bool tryUpdatePathStateDistance( + const float desiredDistance, + const SCameraPathLimits& limits, + ICamera::PathState& ioState, + SCameraPathDistanceUpdateResult* outResult = nullptr) + { + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits) || !sanitizePathState(ioState, sanitizedLimits)) + return false; + + const auto clampedDistance = std::clamp(desiredDistance, sanitizedLimits.minDistance, sanitizedLimits.maxDistance); + double appliedDistance = 0.0; + if (!tryScalePathStateDistance(static_cast(clampedDistance), sanitizedLimits.minU, ioState, &appliedDistance)) + return false; + + if (outResult) + { + outResult->appliedDistance = appliedDistance; + outResult->exact = (clampedDistance == desiredDistance) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); + } + return true; + } + + static inline bool tryBuildPathStateFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const double minU, + ICamera::PathState& outState) + { + outState = {}; + if (!hlsl::CCameraMathUtilities::tryBuildPathStateFromPosition( + targetPosition, + position, + minU, + outState.s, + outState.u, + outState.v)) + { + return false; + } + + outState.roll = 0.0; + return true; + } + + static inline bool tryResolvePathState( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const SCameraPathLimits& limits, + const ICamera::PathState* requestedState, + ICamera::PathState& outState) + { + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + + if (requestedState) + { + outState = *requestedState; + return sanitizePathState(outState, sanitizedLimits); + } + + if (tryBuildPathStateFromPosition(targetPosition, position, sanitizedLimits.minU, outState)) + return sanitizePathState(outState, sanitizedLimits); + + outState = makeDefaultPathState(sanitizedLimits.minU); + return sanitizePathState(outState, sanitizedLimits); + } + + static inline bool tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraPathPose& outPose) + { + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + + return hlsl::CCameraMathUtilities::tryBuildPathPoseFromState( + targetPosition, + state.s, + state.u, + state.v, + state.roll, + sanitizedLimits.minU, + sanitizedLimits.minDistance, + sanitizedLimits.maxDistance, + outPose.position, + outPose.orientation, + &outPose.appliedDistance, + &outPose.orbitUv); + } + + static inline bool tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + hlsl::float64_t3& outPosition, + hlsl::camera_quaternion_t& outOrientation, + hlsl::float64_t* outAppliedDistance = nullptr, + hlsl::float64_t2* outOrbitUv = nullptr) + { + SCameraPathPose pathPose = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, pathPose)) + return false; + + outPosition = pathPose.position; + outOrientation = pathPose.orientation; + if (outAppliedDistance) + *outAppliedDistance = pathPose.appliedDistance; + if (outOrbitUv) + *outOrbitUv = pathPose.orbitUv; + return true; + } + + static inline bool pathStatesNearlyEqual( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds = {}) + { + return hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.s, rhs.s) <= thresholds.sToleranceDeg && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.u, rhs.u, thresholds.scalarTolerance) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.v, rhs.v, thresholds.scalarTolerance) && + hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.roll, rhs.roll) <= thresholds.rollToleranceDeg; + } + + static inline bool pathStatesChanged( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds = {}) + { + return !pathStatesNearlyEqual(lhs, rhs, thresholds); + } + + static inline hlsl::float64_t4 buildPathStateDeltaVector( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) + { + auto deltaVector = desiredState.asVector() - currentState.asVector(); + deltaVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.x); + deltaVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.w); + return deltaVector; + } + + static inline SCameraPathDelta buildPathStateDelta( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) + { + return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); + } + + static inline SCameraPathDelta makePathDeltaFromVirtualPathMotion( + const hlsl::float64_t3& translation, + const hlsl::float64_t3& rotation = hlsl::float64_t3(0.0)) + { + return SCameraPathDelta::fromMotion(translation, rotation.z); + } + + static inline SCameraPathDelta buildDefaultPathControlDelta(const SCameraPathControlContext& context) + { + return makePathDeltaFromVirtualPathMotion(context.translation, context.rotation); + } + + static inline void appendPathDeltaEvents( + std::vector& events, + const SCameraPathDelta& delta, + const double moveDenominator, + const double rotationDenominator, + const SCameraPathComparisonThresholds& thresholds = {}) + { + CCameraVirtualEventUtilities::appendLocalTranslationEvents( + events, + delta.translationVector(), + hlsl::float64_t3(moveDenominator), + hlsl::float64_t3(thresholds.scalarTolerance)); + CCameraVirtualEventUtilities::appendAngularDeltaEvent( + events, + delta.roll, + rotationDenominator, + thresholds.rollToleranceDeg, + CVirtualGimbalEvent::RollRight, + CVirtualGimbalEvent::RollLeft); + } + + static inline bool tryBuildCanonicalPathState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraCanonicalPathState& outState) + { + outState = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, outState.pose)) + return false; + + outState.targetRelative = { + .target = targetPosition, + .orbitUv = outState.pose.orbitUv, + .distance = static_cast(outState.pose.appliedDistance) + }; + return true; + } + + static inline bool tryApplyPathStateDelta( + const ICamera::PathState& currentState, + const SCameraPathDelta& delta, + const SCameraPathLimits& limits, + ICamera::PathState& outState) + { + auto stateVector = currentState.asVector() + delta.asVector(); + stateVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.x); + stateVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.w); + outState = ICamera::PathState::fromVector(stateVector); + return sanitizePathState(outState, limits); + } + + static inline ICamera::PathState blendPathStates( + const ICamera::PathState& from, + const ICamera::PathState& to, + const double alpha) + { + const auto fromVector = from.asVector(); + const auto toVector = to.asVector(); + return { + .s = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.x, toVector.x, alpha), + .u = fromVector.y + (toVector.y - fromVector.y) * alpha, + .v = fromVector.z + (toVector.z - fromVector.z) * alpha, + .roll = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.w, toVector.w, alpha) + }; + } + + static inline bool tryBuildPathStateTransition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& currentPosition, + const hlsl::float64_t3& desiredPosition, + const SCameraPathLimits& limits, + const ICamera::PathState* currentStateOverride, + const ICamera::PathState* desiredStateOverride, + SCameraPathStateTransition& outTransition) + { + if (!tryResolvePathState(targetPosition, currentPosition, limits, currentStateOverride, outTransition.current)) + return false; + if (!tryResolvePathState(targetPosition, desiredPosition, limits, desiredStateOverride, outTransition.desired)) + return false; + + outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); + return true; + } + + static inline SCameraPathModel makeDefaultPathModel() + { + return { + .resolveState = + [](const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const SCameraPathLimits& limits, + const ICamera::PathState* requestedState, + ICamera::PathState& outState) -> bool + { + return tryResolvePathState(targetPosition, position, limits, requestedState, outState); + }, + .controlLaw = + [](const SCameraPathControlContext& context) -> SCameraPathDelta + { + return buildDefaultPathControlDelta(context); + }, + .integrate = + [](const ICamera::PathState& currentState, + const SCameraPathDelta& delta, + const SCameraPathLimits& limits, + ICamera::PathState& outState) -> bool + { + return tryApplyPathStateDelta(currentState, delta, limits, outState); + }, + .evaluate = + [](const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraCanonicalPathState& outState) -> bool + { + return tryBuildCanonicalPathState(targetPosition, state, limits, outState); + }, + .updateDistance = + [](const float desiredDistance, + const SCameraPathLimits& limits, + ICamera::PathState& ioState, + SCameraPathDistanceUpdateResult* outResult) -> bool + { + return tryUpdatePathStateDistance(desiredDistance, limits, ioState, outResult); + } + }; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_PATH_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPersistence.hpp b/include/nbl/ext/Cameras/CCameraPersistence.hpp new file mode 100644 index 0000000000..a1f2487acf --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPersistence.hpp @@ -0,0 +1,33 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PERSISTENCE_HPP_ +#define _C_CAMERA_PERSISTENCE_HPP_ + +#include +#include +#include + +#include "CCameraKeyframeTrackPersistence.hpp" +#include "CCameraPresetPersistence.hpp" +#include "nbl/system/path.h" + +namespace nbl::system +{ + +class ISystem; + +/// @brief Serialize a preset collection to JSON. +bool writePresetCollection(std::ostream& out, std::span presets, int indent = 2); +/// @brief Parse a preset collection from JSON. +bool readPresetCollection(std::istream& in, std::vector& presets); + +/// @brief Save a preset collection to disk as JSON. +bool savePresetCollectionToFile(ISystem& system, const path& path, std::span presets, int indent = 2); +/// @brief Load a preset collection from disk. +bool loadPresetCollectionFromFile(ISystem& system, const path& path, std::vector& presets); + +} // namespace nbl::system + +#endif // _C_CAMERA_PERSISTENCE_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPlaybackTimeline.hpp b/include/nbl/ext/Cameras/CCameraPlaybackTimeline.hpp new file mode 100644 index 0000000000..64dfc811e2 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPlaybackTimeline.hpp @@ -0,0 +1,103 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PLAYBACK_TIMELINE_HPP_ +#define _C_CAMERA_PLAYBACK_TIMELINE_HPP_ + +#include "CCameraKeyframeTrack.hpp" + +namespace nbl::core +{ + +/// @brief Shared playback cursor state for camera keyframe tracks. +/// The cursor stores playback state only: playing flag, looping mode, speed, and current time. +struct CCameraPlaybackCursor +{ + bool playing = false; + bool loop = true; + float speed = 1.f; + float time = 0.f; +}; + +/// @brief Outcome of advancing a playback cursor against a keyframe track. +/// This result reports how time changed during one advance step. +struct SCameraPlaybackAdvanceResult +{ + bool hasTrack = false; + bool changedTime = false; + bool wrapped = false; + bool reachedEnd = false; + bool stopped = false; + float duration = 0.f; + float time = 0.f; +}; + +struct CCameraPlaybackTimelineUtilities final +{ +public: + /// @brief Duration of the current playback track in seconds. + static inline float getPlaybackTrackDuration(const CCameraKeyframeTrack& track) + { + if (track.keyframes.empty()) + return 0.f; + + return track.keyframes.back().time; + } + + /// @brief Reset cursor time and stop playback without mutating loop or speed settings. + static inline void resetPlaybackCursor(CCameraPlaybackCursor& cursor, const float time = 0.f) + { + cursor.playing = false; + cursor.time = std::max(0.f, time); + } + + /// @brief Clamp cursor time into the valid time range of the current track. + static inline void clampPlaybackCursorToTrack(const CCameraKeyframeTrack& track, CCameraPlaybackCursor& cursor) + { + CCameraKeyframeTrackUtilities::clampTrackTimeToKeyframes(track, cursor.time); + } + + /// @brief Advance cursor time by `dtSec * speed` and report whether playback wrapped or stopped. + static inline SCameraPlaybackAdvanceResult advancePlaybackCursor(CCameraPlaybackCursor& cursor, const CCameraKeyframeTrack& track, const double dtSec) + { + SCameraPlaybackAdvanceResult result; + result.hasTrack = !track.keyframes.empty(); + result.duration = getPlaybackTrackDuration(track); + result.time = cursor.time; + + if (!result.hasTrack || !cursor.playing) + return result; + + const auto previousTime = cursor.time; + cursor.time += static_cast(dtSec * cursor.speed); + result.changedTime = cursor.time != previousTime; + result.time = cursor.time; + + if (result.duration <= 0.f) + return result; + + if (cursor.loop) + { + while (cursor.time > result.duration) + { + cursor.time -= result.duration; + result.wrapped = true; + } + } + else if (cursor.time > result.duration) + { + cursor.time = result.duration; + cursor.playing = false; + result.reachedEnd = true; + result.stopped = true; + } + + result.time = cursor.time; + return result; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_PLAYBACK_TIMELINE_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPresentationUtilities.hpp b/include/nbl/ext/Cameras/CCameraPresentationUtilities.hpp new file mode 100644 index 0000000000..9b26db9b79 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPresentationUtilities.hpp @@ -0,0 +1,125 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PRESENTATION_UTILITIES_HPP_ +#define _C_CAMERA_PRESENTATION_UTILITIES_HPP_ + +#include + +#include "CCameraTextUtilities.hpp" + +namespace nbl::ui +{ + +/// @brief Shared exactness-oriented filter used by preset presentation surfaces. +enum class EPresetApplyPresentationFilter : uint8_t +{ + All, + Exact, + BestEffort +}; + +/// @brief Shared badge/pill policy derived from one analyzed presentation answer. +struct SCameraGoalApplyPresentationBadges final +{ + bool exact = false; + bool bestEffort = false; + bool dropsState = false; + bool sharedStateOnly = false; + bool blocked = false; +}; + +/// @brief Presentation-ready wrapper around analyzed goal apply compatibility. +struct SCameraGoalApplyPresentation final : core::SCameraGoalApplyAnalysis +{ + SCameraGoalApplyPresentationBadges badges; + std::string sourceKindLabel; + std::string goalStateLabel; + std::string compatibilityLabel; + std::string policyLabel; + + inline bool matchesFilter(const EPresetApplyPresentationFilter mode) const + { + switch (mode) + { + case EPresetApplyPresentationFilter::All: + return true; + case EPresetApplyPresentationFilter::Exact: + return hasCamera && exact(); + case EPresetApplyPresentationFilter::BestEffort: + return hasCamera && !exact(); + default: + return true; + } + } +}; + +/// @brief Presentation-ready wrapper around analyzed camera capture viability. +struct SCameraCapturePresentation final : core::SCameraCaptureAnalysis +{ + std::string policyLabel; +}; + +struct CCameraPresentationUtilities final +{ + /// @brief Shared user-facing label for the exactness filter selector. + static inline const char* getPresetApplyPresentationFilterLabel(const EPresetApplyPresentationFilter mode) + { + switch (mode) + { + case EPresetApplyPresentationFilter::All: + return "All"; + case EPresetApplyPresentationFilter::Exact: + return "Exact"; + case EPresetApplyPresentationFilter::BestEffort: + return "Best-effort"; + default: + return "All"; + } + } + + /// @brief Build reusable badge flags for one preset/keyframe compatibility answer. + static inline SCameraGoalApplyPresentationBadges collectGoalApplyPresentationBadges(const SCameraGoalApplyPresentation& presentation) + { + SCameraGoalApplyPresentationBadges badges; + badges.exact = presentation.exact(); + badges.bestEffort = presentation.hasCamera && !presentation.exact(); + badges.dropsState = presentation.dropsGoalState(); + badges.sharedStateOnly = presentation.usesSharedStateOnly(); + badges.blocked = !presentation.canApply; + return badges; + } + + /// @brief Build presentation text for one analyzed goal-apply result. + static inline SCameraGoalApplyPresentation makeGoalApplyPresentation(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) + { + SCameraGoalApplyPresentation presentation; + static_cast(presentation) = analysis; + presentation.badges = collectGoalApplyPresentationBadges(presentation); + presentation.sourceKindLabel = std::string(CCameraTextUtilities::getCameraTypeLabel(presentation.goal.sourceKind)); + presentation.goalStateLabel = CCameraTextUtilities::describeGoalStateMask(presentation.goal.sourceGoalStateMask); + presentation.compatibilityLabel = CCameraTextUtilities::describeGoalApplyCompatibility(analysis, targetCamera); + presentation.policyLabel = CCameraTextUtilities::describeGoalApplyPolicy(analysis); + return presentation; + } + + /// @brief Analyze one preset against one camera and return reusable presentation data. + static inline SCameraGoalApplyPresentation analyzePresetPresentation(const core::CCameraGoalSolver& solver, const core::ICamera* camera, const core::CCameraPreset& preset) + { + return makeGoalApplyPresentation(core::CCameraGoalAnalysisUtilities::analyzePresetApply(solver, camera, preset), camera); + } + + /// @brief Analyze one camera capture path and return reusable presentation data. + static inline SCameraCapturePresentation analyzeCapturePresentation(const core::CCameraGoalSolver& solver, core::ICamera* camera) + { + SCameraCapturePresentation presentation; + static_cast(presentation) = core::CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera); + presentation.policyLabel = CCameraTextUtilities::describeCameraCapturePolicy(presentation, camera); + return presentation; + } +}; + +} // namespace nbl::ui + +#endif // _C_CAMERA_PRESENTATION_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPreset.hpp b/include/nbl/ext/Cameras/CCameraPreset.hpp new file mode 100644 index 0000000000..a04be5e8ce --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPreset.hpp @@ -0,0 +1,71 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PRESET_HPP_ +#define _C_CAMERA_PRESET_HPP_ + +#include +#include + +#include "CCameraGoal.hpp" + +namespace nbl::core +{ + +/// @brief Named persisted camera state built on top of `CCameraGoal`. +struct CCameraPreset +{ + std::string name; + std::string identifier; + CCameraGoal goal = {}; +}; + +/// @brief Time-stamped preset entry used by playback and authoring tools. +struct CCameraKeyframe +{ + CCameraPreset preset; + float time = 0.f; +}; + +struct CCameraPresetUtilities final +{ + static inline void assignGoalToPreset(CCameraPreset& preset, const CCameraGoal& goal) + { + preset.goal = CCameraGoalUtilities::canonicalizeGoal(goal); + } + + static inline CCameraGoal makeGoalFromPreset(const CCameraPreset& preset) + { + return CCameraGoalUtilities::canonicalizeGoal(preset.goal); + } + + /// @brief Compare two named presets through their shared canonical goal state. + static inline bool comparePresets(const CCameraPreset& lhs, const CCameraPreset& rhs, + const double posEps, const double rotEpsDeg, const double scalarEps) + { + return lhs.name == rhs.name && + lhs.identifier == rhs.identifier && + CCameraGoalUtilities::compareGoals(makeGoalFromPreset(lhs), makeGoalFromPreset(rhs), posEps, rotEpsDeg, scalarEps); + } + + /// @brief Compare two preset collections element-by-element through the shared canonical goal state. + static inline bool comparePresetCollections(std::span lhs, std::span rhs, + const double posEps, const double rotEpsDeg, const double scalarEps) + { + if (lhs.size() != rhs.size()) + return false; + + for (size_t i = 0u; i < lhs.size(); ++i) + { + if (!comparePresets(lhs[i], rhs[i], posEps, rotEpsDeg, scalarEps)) + return false; + } + + return true; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_PRESET_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPresetFlow.hpp b/include/nbl/ext/Cameras/CCameraPresetFlow.hpp new file mode 100644 index 0000000000..8655b6bd21 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPresetFlow.hpp @@ -0,0 +1,150 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PRESET_FLOW_HPP_ +#define _C_CAMERA_PRESET_FLOW_HPP_ + +#include +#include +#include +#include + +#include "CCameraGoalAnalysis.hpp" + +namespace nbl::core +{ + +/// @brief Reusable aggregate summary for applying one preset to multiple cameras. +struct SCameraPresetApplySummary +{ + uint32_t targetCount = 0u; + uint32_t successCount = 0u; + uint32_t approximateCount = 0u; + uint32_t failureCount = 0u; + + inline bool hasTargets() const + { + return targetCount > 0u; + } + + inline bool succeeded() const + { + return hasTargets() && failureCount == 0u; + } + + inline bool approximate() const + { + return approximateCount > 0u; + } +}; + +struct CCameraPresetFlowUtilities final +{ + /// @brief Compare the current camera state against a preset using the shared goal representation. + static inline bool comparePresetToCameraState(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset, + const double posEps, const double rotEpsDeg, const double scalarEps) + { + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + return CCameraGoalUtilities::compareGoals( + capture.goal, + CCameraPresetUtilities::makeGoalFromPreset(preset), + posEps, + rotEpsDeg, + scalarEps); + } + + /// @brief Explain the first visible mismatch between a camera state and a preset. + static inline std::string describePresetCameraMismatch(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) + { + const auto capture = solver.captureDetailed(camera); + if (!capture.hasCamera) + return "camera=null"; + if (!capture.captured) + return "goal_state=unavailable"; + if (!capture.finiteGoal) + return "goal_state=invalid"; + + return CCameraGoalUtilities::describeGoalMismatch(capture.goal, CCameraPresetUtilities::makeGoalFromPreset(preset)); + } + + /// @brief Build a preset from an already analyzed capture result. + static inline bool tryCapturePreset(const SCameraCaptureAnalysis& captureAnalysis, ICamera* camera, std::string_view name, CCameraPreset& preset) + { + preset = {}; + preset.name = std::string(name); + if (!captureAnalysis.canCapture || !camera) + return false; + + preset.identifier = std::string(camera->getIdentifier()); + CCameraPresetUtilities::assignGoalToPreset(preset, captureAnalysis.goal); + return true; + } + + /// @brief Capture a preset directly from a camera through the shared goal solver. + static inline bool tryCapturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name, CCameraPreset& preset) + { + return tryCapturePreset(CCameraGoalAnalysisUtilities::analyzeCameraCapture(solver, camera), camera, name, preset); + } + + /// @brief Value-returning convenience wrapper around `tryCapturePreset`. + static inline CCameraPreset capturePreset(const CCameraGoalSolver& solver, ICamera* camera, std::string_view name) + { + CCameraPreset preset; + tryCapturePreset(solver, camera, name, preset); + return preset; + } + + /// @brief Apply a preset through the shared goal solver and preserve detailed apply diagnostics. + static inline CCameraGoalSolver::SApplyResult applyPresetDetailed(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) + { + if (!camera) + return {}; + + return solver.applyDetailed(camera, CCameraPresetUtilities::makeGoalFromPreset(preset)); + } + + /// @brief Bool-returning convenience wrapper around `applyPresetDetailed`. + static inline bool applyPreset(const CCameraGoalSolver& solver, ICamera* camera, const CCameraPreset& preset) + { + return applyPresetDetailed(solver, camera, preset).succeeded(); + } + + /// @brief Fold one detailed apply result into an aggregate preset-apply summary. + static inline void accumulatePresetApplySummary(SCameraPresetApplySummary& summary, const CCameraGoalSolver::SApplyResult& result) + { + ++summary.targetCount; + if (result.succeeded()) + { + ++summary.successCount; + if (result.approximate()) + ++summary.approximateCount; + } + else + { + ++summary.failureCount; + } + } + + /// @brief Apply one preset to a camera range and collect a typed aggregate summary. + static inline SCameraPresetApplySummary applyPresetToCameraRange(const CCameraGoalSolver& solver, std::span cameras, const CCameraPreset& preset) + { + SCameraPresetApplySummary summary; + for (auto* camera : cameras) + { + if (!camera) + continue; + + accumulatePresetApplySummary(summary, applyPresetDetailed(solver, camera, preset)); + } + + return summary; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_PRESET_FLOW_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraPresetPersistence.hpp b/include/nbl/ext/Cameras/CCameraPresetPersistence.hpp new file mode 100644 index 0000000000..298163ab6a --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraPresetPersistence.hpp @@ -0,0 +1,40 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PRESET_PERSISTENCE_HPP_ +#define _C_CAMERA_PRESET_PERSISTENCE_HPP_ + +#include + +#include "CCameraPreset.hpp" +#include "nbl/system/path.h" + +namespace nbl::system +{ + +class ISystem; + +/// @brief Serialize one camera goal into an existing stream. +bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, int indent = 2); +/// @brief Deserialize one camera goal from an existing stream. +bool readGoal(std::istream& in, core::CCameraGoal& goal); + +/// @brief Save one camera goal to a file. +bool saveGoalToFile(ISystem& system, const path& path, const core::CCameraGoal& goal, int indent = 2); +/// @brief Load one camera goal from a file. +bool loadGoalFromFile(ISystem& system, const path& path, core::CCameraGoal& goal); + +/// @brief Serialize one camera preset into an existing stream. +bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int indent = 2); +/// @brief Deserialize one camera preset from an existing stream. +bool readPreset(std::istream& in, core::CCameraPreset& preset); + +/// @brief Save one camera preset to a file. +bool savePresetToFile(ISystem& system, const path& path, const core::CCameraPreset& preset, int indent = 2); +/// @brief Load one camera preset from a file. +bool loadPresetFromFile(ISystem& system, const path& path, core::CCameraPreset& preset); + +} // namespace nbl::system + +#endif // _C_CAMERA_PRESET_PERSISTENCE_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraProjectionUtilities.hpp b/include/nbl/ext/Cameras/CCameraProjectionUtilities.hpp new file mode 100644 index 0000000000..220cdc2fc6 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraProjectionUtilities.hpp @@ -0,0 +1,36 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_PROJECTION_UTILITIES_HPP_ +#define _C_CAMERA_PROJECTION_UTILITIES_HPP_ + +#include "IPlanarProjection.hpp" + +namespace nbl::core +{ + +struct CCameraProjectionUtilities final +{ + /// @brief Apply a camera-provided dynamic perspective FOV to one planar projection entry. + static inline bool syncDynamicPerspectiveProjection(ICamera* camera, IPlanarProjection::CProjection& projection) + { + if (!camera) + return false; + + const auto& params = projection.getParameters(); + if (params.m_type != IPlanarProjection::CProjection::Perspective) + return false; + + float dynamicFov = 0.0f; + if (!camera->tryGetDynamicPerspectiveFov(dynamicFov)) + return false; + + projection.setPerspective(params.m_zNear, params.m_zFar, dynamicFov); + return true; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_PROJECTION_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp b/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp new file mode 100644 index 0000000000..a4a23c7ede --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp @@ -0,0 +1,564 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ +#define _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ + +#include +#include +#include +#include +#include +#include +#include + +#include "CCameraFollowRegressionUtilities.hpp" +#include "CCameraScriptedRuntime.hpp" +#include "SCameraRigPose.hpp" + +namespace nbl::system +{ + +/// @brief Runtime state for authored scripted checks. +/// +/// This state stores: +/// - the index of the next authored check to evaluate +/// - one baseline pose reference +/// - one step pose reference +struct CCameraScriptedCheckRuntimeState +{ + struct SPoseReference final : core::SCameraRigPose + { + bool valid = false; + }; + + size_t nextCheckIndex = 0u; + SPoseReference baseline = {}; + SPoseReference step = {}; +}; + +/// @brief Shared per-frame evaluation context for authored scripted checks. +struct CCameraScriptedCheckContext +{ + uint64_t frame = 0ull; + core::ICamera* camera = nullptr; + const core::CVirtualGimbalEvent* imguizmoVirtual = nullptr; + uint32_t imguizmoVirtualCount = 0u; + const core::CTrackedTarget* trackedTarget = nullptr; + const core::SCameraFollowConfig* followConfig = nullptr; + const SCameraProjectionContext* followProjectionContext = nullptr; + const core::CCameraGoalSolver* goalSolver = nullptr; +}; + +/// @brief Reusable log entry produced by scripted check evaluation. +struct CCameraScriptedCheckLogEntry +{ + bool failure = false; + std::string text; +}; + +/// @brief Result for one frame worth of scripted checks. +struct CCameraScriptedCheckFrameResult +{ + std::vector logs; + bool hadFailures = false; +}; + +struct CCameraScriptedCheckRunnerUtilities final +{ + static inline void scriptedCheckSetStepReference( + CCameraScriptedCheckRuntimeState& state, + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) + { + state.step.valid = true; + state.step.position = position; + state.step.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); + } + + static inline void scriptedCheckSetBaselineReference( + CCameraScriptedCheckRuntimeState& state, + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) + { + state.baseline.valid = true; + state.baseline.position = position; + state.baseline.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); + scriptedCheckSetStepReference(state, position, orientation); + } + + static inline bool scriptedCheckComputePoseDelta( + const hlsl::float64_t3& currentPosition, + const hlsl::camera_quaternion_t& currentOrientation, + const hlsl::float64_t3& referencePosition, + const hlsl::camera_quaternion_t& referenceOrientation, + hlsl::SCameraPoseDelta& outDelta) + { + return hlsl::CCameraMathUtilities::tryComputePoseDelta( + currentPosition, + currentOrientation, + referencePosition, + referenceOrientation, + outDelta); + } + + template + static inline std::string buildScriptedCheckMessage(Fn&& formatter) + { + std::ostringstream oss; + formatter(oss); + return oss.str(); + } + + static inline void appendScriptedCheckLog( + CCameraScriptedCheckFrameResult& result, + const bool failure, + std::string&& text) + { + result.logs.push_back({ + .failure = failure, + .text = std::move(text) + }); + result.hadFailures = result.hadFailures || failure; + } + + /// @brief Evaluate all authored scripted checks scheduled for the current frame. + static inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( + const std::vector& checks, + CCameraScriptedCheckRuntimeState& state, + const CCameraScriptedCheckContext& context) + { + CCameraScriptedCheckFrameResult result = {}; + + while (state.nextCheckIndex < checks.size() && checks[state.nextCheckIndex].frame == context.frame) + { + const auto& check = checks[state.nextCheckIndex]; + + if (!context.camera) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " no active camera"; + })); + ++state.nextCheckIndex; + continue; + } + + const auto& gimbal = context.camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); + const auto eulerDeg = hlsl::getCastedVector(hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(orientation)); + + if (!hlsl::CCameraMathUtilities::isFiniteVec3(pos) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(orientation) || !hlsl::CCameraMathUtilities::isFiniteVec3(eulerDeg)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " non-finite gimbal state"; + })); + ++state.nextCheckIndex; + continue; + } + + switch (check.kind) + { + case CCameraScriptedInputCheck::Kind::Baseline: + { + scriptedCheckSetBaselineReference(state, pos, orientation); + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(3); + oss << "[script][pass] baseline frame=" << context.frame + << " pos=(" << pos.x << ", " << pos.y << ", " << pos.z << ")" + << " euler_deg=(" << eulerDeg.x << ", " << eulerDeg.y << ", " << eulerDeg.z << ")"; + })); + break; + } + case CCameraScriptedInputCheck::Kind::ImguizmoVirtual: + { + bool ok = true; + if (!context.imguizmoVirtual || context.imguizmoVirtualCount == 0u) + { + ok = false; + } + else + { + for (const auto& expected : check.expectedVirtualEvents) + { + bool found = false; + double actual = 0.0; + for (uint32_t i = 0u; i < context.imguizmoVirtualCount; ++i) + { + if (context.imguizmoVirtual[i].type == expected.type) + { + found = true; + actual = context.imguizmoVirtual[i].magnitude; + break; + } + } + + if (!found || hlsl::abs(actual - expected.magnitude) > check.tolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] imguizmo_virtual frame=" << context.frame + << " type=" << core::CVirtualGimbalEvent::virtualEventToString(expected.type).data() + << " expected=" << expected.magnitude + << " actual=" << actual + << " tol=" << check.tolerance; + })); + } + } + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][pass] imguizmo_virtual frame=" << context.frame + << " events=" << check.expectedVirtualEvents.size(); + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalNear: + { + bool ok = true; + if (check.hasExpectedPos) + { + const double distance = hlsl::length(pos - hlsl::getCastedVector(check.expectedPos)); + if (distance > check.posTolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_near frame=" << context.frame + << " pos_diff=" << distance + << " tol=" << check.posTolerance; + })); + } + } + if (check.hasExpectedEuler) + { + const auto expectedOrientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( + hlsl::getCastedVector(check.expectedEulerDeg)); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, pos, expectedOrientation, poseDelta)) + poseDelta.rotationDeg = std::numeric_limits::infinity(); + const auto rotationDeltaDeg = poseDelta.rotationDeg; + if (rotationDeltaDeg > check.eulerToleranceDeg) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_near frame=" << context.frame + << " rot_delta_deg=" << rotationDeltaDeg + << " tol=" << check.eulerToleranceDeg; + })); + } + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][pass] gimbal_near frame=" << context.frame; + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalDelta: + { + if (!state.baseline.valid) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_delta frame=" << context.frame << " missing baseline"; + })); + break; + } + + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, state.baseline.position, state.baseline.orientation, poseDelta)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_delta frame=" << context.frame << " non-finite pose delta"; + })); + break; + } + + if (poseDelta.position > check.posTolerance || poseDelta.rotationDeg > check.eulerToleranceDeg) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_delta frame=" << context.frame + << " pos_diff=" << poseDelta.position + << " tol=" << check.posTolerance + << " rot_delta_deg=" << poseDelta.rotationDeg + << " tol=" << check.eulerToleranceDeg; + })); + } + else + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] gimbal_delta frame=" << context.frame + << " pos_diff=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalStep: + { + if (!state.step.valid) + { + if (state.baseline.valid) + { + scriptedCheckSetStepReference(state, state.baseline.position, state.baseline.orientation); + } + else + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_step frame=" << context.frame << " missing step reference"; + })); + scriptedCheckSetStepReference(state, pos, orientation); + ++state.nextCheckIndex; + continue; + } + } + + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, state.step.position, state.step.orientation, poseDelta)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_step frame=" << context.frame << " non-finite pose delta"; + })); + scriptedCheckSetStepReference(state, pos, orientation); + break; + } + + bool ok = true; + bool requiresProgress = false; + bool hasProgress = false; + if (check.hasPosDeltaConstraint) + { + if (poseDelta.position > check.posTolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " pos_delta=" << poseDelta.position + << " max=" << check.posTolerance; + })); + } + if (check.minPosDelta > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || poseDelta.position >= check.minPosDelta; + } + } + if (check.hasEulerDeltaConstraint) + { + if (poseDelta.rotationDeg > check.eulerToleranceDeg) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " rot_delta_deg=" << poseDelta.rotationDeg + << " max=" << check.eulerToleranceDeg; + })); + } + if (check.minEulerDeltaDeg > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || poseDelta.rotationDeg >= check.minEulerDeltaDeg; + } + } + if (requiresProgress && !hasProgress) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " missing progress pos_delta=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; + })); + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] gimbal_step frame=" << context.frame + << " pos_delta=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; + })); + } + scriptedCheckSetStepReference(state, pos, orientation); + break; + } + case CCameraScriptedInputCheck::Kind::FollowTargetLock: + { + if (!context.followConfig) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing follow config"; + })); + break; + } + if (!context.trackedTarget) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing tracked target"; + })); + break; + } + if (!context.goalSolver) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing goal solver"; + })); + break; + } + + SCameraFollowRegressionResult regression = {}; + std::string regressionError; + core::CCameraGoal expectedFollowGoal = {}; + const auto thresholds = CCameraFollowRegressionUtilities::makeFollowRegressionThresholds(check.posTolerance, check.eulerToleranceDeg); + const bool ok = core::CCameraFollowUtilities::tryBuildFollowGoal( + *context.goalSolver, + context.camera, + *context.trackedTarget, + *context.followConfig, + expectedFollowGoal) && + CCameraFollowRegressionUtilities::validateFollowTargetContract( + context.camera, + *context.trackedTarget, + *context.followConfig, + expectedFollowGoal, + regression, + ®ressionError, + context.followProjectionContext, + thresholds); + + if (!ok) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << ' ' + << (regressionError.empty() ? "follow validation mismatch" : regressionError); + })); + } + else + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] follow_lock frame=" << context.frame + << " angle_deg=" << regression.lockAngleDeg + << " target_distance=" << regression.targetDistance + << " screen_ndc=" << regression.projectedTarget.radius; + })); + } + break; + } + } + + ++state.nextCheckIndex; + } + + return result; + } +}; + +} // namespace nbl::system + +#endif // _C_CAMERA_SCRIPTED_CHECK_RUNNER_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp b/include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp new file mode 100644 index 0000000000..99d8c34f03 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp @@ -0,0 +1,392 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SCRIPTED_RUNTIME_HPP_ +#define _C_CAMERA_SCRIPTED_RUNTIME_HPP_ + +#include +#include +#include +#include + +#include "CCameraGoal.hpp" +#include "CCameraFollowRegressionUtilities.hpp" +#include "CVirtualGimbalEvent.hpp" +#include "nbl/ui/KeyCodes.h" + +namespace nbl::system +{ + +/// @brief Shared scripted runtime payload used by camera-sequence consumers. +/// +/// This type stores the expanded per-frame events and checks produced from a +/// compact authored camera sequence. +struct CCameraScriptedInputEvent +{ + enum class Type : uint8_t + { + Keyboard, + Mouse, + Imguizmo, + Action, + Goal, + TrackedTargetTransform, + SegmentLabel + }; + + struct KeyboardData + { + enum class Action : uint8_t + { + Uninitialized = 0, + Pressed = 1, + Released = 2 + }; + + ui::E_KEY_CODE key = ui::EKC_NONE; + Action action = Action::Uninitialized; + }; + + struct MouseData + { + enum class Type : uint8_t + { + Uninitialized = 0, + Click = 1, + Scroll = 2, + Movement = 4 + }; + + enum class ClickAction : uint8_t + { + Uninitialized = 0, + Pressed = 1, + Released = 2 + }; + + Type type = Type::Uninitialized; + ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; + ClickAction action = ClickAction::Uninitialized; + int16_t x = 0; + int16_t y = 0; + int16_t dx = 0; + int16_t dy = 0; + int16_t v = 0; + int16_t h = 0; + }; + + struct ActionData + { + enum class Kind : uint8_t + { + SetActiveRenderWindow, + SetActivePlanar, + SetProjectionType, + SetProjectionIndex, + SetUseWindow, + SetLeftHanded, + ResetActiveCamera + }; + + Kind kind = Kind::SetActiveRenderWindow; + int32_t value = 0; + }; + + struct GoalData + { + core::CCameraGoal goal = {}; + bool requireExact = true; + }; + + struct TrackedTargetTransformData + { + hlsl::float64_t4x4 transform = hlsl::float64_t4x4(1.0); + }; + + struct SegmentLabelData + { + std::string label; + }; + + uint64_t frame = 0; + Type type = Type::Keyboard; + KeyboardData keyboard; + MouseData mouse; + hlsl::float32_t4x4 imguizmo = hlsl::float32_t4x4(1.f); + ActionData action; + GoalData goal; + TrackedTargetTransformData trackedTargetTransform; + SegmentLabelData segmentLabel; +}; + +struct CCameraScriptedCheckDefaults final +{ + static constexpr float VirtualEventTolerance = 1e-3f; + static constexpr float PositionTolerance = static_cast(core::SCameraToolingThresholds::DefaultPositionTolerance); + static constexpr float EulerToleranceDeg = static_cast(core::SCameraToolingThresholds::DefaultAngularToleranceDeg); + static constexpr float FollowScreenToleranceNdc = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance; +}; + +struct CCameraScriptedInputCheck +{ + enum class Kind : uint8_t + { + Baseline, + ImguizmoVirtual, + GimbalNear, + GimbalDelta, + GimbalStep, + FollowTargetLock + }; + + struct ExpectedVirtualEvent + { + core::CVirtualGimbalEvent::VirtualEventType type = core::CVirtualGimbalEvent::None; + hlsl::float64_t magnitude = 0.0; + }; + + uint64_t frame = 0; + Kind kind = Kind::Baseline; + float tolerance = CCameraScriptedCheckDefaults::VirtualEventTolerance; + std::vector expectedVirtualEvents; + + hlsl::float32_t3 expectedPos = hlsl::float32_t3(0.f); + hlsl::float32_t3 expectedEulerDeg = hlsl::float32_t3(0.f); + bool hasExpectedPos = false; + bool hasExpectedEuler = false; + float posTolerance = CCameraScriptedCheckDefaults::PositionTolerance; + float eulerToleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg; + float minPosDelta = 0.0f; + float minEulerDeltaDeg = 0.0f; + bool hasPosDeltaConstraint = false; + bool hasEulerDeltaConstraint = false; +}; + +/// @brief Fully expanded scripted timeline shared between authored parsers and runtime consumers. +struct CCameraScriptedTimeline +{ + std::vector events; + std::vector checks; + std::vector captureFrames; + + inline void clear() + { + events.clear(); + checks.clear(); + captureFrames.clear(); + } + + inline bool empty() const + { + return events.empty() && checks.empty() && captureFrames.empty(); + } +}; + +struct CCameraScriptedRuntimeUtilities final +{ + static inline void finalizeScriptedTimeline( + std::vector& events, + std::vector& checks, + std::vector& captureFrames, + const bool disableCaptureFrames = false) + { + std::stable_sort(events.begin(), events.end(), + [](const CCameraScriptedInputEvent& a, const CCameraScriptedInputEvent& b) { return a.frame < b.frame; }); + std::stable_sort(checks.begin(), checks.end(), + [](const CCameraScriptedInputCheck& a, const CCameraScriptedInputCheck& b) { return a.frame < b.frame; }); + if (!captureFrames.empty()) + { + std::sort(captureFrames.begin(), captureFrames.end()); + captureFrames.erase(std::unique(captureFrames.begin(), captureFrames.end()), captureFrames.end()); + } + if (disableCaptureFrames) + captureFrames.clear(); + } + + static inline void finalizeScriptedTimeline(CCameraScriptedTimeline& timeline, const bool disableCaptureFrames = false) + { + finalizeScriptedTimeline(timeline.events, timeline.checks, timeline.captureFrames, disableCaptureFrames); + } + + static inline void appendScriptedActionEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const CCameraScriptedInputEvent::ActionData::Kind kind, + const int32_t value) + { + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Action; + entry.action.kind = kind; + entry.action.value = value; + timeline.events.emplace_back(std::move(entry)); + } + + static inline void appendScriptedGoalEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const core::CCameraGoal& goal, + const bool requireExact = true) + { + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::Goal; + entry.goal.goal = goal; + entry.goal.requireExact = requireExact; + timeline.events.emplace_back(std::move(entry)); + } + + static inline void appendScriptedTrackedTargetTransformEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const hlsl::float64_t4x4& transform) + { + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::TrackedTargetTransform; + entry.trackedTargetTransform.transform = transform; + timeline.events.emplace_back(std::move(entry)); + } + + static inline void appendScriptedSegmentLabelEvent( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + std::string label) + { + CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = CCameraScriptedInputEvent::Type::SegmentLabel; + entry.segmentLabel.label = std::move(label); + timeline.events.emplace_back(std::move(entry)); + } + + static inline void appendScriptedBaselineCheck(CCameraScriptedTimeline& timeline, const uint64_t frame) + { + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::Baseline; + timeline.checks.emplace_back(std::move(entry)); + } + + static inline void appendScriptedGimbalStepCheck( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const bool hasPosDeltaConstraint, + const float posTolerance, + const float minPosDelta, + const bool hasEulerDeltaConstraint, + const float eulerToleranceDeg, + const float minEulerDeltaDeg) + { + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::GimbalStep; + if (hasPosDeltaConstraint) + { + entry.hasPosDeltaConstraint = true; + entry.posTolerance = posTolerance; + entry.minPosDelta = minPosDelta; + } + if (hasEulerDeltaConstraint) + { + entry.hasEulerDeltaConstraint = true; + entry.eulerToleranceDeg = eulerToleranceDeg; + entry.minEulerDeltaDeg = minEulerDeltaDeg; + } + timeline.checks.emplace_back(std::move(entry)); + } + + static inline void appendScriptedFollowTargetLockCheck( + CCameraScriptedTimeline& timeline, + const uint64_t frame, + const float toleranceDeg = CCameraScriptedCheckDefaults::EulerToleranceDeg, + const float screenToleranceNdc = CCameraScriptedCheckDefaults::FollowScreenToleranceNdc) + { + CCameraScriptedInputCheck entry; + entry.frame = frame; + entry.kind = CCameraScriptedInputCheck::Kind::FollowTargetLock; + entry.eulerToleranceDeg = toleranceDeg; + entry.posTolerance = screenToleranceNdc; + timeline.checks.emplace_back(std::move(entry)); + } +}; + +/// @brief Per-frame scripted runtime batch already partitioned by payload kind. +/// +/// Consumers can dequeue authored events for one frame and then adapt only the buckets they care +/// about, without repeatedly switching on `CCameraScriptedInputEvent::Type` in local glue. +struct CCameraScriptedFrameEvents +{ + std::vector keyboard; + std::vector mouse; + std::vector imguizmo; + std::vector actions; + std::vector goals; + std::vector trackedTargetTransforms; + std::vector segmentLabels; + + inline void clear() + { + keyboard.clear(); + mouse.clear(); + imguizmo.clear(); + actions.clear(); + goals.clear(); + trackedTargetTransforms.clear(); + segmentLabels.clear(); + } + + inline bool empty() const + { + return keyboard.empty() && mouse.empty() && imguizmo.empty() && actions.empty() && + goals.empty() && trackedTargetTransforms.empty() && segmentLabels.empty(); + } +}; + +/// @brief Dequeue all authored scripted events scheduled for one frame. +struct CCameraScriptedFrameEventUtilities final +{ + static inline void dequeueScriptedFrameEvents( + const std::vector& events, + size_t& nextEventIndex, + const uint64_t frame, + CCameraScriptedFrameEvents& out) + { + out.clear(); + while (nextEventIndex < events.size() && events[nextEventIndex].frame == frame) + { + const auto& ev = events[nextEventIndex]; + switch (ev.type) + { + case CCameraScriptedInputEvent::Type::Keyboard: + out.keyboard.emplace_back(ev.keyboard); + break; + case CCameraScriptedInputEvent::Type::Mouse: + out.mouse.emplace_back(ev.mouse); + break; + case CCameraScriptedInputEvent::Type::Imguizmo: + out.imguizmo.emplace_back(ev.imguizmo); + break; + case CCameraScriptedInputEvent::Type::Action: + out.actions.emplace_back(ev.action); + break; + case CCameraScriptedInputEvent::Type::Goal: + out.goals.emplace_back(ev.goal); + break; + case CCameraScriptedInputEvent::Type::TrackedTargetTransform: + out.trackedTargetTransforms.emplace_back(ev.trackedTargetTransform); + break; + case CCameraScriptedInputEvent::Type::SegmentLabel: + out.segmentLabels.emplace_back(ev.segmentLabel.label); + break; + } + + ++nextEventIndex; + } + } +}; + +} // namespace nbl::system + +#endif diff --git a/include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp b/include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp new file mode 100644 index 0000000000..58e6f2dbb0 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp @@ -0,0 +1,74 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ +#define _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ + +#include +#include +#include +#include +#include + +#include "CCameraScriptedRuntime.hpp" +#include "CCameraSequenceScriptPersistence.hpp" + +namespace nbl::system +{ + +class ISystem; + +/// @brief Optional scripted control overrides parsed alongside one runtime payload. +struct CCameraScriptedControlOverrides +{ + bool hasKeyboardScale = false; + float keyboardScale = 1.f; + bool hasMouseMoveScale = false; + float mouseMoveScale = 1.f; + bool hasMouseScrollScale = false; + float mouseScrollScale = 1.f; + bool hasTranslationScale = false; + float translationScale = 1.f; + bool hasRotationScale = false; + float rotationScale = 1.f; +}; + +/// @brief Parsed low-level scripted runtime payload plus optional compact authored sequence. +struct CCameraScriptedInputParseResult +{ + bool enabled = true; + bool hasLog = false; + bool log = false; + bool hardFail = false; + bool visualDebug = false; + float visualTargetFps = 0.f; + float visualCameraHoldSeconds = 0.f; + bool hasEnableActiveCameraMovement = false; + bool enableActiveCameraMovement = true; + bool exclusive = false; + std::string capturePrefix = "script"; + CCameraScriptedControlOverrides cameraControls = {}; + CCameraScriptedTimeline timeline = {}; + std::optional sequence; + std::vector warnings; +}; + +struct CCameraScriptedRuntimePersistenceUtilities final +{ + static inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out, std::string warning) + { + out.warnings.emplace_back(std::move(warning)); + } +}; + +/// @brief Parse one low-level scripted runtime payload from an existing stream. +bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); +/// @brief Parse one low-level scripted runtime payload directly from text. +bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error = nullptr); +/// @brief Load one low-level scripted runtime payload from a file. +bool loadCameraScriptedInputFromFile(ISystem& system, const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); + +} // namespace nbl::system + +#endif // _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp b/include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp new file mode 100644 index 0000000000..22dd66cf4f --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp @@ -0,0 +1,98 @@ +#ifndef _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ +#define _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ + +#include +#include + +#include "CCameraScriptedRuntime.hpp" +#include "nbl/ui/SInputEvent.h" + +namespace nbl::ui +{ + +/// @brief Convert authored scripted keyboard and mouse payloads into runtime UI input events. +/// +/// The scripted runtime stores compact authoring-friendly payloads. This helper +/// expands them into the concrete `SKeyboardEvent` and `SMouseEvent` objects +/// consumed by the same input path as live window events. +struct CCameraScriptedUiInputUtilities final +{ + /// @brief Build one runtime keyboard event from authored scripted keyboard data. + static inline SKeyboardEvent makeScriptedKeyboardEvent( + const std::chrono::microseconds timestamp, + IWindow* const window, + const system::CCameraScriptedInputEvent::KeyboardData& authoredKeyboard) + { + SKeyboardEvent event(timestamp); + event.keyCode = authoredKeyboard.key; + event.action = + authoredKeyboard.action == system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed ? + SKeyboardEvent::ECA_PRESSED : + SKeyboardEvent::ECA_RELEASED; + event.window = window; + return event; + } + + /// @brief Build one runtime mouse event from authored scripted mouse data. + static inline bool tryBuildScriptedMouseEvent( + const std::chrono::microseconds timestamp, + IWindow* const window, + const system::CCameraScriptedInputEvent::MouseData& authoredMouse, + SMouseEvent& outEvent) + { + outEvent = SMouseEvent(timestamp); + outEvent.window = window; + + switch (authoredMouse.type) + { + case system::CCameraScriptedInputEvent::MouseData::Type::Click: + outEvent.type = SMouseEvent::EET_CLICK; + outEvent.clickEvent.mouseButton = authoredMouse.button; + outEvent.clickEvent.action = + authoredMouse.action == system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? + SMouseEvent::SClickEvent::EA_PRESSED : + SMouseEvent::SClickEvent::EA_RELEASED; + outEvent.clickEvent.clickPosX = authoredMouse.x; + outEvent.clickEvent.clickPosY = authoredMouse.y; + return true; + case system::CCameraScriptedInputEvent::MouseData::Type::Scroll: + outEvent.type = SMouseEvent::EET_SCROLL; + outEvent.scrollEvent.verticalScroll = authoredMouse.v; + outEvent.scrollEvent.horizontalScroll = authoredMouse.h; + return true; + case system::CCameraScriptedInputEvent::MouseData::Type::Movement: + outEvent.type = SMouseEvent::EET_MOVEMENT; + outEvent.movementEvent.relativeMovementX = authoredMouse.dx; + outEvent.movementEvent.relativeMovementY = authoredMouse.dy; + return true; + default: + return false; + } + } + + /// @brief Append one authored scripted input batch to existing runtime event buffers. + static inline void appendScriptedUiInputEvents( + const std::chrono::microseconds timestamp, + IWindow* const window, + const std::vector& authoredKeyboard, + const std::vector& authoredMouse, + std::vector& outKeyboard, + std::vector& outMouse) + { + outKeyboard.reserve(outKeyboard.size() + authoredKeyboard.size()); + for (const auto& keyboardEvent : authoredKeyboard) + outKeyboard.emplace_back(makeScriptedKeyboardEvent(timestamp, window, keyboardEvent)); + + outMouse.reserve(outMouse.size() + authoredMouse.size()); + for (const auto& mouseEvent : authoredMouse) + { + SMouseEvent builtEvent(timestamp); + if (tryBuildScriptedMouseEvent(timestamp, window, mouseEvent, builtEvent)) + outMouse.emplace_back(builtEvent); + } + } +}; + +} // namespace nbl::ui + +#endif // _C_CAMERA_SCRIPTED_UI_INPUT_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraSequenceScript.hpp b/include/nbl/ext/Cameras/CCameraSequenceScript.hpp new file mode 100644 index 0000000000..e4e459d698 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraSequenceScript.hpp @@ -0,0 +1,792 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SEQUENCE_SCRIPT_HPP_ +#define _C_CAMERA_SEQUENCE_SCRIPT_HPP_ + +#include +#include +#include +#include +#include +#include + +#include "CCameraMathUtilities.hpp" +#include "CCameraKeyframeTrack.hpp" +#include "CCameraPathUtilities.hpp" +#include "CCameraTargetRelativeUtilities.hpp" +#include "IPlanarProjection.hpp" + +namespace nbl::core +{ + +/// @brief Compact authored camera-sequence format shared by playback, scripting, and validation helpers. +/// +/// The authored file describes: +/// +/// - which camera kind a segment targets +/// - which reusable projection presentations are shown +/// - which keyframed camera goals are sampled over time +/// - which tracked-target poses are sampled over time +/// - which continuity thresholds and capture points are generated +/// +/// The format does not store: +/// +/// - per-frame low-level event dumps +/// - runtime-specific window actions as authored source data +/// - ImGuizmo transforms as the primary authored primitive +/// +/// Consumers may expand the compact sequence into runtime events and per-frame +/// checks. The authored data remains camera-domain data and is not a device- or +/// UI-specific event dump. + +/// @brief Authored projection view request for camera-sequence playback. +struct CCameraSequencePresentation +{ + IPlanarProjection::CProjection::ProjectionType projection = IPlanarProjection::CProjection::Perspective; + bool leftHanded = true; +}; + +/// @brief Shared continuity thresholds authored once and reused per sequence segment. +/// Max bounds are enforced per-step, while minimum progress can be satisfied by either position or rotation change. +struct CCameraSequenceContinuitySettings +{ + bool baseline = true; + bool step = true; + bool hasPosDeltaConstraint = true; + float minPosDelta = 0.00025f; + float maxPosDelta = 2.f; + bool hasEulerDeltaConstraint = false; + float minEulerDeltaDeg = 0.f; + float maxEulerDeltaDeg = 1.f; +}; + +/// @brief Relative goal adjustment authored against an initial preset captured from the target camera. +/// Deltas stay camera-domain and avoid binding the authored file to any specific input device or consumer. +struct CCameraSequenceGoalDelta +{ + struct SOrbitDelta final + { + hlsl::float64_t2 uvDeltaRad = hlsl::float64_t2(0.0); + float distanceDelta = 0.f; + bool hasU = false; + bool hasV = false; + bool hasDistance = false; + + inline bool hasAny() const + { + return hasU || hasV || hasDistance; + } + + inline void setUDeltaDeg(const double valueDeg) + { + uvDeltaRad.x = static_cast(hlsl::radians(valueDeg)); + hasU = true; + } + + inline void setVDeltaDeg(const double valueDeg) + { + uvDeltaRad.y = static_cast(hlsl::radians(valueDeg)); + hasV = true; + } + + inline void setDistanceDelta(const float valueScalar) + { + distanceDelta = valueScalar; + hasDistance = true; + } + }; + + struct SPathDelta final + { + SCameraPathDelta value = {}; + bool hasS = false; + bool hasU = false; + bool hasV = false; + bool hasRoll = false; + + inline bool hasAny() const + { + return hasS || hasU || hasV || hasRoll; + } + + inline void setSDeltaDeg(const double valueDeg) + { + value.s = static_cast(hlsl::radians(valueDeg)); + hasS = true; + } + + inline void setUDelta(const double valueScalar) + { + value.u = static_cast(valueScalar); + hasU = true; + } + + inline void setVDelta(const double valueScalar) + { + value.v = static_cast(valueScalar); + hasV = true; + } + + inline void setRollDeltaDeg(const double valueDeg) + { + value.roll = static_cast(hlsl::radians(valueDeg)); + hasRoll = true; + } + + inline SCameraPathDelta buildAppliedDelta() const + { + SCameraPathDelta delta = {}; + if (hasS) + delta.s = value.s; + if (hasU) + delta.u = value.u; + if (hasV) + delta.v = value.v; + if (hasRoll) + delta.roll = value.roll; + return delta; + } + }; + + bool hasPositionOffset = false; + hlsl::float64_t3 positionOffset = hlsl::float64_t3(0.0); + + bool hasRotationEulerDegOffset = false; + hlsl::float32_t3 rotationEulerDegOffset = hlsl::float32_t3(0.f); + + bool hasTargetOffset = false; + hlsl::float64_t3 targetOffset = hlsl::float64_t3(0.0); + + SOrbitDelta orbitDelta = {}; + + SPathDelta pathDelta = {}; + + bool hasDynamicBaseFovDelta = false; + float dynamicBaseFovDelta = 0.f; + + bool hasDynamicReferenceDistanceDelta = false; + float dynamicReferenceDistanceDelta = 0.f; +}; + +/// @brief One authored keyframe inside a reusable camera-sequence segment. +/// A keyframe can be described either as an absolute preset or as a delta relative to the captured reference preset. +struct CCameraSequenceKeyframe +{ + float time = 0.f; + bool hasAbsolutePreset = false; + CCameraPreset absolutePreset = {}; + bool hasDelta = false; + CCameraSequenceGoalDelta delta = {}; +}; + +/// @brief Concrete tracked-target pose sampled from a shared authored sequence. +struct CCameraSequenceTrackedTargetPose final : SCameraRigPose +{ +}; + +/// @brief Relative tracked-target adjustment authored against an initial tracked-target pose. +struct CCameraSequenceTrackedTargetDelta +{ + bool hasPositionOffset = false; + hlsl::float64_t3 positionOffset = hlsl::float64_t3(0.0); + + bool hasRotationEulerDegOffset = false; + hlsl::float32_t3 rotationEulerDegOffset = hlsl::float32_t3(0.f); +}; + +/// @brief One authored tracked-target keyframe inside a reusable camera-sequence segment. +/// Target keyframes stay camera-domain and can drive follow behavior without runtime-object references. +struct CCameraSequenceTrackedTargetKeyframe +{ + float time = 0.f; + bool hasAbsolutePosition = false; + hlsl::float64_t3 absolutePosition = hlsl::float64_t3(0.0); + bool hasAbsoluteRotationEulerDeg = false; + hlsl::float32_t3 absoluteRotationEulerDeg = hlsl::float32_t3(0.f); + bool hasDelta = false; + CCameraSequenceTrackedTargetDelta delta = {}; +}; + +/// @brief Runtime sampled tracked-target track built from an authored segment plus a reference pose. +/// Keyframes are normalized by time before sampling. Duplicate times collapse to the last authored pose. +struct CCameraSequenceTrackedTargetTrack +{ + struct SKeyframe + { + float time = 0.f; + CCameraSequenceTrackedTargetPose pose = {}; + }; + + std::vector keyframes; +}; + +/// @brief Defaults shared by all camera-sequence segments unless overridden locally. +struct CCameraSequenceSegmentDefaults +{ + float durationSeconds = 4.f; + std::vector presentations; + CCameraSequenceContinuitySettings continuity = {}; + std::vector captureFractions = { 1.f }; + bool resetCamera = true; +}; + +/// @brief Authored reusable camera-sequence segment. +/// A segment is the main unit of authored playback and validation and usually maps to one camera showcase chunk. +struct CCameraSequenceSegment +{ + std::string name; + ICamera::CameraKind cameraKind = ICamera::CameraKind::Unknown; + std::string cameraIdentifier; + + bool hasDurationSeconds = false; + float durationSeconds = 0.f; + + bool hasResetCamera = false; + bool resetCamera = true; + + std::vector presentations; + + bool hasContinuity = false; + CCameraSequenceContinuitySettings continuity = {}; + + bool hasCaptureFractions = false; + std::vector captureFractions; + + std::vector keyframes; + std::vector targetKeyframes; +}; + +/// @brief Top-level reusable camera-sequence script. +/// +/// This type stores the compact authored description that is later expanded +/// into runtime playback and check payloads. +struct CCameraSequenceScript +{ + bool enabled = true; + bool log = false; + bool exclusive = false; + bool hardFail = false; + bool visualDebug = false; + float visualDebugTargetFps = 0.f; + float visualDebugHoldSeconds = 0.f; + bool hasEnableActiveCameraMovement = false; + bool enableActiveCameraMovement = true; + std::string capturePrefix = "script"; + float fps = 60.f; + CCameraSequenceSegmentDefaults defaults = {}; + std::vector segments; +}; + +/// @brief Reusable compiled sequence segment derived from authored data plus captured references. +/// Consumers can build their own runtime actions/checks from this normalized representation. +struct CCameraSequenceCompiledSegment +{ + std::string name; + std::vector presentations; + CCameraSequenceContinuitySettings continuity = {}; + bool resetCamera = true; + float durationSeconds = 0.f; + uint64_t durationFrames = 0ull; + std::vector sampleTimes; + std::vector captureFrameOffsets; + CCameraKeyframeTrack track = {}; + CCameraSequenceTrackedTargetTrack trackedTargetTrack = {}; + + inline bool usesTrackedTargetTrack() const + { + return !trackedTargetTrack.keyframes.empty(); + } +}; + +/// @brief One compiled frame policy entry derived from a reusable compiled segment. +/// Consumers can map these booleans to their own runtime checks and capture requests. +struct CCameraSequenceCompiledFramePolicy +{ + uint64_t frameOffset = 0ull; + float sampleTime = 0.f; + bool capture = false; + bool baseline = false; + bool continuityStep = false; + bool followTargetLock = false; +}; + +struct CCameraSequenceScriptUtilities final +{ + static inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) + { + if (value == "FPS") + outKind = ICamera::CameraKind::FPS; + else if (value == "Free") + outKind = ICamera::CameraKind::Free; + else if (value == "Orbit") + outKind = ICamera::CameraKind::Orbit; + else if (value == "Arcball") + outKind = ICamera::CameraKind::Arcball; + else if (value == "Turntable") + outKind = ICamera::CameraKind::Turntable; + else if (value == "TopDown") + outKind = ICamera::CameraKind::TopDown; + else if (value == "Isometric") + outKind = ICamera::CameraKind::Isometric; + else if (value == "Chase") + outKind = ICamera::CameraKind::Chase; + else if (value == "Dolly") + outKind = ICamera::CameraKind::Dolly; + else if (value == "DollyZoom" || value == "Dolly Zoom") + outKind = ICamera::CameraKind::DollyZoom; + else if (value == "PathRig" || value == "Path Rig") + outKind = ICamera::CameraKind::Path; + else + return false; + + return true; + } + + static inline bool tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType) + { + if (value == "perspective" || value == "Perspective") + outType = IPlanarProjection::CProjection::Perspective; + else if (value == "orthographic" || value == "Orthographic") + outType = IPlanarProjection::CProjection::Orthographic; + else + return false; + + return true; + } + + static inline void normalizeCaptureFractions(std::vector& fractions) + { + for (auto& fraction : fractions) + fraction = std::clamp(fraction, 0.f, 1.f); + + std::sort(fractions.begin(), fractions.end()); + fractions.erase(std::unique(fractions.begin(), fractions.end(), + [](const float lhs, const float rhs) { return hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs, rhs, static_cast(SCameraToolingThresholds::ScalarTolerance)); }), + fractions.end()); + } + + static inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr) + { + if (authored.hasAbsolutePreset) + { + outPreset = authored.absolutePreset; + if (outPreset.identifier.empty()) + outPreset.identifier = reference.identifier; + if (outPreset.name.empty()) + outPreset.name = reference.name; + return CCameraGoalUtilities::isGoalFinite(CCameraPresetUtilities::makeGoalFromPreset(outPreset)); + } + + outPreset = reference; + if (!authored.hasDelta) + return true; + + auto goal = CCameraPresetUtilities::makeGoalFromPreset(reference); + const auto& delta = authored.delta; + + const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; + const bool hasSphericalDelta = delta.hasTargetOffset || delta.orbitDelta.hasAny(); + const bool hasPathDelta = delta.pathDelta.hasAny(); + + if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) + { + if (error) + *error = "Sequence keyframe delta cannot mix pose offsets with spherical/path deltas."; + return false; + } + + if (delta.hasPositionOffset) + goal.position += delta.positionOffset; + + if (delta.hasRotationEulerDegOffset) + { + goal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(goal.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); + } + + if (delta.hasTargetOffset) + { + if (!goal.hasTargetPosition) + { + if (error) + *error = "Sequence keyframe target_offset requires target state."; + return false; + } + goal.targetPosition += delta.targetOffset; + } + + if (delta.orbitDelta.hasAny()) + { + if (!goal.hasOrbitState) + { + if (error) + *error = "Sequence keyframe orbit deltas require spherical orbit state."; + return false; + } + + if (delta.orbitDelta.hasU) + goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad(goal.orbitUv.x + delta.orbitDelta.uvDeltaRad.x); + if (delta.orbitDelta.hasV) + { + goal.orbitUv.y = std::clamp( + goal.orbitUv.y + delta.orbitDelta.uvDeltaRad.y, + -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, + SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); + } + if (delta.orbitDelta.hasDistance) + goal.orbitDistance += delta.orbitDelta.distanceDelta; + } + + if (delta.pathDelta.hasAny()) + { + if (!goal.hasPathState) + { + if (error) + *error = "Sequence keyframe path deltas require path state."; + return false; + } + + if (!CCameraPathUtilities::tryApplyPathStateDelta( + goal.pathState, + delta.pathDelta.buildAppliedDelta(), + CCameraPathUtilities::makeDefaultPathLimits(), + goal.pathState)) + { + if (error) + *error = "Sequence keyframe path deltas produced an invalid path state."; + return false; + } + } + + if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) + { + if (!goal.hasDynamicPerspectiveState) + { + if (error) + *error = "Sequence keyframe dynamic perspective deltas require dynamic perspective state."; + return false; + } + if (delta.hasDynamicBaseFovDelta) + goal.dynamicPerspectiveState.baseFov = std::clamp(goal.dynamicPerspectiveState.baseFov + delta.dynamicBaseFovDelta, 1.f, 179.f); + if (delta.hasDynamicReferenceDistanceDelta) + goal.dynamicPerspectiveState.referenceDistance = std::max(0.001f, goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); + } + + if (hasPathDelta || hasSphericalDelta) + { + if (!CCameraGoalUtilities::applyCanonicalGoalState(goal)) + { + if (error) + *error = hasPathDelta ? + "Sequence keyframe failed to canonicalize path state." : + "Sequence keyframe failed to canonicalize spherical state."; + return false; + } + } + + if (!CCameraGoalUtilities::isGoalFinite(goal)) + { + if (error) + *error = "Sequence keyframe produced a non-finite goal."; + return false; + } + + CCameraPresetUtilities::assignGoalToPreset(outPreset, goal); + return true; + } + + static inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error = nullptr) + { + outTrack = {}; + outTrack.keyframes.reserve(segment.keyframes.size()); + + for (const auto& entry : segment.keyframes) + { + CCameraKeyframe keyframe; + keyframe.time = std::max(0.f, entry.time); + if (!buildSequenceKeyframePreset(reference, entry, keyframe.preset, error)) + return false; + outTrack.keyframes.emplace_back(std::move(keyframe)); + } + + CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(outTrack); + CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(outTrack); + return !outTrack.keyframes.empty(); + } + + static inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) + { + return hlsl::CCameraMathUtilities::isFiniteVec3(pose.position) && + hlsl::CCameraMathUtilities::isFiniteQuaternion(pose.orientation); + } + + static inline bool buildSequenceTrackedTargetPoseFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceTrackedTargetKeyframe& authored, + CCameraSequenceTrackedTargetPose& outPose, + std::string* error = nullptr) + { + outPose = reference; + + if (authored.hasAbsolutePosition) + outPose.position = authored.absolutePosition; + if (authored.hasAbsoluteRotationEulerDeg) + outPose.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); + + if (authored.hasDelta) + { + if (authored.delta.hasPositionOffset) + outPose.position += authored.delta.positionOffset; + if (authored.delta.hasRotationEulerDegOffset) + outPose.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(outPose.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); + } + + if (!isSequenceTrackedTargetPoseFinite(outPose)) + { + if (error) + *error = "Sequence target keyframe produced a non-finite pose."; + return false; + } + + return true; + } + + static inline bool buildSequenceTrackedTargetTrackFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceSegment& segment, + CCameraSequenceTrackedTargetTrack& outTrack, + std::string* error = nullptr) + { + outTrack = {}; + outTrack.keyframes.reserve(segment.targetKeyframes.size()); + + for (const auto& entry : segment.targetKeyframes) + { + CCameraSequenceTrackedTargetTrack::SKeyframe keyframe; + keyframe.time = std::max(0.f, entry.time); + if (!buildSequenceTrackedTargetPoseFromReference(reference, entry, keyframe.pose, error)) + return false; + outTrack.keyframes.emplace_back(std::move(keyframe)); + } + + std::stable_sort(outTrack.keyframes.begin(), outTrack.keyframes.end(), + [](const auto& lhs, const auto& rhs) + { + if (lhs.time == rhs.time) + return false; + return lhs.time < rhs.time; + }); + + std::vector normalized; + normalized.reserve(outTrack.keyframes.size()); + for (const auto& keyframe : outTrack.keyframes) + { + if (!normalized.empty() && hlsl::CCameraMathUtilities::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(SCameraToolingThresholds::ScalarTolerance))) + normalized.back() = keyframe; + else + normalized.emplace_back(keyframe); + } + outTrack.keyframes = std::move(normalized); + + return !outTrack.keyframes.empty(); + } + + static inline bool tryBuildSequenceTrackedTargetPoseAtTime( + const CCameraSequenceTrackedTargetTrack& track, + const float time, + CCameraSequenceTrackedTargetPose& outPose) + { + if (track.keyframes.empty()) + return false; + if (track.keyframes.size() == 1u || time <= track.keyframes.front().time) + { + outPose = track.keyframes.front().pose; + return true; + } + if (time >= track.keyframes.back().time) + { + outPose = track.keyframes.back().pose; + return true; + } + + for (size_t ix = 1u; ix < track.keyframes.size(); ++ix) + { + const auto& lhs = track.keyframes[ix - 1u]; + const auto& rhs = track.keyframes[ix]; + if (time > rhs.time) + continue; + + const auto span = std::max(static_cast(SCameraToolingThresholds::ScalarTolerance), rhs.time - lhs.time); + const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); + outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); + outPose.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); + return true; + } + + outPose = track.keyframes.back().pose; + return true; + } + + static inline bool sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment) + { + return !segment.targetKeyframes.empty(); + } + + static inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr) + { + if (segment.hasDurationSeconds) + return std::max(0.f, segment.durationSeconds); + if (script.defaults.durationSeconds > 0.f) + return script.defaults.durationSeconds; + if (track) + return track->keyframes.empty() ? 0.f : track->keyframes.back().time; + return 0.f; + } + + static inline const std::vector& getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + return segment.presentations.empty() ? script.defaults.presentations : segment.presentations; + } + + static inline CCameraSequenceContinuitySettings getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + return segment.hasContinuity ? segment.continuity : script.defaults.continuity; + } + + static inline std::vector getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + auto captures = segment.hasCaptureFractions ? segment.captureFractions : script.defaults.captureFractions; + normalizeCaptureFractions(captures); + return captures; + } + + static inline bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) + { + return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; + } + + static inline bool sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script) + { + if (script.defaults.presentations.size() > 1u) + return true; + + for (const auto& segment : script.segments) + { + if (getSequenceSegmentPresentations(script, segment).size() > 1u) + return true; + } + + return false; + } + + static inline uint64_t buildSequenceDurationFrames(const float durationSeconds, const float fps) + { + const auto safeDuration = std::max(0.f, durationSeconds); + const auto safeFps = std::max(1.f, fps); + return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); + } + + /// @brief Build one sampled time per authored frame in the compiled segment. + static inline void buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) + { + outTimes.clear(); + outTimes.reserve(durationFrames); + + for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) + { + const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; + outTimes.emplace_back(durationSeconds * alpha); + } + } + + /// @brief Expand normalized capture fractions into concrete frame offsets inside the compiled segment. + static inline void buildSequenceCaptureFrameOffsets( + const uint64_t durationFrames, + const std::vector& captureFractions, + std::vector& outOffsets) + { + outOffsets.clear(); + outOffsets.reserve(captureFractions.size()); + + for (const auto fraction : captureFractions) + { + const auto offset = durationFrames > 1u ? + static_cast(std::llround(static_cast(fraction) * static_cast(durationFrames - 1u))) : + 0ull; + outOffsets.emplace_back(offset); + } + + std::sort(outOffsets.begin(), outOffsets.end()); + outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); + } + + /// @brief Compile one authored sequence segment into normalized reusable data for runtime consumers. + static inline bool compileSequenceSegmentFromReference( + const CCameraSequenceScript& script, + const CCameraSequenceSegment& segment, + const CCameraPreset& referencePreset, + const CCameraSequenceTrackedTargetPose& referenceTrackedTargetPose, + CCameraSequenceCompiledSegment& outSegment, + std::string* error = nullptr) + { + outSegment = {}; + outSegment.name = segment.name; + outSegment.presentations = getSequenceSegmentPresentations(script, segment); + outSegment.continuity = getSequenceSegmentContinuity(script, segment); + outSegment.resetCamera = getSequenceSegmentResetCamera(script, segment); + + if (!buildSequenceTrackFromReference(referencePreset, segment, outSegment.track, error)) + return false; + + if (sequenceSegmentUsesTrackedTargetTrack(segment) && + !buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, outSegment.trackedTargetTrack, error)) + { + return false; + } + + outSegment.durationSeconds = getSequenceSegmentDurationSeconds(script, segment, &outSegment.track); + outSegment.durationFrames = buildSequenceDurationFrames(outSegment.durationSeconds, script.fps); + buildSequenceSampleTimes(outSegment.durationSeconds, outSegment.durationFrames, outSegment.sampleTimes); + buildSequenceCaptureFrameOffsets(outSegment.durationFrames, getSequenceSegmentCaptureFractions(script, segment), outSegment.captureFrameOffsets); + return true; + } + + static inline bool buildCompiledSegmentFramePolicies( + const CCameraSequenceCompiledSegment& segment, + std::vector& outPolicies, + const bool includeFollowTargetLock = false) + { + if (segment.sampleTimes.size() != segment.durationFrames) + return false; + + outPolicies.clear(); + outPolicies.reserve(segment.durationFrames); + + size_t captureIx = 0u; + for (uint64_t frameOffset = 0u; frameOffset < segment.durationFrames; ++frameOffset) + { + CCameraSequenceCompiledFramePolicy policy; + policy.frameOffset = frameOffset; + policy.sampleTime = segment.sampleTimes[frameOffset]; + policy.baseline = segment.continuity.baseline && frameOffset == 0u; + policy.continuityStep = segment.continuity.step && frameOffset > 0u; + policy.followTargetLock = includeFollowTargetLock && segment.usesTrackedTargetTrack() && policy.continuityStep; + + while (captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] < frameOffset) + ++captureIx; + policy.capture = captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] == frameOffset; + if (policy.capture) + ++captureIx; + + outPolicies.emplace_back(std::move(policy)); + } + + return true; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_SEQUENCE_SCRIPT_HPP_ + diff --git a/include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp b/include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp new file mode 100644 index 0000000000..1ec56871e5 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp @@ -0,0 +1,29 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ +#define _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ + +#include +#include +#include + +#include "CCameraSequenceScript.hpp" +#include "nbl/system/path.h" + +namespace nbl::system +{ + +class ISystem; + +/// @brief Parse one compact camera-sequence script from an existing stream. +bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error = nullptr); +/// @brief Parse one compact camera-sequence script directly from text. +bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error = nullptr); +/// @brief Load one compact camera-sequence script from a file. +bool loadCameraSequenceScriptFromFile(ISystem& system, const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); + +} // namespace nbl::system + +#endif // _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp b/include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp new file mode 100644 index 0000000000..f8dd5cf970 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp @@ -0,0 +1,128 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_ +#define _C_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_ + +#include + +#include "CCameraScriptedRuntime.hpp" +#include "CCameraSequenceScript.hpp" +#include "ICamera.hpp" + +namespace nbl::system +{ + +/// @brief Build expanded scripted runtime data from a compiled camera-sequence segment. +/// +/// The builder converts compiled sequence frames into the shared runtime event +/// and check payloads used by camera-sequence consumers. +struct CCameraSequenceScriptedSegmentBuildInfo +{ + /// @brief Planar index that receives the compiled segment. + uint32_t planarIx = 0u; + /// @brief Number of windows the consumer can actually route presentation actions to. + size_t availableWindowCount = 1u; + /// @brief Whether secondary-window presentation actions are emitted. + bool useWindow = false; + /// @brief Whether per-frame follow-lock checks are generated for this segment. + bool includeFollowTargetLock = false; +}; + +struct CCameraSequenceScriptedBuilderUtilities final +{ + /// @brief Append one compiled segment as expanded scripted runtime payloads. + static inline bool appendCompiledSequenceSegmentToScriptedTimeline( + CCameraScriptedTimeline& timeline, + const uint64_t baseFrame, + const core::CCameraSequenceCompiledSegment& compiledSegment, + const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, + std::string* error = nullptr) + { + std::vector framePolicies; + if (!core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) + { + if (error) + *error = "Failed to build compiled frame policies."; + return false; + } + + CCameraScriptedRuntimeUtilities::appendScriptedSegmentLabelEvent(timeline, baseFrame, compiledSegment.name); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); + if (!compiledSegment.presentations.empty()) + { + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); + } + if (compiledSegment.resetCamera) + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); + + if (buildInfo.useWindow) + { + for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), buildInfo.availableWindowCount); ++windowIx) + { + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); + } + CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); + } + + for (const auto& policy : framePolicies) + { + core::CCameraPreset preset; + if (!core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) + { + if (error) + *error = "Failed to sample compiled segment track."; + return false; + } + CCameraScriptedRuntimeUtilities::appendScriptedGoalEvent( + timeline, + baseFrame + policy.frameOffset, + core::CCameraPresetUtilities::makeGoalFromPreset(preset)); + + if (compiledSegment.usesTrackedTargetTrack()) + { + core::CCameraSequenceTrackedTargetPose trackedTargetPose; + if (!core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) + { + if (error) + *error = "Failed to sample compiled tracked-target track."; + return false; + } + + core::ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); + CCameraScriptedRuntimeUtilities::appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); + } + + if (policy.baseline) + CCameraScriptedRuntimeUtilities::appendScriptedBaselineCheck(timeline, baseFrame + policy.frameOffset); + if (policy.continuityStep) + { + CCameraScriptedRuntimeUtilities::appendScriptedGimbalStepCheck( + timeline, + baseFrame + policy.frameOffset, + compiledSegment.continuity.hasPosDeltaConstraint, + compiledSegment.continuity.maxPosDelta, + compiledSegment.continuity.minPosDelta, + compiledSegment.continuity.hasEulerDeltaConstraint, + compiledSegment.continuity.maxEulerDeltaDeg, + compiledSegment.continuity.minEulerDeltaDeg); + } + if (policy.followTargetLock) + CCameraScriptedRuntimeUtilities::appendScriptedFollowTargetLockCheck(timeline, baseFrame + policy.frameOffset); + if (policy.capture) + timeline.captureFrames.emplace_back(baseFrame + policy.frameOffset); + } + + return true; + } +}; + +} // namespace nbl::system + +#endif diff --git a/include/nbl/ext/Cameras/CCameraSmokeRegressionUtilities.hpp b/include/nbl/ext/Cameras/CCameraSmokeRegressionUtilities.hpp new file mode 100644 index 0000000000..9ca40340b1 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraSmokeRegressionUtilities.hpp @@ -0,0 +1,122 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ +#define _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ + +#include + +#include "CCameraKeyframeTrack.hpp" +#include "CCameraMathUtilities.hpp" +#include "CCameraPresetFlow.hpp" +#include "ICamera.hpp" + +namespace nbl::system +{ + +using SCameraManipulationDelta = hlsl::SCameraPoseDelta; + +struct SCameraSmokeComparisonThresholds final +{ + static constexpr double TinyScalarEpsilon = core::SCameraToolingThresholds::TinyScalarEpsilon; + static constexpr double DefaultPositionTolerance = core::SCameraToolingThresholds::DefaultPositionTolerance; + static constexpr double DefaultAngularToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; + static constexpr double DefaultScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static constexpr double StrictPositionTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static constexpr double StrictAngularToleranceDeg = core::SCameraToolingThresholds::DefaultAngularToleranceDeg; + static constexpr double StrictScalarTolerance = core::SCameraToolingThresholds::ScalarTolerance; + static constexpr double TrackTimeTolerance = core::SCameraToolingThresholds::ScalarTolerance; +}; + +struct CCameraSmokeRegressionUtilities final +{ +public: + /// @brief Measure one camera pose delta against an authored reference pose. + static inline bool tryComputeCameraManipulationDelta( + core::ICamera* camera, + const hlsl::float64_t3& beforePosition, + const hlsl::camera_quaternion_t& beforeOrientation, + SCameraManipulationDelta& outDelta) + { + outDelta = {}; + if (!camera) + return false; + + const auto& gimbal = camera->getGimbal(); + const auto afterPosition = gimbal.getPosition(); + const auto afterOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); + return hlsl::CCameraMathUtilities::tryComputePoseDelta(afterPosition, afterOrientation, beforePosition, beforeOrientation, outDelta); + } + + /// @brief Manipulate a camera and report how far its pose moved in position and Euler-angle terms. + static inline bool tryManipulateCameraAndMeasureDelta( + core::ICamera* camera, + std::span events, + SCameraManipulationDelta& outDelta, + const double tinyEpsilon = SCameraSmokeComparisonThresholds::TinyScalarEpsilon) + { + outDelta = {}; + if (!camera || events.empty()) + return false; + + const auto& beforeGimbal = camera->getGimbal(); + const auto beforePosition = beforeGimbal.getPosition(); + const auto beforeOrientation = hlsl::CCameraMathUtilities::normalizeQuaternion(beforeGimbal.getOrientation()); + if (!hlsl::CCameraMathUtilities::isFiniteVec3(beforePosition) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(beforeOrientation)) + return false; + + if (!camera->manipulate(events)) + return false; + + if (!tryComputeCameraManipulationDelta(camera, beforePosition, beforeOrientation, outDelta)) + return false; + + return outDelta.position > tinyEpsilon || outDelta.rotationDeg > tinyEpsilon; + } + + static inline bool comparePresetToCameraStateWithDefaultThresholds( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CCameraPreset& preset) + { + return core::CCameraPresetFlowUtilities::comparePresetToCameraState( + solver, + camera, + preset, + SCameraSmokeComparisonThresholds::DefaultPositionTolerance, + SCameraSmokeComparisonThresholds::DefaultAngularToleranceDeg, + SCameraSmokeComparisonThresholds::DefaultScalarTolerance); + } + + static inline bool comparePresetToCameraStateWithStrictThresholds( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CCameraPreset& preset) + { + return core::CCameraPresetFlowUtilities::comparePresetToCameraState( + solver, + camera, + preset, + SCameraSmokeComparisonThresholds::StrictPositionTolerance, + SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + SCameraSmokeComparisonThresholds::StrictScalarTolerance); + } + + static inline bool compareKeyframeTrackContentWithStrictThresholds( + const core::CCameraKeyframeTrack& lhs, + const core::CCameraKeyframeTrack& rhs) + { + return core::CCameraKeyframeTrackUtilities::compareKeyframeTrackContent( + lhs, + rhs, + SCameraSmokeComparisonThresholds::TrackTimeTolerance, + SCameraSmokeComparisonThresholds::StrictPositionTolerance, + SCameraSmokeComparisonThresholds::StrictAngularToleranceDeg, + SCameraSmokeComparisonThresholds::StrictScalarTolerance); + } +}; + +} // namespace nbl::system + +#endif // _C_CAMERA_SMOKE_REGRESSION_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp b/include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp new file mode 100644 index 0000000000..7271685f5b --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp @@ -0,0 +1,270 @@ +#ifndef _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ +#define _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ + +#include + +#include "SCameraRigPose.hpp" +#include "CCameraVirtualEventUtilities.hpp" + +namespace nbl::core +{ + +/// @brief Canonical target-relative orbit state used by spherical cameras, follow, and goal solving. +struct SCameraTargetRelativeState final +{ + hlsl::float64_t3 target = hlsl::float64_t3(0.0); + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); + float distance = SCameraTargetRelativeTraits::MinDistance; +}; + +/// @brief Pose reconstructed from a target-relative orbit state. +struct SCameraTargetRelativePose final : SCameraRigPose +{ + hlsl::float64_t appliedDistance = static_cast(SCameraTargetRelativeTraits::MinDistance); +}; + +/// @brief Derived basis for target-relative orbit rigs. +struct SCameraTargetRelativeBasis final +{ + hlsl::float64_t3 localOffset = hlsl::float64_t3(0.0); + hlsl::float64_t3 right = hlsl::float64_t3(1.0, 0.0, 0.0); + hlsl::float64_t3 up = hlsl::float64_t3(0.0, 0.0, 1.0); + hlsl::float64_t3 forward = hlsl::float64_t3(0.0, 1.0, 0.0); +}; + +/// @brief Delta between current spherical target state and canonical target-relative goal. +struct SCameraTargetRelativeDelta final +{ + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); + double distance = 0.0; + + inline hlsl::float64_t3 orbitVector() const + { + return hlsl::float64_t3(orbitUv.y, orbitUv.x, 0.0); + } +}; + +/// @brief Mapping policy describing how a target-relative delta is converted into virtual events. +struct SCameraTargetRelativeEventPolicy final +{ + bool translateOrbit = false; + bool allowYaw = true; + bool allowPitch = true; + SCameraVirtualEventAxisBinding distanceBinding = { + CVirtualGimbalEvent::MoveForward, + CVirtualGimbalEvent::MoveBackward + }; +}; + +/// @brief Default constants and event policies used by target-relative rigs. +struct SCameraTargetRelativeRigDefaults final +{ + static constexpr float InitialDistance = 1.0f; + static constexpr double ArcballPitchLimitRad = hlsl::SCameraViewRigDefaults::ArcballPitchLimitRad; + static constexpr double TurntablePitchLimitRad = hlsl::SCameraViewRigDefaults::TurntablePitchLimitRad; + static constexpr double ChaseMaxPitchRad = hlsl::SCameraViewRigDefaults::ChaseMaxPitchRad; + static constexpr double ChaseMinPitchRad = hlsl::SCameraViewRigDefaults::ChaseMinPitchRad; + static constexpr double DollyPitchLimitRad = hlsl::SCameraViewRigDefaults::DollyPitchLimitRad; + static constexpr double TopDownPitchRad = hlsl::SCameraViewRigDefaults::TopDownPitchRad; + static constexpr double IsometricYawRad = hlsl::SCameraViewRigDefaults::IsometricYawRad; + static constexpr double IsometricPitchRad = hlsl::SCameraViewRigDefaults::IsometricPitchRad; + + static inline constexpr SCameraTargetRelativeEventPolicy OrbitTranslatePolicy = { + .translateOrbit = true + }; + static inline constexpr SCameraTargetRelativeEventPolicy RotateDistancePolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = true + }; + static inline constexpr SCameraTargetRelativeEventPolicy TopDownPolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = false + }; + static inline constexpr SCameraTargetRelativeEventPolicy IsometricPolicy = { + .translateOrbit = false, + .allowYaw = false, + .allowPitch = false + }; + static inline constexpr SCameraTargetRelativeEventPolicy DollyPolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = true, + .distanceBinding = { + CVirtualGimbalEvent::None, + CVirtualGimbalEvent::None + } + }; + static inline constexpr SCameraTargetRelativeEventPolicy ChasePolicy = { + .translateOrbit = false, + .allowYaw = true, + .allowPitch = true, + .distanceBinding = { + CVirtualGimbalEvent::MoveUp, + CVirtualGimbalEvent::MoveDown + } + }; +}; + +/// @brief Helpers for converting between target-relative state, pose, basis, and virtual-event deltas. +struct CCameraTargetRelativeUtilities final +{ + static inline bool tryBuildTargetRelativeStateFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const float minDistance, + const float maxDistance, + SCameraTargetRelativeState& outState) + { + outState = {}; + outState.target = targetPosition; + + hlsl::float64_t appliedDistance = static_cast(minDistance); + if (!hlsl::CCameraMathUtilities::tryBuildOrbitFromPosition( + targetPosition, + position, + static_cast(minDistance), + static_cast(maxDistance), + outState.orbitUv, + appliedDistance)) + { + return false; + } + + outState.distance = static_cast(appliedDistance); + return true; + } + + static inline bool tryBuildTargetRelativePoseFromState( + const SCameraTargetRelativeState& state, + const float minDistance, + const float maxDistance, + SCameraTargetRelativePose& outPose) + { + outPose = {}; + return hlsl::CCameraMathUtilities::tryBuildSphericalPoseFromOrbit( + state.target, + state.orbitUv, + static_cast(state.distance), + static_cast(minDistance), + static_cast(maxDistance), + outPose.position, + outPose.orientation, + &outPose.appliedDistance); + } + + static inline bool tryBuildTargetRelativeBasis( + const SCameraTargetRelativeState& state, + const float minDistance, + const float maxDistance, + SCameraTargetRelativeBasis& outBasis) + { + SCameraTargetRelativePose pose = {}; + if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, pose)) + return false; + + outBasis.localOffset = pose.position - state.target; + const auto basis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(pose.orientation); + outBasis.right = basis[0]; + outBasis.up = basis[1]; + outBasis.forward = basis[2]; + return true; + } + + static inline bool tryBuildTargetRelativePoseFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const float minDistance, + const float maxDistance, + SCameraTargetRelativePose& outPose, + SCameraTargetRelativeState* outState = nullptr) + { + SCameraTargetRelativeState state = {}; + if (!tryBuildTargetRelativeStateFromPosition(targetPosition, position, minDistance, maxDistance, state)) + return false; + + if (!tryBuildTargetRelativePoseFromState(state, minDistance, maxDistance, outPose)) + return false; + + if (outState) + *outState = state; + return true; + } + + static inline SCameraTargetRelativeDelta buildTargetRelativeDelta( + const ICamera::SphericalTargetState& currentState, + const SCameraTargetRelativeState& desiredState) + { + return { + .orbitUv = hlsl::float64_t2( + hlsl::CCameraMathUtilities::wrapAngleRad(desiredState.orbitUv.x - currentState.orbitUv.x), + hlsl::CCameraMathUtilities::wrapAngleRad(desiredState.orbitUv.y - currentState.orbitUv.y)), + .distance = static_cast(desiredState.distance - currentState.distance) + }; + } + + static inline void appendTargetRelativeDeltaEvents( + std::vector& events, + const SCameraTargetRelativeDelta& delta, + const double angularDenominator, + const double angularToleranceDeg, + const double distanceDenominator, + const double distanceTolerance, + const SCameraTargetRelativeEventPolicy& policy) + { + if (policy.translateOrbit) + { + CCameraVirtualEventUtilities::appendAngularAxisEvents( + events, + delta.orbitVector(), + hlsl::float64_t3(angularDenominator), + hlsl::float64_t3(angularToleranceDeg, angularToleranceDeg, std::numeric_limits::infinity()), + {{ + { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, + { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, + { CVirtualGimbalEvent::None, CVirtualGimbalEvent::None } + }}); + } + else + { + if (policy.allowYaw) + { + CCameraVirtualEventUtilities::appendAngularDeltaEvent( + events, + delta.orbitUv.x, + angularDenominator, + angularToleranceDeg, + CVirtualGimbalEvent::PanRight, + CVirtualGimbalEvent::PanLeft); + } + if (policy.allowPitch) + { + CCameraVirtualEventUtilities::appendAngularDeltaEvent( + events, + delta.orbitUv.y, + angularDenominator, + angularToleranceDeg, + CVirtualGimbalEvent::TiltUp, + CVirtualGimbalEvent::TiltDown); + } + } + + if (policy.distanceBinding.positive != CVirtualGimbalEvent::None && + policy.distanceBinding.negative != CVirtualGimbalEvent::None) + { + CCameraVirtualEventUtilities::appendScaledVirtualEvent( + events, + delta.distance, + distanceDenominator, + distanceTolerance, + policy.distanceBinding.positive, + policy.distanceBinding.negative); + } + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_TARGET_RELATIVE_UTILITIES_HPP_ + diff --git a/include/nbl/ext/Cameras/CCameraTextUtilities.hpp b/include/nbl/ext/Cameras/CCameraTextUtilities.hpp new file mode 100644 index 0000000000..d07212b399 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraTextUtilities.hpp @@ -0,0 +1,210 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_TEXT_UTILITIES_HPP_ +#define _C_CAMERA_TEXT_UTILITIES_HPP_ + +#include +#include +#include + +#include "CCameraFollowUtilities.hpp" +#include "CCameraGoalAnalysis.hpp" +#include "CCameraPresetFlow.hpp" + +namespace nbl::ui +{ + +struct CCameraTextUtilities final +{ +public: + /// @brief Return a short human-readable label for a camera kind. + static inline std::string_view getCameraTypeLabel(const core::ICamera::CameraKind kind) + { + return core::CCameraKindUtilities::getCameraKindLabel(kind); + } + + /// @brief Return a short human-readable label for a concrete camera instance. + static inline std::string_view getCameraTypeLabel(const core::ICamera* camera) + { + return camera ? getCameraTypeLabel(camera->getKind()) : "Unknown"; + } + + /// @brief Return a short human-readable description for a camera kind. + static inline std::string_view getCameraTypeDescription(const core::ICamera::CameraKind kind) + { + return core::CCameraKindUtilities::getCameraKindDescription(kind); + } + + /// @brief Return a short human-readable description for a concrete camera instance. + static inline std::string_view getCameraTypeDescription(const core::ICamera* camera) + { + return camera ? getCameraTypeDescription(camera->getKind()) : "Unspecified camera behavior"; + } + + /// @brief Return a short human-readable label for a follow mode. + static inline constexpr const char* getCameraFollowModeLabel(const core::ECameraFollowMode mode) + { + switch (mode) + { + case core::ECameraFollowMode::Disabled: return "Disabled"; + case core::ECameraFollowMode::OrbitTarget: return "Orbit target"; + case core::ECameraFollowMode::LookAtTarget: return "Look at target"; + case core::ECameraFollowMode::KeepWorldOffset: return "Keep world offset"; + case core::ECameraFollowMode::KeepLocalOffset: return "Keep local offset"; + default: return "Unknown"; + } + } + + /// @brief Return a short human-readable description for a follow mode. + static inline constexpr const char* getCameraFollowModeDescription(const core::ECameraFollowMode mode) + { + switch (mode) + { + case core::ECameraFollowMode::Disabled: return "Follow disabled"; + case core::ECameraFollowMode::OrbitTarget: return "Keep orbit around moving target and keep it centered"; + case core::ECameraFollowMode::LookAtTarget: return "Keep camera position and lock the view onto the target"; + case core::ECameraFollowMode::KeepWorldOffset: return "Move with the target in world offset and keep it centered"; + case core::ECameraFollowMode::KeepLocalOffset: return "Move with the target in target-local offset and keep it centered"; + default: return "Unknown follow mode"; + } + } + + /// @brief Describe the typed goal-state mask in a stable human-readable format. + static inline std::string describeGoalStateMask(const uint32_t mask) + { + if (mask == core::ICamera::GoalStateNone) + return "Pose only"; + + std::string out; + auto append = [&](const char* label, const uint32_t bit) -> void + { + if ((mask & bit) != bit) + return; + if (!out.empty()) + out += ", "; + out += label; + }; + + append("Spherical target", core::ICamera::GoalStateSphericalTarget); + append("Dynamic perspective", core::ICamera::GoalStateDynamicPerspective); + append("Path rig state", core::ICamera::GoalStatePath); + return out; + } + + /// @brief Describe a detailed goal-apply result for logs, smoke tests, and UI summaries. + static inline std::string describeApplyResult(const core::CCameraGoalSolver::SApplyResult& result) + { + std::ostringstream oss; + oss << "status="; + switch (result.status) + { + case core::CCameraGoalSolver::SApplyResult::EStatus::Unsupported: oss << "Unsupported"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::Failed: oss << "Failed"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AlreadySatisfied: oss << "AlreadySatisfied"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteOnly: oss << "AppliedAbsoluteOnly"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedVirtualEvents: oss << "AppliedVirtualEvents"; break; + case core::CCameraGoalSolver::SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents: oss << "AppliedAbsoluteAndVirtualEvents"; break; + } + oss << " exact=" << (result.exact ? "true" : "false") + << " events=" << result.eventCount; + + if (result.issues != core::CCameraGoalSolver::SApplyResult::NoIssue) + { + oss << " issues="; + bool first = true; + auto appendIssue = [&](const char* label, const core::CCameraGoalSolver::SApplyResult::EIssue issue) -> void + { + if (!result.hasIssue(issue)) + return; + if (!first) + oss << ","; + oss << label; + first = false; + }; + + appendIssue("absolute_pose_fallback", core::CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); + appendIssue("missing_spherical_state", core::CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); + appendIssue("missing_path_state", core::CCameraGoalSolver::SApplyResult::MissingPathState); + appendIssue("missing_dynamic_perspective_state", core::CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); + appendIssue("virtual_event_replay_failed", core::CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); + } + + return oss.str(); + } + + /// @brief Describe compatibility preview for applying one analyzed goal to a target camera. + static inline std::string describeGoalApplyCompatibility(const core::SCameraGoalApplyAnalysis& analysis, const core::ICamera* targetCamera) + { + if (!analysis.hasCamera) + return "No active camera"; + + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact" : "Best-effort") + << " | source=" << getCameraTypeLabel(analysis.goal.sourceKind) + << " | target=" << getCameraTypeLabel(targetCamera); + + if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) + oss << " | missing=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + + return oss.str(); + } + + /// @brief Describe whether an analyzed goal can be meaningfully applied to the target camera. + static inline std::string describeGoalApplyPolicy(const core::SCameraGoalApplyAnalysis& analysis) + { + if (!analysis.hasCamera) + return "Blocked | no active camera"; + if (!analysis.finiteGoal) + return "Blocked | invalid goal state"; + + std::ostringstream oss; + oss << (analysis.compatibility.exact ? "Exact apply" : "Best-effort apply"); + if (analysis.compatibility.missingGoalStateMask != core::ICamera::GoalStateNone) + oss << " | drops=" << describeGoalStateMask(analysis.compatibility.missingGoalStateMask); + else if (!analysis.compatibility.sameKind && analysis.goal.sourceKind != core::ICamera::CameraKind::Unknown) + oss << " | shared goal state only"; + else + oss << " | full preview available"; + + return oss.str(); + } + + /// @brief Describe whether one analyzed camera state can be captured into a reusable goal. + static inline std::string describeCameraCapturePolicy(const core::SCameraCaptureAnalysis& analysis, const core::ICamera* camera) + { + if (!analysis.hasCamera) + return "Blocked | no active camera"; + if (!analysis.capturedGoal) + return "Blocked | goal capture failed"; + if (!analysis.finiteGoal) + return "Blocked | invalid goal state"; + + std::ostringstream oss; + oss << "Ready | source=" << getCameraTypeLabel(camera) + << " | goal=" << describeGoalStateMask(analysis.goal.sourceGoalStateMask); + return oss.str(); + } + + /// @brief Describe the aggregate outcome of applying one preset to multiple cameras. + static inline std::string describePresetApplySummary(const core::SCameraPresetApplySummary& summary, std::string_view noTargetsLabel, std::string_view prefix = "Playback apply") + { + if (!summary.hasTargets()) + return std::string(noTargetsLabel); + + std::ostringstream oss; + oss << prefix << " | targets=" << summary.targetCount << " | ok=" << summary.successCount; + if (summary.approximateCount > 0u) + oss << " | approximate=" << summary.approximateCount; + if (summary.failureCount > 0u) + oss << " | failed=" << summary.failureCount; + return oss.str(); + } +}; + +} // namespace nbl::ui + +#endif // _C_CAMERA_TEXT_UTILITIES_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraTraits.hpp b/include/nbl/ext/Cameras/CCameraTraits.hpp new file mode 100644 index 0000000000..ecc9c2d7a3 --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraTraits.hpp @@ -0,0 +1,41 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_CAMERA_TRAITS_HPP_ +#define _C_CAMERA_TRAITS_HPP_ + +#include + +namespace nbl::core +{ + +/// @brief Geometric constants used by target-relative camera families. +/// +/// `MinDistance` prevents zero-distance target-relative states. +/// `DefaultMaxDistance` is unbounded. Individual cameras and tools may apply +/// their own finite limits on top of it. +struct SCameraTargetRelativeTraits final +{ + /// @brief Smallest valid target-relative distance shared by spherical and path-style rigs. + static inline constexpr float MinDistance = 0.1f; + /// @brief Default upper bound for target-relative distance when no camera-specific cap is requested. + static inline constexpr float DefaultMaxDistance = std::numeric_limits::infinity(); +}; + +/// @brief Comparison thresholds used by helper layers outside the runtime camera interface. +struct SCameraToolingThresholds final +{ + /// @brief Default scalar tolerance used by typed state comparisons. + static inline constexpr double ScalarTolerance = 1e-6; + /// @brief Small epsilon used by replay and comparison helpers that need stricter zero tests. + static inline constexpr double TinyScalarEpsilon = 1e-9; + /// @brief Default world-space position tolerance used by pose comparisons. + static inline constexpr double DefaultPositionTolerance = 2.0 * ScalarTolerance; + /// @brief Default angular tolerance in degrees used by pose and state comparisons. + static inline constexpr double DefaultAngularToleranceDeg = 0.1; +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_TRAITS_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraVirtualEventUtilities.hpp b/include/nbl/ext/Cameras/CCameraVirtualEventUtilities.hpp new file mode 100644 index 0000000000..31baae246e --- /dev/null +++ b/include/nbl/ext/Cameras/CCameraVirtualEventUtilities.hpp @@ -0,0 +1,188 @@ +#ifndef _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ +#define _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ + +#include +#include +#include + +#include "CCameraMathUtilities.hpp" +#include "ICamera.hpp" + +namespace nbl::core +{ + +/// @brief Positive and negative semantic virtual-event pair for one scalar axis. +struct SCameraVirtualEventAxisBinding final +{ + CVirtualGimbalEvent::VirtualEventType positive = CVirtualGimbalEvent::None; + CVirtualGimbalEvent::VirtualEventType negative = CVirtualGimbalEvent::None; +}; + +/// @brief Reusable axis-binding presets shared by helpers that synthesize virtual events. +struct SCameraVirtualEventBindings final +{ + static inline constexpr std::array LocalTranslation = {{ + { CVirtualGimbalEvent::MoveRight, CVirtualGimbalEvent::MoveLeft }, + { CVirtualGimbalEvent::MoveUp, CVirtualGimbalEvent::MoveDown }, + { CVirtualGimbalEvent::MoveForward, CVirtualGimbalEvent::MoveBackward } + }}; +}; + +/// @brief Shared helpers for building and analyzing `CVirtualGimbalEvent` batches. +/// +/// These utilities are reused by goal replay, path-model control translation, +/// scripted tooling, and smoke checks whenever code needs to convert typed deltas +/// into semantic event streams or inspect those streams on the CPU. +struct CCameraVirtualEventUtilities final +{ +public: + /// @brief Append one signed scalar as either the positive or negative event variant. + static inline void appendSignedVirtualEvent( + std::vector& events, + const double value, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative, + const double tolerance = static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) + { + if (!hlsl::CCameraMathUtilities::isFiniteScalar(value) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(value, tolerance)) + return; + + auto& ev = events.emplace_back(); + ev.type = (value > 0.0) ? positive : negative; + ev.magnitude = hlsl::abs(value); + } + + /// @brief Append one signed scalar after normalizing it by a caller-provided denominator. + static inline void appendScaledVirtualEvent( + std::vector& events, + const double value, + const double denominator, + const double tolerance, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative) + { + if (!hlsl::CCameraMathUtilities::isFiniteScalar(denominator) || hlsl::CCameraMathUtilities::isNearlyZeroScalar(denominator, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) + return; + + appendSignedVirtualEvent(events, value / denominator, positive, negative, tolerance); + } + + /// @brief Append one angular delta by comparing it against a tolerance expressed in degrees. + static inline void appendAngularDeltaEvent( + std::vector& events, + const double deltaRadians, + const double denominator, + const double toleranceDeg, + const CVirtualGimbalEvent::VirtualEventType positive, + const CVirtualGimbalEvent::VirtualEventType negative) + { + if (!hlsl::CCameraMathUtilities::isFiniteScalar(deltaRadians) || + hlsl::CCameraMathUtilities::isNearlyZeroScalar(hlsl::degrees(deltaRadians), toleranceDeg)) + { + return; + } + + appendScaledVirtualEvent( + events, + deltaRadians, + denominator, + hlsl::radians(toleranceDeg), + positive, + negative); + } + + /// @brief Append one 3-axis scalar bundle through a caller-provided binding table. + static inline void appendScaledVirtualAxisEvents( + std::vector& events, + const hlsl::float64_t3& values, + const hlsl::float64_t3& denominators, + const hlsl::float64_t3& tolerances, + const std::array& axisBindings) + { + for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) + { + appendScaledVirtualEvent( + events, + values[axisIx], + denominators[axisIx], + tolerances[axisIx], + axisBindings[axisIx].positive, + axisBindings[axisIx].negative); + } + } + + /// @brief Append a local-space translation delta as semantic move events. + static inline void appendLocalTranslationEvents( + std::vector& events, + const hlsl::float64_t3& localDelta, + const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), + const hlsl::float64_t3& tolerances = hlsl::float64_t3(SCameraToolingThresholds::TinyScalarEpsilon)) + { + appendScaledVirtualAxisEvents( + events, + localDelta, + denominators, + tolerances, + SCameraVirtualEventBindings::LocalTranslation); + } + + /// @brief Reinterpret a world-space translation delta in the local frame of a camera orientation. + static inline void appendWorldTranslationAsLocalEvents( + std::vector& events, + const hlsl::camera_quaternion_t& orientation, + const hlsl::float64_t3& worldDelta, + const hlsl::float64_t3& denominators = hlsl::float64_t3(1.0), + const hlsl::float64_t3& tolerances = hlsl::float64_t3(SCameraToolingThresholds::TinyScalarEpsilon)) + { + appendLocalTranslationEvents( + events, + hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(orientation, worldDelta), + denominators, + tolerances); + } + + /// @brief Append one 3-axis angular delta through a caller-provided binding table. + static inline void appendAngularAxisEvents( + std::vector& events, + const hlsl::float64_t3& deltaRadians, + const hlsl::float64_t3& denominators, + const hlsl::float64_t3& toleranceDeg, + const std::array& axisBindings) + { + for (size_t axisIx = 0u; axisIx < axisBindings.size(); ++axisIx) + { + appendAngularDeltaEvent( + events, + deltaRadians[axisIx], + denominators[axisIx], + toleranceDeg[axisIx], + axisBindings[axisIx].positive, + axisBindings[axisIx].negative); + } + } + + /// @brief Accumulate only translation-related virtual events back into a signed delta vector. + static inline hlsl::float64_t3 collectSignedTranslationDelta(std::span events) + { + hlsl::float64_t3 delta = hlsl::float64_t3(0.0); + for (const auto& ev : events) + { + switch (ev.type) + { + case CVirtualGimbalEvent::MoveRight: delta.x += ev.magnitude; break; + case CVirtualGimbalEvent::MoveLeft: delta.x -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveUp: delta.y += ev.magnitude; break; + case CVirtualGimbalEvent::MoveDown: delta.y -= ev.magnitude; break; + case CVirtualGimbalEvent::MoveForward: delta.z += ev.magnitude; break; + case CVirtualGimbalEvent::MoveBackward: delta.z -= ev.magnitude; break; + default: break; + } + } + return delta; + } +}; + +} // namespace nbl::core + +#endif // _C_CAMERA_VIRTUAL_EVENT_UTILITIES_HPP_ + diff --git a/include/nbl/ext/Cameras/CChaseCamera.hpp b/include/nbl/ext/Cameras/CChaseCamera.hpp new file mode 100644 index 0000000000..36d269e077 --- /dev/null +++ b/include/nbl/ext/Cameras/CChaseCamera.hpp @@ -0,0 +1,89 @@ +#ifndef _C_CHASE_CAMERA_HPP_ +#define _C_CHASE_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera with planar target translation on the ground plane. +/// +/// Translation is resolved in a planar forward/right frame derived from the +/// current orbit basis. Rotation updates orbit yaw and pitch. Distance remains +/// clamped to the chase-camera limits. +class CChaseCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CChaseCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(position, target) + { + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); + applyPose(); + } + ~CChaseCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Apply chase-style planar translation, pitch/yaw orbiting, and distance changes. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const auto deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.y); + + const auto basis = computeBasis(m_orbitUv, m_distance); + + const auto planarForward = hlsl::CCameraMathUtilities::safeNormalizeVec3( + hlsl::float64_t3(basis.forward.x, 0.0, basis.forward.z), + hlsl::float64_t3(0.0, 0.0, 1.0)); + const auto planarRight = hlsl::CCameraMathUtilities::safeNormalizeVec3( + hlsl::float64_t3(basis.right.x, 0.0, basis.right.z), + hlsl::float64_t3(1.0, 0.0, 0.0)); + + m_targetPosition += planarRight * deltaTranslation.x + planarForward * deltaTranslation.z; + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Chase; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. + virtual std::string_view getIdentifier() const override { return "Chase Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::ChaseMaxPitchRad; + static inline constexpr double MinPitch = SCameraTargetRelativeRigDefaults::ChaseMinPitchRad; +}; + +} + +#endif diff --git a/include/nbl/ext/Cameras/CCubeProjection.hpp b/include/nbl/ext/Cameras/CCubeProjection.hpp new file mode 100644 index 0000000000..7017e49e00 --- /dev/null +++ b/include/nbl/ext/Cameras/CCubeProjection.hpp @@ -0,0 +1,97 @@ +#ifndef _NBL_CCUBE_PROJECTION_HPP_ +#define _NBL_CCUBE_PROJECTION_HPP_ + +#include "IRange.hpp" +#include "IPerspectiveProjection.hpp" + +namespace nbl::core +{ + +/// @brief Projection where each cube face is a perspective quad. +/// +/// This represents a cube projection for a direction vector where each face of +/// the cube is treated as a quad. Projection onto the cube is done through +/// those quads, each with its own pre-transform and viewport linear matrix. +class CCubeProjection final : public IPerspectiveProjection, public IProjection +{ +public: + /// @brief Represents six face identifiers of a cube. + enum CubeFaces : uint8_t + { + /// @brief Cube face in the +X base direction + PositiveX = 0, + + /// @brief Cube face in the -X base direction + NegativeX, + + /// @brief Cube face in the +Y base direction + PositiveY, + + /// @brief Cube face in the -Y base direction + NegativeY, + + /// @brief Cube face in the +Z base direction + PositiveZ, + + /// @brief Cube face in the -Z base direction + NegativeZ, + + CubeFacesCount + }; + + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) + return nullptr; + + return core::smart_refctd_ptr(new CCubeProjection(core::smart_refctd_ptr(camera)), core::dont_grab); + } + + virtual uint32_t getLinearProjectionCount() const override + { + return static_cast(m_quads.size()); + } + + virtual const ILinearProjection::CProjection& getLinearProjection(uint32_t index) const override + { + assert(index < m_quads.size()); + return m_quads[index]; + } + + void transformCube() + { + // Cube-face quad generation is not implemented yet. + } + + virtual ProjectionType getProjectionType() const override { return ProjectionType::Cube; } + + virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override + { + auto direction = hlsl::normalize(vecToProjectionSpace); + + // Cube-face projection is not implemented yet. + } + + virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override + { + // Reverse projection is not implemented yet. + } + + template + requires (FaceIx != CubeFacesCount) + inline const CProjection& getProjectionQuad() + { + return m_quads[FaceIx]; + } + +private: + CCubeProjection(core::smart_refctd_ptr&& camera) + : IPerspectiveProjection(core::smart_refctd_ptr(camera)) {} + virtual ~CCubeProjection() = default; + + std::array m_quads; +}; + +} // namespace nbl::core + +#endif // _NBL_CCUBE_PROJECTION_HPP_ diff --git a/include/nbl/ext/Cameras/CDollyCamera.hpp b/include/nbl/ext/Cameras/CDollyCamera.hpp new file mode 100644 index 0000000000..043c656103 --- /dev/null +++ b/include/nbl/ext/Cameras/CDollyCamera.hpp @@ -0,0 +1,80 @@ +#ifndef _C_DOLLY_CAMERA_HPP_ +#define _C_DOLLY_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera that translates the target in the full local camera basis. +/// +/// Translation uses the current right/up/forward basis. Rotation updates orbit +/// yaw and pitch while the camera pose is rebuilt from the maintained +/// target-relative offset. +class CDollyCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CDollyCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(position, target) + { + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); + applyPose(); + } + ~CDollyCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Apply one frame of local-frame dolly translation plus orbit rotation. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const auto basis = computeBasis(m_orbitUv, m_distance); + const auto delta = hlsl::CCameraMathUtilities::transformLocalVectorToWorldBasis(deltaTranslation, basis.right, basis.up, basis.forward); + + m_targetPosition += delta; + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaRotation.x, MinPitch, MaxPitch); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Dolly; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. + virtual std::string_view getIdentifier() const override { return "Dolly Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::DollyPitchLimitRad; + static inline constexpr double MinPitch = -MaxPitch; +}; + +} + +#endif diff --git a/include/nbl/ext/Cameras/CDollyZoomCamera.hpp b/include/nbl/ext/Cameras/CDollyZoomCamera.hpp new file mode 100644 index 0000000000..86e16a3ded --- /dev/null +++ b/include/nbl/ext/Cameras/CDollyZoomCamera.hpp @@ -0,0 +1,122 @@ +#ifndef _C_DOLLY_ZOOM_CAMERA_HPP_ +#define _C_DOLLY_ZOOM_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera that preserves subject framing by coupling distance with a derived perspective FOV. +/// +/// The rig reuses spherical target-relative manipulation but exposes an additional +/// dynamic-perspective state describing the authored base FOV and the reference +/// distance used to compute the current dolly-zoom FOV. +class CDollyZoomCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CDollyZoomCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, float baseFov = DefaultBaseFovDeg) + : base_t(position, target), m_baseFov(baseFov), m_referenceDistance(m_distance) + { + applyPose(); + } + ~CDollyZoomCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Return the authored FOV used as the reference value for dolly-zoom evaluation. + float getBaseFov() const { return m_baseFov; } + /// @brief Update the authored reference FOV used for dolly-zoom evaluation. + void setBaseFov(float fov) { m_baseFov = fov; } + + /// @brief Return the reference distance that preserves the authored framing. + float getReferenceDistance() const { return m_referenceDistance; } + /// @brief Update the reference distance used by dolly-zoom FOV evaluation. + void setReferenceDistance(float distance) { m_referenceDistance = distance; } + + /// @brief Evaluate the effective perspective FOV required to preserve subject framing at the current distance. + float computeDollyFov() const + { + const double base = std::tan(hlsl::radians(static_cast(m_baseFov)) * 0.5); + const double ratio = static_cast(m_referenceDistance) / std::max(static_cast(m_distance), static_cast(MinDistance)); + const double fov = 2.0 * std::atan(base * ratio); + const double fovDeg = hlsl::degrees(fov); + return static_cast(std::clamp(fovDeg, static_cast(MinDynamicFovDeg), static_cast(MaxDynamicFovDeg))); + } + + /// @brief Apply one frame of orbit translation and distance input for the dolly-zoom rig. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); + + m_orbitUv += hlsl::float64_t2(deltaTranslation.y, deltaTranslation.x); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::DollyZoom; } + virtual uint32_t getCapabilities() const override { return base_t::getCapabilities() | base_t::DynamicPerspectiveFov; } + /// @brief Query the current derived FOV produced by the dolly-zoom state. + virtual bool tryGetDynamicPerspectiveFov(float& outFov) const override + { + outFov = computeDollyFov(); + return true; + } + /// @brief Query the authored dolly-zoom state used to derive the current dynamic FOV. + virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const override + { + out.baseFov = m_baseFov; + out.referenceDistance = m_referenceDistance; + return true; + } + /// @brief Replace the authored dolly-zoom state after validating both scalars. + virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) override + { + if (!hlsl::CCameraMathUtilities::isFiniteScalar(state.baseFov) || !hlsl::CCameraMathUtilities::isFiniteScalar(state.referenceDistance) || state.referenceDistance <= 0.f) + return false; + + m_baseFov = state.baseFov; + m_referenceDistance = state.referenceDistance; + return true; + } + /// @brief Return the stable user-facing identifier for this concrete camera kind. + virtual std::string_view getIdentifier() const override { return "Dolly Zoom Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; + static inline constexpr float DefaultBaseFovDeg = 40.0f; + static inline constexpr float MinDynamicFovDeg = 10.0f; + static inline constexpr float MaxDynamicFovDeg = 150.0f; + + float m_baseFov = DefaultBaseFovDeg; + float m_referenceDistance = 1.0f; +}; + +} + +#endif + diff --git a/include/nbl/ext/Cameras/CFPSCamera.hpp b/include/nbl/ext/Cameras/CFPSCamera.hpp new file mode 100644 index 0000000000..6e0b34d8d4 --- /dev/null +++ b/include/nbl/ext/Cameras/CFPSCamera.hpp @@ -0,0 +1,125 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_FPS_CAMERA_HPP_ +#define _C_FPS_CAMERA_HPP_ + +#include + +#include "ICamera.hpp" + +namespace nbl::core +{ + +/// @brief Free-position camera with world-space translation and yaw/pitch rotation. +/// +/// The runtime state consists of position plus an upright orientation derived +/// from yaw and pitch. Reference-frame application rejects arbitrary roll and +/// rebuilds the legal FPS orientation from the extracted forward axis. +class CFPSCamera final : public ICamera +{ +public: + using base_t = ICamera; + struct SFpsCameraDefaults final + { + static inline constexpr float RollValidationEpsilonDeg = 1.e-4f; + static inline constexpr float StraightRollDeg = 0.0f; + static inline constexpr float InvertedRollDeg = 180.0f; + }; + + CFPSCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion()) + : base_t(), m_gimbal(typename base_t::CGimbal::base_t::SCreationParameters{ .position = position, .orientation = orientation }) + { + m_gimbal.begin(); + { + const auto pitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromForwardVector(m_gimbal.getZAxis()); + m_gimbal.setOrientation(hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(pitchYaw.x, pitchYaw.y, 0.0))); + } + m_gimbal.end(); + } + ~CFPSCamera() = default; + + const typename base_t::CGimbal& getGimbal() override + { + return m_gimbal; + } + + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + CReferenceTransform reference; + if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) + return false; + + auto validateReference = [&]() + { + if (referenceFrame) + { + const float roll = static_cast(hlsl::degrees(hlsl::CCameraMathUtilities::getQuaternionEulerRadiansYXZ(reference.orientation).z)); + const bool matchesStraightRoll = + hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::StraightRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; + const bool matchesInvertedRoll = + hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(roll, SFpsCameraDefaults::InvertedRollDeg) <= SFpsCameraDefaults::RollValidationEpsilonDeg; + + if (!(matchesStraightRoll || matchesInvertedRoll)) + return false; + } + + return true; + }; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + bool manipulated = true; + + m_gimbal.begin(); + { + const auto pitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromForwardVector(hlsl::float64_t3(reference.frame[2])); + const float newPitch = std::clamp(static_cast(pitchYaw.x + scaleVirtualRotation(impulse.dVirtualRotation.x)), MinVerticalAngle, MaxVerticalAngle); + const float newYaw = static_cast(pitchYaw.y + scaleVirtualRotation(impulse.dVirtualRotation.y)); + + if (validateReference()) + m_gimbal.setOrientation(hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(newPitch, newYaw, 0.0f))); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); + } + m_gimbal.end(); + + manipulated &= bool(m_gimbal.getManipulationCounter()); + + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + virtual uint32_t getAllowedVirtualEvents() const override + { + return AllowedVirtualEvents; + } + + virtual CameraKind getKind() const override + { + return CameraKind::FPS; + } + + virtual std::string_view getIdentifier() const override + { + return "FPS Camera"; + } + +private: + + typename base_t::CGimbal m_gimbal; + + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr float MaxVerticalAngle = static_cast(hlsl::SCameraViewRigDefaults::FpsVerticalPitchLimitRad); + static inline constexpr float MinVerticalAngle = -MaxVerticalAngle; +}; + +} + +#endif // _C_FPS_CAMERA_HPP_ + diff --git a/include/nbl/ext/Cameras/CFreeLockCamera.hpp b/include/nbl/ext/Cameras/CFreeLockCamera.hpp new file mode 100644 index 0000000000..b932290430 --- /dev/null +++ b/include/nbl/ext/Cameras/CFreeLockCamera.hpp @@ -0,0 +1,83 @@ +// Copyright (C) 2018-2024 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_FREE_CAMERA_HPP_ +#define _C_FREE_CAMERA_HPP_ + +#include "ICamera.hpp" + +namespace nbl::core +{ + +/// @brief Free-position camera that allows full yaw/pitch/roll rotation. +class CFreeCamera final : public ICamera +{ +public: + using base_t = ICamera; + + CFreeCamera(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion()) + : base_t(), m_gimbal(typename base_t::CGimbal::base_t::SCreationParameters{ .position = position, .orientation = orientation }) {} + ~CFreeCamera() = default; + + const typename base_t::CGimbal& getGimbal() override + { + return m_gimbal; + } + + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + CReferenceTransform reference; + if (not m_gimbal.extractReferenceTransform(&reference, referenceFrame)) + return false; + + auto impulse = m_gimbal.accumulate(virtualEvents); + + bool manipulated = true; + + m_gimbal.begin(); + { + const auto pitch = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); + const auto yaw = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); + const auto roll = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); + + m_gimbal.setOrientation(hlsl::CCameraMathUtilities::normalizeQuaternion(yaw * pitch * roll * reference.orientation)); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); + } + m_gimbal.end(); + + manipulated &= bool(m_gimbal.getManipulationCounter()); + + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + virtual uint32_t getAllowedVirtualEvents() const override + { + return AllowedVirtualEvents; + } + + virtual CameraKind getKind() const override + { + return CameraKind::Free; + } + + virtual std::string_view getIdentifier() const override + { + return "Free-Look Camera"; + } + +private: + typename base_t::CGimbal m_gimbal; + + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; +}; + +} + +#endif // _C_FREE_CAMERA_HPP_ diff --git a/include/nbl/ext/Cameras/CGeneralPurposeGimbal.hpp b/include/nbl/ext/Cameras/CGeneralPurposeGimbal.hpp new file mode 100644 index 0000000000..d0f7c72b92 --- /dev/null +++ b/include/nbl/ext/Cameras/CGeneralPurposeGimbal.hpp @@ -0,0 +1,24 @@ +#ifndef _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ +#define _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ + +#include "IGimbal.hpp" + +namespace nbl::core +{ + /// @brief Minimal concrete gimbal wrapper for code that only needs the generic `IGimbal` behavior. + /// + /// The class exists mainly as a convenient instantiable type when no additional + /// camera-specific state or manipulation policy is required on top of `IGimbal`. + template + class CGeneralPurposeGimbal : public IGimbal + { + public: + using base_t = IGimbal; + + /// @brief Construct the gimbal from an initial world-space pose. + CGeneralPurposeGimbal(typename base_t::SCreationParameters&& parameters) : base_t(std::move(parameters)) {} + ~CGeneralPurposeGimbal() = default; + }; +} + +#endif // _NBL_CGENERAL_PURPOSE_GIMBAL_HPP_ diff --git a/include/nbl/ext/Cameras/CGimbalInputBinder.hpp b/include/nbl/ext/Cameras/CGimbalInputBinder.hpp new file mode 100644 index 0000000000..785bb02697 --- /dev/null +++ b/include/nbl/ext/Cameras/CGimbalInputBinder.hpp @@ -0,0 +1,131 @@ +#ifndef _NBL_C_GIMBAL_INPUT_BINDER_HPP_ +#define _NBL_C_GIMBAL_INPUT_BINDER_HPP_ + +#include + +#include "IGimbalInputProcessor.hpp" + +namespace nbl::ui +{ + +/// @brief High-level runtime binder for consumers and viewport glue. +/// +/// It owns active runtime mappings and collects one frame of virtual events +/// from raw keyboard, mouse, and ImGuizmo input. +class CGimbalInputBinder final : public IGimbalInputProcessor +{ +public: + using base_t = IGimbalInputProcessor; + using base_t::base_t; + using input_keyboard_event_t = base_t::input_keyboard_event_t; + using input_mouse_event_t = base_t::input_mouse_event_t; + using input_imguizmo_event_t = base_t::input_imguizmo_event_t; + + struct SCollectedVirtualEvents + { + /// @brief Concatenated output buffer plus per-domain counts for diagnostics. + std::vector events; + uint32_t keyboardCount = 0u; + uint32_t mouseCount = 0u; + uint32_t imguizmoCount = 0u; + + inline uint32_t totalCount() const + { + return keyboardCount + mouseCount + imguizmoCount; + } + }; + + /// @brief Translate one frame of external keyboard, mouse, and ImGuizmo input into virtual events. + inline void clearActiveBindings() + { + updateKeyboardMapping([](auto& map) { map.clear(); }); + updateMouseMapping([](auto& map) { map.clear(); }); + updateImguizmoMapping([](auto& map) { map.clear(); }); + } + + inline void clearBindingLayout() + { + clearActiveBindings(); + } + + inline void copyActiveBindingsFromLayout(const IGimbalBindingLayout& layout) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); + } + + inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) + { + copyActiveBindingsFromLayout(layout); + } + + inline void copyActiveBindingsToLayout(IGimbalBindingLayout& layout) const + { + layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); + layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); + layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); + } + + inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const + { + copyActiveBindingsToLayout(layout); + } + + inline SCollectedVirtualEvents collectVirtualEvents( + const std::chrono::microseconds nextPresentationTimeStamp, + const SUpdateParameters parameters = {}) + { + beginInputProcessing(nextPresentationTimeStamp); + + SCollectedVirtualEvents output; + uint32_t keyboardPotentialCount = 0u; + uint32_t mousePotentialCount = 0u; + uint32_t imguizmoPotentialCount = 0u; + + processKeyboard(nullptr, keyboardPotentialCount, {}); + processMouse(nullptr, mousePotentialCount, {}); + processImguizmo(nullptr, imguizmoPotentialCount, {}); + + output.events.resize(keyboardPotentialCount + mousePotentialCount + imguizmoPotentialCount); + auto* dst = output.events.data(); + + if (keyboardPotentialCount) + { + output.keyboardCount = keyboardPotentialCount; + processKeyboard(dst, output.keyboardCount, parameters.keyboardEvents); + dst += output.keyboardCount; + } + + if (mousePotentialCount) + { + output.mouseCount = mousePotentialCount; + processMouse(dst, output.mouseCount, parameters.mouseEvents); + dst += output.mouseCount; + } + + if (imguizmoPotentialCount) + { + output.imguizmoCount = imguizmoPotentialCount; + processImguizmo(dst, output.imguizmoCount, parameters.imguizmoEvents); + } + + endInputProcessing(); + output.events.resize(output.totalCount()); + return output; + } + +private: + template + inline static Map sanitizeMapping(const Map& source) + { + Map result; + for (const auto& [code, hash] : source) + result.emplace(code, typename Map::mapped_type(hash.event.type, hash.magnitudeScale)); + return result; + } +}; + +} // namespace nbl::ui + +#endif // _NBL_C_GIMBAL_INPUT_BINDER_HPP_ diff --git a/include/nbl/ext/Cameras/CIsometricCamera.hpp b/include/nbl/ext/Cameras/CIsometricCamera.hpp new file mode 100644 index 0000000000..3ac1d41e85 --- /dev/null +++ b/include/nbl/ext/Cameras/CIsometricCamera.hpp @@ -0,0 +1,77 @@ +#ifndef _C_ISOMETRIC_CAMERA_HPP_ +#define _C_ISOMETRIC_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera locked to the shared isometric yaw and pitch. +/// +/// Translation moves the tracked target in the current view plane while the +/// authored isometric orientation stays fixed. Distance changes are still allowed. +class CIsometricCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CIsometricCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(position, target) + { + m_orbitUv = hlsl::float64_t2(IsoYaw, IsoPitch); + applyPose(); + } + ~CIsometricCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Apply one frame of planar target translation and distance changes while preserving the fixed isometric angles. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceIsometricState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); + + m_orbitUv = hlsl::float64_t2(IsoYaw, IsoPitch); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + const auto basis = computeBasis(m_orbitUv, m_distance); + applyPlanarTargetTranslation(deltaTranslation, basis); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Isometric; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. + virtual std::string_view getIdentifier() const override { return "Isometric Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; + static inline constexpr double IsoYaw = SCameraTargetRelativeRigDefaults::IsometricYawRad; + static inline constexpr double IsoPitch = SCameraTargetRelativeRigDefaults::IsometricPitchRad; +}; + +} + +#endif diff --git a/include/nbl/ext/Cameras/CLinearProjection.hpp b/include/nbl/ext/Cameras/CLinearProjection.hpp new file mode 100644 index 0000000000..d6e0dd4e05 --- /dev/null +++ b/include/nbl/ext/Cameras/CLinearProjection.hpp @@ -0,0 +1,59 @@ +#ifndef _NBL_C_LINEAR_PROJECTION_HPP_ +#define _NBL_C_LINEAR_PROJECTION_HPP_ + +#include "ILinearProjection.hpp" +#include "IRange.hpp" + +namespace nbl::core +{ + /// @brief Range-backed concrete implementation of `ILinearProjection`. + /// + /// The template owns a caller-selected contiguous container of linear projection + /// entries and exposes it through the generic `ILinearProjection` interface. + template ProjectionsRange> + class CLinearProjection : public ILinearProjection + { + public: + using ILinearProjection::ILinearProjection; + + CLinearProjection() = default; + + /// @brief Create a projection wrapper only when a valid camera instance is available. + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) + return nullptr; + + return core::smart_refctd_ptr(new CLinearProjection(core::smart_refctd_ptr(camera)), core::dont_grab); + } + + /// @brief Return the number of stored linear projection entries. + virtual uint32_t getLinearProjectionCount() const override + { + return static_cast(m_projections.size()); + } + + /// @brief Return one stored projection entry by index. + virtual const CProjection& getLinearProjection(uint32_t index) const override + { + assert(index < m_projections.size()); + return m_projections[index]; + } + + /// @brief Expose mutable access to the owned projection range. + inline std::span getLinearProjections() + { + return std::span(m_projections.data(), m_projections.size()); + } + + private: + CLinearProjection(core::smart_refctd_ptr&& camera) + : ILinearProjection(core::smart_refctd_ptr(camera)) {} + virtual ~CLinearProjection() = default; + + ProjectionsRange m_projections; + }; + +} // nbl::hlsl namespace + +#endif // _NBL_C_LINEAR_PROJECTION_HPP_ diff --git a/include/nbl/ext/Cameras/COrbitCamera.hpp b/include/nbl/ext/Cameras/COrbitCamera.hpp new file mode 100644 index 0000000000..df411c4b81 --- /dev/null +++ b/include/nbl/ext/Cameras/COrbitCamera.hpp @@ -0,0 +1,83 @@ +#ifndef _C_ORBIT_CAMERA_HPP_ +#define _C_ORBIT_CAMERA_HPP_ + +#include +#include +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera with state `(target, orbitUv, distance)`. +/// +/// Runtime input updates only orbit yaw, orbit pitch, and camera distance. +/// The target position remains unchanged during `manipulate(...)`. +class COrbitCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + COrbitCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(position, target) + { + m_distance = std::clamp(hlsl::length(m_targetPosition - position), MinDistance, MaxDistance); + applyPose(); + } + ~COrbitCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Apply one frame of orbit-angle and distance input around the current target. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (virtualEvents.empty() && !referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); + + m_orbitUv += hlsl::float64_t2(deltaTranslation.y, deltaTranslation.x); + + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override + { + return AllowedVirtualEvents; + } + + virtual CameraKind getKind() const override + { + return CameraKind::Orbit; + } + + virtual std::string_view getIdentifier() const override + { + return "Orbit Camera"; + } + + static inline constexpr float MinDistance = base_t::MinDistance; + static inline constexpr float MaxDistance = base_t::MaxDistance; + + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; +}; + +} + +#endif // _C_ORBIT_CAMERA_HPP_ diff --git a/include/nbl/ext/Cameras/CPathCamera.hpp b/include/nbl/ext/Cameras/CPathCamera.hpp new file mode 100644 index 0000000000..aa8b335584 --- /dev/null +++ b/include/nbl/ext/Cameras/CPathCamera.hpp @@ -0,0 +1,342 @@ +#ifndef _C_PATH_CAMERA_HPP_ +#define _C_PATH_CAMERA_HPP_ + +#include +#include + +#include "CCameraPathUtilities.hpp" +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Path-rig camera driven by typed `PathState` plus an injected path model. +/// +/// The public runtime path stays event-only through `manipulate(...)`. +/// `CPathCamera` only interprets the accumulated impulse through `m_pathModel` +/// instead of hardcoding one default target-relative mapping in the method body. +class CPathCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + using path_model_t = SCameraPathModel; + using path_limits_t = PathStateLimits; + + /// @brief Construct the path rig with the shared default path model and default limits. + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), CCameraPathUtilities::makeDefaultPathLimits()) + { + } + + /// @brief Construct the path rig with a caller-provided model and default limits. + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel) + : CPathCamera(position, target, std::move(pathModel), CCameraPathUtilities::makeDefaultPathLimits()) + { + } + + /// @brief Construct the path rig with the shared default model and caller-provided limits. + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_limits_t pathLimits) + : CPathCamera(position, target, CCameraPathUtilities::makeDefaultPathModel(), pathLimits) + { + } + + /// @brief Construct the path rig with fully caller-provided model and path-state limits. + CPathCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target, path_model_t pathModel, path_limits_t pathLimits) + : base_t(position, target) + { + initializePathRig(position, std::move(pathModel), pathLimits); + } + + ~CPathCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Consume virtual events through the active path model and update the runtime pose from the resulting path state. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (virtualEvents.empty() && !referenceFrame) + return false; + + PathState nextPathState = m_pathState; + CReferenceTransform reference = {}; + const CReferenceTransform* resolvedReference = nullptr; + if (referenceFrame) + { + if (!m_gimbal.extractReferenceTransform(&reference, referenceFrame)) + return false; + resolvedReference = &reference; + if (!m_pathModel.resolveState || + !m_pathModel.resolveState( + m_targetPosition, + hlsl::float64_t3(reference.frame[3]), + m_pathLimits, + nullptr, + nextPathState)) + { + return false; + } + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + const SCameraPathControlContext context = { + .currentState = nextPathState, + .translation = scaleVirtualTranslation(impulse.dVirtualTranslate), + .rotation = scaleVirtualRotation(impulse.dVirtualRotation), + .targetPosition = m_targetPosition, + .reference = resolvedReference, + .limits = m_pathLimits + }; + + if (!m_pathModel.controlLaw || !m_pathModel.integrate) + return false; + + const auto stateDelta = m_pathModel.controlLaw(context); + if (!m_pathModel.integrate(nextPathState, stateDelta, m_pathLimits, nextPathState)) + return false; + + const auto previousPathState = m_pathState; + m_pathState = nextPathState; + bool manipulated = false; + if (refreshFromPathState(&manipulated)) + return manipulated; + + m_pathState = previousPathState; + refreshFromPathState(); + return false; + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Path; } + virtual uint32_t getGoalStateMask() const override { return base_t::getGoalStateMask() | base_t::GoalStatePath; } + + /// @brief Query the current typed path state. + virtual bool tryGetPathState(PathState& out) const override + { + out = m_pathState; + return true; + } + + /// @brief Query the active path-state limits used by this camera instance. + virtual bool tryGetPathStateLimits(PathStateLimits& out) const override + { + out = m_pathLimits; + return true; + } + + /// @brief Query the derived spherical-target state corresponding to the current path-state evaluation. + virtual bool tryGetSphericalTargetState(typename base_t::SphericalTargetState& out) const override + { + out.target = m_targetPosition; + out.distance = m_distance; + out.orbitUv = m_orbitUv; + out.minDistance = static_cast(m_pathLimits.minDistance); + out.maxDistance = static_cast(m_pathLimits.maxDistance); + return true; + } + + /// @brief Replace only the tracked target position and rebuild the current path pose against it. + virtual bool trySetSphericalTarget(const hlsl::float64_t3& targetPosition) override + { + if (m_targetPosition == targetPosition) + return true; + + const auto previousTarget = m_targetPosition; + m_targetPosition = targetPosition; + if (refreshFromPathState()) + return true; + + m_targetPosition = previousTarget; + refreshFromPathState(); + return false; + } + + /// @brief Replace the current path state after sanitizing it through the active path model. + virtual bool trySetPathState(const PathState& state) override + { + if (!m_pathModel.resolveState) + return false; + + PathState sanitized = {}; + if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), m_pathLimits, &state, sanitized)) + return false; + + const bool exact = CCameraPathUtilities::pathStatesNearlyEqual(sanitized, state, SCameraPathDefaults::ExactComparisonThresholds); + const auto previousState = m_pathState; + m_pathState = sanitized; + if (refreshFromPathState()) + return exact; + + m_pathState = previousState; + refreshFromPathState(); + return false; + } + + /// @brief Replace the derived path distance while preserving the rest of the typed path state. + virtual bool trySetSphericalDistance(float distance) override + { + SCameraPathDistanceUpdateResult distanceUpdate = {}; + if (!m_pathModel.updateDistance) + { + return false; + } + + const auto previousState = m_pathState; + if (!m_pathModel.updateDistance(distance, m_pathLimits, m_pathState, &distanceUpdate)) + return false; + if (!refreshFromPathState()) + { + m_pathState = previousState; + refreshFromPathState(); + return false; + } + + return distanceUpdate.exact; + } + + virtual std::string_view getIdentifier() const override { return SCameraPathDefaults::Identifier; } + + /// @brief Return the currently installed path model. + inline const path_model_t& getPathModel() const + { + return m_pathModel; + } + + /// @brief Return the current path-state limits enforced by this camera instance. + inline const path_limits_t& getPathStateLimits() const + { + return m_pathLimits; + } + + /// @brief Replace the active path-state limits after sanitizing the current path state against them. + inline bool setPathStateLimits(path_limits_t pathLimits) + { + if (!CCameraPathUtilities::sanitizePathLimits(pathLimits) || !m_pathModel.resolveState) + return false; + + PathState sanitizedState = {}; + if (!m_pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), pathLimits, &m_pathState, sanitizedState)) + return false; + + const auto previousLimits = m_pathLimits; + const auto previousState = m_pathState; + m_pathLimits = pathLimits; + m_pathState = sanitizedState; + if (refreshFromPathState()) + return true; + + m_pathLimits = previousLimits; + m_pathState = previousState; + refreshFromPathState(); + return false; + } + + /// @brief Replace the active path model after validating that it can resolve the current path state. + inline bool setPathModel(path_model_t pathModel) + { + if (!isPathModelComplete(pathModel)) + return false; + + PathState sanitized = {}; + if (!pathModel.resolveState(m_targetPosition, m_gimbal.getPosition(), m_pathLimits, &m_pathState, sanitized)) + return false; + + const auto previousModel = m_pathModel; + const auto previousState = m_pathState; + m_pathModel = std::move(pathModel); + m_pathState = sanitized; + if (refreshFromPathState()) + return true; + + m_pathModel = previousModel; + m_pathState = previousState; + refreshFromPathState(); + return false; + } + +private: + static inline constexpr auto AllowedVirtualEvents = + CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::RollLeft | CVirtualGimbalEvent::RollRight; + + /// @brief Check whether a path model provides all callbacks required by the runtime camera. + static inline bool isPathModelComplete(const path_model_t& pathModel) + { + return pathModel.resolveState && pathModel.controlLaw && pathModel.integrate && pathModel.evaluate && pathModel.updateDistance; + } + + /// @brief Attempt to initialize the runtime path state and pose from one model/limit pair. + inline bool tryInitializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) + { + if (!CCameraPathUtilities::sanitizePathLimits(pathLimits)) + return false; + + if (!isPathModelComplete(pathModel)) + return false; + + PathState resolvedState = {}; + if (!pathModel.resolveState(m_targetPosition, position, pathLimits, nullptr, resolvedState)) + return false; + + m_pathLimits = pathLimits; + m_pathModel = std::move(pathModel); + m_pathState = resolvedState; + return refreshFromPathState(); + } + + /// @brief Initialize the path rig with graceful fallback to the shared default model and limits. + inline void initializePathRig(const hlsl::float64_t3& position, path_model_t pathModel, path_limits_t pathLimits) + { + path_limits_t sanitizedLimits = pathLimits; + const bool hasCustomLimits = CCameraPathUtilities::sanitizePathLimits(sanitizedLimits); + if (!hasCustomLimits) + sanitizedLimits = CCameraPathUtilities::makeDefaultPathLimits(); + + if (tryInitializePathRig(position, std::move(pathModel), sanitizedLimits)) + return; + + if (tryInitializePathRig(position, CCameraPathUtilities::makeDefaultPathModel(), sanitizedLimits)) + return; + + m_pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); + m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); + m_pathState = CCameraPathUtilities::makeDefaultPathState(m_pathLimits.minU); + m_pathModel.resolveState(m_targetPosition, position, m_pathLimits, nullptr, m_pathState); + refreshFromPathState(); + } + + path_model_t m_pathModel = CCameraPathUtilities::makeDefaultPathModel(); + path_limits_t m_pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); + PathState m_pathState = CCameraPathUtilities::makeDefaultPathState(CCameraPathUtilities::makeDefaultPathLimits().minU); + + /// @brief Evaluate the current path state into a canonical pose and write it back to the runtime gimbal. + bool refreshFromPathState(bool* outManipulated = nullptr) + { + if (!m_pathModel.evaluate) + return false; + + SCameraCanonicalPathState canonicalPathState = {}; + if (!m_pathModel.evaluate(m_targetPosition, m_pathState, m_pathLimits, canonicalPathState)) + return false; + + m_distance = canonicalPathState.targetRelative.distance; + m_orbitUv = canonicalPathState.targetRelative.orbitUv; + + m_gimbal.begin(); + { + m_gimbal.setPosition(canonicalPathState.pose.position); + m_gimbal.setOrientation(canonicalPathState.pose.orientation); + } + m_gimbal.end(); + + const bool manipulated = bool(m_gimbal.getManipulationCounter()); + if (manipulated) + m_gimbal.updateView(); + + if (outManipulated) + *outManipulated = manipulated; + return true; + } +}; + +} + +#endif diff --git a/include/nbl/ext/Cameras/CPlanarProjection.hpp b/include/nbl/ext/Cameras/CPlanarProjection.hpp new file mode 100644 index 0000000000..5407b9dd4e --- /dev/null +++ b/include/nbl/ext/Cameras/CPlanarProjection.hpp @@ -0,0 +1,56 @@ +#ifndef _NBL_C_PLANAR_PROJECTION_HPP_ +#define _NBL_C_PLANAR_PROJECTION_HPP_ + +#include "IPlanarProjection.hpp" +#include "IRange.hpp" + +namespace nbl::core +{ + /// @brief Range-backed concrete implementation of `IPlanarProjection`. + /// + /// The template owns a caller-selected contiguous container of planar + /// projection entries together with their viewport-local binding layouts. + template ProjectionsRange> + class CPlanarProjection : public IPlanarProjection + { + public: + virtual ~CPlanarProjection() = default; + + /// @brief Create a planar projection wrapper only when a valid camera instance is available. + inline static core::smart_refctd_ptr create(core::smart_refctd_ptr&& camera) + { + if (!camera) + return nullptr; + + return core::smart_refctd_ptr(new CPlanarProjection(core::smart_refctd_ptr(camera)), core::dont_grab); + } + + /// @brief Return the number of stored planar projection entries. + virtual uint32_t getLinearProjectionCount() const override + { + return static_cast(m_projections.size()); + } + + /// @brief Return one stored planar projection entry through the linear base interface. + virtual const ILinearProjection::CProjection& getLinearProjection(uint32_t index) const override + { + assert(index < m_projections.size()); + return m_projections[index]; + } + + /// @brief Expose mutable access to the owned planar projection range. + inline ProjectionsRange& getPlanarProjections() + { + return m_projections; + } + + protected: + CPlanarProjection(core::smart_refctd_ptr&& camera) + : IPlanarProjection(core::smart_refctd_ptr(camera)) {} + + ProjectionsRange m_projections; + }; + +} // nbl::hlsl namespace + +#endif // _NBL_C_PLANAR_PROJECTION_HPP_ diff --git a/include/nbl/ext/Cameras/CSphericalTargetCamera.hpp b/include/nbl/ext/Cameras/CSphericalTargetCamera.hpp new file mode 100644 index 0000000000..54fd43a2c4 --- /dev/null +++ b/include/nbl/ext/Cameras/CSphericalTargetCamera.hpp @@ -0,0 +1,243 @@ +#ifndef _C_SPHERICAL_TARGET_CAMERA_HPP_ +#define _C_SPHERICAL_TARGET_CAMERA_HPP_ + +#include +#include "CCameraTargetRelativeUtilities.hpp" + +namespace nbl::core +{ + +/// @brief Common base for target-relative cameras represented by target position, distance, and `orbitUv`. +/// +/// Derived cameras keep the same target-relative storage but apply different +/// constraints and event policies in `manipulate(...)`. +class CSphericalTargetCamera : public ICamera +{ +public: + using base_t = ICamera; + + CSphericalTargetCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(), m_targetPosition(target), m_distance(SCameraTargetRelativeRigDefaults::InitialDistance), + m_gimbal(typename base_t::CGimbal::base_t::SCreationParameters{ + .position = position, + .orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion() + }) + { + initFromPosition(position); + } + ~CSphericalTargetCamera() = default; + + inline bool setDistance(float d) + { + const auto clamped = std::clamp(d, MinDistance, MaxDistance); + const bool ok = clamped == d; + if (m_distance == clamped) + return ok; + m_distance = clamped; + applyPose(); + return ok; + } + + inline void target(const hlsl::float64_t3& p) + { + if (m_targetPosition == p) + return; + m_targetPosition = p; + applyPose(); + } + inline hlsl::float64_t3 getTarget() const { return m_targetPosition; } + + inline float getDistance() const { return m_distance; } + inline const hlsl::float64_t2& getOrbitUv() const { return m_orbitUv; } + + static inline constexpr float MinDistance = SCameraTargetRelativeTraits::MinDistance; + static inline constexpr float MaxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; + + virtual uint32_t getCapabilities() const override + { + return base_t::SphericalTarget; + } + + virtual bool tryGetSphericalTargetState(typename base_t::SphericalTargetState& out) const override + { + out.target = m_targetPosition; + out.distance = m_distance; + out.orbitUv = m_orbitUv; + out.minDistance = MinDistance; + out.maxDistance = MaxDistance; + return true; + } + + virtual bool trySetSphericalTarget(const hlsl::float64_t3& targetPosition) override + { + target(targetPosition); + return true; + } + + virtual bool trySetSphericalDistance(float distance) override + { + return setDistance(distance); + } + +protected: + using SphericalBasis = SCameraTargetRelativeBasis; + + /// @brief Return the current canonical target-relative state stored by the spherical rig. + inline SCameraTargetRelativeState currentTargetRelativeState() const + { + return { + .target = m_targetPosition, + .orbitUv = m_orbitUv, + .distance = m_distance + }; + } + + /// @brief Replace the stored target-relative state without touching the gimbal pose yet. + inline void adoptTargetRelativeState(const SCameraTargetRelativeState& state) + { + m_targetPosition = state.target; + m_orbitUv = state.orbitUv; + m_distance = state.distance; + } + + /// @brief Extract one rigid reference transform from the optional external override or the current gimbal pose. + inline bool tryExtractReferenceTransform(CReferenceTransform& outReference, const hlsl::float64_t4x4* referenceFrame) + { + return m_gimbal.extractReferenceTransform(&outReference, referenceFrame); + } + + /// @brief Resolve the current target-relative state from one rigid reference position around the current target. + inline bool tryResolveReferenceTargetRelativeState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const + { + return CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition( + m_targetPosition, + hlsl::float64_t3(reference.frame[3]), + MinDistance, + MaxDistance, + outState); + } + + /// @brief Resolve the top-down yaw encoded by a rigid reference orientation. + static inline double resolveTopDownYawFromReference(const CReferenceTransform& reference, const double fallbackYaw) + { + const auto basis = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(reference.orientation); + const auto planarUp = hlsl::float64_t2(basis[1].x, basis[1].y); + constexpr auto Epsilon = static_cast(SCameraToolingThresholds::TinyScalarEpsilon); + if (!hlsl::CCameraMathUtilities::isNearlyZeroVector(planarUp, Epsilon)) + return hlsl::atan2(planarUp.y, planarUp.x); + + const auto planarRight = hlsl::float64_t2(basis[0].x, basis[0].y); + if (!hlsl::CCameraMathUtilities::isNearlyZeroVector(planarRight, Epsilon)) + return hlsl::atan2(planarRight.x, -planarRight.y); + + return fallbackYaw; + } + + /// @brief Project one rigid reference pose onto the legal top-down state manifold around the current target. + inline bool tryResolveReferenceTopDownState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const + { + const auto offset = hlsl::float64_t3(reference.frame[3]) - m_targetPosition; + const auto distance = hlsl::length(offset); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(distance) || + distance <= static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) + { + return false; + } + + outState = currentTargetRelativeState(); + outState.distance = static_cast(std::clamp( + distance, + static_cast(MinDistance), + static_cast(MaxDistance))); + outState.orbitUv.x = resolveTopDownYawFromReference(reference, m_orbitUv.x); + outState.orbitUv.y = SCameraTargetRelativeRigDefaults::TopDownPitchRad; + return true; + } + + /// @brief Project one rigid reference pose onto the legal fixed-angle isometric manifold around the current target. + inline bool tryResolveReferenceIsometricState(const CReferenceTransform& reference, SCameraTargetRelativeState& outState) const + { + if (!tryResolveReferenceTargetRelativeState(reference, outState)) + return false; + + outState.orbitUv = hlsl::float64_t2( + SCameraTargetRelativeRigDefaults::IsometricYawRad, + SCameraTargetRelativeRigDefaults::IsometricPitchRad); + return true; + } + + inline SphericalBasis computeBasis(const hlsl::float64_t2& orbitUv, float distance) const + { + SphericalBasis basis; + const SCameraTargetRelativeState state = { + .target = m_targetPosition, + .orbitUv = orbitUv, + .distance = distance + }; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeBasis(state, MinDistance, MaxDistance, basis)) + return basis; + return basis; + } + + inline void initFromPosition(const hlsl::float64_t3& position) + { + SCameraTargetRelativeState state = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativeStateFromPosition(m_targetPosition, position, MinDistance, MaxDistance, state)) + { + m_distance = MinDistance; + m_orbitUv = hlsl::float64_t2(0.0); + return; + } + + m_distance = state.distance; + m_orbitUv = state.orbitUv; + } + + inline void applyPlanarTargetTranslation(const hlsl::float64_t3& deltaTranslation, const SphericalBasis& basis) + { + if (!hlsl::CCameraMathUtilities::hasPlanarDeltaXY(deltaTranslation, static_cast(SCameraToolingThresholds::TinyScalarEpsilon))) + return; + + m_targetPosition += hlsl::CCameraMathUtilities::transformLocalVectorToWorldBasis( + hlsl::float64_t3(deltaTranslation.x, deltaTranslation.y, 0.0), + basis.right, + basis.up, + basis.forward); + } + + inline bool applyPose() + { + const SCameraTargetRelativeState state = { + .target = m_targetPosition, + .orbitUv = m_orbitUv, + .distance = m_distance + }; + SCameraTargetRelativePose pose = {}; + if (!CCameraTargetRelativeUtilities::tryBuildTargetRelativePoseFromState(state, MinDistance, MaxDistance, pose)) + return false; + m_distance = static_cast(pose.appliedDistance); + + m_gimbal.begin(); + { + m_gimbal.setPosition(pose.position); + m_gimbal.setOrientation(pose.orientation); + } + m_gimbal.end(); + + const bool manipulated = bool(m_gimbal.getManipulationCounter()); + if (manipulated) + m_gimbal.updateView(); + + return manipulated; + } + + hlsl::float64_t3 m_targetPosition; + float m_distance; + typename base_t::CGimbal m_gimbal; + hlsl::float64_t2 m_orbitUv = hlsl::float64_t2(0.0); +}; + +} + +#endif + diff --git a/include/nbl/ext/Cameras/CTopDownCamera.hpp b/include/nbl/ext/Cameras/CTopDownCamera.hpp new file mode 100644 index 0000000000..63d16872c3 --- /dev/null +++ b/include/nbl/ext/Cameras/CTopDownCamera.hpp @@ -0,0 +1,78 @@ +#ifndef _C_TOPDOWN_CAMERA_HPP_ +#define _C_TOPDOWN_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera constrained to look straight down at the tracked target. +/// +/// Yaw may still rotate the view around the vertical axis, while pitch is fixed to +/// the top-down angle and translation moves the tracked target in the view plane. +class CTopDownCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CTopDownCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(position, target) + { + m_orbitUv.y = TopDownPitch; + applyPose(); + } + ~CTopDownCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Apply one frame of top-down yaw rotation, planar translation, and distance changes. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTopDownState(reference, resolvedState)) + { + return false; + } + + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); + + m_orbitUv.x += deltaRotation.y; + m_orbitUv.y = TopDownPitch; + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + const auto basis = computeBasis(m_orbitUv, m_distance); + applyPlanarTargetTranslation(deltaTranslation, basis); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::TopDown; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. + virtual std::string_view getIdentifier() const override { return "Top-Down Camera"; } + +private: + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double TopDownPitch = SCameraTargetRelativeRigDefaults::TopDownPitchRad; +}; + +} + +#endif diff --git a/include/nbl/ext/Cameras/CTurntableCamera.hpp b/include/nbl/ext/Cameras/CTurntableCamera.hpp new file mode 100644 index 0000000000..dc44d23a3c --- /dev/null +++ b/include/nbl/ext/Cameras/CTurntableCamera.hpp @@ -0,0 +1,85 @@ +// Copyright (C) 2018-2024 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _C_TURNTABLE_CAMERA_HPP_ +#define _C_TURNTABLE_CAMERA_HPP_ + +#include +#include + +#include "CSphericalTargetCamera.hpp" + +namespace nbl::core +{ + +/// @brief Target-relative camera that behaves like a classic turntable around a fixed target. +/// +/// The camera exposes yaw, bounded pitch, and distance changes while keeping the +/// target fixed in space and avoiding arbitrary planar target translation. +class CTurntableCamera final : public CSphericalTargetCamera +{ +public: + using base_t = CSphericalTargetCamera; + + CTurntableCamera(const hlsl::float64_t3& position, const hlsl::float64_t3& target) + : base_t(position, target) + { + m_orbitUv.y = std::clamp(m_orbitUv.y, MinPitch, MaxPitch); + applyPose(); + } + ~CTurntableCamera() = default; + + const typename base_t::CGimbal& getGimbal() override { return m_gimbal; } + + /// @brief Apply one frame of yaw, bounded pitch, and distance input around the tracked target. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) override + { + if (not virtualEvents.size() and not referenceFrame) + return false; + + if (referenceFrame) + { + CReferenceTransform reference = {}; + SCameraTargetRelativeState resolvedState = {}; + if (!tryExtractReferenceTransform(reference, referenceFrame) || + !tryResolveReferenceTargetRelativeState(reference, resolvedState)) + { + return false; + } + + resolvedState.orbitUv.y = std::clamp(resolvedState.orbitUv.y, MinPitch, MaxPitch); + adoptTargetRelativeState(resolvedState); + } + + const auto impulse = m_gimbal.accumulate(virtualEvents); + + const double deltaYaw = scaleVirtualRotation(impulse.dVirtualRotation.y); + const double deltaPitch = scaleVirtualRotation(impulse.dVirtualRotation.x); + const double deltaDistance = scaleUnscaledVirtualTranslation(impulse.dVirtualTranslate.z); + + m_orbitUv.x += deltaYaw; + m_orbitUv.y = std::clamp(m_orbitUv.y + deltaPitch, MinPitch, MaxPitch); + m_distance = std::clamp(m_distance + static_cast(deltaDistance), MinDistance, MaxDistance); + + return applyPose(); + } + + virtual uint32_t getAllowedVirtualEvents() const override { return AllowedVirtualEvents; } + virtual CameraKind getKind() const override { return CameraKind::Turntable; } + /// @brief Return the stable user-facing identifier for this concrete camera kind. + virtual std::string_view getIdentifier() const override { return "Turntable Camera"; } + + static inline constexpr float MinDistance = base_t::MinDistance; + static inline constexpr float MaxDistance = base_t::MaxDistance; + +private: + + static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate | CVirtualGimbalEvent::Rotate; + static inline constexpr double MaxPitch = SCameraTargetRelativeRigDefaults::TurntablePitchLimitRad; + static inline constexpr double MinPitch = -MaxPitch; +}; + +} + +#endif diff --git a/include/nbl/ext/Cameras/CVirtualGimbalEvent.hpp b/include/nbl/ext/Cameras/CVirtualGimbalEvent.hpp new file mode 100644 index 0000000000..e1384ed426 --- /dev/null +++ b/include/nbl/ext/Cameras/CVirtualGimbalEvent.hpp @@ -0,0 +1,159 @@ +#ifndef _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ +#define _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ + +#include +#include +#include + +#include "nbl/builtin/hlsl/cpp_compat/vector.hlsl" +#include "nbl/core/math/intutil.h" + +namespace nbl::core +{ + +/// @brief One semantic camera command passed to `ICamera::manipulate(...)`. +/// +/// `type` selects the command family. `magnitude` stores the non-negative +/// source-normalized scalar amount for that command. Input binders convert +/// raw keyboard, mouse, scroll, and ImGuizmo data into this representation +/// before the camera sees it. Cameras then convert these virtual magnitudes +/// into camera-local motion through their runtime scales and family-specific +/// legalization rules. +struct CVirtualGimbalEvent +{ + /// @brief Bitmask identifiers for semantic movement, rotation, and scale commands. + enum VirtualEventType : uint32_t + { + None = 0, + + MoveForward = core::createBitmask({ 0 }), + MoveBackward = core::createBitmask({ 1 }), + MoveLeft = core::createBitmask({ 2 }), + MoveRight = core::createBitmask({ 3 }), + MoveUp = core::createBitmask({ 4 }), + MoveDown = core::createBitmask({ 5 }), + TiltUp = core::createBitmask({ 6 }), + TiltDown = core::createBitmask({ 7 }), + PanLeft = core::createBitmask({ 8 }), + PanRight = core::createBitmask({ 9 }), + RollLeft = core::createBitmask({ 10 }), + RollRight = core::createBitmask({ 11 }), + ScaleXInc = core::createBitmask({ 12 }), + ScaleXDec = core::createBitmask({ 13 }), + ScaleYInc = core::createBitmask({ 14 }), + ScaleYDec = core::createBitmask({ 15 }), + ScaleZInc = core::createBitmask({ 16 }), + ScaleZDec = core::createBitmask({ 17 }), + + EventsCount = 18, + + Translate = MoveForward | MoveBackward | MoveLeft | MoveRight | MoveUp | MoveDown, + Rotate = TiltUp | TiltDown | PanLeft | PanRight | RollLeft | RollRight, + Scale = ScaleXInc | ScaleXDec | ScaleYInc | ScaleYDec | ScaleZInc | ScaleZDec, + + All = Translate | Rotate | Scale + }; + + /// @brief Scalar type used to encode one event magnitude. + using manipulation_encode_t = hlsl::float64_t; + + /// @brief Semantic event identifier. + VirtualEventType type = None; + /// @brief Non-negative scalar amount associated with `type`. + /// + /// The value is not a raw device unit. It is the virtual amount emitted by + /// the active input path after applying binding-local gains. + manipulation_encode_t magnitude = {}; + + /// @brief Convert one event identifier to its stable string form. + static constexpr std::string_view virtualEventToString(VirtualEventType event) + { + switch (event) + { + case MoveForward: return "MoveForward"; + case MoveBackward: return "MoveBackward"; + case MoveLeft: return "MoveLeft"; + case MoveRight: return "MoveRight"; + case MoveUp: return "MoveUp"; + case MoveDown: return "MoveDown"; + case TiltUp: return "TiltUp"; + case TiltDown: return "TiltDown"; + case PanLeft: return "PanLeft"; + case PanRight: return "PanRight"; + case RollLeft: return "RollLeft"; + case RollRight: return "RollRight"; + case ScaleXInc: return "ScaleXInc"; + case ScaleXDec: return "ScaleXDec"; + case ScaleYInc: return "ScaleYInc"; + case ScaleYDec: return "ScaleYDec"; + case ScaleZInc: return "ScaleZInc"; + case ScaleZDec: return "ScaleZDec"; + case Translate: return "Translate"; + case Rotate: return "Rotate"; + case Scale: return "Scale"; + case None: return "None"; + default: return "Unknown"; + } + } + + /// @brief Convert one stable string identifier back to an event identifier. + static constexpr VirtualEventType stringToVirtualEvent(std::string_view event) + { + if (event == "MoveForward") return MoveForward; + if (event == "MoveBackward") return MoveBackward; + if (event == "MoveLeft") return MoveLeft; + if (event == "MoveRight") return MoveRight; + if (event == "MoveUp") return MoveUp; + if (event == "MoveDown") return MoveDown; + if (event == "TiltUp") return TiltUp; + if (event == "TiltDown") return TiltDown; + if (event == "PanLeft") return PanLeft; + if (event == "PanRight") return PanRight; + if (event == "RollLeft") return RollLeft; + if (event == "RollRight") return RollRight; + if (event == "ScaleXInc") return ScaleXInc; + if (event == "ScaleXDec") return ScaleXDec; + if (event == "ScaleYInc") return ScaleYInc; + if (event == "ScaleYDec") return ScaleYDec; + if (event == "ScaleZInc") return ScaleZInc; + if (event == "ScaleZDec") return ScaleZDec; + if (event == "Translate") return Translate; + if (event == "Rotate") return Rotate; + if (event == "Scale") return Scale; + if (event == "None") return None; + return None; + } + + /// @brief Return whether `event` belongs to the translation subset. + static constexpr bool isTranslationEvent(const VirtualEventType event) + { + return event != None && (event & Translate) == event; + } + + /// @brief Return whether `event` belongs to the rotation subset. + static constexpr bool isRotationEvent(const VirtualEventType event) + { + return event != None && (event & Rotate) == event; + } + + /// @brief Return whether `event` belongs to the scale subset. + static constexpr bool isScaleEvent(const VirtualEventType event) + { + return event != None && (event & Scale) == event; + } + + /// @brief Table listing every individual event bit in declaration order. + static inline constexpr auto VirtualEventsTypeTable = []() + { + std::array output; + + for (uint16_t i = 0u; i < EventsCount; ++i) + output[i] = static_cast(core::createBitmask({ i })); + + return output; + }(); +}; + +} // namespace nbl::core + +#endif // _NBL_C_VIRTUAL_GIMBAL_EVENT_HPP_ diff --git a/include/nbl/ext/Cameras/ICamera.hpp b/include/nbl/ext/Cameras/ICamera.hpp new file mode 100644 index 0000000000..bf5cf3b6da --- /dev/null +++ b/include/nbl/ext/Cameras/ICamera.hpp @@ -0,0 +1,494 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _I_CAMERA_HPP_ +#define _I_CAMERA_HPP_ + +#include +#include + +#include "nbl/core/IReferenceCounted.h" +#include "CCameraTraits.hpp" +#include "IGimbal.hpp" + +namespace nbl::core +{ + +/// @brief Shared runtime camera interface. +/// +/// `ICamera` consumes batches of `CVirtualGimbalEvent` values and updates one +/// camera pose stored in `CGimbal`. A `CVirtualGimbalEvent` identifies one +/// semantic command such as `MoveForward`, `PanLeft`, or `RollRight` and carries +/// one source-normalized scalar magnitude for that command. +/// +/// Keyboard input, mouse input, ImGuizmo interaction, scripted playback, +/// preset replay, follow helpers, and goal solving all drive cameras through +/// the same `manipulate(...)` entry point. +/// +/// The optional typed hooks expose camera-family state for code that needs +/// capture, restore, compatibility analysis, persistence, or validation. +class ICamera : virtual public core::IReferenceCounted +{ +private: + static inline constexpr double DefaultMoveSpeedScaleValue = 0.01; + static inline constexpr double DefaultRotationSpeedScaleValue = 0.003; + static inline constexpr double VirtualTranslationUnit = 0.01; + +public: + /// @brief Camera-local multipliers applied when semantic virtual events are converted into motion. + /// + /// Input binders emit virtual magnitudes. Concrete cameras multiply those + /// magnitudes by this per-camera configuration before applying them to + /// their own state model. + struct SMotionConfig + { + /// @brief Camera-local scale applied to virtual translation magnitudes. + double moveSpeedScale = DefaultMoveSpeedScaleValue; + /// @brief Camera-local scale applied to virtual rotation magnitudes. + double rotationSpeedScale = DefaultRotationSpeedScaleValue; + }; + + /// @brief Stable camera-family identifier used by metadata, presets, follow, and scripted helpers. + enum class CameraKind : uint8_t + { + Unknown, + FPS, + Free, + Orbit, + Arcball, + Turntable, + TopDown, + Isometric, + Chase, + Dolly, + DollyZoom, + Path + }; + + /// @brief Optional typed capabilities exposed by a concrete runtime camera implementation. + enum CameraCapability : uint32_t + { + None = 0u, + SphericalTarget = core::createBitmask({ 0 }), + DynamicPerspectiveFov = core::createBitmask({ 1 }) + }; + + /// @brief Typed state fragments that helper layers may capture from or apply to a camera. + enum GoalStateMask : uint32_t + { + GoalStateNone = 0u, + GoalStateSphericalTarget = core::createBitmask({ 0 }), + GoalStateDynamicPerspective = core::createBitmask({ 1 }), + GoalStatePath = core::createBitmask({ 2 }) + }; + + /// @brief Canonical target-relative state reported by spherical camera families. + /// + /// The state stores the tracked target position, orbit angles in `orbitUv`, + /// and distance limits needed by tooling that wants to capture or reapply a + /// target-relative camera pose without going through free-form setters. + /// `maxDistance` is an optional upper bound and may be infinite when the + /// active camera family does not impose a finite cap. + struct SphericalTargetState + { + /// @brief Tracked target position in world space. + hlsl::float64_t3 target = hlsl::float64_t3(0.0); + /// @brief Orbit yaw and pitch around the target, expressed in radians. + hlsl::float64_t2 orbitUv = hlsl::float64_t2(0.0); + /// @brief Current camera-to-target distance. + float distance = 0.f; + /// @brief Lowest distance that remains valid for the current camera. + float minDistance = 0.f; + /// @brief Highest distance that remains valid for the current camera, or infinity when unbounded. + float maxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; + }; + + /// @brief Typed perspective state reported by cameras with derived FOV behavior. + struct DynamicPerspectiveState + { + /// @brief Authored reference FOV in degrees. + float baseFov = 0.f; + /// @brief Distance at which `baseFov` should be preserved. + float referenceDistance = 0.f; + }; + + /// @brief Limits constraining reusable `PathState` coordinates for `Path Rig` cameras. + /// + /// These limits are part of the typed path-model surface. They are not + /// global engine rules. A concrete `Path Rig` instance may expose an + /// unbounded `maxDistance` by returning infinity. + struct PathStateLimits + { + /// @brief Minimal valid `u` coordinate after path-state sanitization. + double minU = static_cast(SCameraTargetRelativeTraits::MinDistance); + /// @brief Minimal valid radial distance derived from the `(u, v)` pair. + hlsl::float64_t minDistance = static_cast(SCameraTargetRelativeTraits::MinDistance); + /// @brief Maximal valid radial distance derived from the `(u, v)` pair, or infinity when unbounded. + hlsl::float64_t maxDistance = static_cast(SCameraTargetRelativeTraits::DefaultMaxDistance); + }; + + /// @brief Parametric path-rig state used by the `Path Rig` camera kind. + /// + /// The built-in path model interprets `(s, u, v, roll)` as path progress, + /// lateral shape coordinates, and roll around the local forward axis. + /// Other path models may map the same coordinates onto different geometry. + struct PathState + { + /// @brief Primary path-progress coordinate interpreted by the active path model. + double s = 0.0; + /// @brief First lateral/shape coordinate interpreted by the active path model. + double u = 0.0; + /// @brief Second lateral/shape coordinate interpreted by the active path model. + double v = 0.0; + /// @brief Roll around the path-model forward axis, expressed in radians. + double roll = 0.0; + + /// @brief Pack the state into one four-component vector. + inline hlsl::float64_t4 asVector() const + { + return hlsl::float64_t4(s, u, v, roll); + } + + /// @brief Project the state onto the translation-style representation used by replay helpers. + inline hlsl::float64_t3 asTranslationVector() const + { + return hlsl::float64_t3(u, v, s); + } + + /// @brief Rebuild one path state from the packed vector representation. + static inline PathState fromVector(const hlsl::float64_t4& value) + { + return { + .s = value.x, + .u = value.y, + .v = value.z, + .roll = value.w + }; + } + + /// @brief Rebuild one path state from the translation-style helper representation. + static inline PathState fromTranslationVector(const hlsl::float64_t3& value, const double pathRoll = 0.0) + { + return { + .s = value.z, + .u = value.x, + .v = value.y, + .roll = pathRoll + }; + } + }; + + /// @brief Gimbal that stores the runtime camera pose and cached world-to-view transform. + /// + /// Camera implementations own one `CGimbal` instance and update it after + /// applying their internal state model. The gimbal stores world-space + /// position, orientation, and the cached view matrix derived from them. + class CGimbal : public IGimbal + { + public: + using base_t = IGimbal; + using model_matrix_t = typename base_t::model_matrix_t; + + CGimbal(typename base_t::SCreationParameters parameters) : base_t(std::move(parameters)) { updateView(); } + ~CGimbal() = default; + + inline void begin() { base_t::begin(); } + inline void setPosition(const hlsl::float64_t3& position) { base_t::setPosition(position); } + inline void setScale(const hlsl::float64_t3& scale) { base_t::setScale(scale); } + inline void setOrientation(const hlsl::camera_quaternion_t& orientation) { base_t::setOrientation(orientation); } + inline void transform(const CReferenceTransform& reference, const typename base_t::VirtualImpulse& impulse) { base_t::transform(reference, impulse); } + inline void rotate(const hlsl::float64_t3& axis, float dRadians) { base_t::rotate(axis, dRadians); } + inline void move(hlsl::float64_t3 delta) { base_t::move(delta); } + inline void strafe(hlsl::float64_t distance) { base_t::strafe(distance); } + inline void climb(hlsl::float64_t distance) { base_t::climb(distance); } + inline void advance(hlsl::float64_t distance) { base_t::advance(distance); } + inline void end() { base_t::end(); } + + inline const hlsl::float64_t3& getPosition() const { return base_t::getPosition(); } + inline const hlsl::camera_quaternion_t& getOrientation() const { return base_t::getOrientation(); } + inline const hlsl::float64_t3& getScale() const { return base_t::getScale(); } + inline const hlsl::matrix& getOrthonornalMatrix() const { return base_t::getOrthonornalMatrix(); } + inline const hlsl::float64_t3& getXAxis() const { return base_t::getXAxis(); } + inline const hlsl::float64_t3& getYAxis() const { return base_t::getYAxis(); } + inline const hlsl::float64_t3& getZAxis() const { return base_t::getZAxis(); } + inline hlsl::float64_t3 getLocalTarget() const { return base_t::getLocalTarget(); } + inline hlsl::float64_t3 getWorldTarget() const { return base_t::getWorldTarget(); } + inline const size_t& getManipulationCounter() const { return base_t::getManipulationCounter(); } + inline bool isManipulating() const { return base_t::isManipulating(); } + inline bool extractReferenceTransform(CReferenceTransform* out, const hlsl::float64_t4x4* referenceFrame = nullptr) const + { + return base_t::extractReferenceTransform(out, referenceFrame); + } + + template + inline typename base_t::VirtualImpulse accumulate(std::span virtualEvents) + { + return base_t::template accumulate(virtualEvents); + } + + /// @brief Rebuild the cached world-to-view matrix from the current gimbal pose. + inline void updateView() + { + const auto& gRight = this->getXAxis(); + const auto& gUp = this->getYAxis(); + const auto& gForward = this->getZAxis(); + + assert(hlsl::isOrthoBase(gRight, gUp, gForward)); + + const auto& position = this->getPosition(); + + m_viewMatrix[0u] = hlsl::float64_t4(gRight, -hlsl::dot(gRight, position)); + m_viewMatrix[1u] = hlsl::float64_t4(gUp, -hlsl::dot(gUp, position)); + m_viewMatrix[2u] = hlsl::float64_t4(gForward, -hlsl::dot(gForward, position)); + } + + /// @brief Return the cached world-to-view matrix derived from the current pose. + inline const hlsl::float64_t3x4& getViewMatrix() const { return m_viewMatrix; } + + private: + hlsl::float64_t3x4 m_viewMatrix; + }; + + class SScopedMotionScaleOverride + { + public: + /// @brief Temporarily override both motion scales and restore the previous values on destruction. + SScopedMotionScaleOverride(ICamera* camera, const double moveScale, const double rotationScale) + : m_camera(camera) + { + if (!m_camera) + return; + + m_prevMoveScale = m_camera->getMoveSpeedScale(); + m_prevRotationScale = m_camera->getRotationSpeedScale(); + m_camera->setMotionScales(moveScale, rotationScale); + } + + SScopedMotionScaleOverride(const SScopedMotionScaleOverride&) = delete; + SScopedMotionScaleOverride& operator=(const SScopedMotionScaleOverride&) = delete; + + SScopedMotionScaleOverride(SScopedMotionScaleOverride&& other) noexcept + : m_camera(std::exchange(other.m_camera, nullptr)), + m_prevMoveScale(other.m_prevMoveScale), + m_prevRotationScale(other.m_prevRotationScale) + { + } + + SScopedMotionScaleOverride& operator=(SScopedMotionScaleOverride&& other) = delete; + + ~SScopedMotionScaleOverride() + { + if (m_camera) + m_camera->setMotionScales(m_prevMoveScale, m_prevRotationScale); + } + + private: + ICamera* m_camera = nullptr; + double m_prevMoveScale = 0.0; + double m_prevRotationScale = 0.0; + }; + + ICamera() {} + virtual ~ICamera() = default; + + /// @brief Return the mutable gimbal backing the runtime camera pose. + virtual const CGimbal& getGimbal() = 0u; + + /// @brief Apply one frame of semantic virtual events and an optional rigid reference-frame anchor. + /// + /// `virtualEvents` stores one frame of semantic movement, rotation, and + /// scale commands. Translation commands use `Move*`, rotation commands use + /// `Tilt*`, `Pan*`, and `Roll*`, and scale commands use `Scale*`. Cameras + /// interpret only the subset advertised by `getAllowedVirtualEvents()`. + /// + /// `referenceFrame` is an optional rigid world-space transform used as the + /// anchor for this manipulation step. Free-like cameras may apply it + /// directly as pose input. Constrained cameras may first resolve it into + /// their own typed legal state and then apply event deltas in that state + /// space. + virtual bool manipulate(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) = 0; + /// @brief Apply one frame of virtual events while temporarily overriding the camera-local motion scales. + inline bool manipulateWithMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame, const double moveScale, const double rotationScale) + { + auto scopedOverride = overrideMotionScales(moveScale, rotationScale); + return manipulate(virtualEvents, referenceFrame); + } + /// @brief Apply one frame of virtual events with unit translation and rotation scales. + inline bool manipulateWithUnitMotionScales(std::span virtualEvents, const hlsl::float64_t4x4* referenceFrame = nullptr) + { + return manipulateWithMotionScales(virtualEvents, referenceFrame, 1.0, 1.0); + } + + /// @brief Return the semantic virtual-event mask accepted by this camera kind. + /// + /// Input binders, scripted replay, and restore helpers use this mask to + /// decide which `CVirtualGimbalEvent` categories may be passed to + /// `manipulate(...)`. + virtual uint32_t getAllowedVirtualEvents() const = 0u; + + /// @brief Return the stable camera-family identifier for this concrete runtime camera. + virtual CameraKind getKind() const = 0; + /// @brief Return the optional typed capabilities exposed by this camera implementation. + virtual uint32_t getCapabilities() const { return None; } + /// @brief Return the typed goal-state fragments that helper layers may safely use with this camera. + virtual uint32_t getGoalStateMask() const + { + uint32_t mask = GoalStateNone; + if (hasCapability(SphericalTarget)) + mask |= GoalStateSphericalTarget; + if (hasCapability(DynamicPerspectiveFov)) + mask |= GoalStateDynamicPerspective; + return mask; + } + + /// @brief Return the stable human-readable identifier for this concrete camera instance. + virtual std::string_view getIdentifier() const = 0u; + + /// @brief Check whether the camera exposes the requested optional capability. + inline bool hasCapability(CameraCapability capability) const + { + return (getCapabilities() & capability) == capability; + } + + /// @brief Check whether the camera can exchange the requested typed goal-state fragment. + inline bool supportsGoalState(GoalStateMask goalState) const + { + return (getGoalStateMask() & goalState) == goalState; + } + + /// @brief Query the current spherical-target state when the camera exposes it. + virtual bool tryGetSphericalTargetState(SphericalTargetState& out) const + { + return false; + } + + /// @brief Replace only the tracked target position for spherical-target cameras. + virtual bool trySetSphericalTarget(const hlsl::float64_t3& target) + { + return false; + } + + /// @brief Replace only the tracked target distance for spherical-target cameras. + virtual bool trySetSphericalDistance(float distance) + { + return false; + } + + /// @brief Query the current derived dynamic perspective FOV when the camera exposes it. + virtual bool tryGetDynamicPerspectiveFov(float& outFov) const + { + return false; + } + + /// @brief Query the current authored dynamic perspective state when the camera exposes it. + virtual bool tryGetDynamicPerspectiveState(DynamicPerspectiveState& out) const + { + return false; + } + + /// @brief Replace the authored dynamic perspective state when the camera exposes it. + virtual bool trySetDynamicPerspectiveState(const DynamicPerspectiveState& state) + { + return false; + } + + /// @brief Query the current typed path state when the camera exposes it. + virtual bool tryGetPathState(PathState& out) const + { + return false; + } + + /// @brief Query the active typed limits constraining the current path state. + virtual bool tryGetPathStateLimits(PathStateLimits& out) const + { + return false; + } + + /// @brief Replace the current typed path state when the camera exposes it. + virtual bool trySetPathState(const PathState& state) + { + return false; + } + + /// @brief Update only the translation motion scale used by the camera runtime. + inline void setMoveSpeedScale(double scalar) + { + m_motionConfig.moveSpeedScale = scalar; + } + + /// @brief Update only the rotation motion scale used by the camera runtime. + inline void setRotationSpeedScale(double scalar) + { + m_motionConfig.rotationSpeedScale = scalar; + } + + /// @brief Update both translation and rotation motion scales at once. + inline void setMotionScales(const double moveScale, const double rotationScale) + { + setMoveSpeedScale(moveScale); + setRotationSpeedScale(rotationScale); + } + + /// @brief Return the current translation motion scale. + inline double getMoveSpeedScale() const { return m_motionConfig.moveSpeedScale; } + /// @brief Return the current rotation motion scale. + inline double getRotationSpeedScale() const { return m_motionConfig.rotationSpeedScale; } + /// @brief Return the full motion-scale bundle. + inline const SMotionConfig& getMotionConfig() const { return m_motionConfig; } + /// @brief Return the effective world-space translation represented by a unit virtual move event. + inline double getScaledVirtualTranslationMagnitude() const + { + return getUnscaledVirtualTranslationMagnitude() * getMoveSpeedScale(); + } + /// @brief Return the raw translation magnitude before applying the camera-local move scale. + inline double getUnscaledVirtualTranslationMagnitude() const + { + return VirtualTranslationUnit; + } + /// @brief Scale one scalar translation magnitude through the active move scale. + inline double scaleVirtualTranslation(const double magnitude) const + { + return magnitude * getScaledVirtualTranslationMagnitude(); + } + /// @brief Scale one translation vector through the active move scale. + template + inline hlsl::camera_vector_t scaleVirtualTranslation(const hlsl::camera_vector_t& magnitude) const + { + return magnitude * static_cast(getScaledVirtualTranslationMagnitude()); + } + /// @brief Scale one scalar translation magnitude without applying the camera-local move scale. + inline double scaleUnscaledVirtualTranslation(const double magnitude) const + { + return magnitude * getUnscaledVirtualTranslationMagnitude(); + } + /// @brief Scale one translation vector without applying the camera-local move scale. + template + inline hlsl::camera_vector_t scaleUnscaledVirtualTranslation(const hlsl::camera_vector_t& magnitude) const + { + return magnitude * static_cast(getUnscaledVirtualTranslationMagnitude()); + } + /// @brief Scale one scalar rotation magnitude through the active rotation scale. + inline double scaleVirtualRotation(const double magnitude) const + { + return magnitude * getRotationSpeedScale(); + } + /// @brief Scale one rotation vector through the active rotation scale. + template + inline hlsl::camera_vector_t scaleVirtualRotation(const hlsl::camera_vector_t& magnitude) const + { + return magnitude * static_cast(getRotationSpeedScale()); + } + /// @brief Create a scoped helper that restores the previous motion scales on destruction. + inline SScopedMotionScaleOverride overrideMotionScales(const double moveScale, const double rotationScale) + { + return SScopedMotionScaleOverride(this, moveScale, rotationScale); + } + +protected: + SMotionConfig m_motionConfig; +}; + +} + +#endif // _I_CAMERA_HPP_ diff --git a/include/nbl/ext/Cameras/IGimbal.hpp b/include/nbl/ext/Cameras/IGimbal.hpp new file mode 100644 index 0000000000..a2c5dc79e4 --- /dev/null +++ b/include/nbl/ext/Cameras/IGimbal.hpp @@ -0,0 +1,369 @@ +#ifndef _NBL_IGIMBAL_HPP_ +#define _NBL_IGIMBAL_HPP_ + +#include +#include +#include +#include + +#include "CCameraMathUtilities.hpp" +#include "CVirtualGimbalEvent.hpp" + +namespace nbl::core +{ + /// @brief Optional rigid reference frame used to reinterpret a frame of semantic camera input. + /// + /// Some camera consumers replay authored input relative to an external frame + /// instead of the current camera pose. This bundle stores the rigid transform + /// and its orientation in a form ready for `IGimbal::transform(...)`. + struct CReferenceTransform + { + hlsl::float64_t4x4 frame; + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + }; + + /// @brief Generic world-space gimbal used by runtime cameras and tracked targets. + /// + /// The gimbal stores position, orientation, scale, and an orthonormal local + /// basis. It also exposes `accumulate(...)`, which converts one batch of + /// semantic `CVirtualGimbalEvent` values into translation, rotation, and + /// scale impulses for a single manipulation step. + template + requires is_any_of_v + class IGimbal + { + public: + using precision_t = T; + using quaternion_t = hlsl::camera_quaternion_t; + template + using vector_t = hlsl::camera_vector_t; + /// @brief underlying type for world matrix (TRS) + using model_matrix_t = hlsl::matrix; + + /// @brief One frame of accumulated virtual translation, rotation, and scaling intent. + struct VirtualImpulse + { + vector_t<3u> dVirtualTranslate { 0.0f }, dVirtualRotation { 0.0f }, dVirtualScale { 1.0f }; + }; + + /// @brief Accumulates one frame of virtual events into a translation/rotation/scale impulse. + template + VirtualImpulse accumulate(std::span virtualEvents, const vector_t<3u>& gRightOverride, const vector_t<3u>& gUpOverride, const vector_t<3u>& gForwardOverride) + { + VirtualImpulse impulse; + + for (const auto& event : virtualEvents) + { + assert(event.magnitude >= 0); + + // translation events + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveRight) + if (event.type == CVirtualGimbalEvent::MoveRight) + impulse.dVirtualTranslate.x += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveLeft) + if (event.type == CVirtualGimbalEvent::MoveLeft) + impulse.dVirtualTranslate.x -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveUp) + if (event.type == CVirtualGimbalEvent::MoveUp) + impulse.dVirtualTranslate.y += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveDown) + if (event.type == CVirtualGimbalEvent::MoveDown) + impulse.dVirtualTranslate.y -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveForward) + if (event.type == CVirtualGimbalEvent::MoveForward) + impulse.dVirtualTranslate.z += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::MoveBackward) + if (event.type == CVirtualGimbalEvent::MoveBackward) + impulse.dVirtualTranslate.z -= static_cast(event.magnitude); + + // rotation events + if constexpr (AllowedEvents & CVirtualGimbalEvent::TiltUp) + if (event.type == CVirtualGimbalEvent::TiltUp) + impulse.dVirtualRotation.x += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::TiltDown) + if (event.type == CVirtualGimbalEvent::TiltDown) + impulse.dVirtualRotation.x -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::PanRight) + if (event.type == CVirtualGimbalEvent::PanRight) + impulse.dVirtualRotation.y += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::PanLeft) + if (event.type == CVirtualGimbalEvent::PanLeft) + impulse.dVirtualRotation.y -= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::RollRight) + if (event.type == CVirtualGimbalEvent::RollRight) + impulse.dVirtualRotation.z += static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::RollLeft) + if (event.type == CVirtualGimbalEvent::RollLeft) + impulse.dVirtualRotation.z -= static_cast(event.magnitude); + + // scaling events + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXInc) + if (event.type == CVirtualGimbalEvent::ScaleXInc) + impulse.dVirtualScale.x *= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleXDec) + if (event.type == CVirtualGimbalEvent::ScaleXDec) + impulse.dVirtualScale.x *= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYInc) + if (event.type == CVirtualGimbalEvent::ScaleYInc) + impulse.dVirtualScale.y *= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleYDec) + if (event.type == CVirtualGimbalEvent::ScaleYDec) + impulse.dVirtualScale.y *= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZInc) + if (event.type == CVirtualGimbalEvent::ScaleZInc) + impulse.dVirtualScale.z *= static_cast(event.magnitude); + + if constexpr (AllowedEvents & CVirtualGimbalEvent::ScaleZDec) + if (event.type == CVirtualGimbalEvent::ScaleZDec) + impulse.dVirtualScale.z *= static_cast(event.magnitude); + } + + return impulse; + } + + /// @brief Accumulate one frame of virtual events using the current gimbal basis as the reference frame. + template + VirtualImpulse accumulate(std::span virtualEvents) + { + return accumulate(virtualEvents, getXAxis(), getYAxis(), getZAxis()); + } + + /// @brief Construction-time pose for one gimbal instance. + struct SCreationParameters + { + vector_t<3u> position; + quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + }; + + IGimbal(const IGimbal&) = default; + IGimbal(IGimbal&&) noexcept = default; + IGimbal& operator=(const IGimbal&) = default; + IGimbal& operator=(IGimbal&&) noexcept = default; + + IGimbal(SCreationParameters&& parameters) + : m_position(parameters.position), m_orientation(parameters.orientation) + { + updateOrthonormalOrientationBase(); + } + + /// @brief Enter manipulation mode and reset the per-frame manipulation counter. + void begin() + { + m_isManipulating = true; + m_counter = 0u; + } + + /// @brief Replace the world-space position while the gimbal is in manipulation mode. + inline void setPosition(const vector_t<3u>& position) + { + assert(m_isManipulating); + + if (m_position != position) + m_counter++; + + m_position = position; + } + + /// @brief Replace the scale component stored by the gimbal. + inline void setScale(const vector_t<3u>& scale) + { + m_scale = scale; + } + + /// @brief Replace the orientation while keeping the orthonormal basis normalized. + inline void setOrientation(const quaternion_t& orientation) + { + assert(m_isManipulating); + + if (m_orientation.data != orientation.data) + m_counter++; + + m_orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); + updateOrthonormalOrientationBase(); + } + + /// @brief Apply a prebuilt rigid reference transform and an accumulated impulse in one step. + inline void transform(const CReferenceTransform& reference, const VirtualImpulse& impulse) + { + setOrientation(reference.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(impulse.dVirtualRotation)); + setPosition( + hlsl::float64_t3(reference.frame[3]) + + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate)) + ); + } + + /// @brief Rotate the gimbal around a world-space axis by the requested angle in radians. + inline void rotate(const vector_t<3u>& axis, float dRadians) + { + assert(m_isManipulating); + + if(dRadians) + m_counter++; + + const auto dRotation = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(axis, static_cast(dRadians)); + m_orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(dRotation * m_orientation); + updateOrthonormalOrientationBase(); + } + + /// @brief Translate the gimbal directly in world space. + inline void move(vector_t<3u> delta) + { + assert(m_isManipulating); + + auto newPosition = m_position + delta; + + if (newPosition != m_position) + m_counter++; + + m_position = newPosition; + } + + /// @brief Translate the gimbal along its local right axis. + inline void strafe(precision_t distance) + { + move(getXAxis() * distance); + } + + /// @brief Translate the gimbal along its local up axis. + inline void climb(precision_t distance) + { + move(getYAxis() * distance); + } + + /// @brief Translate the gimbal along its local forward axis. + inline void advance(precision_t distance) + { + move(getZAxis() * distance); + } + + /// @brief Leave manipulation mode after all pose updates for the current frame are finished. + inline void end() + { + m_isManipulating = false; + } + + /// @brief Position of gimbal in world space + inline const vector_t<3u>& getPosition() const { return m_position; } + + /// @brief Orientation of gimbal + inline const quaternion_t& getOrientation() const { return m_orientation; } + + /// @brief Scale transform component + inline const vector_t<3u>& getScale() const { return m_scale; } + + /// @brief World matrix (TRS) + template + requires is_any_of_v> + const TRS operator()() const + { + const auto& position = getPosition(); + const auto& rotation = getOrthonornalMatrix(); + const auto& scale = getScale(); + + if constexpr (std::is_same_v) + { + return + { + hlsl::camera_vector_t(rotation[0] * scale.x, position.x), + hlsl::camera_vector_t(rotation[1] * scale.y, position.y), + hlsl::camera_vector_t(rotation[2] * scale.z, position.z) + }; + } + else + { + return + { + hlsl::camera_vector_t(rotation[0] * scale.x, T(0)), + hlsl::camera_vector_t(rotation[1] * scale.y, T(0)), + hlsl::camera_vector_t(rotation[2] * scale.z, T(0)), + hlsl::camera_vector_t(position, T(1)) + }; + } + } + + /// @brief Orthonormal [getXAxis(), getYAxis(), getZAxis()] orientation matrix + inline const hlsl::matrix& getOrthonornalMatrix() const { return m_orthonormal; } + + /// @brief Base "right" vector in orthonormal orientation basis (X-axis) + inline const vector_t<3u>& getXAxis() const { return m_orthonormal[0u]; } + + /// @brief Base "up" vector in orthonormal orientation basis (Y-axis) + inline const vector_t<3u>& getYAxis() const { return m_orthonormal[1u]; } + + /// @brief Base "forward" vector in orthonormal orientation basis (Z-axis) + inline const vector_t<3u>& getZAxis() const { return m_orthonormal[2u]; } + + /// @brief Target vector in local space, alias for getZAxis() + inline vector_t<3u> getLocalTarget() const { return getZAxis(); } + + /// @brief Target vector in world space + inline vector_t<3u> getWorldTarget() const { return getPosition() + getLocalTarget(); } + + /// @brief Counts how many times a valid manipulation has been performed, the counter resets when begin() is called + inline const size_t& getManipulationCounter() const { return m_counter; } + + /// @brief Returns true if gimbal records a manipulation + inline bool isManipulating() const { return m_isManipulating; } + + /// @brief Build a rigid reference transform either from an external frame or from the current gimbal pose. + bool extractReferenceTransform(CReferenceTransform* out, const hlsl::float64_t4x4* referenceFrame = nullptr) const + { + if (not out) + return false; + + if (referenceFrame) + { + if (!hlsl::CCameraMathUtilities::tryBuildRigidFrameFromTransform(*referenceFrame, out->frame, out->orientation)) + return false; + } + else + { + out->orientation = getOrientation(); + out->frame = hlsl::CCameraMathUtilities::composeTransformMatrix(getPosition(), out->orientation); + } + + return true; + } + + private: + inline void updateOrthonormalOrientationBase() + { + m_orthonormal = hlsl::CCameraMathUtilities::getQuaternionBasisMatrix(m_orientation); + } + + /// @brief Position of a gimbal in world space + vector_t<3u> m_position; + + /// @brief Normalized orientation of gimbal + quaternion_t m_orientation; + + /// @brief Scale transform component + vector_t<3u> m_scale = { 1.f, 1.f , 1.f }; + + /// @brief Orthonormal basis reconstructed from the current orientation. + hlsl::matrix m_orthonormal; + + /// @brief Counter that increments for each performed manipulation, resets with each begin() call + size_t m_counter = {}; + + /// @brief Tracks whether gimbal is currently in manipulation mode + bool m_isManipulating = false; + + }; +} // namespace nbl::core + +#endif // _NBL_IGIMBAL_HPP_ diff --git a/include/nbl/ext/Cameras/IGimbalBindingLayout.hpp b/include/nbl/ext/Cameras/IGimbalBindingLayout.hpp new file mode 100644 index 0000000000..c1b09427bb --- /dev/null +++ b/include/nbl/ext/Cameras/IGimbalBindingLayout.hpp @@ -0,0 +1,131 @@ +#ifndef _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ +#define _NBL_I_GIMBAL_BINDING_LAYOUT_HPP_ + +#include +#include + +#include "CVirtualGimbalEvent.hpp" +#include "nbl/ui/KeyCodes.h" + +namespace nbl::ui +{ + +/// @brief Static mapping from external input domains to virtual gimbal events. +/// +/// This type stores binding layout only. It does not process runtime input. +/// Each binding chooses both the semantic virtual event type and the gain used +/// to convert raw producer values into `CVirtualGimbalEvent::magnitude`. +struct IGimbalBindingLayout +{ + IGimbalBindingLayout() {} + virtual ~IGimbalBindingLayout() {} + + using gimbal_event_t = core::CVirtualGimbalEvent; + using encode_keyboard_code_t = ui::E_KEY_CODE; + using encode_mouse_code_t = ui::E_MOUSE_CODE; + using encode_imguizmo_code_t = gimbal_event_t::VirtualEventType; + + enum BindingDomain : uint8_t + { + Keyboard, + Mouse, + Imguizmo, + + Count + }; + + struct CKeyInfo + { + union + { + encode_keyboard_code_t keyboardCode; + encode_mouse_code_t mouseCode; + encode_imguizmo_code_t imguizmoCode; + }; + + CKeyInfo(encode_keyboard_code_t code) : keyboardCode(code), type(Keyboard) {} + CKeyInfo(encode_mouse_code_t code) : mouseCode(code), type(Mouse) {} + CKeyInfo(encode_imguizmo_code_t code) : imguizmoCode(code), type(Imguizmo) {} + + BindingDomain type; + }; + + struct CHashInfo + { + static inline constexpr double DefaultMagnitudeScale = 1.0; + + CHashInfo() {} + CHashInfo(gimbal_event_t::VirtualEventType _type, const double _magnitudeScale = DefaultMagnitudeScale) + : event({ .type = _type }), magnitudeScale(_magnitudeScale) {} + ~CHashInfo() = default; + + /// @brief Virtual event emitted by this binding. + gimbal_event_t event = {}; + /// @brief Per-binding gain applied when raw input is converted into one virtual-event magnitude. + double magnitudeScale = DefaultMagnitudeScale; + /// @brief Runtime latch used by held keyboard and mouse-button bindings. + bool active = false; + }; + + using keyboard_to_virtual_events_t = std::unordered_map; + using mouse_to_virtual_events_t = std::unordered_map; + using imguizmo_to_virtual_events_t = std::unordered_map; + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const = 0; + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const = 0; + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const = 0; + + virtual void updateKeyboardMapping(const std::function& mapKeys) = 0; + virtual void updateMouseMapping(const std::function& mapKeys) = 0; + virtual void updateImguizmoMapping(const std::function& mapKeys) = 0; + + inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) + { + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); + } + + inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const + { + layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); + layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); + layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); + } + +protected: + template + inline static Map sanitizeMapping(const Map& source) + { + Map result; + for (const auto& [code, hash] : source) + result.emplace(code, typename Map::mapped_type(hash.event.type, hash.magnitudeScale)); + return result; + } +}; + +class CGimbalBindingLayoutStorage : public IGimbalBindingLayout +{ +public: + /// @brief Mutable storage for active or preset binding layout. + using IGimbalBindingLayout::IGimbalBindingLayout; + + CGimbalBindingLayoutStorage() {} + virtual ~CGimbalBindingLayoutStorage() {} + + virtual void updateKeyboardMapping(const std::function& mapKeys) override { mapKeys(m_keyboardVirtualEventMap); } + virtual void updateMouseMapping(const std::function& mapKeys) override { mapKeys(m_mouseVirtualEventMap); } + virtual void updateImguizmoMapping(const std::function& mapKeys) override { mapKeys(m_imguizmoVirtualEventMap); } + + virtual const keyboard_to_virtual_events_t& getKeyboardVirtualEventMap() const override { return m_keyboardVirtualEventMap; } + virtual const mouse_to_virtual_events_t& getMouseVirtualEventMap() const override { return m_mouseVirtualEventMap; } + virtual const imguizmo_to_virtual_events_t& getImguizmoVirtualEventMap() const override { return m_imguizmoVirtualEventMap; } + + keyboard_to_virtual_events_t m_keyboardVirtualEventMap; + mouse_to_virtual_events_t m_mouseVirtualEventMap; + imguizmo_to_virtual_events_t m_imguizmoVirtualEventMap; +}; + +} // namespace nbl::ui + +#endif diff --git a/include/nbl/ext/Cameras/IGimbalInputProcessor.hpp b/include/nbl/ext/Cameras/IGimbalInputProcessor.hpp new file mode 100644 index 0000000000..4e6f7bfd4e --- /dev/null +++ b/include/nbl/ext/Cameras/IGimbalInputProcessor.hpp @@ -0,0 +1,438 @@ +#ifndef _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ +#define _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ + +#include +#include + +#include "nbl/ui/KeyCodes.h" +#include "nbl/ui/SInputEvent.h" + +#include "IGimbalBindingLayout.hpp" + +namespace nbl::ui +{ + +/// @brief Runtime processor that turns keyboard, mouse, and ImGuizmo input into virtual events. +/// +/// Held keyboard and mouse-button bindings emit `frameDeltaSeconds * magnitudeScale`. +/// Relative mouse movement, mouse scroll, and ImGuizmo deltas emit +/// `abs(rawDelta) * magnitudeScale` per bound axis. The result is written into +/// `CVirtualGimbalEvent::magnitude`. +class IGimbalInputProcessor : public CGimbalBindingLayoutStorage +{ +public: + struct SInputProcessorDefaults final + { + /// @brief Largest frame interval, in seconds, accepted from held-input accumulation. + static inline constexpr double MaxFrameDeltaSeconds = 0.2; + static inline constexpr float ZeroPivot = 0.0f; + static inline constexpr float UnitPivot = 1.0f; + }; + static inline constexpr double MaxFrameDeltaSeconds = SInputProcessorDefaults::MaxFrameDeltaSeconds; + static inline constexpr float ZeroPivot = SInputProcessorDefaults::ZeroPivot; + static inline constexpr float UnitPivot = SInputProcessorDefaults::UnitPivot; + + using CGimbalBindingLayoutStorage::CGimbalBindingLayoutStorage; + + IGimbalInputProcessor() = default; + virtual ~IGimbalInputProcessor() = default; + + /// @brief Keyboard events consumed by the processor. + using input_keyboard_event_t = ui::SKeyboardEvent; + + /// @brief Mouse events consumed by the processor. + using input_mouse_event_t = ui::SMouseEvent; + + /// @brief ImGuizmo world-space delta transforms consumed by the processor. + using input_imguizmo_event_t = hlsl::float32_t4x4; + + void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) + { + m_nextPresentationTimeStamp = nextPresentationTimeStamp; + m_frameDeltaSeconds = clampFrameDeltaTimeSeconds(m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp); + } + + void endInputProcessing() + { + m_lastVirtualUpTimeStamp = m_nextPresentationTimeStamp; + } + + struct SUpdateParameters + { + std::span keyboardEvents = {}; + std::span mouseEvents = {}; + std::span imguizmoEvents = {}; + }; + + /// @brief Process combined events from `SUpdateParameters` into virtual manipulation events. + /// + /// @note This function combines keyboard, mouse, and ImGuizmo processing. + /// It delegates the actual work to `processKeyboard`, `processMouse`, and + /// `processImguizmo`, then accumulates their output and total count. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output total number of generated gimbal events. + /// @param parameters Individual keyboard, mouse, and ImGuizmo input spans. + void process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters = {}) + { + count = 0u; + uint32_t vKeyboardEventsCount = {}, vMouseEventsCount = {}, vImguizmoEventsCount = {}; + + if (output) + { + processKeyboard(output, vKeyboardEventsCount, parameters.keyboardEvents); output += vKeyboardEventsCount; + processMouse(output, vMouseEventsCount, parameters.mouseEvents); output += vMouseEventsCount; + processImguizmo(output, vImguizmoEventsCount, parameters.imguizmoEvents); + } + else + { + processKeyboard(nullptr, vKeyboardEventsCount, {}); + processMouse(nullptr, vMouseEventsCount, {}); + processImguizmo(nullptr, vImguizmoEventsCount, {}); + } + + count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; + } + + /// @brief Process keyboard events into virtual manipulation events. + /// + /// @note This function maps keyboard press and release events into virtual + /// gimbal manipulation events through the active keyboard bindings. + /// Held keys contribute elapsed seconds scaled by the binding gain. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output number of generated gimbal events. + /// @param events Keyboard events to process. + void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) + { + processBindingMap( + m_keyboardVirtualEventMap, + output, + count, + [&](auto& map) + { + for (const auto& keyboardEvent : events) + { + if (keyboardEvent.action == input_keyboard_event_t::ECA_PRESSED) + setBindingActiveState(map, keyboardEvent.keyCode, true); + else if (keyboardEvent.action == input_keyboard_event_t::ECA_RELEASED) + setBindingActiveState(map, keyboardEvent.keyCode, false); + } + }); + } + + /// @brief Process mouse events into virtual manipulation events. + /// + /// @note This function maps mouse clicks, scrolls, and movements into + /// virtual gimbal manipulation events through the active mouse bindings. + /// Relative movement and scroll contribute absolute signed deltas scaled by + /// the matching binding gain. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output number of generated gimbal events. + /// @param events Mouse events to process. + void processMouse(gimbal_event_t* output, uint32_t& count, std::span events) + { + processBindingMap( + m_mouseVirtualEventMap, + output, + count, + [&](auto& map) + { + for (const auto& mouseEvent : events) + { + switch (mouseEvent.type) + { + case input_mouse_event_t::EET_CLICK: + updateMouseButtonState(map, mouseEvent.clickEvent); + break; + + case input_mouse_event_t::EET_SCROLL: + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + hlsl::float32_t2( + static_cast(mouseEvent.scrollEvent.verticalScroll), + mouseEvent.scrollEvent.horizontalScroll), + SInputProcessorBindingGroups::MouseScroll, + map); + break; + + case input_mouse_event_t::EET_MOVEMENT: + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + hlsl::float32_t2( + mouseEvent.movementEvent.relativeMovementX, + mouseEvent.movementEvent.relativeMovementY), + SInputProcessorBindingGroups::MouseRelativeMovement, + map); + break; + + default: + break; + } + } + }); + } + + /// @brief Process ImGuizmo transforms into virtual gimbal events. + /// + /// @note This function converts world-space delta transforms authored by + /// ImGuizmo into translation, rotation, and scale virtual events. + /// Translation uses world-space delta components. Rotation uses extracted + /// Euler radians. Scale uses multiplicative components around pivot `1`. + /// + /// @param output Pointer to the destination array for generated gimbal events. + /// Pass `nullptr` to query only the total event count. + /// @param count Output number of generated gimbal events. + /// @param events ImGuizmo delta transforms to process. + void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) + { + processBindingMap( + m_imguizmoVirtualEventMap, + output, + count, + [&](auto& map) + { + for (const auto& ev : events) + { + const auto& deltaWorldTRS = ev; + + hlsl::SRigidTransformComponents world = {}; + if (!hlsl::CCameraMathUtilities::tryExtractRigidTransformComponents(deltaWorldTRS, world)) + continue; + + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + world.translation, + SInputProcessorBindingGroups::ImguizmoTranslation, + map); + + const auto dRotationRad = hlsl::CCameraMathUtilities::getCameraOrientationEulerRadians(world.orientation); + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + dRotationRad, + SInputProcessorBindingGroups::ImguizmoRotation, + map); + + requestMagnitudeUpdateWithSignedComponents( + UnitPivot, + world.scale, + SInputProcessorBindingGroups::ImguizmoScale, + map); + } + }); + } + +private: + template + struct SEncodedAxisBindingGroup final + { + std::array positive = {}; + std::array negative = {}; + }; + + struct SInputProcessorBindingGroups final + { + static inline constexpr SEncodedAxisBindingGroup MouseScroll = { + .positive = { + ui::EMC_VERTICAL_POSITIVE_SCROLL, + ui::EMC_HORIZONTAL_POSITIVE_SCROLL + }, + .negative = { + ui::EMC_VERTICAL_NEGATIVE_SCROLL, + ui::EMC_HORIZONTAL_NEGATIVE_SCROLL + } + }; + + static inline constexpr SEncodedAxisBindingGroup MouseRelativeMovement = { + .positive = { + ui::EMC_RELATIVE_POSITIVE_MOVEMENT_X, + ui::EMC_RELATIVE_POSITIVE_MOVEMENT_Y + }, + .negative = { + ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_X, + ui::EMC_RELATIVE_NEGATIVE_MOVEMENT_Y + } + }; + + static inline constexpr SEncodedAxisBindingGroup ImguizmoTranslation = { + .positive = { + gimbal_event_t::MoveRight, + gimbal_event_t::MoveUp, + gimbal_event_t::MoveForward + }, + .negative = { + gimbal_event_t::MoveLeft, + gimbal_event_t::MoveDown, + gimbal_event_t::MoveBackward + } + }; + + static inline constexpr SEncodedAxisBindingGroup ImguizmoRotation = { + .positive = { + gimbal_event_t::TiltUp, + gimbal_event_t::PanRight, + gimbal_event_t::RollRight + }, + .negative = { + gimbal_event_t::TiltDown, + gimbal_event_t::PanLeft, + gimbal_event_t::RollLeft + } + }; + + static inline constexpr SEncodedAxisBindingGroup ImguizmoScale = { + .positive = { + gimbal_event_t::ScaleXInc, + gimbal_event_t::ScaleYInc, + gimbal_event_t::ScaleZInc + }, + .negative = { + gimbal_event_t::ScaleXDec, + gimbal_event_t::ScaleYDec, + gimbal_event_t::ScaleZDec + } + }; + }; + + static double clampFrameDeltaTimeSeconds( + const std::chrono::microseconds nextPresentationTimeStamp, + const std::chrono::microseconds lastVirtualUpTimeStamp) + { + const auto deltaSeconds = std::chrono::duration( + nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); + if (deltaSeconds < 0.0) + return 0.0; + return std::min(deltaSeconds, MaxFrameDeltaSeconds); + } + + template + void processBindingMap(Map& map, gimbal_event_t* output, uint32_t& count, ConsumeFn&& consume) + { + count = 0u; + const auto mappedVirtualEventsCount = static_cast(map.size()); + if (!output) + { + count = mappedVirtualEventsCount; + return; + } + if (!mappedVirtualEventsCount) + return; + + preprocess(map); + consume(map); + postprocess(map, output, count); + } + + static bool tryGetMouseButtonCode( + const ui::E_MOUSE_BUTTON button, + ui::E_MOUSE_CODE& outCode) + { + switch (button) + { + case ui::EMB_LEFT_BUTTON: outCode = ui::EMC_LEFT_BUTTON; return true; + case ui::EMB_RIGHT_BUTTON: outCode = ui::EMC_RIGHT_BUTTON; return true; + case ui::EMB_MIDDLE_BUTTON: outCode = ui::EMC_MIDDLE_BUTTON; return true; + case ui::EMB_BUTTON_4: outCode = ui::EMC_BUTTON_4; return true; + case ui::EMB_BUTTON_5: outCode = ui::EMC_BUTTON_5; return true; + default: + return false; + } + } + + template + void updateMouseButtonState(Map& map, const input_mouse_event_t::SClickEvent& clickEvent) + { + ui::E_MOUSE_CODE mouseCode = ui::EMC_NONE; + if (!tryGetMouseButtonCode(clickEvent.mouseButton, mouseCode)) + return; + + if (clickEvent.action == input_mouse_event_t::SClickEvent::EA_PRESSED) + setBindingActiveState(map, mouseCode, true); + else if (clickEvent.action == input_mouse_event_t::SClickEvent::EA_RELEASED) + setBindingActiveState(map, mouseCode, false); + } + + template + void setBindingActiveState(Map& map, const Code code, const bool active) + { + const auto request = map.find(code); + if (request == map.end()) + return; + + request->second.active = active; + } + + void preprocess(auto& map) + { + for (auto& [key, hash] : map) + { + hash.event.magnitude = 0.0f; + + if (hash.active) + hash.event.magnitude = m_frameDeltaSeconds * hash.magnitudeScale; + } + } + + void postprocess(const auto& map, gimbal_event_t* output, uint32_t& count) + { + for (const auto& [key, hash] : map) + if (hash.event.magnitude) + { + auto* virtualEvent = output + count; + virtualEvent->type = hash.event.type; + virtualEvent->magnitude = hash.event.magnitude; + ++count; + } + } + + template + void requestMagnitudeUpdateWithScalar(float signPivot, float dScalar, EncodeType positive, EncodeType negative, Map& map) + { + if (dScalar != signPivot) + { + const auto dMagnitude = hlsl::abs(dScalar); + auto code = (dScalar > signPivot) ? positive : negative; + auto request = map.find(code); + if (request != map.end()) + request->second.event.magnitude += dMagnitude * request->second.magnitudeScale; + } + } + + template + void requestMagnitudeUpdateWithSignedComponents( + float signPivot, + const hlsl::vector& components, + const std::array& positive, + const std::array& negative, + Map& map) + { + for (uint32_t i = 0u; i < N; ++i) + requestMagnitudeUpdateWithScalar(signPivot, components[i], positive[i], negative[i], map); + } + + template + void requestMagnitudeUpdateWithSignedComponents( + float signPivot, + const hlsl::vector& components, + const SEncodedAxisBindingGroup& bindings, + Map& map) + { + requestMagnitudeUpdateWithSignedComponents( + signPivot, + components, + bindings.positive, + bindings.negative, + map); + } + + double m_frameDeltaSeconds = {}; + std::chrono::microseconds m_nextPresentationTimeStamp = {}, m_lastVirtualUpTimeStamp = {}; +}; + +} // namespace nbl::ui + +#endif // _NBL_I_GIMBAL_INPUT_PROCESSOR_HPP_ diff --git a/include/nbl/ext/Cameras/ILinearProjection.hpp b/include/nbl/ext/Cameras/ILinearProjection.hpp new file mode 100644 index 0000000000..0edf855454 --- /dev/null +++ b/include/nbl/ext/Cameras/ILinearProjection.hpp @@ -0,0 +1,185 @@ +#ifndef _NBL_I_LINEAR_PROJECTION_HPP_ +#define _NBL_I_LINEAR_PROJECTION_HPP_ + +#include + +#include "nbl/core/decl/smart_refctd_ptr.h" +#include "IProjection.hpp" +#include "ICamera.hpp" + +namespace nbl::core +{ + +/// @brief Interface for any custom linear projection transformation. +/// +/// Matrix elements are already evaluated scalars referencing a camera. +/// This covers perspective, orthographic, oblique, axonometric, and shear projections. +class ILinearProjection : virtual public core::IReferenceCounted +{ +protected: + ILinearProjection(core::smart_refctd_ptr&& camera) + : m_camera(std::move(camera)) {} + virtual ~ILinearProjection() = default; + + core::smart_refctd_ptr m_camera; +public: + /// @brief World transform type expected by the linear projection helpers. + using model_matrix_t = typename ICamera::CGimbal::model_matrix_t; + + /// @brief Matrix type used for fully concatenated linear transforms. + using concatenated_matrix_t = hlsl::float64_t4x4; + + /// @brief Optional inverse of a concatenated transform when the matrix is not singular. + using inv_concatenated_matrix_t = std::optional; + + /// @brief One concrete linear projection matrix together with cached inverse metadata. + struct CProjection : public IProjection + { + using IProjection::IProjection; + using projection_matrix_t = concatenated_matrix_t; + using inv_projection_matrix_t = inv_concatenated_matrix_t; + + CProjection() : CProjection(projection_matrix_t(1)) {} + CProjection(const projection_matrix_t& matrix) { setProjectionMatrix(matrix); } + + /// @brief Returns P (Projection matrix) + inline const projection_matrix_t& getProjectionMatrix() const { return m_projectionMatrix; } + + /// @brief Returns P⁻¹ (Inverse of Projection matrix) *if it exists* + inline const inv_projection_matrix_t& getInvProjectionMatrix() const { return m_invProjectionMatrix; } + + inline const std::optional& isProjectionLeftHanded() const { return m_isProjectionLeftHanded; } + inline bool isProjectionSingular() const { return m_isProjectionSingular; } + virtual ProjectionType getProjectionType() const override { return ProjectionType::Linear; } + + virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const override + { + output = hlsl::mul(m_projectionMatrix, vecToProjectionSpace); + } + + virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const override + { + if (m_isProjectionSingular) + return false; + + output = hlsl::mul(m_invProjectionMatrix.value(), vecFromProjectionSpace); + + return true; + } + + protected: + /// @brief Replace the projection matrix and rebuild cached handedness and inverse information. + inline void setProjectionMatrix(const projection_matrix_t& matrix) + { + m_projectionMatrix = matrix; + const auto det = hlsl::determinant(m_projectionMatrix); + + // we will allow you to lose a dimension since such a projection itself *may* + // be valid, however then you cannot un-project because the inverse doesn't exist! + m_isProjectionSingular = not det; + + if (m_isProjectionSingular) + { + m_isProjectionLeftHanded = std::nullopt; + m_invProjectionMatrix = std::nullopt; + } + else + { + m_isProjectionLeftHanded = det < 0.0; + m_invProjectionMatrix = hlsl::inverse(m_projectionMatrix); + } + } + + private: + projection_matrix_t m_projectionMatrix; + inv_projection_matrix_t m_invProjectionMatrix; + std::optional m_isProjectionLeftHanded; + bool m_isProjectionSingular; + }; + + /// @brief Return the number of linear projection entries owned by the concrete wrapper. + virtual uint32_t getLinearProjectionCount() const = 0; + /// @brief Return one linear projection entry by index. + virtual const CProjection& getLinearProjection(uint32_t index) const = 0; + + /// @brief Replace the camera referenced by this projection wrapper. + inline bool setCamera(core::smart_refctd_ptr&& camera) + { + if (camera) + { + m_camera = camera; + return true; + } + + return false; + } + + /// @brief Return the camera referenced by this projection wrapper. + inline ICamera* getCamera() + { + return m_camera.get(); + } + + /// @brief Compute the model-view matrix. + /// + /// @param model World TRS matrix. + /// @return The model-view matrix. + inline concatenated_matrix_t getMV(const model_matrix_t& model) const + { + const auto& v = m_camera->getGimbal().getViewMatrix(); + return hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); + } + + /// @brief Compute the model-view-projection matrix from a model matrix. + /// + /// @param projection Linear projection. + /// @param model World TRS matrix. + /// @return The model-view-projection matrix. + inline concatenated_matrix_t getMVP(const CProjection& projection, const model_matrix_t& model) const + { + const auto& v = m_camera->getGimbal().getViewMatrix(); + const auto& p = projection.getProjectionMatrix(); + auto mv = hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); + return hlsl::mul(p, mv); + } + + /// @brief Compute the model-view-projection matrix from a model-view matrix. + /// + /// @param projection Linear projection. + /// @param mv Model-view matrix. + /// @return The model-view-projection matrix. + inline concatenated_matrix_t getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const + { + const auto& p = projection.getProjectionMatrix(); + return hlsl::mul(p, mv); + } + + /// @brief Compute the inverse model-view matrix. + /// + /// @param model World TRS matrix. + /// @return The inverse model-view matrix when it exists, otherwise `std::nullopt`. + inline inv_concatenated_matrix_t getMVInverse(const model_matrix_t& model) const + { + const auto mv = getMV(model); + if (auto det = hlsl::determinant(mv); det) + return hlsl::inverse(mv); + return std::nullopt; + } + + /// @brief Compute the inverse model-view-projection matrix. + /// + /// @param projection Linear projection. + /// @param model World TRS matrix. + /// @return The inverse model-view-projection matrix when it exists, otherwise `std::nullopt`. + inline inv_concatenated_matrix_t getMVPInverse(const CProjection& projection, const model_matrix_t& model) const + { + const auto mvp = getMVP(projection, model); + if (auto det = hlsl::determinant(mvp); det) + return hlsl::inverse(mvp); + return std::nullopt; + } +}; + +} // namespace nbl::core + +#endif // _NBL_I_LINEAR_PROJECTION_HPP_ diff --git a/include/nbl/ext/Cameras/IPerspectiveProjection.hpp b/include/nbl/ext/Cameras/IPerspectiveProjection.hpp new file mode 100644 index 0000000000..5806089f8c --- /dev/null +++ b/include/nbl/ext/Cameras/IPerspectiveProjection.hpp @@ -0,0 +1,64 @@ +#ifndef _NBL_I_QUAD_PROJECTION_HPP_ +#define _NBL_I_QUAD_PROJECTION_HPP_ + +#include "ILinearProjection.hpp" + +namespace nbl::core +{ + +/// @brief Interface for quad projections. +/// +/// This projection transforms a vector into the model space of a perspective +/// quad defined by the pre-transform matrix and then projects it onto the quad +/// using the linear viewport transform. +/// +/// A perspective quad projection is represented by: +/// - a pre-transform matrix +/// - a linear viewport transform matrix +/// +/// The final projection matrix is the concatenation of those two transforms. +/// +/// @note One perspective quad projection can represent a face quad of a CAVE-like system. +class IPerspectiveProjection : public ILinearProjection +{ +public: + /// @brief One quad projection entry described by a pretransform and a viewport projection. + struct CProjection : ILinearProjection::CProjection + { + using base_t = ILinearProjection::CProjection; + + CProjection() = default; + CProjection(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) + { + setQuadTransform(pretransform, viewport); + } + + /// @brief Rebuild the concatenated quad projection from its authored components. + inline void setQuadTransform(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) + { + auto concatenated = hlsl::mul(hlsl::getMatrix3x4As4x4(pretransform), viewport); + base_t::setProjectionMatrix(concatenated); + + m_pretransform = pretransform; + m_viewport = viewport; + } + + /// @brief Return the authored pretransform applied before the viewport projection. + inline const ILinearProjection::model_matrix_t& getPretransform() const { return m_pretransform; } + /// @brief Return the authored viewport projection matrix stored for this quad. + inline const ILinearProjection::concatenated_matrix_t& getViewportProjection() const { return m_viewport; } + + private: + ILinearProjection::model_matrix_t m_pretransform = ILinearProjection::model_matrix_t(1); + ILinearProjection::concatenated_matrix_t m_viewport = ILinearProjection::concatenated_matrix_t(1); + }; + +protected: + IPerspectiveProjection(core::smart_refctd_ptr&& camera) + : ILinearProjection(core::smart_refctd_ptr(camera)) {} + virtual ~IPerspectiveProjection() = default; +}; + +} // nbl::hlsl namespace + +#endif // _NBL_I_QUAD_PROJECTION_HPP_ diff --git a/include/nbl/ext/Cameras/IPlanarProjection.hpp b/include/nbl/ext/Cameras/IPlanarProjection.hpp new file mode 100644 index 0000000000..bf5df8f1d8 --- /dev/null +++ b/include/nbl/ext/Cameras/IPlanarProjection.hpp @@ -0,0 +1,137 @@ +#ifndef _NBL_I_PLANAR_PROJECTION_HPP_ +#define _NBL_I_PLANAR_PROJECTION_HPP_ + +#include "IGimbalBindingLayout.hpp" +#include "ILinearProjection.hpp" + +namespace nbl::core +{ + +/// @brief Linear projection wrapper for one camera-facing planar viewport. +/// +/// The projection stores viewport-local binding layouts. Runtime input +/// processing is handled by `CGimbalInputBinder`. +class IPlanarProjection : public ILinearProjection +{ +public: + /// @brief One perspective or orthographic projection entry plus its viewport-local bindings. + struct CProjection : public ILinearProjection::CProjection + { + using base_t = ILinearProjection::CProjection; + + /// @brief Stable runtime classification of supported planar projection parameterizations. + enum ProjectionType : uint8_t + { + Perspective, + Orthographic, + + Count + }; + + template + static CProjection create(Args&&... args) + requires (T != Count) + { + CProjection output; + + if constexpr (T == Perspective) output.setPerspective(std::forward(args)...); + else if (T == Orthographic) output.setOrthographic(std::forward(args)...); + + return output; + } + + CProjection(const CProjection& other) = default; + CProjection(CProjection&& other) noexcept = default; + + /// @brief Authored parameter bundle stored by one planar projection entry. + struct ProjectionParameters + { + ProjectionType m_type; + + union PlanarParameters + { + struct + { + float fov; + } perspective; + + struct + { + float orthoWidth; + } orthographic; + + PlanarParameters() {} + ~PlanarParameters() {} + } m_planar; + + float m_zNear; + float m_zFar; + }; + + /// @brief Rebuild the concrete projection matrix from the stored parameters. + inline void update(bool leftHanded, float aspectRatio) + { + switch (m_parameters.m_type) + { + case Perspective: + { + const auto& fov = m_parameters.m_planar.perspective.fov; + + if (leftHanded) + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + else + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + } break; + + case Orthographic: + { + const auto& orthoW = m_parameters.m_planar.orthographic.orthoWidth; + const auto viewHeight = orthoW / aspectRatio; + + if (leftHanded) + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + else + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + } break; + } + } + + /// @brief Switch the entry to perspective mode and store its authored parameters. + inline void setPerspective(float zNear = 0.1f, float zFar = 100.f, float fov = 60.f) + { + m_parameters.m_type = Perspective; + m_parameters.m_planar.perspective.fov = fov; + m_parameters.m_zNear = zNear; + m_parameters.m_zFar = zFar; + } + + /// @brief Switch the entry to orthographic mode and store its authored parameters. + inline void setOrthographic(float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f) + { + m_parameters.m_type = Orthographic; + m_parameters.m_planar.orthographic.orthoWidth = orthoWidth; + m_parameters.m_zNear = zNear; + m_parameters.m_zFar = zFar; + } + + /// @brief Return the authored planar projection parameters. + inline const ProjectionParameters& getParameters() const { return m_parameters; } + /// @brief Return the viewport-local input binding layout stored next to this projection entry. + inline const ui::IGimbalBindingLayout& getInputBinding() const { return m_inputBinding; } + /// @brief Return mutable access to the viewport-local input binding layout. + inline ui::IGimbalBindingLayout& getInputBinding() { return m_inputBinding; } + private: + CProjection() = default; + ProjectionParameters m_parameters; + ui::CGimbalBindingLayoutStorage m_inputBinding; + }; + +protected: + IPlanarProjection(core::smart_refctd_ptr&& camera) + : ILinearProjection(std::move(camera)) {} + virtual ~IPlanarProjection() = default; +}; + +} // namespace nbl::core + +#endif // _NBL_I_PLANAR_PROJECTION_HPP_ diff --git a/include/nbl/ext/Cameras/IProjection.hpp b/include/nbl/ext/Cameras/IProjection.hpp new file mode 100644 index 0000000000..b42cb01a32 --- /dev/null +++ b/include/nbl/ext/Cameras/IProjection.hpp @@ -0,0 +1,71 @@ +#ifndef _NBL_I_PROJECTION_HPP_ +#define _NBL_I_PROJECTION_HPP_ + +#include + +namespace nbl::core +{ + +/// @brief Base interface for any reusable projection model in the camera stack. +/// +/// A projection transforms vectors between some input space and the projection +/// space understood by a concrete viewport or projection consumer. Specialized +/// interfaces such as `ILinearProjection`, `IPlanarProjection`, and +/// `IPerspectiveProjection` refine this abstraction with additional structure. +class IProjection +{ +public: + /// @brief Common vector type used by projection and unprojection operations. + using projection_vector_t = hlsl::float64_t4; + + /// @brief Stable runtime classification of supported projection families. + enum class ProjectionType + { + /// @brief Any raw linear transformation, for example it may represent Perspective, Orthographic, Oblique, Axonometric, Shear projections + Linear, + + /// @brief Specialized linear projection for planar projections with parameters + Planar, + + /// @brief Extension of planar projection represented by pre-transform & planar transform combined projecting onto R3 cave quad + CaveQuad, + + /// @brief Specialized CaveQuad projection, represents planar projections onto cube with 6 quad cube faces + Cube, + + Spherical, + ThinLens, + + Count + }; + + IProjection() = default; + virtual ~IProjection() = default; + + /// @brief Transform a vector from its input space into projection space. + /// + /// @param vecToProjectionSpace Vector to transform into projection space. + /// @param output Result vector in projection space. + virtual void project(const projection_vector_t& vecToProjectionSpace, projection_vector_t& output) const = 0; + + /// @brief Transform a vector from projection space back to the original space. + /// + /// The inverse transform may fail because the original projection may be singular. + /// + /// @param vecFromProjectionSpace Vector in projection space. + /// @param output Result vector in the original space. + /// @return `true` when the inverse transform succeeded, otherwise `false`. + virtual bool unproject(const projection_vector_t& vecFromProjectionSpace, projection_vector_t& output) const = 0; + + /// @brief Return the specific projection family implemented by the concrete instance. + /// + /// Examples include linear, spherical, and thin-lens projections as defined + /// by `ProjectionType`. + /// + /// @return The type of this projection. + virtual ProjectionType getProjectionType() const = 0; +}; + +} // namespace nbl::core + +#endif // _NBL_IPROJECTION_HPP_ diff --git a/include/nbl/ext/Cameras/IRange.hpp b/include/nbl/ext/Cameras/IRange.hpp new file mode 100644 index 0000000000..b4084db2ae --- /dev/null +++ b/include/nbl/ext/Cameras/IRange.hpp @@ -0,0 +1,21 @@ +#ifndef _NBL_IRANGE_HPP_ +#define _NBL_IRANGE_HPP_ + +namespace nbl::core +{ + +/// @brief Minimal concepts used by camera persistence and tooling helpers. +template +concept GeneralPurposeRange = requires +{ + typename std::ranges::range_value_t; +}; + +template +concept ContiguousGeneralPurposeRangeOf = GeneralPurposeRange && +std::ranges::contiguous_range && +std::same_as, T>; + +} // namespace nbl::core + +#endif // _NBL_IRANGE_HPP_ diff --git a/include/nbl/ext/Cameras/README.md b/include/nbl/ext/Cameras/README.md new file mode 100644 index 0000000000..ec4a50a3cd --- /dev/null +++ b/include/nbl/ext/Cameras/README.md @@ -0,0 +1,841 @@ +# Shared Camera API + +This directory contains the reusable camera stack used by [`61_UI`](../../../../examples_tests/61_UI/README.md). + +The stack has two public faces: + +- a runtime face used to move cameras during one frame +- a typed face used to capture, store, restore, compare, replay, and validate camera state + +The runtime face is centered on [`ICamera.hpp`](ICamera.hpp). +The typed face is centered on [`CCameraGoal.hpp`](CCameraGoal.hpp) and [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp). + +## TL;DR + +If you want to know which type to touch first, use this table. + +| I want to... | Use | +|---|---| +| apply one absolute rigid pose request at runtime | `camera->manipulate({}, &referenceFrame)` | +| set exact position or exact orientation on `Free` and `FPS` | `referenceFrame` built from `camera->getGimbal()` | +| set one absolute typed state that can be reused later | `CCameraGoal` + `CCameraGoalSolver` | +| move a camera from live input this frame | `ICamera::manipulate(...)` | +| convert keyboard or mouse input into camera commands | `IGimbalInputProcessor` or `CGimbalInputBinder` | +| capture current camera state | `CCameraGoalSolver::capture...` | +| restore a camera from typed state | `CCameraGoalSolver::apply...` | +| save a named camera state | `CCameraPreset` | +| store camera states over time | `CCameraKeyframeTrack` | +| keep playback cursor state | `CCameraPlaybackTimeline` | +| make a camera follow a moving target | `CCameraFollowUtilities` | +| author compact scripted camera sequences | `CCameraSequenceScript` | +| execute frame-by-frame scripted payloads | `CCameraScriptedRuntime` | +| use the path-rig camera | `CPathCamera` and `SCameraPathModel` | + +## Quick start + +This section shows the common entry points before any deeper explanation. + +### 1. Apply one absolute rigid pose request + +Use this when you already have one rigid transform and want the camera to consume it through the normal runtime entry point. + +```cpp +const auto referenceFrame = + hlsl::CCameraMathUtilities::composeTransformMatrix(desiredPosition, desiredOrientation); + +camera->manipulate({}, &referenceFrame); +``` + +#### Why not just expose `setPosition(...)` and `setOrientation(...)` everywhere? + +Because not every camera kind stores arbitrary rigid pose as its native state. + +`Free` can represent arbitrary position and orientation directly. + +`FPS` cannot. Its legal runtime state is: + +- world-space position +- yaw +- pitch +- upright orientation reconstructed from yaw and pitch + +Consider this `FPS` example: + +```cpp +const auto desiredPosition = hlsl::float64_t3(2.0, 1.0, -3.0); +const auto desiredOrientation = + hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( + hlsl::float64_t3(-15.0, 40.0, 25.0)); +``` + +The requested rigid pose contains `roll = 25 deg`. + +That roll is not legal for `FPS`. + +If the API exposed unrestricted `setOrientation(...)` and accepted that quaternion as-is, the runtime camera would no longer match the rules of the `FPS` rig. + +The current API does this instead: + +1. accept one rigid pose request through `referenceFrame` +2. project that pose onto the legal state space of the concrete camera kind +3. rebuild the final runtime pose from that legal state + +For `FPS` that means: + +- keep the requested position +- read forward direction from the rigid reference +- rebuild legal `pitch/yaw` +- reject arbitrary roll +- write back one upright `FPS` pose + +The same pattern applies to every camera family: + +- `Free` keeps the rigid pose directly +- `FPS` legalizes to upright `position + pitch/yaw` +- target-relative cameras legalize to `target + orbitUv + distance` +- `Path Rig` legalizes to `PathState` + +That is why `camera->manipulate({}, &referenceFrame)` is the shared absolute runtime path. + +It accepts one rigid pose request at the API boundary and lets each camera family legalize it according to its own runtime model. + +Use this path for: + +- one-shot runtime pose application +- ImGuizmo +- world-space or local-space pose anchoring + +### 2. Set exact position or exact orientation on `Free` and `FPS` + +Use this when the target camera is `Free` or `FPS` and you want to replace only one rigid-pose component. + +```cpp +const auto& gimbal = camera->getGimbal(); + +const auto newPosition = desiredPosition; +const auto keepOrientation = gimbal.getOrientation(); + +const auto referenceFrame = + hlsl::CCameraMathUtilities::composeTransformMatrix(newPosition, keepOrientation); + +camera->manipulate({}, &referenceFrame); +``` + +```cpp +const auto& gimbal = camera->getGimbal(); + +const auto keepPosition = gimbal.getPosition(); +const auto newOrientation = desiredOrientation; + +const auto referenceFrame = + hlsl::CCameraMathUtilities::composeTransformMatrix(keepPosition, newOrientation); + +camera->manipulate({}, &referenceFrame); +``` + +`Free` applies these requests exactly. + +`FPS` keeps the exact position but legalizes orientation to its upright `pitch/yaw` state. + +Do not describe this path as exact position-only or exact orientation-only for constrained target-relative or path cameras. Those cameras legalize the rigid pose request into their own family state. + +### 3. Set one absolute typed state + +Use this when the state should survive beyond one frame or should be reused by presets, follow, playback, persistence, or scripts. + +```cpp +core::CCameraGoal goal = {}; +goal.position = desiredPosition; +goal.orientation = desiredOrientation; + +core::CCameraGoalSolver solver; +auto apply = solver.applyDetailed(camera.get(), goal); +``` + +Rule of thumb: + +- use `referenceFrame` for one runtime rigid pose request now +- use `CCameraGoal` for one typed camera state that should be stored, compared, serialized, replayed, or applied later + +### 4. Set one absolute camera-family state + +Use this when you do not want a generic rigid pose and instead want to write the native state of one camera family. + +Target-relative cameras: + +```cpp +camera->trySetSphericalTarget(targetPosition); +camera->trySetSphericalDistance(distance); +``` + +Path camera: + +```cpp +core::ICamera::PathState path = { + .s = desiredS, + .u = desiredU, + .v = desiredV, + .roll = desiredRoll +}; + +camera->trySetPathState(path); +``` + +Use this path when you already have: + +- target-relative state +- path-rig state +- one other family-specific typed fragment exposed by `ICamera` + +### 5. Live runtime camera control + +Use this when keyboard, mouse, or ImGuizmo should move the camera right now. + +```cpp +auto camera = core::make_smart_refctd_ptr(eye, target); + +ui::CGimbalInputBinder binder; +ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); + +auto collected = binder.collectVirtualEvents(timestamp, { + .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, + .keyboardEvents = { keyEvents.data(), keyEvents.size() } +}); + +camera->manipulate(collected.events); +``` + +What happens here: + +1. device input is converted into semantic camera commands +2. the camera consumes those commands through `manipulate(...)` +3. the camera updates its gimbal pose + +Main types involved: + +- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) +- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) +- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) +- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) +- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) +- [`ICamera.hpp`](ICamera.hpp) + +The controller-side stack is: + +- `IGimbalBindingLayout` for the static mapping from device inputs to virtual events +- `IGimbalInputProcessor` for converting one frame of raw input into event magnitudes +- `CGimbalInputBinder` for the common runtime object that owns a layout and collects one frame of events +- `CCameraInputBindingUtilities` for shared preset layouts such as default `FPS`, `Orbit`, or `Path Rig` bindings + +#### How do I bind `FPS` to `WASD`? + +Use the shared default binding preset for the active camera kind. + +```cpp +auto camera = core::make_smart_refctd_ptr(position, orientation); + +ui::CGimbalInputBinder binder; +ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); +``` + +For `FPS`, the default preset gives you: + +- keyboard `W/S/A/D` -> forward, backward, left, right +- keyboard `I/K/J/L` -> tilt up, tilt down, pan left, pan right +- mouse relative movement -> look yaw and pitch + +For `Free`, the default preset adds `Q/E` for roll. + +For target-relative families and `Path Rig`, the default preset keeps the same physical inputs but maps them to the legal state space of that family. + +#### How do I make my own bindings? + +Use one `IGimbalBindingLayout` implementation such as `CGimbalInputBinder` and write the mapping you want. + +```cpp +ui::CGimbalInputBinder binder; +const double customMoveGain = /* choose a sensitivity for this binding */; + +binder.updateKeyboardMapping([customMoveGain](auto& map) +{ + map.clear(); + map.emplace(ui::E_KEY_CODE::EKC_W, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveForward, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_S, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveBackward, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_A, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveLeft, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_D, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveRight, customMoveGain)); +}); +``` + +The same pattern works for: + +- mouse bindings through `updateMouseMapping(...)` +- ImGuizmo bindings through `updateImguizmoMapping(...)` + +#### How are `magnitude` values generated? + +`CVirtualGimbalEvent::magnitude` is one non-negative scalar attached to one semantic command. + +It is not a raw device unit and it is not, by itself, the final world-space or angular motion applied by a camera. + +What stays stable at the API level is the meaning by event family: + +- translation events carry one controller-side translation amount +- rotation events carry one controller-side angular amount +- scale events carry one controller-side scale amount + +The binding layer maps raw producer values onto those amounts. Different sources may start from: + +- elapsed time for held input +- cursor deltas for relative mouse input +- scroll steps for wheel input +- world-space translation or angular deltas for gizmo-driven input + +That means exact numeric gains are binding policy, not API contract. The binding layer owns sensitivity and repeat-rate tuning. + +After the controller side emits virtual magnitudes, the camera runtime applies its own motion scales and legalizes the result to the concrete camera family. + +The motion pipeline is therefore: + +1. raw device input +2. binding-local gain +3. `CVirtualGimbalEvent { type, magnitude }` +4. camera-local motion scale +5. family-specific legalization and state update + +### 6. Capture a camera and restore it later + +Use this when you want explicit camera state instead of one-frame runtime input. + +```cpp +core::CCameraGoalSolver solver; + +auto capture = solver.captureDetailed(camera.get()); +if (capture.canUseGoal()) +{ + auto apply = solver.applyDetailed(camera.get(), capture.goal); +} +``` + +What happens here: + +1. the solver reads runtime camera state +2. the solver writes that state into one `CCameraGoal` +3. the solver later applies that goal back to a camera + +Main types involved: + +- [`CCameraGoal.hpp`](CCameraGoal.hpp) +- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) + +### 7. Save a named camera state + +Use this when one camera state needs a user-facing name or identifier. + +```cpp +core::CCameraGoalSolver solver; + +auto capture = solver.captureDetailed(camera.get()); +if (capture.canUseGoal()) +{ + core::CCameraPreset preset; + preset.name = "Overview"; + preset.identifier = "overview"; + core::CCameraPresetUtilities::assignGoalToPreset(preset, capture.goal); +} +``` + +Main types involved: + +- [`CCameraPreset.hpp`](CCameraPreset.hpp) +- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) + +### 8. Make a camera follow a moving target + +Use this when one tracked subject should drive camera behavior. + +```cpp +core::CTrackedTarget trackedTarget(position, orientation); + +core::SCameraFollowConfig follow = {}; +follow.enabled = true; +follow.mode = core::ECameraFollowMode::LookAtTarget; + +core::CCameraGoalSolver solver; +core::CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); +``` + +Main types involved: + +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) + +### 9. Build scripted runtime payloads from compact authored data + +Use this when camera playback is authored as sequence data and then expanded into per-frame runtime actions and checks. + +```cpp +system::CCameraScriptedTimeline timeline; + +system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( + timeline, + baseFrame, + compiledSegment, + buildInfo); + +system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); +``` + +Main types involved: + +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) +- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) +- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) + +## Core concepts + +### `CVirtualGimbalEvent` + +File: + +- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) + +`CVirtualGimbalEvent` is one semantic camera command plus one scalar magnitude. + +The scalar magnitude is a controller-side virtual amount emitted after binding +gains are applied. It is not a raw device delta and it is not, by itself, the +final world-space motion applied by a camera. + +Examples: + +- `MoveForward` +- `MoveLeft` +- `MoveUp` +- `TiltUp` +- `PanRight` +- `RollLeft` +- `ScaleZInc` + +The event does not store device-specific origin. +The same event type can come from keyboard input, mouse input, ImGuizmo, scripted playback, or replay helpers. + +### `IGimbal` + +Files: + +- [`IGimbal.hpp`](IGimbal.hpp) +- [`ICamera.hpp`](ICamera.hpp) + +The gimbal stores runtime pose: + +- position +- orientation +- scale +- orthonormal basis + +It also accumulates one frame of semantic events into a `VirtualImpulse`. + +`ICamera::CGimbal` extends the base gimbal with a cached world-to-view matrix. + +Every runtime camera owns one `CGimbal`. + +### `ICamera` + +File: + +- [`ICamera.hpp`](ICamera.hpp) + +`ICamera` is the shared runtime interface implemented by every camera kind. + +Its main job is: + +- consume one frame of semantic virtual events +- optionally consume one rigid reference frame +- update internal camera state +- update runtime pose in the gimbal + +Important members: + +- `manipulate(...)` +- `getGimbal()` +- `getAllowedVirtualEvents()` +- `getKind()` +- `getCapabilities()` +- typed hooks such as `tryGetSphericalTargetState(...)` and `tryGetPathState(...)` + +Each camera also stores one local motion-scale bundle in `SMotionConfig`. +Those scales are applied after the binding layer emits virtual magnitudes. + +### `referenceFrame` + +Files: + +- [`ICamera.hpp`](ICamera.hpp) +- [`IGimbal.hpp`](IGimbal.hpp) + +`referenceFrame` is the optional rigid transform passed to `ICamera::manipulate(...)`. + +It is the runtime pose anchor for one manipulation step. + +Typical producers: + +- ImGuizmo +- restore helpers +- replay helpers +- code that wants world-space or local-space manipulation anchored to a specific rigid transform + +When you already have one absolute rigid pose, `referenceFrame` is the direct runtime entry point for requesting that pose through the runtime camera path. + +See Quick start sections 1 and 2 for the concrete absolute-pose usage patterns. + +Shared runtime pattern: + +```text +referenceFrame + -> extract rigid reference transform + -> resolve legal state for this camera kind + -> accumulate virtual events + -> apply deltas in that state space + -> rebuild pose +``` + +### `SCameraRigPose` + +File: + +- [`SCameraRigPose.hpp`](SCameraRigPose.hpp) + +`SCameraRigPose` stores only: + +- world-space position +- world-space orientation + +It is the smallest typed pose object reused across the stack. + +### `CCameraGoal` + +File: + +- [`CCameraGoal.hpp`](CCameraGoal.hpp) + +`CCameraGoal` is the canonical typed transport for camera state. + +You can think of it as: + +> one explicit camera-state snapshot used by higher-level tools + +It may contain: + +- pose +- target position +- target-relative distance +- orbit state +- path state +- dynamic perspective state +- source camera metadata + +It is used by: + +- capture +- restore +- preset flow +- playback +- follow +- scripted checks + +When you want to set a camera absolutely in a reusable, serializable, or comparable way, `CCameraGoal` is the main public state object for that job. + +It is not: + +- a live input object +- a replacement for `manipulate(...)` +- a promise that every camera can represent every arbitrary pose exactly + +For constrained cameras, the solver may project the goal onto legal camera-family state before or during apply. + +### `CCameraGoalSolver` + +File: + +- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) + +`CCameraGoalSolver` converts between typed camera state and runtime cameras. + +It captures runtime cameras into `CCameraGoal`, analyzes whether a target camera can represent that goal directly, and applies the result either through typed state or through runtime replay when needed. + +If you want to restore one absolute camera state and you are not sure which family-specific hook to call, use `CCameraGoalSolver`. + +### `CCameraPreset` + +File: + +- [`CCameraPreset.hpp`](CCameraPreset.hpp) + +`CCameraPreset` is a named saved `CCameraGoal`. + +It contains: + +- `name` +- `identifier` +- `goal` + +### `CCameraKeyframeTrack` + +File: + +- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) + +`CCameraKeyframeTrack` is a sequence of time-stamped presets. + +Each keyframe contains: + +- one preset +- one authored time + +### `CCameraPlaybackTimeline` + +File: + +- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) + +`CCameraPlaybackTimeline` stores playback cursor state over time-based camera data. + +It tracks things such as: + +- current time +- direction +- looping +- paused or playing state + +### `CTrackedTarget` + +File: + +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) + +`CTrackedTarget` is the reusable tracked subject used by follow. + +It owns its own gimbal. +It is not a mesh id and not a scene-node handle. + +### `CCameraSequenceScript` + +File: + +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) + +`CCameraSequenceScript` is the compact authored format for camera sequences. + +It stores camera-domain data such as: + +- targeted camera +- projection presentation requests +- camera keyframes +- tracked-target keyframes +- continuity settings +- capture fractions + +It does not store frame-by-frame low-level input. + +### `CCameraScriptedRuntime` + +File: + +- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) + +`CCameraScriptedRuntime` is the expanded executable form used during scripted playback and validation. + +It stores runtime payloads such as: + +- low-level input events +- action events +- per-frame checks +- capture scheduling + +### `Path Rig` + +Files: + +- [`CPathCamera.hpp`](CPathCamera.hpp) +- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) +- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) + +`Path Rig` is the camera family with typed state: + +- `s` +- `u` +- `v` +- `roll` + +Its runtime and typed tooling are driven by `SCameraPathModel`, which defines how path state is resolved, updated, and converted back into camera pose. + +## Camera families + +### Free cameras + +Files: + +- [`CFPSCamera.hpp`](CFPSCamera.hpp) +- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) + +State: + +- world-space position +- orientation or FPS-constrained yaw/pitch orientation + +Typical use: + +- free-fly navigation +- direct pose-driven manipulation + +### Target-relative cameras + +Base: + +- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) + +Derived: + +- [`COrbitCamera.hpp`](COrbitCamera.hpp) +- [`CArcballCamera.hpp`](CArcballCamera.hpp) +- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) +- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) +- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) +- [`CChaseCamera.hpp`](CChaseCamera.hpp) +- [`CDollyCamera.hpp`](CDollyCamera.hpp) +- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) + +Shared state: + +- target position +- `orbitUv` +- distance + +These cameras resolve pose through target-relative state instead of arbitrary free pose. + +### DollyZoom + +File: + +- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) + +This camera adds dynamic perspective state on top of target-relative state. + +Typed dynamic perspective state: + +- `baseFov` +- `referenceDistance` + +### Path Rig + +Files: + +- [`CPathCamera.hpp`](CPathCamera.hpp) +- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) +- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) + +Typed path state: + +- `s` +- `u` +- `v` +- `roll` + +Typed path limits: + +- `minU` +- `minDistance` +- `maxDistance` + +## Typed tooling + +The key typed types are introduced in the `Core concepts` section above. + +This section focuses on how they fit together in one workflow: + +1. `SCameraRigPose` is the smallest typed pose fragment. +2. `CCameraGoal` is the canonical typed state transport built on top of pose and optional family-specific fragments. +3. `CCameraGoalSolver` captures runtime cameras into goals and applies goals back to runtime cameras. +4. `CCameraPreset` gives one goal a stable user-facing identity. +5. `CCameraKeyframeTrack` stores presets over authored time. +6. `CCameraPlaybackTimeline` stores playback cursor state while a track is being evaluated. + +Use this layer when camera state must outlive the current frame or be exchanged between tools. + +## Follow + +Files: + +- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) + +Follow is built from: + +- one tracked target +- one follow mode +- one follow configuration + +Tracked target type: + +- `CTrackedTarget` + +Follow modes: + +- `OrbitTarget` +- `LookAtTarget` +- `KeepWorldOffset` +- `KeepLocalOffset` + +`CCameraFollowUtilities` reads tracked-target pose, builds resulting camera goal state, and applies it through the shared goal solver. + +## Scripting + +### Compact authored format + +Files: + +- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) + +This layer stores authored camera-domain data. + +### Expanded runtime format + +Files: + +- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) +- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) +- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) +- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) + +This layer stores executable per-frame runtime payloads and validation checks. + +Common flow: + +```text +compact authored sequence + -> compile or expand + -> scripted runtime payload + -> execute against runtime camera state +``` + +## Projection and presentation helpers + +Projection layer: + +- [`IProjection.hpp`](IProjection.hpp) +- [`ILinearProjection.hpp`](ILinearProjection.hpp) +- [`IPerspectiveProjection.hpp`](IPerspectiveProjection.hpp) +- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) +- [`CLinearProjection.hpp`](CLinearProjection.hpp) +- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) +- [`CCubeProjection.hpp`](CCubeProjection.hpp) + +Camera-facing presentation helpers: + +- [`CCameraPresentationUtilities.hpp`](CCameraPresentationUtilities.hpp) +- [`CCameraProjectionUtilities.hpp`](CCameraProjectionUtilities.hpp) +- [`CCameraTextUtilities.hpp`](CCameraTextUtilities.hpp) +- [`CCameraViewportOverlayUtilities.hpp`](CCameraViewportOverlayUtilities.hpp) +- [`CCameraControlPanelUiUtilities.hpp`](CCameraControlPanelUiUtilities.hpp) +- [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp) diff --git a/include/nbl/ext/Cameras/SCameraRigPose.hpp b/include/nbl/ext/Cameras/SCameraRigPose.hpp new file mode 100644 index 0000000000..f8f3d7dfed --- /dev/null +++ b/include/nbl/ext/Cameras/SCameraRigPose.hpp @@ -0,0 +1,26 @@ +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#ifndef _S_CAMERA_RIG_POSE_HPP_ +#define _S_CAMERA_RIG_POSE_HPP_ + +#include "CCameraMathUtilities.hpp" + +namespace nbl::core +{ + +/// @brief Canonical camera pose consisting of world-space position and orientation. +/// +/// This type stores only pose data. Higher-level types add target-relative, +/// dynamic-perspective, or path-specific state around it. +struct SCameraRigPose +{ + /// @brief Camera origin in world space. + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + /// @brief Camera orientation in world space expressed as a unit quaternion. + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); +}; + +} // namespace nbl::core + +#endif // _S_CAMERA_RIG_POSE_HPP_ diff --git a/src/nbl/ext/CMakeLists.txt b/src/nbl/ext/CMakeLists.txt index f3b55531c2..43fd1aeb20 100644 --- a/src/nbl/ext/CMakeLists.txt +++ b/src/nbl/ext/CMakeLists.txt @@ -86,6 +86,16 @@ set(NBL_EXT_FULL_SCREEN_TRIANGLE_LIB PARENT_SCOPE ) +add_subdirectory(Cameras) +set(NBL_EXT_CAMERAS_INCLUDE_DIRS + ${NBL_EXT_Cameras_INCLUDE_DIRS} + PARENT_SCOPE +) +set(NBL_EXT_CAMERAS_LIB + ${NBL_EXT_Cameras_LIB} + PARENT_SCOPE +) + propagate_changed_variables_to_parent_scope() NBL_ADJUST_FOLDERS(ext) diff --git a/src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp b/src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp new file mode 100644 index 0000000000..6941205d21 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp @@ -0,0 +1,98 @@ +#ifndef _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ +#define _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ + +#include + +#include "nbl/ext/Cameras/CCameraFileUtilities.hpp" +#include "nbl/ext/Cameras/CCameraGoal.hpp" +#include "nbl/ext/Cameras/CCameraPresetFlow.hpp" +#include "nlohmann/json.hpp" + +namespace nbl::system +{ + +template +inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) +{ + goal = {}; + + if (entry.contains("camera_kind")) + goal.sourceKind = static_cast(entry["camera_kind"].get()); + if (entry.contains("camera_capabilities")) + goal.sourceCapabilities = entry["camera_capabilities"].get(); + if (entry.contains("camera_goal_state_mask")) + goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); + + if (entry.contains("position") && entry["position"].is_array()) + { + const auto values = entry["position"].get>(); + goal.position = hlsl::float64_t3(values[0], values[1], values[2]); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + const auto values = entry["orientation"].get>(); + goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromComponents(values[0], values[1], values[2], values[3]); + } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + const auto values = entry["target_position"].get>(); + goal.targetPosition = hlsl::float64_t3(values[0], values[1], values[2]); + goal.hasTargetPosition = true; + } + if (entry.contains("distance")) + { + goal.distance = entry["distance"].get(); + goal.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + goal.orbitUv.x = entry["orbit_u"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + goal.orbitUv.y = entry["orbit_v"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + goal.orbitDistance = entry["orbit_distance"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("path_s") && entry.contains("path_u") && entry.contains("path_v")) + { + goal.pathState.s = entry["path_s"].get(); + goal.pathState.u = entry["path_u"].get(); + goal.pathState.v = entry["path_v"].get(); + goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; + goal.hasPathState = true; + } + if (entry.contains("dynamic_base_fov")) + { + goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); + goal.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); + goal.hasDynamicPerspectiveState = true; + } +} + +template +inline void deserializePresetJson(const Json& entry, core::CCameraPreset& preset) +{ + preset = {}; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); + + core::CCameraGoal goal = {}; + deserializeGoalJson(entry, goal); + core::CCameraPresetUtilities::assignGoalToPreset(preset, goal); +} + +} // namespace nbl::system + +#endif // _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ diff --git a/src/nbl/ext/Cameras/CCameraPersistence.cpp b/src/nbl/ext/Cameras/CCameraPersistence.cpp new file mode 100644 index 0000000000..20ca9ca1ad --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraPersistence.cpp @@ -0,0 +1,278 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraPersistence.hpp" + +#include +#include + +#include "CCameraJsonPersistenceUtilities.hpp" +#include "nlohmann/json.hpp" + +using json_t = nlohmann::json; + +json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) +{ + json_t json; + json["position"] = { goal.position.x, goal.position.y, goal.position.z }; + json["orientation"] = { + goal.orientation.data.x, + goal.orientation.data.y, + goal.orientation.data.z, + goal.orientation.data.w + }; + json["camera_kind"] = static_cast(goal.sourceKind); + json["camera_capabilities"] = goal.sourceCapabilities; + json["camera_goal_state_mask"] = goal.sourceGoalStateMask; + + if (goal.hasTargetPosition) + json["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; + if (goal.hasDistance) + json["distance"] = goal.distance; + if (goal.hasOrbitState) + { + json["orbit_u"] = goal.orbitUv.x; + json["orbit_v"] = goal.orbitUv.y; + json["orbit_distance"] = goal.orbitDistance; + } + if (goal.hasPathState) + { + json["path_s"] = goal.pathState.s; + json["path_u"] = goal.pathState.u; + json["path_v"] = goal.pathState.v; + json["path_roll"] = goal.pathState.roll; + } + if (goal.hasDynamicPerspectiveState) + { + json["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; + json["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; + } + + return json; +} + +json_t serializePresetJson(const nbl::core::CCameraPreset& preset) +{ + auto json = serializeGoalJson(nbl::core::CCameraPresetUtilities::makeGoalFromPreset(preset)); + json["name"] = preset.name; + json["identifier"] = preset.identifier; + return json; +} + +json_t serializeKeyframeTrackJson(const nbl::core::CCameraKeyframeTrack& track) +{ + json_t root; + root["keyframes"] = json_t::array(); + + for (const auto& keyframe : track.keyframes) + { + auto json = serializePresetJson(keyframe.preset); + json["time"] = keyframe.time; + root["keyframes"].push_back(std::move(json)); + } + + return root; +} + +bool deserializeKeyframeTrackJson(const json_t& root, nbl::core::CCameraKeyframeTrack& track) +{ + if (!root.contains("keyframes") || !root["keyframes"].is_array()) + return false; + + track = {}; + for (const auto& entry : root["keyframes"]) + { + nbl::core::CCameraKeyframe keyframe; + if (entry.contains("time")) + keyframe.time = std::max(0.f, entry["time"].get()); + nbl::system::deserializePresetJson(entry, keyframe.preset); + track.keyframes.emplace_back(std::move(keyframe)); + } + + nbl::core::CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(track); + nbl::core::CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(track); + return true; +} + +json_t serializePresetCollectionJson(std::span presets) +{ + json_t root; + root["presets"] = json_t::array(); + for (const auto& preset : presets) + root["presets"].push_back(serializePresetJson(preset)); + return root; +} + +bool deserializePresetCollectionJson(const json_t& root, std::vector& presets) +{ + if (!root.contains("presets") || !root["presets"].is_array()) + return false; + + std::vector loadedPresets; + loadedPresets.reserve(root["presets"].size()); + for (const auto& entry : root["presets"]) + { + nbl::core::CCameraPreset preset; + nbl::system::deserializePresetJson(entry, preset); + loadedPresets.emplace_back(std::move(preset)); + } + + presets = std::move(loadedPresets); + return true; +} + +namespace nbl::system +{ + +bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, const int indent) +{ + if (!out) + return false; + + out << serializeGoalJson(goal).dump(indent); + return static_cast(out); +} + +bool readGoal(std::istream& in, core::CCameraGoal& goal) +{ + if (!in) + return false; + + json_t root; + in >> root; + nbl::system::deserializeGoalJson(root, goal); + return true; +} + +bool saveGoalToFile(ISystem& system, const path& filePath, const core::CCameraGoal& goal, const int indent) +{ + std::ostringstream out; + if (!writeGoal(out, goal, indent)) + return false; + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); +} + +bool loadGoalFromFile(ISystem& system, const path& filePath, core::CCameraGoal& goal) +{ + std::string text; + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); + return readGoal(in, goal); +} + +bool writePreset(std::ostream& out, const core::CCameraPreset& preset, const int indent) +{ + if (!out) + return false; + + out << serializePresetJson(preset).dump(indent); + return static_cast(out); +} + +bool readPreset(std::istream& in, core::CCameraPreset& preset) +{ + if (!in) + return false; + + json_t root; + in >> root; + nbl::system::deserializePresetJson(root, preset); + return true; +} + +bool savePresetToFile(ISystem& system, const path& filePath, const core::CCameraPreset& preset, const int indent) +{ + std::ostringstream out; + if (!writePreset(out, preset, indent)) + return false; + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); +} + +bool loadPresetFromFile(ISystem& system, const path& filePath, core::CCameraPreset& preset) +{ + std::string text; + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); + return readPreset(in, preset); +} + +bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, const int indent) +{ + if (!out) + return false; + + out << serializeKeyframeTrackJson(track).dump(indent); + return static_cast(out); +} + +bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track) +{ + if (!in) + return false; + + json_t root; + in >> root; + return deserializeKeyframeTrackJson(root, track); +} + +bool saveKeyframeTrackToFile(ISystem& system, const path& filePath, const core::CCameraKeyframeTrack& track, const int indent) +{ + std::ostringstream out; + if (!writeKeyframeTrack(out, track, indent)) + return false; + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); +} + +bool loadKeyframeTrackFromFile(ISystem& system, const path& filePath, core::CCameraKeyframeTrack& track) +{ + std::string text; + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); + return readKeyframeTrack(in, track); +} + +bool writePresetCollection(std::ostream& out, std::span presets, const int indent) +{ + if (!out) + return false; + + out << serializePresetCollectionJson(presets).dump(indent); + return static_cast(out); +} + +bool readPresetCollection(std::istream& in, std::vector& presets) +{ + if (!in) + return false; + + json_t root; + in >> root; + return deserializePresetCollectionJson(root, presets); +} + +bool savePresetCollectionToFile(ISystem& system, const path& filePath, std::span presets, const int indent) +{ + std::ostringstream out; + if (!writePresetCollection(out, presets, indent)) + return false; + return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); +} + +bool loadPresetCollectionFromFile(ISystem& system, const path& filePath, std::vector& presets) +{ + std::string text; + if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + return false; + + std::istringstream in(text); + return readPresetCollection(in, presets); +} + +} // namespace nbl::system diff --git a/src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp b/src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp new file mode 100644 index 0000000000..5f998f2f4e --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp @@ -0,0 +1,1156 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" + +#include +#include +#include +#include +#include + +#include "CCameraJsonPersistenceUtilities.hpp" +#include "nlohmann/json.hpp" + +using json_t = nlohmann::json; + +bool tryParseCaptureFractionJson(const json_t& entry, float& outFraction) +{ + if (entry.is_number()) + { + outFraction = std::clamp(entry.get(), 0.f, 1.f); + return true; + } + + if (!entry.is_string()) + return false; + + const auto tag = entry.get(); + if (tag == "start") + outFraction = 0.f; + else if (tag == "mid" || tag == "middle") + outFraction = 0.5f; + else if (tag == "end") + outFraction = 1.f; + else + return false; + + return true; +} + +template +void readVector3(const json_t& entry, T& outValue) +{ + using scalar_t = std::remove_reference_t; + const auto values = entry.get>(); + outValue = T(values[0], values[1], values[2]); +} + +bool deserializeSequencePresentationsJson(const json_t& root, std::vector& out, std::string* error) +{ + out.clear(); + if (!root.is_array()) + { + if (error) + *error = "Sequence presentations must be an array."; + return false; + } + + for (const auto& entry : root) + { + if (!entry.is_object() || !entry.contains("projection")) + { + if (error) + *error = "Sequence presentation entry missing \"projection\"."; + return false; + } + + nbl::core::CCameraSequencePresentation presentation; + if (!nbl::core::CCameraSequenceScriptUtilities::tryParseProjectionType(entry["projection"].get(), presentation.projection)) + { + if (error) + *error = "Sequence presentation has invalid projection type."; + return false; + } + if (entry.contains("left_handed")) + presentation.leftHanded = entry["left_handed"].get(); + out.emplace_back(presentation); + } + + return true; +} + +bool deserializeSequenceContinuityJson(const json_t& root, nbl::core::CCameraSequenceContinuitySettings& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence continuity settings must be an object."; + return false; + } + + out = {}; + if (root.contains("baseline")) + out.baseline = root["baseline"].get(); + if (root.contains("step")) + out.step = root["step"].get(); + + if (root.contains("min_pos_delta")) + { + out.minPosDelta = root["min_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + if (root.contains("max_pos_delta")) + { + out.maxPosDelta = root["max_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + else if (root.contains("pos_tolerance")) + { + out.maxPosDelta = root["pos_tolerance"].get(); + out.hasPosDeltaConstraint = true; + } + + if (root.contains("min_euler_delta_deg")) + { + out.minEulerDeltaDeg = root["min_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + if (root.contains("max_euler_delta_deg")) + { + out.maxEulerDeltaDeg = root["max_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + else if (root.contains("euler_tolerance_deg")) + { + out.maxEulerDeltaDeg = root["euler_tolerance_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + + if (root.contains("disable_pos_delta")) + out.hasPosDeltaConstraint = !root["disable_pos_delta"].get(); + if (root.contains("disable_euler_delta")) + out.hasEulerDeltaConstraint = !root["disable_euler_delta"].get(); + + if (out.step && !(out.hasPosDeltaConstraint || out.hasEulerDeltaConstraint)) + { + if (error) + *error = "Sequence continuity step checks require at least one delta constraint."; + return false; + } + + return true; +} + +bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequenceGoalDelta& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe delta must be an object."; + return false; + } + + out = {}; + if (root.contains("position_offset")) + { + readVector3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + if (root.contains("target_offset")) + { + readVector3(root["target_offset"], out.targetOffset); + out.hasTargetOffset = true; + } + if (root.contains("orbit_u_delta_deg")) + { + out.orbitDelta.setUDeltaDeg(root["orbit_u_delta_deg"].get()); + } + if (root.contains("orbit_v_delta_deg")) + { + out.orbitDelta.setVDeltaDeg(root["orbit_v_delta_deg"].get()); + } + if (root.contains("orbit_distance_delta")) + { + out.orbitDelta.setDistanceDelta(root["orbit_distance_delta"].get()); + } + if (root.contains("path_s_delta_deg")) + { + out.pathDelta.setSDeltaDeg(root["path_s_delta_deg"].get()); + } + if (root.contains("path_u_delta")) + { + out.pathDelta.setUDelta(root["path_u_delta"].get()); + } + if (root.contains("path_v_delta")) + { + out.pathDelta.setVDelta(root["path_v_delta"].get()); + } + if (root.contains("path_roll_delta_deg")) + { + out.pathDelta.setRollDeltaDeg(root["path_roll_delta_deg"].get()); + } + if (root.contains("dynamic_base_fov_delta")) + { + out.dynamicBaseFovDelta = root["dynamic_base_fov_delta"].get(); + out.hasDynamicBaseFovDelta = true; + } + if (root.contains("dynamic_reference_distance_delta")) + { + out.dynamicReferenceDistanceDelta = root["dynamic_reference_distance_delta"].get(); + out.hasDynamicReferenceDistanceDelta = true; + } + + return true; +} + +bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSequenceKeyframe& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceGoalDeltaJson(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("preset")) + { + nbl::system::deserializePresetJson(root["preset"], out.absolutePreset); + out.hasAbsolutePreset = true; + } + else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || + root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || + root.contains("orbit_distance") || root.contains("path_s") || root.contains("path_u") || + root.contains("path_v") || root.contains("path_roll") || + root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) + { + nbl::system::deserializePresetJson(root, out.absolutePreset); + out.hasAbsolutePreset = true; + } + + return true; +} + +bool deserializeSequenceTrackedTargetDeltaJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetDelta& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target delta must be an object."; + return false; + } + + out = {}; + if (root.contains("position_offset")) + { + readVector3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + + return true; +} + +bool deserializeSequenceTrackedTargetKeyframeJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetKeyframe& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceTrackedTargetDeltaJson(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("position")) + { + readVector3(root["position"], out.absolutePosition); + out.hasAbsolutePosition = true; + } + if (root.contains("rotation_euler_deg")) + { + readVector3(root["rotation_euler_deg"], out.absoluteRotationEulerDeg); + out.hasAbsoluteRotationEulerDeg = true; + } + + return true; +} + +bool deserializeSequenceSegmentJson(const json_t& root, nbl::core::CCameraSequenceSegment& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence segment must be an object."; + return false; + } + + out = {}; + if (root.contains("name")) + out.name = root["name"].get(); + if (root.contains("camera_identifier")) + out.cameraIdentifier = root["camera_identifier"].get(); + if (root.contains("camera_kind")) + { + if (!nbl::core::CCameraSequenceScriptUtilities::tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) + { + if (error) + *error = "Sequence segment has invalid camera_kind."; + return false; + } + } + if (root.contains("duration_seconds")) + { + out.durationSeconds = std::max(0.f, root["duration_seconds"].get()); + out.hasDurationSeconds = true; + } + if (root.contains("reset_camera")) + { + out.resetCamera = root["reset_camera"].get(); + out.hasResetCamera = true; + } + if (root.contains("presentations")) + { + if (!deserializeSequencePresentationsJson(root["presentations"], out.presentations, error)) + return false; + } + if (root.contains("continuity")) + { + if (!deserializeSequenceContinuityJson(root["continuity"], out.continuity, error)) + return false; + out.hasContinuity = true; + } + if (root.contains("captures")) + { + if (!root["captures"].is_array()) + { + if (error) + *error = "Sequence segment captures must be an array."; + return false; + } + + out.captureFractions.clear(); + for (const auto& entry : root["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFractionJson(entry, fraction)) + { + if (error) + *error = "Sequence segment capture entry is invalid."; + return false; + } + out.captureFractions.emplace_back(fraction); + } + nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.captureFractions); + out.hasCaptureFractions = true; + } + if (root.contains("keyframes")) + { + if (!root["keyframes"].is_array()) + { + if (error) + *error = "Sequence segment keyframes must be an array."; + return false; + } + for (const auto& entry : root["keyframes"]) + { + nbl::core::CCameraSequenceKeyframe keyframe; + if (!deserializeSequenceKeyframeJson(entry, keyframe, error)) + return false; + out.keyframes.emplace_back(std::move(keyframe)); + } + } + if (root.contains("target_keyframes")) + { + if (!root["target_keyframes"].is_array()) + { + if (error) + *error = "Sequence segment target_keyframes must be an array."; + return false; + } + for (const auto& entry : root["target_keyframes"]) + { + nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; + if (!deserializeSequenceTrackedTargetKeyframeJson(entry, keyframe, error)) + return false; + out.targetKeyframes.emplace_back(std::move(keyframe)); + } + } + + if (out.keyframes.empty()) + { + if (error) + *error = "Sequence segment requires at least one keyframe."; + return false; + } + if (out.cameraKind == nbl::core::ICamera::CameraKind::Unknown && out.cameraIdentifier.empty()) + { + if (error) + *error = "Sequence segment requires camera_kind or camera_identifier."; + return false; + } + + return true; +} + +bool deserializeCameraSequenceScriptJson(const json_t& root, nbl::core::CCameraSequenceScript& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Camera sequence script must be an object."; + return false; + } + + out = {}; + if (root.contains("enabled")) + out.enabled = root["enabled"].get(); + if (root.contains("log")) + out.log = root["log"].get(); + if (root.contains("exclusive")) + out.exclusive = root["exclusive"].get(); + if (root.contains("exclusive_input")) + out.exclusive = root["exclusive_input"].get() || out.exclusive; + if (root.contains("hard_fail")) + out.hardFail = root["hard_fail"].get(); + if (root.contains("visual_debug")) + out.visualDebug = root["visual_debug"].get(); + if (root.contains("visual_debug_target_fps")) + out.visualDebugTargetFps = root["visual_debug_target_fps"].get(); + if (root.contains("visual_debug_hold_seconds")) + out.visualDebugHoldSeconds = root["visual_debug_hold_seconds"].get(); + if (root.contains("enableActiveCameraMovement")) + { + out.enableActiveCameraMovement = root["enableActiveCameraMovement"].get(); + out.hasEnableActiveCameraMovement = true; + } + if (root.contains("capture_prefix")) + out.capturePrefix = root["capture_prefix"].get(); + if (root.contains("fps")) + out.fps = std::max(1.f, root["fps"].get()); + + if (root.contains("defaults")) + { + const auto& defaults = root["defaults"]; + if (!defaults.is_object()) + { + if (error) + *error = "Camera sequence defaults must be an object."; + return false; + } + + if (defaults.contains("duration_seconds")) + out.defaults.durationSeconds = std::max(0.f, defaults["duration_seconds"].get()); + if (defaults.contains("reset_camera")) + out.defaults.resetCamera = defaults["reset_camera"].get(); + if (defaults.contains("presentations")) + { + if (!deserializeSequencePresentationsJson(defaults["presentations"], out.defaults.presentations, error)) + return false; + } + if (defaults.contains("continuity")) + { + if (!deserializeSequenceContinuityJson(defaults["continuity"], out.defaults.continuity, error)) + return false; + } + if (defaults.contains("captures")) + { + if (!defaults["captures"].is_array()) + { + if (error) + *error = "Camera sequence default captures must be an array."; + return false; + } + + out.defaults.captureFractions.clear(); + for (const auto& entry : defaults["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFractionJson(entry, fraction)) + { + if (error) + *error = "Camera sequence default capture entry is invalid."; + return false; + } + out.defaults.captureFractions.emplace_back(fraction); + } + nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.defaults.captureFractions); + } + } + + if (!root.contains("segments") || !root["segments"].is_array()) + { + if (error) + *error = "Camera sequence script requires a \"segments\" array."; + return false; + } + + for (const auto& entry : root["segments"]) + { + nbl::core::CCameraSequenceSegment segment; + if (!deserializeSequenceSegmentJson(entry, segment, error)) + return false; + out.segments.emplace_back(std::move(segment)); + } + + if (out.segments.empty()) + { + if (error) + *error = "Camera sequence script must contain at least one segment."; + return false; + } + + return true; +} + +nbl::hlsl::float32_t4x4 composeScriptedImguizmoTransform( + const std::array& translation, + const std::array& rotationDeg, + const std::array& scale) +{ + return nbl::hlsl::CCameraMathUtilities::composeTransformMatrix( + nbl::hlsl::float32_t3(translation[0], translation[1], translation[2]), + nbl::hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegrees(nbl::hlsl::float32_t3(rotationDeg[0], rotationDeg[1], rotationDeg[2])), + nbl::hlsl::float32_t3(scale[0], scale[1], scale[2])); +} + +nbl::hlsl::float32_t4x4 makeScriptedMatrixFromArray(const std::array& values) +{ + nbl::hlsl::float32_t4x4 out(1.f); + for (uint32_t column = 0u; column < 4u; ++column) + { + for (uint32_t row = 0u; row < 4u; ++row) + out[column][row] = values[column * 4u + row]; + } + return out; +} + +std::optional parseScriptedKeyboardAction(std::string_view action) +{ + if (action == "pressed" || action == "press") + return nbl::system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed; + if (action == "released" || action == "release") + return nbl::system::CCameraScriptedInputEvent::KeyboardData::Action::Released; + return std::nullopt; +} + +nbl::ui::E_KEY_CODE parseScriptedKeyCode(std::string_view key) +{ + auto parsed = nbl::ui::stringToKeyCode(key); + if (parsed != nbl::ui::EKC_NONE) + return parsed; + + constexpr std::string_view KeyPrefix = "KEY_"; + constexpr std::string_view EkcPrefix = "EKC_"; + if (key.starts_with(KeyPrefix)) + parsed = nbl::ui::stringToKeyCode(key.substr(KeyPrefix.size())); + if (parsed == nbl::ui::EKC_NONE && key.starts_with(EkcPrefix)) + parsed = nbl::ui::stringToKeyCode(key.substr(EkcPrefix.size())); + return parsed; +} + +std::optional parseScriptedMouseButton(std::string_view button) +{ + if (button == "LEFT_BUTTON") + return nbl::ui::EMB_LEFT_BUTTON; + if (button == "RIGHT_BUTTON") + return nbl::ui::EMB_RIGHT_BUTTON; + if (button == "MIDDLE_BUTTON") + return nbl::ui::EMB_MIDDLE_BUTTON; + if (button == "BUTTON_4") + return nbl::ui::EMB_BUTTON_4; + if (button == "BUTTON_5") + return nbl::ui::EMB_BUTTON_5; + return std::nullopt; +} + +std::optional parseScriptedMouseClickAction(std::string_view action) +{ + if (action == "pressed" || action == "press") + return nbl::system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed; + if (action == "released" || action == "release") + return nbl::system::CCameraScriptedInputEvent::MouseData::ClickAction::Released; + return std::nullopt; +} + +void parseScriptedCaptureFramesJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("capture_frames")) + return; + + for (const auto& frame : script["capture_frames"]) + out.timeline.captureFrames.emplace_back(frame.get()); +} + +void parseScriptedControlOverridesJson(const json_t& controls, nbl::system::CCameraScriptedControlOverrides& out) +{ + if (controls.contains("keyboard_scale")) + { + out.hasKeyboardScale = true; + out.keyboardScale = controls["keyboard_scale"].get(); + } + if (controls.contains("mouse_move_scale")) + { + out.hasMouseMoveScale = true; + out.mouseMoveScale = controls["mouse_move_scale"].get(); + } + if (controls.contains("mouse_scroll_scale")) + { + out.hasMouseScrollScale = true; + out.mouseScrollScale = controls["mouse_scroll_scale"].get(); + } + if (controls.contains("translation_scale")) + { + out.hasTranslationScale = true; + out.translationScale = controls["translation_scale"].get(); + } + if (controls.contains("rotation_scale")) + { + out.hasRotationScale = true; + out.rotationScale = controls["rotation_scale"].get(); + } +} + +bool parseScriptedSequenceIfPresentJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out, std::string* error) +{ + if (!script.contains("segments")) + return true; + + nbl::core::CCameraSequenceScript sequence; + if (!deserializeCameraSequenceScriptJson(script, sequence, error)) + return false; + + out.sequence = std::move(sequence); + return true; +} + +void appendScriptedCaptureFrame(nbl::system::CCameraScriptedInputParseResult& out, const uint64_t frame, const bool captureFrame) +{ + if (captureFrame) + out.timeline.captureFrames.emplace_back(frame); +} + +void parseScriptedKeyboardEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("key") || !event.contains("action")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event missing \"key\" or \"action\"."); + return; + } + + const auto keyText = event["key"].get(); + const auto actionText = event["action"].get(); + const auto key = parseScriptedKeyCode(keyText); + if (key == nbl::ui::EKC_NONE) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyText + "\"."); + return; + } + + const auto action = parseScriptedKeyboardAction(actionText); + if (!action.has_value()) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid action \"" + actionText + "\"."); + return; + } + + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Keyboard; + entry.keyboard.key = key; + entry.keyboard.action = action.value(); + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +void parseScriptedMouseEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("kind")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event missing \"kind\"."); + return; + } + + const auto kind = event["kind"].get(); + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Mouse; + + if (kind == "move") + { + entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Movement; + entry.mouse.dx = event.value("dx", 0); + entry.mouse.dy = event.value("dy", 0); + } + else if (kind == "scroll") + { + entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Scroll; + entry.mouse.v = event.value("v", 0); + entry.mouse.h = event.value("h", 0); + } + else if (kind == "click") + { + if (!event.contains("button") || !event.contains("action")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event missing \"button\" or \"action\"."); + return; + } + + const auto buttonText = event["button"].get(); + const auto actionText = event["action"].get(); + const auto button = parseScriptedMouseButton(buttonText); + if (!button.has_value()) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonText + "\"."); + return; + } + + const auto action = parseScriptedMouseClickAction(actionText); + if (!action.has_value()) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid action \"" + actionText + "\"."); + return; + } + + entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Click; + entry.mouse.button = button.value(); + entry.mouse.action = action.value(); + entry.mouse.x = event.value("x", 0); + entry.mouse.y = event.value("y", 0); + } + else + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event has invalid kind \"" + kind + "\"."); + return; + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +void parseScriptedImguizmoEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) +{ + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Imguizmo; + + if (event.contains("delta_trs")) + { + const auto matrix = event["delta_trs"].get>(); + entry.imguizmo = makeScriptedMatrixFromArray(matrix); + } + else + { + const auto translation = event.contains("translation") ? event["translation"].get>() : std::array{0.f, 0.f, 0.f}; + const auto rotation = event.contains("rotation_deg") ? event["rotation_deg"].get>() : std::array{0.f, 0.f, 0.f}; + const auto scale = event.contains("scale") ? event["scale"].get>() : std::array{1.f, 1.f, 1.f}; + entry.imguizmo = composeScriptedImguizmoTransform(translation, rotation, scale); + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +int32_t parseScriptedActionIntValue(const json_t& event) +{ + if (event.contains("value")) + return event["value"].get(); + if (event.contains("index")) + return event["index"].get(); + return 0; +} + +bool parseScriptedProjectionActionValue(const json_t& event, nbl::system::CCameraScriptedInputEvent::ActionData& action, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (event.contains("value") && event["value"].is_string()) + { + const auto valueText = event["value"].get(); + if (valueText == "perspective") + action.value = static_cast(nbl::core::IPlanarProjection::CProjection::Perspective); + else if (valueText == "orthographic") + action.value = static_cast(nbl::core::IPlanarProjection::CProjection::Orthographic); + else + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action projection type has invalid value \"" + valueText + "\"."); + return false; + } + } + else + { + action.value = parseScriptedActionIntValue(event); + } + + return true; +} + +void parseScriptedActionEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("action")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); + return; + } + + const auto actionText = event["action"].get(); + nbl::system::CCameraScriptedInputEvent entry; + entry.frame = frame; + entry.type = nbl::system::CCameraScriptedInputEvent::Type::Action; + + if (actionText == "set_active_render_window") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_active_planar") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_projection_type") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType; + if (!parseScriptedProjectionActionValue(event, entry.action, out)) + return; + } + else if (actionText == "set_projection_index") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex; + entry.action.value = parseScriptedActionIntValue(event); + } + else if (actionText == "set_use_window") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow; + entry.action.value = event.value("value", false) ? 1 : 0; + } + else if (actionText == "set_left_handed") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded; + entry.action.value = event.value("value", false) ? 1 : 0; + } + else if (actionText == "reset_active_camera") + { + entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera; + entry.action.value = 1; + } + else + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionText + "\"."); + return; + } + + out.timeline.events.emplace_back(std::move(entry)); + appendScriptedCaptureFrame(out, frame, captureFrame); +} + +void parseScriptedInputEventJson(const json_t& event, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!event.contains("frame") || !event.contains("type")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event missing \"frame\" or \"type\"."); + return; + } + + const auto frame = event["frame"].get(); + const auto type = event["type"].get(); + const bool captureFrame = event.value("capture", false); + + if (type == "keyboard") + parseScriptedKeyboardEventJson(event, frame, captureFrame, out); + else if (type == "mouse") + parseScriptedMouseEventJson(event, frame, captureFrame, out); + else if (type == "imguizmo") + parseScriptedImguizmoEventJson(event, frame, captureFrame, out); + else if (type == "action") + parseScriptedActionEventJson(event, frame, captureFrame, out); + else + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); +} + +void parseScriptedInputEventsJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("events")) + return; + + for (const auto& event : script["events"]) + parseScriptedInputEventJson(event, out); +} + +bool parseScriptedImguizmoVirtualCheckJson(const json_t& check, nbl::system::CCameraScriptedInputCheck& outCheck, nbl::system::CCameraScriptedInputParseResult& out) +{ + outCheck.kind = nbl::system::CCameraScriptedInputCheck::Kind::ImguizmoVirtual; + outCheck.tolerance = check.value("tolerance", outCheck.tolerance); + + if (!check.contains("events")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); + return false; + } + + for (const auto& expectedEvent : check["events"]) + { + if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event missing \"type\" or \"magnitude\"."); + continue; + } + + const auto typeText = expectedEvent["type"].get(); + const auto type = nbl::core::CVirtualGimbalEvent::stringToVirtualEvent(typeText); + if (type == nbl::core::CVirtualGimbalEvent::None) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event has invalid type \"" + typeText + "\"."); + continue; + } + + nbl::system::CCameraScriptedInputCheck::ExpectedVirtualEvent expected; + expected.type = type; + expected.magnitude = expectedEvent["magnitude"].get(); + outCheck.expectedVirtualEvents.emplace_back(expected); + } + + return true; +} + +bool parseScriptedCheckJson(const json_t& check, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!check.contains("frame") || !check.contains("kind")) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check missing \"frame\" or \"kind\"."); + return false; + } + + const auto frame = check["frame"].get(); + const auto kind = check["kind"].get(); + + nbl::system::CCameraScriptedInputCheck entry; + entry.frame = frame; + + if (kind == "baseline") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::Baseline; + } + else if (kind == "imguizmo_virtual") + { + if (!parseScriptedImguizmoVirtualCheckJson(check, entry, out)) + return false; + } + else if (kind == "gimbal_near") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalNear; + entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); + + if (check.contains("position")) + { + readVector3(check["position"], entry.expectedPos); + entry.hasExpectedPos = true; + } + if (check.contains("euler_deg")) + { + readVector3(check["euler_deg"], entry.expectedEulerDeg); + entry.hasExpectedEuler = true; + } + } + else if (kind == "gimbal_delta") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalDelta; + entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); + entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); + } + else if (kind == "gimbal_step") + { + entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalStep; + + if (check.contains("min_pos_delta")) + { + entry.minPosDelta = check["min_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + if (check.contains("max_pos_delta")) + { + entry.posTolerance = check["max_pos_delta"].get(); + entry.hasPosDeltaConstraint = true; + } + else if (check.contains("pos_tolerance")) + { + entry.posTolerance = check["pos_tolerance"].get(); + entry.hasPosDeltaConstraint = true; + } + + if (check.contains("min_euler_delta_deg")) + { + entry.minEulerDeltaDeg = check["min_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + if (check.contains("max_euler_delta_deg")) + { + entry.eulerToleranceDeg = check["max_euler_delta_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + else if (check.contains("euler_tolerance_deg")) + { + entry.eulerToleranceDeg = check["euler_tolerance_deg"].get(); + entry.hasEulerDeltaConstraint = true; + } + + if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); + return false; + } + } + else + { + nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check has invalid kind \"" + kind + "\"."); + return false; + } + + out.timeline.checks.emplace_back(std::move(entry)); + return true; +} + +void parseScriptedChecksJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) +{ + if (!script.contains("checks")) + return; + + for (const auto& check : script["checks"]) + parseScriptedCheckJson(check, out); +} + +namespace nbl::system +{ + +bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error) +{ + if (!in) + { + if (error) + *error = "Input stream is not readable."; + return false; + } + + json_t root; + in >> root; + return deserializeCameraSequenceScriptJson(root, out, error); +} + +bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error) +{ + std::istringstream stream{std::string(text)}; + return readCameraSequenceScript(stream, out, error); +} + +bool loadCameraSequenceScriptFromFile(ISystem& system, const path& filePath, core::CCameraSequenceScript& out, std::string* error) +{ + std::string text; + if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open camera sequence script file.")) + return false; + + return readCameraSequenceScript(text, out, error); +} + +bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error) +{ + if (!in) + { + if (error) + *error = "Input stream is not readable."; + return false; + } + + json_t script; + in >> script; + + out = {}; + + if (script.contains("enabled")) + out.enabled = script["enabled"].get(); + if (script.contains("log")) + { + out.hasLog = true; + out.log = script["log"].get(); + } + if (script.contains("hard_fail")) + out.hardFail = script["hard_fail"].get(); + if (script.contains("visual_debug")) + out.visualDebug = script["visual_debug"].get(); + if (script.contains("visual_debug_target_fps")) + out.visualTargetFps = script["visual_debug_target_fps"].get(); + if (script.contains("visual_debug_hold_seconds")) + out.visualCameraHoldSeconds = script["visual_debug_hold_seconds"].get(); + if (script.contains("enableActiveCameraMovement")) + { + out.hasEnableActiveCameraMovement = true; + out.enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); + } + if (script.contains("exclusive_input")) + out.exclusive = script["exclusive_input"].get() || out.exclusive; + if (script.contains("exclusive")) + out.exclusive = script["exclusive"].get() || out.exclusive; + if (script.contains("capture_prefix")) + out.capturePrefix = script["capture_prefix"].get(); + if (out.capturePrefix.empty()) + out.capturePrefix = "script"; + + parseScriptedCaptureFramesJson(script, out); + + if (script.contains("camera_controls")) + parseScriptedControlOverridesJson(script["camera_controls"], out.cameraControls); + + if (!parseScriptedSequenceIfPresentJson(script, out, error)) + return false; + + parseScriptedInputEventsJson(script, out); + parseScriptedChecksJson(script, out); + + CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(out.timeline); + return true; +} + +bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error) +{ + std::istringstream stream{std::string(text)}; + return readCameraScriptedInput(stream, out, error); +} + +bool loadCameraScriptedInputFromFile(ISystem& system, const path& filePath, CCameraScriptedInputParseResult& out, std::string* error) +{ + std::string text; + if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open scripted input file.")) + return false; + + return readCameraScriptedInput(text, out, error); +} + +} // namespace nbl::system diff --git a/src/nbl/ext/Cameras/CMakeLists.txt b/src/nbl/ext/Cameras/CMakeLists.txt new file mode 100644 index 0000000000..1184afeeca --- /dev/null +++ b/src/nbl/ext/Cameras/CMakeLists.txt @@ -0,0 +1,33 @@ +include(common) + +file(GLOB NBL_EXT_CAMERAS_HEADERS CONFIGURE_DEPENDS + "${NBL_ROOT_PATH}/include/nbl/ext/Cameras/*.hpp" +) + +set(NBL_EXT_CAMERAS_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraPersistence.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraScriptedRuntimePersistence.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" +) + +set_source_files_properties( + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" + PROPERTIES + HEADER_FILE_ONLY ON +) + +nbl_create_ext_library_project( + Cameras + "${NBL_EXT_CAMERAS_HEADERS}" + "${NBL_EXT_CAMERAS_SOURCES}" + "" + "" + "" +) + +nbl_install_file_spec( + "${NBL_ROOT_PATH}/include/nbl/ext/Cameras/README.md" + "nbl/ext/Cameras" +) + +add_library(Nabla::ext::Cameras ALIAS ${LIB_NAME}) diff --git a/src/nbl/ext/ImGui/CMakeLists.txt b/src/nbl/ext/ImGui/CMakeLists.txt index e46d93b952..8fbf403602 100644 --- a/src/nbl/ext/ImGui/CMakeLists.txt +++ b/src/nbl/ext/ImGui/CMakeLists.txt @@ -21,7 +21,9 @@ nbl_create_ext_library_project( "" ) -target_link_libraries(${LIB_NAME} PUBLIC imtestengine) +target_link_libraries(${LIB_NAME} PUBLIC imtestengine imguizmo) + +add_library(Nabla::ext::ImGUI ALIAS ${LIB_NAME}) # this should be standard for all extensions set(_ARCHIVE_ENTRY_KEY_ "ImGui/builtin/hlsl") # then each one has unique archive key @@ -44,4 +46,4 @@ if(NBL_EMBED_BUILTIN_RESOURCES) ADD_CUSTOM_BUILTIN_RESOURCES(${_BR_TARGET_} RESOURCES_TO_EMBED "${_ARCHIVE_ABSOLUTE_ENTRY_PATH_}" "${_ARCHIVE_ENTRY_KEY_}" "nbl::ext::imgui::builtin" "${_OUTPUT_DIRECTORY_HEADER_}" "${_OUTPUT_DIRECTORY_SOURCE_}") LINK_BUILTIN_RESOURCES_TO_TARGET(${LIB_NAME} ${_BR_TARGET_}) -endif() \ No newline at end of file +endif() From 6afa742fd4e4cd18c96da2ae52a2f33226408851 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 13:30:15 +0200 Subject: [PATCH 143/161] Clean up cameras ext module --- examples_tests | 2 +- .../transformation_matrix_utils.hlsl | 149 --- .../CCameraFollowRegressionUtilities.hpp | 2 +- include/nbl/ext/Cameras/CCameraGoal.hpp | 8 +- include/nbl/ext/Cameras/CCameraGoalSolver.hpp | 47 +- .../CCameraKeyframeTrackPersistence.hpp | 24 +- .../Cameras/CCameraManipulationUtilities.hpp | 77 -- .../nbl/ext/Cameras/CCameraMathUtilities.hpp | 87 +- .../nbl/ext/Cameras/CCameraPersistence.hpp | 24 +- .../ext/Cameras/CCameraPresetPersistence.hpp | 45 +- .../Cameras/CCameraScriptedCheckRunner.hpp | 6 +- .../ext/Cameras/CCameraScriptedRuntime.hpp | 49 +- .../CCameraScriptedRuntimePersistence.hpp | 74 -- .../CCameraScriptedUiInputUtilities.hpp | 12 +- .../nbl/ext/Cameras/CCameraSequenceScript.hpp | 6 +- .../CCameraSequenceScriptPersistence.hpp | 14 +- .../CCameraSequenceScriptedBuilder.hpp | 128 -- .../CCameraTargetRelativeUtilities.hpp | 2 +- .../nbl/ext/Cameras/CCameraTextUtilities.hpp | 18 +- include/nbl/ext/Cameras/CCameraTraits.hpp | 6 +- include/nbl/ext/Cameras/CIsometricCamera.hpp | 2 +- include/nbl/ext/Cameras/ICamera.hpp | 14 +- include/nbl/ext/Cameras/IGimbal.hpp | 1 + include/nbl/ext/Cameras/ILinearProjection.hpp | 4 +- .../ext/Cameras/IPerspectiveProjection.hpp | 2 +- include/nbl/ext/Cameras/IPlanarProjection.hpp | 2 + include/nbl/ext/Cameras/README.md | 21 +- .../CCameraJsonPersistenceUtilities.hpp | 163 +-- src/nbl/ext/Cameras/CCameraPersistence.cpp | 386 +++--- .../CCameraScriptedRuntimePersistence.cpp | 1156 ----------------- .../CCameraSequenceScriptPersistence.cpp | 558 ++++++++ src/nbl/ext/Cameras/CMakeLists.txt | 2 +- 32 files changed, 1056 insertions(+), 2035 deletions(-) delete mode 100644 include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl delete mode 100644 include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp delete mode 100644 include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp delete mode 100644 src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp create mode 100644 src/nbl/ext/Cameras/CCameraSequenceScriptPersistence.cpp diff --git a/examples_tests b/examples_tests index 87f5310134..4a1326f22f 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 87f531013491f4385ac967ff43a7bb61c480f1e8 +Subproject commit 4a1326f22ff0fa19b07e9282b34d99f339f028ad diff --git a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl b/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl deleted file mode 100644 index f4668769be..0000000000 --- a/include/nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl +++ /dev/null @@ -1,149 +0,0 @@ -#ifndef _NBL_BUILTIN_HLSL_TRANSFORMATION_MATRIX_UTILS_INCLUDED_ -#define _NBL_BUILTIN_HLSL_TRANSFORMATION_MATRIX_UTILS_INCLUDED_ - -#include -#include -#include - -namespace nbl -{ -namespace hlsl -{ - -//! Return true when the three basis vectors form an orthonormal basis within `epsilon`. -template -bool isOrthoBase(const T& x, const T& y, const T& z, const E epsilon = 1e-6) -{ - auto isNormalized = [](const auto& v, const auto& epsilon) -> bool - { - return hlsl::abs(hlsl::length(v) - static_cast(1.0)) <= epsilon; - }; - - auto isOrthogonal = [](const auto& a, const auto& b, const auto& epsilon) -> bool - { - return hlsl::abs(hlsl::dot(a, b)) <= epsilon; - }; - - return isNormalized(x, epsilon) && isNormalized(y, epsilon) && isNormalized(z, epsilon) && - isOrthogonal(x, y, epsilon) && isOrthogonal(x, z, epsilon) && isOrthogonal(y, z, epsilon); -} - -template -matrix getMatrix3x4As4x4(const matrix& mat) -{ - matrix output; - for (int i = 0; i < 3; ++i) - output[i] = mat[i]; - output[3] = vector(T(0), T(0), T(0), T(1)); - - return output; -} - -template -matrix getMatrix3x3As4x4(const matrix& mat) -{ - matrix output; - for (int i = 0; i < 3; ++i) - output[i] = vector(mat[i], T(1)); - output[3] = vector(T(0), T(0), T(0), T(1)); - - return output; -} - -template -inline vector getCastedVector(const vector& in) -{ - vector out; - - for (int i = 0; i < N; ++i) - out[i] = (Tout)(in[i]); - - return out; -} - -template -inline matrix getCastedMatrix(const matrix& in) -{ - matrix out; - - for (int i = 0; i < N; ++i) - out[i] = getCastedVector(in[i]); - - return out; -} - -//! multiplies matrices a and b, 3x4 matrices are treated as 4x4 matrices with 4th row set to (0, 0, 0 ,1) -template -inline matrix concatenateBFollowedByA(const matrix& a, const matrix& b) -{ - const auto a4x4 = getMatrix3x4As4x4(a); - const auto b4x4 = getMatrix3x4As4x4(b); - return matrix(mul(a4x4, b4x4)); -} - -template -inline matrix buildCameraLookAtMatrixLH( - const vector& position, - const vector& target, - const vector& upVector) -{ - const vector zaxis = hlsl::normalize(target - position); - const vector xaxis = hlsl::normalize(hlsl::cross(upVector, zaxis)); - const vector yaxis = hlsl::cross(zaxis, xaxis); - - matrix r; - r[0] = vector(xaxis, -hlsl::dot(xaxis, position)); - r[1] = vector(yaxis, -hlsl::dot(yaxis, position)); - r[2] = vector(zaxis, -hlsl::dot(zaxis, position)); - - return r; -} - -template -inline matrix buildCameraLookAtMatrixRH( - const vector& position, - const vector& target, - const vector& upVector) -{ - const vector zaxis = hlsl::normalize(position - target); - const vector xaxis = hlsl::normalize(hlsl::cross(upVector, zaxis)); - const vector yaxis = hlsl::cross(zaxis, xaxis); - - matrix r; - r[0] = vector(xaxis, -hlsl::dot(xaxis, position)); - r[1] = vector(yaxis, -hlsl::dot(yaxis, position)); - r[2] = vector(zaxis, -hlsl::dot(zaxis, position)); - - return r; -} - -//! Replace the current rotation and scale by `quat`, leaving translation unchanged. -template -inline void setRotation(matrix& outMat, NBL_CONST_REF_ARG(math::quaternion) quat) -{ - static_assert(N == 3 || N == 4); - matrix mat = _static_cast>(quat); - - outMat[0] = mat[0]; - - outMat[1] = mat[1]; - - outMat[2] = mat[2]; -} - -//! Replace the current translation, leaving the linear part unchanged. -template -inline void setTranslation(matrix& outMat, NBL_CONST_REF_ARG(vector) translation) -{ - static_assert(N == 3 || N == 4); - - outMat[0].w = translation.x; - outMat[1].w = translation.y; - outMat[2].w = translation.z; -} - - -} -} - -#endif diff --git a/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp b/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp index 7ea74ae844..1ce8d0b432 100644 --- a/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp @@ -113,7 +113,7 @@ struct CCameraFollowRegressionUtilities final const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) { outMetrics = {}; - const hlsl::float32_t3 target = hlsl::getCastedVector(trackedTarget.getGimbal().getPosition()); + const hlsl::float32_t3 target = hlsl::CCameraMathUtilities::castVector(trackedTarget.getGimbal().getPosition()); const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); const auto clip = hlsl::mul(clipProjection, viewSpace); diff --git a/include/nbl/ext/Cameras/CCameraGoal.hpp b/include/nbl/ext/Cameras/CCameraGoal.hpp index bbf1eae3fb..d4121e2ba8 100644 --- a/include/nbl/ext/Cameras/CCameraGoal.hpp +++ b/include/nbl/ext/Cameras/CCameraGoal.hpp @@ -24,9 +24,9 @@ struct CCameraGoal : SCameraRigPose /// @brief Camera kind that originally produced this goal. ICamera::CameraKind sourceKind = ICamera::CameraKind::Unknown; /// @brief Capability mask captured from the source camera. - uint32_t sourceCapabilities = ICamera::None; + ICamera::capability_flags_t sourceCapabilities = ICamera::None; /// @brief Goal-state fragments that were valid on the source camera. - uint32_t sourceGoalStateMask = ICamera::GoalStateNone; + ICamera::goal_state_flags_t sourceGoalStateMask = ICamera::GoalStateNone; /// @brief Whether `targetPosition` is present in this goal. bool hasTargetPosition = false; /// @brief Tracked target position in world space. @@ -56,9 +56,9 @@ struct CCameraGoalUtilities final { public: /// @brief Compute which typed goal-state fragments are required by the current goal payload. - static inline uint32_t getRequiredGoalStateMask(const CCameraGoal& target) + static inline ICamera::goal_state_flags_t getRequiredGoalStateMask(const CCameraGoal& target) { - uint32_t mask = ICamera::GoalStateNone; + ICamera::goal_state_flags_t mask = ICamera::GoalStateNone; if (target.hasTargetPosition || target.hasDistance || target.hasOrbitState) mask |= ICamera::GoalStateSphericalTarget; if (target.hasDynamicPerspectiveState) diff --git a/include/nbl/ext/Cameras/CCameraGoalSolver.hpp b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp index 91dad92191..b5301957cb 100644 --- a/include/nbl/ext/Cameras/CCameraGoalSolver.hpp +++ b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp @@ -10,6 +10,7 @@ #include "CCameraGoal.hpp" #include "CCameraTargetRelativeUtilities.hpp" #include "CCameraVirtualEventUtilities.hpp" +#include "nbl/core/util/bitflag.h" namespace nbl::core { @@ -42,9 +43,9 @@ class CCameraGoalSolver { bool sameKind = false; bool exact = false; - uint32_t requiredGoalStateMask = ICamera::GoalStateNone; - uint32_t supportedGoalStateMask = ICamera::GoalStateNone; - uint32_t missingGoalStateMask = ICamera::GoalStateNone; + ICamera::goal_state_flags_t requiredGoalStateMask = ICamera::GoalStateNone; + ICamera::goal_state_flags_t supportedGoalStateMask = ICamera::GoalStateNone; + ICamera::goal_state_flags_t missingGoalStateMask = ICamera::GoalStateNone; }; /// @brief Outcome of one goal-application attempt. @@ -60,7 +61,7 @@ class CCameraGoalSolver AppliedAbsoluteAndVirtualEvents }; - enum EIssue : uint32_t + enum class EIssue : uint32_t { NoIssue = 0u, UsedAbsolutePoseFallback = 1u << 0, @@ -73,7 +74,7 @@ class CCameraGoalSolver EStatus status = EStatus::Unsupported; bool exact = false; uint32_t eventCount = 0u; - uint32_t issues = NoIssue; + core::bitflag issues = EIssue::NoIssue; inline bool succeeded() const { @@ -94,7 +95,7 @@ class CCameraGoalSolver inline bool hasIssue(EIssue issue) const { - return (issues & issue) == issue; + return issues.hasFlags(issue); } }; @@ -122,8 +123,8 @@ class CCameraGoalSolver out.position = hlsl::float64_t3(gimbal.getPosition()); out.orientation = gimbal.getOrientation(); out.sourceKind = camera->getKind(); - out.sourceCapabilities = camera->getCapabilities(); - out.sourceGoalStateMask = camera->getGoalStateMask(); + out.sourceCapabilities = ICamera::capability_flags_t(camera->getCapabilities()); + out.sourceGoalStateMask = ICamera::goal_state_flags_t(camera->getGoalStateMask()); ICamera::SphericalTargetState sphericalState; if (camera->tryGetSphericalTargetState(sphericalState)) @@ -175,7 +176,7 @@ class CCameraGoalSolver const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); result.sameKind = canonicalTarget.sourceKind == ICamera::CameraKind::Unknown || canonicalTarget.sourceKind == camera->getKind(); - result.supportedGoalStateMask = camera->getGoalStateMask(); + result.supportedGoalStateMask = ICamera::goal_state_flags_t(camera->getGoalStateMask()); result.requiredGoalStateMask = CCameraGoalUtilities::getRequiredGoalStateMask(canonicalTarget); result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; @@ -199,7 +200,7 @@ class CCameraGoalSolver bool poseExact = false; if (tryApplyAbsoluteReferencePose(camera, canonicalTarget, poseChanged, poseExact)) { - result.issues |= SApplyResult::UsedAbsolutePoseFallback; + result.issues |= SApplyResult::EIssue::UsedAbsolutePoseFallback; absoluteChanged = absoluteChanged || poseChanged; if (poseExact && !canonicalTarget.hasDynamicPerspectiveState) { @@ -217,7 +218,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState beforeState; if (!camera->tryGetSphericalTargetState(beforeState)) { - result.issues |= SApplyResult::MissingSphericalTargetState; + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; exact = false; } else @@ -225,7 +226,7 @@ class CCameraGoalSolver const auto beforeTarget = beforeState.target; if (!camera->trySetSphericalTarget(canonicalTarget.targetPosition)) { - result.issues |= SApplyResult::MissingSphericalTargetState; + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; exact = false; } else @@ -233,7 +234,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState afterState; if (!camera->tryGetSphericalTargetState(afterState)) { - result.issues |= SApplyResult::MissingSphericalTargetState; + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; exact = false; } else @@ -250,7 +251,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState beforeState; if (!camera->tryGetSphericalTargetState(beforeState)) { - result.issues |= SApplyResult::MissingSphericalTargetState; + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; exact = false; } else @@ -259,7 +260,7 @@ class CCameraGoalSolver const float beforeDistance = beforeState.distance; if (!camera->trySetSphericalDistance(desiredDistance)) { - result.issues |= SApplyResult::MissingSphericalTargetState; + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; exact = false; } else @@ -267,7 +268,7 @@ class CCameraGoalSolver ICamera::SphericalTargetState afterState; if (!camera->tryGetSphericalTargetState(afterState)) { - result.issues |= SApplyResult::MissingSphericalTargetState; + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; exact = false; } else @@ -284,12 +285,12 @@ class CCameraGoalSolver ICamera::PathState beforeState; if (!camera->tryGetPathState(beforeState)) { - result.issues |= SApplyResult::MissingPathState; + result.issues |= SApplyResult::EIssue::MissingPathState; exact = false; } else if (!camera->trySetPathState(canonicalTarget.pathState)) { - result.issues |= SApplyResult::MissingPathState; + result.issues |= SApplyResult::EIssue::MissingPathState; exact = false; } else @@ -297,7 +298,7 @@ class CCameraGoalSolver ICamera::PathState afterState; if (!camera->tryGetPathState(afterState)) { - result.issues |= SApplyResult::MissingPathState; + result.issues |= SApplyResult::EIssue::MissingPathState; exact = false; } else @@ -317,12 +318,12 @@ class CCameraGoalSolver ICamera::DynamicPerspectiveState beforeState; if (!camera->tryGetDynamicPerspectiveState(beforeState)) { - result.issues |= SApplyResult::MissingDynamicPerspectiveState; + result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; exact = false; } else if (!camera->trySetDynamicPerspectiveState(canonicalTarget.dynamicPerspectiveState)) { - result.issues |= SApplyResult::MissingDynamicPerspectiveState; + result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; exact = false; } else @@ -330,7 +331,7 @@ class CCameraGoalSolver ICamera::DynamicPerspectiveState afterState; if (!camera->tryGetDynamicPerspectiveState(afterState)) { - result.issues |= SApplyResult::MissingDynamicPerspectiveState; + result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; exact = false; } else @@ -375,7 +376,7 @@ class CCameraGoalSolver return result; } - result.issues |= SApplyResult::VirtualEventReplayFailed; + result.issues |= SApplyResult::EIssue::VirtualEventReplayFailed; result.status = SApplyResult::EStatus::Failed; result.exact = false; return result; diff --git a/include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp b/include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp index 7ce8a45cb4..12c57d7a24 100644 --- a/include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp +++ b/include/nbl/ext/Cameras/CCameraKeyframeTrackPersistence.hpp @@ -5,7 +5,8 @@ #ifndef _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ #define _C_CAMERA_KEYFRAME_TRACK_PERSISTENCE_HPP_ -#include +#include +#include #include "CCameraKeyframeTrack.hpp" #include "nbl/system/path.h" @@ -15,15 +16,18 @@ namespace nbl::system class ISystem; -/// @brief Serialize one camera keyframe track into an existing stream. -bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, int indent = 2); -/// @brief Deserialize one camera keyframe track from an existing stream. -bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track); - -/// @brief Save one camera keyframe track to a file. -bool saveKeyframeTrackToFile(ISystem& system, const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); -/// @brief Load one camera keyframe track from a file. -bool loadKeyframeTrackFromFile(ISystem& system, const path& path, core::CCameraKeyframeTrack& track); +struct CCameraKeyframeTrackPersistenceUtilities final +{ + /// @brief Serialize one camera keyframe track to JSON text. + static std::string serializeKeyframeTrack(const core::CCameraKeyframeTrack& track, int indent = 2); + /// @brief Deserialize one camera keyframe track from JSON text. + static bool deserializeKeyframeTrack(std::string_view text, core::CCameraKeyframeTrack& track, std::string* error = nullptr); + + /// @brief Save one camera keyframe track to a file. + static bool saveKeyframeTrackToFile(ISystem& system, const path& path, const core::CCameraKeyframeTrack& track, int indent = 2); + /// @brief Load one camera keyframe track from a file. + static bool loadKeyframeTrackFromFile(ISystem& system, const path& path, core::CCameraKeyframeTrack& track, std::string* error = nullptr); +}; } // namespace nbl::system diff --git a/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp b/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp index 9ca865a0d1..6e8adbaf06 100644 --- a/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp @@ -8,42 +8,11 @@ #include #include -#include "CCameraPresetFlow.hpp" #include "CCameraVirtualEventUtilities.hpp" namespace nbl::core { -struct SCameraConstraintDefaults final -{ - static constexpr float PitchMinDeg = -80.0f; - static constexpr float PitchMaxDeg = 80.0f; - static constexpr float YawMinDeg = -180.0f; - static constexpr float YawMaxDeg = 180.0f; - static constexpr float RollMinDeg = -180.0f; - static constexpr float RollMaxDeg = 180.0f; - static constexpr float MinDistance = SCameraTargetRelativeTraits::MinDistance; - static constexpr float MaxDistance = SCameraTargetRelativeTraits::DefaultMaxDistance; -}; - -/// @brief Reusable constraint settings for post-manipulation camera clamping. -struct SCameraConstraintSettings -{ - bool enabled = false; - bool clampPitch = false; - bool clampYaw = false; - bool clampRoll = false; - bool clampDistance = false; - float pitchMinDeg = SCameraConstraintDefaults::PitchMinDeg; - float pitchMaxDeg = SCameraConstraintDefaults::PitchMaxDeg; - float yawMinDeg = SCameraConstraintDefaults::YawMinDeg; - float yawMaxDeg = SCameraConstraintDefaults::YawMaxDeg; - float rollMinDeg = SCameraConstraintDefaults::RollMinDeg; - float rollMaxDeg = SCameraConstraintDefaults::RollMaxDeg; - float minDistance = SCameraConstraintDefaults::MinDistance; - float maxDistance = SCameraConstraintDefaults::MaxDistance; -}; - struct CCameraManipulationUtilities final { public: @@ -102,52 +71,6 @@ struct CCameraManipulationUtilities final events = std::move(filtered); count = static_cast(events.size()); } - - /// @brief Apply shared distance and Euler-angle constraints after manipulation. - static inline bool applyCameraConstraints(const CCameraGoalSolver& solver, ICamera* camera, const SCameraConstraintSettings& constraints) - { - if (!constraints.enabled || !camera) - return false; - - if (camera->hasCapability(ICamera::SphericalTarget)) - { - if (!constraints.clampDistance) - return false; - - ICamera::SphericalTargetState sphericalState; - if (!camera->tryGetSphericalTargetState(sphericalState)) - return false; - - const float clamped = std::clamp(sphericalState.distance, constraints.minDistance, constraints.maxDistance); - if (clamped == sphericalState.distance) - return false; - - return camera->trySetSphericalDistance(clamped); - } - - if (!(constraints.clampPitch || constraints.clampYaw || constraints.clampRoll)) - return false; - - const auto& gimbal = camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto eulerDeg = hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(gimbal.getOrientation()); - - auto clamped = eulerDeg; - if (constraints.clampPitch) - clamped.x = std::clamp(clamped.x, static_cast(constraints.pitchMinDeg), static_cast(constraints.pitchMaxDeg)); - if (constraints.clampYaw) - clamped.y = std::clamp(clamped.y, static_cast(constraints.yawMinDeg), static_cast(constraints.yawMaxDeg)); - if (constraints.clampRoll) - clamped.z = std::clamp(clamped.z, static_cast(constraints.rollMinDeg), static_cast(constraints.rollMaxDeg)); - - if (clamped.x == eulerDeg.x && clamped.y == eulerDeg.y && clamped.z == eulerDeg.z) - return false; - - CCameraPreset preset; - preset.goal.position = pos; - preset.goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(clamped); - return CCameraPresetFlowUtilities::applyPreset(solver, camera, preset); - } }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp index 3d8e270787..5c704b41da 100644 --- a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp @@ -6,9 +6,10 @@ #include #include +#include "nbl/builtin/hlsl/cpp_compat/matrix.hlsl" #include "nbl/builtin/hlsl/cpp_compat/vector.hlsl" +#include "nbl/builtin/hlsl/math/quaternions.hlsl" #include "nbl/builtin/hlsl/numbers.hlsl" -#include "nbl/builtin/hlsl/matrix_utils/transformation_matrix_utils.hlsl" namespace nbl::hlsl { @@ -41,15 +42,28 @@ struct SCameraPoseDelta struct SCameraViewRigDefaults final { static constexpr double DegreesToRadians = numbers::pi / 180.0; - static constexpr double ArcballPitchLimitDeg = 89.0; + static constexpr double FullTurnDeg = 360.0; + static constexpr double RightAngleDeg = FullTurnDeg / 4.0; + static constexpr double ArcballPitchMarginDeg = 1.0; + static constexpr double DollyPitchMarginDeg = 5.0; + static constexpr double FpsVerticalPitchMarginDeg = 2.0; + // Arcball and turntable pitch stop short of +-90 deg to avoid a singular up axis. + static constexpr double ArcballPitchLimitDeg = RightAngleDeg - ArcballPitchMarginDeg; static constexpr double TurntablePitchLimitDeg = ArcballPitchLimitDeg; + // Chase rigs keep a narrower pitch envelope so the followed subject stays readable. static constexpr double ChaseMaxPitchDeg = 70.0; static constexpr double ChaseMinPitchDeg = -60.0; - static constexpr double DollyPitchLimitDeg = 85.0; - static constexpr double FpsVerticalPitchLimitDeg = 88.0; - static constexpr double TopDownPitchDeg = -90.0; - static constexpr double IsometricYawDeg = 45.0; - static constexpr double IsometricPitchDeg = 35.264389682754654; + // Dolly and FPS rigs also stop short of straight up/down. + static constexpr double DollyPitchLimitDeg = RightAngleDeg - DollyPitchMarginDeg; + static constexpr double FpsVerticalPitchLimitDeg = RightAngleDeg - FpsVerticalPitchMarginDeg; + static constexpr double TopDownPitchDeg = -RightAngleDeg; + // Half of a right angle is the canonical isometric azimuth. + static constexpr double IsometricYawDeg = RightAngleDeg / 2.0; + // tan(theta) = 1 / sqrt(2) for the canonical isometric pitch. + static constexpr double IsometricPitchTangent = 1.0 / numbers::sqrt2; + // atan(1 / sqrt(2)) is the canonical isometric pitch used by the fixed rig. + static inline const double IsometricPitchRad = std::atan(IsometricPitchTangent); + static inline const double IsometricPitchDeg = IsometricPitchRad / DegreesToRadians; static inline constexpr double ArcballPitchLimitRad = ArcballPitchLimitDeg * DegreesToRadians; static inline constexpr double TurntablePitchLimitRad = TurntablePitchLimitDeg * DegreesToRadians; @@ -59,16 +73,53 @@ struct SCameraViewRigDefaults final static inline constexpr double FpsVerticalPitchLimitRad = FpsVerticalPitchLimitDeg * DegreesToRadians; static inline constexpr double TopDownPitchRad = TopDownPitchDeg * DegreesToRadians; static inline constexpr double IsometricYawRad = IsometricYawDeg * DegreesToRadians; - static inline constexpr double IsometricPitchRad = IsometricPitchDeg * DegreesToRadians; }; struct SCameraRigidMathDefaults final { - static constexpr double LookAtParallelThreshold = 0.99; + // Treat vectors as effectively parallel once the normalized dot product stays within 1e-2 of 1. + static constexpr double LookAtParallelThreshold = 1.0 - 1e-2; }; struct CCameraMathUtilities final { + template + static inline vector castVector(const vector& input) + { + vector output; + for (uint32_t i = 0u; i < N; ++i) + output[i] = static_cast(input[i]); + return output; + } + + template + static inline matrix promoteAffine3x4To4x4(const matrix& input) + { + matrix output; + output[0] = input[0]; + output[1] = input[1]; + output[2] = input[2]; + output[3] = vector(T(0), T(0), T(0), T(1)); + return output; + } + + template + static inline bool isOrthoBase(const Vec& x, const Vec& y, const Vec& z, const E epsilon = 1e-6) + { + const auto isNormalized = [&](const auto& v) -> bool + { + return hlsl::abs(hlsl::length(v) - static_cast(1.0)) <= epsilon; + }; + + const auto isOrthogonal = [&](const auto& a, const auto& b) -> bool + { + return hlsl::abs(hlsl::dot(a, b)) <= epsilon; + }; + + return isNormalized(x) && isNormalized(y) && isNormalized(z) && + isOrthogonal(x, y) && isOrthogonal(x, z) && isOrthogonal(y, z); + } + template static inline T wrapAngleRad(T angle) { @@ -280,7 +331,6 @@ struct CCameraMathUtilities final canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); - const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; const auto desiredRight = canonicalRight; const auto desiredUp = canonicalUp; @@ -683,10 +733,8 @@ struct CCameraMathUtilities final const camera_vector_t& up, const camera_vector_t& forward) { - return camera_vector_t( - dot(worldVector, right), - dot(worldVector, up), - dot(worldVector, forward)); + const camera_matrix_t basis { right, up, forward }; + return hlsl::mul(hlsl::transpose(basis), worldVector); } template @@ -696,7 +744,8 @@ struct CCameraMathUtilities final const camera_vector_t& up, const camera_vector_t& forward) { - return right * localVector.x + up * localVector.y + forward * localVector.z; + const camera_matrix_t basis { right, up, forward }; + return hlsl::mul(basis, localVector); } template @@ -796,11 +845,11 @@ struct CCameraMathUtilities final template static inline camera_matrix_t getQuaternionBasisMatrix(const camera_quaternion_t& orientation) { - const auto q = normalizeQuaternion(orientation); + const auto normalizedOrientation = normalizeQuaternion(orientation); return camera_matrix_t( - q.transformVector(camera_vector_t(T(1), T(0), T(0)), true), - q.transformVector(camera_vector_t(T(0), T(1), T(0)), true), - q.transformVector(camera_vector_t(T(0), T(0), T(1)), true)); + normalizedOrientation.transformVector(getCameraWorldRight(), true), + normalizedOrientation.transformVector(getCameraWorldUp(), true), + normalizedOrientation.transformVector(getCameraWorldForward(), true)); } template diff --git a/include/nbl/ext/Cameras/CCameraPersistence.hpp b/include/nbl/ext/Cameras/CCameraPersistence.hpp index a1f2487acf..e5be662303 100644 --- a/include/nbl/ext/Cameras/CCameraPersistence.hpp +++ b/include/nbl/ext/Cameras/CCameraPersistence.hpp @@ -5,7 +5,8 @@ #ifndef _C_CAMERA_PERSISTENCE_HPP_ #define _C_CAMERA_PERSISTENCE_HPP_ -#include +#include +#include #include #include @@ -18,15 +19,18 @@ namespace nbl::system class ISystem; -/// @brief Serialize a preset collection to JSON. -bool writePresetCollection(std::ostream& out, std::span presets, int indent = 2); -/// @brief Parse a preset collection from JSON. -bool readPresetCollection(std::istream& in, std::vector& presets); - -/// @brief Save a preset collection to disk as JSON. -bool savePresetCollectionToFile(ISystem& system, const path& path, std::span presets, int indent = 2); -/// @brief Load a preset collection from disk. -bool loadPresetCollectionFromFile(ISystem& system, const path& path, std::vector& presets); +struct CCameraPersistenceUtilities final +{ + /// @brief Serialize a preset collection to JSON text. + static std::string serializePresetCollection(std::span presets, int indent = 2); + /// @brief Parse a preset collection from JSON text. + static bool deserializePresetCollection(std::string_view text, std::vector& presets, std::string* error = nullptr); + + /// @brief Save a preset collection to disk as JSON. + static bool savePresetCollectionToFile(ISystem& system, const path& path, std::span presets, int indent = 2); + /// @brief Load a preset collection from disk. + static bool loadPresetCollectionFromFile(ISystem& system, const path& path, std::vector& presets, std::string* error = nullptr); +}; } // namespace nbl::system diff --git a/include/nbl/ext/Cameras/CCameraPresetPersistence.hpp b/include/nbl/ext/Cameras/CCameraPresetPersistence.hpp index 298163ab6a..4b00a029f6 100644 --- a/include/nbl/ext/Cameras/CCameraPresetPersistence.hpp +++ b/include/nbl/ext/Cameras/CCameraPresetPersistence.hpp @@ -5,7 +5,8 @@ #ifndef _C_CAMERA_PRESET_PERSISTENCE_HPP_ #define _C_CAMERA_PRESET_PERSISTENCE_HPP_ -#include +#include +#include #include "CCameraPreset.hpp" #include "nbl/system/path.h" @@ -15,25 +16,29 @@ namespace nbl::system class ISystem; -/// @brief Serialize one camera goal into an existing stream. -bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, int indent = 2); -/// @brief Deserialize one camera goal from an existing stream. -bool readGoal(std::istream& in, core::CCameraGoal& goal); - -/// @brief Save one camera goal to a file. -bool saveGoalToFile(ISystem& system, const path& path, const core::CCameraGoal& goal, int indent = 2); -/// @brief Load one camera goal from a file. -bool loadGoalFromFile(ISystem& system, const path& path, core::CCameraGoal& goal); - -/// @brief Serialize one camera preset into an existing stream. -bool writePreset(std::ostream& out, const core::CCameraPreset& preset, int indent = 2); -/// @brief Deserialize one camera preset from an existing stream. -bool readPreset(std::istream& in, core::CCameraPreset& preset); - -/// @brief Save one camera preset to a file. -bool savePresetToFile(ISystem& system, const path& path, const core::CCameraPreset& preset, int indent = 2); -/// @brief Load one camera preset from a file. -bool loadPresetFromFile(ISystem& system, const path& path, core::CCameraPreset& preset); +/// @brief JSON text and file helpers for goals and presets. +struct CCameraPresetPersistenceUtilities final +{ + /// @brief Serialize one camera goal to JSON text. + static std::string serializeGoal(const core::CCameraGoal& goal, int indent = 2); + /// @brief Deserialize one camera goal from JSON text. + static bool deserializeGoal(std::string_view text, core::CCameraGoal& goal, std::string* error = nullptr); + + /// @brief Save one camera goal to a file. + static bool saveGoalToFile(ISystem& system, const path& path, const core::CCameraGoal& goal, int indent = 2); + /// @brief Load one camera goal from a file. + static bool loadGoalFromFile(ISystem& system, const path& path, core::CCameraGoal& goal, std::string* error = nullptr); + + /// @brief Serialize one camera preset to JSON text. + static std::string serializePreset(const core::CCameraPreset& preset, int indent = 2); + /// @brief Deserialize one camera preset from JSON text. + static bool deserializePreset(std::string_view text, core::CCameraPreset& preset, std::string* error = nullptr); + + /// @brief Save one camera preset to a file. + static bool savePresetToFile(ISystem& system, const path& path, const core::CCameraPreset& preset, int indent = 2); + /// @brief Load one camera preset from a file. + static bool loadPresetFromFile(ISystem& system, const path& path, core::CCameraPreset& preset, std::string* error = nullptr); +}; } // namespace nbl::system diff --git a/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp b/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp index a4a23c7ede..026daf1afc 100644 --- a/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp +++ b/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp @@ -151,7 +151,7 @@ struct CCameraScriptedCheckRunnerUtilities final const auto& gimbal = context.camera->getGimbal(); const auto pos = gimbal.getPosition(); const auto orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); - const auto eulerDeg = hlsl::getCastedVector(hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(orientation)); + const auto eulerDeg = hlsl::CCameraMathUtilities::castVector(hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(orientation)); if (!hlsl::CCameraMathUtilities::isFiniteVec3(pos) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(orientation) || !hlsl::CCameraMathUtilities::isFiniteVec3(eulerDeg)) { @@ -243,7 +243,7 @@ struct CCameraScriptedCheckRunnerUtilities final bool ok = true; if (check.hasExpectedPos) { - const double distance = hlsl::length(pos - hlsl::getCastedVector(check.expectedPos)); + const double distance = hlsl::length(pos - hlsl::CCameraMathUtilities::castVector(check.expectedPos)); if (distance > check.posTolerance) { ok = false; @@ -262,7 +262,7 @@ struct CCameraScriptedCheckRunnerUtilities final if (check.hasExpectedEuler) { const auto expectedOrientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( - hlsl::getCastedVector(check.expectedEulerDeg)); + hlsl::CCameraMathUtilities::castVector(check.expectedEulerDeg)); hlsl::SCameraPoseDelta poseDelta = {}; if (!scriptedCheckComputePoseDelta(pos, orientation, pos, expectedOrientation, poseDelta)) poseDelta.rotationDeg = std::numeric_limits::infinity(); diff --git a/include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp b/include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp index 99d8c34f03..3de6d28f1d 100644 --- a/include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp +++ b/include/nbl/ext/Cameras/CCameraScriptedRuntime.hpp @@ -29,7 +29,6 @@ struct CCameraScriptedInputEvent Keyboard, Mouse, Imguizmo, - Action, Goal, TrackedTargetTransform, SegmentLabel @@ -68,29 +67,9 @@ struct CCameraScriptedInputEvent Type type = Type::Uninitialized; ui::E_MOUSE_BUTTON button = ui::EMB_LEFT_BUTTON; ClickAction action = ClickAction::Uninitialized; - int16_t x = 0; - int16_t y = 0; - int16_t dx = 0; - int16_t dy = 0; - int16_t v = 0; - int16_t h = 0; - }; - - struct ActionData - { - enum class Kind : uint8_t - { - SetActiveRenderWindow, - SetActivePlanar, - SetProjectionType, - SetProjectionIndex, - SetUseWindow, - SetLeftHanded, - ResetActiveCamera - }; - - Kind kind = Kind::SetActiveRenderWindow; - int32_t value = 0; + hlsl::int16_t2 position = hlsl::int16_t2(0); + hlsl::int16_t2 delta = hlsl::int16_t2(0); + hlsl::int16_t2 scroll = hlsl::int16_t2(0); }; struct GoalData @@ -114,7 +93,6 @@ struct CCameraScriptedInputEvent KeyboardData keyboard; MouseData mouse; hlsl::float32_t4x4 imguizmo = hlsl::float32_t4x4(1.f); - ActionData action; GoalData goal; TrackedTargetTransformData trackedTargetTransform; SegmentLabelData segmentLabel; @@ -209,20 +187,6 @@ struct CCameraScriptedRuntimeUtilities final finalizeScriptedTimeline(timeline.events, timeline.checks, timeline.captureFrames, disableCaptureFrames); } - static inline void appendScriptedActionEvent( - CCameraScriptedTimeline& timeline, - const uint64_t frame, - const CCameraScriptedInputEvent::ActionData::Kind kind, - const int32_t value) - { - CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = CCameraScriptedInputEvent::Type::Action; - entry.action.kind = kind; - entry.action.value = value; - timeline.events.emplace_back(std::move(entry)); - } - static inline void appendScriptedGoalEvent( CCameraScriptedTimeline& timeline, const uint64_t frame, @@ -321,7 +285,6 @@ struct CCameraScriptedFrameEvents std::vector keyboard; std::vector mouse; std::vector imguizmo; - std::vector actions; std::vector goals; std::vector trackedTargetTransforms; std::vector segmentLabels; @@ -331,7 +294,6 @@ struct CCameraScriptedFrameEvents keyboard.clear(); mouse.clear(); imguizmo.clear(); - actions.clear(); goals.clear(); trackedTargetTransforms.clear(); segmentLabels.clear(); @@ -339,7 +301,7 @@ struct CCameraScriptedFrameEvents inline bool empty() const { - return keyboard.empty() && mouse.empty() && imguizmo.empty() && actions.empty() && + return keyboard.empty() && mouse.empty() && imguizmo.empty() && goals.empty() && trackedTargetTransforms.empty() && segmentLabels.empty(); } }; @@ -368,9 +330,6 @@ struct CCameraScriptedFrameEventUtilities final case CCameraScriptedInputEvent::Type::Imguizmo: out.imguizmo.emplace_back(ev.imguizmo); break; - case CCameraScriptedInputEvent::Type::Action: - out.actions.emplace_back(ev.action); - break; case CCameraScriptedInputEvent::Type::Goal: out.goals.emplace_back(ev.goal); break; diff --git a/include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp b/include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp deleted file mode 100644 index 58e6f2dbb0..0000000000 --- a/include/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h - -#ifndef _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ -#define _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ - -#include -#include -#include -#include -#include - -#include "CCameraScriptedRuntime.hpp" -#include "CCameraSequenceScriptPersistence.hpp" - -namespace nbl::system -{ - -class ISystem; - -/// @brief Optional scripted control overrides parsed alongside one runtime payload. -struct CCameraScriptedControlOverrides -{ - bool hasKeyboardScale = false; - float keyboardScale = 1.f; - bool hasMouseMoveScale = false; - float mouseMoveScale = 1.f; - bool hasMouseScrollScale = false; - float mouseScrollScale = 1.f; - bool hasTranslationScale = false; - float translationScale = 1.f; - bool hasRotationScale = false; - float rotationScale = 1.f; -}; - -/// @brief Parsed low-level scripted runtime payload plus optional compact authored sequence. -struct CCameraScriptedInputParseResult -{ - bool enabled = true; - bool hasLog = false; - bool log = false; - bool hardFail = false; - bool visualDebug = false; - float visualTargetFps = 0.f; - float visualCameraHoldSeconds = 0.f; - bool hasEnableActiveCameraMovement = false; - bool enableActiveCameraMovement = true; - bool exclusive = false; - std::string capturePrefix = "script"; - CCameraScriptedControlOverrides cameraControls = {}; - CCameraScriptedTimeline timeline = {}; - std::optional sequence; - std::vector warnings; -}; - -struct CCameraScriptedRuntimePersistenceUtilities final -{ - static inline void appendScriptedInputParseWarning(CCameraScriptedInputParseResult& out, std::string warning) - { - out.warnings.emplace_back(std::move(warning)); - } -}; - -/// @brief Parse one low-level scripted runtime payload from an existing stream. -bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error = nullptr); -/// @brief Parse one low-level scripted runtime payload directly from text. -bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error = nullptr); -/// @brief Load one low-level scripted runtime payload from a file. -bool loadCameraScriptedInputFromFile(ISystem& system, const path& path, CCameraScriptedInputParseResult& out, std::string* error = nullptr); - -} // namespace nbl::system - -#endif // _C_CAMERA_SCRIPTED_RUNTIME_PERSISTENCE_HPP_ diff --git a/include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp b/include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp index 22dd66cf4f..6015958c3c 100644 --- a/include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraScriptedUiInputUtilities.hpp @@ -52,18 +52,18 @@ struct CCameraScriptedUiInputUtilities final authoredMouse.action == system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed ? SMouseEvent::SClickEvent::EA_PRESSED : SMouseEvent::SClickEvent::EA_RELEASED; - outEvent.clickEvent.clickPosX = authoredMouse.x; - outEvent.clickEvent.clickPosY = authoredMouse.y; + outEvent.clickEvent.clickPosX = authoredMouse.position.x; + outEvent.clickEvent.clickPosY = authoredMouse.position.y; return true; case system::CCameraScriptedInputEvent::MouseData::Type::Scroll: outEvent.type = SMouseEvent::EET_SCROLL; - outEvent.scrollEvent.verticalScroll = authoredMouse.v; - outEvent.scrollEvent.horizontalScroll = authoredMouse.h; + outEvent.scrollEvent.verticalScroll = authoredMouse.scroll.x; + outEvent.scrollEvent.horizontalScroll = authoredMouse.scroll.y; return true; case system::CCameraScriptedInputEvent::MouseData::Type::Movement: outEvent.type = SMouseEvent::EET_MOVEMENT; - outEvent.movementEvent.relativeMovementX = authoredMouse.dx; - outEvent.movementEvent.relativeMovementY = authoredMouse.dy; + outEvent.movementEvent.relativeMovementX = authoredMouse.delta.x; + outEvent.movementEvent.relativeMovementY = authoredMouse.delta.y; return true; default: return false; diff --git a/include/nbl/ext/Cameras/CCameraSequenceScript.hpp b/include/nbl/ext/Cameras/CCameraSequenceScript.hpp index e4e459d698..1db018fba6 100644 --- a/include/nbl/ext/Cameras/CCameraSequenceScript.hpp +++ b/include/nbl/ext/Cameras/CCameraSequenceScript.hpp @@ -402,7 +402,7 @@ struct CCameraSequenceScriptUtilities final if (delta.hasRotationEulerDegOffset) { - goal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(goal.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(delta.rotationEulerDegOffset))); + goal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(goal.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::CCameraMathUtilities::castVector(delta.rotationEulerDegOffset))); } if (delta.hasTargetOffset) @@ -532,14 +532,14 @@ struct CCameraSequenceScriptUtilities final if (authored.hasAbsolutePosition) outPose.position = authored.absolutePosition; if (authored.hasAbsoluteRotationEulerDeg) - outPose.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.absoluteRotationEulerDeg)); + outPose.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::CCameraMathUtilities::castVector(authored.absoluteRotationEulerDeg)); if (authored.hasDelta) { if (authored.delta.hasPositionOffset) outPose.position += authored.delta.positionOffset; if (authored.delta.hasRotationEulerDegOffset) - outPose.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(outPose.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::getCastedVector(authored.delta.rotationEulerDegOffset))); + outPose.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(outPose.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::CCameraMathUtilities::castVector(authored.delta.rotationEulerDegOffset))); } if (!isSequenceTrackedTargetPoseFinite(outPose)) diff --git a/include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp b/include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp index 1ec56871e5..fdc73e9937 100644 --- a/include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp +++ b/include/nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp @@ -5,7 +5,6 @@ #ifndef _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ #define _C_CAMERA_SEQUENCE_SCRIPT_PERSISTENCE_HPP_ -#include #include #include @@ -17,12 +16,13 @@ namespace nbl::system class ISystem; -/// @brief Parse one compact camera-sequence script from an existing stream. -bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error = nullptr); -/// @brief Parse one compact camera-sequence script directly from text. -bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error = nullptr); -/// @brief Load one compact camera-sequence script from a file. -bool loadCameraSequenceScriptFromFile(ISystem& system, const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); +struct CCameraSequenceScriptPersistenceUtilities final +{ + /// @brief Parse one compact camera-sequence script directly from JSON text. + static bool deserializeCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error = nullptr); + /// @brief Load one compact camera-sequence script from a file. + static bool loadCameraSequenceScriptFromFile(ISystem& system, const path& path, core::CCameraSequenceScript& out, std::string* error = nullptr); +}; } // namespace nbl::system diff --git a/include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp b/include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp deleted file mode 100644 index f8dd5cf970..0000000000 --- a/include/nbl/ext/Cameras/CCameraSequenceScriptedBuilder.hpp +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h - -#ifndef _C_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_ -#define _C_CAMERA_SEQUENCE_SCRIPTED_BUILDER_HPP_ - -#include - -#include "CCameraScriptedRuntime.hpp" -#include "CCameraSequenceScript.hpp" -#include "ICamera.hpp" - -namespace nbl::system -{ - -/// @brief Build expanded scripted runtime data from a compiled camera-sequence segment. -/// -/// The builder converts compiled sequence frames into the shared runtime event -/// and check payloads used by camera-sequence consumers. -struct CCameraSequenceScriptedSegmentBuildInfo -{ - /// @brief Planar index that receives the compiled segment. - uint32_t planarIx = 0u; - /// @brief Number of windows the consumer can actually route presentation actions to. - size_t availableWindowCount = 1u; - /// @brief Whether secondary-window presentation actions are emitted. - bool useWindow = false; - /// @brief Whether per-frame follow-lock checks are generated for this segment. - bool includeFollowTargetLock = false; -}; - -struct CCameraSequenceScriptedBuilderUtilities final -{ - /// @brief Append one compiled segment as expanded scripted runtime payloads. - static inline bool appendCompiledSequenceSegmentToScriptedTimeline( - CCameraScriptedTimeline& timeline, - const uint64_t baseFrame, - const core::CCameraSequenceCompiledSegment& compiledSegment, - const CCameraSequenceScriptedSegmentBuildInfo& buildInfo, - std::string* error = nullptr) - { - std::vector framePolicies; - if (!core::CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies(compiledSegment, framePolicies, buildInfo.includeFollowTargetLock)) - { - if (error) - *error = "Failed to build compiled frame policies."; - return false; - } - - CCameraScriptedRuntimeUtilities::appendScriptedSegmentLabelEvent(timeline, baseFrame, compiledSegment.name); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); - if (!compiledSegment.presentations.empty()) - { - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[0].projection)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[0].leftHanded ? 1 : 0); - } - if (compiledSegment.resetCamera) - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera, 1); - - if (buildInfo.useWindow) - { - for (size_t windowIx = 1u; windowIx < std::min(compiledSegment.presentations.size(), buildInfo.availableWindowCount); ++windowIx) - { - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, static_cast(windowIx)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar, static_cast(buildInfo.planarIx)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType, static_cast(compiledSegment.presentations[windowIx].projection)); - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded, compiledSegment.presentations[windowIx].leftHanded ? 1 : 0); - } - CCameraScriptedRuntimeUtilities::appendScriptedActionEvent(timeline, baseFrame, CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow, 0); - } - - for (const auto& policy : framePolicies) - { - core::CCameraPreset preset; - if (!core::CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(compiledSegment.track, policy.sampleTime, preset)) - { - if (error) - *error = "Failed to sample compiled segment track."; - return false; - } - CCameraScriptedRuntimeUtilities::appendScriptedGoalEvent( - timeline, - baseFrame + policy.frameOffset, - core::CCameraPresetUtilities::makeGoalFromPreset(preset)); - - if (compiledSegment.usesTrackedTargetTrack()) - { - core::CCameraSequenceTrackedTargetPose trackedTargetPose; - if (!core::CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime(compiledSegment.trackedTargetTrack, policy.sampleTime, trackedTargetPose)) - { - if (error) - *error = "Failed to sample compiled tracked-target track."; - return false; - } - - core::ICamera::CGimbal gimbal({ .position = trackedTargetPose.position, .orientation = trackedTargetPose.orientation }); - CCameraScriptedRuntimeUtilities::appendScriptedTrackedTargetTransformEvent(timeline, baseFrame + policy.frameOffset, gimbal.operator()()); - } - - if (policy.baseline) - CCameraScriptedRuntimeUtilities::appendScriptedBaselineCheck(timeline, baseFrame + policy.frameOffset); - if (policy.continuityStep) - { - CCameraScriptedRuntimeUtilities::appendScriptedGimbalStepCheck( - timeline, - baseFrame + policy.frameOffset, - compiledSegment.continuity.hasPosDeltaConstraint, - compiledSegment.continuity.maxPosDelta, - compiledSegment.continuity.minPosDelta, - compiledSegment.continuity.hasEulerDeltaConstraint, - compiledSegment.continuity.maxEulerDeltaDeg, - compiledSegment.continuity.minEulerDeltaDeg); - } - if (policy.followTargetLock) - CCameraScriptedRuntimeUtilities::appendScriptedFollowTargetLockCheck(timeline, baseFrame + policy.frameOffset); - if (policy.capture) - timeline.captureFrames.emplace_back(baseFrame + policy.frameOffset); - } - - return true; - } -}; - -} // namespace nbl::system - -#endif diff --git a/include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp b/include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp index 7271685f5b..6270d718a0 100644 --- a/include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraTargetRelativeUtilities.hpp @@ -67,7 +67,7 @@ struct SCameraTargetRelativeRigDefaults final static constexpr double DollyPitchLimitRad = hlsl::SCameraViewRigDefaults::DollyPitchLimitRad; static constexpr double TopDownPitchRad = hlsl::SCameraViewRigDefaults::TopDownPitchRad; static constexpr double IsometricYawRad = hlsl::SCameraViewRigDefaults::IsometricYawRad; - static constexpr double IsometricPitchRad = hlsl::SCameraViewRigDefaults::IsometricPitchRad; + static inline const double IsometricPitchRad = hlsl::SCameraViewRigDefaults::IsometricPitchRad; static inline constexpr SCameraTargetRelativeEventPolicy OrbitTranslatePolicy = { .translateOrbit = true diff --git a/include/nbl/ext/Cameras/CCameraTextUtilities.hpp b/include/nbl/ext/Cameras/CCameraTextUtilities.hpp index d07212b399..7d2422dd3d 100644 --- a/include/nbl/ext/Cameras/CCameraTextUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraTextUtilities.hpp @@ -72,15 +72,15 @@ struct CCameraTextUtilities final } /// @brief Describe the typed goal-state mask in a stable human-readable format. - static inline std::string describeGoalStateMask(const uint32_t mask) + static inline std::string describeGoalStateMask(const core::ICamera::goal_state_flags_t mask) { if (mask == core::ICamera::GoalStateNone) return "Pose only"; std::string out; - auto append = [&](const char* label, const uint32_t bit) -> void + auto append = [&](const char* label, const core::ICamera::GoalStateMask bit) -> void { - if ((mask & bit) != bit) + if (!mask.hasFlags(bit)) return; if (!out.empty()) out += ", "; @@ -110,7 +110,7 @@ struct CCameraTextUtilities final oss << " exact=" << (result.exact ? "true" : "false") << " events=" << result.eventCount; - if (result.issues != core::CCameraGoalSolver::SApplyResult::NoIssue) + if (result.issues != core::CCameraGoalSolver::SApplyResult::EIssue::NoIssue) { oss << " issues="; bool first = true; @@ -124,11 +124,11 @@ struct CCameraTextUtilities final first = false; }; - appendIssue("absolute_pose_fallback", core::CCameraGoalSolver::SApplyResult::UsedAbsolutePoseFallback); - appendIssue("missing_spherical_state", core::CCameraGoalSolver::SApplyResult::MissingSphericalTargetState); - appendIssue("missing_path_state", core::CCameraGoalSolver::SApplyResult::MissingPathState); - appendIssue("missing_dynamic_perspective_state", core::CCameraGoalSolver::SApplyResult::MissingDynamicPerspectiveState); - appendIssue("virtual_event_replay_failed", core::CCameraGoalSolver::SApplyResult::VirtualEventReplayFailed); + appendIssue("absolute_pose_fallback", core::CCameraGoalSolver::SApplyResult::EIssue::UsedAbsolutePoseFallback); + appendIssue("missing_spherical_state", core::CCameraGoalSolver::SApplyResult::EIssue::MissingSphericalTargetState); + appendIssue("missing_path_state", core::CCameraGoalSolver::SApplyResult::EIssue::MissingPathState); + appendIssue("missing_dynamic_perspective_state", core::CCameraGoalSolver::SApplyResult::EIssue::MissingDynamicPerspectiveState); + appendIssue("virtual_event_replay_failed", core::CCameraGoalSolver::SApplyResult::EIssue::VirtualEventReplayFailed); } return oss.str(); diff --git a/include/nbl/ext/Cameras/CCameraTraits.hpp b/include/nbl/ext/Cameras/CCameraTraits.hpp index ecc9c2d7a3..a333c53eed 100644 --- a/include/nbl/ext/Cameras/CCameraTraits.hpp +++ b/include/nbl/ext/Cameras/CCameraTraits.hpp @@ -18,7 +18,9 @@ namespace nbl::core struct SCameraTargetRelativeTraits final { /// @brief Smallest valid target-relative distance shared by spherical and path-style rigs. - static inline constexpr float MinDistance = 0.1f; + /// This is a semantic lower bound used to avoid zero-distance degeneracy, not + /// the minimum representable `float` value. + static inline constexpr float MinDistance = 1e-1f; /// @brief Default upper bound for target-relative distance when no camera-specific cap is requested. static inline constexpr float DefaultMaxDistance = std::numeric_limits::infinity(); }; @@ -33,7 +35,7 @@ struct SCameraToolingThresholds final /// @brief Default world-space position tolerance used by pose comparisons. static inline constexpr double DefaultPositionTolerance = 2.0 * ScalarTolerance; /// @brief Default angular tolerance in degrees used by pose and state comparisons. - static inline constexpr double DefaultAngularToleranceDeg = 0.1; + static inline constexpr double DefaultAngularToleranceDeg = 1e-1; }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/CIsometricCamera.hpp b/include/nbl/ext/Cameras/CIsometricCamera.hpp index 3ac1d41e85..673a9f37c8 100644 --- a/include/nbl/ext/Cameras/CIsometricCamera.hpp +++ b/include/nbl/ext/Cameras/CIsometricCamera.hpp @@ -69,7 +69,7 @@ class CIsometricCamera final : public CSphericalTargetCamera private: static inline constexpr auto AllowedVirtualEvents = CVirtualGimbalEvent::Translate; static inline constexpr double IsoYaw = SCameraTargetRelativeRigDefaults::IsometricYawRad; - static inline constexpr double IsoPitch = SCameraTargetRelativeRigDefaults::IsometricPitchRad; + static inline const double IsoPitch = SCameraTargetRelativeRigDefaults::IsometricPitchRad; }; } diff --git a/include/nbl/ext/Cameras/ICamera.hpp b/include/nbl/ext/Cameras/ICamera.hpp index bf5cf3b6da..957dd3445d 100644 --- a/include/nbl/ext/Cameras/ICamera.hpp +++ b/include/nbl/ext/Cameras/ICamera.hpp @@ -9,6 +9,7 @@ #include #include "nbl/core/IReferenceCounted.h" +#include "nbl/core/util/bitflag.h" #include "CCameraTraits.hpp" #include "IGimbal.hpp" @@ -83,6 +84,9 @@ class ICamera : virtual public core::IReferenceCounted GoalStatePath = core::createBitmask({ 2 }) }; + using capability_flags_t = core::bitflag; + using goal_state_flags_t = core::bitflag; + /// @brief Canonical target-relative state reported by spherical camera families. /// /// The state stores the tracked target position, orbit angles in `orbitUv`, @@ -234,7 +238,7 @@ class ICamera : virtual public core::IReferenceCounted const auto& gUp = this->getYAxis(); const auto& gForward = this->getZAxis(); - assert(hlsl::isOrthoBase(gRight, gUp, gForward)); + assert(hlsl::CCameraMathUtilities::isOrthoBase(gRight, gUp, gForward)); const auto& position = this->getPosition(); @@ -334,12 +338,12 @@ class ICamera : virtual public core::IReferenceCounted /// @brief Return the typed goal-state fragments that helper layers may safely use with this camera. virtual uint32_t getGoalStateMask() const { - uint32_t mask = GoalStateNone; + goal_state_flags_t mask = GoalStateNone; if (hasCapability(SphericalTarget)) mask |= GoalStateSphericalTarget; if (hasCapability(DynamicPerspectiveFov)) mask |= GoalStateDynamicPerspective; - return mask; + return static_cast(mask.value); } /// @brief Return the stable human-readable identifier for this concrete camera instance. @@ -348,13 +352,13 @@ class ICamera : virtual public core::IReferenceCounted /// @brief Check whether the camera exposes the requested optional capability. inline bool hasCapability(CameraCapability capability) const { - return (getCapabilities() & capability) == capability; + return capability_flags_t(getCapabilities()).hasFlags(capability); } /// @brief Check whether the camera can exchange the requested typed goal-state fragment. inline bool supportsGoalState(GoalStateMask goalState) const { - return (getGoalStateMask() & goalState) == goalState; + return goal_state_flags_t(getGoalStateMask()).hasFlags(goalState); } /// @brief Query the current spherical-target state when the camera exposes it. diff --git a/include/nbl/ext/Cameras/IGimbal.hpp b/include/nbl/ext/Cameras/IGimbal.hpp index a2c5dc79e4..96aea65f73 100644 --- a/include/nbl/ext/Cameras/IGimbal.hpp +++ b/include/nbl/ext/Cameras/IGimbal.hpp @@ -6,6 +6,7 @@ #include #include +#include "nbl/type_traits.h" #include "CCameraMathUtilities.hpp" #include "CVirtualGimbalEvent.hpp" diff --git a/include/nbl/ext/Cameras/ILinearProjection.hpp b/include/nbl/ext/Cameras/ILinearProjection.hpp index 0edf855454..66c21489dd 100644 --- a/include/nbl/ext/Cameras/ILinearProjection.hpp +++ b/include/nbl/ext/Cameras/ILinearProjection.hpp @@ -127,7 +127,7 @@ class ILinearProjection : virtual public core::IReferenceCounted inline concatenated_matrix_t getMV(const model_matrix_t& model) const { const auto& v = m_camera->getGimbal().getViewMatrix(); - return hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); + return hlsl::mul(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(v), hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(model)); } /// @brief Compute the model-view-projection matrix from a model matrix. @@ -139,7 +139,7 @@ class ILinearProjection : virtual public core::IReferenceCounted { const auto& v = m_camera->getGimbal().getViewMatrix(); const auto& p = projection.getProjectionMatrix(); - auto mv = hlsl::mul(hlsl::getMatrix3x4As4x4(v), hlsl::getMatrix3x4As4x4(model)); + auto mv = hlsl::mul(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(v), hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(model)); return hlsl::mul(p, mv); } diff --git a/include/nbl/ext/Cameras/IPerspectiveProjection.hpp b/include/nbl/ext/Cameras/IPerspectiveProjection.hpp index 5806089f8c..c8ee879c44 100644 --- a/include/nbl/ext/Cameras/IPerspectiveProjection.hpp +++ b/include/nbl/ext/Cameras/IPerspectiveProjection.hpp @@ -36,7 +36,7 @@ class IPerspectiveProjection : public ILinearProjection /// @brief Rebuild the concatenated quad projection from its authored components. inline void setQuadTransform(const ILinearProjection::model_matrix_t& pretransform, ILinearProjection::concatenated_matrix_t viewport) { - auto concatenated = hlsl::mul(hlsl::getMatrix3x4As4x4(pretransform), viewport); + auto concatenated = hlsl::mul(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(pretransform), viewport); base_t::setProjectionMatrix(concatenated); m_pretransform = pretransform; diff --git a/include/nbl/ext/Cameras/IPlanarProjection.hpp b/include/nbl/ext/Cameras/IPlanarProjection.hpp index bf5df8f1d8..1b6f193e95 100644 --- a/include/nbl/ext/Cameras/IPlanarProjection.hpp +++ b/include/nbl/ext/Cameras/IPlanarProjection.hpp @@ -1,6 +1,8 @@ #ifndef _NBL_I_PLANAR_PROJECTION_HPP_ #define _NBL_I_PLANAR_PROJECTION_HPP_ +#include "nbl/builtin/hlsl/math/thin_lens_projection.hlsl" + #include "IGimbalBindingLayout.hpp" #include "ILinearProjection.hpp" diff --git a/include/nbl/ext/Cameras/README.md b/include/nbl/ext/Cameras/README.md index ec4a50a3cd..cfc9038d7a 100644 --- a/include/nbl/ext/Cameras/README.md +++ b/include/nbl/ext/Cameras/README.md @@ -1,6 +1,6 @@ # Shared Camera API -This directory contains the reusable camera stack used by [`61_UI`](../../../../examples_tests/61_UI/README.md). +This directory contains the reusable Nabla camera stack. The stack has two public faces: @@ -369,26 +369,19 @@ Main types involved: - [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) - [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) -### 9. Build scripted runtime payloads from compact authored data +### 9. Build and evaluate scripted runtime payloads -Use this when camera playback is authored as sequence data and then expanded into per-frame runtime actions and checks. +Use this when camera playback is authored as compact camera-domain data and then evaluated through generic per-frame runtime payloads and checks. ```cpp system::CCameraScriptedTimeline timeline; - -system::CCameraSequenceScriptedBuilderUtilities::appendCompiledSequenceSegmentToScriptedTimeline( - timeline, - baseFrame, - compiledSegment, - buildInfo); - system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); ``` Main types involved: - [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) +- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) - [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) - [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) @@ -648,10 +641,12 @@ File: It stores runtime payloads such as: - low-level input events -- action events +- goal and tracked-target events - per-frame checks - capture scheduling +Consumer-specific UI actions stay outside this shared runtime payload. + ### `Path Rig` Files: @@ -804,8 +799,6 @@ This layer stores authored camera-domain data. Files: - [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) -- [`CCameraScriptedRuntimePersistence.hpp`](CCameraScriptedRuntimePersistence.hpp) -- [`CCameraSequenceScriptedBuilder.hpp`](CCameraSequenceScriptedBuilder.hpp) - [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) This layer stores executable per-frame runtime payloads and validation checks. diff --git a/src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp b/src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp index 6941205d21..d38f8ac492 100644 --- a/src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp +++ b/src/nbl/ext/Cameras/CCameraJsonPersistenceUtilities.hpp @@ -1,5 +1,5 @@ -#ifndef _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ -#define _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ +#ifndef _NBL_EXT_CAMERAS_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ +#define _NBL_EXT_CAMERAS_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ #include @@ -8,91 +8,94 @@ #include "nbl/ext/Cameras/CCameraPresetFlow.hpp" #include "nlohmann/json.hpp" -namespace nbl::system +namespace nbl::system::impl { -template -inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) +struct CCameraJsonPersistenceUtilities final { - goal = {}; + template + static inline void deserializeGoalJson(const Json& entry, core::CCameraGoal& goal) + { + goal = {}; - if (entry.contains("camera_kind")) - goal.sourceKind = static_cast(entry["camera_kind"].get()); - if (entry.contains("camera_capabilities")) - goal.sourceCapabilities = entry["camera_capabilities"].get(); - if (entry.contains("camera_goal_state_mask")) - goal.sourceGoalStateMask = entry["camera_goal_state_mask"].get(); + if (entry.contains("camera_kind")) + goal.sourceKind = static_cast(entry["camera_kind"].get()); + if (entry.contains("camera_capabilities")) + goal.sourceCapabilities = core::ICamera::capability_flags_t(entry["camera_capabilities"].get()); + if (entry.contains("camera_goal_state_mask")) + goal.sourceGoalStateMask = core::ICamera::goal_state_flags_t(entry["camera_goal_state_mask"].get()); - if (entry.contains("position") && entry["position"].is_array()) - { - const auto values = entry["position"].get>(); - goal.position = hlsl::float64_t3(values[0], values[1], values[2]); - } - if (entry.contains("orientation") && entry["orientation"].is_array()) - { - const auto values = entry["orientation"].get>(); - goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromComponents(values[0], values[1], values[2], values[3]); - } - if (entry.contains("target_position") && entry["target_position"].is_array()) - { - const auto values = entry["target_position"].get>(); - goal.targetPosition = hlsl::float64_t3(values[0], values[1], values[2]); - goal.hasTargetPosition = true; - } - if (entry.contains("distance")) - { - goal.distance = entry["distance"].get(); - goal.hasDistance = true; - } - if (entry.contains("orbit_u")) - { - goal.orbitUv.x = entry["orbit_u"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("orbit_v")) - { - goal.orbitUv.y = entry["orbit_v"].get(); - goal.hasOrbitState = true; + if (entry.contains("position") && entry["position"].is_array()) + { + const auto values = entry["position"].get>(); + goal.position = hlsl::float64_t3(values[0], values[1], values[2]); + } + if (entry.contains("orientation") && entry["orientation"].is_array()) + { + const auto values = entry["orientation"].get>(); + goal.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromComponents(values[0], values[1], values[2], values[3]); + } + if (entry.contains("target_position") && entry["target_position"].is_array()) + { + const auto values = entry["target_position"].get>(); + goal.targetPosition = hlsl::float64_t3(values[0], values[1], values[2]); + goal.hasTargetPosition = true; + } + if (entry.contains("distance")) + { + goal.distance = entry["distance"].get(); + goal.hasDistance = true; + } + if (entry.contains("orbit_u")) + { + goal.orbitUv.x = entry["orbit_u"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_v")) + { + goal.orbitUv.y = entry["orbit_v"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("orbit_distance")) + { + goal.orbitDistance = entry["orbit_distance"].get(); + goal.hasOrbitState = true; + } + if (entry.contains("path_s") && entry.contains("path_u") && entry.contains("path_v")) + { + goal.pathState.s = entry["path_s"].get(); + goal.pathState.u = entry["path_u"].get(); + goal.pathState.v = entry["path_v"].get(); + goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; + goal.hasPathState = true; + } + if (entry.contains("dynamic_base_fov")) + { + goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); + goal.hasDynamicPerspectiveState = true; + } + if (entry.contains("dynamic_reference_distance")) + { + goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); + goal.hasDynamicPerspectiveState = true; + } } - if (entry.contains("orbit_distance")) - { - goal.orbitDistance = entry["orbit_distance"].get(); - goal.hasOrbitState = true; - } - if (entry.contains("path_s") && entry.contains("path_u") && entry.contains("path_v")) - { - goal.pathState.s = entry["path_s"].get(); - goal.pathState.u = entry["path_u"].get(); - goal.pathState.v = entry["path_v"].get(); - goal.pathState.roll = entry.contains("path_roll") ? entry["path_roll"].get() : 0.0; - goal.hasPathState = true; - } - if (entry.contains("dynamic_base_fov")) - { - goal.dynamicPerspectiveState.baseFov = entry["dynamic_base_fov"].get(); - goal.hasDynamicPerspectiveState = true; - } - if (entry.contains("dynamic_reference_distance")) - { - goal.dynamicPerspectiveState.referenceDistance = entry["dynamic_reference_distance"].get(); - goal.hasDynamicPerspectiveState = true; - } -} -template -inline void deserializePresetJson(const Json& entry, core::CCameraPreset& preset) -{ - preset = {}; - if (entry.contains("name")) - preset.name = entry["name"].get(); - if (entry.contains("identifier")) - preset.identifier = entry["identifier"].get(); + template + static inline void deserializePresetJson(const Json& entry, core::CCameraPreset& preset) + { + preset = {}; + if (entry.contains("name")) + preset.name = entry["name"].get(); + if (entry.contains("identifier")) + preset.identifier = entry["identifier"].get(); - core::CCameraGoal goal = {}; - deserializeGoalJson(entry, goal); - core::CCameraPresetUtilities::assignGoalToPreset(preset, goal); -} + core::CCameraGoal goal = {}; + deserializeGoalJson(entry, goal); + core::CCameraPresetUtilities::assignGoalToPreset(preset, goal); + } +}; -} // namespace nbl::system +} // namespace nbl::system::impl -#endif // _NBL_EXAMPLES_CAMERA_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ +#endif // _NBL_EXT_CAMERAS_JSON_PERSISTENCE_UTILITIES_HPP_INCLUDED_ diff --git a/src/nbl/ext/Cameras/CCameraPersistence.cpp b/src/nbl/ext/Cameras/CCameraPersistence.cpp index 20ca9ca1ad..9d3a2ea5b2 100644 --- a/src/nbl/ext/Cameras/CCameraPersistence.cpp +++ b/src/nbl/ext/Cameras/CCameraPersistence.cpp @@ -5,274 +5,294 @@ #include "nbl/ext/Cameras/CCameraPersistence.hpp" #include -#include #include "CCameraJsonPersistenceUtilities.hpp" +#include "nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp" #include "nlohmann/json.hpp" using json_t = nlohmann::json; -json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) +namespace nbl::system { - json_t json; - json["position"] = { goal.position.x, goal.position.y, goal.position.z }; - json["orientation"] = { - goal.orientation.data.x, - goal.orientation.data.y, - goal.orientation.data.z, - goal.orientation.data.w - }; - json["camera_kind"] = static_cast(goal.sourceKind); - json["camera_capabilities"] = goal.sourceCapabilities; - json["camera_goal_state_mask"] = goal.sourceGoalStateMask; - - if (goal.hasTargetPosition) - json["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; - if (goal.hasDistance) - json["distance"] = goal.distance; - if (goal.hasOrbitState) - { - json["orbit_u"] = goal.orbitUv.x; - json["orbit_v"] = goal.orbitUv.y; - json["orbit_distance"] = goal.orbitDistance; - } - if (goal.hasPathState) - { - json["path_s"] = goal.pathState.s; - json["path_u"] = goal.pathState.u; - json["path_v"] = goal.pathState.v; - json["path_roll"] = goal.pathState.roll; - } - if (goal.hasDynamicPerspectiveState) - { - json["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; - json["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; - } - return json; -} - -json_t serializePresetJson(const nbl::core::CCameraPreset& preset) +namespace impl { - auto json = serializeGoalJson(nbl::core::CCameraPresetUtilities::makeGoalFromPreset(preset)); - json["name"] = preset.name; - json["identifier"] = preset.identifier; - return json; -} -json_t serializeKeyframeTrackJson(const nbl::core::CCameraKeyframeTrack& track) +struct CCameraPersistenceJsonUtilities final { - json_t root; - root["keyframes"] = json_t::array(); + static json_t serializeGoalJson(const nbl::core::CCameraGoal& goal) + { + json_t json; + json["position"] = { goal.position.x, goal.position.y, goal.position.z }; + json["orientation"] = { + goal.orientation.data.x, + goal.orientation.data.y, + goal.orientation.data.z, + goal.orientation.data.w + }; + json["camera_kind"] = static_cast(goal.sourceKind); + json["camera_capabilities"] = static_cast(goal.sourceCapabilities.value); + json["camera_goal_state_mask"] = static_cast(goal.sourceGoalStateMask.value); + + if (goal.hasTargetPosition) + json["target_position"] = { goal.targetPosition.x, goal.targetPosition.y, goal.targetPosition.z }; + if (goal.hasDistance) + json["distance"] = goal.distance; + if (goal.hasOrbitState) + { + json["orbit_u"] = goal.orbitUv.x; + json["orbit_v"] = goal.orbitUv.y; + json["orbit_distance"] = goal.orbitDistance; + } + if (goal.hasPathState) + { + json["path_s"] = goal.pathState.s; + json["path_u"] = goal.pathState.u; + json["path_v"] = goal.pathState.v; + json["path_roll"] = goal.pathState.roll; + } + if (goal.hasDynamicPerspectiveState) + { + json["dynamic_base_fov"] = goal.dynamicPerspectiveState.baseFov; + json["dynamic_reference_distance"] = goal.dynamicPerspectiveState.referenceDistance; + } + + return json; + } - for (const auto& keyframe : track.keyframes) + static json_t serializePresetJson(const nbl::core::CCameraPreset& preset) { - auto json = serializePresetJson(keyframe.preset); - json["time"] = keyframe.time; - root["keyframes"].push_back(std::move(json)); + auto json = serializeGoalJson(nbl::core::CCameraPresetUtilities::makeGoalFromPreset(preset)); + json["name"] = preset.name; + json["identifier"] = preset.identifier; + return json; } - return root; -} + static json_t serializeKeyframeTrackJson(const nbl::core::CCameraKeyframeTrack& track) + { + json_t root; + root["keyframes"] = json_t::array(); -bool deserializeKeyframeTrackJson(const json_t& root, nbl::core::CCameraKeyframeTrack& track) -{ - if (!root.contains("keyframes") || !root["keyframes"].is_array()) - return false; + for (const auto& keyframe : track.keyframes) + { + auto json = serializePresetJson(keyframe.preset); + json["time"] = keyframe.time; + root["keyframes"].push_back(std::move(json)); + } - track = {}; - for (const auto& entry : root["keyframes"]) - { - nbl::core::CCameraKeyframe keyframe; - if (entry.contains("time")) - keyframe.time = std::max(0.f, entry["time"].get()); - nbl::system::deserializePresetJson(entry, keyframe.preset); - track.keyframes.emplace_back(std::move(keyframe)); + return root; } - nbl::core::CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(track); - nbl::core::CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(track); - return true; -} + static bool deserializeKeyframeTrackJson(const json_t& root, nbl::core::CCameraKeyframeTrack& track) + { + if (!root.contains("keyframes") || !root["keyframes"].is_array()) + return false; + + track = {}; + for (const auto& entry : root["keyframes"]) + { + nbl::core::CCameraKeyframe keyframe; + if (entry.contains("time")) + keyframe.time = std::max(0.f, entry["time"].get()); + CCameraJsonPersistenceUtilities::deserializePresetJson(entry, keyframe.preset); + track.keyframes.emplace_back(std::move(keyframe)); + } + + nbl::core::CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(track); + nbl::core::CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(track); + return true; + } -json_t serializePresetCollectionJson(std::span presets) -{ - json_t root; - root["presets"] = json_t::array(); - for (const auto& preset : presets) - root["presets"].push_back(serializePresetJson(preset)); - return root; -} + static json_t serializePresetCollectionJson(std::span presets) + { + json_t root; + root["presets"] = json_t::array(); + for (const auto& preset : presets) + root["presets"].push_back(serializePresetJson(preset)); + return root; + } -bool deserializePresetCollectionJson(const json_t& root, std::vector& presets) -{ - if (!root.contains("presets") || !root["presets"].is_array()) - return false; + static bool deserializePresetCollectionJson(const json_t& root, std::vector& presets) + { + if (!root.contains("presets") || !root["presets"].is_array()) + return false; + + std::vector loadedPresets; + loadedPresets.reserve(root["presets"].size()); + for (const auto& entry : root["presets"]) + { + nbl::core::CCameraPreset preset; + CCameraJsonPersistenceUtilities::deserializePresetJson(entry, preset); + loadedPresets.emplace_back(std::move(preset)); + } + + presets = std::move(loadedPresets); + return true; + } - std::vector loadedPresets; - loadedPresets.reserve(root["presets"].size()); - for (const auto& entry : root["presets"]) + template + static bool deserializeJsonText( + std::string_view text, + Value& out, + const char* invalidPayloadMessage, + DeserializeFn&& deserializeFn, + std::string* error) { - nbl::core::CCameraPreset preset; - nbl::system::deserializePresetJson(entry, preset); - loadedPresets.emplace_back(std::move(preset)); + try + { + const auto root = json_t::parse(text); + if (!deserializeFn(root, out)) + { + if (error) + *error = invalidPayloadMessage; + return false; + } + return true; + } + catch (const json_t::exception& e) + { + if (error) + *error = e.what(); + return false; + } } - presets = std::move(loadedPresets); - return true; -} + static bool readTextFileOrSetError(nbl::system::ISystem& system, const nbl::system::path& filePath, std::string& text, std::string* error, const char* openMessage) + { + return nbl::system::CCameraFileUtilities::readTextFile(system, filePath, text, error, openMessage); + } +}; -namespace nbl::system -{ +} // namespace impl -bool writeGoal(std::ostream& out, const core::CCameraGoal& goal, const int indent) +std::string CCameraPresetPersistenceUtilities::serializeGoal(const core::CCameraGoal& goal, const int indent) { - if (!out) - return false; - - out << serializeGoalJson(goal).dump(indent); - return static_cast(out); + return impl::CCameraPersistenceJsonUtilities::serializeGoalJson(goal).dump(indent); } -bool readGoal(std::istream& in, core::CCameraGoal& goal) +bool CCameraPresetPersistenceUtilities::deserializeGoal(std::string_view text, core::CCameraGoal& goal, std::string* error) { - if (!in) - return false; - - json_t root; - in >> root; - nbl::system::deserializeGoalJson(root, goal); - return true; + return impl::CCameraPersistenceJsonUtilities::deserializeJsonText( + text, + goal, + "Camera goal JSON payload is invalid.", + [](const json_t& root, core::CCameraGoal& outGoal) + { + impl::CCameraJsonPersistenceUtilities::deserializeGoalJson(root, outGoal); + return true; + }, + error); } -bool saveGoalToFile(ISystem& system, const path& filePath, const core::CCameraGoal& goal, const int indent) +bool CCameraPresetPersistenceUtilities::saveGoalToFile(ISystem& system, const path& filePath, const core::CCameraGoal& goal, const int indent) { - std::ostringstream out; - if (!writeGoal(out, goal, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, serializeGoal(goal, indent)); } -bool loadGoalFromFile(ISystem& system, const path& filePath, core::CCameraGoal& goal) +bool CCameraPresetPersistenceUtilities::loadGoalFromFile(ISystem& system, const path& filePath, core::CCameraGoal& goal, std::string* error) { std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + if (!impl::CCameraPersistenceJsonUtilities::readTextFileOrSetError(system, filePath, text, error, "Cannot open camera goal file.")) return false; - std::istringstream in(text); - return readGoal(in, goal); + return deserializeGoal(text, goal, error); } -bool writePreset(std::ostream& out, const core::CCameraPreset& preset, const int indent) +std::string CCameraPresetPersistenceUtilities::serializePreset(const core::CCameraPreset& preset, const int indent) { - if (!out) - return false; - - out << serializePresetJson(preset).dump(indent); - return static_cast(out); + return impl::CCameraPersistenceJsonUtilities::serializePresetJson(preset).dump(indent); } -bool readPreset(std::istream& in, core::CCameraPreset& preset) +bool CCameraPresetPersistenceUtilities::deserializePreset(std::string_view text, core::CCameraPreset& preset, std::string* error) { - if (!in) - return false; - - json_t root; - in >> root; - nbl::system::deserializePresetJson(root, preset); - return true; + return impl::CCameraPersistenceJsonUtilities::deserializeJsonText( + text, + preset, + "Camera preset JSON payload is invalid.", + [](const json_t& root, core::CCameraPreset& outPreset) + { + impl::CCameraJsonPersistenceUtilities::deserializePresetJson(root, outPreset); + return true; + }, + error); } -bool savePresetToFile(ISystem& system, const path& filePath, const core::CCameraPreset& preset, const int indent) +bool CCameraPresetPersistenceUtilities::savePresetToFile(ISystem& system, const path& filePath, const core::CCameraPreset& preset, const int indent) { - std::ostringstream out; - if (!writePreset(out, preset, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, serializePreset(preset, indent)); } -bool loadPresetFromFile(ISystem& system, const path& filePath, core::CCameraPreset& preset) +bool CCameraPresetPersistenceUtilities::loadPresetFromFile(ISystem& system, const path& filePath, core::CCameraPreset& preset, std::string* error) { std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + if (!impl::CCameraPersistenceJsonUtilities::readTextFileOrSetError(system, filePath, text, error, "Cannot open camera preset file.")) return false; - std::istringstream in(text); - return readPreset(in, preset); + return deserializePreset(text, preset, error); } -bool writeKeyframeTrack(std::ostream& out, const core::CCameraKeyframeTrack& track, const int indent) +std::string CCameraKeyframeTrackPersistenceUtilities::serializeKeyframeTrack(const core::CCameraKeyframeTrack& track, const int indent) { - if (!out) - return false; - - out << serializeKeyframeTrackJson(track).dump(indent); - return static_cast(out); + return impl::CCameraPersistenceJsonUtilities::serializeKeyframeTrackJson(track).dump(indent); } -bool readKeyframeTrack(std::istream& in, core::CCameraKeyframeTrack& track) +bool CCameraKeyframeTrackPersistenceUtilities::deserializeKeyframeTrack(std::string_view text, core::CCameraKeyframeTrack& track, std::string* error) { - if (!in) - return false; - - json_t root; - in >> root; - return deserializeKeyframeTrackJson(root, track); + return impl::CCameraPersistenceJsonUtilities::deserializeJsonText( + text, + track, + "Camera keyframe track JSON payload is invalid.", + [](const json_t& root, core::CCameraKeyframeTrack& outTrack) + { + return impl::CCameraPersistenceJsonUtilities::deserializeKeyframeTrackJson(root, outTrack); + }, + error); } -bool saveKeyframeTrackToFile(ISystem& system, const path& filePath, const core::CCameraKeyframeTrack& track, const int indent) +bool CCameraKeyframeTrackPersistenceUtilities::saveKeyframeTrackToFile(ISystem& system, const path& filePath, const core::CCameraKeyframeTrack& track, const int indent) { - std::ostringstream out; - if (!writeKeyframeTrack(out, track, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, serializeKeyframeTrack(track, indent)); } -bool loadKeyframeTrackFromFile(ISystem& system, const path& filePath, core::CCameraKeyframeTrack& track) +bool CCameraKeyframeTrackPersistenceUtilities::loadKeyframeTrackFromFile(ISystem& system, const path& filePath, core::CCameraKeyframeTrack& track, std::string* error) { std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + if (!impl::CCameraPersistenceJsonUtilities::readTextFileOrSetError(system, filePath, text, error, "Cannot open camera keyframe track file.")) return false; - std::istringstream in(text); - return readKeyframeTrack(in, track); + return deserializeKeyframeTrack(text, track, error); } -bool writePresetCollection(std::ostream& out, std::span presets, const int indent) +std::string CCameraPersistenceUtilities::serializePresetCollection(std::span presets, const int indent) { - if (!out) - return false; - - out << serializePresetCollectionJson(presets).dump(indent); - return static_cast(out); + return impl::CCameraPersistenceJsonUtilities::serializePresetCollectionJson(presets).dump(indent); } -bool readPresetCollection(std::istream& in, std::vector& presets) +bool CCameraPersistenceUtilities::deserializePresetCollection(std::string_view text, std::vector& presets, std::string* error) { - if (!in) - return false; - - json_t root; - in >> root; - return deserializePresetCollectionJson(root, presets); + return impl::CCameraPersistenceJsonUtilities::deserializeJsonText( + text, + presets, + "Camera preset collection JSON payload is invalid.", + [](const json_t& root, std::vector& outPresets) + { + return impl::CCameraPersistenceJsonUtilities::deserializePresetCollectionJson(root, outPresets); + }, + error); } -bool savePresetCollectionToFile(ISystem& system, const path& filePath, std::span presets, const int indent) +bool CCameraPersistenceUtilities::savePresetCollectionToFile(ISystem& system, const path& filePath, std::span presets, const int indent) { - std::ostringstream out; - if (!writePresetCollection(out, presets, indent)) - return false; - return CCameraFileUtilities::writeTextFile(system, filePath, out.str()); + return CCameraFileUtilities::writeTextFile(system, filePath, serializePresetCollection(presets, indent)); } -bool loadPresetCollectionFromFile(ISystem& system, const path& filePath, std::vector& presets) +bool CCameraPersistenceUtilities::loadPresetCollectionFromFile(ISystem& system, const path& filePath, std::vector& presets, std::string* error) { std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text)) + if (!impl::CCameraPersistenceJsonUtilities::readTextFileOrSetError(system, filePath, text, error, "Cannot open camera preset collection file.")) return false; - std::istringstream in(text); - return readPresetCollection(in, presets); + return deserializePresetCollection(text, presets, error); } } // namespace nbl::system diff --git a/src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp b/src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp deleted file mode 100644 index 5f998f2f4e..0000000000 --- a/src/nbl/ext/Cameras/CCameraScriptedRuntimePersistence.cpp +++ /dev/null @@ -1,1156 +0,0 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h - -#include "nbl/ext/Cameras/CCameraScriptedRuntimePersistence.hpp" - -#include -#include -#include -#include -#include - -#include "CCameraJsonPersistenceUtilities.hpp" -#include "nlohmann/json.hpp" - -using json_t = nlohmann::json; - -bool tryParseCaptureFractionJson(const json_t& entry, float& outFraction) -{ - if (entry.is_number()) - { - outFraction = std::clamp(entry.get(), 0.f, 1.f); - return true; - } - - if (!entry.is_string()) - return false; - - const auto tag = entry.get(); - if (tag == "start") - outFraction = 0.f; - else if (tag == "mid" || tag == "middle") - outFraction = 0.5f; - else if (tag == "end") - outFraction = 1.f; - else - return false; - - return true; -} - -template -void readVector3(const json_t& entry, T& outValue) -{ - using scalar_t = std::remove_reference_t; - const auto values = entry.get>(); - outValue = T(values[0], values[1], values[2]); -} - -bool deserializeSequencePresentationsJson(const json_t& root, std::vector& out, std::string* error) -{ - out.clear(); - if (!root.is_array()) - { - if (error) - *error = "Sequence presentations must be an array."; - return false; - } - - for (const auto& entry : root) - { - if (!entry.is_object() || !entry.contains("projection")) - { - if (error) - *error = "Sequence presentation entry missing \"projection\"."; - return false; - } - - nbl::core::CCameraSequencePresentation presentation; - if (!nbl::core::CCameraSequenceScriptUtilities::tryParseProjectionType(entry["projection"].get(), presentation.projection)) - { - if (error) - *error = "Sequence presentation has invalid projection type."; - return false; - } - if (entry.contains("left_handed")) - presentation.leftHanded = entry["left_handed"].get(); - out.emplace_back(presentation); - } - - return true; -} - -bool deserializeSequenceContinuityJson(const json_t& root, nbl::core::CCameraSequenceContinuitySettings& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence continuity settings must be an object."; - return false; - } - - out = {}; - if (root.contains("baseline")) - out.baseline = root["baseline"].get(); - if (root.contains("step")) - out.step = root["step"].get(); - - if (root.contains("min_pos_delta")) - { - out.minPosDelta = root["min_pos_delta"].get(); - out.hasPosDeltaConstraint = true; - } - if (root.contains("max_pos_delta")) - { - out.maxPosDelta = root["max_pos_delta"].get(); - out.hasPosDeltaConstraint = true; - } - else if (root.contains("pos_tolerance")) - { - out.maxPosDelta = root["pos_tolerance"].get(); - out.hasPosDeltaConstraint = true; - } - - if (root.contains("min_euler_delta_deg")) - { - out.minEulerDeltaDeg = root["min_euler_delta_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - if (root.contains("max_euler_delta_deg")) - { - out.maxEulerDeltaDeg = root["max_euler_delta_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - else if (root.contains("euler_tolerance_deg")) - { - out.maxEulerDeltaDeg = root["euler_tolerance_deg"].get(); - out.hasEulerDeltaConstraint = true; - } - - if (root.contains("disable_pos_delta")) - out.hasPosDeltaConstraint = !root["disable_pos_delta"].get(); - if (root.contains("disable_euler_delta")) - out.hasEulerDeltaConstraint = !root["disable_euler_delta"].get(); - - if (out.step && !(out.hasPosDeltaConstraint || out.hasEulerDeltaConstraint)) - { - if (error) - *error = "Sequence continuity step checks require at least one delta constraint."; - return false; - } - - return true; -} - -bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequenceGoalDelta& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence keyframe delta must be an object."; - return false; - } - - out = {}; - if (root.contains("position_offset")) - { - readVector3(root["position_offset"], out.positionOffset); - out.hasPositionOffset = true; - } - if (root.contains("rotation_euler_deg_offset")) - { - readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); - out.hasRotationEulerDegOffset = true; - } - if (root.contains("target_offset")) - { - readVector3(root["target_offset"], out.targetOffset); - out.hasTargetOffset = true; - } - if (root.contains("orbit_u_delta_deg")) - { - out.orbitDelta.setUDeltaDeg(root["orbit_u_delta_deg"].get()); - } - if (root.contains("orbit_v_delta_deg")) - { - out.orbitDelta.setVDeltaDeg(root["orbit_v_delta_deg"].get()); - } - if (root.contains("orbit_distance_delta")) - { - out.orbitDelta.setDistanceDelta(root["orbit_distance_delta"].get()); - } - if (root.contains("path_s_delta_deg")) - { - out.pathDelta.setSDeltaDeg(root["path_s_delta_deg"].get()); - } - if (root.contains("path_u_delta")) - { - out.pathDelta.setUDelta(root["path_u_delta"].get()); - } - if (root.contains("path_v_delta")) - { - out.pathDelta.setVDelta(root["path_v_delta"].get()); - } - if (root.contains("path_roll_delta_deg")) - { - out.pathDelta.setRollDeltaDeg(root["path_roll_delta_deg"].get()); - } - if (root.contains("dynamic_base_fov_delta")) - { - out.dynamicBaseFovDelta = root["dynamic_base_fov_delta"].get(); - out.hasDynamicBaseFovDelta = true; - } - if (root.contains("dynamic_reference_distance_delta")) - { - out.dynamicReferenceDistanceDelta = root["dynamic_reference_distance_delta"].get(); - out.hasDynamicReferenceDistanceDelta = true; - } - - return true; -} - -bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSequenceKeyframe& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence keyframe must be an object."; - return false; - } - - out = {}; - if (root.contains("time")) - out.time = std::max(0.f, root["time"].get()); - - if (root.contains("delta")) - { - if (!deserializeSequenceGoalDeltaJson(root["delta"], out.delta, error)) - return false; - out.hasDelta = true; - } - - if (root.contains("preset")) - { - nbl::system::deserializePresetJson(root["preset"], out.absolutePreset); - out.hasAbsolutePreset = true; - } - else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || - root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || - root.contains("orbit_distance") || root.contains("path_s") || root.contains("path_u") || - root.contains("path_v") || root.contains("path_roll") || - root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) - { - nbl::system::deserializePresetJson(root, out.absolutePreset); - out.hasAbsolutePreset = true; - } - - return true; -} - -bool deserializeSequenceTrackedTargetDeltaJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetDelta& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence target delta must be an object."; - return false; - } - - out = {}; - if (root.contains("position_offset")) - { - readVector3(root["position_offset"], out.positionOffset); - out.hasPositionOffset = true; - } - if (root.contains("rotation_euler_deg_offset")) - { - readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); - out.hasRotationEulerDegOffset = true; - } - - return true; -} - -bool deserializeSequenceTrackedTargetKeyframeJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetKeyframe& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence target keyframe must be an object."; - return false; - } - - out = {}; - if (root.contains("time")) - out.time = std::max(0.f, root["time"].get()); - - if (root.contains("delta")) - { - if (!deserializeSequenceTrackedTargetDeltaJson(root["delta"], out.delta, error)) - return false; - out.hasDelta = true; - } - - if (root.contains("position")) - { - readVector3(root["position"], out.absolutePosition); - out.hasAbsolutePosition = true; - } - if (root.contains("rotation_euler_deg")) - { - readVector3(root["rotation_euler_deg"], out.absoluteRotationEulerDeg); - out.hasAbsoluteRotationEulerDeg = true; - } - - return true; -} - -bool deserializeSequenceSegmentJson(const json_t& root, nbl::core::CCameraSequenceSegment& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Sequence segment must be an object."; - return false; - } - - out = {}; - if (root.contains("name")) - out.name = root["name"].get(); - if (root.contains("camera_identifier")) - out.cameraIdentifier = root["camera_identifier"].get(); - if (root.contains("camera_kind")) - { - if (!nbl::core::CCameraSequenceScriptUtilities::tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) - { - if (error) - *error = "Sequence segment has invalid camera_kind."; - return false; - } - } - if (root.contains("duration_seconds")) - { - out.durationSeconds = std::max(0.f, root["duration_seconds"].get()); - out.hasDurationSeconds = true; - } - if (root.contains("reset_camera")) - { - out.resetCamera = root["reset_camera"].get(); - out.hasResetCamera = true; - } - if (root.contains("presentations")) - { - if (!deserializeSequencePresentationsJson(root["presentations"], out.presentations, error)) - return false; - } - if (root.contains("continuity")) - { - if (!deserializeSequenceContinuityJson(root["continuity"], out.continuity, error)) - return false; - out.hasContinuity = true; - } - if (root.contains("captures")) - { - if (!root["captures"].is_array()) - { - if (error) - *error = "Sequence segment captures must be an array."; - return false; - } - - out.captureFractions.clear(); - for (const auto& entry : root["captures"]) - { - float fraction = 0.f; - if (!tryParseCaptureFractionJson(entry, fraction)) - { - if (error) - *error = "Sequence segment capture entry is invalid."; - return false; - } - out.captureFractions.emplace_back(fraction); - } - nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.captureFractions); - out.hasCaptureFractions = true; - } - if (root.contains("keyframes")) - { - if (!root["keyframes"].is_array()) - { - if (error) - *error = "Sequence segment keyframes must be an array."; - return false; - } - for (const auto& entry : root["keyframes"]) - { - nbl::core::CCameraSequenceKeyframe keyframe; - if (!deserializeSequenceKeyframeJson(entry, keyframe, error)) - return false; - out.keyframes.emplace_back(std::move(keyframe)); - } - } - if (root.contains("target_keyframes")) - { - if (!root["target_keyframes"].is_array()) - { - if (error) - *error = "Sequence segment target_keyframes must be an array."; - return false; - } - for (const auto& entry : root["target_keyframes"]) - { - nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; - if (!deserializeSequenceTrackedTargetKeyframeJson(entry, keyframe, error)) - return false; - out.targetKeyframes.emplace_back(std::move(keyframe)); - } - } - - if (out.keyframes.empty()) - { - if (error) - *error = "Sequence segment requires at least one keyframe."; - return false; - } - if (out.cameraKind == nbl::core::ICamera::CameraKind::Unknown && out.cameraIdentifier.empty()) - { - if (error) - *error = "Sequence segment requires camera_kind or camera_identifier."; - return false; - } - - return true; -} - -bool deserializeCameraSequenceScriptJson(const json_t& root, nbl::core::CCameraSequenceScript& out, std::string* error) -{ - if (!root.is_object()) - { - if (error) - *error = "Camera sequence script must be an object."; - return false; - } - - out = {}; - if (root.contains("enabled")) - out.enabled = root["enabled"].get(); - if (root.contains("log")) - out.log = root["log"].get(); - if (root.contains("exclusive")) - out.exclusive = root["exclusive"].get(); - if (root.contains("exclusive_input")) - out.exclusive = root["exclusive_input"].get() || out.exclusive; - if (root.contains("hard_fail")) - out.hardFail = root["hard_fail"].get(); - if (root.contains("visual_debug")) - out.visualDebug = root["visual_debug"].get(); - if (root.contains("visual_debug_target_fps")) - out.visualDebugTargetFps = root["visual_debug_target_fps"].get(); - if (root.contains("visual_debug_hold_seconds")) - out.visualDebugHoldSeconds = root["visual_debug_hold_seconds"].get(); - if (root.contains("enableActiveCameraMovement")) - { - out.enableActiveCameraMovement = root["enableActiveCameraMovement"].get(); - out.hasEnableActiveCameraMovement = true; - } - if (root.contains("capture_prefix")) - out.capturePrefix = root["capture_prefix"].get(); - if (root.contains("fps")) - out.fps = std::max(1.f, root["fps"].get()); - - if (root.contains("defaults")) - { - const auto& defaults = root["defaults"]; - if (!defaults.is_object()) - { - if (error) - *error = "Camera sequence defaults must be an object."; - return false; - } - - if (defaults.contains("duration_seconds")) - out.defaults.durationSeconds = std::max(0.f, defaults["duration_seconds"].get()); - if (defaults.contains("reset_camera")) - out.defaults.resetCamera = defaults["reset_camera"].get(); - if (defaults.contains("presentations")) - { - if (!deserializeSequencePresentationsJson(defaults["presentations"], out.defaults.presentations, error)) - return false; - } - if (defaults.contains("continuity")) - { - if (!deserializeSequenceContinuityJson(defaults["continuity"], out.defaults.continuity, error)) - return false; - } - if (defaults.contains("captures")) - { - if (!defaults["captures"].is_array()) - { - if (error) - *error = "Camera sequence default captures must be an array."; - return false; - } - - out.defaults.captureFractions.clear(); - for (const auto& entry : defaults["captures"]) - { - float fraction = 0.f; - if (!tryParseCaptureFractionJson(entry, fraction)) - { - if (error) - *error = "Camera sequence default capture entry is invalid."; - return false; - } - out.defaults.captureFractions.emplace_back(fraction); - } - nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.defaults.captureFractions); - } - } - - if (!root.contains("segments") || !root["segments"].is_array()) - { - if (error) - *error = "Camera sequence script requires a \"segments\" array."; - return false; - } - - for (const auto& entry : root["segments"]) - { - nbl::core::CCameraSequenceSegment segment; - if (!deserializeSequenceSegmentJson(entry, segment, error)) - return false; - out.segments.emplace_back(std::move(segment)); - } - - if (out.segments.empty()) - { - if (error) - *error = "Camera sequence script must contain at least one segment."; - return false; - } - - return true; -} - -nbl::hlsl::float32_t4x4 composeScriptedImguizmoTransform( - const std::array& translation, - const std::array& rotationDeg, - const std::array& scale) -{ - return nbl::hlsl::CCameraMathUtilities::composeTransformMatrix( - nbl::hlsl::float32_t3(translation[0], translation[1], translation[2]), - nbl::hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegrees(nbl::hlsl::float32_t3(rotationDeg[0], rotationDeg[1], rotationDeg[2])), - nbl::hlsl::float32_t3(scale[0], scale[1], scale[2])); -} - -nbl::hlsl::float32_t4x4 makeScriptedMatrixFromArray(const std::array& values) -{ - nbl::hlsl::float32_t4x4 out(1.f); - for (uint32_t column = 0u; column < 4u; ++column) - { - for (uint32_t row = 0u; row < 4u; ++row) - out[column][row] = values[column * 4u + row]; - } - return out; -} - -std::optional parseScriptedKeyboardAction(std::string_view action) -{ - if (action == "pressed" || action == "press") - return nbl::system::CCameraScriptedInputEvent::KeyboardData::Action::Pressed; - if (action == "released" || action == "release") - return nbl::system::CCameraScriptedInputEvent::KeyboardData::Action::Released; - return std::nullopt; -} - -nbl::ui::E_KEY_CODE parseScriptedKeyCode(std::string_view key) -{ - auto parsed = nbl::ui::stringToKeyCode(key); - if (parsed != nbl::ui::EKC_NONE) - return parsed; - - constexpr std::string_view KeyPrefix = "KEY_"; - constexpr std::string_view EkcPrefix = "EKC_"; - if (key.starts_with(KeyPrefix)) - parsed = nbl::ui::stringToKeyCode(key.substr(KeyPrefix.size())); - if (parsed == nbl::ui::EKC_NONE && key.starts_with(EkcPrefix)) - parsed = nbl::ui::stringToKeyCode(key.substr(EkcPrefix.size())); - return parsed; -} - -std::optional parseScriptedMouseButton(std::string_view button) -{ - if (button == "LEFT_BUTTON") - return nbl::ui::EMB_LEFT_BUTTON; - if (button == "RIGHT_BUTTON") - return nbl::ui::EMB_RIGHT_BUTTON; - if (button == "MIDDLE_BUTTON") - return nbl::ui::EMB_MIDDLE_BUTTON; - if (button == "BUTTON_4") - return nbl::ui::EMB_BUTTON_4; - if (button == "BUTTON_5") - return nbl::ui::EMB_BUTTON_5; - return std::nullopt; -} - -std::optional parseScriptedMouseClickAction(std::string_view action) -{ - if (action == "pressed" || action == "press") - return nbl::system::CCameraScriptedInputEvent::MouseData::ClickAction::Pressed; - if (action == "released" || action == "release") - return nbl::system::CCameraScriptedInputEvent::MouseData::ClickAction::Released; - return std::nullopt; -} - -void parseScriptedCaptureFramesJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!script.contains("capture_frames")) - return; - - for (const auto& frame : script["capture_frames"]) - out.timeline.captureFrames.emplace_back(frame.get()); -} - -void parseScriptedControlOverridesJson(const json_t& controls, nbl::system::CCameraScriptedControlOverrides& out) -{ - if (controls.contains("keyboard_scale")) - { - out.hasKeyboardScale = true; - out.keyboardScale = controls["keyboard_scale"].get(); - } - if (controls.contains("mouse_move_scale")) - { - out.hasMouseMoveScale = true; - out.mouseMoveScale = controls["mouse_move_scale"].get(); - } - if (controls.contains("mouse_scroll_scale")) - { - out.hasMouseScrollScale = true; - out.mouseScrollScale = controls["mouse_scroll_scale"].get(); - } - if (controls.contains("translation_scale")) - { - out.hasTranslationScale = true; - out.translationScale = controls["translation_scale"].get(); - } - if (controls.contains("rotation_scale")) - { - out.hasRotationScale = true; - out.rotationScale = controls["rotation_scale"].get(); - } -} - -bool parseScriptedSequenceIfPresentJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out, std::string* error) -{ - if (!script.contains("segments")) - return true; - - nbl::core::CCameraSequenceScript sequence; - if (!deserializeCameraSequenceScriptJson(script, sequence, error)) - return false; - - out.sequence = std::move(sequence); - return true; -} - -void appendScriptedCaptureFrame(nbl::system::CCameraScriptedInputParseResult& out, const uint64_t frame, const bool captureFrame) -{ - if (captureFrame) - out.timeline.captureFrames.emplace_back(frame); -} - -void parseScriptedKeyboardEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("key") || !event.contains("action")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event missing \"key\" or \"action\"."); - return; - } - - const auto keyText = event["key"].get(); - const auto actionText = event["action"].get(); - const auto key = parseScriptedKeyCode(keyText); - if (key == nbl::ui::EKC_NONE) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid key \"" + keyText + "\"."); - return; - } - - const auto action = parseScriptedKeyboardAction(actionText); - if (!action.has_value()) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted keyboard event has invalid action \"" + actionText + "\"."); - return; - } - - nbl::system::CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = nbl::system::CCameraScriptedInputEvent::Type::Keyboard; - entry.keyboard.key = key; - entry.keyboard.action = action.value(); - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedCaptureFrame(out, frame, captureFrame); -} - -void parseScriptedMouseEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("kind")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event missing \"kind\"."); - return; - } - - const auto kind = event["kind"].get(); - nbl::system::CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = nbl::system::CCameraScriptedInputEvent::Type::Mouse; - - if (kind == "move") - { - entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Movement; - entry.mouse.dx = event.value("dx", 0); - entry.mouse.dy = event.value("dy", 0); - } - else if (kind == "scroll") - { - entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Scroll; - entry.mouse.v = event.value("v", 0); - entry.mouse.h = event.value("h", 0); - } - else if (kind == "click") - { - if (!event.contains("button") || !event.contains("action")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event missing \"button\" or \"action\"."); - return; - } - - const auto buttonText = event["button"].get(); - const auto actionText = event["action"].get(); - const auto button = parseScriptedMouseButton(buttonText); - if (!button.has_value()) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid button \"" + buttonText + "\"."); - return; - } - - const auto action = parseScriptedMouseClickAction(actionText); - if (!action.has_value()) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted click event has invalid action \"" + actionText + "\"."); - return; - } - - entry.mouse.type = nbl::system::CCameraScriptedInputEvent::MouseData::Type::Click; - entry.mouse.button = button.value(); - entry.mouse.action = action.value(); - entry.mouse.x = event.value("x", 0); - entry.mouse.y = event.value("y", 0); - } - else - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted mouse event has invalid kind \"" + kind + "\"."); - return; - } - - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedCaptureFrame(out, frame, captureFrame); -} - -void parseScriptedImguizmoEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) -{ - nbl::system::CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = nbl::system::CCameraScriptedInputEvent::Type::Imguizmo; - - if (event.contains("delta_trs")) - { - const auto matrix = event["delta_trs"].get>(); - entry.imguizmo = makeScriptedMatrixFromArray(matrix); - } - else - { - const auto translation = event.contains("translation") ? event["translation"].get>() : std::array{0.f, 0.f, 0.f}; - const auto rotation = event.contains("rotation_deg") ? event["rotation_deg"].get>() : std::array{0.f, 0.f, 0.f}; - const auto scale = event.contains("scale") ? event["scale"].get>() : std::array{1.f, 1.f, 1.f}; - entry.imguizmo = composeScriptedImguizmoTransform(translation, rotation, scale); - } - - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedCaptureFrame(out, frame, captureFrame); -} - -int32_t parseScriptedActionIntValue(const json_t& event) -{ - if (event.contains("value")) - return event["value"].get(); - if (event.contains("index")) - return event["index"].get(); - return 0; -} - -bool parseScriptedProjectionActionValue(const json_t& event, nbl::system::CCameraScriptedInputEvent::ActionData& action, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (event.contains("value") && event["value"].is_string()) - { - const auto valueText = event["value"].get(); - if (valueText == "perspective") - action.value = static_cast(nbl::core::IPlanarProjection::CProjection::Perspective); - else if (valueText == "orthographic") - action.value = static_cast(nbl::core::IPlanarProjection::CProjection::Orthographic); - else - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action projection type has invalid value \"" + valueText + "\"."); - return false; - } - } - else - { - action.value = parseScriptedActionIntValue(event); - } - - return true; -} - -void parseScriptedActionEventJson(const json_t& event, const uint64_t frame, const bool captureFrame, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("action")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event missing \"action\"."); - return; - } - - const auto actionText = event["action"].get(); - nbl::system::CCameraScriptedInputEvent entry; - entry.frame = frame; - entry.type = nbl::system::CCameraScriptedInputEvent::Type::Action; - - if (actionText == "set_active_render_window") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActiveRenderWindow; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionText == "set_active_planar") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetActivePlanar; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionText == "set_projection_type") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionType; - if (!parseScriptedProjectionActionValue(event, entry.action, out)) - return; - } - else if (actionText == "set_projection_index") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetProjectionIndex; - entry.action.value = parseScriptedActionIntValue(event); - } - else if (actionText == "set_use_window") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetUseWindow; - entry.action.value = event.value("value", false) ? 1 : 0; - } - else if (actionText == "set_left_handed") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::SetLeftHanded; - entry.action.value = event.value("value", false) ? 1 : 0; - } - else if (actionText == "reset_active_camera") - { - entry.action.kind = nbl::system::CCameraScriptedInputEvent::ActionData::Kind::ResetActiveCamera; - entry.action.value = 1; - } - else - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted action event has invalid action \"" + actionText + "\"."); - return; - } - - out.timeline.events.emplace_back(std::move(entry)); - appendScriptedCaptureFrame(out, frame, captureFrame); -} - -void parseScriptedInputEventJson(const json_t& event, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!event.contains("frame") || !event.contains("type")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event missing \"frame\" or \"type\"."); - return; - } - - const auto frame = event["frame"].get(); - const auto type = event["type"].get(); - const bool captureFrame = event.value("capture", false); - - if (type == "keyboard") - parseScriptedKeyboardEventJson(event, frame, captureFrame, out); - else if (type == "mouse") - parseScriptedMouseEventJson(event, frame, captureFrame, out); - else if (type == "imguizmo") - parseScriptedImguizmoEventJson(event, frame, captureFrame, out); - else if (type == "action") - parseScriptedActionEventJson(event, frame, captureFrame, out); - else - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted input event has invalid type \"" + type + "\"."); -} - -void parseScriptedInputEventsJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!script.contains("events")) - return; - - for (const auto& event : script["events"]) - parseScriptedInputEventJson(event, out); -} - -bool parseScriptedImguizmoVirtualCheckJson(const json_t& check, nbl::system::CCameraScriptedInputCheck& outCheck, nbl::system::CCameraScriptedInputParseResult& out) -{ - outCheck.kind = nbl::system::CCameraScriptedInputCheck::Kind::ImguizmoVirtual; - outCheck.tolerance = check.value("tolerance", outCheck.tolerance); - - if (!check.contains("events")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check missing \"events\"."); - return false; - } - - for (const auto& expectedEvent : check["events"]) - { - if (!expectedEvent.contains("type") || !expectedEvent.contains("magnitude")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event missing \"type\" or \"magnitude\"."); - continue; - } - - const auto typeText = expectedEvent["type"].get(); - const auto type = nbl::core::CVirtualGimbalEvent::stringToVirtualEvent(typeText); - if (type == nbl::core::CVirtualGimbalEvent::None) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Imguizmo virtual check event has invalid type \"" + typeText + "\"."); - continue; - } - - nbl::system::CCameraScriptedInputCheck::ExpectedVirtualEvent expected; - expected.type = type; - expected.magnitude = expectedEvent["magnitude"].get(); - outCheck.expectedVirtualEvents.emplace_back(expected); - } - - return true; -} - -bool parseScriptedCheckJson(const json_t& check, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!check.contains("frame") || !check.contains("kind")) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check missing \"frame\" or \"kind\"."); - return false; - } - - const auto frame = check["frame"].get(); - const auto kind = check["kind"].get(); - - nbl::system::CCameraScriptedInputCheck entry; - entry.frame = frame; - - if (kind == "baseline") - { - entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::Baseline; - } - else if (kind == "imguizmo_virtual") - { - if (!parseScriptedImguizmoVirtualCheckJson(check, entry, out)) - return false; - } - else if (kind == "gimbal_near") - { - entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalNear; - entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); - entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); - - if (check.contains("position")) - { - readVector3(check["position"], entry.expectedPos); - entry.hasExpectedPos = true; - } - if (check.contains("euler_deg")) - { - readVector3(check["euler_deg"], entry.expectedEulerDeg); - entry.hasExpectedEuler = true; - } - } - else if (kind == "gimbal_delta") - { - entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalDelta; - entry.posTolerance = check.value("pos_tolerance", entry.posTolerance); - entry.eulerToleranceDeg = check.value("euler_tolerance_deg", entry.eulerToleranceDeg); - } - else if (kind == "gimbal_step") - { - entry.kind = nbl::system::CCameraScriptedInputCheck::Kind::GimbalStep; - - if (check.contains("min_pos_delta")) - { - entry.minPosDelta = check["min_pos_delta"].get(); - entry.hasPosDeltaConstraint = true; - } - if (check.contains("max_pos_delta")) - { - entry.posTolerance = check["max_pos_delta"].get(); - entry.hasPosDeltaConstraint = true; - } - else if (check.contains("pos_tolerance")) - { - entry.posTolerance = check["pos_tolerance"].get(); - entry.hasPosDeltaConstraint = true; - } - - if (check.contains("min_euler_delta_deg")) - { - entry.minEulerDeltaDeg = check["min_euler_delta_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - if (check.contains("max_euler_delta_deg")) - { - entry.eulerToleranceDeg = check["max_euler_delta_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - else if (check.contains("euler_tolerance_deg")) - { - entry.eulerToleranceDeg = check["euler_tolerance_deg"].get(); - entry.hasEulerDeltaConstraint = true; - } - - if (!entry.hasPosDeltaConstraint && !entry.hasEulerDeltaConstraint) - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "gimbal_step check requires at least one delta constraint."); - return false; - } - } - else - { - nbl::system::CCameraScriptedRuntimePersistenceUtilities::appendScriptedInputParseWarning(out, "Scripted check has invalid kind \"" + kind + "\"."); - return false; - } - - out.timeline.checks.emplace_back(std::move(entry)); - return true; -} - -void parseScriptedChecksJson(const json_t& script, nbl::system::CCameraScriptedInputParseResult& out) -{ - if (!script.contains("checks")) - return; - - for (const auto& check : script["checks"]) - parseScriptedCheckJson(check, out); -} - -namespace nbl::system -{ - -bool readCameraSequenceScript(std::istream& in, core::CCameraSequenceScript& out, std::string* error) -{ - if (!in) - { - if (error) - *error = "Input stream is not readable."; - return false; - } - - json_t root; - in >> root; - return deserializeCameraSequenceScriptJson(root, out, error); -} - -bool readCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error) -{ - std::istringstream stream{std::string(text)}; - return readCameraSequenceScript(stream, out, error); -} - -bool loadCameraSequenceScriptFromFile(ISystem& system, const path& filePath, core::CCameraSequenceScript& out, std::string* error) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open camera sequence script file.")) - return false; - - return readCameraSequenceScript(text, out, error); -} - -bool readCameraScriptedInput(std::istream& in, CCameraScriptedInputParseResult& out, std::string* error) -{ - if (!in) - { - if (error) - *error = "Input stream is not readable."; - return false; - } - - json_t script; - in >> script; - - out = {}; - - if (script.contains("enabled")) - out.enabled = script["enabled"].get(); - if (script.contains("log")) - { - out.hasLog = true; - out.log = script["log"].get(); - } - if (script.contains("hard_fail")) - out.hardFail = script["hard_fail"].get(); - if (script.contains("visual_debug")) - out.visualDebug = script["visual_debug"].get(); - if (script.contains("visual_debug_target_fps")) - out.visualTargetFps = script["visual_debug_target_fps"].get(); - if (script.contains("visual_debug_hold_seconds")) - out.visualCameraHoldSeconds = script["visual_debug_hold_seconds"].get(); - if (script.contains("enableActiveCameraMovement")) - { - out.hasEnableActiveCameraMovement = true; - out.enableActiveCameraMovement = script["enableActiveCameraMovement"].get(); - } - if (script.contains("exclusive_input")) - out.exclusive = script["exclusive_input"].get() || out.exclusive; - if (script.contains("exclusive")) - out.exclusive = script["exclusive"].get() || out.exclusive; - if (script.contains("capture_prefix")) - out.capturePrefix = script["capture_prefix"].get(); - if (out.capturePrefix.empty()) - out.capturePrefix = "script"; - - parseScriptedCaptureFramesJson(script, out); - - if (script.contains("camera_controls")) - parseScriptedControlOverridesJson(script["camera_controls"], out.cameraControls); - - if (!parseScriptedSequenceIfPresentJson(script, out, error)) - return false; - - parseScriptedInputEventsJson(script, out); - parseScriptedChecksJson(script, out); - - CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(out.timeline); - return true; -} - -bool readCameraScriptedInput(std::string_view text, CCameraScriptedInputParseResult& out, std::string* error) -{ - std::istringstream stream{std::string(text)}; - return readCameraScriptedInput(stream, out, error); -} - -bool loadCameraScriptedInputFromFile(ISystem& system, const path& filePath, CCameraScriptedInputParseResult& out, std::string* error) -{ - std::string text; - if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open scripted input file.")) - return false; - - return readCameraScriptedInput(text, out, error); -} - -} // namespace nbl::system diff --git a/src/nbl/ext/Cameras/CCameraSequenceScriptPersistence.cpp b/src/nbl/ext/Cameras/CCameraSequenceScriptPersistence.cpp new file mode 100644 index 0000000000..5d01fdc118 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraSequenceScriptPersistence.cpp @@ -0,0 +1,558 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraSequenceScriptPersistence.hpp" + +#include +#include +#include +#include + +#include "CCameraJsonPersistenceUtilities.hpp" +#include "nbl/ext/Cameras/CCameraFileUtilities.hpp" +#include "nlohmann/json.hpp" + +using json_t = nlohmann::json; + +namespace nbl::system +{ + +namespace impl +{ + +struct CCameraSequenceScriptJsonUtilities final +{ +static bool tryParseCaptureFractionJson(const json_t& entry, float& outFraction) +{ + if (entry.is_number()) + { + outFraction = std::clamp(entry.get(), 0.f, 1.f); + return true; + } + + if (!entry.is_string()) + return false; + + const auto tag = entry.get(); + if (tag == "start") + outFraction = 0.f; + else if (tag == "mid" || tag == "middle") + outFraction = 0.5f; + else if (tag == "end") + outFraction = 1.f; + else + return false; + + return true; +} + +template +static void readVector3(const json_t& entry, T& outValue) +{ + using scalar_t = std::remove_reference_t; + const auto values = entry.get>(); + outValue = T(values[0], values[1], values[2]); +} + +static bool deserializeSequencePresentationsJson(const json_t& root, std::vector& out, std::string* error) +{ + out.clear(); + if (!root.is_array()) + { + if (error) + *error = "Sequence presentations must be an array."; + return false; + } + + for (const auto& entry : root) + { + if (!entry.is_object() || !entry.contains("projection")) + { + if (error) + *error = "Sequence presentation entry missing \"projection\"."; + return false; + } + + nbl::core::CCameraSequencePresentation presentation; + if (!nbl::core::CCameraSequenceScriptUtilities::tryParseProjectionType(entry["projection"].get(), presentation.projection)) + { + if (error) + *error = "Sequence presentation has invalid projection type."; + return false; + } + if (entry.contains("left_handed")) + presentation.leftHanded = entry["left_handed"].get(); + out.emplace_back(presentation); + } + + return true; +} + +static bool deserializeSequenceContinuityJson(const json_t& root, nbl::core::CCameraSequenceContinuitySettings& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence continuity settings must be an object."; + return false; + } + + out = {}; + if (root.contains("baseline")) + out.baseline = root["baseline"].get(); + if (root.contains("step")) + out.step = root["step"].get(); + + if (root.contains("min_pos_delta")) + { + out.minPosDelta = root["min_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + if (root.contains("max_pos_delta")) + { + out.maxPosDelta = root["max_pos_delta"].get(); + out.hasPosDeltaConstraint = true; + } + else if (root.contains("pos_tolerance")) + { + out.maxPosDelta = root["pos_tolerance"].get(); + out.hasPosDeltaConstraint = true; + } + + if (root.contains("min_euler_delta_deg")) + { + out.minEulerDeltaDeg = root["min_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + if (root.contains("max_euler_delta_deg")) + { + out.maxEulerDeltaDeg = root["max_euler_delta_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + else if (root.contains("euler_tolerance_deg")) + { + out.maxEulerDeltaDeg = root["euler_tolerance_deg"].get(); + out.hasEulerDeltaConstraint = true; + } + + if (root.contains("disable_pos_delta")) + out.hasPosDeltaConstraint = !root["disable_pos_delta"].get(); + if (root.contains("disable_euler_delta")) + out.hasEulerDeltaConstraint = !root["disable_euler_delta"].get(); + + if (out.step && !(out.hasPosDeltaConstraint || out.hasEulerDeltaConstraint)) + { + if (error) + *error = "Sequence continuity step checks require at least one delta constraint."; + return false; + } + + return true; +} + +static bool deserializeSequenceGoalDeltaJson(const json_t& root, nbl::core::CCameraSequenceGoalDelta& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe delta must be an object."; + return false; + } + + out = {}; + if (root.contains("position_offset")) + { + readVector3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + if (root.contains("target_offset")) + { + readVector3(root["target_offset"], out.targetOffset); + out.hasTargetOffset = true; + } + if (root.contains("orbit_u_delta_deg")) + out.orbitDelta.setUDeltaDeg(root["orbit_u_delta_deg"].get()); + if (root.contains("orbit_v_delta_deg")) + out.orbitDelta.setVDeltaDeg(root["orbit_v_delta_deg"].get()); + if (root.contains("orbit_distance_delta")) + out.orbitDelta.setDistanceDelta(root["orbit_distance_delta"].get()); + if (root.contains("path_s_delta_deg")) + out.pathDelta.setSDeltaDeg(root["path_s_delta_deg"].get()); + if (root.contains("path_u_delta")) + out.pathDelta.setUDelta(root["path_u_delta"].get()); + if (root.contains("path_v_delta")) + out.pathDelta.setVDelta(root["path_v_delta"].get()); + if (root.contains("path_roll_delta_deg")) + out.pathDelta.setRollDeltaDeg(root["path_roll_delta_deg"].get()); + if (root.contains("dynamic_base_fov_delta")) + { + out.dynamicBaseFovDelta = root["dynamic_base_fov_delta"].get(); + out.hasDynamicBaseFovDelta = true; + } + if (root.contains("dynamic_reference_distance_delta")) + { + out.dynamicReferenceDistanceDelta = root["dynamic_reference_distance_delta"].get(); + out.hasDynamicReferenceDistanceDelta = true; + } + + return true; +} + +static bool deserializeSequenceKeyframeJson(const json_t& root, nbl::core::CCameraSequenceKeyframe& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceGoalDeltaJson(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("preset")) + { + impl::CCameraJsonPersistenceUtilities::deserializePresetJson(root["preset"], out.absolutePreset); + out.hasAbsolutePreset = true; + } + else if (root.contains("position") || root.contains("orientation") || root.contains("target_position") || + root.contains("distance") || root.contains("orbit_u") || root.contains("orbit_v") || + root.contains("orbit_distance") || root.contains("path_s") || root.contains("path_u") || + root.contains("path_v") || root.contains("path_roll") || + root.contains("dynamic_base_fov") || root.contains("dynamic_reference_distance")) + { + impl::CCameraJsonPersistenceUtilities::deserializePresetJson(root, out.absolutePreset); + out.hasAbsolutePreset = true; + } + + return true; +} + +static bool deserializeSequenceTrackedTargetDeltaJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetDelta& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target delta must be an object."; + return false; + } + + out = {}; + if (root.contains("position_offset")) + { + readVector3(root["position_offset"], out.positionOffset); + out.hasPositionOffset = true; + } + if (root.contains("rotation_euler_deg_offset")) + { + readVector3(root["rotation_euler_deg_offset"], out.rotationEulerDegOffset); + out.hasRotationEulerDegOffset = true; + } + + return true; +} + +static bool deserializeSequenceTrackedTargetKeyframeJson(const json_t& root, nbl::core::CCameraSequenceTrackedTargetKeyframe& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence target keyframe must be an object."; + return false; + } + + out = {}; + if (root.contains("time")) + out.time = std::max(0.f, root["time"].get()); + + if (root.contains("delta")) + { + if (!deserializeSequenceTrackedTargetDeltaJson(root["delta"], out.delta, error)) + return false; + out.hasDelta = true; + } + + if (root.contains("position")) + { + readVector3(root["position"], out.absolutePosition); + out.hasAbsolutePosition = true; + } + if (root.contains("rotation_euler_deg")) + { + readVector3(root["rotation_euler_deg"], out.absoluteRotationEulerDeg); + out.hasAbsoluteRotationEulerDeg = true; + } + + return true; +} + +static bool deserializeSequenceSegmentJson(const json_t& root, nbl::core::CCameraSequenceSegment& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Sequence segment must be an object."; + return false; + } + + out = {}; + if (root.contains("name")) + out.name = root["name"].get(); + if (root.contains("camera_identifier")) + out.cameraIdentifier = root["camera_identifier"].get(); + if (root.contains("camera_kind")) + { + if (!nbl::core::CCameraSequenceScriptUtilities::tryParseCameraKind(root["camera_kind"].get(), out.cameraKind)) + { + if (error) + *error = "Sequence segment has invalid camera_kind."; + return false; + } + } + if (root.contains("duration_seconds")) + { + out.durationSeconds = std::max(0.f, root["duration_seconds"].get()); + out.hasDurationSeconds = true; + } + if (root.contains("reset_camera")) + { + out.resetCamera = root["reset_camera"].get(); + out.hasResetCamera = true; + } + if (root.contains("presentations")) + { + if (!deserializeSequencePresentationsJson(root["presentations"], out.presentations, error)) + return false; + } + if (root.contains("continuity")) + { + if (!deserializeSequenceContinuityJson(root["continuity"], out.continuity, error)) + return false; + out.hasContinuity = true; + } + if (root.contains("captures")) + { + if (!root["captures"].is_array()) + { + if (error) + *error = "Sequence segment captures must be an array."; + return false; + } + + out.captureFractions.clear(); + for (const auto& entry : root["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFractionJson(entry, fraction)) + { + if (error) + *error = "Sequence segment capture entry is invalid."; + return false; + } + out.captureFractions.emplace_back(fraction); + } + nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.captureFractions); + out.hasCaptureFractions = true; + } + if (root.contains("keyframes")) + { + if (!root["keyframes"].is_array()) + { + if (error) + *error = "Sequence segment keyframes must be an array."; + return false; + } + for (const auto& entry : root["keyframes"]) + { + nbl::core::CCameraSequenceKeyframe keyframe; + if (!deserializeSequenceKeyframeJson(entry, keyframe, error)) + return false; + out.keyframes.emplace_back(std::move(keyframe)); + } + } + if (root.contains("target_keyframes")) + { + if (!root["target_keyframes"].is_array()) + { + if (error) + *error = "Sequence segment target_keyframes must be an array."; + return false; + } + for (const auto& entry : root["target_keyframes"]) + { + nbl::core::CCameraSequenceTrackedTargetKeyframe keyframe; + if (!deserializeSequenceTrackedTargetKeyframeJson(entry, keyframe, error)) + return false; + out.targetKeyframes.emplace_back(std::move(keyframe)); + } + } + + if (out.keyframes.empty()) + { + if (error) + *error = "Sequence segment requires at least one keyframe."; + return false; + } + if (out.cameraKind == nbl::core::ICamera::CameraKind::Unknown && out.cameraIdentifier.empty()) + { + if (error) + *error = "Sequence segment requires camera_kind or camera_identifier."; + return false; + } + + return true; +} + +static bool deserializeCameraSequenceScriptJson(const json_t& root, nbl::core::CCameraSequenceScript& out, std::string* error) +{ + if (!root.is_object()) + { + if (error) + *error = "Camera sequence script must be an object."; + return false; + } + + out = {}; + if (root.contains("enabled")) + out.enabled = root["enabled"].get(); + if (root.contains("log")) + out.log = root["log"].get(); + if (root.contains("exclusive")) + out.exclusive = root["exclusive"].get(); + if (root.contains("exclusive_input")) + out.exclusive = root["exclusive_input"].get() || out.exclusive; + if (root.contains("hard_fail")) + out.hardFail = root["hard_fail"].get(); + if (root.contains("visual_debug")) + out.visualDebug = root["visual_debug"].get(); + if (root.contains("visual_debug_target_fps")) + out.visualDebugTargetFps = root["visual_debug_target_fps"].get(); + if (root.contains("visual_debug_hold_seconds")) + out.visualDebugHoldSeconds = root["visual_debug_hold_seconds"].get(); + if (root.contains("enableActiveCameraMovement")) + { + out.enableActiveCameraMovement = root["enableActiveCameraMovement"].get(); + out.hasEnableActiveCameraMovement = true; + } + if (root.contains("capture_prefix")) + out.capturePrefix = root["capture_prefix"].get(); + if (root.contains("fps")) + out.fps = std::max(1.f, root["fps"].get()); + + if (root.contains("defaults")) + { + const auto& defaults = root["defaults"]; + if (!defaults.is_object()) + { + if (error) + *error = "Camera sequence defaults must be an object."; + return false; + } + + if (defaults.contains("duration_seconds")) + out.defaults.durationSeconds = std::max(0.f, defaults["duration_seconds"].get()); + if (defaults.contains("reset_camera")) + out.defaults.resetCamera = defaults["reset_camera"].get(); + if (defaults.contains("presentations")) + { + if (!deserializeSequencePresentationsJson(defaults["presentations"], out.defaults.presentations, error)) + return false; + } + if (defaults.contains("continuity")) + { + if (!deserializeSequenceContinuityJson(defaults["continuity"], out.defaults.continuity, error)) + return false; + } + if (defaults.contains("captures")) + { + if (!defaults["captures"].is_array()) + { + if (error) + *error = "Camera sequence default captures must be an array."; + return false; + } + + out.defaults.captureFractions.clear(); + for (const auto& entry : defaults["captures"]) + { + float fraction = 0.f; + if (!tryParseCaptureFractionJson(entry, fraction)) + { + if (error) + *error = "Camera sequence default capture entry is invalid."; + return false; + } + out.defaults.captureFractions.emplace_back(fraction); + } + nbl::core::CCameraSequenceScriptUtilities::normalizeCaptureFractions(out.defaults.captureFractions); + } + } + + if (!root.contains("segments") || !root["segments"].is_array()) + { + if (error) + *error = "Camera sequence script requires a \"segments\" array."; + return false; + } + + for (const auto& entry : root["segments"]) + { + nbl::core::CCameraSequenceSegment segment; + if (!deserializeSequenceSegmentJson(entry, segment, error)) + return false; + out.segments.emplace_back(std::move(segment)); + } + + if (out.segments.empty()) + { + if (error) + *error = "Camera sequence script must contain at least one segment."; + return false; + } + + return true; +} + +}; // struct CCameraSequenceScriptJsonUtilities + +} // namespace impl + +bool CCameraSequenceScriptPersistenceUtilities::deserializeCameraSequenceScript(std::string_view text, core::CCameraSequenceScript& out, std::string* error) +{ + try + { + const auto root = json_t::parse(text); + return impl::CCameraSequenceScriptJsonUtilities::deserializeCameraSequenceScriptJson(root, out, error); + } + catch (const json_t::exception& e) + { + if (error) + *error = e.what(); + return false; + } +} + +bool CCameraSequenceScriptPersistenceUtilities::loadCameraSequenceScriptFromFile(ISystem& system, const path& filePath, core::CCameraSequenceScript& out, std::string* error) +{ + std::string text; + if (!CCameraFileUtilities::readTextFile(system, filePath, text, error, "Cannot open camera sequence script file.")) + return false; + + return deserializeCameraSequenceScript(text, out, error); +} + +} // namespace nbl::system diff --git a/src/nbl/ext/Cameras/CMakeLists.txt b/src/nbl/ext/Cameras/CMakeLists.txt index 1184afeeca..5e978def69 100644 --- a/src/nbl/ext/Cameras/CMakeLists.txt +++ b/src/nbl/ext/Cameras/CMakeLists.txt @@ -6,7 +6,7 @@ file(GLOB NBL_EXT_CAMERAS_HEADERS CONFIGURE_DEPENDS set(NBL_EXT_CAMERAS_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/CCameraPersistence.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraScriptedRuntimePersistence.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraSequenceScriptPersistence.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" ) From 5b0e78c60858d9e19a33a49fa654bee5654ebbf2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 14:36:20 +0200 Subject: [PATCH 144/161] Polish cameras README --- include/nbl/ext/Cameras/README.md | 801 ++++++++++++++++++------------ 1 file changed, 491 insertions(+), 310 deletions(-) diff --git a/include/nbl/ext/Cameras/README.md b/include/nbl/ext/Cameras/README.md index cfc9038d7a..a08722e491 100644 --- a/include/nbl/ext/Cameras/README.md +++ b/include/nbl/ext/Cameras/README.md @@ -16,11 +16,12 @@ If you want to know which type to touch first, use this table. | I want to... | Use | |---|---| +| move a camera from live input this frame | `ICamera::manipulate(...)` | +| convert keyboard or mouse input into camera commands | `IGimbalInputProcessor` or `CGimbalInputBinder` | +| pair one camera with one or more projection entries | `CPlanarProjection` and `IPlanarProjection::CProjection` | | apply one absolute rigid pose request at runtime | `camera->manipulate({}, &referenceFrame)` | | set exact position or exact orientation on `Free` and `FPS` | `referenceFrame` built from `camera->getGimbal()` | | set one absolute typed state that can be reused later | `CCameraGoal` + `CCameraGoalSolver` | -| move a camera from live input this frame | `ICamera::manipulate(...)` | -| convert keyboard or mouse input into camera commands | `IGimbalInputProcessor` or `CGimbalInputBinder` | | capture current camera state | `CCameraGoalSolver::capture...` | | restore a camera from typed state | `CCameraGoalSolver::apply...` | | save a named camera state | `CCameraPreset` | @@ -35,7 +36,189 @@ If you want to know which type to touch first, use this table. This section shows the common entry points before any deeper explanation. -### 1. Apply one absolute rigid pose request +### 1. Live runtime camera control + +Use this when keyboard, mouse, or ImGuizmo should move the camera right now. + +```cpp +auto camera = core::make_smart_refctd_ptr(eye, target); + +ui::CGimbalInputBinder binder; +ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); + +auto collected = binder.collectVirtualEvents(timestamp, { + .keyboardEvents = { keyEvents.data(), keyEvents.size() }, + .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, + // .imguizmoEvents = { gizmoDeltaTransforms.data(), gizmoDeltaTransforms.size() }, +}); + +camera->manipulate(collected.events); +``` + +The update payload currently accepts: + +- `keyboardEvents` +- `mouseEvents` +- `imguizmoEvents` + +What happens here: + +1. device input is converted into semantic camera commands +2. the camera consumes those commands through `manipulate(...)` +3. the camera updates its gimbal pose + +The controller-side stack is: + +- `IGimbalBindingLayout` for the static mapping from device inputs to virtual events +- `IGimbalInputProcessor` for converting one frame of raw input into event magnitudes +- `CGimbalInputBinder` for the common runtime object that owns a layout and collects one frame of events +- `CCameraInputBindingUtilities` for shared preset layouts such as default `FPS`, `Orbit`, or `Path Rig` bindings + +The two common ways to start are: + +- apply one shared preset for a camera family +- write one binding layout explicitly + +**Question: How do I bind `FPS` to `WASD`?** + +Use the shared default binding preset for the active camera kind. + +```cpp +auto camera = core::make_smart_refctd_ptr(position, orientation); + +ui::CGimbalInputBinder binder; +ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); +``` + +For `FPS`, the default preset gives you: + +- keyboard `W/S/A/D` -> forward, backward, left, right +- keyboard `I/K/J/L` -> tilt up, tilt down, pan left, pan right +- mouse relative movement -> look yaw and pitch + +For `Free`, the default preset adds `Q/E` for roll. + +For target-relative families and `Path Rig`, the default preset keeps the same physical inputs but maps them to the legal state space of that family. + +**Question: How do I define custom bindings?** + +Use one `IGimbalBindingLayout` implementation such as `CGimbalInputBinder` and write the mapping you want. + +```cpp +ui::CGimbalInputBinder binder; +const double customMoveGain = /* choose a sensitivity for this binding */; + +binder.updateKeyboardMapping([customMoveGain](auto& map) +{ + map.clear(); + map.emplace(ui::E_KEY_CODE::EKC_W, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveForward, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_S, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveBackward, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_A, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveLeft, customMoveGain)); + map.emplace(ui::E_KEY_CODE::EKC_D, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveRight, customMoveGain)); +}); +``` + +The same pattern works for: + +- mouse bindings through `updateMouseMapping(...)` +- ImGuizmo bindings through `updateImguizmoMapping(...)` + +**Question: How are `magnitude` values generated?** + +`CVirtualGimbalEvent::magnitude` is one non-negative scalar attached to one semantic command. + +It is not a raw device unit and it is not, by itself, the final world-space or angular motion applied by a camera. + +What stays stable at the API level is the meaning by event family: + +- translation events carry one controller-side translation amount +- rotation events carry one controller-side angular amount +- scale events carry one controller-side scale amount + +The binding layer maps raw producer values onto those amounts. Different sources may start from: + +- elapsed time for held input +- cursor deltas for relative mouse input +- scroll steps for wheel input +- world-space translation or angular deltas for gizmo-driven input + +That means exact numeric gains are binding policy, not API contract. The binding layer owns sensitivity and repeat-rate tuning. + +After the controller side emits virtual magnitudes, the camera runtime applies its own motion scales and legalizes the result to the concrete camera family. + +The motion pipeline is therefore: + +1. raw device input +2. binding-local gain +3. `CVirtualGimbalEvent { type, magnitude }` +4. camera-local motion scale +5. family-specific legalization and state update + +### 2. Projection is separate from camera state + +**Question: Where is `setProjectionMatrix(...)`?** + +There is no `camera->setProjectionMatrix(...)`. + +That is intentional. + +The camera API keeps runtime camera state and projection state separate: + +- `ICamera` owns pose and motion state +- `IProjection` and its derived types own projection state +- one projection wrapper references one camera when it needs `view`, `MV`, or `MVP` + +This keeps the pairing flexible: + +- one camera can be reused with different projection entries +- one viewport can switch projection preset without replacing the camera +- projection parameters such as FOV, orthographic width, near, and far do not have to live inside every camera kind + +The split looks like this: + +```cpp +auto camera = core::make_smart_refctd_ptr(eye, target); + +using projections_t = std::vector; +auto planar = core::CPlanarProjection::create(core::smart_refctd_ptr(camera)); + +planar->getPlanarProjections().push_back( + core::IPlanarProjection::CProjection::create< + core::IPlanarProjection::CProjection::Perspective>(0.1f, 100.0f, 60.0f)); + +planar->getPlanarProjections().push_back( + core::IPlanarProjection::CProjection::create< + core::IPlanarProjection::CProjection::Orthographic>(0.1f, 100.0f, 10.0f)); + +auto& projection = planar->getPlanarProjections()[0]; +projection.update(leftHanded, aspectRatio); + +const auto& view = camera->getGimbal().getViewMatrix(); +const auto& proj = projection.getProjectionMatrix(); +``` + +So the camera does not own projection parameters. + +Instead: + +- the camera owns `view` +- the projection entry owns `projection` +- the wrapper combines both when code needs `MV`, `MVP`, or viewport-local binding state + +When you want to change projection state, touch the projection layer: + +- `IPlanarProjection::CProjection::setPerspective(...)` +- `IPlanarProjection::CProjection::setOrthographic(...)` +- `IPlanarProjection::CProjection::update(...)` + +When you want to change pose or camera-family state, touch the camera layer: + +- `ICamera::manipulate(...)` +- `referenceFrame` +- `CCameraGoal` +- family-specific typed hooks such as `trySetSphericalTarget(...)` or `trySetPathState(...)` + +### 3. Apply one absolute rigid pose request Use this when you already have one rigid transform and want the camera to consume it through the normal runtime entry point. @@ -43,10 +226,21 @@ Use this when you already have one rigid transform and want the camera to consum const auto referenceFrame = hlsl::CCameraMathUtilities::composeTransformMatrix(desiredPosition, desiredOrientation); -camera->manipulate({}, &referenceFrame); +if (camera->manipulate({}, &referenceFrame)) +{ + // reference frame was accepted and applied +} ``` -#### Why not just expose `setPosition(...)` and `setOrientation(...)` everywhere? +`manipulate(...)` can return `false`. + +Common reasons are: + +- there were no virtual events and no `referenceFrame` +- the supplied `referenceFrame` was not a valid rigid orthonormal transform +- the concrete camera kind could not legalize the request into its own runtime state + +**Question: Why not just expose `setPosition(...)` and `setOrientation(...)` everywhere?** Because not every camera kind stores arbitrary rigid pose as its native state. @@ -88,6 +282,8 @@ For `FPS` that means: - reject arbitrary roll - write back one upright `FPS` pose +For `FPS`, that rejection applies to `referenceFrame`, not to stray roll virtual events. `CFPSCamera` advertises only translation plus pitch/yaw runtime control, so `RollLeft` and `RollRight` events are ignored by the `FPS` accumulator instead of causing failure. + The same pattern applies to every camera family: - `Free` keeps the rigid pose directly @@ -95,17 +291,13 @@ The same pattern applies to every camera family: - target-relative cameras legalize to `target + orbitUv + distance` - `Path Rig` legalizes to `PathState` -That is why `camera->manipulate({}, &referenceFrame)` is the shared absolute runtime path. - -It accepts one rigid pose request at the API boundary and lets each camera family legalize it according to its own runtime model. - Use this path for: - one-shot runtime pose application - ImGuizmo - world-space or local-space pose anchoring -### 2. Set exact position or exact orientation on `Free` and `FPS` +### 4. Set exact position or exact orientation on `Free` and `FPS` Use this when the target camera is `Free` or `FPS` and you want to replace only one rigid-pose component. @@ -137,9 +329,9 @@ camera->manipulate({}, &referenceFrame); `FPS` keeps the exact position but legalizes orientation to its upright `pitch/yaw` state. -Do not describe this path as exact position-only or exact orientation-only for constrained target-relative or path cameras. Those cameras legalize the rigid pose request into their own family state. +For constrained target-relative and path cameras, prefer family-specific typed state or `CCameraGoal` instead of describing this as an exact component setter. -### 3. Set one absolute typed state +### 5. Set one absolute typed state Use this when the state should survive beyond one frame or should be reused by presets, follow, playback, persistence, or scripts. @@ -150,14 +342,70 @@ goal.orientation = desiredOrientation; core::CCameraGoalSolver solver; auto apply = solver.applyDetailed(camera.get(), goal); + +if (apply.succeeded() && apply.changed()) +{ + // camera was updated during applyDetailed(...) +} ``` +`applyDetailed(...)` does not build a deferred command object. +It immediately tries to apply `goal` to the runtime camera. + +The returned `apply` value is a report describing what happened: + +- whether the apply succeeded +- whether the camera actually changed +- whether the result was exact or approximate +- whether typed state was applied directly, virtual events were replayed, or both + +So the control flow is: + +1. build one `CCameraGoal` +2. call `applyDetailed(...)` +3. the solver immediately updates the camera if it can +4. inspect `apply` if you need status, exactness, or diagnostics + +Use `applyDetailed(...)` when you want that report. +Use `apply(...)` when you only need a plain success/failure boolean. + +**Question: How is this different from `composeTransformMatrix(...)` plus `camera->manipulate({}, &referenceFrame)`?** + +`referenceFrame` carries one rigid pose request: + +- position +- orientation + +That is enough when the job is "try to place the runtime camera at this pose now". + +`CCameraGoal` can carry more than one rigid pose: + +- pose +- target-relative state +- path state +- dynamic perspective state +- source metadata used by tooling + +So the two paths are different: + +- `referenceFrame` asks the runtime camera to legalize one rigid pose request +- `CCameraGoal` asks the solver to apply one typed camera state, using direct typed hooks when available and virtual-event replay when needed + +For `Free` and often `FPS`, both paths may end up close to the same result. + +For constrained families they are not equivalent, because one rigid pose does not fully describe family-specific state such as: + +- target position +- orbit angles plus distance +- `PathState` +- dynamic perspective parameters + Rule of thumb: - use `referenceFrame` for one runtime rigid pose request now - use `CCameraGoal` for one typed camera state that should be stored, compared, serialized, replayed, or applied later -### 4. Set one absolute camera-family state +### 6. Set one absolute camera-family state Use this when you do not want a generic rigid pose and instead want to write the native state of one camera family. @@ -187,122 +435,7 @@ Use this path when you already have: - path-rig state - one other family-specific typed fragment exposed by `ICamera` -### 5. Live runtime camera control - -Use this when keyboard, mouse, or ImGuizmo should move the camera right now. - -```cpp -auto camera = core::make_smart_refctd_ptr(eye, target); - -ui::CGimbalInputBinder binder; -ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); - -auto collected = binder.collectVirtualEvents(timestamp, { - .mouseEvents = { mouseEvents.data(), mouseEvents.size() }, - .keyboardEvents = { keyEvents.data(), keyEvents.size() } -}); - -camera->manipulate(collected.events); -``` - -What happens here: - -1. device input is converted into semantic camera commands -2. the camera consumes those commands through `manipulate(...)` -3. the camera updates its gimbal pose - -Main types involved: - -- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) -- [`IGimbalBindingLayout.hpp`](IGimbalBindingLayout.hpp) -- [`IGimbalInputProcessor.hpp`](IGimbalInputProcessor.hpp) -- [`CGimbalInputBinder.hpp`](CGimbalInputBinder.hpp) -- [`CCameraInputBindingUtilities.hpp`](CCameraInputBindingUtilities.hpp) -- [`ICamera.hpp`](ICamera.hpp) - -The controller-side stack is: - -- `IGimbalBindingLayout` for the static mapping from device inputs to virtual events -- `IGimbalInputProcessor` for converting one frame of raw input into event magnitudes -- `CGimbalInputBinder` for the common runtime object that owns a layout and collects one frame of events -- `CCameraInputBindingUtilities` for shared preset layouts such as default `FPS`, `Orbit`, or `Path Rig` bindings - -#### How do I bind `FPS` to `WASD`? - -Use the shared default binding preset for the active camera kind. - -```cpp -auto camera = core::make_smart_refctd_ptr(position, orientation); - -ui::CGimbalInputBinder binder; -ui::CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(binder, *camera); -``` - -For `FPS`, the default preset gives you: - -- keyboard `W/S/A/D` -> forward, backward, left, right -- keyboard `I/K/J/L` -> tilt up, tilt down, pan left, pan right -- mouse relative movement -> look yaw and pitch - -For `Free`, the default preset adds `Q/E` for roll. - -For target-relative families and `Path Rig`, the default preset keeps the same physical inputs but maps them to the legal state space of that family. - -#### How do I make my own bindings? - -Use one `IGimbalBindingLayout` implementation such as `CGimbalInputBinder` and write the mapping you want. - -```cpp -ui::CGimbalInputBinder binder; -const double customMoveGain = /* choose a sensitivity for this binding */; - -binder.updateKeyboardMapping([customMoveGain](auto& map) -{ - map.clear(); - map.emplace(ui::E_KEY_CODE::EKC_W, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveForward, customMoveGain)); - map.emplace(ui::E_KEY_CODE::EKC_S, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveBackward, customMoveGain)); - map.emplace(ui::E_KEY_CODE::EKC_A, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveLeft, customMoveGain)); - map.emplace(ui::E_KEY_CODE::EKC_D, ui::IGimbalBindingLayout::CHashInfo(core::CVirtualGimbalEvent::MoveRight, customMoveGain)); -}); -``` - -The same pattern works for: - -- mouse bindings through `updateMouseMapping(...)` -- ImGuizmo bindings through `updateImguizmoMapping(...)` - -#### How are `magnitude` values generated? - -`CVirtualGimbalEvent::magnitude` is one non-negative scalar attached to one semantic command. - -It is not a raw device unit and it is not, by itself, the final world-space or angular motion applied by a camera. - -What stays stable at the API level is the meaning by event family: - -- translation events carry one controller-side translation amount -- rotation events carry one controller-side angular amount -- scale events carry one controller-side scale amount - -The binding layer maps raw producer values onto those amounts. Different sources may start from: - -- elapsed time for held input -- cursor deltas for relative mouse input -- scroll steps for wheel input -- world-space translation or angular deltas for gizmo-driven input - -That means exact numeric gains are binding policy, not API contract. The binding layer owns sensitivity and repeat-rate tuning. - -After the controller side emits virtual magnitudes, the camera runtime applies its own motion scales and legalizes the result to the concrete camera family. - -The motion pipeline is therefore: - -1. raw device input -2. binding-local gain -3. `CVirtualGimbalEvent { type, magnitude }` -4. camera-local motion scale -5. family-specific legalization and state update - -### 6. Capture a camera and restore it later +### 7. Capture a camera and restore it later Use this when you want explicit camera state instead of one-frame runtime input. @@ -322,12 +455,7 @@ What happens here: 2. the solver writes that state into one `CCameraGoal` 3. the solver later applies that goal back to a camera -Main types involved: - -- [`CCameraGoal.hpp`](CCameraGoal.hpp) -- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) - -### 7. Save a named camera state +### 8. Save a named camera state Use this when one camera state needs a user-facing name or identifier. @@ -344,12 +472,7 @@ if (capture.canUseGoal()) } ``` -Main types involved: - -- [`CCameraPreset.hpp`](CCameraPreset.hpp) -- [`CCameraPresetFlow.hpp`](CCameraPresetFlow.hpp) - -### 8. Make a camera follow a moving target +### 9. Make a camera follow a moving target Use this when one tracked subject should drive camera behavior. @@ -364,34 +487,20 @@ core::CCameraGoalSolver solver; core::CCameraFollowUtilities::applyFollowToCamera(solver, camera.get(), trackedTarget, follow); ``` -Main types involved: - -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) - -### 9. Build and evaluate scripted runtime payloads +### 10. Build and evaluate scripted runtime payloads Use this when camera playback is authored as compact camera-domain data and then evaluated through generic per-frame runtime payloads and checks. ```cpp -system::CCameraScriptedTimeline timeline; -system::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); +core::CCameraScriptedTimeline timeline; +core::CCameraScriptedRuntimeUtilities::finalizeScriptedTimeline(timeline); ``` -Main types involved: - -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) -- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) -- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) - ## Core concepts ### `CVirtualGimbalEvent` -File: - -- [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp) +Defined in [`CVirtualGimbalEvent.hpp`](CVirtualGimbalEvent.hpp). `CVirtualGimbalEvent` is one semantic camera command plus one scalar magnitude. @@ -414,10 +523,7 @@ The same event type can come from keyboard input, mouse input, ImGuizmo, scripte ### `IGimbal` -Files: - -- [`IGimbal.hpp`](IGimbal.hpp) -- [`ICamera.hpp`](ICamera.hpp) +Defined in [`IGimbal.hpp`](IGimbal.hpp) and used by [`ICamera.hpp`](ICamera.hpp). The gimbal stores runtime pose: @@ -434,9 +540,7 @@ Every runtime camera owns one `CGimbal`. ### `ICamera` -File: - -- [`ICamera.hpp`](ICamera.hpp) +Defined in [`ICamera.hpp`](ICamera.hpp). `ICamera` is the shared runtime interface implemented by every camera kind. @@ -461,10 +565,7 @@ Those scales are applied after the binding layer emits virtual magnitudes. ### `referenceFrame` -Files: - -- [`ICamera.hpp`](ICamera.hpp) -- [`IGimbal.hpp`](IGimbal.hpp) +Defined by [`ICamera.hpp`](ICamera.hpp) and [`IGimbal.hpp`](IGimbal.hpp). `referenceFrame` is the optional rigid transform passed to `ICamera::manipulate(...)`. @@ -477,9 +578,7 @@ Typical producers: - replay helpers - code that wants world-space or local-space manipulation anchored to a specific rigid transform -When you already have one absolute rigid pose, `referenceFrame` is the direct runtime entry point for requesting that pose through the runtime camera path. - -See Quick start sections 1 and 2 for the concrete absolute-pose usage patterns. +See Quick start sections 1 to 4 for the concrete runtime usage patterns. Shared runtime pattern: @@ -494,9 +593,7 @@ referenceFrame ### `SCameraRigPose` -File: - -- [`SCameraRigPose.hpp`](SCameraRigPose.hpp) +Defined in [`SCameraRigPose.hpp`](SCameraRigPose.hpp). `SCameraRigPose` stores only: @@ -507,9 +604,7 @@ It is the smallest typed pose object reused across the stack. ### `CCameraGoal` -File: - -- [`CCameraGoal.hpp`](CCameraGoal.hpp) +Defined in [`CCameraGoal.hpp`](CCameraGoal.hpp). `CCameraGoal` is the canonical typed transport for camera state. @@ -536,8 +631,6 @@ It is used by: - follow - scripted checks -When you want to set a camera absolutely in a reusable, serializable, or comparable way, `CCameraGoal` is the main public state object for that job. - It is not: - a live input object @@ -548,9 +641,7 @@ For constrained cameras, the solver may project the goal onto legal camera-famil ### `CCameraGoalSolver` -File: - -- [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp) +Defined in [`CCameraGoalSolver.hpp`](CCameraGoalSolver.hpp). `CCameraGoalSolver` converts between typed camera state and runtime cameras. @@ -560,9 +651,7 @@ If you want to restore one absolute camera state and you are not sure which fami ### `CCameraPreset` -File: - -- [`CCameraPreset.hpp`](CCameraPreset.hpp) +Defined in [`CCameraPreset.hpp`](CCameraPreset.hpp). `CCameraPreset` is a named saved `CCameraGoal`. @@ -574,9 +663,7 @@ It contains: ### `CCameraKeyframeTrack` -File: - -- [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp) +Defined in [`CCameraKeyframeTrack.hpp`](CCameraKeyframeTrack.hpp). `CCameraKeyframeTrack` is a sequence of time-stamped presets. @@ -587,9 +674,7 @@ Each keyframe contains: ### `CCameraPlaybackTimeline` -File: - -- [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp) +Defined in [`CCameraPlaybackTimeline.hpp`](CCameraPlaybackTimeline.hpp). `CCameraPlaybackTimeline` stores playback cursor state over time-based camera data. @@ -602,9 +687,7 @@ It tracks things such as: ### `CTrackedTarget` -File: - -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) +Defined in [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp). `CTrackedTarget` is the reusable tracked subject used by follow. @@ -613,9 +696,7 @@ It is not a mesh id and not a scene-node handle. ### `CCameraSequenceScript` -File: - -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) +Defined in [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp). `CCameraSequenceScript` is the compact authored format for camera sequences. @@ -632,9 +713,7 @@ It does not store frame-by-frame low-level input. ### `CCameraScriptedRuntime` -File: - -- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) +Defined in [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp). `CCameraScriptedRuntime` is the expanded executable form used during scripted playback and validation. @@ -649,11 +728,7 @@ Consumer-specific UI actions stay outside this shared runtime payload. ### `Path Rig` -Files: - -- [`CPathCamera.hpp`](CPathCamera.hpp) -- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) -- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) +Defined by [`CPathCamera.hpp`](CPathCamera.hpp), [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp), and [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp). `Path Rig` is the camera family with typed state: @@ -664,144 +739,265 @@ Files: Its runtime and typed tooling are driven by `SCameraPathModel`, which defines how path state is resolved, updated, and converted back into camera pose. -## Camera families +At the API boundary, you can think of `Path Rig` as: -### Free cameras +$$ +\text{choose any parametric camera function } f +\text{ that maps typed path state to pose.} +$$ -Files: +In other words, the reusable seam is not "one built-in rail camera". +It is: -- [`CFPSCamera.hpp`](CFPSCamera.hpp) -- [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) +$$ +f : (t, q, L) \mapsto (p, o) +$$ -State: +with: -- world-space position -- orientation or FPS-constrained yaw/pitch orientation +- $t \in \mathbb{R}^3$: + world-space target or anchor position used by the model +- $q \in \mathcal{Q}$: + typed path state +- $L \in \mathcal{L}$: + path-state limits +- $p \in \mathbb{R}^3$: + evaluated world-space camera position +- $o \in \mathrm{SO}(3)$: + evaluated camera orientation -Typical use: +In the shared built-in state representation, -- free-fly navigation -- direct pose-driven manipulation +$$ +\mathcal{Q} = S^1 \times \mathbb{R} \times \mathbb{R} \times S^1 +$$ -### Target-relative cameras +with -Base: +$$ +q = (s, u, v, \rho), +$$ -- [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) +where: -Derived: +- $s \in S^1$ is one wrapped angular parameter +- $u \in \mathbb{R}$ is one lateral or radial parameter +- $v \in \mathbb{R}$ is one second shape or height parameter +- $\rho \in S^1$ is authored roll around the model forward axis -- [`COrbitCamera.hpp`](COrbitCamera.hpp) -- [`CArcballCamera.hpp`](CArcballCamera.hpp) -- [`CTurntableCamera.hpp`](CTurntableCamera.hpp) -- [`CTopDownCamera.hpp`](CTopDownCamera.hpp) -- [`CIsometricCamera.hpp`](CIsometricCamera.hpp) -- [`CChaseCamera.hpp`](CChaseCamera.hpp) -- [`CDollyCamera.hpp`](CDollyCamera.hpp) -- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) +and the limit bundle is -Shared state: +$$ +\mathcal{L} = \{(u_{\min}, d_{\min}, d_{\max})\}, +$$ -- target position -- `orbitUv` -- distance +where: -These cameras resolve pose through target-relative state instead of arbitrary free pose. +- $u_{\min} \in \mathbb{R}_{\ge 0}$ is the minimal legal `u` +- $d_{\min} \in \mathbb{R}_{\ge 0}$ is the minimal legal radial distance +- $d_{\max} \in \mathbb{R}_{\ge 0} \cup \{\infty\}$ is the maximal legal radial distance -### DollyZoom +The important part is that one caller-provided model decides how typed state becomes final camera pose. -File: +This is why the same API seam can model many constrained motions: circles, cylinders, orbits, guide curves, splines with offsets, crane-style rigs, banking on-rails cameras, and other custom parametric camera laws. -- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) +If you already have one path curve -This camera adds dynamic perspective state on top of target-relative state. +$$ +C(s) \in \mathbb{R}^3 +$$ -Typed dynamic perspective state: +and one moving local frame -- `baseFov` -- `referenceDistance` +$$ +R(s), U(s), F(s) \in \mathbb{R}^3, +$$ -### Path Rig +then one representative evaluator has the shape -Files: +$$ +p(s,u,v) = C(s) + u\,R(s) + v\,U(s), +$$ -- [`CPathCamera.hpp`](CPathCamera.hpp) -- [`CCameraPathUtilities.hpp`](CCameraPathUtilities.hpp) -- [`CCameraPathMetadata.hpp`](CCameraPathMetadata.hpp) +with orientation built from the basis -Typed path state: +$$ +\bigl(R(s), U(s), F(s)\bigr) +$$ -- `s` -- `u` -- `v` -- `roll` +and then rotated by authored roll $\rho$ around the current forward axis. -Typed path limits: +The built-in model below is just one concrete default implementation of that seam. +It happens to have one simple closed-form `resolveState(...)` from world-space position, but custom models only need to provide a legal state-resolution callback. They do not need one strict analytical inverse of the evaluator. -- `minU` -- `minDistance` -- `maxDistance` +**Default built-in model** -## Typed tooling +If you do not supply your own `SCameraPathModel`, `CPathCamera` uses the built-in cylindrical model. -The key typed types are introduced in the `Core concepts` section above. +That default model uses a cylindrical parameterization around the current target position +$t = (t_x, t_y, t_z)$ with typed state +$q = (s, u, v, \rho)$: -This section focuses on how they fit together in one workflow: +$$ +\begin{aligned} +x &= t_x + u \cos s \\ +y &= t_y + v \\ +z &= t_z + u \sin s +\end{aligned} +$$ -1. `SCameraRigPose` is the smallest typed pose fragment. -2. `CCameraGoal` is the canonical typed state transport built on top of pose and optional family-specific fragments. -3. `CCameraGoalSolver` captures runtime cameras into goals and applies goals back to runtime cameras. -4. `CCameraPreset` gives one goal a stable user-facing identity. -5. `CCameraKeyframeTrack` stores presets over authored time. -6. `CCameraPlaybackTimeline` stores playback cursor state while a track is being evaluated. +That means: -Use this layer when camera state must outlive the current frame or be exchanged between tools. +- `s` is the authored angle around the target in the world `XZ` plane +- `u` is the planar `XZ` radius +- `v` is the vertical offset on world `Y` +- `roll` is an extra rotation applied around the resulting forward axis -## Follow +For the built-in model, `resolveState(...)` from one world-space position can be written as: -Files: +$$ +\begin{aligned} +\Delta &= p - t \\ +s &= \operatorname{wrap}\!\left(\operatorname{atan2}(\Delta_z, \Delta_x)\right) \\ +u &= \max\!\left(u_{\min}, \sqrt{\Delta_x^2 + \Delta_z^2}\right) \\ +v &= \Delta_y +\end{aligned} +$$ -- [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) -- [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp) +The default model also derives one radial camera distance from `(u, v)`: -Follow is built from: +$$ +d = \lVert (u, v) \rVert_2 = \sqrt{u^2 + v^2} +$$ -- one tracked target -- one follow mode -- one follow configuration +and sanitizes state as: -Tracked target type: +$$ +\begin{aligned} +s &\leftarrow \operatorname{wrap}(s) \\ +u &\leftarrow \max(u_{\min}, u) \\ +\rho &\leftarrow \operatorname{wrap}(\rho) +\end{aligned} +$$ -- `CTrackedTarget` +The base orientation is then built from the camera looking from the resolved position back at the target, and the authored roll is applied around that resulting forward axis. -Follow modes: +The built-in control law maps runtime local motion into path-state delta as: -- `OrbitTarget` -- `LookAtTarget` -- `KeepWorldOffset` -- `KeepLocalOffset` +$$ +\Delta q = +\begin{bmatrix} +\Delta s \\ +\Delta u \\ +\Delta v \\ +\Delta \rho +\end{bmatrix} += +\begin{bmatrix} +\Delta z_{\text{local}} \\ +\Delta x_{\text{local}} \\ +\Delta y_{\text{local}} \\ +\Delta \mathrm{roll} +\end{bmatrix} +$$ -`CCameraFollowUtilities` reads tracked-target pose, builds resulting camera goal state, and applies it through the shared goal solver. +and integrates it as: -## Scripting +$$ +q_{n+1} = \operatorname{sanitize}(q_n + \Delta q) +$$ -### Compact authored format +Equivalent pseudocode for the built-in model is: -Files: +```cpp +PathState state = sanitize(inputState, limits); -- [`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) -- [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) +const double appliedU = max(limits.minU, state.u); +const dvec3 offset = { + cos(state.s) * appliedU, + state.v, + sin(state.s) * appliedU +}; -This layer stores authored camera-domain data. +const dvec3 requestedPosition = target + offset; +const auto [orbitUv, distance] = + buildOrbitFromPosition(target, requestedPosition, limits.minDistance, limits.maxDistance); -### Expanded runtime format +auto [position, orientation] = + buildSphericalPoseFromOrbit(target, orbitUv, distance, limits.minDistance, limits.maxDistance); + +if (state.roll != 0.0) + orientation = applyRollAroundCurrentForward(orientation, state.roll); + +PathDelta delta = { + .s = localTranslation.z, + .u = localTranslation.x, + .v = localTranslation.y, + .roll = localRotation.z +}; -Files: +state = sanitize(state + delta, limits); +``` + +This is intentionally more general than one hardcoded "rail camera". + +The built-in model shown above is only one concrete parameterization. +The reusable part is `SCameraPathModel`, which lets the runtime reinterpret the same typed `PathState` seam through custom: + +- state resolution +- control law +- integration +- pose evaluation +- distance update + +In practice that means the same `Path Rig` family can be used for many constrained camera designs, for example: + +- cylindrical and orbital rigs around one subject +- dolly or crane-style motion with authored lateral and vertical offsets +- cameras constrained to one spline or guide path with side/up offsets +- banked path cameras where `roll` becomes authored banking around the current forward axis +- on-rails gameplay or cinematic cameras with one path parameter plus local offsets +- custom path-following rigs that keep the runtime API and typed tooling unchanged while replacing only the path model -- [`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) -- [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) +So the important boundary is: -This layer stores executable per-frame runtime payloads and validation checks. +- the built-in model is one cylindrical target-relative parameterization +- the `Path Rig` API surface is the extensible typed seam for path-driven camera families + +It can represent a large class of practical constrained camera motions, but it is still not "arbitrary free pose". +If a camera must store completely unconstrained 6DOF pose as its native state, use `Free`. + +## Camera families + +- `Free` cameras in [`CFPSCamera.hpp`](CFPSCamera.hpp) and [`CFreeLockCamera.hpp`](CFreeLockCamera.hpp) store world-space position plus free or FPS-constrained orientation. +- Target-relative cameras are built on [`CSphericalTargetCamera.hpp`](CSphericalTargetCamera.hpp) and include [`COrbitCamera.hpp`](COrbitCamera.hpp), [`CArcballCamera.hpp`](CArcballCamera.hpp), [`CTurntableCamera.hpp`](CTurntableCamera.hpp), [`CTopDownCamera.hpp`](CTopDownCamera.hpp), [`CIsometricCamera.hpp`](CIsometricCamera.hpp), [`CChaseCamera.hpp`](CChaseCamera.hpp), [`CDollyCamera.hpp`](CDollyCamera.hpp), and [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp). They store target position, `orbitUv`, and distance instead of arbitrary free pose. +- [`CDollyZoomCamera.hpp`](CDollyZoomCamera.hpp) extends the target-relative family with dynamic perspective state `baseFov` and `referenceDistance`. +- [`CPathCamera.hpp`](CPathCamera.hpp) uses the parametric path-state seam described above together with limits `minU`, `minDistance`, and `maxDistance`. + +## Typed tooling + +Use the typed layer when camera state must outlive the current frame or be exchanged between tools: + +1. `SCameraRigPose` is the smallest typed pose fragment. +2. `CCameraGoal` is the canonical typed transport for camera state. +3. `CCameraGoalSolver` captures runtime cameras into goals and applies goals back to runtime cameras. +4. `CCameraPreset` gives one goal a stable user-facing identity. +5. `CCameraKeyframeTrack` stores presets over authored time. +6. `CCameraPlaybackTimeline` stores playback cursor state while a track is being evaluated. + +## Follow + +Follow lives in [`CCameraFollowUtilities.hpp`](CCameraFollowUtilities.hpp) and [`CCameraFollowRegressionUtilities.hpp`](CCameraFollowRegressionUtilities.hpp). It combines one `CTrackedTarget`, one follow mode, and one follow configuration, then builds the resulting camera goal and applies it through the shared goal solver. Available modes are `OrbitTarget`, `LookAtTarget`, `KeepWorldOffset`, and `KeepLocalOffset`. + +## Scripting + +### Compact authored format + +[`CCameraSequenceScript.hpp`](CCameraSequenceScript.hpp) and [`CCameraSequenceScriptPersistence.hpp`](CCameraSequenceScriptPersistence.hpp) store authored camera-domain data. + +### Expanded runtime format + +[`CCameraScriptedRuntime.hpp`](CCameraScriptedRuntime.hpp) and [`CCameraScriptedCheckRunner.hpp`](CCameraScriptedCheckRunner.hpp) store executable per-frame runtime payloads and validation checks. Common flow: @@ -814,21 +1010,6 @@ compact authored sequence ## Projection and presentation helpers -Projection layer: - -- [`IProjection.hpp`](IProjection.hpp) -- [`ILinearProjection.hpp`](ILinearProjection.hpp) -- [`IPerspectiveProjection.hpp`](IPerspectiveProjection.hpp) -- [`IPlanarProjection.hpp`](IPlanarProjection.hpp) -- [`CLinearProjection.hpp`](CLinearProjection.hpp) -- [`CPlanarProjection.hpp`](CPlanarProjection.hpp) -- [`CCubeProjection.hpp`](CCubeProjection.hpp) - -Camera-facing presentation helpers: +Projection types live in [`IProjection.hpp`](IProjection.hpp), [`ILinearProjection.hpp`](ILinearProjection.hpp), [`IPerspectiveProjection.hpp`](IPerspectiveProjection.hpp), [`IPlanarProjection.hpp`](IPlanarProjection.hpp), [`CLinearProjection.hpp`](CLinearProjection.hpp), [`CPlanarProjection.hpp`](CPlanarProjection.hpp), and [`CCubeProjection.hpp`](CCubeProjection.hpp). -- [`CCameraPresentationUtilities.hpp`](CCameraPresentationUtilities.hpp) -- [`CCameraProjectionUtilities.hpp`](CCameraProjectionUtilities.hpp) -- [`CCameraTextUtilities.hpp`](CCameraTextUtilities.hpp) -- [`CCameraViewportOverlayUtilities.hpp`](CCameraViewportOverlayUtilities.hpp) -- [`CCameraControlPanelUiUtilities.hpp`](CCameraControlPanelUiUtilities.hpp) -- [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp) +Camera-facing presentation helpers live in [`CCameraPresentationUtilities.hpp`](CCameraPresentationUtilities.hpp), [`CCameraProjectionUtilities.hpp`](CCameraProjectionUtilities.hpp), [`CCameraTextUtilities.hpp`](CCameraTextUtilities.hpp), [`CCameraViewportOverlayUtilities.hpp`](CCameraViewportOverlayUtilities.hpp), [`CCameraControlPanelUiUtilities.hpp`](CCameraControlPanelUiUtilities.hpp), and [`CCameraScriptVisualDebugOverlayUtilities.hpp`](CCameraScriptVisualDebugOverlayUtilities.hpp). From 5009f8b664dcee9127a6bc0e3b39c22a444469dd Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 15:00:01 +0200 Subject: [PATCH 145/161] Trim non-camera drift from cameras ext --- 3rdparty/dxc/dxc | 2 +- 3rdparty/imguizmo | 2 +- include/nbl/asset/IDescriptorSet.h | 2 +- include/nbl/builtin/hlsl/algorithm.hlsl | 8 ++++++-- .../hlsl/cpp_compat/impl/intrinsics_impl.hlsl | 8 +++----- .../builtin/hlsl/cpp_compat/intrinsics.hlsl | 3 +-- .../hlsl/math/thin_lens_projection.hlsl | 5 ++--- include/nbl/builtin/hlsl/numbers.hlsl | 2 +- include/nbl/core/decl/smart_refctd_ptr.h | 9 --------- include/nbl/core/math/intutil.h | 12 ++++------- include/nbl/ext/Cameras/IPlanarProjection.hpp | 1 + include/nbl/macros.h | 2 +- include/nbl/system/IAsyncQueueDispatcher.h | 20 ------------------- include/nbl/ui/SInputEvent.h | 1 + 14 files changed, 23 insertions(+), 54 deletions(-) diff --git a/3rdparty/dxc/dxc b/3rdparty/dxc/dxc index ef98b9792c..4c5fbdc9c1 160000 --- a/3rdparty/dxc/dxc +++ b/3rdparty/dxc/dxc @@ -1 +1 @@ -Subproject commit ef98b9792c4adf476e4deaa21a536172a25fbfba +Subproject commit 4c5fbdc9c1b9e8c4daaed07a30b60c4f91d96e4a diff --git a/3rdparty/imguizmo b/3rdparty/imguizmo index 4d3445248b..b10e91756d 160000 --- a/3rdparty/imguizmo +++ b/3rdparty/imguizmo @@ -1 +1 @@ -Subproject commit 4d3445248b9d598b92e877d752fd1f7fe6c1f134 +Subproject commit b10e91756d32395f5c1fefd417899b657ed7cb88 diff --git a/include/nbl/asset/IDescriptorSet.h b/include/nbl/asset/IDescriptorSet.h index e5502d2033..34195cd5c1 100644 --- a/include/nbl/asset/IDescriptorSet.h +++ b/include/nbl/asset/IDescriptorSet.h @@ -69,7 +69,7 @@ class IDescriptorSet : public virtual core::IReferenceCounted // TODO: try to re SCombinedImageSamplerInfo combinedImageSampler; } info; - SDescriptorInfo() : desc(), info() {} + SDescriptorInfo() {} template SDescriptorInfo(const SBufferBinding& binding) : desc() diff --git a/include/nbl/builtin/hlsl/algorithm.hlsl b/include/nbl/builtin/hlsl/algorithm.hlsl index 1aaaa02cd8..66442a11a1 100644 --- a/include/nbl/builtin/hlsl/algorithm.hlsl +++ b/include/nbl/builtin/hlsl/algorithm.hlsl @@ -73,16 +73,20 @@ namespace impl rhs ^= lhs; lhs ^= rhs; } +#else + template + NBL_CONSTEXPR_FUNC void swap(NBL_REF_ARG(T) lhs, NBL_REF_ARG(T) rhs) + { + std::swap(lhs, rhs); + } #endif } -#ifdef __HLSL_VERSION template NBL_CONSTEXPR_FUNC void swap(NBL_REF_ARG(T) lhs, NBL_REF_ARG(T) rhs) { impl::swap(lhs, rhs); } -#endif namespace impl diff --git a/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl b/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl index 4e4fcf1672..61081ea327 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/impl/intrinsics_impl.hlsl @@ -2,7 +2,6 @@ #define _NBL_BUILTIN_HLSL_CPP_COMPAT_IMPL_INTRINSICS_IMPL_INCLUDED_ #include -#include #include #include #include @@ -12,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -509,8 +509,7 @@ struct radians_helper using return_t = T; static inline return_t __call(const T degrees) { - constexpr T Pi = ::nbl::hlsl::numbers::template pi; - return degrees * (Pi / static_cast(180.0)); + return degrees * (bit_cast(numbers::pi) / static_cast(180.0)); } }; @@ -521,8 +520,7 @@ struct degrees_helper using return_t = T; static inline return_t __call(const T radians) { - constexpr T Pi = ::nbl::hlsl::numbers::template pi; - return radians * (static_cast(180.0) / Pi); + return radians * (static_cast(180.0) / bit_cast(numbers::pi)); } }; diff --git a/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl b/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl index 45dfb6d909..78367f7924 100644 --- a/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl +++ b/include/nbl/builtin/hlsl/cpp_compat/intrinsics.hlsl @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -332,4 +331,4 @@ inline T fma(NBL_CONST_REF_ARG(T) x, NBL_CONST_REF_ARG(T) y, NBL_CONST_REF_ARG(T } } -#endif +#endif \ No newline at end of file diff --git a/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl b/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl index dd1077f9b7..985ec8a6a3 100644 --- a/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl +++ b/include/nbl/builtin/hlsl/math/thin_lens_projection.hlsl @@ -1,7 +1,6 @@ #ifndef _NBL_BUILTIN_HLSL_MATH_THIN_LENS_PROJECTION_INCLUDED_ #define _NBL_BUILTIN_HLSL_MATH_THIN_LENS_PROJECTION_INCLUDED_ -#include #include #include @@ -17,7 +16,7 @@ namespace thin_lens template) inline matrix rhPerspectiveFovMatrix(FloatingPoint fieldOfViewRadians, FloatingPoint aspectRatio, FloatingPoint zNear, FloatingPoint zFar) { - const FloatingPoint h = ::nbl::core::reciprocal(tan(fieldOfViewRadians * 0.5f)); + const FloatingPoint h = core::reciprocal(tan(fieldOfViewRadians * 0.5f)); _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero const float w = h / aspectRatio; @@ -34,7 +33,7 @@ inline matrix rhPerspectiveFovMatrix(FloatingPoint fieldOfV template) inline matrix lhPerspectiveFovMatrix(FloatingPoint fieldOfViewRadians, FloatingPoint aspectRatio, FloatingPoint zNear, FloatingPoint zFar) { - const FloatingPoint h = ::nbl::core::reciprocal(tan(fieldOfViewRadians * 0.5f)); + const FloatingPoint h = core::reciprocal(tan(fieldOfViewRadians * 0.5f)); _NBL_DEBUG_BREAK_IF(aspectRatio == 0.f); //division by zero const float w = h / aspectRatio; diff --git a/include/nbl/builtin/hlsl/numbers.hlsl b/include/nbl/builtin/hlsl/numbers.hlsl index e55d9794af..4594596590 100644 --- a/include/nbl/builtin/hlsl/numbers.hlsl +++ b/include/nbl/builtin/hlsl/numbers.hlsl @@ -1,7 +1,7 @@ #ifndef _NBL_BUILTIN_HLSL_MATH_NUMBERS_INCLUDED_ #define _NBL_BUILTIN_HLSL_MATH_NUMBERS_INCLUDED_ -#include "nbl/builtin/hlsl/cpp_compat/basic.h" +#include "nbl/builtin/hlsl/cpp_compat.hlsl" namespace nbl { diff --git a/include/nbl/core/decl/smart_refctd_ptr.h b/include/nbl/core/decl/smart_refctd_ptr.h index 74fdf61693..7c231fea4b 100644 --- a/include/nbl/core/decl/smart_refctd_ptr.h +++ b/include/nbl/core/decl/smart_refctd_ptr.h @@ -144,15 +144,6 @@ smart_refctd_ptr move_and_dynamic_cast(smart_refctd_ptr& smart_ptr); template< class U, class T > smart_refctd_ptr move_and_dynamic_cast(smart_refctd_ptr&& smart_ptr) {return move_and_dynamic_cast(smart_ptr);} -template -struct is_smart_refctd_ptr : std::false_type {}; - -template -struct is_smart_refctd_ptr> : std::true_type {}; - -template -inline constexpr bool is_smart_refctd_ptr_v = is_smart_refctd_ptr::value; - } // end namespace nbl::core /* diff --git a/include/nbl/core/math/intutil.h b/include/nbl/core/math/intutil.h index d799f7f885..7a94844258 100644 --- a/include/nbl/core/math/intutil.h +++ b/include/nbl/core/math/intutil.h @@ -3,14 +3,10 @@ // For conditions of distribution and use, see copyright notice in nabla.h // TODO: kill this file -#ifndef __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ -#define __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ - -#include -#include - -#include "nbl/macros.h" -#include "nbl/builtin/hlsl/math/intutil.hlsl" +#ifndef __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ +#define __NBL_CORE_MATH_INTUTIL_H_INCLUDED__ + +#include "nbl/builtin/hlsl/math/intutil.hlsl" namespace nbl diff --git a/include/nbl/ext/Cameras/IPlanarProjection.hpp b/include/nbl/ext/Cameras/IPlanarProjection.hpp index 1b6f193e95..41a7570ceb 100644 --- a/include/nbl/ext/Cameras/IPlanarProjection.hpp +++ b/include/nbl/ext/Cameras/IPlanarProjection.hpp @@ -1,6 +1,7 @@ #ifndef _NBL_I_PLANAR_PROJECTION_HPP_ #define _NBL_I_PLANAR_PROJECTION_HPP_ +#include "nbl/core/math/glslFunctions.h" #include "nbl/builtin/hlsl/math/thin_lens_projection.hlsl" #include "IGimbalBindingLayout.hpp" diff --git a/include/nbl/macros.h b/include/nbl/macros.h index c785b10112..fe93201a11 100644 --- a/include/nbl/macros.h +++ b/include/nbl/macros.h @@ -62,7 +62,7 @@ // define a break macro for debugging. #if defined(_NBL_WINDOWS_API_) && defined(_MSC_VER) #include - #define _NBL_BREAK_IF( _CONDITION_ ) if (_CONDITION_) {_CrtDbgBreak();} + #define _NBL_BREAK_IF( _CONDITION_ ) if (_CONDITION_) {_CrtDbgBreak();} #else #include "signal.h" #define _NBL_BREAK_IF( _CONDITION_ ) if ( (_CONDITION_) ) raise(SIGTRAP); diff --git a/include/nbl/system/IAsyncQueueDispatcher.h b/include/nbl/system/IAsyncQueueDispatcher.h index fa7e6a1b8d..d5b0cb8a1a 100644 --- a/include/nbl/system/IAsyncQueueDispatcher.h +++ b/include/nbl/system/IAsyncQueueDispatcher.h @@ -490,26 +490,6 @@ class IAsyncQueueDispatcher : public IThreadHandler, pro protected: inline ~IAsyncQueueDispatcher() {} inline void background_work() {} - inline void exit(internal_state_t* optional_internal_state=nullptr) - { - while (cb_begin!=cb_end) - { - uint64_t r_id = cb_begin; - r_id = wrapAround(r_id); - - request_t& req = request_pool[r_id]; - if (future_base_t* future=req.wait()) - { - if constexpr (base_t::has_internal_state) - static_cast(this)->process_request(future,req.m_metadata,*optional_internal_state); - else - static_cast(this)->process_request(future,req.m_metadata); - req.notify(); - } - cb_begin++; - cb_begin.notify_one(); - } - } private: template diff --git a/include/nbl/ui/SInputEvent.h b/include/nbl/ui/SInputEvent.h index 9575f626d4..d791d08e53 100644 --- a/include/nbl/ui/SInputEvent.h +++ b/include/nbl/ui/SInputEvent.h @@ -55,6 +55,7 @@ struct SMouseEvent : SEventBase IWindow* window; }; + struct SKeyboardEvent : SEventBase { inline SKeyboardEvent(std::chrono::microseconds ts) : SEventBase(ts) { } From 28cc1d31f4d33252f270f173f2e4a3954eff4b3f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 16:11:41 +0200 Subject: [PATCH 146/161] Use shared quaternion cast in cameras --- .../nbl/builtin/hlsl/math/quaternions.hlsl | 138 +++++++++++++++++- .../nbl/ext/Cameras/CCameraMathUtilities.hpp | 111 ++------------ src/nbl/ext/Cameras/CCameraMathUtilities.cpp | 45 ++++++ src/nbl/ext/Cameras/CMakeLists.txt | 16 +- 4 files changed, 202 insertions(+), 108 deletions(-) create mode 100644 src/nbl/ext/Cameras/CCameraMathUtilities.cpp diff --git a/include/nbl/builtin/hlsl/math/quaternions.hlsl b/include/nbl/builtin/hlsl/math/quaternions.hlsl index 49a8f95d22..9cd344c109 100644 --- a/include/nbl/builtin/hlsl/math/quaternions.hlsl +++ b/include/nbl/builtin/hlsl/math/quaternions.hlsl @@ -380,18 +380,150 @@ struct static_cast_helper, math::quaternion > template struct static_cast_helper, math::quaternion > { - static inline matrix cast(const math::quaternion q) + static inline matrix cast(NBL_CONST_REF_ARG(math::quaternion) q) { return q.__constructMatrix(); } }; +template +inline bool is_finite_quaternion(NBL_CONST_REF_ARG(math::quaternion) q) +{ + return !hlsl::isnan(q.data.x) && + !hlsl::isnan(q.data.y) && + !hlsl::isnan(q.data.z) && + !hlsl::isnan(q.data.w); +} + +template +inline T score_matrix_to_quaternion_cast_candidate( + NBL_CONST_REF_ARG(matrix) target, + NBL_CONST_REF_ARG(math::quaternion) candidate) +{ + if (!is_finite_quaternion(candidate)) + return bit_cast(numeric_limits::infinity); + + const vector rebuiltRight = candidate.transformVector(vector(T(1), T(0), T(0)), true); + const vector rebuiltUp = candidate.transformVector(vector(T(0), T(1), T(0)), true); + const vector rebuiltForward = candidate.transformVector(vector(T(0), T(0), T(1)), true); + return + hlsl::length(rebuiltRight - target[0]) + + hlsl::length(rebuiltUp - target[1]) + + hlsl::length(rebuiltForward - target[2]); +} + +template +inline math::quaternion direct_matrix_to_quaternion_cast(NBL_CONST_REF_ARG(matrix) input) +{ + typedef math::quaternion quaternion_t; + typedef typename quaternion_t::data_type data_type; + + const T xLengthSq = hlsl::dot(input[0], input[0]); + const T yLengthSq = hlsl::dot(input[1], input[1]); + const T zLengthSq = hlsl::dot(input[2], input[2]); + const T uniformScaleSq = (xLengthSq + yLengthSq + zLengthSq) / T(3.0); + if (uniformScaleSq < numeric_limits::min) + { + quaternion_t retval; + retval.data = hlsl::promote(bit_cast(numeric_limits::quiet_NaN)); + return retval; + } + + const T uniformScale = hlsl::sqrt(uniformScaleSq); + matrix m = input; + m /= uniformScale; + + const T m00 = m[0][0]; + const T m11 = m[1][1]; + const T m22 = m[2][2]; + const T neg_m00 = -m00; + const T neg_m11 = -m11; + const T neg_m22 = -m22; + const data_type Qx = data_type(m00, m00, neg_m00, neg_m00); + const data_type Qy = data_type(m11, neg_m11, m11, neg_m11); + const data_type Qz = data_type(m22, neg_m22, neg_m22, m22); + const data_type tmp = Qx + Qy + Qz; + + quaternion_t retval; + if (tmp.x > T(0.0)) + { + const T scales = hlsl::sqrt(tmp.x + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = (m[2][1] - m[1][2]) * invscales; + retval.data.y = (m[0][2] - m[2][0]) * invscales; + retval.data.z = (m[1][0] - m[0][1]) * invscales; + retval.data.w = scales * T(0.5); + } + else if (tmp.y > T(0.0)) + { + const T scales = hlsl::sqrt(tmp.y + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = scales * T(0.5); + retval.data.y = (m[0][1] + m[1][0]) * invscales; + retval.data.z = (m[2][0] + m[0][2]) * invscales; + retval.data.w = (m[2][1] - m[1][2]) * invscales; + } + else if (tmp.z > T(0.0)) + { + const T scales = hlsl::sqrt(tmp.z + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = (m[0][1] + m[1][0]) * invscales; + retval.data.y = scales * T(0.5); + retval.data.z = (m[1][2] + m[2][1]) * invscales; + retval.data.w = (m[0][2] - m[2][0]) * invscales; + } + else + { + const T scales = hlsl::sqrt(tmp.w + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = (m[0][2] + m[2][0]) * invscales; + retval.data.y = (m[1][2] + m[2][1]) * invscales; + retval.data.z = scales * T(0.5); + retval.data.w = (m[1][0] - m[0][1]) * invscales; + } + + retval.data *= uniformScale; + return retval; +} + +template +inline math::quaternion matrix_to_quaternion_cast(NBL_CONST_REF_ARG(matrix) m) +{ + const math::quaternion directCandidate = math::quaternion::create(m, true); + const math::quaternion transposedCandidate = math::quaternion::create(hlsl::transpose(m), true); + const math::quaternion directFallback = direct_matrix_to_quaternion_cast(m); + const math::quaternion transposedFallback = direct_matrix_to_quaternion_cast(hlsl::transpose(m)); + + const T directScore = score_matrix_to_quaternion_cast_candidate(m, directCandidate); + const T transposedScore = score_matrix_to_quaternion_cast_candidate(m, transposedCandidate); + const T directFallbackScore = score_matrix_to_quaternion_cast_candidate(m, directFallback); + const T transposedFallbackScore = score_matrix_to_quaternion_cast_candidate(m, transposedFallback); + + math::quaternion bestCandidate = directCandidate; + T bestScore = directScore; + + if (transposedScore < bestScore) + { + bestCandidate = transposedCandidate; + bestScore = transposedScore; + } + if (directFallbackScore < bestScore) + { + bestCandidate = directFallback; + bestScore = directFallbackScore; + } + if (transposedFallbackScore < bestScore) + bestCandidate = transposedFallback; + + return bestCandidate; +} + template struct static_cast_helper, matrix > { - static inline math::quaternion cast(const matrix m) + static inline math::quaternion cast(NBL_CONST_REF_ARG(matrix) m) { - return math::quaternion::create(m, true); + return matrix_to_quaternion_cast(m); } }; } diff --git a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp index 5c704b41da..4e3fbfa112 100644 --- a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp @@ -331,103 +331,12 @@ struct CCameraMathUtilities final canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); - const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; - const auto desiredRight = canonicalRight; - const auto desiredUp = canonicalUp; - const auto desiredForward = canonicalForward; + static_assert(std::is_same_v || std::is_same_v, "Camera basis conversion is only implemented for float and double."); - const auto scoreCandidate = [&](const camera_quaternion_t& candidate) - { - if (!isFiniteQuaternion(candidate)) - return std::numeric_limits::infinity(); - - const auto normalizedCandidate = normalizeQuaternion(candidate); - const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); - const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); - const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); - - const T rightError = length(rebuiltRight - desiredRight); - const T upError = length(rebuiltUp - desiredUp); - const T forwardError = length(rebuiltForward - desiredForward); - return rightError + upError + forwardError; - }; - - const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) - { - const T m00 = m[0][0]; - const T m11 = m[1][1]; - const T m22 = m[2][2]; - const T trace = m00 + m11 + m22; - - camera_quaternion_t output = makeIdentityQuaternion(); - if (trace > T(0)) - { - const T scale = hlsl::sqrt(trace + T(1)); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][1] - m[1][2]) * invScale; - output.data.y = (m[0][2] - m[2][0]) * invScale; - output.data.z = (m[1][0] - m[0][1]) * invScale; - output.data.w = scale * T(0.5); - } - else if (m00 >= m11 && m00 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); - const T invScale = T(0.5) / scale; - output.data.x = scale * T(0.5); - output.data.y = (m[0][1] + m[1][0]) * invScale; - output.data.z = (m[2][0] + m[0][2]) * invScale; - output.data.w = (m[2][1] - m[1][2]) * invScale; - } - else if (m11 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); - const T invScale = T(0.5) / scale; - output.data.x = (m[0][1] + m[1][0]) * invScale; - output.data.y = scale * T(0.5); - output.data.z = (m[1][2] + m[2][1]) * invScale; - output.data.w = (m[0][2] - m[2][0]) * invScale; - } - else - { - const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][0] + m[0][2]) * invScale; - output.data.y = (m[1][2] + m[2][1]) * invScale; - output.data.z = scale * T(0.5); - output.data.w = (m[1][0] - m[0][1]) * invScale; - } - return normalizeQuaternion(output); - }; - - const camera_matrix_t transposedBasis = hlsl::transpose(basis); - const camera_quaternion_t candidates[] = { - camera_quaternion_t::create(basis, true), - camera_quaternion_t::create(transposedBasis, true), - quaternionFromMatrixFallback(basis), - quaternionFromMatrixFallback(transposedBasis) - }; - - camera_quaternion_t bestCandidate = makeIdentityQuaternion(); - T bestScore = std::numeric_limits::infinity(); - bool foundFiniteCandidate = false; - const auto considerCandidate = [&](const camera_quaternion_t& candidate) - { - const T score = scoreCandidate(candidate); - if (score < bestScore) - { - bestScore = score; - bestCandidate = candidate; - foundFiniteCandidate = true; - } - }; - - for (const auto& candidate : candidates) - considerCandidate(candidate); - - if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) - return makeIdentityQuaternion(); - - return normalizeQuaternion(bestCandidate); + if constexpr (std::is_same_v) + return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); + else + return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); } template @@ -991,6 +900,16 @@ struct CCameraMathUtilities final outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); return isFiniteVec3(outRotationEulerDegrees); } + + static camera_quaternion_t makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward); + + static camera_quaternion_t makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward); }; } // namespace nbl::hlsl diff --git a/src/nbl/ext/Cameras/CCameraMathUtilities.cpp b/src/nbl/ext/Cameras/CCameraMathUtilities.cpp new file mode 100644 index 0000000000..4c88fb81c4 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraMathUtilities.cpp @@ -0,0 +1,45 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" + +namespace nbl::hlsl +{ + +namespace +{ + +template +camera_quaternion_t makeQuaternionFromBasisWithCast( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + const camera_matrix_t basis(right, up, forward); + const auto candidate = _static_cast>(basis); + if (!CCameraMathUtilities::isFiniteQuaternion(candidate)) + return CCameraMathUtilities::makeIdentityQuaternion(); + + return CCameraMathUtilities::normalizeQuaternion(candidate); +} + +} // namespace + +camera_quaternion_t CCameraMathUtilities::makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + return makeQuaternionFromBasisWithCast(right, up, forward); +} + +camera_quaternion_t CCameraMathUtilities::makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + return makeQuaternionFromBasisWithCast(right, up, forward); +} + +} // namespace nbl::hlsl diff --git a/src/nbl/ext/Cameras/CMakeLists.txt b/src/nbl/ext/Cameras/CMakeLists.txt index 5e978def69..0af31024ec 100644 --- a/src/nbl/ext/Cameras/CMakeLists.txt +++ b/src/nbl/ext/Cameras/CMakeLists.txt @@ -4,22 +4,20 @@ file(GLOB NBL_EXT_CAMERAS_HEADERS CONFIGURE_DEPENDS "${NBL_ROOT_PATH}/include/nbl/ext/Cameras/*.hpp" ) -set(NBL_EXT_CAMERAS_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraPersistence.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraSequenceScriptPersistence.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" +file(GLOB NBL_EXT_CAMERAS_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" ) -set_source_files_properties( - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" - PROPERTIES - HEADER_FILE_ONLY ON +file(GLOB NBL_EXT_CAMERAS_LOCAL_HEADERS CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp" ) +set_source_files_properties(${NBL_EXT_CAMERAS_LOCAL_HEADERS} PROPERTIES HEADER_FILE_ONLY ON) + nbl_create_ext_library_project( Cameras "${NBL_EXT_CAMERAS_HEADERS}" - "${NBL_EXT_CAMERAS_SOURCES}" + "${NBL_EXT_CAMERAS_SOURCES};${NBL_EXT_CAMERAS_LOCAL_HEADERS}" "" "" "" From d5335f2bfe2bd7f6e2cb93c65647b83b9ea008ae Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 16:12:41 +0200 Subject: [PATCH 147/161] Restore dxc submodule pointer --- 3rdparty/dxc/dxc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/dxc/dxc b/3rdparty/dxc/dxc index 4c5fbdc9c1..ef98b9792c 160000 --- a/3rdparty/dxc/dxc +++ b/3rdparty/dxc/dxc @@ -1 +1 @@ -Subproject commit 4c5fbdc9c1b9e8c4daaed07a30b60c4f91d96e4a +Subproject commit ef98b9792c4adf476e4deaa21a536172a25fbfba From b582488fe1c55edfaa1ea88928c487d1cb476472 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 17:17:54 +0200 Subject: [PATCH 148/161] Split cameras utilities into source files --- .../CCameraFollowRegressionUtilities.hpp | 257 +------- .../ext/Cameras/CCameraFollowUtilities.hpp | 183 +----- include/nbl/ext/Cameras/CCameraGoalSolver.hpp | 547 +---------------- .../Cameras/CCameraInputBindingUtilities.hpp | 463 +-------------- .../nbl/ext/Cameras/CCameraKeyframeTrack.hpp | 159 +---- .../nbl/ext/Cameras/CCameraPathUtilities.hpp | 365 ++---------- .../Cameras/CCameraScriptedCheckRunner.hpp | 479 +-------------- .../nbl/ext/Cameras/CCameraSequenceScript.hpp | 514 ++-------------- include/nbl/ext/Cameras/ILinearProjection.hpp | 77 +-- include/nbl/ext/Cameras/IPlanarProjection.hpp | 43 +- .../CCameraFollowRegressionUtilities.cpp | 278 +++++++++ .../ext/Cameras/CCameraFollowUtilities.cpp | 211 +++++++ src/nbl/ext/Cameras/CCameraGoalSolver.cpp | 548 ++++++++++++++++++ .../Cameras/CCameraInputBindingUtilities.cpp | 479 +++++++++++++++ src/nbl/ext/Cameras/CCameraKeyframeTrack.cpp | 152 +++++ src/nbl/ext/Cameras/CCameraPathUtilities.cpp | 392 +++++++++++++ .../Cameras/CCameraScriptedCheckRunner.cpp | 492 ++++++++++++++++ src/nbl/ext/Cameras/CCameraSequenceScript.cpp | 509 ++++++++++++++++ src/nbl/ext/Cameras/ILinearProjection.cpp | 86 +++ src/nbl/ext/Cameras/IPlanarProjection.cpp | 53 ++ 20 files changed, 3409 insertions(+), 2878 deletions(-) create mode 100644 src/nbl/ext/Cameras/CCameraFollowRegressionUtilities.cpp create mode 100644 src/nbl/ext/Cameras/CCameraFollowUtilities.cpp create mode 100644 src/nbl/ext/Cameras/CCameraGoalSolver.cpp create mode 100644 src/nbl/ext/Cameras/CCameraInputBindingUtilities.cpp create mode 100644 src/nbl/ext/Cameras/CCameraKeyframeTrack.cpp create mode 100644 src/nbl/ext/Cameras/CCameraPathUtilities.cpp create mode 100644 src/nbl/ext/Cameras/CCameraScriptedCheckRunner.cpp create mode 100644 src/nbl/ext/Cameras/CCameraSequenceScript.cpp create mode 100644 src/nbl/ext/Cameras/ILinearProjection.cpp create mode 100644 src/nbl/ext/Cameras/IPlanarProjection.cpp diff --git a/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp b/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp index 1ce8d0b432..807698248d 100644 --- a/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp @@ -96,99 +96,30 @@ struct SCameraFollowApplyValidationResult struct CCameraFollowRegressionUtilities final { public: - static inline SCameraFollowRegressionThresholds makeFollowRegressionThresholds( - const float projectedNdcTolerance = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance, - const float lockAngleToleranceDeg = SCameraFollowRegressionThresholds::DefaultLockAngleToleranceDeg) - { - auto thresholds = SCameraFollowRegressionThresholds{}; - thresholds.projectedNdcTolerance = projectedNdcTolerance; - thresholds.lockAngleToleranceDeg = lockAngleToleranceDeg; - return thresholds; - } + static SCameraFollowRegressionThresholds makeFollowRegressionThresholds( + float projectedNdcTolerance = SCameraFollowRegressionThresholds::DefaultProjectedNdcTolerance, + float lockAngleToleranceDeg = SCameraFollowRegressionThresholds::DefaultLockAngleToleranceDeg); - static inline bool tryComputeProjectedFollowTargetMetrics( + static bool tryComputeProjectedFollowTargetMetrics( const SCameraProjectionContext& projectionContext, const core::CTrackedTarget& trackedTarget, SCameraProjectedTargetMetrics& outMetrics, - const float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon) - { - outMetrics = {}; - const hlsl::float32_t3 target = hlsl::CCameraMathUtilities::castVector(trackedTarget.getGimbal().getPosition()); - const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); - const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); - const auto clip = hlsl::mul(clipProjection, viewSpace); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(clip.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.z) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.w)) - return false; + float clipWEpsilon = SCameraFollowRegressionThresholds::DefaultClipWEpsilon); - const auto absW = hlsl::abs(clip.w); - if (absW < clipWEpsilon) - return false; - - const float invW = 1.0f / clip.w; - outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; - if (!hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.y)) - return false; - - outMetrics.radius = hlsl::length(outMetrics.ndc); - - return true; - } - - static inline bool validateProjectedFollowTargetContract( + static bool validateProjectedFollowTargetContract( const SCameraProjectionContext& projectionContext, const core::CTrackedTarget& trackedTarget, SCameraProjectedTargetMetrics& outMetrics, std::string* error = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) - { - if (!tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, outMetrics, thresholds.clipWEpsilon)) - { - if (error) - *error = "failed to project follow target"; - return false; - } + const SCameraFollowRegressionThresholds& thresholds = {}); - if (outMetrics.radius > thresholds.projectedNdcTolerance) - { - if (error) - { - *error = "projected target mismatch ndc=(" + std::to_string(outMetrics.ndc.x) + - "," + std::to_string(outMetrics.ndc.y) + ") radius=" + std::to_string(outMetrics.radius); - } - return false; - } - - return true; - } - - static inline SCameraFollowVisualMetrics buildFollowVisualMetrics( + static SCameraFollowVisualMetrics buildFollowVisualMetrics( core::ICamera* camera, const core::CTrackedTarget& trackedTarget, const core::SCameraFollowConfig* followConfig, - const SCameraProjectionContext* projectionContext = nullptr) - { - SCameraFollowVisualMetrics out = {}; - if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) - return out; - - out.active = true; - out.mode = followConfig->mode; + const SCameraProjectionContext* projectionContext = nullptr); - double targetDistance = 0.0; - out.lockValid = core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig->mode) && - core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); - if (out.lockValid) - out.targetDistance = static_cast(targetDistance); - - if (out.lockValid && projectionContext) - { - out.projectedValid = tryComputeProjectedFollowTargetMetrics(*projectionContext, trackedTarget, out.projectedTarget); - } - - return out; - } - - static inline bool validateFollowTargetContract( + static bool validateFollowTargetContract( core::ICamera* camera, const core::CTrackedTarget& trackedTarget, const core::SCameraFollowConfig& followConfig, @@ -196,120 +127,9 @@ struct CCameraFollowRegressionUtilities final SCameraFollowRegressionResult& out, std::string* error = nullptr, const SCameraProjectionContext* projectionContext = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) - { - out = {}; - if (!camera) - { - if (error) - *error = "missing camera"; - return false; - } - - if (core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig.mode)) - { - out.hasLockMetrics = core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); - if (!out.hasLockMetrics) - { - if (error) - *error = "failed to compute follow lock metrics"; - return false; - } - - const auto& trackedTargetGimbal = trackedTarget.getGimbal(); - const auto& cameraGimbal = camera->getGimbal(); - const hlsl::float64_t3 trackedTargetPosition = trackedTargetGimbal.getPosition(); - const hlsl::float64_t3 cameraPosition = cameraGimbal.getPosition(); - const double expectedTargetDistance = hlsl::length(trackedTargetPosition - cameraPosition); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) - { - if (error) - { - *error = "target distance mismatch actual=" + std::to_string(out.targetDistance) + - " expected=" + std::to_string(expectedTargetDistance); - } - return false; - } - - if (out.lockAngleDeg > thresholds.lockAngleToleranceDeg) - { - if (error) - *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); - return false; - } + const SCameraFollowRegressionThresholds& thresholds = {}); - if (projectionContext) - { - out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( - *projectionContext, - trackedTarget, - out.projectedTarget, - thresholds.clipWEpsilon); - if (!out.hasProjectedMetrics) - { - if (error) - *error = "failed to compute projected follow target metrics"; - return false; - } - - if (out.projectedTarget.radius > thresholds.projectedNdcTolerance) - { - if (error) - *error = "projected target mismatch ndc=(" + std::to_string(out.projectedTarget.ndc.x) + - "," + std::to_string(out.projectedTarget.ndc.y) + ") radius=" + std::to_string(out.projectedTarget.radius); - return false; - } - } - } - - if (camera->supportsGoalState(core::ICamera::GoalStateSphericalTarget)) - { - core::ICamera::SphericalTargetState state; - if (!camera->tryGetSphericalTargetState(state)) - { - if (error) - *error = "missing spherical target state"; - return false; - } - - out.hasSphericalState = true; - out.sphericalTarget = state.target; - out.sphericalDistance = state.distance; - - const auto& trackedTargetGimbal = trackedTarget.getGimbal(); - const auto& cameraGimbal = camera->getGimbal(); - const hlsl::float64_t3 trackedTargetPosition = trackedTargetGimbal.getPosition(); - const hlsl::float64_t3 targetDelta = state.target - trackedTargetPosition; - const double targetDeltaLen = hlsl::length(targetDelta); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) - { - if (error) - *error = "spherical target writeback mismatch"; - return false; - } - - const double actualDistance = hlsl::length(cameraGimbal.getPosition() - trackedTargetPosition); - const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : - (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(actualDistance) || !hlsl::CCameraMathUtilities::isFiniteScalar(expectedDistance) || - hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || - hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) - { - if (error) - { - *error = "spherical distance mismatch actual=" + std::to_string(actualDistance) + - " state=" + std::to_string(state.distance) + - " expected=" + std::to_string(expectedDistance); - } - return false; - } - } - - out.passed = true; - return true; - } - - static inline bool buildApplyAndValidateFollowTargetContract( + static bool buildApplyAndValidateFollowTargetContract( const core::CCameraGoalSolver& solver, core::ICamera* camera, const core::CTrackedTarget& trackedTarget, @@ -317,58 +137,7 @@ struct CCameraFollowRegressionUtilities final SCameraFollowApplyValidationResult& out, std::string* error = nullptr, const SCameraProjectionContext* projectionContext = nullptr, - const SCameraFollowRegressionThresholds& thresholds = {}) - { - out = {}; - - if (!core::CCameraFollowUtilities::tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) - { - if (error) - *error = "failed to build follow goal"; - return false; - } - out.hasGoal = true; - - out.applyResult = core::CCameraFollowUtilities::applyFollowToCamera(solver, camera, trackedTarget, followConfig); - if (!out.applyResult.succeeded()) - { - if (error) - *error = "failed to apply follow goal"; - return false; - } - - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - { - if (error) - *error = "failed to capture camera state after follow apply"; - return false; - } - - out.hasCapturedGoal = true; - out.capturedGoal = capture.goal; - if (!core::CCameraGoalUtilities::compareGoals(out.capturedGoal, out.goal, thresholds.positionTolerance, thresholds.rotationToleranceDeg, thresholds.scalarTolerance)) - { - if (error) - *error = std::string("follow goal mismatch. ") + core::CCameraGoalUtilities::describeGoalMismatch(out.capturedGoal, out.goal); - return false; - } - - if (!validateFollowTargetContract( - camera, - trackedTarget, - followConfig, - out.goal, - out.regression, - error, - projectionContext, - thresholds)) - { - return false; - } - - return true; - } + const SCameraFollowRegressionThresholds& thresholds = {}); }; } // namespace nbl::system diff --git a/include/nbl/ext/Cameras/CCameraFollowUtilities.hpp b/include/nbl/ext/Cameras/CCameraFollowUtilities.hpp index df687458b4..c960d80d57 100644 --- a/include/nbl/ext/Cameras/CCameraFollowUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraFollowUtilities.hpp @@ -28,12 +28,7 @@ class CTrackedTarget CTrackedTarget( const hlsl::float64_t3& position = hlsl::float64_t3(0.0), const hlsl::camera_quaternion_t& orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(), - std::string identifier = "Follow Target") - : m_identifier(std::move(identifier)), - m_gimbal(gimbal_t::base_t::SCreationParameters{ .position = position, .orientation = orientation }) - { - m_gimbal.updateView(); - } + std::string identifier = "Follow Target"); /// @brief Return the stable human-readable identifier of the tracked target. inline const std::string& getIdentifier() const { return m_identifier; } @@ -43,38 +38,16 @@ class CTrackedTarget inline gimbal_t& getGimbal() { return m_gimbal; } /// @brief Replace the tracked target pose in world space. - inline void setPose(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation) - { - m_gimbal.begin(); - m_gimbal.setPosition(position); - m_gimbal.setOrientation(orientation); - m_gimbal.end(); - m_gimbal.updateView(); - } + void setPose(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation); /// @brief Replace only the tracked target position. - inline void setPosition(const hlsl::float64_t3& position) - { - setPose(position, m_gimbal.getOrientation()); - } + void setPosition(const hlsl::float64_t3& position); /// @brief Replace only the tracked target orientation. - inline void setOrientation(const hlsl::camera_quaternion_t& orientation) - { - setPose(m_gimbal.getPosition(), orientation); - } + void setOrientation(const hlsl::camera_quaternion_t& orientation); /// @brief Replace the tracked target pose from a rigid transform matrix when possible. - inline bool trySetFromTransform(const hlsl::float64_t4x4& transform) - { - hlsl::float64_t3 position = hlsl::float64_t3(0.0); - hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); - if (!hlsl::CCameraMathUtilities::tryExtractRigidPoseFromTransform(transform, position, orientation)) - return false; - - setPose(position, orientation); - return true; - } + bool trySetFromTransform(const hlsl::float64_t4x4& transform); private: std::string m_identifier; @@ -219,170 +192,52 @@ struct CCameraFollowUtilities final } /// @brief Transform a tracked-target local offset into world space. - static inline hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) - { - return hlsl::CCameraMathUtilities::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); - } + static hlsl::float64_t3 transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset); /// @brief Project a world-space offset into the tracked target local frame. - static inline hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) - { - return hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); - } + static hlsl::float64_t3 projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset); /// @brief Build a look-at orientation that points from `position` toward the tracked target. - static inline bool buildFollowLookAtOrientation( + static bool buildFollowLookAtOrientation( const hlsl::float64_t3& position, const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& preferredUp, - hlsl::camera_quaternion_t& outOrientation) - { - return hlsl::CCameraMathUtilities::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); - } + hlsl::camera_quaternion_t& outOrientation); /// @brief Capture world-space and target-local follow offsets from the current camera pose. - static inline bool captureFollowOffsetsFromCamera( + static bool captureFollowOffsetsFromCamera( const CCameraGoalSolver& solver, ICamera* camera, const CTrackedTarget& trackedTarget, - SCameraFollowConfig& ioConfig) - { - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; - - const auto& targetGimbal = trackedTarget.getGimbal(); - ioConfig.worldOffset = capture.goal.position - targetGimbal.getPosition(); - ioConfig.localOffset = projectFollowWorldOffsetToLocal(targetGimbal, ioConfig.worldOffset); - return true; - } + SCameraFollowConfig& ioConfig); /// @brief Measure the angular lock error between a camera forward axis and a tracked target. - static inline bool tryComputeFollowTargetLockMetrics( + static bool tryComputeFollowTargetLockMetrics( const ICamera::CGimbal& cameraGimbal, const CTrackedTarget& trackedTarget, float& outAngleDeg, - double* outDistance = nullptr) - { - const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); - const auto targetDistance = hlsl::length(toTarget); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDistance) || targetDistance <= SCameraToolingThresholds::TinyScalarEpsilon) - return false; - - const auto forward = cameraGimbal.getZAxis(); - const auto forwardLength = hlsl::length(forward); - if (!hlsl::CCameraMathUtilities::isFiniteVec3(forward) || !hlsl::CCameraMathUtilities::isFiniteScalar(forwardLength) || forwardLength <= SCameraToolingThresholds::TinyScalarEpsilon) - return false; - - const auto forwardDirection = forward / forwardLength; - const auto targetDir = toTarget / targetDistance; - const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); - outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); - if (!hlsl::CCameraMathUtilities::isFiniteScalar(outAngleDeg)) - return false; - - if (outDistance) - *outDistance = targetDistance; - return true; - } + double* outDistance = nullptr); - static inline bool tryBuildFollowPositionGoal( + static bool tryBuildFollowPositionGoal( ICamera* camera, CCameraGoal& outGoal, const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& position, - const hlsl::float64_t3& preferredUp) - { - if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return CCameraGoalUtilities::buildCanonicalTargetRelativeGoalFromPosition(outGoal, targetPosition, position); - - outGoal.position = position; - return buildFollowLookAtOrientation(outGoal.position, targetPosition, preferredUp, outGoal.orientation) && CCameraGoalUtilities::isGoalFinite(outGoal); - } + const hlsl::float64_t3& preferredUp); - static inline bool tryBuildFollowGoal( + static bool tryBuildFollowGoal( const CCameraGoalSolver& solver, ICamera* camera, const CTrackedTarget& trackedTarget, const SCameraFollowConfig& config, - CCameraGoal& outGoal) - { - if (!camera || !config.enabled || config.mode == ECameraFollowMode::Disabled) - return false; - - const auto capture = solver.captureDetailed(camera); - if (!capture.canUseGoal()) - return false; - - outGoal = capture.goal; - - const auto& targetGimbal = trackedTarget.getGimbal(); - const auto targetPosition = targetGimbal.getPosition(); - - switch (config.mode) - { - case ECameraFollowMode::OrbitTarget: - { - if (!camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) - return false; - - if (outGoal.hasPathState) - { - return CCameraGoalUtilities::applyCanonicalPathGoalFields(outGoal, targetPosition, outGoal.pathState) && CCameraGoalUtilities::isGoalFinite(outGoal); - } - - const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; - if (!hasSphericalState) - return false; - - const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; - return CCameraGoalUtilities::applyCanonicalTargetRelativeGoal( - outGoal, - { - .target = targetPosition, - .orbitUv = outGoal.orbitUv, - .distance = orbitDistance - }); - } - - case ECameraFollowMode::LookAtTarget: - { - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, capture.goal.position, targetGimbal.getYAxis()); - } - - case ECameraFollowMode::KeepWorldOffset: - { - const auto position = targetPosition + config.worldOffset; - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); - } + CCameraGoal& outGoal); - case ECameraFollowMode::KeepLocalOffset: - { - const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); - return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); - } - - default: - return false; - } - } - - static inline CCameraGoalSolver::SApplyResult applyFollowToCamera( + static CCameraGoalSolver::SApplyResult applyFollowToCamera( const CCameraGoalSolver& solver, ICamera* camera, const CTrackedTarget& trackedTarget, const SCameraFollowConfig& config, - CCameraGoal* outGoal = nullptr) - { - CCameraGoal goal = {}; - if (!tryBuildFollowGoal(solver, camera, trackedTarget, config, goal)) - return {}; - - if (outGoal) - *outGoal = goal; - - return solver.applyDetailed(camera, goal); - } + CCameraGoal* outGoal = nullptr); }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/CCameraGoalSolver.hpp b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp index b5301957cb..b2a0dde179 100644 --- a/include/nbl/ext/Cameras/CCameraGoalSolver.hpp +++ b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp @@ -99,293 +99,12 @@ class CCameraGoalSolver } }; - bool buildEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const - { - out.clear(); - if (!camera) - return false; - - const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); - - if (camera->hasCapability(ICamera::SphericalTarget)) - return buildSphericalEvents(camera, canonicalTarget, out); - - return buildFreeEvents(camera, canonicalTarget, out); - } - - bool capture(ICamera* camera, CCameraGoal& out) const - { - out = {}; - if (!camera) - return false; - - const ICamera::CGimbal& gimbal = camera->getGimbal(); - out.position = hlsl::float64_t3(gimbal.getPosition()); - out.orientation = gimbal.getOrientation(); - out.sourceKind = camera->getKind(); - out.sourceCapabilities = ICamera::capability_flags_t(camera->getCapabilities()); - out.sourceGoalStateMask = ICamera::goal_state_flags_t(camera->getGoalStateMask()); - - ICamera::SphericalTargetState sphericalState; - if (camera->tryGetSphericalTargetState(sphericalState)) - { - out.targetPosition = sphericalState.target; - out.hasTargetPosition = true; - out.distance = sphericalState.distance; - out.hasDistance = true; - out.orbitDistance = sphericalState.distance; - out.orbitUv = sphericalState.orbitUv; - out.hasOrbitState = true; - } - - ICamera::DynamicPerspectiveState dynamicState; - if (camera->tryGetDynamicPerspectiveState(dynamicState)) - { - out.hasDynamicPerspectiveState = true; - out.dynamicPerspectiveState = dynamicState; - } - - ICamera::PathState pathState; - if (camera->tryGetPathState(pathState)) - { - out.hasPathState = true; - out.pathState = pathState; - } - - out = CCameraGoalUtilities::canonicalizeGoal(out); - return true; - } - - SCaptureResult captureDetailed(ICamera* camera) const - { - SCaptureResult result; - result.hasCamera = camera != nullptr; - if (!result.hasCamera) - return result; - - result.captured = capture(camera, result.goal); - result.finiteGoal = result.captured && CCameraGoalUtilities::isGoalFinite(result.goal); - return result; - } - - SCompatibilityResult analyzeCompatibility(const ICamera* camera, const CCameraGoal& target) const - { - SCompatibilityResult result; - if (!camera) - return result; - - const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); - result.sameKind = canonicalTarget.sourceKind == ICamera::CameraKind::Unknown || canonicalTarget.sourceKind == camera->getKind(); - result.supportedGoalStateMask = ICamera::goal_state_flags_t(camera->getGoalStateMask()); - result.requiredGoalStateMask = CCameraGoalUtilities::getRequiredGoalStateMask(canonicalTarget); - result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; - result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; - return result; - } - - SApplyResult applyDetailed(ICamera* camera, const CCameraGoal& target) const - { - SApplyResult result; - if (!camera) - return result; - - const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); - - bool exact = true; - bool absoluteChanged = false; - - if (!camera->hasCapability(ICamera::SphericalTarget)) - { - bool poseChanged = false; - bool poseExact = false; - if (tryApplyAbsoluteReferencePose(camera, canonicalTarget, poseChanged, poseExact)) - { - result.issues |= SApplyResult::EIssue::UsedAbsolutePoseFallback; - absoluteChanged = absoluteChanged || poseChanged; - if (poseExact && !canonicalTarget.hasDynamicPerspectiveState) - { - result.status = poseChanged ? - SApplyResult::EStatus::AppliedAbsoluteOnly : - SApplyResult::EStatus::AlreadySatisfied; - result.exact = true; - return result; - } - } - } - - if (canonicalTarget.hasTargetPosition) - { - ICamera::SphericalTargetState beforeState; - if (!camera->tryGetSphericalTargetState(beforeState)) - { - result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; - exact = false; - } - else - { - const auto beforeTarget = beforeState.target; - if (!camera->trySetSphericalTarget(canonicalTarget.targetPosition)) - { - result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; - exact = false; - } - else - { - ICamera::SphericalTargetState afterState; - if (!camera->tryGetSphericalTargetState(afterState)) - { - result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; - exact = false; - } - else - { - absoluteChanged = afterState.target != beforeTarget; - exact = exact && afterState.target == canonicalTarget.targetPosition; - } - } - } - } - - if (canonicalTarget.hasDistance || canonicalTarget.hasOrbitState) - { - ICamera::SphericalTargetState beforeState; - if (!camera->tryGetSphericalTargetState(beforeState)) - { - result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; - exact = false; - } - else - { - const float desiredDistance = canonicalTarget.hasOrbitState ? canonicalTarget.orbitDistance : canonicalTarget.distance; - const float beforeDistance = beforeState.distance; - if (!camera->trySetSphericalDistance(desiredDistance)) - { - result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; - exact = false; - } - else - { - ICamera::SphericalTargetState afterState; - if (!camera->tryGetSphericalTargetState(afterState)) - { - result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; - exact = false; - } - else - { - absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; - exact = exact && hlsl::abs(static_cast(afterState.distance - desiredDistance)) <= SCameraToolingThresholds::ScalarTolerance; - } - } - } - } - - if (canonicalTarget.hasPathState) - { - ICamera::PathState beforeState; - if (!camera->tryGetPathState(beforeState)) - { - result.issues |= SApplyResult::EIssue::MissingPathState; - exact = false; - } - else if (!camera->trySetPathState(canonicalTarget.pathState)) - { - result.issues |= SApplyResult::EIssue::MissingPathState; - exact = false; - } - else - { - ICamera::PathState afterState; - if (!camera->tryGetPathState(afterState)) - { - result.issues |= SApplyResult::EIssue::MissingPathState; - exact = false; - } - else - { - const auto thresholds = SCameraPathDefaults::ComparisonThresholds; - const bool pathChanged = CCameraPathUtilities::pathStatesChanged(beforeState, afterState, thresholds); - const bool pathExact = CCameraPathUtilities::pathStatesNearlyEqual(afterState, canonicalTarget.pathState, thresholds); - - absoluteChanged = absoluteChanged || pathChanged; - exact = exact && pathExact; - } - } - } - - if (canonicalTarget.hasDynamicPerspectiveState) - { - ICamera::DynamicPerspectiveState beforeState; - if (!camera->tryGetDynamicPerspectiveState(beforeState)) - { - result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; - exact = false; - } - else if (!camera->trySetDynamicPerspectiveState(canonicalTarget.dynamicPerspectiveState)) - { - result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; - exact = false; - } - else - { - ICamera::DynamicPerspectiveState afterState; - if (!camera->tryGetDynamicPerspectiveState(afterState)) - { - result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; - exact = false; - } - else - { - const bool dynamicChanged = !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) || - !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); - const bool dynamicExact = hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); - - absoluteChanged = absoluteChanged || dynamicChanged; - exact = exact && dynamicExact; - } - } - } - - std::vector events; - buildEvents(camera, canonicalTarget, events); - result.eventCount = static_cast(events.size()); - result.exact = exact; - - if (events.empty()) - { - if (absoluteChanged) - result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; - else if (exact) - result.status = SApplyResult::EStatus::AlreadySatisfied; - return result; - } - - if (camera->manipulate({ events.data(), events.size() })) - { - result.status = absoluteChanged ? - SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents : - SApplyResult::EStatus::AppliedVirtualEvents; - return result; - } - - if (absoluteChanged) - { - result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; - result.exact = false; - return result; - } - - result.issues |= SApplyResult::EIssue::VirtualEventReplayFailed; - result.status = SApplyResult::EStatus::Failed; - result.exact = false; - return result; - } - - bool apply(ICamera* camera, const CCameraGoal& target) const - { - return applyDetailed(camera, target).succeeded(); - } + bool buildEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const; + bool capture(ICamera* camera, CCameraGoal& out) const; + SCaptureResult captureDetailed(ICamera* camera) const; + SCompatibilityResult analyzeCompatibility(const ICamera* camera, const CCameraGoal& target) const; + SApplyResult applyDetailed(ICamera* camera, const CCameraGoal& target) const; + bool apply(ICamera* camera, const CCameraGoal& target) const; private: struct SGoalSolverDefaults final @@ -396,249 +115,33 @@ class CCameraGoalSolver static inline const hlsl::float64_t3 AngularToleranceDegVec = hlsl::float64_t3(SCameraToolingThresholds::DefaultAngularToleranceDeg); }; - inline void appendYawPitchRollEvents( + void appendYawPitchRollEvents( std::vector& events, const hlsl::float64_t3& eulerRadians, - const double denominator, - const bool includeRoll = true) const - { - static constexpr std::array RotationBindings = {{ - { CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown }, - { CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft }, - { CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft } - }}; - - auto tolerances = SGoalSolverDefaults::AngularToleranceDegVec; - if (!includeRoll) - tolerances.z = std::numeric_limits::infinity(); - - CCameraVirtualEventUtilities::appendAngularAxisEvents( - events, - eulerRadians, - hlsl::float64_t3(denominator), - tolerances, - RotationBindings); - } - - inline void appendPathDeltaEvents( + double denominator, + bool includeRoll = true) const; + void appendPathDeltaEvents( std::vector& events, const SCameraPathDelta& delta, - const double moveDenominator, - const double rotationDenominator) const - { - CCameraPathUtilities::appendPathDeltaEvents( - events, - delta, - moveDenominator, - rotationDenominator, - SCameraPathDefaults::ExactComparisonThresholds); - } - - inline double getMoveMagnitudeDenominator(const ICamera* camera) const - { - const double moveScale = camera->getMoveSpeedScale(); - return camera->getUnscaledVirtualTranslationMagnitude() * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); - } - - inline double getRotationMagnitudeDenominator(const ICamera* camera) const - { - const double rotationScale = camera->getRotationSpeedScale(); - return rotationScale == 0.0 ? SGoalSolverDefaults::UnitScale : rotationScale; - } - - inline bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const - { - outPositionDelta = 0.0; - outRotationDeltaDeg = 0.0; - if (!camera) - return false; - - const ICamera::CGimbal& gimbal = camera->getGimbal(); - hlsl::SCameraPoseDelta poseDelta = {}; - if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(gimbal.getPosition(), gimbal.getOrientation(), target.position, target.orientation, poseDelta)) - return false; - - outPositionDelta = poseDelta.position; - outRotationDeltaDeg = poseDelta.rotationDeg; - return true; - } - - inline bool tryApplyAbsoluteReferencePose(ICamera* camera, const CCameraGoal& target, bool& outChanged, bool& outExact) const - { - outChanged = false; - outExact = false; - if (!camera) - return false; - - switch (camera->getKind()) - { - case ICamera::CameraKind::Free: - case ICamera::CameraKind::FPS: - break; - default: - return false; - } - - double beforePosDelta = 0.0; - double beforeRotDeltaDeg = 0.0; - if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) - return false; - - if (beforePosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && beforeRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg) - { - outExact = true; - return true; - } - - const auto targetFrame = hlsl::CCameraMathUtilities::composeTransformMatrix(target.position, target.orientation); - - camera->manipulate({}, &targetFrame); - - double afterPosDelta = 0.0; - double afterRotDeltaDeg = 0.0; - if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) - return false; - - outChanged = !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) || - !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)); - outExact = afterPosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && afterRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg; - return true; - } - - inline bool buildTargetRelativeEvents( + double moveDenominator, + double rotationDenominator) const; + double getMoveMagnitudeDenominator(const ICamera* camera) const; + double getRotationMagnitudeDenominator(const ICamera* camera) const; + bool computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const; + bool tryApplyAbsoluteReferencePose(ICamera* camera, const CCameraGoal& target, bool& outChanged, bool& outExact) const; + bool buildTargetRelativeEvents( ICamera* camera, const ICamera::SphericalTargetState& sphericalState, const SCameraTargetRelativeState& goal, std::vector& out, - const SCameraTargetRelativeEventPolicy& policy) const - { - const auto delta = CCameraTargetRelativeUtilities::buildTargetRelativeDelta(sphericalState, goal); - CCameraTargetRelativeUtilities::appendTargetRelativeDeltaEvents( - out, - delta, - policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), - SCameraToolingThresholds::DefaultAngularToleranceDeg, - camera->getUnscaledVirtualTranslationMagnitude(), - SCameraToolingThresholds::ScalarTolerance, - policy); - return !out.empty(); - } - - inline bool buildPathEvents(ICamera* camera, const CCameraGoal& target, const ICamera::SphericalTargetState& sphericalState, std::vector& out) const - { - if (!camera) - return false; - - const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; - ICamera::PathState currentState = {}; - const ICamera::PathState* currentStateOverride = camera->tryGetPathState(currentState) ? ¤tState : nullptr; - ICamera::PathStateLimits pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); - camera->tryGetPathStateLimits(pathLimits); - SCameraPathStateTransition transition = {}; - if (!CCameraPathUtilities::tryBuildPathStateTransition( - effectiveTarget, - camera->getGimbal().getPosition(), - target.position, - pathLimits, - currentStateOverride, - target.hasPathState ? &target.pathState : nullptr, - transition)) - { - return false; - } - - const auto moveDenom = getMoveMagnitudeDenominator(camera); - const auto rotationDenom = getRotationMagnitudeDenominator(camera); - appendPathDeltaEvents(out, transition.delta, moveDenom, rotationDenom); - return !out.empty(); - } - - inline bool buildSphericalEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const - { - ICamera::SphericalTargetState sphericalState; - if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) - return false; - - if (camera->getKind() == ICamera::CameraKind::Path) - return buildPathEvents(camera, target, sphericalState, out); - - SCameraTargetRelativeState goal; - if (!CCameraGoalUtilities::tryResolveCanonicalTargetRelativeState(target, sphericalState, goal)) - return false; - - switch (camera->getKind()) - { - case ICamera::CameraKind::Orbit: - case ICamera::CameraKind::DollyZoom: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); - - case ICamera::CameraKind::Turntable: - case ICamera::CameraKind::Arcball: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::RotateDistancePolicy); - - case ICamera::CameraKind::TopDown: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::TopDownPolicy); - - case ICamera::CameraKind::Isometric: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::IsometricPolicy); - - case ICamera::CameraKind::Dolly: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::DollyPolicy); - - case ICamera::CameraKind::Chase: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::ChasePolicy); - - default: - return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); - } - } - - inline bool buildFreeEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const - { - const ICamera::CGimbal& gimbal = camera->getGimbal(); - const hlsl::float64_t3 currentPos = gimbal.getPosition(); - const hlsl::float64_t3 deltaWorld = target.position - currentPos; - CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents( - out, - gimbal.getOrientation(), - deltaWorld, - SGoalSolverDefaults::UnitAxisDenominator, - SGoalSolverDefaults::ScalarToleranceVec); - - switch (camera->getKind()) - { - case ICamera::CameraKind::FPS: - { - const hlsl::float64_t2 currentPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(gimbal.getOrientation()); - const hlsl::float64_t2 targetPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(target.orientation); - - const double rotScale = camera->getRotationSpeedScale(); - const double invScale = rotScale == 0.0 ? SGoalSolverDefaults::UnitScale : (SGoalSolverDefaults::UnitScale / rotScale); - - appendYawPitchRollEvents( - out, - hlsl::float64_t3( - hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.x - currentPitchYaw.x) * invScale, - hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.y - currentPitchYaw.y) * invScale, - 0.0), - SGoalSolverDefaults::UnitScale, - false); - } break; - - case ICamera::CameraKind::Free: - { - appendYawPitchRollEvents( - out, - hlsl::CCameraMathUtilities::getOrientationDeltaEulerRadiansYXZ(gimbal.getOrientation(), target.orientation), - SGoalSolverDefaults::UnitScale); - } break; - - default: - break; - } - - return !out.empty(); - } + const SCameraTargetRelativeEventPolicy& policy) const; + bool buildPathEvents( + ICamera* camera, + const CCameraGoal& target, + const ICamera::SphericalTargetState& sphericalState, + std::vector& out) const; + bool buildSphericalEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const; + bool buildFreeEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const; }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp b/include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp index 3529925e6d..24daf077c6 100644 --- a/include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraInputBindingUtilities.hpp @@ -93,467 +93,32 @@ struct CCameraInputBindingUtilities final static inline constexpr double ImguizmoScaleUnitsPerFactor = 1.0; }; - static inline bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) - { - return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); - } + static bool hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset); - static inline bool hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) - { - return containsBindingForAnyCodeGroups( - mousePreset, - SCameraInputBindingPhysicalGroups::PositiveScrollCodes, - SCameraInputBindingPhysicalGroups::NegativeScrollCodes); - } + static bool hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset); - static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera::CameraKind kind) - { - return interactionBindingPresetForKind(kind).keyboard; - } + static const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(core::ICamera::CameraKind kind); - static inline const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) - { - return getDefaultCameraKeyboardMappingPreset(camera.getKind()); - } + static const IGimbalBindingLayout::keyboard_to_virtual_events_t& getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera); - static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera::CameraKind kind) - { - return interactionBindingPresetForKind(kind).mouse; - } + static const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(core::ICamera::CameraKind kind); - static inline const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera) - { - return getDefaultCameraMouseMappingPreset(camera.getKind()); - } + static const IGimbalBindingLayout::mouse_to_virtual_events_t& getDefaultCameraMouseMappingPreset(const core::ICamera& camera); - static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const uint32_t allowedVirtualEvents) - { - return makeImguizmoPreset(allowedVirtualEvents); - } + static IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(uint32_t allowedVirtualEvents); - static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) - { - return buildDefaultCameraImguizmoMappingPreset(camera.getAllowedVirtualEvents()); - } + static IGimbalBindingLayout::imguizmo_to_virtual_events_t buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera); - static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera::CameraKind kind, const uint32_t allowedVirtualEvents) - { - SCameraInputBindingPreset preset; - preset.keyboard = getDefaultCameraKeyboardMappingPreset(kind); - preset.mouse = getDefaultCameraMouseMappingPreset(kind); - preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(allowedVirtualEvents); - return preset; - } + static SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(core::ICamera::CameraKind kind, uint32_t allowedVirtualEvents); - static inline SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera) - { - return buildDefaultCameraInputBindingPreset(camera.getKind(), camera.getAllowedVirtualEvents()); - } + static SCameraInputBindingPreset buildDefaultCameraInputBindingPreset(const core::ICamera& camera); - static inline void applyDefaultCameraInputBindingPreset( + static void applyDefaultCameraInputBindingPreset( IGimbalBindingLayout& layout, - const core::ICamera::CameraKind kind, - const uint32_t allowedVirtualEvents) - { - const auto preset = buildDefaultCameraInputBindingPreset(kind, allowedVirtualEvents); - layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); - layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); - layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); - } - - static inline void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) - { - applyDefaultCameraInputBindingPreset(layout, camera.getKind(), camera.getAllowedVirtualEvents()); - } - -private: - using virtual_event_t = core::CVirtualGimbalEvent::VirtualEventType; - using keyboard_axis_group_t = std::array; - using mouse_axis_group_t = std::array; - using scalar_axis_pair_t = std::array; - - struct SKeyboardPresetSpec final - { - keyboard_axis_group_t wasd = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double wasdScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - scalar_axis_pair_t qe = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double qeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - keyboard_axis_group_t ijkl = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double ijklScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - }; - - struct SMousePresetSpec final - { - mouse_axis_group_t relative = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double relativeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - scalar_axis_pair_t scroll = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None - }; - double scrollScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - }; - - /// @brief Shared virtual-event bundles reused across interaction families. - struct SCameraInputBindingEventGroups final - { - static inline constexpr std::array FpsMove = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array OrbitTranslate = { - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array OrbitZoom = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward - }; - static inline constexpr std::array VerticalMove = { - core::CVirtualGimbalEvent::MoveDown, - core::CVirtualGimbalEvent::MoveUp - }; - static inline constexpr std::array PathRigProgressAndU = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveRight - }; - static inline constexpr std::array PathRigV = VerticalMove; - static inline constexpr std::array TurntableMove = { - core::CVirtualGimbalEvent::MoveForward, - core::CVirtualGimbalEvent::MoveBackward, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array LookYawPitch = { - core::CVirtualGimbalEvent::TiltDown, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array Roll = { - core::CVirtualGimbalEvent::RollLeft, - core::CVirtualGimbalEvent::RollRight - }; - static inline constexpr std::array PanOnly = { - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::None, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::PanRight - }; - static inline constexpr std::array RelativeLook = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::TiltUp, - core::CVirtualGimbalEvent::TiltDown - }; - static inline constexpr std::array RelativeOrbitTranslate = { - core::CVirtualGimbalEvent::MoveRight, - core::CVirtualGimbalEvent::MoveLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }; - static inline constexpr std::array RelativeTopDown = { - core::CVirtualGimbalEvent::PanRight, - core::CVirtualGimbalEvent::PanLeft, - core::CVirtualGimbalEvent::MoveUp, - core::CVirtualGimbalEvent::MoveDown - }; - }; + core::ICamera::CameraKind kind, + uint32_t allowedVirtualEvents); - struct SCameraInteractionBindingSpec - { - SKeyboardPresetSpec keyboard = {}; - SMousePresetSpec mouse = {}; - }; - - struct SCameraMappedInteractionBindingSpec - { - IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; - IGimbalBindingLayout::mouse_to_virtual_events_t mouse; - }; - - template - static inline bool containsBindingForAnyCode(const Map& preset, const Codes& codes) - { - for (const auto code : codes) - { - if (preset.find(code) != preset.end()) - return true; - } - return false; - } - - template - static inline bool containsBindingForAnyCodeGroups(const Map& preset, const Codes&... codes) - { - return (containsBindingForAnyCode(preset, codes) || ...); - } - - static inline constexpr size_t interactionFamilyIndex(const core::ECameraInteractionFamily family) - { - return static_cast(family); - } - - template - static inline void appendBindingSpec(Map& preset, const Codes& codes, const Events& events, const double magnitudeScale) - { - for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) - { - const auto event = events[i]; - if (event == core::CVirtualGimbalEvent::None) - continue; - preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event, magnitudeScale)); - } - } - - template - static inline void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event, const double magnitudeScale) - { - if (event == core::CVirtualGimbalEvent::None) - return; - - std::array> duplicatedEvents = {}; - duplicatedEvents.fill(event); - appendBindingSpec(preset, codes, duplicatedEvents, magnitudeScale); - } - - static inline IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) - { - IGimbalBindingLayout::keyboard_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd, spec.wasdScale); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe, spec.qeScale); - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl, spec.ijklScale); - return preset; - } - - static inline IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) - { - IGimbalBindingLayout::mouse_to_virtual_events_t preset; - appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative, spec.relativeScale); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0], spec.scrollScale); - appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1], spec.scrollScale); - return preset; - } - - static inline IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) - { - IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; - for (const auto event : core::CVirtualGimbalEvent::VirtualEventsTypeTable) - { - if (event == core::CVirtualGimbalEvent::None) - continue; - if ((allowedVirtualEvents & event) != event) - continue; - preset.emplace(event, IGimbalBindingLayout::CHashInfo(event, getDefaultImguizmoMagnitudeScale(event))); - } - return preset; - } - - static inline double getDefaultImguizmoMagnitudeScale(const virtual_event_t event) - { - if (core::CVirtualGimbalEvent::isTranslationEvent(event)) - return SInputMagnitudeDefaults::ImguizmoTranslationUnitsPerWorldUnit; - if (core::CVirtualGimbalEvent::isRotationEvent(event)) - return SInputMagnitudeDefaults::ImguizmoRotationUnitsPerRadian; - if (core::CVirtualGimbalEvent::isScaleEvent(event)) - return SInputMagnitudeDefaults::ImguizmoScaleUnitsPerFactor; - return IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; - } - - static inline constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = {}; - - static inline constexpr SKeyboardPresetSpec FpsKeyboardSpec = { - SCameraInputBindingEventGroups::FpsMove, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, - SCameraInputBindingEventGroups::LookYawPitch, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond - }; - - static inline constexpr SKeyboardPresetSpec FreeKeyboardSpec = { - FpsKeyboardSpec.wasd, - FpsKeyboardSpec.wasdScale, - SCameraInputBindingEventGroups::Roll, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - FpsKeyboardSpec.ijkl, - FpsKeyboardSpec.ijklScale - }; - - static inline constexpr SKeyboardPresetSpec OrbitKeyboardSpec = { - SCameraInputBindingEventGroups::OrbitTranslate, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - SCameraInputBindingEventGroups::OrbitZoom, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale - }; - - static inline constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = { - FpsKeyboardSpec.wasd, - FpsKeyboardSpec.wasdScale, - SCameraInputBindingEventGroups::VerticalMove, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - FpsKeyboardSpec.ijkl, - FpsKeyboardSpec.ijklScale - }; - - static inline constexpr SKeyboardPresetSpec TurntableKeyboardSpec = { - SCameraInputBindingEventGroups::TurntableMove, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, - FpsKeyboardSpec.ijkl, - FpsKeyboardSpec.ijklScale - }; - - static inline constexpr SKeyboardPresetSpec TopDownKeyboardSpec = { - OrbitKeyboardSpec.wasd, - OrbitKeyboardSpec.wasdScale, - OrbitKeyboardSpec.qe, - OrbitKeyboardSpec.qeScale, - SCameraInputBindingEventGroups::PanOnly, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond - }; - - static inline constexpr SKeyboardPresetSpec PathKeyboardSpec = { - SCameraInputBindingEventGroups::PathRigProgressAndU, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - SCameraInputBindingEventGroups::PathRigV, - SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale - }; - - static inline constexpr SMousePresetSpec FpsMouseSpec = { - SCameraInputBindingEventGroups::RelativeLook, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - {}, - IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale - }; - - static inline constexpr SMousePresetSpec OrbitMouseSpec = { - SCameraInputBindingEventGroups::RelativeOrbitTranslate, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - SCameraInputBindingEventGroups::OrbitZoom, - SInputMagnitudeDefaults::ScrollUnitsPerStep - }; - - static inline constexpr SMousePresetSpec TargetRigMouseSpec = { - FpsMouseSpec.relative, - FpsMouseSpec.relativeScale, - OrbitMouseSpec.scroll, - OrbitMouseSpec.scrollScale - }; - - static inline constexpr SMousePresetSpec TopDownMouseSpec = { - SCameraInputBindingEventGroups::RelativeTopDown, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - OrbitMouseSpec.scroll, - OrbitMouseSpec.scrollScale - }; - - static inline constexpr SMousePresetSpec PathMouseSpec = { - SCameraInputBindingEventGroups::RelativeOrbitTranslate, - SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, - SCameraInputBindingEventGroups::OrbitZoom, - SInputMagnitudeDefaults::ScrollUnitsPerStep - }; - - static inline constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = { - FpsKeyboardSpec, - FpsMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec FreeInteractionBindingSpec = { - FreeKeyboardSpec, - FpsMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec OrbitInteractionBindingSpec = { - OrbitKeyboardSpec, - OrbitMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec TargetRigInteractionBindingSpec = { - TargetRigKeyboardSpec, - TargetRigMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec TurntableInteractionBindingSpec = { - TurntableKeyboardSpec, - TargetRigMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec TopDownInteractionBindingSpec = { - TopDownKeyboardSpec, - TopDownMouseSpec - }; - - static inline constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = { - PathKeyboardSpec, - PathMouseSpec - }; - - template - static inline auto makePresetCache(const SpecArray& specs, Builder&& builder) - { - std::array> cache = {}; - for (size_t i = 0u; i < specs.size(); ++i) - cache[i] = builder(specs[i]); - return cache; - } - - static inline SCameraMappedInteractionBindingSpec mapInteractionBindingSpec(const SCameraInteractionBindingSpec& spec) - { - return { - .keyboard = buildKeyboardPreset(spec.keyboard), - .mouse = buildMousePreset(spec.mouse) - }; - } - - static inline constexpr std::array InteractionFamilyPresetSpecs = {{ - EmptyInteractionBindingSpec, - FpsInteractionBindingSpec, - FreeInteractionBindingSpec, - OrbitInteractionBindingSpec, - TargetRigInteractionBindingSpec, - TurntableInteractionBindingSpec, - TopDownInteractionBindingSpec, - PathInteractionBindingSpec - }}; - - static inline const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) - { - const auto familyIx = interactionFamilyIndex(core::CCameraKindUtilities::getCameraInteractionFamily(kind)); - static const auto cache = makePresetCache( - InteractionFamilyPresetSpecs, - [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); - return cache[familyIx < cache.size() ? familyIx : 0u]; - } + static void applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera); }; } // namespace nbl::ui diff --git a/include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp b/include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp index 44894889c7..823f2a4c84 100644 --- a/include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp +++ b/include/nbl/ext/Cameras/CCameraKeyframeTrack.hpp @@ -25,149 +25,32 @@ struct CCameraKeyframeTrackUtilities final { public: /// @brief Compare two keyframes by authored time and shared preset state. - static inline bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) - { - return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && - CCameraPresetUtilities::comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); - } + static bool compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, + double timeEps, double posEps, double rotEpsDeg, double scalarEps); /// @brief Compare two authored keyframe tracks with optional selection-state checking. - static inline bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection = true) - { - if ((compareSelection && lhs.selectedKeyframeIx != rhs.selectedKeyframeIx) || lhs.keyframes.size() != rhs.keyframes.size()) - return false; + static bool compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + double timeEps, double posEps, double rotEpsDeg, double scalarEps, bool compareSelection = true); - for (size_t i = 0u; i < lhs.keyframes.size(); ++i) - { - if (!compareKeyframes(lhs.keyframes[i], rhs.keyframes[i], timeEps, posEps, rotEpsDeg, scalarEps)) - return false; - } + /// @brief Compare only the serialized/authored content of two tracks and ignore transient UI selection state. + static bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + double timeEps, double posEps, double rotEpsDeg, double scalarEps); - return true; - } + static bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, float time, CCameraPreset& preset); - /// @brief Compare only the serialized/authored content of two tracks and ignore transient UI selection state. - static inline bool compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, - const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) - { - return compareKeyframeTracks(lhs, rhs, timeEps, posEps, rotEpsDeg, scalarEps, false); - } - - static inline bool tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) - { - if (track.keyframes.empty()) - return false; - - if (track.keyframes.size() == 1u) - { - preset = track.keyframes.front().preset; - return true; - } - - const auto clampedTime = std::clamp(time, 0.f, track.keyframes.back().time); - size_t idx = 0u; - while (idx + 1u < track.keyframes.size() && track.keyframes[idx + 1u].time < clampedTime) - ++idx; - - const auto& a = track.keyframes[idx]; - const auto& b = track.keyframes[std::min(idx + 1u, track.keyframes.size() - 1u)]; - if (b.time <= a.time) - { - preset = a.preset; - return true; - } - - const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); - preset = a.preset; - CCameraPresetUtilities::assignGoalToPreset( - preset, - CCameraGoalUtilities::blendGoals( - CCameraPresetUtilities::makeGoalFromPreset(a.preset), - CCameraPresetUtilities::makeGoalFromPreset(b.preset), - alpha)); - return true; - } - - static inline void sortKeyframeTrackByTime(CCameraKeyframeTrack& track) - { - std::sort(track.keyframes.begin(), track.keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); - } - - static inline void clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time) - { - if (track.keyframes.empty()) - { - time = 0.f; - return; - } - - time = std::clamp(time, 0.f, track.keyframes.back().time); - } - - static inline int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const float time) - { - if (track.keyframes.empty()) - { - track.selectedKeyframeIx = -1; - return track.selectedKeyframeIx; - } - - size_t bestIx = 0u; - float bestDelta = hlsl::abs(track.keyframes.front().time - time); - for (size_t i = 1u; i < track.keyframes.size(); ++i) - { - const float delta = hlsl::abs(track.keyframes[i].time - time); - if (delta < bestDelta) - { - bestDelta = delta; - bestIx = i; - } - } - - track.selectedKeyframeIx = static_cast(bestIx); - return track.selectedKeyframeIx; - } - - static inline void normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track) - { - if (track.keyframes.empty()) - { - track.selectedKeyframeIx = -1; - return; - } - - if (track.selectedKeyframeIx < 0) - track.selectedKeyframeIx = 0; - else if (track.selectedKeyframeIx >= static_cast(track.keyframes.size())) - track.selectedKeyframeIx = static_cast(track.keyframes.size()) - 1; - } - - static inline CCameraKeyframe* getSelectedKeyframe(CCameraKeyframeTrack& track) - { - normalizeSelectedKeyframeTrack(track); - if (track.selectedKeyframeIx < 0) - return nullptr; - return &track.keyframes[static_cast(track.selectedKeyframeIx)]; - } - - static inline const CCameraKeyframe* getSelectedKeyframe(const CCameraKeyframeTrack& track) - { - if (track.selectedKeyframeIx < 0 || track.selectedKeyframeIx >= static_cast(track.keyframes.size())) - return nullptr; - return &track.keyframes[static_cast(track.selectedKeyframeIx)]; - } - - static inline bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset) - { - auto* selected = getSelectedKeyframe(track); - if (!selected) - return false; - - selected->preset = std::move(preset); - return true; - } + static void sortKeyframeTrackByTime(CCameraKeyframeTrack& track); + + static void clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time); + + static int selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, float time); + + static void normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track); + + static CCameraKeyframe* getSelectedKeyframe(CCameraKeyframeTrack& track); + + static const CCameraKeyframe* getSelectedKeyframe(const CCameraKeyframeTrack& track); + + static bool replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset); }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/CCameraPathUtilities.hpp b/include/nbl/ext/Cameras/CCameraPathUtilities.hpp index 736b944f56..42387f5fe1 100644 --- a/include/nbl/ext/Cameras/CCameraPathUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraPathUtilities.hpp @@ -173,27 +173,12 @@ struct SCameraPathModel final struct CCameraPathUtilities final { /// @brief Build the default path state used by the built-in model. - static inline ICamera::PathState makeDefaultPathState(const double minU = SCameraPathDefaults::MinU) - { - return { - .s = 0.0, - .u = minU, - .v = 0.0, - .roll = 0.0 - }; - } + static ICamera::PathState makeDefaultPathState(double minU = SCameraPathDefaults::MinU); /// @brief Build path-state comparison tolerances from caller-provided angular and scalar thresholds. - static inline SCameraPathComparisonThresholds makePathComparisonThresholds( - const double angularToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, - const double scalarTolerance = SCameraPathDefaults::ScalarTolerance) - { - return { - .sToleranceDeg = angularToleranceDeg, - .rollToleranceDeg = angularToleranceDeg, - .scalarTolerance = scalarTolerance - }; - } + static SCameraPathComparisonThresholds makePathComparisonThresholds( + double angularToleranceDeg = SCameraPathDefaults::AngleToleranceDeg, + double scalarTolerance = SCameraPathDefaults::ScalarTolerance); /// @brief Return the default path-state limits used when a camera does not expose custom ones. static inline constexpr SCameraPathLimits makeDefaultPathLimits() @@ -202,372 +187,120 @@ struct CCameraPathUtilities final } /// @brief Check whether every scalar stored in the path state is finite. - static inline bool isPathStateFinite(const ICamera::PathState& state) - { - return hlsl::CCameraMathUtilities::isFiniteScalar(state.s) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.u) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.v) && - hlsl::CCameraMathUtilities::isFiniteScalar(state.roll); - } + static bool isPathStateFinite(const ICamera::PathState& state); /// @brief Check whether the path limits can be sanitized into a valid numeric domain. - static inline bool isPathLimitsWellFormed(const SCameraPathLimits& limits) - { - return hlsl::CCameraMathUtilities::isFiniteScalar(limits.minU) && - hlsl::CCameraMathUtilities::isFiniteScalar(limits.minDistance) && - !std::isnan(static_cast(limits.maxDistance)); - } + static bool isPathLimitsWellFormed(const SCameraPathLimits& limits); /// @brief Clamp and normalize path-state limits into a valid numeric domain. - static inline bool sanitizePathLimits(SCameraPathLimits& limits) - { - if (!isPathLimitsWellFormed(limits)) - return false; - - limits.minU = std::max(limits.minU, 0.0); - limits.minDistance = std::max( - std::max(limits.minDistance, static_cast(limits.minU)), - static_cast(SCameraTargetRelativeTraits::MinDistance)); - - if (!std::isfinite(static_cast(limits.maxDistance))) - limits.maxDistance = std::numeric_limits::infinity(); - else - limits.maxDistance = std::max(limits.maxDistance, limits.minDistance); - return true; - } + static bool sanitizePathLimits(SCameraPathLimits& limits); /// @brief Sanitize a path state against a caller-provided `minU` lower bound. - static inline bool sanitizePathState(ICamera::PathState& state, const double minU) - { - return hlsl::CCameraMathUtilities::sanitizePathState(state.s, state.u, state.v, state.roll, minU); - } + static bool sanitizePathState(ICamera::PathState& state, double minU); /// @brief Sanitize a path state against a full limit bundle and optionally report the applied distance. - static inline bool sanitizePathState(ICamera::PathState& state, const SCameraPathLimits& limits, double* outAppliedDistance = nullptr) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits)) - return false; - - if (!sanitizePathState(state, sanitizedLimits.minU)) - return false; - - const auto desiredDistance = std::clamp( - hlsl::CCameraMathUtilities::getPathDistance(state.u, state.v), - sanitizedLimits.minDistance, - sanitizedLimits.maxDistance); - return tryScalePathStateDistance(desiredDistance, sanitizedLimits.minU, state, outAppliedDistance); - } + static bool sanitizePathState(ICamera::PathState& state, const SCameraPathLimits& limits, double* outAppliedDistance = nullptr); /// @brief Rescale the `(u, v)` pair so the path state reaches the requested radial distance. - static inline bool tryScalePathStateDistance( - const double desiredDistance, - const double minU, + static bool tryScalePathStateDistance( + double desiredDistance, + double minU, ICamera::PathState& ioState, - double* outAppliedDistance = nullptr) - { - return hlsl::CCameraMathUtilities::tryScalePathStateDistance( - desiredDistance, - minU, - ioState.u, - ioState.v, - outAppliedDistance); - } + double* outAppliedDistance = nullptr); /// @brief Update the distance encoded by a path state while respecting the provided limits. - static inline bool tryUpdatePathStateDistance( - const float desiredDistance, + static bool tryUpdatePathStateDistance( + float desiredDistance, const SCameraPathLimits& limits, ICamera::PathState& ioState, - SCameraPathDistanceUpdateResult* outResult = nullptr) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits) || !sanitizePathState(ioState, sanitizedLimits)) - return false; - - const auto clampedDistance = std::clamp(desiredDistance, sanitizedLimits.minDistance, sanitizedLimits.maxDistance); - double appliedDistance = 0.0; - if (!tryScalePathStateDistance(static_cast(clampedDistance), sanitizedLimits.minU, ioState, &appliedDistance)) - return false; - - if (outResult) - { - outResult->appliedDistance = appliedDistance; - outResult->exact = (clampedDistance == desiredDistance) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); - } - return true; - } + SCameraPathDistanceUpdateResult* outResult = nullptr); - static inline bool tryBuildPathStateFromPosition( + static bool tryBuildPathStateFromPosition( const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& position, - const double minU, - ICamera::PathState& outState) - { - outState = {}; - if (!hlsl::CCameraMathUtilities::tryBuildPathStateFromPosition( - targetPosition, - position, - minU, - outState.s, - outState.u, - outState.v)) - { - return false; - } - - outState.roll = 0.0; - return true; - } + double minU, + ICamera::PathState& outState); - static inline bool tryResolvePathState( + static bool tryResolvePathState( const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& position, const SCameraPathLimits& limits, const ICamera::PathState* requestedState, - ICamera::PathState& outState) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits)) - return false; - - if (requestedState) - { - outState = *requestedState; - return sanitizePathState(outState, sanitizedLimits); - } + ICamera::PathState& outState); - if (tryBuildPathStateFromPosition(targetPosition, position, sanitizedLimits.minU, outState)) - return sanitizePathState(outState, sanitizedLimits); - - outState = makeDefaultPathState(sanitizedLimits.minU); - return sanitizePathState(outState, sanitizedLimits); - } - - static inline bool tryBuildPathPoseFromState( + static bool tryBuildPathPoseFromState( const hlsl::float64_t3& targetPosition, const ICamera::PathState& state, const SCameraPathLimits& limits, - SCameraPathPose& outPose) - { - SCameraPathLimits sanitizedLimits = limits; - if (!sanitizePathLimits(sanitizedLimits)) - return false; - - return hlsl::CCameraMathUtilities::tryBuildPathPoseFromState( - targetPosition, - state.s, - state.u, - state.v, - state.roll, - sanitizedLimits.minU, - sanitizedLimits.minDistance, - sanitizedLimits.maxDistance, - outPose.position, - outPose.orientation, - &outPose.appliedDistance, - &outPose.orbitUv); - } + SCameraPathPose& outPose); - static inline bool tryBuildPathPoseFromState( + static bool tryBuildPathPoseFromState( const hlsl::float64_t3& targetPosition, const ICamera::PathState& state, const SCameraPathLimits& limits, hlsl::float64_t3& outPosition, hlsl::camera_quaternion_t& outOrientation, hlsl::float64_t* outAppliedDistance = nullptr, - hlsl::float64_t2* outOrbitUv = nullptr) - { - SCameraPathPose pathPose = {}; - if (!tryBuildPathPoseFromState(targetPosition, state, limits, pathPose)) - return false; - - outPosition = pathPose.position; - outOrientation = pathPose.orientation; - if (outAppliedDistance) - *outAppliedDistance = pathPose.appliedDistance; - if (outOrbitUv) - *outOrbitUv = pathPose.orbitUv; - return true; - } + hlsl::float64_t2* outOrbitUv = nullptr); - static inline bool pathStatesNearlyEqual( + static bool pathStatesNearlyEqual( const ICamera::PathState& lhs, const ICamera::PathState& rhs, - const SCameraPathComparisonThresholds& thresholds = {}) - { - return hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.s, rhs.s) <= thresholds.sToleranceDeg && - hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.u, rhs.u, thresholds.scalarTolerance) && - hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.v, rhs.v, thresholds.scalarTolerance) && - hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.roll, rhs.roll) <= thresholds.rollToleranceDeg; - } + const SCameraPathComparisonThresholds& thresholds = {}); - static inline bool pathStatesChanged( + static bool pathStatesChanged( const ICamera::PathState& lhs, const ICamera::PathState& rhs, - const SCameraPathComparisonThresholds& thresholds = {}) - { - return !pathStatesNearlyEqual(lhs, rhs, thresholds); - } + const SCameraPathComparisonThresholds& thresholds = {}); - static inline hlsl::float64_t4 buildPathStateDeltaVector( + static hlsl::float64_t4 buildPathStateDeltaVector( const ICamera::PathState& currentState, - const ICamera::PathState& desiredState) - { - auto deltaVector = desiredState.asVector() - currentState.asVector(); - deltaVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.x); - deltaVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.w); - return deltaVector; - } + const ICamera::PathState& desiredState); - static inline SCameraPathDelta buildPathStateDelta( + static SCameraPathDelta buildPathStateDelta( const ICamera::PathState& currentState, - const ICamera::PathState& desiredState) - { - return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); - } + const ICamera::PathState& desiredState); - static inline SCameraPathDelta makePathDeltaFromVirtualPathMotion( + static SCameraPathDelta makePathDeltaFromVirtualPathMotion( const hlsl::float64_t3& translation, - const hlsl::float64_t3& rotation = hlsl::float64_t3(0.0)) - { - return SCameraPathDelta::fromMotion(translation, rotation.z); - } + const hlsl::float64_t3& rotation = hlsl::float64_t3(0.0)); - static inline SCameraPathDelta buildDefaultPathControlDelta(const SCameraPathControlContext& context) - { - return makePathDeltaFromVirtualPathMotion(context.translation, context.rotation); - } + static SCameraPathDelta buildDefaultPathControlDelta(const SCameraPathControlContext& context); - static inline void appendPathDeltaEvents( + static void appendPathDeltaEvents( std::vector& events, const SCameraPathDelta& delta, - const double moveDenominator, - const double rotationDenominator, - const SCameraPathComparisonThresholds& thresholds = {}) - { - CCameraVirtualEventUtilities::appendLocalTranslationEvents( - events, - delta.translationVector(), - hlsl::float64_t3(moveDenominator), - hlsl::float64_t3(thresholds.scalarTolerance)); - CCameraVirtualEventUtilities::appendAngularDeltaEvent( - events, - delta.roll, - rotationDenominator, - thresholds.rollToleranceDeg, - CVirtualGimbalEvent::RollRight, - CVirtualGimbalEvent::RollLeft); - } + double moveDenominator, + double rotationDenominator, + const SCameraPathComparisonThresholds& thresholds = {}); - static inline bool tryBuildCanonicalPathState( + static bool tryBuildCanonicalPathState( const hlsl::float64_t3& targetPosition, const ICamera::PathState& state, const SCameraPathLimits& limits, - SCameraCanonicalPathState& outState) - { - outState = {}; - if (!tryBuildPathPoseFromState(targetPosition, state, limits, outState.pose)) - return false; - - outState.targetRelative = { - .target = targetPosition, - .orbitUv = outState.pose.orbitUv, - .distance = static_cast(outState.pose.appliedDistance) - }; - return true; - } + SCameraCanonicalPathState& outState); - static inline bool tryApplyPathStateDelta( + static bool tryApplyPathStateDelta( const ICamera::PathState& currentState, const SCameraPathDelta& delta, const SCameraPathLimits& limits, - ICamera::PathState& outState) - { - auto stateVector = currentState.asVector() + delta.asVector(); - stateVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.x); - stateVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.w); - outState = ICamera::PathState::fromVector(stateVector); - return sanitizePathState(outState, limits); - } + ICamera::PathState& outState); - static inline ICamera::PathState blendPathStates( + static ICamera::PathState blendPathStates( const ICamera::PathState& from, const ICamera::PathState& to, - const double alpha) - { - const auto fromVector = from.asVector(); - const auto toVector = to.asVector(); - return { - .s = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.x, toVector.x, alpha), - .u = fromVector.y + (toVector.y - fromVector.y) * alpha, - .v = fromVector.z + (toVector.z - fromVector.z) * alpha, - .roll = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.w, toVector.w, alpha) - }; - } + double alpha); - static inline bool tryBuildPathStateTransition( + static bool tryBuildPathStateTransition( const hlsl::float64_t3& targetPosition, const hlsl::float64_t3& currentPosition, const hlsl::float64_t3& desiredPosition, const SCameraPathLimits& limits, const ICamera::PathState* currentStateOverride, const ICamera::PathState* desiredStateOverride, - SCameraPathStateTransition& outTransition) - { - if (!tryResolvePathState(targetPosition, currentPosition, limits, currentStateOverride, outTransition.current)) - return false; - if (!tryResolvePathState(targetPosition, desiredPosition, limits, desiredStateOverride, outTransition.desired)) - return false; - - outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); - return true; - } + SCameraPathStateTransition& outTransition); - static inline SCameraPathModel makeDefaultPathModel() - { - return { - .resolveState = - [](const hlsl::float64_t3& targetPosition, - const hlsl::float64_t3& position, - const SCameraPathLimits& limits, - const ICamera::PathState* requestedState, - ICamera::PathState& outState) -> bool - { - return tryResolvePathState(targetPosition, position, limits, requestedState, outState); - }, - .controlLaw = - [](const SCameraPathControlContext& context) -> SCameraPathDelta - { - return buildDefaultPathControlDelta(context); - }, - .integrate = - [](const ICamera::PathState& currentState, - const SCameraPathDelta& delta, - const SCameraPathLimits& limits, - ICamera::PathState& outState) -> bool - { - return tryApplyPathStateDelta(currentState, delta, limits, outState); - }, - .evaluate = - [](const hlsl::float64_t3& targetPosition, - const ICamera::PathState& state, - const SCameraPathLimits& limits, - SCameraCanonicalPathState& outState) -> bool - { - return tryBuildCanonicalPathState(targetPosition, state, limits, outState); - }, - .updateDistance = - [](const float desiredDistance, - const SCameraPathLimits& limits, - ICamera::PathState& ioState, - SCameraPathDistanceUpdateResult* outResult) -> bool - { - return tryUpdatePathStateDistance(desiredDistance, limits, ioState, outResult); - } - }; - } + static SCameraPathModel makeDefaultPathModel(); }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp b/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp index 026daf1afc..abce94e4e1 100644 --- a/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp +++ b/include/nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp @@ -67,41 +67,20 @@ struct CCameraScriptedCheckFrameResult struct CCameraScriptedCheckRunnerUtilities final { - static inline void scriptedCheckSetStepReference( + static void scriptedCheckSetStepReference( CCameraScriptedCheckRuntimeState& state, const hlsl::float64_t3& position, - const hlsl::camera_quaternion_t& orientation) - { - state.step.valid = true; - state.step.position = position; - state.step.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); - } - - static inline void scriptedCheckSetBaselineReference( + const hlsl::camera_quaternion_t& orientation); + static void scriptedCheckSetBaselineReference( CCameraScriptedCheckRuntimeState& state, const hlsl::float64_t3& position, - const hlsl::camera_quaternion_t& orientation) - { - state.baseline.valid = true; - state.baseline.position = position; - state.baseline.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); - scriptedCheckSetStepReference(state, position, orientation); - } - - static inline bool scriptedCheckComputePoseDelta( + const hlsl::camera_quaternion_t& orientation); + static bool scriptedCheckComputePoseDelta( const hlsl::float64_t3& currentPosition, const hlsl::camera_quaternion_t& currentOrientation, const hlsl::float64_t3& referencePosition, const hlsl::camera_quaternion_t& referenceOrientation, - hlsl::SCameraPoseDelta& outDelta) - { - return hlsl::CCameraMathUtilities::tryComputePoseDelta( - currentPosition, - currentOrientation, - referencePosition, - referenceOrientation, - outDelta); - } + hlsl::SCameraPoseDelta& outDelta); template static inline std::string buildScriptedCheckMessage(Fn&& formatter) @@ -111,452 +90,16 @@ struct CCameraScriptedCheckRunnerUtilities final return oss.str(); } - static inline void appendScriptedCheckLog( + static void appendScriptedCheckLog( CCameraScriptedCheckFrameResult& result, - const bool failure, - std::string&& text) - { - result.logs.push_back({ - .failure = failure, - .text = std::move(text) - }); - result.hadFailures = result.hadFailures || failure; - } + bool failure, + std::string&& text); /// @brief Evaluate all authored scripted checks scheduled for the current frame. - static inline CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( + static CCameraScriptedCheckFrameResult evaluateScriptedChecksForFrame( const std::vector& checks, CCameraScriptedCheckRuntimeState& state, - const CCameraScriptedCheckContext& context) - { - CCameraScriptedCheckFrameResult result = {}; - - while (state.nextCheckIndex < checks.size() && checks[state.nextCheckIndex].frame == context.frame) - { - const auto& check = checks[state.nextCheckIndex]; - - if (!context.camera) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] check frame=" << context.frame << " no active camera"; - })); - ++state.nextCheckIndex; - continue; - } - - const auto& gimbal = context.camera->getGimbal(); - const auto pos = gimbal.getPosition(); - const auto orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); - const auto eulerDeg = hlsl::CCameraMathUtilities::castVector(hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(orientation)); - - if (!hlsl::CCameraMathUtilities::isFiniteVec3(pos) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(orientation) || !hlsl::CCameraMathUtilities::isFiniteVec3(eulerDeg)) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] check frame=" << context.frame << " non-finite gimbal state"; - })); - ++state.nextCheckIndex; - continue; - } - - switch (check.kind) - { - case CCameraScriptedInputCheck::Kind::Baseline: - { - scriptedCheckSetBaselineReference(state, pos, orientation); - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(3); - oss << "[script][pass] baseline frame=" << context.frame - << " pos=(" << pos.x << ", " << pos.y << ", " << pos.z << ")" - << " euler_deg=(" << eulerDeg.x << ", " << eulerDeg.y << ", " << eulerDeg.z << ")"; - })); - break; - } - case CCameraScriptedInputCheck::Kind::ImguizmoVirtual: - { - bool ok = true; - if (!context.imguizmoVirtual || context.imguizmoVirtualCount == 0u) - { - ok = false; - } - else - { - for (const auto& expected : check.expectedVirtualEvents) - { - bool found = false; - double actual = 0.0; - for (uint32_t i = 0u; i < context.imguizmoVirtualCount; ++i) - { - if (context.imguizmoVirtual[i].type == expected.type) - { - found = true; - actual = context.imguizmoVirtual[i].magnitude; - break; - } - } - - if (!found || hlsl::abs(actual - expected.magnitude) > check.tolerance) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] imguizmo_virtual frame=" << context.frame - << " type=" << core::CVirtualGimbalEvent::virtualEventToString(expected.type).data() - << " expected=" << expected.magnitude - << " actual=" << actual - << " tol=" << check.tolerance; - })); - } - } - } - - if (ok) - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][pass] imguizmo_virtual frame=" << context.frame - << " events=" << check.expectedVirtualEvents.size(); - })); - } - break; - } - case CCameraScriptedInputCheck::Kind::GimbalNear: - { - bool ok = true; - if (check.hasExpectedPos) - { - const double distance = hlsl::length(pos - hlsl::CCameraMathUtilities::castVector(check.expectedPos)); - if (distance > check.posTolerance) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_near frame=" << context.frame - << " pos_diff=" << distance - << " tol=" << check.posTolerance; - })); - } - } - if (check.hasExpectedEuler) - { - const auto expectedOrientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( - hlsl::CCameraMathUtilities::castVector(check.expectedEulerDeg)); - hlsl::SCameraPoseDelta poseDelta = {}; - if (!scriptedCheckComputePoseDelta(pos, orientation, pos, expectedOrientation, poseDelta)) - poseDelta.rotationDeg = std::numeric_limits::infinity(); - const auto rotationDeltaDeg = poseDelta.rotationDeg; - if (rotationDeltaDeg > check.eulerToleranceDeg) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_near frame=" << context.frame - << " rot_delta_deg=" << rotationDeltaDeg - << " tol=" << check.eulerToleranceDeg; - })); - } - } - - if (ok) - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][pass] gimbal_near frame=" << context.frame; - })); - } - break; - } - case CCameraScriptedInputCheck::Kind::GimbalDelta: - { - if (!state.baseline.valid) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_delta frame=" << context.frame << " missing baseline"; - })); - break; - } - - hlsl::SCameraPoseDelta poseDelta = {}; - if (!scriptedCheckComputePoseDelta(pos, orientation, state.baseline.position, state.baseline.orientation, poseDelta)) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_delta frame=" << context.frame << " non-finite pose delta"; - })); - break; - } - - if (poseDelta.position > check.posTolerance || poseDelta.rotationDeg > check.eulerToleranceDeg) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_delta frame=" << context.frame - << " pos_diff=" << poseDelta.position - << " tol=" << check.posTolerance - << " rot_delta_deg=" << poseDelta.rotationDeg - << " tol=" << check.eulerToleranceDeg; - })); - } - else - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][pass] gimbal_delta frame=" << context.frame - << " pos_diff=" << poseDelta.position - << " rot_delta_deg=" << poseDelta.rotationDeg; - })); - } - break; - } - case CCameraScriptedInputCheck::Kind::GimbalStep: - { - if (!state.step.valid) - { - if (state.baseline.valid) - { - scriptedCheckSetStepReference(state, state.baseline.position, state.baseline.orientation); - } - else - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_step frame=" << context.frame << " missing step reference"; - })); - scriptedCheckSetStepReference(state, pos, orientation); - ++state.nextCheckIndex; - continue; - } - } - - hlsl::SCameraPoseDelta poseDelta = {}; - if (!scriptedCheckComputePoseDelta(pos, orientation, state.step.position, state.step.orientation, poseDelta)) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] gimbal_step frame=" << context.frame << " non-finite pose delta"; - })); - scriptedCheckSetStepReference(state, pos, orientation); - break; - } - - bool ok = true; - bool requiresProgress = false; - bool hasProgress = false; - if (check.hasPosDeltaConstraint) - { - if (poseDelta.position > check.posTolerance) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " pos_delta=" << poseDelta.position - << " max=" << check.posTolerance; - })); - } - if (check.minPosDelta > 0.0f) - { - requiresProgress = true; - hasProgress = hasProgress || poseDelta.position >= check.minPosDelta; - } - } - if (check.hasEulerDeltaConstraint) - { - if (poseDelta.rotationDeg > check.eulerToleranceDeg) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " rot_delta_deg=" << poseDelta.rotationDeg - << " max=" << check.eulerToleranceDeg; - })); - } - if (check.minEulerDeltaDeg > 0.0f) - { - requiresProgress = true; - hasProgress = hasProgress || poseDelta.rotationDeg >= check.minEulerDeltaDeg; - } - } - if (requiresProgress && !hasProgress) - { - ok = false; - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][fail] gimbal_step frame=" << context.frame - << " missing progress pos_delta=" << poseDelta.position - << " rot_delta_deg=" << poseDelta.rotationDeg; - })); - } - - if (ok) - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][pass] gimbal_step frame=" << context.frame - << " pos_delta=" << poseDelta.position - << " rot_delta_deg=" << poseDelta.rotationDeg; - })); - } - scriptedCheckSetStepReference(state, pos, orientation); - break; - } - case CCameraScriptedInputCheck::Kind::FollowTargetLock: - { - if (!context.followConfig) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << " missing follow config"; - })); - break; - } - if (!context.trackedTarget) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << " missing tracked target"; - })); - break; - } - if (!context.goalSolver) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << " missing goal solver"; - })); - break; - } - - SCameraFollowRegressionResult regression = {}; - std::string regressionError; - core::CCameraGoal expectedFollowGoal = {}; - const auto thresholds = CCameraFollowRegressionUtilities::makeFollowRegressionThresholds(check.posTolerance, check.eulerToleranceDeg); - const bool ok = core::CCameraFollowUtilities::tryBuildFollowGoal( - *context.goalSolver, - context.camera, - *context.trackedTarget, - *context.followConfig, - expectedFollowGoal) && - CCameraFollowRegressionUtilities::validateFollowTargetContract( - context.camera, - *context.trackedTarget, - *context.followConfig, - expectedFollowGoal, - regression, - ®ressionError, - context.followProjectionContext, - thresholds); - - if (!ok) - { - appendScriptedCheckLog( - result, - true, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << "[script][fail] follow_lock frame=" << context.frame << ' ' - << (regressionError.empty() ? "follow validation mismatch" : regressionError); - })); - } - else - { - appendScriptedCheckLog( - result, - false, - buildScriptedCheckMessage([&](std::ostringstream& oss) - { - oss << std::fixed << std::setprecision(6); - oss << "[script][pass] follow_lock frame=" << context.frame - << " angle_deg=" << regression.lockAngleDeg - << " target_distance=" << regression.targetDistance - << " screen_ndc=" << regression.projectedTarget.radius; - })); - } - break; - } - } - - ++state.nextCheckIndex; - } - - return result; - } + const CCameraScriptedCheckContext& context); }; } // namespace nbl::system diff --git a/include/nbl/ext/Cameras/CCameraSequenceScript.hpp b/include/nbl/ext/Cameras/CCameraSequenceScript.hpp index 1db018fba6..feb026ad3f 100644 --- a/include/nbl/ext/Cameras/CCameraSequenceScript.hpp +++ b/include/nbl/ext/Cameras/CCameraSequenceScript.hpp @@ -314,476 +314,50 @@ struct CCameraSequenceCompiledFramePolicy struct CCameraSequenceScriptUtilities final { - static inline bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) - { - if (value == "FPS") - outKind = ICamera::CameraKind::FPS; - else if (value == "Free") - outKind = ICamera::CameraKind::Free; - else if (value == "Orbit") - outKind = ICamera::CameraKind::Orbit; - else if (value == "Arcball") - outKind = ICamera::CameraKind::Arcball; - else if (value == "Turntable") - outKind = ICamera::CameraKind::Turntable; - else if (value == "TopDown") - outKind = ICamera::CameraKind::TopDown; - else if (value == "Isometric") - outKind = ICamera::CameraKind::Isometric; - else if (value == "Chase") - outKind = ICamera::CameraKind::Chase; - else if (value == "Dolly") - outKind = ICamera::CameraKind::Dolly; - else if (value == "DollyZoom" || value == "Dolly Zoom") - outKind = ICamera::CameraKind::DollyZoom; - else if (value == "PathRig" || value == "Path Rig") - outKind = ICamera::CameraKind::Path; - else - return false; - - return true; - } - - static inline bool tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType) - { - if (value == "perspective" || value == "Perspective") - outType = IPlanarProjection::CProjection::Perspective; - else if (value == "orthographic" || value == "Orthographic") - outType = IPlanarProjection::CProjection::Orthographic; - else - return false; - - return true; - } - - static inline void normalizeCaptureFractions(std::vector& fractions) - { - for (auto& fraction : fractions) - fraction = std::clamp(fraction, 0.f, 1.f); - - std::sort(fractions.begin(), fractions.end()); - fractions.erase(std::unique(fractions.begin(), fractions.end(), - [](const float lhs, const float rhs) { return hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs, rhs, static_cast(SCameraToolingThresholds::ScalarTolerance)); }), - fractions.end()); - } - - static inline bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr) - { - if (authored.hasAbsolutePreset) - { - outPreset = authored.absolutePreset; - if (outPreset.identifier.empty()) - outPreset.identifier = reference.identifier; - if (outPreset.name.empty()) - outPreset.name = reference.name; - return CCameraGoalUtilities::isGoalFinite(CCameraPresetUtilities::makeGoalFromPreset(outPreset)); - } - - outPreset = reference; - if (!authored.hasDelta) - return true; - - auto goal = CCameraPresetUtilities::makeGoalFromPreset(reference); - const auto& delta = authored.delta; - - const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; - const bool hasSphericalDelta = delta.hasTargetOffset || delta.orbitDelta.hasAny(); - const bool hasPathDelta = delta.pathDelta.hasAny(); - - if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) - { - if (error) - *error = "Sequence keyframe delta cannot mix pose offsets with spherical/path deltas."; - return false; - } - - if (delta.hasPositionOffset) - goal.position += delta.positionOffset; - - if (delta.hasRotationEulerDegOffset) - { - goal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(goal.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::CCameraMathUtilities::castVector(delta.rotationEulerDegOffset))); - } - - if (delta.hasTargetOffset) - { - if (!goal.hasTargetPosition) - { - if (error) - *error = "Sequence keyframe target_offset requires target state."; - return false; - } - goal.targetPosition += delta.targetOffset; - } - - if (delta.orbitDelta.hasAny()) - { - if (!goal.hasOrbitState) - { - if (error) - *error = "Sequence keyframe orbit deltas require spherical orbit state."; - return false; - } - - if (delta.orbitDelta.hasU) - goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad(goal.orbitUv.x + delta.orbitDelta.uvDeltaRad.x); - if (delta.orbitDelta.hasV) - { - goal.orbitUv.y = std::clamp( - goal.orbitUv.y + delta.orbitDelta.uvDeltaRad.y, - -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, - SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); - } - if (delta.orbitDelta.hasDistance) - goal.orbitDistance += delta.orbitDelta.distanceDelta; - } - - if (delta.pathDelta.hasAny()) - { - if (!goal.hasPathState) - { - if (error) - *error = "Sequence keyframe path deltas require path state."; - return false; - } - - if (!CCameraPathUtilities::tryApplyPathStateDelta( - goal.pathState, - delta.pathDelta.buildAppliedDelta(), - CCameraPathUtilities::makeDefaultPathLimits(), - goal.pathState)) - { - if (error) - *error = "Sequence keyframe path deltas produced an invalid path state."; - return false; - } - } - - if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) - { - if (!goal.hasDynamicPerspectiveState) - { - if (error) - *error = "Sequence keyframe dynamic perspective deltas require dynamic perspective state."; - return false; - } - if (delta.hasDynamicBaseFovDelta) - goal.dynamicPerspectiveState.baseFov = std::clamp(goal.dynamicPerspectiveState.baseFov + delta.dynamicBaseFovDelta, 1.f, 179.f); - if (delta.hasDynamicReferenceDistanceDelta) - goal.dynamicPerspectiveState.referenceDistance = std::max(0.001f, goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); - } - - if (hasPathDelta || hasSphericalDelta) - { - if (!CCameraGoalUtilities::applyCanonicalGoalState(goal)) - { - if (error) - *error = hasPathDelta ? - "Sequence keyframe failed to canonicalize path state." : - "Sequence keyframe failed to canonicalize spherical state."; - return false; - } - } - - if (!CCameraGoalUtilities::isGoalFinite(goal)) - { - if (error) - *error = "Sequence keyframe produced a non-finite goal."; - return false; - } - - CCameraPresetUtilities::assignGoalToPreset(outPreset, goal); - return true; - } - - static inline bool buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error = nullptr) - { - outTrack = {}; - outTrack.keyframes.reserve(segment.keyframes.size()); - - for (const auto& entry : segment.keyframes) - { - CCameraKeyframe keyframe; - keyframe.time = std::max(0.f, entry.time); - if (!buildSequenceKeyframePreset(reference, entry, keyframe.preset, error)) - return false; - outTrack.keyframes.emplace_back(std::move(keyframe)); - } - - CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(outTrack); - CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(outTrack); - return !outTrack.keyframes.empty(); - } - - static inline bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) - { - return hlsl::CCameraMathUtilities::isFiniteVec3(pose.position) && - hlsl::CCameraMathUtilities::isFiniteQuaternion(pose.orientation); - } - - static inline bool buildSequenceTrackedTargetPoseFromReference( - const CCameraSequenceTrackedTargetPose& reference, - const CCameraSequenceTrackedTargetKeyframe& authored, - CCameraSequenceTrackedTargetPose& outPose, - std::string* error = nullptr) - { - outPose = reference; - - if (authored.hasAbsolutePosition) - outPose.position = authored.absolutePosition; - if (authored.hasAbsoluteRotationEulerDeg) - outPose.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::CCameraMathUtilities::castVector(authored.absoluteRotationEulerDeg)); - - if (authored.hasDelta) - { - if (authored.delta.hasPositionOffset) - outPose.position += authored.delta.positionOffset; - if (authored.delta.hasRotationEulerDegOffset) - outPose.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(outPose.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ(hlsl::CCameraMathUtilities::castVector(authored.delta.rotationEulerDegOffset))); - } - - if (!isSequenceTrackedTargetPoseFinite(outPose)) - { - if (error) - *error = "Sequence target keyframe produced a non-finite pose."; - return false; - } - - return true; - } - - static inline bool buildSequenceTrackedTargetTrackFromReference( - const CCameraSequenceTrackedTargetPose& reference, - const CCameraSequenceSegment& segment, - CCameraSequenceTrackedTargetTrack& outTrack, - std::string* error = nullptr) - { - outTrack = {}; - outTrack.keyframes.reserve(segment.targetKeyframes.size()); - - for (const auto& entry : segment.targetKeyframes) - { - CCameraSequenceTrackedTargetTrack::SKeyframe keyframe; - keyframe.time = std::max(0.f, entry.time); - if (!buildSequenceTrackedTargetPoseFromReference(reference, entry, keyframe.pose, error)) - return false; - outTrack.keyframes.emplace_back(std::move(keyframe)); - } - - std::stable_sort(outTrack.keyframes.begin(), outTrack.keyframes.end(), - [](const auto& lhs, const auto& rhs) - { - if (lhs.time == rhs.time) - return false; - return lhs.time < rhs.time; - }); - - std::vector normalized; - normalized.reserve(outTrack.keyframes.size()); - for (const auto& keyframe : outTrack.keyframes) - { - if (!normalized.empty() && hlsl::CCameraMathUtilities::nearlyEqualScalar(normalized.back().time, keyframe.time, static_cast(SCameraToolingThresholds::ScalarTolerance))) - normalized.back() = keyframe; - else - normalized.emplace_back(keyframe); - } - outTrack.keyframes = std::move(normalized); - - return !outTrack.keyframes.empty(); - } - - static inline bool tryBuildSequenceTrackedTargetPoseAtTime( - const CCameraSequenceTrackedTargetTrack& track, - const float time, - CCameraSequenceTrackedTargetPose& outPose) - { - if (track.keyframes.empty()) - return false; - if (track.keyframes.size() == 1u || time <= track.keyframes.front().time) - { - outPose = track.keyframes.front().pose; - return true; - } - if (time >= track.keyframes.back().time) - { - outPose = track.keyframes.back().pose; - return true; - } - - for (size_t ix = 1u; ix < track.keyframes.size(); ++ix) - { - const auto& lhs = track.keyframes[ix - 1u]; - const auto& rhs = track.keyframes[ix]; - if (time > rhs.time) - continue; - - const auto span = std::max(static_cast(SCameraToolingThresholds::ScalarTolerance), rhs.time - lhs.time); - const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); - outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); - outPose.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); - return true; - } - - outPose = track.keyframes.back().pose; - return true; - } - - static inline bool sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment) - { - return !segment.targetKeyframes.empty(); - } - - static inline float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr) - { - if (segment.hasDurationSeconds) - return std::max(0.f, segment.durationSeconds); - if (script.defaults.durationSeconds > 0.f) - return script.defaults.durationSeconds; - if (track) - return track->keyframes.empty() ? 0.f : track->keyframes.back().time; - return 0.f; - } - - static inline const std::vector& getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - return segment.presentations.empty() ? script.defaults.presentations : segment.presentations; - } - - static inline CCameraSequenceContinuitySettings getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - return segment.hasContinuity ? segment.continuity : script.defaults.continuity; - } - - static inline std::vector getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - auto captures = segment.hasCaptureFractions ? segment.captureFractions : script.defaults.captureFractions; - normalizeCaptureFractions(captures); - return captures; - } - - static inline bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) - { - return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; - } - - static inline bool sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script) - { - if (script.defaults.presentations.size() > 1u) - return true; - - for (const auto& segment : script.segments) - { - if (getSequenceSegmentPresentations(script, segment).size() > 1u) - return true; - } - - return false; - } - - static inline uint64_t buildSequenceDurationFrames(const float durationSeconds, const float fps) - { - const auto safeDuration = std::max(0.f, durationSeconds); - const auto safeFps = std::max(1.f, fps); - return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); - } - - /// @brief Build one sampled time per authored frame in the compiled segment. - static inline void buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) - { - outTimes.clear(); - outTimes.reserve(durationFrames); - - for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) - { - const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; - outTimes.emplace_back(durationSeconds * alpha); - } - } - - /// @brief Expand normalized capture fractions into concrete frame offsets inside the compiled segment. - static inline void buildSequenceCaptureFrameOffsets( - const uint64_t durationFrames, - const std::vector& captureFractions, - std::vector& outOffsets) - { - outOffsets.clear(); - outOffsets.reserve(captureFractions.size()); - - for (const auto fraction : captureFractions) - { - const auto offset = durationFrames > 1u ? - static_cast(std::llround(static_cast(fraction) * static_cast(durationFrames - 1u))) : - 0ull; - outOffsets.emplace_back(offset); - } - - std::sort(outOffsets.begin(), outOffsets.end()); - outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); - } - - /// @brief Compile one authored sequence segment into normalized reusable data for runtime consumers. - static inline bool compileSequenceSegmentFromReference( - const CCameraSequenceScript& script, - const CCameraSequenceSegment& segment, - const CCameraPreset& referencePreset, - const CCameraSequenceTrackedTargetPose& referenceTrackedTargetPose, - CCameraSequenceCompiledSegment& outSegment, - std::string* error = nullptr) - { - outSegment = {}; - outSegment.name = segment.name; - outSegment.presentations = getSequenceSegmentPresentations(script, segment); - outSegment.continuity = getSequenceSegmentContinuity(script, segment); - outSegment.resetCamera = getSequenceSegmentResetCamera(script, segment); - - if (!buildSequenceTrackFromReference(referencePreset, segment, outSegment.track, error)) - return false; - - if (sequenceSegmentUsesTrackedTargetTrack(segment) && - !buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, outSegment.trackedTargetTrack, error)) - { - return false; - } - - outSegment.durationSeconds = getSequenceSegmentDurationSeconds(script, segment, &outSegment.track); - outSegment.durationFrames = buildSequenceDurationFrames(outSegment.durationSeconds, script.fps); - buildSequenceSampleTimes(outSegment.durationSeconds, outSegment.durationFrames, outSegment.sampleTimes); - buildSequenceCaptureFrameOffsets(outSegment.durationFrames, getSequenceSegmentCaptureFractions(script, segment), outSegment.captureFrameOffsets); - return true; - } - - static inline bool buildCompiledSegmentFramePolicies( - const CCameraSequenceCompiledSegment& segment, - std::vector& outPolicies, - const bool includeFollowTargetLock = false) - { - if (segment.sampleTimes.size() != segment.durationFrames) - return false; - - outPolicies.clear(); - outPolicies.reserve(segment.durationFrames); - - size_t captureIx = 0u; - for (uint64_t frameOffset = 0u; frameOffset < segment.durationFrames; ++frameOffset) - { - CCameraSequenceCompiledFramePolicy policy; - policy.frameOffset = frameOffset; - policy.sampleTime = segment.sampleTimes[frameOffset]; - policy.baseline = segment.continuity.baseline && frameOffset == 0u; - policy.continuityStep = segment.continuity.step && frameOffset > 0u; - policy.followTargetLock = includeFollowTargetLock && segment.usesTrackedTargetTrack() && policy.continuityStep; - - while (captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] < frameOffset) - ++captureIx; - policy.capture = captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] == frameOffset; - if (policy.capture) - ++captureIx; - - outPolicies.emplace_back(std::move(policy)); - } - - return true; - } + static bool tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind); + static bool tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType); + static void normalizeCaptureFractions(std::vector& fractions); + static bool buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error = nullptr); + static bool buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error = nullptr); + static bool isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose); + static bool buildSequenceTrackedTargetPoseFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceTrackedTargetKeyframe& authored, + CCameraSequenceTrackedTargetPose& outPose, + std::string* error = nullptr); + static bool buildSequenceTrackedTargetTrackFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceSegment& segment, + CCameraSequenceTrackedTargetTrack& outTrack, + std::string* error = nullptr); + static bool tryBuildSequenceTrackedTargetPoseAtTime( + const CCameraSequenceTrackedTargetTrack& track, + float time, + CCameraSequenceTrackedTargetPose& outPose); + static bool sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment); + static float getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track = nullptr); + static const std::vector& getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment); + static CCameraSequenceContinuitySettings getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment); + static std::vector getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment); + static bool getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment); + static bool sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script); + static uint64_t buildSequenceDurationFrames(float durationSeconds, float fps); + static void buildSequenceSampleTimes(float durationSeconds, uint64_t durationFrames, std::vector& outTimes); + static void buildSequenceCaptureFrameOffsets( + uint64_t durationFrames, + const std::vector& captureFractions, + std::vector& outOffsets); + static bool compileSequenceSegmentFromReference( + const CCameraSequenceScript& script, + const CCameraSequenceSegment& segment, + const CCameraPreset& referencePreset, + const CCameraSequenceTrackedTargetPose& referenceTrackedTargetPose, + CCameraSequenceCompiledSegment& outSegment, + std::string* error = nullptr); + static bool buildCompiledSegmentFramePolicies( + const CCameraSequenceCompiledSegment& segment, + std::vector& outPolicies, + bool includeFollowTargetLock = false); }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/ILinearProjection.hpp b/include/nbl/ext/Cameras/ILinearProjection.hpp index 66c21489dd..c4c0ce2d51 100644 --- a/include/nbl/ext/Cameras/ILinearProjection.hpp +++ b/include/nbl/ext/Cameras/ILinearProjection.hpp @@ -39,8 +39,8 @@ class ILinearProjection : virtual public core::IReferenceCounted using projection_matrix_t = concatenated_matrix_t; using inv_projection_matrix_t = inv_concatenated_matrix_t; - CProjection() : CProjection(projection_matrix_t(1)) {} - CProjection(const projection_matrix_t& matrix) { setProjectionMatrix(matrix); } + CProjection(); + explicit CProjection(const projection_matrix_t& matrix); /// @brief Returns P (Projection matrix) inline const projection_matrix_t& getProjectionMatrix() const { return m_projectionMatrix; } @@ -69,26 +69,7 @@ class ILinearProjection : virtual public core::IReferenceCounted protected: /// @brief Replace the projection matrix and rebuild cached handedness and inverse information. - inline void setProjectionMatrix(const projection_matrix_t& matrix) - { - m_projectionMatrix = matrix; - const auto det = hlsl::determinant(m_projectionMatrix); - - // we will allow you to lose a dimension since such a projection itself *may* - // be valid, however then you cannot un-project because the inverse doesn't exist! - m_isProjectionSingular = not det; - - if (m_isProjectionSingular) - { - m_isProjectionLeftHanded = std::nullopt; - m_invProjectionMatrix = std::nullopt; - } - else - { - m_isProjectionLeftHanded = det < 0.0; - m_invProjectionMatrix = hlsl::inverse(m_projectionMatrix); - } - } + void setProjectionMatrix(const projection_matrix_t& matrix); private: projection_matrix_t m_projectionMatrix; @@ -103,81 +84,43 @@ class ILinearProjection : virtual public core::IReferenceCounted virtual const CProjection& getLinearProjection(uint32_t index) const = 0; /// @brief Replace the camera referenced by this projection wrapper. - inline bool setCamera(core::smart_refctd_ptr&& camera) - { - if (camera) - { - m_camera = camera; - return true; - } - - return false; - } + bool setCamera(core::smart_refctd_ptr&& camera); /// @brief Return the camera referenced by this projection wrapper. - inline ICamera* getCamera() - { - return m_camera.get(); - } + ICamera* getCamera(); /// @brief Compute the model-view matrix. /// /// @param model World TRS matrix. /// @return The model-view matrix. - inline concatenated_matrix_t getMV(const model_matrix_t& model) const - { - const auto& v = m_camera->getGimbal().getViewMatrix(); - return hlsl::mul(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(v), hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(model)); - } + concatenated_matrix_t getMV(const model_matrix_t& model) const; /// @brief Compute the model-view-projection matrix from a model matrix. /// /// @param projection Linear projection. /// @param model World TRS matrix. /// @return The model-view-projection matrix. - inline concatenated_matrix_t getMVP(const CProjection& projection, const model_matrix_t& model) const - { - const auto& v = m_camera->getGimbal().getViewMatrix(); - const auto& p = projection.getProjectionMatrix(); - auto mv = hlsl::mul(hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(v), hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(model)); - return hlsl::mul(p, mv); - } + concatenated_matrix_t getMVP(const CProjection& projection, const model_matrix_t& model) const; /// @brief Compute the model-view-projection matrix from a model-view matrix. /// /// @param projection Linear projection. /// @param mv Model-view matrix. /// @return The model-view-projection matrix. - inline concatenated_matrix_t getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const - { - const auto& p = projection.getProjectionMatrix(); - return hlsl::mul(p, mv); - } + concatenated_matrix_t getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const; /// @brief Compute the inverse model-view matrix. /// /// @param model World TRS matrix. /// @return The inverse model-view matrix when it exists, otherwise `std::nullopt`. - inline inv_concatenated_matrix_t getMVInverse(const model_matrix_t& model) const - { - const auto mv = getMV(model); - if (auto det = hlsl::determinant(mv); det) - return hlsl::inverse(mv); - return std::nullopt; - } + inv_concatenated_matrix_t getMVInverse(const model_matrix_t& model) const; /// @brief Compute the inverse model-view-projection matrix. /// /// @param projection Linear projection. /// @param model World TRS matrix. /// @return The inverse model-view-projection matrix when it exists, otherwise `std::nullopt`. - inline inv_concatenated_matrix_t getMVPInverse(const CProjection& projection, const model_matrix_t& model) const - { - const auto mvp = getMVP(projection, model); - if (auto det = hlsl::determinant(mvp); det) - return hlsl::inverse(mvp); - return std::nullopt; - } + inv_concatenated_matrix_t getMVPInverse(const CProjection& projection, const model_matrix_t& model) const; }; } // namespace nbl::core diff --git a/include/nbl/ext/Cameras/IPlanarProjection.hpp b/include/nbl/ext/Cameras/IPlanarProjection.hpp index 41a7570ceb..459a91ad2e 100644 --- a/include/nbl/ext/Cameras/IPlanarProjection.hpp +++ b/include/nbl/ext/Cameras/IPlanarProjection.hpp @@ -72,50 +72,13 @@ class IPlanarProjection : public ILinearProjection }; /// @brief Rebuild the concrete projection matrix from the stored parameters. - inline void update(bool leftHanded, float aspectRatio) - { - switch (m_parameters.m_type) - { - case Perspective: - { - const auto& fov = m_parameters.m_planar.perspective.fov; - - if (leftHanded) - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); - else - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); - } break; - - case Orthographic: - { - const auto& orthoW = m_parameters.m_planar.orthographic.orthoWidth; - const auto viewHeight = orthoW / aspectRatio; - - if (leftHanded) - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); - else - base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); - } break; - } - } + void update(bool leftHanded, float aspectRatio); /// @brief Switch the entry to perspective mode and store its authored parameters. - inline void setPerspective(float zNear = 0.1f, float zFar = 100.f, float fov = 60.f) - { - m_parameters.m_type = Perspective; - m_parameters.m_planar.perspective.fov = fov; - m_parameters.m_zNear = zNear; - m_parameters.m_zFar = zFar; - } + void setPerspective(float zNear = 0.1f, float zFar = 100.f, float fov = 60.f); /// @brief Switch the entry to orthographic mode and store its authored parameters. - inline void setOrthographic(float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f) - { - m_parameters.m_type = Orthographic; - m_parameters.m_planar.orthographic.orthoWidth = orthoWidth; - m_parameters.m_zNear = zNear; - m_parameters.m_zFar = zFar; - } + void setOrthographic(float zNear = 0.1f, float zFar = 100.f, float orthoWidth = 10.f); /// @brief Return the authored planar projection parameters. inline const ProjectionParameters& getParameters() const { return m_parameters; } diff --git a/src/nbl/ext/Cameras/CCameraFollowRegressionUtilities.cpp b/src/nbl/ext/Cameras/CCameraFollowRegressionUtilities.cpp new file mode 100644 index 0000000000..ce7491aed4 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraFollowRegressionUtilities.cpp @@ -0,0 +1,278 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraFollowRegressionUtilities.hpp" + +namespace nbl::system +{ + +SCameraFollowRegressionThresholds CCameraFollowRegressionUtilities::makeFollowRegressionThresholds( + const float projectedNdcTolerance, + const float lockAngleToleranceDeg) +{ + auto thresholds = SCameraFollowRegressionThresholds{}; + thresholds.projectedNdcTolerance = projectedNdcTolerance; + thresholds.lockAngleToleranceDeg = lockAngleToleranceDeg; + return thresholds; +} + +bool CCameraFollowRegressionUtilities::tryComputeProjectedFollowTargetMetrics( + const SCameraProjectionContext& projectionContext, + const core::CTrackedTarget& trackedTarget, + SCameraProjectedTargetMetrics& outMetrics, + const float clipWEpsilon) +{ + outMetrics = {}; + const hlsl::float32_t3 target = hlsl::CCameraMathUtilities::castVector(trackedTarget.getGimbal().getPosition()); + const auto viewSpace = hlsl::mul(projectionContext.viewMatrix, hlsl::float32_t4(target.x, target.y, target.z, 1.0f)); + const auto clipProjection = hlsl::transpose(projectionContext.projectionMatrix); + const auto clip = hlsl::mul(clipProjection, viewSpace); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(clip.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.y) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.z) || !hlsl::CCameraMathUtilities::isFiniteScalar(clip.w)) + return false; + + const auto absW = hlsl::abs(clip.w); + if (absW < clipWEpsilon) + return false; + + const float invW = 1.0f / clip.w; + outMetrics.ndc = hlsl::float32_t2(clip.x, clip.y) * invW; + if (!hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.x) || !hlsl::CCameraMathUtilities::isFiniteScalar(outMetrics.ndc.y)) + return false; + + outMetrics.radius = hlsl::length(outMetrics.ndc); + return true; +} + +bool CCameraFollowRegressionUtilities::validateProjectedFollowTargetContract( + const SCameraProjectionContext& projectionContext, + const core::CTrackedTarget& trackedTarget, + SCameraProjectedTargetMetrics& outMetrics, + std::string* error, + const SCameraFollowRegressionThresholds& thresholds) +{ + if (!tryComputeProjectedFollowTargetMetrics(projectionContext, trackedTarget, outMetrics, thresholds.clipWEpsilon)) + { + if (error) + *error = "failed to project follow target"; + return false; + } + + if (outMetrics.radius > thresholds.projectedNdcTolerance) + { + if (error) + { + *error = "projected target mismatch ndc=(" + std::to_string(outMetrics.ndc.x) + + "," + std::to_string(outMetrics.ndc.y) + ") radius=" + std::to_string(outMetrics.radius); + } + return false; + } + + return true; +} + +SCameraFollowVisualMetrics CCameraFollowRegressionUtilities::buildFollowVisualMetrics( + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig* followConfig, + const SCameraProjectionContext* projectionContext) +{ + SCameraFollowVisualMetrics out = {}; + if (!camera || !followConfig || !followConfig->enabled || followConfig->mode == core::ECameraFollowMode::Disabled) + return out; + + out.active = true; + out.mode = followConfig->mode; + + double targetDistance = 0.0; + out.lockValid = core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig->mode) && + core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &targetDistance); + if (out.lockValid) + out.targetDistance = static_cast(targetDistance); + + if (out.lockValid && projectionContext) + out.projectedValid = tryComputeProjectedFollowTargetMetrics(*projectionContext, trackedTarget, out.projectedTarget); + + return out; +} + +bool CCameraFollowRegressionUtilities::validateFollowTargetContract( + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, + const core::CCameraGoal& followGoal, + SCameraFollowRegressionResult& out, + std::string* error, + const SCameraProjectionContext* projectionContext, + const SCameraFollowRegressionThresholds& thresholds) +{ + out = {}; + if (!camera) + { + if (error) + *error = "missing camera"; + return false; + } + + if (core::CCameraFollowUtilities::cameraFollowModeLocksViewToTarget(followConfig.mode)) + { + out.hasLockMetrics = core::CCameraFollowUtilities::tryComputeFollowTargetLockMetrics(camera->getGimbal(), trackedTarget, out.lockAngleDeg, &out.targetDistance); + if (!out.hasLockMetrics) + { + if (error) + *error = "failed to compute follow lock metrics"; + return false; + } + + const auto& trackedTargetGimbal = trackedTarget.getGimbal(); + const auto& cameraGimbal = camera->getGimbal(); + const hlsl::float64_t3 trackedTargetPosition = trackedTargetGimbal.getPosition(); + const hlsl::float64_t3 cameraPosition = cameraGimbal.getPosition(); + const double expectedTargetDistance = hlsl::length(trackedTargetPosition - cameraPosition); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(expectedTargetDistance) || hlsl::abs(expectedTargetDistance - out.targetDistance) > thresholds.distanceTolerance) + { + if (error) + { + *error = "target distance mismatch actual=" + std::to_string(out.targetDistance) + + " expected=" + std::to_string(expectedTargetDistance); + } + return false; + } + + if (out.lockAngleDeg > thresholds.lockAngleToleranceDeg) + { + if (error) + *error = "lock angle mismatch angle_deg=" + std::to_string(out.lockAngleDeg); + return false; + } + + if (projectionContext) + { + out.hasProjectedMetrics = tryComputeProjectedFollowTargetMetrics( + *projectionContext, + trackedTarget, + out.projectedTarget, + thresholds.clipWEpsilon); + if (!out.hasProjectedMetrics) + { + if (error) + *error = "failed to compute projected follow target metrics"; + return false; + } + + if (out.projectedTarget.radius > thresholds.projectedNdcTolerance) + { + if (error) + { + *error = "projected target mismatch ndc=(" + std::to_string(out.projectedTarget.ndc.x) + + "," + std::to_string(out.projectedTarget.ndc.y) + ") radius=" + std::to_string(out.projectedTarget.radius); + } + return false; + } + } + } + + if (camera->supportsGoalState(core::ICamera::GoalStateSphericalTarget)) + { + core::ICamera::SphericalTargetState state; + if (!camera->tryGetSphericalTargetState(state)) + { + if (error) + *error = "missing spherical target state"; + return false; + } + + out.hasSphericalState = true; + out.sphericalTarget = state.target; + out.sphericalDistance = state.distance; + + const auto& trackedTargetGimbal = trackedTarget.getGimbal(); + const auto& cameraGimbal = camera->getGimbal(); + const hlsl::float64_t3 trackedTargetPosition = trackedTargetGimbal.getPosition(); + const hlsl::float64_t3 targetDelta = state.target - trackedTargetPosition; + const double targetDeltaLen = hlsl::length(targetDelta); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDeltaLen) || targetDeltaLen > thresholds.targetTolerance) + { + if (error) + *error = "spherical target writeback mismatch"; + return false; + } + + const double actualDistance = hlsl::length(cameraGimbal.getPosition() - trackedTargetPosition); + const auto expectedDistance = followGoal.hasOrbitState ? static_cast(followGoal.orbitDistance) : + (followGoal.hasDistance ? static_cast(followGoal.distance) : actualDistance); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(actualDistance) || !hlsl::CCameraMathUtilities::isFiniteScalar(expectedDistance) || + hlsl::abs(actualDistance - expectedDistance) > thresholds.distanceTolerance || + hlsl::abs(static_cast(state.distance) - expectedDistance) > thresholds.distanceTolerance) + { + if (error) + { + *error = "spherical distance mismatch actual=" + std::to_string(actualDistance) + + " state=" + std::to_string(state.distance) + + " expected=" + std::to_string(expectedDistance); + } + return false; + } + } + + out.passed = true; + return true; +} + +bool CCameraFollowRegressionUtilities::buildApplyAndValidateFollowTargetContract( + const core::CCameraGoalSolver& solver, + core::ICamera* camera, + const core::CTrackedTarget& trackedTarget, + const core::SCameraFollowConfig& followConfig, + SCameraFollowApplyValidationResult& out, + std::string* error, + const SCameraProjectionContext* projectionContext, + const SCameraFollowRegressionThresholds& thresholds) +{ + out = {}; + + if (!core::CCameraFollowUtilities::tryBuildFollowGoal(solver, camera, trackedTarget, followConfig, out.goal)) + { + if (error) + *error = "failed to build follow goal"; + return false; + } + out.hasGoal = true; + + out.applyResult = core::CCameraFollowUtilities::applyFollowToCamera(solver, camera, trackedTarget, followConfig); + if (!out.applyResult.succeeded()) + { + if (error) + *error = "failed to apply follow goal"; + return false; + } + + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + { + if (error) + *error = "failed to capture camera state after follow apply"; + return false; + } + + out.hasCapturedGoal = true; + out.capturedGoal = capture.goal; + if (!core::CCameraGoalUtilities::compareGoals(out.capturedGoal, out.goal, thresholds.positionTolerance, thresholds.rotationToleranceDeg, thresholds.scalarTolerance)) + { + if (error) + *error = std::string("follow goal mismatch. ") + core::CCameraGoalUtilities::describeGoalMismatch(out.capturedGoal, out.goal); + return false; + } + + return validateFollowTargetContract( + camera, + trackedTarget, + followConfig, + out.goal, + out.regression, + error, + projectionContext, + thresholds); +} + +} // namespace nbl::system diff --git a/src/nbl/ext/Cameras/CCameraFollowUtilities.cpp b/src/nbl/ext/Cameras/CCameraFollowUtilities.cpp new file mode 100644 index 0000000000..354c87cf78 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraFollowUtilities.cpp @@ -0,0 +1,211 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraFollowUtilities.hpp" + +namespace nbl::core +{ + +CTrackedTarget::CTrackedTarget( + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation, + std::string identifier) + : m_identifier(std::move(identifier)), + m_gimbal(gimbal_t::base_t::SCreationParameters{ .position = position, .orientation = orientation }) +{ + m_gimbal.updateView(); +} + +void CTrackedTarget::setPose(const hlsl::float64_t3& position, const hlsl::camera_quaternion_t& orientation) +{ + m_gimbal.begin(); + m_gimbal.setPosition(position); + m_gimbal.setOrientation(orientation); + m_gimbal.end(); + m_gimbal.updateView(); +} + +void CTrackedTarget::setPosition(const hlsl::float64_t3& position) +{ + setPose(position, m_gimbal.getOrientation()); +} + +void CTrackedTarget::setOrientation(const hlsl::camera_quaternion_t& orientation) +{ + setPose(m_gimbal.getPosition(), orientation); +} + +bool CTrackedTarget::trySetFromTransform(const hlsl::float64_t4x4& transform) +{ + hlsl::float64_t3 position = hlsl::float64_t3(0.0); + hlsl::camera_quaternion_t orientation = hlsl::CCameraMathUtilities::makeIdentityQuaternion(); + if (!hlsl::CCameraMathUtilities::tryExtractRigidPoseFromTransform(transform, position, orientation)) + return false; + + setPose(position, orientation); + return true; +} + +hlsl::float64_t3 CCameraFollowUtilities::transformFollowLocalOffset(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& localOffset) +{ + return hlsl::CCameraMathUtilities::rotateVectorByQuaternion(gimbal.getOrientation(), localOffset); +} + +hlsl::float64_t3 CCameraFollowUtilities::projectFollowWorldOffsetToLocal(const ICamera::CGimbal& gimbal, const hlsl::float64_t3& worldOffset) +{ + return hlsl::CCameraMathUtilities::projectWorldVectorToLocalQuaternionFrame(gimbal.getOrientation(), worldOffset); +} + +bool CCameraFollowUtilities::buildFollowLookAtOrientation( + const hlsl::float64_t3& position, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& preferredUp, + hlsl::camera_quaternion_t& outOrientation) +{ + return hlsl::CCameraMathUtilities::tryBuildLookAtOrientation(position, targetPosition, preferredUp, outOrientation); +} + +bool CCameraFollowUtilities::captureFollowOffsetsFromCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + SCameraFollowConfig& ioConfig) +{ + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + const auto& targetGimbal = trackedTarget.getGimbal(); + ioConfig.worldOffset = capture.goal.position - targetGimbal.getPosition(); + ioConfig.localOffset = projectFollowWorldOffsetToLocal(targetGimbal, ioConfig.worldOffset); + return true; +} + +bool CCameraFollowUtilities::tryComputeFollowTargetLockMetrics( + const ICamera::CGimbal& cameraGimbal, + const CTrackedTarget& trackedTarget, + float& outAngleDeg, + double* outDistance) +{ + const auto toTarget = trackedTarget.getGimbal().getPosition() - cameraGimbal.getPosition(); + const auto targetDistance = hlsl::length(toTarget); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(targetDistance) || targetDistance <= SCameraToolingThresholds::TinyScalarEpsilon) + return false; + + const auto forward = cameraGimbal.getZAxis(); + const auto forwardLength = hlsl::length(forward); + if (!hlsl::CCameraMathUtilities::isFiniteVec3(forward) || !hlsl::CCameraMathUtilities::isFiniteScalar(forwardLength) || forwardLength <= SCameraToolingThresholds::TinyScalarEpsilon) + return false; + + const auto forwardDirection = forward / forwardLength; + const auto targetDir = toTarget / targetDistance; + const auto dotForward = std::clamp(hlsl::dot(forwardDirection, targetDir), -1.0, 1.0); + outAngleDeg = static_cast(hlsl::degrees(hlsl::acos(dotForward))); + if (!hlsl::CCameraMathUtilities::isFiniteScalar(outAngleDeg)) + return false; + + if (outDistance) + *outDistance = targetDistance; + return true; +} + +bool CCameraFollowUtilities::tryBuildFollowPositionGoal( + ICamera* camera, + CCameraGoal& outGoal, + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const hlsl::float64_t3& preferredUp) +{ + if (camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return CCameraGoalUtilities::buildCanonicalTargetRelativeGoalFromPosition(outGoal, targetPosition, position); + + outGoal.position = position; + return buildFollowLookAtOrientation(outGoal.position, targetPosition, preferredUp, outGoal.orientation) && CCameraGoalUtilities::isGoalFinite(outGoal); +} + +bool CCameraFollowUtilities::tryBuildFollowGoal( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal& outGoal) +{ + if (!camera || !config.enabled || config.mode == ECameraFollowMode::Disabled) + return false; + + const auto capture = solver.captureDetailed(camera); + if (!capture.canUseGoal()) + return false; + + outGoal = capture.goal; + + const auto& targetGimbal = trackedTarget.getGimbal(); + const auto targetPosition = targetGimbal.getPosition(); + + switch (config.mode) + { + case ECameraFollowMode::OrbitTarget: + { + if (!camera->supportsGoalState(ICamera::GoalStateSphericalTarget)) + return false; + + if (outGoal.hasPathState) + { + return CCameraGoalUtilities::applyCanonicalPathGoalFields(outGoal, targetPosition, outGoal.pathState) && CCameraGoalUtilities::isGoalFinite(outGoal); + } + + const bool hasSphericalState = outGoal.hasOrbitState || outGoal.hasDistance; + if (!hasSphericalState) + return false; + + const auto orbitDistance = outGoal.hasOrbitState ? outGoal.orbitDistance : outGoal.distance; + return CCameraGoalUtilities::applyCanonicalTargetRelativeGoal( + outGoal, + { + .target = targetPosition, + .orbitUv = outGoal.orbitUv, + .distance = orbitDistance + }); + } + + case ECameraFollowMode::LookAtTarget: + { + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, capture.goal.position, targetGimbal.getYAxis()); + } + + case ECameraFollowMode::KeepWorldOffset: + { + const auto position = targetPosition + config.worldOffset; + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); + } + + case ECameraFollowMode::KeepLocalOffset: + { + const auto position = targetPosition + transformFollowLocalOffset(targetGimbal, config.localOffset); + return tryBuildFollowPositionGoal(camera, outGoal, targetPosition, position, targetGimbal.getYAxis()); + } + + default: + return false; + } +} + +CCameraGoalSolver::SApplyResult CCameraFollowUtilities::applyFollowToCamera( + const CCameraGoalSolver& solver, + ICamera* camera, + const CTrackedTarget& trackedTarget, + const SCameraFollowConfig& config, + CCameraGoal* outGoal) +{ + CCameraGoal goal = {}; + if (!tryBuildFollowGoal(solver, camera, trackedTarget, config, goal)) + return {}; + + if (outGoal) + *outGoal = goal; + + return solver.applyDetailed(camera, goal); +} + +} // namespace nbl::core diff --git a/src/nbl/ext/Cameras/CCameraGoalSolver.cpp b/src/nbl/ext/Cameras/CCameraGoalSolver.cpp new file mode 100644 index 0000000000..8b358377f5 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraGoalSolver.cpp @@ -0,0 +1,548 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraGoalSolver.hpp" + +#include + +namespace nbl::core +{ + +bool CCameraGoalSolver::buildEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const +{ + out.clear(); + if (!camera) + return false; + + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); + + if (camera->hasCapability(ICamera::SphericalTarget)) + return buildSphericalEvents(camera, canonicalTarget, out); + + return buildFreeEvents(camera, canonicalTarget, out); +} + +bool CCameraGoalSolver::capture(ICamera* camera, CCameraGoal& out) const +{ + out = {}; + if (!camera) + return false; + + const ICamera::CGimbal& gimbal = camera->getGimbal(); + out.position = hlsl::float64_t3(gimbal.getPosition()); + out.orientation = gimbal.getOrientation(); + out.sourceKind = camera->getKind(); + out.sourceCapabilities = ICamera::capability_flags_t(camera->getCapabilities()); + out.sourceGoalStateMask = ICamera::goal_state_flags_t(camera->getGoalStateMask()); + + ICamera::SphericalTargetState sphericalState; + if (camera->tryGetSphericalTargetState(sphericalState)) + { + out.targetPosition = sphericalState.target; + out.hasTargetPosition = true; + out.distance = sphericalState.distance; + out.hasDistance = true; + out.orbitDistance = sphericalState.distance; + out.orbitUv = sphericalState.orbitUv; + out.hasOrbitState = true; + } + + ICamera::DynamicPerspectiveState dynamicState; + if (camera->tryGetDynamicPerspectiveState(dynamicState)) + { + out.hasDynamicPerspectiveState = true; + out.dynamicPerspectiveState = dynamicState; + } + + ICamera::PathState pathState; + if (camera->tryGetPathState(pathState)) + { + out.hasPathState = true; + out.pathState = pathState; + } + + out = CCameraGoalUtilities::canonicalizeGoal(out); + return true; +} + +CCameraGoalSolver::SCaptureResult CCameraGoalSolver::captureDetailed(ICamera* camera) const +{ + SCaptureResult result; + result.hasCamera = camera != nullptr; + if (!result.hasCamera) + return result; + + result.captured = capture(camera, result.goal); + result.finiteGoal = result.captured && CCameraGoalUtilities::isGoalFinite(result.goal); + return result; +} + +CCameraGoalSolver::SCompatibilityResult CCameraGoalSolver::analyzeCompatibility(const ICamera* camera, const CCameraGoal& target) const +{ + SCompatibilityResult result; + if (!camera) + return result; + + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); + result.sameKind = canonicalTarget.sourceKind == ICamera::CameraKind::Unknown || canonicalTarget.sourceKind == camera->getKind(); + result.supportedGoalStateMask = ICamera::goal_state_flags_t(camera->getGoalStateMask()); + result.requiredGoalStateMask = CCameraGoalUtilities::getRequiredGoalStateMask(canonicalTarget); + result.missingGoalStateMask = result.requiredGoalStateMask & ~result.supportedGoalStateMask; + result.exact = result.missingGoalStateMask == ICamera::GoalStateNone; + return result; +} + +CCameraGoalSolver::SApplyResult CCameraGoalSolver::applyDetailed(ICamera* camera, const CCameraGoal& target) const +{ + SApplyResult result; + if (!camera) + return result; + + const auto canonicalTarget = CCameraGoalUtilities::canonicalizeGoal(target); + + bool exact = true; + bool absoluteChanged = false; + + if (!camera->hasCapability(ICamera::SphericalTarget)) + { + bool poseChanged = false; + bool poseExact = false; + if (tryApplyAbsoluteReferencePose(camera, canonicalTarget, poseChanged, poseExact)) + { + result.issues |= SApplyResult::EIssue::UsedAbsolutePoseFallback; + absoluteChanged = absoluteChanged || poseChanged; + if (poseExact && !canonicalTarget.hasDynamicPerspectiveState) + { + result.status = poseChanged ? + SApplyResult::EStatus::AppliedAbsoluteOnly : + SApplyResult::EStatus::AlreadySatisfied; + result.exact = true; + return result; + } + } + } + + if (canonicalTarget.hasTargetPosition) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; + exact = false; + } + else + { + const auto beforeTarget = beforeState.target; + if (!camera->trySetSphericalTarget(canonicalTarget.targetPosition)) + { + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; + exact = false; + } + else + { + absoluteChanged = afterState.target != beforeTarget; + exact = exact && afterState.target == canonicalTarget.targetPosition; + } + } + } + } + + if (canonicalTarget.hasDistance || canonicalTarget.hasOrbitState) + { + ICamera::SphericalTargetState beforeState; + if (!camera->tryGetSphericalTargetState(beforeState)) + { + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; + exact = false; + } + else + { + const float desiredDistance = canonicalTarget.hasOrbitState ? canonicalTarget.orbitDistance : canonicalTarget.distance; + const float beforeDistance = beforeState.distance; + if (!camera->trySetSphericalDistance(desiredDistance)) + { + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; + exact = false; + } + else + { + ICamera::SphericalTargetState afterState; + if (!camera->tryGetSphericalTargetState(afterState)) + { + result.issues |= SApplyResult::EIssue::MissingSphericalTargetState; + exact = false; + } + else + { + absoluteChanged = absoluteChanged || afterState.distance != beforeDistance; + exact = exact && hlsl::abs(static_cast(afterState.distance - desiredDistance)) <= SCameraToolingThresholds::ScalarTolerance; + } + } + } + } + + if (canonicalTarget.hasPathState) + { + ICamera::PathState beforeState; + if (!camera->tryGetPathState(beforeState)) + { + result.issues |= SApplyResult::EIssue::MissingPathState; + exact = false; + } + else if (!camera->trySetPathState(canonicalTarget.pathState)) + { + result.issues |= SApplyResult::EIssue::MissingPathState; + exact = false; + } + else + { + ICamera::PathState afterState; + if (!camera->tryGetPathState(afterState)) + { + result.issues |= SApplyResult::EIssue::MissingPathState; + exact = false; + } + else + { + const auto thresholds = SCameraPathDefaults::ComparisonThresholds; + const bool pathChanged = CCameraPathUtilities::pathStatesChanged(beforeState, afterState, thresholds); + const bool pathExact = CCameraPathUtilities::pathStatesNearlyEqual(afterState, canonicalTarget.pathState, thresholds); + + absoluteChanged = absoluteChanged || pathChanged; + exact = exact && pathExact; + } + } + } + + if (canonicalTarget.hasDynamicPerspectiveState) + { + ICamera::DynamicPerspectiveState beforeState; + if (!camera->tryGetDynamicPerspectiveState(beforeState)) + { + result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; + exact = false; + } + else if (!camera->trySetDynamicPerspectiveState(canonicalTarget.dynamicPerspectiveState)) + { + result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; + exact = false; + } + else + { + ICamera::DynamicPerspectiveState afterState; + if (!camera->tryGetDynamicPerspectiveState(afterState)) + { + result.issues |= SApplyResult::EIssue::MissingDynamicPerspectiveState; + exact = false; + } + else + { + const bool dynamicChanged = !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.baseFov, afterState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) || + !hlsl::CCameraMathUtilities::nearlyEqualScalar(beforeState.referenceDistance, afterState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); + const bool dynamicExact = hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.baseFov, canonicalTarget.dynamicPerspectiveState.baseFov, static_cast(SCameraToolingThresholds::ScalarTolerance)) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(afterState.referenceDistance, canonicalTarget.dynamicPerspectiveState.referenceDistance, static_cast(SCameraToolingThresholds::ScalarTolerance)); + + absoluteChanged = absoluteChanged || dynamicChanged; + exact = exact && dynamicExact; + } + } + } + + std::vector events; + buildEvents(camera, canonicalTarget, events); + result.eventCount = static_cast(events.size()); + result.exact = exact; + + if (events.empty()) + { + if (absoluteChanged) + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + else if (exact) + result.status = SApplyResult::EStatus::AlreadySatisfied; + return result; + } + + if (camera->manipulate({ events.data(), events.size() })) + { + result.status = absoluteChanged ? + SApplyResult::EStatus::AppliedAbsoluteAndVirtualEvents : + SApplyResult::EStatus::AppliedVirtualEvents; + return result; + } + + if (absoluteChanged) + { + result.status = SApplyResult::EStatus::AppliedAbsoluteOnly; + result.exact = false; + return result; + } + + result.issues |= SApplyResult::EIssue::VirtualEventReplayFailed; + result.status = SApplyResult::EStatus::Failed; + result.exact = false; + return result; +} + +bool CCameraGoalSolver::apply(ICamera* camera, const CCameraGoal& target) const +{ + return applyDetailed(camera, target).succeeded(); +} + +void CCameraGoalSolver::appendYawPitchRollEvents( + std::vector& events, + const hlsl::float64_t3& eulerRadians, + double denominator, + bool includeRoll) const +{ + static constexpr std::array RotationBindings = {{ + { CVirtualGimbalEvent::TiltUp, CVirtualGimbalEvent::TiltDown }, + { CVirtualGimbalEvent::PanRight, CVirtualGimbalEvent::PanLeft }, + { CVirtualGimbalEvent::RollRight, CVirtualGimbalEvent::RollLeft } + }}; + + auto tolerances = SGoalSolverDefaults::AngularToleranceDegVec; + if (!includeRoll) + tolerances.z = std::numeric_limits::infinity(); + + CCameraVirtualEventUtilities::appendAngularAxisEvents( + events, + eulerRadians, + hlsl::float64_t3(denominator), + tolerances, + RotationBindings); +} + +void CCameraGoalSolver::appendPathDeltaEvents( + std::vector& events, + const SCameraPathDelta& delta, + double moveDenominator, + double rotationDenominator) const +{ + CCameraPathUtilities::appendPathDeltaEvents( + events, + delta, + moveDenominator, + rotationDenominator, + SCameraPathDefaults::ExactComparisonThresholds); +} + +double CCameraGoalSolver::getMoveMagnitudeDenominator(const ICamera* camera) const +{ + const double moveScale = camera->getMoveSpeedScale(); + return camera->getUnscaledVirtualTranslationMagnitude() * (moveScale == 0.0 ? SGoalSolverDefaults::UnitScale : moveScale); +} + +double CCameraGoalSolver::getRotationMagnitudeDenominator(const ICamera* camera) const +{ + const double rotationScale = camera->getRotationSpeedScale(); + return rotationScale == 0.0 ? SGoalSolverDefaults::UnitScale : rotationScale; +} + +bool CCameraGoalSolver::computePoseMismatch(ICamera* camera, const CCameraGoal& target, double& outPositionDelta, double& outRotationDeltaDeg) const +{ + outPositionDelta = 0.0; + outRotationDeltaDeg = 0.0; + if (!camera) + return false; + + const ICamera::CGimbal& gimbal = camera->getGimbal(); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!hlsl::CCameraMathUtilities::tryComputePoseDelta(gimbal.getPosition(), gimbal.getOrientation(), target.position, target.orientation, poseDelta)) + return false; + + outPositionDelta = poseDelta.position; + outRotationDeltaDeg = poseDelta.rotationDeg; + return true; +} + +bool CCameraGoalSolver::tryApplyAbsoluteReferencePose(ICamera* camera, const CCameraGoal& target, bool& outChanged, bool& outExact) const +{ + outChanged = false; + outExact = false; + if (!camera) + return false; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Free: + case ICamera::CameraKind::FPS: + break; + default: + return false; + } + + double beforePosDelta = 0.0; + double beforeRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, beforePosDelta, beforeRotDeltaDeg)) + return false; + + if (beforePosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && beforeRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg) + { + outExact = true; + return true; + } + + const auto targetFrame = hlsl::CCameraMathUtilities::composeTransformMatrix(target.position, target.orientation); + + camera->manipulate({}, &targetFrame); + + double afterPosDelta = 0.0; + double afterRotDeltaDeg = 0.0; + if (!computePoseMismatch(camera, target, afterPosDelta, afterRotDeltaDeg)) + return false; + + outChanged = !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterPosDelta - beforePosDelta, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)) || + !hlsl::CCameraMathUtilities::isNearlyZeroScalar(afterRotDeltaDeg - beforeRotDeltaDeg, static_cast(SCameraToolingThresholds::TinyScalarEpsilon)); + outExact = afterPosDelta <= SCameraToolingThresholds::DefaultPositionTolerance && afterRotDeltaDeg <= SCameraToolingThresholds::DefaultAngularToleranceDeg; + return true; +} + +bool CCameraGoalSolver::buildTargetRelativeEvents( + ICamera* camera, + const ICamera::SphericalTargetState& sphericalState, + const SCameraTargetRelativeState& goal, + std::vector& out, + const SCameraTargetRelativeEventPolicy& policy) const +{ + const auto delta = CCameraTargetRelativeUtilities::buildTargetRelativeDelta(sphericalState, goal); + CCameraTargetRelativeUtilities::appendTargetRelativeDeltaEvents( + out, + delta, + policy.translateOrbit ? getMoveMagnitudeDenominator(camera) : getRotationMagnitudeDenominator(camera), + SCameraToolingThresholds::DefaultAngularToleranceDeg, + camera->getUnscaledVirtualTranslationMagnitude(), + SCameraToolingThresholds::ScalarTolerance, + policy); + return !out.empty(); +} + +bool CCameraGoalSolver::buildPathEvents( + ICamera* camera, + const CCameraGoal& target, + const ICamera::SphericalTargetState& sphericalState, + std::vector& out) const +{ + if (!camera) + return false; + + const auto effectiveTarget = target.hasTargetPosition ? target.targetPosition : sphericalState.target; + ICamera::PathState currentState = {}; + const ICamera::PathState* currentStateOverride = camera->tryGetPathState(currentState) ? ¤tState : nullptr; + ICamera::PathStateLimits pathLimits = CCameraPathUtilities::makeDefaultPathLimits(); + camera->tryGetPathStateLimits(pathLimits); + SCameraPathStateTransition transition = {}; + if (!CCameraPathUtilities::tryBuildPathStateTransition( + effectiveTarget, + camera->getGimbal().getPosition(), + target.position, + pathLimits, + currentStateOverride, + target.hasPathState ? &target.pathState : nullptr, + transition)) + { + return false; + } + + const auto moveDenom = getMoveMagnitudeDenominator(camera); + const auto rotationDenom = getRotationMagnitudeDenominator(camera); + appendPathDeltaEvents(out, transition.delta, moveDenom, rotationDenom); + return !out.empty(); +} + +bool CCameraGoalSolver::buildSphericalEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const +{ + ICamera::SphericalTargetState sphericalState; + if (!camera || !camera->tryGetSphericalTargetState(sphericalState)) + return false; + + if (camera->getKind() == ICamera::CameraKind::Path) + return buildPathEvents(camera, target, sphericalState, out); + + SCameraTargetRelativeState goal; + if (!CCameraGoalUtilities::tryResolveCanonicalTargetRelativeState(target, sphericalState, goal)) + return false; + + switch (camera->getKind()) + { + case ICamera::CameraKind::Orbit: + case ICamera::CameraKind::DollyZoom: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); + + case ICamera::CameraKind::Turntable: + case ICamera::CameraKind::Arcball: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::RotateDistancePolicy); + + case ICamera::CameraKind::TopDown: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::TopDownPolicy); + + case ICamera::CameraKind::Isometric: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::IsometricPolicy); + + case ICamera::CameraKind::Dolly: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::DollyPolicy); + + case ICamera::CameraKind::Chase: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::ChasePolicy); + + default: + return buildTargetRelativeEvents(camera, sphericalState, goal, out, SCameraTargetRelativeRigDefaults::OrbitTranslatePolicy); + } +} + +bool CCameraGoalSolver::buildFreeEvents(ICamera* camera, const CCameraGoal& target, std::vector& out) const +{ + const ICamera::CGimbal& gimbal = camera->getGimbal(); + const hlsl::float64_t3 currentPos = gimbal.getPosition(); + const hlsl::float64_t3 deltaWorld = target.position - currentPos; + CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents( + out, + gimbal.getOrientation(), + deltaWorld, + SGoalSolverDefaults::UnitAxisDenominator, + SGoalSolverDefaults::ScalarToleranceVec); + + switch (camera->getKind()) + { + case ICamera::CameraKind::FPS: + { + const hlsl::float64_t2 currentPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(gimbal.getOrientation()); + const hlsl::float64_t2 targetPitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromOrientation(target.orientation); + + const double rotScale = camera->getRotationSpeedScale(); + const double invScale = rotScale == 0.0 ? SGoalSolverDefaults::UnitScale : (SGoalSolverDefaults::UnitScale / rotScale); + + appendYawPitchRollEvents( + out, + hlsl::float64_t3( + hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.x - currentPitchYaw.x) * invScale, + hlsl::CCameraMathUtilities::wrapAngleRad(targetPitchYaw.y - currentPitchYaw.y) * invScale, + 0.0), + SGoalSolverDefaults::UnitScale, + false); + } break; + + case ICamera::CameraKind::Free: + { + appendYawPitchRollEvents( + out, + hlsl::CCameraMathUtilities::getOrientationDeltaEulerRadiansYXZ(gimbal.getOrientation(), target.orientation), + SGoalSolverDefaults::UnitScale); + } break; + + default: + break; + } + + return !out.empty(); +} + +} // namespace nbl::core diff --git a/src/nbl/ext/Cameras/CCameraInputBindingUtilities.cpp b/src/nbl/ext/Cameras/CCameraInputBindingUtilities.cpp new file mode 100644 index 0000000000..14c6f3cbab --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraInputBindingUtilities.cpp @@ -0,0 +1,479 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraInputBindingUtilities.hpp" + +#include + +namespace nbl::ui +{ + +namespace +{ + +using virtual_event_t = core::CVirtualGimbalEvent::VirtualEventType; +using keyboard_axis_group_t = std::array; +using mouse_axis_group_t = std::array; +using scalar_axis_pair_t = std::array; + +struct SKeyboardPresetSpec final +{ + keyboard_axis_group_t wasd = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double wasdScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + scalar_axis_pair_t qe = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double qeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + keyboard_axis_group_t ijkl = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double ijklScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; +}; + +struct SMousePresetSpec final +{ + mouse_axis_group_t relative = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double relativeScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; + scalar_axis_pair_t scroll = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None + }; + double scrollScale = IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; +}; + +struct SCameraInputBindingEventGroups final +{ + static inline constexpr std::array FpsMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitTranslate = { + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array OrbitZoom = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward + }; + static inline constexpr std::array VerticalMove = { + core::CVirtualGimbalEvent::MoveDown, + core::CVirtualGimbalEvent::MoveUp + }; + static inline constexpr std::array PathRigProgressAndU = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveRight + }; + static inline constexpr std::array PathRigV = VerticalMove; + static inline constexpr std::array TurntableMove = { + core::CVirtualGimbalEvent::MoveForward, + core::CVirtualGimbalEvent::MoveBackward, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array LookYawPitch = { + core::CVirtualGimbalEvent::TiltDown, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array Roll = { + core::CVirtualGimbalEvent::RollLeft, + core::CVirtualGimbalEvent::RollRight + }; + static inline constexpr std::array PanOnly = { + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::None, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::PanRight + }; + static inline constexpr std::array RelativeLook = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::TiltUp, + core::CVirtualGimbalEvent::TiltDown + }; + static inline constexpr std::array RelativeOrbitTranslate = { + core::CVirtualGimbalEvent::MoveRight, + core::CVirtualGimbalEvent::MoveLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; + static inline constexpr std::array RelativeTopDown = { + core::CVirtualGimbalEvent::PanRight, + core::CVirtualGimbalEvent::PanLeft, + core::CVirtualGimbalEvent::MoveUp, + core::CVirtualGimbalEvent::MoveDown + }; +}; + +struct SCameraInteractionBindingSpec +{ + SKeyboardPresetSpec keyboard = {}; + SMousePresetSpec mouse = {}; +}; + +struct SCameraMappedInteractionBindingSpec +{ + IGimbalBindingLayout::keyboard_to_virtual_events_t keyboard; + IGimbalBindingLayout::mouse_to_virtual_events_t mouse; +}; + +template +bool containsBindingForAnyCode(const Map& preset, const Codes& codes) +{ + for (const auto code : codes) + { + if (preset.find(code) != preset.end()) + return true; + } + return false; +} + +template +bool containsBindingForAnyCodeGroups(const Map& preset, const Codes&... codes) +{ + return (containsBindingForAnyCode(preset, codes) || ...); +} + +constexpr size_t interactionFamilyIndex(const core::ECameraInteractionFamily family) +{ + return static_cast(family); +} + +template +void appendBindingSpec(Map& preset, const Codes& codes, const Events& events, const double magnitudeScale) +{ + for (size_t i = 0u; i < codes.size() && i < events.size(); ++i) + { + const auto event = events[i]; + if (event == core::CVirtualGimbalEvent::None) + continue; + preset.emplace(codes[i], IGimbalBindingLayout::CHashInfo(event, magnitudeScale)); + } +} + +template +void appendMirroredBindingSpec(Map& preset, const Codes& codes, const virtual_event_t event, const double magnitudeScale) +{ + if (event == core::CVirtualGimbalEvent::None) + return; + + std::array> duplicatedEvents = {}; + duplicatedEvents.fill(event); + appendBindingSpec(preset, codes, duplicatedEvents, magnitudeScale); +} + +IGimbalBindingLayout::keyboard_to_virtual_events_t buildKeyboardPreset(const SKeyboardPresetSpec& spec) +{ + IGimbalBindingLayout::keyboard_to_virtual_events_t preset; + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardWasdCodes, spec.wasd, spec.wasdScale); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardQeCodes, spec.qe, spec.qeScale); + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::KeyboardIjklCodes, spec.ijkl, spec.ijklScale); + return preset; +} + +IGimbalBindingLayout::mouse_to_virtual_events_t buildMousePreset(const SMousePresetSpec& spec) +{ + IGimbalBindingLayout::mouse_to_virtual_events_t preset; + appendBindingSpec(preset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes, spec.relative, spec.relativeScale); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::PositiveScrollCodes, spec.scroll[0], spec.scrollScale); + appendMirroredBindingSpec(preset, SCameraInputBindingPhysicalGroups::NegativeScrollCodes, spec.scroll[1], spec.scrollScale); + return preset; +} + +double getDefaultImguizmoMagnitudeScale(const virtual_event_t event) +{ + if (core::CVirtualGimbalEvent::isTranslationEvent(event)) + return CCameraInputBindingUtilities::SInputMagnitudeDefaults::ImguizmoTranslationUnitsPerWorldUnit; + if (core::CVirtualGimbalEvent::isRotationEvent(event)) + return CCameraInputBindingUtilities::SInputMagnitudeDefaults::ImguizmoRotationUnitsPerRadian; + if (core::CVirtualGimbalEvent::isScaleEvent(event)) + return CCameraInputBindingUtilities::SInputMagnitudeDefaults::ImguizmoScaleUnitsPerFactor; + return IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale; +} + +IGimbalBindingLayout::imguizmo_to_virtual_events_t makeImguizmoPreset(const uint32_t allowedVirtualEvents) +{ + IGimbalBindingLayout::imguizmo_to_virtual_events_t preset; + for (const auto event : core::CVirtualGimbalEvent::VirtualEventsTypeTable) + { + if (event == core::CVirtualGimbalEvent::None) + continue; + if ((allowedVirtualEvents & event) != event) + continue; + preset.emplace(event, IGimbalBindingLayout::CHashInfo(event, getDefaultImguizmoMagnitudeScale(event))); + } + return preset; +} + +constexpr SCameraInteractionBindingSpec EmptyInteractionBindingSpec = {}; + +constexpr SKeyboardPresetSpec FpsKeyboardSpec = { + SCameraInputBindingEventGroups::FpsMove, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, + SCameraInputBindingEventGroups::LookYawPitch, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond +}; + +constexpr SKeyboardPresetSpec FreeKeyboardSpec = { + FpsKeyboardSpec.wasd, + FpsKeyboardSpec.wasdScale, + SCameraInputBindingEventGroups::Roll, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale +}; + +constexpr SKeyboardPresetSpec OrbitKeyboardSpec = { + SCameraInputBindingEventGroups::OrbitTranslate, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + SCameraInputBindingEventGroups::OrbitZoom, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale +}; + +constexpr SKeyboardPresetSpec TargetRigKeyboardSpec = { + FpsKeyboardSpec.wasd, + FpsKeyboardSpec.wasdScale, + SCameraInputBindingEventGroups::VerticalMove, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale +}; + +constexpr SKeyboardPresetSpec TurntableKeyboardSpec = { + SCameraInputBindingEventGroups::TurntableMove, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale, + FpsKeyboardSpec.ijkl, + FpsKeyboardSpec.ijklScale +}; + +constexpr SKeyboardPresetSpec TopDownKeyboardSpec = { + OrbitKeyboardSpec.wasd, + OrbitKeyboardSpec.wasdScale, + OrbitKeyboardSpec.qe, + OrbitKeyboardSpec.qeScale, + SCameraInputBindingEventGroups::PanOnly, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond +}; + +constexpr SKeyboardPresetSpec PathKeyboardSpec = { + SCameraInputBindingEventGroups::PathRigProgressAndU, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + SCameraInputBindingEventGroups::PathRigV, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::KeyboardHeldUnitsPerSecond, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale +}; + +constexpr SMousePresetSpec FpsMouseSpec = { + SCameraInputBindingEventGroups::RelativeLook, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + {}, + IGimbalBindingLayout::CHashInfo::DefaultMagnitudeScale +}; + +constexpr SMousePresetSpec OrbitMouseSpec = { + SCameraInputBindingEventGroups::RelativeOrbitTranslate, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + SCameraInputBindingEventGroups::OrbitZoom, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::ScrollUnitsPerStep +}; + +constexpr SMousePresetSpec TargetRigMouseSpec = { + FpsMouseSpec.relative, + FpsMouseSpec.relativeScale, + OrbitMouseSpec.scroll, + OrbitMouseSpec.scrollScale +}; + +constexpr SMousePresetSpec TopDownMouseSpec = { + SCameraInputBindingEventGroups::RelativeTopDown, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + OrbitMouseSpec.scroll, + OrbitMouseSpec.scrollScale +}; + +constexpr SMousePresetSpec PathMouseSpec = { + SCameraInputBindingEventGroups::RelativeOrbitTranslate, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::RelativeMouseUnitsPerStep, + SCameraInputBindingEventGroups::OrbitZoom, + CCameraInputBindingUtilities::SInputMagnitudeDefaults::ScrollUnitsPerStep +}; + +constexpr SCameraInteractionBindingSpec FpsInteractionBindingSpec = { + FpsKeyboardSpec, + FpsMouseSpec +}; + +constexpr SCameraInteractionBindingSpec FreeInteractionBindingSpec = { + FreeKeyboardSpec, + FpsMouseSpec +}; + +constexpr SCameraInteractionBindingSpec OrbitInteractionBindingSpec = { + OrbitKeyboardSpec, + OrbitMouseSpec +}; + +constexpr SCameraInteractionBindingSpec TargetRigInteractionBindingSpec = { + TargetRigKeyboardSpec, + TargetRigMouseSpec +}; + +constexpr SCameraInteractionBindingSpec TurntableInteractionBindingSpec = { + TurntableKeyboardSpec, + TargetRigMouseSpec +}; + +constexpr SCameraInteractionBindingSpec TopDownInteractionBindingSpec = { + TopDownKeyboardSpec, + TopDownMouseSpec +}; + +constexpr SCameraInteractionBindingSpec PathInteractionBindingSpec = { + PathKeyboardSpec, + PathMouseSpec +}; + +template +auto makePresetCache(const SpecArray& specs, Builder&& builder) +{ + std::array> cache = {}; + for (size_t i = 0u; i < specs.size(); ++i) + cache[i] = builder(specs[i]); + return cache; +} + +SCameraMappedInteractionBindingSpec mapInteractionBindingSpec(const SCameraInteractionBindingSpec& spec) +{ + return { + .keyboard = buildKeyboardPreset(spec.keyboard), + .mouse = buildMousePreset(spec.mouse) + }; +} + +constexpr std::array InteractionFamilyPresetSpecs = {{ + EmptyInteractionBindingSpec, + FpsInteractionBindingSpec, + FreeInteractionBindingSpec, + OrbitInteractionBindingSpec, + TargetRigInteractionBindingSpec, + TurntableInteractionBindingSpec, + TopDownInteractionBindingSpec, + PathInteractionBindingSpec +}}; + +const SCameraMappedInteractionBindingSpec& interactionBindingPresetForKind(const core::ICamera::CameraKind kind) +{ + const auto familyIx = interactionFamilyIndex(core::CCameraKindUtilities::getCameraInteractionFamily(kind)); + static const auto cache = makePresetCache( + InteractionFamilyPresetSpecs, + [](const SCameraInteractionBindingSpec& spec) { return mapInteractionBindingSpec(spec); }); + return cache[familyIx < cache.size() ? familyIx : 0u]; +} + +} // namespace + +bool CCameraInputBindingUtilities::hasMouseRelativeMovementBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) +{ + return containsBindingForAnyCodeGroups(mousePreset, SCameraInputBindingPhysicalGroups::RelativeMouseCodes); +} + +bool CCameraInputBindingUtilities::hasMouseScrollBinding(const IGimbalBindingLayout::mouse_to_virtual_events_t& mousePreset) +{ + return containsBindingForAnyCodeGroups( + mousePreset, + SCameraInputBindingPhysicalGroups::PositiveScrollCodes, + SCameraInputBindingPhysicalGroups::NegativeScrollCodes); +} + +const IGimbalBindingLayout::keyboard_to_virtual_events_t& CCameraInputBindingUtilities::getDefaultCameraKeyboardMappingPreset(const core::ICamera::CameraKind kind) +{ + return interactionBindingPresetForKind(kind).keyboard; +} + +const IGimbalBindingLayout::keyboard_to_virtual_events_t& CCameraInputBindingUtilities::getDefaultCameraKeyboardMappingPreset(const core::ICamera& camera) +{ + return getDefaultCameraKeyboardMappingPreset(camera.getKind()); +} + +const IGimbalBindingLayout::mouse_to_virtual_events_t& CCameraInputBindingUtilities::getDefaultCameraMouseMappingPreset(const core::ICamera::CameraKind kind) +{ + return interactionBindingPresetForKind(kind).mouse; +} + +const IGimbalBindingLayout::mouse_to_virtual_events_t& CCameraInputBindingUtilities::getDefaultCameraMouseMappingPreset(const core::ICamera& camera) +{ + return getDefaultCameraMouseMappingPreset(camera.getKind()); +} + +IGimbalBindingLayout::imguizmo_to_virtual_events_t CCameraInputBindingUtilities::buildDefaultCameraImguizmoMappingPreset(const uint32_t allowedVirtualEvents) +{ + return makeImguizmoPreset(allowedVirtualEvents); +} + +IGimbalBindingLayout::imguizmo_to_virtual_events_t CCameraInputBindingUtilities::buildDefaultCameraImguizmoMappingPreset(const core::ICamera& camera) +{ + return buildDefaultCameraImguizmoMappingPreset(camera.getAllowedVirtualEvents()); +} + +SCameraInputBindingPreset CCameraInputBindingUtilities::buildDefaultCameraInputBindingPreset( + const core::ICamera::CameraKind kind, + const uint32_t allowedVirtualEvents) +{ + SCameraInputBindingPreset preset; + preset.keyboard = getDefaultCameraKeyboardMappingPreset(kind); + preset.mouse = getDefaultCameraMouseMappingPreset(kind); + preset.imguizmo = buildDefaultCameraImguizmoMappingPreset(allowedVirtualEvents); + return preset; +} + +SCameraInputBindingPreset CCameraInputBindingUtilities::buildDefaultCameraInputBindingPreset(const core::ICamera& camera) +{ + return buildDefaultCameraInputBindingPreset(camera.getKind(), camera.getAllowedVirtualEvents()); +} + +void CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset( + IGimbalBindingLayout& layout, + const core::ICamera::CameraKind kind, + const uint32_t allowedVirtualEvents) +{ + const auto preset = buildDefaultCameraInputBindingPreset(kind, allowedVirtualEvents); + layout.updateKeyboardMapping([&](auto& map) { map = preset.keyboard; }); + layout.updateMouseMapping([&](auto& map) { map = preset.mouse; }); + layout.updateImguizmoMapping([&](auto& map) { map = preset.imguizmo; }); +} + +void CCameraInputBindingUtilities::applyDefaultCameraInputBindingPreset(IGimbalBindingLayout& layout, const core::ICamera& camera) +{ + applyDefaultCameraInputBindingPreset(layout, camera.getKind(), camera.getAllowedVirtualEvents()); +} + +} // namespace nbl::ui diff --git a/src/nbl/ext/Cameras/CCameraKeyframeTrack.cpp b/src/nbl/ext/Cameras/CCameraKeyframeTrack.cpp new file mode 100644 index 0000000000..f7aefd12cd --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraKeyframeTrack.cpp @@ -0,0 +1,152 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraKeyframeTrack.hpp" + +namespace nbl::core +{ + +bool CCameraKeyframeTrackUtilities::compareKeyframes(const CCameraKeyframe& lhs, const CCameraKeyframe& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) +{ + return hlsl::abs(static_cast(lhs.time - rhs.time)) <= timeEps && + CCameraPresetUtilities::comparePresets(lhs.preset, rhs.preset, posEps, rotEpsDeg, scalarEps); +} + +bool CCameraKeyframeTrackUtilities::compareKeyframeTracks(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps, const bool compareSelection) +{ + if ((compareSelection && lhs.selectedKeyframeIx != rhs.selectedKeyframeIx) || lhs.keyframes.size() != rhs.keyframes.size()) + return false; + + for (size_t i = 0u; i < lhs.keyframes.size(); ++i) + { + if (!compareKeyframes(lhs.keyframes[i], rhs.keyframes[i], timeEps, posEps, rotEpsDeg, scalarEps)) + return false; + } + + return true; +} + +bool CCameraKeyframeTrackUtilities::compareKeyframeTrackContent(const CCameraKeyframeTrack& lhs, const CCameraKeyframeTrack& rhs, + const double timeEps, const double posEps, const double rotEpsDeg, const double scalarEps) +{ + return compareKeyframeTracks(lhs, rhs, timeEps, posEps, rotEpsDeg, scalarEps, false); +} + +bool CCameraKeyframeTrackUtilities::tryBuildKeyframeTrackPresetAtTime(const CCameraKeyframeTrack& track, const float time, CCameraPreset& preset) +{ + if (track.keyframes.empty()) + return false; + + if (track.keyframes.size() == 1u) + { + preset = track.keyframes.front().preset; + return true; + } + + const auto clampedTime = std::clamp(time, 0.f, track.keyframes.back().time); + size_t idx = 0u; + while (idx + 1u < track.keyframes.size() && track.keyframes[idx + 1u].time < clampedTime) + ++idx; + + const auto& a = track.keyframes[idx]; + const auto& b = track.keyframes[std::min(idx + 1u, track.keyframes.size() - 1u)]; + if (b.time <= a.time) + { + preset = a.preset; + return true; + } + + const double alpha = static_cast(clampedTime - a.time) / static_cast(b.time - a.time); + preset = a.preset; + CCameraPresetUtilities::assignGoalToPreset( + preset, + CCameraGoalUtilities::blendGoals( + CCameraPresetUtilities::makeGoalFromPreset(a.preset), + CCameraPresetUtilities::makeGoalFromPreset(b.preset), + alpha)); + return true; +} + +void CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(CCameraKeyframeTrack& track) +{ + std::sort(track.keyframes.begin(), track.keyframes.end(), [](const auto& a, const auto& b) { return a.time < b.time; }); +} + +void CCameraKeyframeTrackUtilities::clampTrackTimeToKeyframes(const CCameraKeyframeTrack& track, float& time) +{ + if (track.keyframes.empty()) + { + time = 0.f; + return; + } + + time = std::clamp(time, 0.f, track.keyframes.back().time); +} + +int CCameraKeyframeTrackUtilities::selectKeyframeTrackNearestTime(CCameraKeyframeTrack& track, const float time) +{ + if (track.keyframes.empty()) + { + track.selectedKeyframeIx = -1; + return track.selectedKeyframeIx; + } + + size_t bestIx = 0u; + float bestDelta = hlsl::abs(track.keyframes.front().time - time); + for (size_t i = 1u; i < track.keyframes.size(); ++i) + { + const float delta = hlsl::abs(track.keyframes[i].time - time); + if (delta < bestDelta) + { + bestDelta = delta; + bestIx = i; + } + } + + track.selectedKeyframeIx = static_cast(bestIx); + return track.selectedKeyframeIx; +} + +void CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(CCameraKeyframeTrack& track) +{ + if (track.keyframes.empty()) + { + track.selectedKeyframeIx = -1; + return; + } + + if (track.selectedKeyframeIx < 0) + track.selectedKeyframeIx = 0; + else if (track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + track.selectedKeyframeIx = static_cast(track.keyframes.size()) - 1; +} + +CCameraKeyframe* CCameraKeyframeTrackUtilities::getSelectedKeyframe(CCameraKeyframeTrack& track) +{ + normalizeSelectedKeyframeTrack(track); + if (track.selectedKeyframeIx < 0) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; +} + +const CCameraKeyframe* CCameraKeyframeTrackUtilities::getSelectedKeyframe(const CCameraKeyframeTrack& track) +{ + if (track.selectedKeyframeIx < 0 || track.selectedKeyframeIx >= static_cast(track.keyframes.size())) + return nullptr; + return &track.keyframes[static_cast(track.selectedKeyframeIx)]; +} + +bool CCameraKeyframeTrackUtilities::replaceSelectedKeyframePreset(CCameraKeyframeTrack& track, CCameraPreset preset) +{ + auto* selected = getSelectedKeyframe(track); + if (!selected) + return false; + + selected->preset = std::move(preset); + return true; +} + +} // namespace nbl::core diff --git a/src/nbl/ext/Cameras/CCameraPathUtilities.cpp b/src/nbl/ext/Cameras/CCameraPathUtilities.cpp new file mode 100644 index 0000000000..f7dd138084 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraPathUtilities.cpp @@ -0,0 +1,392 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraPathUtilities.hpp" + +namespace nbl::core +{ + +ICamera::PathState CCameraPathUtilities::makeDefaultPathState(const double minU) +{ + return { + .s = 0.0, + .u = minU, + .v = 0.0, + .roll = 0.0 + }; +} + +SCameraPathComparisonThresholds CCameraPathUtilities::makePathComparisonThresholds( + const double angularToleranceDeg, + const double scalarTolerance) +{ + return { + .sToleranceDeg = angularToleranceDeg, + .rollToleranceDeg = angularToleranceDeg, + .scalarTolerance = scalarTolerance + }; +} + +bool CCameraPathUtilities::isPathStateFinite(const ICamera::PathState& state) +{ + return hlsl::CCameraMathUtilities::isFiniteScalar(state.s) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.u) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.v) && + hlsl::CCameraMathUtilities::isFiniteScalar(state.roll); +} + +bool CCameraPathUtilities::isPathLimitsWellFormed(const SCameraPathLimits& limits) +{ + return hlsl::CCameraMathUtilities::isFiniteScalar(limits.minU) && + hlsl::CCameraMathUtilities::isFiniteScalar(limits.minDistance) && + !std::isnan(static_cast(limits.maxDistance)); +} + +bool CCameraPathUtilities::sanitizePathLimits(SCameraPathLimits& limits) +{ + if (!isPathLimitsWellFormed(limits)) + return false; + + limits.minU = std::max(limits.minU, 0.0); + limits.minDistance = std::max( + std::max(limits.minDistance, static_cast(limits.minU)), + static_cast(SCameraTargetRelativeTraits::MinDistance)); + + if (!std::isfinite(static_cast(limits.maxDistance))) + limits.maxDistance = std::numeric_limits::infinity(); + else + limits.maxDistance = std::max(limits.maxDistance, limits.minDistance); + return true; +} + +bool CCameraPathUtilities::sanitizePathState(ICamera::PathState& state, const double minU) +{ + return hlsl::CCameraMathUtilities::sanitizePathState(state.s, state.u, state.v, state.roll, minU); +} + +bool CCameraPathUtilities::sanitizePathState(ICamera::PathState& state, const SCameraPathLimits& limits, double* outAppliedDistance) +{ + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + + if (!sanitizePathState(state, sanitizedLimits.minU)) + return false; + + const auto desiredDistance = std::clamp( + hlsl::CCameraMathUtilities::getPathDistance(state.u, state.v), + sanitizedLimits.minDistance, + sanitizedLimits.maxDistance); + return tryScalePathStateDistance(desiredDistance, sanitizedLimits.minU, state, outAppliedDistance); +} + +bool CCameraPathUtilities::tryScalePathStateDistance( + const double desiredDistance, + const double minU, + ICamera::PathState& ioState, + double* outAppliedDistance) +{ + return hlsl::CCameraMathUtilities::tryScalePathStateDistance( + desiredDistance, + minU, + ioState.u, + ioState.v, + outAppliedDistance); +} + +bool CCameraPathUtilities::tryUpdatePathStateDistance( + const float desiredDistance, + const SCameraPathLimits& limits, + ICamera::PathState& ioState, + SCameraPathDistanceUpdateResult* outResult) +{ + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits) || !sanitizePathState(ioState, sanitizedLimits)) + return false; + + const auto clampedDistance = std::clamp(desiredDistance, sanitizedLimits.minDistance, sanitizedLimits.maxDistance); + double appliedDistance = 0.0; + if (!tryScalePathStateDistance(static_cast(clampedDistance), sanitizedLimits.minU, ioState, &appliedDistance)) + return false; + + if (outResult) + { + outResult->appliedDistance = appliedDistance; + outResult->exact = (clampedDistance == desiredDistance) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(appliedDistance, static_cast(desiredDistance), SCameraPathDefaults::ScalarTolerance); + } + return true; +} + +bool CCameraPathUtilities::tryBuildPathStateFromPosition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const double minU, + ICamera::PathState& outState) +{ + outState = {}; + if (!hlsl::CCameraMathUtilities::tryBuildPathStateFromPosition( + targetPosition, + position, + minU, + outState.s, + outState.u, + outState.v)) + { + return false; + } + + outState.roll = 0.0; + return true; +} + +bool CCameraPathUtilities::tryResolvePathState( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const SCameraPathLimits& limits, + const ICamera::PathState* requestedState, + ICamera::PathState& outState) +{ + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + + if (requestedState) + { + outState = *requestedState; + return sanitizePathState(outState, sanitizedLimits); + } + + if (tryBuildPathStateFromPosition(targetPosition, position, sanitizedLimits.minU, outState)) + return sanitizePathState(outState, sanitizedLimits); + + outState = makeDefaultPathState(sanitizedLimits.minU); + return sanitizePathState(outState, sanitizedLimits); +} + +bool CCameraPathUtilities::tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraPathPose& outPose) +{ + SCameraPathLimits sanitizedLimits = limits; + if (!sanitizePathLimits(sanitizedLimits)) + return false; + + return hlsl::CCameraMathUtilities::tryBuildPathPoseFromState( + targetPosition, + state.s, + state.u, + state.v, + state.roll, + sanitizedLimits.minU, + sanitizedLimits.minDistance, + sanitizedLimits.maxDistance, + outPose.position, + outPose.orientation, + &outPose.appliedDistance, + &outPose.orbitUv); +} + +bool CCameraPathUtilities::tryBuildPathPoseFromState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + hlsl::float64_t3& outPosition, + hlsl::camera_quaternion_t& outOrientation, + hlsl::float64_t* outAppliedDistance, + hlsl::float64_t2* outOrbitUv) +{ + SCameraPathPose pathPose = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, pathPose)) + return false; + + outPosition = pathPose.position; + outOrientation = pathPose.orientation; + if (outAppliedDistance) + *outAppliedDistance = pathPose.appliedDistance; + if (outOrbitUv) + *outOrbitUv = pathPose.orbitUv; + return true; +} + +bool CCameraPathUtilities::pathStatesNearlyEqual( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds) +{ + return hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.s, rhs.s) <= thresholds.sToleranceDeg && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.u, rhs.u, thresholds.scalarTolerance) && + hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs.v, rhs.v, thresholds.scalarTolerance) && + hlsl::CCameraMathUtilities::getWrappedAngleDistanceDegrees(lhs.roll, rhs.roll) <= thresholds.rollToleranceDeg; +} + +bool CCameraPathUtilities::pathStatesChanged( + const ICamera::PathState& lhs, + const ICamera::PathState& rhs, + const SCameraPathComparisonThresholds& thresholds) +{ + return !pathStatesNearlyEqual(lhs, rhs, thresholds); +} + +hlsl::float64_t4 CCameraPathUtilities::buildPathStateDeltaVector( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) +{ + auto deltaVector = desiredState.asVector() - currentState.asVector(); + deltaVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.x); + deltaVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(deltaVector.w); + return deltaVector; +} + +SCameraPathDelta CCameraPathUtilities::buildPathStateDelta( + const ICamera::PathState& currentState, + const ICamera::PathState& desiredState) +{ + return SCameraPathDelta::fromVector(buildPathStateDeltaVector(currentState, desiredState)); +} + +SCameraPathDelta CCameraPathUtilities::makePathDeltaFromVirtualPathMotion( + const hlsl::float64_t3& translation, + const hlsl::float64_t3& rotation) +{ + return SCameraPathDelta::fromMotion(translation, rotation.z); +} + +SCameraPathDelta CCameraPathUtilities::buildDefaultPathControlDelta(const SCameraPathControlContext& context) +{ + return makePathDeltaFromVirtualPathMotion(context.translation, context.rotation); +} + +void CCameraPathUtilities::appendPathDeltaEvents( + std::vector& events, + const SCameraPathDelta& delta, + const double moveDenominator, + const double rotationDenominator, + const SCameraPathComparisonThresholds& thresholds) +{ + CCameraVirtualEventUtilities::appendLocalTranslationEvents( + events, + delta.translationVector(), + hlsl::float64_t3(moveDenominator), + hlsl::float64_t3(thresholds.scalarTolerance)); + CCameraVirtualEventUtilities::appendAngularDeltaEvent( + events, + delta.roll, + rotationDenominator, + thresholds.rollToleranceDeg, + CVirtualGimbalEvent::RollRight, + CVirtualGimbalEvent::RollLeft); +} + +bool CCameraPathUtilities::tryBuildCanonicalPathState( + const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraCanonicalPathState& outState) +{ + outState = {}; + if (!tryBuildPathPoseFromState(targetPosition, state, limits, outState.pose)) + return false; + + outState.targetRelative = { + .target = targetPosition, + .orbitUv = outState.pose.orbitUv, + .distance = static_cast(outState.pose.appliedDistance) + }; + return true; +} + +bool CCameraPathUtilities::tryApplyPathStateDelta( + const ICamera::PathState& currentState, + const SCameraPathDelta& delta, + const SCameraPathLimits& limits, + ICamera::PathState& outState) +{ + auto stateVector = currentState.asVector() + delta.asVector(); + stateVector.x = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.x); + stateVector.w = hlsl::CCameraMathUtilities::wrapAngleRad(stateVector.w); + outState = ICamera::PathState::fromVector(stateVector); + return sanitizePathState(outState, limits); +} + +ICamera::PathState CCameraPathUtilities::blendPathStates( + const ICamera::PathState& from, + const ICamera::PathState& to, + const double alpha) +{ + const auto fromVector = from.asVector(); + const auto toVector = to.asVector(); + return { + .s = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.x, toVector.x, alpha), + .u = fromVector.y + (toVector.y - fromVector.y) * alpha, + .v = fromVector.z + (toVector.z - fromVector.z) * alpha, + .roll = hlsl::CCameraMathUtilities::lerpWrappedAngleRad(fromVector.w, toVector.w, alpha) + }; +} + +bool CCameraPathUtilities::tryBuildPathStateTransition( + const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& currentPosition, + const hlsl::float64_t3& desiredPosition, + const SCameraPathLimits& limits, + const ICamera::PathState* currentStateOverride, + const ICamera::PathState* desiredStateOverride, + SCameraPathStateTransition& outTransition) +{ + if (!tryResolvePathState(targetPosition, currentPosition, limits, currentStateOverride, outTransition.current)) + return false; + if (!tryResolvePathState(targetPosition, desiredPosition, limits, desiredStateOverride, outTransition.desired)) + return false; + + outTransition.delta = buildPathStateDelta(outTransition.current, outTransition.desired); + return true; +} + +SCameraPathModel CCameraPathUtilities::makeDefaultPathModel() +{ + return { + .resolveState = + [](const hlsl::float64_t3& targetPosition, + const hlsl::float64_t3& position, + const SCameraPathLimits& limits, + const ICamera::PathState* requestedState, + ICamera::PathState& outState) -> bool + { + return tryResolvePathState(targetPosition, position, limits, requestedState, outState); + }, + .controlLaw = + [](const SCameraPathControlContext& context) -> SCameraPathDelta + { + return buildDefaultPathControlDelta(context); + }, + .integrate = + [](const ICamera::PathState& currentState, + const SCameraPathDelta& delta, + const SCameraPathLimits& limits, + ICamera::PathState& outState) -> bool + { + return tryApplyPathStateDelta(currentState, delta, limits, outState); + }, + .evaluate = + [](const hlsl::float64_t3& targetPosition, + const ICamera::PathState& state, + const SCameraPathLimits& limits, + SCameraCanonicalPathState& outState) -> bool + { + return tryBuildCanonicalPathState(targetPosition, state, limits, outState); + }, + .updateDistance = + [](const float desiredDistance, + const SCameraPathLimits& limits, + ICamera::PathState& ioState, + SCameraPathDistanceUpdateResult* outResult) -> bool + { + return tryUpdatePathStateDistance(desiredDistance, limits, ioState, outResult); + } + }; +} + +} // namespace nbl::core diff --git a/src/nbl/ext/Cameras/CCameraScriptedCheckRunner.cpp b/src/nbl/ext/Cameras/CCameraScriptedCheckRunner.cpp new file mode 100644 index 0000000000..a0e977a08a --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraScriptedCheckRunner.cpp @@ -0,0 +1,492 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraScriptedCheckRunner.hpp" + +namespace nbl::system +{ + +void CCameraScriptedCheckRunnerUtilities::scriptedCheckSetStepReference( + CCameraScriptedCheckRuntimeState& state, + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) +{ + state.step.valid = true; + state.step.position = position; + state.step.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); +} + +void CCameraScriptedCheckRunnerUtilities::scriptedCheckSetBaselineReference( + CCameraScriptedCheckRuntimeState& state, + const hlsl::float64_t3& position, + const hlsl::camera_quaternion_t& orientation) +{ + state.baseline.valid = true; + state.baseline.position = position; + state.baseline.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(orientation); + scriptedCheckSetStepReference(state, position, orientation); +} + +bool CCameraScriptedCheckRunnerUtilities::scriptedCheckComputePoseDelta( + const hlsl::float64_t3& currentPosition, + const hlsl::camera_quaternion_t& currentOrientation, + const hlsl::float64_t3& referencePosition, + const hlsl::camera_quaternion_t& referenceOrientation, + hlsl::SCameraPoseDelta& outDelta) +{ + return hlsl::CCameraMathUtilities::tryComputePoseDelta( + currentPosition, + currentOrientation, + referencePosition, + referenceOrientation, + outDelta); +} + +void CCameraScriptedCheckRunnerUtilities::appendScriptedCheckLog( + CCameraScriptedCheckFrameResult& result, + const bool failure, + std::string&& text) +{ + result.logs.push_back({ + .failure = failure, + .text = std::move(text) + }); + result.hadFailures = result.hadFailures || failure; +} + +CCameraScriptedCheckFrameResult CCameraScriptedCheckRunnerUtilities::evaluateScriptedChecksForFrame( + const std::vector& checks, + CCameraScriptedCheckRuntimeState& state, + const CCameraScriptedCheckContext& context) +{ + CCameraScriptedCheckFrameResult result = {}; + + while (state.nextCheckIndex < checks.size() && checks[state.nextCheckIndex].frame == context.frame) + { + const auto& check = checks[state.nextCheckIndex]; + + if (!context.camera) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " no active camera"; + })); + ++state.nextCheckIndex; + continue; + } + + const auto& gimbal = context.camera->getGimbal(); + const auto pos = gimbal.getPosition(); + const auto orientation = hlsl::CCameraMathUtilities::normalizeQuaternion(gimbal.getOrientation()); + const auto eulerDeg = hlsl::CCameraMathUtilities::castVector(hlsl::CCameraMathUtilities::getCameraOrientationEulerDegrees(orientation)); + + if (!hlsl::CCameraMathUtilities::isFiniteVec3(pos) || !hlsl::CCameraMathUtilities::isFiniteQuaternion(orientation) || !hlsl::CCameraMathUtilities::isFiniteVec3(eulerDeg)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] check frame=" << context.frame << " non-finite gimbal state"; + })); + ++state.nextCheckIndex; + continue; + } + + switch (check.kind) + { + case CCameraScriptedInputCheck::Kind::Baseline: + { + scriptedCheckSetBaselineReference(state, pos, orientation); + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(3); + oss << "[script][pass] baseline frame=" << context.frame + << " pos=(" << pos.x << ", " << pos.y << ", " << pos.z << ")" + << " euler_deg=(" << eulerDeg.x << ", " << eulerDeg.y << ", " << eulerDeg.z << ")"; + })); + break; + } + case CCameraScriptedInputCheck::Kind::ImguizmoVirtual: + { + bool ok = true; + if (!context.imguizmoVirtual || context.imguizmoVirtualCount == 0u) + { + ok = false; + } + else + { + for (const auto& expected : check.expectedVirtualEvents) + { + bool found = false; + double actual = 0.0; + for (uint32_t i = 0u; i < context.imguizmoVirtualCount; ++i) + { + if (context.imguizmoVirtual[i].type == expected.type) + { + found = true; + actual = context.imguizmoVirtual[i].magnitude; + break; + } + } + + if (!found || hlsl::abs(actual - expected.magnitude) > check.tolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] imguizmo_virtual frame=" << context.frame + << " type=" << core::CVirtualGimbalEvent::virtualEventToString(expected.type).data() + << " expected=" << expected.magnitude + << " actual=" << actual + << " tol=" << check.tolerance; + })); + } + } + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][pass] imguizmo_virtual frame=" << context.frame + << " events=" << check.expectedVirtualEvents.size(); + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalNear: + { + bool ok = true; + if (check.hasExpectedPos) + { + const double distance = hlsl::length(pos - hlsl::CCameraMathUtilities::castVector(check.expectedPos)); + if (distance > check.posTolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_near frame=" << context.frame + << " pos_diff=" << distance + << " tol=" << check.posTolerance; + })); + } + } + if (check.hasExpectedEuler) + { + const auto expectedOrientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( + hlsl::CCameraMathUtilities::castVector(check.expectedEulerDeg)); + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, pos, expectedOrientation, poseDelta)) + poseDelta.rotationDeg = std::numeric_limits::infinity(); + const auto rotationDeltaDeg = poseDelta.rotationDeg; + if (rotationDeltaDeg > check.eulerToleranceDeg) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_near frame=" << context.frame + << " rot_delta_deg=" << rotationDeltaDeg + << " tol=" << check.eulerToleranceDeg; + })); + } + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][pass] gimbal_near frame=" << context.frame; + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalDelta: + { + if (!state.baseline.valid) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_delta frame=" << context.frame << " missing baseline"; + })); + break; + } + + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, state.baseline.position, state.baseline.orientation, poseDelta)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_delta frame=" << context.frame << " non-finite pose delta"; + })); + break; + } + + if (poseDelta.position > check.posTolerance || poseDelta.rotationDeg > check.eulerToleranceDeg) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_delta frame=" << context.frame + << " pos_diff=" << poseDelta.position + << " tol=" << check.posTolerance + << " rot_delta_deg=" << poseDelta.rotationDeg + << " tol=" << check.eulerToleranceDeg; + })); + } + else + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] gimbal_delta frame=" << context.frame + << " pos_diff=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; + })); + } + break; + } + case CCameraScriptedInputCheck::Kind::GimbalStep: + { + if (!state.step.valid) + { + if (state.baseline.valid) + { + scriptedCheckSetStepReference(state, state.baseline.position, state.baseline.orientation); + } + else + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_step frame=" << context.frame << " missing step reference"; + })); + scriptedCheckSetStepReference(state, pos, orientation); + ++state.nextCheckIndex; + continue; + } + } + + hlsl::SCameraPoseDelta poseDelta = {}; + if (!scriptedCheckComputePoseDelta(pos, orientation, state.step.position, state.step.orientation, poseDelta)) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] gimbal_step frame=" << context.frame << " non-finite pose delta"; + })); + scriptedCheckSetStepReference(state, pos, orientation); + break; + } + + bool ok = true; + bool requiresProgress = false; + bool hasProgress = false; + if (check.hasPosDeltaConstraint) + { + if (poseDelta.position > check.posTolerance) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " pos_delta=" << poseDelta.position + << " max=" << check.posTolerance; + })); + } + if (check.minPosDelta > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || poseDelta.position >= check.minPosDelta; + } + } + if (check.hasEulerDeltaConstraint) + { + if (poseDelta.rotationDeg > check.eulerToleranceDeg) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " rot_delta_deg=" << poseDelta.rotationDeg + << " max=" << check.eulerToleranceDeg; + })); + } + if (check.minEulerDeltaDeg > 0.0f) + { + requiresProgress = true; + hasProgress = hasProgress || poseDelta.rotationDeg >= check.minEulerDeltaDeg; + } + } + if (requiresProgress && !hasProgress) + { + ok = false; + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][fail] gimbal_step frame=" << context.frame + << " missing progress pos_delta=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; + })); + } + + if (ok) + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] gimbal_step frame=" << context.frame + << " pos_delta=" << poseDelta.position + << " rot_delta_deg=" << poseDelta.rotationDeg; + })); + } + scriptedCheckSetStepReference(state, pos, orientation); + break; + } + case CCameraScriptedInputCheck::Kind::FollowTargetLock: + { + if (!context.followConfig) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing follow config"; + })); + break; + } + if (!context.trackedTarget) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing tracked target"; + })); + break; + } + if (!context.goalSolver) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << " missing goal solver"; + })); + break; + } + + SCameraFollowRegressionResult regression = {}; + std::string regressionError; + core::CCameraGoal expectedFollowGoal = {}; + const auto thresholds = CCameraFollowRegressionUtilities::makeFollowRegressionThresholds(check.posTolerance, check.eulerToleranceDeg); + const bool ok = core::CCameraFollowUtilities::tryBuildFollowGoal( + *context.goalSolver, + context.camera, + *context.trackedTarget, + *context.followConfig, + expectedFollowGoal) && + CCameraFollowRegressionUtilities::validateFollowTargetContract( + context.camera, + *context.trackedTarget, + *context.followConfig, + expectedFollowGoal, + regression, + ®ressionError, + context.followProjectionContext, + thresholds); + + if (!ok) + { + appendScriptedCheckLog( + result, + true, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << "[script][fail] follow_lock frame=" << context.frame << ' ' + << (regressionError.empty() ? "follow validation mismatch" : regressionError); + })); + } + else + { + appendScriptedCheckLog( + result, + false, + buildScriptedCheckMessage([&](std::ostringstream& oss) + { + oss << std::fixed << std::setprecision(6); + oss << "[script][pass] follow_lock frame=" << context.frame + << " angle_deg=" << regression.lockAngleDeg + << " target_distance=" << regression.targetDistance + << " screen_ndc=" << regression.projectedTarget.radius; + })); + } + break; + } + } + + ++state.nextCheckIndex; + } + + return result; +} + +} // namespace nbl::system diff --git a/src/nbl/ext/Cameras/CCameraSequenceScript.cpp b/src/nbl/ext/Cameras/CCameraSequenceScript.cpp new file mode 100644 index 0000000000..157613fe9f --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraSequenceScript.cpp @@ -0,0 +1,509 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraSequenceScript.hpp" + +namespace nbl::core +{ + +bool CCameraSequenceScriptUtilities::tryParseCameraKind(std::string_view value, ICamera::CameraKind& outKind) +{ + if (value == "FPS") + outKind = ICamera::CameraKind::FPS; + else if (value == "Free") + outKind = ICamera::CameraKind::Free; + else if (value == "Orbit") + outKind = ICamera::CameraKind::Orbit; + else if (value == "Arcball") + outKind = ICamera::CameraKind::Arcball; + else if (value == "Turntable") + outKind = ICamera::CameraKind::Turntable; + else if (value == "TopDown") + outKind = ICamera::CameraKind::TopDown; + else if (value == "Isometric") + outKind = ICamera::CameraKind::Isometric; + else if (value == "Chase") + outKind = ICamera::CameraKind::Chase; + else if (value == "Dolly") + outKind = ICamera::CameraKind::Dolly; + else if (value == "DollyZoom" || value == "Dolly Zoom") + outKind = ICamera::CameraKind::DollyZoom; + else if (value == "PathRig" || value == "Path Rig") + outKind = ICamera::CameraKind::Path; + else + return false; + + return true; +} + +bool CCameraSequenceScriptUtilities::tryParseProjectionType(std::string_view value, IPlanarProjection::CProjection::ProjectionType& outType) +{ + if (value == "perspective" || value == "Perspective") + outType = IPlanarProjection::CProjection::Perspective; + else if (value == "orthographic" || value == "Orthographic") + outType = IPlanarProjection::CProjection::Orthographic; + else + return false; + + return true; +} + +void CCameraSequenceScriptUtilities::normalizeCaptureFractions(std::vector& fractions) +{ + for (auto& fraction : fractions) + fraction = std::clamp(fraction, 0.f, 1.f); + + std::sort(fractions.begin(), fractions.end()); + fractions.erase( + std::unique( + fractions.begin(), + fractions.end(), + [](const float lhs, const float rhs) + { + return hlsl::CCameraMathUtilities::nearlyEqualScalar(lhs, rhs, static_cast(SCameraToolingThresholds::ScalarTolerance)); + }), + fractions.end()); +} + +bool CCameraSequenceScriptUtilities::buildSequenceKeyframePreset(const CCameraPreset& reference, const CCameraSequenceKeyframe& authored, CCameraPreset& outPreset, std::string* error) +{ + if (authored.hasAbsolutePreset) + { + outPreset = authored.absolutePreset; + if (outPreset.identifier.empty()) + outPreset.identifier = reference.identifier; + if (outPreset.name.empty()) + outPreset.name = reference.name; + return CCameraGoalUtilities::isGoalFinite(CCameraPresetUtilities::makeGoalFromPreset(outPreset)); + } + + outPreset = reference; + if (!authored.hasDelta) + return true; + + auto goal = CCameraPresetUtilities::makeGoalFromPreset(reference); + const auto& delta = authored.delta; + + const bool hasPoseDelta = delta.hasPositionOffset || delta.hasRotationEulerDegOffset; + const bool hasSphericalDelta = delta.hasTargetOffset || delta.orbitDelta.hasAny(); + const bool hasPathDelta = delta.pathDelta.hasAny(); + + if (hasPoseDelta && (hasSphericalDelta || hasPathDelta)) + { + if (error) + *error = "Sequence keyframe delta cannot mix pose offsets with spherical/path deltas."; + return false; + } + + if (delta.hasPositionOffset) + goal.position += delta.positionOffset; + + if (delta.hasRotationEulerDegOffset) + { + goal.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion( + goal.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( + hlsl::CCameraMathUtilities::castVector(delta.rotationEulerDegOffset))); + } + + if (delta.hasTargetOffset) + { + if (!goal.hasTargetPosition) + { + if (error) + *error = "Sequence keyframe target_offset requires target state."; + return false; + } + goal.targetPosition += delta.targetOffset; + } + + if (delta.orbitDelta.hasAny()) + { + if (!goal.hasOrbitState) + { + if (error) + *error = "Sequence keyframe orbit deltas require spherical orbit state."; + return false; + } + + if (delta.orbitDelta.hasU) + goal.orbitUv.x = hlsl::CCameraMathUtilities::wrapAngleRad(goal.orbitUv.x + delta.orbitDelta.uvDeltaRad.x); + if (delta.orbitDelta.hasV) + { + goal.orbitUv.y = std::clamp( + goal.orbitUv.y + delta.orbitDelta.uvDeltaRad.y, + -SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad, + SCameraTargetRelativeRigDefaults::ArcballPitchLimitRad); + } + if (delta.orbitDelta.hasDistance) + goal.orbitDistance += delta.orbitDelta.distanceDelta; + } + + if (delta.pathDelta.hasAny()) + { + if (!goal.hasPathState) + { + if (error) + *error = "Sequence keyframe path deltas require path state."; + return false; + } + + if (!CCameraPathUtilities::tryApplyPathStateDelta( + goal.pathState, + delta.pathDelta.buildAppliedDelta(), + CCameraPathUtilities::makeDefaultPathLimits(), + goal.pathState)) + { + if (error) + *error = "Sequence keyframe path deltas produced an invalid path state."; + return false; + } + } + + if (delta.hasDynamicBaseFovDelta || delta.hasDynamicReferenceDistanceDelta) + { + if (!goal.hasDynamicPerspectiveState) + { + if (error) + *error = "Sequence keyframe dynamic perspective deltas require dynamic perspective state."; + return false; + } + if (delta.hasDynamicBaseFovDelta) + goal.dynamicPerspectiveState.baseFov = std::clamp(goal.dynamicPerspectiveState.baseFov + delta.dynamicBaseFovDelta, 1.f, 179.f); + if (delta.hasDynamicReferenceDistanceDelta) + { + goal.dynamicPerspectiveState.referenceDistance = std::max( + 0.001f, + goal.dynamicPerspectiveState.referenceDistance + delta.dynamicReferenceDistanceDelta); + } + } + + if (hasPathDelta || hasSphericalDelta) + { + if (!CCameraGoalUtilities::applyCanonicalGoalState(goal)) + { + if (error) + { + *error = hasPathDelta ? + "Sequence keyframe failed to canonicalize path state." : + "Sequence keyframe failed to canonicalize spherical state."; + } + return false; + } + } + + if (!CCameraGoalUtilities::isGoalFinite(goal)) + { + if (error) + *error = "Sequence keyframe produced a non-finite goal."; + return false; + } + + CCameraPresetUtilities::assignGoalToPreset(outPreset, goal); + return true; +} + +bool CCameraSequenceScriptUtilities::buildSequenceTrackFromReference(const CCameraPreset& reference, const CCameraSequenceSegment& segment, CCameraKeyframeTrack& outTrack, std::string* error) +{ + outTrack = {}; + outTrack.keyframes.reserve(segment.keyframes.size()); + + for (const auto& entry : segment.keyframes) + { + CCameraKeyframe keyframe; + keyframe.time = std::max(0.f, entry.time); + if (!buildSequenceKeyframePreset(reference, entry, keyframe.preset, error)) + return false; + outTrack.keyframes.emplace_back(std::move(keyframe)); + } + + CCameraKeyframeTrackUtilities::sortKeyframeTrackByTime(outTrack); + CCameraKeyframeTrackUtilities::normalizeSelectedKeyframeTrack(outTrack); + return !outTrack.keyframes.empty(); +} + +bool CCameraSequenceScriptUtilities::isSequenceTrackedTargetPoseFinite(const CCameraSequenceTrackedTargetPose& pose) +{ + return hlsl::CCameraMathUtilities::isFiniteVec3(pose.position) && + hlsl::CCameraMathUtilities::isFiniteQuaternion(pose.orientation); +} + +bool CCameraSequenceScriptUtilities::buildSequenceTrackedTargetPoseFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceTrackedTargetKeyframe& authored, + CCameraSequenceTrackedTargetPose& outPose, + std::string* error) +{ + outPose = reference; + + if (authored.hasAbsolutePosition) + outPose.position = authored.absolutePosition; + if (authored.hasAbsoluteRotationEulerDeg) + { + outPose.orientation = hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( + hlsl::CCameraMathUtilities::castVector(authored.absoluteRotationEulerDeg)); + } + + if (authored.hasDelta) + { + if (authored.delta.hasPositionOffset) + outPose.position += authored.delta.positionOffset; + if (authored.delta.hasRotationEulerDegOffset) + { + outPose.orientation = hlsl::CCameraMathUtilities::normalizeQuaternion( + outPose.orientation * hlsl::CCameraMathUtilities::makeQuaternionFromEulerDegreesYXZ( + hlsl::CCameraMathUtilities::castVector(authored.delta.rotationEulerDegOffset))); + } + } + + if (!isSequenceTrackedTargetPoseFinite(outPose)) + { + if (error) + *error = "Sequence target keyframe produced a non-finite pose."; + return false; + } + + return true; +} + +bool CCameraSequenceScriptUtilities::buildSequenceTrackedTargetTrackFromReference( + const CCameraSequenceTrackedTargetPose& reference, + const CCameraSequenceSegment& segment, + CCameraSequenceTrackedTargetTrack& outTrack, + std::string* error) +{ + outTrack = {}; + outTrack.keyframes.reserve(segment.targetKeyframes.size()); + + for (const auto& entry : segment.targetKeyframes) + { + CCameraSequenceTrackedTargetTrack::SKeyframe keyframe; + keyframe.time = std::max(0.f, entry.time); + if (!buildSequenceTrackedTargetPoseFromReference(reference, entry, keyframe.pose, error)) + return false; + outTrack.keyframes.emplace_back(std::move(keyframe)); + } + + std::stable_sort( + outTrack.keyframes.begin(), + outTrack.keyframes.end(), + [](const auto& lhs, const auto& rhs) + { + if (lhs.time == rhs.time) + return false; + return lhs.time < rhs.time; + }); + + std::vector normalized; + normalized.reserve(outTrack.keyframes.size()); + for (const auto& keyframe : outTrack.keyframes) + { + if (!normalized.empty() && + hlsl::CCameraMathUtilities::nearlyEqualScalar( + normalized.back().time, + keyframe.time, + static_cast(SCameraToolingThresholds::ScalarTolerance))) + { + normalized.back() = keyframe; + } + else + { + normalized.emplace_back(keyframe); + } + } + outTrack.keyframes = std::move(normalized); + + return !outTrack.keyframes.empty(); +} + +bool CCameraSequenceScriptUtilities::tryBuildSequenceTrackedTargetPoseAtTime( + const CCameraSequenceTrackedTargetTrack& track, + const float time, + CCameraSequenceTrackedTargetPose& outPose) +{ + if (track.keyframes.empty()) + return false; + if (track.keyframes.size() == 1u || time <= track.keyframes.front().time) + { + outPose = track.keyframes.front().pose; + return true; + } + if (time >= track.keyframes.back().time) + { + outPose = track.keyframes.back().pose; + return true; + } + + for (size_t ix = 1u; ix < track.keyframes.size(); ++ix) + { + const auto& lhs = track.keyframes[ix - 1u]; + const auto& rhs = track.keyframes[ix]; + if (time > rhs.time) + continue; + + const auto span = std::max(static_cast(SCameraToolingThresholds::ScalarTolerance), rhs.time - lhs.time); + const auto alpha = std::clamp((time - lhs.time) / span, 0.f, 1.f); + outPose.position = lhs.pose.position + (rhs.pose.position - lhs.pose.position) * static_cast(alpha); + outPose.orientation = hlsl::CCameraMathUtilities::slerpQuaternion(lhs.pose.orientation, rhs.pose.orientation, static_cast(alpha)); + return true; + } + + outPose = track.keyframes.back().pose; + return true; +} + +bool CCameraSequenceScriptUtilities::sequenceSegmentUsesTrackedTargetTrack(const CCameraSequenceSegment& segment) +{ + return !segment.targetKeyframes.empty(); +} + +float CCameraSequenceScriptUtilities::getSequenceSegmentDurationSeconds(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment, const CCameraKeyframeTrack* track) +{ + if (segment.hasDurationSeconds) + return std::max(0.f, segment.durationSeconds); + if (script.defaults.durationSeconds > 0.f) + return script.defaults.durationSeconds; + if (track) + return track->keyframes.empty() ? 0.f : track->keyframes.back().time; + return 0.f; +} + +const std::vector& CCameraSequenceScriptUtilities::getSequenceSegmentPresentations(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + return segment.presentations.empty() ? script.defaults.presentations : segment.presentations; +} + +CCameraSequenceContinuitySettings CCameraSequenceScriptUtilities::getSequenceSegmentContinuity(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + return segment.hasContinuity ? segment.continuity : script.defaults.continuity; +} + +std::vector CCameraSequenceScriptUtilities::getSequenceSegmentCaptureFractions(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + auto captures = segment.hasCaptureFractions ? segment.captureFractions : script.defaults.captureFractions; + normalizeCaptureFractions(captures); + return captures; +} + +bool CCameraSequenceScriptUtilities::getSequenceSegmentResetCamera(const CCameraSequenceScript& script, const CCameraSequenceSegment& segment) +{ + return segment.hasResetCamera ? segment.resetCamera : script.defaults.resetCamera; +} + +bool CCameraSequenceScriptUtilities::sequenceScriptUsesMultiplePresentations(const CCameraSequenceScript& script) +{ + if (script.defaults.presentations.size() > 1u) + return true; + + for (const auto& segment : script.segments) + { + if (getSequenceSegmentPresentations(script, segment).size() > 1u) + return true; + } + + return false; +} + +uint64_t CCameraSequenceScriptUtilities::buildSequenceDurationFrames(const float durationSeconds, const float fps) +{ + const auto safeDuration = std::max(0.f, durationSeconds); + const auto safeFps = std::max(1.f, fps); + return std::max(1ull, static_cast(std::llround(static_cast(safeDuration) * static_cast(safeFps)))); +} + +void CCameraSequenceScriptUtilities::buildSequenceSampleTimes(const float durationSeconds, const uint64_t durationFrames, std::vector& outTimes) +{ + outTimes.clear(); + outTimes.reserve(durationFrames); + + for (uint64_t frameOffset = 0u; frameOffset < durationFrames; ++frameOffset) + { + const float alpha = durationFrames > 1u ? static_cast(frameOffset) / static_cast(durationFrames - 1u) : 0.f; + outTimes.emplace_back(durationSeconds * alpha); + } +} + +void CCameraSequenceScriptUtilities::buildSequenceCaptureFrameOffsets( + const uint64_t durationFrames, + const std::vector& captureFractions, + std::vector& outOffsets) +{ + outOffsets.clear(); + outOffsets.reserve(captureFractions.size()); + + for (const auto fraction : captureFractions) + { + const auto offset = durationFrames > 1u ? + static_cast(std::llround(static_cast(fraction) * static_cast(durationFrames - 1u))) : + 0ull; + outOffsets.emplace_back(offset); + } + + std::sort(outOffsets.begin(), outOffsets.end()); + outOffsets.erase(std::unique(outOffsets.begin(), outOffsets.end()), outOffsets.end()); +} + +bool CCameraSequenceScriptUtilities::compileSequenceSegmentFromReference( + const CCameraSequenceScript& script, + const CCameraSequenceSegment& segment, + const CCameraPreset& referencePreset, + const CCameraSequenceTrackedTargetPose& referenceTrackedTargetPose, + CCameraSequenceCompiledSegment& outSegment, + std::string* error) +{ + outSegment = {}; + outSegment.name = segment.name; + outSegment.presentations = getSequenceSegmentPresentations(script, segment); + outSegment.continuity = getSequenceSegmentContinuity(script, segment); + outSegment.resetCamera = getSequenceSegmentResetCamera(script, segment); + + if (!buildSequenceTrackFromReference(referencePreset, segment, outSegment.track, error)) + return false; + + if (sequenceSegmentUsesTrackedTargetTrack(segment) && + !buildSequenceTrackedTargetTrackFromReference(referenceTrackedTargetPose, segment, outSegment.trackedTargetTrack, error)) + { + return false; + } + + outSegment.durationSeconds = getSequenceSegmentDurationSeconds(script, segment, &outSegment.track); + outSegment.durationFrames = buildSequenceDurationFrames(outSegment.durationSeconds, script.fps); + buildSequenceSampleTimes(outSegment.durationSeconds, outSegment.durationFrames, outSegment.sampleTimes); + buildSequenceCaptureFrameOffsets(outSegment.durationFrames, getSequenceSegmentCaptureFractions(script, segment), outSegment.captureFrameOffsets); + return true; +} + +bool CCameraSequenceScriptUtilities::buildCompiledSegmentFramePolicies( + const CCameraSequenceCompiledSegment& segment, + std::vector& outPolicies, + const bool includeFollowTargetLock) +{ + if (segment.sampleTimes.size() != segment.durationFrames) + return false; + + outPolicies.clear(); + outPolicies.reserve(segment.durationFrames); + + size_t captureIx = 0u; + for (uint64_t frameOffset = 0u; frameOffset < segment.durationFrames; ++frameOffset) + { + CCameraSequenceCompiledFramePolicy policy; + policy.frameOffset = frameOffset; + policy.sampleTime = segment.sampleTimes[frameOffset]; + policy.baseline = segment.continuity.baseline && frameOffset == 0u; + policy.continuityStep = segment.continuity.step && frameOffset > 0u; + policy.followTargetLock = includeFollowTargetLock && segment.usesTrackedTargetTrack() && policy.continuityStep; + + while (captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] < frameOffset) + ++captureIx; + policy.capture = captureIx < segment.captureFrameOffsets.size() && segment.captureFrameOffsets[captureIx] == frameOffset; + if (policy.capture) + ++captureIx; + + outPolicies.emplace_back(std::move(policy)); + } + + return true; +} + +} // namespace nbl::core diff --git a/src/nbl/ext/Cameras/ILinearProjection.cpp b/src/nbl/ext/Cameras/ILinearProjection.cpp new file mode 100644 index 0000000000..8657da95e8 --- /dev/null +++ b/src/nbl/ext/Cameras/ILinearProjection.cpp @@ -0,0 +1,86 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/ILinearProjection.hpp" + +namespace nbl::core +{ + +ILinearProjection::CProjection::CProjection() : CProjection(projection_matrix_t(1)) +{ +} + +ILinearProjection::CProjection::CProjection(const projection_matrix_t& matrix) +{ + setProjectionMatrix(matrix); +} + +void ILinearProjection::CProjection::setProjectionMatrix(const projection_matrix_t& matrix) +{ + m_projectionMatrix = matrix; + const auto det = hlsl::determinant(m_projectionMatrix); + + m_isProjectionSingular = !det; + + if (m_isProjectionSingular) + { + m_isProjectionLeftHanded = std::nullopt; + m_invProjectionMatrix = std::nullopt; + } + else + { + m_isProjectionLeftHanded = det < 0.0; + m_invProjectionMatrix = hlsl::inverse(m_projectionMatrix); + } +} + +bool ILinearProjection::setCamera(core::smart_refctd_ptr&& camera) +{ + if (!camera) + return false; + + m_camera = std::move(camera); + return true; +} + +ICamera* ILinearProjection::getCamera() +{ + return m_camera.get(); +} + +ILinearProjection::concatenated_matrix_t ILinearProjection::getMV(const model_matrix_t& model) const +{ + const auto& view = m_camera->getGimbal().getViewMatrix(); + return hlsl::mul( + hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(view), + hlsl::CCameraMathUtilities::promoteAffine3x4To4x4(model)); +} + +ILinearProjection::concatenated_matrix_t ILinearProjection::getMVP(const CProjection& projection, const model_matrix_t& model) const +{ + return getMVP(projection, getMV(model)); +} + +ILinearProjection::concatenated_matrix_t ILinearProjection::getMVP(const CProjection& projection, const concatenated_matrix_t& mv) const +{ + return hlsl::mul(projection.getProjectionMatrix(), mv); +} + +ILinearProjection::inv_concatenated_matrix_t ILinearProjection::getMVInverse(const model_matrix_t& model) const +{ + const auto mv = getMV(model); + if (const auto det = hlsl::determinant(mv); det) + return hlsl::inverse(mv); + return std::nullopt; +} + +ILinearProjection::inv_concatenated_matrix_t ILinearProjection::getMVPInverse(const CProjection& projection, const model_matrix_t& model) const +{ + const auto mvp = getMVP(projection, model); + if (const auto det = hlsl::determinant(mvp); det) + return hlsl::inverse(mvp); + return std::nullopt; +} + +} // namespace nbl::core diff --git a/src/nbl/ext/Cameras/IPlanarProjection.cpp b/src/nbl/ext/Cameras/IPlanarProjection.cpp new file mode 100644 index 0000000000..47f40df79d --- /dev/null +++ b/src/nbl/ext/Cameras/IPlanarProjection.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/IPlanarProjection.hpp" + +namespace nbl::core +{ + +void IPlanarProjection::CProjection::update(const bool leftHanded, const float aspectRatio) +{ + switch (m_parameters.m_type) + { + case Perspective: + { + const auto& fov = m_parameters.m_planar.perspective.fov; + + if (leftHanded) + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovLH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + else + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixPerspectiveFovRH(hlsl::radians(fov), aspectRatio, m_parameters.m_zNear, m_parameters.m_zFar)); + } break; + + case Orthographic: + { + const auto& orthoW = m_parameters.m_planar.orthographic.orthoWidth; + const auto viewHeight = orthoW / aspectRatio; + + if (leftHanded) + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoLH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + else + base_t::setProjectionMatrix(hlsl::buildProjectionMatrixOrthoRH(orthoW, viewHeight, m_parameters.m_zNear, m_parameters.m_zFar)); + } break; + } +} + +void IPlanarProjection::CProjection::setPerspective(const float zNear, const float zFar, const float fov) +{ + m_parameters.m_type = Perspective; + m_parameters.m_planar.perspective.fov = fov; + m_parameters.m_zNear = zNear; + m_parameters.m_zFar = zFar; +} + +void IPlanarProjection::CProjection::setOrthographic(const float zNear, const float zFar, const float orthoWidth) +{ + m_parameters.m_type = Orthographic; + m_parameters.m_planar.orthographic.orthoWidth = orthoWidth; + m_parameters.m_zNear = zNear; + m_parameters.m_zFar = zFar; +} + +} // namespace nbl::core From e9ac109cc3a32d4036e4941e35521f9916f28530 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 17:36:11 +0200 Subject: [PATCH 149/161] Move gimbal input processing into source files --- .../nbl/ext/Cameras/CGimbalInputBinder.hpp | 84 +------- .../nbl/ext/Cameras/IGimbalInputProcessor.hpp | 155 +-------------- src/nbl/ext/Cameras/CGimbalInputBinder.cpp | 99 ++++++++++ src/nbl/ext/Cameras/IGimbalInputProcessor.cpp | 179 ++++++++++++++++++ 4 files changed, 296 insertions(+), 221 deletions(-) create mode 100644 src/nbl/ext/Cameras/CGimbalInputBinder.cpp create mode 100644 src/nbl/ext/Cameras/IGimbalInputProcessor.cpp diff --git a/include/nbl/ext/Cameras/CGimbalInputBinder.hpp b/include/nbl/ext/Cameras/CGimbalInputBinder.hpp index 785bb02697..935ad5dcf0 100644 --- a/include/nbl/ext/Cameras/CGimbalInputBinder.hpp +++ b/include/nbl/ext/Cameras/CGimbalInputBinder.hpp @@ -29,91 +29,25 @@ class CGimbalInputBinder final : public IGimbalInputProcessor uint32_t mouseCount = 0u; uint32_t imguizmoCount = 0u; - inline uint32_t totalCount() const - { - return keyboardCount + mouseCount + imguizmoCount; - } + uint32_t totalCount() const; }; /// @brief Translate one frame of external keyboard, mouse, and ImGuizmo input into virtual events. - inline void clearActiveBindings() - { - updateKeyboardMapping([](auto& map) { map.clear(); }); - updateMouseMapping([](auto& map) { map.clear(); }); - updateImguizmoMapping([](auto& map) { map.clear(); }); - } + void clearActiveBindings(); - inline void clearBindingLayout() - { - clearActiveBindings(); - } + void clearBindingLayout(); - inline void copyActiveBindingsFromLayout(const IGimbalBindingLayout& layout) - { - updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); - updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); - updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); - } + void copyActiveBindingsFromLayout(const IGimbalBindingLayout& layout); - inline void copyBindingLayoutFrom(const IGimbalBindingLayout& layout) - { - copyActiveBindingsFromLayout(layout); - } + void copyBindingLayoutFrom(const IGimbalBindingLayout& layout); - inline void copyActiveBindingsToLayout(IGimbalBindingLayout& layout) const - { - layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); - layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); - layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); - } + void copyActiveBindingsToLayout(IGimbalBindingLayout& layout) const; - inline void copyBindingLayoutTo(IGimbalBindingLayout& layout) const - { - copyActiveBindingsToLayout(layout); - } + void copyBindingLayoutTo(IGimbalBindingLayout& layout) const; - inline SCollectedVirtualEvents collectVirtualEvents( + SCollectedVirtualEvents collectVirtualEvents( const std::chrono::microseconds nextPresentationTimeStamp, - const SUpdateParameters parameters = {}) - { - beginInputProcessing(nextPresentationTimeStamp); - - SCollectedVirtualEvents output; - uint32_t keyboardPotentialCount = 0u; - uint32_t mousePotentialCount = 0u; - uint32_t imguizmoPotentialCount = 0u; - - processKeyboard(nullptr, keyboardPotentialCount, {}); - processMouse(nullptr, mousePotentialCount, {}); - processImguizmo(nullptr, imguizmoPotentialCount, {}); - - output.events.resize(keyboardPotentialCount + mousePotentialCount + imguizmoPotentialCount); - auto* dst = output.events.data(); - - if (keyboardPotentialCount) - { - output.keyboardCount = keyboardPotentialCount; - processKeyboard(dst, output.keyboardCount, parameters.keyboardEvents); - dst += output.keyboardCount; - } - - if (mousePotentialCount) - { - output.mouseCount = mousePotentialCount; - processMouse(dst, output.mouseCount, parameters.mouseEvents); - dst += output.mouseCount; - } - - if (imguizmoPotentialCount) - { - output.imguizmoCount = imguizmoPotentialCount; - processImguizmo(dst, output.imguizmoCount, parameters.imguizmoEvents); - } - - endInputProcessing(); - output.events.resize(output.totalCount()); - return output; - } + const SUpdateParameters parameters = {}); private: template diff --git a/include/nbl/ext/Cameras/IGimbalInputProcessor.hpp b/include/nbl/ext/Cameras/IGimbalInputProcessor.hpp index 4e6f7bfd4e..f8e5265320 100644 --- a/include/nbl/ext/Cameras/IGimbalInputProcessor.hpp +++ b/include/nbl/ext/Cameras/IGimbalInputProcessor.hpp @@ -4,6 +4,7 @@ #include #include +#include "nbl/builtin/hlsl/tgmath.hlsl" #include "nbl/ui/KeyCodes.h" #include "nbl/ui/SInputEvent.h" @@ -46,16 +47,9 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// @brief ImGuizmo world-space delta transforms consumed by the processor. using input_imguizmo_event_t = hlsl::float32_t4x4; - void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) - { - m_nextPresentationTimeStamp = nextPresentationTimeStamp; - m_frameDeltaSeconds = clampFrameDeltaTimeSeconds(m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp); - } + void beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp); - void endInputProcessing() - { - m_lastVirtualUpTimeStamp = m_nextPresentationTimeStamp; - } + void endInputProcessing(); struct SUpdateParameters { @@ -74,26 +68,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// Pass `nullptr` to query only the total event count. /// @param count Output total number of generated gimbal events. /// @param parameters Individual keyboard, mouse, and ImGuizmo input spans. - void process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters = {}) - { - count = 0u; - uint32_t vKeyboardEventsCount = {}, vMouseEventsCount = {}, vImguizmoEventsCount = {}; - - if (output) - { - processKeyboard(output, vKeyboardEventsCount, parameters.keyboardEvents); output += vKeyboardEventsCount; - processMouse(output, vMouseEventsCount, parameters.mouseEvents); output += vMouseEventsCount; - processImguizmo(output, vImguizmoEventsCount, parameters.imguizmoEvents); - } - else - { - processKeyboard(nullptr, vKeyboardEventsCount, {}); - processMouse(nullptr, vMouseEventsCount, {}); - processImguizmo(nullptr, vImguizmoEventsCount, {}); - } - - count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; - } + void process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters = {}); /// @brief Process keyboard events into virtual manipulation events. /// @@ -105,23 +80,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// Pass `nullptr` to query only the total event count. /// @param count Output number of generated gimbal events. /// @param events Keyboard events to process. - void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) - { - processBindingMap( - m_keyboardVirtualEventMap, - output, - count, - [&](auto& map) - { - for (const auto& keyboardEvent : events) - { - if (keyboardEvent.action == input_keyboard_event_t::ECA_PRESSED) - setBindingActiveState(map, keyboardEvent.keyCode, true); - else if (keyboardEvent.action == input_keyboard_event_t::ECA_RELEASED) - setBindingActiveState(map, keyboardEvent.keyCode, false); - } - }); - } + void processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events); /// @brief Process mouse events into virtual manipulation events. /// @@ -134,48 +93,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// Pass `nullptr` to query only the total event count. /// @param count Output number of generated gimbal events. /// @param events Mouse events to process. - void processMouse(gimbal_event_t* output, uint32_t& count, std::span events) - { - processBindingMap( - m_mouseVirtualEventMap, - output, - count, - [&](auto& map) - { - for (const auto& mouseEvent : events) - { - switch (mouseEvent.type) - { - case input_mouse_event_t::EET_CLICK: - updateMouseButtonState(map, mouseEvent.clickEvent); - break; - - case input_mouse_event_t::EET_SCROLL: - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - hlsl::float32_t2( - static_cast(mouseEvent.scrollEvent.verticalScroll), - mouseEvent.scrollEvent.horizontalScroll), - SInputProcessorBindingGroups::MouseScroll, - map); - break; - - case input_mouse_event_t::EET_MOVEMENT: - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - hlsl::float32_t2( - mouseEvent.movementEvent.relativeMovementX, - mouseEvent.movementEvent.relativeMovementY), - SInputProcessorBindingGroups::MouseRelativeMovement, - map); - break; - - default: - break; - } - } - }); - } + void processMouse(gimbal_event_t* output, uint32_t& count, std::span events); /// @brief Process ImGuizmo transforms into virtual gimbal events. /// @@ -188,43 +106,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage /// Pass `nullptr` to query only the total event count. /// @param count Output number of generated gimbal events. /// @param events ImGuizmo delta transforms to process. - void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) - { - processBindingMap( - m_imguizmoVirtualEventMap, - output, - count, - [&](auto& map) - { - for (const auto& ev : events) - { - const auto& deltaWorldTRS = ev; - - hlsl::SRigidTransformComponents world = {}; - if (!hlsl::CCameraMathUtilities::tryExtractRigidTransformComponents(deltaWorldTRS, world)) - continue; - - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - world.translation, - SInputProcessorBindingGroups::ImguizmoTranslation, - map); - - const auto dRotationRad = hlsl::CCameraMathUtilities::getCameraOrientationEulerRadians(world.orientation); - requestMagnitudeUpdateWithSignedComponents( - ZeroPivot, - dRotationRad, - SInputProcessorBindingGroups::ImguizmoRotation, - map); - - requestMagnitudeUpdateWithSignedComponents( - UnitPivot, - world.scale, - SInputProcessorBindingGroups::ImguizmoScale, - map); - } - }); - } + void processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events); private: template @@ -300,14 +182,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage static double clampFrameDeltaTimeSeconds( const std::chrono::microseconds nextPresentationTimeStamp, - const std::chrono::microseconds lastVirtualUpTimeStamp) - { - const auto deltaSeconds = std::chrono::duration( - nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); - if (deltaSeconds < 0.0) - return 0.0; - return std::min(deltaSeconds, MaxFrameDeltaSeconds); - } + const std::chrono::microseconds lastVirtualUpTimeStamp); template void processBindingMap(Map& map, gimbal_event_t* output, uint32_t& count, ConsumeFn&& consume) @@ -329,19 +204,7 @@ class IGimbalInputProcessor : public CGimbalBindingLayoutStorage static bool tryGetMouseButtonCode( const ui::E_MOUSE_BUTTON button, - ui::E_MOUSE_CODE& outCode) - { - switch (button) - { - case ui::EMB_LEFT_BUTTON: outCode = ui::EMC_LEFT_BUTTON; return true; - case ui::EMB_RIGHT_BUTTON: outCode = ui::EMC_RIGHT_BUTTON; return true; - case ui::EMB_MIDDLE_BUTTON: outCode = ui::EMC_MIDDLE_BUTTON; return true; - case ui::EMB_BUTTON_4: outCode = ui::EMC_BUTTON_4; return true; - case ui::EMB_BUTTON_5: outCode = ui::EMC_BUTTON_5; return true; - default: - return false; - } - } + ui::E_MOUSE_CODE& outCode); template void updateMouseButtonState(Map& map, const input_mouse_event_t::SClickEvent& clickEvent) diff --git a/src/nbl/ext/Cameras/CGimbalInputBinder.cpp b/src/nbl/ext/Cameras/CGimbalInputBinder.cpp new file mode 100644 index 0000000000..89f7e55bde --- /dev/null +++ b/src/nbl/ext/Cameras/CGimbalInputBinder.cpp @@ -0,0 +1,99 @@ +#include "nbl/macros.h" + +#ifdef min +#undef min +#endif +#ifdef max +#undef max +#endif + +#include "nbl/ext/Cameras/CGimbalInputBinder.hpp" + +namespace nbl::ui +{ + +uint32_t CGimbalInputBinder::SCollectedVirtualEvents::totalCount() const +{ + return keyboardCount + mouseCount + imguizmoCount; +} + +void CGimbalInputBinder::clearActiveBindings() +{ + updateKeyboardMapping([](auto& map) { map.clear(); }); + updateMouseMapping([](auto& map) { map.clear(); }); + updateImguizmoMapping([](auto& map) { map.clear(); }); +} + +void CGimbalInputBinder::clearBindingLayout() +{ + clearActiveBindings(); +} + +void CGimbalInputBinder::copyActiveBindingsFromLayout(const IGimbalBindingLayout& layout) +{ + updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(layout.getKeyboardVirtualEventMap()); }); + updateMouseMapping([&](auto& map) { map = sanitizeMapping(layout.getMouseVirtualEventMap()); }); + updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(layout.getImguizmoVirtualEventMap()); }); +} + +void CGimbalInputBinder::copyBindingLayoutFrom(const IGimbalBindingLayout& layout) +{ + copyActiveBindingsFromLayout(layout); +} + +void CGimbalInputBinder::copyActiveBindingsToLayout(IGimbalBindingLayout& layout) const +{ + layout.updateKeyboardMapping([&](auto& map) { map = sanitizeMapping(getKeyboardVirtualEventMap()); }); + layout.updateMouseMapping([&](auto& map) { map = sanitizeMapping(getMouseVirtualEventMap()); }); + layout.updateImguizmoMapping([&](auto& map) { map = sanitizeMapping(getImguizmoVirtualEventMap()); }); +} + +void CGimbalInputBinder::copyBindingLayoutTo(IGimbalBindingLayout& layout) const +{ + copyActiveBindingsToLayout(layout); +} + +CGimbalInputBinder::SCollectedVirtualEvents CGimbalInputBinder::collectVirtualEvents( + const std::chrono::microseconds nextPresentationTimeStamp, + const SUpdateParameters parameters) +{ + beginInputProcessing(nextPresentationTimeStamp); + + SCollectedVirtualEvents output; + uint32_t keyboardPotentialCount = 0u; + uint32_t mousePotentialCount = 0u; + uint32_t imguizmoPotentialCount = 0u; + + processKeyboard(nullptr, keyboardPotentialCount, {}); + processMouse(nullptr, mousePotentialCount, {}); + processImguizmo(nullptr, imguizmoPotentialCount, {}); + + output.events.resize(keyboardPotentialCount + mousePotentialCount + imguizmoPotentialCount); + auto* dst = output.events.data(); + + if (keyboardPotentialCount) + { + output.keyboardCount = keyboardPotentialCount; + processKeyboard(dst, output.keyboardCount, parameters.keyboardEvents); + dst += output.keyboardCount; + } + + if (mousePotentialCount) + { + output.mouseCount = mousePotentialCount; + processMouse(dst, output.mouseCount, parameters.mouseEvents); + dst += output.mouseCount; + } + + if (imguizmoPotentialCount) + { + output.imguizmoCount = imguizmoPotentialCount; + processImguizmo(dst, output.imguizmoCount, parameters.imguizmoEvents); + } + + endInputProcessing(); + output.events.resize(output.totalCount()); + return output; +} + +} // namespace nbl::ui diff --git a/src/nbl/ext/Cameras/IGimbalInputProcessor.cpp b/src/nbl/ext/Cameras/IGimbalInputProcessor.cpp new file mode 100644 index 0000000000..8c4b6df0d3 --- /dev/null +++ b/src/nbl/ext/Cameras/IGimbalInputProcessor.cpp @@ -0,0 +1,179 @@ +#include "nbl/macros.h" + +#ifdef min +#undef min +#endif +#ifdef max +#undef max +#endif + +#include "nbl/ext/Cameras/IGimbalInputProcessor.hpp" + +#include + +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" + +namespace nbl::ui +{ + +void IGimbalInputProcessor::beginInputProcessing(const std::chrono::microseconds nextPresentationTimeStamp) +{ + m_nextPresentationTimeStamp = nextPresentationTimeStamp; + m_frameDeltaSeconds = clampFrameDeltaTimeSeconds(m_nextPresentationTimeStamp, m_lastVirtualUpTimeStamp); +} + +void IGimbalInputProcessor::endInputProcessing() +{ + m_lastVirtualUpTimeStamp = m_nextPresentationTimeStamp; +} + +void IGimbalInputProcessor::process(gimbal_event_t* output, uint32_t& count, const SUpdateParameters parameters) +{ + count = 0u; + uint32_t vKeyboardEventsCount = {}, vMouseEventsCount = {}, vImguizmoEventsCount = {}; + + if (output) + { + processKeyboard(output, vKeyboardEventsCount, parameters.keyboardEvents); + output += vKeyboardEventsCount; + processMouse(output, vMouseEventsCount, parameters.mouseEvents); + output += vMouseEventsCount; + processImguizmo(output, vImguizmoEventsCount, parameters.imguizmoEvents); + } + else + { + processKeyboard(nullptr, vKeyboardEventsCount, {}); + processMouse(nullptr, vMouseEventsCount, {}); + processImguizmo(nullptr, vImguizmoEventsCount, {}); + } + + count = vKeyboardEventsCount + vMouseEventsCount + vImguizmoEventsCount; +} + +void IGimbalInputProcessor::processKeyboard(gimbal_event_t* output, uint32_t& count, std::span events) +{ + processBindingMap( + m_keyboardVirtualEventMap, + output, + count, + [&](auto& map) + { + for (const auto& keyboardEvent : events) + { + if (keyboardEvent.action == input_keyboard_event_t::ECA_PRESSED) + setBindingActiveState(map, keyboardEvent.keyCode, true); + else if (keyboardEvent.action == input_keyboard_event_t::ECA_RELEASED) + setBindingActiveState(map, keyboardEvent.keyCode, false); + } + }); +} + +void IGimbalInputProcessor::processMouse(gimbal_event_t* output, uint32_t& count, std::span events) +{ + processBindingMap( + m_mouseVirtualEventMap, + output, + count, + [&](auto& map) + { + for (const auto& mouseEvent : events) + { + switch (mouseEvent.type) + { + case input_mouse_event_t::EET_CLICK: + updateMouseButtonState(map, mouseEvent.clickEvent); + break; + + case input_mouse_event_t::EET_SCROLL: + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + hlsl::float32_t2( + static_cast(mouseEvent.scrollEvent.verticalScroll), + mouseEvent.scrollEvent.horizontalScroll), + SInputProcessorBindingGroups::MouseScroll, + map); + break; + + case input_mouse_event_t::EET_MOVEMENT: + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + hlsl::float32_t2( + mouseEvent.movementEvent.relativeMovementX, + mouseEvent.movementEvent.relativeMovementY), + SInputProcessorBindingGroups::MouseRelativeMovement, + map); + break; + + default: + break; + } + } + }); +} + +void IGimbalInputProcessor::processImguizmo(gimbal_event_t* output, uint32_t& count, std::span events) +{ + processBindingMap( + m_imguizmoVirtualEventMap, + output, + count, + [&](auto& map) + { + for (const auto& ev : events) + { + const auto& deltaWorldTRS = ev; + + hlsl::SRigidTransformComponents world = {}; + if (!hlsl::CCameraMathUtilities::tryExtractRigidTransformComponents(deltaWorldTRS, world)) + continue; + + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + world.translation, + SInputProcessorBindingGroups::ImguizmoTranslation, + map); + + const auto dRotationRad = hlsl::CCameraMathUtilities::getCameraOrientationEulerRadians(world.orientation); + requestMagnitudeUpdateWithSignedComponents( + ZeroPivot, + dRotationRad, + SInputProcessorBindingGroups::ImguizmoRotation, + map); + + requestMagnitudeUpdateWithSignedComponents( + UnitPivot, + world.scale, + SInputProcessorBindingGroups::ImguizmoScale, + map); + } + }); +} + +double IGimbalInputProcessor::clampFrameDeltaTimeSeconds( + const std::chrono::microseconds nextPresentationTimeStamp, + const std::chrono::microseconds lastVirtualUpTimeStamp) +{ + const auto deltaSeconds = std::chrono::duration( + nextPresentationTimeStamp - lastVirtualUpTimeStamp).count(); + if (deltaSeconds < 0.0) + return 0.0; + return std::min(deltaSeconds, MaxFrameDeltaSeconds); +} + +bool IGimbalInputProcessor::tryGetMouseButtonCode( + const ui::E_MOUSE_BUTTON button, + ui::E_MOUSE_CODE& outCode) +{ + switch (button) + { + case ui::EMB_LEFT_BUTTON: outCode = ui::EMC_LEFT_BUTTON; return true; + case ui::EMB_RIGHT_BUTTON: outCode = ui::EMC_RIGHT_BUTTON; return true; + case ui::EMB_MIDDLE_BUTTON: outCode = ui::EMC_MIDDLE_BUTTON; return true; + case ui::EMB_BUTTON_4: outCode = ui::EMC_BUTTON_4; return true; + case ui::EMB_BUTTON_5: outCode = ui::EMC_BUTTON_5; return true; + default: + return false; + } +} + +} // namespace nbl::ui From f5629c7774e27bad8db4d5eab74417abff632f68 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 18:39:45 +0200 Subject: [PATCH 150/161] Revert shared quaternion cast change --- .../nbl/builtin/hlsl/math/quaternions.hlsl | 138 +----------------- .../nbl/ext/Cameras/CCameraMathUtilities.hpp | 111 ++++++++++++-- src/nbl/ext/Cameras/CCameraMathUtilities.cpp | 45 ------ src/nbl/ext/Cameras/CMakeLists.txt | 16 +- 4 files changed, 108 insertions(+), 202 deletions(-) delete mode 100644 src/nbl/ext/Cameras/CCameraMathUtilities.cpp diff --git a/include/nbl/builtin/hlsl/math/quaternions.hlsl b/include/nbl/builtin/hlsl/math/quaternions.hlsl index 9cd344c109..49a8f95d22 100644 --- a/include/nbl/builtin/hlsl/math/quaternions.hlsl +++ b/include/nbl/builtin/hlsl/math/quaternions.hlsl @@ -380,150 +380,18 @@ struct static_cast_helper, math::quaternion > template struct static_cast_helper, math::quaternion > { - static inline matrix cast(NBL_CONST_REF_ARG(math::quaternion) q) + static inline matrix cast(const math::quaternion q) { return q.__constructMatrix(); } }; -template -inline bool is_finite_quaternion(NBL_CONST_REF_ARG(math::quaternion) q) -{ - return !hlsl::isnan(q.data.x) && - !hlsl::isnan(q.data.y) && - !hlsl::isnan(q.data.z) && - !hlsl::isnan(q.data.w); -} - -template -inline T score_matrix_to_quaternion_cast_candidate( - NBL_CONST_REF_ARG(matrix) target, - NBL_CONST_REF_ARG(math::quaternion) candidate) -{ - if (!is_finite_quaternion(candidate)) - return bit_cast(numeric_limits::infinity); - - const vector rebuiltRight = candidate.transformVector(vector(T(1), T(0), T(0)), true); - const vector rebuiltUp = candidate.transformVector(vector(T(0), T(1), T(0)), true); - const vector rebuiltForward = candidate.transformVector(vector(T(0), T(0), T(1)), true); - return - hlsl::length(rebuiltRight - target[0]) + - hlsl::length(rebuiltUp - target[1]) + - hlsl::length(rebuiltForward - target[2]); -} - -template -inline math::quaternion direct_matrix_to_quaternion_cast(NBL_CONST_REF_ARG(matrix) input) -{ - typedef math::quaternion quaternion_t; - typedef typename quaternion_t::data_type data_type; - - const T xLengthSq = hlsl::dot(input[0], input[0]); - const T yLengthSq = hlsl::dot(input[1], input[1]); - const T zLengthSq = hlsl::dot(input[2], input[2]); - const T uniformScaleSq = (xLengthSq + yLengthSq + zLengthSq) / T(3.0); - if (uniformScaleSq < numeric_limits::min) - { - quaternion_t retval; - retval.data = hlsl::promote(bit_cast(numeric_limits::quiet_NaN)); - return retval; - } - - const T uniformScale = hlsl::sqrt(uniformScaleSq); - matrix m = input; - m /= uniformScale; - - const T m00 = m[0][0]; - const T m11 = m[1][1]; - const T m22 = m[2][2]; - const T neg_m00 = -m00; - const T neg_m11 = -m11; - const T neg_m22 = -m22; - const data_type Qx = data_type(m00, m00, neg_m00, neg_m00); - const data_type Qy = data_type(m11, neg_m11, m11, neg_m11); - const data_type Qz = data_type(m22, neg_m22, neg_m22, m22); - const data_type tmp = Qx + Qy + Qz; - - quaternion_t retval; - if (tmp.x > T(0.0)) - { - const T scales = hlsl::sqrt(tmp.x + T(1.0)); - const T invscales = T(0.5) / scales; - retval.data.x = (m[2][1] - m[1][2]) * invscales; - retval.data.y = (m[0][2] - m[2][0]) * invscales; - retval.data.z = (m[1][0] - m[0][1]) * invscales; - retval.data.w = scales * T(0.5); - } - else if (tmp.y > T(0.0)) - { - const T scales = hlsl::sqrt(tmp.y + T(1.0)); - const T invscales = T(0.5) / scales; - retval.data.x = scales * T(0.5); - retval.data.y = (m[0][1] + m[1][0]) * invscales; - retval.data.z = (m[2][0] + m[0][2]) * invscales; - retval.data.w = (m[2][1] - m[1][2]) * invscales; - } - else if (tmp.z > T(0.0)) - { - const T scales = hlsl::sqrt(tmp.z + T(1.0)); - const T invscales = T(0.5) / scales; - retval.data.x = (m[0][1] + m[1][0]) * invscales; - retval.data.y = scales * T(0.5); - retval.data.z = (m[1][2] + m[2][1]) * invscales; - retval.data.w = (m[0][2] - m[2][0]) * invscales; - } - else - { - const T scales = hlsl::sqrt(tmp.w + T(1.0)); - const T invscales = T(0.5) / scales; - retval.data.x = (m[0][2] + m[2][0]) * invscales; - retval.data.y = (m[1][2] + m[2][1]) * invscales; - retval.data.z = scales * T(0.5); - retval.data.w = (m[1][0] - m[0][1]) * invscales; - } - - retval.data *= uniformScale; - return retval; -} - -template -inline math::quaternion matrix_to_quaternion_cast(NBL_CONST_REF_ARG(matrix) m) -{ - const math::quaternion directCandidate = math::quaternion::create(m, true); - const math::quaternion transposedCandidate = math::quaternion::create(hlsl::transpose(m), true); - const math::quaternion directFallback = direct_matrix_to_quaternion_cast(m); - const math::quaternion transposedFallback = direct_matrix_to_quaternion_cast(hlsl::transpose(m)); - - const T directScore = score_matrix_to_quaternion_cast_candidate(m, directCandidate); - const T transposedScore = score_matrix_to_quaternion_cast_candidate(m, transposedCandidate); - const T directFallbackScore = score_matrix_to_quaternion_cast_candidate(m, directFallback); - const T transposedFallbackScore = score_matrix_to_quaternion_cast_candidate(m, transposedFallback); - - math::quaternion bestCandidate = directCandidate; - T bestScore = directScore; - - if (transposedScore < bestScore) - { - bestCandidate = transposedCandidate; - bestScore = transposedScore; - } - if (directFallbackScore < bestScore) - { - bestCandidate = directFallback; - bestScore = directFallbackScore; - } - if (transposedFallbackScore < bestScore) - bestCandidate = transposedFallback; - - return bestCandidate; -} - template struct static_cast_helper, matrix > { - static inline math::quaternion cast(NBL_CONST_REF_ARG(matrix) m) + static inline math::quaternion cast(const matrix m) { - return matrix_to_quaternion_cast(m); + return math::quaternion::create(m, true); } }; } diff --git a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp index 4e3fbfa112..5c704b41da 100644 --- a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp @@ -331,12 +331,103 @@ struct CCameraMathUtilities final canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); - static_assert(std::is_same_v || std::is_same_v, "Camera basis conversion is only implemented for float and double."); + const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; + const auto desiredRight = canonicalRight; + const auto desiredUp = canonicalUp; + const auto desiredForward = canonicalForward; - if constexpr (std::is_same_v) - return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); - else - return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); + const auto scoreCandidate = [&](const camera_quaternion_t& candidate) + { + if (!isFiniteQuaternion(candidate)) + return std::numeric_limits::infinity(); + + const auto normalizedCandidate = normalizeQuaternion(candidate); + const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); + const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); + const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); + + const T rightError = length(rebuiltRight - desiredRight); + const T upError = length(rebuiltUp - desiredUp); + const T forwardError = length(rebuiltForward - desiredForward); + return rightError + upError + forwardError; + }; + + const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) + { + const T m00 = m[0][0]; + const T m11 = m[1][1]; + const T m22 = m[2][2]; + const T trace = m00 + m11 + m22; + + camera_quaternion_t output = makeIdentityQuaternion(); + if (trace > T(0)) + { + const T scale = hlsl::sqrt(trace + T(1)); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][1] - m[1][2]) * invScale; + output.data.y = (m[0][2] - m[2][0]) * invScale; + output.data.z = (m[1][0] - m[0][1]) * invScale; + output.data.w = scale * T(0.5); + } + else if (m00 >= m11 && m00 >= m22) + { + const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); + const T invScale = T(0.5) / scale; + output.data.x = scale * T(0.5); + output.data.y = (m[0][1] + m[1][0]) * invScale; + output.data.z = (m[2][0] + m[0][2]) * invScale; + output.data.w = (m[2][1] - m[1][2]) * invScale; + } + else if (m11 >= m22) + { + const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); + const T invScale = T(0.5) / scale; + output.data.x = (m[0][1] + m[1][0]) * invScale; + output.data.y = scale * T(0.5); + output.data.z = (m[1][2] + m[2][1]) * invScale; + output.data.w = (m[0][2] - m[2][0]) * invScale; + } + else + { + const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); + const T invScale = T(0.5) / scale; + output.data.x = (m[2][0] + m[0][2]) * invScale; + output.data.y = (m[1][2] + m[2][1]) * invScale; + output.data.z = scale * T(0.5); + output.data.w = (m[1][0] - m[0][1]) * invScale; + } + return normalizeQuaternion(output); + }; + + const camera_matrix_t transposedBasis = hlsl::transpose(basis); + const camera_quaternion_t candidates[] = { + camera_quaternion_t::create(basis, true), + camera_quaternion_t::create(transposedBasis, true), + quaternionFromMatrixFallback(basis), + quaternionFromMatrixFallback(transposedBasis) + }; + + camera_quaternion_t bestCandidate = makeIdentityQuaternion(); + T bestScore = std::numeric_limits::infinity(); + bool foundFiniteCandidate = false; + const auto considerCandidate = [&](const camera_quaternion_t& candidate) + { + const T score = scoreCandidate(candidate); + if (score < bestScore) + { + bestScore = score; + bestCandidate = candidate; + foundFiniteCandidate = true; + } + }; + + for (const auto& candidate : candidates) + considerCandidate(candidate); + + if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) + return makeIdentityQuaternion(); + + return normalizeQuaternion(bestCandidate); } template @@ -900,16 +991,6 @@ struct CCameraMathUtilities final outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); return isFiniteVec3(outRotationEulerDegrees); } - - static camera_quaternion_t makeQuaternionFromBasisImpl( - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward); - - static camera_quaternion_t makeQuaternionFromBasisImpl( - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward); }; } // namespace nbl::hlsl diff --git a/src/nbl/ext/Cameras/CCameraMathUtilities.cpp b/src/nbl/ext/Cameras/CCameraMathUtilities.cpp deleted file mode 100644 index 4c88fb81c4..0000000000 --- a/src/nbl/ext/Cameras/CCameraMathUtilities.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. -// This file is part of the "Nabla Engine". -// For conditions of distribution and use, see copyright notice in nabla.h - -#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" - -namespace nbl::hlsl -{ - -namespace -{ - -template -camera_quaternion_t makeQuaternionFromBasisWithCast( - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) -{ - const camera_matrix_t basis(right, up, forward); - const auto candidate = _static_cast>(basis); - if (!CCameraMathUtilities::isFiniteQuaternion(candidate)) - return CCameraMathUtilities::makeIdentityQuaternion(); - - return CCameraMathUtilities::normalizeQuaternion(candidate); -} - -} // namespace - -camera_quaternion_t CCameraMathUtilities::makeQuaternionFromBasisImpl( - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) -{ - return makeQuaternionFromBasisWithCast(right, up, forward); -} - -camera_quaternion_t CCameraMathUtilities::makeQuaternionFromBasisImpl( - const camera_vector_t& right, - const camera_vector_t& up, - const camera_vector_t& forward) -{ - return makeQuaternionFromBasisWithCast(right, up, forward); -} - -} // namespace nbl::hlsl diff --git a/src/nbl/ext/Cameras/CMakeLists.txt b/src/nbl/ext/Cameras/CMakeLists.txt index 0af31024ec..5e978def69 100644 --- a/src/nbl/ext/Cameras/CMakeLists.txt +++ b/src/nbl/ext/Cameras/CMakeLists.txt @@ -4,20 +4,22 @@ file(GLOB NBL_EXT_CAMERAS_HEADERS CONFIGURE_DEPENDS "${NBL_ROOT_PATH}/include/nbl/ext/Cameras/*.hpp" ) -file(GLOB NBL_EXT_CAMERAS_SOURCES CONFIGURE_DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" +set(NBL_EXT_CAMERAS_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraPersistence.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraSequenceScriptPersistence.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" ) -file(GLOB NBL_EXT_CAMERAS_LOCAL_HEADERS CONFIGURE_DEPENDS - "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp" +set_source_files_properties( + "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" + PROPERTIES + HEADER_FILE_ONLY ON ) -set_source_files_properties(${NBL_EXT_CAMERAS_LOCAL_HEADERS} PROPERTIES HEADER_FILE_ONLY ON) - nbl_create_ext_library_project( Cameras "${NBL_EXT_CAMERAS_HEADERS}" - "${NBL_EXT_CAMERAS_SOURCES};${NBL_EXT_CAMERAS_LOCAL_HEADERS}" + "${NBL_EXT_CAMERAS_SOURCES}" "" "" "" From f83004ae9c4c52bb4ccc2f6398068860a28781ff Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 18:48:32 +0200 Subject: [PATCH 151/161] Restore shared quaternion cast --- .../nbl/builtin/hlsl/math/quaternions.hlsl | 138 +++++++++++++++++- .../nbl/ext/Cameras/CCameraMathUtilities.hpp | 111 ++------------ src/nbl/ext/Cameras/CCameraMathUtilities.cpp | 45 ++++++ src/nbl/ext/Cameras/CMakeLists.txt | 16 +- 4 files changed, 202 insertions(+), 108 deletions(-) create mode 100644 src/nbl/ext/Cameras/CCameraMathUtilities.cpp diff --git a/include/nbl/builtin/hlsl/math/quaternions.hlsl b/include/nbl/builtin/hlsl/math/quaternions.hlsl index 49a8f95d22..9cd344c109 100644 --- a/include/nbl/builtin/hlsl/math/quaternions.hlsl +++ b/include/nbl/builtin/hlsl/math/quaternions.hlsl @@ -380,18 +380,150 @@ struct static_cast_helper, math::quaternion > template struct static_cast_helper, math::quaternion > { - static inline matrix cast(const math::quaternion q) + static inline matrix cast(NBL_CONST_REF_ARG(math::quaternion) q) { return q.__constructMatrix(); } }; +template +inline bool is_finite_quaternion(NBL_CONST_REF_ARG(math::quaternion) q) +{ + return !hlsl::isnan(q.data.x) && + !hlsl::isnan(q.data.y) && + !hlsl::isnan(q.data.z) && + !hlsl::isnan(q.data.w); +} + +template +inline T score_matrix_to_quaternion_cast_candidate( + NBL_CONST_REF_ARG(matrix) target, + NBL_CONST_REF_ARG(math::quaternion) candidate) +{ + if (!is_finite_quaternion(candidate)) + return bit_cast(numeric_limits::infinity); + + const vector rebuiltRight = candidate.transformVector(vector(T(1), T(0), T(0)), true); + const vector rebuiltUp = candidate.transformVector(vector(T(0), T(1), T(0)), true); + const vector rebuiltForward = candidate.transformVector(vector(T(0), T(0), T(1)), true); + return + hlsl::length(rebuiltRight - target[0]) + + hlsl::length(rebuiltUp - target[1]) + + hlsl::length(rebuiltForward - target[2]); +} + +template +inline math::quaternion direct_matrix_to_quaternion_cast(NBL_CONST_REF_ARG(matrix) input) +{ + typedef math::quaternion quaternion_t; + typedef typename quaternion_t::data_type data_type; + + const T xLengthSq = hlsl::dot(input[0], input[0]); + const T yLengthSq = hlsl::dot(input[1], input[1]); + const T zLengthSq = hlsl::dot(input[2], input[2]); + const T uniformScaleSq = (xLengthSq + yLengthSq + zLengthSq) / T(3.0); + if (uniformScaleSq < numeric_limits::min) + { + quaternion_t retval; + retval.data = hlsl::promote(bit_cast(numeric_limits::quiet_NaN)); + return retval; + } + + const T uniformScale = hlsl::sqrt(uniformScaleSq); + matrix m = input; + m /= uniformScale; + + const T m00 = m[0][0]; + const T m11 = m[1][1]; + const T m22 = m[2][2]; + const T neg_m00 = -m00; + const T neg_m11 = -m11; + const T neg_m22 = -m22; + const data_type Qx = data_type(m00, m00, neg_m00, neg_m00); + const data_type Qy = data_type(m11, neg_m11, m11, neg_m11); + const data_type Qz = data_type(m22, neg_m22, neg_m22, m22); + const data_type tmp = Qx + Qy + Qz; + + quaternion_t retval; + if (tmp.x > T(0.0)) + { + const T scales = hlsl::sqrt(tmp.x + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = (m[2][1] - m[1][2]) * invscales; + retval.data.y = (m[0][2] - m[2][0]) * invscales; + retval.data.z = (m[1][0] - m[0][1]) * invscales; + retval.data.w = scales * T(0.5); + } + else if (tmp.y > T(0.0)) + { + const T scales = hlsl::sqrt(tmp.y + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = scales * T(0.5); + retval.data.y = (m[0][1] + m[1][0]) * invscales; + retval.data.z = (m[2][0] + m[0][2]) * invscales; + retval.data.w = (m[2][1] - m[1][2]) * invscales; + } + else if (tmp.z > T(0.0)) + { + const T scales = hlsl::sqrt(tmp.z + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = (m[0][1] + m[1][0]) * invscales; + retval.data.y = scales * T(0.5); + retval.data.z = (m[1][2] + m[2][1]) * invscales; + retval.data.w = (m[0][2] - m[2][0]) * invscales; + } + else + { + const T scales = hlsl::sqrt(tmp.w + T(1.0)); + const T invscales = T(0.5) / scales; + retval.data.x = (m[0][2] + m[2][0]) * invscales; + retval.data.y = (m[1][2] + m[2][1]) * invscales; + retval.data.z = scales * T(0.5); + retval.data.w = (m[1][0] - m[0][1]) * invscales; + } + + retval.data *= uniformScale; + return retval; +} + +template +inline math::quaternion matrix_to_quaternion_cast(NBL_CONST_REF_ARG(matrix) m) +{ + const math::quaternion directCandidate = math::quaternion::create(m, true); + const math::quaternion transposedCandidate = math::quaternion::create(hlsl::transpose(m), true); + const math::quaternion directFallback = direct_matrix_to_quaternion_cast(m); + const math::quaternion transposedFallback = direct_matrix_to_quaternion_cast(hlsl::transpose(m)); + + const T directScore = score_matrix_to_quaternion_cast_candidate(m, directCandidate); + const T transposedScore = score_matrix_to_quaternion_cast_candidate(m, transposedCandidate); + const T directFallbackScore = score_matrix_to_quaternion_cast_candidate(m, directFallback); + const T transposedFallbackScore = score_matrix_to_quaternion_cast_candidate(m, transposedFallback); + + math::quaternion bestCandidate = directCandidate; + T bestScore = directScore; + + if (transposedScore < bestScore) + { + bestCandidate = transposedCandidate; + bestScore = transposedScore; + } + if (directFallbackScore < bestScore) + { + bestCandidate = directFallback; + bestScore = directFallbackScore; + } + if (transposedFallbackScore < bestScore) + bestCandidate = transposedFallback; + + return bestCandidate; +} + template struct static_cast_helper, matrix > { - static inline math::quaternion cast(const matrix m) + static inline math::quaternion cast(NBL_CONST_REF_ARG(matrix) m) { - return math::quaternion::create(m, true); + return matrix_to_quaternion_cast(m); } }; } diff --git a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp index 5c704b41da..4e3fbfa112 100644 --- a/include/nbl/ext/Cameras/CCameraMathUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraMathUtilities.hpp @@ -331,103 +331,12 @@ struct CCameraMathUtilities final canonicalRight = safeNormalizeVec3(cross(canonicalUp, canonicalForward), canonicalRight); canonicalUp = safeNormalizeVec3(cross(canonicalForward, canonicalRight), canonicalUp); - const camera_matrix_t basis { canonicalRight, canonicalUp, canonicalForward }; - const auto desiredRight = canonicalRight; - const auto desiredUp = canonicalUp; - const auto desiredForward = canonicalForward; + static_assert(std::is_same_v || std::is_same_v, "Camera basis conversion is only implemented for float and double."); - const auto scoreCandidate = [&](const camera_quaternion_t& candidate) - { - if (!isFiniteQuaternion(candidate)) - return std::numeric_limits::infinity(); - - const auto normalizedCandidate = normalizeQuaternion(candidate); - const auto rebuiltRight = normalizedCandidate.transformVector(camera_vector_t(T(1), T(0), T(0)), true); - const auto rebuiltUp = normalizedCandidate.transformVector(camera_vector_t(T(0), T(1), T(0)), true); - const auto rebuiltForward = normalizedCandidate.transformVector(camera_vector_t(T(0), T(0), T(1)), true); - - const T rightError = length(rebuiltRight - desiredRight); - const T upError = length(rebuiltUp - desiredUp); - const T forwardError = length(rebuiltForward - desiredForward); - return rightError + upError + forwardError; - }; - - const auto quaternionFromMatrixFallback = [&](const camera_matrix_t& m) - { - const T m00 = m[0][0]; - const T m11 = m[1][1]; - const T m22 = m[2][2]; - const T trace = m00 + m11 + m22; - - camera_quaternion_t output = makeIdentityQuaternion(); - if (trace > T(0)) - { - const T scale = hlsl::sqrt(trace + T(1)); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][1] - m[1][2]) * invScale; - output.data.y = (m[0][2] - m[2][0]) * invScale; - output.data.z = (m[1][0] - m[0][1]) * invScale; - output.data.w = scale * T(0.5); - } - else if (m00 >= m11 && m00 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m00 - m11 - m22); - const T invScale = T(0.5) / scale; - output.data.x = scale * T(0.5); - output.data.y = (m[0][1] + m[1][0]) * invScale; - output.data.z = (m[2][0] + m[0][2]) * invScale; - output.data.w = (m[2][1] - m[1][2]) * invScale; - } - else if (m11 >= m22) - { - const T scale = hlsl::sqrt(T(1) + m11 - m00 - m22); - const T invScale = T(0.5) / scale; - output.data.x = (m[0][1] + m[1][0]) * invScale; - output.data.y = scale * T(0.5); - output.data.z = (m[1][2] + m[2][1]) * invScale; - output.data.w = (m[0][2] - m[2][0]) * invScale; - } - else - { - const T scale = hlsl::sqrt(T(1) + m22 - m00 - m11); - const T invScale = T(0.5) / scale; - output.data.x = (m[2][0] + m[0][2]) * invScale; - output.data.y = (m[1][2] + m[2][1]) * invScale; - output.data.z = scale * T(0.5); - output.data.w = (m[1][0] - m[0][1]) * invScale; - } - return normalizeQuaternion(output); - }; - - const camera_matrix_t transposedBasis = hlsl::transpose(basis); - const camera_quaternion_t candidates[] = { - camera_quaternion_t::create(basis, true), - camera_quaternion_t::create(transposedBasis, true), - quaternionFromMatrixFallback(basis), - quaternionFromMatrixFallback(transposedBasis) - }; - - camera_quaternion_t bestCandidate = makeIdentityQuaternion(); - T bestScore = std::numeric_limits::infinity(); - bool foundFiniteCandidate = false; - const auto considerCandidate = [&](const camera_quaternion_t& candidate) - { - const T score = scoreCandidate(candidate); - if (score < bestScore) - { - bestScore = score; - bestCandidate = candidate; - foundFiniteCandidate = true; - } - }; - - for (const auto& candidate : candidates) - considerCandidate(candidate); - - if (!foundFiniteCandidate || !isFiniteQuaternion(bestCandidate)) - return makeIdentityQuaternion(); - - return normalizeQuaternion(bestCandidate); + if constexpr (std::is_same_v) + return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); + else + return makeQuaternionFromBasisImpl(canonicalRight, canonicalUp, canonicalForward); } template @@ -991,6 +900,16 @@ struct CCameraMathUtilities final outRotationEulerDegrees = getCameraOrientationEulerDegrees(components.orientation); return isFiniteVec3(outRotationEulerDegrees); } + + static camera_quaternion_t makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward); + + static camera_quaternion_t makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward); }; } // namespace nbl::hlsl diff --git a/src/nbl/ext/Cameras/CCameraMathUtilities.cpp b/src/nbl/ext/Cameras/CCameraMathUtilities.cpp new file mode 100644 index 0000000000..4c88fb81c4 --- /dev/null +++ b/src/nbl/ext/Cameras/CCameraMathUtilities.cpp @@ -0,0 +1,45 @@ +// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// This file is part of the "Nabla Engine". +// For conditions of distribution and use, see copyright notice in nabla.h + +#include "nbl/ext/Cameras/CCameraMathUtilities.hpp" + +namespace nbl::hlsl +{ + +namespace +{ + +template +camera_quaternion_t makeQuaternionFromBasisWithCast( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + const camera_matrix_t basis(right, up, forward); + const auto candidate = _static_cast>(basis); + if (!CCameraMathUtilities::isFiniteQuaternion(candidate)) + return CCameraMathUtilities::makeIdentityQuaternion(); + + return CCameraMathUtilities::normalizeQuaternion(candidate); +} + +} // namespace + +camera_quaternion_t CCameraMathUtilities::makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + return makeQuaternionFromBasisWithCast(right, up, forward); +} + +camera_quaternion_t CCameraMathUtilities::makeQuaternionFromBasisImpl( + const camera_vector_t& right, + const camera_vector_t& up, + const camera_vector_t& forward) +{ + return makeQuaternionFromBasisWithCast(right, up, forward); +} + +} // namespace nbl::hlsl diff --git a/src/nbl/ext/Cameras/CMakeLists.txt b/src/nbl/ext/Cameras/CMakeLists.txt index 5e978def69..0af31024ec 100644 --- a/src/nbl/ext/Cameras/CMakeLists.txt +++ b/src/nbl/ext/Cameras/CMakeLists.txt @@ -4,22 +4,20 @@ file(GLOB NBL_EXT_CAMERAS_HEADERS CONFIGURE_DEPENDS "${NBL_ROOT_PATH}/include/nbl/ext/Cameras/*.hpp" ) -set(NBL_EXT_CAMERAS_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraPersistence.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraSequenceScriptPersistence.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" +file(GLOB NBL_EXT_CAMERAS_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" ) -set_source_files_properties( - "${CMAKE_CURRENT_SOURCE_DIR}/CCameraJsonPersistenceUtilities.hpp" - PROPERTIES - HEADER_FILE_ONLY ON +file(GLOB NBL_EXT_CAMERAS_LOCAL_HEADERS CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp" ) +set_source_files_properties(${NBL_EXT_CAMERAS_LOCAL_HEADERS} PROPERTIES HEADER_FILE_ONLY ON) + nbl_create_ext_library_project( Cameras "${NBL_EXT_CAMERAS_HEADERS}" - "${NBL_EXT_CAMERAS_SOURCES}" + "${NBL_EXT_CAMERAS_SOURCES};${NBL_EXT_CAMERAS_LOCAL_HEADERS}" "" "" "" From d825ef2523b7295d4a8d9a638033bf18304442ad Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 18:49:20 +0200 Subject: [PATCH 152/161] Use bitmask helpers for goal solver issues --- include/nbl/ext/Cameras/CCameraGoalSolver.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/nbl/ext/Cameras/CCameraGoalSolver.hpp b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp index b2a0dde179..01ca290ed4 100644 --- a/include/nbl/ext/Cameras/CCameraGoalSolver.hpp +++ b/include/nbl/ext/Cameras/CCameraGoalSolver.hpp @@ -64,11 +64,11 @@ class CCameraGoalSolver enum class EIssue : uint32_t { NoIssue = 0u, - UsedAbsolutePoseFallback = 1u << 0, - MissingSphericalTargetState = 1u << 1, - MissingPathState = 1u << 2, - MissingDynamicPerspectiveState = 1u << 3, - VirtualEventReplayFailed = 1u << 4 + UsedAbsolutePoseFallback = core::createBitmask({ 0 }), + MissingSphericalTargetState = core::createBitmask({ 1 }), + MissingPathState = core::createBitmask({ 2 }), + MissingDynamicPerspectiveState = core::createBitmask({ 3 }), + VirtualEventReplayFailed = core::createBitmask({ 4 }) }; EStatus status = EStatus::Unsupported; From b9be28a018d1392486ab4142bfb1a062141b8f2f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 20:18:59 +0200 Subject: [PATCH 153/161] Update examples_tests submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 4a1326f22f..4e4ef81537 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 4a1326f22ff0fa19b07e9282b34d99f339f028ad +Subproject commit 4e4ef815377ef78b8467c68cfb08c8b976294af1 From a80bb8248961927517e355251d6ebd7b1f345e3f Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 23:14:18 +0200 Subject: [PATCH 154/161] Refresh examples cameraz submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 3b05752d18..e93f304b17 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 3b05752d187a97e2f7e2af067dc67906c3df4220 +Subproject commit e93f304b1720a5e4a77f553402162172494f0c38 From 96e0ccd9157972b8688816c0943337a319764699 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Fri, 10 Apr 2026 23:28:48 +0200 Subject: [PATCH 155/161] Refresh examples cameraz submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index e93f304b17..a85f9103fb 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit e93f304b1720a5e4a77f553402162172494f0c38 +Subproject commit a85f9103fb31c252307c913aa30406d3547956fa From 7130af4b1a007930b1dac1a70c25a5c8a27b3b0d Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 00:18:09 +0200 Subject: [PATCH 156/161] Fix GitHub math in cameras README --- include/nbl/ext/Cameras/README.md | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/include/nbl/ext/Cameras/README.md b/include/nbl/ext/Cameras/README.md index a08722e491..0f1696bf29 100644 --- a/include/nbl/ext/Cameras/README.md +++ b/include/nbl/ext/Cameras/README.md @@ -739,15 +739,9 @@ Defined by [`CPathCamera.hpp`](CPathCamera.hpp), [`CCameraPathUtilities.hpp`](CC Its runtime and typed tooling are driven by `SCameraPathModel`, which defines how path state is resolved, updated, and converted back into camera pose. -At the API boundary, you can think of `Path Rig` as: +At the API boundary, you can think of `Path Rig` as one parametric camera map that turns typed path state into pose. -$$ -\text{choose any parametric camera function } f -\text{ that maps typed path state to pose.} -$$ - -In other words, the reusable seam is not "one built-in rail camera". -It is: +In other words, the reusable seam is not "one built-in rail camera". It is: $$ f : (t, q, L) \mapsto (p, o) @@ -822,7 +816,7 @@ $$ with orientation built from the basis $$ -\bigl(R(s), U(s), F(s)\bigr) +(R(s), U(s), F(s)) $$ and then rotated by authored roll $\rho$ around the current forward axis. @@ -858,8 +852,8 @@ For the built-in model, `resolveState(...)` from one world-space position can be $$ \begin{aligned} \Delta &= p - t \\ -s &= \operatorname{wrap}\!\left(\operatorname{atan2}(\Delta_z, \Delta_x)\right) \\ -u &= \max\!\left(u_{\min}, \sqrt{\Delta_x^2 + \Delta_z^2}\right) \\ +s &= \mathrm{wrap}(\mathrm{atan2}(\Delta_z, \Delta_x)) \\ +u &= \max(u_{\min}, \sqrt{\Delta_x^2 + \Delta_z^2}) \\ v &= \Delta_y \end{aligned} $$ @@ -867,16 +861,16 @@ $$ The default model also derives one radial camera distance from `(u, v)`: $$ -d = \lVert (u, v) \rVert_2 = \sqrt{u^2 + v^2} +d = \sqrt{u^2 + v^2} $$ and sanitizes state as: $$ \begin{aligned} -s &\leftarrow \operatorname{wrap}(s) \\ +s &\leftarrow \mathrm{wrap}(s) \\ u &\leftarrow \max(u_{\min}, u) \\ -\rho &\leftarrow \operatorname{wrap}(\rho) +\rho &\leftarrow \mathrm{wrap}(\rho) \end{aligned} $$ @@ -894,9 +888,9 @@ $$ \end{bmatrix} = \begin{bmatrix} -\Delta z_{\text{local}} \\ -\Delta x_{\text{local}} \\ -\Delta y_{\text{local}} \\ +\Delta z_{\mathrm{local}} \\ +\Delta x_{\mathrm{local}} \\ +\Delta y_{\mathrm{local}} \\ \Delta \mathrm{roll} \end{bmatrix} $$ @@ -904,7 +898,7 @@ $$ and integrates it as: $$ -q_{n+1} = \operatorname{sanitize}(q_n + \Delta q) +q_{n+1} = \mathrm{sanitize}(q_n + \Delta q) $$ Equivalent pseudocode for the built-in model is: From 06b1a4bf6980457abaef4d7d5be416b59b42a812 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 00:21:14 +0200 Subject: [PATCH 157/161] Fix Path Rig math rendering --- include/nbl/ext/Cameras/README.md | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/include/nbl/ext/Cameras/README.md b/include/nbl/ext/Cameras/README.md index 0f1696bf29..e207f84a99 100644 --- a/include/nbl/ext/Cameras/README.md +++ b/include/nbl/ext/Cameras/README.md @@ -879,20 +879,8 @@ The base orientation is then built from the camera looking from the resolved pos The built-in control law maps runtime local motion into path-state delta as: $$ -\Delta q = -\begin{bmatrix} -\Delta s \\ -\Delta u \\ -\Delta v \\ -\Delta \rho -\end{bmatrix} -= -\begin{bmatrix} -\Delta z_{\mathrm{local}} \\ -\Delta x_{\mathrm{local}} \\ -\Delta y_{\mathrm{local}} \\ -\Delta \mathrm{roll} -\end{bmatrix} +\Delta q = (\Delta s, \Delta u, \Delta v, \Delta \rho)^{\mathsf{T}} += (\Delta z_{\mathrm{local}}, \Delta x_{\mathrm{local}}, \Delta y_{\mathrm{local}}, \Delta \mathrm{roll})^{\mathsf{T}} $$ and integrates it as: From 33a7793f86d3f413c194b624ac8b4733bc50a903 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 00:32:32 +0200 Subject: [PATCH 158/161] Refresh examples cameraz submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index a85f9103fb..d999e890df 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit a85f9103fb31c252307c913aa30406d3547956fa +Subproject commit d999e890df43e3681bdc6a956ff5c6f090005e45 From f89e4f3b9c40814f20f69e3a3a0857a9e5c14f1c Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 01:02:31 +0200 Subject: [PATCH 159/161] Refresh examples cameraz submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index d999e890df..1c83dbe3ef 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d999e890df43e3681bdc6a956ff5c6f090005e45 +Subproject commit 1c83dbe3ef7381cce4336fa72342200a3b232bb0 From 8f4cc2f9bcf7d42c82c015fdeec0d08c283e3d50 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 02:43:33 +0200 Subject: [PATCH 160/161] Refresh examples cameraz submodule --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index 1c83dbe3ef..49512f20e5 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 1c83dbe3ef7381cce4336fa72342200a3b232bb0 +Subproject commit 49512f20e55f511188969d5532df89f38adace37 From c92b9b2d3bf955490b740e0de8ce89ec26b450b9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Lachowicz Date: Sat, 11 Apr 2026 19:18:23 +0200 Subject: [PATCH 161/161] Fix camera scaling and view accessors --- examples_tests | 2 +- .../Cameras/CCameraManipulationUtilities.hpp | 7 ++++++- include/nbl/ext/Cameras/CFPSCamera.hpp | 3 ++- include/nbl/ext/Cameras/CFreeLockCamera.hpp | 10 ++++++---- include/nbl/ext/Cameras/ICamera.hpp | 17 ++++++++++++++--- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/examples_tests b/examples_tests index 49512f20e5..d3c2f73c70 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 49512f20e55f511188969d5532df89f38adace37 +Subproject commit d3c2f73c70f71bcfefe54d6251614857c8f9ddbe diff --git a/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp b/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp index 6e8adbaf06..6d60e93081 100644 --- a/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp +++ b/include/nbl/ext/Cameras/CCameraManipulationUtilities.hpp @@ -66,7 +66,12 @@ struct CCameraManipulationUtilities final return; } - CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents(filtered, camera->getGimbal().getOrientation(), worldDelta); + const auto scaledTranslationMagnitude = camera->getScaledVirtualTranslationMagnitude(); + CCameraVirtualEventUtilities::appendWorldTranslationAsLocalEvents( + filtered, + camera->getGimbal().getOrientation(), + worldDelta, + hlsl::float64_t3(scaledTranslationMagnitude)); events = std::move(filtered); count = static_cast(events.size()); diff --git a/include/nbl/ext/Cameras/CFPSCamera.hpp b/include/nbl/ext/Cameras/CFPSCamera.hpp index 6e0b34d8d4..8e0735ad52 100644 --- a/include/nbl/ext/Cameras/CFPSCamera.hpp +++ b/include/nbl/ext/Cameras/CFPSCamera.hpp @@ -77,13 +77,14 @@ class CFPSCamera final : public ICamera m_gimbal.begin(); { + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); const auto pitchYaw = hlsl::CCameraMathUtilities::getPitchYawFromForwardVector(hlsl::float64_t3(reference.frame[2])); const float newPitch = std::clamp(static_cast(pitchYaw.x + scaleVirtualRotation(impulse.dVirtualRotation.x)), MinVerticalAngle, MaxVerticalAngle); const float newYaw = static_cast(pitchYaw.y + scaleVirtualRotation(impulse.dVirtualRotation.y)); if (validateReference()) m_gimbal.setOrientation(hlsl::CCameraMathUtilities::makeQuaternionFromEulerRadiansYXZ(hlsl::float64_t3(newPitch, newYaw, 0.0f))); - m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(deltaTranslation))); } m_gimbal.end(); diff --git a/include/nbl/ext/Cameras/CFreeLockCamera.hpp b/include/nbl/ext/Cameras/CFreeLockCamera.hpp index b932290430..40a4968c8c 100644 --- a/include/nbl/ext/Cameras/CFreeLockCamera.hpp +++ b/include/nbl/ext/Cameras/CFreeLockCamera.hpp @@ -40,12 +40,14 @@ class CFreeCamera final : public ICamera m_gimbal.begin(); { - const auto pitch = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[0])), impulse.dVirtualRotation.x); - const auto yaw = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[1])), impulse.dVirtualRotation.y); - const auto roll = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[2])), impulse.dVirtualRotation.z); + const auto deltaRotation = scaleVirtualRotation(impulse.dVirtualRotation); + const auto deltaTranslation = scaleVirtualTranslation(impulse.dVirtualTranslate); + const auto pitch = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[0])), deltaRotation.x); + const auto yaw = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[1])), deltaRotation.y); + const auto roll = hlsl::CCameraMathUtilities::makeQuaternionFromAxisAngle(hlsl::normalize(hlsl::float64_t3(reference.frame[2])), deltaRotation.z); m_gimbal.setOrientation(hlsl::CCameraMathUtilities::normalizeQuaternion(yaw * pitch * roll * reference.orientation)); - m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(impulse.dVirtualTranslate))); + m_gimbal.setPosition(hlsl::float64_t3(reference.frame[3]) + hlsl::CCameraMathUtilities::rotateVectorByQuaternion(reference.orientation, hlsl::float64_t3(deltaTranslation))); } m_gimbal.end(); diff --git a/include/nbl/ext/Cameras/ICamera.hpp b/include/nbl/ext/Cameras/ICamera.hpp index 957dd3445d..d987e74afa 100644 --- a/include/nbl/ext/Cameras/ICamera.hpp +++ b/include/nbl/ext/Cameras/ICamera.hpp @@ -231,7 +231,7 @@ class ICamera : virtual public core::IReferenceCounted return base_t::template accumulate(virtualEvents); } - /// @brief Rebuild the cached world-to-view matrix from the current gimbal pose. + /// @brief Rebuild the cached left-handed world-to-view matrix from the current gimbal pose. inline void updateView() { const auto& gRight = this->getXAxis(); @@ -247,8 +247,19 @@ class ICamera : virtual public core::IReferenceCounted m_viewMatrix[2u] = hlsl::float64_t4(gForward, -hlsl::dot(gForward, position)); } - /// @brief Return the cached world-to-view matrix derived from the current pose. - inline const hlsl::float64_t3x4& getViewMatrix() const { return m_viewMatrix; } + /// @brief Return the cached left-handed world-to-view matrix derived from the current pose. + inline const hlsl::float64_t3x4& getViewMatrix() const { return getViewMatrixLH(); } + + /// @brief Return the cached left-handed world-to-view matrix derived from the current pose. + inline const hlsl::float64_t3x4& getViewMatrixLH() const { return m_viewMatrix; } + + /// @brief Return the right-handed world-to-view matrix derived from the current pose. + inline hlsl::float64_t3x4 getViewMatrixRH() const + { + auto rhViewMatrix = m_viewMatrix; + rhViewMatrix[2u] *= -1.0; + return rhViewMatrix; + } private: hlsl::float64_t3x4 m_viewMatrix;