art(huoshan): 屏幕glitch改荧光体并支持愤怒红
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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: []
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user