Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/EmotionWave/EmotionWaveRenderer.cs
T
bottlefish 17994d4b56 feat(emotionwave): empty 待机态、预设切换过渡、noiseOnlyBlend、渲染优化
empty 改为极弱噪波待机态;增加预设切换 crossfade 与 flash 效果;
noiseOnlyBlend 支持纯噪波渲染;两段式归一化使 fillAmount=1 正确填满;switch_emotion_wave_config 支持 progress 参数。

Made-with: Cursor
2026-03-12 15:26:08 +08:00

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);
}
}
}
}
}