Skip to content

Commit e5957d4

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 dee601b commit e5957d4

13 files changed

Lines changed: 163 additions & 8 deletions

celestia.cfg.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,4 +520,11 @@ StarTextures
520520
#------------------------------------------------------------------------
521521
# SRGBRendering true
522522

523+
#------------------------------------------------------------------------
524+
# ReverseZ replaces depth-buffer partitioning with reverse-Z + an infinite
525+
# far plane. Requires ARB_clip_control / EXT_clip_control (or GL 4.5+);
526+
# falls back to standard partitioning if unavailable.
527+
# The default value is false.
528+
#------------------------------------------------------------------------
529+
# ReverseZ true
523530
}

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
@@ -12,7 +12,9 @@ namespace celestia::gl
1212
CELAPI bool OES_texture_border_clamp = false; //NOSONAR
1313
CELAPI bool OES_geometry_shader = false; //NOSONAR
1414
#endif
15+
CELAPI bool ARB_clip_control = false; //NOSONAR
1516
CELAPI bool ARB_texture_compression_bptc = false; //NOSONAR
17+
CELAPI bool EXT_clip_control = false; //NOSONAR
1618
CELAPI bool EXT_texture_compression_s3tc = false; //NOSONAR
1719
CELAPI bool EXT_texture_compression_s3tc_srgb = false; //NOSONAR
1820
CELAPI bool EXT_texture_filter_anisotropic = false; //NOSONAR
@@ -23,6 +25,7 @@ CELAPI GLint maxTextureSize = 0; //NOSONAR
2325
CELAPI GLfloat maxLineWidth = 0.0f; //NOSONAR
2426
CELAPI GLint maxTextureAnisotropy = 0; //NOSONAR
2527
CELAPI bool sRGBRendering = false; //NOSONAR
28+
CELAPI bool reverseZ = false; //NOSONAR
2629

2730
namespace
2831
{
@@ -99,6 +102,13 @@ bool init(util::array_view<std::string> ignore) noexcept
99102
EXT_texture_filter_anisotropic = check_extension(ignore, "GL_EXT_texture_filter_anisotropic") || check_extension(ignore, "GL_ARB_texture_filter_anisotropic");
100103
EXT_texture_sRGB_R8 = check_extension(ignore, "GL_EXT_texture_sRGB_R8");
101104
MESA_pack_invert = check_extension(ignore, "GL_MESA_pack_invert");
105+
#ifdef GL_ES
106+
EXT_clip_control = check_extension(ignore, "GL_EXT_clip_control");
107+
#else
108+
ARB_clip_control = check_extension(ignore, "GL_ARB_clip_control")
109+
|| checkVersion(celestia::gl::GL_4_5);
110+
EXT_clip_control = check_extension(ignore, "GL_EXT_clip_control");
111+
#endif
102112

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

src/celengine/glsupport.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,23 @@ enum Version
3030
GL_3_1 = 31,
3131
GL_3_2 = 32,
3232
GL_3_3 = 33,
33+
GL_4 = 40,
34+
GL_4_0 = 40,
35+
GL_4_1 = 41,
36+
GL_4_2 = 42,
37+
GL_4_3 = 43,
38+
GL_4_4 = 44,
39+
GL_4_5 = 45,
40+
GL_4_6 = 46,
3341
GLES_3 = 30,
3442
GLES_3_0 = 30,
3543
GLES_3_1 = 31,
3644
GLES_3_2 = 32,
3745
};
3846

47+
extern CELAPI bool ARB_clip_control; //NOSONAR
3948
extern CELAPI bool ARB_texture_compression_bptc; //NOSONAR
49+
extern CELAPI bool EXT_clip_control; //NOSONAR
4050
extern CELAPI bool EXT_texture_compression_s3tc; //NOSONAR
4151
extern CELAPI bool EXT_texture_compression_s3tc_srgb; //NOSONAR
4252
extern CELAPI bool EXT_texture_filter_anisotropic; //NOSONAR
@@ -51,6 +61,7 @@ extern CELAPI GLint maxTextureSize; //NOSONAR
5161
extern CELAPI GLfloat maxLineWidth; //NOSONAR
5262
extern CELAPI GLint maxTextureAnisotropy; //NOSONAR
5363
extern CELAPI bool sRGBRendering; //NOSONAR
64+
extern CELAPI bool reverseZ; //NOSONAR
5465

5566
bool init(util::array_view<std::string> = {}) noexcept;
5667
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)
@@ -2555,14 +2578,13 @@ void Renderer::renderObject(const Vector3f& pos,
25552578

25562579
cloudTex->bind();
25572580

2558-
// Cloud layers can be trouble for the depth buffer, since they tend
2559-
// to be very close to the surface of a planet relative to the radius
2560-
// of the planet. We'll help out by offsetting the cloud layer toward
2561-
// the viewer.
2581+
// Pull clouds toward the viewer to avoid z-fighting with the surface
2582+
// (sign flipped under reverse-Z where closer = larger depth).
25622583
if (distance > radius * 1.1f)
25632584
{
25642585
glEnable(GL_POLYGON_OFFSET_FILL);
2565-
glPolygonOffset(-1.0f, -1.0f);
2586+
float offset = gl::reverseZ ? 1.0f : -1.0f;
2587+
glPolygonOffset(offset, offset);
25662588
}
25672589

25682590
if (lit)
@@ -5447,6 +5469,31 @@ Renderer::buildDepthPartitions()
54475469
// We want to avoid overpartitioning the depth buffer. In this stage, we
54485470
// coalesce partitions that have small spans in the depth buffer.
54495471
// TODO: Implement this step!
5472+
5473+
// Collapse partitions: reverse-Z gives uniform precision, so the per-
5474+
// interval depth-range squeeze is pure waste.
5475+
if (gl::reverseZ && nIntervals > 1)
5476+
{
5477+
float collapsedNearZ = depthPartitions[0].nearZ;
5478+
float collapsedFarZ = depthPartitions[0].farZ;
5479+
for (int j = 1; j < nIntervals; j++)
5480+
{
5481+
collapsedNearZ = std::max(collapsedNearZ, depthPartitions[j].nearZ);
5482+
collapsedFarZ = std::min(collapsedFarZ, depthPartitions[j].farZ);
5483+
}
5484+
// Floor at -MinNearPlaneDistance: fp32 noise around 0 in the closest
5485+
// partition can flip nearZ positive, which makes the reverse-Z near
5486+
// negative and surface depths jitter near vs far frame to frame.
5487+
collapsedNearZ = std::min(collapsedNearZ, -MinNearPlaneDistance);
5488+
depthPartitions.clear();
5489+
DepthBufferPartition collapsed;
5490+
collapsed.index = 0;
5491+
collapsed.nearZ = collapsedNearZ;
5492+
collapsed.farZ = collapsedFarZ;
5493+
depthPartitions.push_back(collapsed);
5494+
nIntervals = 1;
5495+
}
5496+
54505497
return nIntervals;
54515498
}
54525499

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
@@ -2733,9 +2733,11 @@ LoadFontHelper(const Renderer* renderer,
27332733

27342734
bool CelestiaCore::initRenderer(engine::TextureResolution resolution,
27352735
std::optional<bool> sRGBRendering,
2736+
std::optional<bool> reverseZ,
27362737
[[maybe_unused]] bool useMesaPackInvert)
27372738
{
27382739
gl::sRGBRendering = sRGBRendering.value_or(config->renderDetails.sRGBRendering);
2740+
gl::reverseZ = reverseZ.value_or(config->renderDetails.reverseZ);
27392741

27402742
if (gl::sRGBRendering)
27412743
{

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)