When placing a static mesh individually, it will respect LOD settings, but the behavior differs when the same mesh is placed as an instanced static mesh via blueprint.
Based on my research I suspect this is because instanced static meshes are improperly considering the screen size of ALL the instances as if they are one huge mesh.
I understand that every instance of the mesh will LOD at the same time, which of course makes sense, but I believe that calculating the LOD against the TOTAL size of all the instances together is undesirable behavior most of the time because screen size of the individual instances is still going to be the primary indicator of whether or not using a higher LOD makes sense.
This is a known issue and has been requested before. It is also listed under our documentation for Foliage Instanced Meshes as a caveat in our documenation.
Currently the entire cluster of
instances change LODs simultaneously.
We may add support for distance-based
fading per instance in the future.
While this is listed in the foliage section the same is true for instances that are in blueprints as well.
I think I might not be explaining the issue well, it actually has nothing to do with all of the instances LOD’ing at once. As you mentioned, this is what the documentation reflects and is expected behavior.
This issue I’m bringing up has nothing to do with that.
What I’m seeing is that an instanced object does not LOD at the same screen size as a non-instanced object with identical settings.
This issue still exists in 2024, so I’ve created a hacky workaround.
Header file:
// See https://issues.unrealengine.com/issue/UE-5327
// Unknown whether this works for components created in the level editor.
UCLASS()
class UInstancedStaticMeshComponentWithProperLods : public UInstancedStaticMeshComponent {
GENERATED_BODY()
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
FStaticMeshRenderData* dupeRenderData = nullptr;
void freeDupeRenderData();
virtual ~UInstancedStaticMeshComponentWithProperLods() override { freeDupeRenderData(); }
};
cpp file:
static double vectorSum(FVector vec) {
return vec.X + vec.Y + vec.Z;
}
class FInstancedStaticMeshSceneProxyWithAccessors : public FInstancedStaticMeshSceneProxy {
public:
FStaticMeshRenderData* GetRenderData() { return RenderData; }
void SetRenderData(FStaticMeshRenderData* data) { RenderData = data; }
};
void UInstancedStaticMeshComponentWithProperLods::freeDupeRenderData() {
if (dupeRenderData != nullptr) {
free(dupeRenderData);
dupeRenderData = nullptr;
}
}
FPrimitiveSceneProxy* UInstancedStaticMeshComponentWithProperLods::CreateSceneProxy() {
FInstancedStaticMeshSceneProxyWithAccessors* proxy = static_cast<FInstancedStaticMeshSceneProxyWithAccessors*>(Super::CreateSceneProxy());
if (proxy == nullptr) return proxy;
FStaticMeshRenderData* origRenderData = proxy->GetRenderData();
FVector minScale(0), maxScale(0);
GetInstancesMinMaxScale(minScale, maxScale);
double averageScale = (vectorSum(minScale) + vectorSum(maxScale)) / 6;
double compSize = CalcBounds(GetComponentTransform()).SphereRadius;
double meshSize = origRenderData->Bounds.SphereRadius;
double embiggenment = compSize / (meshSize * averageScale);
// Duplicate the render data, avoiding needing to call the (nonexistent) copy constructor
freeDupeRenderData();
dupeRenderData = (FStaticMeshRenderData*) malloc(sizeof(FStaticMeshRenderData));
memcpy(dupeRenderData, origRenderData, sizeof(FStaticMeshRenderData));
// We need to do it this way to avoid calling any destructors
memset(&dupeRenderData->ScreenSize, 0, sizeof(dupeRenderData->ScreenSize));
for (int i = 0; i < MAX_STATIC_MESH_LODS; ++i) {
dupeRenderData->ScreenSize[i].Default = origRenderData->ScreenSize[i].GetValue() * embiggenment;
}
proxy->SetRenderData(dupeRenderData);
return proxy;
}