feat(huoshan): 情感波浪 EmotionWave 模块
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d88539d912cafbe489fa8efdd1fdf0b9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d558d580689c5ba4c9981eee8e436b82
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,375 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: e4cbad02f49607e4c873f5e76ed20513, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
anchors:
|
||||
- position: 0
|
||||
name: Raw Signal
|
||||
stage: BUILD
|
||||
waveParams:
|
||||
waveMode: 0
|
||||
timeSpread: 0.94
|
||||
omegaX: 1.2
|
||||
omegaY: 1
|
||||
phaseSpeed: 0.07
|
||||
numCycles: 2
|
||||
amplitude: 0.36
|
||||
amplitudeY: 0.2
|
||||
waveformType: 0
|
||||
spikiness: 0
|
||||
waveshapeGain: 1
|
||||
harmonicStrength: 0
|
||||
harmonicFreq: 2
|
||||
noiseAmount: 0
|
||||
noiseScale: 0.5
|
||||
noiseSpeed: 0.12
|
||||
jitter: 0
|
||||
weierstrassAmount: 0
|
||||
lineWidth: 0.88
|
||||
persistence: 0.86
|
||||
fragmentation: 0
|
||||
hue: 128
|
||||
saturation: 52
|
||||
lightness: 32
|
||||
glowIntensity: 0.45
|
||||
breathRate: 0.12
|
||||
breathDepth: 0.008
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 0.15
|
||||
name: Harmonic Stack
|
||||
stage: BUILD
|
||||
waveParams:
|
||||
waveMode: 0.32
|
||||
timeSpread: 0.76
|
||||
omegaX: 2
|
||||
omegaY: 1.5
|
||||
phaseSpeed: 0.24
|
||||
numCycles: 2
|
||||
amplitude: 0.4
|
||||
amplitudeY: 0.38
|
||||
waveformType: 0
|
||||
spikiness: 0
|
||||
waveshapeGain: 1
|
||||
harmonicStrength: 0.04
|
||||
harmonicFreq: 2.6
|
||||
noiseAmount: 0.008
|
||||
noiseScale: 0.7
|
||||
noiseSpeed: 0.28
|
||||
jitter: 0.003
|
||||
weierstrassAmount: 0
|
||||
lineWidth: 1.15
|
||||
persistence: 0.88
|
||||
fragmentation: 0
|
||||
hue: 128
|
||||
saturation: 55
|
||||
lightness: 36
|
||||
glowIntensity: 0.58
|
||||
breathRate: 0.28
|
||||
breathDepth: 0.022
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 0.25
|
||||
name: Transition
|
||||
stage: BUILD
|
||||
waveParams:
|
||||
waveMode: 0.55
|
||||
timeSpread: 0.62
|
||||
omegaX: 1.85
|
||||
omegaY: 1.15
|
||||
phaseSpeed: 0.26
|
||||
numCycles: 2
|
||||
amplitude: 0.43
|
||||
amplitudeY: 0.42
|
||||
waveformType: 0
|
||||
spikiness: 0
|
||||
waveshapeGain: 1
|
||||
harmonicStrength: 0.022
|
||||
harmonicFreq: 2.2
|
||||
noiseAmount: 0.004
|
||||
noiseScale: 0.65
|
||||
noiseSpeed: 0.24
|
||||
jitter: 0.001
|
||||
weierstrassAmount: 0
|
||||
lineWidth: 1.22
|
||||
persistence: 0.875
|
||||
fragmentation: 0
|
||||
hue: 128
|
||||
saturation: 55
|
||||
lightness: 37
|
||||
glowIntensity: 0.6
|
||||
breathRate: 0.3
|
||||
breathDepth: 0.024
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 0.35
|
||||
name: Language Formed
|
||||
stage: BUILD
|
||||
waveParams:
|
||||
waveMode: 1
|
||||
timeSpread: 0.5
|
||||
omegaX: 2
|
||||
omegaY: 1
|
||||
phaseSpeed: 0.3
|
||||
numCycles: 2
|
||||
amplitude: 0.45
|
||||
amplitudeY: 0.45
|
||||
waveformType: 0
|
||||
spikiness: 0
|
||||
waveshapeGain: 1
|
||||
harmonicStrength: 0
|
||||
harmonicFreq: 2
|
||||
noiseAmount: 0
|
||||
noiseScale: 0.8
|
||||
noiseSpeed: 0.3
|
||||
jitter: 0
|
||||
weierstrassAmount: 0
|
||||
lineWidth: 1.3
|
||||
persistence: 0.87
|
||||
fragmentation: 0
|
||||
hue: 128
|
||||
saturation: 55
|
||||
lightness: 38
|
||||
glowIntensity: 0.6
|
||||
breathRate: 0.32
|
||||
breathDepth: 0.025
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 0.5
|
||||
name: Deep Review
|
||||
stage: REVIEW
|
||||
waveParams:
|
||||
waveMode: 1
|
||||
timeSpread: 0.5
|
||||
omegaX: 2
|
||||
omegaY: 1
|
||||
phaseSpeed: 0.5
|
||||
numCycles: 2
|
||||
amplitude: 0.48
|
||||
amplitudeY: 0.48
|
||||
waveformType: 0
|
||||
spikiness: 0
|
||||
waveshapeGain: 1
|
||||
harmonicStrength: 0
|
||||
harmonicFreq: 2
|
||||
noiseAmount: 0
|
||||
noiseScale: 1
|
||||
noiseSpeed: 0.3
|
||||
jitter: 0
|
||||
weierstrassAmount: 0
|
||||
lineWidth: 1.3
|
||||
persistence: 0.87
|
||||
fragmentation: 0
|
||||
hue: 128
|
||||
saturation: 55
|
||||
lightness: 40
|
||||
glowIntensity: 0.65
|
||||
breathRate: 0.38
|
||||
breathDepth: 0.022
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 0.65
|
||||
name: Review Pass
|
||||
stage: REVIEW
|
||||
waveParams:
|
||||
waveMode: 1
|
||||
timeSpread: 0.5
|
||||
omegaX: 2
|
||||
omegaY: 1
|
||||
phaseSpeed: 0.35
|
||||
numCycles: 2
|
||||
amplitude: 0.48
|
||||
amplitudeY: 0.48
|
||||
waveformType: 0
|
||||
spikiness: 0
|
||||
waveshapeGain: 1
|
||||
harmonicStrength: 0
|
||||
harmonicFreq: 2
|
||||
noiseAmount: 0
|
||||
noiseScale: 0.9
|
||||
noiseSpeed: 0.25
|
||||
jitter: 0
|
||||
weierstrassAmount: 0
|
||||
lineWidth: 1.3
|
||||
persistence: 0.87
|
||||
fragmentation: 0
|
||||
hue: 128
|
||||
saturation: 55
|
||||
lightness: 40
|
||||
glowIntensity: 0.6
|
||||
breathRate: 0.32
|
||||
breathDepth: 0.02
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 0.75
|
||||
name: Tone Tweak
|
||||
stage: ADJUST
|
||||
waveParams:
|
||||
waveMode: 1
|
||||
timeSpread: 0.5
|
||||
omegaX: 2
|
||||
omegaY: 1
|
||||
phaseSpeed: 0.35
|
||||
numCycles: 2
|
||||
amplitude: 0.48
|
||||
amplitudeY: 0.48
|
||||
waveformType: 0
|
||||
spikiness: 0
|
||||
waveshapeGain: 1
|
||||
harmonicStrength: 0
|
||||
harmonicFreq: 2
|
||||
noiseAmount: 0
|
||||
noiseScale: 0.9
|
||||
noiseSpeed: 0.25
|
||||
jitter: 0
|
||||
weierstrassAmount: 0
|
||||
lineWidth: 1.3
|
||||
persistence: 0.87
|
||||
fragmentation: 0
|
||||
hue: 128
|
||||
saturation: 55
|
||||
lightness: 40
|
||||
glowIntensity: 0.6
|
||||
breathRate: 0.3
|
||||
breathDepth: 0.02
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 0.85
|
||||
name: Error Compress
|
||||
stage: ADJUST
|
||||
waveParams:
|
||||
waveMode: 1
|
||||
timeSpread: 0.5
|
||||
omegaX: 2.5
|
||||
omegaY: 1.5
|
||||
phaseSpeed: 0.45
|
||||
numCycles: 2
|
||||
amplitude: 0.48
|
||||
amplitudeY: 0.48
|
||||
waveformType: 1
|
||||
spikiness: 0
|
||||
waveshapeGain: 3
|
||||
harmonicStrength: 0.04
|
||||
harmonicFreq: 2
|
||||
noiseAmount: 0.012
|
||||
noiseScale: 1
|
||||
noiseSpeed: 0.4
|
||||
jitter: 0.006
|
||||
weierstrassAmount: 0.22
|
||||
lineWidth: 1.35
|
||||
persistence: 0.84
|
||||
fragmentation: 0
|
||||
hue: 50
|
||||
saturation: 56
|
||||
lightness: 46
|
||||
glowIntensity: 0.7
|
||||
breathRate: 0.42
|
||||
breathDepth: 0.025
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
- position: 1
|
||||
name: Joke Spike
|
||||
stage: ADJUST
|
||||
waveParams:
|
||||
waveMode: 1
|
||||
timeSpread: 0.5
|
||||
omegaX: 3
|
||||
omegaY: 2
|
||||
phaseSpeed: 0.5
|
||||
numCycles: 2
|
||||
amplitude: 0.48
|
||||
amplitudeY: 0.48
|
||||
waveformType: 2
|
||||
spikiness: 0
|
||||
waveshapeGain: 5
|
||||
harmonicStrength: 0.08
|
||||
harmonicFreq: 2
|
||||
noiseAmount: 0.02
|
||||
noiseScale: 1.2
|
||||
noiseSpeed: 0.5
|
||||
jitter: 0.01
|
||||
weierstrassAmount: 0.42
|
||||
lineWidth: 1.4
|
||||
persistence: 0.82
|
||||
fragmentation: 0
|
||||
hue: 0
|
||||
saturation: 58
|
||||
lightness: 50
|
||||
glowIntensity: 0.78
|
||||
breathRate: 0.52
|
||||
breathDepth: 0.03
|
||||
reviewIntensity: 0
|
||||
featureBoxRate: 0
|
||||
thresholdLevel: 0
|
||||
cursorSpeed: 0
|
||||
spikeIntensity: 0
|
||||
spikeFreq: 0
|
||||
spikeSharpness: 0
|
||||
envelopeDecay: 0
|
||||
tipLayer: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c046a1bc8b00d149b08de4b96288243
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.MiniGame.HuoShan.EmotionWave
|
||||
{
|
||||
public static class ColorUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// HSL to RGB conversion matching CSS hsla(h, s%, l%, a).
|
||||
/// h: 0~360, s: 0~100, l: 0~100, a: 0~1
|
||||
/// </summary>
|
||||
public static Color HslToRgb(float h, float s, float l, float a = 1f)
|
||||
{
|
||||
h = ((h % 360f) + 360f) % 360f;
|
||||
float sNorm = Mathf.Clamp01(s / 100f);
|
||||
float lNorm = Mathf.Clamp01(l / 100f);
|
||||
|
||||
float c = (1f - Mathf.Abs(2f * lNorm - 1f)) * sNorm;
|
||||
float x = c * (1f - Mathf.Abs((h / 60f) % 2f - 1f));
|
||||
float m = lNorm - c * 0.5f;
|
||||
|
||||
float r, g, b;
|
||||
if (h < 60f) { r = c; g = x; b = 0f; }
|
||||
else if (h < 120f) { r = x; g = c; b = 0f; }
|
||||
else if (h < 180f) { r = 0f; g = c; b = x; }
|
||||
else if (h < 240f) { r = 0f; g = x; b = c; }
|
||||
else if (h < 300f) { r = x; g = 0f; b = c; }
|
||||
else { r = c; g = 0f; b = x; }
|
||||
|
||||
return new Color(
|
||||
Mathf.Clamp01(r + m),
|
||||
Mathf.Clamp01(g + m),
|
||||
Mathf.Clamp01(b + m),
|
||||
Mathf.Clamp01(a)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da528dd9fdd5afb4093ea165b5ec4a55
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,209 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.MiniGame.HuoShan.EmotionWave
|
||||
{
|
||||
[CreateAssetMenu(fileName = "EmotionWaveConfig", menuName = "AibisDream/EmotionWave Config")]
|
||||
public class EmotionWaveConfig : ScriptableObject
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct WaveParams
|
||||
{
|
||||
[Header("Trajectory Shape")]
|
||||
[Range(0f, 1f)] public float waveMode;
|
||||
[Range(0.1f, 1f)] public float timeSpread;
|
||||
public float omegaX;
|
||||
public float omegaY;
|
||||
public float phaseSpeed;
|
||||
[Min(1)] public int numCycles;
|
||||
|
||||
[Header("Amplitude & Waveform")]
|
||||
public float amplitude;
|
||||
public float amplitudeY;
|
||||
[Range(0f, 2f)] public float waveformType;
|
||||
[Range(0f, 1f)] public float spikiness;
|
||||
[Range(1f, 12f)] public float waveshapeGain;
|
||||
public float harmonicStrength;
|
||||
public float harmonicFreq;
|
||||
|
||||
[Header("Noise & Glitch")]
|
||||
public float noiseAmount;
|
||||
public float noiseScale;
|
||||
public float noiseSpeed;
|
||||
public float jitter;
|
||||
[Range(0f, 1f)] public float weierstrassAmount;
|
||||
|
||||
[Header("Line")]
|
||||
public float lineWidth;
|
||||
[Range(0.5f, 0.98f)] public float persistence;
|
||||
[Range(0f, 1f)] public float fragmentation;
|
||||
|
||||
[Header("Color (HSL)")]
|
||||
public float hue;
|
||||
[Range(0f, 100f)] public float saturation;
|
||||
[Range(5f, 80f)] public float lightness;
|
||||
[Range(0f, 1.2f)] public float glowIntensity;
|
||||
|
||||
[Header("Dynamics")]
|
||||
public float breathRate;
|
||||
public float breathDepth;
|
||||
|
||||
[Header("Review (injected at runtime)")]
|
||||
[HideInInspector] public float reviewIntensity;
|
||||
[HideInInspector] public float featureBoxRate;
|
||||
[HideInInspector] public float thresholdLevel;
|
||||
[HideInInspector] public float cursorSpeed;
|
||||
|
||||
[Header("Spike")]
|
||||
[HideInInspector] public float spikeIntensity;
|
||||
[HideInInspector] public float spikeFreq;
|
||||
[HideInInspector] public float spikeSharpness;
|
||||
[HideInInspector] public float envelopeDecay;
|
||||
[HideInInspector] public float tipLayer;
|
||||
|
||||
public static WaveParams Lerp(WaveParams a, WaveParams b, float t)
|
||||
{
|
||||
return new WaveParams
|
||||
{
|
||||
waveMode = Mathf.Lerp(a.waveMode, b.waveMode, t),
|
||||
timeSpread = Mathf.Lerp(a.timeSpread, b.timeSpread, t),
|
||||
omegaX = Mathf.Lerp(a.omegaX, b.omegaX, t),
|
||||
omegaY = Mathf.Lerp(a.omegaY, b.omegaY, t),
|
||||
phaseSpeed = Mathf.Lerp(a.phaseSpeed, b.phaseSpeed, t),
|
||||
numCycles = Mathf.RoundToInt(Mathf.Lerp(a.numCycles, b.numCycles, t)),
|
||||
amplitude = Mathf.Lerp(a.amplitude, b.amplitude, t),
|
||||
amplitudeY = Mathf.Lerp(a.amplitudeY, b.amplitudeY, t),
|
||||
waveformType = Mathf.Lerp(a.waveformType, b.waveformType, t),
|
||||
spikiness = Mathf.Lerp(a.spikiness, b.spikiness, t),
|
||||
waveshapeGain = Mathf.Lerp(a.waveshapeGain, b.waveshapeGain, t),
|
||||
harmonicStrength = Mathf.Lerp(a.harmonicStrength, b.harmonicStrength, t),
|
||||
harmonicFreq = Mathf.Lerp(a.harmonicFreq, b.harmonicFreq, t),
|
||||
noiseAmount = Mathf.Lerp(a.noiseAmount, b.noiseAmount, t),
|
||||
noiseScale = Mathf.Lerp(a.noiseScale, b.noiseScale, t),
|
||||
noiseSpeed = Mathf.Lerp(a.noiseSpeed, b.noiseSpeed, t),
|
||||
jitter = Mathf.Lerp(a.jitter, b.jitter, t),
|
||||
weierstrassAmount = Mathf.Lerp(a.weierstrassAmount, b.weierstrassAmount, t),
|
||||
lineWidth = Mathf.Lerp(a.lineWidth, b.lineWidth, t),
|
||||
persistence = Mathf.Lerp(a.persistence, b.persistence, t),
|
||||
fragmentation = Mathf.Lerp(a.fragmentation, b.fragmentation, t),
|
||||
hue = Mathf.Lerp(a.hue, b.hue, t),
|
||||
saturation = Mathf.Lerp(a.saturation, b.saturation, t),
|
||||
lightness = Mathf.Lerp(a.lightness, b.lightness, t),
|
||||
glowIntensity = Mathf.Lerp(a.glowIntensity, b.glowIntensity, t),
|
||||
breathRate = Mathf.Lerp(a.breathRate, b.breathRate, t),
|
||||
breathDepth = Mathf.Lerp(a.breathDepth, b.breathDepth, t),
|
||||
};
|
||||
}
|
||||
|
||||
public void Clamp()
|
||||
{
|
||||
numCycles = Mathf.Max(2, numCycles);
|
||||
amplitude = Mathf.Max(0.05f, amplitude);
|
||||
amplitudeY = Mathf.Max(0.05f, amplitudeY);
|
||||
lineWidth = Mathf.Max(0.3f, lineWidth);
|
||||
persistence = Mathf.Clamp(persistence, 0.5f, 0.98f);
|
||||
glowIntensity = Mathf.Clamp(glowIntensity, 0f, 1.2f);
|
||||
noiseAmount = Mathf.Max(0f, noiseAmount);
|
||||
fragmentation = Mathf.Clamp01(fragmentation);
|
||||
hue = ((hue % 360f) + 360f) % 360f;
|
||||
saturation = Mathf.Clamp(saturation, 0f, 100f);
|
||||
lightness = Mathf.Clamp(lightness, 5f, 80f);
|
||||
waveMode = Mathf.Clamp01(waveMode);
|
||||
spikiness = Mathf.Clamp01(spikiness);
|
||||
waveformType = Mathf.Clamp(waveformType, 0f, 2f);
|
||||
timeSpread = Mathf.Clamp(timeSpread, 0.1f, 1f);
|
||||
waveshapeGain = Mathf.Clamp(waveshapeGain, 1f, 12f);
|
||||
weierstrassAmount = Mathf.Clamp01(weierstrassAmount);
|
||||
}
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct StageAnchor
|
||||
{
|
||||
public float position;
|
||||
public string name;
|
||||
public string stage;
|
||||
public WaveParams waveParams;
|
||||
}
|
||||
|
||||
public StageAnchor[] anchors = new StageAnchor[9];
|
||||
|
||||
static float Smoothstep(float t)
|
||||
{
|
||||
t = Mathf.Clamp01(t);
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
public WaveParams Interpolate(float value)
|
||||
{
|
||||
value = Mathf.Clamp01(value);
|
||||
int n = anchors.Length;
|
||||
if (n == 0) return default;
|
||||
if (n == 1) return anchors[0].waveParams;
|
||||
|
||||
int idx = 0;
|
||||
for (int i = 0; i < n - 1; i++)
|
||||
{
|
||||
if (value <= anchors[i + 1].position)
|
||||
{
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
if (i == n - 2) idx = n - 2;
|
||||
}
|
||||
|
||||
int i1 = idx;
|
||||
int i2 = Mathf.Min(n - 1, idx + 1);
|
||||
|
||||
float segStart = anchors[i1].position;
|
||||
float segEnd = anchors[i2].position;
|
||||
float segLen = segEnd - segStart;
|
||||
float tLinear = segLen > 0f ? (value - segStart) / segLen : 0f;
|
||||
float t = Smoothstep(tLinear);
|
||||
|
||||
var result = WaveParams.Lerp(anchors[i1].waveParams, anchors[i2].waveParams, t);
|
||||
result.Clamp();
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetStageName(float value)
|
||||
{
|
||||
value = Mathf.Clamp01(value);
|
||||
if (anchors == null || anchors.Length == 0) return "";
|
||||
|
||||
StageAnchor closest = anchors[0];
|
||||
float minDist = float.MaxValue;
|
||||
foreach (var a in anchors)
|
||||
{
|
||||
float d = Mathf.Abs(value - a.position);
|
||||
if (d < minDist) { minDist = d; closest = a; }
|
||||
}
|
||||
if (minDist < 0.03f) return closest.name;
|
||||
|
||||
StageAnchor lower = anchors[0];
|
||||
StageAnchor upper = anchors[anchors.Length - 1];
|
||||
for (int i = 0; i < anchors.Length - 1; i++)
|
||||
{
|
||||
if (value >= anchors[i].position && value <= anchors[i + 1].position)
|
||||
{
|
||||
lower = anchors[i];
|
||||
upper = anchors[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float blend = (upper.position - lower.position) > 0.001f
|
||||
? (value - lower.position) / (upper.position - lower.position)
|
||||
: 0f;
|
||||
if (blend < 0.35f) return lower.name;
|
||||
if (blend > 0.65f) return upper.name;
|
||||
return $"{lower.name} -> {upper.name}";
|
||||
}
|
||||
|
||||
public string GetStagePhase(float value)
|
||||
{
|
||||
if (value < 0.35f) return "BUILD";
|
||||
if (value < 0.65f) return "REVIEW";
|
||||
return "ADJUST";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4cbad02f49607e4c873f5e76ed20513
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,134 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.MiniGame.HuoShan.EmotionWave
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls the EmotionWave visualization.
|
||||
/// Drag the progress slider in the Inspector to drive the 3-act narrative.
|
||||
/// Ported from Web_EmotionWave/js/app.js.
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
public class EmotionWaveController : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private EmotionWaveConfig config;
|
||||
[SerializeField] private EmotionWaveRenderer waveRenderer;
|
||||
|
||||
[Header("Progress Control")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float progress = 0f;
|
||||
|
||||
[Header("Smooth Tracking")]
|
||||
[SerializeField] private float lerpSpeed = 4f;
|
||||
|
||||
[Header("Tweak (active in 0.75~1.0 range)")]
|
||||
[Range(0f, 1f)] [SerializeField] private float tweakFuzz = 0f;
|
||||
[Range(0f, 1f)] [SerializeField] private float tweakSpin = 0.5f;
|
||||
[Range(0f, 1f)] [SerializeField] private float tweakHue = 0f;
|
||||
|
||||
[Header("CRT Startup")]
|
||||
[SerializeField] private float startupDuration = 1.8f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] [ReadOnlyField] private string currentStage = "";
|
||||
[SerializeField] [ReadOnlyField] private string currentStageName = "";
|
||||
[SerializeField] [ReadOnlyField] private float smoothedProgress;
|
||||
|
||||
private float _currentValue;
|
||||
private float _elapsedTime;
|
||||
|
||||
public float Progress
|
||||
{
|
||||
get => progress;
|
||||
set => progress = Mathf.Clamp01(value);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (config == null || waveRenderer == null) return;
|
||||
|
||||
float dt = Time.deltaTime;
|
||||
_elapsedTime += dt;
|
||||
|
||||
// CRT startup ease
|
||||
float startupFactor = Mathf.Clamp01(_elapsedTime / startupDuration);
|
||||
float startupEase = startupFactor * startupFactor;
|
||||
|
||||
// Smooth tracking
|
||||
_currentValue += (progress - _currentValue) * Mathf.Min(1f, dt * lerpSpeed);
|
||||
smoothedProgress = _currentValue;
|
||||
|
||||
// Interpolate wave params
|
||||
var waveParams = config.Interpolate(_currentValue);
|
||||
|
||||
// Inject review parameters based on stage phase
|
||||
string phase = config.GetStagePhase(_currentValue);
|
||||
currentStage = phase;
|
||||
currentStageName = config.GetStageName(_currentValue);
|
||||
|
||||
if (phase == "REVIEW")
|
||||
{
|
||||
float r0 = 0.35f, r1 = 0.5f, r2 = 0.65f;
|
||||
if (_currentValue <= r1)
|
||||
waveParams.reviewIntensity = Smoothstep((_currentValue - r0) / (r1 - r0));
|
||||
else
|
||||
waveParams.reviewIntensity = 0.15f + 0.85f * Smoothstep((r2 - _currentValue) / (r2 - r1));
|
||||
waveParams.featureBoxRate = 3f;
|
||||
waveParams.thresholdLevel = 0.75f;
|
||||
waveParams.cursorSpeed = 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
waveParams.reviewIntensity = 0f;
|
||||
waveParams.featureBoxRate = 0f;
|
||||
waveParams.thresholdLevel = 0.8f;
|
||||
waveParams.cursorSpeed = 0f;
|
||||
}
|
||||
|
||||
// End-stage tweaks (smoothstep fade-in from 0.75)
|
||||
float tweakLinear = Mathf.Clamp01((_currentValue - 0.75f) / 0.1f);
|
||||
float tweakWeight = tweakLinear * tweakLinear * (3f - 2f * tweakLinear);
|
||||
if (tweakWeight > 0.001f)
|
||||
{
|
||||
float fuzz = tweakFuzz * 0.8f;
|
||||
float spin = 0.2f + tweakSpin * 1f;
|
||||
float hueVal = tweakHue * 128f;
|
||||
waveParams.weierstrassAmount = Mathf.Lerp(waveParams.weierstrassAmount, fuzz, tweakWeight);
|
||||
waveParams.phaseSpeed = Mathf.Lerp(waveParams.phaseSpeed, spin, tweakWeight);
|
||||
waveParams.hue = Mathf.Lerp(waveParams.hue, hueVal, tweakWeight);
|
||||
}
|
||||
|
||||
// CRT startup fade
|
||||
waveParams.glowIntensity *= startupEase;
|
||||
waveParams.lightness *= startupEase;
|
||||
|
||||
// Push to renderer
|
||||
waveRenderer.currentParams = waveParams;
|
||||
waveRenderer.time = _elapsedTime;
|
||||
}
|
||||
|
||||
static float Smoothstep(float t)
|
||||
{
|
||||
t = Mathf.Clamp01(t);
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes a field read-only in the Inspector.
|
||||
/// </summary>
|
||||
public class ReadOnlyFieldAttribute : PropertyAttribute { }
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.CustomPropertyDrawer(typeof(ReadOnlyFieldAttribute))]
|
||||
public class ReadOnlyFieldDrawer : UnityEditor.PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, UnityEditor.SerializedProperty property, GUIContent label)
|
||||
{
|
||||
UnityEditor.EditorGUI.BeginDisabledGroup(true);
|
||||
UnityEditor.EditorGUI.PropertyField(position, property, label);
|
||||
UnityEditor.EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10373e9ad77e7df4eaf482f687e678d0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,226 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using DG.Tweening;
|
||||
using AibisDream.FixSystem;
|
||||
|
||||
namespace AibisDream.MiniGame.HuoShan.EmotionWave
|
||||
{
|
||||
/// <summary>
|
||||
/// 情绪波形管理器 - 用于控制情绪波形的显示、隐藏和进度动画
|
||||
///
|
||||
/// 功能:
|
||||
/// - FixSystemCenter 注册/注销
|
||||
/// - DOTween 显示/隐藏动画
|
||||
/// - 进度控制:立即设置 + 平滑动画
|
||||
/// - 平滑追踪:smoothstep lerp 追踪目标进度
|
||||
/// </summary>
|
||||
public class EmotionWaveManager : MonoBehaviour
|
||||
{
|
||||
#region 序列化字段
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private EmotionWaveController controller;
|
||||
[SerializeField] private Transform containerTransform;
|
||||
|
||||
[Header("Slider Sync")]
|
||||
[Tooltip("可选:关联 SliderController 以自动同步进度")]
|
||||
[SerializeField] private SliderController sliderController;
|
||||
[Tooltip("是否启用滑片同步(勾选后滑片进度会自动驱动情绪波形进度)")]
|
||||
[SerializeField] private bool enableSliderSync = true;
|
||||
|
||||
[Header("动画设置")]
|
||||
[SerializeField] private Vector2 hiddenPosition = new Vector2(0f, -600f);
|
||||
[SerializeField] private Vector2 shownPosition = new Vector2(0f, 0f);
|
||||
[SerializeField] private float showDuration = 0.5f;
|
||||
[SerializeField] private float hideDuration = 0.3f;
|
||||
[SerializeField] private Ease showEase = Ease.OutCubic;
|
||||
[SerializeField] private Ease hideEase = Ease.InCubic;
|
||||
|
||||
[Header("Progress Settings")]
|
||||
[SerializeField] private float lerpSpeed = 4f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] [ReadOnly] private float currentProgress;
|
||||
[SerializeField] [ReadOnly] private float targetProgress;
|
||||
[SerializeField] [ReadOnly] private bool isShown;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
|
||||
private Tweener _progressTweener;
|
||||
private AudioSource _audioSource;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性
|
||||
|
||||
public bool IsShown => isShown;
|
||||
public float CurrentProgress => currentProgress;
|
||||
public float TargetProgress => targetProgress;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_audioSource = GetComponent<AudioSource>();
|
||||
if (_audioSource == null)
|
||||
{
|
||||
_audioSource = gameObject.AddComponent<AudioSource>();
|
||||
}
|
||||
|
||||
// 初始化位置为隐藏状态(不控制 scale,由场景/Prefab 决定)
|
||||
if (containerTransform != null)
|
||||
{
|
||||
containerTransform.localPosition = hiddenPosition;
|
||||
}
|
||||
|
||||
// 注册到 FixSystemCenter
|
||||
FixSystemCenter.SystemDic.Register(this);
|
||||
|
||||
// 订阅 SliderController 的进度事件
|
||||
if (sliderController != null && enableSliderSync)
|
||||
{
|
||||
sliderController.onSliderPositionChanged.AddListener(OnSliderPositionChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// 清理 DOTween
|
||||
_progressTweener?.Kill();
|
||||
|
||||
// 取消订阅
|
||||
if (sliderController != null)
|
||||
{
|
||||
sliderController.onSliderPositionChanged.RemoveListener(OnSliderPositionChanged);
|
||||
}
|
||||
|
||||
// 注销
|
||||
FixSystemCenter.SystemDic.Unregister<EmotionWaveManager>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (controller == null) return;
|
||||
|
||||
// smoothstep lerp 追踪目标进度(和 Web 版 lerpSpeed = 4.0 一致)
|
||||
float dt = Time.deltaTime;
|
||||
currentProgress += (targetProgress - currentProgress) * Mathf.Min(1f, dt * lerpSpeed);
|
||||
|
||||
// 将当前进度传递给 Controller
|
||||
controller.Progress = currentProgress;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 显示/隐藏动画
|
||||
|
||||
/// <summary>
|
||||
/// 显示情绪波形(DOTween 动画)
|
||||
/// </summary>
|
||||
public IEnumerator Show(float duration = -1f)
|
||||
{
|
||||
if (isShown) yield break;
|
||||
|
||||
float actualDuration = duration > 0 ? duration : showDuration;
|
||||
isShown = true;
|
||||
|
||||
if (containerTransform != null)
|
||||
{
|
||||
yield return containerTransform.DOLocalMove(shownPosition, actualDuration)
|
||||
.SetEase(showEase)
|
||||
.WaitForCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏情绪波形(DOTween 动画)
|
||||
/// </summary>
|
||||
public IEnumerator Hide(float duration = -1f)
|
||||
{
|
||||
if (!isShown) yield break;
|
||||
|
||||
float actualDuration = duration > 0 ? duration : hideDuration;
|
||||
isShown = false;
|
||||
|
||||
// 停止所有进度动画
|
||||
_progressTweener?.Kill();
|
||||
|
||||
if (containerTransform != null)
|
||||
{
|
||||
yield return containerTransform.DOLocalMove(hiddenPosition, actualDuration)
|
||||
.SetEase(hideEase)
|
||||
.WaitForCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 进度控制
|
||||
|
||||
/// <summary>
|
||||
/// 立即设置进度(不等待)
|
||||
/// </summary>
|
||||
public void SetProgress(float value)
|
||||
{
|
||||
_progressTweener?.Kill();
|
||||
targetProgress = Mathf.Clamp01(value);
|
||||
currentProgress = targetProgress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 动画到指定进度(DOTween 驱动 targetProgress)
|
||||
/// </summary>
|
||||
public IEnumerator AnimateToProgress(float target, float duration)
|
||||
{
|
||||
_progressTweener?.Kill();
|
||||
|
||||
target = Mathf.Clamp01(target);
|
||||
|
||||
// 使用 DOTween.To 驱动 targetProgress
|
||||
_progressTweener = DOTween.To(
|
||||
() => targetProgress,
|
||||
x => targetProgress = x,
|
||||
target,
|
||||
duration
|
||||
).SetEase(Ease.InOutCubic);
|
||||
|
||||
yield return _progressTweener.WaitForCompletion();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Slider 同步
|
||||
|
||||
/// <summary>
|
||||
/// 响应 SliderController 的位置变化事件
|
||||
/// </summary>
|
||||
private void OnSliderPositionChanged(float normalizedPosition)
|
||||
{
|
||||
if (!enableSliderSync) return;
|
||||
|
||||
// 滑片进度直接驱动 targetProgress(平滑追踪会在 Update 中处理)
|
||||
targetProgress = Mathf.Clamp01(normalizedPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用/禁用滑片同步
|
||||
/// </summary>
|
||||
public void SetSliderSyncEnabled(bool enabled)
|
||||
{
|
||||
enableSliderSync = enabled;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助方法
|
||||
|
||||
[System.Serializable]
|
||||
private class ReadOnlyAttribute : PropertyAttribute { }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55e8be7c583c88f44a97e953a6d806e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,805 @@
|
||||
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];
|
||||
}
|
||||
|
||||
public override void DrawShapes(Camera cam)
|
||||
{
|
||||
if (waveScreen == null) return;
|
||||
EnsureArrays();
|
||||
|
||||
var p = currentParams;
|
||||
|
||||
Vector3 center = waveScreen.position;
|
||||
Vector3 size = waveScreen.lossyScale;
|
||||
float halfW = size.x * 0.5f;
|
||||
float halfH = size.y * 0.5f;
|
||||
float scaleX = halfW * 0.9f;
|
||||
float scaleY = halfH * 0.9f;
|
||||
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);
|
||||
|
||||
// --- 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 + time * p.phaseSpeed;
|
||||
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 phaseBlend = Smoothstep((0.25f - eased) / 0.08f);
|
||||
yPhase += time * p.phaseSpeed * phaseBlend;
|
||||
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 + time * p.phaseSpeed * 1.7f;
|
||||
float hPhaseY = hf * p.omegaY * tParam + time * p.phaseSpeed * 0.6f;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Breath
|
||||
x *= totalBreath;
|
||||
y *= totalBreath;
|
||||
|
||||
// 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)
|
||||
float tipIntensity = 0f;
|
||||
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
|
||||
{
|
||||
worldPos = new Vector3(center.x + x * scaleX, center.y + y * scaleY, z),
|
||||
t = tParam,
|
||||
tNorm = tNorm,
|
||||
tipIntensity = tipIntensity,
|
||||
yNorm = yNormVal,
|
||||
rawX = x,
|
||||
rawY = y
|
||||
};
|
||||
}
|
||||
|
||||
// --- 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 = p.phaseSpeed * 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4c0a33f1933d714aa92cba0bff102bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,159 @@
|
||||
using System.Collections;
|
||||
using AibisDream.FixSystem;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream.MiniGame.HuoShan.EmotionWave
|
||||
{
|
||||
/// <summary>
|
||||
/// 情绪波形系统的 Yarn 命令接口
|
||||
///
|
||||
/// 功能:
|
||||
/// - 显示/隐藏:show_emotion_wave, hide_emotion_wave
|
||||
/// - 进度控制:emotion_wave_to(动画到指定进度),set_emotion_wave(立即设置)
|
||||
/// - 状态查询:is_emotion_wave_shown()
|
||||
/// </summary>
|
||||
public static class EmotionWaveYarnCommand
|
||||
{
|
||||
private static EmotionWaveManager GetManager()
|
||||
{
|
||||
return FixSystemCenter.SystemDic.Get<EmotionWaveManager>();
|
||||
}
|
||||
|
||||
#region 显示/隐藏
|
||||
|
||||
/// <summary>
|
||||
/// 显示情绪波形
|
||||
/// 用法: <<show_emotion_wave>>
|
||||
/// <<show_emotion_wave 0.8>>
|
||||
/// </summary>
|
||||
[YarnCommand("show_emotion_wave")]
|
||||
public static IEnumerator ShowEmotionWave(float duration = -1f)
|
||||
{
|
||||
var manager = GetManager();
|
||||
if (manager != null)
|
||||
{
|
||||
yield return manager.Show(duration);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏情绪波形
|
||||
/// 用法: <<hide_emotion_wave>>
|
||||
/// <<hide_emotion_wave 0.5>>
|
||||
/// </summary>
|
||||
[YarnCommand("hide_emotion_wave")]
|
||||
public static IEnumerator HideEmotionWave(float duration = -1f)
|
||||
{
|
||||
var manager = GetManager();
|
||||
if (manager != null)
|
||||
{
|
||||
yield return manager.Hide(duration);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 进度控制
|
||||
|
||||
/// <summary>
|
||||
/// 动画到指定进度(以指定时间动画到指定进度值,完成后返回给 Yarn)
|
||||
/// 用法: <<emotion_wave_to 0.5 2>> -- 2秒内动画到进度0.5
|
||||
/// <<emotion_wave_to 1.0>> -- 默认1秒内动画到进度1.0
|
||||
/// </summary>
|
||||
[YarnCommand("emotion_wave_to")]
|
||||
public static IEnumerator EmotionWaveTo(float progress, float duration = 1f)
|
||||
{
|
||||
var manager = GetManager();
|
||||
if (manager != null)
|
||||
{
|
||||
yield return manager.AnimateToProgress(progress, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即设置进度(不等待)
|
||||
/// 用法: <<set_emotion_wave 0.5>>
|
||||
/// </summary>
|
||||
[YarnCommand("set_emotion_wave")]
|
||||
public static void SetEmotionWave(float progress)
|
||||
{
|
||||
var manager = GetManager();
|
||||
if (manager != null)
|
||||
{
|
||||
manager.SetProgress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 状态查询
|
||||
|
||||
/// <summary>
|
||||
/// 检查情绪波形是否已显示
|
||||
/// 用法: <<if is_emotion_wave_shown()>>
|
||||
/// </summary>
|
||||
[YarnFunction("is_emotion_wave_shown")]
|
||||
public static bool IsEmotionWaveShown()
|
||||
{
|
||||
var manager = GetManager();
|
||||
return manager != null && manager.IsShown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前进度
|
||||
/// 用法: <<set $progress to get_emotion_wave_progress()>>
|
||||
/// </summary>
|
||||
[YarnFunction("get_emotion_wave_progress")]
|
||||
public static float GetEmotionWaveProgress()
|
||||
{
|
||||
var manager = GetManager();
|
||||
return manager != null ? manager.CurrentProgress : 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标进度
|
||||
/// 用法: <<set $target to get_emotion_wave_target()>>
|
||||
/// </summary>
|
||||
[YarnFunction("get_emotion_wave_target")]
|
||||
public static float GetEmotionWaveTarget()
|
||||
{
|
||||
var manager = GetManager();
|
||||
return manager != null ? manager.TargetProgress : 0f;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Slider 同步控制
|
||||
|
||||
/// <summary>
|
||||
/// 启用滑片同步(情绪波形进度跟随 SliderController)
|
||||
/// 用法: <<enable_emotion_wave_slider_sync>>
|
||||
/// </summary>
|
||||
[YarnCommand("enable_emotion_wave_slider_sync")]
|
||||
public static void EnableSliderSync()
|
||||
{
|
||||
var manager = GetManager();
|
||||
if (manager != null)
|
||||
{
|
||||
manager.SetSliderSyncEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用滑片同步(情绪波形进度由 Yarn 命令控制)
|
||||
/// 用法: <<disable_emotion_wave_slider_sync>>
|
||||
/// </summary>
|
||||
[YarnCommand("disable_emotion_wave_slider_sync")]
|
||||
public static void DisableSliderSync()
|
||||
{
|
||||
var manager = GetManager();
|
||||
if (manager != null)
|
||||
{
|
||||
manager.SetSliderSyncEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1afa5722e9c3ef647b6b4461f2e959f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,94 @@
|
||||
# EmotionWave 与 SliderController 同步使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
`EmotionWaveManager` 可以自动同步 `SliderController` 的进度,实现滑片位置驱动情绪波形进度的效果。
|
||||
|
||||
## 设置方法
|
||||
|
||||
### 1. Inspector 配置
|
||||
|
||||
在 `EmotionWaveManager` 组件的 Inspector 中:
|
||||
|
||||
```
|
||||
[Header("Slider Sync")]
|
||||
- Slider Controller: 拖入场景中的 SliderController 组件
|
||||
- Enable Slider Sync: 勾选启用滑片同步(默认启用)
|
||||
```
|
||||
|
||||
### 2. 工作原理
|
||||
|
||||
当 `Enable Slider Sync` 勾选时:
|
||||
- `SliderController` 的 `onSliderPositionChanged` 事件会自动触发
|
||||
- `EmotionWaveManager` 接收到事件后更新 `targetProgress`
|
||||
- `Update` 中的 smoothstep lerp 会平滑追踪到新进度
|
||||
- 最终传递给 `EmotionWaveController` 进行渲染
|
||||
|
||||
### 3. Yarn 脚本控制
|
||||
|
||||
#### 启用/禁用同步
|
||||
|
||||
```yarn
|
||||
// 启用滑片同步(情绪波形跟随滑片)
|
||||
<<enable_emotion_wave_slider_sync>>
|
||||
|
||||
// 禁用滑片同步(情绪波形由 Yarn 命令独立控制)
|
||||
<<disable_emotion_wave_slider_sync>>
|
||||
```
|
||||
|
||||
#### 典型使用场景
|
||||
|
||||
```yarn
|
||||
// 场景1: 显示情绪波形,同时启用滑片同步
|
||||
<<show_emotion_wave>>
|
||||
<<enable_emotion_wave_slider_sync>>
|
||||
|
||||
// 玩家操作滑片时,情绪波形自动跟随
|
||||
// ...
|
||||
|
||||
// 场景2: 禁用同步,手动控制进度
|
||||
<<disable_emotion_wave_slider_sync>>
|
||||
<<emotion_wave_to 0.5 2>> // 2秒动画到进度0.5
|
||||
|
||||
// 场景3: 立即设置进度(跳过动画)
|
||||
<<set_emotion_wave 0.75>>
|
||||
```
|
||||
|
||||
## 事件流程
|
||||
|
||||
```
|
||||
SliderController.SlideToNormalized(0.5f)
|
||||
↓
|
||||
SliderController._currentAngle 更新
|
||||
↓
|
||||
SliderController.onSliderPositionChanged.Invoke(0.5f)
|
||||
↓
|
||||
EmotionWaveManager.OnSliderPositionChanged(0.5f)
|
||||
↓
|
||||
EmotionWaveManager.targetProgress = 0.5f
|
||||
↓
|
||||
EmotionWaveManager.Update() - smoothstep lerp 追踪
|
||||
↓
|
||||
EmotionWaveManager.currentProgress 平滑到 0.5f
|
||||
↓
|
||||
EmotionWaveController.Progress = currentProgress
|
||||
↓
|
||||
EmotionWaveRenderer 渲染对应阶段的波形
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **优先级**:当滑片同步启用时,Yarn 命令的 `emotion_wave_to` 和 `set_emotion_wave` 仍然有效,但会与滑片进度产生竞争。建议:
|
||||
- 滑片控制时:启用同步
|
||||
- Yarn 自动演出时:禁用同步
|
||||
|
||||
2. **平滑追踪**:无论进度来源(滑片/Yarn),都会经过 smoothstep lerp 平滑处理(lerpSpeed = 4.0)
|
||||
|
||||
3. **性能**:事件机制确保只在滑片位置实际改变时触发,不会每帧都更新
|
||||
|
||||
## 调试
|
||||
|
||||
在 Inspector 的 Debug 区域可以实时查看:
|
||||
- `Current Progress`:当前平滑后的进度
|
||||
- `Target Progress`:目标进度(来自滑片或 Yarn)
|
||||
- `Is Shown`:是否显示状态
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 028c214d27760e64a9796858c85940fa
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user