diff --git a/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader b/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader index 29143bdd9..a7155efb8 100644 --- a/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader +++ b/Assets/RawResources/Art/诊所外/SpriteNoiseGlitch.shader @@ -25,6 +25,14 @@ Shader "AibisDream/SpriteNoiseGlitch" _TearStrength("Tear Strength", Float) = 0.3 _FlickerStrength("Flicker Strength", Range(0, 1)) = 0.4 _DropoutSize("Dropout Block Size", Range(0, 1)) = 0.15 + + [Header(Text Calm Field)] + _CalmField("Calm Field (Center XY, Half Length, Radius)", Vector) = (0.5,0.5,0,0.1) + _CalmFieldAspect("Calm Field Aspect", Float) = 1.7778 + _CalmFieldStrength("Calm Field Strength", Range(0, 1)) = 0 + _CalmFieldFeather("Calm Field Feather", Range(0.05, 1)) = 0.45 + _CalmFieldResidual("Calm Field Residual", Range(0, 0.2)) = 0.04 + _CalmFieldWarp("Calm Field Boundary Warp", Range(0, 0.1)) = 0.025 } SubShader @@ -87,6 +95,12 @@ Shader "AibisDream/SpriteNoiseGlitch" float _TearStrength; float _FlickerStrength; float _DropoutSize; + float4 _CalmField; + float _CalmFieldAspect; + float _CalmFieldStrength; + float _CalmFieldFeather; + float _CalmFieldResidual; + float _CalmFieldWarp; CBUFFER_END float hash11(float p) @@ -116,6 +130,27 @@ Shader "AibisDream/SpriteNoiseGlitch" return saturate((v - lo) / (hi - lo)); } + float applyCalmField(float2 uv, float intensity) + { + float2 fieldPosition = float2( + (uv.x - _CalmField.x) * _CalmFieldAspect, + uv.y - _CalmField.y); + float halfLength = max(0.0, _CalmField.z); + fieldPosition.x -= clamp(fieldPosition.x, -halfLength, halfLength); + + float radius = max(0.0001, _CalmField.w); + float boundaryWarp = ( + sin(uv.x * 11.0 + uv.y * 7.0 + _Time.y * 0.35) + + sin(uv.x * -5.0 + uv.y * 13.0 - _Time.y * 0.23)) + * 0.5 * radius * _CalmFieldWarp; + float distanceToField = length(fieldPosition) - radius + boundaryWarp; + float feather = max(0.0001, radius * _CalmFieldFeather); + float calmMask = 1.0 - smoothstep(-feather, feather, distanceToField); + float calmBlend = saturate(calmMask * _CalmFieldStrength); + + return intensity * lerp(1.0, _CalmFieldResidual, calmBlend); + } + Varyings vert(Attributes input) { Varyings output; @@ -127,9 +162,9 @@ Shader "AibisDream/SpriteNoiseGlitch" half4 frag(Varyings input) : SV_Target { - float intensity = _GlitchIntensity; float t = _Time.y * _Speed; float2 uv = input.uv; + float intensity = applyCalmField(uv, _GlitchIntensity); half4 spriteColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv); float spriteAlpha = spriteColor.a * input.color.a; @@ -296,6 +331,12 @@ Shader "AibisDream/SpriteNoiseGlitch" float _TearStrength; float _FlickerStrength; float _DropoutSize; + float4 _CalmField; + float _CalmFieldAspect; + float _CalmFieldStrength; + float _CalmFieldFeather; + float _CalmFieldResidual; + float _CalmFieldWarp; CBUFFER_END float hash11(float p) @@ -325,6 +366,27 @@ Shader "AibisDream/SpriteNoiseGlitch" return saturate((v - lo) / (hi - lo)); } + float applyCalmField(float2 uv, float intensity) + { + float2 fieldPosition = float2( + (uv.x - _CalmField.x) * _CalmFieldAspect, + uv.y - _CalmField.y); + float halfLength = max(0.0, _CalmField.z); + fieldPosition.x -= clamp(fieldPosition.x, -halfLength, halfLength); + + float radius = max(0.0001, _CalmField.w); + float boundaryWarp = ( + sin(uv.x * 11.0 + uv.y * 7.0 + _Time.y * 0.35) + + sin(uv.x * -5.0 + uv.y * 13.0 - _Time.y * 0.23)) + * 0.5 * radius * _CalmFieldWarp; + float distanceToField = length(fieldPosition) - radius + boundaryWarp; + float feather = max(0.0001, radius * _CalmFieldFeather); + float calmMask = 1.0 - smoothstep(-feather, feather, distanceToField); + float calmBlend = saturate(calmMask * _CalmFieldStrength); + + return intensity * lerp(1.0, _CalmFieldResidual, calmBlend); + } + Varyings vert(Attributes input) { Varyings output; @@ -336,9 +398,9 @@ Shader "AibisDream/SpriteNoiseGlitch" half4 frag(Varyings input) : SV_Target { - float intensity = _GlitchIntensity; float t = _Time.y * _Speed; float2 uv = input.uv; + float intensity = applyCalmField(uv, _GlitchIntensity); half4 spriteColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv); float spriteAlpha = spriteColor.a * input.color.a; diff --git a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs index a0b9d3e1e..72908c474 100644 --- a/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs +++ b/Assets/Scripts/Effects/SpriteNoiseGlitchController.cs @@ -18,12 +18,30 @@ public class SpriteNoiseGlitchController : MonoBehaviour [SerializeField] private AudioClip _stage3Clip; [SerializeField, Range(0f, 1f)] private float _audioVolume = 0.5f; + [Header("Text Calm Field")] + [SerializeField] private Vector2 _calmFieldPadding = new Vector2(0.55f, 0.4f); + [SerializeField, Range(0.05f, 1f)] private float _calmFieldFeather = 0.45f; + [SerializeField, Range(0f, 0.2f)] private float _calmFieldResidualIntensity = 0.04f; + [SerializeField, Range(0f, 0.1f)] private float _calmFieldBoundaryWarp = 0.025f; + [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 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 }; @@ -52,6 +70,27 @@ public class SpriteNoiseGlitchController : MonoBehaviour private void OnEnable() { ApplyIntensity(); + ApplyCalmField(); + } + + 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) @@ -95,6 +134,62 @@ public class SpriteNoiseGlitchController : MonoBehaviour } } + /// + /// 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 等需要阻塞的调用使用。 /// @@ -177,6 +272,7 @@ public class SpriteNoiseGlitchController : MonoBehaviour private void ApplyIntensity() { + EnsureRuntimeObjects(); if (_renderer == null) return; _renderer.GetPropertyBlock(_mpb); @@ -184,6 +280,29 @@ public class SpriteNoiseGlitchController : MonoBehaviour _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; @@ -217,12 +336,13 @@ public class SpriteNoiseGlitchController : MonoBehaviour #if UNITY_EDITOR private void OnValidate() { - if (_renderer == null) - _renderer = GetComponent(); - if (_renderer != null && _mpb == null) - _mpb = new MaterialPropertyBlock(); + _calmFieldPadding.x = Mathf.Max(0f, _calmFieldPadding.x); + _calmFieldPadding.y = Mathf.Max(0f, _calmFieldPadding.y); + _calmFieldTransitionDuration = Mathf.Max(0.01f, _calmFieldTransitionDuration); + EnsureRuntimeObjects(); ApplyIntensity(); + ApplyCalmField(); } #endif } diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs index ee27d806d..d5eb8b230 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ExpressionManager.cs @@ -60,12 +60,29 @@ namespace AibisDream } } + private void LateUpdate() + { + if (particleManager == null || glitchController == null) + return; + + if (particleManager.TryGetFocusedTextBounds(out Bounds textBounds)) + { + glitchController.SetCalmField(textBounds); + } + else + { + glitchController.ClearCalmField(); + } + } + /// /// 打开表达视图(只显示美术背景,不启动粒子系统) /// 先设置所有初始状态,再打开视图 /// public void OpenView() { + glitchController?.ClearCalmField(true); + // 先设置所有初始状态再打开 if (particleManager != null) { @@ -87,6 +104,8 @@ namespace AibisDream /// 非目标候选粒子总数;<0 时使用 Inspector 默认候选池大小 public void StartSystem(List anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1) { + glitchController?.ClearCalmField(true); + if (particleManager != null) { // 使用传入的配置,如果没有则使用默认配置 @@ -122,6 +141,8 @@ namespace AibisDream /// public IEnumerator FadeOutParticlesBeforeNewRound(float duration = 0f) { + glitchController?.ClearCalmField(true); + if (particleManager == null) yield break; yield return StartCoroutine(particleManager.FadeOutBeforeNewExpressionRound(duration)); } @@ -161,6 +182,8 @@ namespace AibisDream /// public void CloseView() { + glitchController?.ClearCalmField(true); + if (expressionView != null) { expressionView.SetActive(false); @@ -177,6 +200,8 @@ namespace AibisDream /// public void ResetSystem(bool playEntranceEffect = true) { + glitchController?.ClearCalmField(true); + if (particleManager != null) { particleManager.ResetGame(playEntranceEffect); @@ -236,4 +261,4 @@ namespace AibisDream } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs index 2821a6b6a..a6b9642c3 100644 --- a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs @@ -1480,6 +1480,51 @@ namespace AibisDream.MiniGame.Language } } + /// + /// Returns the combined world-space bounds of the target sentence while it is + /// in the focused completion presentation. Other UI and dialogue text are excluded. + /// + public bool TryGetFocusedTextBounds(out Bounds textBounds) + { + textBounds = default; + if (completionPhase != CompletionPhase.Focusing && + completionPhase != CompletionPhase.FocusHolding && + completionPhase != CompletionPhase.Finished) + { + return false; + } + + bool hasBounds = false; + foreach (var particle in orderedRedParticles) + { + if (particle == null || particle.textMesh == null || + !particle.textMesh.gameObject.activeInHierarchy) + { + continue; + } + + Renderer textRenderer = particle.textMesh.GetComponent(); + if (textRenderer == null || !textRenderer.enabled) + continue; + + Bounds particleBounds = textRenderer.bounds; + if (particleBounds.size.sqrMagnitude <= Mathf.Epsilon) + continue; + + if (!hasBounds) + { + textBounds = particleBounds; + hasBounds = true; + } + else + { + textBounds.Encapsulate(particleBounds); + } + } + + return hasBounds; + } + private void DestroyFocusVisualDecorationsAndClear() { foreach (var visual in focusVisuals.Values)