LOD not updating for entities with an LOD group that are moved by a system?

Hi all,
I'm working on a large scale forest visualisation (~1.5e7 trees). Each tree is an entity with an LOD Group. The idea is that when you are zoomed out, you see a constant set of around 50,000 static trees which are never moved and give a general overview of the density in the forest. When you zoom in close enough, the visualisation begins using a cache of entities that it can move around the map to draw more trees in the area you are looking at so you can see the exact makeup of the forest at any given point whilst never having more than a couple of hundred thousand entities. These "MoveableTrees" are moved by a System which executes an IJobEntity when the user moves the camera enough to warrant recalculating this.

The LOD behaves normally for the static 50,000 trees, however it is often possible to approach the MoveableTrees when they have just been moved into a new location without their LOD updating. In order to diagnose the problem, I switched to a simple cube for the low LOD and a sphere for high LOD. You can see what I mean in the screenshot below: The moveable "trees" that are closest to the camera (and have just been teleported in) are still rendering as cubes, even though the static "trees" sitting behind them have already become spheres. the further away static trees are then cubes again, as expected.
9221142--1287393--upload_2023-8-10_21-42-4.jpg

My impression is that I (or Unity) am failing to update something when move the entities around. Currently all I do is update the LocalTransform of a given entity, my impression being that Unity should automatically update LOD components after this. Below you can see my code for moving the trees:

[BurstCompile]
public partial struct MoveMovableTrees :  IJobEntity
{
    public EntityCommandBuffer.ParallelWriter Ecb;
    [ReadOnly]
    public NativeList<LandClimTree> trees;
    [ReadOnly]
    public NativeList<uint> id;
    [ReadOnly]
    public NativeList<uint> nRenderTypes;
    public void Execute([ChunkIndexInQuery] int chunkIndex, ref MovableTree movableTree, ref Tree tree,ref MeshLODGroupComponent lod, Entity entity)
    {
        if(movableTree.idInRenderType < nRenderTypes[movableTree.renderType])
        {
            int location = 0;
            for(int i = 0; i < (int)movableTree.renderType; i++)
                location += (int)nRenderTypes[i]; //Add up all the trees in all the types before this. Change List this to a cumulative sum in future to save a bit of time.
            location += movableTree.idInRenderType;
            LandClimTree lctree = trees[(int)id[location]];
            LocalTransform lt = LocalTransform.FromPosition(lctree.x, lctree.z, lctree.y);
            lt.Scale = (lctree.height/tree.scale);
            Ecb.SetComponent(chunkIndex, entity, lt);
            movableTree.currentLandclimTreeID = lctree.id;
        }
        else
        {
            LocalTransform lt = LocalTransform.FromPosition(0, -100, 0);
            lt.Scale = 1.0f;
            Ecb.SetComponent(chunkIndex, entity, lt);
        }
    }
}

I'd be grateful of any help or ideas people might have. I have been unable to recreate this behaviour with GameObjects.

EDIT: i just discovered that if I pull the camera far enough out and zoom it back in, the LODs update.

PS. Labelled as a bug but might also just be a question in the case that I've missed something obvious...

Hey, reposting my own comment since it helped someone back in the day. I'm pretty sure this bug is fixed by now, though.

1 Like


Holy hell, you are a certified genius. This worked perfectly, I've been struggling with this problem for several weeks!

Of course, it's not the most elegant solution, but it does the trick for now. Thank you again!

1 Like

Hi,
i'm having the same bug with a baked prefab..

Currently using 2022.3.0f (DX12) and the package versions are 14.0.7 for HDRP and 1.0.16 for Entities/Graphics.
So I wanted to ask, if this bug is fixed by now and if so, what Unity Version I need?

Changing the Fov of the camera as suggested works, but thats not really a solution imho.
Are there docs about how the lod system works, or which "systems/components" are responsible for that, so I can debug the code myself?


Look for OnPerformCulling in EntitiesGraphicsSystem. By the way, if Unity didn't already fix this, my factor of the update to use ISystem in my framework may have. I remember seeing some oddities with their caching strategy.

1 Like

Hi,
thanks for pointing in the right direction.

Looking at the corresponding code (job SelectLodEnabled) there are multiple ways which can lead to Lod errors.

There are three conditions that are checked if the selected Lod have to be recomputed for the chunks and instances.

  • if entering a new lod region (LODRange), based on the movement of the camera
  • if the forced lod changed
  • if the "DistanceScale" value changed <- this changes when the Fov is changed btw.
  1. Depending on the execution order of the spawning system and handling of the structural changes, the LocalTransform component might not be correctly initialized leading to a wrong computation for the distance thats used for the camera movement condition.
  2. If the object itself is moving.

Also after some time the first condition is triggered because the movement calculation for the camera does a ceil(..) operation before converting into fixed16 (EntitiesGraphicsSystem.cs@Line1436).

At this point, I guess this is expected behaviour, as most of the objects in the MegaCity Demo didn't move iirc.
Maybe this constrain is listed somewhere and I didn't read it..

1 Like

1) Structural changes should NOT happen between PresentationSystemGroup and when the culling callbacks happen. Otherwise, the whole EntitiesGraphicsPipeline works with wrong data. That's why there's no EndPresentationEntityCommandBufferSystem.
2) I plan to completely rewrite the LOD system in my framework to support LOD Crossfade. I already have the BRG part working. I just need to rework the ECS components and update routine. If this is something you'd like to be involved with, let me know!

1 Like

Hi,

yes that's very important. In my case everything is done via the Begin/End-InitCommandBufferSystems.
But I mixed up the order in two of the spawning systems, which leads to the Entity being setup over two frames, instead of one.
E.g in frame0 SystemA does some stuff and in frame1 SystemB does the rest, but both should be executed in frame0.
In my case the Lod did also run in frame0 and picks up that entity which wasn't setup completly...

Wow, looked at the thread for your framework, great work! Nice to know, currently I'm exploring some ideas on how to deal with this.
I don't want to change to much in the package, so I think, I'm only adding some sort of tag, that marks an entity as "moveable" and use that tag as filter for the LodSystem.

For the moving objects I would implement a new system, so we have atleast a specialized LodSystem for non-moving objects.


My quick fix was to just force the job to always run the first time each frame. I realized my latest release also had the issue, so the fix is still in a development version.

The grace distance caching is very fickle, because it doesn't play with shadows or multiple cameras in a well-behaved manner. I have been trying to think of an alternative caching strategy, as well as trying to think of a strategy for allowing all LODs to exist on the same entity by having the LOD system swap MMIs.

Oh good point, with that in mind, the LOD selection (sometimes) depend on the RenderPass that's executing.
Not sure how easy it is to create a seperate "World" inside the Renderpipline and have the LODsystems there, like a "Culling World". I already have the HDRP as a local package, so it's not much work to test that.

The BatchRendererGroup (and therefore the OnPerformCulling) seems to be invoked by the Engine as
the function has the "RequiredByNativeCode" Attribute? I guess that makes it a bit more complicated.


I don't think having a separate world helps. The issue is that for caching to be effective, you need a cache for each object that could request culling from the SRP (cameras, shadows, cubemaps, ect). Each one of those will result in an OnPerformCulling() callback. In my framework, I have that callback call Update() on a ComponentSystemGroup where I have a bunch of systems that do the LOD and culling and draw command generation, along with some other fancy things. It would be trivial to add multiple different LOD algorithms that operate side-by-side using WriteGroups or similar. The challenging piece is just coming up with something performant.

But really the challenge for me is just having a project that can stress-test a new LOD implementation in the first place, especially one that uses LOD Crossfade.

You could try to implement something similar to this one mentionend here (starts around 41min till 54min).


Here are additional informations for the reference in the video https://advances.realtimerendering.com/s2015/


That's not really achievable in 2022 LTS with BatchRendererGroup. And also, the technique has the major downside in that you have to sync your entire render state with the GPU. That can actually do a lot more harm than good depending on the use case.

Really, I want a CPU-based technique that can store some chunk-level cache that for any CPU view can more efficiently determine something about LOD levels rather than having to check each entity individually.

And CPU-based occlusion culling is another hot topic that I've been puzzling over for quite a while now. It is so easy to do something that actually makes things worse in practice.