updated formulas

This commit is contained in:
2026-04-23 19:48:55 +03:00
parent 42a8a5a763
commit b3ba9eaadf
3 changed files with 72 additions and 38 deletions

View File

@@ -707,6 +707,59 @@ namespace omath
else else
std::unreachable(); 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, template<class Type = float, MatStoreType St = MatStoreType::ROW_MAJOR,
NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE> NDCDepthRange DepthRange = NDCDepthRange::NEGATIVE_ONE_TO_ONE>
[[nodiscard]] [[nodiscard]]

View File

@@ -39,30 +39,23 @@ namespace omath::source_engine
const float far, const NDCDepthRange ndc_depth_range) noexcept const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
// Source (inherited from Quake) stores FOV as horizontal FOV at a 4:3 // Source (inherited from Quake) stores FOV as horizontal FOV at a 4:3
// reference aspect. Convert to vertical FOV first, then use the // reference aspect. Convert to true vertical FOV, then delegate to the
// standard vfov-based projection against the caller's actual aspect. // standard vertical-FOV left-handed builder with the caller's actual
// aspect ratio.
// vfov = 2 · atan( tan(hfov_4:3 / 2) / (4/3) ) // vfov = 2 · atan( tan(hfov_4:3 / 2) / (4/3) )
constexpr float k_source_reference_aspect = 4.f / 3.f; 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 half_hfov_4_3 = angles::degrees_to_radians(field_of_view) / 2.f;
const float tan_half_vfov = std::tan(half_hfov_4_3) / k_source_reference_aspect; const float vfov_deg = angles::radians_to_degrees(
2.f * std::atan(std::tan(half_hfov_4_3) / k_source_reference_aspect));
const float x_axis = 1.f / (aspect_ratio * tan_half_vfov);
const float y_axis = 1.f / tan_half_vfov;
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return { return mat_perspective_left_handed<
{x_axis, 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
{0, y_axis, 0, 0}, vfov_deg, aspect_ratio, near, far);
{0, 0, far / (far - near), -(near * far) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return { return mat_perspective_left_handed<
{x_axis, 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
{0, y_axis, 0, 0}, vfov_deg, aspect_ratio, near, far);
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
std::unreachable(); std::unreachable();
} }
} // namespace omath::source_engine } // namespace omath::source_engine

View File

@@ -38,28 +38,16 @@ namespace omath::unreal_engine
Mat4X4 calc_perspective_projection_matrix(const float field_of_view, const float aspect_ratio, const float near, 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 const float far, const NDCDepthRange ndc_depth_range) noexcept
{ {
// UE stores horizontal FOV in FMinimalViewInfo — mirror engine's // UE stores horizontal FOV in FMinimalViewInfo — use the left-handed
// FMinimalViewInfo::CalculateProjectionMatrixGivenViewRectangle: // horizontal-FOV builder directly.
// XAxisMultiplier = 1 / tan(hfov/2)
// YAxisMultiplier = aspect / tan(hfov/2)
const float inv_tan_half_hfov = 1.f / std::tan(angles::degrees_to_radians(field_of_view) / 2.f);
const float x_axis = inv_tan_half_hfov;
const float y_axis = inv_tan_half_hfov * aspect_ratio;
if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE) if (ndc_depth_range == NDCDepthRange::ZERO_TO_ONE)
return { return mat_perspective_left_handed_horizontal_fov<
{x_axis, 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::ZERO_TO_ONE>(
{0, y_axis, 0, 0}, field_of_view, aspect_ratio, near, far);
{0, 0, far / (far - near), -(near * far) / (far - near)},
{0, 0, 1, 0},
};
if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE) if (ndc_depth_range == NDCDepthRange::NEGATIVE_ONE_TO_ONE)
return { return mat_perspective_left_handed_horizontal_fov<
{x_axis, 0, 0, 0}, float, MatStoreType::ROW_MAJOR, NDCDepthRange::NEGATIVE_ONE_TO_ONE>(
{0, y_axis, 0, 0}, field_of_view, aspect_ratio, near, far);
{0, 0, (far + near) / (far - near), -(2.f * far * near) / (far - near)},
{0, 0, 1, 0},
};
std::unreachable(); std::unreachable();
} }
} // namespace omath::unreal_engine } // namespace omath::unreal_engine