Skip to content

Commit

Permalink
Light-Batching for 2D
Browse files Browse the repository at this point in the history
JIRA : https://jira.unity3d.com/browse/D2D-3850
In 2D URP we currently use RenderingCommandBuffer to render a set of meshes for Lights/Volumes. This PR attempts to minimize API calls and CPU cost by using the recently added [DrawMultipleMeshes ](https://github.cds.internal.unity3d.com/unity/unity/pull/17488) API. 
Also did a bit of refactoring to make porting to RenderGraph easier. Also the size of Light struct in the Gfx Buffer is 128 bytes which is nice for alignment.
  • Loading branch information
venkify authored and Evergreen committed Dec 13, 2022
1 parent 68f74a5 commit 1baca98
Show file tree
Hide file tree
Showing 19 changed files with 800 additions and 500 deletions.
Expand Up @@ -102,6 +102,7 @@ static Light2D CreateLight(MenuCommand menuCommand, Light2D.LightType type, Vect
{
GameObject go = ObjectFactory.CreateGameObject("Light 2D", typeof(Light2D));
Light2D light2D = go.GetComponent<Light2D>();
light2D.batchSlotIndex = LightBatch.batchSlotIndex;
light2D.lightType = type;

if (shapePath != null && shapePath.Length > 0)
Expand Down
Expand Up @@ -3,6 +3,7 @@
using UnityEngine.Serialization;
using UnityEngine.Scripting.APIUpdating;
using UnityEngine.U2D;
using Unity.Collections;
#if UNITY_EDITOR
using UnityEditor.Experimental.SceneManagement;
#endif
Expand Down Expand Up @@ -177,6 +178,9 @@ private enum ComponentVersions
int m_PreviousLightCookieSprite;
internal Vector3 m_CachedPosition;

// We use Blue Channel of LightMesh's vertex color to indicate Slot Index.
int m_BatchSlotIndex = 0;
internal int batchSlotIndex { get { return m_BatchSlotIndex; } set { m_BatchSlotIndex = value; } }
internal int[] affectedSortingLayers => m_ApplyToSortingLayers;

private int lightCookieSpriteInstanceID => m_LightCookieSprite?.GetInstanceID() ?? 0;
Expand Down Expand Up @@ -369,7 +373,13 @@ internal Bounds UpdateSpriteMesh()
m_Vertices = new LightUtility.LightMeshVertex[1];
m_Triangles = new ushort[1];
}
return LightUtility.GenerateSpriteMesh(this, m_LightCookieSprite);
return LightUtility.GenerateSpriteMesh(this, m_LightCookieSprite, LightBatch.GetBatchColor(batchSlotIndex));
}

internal void UpdateBatchSlotIndex()
{
if (lightMesh && lightMesh.colors != null && lightMesh.colors.Length != 0)
m_BatchSlotIndex = LightBatch.GetBatchSlotIndex(lightMesh.colors[0].b);
}

internal void UpdateMesh(bool forceUpdate = false)
Expand All @@ -388,21 +398,25 @@ internal void UpdateMesh(bool forceUpdate = false)
// Mesh Rebuilding
if (hashChanged || forceUpdate)
{
var batchChannelColor = LightBatch.GetBatchColor(batchSlotIndex);

switch (m_LightType)
{
case LightType.Freeform:
m_LocalBounds = LightUtility.GenerateShapeMesh(this, m_ShapePath, m_ShapeLightFalloffSize);
m_LocalBounds = LightUtility.GenerateShapeMesh(this, m_ShapePath, m_ShapeLightFalloffSize, batchChannelColor);
break;
case LightType.Parametric:
m_LocalBounds = LightUtility.GenerateParametricMesh(this, m_ShapeLightParametricRadius, m_ShapeLightFalloffSize, m_ShapeLightParametricAngleOffset, m_ShapeLightParametricSides);
m_LocalBounds = LightUtility.GenerateParametricMesh(this, m_ShapeLightParametricRadius, m_ShapeLightFalloffSize, m_ShapeLightParametricAngleOffset, m_ShapeLightParametricSides, batchChannelColor);
break;
case LightType.Sprite:
m_LocalBounds = UpdateSpriteMesh();
break;
case LightType.Point:
m_LocalBounds = LightUtility.GenerateParametricMesh(this, 1.412135f, 0, 0, 4);
m_LocalBounds = LightUtility.GenerateParametricMesh(this, 1.412135f, 0, 0, 4, batchChannelColor);
break;
}

UpdateBatchSlotIndex();
}
}

Expand Down Expand Up @@ -434,6 +448,17 @@ internal bool IsLitLayer(int layer)
return false;
}

internal Matrix4x4 GetMatrix()
{
var matrix = transform.localToWorldMatrix;
if (lightType == Light2D.LightType.Point)
{
var scale = new Vector3(pointLightOuterRadius, pointLightOuterRadius, pointLightOuterRadius);
matrix = Matrix4x4.TRS(transform.position, transform.rotation, scale);
}
return matrix;
}

private void Awake()
{
#if UNITY_EDITOR
Expand All @@ -453,6 +478,8 @@ private void Awake()
lightMesh.SetIndices(indices, MeshTopology.Triangles, 0, false);
}
}

UpdateBatchSlotIndex();
}

void OnEnable()
Expand Down
Expand Up @@ -8,6 +8,22 @@

namespace UnityEngine.Rendering.Universal
{
// Per Light parameters to batch.
struct PerLight2D
{
internal float4x4 InvMatrix;
internal float4 Color;
internal float4 Position;
internal float FalloffIntensity;
internal float FalloffDistance;
internal float OuterAngle;
internal float InnerAngle;
internal float InnerRadiusMult;
internal float VolumeOpacity;
internal float ShadowIntensity;
internal int LightType;
};

internal static class LightUtility
{
public static bool CheckForChange(Light2D.LightType a, ref Light2D.LightType b)
Expand Down Expand Up @@ -253,7 +269,7 @@ internal static List<Vector2> GetOutlinePath(Vector3[] shapePath, float offsetDi
NativeArray<ushort>.Copy(indices, light.indices, indexCount);
}

public static Bounds GenerateShapeMesh(Light2D light, Vector3[] shapePath, float falloffDistance)
public static Bounds GenerateShapeMesh(Light2D light, Vector3[] shapePath, float falloffDistance, float batchColor)
{
var ix = 0;
var vcount = 0;
Expand All @@ -262,8 +278,8 @@ public static Bounds GenerateShapeMesh(Light2D light, Vector3[] shapePath, float
var mesh = light.lightMesh;

// todo Revisit this while we do Batching.
var meshInteriorColor = new Color(0.0f, 0, 0, 1.0f);
var meshExteriorColor = new Color(0.0f, 0, 0, 0.0f);
var meshInteriorColor = new Color(0, 0, batchColor, 1.0f);
var meshExteriorColor = new Color(0, 0, batchColor, 0.0f);
var vertices = new NativeArray<LightMeshVertex>(shapePath.Length * 256, Allocator.Temp);
var indices = new NativeArray<ushort>(shapePath.Length * 256, Allocator.Temp);

Expand Down Expand Up @@ -376,7 +392,7 @@ public static Bounds GenerateShapeMesh(Light2D light, Vector3[] shapePath, float
return mesh.GetSubMesh(0).bounds;
}

public static Bounds GenerateParametricMesh(Light2D light, float radius, float falloffDistance, float angle, int sides)
public static Bounds GenerateParametricMesh(Light2D light, float radius, float falloffDistance, float angle, int sides, float batchColor)
{
var angleOffset = Mathf.PI / 2.0f + Mathf.Deg2Rad * angle;
if (sides < 3)
Expand All @@ -398,7 +414,7 @@ public static Bounds GenerateParametricMesh(Light2D light, float radius, float f
var mesh = light.lightMesh;

// Only Alpha value in Color channel is ever used. May remove it or keep it for batching params in the future.
var color = new Color(0, 0, 0, 1);
var color = new Color(0, 0, batchColor, 1.0f);
vertices[centerIndex] = new LightMeshVertex
{
position = float3.zero,
Expand All @@ -419,7 +435,7 @@ public static Bounds GenerateParametricMesh(Light2D light, float radius, float f
vertices[vertexIndex] = new LightMeshVertex
{
position = endPoint,
color = new Color(extrudeDir.x, extrudeDir.y, 0, 0)
color = new Color(extrudeDir.x, extrudeDir.y, batchColor, 0)
};
vertices[vertexIndex + 1] = new LightMeshVertex
{
Expand Down Expand Up @@ -463,7 +479,7 @@ public static Bounds GenerateParametricMesh(Light2D light, float radius, float f
};
}

public static Bounds GenerateSpriteMesh(Light2D light, Sprite sprite)
public static Bounds GenerateSpriteMesh(Light2D light, Sprite sprite, float batchColor)
{
var mesh = light.lightMesh;

Expand All @@ -483,7 +499,7 @@ public static Bounds GenerateSpriteMesh(Light2D light, Sprite sprite)

var center = 0.5f * (sprite.bounds.min + sprite.bounds.max);
var vertices = new NativeArray<LightMeshVertex>(srcIndices.Length, Allocator.Temp);
var color = new Color(0, 0, 0, 1);
var color = new Color(0, 0, batchColor, 1);

for (var i = 0; i < srcVertices.Length; i++)
{
Expand Down
Expand Up @@ -466,6 +466,7 @@ public override void Execute(ScriptableRenderContext context, ref RenderingData

LayerUtility.InitializeBudget(m_Renderer2DData.lightRenderTextureMemoryBudget);
ShadowRendering.InitializeBudget(m_Renderer2DData.shadowRenderTextureMemoryBudget);
RendererLighting.lightBatch.Reset();

var isSceneLit = m_Renderer2DData.lightCullResult.IsSceneLit();
if (isSceneLit)
Expand Down
@@ -0,0 +1,158 @@
using System.Collections.Generic;
using UnityEngine.Experimental.Rendering;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Collections.LowLevel.Unsafe;

namespace UnityEngine.Rendering.Universal
{

// The idea is to avoid CPU cost when rendering meshes with the same shader (Consider this a light-weight SRP batcher). To identify the Mesh instance ID in the Light Buffer we utilize a Slot Index
// identified from the Blue Channel of the Vertex Colors (Solely used for this purpose). This can batch a maximum of kLightMod meshes in best-case scenario. Simple but no optizations have been added yet
internal class LightBatch
{
static readonly int kMax = 2048;
static readonly int kLightMod = 64;
static readonly int kBatchMax = 256;
static readonly ProfilingSampler profilingDrawBatched = new ProfilingSampler("Light2D Batcher");
static readonly int k_BufferOffset = Shader.PropertyToID("_BatchBufferOffset");
static int sBatchIndexCounter = 0; // For LightMesh asset conditioning to facilitate batching.

private static int batchLightMod => kLightMod;
private static float batchRunningIndex => (sBatchIndexCounter++) % kLightMod / (float)kLightMod;
// Should be in Sync with USE_STRUCTURED_BUFFER_FOR_LIGHT2D_DATA
public static bool isBatchingSupported => (SystemInfo.graphicsDeviceType != GraphicsDeviceType.OpenGLES3 && SystemInfo.graphicsDeviceType != GraphicsDeviceType.OpenGLCore && SystemInfo.graphicsDeviceType != GraphicsDeviceType.Switch);

private int[] subsets = new int[kMax];
private Mesh[] lightMeshes = new Mesh[kMax];
private Matrix4x4[] matrices = new Matrix4x4[kMax];
private NativeArray<PerLight2D> lightNativeBuffer = new NativeArray<PerLight2D>(kMax, Allocator.Persistent, NativeArrayOptions.ClearMemory);
private NativeArray<int> lightMarkers = new NativeArray<int>(kBatchMax, Allocator.Persistent, NativeArrayOptions.ClearMemory);
private GraphicsBuffer lightGraphicsBuffer;

private Light2D cachedLight;
private Material cachedMaterial;
private int hashCode = 0;
private int lightCount = 0;
private int maxIndex = 0;
private int batchCount = 0;
internal PerLight2D GetLight(int index) => lightNativeBuffer[index];
internal static int batchSlotIndex => (int)(batchRunningIndex * kLightMod);
#if UNITY_EDITOR
static bool kRegisterCallback = false;
#endif

internal void SetLight(int index, PerLight2D light)
{
lightNativeBuffer[index] = light;
}

internal static float GetBatchColor(int batchSlotIndex)
{
return (float)batchSlotIndex / (float)batchLightMod;
}

internal static int GetBatchSlotIndex(float channelColor)
{
return (int)(channelColor * kLightMod);
}

static int Hash(Light2D light, Material material)
{
unchecked
{
int _hashCode = (int)2166136261;
_hashCode = _hashCode * 16777619 ^ material.GetHashCode();
_hashCode = _hashCode * 16777619 ^ (light.lightCookieSprite == null ? 0 : light.lightCookieSprite.GetHashCode());
return _hashCode;
}
}

void Validate()
{
#if UNITY_EDITOR
if (!kRegisterCallback)
UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += OnAssemblyReload;
kRegisterCallback = true;
#endif
if (lightGraphicsBuffer == null)
lightGraphicsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, kMax, UnsafeUtility.SizeOf<PerLight2D>());
}

void OnAssemblyReload()
{
lightGraphicsBuffer.Release();
}

void SetBuffer(CommandBuffer cmd)
{
Validate();
lightGraphicsBuffer.SetData(lightNativeBuffer, lightCount, lightCount, kBatchMax);
Shader.SetGlobalBuffer("_Light2DBuffer", lightGraphicsBuffer);
}

internal int SlotIndex(int x)
{
return lightCount + x;
}

internal void Reset()
{
unsafe { UnsafeUtility.MemClear(lightNativeBuffer.GetUnsafePtr(), UnsafeUtility.SizeOf<PerLight2D>() * kMax); }
maxIndex = 0;
hashCode = 0;
batchCount = 0;
lightCount = 0;
}

internal bool CanBatch(Light2D light, Material material, int index, out int lightHash)
{
Debug.Assert(lightCount < kMax);
lightHash = Hash(light, material);
hashCode = (hashCode == 0) ? lightHash : hashCode;
if (batchCount == 0)
{
hashCode = lightHash;
}
else if (hashCode != lightHash || SlotIndex(index) >= kMax || lightMarkers[index] == 1)
{
hashCode = lightHash;
return false;
}
return true;
}

internal bool AddBatch(Light2D light, Material material, Matrix4x4 mat, Mesh mesh, int subset, int lightHash, int index)
{
Debug.Assert(lightHash == hashCode);
cachedLight = light;
cachedMaterial = material;
matrices[batchCount] = mat;
lightMeshes[batchCount] = mesh;
subsets[batchCount] = subset;
batchCount++;
maxIndex = math.max(maxIndex, index);
lightMarkers[index] = 1;
return true;
}

internal void Flush(CommandBuffer cmd)
{
if (batchCount > 0)
{
using (new ProfilingScope(cmd, profilingDrawBatched))
{
SetBuffer(cmd);
cmd.SetGlobalInt(k_BufferOffset, lightCount);
cmd.DrawMultipleMeshes(matrices, lightMeshes, subsets, batchCount, cachedMaterial, -1, null);
}
lightCount = lightCount + maxIndex + 1;
}
for (int i = 0; i < batchCount; ++i)
lightMeshes[i] = null;
unsafe { UnsafeUtility.MemClear(lightMarkers.GetUnsafePtr(), UnsafeUtility.SizeOf<int>() * kBatchMax); }
batchCount = 0;
maxIndex = 0;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1baca98

Please sign in to comment.