-
-
Notifications
You must be signed in to change notification settings - Fork 21.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for infinite projection matrix #95944
base: master
Are you sure you want to change the base?
Conversation
5d27f77
to
3287465
Compare
Does this introduce another change in behavior when working with clip space in shaders like reverse-z did, like e.g. reconstructing position from depth? Or does the math work out such that shaders are unchanged? I don't see any GLSL in the changed files. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, there is no impact on shaders.
In fact, the only thing this PR changes is how the frustum planes (and specifically the far plane) are extracted from the projection matrix. The matrix itself is passed through unchanged as before to the rest of the pipeline, including the GPU parts.
Frustum planes are extracted and used in a handful of culling operations, on the CPU, mainly geometry culling and light culling.
You can find them back by searching calls to Projection::get_projection_planes()
throughout the code.
This is really the only function affected by the PR, speaking of the core computational logic.
3287465
to
3490a89
Compare
Stumbled upon a few other places in Some of those functions are not used anywhere in the code but could still be called in user modules or custom builds :
I just updated the PR. |
Notes from the rendering weekly meeting : is it fine to “detect” infinite matrices from their values right into Projection, instead of passing through a flag set by Camera3D based on far value ? (I’ve seen a “is_orthogonal” flag passed through whereas orthogonality could have been likewise detected from the matrix components themselves)
any further test project needed to bullet proof the fix ? (thinking of light culling, fog rendering, gi, and other stuff that uses far plane extraction)
any other changes you may have in mind to fully unlock infinite far planes ? (thinking of 1 minor improvement : remove far distance capping in editor - but there might be more)
|
I am sorry, I can not help with tests on this one after all. Too much work and life issues now. |
Just rewrote the description of this PR to clarify what it is about exactly. Please refer to it as it may help with the below. To the points raised during the rendering meeting (assuming @clayjohn you made most of them) :
This propagates already by design : the current workflow is basically The only thing this PR changes is which far plane is returned at step 2. when the matrix is detected infinite, instead of a zero-plane (zero normal and zero intercept) which raise errors for obvious reasons. What it does right now is that it returns I believe what's in question is not the propagation, but rather whether
I agree it's needed for intercept = I added a todolist in the PR description to track those additional steps down. |
3490a89
to
7e99e93
Compare
@clayjohn I've started to document each impact across the rendering pipeline. See section Infinite zfar impact analysis in the PR description. |
7e99e93
to
b10459b
Compare
b10459b
to
2c075ea
Compare
Closes godotengine/godot-proposals#10515.
Fixes #55070
What is it about ?
This PR ensures that :
Projection
returns a well-formed far plane when the matrix has the form of an infinite projectionWithout this PR, when the projection matrix is infinite,
Projection
generate zero vectors for the far plane normal, and 0 as z-far distance. This raise errors at different places in the rendering logic (culling, shadow mapping, GI, fog ...) and ultimately prevents anything to be rendered.With this PR,
Projection
returns z-far distance asMAX_REAL_T
and the far plane's normal asVector3(0, 0, -1)
when the projection matrix has the form of an infinite projection.Why it does matter ?
Godot 4.3 brought reverse z-buffer which unlocks rendering with very far apart near and far planes, with very limited risks of z-fighting in normal conditions.
However, in practice the usage of very far apart near and far planes is still not supported because it makes rendering fail for the reason described above.
This PR ultimately untaps the potential of natively supporting very deep scenes in Godot, without having to workaround it with multiple nested/layered cameras. This is especially useful for terrain rendering and space simulations.
Under which conditions is an infinite projection matrix generated ?
The infinite form of a projection matrix is obtained when zfar tends towards infinity.
Due to numerical precision limits, in practice it's enough that znear and zfar values are far apart enough to obtain an infinite projection matrix.
I experienced that beyond a difference of 1e7 ~ 1e8 between the near and far distances, the matrix is generated infinite.
The below collapsed sections show how Perspective, Frustum and Orthogonal projections react to far apart znear and zfar when constructed from camera attributes. In a nutshell :
columns[2][2] = -1
andcolumns[3][2] = -2 * p_z_near
columns[3][2] = -1
Expand details
Perspective projection
From
Projection::set_perspective()
:Frustum projection
From
Projection::set_frustum()
, rewritten for clarity:Orthogonal projection
From
Projection::set_orthogonal()
Why an infinite Perspective and Frustum projection matrices breaks zfar and far plane extraction ?
User camera settings are transformed into a projection matrix stored in a
Projection
object.When needed, the rendering server extracts back the frustum planes and distances from the
Projection
object.This happens in one of these 4 functions :
Projection::get_projection_planes()
Projection::get_projection_plane()
Projection::get_z_far()
Projection::get_far_plane_half_extents()
When the projection matrix degenerates to its infinite form due to numerical precision effects, it becomes mathematically impossible to extract the far plane normal and intercept from it.
When performed by one of those functions, the calculation gives 0 (zero vector as the normal, and 0 intercept).
This is the relevant code excerpt taken from
Projection::get_projection_plane()
and rewritten for clarity (the logic is the same for other functions) :Orthogonal projections aren't affected because
columns[2][2] = -2.0 / (p_zfar - p_znear)
which becomes-2.0 / p_zfar
whenz_far
is big, which is still a (small) finite value. This is under the assumption thatp_zfar
is not set toINFINITY
by the user, which is currently not possible through the editor, but possible with code.TODOs
Vector3(0, 0, -1)
and intercept =MAX_REAL_T
)MAX_REAL_T
breaks any further logicMAX_REAL_T
Infinite zfar impact analysis
The following collapsed sections document the behaviors of the various impacted steps of the rendering pipeline when the zfar value extracted from the projection matrix is
MAX_REAL_T
.✅ means the behaviour is likely fine.
⚠️ means there is likely a problem. Further investigation is needed.
Note that for now this assessment is only based code inspection.
Proper testing will be needed in all cases.
Show detailed analysis
RenderForwardClustered
(+RasterizerSceneGLES3
)RenderForwardClustered::_fill_render_list()
RasterizerSceneGLES3::_fill_render_list()
:depth_layer
0. OK sincedepth_layer
seems not to be used as a sort keyRenderForwardClustered
andRenderForwardClusteredMobile
(+RasterizerSceneGLES3
)RenderForwardClustered::_render_particle_collider_heightfield()
RenderForwardClusteredMobile::_render_particle_collider_heightfield()
RasterizerSceneGLES3::render_particle_collider_heightfield()
scene_data
hasz_far
==MAX_REAL_T
which leads the ubo to havez_far
=MAX_REAL_T
tooSkyRD
(+RasterizerSceneGLES3
)SkyRD::setup_sky()
:z_far
=MAX_REAL_T
, which leads the sky shader to havez_far
=MAX_REAL_T
. This value is not used anywhere in the glsl file though.SkyRD::update_res_buffers()
SkyRD::draw_sky()
:RasterizerSceneGLES3::_draw_sky()
GI
GI::process_gi()
:zfar
=MAX_REAL_T
via the push constants. This makes the glsl functionreconstruct_position()
returnvec2(NaN)
in all cases, which makesprocess_gi()
run on NaN vertices, which likely givesNaN
in the end too (not checked)Fog
Fog::volumetric_fog_update()
:✅ For each fog volume : Fog shader gets
zfar
=MAX_REAL_T
via the UBO , but this value is not used anywhere in the glsl filefog_frustum_size_begin
=fog_frustum_size_end
=frustum_near_size
as well aszfar
=MAX_REAL_T
via the UBO. I haven't checked the effets yet but it's lilkely the fog is not rendered correctlySSEffects
SSEffects::downsample_depth()
:zfar
=MAX_REAL_T
via the push constants only in Orthogonal mode, which is not affected by the infinite matrix issueSSEffects::screen_space_indirect_lighting
:zfar
=MAX_REAL_T
via push constants which makes all reprojected samples in last frame's coordinates lie on the near plane. I'm not sure what are the impacts for now.SSEffects::screen_space_reflection
:camera_z_far
=MAX_REAL_T
via push constants which generatesNaN
s in depth values in two places (1 2). Impacts not checked, but probably not goodcamera_z_far
=MAX_REAL_T
via push constants which generates infinite ray ends andNaN
depths. Impacts not checked, but probably not goodSSEffects::sub_surface_scattering
:camera_z_far
=MAX_REAL_T
via push constants which generatesNaN
s in depth values. Impacts not checked, but probably not goodDebugEffects
DebugEffects::draw_shadow_frustum()
:MAX_REAL_T
. Impacts not checked, but probably not goodRendererSceneRenderRD
(+RasterizerSceneGLES3
)RendererSceneRenderRD::render_scene()
RasterizerSceneGLES3::render_scene()
:RenderSceneDataRD
UBO haszfar
=MAX_REAL_T
which has several effects in the scene shader : fog mip level is always 1, multiview cluster is always 0, and interpolated vertex for dual paraboloid always lies on the near planeRendererSceneRenderRD::render_scene()
TAA::process()
andTAA::resolve()
take z-depth and z-near as parameters but aren't used in the functions bodiesClusterBuilderRD
ClusterBuilderRD::begin()
:ClusterBuilderRD::add_box()
andClusterBuilderRD::add_light()
: render elements never touch the far planestate_uniform buffer
getsinv_z = 0
, which makes the z cluster be always 0 in the cluster_render shaderzfar
==MAX_REAL_T
through push constants which generatesNaN
depth calculation and likely make it output a solid colorRendererSceneCull
RendererSceneCull::_light_instance_setup_directional_shadow()
:RendererSceneCull::_light_instance_update_shadow()
:RendererSceneCull::_scene_cull()
:RaycastOcclusionCull::RaycastHZBuffer
RaycastOcclusionCull::RaycastHZBuffer::update_camera_rays()
:zfar
=MAX_REAL_T
. Might have impacts on Embree logicMAX_REAL_T
tooRenderingLightCuller
RenderingLightCuller::_add_light_camera_planes
RenderingLightCuller::add_light_camera_planes_directional
RenderingLightCuller::cull_regular_light
:RendererSceneRender::CameraData
RendererSceneRender::CameraData::set_multiview_camera()
:Test project
PR95944.zip
Camera has near = 0.05 and far ~= 1e20, perspective projection.
Pink sphere is at z ~= -1e20
Blue sphere is at z = -1e15
Green sphere is at z = -1e10
Orange sphere is at z = -1e5
Black sphere is at z = -1
Without this PR :
With this PR :