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 { 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/2;2=1/4;3=1/8。")] public ClampedIntParameter downscaleStep1 = new(1, 0, 3); [Tooltip("第 3 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊;1=1/2;2=1/4;3=1/8。")] public ClampedIntParameter downscaleStep2 = new(1, 0, 3); [Tooltip("第 4 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊;1=1/2;2=1/4;3=1/8。")] public ClampedIntParameter downscaleStep3 = new(1, 0, 3); [Tooltip("第 5 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊;1=1/2;2=1/4;3=1/8。")] public ClampedIntParameter downscaleStep4 = new(1, 0, 3); [Tooltip("第 6 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊;1=1/2;2=1/4;3=1/8。")] public ClampedIntParameter downscaleStep5 = new(1, 0, 3); [Tooltip("第 7 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊;1=1/2;2=1/4;3=1/8。")] public ClampedIntParameter downscaleStep6 = new(1, 0, 3); [Tooltip("第 8 层相对上一层的降采样倍率。\n0=保持上一层分辨率只做模糊;1=1/2;2=1/4;3=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 Loop:Dual 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 Loop:Dual 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(); } } } }