Level of Detail (LOD)

This feature requires baking.

Motivation

coherence can support large game worlds with many objects. Since the amount of data that can be transmitted over the network is limited, it's very important to only send the most important things.

You already know a very efficient tool for enabling this – the LiveQuery. It ensures that a client is only sent data when an object in its vicinity has been updated.

Often though, there is a possibility for an even more nuanced and optimized approach. It is based on the fact that we might not need to send as much data for an entity that is far away, compared to a close one. A similar technique is often used in 3D-programming to show a simpler model when something is far away, and a more detailed when close-up.

This idea works really well for networking too. For example, when another player is close to you it's important to know exactly what animation it is playing, what it's carrying around, etc. When the same player is far off in the horizon, it might suffice to only know it's position and orientation, since nothing else will be discernible anyways.

To use this technique we must learn about something called archetypes.

Levels of Detail

Any Prefab with the CoherenceSync component can be optimized to use a various levels of details (LODs).

There must always exist a LOD 0, this is the default level and it always has all components enabled (it can have per-field overrides though, see below.)

There can be any number of subsequent LODs (e.g. LOD 1, LOD 2, etc.) and each one must have a distance threshold higher than the previous one. The coherence SDK will try to use the LOD with the highest number, but that is still within the distance threshold.

Example

An object has three LODs, like this:

  • LOD 0 (threshold 0)

  • LOD 1 (threshold 10)

  • LOD 2 (threshold 20)

If this object is 15 units away, it will use LOD 1.

Confusingly, the highest numbered LOD is usually called the lowest one, since it has the least detail.

On each LOD, there are two options for optimizing data being transferred:

  1. Components can be turned off, meaning you won't receive any updates from them.

  2. Its fields can be configured to use fewer bits, usually leading to less fine-grained information. The idea is that this won't be noticeable at the distance of the LOD.

Bits and range

coherence allows us to define the range of numeric fields and how many bits we want to allocate to them.

Here are some terms we will be using:

  • Bits. The number of bits (octets) used for the field. When used for vectors, the number defined the number of bits used for each component (x, y and z). A vector3 set to 24 bits will consume 3 * 24 = 72 bits.

  • Range. For integer values and fixed-point floats, we define a minimum and maximum possible value (e.g. Health can lie between 0 and 100).

More bits mean more precision. Increasing the range while leaving the bit count the same will lower the precision of the field.

The maximum number of bits used for any field/component is currently 32.

coherence allows us to define these values for specific components and fields. Furthermore, we can define levels of detail so that precision and therefore bandwidth consumption falls with the distance of the object to the point of observation.

Levels of detail are calculated from the distance between the entity and the center of the LiveQuery.

Defining levels of detail

On each LOD you can configure the individual fields of any component to use less data. You can only decrease the fidelity, so a field can't use more data on a lower (more far away) LOD. The Archetype editor interface will help you to follow these rules.

In order to define levels of detail, we have to click the Optimize button on a Prefab's CoherenceSync component with defined field bindings.

That opens the Optimization window. We can override the base component settings even without defining further levels of detail.

Clicking on Add new Level Of Detail will add a new LOD. We can now define the distance at which the LOD starts. This is the minimum distance between the entity and the center of the LiveQuery at which the new level of detail becomes active (i.e. the Replicator will start sending data as defined here at this distance).

You can also disable components at later LOD levels if they are not needed. In the example above, you can see that in LOD2 the entire Transform and Animator components are disabled beyond the distance of 20 units. At 100 units (a.k.a. meters), we usually do not see animation details, so we can save a lot of bandwidth and processing power by not replicating this data.

The Data Cost Overview shows us that this takes the original 913 bits down to just 372 bits at LOD level 2.

Field overrides per type

The primitive types that coherence supports can be configured in different ways:

Float, Vector2 & Vector3

These three types can all be configured in the same way, using different compression types:

None

No compression will be used, a full 32-bit float will be transmitted every time.

Truncated

Allows for specifying the number of bits for compression. Less bits means lower bandwidth usage but at the cost of precision loss. The minimum number of bits is 10. Using 22 bits will result in around half of the precision of the full float, while 16 will result in the quarter of the precision.

Fixed point

Allows for specifying the range of values used together with either number of bits or a desired precision.

  • Range affects the maximum and minimum value that the data type can take on. For example, a range of 100 to 200 means only values within that range can be sent - any value outside of this range will be clamped to the nearest correct value.

  • Precision defines the greatest deviation allowed for the data type. For example, a precision of 0.1 means that a float of value 10.0 can be transmitted as anything from 9.9 to 10.1 over the network. The minimum allowed precision is 0.1, while the maximum precision depends on the range. Changing precision automatically recalculates the number of bits required for given range.

  • Bits dictate how many bits to use when calculating the precision for a given range. When set manually, it will trigger recalculation of the precision for a given range. Mind that the number of bits can be rounded down if the calculated precision uses less, e.g. for a range of [0, 1] setting the number of bits to 6 will result in precision of 0.1 and a final bit count of 4, since 4 bits suffice to represent this range with a calculated precision.

When using these range settings for vectors, it affects each axis of the vector separately. Imagine shrinking its bounding box, rather than a sphere.

Integers

Integers can be configured to any span (that fits within a 32-bit integer) by setting its minimum and maximum value.

For example, the member variable age in a game about ancient trolls might use a minimum of 100 and a maximum of 2000. Based on the size of the range (1900 in this case) a bit-count will be calculated for you.

For integers, it usually make sense to not decrease the range on lower LODs since it will overflow (and wrap-around) any member on an entity that switches to a lower LOD. Instead, use this setting on LOD 0 to save data for the whole Archetype.

Quaternions & Colors

Quaternions and Colors can be configured using the number of bits per component. Quaternions require sending 3 components while Colors require 4 components.

Other types

All other types (strings, booleans, entity references) have no settings that can be overridden, so your only option for optimizing those are to turn them off completely at lower LODs.

Using LODs with connected entities

If a LODed game object is parented to another synced object, the child will base its LOD level on the World position of its parent. This means that the (local) position of the LODed child does not have any effect on its LOD, until it is unparented.

Also – to save bandwidth, detection of LOD changes on the client only happens when the entity sends a component update. This means that a child object might appear to be using a nonsensical LOD until it changes in some way, for example by modifying its position.

LODs in the schema

When we bake, information from the CoherenceArchetype component gets written into our schema. Below, you can see the setup presented earlier reflected in the resulting schema file.

archetype FemZombie
  lod 0
    WorldPosition 
      value [compression "FixedPoint", range-min "-2400", range-max "2400", bits "19", precision "0.01"]
    WorldOrientation 
      value [bits "24"]
    FemZombie_UnityEngine_Animator 
      InputHorizontal [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      InputVertical [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      InputMagnitude [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      TurnOnSpotDirection [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      ActionState [bits "15", range-min "0", range-max "10"]
  lod 1 [distance "50"]
    WorldPosition 
      value [compression "FixedPoint", range-min "-2400", range-max "2400", bits "16", precision "0.1"]
    WorldOrientation 
      value [bits "20"]
    FemZombie_UnityEngine_Animator 
      InputHorizontal [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      InputVertical [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      InputMagnitude [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      TurnOnSpotDirection [compression "FixedPoint", range-min "-1", range-max "1", bits "15", precision "0.0001"]
      ActionState [bits "15", range-min "0", range-max "10"]
      IdleRandom [bits "15", range-min "-9999", range-max "-9999"]
      RandomAttack [bits "15", range-min "-9999", range-max "-9999"]
      AttackID [bits "15", range-min "-9999", range-max "-9999"]
      DefenseID [bits "15", range-min "-9999", range-max "-9999"]
      RecoilID [bits "15", range-min "-9999", range-max "-9999"]
      ReactionID [bits "15", range-min "-9999", range-max "-9999"]
      HitDirection [bits "15", range-min "-9999", range-max "-9999"]
  lod 2 [distance "100"]
    WorldPosition 
      value [compression "FixedPoint", range-min "-2400", range-max "2400", bits "16", precision "0.1"]
    WorldOrientation 
      value [bits "16"]

If you want to know more about how LODs work inside the schema files, take a look at Archetypes.

Caveats

The most unintuitive thing about archetypes and LOD-ing is that it doesn't affect the sending of data. This means that a "fat" object with tons of fields will still tax the network and the Replication Server if it is constantly updated, even if it uses a very optimized Archetype.

Also, it's important to realize that the exact LOD used on an entity varies for each other client, depending on the position of their query (or the closest one, if several are used.)

Last updated