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: