feat(emotionwave): empty 待机态、预设切换过渡、noiseOnlyBlend、渲染优化
empty 改为极弱噪波待机态;增加预设切换 crossfade 与 flash 效果; noiseOnlyBlend 支持纯噪波渲染;两段式归一化使 fillAmount=1 正确填满;switch_emotion_wave_config 支持 progress 参数。 Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user