godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Generate shadow meshes from a lower LOD level than the source mesh #2600

Open smix8 opened 3 years ago

smix8 commented 3 years ago

bugsquad edit: Transferred from Godot repository: proposal can be found at https://github.com/godotengine/godot-proposals/issues/2600#issuecomment-818838624

Godot version: Godot 4.x commit b4b7c97d3839170180e4f3f7c2bd3179b374057a

OS/device including version: Win 10 64x

Issue description: After testing the new ShadowMeshes in Godot 4.x and comparing the new feature with manual ShadowMeshes created for Godot 3.x I couldn't help but feel very disappointed by the performance (~80% less than Godot 3.x).

So I looked at the shadowmesh source code and I consider this an implementation bug and not a missing feature for a proposal.

Autogenerated shadowmeshes in Godot 4.x are matched to the same source mesh LoD level while they should be matched 1-3 LoD levels lower. A shadow imposter mesh created manual by an artist always starts at least 1-2 LoD levels below the visible LoD mesh but never at the same level. A typical example would be a characters hair mesh, starting at 150.000 vertex. The shadow looks the same with a LOD3 mesh that has only 5.000 vertex cause the outside mesh shape stays the same and this is enough for shadow casting on solid objects. In the current implementation the shadow casting mesh has a 145.000 vertex overhead that does nothing for the endresult and just consumes performance.

Due to this as mentioned in godotengine/godot#45452 pull request there is currently only a little performance gain with shadow imposters while it should be a considerable one.

I suggest that the autogenerated shadow imposters should be generated and matched with LOD2 or LOD3 meshes by default to fix this issue and give the expected performance boost from shadow imposters.

There is no reason to ever use the same mesh geometry detail for a shadow-only casting object except for edgecases like meshes mixed with semi-transparent surfaces and very lowpoly meshes (that don't need shadow imposters to begin with). A Shadowmesh LoD selection option in the new ResourceImporter would be welcome to solve those edgecases.

Steps to reproduce:

Minimal reproduction project:

mrjustaguy commented 3 years ago

Eh, godotengine/godot#45452 isn't about generating Shadow Mesh LODs...

as @reduz said:

-When importing, a vertex-only version of the mesh is created. -This version is used when rendering shadows, and improves performance by reducing bandwidth

This doesn't Mean that the Generated Mesh is supposed to be a LOD mesh. If I understand what the Optimization is doing (Not a Rendering Expert) It's Giving The Renderer Only Verts to render the shadow, instead of triangles, because with triangles it wouldn't be uncommon for a vertex to be a part of multiple triangles, resulting in multiple copies of the same vertex, which increases bandwidth usage, and thus reduces performance.

P.S. Someone with some Rendering Experience please debunk this if it's wrong. If I am right however, the behavior described in the report is Not a Bug, rather a missing feature (Shadows using LODs to cast shadows instead of a full mesh for usually slightly worse shadow quality but much better shadow performance).

clayjohn commented 3 years ago

@mrjustaguy is very close, ShadowMeshes are not the same thing as Shadow Imposters. ShadowMeshes are a copy of the mesh that contain vertex positions and indices only (rather than including normals, tangents, UVs, etc.). ShadowMeshes contain all the same LODs as the regular mesh.

I think there needs to be a proposal for adding a "Shadow LOD bias" parameter somewhere that shifts which LOD to use for the shadow meshes. Perhaps an import setting?

mrjustaguy commented 3 years ago

Could be a Mesh Instance Setting, like LOD bias is.

smix8 commented 3 years ago

@mrjustaguy Yep you are right, the linked pullrequest is not directly the same but I thought it is involved. @clayjohn thanks for the more detailed information.

I think one issue is that the terms ShadowMesh and ShadowImposter are mixed all the time by people that have no technical render background. I see experiences 3D artists use both terms all the time when they mean ShadowImposters and the user interface in Godot 4.x uses the term "Generate -> Shadow Meshes" right next to LOD generation.

I think a pulldown option in the new Advanced Import Settings menu with all available mesh LOD levels could work to pick the starting mesh detail for the shadow meshes / imposters. Something like "ShadowMesh Base LOD Level".

Set to default LOD1 or LOD2 if available on the mesh.

shadowmeshes

mrjustaguy commented 3 years ago

Currently a Workaround would be (if Cast Shadow property, under geometry, worked, which it doesn't seem to do in current G4.0) is to make 2 mesh instances, a Mesh that is visible but doesn't cast shadows, and a shadow mesh that is set to just cast shadows, with a lower LOD Bias.

smix8 commented 3 years ago

@mrjustaguy I already use this approach in Godot 3.x, lower mesh LOD level and set to cast shadows only while disabling shadow casting on the more detailed visible mesh, hence my performance comparison. I didn't know at that point that cast shadow is broken in Godot 4.x and I am also comparing apple with fish.

Calinou commented 2 years ago

I started toying around with this. Here's a diff that quadruples the LOD bias when rendering shadow passes (directional + point):

diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
index 7c35b01b50..604fc7ee07 100644
--- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
+++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
@@ -4560,7 +4560,7 @@ void RendererSceneRenderRD::_pre_opaque_render(RenderDataRD *p_render_data, bool

        //cube shadows are rendered in their own way
        for (uint32_t i = 0; i < render_state.cube_shadows.size(); i++) {
-           _render_shadow_pass(render_state.render_shadows[render_state.cube_shadows[i]].light, p_render_data->shadow_atlas, render_state.render_shadows[render_state.cube_shadows[i]].pass, render_state.render_shadows[render_state.cube_shadows[i]].instances, camera_plane, lod_distance_multiplier, p_render_data->screen_lod_threshold, true, true, true, p_render_data->render_info);
+           _render_shadow_pass(render_state.render_shadows[render_state.cube_shadows[i]].light, p_render_data->shadow_atlas, render_state.render_shadows[render_state.cube_shadows[i]].pass, render_state.render_shadows[render_state.cube_shadows[i]].instances, camera_plane, lod_distance_multiplier, p_render_data->screen_lod_threshold * 4, true, true, true, p_render_data->render_info);
        }

        if (render_state.directional_shadows.size()) {
@@ -4590,11 +4590,11 @@ void RendererSceneRenderRD::_pre_opaque_render(RenderDataRD *p_render_data, bool

        //render directional shadows
        for (uint32_t i = 0; i < render_state.directional_shadows.size(); i++) {
-           _render_shadow_pass(render_state.render_shadows[render_state.directional_shadows[i]].light, p_render_data->shadow_atlas, render_state.render_shadows[render_state.directional_shadows[i]].pass, render_state.render_shadows[render_state.directional_shadows[i]].instances, camera_plane, lod_distance_multiplier, p_render_data->screen_lod_threshold, false, i == render_state.directional_shadows.size() - 1, false, p_render_data->render_info);
+           _render_shadow_pass(render_state.render_shadows[render_state.directional_shadows[i]].light, p_render_data->shadow_atlas, render_state.render_shadows[render_state.directional_shadows[i]].pass, render_state.render_shadows[render_state.directional_shadows[i]].instances, camera_plane, lod_distance_multiplier, p_render_data->screen_lod_threshold * 4, false, i == render_state.directional_shadows.size() - 1, false, p_render_data->render_info);
        }
        //render positional shadows
        for (uint32_t i = 0; i < render_state.shadows.size(); i++) {
-           _render_shadow_pass(render_state.render_shadows[render_state.shadows[i]].light, p_render_data->shadow_atlas, render_state.render_shadows[render_state.shadows[i]].pass, render_state.render_shadows[render_state.shadows[i]].instances, camera_plane, lod_distance_multiplier, p_render_data->screen_lod_threshold, i == 0, i == render_state.shadows.size() - 1, true, p_render_data->render_info);
+           _render_shadow_pass(render_state.render_shadows[render_state.shadows[i]].light, p_render_data->shadow_atlas, render_state.render_shadows[render_state.shadows[i]].pass, render_state.render_shadows[render_state.shadows[i]].instances, camera_plane, lod_distance_multiplier, p_render_data->screen_lod_threshold * 4, i == 0, i == render_state.shadows.size() - 1, true, p_render_data->render_info);
        }

        _render_shadow_process();
@@ -4987,7 +4987,7 @@ void RendererSceneRenderRD::_render_shadow_pass(RID p_light, RID p_shadow_atlas,

    if (render_cubemap) {
        //rendering to cubemap
-       _render_shadow_append(render_fb, p_instances, light_projection, light_transform, zfar, 0, 0, false, false, use_pancake, p_camera_plane, p_lod_distance_multiplier, p_screen_lod_threshold, Rect2(), false, true, true, true, p_render_info);
+       _render_shadow_append(render_fb, p_instances, light_projection, light_transform, zfar, 0, 0, false, false, use_pancake, p_camera_plane, p_lod_distance_multiplier, p_screen_lod_threshold * 4, Rect2(), false, true, true, true, p_render_info);
        if (finalize_cubemap) {
            _render_shadow_process();
            _render_shadow_end();
@@ -5005,7 +5005,7 @@ void RendererSceneRenderRD::_render_shadow_pass(RID p_light, RID p_shadow_atlas,

    } else {
        //render shadow
-       _render_shadow_append(render_fb, p_instances, light_projection, light_transform, zfar, 0, 0, using_dual_paraboloid, using_dual_paraboloid_flip, use_pancake, p_camera_plane, p_lod_distance_multiplier, p_screen_lod_threshold, atlas_rect, flip_y, p_clear_region, p_open_pass, p_close_pass, p_render_info);
+       _render_shadow_append(render_fb, p_instances, light_projection, light_transform, zfar, 0, 0, using_dual_paraboloid, using_dual_paraboloid_flip, use_pancake, p_camera_plane, p_lod_distance_multiplier, p_screen_lod_threshold * 4, atlas_rect, flip_y, p_clear_region, p_open_pass, p_close_pass, p_render_info);
    }
 }

It acts as a multiplier of the viewport and mesh's LOD threshold. This multiplier could be exposed as a project setting and a Viewport property, likely defaulting to 3 or perhaps 4. Higher values are possible, but they start to be quite aggressive.

The mesh I'm using in the test project below isn't that detailed by today's AAA standards, so real world gains can be more important in scenes with very detailed (and distant) objects. Performance gains will also increase as you add more lights with shadows (the testing project only has 1 DirectionalLight3D).

Testing project: test_auto_lod_2.zip

Visual comparison

1024×600

The difference is more noticeable at this resolution.

Before After
2021-12-11_20 49 49 2021-12-11_20 49 32

2560×1440

The difference is less obvious here. If you haven't looked at the original image, it's difficult to discern that shadows use simplified meshes.

Before After
2021-12-11_20 49 56 2021-12-11_20 49 36

Performance comparison

OS: Fedora 34 CPU: Intel Core i7-6700K GPU: GeForce GTX 1080 (NVIDIA 470.74)

1024×600

Before After
740 FPS (1.35 mspf) 805 (1.24 mspf)

2560×1440

Before After
262 FPS (3.81 mspf) 269 FPS (3.71 mspf)
mrjustaguy commented 2 years ago

The difference in performance will be much more visible with more distant meshes, these are very close to the camera, so they're still fairly high LOD.