unity图像处理
图像处理API
-
Graphics
-
Blit
纹理通过材质渲染到目标,纹理设为材质的 _MainTex 属性
将 dest 设置为渲染目标,为null时直接渲染到屏幕,使用后RenderTexture.active变成dest
该操作在GPU上复制纹理,速度很快
在 android 上如果不生效,尝试创建 RenderTexture 时深度值设为0
Blit是纹理操作,所有参数都是针对纹理坐标,scale 和 offset 用来设置uv偏移和缩放,
target 上的纹理坐标uv的颜色 color=texture(sourceTexture,uv*scale+offset)- 比如 上下颠倒
Blit(screen, target, new Vector2(1f, -1f), new Vector2(0f,1f)); - 比如 截取原纹理的 (x,y,w,h) 区域,值都是百分比的形式,比如 (0.2,0.2,0.6,0.6)
Blit(screen, target, new Vector2(w,h), new Vector2(x,y));
- 比如 上下颠倒
-
ConvertTexture
在不同格式和尺寸的纹理之间进行转换的有效方式,可以理解为直接对纹理数据进行加工,因此源和目标都是 Texture2D
注意 Blit 目标是 RenderTexture
ConvertTexture 对目标格式也有要求,必须是 RenderTarget 支持的格式,目前只测出支持 ARBG32
请注意,由于 API 限制,DX9 或 Mac+OpenGL 中不支持此函数 -
CopyTexture
高效复制,大小必须相同,格式必须兼容
不是所有平台都支持,必须先用 SystemInfo.copyTextureSupport 检测是否支持
如果源和目标纹理都标记为“可读”(即系统内存中存在 用于 CPU 读取/写入的数据副本),这些函数也将复制它
否则只复制gpu数据 -
DrawMesh 绘制网格,只是提交到渲染队列
-
DrawTexture 在屏幕坐标系中绘制纹理,只能在OnGUI以及之后的生命周期方法中调用,否则无法显示。
-
SetRenderTarget 设置当前渲染目标
-
preserveFramebufferAlpha 渲染缓存保存alpha值
-
-
Texture2D
- ReadPixels
读取屏幕缓存到Texture2D,该函数从GPU上读取像素到CPU,比较耗时
如果先设置 RenderTexture.active=XXX; 则读取的就是 XXX 这个 RenderTexture
如果只是用来获取像素数据,不需要渲染,则不要调用 Texture2D.Apply ,因为Apply把像素数据从CPU上传到GPU,很耗时
如果要用来渲染,则要调用 Texture2D.Apply
该函数按像素拷贝,不会进行图像缩放,所有参数都是以像素为单位,[0,0] 表示左下
不适合大分辨率的录屏操作,会卡,因为不能在GPU端缩放完再读回CPU
可以用 ScreenCapture.CaptureScreenshotIntoRenderTexture - EncodeToPNG 编码成图像文件格式
- EncodeToJPG
- ReadPixels
-
ImageConversion 图像编解码
- EncodeToPNG
- EncodeToJPG
- UnsafeEncodeNativeArrayToPNG
- UnsafeEncodeNativeArrayToJPG
-
RenderTexture 只包含GPU上的数据,要获取cpu上的数据(比如像素字节数组),需要使用 Texture2D.ReadPixels 从 GPU 传到 CPU
-
ScreenCapture 截屏
- CaptureScreenshot 截屏保存成png
- CaptureScreenshotAsTexture 截屏保存成 Texture2D
- CaptureScreenshotIntoRenderTexture
- 截屏保存成 RenderTexture,速度非常快,因为是GPU传到GPU,
- 由于数据存在GPU,可以非常方便的对图像进行操作,比如用 Blit 进行缩放,
- 截取黑屏,或无数据,参考 截屏常见问题
- 由于截取的图是上下颠倒的,因此颠倒回来 Graphics.Blit(screen, target, new Vector2(1f, -1f), new Vector2(0f,1f));
- 如果用 Texture2D.ReadPixels 截屏再缩放,则要先从GPU传到CPU,再从CPU传到GPU,效率会非常低
-
WebCamTexture
捕获手机相机画面,可以参考 OpenCVForUnity\org\opencv\unity\helper\WebCamTextureToMatHelper.cs -
GraphicsFormatUtility 格式转换
- GetGraphicsFormat 把 TextureFormat 或 RenderTextureFormat 转成 GraphicsFormat
- GetRenderTextureFormat 把 graphicsFormat 转成 RenderTextureFormat
- GetComponentCount 获得图像通道数
图像格式转换
- TextureFormat 转 GraphicsFormat
GraphicsFormatUtility.GraphicsFormat - GraphicsFormat 转 RenderTextureFormat
GraphicsFormatUtility.GetRenderTextureFormat
计算图像通道数
图像处理的一般流程
- 渲染到 RenderTexture
使用 Graphics.Blit
或者 设置 Camera.targetTexture 然后调用 Camera.Render() - 用 RenderTexture 生成 Texture2D
使用 Texture2D.ReadPixels - 将 Texture2D 保存成文件或设置给 RawImage 进行显示
使用 Texture2D.EncodeToPNG - 截屏
使用 Texture2D.ReadPixels(慢) 或 ScreenCapture.CaptureScreenshotIntoRenderTexture(快)
动态创建材质
// shaderName 是可以在任何材质的着色器弹出窗口中看到的名称,例如“Standard”、“Unlit/Texture”、“Legacy Shaders/Diffuse”等
// 场景中必须有对象引用了着色器,否则着色器有可能缺失
// 可以直接把着色器添加到 ProjectSettings->Graphics->Built-in Shader Settings->Always included shaders
// 如果只是某个平台用,可以加到 ProjectSettings->Player->Android->Other Settings->PreloadAssets
Shader shader = Shader.Find(shaderName);
Material material = new Material(shader);
创建RenderTexture
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;
}
RenderTexture rt = CreateRenderTexture(1024,720,24,RenderTextureFormat.ARGB32);
// 如果是临时使用,经常使用
RenderTexture renderTexture = RenderTexture.GetTemporary(texture2D.width, texture2D.height);
// 使用 renderTexture 后释放
RenderTexture.ReleaseTemporary(renderTexture);
RenderTexture 和 Texture2D 互转
RenderTexture 代表GPU纹理,Texture2D 代表CPU纹理,当要操作纹理时使用 RenderTexture 更快,当要操作数据时使用 Texture2D
-
RenderTexture 转 Texture2D
-
方法一:
Texture2D tex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGB24, false); var prevActive = RenderTexture.active; RenderTexture.active = renderTexture; // ReadPixels 读取当前渲染目标某个区域的像素并写入 tex 中,[0,0] 表示左下 tex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); // Apply 把纹理数据上传给GPU,如果这个纹理只是用来获取像素数据,不需要渲染,则不要调用该函数,因为很耗时 tex.Apply(); RenderTexture.active = prevActive;
-
方法二:
renderTexture.enableRandomWrite = true; Texture2D tex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGB24, false); Graphics.ConvertTexture(renderTexture, tex); //RT要enableRandomWrite,T2D要关闭mipmap,否则报错误
-
-
Texture2D 转 RenderTexture
// 如果只是临时使用,用这个效率更高 RenderTexture renderTexture = RenderTexture.GetTemporary(texture2D.width, texture2D.height); RenderTexture prev = RenderTexture.active; RenderTexture.active = renderTexture; Graphics.Blit(texture2D, renderTexture); RenderTexture.active = prev; RenderTexture.ReleaseTemporary(renderTexture);
获取 RenderTexture 图像数据
// RenderTexture 数据是在 GPU 上,要获取数据必须先传到 cpu 上
byte[] pixelBuffer = new byte[renderTexture.width * renderTexture.height * 4];
if (SystemInfo.supportsAsyncGPUReadback)
// 异步请求,Android 上 opengl3.0 不支持
AsyncGPUReadback.Request(renderTexture, 0, request => {
if (pixelBuffer != null) {
request.GetData<byte>().CopyTo(pixelBuffer);
}
});
else {
// 同步请求,很耗时,至少30ms,渲染复杂场景时会掉到10几帧,暂无解决方案
// 因为cpu要等待gpu完成所有任务后才会读取数据,在这个过程cpu保持阻塞
// 并且由于gpu被设计成很容易从cpu向gpu传输数据远快于gpu向cpu传输数据
var texture2D = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBA32, false, false);
var prevActive = RenderTexture.active;
RenderTexture.active = renderTexture;
texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0, false);
// 只是读取数据不需要调用,apply 是把cpu数据传输到gpu,当你要用 texture2D 来渲染时才需要
//texture2D.apply();
texture2D.GetRawTextureData<byte>().CopyTo(pixelBuffer);
RenderTexture.active = prevActive;
}
纹理和图像互转
主要使用 UnityEngine.ImageConversion
-
纹理编码成文件格式
byte[] data = texture2D.EncodeToTGA(); byte[] data = texture2D.EncodeToJPG(); byte[] data = texture2D.EncodeToPNG(); // 下面这句跟上面那句等价 byte[] bytes = ImageConversion.EncodeArrayToPNG(texture2D.GetRawTextureData(), texture2D.graphicsFormat, (uint)texture2D.width, (uint)texture2D.height);
public void SaveTextureToFile(Texture2D texture, string filePath) { var bytes = texture.EncodeToPNG(); var file = File.Open(filePath, FileMode.Create); var binary = new BinaryWriter(file); binary.Write(bytes); file.Close(); }
-
加载图片成纹理
FileStream fs = new System.IO.FileStream("d:\\1.jpg", System.IO.FileMode.Open, System.IO.FileAccess.Read); byte[] thebytes = new byte[fs.Length]; fs.Read(thebytes, 0, (int)fs.Length); //实例化一个Texture2D,宽和高设置可以是任意的,因为当使用LoadImage方法会对Texture2D的宽和高会做相应的调整 Texture2D texture = new Texture2D(1,1); texture.LoadImage(thebytes); material.mainTexture = texture; material.mainTextureScale = new Vector2(1, 1); material.mainTextureOffset = new Vector2(0, 0);
-
直接设置纹理数据
// 最后一个参数 false 表示不需要 mipmap Texture2D tex = new Texture2D(16, 16, TextureFormat.R8, false); // bytes 长度为 w * h * 每像素占的字节数,如果有 mipmap,还要加上 mipmap 大小 byte[] bytes = new byte[tex.width*tex.height]; for (int y = 0; y < tex.height; y++) { for (int x = 0; x < tex.width; x++) { bytes[index++] = 255; } } if ( true ) { // 整个字节数组都是图像数据 tex.LoadRawTextureData(bytes); } else { // 如果字节数组的某一段是图像数据,可以直接获取纹理内存指针进行拷贝 int offset = 0; int len = bytes.Length; NativeArray<byte> data = tex.GetRawTextureData<byte>(); NativeArray<byte>.Copy(bytes, offset, data, 0, len); } tex.Apply();
-
UI中显示纹理
// 使用 RawImage rawImage.texture = texture2D; // 使用 Image,必须先创建 Sprite Sprite sprite = Sprite.Create(texture2D, new Rect(0, 0, texture2D.width, texture2D.height), new Vector2(0, 0)); image.sprite = sprite;
截图截屏录屏
使用 ReadPixels 拼接图像
RenderTexture prev = RenderTexture.active;
RenderTexture.active = m_RGBSource;
tempdest.ReadPixels(new Rect(0, 0, m_RGBSource.width, m_RGBSource.height), 0, 0);
RenderTexture.active = foresource;
tempdest.ReadPixels(new Rect(0, 0, foresource.width, foresource.height), foresource.width, 0);
tempdest.Apply();
RenderTexture.active = prev;
Graphics.Blit(tempdest, dest);
相机渲染到纹理
var main = Camera.main;
var renderTexture = new RenderTexture(Screen.width, Screen.height, 16);
main.targetTexture = renderTexture;
main.Render();
RenderTexture.active = renderTexture;
Texture2D texture = new Texture2D(Screen.width, Screen.height, TextureFormat.RGBA32, false, false);
texture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0, false);
对纹理进行shader处理
// 这个 shader 我们一般创建时选择 ImageEffectShader
Material material = new Material(Resources.Load<Shader>(shader_name));
RenderTexture renderTexture = RenderTexture.GetTemporary(texture2D.width, texture2D.height);
// Blit 将 dest 设置为渲染目标,在材质上设置 source _MainTex 属性, 并绘制全屏四边形
Graphics.Blit(texture2D, renderTexture, material);
RenderTexture.ReleaseTemporary(renderTexture);
图像拷贝
// 拷贝某个区域
public void CopyImage(Texture2D source, int sx, int sy, int sw, int sh, Texture2D dest, int dx, int dy)
{
Color[] pixels = source.GetPixels(sx, sy, sw, sh);
dest.SetPixels(dx, dy, sw, sh, pixels);
}
图像缩放
-
使用 Graphics.Blit/ConvertTexture 缩放(推荐)
如果只是缩放用 ConvertTexture,如果涉及颜色变换等就用 Blit// 用 ConvertTexture 缩放 // 限制比较多,有些平台不支持,目标格式支持的较少,只支持带alpha的能做为渲染目标的纹理 static Texture2D Resize(Texture2D source, int newWidth, int newHeight) { Texture2D tex = new Texture2D(newWidth, newHeight,TextureFormat.ARGB32,false); Graphics.ConvertTexture(source, tex); return tex; } // 用 Blit 缩放 // ReadPixels 比较耗时,甚至造成卡顿,且暂时没有优化方案 static Texture2D Resize(Texture2D source, int newWidth, int newHeight) { source.filterMode = FilterMode.Point; RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight); rt.filterMode = FilterMode.Point; RenderTexture prev = RenderTexture.active; RenderTexture.active = rt; Graphics.Blit(source, rt); var nTex = new Texture2D(newWidth, newHeight); nTex.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0); // 这步很耗时 nTex.Apply(); // 只是读取数据,不需要调用这句,这句是把数据上传到gpu,渲染用的 RenderTexture.active = prev; RenderTexture.ReleaseTemporary(renderTexture); return nTex; }
-
直接缩放(耗性能)
Texture2D ScaleTexture(Texture2D source, int targetWidth, int targetHeight) { Texture2D result = new Texture2D(targetWidth, targetHeight, source.format, false); float incX = (1.0f / (float)targetWidth); float incY = (1.0f / (float)targetHeight); for (int i = 0; i < result.height; ++i) { for (int j = 0; j < result.width; ++j) { Color newColor = source.GetPixelBilinear((float)j / (float)result.width, (float)i / (float)result.height); result.SetPixel(j, i, newColor); } } result.Apply(); return result; }
-
使用gpu计算(略显复杂)
-
c#代码:
RenderTexture Resize(ComputeShader shader, int divideSize) { RenderTexture t = new RenderTexture(inputTexture.width/ divideSize, inputTexture.height / divideSize, 24, RenderTextureFormat.ARGBFloat); t.enableRandomWrite = true; t.Create(); int k = shader.FindKernel("Resize"); shader.SetInt("divideSize", divideSize); shader.SetTexture(k, "inputTexture", inputTexture); shader.SetTexture(k, "outputTexture", t); shader.Dispatch(k, inputTexture.width / 8, inputTexture.height / 8, 1); return t; }
-
Compute Shader 代码:
#pragma kernel Resize int divideSize; // 原始圖像大小除以這個值 Texture2D inputTexture; RWTexture2D <float4> outputTexture; [numthreads(8, 8, 1)] void Resize(uint3 id : SV_DispatchThreadID) { outputTexture[id.xy / divideSize] = inputTexture[id.xy]; }
-
-
使用IJobParallelFor
- 使用并行计算可能是当前的最优解
跟android共享纹理
-
android端获取EGLContext
在 EGL_VERSION_1_4 (Android 5.0)版本,android端在当前渲染线程直接调用 eglGetCurrentContext 就可以直接获取到上下文对象 EGLContext 。
unity的渲染线程是在多线程中,因此android端必须在对应的渲染线程中执行代码才行class SharedEGL { public static EGLContext s_sharedContext; public static void InitContext() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { // 获取当前线程的 context s_sharedContext = EGL14.eglGetCurrentContext(); } } }
class InitSharedEGL : MonoBehaviour { // 定义渲染线程的委托类型 delegate void RenderEventDelegate(int eventID); void Awake() { // 委托转成 c指针 RenderEventDelegate RenderThreadHandle = new RenderEventDelegate(RunOnRenderThread); IntPtr RenderThreadHandlePtr = Marshal.GetFunctionPointerForDelegate(RenderThreadHandle); // 向渲染线程注册一次回调,2是自定义的回调事件,相当于用户参数 GL.IssuePluginEvent(RenderThreadHandlePtr, 2); } // 定义在渲染线程执行的函数 [MonoPInvokeCallback(typeof(RenderEventDelegate))] private static void RunOnRenderThread(int eventID) { if (eventID == 2) { Debug.Log("============= RunOnRenderThread eventID==2"); // 在多线程中调用 java 函数,至少要执行一次 AttachCurrentThread 来初始化环境 AndroidJNI.AttachCurrentThread(); AndroidJavaClass sharedEGLClass = new AndroidJavaClass("com.qml.SharedEGL"); sharedEGLClass.CallStatic("InitContext"); sharedEGLClass.Dispose(); // 因为后续不再使用,可以删除环境 AndroidJNI.DetachCurrentThread(); } } }
-
unity端传递纹理给android
首先这个只支持 OPENGLES,不支持 vulkan,所以打包 android 时
PlayerSettings -> OtherSettings -> Graphics APIs 必须选择 OPENGLES3class SharedEGL { public static EGLContext s_sharedContext; public static void SaveTexture(int textureId) { // 这个实现比较复杂,具体请看 VideoRecorder 插件中 // java 函数 GTVImageFilter.onDrawFrame // 就是OPENGL 那一套,创建网格,编译shader,绑定纹理,渲染 } } class InitSharedEGL : MonoBehaviour { public IEnumerator CaptureScreen() { yield return new WaitForEndOfFrame(); RenderTexture texture = CreateRenderTexture(); ScreenCapture.CaptureScreenshotIntoRenderTexture(texture); AndroidJavaClass sharedEGLClass = new AndroidJavaClass("com.qml.SharedEGL"); // 纹理指针就是共享的句柄 sharedEGLClass.CallStatic("SaveTexture", (int)texture.GetNativeTexturePtr()); sharedEGLClass.Dispose(); UnityEngine.Object.Destroy(texture); } RenderTexture CreateRenderTexture() { RenderTexture texture = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.ARGB32); texture.wrapMode = TextureWrapMode.Clamp; if (QualitySettings.antiAliasing > 0 ) { texture.antiAliasing = QualitySettings.antiAliasing; } texture.Create(); return texture; } }
-
unity获取android传入的纹理
// 使用安卓传入的纹理ID创建纹理 Texture2D m_texture; private void CreateTextureFromAndroidId(int textureId) { if ( m_texture != null ) { m_texture.UpdateExternalTexture((IntPtr)textureId); } else { m_texture = Texture2D.CreateExternalTexture(512,512, TextureFormat.RGBA32, false, false, (IntPtr)textureId); } }