art(huoshan): 重做屏幕glitch为坏电视故障语汇

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-25 16:06:09 +08:00
co-authored by Cursor
parent b0c6068ea8
commit a1afc021f1
4 changed files with 159 additions and 234 deletions
+5 -7
View File
@@ -29,16 +29,14 @@ Material:
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BloomBoost: 0.9
- _Chaos: 0.6
- _ColorAmount: 1.35
- _Desaturate: 0.9
- _Dispersion: 0.19
- _BloomBoost: 0.6
- _Chaos: 0.5
- _ColorDamage: 1
- _Ghost: 0.35
- _GlitchIntensity: 0
- _Grit: 1
- _PalPhase: 0.62
- _Scale: 1.6
- _Speed: 10
- _Speed: 1
- _SurgeRate: 0.5
- _Vignette: 0.28
- _Warp: 1
@@ -40,7 +40,7 @@ namespace AibisDream
[Header("屏幕 Glitch Shader")]
[Tooltip("仅在屏幕淡入 / expression_screen_glitch 时套用;小游戏阶段保持默认 Sprite 材质,避免进 VolFx 盖住粒子")]
[SerializeField] private Material screenGlitchMaterial;
// 观感全部在材质 M_HuoshanScreenGlitch 上调(只有 6 个宏观旋钮:Scale / Color /
// 观感全部在材质 M_HuoshanScreenGlitch 上调(只有 6 个宏观旋钮:Scale / ColorDamage /
// Warp / Chaos / Grit / SurgeRate)。代码这边只驱动 _GlitchIntensity 档位。
// 不要再把观感参数搬回 Inspector——两处都能改就等于没有真相源,上一版 40 个属性
// 分散在材质和组件两边,结果谁也调不动。
+145 -216
View File
@@ -1,25 +1,28 @@
#ifndef HUOSHAN_SCREEN_GLITCH_INCLUDED
#define HUOSHAN_SCREEN_GLITCH_INCLUDED
// 火山「屏幕」故障:不是电视雪花,而是"想说真心话却卡在喉咙里"的憋压感
// 火山「屏幕」故障:一台坏掉的电视
//
// 只有 6 个宏观旋钮,内部的十几个系数都从它们推导——不要再往外暴露单项参数
// 上一版 40 个属性的教训是:可调 ≠ 能调。
// _Scale 效果尺度(屏幕小就调大,一切跟着变粗)
// _ColorAmount 颜色
// _Warp 撕裂位移
// _Chaos 错误感(块级损坏 + 突发时机
// _Grit 颗粒与扫描线
// 上一版的教训:把原画面整个换成调色板生成的彩虹信号,出来是"彩虹汤"
// 不是"电视坏了"。这版反过来——底下的画面永远保留,损坏全部叠在它上面,
// 语汇只用真实电视的故障:
// · 场同步失锁:整幅画面向上滚,接缝处一条消隐黑条,条附近画面横向弯折
// · 行同步误差:逐行细抖 + 成带的横向撕裂(带内亮度/色相跟着错)
// · 色度不稳:色相漂移 + 色度增益忽大忽小(那块绿会往黄/青方向晃
// · 雪花:静噪颗粒、行级雪花条;「掉台」时整屏塌成雪花再闪回
// · 拟真骨架:扫描线、隔行闪烁、亮点划痕、亮度泵、暗角
//
// 仍然只有 6 个宏观旋钮 + 3 个基调,内部系数全部从它们推导。
// 不要再往外暴露单项参数:可调 ≠ 能调。
// _Scale 效果尺度(屏幕小就调大,扫描线/雪花跟着变粗)
// _ColorDamage 色彩损伤(色相漂移 + 串色 + 彩色镶边)
// _Warp 撕裂 / 滚屏
// _Chaos 掉台频率(信号中断爆发)
// _Grit 雪花与扫描线
// _SurgeRate 情绪节奏
//
// 结构要点:
// 1. 一切先汇成一条「信号」标量:亮度 + 副载波 + 逐行相位误差 + 颗粒 + 块损坏
// 颜色由色散调色板对信号取值(连续渐变,不是纯 RGB 色块=不卡通)。
// 屏幕贴图那块均匀的绿只位移是看不见的,所以它由 _Desaturate 随强度退场。
// 2. 密度按屏幕像素计(fwidth 换算),乘 _Scale;屏幕物件很小时把 _Scale 调大即可,
// 不会因为 UV 空间频率被挤成一团细线。
// 3. 错误感来自「块级损坏」:随机时隙里整块整块地错位、换色、通道旋转,
// 而不是均匀连续地抖——均匀 = 规律 = 假。
// 密度一律按屏幕像素计(fwidth 换算)再乘 _Scale,屏幕物件很小时调大
// _Scale 即可,不会被 UV 空间频率挤成细线
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Attributes
@@ -45,14 +48,12 @@ CBUFFER_START(UnityPerMaterial)
float _GlitchIntensity;
float _Speed;
float _Scale;
float _ColorAmount;
float _ColorDamage;
float _Warp;
float _Chaos;
float _Grit;
float _SurgeRate;
float _Dispersion;
float _PalPhase;
float _Desaturate;
float _Ghost;
float _BloomBoost;
float _Vignette;
CBUFFER_END
@@ -72,7 +73,7 @@ float hash21(float2 p)
return frac((p3.x + p3.y) * p3.z);
}
// 值噪声:正弦叠正弦永远是周期性壁纸,需要非周期调制的地方一律用它。
// 值噪声:需要非周期调制的地方一律用它,正弦叠正弦永远是周期性壁纸
float vnoise(float2 p)
{
float2 i = floor(p);
@@ -84,36 +85,31 @@ float vnoise(float2 p)
f.y);
}
// 色散调色板:三通道相位错开地对同一条信号取值
// 相位差小 = 轻微色偏(很老的机器),接近 1/3 = 满彩虹。输出连续渐变,不会出现卡通纯色块
half3 AnalogPalette(float sig)
// NTSC 味道的色彩不稳定都在 YIQ 空间做:Y 是亮度,IQ 是色度平面
// 色相漂移 = 旋转 IQ,色度增益不稳 = 缩放 IQ,亮度细节始终不动
float3 RGBToYIQ(float3 c)
{
float3 ph = 6.2831853 * (sig * 1.15 + float3(0.0, 1.0, 2.0) * _Dispersion + _PalPhase);
return 0.5 + 0.5 * cos(ph);
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)));
}
// 横向拖抹 + 通道分离:亮度信号被硬拽出拖尾。
half3 SampleSmeared(float2 uv, float smear, float dir, float chroma)
float3 YIQToRGB(float3 c)
{
half3 acc = 0;
float wsum = 0;
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);
}
[unroll]
for (int i = 0; i < 4; i++)
{
float k = (float)i / 3.0;
float w = 1.0 - k * 0.72;
float2 p = uv - float2(smear * k * dir, 0);
half r = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, p + float2(chroma, 0)).r;
half g = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, p).g;
half b = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, p - float2(chroma, 0)).b;
acc += half3(r, g, b) * w;
wsum += w;
}
return acc / max(wsum, 1e-4);
// 彩色镶边:R/B 通道横向拉开。串色最便宜也最"模拟信号"的一层。
half3 SampleFringed(float2 uv, float fringe)
{
half r = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(fringe, 0)).r;
half g = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).g;
half b = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv - float2(fringe, 0)).b;
return half3(r, g, b);
}
Varyings HuoshanScreenGlitchVert(Attributes input)
@@ -143,201 +139,134 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target
return clean;
}
// ── 从 6 个旋钮推导内部系数 ─────────────────────────────
float scale = max(0.25, _Scale);
float ribbonPx = 90.0 * scale; // 彩带周期
float scanPx = 8.0 * scale; // 扫描线周期
float grainPx = 3.0 * scale; // 颗粒格子
float rowPx = 14.0 * scale; // 逐行相位误差 / 掉行的行高
float blockPx = 56.0 * scale; // 块损坏的基准块高
float jagPx = 26.0 * scale; // 锯齿周期
float tearAmt = 0.16 * _Warp;
float jagAmt = 0.05 * _Warp;
float smearAmt = 0.07 * _Warp;
float squeezeAmt= 0.05 * _Warp;
float splitAmt = 0.05 * _Warp;
float blockShift= 0.11 * _Warp;
float grainAmt = 0.16 * _Grit;
float scanAmt = saturate(0.5 * _Grit);
// 每个屏幕像素对应多少 UV。
float2 texel = float2(max(fwidth(uv.x), 1e-7), max(fwidth(uv.y), 1e-7));
float2 pixelPos = uv / texel;
float t = _Time.y * _Speed;
// 模拟信号按"帧"刷新:雪花、行抖动、撕裂带都在帧粒度上重抽,
// 逐渲染帧刷新会快成一片糊,反而没有电视感。
float frameSeed = floor(_Time.y * 18.0 * max(0.05, _Speed));
// ── 情绪泵:越用力越急促。build 慢慢积压,vent 一下泄掉,然后重来
float rate = max(0.02, _SurgeRate) * lerp(0.55, 1.9, intensity);
float phase = _Time.y * rate;
float cyc = frac(phase);
float beat = floor(phase);
float build = pow(smoothstep(0.0, 0.78, cyc), 2.2);
float vent = 1.0 - smoothstep(0.80, 0.96, cyc);
float surge = saturate(build * vent);
// ── 情绪节奏:呼吸式起伏,不给 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 choke = step(1.0 - saturate(0.35 * intensity), hash11(beat * 7.31 + 0.17));
float timeSeed = lerp(floor(t * 10.0), beat * 13.0, choke);
// ── 掉台:随机某几拍信号短暂中断——画面塌成雪花,然后闪回
// 这是"坏掉"最有说服力的一下,频率由 _Chaos 控制。
float dropGate = step(1.0 - saturate(_Chaos * (0.2 + 0.45 * intensity)),
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));
// 持续档位也一直在呼吸:不给 0,保证 hold 住时屏幕仍在小幅挣扎
float drive = intensity * lerp(0.42, 1.0, surge) * lerp(1.0, 1.3, choke * build);
// ── 场同步失锁:某几拍整幅画面向上滚一整圈
float rollGate = step(1.0 - saturate(_Warp * (0.12 + 0.38 * intensity)),
hash11(beat * 3.77 + 1.31));
float rollPos = rollGate * cyc;
float vJit = (hash11(frameSeed * 0.713) - 0.5) * 0.006 * _Warp * drive;
// ── 自下而上的行波:一口气往上顶。结构性的,跟着屏幕比例走
float waveCenter = frac(-_Time.y * 0.45 * lerp(0.45, 1.35, intensity));
float d = uv.y - waveCenter;
d -= floor(d + 0.5);
float waveEnv = exp(-(d * d) / 0.0484);
waveEnv = saturate(waveEnv + intensity * 0.5);
// 消隐黑条骑在滚动接缝上;条附近画面横向弯折(同步电路在挣扎)
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;
// ── 块级损坏:随机时隙里成片出错。这是"错误感"的来源——
// 均匀连续的抖动读作机器在正常地坏,成块的突发才读作数据出错。
float chaosSlot = floor(_Time.y * 7.0);
float burst = step(1.0 - saturate(_Chaos * 0.55), hash11(chaosSlot * 3.71 + 0.5));
float chaosDrive = saturate(drive * (0.3 + burst * 1.7) * _Chaos);
// ── 行同步误差:逐行细抖。drive 平方压住低档,别一开就满屏毛刺。
float rowPx = 3.0 * scale;
float row = floor(pixelPos.y / max(1.0, rowPx));
float rowJit = (hash21(float2(row, frameSeed)) - 0.5)
* 0.005 * _Warp * (drive * drive + drop * 2.0);
// 大块:宽而扁,像整段数据行被搬走。格子原点和大小每个时隙重抽——
// 固定网格上的块一眼就看出是"贴上去的"。
float sizeJit = lerp(0.7, 1.7, hash11(chaosSlot * 4.31));
float2 gridJit = float2(hash11(chaosSlot * 1.71), hash11(chaosSlot * 2.93)) * 17.0;
float2 cellA = floor(float2(pixelPos.x / (blockPx * 2.2 * sizeJit),
pixelPos.y / (blockPx * sizeJit)) + gridJit);
float hitA = step(1.0 - chaosDrive * 0.9, hash21(cellA + chaosSlot));
float shiftA = (hash11(dot(cellA, float2(7.31, 13.17)) + chaosSlot) - 0.5) * 2.0
* blockShift * hitA;
// 块内逐行错位:真数据损坏里块内部每行错得不一样,整块刚体平移读作贴图。
float innerRow = floor(pixelPos.y / max(2.0, rowPx * 0.5));
shiftA *= 0.55 + 0.9 * hash21(float2(innerRow, cellA.x * 3.1 + chaosSlot));
// ── 撕裂带:带高每帧重抽,带内整段错位。等高带太机械。
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(0.90 - 0.32 * saturate(drive + drop), bandRand);
float tear = bandOn * (hash11(bandId * 13.7 + frameSeed) - 0.5) * 2.0
* 0.09 * _Warp * (0.25 + drive + drop);
// 小块:碎片,负责细节上的"啃咬"
float2 cellB = floor(float2(pixelPos.x / (blockPx * 0.55), pixelPos.y / (blockPx * 0.4))
+ gridJit.yx);
float hitB = step(1.0 - chaosDrive * 0.7, hash21(cellB * 1.7 + chaosSlot * 2.3));
float shiftB = (hash11(dot(cellB, float2(3.71, 9.13)) + chaosSlot) - 0.5) * 2.0
* blockShift * 0.45 * hitB;
// 掉台时整帧还会横向乱跳一下。
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 jagNoise = vnoise(float2(uv.y * 3.0, _Time.y * 1.3));
float zig = sin(pixelPos.y * 6.2831853 / max(2.0, jagPx * (0.6 + jagNoise)) + t * 3.0);
zig = sign(zig) * pow(abs(zig), 0.55);
float jag = zig * jagAmt * drive * waveEnv * (0.35 + 0.65 * jagNoise);
// ── 采样:彩色镶边随损坏加深,撕裂带内额外拉开
float fringe = (0.0012 + 0.005 * drive + 0.015 * drop)
* _ColorDamage * (1.0 + bandOn * 1.5);
half3 col = SampleFringed(sampleUV, fringe);
// 撕裂带:带高每个时隙重抽,粗细两层叠加——固定 22 条等高带太机械
float bandScale = lerp(7.0, 24.0, hash11(floor(timeSeed * 0.37) * 5.13));
float band = floor(uv.y * bandScale + hash11(timeSeed) * 4.0);
float bandRand = hash21(float2(band, timeSeed));
float bandActive = step(1.0 - saturate(drive * 0.85), bandRand);
float tear = (hash11(band * 17.13 + timeSeed) - 0.5) * 2.0
* tearAmt * drive * bandActive * (0.35 + waveEnv * 0.85);
// 重影:微弱的错位副本(天线反射的鬼影)
half3 ghost = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex,
sampleUV + float2(0.025 + 0.03 * drive, 0)).rgb;
col += ghost * _Ghost * 0.22 * (0.5 + drive);
// 细撕裂:几像素高的毛刺行,让撕裂边缘"起毛"而不是整齐的粗条
float bandF = floor(uv.y * bandScale * 4.3 + hash11(timeSeed + 2.0) * 9.0);
float fineActive = step(1.0 - saturate(drive * 0.5), hash21(float2(bandF, timeSeed + 5.0)));
tear += (hash11(bandF * 7.77 + timeSeed) - 0.5) * 2.0
* tearAmt * 0.35 * drive * fineActive;
// ── 撕裂带内亮度错档:带和带之间电平接不上,撕裂在平色区域也看得见
col *= 1.0 + bandOn * (hash11(bandId * 7.7 + frameSeed) - 0.4)
* 0.6 * saturate(drive + drop);
float shove = (hash11(beat + 0.5) - 0.5) * 2.0 * tearAmt * 0.3 * surge * intensity;
// ── 色度不稳:色相慢漂 + 撕裂带内色相硬错 + 色度增益忽大忽小。
// 对着那块绿,这层就是"往黄/青方向晃、时浓时淡"。
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));
// 隔行抖动:奇偶行反向的细小错位,CRT 失锁最便宜也最有效的一层
float odd = fmod(floor(pixelPos.y / max(1.0, rowPx * 0.25)), 2.0) * 2.0 - 1.0;
float interlace = odd * 0.0045 * _Warp * drive * (0.3 + waveEnv * 0.7);
// ── 雪花:常驻底噪 + 行级雪花条;掉台时雪花吃掉整个画面
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.96 - 0.10 * drive, hash21(float2(row, frameSeed + 3.0)));
float snowAmt = saturate(
saturate(_Grit) * (0.05 + 0.16 * drive + rowSnow * 0.45 * drive)
+ drop * 0.9);
col = lerp(col, snowVal.xxx * lerp(0.75, 1.1, hash21(cell.yx + frameSeed)), snowAmt);
// 场滚动:泄压瞬间垂直保持失锁,整幅画面向上滑一口——电视坏掉的标志性动作
float rollGate = step(0.7, hash11(beat * 3.77 + 1.3)) * intensity;
float roll = rollGate * smoothstep(0.78, 1.0, cyc) * 0.055 * _Warp;
// ── 亮点划痕:细行上随机的短横白痕,磁带/信号损伤的标志性亮斑
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.995 - 0.015 * drive * _Grit - 0.03 * drop,
hash21(float2(fineRow * 1.7 + seg * 0.31, frameSeed)));
col += spark * (1.0 - frac(pixelPos.x / segPx)) * 1.3 * saturate(drive + drop);
float squeeze = squeezeAmt * surge * intensity;
float2 uvDist;
uvDist.x = uv.x + tear + jag + shove + shiftA + shiftB + interlace;
uvDist.y = 0.5 + (uv.y - 0.5) * (1.0 - squeeze) + roll;
float2 pixelDist = uvDist / texel;
// ── 消隐黑条压暗。
col *= 1.0 - barMask * 0.8;
float smearDir = (hash11(band * 5.71 + timeSeed) < 0.5) ? -1.0 : 1.0;
float smear = smearAmt * drive * (0.3 + waveEnv * 1.0) * (0.4 + bandActive);
float chromaSplit = splitAmt * drive * (0.4 + waveEnv * 1.05) * (1.0 + hitA * 2.0);
half3 tornSprite = SampleSmeared(uvDist, smear, smearDir, chromaSplit);
// ── 亮度信号。原本那块绿随强度退场,否则怎么撕都还是一片绿。
float luma = dot(tornSprite, half3(0.299, 0.587, 0.114));
float signalY = lerp(luma, saturate((luma - 0.5) * 1.6 + 0.5), drive);
// ── 颗粒:满屏细噪,是"被拍下来"最关键的一层,没有它就是矢量图。
float2 grainCell = floor(pixelPos / max(0.5, grainPx));
float grain = (hash21(grainCell + floor(_Time.y * 24.0) * 0.137) - 0.5) * grainAmt;
// ── 副载波:多个八度 + 逐行相位误差(真实模拟信号每一行相位都不同,
// 这是规则几何波形和真实故障的分界线)。相位按位移后的像素坐标取,
// 所以撕裂和块位移会自动把色带错开。
float scanRow = floor(pixelPos.y / max(1.0, rowPx));
float rowPhase = (hash21(float2(scanRow, timeSeed)) - 0.5) * 1.6;
// 纵向调制改用两个八度的值噪声:之前的 sin(y*14)+sin(y*37.8) 是固定周期,
// 出来的彩带像壁纸一样重复。每行的载波频率也随机抖,彩带宽度不再全屏一致。
float phNoise = vnoise(float2(uvDist.y * 7.0, _Time.y * 0.8)) * 2.0
+ vnoise(float2(uvDist.y * 23.0 + 11.0, _Time.y * 1.6));
float rowFreq = 0.72 + 0.56 * hash21(float2(scanRow, floor(_Time.y * 2.0)));
float ribbon =
pixelDist.x * 6.2831853 / max(4.0, ribbonPx * rowFreq)
+ (phNoise - 1.5) * 3.2
+ rowPhase
+ waveEnv * 2.4
- _Time.y * 2.2;
float sI = sin(ribbon);
sI = sign(sI) * pow(abs(sI), 0.85);
// 高强度时把逐像素噪声灌进信号——出来是密集彩点而不是纯色方块。
float speckGate = saturate((intensity - 0.5) / 0.5) * surge
* saturate(bandActive * 0.75 + waveEnv * 0.55);
float2 speckCell = floor(pixelDist / max(1.0, grainPx * 2.0));
float speck = (hash21(speckCell + timeSeed * 0.31) - 0.5) * 2.0;
float gain = _ColorAmount * drive * (0.55 + waveEnv * 0.8);
float sig = signalY + sI * gain + speck * 0.55 * speckGate + grain * 0.6;
// ── 亮度包络:暗处保持暗、亮处冲白,色带自身也有明暗起伏。
float envelope = saturate(signalY * 1.15 + sI * gain * 0.35 + grain);
envelope += smoothstep(0.55, 1.0, envelope) * 0.35; // 光晕溢出
half3 analog = AnalogPalette(sig) * envelope * 1.32;
// ── 混合:原色随强度退场,色散层顶上来。
half3 desatBase = lerp(tornSprite, half3(signalY, signalY, signalY),
saturate(_Desaturate * drive * 1.35));
half3 col = lerp(desatBase, analog, saturate(drive * 1.35));
// ── 块损坏的上色:块必须带着底下的画面坏,整块换成无关颜色就是"贴纸"。
// 量化(马赛克色阶)+ 通道旋转 + 淡淡的调色板偏色,明暗仍来自原信号。
float recolorSeed = hash11(dot(cellA, float2(19.3, 5.7)) + chaosSlot * 1.3);
float post = lerp(5.0, 2.0, recolorSeed);
half3 blockCol = floor(col * post + 0.5) / post;
blockCol = lerp(blockCol, blockCol.gbr, step(0.6, recolorSeed));
blockCol = lerp(blockCol, AnalogPalette(recolorSeed * 3.0 + signalY) * envelope,
0.35 * saturate(_Chaos));
col = lerp(col, blockCol, hitA * saturate(_Chaos * 0.85));
// 小碎块直接旋转通道:硬色偏,参考图里那种突兀的品红/青块。
col = lerp(col, col.brg, hitB * saturate(_Chaos * 0.5));
// ── 掉行:黑色横缝与偶发白色爆行,行高跟着尺度走。
float blackRow = step(0.985 - chaosDrive * 0.14, hash21(float2(scanRow, timeSeed + 3.0)));
col *= 1.0 - blackRow * 0.9;
float whiteRow = step(0.996 - chaosDrive * 0.03, hash21(float2(scanRow, timeSeed + 7.0)));
col += whiteRow * drive * 0.9;
// ── 扫描线:拟真感的骨架。低于约 2 像素分辨不出来只会闪,自动淡出。
// ── 扫描线:低于约 2 像素分辨不出来只会闪,自动淡出。
float scanPx = 4.0 * scale;
float scanFade = saturate((scanPx - 1.6) / 1.4);
float scanWave = 0.5 + 0.5 * sin(pixelPos.y * 6.2831853 / max(1.0, scanPx) + _Time.y * 1.5);
col *= lerp(1.0, scanWave, saturate(scanAmt * scanFade));
float scan = 0.5 + 0.5 * sin(pixelPos.y * 6.2831853 / max(1.0, scanPx));
col *= lerp(1.0, 0.78 + 0.30 * scan, saturate(_Grit * 0.6) * scanFade);
// 隔行闪烁:奇偶场亮度轮流差一点。
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;
// ── 暗角:照片感。
float2 vp = uv - 0.5;
col *= 1.0 - _Vignette * saturate(dot(vp, vp) * 2.0);
// ── 波峰过曝:接现有 Bloom,把"顶到嗓子眼"那一下推出去。
float crest = waveEnv * surge * intensity;
col *= 1.0 + _BloomBoost * crest;
half4 result;
result.rgb = max(0.0, col) * input.color.rgb;
result.a = spriteAlpha;
+8 -10
View File
@@ -1,6 +1,6 @@
Shader "AibisDream/HuoshanScreenGlitch"
{
// 只暴露 6 个宏观旋钮 + 4 个基调,内部十几个系数全部从它们推导。
// 只暴露 6 个宏观旋钮 + 3 个基调,内部十几个系数全部从它们推导。
// 想加新参数前先想清楚:上一版 40 个属性根本没法调。
Properties
{
@@ -9,21 +9,19 @@ Shader "AibisDream/HuoshanScreenGlitch"
[Header(Drive)]
_GlitchIntensity("Glitch Intensity (driven by Yarn)", Range(0, 1)) = 0
_Speed("Animation Speed", Float) = 10
_Speed("Signal Refresh Speed", Range(0.05, 4)) = 1
[Header(Main Knobs)]
_Scale("Scale (screen too small then raise this)", Range(0.25, 6)) = 1.6
_ColorAmount("Color", Range(0, 3)) = 1.35
_Warp("Warp / Tear", Range(0, 2)) = 1
_Chaos("Chaos (block corruption)", Range(0, 1)) = 0.6
_Grit("Grit (grain and scanlines)", Range(0, 2)) = 1
_ColorDamage("Color Damage (hue drift and fringing)", Range(0, 2)) = 1
_Warp("Warp (tear and vertical roll)", Range(0, 2)) = 1
_Chaos("Chaos (signal dropout rate)", Range(0, 1)) = 0.5
_Grit("Grit (static and scanlines)", Range(0, 2)) = 1
_SurgeRate("Surge Rate (emotional pacing)", Range(0.05, 3)) = 0.5
[Header(Tone)]
_Dispersion("Channel Dispersion", Range(0, 0.34)) = 0.19
_PalPhase("Hue Bias", Range(0, 1)) = 0.62
_Desaturate("Original Hue Washout", Range(0, 1)) = 0.9
_BloomBoost("Crest Bloom Boost", Range(0, 3)) = 0.9
_Ghost("Ghost Image", Range(0, 1)) = 0.35
_BloomBoost("Recover Bloom Boost", Range(0, 3)) = 0.6
_Vignette("Vignette", Range(0, 1)) = 0.28
}