DSprtn / GTFO_VR_Plugin

A plugin to add full roomscale Virtual Reality support to your favorite game!
MIT License
144 stars 13 forks source link

Disable any materials using the GTFO/TankGlass shader #30

Closed Nordskog closed 2 years ago

Nordskog commented 2 years ago

The what

This is a bad and overkill but functional fix for the severe framedrops experienced when encountering water.

As titled, this iterates through every GameObject in the scene after the elevator sequence has completed, and calls forceRenderingOff=true on its Renderer anywhere the GTFO/TankGlass shader is used. This keeps any hitboxes in the world, but disables actual rendering.

This is rather invasive and I cover some potentially better solutions below. Someone who actually knows Unity should probably come up with something better.

The why

The root cause is the GTFO/TankGlass shader. This shader is used not only for the animated water planes ( Object: g_service_water_plane_2x2, Material: service_water_plane_2x2), but also a number of specimen tanks, some glass panels on various equipment, and on the floor in at least on location.

The performance hit seems relative to surface area, even if occluded, so anywhere with significant amounts of water on the floor, such as:

Animated water planes are the worst offender, because they cover such massive areas of the ground. Specimen tanks behave similarly and will tank performance when looking at them up close. Oddly this is limited to some maps ( seems fine in B1, 40ms framertimes in C2 ).

Some other objects using the same shader do not negatively affect performance; it's mostly the specimen tanks. They will also render unlit if you apply most other shaders ( e.g. Standard ) to them, while non-problematic objects with the same shader are lit properly. They will look okay with other transparent shaders (e.g. GTFO/RefractiveGlass), but performance does not improve.

Better options

Nuking everything is obviously a bit of an extreme. Specimen tanks don't look great without their murky water, and there is that one spot where the glass floor is gone .

Simply disabling the renderers of anything with the service_water_plane_2x2 material would be the next step down. The unplayable sections are fixed, and you'll just have to avoid sticking your nose into the specimen tanks.

If you limit yourself to the service_water_plane_2x2 material and simply disable the IS_WATER_PLANE keyword, performance is is greatly improved, and all it'll cost is the water animation. When standing in the projector chamber, this roughly halves my frames compared to removing the water outright.

Solutions I can't get to work

If you remove the renderer of problematic objects using the water tank shader ( other than water planes ), then add a new MeshRenderer and apply the same material, it performs fine while looking exactly the same. Unfortunately destroying renderers makes the OcclusionCulling thing unhappy ("These are supposed to be static!"), and if the new renderer also uses the GTFO/TankGlass shader there is a constant flood of non-crashing NPEs from somewhere. Performance when looking at the water tanks up close is great though.

Words

Not a super serious PR but assuming a quick fix for R6.5 is imminent the water should at least be removed; other things using the same shader are less important.

DSprtn commented 2 years ago

The fact that the performance is much better if you recreate the object is probably a clue that one or more shader parameter is responsible for the big performance hit. If it looks the same when it's recreated it's probably possible to find and disable this parameter to get similar looking water without the performance hit.

Nordskog commented 2 years ago

Re: 55fe340 above. Better solution, moved to Injections/Rendering, added toggleable setting defaulting to on.

tl;dr: The role of EarlyTransparentRenderer is to make certain transparent objects visible when viewed through other transparent objects ( glass ). Preventing OnWillRender() from running prevents any of that logic from happening, and the setting can be toggled after cage drop ( kinda ).

How is this supposed to work? OnWillRenderObject() adds this instance, if enabled, to the static VisibleInstances HashSet, which is processed in ClusteredRenderer.OnPreRender().That calls InjectDrawCommand() which hides the MeshRendere on the parent GameObject while also submitting it to ClusteredRendering.m_earlyTransparentCmd. m_earlyTransparentCmd is added to the camera command buffer in ClusteredRendering.SetupDeferredCmd(). ClusteredRendering.OnPostRender() calls Restore() to re-enable the MeshRenderer disabled earlier, and we've reached the end of the loop.

What is going wrong In VR, OnWillRenderObject() will be called multiple times for the same instance, increasing for every time the object is culled and unculled. I guess it's being duplicated in some culling list and then rendered multiple times. Sometimes there will be a ton of duplicates even though it's the first time you see the object.

How do we fix it The actual problem appears to be toggling the MeshRenderer. If we enable m_drawTwice, the only difference is that the MeshRenderer will not be disabled ( Sentry laser beam does this ) and the problem does not occur. However, rendering these already expensive objects twice is not ideal ( 9ms -> 11ms when up close ), and also makes them less transparent than they should be.

If we skip OnWillRenderObject() completely, it will simply render once, as a normal object, with no performance issues. The downside to this is that it will not render when viewed through other transparent surfaces ( glass ). This only actually happens in a few locations: B1 center, B1 end.

Curiously there are a number of transparent objects that lack the EarlyTransparentRenderer component, and already do not render when viewed through glass in vanilla GTFO, such as these glass panels on the row of cabinets to the right of the tank. They are only ever visible when viewed directly. The tank water not being visible here is our doing though.

Toggling the setting We could just remove EarlyTransparentRenderer, but doing it this way allows you to disable the fix after cage drop, should it break something. Enabling the fix after you have already encountered a problematic object will not erase any duplicate renders already present and performance will not improve, but neither will it degrade any further.

For this reason I have set it to be enabled by default. The setting is named "Water performance fix".

Words Should ideally figure out why the duplication problem is occurring in the first place, but for the time being I think this is a decent solution.

DSprtn commented 2 years ago

Great work on figuring this out!

There seem to be a lot of symbols missing in the latest decompiled assemblies that I have (entirety of EarlyTransparentRenderer, for instance.) Did you do all the investigating through something like IDA Pro?

Nordskog commented 2 years ago

(∩´∀`∩)

The GTFO modding discord gave me some pointers and I ended up using Il2CppInspector's output along with IDA Freeware, after using a copy of Pro to run Il2CppInspector's script, since free doesn't support that. Ghidra unfortunately seems to choke trying to analyze GTFO at the moment.