using System; using System.IO; using System.Linq; using System.Collections.Generic; using System.Reflection; using GameCult.Eve.PluginFields; using GameCult.Eve.UnityScene.Fields; using NUnit.Framework; using UnityEngine; using UnityEngine.Rendering.Universal; namespace GameCult.Eve.UnityScene.Tests { public sealed class EveFieldsUnitySceneProjectionTests { [Test] public void VolumeCameraToWorldPreservesUnityShaderPositiveZForward() { var cameraObject = new GameObject("eve-volume-camera-matrix"); try { var camera = cameraObject.AddComponent(); camera.transform.SetPositionAndRotation( new Vector3(12f, -3f, 8f), Quaternion.Euler(17f, 43f, -9f)); var method = typeof(EveUnityFieldsVolumeRenderer).GetMethod( "CameraTransformToWorld", BindingFlags.NonPublic | BindingFlags.Static); Assert.That(method, Is.Not.Null); var matrix = (Matrix4x4)method!.Invoke(null, new object[] { camera }); Assert.That(Vector3.Dot( matrix.MultiplyVector(Vector3.forward).normalized, camera.transform.forward), Is.EqualTo(1f).Within(0.00001f)); Assert.That(Vector3.Distance( matrix.MultiplyPoint3x4(Vector3.zero), camera.transform.position), Is.LessThan(0.00001f)); } finally { UnityEngine.Object.DestroyImmediate(cameraObject); } } [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("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() { var render = typeof(EveFieldsSplatRasterizer).GetMethod( "Render", new[] { typeof(IEveFieldsSplatsDocument), typeof(int), typeof(int) }); Assert.That(render, Is.Not.Null); Assert.That(typeof(EveFieldsSplatRasterizer).Assembly.GetReferencedAssemblies().Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); } [Test] public void CanonicalFieldsPackageOwnsTheCultMeshWireDocument() { Assert.That(typeof(EveFieldsSplatsDocument).GetCustomAttributes(false) .Any(attribute => attribute.GetType().Name == "CultDocumentAttribute"), Is.True); Assert.That(new EveFieldsSplatsDocument().Schema, Is.EqualTo(EveFieldsSchemas.Splats)); Assert.That(typeof(EveFieldsSplatsDocument).Assembly.GetReferencedAssemblies() .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); } [Test] public void VolumeRendererConsumesCanonicalFieldsWithoutProviderCode() { Assert.That(typeof(EveUnityFieldsVolumeRenderer).Assembly.GetReferencedAssemblies() .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null); } [Test] public void UrpRendererFeatureOwnsGenericWorldEffectScheduling() { Assert.That(typeof(EveUnityRendererFeature).IsSubclassOf(typeof(ScriptableRendererFeature)), Is.True); var supportsCamera = typeof(EveUnityRendererFeature).GetMethod( "SupportsCamera", BindingFlags.NonPublic | BindingFlags.Static); Assert.That(supportsCamera, Is.Not.Null); Assert.That(supportsCamera!.Invoke(null, new object[] { CameraType.Game }), Is.True); Assert.That(supportsCamera.Invoke(null, new object[] { CameraType.SceneView }), Is.True); Assert.That(supportsCamera.Invoke(null, new object[] { CameraType.Preview }), Is.False); AssertRenderOrder(typeof(EveUnityFieldsVolumeRenderer), 100); AssertRenderOrder(typeof(EveUnityFieldsParticleRenderer), 200); AssertRenderOrder(typeof(EveUnityAdaptiveExposureRenderer), 300); } [Test] public void VolumeRendererCompositesBeforeProviderPostProcessing() { Assert.That( EveUnityFieldsVolumeRenderer.CompositionRenderPassEvent, Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); } private static void AssertRenderOrder(Type sourceType, int expected) { var schedulingInterface = sourceType.GetInterfaces().Single(type => string.Equals(type.Name, "IEveUnityRenderPassSource", StringComparison.Ordinal)); var property = schedulingInterface.GetProperty("RenderOrder"); Assert.That(property, Is.Not.Null); var gameObject = new GameObject("eve-render-pass-order-test"); try { var component = gameObject.AddComponent(sourceType); Assert.That(property!.GetValue(component), Is.EqualTo(expected)); } finally { UnityEngine.Object.DestroyImmediate(gameObject); } } [Test] public void ParticleRendererExecutesProviderProgramWithoutProviderCode() { Assert.That(typeof(EveUnityFieldsParticleRenderer).Assembly.GetReferencedAssemblies() .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); Assert.That(EveUnityFieldsParticleRenderer.CompositionRenderPassEvent, Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); } [TestCase(11f, 6f)] [TestCase(-11f, -6f)] [TestCase(5.999f, 0f)] public void FieldViewportFrameSnapsToAnIntegralGravityPixelMultiple(float coordinate, float expected) { Assert.That(EveUnityFieldsViewportFrame.SnapCameraCoordinate( coordinate, 1536f, 2048, 8f), Is.EqualTo(expected).Within(0.0001f)); } [Test] public void FieldViewportFrameAcceptsAlignedGravityAndParticleLattices() { Assert.That(EveUnityFieldsViewportFrame.TryValidateSpatialLattice( 1536f, 1536f, 2048, 2048, 256, 6f, 8), Is.True); } [Test] public void FieldViewportFrameRejectsAliasedGravityAndParticleLattices() { Assert.That(EveUnityFieldsViewportFrame.TryValidateSpatialLattice( 2000f, 2000f, 2048, 2048, 256, 6f, 8), Is.False); } [Test] public void FogAndParticlesResolveTheSameSnappedWorldFrame() { var document = new EveFieldsSplatsDocument { FrameId = 7, Viewport = new EveFieldsViewport { MinX = -768, MinY = -768, MaxX = 768, MaxY = 768 } }; var contract = new Dictionary(StringComparer.Ordinal) { ["viewportAnchor"] = "active-camera.xz", ["span"] = "256", ["cellWorldSize"] = "6", ["viewportSnapTexels"] = "8" }; Assert.That(EveUnityFieldsViewportFrame.TryResolve( document, contract, new Vector2(11f, -11f), 2048, 2048, out var fog, out var fogCenter), Is.True); Assert.That(EveUnityFieldsViewportFrame.TryResolve( document, contract, new Vector2(11f, -11f), 2048, 2048, out var particles, out var particleCenter), Is.True); Assert.That(fogCenter, Is.EqualTo(new Vector2(6f, -6f))); Assert.That(particleCenter, Is.EqualTo(fogCenter)); Assert.That(particles.Viewport.MinX, Is.EqualTo(fog.Viewport.MinX)); Assert.That(particles.Viewport.MinY, Is.EqualTo(fog.Viewport.MinY)); Assert.That(particles.Viewport.MaxX, Is.EqualTo(fog.Viewport.MaxX)); Assert.That(particles.Viewport.MaxY, Is.EqualTo(fog.Viewport.MaxY)); } [Test] public void ParticleRendererFailsClosedWithoutCompleteNativeAbi() { var compute = RequiredParticleComputeMetadata(); var material = RequiredParticleMaterialMetadata(); Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata(compute, material, out var error), Is.True, error); compute.Remove("unity.particles.intPort.span"); Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata(compute, material, out error), Is.False); Assert.That(error, Does.Contain("logical port")); } [Test] public void ParticleMaterialAcceptsAdvertisedTemporalDitherInputs() { var field = new EveUnityFieldParticlesProjection( "particles", "document", EveFieldsSchemas.Splats, "compute", "material", "world.transparent", new Dictionary(StringComparer.Ordinal) { ["materialAssetTextureBindings"] = "texture.noise=dither", ["materialViewportTextureScaleBindings"] = "ditherCoordinates=dither", ["materialRenderFrameIndexPort"] = "frameIndex" }); var metadata = RequiredParticleMaterialMetadata(); metadata["unity.particles.texturePort.dither"] = "_DitheringTex"; metadata["unity.particles.vectorPort.ditherCoordinates"] = "_DitheringCoords"; metadata["unity.particles.intPort.frameIndex"] = "_FrameNumber"; Assert.That(EveUnityFieldsParticleRenderer.TryValidateMaterialPresentationContract( field, metadata, out var error), Is.True, error); } [Test] public void ParticleMaterialRejectsPartialTemporalDitherInputs() { var field = new EveUnityFieldParticlesProjection( "particles", "document", EveFieldsSchemas.Splats, "compute", "material", "world.transparent", new Dictionary(StringComparer.Ordinal) { ["materialAssetTextureBindings"] = "texture.noise=dither", ["materialViewportTextureScaleBindings"] = "ditherCoordinates=dither", ["materialRenderFrameIndexPort"] = "frameIndex" }); var metadata = RequiredParticleMaterialMetadata(); metadata["unity.particles.texturePort.dither"] = "_DitheringTex"; metadata["unity.particles.vectorPort.ditherCoordinates"] = "_DitheringCoords"; Assert.That(EveUnityFieldsParticleRenderer.TryValidateMaterialPresentationContract( field, metadata, out var error), Is.False); Assert.That(error, Does.Contain("render-frame")); } [Test] public void ParticleRendererPreservesRepeatedDocumentSourcesForDistinctPorts() { var document = new EveFieldsSplatsDocument { SimulationTimeSeconds = 12.5 }; Assert.That(EveUnityFieldsParticleRenderer.TryEvaluateDocumentFloatBindings( document, "simulationTimeSeconds=time,1,0;simulationTimeSeconds=flowScroll,0.025,0", out var bindings), Is.True); Assert.That(bindings.Count, Is.EqualTo(2)); Assert.That(bindings[0].Key, Is.EqualTo("time")); Assert.That(bindings[0].Value, Is.EqualTo(12.5f)); Assert.That(bindings[1].Key, Is.EqualTo("flowScroll")); Assert.That(bindings[1].Value, Is.EqualTo(0.3125f)); } [Test] public void VolumeProgramAcceptsPortableTwoPassLifecycle() { var metadata = RequiredVolumeProgramMetadata(); Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.True, error); } [Test] public void VolumeProgramAcceptsCompleteTemporalLifecycle() { var metadata = RequiredVolumeProgramMetadata(); metadata["unity.volume.pass.temporal"] = "1"; metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.True, error); } [Test] public void VolumeRendererUsesCurrentProjectionWithPreviousViewWhenAdvertised() { var metadata = RequiredVolumeProgramMetadata(); metadata["unity.volume.matrixSemantic.previousViewProjection"] = "current-projection.previous-view.v1"; var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); var currentProjection = Matrix4x4.Scale(new Vector3(2f, 3f, 4f)); var previousView = Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f)); var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( metadata, previousViewProjection, currentProjection, Matrix4x4.identity, previousView); Assert.That(resolved, Is.EqualTo(currentProjection * previousView)); Assert.That(resolved, Is.Not.EqualTo(previousViewProjection)); } [Test] public void VolumeRendererDefaultsToTruePreviousViewProjection() { var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( RequiredVolumeProgramMetadata(), previousViewProjection, Matrix4x4.Scale(Vector3.one * 2f), Matrix4x4.identity, Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f))); Assert.That(resolved, Is.EqualTo(previousViewProjection)); } [Test] public void VolumeRendererUsesNonRenderTargetProjectionWhenAdvertised() { var metadata = RequiredVolumeProgramMetadata(); metadata["unity.volume.matrixSemantic.previousViewProjection"] = "non-render-target-projection.previous-view.v1"; var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); var renderTargetProjection = Matrix4x4.Scale(new Vector3(2f, -3f, 4f)); var nonRenderTargetProjection = Matrix4x4.Scale(new Vector3(2f, 3f, 4f)); var previousView = Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f)); var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( metadata, previousViewProjection, renderTargetProjection, nonRenderTargetProjection, previousView); Assert.That(resolved, Is.EqualTo(nonRenderTargetProjection * previousView)); Assert.That(resolved, Is.Not.EqualTo(renderTargetProjection * previousView)); } [Test] public void VolumeProgramRejectsUnknownPreviousViewProjectionSemantic() { var metadata = RequiredVolumeProgramMetadata(); metadata["unity.volume.pass.temporal"] = "1"; metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; metadata["unity.volume.matrixSemantic.previousViewProjection"] = "made-up"; Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); Assert.That(error, Does.Contain("previous-view-projection semantic")); } [Test] public void VolumeRendererUsesAdvertisedBootstrapQualityOnlyForEmptyHistory() { var metadata = RequiredVolumeProgramMetadata(); metadata["unity.volume.quality.bootstrap"] = "ultra"; metadata["unity.volume.quality.ultra.keyword"] = "ULTRA_QUALITY"; Assert.That(EveUnityFieldsVolumeRenderer.ResolveQuality(metadata, "high", true), Is.EqualTo("ultra")); Assert.That(EveUnityFieldsVolumeRenderer.ResolveQuality(metadata, "high", false), Is.EqualTo("high")); } [Test] public void VolumeProgramRejectsBootstrapQualityWithoutAdvertisedKeyword() { var metadata = RequiredVolumeProgramMetadata(); metadata["unity.volume.pass.temporal"] = "1"; metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; metadata["unity.volume.quality.bootstrap"] = "ultra"; Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); Assert.That(error, Does.Contain("bootstrap quality")); } [Test] public void VolumeProgramRejectsPartialTemporalLifecycle() { var metadata = RequiredVolumeProgramMetadata(); metadata["unity.volume.pass.temporal"] = "1"; metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); Assert.That(error, Does.Contain("history port")); } [Test] public void VolumeRendererDerivesViewportTextureScaleFromNativeDimensions() { var texture = new Texture2D(128, 64, TextureFormat.RGBA32, false); try { Assert.That(EveUnityFieldsVolumeRenderer.TryComputeViewportTextureScale( 640, 360, texture, out var scale), Is.True); Assert.That(scale, Is.EqualTo(new Vector4(5f, 5.625f, 0f, 0f))); } finally { UnityEngine.Object.DestroyImmediate(texture); } } [Test] public void VolumeRendererAppliesProviderAdvertisedLayerTargetShape() { var target = new EveFieldsSplatLayerTarget { LayerKey = "fog.tint" }; Assert.That( EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor( target, "0.5,0.25,true,trilinear"), Is.True); Assert.That(target.WidthScale, Is.EqualTo(0.5f)); Assert.That(target.HeightScale, Is.EqualTo(0.25f)); Assert.That(target.UseMipMaps, Is.True); Assert.That(target.FilterMode, Is.EqualTo(FilterMode.Trilinear)); } [Test] public void VolumeRendererRejectsMalformedLayerTargetShape() { var target = new EveFieldsSplatLayerTarget { WidthScale = 2f, HeightScale = 2f, UseMipMaps = true, FilterMode = FilterMode.Trilinear }; Assert.That( EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor(target, "0,1,true,bilinear"), Is.False); Assert.That(target.WidthScale, Is.EqualTo(2f)); Assert.That(target.HeightScale, Is.EqualTo(2f)); Assert.That(target.UseMipMaps, Is.True); Assert.That(target.FilterMode, Is.EqualTo(FilterMode.Trilinear)); } [Test] public void VolumeRendererBindsAuthoritativeDocumentTimeToLogicalPort() { var document = new EveFieldsSplatsDocument { SimulationTimeSeconds = 12.5 }; Assert.That( EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( document, "simulationTimeSeconds", "flowScroll,0.025,-0.1", out var port, out var value), Is.True); Assert.That(port, Is.EqualTo("flowScroll")); Assert.That(value, Is.EqualTo(0.2125f).Within(0.00001f)); } [Test] public void VolumeRendererRejectsUnknownDocumentFloatSource() { Assert.That( EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( new EveFieldsSplatsDocument(), "unityTime", "flowScroll,1,0", out _, out _), Is.False); } [Test] public void PackageContainsGenericFieldsShader() { Assert.That(Shader.Find("Eve/Fields/Splats"), Is.Not.Null); Assert.That(File.Exists("Packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl"), Is.True); } private static Dictionary RequiredParticleComputeMetadata() => new Dictionary(StringComparer.Ordinal) { ["unity.particles.kernel.update"] = "UpdateParticles", ["unity.particles.bufferPort.particles"] = "particles", ["unity.particles.texturePort.surfaceHeight"] = "_SurfaceHeight", ["unity.particles.texturePort.tint"] = "_Tint", ["unity.particles.texturePort.hue"] = "_Hue", ["unity.particles.vectorPort.viewportTransform"] = "_GridTransform", ["unity.particles.vectorPort.timeVector"] = "_Time", ["unity.particles.floatPort.time"] = "time", ["unity.particles.floatPort.period"] = "period", ["unity.particles.floatPort.spacing"] = "spacing", ["unity.particles.intPort.span"] = "span" }; private static Dictionary RequiredParticleMaterialMetadata() => new Dictionary(StringComparer.Ordinal) { ["unity.particles.pass.render"] = "0", ["unity.particles.bufferPort.particles"] = "particles", ["unity.particles.bufferPort.quadPoints"] = "quadPoints" }; [Test] public void LayerRendererConsumesPluginContractsWithoutProviderBindings() { var render = typeof(EveFieldsSplatLayerRenderer).GetMethod("Render"); Assert.That(render, Is.Not.Null); Assert.That(render.GetParameters()[0].ParameterType, Is.EqualTo(typeof(IEveFieldsSplatsDocument))); Assert.That(typeof(EveFieldsSplatLayerRenderer).GetField("GlobalTextureName"), Is.Null); } [Test] public void RasterizerProducesProviderIndependentFieldsCapture() { var host = new GameObject("eve-fields-capture"); var rasterizer = host.AddComponent(); var output = rasterizer.Render(new CaptureDocument(), 64, 64); Assert.That(output, Is.Not.Null); var previous = RenderTexture.active; RenderTexture.active = output; var readback = new Texture2D(64, 64, TextureFormat.RGBA32, false, true); readback.ReadPixels(new Rect(0, 0, 64, 64), 0, 0); readback.Apply(); RenderTexture.active = previous; Assert.That(readback.GetPixels().Any(pixel => pixel.r > 0.01f), Is.True); var outputPath = Path.GetFullPath(Path.Combine(Application.dataPath, "../../artifacts/fields-surface/latest/unity-fields.png")); Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); File.WriteAllBytes(outputPath, readback.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(readback); UnityEngine.Object.DestroyImmediate(host); } [Test] public void RasterizerLowersPortablePowerPulseParameters() { var document = new EveFieldsSplatsDocument { Viewport = new EveFieldsViewport { MinX = -1, MinY = -1, MaxX = 1, MaxY = 1 }, Splats = new EveFieldsSplatSoa { Count = 1, CenterX = new[] { 0d }, CenterY = new[] { 0d }, HalfExtentX = new[] { 1d }, HalfExtentY = new[] { 1d }, RotationCos = new[] { 1d }, RotationSin = new[] { 0d }, Channel = new[] { 0 }, Falloff = new[] { EveFieldsSplatFalloffs.PowerPulse }, FalloffScale = new[] { 2d }, FalloffExponent = new[] { 1d }, ValueR = new[] { 1d }, ValueA = new[] { 1d }, LayerIndex = new[] { 0 }, SourceKind = new[] { EveFieldsSplatSourceKinds.Constant } } }; var host = new GameObject("eve-fields-power-pulse"); var rasterizer = host.AddComponent(); var output = rasterizer.Render(document, 128, 128); var previous = RenderTexture.active; RenderTexture.active = output; var readback = new Texture2D(128, 128, TextureFormat.RGBAFloat, false, true); readback.ReadPixels(new Rect(0, 0, 128, 128), 0, 0); readback.Apply(); RenderTexture.active = previous; Assert.That(readback.GetPixel(64, 64).r, Is.GreaterThan(0.95f)); Assert.That(readback.GetPixel(80, 64).r, Is.InRange(0.65f, 0.85f)); Assert.That(readback.GetPixel(96, 64).r, Is.LessThan(0.02f)); UnityEngine.Object.DestroyImmediate(readback); UnityEngine.Object.DestroyImmediate(host); } [Test] public void RasterizerAnchorsAnimatedSimplexToFieldWorldAndPublishedSimulationTime() { var first = ProceduralDocument( 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); var shifted = ProceduralDocument( 25, 125, 75, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); var later = ProceduralDocument( 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 7); var firstValue = RenderFieldPixel(first, 64, 64); var shiftedValue = RenderFieldPixel(shifted, 32, 64); var laterValue = RenderFieldPixel(later, 64, 64); Assert.That(shiftedValue, Is.EqualTo(firstValue).Within(0.015f), "the same field-world coordinate must not swim when the viewport moves"); Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.02f), "animation must use the document's simulation time rather than Unity wall time"); } [Test] public void RasterizerLowersPortableAnimatedCellNoiseB() { var first = ProceduralDocument( 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 2); var later = ProceduralDocument( 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 11); var firstValue = RenderFieldPixel(first, 57, 71); var laterValue = RenderFieldPixel(later, 57, 71); Assert.That(firstValue, Is.GreaterThanOrEqualTo(0)); Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.005f)); } [Test] public void RasterizerLowersPortableAnimatedRadialCosine() { var initial = RadialCosineDocument(0); var halfTurnLater = RadialCosineDocument(Math.PI); var center = RenderFieldPixel(initial, 64, 64); var radial = RenderFieldPixel(initial, 80, 64); var laterCenter = RenderFieldPixel(halfTurnLater, 64, 64); var localX = (80.5f / 128f) * 2f - 1f; var localY = (64.5f / 128f) * 2f - 1f; var expected = Mathf.Cos(Mathf.Pow(Mathf.Sqrt(localX * localX + localY * localY), 1.25f) * Mathf.PI * 2f); Assert.That(center, Is.GreaterThan(0.98f)); Assert.That(radial, Is.EqualTo(expected).Within(0.04f)); Assert.That(laterCenter, Is.LessThan(-0.98f), "radial phase animation must use provider-published simulation time"); } private static EveFieldsSplatsDocument RadialCosineDocument(double simulationTime) { return new EveFieldsSplatsDocument { SimulationTimeSeconds = simulationTime, Viewport = new EveFieldsViewport { MinX = -1, MinY = -1, MaxX = 1, MaxY = 1 }, Splats = new EveFieldsSplatSoa { Count = 1, CenterX = new[] { 0d }, CenterY = new[] { 0d }, HalfExtentX = new[] { 1d }, HalfExtentY = new[] { 1d }, RotationCos = new[] { 1d }, RotationSin = new[] { 0d }, Channel = new[] { 0 }, Falloff = new[] { EveFieldsSplatFalloffs.Solid }, ValueR = new[] { 1d }, ValueA = new[] { 1d }, LayerIndex = new[] { 0 }, SourceKind = new[] { EveFieldsSplatSourceKinds.AnimatedRadialCosine }, FrequencyX = new[] { Math.PI * 2d }, FrequencyY = new[] { 1.25d }, PhaseX = new[] { 0d }, AnimationSpeed = new[] { 1d } } }; } private static EveFieldsSplatsDocument ProceduralDocument( double min, double max, double center, int sourceKind, double simulationTime) { return new EveFieldsSplatsDocument { SimulationTimeSeconds = simulationTime, Viewport = new EveFieldsViewport { MinX = min, MinY = 0, MaxX = max, MaxY = 100 }, Splats = new EveFieldsSplatSoa { Count = 1, CenterX = new[] { center }, CenterY = new[] { 50d }, HalfExtentX = new[] { 50d }, HalfExtentY = new[] { 50d }, RotationCos = new[] { 1d }, RotationSin = new[] { 0d }, Channel = new[] { 0 }, Falloff = new[] { EveFieldsSplatFalloffs.Solid }, ValueR = new[] { 1d }, ValueA = new[] { 1d }, LayerIndex = new[] { 0 }, SourceKind = new[] { sourceKind }, FrequencyX = new[] { 0.037d }, FrequencyY = new[] { 0.041d }, PhaseX = new[] { 0.17d }, PhaseY = new[] { -0.23d }, AnimationSpeed = new[] { 0.31d }, SourceFlags = new[] { (double)EveFieldsSplatSourceFlags.AbsoluteValue } } }; } private static float RenderFieldPixel(EveFieldsSplatsDocument document, int x, int y) { var host = new GameObject("eve-fields-procedural-capture"); var rasterizer = host.AddComponent(); var output = rasterizer.Render(document, 128, 128); var previous = RenderTexture.active; RenderTexture.active = output; var readback = new Texture2D(128, 128, TextureFormat.RGBAFloat, false, true); readback.ReadPixels(new Rect(0, 0, 128, 128), 0, 0); readback.Apply(); RenderTexture.active = previous; var value = readback.GetPixel(x, y).r; UnityEngine.Object.DestroyImmediate(readback); UnityEngine.Object.DestroyImmediate(host); return value; } private sealed class CaptureDocument : IEveFieldsSplatsDocument { public string Schema => EveFieldsSchemas.Splats; public long FrameId => 1; public double SimulationTimeSeconds => 0; public IEveFieldsViewport Viewport { get; } = new CaptureViewport(); public IReadOnlyList Layers { get; } = Array.Empty(); public IEveFieldsSplatSoa Splats { get; } = new CaptureSplats(); } private static Dictionary RequiredVolumeProgramMetadata() => new Dictionary(StringComparer.Ordinal) { ["unity.volume.pass.raymarch"] = "0", ["unity.volume.pass.composite"] = "2", ["unity.volume.texturePort.cloud"] = "_Cloud", ["unity.volume.matrixPort.cameraInverseViewProjection"] = "_CameraInverseViewProjection", ["unity.volume.vectorPort.cameraProjectionExtents"] = "_CameraProjectionExtents", ["unity.volume.vectorPort.viewportTransform"] = "_ViewportTransform", ["unity.volume.floatPort.raymarchOffset"] = "_RaymarchOffset" }; private sealed class CaptureViewport : IEveFieldsViewport { public double MinX => -1; public double MinY => -1; public double MaxX => 1; public double MaxY => 1; } private sealed class CaptureSplats : IEveFieldsSplatSoa { private static readonly double[] One = { 1d }; private static readonly double[] Zero = { 0d }; private static readonly int[] ZeroInt = { 0 }; public int Count => 1; public IReadOnlyList CenterX => Zero; public IReadOnlyList CenterY => Zero; public IReadOnlyList HalfExtentX => One; public IReadOnlyList HalfExtentY => One; public IReadOnlyList RotationCos => One; public IReadOnlyList RotationSin => Zero; public IReadOnlyList Channel => ZeroInt; public IReadOnlyList Falloff => ZeroInt; public IReadOnlyList ValueR => One; public IReadOnlyList ValueG => Zero; public IReadOnlyList ValueB => Zero; public IReadOnlyList ValueA => One; public IReadOnlyList SourceKey { get; } = new[] { "capture" }; public IReadOnlyList LayerIndex => ZeroInt; public IReadOnlyList SourceKind => ZeroInt; public IReadOnlyList FrequencyX => One; public IReadOnlyList FrequencyY => One; public IReadOnlyList PhaseX => Zero; public IReadOnlyList PhaseY => Zero; public IReadOnlyList AnimationSpeed => Zero; public IReadOnlyList SourceFlags => Zero; public IReadOnlyList FalloffScale => One; public IReadOnlyList FalloffExponent => One; } } }