Files
aibis-dream/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs
T

443 lines
15 KiB
C#

using UnityEngine;
using System.Collections;
[RequireComponent(typeof(SpriteRenderer))]
public class SpriteNoiseGlitchController : MonoBehaviour
{
public enum GlitchStage { None, Light, Medium, Heavy }
public enum CompositeMode { OpaqueBackground, TransparentOverlay }
[Header("Runtime")]
[SerializeField] private GlitchStage _currentStage = GlitchStage.None;
[SerializeField, Range(0f, 1f)] private float _intensity;
[SerializeField] private float _transitionDuration = 0.5f;
[SerializeField] private CompositeMode _compositeMode = CompositeMode.OpaqueBackground;
[Header("Audio (Optional)")]
[Tooltip("由 Tools > Generate Glitch Noise Audio 生成")]
[SerializeField] private AudioClip _stage1Clip;
[SerializeField] private AudioClip _stage2Clip;
[SerializeField] private AudioClip _stage3Clip;
[SerializeField, Range(0f, 1f)] private float _audioVolume = 0.5f;
[Header("Ambient Fine Noise(常驻细密噪点,密度 0 = 关闭)")]
[SerializeField, Range(0f, 0.3f)] private float _ambientNoiseDensity = 0f;
[SerializeField, Range(80f, 800f)] private float _ambientNoiseScale = 320f;
[SerializeField, Range(0f, 1f)] private float _ambientNoiseBrightness = 0.55f;
[Header("Text Calm Field")]
[SerializeField] private Vector2 _calmFieldPadding = new Vector2(0.55f, 0.4f);
[SerializeField, Range(0.05f, 1f)] private float _calmFieldFeather = 0.85f;
[SerializeField, Range(0f, 0.2f)] private float _calmFieldResidualIntensity = 0.08f;
[SerializeField, Range(0f, 0.1f)] private float _calmFieldBoundaryWarp = 0.05f;
[SerializeField, Min(0.01f)] private float _calmFieldTransitionDuration = 0.45f;
private SpriteRenderer _renderer;
private AudioSource _audioSource;
private MaterialPropertyBlock _mpb;
private Coroutine _transitionCoroutine;
private Vector4 _calmField = new Vector4(0.5f, 0.5f, 0f, 0.1f);
private float _calmFieldAspect = 1f;
private float _calmFieldStrength;
private float _calmFieldTargetStrength;
private bool _calmFieldRequested;
private static readonly int PropGlitchIntensity = Shader.PropertyToID("_GlitchIntensity");
private static readonly int PropCompositeMode = Shader.PropertyToID("_CompositeMode");
private static readonly int PropBaseNoiseDensity = Shader.PropertyToID("_BaseNoiseDensity");
private static readonly int PropBaseNoiseScale = Shader.PropertyToID("_BaseNoiseScale");
private static readonly int PropBaseNoiseBrightness = Shader.PropertyToID("_BaseNoiseBrightness");
private static readonly int PropCalmField = Shader.PropertyToID("_CalmField");
private static readonly int PropCalmFieldAspect = Shader.PropertyToID("_CalmFieldAspect");
private static readonly int PropCalmFieldStrength = Shader.PropertyToID("_CalmFieldStrength");
private static readonly int PropCalmFieldFeather = Shader.PropertyToID("_CalmFieldFeather");
private static readonly int PropCalmFieldResidual = Shader.PropertyToID("_CalmFieldResidual");
private static readonly int PropCalmFieldWarp = Shader.PropertyToID("_CalmFieldWarp");
private static readonly float[] StageValues = { 0f, 0.25f, 0.55f, 0.95f };
public float Intensity
{
get => _intensity;
set
{
_intensity = Mathf.Clamp01(value);
ApplyIntensity();
}
}
public GlitchStage CurrentStage
{
get => _currentStage;
set => SetStage(value);
}
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>();
_mpb = new MaterialPropertyBlock();
}
private void OnEnable()
{
ApplyIntensity();
ApplyCompositeMode();
ApplyCalmField();
ApplyAmbientNoise();
}
/// <summary>
/// 小游戏阶段使用不透明黑底;记忆阶段使用透明叠加。透明模式下强度为 0 时
/// Shader 不写入任何可见像素,避免黑底遮挡记忆图。
/// </summary>
public void SetCompositeMode(CompositeMode mode)
{
_compositeMode = mode;
ApplyCompositeMode();
}
/// <summary>
/// 设置常驻细密噪点(与 glitch 强度无关,一直显示)。density 为 0 时关闭。
/// </summary>
public void SetAmbientNoise(float density, float scale, float brightness)
{
_ambientNoiseDensity = Mathf.Clamp(density, 0f, 0.3f);
_ambientNoiseScale = Mathf.Max(1f, scale);
_ambientNoiseBrightness = Mathf.Clamp01(brightness);
ApplyAmbientNoise();
}
private void ApplyAmbientNoise()
{
EnsureRuntimeObjects();
if (_renderer == null) return;
_renderer.GetPropertyBlock(_mpb);
_mpb.SetFloat(PropBaseNoiseDensity, _ambientNoiseDensity);
_mpb.SetFloat(PropBaseNoiseScale, _ambientNoiseScale);
_mpb.SetFloat(PropBaseNoiseBrightness, _ambientNoiseBrightness);
_renderer.SetPropertyBlock(_mpb);
}
private void OnDisable()
{
ClearCalmField(true);
}
private void Update()
{
float duration = Mathf.Max(0.01f, _calmFieldTransitionDuration);
float nextStrength = Mathf.MoveTowards(
_calmFieldStrength,
_calmFieldTargetStrength,
Time.deltaTime / duration);
if (!Mathf.Approximately(nextStrength, _calmFieldStrength))
{
_calmFieldStrength = nextStrength;
ApplyCalmField();
}
}
public void SetStage(GlitchStage stage, bool instant = false)
{
_currentStage = stage;
float target = StageValues[(int)stage];
ApplyAudio(stage);
if (instant || !gameObject.activeInHierarchy)
{
Intensity = target;
return;
}
if (_transitionCoroutine != null)
StopCoroutine(_transitionCoroutine);
_transitionCoroutine = StartCoroutine(TransitionTo(target));
}
public void SetStageByIndex(int index)
{
index = Mathf.Clamp(index, 0, 3);
SetStage((GlitchStage)index);
}
/// <summary>
/// 从当前强度平滑过渡到目标值(类似 DOTween),不瞬移。
/// </summary>
/// <param name="target">目标强度 [0,1]</param>
/// <param name="durationSeconds">过渡时长(秒)</param>
public void TransitionTo(float target, float durationSeconds)
{
if (durationSeconds > 0f && gameObject.activeInHierarchy)
StartCoroutine(TransitionToAndWait(target, durationSeconds));
else
{
target = Mathf.Clamp01(target);
Intensity = target;
_currentStage = ValueToStage(target);
ApplyAudio(_currentStage);
}
}
/// <summary>
/// Enables a soft horizontal capsule around the focused text. The supplied bounds
/// are converted into the glitch sprite's UV space, so the field follows text motion
/// without requiring a texture mask or material instance.
/// </summary>
public void SetCalmField(Bounds worldBounds)
{
EnsureRuntimeObjects();
if (_renderer == null)
return;
Bounds overlayBounds = _renderer.bounds;
if (overlayBounds.size.x <= Mathf.Epsilon || overlayBounds.size.y <= Mathf.Epsilon)
return;
float invOverlayHeight = 1f / overlayBounds.size.y;
float halfWidth = Mathf.Max(0f, worldBounds.extents.x + _calmFieldPadding.x);
float radius = Mathf.Max(0.001f, worldBounds.extents.y + _calmFieldPadding.y);
float halfLine = Mathf.Max(0f, halfWidth - radius);
float centerX = Mathf.InverseLerp(overlayBounds.min.x, overlayBounds.max.x, worldBounds.center.x);
float centerY = Mathf.InverseLerp(overlayBounds.min.y, overlayBounds.max.y, worldBounds.center.y);
_calmField = new Vector4(
centerX,
centerY,
halfLine * invOverlayHeight,
radius * invOverlayHeight);
_calmFieldAspect = overlayBounds.size.x * invOverlayHeight;
_calmFieldRequested = true;
_calmFieldTargetStrength = 1f;
ApplyCalmField();
}
/// <summary>
/// Removes the focused-text calm field. Use immediate when closing or rebuilding the view.
/// </summary>
public void ClearCalmField(bool immediate = false)
{
if (!_calmFieldRequested && _calmFieldTargetStrength <= 0f &&
(!immediate || _calmFieldStrength <= 0f))
{
return;
}
_calmFieldRequested = false;
_calmFieldTargetStrength = 0f;
if (immediate)
{
_calmFieldStrength = 0f;
}
ApplyCalmField();
}
/// <summary>
/// 从当前强度平滑过渡到目标值,等待完成后返回。供 Yarn 等需要阻塞的调用使用。
/// </summary>
public IEnumerator TransitionToAndWait(float target, float durationSeconds)
{
target = Mathf.Clamp01(target);
float startValue = _intensity;
if (_transitionCoroutine != null)
StopCoroutine(_transitionCoroutine);
if (durationSeconds <= 0f || !gameObject.activeInHierarchy)
{
Intensity = target;
_currentStage = ValueToStage(target);
ApplyAudio(_currentStage);
yield break;
}
_transitionCoroutine = StartCoroutine(TransitionToValue(startValue, target, durationSeconds));
yield return _transitionCoroutine;
_transitionCoroutine = null;
}
/// <summary>
/// 将 [0,1] 强度映射到对应阶段(用于音效匹配)
/// </summary>
private static GlitchStage ValueToStage(float value)
{
if (value < 0.125f) return GlitchStage.None;
if (value < 0.4f) return GlitchStage.Light;
if (value < 0.75f) return GlitchStage.Medium;
return GlitchStage.Heavy;
}
private IEnumerator TransitionTo(float target)
{
float start = _intensity;
float elapsed = 0f;
while (elapsed < _transitionDuration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / _transitionDuration);
Intensity = Mathf.Lerp(start, target, t);
yield return null;
}
Intensity = target;
_transitionCoroutine = null;
}
private IEnumerator TransitionToValue(float from, float to, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
float current = Mathf.Lerp(from, to, t);
Intensity = current;
// 过渡过程中,若跨越阶段边界则切换对应音效
var stage = ValueToStage(current);
if (stage != _currentStage)
{
_currentStage = stage;
ApplyAudio(stage);
}
yield return null;
}
Intensity = to;
_currentStage = ValueToStage(to);
ApplyAudio(_currentStage);
_transitionCoroutine = null;
}
private void ApplyIntensity()
{
EnsureRuntimeObjects();
if (_renderer == null) return;
_renderer.GetPropertyBlock(_mpb);
_mpb.SetFloat(PropGlitchIntensity, _intensity);
_renderer.SetPropertyBlock(_mpb);
}
private void ApplyCompositeMode()
{
EnsureRuntimeObjects();
if (_renderer == null) return;
_renderer.GetPropertyBlock(_mpb);
_mpb.SetFloat(PropCompositeMode, (float)_compositeMode);
_renderer.SetPropertyBlock(_mpb);
}
private void ApplyCalmField()
{
EnsureRuntimeObjects();
if (_renderer == null) return;
_renderer.GetPropertyBlock(_mpb);
_mpb.SetVector(PropCalmField, _calmField);
_mpb.SetFloat(PropCalmFieldAspect, _calmFieldAspect);
_mpb.SetFloat(PropCalmFieldStrength, _calmFieldStrength);
_mpb.SetFloat(PropCalmFieldFeather, _calmFieldFeather);
_mpb.SetFloat(PropCalmFieldResidual, _calmFieldResidualIntensity);
_mpb.SetFloat(PropCalmFieldWarp, _calmFieldBoundaryWarp);
_renderer.SetPropertyBlock(_mpb);
}
private void EnsureRuntimeObjects()
{
if (_renderer == null)
_renderer = GetComponent<SpriteRenderer>();
if (_mpb == null)
_mpb = new MaterialPropertyBlock();
}
private void ApplyAudio(GlitchStage stage)
{
if (_stage1Clip == null && _stage2Clip == null && _stage3Clip == null) return;
if (_audioSource == null)
{
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null)
_audioSource = gameObject.AddComponent<AudioSource>();
}
_audioSource.Stop();
AudioClip clip = stage switch
{
GlitchStage.Light => _stage1Clip,
GlitchStage.Medium => _stage2Clip,
GlitchStage.Heavy => _stage3Clip,
_ => null
};
if (clip != null)
{
_audioSource.clip = clip;
_audioSource.volume = _audioVolume;
_audioSource.loop = true;
_audioSource.Play();
}
}
#if UNITY_EDITOR
private void OnValidate()
{
_calmFieldPadding.x = Mathf.Max(0f, _calmFieldPadding.x);
_calmFieldPadding.y = Mathf.Max(0f, _calmFieldPadding.y);
_calmFieldTransitionDuration = Mathf.Max(0.01f, _calmFieldTransitionDuration);
EnsureRuntimeObjects();
ApplyIntensity();
ApplyCompositeMode();
ApplyCalmField();
ApplyAmbientNoise();
}
#endif
}