Lower adaptive exposure from Eve camera semantics
This commit is contained in:
parent
32ac89a7a4
commit
4d5257b8ca
13 changed files with 602 additions and 6 deletions
|
|
@ -0,0 +1,220 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.RenderGraphModule;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityAdaptiveExposureRenderer : MonoBehaviour
|
||||
{
|
||||
private const int HistogramBins = 128;
|
||||
private ComputeShader? _compute;
|
||||
private Material? _multiplyMaterial;
|
||||
private ComputeBuffer? _histogram;
|
||||
private readonly RenderTexture?[] _exposure = new RenderTexture?[2];
|
||||
private int _currentExposure = -1;
|
||||
private Camera? _camera;
|
||||
private AdaptiveExposurePass? _pass;
|
||||
|
||||
private float _lowPercent;
|
||||
private float _highPercent;
|
||||
private float _minimumEv;
|
||||
private float _maximumEv;
|
||||
private float _keyValue;
|
||||
private float _speedUp;
|
||||
private float _speedDown;
|
||||
private bool _progressive;
|
||||
|
||||
public long AppliedFrameCount { get; private set; }
|
||||
public bool IsConfigured { get; private set; }
|
||||
public static RenderPassEvent ExposureRenderPassEvent =>
|
||||
(RenderPassEvent)((int)RenderPassEvent.BeforeRenderingPostProcessing + 1);
|
||||
|
||||
public void Configure(
|
||||
float lowPercent,
|
||||
float highPercent,
|
||||
float minimumEv,
|
||||
float maximumEv,
|
||||
float keyValue,
|
||||
string adaptation,
|
||||
float speedUp,
|
||||
float speedDown)
|
||||
{
|
||||
_lowPercent = Mathf.Clamp(lowPercent, 1f, 98.99f);
|
||||
_highPercent = Mathf.Clamp(highPercent, _lowPercent + 0.01f, 99f);
|
||||
_minimumEv = Mathf.Min(minimumEv, maximumEv);
|
||||
_maximumEv = Mathf.Max(minimumEv, maximumEv);
|
||||
_keyValue = Mathf.Max(0f, keyValue);
|
||||
_speedUp = Mathf.Max(0f, speedUp);
|
||||
_speedDown = Mathf.Max(0f, speedDown);
|
||||
_progressive = string.Equals(adaptation, "progressive", StringComparison.Ordinal);
|
||||
IsConfigured = true;
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_camera = GetComponent<Camera>();
|
||||
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
|
||||
ReleaseResources();
|
||||
}
|
||||
|
||||
private void OnDestroy() => ReleaseResources();
|
||||
|
||||
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
|
||||
{
|
||||
if (!IsConfigured || _camera == null || camera != _camera) return;
|
||||
_pass ??= new AdaptiveExposurePass(this);
|
||||
camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_pass);
|
||||
}
|
||||
|
||||
private bool EnsureResources()
|
||||
{
|
||||
_compute ??= Resources.Load<ComputeShader>("EveUnity/AdaptiveExposure");
|
||||
if (_multiplyMaterial == null)
|
||||
{
|
||||
var shader = Shader.Find("Hidden/EveUnity/AdaptiveExposureMultiply");
|
||||
if (shader != null) _multiplyMaterial = new Material(shader) { hideFlags = HideFlags.HideAndDontSave };
|
||||
}
|
||||
_histogram ??= new ComputeBuffer(HistogramBins, sizeof(uint), ComputeBufferType.Structured);
|
||||
for (var index = 0; index < _exposure.Length; index++)
|
||||
{
|
||||
if (_exposure[index] != null && _exposure[index]!.IsCreated()) continue;
|
||||
_exposure[index] = new RenderTexture(1, 1, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear)
|
||||
{
|
||||
name = $"Eve Adaptive Exposure {index}",
|
||||
enableRandomWrite = true,
|
||||
filterMode = FilterMode.Point,
|
||||
wrapMode = TextureWrapMode.Clamp,
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
_exposure[index]!.Create();
|
||||
}
|
||||
return _compute != null && _multiplyMaterial != null && _histogram != null;
|
||||
}
|
||||
|
||||
private void Apply(UnsafeCommandBuffer commands, TextureHandle colorTarget, int width, int height)
|
||||
{
|
||||
if (!EnsureResources() || _compute == null || _histogram == null || _multiplyMaterial == null) return;
|
||||
|
||||
var clear = _compute.FindKernel("ClearHistogram");
|
||||
commands.SetComputeBufferParam(_compute, clear, "_Histogram", _histogram);
|
||||
commands.DispatchCompute(_compute, clear, Mathf.CeilToInt(HistogramBins / 64f), 1, 1);
|
||||
|
||||
var build = _compute.FindKernel("BuildHistogram");
|
||||
commands.SetComputeBufferParam(_compute, build, "_Histogram", _histogram);
|
||||
commands.SetComputeTextureParam(_compute, build, "_Source", colorTarget);
|
||||
commands.SetComputeVectorParam(_compute, "_HistogramScaleOffsetResolution", new Vector4(
|
||||
1f / 18f,
|
||||
0.5f,
|
||||
Mathf.Max(1, width),
|
||||
Mathf.Max(1, height)));
|
||||
commands.DispatchCompute(
|
||||
_compute,
|
||||
build,
|
||||
Mathf.CeilToInt(width / 2f / 16f),
|
||||
Mathf.CeilToInt(height / 2f / 16f),
|
||||
1);
|
||||
|
||||
var destination = _currentExposure < 0 ? 0 : (_currentExposure + 1) % 2;
|
||||
var calculate = _compute.FindKernel(
|
||||
_currentExposure < 0 || !_progressive ? "CalculateFixedExposure" : "CalculateProgressiveExposure");
|
||||
commands.SetComputeBufferParam(_compute, calculate, "_Histogram", _histogram);
|
||||
commands.SetComputeVectorParam(_compute, "_ExposureFilterAndBrightness", new Vector4(
|
||||
_lowPercent * 0.01f,
|
||||
_highPercent * 0.01f,
|
||||
Mathf.Pow(2f, _minimumEv),
|
||||
Mathf.Pow(2f, _maximumEv)));
|
||||
commands.SetComputeVectorParam(_compute, "_ExposureAdaptation", new Vector4(
|
||||
_speedDown,
|
||||
_speedUp,
|
||||
_keyValue,
|
||||
Mathf.Max(0f, Time.deltaTime)));
|
||||
if (_currentExposure >= 0)
|
||||
commands.SetComputeTextureParam(_compute, calculate, "_PreviousExposure", _exposure[_currentExposure]!);
|
||||
commands.SetComputeTextureParam(_compute, calculate, "_DestinationExposure", _exposure[destination]!);
|
||||
commands.DispatchCompute(_compute, calculate, 1, 1, 1);
|
||||
|
||||
_currentExposure = destination;
|
||||
_multiplyMaterial.SetTexture("_ExposureTexture", _exposure[_currentExposure]);
|
||||
commands.SetRenderTarget(colorTarget);
|
||||
commands.DrawProcedural(Matrix4x4.identity, _multiplyMaterial, 0, MeshTopology.Triangles, 3, 1);
|
||||
AppliedFrameCount++;
|
||||
}
|
||||
|
||||
internal static float ExposureForAverageLuminance(float averageLuminance, float keyValue) =>
|
||||
Mathf.Max(0f, keyValue) / Mathf.Max(1e-6f, averageLuminance);
|
||||
|
||||
internal static float AdaptExposure(float previous, float next, float deltaTime, float speedUp, float speedDown)
|
||||
{
|
||||
var speed = next > previous ? Mathf.Max(0f, speedDown) : Mathf.Max(0f, speedUp);
|
||||
return previous + (next - previous) * (1f - Mathf.Pow(2f, -Mathf.Max(0f, deltaTime) * speed));
|
||||
}
|
||||
|
||||
private void ReleaseResources()
|
||||
{
|
||||
_histogram?.Release();
|
||||
_histogram = null;
|
||||
foreach (var texture in _exposure)
|
||||
{
|
||||
if (texture == null) continue;
|
||||
texture.Release();
|
||||
if (Application.isPlaying) Destroy(texture); else DestroyImmediate(texture);
|
||||
}
|
||||
_exposure[0] = null;
|
||||
_exposure[1] = null;
|
||||
_currentExposure = -1;
|
||||
if (_multiplyMaterial != null)
|
||||
{
|
||||
if (Application.isPlaying) Destroy(_multiplyMaterial); else DestroyImmediate(_multiplyMaterial);
|
||||
_multiplyMaterial = null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AdaptiveExposurePass : ScriptableRenderPass
|
||||
{
|
||||
private readonly EveUnityAdaptiveExposureRenderer _owner;
|
||||
|
||||
private sealed class PassData
|
||||
{
|
||||
internal EveUnityAdaptiveExposureRenderer Owner = null!;
|
||||
internal TextureHandle ColorTarget;
|
||||
internal int Width;
|
||||
internal int Height;
|
||||
}
|
||||
|
||||
internal AdaptiveExposurePass(EveUnityAdaptiveExposureRenderer owner)
|
||||
{
|
||||
_owner = owner;
|
||||
renderPassEvent = ExposureRenderPassEvent;
|
||||
}
|
||||
|
||||
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
|
||||
{
|
||||
var cameraData = frameData.Get<UniversalCameraData>();
|
||||
if (cameraData.camera == null || cameraData.camera != _owner._camera) return;
|
||||
var resourceData = frameData.Get<UniversalResourceData>();
|
||||
var colorTarget = resourceData.activeColorTexture;
|
||||
if (!colorTarget.IsValid()) return;
|
||||
|
||||
using var builder = renderGraph.AddUnsafePass<PassData>("Eve Adaptive Exposure", out var passData);
|
||||
passData.Owner = _owner;
|
||||
passData.ColorTarget = colorTarget;
|
||||
passData.Width = cameraData.camera.pixelWidth;
|
||||
passData.Height = cameraData.camera.pixelHeight;
|
||||
builder.UseTexture(passData.ColorTarget, AccessFlags.ReadWrite);
|
||||
builder.AllowPassCulling(false);
|
||||
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) =>
|
||||
data.Owner.Apply(context.cmd, data.ColorTarget, data.Width, data.Height));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3cb42e7da8b34febb348f01a598f2eea
|
||||
|
|
@ -38,6 +38,7 @@ namespace GameCult.Eve.UnityScene
|
|||
private GameObject? _postProcessVolumeObject;
|
||||
private Volume? _postProcessVolume;
|
||||
private UniversalAdditionalCameraData? _postProcessCameraData;
|
||||
private EveUnityAdaptiveExposureRenderer? _adaptiveExposure;
|
||||
private bool _createdPostProcessCameraData;
|
||||
private bool _previousRenderPostProcessing;
|
||||
private AntialiasingMode _previousAntialiasing;
|
||||
|
|
@ -477,7 +478,11 @@ namespace GameCult.Eve.UnityScene
|
|||
world.CameraReconstruction,
|
||||
"temporal-reprojection.v1",
|
||||
System.StringComparison.Ordinal);
|
||||
if (profile == null && !usesTemporalReprojection)
|
||||
var usesAdaptiveExposure = string.Equals(
|
||||
world.ExposureMode,
|
||||
"histogram.v1",
|
||||
System.StringComparison.Ordinal);
|
||||
if (profile == null && !usesTemporalReprojection && !usesAdaptiveExposure)
|
||||
{
|
||||
RestorePostProcess();
|
||||
return;
|
||||
|
|
@ -530,6 +535,25 @@ namespace GameCult.Eve.UnityScene
|
|||
{
|
||||
_postProcessVolumeObject.SetActive(false);
|
||||
}
|
||||
|
||||
if (usesAdaptiveExposure)
|
||||
{
|
||||
if (_adaptiveExposure == null)
|
||||
_adaptiveExposure = camera.gameObject.AddComponent<EveUnityAdaptiveExposureRenderer>();
|
||||
_adaptiveExposure.Configure(
|
||||
world.ExposureLowPercent,
|
||||
world.ExposureHighPercent,
|
||||
world.ExposureMinimumEv,
|
||||
world.ExposureMaximumEv,
|
||||
world.ExposureKeyValue,
|
||||
world.ExposureAdaptation,
|
||||
world.ExposureSpeedUp,
|
||||
world.ExposureSpeedDown);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReleaseAdaptiveExposure();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTemporalQuality(string quality) =>
|
||||
|
|
@ -550,6 +574,7 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
private void RestorePostProcess()
|
||||
{
|
||||
ReleaseAdaptiveExposure();
|
||||
if (_postProcessCameraData != null)
|
||||
{
|
||||
if (_createdPostProcessCameraData)
|
||||
|
|
@ -579,6 +604,16 @@ namespace GameCult.Eve.UnityScene
|
|||
_postProcessVolume = null;
|
||||
}
|
||||
|
||||
private void ReleaseAdaptiveExposure()
|
||||
{
|
||||
if (_adaptiveExposure == null) return;
|
||||
if (Application.isPlaying)
|
||||
Destroy(_adaptiveExposure);
|
||||
else
|
||||
DestroyImmediate(_adaptiveExposure);
|
||||
_adaptiveExposure = null;
|
||||
}
|
||||
|
||||
private void RestoreAmbientEnvironment()
|
||||
{
|
||||
ReleaseKeyLight();
|
||||
|
|
|
|||
|
|
@ -161,7 +161,16 @@ namespace GameCult.Eve.UnityScene
|
|||
worldRoot.GetProp("temporalQuality", "high"),
|
||||
ParseFloat(worldRoot.GetProp("temporalHistoryBlend"), 0f),
|
||||
ParseFloat(worldRoot.GetProp("temporalJitterScale"), 0f),
|
||||
ParseFloat(worldRoot.GetProp("temporalSharpening"), 0f));
|
||||
ParseFloat(worldRoot.GetProp("temporalSharpening"), 0f),
|
||||
worldRoot.GetProp("exposureMode"),
|
||||
ParseFloat(worldRoot.GetProp("exposureLowPercent"), 50f),
|
||||
ParseFloat(worldRoot.GetProp("exposureHighPercent"), 95f),
|
||||
ParseFloat(worldRoot.GetProp("exposureMinimumEv"), 0f),
|
||||
ParseFloat(worldRoot.GetProp("exposureMaximumEv"), 0f),
|
||||
ParseFloat(worldRoot.GetProp("exposureKeyValue"), 1f),
|
||||
worldRoot.GetProp("exposureAdaptation", "progressive"),
|
||||
ParseFloat(worldRoot.GetProp("exposureSpeedUp"), 2f),
|
||||
ParseFloat(worldRoot.GetProp("exposureSpeedDown"), 1f));
|
||||
}
|
||||
|
||||
private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) =>
|
||||
|
|
@ -421,7 +430,16 @@ namespace GameCult.Eve.UnityScene
|
|||
string temporalQuality = "high",
|
||||
float temporalHistoryBlend = 0f,
|
||||
float temporalJitterScale = 0f,
|
||||
float temporalSharpening = 0f)
|
||||
float temporalSharpening = 0f,
|
||||
string exposureMode = "",
|
||||
float exposureLowPercent = 50f,
|
||||
float exposureHighPercent = 95f,
|
||||
float exposureMinimumEv = 0f,
|
||||
float exposureMaximumEv = 0f,
|
||||
float exposureKeyValue = 1f,
|
||||
string exposureAdaptation = "progressive",
|
||||
float exposureSpeedUp = 2f,
|
||||
float exposureSpeedDown = 1f)
|
||||
{
|
||||
WorldRootId = worldRootId ?? "";
|
||||
StatePointerId = statePointerId ?? "";
|
||||
|
|
@ -474,6 +492,15 @@ namespace GameCult.Eve.UnityScene
|
|||
TemporalHistoryBlend = temporalHistoryBlend;
|
||||
TemporalJitterScale = temporalJitterScale;
|
||||
TemporalSharpening = temporalSharpening;
|
||||
ExposureMode = exposureMode ?? "";
|
||||
ExposureLowPercent = exposureLowPercent;
|
||||
ExposureHighPercent = exposureHighPercent;
|
||||
ExposureMinimumEv = exposureMinimumEv;
|
||||
ExposureMaximumEv = exposureMaximumEv;
|
||||
ExposureKeyValue = exposureKeyValue;
|
||||
ExposureAdaptation = exposureAdaptation ?? "";
|
||||
ExposureSpeedUp = exposureSpeedUp;
|
||||
ExposureSpeedDown = exposureSpeedDown;
|
||||
}
|
||||
|
||||
public string WorldRootId { get; }
|
||||
|
|
@ -508,6 +535,24 @@ namespace GameCult.Eve.UnityScene
|
|||
|
||||
public float TemporalSharpening { get; }
|
||||
|
||||
public string ExposureMode { get; }
|
||||
|
||||
public float ExposureLowPercent { get; }
|
||||
|
||||
public float ExposureHighPercent { get; }
|
||||
|
||||
public float ExposureMinimumEv { get; }
|
||||
|
||||
public float ExposureMaximumEv { get; }
|
||||
|
||||
public float ExposureKeyValue { get; }
|
||||
|
||||
public string ExposureAdaptation { get; }
|
||||
|
||||
public float ExposureSpeedUp { get; }
|
||||
|
||||
public float ExposureSpeedDown { get; }
|
||||
|
||||
public float CameraDistance { get; }
|
||||
|
||||
public float CameraVerticalFieldOfViewDegrees { get; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 63a640b4e19e5764eb030d665a2b7c00
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6435ff06293834c49aa631e74b51a552
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
#pragma kernel ClearHistogram
|
||||
#pragma kernel BuildHistogram
|
||||
#pragma kernel CalculateFixedExposure
|
||||
#pragma kernel CalculateProgressiveExposure
|
||||
|
||||
RWStructuredBuffer<uint> _Histogram;
|
||||
Texture2D<float4> _Source;
|
||||
SamplerState sampler_LinearClamp;
|
||||
Texture2D<float> _PreviousExposure;
|
||||
RWTexture2D<float> _DestinationExposure;
|
||||
|
||||
float4 _HistogramScaleOffsetResolution;
|
||||
float4 _ExposureFilterAndBrightness;
|
||||
float4 _ExposureAdaptation;
|
||||
|
||||
groupshared uint GroupHistogram[128];
|
||||
|
||||
[numthreads(64, 1, 1)]
|
||||
void ClearHistogram(uint threadId : SV_DispatchThreadID)
|
||||
{
|
||||
if (threadId < 128) _Histogram[threadId] = 0;
|
||||
}
|
||||
|
||||
[numthreads(16, 16, 1)]
|
||||
void BuildHistogram(uint2 dispatchId : SV_DispatchThreadID, uint2 groupId : SV_GroupThreadID)
|
||||
{
|
||||
uint localId = groupId.y * 16 + groupId.x;
|
||||
if (localId < 128) GroupHistogram[localId] = 0;
|
||||
GroupMemoryBarrierWithGroupSync();
|
||||
|
||||
float2 pixel = float2(dispatchId) * 2.0;
|
||||
if (pixel.x < _HistogramScaleOffsetResolution.z && pixel.y < _HistogramScaleOffsetResolution.w)
|
||||
{
|
||||
float2 uv = pixel / _HistogramScaleOffsetResolution.zw;
|
||||
float2 distanceFromCenter = abs(uv - 0.5.xx);
|
||||
float vignette = saturate(1.0 - dot(distanceFromCenter, distanceFromCenter));
|
||||
uint weight = (uint)(64.0 * vignette * vignette);
|
||||
float3 color = _Source.SampleLevel(sampler_LinearClamp, uv, 0).rgb;
|
||||
float luminance = max(1e-6, dot(color, float3(0.2126729, 0.7151522, 0.0721750)));
|
||||
float normalizedLogLuminance = saturate(
|
||||
log2(luminance) * _HistogramScaleOffsetResolution.x + _HistogramScaleOffsetResolution.y);
|
||||
uint bin = (uint)(normalizedLogLuminance * 127.0);
|
||||
InterlockedAdd(GroupHistogram[bin], weight);
|
||||
}
|
||||
|
||||
GroupMemoryBarrierWithGroupSync();
|
||||
if (localId < 128) InterlockedAdd(_Histogram[localId], GroupHistogram[localId]);
|
||||
}
|
||||
|
||||
float LuminanceForBin(uint bin)
|
||||
{
|
||||
float normalized = (float)bin / 128.0;
|
||||
return exp2((normalized - _HistogramScaleOffsetResolution.y) / _HistogramScaleOffsetResolution.x);
|
||||
}
|
||||
|
||||
float CalculateExposure()
|
||||
{
|
||||
float total = 0.0;
|
||||
for (uint index = 0; index < 128; index++) total += (float)_Histogram[index];
|
||||
|
||||
float discardDark = total * _ExposureFilterAndBrightness.x;
|
||||
float includeThrough = total * _ExposureFilterAndBrightness.y;
|
||||
float weightedLuminance = 0.0;
|
||||
float includedWeight = 0.0;
|
||||
for (uint index = 0; index < 128; index++)
|
||||
{
|
||||
float count = (float)_Histogram[index];
|
||||
float darkOffset = min(discardDark, count);
|
||||
count -= darkOffset;
|
||||
discardDark -= darkOffset;
|
||||
includeThrough -= darkOffset;
|
||||
count = min(includeThrough, count);
|
||||
includeThrough -= count;
|
||||
weightedLuminance += LuminanceForBin(index) * count;
|
||||
includedWeight += count;
|
||||
}
|
||||
|
||||
float average = weightedLuminance / max(includedWeight, 1e-6);
|
||||
average = clamp(average, _ExposureFilterAndBrightness.z, _ExposureFilterAndBrightness.w);
|
||||
return _ExposureAdaptation.z / max(average, 1e-6);
|
||||
}
|
||||
|
||||
[numthreads(1, 1, 1)]
|
||||
void CalculateFixedExposure(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
_DestinationExposure[uint2(0, 0)] = CalculateExposure();
|
||||
}
|
||||
|
||||
[numthreads(1, 1, 1)]
|
||||
void CalculateProgressiveExposure(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
float nextExposure = CalculateExposure();
|
||||
float previousExposure = _PreviousExposure[uint2(0, 0)];
|
||||
float delta = nextExposure - previousExposure;
|
||||
float speed = delta > 0.0 ? _ExposureAdaptation.x : _ExposureAdaptation.y;
|
||||
_DestinationExposure[uint2(0, 0)] = previousExposure +
|
||||
delta * (1.0 - exp2(-_ExposureAdaptation.w * speed));
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c8c45489b6ba44558342ebc5381c12d1
|
||||
ComputeShaderImporter:
|
||||
externalObjects: {}
|
||||
currentAPIMask: 4
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
Shader "Hidden/EveUnity/AdaptiveExposureMultiply"
|
||||
{
|
||||
SubShader
|
||||
{
|
||||
Pass
|
||||
{
|
||||
Cull Off ZWrite Off ZTest Always
|
||||
Blend DstColor Zero
|
||||
ColorMask RGB
|
||||
|
||||
HLSLPROGRAM
|
||||
#pragma target 4.5
|
||||
#pragma vertex Vert
|
||||
#pragma fragment Frag
|
||||
|
||||
Texture2D<float> _ExposureTexture;
|
||||
|
||||
struct Varyings
|
||||
{
|
||||
float4 position : SV_POSITION;
|
||||
};
|
||||
|
||||
Varyings Vert(uint vertexId : SV_VertexID)
|
||||
{
|
||||
float2 uv = float2((vertexId << 1) & 2, vertexId & 2);
|
||||
Varyings output;
|
||||
output.position = float4(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||
return output;
|
||||
}
|
||||
|
||||
float4 Frag(Varyings input) : SV_Target
|
||||
{
|
||||
float exposure = _ExposureTexture.Load(int3(0, 0, 0));
|
||||
return exposure.xxxx;
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 48315c6017f548e7a4b46698b3d7229e
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -42,6 +42,89 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AdaptiveExposureMatchesFossilCompensationAndProgressiveDirection()
|
||||
{
|
||||
var exposureMethod = typeof(EveUnityAdaptiveExposureRenderer).GetMethod(
|
||||
"ExposureForAverageLuminance",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
var adaptationMethod = typeof(EveUnityAdaptiveExposureRenderer).GetMethod(
|
||||
"AdaptExposure",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.That(exposureMethod, Is.Not.Null);
|
||||
Assert.That(adaptationMethod, Is.Not.Null);
|
||||
var brightBound = Mathf.Pow(2f, 0.3f);
|
||||
var exposure = (float)exposureMethod!.Invoke(null, new object[] { brightBound, 0.5f });
|
||||
Assert.That(exposure, Is.EqualTo(0.5f / brightBound).Within(0.000001f));
|
||||
|
||||
var darkening = (float)adaptationMethod!.Invoke(
|
||||
null,
|
||||
new object[] { 1f, 0.25f, 0.5f, 2f, 1f });
|
||||
var brightening = (float)adaptationMethod.Invoke(
|
||||
null,
|
||||
new object[] { 1f, 4f, 0.5f, 2f, 1f });
|
||||
Assert.That(darkening, Is.EqualTo(0.625f).Within(0.000001f));
|
||||
Assert.That(brightening, Is.EqualTo(1f + 3f * (1f - Mathf.Pow(2f, -0.5f))).Within(0.000001f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AdaptiveExposureGpuHistogramClampsBrightFrameToAdvertisedMaximumEv()
|
||||
{
|
||||
Assert.That(SystemInfo.supportsComputeShaders, Is.True);
|
||||
var compute = Resources.Load<ComputeShader>("EveUnity/AdaptiveExposure");
|
||||
Assert.That(compute, Is.Not.Null);
|
||||
var source = new Texture2D(32, 32, TextureFormat.RGBAFloat, false, true);
|
||||
var pixels = Enumerable.Repeat(new Color(16f, 16f, 16f, 1f), 32 * 32).ToArray();
|
||||
source.SetPixels(pixels);
|
||||
source.Apply(false, false);
|
||||
using var histogram = new ComputeBuffer(128, sizeof(uint), ComputeBufferType.Structured);
|
||||
var exposure = new RenderTexture(1, 1, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear)
|
||||
{
|
||||
enableRandomWrite = true
|
||||
};
|
||||
exposure.Create();
|
||||
try
|
||||
{
|
||||
var clear = compute!.FindKernel("ClearHistogram");
|
||||
compute.SetBuffer(clear, "_Histogram", histogram);
|
||||
compute.Dispatch(clear, 2, 1, 1);
|
||||
|
||||
var build = compute.FindKernel("BuildHistogram");
|
||||
compute.SetBuffer(build, "_Histogram", histogram);
|
||||
compute.SetTexture(build, "_Source", source);
|
||||
compute.SetVector("_HistogramScaleOffsetResolution", new Vector4(1f / 18f, 0.5f, 32f, 32f));
|
||||
compute.Dispatch(build, 1, 1, 1);
|
||||
|
||||
var calculate = compute.FindKernel("CalculateFixedExposure");
|
||||
compute.SetBuffer(calculate, "_Histogram", histogram);
|
||||
compute.SetVector("_ExposureFilterAndBrightness", new Vector4(
|
||||
0.4737294f,
|
||||
0.99f,
|
||||
Mathf.Pow(2f, -3f),
|
||||
Mathf.Pow(2f, 0.3f)));
|
||||
compute.SetVector("_ExposureAdaptation", new Vector4(1f, 2f, 0.5f, 1f / 60f));
|
||||
compute.SetTexture(calculate, "_DestinationExposure", exposure);
|
||||
compute.Dispatch(calculate, 1, 1, 1);
|
||||
|
||||
var previous = RenderTexture.active;
|
||||
RenderTexture.active = exposure;
|
||||
var readback = new Texture2D(1, 1, TextureFormat.RFloat, false, true);
|
||||
readback.ReadPixels(new Rect(0, 0, 1, 1), 0, 0);
|
||||
readback.Apply();
|
||||
RenderTexture.active = previous;
|
||||
var actual = readback.GetPixel(0, 0).r;
|
||||
UnityEngine.Object.DestroyImmediate(readback);
|
||||
Assert.That(actual, Is.EqualTo(0.5f / Mathf.Pow(2f, 0.3f)).Within(0.001f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exposure.Release();
|
||||
UnityEngine.Object.DestroyImmediate(exposure);
|
||||
UnityEngine.Object.DestroyImmediate(source);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RasterizerConsumesPluginOwnedDocumentContract()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,16 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
temporalQuality: "high",
|
||||
temporalHistoryBlend: "0.99",
|
||||
temporalJitterScale: "0.1",
|
||||
temporalSharpening: "0"),
|
||||
temporalSharpening: "0",
|
||||
exposureMode: "histogram.v1",
|
||||
exposureLowPercent: "47.37294",
|
||||
exposureHighPercent: "99",
|
||||
exposureMinimumEv: "-3",
|
||||
exposureMaximumEv: "0.3",
|
||||
exposureKeyValue: "0.5",
|
||||
exposureAdaptation: "progressive",
|
||||
exposureSpeedUp: "2",
|
||||
exposureSpeedDown: "1"),
|
||||
Advertisement("aetheria.daemon.game"),
|
||||
"cultmesh://aetheria/eve/surfaces/aetheria.daemon.game",
|
||||
1),
|
||||
|
|
@ -1172,6 +1181,17 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
host.Configure(rootObject.transform, provider, provider, provider, provider, nativeAssets);
|
||||
host.Connect();
|
||||
hostObject.SetActive(true);
|
||||
var exposedWorld = host.ActiveProjection?.PlayableWorld;
|
||||
Assert.That(exposedWorld, Is.Not.Null);
|
||||
Assert.That(exposedWorld!.ExposureMode, Is.EqualTo("histogram.v1"));
|
||||
Assert.That(exposedWorld.ExposureLowPercent, Is.EqualTo(47.37294f).Within(0.00001f));
|
||||
Assert.That(exposedWorld.ExposureHighPercent, Is.EqualTo(99f));
|
||||
Assert.That(exposedWorld.ExposureMinimumEv, Is.EqualTo(-3f));
|
||||
Assert.That(exposedWorld.ExposureMaximumEv, Is.EqualTo(0.3f).Within(0.00001f));
|
||||
Assert.That(exposedWorld.ExposureKeyValue, Is.EqualTo(0.5f));
|
||||
Assert.That(exposedWorld.ExposureAdaptation, Is.EqualTo("progressive"));
|
||||
Assert.That(exposedWorld.ExposureSpeedUp, Is.EqualTo(2f));
|
||||
Assert.That(exposedWorld.ExposureSpeedDown, Is.EqualTo(1f));
|
||||
|
||||
var rig = hostObject.AddComponent<EveUnityPlayableWorldCameraRig>();
|
||||
rig.Host = host;
|
||||
|
|
@ -1217,6 +1237,9 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
Assert.That(cameraData.taaSettings.baseBlendFactor, Is.EqualTo(0.99f).Within(0.0001f));
|
||||
Assert.That(cameraData.taaSettings.jitterScale, Is.EqualTo(0.1f).Within(0.0001f));
|
||||
Assert.That(cameraData.taaSettings.contrastAdaptiveSharpening, Is.Zero);
|
||||
var adaptiveExposure = camera.GetComponent<EveUnityAdaptiveExposureRenderer>();
|
||||
Assert.That(adaptiveExposure, Is.Not.Null);
|
||||
Assert.That(adaptiveExposure.IsConfigured, Is.True);
|
||||
var postProcessTransform = hostObject.transform.Find("Eve World Post Process");
|
||||
Assert.That(postProcessTransform, Is.Not.Null);
|
||||
Assert.That(postProcessTransform.GetComponent<Volume>().sharedProfile, Is.SameAs(postProcessProfile));
|
||||
|
|
@ -1910,7 +1933,16 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
string temporalQuality = "high",
|
||||
string temporalHistoryBlend = "0",
|
||||
string temporalJitterScale = "0",
|
||||
string temporalSharpening = "0")
|
||||
string temporalSharpening = "0",
|
||||
string exposureMode = "",
|
||||
string exposureLowPercent = "50",
|
||||
string exposureHighPercent = "95",
|
||||
string exposureMinimumEv = "0",
|
||||
string exposureMaximumEv = "0",
|
||||
string exposureKeyValue = "1",
|
||||
string exposureAdaptation = "progressive",
|
||||
string exposureSpeedUp = "2",
|
||||
string exposureSpeedDown = "1")
|
||||
{
|
||||
var playableChildren = new List<EveSurfaceComponent>
|
||||
{
|
||||
|
|
@ -2083,6 +2115,15 @@ namespace GameCult.Eve.UnityScene.Tests
|
|||
["temporalHistoryBlend"] = temporalHistoryBlend,
|
||||
["temporalJitterScale"] = temporalJitterScale,
|
||||
["temporalSharpening"] = temporalSharpening,
|
||||
["exposureMode"] = exposureMode,
|
||||
["exposureLowPercent"] = exposureLowPercent,
|
||||
["exposureHighPercent"] = exposureHighPercent,
|
||||
["exposureMinimumEv"] = exposureMinimumEv,
|
||||
["exposureMaximumEv"] = exposureMaximumEv,
|
||||
["exposureKeyValue"] = exposureKeyValue,
|
||||
["exposureAdaptation"] = exposureAdaptation,
|
||||
["exposureSpeedUp"] = exposureSpeedUp,
|
||||
["exposureSpeedDown"] = exposureSpeedDown,
|
||||
["keyLightDirection"] = "0.4,-1,0.25",
|
||||
["keyLightColor"] = "1,0.95,0.9",
|
||||
["keyLightIntensity"] = "0.75",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.3.58",
|
||||
"version": "0.3.59",
|
||||
"unity": "6000.0",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
|
|
|
|||
Reference in a new issue