unity截屏

unity截屏截图录屏

实现方式

  • 获取屏幕纹理

    • 使用 Texture2D.ReadPixels
      缺点是不能在GPU上缩放后再取回数据,只能取回数据后再缩放,分辨率大的情况下会卡
    • ScreenCapture.CaptureScreenshotIntoRenderTexture
      直接在GPU中复制图像,速度快,但要注意图像是上下颠倒的
    • 使用 ScreenCapture.CaptureScreenshot
      最简单,但是直接保存成文件
  • 获取相机纹理

    • 使用 Camera.Render 渲染到纹理
      缺点是相机渲染2遍,效率低
    • URP使用 CameraCaptureBridge.AddCaptureAction,内置管线使用 Camera.AddCommandBuffer
      直接抓取相机渲染的画面,注意只对最后一个相机生效,并且会叠加前面的相机
      urp7.7.1有问题

优化建议

无论是哪种方法,其瓶颈都在于从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,否则会黑屏
  • CameraCaptureBridge.AddCaptureAction 抓屏卡住

    • 该函数在 URP 7.3.1 和 7.7.1 下如果相机勾选了 Post Processing ,则录屏时屏幕卡住
      这个是URP代码自身的问题,参考 URP管线自身代码问题
  • 录制视频时偏暗
    如果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;
            }
    
        }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值