文章目录
- 一、前言
- 二、思路
- (一)直接开一张小的 RenderTexture?不行
- (二)整屏渲染,读取的时候挑地方
- 三、完整代码
- 四、代码讲解
- (一)中间区域的坐标怎么算
- (二)ReadPixels 只读一块
- (三)收尾工作
- 五、注意事项
- 六、结语
一、前言
上一篇文章讲的长截图,有长截图就有短截图,或者区域截图,这篇文章就是说的区域截图,而且比长截图简单得多——不用滚动、不用拼接、不用算零头,渲染还是完整渲染一遍,只是读取像素的时候指定一个区域就行。比如只截游戏画面中间 500*400 的区域,把边缘的 UI 完美避开,用的就是这一招。
那么话不多说,直接开始今天的内容吧!
二、思路
(一)直接开一张小的 RenderTexture?不行
肯定有同学会想:截 500400 的区域,那我把 RenderTexture 直接开成 500400 不就行了?
不行👇👇👇。相机渲染到一张小的 RenderTexture 上,是把整个视野缩放着塞进去,相当于给画面打了个缩小镜头,并不是裁剪。不信的话可以自己试,截出来该有的东西一样不少,只是全都变小了。
所以裁剪这件事,只能在读像素这一步做。
(二)整屏渲染,读取的时候挑地方
正解分两步:
- RenderTexture 该开多大还开多大,和屏幕保持一致,相机完整渲染一遍;
ReadPixels的时候传一个Rect,只读中间那块区域的像素,读出来的纹理就只有这么大。
一句话概括:渲染要完整,读取挑地方。下面直接上代码。
三、完整代码
usingSystem.IO;usingUnityEngine;publicclassShotPartScreen:MonoBehaviour{publicCameracamera1;// 用来截图的相机publicGameObjectbutton1;// 截图期间要藏起来的按钮/// <summary>/// 截取屏幕中间 500*400 的区域/// </summary>publicvoidStartShotPart(){button1.SetActive(false);StartCoroutine(ShotPartTexture());}privateIEnumeratorShotPartTexture(){// 和上一期长截图一个道理:等到帧结束,画面渲染完了再动手yieldreturnnewWaitForEndOfFrame();// 创建一张和屏幕一样大的 RenderTexture,作为相机的渲染目标RenderTexturerenderTexture=newRenderTexture(Screen.width,Screen.height,24);camera1.targetTexture=renderTexture;camera1.Render();// 计算屏幕中间 500*400 区域的坐标(注意坐标原点在左下角)intcenterX=Screen.width/2;intcenterY=Screen.height/2;inthalfWidth=250;// 500的一半inthalfHeight=200;// 400的一半RectmiddleRect=newRect(centerX-halfWidth,centerY-halfHeight,500,400);// 把 RenderTexture 设为激活状态,然后只读取中间区域的像素RenderTexture.active=renderTexture;Texture2Dtexture2D=newTexture2D(500,400,TextureFormat.RGB24,false);texture2D.ReadPixels(middleRect,0,0);texture2D.Apply();// 收尾:激活状态和渲染目标都还原,RenderTexture 该释放就释放RenderTexture.active=null;camera1.targetTexture=null;renderTexture.Release();// 保存成 PNGstringpath=Path.Combine(Application.dataPath,"SavedScreen.png");File.WriteAllBytes(path,texture2D.EncodeToPNG());Debug.Log($"xiao=====:{path}");// 把藏起来的按钮恢复回去button1.SetActive(true);}}四、代码讲解
(一)中间区域的坐标怎么算
思路很直白:屏幕中心点减去半个宽度和高度,拿到矩形的左下角。
intcenterX=Screen.width/2;intcenterY=Screen.height/2;inthalfWidth=250;// 500的一半inthalfHeight=200;// 400的一半RectmiddleRect=newRect(centerX-halfWidth,centerY-halfHeight,500,400);这里有一个坑要特别注意:屏幕和 RenderTexture 的坐标原点在左下角,y 轴向上。不少同学习惯性地按左上角为原点去算,算出来的位置直接就偏了。自检方法很简单:算出来的 x、y 应该都是正数,且x + 500、y + 400不超过屏幕宽高,那就对了。
(二)ReadPixels 只读一块
ReadPixels的三个参数:从哪读、贴到哪、贴多大区域。
texture2D.ReadPixels(middleRect,0,0);意思就是:从当前激活的 RenderTexture 里middleRect这块区域读像素,贴到纹理的(0, 0)处。纹理本身开的是 500*400,和middleRect的尺寸一一对应,刚好贴满。
这里要强调原话:ReadPixels 里的坐标和尺寸是相对于 RenderTexture 的,不是相对于屏幕的。本例中 RenderTexture 和屏幕一样大,所以直接用屏幕坐标没问题;如果你的 RenderTexture 是别的尺寸(比如为了性能开了一半分辨率),坐标就要按比例换算,不能直接套屏幕坐标。
另外,Texture2D 的尺寸必须和 Rect 的尺寸一致,不一致 ReadPixels 会直接给你脸色看(读取异常或者花屏),这个没有商量。
(三)收尾工作
截图这活,重点都在收尾,三件事漏一件就出事:
RenderTexture.active = null。不清掉的话,后面所有正常渲染都会写到这张 RenderTexture 上,游戏画面直接消失,这个坑踩过的人都懂。renderTexture.Release()。RenderTexture 占的是 GPU 内存,不释放就一直攒着,截得越多爆得越快。button1.SetActive(true)。截图前把按钮藏了,截完记得放出来,不然用户对着空气点半天。
哦对了,原来代码里保存路径是这么写的:
File.WriteAllBytes(Application.dataPath+" /SavedScreen.png",bytes);引号里面多打了一个空格,存出来的文件名前面带空格,路径看着没毛病,找文件的时候能给你找懵。所以优化版换成了Path.Combine,分隔符也不用自己操心了:
stringpath=Path.Combine(Application.dataPath,"SavedScreen.png");五、注意事项
- 坐标原点在左下角,算中间区域的时候别按左上角算,这是最常见的错。
- Texture2D 尺寸必须和 Rect 尺寸一致,都是 500400,不能一个 500400 一个 600*400。
- 截取区域别超出屏幕范围。现在机型分辨率五花八门,小屏机上 500*400 也可能贴边,需要的话按屏幕尺寸做个 clamp,把宽高夹一下。
- RenderTexture 用完必须释放,active 状态必须还原,上面说过了,这里再啰嗦一遍。
- 路径按平台区分。Editor 里
Application.dataPath指向 Assets 目录,真机上更建议用Application.persistentDataPath。 - 老生常谈:如果 UI 是 Screen Space - Overlay,相机渲染抓不到 UI,得换成等帧结束直接读屏幕后备缓冲的思路,这个在长截图那期说过,这里不重复了。
六、结语
局部截图比长截图简单得多——不用滚、不用拼,RenderTexture 完整渲染一遍,ReadPixels 指定个 Rect 就完事了。真正的坑全在细节里:坐标原点、尺寸匹配、释放还原,这三件事忘了任何一件,轻则截图截歪了,重则画面全黑。
建议大家照着代码自己敲一遍,顺手把 500*400 改成别的尺寸试试,看看截出来的位置对不对。
如果在实践的过程中遇到什么问题,欢迎在评论区留言,我看到的话会及时回复。这期的内容就到这里,下期见,拜拜!