feat(fix-system): 迁移记忆调频到精灵材质

This commit is contained in:
2026-06-29 22:23:10 +08:00
parent c392c81d31
commit 814b659c39
3 changed files with 271 additions and 56 deletions
@@ -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<RenderTexture> 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<Sprite>(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;
}
+65 -38
View File
@@ -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;
}
+128
View File
@@ -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。
- 涉及两层记忆图像时,优先考虑两层同步。
- 如果要迁移新的后处理效果,先确认它属于调频、加载过场还是默认屏幕氛围。