Skip to content

Commit cba868d

Browse files
committed
Add reverse-Z + infinite-far projection path
ReverseZ + glClipControl(ZERO_TO_ONE) + GL_GEQUAL gives uniform fp32 depth precision across the entire scene, eliminating the B-coefficient cancellation that motivated multi-interval depth partitioning at far distances.
1 parent 5fedffb commit cba868d

13 files changed

Lines changed: 155 additions & 8 deletions

celestia.cfg.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,4 +527,11 @@ StarTextures
527527
#------------------------------------------------------------------------
528528
# SRGBRendering true
529529

530+
#------------------------------------------------------------------------
531+
# ReverseZ replaces depth-buffer partitioning with reverse-Z + an infinite
532+
# far plane. Requires ARB_clip_control / EXT_clip_control (or GL 4.5+);
533+
# falls back to standard partitioning if unavailable.
534+
# The default value is false.
535+
#------------------------------------------------------------------------
536+
# ReverseZ true
530537
}

src/celengine/fisheyeprojectionmode.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// of the License, or (at your option) any later version.
99

1010
#include "fisheyeprojectionmode.h"
11+
#include <celengine/glsupport.h>
1112
#include <celmath/frustum.h>
1213
#include <celmath/geomutil.h>
1314
#include <celengine/shadermanager.h>
@@ -25,6 +26,17 @@ FisheyeProjectionMode::FisheyeProjectionMode(float width, float height, int scre
2526
Eigen::Matrix4f FisheyeProjectionMode::getProjectionMatrix(float nearZ, float farZ, float /*zoom*/) const
2627
{
2728
float aspectRatio = width / height;
29+
if (gl::reverseZ)
30+
{
31+
// Reverse-Z, output z in [0, +1]. Only row 2 differs from standard
32+
// Ortho — patch it in place.
33+
Eigen::Matrix4f m = math::Ortho(-aspectRatio, aspectRatio,
34+
-1.0f, 1.0f, nearZ, farZ);
35+
float d = farZ - nearZ;
36+
m(2, 2) = 1.0f / d;
37+
m(2, 3) = farZ / d;
38+
return m;
39+
}
2840
return math::Ortho(-aspectRatio, aspectRatio, -1.0f, 1.0f, nearZ, farZ);
2941
}
3042

@@ -80,6 +92,11 @@ float FisheyeProjectionMode::getNormalizedDeviceZ(float nearZ, float farZ, float
8092
// just apply a linear transformation since fisheye uses
8193
// orthographic projection already.
8294
float d0 = farZ - nearZ;
95+
if (gl::reverseZ)
96+
{
97+
// Mirrors the near/far swap in getProjectionMatrix.
98+
return (z - nearZ) / d0 * 2.0f - 1.0f;
99+
}
83100
return 1.0f - (z - nearZ) / d0 * 2.0f;
84101
}
85102

src/celengine/glsupport.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ CELAPI bool OES_geometry_shader = false; //NOSONAR
1414
#else
1515
CELAPI bool ARB_invalidate_subdata = false; //NOSONAR
1616
#endif
17+
CELAPI bool ARB_clip_control = false; //NOSONAR
1718
CELAPI bool ARB_texture_compression_bptc = false; //NOSONAR
19+
CELAPI bool EXT_clip_control = false; //NOSONAR
1820
CELAPI bool EXT_texture_compression_s3tc = false; //NOSONAR
1921
CELAPI bool EXT_texture_compression_s3tc_srgb = false; //NOSONAR
2022
CELAPI bool EXT_texture_filter_anisotropic = false; //NOSONAR
@@ -25,6 +27,7 @@ CELAPI GLint maxTextureSize = 0; //NOSONAR
2527
CELAPI GLfloat maxLineWidth = 0.0f; //NOSONAR
2628
CELAPI GLint maxTextureAnisotropy = 0; //NOSONAR
2729
CELAPI bool sRGBRendering = false; //NOSONAR
30+
CELAPI bool reverseZ = false; //NOSONAR
2831

2932
namespace
3033
{
@@ -102,6 +105,13 @@ bool init(util::array_view<std::string> ignore) noexcept
102105
EXT_texture_filter_anisotropic = check_extension(ignore, "GL_EXT_texture_filter_anisotropic") || check_extension(ignore, "GL_ARB_texture_filter_anisotropic");
103106
EXT_texture_sRGB_R8 = check_extension(ignore, "GL_EXT_texture_sRGB_R8");
104107
MESA_pack_invert = check_extension(ignore, "GL_MESA_pack_invert");
108+
#ifdef GL_ES
109+
EXT_clip_control = check_extension(ignore, "GL_EXT_clip_control");
110+
#else
111+
ARB_clip_control = check_extension(ignore, "GL_ARB_clip_control")
112+
|| checkVersion(celestia::gl::GL_4_5);
113+
EXT_clip_control = check_extension(ignore, "GL_EXT_clip_control");
114+
#endif
105115

106116
std::array<GLint, 2> pointSizeRange = { 0, 0 };
107117
std::array<GLfloat, 2> lineWidthRange = { 0.0f, 0.0f };

src/celengine/glsupport.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ enum Version
4747
#ifndef GL_ES
4848
extern CELAPI bool ARB_invalidate_subdata; //NOSONAR
4949
#endif
50+
extern CELAPI bool ARB_clip_control; //NOSONAR
5051
extern CELAPI bool ARB_texture_compression_bptc; //NOSONAR
52+
extern CELAPI bool EXT_clip_control; //NOSONAR
5153
extern CELAPI bool EXT_texture_compression_s3tc; //NOSONAR
5254
extern CELAPI bool EXT_texture_compression_s3tc_srgb; //NOSONAR
5355
extern CELAPI bool EXT_texture_filter_anisotropic; //NOSONAR
@@ -62,6 +64,7 @@ extern CELAPI GLint maxTextureSize; //NOSONAR
6264
extern CELAPI GLfloat maxLineWidth; //NOSONAR
6365
extern CELAPI GLint maxTextureAnisotropy; //NOSONAR
6466
extern CELAPI bool sRGBRendering; //NOSONAR
67+
extern CELAPI bool reverseZ; //NOSONAR
6568

6669
bool init(util::array_view<std::string> = {}) noexcept;
6770
bool checkVersion(int) noexcept;

src/celengine/perspectiveprojectionmode.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// of the License, or (at your option) any later version.
99

1010
#include "perspectiveprojectionmode.h"
11+
#include <celengine/glsupport.h>
1112
#include <celmath/frustum.h>
1213
#include <celmath/geomutil.h>
1314
#include <celengine/shadermanager.h>
@@ -22,6 +23,11 @@ PerspectiveProjectionMode::PerspectiveProjectionMode(float width, float height,
2223

2324
Eigen::Matrix4f PerspectiveProjectionMode::getProjectionMatrix(float nearZ, float farZ, float zoom) const
2425
{
26+
if (gl::reverseZ)
27+
{
28+
return math::PerspectiveReverseZInfiniteZeroToOne(
29+
math::radToDeg(getFOV(zoom)), width / height, nearZ);
30+
}
2531
return math::Perspective(math::radToDeg(getFOV(zoom)), width / height, nearZ, farZ);
2632
}
2733

@@ -79,6 +85,11 @@ double PerspectiveProjectionMode::getViewConeAngleMax(float zoom) const
7985

8086
float PerspectiveProjectionMode::getNormalizedDeviceZ(float nearZ, float farZ, float z) const
8187
{
88+
if (gl::reverseZ)
89+
{
90+
// -1 at near, +1 at far; Ortho2D negates this for the annotation pass.
91+
return 1.0f - 2.0f * nearZ / z;
92+
}
8293
float d0 = farZ - nearZ;
8394
float d1 = -(farZ + nearZ) / d0;
8495
float d2 = -2.0f * nearZ * farZ / d0;

src/celengine/render.cpp

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,25 @@ bool Renderer::init(int winWidth, int winHeight,
439439
detailOptions.useMesaPackInvert = false;
440440
#endif
441441

442-
// LEQUAL rather than LESS required for multipass rendering
443-
glDepthFunc(GL_LEQUAL);
442+
// Reverse-Z needs clip-control for ZERO_TO_ONE NDC; without it, fall back
443+
// to standard partitioning rather than the precision-poor [-1, +1] variant.
444+
if (gl::reverseZ && !(gl::ARB_clip_control || gl::EXT_clip_control))
445+
{
446+
gl::reverseZ = false;
447+
GetLogger()->warn("ReverseZ requested but clip-control is unavailable; falling back to standard depth.\n");
448+
}
449+
450+
if (gl::reverseZ)
451+
{
452+
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
453+
glDepthFunc(GL_GEQUAL);
454+
glClearDepthf(0.0f);
455+
}
456+
else
457+
{
458+
glDepthFunc(GL_LEQUAL);
459+
glClearDepthf(1.0f);
460+
}
444461

445462
resize(winWidth, winHeight);
446463

@@ -454,6 +471,12 @@ void Renderer::resize(int width, int height)
454471
viewportHeight = height;
455472
projectionMode->setSize(static_cast<float>(viewportWidth), static_cast<float>(viewportHeight));
456473
m_orthoProjMatrix = math::Ortho2D(0.0f, static_cast<float>(viewportWidth), 0.0f, static_cast<float>(viewportHeight));
474+
if (gl::reverseZ)
475+
{
476+
// Remap Ortho2D z output from [-1, +1] to [0, +1] reverse-Z.
477+
m_orthoProjMatrix(2, 2) = -0.5f;
478+
m_orthoProjMatrix(2, 3) = 0.5f;
479+
}
457480
}
458481

459482
void Renderer::setFieldOfView(float _fov)
@@ -2548,14 +2571,13 @@ void Renderer::renderObject(const Vector3f& pos,
25482571

25492572
cloudTex->bind();
25502573

2551-
// Cloud layers can be trouble for the depth buffer, since they tend
2552-
// to be very close to the surface of a planet relative to the radius
2553-
// of the planet. We'll help out by offsetting the cloud layer toward
2554-
// the viewer.
2574+
// Pull clouds toward the viewer to avoid z-fighting with the surface
2575+
// (sign flipped under reverse-Z where closer = larger depth).
25552576
if (distance > radius * 1.1f)
25562577
{
25572578
glEnable(GL_POLYGON_OFFSET_FILL);
2558-
glPolygonOffset(-1.0f, -1.0f);
2579+
float offset = gl::reverseZ ? 1.0f : -1.0f;
2580+
glPolygonOffset(offset, offset);
25592581
}
25602582

25612583
if (lit)
@@ -5556,6 +5578,31 @@ Renderer::buildDepthPartitions()
55565578
// We want to avoid overpartitioning the depth buffer. In this stage, we
55575579
// coalesce partitions that have small spans in the depth buffer.
55585580
// TODO: Implement this step!
5581+
5582+
// Collapse partitions: reverse-Z gives uniform precision, so the per-
5583+
// interval depth-range squeeze is pure waste.
5584+
if (gl::reverseZ && nIntervals > 1)
5585+
{
5586+
float collapsedNearZ = depthPartitions[0].nearZ;
5587+
float collapsedFarZ = depthPartitions[0].farZ;
5588+
for (int j = 1; j < nIntervals; j++)
5589+
{
5590+
collapsedNearZ = std::max(collapsedNearZ, depthPartitions[j].nearZ);
5591+
collapsedFarZ = std::min(collapsedFarZ, depthPartitions[j].farZ);
5592+
}
5593+
// Floor at -MinNearPlaneDistance: fp32 noise around 0 in the closest
5594+
// partition can flip nearZ positive, which makes the reverse-Z near
5595+
// negative and surface depths jitter near vs far frame to frame.
5596+
collapsedNearZ = std::min(collapsedNearZ, -MinNearPlaneDistance);
5597+
depthPartitions.clear();
5598+
DepthBufferPartition collapsed;
5599+
collapsed.index = 0;
5600+
collapsed.nearZ = collapsedNearZ;
5601+
collapsed.farZ = collapsedFarZ;
5602+
depthPartitions.push_back(collapsed);
5603+
nIntervals = 1;
5604+
}
5605+
55595606
return nIntervals;
55605607
}
55615608

src/celengine/renderglsl.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ void renderGeometryShadow_GLSL(RenderGeometry* geometry,
8585
shadowFbo->bind();
8686
glViewport(0, 0, shadowFbo->width(), shadowFbo->height());
8787

88+
// The shadow pass is forward-Z throughout (Ortho [-1,+1] NDC, clear=1,
89+
// LEQUAL, positive polygon offset, sampler does `myDepth > pcfDepth`).
90+
// Isolate it from the global reverseZ state.
91+
if (gl::reverseZ)
92+
{
93+
glClipControl(GL_LOWER_LEFT, GL_NEGATIVE_ONE_TO_ONE);
94+
glDepthFunc(GL_LEQUAL);
95+
glClearDepthf(1.0f);
96+
}
97+
8898
// Write only to the depth buffer
8999
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
90100
glClear(GL_DEPTH_BUFFER_BIT);
@@ -115,6 +125,14 @@ void renderGeometryShadow_GLSL(RenderGeometry* geometry,
115125
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
116126
glCullFace(GL_BACK);
117127
shadowFbo->unbind(oldFboId);
128+
129+
// Restore reverseZ globals.
130+
if (gl::reverseZ)
131+
{
132+
glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE);
133+
glDepthFunc(GL_GEQUAL);
134+
glClearDepthf(0.0f);
135+
}
118136
}
119137

120138
} // end unnamed namespace

src/celestia/celestiacore.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2741,9 +2741,11 @@ LoadFontHelper(const Renderer* renderer,
27412741

27422742
bool CelestiaCore::initRenderer(engine::TextureResolution resolution,
27432743
std::optional<bool> sRGBRendering,
2744+
std::optional<bool> reverseZ,
27442745
[[maybe_unused]] bool useMesaPackInvert)
27452746
{
27462747
gl::sRGBRendering = sRGBRendering.value_or(config->renderDetails.sRGBRendering);
2748+
gl::reverseZ = reverseZ.value_or(config->renderDetails.reverseZ);
27472749

27482750
if (gl::sRGBRendering)
27492751
{

src/celestia/celestiacore.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class CelestiaCore // : public Watchable<CelestiaCore>
193193
ProgressNotifier* progressNotifier = nullptr);
194194
bool initRenderer(celestia::engine::TextureResolution,
195195
std::optional<bool> sRGBRendering = std::nullopt,
196+
std::optional<bool> reverseZ = std::nullopt,
196197
bool useMesaPackInvert = true);
197198
void start(double t);
198199
void start();

src/celestia/configfile.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ applyRenderDetails(CelestiaConfig::RenderDetails& renderDetails, const Associati
202202
applyNumber(renderDetails.ShadowMapSize, hash, "ShadowMapSize"sv);
203203
applyStringArray(renderDetails.ignoreGLExtensions, hash, "IgnoreGLExtensions"sv);
204204
applyBoolean(renderDetails.sRGBRendering, hash, "SRGBRendering"sv);
205+
applyBoolean(renderDetails.reverseZ, hash, "ReverseZ"sv);
205206
applyNumber(renderDetails.stars.pointRadius, hash, "StarPointRadius"sv);
206207
renderDetails.stars.pointRadius = std::clamp(renderDetails.stars.pointRadius, 1.0f, 10.0f);
207208
applyNumber(renderDetails.stars.optimization, hash, "StarOptimization"sv);

0 commit comments

Comments
 (0)