feat(peipei-eye): 新增视口椭圆遮罩与眨眼效果
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
Shader "UI/EyeViewportOverlay"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
|
||||
_Color ("Tint", Color) = (0, 0, 0, 1)
|
||||
|
||||
[Header(Eye Opening)]
|
||||
_EyeWidth ("Eye Width", Range(0.22, 0.58)) = 0.46
|
||||
_EyeHeight ("Eye Height", Range(0.15, 0.72)) = 0.28
|
||||
_EdgeSoftness ("Edge Softness", Range(0.001, 0.2)) = 0.022
|
||||
_BlinkOpen ("Blink Open", Range(0, 1)) = 1
|
||||
|
||||
_StencilComp ("Stencil Comparison", Float) = 8
|
||||
_Stencil ("Stencil ID", Float) = 0
|
||||
_StencilOp ("Stencil Operation", Float) = 0
|
||||
_StencilWriteMask ("Stencil Write Mask", Float) = 255
|
||||
_StencilReadMask ("Stencil Read Mask", Float) = 255
|
||||
_ColorMask ("Color Mask", Float) = 15
|
||||
}
|
||||
|
||||
SubShader
|
||||
{
|
||||
Tags
|
||||
{
|
||||
"Queue" = "Transparent"
|
||||
"IgnoreProjector" = "True"
|
||||
"RenderType" = "Transparent"
|
||||
"PreviewType" = "Plane"
|
||||
"CanUseSpriteAtlas" = "True"
|
||||
}
|
||||
|
||||
Stencil
|
||||
{
|
||||
Ref [_Stencil]
|
||||
Comp [_StencilComp]
|
||||
Pass [_StencilOp]
|
||||
ReadMask [_StencilReadMask]
|
||||
WriteMask [_StencilWriteMask]
|
||||
}
|
||||
|
||||
Cull Off
|
||||
Lighting Off
|
||||
ZWrite Off
|
||||
ZTest [unity_GUIZTestMode]
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
ColorMask [_ColorMask]
|
||||
|
||||
Pass
|
||||
{
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
#pragma target 2.0
|
||||
#include "UnityCG.cginc"
|
||||
#include "UnityUI.cginc"
|
||||
|
||||
struct appdata
|
||||
{
|
||||
float4 vertex : POSITION;
|
||||
float4 color : COLOR;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float4 vertex : SV_POSITION;
|
||||
fixed4 color : COLOR;
|
||||
float2 uv : TEXCOORD0;
|
||||
float4 worldPosition : TEXCOORD1;
|
||||
};
|
||||
|
||||
sampler2D _MainTex;
|
||||
fixed4 _Color;
|
||||
float4 _ClipRect;
|
||||
|
||||
float _EyeWidth;
|
||||
float _EyeHeight;
|
||||
float _EdgeSoftness;
|
||||
float _BlinkOpen;
|
||||
|
||||
v2f vert(appdata v)
|
||||
{
|
||||
v2f o;
|
||||
o.worldPosition = v.vertex;
|
||||
o.vertex = UnityObjectToClipPos(v.vertex);
|
||||
o.uv = v.uv;
|
||||
o.color = v.color * _Color;
|
||||
return o;
|
||||
}
|
||||
|
||||
float FeatherBoundaryAlpha(float insideHard, float edgeDist, float softness)
|
||||
{
|
||||
float outside = 1.0 - insideHard;
|
||||
float edgeFeather = (1.0 - smoothstep(0.0, softness, edgeDist)) * insideHard;
|
||||
return saturate(outside + edgeFeather);
|
||||
}
|
||||
|
||||
float ComputeViewportAlpha(float2 uv)
|
||||
{
|
||||
float2 c = uv - 0.5;
|
||||
float softness = max(0.001, _EdgeSoftness);
|
||||
float blinkOpen = saturate(_BlinkOpen);
|
||||
|
||||
if (blinkOpen <= 0.001)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float rx = max(_EyeWidth, 0.001);
|
||||
float ry = max(_EyeHeight * blinkOpen, 0.001);
|
||||
|
||||
float ellipseDist = length(float2(c.x / rx, c.y / ry));
|
||||
float insideHard = 1.0 - step(1.0, ellipseDist);
|
||||
float edgeDist = (1.0 - ellipseDist) * insideHard;
|
||||
return FeatherBoundaryAlpha(insideHard, edgeDist, softness);
|
||||
}
|
||||
|
||||
fixed4 frag(v2f i) : SV_Target
|
||||
{
|
||||
float alpha = ComputeViewportAlpha(i.uv);
|
||||
fixed4 col = fixed4(i.color.rgb, i.color.a * alpha);
|
||||
col.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect);
|
||||
return col;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
|
||||
Fallback Off
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6652ed847ed1bc641bd702f03c650adf
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+1100
-1072
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,9 @@ namespace AibisDream
|
||||
[CustomEditor(typeof(EyeSystem))]
|
||||
public class EyeSystemEditor : Editor
|
||||
{
|
||||
private bool editorOverlayPreview;
|
||||
private float editorBlinkPreview = 1f;
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
@@ -29,11 +32,13 @@ namespace AibisDream
|
||||
EditorGUILayout.PropertyField(replayOnChange);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"Blur / Reveal 扫过 / Wrong Color / HSV:改完即写入材质。\n" +
|
||||
"时长 / colorReveal 范围:Play 下需勾选「自动重播」或点「重播当前目标效果」。\n" +
|
||||
"Edit 模式:改参数后会自动把所有目标重置为 idle(灰+糊)预览。",
|
||||
"常驻视线 = 椭圆(eyeWidth / eyeHeight)。\n" +
|
||||
"眨眼 = 同一椭圆纵向压扁(BlinkOpen 1→0),不是另一套眼皮形状。\n" +
|
||||
"用「BlinkOpen 预览」滑条看闭合;eyeWidth / eyeHeight 在 BlinkOpen=1 时调。",
|
||||
MessageType.Info);
|
||||
|
||||
DrawViewportOverlayControls(visualChanged);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("立即应用(Idle 预览)"))
|
||||
{
|
||||
@@ -55,6 +60,12 @@ namespace AibisDream
|
||||
system.ApplyVisualSettingsNow(
|
||||
previewIdleState: !Application.isPlaying,
|
||||
replayFocusedEffect: Application.isPlaying && replayOnChange.boolValue);
|
||||
|
||||
if (editorOverlayPreview)
|
||||
{
|
||||
RefreshEditorOverlayPreview(system);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -68,10 +79,107 @@ namespace AibisDream
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private void DrawViewportOverlayControls(bool visualChanged)
|
||||
{
|
||||
EditorGUILayout.Space(4f);
|
||||
EditorGUILayout.LabelField("Viewport Overlay 预览", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
editorOverlayPreview = EditorGUILayout.Toggle("Edit 模式预览遮罩", editorOverlayPreview);
|
||||
if (EditorGUI.EndChangeCheck() || visualChanged)
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
var system = GetEyeSystem();
|
||||
if (editorOverlayPreview)
|
||||
{
|
||||
RefreshEditorOverlayPreview(system);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureOverlay(system).ExitEditorPreview();
|
||||
editorBlinkPreview = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
GUI.enabled = editorOverlayPreview;
|
||||
EditorGUI.BeginChangeCheck();
|
||||
editorBlinkPreview = EditorGUILayout.Slider("BlinkOpen 预览(眨眼)", editorBlinkPreview, 0f, 1f);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
var overlay = EnsureOverlay(GetEyeSystem());
|
||||
overlay.SetBlinkOpenPreview(editorBlinkPreview);
|
||||
}
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("BlinkOpen = 睁开"))
|
||||
{
|
||||
editorBlinkPreview = 1f;
|
||||
EnsureOverlay(GetEyeSystem()).SetBlinkOpenPreview(1f);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("BlinkOpen = 闭合"))
|
||||
{
|
||||
editorBlinkPreview = 0f;
|
||||
EnsureOverlay(GetEyeSystem()).SetBlinkOpenPreview(0f);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
GUI.enabled = true;
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUI.enabled = Application.isPlaying;
|
||||
if (GUILayout.Button("测试眨眼动画"))
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
var overlay = EnsureOverlay(GetEyeSystem());
|
||||
editorBlinkPreview = 1f;
|
||||
overlay.SetBlinkOpenPreview(1f);
|
||||
overlay.PlayBlink();
|
||||
}
|
||||
GUI.enabled = true;
|
||||
|
||||
if (GUILayout.Button("刷新遮罩参数"))
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
RefreshEditorOverlayPreview(GetEyeSystem());
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void RefreshEditorOverlayPreview(EyeSystem system)
|
||||
{
|
||||
var overlay = EnsureOverlay(system);
|
||||
overlay.EnterEditorPreview(system.VisualSettings);
|
||||
overlay.SetBlinkOpenPreview(editorBlinkPreview);
|
||||
}
|
||||
|
||||
private static EyeViewportOverlay EnsureOverlay(EyeSystem system)
|
||||
{
|
||||
var overlay = system.GetComponentInChildren<EyeViewportOverlay>(true);
|
||||
if (overlay == null)
|
||||
{
|
||||
overlay = system.gameObject.AddComponent<EyeViewportOverlay>();
|
||||
}
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
private EyeSystem GetEyeSystem()
|
||||
{
|
||||
return (EyeSystem)target;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!editorOverlayPreview || target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
editorOverlayPreview = false;
|
||||
editorBlinkPreview = 1f;
|
||||
var system = target as EyeSystem;
|
||||
system?.GetComponentInChildren<EyeViewportOverlay>(true)?.ExitEditorPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace AibisDream
|
||||
[Header("Visual Tuning")]
|
||||
[SerializeField] private EyeVisualSettings visualSettings = new();
|
||||
|
||||
[SerializeField] private EyeViewportOverlay viewportOverlay;
|
||||
|
||||
[Tooltip("Play 模式下改 Visual Tuning 后,自动对当前目标重播一次 Focused 效果(如 ColorIn)。")]
|
||||
[SerializeField] private bool replayFocusedEffectOnSettingsChange = true;
|
||||
|
||||
@@ -54,13 +56,17 @@ namespace AibisDream
|
||||
{
|
||||
ApplyVisualSettingsNow(
|
||||
previewIdleState: !Application.isPlaying,
|
||||
replayFocusedEffect: Application.isPlaying && replayFocusedEffectOnSettingsChange);
|
||||
replayFocusedEffect: Application.isPlaying && replayFocusedEffectOnSettingsChange,
|
||||
touchViewportOverlay: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Visual Tuning 推送到所有 EyeTarget。Edit 模式预览 idle;Play 模式可选重播当前目标效果。
|
||||
/// </summary>
|
||||
public void ApplyVisualSettingsNow(bool previewIdleState = false, bool replayFocusedEffect = false)
|
||||
public void ApplyVisualSettingsNow(
|
||||
bool previewIdleState = false,
|
||||
bool replayFocusedEffect = false,
|
||||
bool touchViewportOverlay = true)
|
||||
{
|
||||
if (targets == null)
|
||||
{
|
||||
@@ -81,6 +87,11 @@ namespace AibisDream
|
||||
}
|
||||
}
|
||||
|
||||
if (touchViewportOverlay && Application.isPlaying)
|
||||
{
|
||||
EnsureViewportOverlay()?.ApplySettings(visualSettings);
|
||||
}
|
||||
|
||||
if (!replayFocusedEffect || !Application.isPlaying || currentTarget == null || eyeStateEffect == null)
|
||||
{
|
||||
return;
|
||||
@@ -109,6 +120,22 @@ namespace AibisDream
|
||||
Init();
|
||||
}
|
||||
|
||||
private EyeViewportOverlay EnsureViewportOverlay()
|
||||
{
|
||||
if (viewportOverlay != null)
|
||||
{
|
||||
return viewportOverlay;
|
||||
}
|
||||
|
||||
viewportOverlay = GetComponentInChildren<EyeViewportOverlay>(true);
|
||||
if (viewportOverlay == null)
|
||||
{
|
||||
viewportOverlay = gameObject.AddComponent<EyeViewportOverlay>();
|
||||
}
|
||||
|
||||
return viewportOverlay;
|
||||
}
|
||||
|
||||
private bool EnsureMouseFollowAndZoom()
|
||||
{
|
||||
if (mouseFollowAndZoom != null)
|
||||
@@ -199,6 +226,7 @@ namespace AibisDream
|
||||
mouseFollowAndZoom.ResetPanOrigin();
|
||||
ApplyVisualSettingsToTargets();
|
||||
InitializeAllTargets();
|
||||
EnsureViewportOverlay()?.EnterModule(visualSettings);
|
||||
}
|
||||
|
||||
private void InitializeAllTargets()
|
||||
@@ -220,6 +248,8 @@ namespace AibisDream
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
EnsureViewportOverlay()?.ExitModule();
|
||||
|
||||
if (currentTarget != null)
|
||||
{
|
||||
eyeStateEffect?.CleanupEffect(CurrentTarget);
|
||||
@@ -256,6 +286,12 @@ namespace AibisDream
|
||||
StartCoroutine(SetTargetCoroutine(newTarget.gameObject.name));
|
||||
}
|
||||
|
||||
[YarnCommand("EyeBlink")]
|
||||
public void EyeBlink(float duration = 0f)
|
||||
{
|
||||
EnsureViewportOverlay()?.PlayBlink(duration > 0f ? duration : null);
|
||||
}
|
||||
|
||||
[YarnCommand("set_Eyetarget")]
|
||||
public IEnumerator SetTargetCoroutine(string targetName, bool startDialogue = true)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
using System.Collections;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace AibisDream
|
||||
{
|
||||
/// <summary>
|
||||
/// Mobile-friendly fullscreen eye viewport: elliptical sight + vertical ellipse blink.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[ExecuteAlways]
|
||||
public class EyeViewportOverlay : MonoBehaviour
|
||||
{
|
||||
private static readonly int EyeWidthId = Shader.PropertyToID("_EyeWidth");
|
||||
private static readonly int EyeHeightId = Shader.PropertyToID("_EyeHeight");
|
||||
private static readonly int EdgeSoftnessId = Shader.PropertyToID("_EdgeSoftness");
|
||||
private static readonly int BlinkOpenId = Shader.PropertyToID("_BlinkOpen");
|
||||
|
||||
[SerializeField] private int sortingOrder = 120;
|
||||
|
||||
private Canvas overlayCanvas;
|
||||
private Image overlayImage;
|
||||
private Material instanceMaterial;
|
||||
private EyeVisualSettings activeSettings;
|
||||
private Coroutine autoBlinkRoutine;
|
||||
private Tween blinkTween;
|
||||
private float blinkOpen = 1f;
|
||||
private bool moduleActive;
|
||||
private bool editorPreviewActive;
|
||||
private bool hierarchyReady;
|
||||
private bool lifecycleReady;
|
||||
private Coroutine deferPrepareRoutine;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
lifecycleReady = true;
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PrepareHierarchyIfNeeded();
|
||||
SetOverlayActive(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopEffects();
|
||||
if (instanceMaterial != null)
|
||||
{
|
||||
Destroy(instanceMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
public void EnterModule(EyeVisualSettings settings)
|
||||
{
|
||||
moduleActive = true;
|
||||
editorPreviewActive = false;
|
||||
ShowOverlay(settings);
|
||||
}
|
||||
|
||||
public void ExitModule()
|
||||
{
|
||||
moduleActive = false;
|
||||
if (editorPreviewActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopEffects();
|
||||
SetOverlayActive(false);
|
||||
activeSettings = null;
|
||||
}
|
||||
|
||||
public void EnterEditorPreview(EyeVisualSettings settings)
|
||||
{
|
||||
editorPreviewActive = true;
|
||||
ShowOverlay(settings);
|
||||
}
|
||||
|
||||
public void ExitEditorPreview()
|
||||
{
|
||||
editorPreviewActive = false;
|
||||
if (moduleActive)
|
||||
{
|
||||
ShowOverlay(activeSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
StopEffects();
|
||||
SetOverlayActive(false);
|
||||
activeSettings = null;
|
||||
}
|
||||
|
||||
public void ApplySettings(EyeVisualSettings settings)
|
||||
{
|
||||
if (settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.enableViewportOverlay)
|
||||
{
|
||||
if (editorPreviewActive)
|
||||
{
|
||||
ExitEditorPreview();
|
||||
}
|
||||
else if (moduleActive)
|
||||
{
|
||||
ExitModule();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
PrepareHierarchyIfNeeded();
|
||||
if (instanceMaterial == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
activeSettings = settings;
|
||||
PushViewportParams(settings);
|
||||
SetBlinkOpen(blinkOpen);
|
||||
|
||||
if (!overlayCanvas.gameObject.activeInHierarchy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.autoBlink && Application.isPlaying && (moduleActive || editorPreviewActive))
|
||||
{
|
||||
StartAutoBlink(settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
StopAutoBlink();
|
||||
}
|
||||
}
|
||||
|
||||
public Tween PlayBlink(float? totalDuration = null)
|
||||
{
|
||||
if (instanceMaterial == null || activeSettings == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
blinkTween?.Kill();
|
||||
|
||||
float closeDuration = activeSettings.blinkCloseDuration;
|
||||
float openDuration = activeSettings.blinkOpenDuration;
|
||||
if (totalDuration.HasValue)
|
||||
{
|
||||
float total = Mathf.Max(0.05f, totalDuration.Value);
|
||||
closeDuration = total * 0.42f;
|
||||
openDuration = total * 0.58f;
|
||||
}
|
||||
|
||||
blinkTween = DOTween.Sequence()
|
||||
.Append(CreateBlinkTween(blinkOpen, 0f, closeDuration, Ease.InQuad))
|
||||
.Append(CreateBlinkTween(0f, 1f, openDuration, Ease.OutQuad))
|
||||
.OnComplete(() => SetBlinkOpen(1f));
|
||||
|
||||
return blinkTween;
|
||||
}
|
||||
|
||||
private void ShowOverlay(EyeVisualSettings settings)
|
||||
{
|
||||
if (settings == null || !settings.enableViewportOverlay)
|
||||
{
|
||||
ExitModule();
|
||||
return;
|
||||
}
|
||||
|
||||
PrepareHierarchyIfNeeded();
|
||||
activeSettings = settings;
|
||||
ApplySettings(settings);
|
||||
if (!editorPreviewActive)
|
||||
{
|
||||
SetBlinkOpen(1f);
|
||||
}
|
||||
|
||||
SetOverlayActive(true);
|
||||
|
||||
if (settings.autoBlink && Application.isPlaying)
|
||||
{
|
||||
StartAutoBlink(settings);
|
||||
}
|
||||
}
|
||||
|
||||
private void PushViewportParams(EyeVisualSettings settings)
|
||||
{
|
||||
instanceMaterial.SetFloat(EyeWidthId, settings.eyeWidth);
|
||||
instanceMaterial.SetFloat(EyeHeightId, settings.eyeHeight);
|
||||
instanceMaterial.SetFloat(EdgeSoftnessId, settings.edgeSoftness);
|
||||
}
|
||||
|
||||
private Tween CreateBlinkTween(float from, float to, float duration, Ease ease)
|
||||
{
|
||||
blinkOpen = from;
|
||||
SetBlinkOpen(from);
|
||||
return DOTween.To(() => blinkOpen, value =>
|
||||
{
|
||||
blinkOpen = value;
|
||||
SetBlinkOpen(value);
|
||||
}, to, duration).SetEase(ease);
|
||||
}
|
||||
|
||||
private void StartAutoBlink(EyeVisualSettings settings)
|
||||
{
|
||||
StopAutoBlink();
|
||||
if (!settings.autoBlink)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
autoBlinkRoutine = StartCoroutine(AutoBlinkRoutine(settings));
|
||||
}
|
||||
|
||||
private IEnumerator AutoBlinkRoutine(EyeVisualSettings settings)
|
||||
{
|
||||
while (enabled && overlayCanvas != null && overlayCanvas.gameObject.activeInHierarchy)
|
||||
{
|
||||
float min = Mathf.Max(1f, settings.autoBlinkInterval.x);
|
||||
float max = Mathf.Max(min, settings.autoBlinkInterval.y);
|
||||
yield return new WaitForSeconds(Random.Range(min, max));
|
||||
|
||||
Tween blink = PlayBlink();
|
||||
if (blink != null)
|
||||
{
|
||||
yield return blink.WaitForCompletion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StopEffects()
|
||||
{
|
||||
blinkTween?.Kill();
|
||||
blinkTween = null;
|
||||
StopAutoBlink();
|
||||
}
|
||||
|
||||
private void StopAutoBlink()
|
||||
{
|
||||
if (autoBlinkRoutine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(autoBlinkRoutine);
|
||||
autoBlinkRoutine = null;
|
||||
}
|
||||
|
||||
public void SetBlinkOpenPreview(float value)
|
||||
{
|
||||
SetBlinkOpen(value);
|
||||
}
|
||||
|
||||
public float GetBlinkOpenPreview() => blinkOpen;
|
||||
|
||||
private void SetBlinkOpen(float value)
|
||||
{
|
||||
blinkOpen = Mathf.Clamp01(value);
|
||||
instanceMaterial?.SetFloat(BlinkOpenId, blinkOpen);
|
||||
}
|
||||
|
||||
private void SetOverlayActive(bool active)
|
||||
{
|
||||
if (overlayCanvas != null)
|
||||
{
|
||||
overlayCanvas.gameObject.SetActive(active);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareHierarchyIfNeeded()
|
||||
{
|
||||
if (hierarchyReady && overlayCanvas != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.isPlaying && !lifecycleReady)
|
||||
{
|
||||
ScheduleDeferredPrepare();
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureOverlayHierarchy();
|
||||
hierarchyReady = overlayCanvas != null;
|
||||
}
|
||||
|
||||
private void ScheduleDeferredPrepare()
|
||||
{
|
||||
if (deferPrepareRoutine != null || !isActiveAndEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
deferPrepareRoutine = StartCoroutine(DeferredPrepareHierarchy());
|
||||
}
|
||||
|
||||
private IEnumerator DeferredPrepareHierarchy()
|
||||
{
|
||||
yield return null;
|
||||
deferPrepareRoutine = null;
|
||||
EnsureOverlayHierarchy();
|
||||
hierarchyReady = overlayCanvas != null;
|
||||
|
||||
if (activeSettings != null && instanceMaterial != null)
|
||||
{
|
||||
PushViewportParams(activeSettings);
|
||||
SetBlinkOpen(blinkOpen);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureOverlayHierarchy()
|
||||
{
|
||||
if (overlayCanvas != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var canvasGo = new GameObject("EyeViewportOverlayCanvas");
|
||||
canvasGo.transform.SetParent(transform, false);
|
||||
|
||||
overlayCanvas = canvasGo.AddComponent<Canvas>();
|
||||
overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
overlayCanvas.sortingOrder = sortingOrder;
|
||||
overlayCanvas.pixelPerfect = false;
|
||||
|
||||
var scaler = canvasGo.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
|
||||
canvasGo.AddComponent<GraphicRaycaster>();
|
||||
|
||||
var imageGo = new GameObject("Overlay", typeof(RectTransform));
|
||||
imageGo.transform.SetParent(canvasGo.transform, false);
|
||||
|
||||
var rect = (RectTransform)imageGo.transform;
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
|
||||
overlayImage = imageGo.AddComponent<Image>();
|
||||
overlayImage.raycastTarget = false;
|
||||
overlayImage.color = Color.black;
|
||||
SetupMaterial();
|
||||
}
|
||||
|
||||
private void SetupMaterial()
|
||||
{
|
||||
Shader shader = Shader.Find("UI/EyeViewportOverlay");
|
||||
if (shader == null)
|
||||
{
|
||||
Debug.LogError("[EyeViewportOverlay] 找不到 UI/EyeViewportOverlay shader。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
instanceMaterial = new Material(shader)
|
||||
{
|
||||
name = "EyeViewportOverlay (Instance)",
|
||||
hideFlags = HideFlags.DontSave
|
||||
};
|
||||
overlayImage.material = instanceMaterial;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b95035b93d484ef4da0b45fcf2c2a380
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -58,6 +58,25 @@ namespace AibisDream
|
||||
public float roundWaveOutDuration = 0.5f;
|
||||
public float glitchOutDuration = 0.5f;
|
||||
|
||||
[Header("Viewport Overlay (no post-processing)")]
|
||||
public bool enableViewportOverlay = true;
|
||||
|
||||
[Tooltip("Horizontal half-size of the resting sight almond (left/right darkness).")]
|
||||
[Range(0.22f, 0.58f)] public float eyeWidth = 0.46f;
|
||||
|
||||
[Tooltip("Vertical half-size of the resting sight almond (top/bottom darkness).")]
|
||||
[Range(0.15f, 0.72f)] public float eyeHeight = 0.28f;
|
||||
|
||||
[Tooltip("Softness of the elliptical sight boundary.")]
|
||||
[Range(0.001f, 0.2f)] public float edgeSoftness = 0.022f;
|
||||
|
||||
[Tooltip("Off by default so you can tune the sight without blinking.")]
|
||||
public bool autoBlink;
|
||||
|
||||
public Vector2 autoBlinkInterval = new(8f, 15f);
|
||||
public float blinkCloseDuration = 0.09f;
|
||||
public float blinkOpenDuration = 0.14f;
|
||||
|
||||
public float GetDurationForState(EyeSystem.EyeColorState state) => state switch
|
||||
{
|
||||
EyeSystem.EyeColorState.CanImagineColor => imagineColorDuration,
|
||||
|
||||
Reference in New Issue
Block a user