diff --git a/celestia.cfg.in b/celestia.cfg.in index 2e1d7efb4f..95d71e04d0 100644 --- a/celestia.cfg.in +++ b/celestia.cfg.in @@ -527,4 +527,11 @@ StarTextures #------------------------------------------------------------------------ # SRGBRendering true +#------------------------------------------------------------------------ +# ReverseZ replaces depth-buffer partitioning with reverse-Z + an infinite +# far plane. Requires ARB_clip_control / EXT_clip_control (or GL 4.5+); +# falls back to standard partitioning if unavailable. +# The default value is false. +#------------------------------------------------------------------------ +# ReverseZ true } diff --git a/src/celengine/fisheyeprojectionmode.cpp b/src/celengine/fisheyeprojectionmode.cpp index c389c45322..09a2169ec5 100644 --- a/src/celengine/fisheyeprojectionmode.cpp +++ b/src/celengine/fisheyeprojectionmode.cpp @@ -8,6 +8,7 @@ // of the License, or (at your option) any later version. #include "fisheyeprojectionmode.h" +#include #include #include #include @@ -25,6 +26,17 @@ FisheyeProjectionMode::FisheyeProjectionMode(float width, float height, int scre Eigen::Matrix4f FisheyeProjectionMode::getProjectionMatrix(float nearZ, float farZ, float /*zoom*/) const { float aspectRatio = width / height; + if (gl::reverseZ) + { + // Reverse-Z, output z in [0, +1]. Only row 2 differs from standard + // Ortho — patch it in place. + Eigen::Matrix4f m = math::Ortho(-aspectRatio, aspectRatio, + -1.0f, 1.0f, nearZ, farZ); + float d = farZ - nearZ; + m(2, 2) = 1.0f / d; + m(2, 3) = farZ / d; + return m; + } return math::Ortho(-aspectRatio, aspectRatio, -1.0f, 1.0f, nearZ, farZ); } @@ -80,6 +92,11 @@ float FisheyeProjectionMode::getNormalizedDeviceZ(float nearZ, float farZ, float // just apply a linear transformation since fisheye uses // orthographic projection already. float d0 = farZ - nearZ; + if (gl::reverseZ) + { + // Mirrors the near/far swap in getProjectionMatrix. + return (z - nearZ) / d0 * 2.0f - 1.0f; + } return 1.0f - (z - nearZ) / d0 * 2.0f; } diff --git a/src/celengine/glsupport.cpp b/src/celengine/glsupport.cpp index 7770f01736..247025898f 100644 --- a/src/celengine/glsupport.cpp +++ b/src/celengine/glsupport.cpp @@ -14,7 +14,9 @@ CELAPI bool OES_geometry_shader = false; //NOSONAR #else CELAPI bool ARB_invalidate_subdata = false; //NOSONAR #endif +CELAPI bool ARB_clip_control = false; //NOSONAR CELAPI bool ARB_texture_compression_bptc = false; //NOSONAR +CELAPI bool EXT_clip_control = false; //NOSONAR CELAPI bool EXT_texture_compression_s3tc = false; //NOSONAR CELAPI bool EXT_texture_compression_s3tc_srgb = false; //NOSONAR CELAPI bool EXT_texture_filter_anisotropic = false; //NOSONAR @@ -25,6 +27,7 @@ CELAPI GLint maxTextureSize = 0; //NOSONAR CELAPI GLfloat maxLineWidth = 0.0f; //NOSONAR CELAPI GLint maxTextureAnisotropy = 0; //NOSONAR CELAPI bool sRGBRendering = false; //NOSONAR +CELAPI bool reverseZ = false; //NOSONAR namespace { @@ -102,6 +105,13 @@ bool init(util::array_view ignore) noexcept EXT_texture_filter_anisotropic = check_extension(ignore, "GL_EXT_texture_filter_anisotropic") || check_extension(ignore, "GL_ARB_texture_filter_anisotropic"); EXT_texture_sRGB_R8 = check_extension(ignore, "GL_EXT_texture_sRGB_R8"); MESA_pack_invert = check_extension(ignore, "GL_MESA_pack_invert"); +#ifdef GL_ES + EXT_clip_control = check_extension(ignore, "GL_EXT_clip_control"); +#else + ARB_clip_control = check_extension(ignore, "GL_ARB_clip_control") + || checkVersion(celestia::gl::GL_4_5); + EXT_clip_control = check_extension(ignore, "GL_EXT_clip_control"); +#endif std::array pointSizeRange = { 0, 0 }; std::array lineWidthRange = { 0.0f, 0.0f }; diff --git a/src/celengine/glsupport.h b/src/celengine/glsupport.h index 8f55216983..0a716a496e 100644 --- a/src/celengine/glsupport.h +++ b/src/celengine/glsupport.h @@ -47,7 +47,9 @@ enum Version #ifndef GL_ES extern CELAPI bool ARB_invalidate_subdata; //NOSONAR #endif +extern CELAPI bool ARB_clip_control; //NOSONAR extern CELAPI bool ARB_texture_compression_bptc; //NOSONAR +extern CELAPI bool EXT_clip_control; //NOSONAR extern CELAPI bool EXT_texture_compression_s3tc; //NOSONAR extern CELAPI bool EXT_texture_compression_s3tc_srgb; //NOSONAR extern CELAPI bool EXT_texture_filter_anisotropic; //NOSONAR @@ -62,6 +64,7 @@ extern CELAPI GLint maxTextureSize; //NOSONAR extern CELAPI GLfloat maxLineWidth; //NOSONAR extern CELAPI GLint maxTextureAnisotropy; //NOSONAR extern CELAPI bool sRGBRendering; //NOSONAR +extern CELAPI bool reverseZ; //NOSONAR bool init(util::array_view = {}) noexcept; bool checkVersion(int) noexcept; diff --git a/src/celengine/perspectiveprojectionmode.cpp b/src/celengine/perspectiveprojectionmode.cpp index d30242b881..7bd10a6f2a 100644 --- a/src/celengine/perspectiveprojectionmode.cpp +++ b/src/celengine/perspectiveprojectionmode.cpp @@ -8,6 +8,7 @@ // of the License, or (at your option) any later version. #include "perspectiveprojectionmode.h" +#include #include #include #include @@ -22,6 +23,11 @@ PerspectiveProjectionMode::PerspectiveProjectionMode(float width, float height, Eigen::Matrix4f PerspectiveProjectionMode::getProjectionMatrix(float nearZ, float farZ, float zoom) const { + if (gl::reverseZ) + { + return math::PerspectiveReverseZInfiniteZeroToOne( + math::radToDeg(getFOV(zoom)), width / height, nearZ); + } return math::Perspective(math::radToDeg(getFOV(zoom)), width / height, nearZ, farZ); } @@ -79,6 +85,11 @@ double PerspectiveProjectionMode::getViewConeAngleMax(float zoom) const float PerspectiveProjectionMode::getNormalizedDeviceZ(float nearZ, float farZ, float z) const { + if (gl::reverseZ) + { + // -1 at near, +1 at far; Ortho2D negates this for the annotation pass. + return 1.0f - 2.0f * nearZ / z; + } float d0 = farZ - nearZ; float d1 = -(farZ + nearZ) / d0; float d2 = -2.0f * nearZ * farZ / d0; diff --git a/src/celengine/render.cpp b/src/celengine/render.cpp index d62362c0e2..8c6d1554e0 100644 --- a/src/celengine/render.cpp +++ b/src/celengine/render.cpp @@ -439,8 +439,25 @@ bool Renderer::init(int winWidth, int winHeight, detailOptions.useMesaPackInvert = false; #endif - // LEQUAL rather than LESS required for multipass rendering - glDepthFunc(GL_LEQUAL); + // Reverse-Z needs clip-control for ZERO_TO_ONE NDC; without it, fall back + // to standard partitioning rather than the precision-poor [-1, +1] variant. + if (gl::reverseZ && !(gl::ARB_clip_control || gl::EXT_clip_control)) + { + gl::reverseZ = false; + GetLogger()->warn("ReverseZ requested but clip-control is unavailable; falling back to standard depth.\n"); + } + + if (gl::reverseZ) + { + glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); + glDepthFunc(GL_GEQUAL); + glClearDepthf(0.0f); + } + else + { + glDepthFunc(GL_LEQUAL); + glClearDepthf(1.0f); + } resize(winWidth, winHeight); @@ -454,6 +471,12 @@ void Renderer::resize(int width, int height) viewportHeight = height; projectionMode->setSize(static_cast(viewportWidth), static_cast(viewportHeight)); m_orthoProjMatrix = math::Ortho2D(0.0f, static_cast(viewportWidth), 0.0f, static_cast(viewportHeight)); + if (gl::reverseZ) + { + // Remap Ortho2D z output from [-1, +1] to [0, +1] reverse-Z. + m_orthoProjMatrix(2, 2) = -0.5f; + m_orthoProjMatrix(2, 3) = 0.5f; + } } void Renderer::setFieldOfView(float _fov) @@ -2548,14 +2571,13 @@ void Renderer::renderObject(const Vector3f& pos, cloudTex->bind(); - // Cloud layers can be trouble for the depth buffer, since they tend - // to be very close to the surface of a planet relative to the radius - // of the planet. We'll help out by offsetting the cloud layer toward - // the viewer. + // Pull clouds toward the viewer to avoid z-fighting with the surface + // (sign flipped under reverse-Z where closer = larger depth). if (distance > radius * 1.1f) { glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(-1.0f, -1.0f); + float offset = gl::reverseZ ? 1.0f : -1.0f; + glPolygonOffset(offset, offset); } if (lit) @@ -5556,6 +5578,31 @@ Renderer::buildDepthPartitions() // We want to avoid overpartitioning the depth buffer. In this stage, we // coalesce partitions that have small spans in the depth buffer. // TODO: Implement this step! + + // Collapse partitions: reverse-Z gives uniform precision, so the per- + // interval depth-range squeeze is pure waste. + if (gl::reverseZ && nIntervals > 1) + { + float collapsedNearZ = depthPartitions[0].nearZ; + float collapsedFarZ = depthPartitions[0].farZ; + for (int j = 1; j < nIntervals; j++) + { + collapsedNearZ = std::max(collapsedNearZ, depthPartitions[j].nearZ); + collapsedFarZ = std::min(collapsedFarZ, depthPartitions[j].farZ); + } + // Floor at -MinNearPlaneDistance: fp32 noise around 0 in the closest + // partition can flip nearZ positive, which makes the reverse-Z near + // negative and surface depths jitter near vs far frame to frame. + collapsedNearZ = std::min(collapsedNearZ, -MinNearPlaneDistance); + depthPartitions.clear(); + DepthBufferPartition collapsed; + collapsed.index = 0; + collapsed.nearZ = collapsedNearZ; + collapsed.farZ = collapsedFarZ; + depthPartitions.push_back(collapsed); + nIntervals = 1; + } + return nIntervals; } diff --git a/src/celengine/renderglsl.cpp b/src/celengine/renderglsl.cpp index ba3d7350c3..a5af7554b1 100644 --- a/src/celengine/renderglsl.cpp +++ b/src/celengine/renderglsl.cpp @@ -85,6 +85,16 @@ void renderGeometryShadow_GLSL(RenderGeometry* geometry, shadowFbo->bind(); glViewport(0, 0, shadowFbo->width(), shadowFbo->height()); + // The shadow pass is forward-Z throughout (Ortho [-1,+1] NDC, clear=1, + // LEQUAL, positive polygon offset, sampler does `myDepth > pcfDepth`). + // Isolate it from the global reverseZ state. + if (gl::reverseZ) + { + glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE); + glDepthFunc(GL_LEQUAL); + glClearDepthf(1.0f); + } + // Write only to the depth buffer glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glClear(GL_DEPTH_BUFFER_BIT); @@ -115,6 +125,14 @@ void renderGeometryShadow_GLSL(RenderGeometry* geometry, glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glCullFace(GL_BACK); shadowFbo->unbind(oldFboId); + + // Restore reverseZ globals. + if (gl::reverseZ) + { + glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); + glDepthFunc(GL_GEQUAL); + glClearDepthf(0.0f); + } } } // end unnamed namespace diff --git a/src/celestia/celestiacore.cpp b/src/celestia/celestiacore.cpp index 38b8da96b5..fc79320ecd 100644 --- a/src/celestia/celestiacore.cpp +++ b/src/celestia/celestiacore.cpp @@ -2741,9 +2741,11 @@ LoadFontHelper(const Renderer* renderer, bool CelestiaCore::initRenderer(engine::TextureResolution resolution, std::optional sRGBRendering, + std::optional reverseZ, [[maybe_unused]] bool useMesaPackInvert) { gl::sRGBRendering = sRGBRendering.value_or(config->renderDetails.sRGBRendering); + gl::reverseZ = reverseZ.value_or(config->renderDetails.reverseZ); if (gl::sRGBRendering) { diff --git a/src/celestia/celestiacore.h b/src/celestia/celestiacore.h index 21d3b31bba..a649549722 100644 --- a/src/celestia/celestiacore.h +++ b/src/celestia/celestiacore.h @@ -193,6 +193,7 @@ class CelestiaCore // : public Watchable ProgressNotifier* progressNotifier = nullptr); bool initRenderer(celestia::engine::TextureResolution, std::optional sRGBRendering = std::nullopt, + std::optional reverseZ = std::nullopt, bool useMesaPackInvert = true); void start(double t); void start(); diff --git a/src/celestia/configfile.cpp b/src/celestia/configfile.cpp index 450ca1ec77..e4a46442b5 100644 --- a/src/celestia/configfile.cpp +++ b/src/celestia/configfile.cpp @@ -202,6 +202,7 @@ applyRenderDetails(CelestiaConfig::RenderDetails& renderDetails, const Associati applyNumber(renderDetails.ShadowMapSize, hash, "ShadowMapSize"sv); applyStringArray(renderDetails.ignoreGLExtensions, hash, "IgnoreGLExtensions"sv); applyBoolean(renderDetails.sRGBRendering, hash, "SRGBRendering"sv); + applyBoolean(renderDetails.reverseZ, hash, "ReverseZ"sv); applyNumber(renderDetails.stars.pointRadius, hash, "StarPointRadius"sv); renderDetails.stars.pointRadius = std::clamp(renderDetails.stars.pointRadius, 1.0f, 10.0f); applyNumber(renderDetails.stars.optimization, hash, "StarOptimization"sv); diff --git a/src/celestia/configfile.h b/src/celestia/configfile.h index 5c954a220d..a834c5d335 100644 --- a/src/celestia/configfile.h +++ b/src/celestia/configfile.h @@ -94,6 +94,7 @@ struct CelestiaConfig unsigned int ShadowMapSize{ 0 }; std::vector ignoreGLExtensions{ }; bool sRGBRendering{ false }; + bool reverseZ{ false }; struct StarRendering { float pointRadius{ 1.5f }; diff --git a/src/celestia/qt/qtglwidget.cpp b/src/celestia/qt/qtglwidget.cpp index a73e30f436..5fd36eb0e2 100644 --- a/src/celestia/qt/qtglwidget.cpp +++ b/src/celestia/qt/qtglwidget.cpp @@ -139,7 +139,12 @@ CelestiaGlWidget::initializeGL() if (settings.contains("sRGBRendering")) sRGBOverride = settings.value("sRGBRendering").toBool(); - if (!appCore->initRenderer(static_cast(textureResolution), sRGBOverride, false)) + std::optional reverseZOverride; + if (settings.contains("ReverseZ")) + reverseZOverride = settings.value("ReverseZ").toBool(); + + if (!appCore->initRenderer(static_cast(textureResolution), + sRGBOverride, reverseZOverride, false)) { // cerr << "Failed to initialize renderer.\n"; exit(1); diff --git a/src/celmath/geomutil.h b/src/celmath/geomutil.h index b21b78c970..485d098cd7 100644 --- a/src/celmath/geomutil.h +++ b/src/celmath/geomutil.h @@ -217,6 +217,30 @@ Perspective(T fovy, T aspect, T nearZ, T farZ) return m; } +/*! Reverse-Z infinite-far perspective for [0, +1] NDC; requires glClipControl(ZERO_TO_ONE). */ +template Eigen::Matrix +PerspectiveReverseZInfiniteZeroToOne(T fovy, T aspect, T nearZ) +{ + using std::cos, std::sin; + + if (aspect == static_cast(0)) + return Eigen::Matrix::Identity(); + + T angle = degToRad(fovy / static_cast(2)); + T sine = sin(angle); + if (sine == static_cast(0)) + return Eigen::Matrix::Identity(); + T ctg = cos(angle) / sine; + + Eigen::Matrix m = Eigen::Matrix::Zero(); + m(0, 0) = ctg / aspect; + m(1, 1) = ctg; + m(2, 2) = static_cast(0); + m(2, 3) = nearZ; + m(3, 2) = static_cast(-1); + return m; +} + /*! Return an orthographic projection matrix */ template Eigen::Matrix