Files
ichni_Official/Assets/Shaders/ScriptablePostProcessor/Volumes/AnimeBloom.cs
2026-07-23 14:52:15 +08:00

318 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Echovoid.Runtime.Behavior.Rendering;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Experimental.Rendering;
namespace SLSUtilities.Rendering.PostProcessing
{
public enum AnimeBloomBlurMode
{
DualKawaseFast,
DualKawaseQuality,
Box
}
[System.Serializable]
public sealed class AnimeBloomBlurModeParameter : VolumeParameter<AnimeBloomBlurMode>
{
public AnimeBloomBlurModeParameter(AnimeBloomBlurMode value, bool overrideState = false)
: base(value, overrideState)
{
}
}
[System.Serializable, VolumeComponentMenu("SLS/Postprocessing/Anime Bloom")]
public class AnimeBloom : ScriptablePostProcessorVolume
{
// BeforePostProcess: 在 ToneMapping 之前运行,确保 HDR bloom 颜色不被截断
// 这与 Unity 原生 Bloom 的执行阶段完全一致
public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.BeforePostProcess;
public override int OrderInInjectionPoint => 5;
[Header("Glow Settings")]
public ClampedFloatParameter intensity = new(1f, 0f, 10f);
// 注意threshold 在 Gamma 空间中设置C# 侧会转为 Linear。
// 0.9 在 gamma 空间 ≈ 0.79 在 linear 空间,这与 Unity 原生默认行为完全一致。
public MinFloatParameter threshold = new(0.9f, 0f);
[Header("Scatter")]
// scatter 在 [0,1] 范围内由用户设置。
// C# 侧映射Mathf.Lerp(0.05f, 0.95f, scatter.value),与 Unity 原生完全一致。
// 低值 = 光晕聚拢,高值 = 光晕向外大范围扩散。
public ClampedFloatParameter scatter = new(0.7f, 0f, 1f);
public MinFloatParameter clamp = new(65472f, 1f);
[Header("Iterations")]
// 迭代次数Dual Kawase 每层只需 1 Pass性能远低于原生双趟高斯。
// 现在主要作为质量 / pass budget 控制,视觉扩散范围由 bloomRadius 统一塑形。
public ClampedIntParameter diffusion = new(4, 1, 8);
[Header("Radius")]
[Tooltip("Bloom 视觉扩散半径。\n1.0=旧默认半径;调大可扩大光晕范围,调小可让光晕更贴近高亮区域。")]
public ClampedFloatParameter bloomRadius = new(1.0f, 0.1f, 8.0f);
[Tooltip("当 diffusion 改变时,自动补偿采样跨度,尽量保持 Bloom Radius 的视觉范围稳定。\n关闭后 diffusion 会像传统 mip 金字塔一样明显改变光晕大小。")]
public BoolParameter preserveRadiusAcrossDiffusion = new(true);
[Tooltip("半径补偿的参考 diffusion。\n默认 4 表示 bloomRadius=1 时以 4 层金字塔的旧效果作为基准。")]
public ClampedIntParameter radiusReferenceDiffusion = new(4, 1, 8);
[Tooltip("自动半径补偿的最大倍率。\n低 diffusion 为了维持大半径会拉大采样跨度;该上限用于限制色块感、十字感和采样离散感。")]
public ClampedFloatParameter maxRadiusCompensation = new(2.5f, 1.0f, 8.0f);
[Header("Blur Mode")]
[Tooltip("模糊方式。\nDualKawaseFast=当前默认移动端低采样方案DualKawaseQuality=更多采样、更平滑Box=更便宜但质感更硬。")]
public AnimeBloomBlurModeParameter blurMode = new(AnimeBloomBlurMode.DualKawaseFast);
[Header("Downsample Steps")]
[Tooltip("初始降采样倍率(首趟大幅压缩分辨率)。\n1=原生半分辨率(1/2)2=1/4分辨率3=1/8分辨率。\n调大此值可用极低的迭代次数跑出巨大且柔和的光晕大幅节省 GPU 带宽。")]
public ClampedIntParameter initialDownscaleShift = new(1, 1, 3);
[Tooltip("第 2 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊1=1/22=1/43=1/8。")]
public ClampedIntParameter downscaleStep1 = new(1, 0, 3);
[Tooltip("第 3 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊1=1/22=1/43=1/8。")]
public ClampedIntParameter downscaleStep2 = new(1, 0, 3);
[Tooltip("第 4 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊1=1/22=1/43=1/8。")]
public ClampedIntParameter downscaleStep3 = new(1, 0, 3);
[Tooltip("第 5 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊1=1/22=1/43=1/8。")]
public ClampedIntParameter downscaleStep4 = new(1, 0, 3);
[Tooltip("第 6 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊1=1/22=1/43=1/8。")]
public ClampedIntParameter downscaleStep5 = new(1, 0, 3);
[Tooltip("第 7 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊1=1/22=1/43=1/8。")]
public ClampedIntParameter downscaleStep6 = new(1, 0, 3);
[Tooltip("第 8 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊1=1/22=1/43=1/8。")]
public ClampedIntParameter downscaleStep7 = new(1, 0, 3);
[Header("Kernel")]
[Tooltip("采样偏移微调倍率。\n最终采样跨度 = bloomRadius * diffusion 自动补偿 * kernelScale。\n一般保持 1.0,用于针对具体画面做细调。")]
public ClampedFloatParameter kernelScale = new(1.0f, 0.5f, 3.0f);
[Header("Tint")]
// tint 染色C# 侧会归一化亮度(只保留色相/饱和度),与原生行为一致。
// 这样调整 tint 颜色不会意外改变 bloom 总亮度。
public ColorParameter tint = new(Color.white, true, true, true);
// RT 数组(仅 Down 金字塔 + Up 金字塔,数量与 diffusion 对齐)
private RTHandle[] _bloomMipDown;
private RTHandle[] _bloomMipUp;
private const int k_MaxMips = 8;
public override string GetShaderName() => "SLS/Postprocessing/AnimeBloom";
public override bool IsActive() => intensity.value > 0f;
public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination)
{
if (material == null) return;
var desc = renderingData.cameraData.cameraTargetDescriptor;
desc.msaaSamples = 1;
desc.depthBufferBits = 0;
// 【移动端极限优化】:强制降级到 32位 HDR 格式 (B10G11R11)
// 相比主相机默认的 64位 R16G16B16A16带宽消耗直接砍半且肉眼几乎无损
if (SystemInfo.IsFormatSupported(GraphicsFormat.B10G11R11_UFloatPack32,
GraphicsFormatUsage.Linear | GraphicsFormatUsage.Render))
{
desc.graphicsFormat = GraphicsFormat.B10G11R11_UFloatPack32;
}
// ─────────────────────────────────────────────────────────────
// 1. 参数打包(完全对齐 Unity SetupBloom 的计算逻辑)
// ─────────────────────────────────────────────────────────────
// scatter: 用户 [0,1] → shader [0.05, 0.95](与原生 Lerp 映射一致)
float scatterMapped = Mathf.Lerp(0.05f, 0.95f, scatter.value);
// threshold: Gamma → Linear原生使用 GammaToLinearSpace 转换)
float thresholdLinear = Mathf.GammaToLinearSpace(threshold.value);
float thresholdKnee = thresholdLinear * 0.5f; // 硬编码 soft knee与原生一致
material.SetVector(InternalShaderHelpers.ID._BloomScatterParams,
new Vector4(scatterMapped, clamp.value, thresholdLinear, thresholdKnee));
// ─────────────────────────────────────────────────────────────
// 2. Tint 归一化(原生做法:亮度归一为 1只携带色相/饱和度)
// 这样 tint 调整颜色时不会改变 bloom 总亮度。
// ─────────────────────────────────────────────────────────────
Color tintLinear = tint.value.linear;
float luma = 0.2126f * tintLinear.r + 0.7152f * tintLinear.g + 0.0722f * tintLinear.b;
Color tintNormalized = luma > 0f ? tintLinear * (1f / luma) : Color.white;
// 将 intensity 和归一化 tint 一并打包进 _BloomParams原生 uber 方式)
material.SetVector(InternalShaderHelpers.ID._BloomParams,
new Vector4(intensity.value, tintNormalized.r, tintNormalized.g, tintNormalized.b));
// ─────────────────────────────────────────────────────────────
// 3. RT 数组初始化
// ─────────────────────────────────────────────────────────────
int mipCount = Mathf.Clamp(diffusion.value, 1, k_MaxMips);
material.SetFloat(InternalShaderHelpers.ID._AnimeBloom_KernelScale, GetEffectiveKernelScale(mipCount));
if (_bloomMipDown == null || _bloomMipDown.Length != k_MaxMips)
{
_bloomMipDown = new RTHandle[k_MaxMips];
_bloomMipUp = new RTHandle[k_MaxMips];
}
// ─────────────────────────────────────────────────────────────
// 4. Prefilter提取高亮像素 + 初始激进降采样)
// 根据用户选择,起始分辨率可能是 1/2, 1/4 或 1/8。
// ─────────────────────────────────────────────────────────────
int shift = GetDownscaleShiftForMip(0);
desc.width = Mathf.Max(1, desc.width >> shift);
desc.height = Mathf.Max(1, desc.height >> shift);
RenderingUtils.ReAllocateHandleIfNeeded(ref _bloomMipDown[0], desc, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: "_BloomDown0");
// Source → Down[0] via Pass 0 (Prefilter)
Blitter.BlitCameraTexture(cmd, source, _bloomMipDown[0], material, 0);
// ─────────────────────────────────────────────────────────────
// 5. Downsample LoopDual Kawase 降采样金字塔Pass 1
// 每层可单独设置相对上一层的降采样倍率,产生可手动塑形的模糊纹理
// ─────────────────────────────────────────────────────────────
int downsamplePass = GetDownsamplePass();
for (int i = 1; i < mipCount; i++)
{
shift = GetDownscaleShiftForMip(i);
desc.width = Mathf.Max(1, desc.width >> shift);
desc.height = Mathf.Max(1, desc.height >> shift);
RenderingUtils.ReAllocateHandleIfNeeded(ref _bloomMipDown[i], desc, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: "_BloomDown" + i);
// Down[i-1] → Down[i] via selected downsample pass
Blitter.BlitCameraTexture(cmd, _bloomMipDown[i - 1], _bloomMipDown[i], material, downsamplePass);
}
// ─────────────────────────────────────────────────────────────
// 6. Upsample LoopDual Kawase 升采样 + lerp(high, low, scatter)
//
// 与 Unity 原生完全一致的数据流向:
// _BlitTexture = highMip = Down[i] (当前层高分辨率细节)
// _SourceTexLowMip = lowMip = Up[i+1] 或 Down[last](低分辨率扩散光晕)
// output = Up[i]
//
// 从最底层开始,逐层往上合并,最终 Up[0] 就是完整的 bloom 纹理
// ─────────────────────────────────────────────────────────────
// 分配所有 Up RT从最大 mip 往上,分辨率逐步翻倍)
// 由于 desc 已经在 downsample 中不断减半,我们要重新从 Down[i] 读尺寸
for (int i = mipCount - 1; i >= 0; i--)
{
RenderingUtils.ReAllocateHandleIfNeeded(ref _bloomMipUp[i],
_bloomMipDown[i].rt.descriptor, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: "_BloomUp" + i);
}
// 最底层lowMip = Down[last]highMip 也是 Down[last](无 low 可用,直接 lerp 自身,结果仍 = Down[last]
// 简化做法:直接将 Down[last] 拷到 Up[last] 作为起点
Blitter.BlitCameraTexture(cmd, _bloomMipDown[mipCount - 1], _bloomMipUp[mipCount - 1]);
// 从第二底层开始向上 upsample
int upsamplePass = GetUpsamplePass();
for (int i = mipCount - 2; i >= 0; i--)
{
// highMip当前层降采样纹理 Down[i],作为 Blit 的 source_BlitTexture
// lowMip 上一级升采样结果 Up[i+1],通过 SetTexture 传入
cmd.SetGlobalTexture(InternalShaderHelpers.ID._SourceTexLowMip, _bloomMipUp[i + 1]);
Blitter.BlitCameraTexture(cmd, _bloomMipDown[i], _bloomMipUp[i], material, upsamplePass);
}
// ─────────────────────────────────────────────────────────────
// 7. Composite将 bloom 叠加回原始画面Pass 3
// ─────────────────────────────────────────────────────────────
material.SetTexture(InternalShaderHelpers.ID._BloomTex, _bloomMipUp[0]);
Blitter.BlitCameraTexture(cmd, source, destination, material, 3);
}
private int GetDownscaleShiftForMip(int mipIndex)
{
return mipIndex switch
{
0 => initialDownscaleShift.value,
1 => downscaleStep1.value,
2 => downscaleStep2.value,
3 => downscaleStep3.value,
4 => downscaleStep4.value,
5 => downscaleStep5.value,
6 => downscaleStep6.value,
7 => downscaleStep7.value,
_ => 1
};
}
private float GetEffectiveKernelScale(int mipCount)
{
float compensation = preserveRadiusAcrossDiffusion.value
? GetRadiusCompensation(mipCount)
: 1f;
return Mathf.Max(0.01f, bloomRadius.value * kernelScale.value * compensation);
}
private float GetRadiusCompensation(int mipCount)
{
float referenceSpread = GetPyramidSpread(Mathf.Clamp(radiusReferenceDiffusion.value, 1, k_MaxMips));
float currentSpread = GetPyramidSpread(mipCount);
float compensation = referenceSpread / Mathf.Max(1f, currentSpread);
return Mathf.Clamp(compensation, 1f / maxRadiusCompensation.value, maxRadiusCompensation.value);
}
private float GetPyramidSpread(int mipCount)
{
int cumulativeShift = 0;
float spread = 0f;
for (int i = 0; i < mipCount; i++)
{
cumulativeShift += GetDownscaleShiftForMip(i);
spread += 1 << Mathf.Clamp(cumulativeShift, 0, 16);
}
return Mathf.Max(1f, spread);
}
private int GetDownsamplePass()
{
return blurMode.value switch
{
AnimeBloomBlurMode.DualKawaseQuality => 4,
AnimeBloomBlurMode.Box => 5,
_ => 1
};
}
private int GetUpsamplePass()
{
return blurMode.value switch
{
AnimeBloomBlurMode.DualKawaseQuality => 6,
AnimeBloomBlurMode.Box => 7,
_ => 2
};
}
public void Dispose()
{
if (_bloomMipDown == null) return;
for (int i = 0; i < _bloomMipDown.Length; i++)
{
_bloomMipDown[i]?.Release();
_bloomMipUp[i]?.Release();
}
}
}
}