To render large instance counts efficiently, BRG uses a new shaderA program that runs on the GPU. More info
See in Glossary instancing mode called DOTS Instancing. Every shader that BRG uses must support DOTS Instancing. In traditional instanced shaders, the shader is passed an array for each instanced property in a constant or uniform buffer, such that each element in each array contains the property value for a single instance in the draw. In DOTS Instanced shaders, Unity passes one 32-bit integer to the shader for each DOTS Instanced property. This 32-bit integer is called a metadata value. This integer can represent anything you want, but typically it represents an offset in the buffer from where the shader loads property data for the instance that the shader is rendering.
DOTS Instancing has many advantages compared to traditional instancing, including the following:
To support DOTS Instancing, a shader needs to do the following:
#pragma target 4.5
or higher.DOTS_INSTANCING_ON
keyword. Declare this with #pragma multi_compile _ DOTS_INSTANCING_ON
.Note: Shader Graphs and shaders that Unity provides in URP and HDRP support DOTS Instancing.
To load instance data, such as transform matrices, the shader needs to define DOTS Instanced properties. Below is an example of a simple DOTS Instanced property block:
UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP(float4, Color)
UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)
To mark the beginning and end of the property block, use the UNITY_DOTS_INSTANCING_START
and UNITY_DOTS_INSTANCING_END
macros followed by the name of the block. The example uses the name MaterialPropertyMetadata
. There are three allowed block names:
The shader can declare one of each, so a DOTS Instanced shader can have between zero and three of such blocks. Unity-defined shader code doesn’t use UserPropertyMetadata so this name is guaranteed to be free for you to use. URP and HDRP define BuiltinPropertyMetadata for every shader they provide and define MaterialPropertyMetadata for most of them too, so it’s best practice to use UserPropertyMetadata. Your custom shaders can use all three possible names, even all at once.
The block can contain any number of DOTS Instanced property definitions formatted like:
UNITY_DOTS_INSTANCED_PROP(PropertyType, PropertyName)
PropertyType
can be any HLSL built-in type (like uint, float4, float4x4, or int2x4) except a bool vector, and PropertyName
is the name of the DOTS Instanced property. DOTS Instanced properties are completely separate from regular material properties, and you can give them the same name as another regular material property. This is possible because the UNITY_DOTS_INSTANCED_PROP
macro generates special constant names which Unity recognizes that don’t conflict with other property names. Shaders that Unity provides give DOTS Instanced properties the same names as regular material properties, but you don’t need to follow this convention.
Internally, Unity provides the shader with a 32-bit integer metadata value for every DOTS Instanced property the shader declares. Unity sets the metadata value when your code makes a BatchRendererGroup.AddBatch call to create the batch associated with the draw. The metadata value defaults to 0
if Unity doesn’t set it. The shader also has access to ByteAddressBuffer unity_DOTSInstanceData
which Unity sets to the GraphicsBuffer you pass as an argument to BatchRendererGroup.AddBatch
. This buffer is typically where the shader loads the instance data from. Multiple batches can share a single GraphicsBuffer, but it is also possible for each batch to use its own separate GraphicsBuffer for unity_DOTSInstanceData
.
Note: Unity doesn’t provide any DOTS Instanced data automatically. It’s your responsibility to make sure that the unity_DOTSInstanceData
buffer of each batch contains the correct data. Instance data must include many properties that are Unity normally provides for GameObjectsThe fundamental object in Unity scenes, which can represent characters, props, scenery, cameras, waypoints, and more. A GameObject’s functionality is defined by the Components attached to it. More info
See in Glossary, such as transform matrices, light probeLight probes store information about how light passes through space in your scene. A collection of light probes arranged within a given space can improve lighting on moving objects and static LOD scenery within that space. More info
See in Glossary coefficients, and lightmapA pre-rendered texture that contains the effects of light sources on static objects in the scene. Lightmaps are overlaid on top of scene geometry to create the effect of lighting. More info
See in Glossary texture coordinates.
To access DOTS Instanced properties, your shader can use one of the access macros that Unity provides. The access macros assume that instance data in unity_DOTSInstanceData
uses the following layout:
unity_DOTSInstanceData
buffer.0
, every instance uses the value from instance index zero. This means each instance loads directly from the byte address in the metadata value. In this case, the buffer only needs to store a single value, instead of one value per instance.1
, the address should contain an array where you can find the value for instance index instanceID
using AddressOfInstance0 + sizeof(PropertyType) * instanceID
. In this case, you should ensure that every rendered instance index has valid data in buffer. Otherwise, out-of-bounds access and undefined behavior can occur.You can also set the the metadata value directly which is useful if you want to use a custom data source that doesn’t use the above layout, such as a texture.
Unity provides the following access macros:
Access macro | Description |
---|---|
UNITY_ACCESS_DOTS_INSTANCED_PROP(PropertyType, PropertyName) |
Returns the value loaded from unity_DOTSInstanceData using the layout described above. Shaders that Unity provides use this version for DOTS Instanced built-in properties that don’t have a default value to fall back on. |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(PropertyType, PropertyName) |
Returns the same as UNITY_ACCESS_DOTS_INSTANCED_PROP , except if the most significant bit of the metadata value is zero, it returns a default value. The default value is the value of the regular material property with the same name as the DOTS Instanced property, which is why Shaders that Unity provides use the convention where DOTS Instanced properties have the same name as regular material properties. When using the default value, the access macro doesn’t access unity_DOTSInstanceData at all. Shaders that Unity provides use this access macro for DOTS Instanced material properties, so the loads fall back to the value set on the material. |
UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(PropertyType, PropertyName, DefaultValue) |
Returns the same as UNITY_ACCESS_DOTS_INSTANCED_PROP unless the most significant bit of the metadata value is zero, in which case this macroreturns DefaultValue instead, and doesn’t access unity_DOTSInstanceData . |
UNITY_DOTS_INSTANCED_METADATA_NAME(PropertyType, PropertyName) |
Returns the metadata value directly without accessing anything. This is useful for custom instance data loading schemes. |
For an example of how to use these macros, see Access macro example.
Alongside the access macros, Unity provides shader functions that load the values of constants directly from the draw command data. Shaders that Unity provides use these functions.
Unity provides the following shader functions:
Shader function | Description |
---|---|
LoadDOTSInstancedData_RenderingLayer |
Returns the renderingLayerMask for the draw command. |
LoadDOTSInstancedData_MotionVectorsParams |
Returns the motion vector generation mode for the draw command. This is formatted as a float4, which is what Unity shaders expect. |
LoadDOTSInstancedData_WorldTransformParams |
Returns whether to draw the instance with a flipped triangle winding. See FlipWinding. |
LoadDOTSInstancedData_LightData |
Returns whether the sceneA Scene contains the environments and menus of your game. Think of each unique Scene file as a unique level. In each Scene, you place your environments, obstacles, and decorations, essentially designing and building your game in pieces. More info See in Glossary’s main Directional Light is active for the instance. The main light can be deactivated for multiple reasons, for example if the light already included in light maps. |
LoadDOTSInstancedData_LODFade |
Returns the 8 bit crossfade value you set if the LODCrossFade flag is set. If the flag is not set, the return value is undefined. |
This section contains examples of the access macros that Unity provides and instructions for how to use these macros to access per-instance data and constant data.
In this example:
Color
is 0x80001000
.5
.Because the most significant bit is already set, the accessor macros don’t load defaults. This means that c0
, c1
, and c2
will all have the same value, loaded from unity_DOTSInstanceData
address 0x1050
.
void ExamplePerInstance()
{
// rawMetadataValue will contain 0x80001000
uint rawMetadataValue = UNITY_DOTS_INSTANCED_METADATA_NAME(float4, Color);
float4 c0 = UNITY_ACCESS_DOTS_INSTANCED_PROP(float4, Color);
float4 c1 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, Color);
float4 c2 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(float4, Color, float4(1, 2, 3, 4));
}
In this example:
Color
is 0x00001000
.5
.Because the most significant bit is not set, the accessor macros that fall back to defaults don’t access unity_DOTSInstanceData
. This means that:
c0
will contain the value from unity_DOTSInstanceData
address 0x1000
.c1
will contain the value of the regular material property Color, and cause a compile error if the Color property doesn’t exist.c2
will contain (1, 2, 3, 4)
because that was passed as the explicit default value.void ExampleConstant()
{
// rawMetadataValue will contain 0x00001000
uint rawMetadataValue = UNITY_DOTS_INSTANCED_METADATA_NAME(float4, Color);
float4 c0 = UNITY_ACCESS_DOTS_INSTANCED_PROP(float4, Color);
float4 c1 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, Color);
float4 c2 = UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_CUSTOM_DEFAULT(float4, Color, float4(1, 2, 3, 4));
}
The UNITY_DOTS_INSTANCED_PROP
macro has 3 variants:
UNITY_DOTS_INSTANCED_PROP_OVERRIDE_DISABLED(PropertyType, PropertyName)
UNITY_DOTS_INSTANCED_PROP_OVERRIDE_SUPPORTED(PropertyType, PropertyName)
UNITY_DOTS_INSTANCED_PROP_OVERRIDE_REQUIRED(PropertyType, PropertyName)
These macros allow you to specify if a property can be instanced or not at compile-time. It allows the access macros such as UNITY_ACCESS_DOTS_INSTANCED_PROP
to expand to more optimal code and can have significant impact on low-end platforms.
Here is an example of a DOTS Instanced property block using all the macro variants above:
UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
UNITY_DOTS_INSTANCED_PROP_OVERRIDE_SUPPORTED(float4, Color)
UNITY_DOTS_INSTANCED_PROP_OVERRIDE_DISABLED(float4, SpecColor)
UNITY_DOTS_INSTANCED_PROP_OVERRIDE_REQUIRED(float4, EmissionColor)
UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)
Here is what this declaration means:
Color
property can either be instanced or not. The correct loading path is selected dynamically depending on the property metadata high-bit.SpecColor
property is not instantiable. This declaration won’t add an uint32 field in the constant buffer. It is equivalent to not declaring anything at all. It can be useful to quickly disable instancing on a property without the need to modify other parts of the code.EmissionColor
property must be instanced. The property is always loaded from the unity_DOTSInstanceData
buffer, and no dynamic branch is ever emitted when accessing the property.By default, UNITY_DOTS_INSTANCED_PROP
is the same as UNITY_DOTS_INSTANCED_PROP_OVERRIDE_SUPPORTED
. This default behavior can be changed by uncommenting the define UNITY_DOTS_INSTANCED_PROP_OVERRIDE_DISABLED_BY_DEFAULT
in “com.unity.render-pipelines.core\ShaderLibrary\UnityDOTSInstancing.hlsl”. When you do this, the define is enabled, and UNITY_DOTS_INSTANCED_PROP
becomes the same as UNITY_DOTS_INSTANCED_PROP_OVERRIDE_DISABLED
.
Note: When uncommenting the define UNITY_DOTS_INSTANCED_PROP_OVERRIDE_DISABLED_BY_DEFAULT
, you might need to clear the Library folder to make sure the shaders are correctly recompiled.
On low-end devices, instanced properties can have a significant performance cost. Loading from an SSBO for example, can be a lot slower than a normal constant buffer load. This is because on many low-end devices, this type of buffer load goes through texture samplers, whereas constant buffer loads uses faster hardware unless a dynamic index is used to access the buffer. Instanced properties are always loaded with dynamic indexing since it depends on the property metadata, this means they always go through the texture samplers on low-end devices.
To better optimize your project for low-end devices, you can disable property instancing by default. To do this, enable the define UNITY_DOTS_INSTANCED_PROP_OVERRIDE_DISABLED_BY_DEFAULT
, this sets property instancing to be disabled as default. Once this is done, you can then enable property instancing manually only for the properties that require it.
It is best practice to initialize the first 64 bytes of all unity_DOTSInstanceData
buffers to zero and leave them unused. This is because the default metadata value that Unity uses for all metadata values not specified during batch creation is zero. Specifically, when a shader loads a zero metadata value from the UNITY_ACCESS_DOTS_INSTANCED_PROP
macro, the shader loads this value from the address zero
because the instance index will be disregarded. Ensuring that the first 64 bytes, which is the size of the largest value type (a float4x4 matrix), are zeroes guarantees that such loads predictably return a result of zero. Otherwise, the shader could load something unpredictable, depending on what happens to be located at address zero.
When using DOTS Instancing, Shader Graphs and Shaders that Unity provides use a special convention for transform matrices. To save GPU memory and bandwidth, they store these matrices using only 12 floats instead of the full 16, because four floats are always constant. These shaders expect floats formatted in such a way that the x, y, and z of each column in the matrix are stored in order. In other words, the first three floats are the x, y, and z of the first column, the next three floats are the x, y, and z of the second column, and so on. The matrices don’t store the w
element of each column. The transform matrices this affects are:
unity_ObjectToWorld
unity_WorldToObject
unity_MatrixPreviousM
unity_MatrixPreviousMI
The following code sample includes a struct that converts regular four-by-four matrices into the 12 floats convention.
struct PackedMatrix
{
public float c0x;
public float c0y;
public float c0z;
public float c1x;
public float c1y;
public float c1z;
public float c2x;
public float c2y;
public float c2z;
public float c3x;
public float c3y;
public float c3z;
public PackedMatrix(Matrix4x4 m)
{
c0x = m.m00;
c0y = m.m10;
c0z = m.m20;
c1x = m.m01;
c1y = m.m11;
c1z = m.m21;
c2x = m.m02;
c2y = m.m12;
c2z = m.m22;
c3x = m.m03;
c3y = m.m13;
c3z = m.m23;
}
}