调整Bloom

This commit is contained in:
SoulliesOfficial
2026-06-30 04:36:54 -04:00
parent c5b6b4a089
commit 8e4690c964
34 changed files with 1883 additions and 1150 deletions

View File

@@ -213,7 +213,7 @@ Material:
- _Dst: 10
- _DstBlend: 0
- _DstBlendAlpha: 0
- _EdgeValue: 0.28290388
- _EdgeValue: 0.9463588
- _EnvironmentReflections: 1
- _FNLfanxiangkaiguan: 0
- _Face: 1
@@ -258,7 +258,7 @@ Material:
- _Mask_scale: 1
- _Metallic: 0
- _OcclusionStrength: 1
- _Opacity: 0.7170961
- _Opacity: 0.0536412
- _Parallax: 0.005
- _Pass: 0
- _QueueOffset: 0

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,180 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &4179914404395760092
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4793278966605267409}
- component: {fileID: 3144517396823347096}
- component: {fileID: 5649858463350041749}
m_Layer: 0
m_Name: ObjectTracker
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4793278966605267409
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4179914404395760092}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &3144517396823347096
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4179914404395760092}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: acb0592a986cebb4287d41702ab6ea22, type: 3}
m_Name:
m_EditorClassIdentifier:
updateMethod: 0
_spline: {fileID: 0}
_autoUpdate: 1
_rotationModifier:
blend: 1
useClippedPercent: 0
keys: []
_offsetModifier:
blend: 1
useClippedPercent: 0
keys: []
_colorModifier:
blend: 1
useClippedPercent: 0
keys: []
_sizeModifier:
blend: 1
useClippedPercent: 0
keys: []
_clipFromSample:
position: {x: 0, y: 0, z: 0}
up: {x: 0, y: 0, z: 0}
forward: {x: 0, y: 0, z: 0}
color: {r: 0, g: 0, b: 0, a: 0}
size: 0
percent: 0
_clipToSample:
position: {x: 0, y: 0, z: 0}
up: {x: 0, y: 0, z: 0}
forward: {x: 0, y: 0, z: 0}
color: {r: 0, g: 0, b: 0, a: 0}
size: 0
percent: 0
_loopSamples: 0
_clipFrom: 0
_clipTo: 1
animClipFrom: 0
animClipTo: 1
multithreaded: 0
buildOnAwake: 1
buildOnEnable: 0
objects: []
_evaluateOffset: 0
_spawnMethod: 0
_spawnCount: 0
_retainPrefabInstancesInEditor: 1
_objectPositioning: 0
_iteration: 0
_randomSeed: 1
_minOffset: {x: 0, y: 0, z: 0}
_maxOffset: {x: 0, y: 0, z: 0}
_offsetUseWorldCoords: 0
_minRotation: {x: 0, y: 0, z: 0}
_maxRotation: {x: 0, y: 0, z: 0}
_uniformScaleLerp: 1
_minScaleMultiplier: {x: 1, y: 1, z: 1}
_maxScaleMultiplier: {x: 1, y: 1, z: 1}
_shellOffset: 0
_applyRotation: 0
_rotateByOffset: 0
_applyScale: 0
_objectMethod: 0
delayedSpawn: 0
spawnDelay: 0.1
lastChildCount: 0
lastPointCount: 0
spawned: []
_useCustomObjectDistance: 0
_minObjectDistance: 0
_maxObjectDistance: 0
_customOffsetRule: {fileID: 0}
_customRotationRule: {fileID: 0}
_customScaleRule: {fileID: 0}
--- !u!114 &5649858463350041749
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4179914404395760092}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bb900d6a5df01384481372f1fdeca79f, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Ichni.RhythmGame.ObjectTracker
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes:
- Name: elementGuid
Entry: 2
Data: 00000000000000000000000000000000
- Name: submoduleList
Entry: 7
Data: 0|System.Collections.Generic.List`1[[Ichni.RhythmGame.SubmoduleBase,
Assembly-CSharp]], mscorlib
- Name:
Entry: 12
Data: 0
- Name:
Entry: 13
Data:
- Name:
Entry: 8
Data:
elementName:
tags: []
parentElement: {fileID: 0}
childElementList: []
track: {fileID: 0}
objectController: {fileID: 3144517396823347096}
objectPrefab: {fileID: 0}
themeBundleName:
objectName:
playTime: 0
stopTime: 0
spawnCount: 0
positionOffsetMin: {x: 0, y: 0}
positionOffsetMax: {x: 0, y: 0}
customPositionRuleName:
applyRotationOffset: 0
rotationOffsetMin: {x: 0, y: 0, z: 0}
rotationOffsetMax: {x: 0, y: 0, z: 0}
customRotationRuleName:
applyScaleOffset: 0
scaleOffsetMin: {x: 1, y: 1, z: 1}
scaleOffsetMax: {x: 1, y: 1, z: 1}
customScaleRuleName:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f0debcd3b25df1546b976a04672d2e6f
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1501ffe885c2449438946c44a354b79d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: af6f1decfdfe6064587ed356fcd11cc8
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,5 @@
+V#*‡x­©Ý/,…²þŽ”¤œ!ý
ºdIŸ]íœìâ<66>þ0a¯± 
®•¸00‰æÎ
E£'7“ªÚÛš
d`Þ¤•AßžsB.œ¼Ô_ PbÏAõá!<;‡Û‰ØqŸ1

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c7701c20e06b8aa42ba4ef23f0e03491
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
NΌS,gA-G)Κ9TT/»>’ωΤ?σ <CF83>L,./<2F>η/Q£qlΜF>l$<24>*έΉ<CEAD>kΕ<6B>F=‹Λί <20>Τ“\πόmβ(><3E>!°-sβrv(΄
1(:<Μφ~ήόz:ί ξ¬<CEBE>  ic-3Π Ν<04>KΫΠρ<CEA0><EFBFBD><E280A1>Ο^<έδs
ΑUοΰq•π
D\―pp&±FάPηRQ\θκΘ¨ ύ*|†U™o‰¥‰ Υ™)όόμΚΖμG<_nγι!Έώ]N…•UΟμ¦&²„λΙC3!Σ$f­φ…®<66>a•3ύΡ!³Φ}ν‘Π.jKΠ­?]

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 25c731803add7274989b91013dff8639
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1 @@
ΞF―<EFBFBD>Θ8uη<EFBFBD>ήΪτΥήg¤ §†)RΏ—΅Ώ•Χ ϋ>+­2½1εr¶P σPΦ‰8IΧΩ­Q5“V<E2809C>[#<23>D<E2809A>½z™ kzΞ‰aΰ9'υΦθψ?Β,<2C><63>G}Ψ<>#fκπ z lόM]DD<44>α²μΧZΓXψjR1ΰήιψ8σΔ0µΟ<C2B5>ς΅ΣV®Α<C2AE>®ϊν³µ<C2B3>μ<EFBFBD><CEBC>³­΄LθM#VτΑν<CE91>1ή}£P„w_@?Ή•O~7&S9<><39>λ(ωθ°<µg£γ;ΠΑΝ>μ „j

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cb76f0f963b6e3746972c79fa7d00eb6
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1 +1,2 @@
à"朤öï““ÌASãÙ<C3A3>ŠI§+;ø6ûÝ,¿?¼"yÛ”ÙXbøû3ƒMO<4D>¨úlB ¨J˜æiAõ*q&_áŽzyþÉ);ã¬"%15v¤åÆoHÄRõÞ×u«|maƒUÅ]äôˆÖ;ž֌ͅS]Å<1E>[A«A,ÞS.ÊâŠÝ<¡q2fOÙ¥¡XU™¬ùãR2“*S+ÀSqÃÀàôæ OôÍù5|ü_@4Uæï<C3A6>È-[W!ª5† ÷Ž¥ªS¢
и┴Щ┌╒}┴cА~щ╙⌡╜а╛┐╣╝уЪoU╦#▌яb1h><╒I≈wЪwИ╩╩Иов═з}╟╟▐!М┘ящ╔гСШмЯi`Б·:е╞■ь1╝╙@╙X║MГj├V
б~и#≥r9└╚qвзОР©▌Ю╖j[{л⌠Ъ┌И╝╞_ФСs▒ь┌⌠Л╬кuz≈╣╓нЛ╠╥гёугЯ√ШKbp╥iй┌;▄бЬЭЛ

Binary file not shown.

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Ichni.RhythmGame.Beatmap
{
public class ObjectTracker_BM : GameElement_BM
{
public string themeBundleName;
public string objectName;
public float playTime;
public float stopTime;
public int spawnCount;
public Vector2 positionOffsetMin;
public Vector2 positionOffsetMax;
public string customPositionRuleName;
public bool applyRotationOffset;
public Vector3 rotationOffsetMin;
public Vector3 rotationOffsetMax;
public string customRotationRuleName;
public bool applyScaleOffset;
public Vector3 scaleOffsetMin;
public Vector3 scaleOffsetMax;
public string customScaleRuleName;
public ObjectTracker_BM()
{
}
public ObjectTracker_BM(string elementName, Guid elementGuid, List<string> tags, GameElement_BM attachedElement,
string themeBundleName, string objectName,
float playTime, float stopTime,
int spawnCount,
Vector2 positionOffsetMin, Vector2 positionOffsetMax, string customPositionRuleName,
bool applyRotationOffset, Vector3 rotationOffsetMin, Vector3 rotationOffsetMax, string customRotationRuleName,
bool applyScaleOffset, Vector3 scaleOffsetMin, Vector3 scaleOffsetMax, string customScaleRuleName)
: base(elementName, elementGuid, tags, attachedElement)
{
this.themeBundleName = themeBundleName;
this.objectName = objectName;
this.playTime = playTime;
this.stopTime = stopTime;
this.spawnCount = spawnCount;
this.positionOffsetMin = positionOffsetMin;
this.positionOffsetMax = positionOffsetMax;
this.customPositionRuleName = customPositionRuleName;
this.applyRotationOffset = applyRotationOffset;
this.rotationOffsetMin = rotationOffsetMin;
this.rotationOffsetMax = rotationOffsetMax;
this.customRotationRuleName = customRotationRuleName;
this.applyScaleOffset = applyScaleOffset;
this.scaleOffsetMin = scaleOffsetMin;
this.scaleOffsetMax = scaleOffsetMax;
this.customScaleRuleName = customScaleRuleName;
}
public override void ExecuteBM()
{
matchedElement = ObjectTracker.GenerateElement(
elementName, elementGuid, tags, false,
GetElement(attachedElementGuid) as Track,
themeBundleName, objectName,
spawnCount,
positionOffsetMin, positionOffsetMax, customPositionRuleName,
applyRotationOffset, rotationOffsetMin, rotationOffsetMax, customRotationRuleName,
applyScaleOffset, scaleOffsetMin, scaleOffsetMax, customScaleRuleName);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 70dc1a61f3299c642b9c9775d8446595

View File

@@ -57,16 +57,23 @@ namespace Ichni.RhythmGame
gameCamera.orthographicSize = orthographicSize;
gameCamera.cameraTransform = gameCamera.transform;
float ratioDifference = UIManager.GetScreenRatio() - UIManager.StandardRatio;
if (ratioDifference > 0)
float currentAspect = UIManager.GetScreenRatio();
float targetAspect = UIManager.StandardRatio;
if (currentAspect < targetAspect)
{
//gameCamera.perspectiveOffset = 12.5f * ratioDifference;
// 屏幕比较方 (如 4:3 平板),需要增加垂直 FOV 以保持 16:9 标准下的水平绝对视野
float hFovRad = 2.0f * Mathf.Atan(Mathf.Tan(perspectiveAngle * Mathf.Deg2Rad / 2.0f) * targetAspect);
float newVFovRad = 2.0f * Mathf.Atan(Mathf.Tan(hFovRad / 2.0f) / currentAspect);
// 设置 perspectiveOffset 以完美补足被裁切的空间
gameCamera.perspectiveOffset = (newVFovRad * Mathf.Rad2Deg) - perspectiveAngle;
}
else
{
gameCamera.perspectiveOffset = -25f * ratioDifference;
// 屏幕比较长 (如 20:9 手机),维持原有垂直 FOV左右延展屏幕用于渲染环境背景
gameCamera.perspectiveOffset = 0f;
}
gameCamera.RefreshFOV();
return gameCamera;
}

View File

@@ -7,7 +7,7 @@ using UnityEngine;
namespace Ichni.RhythmGame
{
public abstract partial class GameElement : SerializedMonoBehaviour, IBaseElement, IComparable<GameElement>
public abstract partial class GameElement : SerializedMonoBehaviour, IBaseElement, IComparable<GameElement>, IScheduledElement
{
#region [] Essential & Tracking Info
//物体名
@@ -145,6 +145,19 @@ namespace Ichni.RhythmGame
return HierarchyPriority.CompareTo(other.HierarchyPriority);
}
#endregion
#region [] IScheduledElement Implementation
/// <summary>
/// 由 ElementUpdateScheduler 在对应阶段调用。
/// 子类按需重写以实现特定阶段的更新逻辑。
/// </summary>
public virtual void ScheduledUpdate(UpdatePhase phase, float songTime) { }
/// <summary>
/// 元素是否处于活跃状态。调度器跳过非活跃元素以节省开销。
/// </summary>
public virtual bool IsScheduledActive => isActiveAndEnabled;
#endregion
}
#region [] Editor Interaction & Interfaces Overrides

View File

@@ -1,5 +1,137 @@
using System;
using System.Collections.Generic;
using Dreamteck.Splines;
using UnityEngine;
public class ObjectTracker : MonoBehaviour
namespace Ichni.RhythmGame
{
public partial class ObjectTracker : GameElement
{
#region [] Property Caches
public Track track;
public ObjectController objectController;
public GameObject objectPrefab;
private List<string> themeBundleList;
private List<string> objectNameList;
public string themeBundleName;
public string objectName;
public float playTime;
public float stopTime;
public int spawnCount;
public Vector2 positionOffsetMin = Vector2.zero;
public Vector2 positionOffsetMax = Vector2.zero;
public string customPositionRuleName;
public bool applyRotationOffset = false;
public Vector3 rotationOffsetMin = Vector3.zero;
public Vector3 rotationOffsetMax = Vector3.zero;
public string customRotationRuleName;
public bool applyScaleOffset = false;
public Vector3 scaleOffsetMin = Vector3.one;
public Vector3 scaleOffsetMax = Vector3.one;
public string customScaleRuleName;
#endregion
#region [] Generation & Initialization
public static ObjectTracker GenerateElement(string elementName, Guid id, List<string> tags,
bool isFirstGenerated, Track track, string themeBundleName, string objectName, int spawnCount,
Vector2 positionOffsetMin, Vector2 positionOffsetMax, string customPositionRuleName,
bool applyRotationOffset, Vector3 rotationOffsetMin, Vector3 rotationOffsetMax, string customRotationRuleName,
bool applyScaleOffset, Vector3 scaleOffsetMin, Vector3 scaleOffsetMax, string customScaleRuleName)
{
ObjectTracker objectTracker = Instantiate(GameManager.Instance.basePrefabs.objectTracker, track.transform)
.GetComponent<ObjectTracker>();
objectTracker.objectPrefab = ThemeBundleManager.instance.GetObject<GameObject>(themeBundleName, objectName);
objectTracker.objectController.objects = new[] { objectTracker.objectPrefab };
objectTracker.Initialize(elementName, id, tags, isFirstGenerated, track);
objectTracker.track = track;
objectTracker.objectController.spline = track.trackPathSubmodule.path;
objectTracker.themeBundleList = ThemeBundleManager.instance.loadedThemeBundleList.ConvertAll(x => x.themeBundleName);
objectTracker.objectNameList = new List<string>();
objectTracker.themeBundleName = themeBundleName;
objectTracker.objectName = objectName;
objectTracker.SetSpawnSettings(spawnCount,
positionOffsetMin, positionOffsetMax, customPositionRuleName,
applyRotationOffset, rotationOffsetMin, rotationOffsetMax, customRotationRuleName,
applyScaleOffset, scaleOffsetMin, scaleOffsetMax, customScaleRuleName);
if (isFirstGenerated) objectTracker.AfterInitialize();
return objectTracker;
}
public override void AfterInitialize()
{
base.AfterInitialize();
// 向 ElementUpdateScheduler 注册 Phase.TrackFollower
CoreServices.UpdateScheduler.Register(UpdatePhase.TrackFollower, this);
}
public void SetSpawnSettings(int spawnCount,
Vector2 positionOffsetMin, Vector2 positionOffsetMax, string customPositionRuleName,
bool applyRotationOffset, Vector3 rotationOffsetMin, Vector3 rotationOffsetMax, string customRotationRuleName,
bool applyScaleOffset, Vector3 scaleOffsetMin, Vector3 scaleOffsetMax, string customScaleRuleName)
{
this.spawnCount = spawnCount;
this.positionOffsetMin = positionOffsetMin;
this.positionOffsetMax = positionOffsetMax;
this.customPositionRuleName = customPositionRuleName;
this.applyRotationOffset = applyRotationOffset;
this.rotationOffsetMin = rotationOffsetMin;
this.rotationOffsetMax = rotationOffsetMax;
this.customRotationRuleName = customRotationRuleName;
this.applyScaleOffset = applyScaleOffset;
this.scaleOffsetMin = scaleOffsetMin;
this.scaleOffsetMax = scaleOffsetMax;
this.customScaleRuleName = customScaleRuleName;
objectController.spawnCount = spawnCount;
objectController.minOffset = positionOffsetMin;
objectController.maxOffset = positionOffsetMax;
objectController.applyRotation = applyRotationOffset;
objectController.minRotation = rotationOffsetMin;
objectController.maxRotation = rotationOffsetMax;
objectController.applyScale = applyScaleOffset;
objectController.minScaleMultiplier = scaleOffsetMin;
objectController.maxScaleMultiplier = scaleOffsetMax;
objectController.Spawn();
}
#endregion
#region [] Update
/// <summary>
/// IScheduledElement 实现:在 Phase.TrackFollower 阶段控制 objectController 的启用/禁用。
/// </summary>
public override void ScheduledUpdate(UpdatePhase phase, float songTime)
{
if (phase == UpdatePhase.TrackFollower)
{
if (playTime > songTime || stopTime < songTime)
{
if (objectController.enabled)
objectController.enabled = false;
}
else
{
if (!objectController.enabled)
{
objectController.enabled = true;
objectController.Spawn();
}
}
}
}
public override void OnDelete()
{
CoreServices.UpdateScheduler.Unregister(UpdatePhase.TrackFollower, this);
}
#endregion
}
}

View File

@@ -21,6 +21,7 @@ public partial class BasePrefabsCollection : SerializedScriptableObject
public GameObject crossTrackPoint;
public GameObject trackPercentPoint;
public GameObject trackHeadPoint;
public GameObject objectTracker;
[Title("Trail相关")] public GameObject trail;
public Material defaultTrailMaterial;

View File

@@ -44,7 +44,7 @@ namespace Ichni.UI
AudioManager.Post(AK.EVENTS.TOUCHTOSTART);
// 已有登录缓存 → 跳过 LoginPage直接进入章节选择
if (LoginCacheManager.HasValidSession)
//if (LoginCacheManager.HasValidSession)
{
FadeOut();
floatingParticles.GetComponent<Renderer>().material.DOColor(Color.clear, "_BaseColor", 0.5f).Play();

Binary file not shown.

View File

@@ -60,7 +60,10 @@ namespace Echovoid.Runtime.Behavior.Rendering
public static readonly int _BloomParams = Shader.PropertyToID("_BloomParams");
public static readonly int _BloomTint = Shader.PropertyToID("_BloomTint");
public static readonly int _AnimeBloom_BlurRadius = Shader.PropertyToID("_BlurRadius");
public static readonly int _AnimeBloom_KernelScale = Shader.PropertyToID("_KernelScale");
public static readonly int _BloomTex = Shader.PropertyToID("_BloomTex");
public static readonly int _SourceTexLowMip = Shader.PropertyToID("_SourceTexLowMip");
public static readonly int _BloomScatterParams = Shader.PropertyToID("_BloomScatterParams");
// --- Anime ACES ---
public static readonly int _TonemapParams = Shader.PropertyToID("_TonemapParams");

View File

@@ -9,118 +9,170 @@ Shader "SLS/Postprocessing/AnimeBloom"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
// --- 参数定义 ---
float4 _BloomParams; // x: Intensity, y: Threshold, z: SoftKnee, w: Clamp
float4 _BloomTint; // 泛光染色
float _BlurRadius; // 模糊扩散半径 (控制光晕大小的关键)
// =====================================================
// 参数定义(与 Unity 原生 Bloom.shader 对齐)
// =====================================================
// _BloomScatterParams:
// x: scatter (已经过 C# 侧 Lerp(0.05, 0.95, userValue) 映射)
// y: clamp (最大亮度限制,防萤火虫)
// z: threshold (线性空间C# 侧已做 GammaToLinear)
// w: thresholdKnee (= threshold * 0.5f,硬编码 soft knee)
float4 _BloomScatterParams;
#define Scatter _BloomScatterParams.x
#define ClampMax _BloomScatterParams.y
#define Threshold _BloomScatterParams.z
#define ThresholdKnee _BloomScatterParams.w
// 纹理
// Composite 阶段参数x=intensity, y/z/w=tint.rgb (已归一化亮度)
float4 _BloomParams;
float4 _BloomTint; // 保留兼容,不再使用
float _KernelScale; // 采样跨度放大乘数,默认 1.0
// SourceTexLowMip: upsample 时的"低频大光晕"纹理 (lowMip)
TEXTURE2D(_SourceTexLowMip);
SAMPLER(sampler_SourceTexLowMip);
// 最终 bloom 结果纹理(传入 Composite Pass
TEXTURE2D(_BloomTex);
SAMPLER(sampler_BloomTex);
// --- 辅助函数Prefilter (提取高亮) ---
half3 Prefilter(half3 color)
// =====================================================
// HDR 编解码(与 Unity 原生 Bloom.shader 完全一致)
// 在线性工作流下 encode/decode 是 no-op但写清楚以防 gamma 空间项目
// =====================================================
half4 EncodeHDR(half3 color)
{
float threshold = _BloomParams.y;
float softKnee = _BloomParams.z;
float clampVal = _BloomParams.w;
// 1. 限制最大亮度 (防闪烁/萤火虫噪点)
color = min(color, clampVal);
// 2. 阈值计算 (使用 Soft Knee 曲线让过渡更自然)
// 标准公式:(Brightness - Threshold) / max(Brightness, 0.0001)
// 这里使用一个更平滑的曲线版本,防止高光边缘切变太硬
float brightness = Max3(color.r, color.g, color.b);
float soft = brightness - threshold + softKnee;
soft = clamp(soft, 0, 2 * softKnee);
soft = soft * soft / (4 * softKnee + 1e-4);
float contribution = max(soft, brightness - threshold);
contribution /= max(brightness, 1e-4);
return color * contribution;
}
// --- Pass 0: Prefilter ---
half4 FragPrefilter(Varyings input) : SV_Target
{
// 采样原图
half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.texcoord);
// 提取高亮
half3 bloom = Prefilter(color.rgb);
return half4(bloom, 1.0);
}
// --- Pass 1: Downsample (Kawase 4-Tap) ---
// 降采样:取 4 个对角像素的平均值,范围随分辨率降低而扩大
half4 FragDownsample(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
float4 texelSize = _BlitTexture_TexelSize;
// 原生的 Kawase Offset 是 1.0(即 0.5 个对角像素距离),
// 这里加入 _BlurRadius 按比例扩大步幅,能够使用 3 次迭代跑出原版 6 次的扩散面积
float spreadOffset = 1.0 + _BlurRadius * 0.5;
float2 offset = texelSize.xy * spreadOffset;
half3 c0 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - offset).rgb;
half3 c1 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(offset.x, -offset.y)).rgb;
half3 c2 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - float2(offset.x, -offset.y)).rgb;
half3 c3 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + offset).rgb;
half3 color = (c0 + c1 + c2 + c3) * 0.25;
#if UNITY_COLORSPACE_GAMMA
color = sqrt(color);
#endif
return half4(color, 1.0);
}
TEXTURE2D(_BloomMipDown);
SAMPLER(sampler_BloomMipDown);
// --- Pass 2: Upsample (Kawase 4-Tap + Scatter) ---
half4 FragUpsample(Varyings input) : SV_Target
half3 DecodeHDR(half4 data)
{
float2 uv = input.texcoord;
float4 texelSize = _BlitTexture_TexelSize;
// 原生的 Kawase 升采样偏移是 0.5。
// 配合宽幅降采样,这里稍微放大一点点就可以获得非常柔顺的大面积泛光
float spreadOffset = 0.5 + _BlurRadius * 0.2;
float2 offset = texelSize.xy * spreadOffset;
// 4-Tap 从更低分辨率纹理 (Up[i+1]) 采样外围光晕
half3 c0 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - offset * float2(1, 1)).rgb;
half3 c1 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + offset * float2(1, -1)).rgb;
half3 c2 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - offset * float2(1, -1)).rgb;
half3 c3 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + offset * float2(1, 1)).rgb;
half3 lowRes = (c0 + c1 + c2 + c3) * 0.25;
// 采样当前层级的高清纹理 (Down[i])
half3 highRes = SAMPLE_TEXTURE2D_X(_BloomMipDown, sampler_LinearClamp, uv).rgb;
// 【完全复刻 Unity 原生逻辑】: highRes + lowRes * scatter
// 这既保证了光晕向外漫射(低迭代扩散广),又保证了核心亮区的能量守恒!
float scatter = saturate(_BlurRadius);
half3 bloom = highRes + lowRes * scatter;
return half4(bloom, 1.0);
half3 color = data.xyz;
#if UNITY_COLORSPACE_GAMMA
color *= color;
#endif
return color;
}
// --- Pass 3: Composite (最终合成) ---
// =====================================================
// Pass 0: Prefilter — 提取超过阈值的高亮像素
// 与原生完全一致的 soft knee 公式
// =====================================================
half4 FragPrefilter(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
half3 color = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv));
// 1. 亮度钳制(防止极亮萤火虫像素跳变)
color = min(color, ClampMax);
// 2. Soft Knee 阈值(与 Unity 原生公式完全一致)
half brightness = Max3(color.r, color.g, color.b);
half softness = clamp(brightness - Threshold + ThresholdKnee, 0.0, 2.0 * ThresholdKnee);
softness = (softness * softness) / (4.0 * ThresholdKnee + 1e-4);
half multiplier = max(brightness - Threshold, softness) / max(brightness, 1e-4);
color *= multiplier;
// 3. 防止 NaN 传播(负值在 EncodeHDR sqrt 时会产生 NaN
color = max(color, 0);
return EncodeHDR(color);
}
// =====================================================
// Pass 1: Dual Kawase Downsample
// 公式1/8 * (center*4 + 4corners)
// 采样次数5次相当于 3×3 box + 双线性权重,效果非常柔和)
// 与 Unity 原生 Bloom.shader FragDualDownsample 完全一致
// =====================================================
half4 FragDualDownsample(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
// 乘以 _KernelScale在极低迭代次数下强制向外大跨步拉扯光晕
float2 ts = _BlitTexture_TexelSize.xy * _KernelScale;
// 中心点(权重 4
half3 c0 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv));
// 4 个对角偏移各 0.5 像素(恰好落在 4 像素的双线性插值中心)
half3 c1 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2( 0.5, 0.5) * ts));
half3 c2 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(-0.5, 0.5) * ts));
half3 c3 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(-0.5, -0.5) * ts));
half3 c4 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2( 0.5, -0.5) * ts));
// 加权平均:(c0*4 + c1+c2+c3+c4) / 8
half3 color = (1.0 / 8.0) * (c0 * 4.0 + c1 + c2 + c3 + c4);
return EncodeHDR(color);
}
// =====================================================
// Pass 2: Dual Kawase Upsample + Energy-Conserving Lerp
//
// 关键核心:与 Unity 原生 FragUpsample 一致的混合公式
// result = lerp(highMip, lowMip, Scatter)
//
// highMip (_BlitTexture) = 当前层的降采样纹理 Down[i](高分辨率,细节)
// lowMip (_SourceTexLowMip) = 上一层的升采样结果 Up[i+1](低分辨率,扩散光晕)
//
// lerp 是能量守恒的:总亮度 = (1-s)*high + s*low永远 ≤ max(high,low)
// 这是防止"死白"的数学保证。
// =====================================================
half4 FragDualUpsample(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
// 同样乘以 _KernelScale
float2 ts = _BlitTexture_TexelSize.xy * _KernelScale;
// Dual Kawase 8-tap 升采样4 对角 + 4 正交
// 正交距离 = 1.0 texel对角距离 = 0.5 texel与原生完全一致
half3 c1 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2( 0.5, 0.5) * ts));
half3 c2 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(-0.5, 0.5) * ts));
half3 c3 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(-0.5, -0.5) * ts));
half3 c4 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2( 0.5, -0.5) * ts));
half3 c5 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2(-1.0, 0.0) * ts));
half3 c6 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2( 1.0, 0.0) * ts));
half3 c7 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2( 0.0, 1.0) * ts));
half3 c8 = DecodeHDR(SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + float2( 0.0, -1.0) * ts));
// 加权平均 (4角×2 + 4正交×1) / 12产生柔和的钟形分布
half3 highMip = (1.0 / 12.0) * ((c1 + c2 + c3 + c4) * 2.0 + c5 + c6 + c7 + c8);
// lowMip = 来自更低分辨率层Up[i+1])的扩散光晕,双线性采样
half3 lowMip = DecodeHDR(SAMPLE_TEXTURE2D(_SourceTexLowMip, sampler_LinearClamp, uv));
// 【核心公式,与原生完全一致】
// Scatter 控制光晕扩散程度0=只有当前层细节1=完全使用模糊层
// 始终能量守恒,绝对不会产生死白
half3 result = lerp(highMip, lowMip, Scatter);
return EncodeHDR(result);
}
// =====================================================
// Pass 3: Composite — 将最终 bloom 叠加回原始画面
// _BloomParams.x = intensity
// _BloomParams.yzw = tint.rgb (亮度已归一化,仅携带色相/饱和度)
// =====================================================
half4 FragComposite(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
// 原始画面
// 原始 HDR 画面
half4 baseColor = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
// 泛光结果 (经过多次升采样后的最终纹理)
half3 bloom = SAMPLE_TEXTURE2D(_BloomTex, sampler_BloomTex, uv).rgb;
// 应用强度和染色
bloom *= _BloomParams.x * _BloomTint.rgb;
// Bloom 纹理(已经过多轮 Dual Kawase 升降采样)
half3 bloom = DecodeHDR(SAMPLE_TEXTURE2D(_BloomTex, sampler_BloomTex, uv));
// 叠加 (Additive)
// 也可以尝试 Screen 混合模式让光变得更柔和,但 Additive 最符合物理发光
// 应用强度和染色_BloomParams.yzw 是亮度归一化的 tint
float intensity = _BloomParams.x;
half3 tint = _BloomParams.yzw;
bloom *= intensity * tint;
// Additive 合并(物理正确的发光叠加)
return half4(baseColor.rgb + bloom, baseColor.a);
}
@@ -129,9 +181,9 @@ Shader "SLS/Postprocessing/AnimeBloom"
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
ZWrite Off Cull Off
ZWrite Off Cull Off ZTest Always
// 0: Prefilter
// Pass 0: Prefilter
Pass
{
Name "Bloom Prefilter"
@@ -141,28 +193,27 @@ Shader "SLS/Postprocessing/AnimeBloom"
ENDHLSL
}
// 1: Downsample
// Pass 1: Dual Kawase Downsample
Pass
{
Name "Bloom Downsample"
Name "Bloom Dual Downsample"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragDownsample
#pragma fragment FragDualDownsample
ENDHLSL
}
// 2: Upsample
// Pass 2: Dual Kawase Upsample + lerp(high, low, scatter)
Pass
{
Name "Bloom Upsample"
// 已在内部 Lerp无需外部 Additive Blend
Name "Bloom Dual Upsample"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragUpsample
#pragma fragment FragDualUpsample
ENDHLSL
}
// 3: Composite
// Pass 3: Composite
Pass
{
Name "Bloom Composite"

View File

@@ -8,39 +8,50 @@ namespace SLSUtilities.Rendering.PostProcessing
[System.Serializable, VolumeComponentMenu("SLS/Postprocessing/Anime Bloom")]
public class AnimeBloom : ScriptablePostProcessorVolume
{
// 【核心修改】:放在 ToneMapping 之前执行BeforePostProcess配合 ACES 才能彻底解决“高光泛白”问题
// BeforePostProcess: 在 ToneMapping 之前运行,确保 HDR bloom 颜色不被截断
// 这与 Unity 原生 Bloom 的执行阶段完全一致
public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.BeforePostProcess;
public override int OrderInInjectionPoint => 5; // 放在 Vignette 之前
public override int OrderInInjectionPoint => 5;
[Header("Glow Settings")]
[Tooltip("泛光强度。值越大越亮。")]
public ClampedFloatParameter intensity = new(0f, 0f, 10f);
public ClampedFloatParameter intensity = new(1f, 0f, 10f);
[Tooltip("阈值。亮度超过此值的像素才会发光。\n关键设为 1.1 可以过滤掉白墙(1.0),只让灯光发光。")]
public MinFloatParameter threshold = new(1.1f, 0f); // 默认设为 1.1
// 注意threshold 在 Gamma 空间中设置C# 侧会转为 Linear。
// 0.9 在 gamma 空间 ≈ 0.79 在 linear 空间,这与 Unity 原生默认行为完全一致。
public MinFloatParameter threshold = new(0.9f, 0f);
[Tooltip("柔膝 (Soft Knee)。让阈值过渡更平滑,避免高光边缘有硬切痕迹。")]
public ClampedFloatParameter softKnee = new(0.5f, 0f, 1f);
[Header("Scatter")]
// scatter 在 [0,1] 范围内由用户设置。
// C# 侧映射Mathf.Lerp(0.05f, 0.95f, scatter.value),与 Unity 原生完全一致。
// 低值 = 光晕聚拢,高值 = 光晕向外大范围扩散。
public ClampedFloatParameter scatter = new(0.7f, 0f, 1f);
[Tooltip("最大亮度钳制。防止极亮像素(如太阳)产生乱跳的噪点。")]
public MinFloatParameter clamp = new(65472f, 1f); // 默认很大,基本不限制
public MinFloatParameter clamp = new(65472f, 1f);
[Header("Anime Style")]
[Tooltip("扩散半径。稍微增加扩展范围来弥补低迭代带来的发光不足。")]
public ClampedFloatParameter scatter = new(0.85f, 0f, 5f); // 推荐增量以适应低迭代
[Header("Iterations")]
// 迭代次数Dual Kawase 每层只需 1 Pass性能远低于原生双趟高斯。
// 4 次迭代 Dual Kawase ≈ 原生 6 次双趟高斯的扩散范围,但 Pass 数只有其 1/3。
public ClampedIntParameter diffusion = new(4, 1, 8);
[Tooltip("迭代次数。针对移动端带宽优化,建议控制在 2~3 次。")]
public ClampedIntParameter diffusion = new(3, 1, 4); // 移动端性能优化最高4次
[Header("Optimization (Mobile)")]
[Tooltip("初始降采样倍率(首趟大幅压缩分辨率)。\n1=原生半分辨率(1/2)2=1/4分辨率3=1/8分辨率。\n调大此值可用极低的迭代次数跑出巨大且柔和的光晕大幅节省 GPU 带宽。")]
public ClampedIntParameter initialDownscaleShift = new(1, 1, 3);
[Tooltip("采样偏移跨度放大Kernel Scale。\n1.0=原生标准跨度。适度放大(如1.2-1.5)可拉扯光晕扩散范围,但过大会产生轻微十字星/方格马赛克。")]
public ClampedFloatParameter kernelScale = new(1.0f, 0.5f, 3.0f);
[Tooltip("泛光染色。可以做粉色霓虹、蓝色科技光等效果。")]
[Header("Tint")]
// tint 染色C# 侧会归一化亮度(只保留色相/饱和度),与原生行为一致。
// 这样调整 tint 颜色不会意外改变 bloom 总亮度。
public ColorParameter tint = new(Color.white, true, true, true);
// 内部使用的 RT 数组
private RTHandle[] _bloomPyramidUp;
private RTHandle[] _bloomPyramidDown;
private const int k_MaxPyramidSize = 6; // 减少不必要的最大数组长度
// 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)
{
@@ -50,86 +61,123 @@ namespace SLSUtilities.Rendering.PostProcessing
desc.msaaSamples = 1;
desc.depthBufferBits = 0;
// 1. 设置参数
Vector4 bloomParams = new Vector4(intensity.value, threshold.value, softKnee.value, clamp.value);
material.SetVector(InternalShaderHelpers.ID._BloomParams, bloomParams);
material.SetVector(InternalShaderHelpers.ID._BloomTint, tint.value);
material.SetFloat(InternalShaderHelpers.ID._BlurRadius, scatter.value);
// ─────────────────────────────────────────────────────────────
// 1. 参数打包(完全对齐 Unity SetupBloom 的计算逻辑)
// ─────────────────────────────────────────────────────────────
// 2. 初始化金字塔数组
int iterations = Mathf.Clamp(diffusion.value, 1, k_MaxPyramidSize);
// 确保 RT 数组大小足够
if (_bloomPyramidUp == null || _bloomPyramidUp.Length != k_MaxPyramidSize)
// 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));
material.SetFloat(InternalShaderHelpers.ID._AnimeBloom_KernelScale, kernelScale.value);
// ─────────────────────────────────────────────────────────────
// 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);
if (_bloomMipDown == null || _bloomMipDown.Length != k_MaxMips)
{
_bloomPyramidUp = new RTHandle[k_MaxPyramidSize];
_bloomPyramidDown = new RTHandle[k_MaxPyramidSize];
_bloomMipDown = new RTHandle[k_MaxMips];
_bloomMipUp = new RTHandle[k_MaxMips];
}
// 3. Prefilter Pass (提取高亮)
// 先降一半分辨率,节省性能且增加模糊感
desc.width = Mathf.Max(1, desc.width >> 1);
desc.height = Mathf.Max(1, desc.height >> 1);
RenderingUtils.ReAllocateIfNeeded(ref _bloomPyramidDown[0], desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_BloomMipDown0");
RenderingUtils.ReAllocateIfNeeded(ref _bloomPyramidUp[0], desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_BloomMipUp0");
// ─────────────────────────────────────────────────────────────
// 4. Prefilter提取高亮像素 + 初始激进降采样)
// 根据用户选择,起始分辨率可能是 1/2, 1/4 或 1/8。
// ─────────────────────────────────────────────────────────────
int shift = initialDownscaleShift.value;
desc.width = Mathf.Max(1, desc.width >> shift);
desc.height = Mathf.Max(1, desc.height >> shift);
// Source -> Down[0] (Prefilter)
Blitter.BlitCameraTexture(cmd, source, _bloomPyramidDown[0], material, 0);
RenderingUtils.ReAllocateIfNeeded(ref _bloomMipDown[0], desc, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: "_BloomDown0");
// 4. Downsample Loop (降采样金字塔)
int lastDown = 0;
for (int i = 1; i < iterations; i++)
// Source → Down[0] via Pass 0 (Prefilter)
Blitter.BlitCameraTexture(cmd, source, _bloomMipDown[0], material, 0);
// ─────────────────────────────────────────────────────────────
// 5. Downsample LoopDual Kawase 降采样金字塔Pass 1
// 每次分辨率减半,产生多层不同尺度的模糊纹理
// ─────────────────────────────────────────────────────────────
for (int i = 1; i < mipCount; i++)
{
// 每次分辨率减半
desc.width = Mathf.Max(1, desc.width >> 1);
desc.width = Mathf.Max(1, desc.width >> 1);
desc.height = Mathf.Max(1, desc.height >> 1);
RenderingUtils.ReAllocateIfNeeded(ref _bloomPyramidDown[i], desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_BloomMipDown" + i);
RenderingUtils.ReAllocateIfNeeded(ref _bloomPyramidUp[i], desc, FilterMode.Bilinear, TextureWrapMode.Clamp, name: "_BloomMipUp" + i);
RenderingUtils.ReAllocateIfNeeded(ref _bloomMipDown[i], desc, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: "_BloomDown" + i);
// Down[i-1] -> Down[i]
Blitter.BlitCameraTexture(cmd, _bloomPyramidDown[i - 1], _bloomPyramidDown[i], material, 1);
lastDown = i;
// Down[i-1] Down[i] via Pass 1 (Dual Kawase Downsample)
Blitter.BlitCameraTexture(cmd, _bloomMipDown[i - 1], _bloomMipDown[i], material, 1);
}
// 5. Upsample Loop (升采样并混合)
// 从最小的一张开始,往上叠加
// 先把最小的 Down 直接拷给 Up
Blitter.BlitCameraTexture(cmd, _bloomPyramidDown[lastDown], _bloomPyramidUp[lastDown]);
// ─────────────────────────────────────────────────────────────
// 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 纹理
// ─────────────────────────────────────────────────────────────
for (int i = lastDown - 1; i >= 0; i--)
// 分配所有 Up RT从最大 mip 往上,分辨率逐步翻倍)
// 由于 desc 已经在 downsample 中不断减半,我们要重新从 Down[i] 读尺寸
for (int i = mipCount - 1; i >= 0; i--)
{
// 设置上一级 Up 为输入
// Upsample Pass 会混合Up[i+1] (Blur) + Down[i] (High Res Detail)
// 这里我们稍微简化逻辑:直接把 Up[i+1] 升采样并叠加到 Up[i] 上
// 第二步:把 Up[i+1] (LowRes) 和 Down[i] (HighRes) 一起传给 Shader
// 在 Shader 内部执行 Lerp 能量守恒融合,彻底替代会导致死白的 Additive 叠加模式
material.SetTexture("_BloomMipDown", _bloomPyramidDown[i]);
Blitter.BlitCameraTexture(cmd, _bloomPyramidUp[i + 1], _bloomPyramidUp[i], material, 2);
RenderingUtils.ReAllocateIfNeeded(ref _bloomMipUp[i],
_bloomMipDown[i].rt.descriptor, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: "_BloomUp" + i);
}
// 6. Composite (合成)
// 此时 _bloomPyramidUp[0] 包含了最终的泛光纹理
material.SetTexture(InternalShaderHelpers.ID._BloomTex, _bloomPyramidUp[0]);
// Source + BloomTex -> Destination
// 最底层lowMip = Down[last]highMip 也是 Down[last](无 low 可用,直接 lerp 自身,结果仍 = Down[last]
// 简化做法:直接将 Down[last] 拷到 Up[last] 作为起点
Blitter.BlitCameraTexture(cmd, _bloomMipDown[mipCount - 1], _bloomMipUp[mipCount - 1]);
// 从第二底层开始向上 upsample
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, 2);
}
// ─────────────────────────────────────────────────────────────
// 7. Composite将 bloom 叠加回原始画面Pass 3
// ─────────────────────────────────────────────────────────────
material.SetTexture(InternalShaderHelpers.ID._BloomTex, _bloomMipUp[0]);
Blitter.BlitCameraTexture(cmd, source, destination, material, 3);
}
// 清理 RT
public void Dispose()
{
if (_bloomPyramidDown != null)
if (_bloomMipDown == null) return;
for (int i = 0; i < _bloomMipDown.Length; i++)
{
for (int i = 0; i < _bloomPyramidDown.Length; i++)
{
if (_bloomPyramidDown[i] != null) _bloomPyramidDown[i].Release();
if (_bloomPyramidUp[i] != null) _bloomPyramidUp[i].Release();
}
_bloomMipDown[i]?.Release();
_bloomMipUp[i]?.Release();
}
}
public override bool IsActive() => intensity.value > 0f;
}
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff