Compare commits

..

22 Commits

Author SHA1 Message Date
d8188de736 keeping 1 AABB type 2026-03-28 14:22:36 +03:00
33cd3f64e4 Merge pull request #180 from orange-cpp/feature/aabb-linetrace
added aabb line trace
2026-03-25 03:37:35 +03:00
67a07eed45 added aabb line trace 2026-03-25 03:14:22 +03:00
0b52b2847b Merge pull request #179 from orange-cpp/feature/aabb_check
added AABB check
2026-03-24 10:45:00 +03:00
d38895e4d7 added AABB check 2026-03-24 10:20:50 +03:00
04203d46ff patch 2026-03-24 06:44:10 +03:00
bcbb5c1a8d fixed index 2026-03-24 06:05:56 +03:00
ba46c86664 simplified method 2026-03-24 06:03:35 +03:00
3b0470cc11 Merge pull request #178 from orange-cpp/feature/imrovements
Feature/imrovements
2026-03-24 05:55:47 +03:00
8562c5d1f2 added more unreachable checks 2026-03-24 05:28:01 +03:00
8daba25c29 added ureachable 2026-03-24 05:21:00 +03:00
29b7ac6450 Merge pull request #177 from orange-cpp/feature/custom_ndc_z_range
Feature/custom ndc z range
2026-03-24 04:20:57 +03:00
89df10b778 specifeid ndc for game engines 2026-03-24 00:08:06 +03:00
8fb96b83db removed dead code 2026-03-23 23:52:41 +03:00
4b6db0c402 updated z range 2026-03-23 23:36:19 +03:00
a9ff7868cf simplified code 2026-03-23 05:52:35 +03:00
be80a5d243 added as_vector3 to view angles 2026-03-23 05:23:53 +03:00
881d3b9a2a added fields 2026-03-22 19:07:38 +03:00
f60e18b6ba replaced with table offset 2026-03-22 18:58:07 +03:00
0769d3d079 replaced with auto 2026-03-22 17:30:25 +03:00
b6755e21f9 fix 2026-03-22 16:32:00 +03:00
2287602fa2 Merge pull request #176 from orange-cpp/feature/vtable_index
added stuff
2026-03-22 16:21:39 +03:00
57 changed files with 1592 additions and 148 deletions

View File

@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(ls:*)"
]
}
}

10
.idea/editor.xml generated
View File

@@ -17,7 +17,7 @@
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppBoostFormatTooManyArgs/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppCStyleCast/@EntryIndexedValue" value="SUGGESTION" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppCVQualifierCanNotBeAppliedToReference/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassCanBeFinal/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassCanBeFinal/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassIsIncomplete/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassNeedsConstructorBecauseOfUninitializedMember/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppClassNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
@@ -103,14 +103,14 @@
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppImplicitDefaultConstructorNotAvailable/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIncompatiblePointerConversion/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIncompleteSwitchStatement/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppInconsistentNaming/@EntryIndexedValue" value="HINT" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppInconsistentNaming/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppIntegralToPointerConversion/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppInvalidLineContinuation/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppJoinDeclarationAndAssignment/@EntryIndexedValue" value="SUGGESTION" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLambdaCaptureNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableMayBeConst/@EntryIndexedValue" value="HINT" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableMightNotBeInitialized/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableWithNonTrivialDtorIsNeverUsed/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLocalVariableWithNonTrivialDtorIsNeverUsed/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppLongFloat/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMemberFunctionMayBeConst/@EntryIndexedValue" value="SUGGESTION" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppMemberFunctionMayBeStatic/@EntryIndexedValue" value="SUGGESTION" type="string" />
@@ -202,7 +202,7 @@
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStaticDataMemberInUnnamedStruct/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStaticSpecifierOnAnonymousNamespaceMember/@EntryIndexedValue" value="SUGGESTION" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppStringLiteralToCharPointerConversion/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTabsAreDisallowed/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTabsAreDisallowed/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateArgumentsCanBeDeduced/@EntryIndexedValue" value="HINT" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateParameterNeverUsed/@EntryIndexedValue" value="HINT" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppTemplateParameterShadowing/@EntryIndexedValue" value="WARNING" type="string" />
@@ -216,7 +216,7 @@
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnmatchedPragmaEndRegionDirective/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnmatchedPragmaRegionDirective/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnamedNamespaceInHeaderFile/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnecessaryWhitespace/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnnecessaryWhitespace/@EntryIndexedValue" value="DO_NOT_SHOW" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnsignedZeroComparison/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUnusedIncludeDirective/@EntryIndexedValue" value="WARNING" type="string" />
<option name="/Default/CodeInspection/Highlighting/InspectionSeverities/=CppUseAlgorithmWithCount/@EntryIndexedValue" value="SUGGESTION" type="string" />

View File

@@ -0,0 +1,28 @@
//
// Created by Vladislav on 24.03.2026.
//
#pragma once
#include "omath/linear_algebra/vector3.hpp"
namespace omath::primitives
{
template<class Type>
struct Aabb final
{
Vector3<Type> min;
Vector3<Type> max;
[[nodiscard]]
constexpr Vector3<Type> center() const noexcept
{
return (min + max) / static_cast<Type>(2);
}
[[nodiscard]]
constexpr Vector3<Type> extents() const noexcept
{
return (max - min) / static_cast<Type>(2);
}
};
} // namespace omath::primitives

View File

@@ -35,13 +35,8 @@ namespace omath::algorithm
const auto current_target_angles = camera.calc_look_at_angles(get_position(*current));
const auto best_target_angles = camera.calc_look_at_angles(get_position(*best_target));
const Vector2<float> current_angles_vec = {current_target_angles.pitch.as_degrees(),
current_target_angles.yaw.as_degrees()};
const Vector2<float> best_angles_vec = {best_target_angles.pitch.as_degrees(),
best_target_angles.yaw.as_degrees()};
const auto current_target_distance = camera_angles_vec.distance_to(current_angles_vec);
const auto best_target_distance = camera_angles_vec.distance_to(best_angles_vec);
const auto current_target_distance = camera_angles_vec.distance_to(current_target_angles.as_vector3());
const auto best_target_distance = camera_angles.as_vector3().distance_to(best_target_angles.as_vector3());
if (current_target_distance < best_target_distance)
best_target = current;
}

View File

@@ -3,6 +3,7 @@
//
#pragma once
#include "omath/3d_primitives/aabb.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
@@ -34,6 +35,7 @@ namespace omath::collision
class LineTracer final
{
using TriangleType = Triangle<typename RayType::VectorType>;
using AABBType = primitives::Aabb<typename RayType::VectorType::ContainedType>;
public:
LineTracer() = delete;
@@ -87,6 +89,54 @@ namespace omath::collision
return ray.start + ray_dir * t_hit;
}
// Slab method ray-AABB intersection
// Returns the hit point on the AABB surface, or ray.end if no intersection
[[nodiscard]]
constexpr static auto get_ray_hit_point(const RayType& ray, const AABBType& aabb) noexcept
{
using T = typename RayType::VectorType::ContainedType;
const auto dir = ray.direction_vector();
auto t_min = -std::numeric_limits<T>::infinity();
auto t_max = std::numeric_limits<T>::infinity();
const auto process_axis = [&](const T& d, const T& origin, const T& box_min,
const T& box_max) -> bool
{
constexpr T k_epsilon = std::numeric_limits<T>::epsilon();
if (std::abs(d) < k_epsilon)
return origin >= box_min && origin <= box_max;
const T inv = T(1) / d;
T t0 = (box_min - origin) * inv;
T t1 = (box_max - origin) * inv;
if (t0 > t1)
std::swap(t0, t1);
t_min = std::max(t_min, t0);
t_max = std::min(t_max, t1);
return t_min <= t_max;
};
if (!process_axis(dir.x, ray.start.x, aabb.min.x, aabb.max.x))
return ray.end;
if (!process_axis(dir.y, ray.start.y, aabb.min.y, aabb.max.y))
return ray.end;
if (!process_axis(dir.z, ray.start.z, aabb.min.z, aabb.max.z))
return ray.end;
// t_hit: use entry point if in front of origin, otherwise 0 (started inside)
const T t_hit = std::max(T(0), t_min);
if (t_max < T(0))
return ray.end; // box entirely behind origin
if (!ray.infinite_length && t_hit > T(1))
return ray.end; // box beyond ray endpoint
return ray.start + dir * t_hit;
}
template<class MeshType>
[[nodiscard]]
constexpr static auto get_ray_hit_point(const RayType& ray, const MeshType& mesh) noexcept

View File

@@ -9,5 +9,5 @@
namespace omath::cry_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::cry_engine

View File

@@ -22,7 +22,8 @@ namespace omath::cry_engine
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>

View File

@@ -18,7 +18,7 @@ namespace omath::cry_engine
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
float near, float far, NDCDepthRange ndc_depth_range) noexcept;
};
} // namespace omath::cry_engine

View File

@@ -9,5 +9,5 @@
namespace omath::frostbite_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::unity_engine

View File

@@ -22,7 +22,8 @@ namespace omath::frostbite_engine
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>

View File

@@ -18,7 +18,7 @@ namespace omath::frostbite_engine
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
float near, float far, NDCDepthRange ndc_depth_range) noexcept;
};
} // namespace omath::unreal_engine

View File

@@ -9,5 +9,5 @@
namespace omath::iw_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::iw_engine

View File

@@ -22,7 +22,8 @@ namespace omath::iw_engine
[[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>

View File

@@ -18,7 +18,7 @@ namespace omath::iw_engine
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
float near, float far, NDCDepthRange ndc_depth_range) noexcept;
};
} // namespace omath::iw_engine

View File

@@ -8,5 +8,5 @@
namespace omath::opengl_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, true>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, true, NDCDepthRange::NEGATIVE_ONE_TO_ONE>;
} // namespace omath::opengl_engine

View File

@@ -21,7 +21,8 @@ namespace omath::opengl_engine
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>

View File

@@ -18,7 +18,7 @@ namespace omath::opengl_engine
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
float near, float far, NDCDepthRange ndc_depth_range) noexcept;
};
} // namespace omath::opengl_engine

View File

@@ -7,5 +7,5 @@
#include "traits/camera_trait.hpp"
namespace omath::source_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::source_engine

View File

@@ -21,7 +21,8 @@ namespace omath::source_engine
[[nodiscard]] Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>

View File

@@ -18,7 +18,7 @@ namespace omath::source_engine
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
float near, float far, NDCDepthRange ndc_depth_range) noexcept;
};
} // namespace omath::source_engine

View File

@@ -9,5 +9,5 @@
namespace omath::unity_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::unity_engine

View File

@@ -22,7 +22,8 @@ namespace omath::unity_engine
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>

View File

@@ -18,7 +18,7 @@ namespace omath::unity_engine
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
float near, float far, NDCDepthRange ndc_depth_range) noexcept;
};
} // namespace omath::unity_engine

View File

@@ -9,5 +9,5 @@
namespace omath::unreal_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, false, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::unreal_engine

View File

@@ -22,7 +22,8 @@ namespace omath::unreal_engine
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept;
[[nodiscard]]
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far) noexcept;
Mat4X4 calc_perspective_projection_matrix(float field_of_view, float aspect_ratio, float near, float far,
NDCDepthRange ndc_depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE) noexcept;
template<class FloatingType>
requires std::is_floating_point_v<FloatingType>

View File

@@ -18,7 +18,7 @@ namespace omath::unreal_engine
static Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept;
[[nodiscard]]
static Mat4X4 calc_projection_matrix(const projection::FieldOfView& fov, const projection::ViewPort& view_port,
float near, float far) noexcept;
float near, float far, NDCDepthRange ndc_depth_range) noexcept;
};
} // namespace omath::unreal_engine

View File

@@ -37,6 +37,12 @@ namespace omath
COLUMN_MAJOR
};
enum class NDCDepthRange : uint8_t
{
NEGATIVE_ONE_TO_ONE = 0, // OpenGL: [-1.0, 1.0]
ZERO_TO_ONE // DirectX / Vulkan: [0.0, 1.0]
};
template<typename M1, typename M2> concept MatTemplateEqual
= (M1::rows == M2::rows) && (M1::columns == M2::columns)
&& std::is_same_v<typename M1::value_type, typename M2::value_type> && (M1::store_type == M2::store_type);
@@ -658,56 +664,98 @@ namespace omath
} * mat_translation<Type, St>(-camera_origin);
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Mat<4, 4, Type, St> mat_perspective_left_handed(const float field_of_view, const float aspect_ratio,
const float near, const float far) noexcept
{
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f);
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f},
{0.f, 1.f / fov_half_tan, 0.f, 0.f},
{0.f, 0.f, (far + near) / (far - near), -(2.f * near * far) / (far - near)},
{0.f, 0.f, 1.f, 0.f}};
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f},
{0.f, 1.f / fov_half_tan, 0.f, 0.f},
{0.f, 0.f, far / (far - near), -(near * far) / (far - near)},
{0.f, 0.f, 1.f, 0.f}};
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f},
{0.f, 1.f / fov_half_tan, 0.f, 0.f},
{0.f, 0.f, (far + near) / (far - near), -(2.f * near * far) / (far - near)},
{0.f, 0.f, 1.f, 0.f}};
else
std::unreachable();
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Mat<4, 4, Type, St> mat_perspective_right_handed(const float field_of_view, const float aspect_ratio,
const float near, const float far) noexcept
{
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f);
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f},
{0.f, 1.f / fov_half_tan, 0.f, 0.f},
{0.f, 0.f, -(far + near) / (far - near), -(2.f * near * far) / (far - near)},
{0.f, 0.f, -1.f, 0.f}};
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f},
{0.f, 1.f / fov_half_tan, 0.f, 0.f},
{0.f, 0.f, -far / (far - near), -(near * far) / (far - near)},
{0.f, 0.f, -1.f, 0.f}};
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {{1.f / (aspect_ratio * fov_half_tan), 0.f, 0.f, 0.f},
{0.f, 1.f / fov_half_tan, 0.f, 0.f},
{0.f, 0.f, -(far + near) / (far - near), -(2.f * near * far) / (far - near)},
{0.f, 0.f, -1.f, 0.f}};
else
std::unreachable();
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Mat<4, 4, Type, St> mat_ortho_left_handed(const Type left, const Type right, const Type bottom, const Type top,
const Type near, const Type far) noexcept
{
return
{
{ static_cast<Type>(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)},
{ 0.f, static_cast<Type>(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)},
{ 0.f, 0.f, static_cast<Type>(2) / (far - near), -(far + near) / (far - near) },
{ 0.f, 0.f, 0.f, 1.f }
};
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return
{
{ static_cast<Type>(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)},
{ 0.f, static_cast<Type>(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)},
{ 0.f, 0.f, static_cast<Type>(1) / (far - near), -near / (far - near) },
{ 0.f, 0.f, 0.f, 1.f }
};
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return
{
{ static_cast<Type>(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)},
{ 0.f, static_cast<Type>(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)},
{ 0.f, 0.f, static_cast<Type>(2) / (far - near), -(far + near) / (far - near) },
{ 0.f, 0.f, 0.f, 1.f }
};
else
std::unreachable();
}
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR>
template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Mat<4, 4, Type, St> mat_ortho_right_handed(const Type left, const Type right, const Type bottom, const Type top,
const Type near, const Type far) noexcept
{
return
{
{ static_cast<Type>(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)},
{ 0.f, static_cast<Type>(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)},
{ 0.f, 0.f, -static_cast<Type>(2) / (far - near), -(far + near) / (far - near) },
{ 0.f, 0.f, 0.f, 1.f }
};
if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return
{
{ static_cast<Type>(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)},
{ 0.f, static_cast<Type>(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)},
{ 0.f, 0.f, -static_cast<Type>(1) / (far - near), -near / (far - near) },
{ 0.f, 0.f, 0.f, 1.f }
};
else if constexpr (DepthRange == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return
{
{ static_cast<Type>(2) / (right - left), 0.f, 0.f, -(right + left) / (right - left)},
{ 0.f, static_cast<Type>(2) / (top - bottom), 0.f, -(top + bottom) / (top - bottom)},
{ 0.f, 0.f, -static_cast<Type>(2) / (far - near), -(far + near) / (far - near) },
{ 0.f, 0.f, 0.f, 1.f }
};
else
std::unreachable();
}
template<class T = float, MatStoreType St = MatStoreType::COLUMN_MAJOR>
Mat<4, 4, T, St> mat_look_at_left_handed(const Vector3<T>& eye, const Vector3<T>& center, const Vector3<T>& up)

View File

@@ -4,6 +4,7 @@
#pragma once
#include "omath/3d_primitives/aabb.hpp"
#include "omath/linear_algebra/mat.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
@@ -44,19 +45,21 @@ namespace omath::projection
template<class T, class MatType, class ViewAnglesType>
concept CameraEngineConcept =
requires(const Vector3<float>& cam_origin, const Vector3<float>& look_at, const ViewAnglesType& angles,
const FieldOfView& fov, const ViewPort& viewport, float znear, float zfar) {
const FieldOfView& fov, const ViewPort& viewport, float znear, float zfar,
NDCDepthRange ndc_depth_range) {
// Presence + return types
{ T::calc_look_at_angle(cam_origin, look_at) } -> std::same_as<ViewAnglesType>;
{ T::calc_view_matrix(angles, cam_origin) } -> std::same_as<MatType>;
{ T::calc_projection_matrix(fov, viewport, znear, zfar) } -> std::same_as<MatType>;
{ T::calc_projection_matrix(fov, viewport, znear, zfar, ndc_depth_range) } -> std::same_as<MatType>;
// Enforce noexcept as in the trait declaration
requires noexcept(T::calc_look_at_angle(cam_origin, look_at));
requires noexcept(T::calc_view_matrix(angles, cam_origin));
requires noexcept(T::calc_projection_matrix(fov, viewport, znear, zfar));
requires noexcept(T::calc_projection_matrix(fov, viewport, znear, zfar, ndc_depth_range));
};
template<class Mat4X4Type, class ViewAnglesType, class TraitClass, bool inverted_z = false>
template<class Mat4X4Type, class ViewAnglesType, class TraitClass, bool inverted_z = false,
NDCDepthRange depth_range = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
requires CameraEngineConcept<TraitClass, Mat4X4Type, ViewAnglesType>
class Camera final
{
@@ -135,7 +138,8 @@ namespace omath::projection
{
if (!m_projection_matrix.has_value())
m_projection_matrix = TraitClass::calc_projection_matrix(m_field_of_view, m_view_port,
m_near_plane_distance, m_far_plane_distance);
m_near_plane_distance, m_far_plane_distance,
depth_range);
return m_projection_matrix.value();
}
@@ -271,18 +275,94 @@ namespace omath::projection
return a[axis] < -a[3] && b[axis] < -b[3] && c[axis] < -c[3];
};
// Clip volume in clip space (OpenGL-style):
// Clip volume in clip space:
// -w <= x <= w
// -w <= y <= w
// -w <= z <= w
// z_min <= z <= w (z_min = -w for [-1,1], 0 for [0,1])
for (int i = 0; i < 3; i++)
// x and y planes
for (int i = 0; i < 2; i++)
{
if (all_outside_plane(i, c0, c1, c2, false))
return true; // x < -w (left)
return true;
if (all_outside_plane(i, c0, c1, c2, true))
return true; // x > w (right)
return true;
}
// z far plane: z > w
if (all_outside_plane(2, c0, c1, c2, true))
return true;
// z near plane
if constexpr (depth_range == NDCDepthRange::ZERO_TO_ONE)
{
// 0 <= z, so reject if z < 0 for all vertices
if (c0[2] < 0.f && c1[2] < 0.f && c2[2] < 0.f)
return true;
}
else
{
// -w <= z
if (all_outside_plane(2, c0, c1, c2, false))
return true;
}
return false;
}
[[nodiscard]] bool is_aabb_culled_by_frustum(const primitives::Aabb<float>& aabb) const noexcept
{
const auto& m = get_view_projection_matrix();
// Gribb-Hartmann: extract 6 frustum planes from the view-projection matrix.
// Each plane is (a, b, c, d) such that ax + by + cz + d >= 0 means inside.
// For a 4x4 matrix with rows r0..r3:
// Left = r3 + r0
// Right = r3 - r0
// Bottom = r3 + r1
// Top = r3 - r1
// Near = r3 + r2 ([-1,1]) or r2 ([0,1])
// Far = r3 - r2
struct Plane final
{
float a, b, c, d;
};
const auto extract_plane = [&m](const int sign, const int row) -> Plane
{
return {
m.at(3, 0) + static_cast<float>(sign) * m.at(row, 0),
m.at(3, 1) + static_cast<float>(sign) * m.at(row, 1),
m.at(3, 2) + static_cast<float>(sign) * m.at(row, 2),
m.at(3, 3) + static_cast<float>(sign) * m.at(row, 3),
};
};
std::array<Plane, 6> planes = {
extract_plane(1, 0), // left
extract_plane(-1, 0), // right
extract_plane(1, 1), // bottom
extract_plane(-1, 1), // top
extract_plane(-1, 2), // far
};
// Near plane depends on NDC depth range
if constexpr (depth_range == NDCDepthRange::ZERO_TO_ONE)
planes[5] = {m.at(2, 0), m.at(2, 1), m.at(2, 2), m.at(2, 3)};
else
planes[5] = extract_plane(1, 2);
// For each plane, find the AABB corner most in the direction of the plane normal
// (the "positive vertex"). If it's outside, the entire AABB is outside.
for (const auto& [a, b, c, d] : planes)
{
const float px = a >= 0.f ? aabb.max.x : aabb.min.x;
const float py = b >= 0.f ? aabb.max.y : aabb.min.y;
const float pz = c >= 0.f ? aabb.max.z : aabb.min.z;
if (a * px + b * py + c * pz + d < 0.f)
return true;
}
return false;
}
@@ -306,7 +386,8 @@ namespace omath::projection
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
// ReSharper disable once CppTooWideScope
const auto clipped_manually = clipping == ViewPortClipping::MANUAL && (projected.at(2, 0) < 0.0f - eps
constexpr auto z_min = depth_range == NDCDepthRange::ZERO_TO_ONE ? 0.0f : -1.0f;
const auto clipped_manually = clipping == ViewPortClipping::MANUAL && (projected.at(2, 0) < z_min - eps
|| projected.at(2, 0) > 1.0f + eps);
if (clipped_manually)
return std::unexpected(Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS);
@@ -368,8 +449,26 @@ namespace omath::projection
[[nodiscard]] constexpr static bool is_ndc_out_of_bounds(const Type& ndc) noexcept
{
constexpr auto eps = std::numeric_limits<float>::epsilon();
return std::ranges::any_of(ndc.raw_array(),
[](const auto& val) { return val < -1.0f - eps || val > 1.0f + eps; });
const auto& data = ndc.raw_array();
// x and y are always in [-1, 1]
if (data[0] < -1.0f - eps || data[0] > 1.0f + eps)
return true;
if (data[1] < -1.0f - eps || data[1] > 1.0f + eps)
return true;
return is_ndc_z_value_out_of_bounds(data[2]);
}
template<class ZType>
[[nodiscard]]
constexpr static bool is_ndc_z_value_out_of_bounds(const ZType& z_ndc) noexcept
{
constexpr auto eps = std::numeric_limits<float>::epsilon();
if constexpr (depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return z_ndc < -1.0f - eps || z_ndc > 1.0f + eps;
if constexpr (depth_range == NDCDepthRange::ZERO_TO_ONE)
return z_ndc < 0.0f - eps || z_ndc > 1.0f + eps;
std::unreachable();
}
// NDC REPRESENTATION:

View File

@@ -116,19 +116,31 @@ namespace omath::rev_eng
return call_method<ReturnType>(vtable[Id], arg_list...);
}
template<std::size_t TableIndex, std::size_t Id, class ReturnType>
template<std::ptrdiff_t TableOffset, std::size_t Id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list)
{
const auto vtable = *reinterpret_cast<void***>(
reinterpret_cast<std::uintptr_t>(this) + TableIndex * sizeof(std::uintptr_t));
return call_method<ReturnType>(vtable[Id], arg_list...);
auto sub_this = reinterpret_cast<void*>(
reinterpret_cast<std::uintptr_t>(this) + TableOffset);
const auto vtable = *reinterpret_cast<void***>(sub_this);
#ifdef _MSC_VER
using Fn = ReturnType(__thiscall*)(void*, decltype(arg_list)...);
#else
using Fn = ReturnType(*)(void*, decltype(arg_list)...);
#endif
return reinterpret_cast<Fn>(vtable[Id])(sub_this, arg_list...);
}
template<std::size_t TableIndex, std::size_t Id, class ReturnType>
template<std::ptrdiff_t TableOffset, std::size_t Id, class ReturnType>
ReturnType call_virtual_method(auto... arg_list) const
{
const auto vtable = *reinterpret_cast<void* const* const*>(
reinterpret_cast<std::uintptr_t>(this) + TableIndex * sizeof(std::uintptr_t));
return call_method<ReturnType>(vtable[Id], arg_list...);
auto sub_this = reinterpret_cast<const void*>(
reinterpret_cast<std::uintptr_t>(this) + TableOffset);
const auto vtable = *reinterpret_cast<void* const* const*>(sub_this);
#ifdef _MSC_VER
using Fn = ReturnType(__thiscall*)(const void*, decltype(arg_list)...);
#else
using Fn = ReturnType(*)(const void*, decltype(arg_list)...);
#endif
return reinterpret_cast<Fn>(vtable[Id])(sub_this, arg_list...);
}
private:

View File

@@ -36,6 +36,7 @@ namespace omath
}
public:
using ArithmeticType = Type;
[[nodiscard]]
constexpr static Angle from_degrees(const Type& degrees) noexcept
{

View File

@@ -2,14 +2,25 @@
// Created by Orange on 11/30/2024.
//
#pragma once
#include "omath/linear_algebra/vector3.hpp"
#include <type_traits>
namespace omath
{
template<class PitchType, class YawType, class RollType>
requires std::is_same_v<typename PitchType::ArithmeticType, typename YawType::ArithmeticType>
&& std::is_same_v<typename YawType::ArithmeticType, typename RollType::ArithmeticType>
struct ViewAngles
{
using ArithmeticType = PitchType::ArithmeticType;
PitchType pitch;
YawType yaw;
RollType roll;
[[nodiscard]]
Vector3<ArithmeticType> as_vector3() const
{
return {pitch.as_degrees(), yaw.as_degrees(), roll.as_degrees()};
}
};
} // namespace omath

View File

@@ -35,8 +35,15 @@ namespace omath::cry_engine
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
}
} // namespace omath::unity_engine

View File

@@ -19,8 +19,9 @@ namespace omath::cry_engine
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::unity_engine

View File

@@ -35,8 +35,16 @@ namespace omath::frostbite_engine
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
}
} // namespace omath::unity_engine

View File

@@ -19,8 +19,9 @@ namespace omath::frostbite_engine
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::unity_engine

View File

@@ -36,18 +36,27 @@ namespace omath::iw_engine
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
// NOTE: Need magic number to fix fov calculation, since IW engine inherit Quake proj matrix calculation
constexpr auto k_multiply_factor = 0.75f;
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f) * k_multiply_factor;
return {
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
{0, 1.f / (fov_half_tan), 0, 0},
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return {
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
{0, 1.f / (fov_half_tan), 0, 0},
{0, 0, far / (far - near), -(near * far) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
{0, 1.f / (fov_half_tan), 0, 0},
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
std::unreachable();
};
} // namespace omath::iw_engine

View File

@@ -19,8 +19,9 @@ namespace omath::iw_engine
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::iw_engine

View File

@@ -8,15 +8,15 @@ namespace omath::opengl_engine
Vector3<float> forward_vector(const ViewAngles& angles) noexcept
{
const auto vec
= rotation_matrix(angles) * mat_column_from_vector<float, MatStoreType::COLUMN_MAJOR>(k_abs_forward);
const auto vec =
rotation_matrix(angles) * mat_column_from_vector<float, MatStoreType::COLUMN_MAJOR>(k_abs_forward);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
}
Vector3<float> right_vector(const ViewAngles& angles) noexcept
{
const auto vec
= rotation_matrix(angles) * mat_column_from_vector<float, MatStoreType::COLUMN_MAJOR>(k_abs_right);
const auto vec =
rotation_matrix(angles) * mat_column_from_vector<float, MatStoreType::COLUMN_MAJOR>(k_abs_right);
return {vec.at(0, 0), vec.at(1, 0), vec.at(2, 0)};
}
@@ -28,7 +28,7 @@ namespace omath::opengl_engine
}
Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept
{
return mat_look_at_right_handed(cam_origin, cam_origin+forward_vector(angles), up_vector(angles));
return mat_look_at_right_handed(cam_origin, cam_origin + forward_vector(angles), up_vector(angles));
}
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept
{
@@ -37,15 +37,16 @@ namespace omath::opengl_engine
* mat_rotation_axis_x<float, MatStoreType::COLUMN_MAJOR>(angles.pitch);
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_right_handed<float, MatStoreType::COLUMN_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
return {
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
{0, 1.f / (fov_half_tan), 0, 0},
{0, 0, -(far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, -1, 0},
};
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_right_handed<float, MatStoreType::COLUMN_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
}
} // namespace omath::opengl_engine

View File

@@ -20,8 +20,9 @@ namespace omath::opengl_engine
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::opengl_engine

View File

@@ -36,18 +36,27 @@ namespace omath::source_engine
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
// NOTE: Need magic number to fix fov calculation, since source inherit Quake proj matrix calculation
constexpr auto k_multiply_factor = 0.75f;
const float fov_half_tan = std::tan(angles::degrees_to_radians(field_of_view) / 2.f) * k_multiply_factor;
return {
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
{0, 1.f / (fov_half_tan), 0, 0},
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return {
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
{0, 1.f / (fov_half_tan), 0, 0},
{0, 0, far / (far - near), -(near * far) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return {
{1.f / (aspect_ratio * fov_half_tan), 0, 0, 0},
{0, 1.f / (fov_half_tan), 0, 0},
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
std::unreachable();
}
} // namespace omath::source_engine

View File

@@ -20,8 +20,9 @@ namespace omath::source_engine
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::source_engine

View File

@@ -35,8 +35,15 @@ namespace omath::unity_engine
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return omath::mat_perspective_right_handed(field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return omath::mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return omath::mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(field_of_view, aspect_ratio,
near, far);
std::unreachable();
}
} // namespace omath::unity_engine

View File

@@ -19,8 +19,9 @@ namespace omath::unity_engine
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::unity_engine

View File

@@ -35,8 +35,12 @@ namespace omath::unreal_engine
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(angles.pitch);
}
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
field_of_view, aspect_ratio, near, far);
return mat_perspective_left_handed(field_of_view, aspect_ratio, near, far);
}
} // namespace omath::unreal_engine

View File

@@ -19,8 +19,9 @@ namespace omath::unreal_engine
}
Mat4X4 CameraTrait::calc_projection_matrix(const projection::FieldOfView& fov,
const projection::ViewPort& view_port, const float near,
const float far) noexcept
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far);
return calc_perspective_projection_matrix(fov.as_degrees(), view_port.aspect_ratio(), near, far,
ndc_depth_range);
}
} // namespace omath::unreal_engine

View File

@@ -237,4 +237,54 @@ TEST(unit_test_cry_engine, loook_at_random_z_axis)
failed_points++;
}
EXPECT_LE(failed_points, 100);
}
TEST(unit_test_cry_engine, ViewAnglesAsVector3Zero)
{
const omath::cry_engine::ViewAngles angles{};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 0.f);
EXPECT_FLOAT_EQ(vec.y, 0.f);
EXPECT_FLOAT_EQ(vec.z, 0.f);
}
TEST(unit_test_cry_engine, ViewAnglesAsVector3Values)
{
const omath::cry_engine::ViewAngles angles{
omath::cry_engine::PitchAngle::from_degrees(45.f),
omath::cry_engine::YawAngle::from_degrees(-90.f),
omath::cry_engine::RollAngle::from_degrees(30.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 45.f);
EXPECT_FLOAT_EQ(vec.y, -90.f);
EXPECT_FLOAT_EQ(vec.z, 30.f);
}
TEST(unit_test_cry_engine, ViewAnglesAsVector3ClampedPitch)
{
// Pitch is clamped to [-90, 90]
const omath::cry_engine::ViewAngles angles{
omath::cry_engine::PitchAngle::from_degrees(120.f),
omath::cry_engine::YawAngle::from_degrees(0.f),
omath::cry_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 90.f);
}
TEST(unit_test_cry_engine, ViewAnglesAsVector3NormalizedYaw)
{
// Yaw is normalized to [-180, 180], 270 wraps to -90
const omath::cry_engine::ViewAngles angles{
omath::cry_engine::PitchAngle::from_degrees(0.f),
omath::cry_engine::YawAngle::from_degrees(270.f),
omath::cry_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_NEAR(vec.y, -90.f, 0.01f);
}

View File

@@ -405,3 +405,51 @@ TEST(unit_test_frostbite_engine, look_at_down)
std::views::zip(dir_vector.as_array(), (-omath::frostbite_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_frostbite_engine, ViewAnglesAsVector3Zero)
{
const omath::frostbite_engine::ViewAngles angles{};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 0.f);
EXPECT_FLOAT_EQ(vec.y, 0.f);
EXPECT_FLOAT_EQ(vec.z, 0.f);
}
TEST(unit_test_frostbite_engine, ViewAnglesAsVector3Values)
{
const omath::frostbite_engine::ViewAngles angles{
omath::frostbite_engine::PitchAngle::from_degrees(45.f),
omath::frostbite_engine::YawAngle::from_degrees(-90.f),
omath::frostbite_engine::RollAngle::from_degrees(30.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 45.f);
EXPECT_FLOAT_EQ(vec.y, -90.f);
EXPECT_FLOAT_EQ(vec.z, 30.f);
}
TEST(unit_test_frostbite_engine, ViewAnglesAsVector3ClampedPitch)
{
const omath::frostbite_engine::ViewAngles angles{
omath::frostbite_engine::PitchAngle::from_degrees(120.f),
omath::frostbite_engine::YawAngle::from_degrees(0.f),
omath::frostbite_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 90.f);
}
TEST(unit_test_frostbite_engine, ViewAnglesAsVector3NormalizedYaw)
{
const omath::frostbite_engine::ViewAngles angles{
omath::frostbite_engine::PitchAngle::from_degrees(0.f),
omath::frostbite_engine::YawAngle::from_degrees(270.f),
omath::frostbite_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_NEAR(vec.y, -90.f, 0.01f);
}

View File

@@ -280,4 +280,54 @@ TEST(unit_test_iw_engine, look_at_down)
EXPECT_NEAR(dir_vector.z, -0.99984f, 0.0001f);
EXPECT_NEAR(dir_vector.x,- 0.017f, 0.01f);
EXPECT_NEAR(dir_vector.y, 0.f, 0.001f);
}
TEST(unit_test_iw_engine, ViewAnglesAsVector3Zero)
{
const omath::iw_engine::ViewAngles angles{};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 0.f);
EXPECT_FLOAT_EQ(vec.y, 0.f);
EXPECT_FLOAT_EQ(vec.z, 0.f);
}
TEST(unit_test_iw_engine, ViewAnglesAsVector3Values)
{
const omath::iw_engine::ViewAngles angles{
omath::iw_engine::PitchAngle::from_degrees(45.f),
omath::iw_engine::YawAngle::from_degrees(-90.f),
omath::iw_engine::RollAngle::from_degrees(30.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 45.f);
EXPECT_FLOAT_EQ(vec.y, -90.f);
EXPECT_FLOAT_EQ(vec.z, 30.f);
}
TEST(unit_test_iw_engine, ViewAnglesAsVector3ClampedPitch)
{
// Pitch is clamped to [-89, 89]
const omath::iw_engine::ViewAngles angles{
omath::iw_engine::PitchAngle::from_degrees(120.f),
omath::iw_engine::YawAngle::from_degrees(0.f),
omath::iw_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 89.f);
}
TEST(unit_test_iw_engine, ViewAnglesAsVector3NormalizedYaw)
{
// Yaw is normalized to [-180, 180], 270 wraps to -90
const omath::iw_engine::ViewAngles angles{
omath::iw_engine::PitchAngle::from_degrees(0.f),
omath::iw_engine::YawAngle::from_degrees(270.f),
omath::iw_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_NEAR(vec.y, -90.f, 0.01f);
}

View File

@@ -394,4 +394,52 @@ TEST(unit_test_opengl_engine, look_at_down)
const auto dir_vector = omath::opengl_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::opengl_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_opengl, ViewAnglesAsVector3Zero)
{
const omath::opengl_engine::ViewAngles angles{};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 0.f);
EXPECT_FLOAT_EQ(vec.y, 0.f);
EXPECT_FLOAT_EQ(vec.z, 0.f);
}
TEST(unit_test_opengl, ViewAnglesAsVector3Values)
{
const omath::opengl_engine::ViewAngles angles{
omath::opengl_engine::PitchAngle::from_degrees(45.f),
omath::opengl_engine::YawAngle::from_degrees(-90.f),
omath::opengl_engine::RollAngle::from_degrees(30.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 45.f);
EXPECT_FLOAT_EQ(vec.y, -90.f);
EXPECT_FLOAT_EQ(vec.z, 30.f);
}
TEST(unit_test_opengl, ViewAnglesAsVector3ClampedPitch)
{
const omath::opengl_engine::ViewAngles angles{
omath::opengl_engine::PitchAngle::from_degrees(120.f),
omath::opengl_engine::YawAngle::from_degrees(0.f),
omath::opengl_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 90.f);
}
TEST(unit_test_opengl, ViewAnglesAsVector3NormalizedYaw)
{
const omath::opengl_engine::ViewAngles angles{
omath::opengl_engine::PitchAngle::from_degrees(0.f),
omath::opengl_engine::YawAngle::from_degrees(270.f),
omath::opengl_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_NEAR(vec.y, -90.f, 0.01f);
}

View File

@@ -422,4 +422,54 @@ TEST(unit_test_source_engine, look_at_down)
EXPECT_NEAR(dir_vector.z, -0.99984f, 0.0001f);
EXPECT_NEAR(dir_vector.x,- 0.017f, 0.01f);
EXPECT_NEAR(dir_vector.y, 0.f, 0.001f);
}
TEST(unit_test_source_engine, ViewAnglesAsVector3Zero)
{
const omath::source_engine::ViewAngles angles{};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 0.f);
EXPECT_FLOAT_EQ(vec.y, 0.f);
EXPECT_FLOAT_EQ(vec.z, 0.f);
}
TEST(unit_test_source_engine, ViewAnglesAsVector3Values)
{
const omath::source_engine::ViewAngles angles{
omath::source_engine::PitchAngle::from_degrees(45.f),
omath::source_engine::YawAngle::from_degrees(-90.f),
omath::source_engine::RollAngle::from_degrees(30.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 45.f);
EXPECT_FLOAT_EQ(vec.y, -90.f);
EXPECT_FLOAT_EQ(vec.z, 30.f);
}
TEST(unit_test_source_engine, ViewAnglesAsVector3ClampedPitch)
{
// Pitch is clamped to [-89, 89]
const omath::source_engine::ViewAngles angles{
omath::source_engine::PitchAngle::from_degrees(120.f),
omath::source_engine::YawAngle::from_degrees(0.f),
omath::source_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 89.f);
}
TEST(unit_test_source_engine, ViewAnglesAsVector3NormalizedYaw)
{
// Yaw is normalized to [-180, 180], 270 wraps to -90
const omath::source_engine::ViewAngles angles{
omath::source_engine::PitchAngle::from_degrees(0.f),
omath::source_engine::YawAngle::from_degrees(270.f),
omath::source_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_NEAR(vec.y, -90.f, 0.01f);
}

View File

@@ -21,6 +21,9 @@
#include <omath/engines/unreal_engine/traits/camera_trait.hpp>
#include <omath/engines/source_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/source_engine/traits/camera_trait.hpp>
#include <omath/engines/cry_engine/traits/camera_trait.hpp>
#include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp>
@@ -218,9 +221,14 @@ TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera)
// CameraTrait look at should be callable
const auto angles = e::CameraTrait::calc_look_at_angle({0, 0, 0}, {0, 1, 1});
(void)angles;
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
const auto proj_zo = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
const auto expected_zo = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
expect_matrix_near(proj_zo, expected_zo);
EXPECT_NE(proj, proj_zo);
}
TEST(TraitTests, IW_Pred_And_Mesh_And_Camera)
@@ -264,10 +272,15 @@ TEST(TraitTests, IW_Pred_And_Mesh_And_Camera)
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f);
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto expected = e::calc_perspective_projection_matrix(45.f, 1920.f / 1080.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
const auto proj_zo = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
const auto expected_zo = e::calc_perspective_projection_matrix(45.f, 1920.f / 1080.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
expect_matrix_near(proj_zo, expected_zo);
EXPECT_NE(proj, proj_zo);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_iw = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
@@ -314,10 +327,15 @@ TEST(TraitTests, OpenGL_Pred_And_Mesh_And_Camera)
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
const auto proj_zo = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
const auto expected_zo = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
expect_matrix_near(proj_zo, expected_zo);
EXPECT_NE(proj, proj_zo);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_gl = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
@@ -364,10 +382,15 @@ TEST(TraitTests, Unity_Pred_And_Mesh_And_Camera)
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
const auto proj_zo = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
const auto expected_zo = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
expect_matrix_near(proj_zo, expected_zo);
EXPECT_NE(proj, proj_zo);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_unity = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
@@ -414,12 +437,237 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f);
const auto proj = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto expected = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f);
expect_matrix_near(proj, expected);
const auto proj_zo = e::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
const auto expected_zo = e::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
expect_matrix_near(proj_zo, expected_zo);
EXPECT_NE(proj, proj_zo);
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f);
}
// ── NDC Depth Range tests for Source and CryEngine camera traits ────────────
TEST(NDCDepthRangeTests, Source_BothDepthRanges)
{
namespace e = omath::source_engine;
const auto proj_no = e::CameraTrait::calc_projection_matrix(
projection::FieldOfView::from_degrees(90.f), {1920.f, 1080.f}, 0.1f, 1000.f,
NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto expected_no = e::calc_perspective_projection_matrix(
90.f, 1920.f / 1080.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
expect_matrix_near(proj_no, expected_no);
const auto proj_zo = e::CameraTrait::calc_projection_matrix(
projection::FieldOfView::from_degrees(90.f), {1920.f, 1080.f}, 0.1f, 1000.f,
NDCDepthRange::ZERO_TO_ONE);
const auto expected_zo = e::calc_perspective_projection_matrix(
90.f, 1920.f / 1080.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
expect_matrix_near(proj_zo, expected_zo);
EXPECT_NE(proj_no, proj_zo);
}
TEST(NDCDepthRangeTests, CryEngine_BothDepthRanges)
{
namespace e = omath::cry_engine;
const auto proj_no = e::CameraTrait::calc_projection_matrix(
projection::FieldOfView::from_degrees(90.f), {1920.f, 1080.f}, 0.1f, 1000.f,
NDCDepthRange::NEGATIVE_ONE_TO_ONE);
const auto expected_no = e::calc_perspective_projection_matrix(
90.f, 1920.f / 1080.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
expect_matrix_near(proj_no, expected_no);
const auto proj_zo = e::CameraTrait::calc_projection_matrix(
projection::FieldOfView::from_degrees(90.f), {1920.f, 1080.f}, 0.1f, 1000.f,
NDCDepthRange::ZERO_TO_ONE);
const auto expected_zo = e::calc_perspective_projection_matrix(
90.f, 1920.f / 1080.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
expect_matrix_near(proj_zo, expected_zo);
EXPECT_NE(proj_no, proj_zo);
}
// ── Verify Z mapping for ZERO_TO_ONE across all engines ─────────────────────
// Helper: projects a point at given z through a left-handed projection matrix and returns NDC z
static float project_z_lh(const Mat<4, 4>& proj, float z)
{
auto clip = proj * mat_column_from_vector<float>({0, 0, z});
return clip.at(2, 0) / clip.at(3, 0);
}
TEST(NDCDepthRangeTests, Source_ZeroToOne_ZRange)
{
namespace e = omath::source_engine;
// Source is left-handed
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), 0.0f, 1e-4f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-4f);
EXPECT_GT(project_z_lh(proj, 500.f), 0.0f);
EXPECT_LT(project_z_lh(proj, 500.f), 1.0f);
}
TEST(NDCDepthRangeTests, IW_ZeroToOne_ZRange)
{
namespace e = omath::iw_engine;
// IW is left-handed
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), 0.0f, 1e-4f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-4f);
EXPECT_GT(project_z_lh(proj, 500.f), 0.0f);
EXPECT_LT(project_z_lh(proj, 500.f), 1.0f);
}
TEST(NDCDepthRangeTests, OpenGL_ZeroToOne_ZRange)
{
namespace e = omath::opengl_engine;
// OpenGL is right-handed (negative z forward), column-major
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
// OpenGL engine uses column-major matrices, project manually
auto proj_z = [&](float z) {
auto clip = proj * mat_column_from_vector<float, MatStoreType::COLUMN_MAJOR>({0, 0, z});
return clip.at(2, 0) / clip.at(3, 0);
};
EXPECT_NEAR(proj_z(-0.1f), 0.0f, 1e-4f);
EXPECT_NEAR(proj_z(-1000.f), 1.0f, 1e-4f);
EXPECT_GT(proj_z(-500.f), 0.0f);
EXPECT_LT(proj_z(-500.f), 1.0f);
}
TEST(NDCDepthRangeTests, Frostbite_ZeroToOne_ZRange)
{
namespace e = omath::frostbite_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), 0.0f, 1e-4f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-4f);
EXPECT_GT(project_z_lh(proj, 500.f), 0.0f);
EXPECT_LT(project_z_lh(proj, 500.f), 1.0f);
}
TEST(NDCDepthRangeTests, Unity_ZeroToOne_ZRange)
{
namespace e = omath::unity_engine;
// Unity is right-handed, row-major
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
auto proj_z = [&](float z) {
auto clip = proj * mat_column_from_vector<float>({0, 0, z});
return clip.at(2, 0) / clip.at(3, 0);
};
EXPECT_NEAR(proj_z(-0.1f), 0.0f, 1e-4f);
EXPECT_NEAR(proj_z(-1000.f), 1.0f, 1e-4f);
EXPECT_GT(proj_z(-500.f), 0.0f);
EXPECT_LT(proj_z(-500.f), 1.0f);
}
TEST(NDCDepthRangeTests, Unreal_ZeroToOne_ZRange)
{
namespace e = omath::unreal_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), 0.0f, 1e-4f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-4f);
EXPECT_GT(project_z_lh(proj, 500.f), 0.0f);
EXPECT_LT(project_z_lh(proj, 500.f), 1.0f);
}
TEST(NDCDepthRangeTests, CryEngine_ZeroToOne_ZRange)
{
namespace e = omath::cry_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::ZERO_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), 0.0f, 1e-4f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-4f);
EXPECT_GT(project_z_lh(proj, 500.f), 0.0f);
EXPECT_LT(project_z_lh(proj, 500.f), 1.0f);
}
// ── Verify Z mapping for NEGATIVE_ONE_TO_ONE across all engines ─────────────
TEST(NDCDepthRangeTests, Source_NegativeOneToOne_ZRange)
{
namespace e = omath::source_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), -1.0f, 1e-3f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-3f);
}
TEST(NDCDepthRangeTests, IW_NegativeOneToOne_ZRange)
{
namespace e = omath::iw_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), -1.0f, 1e-3f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-3f);
}
TEST(NDCDepthRangeTests, Frostbite_NegativeOneToOne_ZRange)
{
namespace e = omath::frostbite_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), -1.0f, 1e-3f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-3f);
}
TEST(NDCDepthRangeTests, Unreal_NegativeOneToOne_ZRange)
{
namespace e = omath::unreal_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), -1.0f, 1e-3f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-3f);
}
TEST(NDCDepthRangeTests, CryEngine_NegativeOneToOne_ZRange)
{
namespace e = omath::cry_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
EXPECT_NEAR(project_z_lh(proj, 0.1f), -1.0f, 1e-3f);
EXPECT_NEAR(project_z_lh(proj, 1000.f), 1.0f, 1e-3f);
}
TEST(NDCDepthRangeTests, OpenGL_NegativeOneToOne_ZRange)
{
namespace e = omath::opengl_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
auto proj_z = [&](float z) {
auto clip = proj * mat_column_from_vector<float, MatStoreType::COLUMN_MAJOR>({0, 0, z});
return clip.at(2, 0) / clip.at(3, 0);
};
EXPECT_NEAR(proj_z(-0.1f), -1.0f, 1e-3f);
EXPECT_NEAR(proj_z(-1000.f), 1.0f, 1e-3f);
}
TEST(NDCDepthRangeTests, Unity_NegativeOneToOne_ZRange)
{
namespace e = omath::unity_engine;
const auto proj = e::calc_perspective_projection_matrix(90.f, 16.f / 9.f, 0.1f, 1000.f, NDCDepthRange::NEGATIVE_ONE_TO_ONE);
auto proj_z = [&](float z) {
auto clip = proj * mat_column_from_vector<float>({0, 0, z});
return clip.at(2, 0) / clip.at(3, 0);
};
EXPECT_NEAR(proj_z(-0.1f), -1.0f, 1e-3f);
EXPECT_NEAR(proj_z(-1000.f), 1.0f, 1e-3f);
}

View File

@@ -417,3 +417,51 @@ TEST(unit_test_unity_engine, look_at_down)
std::views::zip(dir_vector.as_array(), (-omath::unity_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unity_engine, ViewAnglesAsVector3Zero)
{
const omath::unity_engine::ViewAngles angles{};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 0.f);
EXPECT_FLOAT_EQ(vec.y, 0.f);
EXPECT_FLOAT_EQ(vec.z, 0.f);
}
TEST(unit_test_unity_engine, ViewAnglesAsVector3Values)
{
const omath::unity_engine::ViewAngles angles{
omath::unity_engine::PitchAngle::from_degrees(45.f),
omath::unity_engine::YawAngle::from_degrees(-90.f),
omath::unity_engine::RollAngle::from_degrees(30.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 45.f);
EXPECT_FLOAT_EQ(vec.y, -90.f);
EXPECT_FLOAT_EQ(vec.z, 30.f);
}
TEST(unit_test_unity_engine, ViewAnglesAsVector3ClampedPitch)
{
const omath::unity_engine::ViewAngles angles{
omath::unity_engine::PitchAngle::from_degrees(120.f),
omath::unity_engine::YawAngle::from_degrees(0.f),
omath::unity_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 90.f);
}
TEST(unit_test_unity_engine, ViewAnglesAsVector3NormalizedYaw)
{
const omath::unity_engine::ViewAngles angles{
omath::unity_engine::PitchAngle::from_degrees(0.f),
omath::unity_engine::YawAngle::from_degrees(270.f),
omath::unity_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_NEAR(vec.y, -90.f, 0.01f);
}

View File

@@ -417,4 +417,52 @@ TEST(unit_test_unreal_engine, look_at_down)
const auto dir_vector = omath::unreal_engine::forward_vector(angles);
for (const auto& [result, etalon] : std::views::zip(dir_vector.as_array(), (-omath::unreal_engine::k_abs_up).as_array()))
EXPECT_NEAR(result, etalon, 0.0001f);
}
TEST(unit_test_unreal_engine, ViewAnglesAsVector3Zero)
{
const omath::unreal_engine::ViewAngles angles{};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 0.f);
EXPECT_FLOAT_EQ(vec.y, 0.f);
EXPECT_FLOAT_EQ(vec.z, 0.f);
}
TEST(unit_test_unreal_engine, ViewAnglesAsVector3Values)
{
const omath::unreal_engine::ViewAngles angles{
omath::unreal_engine::PitchAngle::from_degrees(45.f),
omath::unreal_engine::YawAngle::from_degrees(-90.f),
omath::unreal_engine::RollAngle::from_degrees(30.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 45.f);
EXPECT_FLOAT_EQ(vec.y, -90.f);
EXPECT_FLOAT_EQ(vec.z, 30.f);
}
TEST(unit_test_unreal_engine, ViewAnglesAsVector3ClampedPitch)
{
const omath::unreal_engine::ViewAngles angles{
omath::unreal_engine::PitchAngle::from_degrees(120.f),
omath::unreal_engine::YawAngle::from_degrees(0.f),
omath::unreal_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_FLOAT_EQ(vec.x, 90.f);
}
TEST(unit_test_unreal_engine, ViewAnglesAsVector3NormalizedYaw)
{
const omath::unreal_engine::ViewAngles angles{
omath::unreal_engine::PitchAngle::from_degrees(0.f),
omath::unreal_engine::YawAngle::from_degrees(270.f),
omath::unreal_engine::RollAngle::from_degrees(0.f)
};
const auto vec = angles.as_vector3();
EXPECT_NEAR(vec.y, -90.f, 0.01f);
}

View File

@@ -0,0 +1,125 @@
//
// Created by Vlad on 3/25/2025.
//
#include "omath/collision/line_tracer.hpp"
#include "omath/3d_primitives/aabb.hpp"
#include <gtest/gtest.h>
using Vec3 = omath::Vector3<float>;
using Ray = omath::collision::Ray<>;
using LineTracer = omath::collision::LineTracer<>;
using AABB = omath::primitives::Aabb<float>;
static Ray make_ray(Vec3 start, Vec3 end, bool infinite = false)
{
Ray r;
r.start = start;
r.end = end;
r.infinite_length = infinite;
return r;
}
// Ray passing straight through the center along Z axis
TEST(LineTracerAABBTests, HitCenterAlongZ)
{
const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
const auto ray = make_ray({0.f, 0.f, -5.f}, {0.f, 0.f, 5.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.z, -1.f, 1e-4f);
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
}
// Ray passing straight through the center along X axis
TEST(LineTracerAABBTests, HitCenterAlongX)
{
const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
const auto ray = make_ray({-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, -1.f, 1e-4f);
}
// Ray that misses entirely (too far in Y)
TEST(LineTracerAABBTests, MissReturnsEnd)
{
const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
const auto ray = make_ray({0.f, 5.f, -5.f}, {0.f, 5.f, 5.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
// Ray that stops short before reaching the box
TEST(LineTracerAABBTests, RayTooShortReturnsEnd)
{
const AABB box{{3.f, -1.f, -1.f}, {5.f, 1.f, 1.f}};
const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
// Infinite ray that starts before the box should hit
TEST(LineTracerAABBTests, InfiniteRayHits)
{
const AABB box{{3.f, -1.f, -1.f}, {5.f, 1.f, 1.f}};
const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}, true);
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, 3.f, 1e-4f);
}
// Ray starting inside the box — t_min=0, so hit point equals ray.start
TEST(LineTracerAABBTests, RayStartsInsideBox)
{
const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
const auto ray = make_ray({0.f, 0.f, 0.f}, {0.f, 0.f, 5.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
// t_min is clamped to 0, so hit == start
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
}
// Ray parallel to XY plane, pointing along X, at Z outside the box
TEST(LineTracerAABBTests, ParallelRayOutsideSlabMisses)
{
const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
// Z component of ray is 3.0 — outside box's Z slab
const auto ray = make_ray({-5.f, 0.f, 3.f}, {5.f, 0.f, 3.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_EQ(hit, ray.end);
}
// Ray parallel to XY plane, pointing along X, at Z inside the box
TEST(LineTracerAABBTests, ParallelRayInsideSlabHits)
{
const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
const auto ray = make_ray({-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
EXPECT_NEAR(hit.x, -1.f, 1e-4f);
}
// Diagonal ray hitting a corner region
TEST(LineTracerAABBTests, DiagonalRayHits)
{
const AABB box{{0.f, 0.f, 0.f}, {2.f, 2.f, 2.f}};
const auto ray = make_ray({-1.f, -1.f, -1.f}, {3.f, 3.f, 3.f});
const auto hit = LineTracer::get_ray_hit_point(ray, box);
EXPECT_NE(hit, ray.end);
// Entry point should be at (0,0,0)
EXPECT_NEAR(hit.x, 0.f, 1e-4f);
EXPECT_NEAR(hit.y, 0.f, 1e-4f);
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
}

View File

@@ -240,4 +240,126 @@ TEST(UnitTestMatStandalone, MatPerspectiveLeftHanded)
projected /= projected.at(3, 0);
EXPECT_TRUE(projected.at(2, 0) > -1.0f && projected.at(2, 0) < 0.f);
}
TEST(UnitTestMatStandalone, MatPerspectiveLeftHandedZeroToOne)
{
const auto proj = mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane point should map to z ~ 0
auto near_pt = proj * mat_column_from_vector<float>({0, 0, 0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f);
// Far plane point should map to z ~ 1
auto far_pt = proj * mat_column_from_vector<float>({0, 0, 1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f);
// Mid-range point should be in [0, 1]
auto mid_pt = proj * mat_column_from_vector<float>({0, 0, 500.f});
mid_pt /= mid_pt.at(3, 0);
EXPECT_GT(mid_pt.at(2, 0), 0.0f);
EXPECT_LT(mid_pt.at(2, 0), 1.0f);
}
TEST(UnitTestMatStandalone, MatPerspectiveRightHandedZeroToOne)
{
const auto proj = mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
// Near plane point (negative z for right-handed) should map to z ~ 0
auto near_pt = proj * mat_column_from_vector<float>({0, 0, -0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f);
// Far plane point should map to z ~ 1
auto far_pt = proj * mat_column_from_vector<float>({0, 0, -1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f);
// Mid-range point should be in [0, 1]
auto mid_pt = proj * mat_column_from_vector<float>({0, 0, -500.f});
mid_pt /= mid_pt.at(3, 0);
EXPECT_GT(mid_pt.at(2, 0), 0.0f);
EXPECT_LT(mid_pt.at(2, 0), 1.0f);
}
TEST(UnitTestMatStandalone, MatPerspectiveNegativeOneToOneRange)
{
// Verify existing [-1, 1] behavior with explicit template arg matches default
const auto proj_default = mat_perspective_left_handed(90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_explicit = mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(90.f, 16.f / 9.f, 0.1f, 1000.f);
EXPECT_EQ(proj_default, proj_explicit);
// Near plane should map to z ~ -1
auto near_pt = proj_default * mat_column_from_vector<float>({0, 0, 0.1f});
near_pt /= near_pt.at(3, 0);
EXPECT_NEAR(near_pt.at(2, 0), -1.0f, 1e-3f);
// Far plane should map to z ~ 1
auto far_pt = proj_default * mat_column_from_vector<float>({0, 0, 1000.f});
far_pt /= far_pt.at(3, 0);
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-3f);
}
TEST(UnitTestMatStandalone, MatPerspectiveZeroToOneEquanity)
{
// LH and RH should produce same NDC for mirrored z
constexpr omath::Vector3<float> left_handed = {0, 2, 10};
constexpr omath::Vector3<float> right_handed = {0, 2, -10};
const auto proj_lh = mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
const auto proj_rh = mat_perspective_right_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
90.f, 16.f / 9.f, 0.1f, 1000.f);
auto ndc_lh = proj_lh * mat_column_from_vector(left_handed);
auto ndc_rh = proj_rh * mat_column_from_vector(right_handed);
ndc_lh /= ndc_lh.at(3, 0);
ndc_rh /= ndc_rh.at(3, 0);
EXPECT_EQ(ndc_lh, ndc_rh);
}
TEST(UnitTestMatStandalone, MatOrthoLeftHandedZeroToOne)
{
const auto ortho = mat_ortho_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
-1.f, 1.f, -1.f, 1.f, 0.1f, 100.f);
// Near plane should map to z ~ 0
auto near_pt = ortho * mat_column_from_vector<float>({0, 0, 0.1f});
EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f);
// Far plane should map to z ~ 1
auto far_pt = ortho * mat_column_from_vector<float>({0, 0, 100.f});
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f);
}
TEST(UnitTestMatStandalone, MatOrthoRightHandedZeroToOne)
{
const auto ortho = mat_ortho_right_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
-1.f, 1.f, -1.f, 1.f, 0.1f, 100.f);
// Near plane (negative z for RH) should map to z ~ 0
auto near_pt = ortho * mat_column_from_vector<float>({0, 0, -0.1f});
EXPECT_NEAR(near_pt.at(2, 0), 0.0f, 1e-4f);
// Far plane should map to z ~ 1
auto far_pt = ortho * mat_column_from_vector<float>({0, 0, -100.f});
EXPECT_NEAR(far_pt.at(2, 0), 1.0f, 1e-4f);
}
TEST(UnitTestMatStandalone, MatOrthoNegativeOneToOneDefault)
{
// Verify explicit [-1, 1] matches default
const auto ortho_default = mat_ortho_left_handed(-1.f, 1.f, -1.f, 1.f, 0.1f, 100.f);
const auto ortho_explicit = mat_ortho_left_handed<float, MatStoreType::ROW_MAJOR,
NDCDepthRange::NEGATIVE_ONE_TO_ONE>(-1.f, 1.f, -1.f, 1.f, 0.1f, 100.f);
EXPECT_EQ(ortho_default, ortho_explicit);
}

View File

@@ -4,6 +4,8 @@
#include "omath/engines/unity_engine/camera.hpp"
#include <complex>
#include <gtest/gtest.h>
#include <omath/3d_primitives/aabb.hpp>
#include <omath/engines/opengl_engine/camera.hpp>
#include <omath/engines/source_engine/camera.hpp>
#include <omath/projection/camera.hpp>
#include <print>
@@ -216,4 +218,296 @@ TEST(UnitTestProjection, ScreenToWorldBottomLeftCorner)
EXPECT_NEAR(screen_cords->x, initial_screen_cords.x, 0.001f);
EXPECT_NEAR(screen_cords->y, initial_screen_cords.y, 0.001f);
}
}
TEST(UnitTestProjection, AabbInsideFrustumNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Small box directly in front of camera (Source Engine: +X forward, +Y left, +Z up)
const omath::primitives::Aabb<float> aabb{{90.f, -1.f, -1.f}, {110.f, 1.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbBehindCameraCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box entirely behind the camera
const omath::primitives::Aabb<float> aabb{{-200.f, -1.f, -1.f}, {-100.f, 1.f, 1.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbBeyondFarPlaneCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box beyond far plane (1000)
const omath::primitives::Aabb<float> aabb{{1500.f, -1.f, -1.f}, {2000.f, 1.f, 1.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbFarLeftCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box far to the side, outside the frustum
const omath::primitives::Aabb<float> aabb{{90.f, 4000.f, -1.f}, {110.f, 5000.f, 1.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbFarRightCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box far to the other side, outside the frustum
const omath::primitives::Aabb<float> aabb{{90.f, -5000.f, -1.f}, {110.f, -4000.f, 1.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbAboveCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box far above the frustum
const omath::primitives::Aabb<float> aabb{{90.f, -1.f, 5000.f}, {110.f, 1.f, 6000.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbPartiallyInsideNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Large box that straddles the frustum boundary — partially inside
const omath::primitives::Aabb<float> aabb{{50.f, -5000.f, -5000.f}, {500.f, 5000.f, 5000.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbStraddlesNearPlaneNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box that straddles the near plane — partially in front
const omath::primitives::Aabb<float> aabb{{-5.f, -1.f, -1.f}, {5.f, 1.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbStraddlesFarPlaneNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box that straddles the far plane
const omath::primitives::Aabb<float> aabb{{900.f, -1.f, -1.f}, {1100.f, 1.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbCulledUnityEngine)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f);
// Box in front — not culled
const omath::primitives::Aabb<float> inside{{-1.f, -1.f, 50.f}, {1.f, 1.f, 100.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(inside));
// Box behind — culled
const omath::primitives::Aabb<float> behind{{-1.f, -1.f, -200.f}, {1.f, 1.f, -100.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(behind));
}
TEST(UnitTestProjection, AabbBelowCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box far below the frustum (Source Engine: +Z up)
const omath::primitives::Aabb<float> aabb{{90.f, -1.f, -6000.f}, {110.f, 1.f, -5000.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbEnclosesCameraNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Huge box that fully encloses the camera
const omath::primitives::Aabb<float> aabb{{-500.f, -500.f, -500.f}, {500.f, 500.f, 500.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbExactlyAtNearPlaneNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box starting exactly at the near plane distance
const omath::primitives::Aabb<float> aabb{{0.01f, -1.f, -1.f}, {10.f, 1.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbExactlyAtFarPlaneNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Box ending exactly at the far plane distance
const omath::primitives::Aabb<float> aabb{{990.f, -1.f, -1.f}, {1000.f, 1.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbNarrowFovCulledAtEdge)
{
// Narrow FOV — box that would be visible at 90 is culled at 30
constexpr auto fov = omath::projection::FieldOfView::from_degrees(30.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
const omath::primitives::Aabb<float> aabb{{100.f, 200.f, -1.f}, {110.f, 210.f, 1.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbWideFovNotCulledAtEdge)
{
// Wide FOV — same box is visible
constexpr auto fov = omath::projection::FieldOfView::from_degrees(120.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
const omath::primitives::Aabb<float> aabb{{100.f, 200.f, -1.f}, {110.f, 210.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbCameraOffOrigin)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({500.f, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f},
fov, 0.01f, 1000.f);
// Box in front of shifted camera
const omath::primitives::Aabb<float> in_front{{600.f, -1.f, -1.f}, {700.f, 1.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(in_front));
// Box behind shifted camera
const omath::primitives::Aabb<float> behind{{0.f, -1.f, -1.f}, {100.f, 1.f, 1.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(behind));
}
TEST(UnitTestProjection, AabbShortFarPlaneCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
// Very short far plane
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 50.f);
// Box at distance 100 — beyond the 50-unit far plane
const omath::primitives::Aabb<float> aabb{{90.f, -1.f, -1.f}, {110.f, 1.f, 1.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
// Box at distance 30 — within range
const omath::primitives::Aabb<float> near_box{{25.f, -1.f, -1.f}, {35.f, 1.f, 1.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(near_box));
}
TEST(UnitTestProjection, AabbPointSizedInsideNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::source_engine::Camera({0, 0, 0}, omath::source_engine::ViewAngles{}, {1920.f, 1080.f}, fov,
0.01f, 1000.f);
// Degenerate zero-volume AABB (a point) inside the frustum
const omath::primitives::Aabb<float> aabb{{100.f, 0.f, 0.f}, {100.f, 0.f, 0.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbOpenGlEngineInsideNotCulled)
{
// OpenGL: COLUMN_MAJOR, NEGATIVE_ONE_TO_ONE, inverted_z, forward = -Z
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f);
// Box in front of camera (OpenGL: -Z forward)
const omath::primitives::Aabb<float> aabb{{-1.f, -1.f, -110.f}, {1.f, 1.f, -90.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbOpenGlEngineBehindCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f);
// Box behind (OpenGL: +Z is behind the camera)
const omath::primitives::Aabb<float> aabb{{-1.f, -1.f, 100.f}, {1.f, 1.f, 200.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbOpenGlEngineBeyondFarCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f);
// Box beyond far plane along -Z
const omath::primitives::Aabb<float> aabb{{-1.f, -1.f, -2000.f}, {1.f, 1.f, -1500.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbOpenGlEngineSideCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(90.f);
const auto cam = omath::opengl_engine::Camera({0, 0, 0}, {}, {1920.f, 1080.f}, fov, 0.01f, 1000.f);
// Box far to the right (OpenGL: +X right)
const omath::primitives::Aabb<float> aabb{{4000.f, -1.f, -110.f}, {5000.f, 1.f, -90.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbUnityEngineBeyondFarCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 500.f);
// Box beyond 500-unit far plane (Unity: +Z forward)
const omath::primitives::Aabb<float> aabb{{-1.f, -1.f, 600.f}, {1.f, 1.f, 700.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbUnityEngineSideCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f);
// Box far above (Unity: +Y up)
const omath::primitives::Aabb<float> aabb{{-1.f, 5000.f, 50.f}, {1.f, 6000.f, 100.f}};
EXPECT_TRUE(cam.is_aabb_culled_by_frustum(aabb));
}
TEST(UnitTestProjection, AabbUnityEngineStraddlesNearNotCulled)
{
constexpr auto fov = omath::projection::FieldOfView::from_degrees(60.f);
const auto cam = omath::unity_engine::Camera({0, 0, 0}, {}, {1280.f, 720.f}, fov, 0.03f, 1000.f);
// Box straddles near plane (Unity: +Z forward)
const omath::primitives::Aabb<float> aabb{{-1.f, -1.f, -5.f}, {1.f, 1.f, 5.f}};
EXPECT_FALSE(cam.is_aabb_culled_by_frustum(aabb));
}

View File

@@ -30,6 +30,7 @@ inline const void* get_vtable_entry(const void* obj, const std::size_t index)
class BaseA
{
public:
int m_field_a{42};
[[nodiscard]] virtual int get_a() const { return 10; }
[[nodiscard]] virtual int get_a2() const { return 11; }
};
@@ -37,6 +38,8 @@ public:
class BaseB
{
public:
float m_field_b{3.14f};
double m_field_b2{2.71};
[[nodiscard]] virtual int get_b() const { return 20; }
[[nodiscard]] virtual int get_b2() const { return 21; }
};
@@ -44,26 +47,31 @@ public:
class MultiPlayer final : public BaseA, public BaseB
{
public:
int m_own_field{999};
[[nodiscard]] int get_a() const override { return 100; }
[[nodiscard]] int get_a2() const override { return 101; }
[[nodiscard]] int get_b() const override { return 200; }
[[nodiscard]] int get_b2() const override { return 201; }
};
// BaseA layout: [vptr_a][m_field_a(int)] — sizeof(BaseA) gives the full subobject size
// BaseB starts right after BaseA in MultiPlayer's layout
constexpr std::ptrdiff_t BASE_B_OFFSET = static_cast<std::ptrdiff_t>(sizeof(BaseA));
class RevMultiPlayer final : omath::rev_eng::InternalReverseEngineeredObject
{
public:
// Table 0 (BaseA vtable): index 0 = get_a, 1 = get_a2
// Table at offset 0 (BaseA vtable): index 0 = get_a, 1 = get_a2
[[nodiscard]] int rev_get_a() const { return call_virtual_method<0, 0, int>(); }
[[nodiscard]] int rev_get_a2() const { return call_virtual_method<0, 1, int>(); }
// Table 1 (BaseB vtable): index 0 = get_b, 1 = get_b2
[[nodiscard]] int rev_get_b() const { return call_virtual_method<1, 0, int>(); }
[[nodiscard]] int rev_get_b2() const { return call_virtual_method<1, 1, int>(); }
// Table at BaseB offset (BaseB vtable): index 0 = get_b, 1 = get_b2
[[nodiscard]] int rev_get_b() const { return call_virtual_method<BASE_B_OFFSET, 0, int>(); }
[[nodiscard]] int rev_get_b2() const { return call_virtual_method<BASE_B_OFFSET, 1, int>(); }
// Non-const versions
int rev_get_a_mut() { return call_virtual_method<0, 0, int>(); }
int rev_get_b_mut() { return call_virtual_method<1, 0, int>(); }
int rev_get_b_mut() { return call_virtual_method<BASE_B_OFFSET, 0, int>(); }
};
class RevPlayer final : omath::rev_eng::InternalReverseEngineeredObject
@@ -158,6 +166,15 @@ TEST(unit_test_reverse_enineering, call_virtual_method_delegates_to_call_method)
EXPECT_EQ(2, rev->rev_bar_const());
}
TEST(unit_test_reverse_enineering, multi_player_base_b_offset_is_correct)
{
// Verify our compile-time offset matches the actual layout
MultiPlayer mp;
const auto* mp_addr = reinterpret_cast<const char*>(&mp);
const auto* b_addr = reinterpret_cast<const char*>(static_cast<const BaseB*>(&mp));
EXPECT_EQ(b_addr - mp_addr, BASE_B_OFFSET);
}
TEST(unit_test_reverse_enineering, call_virtual_method_table_index_first_table)
{
MultiPlayer mp;
@@ -171,7 +188,7 @@ TEST(unit_test_reverse_enineering, call_virtual_method_table_index_first_table)
TEST(unit_test_reverse_enineering, call_virtual_method_table_index_second_table)
{
MultiPlayer mp;
constexpr MultiPlayer mp;
const auto* rev = reinterpret_cast<const RevMultiPlayer*>(&mp);
EXPECT_EQ(mp.get_b(), rev->rev_get_b());
@@ -192,7 +209,7 @@ TEST(unit_test_reverse_enineering, call_virtual_method_table_index_non_const)
TEST(unit_test_reverse_enineering, call_virtual_method_table_zero_matches_default)
{
// Table 0 with the TableIndex overload should match the original non-TableIndex overload
MultiPlayer mp;
constexpr MultiPlayer mp;
const auto* rev = reinterpret_cast<const RevMultiPlayer*>(&mp);
// Both access table 0, method index 1 — should return the same value