Repository navigation
[Feature] Select LOD levels by the size of the entity on screen - #1322
Conversation
The LOD chain of a pack model gives each level a screen size, and the loader turned it into a distance for each placement as the pack loaded: right for the scale and the field of view of that moment only. A placement scaled afterwards, or a zoom, kept switching at the old distances. A LOD component can now select by screen size (LODComponent.selectsByScreenSize): each level ends at the distance where the entity covers the screenPercentage of the next one. The size is read as each LOD pass runs, from the sphere around the entity's bounds under its world scale and from the projection in use. It is a share of the viewport height whatever the resolution, so a denser display draws the same levels. The bias and the hysteresis act on these distances as they do on maxDistance. The placements of a pack select this way. The parts of a model with several nodes carry the radius of the whole model (LODComponent.screenSizeRadius) and change level together. The distance of the load stays in maxDistance: it serves an orthographic view, and a level without a screen size. LODLevel.screenPercentage was stored and never read. A component selects by it only when told to, so entities that were given distances keep them.
|
Nice work on this one — the screen-size-based switching is a clean fix for the problem #1299 left open, and the write-up made it easy to follow the reasoning. Two small things worth a look, neither blocking:
Nothing here should hold up the merge — great addition. |
…he last frame The LOD update runs before the eyes are rendered, so in stereo the reach it took from renderInfo.perspectiveSpace was the second eye's of the previous frame, and on the first pass of a headset session the window's projection. The update now takes, in stereo, the larger of the two eye projections' reaches from the frame rendered last, and until an eye has been rendered (the projections are still the identity) the stored distances apply, as under an orthographic projection. A headset's field of view does not change between frames.
|
Thanks, both looked at. The projection in stereo. You are right that the update reads what the previous frame left, and before any eye has been rendered it read the window's projection: the first LOD pass of a headset session chose its levels with the Mac window's field of view. Now in stereo the update takes the reach from the two eye projections of the frame rendered last, the larger of the two (so an entity is drawn at the level the eye that sees it largest asks for), and until an eye has been rendered the projections are still the identity and the stored distances apply, as they do under an orthographic projection. A headset's field of view does not change between frames, so the one-frame lag has no effect after the first pass. Two tests cover the choice and the first frame; the architecture doc says where the reach comes from in stereo. Serialization. Not tracked as an issue yet, only as a follow-up in the plan: a scene saved by the editor reloads its placements one by one, not through the pack loader, so neither the shared builds nor the chains of multi-node models survive a save, and these two fields go with that. I opened #1326 for it, with the three things a saved scene loses in one place. |
Summary
Stage 1d of
docs/proposals/LargeSceneRendering.md, the half that #1299 left open. A level of detail is chosen by the camera's distance to the entity, and a distance suits one size of object under one field of view: a chain cooked for a tree is wrong for the same tree placed at half its scale, and every chain is wrong once the camera zooms. An entity can now choose its level by how large it is on screen: each level ends where the entity covers thescreenPercentageof the next one, read as the LOD pass runs from the entity's world radius and the projection in use.The sheet is a test model with the chain
untoldengine bake-lodswrites for it, from one camera position. Top row, a 60° field of view: the model is small on screen and both rules draw its coarsest level. Bottom row, the camera zoomed to 20°: the distance has not changed, so the distance rule keeps the coarsest level, now large and visibly simplified; the screen-size rule draws the full model.The levels of the automatic chains of a
.untoldpack(#1299) are selected this way. #1299 turned each level's screen size into a switch distance when the pack loaded, from the field of view of that moment; the switch now follows the field of view and the entity's scale as they are, and a placement scaled after the load switches at its new size.Changes
LODComponent.selectsByScreenSize(opt-in; the pack loader sets it on the levels of a chain) andLODComponent.screenSizeRadius: the radius, in the entity's own space, of the sphere measured on screen; 0 measures the sphere around the entity's bounding box. The parts of a pack model of several nodes carry the radius of the whole model, so they switch together.LODSystem:lodScreenSizeReach(projection:)readsprojection[1][1](that is1 / tan(fovY / 2)) once per update, nil under an orthographic projection. In stereo the eyes are rendered after the update, so the reach is the larger of the two eye projections' from the frame rendered last, and until an eye has been rendered the stored distances apply (the review's first point). For a component that selects by screen size, level i ends atworldRadius × reach / screenPercentage(i + 1); the bias and the hysteresis apply to these distances as to stored ones. A level whose successor has no screen size, and every level under an orthographic projection, ends at its storedmaxDistanceas before.setLOD(.distanceBias(...))is the knob for a platform that wants more or less detail. (A first version scaled the size by the viewport's pixel height; a Retina window or a headset then drew several times the triangles for detail that is not seen. Unreal and Unity do not scale it either.)LODLevel.screenPercentageexisted, was serialized and was never read; existing tests set it to arbitrary values, which is why the selection is opt-in rather than automatic for every component with a size.docs/API/UsingLODSystem.md("Selecting by Screen Size") anddocs/Architecture/lodSystem.md.Verification
LODScreenSizeTests(14): the reach of a projection, the switch distance for a radius and a screen size, levels without a size, the orthographic fallback, the bias and the hysteresis on computed distances,screenSizeRadius, and that a component without the flag selects as before.UntoldPackLODRenderTests(+4, 15 in all): a placement selects its levels by its size on screen and its parts carry the model's radius; a placement scaled after the load switches at its new size; halving the field of view moves the switches out and doubling the resolution does not; a view without perspective switches at the distances of the load.swift test --filter UntoldEngineTests: 1,640 run. The only failure isExternalRenderExtensionPackageTests, which fails locally whenever the checkout folder is not namedUntoldEngine; it passes from a checkout with that name.compare_psnr.pythat does the same arithmetic without OpenCV and scikit-image, which this machine does not have: 14 match their reference exactly, the lowest of the others is at 68 dB against thresholds of 11 to 30.scripts/strict-concurrency-guardrails.shwithMAX_UNIQUE_CONCURRENCY_WARNINGS=0); no new SwiftFormat findings in the changed files.Limits
addLODLevelwith distances only. Tracked in [Bug] A scene saved by the editor loses what the pack loader set up: shared builds, LOD chains, and the screen-size selection #1326 with the rest of what a saved scene loses (shared builds and chains do not survive a save either).