Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/omath/engines/unreal_engine/camera.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@

namespace omath::unreal_engine
{
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE, {.inverted_right = true}>;
using Camera = projection::Camera<Mat4X4, ViewAngles, CameraTrait, NDCDepthRange::ZERO_TO_ONE>;
} // namespace omath::unreal_engine
53 changes: 53 additions & 0 deletions include/omath/linear_algebra/mat.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,59 @@ namespace omath
else
std::unreachable();
}

// Horizontal-FOV variants — use these when the engine reports FOV as
// horizontal (UE's FMinimalViewInfo::FOV, Quake-family fov_x, etc.).
// X and Y scales derived as: X = 1 / tan(hfov/2), Y = aspect / tan(hfov/2).
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_horizontal_fov(const float horizontal_fov,
const float aspect_ratio, const float near,
const float far) noexcept
{
const float inv_tan_half_hfov = 1.f / std::tan(angles::degrees_to_radians(horizontal_fov) / 2.f);
const float x_axis = inv_tan_half_hfov;
const float y_axis = inv_tan_half_hfov * aspect_ratio;

if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{x_axis, 0.f, 0.f, 0.f},
{0.f, y_axis, 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 {{x_axis, 0.f, 0.f, 0.f},
{0.f, y_axis, 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,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Mat<4, 4, Type, St> mat_perspective_right_handed_horizontal_fov(const float horizontal_fov,
const float aspect_ratio, const float near,
const float far) noexcept
{
const float inv_tan_half_hfov = 1.f / std::tan(angles::degrees_to_radians(horizontal_fov) / 2.f);
const float x_axis = inv_tan_half_hfov;
const float y_axis = inv_tan_half_hfov * aspect_ratio;

if constexpr (DepthRange == NDCDepthRange::ZERO_TO_ONE)
return {{x_axis, 0.f, 0.f, 0.f},
{0.f, y_axis, 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 {{x_axis, 0.f, 0.f, 0.f},
{0.f, y_axis, 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,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]]
Expand Down
31 changes: 15 additions & 16 deletions source/engines/source_engine/formulas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,24 @@ namespace omath::source_engine
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
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;
// Source (inherited from Quake) stores FOV as horizontal FOV at a 4:3
// reference aspect. Convert to true vertical FOV, then delegate to the
// standard vertical-FOV left-handed builder with the caller's actual
// aspect ratio.
// vfov = 2 · atan( tan(hfov_4:3 / 2) / (4/3) )
constexpr float k_source_reference_aspect = 4.f / 3.f;
const float half_hfov_4_3 = angles::degrees_to_radians(field_of_view) / 2.f;
const float vfov_deg = angles::radians_to_degrees(
2.f * std::atan(std::tan(half_hfov_4_3) / k_source_reference_aspect));

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},
};
return mat_perspective_left_handed<
float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
vfov_deg, aspect_ratio, near, far);
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},
};
return mat_perspective_left_handed<
float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
vfov_deg, aspect_ratio, near, far);
std::unreachable();
}
} // namespace omath::source_engine
27 changes: 19 additions & 8 deletions source/engines/unreal_engine/formulas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Created by Vlad on 3/22/2025.
//
#include "omath/engines/unreal_engine/formulas.hpp"

namespace omath::unreal_engine
{
Vector3<float> forward_vector(const ViewAngles& angles) noexcept
Expand All @@ -25,22 +24,34 @@ namespace omath::unreal_engine
}
Mat4X4 calc_view_matrix(const ViewAngles& angles, const Vector3<float>& cam_origin) noexcept
{
return mat_camera_view<float, MatStoreType::ROW_MAJOR>(forward_vector(angles), -right_vector(angles),
return mat_camera_view<float, MatStoreType::ROW_MAJOR>(forward_vector(angles), right_vector(angles),
up_vector(angles), cam_origin);
}
Mat4X4 rotation_matrix(const ViewAngles& angles) noexcept
{
return mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(angles.roll)
* mat_rotation_axis_z<float, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(-angles.pitch);
// UE FRotator is intrinsic Z-Y-X (Yaw → Pitch → Roll applied in local
// frame), which for column-vector composition is Rz·Ry·Rx.
// Pitch and roll axes in omath spin opposite to UE's convention, so
// both carry a sign flip.
return mat_rotation_axis_z<float, MatStoreType::ROW_MAJOR>(angles.yaw)
* mat_rotation_axis_y<float, MatStoreType::ROW_MAJOR>(-angles.pitch)
* mat_rotation_axis_x<float, MatStoreType::ROW_MAJOR>(-angles.roll);
}


Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near,
const float far, const NDCDepthRange ndc_depth_range) noexcept
{
// UE stores horizontal FOV in FMinimalViewInfo — use the left-handed
// horizontal-FOV builder directly.
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return mat_perspective_left_handed<float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
return mat_perspective_left_handed_horizontal_fov<
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);
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return mat_perspective_left_handed_horizontal_fov<
float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
field_of_view, aspect_ratio, near, far);
std::unreachable();
}
} // namespace omath::unreal_engine
2 changes: 1 addition & 1 deletion tests/engines/unit_test_unreal_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ TEST(unit_test_unreal_engine, ForwardVectorRotationRoll)
{
omath::unreal_engine::ViewAngles angles;

angles.roll = omath::unreal_engine::RollAngle::from_degrees(-90.f);
angles.roll = omath::unreal_engine::RollAngle::from_degrees(90.f);

const auto forward = omath::unreal_engine::up_vector(angles);
EXPECT_NEAR(forward.x, omath::unreal_engine::k_abs_right.x, 0.00001f);
Expand Down
Loading