You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中拆分超宽音频波形Sprite并实现视口内渲染的方案问询

解决Unity Scroll Rect中大型音频波形Sprite的拆分与按需渲染问题

核心思路

  1. 先预计算完整的波形数据数组(而非直接生成超大型Texture2D),浮点数组的内存占用远低于超大纹理,更易处理。
  2. 将波形数据拆分为多个小片段,每个片段生成一个符合Unity纹理尺寸限制的小Texture2D(比如单块宽度设为2048,远低于16000的上限)。
  3. 在Scroll Rect的Content下创建多个Image子对象,每个对应一个波形片段,按水平顺序排列。
  4. 实现视口可见性检测,只渲染当前在Scroll Rect视口内的波形片段,节省渲染资源。

步骤1:重构波形数据生成逻辑

修改WaveformSprite类,分离波形数据计算与纹理生成,先得到完整的波形数组:

public class WaveformSprite : MonoBehaviour
{
    public int chunkWidth = 2048; // 单块波形纹理的宽度,需小于16000
    public int height = 180;
    public Color background = Color.black;
    public Color foreground = Color.yellow;

    // 获取完整的波形数据数组
    public float[] GetFullWaveformData(AudioClip clip, int totalWidth)
    {
        float[] waveform = new float[totalWidth];
        int sampleSize = clip.samples * clip.channels;
        float[] samples = new float[sampleSize];
        clip.GetData(samples, 0);

        int packSize = sampleSize / totalWidth;
        for (int w = 0; w < totalWidth; w++)
        {
            waveform[w] = Mathf.Abs(samples[w * packSize]);
        }
        return waveform;
    }

    // 根据波形数据的某一段生成纹理
    public Texture2D GenerateChunkTexture(float[] waveformSegment)
    {
        int chunkWidth = waveformSegment.Length;
        int halfHeight = height / 2;
        float heightScale = (float)height * 0.75f;

        Texture2D tex = new Texture2D(chunkWidth, height, TextureFormat.RGBA32, false);

        // 填充背景
        for (int x = 0; x < chunkWidth; x++)
        {
            for (int y = 0; y < height; y++)
            {
                tex.SetPixel(x, y, background);
            }
        }

        // 绘制波形
        for (int x = 0; x < chunkWidth; x++)
        {
            float waveHeight = waveformSegment[x] * heightScale;
            for (int y = 0; y < waveHeight; y++)
            {
                tex.SetPixel(x, halfHeight + y, foreground);
                tex.SetPixel(x, halfHeight - y, foreground);
            }
        }

        tex.Apply();
        return tex;
    }
}

步骤2:拆分波形并生成片段Sprite

修改LoadWaveform方法,生成所有波形片段并排列在Scroll Rect的Content中:

public class WaveformLoader : MonoBehaviour
{
    public WaveformSprite waveformSprite;
    public ScrollRect scrollRect;
    public GameObject chunkImagePrefab; // 单个波形片段的Image预制体(提前创建带Image组件的空对象)
    private int totalWaveformWidth; // 从外部传入的完整波形宽度(比如60000)

    public void LoadWaveform(AudioClip clip)
    {
        totalWaveformWidth = GetTotalWaveformWidthFromOtherScript(); // 替换为你的外部宽度获取逻辑

        // 获取完整波形数据
        float[] fullWaveform = waveformSprite.GetFullWaveformData(clip, totalWaveformWidth);

        // 计算需要的片段数量
        int chunkCount = Mathf.CeilToInt((float)totalWaveformWidth / waveformSprite.chunkWidth);
        Transform contentTransform = scrollRect.content;

        // 清空原有片段
        foreach (Transform child in contentTransform)
        {
            Destroy(child.gameObject);
        }

        // 创建并排列每个波形片段
        for (int i = 0; i < chunkCount; i++)
        {
            // 计算当前片段的波形数据范围
            int startIndex = i * waveformSprite.chunkWidth;
            int segmentLength = Mathf.Min(waveformSprite.chunkWidth, totalWaveformWidth - startIndex);
            float[] segmentData = new float[segmentLength];
            Array.Copy(fullWaveform, startIndex, segmentData, 0, segmentLength);

            // 生成片段纹理和Sprite
            Texture2D chunkTex = waveformSprite.GenerateChunkTexture(segmentData);
            Sprite chunkSprite = Sprite.Create(chunkTex, new Rect(0, 0, chunkTex.width, chunkTex.height), Vector2.zero);

            // 创建片段Image对象
            GameObject chunkObj = Instantiate(chunkImagePrefab, contentTransform);
            Image chunkImage = chunkObj.GetComponent<Image>();
            chunkImage.sprite = chunkSprite;
            chunkImage.SetNativeSize();

            // 设置片段位置
            RectTransform chunkRect = chunkObj.GetComponent<RectTransform>();
            chunkRect.anchorMin = new Vector2(0, 0.5f);
            chunkRect.anchorMax = new Vector2(0, 0.5f);
            chunkRect.pivot = new Vector2(0, 0.5f);
            chunkRect.anchoredPosition = new Vector2(startIndex, 0);
        }

        // 更新Content的宽度以适配完整波形
        RectTransform contentRect = contentTransform.GetComponent<RectTransform>();
        contentRect.sizeDelta = new Vector2(totalWaveformWidth, contentRect.sizeDelta.y);
    }

    // 替换为获取外部宽度的逻辑
    private int GetTotalWaveformWidthFromOtherScript()
    {
        // 示例:返回3分钟歌曲对应的宽度
        return 60000;
    }
}

步骤3:实现视口可见性裁剪

添加一个脚本到Scroll Rect的Content上,实时检测每个片段是否在视口内,不在则禁用渲染:

public class WaveformCulling : MonoBehaviour
{
    private ScrollRect scrollRect;
    private RectTransform viewportRect;
    private RectTransform contentRect;
    private List<RectTransform> chunkRects = new List<RectTransform>();

    void Start()
    {
        scrollRect = GetComponentInParent<ScrollRect>();
        viewportRect = scrollRect.viewport;
        contentRect = scrollRect.content;

        // 收集所有波形片段的RectTransform
        foreach (Transform child in contentRect)
        {
            chunkRects.Add(child.GetComponent<RectTransform>());
        }

        // 监听滚动事件,减少不必要的帧更新
        scrollRect.onValueChanged.AddListener(OnScrollChanged);
    }

    void OnScrollChanged(Vector2 scrollPos)
    {
        // 获取视口在世界空间的矩形
        Rect viewportWorldRect = GetWorldRect(viewportRect);

        foreach (var chunkRect in chunkRects)
        {
            Rect chunkWorldRect = GetWorldRect(chunkRect);
            bool isVisible = viewportWorldRect.Overlaps(chunkWorldRect);

            // 启用/禁用Image组件(或直接禁用GameObject)
            chunkRect.GetComponent<Image>().enabled = isVisible;
        }
    }

    // 将RectTransform转换为世界空间矩形
    private Rect GetWorldRect(RectTransform rectTransform)
    {
        Vector3[] corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);
        float left = corners[0].x;
        float right = corners[2].x;
        float bottom = corners[0].y;
        float top = corners[2].y;
        return new Rect(left, bottom, right - left, top - bottom);
    }
}

注意事项

  • 选择合适的chunkWidth:建议设为2048或4096,既符合Unity纹理限制,又能减少片段数量。
  • 内存优化:如果波形片段过多,可以考虑对象池复用,避免频繁创建销毁GameObject。
  • 性能优化:通过监听Scroll Rect的onValueChanged事件替代Update,减少不必要的计算。

内容的提问来源于stack exchange,提问作者Shapdargame

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 08:35:21