Lower temporal field volumes generically
This commit is contained in:
parent
4a513f4264
commit
de580fe883
5 changed files with 188 additions and 27 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Reference in a new issue