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
相关产品推荐
相关产品推荐

