Lower temporal field volumes generically

This commit is contained in:
Meta Vens 2026-07-15 20:59:30 +01:00
commit de580fe883
5 changed files with 188 additions and 27 deletions

View file

@ -18,6 +18,7 @@ drivers, asset-provider hooks, package release, runtime tests, and captures.
- `gamecult.eve.surface.v1`
- provider asset-manifest documents
- runtime-selected asset-variant metadata, including native rendering-program bindings
- generic field-volume lifecycle declarations and logical texture/scalar/matrix ports
- provider command receipts
- semantic render-channel inclusion and exclusion policy
- Unity input and local presentation assets
@ -33,8 +34,10 @@ drivers, asset-provider hooks, package release, runtime tests, and captures.
`ActiveWorld`, scene objects, transforms, markers, camera pose, native
render-channel layer mappings, resolved native shader ports/passes, asset caches,
per-volume raymarch targets, temporal-history targets, previous camera matrices,
presentation counters, and receipt display state are projections. They are not
world truth.
world truth. Temporal targets reset when the volume program, node, or render size
changes and can never become provider state.
## Forbidden Writers
@ -43,7 +46,10 @@ pending receipts may not mutate or simulate provider world state. Plugin
projection shells may not implement Sai, Norn, or TeX semantics. Portable
surfaces may not carry Unity layer numbers, camera culling masks, shader property
names, shader keywords, or native pass indices. Those bindings belong to the
selected runtime asset variant.
selected runtime asset variant. A native volume program that advertises a
temporal pass must also advertise its current-sample, history,
previous-view-projection, and reset-history ports; partial temporal programs fail
closed.
## Shared Paths

View file

@ -20,13 +20,17 @@ namespace GameCult.Eve.UnityScene.Fields
private Material? _material;
private Shader? _sourceShader;
private IReadOnlyDictionary<string, string>? _programMetadata;
private RenderTexture? _cloudTexture;
private RenderTexture? _raymarchTexture;
private readonly RenderTexture?[] _historyTextures = new RenderTexture?[2];
private int _historyTextureIndex = -1;
private Matrix4x4 _previousViewProjection;
private CommandBuffer? _commands;
private string _activeNodeId = "";
public long PresentedFrameId { get; private set; } = -1;
public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null);
public long CompositeCount { get; private set; }
public bool UsesTemporalHistory => HasTemporalProgram();
public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source)
{
@ -84,28 +88,58 @@ namespace GameCult.Eve.UnityScene.Fields
if (field == null || !EnsureMaterial(field)) return;
ApplyFieldBindings(field, _document);
camera.depthTextureMode |= DepthTextureMode.Depth;
EnsureCloudTexture(camera, field);
if (_cloudTexture == null) return;
EnsureVolumeTextures(camera, field);
if (_raymarchTexture == null) return;
var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
SetMatrixPort("cameraInverseViewProjection", (gpuProjection * camera.worldToCameraMatrix).inverse);
var viewProjection = gpuProjection * camera.worldToCameraMatrix;
SetMatrixPort("cameraInverseViewProjection", viewProjection.inverse);
SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera));
SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3));
var raymarchPass = ProgramPass("raymarch");
var compositePass = ProgramPass("composite");
SetTexturePort("cloud", _cloudTexture);
var temporalPass = ProgramPass("temporal");
var useTemporalHistory = temporalPass >= 0;
var historyWriteIndex = -1;
RenderTexture finalCloudTexture = _raymarchTexture;
if (useTemporalHistory)
{
historyWriteIndex = _historyTextureIndex < 0 ? 0 : (_historyTextureIndex + 1) % _historyTextures.Length;
var historyReadTexture = _historyTextureIndex < 0
? _raymarchTexture
: _historyTextures[_historyTextureIndex];
finalCloudTexture = _historyTextures[historyWriteIndex]!;
SetTexturePort("currentSample", _raymarchTexture);
SetTexturePort("history", historyReadTexture!);
SetMatrixPort("previousViewProjection", _historyTextureIndex < 0 ? viewProjection : _previousViewProjection);
SetFloatPort("resetHistory", _historyTextureIndex < 0 ? 1f : 0f);
}
SetTexturePort("cloud", finalCloudTexture);
_commands ??= new CommandBuffer { name = "Eve Fields Volume" };
_commands.Clear();
_commands.SetRenderTarget(_cloudTexture);
_commands.SetViewport(new Rect(0f, 0f, _cloudTexture.width, _cloudTexture.height));
_commands.SetRenderTarget(_raymarchTexture);
_commands.SetViewport(new Rect(0f, 0f, _raymarchTexture.width, _raymarchTexture.height));
_commands.ClearRenderTarget(false, true, Color.clear);
_commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1);
if (useTemporalHistory)
{
_commands.SetRenderTarget(finalCloudTexture);
_commands.SetViewport(new Rect(0f, 0f, finalCloudTexture.width, finalCloudTexture.height));
_commands.ClearRenderTarget(false, true, Color.clear);
_commands.DrawProcedural(Matrix4x4.identity, _material!, temporalPass, MeshTopology.Triangles, 3, 1);
}
_commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget);
_commands.SetViewport(camera.pixelRect);
_commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1);
Graphics.ExecuteCommandBuffer(_commands);
if (useTemporalHistory)
{
_historyTextureIndex = historyWriteIndex;
_previousViewProjection = viewProjection;
}
CompositeCount++;
}
@ -126,12 +160,7 @@ namespace GameCult.Eve.UnityScene.Fields
!metadataProvider.TryResolveAssetMetadata(shaderBinding, out var metadata))
return false;
_programMetadata = metadata;
if (ProgramPass("raymarch") < 0 || ProgramPass("composite") < 0 ||
string.IsNullOrWhiteSpace(ProgramPort("texture", "cloud")) ||
string.IsNullOrWhiteSpace(ProgramPort("matrix", "cameraInverseViewProjection")) ||
string.IsNullOrWhiteSpace(ProgramPort("vector", "cameraProjectionExtents")) ||
string.IsNullOrWhiteSpace(ProgramPort("vector", "viewportTransform")) ||
string.IsNullOrWhiteSpace(ProgramPort("float", "raymarchOffset")))
if (!TryValidateProgramMetadata(metadata, out _))
return false;
if (_material != null && ReferenceEquals(shader, _sourceShader) &&
string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal)) return true;
@ -139,6 +168,7 @@ namespace GameCult.Eve.UnityScene.Fields
_sourceShader = shader;
_activeNodeId = field.NodeId;
_material = new Material(shader) { hideFlags = HideFlags.DontSave };
ResetTemporalHistory();
foreach (var parameter in ParseBindings(Prop(field, "floatParameters")))
{
if (TryFloat(parameter.Value, out var value)) SetFloatPort(parameter.Key, value);
@ -203,32 +233,108 @@ namespace GameCult.Eve.UnityScene.Fields
if (_layers == null) _layers = gameObject.AddComponent<EveFieldsSplatLayerRenderer>();
}
private void EnsureCloudTexture(Camera camera, EveUnityFieldVolumeProjection field)
private void EnsureVolumeTextures(Camera camera, EveUnityFieldVolumeProjection field)
{
var downsample = Mathf.Clamp(NonNegativeInt(field, "downsample", 1), 0, 3);
var width = Mathf.Max(1, camera.pixelWidth >> downsample);
var height = Mathf.Max(1, camera.pixelHeight >> downsample);
if (_cloudTexture != null && _cloudTexture.width == width && _cloudTexture.height == height) return;
if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); }
_cloudTexture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat)
var resetHistory = EnsureRenderTexture(ref _raymarchTexture, width, height, "Eve Fields Volume Raymarch");
if (HasTemporalProgram())
{
name = "Eve Fields Volume",
filterMode = FilterMode.Bilinear,
wrapMode = TextureWrapMode.Clamp
};
_cloudTexture.Create();
resetHistory |= EnsureRenderTexture(ref _historyTextures[0], width, height, "Eve Fields Volume History A");
resetHistory |= EnsureRenderTexture(ref _historyTextures[1], width, height, "Eve Fields Volume History B");
}
else
{
ReleaseRenderTexture(ref _historyTextures[0]);
ReleaseRenderTexture(ref _historyTextures[1]);
resetHistory = true;
}
if (resetHistory) ResetTemporalHistory();
}
private void ReleaseRenderState()
{
if (_commands != null) { _commands.Release(); _commands = null; }
if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); _cloudTexture = null; }
ReleaseRenderTexture(ref _raymarchTexture);
ReleaseRenderTexture(ref _historyTextures[0]);
ReleaseRenderTexture(ref _historyTextures[1]);
if (_material != null) { Destroy(_material); _material = null; }
_sourceShader = null;
_programMetadata = null;
_activeNodeId = "";
PresentedFrameId = -1;
CompositeCount = 0;
ResetTemporalHistory();
}
private void ResetTemporalHistory()
{
_historyTextureIndex = -1;
_previousViewProjection = Matrix4x4.identity;
}
private bool HasTemporalProgram() => ProgramPass("temporal") >= 0;
private static bool EnsureRenderTexture(ref RenderTexture? texture, int width, int height, string name)
{
if (texture != null && texture.width == width && texture.height == height) return false;
ReleaseRenderTexture(ref texture);
texture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat)
{
name = name,
filterMode = FilterMode.Bilinear,
wrapMode = TextureWrapMode.Clamp
};
texture.Create();
return true;
}
private static void ReleaseRenderTexture(ref RenderTexture? texture)
{
if (texture == null) return;
texture.Release();
Destroy(texture);
texture = null;
}
public static bool TryValidateProgramMetadata(
IReadOnlyDictionary<string, string>? metadata,
out string error)
{
error = "";
if (metadata == null) return Fail("Native volume program metadata is missing.", out error);
if (!ValidPass(metadata, "raymarch")) return Fail("Native volume raymarch pass is missing.", out error);
if (!ValidPass(metadata, "composite")) return Fail("Native volume composite pass is missing.", out error);
if (!ValidPort(metadata, "texture", "cloud") ||
!ValidPort(metadata, "matrix", "cameraInverseViewProjection") ||
!ValidPort(metadata, "vector", "cameraProjectionExtents") ||
!ValidPort(metadata, "vector", "viewportTransform") ||
!ValidPort(metadata, "float", "raymarchOffset"))
return Fail("Native volume program is missing a required raymarch or composite port.", out error);
if (!metadata.ContainsKey("unity.volume.pass.temporal")) return true;
if (!ValidPass(metadata, "temporal")) return Fail("Native volume temporal pass is invalid.", out error);
if (!ValidPort(metadata, "texture", "currentSample") ||
!ValidPort(metadata, "texture", "history") ||
!ValidPort(metadata, "matrix", "previousViewProjection") ||
!ValidPort(metadata, "float", "resetHistory"))
return Fail("Native volume temporal pass is missing a required history port.", out error);
return true;
}
private static bool ValidPass(IReadOnlyDictionary<string, string> metadata, string pass) =>
metadata.TryGetValue($"unity.volume.pass.{pass}", out var value) &&
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0;
private static bool ValidPort(IReadOnlyDictionary<string, string> metadata, string kind, string port) =>
metadata.TryGetValue($"unity.volume.{kind}Port.{port}", out var value) &&
!string.IsNullOrWhiteSpace(value);
private static bool Fail(string message, out string error)
{
error = message;
return false;
}
private void SetMatrixPort(string port, Matrix4x4 value)

View file

@ -41,6 +41,38 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null);
}
[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 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 PackageContainsGenericFieldsShader()
{
@ -90,6 +122,18 @@ namespace GameCult.Eve.UnityScene.Tests
public IEveFieldsSplatSoa Splats { get; } = new CaptureSplats();
}
private static Dictionary<string, string> RequiredVolumeProgramMetadata() =>
new Dictionary<string, string>(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;

View file

@ -46,12 +46,17 @@
"pluginId": "fields.surface",
"capabilities": [
"field.surface2d",
"gravity.surface"
"gravity.surface",
"field.volume3d",
"volume.raymarch",
"volume.temporal-history",
"volume.composite"
],
"projectionAdapter": "EveFieldsSplatRasterizer",
"evidencePaths": [
"Runtime/Fields/EveFieldsSplatRasterizer.cs",
"Runtime/Fields/EveFieldsSplatLayerRenderer.cs",
"Runtime/Fields/EveUnityFieldsVolumeRenderer.cs",
"Tests/Editor/EveFieldsUnitySceneProjectionTests.cs",
"artifacts/fields-surface/latest/unity-fields.png"
]

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.37",
"version": "0.3.38",
"unity": "2021.3",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {