feat(huoshan): 为聚焦文本添加 Glitch 平静场

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 14:23:41 +08:00
co-authored by Cursor
parent 6689effb87
commit 4b3260dfcd
4 changed files with 259 additions and 7 deletions
@@ -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;
@@ -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
}
}
/// <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>
@@ -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<SpriteRenderer>();
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<SpriteRenderer>();
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
}
@@ -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();
}
}
/// <summary>
/// 打开表达视图(只显示美术背景,不启动粒子系统)
/// 先设置所有初始状态,再打开视图
/// </summary>
public void OpenView()
{
glitchController?.ClearCalmField(true);
// 先设置所有初始状态再打开
if (particleManager != null)
{
@@ -87,6 +104,8 @@ namespace AibisDream
/// <param name="nonTargetParticleTotal">非目标候选粒子总数;&lt;0 时使用 Inspector 默认候选池大小</param>
public void StartSystem(List<string> anxietyPhrases, string targetSentence, string completionNodeName = null, int nonTargetParticleTotal = -1)
{
glitchController?.ClearCalmField(true);
if (particleManager != null)
{
// 使用传入的配置,如果没有则使用默认配置
@@ -122,6 +141,8 @@ namespace AibisDream
/// </summary>
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
/// </summary>
public void CloseView()
{
glitchController?.ClearCalmField(true);
if (expressionView != null)
{
expressionView.SetActive(false);
@@ -177,6 +200,8 @@ namespace AibisDream
/// </summary>
public void ResetSystem(bool playEntranceEffect = true)
{
glitchController?.ClearCalmField(true);
if (particleManager != null)
{
particleManager.ResetGame(playEntranceEffect);
@@ -236,4 +261,4 @@ namespace AibisDream
}
}
}
}
@@ -1480,6 +1480,51 @@ namespace AibisDream.MiniGame.Language
}
}
/// <summary>
/// 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.
/// </summary>
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<Renderer>();
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)