Skip to content
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

Improve DirectionalLight shadow rendering efficiency #57549

Closed
parasyte opened this issue Feb 2, 2022 · 10 comments · Fixed by #84745
Closed

Improve DirectionalLight shadow rendering efficiency #57549

parasyte opened this issue Feb 2, 2022 · 10 comments · Fixed by #84745

Comments

@parasyte
Copy link

parasyte commented Feb 2, 2022

Godot version

3.4.2

System information

Windows 11, GLES3, NVIDIA RTX 3090 (511.23)

Issue description

A screenshot was posted to the discord memes channel a few days ago, and I got curious. The screenshot is below, along with the original comment.

Godot demo project. 12 cubes. 100k vertices. 20! drawcalls.
And people asking why Godot so slow when they run demos.

Screenshot_245435


I created a test scene with a few objects and found that DirectionalLight with shadows enabled was the biggest issue. There is also some odd behavior with this combination that looks like false positives in the renderer culling, but I haven't found any reason for it while looking through the spatial partitioning code. At certain camera angles, objects that are no where near the camera frustum will contribute to the "Vertices Drawn", "Surface Changes", and "Draw Calls" metrics in the monitor.

Here are some screenshots from the test procedure described below. First, the scene is running, and I have the monitor shown in the editor:

DirectionalLight1

Then I hide the SportsCar node, and "Vertices Drawn" is reduced by over 20,000! Surface Changes and Draw Calls also see some improvements:

DirectionalLight2

Then I turn SportsCar back on and disable the shadow on DirectionalLight. Vertices Drawn has reduced by almost 60,000 (a massive 76% improvement)! and Surface Changes and Draw Calls see a 75% reduction; another huge improvement!

DirectionalLight3

And finally, changing SportsCar visibility with the shadow off has no impact whatsoever, exactly what is expected for a node that cannot be seen by the camera at all.

DirectionalLight4

This might be the cause of #35746.

Steps to reproduce

  1. Open the example repro project and run the scene.
  2. Open the Debugger/Monitors tab and enable everything under Raster.
  3. Change the visibility of the "SportsCar" node, which is behind the camera and its shadow is being cast away from it.
    • Notice that this object is being drawn when it should not.
  4. Do the same SportsCar visibility test with the DirectionalLight shadow disabled.
    • Notice that the monitor numbers no longer change with the SportsCar visibility! The numbers are also much lower with the shadow disabled.
  5. Do the very same test, but hide the DirectionalLight, and use either of the other lights instead.
    • I found that any combination of the omni light and spotlight with their shadows does not change how many things are drawn, regardless of the SportsCar visibility. Shadows for these lights are free???
  6. Do the same test but this time use DirectionalLight+Shadow and change the camera rotation along its local X axis while SportsCar visibility is enabled.
    • Even a slight adjustment upward from the camera's default rotation will reduce the number of things drawn. And yes, these are all from the SportsCar. Do the same rotation test with SportsCar hidden.

Minimal reproduction project

Render Culling Test.zip

@Zireael07
Copy link
Contributor

The first pic is in editor. I can't find it right now, but I recall discussing it, and it turns out the editor also counts the gizmos and the grid, hence more draw calls than you expect.

Directional light basically renders the whole scene again when casting shadows, so careful tuning of directional light shadow distance and what meshes cast shadows is needed. You also have the option of using a simplified mesh for shadow casting, i.e. placing a second invisible mesh set to shadows only.

@parasyte
Copy link
Author

parasyte commented Feb 2, 2022

Yes, but the gizmos and grid are not included in metrics while running the game.

Shadow map rendering is expected to be a little more expensive, but not 400% more expensive. Even if the camera encompasses the whole scene, I would expect the worst case for the shadow rendering pass to be 2x everything. Do that with the repro scene and you'll see it tops out around 127 draw calls with DirectionalLight shadows enabled. Disable those shadows and fake them with spot lights and you only have 49 draw calls.

@lawnjelly
Copy link
Member

lawnjelly commented Feb 2, 2022

There are also splits for a directional light, and depth prepass. The culling of the shadow volume against the view frustum is not that good either, I did a PR for this many moons ago .. #33340 . Ah yes I'd like to resurrect that at some point.

Change your directional light to orthogonal and the verts rendered will drop a lot, it will be rendering shadow map once for every of the 4 splits you have it set to.

@mrjustaguy
Copy link
Contributor

Shadows in Godot generally need some optimizing, albeit things are much worse in 3.x compared to 4.0
However Primitive count and Draw calls don't mean much, Frame time contribution matters, but that's available in Visual Profiler in 4.0

On the topic of Culling godotengine/godot-proposals#3890 proposes a method to Improve on Shadow rendering, with the occlusion culling part also being applicable to Directional Shadows if that is doable however I'm unsure if that would improve performance as they have to be rerendered every frame and the main benefit of it is to do a quick check to see if you need to render a new shadow or not.. But that is by using GPU's Shadow Maps, Using Occlusion culling available in 4.0 for Normal Rendering just used to help with Directional Shadows on the other hand might benefit performance.

Basically before rendering a given shadow in the shadowmap checks if there were any changes in the area that it's rendering (as in The radius around the Omni light, or Cone of the Spot Light, or less ideally their AABBs) and if there were changes there it pulls up the Last frame's Shadow to use as a Depth Buffer for Occlusion Culling, along with the Last frame's recorded object positions (to determine which objects were the casters in the last frame) and see if the Casters moved, or new Casters arrived in a possibly non shadowed area, in both cases requiring a new shadow rendering.

@parasyte
Copy link
Author

parasyte commented Feb 2, 2022

Even though draw calls and primitive counts don't mean much on their own, this issue gets much worse in a real scene where several nodes have multiple materials and shaders. Even if they are hundreds of units behind the camera, we still end up paying for shader switches for these nodes that should be culled early on.

Although if it could avoid the shader switching for shadow passes, that might be nice. And of course, some other tricks like rendering shadows with lower LOD meshes.

@Calinou
Copy link
Member

Calinou commented Feb 2, 2022

The culling of the shadow volume against the view frustum is not that good either, I did a PR for this many moons ago .. #33340 . Ah yes I'd like to resurrect that at some point.

See also godotengine/godot-proposals#3908, which would help improving the effective directional shadow map resolution (at the cost of slightly less effective object culling within each split). If we can combine both approaches together, we should be able to get back to the original efficiency level but with much greater effective shadow resolution.

And of course, some other tricks like rendering shadows with lower LOD meshes.

This is feasible to implement in Godot 4.0 with the automatic mesh LOD, see godotengine/godot-proposals#2600.

Godot 4.0 can also use dedicated shadow meshes that are automatically created on import. These shadow meshes weld vertices together to reduce memory bandwidth and vertex processing cost.

In Godot 3.x, you can use shadow meshes manually by setting a MeshInstance's Cast Shadow property to Off, duplicating the mesh, clearing its material so it uses the fallback opaque material and setting the duplicate's Cast Shadow property to Shadows Only. You can then load a lower-quality version of the mesh. See also the Level of Detail add-on.

Although if it could avoid the shader switching for shadow passes, that might be nice.

I'm not sure if this is always feasible, as shaders can influence where shadows are casted. Fully opaque materials are not affected by this, but this applies to any material that uses alpha scissor or opaque prepass. (Alpha blended materials can't cast shadows in Godot.)

It's definitely something to look into, but it may already be done in the master branch (which uses an entirely different renderer). Feel free to open a pull request for this on 3.x either way 🙂

PS: If you need help navigating the rendering code, feel free to ask questions in the Godot Contributors Chat's #rendering channel.

@Calinou Calinou changed the title DirectionalLight shadows are extremely expensive Improve DirectionalLight shadow rendering efficiency Feb 2, 2022
@Calinou Calinou added enhancement and removed bug labels Feb 2, 2022
@Calinou Calinou added this to the 4.0 milestone Feb 2, 2022
@parasyte
Copy link
Author

parasyte commented Feb 2, 2022

I made a few minor changes to the repro project; Positioned the camera specifically so it can trigger a large change in the shadow map projection with just a minor change in camera angle. Use the space bar to switch between the two angles (the difference is only 1 degree). I also changed the shadow mode to 2 splits and the depth range to optimized which shows the issue pretty well.

There are other camera angles and shadow settings that are far worse. And throwing various things into the scene can completely resolve the "disappearing shadow" issue, but I don't know why.

Render Culling Test-optimized shadowmap.zip

And I also captured the frames between the two minor camera angles in RenderDoc. Here are the interesting textures.

Frame buffer at -50 degrees:

-50 degrees-framebuffer

Shadow map at -50 degrees:

-50 degrees-shadowmap


Frame buffer at -51 degrees:

-51 degrees-framebuffer

Shadow map at -51 degrees:

-51 degrees-shadowmap

Looks like the light-space projection is very inaccurate. I'll test the patch in #43207, I'm wondering how much it will help in this case.

This is definitely related to the issue. Hiding SportsCar makes both camera angles look like the second image, and fewer things get drawn.

@Calinou
Copy link
Member

Calinou commented Nov 28, 2022

Looks like the light-space projection is very inaccurate. I'll test the patch in #43207, I'm wondering how much it will help in this case.

@parasyte Bump 🙂 Did you have time to try it out?

@Calinou Calinou modified the milestones: 4.0, 4.x Dec 24, 2022
@parasyte
Copy link
Author

Oof, sorry for the long delay! I finally got around to updating one of my projects to 3.5 (from 3.4.4!) so I'm still behind and getting caught up. I'll take a look at making sure my dev environment for Godot still works, so I can build the latest 3.x and try that patch. The repro scene with the cars definitely still has the issue shown above in 3.5, though. That is one of the things I tested while checking out 3.5...

I don't have a strict timeline on doing any of this, but I kind of have the interest again.

@akien-mga
Copy link
Member

Fixed by #82584.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants