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

Unity项目中通过C++原生插件拷贝纹理至主内存以规避GC频繁触发

刚好做过类似的需求,给你一套完整的实现方案,解决Texture2D.GetRawTextureData()每帧触发GC的问题:

核心思路

要避免频繁内存分配,核心是预先分配一块固定大小的非托管内存缓冲区,每帧只将纹理数据拷贝到这块缓冲区中,而不是每次都新建数组。我们通过Unity的原生插件接口直接从GPU纹理映射到CPU可访问内存,跳过托管内存的频繁分配。

具体实现步骤

1. Unity C# 端代码

负责预分配非托管内存、调用原生插件函数,以及在生命周期结束时释放内存:

using System;
using System.Runtime.InteropServices;
using UnityEngine;

public class TextureNativeCopier : MonoBehaviour
{
    [DllImport("YourNativePlugin")] // 替换成你的插件名(比如Windows下是YourNativePlugin.dll)
    private static extern void CopyTextureToNativeBuffer(IntPtr nativeTexPtr, IntPtr destBuffer, int bufferSize);

    private IntPtr _nativeBuffer;
    private int _textureDataSize;
    private Texture2D _targetTexture;

    void Start()
    {
        // 获取目标纹理(这里以材质的主纹理为例,你可以替换成自己的纹理)
        _targetTexture = GetComponent<Renderer>().material.mainTexture as Texture2D;
        if (_targetTexture == null)
        {
            Debug.LogError("Target texture is missing or not a Texture2D!");
            return;
        }

        // 计算纹理数据总大小(根据纹理格式动态计算字节数)
        int bytesPerPixel = GetBytesPerPixel(_targetTexture.format);
        _textureDataSize = _targetTexture.width * _targetTexture.height * bytesPerPixel;

        // 预分配非托管内存(不会触发GC)
        _nativeBuffer = Marshal.AllocHGlobal(_textureDataSize);
    }

    void Update()
    {
        if (_targetTexture == null || _nativeBuffer == IntPtr.Zero) return;

        // 调用原生插件拷贝纹理数据到预分配缓冲区
        CopyTextureToNativeBuffer(_targetTexture.GetNativeTexturePtr(), _nativeBuffer, _textureDataSize);

        // (可选)如果需要在C#端处理数据,再从非托管缓冲区拷贝到托管数组
        // 注意:尽量减少这一步,因为拷贝会有开销,能在C++端处理就直接在C++做
        byte[] managedData = new byte[_textureDataSize];
        Marshal.Copy(_nativeBuffer, managedData, 0, _textureDataSize);
        // 这里添加你的数据处理逻辑...
    }

    void OnDestroy()
    {
        // 必须释放非托管内存,避免泄漏
        if (_nativeBuffer != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_nativeBuffer);
            _nativeBuffer = IntPtr.Zero;
        }
    }

    // 辅助函数:根据纹理格式计算每个像素的字节数
    private int GetBytesPerPixel(TextureFormat format)
    {
        return format switch
        {
            TextureFormat.RGBA32 => 4,
            TextureFormat.RGB24 => 3,
            TextureFormat.R16 => 2,
            TextureFormat.RFloat => 4,
            TextureFormat.RGBAHalf => 8,
            // 其他格式自行补充
            _ => 4
        };
    }
}

2. C++ 原生插件代码

需要引入Unity的UnityRendering.h头文件(可以在Unity安装目录的Editor/Data/PluginAPI/路径下找到),通过Unity提供的渲染接口映射纹理并拷贝数据:

#include "UnityRendering.h"
#include <cstring>

// 导出函数给Unity调用,必须用extern "C"避免名字修饰
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API
CopyTextureToNativeBuffer(void* nativeTexPtr, void* destBuffer, int bufferSize)
{
    // 将Unity传入的原生纹理指针转为UnityRenderingTexture结构体
    UnityRenderingTexture* texture = static_cast<UnityRenderingTexture*>(nativeTexPtr);
    
    // 映射纹理到CPU可访问内存(只读模式)
    void* textureData = UnityRenderingTextureMap(texture, kUnityRenderingTextureAccess_ReadOnly);
    if (textureData != nullptr)
    {
        // 拷贝纹理数据到预分配的缓冲区
        memcpy(destBuffer, textureData, bufferSize);
        
        // 必须解除映射,否则GPU无法正常访问该纹理
        UnityRenderingTextureUnmap(texture);
    }
}
关键注意事项
  • 纹理可读权限:确保你的Texture2D在创建时勾选了Read/Write Enabled,否则无法映射到CPU内存。如果是RenderTexture,需要设置enableRandomWrite或者改用异步读取方案。
  • 格式匹配:一定要确保C#端计算的bufferSize和纹理实际格式一致,否则会出现拷贝越界或数据错误。
  • 线程安全:上述代码是在主线程的Update中调用的,是安全的。如果要在异步线程处理,需要额外做同步处理,或者改用Unity的AsyncGPUReadback接口配合原生插件。
  • 内存泄漏:务必在OnDestroy中释放非托管内存,否则会持续占用系统内存。
性能优化建议
  • 减少托管/非托管拷贝:如果你的数据处理逻辑可以放在C++中完成,直接在原生插件里处理destBuffer的数据,不用再拷贝回C#的托管数组,能大幅减少开销。
  • 异步读取:如果项目使用Unity 2019及以上版本,可以结合AsyncGPUReadback.Request实现异步纹理读取,避免阻塞主线程,进一步提升帧率。
  • 压缩纹理处理:如果使用了压缩纹理格式(如ETC2、ASTC),UnityRenderingTextureMap会自动返回解压后的数据,但性能会有损耗。如果追求极致性能,建议将纹理转为非压缩格式(如RGBA32)再处理。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 08:44:41