From e2461ff0e578311b4020751c9c3407bbf4d2a55f Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Sat, 25 Jul 2026 15:56:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(huoshan):=20=E6=96=B0=E5=A2=9E=E5=B1=8F?= =?UTF-8?q?=E5=B9=95glitch=20shader=E4=B8=8E=E5=BC=BA=E5=BA=A6=E9=A9=B1?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- Assets/Materials/M_HuoshanScreenGlitch.mat | 47 +++ .../Materials/M_HuoshanScreenGlitch.mat.meta | 8 + .../Effects/SpriteNoiseGlitchController.cs | 34 ++ .../HuoShan/Language/ExpressionManager.cs | 281 +++++++++++++- Assets/Shader/HuoshanScreenGlitch.hlsl | 347 ++++++++++++++++++ Assets/Shader/HuoshanScreenGlitch.hlsl.meta | 7 + Assets/Shader/HuoshanScreenGlitch.shader | 72 ++++ Assets/Shader/HuoshanScreenGlitch.shader.meta | 9 + 8 files changed, 803 insertions(+), 2 deletions(-) create mode 100644 Assets/Materials/M_HuoshanScreenGlitch.mat create mode 100644 Assets/Materials/M_HuoshanScreenGlitch.mat.meta create mode 100644 Assets/Shader/HuoshanScreenGlitch.hlsl create mode 100644 Assets/Shader/HuoshanScreenGlitch.hlsl.meta create mode 100644 Assets/Shader/HuoshanScreenGlitch.shader create mode 100644 Assets/Shader/HuoshanScreenGlitch.shader.meta 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: