diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..881e856 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f5c565fe4e68ffb4ab3749497f16016a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/BillboardDrawCall.cs b/Runtime/BillboardDrawCall.cs new file mode 100644 index 0000000..53ea2cb --- /dev/null +++ b/Runtime/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/Runtime/BillboardDrawCall.cs.meta b/Runtime/BillboardDrawCall.cs.meta new file mode 100644 index 0000000..88e477a --- /dev/null +++ b/Runtime/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/Runtime/CameraRenderSettings.cs b/Runtime/CameraRenderSettings.cs new file mode 100644 index 0000000..7c401a7 --- /dev/null +++ b/Runtime/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/Runtime/CameraRenderSettings.cs.meta b/Runtime/CameraRenderSettings.cs.meta new file mode 100644 index 0000000..940980c --- /dev/null +++ b/Runtime/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/Runtime/CameraRenderer.cs b/Runtime/CameraRenderer.cs new file mode 100644 index 0000000..05c6bc6 --- /dev/null +++ b/Runtime/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/Runtime/CameraRenderer.cs.meta b/Runtime/CameraRenderer.cs.meta new file mode 100644 index 0000000..6e82e1a --- /dev/null +++ b/Runtime/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/Runtime/CellLayout.cs b/Runtime/CellLayout.cs new file mode 100644 index 0000000..8909a94 --- /dev/null +++ b/Runtime/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/Runtime/CellLayout.cs.meta b/Runtime/CellLayout.cs.meta new file mode 100644 index 0000000..4193062 --- /dev/null +++ b/Runtime/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/Runtime/CellLayoutPool.cs b/Runtime/CellLayoutPool.cs new file mode 100644 index 0000000..f4a9da2 --- /dev/null +++ b/Runtime/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/Runtime/CellLayoutPool.cs.meta b/Runtime/CellLayoutPool.cs.meta new file mode 100644 index 0000000..81db81a --- /dev/null +++ b/Runtime/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/Runtime/DrawCall.cs b/Runtime/DrawCall.cs new file mode 100644 index 0000000..c1b7dda --- /dev/null +++ b/Runtime/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/Runtime/DrawCall.cs.meta b/Runtime/DrawCall.cs.meta new file mode 100644 index 0000000..f706731 --- /dev/null +++ b/Runtime/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/Runtime/DrawGroup.cs b/Runtime/DrawGroup.cs new file mode 100644 index 0000000..27fb676 --- /dev/null +++ b/Runtime/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/Runtime/DrawGroup.cs.meta b/Runtime/DrawGroup.cs.meta new file mode 100644 index 0000000..4ddfb14 --- /dev/null +++ b/Runtime/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/Runtime/FrameRenderer.cs b/Runtime/FrameRenderer.cs new file mode 100644 index 0000000..0d1fd57 --- /dev/null +++ b/Runtime/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/Runtime/FrameRenderer.cs.meta b/Runtime/FrameRenderer.cs.meta new file mode 100644 index 0000000..6f87a9b --- /dev/null +++ b/Runtime/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/Runtime/GrassRenderer.cs b/Runtime/GrassRenderer.cs new file mode 100644 index 0000000..630d3b1 --- /dev/null +++ b/Runtime/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/Runtime/GrassRenderer.cs.meta b/Runtime/GrassRenderer.cs.meta new file mode 100644 index 0000000..8ffedca --- /dev/null +++ b/Runtime/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/Runtime/GrassStreamer.cs b/Runtime/GrassStreamer.cs new file mode 100644 index 0000000..006162f --- /dev/null +++ b/Runtime/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/Runtime/GrassStreamer.cs.meta b/Runtime/GrassStreamer.cs.meta new file mode 100644 index 0000000..ab6d341 --- /dev/null +++ b/Runtime/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/Runtime/InstanceBuffer.cs b/Runtime/InstanceBuffer.cs new file mode 100644 index 0000000..090659f --- /dev/null +++ b/Runtime/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/Runtime/InstanceBuffer.cs.meta b/Runtime/InstanceBuffer.cs.meta new file mode 100644 index 0000000..20b1044 --- /dev/null +++ b/Runtime/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/Runtime/InstanceMatrix.cs b/Runtime/InstanceMatrix.cs new file mode 100644 index 0000000..0d9e6da --- /dev/null +++ b/Runtime/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/Runtime/InstanceMatrix.cs.meta b/Runtime/InstanceMatrix.cs.meta new file mode 100644 index 0000000..9ad60f0 --- /dev/null +++ b/Runtime/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/Runtime/InstanceRenderSettings.cs b/Runtime/InstanceRenderSettings.cs new file mode 100644 index 0000000..4d45fc3 --- /dev/null +++ b/Runtime/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/Runtime/InstanceRenderSettings.cs.meta b/Runtime/InstanceRenderSettings.cs.meta new file mode 100644 index 0000000..1e32039 --- /dev/null +++ b/Runtime/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/Runtime/InstanceStreamer.cs b/Runtime/InstanceStreamer.cs new file mode 100644 index 0000000..2ba7f05 --- /dev/null +++ b/Runtime/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/Runtime/InstanceStreamer.cs.meta b/Runtime/InstanceStreamer.cs.meta new file mode 100644 index 0000000..48e76d1 --- /dev/null +++ b/Runtime/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/Runtime/ObjectData.cs b/Runtime/ObjectData.cs new file mode 100644 index 0000000..9974787 --- /dev/null +++ b/Runtime/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/Runtime/ObjectData.cs.meta b/Runtime/ObjectData.cs.meta new file mode 100644 index 0000000..06d6036 --- /dev/null +++ b/Runtime/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/Runtime/RendererPool.cs b/Runtime/RendererPool.cs new file mode 100644 index 0000000..e2eb2fd --- /dev/null +++ b/Runtime/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/Runtime/RendererPool.cs.meta b/Runtime/RendererPool.cs.meta new file mode 100644 index 0000000..bbdf6c6 --- /dev/null +++ b/Runtime/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/Runtime/RendererUtility.cs b/Runtime/RendererUtility.cs new file mode 100644 index 0000000..a023180 --- /dev/null +++ b/Runtime/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/Runtime/RendererUtility.cs.meta b/Runtime/RendererUtility.cs.meta new file mode 100644 index 0000000..b2311b0 --- /dev/null +++ b/Runtime/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/Runtime/Resources.meta b/Runtime/Resources.meta new file mode 100644 index 0000000..fe08a86 --- /dev/null +++ b/Runtime/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 66cad6ae64c1463448771e8e1325a51c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Resources/ThoMagic Instanced Position.shadersubgraph b/Runtime/Resources/ThoMagic Instanced Position.shadersubgraph new file mode 100644 index 0000000..e7cb95b --- /dev/null +++ b/Runtime/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/Runtime/Resources/ThoMagic Instanced Position.shadersubgraph.meta b/Runtime/Resources/ThoMagic Instanced Position.shadersubgraph.meta new file mode 100644 index 0000000..bffaa82 --- /dev/null +++ b/Runtime/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/Runtime/Resources/ThoMagic Renderer Instancing.compute b/Runtime/Resources/ThoMagic Renderer Instancing.compute new file mode 100644 index 0000000..6810df1 --- /dev/null +++ b/Runtime/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/Runtime/Resources/ThoMagic Renderer Instancing.compute.meta b/Runtime/Resources/ThoMagic Renderer Instancing.compute.meta new file mode 100644 index 0000000..10b3d3b --- /dev/null +++ b/Runtime/Resources/ThoMagic Renderer Instancing.compute.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4a840c1c229294d4996e80608a899881 +ComputeShaderImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Resources/ThoMagicRenderer.cginc b/Runtime/Resources/ThoMagicRenderer.cginc new file mode 100644 index 0000000..465a771 --- /dev/null +++ b/Runtime/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/Runtime/Resources/ThoMagicRenderer.cginc.meta b/Runtime/Resources/ThoMagicRenderer.cginc.meta new file mode 100644 index 0000000..c7c8a50 --- /dev/null +++ b/Runtime/Resources/ThoMagicRenderer.cginc.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7fe5ed120e85ccf499d429b359f89173 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Resources/ThoMagicRenderer.hlsl b/Runtime/Resources/ThoMagicRenderer.hlsl new file mode 100644 index 0000000..3e0e6d6 --- /dev/null +++ b/Runtime/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/Runtime/Resources/ThoMagicRenderer.hlsl.meta b/Runtime/Resources/ThoMagicRenderer.hlsl.meta new file mode 100644 index 0000000..f0ab5f9 --- /dev/null +++ b/Runtime/Resources/ThoMagicRenderer.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 678dff042fe9c004cb7522c2233af44d +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/SceneRenderSettings.cs b/Runtime/SceneRenderSettings.cs new file mode 100644 index 0000000..50a0046 --- /dev/null +++ b/Runtime/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/Runtime/SceneRenderSettings.cs.meta b/Runtime/SceneRenderSettings.cs.meta new file mode 100644 index 0000000..c696fc0 --- /dev/null +++ b/Runtime/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/Runtime/TerrainDetailRenderer.cs b/Runtime/TerrainDetailRenderer.cs new file mode 100644 index 0000000..d1347bd --- /dev/null +++ b/Runtime/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/Runtime/TerrainDetailRenderer.cs.meta b/Runtime/TerrainDetailRenderer.cs.meta new file mode 100644 index 0000000..004d510 --- /dev/null +++ b/Runtime/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/Runtime/ThoMagicRendererObjectSettings.cs b/Runtime/ThoMagicRendererObjectSettings.cs new file mode 100644 index 0000000..b79c772 --- /dev/null +++ b/Runtime/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/Runtime/ThoMagicRendererObjectSettings.cs.meta b/Runtime/ThoMagicRendererObjectSettings.cs.meta new file mode 100644 index 0000000..44f96db --- /dev/null +++ b/Runtime/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/Runtime/TileObjectRenderer.cs b/Runtime/TileObjectRenderer.cs new file mode 100644 index 0000000..5fe2f59 --- /dev/null +++ b/Runtime/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/Runtime/TileObjectRenderer.cs.meta b/Runtime/TileObjectRenderer.cs.meta new file mode 100644 index 0000000..2b5fd3b --- /dev/null +++ b/Runtime/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/Runtime/TileObjectStreamer.cs b/Runtime/TileObjectStreamer.cs new file mode 100644 index 0000000..382b1e5 --- /dev/null +++ b/Runtime/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/Runtime/TileObjectStreamer.cs.meta b/Runtime/TileObjectStreamer.cs.meta new file mode 100644 index 0000000..74bddf7 --- /dev/null +++ b/Runtime/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/Runtime/TreeRenderer.cs b/Runtime/TreeRenderer.cs new file mode 100644 index 0000000..534f74f --- /dev/null +++ b/Runtime/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/Runtime/TreeRenderer.cs.meta b/Runtime/TreeRenderer.cs.meta new file mode 100644 index 0000000..72b0adf --- /dev/null +++ b/Runtime/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/Runtime/TreeStreamer.cs b/Runtime/TreeStreamer.cs new file mode 100644 index 0000000..2d9100c --- /dev/null +++ b/Runtime/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/Runtime/TreeStreamer.cs.meta b/Runtime/TreeStreamer.cs.meta new file mode 100644 index 0000000..aab0238 --- /dev/null +++ b/Runtime/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/readme.md.meta b/readme.md.meta new file mode 100644 index 0000000..01d2f38 --- /dev/null +++ b/readme.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 837e822b4df87d1499c0b28ad5c40ddb +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: