From 43465e8f3483bc809b14af8723ac85a7e8b723b9 Mon Sep 17 00:00:00 2001
From: bottlefish <781230111@qq.com>
Date: Fri, 26 Jun 2026 23:59:10 +0800
Subject: [PATCH] feat: add memory tuning sprite shader
---
Assets/Materials/MemoryForFade.mat | 2 +-
.../Scripts/FixSystem/Memory/MemoryProcess.cs | 105 ++++++++++-
.../Scripts/FixSystem/Memory/MemorySlider.cs | 14 +-
.../Shader/Shader/MemoryTuningSprite.shader | 171 ++++++++++++++++++
.../Shader/MemoryTuningSprite.shader.meta | 10 +
Assets/Shader/Shader/NewMemory.mat | 49 +++++
Assets/Shader/Shader/NewMemory.mat.meta | 8 +
7 files changed, 356 insertions(+), 3 deletions(-)
create mode 100644 Assets/Shader/Shader/MemoryTuningSprite.shader
create mode 100644 Assets/Shader/Shader/MemoryTuningSprite.shader.meta
create mode 100644 Assets/Shader/Shader/NewMemory.mat
create mode 100644 Assets/Shader/Shader/NewMemory.mat.meta
diff --git a/Assets/Materials/MemoryForFade.mat b/Assets/Materials/MemoryForFade.mat
index 0659b3043..fefccf1e9 100644
--- a/Assets/Materials/MemoryForFade.mat
+++ b/Assets/Materials/MemoryForFade.mat
@@ -8,7 +8,7 @@ Material:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: MemoryForFade
- m_Shader: {fileID: 4800000, guid: 0803c241834332942abfcbd27c498b46, type: 3}
+ m_Shader: {fileID: 4800000, guid: 9f7b2ef66c2842c496327822f79da521, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
diff --git a/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs b/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs
index b4653824a..e84970960 100644
--- a/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs
+++ b/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs
@@ -17,6 +17,23 @@ 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 MaxFrequencyBlur = 0.6f;
+ private const float MinPixelDensity = 18f;
+ private const float MaxPixelDensity = 96f;
+
+ private static readonly int EnablePixelateId = Shader.PropertyToID("_EnablePixelate");
+ private static readonly int FrequencyTuningId = Shader.PropertyToID("_FrequencyTuning");
+ private static readonly int ClarityTuningId = Shader.PropertyToID("_ClarityTuning");
+ private static readonly int PixelateFadeId = Shader.PropertyToID("_PixelateFade");
+ private static readonly int PixelatePixelDensityId = Shader.PropertyToID("_PixelatePixelDensity");
+ private static readonly int EnableGaussianBlurId = Shader.PropertyToID("_EnableGaussianBlur");
+ private static readonly int GaussianBlurFadeId = Shader.PropertyToID("_GaussianBlurFade");
+ private static readonly int GaussianBlurOffsetId = Shader.PropertyToID("_GaussianBlurOffset");
+ private static readonly int EnableUvDistortId = Shader.PropertyToID("_EnableUVDistort");
+ private static readonly int UvDistortFadeId = Shader.PropertyToID("_UVDistortFade");
+ private static readonly int EnableSaturationId = Shader.PropertyToID("_EnableSaturation");
+ private static readonly int SaturationId = Shader.PropertyToID("_Saturation");
[SerializeField] private BubbleSlotGroup bubbleSlotGroup;
public BubbleSlotGroup BubbleSlotGroup => bubbleSlotGroup;
@@ -31,6 +48,7 @@ namespace AibisDream.FixSystem
[SerializeField] private Volume memoryProcessVolume;
[SerializeField] private MemorySlider frequencySlider; // 频率调节滑动条
[SerializeField] private MemorySlider claritySlider; // 清晰度调节滑动条
+ [SerializeField] private bool useSpriteShaderTuning = true;
[SerializeField] private CanvasGroup playBackground;
@@ -45,6 +63,7 @@ namespace AibisDream.FixSystem
[SerializeField] private MemoryPunchTapeSlot punchTapeSlot;
[SerializeField] private Material fadeMaterial;
+ private Material runtimeMemoryMaterial;
public void OpenMemoryView()
{
@@ -147,11 +166,18 @@ namespace AibisDream.FixSystem
private void Start()
{
+ PrepareRuntimeMemoryMaterial();
frequencySlider.OnFound += CheckAllFound;
claritySlider.OnFound += CheckAllFound;
Init();
}
+ private void OnDestroy()
+ {
+ if (runtimeMemoryMaterial != null)
+ Destroy(runtimeMemoryMaterial);
+ }
+
private void CheckAllFound()
{
if (frequencySlider.IsFound && claritySlider.IsFound)
@@ -206,9 +232,19 @@ namespace AibisDream.FixSystem
}
if (frequencySlider != null)
+ {
+ frequencySlider.OnTuningChanged -= ApplyFrequencyTuning;
frequencySlider.Init(memoryFrequencyVolume);
+ frequencySlider.SetVolumeOutputEnabled(!useSpriteShaderTuning);
+ frequencySlider.OnTuningChanged += ApplyFrequencyTuning;
+ }
if (claritySlider != null)
+ {
+ claritySlider.OnTuningChanged -= ApplyClarityTuning;
claritySlider.Init(memoryClarityVolume);
+ claritySlider.SetVolumeOutputEnabled(!useSpriteShaderTuning);
+ claritySlider.OnTuningChanged += ApplyClarityTuning;
+ }
ResetPresentationState();
}
@@ -239,6 +275,7 @@ namespace AibisDream.FixSystem
{
if (fadeMaterial != null)
fadeMaterial.SetFloat("_DirectionalDistortionFade", FadeMaterialStartValue);
+ ResetSpriteTuning();
memoryFinishedSpriteRenderer.color = Color.black;
memoryProcessVolume.weight = 0;
_activePunchTape = null;
@@ -377,10 +414,76 @@ namespace AibisDream.FixSystem
claritySlider.SetInteractable(true);
}
+ private void ApplyFrequencyTuning(float strength)
+ {
+ if (!useSpriteShaderTuning || fadeMaterial == null)
+ return;
+
+ bool active = strength > 0.001f;
+ SetFloatIfExists(FrequencyTuningId, strength);
+ SetFloatIfExists(EnablePixelateId, active ? 1f : 0f);
+ SetFloatIfExists(PixelateFadeId, strength);
+ SetFloatIfExists(PixelatePixelDensityId, Mathf.Lerp(MaxPixelDensity, MinPixelDensity, strength));
+ SetFloatIfExists(EnableGaussianBlurId, active ? 1f : 0f);
+ SetFloatIfExists(GaussianBlurFadeId, strength * MaxFrequencyBlur);
+ SetFloatIfExists(GaussianBlurOffsetId, Mathf.Lerp(0.05f, 0.35f, strength));
+ }
+
+ private void ApplyClarityTuning(float strength)
+ {
+ if (!useSpriteShaderTuning || fadeMaterial == null)
+ return;
+
+ bool active = strength > 0.001f;
+ SetFloatIfExists(ClarityTuningId, strength);
+ SetFloatIfExists(EnableUvDistortId, active ? 1f : 0f);
+ SetFloatIfExists(UvDistortFadeId, strength * MaxClarityDistortion);
+ SetFloatIfExists(EnableSaturationId, active ? 1f : 0f);
+ SetFloatIfExists(SaturationId, Mathf.Lerp(1f, 0.25f, strength));
+ }
+
+ private void ResetSpriteTuning()
+ {
+ if (!useSpriteShaderTuning || fadeMaterial == null)
+ return;
+
+ ApplyFrequencyTuning(0f);
+ ApplyClarityTuning(0f);
+ }
+
+ private void SetFloatIfExists(int propertyId, float value)
+ {
+ if (fadeMaterial.HasProperty(propertyId))
+ fadeMaterial.SetFloat(propertyId, value);
+ }
+
+ private void PrepareRuntimeMemoryMaterial()
+ {
+ if (!useSpriteShaderTuning || memoryFinishedSpriteRenderer == null || fadeMaterial == null)
+ return;
+
+ var shader = fadeMaterial.shader;
+ if (shader == null || shader.name != "AibisDream/MemoryTuningSprite")
+ shader = Shader.Find("AibisDream/MemoryTuningSprite");
+ if (shader == null)
+ {
+ Debug.LogWarning("[MemoryProcess] 未找到 AibisDream/MemoryTuningSprite,调频将回退到当前材质表现。", this);
+ return;
+ }
+
+ runtimeMemoryMaterial = new Material(fadeMaterial)
+ {
+ shader = shader,
+ name = $"{fadeMaterial.name}_RuntimeMemoryTuning"
+ };
+ memoryFinishedSpriteRenderer.sharedMaterial = runtimeMemoryMaterial;
+ fadeMaterial = runtimeMemoryMaterial;
+ }
+
public bool IsProcessing()
{
return isProcessing;
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Scripts/FixSystem/Memory/MemorySlider.cs b/Assets/Scripts/FixSystem/Memory/MemorySlider.cs
index 961892632..039c4872f 100644
--- a/Assets/Scripts/FixSystem/Memory/MemorySlider.cs
+++ b/Assets/Scripts/FixSystem/Memory/MemorySlider.cs
@@ -19,6 +19,7 @@ namespace AibisDream.FixSystem
private Slider slider;
private PlayableDirector playableDirector;
private Volume _volume;
+ private bool driveVolume = true;
/// 视觉 Lock/Unlock(Timeline);true = 收起未开,false = 已打开。
private bool isLocked;
@@ -28,6 +29,7 @@ namespace AibisDream.FixSystem
public event Action OnFound;
public event Action OnLost;
+ public event Action OnTuningChanged;
public void Init(Volume volume)
{
@@ -35,6 +37,7 @@ namespace AibisDream.FixSystem
playableDirector = GetComponent();
_volume = volume;
+ driveVolume = true;
IsFound = false;
slider.onValueChanged.RemoveAllListeners();
slider.onValueChanged.AddListener(HandleValueChanged);
@@ -49,6 +52,13 @@ namespace AibisDream.FixSystem
/// 仅控制是否可拖动,与 / 无关。
public void SetInteractable(bool interactable) => slider.interactable = interactable;
+ public void SetVolumeOutputEnabled(bool enabled)
+ {
+ driveVolume = enabled;
+ if (!driveVolume && _volume != null)
+ _volume.weight = 0f;
+ }
+
///
/// 新一轮搜索前复位滑条与 IsFound(不播 Timeline、不改变视觉锁状态)。
/// 需先设置好 。
@@ -116,9 +126,11 @@ namespace AibisDream.FixSystem
);
float t = Mathf.Clamp01(distance / maxDistance);
- if (_volume != null)
+ if (_volume != null && driveVolume)
_volume.weight = Mathf.Lerp(0, 1, t);
+ OnTuningChanged?.Invoke(t);
+
return t;
}
diff --git a/Assets/Shader/Shader/MemoryTuningSprite.shader b/Assets/Shader/Shader/MemoryTuningSprite.shader
new file mode 100644
index 000000000..f56aa1ece
--- /dev/null
+++ b/Assets/Shader/Shader/MemoryTuningSprite.shader
@@ -0,0 +1,171 @@
+Shader "AibisDream/MemoryTuningSprite"
+{
+ Properties
+ {
+ [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
+ _Color ("Tint", Color) = (1,1,1,1)
+ _RendererColor ("Renderer Color", Color) = (1,1,1,1)
+ _ClarityTuning ("Clarity Tuning", Range(0,1)) = 0
+ _FrequencyTuning ("Frequency Tuning", Range(0,1)) = 0
+ _DirectionalDistortionFade ("Directional Distortion Fade", Range(-15,1)) = -15
+ _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
+ _ChromaticStrength ("Chromatic Strength", Range(0,0.03)) = 0.008
+ _FadeDistortStrength ("Fade Distort Strength", Range(0,0.2)) = 0.08
+ }
+
+ SubShader
+ {
+ Tags
+ {
+ "Queue" = "Transparent"
+ "RenderType" = "Transparent"
+ "RenderPipeline" = "UniversalPipeline"
+ "IgnoreProjector" = "True"
+ "CanUseSpriteAtlas" = "True"
+ }
+
+ Cull Off
+ Lighting Off
+ ZWrite Off
+ Blend SrcAlpha OneMinusSrcAlpha
+
+ Pass
+ {
+ HLSLPROGRAM
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma multi_compile_instancing
+
+ #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
+
+ struct Attributes
+ {
+ float4 positionOS : POSITION;
+ float4 color : COLOR;
+ float2 uv : TEXCOORD0;
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+ };
+
+ struct Varyings
+ {
+ float4 positionHCS : SV_POSITION;
+ half4 color : COLOR;
+ float2 uv : TEXCOORD0;
+ UNITY_VERTEX_OUTPUT_STEREO
+ };
+
+ TEXTURE2D(_MainTex);
+ SAMPLER(sampler_MainTex);
+ float4 _MainTex_TexelSize;
+
+ CBUFFER_START(UnityPerMaterial)
+ half4 _Color;
+ half4 _RendererColor;
+ half _ClarityTuning;
+ half _FrequencyTuning;
+ half _DirectionalDistortionFade;
+ half _PixelMin;
+ half _PixelMax;
+ half _BlurStep;
+ half _DistortStrength;
+ half _ChromaticStrength;
+ half _FadeDistortStrength;
+ CBUFFER_END
+
+ Varyings vert(Attributes input)
+ {
+ Varyings output;
+ UNITY_SETUP_INSTANCE_ID(input);
+ UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
+
+ output.positionHCS = TransformObjectToHClip(input.positionOS.xyz);
+ output.uv = input.uv;
+ output.color = input.color * _Color * _RendererColor;
+ return output;
+ }
+
+ half4 SampleSprite(float2 uv)
+ {
+ return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, saturate(uv));
+ }
+
+ float2 PixelSnap(float2 uv, half strength)
+ {
+ half pixels = lerp(_PixelMax, _PixelMin, strength);
+ float2 aspectPixels = float2(pixels, max(1.0, pixels * _MainTex_TexelSize.z / _MainTex_TexelSize.w));
+ return (floor(uv * aspectPixels) + 0.5) / aspectPixels;
+ }
+
+ float2 DistortUv(float2 uv, half strength)
+ {
+ float angle = radians(40.0);
+ float2 dir = float2(cos(angle), sin(angle));
+ float2 side = float2(-sin(angle), cos(angle));
+ float wave = sin(dot(uv - 0.5, dir) * 85.0 + _Time.y * 1.35);
+ return uv + side * wave * _DistortStrength * strength;
+ }
+
+ half4 BlurSample(float2 uv, half strength)
+ {
+ float radial = distance(uv, 0.5) * 0.45;
+ float stepSize = _BlurStep * strength * (1.0 + radial);
+ float2 stepX = float2(stepSize, 0);
+ float2 stepY = float2(0, stepSize);
+
+ half4 col = SampleSprite(uv) * 0.36;
+ col += SampleSprite(uv + stepX) * 0.12;
+ col += SampleSprite(uv - stepX) * 0.12;
+ col += SampleSprite(uv + stepY) * 0.12;
+ col += SampleSprite(uv - stepY) * 0.12;
+ col += SampleSprite(uv + stepX + stepY) * 0.05;
+ col += SampleSprite(uv - stepX - stepY) * 0.05;
+ col += SampleSprite(uv + stepX - stepY) * 0.03;
+ col += SampleSprite(uv - stepX + stepY) * 0.03;
+ return col;
+ }
+
+ half3 ApplySaturation(half3 color, half saturation)
+ {
+ half luma = dot(color, half3(0.299, 0.587, 0.114));
+ return lerp(half3(luma, luma, luma), color, saturation);
+ }
+
+ half4 frag(Varyings input) : SV_Target
+ {
+ half clarity = saturate(_ClarityTuning);
+ half frequency = saturate(_FrequencyTuning);
+
+ float2 uv = input.uv;
+ half fade = saturate((_DirectionalDistortionFade + 15.0) / 15.0);
+ if (fade > 0.001)
+ {
+ float fadeWave = sin((uv.y + uv.x * 0.25 + _Time.y * 0.4) * 38.0);
+ uv.x += fadeWave * _FadeDistortStrength * fade;
+ }
+
+ uv = DistortUv(uv, clarity);
+ float2 pixelUv = PixelSnap(uv, frequency);
+ uv = lerp(uv, pixelUv, frequency);
+
+ half4 baseCol = lerp(SampleSprite(uv), BlurSample(uv, frequency), frequency * 0.65);
+
+ half chroma = _ChromaticStrength * clarity;
+ float radial = pow(distance(uv, 0.5), 3.0) * 3.0;
+ float2 chromaOffset = float2(chroma + radial * chroma, chroma * 0.35);
+ half4 r = SampleSprite(uv + chromaOffset);
+ half4 g = baseCol;
+ half4 b = SampleSprite(uv - chromaOffset.yx);
+ half4 chromaCol = half4(r.r, g.g, b.b, max(max(r.a, g.a), b.a));
+
+ half4 col = lerp(baseCol, chromaCol, clarity);
+ col.rgb = ApplySaturation(col.rgb, lerp(1.0, 0.28, clarity));
+ col *= input.color;
+ return col;
+ }
+ ENDHLSL
+ }
+ }
+}
diff --git a/Assets/Shader/Shader/MemoryTuningSprite.shader.meta b/Assets/Shader/Shader/MemoryTuningSprite.shader.meta
new file mode 100644
index 000000000..fe4948d7d
--- /dev/null
+++ b/Assets/Shader/Shader/MemoryTuningSprite.shader.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 9f7b2ef66c2842c496327822f79da521
+ShaderImporter:
+ externalObjects: {}
+ defaultTextures: []
+ nonModifiableTextures: []
+ preprocessorOverride: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Shader/Shader/NewMemory.mat b/Assets/Shader/Shader/NewMemory.mat
new file mode 100644
index 000000000..d9c8fdaf2
--- /dev/null
+++ b/Assets/Shader/Shader/NewMemory.mat
@@ -0,0 +1,49 @@
+%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: NewMemory
+ m_Shader: {fileID: 4800000, guid: e260cfa7296ee7642b167f1eb5be5023, 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:
+ - _AlphaTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MaskTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _NormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _EnableExternalAlpha: 0
+ m_Colors:
+ - _Color: {r: 1, g: 1, b: 1, a: 1}
+ - _Flip: {r: 1, g: 1, b: 1, a: 1}
+ - _RendererColor: {r: 1, g: 1, b: 1, a: 1}
+ m_BuildTextureStacks: []
diff --git a/Assets/Shader/Shader/NewMemory.mat.meta b/Assets/Shader/Shader/NewMemory.mat.meta
new file mode 100644
index 000000000..13cd4bb7e
--- /dev/null
+++ b/Assets/Shader/Shader/NewMemory.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7f4fa9a087a439b4d9c1939e6812b7e5
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant: