Lower adaptive exposure from Eve camera semantics

This commit is contained in:
Meta Vens 2026-07-16 19:51:25 +01:00
commit 4d5257b8ca
13 changed files with 602 additions and 6 deletions

View file

@ -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));
}
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3cb42e7da8b34febb348f01a598f2eea

View file

@ -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();

View file

@ -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; }

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 63a640b4e19e5764eb030d665a2b7c00
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6435ff06293834c49aa631e74b51a552
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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));
}

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c8c45489b6ba44558342ebc5381c12d1
ComputeShaderImporter:
externalObjects: {}
currentAPIMask: 4
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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
}
}
}

View file

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 48315c6017f548e7a4b46698b3d7229e
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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()
{

View file

@ -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",

View file

@ -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": {