Files
ichni_Official/Assets/Le Tai's Asset/TranslucentImage/Script/TranslucentImageSource.cs
SoulliesOfficial d4e860fa16 initial
2025-06-03 02:42:28 -04:00

621 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Jobs;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Serialization;
#if ENABLE_VR
using UnityEngine.XR;
#endif
namespace LeTai.Asset.TranslucentImage
{
/// <summary>
/// Common source of blur for Translucent Images.
/// </summary>
[ExecuteAlways]
[RequireComponent(typeof(Camera))]
[AddComponentMenu("Image Effects/Tai Le Assets/Translucent Image Source")]
[HelpURL("https://leloctai.com/asset/translucentimage/docs/articles/customize.html#translucent-image-source")]
public partial class TranslucentImageSource : MonoBehaviour
{
#region Private Field
[SerializeField]
BlurConfig blurConfig;
[SerializeField] [Range(0, 3)]
[Tooltip("Reduce the size of the screen before processing. Increase will improve performance but create more artifact")]
int downsample;
[SerializeField]
[Tooltip("Choose which part of the screen to blur. Smaller region is faster")]
Rect blurRegion = new Rect(0, 0, 1, 1);
[SerializeField]
[Tooltip("How many time to blur per second. Reduce to increase performance and save battery for slow moving background")]
float maxUpdateRate = float.PositiveInfinity;
[SerializeField]
[Tooltip("Expand the blurred area to avoid gap around moving UIs when using a low Max Update Rate." +
"\nUse higher value for lower Max Update Rate or faster UI movement. Use 0 for infinite Update Rate or static UIs." +
"\nFor the best culling effectiveness, set this at runtime while UIs are moving, and reset to 0 while they're static." +
"\nUnit: fraction of the screen's shorter side")]
[Range(0, 1)]
float cullPadding = 0;
[SerializeField]
[Tooltip("Preview the effect fullscreen. Not recommended for runtime use")]
bool preview;
[SerializeField]
[Tooltip("Fill the background where the frame buffer alpha is 0. Useful for VR Underlay and Passthrough, where these areas would otherwise be black")]
BackgroundFill backgroundFill = new BackgroundFill();
[SerializeField]
[Tooltip("Always blur the entire blur region to reduce cpu usage")]
bool skipCulling;
int lastDownsample;
Rect lastBlurRegion = new Rect(0, 0, 1, 1);
Rect lastCamPixelRect = new Rect(0, 0, 1, 1);
Vector2Int lastCamPixelSize = Vector2Int.zero;
float lastUpdate;
IBlurAlgorithm blurAlgorithm;
#pragma warning disable 0108
Camera camera;
#pragma warning restore 0108
Material previewMaterial;
RenderTexture blurredScreen;
CommandBuffer cmd;
#pragma warning disable CS0169
bool isForOverlayCanvas;
#pragma warning restore CS0169
bool needRegisterCanvasPreRenderCallback;
readonly List<IActiveRegionProvider> activeRegionProviders = new List<IActiveRegionProvider>();
VPMatrixCache vpMatrixCache;
NativeList<ActiveRegion> activeRegions;
NativeArray<Rect> activeRegionJobResult;
JobHandle findBoundsJobHandle;
static readonly Rect FULLSCREEN_REGION = new Rect(0, 0, 1, 1);
static ProfilerMarker profilerMarkerCull = new ProfilerMarker(nameof(TranslucentImageSource) + ".Culling");
#endregion
#region Properties
public BlurConfig BlurConfig
{
get { return blurConfig; }
set
{
blurConfig = value;
InitializeBlurAlgorithm();
}
}
/// <summary>
/// The rendered image will be shrinked by a factor of 2^{{Downsample}} before bluring to reduce processing time
/// </summary>
/// <value>
/// Must be non-negative. Default to 0
/// </value>
public int Downsample
{
get { return downsample; }
set { downsample = Mathf.Max(0, value); }
}
/// <summary>
/// Define the rectangular area on screen that will be blurred.
/// </summary>
/// <value>
/// Between 0 and 1
/// </value>
public Rect BlurRegion
{
get { return blurRegion; }
set
{
Vector2 min = new Vector2(1 / (float)Cam.pixelWidth, 1 / (float)Cam.pixelHeight);
blurRegion = value;
blurRegion.xMin = Mathf.Clamp(blurRegion.x, 0, 1 - min.x);
blurRegion.yMin = Mathf.Clamp(blurRegion.y, 0, 1 - min.y);
blurRegion.width = Mathf.Clamp(blurRegion.width, min.x, 1 - blurRegion.x);
blurRegion.height = Mathf.Clamp(blurRegion.height, min.y, 1 - blurRegion.y);
OnBlurRegionChanged();
}
}
public Rect ActiveRegion { get; private set; }
/// <summary>
/// Maximum number of times to update the blurred image each second
/// </summary>
public float MaxUpdateRate
{
get => maxUpdateRate;
set => maxUpdateRate = Mathf.Max(0, value);
}
/// <summary>
/// Expand the blurred area to avoid gap around moving UIs when using a low Max Update Rate.
/// Use higher value for lower Max Update Rate or faster UI movement. Use 0 for infinite Update Rate or static UIs.
/// For the best culling effectiveness, set this at runtime while UIs are moving, and reset to 0 while they're static.
/// Unit: fraction of the screen's shorter side.
/// </summary>
public float CullPadding
{
get => cullPadding;
set => cullPadding = Mathf.Max(0, value);
}
/// <summary>
/// Fill the background where the frame buffer alpha is 0. Useful for VR Underlay and Passthrough, where these areas would otherwise be black
/// </summary>
public BackgroundFill BackgroundFill
{
get => backgroundFill;
set => backgroundFill = value;
}
/// <summary>
/// Render the blurred result to the render target
/// </summary>
public bool Preview
{
get => preview;
set => preview = value;
}
/// <summary>
/// Always blur the entire blur region to reduce cpu usage
/// </summary>
public bool SkipCulling
{
get => skipCulling;
set => skipCulling = value;
}
/// <summary>
/// Result of the image effect. Translucent Image use this as their content (read-only)
/// </summary>
public RenderTexture BlurredScreen
{
get { return blurredScreen; }
set { blurredScreen = value; }
}
/// <summary>
/// Set in SRP to provide Cam.rect for overlay cameras
/// </summary>
public Rect CamRectOverride { get; set; } = Rect.zero;
/// <summary>
/// Blur Region rect is relative to Cam.rect . This is relative to the full screen
/// </summary>
public Rect BlurRegionNormalizedScreenSpace
{
get => ViewportToScreen01Space(BlurRegion);
set => BlurRegion = Screen01ToViewportSpace(value);
}
/// <summary>
/// The Camera attached to the same GameObject. Cached in field 'camera'
/// </summary>
internal Camera Cam
{
get { return camera ? camera : camera = GetComponent<Camera>(); }
}
/// <summary>
/// Minimum time in second to wait before refresh the blurred image.
/// If maxUpdateRate non-positive then just stop updating
/// </summary>
float MinUpdateCycle
{
get { return (MaxUpdateRate > 0) ? (1f / MaxUpdateRate) : float.PositiveInfinity; }
}
bool ShouldCull
{
get
{
return !SkipCulling
#if ENABLE_VR
&& !XRSettings.enabled
#endif
;
}
}
#endregion
public event Action blurredScreenChanged;
public event Action blurRegionChanged;
public void OnBlurRegionChanged()
{
blurRegionChanged?.Invoke();
}
public void RegisterActiveRegionProvider(IActiveRegionProvider provider)
{
activeRegionProviders.Add(provider);
}
public void UnRegisterActiveRegionProvider(IActiveRegionProvider provider)
{
activeRegionProviders.Remove(provider);
}
void OnEnable()
{
#if UNITY_EDITOR
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
{
Init();
}
#endif
vpMatrixCache = new VPMatrixCache();
activeRegions = new NativeList<ActiveRegion>(4, Allocator.Persistent);
activeRegionJobResult = new NativeArray<Rect>(1, Allocator.Persistent);
needRegisterCanvasPreRenderCallback = true;
}
void OnDisable()
{
needRegisterCanvasPreRenderCallback = false;
// Must unregister callback before collections disposal!
Canvas.willRenderCanvases -= OnWillRenderCanvases;
CompleteCull();
vpMatrixCache.Dispose();
activeRegions.Dispose();
activeRegionJobResult.Dispose();
}
protected virtual void Start()
{
Init();
}
#if UNITY_EDITOR
/// <summary>
/// Attempt to filter out scene view calls. Not sure if this is robust enough
/// </summary>
bool isFirstOnWillRenderCanvas = false;
#endif
void Update()
{
// Register this earlier would cause it to be called before the builtin Layout Groups
if (needRegisterCanvasPreRenderCallback)
{
Canvas.willRenderCanvases += OnWillRenderCanvases;
needRegisterCanvasPreRenderCallback = false;
}
#if UNITY_EDITOR
isFirstOnWillRenderCanvas = true;
#endif
}
void OnDestroy()
{
if (BlurredScreen)
BlurredScreen.Release();
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (cmd == null)
{
cmd = new CommandBuffer();
cmd.name = "Translucent Image Source";
}
if (blurAlgorithm != null && BlurConfig != null)
{
if (ShouldUpdateBlur())
{
cmd.Clear();
if (CompleteCull())
{
ReallocateBlurTexIfNeeded(Cam.pixelRect);
blurAlgorithm.Init(BlurConfig, true);
var blurExecData = new BlurExecutor.BlurExecutionData(source,
this,
blurAlgorithm);
BlurExecutor.ExecuteBlurWithTempTextures(cmd, ref blurExecData);
Graphics.ExecuteCommandBuffer(cmd);
}
}
// Using custom Blit for this lead to warning: OnRenderImage() possibly didn't write anything to the destination texture
if (Preview)
{
previewMaterial.SetVector(ShaderID.CROP_REGION, RectUtils.ToMinMaxVector(BlurRegion));
Graphics.Blit(BlurredScreen, destination, previewMaterial);
}
else
{
Graphics.Blit(source, destination);
}
}
else
{
Graphics.Blit(source, destination);
}
}
void Init()
{
previewMaterial = new Material(Shader.Find("Hidden/FillCrop"));
InitializeBlurAlgorithm();
ReallocateBlurTexIfNeeded(Cam.pixelRect);
lastDownsample = Downsample;
}
void InitializeBlurAlgorithm()
{
switch (blurConfig)
{
case ScalableBlurConfig _:
blurAlgorithm = new ScalableBlur();
break;
default:
blurAlgorithm = new ScalableBlur();
break;
}
}
void OnWillRenderCanvases()
{
#if UNITY_EDITOR
if (!isFirstOnWillRenderCanvas)
return;
isFirstOnWillRenderCanvas = false;
#endif
StartCull();
}
void StartCull()
{
profilerMarkerCull.Begin();
findBoundsJobHandle.Complete(); // No way to cancel job
vpMatrixCache.Clear();
activeRegions.Clear();
bool haveAnyActiveRegion = false;
for (var i = 0; i < activeRegionProviders.Count; i++)
{
if (!activeRegionProviders[i].HaveActiveRegion())
continue;
haveAnyActiveRegion = true;
if (!ShouldCull)
break;
activeRegionProviders[i].GetActiveRegion(vpMatrixCache, out var activeRegion);
activeRegions.Add(activeRegion);
}
if (haveAnyActiveRegion)
{
if (ShouldCull)
{
findBoundsJobHandle = new ActiveRegionMergeJob {
vpMatrices = vpMatrixCache.VpMatrices,
activeRegions = activeRegions,
screenSize = new Vector2(Screen.width, Screen.height),
viewport = GetActiveCameraRect(),
merged = activeRegionJobResult
}.Schedule();
}
else
{
ActiveRegion = FULLSCREEN_REGION;
}
}
else
{
ActiveRegion = Rect.zero;
}
profilerMarkerCull.End();
}
/// <summary>
/// Merge active regions into one
/// </summary>
/// <returns>False if the merged region is empty</returns>
public bool CompleteCull()
{
if (!ShouldCull)
return true;
profilerMarkerCull.Begin();
findBoundsJobHandle.Complete();
ActiveRegion = activeRegionJobResult[0];
if (MaxUpdateRate < float.PositiveInfinity)
{
var padding = new Vector2(CullPadding, CullPadding);
var aspect = Screen.width / (float)Screen.height;
if (aspect > 1)
{
padding.x /= aspect;
}
else
{
padding.y *= aspect;
}
ActiveRegion = RectUtils.Expand(ActiveRegion, padding);
}
profilerMarkerCull.End();
return ActiveRegion.width > 0 && ActiveRegion.height > 0;
}
void CreateNewBlurredScreen(Vector2Int camPixelSize)
{
if (BlurredScreen)
BlurredScreen.Release();
#if ENABLE_VR
if (XRSettings.enabled)
{
BlurredScreen = new RenderTexture(XRSettings.eyeTextureDesc);
BlurredScreen.width = Mathf.RoundToInt(BlurredScreen.width * BlurRegion.width) >> Downsample;
BlurredScreen.height = Mathf.RoundToInt(BlurredScreen.height * BlurRegion.height) >> Downsample;
BlurredScreen.depth = 0;
}
else
#endif
{
BlurredScreen = new RenderTexture(Mathf.RoundToInt(camPixelSize.x * BlurRegion.width) >> Downsample,
Mathf.RoundToInt(camPixelSize.y * BlurRegion.height) >> Downsample, 0);
}
BlurredScreen.antiAliasing = 1;
BlurredScreen.useMipMap = false;
BlurredScreen.name = $"{gameObject.name} Translucent Image Source";
BlurredScreen.filterMode = FilterMode.Bilinear;
#if UNITY_EDITOR
// Avoid error logging when dragging related fields in the inspector
if (BlurredScreen.width > 0 && BlurredScreen.height > 0)
#endif
BlurredScreen.Create();
blurredScreenChanged?.Invoke();
}
TextureDimension lastEyeTexDim;
public void ReallocateBlurTexIfNeeded(Rect camPixelRect)
{
if (camPixelRect != lastCamPixelRect)
{
blurRegionChanged?.Invoke();
lastCamPixelRect = camPixelRect;
}
var camPixelSize = Vector2Int.RoundToInt(camPixelRect.size);
if (
BlurredScreen == null
|| !BlurredScreen.IsCreated()
|| Downsample != lastDownsample
|| !RectUtils.ApproximateEqual01(BlurRegion, lastBlurRegion)
|| camPixelSize != lastCamPixelSize
#if ENABLE_VR
|| XRSettings.deviceEyeTextureDimension != lastEyeTexDim
#endif
)
{
CreateNewBlurredScreen(camPixelSize);
lastDownsample = Downsample;
lastBlurRegion = BlurRegion;
lastCamPixelSize = camPixelSize;
#if ENABLE_VR
lastEyeTexDim = XRSettings.deviceEyeTextureDimension;
#endif
}
lastUpdate = GetTrueCurrentTime();
}
public bool ShouldUpdateBlur()
{
if (!enabled)
return false;
if (Preview)
return true;
float now = GetTrueCurrentTime();
bool should = now - lastUpdate >= MinUpdateCycle;
return should;
}
private static float GetTrueCurrentTime()
{
#if UNITY_EDITOR
return (float)UnityEditor.EditorApplication.timeSinceStartup;
#else
return Time.unscaledTime;
#endif
}
Rect GetActiveCameraRect()
{
var camRect = CamRectOverride.width == 0 ? Cam.rect : CamRectOverride;
camRect.min = Vector2.Max(Vector2.zero, camRect.min);
camRect.max = Vector2.Min(Vector2.one, camRect.max);
return camRect;
}
Rect ViewportToScreen01Space(Rect rect)
{
var camRect = GetActiveCameraRect();
return new Rect(camRect.position + rect.position * camRect.size,
rect.size * camRect.size);
}
Rect Screen01ToViewportSpace(Rect rect)
{
var camRect = GetActiveCameraRect();
return new Rect((rect.position - camRect.position) / camRect.size,
rect.size / camRect.size);
}
#if UNITY_EDITOR
protected virtual void OnGUI()
{
if (!Preview) return;
var curBlurRegionNSS = BlurRegionNormalizedScreenSpace;
var newBlurRegionNSS = ResizableScreenRect.Draw(curBlurRegionNSS, true);
GUI.color = Color.green;
ResizableScreenRect.Draw(RectUtils.Intersect(curBlurRegionNSS, ViewportToScreen01Space(ActiveRegion)));
if (newBlurRegionNSS != curBlurRegionNSS)
{
UnityEditor.Undo.RecordObject(this, "Change Blur Region");
BlurRegionNormalizedScreenSpace = newBlurRegionNSS;
}
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
}
#endif
}
}