feat(emotionwave): empty 待机态、预设切换过渡、noiseOnlyBlend、渲染优化

empty 改为极弱噪波待机态;增加预设切换 crossfade 与 flash 效果;
noiseOnlyBlend 支持纯噪波渲染;两段式归一化使 fillAmount=1 正确填满;switch_emotion_wave_config 支持 progress 参数。

Made-with: Cursor
This commit is contained in:
2026-03-12 15:26:08 +08:00
parent 0bcdcba8a1
commit 17994d4b56
5 changed files with 191 additions and 29 deletions
@@ -52,6 +52,7 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
[HideInInspector] public float featureBoxRate;
[HideInInspector] public float thresholdLevel;
[HideInInspector] public float cursorSpeed;
[HideInInspector] public float noiseOnlyBlend;
[Header("Spike")]
[HideInInspector] public float spikeIntensity;
@@ -91,6 +92,7 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
glowIntensity = Mathf.Lerp(a.glowIntensity, b.glowIntensity, t),
breathRate = Mathf.Lerp(a.breathRate, b.breathRate, t),
breathDepth = Mathf.Lerp(a.breathDepth, b.breathDepth, t),
noiseOnlyBlend = Mathf.Lerp(a.noiseOnlyBlend, b.noiseOnlyBlend, t),
};
}
@@ -104,6 +106,7 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
glowIntensity = Mathf.Clamp(glowIntensity, 0f, 1.2f);
noiseAmount = Mathf.Max(0f, noiseAmount);
fragmentation = Mathf.Clamp01(fragmentation);
noiseOnlyBlend = Mathf.Clamp01(noiseOnlyBlend);
hue = ((hue % 360f) + 360f) % 360f;
saturation = Mathf.Clamp(saturation, 0f, 100f);
lightness = Mathf.Clamp(lightness, 5f, 80f);
@@ -255,16 +258,41 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
return result;
}
/// <summary>空状态:什么都不显示,用于 switch_emotion_wave_config "empty"</summary>
/// <summary>空状态:保留极弱待机噪波,用于 switch_emotion_wave_config "empty"</summary>
static WaveParams GetEmptyParams()
{
var p = new WaveParams
{
glowIntensity = 0f,
lineWidth = 0f,
amplitude = 0.01f,
amplitudeY = 0.01f
waveMode = 0f,
timeSpread = 1f,
omegaX = 0f,
omegaY = 0f,
phaseSpeed = 0f,
numCycles = 2,
amplitude = 0.5f,
amplitudeY = 0.5f,
waveformType = 0f,
spikiness = 0f,
waveshapeGain = 1f,
harmonicStrength = 0f,
harmonicFreq = 1f,
noiseAmount = 0.3f,
noiseScale = 5f,
noiseSpeed = 0.8f,
jitter = 0.65f,
weierstrassAmount = 0f,
lineWidth = 0.8f,
persistence = 0.82f,
fragmentation = 0f,
hue = 145f,
saturation = 30f,
lightness = 35f,
glowIntensity = 0.5f,
breathRate = 0.15f,
breathDepth = 0.005f,
noiseOnlyBlend = 1f
};
p.Clamp();
return p;
}
@@ -18,6 +18,13 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
[Tooltip("当前波形预设:normal / predicted / modified_joke / empty,可由 switch_emotion_wave_config 切换")]
[SerializeField] private string presetId = "normal";
[Header("Preset Transition")]
[SerializeField] private float presetTransitionDuration = 0.22f;
[SerializeField] private float presetFlashDuration = 0.12f;
[SerializeField] private float presetFlashGlowBoost = 0.45f;
[SerializeField] private float presetFlashLightnessBoost = 18f;
[SerializeField] private float presetFlashGlitchBoost = 0.28f;
[Header("Progress Control")]
[Range(0f, 1f)]
[SerializeField] private float progress = 0f;
@@ -35,6 +42,9 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
private float _currentValue;
private float _elapsedTime;
private string _previousPresetId = "normal";
private float _presetTransitionElapsed = 999f;
private bool _isPresetTransitioning;
public float Progress
{
@@ -47,7 +57,19 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
public void SetPreset(string id)
{
if (!string.IsNullOrEmpty(id))
{
_previousPresetId = string.IsNullOrEmpty(presetId) ? id : presetId;
presetId = id;
_presetTransitionElapsed = 0f;
_isPresetTransitioning = true;
}
}
private void OnEnable()
{
_previousPresetId = presetId;
_presetTransitionElapsed = presetTransitionDuration;
_isPresetTransitioning = false;
}
void Update()
@@ -55,6 +77,8 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
if (config == null || waveRenderer == null) return;
float dt = Time.deltaTime;
if (dt <= 0f)
dt = 1f / 60f;
_elapsedTime += dt;
// CRT startup ease
@@ -65,8 +89,26 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
_currentValue += (progress - _currentValue) * Mathf.Min(1f, dt * lerpSpeed);
smoothedProgress = _currentValue;
// Interpolate wave params(按当前预设)
var waveParams = config.InterpolatePreset(presetId, _currentValue);
// Interpolate wave params(按当前预设;切换时做一次短暂 crossfade
var targetParams = config.InterpolatePreset(presetId, _currentValue);
var waveParams = targetParams;
if (_isPresetTransitioning)
{
float duration = Mathf.Max(0.01f, presetTransitionDuration);
_presetTransitionElapsed += dt;
float blend = Smoothstep(Mathf.Clamp01(_presetTransitionElapsed / duration));
var previousParams = config.InterpolatePreset(_previousPresetId, _currentValue);
waveParams = EmotionWaveConfig.WaveParams.Lerp(previousParams, targetParams, blend);
waveParams.Clamp();
if (_presetTransitionElapsed >= duration)
{
_isPresetTransitioning = false;
_previousPresetId = presetId;
}
}
// Inject review parameters based on stage phase
string phase = config.GetStagePhase(_currentValue);
@@ -96,11 +138,32 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
waveParams.glowIntensity *= startupEase;
waveParams.lightness *= startupEase;
ApplyPresetSwitchFlash(ref waveParams);
// Push to renderer
waveRenderer.currentParams = waveParams;
waveRenderer.time = _elapsedTime;
}
private void ApplyPresetSwitchFlash(ref EmotionWaveConfig.WaveParams waveParams)
{
float duration = Mathf.Max(0.01f, presetFlashDuration);
if (_presetTransitionElapsed >= duration)
return;
float flash = 1f - Mathf.Clamp01(_presetTransitionElapsed / duration);
flash *= flash;
waveParams.glowIntensity = Mathf.Min(1.2f, waveParams.glowIntensity + presetFlashGlowBoost * flash);
waveParams.lightness = Mathf.Min(80f, waveParams.lightness + presetFlashLightnessBoost * flash);
waveParams.noiseAmount += presetFlashGlitchBoost * flash;
waveParams.jitter += presetFlashGlitchBoost * 0.45f * flash;
waveParams.fragmentation = Mathf.Clamp01(waveParams.fragmentation + 0.2f * flash);
waveParams.noiseSpeed += 6f * flash;
waveParams.lineWidth += 0.35f * flash;
waveParams.Clamp();
}
static float Smoothstep(float t)
{
t = Mathf.Clamp01(t);
@@ -60,7 +60,7 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
/// <summary>
/// 切换波形预设(normal / predicted / modified_joke / empty
/// empty 为空状态,什么都不显示
/// empty 为低存在感待机态,会保留极弱噪波
/// </summary>
public void SetPreset(string presetId)
{
@@ -123,6 +123,23 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
_points = new WavePoint[numPoints + 1];
}
void GetWaveScreenBounds(out Vector3 center, out Vector3 size)
{
center = waveScreen.position;
size = waveScreen.lossyScale;
// 对 SpriteRenderer 使用实际世界包围盒,保证 fillAmount 按精灵可见尺寸生效
if (waveScreen.TryGetComponent<SpriteRenderer>(out var spriteRenderer))
{
Bounds bounds = spriteRenderer.bounds;
if (bounds.size.x > 0.0001f && bounds.size.y > 0.0001f)
{
center = bounds.center;
size = bounds.size;
}
}
}
public override void DrawShapes(Camera cam)
{
if (waveScreen == null) return;
@@ -133,15 +150,9 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
var p = currentParams;
Vector3 center = waveScreen.position;
Vector3 size = waveScreen.lossyScale;
GetWaveScreenBounds(out Vector3 center, out Vector3 size);
float halfW = size.x * 0.5f;
float halfH = size.y * 0.5f;
// 归一化:使波形峰值(±amplitude)映射到绘制区边缘,fillAmount=1 时真正填满
float ampX = Mathf.Max(0.01f, p.amplitude);
float ampY = Mathf.Max(0.01f, p.amplitudeY);
float scaleX = halfW * fillAmount / ampX;
float scaleY = halfH * fillAmount / ampY;
float z = center.z;
float dt = Time.deltaTime;
@@ -183,6 +194,12 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
float waveformType = p.waveformType;
float eased = Smoothstep(waveMode);
float timeDomainWeight = 1f - eased;
// 低 waveMode 时更像“固定 X + 相位推进”的时域波形,需要更明显的最小相位速度
float minPhaseSpeed = Mathf.Lerp(0.9f, 0.35f, eased);
float effectivePhaseSpeed = Mathf.Max(p.phaseSpeed, minPhaseSpeed);
float phaseOffset = time * effectivePhaseSpeed * Mathf.Lerp(2.4f, 1f, eased);
// --- Generate wave points ---
for (int i = 0; i <= numPoints; i++)
@@ -192,7 +209,7 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
// X: sweep + Lissajous oscillation + transition bend
float sweepX = (tNorm * 2f - 1f) * p.timeSpread;
float lissPhase = p.omegaX * tParam + time * p.phaseSpeed;
float lissPhase = p.omegaX * tParam + phaseOffset;
float blendSpiky = (spikiness > 0.01f) ? Smoothstep(1f - Mathf.Min(1f, waveformType / 0.5f)) : 0f;
float spikyX = SpikyWave(lissPhase, spikiness);
float periodicX = PeriodicWave(lissPhase, waveformType);
@@ -206,8 +223,10 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
// Y: waveform amplitude
float yPhase = p.omegaY * tParam + 0.3f;
float phaseBlend = Smoothstep((0.25f - eased) / 0.08f);
yPhase += time * p.phaseSpeed * phaseBlend;
float timeDomainBlend = Smoothstep((0.25f - eased) / 0.08f);
// XY mode 使用较柔和的相位比;时域模式则增强相位推进,视觉上更像整条波形在流动
float yPhaseSpeedMul = (0.618f + 0.382f * timeDomainBlend) * Mathf.Lerp(1.8f, 1f, eased);
yPhase += phaseOffset * yPhaseSpeedMul;
float spikyY = SpikyWave(yPhase, spikiness);
float periodicY = PeriodicWave(yPhase, waveformType);
float yRaw = blendSpiky * spikyY + (1f - blendSpiky) * periodicY;
@@ -228,13 +247,13 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
if (p.harmonicStrength > 0.001f)
{
float hf = p.harmonicFreq;
float hPhaseX = hf * p.omegaX * tParam + time * p.phaseSpeed * 1.7f;
float hPhaseY = hf * p.omegaY * tParam + time * p.phaseSpeed * 0.6f;
float hPhaseX = hf * p.omegaX * tParam + phaseOffset * 1.7f;
float hPhaseY = hf * p.omegaY * tParam + phaseOffset * Mathf.Lerp(1.15f, 0.6f, eased);
float hx = p.harmonicStrength * PeriodicWave(hPhaseX, waveformType);
float hy = p.harmonicStrength * PeriodicWave(hPhaseY, waveformType);
x += hx * eased;
y += hy;
if (eased < 0.9f) y += hx * (1f - eased) * 0.5f;
if (eased < 0.9f) y += hx * timeDomainWeight * 0.5f;
}
// Weierstrass fractal
@@ -293,9 +312,30 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
}
}
// Breath
x *= totalBreath;
y *= totalBreath;
float tipIntensity = 0f;
// empty 待机态:纯横向直线 + Y 方向多层 Perlin 噪波(模拟音频底噪)
float noiseOnlyBlend = p.noiseOnlyBlend;
if (noiseOnlyBlend > 0.001f)
{
float screenEdge = p.amplitude * 1.1f;
float noiseBaseX = Mathf.Lerp(-screenEdge, screenEdge, tNorm);
float noiseTime = time * Mathf.Max(0.1f, p.noiseSpeed);
float sharpNoise = (Random.value - 0.5f) * 2f;
float ns = Mathf.Max(0.5f, p.noiseScale);
float envelope = 1f + Noise2D(tNorm * ns * 2f, noiseTime + 47.2f) * p.noiseAmount;
float noiseX = noiseBaseX;
float noiseY = sharpNoise * p.amplitudeY * p.jitter * envelope;
noiseY = Mathf.Clamp(noiseY, -p.amplitudeY, p.amplitudeY);
x = Mathf.Lerp(x, noiseX, noiseOnlyBlend);
y = Mathf.Lerp(y, noiseY, noiseOnlyBlend);
tipIntensity *= 1f - noiseOnlyBlend;
}
// Ferrofluid spikes
if (spikeIntensity > 0.01f)
@@ -312,7 +352,6 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
}
// Tip intensity (red highlight)
float tipIntensity = 0f;
if (spikeIntensity > 0.2f)
{
float spikeRawTip = Mathf.Sin(spikeFreq * tParam + time * 0.5f);
@@ -355,7 +394,6 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
_points[i] = new WavePoint
{
worldPos = new Vector3(center.x + x * scaleX, center.y + y * scaleY, z),
t = tParam,
tNorm = tNorm,
tipIntensity = tipIntensity,
@@ -365,6 +403,34 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
};
}
// Two-pass: normalize by actual wave extent so fillAmount=1 truly fills the area
float actualMaxX = 0.001f, actualMaxY = 0.001f;
for (int i = 0; i <= numPoints; i++)
{
float ax = Mathf.Abs(_points[i].rawX);
float ay = Mathf.Abs(_points[i].rawY);
if (ax > actualMaxX) actualMaxX = ax;
if (ay > actualMaxY) actualMaxY = ay;
}
// In noise-only mode, blend toward base amplitude to prevent tiny noise from being amplified to full screen
float baseMaxX = Mathf.Max(0.01f, p.amplitude);
float baseMaxY = Mathf.Max(0.01f, p.amplitudeY);
float nBlend = p.noiseOnlyBlend;
float normX = Mathf.Lerp(actualMaxX, baseMaxX, nBlend);
float normY = Mathf.Lerp(actualMaxY, baseMaxY, nBlend);
float scaleX = halfW * fillAmount / normX;
float scaleY = halfH * fillAmount / normY;
for (int i = 0; i <= numPoints; i++)
{
_points[i].worldPos = new Vector3(
center.x + _points[i].rawX * totalBreath * scaleX,
center.y + _points[i].rawY * totalBreath * scaleY,
z);
}
// --- Draw ---
using (Draw.Command(cam))
{
@@ -465,7 +531,7 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
// --- Beam spot ---
Draw.BlendMode = ShapesBlendMode.Additive;
float beamSpeed = p.phaseSpeed * 8f;
float beamSpeed = effectivePhaseSpeed * 8f;
float beamPhase = (time * beamSpeed) % totalT;
int beamIdx = Mathf.FloorToInt((beamPhase / totalT) * numPoints);
if (beamIdx >= 0 && beamIdx < _points.Length)
@@ -128,17 +128,22 @@ namespace AibisDream.MiniGame.HuoShan.EmotionWave
/// <summary>
/// 切换波形预设(normal / predicted / modified_joke / empty
/// empty 为空状态,什么都不显示
/// empty 为低存在感待机态,会保留极弱噪波
/// 用法: <<switch_emotion_wave_config "predicted">>
/// <<switch_emotion_wave_config "empty">>
/// <<switch_emotion_wave_config "normal">>
/// <<switch_emotion_wave_config "normal" 0.6>>
/// </summary>
[YarnCommand("switch_emotion_wave_config")]
public static void SwitchEmotionWaveConfig(string presetId)
public static void SwitchEmotionWaveConfig(string presetId, float progress = -1f)
{
var manager = GetManager();
if (manager != null)
{
manager.SetPreset(presetId);
if (progress >= 0f)
manager.SetProgress(progress);
}
}
#endregion