Hi, we recently switched a large portion of our static level content to use ISMs for performance and memory savings, but noticed that there’s an issue where instances stop rendering their CSM shadows when they shouldn’t (see video). This happens consistently whenever the field of view changes, but we’ve also observed it intermittently as the camera is just moved around (often correcting itself when the mesh instance comes fully into the frustum). Our project was in 5.6.1, but I copied our rendering/scalability settings and lighting setup to a stock 5.7.4 project and it still reproduces. We believe this is incorrect behavior as normal (non-instanced) static meshes cast shadows the entire FOV as expected and, after the FOV stops changing, the ISM shadows eventually render as they did before the FOV started animating.
In the repro project I have set up, you can simply set the graphics quality to Medium (which tightens the shadow distance) and right click to animate the FOV in order to see the issue in question.
Please let me know if there’s any other information that I can provide that would be useful. Thanks.
It looks like there’s an issue with shadow caching not being used when changing the FOV. If you turn off caching with r.Shadows.CSMCaching 0 then you can see the missing shadows are due to the r.Shadows.RadiusThreshold being set to 0.05 so the cubes that are further away from the camera are small enough on the screen to be culled from shadow casting. If you lower r.Shadows.RadiusThreshold enough, these objects will be included in shadow casting as expected and the shadows will appear as expected. That said, with caching enabled I would expect shadows to remain consistent while changing the FOV.
As far as I can tell, it doesn’t matter if the meshes are static or ISMs. I’ve created the following issue for tracking which should be visible publicly soon: https://issues.unrealengine.com/issue/UE\-372125
Yes, we are also looking at why screen size culling was removed in CL#15570922, though the code you are pointing to was committed later in CL#35334977. If “CacheMode != SDCM_StaticPrimitivesOnly” is removed from both places then it appears the screen radius culling is used, but there’s still some oddities and we’re looking into why these changes were made originally.
That’s interesting, so it sounds like you’re modifying the ScreenSize() code in NaniteCullingCommon.ush - does this mean you’re only seeing this issue is worse on Nanite meshes than non-Nanite?
Are the cached shadows doing any screen size culling at all? From what I could tell in the test project, when cached shadows were enabled, the min screen radius wasn’t being evaluated when drawing those shadows.
Thanks for clearing that up. I’ve updated the internal ticket with that info, but in the meantime it’s probably fine to use your workaround till the issue is addressed in-engine.
In your testing, have you noticed a difference in the culling behavior of a single SM vs that mesh when converted to an ISM? It might be helpful to have a side by side test to verify if there’s any discrepancy. I didn’t see any in the provided repro level, but all those cubes are the same size.
OK, thanks for checking that. Our current thinking is when the FOV changes the rendering goes down the dynamic path which applies the radius based culling to maintain performance. This works out very similarly for VSM (except that FOV changes don’t invalidate clipmaps). Generally, it makes sense since if the light is not cacehable we want the more aggressive culling (if not you can turn down the radius threshold). It might be possible to differentiate the cases further, but must be done with care to not make the already complex logic worse & to not regress the more common uncached use case.
If there’s a repro case where we can see that ISMs and SMs are being treated differently we can pursue fixing that also, but haven’t found that to be the case yet.
Thanks. Our original repro has some fairly large ISMs exhibiting this issue (as opposed to the smaller cubes in the repro project), so if the RadiusThreshold is indeed the culprit, it sounds like the calculation may be fairly substantially incorrect.
That’s good to know. I’ve been fiddling around with the shadow culling to see if I could get something approximate as a band-aid fix in the meantime. It seems that modifying the RadiusSq in the ScreenSize() function as such causes the uncached shadows to line up much more closely with the cached shadows:
const float RadiusSq = pow(max(max(LocalBoxExtent.x * NonUniformScale.x, LocalBoxExtent.y * NonUniformScale.y), LocalBoxExtent.z * NonUniformScale.z), 4);I haven’t studied the exact values being passed through to be confident this is mathematically correct (and may still result in over/under culling), but it seems to be close enough in our project that I may use that for the time being so we can still rely on ISMs for our levels (while avoiding FOV change shadow artifacts).
Ah sorry, I should have clarified: we don’t have nanite enabled at all (it’s only non-nanite). It seems that the engine runs this logic for both paths though.
It certainly seems like the cached shadows do screen size culling as well (though something needs to invalidate them before the new shadow RadiusThreshold value becomes apparent, perhaps the repro project has something configured slightly differently to our main project where I observed this behavior). Setting the RadiusThreshold to something high like 0.5 makes it more apparent in our main project. When I apply the modification I mentioned above, it makes it so there’s little perceivable difference in the cached/uncached shadows getting culled even at a high RadiusThreshold of 0.5. Toggling cached shadows on/off (for debugging) results in nearly, if not completely, identical appearance between the two settings, even when changing FOV values.
Hmm, I’m not sure, but I’m not seeing the difference in behavior when it’s a single static mesh vs single ISM instance in the repro project. It’s definitely possible that the fact that the ISM has multiple instances could be throwing off the bounds calculation or something.