feat(huoshan): 为聚焦文本添加 Glitch 平静场
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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">非目标候选粒子总数;<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)
|
||||
|
||||
Reference in New Issue
Block a user