diff --git a/Assets/Materials/M_HuoshanScreenGlitch.mat b/Assets/Materials/M_HuoshanScreenGlitch.mat index d14034182..8712ba60f 100644 --- a/Assets/Materials/M_HuoshanScreenGlitch.mat +++ b/Assets/Materials/M_HuoshanScreenGlitch.mat @@ -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 diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs index 64553cb61..2487e98e8 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs @@ -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 个属性 // 分散在材质和组件两边,结果谁也调不动。 diff --git a/Assets/Shader/HuoshanScreenGlitch.hlsl b/Assets/Shader/HuoshanScreenGlitch.hlsl index c4803a9f5..0bfae5dea 100644 --- a/Assets/Shader/HuoshanScreenGlitch.hlsl +++ b/Assets/Shader/HuoshanScreenGlitch.hlsl @@ -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; diff --git a/Assets/Shader/HuoshanScreenGlitch.shader b/Assets/Shader/HuoshanScreenGlitch.shader index dd5200a55..3d01584fe 100644 --- a/Assets/Shader/HuoshanScreenGlitch.shader +++ b/Assets/Shader/HuoshanScreenGlitch.shader @@ -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 }