diff --git a/Assets/Editor/GlitchNoiseAudioGenerator.cs b/Assets/Editor/GlitchNoiseAudioGenerator.cs
new file mode 100644
index 000000000..8e4e1749a
--- /dev/null
+++ b/Assets/Editor/GlitchNoiseAudioGenerator.cs
@@ -0,0 +1,149 @@
+using UnityEngine;
+using UnityEditor;
+using System.IO;
+
+///
+/// 生成与 SpriteNoiseGlitch shader 三阶段对应的程序化音效。
+/// 菜单: Tools > Generate Glitch Noise Audio
+///
+public static class GlitchNoiseAudioGenerator
+{
+ private const int SampleRate = 44100;
+ private const float Duration = 4f; // 每段音效时长(秒),便于循环使用
+ private const string OutputFolder = "Assets/RawResources/Audio/GlitchNoise";
+
+ [MenuItem("Tools/Generate Glitch Noise Audio")]
+ public static void Generate()
+ {
+ string dir = Path.Combine(Application.dataPath, "RawResources", "Audio", "GlitchNoise");
+ Directory.CreateDirectory(dir);
+
+ GenerateStage1(dir);
+ GenerateStage2(dir);
+ GenerateStage3(dir);
+
+ AssetDatabase.Refresh();
+ Debug.Log($"[GlitchNoise] 已生成三阶段音效至 Assets/RawResources/Audio/GlitchNoise");
+ }
+
+ /// Stage 1: 轻微静态噪点 - 柔和白噪声
+ private static void GenerateStage1(string dir)
+ {
+ int samples = (int)(SampleRate * Duration);
+ float[] data = new float[samples];
+ var rnd = new System.Random(12345);
+
+ float gain = 0.12f;
+ for (int i = 0; i < samples; i++)
+ {
+ data[i] = ((float)rnd.NextDouble() * 2f - 1f) * gain;
+ }
+
+ SaveWav(dir, "GlitchNoise_Stage1_Light.wav", data);
+ }
+
+ /// Stage 2: 中等 - 更多噪点 + 偶尔 glitch 爆音
+ private static void GenerateStage2(string dir)
+ {
+ int samples = (int)(SampleRate * Duration);
+ float[] data = new float[samples];
+ var rnd = new System.Random(23456);
+
+ float noiseGain = 0.22f;
+ int glitchInterval = SampleRate / 4; // 约每 0.25 秒一次 glitch 爆音
+
+ for (int i = 0; i < samples; i++)
+ {
+ float n = ((float)rnd.NextDouble() * 2f - 1f) * noiseGain;
+
+ // 随机 glitch 爆音
+ if (i > 0 && i % glitchInterval < 120)
+ {
+ float burst = ((float)rnd.NextDouble() * 2f - 1f) * 0.5f;
+ n += burst * (1f - (i % glitchInterval) / 120f);
+ }
+
+ data[i] = Mathf.Clamp(n, -1f, 1f);
+ }
+
+ SaveWav(dir, "GlitchNoise_Stage2_Medium.wav", data);
+ }
+
+ /// Stage 3: 严重 - 强烈噪点 + 频繁 glitch + 数字故障感
+ private static void GenerateStage3(string dir)
+ {
+ int samples = (int)(SampleRate * Duration);
+ float[] data = new float[samples];
+ var rnd = new System.Random(34567);
+
+ float noiseGain = 0.4f;
+ int blockSize = 2205; // 约 0.05 秒一块
+ int glitchBlockEvery = 4;
+
+ for (int i = 0; i < samples; i++)
+ {
+ int block = i / blockSize;
+ float n = ((float)rnd.NextDouble() * 2f - 1f) * noiseGain;
+
+ // 块状 glitch:整块随机反转/爆音
+ if (block % glitchBlockEvery == 0)
+ {
+ int posInBlock = i % blockSize;
+ float t = (float)posInBlock / blockSize;
+ n += ((float)rnd.NextDouble() * 2f - 1f) * 0.6f * (1f - t);
+ }
+
+ // 随机“卡顿”短静音后爆音
+ if (rnd.NextDouble() < 0.0003)
+ {
+ int silenceLen = 100 + rnd.Next(300);
+ int end = Mathf.Min(i + silenceLen, samples);
+ for (int j = i; j < end; j++)
+ {
+ data[j] = j == end - 1 ? ((float)rnd.NextDouble() * 2f - 1f) * 0.8f : 0f;
+ }
+ i = end - 1;
+ }
+ else
+ {
+ data[i] = Mathf.Clamp(n, -1f, 1f);
+ }
+ }
+
+ SaveWav(dir, "GlitchNoise_Stage3_Heavy.wav", data);
+ }
+
+ private static void SaveWav(string dir, string filename, float[] samples)
+ {
+ string path = Path.Combine(dir, filename);
+ using (var fs = new FileStream(path, FileMode.Create))
+ using (var bw = new BinaryWriter(fs))
+ {
+ // RIFF header
+ bw.Write(new[] { 'R', 'I', 'F', 'F' });
+ int dataSize = samples.Length * 2; // 16-bit
+ bw.Write(36 + dataSize);
+ bw.Write(new[] { 'W', 'A', 'V', 'E' });
+
+ // fmt chunk
+ bw.Write(new[] { 'f', 'm', 't', ' ' });
+ bw.Write(16); // chunk size
+ bw.Write((short)1); // PCM
+ bw.Write((short)1); // mono
+ bw.Write(SampleRate);
+ bw.Write(SampleRate * 2);
+ bw.Write((short)2);
+ bw.Write((short)16);
+
+ // data chunk
+ bw.Write(new[] { 'd', 'a', 't', 'a' });
+ bw.Write(dataSize);
+
+ foreach (float s in samples)
+ {
+ short sample = (short)Mathf.Clamp((int)(s * 32767), -32768, 32767);
+ bw.Write(sample);
+ }
+ }
+ }
+}
diff --git a/Assets/Editor/GlitchNoiseAudioGenerator.cs.meta b/Assets/Editor/GlitchNoiseAudioGenerator.cs.meta
new file mode 100644
index 000000000..ccc2628e0
--- /dev/null
+++ b/Assets/Editor/GlitchNoiseAudioGenerator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: cd5ec77f7adcc584ebc901ac1f73fab6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Materials/M_noiseScreen.mat b/Assets/Materials/M_noiseScreen.mat
new file mode 100644
index 000000000..97b8f6fd3
--- /dev/null
+++ b/Assets/Materials/M_noiseScreen.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_noiseScreen
+ m_Shader: {fileID: 4800000, guid: 505a54c499a926e419139bbed9fe5a81, 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:
+ - _BlockSize: 12
+ - _ChromaticShift: 0.04
+ - _DisplaceStrength: 0.15
+ - _DropoutSize: 0.15
+ - _FlickerStrength: 0.4
+ - _GlitchIntensity: 0
+ - _NoiseBrightness: 1
+ - _NoiseDensity: 0.053
+ - _NoiseScale: 132
+ - _ScanlineCount: 80
+ - _ScanlineStrength: 0.3
+ - _Speed: 5
+ - _TearStrength: 0.3
+ m_Colors:
+ - _Color: {r: 1, g: 1, b: 1, a: 1}
+ m_BuildTextureStacks: []
diff --git a/Assets/Materials/M_noiseScreen.mat.meta b/Assets/Materials/M_noiseScreen.mat.meta
new file mode 100644
index 000000000..b24e8a23e
--- /dev/null
+++ b/Assets/Materials/M_noiseScreen.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 77ae0e621bc487c439f7c6f9a41b08fe
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/RawResources/Audio.meta b/Assets/RawResources/Audio.meta
new file mode 100644
index 000000000..943cec9dc
--- /dev/null
+++ b/Assets/RawResources/Audio.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 94284a48ce2f0164a86f7f8a0c1b4859
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/RawResources/Audio/GlitchNoise.meta b/Assets/RawResources/Audio/GlitchNoise.meta
new file mode 100644
index 000000000..53ef63704
--- /dev/null
+++ b/Assets/RawResources/Audio/GlitchNoise.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 324f307b690f8a546a58256f6a8b3117
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage1_Light.wav b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage1_Light.wav
new file mode 100644
index 000000000..1712e3554
Binary files /dev/null and b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage1_Light.wav differ
diff --git a/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage1_Light.wav.meta b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage1_Light.wav.meta
new file mode 100644
index 000000000..784cedbcb
--- /dev/null
+++ b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage1_Light.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 9f4ca2e877b18ff46902c106f56ed672
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 7
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage2_Medium.wav b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage2_Medium.wav
new file mode 100644
index 000000000..91b0afcda
Binary files /dev/null and b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage2_Medium.wav differ
diff --git a/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage2_Medium.wav.meta b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage2_Medium.wav.meta
new file mode 100644
index 000000000..7a5322993
--- /dev/null
+++ b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage2_Medium.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 5b5068defdc3579479bf0efa306a07ca
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 7
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage3_Heavy.wav b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage3_Heavy.wav
new file mode 100644
index 000000000..ff14b2586
Binary files /dev/null and b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage3_Heavy.wav differ
diff --git a/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage3_Heavy.wav.meta b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage3_Heavy.wav.meta
new file mode 100644
index 000000000..3d2c25035
--- /dev/null
+++ b/Assets/RawResources/Audio/GlitchNoise/GlitchNoise_Stage3_Heavy.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: a965c5c0ea798db4787e279d5bef59fa
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 7
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/Shader/SpriteNoiseGlitch.shader b/Assets/Resources/Shader/SpriteNoiseGlitch.shader
new file mode 100644
index 000000000..29143bdd9
--- /dev/null
+++ b/Assets/Resources/Shader/SpriteNoiseGlitch.shader
@@ -0,0 +1,441 @@
+Shader "AibisDream/SpriteNoiseGlitch"
+{
+ Properties
+ {
+ [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
+ _Color("Tint", Color) = (1,1,1,1)
+
+ [Header(Glitch Control)]
+ _GlitchIntensity("Glitch Intensity", Range(0, 1)) = 0
+ _Speed("Animation Speed", Float) = 5.0
+
+ [Header(Noise)]
+ _NoiseScale("Noise Scale (lower = bigger pixel dots)", Range(40, 500)) = 80
+ _NoiseDensity("Noise Density (white dot probability)", Range(0, 1)) = 0.1
+ _NoiseBrightness("Noise Brightness", Range(0, 2)) = 1.0
+ _ScanlineCount("Scanline Count", Float) = 80
+ _ScanlineStrength("Scanline Strength", Range(0, 1)) = 0.3
+
+ [Header(Block Glitch)]
+ _BlockSize("Block Row Count", Range(2, 40)) = 12
+ _DisplaceStrength("Displace Strength", Float) = 0.15
+ _ChromaticShift("Chromatic Shift", Float) = 0.04
+
+ [Header(Heavy Glitch)]
+ _TearStrength("Tear Strength", Float) = 0.3
+ _FlickerStrength("Flicker Strength", Range(0, 1)) = 0.4
+ _DropoutSize("Dropout Block Size", Range(0, 1)) = 0.15
+ }
+
+ SubShader
+ {
+ Tags
+ {
+ "Queue" = "Transparent"
+ "RenderType" = "Transparent"
+ "RenderPipeline" = "UniversalPipeline"
+ "IgnoreProjector" = "True"
+ "CanUseSpriteAtlas" = "True"
+ }
+
+ Cull Off
+ ZWrite Off
+ Blend SrcAlpha OneMinusSrcAlpha
+
+ Pass
+ {
+ Name "SpriteNoiseGlitch"
+ Tags { "LightMode" = "Universal2D" }
+
+ HLSLPROGRAM
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma target 3.0
+
+ #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 _NoiseScale;
+ float _NoiseDensity;
+ float _NoiseBrightness;
+ float _ScanlineCount;
+ float _ScanlineStrength;
+ float _BlockSize;
+ float _DisplaceStrength;
+ float _ChromaticShift;
+ float _TearStrength;
+ float _FlickerStrength;
+ float _DropoutSize;
+ 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 hash31(float3 p)
+ {
+ p = frac(p * float3(0.1031, 0.1030, 0.0973));
+ p += dot(p, p.yxz + 33.33);
+ return frac((p.x + p.y) * p.z);
+ }
+
+ float remap01(float v, float lo, float hi)
+ {
+ return saturate((v - lo) / (hi - lo));
+ }
+
+ Varyings vert(Attributes input)
+ {
+ Varyings output;
+ output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
+ output.uv = TRANSFORM_TEX(input.uv, _MainTex);
+ output.color = input.color * _Color;
+ return output;
+ }
+
+ half4 frag(Varyings input) : SV_Target
+ {
+ float intensity = _GlitchIntensity;
+ float t = _Time.y * _Speed;
+ float2 uv = input.uv;
+
+ half4 spriteColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
+ float spriteAlpha = spriteColor.a * input.color.a;
+ clip(spriteAlpha - 0.001);
+
+ float w1 = remap01(intensity, 0.0, 0.45);
+ float w2 = remap01(intensity, 0.3, 0.75);
+ float w3 = remap01(intensity, 0.6, 1.0);
+
+ // 噪点密度:强度 0 时几乎为 0,随强度增加逐渐出现
+ float noiseDensity = _NoiseDensity * remap01(intensity, 0.0, 0.5);
+
+ // ============================================================
+ // STAGE 1: black base + white noise dots + scanlines
+ // ============================================================
+
+ float timeSeed = floor(t * 8.0);
+ float2 cell = floor(uv * _NoiseScale);
+
+ float noiseVal = hash31(float3(cell, timeSeed));
+ float whiteDot = step(1.0 - noiseDensity, noiseVal) * _NoiseBrightness;
+
+ float scanline = sin(uv.y * _ScanlineCount * PI);
+ float scanMask = smoothstep(0.3, 1.0, scanline) * _ScanlineStrength * w1;
+
+ half3 baseColor = half3(whiteDot, whiteDot, whiteDot);
+ baseColor *= (1.0 - scanMask * 0.5);
+
+ // ============================================================
+ // STAGE 2: horizontal block displacement + chromatic aberration
+ // ============================================================
+
+ float blockRow = floor(uv.y * _BlockSize);
+ float blockTimeSeed = floor(t * 6.0);
+ float blockRand = hash21(float2(blockRow, blockTimeSeed));
+
+ float blockThreshold = lerp(1.1, 0.3, w2);
+ float blockActive = step(blockThreshold, blockRand);
+
+ float displaceDir = (hash11(blockRow * 7.3 + blockTimeSeed * 1.7) - 0.5) * 2.0;
+ float displace = displaceDir * _DisplaceStrength * w2 * blockActive;
+
+ float2 uvShifted = uv + float2(displace, 0);
+ float chromaOffset = _ChromaticShift * w2 * (1.0 + blockActive * 2.0);
+
+ float noiseR = hash31(float3(floor((uvShifted + float2( chromaOffset, 0)) * _NoiseScale), timeSeed));
+ float noiseG = hash31(float3(floor( uvShifted * _NoiseScale), timeSeed));
+ float noiseB = hash31(float3(floor((uvShifted + float2(-chromaOffset, 0)) * _NoiseScale), timeSeed));
+
+ float dotR = step(1.0 - noiseDensity, noiseR) * _NoiseBrightness;
+ float dotG = step(1.0 - noiseDensity, noiseG) * _NoiseBrightness;
+ float dotB = step(1.0 - noiseDensity, noiseB) * _NoiseBrightness;
+
+ half3 chromaColor = half3(dotR, dotG, dotB);
+ chromaColor *= (1.0 - scanMask * 0.5);
+
+ baseColor = lerp(baseColor, chromaColor, w2);
+
+ // add shifted solid color bands in displaced blocks
+ float bandBrightness = blockActive * w2 * 0.15
+ * hash11(blockRow + blockTimeSeed * 3.1);
+ baseColor += half3(bandBrightness, bandBrightness * 0.7, bandBrightness * 0.5);
+
+ // ============================================================
+ // STAGE 3: large tear blocks + flicker + dropout regions
+ // ============================================================
+
+ float tearRow = floor(uv.y * _BlockSize * 0.4);
+ float tearTimeSeed = floor(t * 10.0);
+ float tearRand = hash21(float2(tearRow, tearTimeSeed));
+ float tearActive = step(lerp(1.1, 0.4, w3), tearRand);
+
+ float tearDisplace = (hash11(tearRow * 13.7 + tearTimeSeed) - 0.5)
+ * _TearStrength * w3 * tearActive;
+ float2 uvTorn = uv + float2(tearDisplace, 0);
+
+ float tornNoise = hash31(float3(floor(uvTorn * _NoiseScale * 0.8), timeSeed * 1.3));
+ float tornDot = step(1.0 - noiseDensity * 1.5, tornNoise);
+
+ half3 tearColor = half3(tornDot, tornDot, tornDot) * _NoiseBrightness;
+
+ float tearChroma = _ChromaticShift * w3 * 3.0;
+ float tornR = hash31(float3(floor((uvTorn + float2( tearChroma, 0)) * _NoiseScale * 0.8), timeSeed * 1.3));
+ float tornB = hash31(float3(floor((uvTorn + float2(-tearChroma, 0)) * _NoiseScale * 0.8), timeSeed * 1.3));
+ tearColor.r = max(tearColor.r, step(1.0 - noiseDensity * 1.5, tornR) * _NoiseBrightness);
+ tearColor.b = max(tearColor.b, step(1.0 - noiseDensity * 1.5, tornB) * _NoiseBrightness);
+
+ baseColor = lerp(baseColor, tearColor, w3 * tearActive);
+
+ // flicker
+ float flicker = 1.0 + (hash11(floor(t * 15.0)) - 0.5) * _FlickerStrength * w3;
+ baseColor *= flicker;
+
+ // dropout: entire rectangular regions go white or black
+ float2 dropBlock = floor(uv * float2(6, _BlockSize * 0.3));
+ float dropSeed = hash21(dropBlock + floor(t * 12.0));
+ float dropActive = step(1.0 - _DropoutSize * w3, dropSeed);
+ float dropWhite = step(0.5, hash11(dropSeed * 77.7));
+ baseColor = lerp(baseColor, half3(dropWhite, dropWhite, dropWhite), dropActive * w3);
+
+ // random bright horizontal lines
+ float lineRow = floor(uv.y * 200.0);
+ float lineActive = step(0.995 - 0.02 * w3,
+ hash21(float2(lineRow, floor(t * 20.0))));
+ baseColor += lineActive * w3 * 0.6;
+
+ // ============================================================
+ // final output
+ // ============================================================
+
+ half4 result;
+ result.rgb = baseColor;
+ result.a = spriteAlpha;
+ result *= input.color;
+ result.a = spriteAlpha;
+
+ return result;
+ }
+ ENDHLSL
+ }
+
+ Pass
+ {
+ Name "SpriteNoiseGlitchUnlit"
+ Tags { "LightMode" = "SRPDefaultUnlit" }
+
+ HLSLPROGRAM
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma target 3.0
+
+ #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 _NoiseScale;
+ float _NoiseDensity;
+ float _NoiseBrightness;
+ float _ScanlineCount;
+ float _ScanlineStrength;
+ float _BlockSize;
+ float _DisplaceStrength;
+ float _ChromaticShift;
+ float _TearStrength;
+ float _FlickerStrength;
+ float _DropoutSize;
+ 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 hash31(float3 p)
+ {
+ p = frac(p * float3(0.1031, 0.1030, 0.0973));
+ p += dot(p, p.yxz + 33.33);
+ return frac((p.x + p.y) * p.z);
+ }
+
+ float remap01(float v, float lo, float hi)
+ {
+ return saturate((v - lo) / (hi - lo));
+ }
+
+ Varyings vert(Attributes input)
+ {
+ Varyings output;
+ output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
+ output.uv = TRANSFORM_TEX(input.uv, _MainTex);
+ output.color = input.color * _Color;
+ return output;
+ }
+
+ half4 frag(Varyings input) : SV_Target
+ {
+ float intensity = _GlitchIntensity;
+ float t = _Time.y * _Speed;
+ float2 uv = input.uv;
+
+ half4 spriteColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
+ float spriteAlpha = spriteColor.a * input.color.a;
+ clip(spriteAlpha - 0.001);
+
+ float w1 = remap01(intensity, 0.0, 0.45);
+ float w2 = remap01(intensity, 0.3, 0.75);
+ float w3 = remap01(intensity, 0.6, 1.0);
+
+ float timeSeed = floor(t * 8.0);
+ float noiseDensity = _NoiseDensity * remap01(intensity, 0.0, 0.5);
+
+ float noiseVal = hash31(float3(floor(uv * _NoiseScale), timeSeed));
+ float whiteDot = step(1.0 - noiseDensity, noiseVal) * _NoiseBrightness;
+
+ float scanline = sin(uv.y * _ScanlineCount * PI);
+ float scanMask = smoothstep(0.3, 1.0, scanline) * _ScanlineStrength * w1;
+
+ half3 baseColor = half3(whiteDot, whiteDot, whiteDot);
+ baseColor *= (1.0 - scanMask * 0.5);
+
+ float blockRow = floor(uv.y * _BlockSize);
+ float blockTimeSeed = floor(t * 6.0);
+ float blockRand = hash21(float2(blockRow, blockTimeSeed));
+ float blockThreshold = lerp(1.1, 0.3, w2);
+ float blockActive = step(blockThreshold, blockRand);
+
+ float displaceDir = (hash11(blockRow * 7.3 + blockTimeSeed * 1.7) - 0.5) * 2.0;
+ float displace = displaceDir * _DisplaceStrength * w2 * blockActive;
+
+ float2 uvShifted = uv + float2(displace, 0);
+ float chromaOffset = _ChromaticShift * w2 * (1.0 + blockActive * 2.0);
+
+ float noiseR = hash31(float3(floor((uvShifted + float2( chromaOffset, 0)) * _NoiseScale), timeSeed));
+ float noiseG = hash31(float3(floor( uvShifted * _NoiseScale), timeSeed));
+ float noiseB = hash31(float3(floor((uvShifted + float2(-chromaOffset, 0)) * _NoiseScale), timeSeed));
+
+ float dotR = step(1.0 - noiseDensity, noiseR) * _NoiseBrightness;
+ float dotG = step(1.0 - noiseDensity, noiseG) * _NoiseBrightness;
+ float dotB = step(1.0 - noiseDensity, noiseB) * _NoiseBrightness;
+
+ half3 chromaColor = half3(dotR, dotG, dotB);
+ chromaColor *= (1.0 - scanMask * 0.5);
+
+ baseColor = lerp(baseColor, chromaColor, w2);
+
+ float bandBrightness = blockActive * w2 * 0.15
+ * hash11(blockRow + blockTimeSeed * 3.1);
+ baseColor += half3(bandBrightness, bandBrightness * 0.7, bandBrightness * 0.5);
+
+ float tearRow = floor(uv.y * _BlockSize * 0.4);
+ float tearTimeSeed = floor(t * 10.0);
+ float tearRand = hash21(float2(tearRow, tearTimeSeed));
+ float tearActive = step(lerp(1.1, 0.4, w3), tearRand);
+
+ float tearDisplace = (hash11(tearRow * 13.7 + tearTimeSeed) - 0.5)
+ * _TearStrength * w3 * tearActive;
+ float2 uvTorn = uv + float2(tearDisplace, 0);
+
+ float tornNoise = hash31(float3(floor(uvTorn * _NoiseScale * 0.8), timeSeed * 1.3));
+ float tornDot = step(1.0 - noiseDensity * 1.5, tornNoise);
+
+ half3 tearColor = half3(tornDot, tornDot, tornDot) * _NoiseBrightness;
+
+ float tearChroma = _ChromaticShift * w3 * 3.0;
+ float tornR = hash31(float3(floor((uvTorn + float2( tearChroma, 0)) * _NoiseScale * 0.8), timeSeed * 1.3));
+ float tornB = hash31(float3(floor((uvTorn + float2(-tearChroma, 0)) * _NoiseScale * 0.8), timeSeed * 1.3));
+ tearColor.r = max(tearColor.r, step(1.0 - noiseDensity * 1.5, tornR) * _NoiseBrightness);
+ tearColor.b = max(tearColor.b, step(1.0 - noiseDensity * 1.5, tornB) * _NoiseBrightness);
+
+ baseColor = lerp(baseColor, tearColor, w3 * tearActive);
+
+ float flicker = 1.0 + (hash11(floor(t * 15.0)) - 0.5) * _FlickerStrength * w3;
+ baseColor *= flicker;
+
+ float2 dropBlock = floor(uv * float2(6, _BlockSize * 0.3));
+ float dropSeed = hash21(dropBlock + floor(t * 12.0));
+ float dropActive = step(1.0 - _DropoutSize * w3, dropSeed);
+ float dropWhite = step(0.5, hash11(dropSeed * 77.7));
+ baseColor = lerp(baseColor, half3(dropWhite, dropWhite, dropWhite), dropActive * w3);
+
+ float lineRow = floor(uv.y * 200.0);
+ float lineActive = step(0.995 - 0.02 * w3,
+ hash21(float2(lineRow, floor(t * 20.0))));
+ baseColor += lineActive * w3 * 0.6;
+
+ half4 result;
+ result.rgb = baseColor;
+ result.a = spriteAlpha;
+ result *= input.color;
+ result.a = spriteAlpha;
+
+ return result;
+ }
+ ENDHLSL
+ }
+ }
+
+ Fallback "Sprites/Default"
+}
diff --git a/Assets/Resources/Shader/SpriteNoiseGlitch.shader.meta b/Assets/Resources/Shader/SpriteNoiseGlitch.shader.meta
new file mode 100644
index 000000000..2ad28b4c2
--- /dev/null
+++ b/Assets/Resources/Shader/SpriteNoiseGlitch.shader.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 505a54c499a926e419139bbed9fe5a81
+ShaderImporter:
+ externalObjects: {}
+ defaultTextures: []
+ nonModifiableTextures: []
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
new file mode 100644
index 000000000..a0b9d3e1e
--- /dev/null
+++ b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
@@ -0,0 +1,228 @@
+using UnityEngine;
+using System.Collections;
+
+[RequireComponent(typeof(SpriteRenderer))]
+public class SpriteNoiseGlitchController : MonoBehaviour
+{
+ public enum GlitchStage { None, Light, Medium, Heavy }
+
+ [Header("Runtime")]
+ [SerializeField] private GlitchStage _currentStage = GlitchStage.None;
+ [SerializeField, Range(0f, 1f)] private float _intensity;
+ [SerializeField] private float _transitionDuration = 0.5f;
+
+ [Header("Audio (Optional)")]
+ [Tooltip("由 Tools > Generate Glitch Noise Audio 生成")]
+ [SerializeField] private AudioClip _stage1Clip;
+ [SerializeField] private AudioClip _stage2Clip;
+ [SerializeField] private AudioClip _stage3Clip;
+ [SerializeField, Range(0f, 1f)] private float _audioVolume = 0.5f;
+
+ private SpriteRenderer _renderer;
+ private AudioSource _audioSource;
+ private MaterialPropertyBlock _mpb;
+ private Coroutine _transitionCoroutine;
+
+ private static readonly int PropGlitchIntensity = Shader.PropertyToID("_GlitchIntensity");
+
+ private static readonly float[] StageValues = { 0f, 0.25f, 0.55f, 0.95f };
+
+ public float Intensity
+ {
+ get => _intensity;
+ set
+ {
+ _intensity = Mathf.Clamp01(value);
+ ApplyIntensity();
+ }
+ }
+
+ public GlitchStage CurrentStage
+ {
+ get => _currentStage;
+ set => SetStage(value);
+ }
+
+ private void Awake()
+ {
+ _renderer = GetComponent();
+ _mpb = new MaterialPropertyBlock();
+ }
+
+ private void OnEnable()
+ {
+ ApplyIntensity();
+ }
+
+ public void SetStage(GlitchStage stage, bool instant = false)
+ {
+ _currentStage = stage;
+ float target = StageValues[(int)stage];
+ ApplyAudio(stage);
+
+ if (instant || !gameObject.activeInHierarchy)
+ {
+ Intensity = target;
+ return;
+ }
+
+ if (_transitionCoroutine != null)
+ StopCoroutine(_transitionCoroutine);
+ _transitionCoroutine = StartCoroutine(TransitionTo(target));
+ }
+
+ public void SetStageByIndex(int index)
+ {
+ index = Mathf.Clamp(index, 0, 3);
+ SetStage((GlitchStage)index);
+ }
+
+ ///
+ /// 从当前强度平滑过渡到目标值(类似 DOTween),不瞬移。
+ ///
+ /// 目标强度 [0,1]
+ /// 过渡时长(秒)
+ public void TransitionTo(float target, float durationSeconds)
+ {
+ if (durationSeconds > 0f && gameObject.activeInHierarchy)
+ StartCoroutine(TransitionToAndWait(target, durationSeconds));
+ else
+ {
+ target = Mathf.Clamp01(target);
+ Intensity = target;
+ _currentStage = ValueToStage(target);
+ ApplyAudio(_currentStage);
+ }
+ }
+
+ ///
+ /// 从当前强度平滑过渡到目标值,等待完成后返回。供 Yarn 等需要阻塞的调用使用。
+ ///
+ public IEnumerator TransitionToAndWait(float target, float durationSeconds)
+ {
+ target = Mathf.Clamp01(target);
+ float startValue = _intensity;
+
+ if (_transitionCoroutine != null)
+ StopCoroutine(_transitionCoroutine);
+
+ if (durationSeconds <= 0f || !gameObject.activeInHierarchy)
+ {
+ Intensity = target;
+ _currentStage = ValueToStage(target);
+ ApplyAudio(_currentStage);
+ yield break;
+ }
+
+ _transitionCoroutine = StartCoroutine(TransitionToValue(startValue, target, durationSeconds));
+ yield return _transitionCoroutine;
+ _transitionCoroutine = null;
+ }
+
+ ///
+ /// 将 [0,1] 强度映射到对应阶段(用于音效匹配)
+ ///
+ private static GlitchStage ValueToStage(float value)
+ {
+ if (value < 0.125f) return GlitchStage.None;
+ if (value < 0.4f) return GlitchStage.Light;
+ if (value < 0.75f) return GlitchStage.Medium;
+ return GlitchStage.Heavy;
+ }
+
+ private IEnumerator TransitionTo(float target)
+ {
+ float start = _intensity;
+ float elapsed = 0f;
+
+ while (elapsed < _transitionDuration)
+ {
+ elapsed += Time.deltaTime;
+ float t = Mathf.SmoothStep(0f, 1f, elapsed / _transitionDuration);
+ Intensity = Mathf.Lerp(start, target, t);
+ yield return null;
+ }
+
+ Intensity = target;
+ _transitionCoroutine = null;
+ }
+
+ private IEnumerator TransitionToValue(float from, float to, float duration)
+ {
+ float elapsed = 0f;
+
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
+ float current = Mathf.Lerp(from, to, t);
+ Intensity = current;
+
+ // 过渡过程中,若跨越阶段边界则切换对应音效
+ var stage = ValueToStage(current);
+ if (stage != _currentStage)
+ {
+ _currentStage = stage;
+ ApplyAudio(stage);
+ }
+
+ yield return null;
+ }
+
+ Intensity = to;
+ _currentStage = ValueToStage(to);
+ ApplyAudio(_currentStage);
+ _transitionCoroutine = null;
+ }
+
+ private void ApplyIntensity()
+ {
+ if (_renderer == null) return;
+
+ _renderer.GetPropertyBlock(_mpb);
+ _mpb.SetFloat(PropGlitchIntensity, _intensity);
+ _renderer.SetPropertyBlock(_mpb);
+ }
+
+ private void ApplyAudio(GlitchStage stage)
+ {
+ if (_stage1Clip == null && _stage2Clip == null && _stage3Clip == null) return;
+
+ if (_audioSource == null)
+ {
+ _audioSource = GetComponent();
+ if (_audioSource == null)
+ _audioSource = gameObject.AddComponent();
+ }
+
+ _audioSource.Stop();
+
+ AudioClip clip = stage switch
+ {
+ GlitchStage.Light => _stage1Clip,
+ GlitchStage.Medium => _stage2Clip,
+ GlitchStage.Heavy => _stage3Clip,
+ _ => null
+ };
+
+ if (clip != null)
+ {
+ _audioSource.clip = clip;
+ _audioSource.volume = _audioVolume;
+ _audioSource.loop = true;
+ _audioSource.Play();
+ }
+ }
+
+#if UNITY_EDITOR
+ private void OnValidate()
+ {
+ if (_renderer == null)
+ _renderer = GetComponent();
+ if (_renderer != null && _mpb == null)
+ _mpb = new MaterialPropertyBlock();
+
+ ApplyIntensity();
+ }
+#endif
+}
diff --git a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs.meta b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs.meta
new file mode 100644
index 000000000..ce6ef7485
--- /dev/null
+++ b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 85efd99becf18bd48a4cc39cc38100fe
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: