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)