commit 29e55b293e4a3215dd7f4de55a8ed5e155ca3533 Author: Thomas Woischnig Date: Thu Feb 6 19:41:47 2025 +0100 first commit diff --git a/BillboardDrawCall.cs b/BillboardDrawCall.cs new file mode 100644 index 0000000..53ea2cb --- /dev/null +++ b/BillboardDrawCall.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class BillboardDrawCall : DrawCall + { + private static Mesh _billboardMesh; + + private static Mesh GetBillboardMesh() + { + if (_billboardMesh == null) + { + _billboardMesh = new Mesh(); + _billboardMesh.name = "Billboard Mesh"; + _billboardMesh.vertices = new Vector3[6]; + _billboardMesh.triangles = new int[6] { + 0, + 1, + 2, + 3, + 4, + 5 + }; + + _billboardMesh.SetUVs(0,new Vector2[6] + { + new Vector2(0.0f, 0.0f), + new Vector2(0.0f, 1f), + new Vector2(1f, 1f), + new Vector2(0.0f, 0.0f), + new Vector2(1f, 1f), + new Vector2(1f, 0.0f) + }); + + _billboardMesh.SetUVs(1, new Vector4[6] + { + new Vector4(1f, 1f, 0.0f, 0.0f), + new Vector4(1f, 1f, 0.0f, 0.0f), + new Vector4(1f, 1f, 0.0f, 0.0f), + new Vector4(1f, 1f, 0.0f, 0.0f), + new Vector4(1f, 1f, 0.0f, 0.0f), + new Vector4(1f, 1f, 0.0f, 0.0f) + }); + + _billboardMesh.UploadMeshData(true); + } + + return _billboardMesh; + } + + public BillboardDrawCall( + int lodNr, + BillboardAsset billboardAsset, + Material material, + Matrix4x4 localToWorldMatrix, + uint baseIndirectArgsIndex, + List indirectDrawIndexedArgs, + in RenderParams renderParams) + : base(lodNr, + GetBillboardMesh(), + new Material[1]{ material }, + localToWorldMatrix, + baseIndirectArgsIndex, + indirectDrawIndexedArgs, + in renderParams) + { + if (billboardAsset == null) + throw new ArgumentNullException(nameof(billboardAsset)); + + SetBillboardPerBatch(billboardAsset); + } + + internal override void Dispose() { + UnityEngine.Object.Destroy(_billboardMesh); + base.Dispose(); + } + + public override void Draw(Camera camera, ObjectData obj, GraphicsBuffer indirectDrawIndexedArgs) + { + SetBillboardPerCamera(camera); + base.Draw(camera, obj, indirectDrawIndexedArgs); + } + + private void SetBillboardPerCamera(Camera camera) + { + Vector3 position = camera.transform.position; + Vector3 vector3_1 = camera.transform.forward * -1f; + vector3_1.y = 0.0f; + vector3_1.Normalize(); + Vector3 vector3_2 = camera.transform.right * -1f; + vector3_2.y = 0.0f; + vector3_2.Normalize(); + float num1 = Mathf.Atan2(vector3_1.z, vector3_1.x); + float num2 = num1 + (num1 < 0.0f ? 6.283185f : 0.0f); + Vector4 vector4; + vector4.x = position.x; + vector4.y = position.y; + vector4.z = position.z; + vector4.w = num2; + for (int index = 0; index < RenderParams.Length; ++index) + { + RenderParams[index].matProps.SetVector("unity_BillboardNormal", vector3_1); + RenderParams[index].matProps.SetVector("unity_BillboardTangent", vector3_2); + RenderParams[index].matProps.SetVector("unity_BillboardCameraParams", vector4); + } + } + + private void SetBillboardPerBatch(BillboardAsset asset) + { + Vector4 vector4_1 = Vector4.zero; + vector4_1.x = asset.imageCount; + vector4_1.y = 1.0f / (6.28318548202515f / asset.imageCount); + Vector4 vector4_2 = Vector4.zero; + vector4_2.x = asset.width; + vector4_2.y = asset.height; + vector4_2.z = asset.bottom; + Vector4[] imageTexCoords = asset.GetImageTexCoords(); + for (int index = 0; index < RenderParams.Length; ++index) + { + RenderParams[index].matProps.SetVector("unity_BillboardInfo", vector4_1); + RenderParams[index].matProps.SetVector("unity_BillboardSize", vector4_2); + RenderParams[index].matProps.SetVectorArray("unity_BillboardImageTexCoords", imageTexCoords); + } + } + } +} diff --git a/BillboardDrawCall.cs.meta b/BillboardDrawCall.cs.meta new file mode 100644 index 0000000..88e477a --- /dev/null +++ b/BillboardDrawCall.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 462b041640bdab24f87e7de6f8a16fb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CameraRenderSettings.cs b/CameraRenderSettings.cs new file mode 100644 index 0000000..7c401a7 --- /dev/null +++ b/CameraRenderSettings.cs @@ -0,0 +1,161 @@ +using System; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + [DisallowMultipleComponent] + [ExecuteAlways] + public class ThoMagicRendererCameraSettings : MonoBehaviour, IInstanceRenderSettings + { + [SerializeField] + private bool _supported; + [SerializeField] + private bool _overrideSupported; + [SerializeField] + private bool _render; + [SerializeField] + private bool _overrideRendering; + [Min(0.0f)] + [SerializeField] + private float _renderDistance; + [SerializeField] + private bool _overrideRenderDistance; + [SerializeField] + private bool _renderShadows; + [SerializeField] + private bool _overrideRenderShadows; + [Min(0.0f)] + [SerializeField] + private float _shadowDistance; + [SerializeField] + private bool _overrideShadowDistance; + [Range(0.01f, 1f)] + [SerializeField] + private float _densityInDistance; + [SerializeField] + private bool _overrideDensityInDistance; + [SerializeField] + private Vector2 _densityInDistanceFalloff; + [SerializeField] + private bool _overrideDensityInDistanceFalloff; + private ReflectionProbe _reflectionProbe; + + public InstanceRenderSettings Settings => new InstanceRenderSettings() + { + Supported = !_overrideSupported || _supported, + Render = !_overrideRendering || _render, + RenderDistance = _overrideRenderDistance ? _renderDistance : -1f, + ShadowDistance = _overrideShadowDistance ? _shadowDistance : -1f, + Shadows = !_overrideRenderShadows || _renderShadows, + DensityInDistance = _overrideDensityInDistance ? _densityInDistance : 1f, + DensityInDistanceFalloff = _overrideDensityInDistanceFalloff ? _densityInDistanceFalloff : Vector2.zero + }; + + public bool? Supported + { + get => !_overrideSupported ? new bool?() : new bool?(_supported); + set + { + _overrideSupported = value.HasValue; + if (!value.HasValue) + return; + _supported = value.Value; + } + } + + public bool? Render + { + get => !_overrideRendering ? new bool?() : new bool?(_render); + set + { + _overrideRendering = value.HasValue; + if (!value.HasValue) + return; + _render = value.Value; + } + } + + public float? RenderDistance + { + get => !_overrideRenderDistance ? new float?() : new float?(_renderDistance); + set + { + _overrideRenderDistance = value.HasValue; + if (!value.HasValue) + return; + _renderDistance = value.Value; + } + } + + public bool? RenderShadows + { + get => !_overrideRenderShadows ? new bool?() : new bool?(_renderShadows); + set + { + _overrideRenderShadows = value.HasValue; + if (!value.HasValue) + return; + _renderShadows = value.Value; + } + } + + public float? ShadowDistance + { + get => !_overrideShadowDistance ? new float?() : new float?(_shadowDistance); + set + { + _overrideShadowDistance = value.HasValue; + if (!value.HasValue) + return; + _shadowDistance = value.Value; + } + } + + public float? DensityInDistance + { + get => !_overrideDensityInDistance ? new float?() : new float?(_densityInDistance); + set + { + _overrideDensityInDistance = value.HasValue; + if (!value.HasValue) + return; + _densityInDistance = value.Value; + } + } + + public Vector2? DensityInDistanceFalloff + { + get => !_overrideDensityInDistanceFalloff ? new Vector2?() : new Vector2?(_densityInDistanceFalloff); + set + { + _overrideDensityInDistanceFalloff = value.HasValue; + if (!value.HasValue) + return; + _densityInDistanceFalloff = value.Value; + } + } + + private void OnEnable() + { + _reflectionProbe = GetComponent(); + if (_reflectionProbe == null) + return; + CameraRenderer.ReflectionProbeSettings = this; + } + + private void OnDisable() + { + if (_reflectionProbe == null || CameraRenderer.ReflectionProbeSettings as ThoMagicRendererCameraSettings == this) + return; + + CameraRenderer.ReflectionProbeSettings = null; + } + + private void OnValidate() + { + if (_reflectionProbe == null || !Application.isEditor) + return; + _reflectionProbe.RenderProbe(); + } + } +} diff --git a/CameraRenderSettings.cs.meta b/CameraRenderSettings.cs.meta new file mode 100644 index 0000000..940980c --- /dev/null +++ b/CameraRenderSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 937c543b6caf2384898a535da09c337d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CameraRenderer.cs b/CameraRenderer.cs new file mode 100644 index 0000000..05c6bc6 --- /dev/null +++ b/CameraRenderer.cs @@ -0,0 +1,676 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.XR; + +namespace Assets.ThoMagic.Renderer +{ + public class CameraRenderer : IDisposable + { + internal static int nextCameraRendererId = 0; + int cameraRendererId; + public static IInstanceRenderSettings ReflectionProbeSettings; + public readonly Camera Camera; + public IInstanceRenderSettings cameraSettings; + + private readonly bool _singlePassInstanced = false; + + private int cachedBufferHash; + + private Vector3 _origin; + private Light _mainLight; + + List visibleStreams = new List(); + + List prefabData = new List(); + List indirectDrawIndexedArgs = new List(); + Dictionary objRenderDistanceCache = new Dictionary(); + Dictionary appliedSettings = new Dictionary(); + List cullableIndexes = new List(); + + public Camera ReferenceCamera { get; private set; } + + private CommandBuffer commandBuffer; + private ComputeShader instancingShader; + private ComputeBuffer prefabBuffer; + + private GraphicsBuffer indirectDrawIndexedArgsBuffer; + private ComputeBuffer metaBuffer; + private ComputeBuffer cullableIndexesBuffer; + private ComputeBuffer visibleIndexesBuffer; + private ComputeBuffer visibleShadowIndexesBuffer; + + private float cachedFov; + private float cachedBias; + private bool lodInvalid; + + SceneRenderSettings sceneSettings = new SceneRenderSettings(); + private Plane[] _cachedFrustumPlanes; + private Vector4[] _cachedShaderFrustumPlanes; + internal bool instanceCountChanged = true; + internal bool rebuildPrefabs = true; + + private int cullShaderId = 0, clearShaderId = 0; + + private uint maxInstancesCount = 0; + private uint indexBufferOffset = 0; + + private int lastCamSettingsHash = 0; + + public CameraRenderer(Camera camera) + { + commandBuffer = new CommandBuffer(); + cameraRendererId = Interlocked.Increment(ref nextCameraRendererId); + + instancingShader = UnityEngine.Object.Instantiate(Resources.Load("ThoMagic Renderer Instancing")); + instancingShader.hideFlags = HideFlags.HideAndDontSave; + cullShaderId = instancingShader.FindKernel("Cull_64"); + clearShaderId = instancingShader.FindKernel("Clear_64"); + + instancingShader.name = $"ThoMagic Renderer Instancing - {camera.name}"; + Camera = camera != null ? camera : throw new ArgumentNullException(nameof(camera)); + cameraSettings = camera.GetComponent(); + ReferenceCamera = camera; + if (camera.cameraType == CameraType.Game || camera.cameraType == CameraType.VR) + { + _singlePassInstanced = XRSettings.enabled && (XRSettings.stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePassInstanced || XRSettings.stereoRenderingMode == XRSettings.StereoRenderingMode.SinglePassMultiview); + if (Application.isEditor && XRSettings.enabled && !_singlePassInstanced && XRSettings.loadedDeviceName == "MockHMD Display" && this.LoadMockHmdSetting() == "SinglePassInstanced") + _singlePassInstanced = true; + } + } + + public void SetFloatingOrigin(in Vector3 origin) + { + _origin = origin; + } + + public void SetReferenceCamera(Camera camera) + { + ReferenceCamera = camera != null ? camera : Camera; + cameraSettings = ReferenceCamera.GetComponent(); + } + + public void SetMainLight(Light light) => _mainLight = light; + + private void CalculateFrustumPlanes() + { + if (_cachedFrustumPlanes == null) + _cachedFrustumPlanes = new Plane[6]; + GeometryUtility.CalculateFrustumPlanes(ReferenceCamera, _cachedFrustumPlanes); + + if (_cachedShaderFrustumPlanes == null) + _cachedShaderFrustumPlanes = new Vector4[6]; + + for (int index = 0; index < 6; ++index) + _cachedShaderFrustumPlanes[index] = new Vector4(_cachedFrustumPlanes[index].normal.x, _cachedFrustumPlanes[index].normal.y, _cachedFrustumPlanes[index].normal.z, _cachedFrustumPlanes[index].distance); + } + + public void Render() + { + //Used in editor if selected camera is not editor scene camera. Then only the culled instances of the selected camera is rendered. + if (ReferenceCamera == null) + SetReferenceCamera(Camera); + + if (_mainLight == null) + _mainLight = FindMainLight(); + + if (_mainLight != null) + { + sceneSettings.HasMainLight = true; + sceneSettings.HasMainLightShadows = _mainLight.shadows > 0; + sceneSettings.MainLightDirection = _mainLight.transform.forward; + } + else + { + sceneSettings.HasMainLight = false; + sceneSettings.HasMainLightShadows = false; + } + + var finalSettings = InstanceRenderSettings.Default(ReferenceCamera); + + if (ReferenceCamera.cameraType == CameraType.Reflection && ReflectionProbeSettings != null) + finalSettings.Merge(ReflectionProbeSettings.Settings); + + if (cameraSettings == null && Application.isEditor && !Application.isPlaying) + cameraSettings = ReferenceCamera.GetComponent(); + + if (cameraSettings != null) + finalSettings.Merge(cameraSettings.Settings); + + if(finalSettings.GetHashCode() != lastCamSettingsHash) + { + lastCamSettingsHash = finalSettings.GetHashCode(); + rebuildPrefabs = true; + } + + if (!rebuildPrefabs && Application.isEditor && !Application.isPlaying) + { + foreach (var renderObject in RendererPool.objectsList) + { + + var settingsEditor = finalSettings; + var renderObjectSettingsNew = renderObject.GameObject.GetComponent(); + + if (renderObjectSettingsNew != null) + settingsEditor.Merge(renderObjectSettingsNew.Settings); + + if(!appliedSettings.ContainsKey(renderObject.prefabId) || settingsEditor.GetHashCode() != appliedSettings[renderObject.prefabId].GetHashCode()) + { + renderObject.Settings = renderObjectSettingsNew; + rebuildPrefabs = true; + } + } + } + + CalculateFrustumPlanes(); + //Find visible streams for this frame + visibleStreams.Clear(); + + foreach (var streamer in RendererPool.streamers.Values) + if (streamer.IsInRange(ReferenceCamera, _cachedFrustumPlanes)) + { + streamer.UpdateForCamera(ReferenceCamera, _cachedFrustumPlanes); + visibleStreams.Add(streamer); + } + + bool prefabDataChanged = false; + bool noNeedToUpdateIndirectIndexOffsets = false; + //A prefab (object) has been added, removed or changed + if (rebuildPrefabs) + { + noNeedToUpdateIndirectIndexOffsets = true; + rebuildPrefabs = false; + + lodInvalid = false; + cachedFov = ReferenceCamera.fieldOfView; + cachedBias = QualitySettings.lodBias; + + appliedSettings.Clear(); + indirectDrawIndexedArgs.Clear(); + + prefabData.Clear(); + prefabData.AddRange(RendererPool.prefabData); + + maxInstancesCount = 0; + indexBufferOffset = 0; + + uint batchIndex = 0; + uint prefabCount = 0; + uint indexArgsSize = 0; + int totalIndirectArgsIndex = 0; + + foreach (var renderObject in RendererPool.objectsList) + { + var localPrefabData = prefabData[(int)renderObject.prefabId]; + + var finalObjSettings = finalSettings; + + if (renderObject.Settings != null) + { + finalObjSettings.Merge(renderObject.Settings.Settings); + ValidateSettings(ref finalObjSettings); + } + + CalculateCullingDistances(renderObject, finalObjSettings, cachedFov, cachedBias, renderObject.LodTransitions, renderObject.LodSizes, renderObject.lods); + + float distance1 = RelativeHeightToDistance(finalObjSettings.DensityInDistanceFalloff.x, renderObject.LodSizes[0], cachedFov); + float distance2 = RelativeHeightToDistance(finalObjSettings.DensityInDistanceFalloff.y, renderObject.LodSizes[0], cachedFov); + + var densityInDistance = new Vector4(finalObjSettings.DensityInDistance, + distance1, + distance2 - distance1, + finalObjSettings.ShadowDistance); + + localPrefabData.lodCount = renderObject.lodCount; + localPrefabData.fadeLods = renderObject.fadeLods; + localPrefabData.batchIndex = batchIndex; + localPrefabData.indexBufferStartOffset = indexBufferOffset; + localPrefabData.maxCount = renderObject.count; + localPrefabData.densityInDistance = densityInDistance; + + renderObject.indirectArgsPerSubmeshOffsets.Clear(); + + indirectDrawIndexedArgs.AddRange(renderObject.indirectDrawIndexedArgs); + + for (int i = 0; i < renderObject.lodCount; i++) + { + renderObject.indirectArgsPerLodOffsets[i] = indexArgsSize; + + //All indirect arguments for this lod including shadows + for (int z = 0; z < renderObject.indirectArgsPerLodWithShadowsCounts[i]; z++) + { + //Set batchoffset + var idia = indirectDrawIndexedArgs[totalIndirectArgsIndex]; + idia.startInstance = indexBufferOffset + (uint)(i * renderObject.count); + indirectDrawIndexedArgs[totalIndirectArgsIndex] = idia; + //** + + renderObject.indirectArgsPerSubmeshOffsets.Add(totalIndirectArgsIndex); + totalIndirectArgsIndex++; + indexArgsSize += 5; + } + } + + localPrefabData.SetLods(renderObject.lods, renderObject.indirectArgsPerLodOffsets, renderObject.indirectArgsPerLodCounts); + prefabData[(int)renderObject.prefabId] = localPrefabData; + appliedSettings[renderObject.prefabId] = finalObjSettings; + + batchIndex += renderObject.lodCount; + indexBufferOffset += renderObject.count * renderObject.lodCount; + maxInstancesCount += renderObject.count; + prefabCount++; + } + + //Nothing to draw yet + if (maxInstancesCount == 0) + { + rebuildPrefabs = true; + return; + } + + if (metaBuffer == null || metaBuffer.count < maxInstancesCount) + { + metaBuffer?.Dispose(); + int nextCount = RendererPool.RESIZE_COUNT * (((int)maxInstancesCount - 1) / RendererPool.RESIZE_COUNT + 1); + metaBuffer = new ComputeBuffer(nextCount, sizeof(uint) * 4, ComputeBufferType.Structured); + metaBuffer.name = $"ThoMagic metaBuffer ({Camera.name})"; + + cullableIndexesBuffer?.Dispose(); + cullableIndexesBuffer = new ComputeBuffer(nextCount, sizeof(uint), ComputeBufferType.Structured); + cullableIndexesBuffer.name = $"ThoMagic cullingIndexesBuffer ({Camera.name})"; + } + + if (indirectDrawIndexedArgsBuffer == null || indirectDrawIndexedArgsBuffer.count < indirectDrawIndexedArgs.Count) + { + indirectDrawIndexedArgsBuffer?.Dispose(); + int nextCount = RendererPool.RESIZE_COUNT * (((int)indirectDrawIndexedArgs.Count - 1) / RendererPool.RESIZE_COUNT + 1); + indirectDrawIndexedArgsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, nextCount, GraphicsBuffer.IndirectDrawIndexedArgs.size); + indirectDrawIndexedArgsBuffer.name = $"ThoMagic indirectDrawIndexedArgsBuffer ({Camera.name})"; + } + + indirectDrawIndexedArgsBuffer.SetData(indirectDrawIndexedArgs); + + if (prefabBuffer == null || prefabBuffer.count < prefabCount) + { + prefabBuffer?.Dispose(); + int nextCount = RendererPool.RESIZE_COUNT * (((int)prefabCount - 1) / RendererPool.RESIZE_COUNT + 1); + prefabBuffer = new ComputeBuffer(nextCount, PrefabData.Stride, ComputeBufferType.Structured); + prefabBuffer.name = $"ThoMagic prefabBuffer ({Camera.name})"; + } + + if (visibleIndexesBuffer == null || visibleIndexesBuffer.count < indexBufferOffset) + { + visibleIndexesBuffer?.Dispose(); + int nextCount = RendererPool.RESIZE_COUNT * (((int)indexBufferOffset - 1) / RendererPool.RESIZE_COUNT + 1); + visibleIndexesBuffer = new ComputeBuffer(nextCount, sizeof(uint), ComputeBufferType.Structured); + visibleIndexesBuffer.name = $"ThoMagic visibleIndexesBuffer ({Camera.name})"; + + visibleShadowIndexesBuffer?.Dispose(); + visibleShadowIndexesBuffer = new ComputeBuffer(nextCount, sizeof(uint), ComputeBufferType.Structured); + visibleShadowIndexesBuffer.name = $"ThoMagic visibleShadowIndexesBuffer ({Camera.name})"; + }; + + prefabDataChanged = true; + instanceCountChanged = true; + } + + //Nothing to draw yet + if (maxInstancesCount == 0) + return; + + //Either instances have been added/removed or the visible streams have changed. + //We need to rebuild the visible instances index buffer + if (instanceCountChanged || BuffersChanged(visibleStreams, ref cachedBufferHash)) + { + if (instanceCountChanged) + { + if (!noNeedToUpdateIndirectIndexOffsets)//If already rebuild by prefab update, this is not needed + { + maxInstancesCount = 0; + indexBufferOffset = 0; + uint indexArgsSize = 0; + int totalIndirectArgsIndex = 0; + + foreach (var renderObject in RendererPool.objectsList) + { + for (int i = 0; i < renderObject.lodCount; i++) + { + //All indirect arguments for this lod including shadows + for (int z = 0; z < renderObject.indirectArgsPerLodWithShadowsCounts[i]; z++) + { + //Set batchoffset + var idia = indirectDrawIndexedArgs[totalIndirectArgsIndex]; + idia.startInstance = indexBufferOffset + (uint)(i * renderObject.count); + indirectDrawIndexedArgs[totalIndirectArgsIndex] = idia; + //** + + totalIndirectArgsIndex++; + indexArgsSize += 5; + } + } + + var pd = prefabData[(int)renderObject.prefabId]; + if (pd.maxCount != renderObject.count) + prefabDataChanged = true; + pd.indexBufferStartOffset = indexBufferOffset; + pd.maxCount = renderObject.count; + prefabData[(int)renderObject.prefabId] = pd; + + indexBufferOffset += renderObject.count * renderObject.lodCount; + maxInstancesCount += renderObject.count; + } + + if (metaBuffer == null || metaBuffer.count < maxInstancesCount) + { + metaBuffer?.Dispose(); + int nextCount = RendererPool.RESIZE_COUNT * (((int)maxInstancesCount - 1) / RendererPool.RESIZE_COUNT + 1); + metaBuffer = new ComputeBuffer(nextCount, sizeof(uint) * 4, ComputeBufferType.Structured); + metaBuffer.name = $"ThoMagic metaBuffer ({Camera.name})"; + + cullableIndexesBuffer?.Dispose(); + cullableIndexesBuffer = new ComputeBuffer(nextCount, sizeof(uint), ComputeBufferType.Structured); + cullableIndexesBuffer.name = $"ThoMagic cullingIndexesBuffer ({Camera.name})"; + } + + if (visibleIndexesBuffer == null || visibleIndexesBuffer.count < indexBufferOffset) + { + visibleIndexesBuffer?.Dispose(); + int nextCount = RendererPool.RESIZE_COUNT * (((int)indexBufferOffset - 1) / RendererPool.RESIZE_COUNT + 1); + visibleIndexesBuffer = new ComputeBuffer(nextCount, sizeof(uint), ComputeBufferType.Structured); + visibleIndexesBuffer.name = $"ThoMagic visibleIndexesBuffer ({Camera.name})"; + + visibleShadowIndexesBuffer?.Dispose(); + visibleShadowIndexesBuffer = new ComputeBuffer(nextCount, sizeof(uint), ComputeBufferType.Structured); + visibleShadowIndexesBuffer.name = $"ThoMagic visibleShadowIndexesBuffer ({Camera.name})"; + }; + + indirectDrawIndexedArgsBuffer.SetData(indirectDrawIndexedArgs); + } + } + + instanceCountChanged = false; + + cullableIndexes.Clear(); + + foreach (var stream in visibleStreams) + { + foreach (var objectInstanceIds in stream.objectInstanceIds.Values) + { + cullableIndexes.AddRange(objectInstanceIds); + } + } + + cullableIndexesBuffer.SetData(cullableIndexes); + } + + //Nothing to render + if (cullableIndexes.Count == 0 || RendererPool.globalInstanceBuffer == null) + return; + + //Lod settings changed, update prefab lod data + if (cachedFov != ReferenceCamera.fieldOfView || cachedBias != QualitySettings.lodBias || lodInvalid) + { + lodInvalid = false; + cachedFov = ReferenceCamera.fieldOfView; + cachedBias = QualitySettings.lodBias; + + foreach (var renderObject in RendererPool.objectsList) + { + CalculateCullingDistances(renderObject, appliedSettings[renderObject.prefabId], cachedFov, cachedBias, renderObject.LodTransitions, renderObject.LodSizes, renderObject.lods); + + float distance1 = RelativeHeightToDistance(appliedSettings[renderObject.prefabId].DensityInDistanceFalloff.x, renderObject.LodSizes[0], cachedFov); + float distance2 = RelativeHeightToDistance(appliedSettings[renderObject.prefabId].DensityInDistanceFalloff.y, renderObject.LodSizes[0], cachedFov); + + var densityInDistance = new Vector4(appliedSettings[renderObject.prefabId].DensityInDistance, + distance1, + distance2 - distance1, + appliedSettings[renderObject.prefabId].ShadowDistance); + + var pd = prefabData[(int)renderObject.prefabId]; + pd.SetLods(renderObject.lods, renderObject.indirectArgsPerLodOffsets, renderObject.indirectArgsPerLodCounts); + pd.densityInDistance = densityInDistance; + prefabData[(int)renderObject.prefabId] = pd; + } + + prefabDataChanged = true; + } + + if(prefabDataChanged) + prefabBuffer.SetData(prefabData); + + //Compute culling + + //Reset visible count of instances to zero + /*instancingShader.SetInt("_Count", indirectDrawIndexedArgs.Count); + instancingShader.SetBuffer(clearShaderId, "perCamIndirectArgumentsBuffer", indirectDrawIndexedArgsBuffer); + instancingShader.Dispatch(clearShaderId, Mathf.CeilToInt(indirectDrawIndexedArgs.Count / 64.0f), 1, 1); + + //Cull + instancingShader.SetBuffer(cullShaderId, "globalInstances", RendererPool.globalInstanceBuffer); + instancingShader.SetBuffer(cullShaderId, "perCamPrefabs", prefabBuffer); + instancingShader.SetBuffer(cullShaderId, "perCamMeta", metaBuffer); + instancingShader.SetBuffer(cullShaderId, "perCamIndirectArgumentsBuffer", indirectDrawIndexedArgsBuffer); + instancingShader.SetBuffer(cullShaderId, "perCamCullableIndexesBuffer", cullableIndexesBuffer); + instancingShader.SetBuffer(cullShaderId, "perCamVisibleIndexesBuffer", visibleIndexesBuffer); + instancingShader.SetBuffer(cullShaderId, "perCamShadowVisibleIndexesBuffer", visibleShadowIndexesBuffer); + + instancingShader.SetVector("_CameraPosition", ReferenceCamera.transform.position); + instancingShader.SetVectorArray("_FrustumPlanes", _cachedShaderFrustumPlanes); + instancingShader.SetVector("_ShadowDirection", sceneSettings.MainLightDirection); + instancingShader.SetInt("_Count", cullableIndexes.Count); + + instancingShader.Dispatch(cullShaderId, Mathf.CeilToInt(cullableIndexes.Count / 64.0f), 1, 1);*/ + + commandBuffer.Clear(); + + commandBuffer.SetComputeIntParam(instancingShader, "_CountClear", indirectDrawIndexedArgs.Count); + commandBuffer.SetComputeBufferParam(instancingShader, clearShaderId, "perCamIndirectArgumentsBuffer", indirectDrawIndexedArgsBuffer); + commandBuffer.DispatchCompute(instancingShader, clearShaderId, Mathf.CeilToInt(indirectDrawIndexedArgs.Count / 64.0f), 1, 1); + + commandBuffer.SetComputeBufferParam(instancingShader, cullShaderId, "globalInstances", RendererPool.globalInstanceBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, cullShaderId, "perCamPrefabs", prefabBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, cullShaderId, "perCamMeta", metaBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, cullShaderId, "perCamIndirectArgumentsBuffer", indirectDrawIndexedArgsBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, cullShaderId, "perCamCullableIndexesBuffer", cullableIndexesBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, cullShaderId, "perCamVisibleIndexesBuffer", visibleIndexesBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, cullShaderId, "perCamShadowVisibleIndexesBuffer", visibleShadowIndexesBuffer); + commandBuffer.SetComputeVectorParam(instancingShader, "_CameraPosition", ReferenceCamera.transform.position); + commandBuffer.SetComputeVectorArrayParam(instancingShader, "_FrustumPlanes", _cachedShaderFrustumPlanes); + commandBuffer.SetComputeVectorParam(instancingShader, "_ShadowDirection", sceneSettings.MainLightDirection); + commandBuffer.SetComputeIntParam(instancingShader, "_CountCull", cullableIndexes.Count); + commandBuffer.DispatchCompute(instancingShader, cullShaderId, Mathf.CeilToInt(cullableIndexes.Count / 64.0f), 1, 1); + + Graphics.ExecuteCommandBuffer(commandBuffer); + + var fence = Graphics.CreateGraphicsFence(GraphicsFenceType.AsyncQueueSynchronisation, SynchronisationStageFlags.ComputeProcessing); + Graphics.WaitOnAsyncGraphicsFence(fence); + + //Render all prefabs (objects) + foreach (var renderObject in RendererPool.objectsList) + if (renderObject.count > 0)//Ignore if no instance is registered + RenderObject(renderObject, appliedSettings[renderObject.prefabId]); + } + + private void CalculateCullingDistances( + ObjectData renderObject, + InstanceRenderSettings finalSettings, + float fieldOfView, + float bias, + float[] relativeTransitionHeight, + float[] detailSize, + Vector4[] lodData) + { + for (int index = 0; index < relativeTransitionHeight.Length; ++index) + lodData[index] = new Vector4(index > 0 ? lodData[index - 1].y : 0.0f, RelativeHeightToDistance(relativeTransitionHeight[index], detailSize[index], fieldOfView) * bias, detailSize[index], 0.0f); + + var renderDistance = Mathf.Min(finalSettings.RenderDistance, RelativeHeightToDistance(renderObject.LodTransitions[renderObject.LodTransitions.Length - 1], renderObject.LodSizes[renderObject.LodTransitions.Length - 1], this.ReferenceCamera.fieldOfView, QualitySettings.lodBias)); + objRenderDistanceCache[renderObject.GetHashCode()] = renderDistance; + + for (int index = 0; index < renderObject.LodSizes.Length; ++index) + { + renderObject.lods[index].x = Mathf.Min(renderObject.lods[index].x, renderDistance); + renderObject.lods[index].y = Mathf.Min(renderObject.lods[index].y, renderDistance); + } + } + + public static float RelativeHeightToDistance( + float relativeHeight, + float size, + float fieldOfView) + { + if (relativeHeight <= 0.0f || relativeHeight == float.MaxValue) + return float.MaxValue; + + float num = Mathf.Tan((float)(Math.PI / 180.0 * (double)fieldOfView * 0.5)); + return size * 0.5f / relativeHeight / num; + } + + private void RenderObject( + ObjectData obj, + InstanceRenderSettings renderSettings) + { + if (LayerIsCulled(ReferenceCamera, obj.GameObject.layer) || (/*(!Application.isEditor || Application.isPlaying) &&*/ SceneIsCulled(ReferenceCamera, obj.GameObject))) + return; + + if (!renderSettings.Render) + return; + + foreach(var drawGroup in obj.drawGroups) + { + if (drawGroup == null) continue; + drawGroup.SetInstances(RendererPool.globalInstanceBuffer, visibleIndexesBuffer, metaBuffer); + drawGroup.Draw(Camera, obj, indirectDrawIndexedArgsBuffer); + } + + if (!renderSettings.Shadows) + return; + + foreach (var drawGroup in obj.drawShadowGroups) + { + if (drawGroup == null) continue; + drawGroup.SetInstances(RendererPool.globalInstanceBuffer, visibleShadowIndexesBuffer, metaBuffer); + drawGroup.Draw(Camera, obj, indirectDrawIndexedArgsBuffer); + } + } + + private void ValidateSettings(ref InstanceRenderSettings settings) + { + if (settings.RenderDistance <= 0.0f) + settings.RenderDistance = this.ReferenceCamera.farClipPlane; + if (settings.ShadowDistance > 0.0f) + return; + settings.ShadowDistance = settings.RenderDistance; + } + + private bool LayerIsCulled(Camera camera, int layer) + { + if (camera.cullingMask != -1) + { + int num = layer; + if ((camera.cullingMask & 1 << num) == 0) + return true; + } + return false; + } + + private bool SceneIsCulled(Camera camera, GameObject gameObject) + { + if (camera.scene != null && camera.scene.handle != 0 && camera.scene != gameObject.scene) + return true; + else + return false; + } + + private string LoadMockHmdSetting() + { + try + { + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + Type type = assembly.GetType("Unity.XR.MockHMD.MockHMDBuildSettings", false); + if (!(type == (Type)null)) + { + object obj = type.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).GetValue((object)null); + return type.GetField("renderMode").GetValue(obj).ToString(); + } + } + } + catch + { + } + return null; + } + + private Light FindMainLight() + { + Light currentLight = null; + foreach (Light otherLight in UnityEngine.Object.FindObjectsOfType()) + { + if (otherLight.type == LightType.Directional) + { + if (otherLight.shadows > 0) + { + currentLight = otherLight; + break; + } + if (currentLight == null) + currentLight = otherLight; + else if (otherLight.intensity > currentLight.intensity) + currentLight = otherLight; + } + } + return currentLight; + } + + private float RelativeHeightToDistance( + float relativeHeight, + float size, + float fieldOfView, + float bias) + { + if ((double)relativeHeight <= 0.0 || (double)relativeHeight == 3.40282346638529E+38) + return float.MaxValue; + float num = Mathf.Tan((float)(Math.PI / 180.0 * (double)fieldOfView * 0.5)); + return size * 0.5f / relativeHeight / num * bias; + } + + private bool BuffersChanged(List buffers, ref int cachedHash) + { + int hash = ComputeHash(buffers); + if (hash == cachedHash) + return false; + cachedHash = hash; + return true; + } + + private int ComputeHash(List buffers) + { + if (buffers == null || buffers.Count == 0) + return 0; + int num = buffers[0].GetHashCode(); + for (int index = 1; index < buffers.Count; ++index) + num = HashCode.Combine(num, buffers[index].GetHashCode()); + return num; + } + + public void Dispose() + { + UnityEngine.Object.DestroyImmediate(instancingShader); + prefabBuffer?.Dispose(); + indirectDrawIndexedArgsBuffer?.Dispose(); + metaBuffer?.Dispose(); + cullableIndexesBuffer?.Dispose(); + visibleIndexesBuffer?.Dispose(); + visibleShadowIndexesBuffer?.Dispose(); + commandBuffer?.Dispose(); + prefabBuffer = null; + indirectDrawIndexedArgsBuffer = null; + metaBuffer = null; + visibleIndexesBuffer = null; + visibleShadowIndexesBuffer = null; + } + } +} diff --git a/CameraRenderer.cs.meta b/CameraRenderer.cs.meta new file mode 100644 index 0000000..6e82e1a --- /dev/null +++ b/CameraRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f2218c3942cfa8745866f4503b313216 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CellLayout.cs b/CellLayout.cs new file mode 100644 index 0000000..8909a94 --- /dev/null +++ b/CellLayout.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class CellLayout + { + public Vector3 Origin; + private readonly int _cellsX; + private readonly int _cellsZ; + private readonly float _cellSizeX; + private readonly float _cellSizeZ; + private readonly Cell[] _cells; + private readonly Dictionary _cameraPlanes = new Dictionary(); + private const int _planeCount = 6; + private int _cachedFrameId; + private Camera _cachedCamera; + private Plane[] _planes = new Plane[6]; + private Double3[] _absNormals = new Double3[6]; + private Double3[] _planeNormal = new Double3[6]; + private double[] _planeDistance = new double[6]; + + public CellLayout(float cellSizeX, float cellSizeZ, Bounds worldBounds) + { + Origin = worldBounds.min; + _cellSizeX = cellSizeX; + _cellSizeZ = cellSizeZ; + _cellsX = Mathf.CeilToInt(worldBounds.size.x / _cellSizeX); + _cellsZ = Mathf.CeilToInt(worldBounds.size.z / _cellSizeZ); + _cells = new Cell[_cellsX * _cellsZ]; + float num1 = _cellSizeX * 0.5f; + float num2 = _cellSizeZ * 0.5f; + for (int index1 = 0; index1 < _cellsZ; ++index1) + { + for (int index2 = 0; index2 < _cellsX; ++index2) + { + int index3 = index1 * _cellsX + index2; + float num3 = (float)index2 * _cellSizeX + num1; + float num4 = (float)index1 * _cellSizeZ + num2; + _cells[index3].HeightMin = double.MaxValue; + _cells[index3].HeightMax = double.MinValue; + _cells[index3].Center = new Double3((double)num3, 0.0, (double)num4); + _cells[index3].Extends = new Double3((double)num1, double.NaN, (double)num2); + } + } + } + + public Cell[] GetCells() => _cells; + + public int GetCellCount() => _cells.Length; + + public void OverwriteCellHeightBounds() + { + int length = _cells.Length; + for (int index = 0; index < length; ++index) + _cells[index].HeightOverwrite = true; + } + + public void SetCellHeightBounds(int cellIndex, double min, double max) + { + ref Cell local = ref _cells[cellIndex]; + if (local.HeightOverwrite) + { + local.HeightOverwrite = false; + local.HeightMax = max; + local.HeightMin = min; + } + else + { + local.HeightMax = Math.Max(local.HeightMax, max); + local.HeightMin = Math.Min(local.HeightMin, min); + } + local.Center = new Double3(local.Center.x, (local.HeightMax + local.HeightMin) * 0.5, local.Center.z); + local.Extends = new Double3(local.Extends.x, (local.HeightMax + local.HeightMin) * 0.5, local.Extends.z); + } + + public void Update(Camera camera, int frameId, bool frustumCulling) + { + if (camera == _cachedCamera && frameId == _cachedFrameId) + return; + _cachedCamera = camera; + _cachedFrameId = frameId; + Plane[] planes; + if (!_cameraPlanes.TryGetValue(((object)camera).GetHashCode(), out planes)) + _cameraPlanes[((object)camera).GetHashCode()] = planes = new Plane[6]; + GeometryUtility.CalculateFrustumPlanes(camera, planes); + SetPlanes(planes); + double x1 = camera.transform.position.x; + double z1 = camera.transform.position.z; + Double3 absNormal1 = _absNormals[0]; + Double3 absNormal2 = _absNormals[1]; + Double3 absNormal3 = _absNormals[2]; + Double3 absNormal4 = _absNormals[3]; + Double3 absNormal5 = _absNormals[4]; + Double3 absNormal6 = _absNormals[5]; + Double3 double3_1 = _planeNormal[0]; + Double3 double3_2 = _planeNormal[1]; + Double3 double3_3 = _planeNormal[2]; + Double3 double3_4 = _planeNormal[3]; + Double3 double3_5 = _planeNormal[4]; + Double3 double3_6 = _planeNormal[5]; + double num1 = _planeDistance[0]; + double num2 = _planeDistance[1]; + double num3 = _planeDistance[2]; + double num4 = _planeDistance[3]; + double num5 = _planeDistance[4]; + double num6 = _planeDistance[5]; + double x2 = (double)Origin.x; + double y = (double)Origin.y; + double z2 = (double)Origin.z; + int length = _cells.Length; + for (int index = 0; index < length; ++index) + { + ref Cell local = ref _cells[index]; + double num7 = local.Center.x + x2; + double num8 = local.Center.y + y; + double num9 = local.Center.z + z2; + Double3 extends = local.Extends; + local.DistanceX = Math.Abs(x1 - num7); + local.DistanceZ = Math.Abs(z1 - num9); + if (frustumCulling && !double.IsNaN(extends.y)) + { + bool flag = extends.x * absNormal1.x + extends.y * absNormal1.y + extends.z * absNormal1.z + (double3_1.x * num7 + double3_1.y * num8 + double3_1.z * num9) < -num1 || extends.x * absNormal2.x + extends.y * absNormal2.y + extends.z * absNormal2.z + (double3_2.x * num7 + double3_2.y * num8 + double3_2.z * num9) < -num2 || extends.x * absNormal3.x + extends.y * absNormal3.y + extends.z * absNormal3.z + (double3_3.x * num7 + double3_3.y * num8 + double3_3.z * num9) < -num3 || extends.x * absNormal4.x + extends.y * absNormal4.y + extends.z * absNormal4.z + (double3_4.x * num7 + double3_4.y * num8 + double3_4.z * num9) < -num4 || extends.x * absNormal5.x + extends.y * absNormal5.y + extends.z * absNormal5.z + (double3_5.x * num7 + double3_5.y * num8 + double3_5.z * num9) < -num5 || extends.x * absNormal6.x + extends.y * absNormal6.y + extends.z * absNormal6.z + (double3_6.x * num7 + double3_6.y * num8 + double3_6.z * num9) < -num6; + local.InFrustum = !flag; + } + else + local.InFrustum = true; + } + } + + private void SetPlanes(Plane[] planes) + { + _planes = planes; + for (int index = 0; index < 6; ++index) + { + Plane plane = _planes[index]; + Vector3 vector = plane.normal; + Double3 double3 = new Double3(in vector); + _absNormals[index] = new Double3(Math.Abs(double3.x), Math.Abs(double3.y), Math.Abs(double3.z)); + _planeNormal[index] = double3; + _planeDistance[index] = plane.distance; + } + } + + public struct Cell + { + public double DistanceX; + public double DistanceZ; + public bool InFrustum; + public Double3 Center; + public Double3 Extends; + public double HeightMin; + public double HeightMax; + public bool HeightOverwrite; + } + + public readonly struct Double3 + { + public readonly double x; + public readonly double y; + public readonly double z; + + public Double3(in Vector3 vector) + { + x = (double)vector.x; + y = (double)vector.y; + z = (double)vector.z; + } + + public Double3(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + } + } +} diff --git a/CellLayout.cs.meta b/CellLayout.cs.meta new file mode 100644 index 0000000..4193062 --- /dev/null +++ b/CellLayout.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4cf46f5589518ec4f9ea40aaf072cfbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/CellLayoutPool.cs b/CellLayoutPool.cs new file mode 100644 index 0000000..f4a9da2 --- /dev/null +++ b/CellLayoutPool.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + internal class CellLayoutPool + { + private static readonly Dictionary _hashLookup = new Dictionary(); + private static readonly Dictionary _sharedCells = new Dictionary(); + private static readonly Dictionary> _usageTracker = new Dictionary>(); + + public static int Count => CellLayoutPool._sharedCells.Count; + + internal static bool Validate() => CellLayoutPool._hashLookup.Count == CellLayoutPool._sharedCells.Count && CellLayoutPool._hashLookup.Count == CellLayoutPool._usageTracker.Count; + + public static CellLayout Get( + object owner, + float cellSizeX, + float cellSizeZ, + int cellsX, + int cellsZ, + Bounds bounds) + { + if (owner == null) + throw new ArgumentNullException(nameof(owner)); + int hashCode = CellLayoutPool.GetHashCode(cellSizeX, cellSizeZ, cellsX, cellsZ, bounds.min); + CellLayout key; + if (!CellLayoutPool._sharedCells.TryGetValue(hashCode, out key)) + { + CellLayoutPool._sharedCells[hashCode] = key = new CellLayout(cellSizeX, cellSizeZ, bounds); + CellLayoutPool._hashLookup[key] = hashCode; + } + CellLayoutPool.IncreaseUsage(hashCode, owner); + return key; + } + + public static void Return(object owner, CellLayout layout) + { + if (owner == null) + throw new ArgumentNullException(nameof(owner)); + int hash = layout != null ? CellLayoutPool.GetHashCode(layout) : throw new ArgumentNullException(nameof(layout)); + if (CellLayoutPool.DecreaseUsage(hash, owner) != 0) + return; + CellLayoutPool.Dispose(hash, layout); + } + + private static void Dispose(int hash, CellLayout layout) + { + CellLayoutPool._usageTracker.Remove(hash); + CellLayoutPool._sharedCells.Remove(hash); + CellLayoutPool._hashLookup.Remove(layout); + } + + private static int GetHashCode(CellLayout layout) => CellLayoutPool._hashLookup[layout]; + + private static void IncreaseUsage(int hash, object owner) + { + HashSet intSet; + if (!CellLayoutPool._usageTracker.TryGetValue(hash, out intSet) || intSet == null) + CellLayoutPool._usageTracker[hash] = intSet = new HashSet(); + int hashCode = owner.GetHashCode(); + if (intSet.Contains(hashCode)) + return; + intSet.Add(hashCode); + } + + private static int DecreaseUsage(int hash, object owner) + { + HashSet intSet = CellLayoutPool._usageTracker[hash]; + intSet.Remove(owner.GetHashCode()); + return intSet.Count; + } + + private static int GetHashCode( + float cellSizeX, + float cellSizeZ, + int cellsX, + int cellsZ, + Vector3 initialOrigin) + { + return HashCode.Combine(cellSizeX, cellSizeZ, cellsX, cellsZ, initialOrigin); + } + } +} diff --git a/CellLayoutPool.cs.meta b/CellLayoutPool.cs.meta new file mode 100644 index 0000000..81db81a --- /dev/null +++ b/CellLayoutPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d431ea020bbd81c42b897bd3afc640cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/DrawCall.cs b/DrawCall.cs new file mode 100644 index 0000000..c1b7dda --- /dev/null +++ b/DrawCall.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class DrawCall + { + public readonly Mesh Mesh; + public readonly Material[] Materials; + public readonly RenderParams[] RenderParams; + private readonly Matrix4x4 _localToWorldMatrix; + private readonly uint baseIndirectArgsIndex; + private readonly int lodNr; + public DrawCall( + int lodNr, + Mesh mesh, + Material[] materials, + Matrix4x4 localToWorldMatrix, + uint baseIndirectArgsIndex, + List indirectDrawIndexedArgs, + in RenderParams renderParams) + { + if (mesh == null) + throw new ArgumentNullException(nameof(mesh)); + if (materials == null) + throw new ArgumentNullException(nameof(materials)); + if (materials.Length != mesh.subMeshCount) + throw new IndexOutOfRangeException(nameof(materials)); + + this.baseIndirectArgsIndex = baseIndirectArgsIndex; + _localToWorldMatrix = localToWorldMatrix; + Mesh = mesh; + Materials = materials; + RenderParams = new RenderParams[mesh.subMeshCount]; + + this.lodNr = lodNr; + + for (int index = 0; index < mesh.subMeshCount; ++index) + { + RenderParams[index] = renderParams; + RenderParams[index].material = materials[index]; + RenderParams[index].matProps = new MaterialPropertyBlock(); + RenderParams[index].matProps.SetMatrix("trInstanceMatrix", _localToWorldMatrix); + RenderParams[index].matProps.SetInteger("trLodNr", lodNr + 1); + + RenderParams[index].worldBounds = new Bounds(Vector3.zero, 1000f * Vector3.one); + + indirectDrawIndexedArgs.Add(new GraphicsBuffer.IndirectDrawIndexedArgs + { + indexCountPerInstance = mesh.GetIndexCount(index), + baseVertexIndex = mesh.GetBaseVertex(index), + startIndex = mesh.GetIndexStart(index), + startInstance = 0, + instanceCount = 0 + }); + } + } + + public virtual void SetInstances(ComputeBuffer instances, ComputeBuffer visibleIndexBuffer, ComputeBuffer metaBuffer) + { + for (int index = 0; index < this.RenderParams.Length; ++index) + { + RenderParams[index].matProps.SetBuffer("trInstances", instances); + RenderParams[index].matProps.SetBuffer("trPerCamVisibleIndexesBuffer", visibleIndexBuffer); + RenderParams[index].matProps.SetBuffer("trPerCamMeta", metaBuffer); + } + } + + public virtual void Draw(Camera camera, ObjectData obj, GraphicsBuffer indirectDrawIndexedArgs) + { + if (Materials == null) + throw new ObjectDisposedException("Materials"); + + if (Mesh == null) + throw new ObjectDisposedException("Mesh"); + + var bounds = new Bounds(camera.transform.position, Vector3.one * 1000f); + + for (int index = 0; index < RenderParams.Length; ++index) + { + var renderParam = RenderParams[index]; + if (renderParam.material != null) + { + renderParam.camera = camera; + renderParam.worldBounds = bounds; + Graphics.RenderMeshIndirect(in renderParam, Mesh, indirectDrawIndexedArgs, 1, obj.indirectArgsPerSubmeshOffsets[(int)baseIndirectArgsIndex + index]); + } + } + } + + internal virtual void Dispose() + { + + } + } +} diff --git a/DrawCall.cs.meta b/DrawCall.cs.meta new file mode 100644 index 0000000..f706731 --- /dev/null +++ b/DrawCall.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0345ca3ff11f4114a9cb20dfdeee77a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/DrawGroup.cs b/DrawGroup.cs new file mode 100644 index 0000000..27fb676 --- /dev/null +++ b/DrawGroup.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class DrawGroup + { + private List _drawCalls = new List(); + + public void Add(DrawCall drawCall) => _drawCalls.Add(drawCall); + + private int lodNr; + + public DrawGroup(int lodNr) + { + this.lodNr = lodNr; + } + + public uint Add( + Mesh mesh, + Material[] materials, + Matrix4x4 matrix, + uint indirectArgsCount, + List indirectDrawIndexedArgs, + in RenderParams renderParams) + { + var drawCall = new DrawCall(lodNr, mesh, materials, matrix, indirectArgsCount, indirectDrawIndexedArgs, in renderParams); + _drawCalls.Add(drawCall); + return (uint)drawCall.Mesh.subMeshCount; + } + + public uint Add( + BillboardAsset mesh, + Material material, + Matrix4x4 matrix, + uint indirectArgsCount, + List indirectDrawIndexedArgs, + in RenderParams renderParams) + { + var drawCall = new BillboardDrawCall(lodNr, mesh, material, matrix, indirectArgsCount, indirectDrawIndexedArgs, in renderParams); + _drawCalls.Add(drawCall); + return (uint)drawCall.Mesh.subMeshCount; + } + + public void Draw(Camera camera, ObjectData obj, GraphicsBuffer indirectDrawIndexedArgs) + { + foreach (var drawCall in _drawCalls) + drawCall?.Draw(camera, obj, indirectDrawIndexedArgs); + } + + public virtual void SetInstances(ComputeBuffer instances, ComputeBuffer visibleIndexBuffer, ComputeBuffer metaBuffer) + { + foreach (var drawCall in _drawCalls) + drawCall?.SetInstances(instances, visibleIndexBuffer, metaBuffer); + } + + internal void Dispose() + { + foreach (var drawCall in _drawCalls) + drawCall?.Dispose(); + + _drawCalls.Clear(); + } + } +} diff --git a/DrawGroup.cs.meta b/DrawGroup.cs.meta new file mode 100644 index 0000000..4ddfb14 --- /dev/null +++ b/DrawGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 14eaba28b1cfbac43acbf8a698ee5dbe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/FrameRenderer.cs b/FrameRenderer.cs new file mode 100644 index 0000000..0d1fd57 --- /dev/null +++ b/FrameRenderer.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +#if UNITY_EDITOR +using UnityEditor; +#endif +using UnityEngine; +using UnityEngine.Rendering; + +namespace Assets.ThoMagic.Renderer +{ + public class FrameRenderer + { + [RuntimeInitializeOnLoadMethod] + public static void Initialize() + { +#if UNITY_EDITOR + AssemblyReloadEvents.beforeAssemblyReload += AssemblyReloadEvents_beforeAssemblyReload; +#endif + RendererPool.RemoveDestroyedCameras(); + RendererPool.Initialize(); + Camera.onPreCull -= RenderCamera; + Camera.onPreCull += RenderCamera; + RenderPipelineManager.beginContextRendering -= OnBeginContextRendering; + RenderPipelineManager.beginContextRendering += OnBeginContextRendering; + } + + private static void AssemblyReloadEvents_beforeAssemblyReload() + { + RendererPool.Destroy(); + } + + private static void OnBeginContextRendering( + ScriptableRenderContext context, + List cameras) + { + RendererPool.BuildBuffers(); + RendererPool.RemoveDestroyedCameras(); + foreach(var camera in cameras) + { + if (!CameraIsSupported(camera)) + { + RendererPool.RemoveCamera(camera.GetHashCode()); + } + else + { + CameraRenderer cameraRenderer = RendererPool.GetCamera(RendererPool.RegisterCamera(camera)); + try + { + cameraRenderer.Render(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + } + } + + private static void RenderCamera(Camera camera) + { + if (camera == null) + throw new NullReferenceException(nameof(camera)); + + if (!CameraIsSupported(camera)) + { + RendererPool.RemoveCamera(camera.GetHashCode()); + } + else + { + RendererPool.BuildBuffers(); + RendererPool.RemoveDestroyedCameras(); + int cameraId = RendererPool.RegisterCamera(camera); + try + { + RendererPool.GetCamera(cameraId)?.Render(); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + } + } + + private static bool CameraIsSupported(Camera camera) + { + IInstanceRenderSettings instanceRenderSettings; + return (Application.isPlaying || camera.cameraType != CameraType.Preview) && (!camera.TryGetComponent(out instanceRenderSettings) || instanceRenderSettings.Settings.Supported); + } + } +} diff --git a/FrameRenderer.cs.meta b/FrameRenderer.cs.meta new file mode 100644 index 0000000..6f87a9b --- /dev/null +++ b/FrameRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9b14e1d6d523834db575b4973d22c0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/GrassRenderer.cs b/GrassRenderer.cs new file mode 100644 index 0000000..630d3b1 --- /dev/null +++ b/GrassRenderer.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class GrassRenderer : GrassStreamer + { + + private readonly Terrain terrain; + private readonly TerrainData terrainData; + private bool isDirty; + + public GrassRenderer(Terrain terrain) + : base(terrain) + { + this.terrain = terrain; + terrainData = terrain.terrainData; + } + + public void OnTerrainChanged(TerrainChangedFlags flags) + { + if (flags.HasFlag(TerrainChangedFlags.DelayedHeightmapUpdate)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.Heightmap)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.HeightmapResolution)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.Holes)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.DelayedHolesUpdate)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.FlushEverythingImmediately)) + isDirty = true; + } + + public void Destroy() + { + Recycle(); + } + + public void LateUpdate() + { + if (isDirty) + { + isDirty = false; + Load(); + } + + if (Application.isEditor && !Application.isPlaying) + RebuildChangedPrototypes(); + + Update(); + } + + public void Load() + { + DetailPrototype[] detailPrototypes = terrainData.detailPrototypes; + + Recycle(); + + int ownerHash = GetHashCode(); + + if (grassPrefabs == null) + { + grassPrefabs = new List(); + grassPrefabSet = new List(); + } + + float maxDetailDistance = float.MinValue; + + for (int index = 0; index < detailPrototypes.Length; ++index) + { + if (!detailPrototypes[index].usePrototypeMesh || detailPrototypes[index].prototype == null) + { + AddDummy(index); + } + else + { + GameObject prototypeToRender = RendererUtility.GetPrototypeToRender(detailPrototypes[index]); + if (!RendererUtility.SupportsProceduralInstancing(prototypeToRender)) + { + AddDummy(index); + } + else + { + var settings = GetSettingsOrDefault(prototypeToRender); + maxDetailDistance = Mathf.Max(maxDetailDistance, settings.Settings.RenderDistance); + grassPrefabs.Add(RendererPool.RegisterObject(prototypeToRender, settings, this, ownerHash)); + grassPrefabSet.Add(true); + } + } + } + + Build(maxDetailDistance, detailPrototypes.Length); + } + + private void AddDummy(int i) + { + grassPrefabs.Add(-1); + grassPrefabSet.Add(false); + } + + public override void Recycle() + { + if (grassPrefabs == null) + return; + + base.Recycle(); + + int ownerHash = GetHashCode(); + for (int index = 0; index < grassPrefabs.Count; ++index) + { + if (grassPrefabSet[index]) + { + RendererPool.RemoveObject(grassPrefabs[index], ownerHash); + } + } + + grassPrefabs.Clear(); + grassPrefabSet.Clear(); + } + + private void RebuildChangedPrototypes() + { + DetailPrototype[] detailPrototypes = terrainData.detailPrototypes; + if (detailPrototypes.Length != grassPrefabs.Count) + { + Load(); + } + else + { + int ownerHash = GetHashCode(); + + for (int index = 0; index < grassPrefabs.Count; ++index) + { + if (grassPrefabSet[index]) + { + GameObject prototypeToRender = RendererUtility.GetPrototypeToRender(detailPrototypes[index]); + GameObject gameObject = RendererPool.GetObject(grassPrefabs[index]); + if (prototypeToRender != gameObject) + { + RendererPool.RemoveObject(grassPrefabs[index], ownerHash); + + if (prototypeToRender != null) + { + grassPrefabs[index] = RendererPool.RegisterObject(prototypeToRender, GetSettingsOrDefault(prototypeToRender), this, ownerHash); + grassPrefabSet[index] = true; + } + else + { + grassPrefabs[index] = -1; + grassPrefabSet[index] = false; + } + } + else if (RendererPool.ContentHashChanged(grassPrefabs[index])) + { + RendererPool.RemoveObject(grassPrefabs[index], GetHashCode()); + if (prototypeToRender != null) + { + grassPrefabs[index] = RendererPool.RegisterObject(prototypeToRender, GetSettingsOrDefault(prototypeToRender), this, ownerHash); + grassPrefabSet[index] = true; + } + else + { + grassPrefabs[index] = -1; + grassPrefabSet[index] = false; + } + } + else if (prototypeToRender != null) + RendererPool.SetObjectSettings(prototypeToRender, GetSettingsOrDefault(prototypeToRender)); + } + } + } + } + + private IInstanceRenderSettings GetSettingsOrDefault(GameObject gameObject) + { + IInstanceRenderSettings instanceRenderSettings; + return gameObject.TryGetComponent(out instanceRenderSettings) ? instanceRenderSettings : new DefaultRenderSettings(); + } + + private class DefaultRenderSettings : IInstanceRenderSettings + { + public InstanceRenderSettings Settings => new InstanceRenderSettings() + { + Render = true, + DensityInDistance = 0.125f, + DensityInDistanceFalloff = new Vector2(0.08f, 0.0075f), + RenderDistance = 150f, + ShadowDistance = 50f, + Shadows = true, + Supported = true + }; + } + } +} diff --git a/GrassRenderer.cs.meta b/GrassRenderer.cs.meta new file mode 100644 index 0000000..8ffedca --- /dev/null +++ b/GrassRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cdb66824e49e1344faf75691d1c8788d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/GrassStreamer.cs b/GrassStreamer.cs new file mode 100644 index 0000000..006162f --- /dev/null +++ b/GrassStreamer.cs @@ -0,0 +1,301 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + /// + /// Subdivides a terrain into smaller 2d-cells in the xz-plane based on terrain settings. Only cells in range 'maxDetailDistance' of the camera + /// are alive. + /// + public class GrassStreamer : InstanceStreamer + { + /// + /// The limit of how many instances are created per frame. + /// TODO: While the setting is global, the budget is per streamer and each + /// terrain has it's own streamer. This should be a real global budget. + /// + static int globalStreamingBudget = 10000; + + protected List grassPrefabs; + protected List grassPrefabSet; + + private readonly Terrain terrain; + private readonly TerrainData terrainData; + + /// + /// Used to find cells in range of the camera. + /// + private float maxDetailDistance; + private Bounds worldBounds; + private int layerCount; + + private Cell[] Cells; + private float cellSizeX, cellSizeZ; + private int cellsX, cellsZ; + + /// + /// A queue with a list of cells to load. Cells are loaded per frame + /// til the streaming budget is spent. + /// + private Queue cellsToLoad = new Queue(); + /// + /// List of cells unloaded in a frame, only temporary list used to remove from loadedCells. + /// + private List cellsToUnloaded = new List(); + //A map of loaded cells. The cell index is z * cellCountX + x. + private Dictionary loadedCells = new Dictionary(); + + public static void SetStreamingBudget(int budget) => globalStreamingBudget = budget; + + private static float CalculatePatchSizeX(TerrainData terrainData) => (float)((double)terrainData.detailResolutionPerPatch / (double)terrainData.detailResolution * terrainData.size.x); + + private static float CalculatePatchSizeZ(TerrainData terrainData) => (float)((double)terrainData.detailResolutionPerPatch / (double)terrainData.detailResolution * terrainData.size.z); + + public GrassStreamer( + Terrain terrain) + : base() + { + this.terrain = terrain; + terrainData = terrain.terrainData; + } + + /// + /// Update loaded cells. Called by renderer. The renderer calls this once each frame. + /// + public void Update() + { + //Load cells until either all cells are loaded or budget is spent. + //Returns the budget left. + int streamingBudget = UpdateLoadCellsAsync(); + + //Go through all loaded cells and reduce 'inRangeOfAnyCamera'. + //If 'inRangeOfAnyCamera' is equal or below zero unload the cell. + //'inRangeOfAnyCamera' is reset to x if a camera renders and this + //cell is in range. + foreach (var cellIndex in loadedCells.Keys) + { + var cell = loadedCells[cellIndex]; + + //If cell is out of range and budget exist, unload. + if (cell.inRangeOfAnyCamera <= 0 && streamingBudget > 0) + { + streamingBudget -= UnloadCell(cellIndex); + cellsToUnloaded.Add(cellIndex); + } + + --cell.inRangeOfAnyCamera; + } + + //Remove all cells unloaded from 'loadedCells'. + if (cellsToUnloaded.Count > 0) + { + foreach (var id in cellsToUnloaded) + loadedCells.Remove(id); + cellsToUnloaded.Clear(); + } + } + + /// + /// Rebuild the cell layout. Called on initialize and on certain terrain changes. + /// + /// Max distance where a cell is in range + /// Count of detail layers + public void Build(float maxDistance, int layerCount) + { + maxDetailDistance = maxDistance; + this.layerCount = layerCount; + + cellSizeX = CalculatePatchSizeX(terrainData); + cellSizeZ = CalculatePatchSizeZ(terrainData); + + Vector3 position = terrain.GetPosition(); + worldBounds = new Bounds(terrainData.bounds.center + position, terrainData.bounds.size); + + cellsX = Mathf.CeilToInt(worldBounds.size.x / cellSizeX); + cellsZ = Mathf.CeilToInt(worldBounds.size.z / cellSizeZ); + Cells = new Cell[cellsX * cellsZ]; + + float cellSizeX_2 = cellSizeX * 0.5f; + float cellSizeZ_2 = cellSizeZ * 0.5f; + for (int indexZ = 0; indexZ < cellsZ; ++indexZ) + { + for (int indexX = 0; indexX < cellsX; ++indexX) + { + int cellIndex = indexZ * cellsX + indexX; + Cells[cellIndex] = new Cell(); + float cellCenterX = indexX * cellSizeX + cellSizeX_2; + float cellCenterZ = indexZ * cellSizeZ + cellSizeZ_2; + Cells[cellIndex].CenterX = cellCenterX; + Cells[cellIndex].CenterZ = cellCenterZ; + } + } + } + + /// + /// Is this streamer within range? + /// + /// The camera to check for + /// Planes of the camera frustum + /// + public override bool IsInRange(Camera camera, Plane[] planes) + { + if (layerCount == 0) + return false; + + if (!terrain.editorRenderFlags.HasFlag(TerrainRenderFlags.Details)) + return false; + + var eyePos = camera.transform.position; + + if ((eyePos - worldBounds.ClosestPoint(eyePos)).magnitude <= Mathf.Min(camera.farClipPlane, maxDetailDistance)) + return true; + + return false; + } + + /// + /// Load cells until either all cells are loaded from the cellsToLoad list + /// or the budget is spent. + /// + /// Unspent budget + private int UpdateLoadCellsAsync() + { + int streamingBudget = globalStreamingBudget; + + if (globalStreamingBudget <= 0 || Cells == null || Cells.Length == 0) + return streamingBudget; + + while(streamingBudget > 0 && cellsToLoad.TryDequeue(out var cellIndex)) + { + streamingBudget -= LoadCell(cellIndex); + } + + return streamingBudget; + } + + public virtual void Recycle() + { + UnloadAll(); + } + + private void UnloadAll() + { + if (Cells == null || Cells.Length == 0) + return; + + for (int cellIndex = 0; cellIndex < Cells.Length; ++cellIndex) + { + UnloadCell(cellIndex); + } + + loadedCells.Clear(); + } + + /// + /// Remove all instances of a cell. + /// + /// + /// The amount of deleted instances + private int UnloadCell(int cellIndex) + { + if (cellIndex < 0 || cellIndex >= Cells.Length || !Cells[cellIndex].IsLoaded) + return 0; + + var cell = Cells[cellIndex]; + cell.IsLoaded = false; + cell.IsLoading = false; + + int deletedInstances = cell.loadedInstances.Count; + + foreach (var instanceInfo in cell.loadedInstances) + RemoveInstance(instanceInfo.objectId, instanceInfo.instanceId); + + cell.loadedInstances.Clear(); + + return deletedInstances; + } + + /// + /// Load all instances for a cell. + /// + /// + /// The amount of loaded instances + private int LoadCell(int cellIndex) + { + if (cellIndex < 0 || cellIndex >= Cells.Length || Cells[cellIndex].IsLoaded) + return 0; + + var cell = Cells[cellIndex]; + cell.IsLoaded = true; + cell.IsLoading = false; + + loadedCells.Add(cellIndex, cell); + int x = Mathf.FloorToInt(cell.CenterX / cellSizeX); + int z = Mathf.FloorToInt(cell.CenterZ / cellSizeZ); + Vector3 size = terrainData.size; + Vector3 terrainOffset = terrain.transform.position; + Quaternion terrainRotation = terrain.transform.rotation; + float detailObjectDensity = terrain.detailObjectDensity; + + for (int layer = 0; layer < layerCount; layer++) + { + var instanceTransforms = terrainData.ComputeDetailInstanceTransforms(x, z, layer, detailObjectDensity, out var _); + + for (int index = 0; index < instanceTransforms.Length; ++index) + { + var local = instanceTransforms[index]; + + Vector3 interpolatedNormal = terrainData.GetInterpolatedNormal((local.posX / size.x), (local.posZ / size.z)); + Quaternion rotation = terrainRotation * Quaternion.FromToRotation(Vector3.up, interpolatedNormal) * Quaternion.AngleAxis((local.rotationY * 57.2957801818848f), Vector3.up); + + var instanceId = AddInstance(grassPrefabs[layer], new Vector3(local.posX, local.posY, local.posZ) + terrainOffset, rotation, local.scaleXZ, local.scaleY); + cell.loadedInstances.Add(new InstanceInfo { instanceId = instanceId, objectId = grassPrefabs[layer] }); + } + } + + return cell.loadedInstances.Count; + } + + /// + /// Called by each camera at the start of a frame. + /// Finds all cells visible in range of a camera. Either + /// resets the 'inRangeOfAnyCamera' counter or adds the cell + /// to the load list. + /// + /// + /// + public override void UpdateForCamera(Camera camera, Plane[] planes) + { + if (Cells == null || Cells.Length == 0) + return; + + var range = Mathf.Min(camera.farClipPlane, maxDetailDistance); + var rangeX = Mathf.CeilToInt((range + cellSizeX * 0.5f) / cellSizeX); + var rangeZ = Mathf.CeilToInt((range + cellSizeZ * 0.5f) / cellSizeZ); + + var eyePos = camera.transform.position - worldBounds.min; + var eyeCellX = Mathf.FloorToInt(eyePos.x / cellSizeX); + var eyeCellZ = Mathf.FloorToInt(eyePos.z / cellSizeZ); + + for(int indexZ = eyeCellZ - rangeZ; indexZ < eyeCellZ + rangeZ; indexZ++) + for (int indexX = eyeCellX - rangeX; indexX < eyeCellX + rangeX; indexX++) + { + if(indexZ >= 0 && indexZ < cellsZ && + indexX >= 0 && indexX < cellsX) + { + int cellIndex = indexZ * cellsX + indexX; + if (Cells[cellIndex].IsLoaded || Cells[cellIndex].IsLoading) + { + Cells[cellIndex].inRangeOfAnyCamera = 2; + } + else + { + Cells[cellIndex].IsLoading = true; + Cells[cellIndex].inRangeOfAnyCamera = 2; + cellsToLoad.Enqueue(cellIndex); + } + } + } + } + } +} diff --git a/GrassStreamer.cs.meta b/GrassStreamer.cs.meta new file mode 100644 index 0000000..ab6d341 --- /dev/null +++ b/GrassStreamer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9909a9557ba0b94e9de512444a0629e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/InstanceBuffer.cs b/InstanceBuffer.cs new file mode 100644 index 0000000..090659f --- /dev/null +++ b/InstanceBuffer.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading; + +namespace Assets.ThoMagic.Renderer +{ + public class InstanceBuffer + { + private static int nextInstanceBufferId = 1; + private readonly int instanceBufferId; + + public readonly ObjectData objectData; + public readonly Dictionary instanceData; + + public InstanceBuffer(ObjectData objectData) + { + this.objectData = objectData; + instanceData = new Dictionary(); + instanceBufferId = Interlocked.Increment(ref InstanceBuffer.nextInstanceBufferId); + } + + public override int GetHashCode() + { + return instanceBufferId; + } + } +} \ No newline at end of file diff --git a/InstanceBuffer.cs.meta b/InstanceBuffer.cs.meta new file mode 100644 index 0000000..20b1044 --- /dev/null +++ b/InstanceBuffer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d85f6533dc1bb84bb236fd27ab68b38 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/InstanceMatrix.cs b/InstanceMatrix.cs new file mode 100644 index 0000000..0d9e6da --- /dev/null +++ b/InstanceMatrix.cs @@ -0,0 +1,174 @@ +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public struct InstanceData + { + public const int Add = 1; + public const int Delete = 2; + public static readonly int Stride = 48; + public readonly Vector4 Packed1; + public readonly Vector4 Packed2; + public readonly uint prefabId; + //Used to stream out new instances + public readonly int uploadAction; + public readonly int uploadId; + public readonly int padding; + + public InstanceData( + int uploadAction, + int uploadId, + uint prefabId, + float x, + float y, + float z, + in Quaternion rotation, + float scaleXZ, + float scaleY) + { + float num = rotation.w > 0.0 ? 1f : -1f; + Packed1.x = x; + Packed1.y = y; + Packed1.z = z; + Packed1.w = (rotation.x * num); + Packed2.x = scaleXZ; + Packed2.y = scaleY; + Packed2.z = (rotation.y * num); + Packed2.w = (rotation.z * num); + this.prefabId = prefabId; + this.uploadAction = uploadAction; + this.uploadId = uploadId; + padding = 0; + } + + public InstanceData( + int uploadAction, + int uploadId) + { + Packed1.x = 0; + Packed1.y = 0; + Packed1.z = 0; + Packed1.w = 0; + Packed2.x = 0; + Packed2.y = 0; + Packed2.z = 0; + Packed2.w = 0; + this.prefabId = 0; + this.uploadAction = uploadAction; + this.uploadId = uploadId; + padding = 0; + } + } + + public struct PrefabData + { + public static readonly int Stride = 228; + public uint batchIndex; + public uint indexBufferStartOffset; + public uint maxCount; + public uint lodCount; + public uint fadeLods; + public Vector4 densityInDistance; + public Vector4 lodData0; + public Vector4 lodData1; + public Vector4 lodData2; + public Vector4 lodData3; + public Vector4 lodData4; + public Vector4 lodData5; + public Vector4 lodData6; + public Vector4 lodData7; + public uint indirectArgsOffset0; + public uint indirectArgsCount0; + public uint indirectArgsOffset1; + public uint indirectArgsCount1; + public uint indirectArgsOffset2; + public uint indirectArgsCount2; + public uint indirectArgsOffset3; + public uint indirectArgsCount3; + public uint indirectArgsOffset4; + public uint indirectArgsCount4; + public uint indirectArgsOffset5; + public uint indirectArgsCount5; + public uint indirectArgsOffset6; + public uint indirectArgsCount6; + public uint indirectArgsOffset7; + public uint indirectArgsCount7; + + public void SetLods(Vector4[] lodData, uint[] indirectArgsPerLodOffset, uint[] indirectArgsPerLodCount) + { + this.lodData0 = lodCount > 0 ? lodData[0] : Vector4.zero; + this.lodData1 = lodCount > 1 ? lodData[1] : Vector4.zero; + this.lodData2 = lodCount > 2 ? lodData[2] : Vector4.zero; + this.lodData3 = lodCount > 3 ? lodData[3] : Vector4.zero; + this.lodData4 = lodCount > 4 ? lodData[4] : Vector4.zero; + this.lodData5 = lodCount > 5 ? lodData[5] : Vector4.zero; + this.lodData6 = lodCount > 6 ? lodData[6] : Vector4.zero; + this.lodData7 = lodCount > 7 ? lodData[7] : Vector4.zero; + + this.indirectArgsOffset0 = lodCount > 0 ? indirectArgsPerLodOffset[0] : 0; + this.indirectArgsOffset1 = lodCount > 1 ? indirectArgsPerLodOffset[1] : 0; + this.indirectArgsOffset2 = lodCount > 2 ? indirectArgsPerLodOffset[2] : 0; + this.indirectArgsOffset3 = lodCount > 3 ? indirectArgsPerLodOffset[3] : 0; + this.indirectArgsOffset4 = lodCount > 4 ? indirectArgsPerLodOffset[4] : 0; + this.indirectArgsOffset5 = lodCount > 5 ? indirectArgsPerLodOffset[5] : 0; + this.indirectArgsOffset6 = lodCount > 6 ? indirectArgsPerLodOffset[6] : 0; + this.indirectArgsOffset7 = lodCount > 7 ? indirectArgsPerLodOffset[7] : 0; + + this.indirectArgsCount0 = lodCount > 0 ? indirectArgsPerLodCount[0] : 0; + this.indirectArgsCount1 = lodCount > 1 ? indirectArgsPerLodCount[1] : 0; + this.indirectArgsCount2 = lodCount > 2 ? indirectArgsPerLodCount[2] : 0; + this.indirectArgsCount3 = lodCount > 3 ? indirectArgsPerLodCount[3] : 0; + this.indirectArgsCount4 = lodCount > 4 ? indirectArgsPerLodCount[4] : 0; + this.indirectArgsCount5 = lodCount > 5 ? indirectArgsPerLodCount[5] : 0; + this.indirectArgsCount6 = lodCount > 6 ? indirectArgsPerLodCount[6] : 0; + this.indirectArgsCount7 = lodCount > 7 ? indirectArgsPerLodCount[7] : 0; + } + + public PrefabData( + uint batchIndex, + uint indexBufferStartOffset, + uint maxCount, + uint lodCount, + uint fadeLods, + Vector4 densityInDistance, + Vector4[] lodData, + uint[] indirectArgsPerLodOffset, + uint[] indirectArgsPerLodCount + ) + { + this.batchIndex = batchIndex; + this.indexBufferStartOffset = indexBufferStartOffset; + this.maxCount = maxCount; + this.lodCount = lodCount; + this.fadeLods = fadeLods; + this.densityInDistance = densityInDistance; + + this.lodData0 = lodCount > 0 ? lodData[0] : Vector4.zero; + this.lodData1 = lodCount > 1 ? lodData[1] : Vector4.zero; + this.lodData2 = lodCount > 2 ? lodData[2] : Vector4.zero; + this.lodData3 = lodCount > 3 ? lodData[3] : Vector4.zero; + this.lodData4 = lodCount > 4 ? lodData[4] : Vector4.zero; + this.lodData5 = lodCount > 5 ? lodData[5] : Vector4.zero; + this.lodData6 = lodCount > 6 ? lodData[6] : Vector4.zero; + this.lodData7 = lodCount > 7 ? lodData[7] : Vector4.zero; + + this.indirectArgsOffset0 = lodCount > 0 ? indirectArgsPerLodOffset[0] : 0; + this.indirectArgsOffset1 = lodCount > 1 ? indirectArgsPerLodOffset[1] : 0; + this.indirectArgsOffset2 = lodCount > 2 ? indirectArgsPerLodOffset[2] : 0; + this.indirectArgsOffset3 = lodCount > 3 ? indirectArgsPerLodOffset[3] : 0; + this.indirectArgsOffset4 = lodCount > 4 ? indirectArgsPerLodOffset[4] : 0; + this.indirectArgsOffset5 = lodCount > 5 ? indirectArgsPerLodOffset[5] : 0; + this.indirectArgsOffset6 = lodCount > 6 ? indirectArgsPerLodOffset[6] : 0; + this.indirectArgsOffset7 = lodCount > 7 ? indirectArgsPerLodOffset[7] : 0; + + this.indirectArgsCount0 = lodCount > 0 ? indirectArgsPerLodCount[0] : 0; + this.indirectArgsCount1 = lodCount > 1 ? indirectArgsPerLodCount[1] : 0; + this.indirectArgsCount2 = lodCount > 2 ? indirectArgsPerLodCount[2] : 0; + this.indirectArgsCount3 = lodCount > 3 ? indirectArgsPerLodCount[3] : 0; + this.indirectArgsCount4 = lodCount > 4 ? indirectArgsPerLodCount[4] : 0; + this.indirectArgsCount5 = lodCount > 5 ? indirectArgsPerLodCount[5] : 0; + this.indirectArgsCount6 = lodCount > 6 ? indirectArgsPerLodCount[6] : 0; + this.indirectArgsCount7 = lodCount > 7 ? indirectArgsPerLodCount[7] : 0; + } + } +} diff --git a/InstanceMatrix.cs.meta b/InstanceMatrix.cs.meta new file mode 100644 index 0000000..9ad60f0 --- /dev/null +++ b/InstanceMatrix.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 80369a8a9c9a0e64daddf39789fa0f66 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/InstanceRenderSettings.cs b/InstanceRenderSettings.cs new file mode 100644 index 0000000..4d45fc3 --- /dev/null +++ b/InstanceRenderSettings.cs @@ -0,0 +1,60 @@ +using System; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public interface IInstanceRenderSettings + { + InstanceRenderSettings Settings { get; } + } + + public struct InstanceRenderSettings + { + int hashCodeCache; + public bool Supported; + public bool Render; + public float RenderDistance; + public float ShadowDistance; + public bool Shadows; + public float DensityInDistance; + public Vector2 DensityInDistanceFalloff; + public static InstanceRenderSettings Default(Camera camera) => new InstanceRenderSettings() + { + Supported = true, + Render = true, + Shadows = true, + RenderDistance = camera.farClipPlane, + ShadowDistance = camera.farClipPlane, + DensityInDistance = 1f, + DensityInDistanceFalloff = Vector2.zero, + }; + + public void Merge(InstanceRenderSettings other) + { + Supported = Supported && other.Supported; + Render = Render && other.Render; + Shadows = Shadows && other.Shadows; + if (RenderDistance > 0.0f && other.RenderDistance > 0.0f) + RenderDistance = Mathf.Min(RenderDistance, other.RenderDistance); + else if (other.RenderDistance > 0.0f) + RenderDistance = other.RenderDistance; + if (ShadowDistance > 0.0f && other.ShadowDistance > 0.0f) + ShadowDistance = Mathf.Min(ShadowDistance, other.ShadowDistance); + else if (other.ShadowDistance > 0.0f) + ShadowDistance = other.ShadowDistance; + DensityInDistance = Mathf.Min(DensityInDistance, other.DensityInDistance); + DensityInDistanceFalloff.x = Mathf.Max(DensityInDistanceFalloff.x, other.DensityInDistanceFalloff.x); + DensityInDistanceFalloff.y = Mathf.Max(DensityInDistanceFalloff.y, other.DensityInDistanceFalloff.y); + + hashCodeCache = HashCode.Combine(Render, Shadows, RenderDistance, ShadowDistance, DensityInDistance, DensityInDistanceFalloff.x, DensityInDistanceFalloff.y); + } + + public override int GetHashCode() + { + if(hashCodeCache == 0 || (Application.isEditor && !Application.isPlaying)) + hashCodeCache = HashCode.Combine(Render, Shadows, RenderDistance, ShadowDistance, DensityInDistance, DensityInDistanceFalloff.x, DensityInDistanceFalloff.y); + + return hashCodeCache; + } + } +} diff --git a/InstanceRenderSettings.cs.meta b/InstanceRenderSettings.cs.meta new file mode 100644 index 0000000..1e32039 --- /dev/null +++ b/InstanceRenderSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ada01dd02c566634092d55b23558e707 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/InstanceStreamer.cs b/InstanceStreamer.cs new file mode 100644 index 0000000..2ba7f05 --- /dev/null +++ b/InstanceStreamer.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class InstanceStreamer + { + public struct InstanceInfo + { + public int instanceId; + public int objectId; + } + + public class Cell + { + public float CenterX; + public float CenterZ; + public bool IsLoaded; + public bool IsLoading; + public int inRangeOfAnyCamera; + public List loadedInstances = new List(); + } + + internal static int nextStreamerId = 1; + protected readonly int streamerInstanceId = Interlocked.Increment(ref nextStreamerId); + + public readonly HashSet owners = new HashSet(); + public Dictionary> objectInstanceIds = new Dictionary>(); + public virtual int AddInstance(int objectId, Vector3 pos, Quaternion orientation, float scaleXZ, float scaleY) + { + var instanceId = RendererPool.AddInstance(objectId, pos, orientation, scaleXZ, scaleY); + + if (!objectInstanceIds.ContainsKey(objectId)) + objectInstanceIds.Add(objectId, new HashSet()); + + objectInstanceIds[objectId].Add((uint)instanceId); + + return instanceId; + } + + public virtual void RemoveInstance(int objectId, int instanceId, bool noRemove = false) + { + if (noRemove) + { + RendererPool.RemoveInstance(objectId, instanceId); + } + else + { + if (!objectInstanceIds.ContainsKey(objectId)) + return; + + if (objectInstanceIds[objectId].Remove((uint)instanceId)) + RendererPool.RemoveInstance(objectId, instanceId); + } + } + + public void Clear() + { + foreach (var obj in objectInstanceIds) + { + if (!objectInstanceIds.ContainsKey(obj.Key)) + continue; + + foreach (var id in obj.Value) + { + RemoveInstance(obj.Key, (int)id, true); + } + } + + objectInstanceIds.Clear(); + } + + public virtual void UpdateForCamera(Camera camera, Plane[] planes) + { + + } + + public virtual bool IsInRange(Camera camera, Plane[] planes) + { + return true; + } + + public override int GetHashCode() + { + return streamerInstanceId.GetHashCode();//HashCode.Combine(streamerInstanceId, objectInstanceIds.Count); + } + } +} diff --git a/InstanceStreamer.cs.meta b/InstanceStreamer.cs.meta new file mode 100644 index 0000000..48e76d1 --- /dev/null +++ b/InstanceStreamer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e591f7bae0131d548ab936dcae307d64 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ObjectData.cs b/ObjectData.cs new file mode 100644 index 0000000..9974787 --- /dev/null +++ b/ObjectData.cs @@ -0,0 +1,266 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Assets.ThoMagic.Renderer +{ + public class ObjectData + { + public readonly GameObject GameObject; + public readonly HashSet Owners = new HashSet(); + public readonly uint prefabId; + public float[] LodSizes; + public float[] LodTransitions; + public LOD[] LODs; + public Vector4[] lods = new Vector4[8]; + public uint[] indirectArgsPerLodOffsets = new uint[8]; + public uint[] indirectArgsPerLodCounts = new uint[8]; + public uint[] indirectArgsPerLodWithShadowsCounts = new uint[8]; + public List indirectArgsPerSubmeshOffsets = new List(); + public uint lodCount = 1; + public uint fadeLods = 0; + public uint indirectArgsCount = 1; + public uint count = 0; + public IInstanceRenderSettings Settings; + + public List indirectDrawIndexedArgs = new List(); + + public DrawGroup[] drawGroups; + public DrawGroup[] drawShadowGroups; + + public int contentHashCache; + private bool _isDisposed; + + public ObjectData(GameObject gameObject, IInstanceRenderSettings settings) + { + GameObject = gameObject; + Settings = settings; + + var lodGroup = GameObject.GetComponent(); + + LOD[] lodArray; + + if (lodGroup == null) + lodArray = new LOD[1] + { + new LOD(0.0001f, GameObject.GetComponentsInChildren()) + }; + else + lodArray = lodGroup.GetLODs(); + + LODs = lodArray; + + LodSizes = new float[lodArray.Length]; + LodTransitions = new float[lodArray.Length]; + lodCount = (uint)lodArray.Length; + + if (lodGroup != null && lodGroup.fadeMode == LODFadeMode.CrossFade) + fadeLods = 1; + + for (int index = 0; index < lodArray.Length; ++index) + { + LodSizes[index] = CalculateBoundsForRenderers(lodArray[index].renderers); + LodTransitions[index] = lodArray[index].screenRelativeTransitionHeight; + } + + if (RendererPool.prefabDataFreeSlot.Count > 0) + { + prefabId = RendererPool.prefabDataFreeSlot.Dequeue(); + RendererPool.prefabData[(int)prefabId] = new PrefabData(); + } + else + { + prefabId = (uint)RendererPool.prefabData.Count; + RendererPool.prefabData.Add(new PrefabData()); + } + + RendererPool.rebuildPrefabs = true; + + indirectArgsCount = BuildRenderer(indirectDrawIndexedArgs); + } + + public void Dispose() + { + if (this._isDisposed) + return; + + foreach (var drawGroup in drawGroups) + { + drawGroup?.Dispose(); + } + + foreach (var drawGroup in drawShadowGroups) + { + drawGroup?.Dispose(); + } + + this._isDisposed = true; + } + + private uint BuildRenderer(List indirectDrawIndexedArgs) + { + uint indirectArgsCount = 0; + + drawGroups = new DrawGroup[lodCount]; + drawShadowGroups = new DrawGroup[lodCount]; + + for (int index = 0; index < lodCount; ++index) + { + uint indirectArgsPerLodCount = 0; + + foreach (var renderer in LODs[index].renderers) + { + if (renderer != null) + { + if (renderer is MeshRenderer) + { + var meshFilter = renderer.GetComponent(); + if (meshFilter != null) + { + var sharedMesh = meshFilter.sharedMesh; + var sharedMaterials = renderer.sharedMaterials; + + if (sharedMesh != null && sharedMaterials != null && sharedMaterials.Length == sharedMesh.subMeshCount) + { + if (drawGroups[index] == null) + drawGroups[index] = new DrawGroup(index); + + RenderParams renderParams1 = new RenderParams(); + renderParams1.layer = renderer.gameObject.layer; + renderParams1.receiveShadows = renderer.receiveShadows; + renderParams1.rendererPriority = renderer.rendererPriority; + renderParams1.renderingLayerMask = renderer.renderingLayerMask; + renderParams1.shadowCastingMode = ShadowCastingMode.Off; + renderParams1.reflectionProbeUsage = renderer.reflectionProbeUsage; + renderParams1.motionVectorMode = renderer.motionVectorGenerationMode; + + var count = drawGroups[index].Add(sharedMesh, sharedMaterials, Matrix4x4.identity, //renderer.transform.localToWorldMatrix, + indirectArgsCount, indirectDrawIndexedArgs, in renderParams1); + indirectArgsPerLodCount += count; + + } + } + } + else if (renderer is BillboardRenderer billboardRenderer) + { + if (billboardRenderer.billboard != null && billboardRenderer.billboard.material != null) + { + if (drawGroups[index] == null) + drawGroups[index] = new DrawGroup(index); + + RenderParams renderParams1 = new RenderParams(); + renderParams1.layer = renderer.gameObject.layer; + renderParams1.receiveShadows = renderer.receiveShadows; + renderParams1.rendererPriority = renderer.rendererPriority; + renderParams1.renderingLayerMask = renderer.renderingLayerMask; + renderParams1.shadowCastingMode = ShadowCastingMode.Off; + renderParams1.reflectionProbeUsage = renderer.reflectionProbeUsage; + renderParams1.motionVectorMode = renderer.motionVectorGenerationMode; + + var count = drawGroups[index].Add(billboardRenderer.billboard, billboardRenderer.material, renderer.transform.localToWorldMatrix, + indirectArgsCount, indirectDrawIndexedArgs, in renderParams1); + + indirectArgsPerLodCount += count; + } + } + } + } + + indirectArgsPerLodCounts[index] = indirectArgsPerLodCount; + uint indirectArgsPerLodCountShadowOffset = indirectArgsPerLodCount; + + foreach (var renderer in LODs[index].renderers) + { + if (renderer != null) + { + if (renderer is MeshRenderer) + { + var meshFilter = renderer.GetComponent(); + if (meshFilter != null) + { + var sharedMesh = meshFilter.sharedMesh; + var sharedMaterials = renderer.sharedMaterials; + + if (sharedMesh != null && sharedMaterials != null && sharedMaterials.Length == sharedMesh.subMeshCount) + { + if (renderer.shadowCastingMode > 0) + { + if (drawShadowGroups[index] == null) + drawShadowGroups[index] = new DrawGroup(index); + + var renderParams1 = new RenderParams(); + renderParams1.layer = renderer.gameObject.layer; + renderParams1.shadowCastingMode = ShadowCastingMode.ShadowsOnly; + renderParams1.renderingLayerMask = renderer.renderingLayerMask; + renderParams1.rendererPriority = renderer.rendererPriority; + + indirectArgsPerLodCount += drawShadowGroups[index].Add(sharedMesh, sharedMaterials, Matrix4x4.identity, //renderer.transform.localToWorldMatrix, + indirectArgsPerLodCountShadowOffset + indirectArgsCount, indirectDrawIndexedArgs, in renderParams1); + } + else + { + //indirectArgsPerLodCount += (uint)sharedMesh.subMeshCount; + } + } + } + } + else if (renderer is BillboardRenderer billboardRenderer) + { + if (renderer.shadowCastingMode > 0) + { + if (billboardRenderer.billboard != null && billboardRenderer.billboard.material != null) + { + if (drawShadowGroups[index] == null) + drawShadowGroups[index] = new DrawGroup(index); + + var renderParams1 = new RenderParams(); + renderParams1.layer = renderer.gameObject.layer; + renderParams1.shadowCastingMode = ShadowCastingMode.ShadowsOnly; + renderParams1.renderingLayerMask = renderer.renderingLayerMask; + renderParams1.rendererPriority = renderer.rendererPriority; + + indirectArgsPerLodCount += drawShadowGroups[index].Add(billboardRenderer.billboard, billboardRenderer.material, renderer.transform.localToWorldMatrix, + indirectArgsPerLodCountShadowOffset + indirectArgsCount, indirectDrawIndexedArgs, in renderParams1); + } + else + { + //indirectArgsPerLodCount += 1; + } + } + } + } + } + + indirectArgsPerLodWithShadowsCounts[index] = indirectArgsPerLodCount; + indirectArgsCount += indirectArgsPerLodCount; + } + + return indirectArgsCount; + } + + private static float CalculateBoundsForRenderers(UnityEngine.Renderer[] renderers) + { + Bounds bounds = new Bounds(); + bool first = true; + foreach (var renderer in renderers) + { + if (renderer != null) + { + if (first) + { + bounds = renderer.bounds; + first = false; + } + else + bounds.Encapsulate(renderer.bounds); + } + } + return Mathf.Max(Mathf.Max(bounds.size.x, bounds.size.y), bounds.size.z); + } + + public override int GetHashCode() + { + return GameObject.GetHashCode(); + } + } +} diff --git a/ObjectData.cs.meta b/ObjectData.cs.meta new file mode 100644 index 0000000..06d6036 --- /dev/null +++ b/ObjectData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 38c29b0618523cb44971f341d53349bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RendererPool.cs b/RendererPool.cs new file mode 100644 index 0000000..e2eb2fd --- /dev/null +++ b/RendererPool.cs @@ -0,0 +1,541 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Unity.Collections; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Assets.ThoMagic.Renderer +{ + public static class RendererPool + { + public const int RESIZE_COUNT = 100000; + public const int STREAMOUT_SIZE = 10000; + + public static bool isDisposed = true; + private static ComputeShader instancingShader = null; + private static CommandBuffer commandBuffer = null; + private static int uploadInstancesShaderId = 0, resizeInstanceBufferShaderId = 0; + + private static object threadSecurity = new object(); + private static int maxInstanceCount = 0; + private static int instanceUploadCount = 0; + private static readonly Dictionary cameras = new Dictionary(); + public static readonly Dictionary objects = new Dictionary(); + public static readonly Dictionary streamers = new Dictionary(); + public static readonly List objectsList = new List(); + public static readonly List prefabData = new List(); + + public static NativeArray uploadInstancesArray; + + public static readonly Queue uploadInstancesArrayQueue = new Queue(); + + public static readonly Queue prefabDataFreeSlot = new Queue(); + public static readonly Queue instanceBufferFreeSlot = new Queue(); + + public static ComputeBuffer globalUpdateInstanceBuffer = null; + public static ComputeBuffer globalInstanceBuffer = null; + public static ComputeBuffer tempGlobalInstanceBuffer = null; + + private static Vector3 _origin; + private static List _keysToRemove = new List(); + + private static bool _instanceCountChanged; + + public static bool externInstanceCountChanged + { + set + { + if (value == true) + { + foreach (var camera in cameras.Values) + { + camera.instanceCountChanged = value; + } + } + } + } + + public static bool instanceCountChanged + { + set + { + if (value == true && !_instanceCountChanged) + { + _instanceCountChanged = true; + foreach (var camera in cameras.Values) + { + camera.instanceCountChanged = value; + } + } + } + } + + public static bool rebuildPrefabs + { + set + { + if (value == true) + { + foreach (var camera in cameras.Values) + { + camera.rebuildPrefabs = value; + } + } + } + } + + public static void Destroy() + { + if (isDisposed) + return; + + isDisposed = true; + + _keysToRemove.Clear(); + + foreach (KeyValuePair camera in cameras) + { + _keysToRemove.Add(camera.Key); + } + + foreach (int id in _keysToRemove) + RemoveCamera(id); + + maxInstanceCount = 0; + instanceUploadCount = 0; + + uploadInstancesArrayQueue.Clear(); + + globalInstanceBuffer?.Dispose(); + globalInstanceBuffer = null; + globalUpdateInstanceBuffer?.Dispose(); + globalUpdateInstanceBuffer = null; + tempGlobalInstanceBuffer?.Dispose(); + tempGlobalInstanceBuffer = null; + commandBuffer?.Dispose(); + commandBuffer = null; + + prefabDataFreeSlot.Clear(); + instanceBufferFreeSlot.Clear(); + _keysToRemove.Clear(); + InstanceStreamer.nextStreamerId = 1; + CameraRenderer.nextCameraRendererId = 1; + + uploadInstancesArray.Dispose(); + } + + internal static void BuildBuffers() + { + Monitor.Enter(threadSecurity); + bool deleteOldBuffer = false; + //Load shader TODO: (Move this to init?) + if(instancingShader == null) + { + uploadInstancesArray = new NativeArray(STREAMOUT_SIZE, Allocator.Persistent); + + instancingShader = UnityEngine.Object.Instantiate(Resources.Load("ThoMagic Renderer Instancing")); + instancingShader.hideFlags = HideFlags.HideAndDontSave; + uploadInstancesShaderId = instancingShader.FindKernel("Upload_64"); + resizeInstanceBufferShaderId = instancingShader.FindKernel("Resize_64"); + + instancingShader.name = $"ThoMagic Renderer Instancing - RendererPool"; + commandBuffer = new CommandBuffer(); + } + + commandBuffer.Clear(); + + //Are there queued instances ready for upload no current upload is planned? + if (instanceUploadCount == 0 && uploadInstancesArrayQueue.Count > 0) + { + int take = Math.Min(STREAMOUT_SIZE, uploadInstancesArrayQueue.Count); + for (int i = 0; i < take; i++) + { + uploadInstancesArray[instanceUploadCount++] = uploadInstancesArrayQueue.Dequeue(); + } + + instanceCountChanged = true; + } + + //Upload instances + if (_instanceCountChanged && maxInstanceCount > 0) + { + //Create update buffer if null. TODO: Move to init? + if(globalUpdateInstanceBuffer == null) + { + globalUpdateInstanceBuffer = new ComputeBuffer(STREAMOUT_SIZE, InstanceData.Stride, ComputeBufferType.Structured);//, ComputeBufferMode.SubUpdates); + } + + //Resize and copy global instance buffer in gpu + if (globalInstanceBuffer == null || globalInstanceBuffer.count < maxInstanceCount) + { + int nextCount = RESIZE_COUNT * ((maxInstanceCount - 1) / RESIZE_COUNT + 1); + + tempGlobalInstanceBuffer = new ComputeBuffer(nextCount, InstanceData.Stride, ComputeBufferType.Structured); + + tempGlobalInstanceBuffer.name = $"ThoMagic global instance buffer"; + Debug.Log($"Upsized globalInstanceBuffer {nextCount.ToString("N")} / {(nextCount * InstanceData.Stride / 1024 / 1024)}mb"); + + if (globalInstanceBuffer != null) + { + deleteOldBuffer = true; + commandBuffer.SetComputeBufferParam(instancingShader, resizeInstanceBufferShaderId, "tempGlobalInstances", tempGlobalInstanceBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, resizeInstanceBufferShaderId, "globalInstances", globalInstanceBuffer); + commandBuffer.SetComputeIntParam(instancingShader, "_CountResize", globalInstanceBuffer.count); + commandBuffer.DispatchCompute(instancingShader, resizeInstanceBufferShaderId, Mathf.CeilToInt(globalInstanceBuffer.count / 64.0f), 1, 1); + + /*instancingShader.SetBuffer(resizeInstanceBufferShaderId, "tempGlobalInstances", tempGlobalInstanceBuffer); + instancingShader.SetBuffer(resizeInstanceBufferShaderId, "globalInstances", globalInstanceBuffer); + instancingShader.SetInt("_Count", globalInstanceBuffer.count); + instancingShader.Dispatch(resizeInstanceBufferShaderId, Mathf.CeilToInt(globalInstanceBuffer.count / 64.0f), 1, 1); + + globalInstanceBuffer.Dispose(); + globalInstanceBuffer = tempGlobalInstanceBuffer; + tempGlobalInstanceBuffer = null;*/ + } + else + { + globalInstanceBuffer = tempGlobalInstanceBuffer; + //tempGlobalInstanceBuffer = null; + } + } + else + { + tempGlobalInstanceBuffer = globalInstanceBuffer; + } + + //Upload + globalUpdateInstanceBuffer.SetData(uploadInstancesArray); + /*var updateTarget = globalUpdateInstanceBuffer.BeginWrite(0, instanceUploadCount); + NativeArray.Copy(uploadInstancesArray, updateTarget, instanceUploadCount); + globalUpdateInstanceBuffer.EndWrite(instanceUploadCount);*/ + + /*instancingShader.SetBuffer(uploadInstancesShaderId, "globalUploadInstances", globalUpdateInstanceBuffer); + instancingShader.SetBuffer(uploadInstancesShaderId, "globalInstances", globalInstanceBuffer); + instancingShader.SetInt("_Count", instanceUploadCount); + instancingShader.Dispatch(uploadInstancesShaderId, Mathf.CeilToInt(instanceUploadCount / 64.0f), 1, 1);*/ + + commandBuffer.SetComputeBufferParam(instancingShader, uploadInstancesShaderId, "globalUploadInstances", globalUpdateInstanceBuffer); + commandBuffer.SetComputeBufferParam(instancingShader, uploadInstancesShaderId, "globalInstances", tempGlobalInstanceBuffer); + commandBuffer.SetComputeIntParam(instancingShader, "_CountUpload", instanceUploadCount); + commandBuffer.DispatchCompute(instancingShader, uploadInstancesShaderId, Mathf.CeilToInt(instanceUploadCount / 64.0f), 1, 1); + + Graphics.ExecuteCommandBuffer(commandBuffer); + + var fence = Graphics.CreateGraphicsFence(GraphicsFenceType.AsyncQueueSynchronisation, SynchronisationStageFlags.ComputeProcessing); + Graphics.WaitOnAsyncGraphicsFence(fence); + + if(deleteOldBuffer) + { + globalInstanceBuffer.Dispose(); + globalInstanceBuffer = tempGlobalInstanceBuffer; + tempGlobalInstanceBuffer = null; + } + + instanceUploadCount = 0; + _instanceCountChanged = false; + } + + Monitor.Exit(threadSecurity); + } + + #region Instance handling + internal static int AddInstance(int objectId, Vector3 pos, Quaternion orientation, float scaleXZ, float scaleY) + { + if (isDisposed) + return -1; + + Monitor.Enter(threadSecurity); + + var myId = maxInstanceCount; + var objectData = GetObjectData(objectId); + objectData.count++; + + //Take free slot in instance buffer + if (instanceBufferFreeSlot.Count > 0) + { + myId = instanceBufferFreeSlot.Dequeue(); + } + else + { + maxInstanceCount++; + } + + //Directly upload if STREAMOUT_SIZE not reached, otherwise queue for upload + if (instanceUploadCount < STREAMOUT_SIZE && uploadInstancesArrayQueue.Count == 0) + uploadInstancesArray[instanceUploadCount++] = new InstanceData(InstanceData.Add, myId, objectData.prefabId, pos.x, pos.y, pos.z, orientation, scaleXZ, scaleY); + else + uploadInstancesArrayQueue.Enqueue(new InstanceData(InstanceData.Add, myId, objectData.prefabId, pos.x, pos.y, pos.z, orientation, scaleXZ, scaleY)); + + instanceCountChanged = true; + + Monitor.Exit(threadSecurity); + + return myId; + } + + internal static void RemoveInstance(int objectId, int instanceId) + { + if (isDisposed) + return; + + Monitor.Enter(threadSecurity); + + var objectData = GetObjectData(objectId); + objectData.count--; + + //No need to reset instance data because the cullable index is removed, instance data will be reset on reuse. + /*if (instanceUploadCount < STREAMOUT_SIZE && uploadInstancesArrayQueue.Count == 0) + uploadInstancesArray[instanceUploadCount++] = new InstanceData(InstanceData.Delete, instanceId); + else + uploadInstancesArrayQueue.Enqueue(new InstanceData(InstanceData.Delete, instanceId));*/ + + externInstanceCountChanged = true; + //Remember free slot in instance buffer + instanceBufferFreeSlot.Enqueue(instanceId); + + Monitor.Exit(threadSecurity); + } + #endregion + + #region Camera handling + public static IEnumerable GetCameras() + { + return cameras.Values; + } + + public static int RegisterCamera(Camera camera) + { + int num = camera != null ? camera.GetHashCode() : throw new ArgumentNullException(nameof(camera)); + if (!cameras.ContainsKey(num)) + { + CameraRenderer cameraRenderer = new CameraRenderer(camera); + cameraRenderer.SetFloatingOrigin(in _origin); + cameras.Add(num, cameraRenderer); + } + return num; + } + + public static CameraRenderer GetCamera(int cameraId) + { + CameraRenderer cameraRenderer; + return cameras.TryGetValue(cameraId, out cameraRenderer) ? cameraRenderer : null; + } + + public static bool RemoveCamera(int cameraId) + { + CameraRenderer cameraRenderer; + if (!cameras.TryGetValue(cameraId, out cameraRenderer)) + return false; + cameraRenderer?.Dispose(); + return cameras.Remove(cameraId); + } + + public static void RemoveDestroyedCameras() + { + _keysToRemove.Clear(); + + foreach (KeyValuePair camera in cameras) + { + if (camera.Value.Camera == null) + _keysToRemove.Add(camera.Key); + } + + foreach (int id in _keysToRemove) + RemoveCamera(id); + } + #endregion + + #region Object handling + public static int RegisterObject(GameObject gameObject, IInstanceRenderSettings settings, InstanceStreamer source, int ownerHash) + { + int num = gameObject != null ? /*gameObject.GetHashCode()*/ CalculateContentHash(gameObject) : throw new ArgumentNullException(nameof(gameObject)); + + if(!streamers.ContainsKey(ownerHash)) + streamers.Add(ownerHash, source); + + if (!objects.ContainsKey(num)) + { + var obj = new ObjectData(gameObject, settings); + objects.Add(num, obj); + objectsList.Add(obj); + obj.contentHashCache = CalculateContentHash(num, false); + } + else + SetObjectSettings(gameObject, settings); + + if (!objects[num].Owners.Contains(ownerHash)) + objects[num].Owners.Add(ownerHash); + + streamers[ownerHash].owners.Add(num); + + return num; + } + + public static void RemoveObject(int objectId, int ownerHash) + { + if (objects.TryGetValue(objectId, out var objectData)) + { + objectData.Owners.Remove(ownerHash); + if (objectData.Owners.Count == 0) + { + objects.Remove(objectId); + objectsList.Remove(objectData); + prefabDataFreeSlot.Enqueue(objectData.prefabId); + } + } + + if (streamers.TryGetValue(ownerHash, out var streamer)) + { + streamer.owners.Remove(objectId); + + if (streamer.owners.Count == 0) + streamers.Remove(ownerHash); + } + + rebuildPrefabs = true; + } + + public static GameObject GetObject(int objectId) + { + return objects.TryGetValue(objectId, out var objectData) ? objectData.GameObject : null; + } + + public static ObjectData GetObjectData(int objectId) + { + return objects[objectId]; + } + + public static void SetObjectSettings(GameObject prefab, IInstanceRenderSettings instanceRenderSettings) + { + int hashCode = prefab.GetHashCode(); + ObjectData objectData; + if (!objects.TryGetValue(hashCode, out objectData)) + return; + + if ((objectData.Settings == null && instanceRenderSettings != null) + || (objectData.Settings != null && instanceRenderSettings == null) + || instanceRenderSettings.Settings.GetHashCode() != objectData.Settings.Settings.GetHashCode()) + { + objectData.Settings = instanceRenderSettings; + rebuildPrefabs = true; + } + } + + private static int CalculateContentHash(GameObject gameObject) + { + int num = 13; + if (gameObject == null) + return num; + + var lodGroup = gameObject.GetComponent(); + + LOD[] lodArray; + + if (lodGroup == null) + lodArray = new LOD[1] + { + new LOD(0.0001f, gameObject.GetComponentsInChildren()) + }; + else + lodArray = lodGroup.GetLODs(); + + + foreach (LOD loD in lodArray) + num = HashCode.Combine(num, CalculateContentHash(loD)); + + return num; + } + + public static int CalculateContentHash(int objectId, bool reload = false) + { + var obj = objects[objectId]; + + int num = 13; + if (obj.GameObject == null) + return num; + + if (reload) + { + var lodGroup = obj.GameObject.GetComponent(); + + LOD[] lodArray; + + if (lodGroup == null) + lodArray = new LOD[1] + { + new LOD(0.0001f, obj.GameObject.GetComponentsInChildren()) + }; + else + lodArray = lodGroup.GetLODs(); + + obj.LODs = lodArray; + } + + foreach (LOD loD in obj.LODs) + num = HashCode.Combine(num, CalculateContentHash(loD)); + + return num; + } + + private static int CalculateContentHash(LOD lod) + { + int num = 13; + if (lod.renderers != null) + { + foreach (var renderer in lod.renderers) + { + if (renderer == null) + { + num = HashCode.Combine(num, 13); + } + else + { + MeshFilter component = renderer.GetComponent(); + + num = HashCode.Combine(HashCode.Combine( + num, + component == null || component.sharedMesh == null ? 13 : component.sharedMesh.GetHashCode(), + renderer.shadowCastingMode.GetHashCode(), + CalculateContentHash(renderer.sharedMaterials), + renderer.motionVectorGenerationMode.GetHashCode(), + renderer.receiveShadows.GetHashCode(), + renderer.rendererPriority.GetHashCode(), + renderer.renderingLayerMask.GetHashCode()), + renderer.gameObject.layer.GetHashCode(), + renderer.gameObject.transform.localPosition.GetHashCode(), + lod.screenRelativeTransitionHeight.GetHashCode()); + } + } + } + return num; + } + + private static int CalculateContentHash(Material[] materials) + { + int num = 13; + if (materials != null) + { + foreach (Material material in materials) + num = HashCode.Combine(num, + material != null ? material.GetHashCode() : 13); + } + return num; + } + + public static bool ContentHashChanged(int objectId) + { + if (!objects.ContainsKey(objectId)) + return false; + + return objects[objectId].contentHashCache != CalculateContentHash(objectId, true); + } + + internal static void Initialize() + { + isDisposed = false; + BuildBuffers(); + } + #endregion + } +} diff --git a/RendererPool.cs.meta b/RendererPool.cs.meta new file mode 100644 index 0000000..bbdf6c6 --- /dev/null +++ b/RendererPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76aaa879e050ed44ba26642123a3d6fe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RendererUtility.cs b/RendererUtility.cs new file mode 100644 index 0000000..a023180 --- /dev/null +++ b/RendererUtility.cs @@ -0,0 +1,55 @@ +using System; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class RendererUtility + { + public static GameObject GetPrototypeToRender(DetailPrototype prototype) => prototype.prototype == null ? null : + prototype.prototype.transform.root.gameObject; + + public static bool SupportsProceduralInstancing(GameObject gameObject) + { + foreach (var componentsInChild in gameObject.GetComponentsInChildren()) + { + foreach (var sharedMaterial in componentsInChild.sharedMaterials) + { + if (sharedMaterial != null && sharedMaterial.shader != null && !RendererUtility.SupportsProceduralInstancing(sharedMaterial)) + return false; + } + } + return true; + } + + public static bool SupportsProceduralInstancing(Material material) + { + if (material == null) + throw new ArgumentNullException(nameof(material)); + if (material.shader == null) + throw new ArgumentNullException("material.shader"); + return true; + } + + public static bool IsSupportedByUnity(DetailPrototype prototype) => !prototype.usePrototypeMesh || prototype.prototype == null || prototype.prototype.GetComponent() == null; + + public static GameObject GetSupportedPlaceholder(DetailPrototype prototype) => IsSupportedByUnity(prototype) ? prototype.prototype : GetSupportedPlaceholder(prototype.prototype); + + public static GameObject GetSupportedPlaceholder(GameObject prototype) + { + if (prototype == null) + return prototype; + LODGroup component = prototype.GetComponent(); + if (component == null) + return prototype; + foreach (LOD loD in component.GetLODs()) + { + foreach (UnityEngine.Renderer renderer in loD.renderers) + { + if (renderer != null) + return renderer.gameObject; + } + } + return null; + } + } +} diff --git a/RendererUtility.cs.meta b/RendererUtility.cs.meta new file mode 100644 index 0000000..b2311b0 --- /dev/null +++ b/RendererUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8948a91a0e0980f428ba7d5418d8dc6f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources.meta b/Resources.meta new file mode 100644 index 0000000..fe08a86 --- /dev/null +++ b/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 66cad6ae64c1463448771e8e1325a51c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources/ThoMagic Instanced Position.shadersubgraph b/Resources/ThoMagic Instanced Position.shadersubgraph new file mode 100644 index 0000000..e7cb95b --- /dev/null +++ b/Resources/ThoMagic Instanced Position.shadersubgraph @@ -0,0 +1,438 @@ +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.GraphData", + "m_ObjectId": "4ea35b44b3fe4b25a7cb4e39a4b9b407", + "m_Properties": [], + "m_Keywords": [ + { + "m_Id": "9ba4f485275c4fc9b2f1ce82a79369c4" + } + ], + "m_Dropdowns": [], + "m_CategoryData": [ + { + "m_Id": "480708220ad2491387a14ef84b7fe431" + } + ], + "m_Nodes": [ + { + "m_Id": "281b26d889224fc487a79fa8214bd0b1" + }, + { + "m_Id": "c5119037ba0c48fbba1bb84ead9a5adb" + }, + { + "m_Id": "4e57f58cc3d24bf1b53cde5e846cbd31" + }, + { + "m_Id": "304803996acc4ca999953697d05a2515" + } + ], + "m_GroupDatas": [], + "m_StickyNoteDatas": [], + "m_Edges": [ + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "304803996acc4ca999953697d05a2515" + }, + "m_SlotId": 1 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "281b26d889224fc487a79fa8214bd0b1" + }, + "m_SlotId": 1 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "4e57f58cc3d24bf1b53cde5e846cbd31" + }, + "m_SlotId": 1 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "304803996acc4ca999953697d05a2515" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "c5119037ba0c48fbba1bb84ead9a5adb" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "4e57f58cc3d24bf1b53cde5e846cbd31" + }, + "m_SlotId": 0 + } + } + ], + "m_VertexContext": { + "m_Position": { + "x": 0.0, + "y": 0.0 + }, + "m_Blocks": [] + }, + "m_FragmentContext": { + "m_Position": { + "x": 0.0, + "y": 0.0 + }, + "m_Blocks": [] + }, + "m_PreviewData": { + "serializedMesh": { + "m_SerializedMesh": "{\"mesh\":{\"instanceID\":0}}", + "m_Guid": "" + }, + "preventRotation": false + }, + "m_Path": "Sub Graphs", + "m_GraphPrecision": 1, + "m_PreviewMode": 2, + "m_OutputNode": { + "m_Id": "281b26d889224fc487a79fa8214bd0b1" + }, + "m_ActiveTargets": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "13e38f92c90c4e709dc150f29929c84a", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.SubGraphOutputNode", + "m_ObjectId": "281b26d889224fc487a79fa8214bd0b1", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Output", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 25.00006866455078, + "y": -310.0, + "width": 96.99996185302735, + "height": 77.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "aca287a71573483789d5dcb380daa40c" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "IsFirstSlotValid": true +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.CustomFunctionNode", + "m_ObjectId": "304803996acc4ca999953697d05a2515", + "m_Group": { + "m_Id": "" + }, + "m_Name": "IncludeThoMagicRenderer (Custom Function)", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -330.0, + "y": -310.0, + "width": 304.0, + "height": 94.0 + } + }, + "m_Slots": [ + { + "m_Id": "59dd7ed4b8b14695ab4443b7b89ab9df" + }, + { + "m_Id": "79b2dd99d8ea4726856c03ea82ede95b" + } + ], + "synonyms": [ + "code", + "HLSL" + ], + "m_Precision": 0, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SourceType": 0, + "m_FunctionName": "IncludeThoMagicRenderer", + "m_FunctionSource": "678dff042fe9c004cb7522c2233af44d", + "m_FunctionBody": "Enter function body here..." +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "45a92477b6a7449c9c4d29eac6f7f4c1", + "m_Id": 0, + "m_DisplayName": "In", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "In", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.CategoryData", + "m_ObjectId": "480708220ad2491387a14ef84b7fe431", + "m_Name": "", + "m_ChildObjectList": [ + { + "m_Id": "9ba4f485275c4fc9b2f1ce82a79369c4" + } + ] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.CustomFunctionNode", + "m_ObjectId": "4e57f58cc3d24bf1b53cde5e846cbd31", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SetupThoMagicRenderer (Custom Function)", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -694.0, + "y": -310.0000305175781, + "width": 296.9999694824219, + "height": 94.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "45a92477b6a7449c9c4d29eac6f7f4c1" + }, + { + "m_Id": "ca8752373fc04409ae302adfc1808024" + } + ], + "synonyms": [ + "code", + "HLSL" + ], + "m_Precision": 0, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SourceType": 1, + "m_FunctionName": "SetupThoMagicRenderer", + "m_FunctionSource": "06018a56d442f704f82b130cdeaf74ef", + "m_FunctionBody": "Out = In;\n#pragma instancing_options procedural:SetupThoMagicRenderer\r\r\n" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "59dd7ed4b8b14695ab4443b7b89ab9df", + "m_Id": 0, + "m_DisplayName": "In", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "In", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "79b2dd99d8ea4726856c03ea82ede95b", + "m_Id": 1, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.ShaderKeyword", + "m_ObjectId": "9ba4f485275c4fc9b2f1ce82a79369c4", + "m_Guid": { + "m_GuidSerialized": "019818f3-0f1b-49f3-8508-39d308ae3731" + }, + "m_Name": "PROCEDURAL_INSTANCING_ON", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "PROCEDURAL_INSTANCING_ON", + "m_DefaultReferenceName": "_PROCEDURAL_INSTANCING_ON", + "m_OverrideReferenceName": "", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_KeywordType": 0, + "m_KeywordDefinition": 0, + "m_KeywordScope": 1, + "m_KeywordStages": 63, + "m_Entries": [], + "m_Value": 1, + "m_IsEditable": true +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "aca287a71573483789d5dcb380daa40c", + "m_Id": 1, + "m_DisplayName": "Position", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Position", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.PositionNode", + "m_ObjectId": "c5119037ba0c48fbba1bb84ead9a5adb", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -952.0, + "y": -310.0, + "width": 206.0, + "height": 131.00003051757813 + } + }, + "m_Slots": [ + { + "m_Id": "13e38f92c90c4e709dc150f29929c84a" + } + ], + "synonyms": [ + "location" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 0, + "m_PositionSource": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "ca8752373fc04409ae302adfc1808024", + "m_Id": 1, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + diff --git a/Resources/ThoMagic Instanced Position.shadersubgraph.meta b/Resources/ThoMagic Instanced Position.shadersubgraph.meta new file mode 100644 index 0000000..bffaa82 --- /dev/null +++ b/Resources/ThoMagic Instanced Position.shadersubgraph.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 9ac8f6239a1b8d94a9aa0ba4ece1f714 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 60072b568d64c40a485e0fc55012dc9f, type: 3} diff --git a/Resources/ThoMagic Renderer Instancing.compute b/Resources/ThoMagic Renderer Instancing.compute new file mode 100644 index 0000000..6810df1 --- /dev/null +++ b/Resources/ThoMagic Renderer Instancing.compute @@ -0,0 +1,418 @@ +#pragma kernel Cull_32 GROUPS=32 +#pragma kernel Cull_64 GROUPS=64 +#pragma kernel Cull_128 GROUPS=128 +#pragma kernel Cull_256 GROUPS=256 +#pragma kernel Cull_512 GROUPS=512 +#pragma kernel Cull_1024 GROUPS=1024 + +#pragma kernel Clear_32 GROUPS=32 +#pragma kernel Clear_64 GROUPS=64 +#pragma kernel Clear_128 GROUPS=128 +#pragma kernel Clear_256 GROUPS=256 +#pragma kernel Clear_512 GROUPS=512 +#pragma kernel Clear_1024 GROUPS=1024 + +#pragma kernel Upload_32 GROUPS=32 +#pragma kernel Upload_64 GROUPS=64 +#pragma kernel Upload_128 GROUPS=128 +#pragma kernel Upload_256 GROUPS=256 +#pragma kernel Upload_512 GROUPS=512 +#pragma kernel Upload_1024 GROUPS=1024 + +#pragma kernel Resize_32 GROUPS=32 +#pragma kernel Resize_64 GROUPS=64 +#pragma kernel Resize_128 GROUPS=128 +#pragma kernel Resize_256 GROUPS=256 +#pragma kernel Resize_512 GROUPS=512 +#pragma kernel Resize_1024 GROUPS=1024 + +#include "UnityCG.cginc" + +struct InstanceData +{ + // position.xyz : position.xyz + // position.w : rotation.x + // scale.x : scale.xz + // scale.y : scale.y + // scale.zw : rotation.yz + float4 position; + float4 scale; + uint prefabId; + //Used to stream out new instances + int uploadAction; + int uploadId; + int age; +}; + +struct PrefabData +{ + uint batchIndex; + uint indexBufferStartOffset; + uint maxCount; + uint lodCount; + uint fadeLods;//Todo: Use Bitwise and save memory? + //x: density threshold, y: density range start, z: density range length, w: shadow distance + float4 densityInDistance; + float4 lodData[8]; + uint2 indirectArgsIndexAndCountPerLod[8]; +}; + +struct InstanceMeta +{ + uint visibleLod; + uint fadeOutLod; + float fadeAnim; + + // The scale of the instance can be adjusted + float Scale; +}; + +StructuredBuffer globalUploadInstances; +RWStructuredBuffer globalInstances; +RWStructuredBuffer tempGlobalInstances; +StructuredBuffer perCamPrefabs; +StructuredBuffer perCamCullableIndexesBuffer; +RWStructuredBuffer perCamMeta; +RWStructuredBuffer perCamIndirectArgumentsBuffer; +RWStructuredBuffer perCamVisibleIndexesBuffer; +RWStructuredBuffer perCamShadowVisibleIndexesBuffer; + +uint _CountClear; +uint _CountUpload; +uint _CountResize; +uint _CountCull; +float3 _CameraPosition; +float3 _ShadowDirection; +float4 _FrustumPlanes[6]; + +// Plane equation: {(a, b, c) = N, d = -dot(N, P)}. +// Returns the distance from the plane to the point 'p' along the normal. +// Positive -> in front (above), negative -> behind (below). +float DistanceFromPlane(float3 p, float4 plane) +{ + return dot(float4(p, 1.0), plane); +} + +// Returns 'true' if the object is outside of the frustum. +// 'size' is the (negative) size of the object's bounds. +bool CullFrustum(float3 center, float size, float4 frustumPlanes[6], int numPlanes) +{ + bool outside = false; + [unroll(6)] + for (int i = 0; i < numPlanes; i++) + outside = outside || DistanceFromPlane(center, frustumPlanes[i]) < size; + + return outside; +} + +// Returns 'true' if the shadow of the object is outside of the frustum. +// 'size' is the (negative) size of the object's bounds. +bool CullShadowFrustum(float3 center, float size, float3 lightDirection, float4 frustumPlanes[6], int numPlanes) +{ + bool outside = false; + [unroll(6)] + for (int i = 0; i < numPlanes; i++) + outside = outside || max(DistanceFromPlane(center, frustumPlanes[i]), DistanceFromPlane(center + lightDirection, frustumPlanes[i])) < size; + + return outside; +} + +// Calculates the adjusted scale of the instance based on the "Density in Distance" +// setting. Instances get scaled down if the density is reduced. +float ReduceDensityScale(inout InstanceData instance, float4 densityInDistance, float distance) +{ + float rangeStart = densityInDistance.y; + float rangeLength = densityInDistance.z; + + // Calculate a dither pattern with range [0..1] + float dither = frac(dot(float3((instance.position.xz) * 16.0f, 0), uint3(2, 7, 23) / 17.0f)); + + float densityThreshold = 1 - densityInDistance.x; + float distanceNormalized = (distance - rangeStart) / (rangeLength * dither + 0.001f); + + if(dither > densityInDistance.x && distanceNormalized > 0) + return 1 - distanceNormalized; + else + return 1; +} + +void ClearArgumentsBuffer(uint3 id : SV_DispatchThreadID) +{ + if (id.x >= _CountClear) + return; + + uint indirectArgsCountOffset = id.x * 5 + 1; + perCamIndirectArgumentsBuffer[indirectArgsCountOffset] = 0; +} + +void Upload (uint3 id : SV_DispatchThreadID) +{ + if (id.x >= _CountUpload) + return; + + InstanceData instance = globalUploadInstances[id.x]; + + if (instance.uploadAction == 1) + { + globalInstances[instance.uploadId] = instance; + } +} + +void Resize(uint3 id : SV_DispatchThreadID) +{ + if (id.x >= _CountResize) + return; + + tempGlobalInstances[id.x] = globalInstances[id.x]; +} + +void Cull (uint3 id : SV_DispatchThreadID) +{ + if(id.x >= _CountCull) + return; + + uint instanceIndex = perCamCullableIndexesBuffer[id.x]; + InstanceData instance = globalInstances[instanceIndex]; + InstanceMeta meta = perCamMeta[instanceIndex]; + PrefabData prefab = perCamPrefabs[instance.prefabId]; + + if (instance.age == 0) + { + meta = (InstanceMeta)0; + globalInstances[instanceIndex].age = 1; + } + + uint lodCount = prefab.lodCount; + + uint visibleLod = 0; + uint visibleShadowLod = 0; + uint batchOffset = 0; + uint indirectArgsCount = 0; + uint indirectArgsCountOffset = 0; + uint i = 0; + uint u = 0; + + // Calculate active LOD + float dist = distance(instance.position.xyz, _CameraPosition.xyz); + + [unroll(8)] + for(i=0; i < lodCount; i++) + { + float maxDist = + i < lodCount - 1 ? prefab.lodData[i].y * instance.scale.y : prefab.lodData[i].y; + + if(dist >= prefab.lodData[i].x * instance.scale.y && dist < maxDist) + { + visibleLod = i + 1; + if (dist < prefab.densityInDistance.w)//prefab.densityInDistance.w = max shadow distance + visibleShadowLod = i + 1; + } + } + + // Reduce density + if(prefab.densityInDistance.x < 1) + { + meta.Scale = ReduceDensityScale(instance, prefab.densityInDistance, dist); + if(meta.Scale < 0.3) + { + visibleLod = 0; + visibleShadowLod = 0; + } + } + else + { + meta.Scale = 1; + } + + // Frustum Culling + if(visibleLod > 0) + { + float size = -prefab.lodData[visibleLod - 1].z * length(instance.scale.xy); + const int planeCount = 5; // Do not test near/far planes + if (CullFrustum(instance.position.xyz, size, _FrustumPlanes, planeCount)) + { + // Setting active LOD to 0 culls the instance. The LODs start from 1. + visibleLod = 0; + + if (CullShadowFrustum( + instance.position.xyz, + size, + _ShadowDirection * 1000, + _FrustumPlanes, + planeCount)) + { + visibleShadowLod = 0; + } + } + } + + uint visibleCount = 0; + + if (visibleLod > 0) + { + batchOffset = prefab.indexBufferStartOffset + ((visibleLod - 1) * prefab.maxCount); + indirectArgsCount = prefab.indirectArgsIndexAndCountPerLod[visibleLod - 1].y; + indirectArgsCountOffset = prefab.indirectArgsIndexAndCountPerLod[visibleLod - 1].x + 1; + + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset], 1, visibleCount); + + for (i = 1, u = 5; i < indirectArgsCount; i++, u += 5) + { + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset + u], 1); + } + + perCamVisibleIndexesBuffer[batchOffset + visibleCount] = instanceIndex; + + if (meta.visibleLod != visibleLod && meta.visibleLod > 0 && prefab.fadeLods > 0) + { + meta.fadeOutLod = meta.visibleLod; + meta.fadeAnim = 1; + } + } + + if (visibleShadowLod > 0) + { + batchOffset = prefab.indexBufferStartOffset + ((visibleShadowLod - 1) * prefab.maxCount); + indirectArgsCount = prefab.indirectArgsIndexAndCountPerLod[visibleShadowLod - 1].y; + indirectArgsCountOffset = indirectArgsCount * 5 + prefab.indirectArgsIndexAndCountPerLod[visibleShadowLod - 1].x + 1; + + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset], 1, visibleCount); + + for (i = 1, u = 5; i < indirectArgsCount; i++, u += 5) + { + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset + u], 1); + } + + perCamShadowVisibleIndexesBuffer[batchOffset + visibleCount] = instanceIndex; + + if (meta.visibleLod != visibleShadowLod && meta.visibleLod > 0 && prefab.fadeLods > 0) + { + meta.fadeOutLod = meta.visibleLod; + meta.fadeAnim = 1; + } + } + + if (meta.fadeOutLod == 0) + meta.fadeAnim = 0; + + if (meta.fadeAnim == 0) + meta.fadeOutLod = 0; + + //Add lod cross fade + if (meta.fadeAnim > 0 && meta.fadeOutLod > 0) + { + meta.fadeAnim = max(0, meta.fadeAnim - unity_DeltaTime.z); + + //Normal + if (visibleLod > 0 && meta.fadeAnim > 0) + { + batchOffset = prefab.indexBufferStartOffset + ((meta.fadeOutLod - 1) * prefab.maxCount); + indirectArgsCount = prefab.indirectArgsIndexAndCountPerLod[meta.fadeOutLod - 1].y; + indirectArgsCountOffset = prefab.indirectArgsIndexAndCountPerLod[meta.fadeOutLod - 1].x + 1; + + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset], 1, visibleCount); + + for (i = 1, u = 5; i < indirectArgsCount; i++, u += 5) + { + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset + u], 1); + } + + perCamVisibleIndexesBuffer[batchOffset + visibleCount] = instanceIndex; + } + + //Shadow + if (visibleShadowLod > 0 && meta.fadeAnim > 0) + { + batchOffset = prefab.indexBufferStartOffset + ((meta.fadeOutLod - 1) * prefab.maxCount); + indirectArgsCount = prefab.indirectArgsIndexAndCountPerLod[meta.fadeOutLod - 1].y; + indirectArgsCountOffset = indirectArgsCount * 5 + prefab.indirectArgsIndexAndCountPerLod[meta.fadeOutLod - 1].x + 1; + + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset], 1, visibleCount); + + for (i = 1, u = 5; i < indirectArgsCount; i++, u += 5) + { + InterlockedAdd(perCamIndirectArgumentsBuffer[indirectArgsCountOffset + u], 1); + } + + perCamShadowVisibleIndexesBuffer[batchOffset + visibleCount] = instanceIndex; + } + } + + meta.visibleLod = max(visibleLod, visibleShadowLod); + perCamMeta[instanceIndex] = meta; +} + +[numthreads(GROUPS,1,1)] +void Cull_32(uint3 id : SV_DispatchThreadID) { Cull(id); } + +[numthreads(GROUPS,1,1)] +void Cull_64(uint3 id : SV_DispatchThreadID) { Cull(id); } + +[numthreads(GROUPS,1,1)] +void Cull_128(uint3 id : SV_DispatchThreadID) { Cull(id); } + +[numthreads(GROUPS,1,1)] +void Cull_256(uint3 id : SV_DispatchThreadID) { Cull(id); } + +[numthreads(GROUPS,1,1)] +void Cull_512(uint3 id : SV_DispatchThreadID) { Cull(id); } + +[numthreads(GROUPS,1,1)] +void Cull_1024(uint3 id : SV_DispatchThreadID) { Cull(id); } + +[numthreads(GROUPS, 1, 1)] +void Clear_32(uint3 id : SV_DispatchThreadID) { ClearArgumentsBuffer(id); } + +[numthreads(GROUPS, 1, 1)] +void Clear_64(uint3 id : SV_DispatchThreadID) { ClearArgumentsBuffer(id); } + +[numthreads(GROUPS, 1, 1)] +void Clear_128(uint3 id : SV_DispatchThreadID) { ClearArgumentsBuffer(id); } + +[numthreads(GROUPS, 1, 1)] +void Clear_256(uint3 id : SV_DispatchThreadID) { ClearArgumentsBuffer(id); } + +[numthreads(GROUPS, 1, 1)] +void Clear_512(uint3 id : SV_DispatchThreadID) { ClearArgumentsBuffer(id); } + +[numthreads(GROUPS, 1, 1)] +void Clear_1024(uint3 id : SV_DispatchThreadID) { ClearArgumentsBuffer(id); } + +[numthreads(GROUPS, 1, 1)] +void Upload_32(uint3 id : SV_DispatchThreadID) { Upload(id); } + +[numthreads(GROUPS, 1, 1)] +void Upload_64(uint3 id : SV_DispatchThreadID) { Upload(id); } + +[numthreads(GROUPS, 1, 1)] +void Upload_128(uint3 id : SV_DispatchThreadID) { Upload(id); } + +[numthreads(GROUPS, 1, 1)] +void Upload_256(uint3 id : SV_DispatchThreadID) { Upload(id); } + +[numthreads(GROUPS, 1, 1)] +void Upload_512(uint3 id : SV_DispatchThreadID) { Upload(id); } + +[numthreads(GROUPS, 1, 1)] +void Upload_1024(uint3 id : SV_DispatchThreadID) { Upload(id); } + + +[numthreads(GROUPS, 1, 1)] +void Resize_32(uint3 id : SV_DispatchThreadID) { Resize(id); } + +[numthreads(GROUPS, 1, 1)] +void Resize_64(uint3 id : SV_DispatchThreadID) { Resize(id); } + +[numthreads(GROUPS, 1, 1)] +void Resize_128(uint3 id : SV_DispatchThreadID) { Resize(id); } + +[numthreads(GROUPS, 1, 1)] +void Resize_256(uint3 id : SV_DispatchThreadID) { Resize(id); } + +[numthreads(GROUPS, 1, 1)] +void Resize_512(uint3 id : SV_DispatchThreadID) { Resize(id); } + +[numthreads(GROUPS, 1, 1)] +void Resize_1024(uint3 id : SV_DispatchThreadID) { Resize(id); } + + diff --git a/Resources/ThoMagic Renderer Instancing.compute.meta b/Resources/ThoMagic Renderer Instancing.compute.meta new file mode 100644 index 0000000..10b3d3b --- /dev/null +++ b/Resources/ThoMagic Renderer Instancing.compute.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4a840c1c229294d4996e80608a899881 +ComputeShaderImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources/ThoMagicRenderer.cginc b/Resources/ThoMagicRenderer.cginc new file mode 100644 index 0000000..465a771 --- /dev/null +++ b/Resources/ThoMagicRenderer.cginc @@ -0,0 +1,136 @@ +#pragma multi_compile _LOD_FADE_CROSSFADE + +#ifndef NODE_THOMAGIC_RENDERER_INCLUDED +#define NODE_THOMAGIC_RENDERER_INCLUDED + +#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED + + +#define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs +#include "UnityIndirect.cginc" + +#define Use_Macro_UNITY_MATRIX_M_instead_of_unity_ObjectToWorld unity_ObjectToWorld +#define Use_Macro_UNITY_MATRIX_I_M_instead_of_unity_WorldToObject unity_WorldToObject + +#define LOD_FADE_CROSSFADE + +struct InstanceData +{ + // position.xyz : position.xyz + // position.w : rotation.x + // scale.x : scale.xz + // scale.y : scale.y + // scale.zw : rotation.yz + float4 position; + float4 scale; + uint prefabId; + int3 padding; +}; + +struct InstanceMeta +{ + uint visibleLod; + uint fadeOutLod; + float fadeAnim; + + // The scale of the instance can be adjusted + float Scale; +}; + +StructuredBuffer trInstances; +StructuredBuffer trPerCamVisibleIndexesBuffer; +StructuredBuffer trPerCamMeta; + +float4x4 trInstanceMatrix; +int trLodNr; + +float4x4 TRS(float3 t, float4 r, float3 s) +{ + float4x4 result = (float4x4)0; + result[0][0] = (1.0f - 2.0f * (r.y * r.y + r.z * r.z)) * s.x; + result[1][0] = (r.x * r.y + r.z * r.w) * s.x * 2.0f; + result[2][0] = (r.x * r.z - r.y * r.w) * s.x * 2.0f; + result[3][0] = 0.0f; + result[0][1] = (r.x * r.y - r.z * r.w) * s.y * 2.0f; + result[1][1] = (1.0f - 2.0f * (r.x * r.x + r.z * r.z)) * s.y; + result[2][1] = (r.y * r.z + r.x * r.w) * s.y * 2.0f; + result[3][1] = 0.0f; + result[0][2] = (r.x * r.z + r.y * r.w) * s.z * 2.0f; + result[1][2] = (r.y * r.z - r.x * r.w) * s.z * 2.0f; + result[2][2] = (1.0f - 2.0f * (r.x * r.x + r.y * r.y)) * s.z; + result[3][2] = 0.0f; + result[0][3] = t.x; + result[1][3] = t.y; + result[2][3] = t.z; + result[3][3] = 1.0f; + return result; +} + +void DecompressInstanceMatrix(inout float4x4 m, InstanceData instanceData, InstanceMeta meta) +{ + float3 position = instanceData.position.xyz; + float4 rotation = float4(instanceData.position.w, instanceData.scale.zw, 0); + float3 scale = instanceData.scale.xyx * meta.Scale; + rotation.w = sqrt(1.0 - rotation.x * rotation.x - rotation.y * rotation.y - rotation.z * rotation.z); + m = TRS(position, rotation, scale); +} + +float4x4 inverse(float4x4 input) +{ +#define minor(a,b,c) determinant(float3x3(input.a, input.b, input.c)) + + float4x4 cofactors = float4x4( + minor(_22_23_24, _32_33_34, _42_43_44), + -minor(_21_23_24, _31_33_34, _41_43_44), + minor(_21_22_24, _31_32_34, _41_42_44), + -minor(_21_22_23, _31_32_33, _41_42_43), + + -minor(_12_13_14, _32_33_34, _42_43_44), + minor(_11_13_14, _31_33_34, _41_43_44), + -minor(_11_12_14, _31_32_34, _41_42_44), + minor(_11_12_13, _31_32_33, _41_42_43), + + minor(_12_13_14, _22_23_24, _42_43_44), + -minor(_11_13_14, _21_23_24, _41_43_44), + minor(_11_12_14, _21_22_24, _41_42_44), + -minor(_11_12_13, _21_22_23, _41_42_43), + + -minor(_12_13_14, _22_23_24, _32_33_34), + minor(_11_13_14, _21_23_24, _31_33_34), + -minor(_11_12_14, _21_22_24, _31_32_34), + minor(_11_12_13, _21_22_23, _31_32_33) + ); +#undef minor + return transpose(cofactors) / determinant(input); +} +#endif + +void SetupThoMagicRenderer() +{ +#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED + InitIndirectDrawArgs(0); + + uint instanceID = GetIndirectInstanceID_Base(unity_InstanceID); + uint instanceIndex = trPerCamVisibleIndexesBuffer[instanceID]; + InstanceMeta meta = trPerCamMeta[instanceIndex]; + DecompressInstanceMatrix(unity_ObjectToWorld, trInstances[instanceIndex], meta); + unity_ObjectToWorld = mul(unity_ObjectToWorld, trInstanceMatrix); + unity_WorldToObject = inverse(unity_ObjectToWorld); + + if (meta.visibleLod == trLodNr) + unity_LODFade.x = (1.0f - meta.fadeAnim); + else + unity_LODFade.x = -(1.0f - meta.fadeAnim); + + // {% hd %} + // Instances are static so the previous matrix is the same as the current one. + // These matrices are required for correct motion vectors in HDRP. +#if SHADERPASS == SHADERPASS_MOTION_VECTORS && defined(SHADERPASS_CS_HLSL) + unity_MatrixPreviousM = unity_ObjectToWorld; + unity_MatrixPreviousMI = unity_WorldToObject; +#endif + // {% endhd %} +#endif +} + +#endif \ No newline at end of file diff --git a/Resources/ThoMagicRenderer.cginc.meta b/Resources/ThoMagicRenderer.cginc.meta new file mode 100644 index 0000000..c7c8a50 --- /dev/null +++ b/Resources/ThoMagicRenderer.cginc.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7fe5ed120e85ccf499d429b359f89173 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Resources/ThoMagicRenderer.hlsl b/Resources/ThoMagicRenderer.hlsl new file mode 100644 index 0000000..3e0e6d6 --- /dev/null +++ b/Resources/ThoMagicRenderer.hlsl @@ -0,0 +1,9 @@ +#include "ThoMagicRenderer.cginc" + +#ifndef THOMAGICRENDERERSETUP_INClUDED +#define THOMAGICRENDERERSETUP_INClUDED +void IncludeThoMagicRenderer_float(float3 In, out float3 Out) +{ + Out = In; +} +#endif \ No newline at end of file diff --git a/Resources/ThoMagicRenderer.hlsl.meta b/Resources/ThoMagicRenderer.hlsl.meta new file mode 100644 index 0000000..f0ab5f9 --- /dev/null +++ b/Resources/ThoMagicRenderer.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 678dff042fe9c004cb7522c2233af44d +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/SceneRenderSettings.cs b/SceneRenderSettings.cs new file mode 100644 index 0000000..50a0046 --- /dev/null +++ b/SceneRenderSettings.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public struct SceneRenderSettings + { + public bool HasMainLight; + public bool HasMainLightShadows; + public Vector3 MainLightDirection; + } +} diff --git a/SceneRenderSettings.cs.meta b/SceneRenderSettings.cs.meta new file mode 100644 index 0000000..c696fc0 --- /dev/null +++ b/SceneRenderSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c1bc4c6b611d9d439d0f03feecc4e71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TerrainDetailRenderer.cs b/TerrainDetailRenderer.cs new file mode 100644 index 0000000..d1347bd --- /dev/null +++ b/TerrainDetailRenderer.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +#if UNITY_EDITOR +using UnityEditor; +#endif +using UnityEngine; +using UnityEngine.Rendering; + +namespace Assets.ThoMagic.Renderer +{ + /// + /// Adds a tree renderer and grass renderer to a terrain object. + /// + [AddComponentMenu("ThoMagic/Detail Renderer")] + [ExecuteAlways] + [DisallowMultipleComponent] + public class TerrainDetailRenderer : MonoBehaviour + { + /// + /// This event is called after this detail renderer is initialized. + /// + public event EventHandler Initialized; + + /// + /// The terrain object. + /// + public Terrain terrain; + /// + /// Cached terrain data. + /// + public TerrainData terrainData; + + [NonSerialized] + private bool isInitialized = false; + + [NonSerialized] + private TreeRenderer treeRenderer; + [NonSerialized] + private GrassRenderer grassRenderer; + + [NonSerialized] + private Vector3 lastPosition; + + [NonSerialized] + private Quaternion lastOrientation; + + [NonSerialized] + private Vector3 lastScale; + + + [Tooltip("Should ThoMagic Renderer automatically refresh the terrain data if the terrain was modified at runtime? If disabled, use Refresh() from a custom script to refresh the terrain data.")] + [SerializeField] + private bool _autoRefreshTerrainAtRuntime = true; + + [Tooltip("Should the default grass and tree rendering of Unity be enabled when ThoMagic Renderer is disabled?")] + [SerializeField] + private bool _enableUnityRendererWhenDisabled = true; + + [Tooltip("Delay the initialization of ThoMagic Renderer until the first LateUpdate event")] + [SerializeField] + private bool _delayInitialize = true; + + /// + /// If true and the terrain is changed at runtime the renderers are updated automaticly. + /// If needed you can also refresh them manually by calling Refresh(). + /// + public bool AutoRefreshTerrainAtRuntime + { + get => _autoRefreshTerrainAtRuntime; + set => _autoRefreshTerrainAtRuntime = value; + } + + /// + /// Enable the default unity renderer for trees and grass if this component gets disabled? + /// + public bool EnableUnityRendererWhenDisabled + { + get => _enableUnityRendererWhenDisabled; + set => _enableUnityRendererWhenDisabled = value; + } + + private bool HasValidTerrainData() + { + TerrainData terrainData = GetTerrainData(); + return terrainData != null && terrainData.detailResolution > 0 && terrainData.alphamapResolution > 0 && terrainData.heightmapResolution > 0 && terrainData.size != Vector3.zero; + } + + private bool CanInitialize() => isActiveAndEnabled && HasValidTerrainData(); + + private void Awake() + { + //Load resources + + } + + private void OnEnable() + { + if (_delayInitialize && Application.isPlaying || (isInitialized || !CanInitialize())) + return; + Initialize(); + } + + private void Start() + { + if (_delayInitialize && Application.isPlaying || (isInitialized || !CanInitialize())) + return; + Initialize(); + } + + private void OnDisable() + { + if (!isInitialized) + return; + Destroy(); + } + + private void OnDestroy() + { + if (!isInitialized) + return; + Destroy(); + } + + //Called by unity on terrain changes + public void OnTerrainChanged(TerrainChangedFlags flags) + { + if (!isInitialized || !AutoRefreshTerrainAtRuntime) + return; + Refresh(flags); + } + + /// + /// Used to refresh the renderers. + /// + /// The terrain changes + public void Refresh(TerrainChangedFlags flags) + { + //if a detail object has to be replaced by a placeholder return immediatly, as a second OnTerrainChanged will be called + //cause the detail protoypes where changed on the terrain. + if(flags.HasFlag(TerrainChangedFlags.FlushEverythingImmediately) && ReplaceUnsupportedDetails() && AutoRefreshTerrainAtRuntime) + return; + + treeRenderer?.OnTerrainChanged(flags); + grassRenderer?.OnTerrainChanged(flags); + } + + /// + /// Replace unsupported grass details with placeholders. + /// + /// True if a placeholder was created, otherwise false. + private bool ReplaceUnsupportedDetails() + { + bool flag = false; + DetailPrototype[] detailPrototypes = terrainData.detailPrototypes; + for (int index = 0; index < detailPrototypes.Length; ++index) + { + if (!RendererUtility.IsSupportedByUnity(detailPrototypes[index])) + { + GameObject supportedPlaceholder = RendererUtility.GetSupportedPlaceholder(detailPrototypes[index]); + if (supportedPlaceholder != detailPrototypes[index].prototype) + { + detailPrototypes[index].prototype = supportedPlaceholder; + flag = true; + } + } + } + if (flag) + terrainData.detailPrototypes = detailPrototypes; + return flag; + } + + private void DisableUnityRenderer() + { + if (terrain == null) + return; + terrain.drawTreesAndFoliage = false; + } + + private void RestoreUnityRenderer() + { + if (terrain == null || !EnableUnityRendererWhenDisabled) + return; + terrain.drawTreesAndFoliage = true; + } + + private void Initialize() + { + if (isInitialized) + return; + + DisableUnityRenderer(); + GetTerrainData(); + ReplaceUnsupportedDetails(); + + if (treeRenderer == null) + treeRenderer = new TreeRenderer(terrain); + + treeRenderer?.Load(); + + if (grassRenderer == null) + grassRenderer = new GrassRenderer(terrain); + + grassRenderer?.Load(); + + if (Initialized != null) + Initialized(this, this); + + isInitialized = true; + + lastPosition = terrain.transform.position; + lastOrientation = terrain.transform.rotation; + lastScale = terrain.transform.localScale; + +#if UNITY_EDITOR + SceneView.lastActiveSceneView?.Repaint(); + + RenderPipelineManager.endContextRendering -= OnBeginContextRendering; + RenderPipelineManager.endContextRendering += OnBeginContextRendering; +#endif + } + +#if UNITY_EDITOR + private void OnBeginContextRendering( + ScriptableRenderContext context, + List cameras) + { + LateUpdate(); + } +#endif + private void LateUpdate() + { + if (!isInitialized && CanInitialize()) + Initialize(); + + if (!isInitialized) + return; + + //TODO: We are currently not supporting a floating offset, + //so if the terrain moves we refresh/rebuild everything, + //which is expenisve. If possible we should switch to a + //floating offset. + if (terrain.transform.position != lastPosition || + terrain.transform.localScale != lastScale || + terrain.transform.rotation != lastOrientation) + { + lastPosition = terrain.transform.position; + lastOrientation = terrain.transform.rotation; + lastScale = terrain.transform.localScale; + Refresh(TerrainChangedFlags.FlushEverythingImmediately); + } + + treeRenderer?.LateUpdate(); + grassRenderer?.LateUpdate(); + } + + private void Destroy() + { + if (!isInitialized) + return; + + treeRenderer?.Destroy(); + treeRenderer = null; + grassRenderer?.Destroy(); + grassRenderer = null; + + RestoreUnityRenderer(); + isInitialized = false; + +#if UNITY_EDITOR + RenderPipelineManager.endContextRendering -= OnBeginContextRendering; +#endif + } + + private TerrainData GetTerrainData() + { + if (terrain == null) + terrain = GetComponent(); + terrainData = terrain != null ? terrain.terrainData : null; + return terrainData; + } + + private Bounds CalculateBounds(GameObject obj) + { + var meshRenderer = obj.GetComponentsInChildren(); + + Bounds b = new Bounds(); + if (meshRenderer.Length > 0) + { + b = new Bounds(meshRenderer[0].bounds.center, meshRenderer[0].bounds.size); + for (int r = 1; r < meshRenderer.Length; r++) + { + b.Encapsulate(meshRenderer[r].bounds); + } + } + + return b; + } + } +} diff --git a/TerrainDetailRenderer.cs.meta b/TerrainDetailRenderer.cs.meta new file mode 100644 index 0000000..004d510 --- /dev/null +++ b/TerrainDetailRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 533ab6e6ebf6be745ae7f02dc1922e05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ThoMagicRendererObjectSettings.cs b/ThoMagicRendererObjectSettings.cs new file mode 100644 index 0000000..b79c772 --- /dev/null +++ b/ThoMagicRendererObjectSettings.cs @@ -0,0 +1,145 @@ +using System; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + [AddComponentMenu("ThoMagic/Renderer Object Settings")] + [DisallowMultipleComponent] + [ExecuteAlways] + public class ThoMagicRendererObjectSettings : MonoBehaviour, IInstanceRenderSettings + { + [SerializeField] + private bool _render = true; + [SerializeField] + private bool _overrideRendering; + [Min(0.0f)] + [SerializeField] + private float _renderDistance = 150; + [SerializeField] + private bool _overrideRenderDistance; + [SerializeField] + private bool _renderShadows = true; + [SerializeField] + private bool _overrideRenderShadows; + [Min(0.0f)] + [SerializeField] + private float _shadowDistance = 80; + [SerializeField] + private bool _overrideShadowDistance; + [Range(0.01f, 1f)] + [SerializeField] + private float _densityInDistance = 0.125f; + [SerializeField] + private bool _overrideDensityInDistance; + [SerializeField] + private Vector2 _densityInDistanceFalloff = new Vector2(0.08f, 0.0075f); + [SerializeField] + private bool _overrideDensityInDistanceFalloff; + private ReflectionProbe _reflectionProbe; + + public InstanceRenderSettings Settings => new InstanceRenderSettings() + { + Supported = true, + Render = !this._overrideRendering || this._render, + RenderDistance = this._overrideRenderDistance ? this._renderDistance : -1f, + ShadowDistance = this._overrideShadowDistance ? this._shadowDistance : -1f, + Shadows = !this._overrideRenderShadows || this._renderShadows, + DensityInDistance = this._overrideDensityInDistance ? this._densityInDistance : 1f, + DensityInDistanceFalloff = this._overrideDensityInDistanceFalloff ? this._densityInDistanceFalloff : Vector2.zero + }; + + public bool? Render + { + get => !this._overrideRendering ? new bool?() : new bool?(this._render); + set + { + this._overrideRendering = value.HasValue; + if (!value.HasValue) + return; + this._render = value.Value; + } + } + + public float? RenderDistance + { + get => !this._overrideRenderDistance ? new float?() : new float?(this._renderDistance); + set + { + this._overrideRenderDistance = value.HasValue; + if (!value.HasValue) + return; + this._renderDistance = value.Value; + } + } + + public bool? RenderShadows + { + get => !this._overrideRenderShadows ? new bool?() : new bool?(this._renderShadows); + set + { + this._overrideRenderShadows = value.HasValue; + if (!value.HasValue) + return; + this._renderShadows = value.Value; + } + } + + public float? ShadowDistance + { + get => !this._overrideShadowDistance ? new float?() : new float?(this._shadowDistance); + set + { + this._overrideShadowDistance = value.HasValue; + if (!value.HasValue) + return; + this._shadowDistance = value.Value; + } + } + + public float? DensityInDistance + { + get => !this._overrideDensityInDistance ? new float?() : new float?(this._densityInDistance); + set + { + this._overrideDensityInDistance = value.HasValue; + if (!value.HasValue) + return; + this._densityInDistance = value.Value; + } + } + + public Vector2? DensityInDistanceFalloff + { + get => !this._overrideDensityInDistanceFalloff ? new Vector2?() : new Vector2?(this._densityInDistanceFalloff); + set + { + this._overrideDensityInDistanceFalloff = value.HasValue; + if (!value.HasValue) + return; + this._densityInDistanceFalloff = value.Value; + } + } + + private void OnEnable() + { + _reflectionProbe = GetComponent(); + if (_reflectionProbe == null) + return; + CameraRenderer.ReflectionProbeSettings = this; + } + + private void OnDisable() + { + if (_reflectionProbe == null || CameraRenderer.ReflectionProbeSettings as ThoMagicRendererObjectSettings != this) + return; + CameraRenderer.ReflectionProbeSettings = null; + } + + private void OnValidate() + { + if (_reflectionProbe == null || !Application.isEditor) + return; + this._reflectionProbe.RenderProbe(); + } + } +} diff --git a/ThoMagicRendererObjectSettings.cs.meta b/ThoMagicRendererObjectSettings.cs.meta new file mode 100644 index 0000000..44f96db --- /dev/null +++ b/ThoMagicRendererObjectSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2a37bd5b745b59448b6bc6c1ff5f09c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TileObjectRenderer.cs b/TileObjectRenderer.cs new file mode 100644 index 0000000..5fe2f59 --- /dev/null +++ b/TileObjectRenderer.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +#if UNITY_EDITOR +using UnityEditor; +#endif +using UnityEngine; +using UnityEngine.Rendering; + +namespace Assets.ThoMagic.Renderer +{ + [ExecuteAlways] + [DisallowMultipleComponent] + public class TileObjectRenderer : MonoBehaviour + { + /// + /// This event is called after this detail renderer is initialized. + /// + public event EventHandler Initialized; + + [NonSerialized] + private bool isInitialized = false; + + + [Tooltip("Delay the initialization of ThoMagic Renderer until the first LateUpdate event")] + [SerializeField] + private bool _delayInitialize = true; + + private bool CanInitialize() => isActiveAndEnabled; + + [NonSerialized] + private TileObjectStreamer tileObjectStreamer; + + private void Awake() + { + //Load resources + + } + + private void OnEnable() + { + if (_delayInitialize && Application.isPlaying || (isInitialized || !CanInitialize())) + return; + Initialize(); + } + + private void Start() + { + if (_delayInitialize && Application.isPlaying || (isInitialized || !CanInitialize())) + return; + Initialize(); + } + + private void OnDisable() + { + if (!isInitialized) + return; + Destroy(); + } + + private void OnDestroy() + { + if (!isInitialized) + return; + Destroy(); + } + + private void Initialize() + { + if (isInitialized) + return; + + if (tileObjectStreamer == null) + tileObjectStreamer = new TileObjectStreamer(gameObject); + + tileObjectStreamer?.Load(); + + if (Initialized != null) + Initialized(this, this); + + isInitialized = true; + +#if UNITY_EDITOR + SceneView.lastActiveSceneView?.Repaint(); + + RenderPipelineManager.endContextRendering -= OnBeginContextRendering; + RenderPipelineManager.endContextRendering += OnBeginContextRendering; +#endif + } + +#if UNITY_EDITOR + private void OnBeginContextRendering( + ScriptableRenderContext context, + List cameras) + { + //LateUpdate(); + } +#endif + private void LateUpdate() + { + if (!isInitialized && CanInitialize()) + Initialize(); + + if (!isInitialized) + return; + + tileObjectStreamer?.LateUpdate(); + } + + private void Destroy() + { + if (!isInitialized) + return; + + try + { + tileObjectStreamer?.Destroy(); + } + catch { } + tileObjectStreamer = null; + + isInitialized = false; + +#if UNITY_EDITOR + RenderPipelineManager.endContextRendering -= OnBeginContextRendering; +#endif + } + + private void OnTransformChildrenChanged() + { + tileObjectStreamer?.MarkDirty(); + } + } +} diff --git a/TileObjectRenderer.cs.meta b/TileObjectRenderer.cs.meta new file mode 100644 index 0000000..2b5fd3b --- /dev/null +++ b/TileObjectRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 365a0b5496e0c71438e4b4a0b232aca6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TileObjectStreamer.cs b/TileObjectStreamer.cs new file mode 100644 index 0000000..382b1e5 --- /dev/null +++ b/TileObjectStreamer.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class TileObjectStreamer : InstanceStreamer + { + private readonly GameObject objectContainer; + private Bounds worldBounds; + private float maxDistance; + + private Dictionary registeredPrefabs = new Dictionary(); + private Dictionary instanceToGameObject = new Dictionary(); + + private bool isDirty = true; + + public TileObjectStreamer(GameObject objectContainer) + : base() + { + this.objectContainer = objectContainer; + } + + public void Recycle() + { + Clear(); + + int ownerHash = GetHashCode(); + foreach (var item in registeredPrefabs) + { + RendererPool.RemoveObject(item.Value, ownerHash); + } + + foreach (var item in instanceToGameObject) + { + var mrs = item.Value.gameObject.GetComponentsInChildren(); + foreach(var mr in mrs) + mr.enabled = true; + + var lod = item.Value.gameObject.GetComponent(); + if (lod != null) + lod.enabled = true; + + } + + registeredPrefabs.Clear(); + instanceToGameObject.Clear(); + } + + public void Load() + { + isDirty = false; + + Recycle(); + + worldBounds.SetMinMax(Vector3.zero, Vector3.zero); + + registeredPrefabs.Clear(); + instanceToGameObject.Clear(); + + int ownerHash = GetHashCode(); + + foreach (Transform child in objectContainer.transform) + { + var hash = CalculateHash(child.gameObject); + + if(hash != 0) + { + int objectId; + + if (registeredPrefabs.ContainsKey(hash)) + { + objectId = registeredPrefabs[hash]; + } + else + { + var settings = GetSettingsOrDefault(child.gameObject); + objectId = RendererPool.RegisterObject(child.gameObject, settings, this, ownerHash); + registeredPrefabs.Add(hash, objectId); + if (settings.Settings.RenderDistance == 0) + maxDistance = float.MaxValue; + + maxDistance = Mathf.Max(maxDistance, settings.Settings.RenderDistance); + } + + var mrs = child.gameObject.GetComponentsInChildren(); + Bounds bounds = new Bounds(); + bool first = true; + foreach (var mr in mrs) + { + if (first) + { + bounds = mr.bounds; + } + else + { + bounds.Encapsulate(mr.bounds); + } + + mr.enabled = false; + } + + var lod = child.gameObject.GetComponent(); + if(lod != null) + lod.enabled = false; + + var instanceId = AddInstance(objectId, new Vector3(child.position.x, child.position.y, child.position.z), child.rotation, child.lossyScale.x, child.lossyScale.z); + + instanceToGameObject.Add(instanceId, child.gameObject); + if (instanceToGameObject.Count == 1) + worldBounds = bounds; + else + worldBounds.Encapsulate(bounds); + } + } + } + + private IInstanceRenderSettings GetSettingsOrDefault(GameObject gameObject) + { + IInstanceRenderSettings instanceRenderSettings; + return gameObject.TryGetComponent(out instanceRenderSettings) ? instanceRenderSettings : new DefaultRenderSettings(); + } + + private int CalculateHash(GameObject gameObject) + { + int num = 13; + + var lodGroup = gameObject.GetComponent(); + + LOD[] lodArray; + + if (lodGroup == null) + { + var mr = gameObject.GetComponentsInChildren(); + + if (mr == null) + return 0; + + lodArray = new LOD[1] + { + new LOD(0.0001f, mr) + }; + } + else + lodArray = lodGroup.GetLODs(); + + + foreach (var loD in lodArray) + num = HashCode.Combine(num, CalcualteContentHash(loD)); + + return num; + } + + private int CalcualteContentHash(LOD lod) + { + int num = 13; + if (lod.renderers != null) + { + foreach (var renderer in lod.renderers) + { + if (renderer == null) + { + num = HashCode.Combine(num, 13); + } + else + { + MeshFilter component = renderer.GetComponent(); + + num = HashCode.Combine(HashCode.Combine( + num, + component == null || component.sharedMesh == null ? 13 : component.sharedMesh.GetHashCode(), + renderer.shadowCastingMode.GetHashCode(), + CalculateContentHash(renderer.sharedMaterials), + renderer.motionVectorGenerationMode.GetHashCode(), + renderer.receiveShadows.GetHashCode(), + renderer.rendererPriority.GetHashCode(), + renderer.renderingLayerMask.GetHashCode()), + renderer.gameObject.layer.GetHashCode(), + lod.screenRelativeTransitionHeight.GetHashCode()); + } + } + } + + return num; + } + + private int CalculateContentHash(Material[] materials) + { + int num = 13; + if (materials != null) + { + foreach (Material material in materials) + num = HashCode.Combine(num, + material != null ? material.GetHashCode() : 13); + } + return num; + } + + public override bool IsInRange(Camera referenceCamera, Plane[] planes) + { + var eyePos = referenceCamera.transform.position; + + if ((eyePos - worldBounds.ClosestPoint(eyePos)).magnitude <= Mathf.Min(referenceCamera.farClipPlane, maxDistance)) + return true; + + return false; + } + + public override int GetHashCode() + { + return streamerInstanceId.GetHashCode();//HashCode.Combine(streamerInstanceId, objectInstanceIds.Count); + } + + public void Destroy() + { + Recycle(); + } + + public void LateUpdate() + { + if (isDirty) + { + Load(); + } + } + + public void MarkDirty() + { + isDirty = true; + } + + private class DefaultRenderSettings : IInstanceRenderSettings + { + public InstanceRenderSettings Settings => new InstanceRenderSettings() + { + Render = true, + DensityInDistance = 1f, + DensityInDistanceFalloff = Vector2.zero, + RenderDistance = 0.0f, + ShadowDistance = 0.0f, + Shadows = true, + Supported = true + }; + } + } +} diff --git a/TileObjectStreamer.cs.meta b/TileObjectStreamer.cs.meta new file mode 100644 index 0000000..74bddf7 --- /dev/null +++ b/TileObjectStreamer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 14d63a559cc9211479181fed32a2a5c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TreeRenderer.cs b/TreeRenderer.cs new file mode 100644 index 0000000..534f74f --- /dev/null +++ b/TreeRenderer.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class TreeRenderer : TreeStreamer + { + private Terrain terrain; + private TerrainData terrainData; + private List treePrototypesRegistered; + private Dictionary treeToInstances = new Dictionary(); + private HashSet touchedTrees = new HashSet(); + private bool isDirty; + private float maxTreeDistance = 0; + + class TreeInfo + { + internal int instanceId; + internal int objectId; + } + + public TreeRenderer(Terrain terrain) + : base(terrain) + { + this.terrain = terrain; + terrainData = terrain.terrainData; + } + + public void LateUpdate() + { + if (isDirty) + { + isDirty = false; + ApplyChanges(); + } + + if (Application.isEditor && !Application.isPlaying) + RebuildChangedPrototypes(); + } + + int GetTreeTerrainInstanceId(TreeInstance tree) + { + return HashCode.Combine(tree.position.x, tree.position.y, tree.position.z, tree.rotation, tree.widthScale, tree.heightScale, tree.prototypeIndex); + } + + public void ApplyChanges() + { + Vector3 size = terrainData.size; + Vector3 offset = terrain.transform.position; + + var treeInstances = terrainData.treeInstances; + touchedTrees.Clear(); + foreach (var treeInstance in treeInstances) + { + if (treeInstance.widthScale == 0) + continue; + + var treeTerrainInstanceId = GetTreeTerrainInstanceId(treeInstance); + if (!treeToInstances.ContainsKey(treeTerrainInstanceId)) + { + var treeId = treePrototypesRegistered[treeInstance.prototypeIndex]; + var prefab = RendererPool.GetObject(treeId); + var quaternion = Quaternion.Euler(0.0f, 57.2957801818848f * treeInstance.rotation, 0.0f); + var instanceId = AddInstance(treeId, new Vector3(treeInstance.position.x * size.x, + treeInstance.position.y * size.y, + treeInstance.position.z * size.z) + offset, quaternion, + treeInstance.widthScale * prefab.transform.localScale.x, + treeInstance.heightScale * prefab.transform.localScale.y + ); + treeToInstances[treeTerrainInstanceId] = new TreeInfo { instanceId = instanceId, objectId = treeId }; + touchedTrees.Add(treeTerrainInstanceId); + } + else + { + touchedTrees.Add(treeTerrainInstanceId); + } + } + + foreach(var treeTerrainInstanceId in treeToInstances.Keys.ToList()) + { + if(!touchedTrees.Contains(treeTerrainInstanceId)) + { + RemoveInstance(treeToInstances[treeTerrainInstanceId].objectId, treeToInstances[treeTerrainInstanceId].instanceId); + treeToInstances.Remove(treeTerrainInstanceId); + } + } + + Build(maxTreeDistance); + } + + public void Load() + { + maxTreeDistance = 0; + + Vector3 size = terrainData.size; + Vector3 offset = terrain.transform.position; + + Recycle(); + + int ownerHash = GetHashCode(); + + if (treePrototypesRegistered == null) + treePrototypesRegistered = new List(); + + var treePrototypes = terrainData.treePrototypes; + if(treePrototypes.Length > 0) + { + foreach(var treePrototype in treePrototypes) + { + if(treePrototype.prefab != null) + { + var settings = GetSettingsOrDefault(treePrototype.prefab); + treePrototypesRegistered.Add(RendererPool.RegisterObject(treePrototype.prefab, settings, this, ownerHash)); + + if (settings.Settings.RenderDistance == 0) + maxTreeDistance = float.MaxValue; + + if(settings.Settings.RenderDistance > maxTreeDistance) + maxTreeDistance = settings.Settings.RenderDistance; + } + } + + var treeInstances = terrainData.treeInstances; + foreach (var treeInstance in treeInstances) + { + var treeTerrainInstanceId = GetTreeTerrainInstanceId(treeInstance); + var treeId = treePrototypesRegistered[treeInstance.prototypeIndex]; + var prefab = RendererPool.GetObject(treeId); + var quaternion = Quaternion.Euler(0.0f, 57.2957801818848f * treeInstance.rotation, 0.0f); + var instanceId = AddInstance(treeId, new Vector3(treeInstance.position.x * size.x, + treeInstance.position.y * size.y, + treeInstance.position.z * size.z) + offset, quaternion, + treeInstance.widthScale * prefab.transform.localScale.x, + treeInstance.heightScale * prefab.transform.localScale.y); + treeToInstances[treeTerrainInstanceId] = new TreeInfo { instanceId = instanceId, objectId = treeId }; + } + } + + Build(maxTreeDistance); + } + + private void Recycle() + { + if (treePrototypesRegistered == null) + return; + + treeToInstances.Clear(); + + Clear(); + + var ownerHash = GetHashCode(); + + for (int index = 0; index < treePrototypesRegistered.Count; ++index) + { + var objectId = treePrototypesRegistered[index]; + RendererPool.RemoveObject(objectId, ownerHash); + } + + treePrototypesRegistered.Clear(); + } + + private void RebuildChangedPrototypes() + { + TreePrototype[] treePrototypes = terrainData.treePrototypes; + if (treePrototypes.Length != treePrototypesRegistered.Count) + { + Load(); + } + else + { + int ownerHash = GetHashCode(); + maxTreeDistance = 0; + for (int index = 0; index < treePrototypesRegistered.Count; ++index) + { + int treeId = treePrototypesRegistered[index]; + + GameObject prefab = treePrototypes[index].prefab; + GameObject gameObject = RendererPool.GetObject(treeId); + if (prefab != gameObject) + { + RendererPool.RemoveObject(treeId, ownerHash); + if (prefab != null) + { + var settings = GetSettingsOrDefault(prefab); + + if (settings.Settings.RenderDistance == 0) + maxTreeDistance = float.MaxValue; + + if (settings.Settings.RenderDistance > maxTreeDistance) + maxTreeDistance = settings.Settings.RenderDistance; + + treePrototypesRegistered[index] = RendererPool.RegisterObject(prefab, settings, this, ownerHash); + } + } + else if (RendererPool.ContentHashChanged(treeId)) + { + RendererPool.RemoveObject(treeId, ownerHash); + if (prefab != null) + { + var settings = GetSettingsOrDefault(prefab); + + if (settings.Settings.RenderDistance == 0) + maxTreeDistance = float.MaxValue; + + if (settings.Settings.RenderDistance > maxTreeDistance) + maxTreeDistance = settings.Settings.RenderDistance; + + treePrototypesRegistered[index] = RendererPool.RegisterObject(prefab, settings, this, ownerHash); + } + } + else if (prefab != null) + { + var settings = GetSettingsOrDefault(prefab); + + if (settings.Settings.RenderDistance == 0) + maxTreeDistance = float.MaxValue; + + if (settings.Settings.RenderDistance > maxTreeDistance) + maxTreeDistance = settings.Settings.RenderDistance; + + RendererPool.SetObjectSettings(prefab, settings); + } + } + } + } + + public void Destroy() + { + Recycle(); + } + + public void OnTerrainChanged(TerrainChangedFlags flags) + { + if(flags.HasFlag(TerrainChangedFlags.TreeInstances)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.DelayedHeightmapUpdate)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.Heightmap)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.HeightmapResolution)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.Holes)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.DelayedHolesUpdate)) + isDirty = true; + else if (flags.HasFlag(TerrainChangedFlags.FlushEverythingImmediately)) + isDirty = true; + } + + private IInstanceRenderSettings GetSettingsOrDefault(GameObject gameObject) + { + IInstanceRenderSettings instanceRenderSettings; + return gameObject.TryGetComponent(out instanceRenderSettings) ? instanceRenderSettings : new DefaultRenderSettings(); + } + + private class DefaultRenderSettings : IInstanceRenderSettings + { + public InstanceRenderSettings Settings => new InstanceRenderSettings() + { + Render = true, + DensityInDistance = 1f, + DensityInDistanceFalloff = Vector2.zero, + RenderDistance = 0.0f, + ShadowDistance = 0.0f, + Shadows = true, + Supported = true + }; + } + } +} diff --git a/TreeRenderer.cs.meta b/TreeRenderer.cs.meta new file mode 100644 index 0000000..72b0adf --- /dev/null +++ b/TreeRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0f78ad04f9bf164e81068b0dcffd13b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TreeStreamer.cs b/TreeStreamer.cs new file mode 100644 index 0000000..2d9100c --- /dev/null +++ b/TreeStreamer.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +namespace Assets.ThoMagic.Renderer +{ + public class TreeStreamer : InstanceStreamer + { + private readonly Terrain _terrain; + private Bounds worldBounds; + private float maxTreeDistance; + + public TreeStreamer(Terrain terrain) + : base() + => _terrain = terrain; + + public void Build(float maxDistance) + { + maxTreeDistance = maxDistance; + + Vector3 position = _terrain.GetPosition(); + worldBounds = new Bounds(_terrain.terrainData.bounds.center + position, _terrain.terrainData.bounds.size); + } + + public override bool IsInRange(Camera referenceCamera, Plane[] planes) + { + if (!_terrain.editorRenderFlags.HasFlag(TerrainRenderFlags.Trees)) + return false; + + var eyePos = referenceCamera.transform.position; + + if ((eyePos - worldBounds.ClosestPoint(eyePos)).magnitude <= Mathf.Min(referenceCamera.farClipPlane, maxTreeDistance)) + return true; + + return false; + } + } +} diff --git a/TreeStreamer.cs.meta b/TreeStreamer.cs.meta new file mode 100644 index 0000000..aab0238 --- /dev/null +++ b/TreeStreamer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 049083d3196ea1b429c2057a188c2bf2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..36becbd --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "com.incobyte.thomagicrenderer", + "version": "1.0.0", + "displayName": "ThoMagic Renderer", + "description": "A gpu driven renderer for Unity 2022.3 srp", + "unity": "2022.3", + "keywords": [ + "gpu culling" + ], + "author": { + "name": "Thomas Woischnig", + "email": "twoischnig@incobyte.de", + "url": "https://www.incobyte.de" + } +} \ No newline at end of file diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..9aeea46 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 17e2836121df18a49b08e0dd8cd807ec +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e5cceff --- /dev/null +++ b/readme.md @@ -0,0 +1,136 @@ +# ThoMagicRenderer + +## Overview +ThoMagicRenderer is a GPU-driven rendering system designed for efficiently managing and rendering large numbers of objects with minimal performance impact. Leveraging advanced GPU Instancing techniques, it enables developers to integrate and optimize massive amounts of assets, such as trees, grass, rocks, and other prefabs, without the need for deep expertise in Compute Shaders and GPU infrastructure. + +## Features +- **Efficient GPU Instancing**: Uses Indirect GPU Instancing to handle large numbers of objects efficiently. +- **User-Friendly Integration**: No need to master Compute Shaders; provides simple tools for easy setup. +- **Supports Various Assets**: Works seamlessly with Unity terrain details, trees, and prefabs. +- **Advanced Culling System**: Optimized frustum and occlusion culling for better performance. +- **LOD Management**: Automatically adjusts Level of Detail (LOD) settings for optimal rendering. +- **Custom Rendering Parameters**: Fine-tune rendering settings to fit project needs. +- **Compute Shader Acceleration**: Utilizes Unity's `RenderMeshIndirect` method and Compute Shaders for peak efficiency. + +## Installation +ThoMagicRenderer is available as a Unity Package Manager (UPM) package. + +1. Open Unity (2022.3 or later) with a project using a Scriptable Render Pipeline (SRP). +2. Open **Edit > Project Settings > Package Manager** and add a scoped registry if required. +3. Add the package via Git URL: + ```sh + https://github.com/yourusername/ThoMagicRenderer.git + ``` +4. Wait for Unity to download and install the package. +5. Follow the usage guide to set up your scene. + +## Usage +### Shader Graph +To use **ThoMagicRenderer** in Shader Graph: +1. Add the **ThoMagic Instanced Position** subgraph. +2. Connect it to the **Position** output. +3. If you are already using the Unity Position node, replace it with **ThoMagic Instanced Position**. + +### HLSL Shaders +#### URP Shaders +For all passes, include the following pragma directives: +```hlsl +#pragma multi_compile_instancing +#pragma instancing_options procedural:setupThoMagic +``` +Ensure that `ThoMagicRendererInclude.cginc` is included after the URP include files: +```hlsl +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" +#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl" +#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl" +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl" +#include "ThoMagicRenderer/Shaders/Include/ThoMagicRendererInclude.cginc" +``` +Modify your shader structs and functions: +```hlsl +struct VertexInput +{ + ... + UNITY_VERTEX_INPUT_INSTANCE_ID +}; + +struct VertexOutput +{ + ... + UNITY_VERTEX_INPUT_INSTANCE_ID +}; + +VertexOutput vert(VertexInput v) +{ + VertexOutput o; + UNITY_SETUP_INSTANCE_ID(v); + ... + return o; +} + +half4 frag(VertexOutput IN) +{ + UNITY_SETUP_INSTANCE_ID(IN); + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN); + ... + return o; +} +``` +#### HDRP Shaders +HDRP shaders follow the same pattern as URP shaders. Include the necessary directives: +```hlsl +#pragma multi_compile_instancing +#pragma instancing_options procedural:setupThoMagic +``` +Ensure `ThoMagicRendererInclude.cginc` is included after the HDRP include files: +```hlsl +#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" +#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl" +#include "ThoMagicRenderer/Shaders/Include/ThoMagicRendererInclude.cginc" +``` +Modify your shader structs and functions: +```hlsl +struct AttributesMesh +{ + ... + UNITY_VERTEX_INPUT_INSTANCE_ID +}; + +struct PackedVaryingsMeshToPS +{ + ... + UNITY_VERTEX_INPUT_INSTANCE_ID +}; + +PackedVaryingsMeshToPS vert(AttributesMesh inputMesh) +{ + PackedVaryingsMeshToPS output; + UNITY_SETUP_INSTANCE_ID(inputMesh); + UNITY_TRANSFER_INSTANCE_ID(inputMesh, output); + ... + return output; +} + +void Frag(PackedVaryingsMeshToPS packedInput, OUTPUT_GBUFFER(outGBuffer) #ifdef _DEPTHOFFSET_ON, out float outputDepth : SV_Depth #endif) +{ + UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(packedInput); + UNITY_SETUP_INSTANCE_ID(packedInput); + ... +} +``` + +## Requirements +- Unity 2022.3+ (or later) +- Compatible GPU supporting Compute Shaders +- Scriptable Render Pipeline (SRP) + +## License +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Contributing +Pull requests are welcome! If you have any improvements or bug fixes, feel free to contribute. + +## Contact +For questions or support, please reach out via [GitHub Issues](https://github.com/yourusername/ThoMagicRenderer/issues). +