diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 2ba43ef73..68d107800 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -222,13 +222,22 @@ internal sealed class AetheriaDaemonYmirSmokeChecks var volume = Flatten(surface.Surface.Root).Single(node => node.Kind == "field.volume3d"); Require(volume.Props.Values.All(value => value == null || !value.Contains("_Nebula", StringComparison.Ordinal)) && volume.Props["layerBindings"].Contains("fog.surface_height=surfaceHeight", StringComparison.Ordinal) && + volume.Props["layerTargetDescriptors"].Contains("fog.surface_height=2,2,false,bilinear", StringComparison.Ordinal) && + volume.Props["layerTargetDescriptors"].Contains("fog.tint=0.5,0.5,true,trilinear", StringComparison.Ordinal) && volume.Props["viewportTextureScaleBindings"] == "ditherCoordinates=dither" && + volume.Props["documentFloatBindings"] == "simulationTimeSeconds=flowScroll,0.025,0" && !volume.Props.ContainsKey("vectorParameters"), "portable Eve volume surfaces must name logical ports rather than Unity shader properties"); var shader = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset => asset.Ref.AssetKey == "shader.environment.gravity-fog"); + var postProcess = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset => + asset.Ref.AssetKey == "profile.environment.flight"); + Require(postProcess.Ref.Kind == AetheriaRuntimeAssetKinds.VolumeProfile && + postProcess.Ref.Metadata["presentationRole"] == "environment.post-process.flight", + "provider asset catalog must carry the advertised flight post-process profile variant"); Require(shader.Ref.Metadata["unity.volume.texturePort.surfaceHeight"] == "_NebulaSurfaceHeight" && + shader.Ref.Metadata["unity.volume.matrixPort.cameraToWorld"] == "_CamToWorld" && shader.Ref.Metadata["unity.volume.pass.raymarch"] == "0" && shader.Ref.Metadata["unity.volume.pass.temporal"] == "1" && shader.Ref.Metadata["unity.volume.pass.composite"] == "2" && @@ -715,6 +724,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "undocked world must preserve the authored ARPG Third Person Rig rather than the distinct Docked Rig"); Require(world.Props["skyboxAssetRef"] == "material.environment.skybox" && world.Props["reflectionAssetRef"] == "texture.environment.reflection" && + world.Props["postProcessProfileAssetRef"] == "profile.environment.flight" && world.Props["reflectionIntensity"] == "1" && world.Props["ambientLightIntensity"] == "1.46" && world.Props["keyLightDirection"] == "0.4,-1,0.25" && @@ -3169,7 +3179,10 @@ internal sealed class AetheriaDaemonYmirSmokeChecks RequireEqual("effect.beam.tractor", tractorEffect.Ref.Metadata["presentationRole"], "provider manifest must bind the stripped fossil tractor prefab to the generic beam role"); var thermalProfiles = assets.Where(asset => - string.Equals(asset.Ref.Kind, AetheriaRuntimeAssetKinds.VolumeProfile, StringComparison.Ordinal)).ToArray(); + string.Equals(asset.Ref.Kind, AetheriaRuntimeAssetKinds.VolumeProfile, StringComparison.Ordinal) && + asset.Ref.Metadata.TryGetValue("presentationRole", out var role) && + (role.StartsWith("post.thermal.", StringComparison.Ordinal) || + string.Equals(role, "post.death", StringComparison.Ordinal))).ToArray(); RequireEqual(5, thermalProfiles.Length, "provider manifest must advertise every original thermal and death volume profile"); Require(thermalProfiles.All(asset => diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index d920f852c..789ae4d63 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -794,7 +794,7 @@ static void RequireSharedEvePackagesImportedFromEveRepo(string root) "\"org.gamecult.cultlib\": \"https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13\"", "\"org.gamecult.eve.plugin-fields\": \"https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3\"", "\"org.gamecult.eve.surface\": \"https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2\"", - "\"org.gamecult.eve.unity-scene\": \"https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.44\"" + "\"org.gamecult.eve.unity-scene\": \"https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.49\"" }; var missingManifestSymbols = requiredManifestSymbols @@ -813,7 +813,7 @@ static void RequireSharedEvePackagesImportedFromEveRepo(string root) "cultlib-unity-v1.0.13", "eve-plugin-fields-unity-v0.2.3", "eveunity-surface-v0.2.2", - "eveunity-scene-v0.3.44", + "eveunity-scene-v0.3.49", "\"source\": \"git\"" }; diff --git a/Aetheria.Unity/Packages/manifest.json b/Aetheria.Unity/Packages/manifest.json index 258eb92fe..8847f1a1a 100644 --- a/Aetheria.Unity/Packages/manifest.json +++ b/Aetheria.Unity/Packages/manifest.json @@ -9,6 +9,6 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.44" + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.49" } } diff --git a/Aetheria.Unity/Packages/packages-lock.json b/Aetheria.Unity/Packages/packages-lock.json index 5b29ec668..0524e8dd8 100644 --- a/Aetheria.Unity/Packages/packages-lock.json +++ b/Aetheria.Unity/Packages/packages-lock.json @@ -144,7 +144,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.44", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.49", "depth": 0, "source": "git", "dependencies": { @@ -152,9 +152,10 @@ "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", + "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "03ce78031f28d7bfae4dcdc477a7273c6ecae55b" + "hash": "e6ba1a36f6f0bce8df2c9834c25a8a980f03413c" }, "com.unity.modules.assetbundle": { "version": "1.0.0", diff --git a/Assets/Editor/AetheriaRenderProbe.cs b/Assets/Editor/AetheriaRenderProbe.cs index 4f08025c1..82211ee2d 100644 --- a/Assets/Editor/AetheriaRenderProbe.cs +++ b/Assets/Editor/AetheriaRenderProbe.cs @@ -3,13 +3,16 @@ using System.IO; using System.Text; using GameCult.Aetheria.State.Verse; using UnityEditor; +using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; [InitializeOnLoad] public static class AetheriaRenderProbe { - private const string RelativeOutputDirectory = "Temp/AetheriaRenderProbe"; + private const string RelativeOutputDirectory = "Library/AetheriaRenderProbe"; + private const string ReferenceCaptureSessionKey = "AetheriaRenderProbe.CaptureArpgReference"; + private const string ReferenceCaptureDeadlineSessionKey = "AetheriaRenderProbe.CaptureArpgReference.Deadline"; private static string ProjectRoot => Path.GetFullPath(Path.Combine(Application.dataPath, "..")); @@ -23,6 +26,7 @@ public static class AetheriaRenderProbe static AetheriaRenderProbe() { EditorApplication.update += Poll; + EditorApplication.update += CaptureArpgWhenReady; } private static void Poll() @@ -72,6 +76,9 @@ public static class AetheriaRenderProbe if (camera != null) VolumeSampling.RenderForCamera(camera); + if (camera != null) + DumpCamera(camera, summary); + DumpVolumeSamplers(summary); DumpVolumeCloudRenderers(camera, summary); DumpGlobalTexture(summary, "_NebulaSurfaceHeight", "nebula-surface-height.png"); @@ -80,14 +87,63 @@ public static class AetheriaRenderProbe DumpGlobalTexture(summary, "_NebulaTint", "nebula-tint.png"); DumpGlobalTexture(summary, "_AetheriaGravityHeight", "gravity-height.png"); - if (camera != null) - DumpCamera(camera, summary); - File.WriteAllText(Path.Combine(OutputDirectory, "summary.txt"), summary.ToString()); AssetDatabase.Refresh(); Debug.Log($"Aetheria render probe wrote {Path.GetFullPath(OutputDirectory)}"); } + public static void CaptureArpgReference() + { + EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single); + AetheriaUrpBootstrap.EnsureUrpAssetsAndAssign(); + SessionState.SetBool(ReferenceCaptureSessionKey, true); + SessionState.SetString( + ReferenceCaptureDeadlineSessionKey, + (EditorApplication.timeSinceStartup + 15.0).ToString("R", System.Globalization.CultureInfo.InvariantCulture)); + EditorApplication.EnterPlaymode(); + } + + private static void CaptureArpgWhenReady() + { + if (!SessionState.GetBool(ReferenceCaptureSessionKey, false)) + return; + if (!EditorApplication.isPlaying || EditorApplication.isCompiling) + return; + + var rendered = false; + foreach (var renderer in Resources.FindObjectsOfTypeAll()) + { + if (renderer != null && renderer.gameObject.scene.IsValid() && renderer.DebugRenderCount > 0) + { + rendered = true; + break; + } + } + + var deadlineText = SessionState.GetString(ReferenceCaptureDeadlineSessionKey, "0"); + double.TryParse( + deadlineText, + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out var deadline); + if (!rendered && EditorApplication.timeSinceStartup < deadline) + return; + + SessionState.SetBool(ReferenceCaptureSessionKey, false); + try + { + Capture(); + EditorApplication.Exit(0); + } + catch (Exception error) + { + Directory.CreateDirectory(OutputDirectory); + File.WriteAllText(Path.Combine(OutputDirectory, "error.txt"), error.ToString()); + Debug.LogException(error); + EditorApplication.Exit(1); + } + } + private static Camera ResolveCamera() { if (Camera.main != null) @@ -155,15 +211,16 @@ public static class AetheriaRenderProbe summary.AppendLine($"- {sampler.name} enabled={sampler.enabled} active={sampler.gameObject.activeInHierarchy}"); summary.AppendLine($" GridCamera={NameOf(sampler.GridCamera)} GridMesh={NameOf(sampler.GridMesh)}"); - summary.AppendLine($" SceneSplatSource={NameOf(sampler.SceneSplatSource)} SplatLayerRenderer={NameOf(sampler.SplatLayerRenderer)}"); + summary.AppendLine($" SplatLayerRenderer={NameOf(sampler.SplatLayerRenderer)}"); summary.AppendLine($" Surface={TextureFacts(sampler.NebulaSurfaceHeight)}"); summary.AppendLine($" PatchHeight={TextureFacts(sampler.NebulaPatchHeight)}"); summary.AppendLine($" Patch={TextureFacts(sampler.NebulaPatch)}"); summary.AppendLine($" Tint={TextureFacts(sampler.NebulaTint)}"); DumpSamplerEnvironment(sampler, summary); - - if (sampler.SceneSplatSource != null) - DumpSceneSplatDocument(sampler, summary); + SaveTextureExr(sampler.NebulaSurfaceHeight, Path.Combine(OutputDirectory, "fossil-nebula-surface-height.exr")); + SaveTextureExr(sampler.NebulaPatchHeight, Path.Combine(OutputDirectory, "fossil-nebula-patch-height.exr")); + SaveTextureExr(sampler.NebulaPatch, Path.Combine(OutputDirectory, "fossil-nebula-patch.exr")); + SaveTextureExr(sampler.NebulaTint, Path.Combine(OutputDirectory, "fossil-nebula-tint.exr")); } } @@ -194,27 +251,6 @@ public static class AetheriaRenderProbe $" Grid Enabled={environment.Grid.Enabled} Offset={environment.Grid.Offset}"); } - private static void DumpSceneSplatDocument(VolumeSampling sampler, StringBuilder summary) - { - var size = sampler.GridCamera != null - ? new Vector2(sampler.GridCamera.orthographicSize * 2f, sampler.GridCamera.orthographicSize * 2f) - : new Vector2(1024f, 1024f); - var centerTransform = sampler.GridCamera != null ? sampler.GridCamera.transform : sampler.transform; - var center = centerTransform != null - ? new Vector2(centerTransform.position.x, centerTransform.position.z) - : Vector2.zero; - var half = size * 0.5f; - var document = sampler.SceneSplatSource.BuildDocument(new AetheriaRuntimeViewportBounds - { - MinX = center.x - half.x, - MinY = center.y - half.y, - MaxX = center.x + half.x, - MaxY = center.y + half.y - }); - - summary.AppendLine($" SceneDocument layers={document.Layers?.Count ?? 0} splats={document.Splats?.Count ?? 0}"); - } - private static void DumpGlobalTexture(StringBuilder summary, string globalName, string fileName) { var texture = Shader.GetGlobalTexture(globalName); @@ -263,6 +299,37 @@ public static class AetheriaRenderProbe } } + private static void SaveTextureExr(Texture texture, string path) + { + if (texture == null) + return; + var width = Mathf.Max(1, texture.width); + var height = Mathf.Max(1, texture.height); + var target = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGBFloat); + var previous = RenderTexture.active; + try + { + Graphics.Blit(texture, target); + RenderTexture.active = target; + var image = new Texture2D(width, height, TextureFormat.RGBAFloat, false, true); + try + { + image.ReadPixels(new Rect(0, 0, width, height), 0, 0); + image.Apply(); + File.WriteAllBytes(path, image.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat)); + } + finally + { + UnityEngine.Object.DestroyImmediate(image); + } + } + finally + { + RenderTexture.active = previous; + RenderTexture.ReleaseTemporary(target); + } + } + private static void SaveRenderTexture(RenderTexture target, string path) { var previous = RenderTexture.active; diff --git a/Assets/Editor/EveAssetBundleBuilder.cs b/Assets/Editor/EveAssetBundleBuilder.cs index fdefcd302..3cf508441 100644 --- a/Assets/Editor/EveAssetBundleBuilder.cs +++ b/Assets/Editor/EveAssetBundleBuilder.cs @@ -28,6 +28,7 @@ namespace Aetheria.Editor private static void Build(BuildTarget target) { EveThermalProfileMigrator.EnsureGenerated(); + EveEnvironmentProfileMigrator.EnsureGenerated(); var output = ResolveOutput(target); Directory.CreateDirectory(output); @@ -529,6 +530,9 @@ namespace Aetheria.Editor var reflection = LoadAuthoredAsset(bundle, assetNames, "Assets/Textures/studio2.hdr"); if (reflection == null) throw new InvalidOperationException("Aetheria Eve bundle has no provider reflection cubemap."); + var postProcess = LoadAuthoredAsset(bundle, assetNames, EveEnvironmentProfileMigrator.FlightProfilePath); + if (postProcess == null) + throw new InvalidOperationException("Aetheria Eve bundle has no provider flight post-process profile."); var gravityFog = LoadAuthoredAsset(bundle, assetNames, "Assets/Shaders/Raymarching/CloudShader.shader"); if (gravityFog == null || !gravityFog.isSupported) throw new InvalidOperationException("Aetheria Eve bundle has no supported gravity-fog volume shader."); diff --git a/Assets/Editor/EveEnvironmentProfileMigrator.cs b/Assets/Editor/EveEnvironmentProfileMigrator.cs new file mode 100644 index 000000000..7b50e896b --- /dev/null +++ b/Assets/Editor/EveEnvironmentProfileMigrator.cs @@ -0,0 +1,50 @@ +using System.IO; +using UnityEditor; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Rendering.Universal; + +namespace Aetheria.Editor +{ + public static class EveEnvironmentProfileMigrator + { + public const string Root = "Assets/Generated/Eve/Environment"; + public const string FlightProfilePath = Root + "/Flight.asset"; + + public static void EnsureGenerated() + { + Directory.CreateDirectory(Root); + AssetDatabase.DeleteAsset(FlightProfilePath); + var profile = ScriptableObject.CreateInstance(); + profile.name = "Aetheria Flight"; + AssetDatabase.CreateAsset(profile, FlightProfilePath); + + var tonemapping = profile.Add(true); + tonemapping.mode.Override(TonemappingMode.ACES); + + var color = profile.Add(true); + // The fossil histogram exposure clamps average luminance to 2^-3 and + // uses a 0.5 middle-grey key. At that dark-scene bound the resulting + // exposure is 4x, which is the fixed URP equivalent available here. + color.postExposure.Override(2f); + color.contrast.Override(15f); + + var bloom = profile.Add(true); + bloom.intensity.Override(3f); + bloom.threshold.Override(1.5f); + bloom.scatter.Override(1f); + + var vignette = profile.Add(true); + vignette.intensity.Override(0.3f); + vignette.smoothness.Override(0.4f); + + var grain = profile.Add(true); + grain.type.Override(FilmGrainLookup.Thin1); + grain.intensity.Override(0.1f); + + EditorUtility.SetDirty(profile); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + } +} diff --git a/Assets/Editor/EveEnvironmentProfileMigrator.cs.meta b/Assets/Editor/EveEnvironmentProfileMigrator.cs.meta new file mode 100644 index 000000000..5278f00d4 --- /dev/null +++ b/Assets/Editor/EveEnvironmentProfileMigrator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2fc598800cd0521408627e17b566c973 \ No newline at end of file diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 8bcb5617b..af127f46e 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -719,12 +719,17 @@ public class DaemonRuntimeDocumentTests asset.Ref.AssetKey == "shader.environment.gravity-fog" && asset.Ref.Kind == AetheriaRuntimeAssetKinds.Shader && asset.Ref.Metadata["unity.volume.texturePort.surfaceHeight"] == "_NebulaSurfaceHeight" && + asset.Ref.Metadata["unity.volume.matrixPort.cameraToWorld"] == "_CamToWorld" && asset.Ref.Metadata["unity.volume.pass.temporal"] == "1" && asset.Ref.Metadata["unity.volume.texturePort.currentSample"] == "_UndersampleCloudTex" && asset.Ref.Metadata["unity.volume.texturePort.history"] == "_MainTex")); Assert.IsTrue(assetManifest.Assets.Any(asset => asset.Ref.AssetKey == "texture.environment.volume-dither" && asset.Ref.Kind == AetheriaRuntimeAssetKinds.Texture)); + Assert.IsTrue(assetManifest.Assets.Any(asset => + asset.Ref.AssetKey == "profile.environment.flight" && + asset.Ref.Kind == AetheriaRuntimeAssetKinds.VolumeProfile && + asset.Ref.Metadata["presentationRole"] == "environment.post-process.flight")); var editorTuiSurface = result.EditorTuiSurface; Assert.AreEqual("aetheria.daemon", editorTuiSurface.ProviderId); Assert.AreEqual("editor.daemon", editorTuiSurface.ProviderKind); diff --git a/Assets/Shaders/Raymarching/CloudShader.shader b/Assets/Shaders/Raymarching/CloudShader.shader index eef208263..05718a266 100644 --- a/Assets/Shaders/Raymarching/CloudShader.shader +++ b/Assets/Shaders/Raymarching/CloudShader.shader @@ -11,6 +11,7 @@ Shader "Aetheria/CloudShader" [HideInInspector] _NebulaPatch("NebulaPatch", 2D) = "black" {} [HideInInspector] _NebulaTint("NebulaTint", 2D) = "black" {} [HideInInspector] _CloudTex("CloudTex", 2D) = "black" {} + [HideInInspector] _UndersampleCloudTex("UndersampleCloudTex", 2D) = "black" {} [HideInInspector] _CompositeOpacity("CompositeOpacity", Range(0, 1)) = 1 } @@ -49,6 +50,7 @@ Shader "Aetheria/CloudShader" sampler2D _DitheringTex; float4 _DitheringCoords; uniform float4x4 _CamInvProj; + uniform float4x4 _CamToWorld; float _ExtinctionCoefficient; @@ -164,7 +166,7 @@ Shader "Aetheria/CloudShader" float4 frag (Interpolator i) : SV_Target { float3 vspos = float3(i.vsray, 1.0); - float4 worldPos = mul(unity_CameraToWorld,float4(vspos,1.0)); + float4 worldPos = mul(_CamToWorld,float4(vspos,1.0)); worldPos /= worldPos.w; float2 screenUV = i.screenPos.xy / i.screenPos.w; float depthSample = tex2D(_CameraDepthTexture, screenUV).r; @@ -205,6 +207,7 @@ Shader "Aetheria/CloudShader" float4 _UndersampleCloudTex_TexelSize; float4x4 _PrevVP; //View projection matrix of last frame. Used to temporal reprojection. + float4x4 _CamToWorld; float _ResetHistory; //These values are needed for doing extra raymarch when out of bound. @@ -279,7 +282,7 @@ Shader "Aetheria/CloudShader" half outOfBound; float3 previousWorldPosition = mul( - unity_CameraToWorld, + _CamToWorld, float4(normalize(vspos) * distance, 1.0f)).xyz; float2 prevUV = PrevUV(float4(previousWorldPosition, 1.0f), outOfBound); float4 prevSample = tex2D(_MainTex, prevUV); diff --git a/Packages/manifest.json b/Packages/manifest.json index 0aeb618f4..574e650d1 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -23,9 +23,9 @@ "org.gamecult.cultmath": "file:../../CultMath", "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.surface": "file:../../EveUnity/packages/org.gamecult.eve.surface", - "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", + "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.unity-scene": "file:../../EveUnity/packages/org.gamecult.eve.unity-scene", - "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", + "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", "com.unity.modules.accessibility": "1.0.0", "com.unity.modules.adaptiveperformance": "1.0.0", "com.unity.modules.ai": "1.0.0", diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs index 53bfc4ec8..88748b013 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAssets.cs @@ -27,6 +27,7 @@ namespace GameCult.Aetheria.State.Verse var entries = new Dictionary(StringComparer.Ordinal); Add(entries, EnvironmentSkyboxMaterial()); Add(entries, EnvironmentReflectionCubemap()); + Add(entries, EnvironmentPostProcessProfile()); Add(entries, EnvironmentGravityFogShader()); Add(entries, EnvironmentDitherTexture()); Add(entries, MapIcon("entity.player", "Player", "Sprites/Icons/Stroked/Ship")); @@ -240,6 +241,7 @@ namespace GameCult.Aetheria.State.Verse ["unity.volume.texturePort.cloud"] = "_CloudTex", ["unity.volume.vectorPort.viewportTransform"] = "_GridTransform", ["unity.volume.matrixPort.cameraInverseViewProjection"] = "_CamInvProj", + ["unity.volume.matrixPort.cameraToWorld"] = "_CamToWorld", ["unity.volume.matrixPort.previousViewProjection"] = "_PrevVP", ["unity.volume.vectorPort.cameraProjectionExtents"] = "_ProjectionExtents", ["unity.volume.floatPort.raymarchOffset"] = "_RaymarchOffset", @@ -259,6 +261,7 @@ namespace GameCult.Aetheria.State.Verse ["unity.volume.floatPort.safetyDistance"] = "_SafetyDistance", ["unity.volume.floatPort.flowScale"] = "_FlowScale", ["unity.volume.floatPort.flowAmplitude"] = "_FlowAmplitude", + ["unity.volume.floatPort.flowScroll"] = "_FlowScroll", ["unity.volume.floatPort.flowPeriod"] = "_FlowPeriod", ["unity.volume.floatPort.flowSlopeAmplitude"] = "_FlowSlopeAmplitude", ["unity.volume.floatPort.flowSwirlAmplitude"] = "_FlowSwirlAmplitude", @@ -278,6 +281,28 @@ namespace GameCult.Aetheria.State.Verse }; } + public static AetheriaRuntimeAssetManifestEntry EnvironmentPostProcessProfile() + { + return new AetheriaRuntimeAssetManifestEntry + { + Ref = new AetheriaRuntimeAssetRef + { + AssetKey = "profile.environment.flight", + Kind = AetheriaRuntimeAssetKinds.VolumeProfile, + Uri = CultMeshAssetUri("profile.environment.flight"), + MimeType = "application/vnd.unity.volume-profile", + Metadata = new Dictionary(StringComparer.Ordinal) + { + ["unityAssetPath"] = "Assets/Generated/Eve/Environment/Flight.asset", + ["presentationRole"] = "environment.post-process.flight", + ["sourceProfile"] = "Assets/Scenes/ARPG_Profiles/Postprocessing Profile.asset", + ["profileSemantics"] = "aces;bloom;fixed-dark-scene-exposure;contrast;vignette;grain" + } + }, + Tags = new[] { "presentation", "environment", "post-process", "flight" } + }; + } + public static AetheriaRuntimeAssetManifestEntry EnvironmentDitherTexture() { return new AetheriaRuntimeAssetManifestEntry diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs index d78656211..dfff47874 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs @@ -621,6 +621,7 @@ namespace GameCult.Aetheria.State.Verse ["skyboxAssetRef"] = "material.environment.skybox", ["reflectionAssetRef"] = "texture.environment.reflection", ["reflectionIntensity"] = "1", + ["postProcessProfileAssetRef"] = "profile.environment.flight", ["keyLightDirection"] = "0.4,-1,0.25", ["keyLightColor"] = "1,0.95,0.9", ["keyLightIntensity"] = "0.75", @@ -698,8 +699,13 @@ namespace GameCult.Aetheria.State.Verse ["textureHeight"] = "1024", ["downsample"] = "0", ["layerBindings"] = "fog.surface_height=surfaceHeight;fog.patch_height=patchHeight;fog.patch=patch;fog.tint=tint", + ["layerTargetDescriptors"] = "fog.surface_height=2,2,false,bilinear;" + + "fog.patch_height=0.25,0.25,false,bilinear;" + + "fog.patch=0.5,0.5,false,bilinear;" + + "fog.tint=0.5,0.5,true,trilinear", ["assetTextureBindings"] = "texture.environment.volume-dither=dither", ["viewportTextureScaleBindings"] = "ditherCoordinates=dither", + ["documentFloatBindings"] = "simulationTimeSeconds=flowScroll,0.025,0", ["floatParameters"] = "fillDensity=0.000000001;fillDistance=120;fillExponent=5;fillOffset=70;" + "patchDensity=0.35;floorOffset=-20;floorBlend=10;patchBlend=25;luminance=1;extinction=0.5;" + "tintLodExponent=-0.45;safetyDistance=30;flowScale=512;flowAmplitude=15;flowPeriod=8;" + diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index b73af246a..583d4eaf3 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -324,13 +324,13 @@ "dependencies": {} }, "org.gamecult.eve.plugin-fields": { - "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.0", + "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "depth": 0, "source": "git", "dependencies": { "org.gamecult.cultlib": "1.0.13" }, - "hash": "382a23d8f8a07e4b5eef5f81f84655861a858367" + "hash": "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" }, "org.gamecult.eve.surface": { "version": "file:../../EveUnity/packages/org.gamecult.eve.surface", @@ -346,20 +346,21 @@ "source": "local", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.2.0", + "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", + "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" } }, "org.gamecult.eve.unity-uitoolkit": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", "depth": 0, "source": "git", "dependencies": { "org.gamecult.eve.surface": "0.1.0" }, - "hash": "e10821f1cc81f619479c6507438f6a097d1f0ce2" + "hash": "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" }, "com.unity.modules.accessibility": { "version": "1.0.0", diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index ad605590c..119aad249 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -884,6 +884,27 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. - Cut line: the unconditional `planar.top-down-follow.v1` projection no longer collapses the fossil dock and flight cameras into one false top-down view. +### Flight Post-Processing Contract + +- Owner: Aetheria selects the logical flight profile and owns its native asset + variants. EveUnity owns only a reversible lowering lease for the active world + camera. +- Inputs: `postProcessProfileAssetRef` from the world surface and the matching + provider-catalog variant. The Unity authoring build derives + `profile.environment.flight` from the fossil ARPG global profile's ACES, + bloom, histogram dark-scene exposure bound, contrast, vignette, and grain. +- Outputs: the generic Unity lowerer resolves a `VolumeProfile`, enables URP + post-processing on the leased camera, and installs one global runtime volume. +- Derived state: the Unity `Volume`, additional-camera data, and enabled flag are + client presentation state. They restore or disappear when the camera lease + ends. +- Forbidden writers: scene-local Aetheria scripts, provider-id checks, fog-color + heuristics, and client-authored exposure values cannot choose flight + presentation. +- Cut line: the fossil scene's global `PostProcessVolume` is reference evidence, + not a surviving runtime owner. Missing or incompatible advertised profiles + fail closed instead of silently rendering a different look. + ### Gravity Fog Presentation Contract - Owner: Aetheria owns gravity/fog brush placement and values, authored volume @@ -902,6 +923,15 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. raymarch targets, ping-pong history textures, previous camera matrices, and viewport-derived texture scales and composite counters are runtime projections. They are neither daemon truth nor portable Eve state. +- Native target topology: the provider advertises the fossil layer shapes rather + than letting the lowerer invent one common texture: surface height is + `2048x2048`, patch height is `256x256`, patch and tint are `512x512`, and only + tint is mipmapped/trilinear. EveUnity applies those descriptors without owning + the layer meanings. +- Time and camera bindings: the splat document's daemon-authored simulation time + drives logical `flowScroll` through an advertised scale. The provider variant + maps logical camera-to-world and flow-scroll ports to native uniforms; the + generic runtime does not use local gameplay time or provider identifiers. - Semantic split: `gravity.height` is the negative gameplay terrain projection; `fog.surface_height` is the positive visual displacement the fossil `PowerBrush` camera supplied to `_NebulaSurfaceHeight`. Both derive from the @@ -949,8 +979,14 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. complete temporal-program acceptance, partial-program rejection, portable `PowerPulse` GPU evaluation, and absence of Aetheria assembly references. Aetheria daemon smoke proves radial brush parity and that the exact fossil - shader ABI is advertised outside the portable surface. A live capture is still - required for visual parity. + shader ABI is advertised outside the portable surface. Released package + `0.3.49` proves target topology, authoritative flow time, explicit camera + transforms, float32 packed-output diagnostics, and both camera-channel masks. + The live warm capture remains visually failing: the fossil vertical-noise + displacement drives raymarch density to `1` within roughly three metres. + A controlled zero-amplitude run restores a fog horizon, isolating the open + parity defect to the noise/daemon-height interaction rather than transport, + pass ordering, camera pose, or missing field textures. ### Daemon Asset Content Delivery diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 1def926f7..aeb35da4d 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -34,7 +34,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. | Pilot camera | ARPG follow/dock cameras | Eve view semantics + runtime | camera rig and semantic view id | EveUnity camera rig | daemon dock state selects distinct contracts instead of collapsing both fossil cameras: undocked flight advertises the entity-forward `Third Person Rig` framing (distance 30, screen 0.64/0.81, FOV 60, lens 1-4096) and the shared `aim.presentation` convergence point as its look-at; EveUnity solves native pose while preserving both optical aim and advertised entity framing, without provider types; the retained dock projection remains separate | reproduce the dock camera's separate follow/look-at subjects, blend timeline, noise, and reconnect state | | Render channels | Unity layers/culling masks | provider surface policy + runtime asset variant | semantic channel exclusions | EveUnity native layer mapping | wired, exclusive, and lowered: the pilot surface excludes the semantic `map` channel; the Unity asset variant maps `map` to its authored native layer; EveUnity subtracts that layer from the configured camera mask; portable state contains no Unity layer or mask; package tests and the clean-client dependency verifier pass | live pilot capture proving no map glyphs while the map camera retains them | | Minimap/tactical map | `ZoneRenderer`, map cameras | daemon world facts | explicit map projections | Unity and Electron | facts fragmented; generic lowerers absent | same state in both runtimes | -| Fields/fog/gravity | zone renderer and shaders | daemon/plugin semantics | Fields plugin documents | EveUnity fields lowerer | daemon publishes canonical gravity/fog splat layers through `field.volume3d`; Eve Fields `0.2.3` owns portable `PowerPulse` and `AnimatedRadialCosine` semantics; Aetheria emits the fossil global animated simplex/cellular/ambient stack, body-owned radial waves, and separate negative gameplay `gravity.height` and positive visual `fog.surface_height`; daemon entities follow fossil terrain height plus hull grid offset after Ymir XZ motion; released EveUnity `0.3.44` lowers the field sources, temporal history, viewport-derived dither scale, and finite aim convergence without provider code; both camera-channel assertions pass and all field/HDR targets are populated, but the pilot capture is nearly black with sparse clipped geometry rather than the fossil blue fog sea | diagnose native composition and geometry visibility at the pilot camera, then restore tint/color, lighting, patch structure, and gravity-wave visual parity from fossil settings and measured native outputs; keep visual parity explicitly open | +| Fields/fog/gravity | zone renderer and shaders | daemon/plugin semantics | Fields plugin documents | EveUnity fields lowerer | daemon publishes canonical gravity/fog splat layers through `field.volume3d`; Eve Fields `0.2.3` owns portable `PowerPulse` and `AnimatedRadialCosine` semantics; Aetheria emits the fossil global animated simplex/cellular/ambient stack, body-owned radial waves, and separate negative gameplay `gravity.height` and positive visual `fog.surface_height`; daemon entities follow fossil terrain height plus hull grid offset after Ymir XZ motion; released EveUnity `0.3.49` lowers the authored 2048/256/512 layer topology, provider shader ABI, pre-postprocess URP pass, authoritative flow time, explicit camera matrix, and float32 packed-output diagnostics without provider code; both camera-channel assertions pass. The warm capture remains an opaque brown slab because fossil vertical noise saturates ray density to `1`; a controlled zero-amplitude run restores the expected fog horizon and isolates the remaining mismatch to noise versus daemon-generated heights | establish a measured parity rule for the fossil noise displacement against daemon height gradients, then restore blue tint, patch structure, and gravity-wave visual parity; keep the zero-noise result diagnostic-only and visual parity explicitly open | | HUD/schematic | ARPG HUD and `SchematicDisplay` | Eve composition from daemon facts | semantic status/components | EveUnity UI | provider-authored transparent cockpit is wired, projected, and lowered through generic UI Toolkit panes, metrics, and progress components for hull, shield, capacitor, temperature, cargo, weapon cooldown/groups, selected target, target lock, target shield, and target hull; the original armor/equipment schematic and transient warning choreography remain reference-only | schematic topology, warning timelines, dynamic action state, and composited live capture | | Hit/lock/warnings | `ActionGameManager` feedback | daemon event facts | transient feedback stream | VFX/UI/audio | bounded deduplicated game-event ledger and Eve `feedback.stream` wired for pickup outcomes, projectile launch/impact, entity damage, alive-to-dead destruction, authoritative weapon reload chronology, and episode-deduplicated `weapon.fire.refused` facts for insufficient energy or missing ammunition; refusal identity, authored weapon, target, reason, and remaining magazine state are runtime-independent cockpit inputs | add lock transitions, thermal and docking event families, negative transaction fixtures, and runtime effect consumption | | Assets | `Assets/Resources` | Aetheria provider | asset catalog + CultMesh CDN | runtime cache/load | provider manifests name deterministic bundle-internal paths; the authoring build derives presentation-only prefabs, strips missing and non-Eve `MonoBehaviour` components, verifies both saved prefabs and the loaded bundle, and publishes the immutable bundle through CultMesh; released-client warm-cache loading, content verification, and rendered provider identity are proven, while cold transfer still times out because the 46.1 MB body travels through batched snapshot records | mapped/network body transport followed by empty-cache download, atomic promotion, and cache reuse proof |