Files
aibis-dream/Assets/Shader/HuoshanScreenGlitch.hlsl
T

347 lines
14 KiB
HLSL

#ifndef HUOSHAN_SCREEN_GLITCH_INCLUDED
#define HUOSHAN_SCREEN_GLITCH_INCLUDED
// 火山「屏幕」故障:一台坏掉的电视。
//
// 三条原则:
// 1. 屏幕是发光体,不是贴图。贴图固有色被抛弃,只取亮度结构,颜色由
// _PhosphorColor 荧光色给出;哪怕 intensity=0 也有荧光面、刷新暗带、
// 微颤、待机扫描线——屏幕平时就"通着电"。超过 1 的亮度交给场景 Bloom。
// 2. 坏掉丢的是「画面」不是屏幕。信号越差画面内容越暗淡(偶尔挣扎着闪回),
// 顶上来的是电子枪自己画的东西:灰蓝雪花、黑条、回扫亮痕——所以高损坏时
// 屏幕不再是绿色。
// 3. 升级感靠新症状而不是"同样的东西更多"。强度分三段解锁:
// 低档:色相不稳 + 逐行细抖;中档:撕裂带 + 滚屏;高档:掉台(塌成雪花再闪回)。
//
// 颜色一律不要从色轮 / HSV 里取纯 R/G/B/C/M/Y——纯色块是卡通感的唯一来源,
// 这条踩过两次。偏色只能来自"三个通道对不齐",或 YIQ 上的连续漂移。
//
// 仍然只有 6 个宏观旋钮 + 基调,内部系数全部从它们推导。
// 不要再往外暴露单项参数:可调 ≠ 能调。
// _Scale 效果尺度(屏幕小就调大,扫描线/雪花跟着变粗)
// _ColorDamage 色彩损伤(色相漂移 + 串色 + 彩色镶边)
// _Warp 撕裂 / 滚屏
// _Chaos 掉台频率(信号中断爆发)
// _Grit 雪花与扫描线
// _SurgeRate 情绪节奏
//
// 密度一律按屏幕像素计(fwidth 换算)再乘 _Scale。
#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;
float4 _PhosphorColor;
float4 _RageColor;
float _GlitchIntensity;
float _Rage;
float _Speed;
float _Scale;
float _ColorDamage;
float _Warp;
float _Chaos;
float _Grit;
float _SurgeRate;
float _Ghost;
float _Emissive;
float _BloomBoost;
float _Vignette;
CBUFFER_END
float hash11(float p)
{
p = frac(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return frac(p);
}
float hash21(float2 p)
{
float3 p3 = frac(float3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.x + p3.y) * p3.z);
}
// 值噪声:需要非周期调制的地方一律用它,正弦叠正弦永远是周期性壁纸。
float vnoise(float2 p)
{
float2 i = floor(p);
float2 f = frac(p);
f = f * f * (3.0 - 2.0 * f);
return lerp(
lerp(hash21(i), hash21(i + float2(1, 0)), f.x),
lerp(hash21(i + float2(0, 1)), hash21(i + float2(1, 1)), f.x),
f.y);
}
float Luma(half3 c)
{
return dot(c, half3(0.299, 0.587, 0.114));
}
// NTSC 味道的色彩不稳定都在 YIQ 空间做:Y 是亮度,IQ 是色度平面。
// 色相漂移 = 旋转 IQ,色度增益不稳 = 缩放 IQ,亮度细节始终不动。
float3 RGBToYIQ(float3 c)
{
return float3(
dot(c, float3(0.299, 0.587, 0.114)),
dot(c, float3(0.596, -0.274, -0.322)),
dot(c, float3(0.211, -0.523, 0.312)));
}
float3 YIQToRGB(float3 c)
{
return float3(
c.x + 0.956 * c.y + 0.621 * c.z,
c.x - 0.272 * c.y - 0.647 * c.z,
c.x - 1.106 * c.y + 1.703 * c.z);
}
// 荧光采样:三个通道各取一次横向错开的「亮度」,再乘荧光色。
// 固有色被抛弃后,串色靠亮度边缘上的通道分离重新长出彩色镶边。
half3 SamplePhosphorFringed(float2 uv, float fringe, half3 tint)
{
half lr = Luma(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(fringe, 0)).rgb);
half lg = Luma(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).rgb);
half lb = Luma(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(fringe, 0)).rgb);
return tint * half3(lr, lg, lb);
}
// 通电的屏幕:荧光面(中心微亮)+ 刷新暗带 + 亮度微颤 + 扫描线 + 暗角 + 发光增益。
// 待机时它就是全部——没有这层,屏幕就是一张静态贴图。
// scanExtra:损坏时扫描线额外加深的量。
half3 PoweredScreen(half3 col, float2 uv, float2 pixelPos, float scanExtra)
{
float2 vp = uv - 0.5;
float r2 = dot(vp, vp);
// 荧光面:中心微亮,像电子枪打在玻璃后面。
col *= 1.0 + 0.16 * exp(-r2 * 5.0);
// 刷新暗带:一条宽而淡的暗带缓慢下移(摄像机拍 CRT 的拍频感),
// 是"这是一块正在发光的屏幕"最便宜也最强的暗示。
float bandPos = frac(_Time.y * 0.09);
float dBand = uv.y - bandPos;
dBand -= floor(dBand + 0.5);
col *= 1.0 - 0.05 * exp(-dBand * dBand / 0.02);
// 亮度微颤:1~2%,肉眼几乎读不出来但"活着"。
col *= 1.0 + (hash11(floor(_Time.y * 47.0)) - 0.5) * 0.025;
// 扫描线:待机就有一点,损坏时加深;低于约 2 像素分辨不出来只会闪,自动淡出。
float scanPx = 4.0 * max(0.25, _Scale);
float scanFade = saturate((scanPx - 1.6) / 1.4);
float scan = 0.5 + 0.5 * sin(pixelPos.y * 6.2831853 / max(1.0, scanPx));
float scanAmt = saturate(_Grit) * saturate(0.12 + scanExtra) * scanFade;
col *= lerp(1.0, 0.7 + 0.3 * scan, scanAmt);
// 暗角。
col *= 1.0 - _Vignette * saturate(r2 * 2.0);
// 发光:整体抬过 1,超出部分交给场景 Bloom。
return col * _Emissive;
}
Varyings HuoshanScreenGlitchVert(Attributes input)
{
Varyings o;
o.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
o.uv = TRANSFORM_TEX(input.uv, _MainTex);
o.color = input.color * _Color;
return o;
}
half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target
{
float intensity = saturate(_GlitchIntensity);
float2 uv = input.uv;
// 轮廓永远取自未位移的 UV:屏幕形状不会被撕出缺口。
half4 baseSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
float spriteAlpha = baseSample.a * input.color.a;
clip(spriteAlpha - 0.001);
float2 texel = float2(max(fwidth(uv.x), 1e-7), max(fwidth(uv.y), 1e-7));
float2 pixelPos = uv / texel;
// 愤怒红:LOG3 攻击时整块屏幕的荧光换成红。
half3 tint = lerp(_PhosphorColor.rgb, _RageColor.rgb, saturate(_Rage));
// ── 待机:无损坏时也要是一块通电的荧光屏。
if (intensity <= 0.0005)
{
half3 idle = tint * Luma(baseSample.rgb);
idle = PoweredScreen(idle, uv, pixelPos, 0.0);
half4 clean;
clean.rgb = idle * input.color.rgb;
clean.a = spriteAlpha;
return clean;
}
float scale = max(0.25, _Scale);
// 模拟信号按"帧"刷新:雪花、行抖动、撕裂带都在帧粒度上重抽,
// 逐渲染帧刷新会快成一片糊,反而没有电视感。
float frameSeed = floor(_Time.y * 18.0 * max(0.05, _Speed));
// ── 症状分段:升级感来自新的坏法解锁,而不是同一种坏法变多。
float tearStage = smoothstep(0.15, 0.40, intensity); // 撕裂带 / 划痕
float rollStage = smoothstep(0.35, 0.62, intensity); // 滚屏
float dropStage = smoothstep(0.55, 0.80, intensity); // 掉台
// ── 情绪节奏:呼吸式起伏,不给 0,保证 hold 住时画面仍在小幅挣扎。
float rate = max(0.05, _SurgeRate) * lerp(0.7, 1.7, intensity);
float ph = _Time.y * rate;
float beat = floor(ph);
float cyc = frac(ph);
float breathe = vnoise(float2(ph * 1.3, 7.7));
float drive = intensity * (0.35 + 0.65 * breathe);
// ── 掉台:随机某几拍信号短暂中断——画面塌成雪花,然后闪回。
float dropGate = step(1.0 - saturate(_Chaos * 0.65 * dropStage),
hash11(beat * 5.13 + 0.71));
float drop = dropGate
* smoothstep(0.30, 0.34, cyc)
* (1.0 - smoothstep(0.58, 0.66, cyc));
// 信号回来那一瞬电平过冲,交给场景 Bloom 推出去。
float recover = dropGate
* smoothstep(0.60, 0.64, cyc)
* (1.0 - smoothstep(0.64, 0.80, cyc));
// ── 场同步失锁:某几拍整幅画面向上滚一整圈。
float rollGate = step(1.0 - saturate(_Warp * 0.5 * rollStage),
hash11(beat * 3.77 + 1.31));
float rollPos = rollGate * cyc;
float vJit = (hash11(frameSeed * 0.713) - 0.5) * 0.006 * _Warp * drive;
// 消隐黑条骑在滚动接缝上;条附近画面横向弯折(同步电路在挣扎)。
float dSeam = uv.y - frac(1.0 - rollPos - vJit);
dSeam -= floor(dSeam + 0.5);
float barMask = rollGate * exp(-dSeam * dSeam / 0.0009);
float skewDir = hash11(beat * 9.13 + 0.4) < 0.5 ? -1.0 : 1.0;
float skew = rollGate * skewDir * exp(-abs(dSeam) * 22.0) * 0.10 * _Warp;
// ── 行同步误差:逐行细抖。drive 平方压住低档,别一开就满屏毛刺。
float rowPx = 3.0 * scale;
float row = floor(pixelPos.y / max(1.0, rowPx));
float rowJit = (hash21(float2(row, frameSeed)) - 0.5)
* 0.004 * _Warp * (drive * drive + drop * 2.0);
// ── 撕裂带:带高每帧重抽,带内整段错位。等高带太机械。
float bandH = lerp(0.05, 0.16, hash11(frameSeed * 1.71));
float bandId = floor((uv.y + hash11(frameSeed) * 0.37) / bandH);
float bandRand = hash21(float2(bandId, frameSeed));
float bandOn = step(1.0 - (0.10 + 0.30 * saturate(drive + drop)) * tearStage, bandRand);
float tear = bandOn * (hash11(bandId * 13.7 + frameSeed) - 0.5) * 2.0
* 0.09 * _Warp * (0.25 + drive + drop);
// 掉台时整帧还会横向乱跳一下。
float xOff = rowJit + tear + skew
+ (hash11(frameSeed * 3.31) - 0.5) * 0.08 * drop;
float2 sampleUV = float2(uv.x + xOff, frac(uv.y + rollPos + vJit));
// ── 采样:荧光化 + 彩色镶边(随损坏加深,撕裂带内额外拉开)。
float fringe = (0.0018 + 0.009 * drive + 0.022 * drop)
* _ColorDamage * (1.0 + bandOn * 1.5);
half3 col = SamplePhosphorFringed(sampleUV, fringe, tint);
// 重影:微弱的错位副本(天线反射的鬼影)。
half lGhost = Luma(SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex,
sampleUV + float2(0.025 + 0.03 * drive, 0)).rgb);
col += tint * lGhost * _Ghost * 0.22 * (0.5 + drive);
// ── 信号丢失:坏掉丢的是画面。信号越差画面内容越暗淡,
// 只在随机某几拍挣扎着闪回来一下——高损坏时屏幕不再是绿色。
float loss = smoothstep(0.30, 0.95, intensity) * (0.55 + 0.45 * breathe);
float flashGate = step(0.55, hash11(beat * 2.17 + 0.9));
float flash = flashGate
* smoothstep(0.15, 0.25, cyc)
* (1.0 - smoothstep(0.40, 0.55, cyc));
// 画面不压到全黑:留一层底,撕裂 / 滚屏 / 色度错乱才有东西可作用在上面。
// 压死了就只剩雪花,读作"没信号"而不是"信号在坏"。
col *= 1.0 - loss * 0.66 * (1.0 - 0.75 * flash);
// ── 撕裂带内亮度错档:带和带之间电平接不上,撕裂在平色区域也看得见。
col *= 1.0 + bandOn * (hash11(bandId * 7.7 + frameSeed) - 0.4)
* 0.45 * saturate(drive + drop);
// ── 色度不稳:色相慢漂 + 撕裂带内色相硬错 + 色度增益忽大忽小。
// 这是低档唯一的彩色症状:荧光色往黄/青方向晃、时浓时淡。
float3 yiq = RGBToYIQ(col);
float hueAng = (vnoise(float2(_Time.y * 0.9, row * 0.02)) - 0.5)
* 2.4 * _ColorDamage * drive;
hueAng += bandOn * (bandRand - 0.5) * 1.4 * _ColorDamage * saturate(drive + drop);
float cs = cos(hueAng);
float sn = sin(hueAng);
yiq.yz = float2(yiq.y * cs - yiq.z * sn, yiq.y * sn + yiq.z * cs);
yiq.yz *= 1.0 + (vnoise(float2(_Time.y * 1.7, 13.1)) - 0.5)
* 1.6 * _ColorDamage * drive;
col = max(0.0, YIQToRGB(yiq));
// ── 雪花:电子枪自己画的东西,灰蓝色调不带荧光绿。
// 底噪压得很低(干净),信号丢失和掉台时才大量顶上来。
float grainPx = 1.6 * scale;
float2 cell = floor(pixelPos / max(1.0, grainPx));
float snowVal = hash21(cell + frameSeed * float2(11.13, 5.71));
float rowSnow = step(0.975 - 0.08 * drive, hash21(float2(row, frameSeed + 3.0)));
// 信号丢失 / 掉台的雪花也走 _Grit——否则高档位时雪花由 loss、drop 独断,
// 旋钮拧到 0 都压不下去,屏幕只会糊成一片白。
float snowAmt = saturate(saturate(_Grit) * (
0.025 + 0.09 * drive + rowSnow * 0.30 * drive
+ loss * 0.16
+ drop * 0.55));
half3 snowCol = snowVal * half3(0.92, 0.97, 1.08)
* lerp(0.7, 1.05, hash21(cell.yx + frameSeed));
col = lerp(col, snowCol, snowAmt);
// ── 亮点划痕:细行上偶发的短横白痕(回扫亮痕),别太密,密了就脏。
float fineRow = floor(pixelPos.y / max(1.0, 1.5 * scale));
float segPx = 26.0 * scale;
float seg = floor(pixelPos.x / segPx);
float spark = step(0.997 - (0.010 * drive * _Grit + 0.02 * drop) * tearStage,
hash21(float2(fineRow * 1.7 + seg * 0.31, frameSeed)));
col += spark * (1.0 - frac(pixelPos.x / segPx)) * saturate(drive + drop);
// ── 消隐黑条压暗。
col *= 1.0 - barMask * 0.8;
// ── 隔行闪烁:奇偶场亮度轮流差一点。
float scanPx = 4.0 * scale;
float parity = fmod(floor(_Time.y * 29.97), 2.0);
float field = fmod(floor(pixelPos.y / max(1.0, scanPx * 0.5)), 2.0);
col *= 1.0 - abs(field - parity) * 0.08 * saturate(_Grit) * drive;
// ── 整体电平:亮度泵着走,掉台时电平跌,恢复瞬间过冲。
col *= 1.0 + (vnoise(float2(_Time.y * 2.3, 31.7)) - 0.5) * 0.3 * drive;
col *= 1.0 - drop * 0.25;
col *= 1.0 + _BloomBoost * recover;
// ── 通电质感 + 发光收尾(损坏时扫描线额外加深)。
col = PoweredScreen(col, uv, pixelPos, 0.30 * drive);
half4 result;
result.rgb = max(0.0, col) * input.color.rgb;
result.a = spriteAlpha;
return result;
}
#endif // HUOSHAN_SCREEN_GLITCH_INCLUDED