@kognifai/cogsengine
    Preparing search index...

    Handling of level-of-detail in the Cogs Asset System

    Cogs Asset System handles dynamic LOD display of geometries defined in AssetComponent.asset field.

    Asset System is initialized when creating an Asset entity and setting the AssetComponent.asset field. Note that also the AssetComponent.flags should be set for correct loading of the asset.

    AssetComponents with InstantiateOnDemand and with lods somewhere in its .asset-file hierarchy get dynamic lod'ing.

    In the .asset-file, the lod levels are described by entities with type LodGroup. The lod levels are specified in the lods array, with corresponding errors in the errors array.

    Each error corresponds to the error of the model, and the first element of the lod array is the most detailed level. For example,

    {
    "entities": [
    {
    "type": "LodGroup",
    "properties": {
    "bbox": [x_min, y_min, z_min, x_max, y_max, z_max],
    "errors": [a, b, c, d]
    },
    "lods": [

    {
    "name": "A",
    "type": "LodGroup",
    "properties": { "bbox": [...], "errors": [..]},
    "lods": [...]
    },

    {
    "name": "B",
    "model": {
    "source": "somefile.cogsbin",
    "part": 0
    }
    },

    {
    "name": "C",
    "model": {
    "source": "somefile.cogsbin",
    "part": 1
    }
    },

    {
    "name": "D",
    "type": "Asset",
    "asset": {
    "source": "someotherfile.asset"
    }
    }
    ]
    }
    ]
    }

    Here, A is the most detailed lod level valid from error=0 to error=a. It is itself a lodgroup, which is expanded when that lod-level is active.

    B is the middle detailed lod level. This is instances a model, in particular the model in "somefile.cogsbin", the first root element (due to "part": 0).

    C is the next to least detailed lod level and it is also a model. It is in the same file as B, but is the 2nd root element ("part": 1). Thus, it is possible to pack multiple models inside the cogsbin file to avoid TBD.

    Finally D is the least detailed lod level, and it is described in another asset file.

    To choose a lod level, we calculate a tolerance and try to choose a lod-level below the error threshold. This starts at the root asset file and is done recursively, loading resources on demand.

    The AssetComponent has a tolerance property that is the starting point.

    The tolerance is scaled by the distance between the camera eye point and the closest point inside the asset's bounding box. This distance is clamped to the maximum of the component's minDistance and the camera's nearPlane, enforcing that the tolerance never is scaled by zero (which would make it impossible to force coarse lod-levels when the camera was inside the asset's bounding box).

    In addition, if forceTolerance is false, the tolerance is scaled by number that gets adjusted based on projected resource usage. Currently this the min and max value of this scaling are both 1 with no apparent way to modify these values, so this scaling is effectively disabled.

    Finally, if forceTolerance is false, if the quality-system is operational, it calculates an additional scale factor. This is controlled by the variables:

    • quality.assetSystem.toleranceScale.coarse: This is the scaling factor at 0% quality and should be a number larger than 1 (i.e. to decrease detail, increase tolerance).

    • quality.assetSystem.toleranceScale.detailed: This is the scaling factor at 200% quality and should be a number less than 1 (i.e. to increase detail, reduce tolerance).

    The asset system scale factor at 100% is currently always 1, i.e. no effect. This may change in the future.

    Model loading is controlled by the two variables that defines two conditions:

    • resources.assets.maxModelsInFlight: The maximum number of model load requests that can be in-flight at the same time.

    • resources.assets.maxModelLoadsPerFrame: The maximum number of model load requests that is issued in a single frame.

    New model load requests are issued when these two conditions are satisfied.

    COGS culls away parts of the hierarchy that it can guarantee is outside of the view frustum. Thus, you want to chunk the pieces so that a piece that is visible doesn't have to much geometry outside of the screen. The smaller the pieces are, the more precise this culling becomes, but - issuing a draw call is expensive, and this pushes for large pieces.

    A compromise is to have pieces that fit maybe a quarter of the screen up to fit the screen, but not larger than the screen. But what fits on the screen is dependent on how far away it is, and that is the point of the LOD hierarchy.

    The LOD hierarchy is organized by distance from the camera (that is the number in the .asset-file). From a to b meters from the camera, use these models, from b to c, use these etc. So, if we can figure out what fits inside the camera over these ranges, we have some guide to how big the pieces are.

    And this depends on FOV. The COGS camera vertical FOV defaults to pi/4 (45 degrees), so at e.g. 10m distance the height of the camera view is 2sin(pi/8)*10 ~= 0.77 * 10m.

    This is linear, so that at 50m the height is 0.77 * 50m etc.

    Thus, at the range e.g. from 10m to 50m you will see this version of the model. In this range, the height of the camera view is from 0.77*10m = 7m to 0.77*50m = 38.5m. A starting point is to make chunks fit the full screen at the nearest distance. So for this range, chunks of size 7m. At the furthest distance of the range, 50m, 38.5/7=5.5 chunks span the screen. Thus, this can also be used to guide how big the LOD distance ranges should be.

    Now, how man triangles should a chunk have? First off, a triangle sized less than a pixel is a total waste, less than 2-3 pixels wide starts to get really small. For a middle-manager laptop, the screen is maybe 1920 x 1080. If we say that we do not want pixels smaller than 3 pixels, this is 1080/3=360 triangles across. At e.g. 10m, the distance vertically across the screen is 7m, so a triangle shouldn't have a size larger than 7m/360 ~= 2cm. So a pixel that spans less than 2cm is just waste.

    To get a grip of total triangle budget, assume that we are willing to use 100MB of geometry data. This will be represented at least twice (100MB cpu side data for picking + 100MB gpu side data for rendering).

    A vertex is typically position-normal-texcoord=8*4=32 bytes per vertex. Each triangle is 3*4=12 bytes with index data. It is obviously possible to encode data smarter, but we use this as a starting point.

    For a closed triangle mesh with genus 0, the Euler-Poincaré formula gives us V=2F, i.e. two triangles per vertex. So with index data we have 32 bytes + 2 * 12=56 bytes per vertex. So 100MB allows approx 1.8 million triangles. So if we have 7 chunks across the screen and 7 chunk of depth, each chunk should have about 38k vertices. Also, keeping models below 65k vertices allows us to use 16-bit indices, which reduces vertex footprint to 32 + 2*3*2=50 (i.e shaving off 11%). (edit: sphere has genus 0) (edited)

    If there are multiple layers on top, this number should be further divided.

    Paths to child resources within an asset file can be replaced with a 32-bit resource index, instead of a string. The resource index of an asset file is the path relative to the parent asset produced by the printf string

    "%08x.asset", resourceIndex
    

    and similarily for models

    "%05x/%03x.cogsbin", (resourceIndex >> 12u) & 0xFFFFFu, resourceIndex & 0xFFFu
    

    that is, there are folders with 4096 and 4096 sequentially ordered files, this to avoid an folders with a troublesome number of files.

    The reason for this is to avoid representing a huge amount of strings with paths in memory and instead generate the paths dynamically when needed.

    In addition, both .asset files and .cogsbin models can be compressed with zstd. The loaders for these types check the start of the files for zstd magic number and decompresses transparently, so it isn't necessary to add .zst suffices.

    The benefit of not having to change suffices is that parts of an hierarchy can be compressed/decompressed for inspection or packing without having to change the parent asset.