UnityGame/Library/PackageCache/com.unity.render-pipelines.universal/Runtime/ReflectionProbeManager.cs

340 lines
16 KiB
C#
Raw Normal View History

2024-10-27 10:53:47 +03:00
using System;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine.Experimental.Rendering;
namespace UnityEngine.Rendering.Universal
{
struct ReflectionProbeManager : IDisposable
{
int2 m_Resolution;
RenderTexture m_AtlasTexture0;
RenderTexture m_AtlasTexture1;
RTHandle m_AtlasTexture0Handle;
BuddyAllocator m_AtlasAllocator;
Dictionary<int, CachedProbe> m_Cache;
Dictionary<int, int> m_WarningCache;
List<int> m_NeedsUpdate;
List<int> m_NeedsRemove;
// Pre-allocated arrays for filling constant buffers
Vector4[] m_BoxMax;
Vector4[] m_BoxMin;
Vector4[] m_ProbePosition;
Vector4[] m_MipScaleOffset;
// There is a global max of 7 mips in Unity.
const int k_MaxMipCount = 7;
const string k_ReflectionProbeAtlasName = "URP Reflection Probe Atlas";
unsafe struct CachedProbe
{
public uint updateCount;
public Hash128 imageContentsHash;
public int size;
public int mipCount;
// One for each mip.
public fixed int dataIndices[k_MaxMipCount];
public fixed int levels[k_MaxMipCount];
public Texture texture;
public int lastUsed;
public Vector4 hdrData;
}
static class ShaderProperties
{
public static readonly int BoxMin = Shader.PropertyToID("urp_ReflProbes_BoxMin");
public static readonly int BoxMax = Shader.PropertyToID("urp_ReflProbes_BoxMax");
public static readonly int ProbePosition = Shader.PropertyToID("urp_ReflProbes_ProbePosition");
public static readonly int MipScaleOffset = Shader.PropertyToID("urp_ReflProbes_MipScaleOffset");
public static readonly int Count = Shader.PropertyToID("urp_ReflProbes_Count");
public static readonly int Atlas = Shader.PropertyToID("urp_ReflProbes_Atlas");
}
public RenderTexture atlasRT => m_AtlasTexture0;
public RTHandle atlasRTHandle => m_AtlasTexture0Handle;
public static ReflectionProbeManager Create()
{
var instance = new ReflectionProbeManager();
instance.Init();
return instance;
}
void Init()
{
var maxProbes = UniversalRenderPipeline.maxVisibleReflectionProbes;
// m_Resolution = math.min((int)reflectionProbeResolution, SystemInfo.maxTextureSize);
m_Resolution = 1;
var format = GraphicsFormat.B10G11R11_UFloatPack32;
if (!SystemInfo.IsFormatSupported(format, GraphicsFormatUsage.Render)) { format = GraphicsFormat.R16G16B16A16_SFloat; }
m_AtlasTexture0 = new RenderTexture(new RenderTextureDescriptor
{
width = m_Resolution.x,
height = m_Resolution.y,
volumeDepth = 1,
dimension = TextureDimension.Tex2D,
graphicsFormat = format,
useMipMap = false,
msaaSamples = 1
});
m_AtlasTexture0.name = k_ReflectionProbeAtlasName;
m_AtlasTexture0.filterMode = FilterMode.Bilinear;
m_AtlasTexture0.hideFlags = HideFlags.HideAndDontSave;
m_AtlasTexture0.Create();
m_AtlasTexture0Handle = RTHandles.Alloc(m_AtlasTexture0, transferOwnership: true);
m_AtlasTexture1 = new RenderTexture(m_AtlasTexture0.descriptor);
m_AtlasTexture1.name = k_ReflectionProbeAtlasName;
m_AtlasTexture1.filterMode = FilterMode.Bilinear;
m_AtlasTexture1.hideFlags = HideFlags.HideAndDontSave;
// The smallest allocatable resolution we want is 4x4. We calculate the number of levels as:
// log2(max) - log2(4) = log2(max) - 2
m_AtlasAllocator = new BuddyAllocator(math.floorlog2(SystemInfo.maxTextureSize) - 2, 2);
m_Cache = new Dictionary<int, CachedProbe>(maxProbes);
m_WarningCache = new Dictionary<int, int>(maxProbes);
m_NeedsUpdate = new List<int>(maxProbes);
m_NeedsRemove = new List<int>(maxProbes);
m_BoxMax = new Vector4[maxProbes];
m_BoxMin = new Vector4[maxProbes];
m_ProbePosition = new Vector4[maxProbes];
m_MipScaleOffset = new Vector4[maxProbes * 7];
}
public unsafe void UpdateGpuData(CommandBuffer cmd, ref CullingResults cullResults)
{
var probes = cullResults.visibleReflectionProbes;
var probeCount = math.min(probes.Length, UniversalRenderPipeline.maxVisibleReflectionProbes);
var frameIndex = Time.renderedFrameCount;
// Populate list of probes we need to remove to avoid modifying dictionary while iterating.
foreach (var (id, cachedProbe) in m_Cache)
{
// Evict probe if not used for more than 1 frame, if the texture no longer exists, or if the size changed.
if (Math.Abs(cachedProbe.lastUsed - frameIndex) > 1 ||
!cachedProbe.texture ||
cachedProbe.size != cachedProbe.texture.width)
{
m_NeedsRemove.Add(id);
for (var i = 0; i < k_MaxMipCount; i++)
{
if (cachedProbe.dataIndices[i] != -1) m_AtlasAllocator.Free(new BuddyAllocation(cachedProbe.levels[i], cachedProbe.dataIndices[i]));
}
}
}
foreach (var probeIndex in m_NeedsRemove)
{
m_Cache.Remove(probeIndex);
}
m_NeedsRemove.Clear();
foreach (var (id, lastUsed) in m_WarningCache)
{
if (Math.Abs(lastUsed - frameIndex) > 1)
{
m_NeedsRemove.Add(id);
}
}
foreach (var probeIndex in m_NeedsRemove)
{
m_WarningCache.Remove(probeIndex);
}
m_NeedsRemove.Clear();
var showFullWarning = false;
var requiredAtlasSize = math.int2(0, 0);
for (var probeIndex = 0; probeIndex < probeCount; probeIndex++)
{
var probe = probes[probeIndex];
var texture = probe.texture;
var id = probe.reflectionProbe.GetInstanceID();
var wasCached = m_Cache.TryGetValue(id, out var cachedProbe);
if (!texture)
{
continue;
}
if (!wasCached)
{
cachedProbe.size = texture.width;
var mipCount = math.ceillog2(cachedProbe.size * 4) + 1;
var level = m_AtlasAllocator.levelCount + 2 - mipCount;
cachedProbe.mipCount = math.min(mipCount, k_MaxMipCount);
cachedProbe.texture = texture;
var mip = 0;
for (; mip < cachedProbe.mipCount; mip++)
{
// Clamp to maximum level. This is relevant for 64x64 and lower, which will have valid content
// in 1x1 mip. The octahedron size is double the face size, so that ends up at 2x2. Due to
// borders the final mip must be 4x4 as that leaves 2x2 texels for the octahedron.
var mipLevel = math.min(level + mip, m_AtlasAllocator.levelCount - 1);
if (!m_AtlasAllocator.TryAllocate(mipLevel, out var allocation)) break;
// We split up the allocation struct because C# cannot do struct fixed arrays :(
cachedProbe.levels[mip] = allocation.level;
cachedProbe.dataIndices[mip] = allocation.index;
var scaleOffset = (int4)(GetScaleOffset(mipLevel, allocation.index, true, false) * m_Resolution.xyxy);
requiredAtlasSize = math.max(requiredAtlasSize, scaleOffset.zw + scaleOffset.xy);
}
// Check if we ran out of space in the atlas.
if (mip < cachedProbe.mipCount)
{
if (!m_WarningCache.ContainsKey(id)) showFullWarning = true;
m_WarningCache[id] = frameIndex;
for (var i = 0; i < mip; i++) m_AtlasAllocator.Free(new BuddyAllocation(cachedProbe.levels[i], cachedProbe.dataIndices[i]));
for (var i = 0; i < k_MaxMipCount; i++) cachedProbe.dataIndices[i] = -1;
continue;
}
for (; mip < k_MaxMipCount; mip++)
{
cachedProbe.dataIndices[mip] = -1;
}
}
var needsUpdate = !wasCached || cachedProbe.updateCount != texture.updateCount;
#if UNITY_EDITOR
needsUpdate |= cachedProbe.imageContentsHash != texture.imageContentsHash;
#endif
needsUpdate |= cachedProbe.hdrData != probe.hdrData; // The probe needs update if the runtime intensity multiplier changes
if (needsUpdate)
{
cachedProbe.updateCount = texture.updateCount;
#if UNITY_EDITOR
cachedProbe.imageContentsHash = texture.imageContentsHash;
#endif
m_NeedsUpdate.Add(id);
}
// If the probe is set to be updated every frame, we assign the last used frame to -1 so it's evicted in next frame.
if (probe.reflectionProbe.refreshMode == ReflectionProbeRefreshMode.EveryFrame)
cachedProbe.lastUsed = -1;
else
cachedProbe.lastUsed = frameIndex;
cachedProbe.hdrData = probe.hdrData;
m_Cache[id] = cachedProbe;
}
// Grow the atlas if it's not big enough to contain the current allocations.
if (math.any(m_Resolution < requiredAtlasSize))
{
requiredAtlasSize = math.max(m_Resolution, math.ceilpow2(requiredAtlasSize));
var desc = m_AtlasTexture0.descriptor;
desc.width = requiredAtlasSize.x;
desc.height = requiredAtlasSize.y;
m_AtlasTexture1.width = requiredAtlasSize.x;
m_AtlasTexture1.height = requiredAtlasSize.y;
m_AtlasTexture1.Create();
if (m_AtlasTexture0.width != 1)
{
if (SystemInfo.copyTextureSupport != CopyTextureSupport.None)
{
Graphics.CopyTexture(m_AtlasTexture0, 0, 0, 0, 0, m_Resolution.x, m_Resolution.y, m_AtlasTexture1, 0, 0, 0, 0);
}
else
{
Graphics.Blit(m_AtlasTexture0, m_AtlasTexture1, (float2)m_Resolution / requiredAtlasSize, Vector2.zero);
}
}
m_AtlasTexture0.Release();
(m_AtlasTexture0, m_AtlasTexture1) = (m_AtlasTexture1, m_AtlasTexture0);
m_Resolution = requiredAtlasSize;
}
var skipCount = 0;
for (var probeIndex = 0; probeIndex < probeCount; probeIndex++)
{
var probe = probes[probeIndex];
var id = probe.reflectionProbe.GetInstanceID();
var dataIndex = probeIndex - skipCount;
if (!m_Cache.TryGetValue(id, out var cachedProbe) || !probe.texture)
{
skipCount++;
continue;
}
m_BoxMax[dataIndex] = new Vector4(probe.bounds.max.x, probe.bounds.max.y, probe.bounds.max.z, probe.blendDistance);
m_BoxMin[dataIndex] = new Vector4(probe.bounds.min.x, probe.bounds.min.y, probe.bounds.min.z, probe.importance);
m_ProbePosition[dataIndex] = new Vector4(probe.localToWorldMatrix.m03, probe.localToWorldMatrix.m13, probe.localToWorldMatrix.m23, (probe.isBoxProjection ? 1 : -1) * (cachedProbe.mipCount));
for (var i = 0; i < cachedProbe.mipCount; i++) m_MipScaleOffset[dataIndex * k_MaxMipCount + i] = GetScaleOffset(cachedProbe.levels[i], cachedProbe.dataIndices[i], false, false);
}
if (showFullWarning)
{
Debug.LogWarning("A number of reflection probes have been skipped due to the reflection probe atlas being full.\nTo fix this, you can decrease the number or resolution of probes.");
}
using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.UpdateReflectionProbeAtlas)))
{
cmd.SetRenderTarget(m_AtlasTexture0);
foreach (var probeId in m_NeedsUpdate)
{
var cachedProbe = m_Cache[probeId];
for (var mip = 0; mip < cachedProbe.mipCount; mip++)
{
var level = cachedProbe.levels[mip];
var dataIndex = cachedProbe.dataIndices[mip];
// If we need to y-flip we will instead flip the atlas since that is updated less frequent and then the lookup should be correct.
// By doing this we won't have to y-flip the lookup in the shader code.
var scaleBias = GetScaleOffset(level, dataIndex, true, !SystemInfo.graphicsUVStartsAtTop);
var sizeWithoutPadding = (1 << (m_AtlasAllocator.levelCount + 1 - level)) - 2;
Blitter.BlitCubeToOctahedral2DQuadWithPadding(cmd, cachedProbe.texture, new Vector2(sizeWithoutPadding, sizeWithoutPadding), scaleBias, mip, true, 2, cachedProbe.hdrData);
}
}
cmd.SetGlobalVectorArray(ShaderProperties.BoxMin, m_BoxMin);
cmd.SetGlobalVectorArray(ShaderProperties.BoxMax, m_BoxMax);
cmd.SetGlobalVectorArray(ShaderProperties.ProbePosition, m_ProbePosition);
cmd.SetGlobalVectorArray(ShaderProperties.MipScaleOffset, m_MipScaleOffset);
cmd.SetGlobalFloat(ShaderProperties.Count, probeCount - skipCount);
cmd.SetGlobalTexture(ShaderProperties.Atlas, m_AtlasTexture0);
}
m_NeedsUpdate.Clear();
}
float4 GetScaleOffset(int level, int dataIndex, bool includePadding, bool yflip)
{
// level = m_AtlasAllocator.levelCount + 2 - (log2(size) + 1) <=>
// log2(size) + 1 = m_AtlasAllocator.levelCount + 2 - level <=>
// log2(size) = m_AtlasAllocator.levelCount + 1 - level <=>
// size = 2^(m_AtlasAllocator.levelCount + 1 - level)
var size = (1 << (m_AtlasAllocator.levelCount + 1 - level));
var coordinate = SpaceFillingCurves.DecodeMorton2D((uint)dataIndex);
var scale = (size - (includePadding ? 0 : 2)) / ((float2)m_Resolution);
var bias = ((float2) coordinate * size + (includePadding ? 0 : 1)) / (m_Resolution);
if (yflip) bias.y = 1.0f - bias.y - scale.y;
return math.float4(scale, bias);
}
public void Dispose()
{
if (m_AtlasTexture0)
{
m_AtlasTexture0.Release();
m_AtlasTexture0Handle.Release();
}
Object.DestroyImmediate(m_AtlasTexture0);
Object.DestroyImmediate(m_AtlasTexture1);
this = default;
}
}
}