feat(huoshan): 新增屏幕glitch shader与强度驱动
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: M_HuoshanScreenGlitch
|
||||
m_Shader: {fileID: 4800000, guid: 34b7b1bac3f94aae80724d2934758989, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
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
|
||||
- _GlitchIntensity: 0
|
||||
- _Grit: 1
|
||||
- _PalPhase: 0.62
|
||||
- _Scale: 1.6
|
||||
- _Speed: 10
|
||||
- _SurgeRate: 0.5
|
||||
- _Vignette: 0.28
|
||||
- _Warp: 1
|
||||
m_Colors:
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7409d06033d4497a8aff0a45ed262adf
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -74,6 +74,40 @@ public class SpriteNoiseGlitchController : MonoBehaviour
|
||||
|
||||
public CompositeMode CurrentCompositeMode => _compositeMode;
|
||||
|
||||
/// <summary>
|
||||
/// 仅对齐位置/尺寸与 sortingOrder,不改 Layer。
|
||||
/// glitch_transition 的 noise 必须留在 Default;若改成 HuoshanScreen 会进 VolFx 叠在粒子上。
|
||||
/// </summary>
|
||||
public void ConfigureOverlayFor(SpriteRenderer targetRenderer, int sortingOrderOffset = 1)
|
||||
{
|
||||
EnsureRuntimeObjects();
|
||||
if (_renderer == null || _renderer.sprite == null || targetRenderer == null)
|
||||
return;
|
||||
|
||||
_renderer.sortingLayerID = targetRenderer.sortingLayerID;
|
||||
_renderer.sortingOrder = targetRenderer.sortingOrder + sortingOrderOffset;
|
||||
|
||||
Bounds targetBounds = targetRenderer.bounds;
|
||||
Bounds overlayBounds = _renderer.bounds;
|
||||
if (targetBounds.size.x <= Mathf.Epsilon ||
|
||||
targetBounds.size.y <= Mathf.Epsilon ||
|
||||
overlayBounds.size.x <= Mathf.Epsilon ||
|
||||
overlayBounds.size.y <= Mathf.Epsilon)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 position = transform.position;
|
||||
position.x = targetBounds.center.x;
|
||||
position.y = targetBounds.center.y;
|
||||
transform.position = position;
|
||||
|
||||
Vector3 scale = transform.localScale;
|
||||
scale.x *= targetBounds.size.x / overlayBounds.size.x;
|
||||
scale.y *= targetBounds.size.y / overlayBounds.size.y;
|
||||
transform.localScale = scale;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_renderer = GetComponent<SpriteRenderer>();
|
||||
|
||||
@@ -34,11 +34,31 @@ namespace AibisDream
|
||||
[SerializeField, Min(1f)] private float screenBloomBrightness = 1.35f;
|
||||
|
||||
[Header("Glitch 噪波效果")]
|
||||
[Tooltip("噪波故障效果控制器,可选;置于 Expression View 子物体上")]
|
||||
[Tooltip("记忆/对白阶段的透明噪波叠层(glitch_transition)。expression_screen_glitch 已改走屏幕 Shader。")]
|
||||
[SerializeField] private SpriteNoiseGlitchController glitchController;
|
||||
|
||||
[Header("屏幕 Glitch Shader")]
|
||||
[Tooltip("仅在屏幕淡入 / expression_screen_glitch 时套用;小游戏阶段保持默认 Sprite 材质,避免进 VolFx 盖住粒子")]
|
||||
[SerializeField] private Material screenGlitchMaterial;
|
||||
// 观感全部在材质 M_HuoshanScreenGlitch 上调(只有 6 个宏观旋钮:Scale / Color /
|
||||
// Warp / Chaos / Grit / SurgeRate)。代码这边只驱动 _GlitchIntensity 档位。
|
||||
// 不要再把观感参数搬回 Inspector——两处都能改就等于没有真相源,上一版 40 个属性
|
||||
// 分散在材质和组件两边,结果谁也调不动。
|
||||
|
||||
public LogReleasePresentationController LogReleasePresentation => logReleasePresentation;
|
||||
|
||||
/// <summary>当前屏幕 Glitch 档位(0–1),由 expression_screen_glitch 持续保持。</summary>
|
||||
public float ScreenGlitchLevel => screenGlitchIntensity;
|
||||
|
||||
private static readonly int ScreenGlitchIntensityId = Shader.PropertyToID("_GlitchIntensity");
|
||||
|
||||
private Material screenDefaultMaterial;
|
||||
private Material screenFallbackMaterial;
|
||||
private Material screenGlitchMaterialInstance;
|
||||
private Coroutine screenGlitchRampRoutine;
|
||||
private float screenGlitchIntensity;
|
||||
private bool screenGlitchMaterialActive;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 注册到系统字典
|
||||
@@ -66,6 +86,13 @@ namespace AibisDream
|
||||
glitchController = GetComponentInChildren<SpriteNoiseGlitchController>(true);
|
||||
}
|
||||
|
||||
// glitch_transition 用的 noise:必须停在 Default,绝不能进 HuoshanScreen(否则会叠进 VolFx 盖住粒子)。
|
||||
PrepareNoiseOverlayForGlitchTransition(false);
|
||||
|
||||
CacheScreenDefaultMaterial();
|
||||
// 小游戏阶段绝不能提前套 Glitch 材质:其 SRPDefaultUnlit 会进 VolFx huoshan 合成并盖住粒子。
|
||||
RestoreScreenDefaultMaterial();
|
||||
|
||||
if (logReleasePresentation == null)
|
||||
{
|
||||
logReleasePresentation = GetComponent<LogReleasePresentationController>();
|
||||
@@ -119,6 +146,15 @@ namespace AibisDream
|
||||
|
||||
private void HideSpriteOverlaysForMinigame()
|
||||
{
|
||||
if (screenGlitchRampRoutine != null)
|
||||
{
|
||||
StopCoroutine(screenGlitchRampRoutine);
|
||||
screenGlitchRampRoutine = null;
|
||||
}
|
||||
screenGlitchIntensity = 0f;
|
||||
// 粒子玩法必须回到默认 Sprite 材质,否则屏幕会进 VolFx 层把字盖掉。
|
||||
RestoreScreenDefaultMaterial();
|
||||
|
||||
List<SpriteRenderer> renderers = CollectSpriteOverlayRenderers();
|
||||
for (int i = 0; i < renderers.Count; i++)
|
||||
{
|
||||
@@ -262,6 +298,13 @@ namespace AibisDream
|
||||
/// </summary>
|
||||
public void CloseView()
|
||||
{
|
||||
if (screenGlitchRampRoutine != null)
|
||||
{
|
||||
StopCoroutine(screenGlitchRampRoutine);
|
||||
screenGlitchRampRoutine = null;
|
||||
}
|
||||
SetScreenGlitchIntensity(0f);
|
||||
|
||||
if (logReleasePresentation != null)
|
||||
{
|
||||
logReleasePresentation.ForceCleanupImmediate(true);
|
||||
@@ -315,6 +358,9 @@ namespace AibisDream
|
||||
sr.gameObject.SetActive(true);
|
||||
sequence.Join(sr.DOFade(targetAlpha, fadeDuration).SetEase(Ease.OutQuad));
|
||||
}
|
||||
// 淡入演出屏幕时才启用 Glitch 材质;强度保持 0,外观与普通屏幕一致。
|
||||
ActivateScreenGlitchMaterial();
|
||||
ApplyScreenGlitchProperties();
|
||||
|
||||
yield return sequence.WaitForCompletion();
|
||||
|
||||
@@ -332,6 +378,9 @@ namespace AibisDream
|
||||
if (renderers.Count == 0) yield break;
|
||||
|
||||
float fadeDuration = Mathf.Max(0.01f, duration);
|
||||
// Glitch 档位是保持型的:屏幕淡出时一并收掉,免得下一段淡入时带着上一段的故障。
|
||||
SetScreenGlitchLevel(0f, fadeDuration);
|
||||
|
||||
Sequence sequence = DOTween.Sequence().SetTarget(this);
|
||||
for (int i = 0; i < renderers.Count; i++)
|
||||
{
|
||||
@@ -392,21 +441,220 @@ namespace AibisDream
|
||||
sr.color = locked;
|
||||
}
|
||||
|
||||
private void CacheScreenDefaultMaterial()
|
||||
{
|
||||
if (screenOverlayRenderer == null || screenDefaultMaterial != null)
|
||||
return;
|
||||
|
||||
var current = screenOverlayRenderer.sharedMaterial;
|
||||
// 若场景里误挂了 Glitch 材质,仍回退到 URP 默认 Sprite,保证小游戏可见。
|
||||
if (current != null && current.shader != null &&
|
||||
current.shader.name == "AibisDream/HuoshanScreenGlitch")
|
||||
{
|
||||
screenDefaultMaterial = null;
|
||||
return;
|
||||
}
|
||||
|
||||
screenDefaultMaterial = current;
|
||||
}
|
||||
|
||||
private void RestoreScreenDefaultMaterial()
|
||||
{
|
||||
if (screenOverlayRenderer == null)
|
||||
return;
|
||||
|
||||
CacheScreenDefaultMaterial();
|
||||
screenGlitchMaterialActive = false;
|
||||
screenGlitchIntensity = 0f;
|
||||
|
||||
if (screenDefaultMaterial != null)
|
||||
{
|
||||
screenOverlayRenderer.sharedMaterial = screenDefaultMaterial;
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenOverlayRenderer.sharedMaterial != null &&
|
||||
screenOverlayRenderer.sharedMaterial.shader != null &&
|
||||
screenOverlayRenderer.sharedMaterial.shader.name == "AibisDream/HuoshanScreenGlitch")
|
||||
{
|
||||
// 场景直接挂了 Glitch 材质时要换掉,否则它的 SRPDefaultUnlit 会进 VolFx 合成盖住粒子。
|
||||
// 绝不能置 null——SpriteRenderer 的 material 为 null 会渲染成品红方块,
|
||||
// 表现为 switch_fix_system_to Expression 后闪一下粉色。
|
||||
screenOverlayRenderer.sharedMaterial = GetScreenFallbackMaterial();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Glitch 噪波过渡:从当前强度平滑过渡到 to,不瞬移(类似 DOTween)。
|
||||
/// 场景没有提供干净的屏幕材质时的兜底:标准 Sprite 材质。
|
||||
/// </summary>
|
||||
private Material GetScreenFallbackMaterial()
|
||||
{
|
||||
if (screenFallbackMaterial != null)
|
||||
return screenFallbackMaterial;
|
||||
|
||||
Shader spriteShader = Shader.Find("Sprites/Default");
|
||||
if (spriteShader == null)
|
||||
{
|
||||
Debug.LogError("[ExpressionManager] 找不到 Sprites/Default;屏幕材质无法回退。", this);
|
||||
return null;
|
||||
}
|
||||
|
||||
screenFallbackMaterial = new Material(spriteShader) { name = "Screen Sprite Fallback" };
|
||||
return screenFallbackMaterial;
|
||||
}
|
||||
|
||||
private void ActivateScreenGlitchMaterial()
|
||||
{
|
||||
if (screenOverlayRenderer == null)
|
||||
return;
|
||||
|
||||
CacheScreenDefaultMaterial();
|
||||
|
||||
if (screenGlitchMaterial == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"[ExpressionManager] 未找到屏幕 Glitch 材质;请把 M_HuoshanScreenGlitch 赋给 screenGlitchMaterial。",
|
||||
this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenGlitchMaterialInstance == null)
|
||||
{
|
||||
screenGlitchMaterialInstance = new Material(screenGlitchMaterial)
|
||||
{
|
||||
name = screenGlitchMaterial.name + " (Screen Instance)"
|
||||
};
|
||||
}
|
||||
|
||||
screenOverlayRenderer.sharedMaterial = screenGlitchMaterialInstance;
|
||||
screenGlitchMaterialActive = true;
|
||||
ApplyScreenGlitchProperties();
|
||||
}
|
||||
|
||||
private void ApplyScreenGlitchProperties()
|
||||
{
|
||||
if (!screenGlitchMaterialActive || screenGlitchMaterialInstance == null)
|
||||
return;
|
||||
|
||||
// 只驱动强度档位;其余观感是材质的事,见 lookLivesOnMaterial。
|
||||
screenGlitchMaterialInstance.SetFloat(ScreenGlitchIntensityId, screenGlitchIntensity);
|
||||
}
|
||||
|
||||
private void SetScreenGlitchIntensity(float value)
|
||||
{
|
||||
screenGlitchIntensity = Mathf.Clamp01(value);
|
||||
if (screenGlitchIntensity > 0f && !screenGlitchMaterialActive)
|
||||
ActivateScreenGlitchMaterial();
|
||||
ApplyScreenGlitchProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复 glitch_transition 用的 noise 叠层到原始层级关系:
|
||||
/// Default 层 + sortingOrder 10 + OpaqueBackground。
|
||||
/// 不要对齐/改到 HuoshanScreen,否则会进 VolFx 合成叠在粒子上面。
|
||||
/// </summary>
|
||||
private void PrepareNoiseOverlayForGlitchTransition(bool activateForUse)
|
||||
{
|
||||
if (glitchController == null)
|
||||
return;
|
||||
|
||||
const int defaultLayer = 0;
|
||||
glitchController.gameObject.layer = defaultLayer;
|
||||
|
||||
var noiseRenderer = glitchController.GetComponent<SpriteRenderer>();
|
||||
if (noiseRenderer != null)
|
||||
{
|
||||
noiseRenderer.sortingLayerID = 0;
|
||||
noiseRenderer.sortingOrder = 10;
|
||||
if (activateForUse)
|
||||
noiseRenderer.enabled = true;
|
||||
}
|
||||
|
||||
// 与场景原始配置一致:不透明黑底噪波(强度 0 时几乎全黑底,由上层 HuoshanScreen 内容盖住中心)。
|
||||
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.OpaqueBackground);
|
||||
|
||||
if (activateForUse && !glitchController.gameObject.activeSelf)
|
||||
glitchController.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Glitch 噪波过渡:驱动 ExpressView/noise(SpriteNoiseGlitch),非屏幕 Shader。
|
||||
/// </summary>
|
||||
public void TransitionGlitch(float to, float duration = 0f)
|
||||
{
|
||||
if (glitchController == null) return;
|
||||
PrepareNoiseOverlayForGlitchTransition(true);
|
||||
glitchController.TransitionTo(to, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把「屏幕」Sprite 的故障档位推到 <paramref name="level"/> 并保持住(非阻塞)。
|
||||
/// 从当前档位平滑过渡,<paramref name="duration"/> 是到达该档位的时间;到位后不会自动回落,
|
||||
/// 需要再调一次 level=0 才收掉。Shader 内部自带「憋气→用力→泄掉」的循环,
|
||||
/// 所以保持档位时屏幕依然在持续挣扎,不是一张静止的花屏。
|
||||
/// </summary>
|
||||
public void SetScreenGlitchLevel(float level, float duration)
|
||||
{
|
||||
if (screenOverlayRenderer == null)
|
||||
return;
|
||||
|
||||
float target = Mathf.Clamp01(level);
|
||||
|
||||
if (target > 0f)
|
||||
{
|
||||
ActivateScreenGlitchMaterial();
|
||||
if (!screenGlitchMaterialActive)
|
||||
return;
|
||||
|
||||
// 确保屏幕可见(否则材质在动但物体是关的)。
|
||||
screenOverlayRenderer.enabled = true;
|
||||
screenOverlayRenderer.gameObject.SetActive(true);
|
||||
}
|
||||
else if (!screenGlitchMaterialActive)
|
||||
{
|
||||
screenGlitchIntensity = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenGlitchRampRoutine != null)
|
||||
{
|
||||
StopCoroutine(screenGlitchRampRoutine);
|
||||
screenGlitchRampRoutine = null;
|
||||
}
|
||||
|
||||
float ramp = Mathf.Max(0f, duration);
|
||||
if (ramp <= 0.02f || !isActiveAndEnabled)
|
||||
{
|
||||
SetScreenGlitchIntensity(target);
|
||||
return;
|
||||
}
|
||||
|
||||
screenGlitchRampRoutine = StartCoroutine(RampScreenGlitch(target, ramp));
|
||||
}
|
||||
|
||||
private IEnumerator RampScreenGlitch(float target, float duration)
|
||||
{
|
||||
float start = screenGlitchIntensity;
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
|
||||
SetScreenGlitchIntensity(Mathf.Lerp(start, target, t));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetScreenGlitchIntensity(target);
|
||||
screenGlitchRampRoutine = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Glitch 噪波过渡并等待完成:从当前强度平滑过渡到 to。
|
||||
/// </summary>
|
||||
public IEnumerator TransitionGlitchAndWait(float to, float duration = 0f)
|
||||
{
|
||||
if (glitchController == null) yield break;
|
||||
PrepareNoiseOverlayForGlitchTransition(true);
|
||||
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
|
||||
}
|
||||
|
||||
@@ -416,6 +664,7 @@ namespace AibisDream
|
||||
public IEnumerator TransitionGlitchAndWait(float from, float to, float duration)
|
||||
{
|
||||
if (glitchController == null) yield break;
|
||||
PrepareNoiseOverlayForGlitchTransition(true);
|
||||
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
|
||||
}
|
||||
|
||||
@@ -476,9 +725,37 @@ namespace AibisDream
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (screenGlitchRampRoutine != null)
|
||||
{
|
||||
StopCoroutine(screenGlitchRampRoutine);
|
||||
screenGlitchRampRoutine = null;
|
||||
}
|
||||
SetScreenGlitchIntensity(0f);
|
||||
|
||||
if (logReleasePresentation != null)
|
||||
logReleasePresentation.ForceCleanupImmediate(true);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (screenGlitchMaterialInstance != null)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
Destroy(screenGlitchMaterialInstance);
|
||||
else
|
||||
DestroyImmediate(screenGlitchMaterialInstance);
|
||||
screenGlitchMaterialInstance = null;
|
||||
}
|
||||
|
||||
if (screenFallbackMaterial != null)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
Destroy(screenFallbackMaterial);
|
||||
else
|
||||
DestroyImmediate(screenFallbackMaterial);
|
||||
screenFallbackMaterial = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
#ifndef HUOSHAN_SCREEN_GLITCH_INCLUDED
|
||||
#define HUOSHAN_SCREEN_GLITCH_INCLUDED
|
||||
|
||||
// 火山「屏幕」故障:不是电视雪花,而是"想说真心话却卡在喉咙里"的憋压感。
|
||||
//
|
||||
// 只有 6 个宏观旋钮,内部的十几个系数都从它们推导——不要再往外暴露单项参数,
|
||||
// 上一版 40 个属性的教训是:可调 ≠ 能调。
|
||||
// _Scale 效果尺度(屏幕小就调大,一切跟着变粗)
|
||||
// _ColorAmount 颜色
|
||||
// _Warp 撕裂位移
|
||||
// _Chaos 错误感(块级损坏 + 突发时机)
|
||||
// _Grit 颗粒与扫描线
|
||||
// _SurgeRate 情绪节奏
|
||||
//
|
||||
// 结构要点:
|
||||
// 1. 一切先汇成一条「信号」标量:亮度 + 副载波 + 逐行相位误差 + 颗粒 + 块损坏。
|
||||
// 颜色由色散调色板对信号取值(连续渐变,不是纯 RGB 色块=不卡通)。
|
||||
// 屏幕贴图那块均匀的绿只位移是看不见的,所以它由 _Desaturate 随强度退场。
|
||||
// 2. 密度按屏幕像素计(fwidth 换算),乘 _Scale;屏幕物件很小时把 _Scale 调大即可,
|
||||
// 不会因为 UV 空间频率被挤成一团细线。
|
||||
// 3. 错误感来自「块级损坏」:随机时隙里整块整块地错位、换色、通道旋转,
|
||||
// 而不是均匀连续地抖——均匀 = 规律 = 假。
|
||||
#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;
|
||||
float _GlitchIntensity;
|
||||
float _Speed;
|
||||
float _Scale;
|
||||
float _ColorAmount;
|
||||
float _Warp;
|
||||
float _Chaos;
|
||||
float _Grit;
|
||||
float _SurgeRate;
|
||||
float _Dispersion;
|
||||
float _PalPhase;
|
||||
float _Desaturate;
|
||||
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);
|
||||
}
|
||||
|
||||
// 色散调色板:三通道相位错开地对同一条信号取值。
|
||||
// 相位差小 = 轻微色偏(很老的机器),接近 1/3 = 满彩虹。输出连续渐变,不会出现卡通纯色块。
|
||||
half3 AnalogPalette(float sig)
|
||||
{
|
||||
float3 ph = 6.2831853 * (sig * 1.15 + float3(0.0, 1.0, 2.0) * _Dispersion + _PalPhase);
|
||||
return 0.5 + 0.5 * cos(ph);
|
||||
}
|
||||
|
||||
// 横向拖抹 + 通道分离:亮度信号被硬拽出拖尾。
|
||||
half3 SampleSmeared(float2 uv, float smear, float dir, float chroma)
|
||||
{
|
||||
half3 acc = 0;
|
||||
float wsum = 0;
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (intensity <= 0.0005)
|
||||
{
|
||||
half4 clean = baseSample;
|
||||
clean.rgb *= input.color.rgb;
|
||||
clean.a = spriteAlpha;
|
||||
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;
|
||||
|
||||
// ── 情绪泵:越用力越急促。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);
|
||||
|
||||
// ── 卡喉:随机某几拍把画面"憋住"——时间种子冻结、力度更大。
|
||||
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);
|
||||
|
||||
// 持续档位也一直在呼吸:不给 0,保证 hold 住时屏幕仍在小幅挣扎。
|
||||
float drive = intensity * lerp(0.42, 1.0, surge) * lerp(1.0, 1.3, choke * build);
|
||||
|
||||
// ── 自下而上的行波:一口气往上顶。结构性的,跟着屏幕比例走。
|
||||
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 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);
|
||||
|
||||
// 大块:宽而扁,像整段数据行被搬走。格子原点和大小每个时隙重抽——
|
||||
// 固定网格上的块一眼就看出是"贴上去的"。
|
||||
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));
|
||||
|
||||
// 小块:碎片,负责细节上的"啃咬"
|
||||
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 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);
|
||||
|
||||
// 撕裂带:带高每个时隙重抽,粗细两层叠加——固定 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);
|
||||
|
||||
// 细撕裂:几像素高的毛刺行,让撕裂边缘"起毛"而不是整齐的粗条。
|
||||
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;
|
||||
|
||||
float shove = (hash11(beat + 0.5) - 0.5) * 2.0 * tearAmt * 0.3 * surge * intensity;
|
||||
|
||||
// 隔行抖动:奇偶行反向的细小错位,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 rollGate = step(0.7, hash11(beat * 3.77 + 1.3)) * intensity;
|
||||
float roll = rollGate * smoothstep(0.78, 1.0, cyc) * 0.055 * _Warp;
|
||||
|
||||
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;
|
||||
|
||||
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 像素分辨不出来只会闪,自动淡出。
|
||||
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));
|
||||
|
||||
// ── 暗角:照片感。
|
||||
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;
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif // HUOSHAN_SCREEN_GLITCH_INCLUDED
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1846a133ff0bc9a48981189d982a490f
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
Shader "AibisDream/HuoshanScreenGlitch"
|
||||
{
|
||||
// 只暴露 6 个宏观旋钮 + 4 个基调,内部十几个系数全部从它们推导。
|
||||
// 想加新参数前先想清楚:上一版 40 个属性根本没法调。
|
||||
Properties
|
||||
{
|
||||
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
|
||||
_Color("Tint", Color) = (1,1,1,1)
|
||||
|
||||
[Header(Drive)]
|
||||
_GlitchIntensity("Glitch Intensity (driven by Yarn)", Range(0, 1)) = 0
|
||||
_Speed("Animation Speed", Float) = 10
|
||||
|
||||
[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
|
||||
_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
|
||||
_Vignette("Vignette", Range(0, 1)) = 0.28
|
||||
}
|
||||
|
||||
SubShader
|
||||
{
|
||||
Tags
|
||||
{
|
||||
"Queue" = "Transparent"
|
||||
"RenderType" = "Transparent"
|
||||
"RenderPipeline" = "UniversalPipeline"
|
||||
"IgnoreProjector" = "True"
|
||||
"CanUseSpriteAtlas" = "True"
|
||||
}
|
||||
|
||||
Cull Off
|
||||
ZWrite Off
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
|
||||
Pass
|
||||
{
|
||||
Name "HuoshanScreenGlitch"
|
||||
Tags { "LightMode" = "Universal2D" }
|
||||
|
||||
HLSLPROGRAM
|
||||
#pragma vertex HuoshanScreenGlitchVert
|
||||
#pragma fragment HuoshanScreenGlitchFrag
|
||||
#pragma target 3.0
|
||||
#include "HuoshanScreenGlitch.hlsl"
|
||||
ENDHLSL
|
||||
}
|
||||
|
||||
Pass
|
||||
{
|
||||
Name "HuoshanScreenGlitchUnlit"
|
||||
Tags { "LightMode" = "SRPDefaultUnlit" }
|
||||
|
||||
HLSLPROGRAM
|
||||
#pragma vertex HuoshanScreenGlitchVert
|
||||
#pragma fragment HuoshanScreenGlitchFrag
|
||||
#pragma target 3.0
|
||||
#include "HuoshanScreenGlitch.hlsl"
|
||||
ENDHLSL
|
||||
}
|
||||
}
|
||||
FallBack "Sprites/Default"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34b7b1bac3f94aae80724d2934758989
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user