Texture Streaming issue on Instanced Static Mesh Components with static lighting

Hi, in our project we’ve been facing an issue where in packaged game some textures wouldn’t ever load higher resolution mips no matter how close you were to the object. We’ve managed to track it down to an engine issue, and since I haven’t found people discussing it anywhere else I thought in addition to reporting the bug to epic, I’ll also mention it here to hopefully save some people hours of debugging.

First of all, this does NOT affect all the projects. It only affects projects that:

  • Use Baked Lighting
  • Don’t use Virtual Textures

We’ve reproduced the bug in UE 5.3 - UE 5.5 (but it might affect other versions as well)

In those projects the bug affects all instanced static mesh components and hierarchical instanced static mesh components that are spawned via Actor::ExecuteConstruction - for example Packed Level Actors

You can read more about the issue bellow, but there are some good news. It’s very easily fixable with a minimal code change to the engine (yes, it does require a custom engine build :frowning: .) If anyone’s interested, here’s a patch file for UE 5.5:

CopyStreamingTextureDataForInstancedStaticMeshes.patch (2.5 KB)

As we’ve tracked down the bug, we’ve found out that it has two different manifestations:

  1. Textures on affected components never request higher resolution mips than the lowest one and appear permanently blurry. This happens if there are other static mesh components that are far away and are referencing the same textures as the affected components.

  2. Textures on affected components always request the highest resolution mip, no matter how far away from the camera it is. This happens if there’s no other static mesh component using the same textures.

You can see those cases in the following screenshots. The cube on the left is a static mesh component - the control group. The middle and the left one are part of a Packed Level Actor blueprint, while the right cube is the only referencer of the wood textures, and the middle cube has a duplicate static mesh component cube with the same material far behind it. There’s also a “ListStreamingTextures” printout near them showing the inner state of the texture streaming.

Before Fix:


After Fix:
(Small differences in order of one mip level are caused by slightly different camera positions)


If anyone is interested in reproduction steps or technical details here’s the bug submission I’ve reported to Epic:

Bug Details

Reproduction Steps:

Reproduction Steps
  1. Create a new Blank project with following settings:
    Target Platform: Desktop
    Quality Preset: Maximum
    Starter Content: True

  2. Change project settings to:
    Rendering → Direct Lighting → “Shadow Map Method”: Shadow Maps
    Rendering → Misc Lighting → “Allow Static Lighting”: True
    and restart the engine to apply the settings

  3. Use File → New Level to create a new level with “Basic” template

  4. Select “DirectionalLight” in the level and set it’s mobility to Stationary

  5. Select “SkyLight” in the level and set it’s mobility to Static

  6. Add four cubes to the level, move them to following coordinates and apply following materials from starter content
    Cube 1: Location(5000, -200, 0), Material: “M_Brick_Clay_Beveled”
    Cube 2: Location(5000, 0, 0), Material: “M_CobbleStone_Pebble”
    Cube 3: Location(5000, 200, 0), Material: “M_Wood_Floor_Walnut_Polished”
    Cube 4: Location(10000, 0, 0), Material: “M_CobbleStone_Pebble”

  7. Select Cube 2 and Cube 3, and from the right click menu select Level → Create Packed Level Actor…

  8. Press Ok in the pop up dialogue, select Content folder as a target save folder for both the level and blueprint

  9. From the Build menu build both “Build Lighting Only” and “Build Texture Streaming”

  10. Save All, save the level into Content directory as “L_Test”, it should also save the used material with correct usage flags,
    if that’s not the case set “Used with Static Lighting” and “Used with Instanced Static Meshes” on them manually

  11. Open a different level or a Empty new level

  12. Open the previous level once again (“L_Test”)

  13. Force resave the level by clicking on the floppy disk icon in the level toolbar

  14. Select the level “L_Test” as “Game Default Map” in the Project Settings → Maps & Modes

  15. Package the game in development build by clicking on Platforms → Windows → Package Project and select a target folder

  16. Start the packaged game (don’t move yet), open the console and use “r.Streaming.DropMips 1” (without quotes) to drop previously loaded textures followed by “r.Streaming.DropMips 0” to revert it to default

  17. Use console command “ListStreamingTextures” (without quotes) to list current state of texture streaming

  18. Fly closer to the three cubes and use “ListStreamingTextures” (without quotes) console command again

Expected result:
When the camera is far away from the packed actor cubes, they shouldn’t be requesting full texture resolution, and when the camera is close to them they should load the full resolution texture (same as the Cube 1 - control group)

Actual result:
When the camera is far away from the packed actor cubes, the Cube 3 is requesting full resolution of the texture as seen in log:
Texture [21] : Texture2D /Game/StarterContent/Textures/T_Wood_Floor_Walnut_D.T_Wood_Floor_Walnut_D
Current=2048x2048 Wanted=2048x2048 MaxAllowed=2048x2048 LastRenderTime=0.000 BudgetBias=0 Group=TEXTUREGROUP_World

When the camera is close to the cubes, the Cube 2 is not requesting higher resolution of the texture and appears blurry:
Texture [16] : Texture2D /Game/StarterContent/Textures/T_CobbleStone_Pebble_D.T_CobbleStone_Pebble_D
Current=64x64 Wanted=64x64 MaxAllowed=2048x2048 LastRenderTime=0.000 BudgetBias=0 Group=TEXTUREGROUP_World

Description:
This issue causes incorrect wanted mip level of streaming textures on all instanced static mesh components and hierarchical instanced static mesh components spawned from Actor::ExecuteConstruction (for example Packed Level Actors), when the project is using static lighting after texture streaming is built. It can manifest both as always requesting the highest resolution mip in case there’s no other static mesh in the scene using the same textures, or not requesting higher resolution mips if there’s an other actor using the same texture that’s far away.

We’ve managed to track the bug down to FInstancedStaticMeshComponentInstanceData not copying and restoring StreamingTextureData on the component (while static mesh components handle this properly in FStaticMeshComponentInstanceData). This results in StreamingTextureData being empty after loading the level second time (step 12.) and saving it. This doesn’t manifest as an issue in projects that don’t use static lighting since the RenderAssetInstanceInfos array in FRenderAssetInstanceState::AddComponent will be left empty which triggeres condition “if (!RenderAssetInstanceInfos.Num())” that causes the function to return “fail” and the system will fall back on getting the textures dynamically. However in levels with static lighting the RenderAssetInstanceInfos won’t be empty, but will contain lighting textures while it’s missing the material textures. This will cause the function to continue normally, but won’t ever register the component as a referencer of the material textures. Because of this the texture streaming system treats the textures on Cube 3 as bUseUnkownRefHeuristic and requests the highest resolution texture mip, as there isn’t any other object referencing the texture in the level. In case of Cube 2 there’s a valid referencer of the texture - Cube 4, but it’s very far away. The texture streaming system only considers Cube 4 as a texture referencer and doesn’t think it needs higher resolution mips, even if camera is close to Cube 2.

We’ve solved this issue with a custom engine build that implements functionality of copying and restoring StreamingTextureData found in FStaticMeshComponentInstanceData to FInstancedStaticMeshComponentInstanceData.

2 Likes