如何访问Unity Camera临时渲染缓冲区?Android帧存储内存问题求解
刚好做过类似的需求,来给你梳理一下这两个问题的解决方案:
Unity Camera渲染的中间临时缓冲区,其实就是Camera最终输出的源纹理,你可以通过两种常用方式获取:
1. 使用OnRenderImage回调(最直接)
这是Unity提供的专门用于处理Camera渲染后纹理的回调,每帧Camera完成渲染后都会触发,参数里的src就是你要找的那个临时缓冲区纹理。
只需要写一个脚本挂在目标Camera上,实现这个方法就能拿到每帧的渲染结果:
using UnityEngine; public class CameraFrameCapture : MonoBehaviour { void OnRenderImage(RenderTexture src, RenderTexture dest) { // 务必先把源纹理输出到屏幕,否则画面会黑屏 Graphics.Blit(src, dest); // 这里的src就是Camera渲染的临时缓冲区,你可以在这里做任何处理 // 比如读取像素、复制到其他纹理等 Debug.Log($"当前帧分辨率:{src.width}x{src.height}"); } }
2. 使用CommandBuffer注入渲染流程
如果需要在Camera渲染的某个特定阶段(比如BeforeImageEffects、AfterSkybox)获取缓冲区,可以用CommandBuffer提前插入渲染命令,把当前的渲染目标复制出来:
using UnityEngine; public class CommandBufferCapture : MonoBehaviour { private CommandBuffer cmdBuffer; private RenderTexture tempRT; void Start() { cmdBuffer = new CommandBuffer(); cmdBuffer.name = "CaptureCameraFrame"; // 创建临时RenderTexture用来存储复制的帧 tempRT = new RenderTexture(Screen.width, Screen.height, 0); // 在Camera渲染完成后,把当前渲染目标复制到tempRT cmdBuffer.Blit(BuiltinRenderTextureType.CameraTarget, tempRT); // 将CommandBuffer添加到Camera的AfterImageEffects阶段 GetComponent<Camera>().AddCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer); } void Update() { // 每帧可以从tempRT读取数据 if(tempRT != null) { // 这里可以处理tempRT中的帧数据 } } void OnDestroy() { if(cmdBuffer != null) { GetComponent<Camera>().RemoveCommandBuffer(CameraEvent.AfterImageEffects, cmdBuffer); cmdBuffer.Dispose(); } if(tempRT != null) { tempRT.Release(); } } }
Android移动端内存有限,直接在内存里存所有帧肯定会爆内存,得从压缩、异步、复用这几个角度入手:
1. 实时压缩帧数据,避免存储原始像素
原始RGBA32格式的1080p帧大概占8MB内存,60帧每秒就是480MB/s,完全扛不住。你可以在OnRenderImage里把帧压缩成JPG/WEBP格式,压缩后的1080p帧大概只有100-200KB,内存占用骤降:
// 在OnRenderImage中添加压缩逻辑 Texture2D tempTex = new Texture2D(src.width, src.height, TextureFormat.RGB24, false); RenderTexture.active = src; tempTex.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0); tempTex.Apply(); // 压缩成JPG,质量参数0-100,值越小压缩率越高 byte[] compressedData = tempTex.EncodeToJPG(70); // 用完销毁临时Texture2D,避免内存泄漏 Destroy(tempTex); RenderTexture.active = null;
如果需要更高的压缩率,可以用WEBP格式(需要引入Unity的WebP插件或者第三方库),相同画质下WEBP比JPG小30%左右。
2. 异步写入磁盘,避免主线程卡顿
磁盘IO操作非常耗时,绝对不能在主线程做,否则会导致游戏掉帧。可以把压缩后的帧数据放到线程安全队列里,开启后台线程专门负责写入:
private Queue<byte[]> frameQueue = new Queue<byte[]>(); private object queueLock = new object(); private bool isWriting = true; void Start() { // 开启后台写入线程 System.Threading.Thread writeThread = new System.Threading.Thread(WriteFramesToDisk); writeThread.IsBackground = true; writeThread.Start(); } void WriteFramesToDisk() { int frameCount = 0; string saveDir = Application.persistentDataPath + "/CapturedFrames/"; System.IO.Directory.CreateDirectory(saveDir); while(isWriting) { byte[] frameData = null; // 线程安全地从队列取数据 lock(queueLock) { if(frameQueue.Count > 0) { frameData = frameQueue.Dequeue(); } } if(frameData != null) { string filePath = $"{saveDir}frame_{frameCount:D6}.jpg"; System.IO.File.WriteAllBytes(filePath, frameData); frameCount++; } else { // 队列空时休眠,减少CPU占用 System.Threading.Thread.Sleep(10); } } } // 在OnRenderImage中把压缩后的数据加入队列 lock(queueLock) { frameQueue.Enqueue(compressedData); } // 退出时停止线程 void OnDestroy() { isWriting = false; }
3. 帧采样/降分辨率,进一步降低开销
如果不是必须获取每帧,可以按固定间隔采样(比如每2帧存一帧);或者把渲染帧降分辨率后再存储(比如1080p缩到720p),这样内存和磁盘占用都会大幅降低:
// 创建缩小后的临时RenderTexture RenderTexture smallRT = RenderTexture.GetTemporary(src.width / 2, src.height / 2); Graphics.Blit(src, smallRT); // 读取smallRT的像素进行压缩 Texture2D tempTex = new Texture2D(smallRT.width, smallRT.height, TextureFormat.RGB24, false); RenderTexture.active = smallRT; tempTex.ReadPixels(new Rect(0, 0, smallRT.width, smallRT.height), 0, 0); tempTex.Apply(); // 用完释放临时RenderTexture RenderTexture.ReleaseTemporary(smallRT);
4. 直接用Android原生MediaRecorder录制视频
如果你的最终目的是保存所有帧成视频,没必要自己存单帧,直接调用Android原生的MediaRecorder来录制Camera的渲染画面,系统会帮你处理压缩、编码和存储,效率比自己存帧高得多,内存占用几乎可以忽略。你可以通过AndroidJavaObject调用原生API,或者用Unity的VideoCapture类(Unity 2018+支持)。
5. 内存复用,避免频繁GC
不要每次创建新的Texture2D或RenderTexture,用对象池复用这些资源,减少内存碎片和GC开销:
// 简单的Texture2D对象池示例 private Queue<Texture2D> texPool = new Queue<Texture2D>(); private Texture2D GetPooledTexture(int width, int height) { if(texPool.Count > 0) { Texture2D tex = texPool.Dequeue(); if(tex.width == width && tex.height == height) { return tex; } else { Destroy(tex); } } return new Texture2D(width, height, TextureFormat.RGB24, false); } private void ReturnTextureToPool(Texture2D tex) { lock(texPool) { texPool.Enqueue(tex); } }
内容的提问来源于stack exchange,提问作者MSD Paul

