Merge branch '7.1春细节优化' into 'develop'

fix(eye): 修复视口遮罩不可见并避免遮挡系统 UI

See merge request aibis-dream/aibis-dream!720
This commit is contained in:
2026-07-01 14:04:29 +00:00
18 changed files with 1778 additions and 1081 deletions
@@ -214,7 +214,67 @@ ScriptedImporter:
characterGroups: []
pivot: {x: 0, y: 0}
boneReadOnly: 0
platformSettings: []
platformSettings:
- name: DefaultTexturePlatform
overridden: 0
ignorePlatformSupport: 0
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
forceMaximumCompressionQuality_BC6H_BC7: 0
crunchedCompression: 0
allowsAlphaSplitting: 0
androidETC2FallbackOverride: 0
- name: Standalone
overridden: 0
ignorePlatformSupport: 0
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
forceMaximumCompressionQuality_BC6H_BC7: 0
crunchedCompression: 0
allowsAlphaSplitting: 0
androidETC2FallbackOverride: 0
- name: Server
overridden: 0
ignorePlatformSupport: 0
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
forceMaximumCompressionQuality_BC6H_BC7: 0
crunchedCompression: 0
allowsAlphaSplitting: 0
androidETC2FallbackOverride: 0
- name: Android
overridden: 0
ignorePlatformSupport: 0
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
forceMaximumCompressionQuality_BC6H_BC7: 0
crunchedCompression: 0
allowsAlphaSplitting: 0
androidETC2FallbackOverride: 0
- name: WebGL
overridden: 0
ignorePlatformSupport: 0
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
forceMaximumCompressionQuality_BC6H_BC7: 0
crunchedCompression: 0
allowsAlphaSplitting: 0
androidETC2FallbackOverride: 0
mosaicLayers: 1
characterMode: 1
documentPivot: {x: 0, y: 0}
@@ -233,146 +293,6 @@ ScriptedImporter:
resliceFromLayer: 0
mosaicPSDLayers: []
rigPSDLayers:
- name: "UF\u89C6\u89D2\u4E0B\u751F\u6210\u7684\u56FE\u50CF\u5143\u7D20"
spriteName:
isGroup: 1
parentIndex: -1
spriteID: e941588a141b56f479db3454d668ebef
layerID: 637
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u56FE\u5C42 5"
spriteName:
isGroup: 0
parentIndex: 0
spriteID: d895094d526880e4684305bc55024019
layerID: 666
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 1
- name: "\u6D77\u4E2D\u5927\u8C61"
spriteName:
isGroup: 0
parentIndex: 0
spriteID: 8c2a75dae64ffd749bd065803bcdc2dc
layerID: 646
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 1
- name: "\u690D\u7269\u53D8\u6210\u7684\u753B\u7B14\u7B14\u7B52"
spriteName:
isGroup: 0
parentIndex: 0
spriteID: 5f1bf878f8b636d4aa7198c430cd8ddb
layerID: 651
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u5915\u9633\u5C71\u4E18"
spriteName:
isGroup: 1
parentIndex: 0
spriteID: 40d012f90d12d054b8b4b095c5d17097
layerID: 659
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u56FE\u5C42 1 \u62F7\u8D1D"
spriteName:
isGroup: 0
parentIndex: 4
spriteID: 6e6a26c6b33e3f046a0fb07c7a0be1d2
layerID: 655
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u8272\u76F8/\u9971\u548C\u5EA6 1"
spriteName:
isGroup: 0
parentIndex: 4
spriteID: e00cb4f5c86d7af42810b4b13c2b9311
layerID: 665
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 1
- name: "\u8272\u9636 1"
spriteName:
isGroup: 0
parentIndex: 4
spriteID: d4750bcb749c6be489a6433b8733ffb3
layerID: 662
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 1
- name: "\u56FE\u5C42 1 \u62F7\u8D1D 2"
spriteName:
isGroup: 0
parentIndex: 4
spriteID: dd619b01f886515448f78aeb6d501809
layerID: 656
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 1
- name: "\u5915\u9633\u5C71\u4E18"
spriteName:
isGroup: 1
parentIndex: 0
spriteID: 9713048476e1591449391ce052b71542
layerID: 657
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u56FE\u5C42 4"
spriteName:
isGroup: 0
parentIndex: 9
spriteID: 22f2788988d685542a68985f6c194113
layerID: 664
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u56FE\u5C42 3"
spriteName:
isGroup: 0
parentIndex: 9
spriteID: db1382cf3f3423d42b9ac9c9b9ecc7fc
layerID: 661
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u56FE\u5C42 2"
spriteName:
isGroup: 0
parentIndex: 9
spriteID: 8a4910856da3a3e459c28726ad02e802
layerID: 654
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u56FE\u5C42 1"
spriteName:
isGroup: 0
parentIndex: 9
spriteID: 4f403b854fa76234fabfccf65b54804a
layerID: 653
mosaicPosition: {x: 0, y: 0}
flatten: 0
isImported: 0
isVisible: 0
- name: "\u7A97\u5B50"
spriteName: "\u7A97\u5B50"
isGroup: 0
Binary file not shown.
@@ -48,7 +48,7 @@ TextureImporter:
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spritePixelsToUnits: 50
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
Binary file not shown.
@@ -48,7 +48,7 @@ TextureImporter:
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spritePixelsToUnits: 50
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
+41 -14
View File
@@ -133,6 +133,29 @@ Shader "AIBIS/EyeUnifiedSprite"
return saturate(mul(mat, color));
}
fixed4 sampleMainTex(float2 uv)
{
return tex2D(_MainTex, uv);
}
fixed4 gaussianBlurSample(float2 uv, float stepSize)
{
float angle = 1.937315; // ~111 deg, matches MemoryTuningSprite
float2 stepDir = float2(sin(angle), cos(angle));
float2 stepUv = stepDir * stepSize;
fixed4 col = sampleMainTex(uv) * 0.18;
col += sampleMainTex(uv + stepUv * 1.0) * 0.15;
col += sampleMainTex(uv - stepUv * 1.0) * 0.15;
col += sampleMainTex(uv + stepUv * 2.0) * 0.12;
col += sampleMainTex(uv - stepUv * 2.0) * 0.12;
col += sampleMainTex(uv + stepUv * 3.0) * 0.09;
col += sampleMainTex(uv - stepUv * 3.0) * 0.09;
col += sampleMainTex(uv + stepUv * 4.0) * 0.05;
col += sampleMainTex(uv - stepUv * 4.0) * 0.05;
return col;
}
fixed4 sampleSprite(float2 uv, float2 screenUv)
{
float2 distortedUv = uv;
@@ -141,23 +164,21 @@ Shader "AIBIS/EyeUnifiedSprite"
float ripple = sin((length(centered) * 42.0) - (_Time.y * 8.0));
distortedUv += centered * ripple * _RoundWaveStrength * 0.018;
float glitchRow = floor(screenUv.y * 240.0);
float tear = (hash21(float2(glitchRow, floor(_Time.y * 18.0))) - 0.5) * _GlitchAmount * 0.04;
float glitchRow = floor(screenUv.y * 360.0);
float tear = (hash21(float2(glitchRow, floor(_Time.y * 20.0))) - 0.5) * _GlitchAmount * 0.012;
distortedUv.x += tear;
float noise = hash21(floor(uv * 42.0) + floor(_Time.y * 16.0));
distortedUv += (noise - 0.5) * _DistortAmount;
float2 blur = _MainTex_TexelSize.xy + _BlurSize;
fixed4 col = tex2D(_MainTex, distortedUv);
fixed4 blurCol = col;
blurCol += tex2D(_MainTex, distortedUv + float2(blur.x, 0));
blurCol += tex2D(_MainTex, distortedUv - float2(blur.x, 0));
blurCol += tex2D(_MainTex, distortedUv + float2(0, blur.y));
blurCol += tex2D(_MainTex, distortedUv - float2(0, blur.y));
blurCol *= 0.2;
// Match legacy AllIn1 scale (~offset * fade * 0.005); avoid MemoryTuning's 0.014 base.
float2 texel = _MainTex_TexelSize.xy;
float stepSize = length(texel) + _BlurSize;
fixed4 col = sampleMainTex(distortedUv);
fixed4 blurCol = gaussianBlurSample(distortedUv, stepSize);
return lerp(col, blurCol, _BlurAmount);
float blurStrength = pow(saturate(_BlurAmount), 0.75);
return lerp(col, blurCol, blurStrength);
}
fixed4 frag(v2f i) : SV_Target
@@ -172,10 +193,16 @@ Shader "AIBIS/EyeUnifiedSprite"
float directional = dot(uv - 0.5, revealDir) + 0.5;
float dissolveNoise = (hash21(floor(uv * 96.0)) - 0.5) * _RevealNoise;
float revealMask = smoothstep(_ColorReveal - _RevealWidth, _ColorReveal + _RevealWidth, directional + dissolveNoise);
// Sweep only applies while the target is still desaturated; full color when _GrayAmount reaches 0.
float grayMix = saturate(max(_GrayAmount, (1.0 - revealMask) * _GrayAmount));
// Idle (_GrayAmount=1) or fully colored (_GrayAmount=0): uniform mix; sweep only mid-transition.
float inSweep = step(0.001, 1.0 - _GrayAmount) * step(0.001, _GrayAmount);
float sweepFactor = lerp(1.0, (1.0 - revealMask), inSweep);
float grayMix = saturate(_GrayAmount * sweepFactor);
col.rgb = lerp(col.rgb, gray.xxx, grayMix);
float sweepEdge = 1.0 - abs(directional + dissolveNoise - _ColorReveal);
float edgeGlow = smoothstep(_RevealWidth * 0.15, _RevealWidth * 0.55, sweepEdge) * inSweep * _GrayAmount * (1.0 - revealMask);
col.rgb += float3(1.0, 0.92, 0.78) * edgeGlow * 0.35;
col.rgb = lerp(col.rgb, _WrongColor.rgb * max(gray, 0.08), _WrongColorAmount * _WrongColor.a);
col.rgb = hueShift(col.rgb, _HsvShift);
@@ -197,7 +224,7 @@ Shader "AIBIS/EyeUnifiedSprite"
float ascii = asciiShape * asciiBrightness;
col.rgb = lerp(col.rgb, ascii.xxx * lerp(float3(1, 1, 1), col.rgb, 0.35), _AsciiAmount);
float chromaOffset = _GlitchAmount * 0.006;
float chromaOffset = _GlitchAmount * 0.004;
if (chromaOffset > 0.0001)
{
float red = tex2D(_MainTex, uv + float2(chromaOffset, 0)).r;
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
using AibisDream.Framework;
using DG.Tweening;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.Serialization;
@@ -83,11 +84,11 @@ namespace AibisDream
initialPosition = PanTransform.position;
}
public void MoveFocusTo(Transform target, float duration, Vector2 minBounds, Vector2 maxBounds, Vector2 viewportSize)
public Tween MoveFocusTo(Transform target, float duration, Vector2 minBounds, Vector2 maxBounds, Vector2 viewportSize)
{
if (target == null)
{
return;
return null;
}
Vector3 focusPosition = ClampFocusPosition(target.position, minBounds, maxBounds, viewportSize);
@@ -115,10 +116,26 @@ namespace AibisDream
if (duration <= 0f)
{
PanTransform.position = initialPosition + currentOffset;
return;
focusTween = null;
return null;
}
focusTween = PanTransform.DOMove(initialPosition + currentOffset, duration).SetEase(Ease.InOutSine);
return focusTween;
}
public IEnumerator WaitForFocusMove(float fallbackDuration = 0f)
{
if (focusTween != null && focusTween.IsActive())
{
yield return focusTween.WaitForCompletion();
yield break;
}
if (fallbackDuration > 0f)
{
yield return new WaitForSeconds(fallbackDuration);
}
}
private void Start()
@@ -265,10 +282,11 @@ namespace AibisDream
indicatorRectTransform.position = boxCenter;
indicatorRectTransform.sizeDelta = new Vector2(boxSize.x, boxSize.y);
Color indicatorColor = currentTarget.GetIndicatorColor();
targetNameText.text = currentTarget.name + " " +
"R" + currentTarget._color.r.ToString("F1") +
" G" + currentTarget._color.g.ToString("F1") +
" B" + currentTarget._color.b.ToString("F1");
"R" + indicatorColor.r.ToString("F1") +
" G" + indicatorColor.g.ToString("F1") +
" B" + indicatorColor.b.ToString("F1");
targetIndicator.SetActive(true);
}
@@ -18,7 +18,7 @@ namespace AibisDream.FixSystem
private const float DefaultFadeDuration = 1f;
private const float SearchStartDelay = 1f;
private const float ProjectorMoveDuration = 2f;
private const float MaxClarityDistortion = 0.58f;
private const float MaxClarityDistortion = 1f;
private const float MaxFrequencyBlur = 0.6f;
private const float MinPixelDensity = 18f;
private const float MaxPixelDensity = 96f;
+165 -24
View File
@@ -52,6 +52,119 @@ namespace AibisDream
public float GetEffectTransitionDuration() => visualSettings.GetDurationForState(_currentState);
private bool ShouldPreBlinkForFocusedEffect()
{
return _currentState is EyeColorState.CanImagineColor
or EyeColorState.CannotImagineColor
or EyeColorState.CanSeeColor;
}
private IEnumerator WaitForFocusMovement(float moveDuration)
{
if (!EnsureMouseFollowAndZoom())
{
yield break;
}
yield return mouseFollowAndZoom.WaitForFocusMove(moveDuration);
}
private IEnumerator RunFocusedEffectWithPreBlink(EyeTarget target)
{
if (EnsureMouseFollowAndZoom())
{
yield return mouseFollowAndZoom.WaitForFocusMove();
}
float duration = GetEffectTransitionDuration();
EyeViewportOverlay overlay = EnsureViewportOverlay();
if (overlay != null && Application.isPlaying && ShouldPreBlinkForFocusedEffect())
{
if (_currentState == EyeColorState.CanImagineColor)
{
yield return overlay.PlayPreEffectBlinkThen(
2,
visualSettings,
() => eyeStateEffect?.ApplyFocusedEffect(target, this),
visualSettings.imagineEffectAtBlinkOpen,
visualSettings.imagineBlinkGap);
}
else
{
yield return overlay.PlayPreEffectBlinkThen(
1,
visualSettings,
() => eyeStateEffect?.ApplyFocusedEffect(target, this),
visualSettings.focusEffectAtBlinkOpen);
}
}
else
{
eyeStateEffect?.ApplyFocusedEffect(target, this);
}
yield return new WaitForSeconds(duration);
}
private IEnumerator RunBlurOutWithPreBlink(EyeTarget target, float duration)
{
EyeViewportOverlay overlay = EnsureViewportOverlay();
if (overlay != null && Application.isPlaying)
{
yield return overlay.PlayPreEffectBlinkThen(
1,
visualSettings,
() => target.BlurOut(duration),
visualSettings.focusEffectAtBlinkOpen);
}
else
{
target.BlurOut(duration);
}
yield return new WaitForSeconds(duration);
}
private IEnumerator RunUnfocusedEffectWithPreBlink(EyeTarget target)
{
if (target == null || eyeStateEffect == null)
{
yield break;
}
float duration = GetEffectTransitionDuration();
EyeViewportOverlay overlay = EnsureViewportOverlay();
if (overlay != null && Application.isPlaying && ShouldPreBlinkForFocusedEffect())
{
if (_currentState == EyeColorState.CanImagineColor)
{
yield return overlay.PlayPreEffectBlinkThen(
2,
visualSettings,
() => eyeStateEffect.ApplyUnfocusedEffect(target, this),
visualSettings.imagineEffectAtBlinkOpen,
visualSettings.imagineBlinkGap);
}
else
{
yield return overlay.PlayPreEffectBlinkThen(
1,
visualSettings,
() => eyeStateEffect.ApplyUnfocusedEffect(target, this),
visualSettings.focusEffectAtBlinkOpen);
}
}
else
{
eyeStateEffect.ApplyUnfocusedEffect(target, this);
}
yield return new WaitForSeconds(duration);
}
private void OnValidate()
{
ApplyVisualSettingsNow(
@@ -183,6 +296,11 @@ namespace AibisDream
return;
}
if (duration > 0.01f)
{
AudioManager.Instance.PlaySfx("event:/ActionFB/visualturn");
}
mouseFollowAndZoom.MoveFocusTo(
focusTransform,
duration,
@@ -266,6 +384,14 @@ namespace AibisDream
private void Start()
{
ApplyVisualSettingsToTargets();
if (targets != null)
{
foreach (EyeTarget target in targets)
{
target?.Init();
}
}
SetEyeColorState(_currentState);
EnsureMouseFollowAndZoom();
}
@@ -289,7 +415,7 @@ namespace AibisDream
[YarnCommand("EyeBlink")]
public void EyeBlink(float duration = 0f)
{
EnsureViewportOverlay()?.PlayBlink(duration > 0f ? duration : null);
EnsureViewportOverlay()?.PlayBlink(duration > 0f ? duration : null, restartAutoBlinkAfter: true);
}
[YarnCommand("set_Eyetarget")]
@@ -309,30 +435,33 @@ namespace AibisDream
}
YarnVariableStorage.Instance.SetValue("$currentEyeTarget", target.name);
StartCoroutine(EyeEffect_FadeOut());
FocusOnTarget(target, 1f);
if (currentTarget != null)
{
currentTarget.BlurIn(visualSettings.targetSwitchDuration);
}
if (currentTarget == target)
{
DialogController.Instance.StartDialogNode("重复看同一个目标");
currentTarget = target;
mouseFollowAndZoom.SetIndicatorTarget(currentTarget);
currentTarget.BlurOut(visualSettings.targetSwitchDuration);
yield return new WaitForSeconds(visualSettings.targetSwitchDuration);
if (startDialogue)
{
DialogController.Instance.StartDialogNode("重复看同一个目标");
yield return RunBlurOutWithPreBlink(currentTarget, visualSettings.targetSwitchDuration);
}
yield break;
}
EyeTarget leavingTarget = currentTarget;
if (leavingTarget != null)
{
StartCoroutine(EyeEffect_FadeOut());
}
const float focusMoveDuration = 1f;
FocusOnTarget(target, focusMoveDuration);
yield return WaitForFocusMovement(focusMoveDuration);
currentTarget = target;
mouseFollowAndZoom.SetIndicatorTarget(currentTarget);
currentTarget.BlurOut(visualSettings.targetSwitchDuration);
yield return new WaitForSeconds(visualSettings.targetSwitchDuration);
yield return RunBlurOutWithPreBlink(currentTarget, visualSettings.targetSwitchDuration);
if (startDialogue)
{
@@ -354,25 +483,37 @@ namespace AibisDream
[YarnCommand("EyeEffect_FadeIn")]
public IEnumerator EyeEffect_FadeIn()
{
if (currentTarget != null)
if (currentTarget == null)
{
AudioManager.Instance.PlaySfx("event:/ActionFB/color_imagine");
eyeStateEffect?.ApplyFocusedEffect(currentTarget, this);
yield break;
}
yield return new WaitForSeconds(GetEffectTransitionDuration());
if (_currentState == EyeColorState.CanImagineColor
&& currentTarget.IsAtFocusedColorState(visualSettings))
{
yield break;
}
AudioManager.Instance.PlaySfx("event:/ActionFB/color_imagine");
yield return RunFocusedEffectWithPreBlink(currentTarget);
}
[YarnCommand("EyeEffect_FadeOut")]
public IEnumerator EyeEffect_FadeOut()
{
if (currentTarget != null)
if (currentTarget == null)
{
AudioManager.Instance.PlaySfx("event:/ActionFB/color_imagine");
eyeStateEffect?.ApplyUnfocusedEffect(currentTarget, this);
yield break;
}
yield return new WaitForSeconds(GetEffectTransitionDuration());
if (_currentState == EyeColorState.CanImagineColor
&& currentTarget.IsAtUnfocusedColorState(visualSettings))
{
yield break;
}
AudioManager.Instance.PlaySfx("event:/ActionFB/color_imagine");
yield return RunUnfocusedEffectWithPreBlink(currentTarget);
}
[YarnCommand("EyeEffect_Init")]
+36 -16
View File
@@ -20,8 +20,23 @@ public class EyeTarget : MonoBehaviour
[SerializeField] private Shader unifiedSpriteShader;
[SerializeField] private EyeTargetVisual unifiedVisual;
[Header("Indicator RGB (imagined color for debug overlay)")]
public Color _color;
public Color GetIndicatorColor()
{
return new Color(
RoundIndicatorChannel(_color.r),
RoundIndicatorChannel(_color.g),
RoundIndicatorChannel(_color.b),
_color.a);
}
private static float RoundIndicatorChannel(float value)
{
return Mathf.Round(value * 10f) / 10f;
}
private EyeVisualSettings visualSettings = new();
private Material wrongColorMaterial;
@@ -80,6 +95,7 @@ public class EyeTarget : MonoBehaviour
public void Init()
{
unifiedVisual?.ClearTransientEffects(visualSettings);
SetToMinColorReveal();
SetToMaxBlur();
SetToNoWrongColor();
@@ -111,6 +127,20 @@ public class EyeTarget : MonoBehaviour
unifiedVisual?.SetColorReveal(visualSettings.colorRevealMax);
}
public bool IsAtFocusedColorState(EyeVisualSettings settings = null)
{
EyeVisualSettings tuning = settings ?? visualSettings;
return unifiedVisual != null
&& unifiedVisual.IsAtColorReveal(tuning.colorRevealMax);
}
public bool IsAtUnfocusedColorState(EyeVisualSettings settings = null)
{
EyeVisualSettings tuning = settings ?? visualSettings;
return unifiedVisual != null
&& unifiedVisual.IsAtColorReveal(tuning.colorRevealMin);
}
public void SetToMaxBlur()
{
unifiedVisual?.SetBlur(visualSettings.idleBlurAmount);
@@ -135,16 +165,14 @@ public class EyeTarget : MonoBehaviour
public void FadeOut(float duration)
{
unifiedVisual?.TweenColorReveal(visualSettings.colorRevealMin, duration)?.SetEase(Ease.InSine);
unifiedVisual?.TweenBlur(visualSettings.idleBlurAmount, duration)?.SetEase(Ease.InSine);
unifiedVisual?.PulseDistortion(visualSettings.colorPulseDistortion, duration);
unifiedVisual?.KillActiveTweens();
unifiedVisual?.PlayColorSweepTransition(visualSettings, duration, colorIn: false);
}
public void ColorIn(float duration)
{
unifiedVisual?.TweenColorReveal(visualSettings.colorRevealMax, duration)?.SetEase(Ease.InOutSine);
unifiedVisual?.TweenBlur(visualSettings.focusedBlurAmount, duration)?.SetEase(Ease.InOutSine);
unifiedVisual?.PulseDistortion(visualSettings.colorPulseDistortion, duration);
unifiedVisual?.KillActiveTweens();
unifiedVisual?.PlayColorSweepTransition(visualSettings, duration, colorIn: true);
}
public void WrongColorIn(float duration)
@@ -161,16 +189,13 @@ public class EyeTarget : MonoBehaviour
public void BlurIn(float duration)
{
AudioManager.Instance.PlaySfx("event:/ActionFB/visualturn");
var tween = unifiedVisual?.TweenBlur(visualSettings.idleBlurAmount, duration);
tween?.SetEase(Ease.InOutSine).OnComplete(StopVisualTurnSfx);
tween?.SetEase(Ease.InOutSine);
}
public void BlurOut(float duration)
{
AudioManager.Instance.PlaySfx("event:/ActionFB/visualturn");
var tween = unifiedVisual?.TweenBlur(visualSettings.focusedBlurAmount, duration);
tween?.SetEase(Ease.InOutSine).OnComplete(StopVisualTurnSfx);
unifiedVisual?.TweenBlurFocusSettle(visualSettings, duration);
}
public void ColorBack()
@@ -421,9 +446,4 @@ public class EyeTarget : MonoBehaviour
renderer.enabled = false;
}
}
private static void StopVisualTurnSfx()
{
AudioManager.Instance.StopSfx("event:/ActionFB/visualturn");
}
}
@@ -115,6 +115,22 @@ namespace AibisDream
{
imageSpriteRenderer.DOKill();
}
ClearTransientEffects(null);
}
public void ClearTransientEffects(EyeVisualSettings settings)
{
if (!EnsureMaterial())
{
return;
}
SetFloat(GlitchAmountId, 0f);
SetFloat(DistortAmountId, 0f);
SetFloat(ScanlineAmountId, 0f);
SetFloatRaw(HsvShiftId, 0f);
SetFloatRaw(RevealNoiseId, settings != null ? settings.revealNoise : 0f);
}
public void ResetVisual(float reveal)
@@ -151,6 +167,21 @@ namespace AibisDream
SetFloat(GrayAmountId, 1f - value);
}
public float GetColorReveal()
{
if (!EnsureMaterial() || !material.HasProperty(ColorRevealId))
{
return 0f;
}
return material.GetFloat(ColorRevealId);
}
public bool IsAtColorReveal(float target, float epsilon = 0.02f)
{
return Mathf.Abs(GetColorReveal() - target) <= epsilon;
}
public Tween TweenColorReveal(float value, float duration)
{
if (!EnsureMaterial())
@@ -176,7 +207,69 @@ namespace AibisDream
public Tween TweenBlur(float value, float duration)
{
return TweenFloat(BlurAmountId, value, duration);
if (!EnsureMaterial())
{
return null;
}
Tween tween = material.DOFloat(value, BlurAmountId, duration);
tween.OnUpdate(SyncBlurSizeToMaterial);
tween.OnComplete(SyncBlurSizeToMaterial);
return tween;
}
/// <summary>
/// One focus transition: sharpen, brief defocus, settle. Total wall time equals <paramref name="duration"/>.
/// </summary>
public Tween TweenBlurFocusSettle(EyeVisualSettings settings, float duration)
{
if (!EnsureMaterial() || settings == null || duration <= 0f)
{
return null;
}
float sharpBlur = settings.focusedBlurAmount;
float bumpBlur = Mathf.Clamp(
sharpBlur + settings.focusSettleDefocusBump,
sharpBlur,
settings.idleBlurAmount);
Vector3 ratios = settings.focusSettlePhaseRatios;
float ratioSum = ratios.x + ratios.y + ratios.z;
if (ratioSum <= 0.001f)
{
ratios = new Vector3(0.5f, 0.22f, 0.28f);
ratioSum = ratios.x + ratios.y + ratios.z;
}
float primaryDuration = duration * (ratios.x / ratioSum);
float defocusDuration = duration * (ratios.y / ratioSum);
float refocusDuration = duration * (ratios.z / ratioSum);
Sequence sequence = DOTween.Sequence();
Tween primaryFocus = TweenBlur(sharpBlur, primaryDuration);
if (primaryFocus != null)
{
primaryFocus.SetEase(Ease.InOutSine);
sequence.Append(primaryFocus);
}
Tween microDefocus = TweenBlur(bumpBlur, defocusDuration);
if (microDefocus != null)
{
microDefocus.SetEase(Ease.InOutSine);
sequence.Append(microDefocus);
}
Tween refocus = TweenBlur(sharpBlur, refocusDuration);
if (refocus != null)
{
refocus.SetEase(Ease.InOutSine);
sequence.Append(refocus);
}
return sequence;
}
public void SetWrongColor(float value)
@@ -189,16 +282,53 @@ namespace AibisDream
return TweenFloat(WrongColorAmountId, value, duration);
}
public Tween PulseDistortion(float amount, float duration)
public Tween PlayColorSweepTransition(EyeVisualSettings settings, float duration, bool colorIn)
{
if (!EnsureMaterial())
if (!EnsureMaterial() || settings == null || duration <= 0f)
{
return null;
}
return material.DOFloat(amount, DistortAmountId, Mathf.Max(0.01f, duration * 0.5f))
.SetEase(Ease.InOutSine)
.SetLoops(2, LoopType.Yoyo);
duration = Mathf.Max(0.01f, duration);
float chromaPeak = settings.colorSweepChromaPeak;
float ramp = Mathf.Clamp(settings.colorSweepChromaRamp, 0.1f, 0.6f);
float rampDuration = duration * ramp;
float settleDuration = duration - rampDuration;
float targetReveal = colorIn ? settings.colorRevealMax : settings.colorRevealMin;
float targetBlur = colorIn ? settings.focusedBlurAmount : settings.idleBlurAmount;
Ease sweepEase = colorIn ? Ease.InOutSine : Ease.InSine;
ClearTransientEffects(null);
SetFloatRaw(RevealNoiseId, 0f);
Sequence sequence = DOTween.Sequence();
Tween colorTween = TweenColorReveal(targetReveal, duration);
if (colorTween != null)
{
colorTween.SetEase(sweepEase);
sequence.Join(colorTween);
}
Tween blurTween = TweenBlur(targetBlur, duration);
if (blurTween != null)
{
blurTween.SetEase(sweepEase);
sequence.Join(blurTween);
}
if (chromaPeak > 0.001f)
{
Sequence chromaEnvelope = DOTween.Sequence();
chromaEnvelope.Append(material.DOFloat(chromaPeak, GlitchAmountId, rampDuration)
.SetEase(Ease.OutSine));
chromaEnvelope.Append(material.DOFloat(0f, GlitchAmountId, settleDuration)
.SetEase(Ease.InSine));
sequence.Join(chromaEnvelope);
}
sequence.OnComplete(() => ClearTransientEffects(settings));
return sequence;
}
public void SetImageAlpha(float alpha)
@@ -20,7 +20,7 @@ namespace AibisDream
private static readonly int EdgeSoftnessId = Shader.PropertyToID("_EdgeSoftness");
private static readonly int BlinkOpenId = Shader.PropertyToID("_BlinkOpen");
/// <summary>须低于 PersistenceUI Canvas(1) 与 Dialog Canvas(3)。</summary>
/// <summary>Persistence:本遮罩=0UI Canvas=1Dialog Canvas=3。</summary>
[SerializeField] private int sortingOrder;
private Canvas overlayCanvas;
@@ -45,7 +45,10 @@ namespace AibisDream
}
PrepareHierarchyIfNeeded();
SetOverlayActive(false);
if (!moduleActive && !editorPreviewActive)
{
SetOverlayActive(false);
}
}
private void OnDestroy()
@@ -143,7 +146,7 @@ namespace AibisDream
}
}
public Tween PlayBlink(float? totalDuration = null)
public Tween PlayBlink(float? totalDuration = null, bool restartAutoBlinkAfter = false)
{
if (instanceMaterial == null || activeSettings == null)
{
@@ -164,11 +167,109 @@ namespace AibisDream
blinkTween = DOTween.Sequence()
.Append(CreateBlinkTween(blinkOpen, 0f, closeDuration, Ease.InQuad))
.Append(CreateBlinkTween(0f, 1f, openDuration, Ease.OutQuad))
.OnComplete(() => SetBlinkOpen(1f));
.OnComplete(() =>
{
SetBlinkOpen(1f);
if (restartAutoBlinkAfter)
{
RestartAutoBlinkCooldown(activeSettings);
}
});
return blinkTween;
}
/// <summary>
/// 先眨眼,再在最后一次眨眼的开眼阶段触发效果(聚焦 1 次 / 想象 2 次)。
/// </summary>
public IEnumerator PlayPreEffectBlinkThen(
int blinkCount,
EyeVisualSettings settings,
System.Action onEffectStart,
float effectAtOpenRatio,
float betweenBlinkGap = 0f)
{
if (!ShouldPlayTransitionBlink(settings))
{
onEffectStart?.Invoke();
yield break;
}
StopAutoBlink();
bool effectStarted = false;
blinkCount = Mathf.Max(1, blinkCount);
effectAtOpenRatio = Mathf.Clamp01(effectAtOpenRatio);
for (int i = 0; i < blinkCount; i++)
{
bool isLastBlink = i == blinkCount - 1;
Tween blink = PlayBlink();
if (blink == null)
{
if (isLastBlink && !effectStarted)
{
effectStarted = true;
onEffectStart?.Invoke();
}
continue;
}
if (isLastBlink)
{
float effectDelay = settings.blinkCloseDuration + settings.blinkOpenDuration * effectAtOpenRatio;
yield return new WaitForSeconds(effectDelay);
if (!effectStarted)
{
effectStarted = true;
onEffectStart?.Invoke();
}
yield return blink.WaitForCompletion();
}
else
{
yield return blink.WaitForCompletion();
if (betweenBlinkGap > 0f)
{
yield return new WaitForSeconds(betweenBlinkGap);
}
}
}
if (!effectStarted)
{
onEffectStart?.Invoke();
}
RestartAutoBlinkCooldown(settings);
}
private bool ShouldPlayTransitionBlink(EyeVisualSettings settings)
{
return Application.isPlaying
&& moduleActive
&& !editorPreviewActive
&& settings != null
&& settings.enableViewportOverlay;
}
private void RestartAutoBlinkCooldown(EyeVisualSettings settings)
{
if (settings == null || !settings.autoBlink || !Application.isPlaying)
{
return;
}
if (!(moduleActive || editorPreviewActive))
{
return;
}
StartAutoBlink(settings);
}
private void ShowOverlay(EyeVisualSettings settings)
{
if (settings == null || !settings.enableViewportOverlay)
@@ -178,6 +279,7 @@ namespace AibisDream
}
PrepareHierarchyIfNeeded();
ApplyCanvasRenderSettings();
activeSettings = settings;
ApplySettings(settings);
if (!editorPreviewActive)
@@ -279,11 +381,6 @@ namespace AibisDream
private void PrepareHierarchyIfNeeded()
{
if (hierarchyReady && overlayCanvas != null)
{
return;
}
if (Application.isPlaying && !lifecycleReady)
{
ScheduleDeferredPrepare();
@@ -292,6 +389,7 @@ namespace AibisDream
EnsureOverlayHierarchy();
hierarchyReady = overlayCanvas != null;
ApplyCanvasRenderSettings();
}
private void ScheduleDeferredPrepare()
@@ -316,6 +414,8 @@ namespace AibisDream
PushViewportParams(activeSettings);
SetBlinkOpen(blinkOpen);
}
ApplyCanvasRenderSettings();
}
private void EnsureOverlayHierarchy()
@@ -382,6 +482,21 @@ namespace AibisDream
overlayCanvas.planeDistance = OverlayPlaneDistance;
overlayCanvas.sortingLayerName = OverlaySortingLayer;
overlayCanvas.sortingOrder = sortingOrder;
SetOverlayUILayer(overlayCanvas.gameObject);
}
private static void SetOverlayUILayer(GameObject root)
{
int uiLayer = LayerMask.NameToLayer("UI");
if (uiLayer < 0)
{
uiLayer = 5;
}
foreach (Transform node in root.GetComponentsInChildren<Transform>(true))
{
node.gameObject.layer = uiLayer;
}
}
private static Camera ResolveUICamera()
@@ -391,7 +506,7 @@ namespace AibisDream
return UIManager.Instance.UICamera;
}
var uiManager = FindObjectOfType<UIManager>();
var uiManager = Object.FindObjectOfType<UIManager>();
if (uiManager != null && uiManager.UICamera != null)
{
return uiManager.UICamera;
@@ -42,9 +42,23 @@ namespace AibisDream
[Range(0f, 1f)] public float revealNoise = 0.08f;
[Header("Distortion Pulse")]
[Tooltip("Peak _DistortAmount during ColorIn / FadeOut.")]
[Range(0f, 0.08f)] public float colorPulseDistortion = 0.035f;
[Header("Color Sweep (ColorIn / FadeOut)")]
[Tooltip("Light RGB fringe during color sweep; 0 = sweep only.")]
[Range(0f, 1f)] public float colorSweepChromaPeak = 0.35f;
[Tooltip("Sweep duration fraction to ramp chroma fringe to peak.")]
[Range(0.1f, 0.6f)] public float colorSweepChromaRamp = 0.4f;
[HideInInspector] public float imagineGlitchPeak = 0.35f;
[HideInInspector] public float imagineGlitchRamp = 0.4f;
[HideInInspector] public float colorPulseDistortion = 0.06f;
[Header("Focus Settle (BlurOut)")]
[Tooltip("Mid-phase micro-defocus added on top of focused blur.")]
[Range(0f, 0.5f)] public float focusSettleDefocusBump = 0.12f;
[Tooltip("Single BlurOut timing split (focus | micro-defocus | refocus). Sum = 1; total seconds unchanged.")]
public Vector3 focusSettlePhaseRatios = new(0.5f, 0.22f, 0.28f);
[Header("Wrong Color (Eye Disorder)")]
public Color wrongColor = new(1f, 0.16f, 0.08f, 1f);
@@ -77,6 +91,16 @@ namespace AibisDream
public float blinkCloseDuration = 0.09f;
public float blinkOpenDuration = 0.14f;
[Header("Transition Blink")]
[Tooltip("聚焦:单次眨眼开眼到该进度时触发变清晰(0=刚睁眼,1=完全睁开)。")]
[Range(0f, 1f)] public float focusEffectAtBlinkOpen = 0.85f;
[Tooltip("想象:两次眨眼之间的间隔(秒)。")]
public float imagineBlinkGap = 0.04f;
[Tooltip("想象:第二次眨眼开眼到该进度时触发上色。")]
[Range(0f, 1f)] public float imagineEffectAtBlinkOpen = 1f;
public float GetDurationForState(EyeSystem.EyeColorState state) => state switch
{
EyeSystem.EyeColorState.CanImagineColor => imagineColorDuration,
@@ -11,9 +11,9 @@ Shader "AibisDream/MemoryTuningSprite"
_PixelMin ("Pixel Min", Range(8,64)) = 18
_PixelMax ("Pixel Max", Range(32,256)) = 128
_BlurStep ("Blur Step", Range(0,0.01)) = 0.0025
_DistortStrength ("Distort Strength", Range(0,0.05)) = 0.018
_DistortStrength ("Distort Strength", Range(0,0.08)) = 0.035
_ChromaticStrength ("Chromatic Strength", Range(0,0.03)) = 0.008
_FadeDistortStrength ("Fade Distort Strength", Range(0,0.2)) = 0.08
_FadeDistortStrength ("Fade Distort Strength", Range(0,0.2)) = 0.12
_PixelateFade ("Pixelate Fade", Range(0,1)) = 0
_PixelatePixelDensity ("Pixelate Pixel Density", Range(6,160)) = 96
_GaussianBlurFade ("Gaussian Blur Fade", Range(0,1)) = 0
@@ -123,10 +123,10 @@ Shader "AibisDream/MemoryTuningSprite"
float angle = radians(130.0);
float2 dir = float2(cos(angle), sin(angle));
float2 side = float2(-sin(angle), cos(angle));
float2 edge = smoothstep(0.0, 0.12, uv) * smoothstep(0.0, 0.12, 1.0 - uv);
float2 edge = smoothstep(0.0, 0.07, uv) * smoothstep(0.0, 0.07, 1.0 - uv);
float edgeMask = edge.x * edge.y;
float wave = sin(dot(uv - 0.5, dir) * 27.2 + _Time.y * 2.15);
float volFxStrength = lerp(0.006, 0.026, saturate(_UVDistortFade)) * strength;
float volFxStrength = lerp(_DistortStrength * 0.55, _DistortStrength, saturate(_UVDistortFade)) * strength;
return uv + side * wave * volFxStrength * edgeMask;
}
@@ -173,7 +173,7 @@ Shader "AibisDream/MemoryTuningSprite"
half frequency = saturate(_FrequencyTuning);
half pixelate = pow(max(frequency, saturate(_PixelateFade)), 0.75);
half blur = pow(max(frequency, saturate(_GaussianBlurFade)), 0.75);
half distortion = pow(max(clarity, saturate(_UVDistortFade / 0.58)), 0.75);
half distortion = pow(max(clarity, saturate(_UVDistortFade)), 0.85);
float2 uv = input.uv;
half fade = saturate((_DirectionalDistortionFade + 15.0) / 15.0);
@@ -210,7 +210,6 @@ position: -1467,-2207
<<set $eyeTargetTriggerCount = $eyeTargetTriggerCount + 1>>
me:佩佩,看到那张海报了么? #line:005434c
<<set_EyeColorState EyeManager "CanImagineColor">>
<<set_Eyetarget EyeManager "poster">>
peipei: 那是,啊……<br>那是我装上UF之后画的的第一张广告画。<br>“护肤机油”。 #line:0d47b90
me:原来这是UF画的。我就说…<br>好像从那个时候开始,公司的美术都变得…怪怪的。 #line:06dd03c
peipei: 那是我的错……<br>我不习惯UF,本来想用笔来画的… #line:05f750d
@@ -231,8 +230,6 @@ position: -1467,-2207
<<CollectionClue>>
//<<jump EyeCheckEnd>>
<<endif>>
<<set_EyeColorState EyeManager "CanImagineColor">>
<<EyeEffect_FadeIn EyeManager>>
<<jump EyeCheckEnd>>
===
@@ -268,8 +265,6 @@ peipei: 我愧疚了好久,那时的我真的以为小草可以长成一棵树
<<CollectionClue>>
//<<jump EyeCheckEnd>>
<<endif>>
<<set_EyeColorState EyeManager "CanImagineColor">>
<<EyeEffect_FadeIn EyeManager>>
<<jump EyeCheckEnd>>
===
+120
View File
@@ -0,0 +1,120 @@
# AgentBridge 计划草案
> 状态:草案,待讨论后实施。
> 目的:让 Cursor Agent / Unity MCP 能自动到达可测状态、读取结构化运行时状态、做回归验证,减少手动 Play + 复现。
## 背景
当前 Agent 已可通过 **Unity MCP** 做:
- Play / Stop、加载场景、查 GameObject / 组件
- 读 Console、截图
- 触发 Editor MenuItem`execute_menu_item`
难以自动完成的部分:
- 完整叙事流程(Persistence → 维修场景 → Yarn → 进模块)
- UI 交互(Esc 开设置、点 log 面板等)
- 缺乏结构化状态输出,debug 依赖临时埋点
本项目已有大量可复用入口:`FixSystemCenter``EyeSystem``DialogController``UIManager`、各 `*YarnCommand``PeipeiFixCues`(如 `EyeCue.EnterImmediate`)等。AgentBridge 只做薄包装,不重写业务逻辑。
## 目标
1. **一键到可测状态** — 场景预设,而非暴露底层细节
2. **状态可查询** — JSON / Log,便于 Agent 自动验证
3. **Editor / Dev 专用**`#if UNITY_EDITOR || DEVELOPMENT_BUILD`,不进 Release
4. **菜单路径稳定** — 供 MCP `execute_menu_item` 与文档引用
## 阶段 1:最小可用(建议优先)
| 项 | 内容 | 调用方式 |
|---|---|---|
| `AgentBridge.cs` | 静态薄包装 | — |
| 场景预设 | `EnterPeipeiEyeModule()``EyeCue.EnterImmediate` | `Tools/Agent/Enter Peipei Eye Module` |
| UI 操作 | `ToggleSettingPanel()``ToggleMainPanel()` | `Tools/Agent/Toggle Setting Panel` 等 |
| 状态查询 | `DumpEyeOverlayState()` → 输出 layer / sortingOrder / active | `Tools/Agent/Dump Eye Overlay State` |
| 文档 | 本文件 + 菜单清单维护 | Agent / `.cursor/rules` 引用 |
### 建议 Editor 菜单(第一批)
```
Tools/Agent/Enter Peipei Eye Module
Tools/Agent/Toggle Setting Panel
Tools/Agent/Toggle Main Panel
Tools/Agent/Dump Eye Overlay State
```
### 代码位置(待拍板)
- 方案 A`Assets/Scripts/Framework/AgentBridge/`Runtime + `#if` 守卫)
- 方案 B`Assets/Editor/Agent/`(仅 EditorMenuItem 与 Bridge 同目录)
## 阶段 2:常用模块覆盖(按需扩展)
按实际 debug 频率扩展场景预设:
| 模块 | 预设示例 | 复用 API |
|---|---|---|
| FixSystem | `TransitionTo(Eye / UF / Memory)``ResetCablePanel` | `FixSystemCenter` |
| Eye | `EyeOverlayOn/Off``SetEyeTarget(name)` | `EyeSystem` |
| Dialog | `StartDialogNode(nodeName)` | `DialogController` |
| Save | `LoadTestSave("peipei_eye")` | `SaveSystem` / 固定测试档 |
| Camera | `SwitchCamera(EyeDeep)` 等 | `CameraKit` |
每个预设 = 一个 MenuItem + 一个 `AgentBridge` 方法 +(可选)一个 `GetXxxState()`
## 阶段 3:可验证 + 防回归
| 项 | 用途 |
|---|---|
| `GetXxxState()` 返回 JSON | Agent 读 Console / 文件验证,少依赖截图 |
| PlayMode Tests | 如 overlay UI 层、sortingOrder、Dialog 不被挡 |
| MCP `run_tests` | UI / 渲染相关改动后自动跑 |
### 示例:Eye Overlay 状态结构
```json
{
"overlayActive": true,
"canvasLayer": 5,
"sortingOrder": 0,
"sortingLayer": "StartUI",
"worldCamera": "UI Camera",
"layerVisibleToCamera": true
}
```
## 设计原则
1. **只包装,不重写** — 调用现有 public API
2. **场景预设 > 原子命令** — 优先「一键到眼动模块」
3. **状态可查询** — 每个常 debug 模块至少一个 `GetState()` / `Dump*()`
4. **不进入 Release** — 编译条件守卫 + 不进 Addressables
5. **菜单路径稳定** — 变更需同步更新本文档
## 待讨论问题
1. **代码放哪**`Framework/AgentBridge` 还是 `Editor/Agent`
2. **测试存档**:是否需要固定 PlayMode 存档 slot,还是每次冷启动场景?
3. **第一批优先级**:眼动 / 插线 / 对话 / 记忆 — 哪个最常需要 Agent 介入?
4. **是否写入 `.cursor/rules`**:固定 MenuItem 清单,让 Agent 默认走 Bridge 流程?
5. **与现有 Editor 工具关系**:如 `EyeSystemEditor` 预览 — 合并还是并存?
## 参考:Persistence UI 层级
Eye 视口遮罩修复后的约定(`EyeViewportOverlay`):
| Canvas | sortingOrder | 说明 |
|---|---|---|
| EyeViewportOverlay | 0 | 遮罩,不挡系统 UI |
| UI Canvas | 1 | 设置、MainPanel、PlayTool 等 |
| Dialog Canvas | 3 | 对话 |
Overlay 使用 `ScreenSpaceCamera` + UI CameraCanvas 须在 **UI 层 (5)** 以匹配 UI Camera culling mask。
## 变更记录
| 日期 | 说明 |
|---|---|
| 2026-07-01 | 初稿:overlay debug 后整理 Agent 自动化计划 |