diff --git a/Assets/Materials/M_HuoshanScreenGlitch.mat b/Assets/Materials/M_HuoshanScreenGlitch.mat
new file mode 100644
index 000000000..d14034182
--- /dev/null
+++ b/Assets/Materials/M_HuoshanScreenGlitch.mat
@@ -0,0 +1,47 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: M_HuoshanScreenGlitch
+ m_Shader: {fileID: 4800000, guid: 34b7b1bac3f94aae80724d2934758989, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap: {}
+ disabledShaderPasses: []
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _BloomBoost: 0.9
+ - _Chaos: 0.6
+ - _ColorAmount: 1.35
+ - _Desaturate: 0.9
+ - _Dispersion: 0.19
+ - _GlitchIntensity: 0
+ - _Grit: 1
+ - _PalPhase: 0.62
+ - _Scale: 1.6
+ - _Speed: 10
+ - _SurgeRate: 0.5
+ - _Vignette: 0.28
+ - _Warp: 1
+ m_Colors:
+ - _Color: {r: 1, g: 1, b: 1, a: 1}
+ m_BuildTextureStacks: []
diff --git a/Assets/Materials/M_HuoshanScreenGlitch.mat.meta b/Assets/Materials/M_HuoshanScreenGlitch.mat.meta
new file mode 100644
index 000000000..518d64f69
--- /dev/null
+++ b/Assets/Materials/M_HuoshanScreenGlitch.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7409d06033d4497a8aff0a45ed262adf
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
index 52e2f4e4b..27265c8e0 100644
--- a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
+++ b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
@@ -74,6 +74,40 @@ public class SpriteNoiseGlitchController : MonoBehaviour
public CompositeMode CurrentCompositeMode => _compositeMode;
+ ///
+ /// 仅对齐位置/尺寸与 sortingOrder,不改 Layer。
+ /// glitch_transition 的 noise 必须留在 Default;若改成 HuoshanScreen 会进 VolFx 叠在粒子上。
+ ///
+ public void ConfigureOverlayFor(SpriteRenderer targetRenderer, int sortingOrderOffset = 1)
+ {
+ EnsureRuntimeObjects();
+ if (_renderer == null || _renderer.sprite == null || targetRenderer == null)
+ return;
+
+ _renderer.sortingLayerID = targetRenderer.sortingLayerID;
+ _renderer.sortingOrder = targetRenderer.sortingOrder + sortingOrderOffset;
+
+ Bounds targetBounds = targetRenderer.bounds;
+ Bounds overlayBounds = _renderer.bounds;
+ if (targetBounds.size.x <= Mathf.Epsilon ||
+ targetBounds.size.y <= Mathf.Epsilon ||
+ overlayBounds.size.x <= Mathf.Epsilon ||
+ overlayBounds.size.y <= Mathf.Epsilon)
+ {
+ return;
+ }
+
+ Vector3 position = transform.position;
+ position.x = targetBounds.center.x;
+ position.y = targetBounds.center.y;
+ transform.position = position;
+
+ Vector3 scale = transform.localScale;
+ scale.x *= targetBounds.size.x / overlayBounds.size.x;
+ scale.y *= targetBounds.size.y / overlayBounds.size.y;
+ transform.localScale = scale;
+ }
+
private void Awake()
{
_renderer = GetComponent();
diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
index 92abff0cd..64553cb61 100644
--- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
+++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs
@@ -34,11 +34,31 @@ namespace AibisDream
[SerializeField, Min(1f)] private float screenBloomBrightness = 1.35f;
[Header("Glitch 噪波效果")]
- [Tooltip("噪波故障效果控制器,可选;置于 Expression View 子物体上")]
+ [Tooltip("记忆/对白阶段的透明噪波叠层(glitch_transition)。expression_screen_glitch 已改走屏幕 Shader。")]
[SerializeField] private SpriteNoiseGlitchController glitchController;
+ [Header("屏幕 Glitch Shader")]
+ [Tooltip("仅在屏幕淡入 / expression_screen_glitch 时套用;小游戏阶段保持默认 Sprite 材质,避免进 VolFx 盖住粒子")]
+ [SerializeField] private Material screenGlitchMaterial;
+ // 观感全部在材质 M_HuoshanScreenGlitch 上调(只有 6 个宏观旋钮:Scale / Color /
+ // Warp / Chaos / Grit / SurgeRate)。代码这边只驱动 _GlitchIntensity 档位。
+ // 不要再把观感参数搬回 Inspector——两处都能改就等于没有真相源,上一版 40 个属性
+ // 分散在材质和组件两边,结果谁也调不动。
+
public LogReleasePresentationController LogReleasePresentation => logReleasePresentation;
+ /// 当前屏幕 Glitch 档位(0–1),由 expression_screen_glitch 持续保持。
+ public float ScreenGlitchLevel => screenGlitchIntensity;
+
+ private static readonly int ScreenGlitchIntensityId = Shader.PropertyToID("_GlitchIntensity");
+
+ private Material screenDefaultMaterial;
+ private Material screenFallbackMaterial;
+ private Material screenGlitchMaterialInstance;
+ private Coroutine screenGlitchRampRoutine;
+ private float screenGlitchIntensity;
+ private bool screenGlitchMaterialActive;
+
void Start()
{
// 注册到系统字典
@@ -66,6 +86,13 @@ namespace AibisDream
glitchController = GetComponentInChildren(true);
}
+ // glitch_transition 用的 noise:必须停在 Default,绝不能进 HuoshanScreen(否则会叠进 VolFx 盖住粒子)。
+ PrepareNoiseOverlayForGlitchTransition(false);
+
+ CacheScreenDefaultMaterial();
+ // 小游戏阶段绝不能提前套 Glitch 材质:其 SRPDefaultUnlit 会进 VolFx huoshan 合成并盖住粒子。
+ RestoreScreenDefaultMaterial();
+
if (logReleasePresentation == null)
{
logReleasePresentation = GetComponent();
@@ -119,6 +146,15 @@ namespace AibisDream
private void HideSpriteOverlaysForMinigame()
{
+ if (screenGlitchRampRoutine != null)
+ {
+ StopCoroutine(screenGlitchRampRoutine);
+ screenGlitchRampRoutine = null;
+ }
+ screenGlitchIntensity = 0f;
+ // 粒子玩法必须回到默认 Sprite 材质,否则屏幕会进 VolFx 层把字盖掉。
+ RestoreScreenDefaultMaterial();
+
List renderers = CollectSpriteOverlayRenderers();
for (int i = 0; i < renderers.Count; i++)
{
@@ -262,6 +298,13 @@ namespace AibisDream
///
public void CloseView()
{
+ if (screenGlitchRampRoutine != null)
+ {
+ StopCoroutine(screenGlitchRampRoutine);
+ screenGlitchRampRoutine = null;
+ }
+ SetScreenGlitchIntensity(0f);
+
if (logReleasePresentation != null)
{
logReleasePresentation.ForceCleanupImmediate(true);
@@ -315,6 +358,9 @@ namespace AibisDream
sr.gameObject.SetActive(true);
sequence.Join(sr.DOFade(targetAlpha, fadeDuration).SetEase(Ease.OutQuad));
}
+ // 淡入演出屏幕时才启用 Glitch 材质;强度保持 0,外观与普通屏幕一致。
+ ActivateScreenGlitchMaterial();
+ ApplyScreenGlitchProperties();
yield return sequence.WaitForCompletion();
@@ -332,6 +378,9 @@ namespace AibisDream
if (renderers.Count == 0) yield break;
float fadeDuration = Mathf.Max(0.01f, duration);
+ // Glitch 档位是保持型的:屏幕淡出时一并收掉,免得下一段淡入时带着上一段的故障。
+ SetScreenGlitchLevel(0f, fadeDuration);
+
Sequence sequence = DOTween.Sequence().SetTarget(this);
for (int i = 0; i < renderers.Count; i++)
{
@@ -392,21 +441,220 @@ namespace AibisDream
sr.color = locked;
}
+ private void CacheScreenDefaultMaterial()
+ {
+ if (screenOverlayRenderer == null || screenDefaultMaterial != null)
+ return;
+
+ var current = screenOverlayRenderer.sharedMaterial;
+ // 若场景里误挂了 Glitch 材质,仍回退到 URP 默认 Sprite,保证小游戏可见。
+ if (current != null && current.shader != null &&
+ current.shader.name == "AibisDream/HuoshanScreenGlitch")
+ {
+ screenDefaultMaterial = null;
+ return;
+ }
+
+ screenDefaultMaterial = current;
+ }
+
+ private void RestoreScreenDefaultMaterial()
+ {
+ if (screenOverlayRenderer == null)
+ return;
+
+ CacheScreenDefaultMaterial();
+ screenGlitchMaterialActive = false;
+ screenGlitchIntensity = 0f;
+
+ if (screenDefaultMaterial != null)
+ {
+ screenOverlayRenderer.sharedMaterial = screenDefaultMaterial;
+ return;
+ }
+
+ if (screenOverlayRenderer.sharedMaterial != null &&
+ screenOverlayRenderer.sharedMaterial.shader != null &&
+ screenOverlayRenderer.sharedMaterial.shader.name == "AibisDream/HuoshanScreenGlitch")
+ {
+ // 场景直接挂了 Glitch 材质时要换掉,否则它的 SRPDefaultUnlit 会进 VolFx 合成盖住粒子。
+ // 绝不能置 null——SpriteRenderer 的 material 为 null 会渲染成品红方块,
+ // 表现为 switch_fix_system_to Expression 后闪一下粉色。
+ screenOverlayRenderer.sharedMaterial = GetScreenFallbackMaterial();
+ }
+ }
+
///
- /// Glitch 噪波过渡:从当前强度平滑过渡到 to,不瞬移(类似 DOTween)。
+ /// 场景没有提供干净的屏幕材质时的兜底:标准 Sprite 材质。
+ ///
+ private Material GetScreenFallbackMaterial()
+ {
+ if (screenFallbackMaterial != null)
+ return screenFallbackMaterial;
+
+ Shader spriteShader = Shader.Find("Sprites/Default");
+ if (spriteShader == null)
+ {
+ Debug.LogError("[ExpressionManager] 找不到 Sprites/Default;屏幕材质无法回退。", this);
+ return null;
+ }
+
+ screenFallbackMaterial = new Material(spriteShader) { name = "Screen Sprite Fallback" };
+ return screenFallbackMaterial;
+ }
+
+ private void ActivateScreenGlitchMaterial()
+ {
+ if (screenOverlayRenderer == null)
+ return;
+
+ CacheScreenDefaultMaterial();
+
+ if (screenGlitchMaterial == null)
+ {
+ Debug.LogError(
+ "[ExpressionManager] 未找到屏幕 Glitch 材质;请把 M_HuoshanScreenGlitch 赋给 screenGlitchMaterial。",
+ this);
+ return;
+ }
+
+ if (screenGlitchMaterialInstance == null)
+ {
+ screenGlitchMaterialInstance = new Material(screenGlitchMaterial)
+ {
+ name = screenGlitchMaterial.name + " (Screen Instance)"
+ };
+ }
+
+ screenOverlayRenderer.sharedMaterial = screenGlitchMaterialInstance;
+ screenGlitchMaterialActive = true;
+ ApplyScreenGlitchProperties();
+ }
+
+ private void ApplyScreenGlitchProperties()
+ {
+ if (!screenGlitchMaterialActive || screenGlitchMaterialInstance == null)
+ return;
+
+ // 只驱动强度档位;其余观感是材质的事,见 lookLivesOnMaterial。
+ screenGlitchMaterialInstance.SetFloat(ScreenGlitchIntensityId, screenGlitchIntensity);
+ }
+
+ private void SetScreenGlitchIntensity(float value)
+ {
+ screenGlitchIntensity = Mathf.Clamp01(value);
+ if (screenGlitchIntensity > 0f && !screenGlitchMaterialActive)
+ ActivateScreenGlitchMaterial();
+ ApplyScreenGlitchProperties();
+ }
+
+ ///
+ /// 恢复 glitch_transition 用的 noise 叠层到原始层级关系:
+ /// Default 层 + sortingOrder 10 + OpaqueBackground。
+ /// 不要对齐/改到 HuoshanScreen,否则会进 VolFx 合成叠在粒子上面。
+ ///
+ private void PrepareNoiseOverlayForGlitchTransition(bool activateForUse)
+ {
+ if (glitchController == null)
+ return;
+
+ const int defaultLayer = 0;
+ glitchController.gameObject.layer = defaultLayer;
+
+ var noiseRenderer = glitchController.GetComponent();
+ if (noiseRenderer != null)
+ {
+ noiseRenderer.sortingLayerID = 0;
+ noiseRenderer.sortingOrder = 10;
+ if (activateForUse)
+ noiseRenderer.enabled = true;
+ }
+
+ // 与场景原始配置一致:不透明黑底噪波(强度 0 时几乎全黑底,由上层 HuoshanScreen 内容盖住中心)。
+ glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.OpaqueBackground);
+
+ if (activateForUse && !glitchController.gameObject.activeSelf)
+ glitchController.gameObject.SetActive(true);
+ }
+
+ ///
+ /// Glitch 噪波过渡:驱动 ExpressView/noise(SpriteNoiseGlitch),非屏幕 Shader。
///
public void TransitionGlitch(float to, float duration = 0f)
{
if (glitchController == null) return;
+ PrepareNoiseOverlayForGlitchTransition(true);
glitchController.TransitionTo(to, duration);
}
+ ///
+ /// 把「屏幕」Sprite 的故障档位推到 并保持住(非阻塞)。
+ /// 从当前档位平滑过渡, 是到达该档位的时间;到位后不会自动回落,
+ /// 需要再调一次 level=0 才收掉。Shader 内部自带「憋气→用力→泄掉」的循环,
+ /// 所以保持档位时屏幕依然在持续挣扎,不是一张静止的花屏。
+ ///
+ public void SetScreenGlitchLevel(float level, float duration)
+ {
+ if (screenOverlayRenderer == null)
+ return;
+
+ float target = Mathf.Clamp01(level);
+
+ if (target > 0f)
+ {
+ ActivateScreenGlitchMaterial();
+ if (!screenGlitchMaterialActive)
+ return;
+
+ // 确保屏幕可见(否则材质在动但物体是关的)。
+ screenOverlayRenderer.enabled = true;
+ screenOverlayRenderer.gameObject.SetActive(true);
+ }
+ else if (!screenGlitchMaterialActive)
+ {
+ screenGlitchIntensity = 0f;
+ return;
+ }
+
+ if (screenGlitchRampRoutine != null)
+ {
+ StopCoroutine(screenGlitchRampRoutine);
+ screenGlitchRampRoutine = null;
+ }
+
+ float ramp = Mathf.Max(0f, duration);
+ if (ramp <= 0.02f || !isActiveAndEnabled)
+ {
+ SetScreenGlitchIntensity(target);
+ return;
+ }
+
+ screenGlitchRampRoutine = StartCoroutine(RampScreenGlitch(target, ramp));
+ }
+
+ private IEnumerator RampScreenGlitch(float target, float duration)
+ {
+ float start = screenGlitchIntensity;
+ float elapsed = 0f;
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
+ SetScreenGlitchIntensity(Mathf.Lerp(start, target, t));
+ yield return null;
+ }
+
+ SetScreenGlitchIntensity(target);
+ screenGlitchRampRoutine = null;
+ }
+
///
/// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to。
///
public IEnumerator TransitionGlitchAndWait(float to, float duration = 0f)
{
if (glitchController == null) yield break;
+ PrepareNoiseOverlayForGlitchTransition(true);
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
@@ -416,6 +664,7 @@ namespace AibisDream
public IEnumerator TransitionGlitchAndWait(float from, float to, float duration)
{
if (glitchController == null) yield break;
+ PrepareNoiseOverlayForGlitchTransition(true);
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
@@ -476,9 +725,37 @@ namespace AibisDream
private void OnDisable()
{
+ if (screenGlitchRampRoutine != null)
+ {
+ StopCoroutine(screenGlitchRampRoutine);
+ screenGlitchRampRoutine = null;
+ }
+ SetScreenGlitchIntensity(0f);
+
if (logReleasePresentation != null)
logReleasePresentation.ForceCleanupImmediate(true);
}
+ private void OnDestroy()
+ {
+ if (screenGlitchMaterialInstance != null)
+ {
+ if (Application.isPlaying)
+ Destroy(screenGlitchMaterialInstance);
+ else
+ DestroyImmediate(screenGlitchMaterialInstance);
+ screenGlitchMaterialInstance = null;
+ }
+
+ if (screenFallbackMaterial != null)
+ {
+ if (Application.isPlaying)
+ Destroy(screenFallbackMaterial);
+ else
+ DestroyImmediate(screenFallbackMaterial);
+ screenFallbackMaterial = null;
+ }
+ }
+
}
}
diff --git a/Assets/Shader/HuoshanScreenGlitch.hlsl b/Assets/Shader/HuoshanScreenGlitch.hlsl
new file mode 100644
index 000000000..c4803a9f5
--- /dev/null
+++ b/Assets/Shader/HuoshanScreenGlitch.hlsl
@@ -0,0 +1,347 @@
+#ifndef HUOSHAN_SCREEN_GLITCH_INCLUDED
+#define HUOSHAN_SCREEN_GLITCH_INCLUDED
+
+// 火山「屏幕」故障:不是电视雪花,而是"想说真心话却卡在喉咙里"的憋压感。
+//
+// 只有 6 个宏观旋钮,内部的十几个系数都从它们推导——不要再往外暴露单项参数,
+// 上一版 40 个属性的教训是:可调 ≠ 能调。
+// _Scale 效果尺度(屏幕小就调大,一切跟着变粗)
+// _ColorAmount 颜色
+// _Warp 撕裂位移
+// _Chaos 错误感(块级损坏 + 突发时机)
+// _Grit 颗粒与扫描线
+// _SurgeRate 情绪节奏
+//
+// 结构要点:
+// 1. 一切先汇成一条「信号」标量:亮度 + 副载波 + 逐行相位误差 + 颗粒 + 块损坏。
+// 颜色由色散调色板对信号取值(连续渐变,不是纯 RGB 色块=不卡通)。
+// 屏幕贴图那块均匀的绿只位移是看不见的,所以它由 _Desaturate 随强度退场。
+// 2. 密度按屏幕像素计(fwidth 换算),乘 _Scale;屏幕物件很小时把 _Scale 调大即可,
+// 不会因为 UV 空间频率被挤成一团细线。
+// 3. 错误感来自「块级损坏」:随机时隙里整块整块地错位、换色、通道旋转,
+// 而不是均匀连续地抖——均匀 = 规律 = 假。
+#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
+
+struct Attributes
+{
+ float4 positionOS : POSITION;
+ float2 uv : TEXCOORD0;
+ float4 color : COLOR;
+};
+
+struct Varyings
+{
+ float4 positionHCS : SV_POSITION;
+ float2 uv : TEXCOORD0;
+ float4 color : COLOR;
+};
+
+TEXTURE2D(_MainTex);
+SAMPLER(sampler_MainTex);
+
+CBUFFER_START(UnityPerMaterial)
+ float4 _MainTex_ST;
+ float4 _Color;
+ float _GlitchIntensity;
+ float _Speed;
+ float _Scale;
+ float _ColorAmount;
+ float _Warp;
+ float _Chaos;
+ float _Grit;
+ float _SurgeRate;
+ float _Dispersion;
+ float _PalPhase;
+ float _Desaturate;
+ float _BloomBoost;
+ float _Vignette;
+CBUFFER_END
+
+float hash11(float p)
+{
+ p = frac(p * 0.1031);
+ p *= p + 33.33;
+ p *= p + p;
+ return frac(p);
+}
+
+float hash21(float2 p)
+{
+ float3 p3 = frac(float3(p.xyx) * 0.1031);
+ p3 += dot(p3, p3.yzx + 33.33);
+ return frac((p3.x + p3.y) * p3.z);
+}
+
+// 值噪声:正弦叠正弦永远是周期性壁纸,需要非周期调制的地方一律用它。
+float vnoise(float2 p)
+{
+ float2 i = floor(p);
+ float2 f = frac(p);
+ f = f * f * (3.0 - 2.0 * f);
+ return lerp(
+ lerp(hash21(i), hash21(i + float2(1, 0)), f.x),
+ lerp(hash21(i + float2(0, 1)), hash21(i + float2(1, 1)), f.x),
+ f.y);
+}
+
+// 色散调色板:三通道相位错开地对同一条信号取值。
+// 相位差小 = 轻微色偏(很老的机器),接近 1/3 = 满彩虹。输出连续渐变,不会出现卡通纯色块。
+half3 AnalogPalette(float sig)
+{
+ float3 ph = 6.2831853 * (sig * 1.15 + float3(0.0, 1.0, 2.0) * _Dispersion + _PalPhase);
+ return 0.5 + 0.5 * cos(ph);
+}
+
+// 横向拖抹 + 通道分离:亮度信号被硬拽出拖尾。
+half3 SampleSmeared(float2 uv, float smear, float dir, float chroma)
+{
+ half3 acc = 0;
+ float wsum = 0;
+
+ [unroll]
+ for (int i = 0; i < 4; i++)
+ {
+ float k = (float)i / 3.0;
+ float w = 1.0 - k * 0.72;
+ float2 p = uv - float2(smear * k * dir, 0);
+
+ half r = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, p + float2(chroma, 0)).r;
+ half g = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, p).g;
+ half b = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, p - float2(chroma, 0)).b;
+
+ acc += half3(r, g, b) * w;
+ wsum += w;
+ }
+
+ return acc / max(wsum, 1e-4);
+}
+
+Varyings HuoshanScreenGlitchVert(Attributes input)
+{
+ Varyings o;
+ o.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
+ o.uv = TRANSFORM_TEX(input.uv, _MainTex);
+ o.color = input.color * _Color;
+ return o;
+}
+
+half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target
+{
+ float intensity = saturate(_GlitchIntensity);
+ float2 uv = input.uv;
+
+ // 轮廓永远取自未位移的 UV:屏幕形状不会被撕出缺口。
+ half4 baseSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
+ float spriteAlpha = baseSample.a * input.color.a;
+ clip(spriteAlpha - 0.001);
+
+ if (intensity <= 0.0005)
+ {
+ half4 clean = baseSample;
+ clean.rgb *= input.color.rgb;
+ clean.a = spriteAlpha;
+ return clean;
+ }
+
+ // ── 从 6 个旋钮推导内部系数 ─────────────────────────────
+ float scale = max(0.25, _Scale);
+ float ribbonPx = 90.0 * scale; // 彩带周期
+ float scanPx = 8.0 * scale; // 扫描线周期
+ float grainPx = 3.0 * scale; // 颗粒格子
+ float rowPx = 14.0 * scale; // 逐行相位误差 / 掉行的行高
+ float blockPx = 56.0 * scale; // 块损坏的基准块高
+ float jagPx = 26.0 * scale; // 锯齿周期
+
+ float tearAmt = 0.16 * _Warp;
+ float jagAmt = 0.05 * _Warp;
+ float smearAmt = 0.07 * _Warp;
+ float squeezeAmt= 0.05 * _Warp;
+ float splitAmt = 0.05 * _Warp;
+ float blockShift= 0.11 * _Warp;
+
+ float grainAmt = 0.16 * _Grit;
+ float scanAmt = saturate(0.5 * _Grit);
+
+ // 每个屏幕像素对应多少 UV。
+ float2 texel = float2(max(fwidth(uv.x), 1e-7), max(fwidth(uv.y), 1e-7));
+ float2 pixelPos = uv / texel;
+
+ float t = _Time.y * _Speed;
+
+ // ── 情绪泵:越用力越急促。build 慢慢积压,vent 一下泄掉,然后重来。
+ float rate = max(0.02, _SurgeRate) * lerp(0.55, 1.9, intensity);
+ float phase = _Time.y * rate;
+ float cyc = frac(phase);
+ float beat = floor(phase);
+ float build = pow(smoothstep(0.0, 0.78, cyc), 2.2);
+ float vent = 1.0 - smoothstep(0.80, 0.96, cyc);
+ float surge = saturate(build * vent);
+
+ // ── 卡喉:随机某几拍把画面"憋住"——时间种子冻结、力度更大。
+ float choke = step(1.0 - saturate(0.35 * intensity), hash11(beat * 7.31 + 0.17));
+ float timeSeed = lerp(floor(t * 10.0), beat * 13.0, choke);
+
+ // 持续档位也一直在呼吸:不给 0,保证 hold 住时屏幕仍在小幅挣扎。
+ float drive = intensity * lerp(0.42, 1.0, surge) * lerp(1.0, 1.3, choke * build);
+
+ // ── 自下而上的行波:一口气往上顶。结构性的,跟着屏幕比例走。
+ float waveCenter = frac(-_Time.y * 0.45 * lerp(0.45, 1.35, intensity));
+ float d = uv.y - waveCenter;
+ d -= floor(d + 0.5);
+ float waveEnv = exp(-(d * d) / 0.0484);
+ waveEnv = saturate(waveEnv + intensity * 0.5);
+
+ // ── 块级损坏:随机时隙里成片出错。这是"错误感"的来源——
+ // 均匀连续的抖动读作机器在正常地坏,成块的突发才读作数据出错。
+ float chaosSlot = floor(_Time.y * 7.0);
+ float burst = step(1.0 - saturate(_Chaos * 0.55), hash11(chaosSlot * 3.71 + 0.5));
+ float chaosDrive = saturate(drive * (0.3 + burst * 1.7) * _Chaos);
+
+ // 大块:宽而扁,像整段数据行被搬走。格子原点和大小每个时隙重抽——
+ // 固定网格上的块一眼就看出是"贴上去的"。
+ float sizeJit = lerp(0.7, 1.7, hash11(chaosSlot * 4.31));
+ float2 gridJit = float2(hash11(chaosSlot * 1.71), hash11(chaosSlot * 2.93)) * 17.0;
+ float2 cellA = floor(float2(pixelPos.x / (blockPx * 2.2 * sizeJit),
+ pixelPos.y / (blockPx * sizeJit)) + gridJit);
+ float hitA = step(1.0 - chaosDrive * 0.9, hash21(cellA + chaosSlot));
+ float shiftA = (hash11(dot(cellA, float2(7.31, 13.17)) + chaosSlot) - 0.5) * 2.0
+ * blockShift * hitA;
+ // 块内逐行错位:真数据损坏里块内部每行错得不一样,整块刚体平移读作贴图。
+ float innerRow = floor(pixelPos.y / max(2.0, rowPx * 0.5));
+ shiftA *= 0.55 + 0.9 * hash21(float2(innerRow, cellA.x * 3.1 + chaosSlot));
+
+ // 小块:碎片,负责细节上的"啃咬"
+ float2 cellB = floor(float2(pixelPos.x / (blockPx * 0.55), pixelPos.y / (blockPx * 0.4))
+ + gridJit.yx);
+ float hitB = step(1.0 - chaosDrive * 0.7, hash21(cellB * 1.7 + chaosSlot * 2.3));
+ float shiftB = (hash11(dot(cellB, float2(3.71, 9.13)) + chaosSlot) - 0.5) * 2.0
+ * blockShift * 0.45 * hitB;
+
+ // ── 位移:锯齿 + 分带撕裂 + 块位移 + 整帧推挤。
+ // 锯齿的频率和幅度都用噪声调制,等周期的全屏锯齿太规律。
+ float jagNoise = vnoise(float2(uv.y * 3.0, _Time.y * 1.3));
+ float zig = sin(pixelPos.y * 6.2831853 / max(2.0, jagPx * (0.6 + jagNoise)) + t * 3.0);
+ zig = sign(zig) * pow(abs(zig), 0.55);
+ float jag = zig * jagAmt * drive * waveEnv * (0.35 + 0.65 * jagNoise);
+
+ // 撕裂带:带高每个时隙重抽,粗细两层叠加——固定 22 条等高带太机械。
+ float bandScale = lerp(7.0, 24.0, hash11(floor(timeSeed * 0.37) * 5.13));
+ float band = floor(uv.y * bandScale + hash11(timeSeed) * 4.0);
+ float bandRand = hash21(float2(band, timeSeed));
+ float bandActive = step(1.0 - saturate(drive * 0.85), bandRand);
+ float tear = (hash11(band * 17.13 + timeSeed) - 0.5) * 2.0
+ * tearAmt * drive * bandActive * (0.35 + waveEnv * 0.85);
+
+ // 细撕裂:几像素高的毛刺行,让撕裂边缘"起毛"而不是整齐的粗条。
+ float bandF = floor(uv.y * bandScale * 4.3 + hash11(timeSeed + 2.0) * 9.0);
+ float fineActive = step(1.0 - saturate(drive * 0.5), hash21(float2(bandF, timeSeed + 5.0)));
+ tear += (hash11(bandF * 7.77 + timeSeed) - 0.5) * 2.0
+ * tearAmt * 0.35 * drive * fineActive;
+
+ float shove = (hash11(beat + 0.5) - 0.5) * 2.0 * tearAmt * 0.3 * surge * intensity;
+
+ // 隔行抖动:奇偶行反向的细小错位,CRT 失锁最便宜也最有效的一层。
+ float odd = fmod(floor(pixelPos.y / max(1.0, rowPx * 0.25)), 2.0) * 2.0 - 1.0;
+ float interlace = odd * 0.0045 * _Warp * drive * (0.3 + waveEnv * 0.7);
+
+ // 场滚动:泄压瞬间垂直保持失锁,整幅画面向上滑一口——电视坏掉的标志性动作。
+ float rollGate = step(0.7, hash11(beat * 3.77 + 1.3)) * intensity;
+ float roll = rollGate * smoothstep(0.78, 1.0, cyc) * 0.055 * _Warp;
+
+ float squeeze = squeezeAmt * surge * intensity;
+ float2 uvDist;
+ uvDist.x = uv.x + tear + jag + shove + shiftA + shiftB + interlace;
+ uvDist.y = 0.5 + (uv.y - 0.5) * (1.0 - squeeze) + roll;
+ float2 pixelDist = uvDist / texel;
+
+ float smearDir = (hash11(band * 5.71 + timeSeed) < 0.5) ? -1.0 : 1.0;
+ float smear = smearAmt * drive * (0.3 + waveEnv * 1.0) * (0.4 + bandActive);
+ float chromaSplit = splitAmt * drive * (0.4 + waveEnv * 1.05) * (1.0 + hitA * 2.0);
+ half3 tornSprite = SampleSmeared(uvDist, smear, smearDir, chromaSplit);
+
+ // ── 亮度信号。原本那块绿随强度退场,否则怎么撕都还是一片绿。
+ float luma = dot(tornSprite, half3(0.299, 0.587, 0.114));
+ float signalY = lerp(luma, saturate((luma - 0.5) * 1.6 + 0.5), drive);
+
+ // ── 颗粒:满屏细噪,是"被拍下来"最关键的一层,没有它就是矢量图。
+ float2 grainCell = floor(pixelPos / max(0.5, grainPx));
+ float grain = (hash21(grainCell + floor(_Time.y * 24.0) * 0.137) - 0.5) * grainAmt;
+
+ // ── 副载波:多个八度 + 逐行相位误差(真实模拟信号每一行相位都不同,
+ // 这是规则几何波形和真实故障的分界线)。相位按位移后的像素坐标取,
+ // 所以撕裂和块位移会自动把色带错开。
+ float scanRow = floor(pixelPos.y / max(1.0, rowPx));
+ float rowPhase = (hash21(float2(scanRow, timeSeed)) - 0.5) * 1.6;
+ // 纵向调制改用两个八度的值噪声:之前的 sin(y*14)+sin(y*37.8) 是固定周期,
+ // 出来的彩带像壁纸一样重复。每行的载波频率也随机抖,彩带宽度不再全屏一致。
+ float phNoise = vnoise(float2(uvDist.y * 7.0, _Time.y * 0.8)) * 2.0
+ + vnoise(float2(uvDist.y * 23.0 + 11.0, _Time.y * 1.6));
+ float rowFreq = 0.72 + 0.56 * hash21(float2(scanRow, floor(_Time.y * 2.0)));
+ float ribbon =
+ pixelDist.x * 6.2831853 / max(4.0, ribbonPx * rowFreq)
+ + (phNoise - 1.5) * 3.2
+ + rowPhase
+ + waveEnv * 2.4
+ - _Time.y * 2.2;
+
+ float sI = sin(ribbon);
+ sI = sign(sI) * pow(abs(sI), 0.85);
+
+ // 高强度时把逐像素噪声灌进信号——出来是密集彩点而不是纯色方块。
+ float speckGate = saturate((intensity - 0.5) / 0.5) * surge
+ * saturate(bandActive * 0.75 + waveEnv * 0.55);
+ float2 speckCell = floor(pixelDist / max(1.0, grainPx * 2.0));
+ float speck = (hash21(speckCell + timeSeed * 0.31) - 0.5) * 2.0;
+
+ float gain = _ColorAmount * drive * (0.55 + waveEnv * 0.8);
+ float sig = signalY + sI * gain + speck * 0.55 * speckGate + grain * 0.6;
+
+ // ── 亮度包络:暗处保持暗、亮处冲白,色带自身也有明暗起伏。
+ float envelope = saturate(signalY * 1.15 + sI * gain * 0.35 + grain);
+ envelope += smoothstep(0.55, 1.0, envelope) * 0.35; // 光晕溢出
+
+ half3 analog = AnalogPalette(sig) * envelope * 1.32;
+
+ // ── 混合:原色随强度退场,色散层顶上来。
+ half3 desatBase = lerp(tornSprite, half3(signalY, signalY, signalY),
+ saturate(_Desaturate * drive * 1.35));
+ half3 col = lerp(desatBase, analog, saturate(drive * 1.35));
+
+ // ── 块损坏的上色:块必须带着底下的画面坏,整块换成无关颜色就是"贴纸"。
+ // 量化(马赛克色阶)+ 通道旋转 + 淡淡的调色板偏色,明暗仍来自原信号。
+ float recolorSeed = hash11(dot(cellA, float2(19.3, 5.7)) + chaosSlot * 1.3);
+ float post = lerp(5.0, 2.0, recolorSeed);
+ half3 blockCol = floor(col * post + 0.5) / post;
+ blockCol = lerp(blockCol, blockCol.gbr, step(0.6, recolorSeed));
+ blockCol = lerp(blockCol, AnalogPalette(recolorSeed * 3.0 + signalY) * envelope,
+ 0.35 * saturate(_Chaos));
+ col = lerp(col, blockCol, hitA * saturate(_Chaos * 0.85));
+ // 小碎块直接旋转通道:硬色偏,参考图里那种突兀的品红/青块。
+ col = lerp(col, col.brg, hitB * saturate(_Chaos * 0.5));
+
+ // ── 掉行:黑色横缝与偶发白色爆行,行高跟着尺度走。
+ float blackRow = step(0.985 - chaosDrive * 0.14, hash21(float2(scanRow, timeSeed + 3.0)));
+ col *= 1.0 - blackRow * 0.9;
+ float whiteRow = step(0.996 - chaosDrive * 0.03, hash21(float2(scanRow, timeSeed + 7.0)));
+ col += whiteRow * drive * 0.9;
+
+ // ── 扫描线:拟真感的骨架。低于约 2 像素分辨不出来只会闪,自动淡出。
+ float scanFade = saturate((scanPx - 1.6) / 1.4);
+ float scanWave = 0.5 + 0.5 * sin(pixelPos.y * 6.2831853 / max(1.0, scanPx) + _Time.y * 1.5);
+ col *= lerp(1.0, scanWave, saturate(scanAmt * scanFade));
+
+ // ── 暗角:照片感。
+ float2 vp = uv - 0.5;
+ col *= 1.0 - _Vignette * saturate(dot(vp, vp) * 2.0);
+
+ // ── 波峰过曝:接现有 Bloom,把"顶到嗓子眼"那一下推出去。
+ float crest = waveEnv * surge * intensity;
+ col *= 1.0 + _BloomBoost * crest;
+
+ half4 result;
+ result.rgb = max(0.0, col) * input.color.rgb;
+ result.a = spriteAlpha;
+ return result;
+}
+
+#endif // HUOSHAN_SCREEN_GLITCH_INCLUDED
diff --git a/Assets/Shader/HuoshanScreenGlitch.hlsl.meta b/Assets/Shader/HuoshanScreenGlitch.hlsl.meta
new file mode 100644
index 000000000..6221a9f64
--- /dev/null
+++ b/Assets/Shader/HuoshanScreenGlitch.hlsl.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1846a133ff0bc9a48981189d982a490f
+ShaderIncludeImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Shader/HuoshanScreenGlitch.shader b/Assets/Shader/HuoshanScreenGlitch.shader
new file mode 100644
index 000000000..dd5200a55
--- /dev/null
+++ b/Assets/Shader/HuoshanScreenGlitch.shader
@@ -0,0 +1,72 @@
+Shader "AibisDream/HuoshanScreenGlitch"
+{
+ // 只暴露 6 个宏观旋钮 + 4 个基调,内部十几个系数全部从它们推导。
+ // 想加新参数前先想清楚:上一版 40 个属性根本没法调。
+ Properties
+ {
+ [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
+ _Color("Tint", Color) = (1,1,1,1)
+
+ [Header(Drive)]
+ _GlitchIntensity("Glitch Intensity (driven by Yarn)", Range(0, 1)) = 0
+ _Speed("Animation Speed", Float) = 10
+
+ [Header(Main Knobs)]
+ _Scale("Scale (screen too small then raise this)", Range(0.25, 6)) = 1.6
+ _ColorAmount("Color", Range(0, 3)) = 1.35
+ _Warp("Warp / Tear", Range(0, 2)) = 1
+ _Chaos("Chaos (block corruption)", Range(0, 1)) = 0.6
+ _Grit("Grit (grain and scanlines)", Range(0, 2)) = 1
+ _SurgeRate("Surge Rate (emotional pacing)", Range(0.05, 3)) = 0.5
+
+ [Header(Tone)]
+ _Dispersion("Channel Dispersion", Range(0, 0.34)) = 0.19
+ _PalPhase("Hue Bias", Range(0, 1)) = 0.62
+ _Desaturate("Original Hue Washout", Range(0, 1)) = 0.9
+ _BloomBoost("Crest Bloom Boost", Range(0, 3)) = 0.9
+ _Vignette("Vignette", Range(0, 1)) = 0.28
+ }
+
+ SubShader
+ {
+ Tags
+ {
+ "Queue" = "Transparent"
+ "RenderType" = "Transparent"
+ "RenderPipeline" = "UniversalPipeline"
+ "IgnoreProjector" = "True"
+ "CanUseSpriteAtlas" = "True"
+ }
+
+ Cull Off
+ ZWrite Off
+ Blend SrcAlpha OneMinusSrcAlpha
+
+ Pass
+ {
+ Name "HuoshanScreenGlitch"
+ Tags { "LightMode" = "Universal2D" }
+
+ HLSLPROGRAM
+ #pragma vertex HuoshanScreenGlitchVert
+ #pragma fragment HuoshanScreenGlitchFrag
+ #pragma target 3.0
+ #include "HuoshanScreenGlitch.hlsl"
+ ENDHLSL
+ }
+
+ Pass
+ {
+ Name "HuoshanScreenGlitchUnlit"
+ Tags { "LightMode" = "SRPDefaultUnlit" }
+
+ HLSLPROGRAM
+ #pragma vertex HuoshanScreenGlitchVert
+ #pragma fragment HuoshanScreenGlitchFrag
+ #pragma target 3.0
+ #include "HuoshanScreenGlitch.hlsl"
+ ENDHLSL
+ }
+ }
+ FallBack "Sprites/Default"
+}
diff --git a/Assets/Shader/HuoshanScreenGlitch.shader.meta b/Assets/Shader/HuoshanScreenGlitch.shader.meta
new file mode 100644
index 000000000..7b9a09969
--- /dev/null
+++ b/Assets/Shader/HuoshanScreenGlitch.shader.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 34b7b1bac3f94aae80724d2934758989
+ShaderImporter:
+ externalObjects: {}
+ defaultTextures: []
+ nonModifiableTextures: []
+ userData:
+ assetBundleName:
+ assetBundleVariant: