feat(huoshan): 添加 Sprite 噪波 Glitch 效果
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 生成与 SpriteNoiseGlitch shader 三阶段对应的程序化音效。
|
||||
/// 菜单: Tools > Generate Glitch Noise Audio
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>Stage 1: 轻微静态噪点 - 柔和白噪声</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Stage 2: 中等 - 更多噪点 + 偶尔 glitch 爆音</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Stage 3: 严重 - 强烈噪点 + 频繁 glitch + 数字故障感</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd5ec77f7adcc584ebc901ac1f73fab6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77ae0e621bc487c439f7c6f9a41b08fe
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94284a48ce2f0164a86f7f8a0c1b4859
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 324f307b690f8a546a58256f6a8b3117
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 505a54c499a926e419139bbed9fe5a81
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<SpriteRenderer>();
|
||||
_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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前强度平滑过渡到目标值(类似 DOTween),不瞬移。
|
||||
/// </summary>
|
||||
/// <param name="target">目标强度 [0,1]</param>
|
||||
/// <param name="durationSeconds">过渡时长(秒)</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前强度平滑过渡到目标值,等待完成后返回。供 Yarn 等需要阻塞的调用使用。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 [0,1] 强度映射到对应阶段(用于音效匹配)
|
||||
/// </summary>
|
||||
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<AudioSource>();
|
||||
if (_audioSource == null)
|
||||
_audioSource = gameObject.AddComponent<AudioSource>();
|
||||
}
|
||||
|
||||
_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<SpriteRenderer>();
|
||||
if (_renderer != null && _mpb == null)
|
||||
_mpb = new MaterialPropertyBlock();
|
||||
|
||||
ApplyIntensity();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85efd99becf18bd48a4cc39cc38100fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user