unity截屏截图录屏
实现方式
-
获取屏幕纹理
- 使用 Texture2D.ReadPixels
缺点是不能在GPU上缩放后再取回数据,只能取回数据后再缩放,分辨率大的情况下会卡 - ScreenCapture.CaptureScreenshotIntoRenderTexture
直接在GPU中复制图像,速度快,但要注意图像是上下颠倒的 - 使用 ScreenCapture.CaptureScreenshot
最简单,但是直接保存成文件
- 使用 Texture2D.ReadPixels
-
获取相机纹理
- 使用 Camera.Render 渲染到纹理
缺点是相机渲染2遍,效率低 - URP使用 CameraCaptureBridge.AddCaptureAction,内置管线使用 Camera.AddCommandBuffer
直接抓取相机渲染的画面,注意只对最后一个相机生效,并且会叠加前面的相机
urp7.7.1有问题
- 使用 Camera.Render 渲染到纹理
优化建议
无论是哪种方法,其瓶颈都在于从GPU取回数据到CPU,也就是 Texture2D.ReadPixels
有以下几种方案可以优化性能
- 最简单有效的就是减小图像分辨率,也就是缩放,而且这个操作必须在GPU上完成
比如使用 ScreenCapture.CaptureScreenshotIntoRenderTexture 抓取纹理,
然后使用 Graphics.Blit 缩放纹理,这些都是在 GPU 上完成的 - 最实用的是把纹理句柄传给原生端去取数据,这个也是会快一些的,截图时一般不需要,但录屏需要
原生端用Unity的OPENGL配置初始化OPENGL环境,然后使用传递过来的纹理句柄渲染到图片,再取像素数据 - 最高效的是使用 AsyncGPUReadback.Request 异步取回数据
参考 获取 RenderTexture 图像数据
但是该函数在很多手机平台上不支持
常见问题
-
ScreenCapture.CaptureScreenshotIntoRenderTexture 截取图像失败
- 该函数在 URP 7.7.1 中开启 UniversalRenderPipelineAsset 的 MSAA 后,如果传入的 RenderTexture 创建时设置了
m_texture.antiAliasing = QualitySettings.antiAliasing;
则抓取的是已经释放的图像,只要不设置 antiAliasing 就可以,在 7.3.1 中则无此问题 - 采用URP7.7.1,在编辑器模式下,如果调用该函数后调用了 Graphics.Blit 函数,要还原 RenderTexture.active,否则会黑屏
- 该函数在 URP 7.7.1 中开启 UniversalRenderPipelineAsset 的 MSAA 后,如果传入的 RenderTexture 创建时设置了
-
CameraCaptureBridge.AddCaptureAction 抓屏卡住
- 该函数在 URP 7.3.1 和 7.7.1 下如果相机勾选了 Post Processing ,则录屏时屏幕卡住
这个是URP代码自身的问题,参考 URP管线自身代码问题
- 该函数在 URP 7.3.1 和 7.7.1 下如果相机勾选了 Post Processing ,则录屏时屏幕卡住
-
录制视频时偏暗
如果unity采用Linear颜色空间,则渲染纹理时是在线性空间计算,最后上屏时才转成Gamma空间,而录制视频需要手动把纹理转换成Gamma空间,否则会变暗
代码实现
- 重点推荐用 ScreenCapture.CaptureScreenshotIntoRenderTexture 截取图像,缩放后取回
public class TestScreenShot : MonoBehaviour { public Button RecordBtn; public RawImage ScreenImage; RenderTexture m_renderTexture; RenderTexture m_targetTexture; Texture2D m_captureTexture; // Start is called before the first frame update void Start() { RecordBtn.onClick.AddListener(OnRecordClick); } private void OnRecordClick() { StartCoroutine(RecordCoroutine2()); } // 截屏方式1: 使用 Texture2D.ReadPixels // 缺点是不能在GPU上缩放后再取回数据,只能取回数据后再缩放,分辨率大的情况下会卡 IEnumerator RecordCoroutine() { // 必须等渲染完成,否则会报 // ReadPixels was called to read pixels from system frame buffer, while not inside drawing frame yield return new WaitForEndOfFrame(); Texture2D texture = GetCaptureTexture(Screen.width, Screen.height); // ReadPixels 从 GPU 取回图像数据,比较耗时 // 不会进行任何缩放,截取屏幕矩形区域,然后把该矩形直接帖到纹理上,超出范围的被裁剪 // destX 和 destY 是像素坐标 texture.ReadPixels(new Rect(0, 0, Screen.width / 2, Screen.height / 2), 0, 0, false); // Apply 把图像数据上传 GPU,比较耗时,如果是保存到图片,则不需要调用,如果是给 RawImage 显示,则需要调用 texture.Apply(); SaveTexture(texture); } // 截屏方式2(最高效): 使用 ScreenCapture.CaptureScreenshotIntoRenderTexture // 截取的是GPU图像,还未取回CPU,所以非常高效,配合 Graphics.Blit 进行缩放再取回数据,分辨率再大也不卡 IEnumerator RecordCoroutine2() { // 必须等渲染完成,否则会报 // ReadPixels was called to read pixels from system frame buffer, while not inside drawing frame yield return new WaitForEndOfFrame(); RenderTexture screen = GetRenderTexture(Screen.width, Screen.height); // 截取屏幕图像存在 GPU中,速度快,但要注意图像是上下颠倒的 // 在 Editor 模式下,横屏场景中的相机录出来变形,原因可能是 Editor 模式下 Screen.orientation 永远是 Portrait // 抓取失败参考上面的 常见问题 ScreenCapture.CaptureScreenshotIntoRenderTexture(screen); RenderTexture target = GetTargetTexture(Screen.width/2, Screen.height/2); // 直接在 GPU 中缩放,速度快 // Blit 操作是进行纹理映射,target 上的纹理坐标uv的颜色 color=texture(sourceTexture,uv*scale+offset) // 由于截屏是上下颠倒的,通过设置 scale=(1,-1) offset=(0,1) 可以实现上下颠倒,摆正图像 // Blit 会设置 RenderTexture.active,所以调用完要设置 active=null,否则target可能会被其它代码写入其它东西 Graphics.Blit(screen, target, new Vector2(1f, -1f), new Vector2(0f,1f)); RenderTexture.active = null; SaveTexture(target); } // 截屏方式3(最简单): 使用 ScreenCapture.CaptureScreenshot IEnumerator RecordCoroutine3() { // 必须等渲染完成,否则会报 // ReadPixels was called to read pixels from system frame buffer, while not inside drawing frame yield return new WaitForEndOfFrame(); ScreenCapture.CaptureScreenshot(SAVE_IMAGE_PATH); } public string SAVE_IMAGE_PATH => Path.Combine(Application.persistentDataPath, "ScreenShot.png"); void SaveTexture(Texture texture) { ScreenImage.texture = texture; Texture2D saveTexture = null; if ( texture is Texture2D t2d ) { saveTexture = t2d; } else if (texture is RenderTexture rt) { saveTexture = GetCaptureTexture(texture.width, texture.height); RenderTexture old = RenderTexture.active; RenderTexture.active = rt; saveTexture.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0, false); RenderTexture.active = old; } byte[] data = saveTexture.EncodeToPNG(); File.WriteAllBytes(SAVE_IMAGE_PATH, data); Debug.Log($"ScreenShot SaveToPng {SAVE_IMAGE_PATH}"); } RenderTexture GetRenderTexture(int width, int height) { if ( m_renderTexture != null && m_renderTexture.width == width && m_renderTexture.height == height ) return m_renderTexture; if ( m_renderTexture != null ) { Destroy(m_renderTexture); } m_renderTexture = CreateRenderTexture(width, height, 0); return m_renderTexture; } RenderTexture GetTargetTexture(int width, int height) { if (m_targetTexture != null && m_targetTexture.width == width && m_targetTexture.height == height) return m_targetTexture; if (m_targetTexture != null) { Destroy(m_targetTexture); } m_targetTexture = CreateRenderTexture(width, height, 0); return m_targetTexture; } Texture2D GetCaptureTexture(int width, int height) { if (m_captureTexture != null && m_captureTexture.width == width && m_captureTexture.height == height) return m_captureTexture; if (m_captureTexture != null) Destroy(m_captureTexture); m_captureTexture = new Texture2D(width, height, TextureFormat.RGBA32, false, false); return m_captureTexture; } public static RenderTexture CreateRenderTexture(int width, int height, int depth = 24, RenderTextureFormat format = RenderTextureFormat.ARGB32, bool usequaAnti = true) { var rt = new RenderTexture(width, height, depth, format); rt.wrapMode = TextureWrapMode.Clamp; if (QualitySettings.antiAliasing > 0 && usequaAnti) { rt.antiAliasing = QualitySettings.antiAliasing; } rt.Create(); return rt; } }
截取相机图像
- 重点推荐用 CameraCaptureBridge.AddCaptureAction 直接抓取相机画面
public class TestCameraShot : MonoBehaviour { public Button RecordBtn; public RawImage ScreenImage; public Camera RecordCamera; RenderTexture m_renderTexture; Texture2D m_captureTexture; // Start is called before the first frame update void Start() { RecordCamera = RecordCamera ?? Camera.main; RecordBtn.onClick.AddListener(OnRecordClick); } private void OnRecordClick() { StartCoroutine(RecordCoroutine2()); } // 截屏方式1: 使用 Camera.Render // 缺点是相机渲染2遍,效率低 IEnumerator RecordCoroutine() { // 必须等渲染完成,否则会报 // ReadPixels was called to read pixels from system frame buffer, while not inside drawing frame yield return new WaitForEndOfFrame(); RenderTexture texture = GetRenderTexture(Screen.width, Screen.height); RenderTexture old = RecordCamera.targetTexture; RecordCamera.targetTexture = texture; RecordCamera.Render(); RecordCamera.targetTexture = old; SaveTexture(texture); } // 截屏方式2(最高效): URP使用 CameraCaptureBridge.AddCaptureAction,内置管线使用 Camera.AddCommandBuffer // 直接抓取相机渲染的画面 // 抓取问题参考上面的 常见问题 IEnumerator RecordCoroutine2() { RenderTexture renderTexture = GetRenderTexture(Screen.width, Screen.height); if (GraphicsSettings.renderPipelineAsset == null ) { // 内置管线 CommandBuffer cb = new CommandBuffer { name = "Record: copy frame buffer" }; // 内置管线抓取的是颠倒的,要翻转下 AddCaptureCommands(BuiltinRenderTextureType.CurrentActive, cb, renderTexture, true); // 向相机添加渲染指令 RecordCamera.AddCommandBuffer(CameraEvent.AfterEverything, cb); yield return null; RecordCamera.RemoveCommandBuffer(CameraEvent.AfterEverything, cb); cb.Release(); } else { // URP管线 bool record = false; void AddCaptureCommandsWrap(RenderTargetIdentifier source, CommandBuffer cb) { // URP截取的图像是正常的 AddCaptureCommands(source, cb, renderTexture, false); record = true; } // 添加抓取相机画面指令 // 抓取失败参考上面的 常见问题 CameraCaptureBridge.AddCaptureAction(RecordCamera, AddCaptureCommandsWrap); yield return new WaitUntil(() => record); CameraCaptureBridge.RemoveCaptureAction(RecordCamera, AddCaptureCommandsWrap); } SaveTexture(renderTexture); } // 添加抓取渲染画面的指令 protected void AddCaptureCommands(RenderTargetIdentifier source, CommandBuffer cb, RenderTexture renderTexture, bool flipY) { if (source == BuiltinRenderTextureType.CurrentActive) { // 理论上应该不会进到这里,CurrentActive 只能做为目标,不会做为源传进来,就算进到这里下面的代码也是没用的 var tid = Shader.PropertyToID("_MainTex"); cb.GetTemporaryRT(tid, renderTexture.width, renderTexture.height, 0, FilterMode.Bilinear); cb.Blit(source, tid); if (flipY) cb.Blit(tid, renderTexture, new Vector2(1, -1), new Vector2(0, 1)); else cb.Blit(tid, renderTexture); cb.ReleaseTemporaryRT(tid); } else { if (flipY) cb.Blit(source, renderTexture, new Vector2(1, -1), new Vector2(0, 1)); else cb.Blit(source, renderTexture); } } public string SAVE_IMAGE_PATH => Path.Combine(Application.persistentDataPath, "ScreenShot.png"); void SaveTexture(Texture texture) { ScreenImage.texture = texture; Texture2D saveTexture = null; if (texture is Texture2D t2d) { saveTexture = t2d; } else if (texture is RenderTexture rt) { saveTexture = GetCaptureTexture(texture.width, texture.height); RenderTexture old = RenderTexture.active; RenderTexture.active = rt; saveTexture.ReadPixels(new Rect(0, 0, texture.width, texture.height), 0, 0, false); RenderTexture.active = old; } byte[] data = saveTexture.EncodeToPNG(); File.WriteAllBytes(SAVE_IMAGE_PATH, data); Debug.Log($"ScreenShot SaveToPng {SAVE_IMAGE_PATH}"); } RenderTexture GetRenderTexture(int width, int height) { if (m_renderTexture != null && m_renderTexture.width == width && m_renderTexture.height == height) return m_renderTexture; if (m_renderTexture != null) { Destroy(m_renderTexture); } m_renderTexture = CreateRenderTexture(width, height, 0); return m_renderTexture; } Texture2D GetCaptureTexture(int width, int height) { if (m_captureTexture != null && m_captureTexture.width == width && m_captureTexture.height == height) return m_captureTexture; if (m_captureTexture != null) Destroy(m_captureTexture); m_captureTexture = new Texture2D(width, height, TextureFormat.RGBA32, false, false); return m_captureTexture; } public static RenderTexture CreateRenderTexture(int width, int height, int depth = 24, RenderTextureFormat format = RenderTextureFormat.ARGB32, bool usequaAnti = true) { var rt = new RenderTexture(width, height, depth, format); rt.wrapMode = TextureWrapMode.Clamp; if (QualitySettings.antiAliasing > 0 && usequaAnti) { rt.antiAliasing = QualitySettings.antiAliasing; } rt.Create(); return rt; } }