347 lines
14 KiB
HLSL
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
|