From 814b659c395f854e1d4ab09928bd8b80661d5ee8 Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Mon, 29 Jun 2026 22:23:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(fix-system):=20=E8=BF=81=E7=A7=BB=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E8=B0=83=E9=A2=91=E5=88=B0=E7=B2=BE=E7=81=B5=E6=9D=90?= =?UTF-8?q?=E8=B4=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/FixSystem/Memory/MemoryProcess.cs | 96 ++++++++++--- .../Shader/Shader/MemoryTuningSprite.shader | 103 ++++++++------ Docs/MemoryProcessSpriteShaderNotes.md | 128 ++++++++++++++++++ 3 files changed, 271 insertions(+), 56 deletions(-) create mode 100644 Docs/MemoryProcessSpriteShaderNotes.md diff --git a/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs b/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs index 2159af7f7..8f89427cf 100644 --- a/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs +++ b/Assets/Scripts/FixSystem/Memory/MemoryProcess.cs @@ -23,10 +23,12 @@ namespace AibisDream.FixSystem private const float MaxFrequencyBlur = 0.6f; private const float MinPixelDensity = 18f; private const float MaxPixelDensity = 96f; + private const string DirectionalDistortionFadeProperty = "_DirectionalDistortionFade"; 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 DirectionalDistortionFadeId = Shader.PropertyToID(DirectionalDistortionFadeProperty); private static readonly int PixelateFadeId = Shader.PropertyToID("_PixelateFade"); private static readonly int PixelatePixelDensityId = Shader.PropertyToID("_PixelatePixelDensity"); private static readonly int EnableGaussianBlurId = Shader.PropertyToID("_EnableGaussianBlur"); @@ -34,8 +36,8 @@ namespace AibisDream.FixSystem 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"); + private static readonly int MemoryGrayscaleId = Shader.PropertyToID("_MemoryGrayscale"); + private static readonly int MemoryContrastId = Shader.PropertyToID("_MemoryContrast"); [SerializeField] private GameObject memoryView; [SerializeField] private GameObject projector; @@ -63,6 +65,7 @@ namespace AibisDream.FixSystem [SerializeField] private Material fadeMaterial; private Material runtimeMemoryMaterial; + private Material runtimeNoColorMemoryMaterial; private readonly List runtimeRenderTextures = new(); public void OpenMemoryView() @@ -175,6 +178,8 @@ namespace AibisDream.FixSystem { if (runtimeMemoryMaterial != null) Destroy(runtimeMemoryMaterial); + if (runtimeNoColorMemoryMaterial != null) + Destroy(runtimeNoColorMemoryMaterial); foreach (var renderTexture in runtimeRenderTextures) { @@ -323,6 +328,9 @@ namespace AibisDream.FixSystem public void TweenWeight(Volume postProcessingVolume, float targetWeight, float duration) { + if (postProcessingVolume == null) + return; + // 动态调整权重 DOTween.To(() => postProcessingVolume.weight, x => postProcessingVolume.weight = x, targetWeight, duration); } @@ -381,10 +389,10 @@ namespace AibisDream.FixSystem private void ResetPresentationState() { if (fadeMaterial != null) - fadeMaterial.SetFloat("_DirectionalDistortionFade", FadeMaterialStartValue); + SetFloatIfExists(DirectionalDistortionFadeId, FadeMaterialStartValue); ResetSpriteTuning(); memoryFinishedSpriteRenderer.color = Color.black; - memoryProcessVolume.weight = 0; + SetVolumeWeight(memoryProcessVolume, 0f); _activePunchTape = null; playBackground.gameObject.SetActive(false); @@ -414,8 +422,12 @@ namespace AibisDream.FixSystem { if (fadeMaterial == null) yield break; - fadeMaterial.SetFloat("_DirectionalDistortionFade", FadeMaterialStartValue); - Tween tween = fadeMaterial.DOFloat(0, "_DirectionalDistortionFade", 3); + SetFloatIfExists(DirectionalDistortionFadeId, FadeMaterialStartValue); + DG.Tweening.Sequence sequence = DOTween.Sequence(); + AppendMaterialFloatTween(sequence, fadeMaterial, 0f, DirectionalDistortionFadeProperty, 3f); + if (runtimeNoColorMemoryMaterial != null && runtimeNoColorMemoryMaterial != fadeMaterial) + AppendMaterialFloatTween(sequence, runtimeNoColorMemoryMaterial, 0f, DirectionalDistortionFadeProperty, 3f); + Tween tween = sequence; yield return tween.WaitForCompletion(); } @@ -428,7 +440,7 @@ namespace AibisDream.FixSystem if (useYarn) { if (fadeMaterial != null) - fadeMaterial.SetFloat("_DirectionalDistortionFade", FadeMaterialStartValue); + SetFloatIfExists(DirectionalDistortionFadeId, FadeMaterialStartValue); var memoryKey = string.Format(MemorySpritePath, memName); var memoryHandle = ResourceSystem.LoadAsync(memoryKey); yield return memoryHandle; @@ -501,14 +513,14 @@ namespace AibisDream.FixSystem Debug.LogError("Memory not found: " + searchMemoryKey); } - memoryProcessVolume.weight = 1; + SetVolumeWeight(memoryProcessVolume, 1f); AudioManager.Instance.PlaySfx("event:/Scriptal/mem_load"); yield return new WaitForSeconds(SearchStartDelay); AudioManager.Instance.SetSfxParam("event:/Amb/amb_mem_tuning", "is_mem_tuning", 1); memoryFinishedSpriteRenderer.color = Color.white; - memoryProcessVolume.weight = 0; + SetVolumeWeight(memoryProcessVolume, 0f); isProcessing = true; frequencySlider.PrepareForSearch(); claritySlider.PrepareForSearch(); @@ -545,8 +557,6 @@ namespace AibisDream.FixSystem 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() @@ -560,8 +570,32 @@ namespace AibisDream.FixSystem private void SetFloatIfExists(int propertyId, float value) { - if (fadeMaterial.HasProperty(propertyId)) - fadeMaterial.SetFloat(propertyId, value); + SetFloatIfExists(fadeMaterial, propertyId, value); + if (runtimeNoColorMemoryMaterial != null && runtimeNoColorMemoryMaterial != fadeMaterial) + SetFloatIfExists(runtimeNoColorMemoryMaterial, propertyId, value); + } + + private static void SetVolumeWeight(Volume volume, float weight) + { + if (volume != null) + volume.weight = weight; + } + + private static void SetFloatIfExists(Material material, int propertyId, float value) + { + if (material != null && material.HasProperty(propertyId)) + material.SetFloat(propertyId, value); + } + + private static void AppendMaterialFloatTween( + DG.Tweening.Sequence sequence, + Material material, + float target, + string property, + float duration) + { + if (sequence != null && material != null && material.HasProperty(property)) + sequence.Join(material.DOFloat(target, property, duration)); } private void PrepareRuntimeMemoryMaterial() @@ -578,13 +612,39 @@ namespace AibisDream.FixSystem return; } - runtimeMemoryMaterial = new Material(fadeMaterial) - { - shader = shader, - name = $"{fadeMaterial.name}_RuntimeMemoryTuning" - }; + runtimeMemoryMaterial = CreateRuntimeMemoryMaterial(fadeMaterial, shader, "Color", 0f, 1f); memoryFinishedSpriteRenderer.sharedMaterial = runtimeMemoryMaterial; fadeMaterial = runtimeMemoryMaterial; + + if (memoryFinishedImageNoColor != null && memoryFinishedImageNoColor.sharedMaterial != null) + { + runtimeNoColorMemoryMaterial = CreateRuntimeMemoryMaterial( + memoryFinishedImageNoColor.sharedMaterial, + shader, + "NoColor", + 1f, + 1.8f); + memoryFinishedImageNoColor.sharedMaterial = runtimeNoColorMemoryMaterial; + } + + ResetSpriteTuning(); + } + + private static Material CreateRuntimeMemoryMaterial( + Material source, + Shader shader, + string suffix, + float grayscale, + float contrast) + { + var material = new Material(source) + { + shader = shader, + name = $"{source.name}_RuntimeMemoryTuning_{suffix}" + }; + SetFloatIfExists(material, MemoryGrayscaleId, grayscale); + SetFloatIfExists(material, MemoryContrastId, contrast); + return material; } diff --git a/Assets/Shader/Shader/MemoryTuningSprite.shader b/Assets/Shader/Shader/MemoryTuningSprite.shader index f56aa1ece..3313912c6 100644 --- a/Assets/Shader/Shader/MemoryTuningSprite.shader +++ b/Assets/Shader/Shader/MemoryTuningSprite.shader @@ -14,6 +14,13 @@ Shader "AibisDream/MemoryTuningSprite" _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 + _PixelateFade ("Pixelate Fade", Range(0,1)) = 0 + _PixelatePixelDensity ("Pixelate Pixel Density", Range(6,160)) = 96 + _GaussianBlurFade ("Gaussian Blur Fade", Range(0,1)) = 0 + _GaussianBlurOffset ("Gaussian Blur Offset", Range(0,1)) = 0.05 + _UVDistortFade ("UV Distort Fade", Range(0,1)) = 0 + _MemoryGrayscale ("Memory Grayscale", Range(0,1)) = 0 + _MemoryContrast ("Memory Contrast", Range(0,3)) = 1 } SubShader @@ -73,6 +80,13 @@ Shader "AibisDream/MemoryTuningSprite" half _DistortStrength; half _ChromaticStrength; half _FadeDistortStrength; + half _PixelateFade; + half _PixelatePixelDensity; + half _GaussianBlurFade; + half _GaussianBlurOffset; + half _UVDistortFade; + half _MemoryGrayscale; + half _MemoryContrast; CBUFFER_END Varyings vert(Attributes input) @@ -89,54 +103,77 @@ Shader "AibisDream/MemoryTuningSprite" half4 SampleSprite(float2 uv) { - return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, saturate(uv)); + half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, saturate(uv)); + half luma = dot(col.rgb, half3(0.299, 0.587, 0.114)); + col.rgb = lerp(col.rgb, half3(luma, luma, luma), saturate(_MemoryGrayscale)); + col.rgb = saturate(lerp(half3(0.5, 0.5, 0.5), col.rgb, _MemoryContrast)); + return col; } - float2 PixelSnap(float2 uv, half strength) + float2 PixelSnap(float2 uv, half strength, half density) { - half pixels = lerp(_PixelMax, _PixelMin, strength); + half pixels = max(1.0, lerp(_PixelMax, _PixelMin, strength)); + pixels = lerp(pixels, max(1.0, density), step(0.001, density)); float2 aspectPixels = float2(pixels, max(1.0, pixels * _MainTex_TexelSize.z / _MainTex_TexelSize.w)); - return (floor(uv * aspectPixels) + 0.5) / aspectPixels; + return round((uv - 0.5) * aspectPixels) / aspectPixels + 0.5; } float2 DistortUv(float2 uv, half strength) { - float angle = radians(40.0); + float angle = radians(130.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; + float2 edge = smoothstep(0.0, 0.12, uv) * smoothstep(0.0, 0.12, 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; + return uv + side * wave * volFxStrength * edgeMask; } 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); + float angle = radians(111.0); + float radial = distance(uv, 0.5) * 0.0026; + float stepSize = (0.014 + radial + _GaussianBlurOffset * 0.004) * strength; + float2 stepDir = normalize(float2(sin(angle), cos(angle))); + float2 stepUv = stepDir * 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; + half4 col = SampleSprite(uv) * 0.18; + col += SampleSprite(uv + stepUv * 1.0) * 0.15; + col += SampleSprite(uv - stepUv * 1.0) * 0.15; + col += SampleSprite(uv + stepUv * 2.0) * 0.12; + col += SampleSprite(uv - stepUv * 2.0) * 0.12; + col += SampleSprite(uv + stepUv * 3.0) * 0.09; + col += SampleSprite(uv - stepUv * 3.0) * 0.09; + col += SampleSprite(uv + stepUv * 4.0) * 0.05; + col += SampleSprite(uv - stepUv * 4.0) * 0.05; return col; } - half3 ApplySaturation(half3 color, half saturation) + half4 ChromaticSample(float2 uv, half4 source, half strength) { - half luma = dot(color, half3(0.299, 0.587, 0.114)); - return lerp(half3(luma, luma, luma), color, saturation); + float aspect = max(0.001, _MainTex_TexelSize.z / _MainTex_TexelSize.w); + float angle = 0.496 * 6.2831853; + float2 dirA = float2(cos(angle) / aspect, sin(angle)); + float2 dirB = -dirA; + float response = pow(saturate(strength), 0.75); + float mov = (0.07 + pow(distance(float2(0.5, 0.5), uv), 3.0) * 0.644) * response; + + half4 colA = SampleSprite(uv + dirA * mov) * half4(1.0, 0.9888, 0.705, 1.0); + half4 colB = SampleSprite(uv + dirB * mov) * half4(0.705, 0.729, 1.0, 1.0); + half3 splitRgb = (colA.rgb + colB.rgb) * 0.5865; + half splitAlpha = max(max(colA.a, colB.a), source.a); + half4 splitCol = half4(splitRgb, splitAlpha); + return lerp(source, splitCol, 0.85 * response); } half4 frag(Varyings input) : SV_Target { half clarity = saturate(_ClarityTuning); 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); float2 uv = input.uv; half fade = saturate((_DirectionalDistortionFade + 15.0) / 15.0); @@ -146,22 +183,12 @@ Shader "AibisDream/MemoryTuningSprite" uv.x += fadeWave * _FadeDistortStrength * fade; } - uv = DistortUv(uv, clarity); - float2 pixelUv = PixelSnap(uv, frequency); - uv = lerp(uv, pixelUv, frequency); + uv = DistortUv(uv, distortion); + float2 pixelUv = PixelSnap(uv, pixelate, _PixelatePixelDensity); + uv = lerp(uv, pixelUv, pixelate); - 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)); + half4 col = lerp(SampleSprite(uv), BlurSample(uv, blur), blur); + col = ChromaticSample(uv, col, clarity); col *= input.color; return col; } diff --git a/Docs/MemoryProcessSpriteShaderNotes.md b/Docs/MemoryProcessSpriteShaderNotes.md new file mode 100644 index 000000000..b9c0f3936 --- /dev/null +++ b/Docs/MemoryProcessSpriteShaderNotes.md @@ -0,0 +1,128 @@ +# MemoryProcess Sprite Shader Notes + +本文记录 `FixSystem/Memory` 记忆调频表现的结构与维护边界。该系统属于旧版 `FixSystem`,但仍承载佩佩修复场景里的记忆播放、打孔带检索和双滑杆调频表现。 + +## 相关文件 + +- `Assets/Scripts/FixSystem/Memory/MemoryProcess.cs` +- `Assets/Scripts/FixSystem/Memory/MemorySlider.cs` +- `Assets/Shader/Shader/MemoryTuningSprite.shader` +- 历史 VolFx profile 参考位于 `Assets/Scenes/VFXtest/` + +## 当前结构 + +记忆图像由两层 `SpriteRenderer` 叠加: + +- `memoryFinishedSpriteRenderer`:彩色记忆层。 +- `memoryFinishedImageNoColor`:无色底图层。 + +旧版表现主要依赖 VolFx 后处理。现在调频阶段默认由 `AibisDream/MemoryTuningSprite` 在 sprite 材质上完成。运行时会为两层分别创建材质实例,并把同一组调频参数同步写入两层,避免只扰动彩色层时露出或错开无色底图。 + +`memoryFinishedImageNoColor` 使用同一个 shader,但通过材质参数保留无色底图观感: + +- `_MemoryGrayscale = 1` +- `_MemoryContrast = 1.8` + +彩色层保持: + +- `_MemoryGrayscale = 0` +- `_MemoryContrast = 1` + +## 调频语义 + +`MemorySlider` 输出的是距离目标值的归一化偏差: + +- 越接近目标区间,传出的值越接近 `0`。 +- 越远离目标区间,传出的值越接近 `1`。 + +因此 shader 表现应遵循“远离目标时混乱,靠近目标时清晰稳定”的方向。 + +目前两个轴的职责是: + +- 频率轴:像素化、颗粒化、焦糊感。 +- 清晰度轴:色散、轻微相位错位、弱 UV 扭曲。 + +后续如果要增强反馈,优先保持两个轴的视觉语义差异,不要让两个轴都只表现为普通模糊。 + +## 后处理迁移边界 + +旧 VolFx profile 可以作为视觉参考,但不要直接照搬参数到 sprite shader。原因是后处理作用于最终合成画面,而 sprite shader 只作用于单个 sprite 层。尤其是 UV 扭曲: + +- 后处理中的强扭曲通常不会暴露底层。 +- sprite 层上的强扭曲会撕开当前层边缘,露出下面的 `nocolor` 层或背景。 + +因此 sprite shader 中的扭曲应保持较弱,并在 UV 边缘做衰减。 + +## 旧 VolFx 参考 + +调频滑杆的历史 profile: + +- `Assets/Scenes/VFXtest/记忆像素滑块.asset` + - `PixelationVol` + - `BlurVol` +- `Assets/Scenes/VFXtest/记忆清晰度滑块.asset` + - `ChromaticVol` + - `DistortVol` + +加载与屏幕类历史 profile: + +- `Assets/Scenes/VFXtest/花屏.asset` + - 用于放入记忆卡带后的加载花屏。 + - 不属于双滑杆调频效果。 +- `Assets/Scenes/VFXtest/记忆屏幕.asset` + - 曾用于 `mainEffect` 的默认记忆屏幕后处理。 + +这些 profile 的用途不同,迁移或复刻时需要先确认当前目标是“调频反馈”、“卡带加载过场”,还是“进入记忆界面的默认屏幕质感”。 + +## Volume 字段现状 + +`MemoryProcess` 中仍保留若干 Volume 字段: + +- `memoryClarityVolume` +- `memoryFrequencyVolume` +- `memoryProcessVolume` +- `mainEffect` + +当 `useSpriteShaderTuning = true` 时,`memoryClarityVolume` 与 `memoryFrequencyVolume` 不再驱动调频表现,只作为后处理模式的兼容入口。 + +`memoryProcessVolume` 仍可用于放入记忆卡带后的加载花屏,并已做空引用安全处理。 + +`mainEffect` 曾指向默认记忆屏幕后处理。如果未来要恢复或迁移它,需要先明确是否要作为常驻屏幕质感,而不是误接到卡带加载流程中。 + +## 常见问题 + +### sprite 变紫 + +优先检查 `MemoryTuningSprite.shader` 编译错误。Unity 的紫色材质通常表示 shader 编译失败或材质 shader 丢失。 + +如果 Unity Console 不方便读取,可以查看: + +`%LOCALAPPDATA%\Unity\Editor\Editor.log` + +曾经出现过的坑: + +- HLSL 局部变量命名为 `line` 会触发 `syntax error: unexpected token 'line'`。 +- 与 Unity/URP include 中的函数名冲突也可能导致编译失败,工具函数建议使用项目专属前缀。 + +### nocolor 层露出 + +通常是彩色层和无色层没有同步应用同一套采样扰动,或 UV 扭曲太强。检查: + +- 两层是否都使用运行时材质。 +- 调频参数是否同步写入两层。 +- 扭曲是否有边缘衰减。 + +### 调频差异不明显 + +优先调整 shader 内响应曲线,而不是恢复 VolFx 后处理。可以从以下方向处理: + +- 频率轴加强像素密度、模糊半径或响应曲线。 +- 清晰度轴加强色散方向、相位错位或轻微漂移。 +- 避免把两个轴都调成类似的模糊/噪声效果。 + +## 维护建议 + +- 手动编辑 shader 后,及时检查 Unity Console 或 `Editor.log`。 +- 不要把后处理参数机械映射到 sprite shader。 +- 涉及两层记忆图像时,优先考虑两层同步。 +- 如果要迁移新的后处理效果,先确认它属于调频、加载过场还是默认屏幕氛围。