Migrate native flight environment contract

This commit is contained in:
Meta Vens 2026-07-16 01:56:29 +01:00
commit 94d3a7ba84
16 changed files with 260 additions and 47 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Cubemap>(bundle, assetNames, "Assets/Textures/studio2.hdr");
if (reflection == null)
throw new InvalidOperationException("Aetheria Eve bundle has no provider reflection cubemap.");
var postProcess = LoadAuthoredAsset<VolumeProfile>(bundle, assetNames, EveEnvironmentProfileMigrator.FlightProfilePath);
if (postProcess == null)
throw new InvalidOperationException("Aetheria Eve bundle has no provider flight post-process profile.");
var gravityFog = LoadAuthoredAsset<Shader>(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.");

View file

@ -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<VolumeProfile>();
profile.name = "Aetheria Flight";
AssetDatabase.CreateAsset(profile, FlightProfilePath);
var tonemapping = profile.Add<Tonemapping>(true);
tonemapping.mode.Override(TonemappingMode.ACES);
var color = profile.Add<ColorAdjustments>(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<Bloom>(true);
bloom.intensity.Override(3f);
bloom.threshold.Override(1.5f);
bloom.scatter.Override(1f);
var vignette = profile.Add<Vignette>(true);
vignette.intensity.Override(0.3f);
vignette.smoothness.Override(0.4f);
var grain = profile.Add<FilmGrain>(true);
grain.type.Override(FilmGrainLookup.Thin1);
grain.intensity.Override(0.1f);
EditorUtility.SetDirty(profile);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2fc598800cd0521408627e17b566c973

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@ namespace GameCult.Aetheria.State.Verse
var entries = new Dictionary<string, AetheriaRuntimeAssetManifestEntry>(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<string, string>(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

View file

@ -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;" +

View file

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

View file

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

View file

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