From cfacff99438c8314df49243cd16691a0000cb35a Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Sat, 25 Jul 2026 17:28:35 +0800 Subject: [PATCH] =?UTF-8?q?art(huoshan):=20=E5=B1=8F=E5=B9=95glitch?= =?UTF-8?q?=E6=94=B9=E8=8D=A7=E5=85=89=E4=BD=93=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=84=A4=E6=80=92=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- Assets/Materials/M_HuoshanScreenGlitch.mat | 4 + Assets/Shader/HuoshanScreenGlitch.hlsl | 164 +++++++++++++++------ Assets/Shader/HuoshanScreenGlitch.shader | 6 +- 3 files changed, 124 insertions(+), 50 deletions(-) diff --git a/Assets/Materials/M_HuoshanScreenGlitch.mat b/Assets/Materials/M_HuoshanScreenGlitch.mat index 8712ba60f..32cb1e531 100644 --- a/Assets/Materials/M_HuoshanScreenGlitch.mat +++ b/Assets/Materials/M_HuoshanScreenGlitch.mat @@ -32,9 +32,11 @@ Material: - _BloomBoost: 0.6 - _Chaos: 0.5 - _ColorDamage: 1 + - _Emissive: 1.5 - _Ghost: 0.35 - _GlitchIntensity: 0 - _Grit: 1 + - _Rage: 0 - _Scale: 1.6 - _Speed: 1 - _SurgeRate: 0.5 @@ -42,4 +44,6 @@ Material: - _Warp: 1 m_Colors: - _Color: {r: 1, g: 1, b: 1, a: 1} + - _PhosphorColor: {r: 0.44716978, g: 1, b: 0.74689186, a: 1} + - _RageColor: {r: 1, g: 0.188, b: 0.302, a: 1} m_BuildTextureStacks: [] diff --git a/Assets/Shader/HuoshanScreenGlitch.hlsl b/Assets/Shader/HuoshanScreenGlitch.hlsl index 0bfae5dea..c50aefe51 100644 --- a/Assets/Shader/HuoshanScreenGlitch.hlsl +++ b/Assets/Shader/HuoshanScreenGlitch.hlsl @@ -3,16 +3,20 @@ // 火山「屏幕」故障:一台坏掉的电视。 // -// 上一版的教训:把原画面整个换成调色板生成的彩虹信号,出来是"彩虹汤", -// 不是"电视坏了"。这版反过来——底下的画面永远保留,损坏全部叠在它上面, -// 语汇只用真实电视的故障: -// · 场同步失锁:整幅画面向上滚,接缝处一条消隐黑条,条附近画面横向弯折 -// · 行同步误差:逐行细抖 + 成带的横向撕裂(带内亮度/色相跟着错) -// · 色度不稳:色相漂移 + 色度增益忽大忽小(那块绿会往黄/青方向晃) -// · 雪花:静噪颗粒、行级雪花条;「掉台」时整屏塌成雪花再闪回 -// · 拟真骨架:扫描线、隔行闪烁、亮点划痕、亮度泵、暗角 +// 三条原则: +// 1. 屏幕是发光体,不是贴图。贴图固有色被抛弃,只取亮度结构,颜色由 +// _PhosphorColor 荧光色给出;哪怕 intensity=0 也有荧光面、刷新暗带、 +// 微颤、待机扫描线——屏幕平时就"通着电"。超过 1 的亮度交给场景 Bloom。 +// 2. 坏掉丢的是「画面」不是屏幕。信号越差画面内容越暗淡(偶尔挣扎着闪回), +// 顶上来的是电子枪自己画的东西:灰蓝雪花、黑条、回扫亮痕——所以高损坏时 +// 屏幕不再是绿色。 +// 3. 升级感靠新症状而不是"同样的东西更多"。强度分三段解锁: +// 低档:色相不稳 + 逐行细抖;中档:撕裂带 + 滚屏;高档:掉台(塌成雪花再闪回)。 // -// 仍然只有 6 个宏观旋钮 + 3 个基调,内部系数全部从它们推导。 +// 颜色一律不要从色轮 / HSV 里取纯 R/G/B/C/M/Y——纯色块是卡通感的唯一来源, +// 这条踩过两次。偏色只能来自"三个通道对不齐",或 YIQ 上的连续漂移。 +// +// 仍然只有 6 个宏观旋钮 + 基调,内部系数全部从它们推导。 // 不要再往外暴露单项参数:可调 ≠ 能调。 // _Scale 效果尺度(屏幕小就调大,扫描线/雪花跟着变粗) // _ColorDamage 色彩损伤(色相漂移 + 串色 + 彩色镶边) @@ -21,8 +25,7 @@ // _Grit 雪花与扫描线 // _SurgeRate 情绪节奏 // -// 密度一律按屏幕像素计(fwidth 换算)再乘 _Scale,屏幕物件很小时调大 -// _Scale 即可,不会被 UV 空间频率挤成细线。 +// 密度一律按屏幕像素计(fwidth 换算)再乘 _Scale。 #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes @@ -45,7 +48,10 @@ 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; @@ -54,6 +60,7 @@ CBUFFER_START(UnityPerMaterial) float _Grit; float _SurgeRate; float _Ghost; + float _Emissive; float _BloomBoost; float _Vignette; CBUFFER_END @@ -85,6 +92,11 @@ float vnoise(float2 p) 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) @@ -103,13 +115,49 @@ float3 YIQToRGB(float3 c) c.x - 1.106 * c.y + 1.703 * c.z); } -// 彩色镶边:R/B 通道横向拉开。串色最便宜也最"模拟信号"的一层。 -half3 SampleFringed(float2 uv, float fringe) +// 荧光采样:三个通道各取一次横向错开的「亮度」,再乘荧光色。 +// 固有色被抛弃后,串色靠亮度边缘上的通道分离重新长出彩色镶边。 +half3 SamplePhosphorFringed(float2 uv, float fringe, half3 tint) { - 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); + 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) @@ -131,22 +179,34 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target 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) { - half4 clean = baseSample; - clean.rgb *= input.color.rgb; + 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); - float2 texel = float2(max(fwidth(uv.x), 1e-7), max(fwidth(uv.y), 1e-7)); - float2 pixelPos = uv / texel; // 模拟信号按"帧"刷新:雪花、行抖动、撕裂带都在帧粒度上重抽, // 逐渲染帧刷新会快成一片糊,反而没有电视感。 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; @@ -156,8 +216,7 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target float drive = intensity * (0.35 + 0.65 * breathe); // ── 掉台:随机某几拍信号短暂中断——画面塌成雪花,然后闪回。 - // 这是"坏掉"最有说服力的一下,频率由 _Chaos 控制。 - float dropGate = step(1.0 - saturate(_Chaos * (0.2 + 0.45 * intensity)), + 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) @@ -168,7 +227,7 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target * (1.0 - smoothstep(0.64, 0.80, cyc)); // ── 场同步失锁:某几拍整幅画面向上滚一整圈。 - float rollGate = step(1.0 - saturate(_Warp * (0.12 + 0.38 * intensity)), + 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; @@ -184,13 +243,13 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target 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); + * 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(0.90 - 0.32 * saturate(drive + drop), bandRand); + 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); @@ -199,22 +258,31 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target + (hash11(frameSeed * 3.31) - 0.5) * 0.08 * drop; float2 sampleUV = float2(uv.x + xOff, frac(uv.y + rollPos + vJit)); - // ── 采样:彩色镶边随损坏加深,撕裂带内额外拉开。 + // ── 采样:荧光化 + 彩色镶边(随损坏加深,撕裂带内额外拉开)。 float fringe = (0.0012 + 0.005 * drive + 0.015 * drop) * _ColorDamage * (1.0 + bandOn * 1.5); - half3 col = SampleFringed(sampleUV, fringe); + half3 col = SamplePhosphorFringed(sampleUV, fringe, tint); // 重影:微弱的错位副本(天线反射的鬼影)。 - half3 ghost = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, - sampleUV + float2(0.025 + 0.03 * drive, 0)).rgb; - col += ghost * _Ghost * 0.22 * (0.5 + drive); + 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 * (1.0 - 0.75 * flash); // ── 撕裂带内亮度错档:带和带之间电平接不上,撕裂在平色区域也看得见。 col *= 1.0 + bandOn * (hash11(bandId * 7.7 + frameSeed) - 0.4) - * 0.6 * saturate(drive + drop); + * 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; @@ -226,34 +294,33 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target * 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.96 - 0.10 * drive, hash21(float2(row, frameSeed + 3.0))); + float rowSnow = step(0.975 - 0.08 * drive, hash21(float2(row, frameSeed + 3.0))); float snowAmt = saturate( - saturate(_Grit) * (0.05 + 0.16 * drive + rowSnow * 0.45 * drive) + saturate(_Grit) * (0.025 + 0.09 * drive + rowSnow * 0.30 * drive) + + loss * 0.30 + drop * 0.9); - col = lerp(col, snowVal.xxx * lerp(0.75, 1.1, hash21(cell.yx + frameSeed)), snowAmt); + 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.995 - 0.015 * drive * _Grit - 0.03 * drop, + 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)) * 1.3 * saturate(drive + drop); + col += spark * (1.0 - frac(pixelPos.x / segPx)) * saturate(drive + drop); // ── 消隐黑条压暗。 col *= 1.0 - barMask * 0.8; - // ── 扫描线:低于约 2 像素分辨不出来只会闪,自动淡出。 + // ── 隔行闪烁:奇偶场亮度轮流差一点。 float scanPx = 4.0 * scale; - float scanFade = saturate((scanPx - 1.6) / 1.4); - 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; @@ -263,9 +330,8 @@ half4 HuoshanScreenGlitchFrag(Varyings input) : SV_Target 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); + // ── 通电质感 + 发光收尾(损坏时扫描线额外加深)。 + col = PoweredScreen(col, uv, pixelPos, 0.30 * drive); half4 result; result.rgb = max(0.0, col) * input.color.rgb; diff --git a/Assets/Shader/HuoshanScreenGlitch.shader b/Assets/Shader/HuoshanScreenGlitch.shader index 3d01584fe..934637901 100644 --- a/Assets/Shader/HuoshanScreenGlitch.shader +++ b/Assets/Shader/HuoshanScreenGlitch.shader @@ -1,6 +1,6 @@ Shader "AibisDream/HuoshanScreenGlitch" { - // 只暴露 6 个宏观旋钮 + 3 个基调,内部十几个系数全部从它们推导。 + // 只暴露 6 个宏观旋钮 + 5 个基调,内部十几个系数全部从它们推导。 // 想加新参数前先想清楚:上一版 40 个属性根本没法调。 Properties { @@ -9,6 +9,7 @@ Shader "AibisDream/HuoshanScreenGlitch" [Header(Drive)] _GlitchIntensity("Glitch Intensity (driven by Yarn)", Range(0, 1)) = 0 + _Rage("Rage (driven by Yarn, phosphor to red)", Range(0, 1)) = 0 _Speed("Signal Refresh Speed", Range(0.05, 4)) = 1 [Header(Main Knobs)] @@ -20,6 +21,9 @@ Shader "AibisDream/HuoshanScreenGlitch" _SurgeRate("Surge Rate (emotional pacing)", Range(0.05, 3)) = 0.5 [Header(Tone)] + _PhosphorColor("Phosphor Color (screen self color)", Color) = (0.72, 1, 0.82, 1) + _RageColor("Rage Phosphor (LOG3 attack)", Color) = (1, 0.188, 0.302, 1) + _Emissive("Emissive Gain (above 1 feeds Bloom)", Range(0.5, 3)) = 1.5 _Ghost("Ghost Image", Range(0, 1)) = 0.35 _BloomBoost("Recover Bloom Boost", Range(0, 3)) = 0.6 _Vignette("Vignette", Range(0, 1)) = 0.28