SHA256
Add Custom Frame Modification Hook API for Video Rendering
This commit is contained in:
5 files changed
+961
-1
No files matched your search
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace ADOFAIRenderer.API;
|
||||
|
||||
/// <summary>
|
||||
/// Execution context provided to frame modification hooks, managing GPU ping-pong double buffering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>Zero-Allocation Notice:</b> For maximum performance on the render hot path, the pipeline reuses context instances
|
||||
/// across frames. Do not cache the context instance itself across frames.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Execution Frequency Notice:</b> This hook is executed only when a frame is captured for video encoding.
|
||||
/// When <c>RenderFPS</c> exceeds <c>VideoFPS</c>, intermediate game rendering frames are skipped and the hook
|
||||
/// will <b>not</b> be invoked on every rendered game frame.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
|
||||
public class FrameHookContext
|
||||
{
|
||||
private RenderTexture _originalSource = null!;
|
||||
private RenderTexture _pingRT = null!;
|
||||
private RenderTexture _pongRT = null!;
|
||||
|
||||
private RenderTexture _currentSource = null!;
|
||||
private RenderTexture _currentDestination = null!;
|
||||
private bool _hasModified;
|
||||
|
||||
/// <summary>
|
||||
/// The input <see cref="RenderTexture"/> containing the rendered frame before the current pass.
|
||||
/// </summary>
|
||||
public RenderTexture Source => _currentSource;
|
||||
|
||||
/// <summary>
|
||||
/// The writable destination <see cref="RenderTexture"/> for the current pass.
|
||||
/// </summary>
|
||||
public RenderTexture Destination => _currentDestination;
|
||||
|
||||
/// <summary>
|
||||
/// The active <see cref="RenderTexture"/> representing the current state of the modified frame.
|
||||
/// </summary>
|
||||
public RenderTexture Current => _currentSource;
|
||||
|
||||
/// <summary>
|
||||
/// The original unmodified render texture captured from the game for the current frame before any hooks were applied.
|
||||
/// </summary>
|
||||
public RenderTexture OriginalSource => _originalSource;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether any modifications or swaps have been applied to this frame.
|
||||
/// </summary>
|
||||
public bool HasModified => _hasModified;
|
||||
|
||||
/// <summary>
|
||||
/// The sequential 0-based index of the video frame currently being encoded.
|
||||
/// </summary>
|
||||
public long FrameIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="FrameIndex"/>. The sequential 0-based index of the video frame.
|
||||
/// </summary>
|
||||
public long VideoFrameIndex => FrameIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The sequential 0-based index of the game render frame.
|
||||
/// </summary>
|
||||
public long RenderFrameIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp of the current frame in seconds relative to video start.
|
||||
/// </summary>
|
||||
public double VideoTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The delta time duration in seconds of a single video frame (equivalent to 1.0 / VideoFPS).
|
||||
/// </summary>
|
||||
public double DeltaTime => VideoFPS > 0 ? 1.0 / VideoFPS : 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// The width of the frame in pixels.
|
||||
/// </summary>
|
||||
public int Width => _currentSource.width;
|
||||
|
||||
/// <summary>
|
||||
/// The height of the frame in pixels.
|
||||
/// </summary>
|
||||
public int Height => _currentSource.height;
|
||||
|
||||
/// <summary>
|
||||
/// The configured render frames per second.
|
||||
/// </summary>
|
||||
public double RenderFPS { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The output video frames per second.
|
||||
/// </summary>
|
||||
public double VideoFPS { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of successful modification passes completed for the current frame.
|
||||
/// </summary>
|
||||
public int PassCount { get; private set; }
|
||||
|
||||
internal FrameHookContext(
|
||||
RenderTexture originalSource,
|
||||
RenderTexture pingRT,
|
||||
RenderTexture pongRT,
|
||||
RenderTexture firstDestination,
|
||||
long frameIndex,
|
||||
long renderFrameIndex,
|
||||
double videoTime,
|
||||
double renderFps,
|
||||
double videoFps)
|
||||
{
|
||||
Reset(originalSource, pingRT, pongRT, firstDestination, frameIndex, renderFrameIndex, videoTime, renderFps, videoFps);
|
||||
}
|
||||
|
||||
internal void Reset(
|
||||
RenderTexture originalSource,
|
||||
RenderTexture pingRT,
|
||||
RenderTexture pongRT,
|
||||
RenderTexture firstDestination,
|
||||
long frameIndex,
|
||||
long renderFrameIndex,
|
||||
double videoTime,
|
||||
double renderFps,
|
||||
double videoFps)
|
||||
{
|
||||
_originalSource = originalSource;
|
||||
_pingRT = pingRT;
|
||||
_pongRT = pongRT;
|
||||
|
||||
_currentSource = originalSource;
|
||||
_currentDestination = firstDestination;
|
||||
_hasModified = false;
|
||||
PassCount = 0;
|
||||
|
||||
FrameIndex = frameIndex;
|
||||
RenderFrameIndex = renderFrameIndex;
|
||||
VideoTime = videoTime;
|
||||
RenderFPS = renderFps;
|
||||
VideoFPS = videoFps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blits from <see cref="Source"/> to <see cref="Destination"/> using the specified <see cref="Material"/> and automatically swaps ping-pong buffers.
|
||||
/// </summary>
|
||||
/// <param name="material">The material containing the shader to apply, or <c>null</c> for a standard copy.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
public void Blit(Material? material = null, int pass = -1)
|
||||
{
|
||||
if (material != null)
|
||||
{
|
||||
if (pass >= 0)
|
||||
Graphics.Blit(_currentSource, _currentDestination, material, pass);
|
||||
else
|
||||
Graphics.Blit(_currentSource, _currentDestination, material);
|
||||
}
|
||||
else
|
||||
{
|
||||
Graphics.Blit(_currentSource, _currentDestination);
|
||||
}
|
||||
|
||||
Swap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blits from a custom source <see cref="Texture"/> to <see cref="Destination"/> using the specified <see cref="Material"/> and automatically swaps ping-pong buffers.
|
||||
/// </summary>
|
||||
/// <param name="customSource">The custom source texture to blit from.</param>
|
||||
/// <param name="material">The material containing the shader to apply, or <c>null</c> for a standard copy.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="customSource"/> is null.</exception>
|
||||
public void Blit(Texture customSource, Material? material = null, int pass = -1)
|
||||
{
|
||||
if (customSource == null) throw new ArgumentNullException(nameof(customSource));
|
||||
|
||||
if (material != null)
|
||||
{
|
||||
if (pass >= 0)
|
||||
Graphics.Blit(customSource, _currentDestination, material, pass);
|
||||
else
|
||||
Graphics.Blit(customSource, _currentDestination, material);
|
||||
}
|
||||
else
|
||||
{
|
||||
Graphics.Blit(customSource, _currentDestination);
|
||||
}
|
||||
|
||||
Swap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the contents of <see cref="Source"/> into <see cref="Destination"/> without swapping buffers.
|
||||
/// Call this before drawing custom overlays into <see cref="Destination"/> if you wish to preserve the background.
|
||||
/// </summary>
|
||||
public void Copy()
|
||||
{
|
||||
Graphics.Blit(_currentSource, _currentDestination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears <see cref="Destination"/> with the specified color.
|
||||
/// </summary>
|
||||
/// <param name="clearColor">The color to clear with, or <c>null</c> for <see cref="Color.clear"/>.</param>
|
||||
public void ClearDestination(Color? clearColor = null)
|
||||
{
|
||||
var prev = RenderTexture.active;
|
||||
try
|
||||
{
|
||||
RenderTexture.active = _currentDestination;
|
||||
GL.Clear(false, true, clearColor ?? Color.clear);
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = prev;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily activates <see cref="Destination"/> as <see cref="RenderTexture.active"/> to perform custom draw calls,
|
||||
/// safely restoring the previous active RenderTexture afterwards.
|
||||
/// </summary>
|
||||
/// <param name="drawAction">The action that performs custom draw operations.</param>
|
||||
/// <param name="autoSwap">If <c>true</c> (default), automatically calls <see cref="Swap"/> after drawing.</param>
|
||||
/// <param name="preserveBackground">If <c>true</c>, copies <see cref="Source"/> into <see cref="Destination"/> before executing <paramref name="drawAction"/>.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="drawAction"/> is null.</exception>
|
||||
public void DrawToDestination(Action drawAction, bool autoSwap = true, bool preserveBackground = false)
|
||||
{
|
||||
if (drawAction == null) throw new ArgumentNullException(nameof(drawAction));
|
||||
|
||||
if (preserveBackground)
|
||||
{
|
||||
Copy();
|
||||
}
|
||||
|
||||
var prev = RenderTexture.active;
|
||||
try
|
||||
{
|
||||
RenderTexture.active = _currentDestination;
|
||||
drawAction();
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = prev;
|
||||
}
|
||||
|
||||
if (autoSwap)
|
||||
{
|
||||
Swap();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a <see cref="CommandBuffer"/> against the GPU and optionally swaps buffers.
|
||||
/// </summary>
|
||||
/// <param name="commandBuffer">The command buffer to execute.</param>
|
||||
/// <param name="autoSwap">If <c>true</c> (default), automatically calls <see cref="Swap"/> after execution.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="commandBuffer"/> is null.</exception>
|
||||
public void ExecuteCommandBuffer(CommandBuffer commandBuffer, bool autoSwap = true)
|
||||
{
|
||||
if (commandBuffer == null) throw new ArgumentNullException(nameof(commandBuffer));
|
||||
Graphics.ExecuteCommandBuffer(commandBuffer);
|
||||
if (autoSwap)
|
||||
{
|
||||
Swap();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the active <see cref="Source"/> and <see cref="Destination"/> ping-pong buffers.
|
||||
/// Call this after manually writing or rendering into <see cref="Destination"/> so that subsequent hooks or the video encoder read the updated frame.
|
||||
/// </summary>
|
||||
public void Swap()
|
||||
{
|
||||
_hasModified = true;
|
||||
PassCount++;
|
||||
|
||||
var previousDestination = _currentDestination;
|
||||
_currentSource = previousDestination;
|
||||
_currentDestination = (previousDestination == _pingRT) ? _pongRT : _pingRT;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the current modified frame texture with a custom <see cref="RenderTexture"/>.
|
||||
/// </summary>
|
||||
/// <param name="customTexture">The custom texture to set as the active frame.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="customTexture"/> is null.</exception>
|
||||
public void ReplaceCurrent(RenderTexture customTexture)
|
||||
{
|
||||
_currentSource = customTexture ?? throw new ArgumentNullException(nameof(customTexture));
|
||||
_hasModified = true;
|
||||
PassCount++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ADOFAIRenderer.API;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a custom frame modification hook that can modify rendered frames using GPU ping-pong double buffering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Execution Frequency Note:</b> Frame hooks are only invoked for frames that are actually encoded into the final video (<c>VideoFPS</c>).
|
||||
/// When <c>RenderFPS</c> is higher than <c>VideoFPS</c>, intermediate game render frames are skipped and this hook
|
||||
/// will <b>not</b> be called on every game frame.
|
||||
/// </remarks>
|
||||
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
||||
public interface IFrameHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked for each captured video frame before it is encoded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is not called on every rendered game frame if <c>RenderFPS</c> exceeds <c>VideoFPS</c>.
|
||||
/// </remarks>
|
||||
/// <param name="context">The frame hook context providing access to ping-pong textures and frame metadata.</param>
|
||||
void OnRenderFrame(FrameHookContext context);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using ADOFAIRenderer.Core;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.Util;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.API;
|
||||
|
||||
@@ -11,8 +13,47 @@ namespace ADOFAIRenderer.API;
|
||||
/// Fluent builder for configuring and starting a render session.
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
public class RenderSession
|
||||
{
|
||||
private sealed class GlobalHookRegistration : IDisposable
|
||||
{
|
||||
public object Key { get; }
|
||||
public Action<FrameHookContext> Action { get; }
|
||||
private bool _isDisposed;
|
||||
|
||||
public GlobalHookRegistration(object key, Action<FrameHookContext> action)
|
||||
{
|
||||
Key = key;
|
||||
Action = action;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
_isDisposed = true;
|
||||
RemoveGlobalRegistration(this);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SessionHookEntry
|
||||
{
|
||||
public object Key { get; }
|
||||
public Action<FrameHookContext> Action { get; }
|
||||
|
||||
public SessionHookEntry(object key, Action<FrameHookContext> action)
|
||||
{
|
||||
Key = key;
|
||||
Action = action;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly List<GlobalHookRegistration> GlobalFrameHooks = new();
|
||||
private static readonly object GlobalHooksLock = new();
|
||||
private static int _globalHooksVersion;
|
||||
|
||||
private readonly List<SessionHookEntry> _frameHooks = new();
|
||||
private bool _auto = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -126,6 +167,331 @@ public class RenderSession
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom frame modification hook to be executed for each encoded video frame.
|
||||
/// The hook can modify the rendered frame using GPU ping-pong double buffering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Execution Frequency Note:</b> This hook is invoked only for frames that are captured for the output video.
|
||||
/// When <c>RenderFPS</c> is higher than <c>VideoFPS</c>, intermediate game render frames are skipped,
|
||||
/// so this hook will <b>not</b> be called on every game frame.
|
||||
/// </remarks>
|
||||
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public RenderSession AddFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
_frameHooks.Add(new SessionHookEntry(hook, hook));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a material-based post-processing pass to be executed for each encoded video frame.
|
||||
/// Automatically performs a GPU blit from source to destination and swaps buffers.
|
||||
/// </summary>
|
||||
/// <param name="material">The material containing the shader to apply.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="material"/> is null.</exception>
|
||||
public RenderSession AddFrameHook(Material material, int pass = -1)
|
||||
{
|
||||
if (material is null) throw new ArgumentNullException(nameof(material));
|
||||
_frameHooks.Add(new SessionHookEntry(material, context => context.Blit(material, pass)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom frame modification hook implementing <see cref="IFrameHook"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to execute.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public RenderSession AddFrameHook(IFrameHook hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
_frameHooks.Add(new SessionHookEntry(hook, hook.OnRenderFrame));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="AddFrameHook(Action{FrameHookContext})"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession WithFrameHook(Action<FrameHookContext> hook) => AddFrameHook(hook);
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="AddFrameHook(Material, int)"/>.
|
||||
/// </summary>
|
||||
/// <param name="material">The material containing the shader to apply.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession WithFrameHook(Material material, int pass = -1) => AddFrameHook(material, pass);
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="AddFrameHook(IFrameHook)"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to execute.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession WithFrameHook(IFrameHook hook) => AddFrameHook(hook);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously added frame modification hook callback from this session.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook action to remove.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession RemoveFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook) || Equals(e.Action, hook));
|
||||
if (idx >= 0) _frameHooks.RemoveAt(idx);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously added material-based post-processing pass from this session.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to remove.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession RemoveFrameHook(Material material)
|
||||
{
|
||||
if (material)
|
||||
{
|
||||
for (var i = _frameHooks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (ReferenceEquals(_frameHooks[i].Key, material))
|
||||
{
|
||||
_frameHooks.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously added <see cref="IFrameHook"/> from this session.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to remove.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession RemoveFrameHook(IFrameHook hook)
|
||||
{
|
||||
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook));
|
||||
if (idx >= 0) _frameHooks.RemoveAt(idx);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all frame modification hooks configured for this render session.
|
||||
/// </summary>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession ClearFrameHooks()
|
||||
{
|
||||
_frameHooks.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a global frame modification hook that will execute on every render session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Execution Frequency Note:</b> Global frame hooks are executed only for frames that are captured for the output video.
|
||||
/// When <c>RenderFPS</c> is higher than <c>VideoFPS</c>, intermediate game render frames are skipped,
|
||||
/// so this hook will <b>not</b> be called on every game frame.
|
||||
/// </remarks>
|
||||
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public static IDisposable AddGlobalFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
var reg = new GlobalHookRegistration(hook, hook);
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
GlobalFrameHooks.Add(reg);
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a global material-based post-processing pass that will execute on every render session.
|
||||
/// </summary>
|
||||
/// <param name="material">The material containing the shader to apply.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="material"/> is null.</exception>
|
||||
public static IDisposable AddGlobalFrameHook(Material material, int pass = -1)
|
||||
{
|
||||
if (material is null) throw new ArgumentNullException(nameof(material));
|
||||
var reg = new GlobalHookRegistration(material, context => context.Blit(material, pass));
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
GlobalFrameHooks.Add(reg);
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a global frame modification hook implementing <see cref="IFrameHook"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to execute.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public static IDisposable AddGlobalFrameHook(IFrameHook hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
var reg = new GlobalHookRegistration(hook, hook.OnRenderFrame);
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
GlobalFrameHooks.Add(reg);
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a previously registered global frame modification hook.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook action to remove.</param>
|
||||
/// <returns><c>true</c> if the hook was successfully removed; otherwise, <c>false</c>.</returns>
|
||||
public static bool RemoveGlobalFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
var idx = GlobalFrameHooks.FindIndex(r => ReferenceEquals(r.Key, hook) || Equals(r.Action, hook));
|
||||
if (idx >= 0)
|
||||
{
|
||||
GlobalFrameHooks.RemoveAt(idx);
|
||||
_globalHooksVersion++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a previously registered global material-based post-processing pass.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to remove.</param>
|
||||
/// <returns><c>true</c> if any hooks matching <paramref name="material"/> were removed; otherwise, <c>false</c>.</returns>
|
||||
public static bool RemoveGlobalFrameHook(Material material)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
var removed = false;
|
||||
for (var i = GlobalFrameHooks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (ReferenceEquals(GlobalFrameHooks[i].Key, material))
|
||||
{
|
||||
GlobalFrameHooks.RemoveAt(i);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
if (removed)
|
||||
{
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a previously registered global frame modification hook implementing <see cref="IFrameHook"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to remove.</param>
|
||||
/// <returns><c>true</c> if the hook was successfully removed; otherwise, <c>false</c>.</returns>
|
||||
public static bool RemoveGlobalFrameHook(IFrameHook hook)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
var idx = GlobalFrameHooks.FindIndex(r => ReferenceEquals(r.Key, hook));
|
||||
if (idx >= 0)
|
||||
{
|
||||
GlobalFrameHooks.RemoveAt(idx);
|
||||
_globalHooksVersion++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveGlobalRegistration(GlobalHookRegistration registration)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
if (GlobalFrameHooks.Remove(registration))
|
||||
{
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all registered global frame modification hooks.
|
||||
/// </summary>
|
||||
public static void ClearGlobalFrameHooks()
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
if (GlobalFrameHooks.Count > 0)
|
||||
{
|
||||
GlobalFrameHooks.Clear();
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether any global frame modification hooks are currently registered.
|
||||
/// </summary>
|
||||
public static bool HasGlobalFrameHooks
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
return GlobalFrameHooks.Count > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of currently registered global frame modification hooks.
|
||||
/// </summary>
|
||||
public static int GlobalFrameHookCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
return GlobalFrameHooks.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static int GlobalHooksVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
return _globalHooksVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CopyGlobalFrameHooksTo(List<Action<FrameHookContext>> destination)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
for (var i = 0; i < GlobalFrameHooks.Count; i++)
|
||||
{
|
||||
destination.Add(GlobalFrameHooks[i].Action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the configured flags and initiates the render session.
|
||||
/// </summary>
|
||||
@@ -143,6 +509,15 @@ public class RenderSession
|
||||
if (_endCondition is not null) RenderContext.EndCondition = _endCondition;
|
||||
if (_onComplete is not null) RenderContext.OnComplete = _onComplete;
|
||||
if (_onLastUpdate is not null) RenderContext.OnLastUpdate = _onLastUpdate;
|
||||
|
||||
RenderContext.SessionFrameHooks.Clear();
|
||||
if (_frameHooks.Count > 0)
|
||||
{
|
||||
foreach (var t in _frameHooks)
|
||||
{
|
||||
RenderContext.SessionFrameHooks.Add(t.Action);
|
||||
}
|
||||
}
|
||||
|
||||
RenderManager.Instance.levelPath = levelPath;
|
||||
RenderManager.Instance.Configuration = renderConfig;
|
||||
|
||||
Reference in new issue
Block a user