empty 改为极弱噪波待机态;增加预设切换 crossfade 与 flash 效果; noiseOnlyBlend 支持纯噪波渲染;两段式归一化使 fillAmount=1 正确填满;switch_emotion_wave_config 支持 progress 参数。 Made-with: Cursor
878 lines
37 KiB
C#
878 lines
37 KiB
C#
using UnityEngine;
|
|
using Shapes;
|
|
|
|
namespace AibisDream.MiniGame.HuoShan.EmotionWave
|
|
{
|
|
/// <summary>
|
|
/// Emotion Wave renderer using Shapes immediate mode.
|
|
/// Ported from Web_EmotionWave/js/renderer.js (OscilloscopeRenderer).
|
|
/// </summary>
|
|
[ExecuteAlways]
|
|
public class EmotionWaveRenderer : ImmediateModeShapeDrawer
|
|
{
|
|
[Header("Drawing Area")]
|
|
[Tooltip("绘制区域参考。若挂载了 RectTransform 则用 rect 尺寸转世界坐标,否则用 lossyScale")]
|
|
[SerializeField] private Transform waveScreen;
|
|
[Tooltip("波形在绘制区域内的填充比例,1=贴满边线,0.82=默认留边距")]
|
|
[Range(0.5f, 1f)]
|
|
[SerializeField] private float fillAmount = 0.82f;
|
|
[SerializeField] private float thicknessScale = 0.01f;
|
|
|
|
[Header("Sampling")]
|
|
[SerializeField] private int numPoints = 600;
|
|
|
|
internal EmotionWaveConfig.WaveParams currentParams;
|
|
internal float time;
|
|
|
|
struct WavePoint
|
|
{
|
|
public Vector3 worldPos;
|
|
public float t;
|
|
public float tNorm;
|
|
public float tipIntensity;
|
|
public float yNorm;
|
|
public float rawX, rawY;
|
|
}
|
|
|
|
private WavePoint[] _points;
|
|
private float _scanLineX;
|
|
private float _cursorX;
|
|
|
|
// Blob tracker state
|
|
private struct BlobTracker
|
|
{
|
|
public int pointIndex;
|
|
public float birth;
|
|
public float life;
|
|
}
|
|
private BlobTracker[] _trackers = new BlobTracker[0];
|
|
private int _trackerCount;
|
|
private float _lastTrackerTime;
|
|
|
|
// Peak markers
|
|
private struct PeakMarker
|
|
{
|
|
public Vector3 pos;
|
|
public bool isMax;
|
|
}
|
|
private PeakMarker[] _peakMarkers = new PeakMarker[12];
|
|
private int _peakCount;
|
|
|
|
private int _spectrumScanIdx;
|
|
|
|
#region Math helpers
|
|
|
|
static float Smoothstep(float x)
|
|
{
|
|
x = Mathf.Clamp01(x);
|
|
return x * x * (3f - 2f * x);
|
|
}
|
|
|
|
static float SpikyWave(float t, float spikiness)
|
|
{
|
|
float s = Mathf.Sin(t);
|
|
if (spikiness < 0.01f) return s;
|
|
float exp = 1f - spikiness * 0.85f;
|
|
return Mathf.Sign(s) * Mathf.Pow(Mathf.Abs(s) + 0.0001f, exp);
|
|
}
|
|
|
|
static float Waveshape(float x, float gain)
|
|
{
|
|
if (gain < 1.05f) return x;
|
|
float tg = (float)System.Math.Tanh(gain);
|
|
return (float)System.Math.Tanh(gain * x) / tg;
|
|
}
|
|
|
|
static float PeriodicWave(float phase, float type)
|
|
{
|
|
float s = Mathf.Sin(phase);
|
|
if (type <= 0f) return s;
|
|
float TWO_PI = Mathf.PI * 2f;
|
|
float p = (phase / TWO_PI) % 1f;
|
|
if (p < 0f) p += 1f;
|
|
float tri = 4f * Mathf.Abs(p - 0.5f) - 1f;
|
|
float sq = s >= 0f ? 1f : -1f;
|
|
if (type >= 2f) return sq;
|
|
if (type >= 1f) return (2f - type) * tri + (type - 1f) * sq;
|
|
return (1f - type) * s + type * tri;
|
|
}
|
|
|
|
static float Weierstrass(float phase, int layers = 5, float a = 0.55f, float b = 5f)
|
|
{
|
|
float sum = 0f;
|
|
float norm = 0f;
|
|
for (int k = 0; k <= layers; k++)
|
|
{
|
|
float coef = Mathf.Pow(a, k);
|
|
sum += coef * Mathf.Sin(Mathf.Pow(b, k) * phase);
|
|
norm += coef;
|
|
}
|
|
return norm > 0.001f ? sum / norm : 0f;
|
|
}
|
|
|
|
static float Noise2D(float x, float y)
|
|
{
|
|
return Mathf.PerlinNoise(x + 1000f, y + 1000f) * 2f - 1f;
|
|
}
|
|
|
|
#endregion
|
|
|
|
void EnsureArrays()
|
|
{
|
|
if (_points == null || _points.Length != numPoints + 1)
|
|
_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;
|
|
// 空状态:glowIntensity=0 表示什么都不显示
|
|
if (currentParams.glowIntensity <= 0f) return;
|
|
|
|
EnsureArrays();
|
|
|
|
var p = currentParams;
|
|
|
|
GetWaveScreenBounds(out Vector3 center, out Vector3 size);
|
|
float halfW = size.x * 0.5f;
|
|
float halfH = size.y * 0.5f;
|
|
float z = center.z;
|
|
|
|
float dt = Time.deltaTime;
|
|
|
|
// Breath
|
|
float breathPhase = time * p.breathRate * Mathf.PI * 2f;
|
|
float breath = 1f + Mathf.Sin(breathPhase) * p.breathDepth;
|
|
float subBreath = 1f + Mathf.Sin(breathPhase * 2.7f + 1.3f) * p.breathDepth * 0.35f;
|
|
float totalBreath = breath * subBreath;
|
|
|
|
// Review pulse
|
|
float ri = p.reviewIntensity;
|
|
float reviewPulse = 1f;
|
|
if (ri > 0.05f)
|
|
{
|
|
float pulsePhase = time * 1.5f;
|
|
reviewPulse = 1f + Mathf.Sin(pulsePhase * Mathf.PI * 2f) * 0.2f * ri;
|
|
}
|
|
|
|
// Scan line position
|
|
if (ri > 0.05f)
|
|
_scanLineX = (_scanLineX + dt * 0.3f) % 1f;
|
|
|
|
// Cursor position
|
|
float cursorSpeed = p.cursorSpeed;
|
|
if (cursorSpeed > 0.001f)
|
|
_cursorX = (_cursorX + dt * cursorSpeed * 0.25f) % 1f;
|
|
|
|
float waveshapeGain = Mathf.Max(1f, p.waveshapeGain);
|
|
float envelopeDecay = p.envelopeDecay;
|
|
float spikeIntensity = p.spikeIntensity;
|
|
float spikeFreq = p.spikeFreq > 0.01f ? p.spikeFreq : 3f;
|
|
float spikeSharpness = p.spikeSharpness > 0.01f ? p.spikeSharpness : 1f;
|
|
float weierstrassAmount = p.weierstrassAmount;
|
|
|
|
float totalT = Mathf.PI * 2f * Mathf.Max(2, p.numCycles);
|
|
float waveMode = p.waveMode;
|
|
float spikiness = p.spikiness;
|
|
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++)
|
|
{
|
|
float tParam = (float)i / numPoints * totalT;
|
|
float tNorm = (float)i / numPoints;
|
|
|
|
// X: sweep + Lissajous oscillation + transition bend
|
|
float sweepX = (tNorm * 2f - 1f) * p.timeSpread;
|
|
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);
|
|
float baseX = blendSpiky * spikyX + (1f - blendSpiky) * periodicX;
|
|
float lissX = p.amplitude * baseX;
|
|
|
|
float baseSweep = sweepX * (1f - eased);
|
|
float oscillation = lissX * eased;
|
|
float transitionBend = Mathf.Sin(lissPhase) * p.amplitude * 4f * eased * (1f - eased);
|
|
float x = baseSweep + oscillation + transitionBend;
|
|
|
|
// Y: waveform amplitude
|
|
float yPhase = p.omegaY * tParam + 0.3f;
|
|
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;
|
|
|
|
// tanh waveshaping
|
|
float wsBlend = Smoothstep((waveshapeGain - 1f) / 0.12f);
|
|
float y = p.amplitudeY * ((1f - wsBlend) * yRaw + wsBlend * Waveshape(yRaw, Mathf.Max(1.01f, waveshapeGain)));
|
|
|
|
// Envelope decay
|
|
if (envelopeDecay > 0.01f)
|
|
{
|
|
float envelope = Mathf.Max(0.1f, Mathf.Exp(-tNorm * envelopeDecay * 5f));
|
|
y *= envelope;
|
|
x *= Mathf.Max(0.5f, Mathf.Exp(-tNorm * envelopeDecay * 1.5f));
|
|
}
|
|
|
|
// Harmonics
|
|
if (p.harmonicStrength > 0.001f)
|
|
{
|
|
float hf = p.harmonicFreq;
|
|
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 * timeDomainWeight * 0.5f;
|
|
}
|
|
|
|
// Weierstrass fractal
|
|
if (weierstrassAmount > 0.001f)
|
|
{
|
|
int wLayers = Mathf.Clamp(5, 3, 6);
|
|
float wx = Weierstrass(lissPhase * 2.1f + time * 0.4f, wLayers);
|
|
float wy = Weierstrass(yPhase * 2.3f + time * 0.3f, wLayers);
|
|
x += p.amplitude * wx * weierstrassAmount * 0.5f;
|
|
y += p.amplitudeY * wy * weierstrassAmount * 0.5f;
|
|
}
|
|
|
|
// Noise
|
|
if (p.noiseAmount > 0.001f)
|
|
{
|
|
float ng = 0f;
|
|
if (waveformType > 0.35f)
|
|
{
|
|
float gt = Mathf.Clamp01((waveformType - 0.35f) / 0.25f);
|
|
ng = gt * gt * (3f - 2f * gt);
|
|
}
|
|
else if (waveMode > 0.2f && waveMode < 0.95f)
|
|
{
|
|
float raw = (waveMode - 0.2f) * (0.95f - waveMode) * 4.5f;
|
|
ng = raw * raw * (3f - 2f * Mathf.Min(1f, raw));
|
|
}
|
|
if (ng > 0.001f)
|
|
{
|
|
float nt = tParam * p.noiseScale * 0.3f;
|
|
float nTime = time * p.noiseSpeed;
|
|
float nx = Noise2D(nt, nTime);
|
|
float ny = Noise2D(nt + 37.7f, nTime + 19.3f);
|
|
x += nx * p.noiseAmount * ng;
|
|
y += ny * p.noiseAmount * ng;
|
|
}
|
|
}
|
|
|
|
// Jitter
|
|
if (p.jitter > 0.001f)
|
|
{
|
|
float jg = 0f;
|
|
if (waveformType > 0.35f)
|
|
{
|
|
float jt = Mathf.Clamp01((waveformType - 0.35f) / 0.25f);
|
|
jg = jt * jt * (3f - 2f * jt);
|
|
}
|
|
else if (waveMode > 0.2f && waveMode < 0.95f)
|
|
{
|
|
float rawJ = (waveMode - 0.2f) * (0.95f - waveMode) * 4.5f;
|
|
jg = rawJ * rawJ * (3f - 2f * Mathf.Min(1f, rawJ));
|
|
}
|
|
if (jg > 0.001f)
|
|
{
|
|
x += (Random.value - 0.5f) * p.jitter * 2f * jg;
|
|
y += (Random.value - 0.5f) * p.jitter * 2f * jg;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
float r = Mathf.Sqrt(x * x + y * y);
|
|
if (r > 0.001f)
|
|
{
|
|
float spikeRaw = Mathf.Sin(spikeFreq * tParam + time * 0.5f);
|
|
float spike = spikeIntensity * Mathf.Pow(Mathf.Max(0f, spikeRaw), spikeSharpness);
|
|
float rNew = r + spike * 0.3f;
|
|
x *= rNew / r;
|
|
y *= rNew / r;
|
|
}
|
|
}
|
|
|
|
// Tip intensity (red highlight)
|
|
if (spikeIntensity > 0.2f)
|
|
{
|
|
float spikeRawTip = Mathf.Sin(spikeFreq * tParam + time * 0.5f);
|
|
float spikeValTip = Mathf.Pow(Mathf.Max(0f, spikeRawTip), spikeSharpness);
|
|
if (spikeValTip > 0.5f)
|
|
{
|
|
tipIntensity = (spikeValTip - 0.5f) / 0.5f;
|
|
tipIntensity *= Mathf.Clamp01((spikeIntensity - 0.2f) / 0.4f);
|
|
tipIntensity = Mathf.Clamp01(tipIntensity);
|
|
}
|
|
}
|
|
|
|
float wsTipBlend = waveshapeGain > 1.8f ? Mathf.Clamp01((waveshapeGain - 1.8f) / 0.4f) : 0f;
|
|
wsTipBlend = wsTipBlend * wsTipBlend * (3f - 2f * wsTipBlend);
|
|
if (wsTipBlend > 0.001f)
|
|
{
|
|
float shapedY = Mathf.Abs(Waveshape(yRaw, waveshapeGain));
|
|
float wsThreshold = 0.82f;
|
|
if (shapedY > wsThreshold)
|
|
{
|
|
float wsTip = (shapedY - wsThreshold) / (1f - wsThreshold);
|
|
wsTip *= Mathf.Clamp01((waveshapeGain - 1.8f) / 2.2f);
|
|
wsTip = Mathf.Min(1f, wsTip * wsTipBlend);
|
|
tipIntensity = Mathf.Max(tipIntensity, wsTip);
|
|
}
|
|
}
|
|
if (spikiness > 0.2f)
|
|
{
|
|
float rawAbsY = Mathf.Abs(PeriodicWave(p.omegaY * tParam + 0.3f, waveformType));
|
|
float threshold = 0.85f;
|
|
if (rawAbsY > threshold)
|
|
{
|
|
float classicTip = (rawAbsY - threshold) / (1f - threshold);
|
|
classicTip *= (spikiness - 0.2f) / 0.8f;
|
|
tipIntensity = Mathf.Max(tipIntensity, Mathf.Clamp01(classicTip));
|
|
}
|
|
}
|
|
|
|
float yNormVal = y / (p.amplitudeY > 0.001f ? p.amplitudeY : 1f);
|
|
|
|
_points[i] = new WavePoint
|
|
{
|
|
t = tParam,
|
|
tNorm = tNorm,
|
|
tipIntensity = tipIntensity,
|
|
yNorm = yNormVal,
|
|
rawX = x,
|
|
rawY = y
|
|
};
|
|
}
|
|
|
|
// 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))
|
|
{
|
|
Draw.ResetAllDrawStates();
|
|
Draw.LineGeometry = LineGeometry.Flat2D;
|
|
Draw.ThicknessSpace = ThicknessSpace.Meters;
|
|
|
|
float hue = p.hue;
|
|
float sat = p.saturation;
|
|
float lit = p.lightness;
|
|
float gi = p.glowIntensity * reviewPulse;
|
|
float frag = p.fragmentation;
|
|
|
|
// 6 glow layers (additive)
|
|
Draw.BlendMode = ShapesBlendMode.Additive;
|
|
float[] glowWMul = { 10f, 6f, 3.5f, 2f, 1f, 0.5f };
|
|
float[] glowAMul = { 0.015f, 0.03f, 0.08f, 0.18f, 0.55f, 0.85f };
|
|
|
|
for (int la = 0; la < glowWMul.Length; la++)
|
|
{
|
|
float alpha = glowAMul[la] * gi;
|
|
if (alpha < 0.003f) continue;
|
|
|
|
float lw = p.lineWidth * glowWMul[la] * thicknessScale;
|
|
float layerLit = glowWMul[la] > 2f ? Mathf.Min(90f, lit + glowWMul[la] * 3f) : lit;
|
|
Color layerColor = ColorUtil.HslToRgb(hue, sat, layerLit, alpha);
|
|
|
|
Draw.Thickness = lw;
|
|
|
|
for (int i = 0; i < numPoints; i++)
|
|
{
|
|
// Fragmentation
|
|
if (frag > 0.01f)
|
|
{
|
|
float fragVal = Noise2D(_points[i].t * 1.5f + 5.5f, time * 0.3f + 100f);
|
|
if (fragVal > (1f - frag * 1.8f)) continue;
|
|
}
|
|
|
|
// Scan line boost
|
|
Color c = layerColor;
|
|
if (ri > 0.05f)
|
|
{
|
|
float distToScan = Mathf.Abs(_points[i].tNorm - _scanLineX);
|
|
if (distToScan < 0.03f)
|
|
{
|
|
float boost = (1f - distToScan / 0.03f) * 0.8f * ri;
|
|
c = new Color(
|
|
Mathf.Clamp01(c.r * (1f + boost)),
|
|
Mathf.Clamp01(c.g * (1f + boost)),
|
|
Mathf.Clamp01(c.b * (1f + boost)),
|
|
Mathf.Clamp01(c.a * (1f + boost * 0.5f))
|
|
);
|
|
}
|
|
}
|
|
|
|
Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, c);
|
|
}
|
|
}
|
|
|
|
// --- Tip layer (red highlights) ---
|
|
float tipLayer = p.tipLayer;
|
|
float redTipIntensity = Mathf.Clamp01((tipLayer - 0.4f) / 0.2f);
|
|
redTipIntensity = redTipIntensity * redTipIntensity * (3f - 2f * redTipIntensity);
|
|
float redCond = Mathf.Clamp01(
|
|
Mathf.Max(0f, (spikiness - 0.08f) / 0.2f) +
|
|
Mathf.Max(0f, (waveshapeGain - 1.5f) / 0.8f) +
|
|
Mathf.Max(0f, (spikeIntensity - 0.08f) / 0.2f));
|
|
redTipIntensity *= Mathf.Clamp01(redCond);
|
|
|
|
if (redTipIntensity > 0.005f)
|
|
{
|
|
float[] tipWMul = { 4f, 2f, 1f, 0.5f };
|
|
float[] tipAMul = { 0.06f, 0.15f, 0.5f, 0.8f };
|
|
for (int rl = 0; rl < tipWMul.Length; rl++)
|
|
{
|
|
Draw.Thickness = p.lineWidth * tipWMul[rl] * thicknessScale;
|
|
for (int i = 0; i < numPoints; i++)
|
|
{
|
|
float tip = Mathf.Max(_points[i].tipIntensity, _points[i + 1].tipIntensity);
|
|
if (tip < 0.05f) continue;
|
|
|
|
float tipHue = (hue <= 30f || hue >= 330f) ? hue : 128f - tip * 128f;
|
|
float tipLit = lit + tip * 15f;
|
|
float tipAlpha = tipAMul[rl] * gi * tip * redTipIntensity;
|
|
Color tipColor = ColorUtil.HslToRgb(tipHue, sat + tip * 20f, tipLit, tipAlpha);
|
|
|
|
Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, tipColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Review overlay ---
|
|
if (ri > 0.05f)
|
|
{
|
|
Draw.BlendMode = ShapesBlendMode.Transparent;
|
|
DrawReviewOverlay(cam, center, size, scaleY, z, p, ri, hue, sat, lit, gi, dt);
|
|
}
|
|
|
|
// --- Beam spot ---
|
|
Draw.BlendMode = ShapesBlendMode.Additive;
|
|
float beamSpeed = effectivePhaseSpeed * 8f;
|
|
float beamPhase = (time * beamSpeed) % totalT;
|
|
int beamIdx = Mathf.FloorToInt((beamPhase / totalT) * numPoints);
|
|
if (beamIdx >= 0 && beamIdx < _points.Length)
|
|
{
|
|
float spotR = p.lineWidth * 2.5f * thicknessScale;
|
|
float spotLit = Mathf.Min(95f, lit + 35f);
|
|
Color spotColor = ColorUtil.HslToRgb(hue, Mathf.Max(0f, sat - 15f), spotLit, 0.9f);
|
|
Draw.Disc(_points[beamIdx].worldPos, spotR, DiscColors.Radial(spotColor, Color.clear));
|
|
}
|
|
}
|
|
}
|
|
|
|
void DrawReviewOverlay(Camera cam, Vector3 center, Vector3 size, float scale,
|
|
float z, EmotionWaveConfig.WaveParams p, float ri,
|
|
float hue, float sat, float lit, float gi, float dt)
|
|
{
|
|
float halfW = size.x * 0.5f;
|
|
float halfH = size.y * 0.5f;
|
|
float trackerRate = p.featureBoxRate;
|
|
float thresholdLevel = p.thresholdLevel;
|
|
float cursorSpeed = p.cursorSpeed;
|
|
|
|
// --- Threshold / Limiter lines ---
|
|
if (thresholdLevel > 0f && ri > 0.1f)
|
|
{
|
|
float threshYTop = center.y + thresholdLevel * p.amplitudeY * scale;
|
|
float threshYBot = center.y - thresholdLevel * p.amplitudeY * scale;
|
|
float threshAlpha = 0.3f * ri;
|
|
Color threshColor = new Color(1f, 0.8f, 0.24f, threshAlpha);
|
|
|
|
Draw.Thickness = 0.008f;
|
|
|
|
// Dashed lines (simulated)
|
|
float dashLen = halfW * 0.03f;
|
|
float gapLen = dashLen * 0.67f;
|
|
float xStart = center.x - halfW;
|
|
float xEnd = center.x + halfW;
|
|
for (float dx = xStart; dx < xEnd; dx += dashLen + gapLen)
|
|
{
|
|
float dxEnd = Mathf.Min(dx + dashLen, xEnd);
|
|
Draw.Line(new Vector3(dx, threshYTop, z), new Vector3(dxEnd, threshYTop, z), threshColor);
|
|
Draw.Line(new Vector3(dx, threshYBot, z), new Vector3(dxEnd, threshYBot, z), threshColor);
|
|
}
|
|
|
|
// Arrow indicators
|
|
float arrowSize = 0.02f;
|
|
Color arrowColor = new Color(1f, 0.8f, 0.24f, threshAlpha * 1.5f);
|
|
Vector3 arrowLeft = new Vector3(center.x - halfW + 0.01f, 0, z);
|
|
// Top arrow
|
|
arrowLeft.y = threshYTop;
|
|
Draw.Triangle(
|
|
arrowLeft + new Vector3(0, arrowSize, 0),
|
|
arrowLeft + new Vector3(arrowSize * 2f, 0, 0),
|
|
arrowLeft + new Vector3(0, -arrowSize, 0),
|
|
arrowColor);
|
|
// Bottom arrow
|
|
arrowLeft.y = threshYBot;
|
|
Draw.Triangle(
|
|
arrowLeft + new Vector3(0, arrowSize, 0),
|
|
arrowLeft + new Vector3(arrowSize * 2f, 0, 0),
|
|
arrowLeft + new Vector3(0, -arrowSize, 0),
|
|
arrowColor);
|
|
|
|
// Highlight wave segments exceeding threshold
|
|
for (int i = 0; i < numPoints; i++)
|
|
{
|
|
float py = _points[i].worldPos.y;
|
|
bool exceeds = py > threshYTop || py < threshYBot;
|
|
if (!exceeds) continue;
|
|
|
|
float exceedAmount;
|
|
if (py > threshYTop)
|
|
exceedAmount = (py - threshYTop) / Mathf.Max(0.001f, threshYTop - center.y);
|
|
else
|
|
exceedAmount = (threshYBot - py) / Mathf.Max(0.001f, center.y - threshYBot);
|
|
exceedAmount = Mathf.Clamp01(exceedAmount);
|
|
|
|
float exHue = 40f - exceedAmount * 40f;
|
|
float exAlpha = (0.25f + exceedAmount * 0.45f) * ri;
|
|
Color exColor = ColorUtil.HslToRgb(exHue, 90f, 55f, exAlpha);
|
|
Draw.Thickness = p.lineWidth * 1.8f * thicknessScale;
|
|
Draw.Line(_points[i].worldPos, _points[i + 1].worldPos, exColor);
|
|
}
|
|
}
|
|
|
|
// --- Scan line (vertical) ---
|
|
if (ri > 0.1f)
|
|
{
|
|
float scanX = center.x - halfW + _scanLineX * size.x;
|
|
float scanAlpha = 0.25f * ri;
|
|
Color scanColor = ColorUtil.HslToRgb(128f, 80f, 60f, scanAlpha);
|
|
Draw.Thickness = 0.005f;
|
|
Draw.Line(new Vector3(scanX, center.y - halfH, z), new Vector3(scanX, center.y + halfH, z), scanColor);
|
|
|
|
// Scan line glow (wider, fainter)
|
|
Color scanGlow = ColorUtil.HslToRgb(128f, 80f, 60f, scanAlpha * 0.3f);
|
|
Draw.Thickness = 0.04f;
|
|
Draw.Line(new Vector3(scanX, center.y - halfH, z), new Vector3(scanX, center.y + halfH, z), scanGlow);
|
|
}
|
|
|
|
// --- Measurement cursor ---
|
|
if (cursorSpeed > 0.001f && ri > 0.1f)
|
|
{
|
|
float cursorScreenX = center.x - halfW + _cursorX * size.x;
|
|
float cursorAlpha = 0.25f * ri;
|
|
Color cursorColor = new Color(0.4f, 1f, 0.7f, cursorAlpha);
|
|
|
|
// Vertical dashed line
|
|
Draw.Thickness = 0.004f;
|
|
float dashLen = halfH * 0.04f;
|
|
float gapLen = dashLen * 1.5f;
|
|
for (float dy = center.y - halfH; dy < center.y + halfH; dy += dashLen + gapLen)
|
|
{
|
|
float dyEnd = Mathf.Min(dy + dashLen, center.y + halfH);
|
|
Draw.Line(new Vector3(cursorScreenX, dy, z), new Vector3(cursorScreenX, dyEnd, z), cursorColor);
|
|
}
|
|
|
|
// Find closest wave point
|
|
int closestIdx = -1;
|
|
float closestDist = float.MaxValue;
|
|
for (int i = 0; i <= numPoints; i++)
|
|
{
|
|
float dist = Mathf.Abs(_points[i].worldPos.x - cursorScreenX);
|
|
if (dist < closestDist) { closestDist = dist; closestIdx = i; }
|
|
}
|
|
|
|
if (closestIdx >= 0 && closestDist < size.x * 0.05f)
|
|
{
|
|
Vector3 cp = _points[closestIdx].worldPos;
|
|
float crossSize = 0.02f;
|
|
Color crossColor = new Color(0.4f, 1f, 0.7f, cursorAlpha * 2.5f);
|
|
Draw.Thickness = 0.003f;
|
|
Draw.Line(cp + Vector3.left * crossSize, cp + Vector3.right * crossSize, crossColor);
|
|
Draw.Line(cp + Vector3.down * crossSize, cp + Vector3.up * crossSize, crossColor);
|
|
Draw.Disc(cp, 0.006f, crossColor);
|
|
}
|
|
}
|
|
|
|
// --- Blob Trackers ---
|
|
if (trackerRate > 0.01f)
|
|
{
|
|
float trackerInterval = 1f / trackerRate;
|
|
if (time - _lastTrackerTime > trackerInterval && _trackerCount < 8)
|
|
{
|
|
// Find high-curvature point
|
|
int bestIdx = numPoints / 2;
|
|
float bestCurv = 0f;
|
|
for (int ci = 2; ci < numPoints - 1; ci++)
|
|
{
|
|
float curvX = Mathf.Abs(_points[ci - 1].worldPos.x + _points[ci + 1].worldPos.x - 2f * _points[ci].worldPos.x);
|
|
float curvY = Mathf.Abs(_points[ci - 1].worldPos.y + _points[ci + 1].worldPos.y - 2f * _points[ci].worldPos.y);
|
|
float curv = curvX + curvY;
|
|
if (curv > bestCurv) { bestCurv = curv; bestIdx = ci; }
|
|
}
|
|
|
|
if (_trackers.Length < 8) _trackers = new BlobTracker[8];
|
|
_trackers[_trackerCount] = new BlobTracker
|
|
{
|
|
pointIndex = bestIdx,
|
|
birth = time,
|
|
life = 1.5f + Random.value
|
|
};
|
|
_trackerCount++;
|
|
_lastTrackerTime = time;
|
|
}
|
|
|
|
// Draw trackers
|
|
int writeIdx = 0;
|
|
for (int ti = 0; ti < _trackerCount; ti++)
|
|
{
|
|
var tr = _trackers[ti];
|
|
float age = time - tr.birth;
|
|
if (age >= tr.life) continue;
|
|
|
|
float lifeRatio = age / tr.life;
|
|
float tAlpha = 1f;
|
|
if (age < 0.15f) tAlpha = age / 0.15f;
|
|
else if (lifeRatio > 0.75f) tAlpha = 1f - (lifeRatio - 0.75f) / 0.25f;
|
|
tAlpha *= ri;
|
|
tAlpha = Mathf.Clamp01(tAlpha);
|
|
|
|
int pIdx = Mathf.Min(tr.pointIndex, numPoints);
|
|
Vector3 px = _points[pIdx].worldPos;
|
|
float flashBoost = age < 0.1f ? 1.5f : 1f;
|
|
|
|
// Glow disc
|
|
Color dotGlow = new Color(0f, 1f, 0.47f, 0.15f * tAlpha);
|
|
Draw.Disc(px, 0.03f, DiscColors.Radial(new Color(0f, 1f, 0.47f, 0.7f * tAlpha * flashBoost), Color.clear));
|
|
|
|
// Core dot
|
|
Color dotCore = new Color(0f, 1f, 0.47f, 0.9f * tAlpha * flashBoost);
|
|
Draw.Disc(px, 0.008f, dotCore);
|
|
|
|
// Leader line
|
|
float lineDir = px.y < center.y ? -1f : 1f;
|
|
float lineLen = 0.04f;
|
|
Color lineColor = new Color(0f, 1f, 0.47f, 0.25f * tAlpha);
|
|
Draw.Thickness = 0.002f;
|
|
Draw.Line(px + Vector3.up * lineDir * 0.01f, px + Vector3.up * lineDir * lineLen, lineColor);
|
|
|
|
_trackers[writeIdx++] = tr;
|
|
}
|
|
_trackerCount = writeIdx;
|
|
}
|
|
|
|
// --- Peak markers ---
|
|
if (ri > 0.15f)
|
|
{
|
|
_peakCount = 0;
|
|
for (int i = 2; i < numPoints - 1 && _peakCount < 12; i++)
|
|
{
|
|
bool isMax = _points[i].worldPos.y > _points[i - 1].worldPos.y && _points[i].worldPos.y > _points[i + 1].worldPos.y;
|
|
bool isMin = _points[i].worldPos.y < _points[i - 1].worldPos.y && _points[i].worldPos.y < _points[i + 1].worldPos.y;
|
|
if (!isMax && !isMin) continue;
|
|
|
|
float peakDist = Mathf.Abs(_points[i].worldPos.y - center.y);
|
|
if (peakDist < scale * 0.15f) continue;
|
|
|
|
_peakMarkers[_peakCount++] = new PeakMarker { pos = _points[i].worldPos, isMax = isMax };
|
|
}
|
|
|
|
float peakAlpha = 0.35f * ri;
|
|
Color peakColor = new Color(0f, 1f, 0.47f, peakAlpha);
|
|
float triSize = 0.012f;
|
|
|
|
for (int pi = 0; pi < _peakCount; pi++)
|
|
{
|
|
var pk = _peakMarkers[pi];
|
|
if (pk.isMax)
|
|
{
|
|
Draw.Triangle(
|
|
pk.pos + new Vector3(-triSize, triSize * 2f + 0.005f, 0),
|
|
pk.pos + new Vector3(triSize, triSize * 2f + 0.005f, 0),
|
|
pk.pos + new Vector3(0, 0.005f, 0),
|
|
peakColor);
|
|
}
|
|
else
|
|
{
|
|
Draw.Triangle(
|
|
pk.pos + new Vector3(-triSize, -triSize * 2f - 0.005f, 0),
|
|
pk.pos + new Vector3(triSize, -triSize * 2f - 0.005f, 0),
|
|
pk.pos + new Vector3(0, -0.005f, 0),
|
|
peakColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Frequency spectrum bars ---
|
|
if (ri > 0.05f)
|
|
DrawFrequencyBars(center, size, z, hue, sat, lit, ri);
|
|
}
|
|
|
|
void DrawFrequencyBars(Vector3 center, Vector3 size, float z,
|
|
float hue, float sat, float lit, float ri)
|
|
{
|
|
int numFreqs = 8;
|
|
float[] amps = new float[numFreqs];
|
|
|
|
int step = Mathf.Max(1, numPoints / 128);
|
|
int sampleN = numPoints / step;
|
|
|
|
for (int f = 0; f < numFreqs; f++)
|
|
{
|
|
float cosSum = 0f, sinSum = 0f;
|
|
int freq = f + 1;
|
|
for (int si = 0; si < sampleN; si++)
|
|
{
|
|
int idx = si * step;
|
|
if (idx > numPoints) break;
|
|
float val = _points[idx].rawY;
|
|
float angle = 2f * Mathf.PI * freq * si / sampleN;
|
|
cosSum += val * Mathf.Cos(angle);
|
|
sinSum += val * Mathf.Sin(angle);
|
|
}
|
|
cosSum /= sampleN;
|
|
sinSum /= sampleN;
|
|
amps[f] = 2f * Mathf.Sqrt(cosSum * cosSum + sinSum * sinSum);
|
|
}
|
|
|
|
float maxAmp = 0f;
|
|
for (int i = 0; i < numFreqs; i++)
|
|
if (amps[i] > maxAmp) maxAmp = amps[i];
|
|
if (maxAmp < 0.001f) return;
|
|
|
|
_spectrumScanIdx = Mathf.FloorToInt(time * 1.2f) % numFreqs;
|
|
|
|
float halfW = size.x * 0.5f;
|
|
float halfH = size.y * 0.5f;
|
|
float chartW = Mathf.Min(halfW * 0.4f, 0.15f);
|
|
float chartH = Mathf.Min(halfH * 0.3f, 0.06f);
|
|
float chartX = center.x + halfW - chartW - 0.02f;
|
|
float chartY = center.y - halfH + 0.02f;
|
|
|
|
// Background
|
|
Color bgColor = new Color(0, 0, 0, 0.5f * ri);
|
|
Draw.Rectangle(new Vector3(chartX + chartW * 0.5f, chartY + chartH * 0.5f, z),
|
|
new Vector2(chartW + 0.01f, chartH + 0.01f), bgColor);
|
|
|
|
// Border
|
|
Draw.Thickness = 0.001f;
|
|
Color borderColor = ColorUtil.HslToRgb(hue, sat, lit, 0.2f * ri);
|
|
float bx0 = chartX - 0.005f;
|
|
float by0 = chartY - 0.005f;
|
|
float bx1 = chartX + chartW + 0.005f;
|
|
float by1 = chartY + chartH + 0.005f;
|
|
Draw.Line(new Vector3(bx0, by0, z), new Vector3(bx1, by0, z), borderColor);
|
|
Draw.Line(new Vector3(bx1, by0, z), new Vector3(bx1, by1, z), borderColor);
|
|
Draw.Line(new Vector3(bx1, by1, z), new Vector3(bx0, by1, z), borderColor);
|
|
Draw.Line(new Vector3(bx0, by1, z), new Vector3(bx0, by0, z), borderColor);
|
|
|
|
float barGap = 0.002f;
|
|
float barW = Mathf.Max(0.005f, (chartW - (numFreqs - 1) * barGap) / numFreqs);
|
|
|
|
for (int i = 0; i < numFreqs; i++)
|
|
{
|
|
float normAmp = amps[i] / maxAmp;
|
|
float barH = normAmp * chartH * 0.9f;
|
|
float bx = chartX + i * (barW + barGap);
|
|
float by = chartY;
|
|
|
|
bool isScanned = i == _spectrumScanIdx;
|
|
float barAlpha = isScanned ? 0.8f * ri : 0.35f * ri;
|
|
float barLit = isScanned ? lit + 15f : lit;
|
|
|
|
Color barColor = ColorUtil.HslToRgb(hue, sat, barLit, barAlpha);
|
|
Draw.Rectangle(new Vector3(bx + barW * 0.5f, by + barH * 0.5f, z),
|
|
new Vector2(barW, barH), barColor);
|
|
|
|
if (isScanned)
|
|
{
|
|
Color markColor = ColorUtil.HslToRgb(hue, sat, lit + 25f, 0.7f * ri);
|
|
float mx = bx + barW * 0.5f;
|
|
float my = by + barH + 0.005f;
|
|
Draw.Triangle(
|
|
new Vector3(mx - 0.005f, my + 0.008f, z),
|
|
new Vector3(mx + 0.005f, my + 0.008f, z),
|
|
new Vector3(mx, my, z),
|
|
markColor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|