746 lines
34 KiB
C#
746 lines
34 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using GameCult.Eve.PluginFields;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using UnityEngine.Rendering.RenderGraphModule;
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
#nullable enable
|
|
|
|
namespace GameCult.Eve.UnityScene.Fields
|
|
{
|
|
public sealed class EveUnityFieldsVolumeRenderer : MonoBehaviour, IEveUnityRenderPassSource
|
|
{
|
|
private EveUnityPlayableWorldClientHost? _host;
|
|
private IEveUnityFieldsSplatsDocumentSource? _source;
|
|
private EveFieldsSplatsDocument? _document;
|
|
private EveFieldsSplatLayerRenderer? _layers;
|
|
private readonly List<EveFieldsSplatLayerTarget> _targets = new List<EveFieldsSplatLayerTarget>();
|
|
private Material? _material;
|
|
private Shader? _sourceShader;
|
|
private IReadOnlyDictionary<string, string>? _programMetadata;
|
|
private readonly Dictionary<string, Texture> _assetTexturesByPort =
|
|
new Dictionary<string, Texture>(StringComparer.Ordinal);
|
|
private readonly Dictionary<int, CameraRenderState> _cameraRenderStates =
|
|
new Dictionary<int, CameraRenderState>();
|
|
private EveUnityFieldsVolumePass? _renderPass;
|
|
private string _activeNodeId = "";
|
|
private long _rasterizedFrameId = -1;
|
|
private Vector2 _rasterizedCenter = new Vector2(float.NaN, float.NaN);
|
|
|
|
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 Vector2 LastGridCenter { get; private set; }
|
|
public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing;
|
|
int IEveUnityRenderPassSource.RenderOrder => 100;
|
|
|
|
public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source)
|
|
{
|
|
if (host == null) throw new ArgumentNullException(nameof(host));
|
|
if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable;
|
|
_host = host;
|
|
_source = source;
|
|
if (_source != null) _source.FieldsSplatsAvailable += OnFieldsSplatsAvailable;
|
|
enabled = _source != null;
|
|
}
|
|
|
|
private void OnEnable() => EveUnityRenderPassRegistry.Register(this);
|
|
|
|
private void OnDisable()
|
|
{
|
|
EveUnityRenderPassRegistry.Unregister(this);
|
|
ReleaseRenderState();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable;
|
|
_source = null;
|
|
_host = null;
|
|
}
|
|
|
|
private void OnFieldsSplatsAvailable(EveFieldsSplatsDocument document)
|
|
{
|
|
if (document == null || document.FrameId < PresentedFrameId) return;
|
|
_document = document;
|
|
PresentedFrameId = document.FrameId;
|
|
}
|
|
|
|
private bool TryRenderLayers(
|
|
EveFieldsSplatsDocument document,
|
|
Camera camera,
|
|
out EveFieldsSplatsDocument projected)
|
|
{
|
|
projected = document;
|
|
var field = ActiveField();
|
|
if (field == null) return false;
|
|
EnsureLayerRenderer();
|
|
EnsureTargets(field);
|
|
var width = PositiveInt(field, "textureWidth", 512);
|
|
var height = PositiveInt(field, "textureHeight", 512);
|
|
var snapLayer = Prop(field, "viewportSnapLayer");
|
|
var snapTarget = _targets.FirstOrDefault(target =>
|
|
string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal));
|
|
var snapWidth = snapTarget == null
|
|
? 0
|
|
: Mathf.Max(1, Mathf.RoundToInt(width * Mathf.Max(0.01f, snapTarget.WidthScale)));
|
|
var snapHeight = snapTarget == null
|
|
? 0
|
|
: Mathf.Max(1, Mathf.RoundToInt(height * Mathf.Max(0.01f, snapTarget.HeightScale)));
|
|
if (!EveUnityFieldsViewportFrame.TryResolve(
|
|
document,
|
|
field.Props,
|
|
new Vector2(camera.transform.position.x, camera.transform.position.z),
|
|
snapWidth,
|
|
snapHeight,
|
|
out projected,
|
|
out var center)) return false;
|
|
if (_rasterizedFrameId != document.FrameId || _rasterizedCenter != center)
|
|
{
|
|
_layers!.Render(projected, _targets, width, height);
|
|
_rasterizedFrameId = document.FrameId;
|
|
_rasterizedCenter = center;
|
|
}
|
|
LastGridCenter = center;
|
|
if (!EnsureMaterial(field)) return false;
|
|
ApplyFieldBindings(field, projected);
|
|
return true;
|
|
}
|
|
|
|
bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer)
|
|
{
|
|
if (_host == null || _document == null || camera == null ||
|
|
!CanRenderCamera(_host, camera))
|
|
return false;
|
|
_renderPass ??= new EveUnityFieldsVolumePass(this);
|
|
renderer.EnqueuePass(_renderPass);
|
|
return true;
|
|
}
|
|
|
|
internal static bool CanRenderCamera(EveUnityPlayableWorldClientHost host, Camera camera) =>
|
|
camera.cameraType == CameraType.SceneView || host.ActiveCameraTransform == camera.transform;
|
|
|
|
private void RenderVolume(
|
|
UnsafeCommandBuffer commands,
|
|
Camera camera,
|
|
TextureHandle colorTarget,
|
|
TextureHandle depthTarget)
|
|
{
|
|
if (_host == null || _document == null || camera == null ||
|
|
!CanRenderCamera(_host, camera))
|
|
return;
|
|
var field = ActiveField();
|
|
if (field == null || !TryRenderLayers(_document, camera, out _)) return;
|
|
camera.depthTextureMode |= DepthTextureMode.Depth;
|
|
var state = CameraState(camera);
|
|
EnsureVolumeTextures(camera, field, state);
|
|
if (state.RaymarchTexture == null || !ApplyViewportTextureScaleBindings(field, camera)) return;
|
|
ApplyQuality(field, state.HistoryTextureIndex < 0);
|
|
|
|
var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
|
|
var nonRenderTargetProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false);
|
|
var viewProjection = gpuProjection * camera.worldToCameraMatrix;
|
|
SetMatrixPort("cameraInverseViewProjection", viewProjection.inverse);
|
|
SetMatrixPort("cameraToWorld", CameraTransformToWorld(camera));
|
|
SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera));
|
|
SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3));
|
|
|
|
var raymarchPass = ProgramPass("raymarch");
|
|
var compositePass = ProgramPass("composite");
|
|
var temporalPass = ProgramPass("temporal");
|
|
var useTemporalHistory = temporalPass >= 0;
|
|
var historyWriteIndex = -1;
|
|
RenderTexture finalCloudTexture = state.RaymarchTexture;
|
|
|
|
if (useTemporalHistory)
|
|
{
|
|
historyWriteIndex = state.HistoryTextureIndex < 0 ? 0 : (state.HistoryTextureIndex + 1) % state.HistoryTextures.Length;
|
|
var historyReadTexture = state.HistoryTextureIndex < 0
|
|
? state.RaymarchTexture
|
|
: state.HistoryTextures[state.HistoryTextureIndex];
|
|
finalCloudTexture = state.HistoryTextures[historyWriteIndex]!;
|
|
SetTexturePort("currentSample", state.RaymarchTexture);
|
|
SetTexturePort("history", historyReadTexture!);
|
|
SetMatrixPort(
|
|
"previousViewProjection",
|
|
state.HistoryTextureIndex < 0
|
|
? viewProjection
|
|
: ResolvePreviousViewProjection(
|
|
_programMetadata,
|
|
state.PreviousViewProjection,
|
|
gpuProjection,
|
|
nonRenderTargetProjection,
|
|
state.PreviousView));
|
|
SetFloatPort("resetHistory", state.HistoryTextureIndex < 0 ? 1f : 0f);
|
|
}
|
|
SetTexturePort("cloud", finalCloudTexture);
|
|
|
|
commands.SetRenderTarget(state.RaymarchTexture);
|
|
commands.SetViewport(new Rect(0f, 0f, state.RaymarchTexture.width, state.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);
|
|
}
|
|
if (depthTarget.IsValid())
|
|
commands.SetRenderTarget(colorTarget, depthTarget);
|
|
else
|
|
commands.SetRenderTarget(colorTarget);
|
|
commands.SetViewport(camera.pixelRect);
|
|
commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1);
|
|
if (useTemporalHistory)
|
|
{
|
|
state.HistoryTextureIndex = historyWriteIndex;
|
|
state.PreviousViewProjection = viewProjection;
|
|
state.PreviousView = camera.worldToCameraMatrix;
|
|
}
|
|
CompositeCount++;
|
|
}
|
|
|
|
private EveUnityFieldVolumeProjection? ActiveField() =>
|
|
_host?.ActiveWorld?.FieldVolumes.FirstOrDefault(field =>
|
|
string.Equals(field.DocumentSchema, EveFieldsSchemas.Splats, StringComparison.Ordinal));
|
|
|
|
private bool EnsureMaterial(EveUnityFieldVolumeProjection field)
|
|
{
|
|
var provider = _host?.NativeAssetProvider;
|
|
if (provider == null || string.IsNullOrWhiteSpace(field.MaterialAssetRef)) return false;
|
|
var shader = provider.ResolveAsset(
|
|
new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"),
|
|
typeof(Shader)) as Shader;
|
|
if (shader == null || !shader.isSupported) return false;
|
|
var shaderBinding = new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref");
|
|
if (provider is not IEveUnityNativeAssetMetadataProvider metadataProvider ||
|
|
!metadataProvider.TryResolveAssetMetadata(shaderBinding, out var metadata))
|
|
return false;
|
|
_programMetadata = metadata;
|
|
if (!TryValidateProgramMetadata(metadata, out _))
|
|
return false;
|
|
if (_material != null && ReferenceEquals(shader, _sourceShader) &&
|
|
string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal)) return true;
|
|
if (_material != null) Destroy(_material);
|
|
_sourceShader = shader;
|
|
_activeNodeId = field.NodeId;
|
|
_material = new Material(shader) { hideFlags = HideFlags.DontSave };
|
|
_assetTexturesByPort.Clear();
|
|
ResetTemporalHistory();
|
|
foreach (var parameter in ParseBindings(Prop(field, "floatParameters")))
|
|
{
|
|
if (TryFloat(parameter.Value, out var value)) SetFloatPort(parameter.Key, value);
|
|
}
|
|
foreach (var parameter in ParseBindings(Prop(field, "vectorParameters")))
|
|
if (TryVector(parameter.Value, out var vector)) SetVectorPort(parameter.Key, vector);
|
|
foreach (var binding in ParseBindings(Prop(field, "assetTextureBindings")))
|
|
{
|
|
var texture = provider.ResolveAsset(
|
|
new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"),
|
|
typeof(Texture)) as Texture;
|
|
if (texture != null)
|
|
{
|
|
_assetTexturesByPort[binding.Value] = texture;
|
|
SetTexturePort(binding.Value, texture);
|
|
}
|
|
}
|
|
foreach (var feature in Prop(field, "features").Split(';'))
|
|
{
|
|
var logicalFeature = feature.Trim();
|
|
if (!string.IsNullOrWhiteSpace(logicalFeature) &&
|
|
TryProgramValue($"unity.volume.feature.{logicalFeature}.keyword", out var featureKeyword) &&
|
|
!string.IsNullOrWhiteSpace(featureKeyword))
|
|
_material.EnableKeyword(featureKeyword);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void ApplyQuality(EveUnityFieldVolumeProjection field, bool bootstrap)
|
|
{
|
|
if (_material == null || _programMetadata == null) return;
|
|
foreach (var binding in _programMetadata)
|
|
{
|
|
if (binding.Key.StartsWith("unity.volume.quality.", StringComparison.Ordinal) &&
|
|
binding.Key.EndsWith(".keyword", StringComparison.Ordinal) &&
|
|
!string.IsNullOrWhiteSpace(binding.Value))
|
|
_material.DisableKeyword(binding.Value);
|
|
}
|
|
var quality = ResolveQuality(_programMetadata, Prop(field, "quality"), bootstrap);
|
|
if (TryProgramValue($"unity.volume.quality.{quality}.keyword", out var keyword) &&
|
|
!string.IsNullOrWhiteSpace(keyword))
|
|
_material.EnableKeyword(keyword);
|
|
}
|
|
|
|
public static string ResolveQuality(
|
|
IReadOnlyDictionary<string, string>? metadata,
|
|
string steadyQuality,
|
|
bool bootstrap)
|
|
{
|
|
if (bootstrap && metadata != null &&
|
|
metadata.TryGetValue("unity.volume.quality.bootstrap", out var bootstrapQuality) &&
|
|
!string.IsNullOrWhiteSpace(bootstrapQuality))
|
|
return bootstrapQuality.Trim().ToLowerInvariant();
|
|
return (steadyQuality ?? "").Trim().ToLowerInvariant();
|
|
}
|
|
|
|
private void ApplyFieldBindings(EveUnityFieldVolumeProjection field, EveFieldsSplatsDocument document)
|
|
{
|
|
if (_material == null) return;
|
|
var bindings = ParseBindings(Prop(field, "layerBindings"));
|
|
foreach (var binding in bindings)
|
|
{
|
|
var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal));
|
|
if (target?.TargetTexture != null) SetTexturePort(binding.Value, target.TargetTexture);
|
|
}
|
|
var viewport = document.Viewport;
|
|
var width = viewport.MaxX - viewport.MinX;
|
|
var height = viewport.MaxY - viewport.MinY;
|
|
SetVectorPort("viewportTransform", new Vector4(
|
|
(float)((viewport.MinX + viewport.MaxX) * 0.5),
|
|
(float)((viewport.MinY + viewport.MaxY) * 0.5),
|
|
(float)width,
|
|
(float)height));
|
|
foreach (var binding in ParseBindings(Prop(field, "documentFloatBindings")))
|
|
{
|
|
if (TryEvaluateDocumentFloatBinding(
|
|
document,
|
|
binding.Key,
|
|
binding.Value,
|
|
out var port,
|
|
out var value))
|
|
SetFloatPort(port, value);
|
|
}
|
|
}
|
|
|
|
public static bool TryEvaluateDocumentFloatBinding(
|
|
EveFieldsSplatsDocument? document,
|
|
string source,
|
|
string descriptor,
|
|
out string port,
|
|
out float value)
|
|
{
|
|
port = "";
|
|
value = 0f;
|
|
if (document == null) return false;
|
|
var values = (descriptor ?? "").Split(',');
|
|
if (values.Length != 3 ||
|
|
string.IsNullOrWhiteSpace(values[0]) ||
|
|
!TryFloat(values[1].Trim(), out var scale) ||
|
|
!TryFloat(values[2].Trim(), out var offset))
|
|
return false;
|
|
|
|
double sourceValue;
|
|
switch ((source ?? "").Trim())
|
|
{
|
|
case "simulationTimeSeconds":
|
|
sourceValue = document.SimulationTimeSeconds;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
port = values[0].Trim();
|
|
value = (float)(sourceValue * scale + offset);
|
|
return float.IsFinite(value);
|
|
}
|
|
|
|
private void EnsureTargets(EveUnityFieldVolumeProjection field)
|
|
{
|
|
var bindings = ParseBindings(Prop(field, "layerBindings"));
|
|
var descriptors = ParseBindings(Prop(field, "layerTargetDescriptors"));
|
|
if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) &&
|
|
_targets.Count == bindings.Count &&
|
|
_targets.All(target => bindings.ContainsKey(target.LayerKey)))
|
|
{
|
|
foreach (var target in _targets)
|
|
ApplyLayerTargetDescriptor(target, descriptors);
|
|
return;
|
|
}
|
|
_targets.Clear();
|
|
foreach (var binding in bindings)
|
|
{
|
|
var target = new EveFieldsSplatLayerTarget { LayerKey = binding.Key };
|
|
ApplyLayerTargetDescriptor(target, descriptors);
|
|
_targets.Add(target);
|
|
}
|
|
}
|
|
|
|
private static void ApplyLayerTargetDescriptor(
|
|
EveFieldsSplatLayerTarget target,
|
|
IReadOnlyDictionary<string, string> descriptors)
|
|
{
|
|
target.WidthScale = 1f;
|
|
target.HeightScale = 1f;
|
|
target.UseMipMaps = false;
|
|
target.FilterMode = FilterMode.Bilinear;
|
|
if (descriptors.TryGetValue(target.LayerKey, out var descriptor))
|
|
TryApplyLayerTargetDescriptor(target, descriptor);
|
|
}
|
|
|
|
public static bool TryApplyLayerTargetDescriptor(EveFieldsSplatLayerTarget? target, string descriptor)
|
|
{
|
|
if (target == null) return false;
|
|
var values = (descriptor ?? "").Split(',');
|
|
if (values.Length != 4 ||
|
|
!TryFloat(values[0].Trim(), out var widthScale) || widthScale <= 0 ||
|
|
!TryFloat(values[1].Trim(), out var heightScale) || heightScale <= 0 ||
|
|
!bool.TryParse(values[2].Trim(), out var useMipMaps) ||
|
|
!Enum.TryParse(values[3].Trim(), true, out FilterMode filterMode))
|
|
return false;
|
|
target.WidthScale = widthScale;
|
|
target.HeightScale = heightScale;
|
|
target.UseMipMaps = useMipMaps;
|
|
target.FilterMode = filterMode;
|
|
return true;
|
|
}
|
|
|
|
private void EnsureLayerRenderer()
|
|
{
|
|
if (_layers != null) return;
|
|
_layers = GetComponent<EveFieldsSplatLayerRenderer>();
|
|
if (_layers == null) _layers = gameObject.AddComponent<EveFieldsSplatLayerRenderer>();
|
|
}
|
|
|
|
private CameraRenderState CameraState(Camera camera)
|
|
{
|
|
var cameraId = camera.GetInstanceID();
|
|
if (_cameraRenderStates.TryGetValue(cameraId, out var state)) return state;
|
|
state = new CameraRenderState();
|
|
_cameraRenderStates.Add(cameraId, state);
|
|
return state;
|
|
}
|
|
|
|
private void EnsureVolumeTextures(
|
|
Camera camera,
|
|
EveUnityFieldVolumeProjection field,
|
|
CameraRenderState state)
|
|
{
|
|
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);
|
|
var resetHistory = EnsureRenderTexture(
|
|
ref state.RaymarchTexture,
|
|
width,
|
|
height,
|
|
$"Eve Fields Volume Raymarch {camera.GetInstanceID()}");
|
|
if (HasTemporalProgram())
|
|
{
|
|
resetHistory |= EnsureRenderTexture(ref state.HistoryTextures[0], width, height, "Eve Fields Volume History A");
|
|
resetHistory |= EnsureRenderTexture(ref state.HistoryTextures[1], width, height, "Eve Fields Volume History B");
|
|
}
|
|
else
|
|
{
|
|
ReleaseRenderTexture(ref state.HistoryTextures[0]);
|
|
ReleaseRenderTexture(ref state.HistoryTextures[1]);
|
|
resetHistory = true;
|
|
}
|
|
if (resetHistory) ResetTemporalHistory(state);
|
|
}
|
|
|
|
private void ReleaseRenderState()
|
|
{
|
|
foreach (var state in _cameraRenderStates.Values)
|
|
{
|
|
ReleaseRenderTexture(ref state.RaymarchTexture);
|
|
ReleaseRenderTexture(ref state.HistoryTextures[0]);
|
|
ReleaseRenderTexture(ref state.HistoryTextures[1]);
|
|
}
|
|
_cameraRenderStates.Clear();
|
|
if (_material != null) { Destroy(_material); _material = null; }
|
|
_sourceShader = null;
|
|
_programMetadata = null;
|
|
_assetTexturesByPort.Clear();
|
|
_activeNodeId = "";
|
|
_rasterizedFrameId = -1;
|
|
_rasterizedCenter = new Vector2(float.NaN, float.NaN);
|
|
LastGridCenter = default;
|
|
PresentedFrameId = -1;
|
|
CompositeCount = 0;
|
|
ResetTemporalHistory();
|
|
}
|
|
|
|
private sealed class EveUnityFieldsVolumePass : ScriptableRenderPass
|
|
{
|
|
private readonly EveUnityFieldsVolumeRenderer _owner;
|
|
|
|
private sealed class PassData
|
|
{
|
|
internal Camera Camera = null!;
|
|
internal EveUnityFieldsVolumeRenderer Owner = null!;
|
|
internal TextureHandle ColorTarget;
|
|
internal TextureHandle DepthTarget;
|
|
}
|
|
|
|
internal EveUnityFieldsVolumePass(EveUnityFieldsVolumeRenderer owner)
|
|
{
|
|
_owner = owner;
|
|
renderPassEvent = CompositionRenderPassEvent;
|
|
ConfigureInput(ScriptableRenderPassInput.Depth);
|
|
}
|
|
|
|
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
|
|
{
|
|
var cameraData = frameData.Get<UniversalCameraData>();
|
|
if (cameraData.camera == null || _owner._host == null ||
|
|
!CanRenderCamera(_owner._host, cameraData.camera))
|
|
return;
|
|
|
|
var resourceData = frameData.Get<UniversalResourceData>();
|
|
var colorTarget = resourceData.activeColorTexture;
|
|
if (!colorTarget.IsValid()) return;
|
|
|
|
using var builder = renderGraph.AddUnsafePass<PassData>("Eve Fields Volume", out var passData);
|
|
passData.Camera = cameraData.camera;
|
|
passData.Owner = _owner;
|
|
passData.ColorTarget = colorTarget;
|
|
passData.DepthTarget = resourceData.activeDepthTexture;
|
|
|
|
builder.UseTexture(passData.ColorTarget, AccessFlags.Write);
|
|
if (passData.DepthTarget.IsValid())
|
|
builder.UseTexture(passData.DepthTarget, AccessFlags.Read);
|
|
builder.AllowPassCulling(false);
|
|
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) =>
|
|
data.Owner.RenderVolume(context.cmd, data.Camera, data.ColorTarget, data.DepthTarget));
|
|
}
|
|
}
|
|
|
|
private void ResetTemporalHistory()
|
|
{
|
|
foreach (var state in _cameraRenderStates.Values) ResetTemporalHistory(state);
|
|
}
|
|
|
|
private static void ResetTemporalHistory(CameraRenderState state)
|
|
{
|
|
state.HistoryTextureIndex = -1;
|
|
state.PreviousViewProjection = Matrix4x4.identity;
|
|
state.PreviousView = Matrix4x4.identity;
|
|
}
|
|
|
|
private sealed class CameraRenderState
|
|
{
|
|
internal RenderTexture? RaymarchTexture;
|
|
internal readonly RenderTexture?[] HistoryTextures = new RenderTexture?[2];
|
|
internal int HistoryTextureIndex = -1;
|
|
internal Matrix4x4 PreviousViewProjection = Matrix4x4.identity;
|
|
internal Matrix4x4 PreviousView = Matrix4x4.identity;
|
|
}
|
|
|
|
public static Matrix4x4 ResolvePreviousViewProjection(
|
|
IReadOnlyDictionary<string, string>? metadata,
|
|
Matrix4x4 previousViewProjection,
|
|
Matrix4x4 currentGpuProjection,
|
|
Matrix4x4 currentNonRenderTargetProjection,
|
|
Matrix4x4 previousView)
|
|
{
|
|
if (metadata == null ||
|
|
!metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic))
|
|
return previousViewProjection;
|
|
if (string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal))
|
|
return currentGpuProjection * previousView;
|
|
if (string.Equals(semantic, "non-render-target-projection.previous-view.v1", StringComparison.Ordinal))
|
|
return currentNonRenderTargetProjection * previousView;
|
|
return previousViewProjection;
|
|
}
|
|
|
|
private bool HasTemporalProgram() => ProgramPass("temporal") >= 0;
|
|
|
|
private bool ApplyViewportTextureScaleBindings(EveUnityFieldVolumeProjection field, Camera camera)
|
|
{
|
|
foreach (var binding in ParseBindings(Prop(field, "viewportTextureScaleBindings")))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ProgramPort("vector", binding.Key)) ||
|
|
string.IsNullOrWhiteSpace(ProgramPort("texture", binding.Value)) ||
|
|
!_assetTexturesByPort.TryGetValue(binding.Value, out var texture) ||
|
|
!TryComputeViewportTextureScale(camera.pixelWidth, camera.pixelHeight, texture, out var scale))
|
|
return false;
|
|
SetVectorPort(binding.Key, scale);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public static bool TryComputeViewportTextureScale(
|
|
int viewportWidth,
|
|
int viewportHeight,
|
|
Texture? texture,
|
|
out Vector4 scale)
|
|
{
|
|
scale = default;
|
|
if (texture == null || texture.width <= 0 || texture.height <= 0) return false;
|
|
scale = new Vector4(
|
|
(float)Math.Max(1, viewportWidth) / texture.width,
|
|
(float)Math.Max(1, viewportHeight) / texture.height,
|
|
0f,
|
|
0f);
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
if (metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic) &&
|
|
!string.Equals(semantic, "previous-view-projection.v1", StringComparison.Ordinal) &&
|
|
!string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal) &&
|
|
!string.Equals(semantic, "non-render-target-projection.previous-view.v1", StringComparison.Ordinal))
|
|
return Fail("Native volume temporal pass has an unsupported previous-view-projection semantic.", out error);
|
|
if (metadata.TryGetValue("unity.volume.quality.bootstrap", out var bootstrapQuality) &&
|
|
(!metadata.TryGetValue(
|
|
$"unity.volume.quality.{bootstrapQuality.Trim().ToLowerInvariant()}.keyword",
|
|
out var bootstrapKeyword) || string.IsNullOrWhiteSpace(bootstrapKeyword)))
|
|
return Fail("Native volume temporal pass has no keyword for its bootstrap quality.", 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)
|
|
{
|
|
var property = ProgramPort("matrix", port);
|
|
if (!string.IsNullOrWhiteSpace(property)) _material!.SetMatrix(property, value);
|
|
}
|
|
|
|
private void SetVectorPort(string port, Vector4 value)
|
|
{
|
|
var property = ProgramPort("vector", port);
|
|
if (!string.IsNullOrWhiteSpace(property)) _material!.SetVector(property, value);
|
|
}
|
|
|
|
private void SetFloatPort(string port, float value)
|
|
{
|
|
var property = ProgramPort("float", port);
|
|
if (!string.IsNullOrWhiteSpace(property)) _material!.SetFloat(property, value);
|
|
}
|
|
|
|
private void SetTexturePort(string port, Texture value)
|
|
{
|
|
var property = ProgramPort("texture", port);
|
|
if (!string.IsNullOrWhiteSpace(property)) _material!.SetTexture(property, value);
|
|
}
|
|
|
|
private string ProgramPort(string kind, string port) =>
|
|
TryProgramValue($"unity.volume.{kind}Port.{port}", out var value) ? value : "";
|
|
|
|
private int ProgramPass(string pass) =>
|
|
TryProgramValue($"unity.volume.pass.{pass}", out var value) &&
|
|
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0
|
|
? parsed
|
|
: -1;
|
|
|
|
private bool TryProgramValue(string key, out string value)
|
|
{
|
|
value = "";
|
|
return _programMetadata != null && _programMetadata.TryGetValue(key, out value!);
|
|
}
|
|
|
|
private static Vector4 ProjectionExtents(Camera camera)
|
|
{
|
|
var y = camera.orthographic ? camera.orthographicSize : Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView);
|
|
return new Vector4(y * camera.aspect, y, 0f, 0f);
|
|
}
|
|
|
|
private static Matrix4x4 CameraTransformToWorld(Camera camera) =>
|
|
camera.transform.localToWorldMatrix;
|
|
|
|
private static float Halton(int index, int radix)
|
|
{
|
|
var result = 0f;
|
|
var fraction = 1f / radix;
|
|
while (index > 0) { result += (index % radix) * fraction; index /= radix; fraction /= radix; }
|
|
return result;
|
|
}
|
|
|
|
private static Dictionary<string, string> ParseBindings(string value)
|
|
{
|
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
foreach (var pair in (value ?? "").Split(';'))
|
|
{
|
|
var separator = pair.IndexOf('=');
|
|
if (separator <= 0 || separator >= pair.Length - 1) continue;
|
|
result[pair.Substring(0, separator).Trim()] = pair.Substring(separator + 1).Trim();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static string Prop(EveUnityFieldVolumeProjection field, string key) =>
|
|
field.Props.TryGetValue(key, out var value) ? value ?? "" : "";
|
|
|
|
private static int PositiveInt(EveUnityFieldVolumeProjection field, string key, int fallback) =>
|
|
int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 ? value : fallback;
|
|
|
|
private static int NonNegativeInt(EveUnityFieldVolumeProjection field, string key, int fallback) =>
|
|
int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value >= 0 ? value : fallback;
|
|
|
|
private static bool TryFloat(string value, out float result) =>
|
|
float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) && !float.IsNaN(result) && !float.IsInfinity(result);
|
|
|
|
private static bool TryVector(string value, out Vector4 result)
|
|
{
|
|
result = default;
|
|
var values = (value ?? "").Split(',');
|
|
if (values.Length < 2 || values.Length > 4) return false;
|
|
var parsed = new float[4];
|
|
for (var index = 0; index < values.Length; index++)
|
|
if (!TryFloat(values[index], out parsed[index])) return false;
|
|
result = new Vector4(parsed[0], parsed[1], parsed[2], parsed[3]);
|
|
return true;
|
|
}
|
|
}
|
|
}
|