using System.Threading; using Unity.Mathematics; using UnityEngine; using UnityEngine.UI; namespace Ichni.Editor { /// /// 非 MonoBehaviour 的 Mel 频谱图渲染器。 /// /// === 架构 === /// PathNodeCurveDrawer.Update() /// └─ AudioMelSpectrogram.UpdateView(curT, maxT) /// ├─ 首次检测 AudioClip → 后台线程预计算全曲 FFT /// ├─ 每帧从缓存取出当前可见时间段 → 颜色 LUT → Color32[] → SetPixelData → Apply /// └─ 视角未变时整帧跳过 /// /// 后台线程:PCM 读取(主线程) → FFT + Mel 滤波 + Log(子线程) → 缓存 Log 能量 /// 主线程: 按 visibleStart/End 映射到帧范围 → 逐列查 LUT → 写像素 → GPU upload /// public class AudioMelSpectrogram { // ══════════════════════════════════════════════════════════════════════ // [公共参数] 由 PathNodeCurveDrawer 每帧通过字段赋值同步 // ══════════════════════════════════════════════════════════════════════ /// 渲染目标的 RawImage。尺寸从此读取 × resolutionScale 决定纹理大小。 public RawImage targetImage; /// 纹理分辨率缩放。0.5 = 半分辨率(宽/高各砍一半,像素数 1/4)。 [Range(0.1f, 1f)] public float resolutionScale = 0.5f; /// 颜色渐变:归一化能量 [0,1] → Color。Inspector 修改实时生效。 public Gradient colorGradient; // ══════════════════════════════════════════════════════════════════════ // [FFT / Mel 常量] 改动需要重新预计算才会生效 // ══════════════════════════════════════════════════════════════════════ /// FFT 窗口大小(采样点数)。越大频率分辨率越高、时间分辨率越低。 /// 4096 @ 44100Hz ≈ 93ms 窗口,频率分辨率 ≈ 10.8Hz。 /// 配合 512 帧移(87.5% overlap)维持时间分辨率 ~11.6ms。 private const int FFT_SIZE = 4096; /// 帧移(相邻 FFT 窗口的起始采样偏移量)。 /// 512 = 50% overlap(HOP_SIZE = FFT_SIZE / 2)。 /// 每帧对应时间 = HOP_SIZE / sampleRate ≈ 11.6ms。 private const int HOP_SIZE = 512; /// Mel 滤波器组数量。也是缓存和纹理的高度方向分辨率。 /// 128 个三角滤波器在 Mel 尺度上等距分布,覆盖 0 ~ sampleRate/2 Hz。 /// 更高 → 频率细节更多,但低频段相邻 bin 间距更窄(~40Hz), /// 对钢琴相邻半音(~2Hz @ 440Hz)仍偏粗,但已是实用上限。 private const int MEL_BINS = 128; /// Mel 尺度常数:m = 2595 × log10(1 + f/700)。 private const float MEL_A = 2595f; /// Mel 尺度常数:f = 700 × (10^(m/2595) - 1)。 private const float MEL_B = 700f; // ══════════════════════════════════════════════════════════════════════ // [预计算缓存] 后台线程写入 → 主线程只读 // ══════════════════════════════════════════════════════════════════════ /// 当前已缓存的 AudioClip 引用。检测 clip 变化时触发重新预计算。 private AudioClip _cachedClip; /// 预计算完成后的 Log-Mel 能量缓存。 /// 布局:flat[ frameIndex × MEL_BINS + melBin ],每个元素 = Log(能量)。 /// 存 Log 值避免每帧重复 Mathf.Log。volatile 保证主线程读到最新值。 private volatile float[] _logMelData; /// 后台线程正在写入的临时缓存。完成后通过 _cacheReady 交给主线程。 private float[] _pendingLogData; /// 后台线程完成标记。主线程检测到此标志后切 _pendingLogData → data。 private volatile bool _cacheReady; /// 总帧数。由 (totalSamples - FFT_SIZE) / HOP_SIZE + 1 计算。 /// 决定了 data 的第一维长度。 private int _totalFrames; /// 每帧对应的时间(秒)= HOP_SIZE / sampleRate。 /// frame f 的中心时间 = (f + 1) × _timePerFrame。 private float _timePerFrame; /// Mel 三角滤波器组。_melFilters[m][i] = 第 m 个 mel bin 在 FFT bin i 上的权重。 /// 在预计算开始时构建,之后只读。后台线程安全(写入完成后才启动线程)。 private float[][] _melFilters; /// AudioClip 的采样率(Hz)。用于 _timePerFrame 和 Mel 滤波器组的频率轴计算。 private int _sampleRate; /// 后台线程正在运行的标记。防止重复启动线程。 private volatile bool _computing; /// 后台线程已计算完成的帧数。主线程用此判断可以读 _pendingLogData 的前 N 帧。 /// 后台线程每算完一批(每 16 帧)通过 Volatile.Write 更新, /// 主线程通过 Volatile.Read 读取,保证写 release/读 acquire 语义。 private volatile int _framesComputed; // ══════════════════════════════════════════════════════════════════════ // [运行时纹理] // ══════════════════════════════════════════════════════════════════════ /// 实际写入的 Texture2D。尺寸 = RawImage 像素尺寸 × resolutionScale。 /// 纹理格式 RGBA32,FilterMode 可切换为 Point 避免插值模糊。 private Texture2D _texture; /// 像素缓冲区(CPU 侧)。每帧通过 unsafe 指针直接写入, /// 然后 SetPixelData 整块提交到 GPU。SetPixelData 比 SetPixels 快因为走原始字节拷贝。 private Color32[] _pixels32; /// 当前纹理尺寸。用于检测 RawImage 尺寸变化并重建纹理。 private int _texW, _texH; /// 上一帧渲染的 FFT 帧范围。用于帧级抖动过滤: /// 当 songTime 在帧边界附近振荡时,startF/endF 不变就不重绘, /// 避免子帧级变化导致整张图反复左右偏移。 private int _prevStartF = -1, _prevEndF = -1; // ══════════════════════════════════════════════════════════════════════ // [颜色 LUT] 256 级预计算查找表 // ══════════════════════════════════════════════════════════════════════ /// 256 级颜色查找表。norm[0,1] → LUT[(int)(norm × 255)]。 /// 避免每像素调用 Gradient.Evaluate(函数调用 + 键插值开销)。 private Color32[] _colorLut; /// 上次构建 LUT 时使用的 Gradient 引用。 /// 为 null 或与当前 colorGradient 不同时重建。 /// 注意:Inspector 修改 Gradient 时不改变引用,所以用了直接引用 + 用户 Gradient 每次重建的策略。 private Gradient _appliedGradient; // ══════════════════════════════════════════════════════════════════════ // [Bin 查找表] y → melBin 的预计算映射 // ══════════════════════════════════════════════════════════════════════ /// 纹理行 y → Mel 滤波器索引。_binForY[y] = round(y / h × MEL_BINS)。 /// 避免每像素重复计算浮点除法和 round。 private int[] _binForY; // ══════════════════════════════════════════════════════════════════════ // 每帧入口 // ══════════════════════════════════════════════════════════════════════ /// 每帧由 PathNodeCurveDrawer.Update() 调用。 /// 从 ModuleTime 获取的 AudioSource,用于检测 AudioClip。 /// 可见范围起点(秒),通常 = ModuleTime.SmoothTime(与 CurrentBeatF 同源)。 /// 可见范围终点(秒),由 PathNodeCurveDrawer 按 beatRange × 60/BPM 换算。 public void UpdateView(AudioSource audioSource, float visibleStart, float visibleEnd, bool forceUpdate = false) { if (targetImage == null) return; // ── 检测新 AudioClip → 启动后台预计算 ────────────────────────── // _cachedClip 记录上次预计算的 clip。只要 clip 引用变了就触发重新计算。 // _computing 防重入,后台线程运行期间不重复启动。 if (audioSource != null && audioSource.clip != null && audioSource.clip != _cachedClip) { if (!_computing) StartPreCompute(audioSource.clip); } // ── 后台线程完成 → 切缓存 ────────────────────────────────────── // 必须放在 _computing 检查之前:先完成缓存切换,再让 _computing check 通过。 if (_cacheReady) { _logMelData = _pendingLogData; _pendingLogData = null; _totalFrames = _logMelData.Length / MEL_BINS; _cachedClip = audioSource.clip; _cacheReady = false; _computing = false; } // ── 完全态:全缓存就绪,正常渲染 ──────────────────────────── if (!_computing && _logMelData != null && _totalFrames > 0) { RenderCurrentView(visibleStart, visibleEnd, forceUpdate); return; } // ── 部分态:后台计算中,但已有部分帧可供显示 ──────────────────── if (_computing && _pendingLogData != null && _framesComputed > 0) { // 用 _pendingLogData 渲染已完成部分 RenderCurrentView(visibleStart, visibleEnd, true, _pendingLogData, _framesComputed); return; } } // ══════════════════════════════════════════════════════════════════════ // 渲染当前视图(被完全态和部分态共用) // ══════════════════════════════════════════════════════════════════════ /// 根据 visibleStart/End 计算帧范围,渲染到纹理并提交 GPU。 /// 数据源:完全态用 data,部分态用 _pendingLogData。 /// 可用帧数:完全态 = _totalFrames,部分态 = _framesComputed。 private void RenderCurrentView(float visibleStart, float visibleEnd, bool forceUpdate, float[] data = null, int framesAvail = 0) { // 默认走完全态 _logMelData / _totalFrames if (data == null) { data = _logMelData; framesAvail = _totalFrames; } if (data == null || framesAvail == 0) return; // ── 可见时间范围 → FFT 帧范围 ───────────────────────────────── // FFT 帧 f 的时间中心 = (f × HOP_SIZE + FFT_SIZE/2) / sampleRate // 当 FFT_SIZE=4096, HOP_SIZE=512: 帧 f 中心在 (f + 4) × _timePerFrame float frameOffset = FFT_SIZE / (2f * HOP_SIZE); // 先算帧号,后判断跳过。帧级检测能过滤 songTime 在帧边界附近的振荡, // 避免 startF/endF 没变时整张图反复左右偏移。 int startF = Mathf.Max(0, Mathf.RoundToInt(visibleStart / _timePerFrame - frameOffset)); int endF = Mathf.Max(1, Mathf.RoundToInt(visibleEnd / _timePerFrame - frameOffset)); // ── 帧号未变 → 跳过重绘(部分态 forceUpdate=true 不会跳过)── if (!forceUpdate && startF == _prevStartF && endF == _prevEndF && _pixels32 != null) return; int visFrames = endF - startF; float dur = visibleEnd - visibleStart; if (dur <= 0f) return; // ── 纹理尺寸 ── var rect = targetImage.rectTransform.rect; int w = Mathf.Max(1, Mathf.RoundToInt(rect.width * resolutionScale)); int h = Mathf.Max(1, Mathf.RoundToInt(rect.height * resolutionScale)); bool sizeChanged = _texture == null || _texture.width != w || _texture.height != h; if (sizeChanged) InitTexture(w, h); // ── 全量重绘 ── BuildColorLut(); RenderColumns(startF, visFrames, w, h, 0, w - 1, data, framesAvail); _texture.SetPixelData(_pixels32, 0); _texture.Apply(false); _prevStartF = startF; _prevEndF = endF; _texW = w; _texH = h; } // ══════════════════════════════════════════════════════════════════════ // 列渲染(热路径) // ══════════════════════════════════════════════════════════════════════ /// 渲染 colFrom~colTo 列的像素。被 UpdateView 每帧调用(全量重绘)。 /// 可见范围起点帧号 /// 可见范围总帧数 /// 纹理像素宽度 /// 纹理像素高度 /// 起始列(含) /// 结束列(含) private void RenderColumns(int startF, int visFrames, int w, int h, int colFrom, int colTo, float[] data, int totalFrames) { // 在堆上分配每列的临时缓冲区。 // 相比 stackalloc 有少量 GC 压力,但在 h ≤ 128、每帧最多调用一次的情况下可以忽略。 // 用途:一趟读 data 同时写入 colVals + 扫描 min/max, // 第二趟从 colVals 读(避免第二趟再访存托管数组)。 float[] colVals = new float[h]; for (int x = colFrom; x <= colTo; x++) { // 计算当前列对应的 FFT 帧号 float t = (float)x / w; // 归一化列位置 [0,1] int frameIdx = startF + (int)math.round(t * visFrames); // 超出已计算帧 → 跳过该列,维持透明背景 if (frameIdx >= totalFrames) continue; if (frameIdx < 0) continue; int rowBase = frameIdx * MEL_BINS; // 该帧在 data 中的起始偏移 // ── 一趟读入 stack + 扫描 min/max ── // 遍历 h 行,每行用 _binForY 映射到 Mel bin, // 从 data 取值 → 存入 colVals[y] → 同时更新 mn/mx。 float mn = float.MaxValue, mx = float.MinValue; for (int y = 0; y < h; y++) { float v = data[rowBase + _binForY[y]]; colVals[y] = v; if (v < mn) mn = v; if (v > mx) mx = v; } // 防止全静音帧导致除零。invRange 在后续乘 norm,不额外乘除。 float invRange = 1f / math.max(mx - mn, 0.001f); // ── 从 stack 渲染 ── // norm[0,1] → idx[0,255] → LUT[idx] → 写入像素。 for (int y = 0; y < h; y++) { float norm = (colVals[y] - mn) * invRange; int idx = (int)(norm * 255); if (idx > 255) idx = 255; _pixels32[y * w + x] = _colorLut[idx]; } } } // ══════════════════════════════════════════════════════════════════════ // 后台预计算 // ══════════════════════════════════════════════════════════════════════ /// 启动后台线程预计算。 /// 主线程:读取 PCM + 混单声道 + 构建 Mel 滤波器组。 /// 子线程:Hann 加窗 → FFT → 功率谱 → Mel 三角滤波 → Log → 写入 _pendingLogData。 private void StartPreCompute(AudioClip clip) { _computing = true; _cacheReady = false; _framesComputed = 0; // 重置进度计数器 _cachedClip = clip; _sampleRate = clip.frequency; _timePerFrame = (float)HOP_SIZE / _sampleRate; // ≈ 11.6ms @44100Hz // 构建 Mel 滤波器组(主线程,因为 _melFilters 不是 volatile) BuildMelFilterBank(); // ── 主线程:读取 PCM 并混为单声道 ── // AudioClip.GetData() 必须在主线程调用(Unity API 限制)。 // clip.samples = 每声道的采样数,totalSamples × channels = 总 float 数。 int totalSamples = clip.samples; int channels = clip.channels; float[] samples = new float[totalSamples * channels]; clip.GetData(samples, 0); // 多声道 → 单声道:取各声道平均值。 float[] mono = new float[totalSamples]; if (channels == 1) System.Array.Copy(samples, mono, totalSamples); else for (int i = 0; i < totalSamples; i++) { float sum = 0f; for (int c = 0; c < channels; c++) sum += samples[i * channels + c]; mono[i] = sum / channels; } // 总帧数 = 能容纳的 HOP_SIZE 步进数 // 最后一帧的起始偏移 = totalSamples - FFT_SIZE,不足一帧时返回至少 1 帧。 int totalFrames = Mathf.Max(1, (totalSamples - FFT_SIZE) / HOP_SIZE + 1); float[] pending = new float[totalFrames * MEL_BINS]; _pendingLogData = pending; // 立即暴露给主线程读取,线程写内容 + 更新 _framesComputed // ── 后台线程:FFT + Mel 滤波 + Log ── // 所有数据(mono, window, _melFilters)在线程启动前已就绪,不需要同步。 // 只写 pending[],不碰任何 Unity API。 var thread = new Thread(() => { // Blackman-Harris(4 项最小旁瓣窗):旁瓣 ≤ -92dB,远优于 Hann 的 -32dB。 // 代价:主瓣宽度比 Hann 宽约 60%,但 FFT_SIZE=4096 下仍只有 ~22Hz, // 远窄于 Mel 滤波器间距,不影响频率分辨率。 float[] window = new float[FFT_SIZE]; float bhA0 = 0.35875f, bhA1 = 0.48829f, bhA2 = 0.14128f, bhA3 = 0.01168f; float invN1 = 1f / (FFT_SIZE - 1); for (int i = 0; i < FFT_SIZE; i++) { float p = Mathf.PI * 2f * i * invN1; window[i] = bhA0 - bhA1 * Mathf.Cos(p) + bhA2 * Mathf.Cos(2f * p) - bhA3 * Mathf.Cos(3f * p); } float[] real = new float[FFT_SIZE]; float[] imag = new float[FFT_SIZE]; float[] power = new float[FFT_SIZE / 2]; for (int f = 0; f < totalFrames; f++) { int offset = f * HOP_SIZE; // 加窗:mono[offset + i] × window[i] → real[i] for (int i = 0; i < FFT_SIZE; i++) { int idx = offset + i; real[i] = idx < totalSamples ? mono[idx] * window[i] : 0f; imag[i] = 0f; } // 原位 Cooley-Tukey radix-2 FFT → real/imag 为复频谱 FFT(real, imag); // 功率谱 |X|² = real² + imag²(只取前 N/2 个正频率 bin) for (int i = 0; i < FFT_SIZE / 2; i++) power[i] = real[i] * real[i] + imag[i] * imag[i]; // Mel 三角滤波:每个 Mel bin 是若干 FFT bin 的加权和 int row = f * MEL_BINS; for (int m = 0; m < MEL_BINS; m++) { float e = 0f; var filter = _melFilters[m]; for (int i = 0; i < FFT_SIZE / 2; i++) e += power[i] * filter[i]; // 存 Log(energy) 而不是 raw energy! // 人耳对声压的感知是对数的,且 Log 压缩后帧循环不再需要 Mathf.Log。 pending[row + m] = Mathf.Log(Mathf.Max(e, 1e-10f)); } // 每算完 16 帧(≈ 186ms @11.6ms/帧),更新进度计数器。 // Volatile.Write 保证对主线程可见,同时隐含 release 语义, // 确保写 pending[] 的 stores 在写 _framesComputed 之前完成。 if ((f & 15) == 0 || f == totalFrames - 1) _framesComputed = f + 1; // volatile 写入,主线程立即可见 } _cacheReady = true; // 通知主线程 }); thread.IsBackground = true; // 应用程序退出时自动终止 thread.Start(); } // ══════════════════════════════════════════════════════════════════════ // Mel 三角滤波器组 // ══════════════════════════════════════════════════════════════════════ /// 构建 Mel 三角滤波器组。 /// 在 Mel 尺度上均匀放置 MEL_BINS 个三角滤波器,每个覆盖 3 个 Mel 点(左/中心/右)。 /// 频率范围:0 ~ sampleRate/2 Hz。 /// 三角峰值为 1(归一化),相邻滤波器间交叠 ≈ 50%。 private void BuildMelFilterBank() { int half = FFT_SIZE / 2; float freqMax = _sampleRate / 2f; // Nyquist 频率 float melMin = FreqToMel(0f); float melMax = FreqToMel(freqMax); // 每个 FFT bin 的频率(Hz) float[] binFreq = new float[half]; for (int i = 0; i < half; i++) binFreq[i] = (float)i / FFT_SIZE * _sampleRate; _melFilters = new float[MEL_BINS][]; for (int m = 0; m < MEL_BINS; m++) { // 在 Mel 尺度上等距取三个点:左 / 中心 / 右 float mL = melMin + (melMax - melMin) * (m) / (MEL_BINS + 1); float mC = melMin + (melMax - melMin) * (m + 1) / (MEL_BINS + 1); float mR = melMin + (melMax - melMin) * (m + 2) / (MEL_BINS + 1); float fL = MelToFreq(mL), fC = MelToFreq(mC), fR = MelToFreq(mR); // 三角滤波器:左→中心上升段,中心→右下降段,其余为 0 var filter = new float[half]; for (int i = 0; i < half; i++) { float f = binFreq[i]; if (f >= fL && f < fC) filter[i] = (f - fL) / (fC - fL); // 上升 else if (f >= fC && f < fR) filter[i] = (fR - f) / (fR - fC); // 下降 // else: 0 } _melFilters[m] = filter; } } // ══════════════════════════════════════════════════════════════════════ // FFT — 实序列原位 Cooley-Tukey radix-2 DIT(时域抽取) // ══════════════════════════════════════════════════════════════════════ /// 一维实序列 FFT(正变换)。 /// 输入:real[i] = 实部(时域信号),imag[i] = 0。 /// 输出:real[i] + j×imag[i] = 复频谱(0 ~ sampleRate,对称)。 /// 只需要前 N/2 个 bin(0 ~ Nyquist)做功率谱计算。 /// /// radix-2 = 输入长度必须为 2 的幂(FFT_SIZE = 1024 ✅)。 /// DIT = 先 bit-reversal 重排,再逐级蝶形运算。 private static void FFT(float[] real, float[] imag) { int n = real.Length; // 1. Bit-reversal 重排:使蝶形运算的输入自然有序 for (int i = 1, j = 0; i < n; i++) { int bit = n >> 1; for (; j >= bit; bit >>= 1) j -= bit; j += bit; if (i < j) { (real[i], real[j]) = (real[j], real[i]); (imag[i], imag[j]) = (imag[j], imag[i]); } } // 2. 蝶形运算:逐级合并,len = 2 → 4 → 8 → ... → n for (int len = 2; len <= n; len <<= 1) { float ang = 2f * Mathf.PI / len; // 旋转因子角度 float wlenR = Mathf.Cos(ang); float wlenI = -Mathf.Sin(ang); // 负号 = 正变换 for (int i = 0; i < n; i += len) { float wr = 1f, wi = 0f; // 初始旋转因子 = 1 int half = len >> 1; for (int j = 0; j < half; j++) { int i1 = i + j; // 偶序号 int i2 = i + j + half; // 奇序号 // t = W × X[i2] float tr = wr * real[i2] - wi * imag[i2]; float ti = wr * imag[i2] + wi * real[i2]; // 蝶形:X[i1] = X[i1] + t, X[i2] = X[i1] - t real[i2] = real[i1] - tr; imag[i2] = imag[i1] - ti; real[i1] += tr; imag[i1] += ti; // 更新旋转因子 (wr, wi) = (wr * wlenR - wi * wlenI, wr * wlenI + wi * wlenR); } } } } // ══════════════════════════════════════════════════════════════════════ // Mel 尺度转换 // ══════════════════════════════════════════════════════════════════════ /// 频率(Hz)→ Mel 值。m = 2595 × log10(1 + f/700)。 private static float FreqToMel(float f) => MEL_A * Mathf.Log10(1f + f / MEL_B); /// Mel 值 → 频率(Hz)。f = 700 × (10^(m/2595) - 1)。 private static float MelToFreq(float m) => MEL_B * (Mathf.Pow(10f, m / MEL_A) - 1f); // ══════════════════════════════════════════════════════════════════════ // 纹理 / LUT / Bin 表 // ══════════════════════════════════════════════════════════════════════ /// 创建或重建纹理与辅助表。 private void InitTexture(int w, int h) { // 纹理格式 RGBA32 = 每像素 4 字节(R/G/B/A × byte),与 Color32 结构一致。 // mipChain: false = 不生成 mipmap,节省 GPU 内存和 upload 带宽。 _texture = new Texture2D(w, h, TextureFormat.RGBA32, false); _texture.filterMode = FilterMode.Point; // 无插值,每格锐利显示频率带 _texture.wrapMode = TextureWrapMode.Clamp; // 边缘不循环 _pixels32 = new Color32[w * h]; var bg = (Color32)Color.clear; // 全透明背景 for (int i = 0; i < _pixels32.Length; i++) _pixels32[i] = bg; _texture.SetPixelData(_pixels32, 0); _texture.Apply(false); if (targetImage != null) targetImage.texture = _texture; // 预计算 y → bin 索引:每行 y 映射到最近的 Mel 滤波器 _binForY = new int[h]; for (int y = 0; y < h; y++) _binForY[y] = Mathf.Clamp( Mathf.RoundToInt((float)y / h * MEL_BINS), 0, MEL_BINS - 1); _appliedGradient = null; // 强制下次重建 LUT } /// 构建 256 级颜色 LUT。 /// 用户设置了 Gradient → 每次重建(Inspector 修改不改变引用)。 /// 使用默认 Gradient → 缓存到静态引用。 private void BuildColorLut() { var grad = colorGradient != null ? colorGradient : GetDefaultGradient(); // 只在「使用默认 Gradient 且已缓存」时跳过重建 if (colorGradient == null && _appliedGradient == grad && _colorLut != null) return; _colorLut = new Color32[256]; for (int i = 0; i < 256; i++) _colorLut[i] = (Color32)grad.Evaluate(i / 255f); _appliedGradient = grad; } /// 默认半透渐变:低能透明 → 深紫 → 青蓝 → 青绿 → 金黄 → 白。 /// 透明度逐渐升高,叠在曲线下方也不会遮挡。 private static Gradient _defaultGradient; private static Gradient GetDefaultGradient() { if (_defaultGradient != null) return _defaultGradient; _defaultGradient = new Gradient(); _defaultGradient.SetKeys( new GradientColorKey[] { new GradientColorKey(new Color(0.4f, 0.1f, 0.6f), 0f), new GradientColorKey(new Color(0f, 0.6f, 0.8f), 0.35f), new GradientColorKey(new Color(0.2f, 0.9f, 0.5f), 0.6f), new GradientColorKey(new Color(1f, 0.85f,0.3f), 0.8f), new GradientColorKey(Color.white, 1f), }, new GradientAlphaKey[] { new GradientAlphaKey(0f, 0f), new GradientAlphaKey(0.5f, 0.15f), new GradientAlphaKey(0.75f,0.4f), new GradientAlphaKey(0.9f, 0.7f), new GradientAlphaKey(1f, 1f), } ); return _defaultGradient; } } }