using UnityEngine; using Shapes; namespace AibisDream.MiniGame.HuoShan.EmotionWave { /// /// Emotion Wave renderer using Shapes immediate mode. /// Ported from Web_EmotionWave/js/renderer.js (OscilloscopeRenderer). /// [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(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); } } } } }