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; /// /// 仅对齐位置/尺寸与 sortingOrder,不改 Layer。 /// glitch_transition 的 noise 必须留在 Default;若改成 HuoshanScreen 会进 VolFx 叠在粒子上。 /// 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(); _mpb = new MaterialPropertyBlock(); } private void OnEnable() { ApplyIntensity(); ApplyCompositeMode(); ApplyCalmField(); ApplyAmbientNoise(); } /// /// 小游戏阶段使用不透明黑底;记忆阶段使用透明叠加。透明模式下强度为 0 时 /// Shader 不写入任何可见像素,避免黑底遮挡记忆图。 /// public void SetCompositeMode(CompositeMode mode) { _compositeMode = mode; ApplyCompositeMode(); } /// /// 设置常驻细密噪点(与 glitch 强度无关,一直显示)。density 为 0 时关闭。 /// 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); } /// /// 从当前强度平滑过渡到目标值(类似 DOTween),不瞬移。 /// /// 目标强度 [0,1] /// 过渡时长(秒) 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); } } /// /// 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. /// 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(); } /// /// Removes the focused-text calm field. Use immediate when closing or rebuilding the view. /// public void ClearCalmField(bool immediate = false) { if (!_calmFieldRequested && _calmFieldTargetStrength <= 0f && (!immediate || _calmFieldStrength <= 0f)) { return; } _calmFieldRequested = false; _calmFieldTargetStrength = 0f; if (immediate) { _calmFieldStrength = 0f; } ApplyCalmField(); } /// /// 从当前强度平滑过渡到目标值,等待完成后返回。供 Yarn 等需要阻塞的调用使用。 /// 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; } /// /// 将 [0,1] 强度映射到对应阶段(用于音效匹配) /// 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(); if (_mpb == null) _mpb = new MaterialPropertyBlock(); } private void ApplyAudio(GlitchStage stage) { if (_stage1Clip == null && _stage2Clip == null && _stage3Clip == null) return; if (_audioSource == null) { _audioSource = GetComponent(); if (_audioSource == null) _audioSource = gameObject.AddComponent(); } _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 }