LOD/Batching/Culling/Collision solution all in one?

Note: (Postings of WIP)

Placing a bunch of prop-items into your 3D game scene and need robust LOD/Batching/Culling/Collision support for any large world/level/map, with all the runtime performance options necessary to help balance memory, draw calls and polycount? I’m trying to come up with a prop-item system suitable for end user which would hopefully be “the last LOD solution you’ll ever need”.

The design:

So, the user create an entity in some “library” reference portion of the scene graph tree that is used purely for initialization. Add a LOD script which provides the following attributes for the given prop-item:

LODsModel: [asset.Model]
LODsThresholdBoundDistRatio: [Number]
LODsUse2DThresholdDistBoundRatio: [Boolean]
Positions: [Vec3/mixed]
UseHierachicalBoundsAboveCount: Number  :: 1
LODsBoundBatchSize: [Number]
LODsStaticBatchLimit`: [Number]
LODsDynamicBatchOrCulling: [String]
LODsMaxBoundingBoxCullingSizes [Number]
LODsShader [asset.Shader]
BillboardTexture: [pc.Texture]
BillboardScheme: [..options]
TruncateTree: [Boolean]
ExportTree: [Boolean]

Attributes are explained below:

  • LODsModel: Array of asset models to use for different LOD levels (LOD0 =…highest level of detail, proceeding onwards to lower levels of detail) for given prop-item. If left undefined at given slot, will attempt to search current entity’s children list for the list of models via some given convention, taking into account relative transforms per LOD level.

  • LODsThresholdBoundDistRatio: Distance in bound units (in ratio units of prop-itembound’s diameter) from given prop-item before LOD level is used. If you are within that given distance, that LOD level is used. The prop-items 's bounding Box is determined based off supplied Collision volume of the entity, or through some scene graph convention within the entity. This bounding box is expected to be conservative, ie. it must fit all LOD levels, taking into account any animations, etc. and needs to be big enough to avoid any negative results when it comes to frustum culling. If bounding box is left undefined, a default implementation is used to determine it based off the union of all LOD geometries.

  • LODsUse2DThresholdDistBoundRatio: Whether to use 2D distance (x and z axes only) instead of 3D distance (all axes) for LODsThresholdBoundDistRatio. In some cases, this is useful for billboard-like stand-up models where you still want to keep higher LOD level (non-billboard mesh) to be used when looking downards at the tree from high above but are still considered “close” to the model in 2D space.

  • Positions: A list of positions to spawn for given prop-item. This can be implied from the scene graph node of the entity itself or via some convention or external reference node. Or can be supplied via code prior to generating the model prop-items /LODs.

  • UseHierachicalBoundsAboveCount: A hierchical bounding volume tree is generated from all positions of given LODsModel if number of spawned prop-items exceed this given count. A hierahical bounding volume tree of bounding boxes is useful for hierchically frustum culling of many multiple prop-items (particularly numerous prop-items ) and is necessary if you are using LODsBounds to generate static/dynamic prop-item batches off parent containing nodes of the tree. At the same time, it is useful for optimizing collision queries over large number of prop-items in the gameworld.

  • LODsBoundBatchSize: If the diameter distance a given bounding box node in the hierahical bounds tree ends up being equal or lower than this stipulated threshold value (bound diameter size in world units) at a given (lowest index) LOD level, a “batch” is designated for that given node. By default, this batch will be a baked static mesh of all prop-item positions at that resultant LOD level under that given node being combined. This will obviously add to the memory overhead. A static pre-baked mesh cannot support any culling whatsoever of individual prop-items within it because it has all been combined. This batch is only rendered if the player is located “far enough” from the AABB (axis aligned bounding box) of that given node. If it fails the test, the batch doesn’t get rendered and the render-consideration tree is recursed in deeper to consider which further nodes down the tree may get rendered.

  • LODsStaticBatchLimit: By default, when left at zero or undefined, it’s implied “auto: Will only allow up to 1 static batch model to be generaated for that given LOD under a particular level of the bounding volume hierachy tree so long as dynamic batching isn’t triggered in LODsDynamicBatchOrCulling (if dynamic batching is enabled for that LOD level, then no static models are baked under auto)”. In any case, it won’t create any more static batch models further down to tree with a value of either zero/undefined or 1. If set to higher values than 1, it means that if recursing down the bounding volume hierachy tree of nodes might still involve node bounds that still fall within the current LOD’s LODsBoundBatchSize, and if so, will continue creating another pre-baked static models at that level up until the given limit. So long as the value is explicity set to “1” or higher, it will always create static models up until that limit, even if LODsDynamicBatchOrCulling is enabled, but after that limit is reached, dynamic batching based off LODsDynamicBatchOrCulling would trigger accordingly if available to manage prop-items without any static model batches for it at a given node tree level.

  • LODsDynamicBatchOrCulling: you may have 1 or 2 integer sepreted by a / slash character to indicate dynamic batching or/and culling. The left(first) portion indicates whether to assert to use Dynamic (repeater-style) batching instead of the default Static batching per batch in order to conserve memory compared to static batching, at the cost of a bit more processing/management but still keeping draw calls low. Use 1 to indiciate DYnamic batching always. Use any integer greater than 1 to indicate conditionally dynamic batching if the amount of prop-items at that given node falls down to less than or equal that value. The 2nd integer portion after a slash indiciates how much culling to perform (if any) for a given LOD level. You can also perform culling without relying on Dynamic batching as well, but won’t receive benefits of reduced draw calls that come with batching in general under that given LOD. Dynamic batching is intended to involve the use of duplicated meshInstances within a single LOD Model and controlling them remotely within the draw call list of the respective draw layer of that batch by altering/shifiting their transforms per duplicated meshInstance in the linear list, then clamping down the primitive draw call triangle count for the Batch’s combined MeshInstance to dynamically match the number of “visible” prop-items required to be rendered within the scene. (I hope this “trick” is possible to pull off with Playcanvas’ engine codebase without jumping too many hoops.)
    Example dynamic batching/culling values you can use for a given LOD level:

    • 1 - If bounded batch is being rendered for this LOD level, it will always be rendered it as a dynamic batch of pre-arranged prop-items instead from the batch’s given meshInstance list buffer for that LoD. Culling is skipped and it’s assumed everything at that given node downwards for that LOD level is visible.
    • 1/5 - Performs dynamic batching always. Culls in groups of 5 (or less) prop-items down to their respective nodes.
    • 8/5 - Performs dynamic batching only if a total of 8 or less prop-items are within current node’s leaf descendants. Culls in groups of 5 (or less) prop-items down to their respective nodes.
    • 1/1 - Performs dynamic batching always. Culls down to each and every individual prop-item.
    • 8/1 - Performs dynamic batching only if a total of 8 or less prop-items are within current node’s leaf descendants. Culls down to each and every individual prop-item.
    • 0/1 - No dynamic batching. Culls down to each and every individual prop-item.
    • 0/5 - No batching. Culls down to groups of 5 items or less each.
  • LODsMaxBoundingBoxCullingSizes: If hierachical bounding volume hierachy tree is used and a value is specificed at given LOD, this will assert continually culling down to smaller hierchical bounding volumes during culling, so long as max size (ie. diameter of the bounds) of a given node is exceeded.

  • LODsShader: Any custom shaders to use for a specific LOD level for the specific LODsModel.

  • BillboardTexture: If supplied, adds another “last LOD level” to act as billboard texture and auto-generates geometry/shader required for given BillboardScheme.

  • BillboardScheme: Geometry type for Billboard. Uses “cross-cross mesh” by default. Or optionally, use “z-locked” sprite with a specific shader to align to camera view. Other options like “facing sprite” and respective non-view-aligned shader method (ie. cross product, rotate directly to face camera position) variants can be used. But likely, given the nature of LOD, i think the first 2 options (or typically just the 1st one) is most common and useful.

  • TruncateTree: If you are using the Hierchical Bounding Volume tree purely for rendering and do not wish to reuse it for individual object collision testing within individual object leaf’s nodes (or only wish to re-create sub-trees on demand for collisions), you may check this option to clean up deeper nodes within the tree that are’t necessary anymore for rendering, depending on the culling options, in order to save memory.

  • ExportTree: Export Hierchical Bounding Volume tree in some JSON/file format…so it can be reloaded again instead of being re-calculated again.


https://playcanvas.com/editor/scene/639519 - Dynamic Batch Clone Repeater Test

One of the problems with Playcanvas’ Batch manager, is the BatchManager doesn’t provide (right out of the editor) a way to handle dynamic removal/addition of batched elements while maintaining constant geometry memory buffer footprint that can be reused across all draw call requests. However, it has some key methods that one may use for reference (namely BatchManager.create), which serve as a useful foundation to handle your own fixed buffers and dealing with buffer overflows. Since the engine code is open sourced, it’s fairly easy to involve a few workarounds/hacks via a Script, and thus be able to self-manage localised batch(es) of repeatable Models that may spawn/unspawn on demand without having to destroy/re-create entire batches, while not adding additional draw calls per instance and still giving you full control of polycounts by allowing you to decide exactly what gets rendered or not. Several custom methods were used to directly update matrixPalette values and move objects around (if you want to use actual pc.Nodes to make it easier to manipulate/transform spawned elements, you have to handle this seperately on your own though). When removing spawned elements in the middle of matrix palette area, it adopts a pop-back-into strategy (without splicing/resizing the matrix palette array), and simply updates the instanceLen (ie. number of triangles to draw) of the batch by updating batch.meshInstance.primitive[0].count to match the quantity of objects in the scene. In the end, what you get is something essential for batching a lot of extinguishable game elements like projectiles, bullet holes, ejected shell-casings, etc. or simply just cullable elements that can be culled by your app-specific methods instead, etc. Such a “resizable/self-managed” batch sets batch.meshInstance.cull = false by default since it’s left up to you to decide whether culling is necessary or not). You may manually set a (conservative/or not) bounding box _aabb (a helper method in the script would be useful) to the the respective batch’s meshInstance and set batch.meshInstance.cull = true if you want to continue to use Playcanvas’ default culling approach of the entire batch, though. But again, it all depends on the needs of your app.


Final streamlined feature list of what will be deemed configurable for the ultimate batched geometry system with optional culling/collision/LOD features.

Really interesting @Glenn_Ko, did you get anywhere with this, or find out any further info?

If you are interested for a batched + LOD solution the uranus-editor-entities-paint.js script has a runtime that supports it, using HW instancing.

1 Like