feat(dream): 完善梦境门与星空聚焦演出

This commit is contained in:
2026-07-16 17:52:34 +08:00
parent 0039beb5f9
commit ea95111f2a
18 changed files with 951 additions and 54 deletions
+1
View File
@@ -75,6 +75,7 @@
*.psd filter=lfs diff=lfs merge=lfs -text
*.psb filter=lfs diff=lfs merge=lfs -text
*.bank filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
# DOTween editor images - exclude from LFS (stored as regular binaries, Unity re-import causes false changes)
Assets/Plugins/Demigiant/DOTween/Editor/Imgs/*.png -filter -diff -merge
@@ -0,0 +1,43 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: DreamSeaMemory
m_Shader: {fileID: 4800000, guid: a8d86af0aff94e1794fa50e65ac676d5, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _VideoTex:
m_Texture: {fileID: 8400000, guid: d873791053c443c1a79eedd22a687780, type: 2}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Contrast: 1
- _DitherStrength: 0.002
- _Levels: 10
- _UseVideo: 0
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _PixelGrid: {r: 360, g: 640, b: 0, a: 0}
m_BuildTextureStacks: []
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e04544198a7d4f88ba592bf1feebbca2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!84 &8400000
RenderTexture:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: DreamSeaMemory
m_ImageContentsHash:
serializedVersion: 2
Hash: 00000000000000000000000000000000
m_ForcedFallbackFormat: 4
m_DownscaleFallback: 0
m_IsAlphaChannelOptional: 0
serializedVersion: 5
m_Width: 720
m_Height: 1280
m_AntiAliasing: 1
m_MipCount: -1
m_DepthStencilFormat: 0
m_ColorFormat: 4
m_MipMap: 0
m_GenerateMips: 0
m_SRGB: 1
m_UseDynamicScale: 0
m_BindMS: 0
m_EnableCompatibleFormat: 1
m_EnableRandomWrite: 0
m_TextureSettings:
serializedVersion: 2
m_FilterMode: 1
m_Aniso: 0
m_MipBias: 0
m_WrapU: 1
m_WrapV: 1
m_WrapW: 1
m_Dimension: 2
m_VolumeDepth: 1
m_ShadowSamplingMode: 2
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d873791053c443c1a79eedd22a687780
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 8400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,120 @@
Shader "AibisDream/DreamSeaMemory"
{
Properties
{
[PerRendererData] _MainTex ("Poster Texture", 2D) = "white" {}
_VideoTex ("Video Texture", 2D) = "black" {}
_UseVideo ("Use Prepared Video", Range(0, 1)) = 0
_Color ("Tint", Color) = (1,1,1,1)
_PixelGrid ("Virtual Pixel Grid", Vector) = (240,426,0,0)
_Contrast ("Contrast", Range(0.5, 2)) = 1
_Levels ("Luminance Levels", Range(2, 16)) = 10
_DitherStrength ("Dither Strength", Range(0, 0.1)) = 0.002
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}
Cull Off
Lighting Off
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
sampler2D _VideoTex;
fixed4 _Color;
float _UseVideo;
float4 _PixelGrid;
float _Contrast;
float _Levels;
float _DitherStrength;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.color = v.color * _Color;
return o;
}
float Bayer4x4(float2 pixel)
{
int x = ((int)pixel.x) & 3;
int y = ((int)pixel.y) & 3;
int index = x + y * 4;
if (index == 0) return 0.0 / 16.0;
if (index == 1) return 8.0 / 16.0;
if (index == 2) return 2.0 / 16.0;
if (index == 3) return 10.0 / 16.0;
if (index == 4) return 12.0 / 16.0;
if (index == 5) return 4.0 / 16.0;
if (index == 6) return 14.0 / 16.0;
if (index == 7) return 6.0 / 16.0;
if (index == 8) return 3.0 / 16.0;
if (index == 9) return 11.0 / 16.0;
if (index == 10) return 1.0 / 16.0;
if (index == 11) return 9.0 / 16.0;
if (index == 12) return 15.0 / 16.0;
if (index == 13) return 7.0 / 16.0;
if (index == 14) return 13.0 / 16.0;
return 5.0 / 16.0;
}
fixed4 frag(v2f i) : SV_Target
{
float2 grid = max(_PixelGrid.xy, float2(1.0, 1.0));
float2 pixel = floor(i.uv * grid);
float2 sampleUv = (pixel + 0.5) / grid;
fixed4 poster = tex2D(_MainTex, sampleUv);
fixed4 video = tex2D(_VideoTex, sampleUv);
float3 source = lerp(poster.rgb, video.rgb, saturate(_UseVideo));
float luminance = dot(source, float3(0.299, 0.587, 0.114));
luminance = saturate((luminance - 0.5) * _Contrast + 0.5);
luminance = saturate(luminance + (Bayer4x4(pixel) - 0.5) * _DitherStrength);
float levels = max(2.0, floor(_Levels + 0.5));
luminance = floor(luminance * (levels - 1.0) + 0.5) / (levels - 1.0);
return fixed4(luminance.xxx * i.color.rgb, poster.a * i.color.a);
}
ENDCG
}
}
Fallback "Sprites/Default"
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: a8d86af0aff94e1794fa50e65ac676d5
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
fileFormatVersion: 2
guid: 9ed84eb452114d46b39fa43322d186ec
VideoClipImporter:
externalObjects: {}
serializedVersion: 2
frameRange: 0
startFrame: -1
endFrame: -1
colorSpace: 0
deinterlace: 0
encodeAlpha: 0
flipVertical: 0
flipHorizontal: 0
importAudio: 0
targetSettings:
0:
enableTranscoding: 0
codec: 0
resizeFormat: 0
aspectRatio: 0
customWidth: 1
customHeight: 1
bitrateMode: 2
spatialQuality: 2
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,114 @@
fileFormatVersion: 2
guid: 0d38a05e2cc64f5bb1958d3bdf78925d
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 0
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
+41
View File
@@ -8,6 +8,7 @@ Shader "AIBIS/KiteFocusSprite"
[Header(Focus Blur)]
_BlurAmount ("Blur Amount", Range(0, 1)) = 0
_BlurSize ("Blur Size", Range(0, 0.05)) = 0.012
[HideInInspector] _UseSoftFocus ("Use Soft Focus", Float) = 0
_DefocusBloom ("Defocus Highlight Bloom", Range(0, 1)) = 0.32
_HaloStrength ("Large Circle Halo", Range(0, 0.6)) = 0.22
_ChromaticFringe ("Lens Color Fringe", Range(0, 0.6)) = 0.14
@@ -75,6 +76,7 @@ Shader "AIBIS/KiteFocusSprite"
fixed4 _Color;
float _BlurAmount;
float _BlurSize;
float _UseSoftFocus;
float _DefocusBloom;
float _HaloStrength;
float _ChromaticFringe;
@@ -150,6 +152,33 @@ Shader "AIBIS/KiteFocusSprite"
return result * 0.125;
}
half4 SoftFocusBlur(float2 uv, float radius)
{
float nearRadius = radius * 0.48;
half4 result = samplePremultiplied(uv) * 0.20;
result += samplePremultiplied(uv + float2( nearRadius, 0)) * 0.08;
result += samplePremultiplied(uv + float2(-nearRadius, 0)) * 0.08;
result += samplePremultiplied(uv + float2(0, nearRadius)) * 0.08;
result += samplePremultiplied(uv + float2(0, -nearRadius)) * 0.08;
result += samplePremultiplied(uv + float2( nearRadius, nearRadius)) * 0.055;
result += samplePremultiplied(uv + float2(-nearRadius, nearRadius)) * 0.055;
result += samplePremultiplied(uv + float2( nearRadius, -nearRadius)) * 0.055;
result += samplePremultiplied(uv + float2(-nearRadius, -nearRadius)) * 0.055;
result += samplePremultiplied(uv + float2( radius, 0)) * 0.04;
result += samplePremultiplied(uv + float2(-radius, 0)) * 0.04;
result += samplePremultiplied(uv + float2(0, radius)) * 0.04;
result += samplePremultiplied(uv + float2(0, -radius)) * 0.04;
result += samplePremultiplied(uv + float2( radius, radius)) * 0.025;
result += samplePremultiplied(uv + float2(-radius, radius)) * 0.025;
result += samplePremultiplied(uv + float2( radius, -radius)) * 0.025;
result += samplePremultiplied(uv + float2(-radius, -radius)) * 0.025;
return result;
}
fixed4 frag(v2f i) : SV_Target
{
float blurAmt = saturate(_BlurAmount);
@@ -164,6 +193,18 @@ Shader "AIBIS/KiteFocusSprite"
float2 texel = max(abs(_MainTex_TexelSize.xy), float2(1e-5, 1e-5));
float radius = (length(texel) * 0.5 + _BlurSize) * (0.28 + blurAmt * 1.72);
if (_UseSoftFocus > 0.5)
{
half4 softBlurred = SoftFocusBlur(i.uv, radius);
half4 softSharp = sharp;
softSharp.rgb *= softSharp.a;
half4 softColor = lerp(softSharp, softBlurred, smoothstep(0.0, 1.0, blurAmt));
softColor.rgb *= i.color.rgb * i.color.a;
softColor.a *= i.color.a;
return softColor;
}
half4 disc = ApertureDisc(i.uv, radius);
half4 halo = WideHalo(i.uv, radius * 1.85);
half4 blurred = lerp(disc, halo, saturate(_HaloStrength * blurAmt));
+39 -24
View File
@@ -421,20 +421,20 @@ MonoBehaviour:
m_EditorClassIdentifier:
director: {fileID: 132823524}
starfieldRenderer: {fileID: 1420961919}
initialBlur: 0.78
blurSize: 0.009
initialBlur: 0.58
blurSize: 0.0045
firstAttemptStart: 0.45
firstFocusDuration: 0.48
firstNearFocusBlur: 0.38
firstHoldDuration: 0.14
firstSlipDuration: 0.38
firstSlipBlur: 0.72
secondAttemptStart: 1.72
secondFocusDuration: 0.55
secondNearFocusBlur: 0.28
secondHoldDuration: 0.18
secondSlipDuration: 0.46
unresolvedBlur: 0.62
firstFocusDuration: 0.72
firstNearFocusBlur: 0.16
firstHoldDuration: 0.1
firstSlipDuration: 0.42
firstSlipBlur: 0.52
secondAttemptStart: 1.85
secondFocusDuration: 0.82
secondNearFocusBlur: 0.08
secondHoldDuration: 0.12
secondSlipDuration: 0.5
unresolvedBlur: 0.44
blurFadeStart: 4
blurFadeDuration: 3.2
--- !u!1 &218986882
@@ -663,6 +663,9 @@ MonoBehaviour:
frameRenderers:
- {fileID: 1847534534}
sceneBlackOverlay: {fileID: 1373573734}
glassSeaVideoClip: {fileID: 32900000, guid: 9ed84eb452114d46b39fa43322d186ec, type: 3}
glassSeaTargetTexture: {fileID: 8400000, guid: d873791053c443c1a79eedd22a687780, type: 2}
glassSeaPrepareTimeout: 5
terminalUI: {fileID: 1195418928}
doorShellTransform: {fileID: 0}
terminalShapeReference: {fileID: 1847534534}
@@ -1510,15 +1513,27 @@ MonoBehaviour:
typewriterCharDelay: 0.025
bootSpeedMultiplier: 4
bootSequence:
- text: '>> SYSTEM: Reality_Engine.sys stopped.'
- text: 'root@sys:~$ dream.render --stop --session 0742'
style: 3
delayAfter: 0.2
- text: '>> SYSTEM: Rendering underlying code...'
delayAfter: 0.15
- text: '[ OK ] Dream renderer stopped.'
style: 0
delayAfter: 0.25
- text: '[ RAW ] Untitled_Greybox_0 exposed.'
style: 0
delayAfter: 0.35
- text: 'root@sys:~$ trace --tail /log/dream_0742.rec'
style: 3
delayAfter: 0.2
- text: '>> LOADING: Ocean_Simulation.sh ... OK'
delayAfter: 0.15
- text: '[0005] audio.play("sea_ambient", source=door.rear)'
style: 3
delayAfter: 0.8
delayAfter: 0.1
- text: '[0006] memory.project("glass_sea")'
style: 3
delayAfter: 0.1
- text: '[ WARN ] memory source unresolved; overflow detected.'
style: 1
delayAfter: 0.4
- text: "root@sys:~$ print_string(\"\u6D77\u662F\u5B58\u5728\u7684\u5417?\")"
style: 2
delayAfter: 0.5
@@ -1791,7 +1806,7 @@ SpriteRenderer:
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2}
- {fileID: 2100000, guid: 78265ded39f017d45a2afc117effab89, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
@@ -2602,7 +2617,7 @@ SpriteRenderer:
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 1583441590}
- {fileID: 2100000, guid: e04544198a7d4f88ba592bf1feebbca2, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
@@ -2623,7 +2638,7 @@ SpriteRenderer:
m_SortingLayerID: 844151739
m_SortingLayer: 1
m_SortingOrder: 5
m_Sprite: {fileID: 21300000, guid: 8e0b68f1e994fc24b80e5a480c61602b, type: 3}
m_Sprite: {fileID: 21300000, guid: 0d38a05e2cc64f5bb1958d3bdf78925d, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_FlipX: 0
m_FlipY: 0
@@ -2643,8 +2658,8 @@ Transform:
m_GameObject: {fileID: 1961593256904873106}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -0.07, y: -0.03, z: 0}
m_LocalScale: {x: 0.7074555, y: 0.62522215, z: 0.8706811}
m_LocalPosition: {x: 0.02, y: 0.03, z: 0}
m_LocalScale: {x: 0.636667, y: 0.615, z: 0.8706811}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 450371655}
+293 -11
View File
@@ -4,6 +4,7 @@ using AibisDream.UI;
using AibisDream.Utility;
using DG.Tweening;
using UnityEngine;
using UnityEngine.Video;
namespace AibisDream
{
@@ -22,6 +23,11 @@ namespace AibisDream
[SerializeField] private Renderer[] frameRenderers;
[SerializeField] private SpriteRenderer sceneBlackOverlay;
[Header("Door Sea Memory")]
[SerializeField] private VideoClip glassSeaVideoClip;
[SerializeField] private RenderTexture glassSeaTargetTexture;
[SerializeField, Min(0.5f)] private float glassSeaPrepareTimeout = 5f;
[Header("Terminal(挂在 Door 子节点上即可,无需 Canvas")]
[SerializeField] private DreamTerminalUI terminalUI;
@@ -56,6 +62,14 @@ namespace AibisDream
private Vector3 _terminalUiOriginalLocalPosition;
private Transform _doorShellTransform;
private Vector3 _doorShellOriginalScale;
private VideoPlayer _glassSeaVideoPlayer;
private Material _glassSeaMaterial;
private bool _glassSeaVideoPrepared;
private bool _glassSeaVideoFailed;
private bool _glassSeaFrameReady;
private DoorPresentationPhase _presentationPhase = DoorPresentationPhase.Hidden;
private static readonly int UseVideoId = Shader.PropertyToID("_UseVideo");
private enum FrameHidePhase
{
@@ -64,6 +78,15 @@ namespace AibisDream
OnTerminal
}
private enum DoorPresentationPhase
{
Hidden,
Door,
GlassSea,
CodeSea,
Terminal
}
public override void OnSingletonInit()
{
if (doorRoot != null)
@@ -75,10 +98,14 @@ namespace AibisDream
if (glassSea != null)
{
_glassSeaMaterial = glassSea.material;
SetGlassSeaVideoVisible(false);
glassSea.SetAlpha(0);
glassSea.gameObject.SetActive(false);
}
InitializeGlassSeaVideo();
if (codeSea != null)
{
codeSea.SetAlpha(0);
@@ -108,19 +135,33 @@ namespace AibisDream
_terminalUiOriginalScale = _terminalUiTransform.localScale;
_terminalUiOriginalLocalPosition = _terminalUiTransform.localPosition;
}
_presentationPhase = DoorPresentationPhase.Hidden;
}
public override void OnSingletonDestroy()
{
if (_glassSeaVideoPlayer != null)
{
_glassSeaVideoPlayer.prepareCompleted -= OnGlassSeaVideoPrepared;
_glassSeaVideoPlayer.frameReady -= OnGlassSeaVideoFrameReady;
_glassSeaVideoPlayer.errorReceived -= OnGlassSeaVideoError;
}
base.OnSingletonDestroy();
}
/// <summary>
/// 显示整个门(仅 background + FrameGlassSea/CodeSea 保持隐藏直到各自命令调用)
/// 显示普通门。该命令是一个完整状态切换,会清掉之前残留的海景、代码海浪和终端。
/// </summary>
public IEnumerator ShowDoor(float duration = 0f)
{
if (doorRoot != null)
doorRoot.gameObject.SetActive(true);
SetBackgroundVisible(true);
SetFrameVisible(true);
ResetDoorShellScale();
StopAllEffects();
EnsureDoorBaseVisible();
ResetGlassSeaImmediate(stopVideo: true);
ResetCodeSeaImmediate();
ResetTerminalImmediate();
_presentationPhase = DoorPresentationPhase.Door;
if (duration > 0f)
yield return new WaitForSeconds(duration);
@@ -139,18 +180,48 @@ namespace AibisDream
public IEnumerator ShowGlassSea(float duration = 2f)
{
if (glassSea == null)
yield break;
StopAllEffects();
EnsureDoorBaseVisible();
ResetCodeSeaImmediate();
ResetTerminalImmediate();
if (_presentationPhase != DoorPresentationPhase.GlassSea || !glassSea.gameObject.activeSelf)
glassSea.SetAlpha(0f);
glassSea.gameObject.SetActive(true);
yield return glassSea.DOFade(1f, duration)
_presentationPhase = DoorPresentationPhase.GlassSea;
yield return EnsureGlassSeaVideoPlaying();
yield return glassSea.DOFade(1f, Mathf.Max(0f, duration))
.SetEase(Ease.InOutSine)
.WaitForCompletion();
}
public IEnumerator HideGlassSea(float duration = 1f)
{
yield return glassSea.DOFade(0f, duration)
if (glassSea == null)
{
PauseGlassSeaVideo();
yield break;
}
DOTween.Kill(glassSea);
if (glassSea.gameObject.activeSelf)
{
yield return glassSea.DOFade(0f, Mathf.Max(0f, duration))
.SetEase(Ease.InOutSine)
.WaitForCompletion();
}
glassSea.SetAlpha(0f);
glassSea.gameObject.SetActive(false);
PauseGlassSeaVideo();
if (_presentationPhase == DoorPresentationPhase.GlassSea)
_presentationPhase = DoorPresentationPhase.Door;
}
#endregion
@@ -159,29 +230,48 @@ namespace AibisDream
public IEnumerator ShowCodeSea(float duration = 2f)
{
if (codeSea == null)
yield break;
StopAllEffects();
EnsureDoorBaseVisible();
ResetTerminalImmediate();
codeSea.color = _codeSeaOriginalColor;
if (_codeSeaTransform != null)
_codeSeaTransform.localScale = _codeSeaOriginalScale;
codeSea.SetAlpha(0f);
codeSea.gameObject.SetActive(true);
_presentationPhase = DoorPresentationPhase.CodeSea;
if (frameHidePhase == FrameHidePhase.OnCodeSea)
SetFrameVisible(false);
var seq = DOTween.Sequence();
seq.Append(codeSea.DOFade(1f, duration).SetEase(Ease.InOutSine));
seq.Append(codeSea.DOFade(1f, Mathf.Max(0f, duration)).SetEase(Ease.InOutSine));
if (glassSea != null && glassSea.gameObject.activeSelf)
{
seq.Join(glassSea.DOFade(0f, duration * 0.7f).SetEase(Ease.InSine));
seq.Join(glassSea.DOFade(0f, Mathf.Max(0f, duration) * 0.7f).SetEase(Ease.InSine));
}
yield return seq.WaitForCompletion();
if (glassSea != null)
{
glassSea.SetAlpha(0f);
glassSea.gameObject.SetActive(false);
}
StopGlassSeaVideo();
}
#endregion
#region Phase 3: CodeSea
public IEnumerator ShowTerminal()
{
_presentationPhase = DoorPresentationPhase.Terminal;
yield return AnimateDoorToTerminal();
if (terminalUI != null)
{
@@ -214,6 +304,8 @@ namespace AibisDream
if (glassSea != null)
glassSea.gameObject.SetActive(false);
StopGlassSeaVideo();
if (codeSea != null)
codeSea.gameObject.SetActive(false);
@@ -288,6 +380,7 @@ namespace AibisDream
public void StopAllEffects()
{
DOTween.Kill(glassSea);
DOTween.Kill(_codeSeaTransform);
DOTween.Kill(codeSea);
DOTween.Kill(_terminalUiTransform);
@@ -314,6 +407,8 @@ namespace AibisDream
glassSea.gameObject.SetActive(false);
}
StopGlassSeaVideo();
if (codeSea != null)
{
codeSea.color = _codeSeaOriginalColor;
@@ -340,6 +435,193 @@ namespace AibisDream
if (sceneBlackOverlay != null)
sceneBlackOverlay.SetAlpha(0);
_presentationPhase = DoorPresentationPhase.Hidden;
}
#endregion
#region Presentation State
private void EnsureDoorBaseVisible()
{
if (doorRoot != null)
doorRoot.gameObject.SetActive(true);
SetBackgroundVisible(true);
SetFrameVisible(true);
ResetDoorShellScale();
}
private void ResetGlassSeaImmediate(bool stopVideo)
{
DOTween.Kill(glassSea);
if (glassSea != null)
{
glassSea.SetAlpha(0f);
glassSea.gameObject.SetActive(false);
}
if (stopVideo)
StopGlassSeaVideo();
else
PauseGlassSeaVideo();
}
private void ResetCodeSeaImmediate()
{
DOTween.Kill(_codeSeaTransform);
DOTween.Kill(codeSea);
if (codeSea == null)
return;
codeSea.color = _codeSeaOriginalColor;
codeSea.SetAlpha(0f);
codeSea.gameObject.SetActive(false);
if (_codeSeaTransform != null)
_codeSeaTransform.localScale = _codeSeaOriginalScale;
}
private void ResetTerminalImmediate()
{
DOTween.Kill(_terminalUiTransform);
if (terminalUI != null)
{
terminalUI.Alpha = 0f;
terminalUI.ResetVisualState();
}
if (_terminalUiTransform != null)
{
_terminalUiTransform.localPosition = _terminalUiOriginalLocalPosition;
_terminalUiTransform.localScale = _terminalUiOriginalScale;
}
if (flashOverlay != null)
flashOverlay.SetAlpha(0f);
if (sceneBlackOverlay != null)
sceneBlackOverlay.SetAlpha(0f);
}
#endregion
#region GlassSea Video
private void InitializeGlassSeaVideo()
{
if (glassSeaVideoClip == null || glassSeaTargetTexture == null)
{
SetGlassSeaVideoVisible(false);
return;
}
_glassSeaVideoPlayer = GetComponent<VideoPlayer>();
if (_glassSeaVideoPlayer == null)
_glassSeaVideoPlayer = gameObject.AddComponent<VideoPlayer>();
_glassSeaVideoPlayer.playOnAwake = false;
_glassSeaVideoPlayer.source = VideoSource.VideoClip;
_glassSeaVideoPlayer.clip = glassSeaVideoClip;
_glassSeaVideoPlayer.renderMode = VideoRenderMode.RenderTexture;
_glassSeaVideoPlayer.targetTexture = glassSeaTargetTexture;
_glassSeaVideoPlayer.audioOutputMode = VideoAudioOutputMode.None;
_glassSeaVideoPlayer.isLooping = true;
_glassSeaVideoPlayer.skipOnDrop = true;
_glassSeaVideoPlayer.waitForFirstFrame = true;
_glassSeaVideoPlayer.sendFrameReadyEvents = true;
_glassSeaVideoPlayer.prepareCompleted += OnGlassSeaVideoPrepared;
_glassSeaVideoPlayer.frameReady += OnGlassSeaVideoFrameReady;
_glassSeaVideoPlayer.errorReceived += OnGlassSeaVideoError;
_glassSeaVideoPrepared = false;
_glassSeaVideoFailed = false;
_glassSeaFrameReady = false;
SetGlassSeaVideoVisible(false);
_glassSeaVideoPlayer.Prepare();
}
private IEnumerator EnsureGlassSeaVideoPlaying()
{
if (_glassSeaVideoPlayer == null || _glassSeaVideoFailed)
{
SetGlassSeaVideoVisible(false);
yield break;
}
if (!_glassSeaVideoPrepared && !_glassSeaVideoPlayer.isPrepared)
{
float deadline = Time.realtimeSinceStartup + glassSeaPrepareTimeout;
while (!_glassSeaVideoPrepared && !_glassSeaVideoFailed && Time.realtimeSinceStartup < deadline)
yield return null;
if (!_glassSeaVideoPrepared && !_glassSeaVideoPlayer.isPrepared)
{
_glassSeaVideoFailed = true;
_glassSeaVideoPlayer.Stop();
SetGlassSeaVideoVisible(false);
Debug.LogWarning("[DreamDoorSystem] 海景视频准备超时,回退到静态海报。", this);
yield break;
}
}
_glassSeaVideoPrepared = true;
if (!_glassSeaVideoPlayer.isPlaying)
_glassSeaVideoPlayer.Play();
if (!_glassSeaFrameReady)
{
float firstFrameDeadline = Time.realtimeSinceStartup + 1f;
while (!_glassSeaFrameReady && !_glassSeaVideoFailed && Time.realtimeSinceStartup < firstFrameDeadline)
yield return null;
}
}
private void PauseGlassSeaVideo()
{
if (_glassSeaVideoPlayer != null && _glassSeaVideoPlayer.isPlaying)
_glassSeaVideoPlayer.Pause();
}
private void StopGlassSeaVideo()
{
if (_glassSeaVideoPlayer != null)
_glassSeaVideoPlayer.Stop();
_glassSeaFrameReady = false;
SetGlassSeaVideoVisible(false);
}
private void OnGlassSeaVideoPrepared(VideoPlayer source)
{
_glassSeaVideoPrepared = true;
_glassSeaVideoFailed = false;
}
private void OnGlassSeaVideoFrameReady(VideoPlayer source, long frameIndex)
{
_glassSeaFrameReady = true;
SetGlassSeaVideoVisible(true);
}
private void OnGlassSeaVideoError(VideoPlayer source, string message)
{
_glassSeaVideoFailed = true;
_glassSeaVideoPrepared = false;
_glassSeaFrameReady = false;
SetGlassSeaVideoVisible(false);
Debug.LogWarning($"[DreamDoorSystem] 海景视频播放失败,回退到静态海报:{message}", this);
}
private void SetGlassSeaVideoVisible(bool visible)
{
if (_glassSeaMaterial != null && _glassSeaMaterial.HasProperty(UseVideoId))
_glassSeaMaterial.SetFloat(UseVideoId, visible ? 1f : 0f);
}
#endregion
@@ -1,5 +1,7 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Playables;
using Yarn.Unity;
namespace AibisDream
{
@@ -12,6 +14,7 @@ namespace AibisDream
{
private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount");
private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize");
private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus");
[Header("References")]
[SerializeField] private PlayableDirector director;
@@ -60,6 +63,114 @@ namespace AibisDream
private MaterialPropertyBlock propertyBlock;
[YarnCommand("starfield_prepare")]
public static void PrepareStarfieldCommand()
{
var controller = FindFirstObjectByType<StarfieldDefocusController>();
if (controller == null)
{
Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_prepare.");
return;
}
controller.PrepareStarfield();
}
[YarnCommand("starfield_focus")]
public static IEnumerator PlayFocusAttemptCommand(int attempt)
{
var controller = FindFirstObjectByType<StarfieldDefocusController>();
if (controller == null)
{
Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_focus.");
yield break;
}
yield return controller.PlayFocusAttempt(attempt);
}
[YarnCommand("starfield_descend")]
public static IEnumerator DescendToSeaCommand()
{
var controller = FindFirstObjectByType<StarfieldDefocusController>();
if (controller == null)
{
Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_descend.");
yield break;
}
yield return controller.DescendToSea();
}
private void PrepareStarfield()
{
if (!TryValidateReferences())
{
return;
}
director.Stop();
SetTimelineTime(0f);
}
private IEnumerator PlayFocusAttempt(int attempt)
{
if (!TryValidateReferences())
{
yield break;
}
float startTime;
float endTime;
switch (attempt)
{
case 1:
startTime = firstAttemptStart;
endTime = firstAttemptStart + firstFocusDuration + firstHoldDuration + firstSlipDuration;
break;
case 2:
startTime = secondAttemptStart;
endTime = secondAttemptStart + secondFocusDuration + secondHoldDuration + secondSlipDuration;
break;
default:
Debug.LogWarning($"[StarfieldDefocusController] Unsupported focus attempt '{attempt}'. Expected 1 or 2.", this);
yield break;
}
director.Pause();
SetTimelineTime(startTime);
float duration = Mathf.Max(0.01f, endTime - startTime);
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
SetTimelineTime(Mathf.Lerp(startTime, endTime, Mathf.Clamp01(elapsed / duration)));
yield return null;
}
SetTimelineTime(endTime);
}
private IEnumerator DescendToSea()
{
if (!TryValidateReferences())
{
yield break;
}
director.Pause();
SetTimelineTime(blurFadeStart);
director.Play();
while (director.state == PlayState.Playing)
{
yield return null;
}
ApplyAtTime((float)director.time);
}
private void Reset()
{
director = GetComponent<PlayableDirector>();
@@ -87,6 +198,45 @@ namespace AibisDream
ApplyAtTime((float)director.time);
}
private bool TryValidateReferences()
{
if (director == null)
{
Debug.LogWarning("[StarfieldDefocusController] PlayableDirector is not assigned.", this);
return false;
}
if (director.playableAsset == null)
{
Debug.LogWarning("[StarfieldDefocusController] PlayableDirector has no Timeline asset.", this);
return false;
}
if (starfieldRenderer == null)
{
Debug.LogWarning("[StarfieldDefocusController] Starfield SpriteRenderer is not assigned.", this);
return false;
}
var material = starfieldRenderer.sharedMaterial;
if (material == null || !material.HasProperty(BlurAmountId) || !material.HasProperty(BlurSizeId))
{
Debug.LogWarning(
"[StarfieldDefocusController] Starfield material must support _BlurAmount and _BlurSize.",
starfieldRenderer);
return false;
}
return true;
}
private void SetTimelineTime(float time)
{
director.time = Mathf.Clamp(time, 0f, (float)director.duration);
director.Evaluate();
ApplyAtTime((float)director.time);
}
private void ApplyAtTime(float time)
{
if (starfieldRenderer == null)
@@ -98,6 +248,7 @@ namespace AibisDream
starfieldRenderer.GetPropertyBlock(propertyBlock);
propertyBlock.SetFloat(BlurAmountId, EvaluateBlur(time));
propertyBlock.SetFloat(BlurSizeId, blurSize);
propertyBlock.SetFloat(UseSoftFocusId, 1f);
starfieldRenderer.SetPropertyBlock(propertyBlock);
}
@@ -118,7 +269,10 @@ namespace AibisDream
if (time < firstFocusEnd)
{
return SmoothLerp(initialBlur, firstNearFocusBlur, InverseLerp(firstAttemptStart, firstFocusEnd, time));
return EvaluateFocusTurn(
initialBlur,
firstNearFocusBlur,
InverseLerp(firstAttemptStart, firstFocusEnd, time));
}
if (time < firstHoldEnd)
@@ -138,7 +292,10 @@ namespace AibisDream
if (time < secondFocusEnd)
{
return SmoothLerp(firstSlipBlur, secondNearFocusBlur, InverseLerp(secondAttemptStart, secondFocusEnd, time));
return EvaluateFocusTurn(
firstSlipBlur,
secondNearFocusBlur,
InverseLerp(secondAttemptStart, secondFocusEnd, time));
}
if (time < secondHoldEnd)
@@ -159,6 +316,29 @@ namespace AibisDream
return SmoothLerp(unresolvedBlur, 0f, InverseLerp(blurFadeStart, blurFadeStart + blurFadeDuration, time));
}
/// <summary>
/// Simulates turning a focus ring toward focus, passing the focal plane,
/// then reversing back. The following slip phase still lets the image
/// drift out of focus, keeping both attempts deliberately unsuccessful.
/// </summary>
private static float EvaluateFocusTurn(float fromBlur, float nearFocusBlur, float t)
{
t = Mathf.Clamp01(t);
float overshootBlur = Mathf.Lerp(nearFocusBlur, fromBlur, 0.68f);
if (t < 0.44f)
{
return SmoothLerp(fromBlur, nearFocusBlur, t / 0.44f);
}
if (t < 0.72f)
{
return SmoothLerp(nearFocusBlur, overshootBlur, (t - 0.44f) / 0.28f);
}
return SmoothLerp(overshootBlur, nearFocusBlur, (t - 0.72f) / 0.28f);
}
private static float InverseLerp(float from, float to, float value)
{
return Mathf.Clamp01((value - from) / Mathf.Max(0.0001f, to - from));
@@ -98,14 +98,14 @@ namespace AibisDream.UI
[Header("启动日志")]
[SerializeField] private List<BootLogEntry> bootSequence = new()
{
new BootLogEntry { text = "root@sys:~$ ./boot --test", style = LogStyle.Dim, delayAfter = 0.2f },
new BootLogEntry { text = "[ OK ] Test passed.", style = LogStyle.Normal, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ render --target ocean && aplay sea.wav", style = LogStyle.Dim, delayAfter = 0.2f },
new BootLogEntry { text = "[ OK ] Ocean rendered. Audio stream started.", style = LogStyle.Normal, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ render --stop", style = LogStyle.Dim, delayAfter = 0.2f },
new BootLogEntry { text = "[ OK ] Render pipeline closed.", style = LogStyle.Normal, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ showcode", style = LogStyle.Alert, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ print_string(\"海是存在的吗?\")", style = LogStyle.Highlight, delayAfter = 0.2f },
new BootLogEntry { text = "root@sys:~$ dream.render --stop --session 0742", style = LogStyle.Dim, delayAfter = 0.15f },
new BootLogEntry { text = "[ OK ] Dream renderer stopped.", style = LogStyle.Normal, delayAfter = 0.25f },
new BootLogEntry { text = "[ RAW ] Untitled_Greybox_0 exposed.", style = LogStyle.Normal, delayAfter = 0.35f },
new BootLogEntry { text = "root@sys:~$ trace --tail /log/dream_0742.rec", style = LogStyle.Dim, delayAfter = 0.15f },
new BootLogEntry { text = "[0005] audio.play(\"sea_ambient\", source=door.rear)", style = LogStyle.Dim, delayAfter = 0.1f },
new BootLogEntry { text = "[0006] memory.project(\"glass_sea\")", style = LogStyle.Dim, delayAfter = 0.1f },
new BootLogEntry { text = "[ WARN ] memory source unresolved; overflow detected.", style = LogStyle.Alert, delayAfter = 0.4f },
new BootLogEntry { text = "root@sys:~$ print_string(\"海是存在的吗?\")", style = LogStyle.Highlight, delayAfter = 0.5f },
};
#endregion
@@ -345,7 +345,7 @@ title: 女孩_已完成
position: 743,-1403
tags: detour
---
//girl: <i>女孩一边哼歌一边继续涂画着,<br>没有听到你的声音。</i> #line:01ef1ed
girl: <i>女孩一边哼歌一边继续涂画着,<br>没有听到你的声音。</i> #line:01ef1ed
===
title: 天台
@@ -606,17 +606,17 @@ tags: linear
<<play_sfx "event:/Scriptal/sea_loop">>
<<set_global_param prologueDreamStage 1>>
【星空是模糊的,很难看清】
<<starfield_prepare>>
girl: 怎么还是看不清?
【调试镜头】
<<starfield_focus 1>>
jj: 这都漏雨了。它太旧了,应该很难修好了。
girl: 不好!肯定是还有什么地方没弄好…
【调试镜头】
<<starfield_focus 2>>
jj: 走吧,我们在这里也玩够了。
@@ -634,10 +634,9 @@ girl: 你刚才还不是这么说的。
me: 姐姐……
【镜头变得模糊,下摇】
<<starfield_descend>>
<<play_sfx "event:/Scriptal/sea_loop">>
<<play_timeline 星空到海浪>>
<<set_global_param dream1Stage 5>>
<<wait 2>>
@@ -822,6 +822,7 @@ position: -4,927
<<if $gameStage==5>>
<<show_large_sprite "全景门乌鸦">>
<<else>>
<<hide_dream_door>>
<<show_large_sprite "D2S门">>
<<endif>>
一扇紧闭的手术室门。让人感觉很熟悉。 #line:0ba4b43
@@ -886,6 +887,8 @@ position: -4,927
<<show_door_glass_sea 2>>
<<endif>>
一种莫名的亲切感。 #line:0dfe59e
<<hide_door_glass_sea 0.5>>
<<hide_dream_door>>
->靠近黑暗<<if $gameStage==5>> #line:02cb5f1
// 黑暗中闪烁着无数微弱的绿色光点。那是无数段代码。 #line:0dc92eb
// 其中一段正静静地漂浮在你的面前。 #line:07400da
@@ -981,6 +984,7 @@ me: 海… #line:00a7bf5
<<fade_in 0.5>>
<<show_large_sprite "门框特写">>
<<show_door 0.1>>
<<show_door_glass_sea 0>>
<<fade_out 0.5>>
<<wait 1>>
<<show_door_code_sea 2>>