CSharpGL(19)用glReadPixels把渲染的内容保存为PNG图片(C#)
CSharpGL(19)用glReadPixels把渲染的内容保存为PNG图片(C#)
效果图
本文解决了将OpenGL渲染出来的内容保存到PNG图片的方法。
下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
glReadPixels
C#里声明glReadPixels的形式如下:
1 /// <summary> 2 /// Reads a block of pixels from the frame buffer. 3 /// </summary> 4 /// <param name="x">Top-Left X value.</param> 5 /// <param name="y">Top-Left Y value.</param> 6 /// <param name="width">Width of block to read.</param> 7 /// <param name="height">Height of block to read.</param> 8 /// <param name="format">Specifies the format of the pixel data. The following symbolic values are accepted: OpenGL.COLOR_INDEX, OpenGL.STENCIL_INDEX, OpenGL.DEPTH_COMPONENT, OpenGL.RED, OpenGL.GREEN, OpenGL.BLUE, OpenGL.ALPHA, OpenGL.RGB, OpenGL.RGBA, OpenGL.LUMINANCE and OpenGL.LUMINANCE_ALPHA.</param> 9 /// <param name="type">Specifies the data type of the pixel data.Must be one of OpenGL.UNSIGNED_BYTE, OpenGL.BYTE, OpenGL.BITMAP, OpenGL.UNSIGNED_SHORT, OpenGL.SHORT, OpenGL.UNSIGNED_INT, OpenGL.INT or OpenGL.FLOAT.</param> 10 /// <param name="pixels">Storage for the pixel data received.</param> 11 [DllImport(Win32.OpenGL32, EntryPoint = "glReadPixels", SetLastError = true)] 12 public static extern void ReadPixels(int x, int y, int width, int height, uint format, uint type, IntPtr pixels);
这个函数的功能是:将指定范围(x, y, width, height)的像素值读入指定的内存处(pixels)。能把像素信息读到内存里,就可以保存到文件了。
其中(x, y)是以窗口左下角为原点(0, 0)的。而Windows窗口是以左上角为原点的。所以用的时候要注意转换一下。
数据结构
为方便起见,我先定义一个描述像素的数据结构。
1 struct Pixel 2 { 3 public byte r; 4 public byte g; 5 public byte b; 6 public byte a; 7 8 public Pixel(byte r, byte g, byte b, byte a) 9 { 10 this.r = r; this.g = g; this.b = b; this.a = a; 11 } 12 13 public Color ToColor() 14 { 15 return Color.FromArgb(a, r, g, b); 16 } 17 18 public override string ToString() 19 { 20 return string.Format("{0}, {1}, {2}, {3}", r, g, b, a); 21 } 22 }
为了使用非托管数组,还需要用到 UnmanagedArray<T> 。关于这个类型详情见(C#+无unsafe的非托管大数组(large unmanaged array in c# without 'unsafe' keyword))。
方法一:Bitmap.SetPixel()
最直接的方法是用Bitmap.SetPixel()来一个一个地指定图片的像素值。
1 /// <summary> 2 /// 把OpenGL渲染的内容保存到图片文件。 3 /// </summary> 4 /// <param name="x">左下角坐标为(0, 0)</param> 5 /// <param name="y">左下角坐标为(0, 0)</param> 6 /// <param name="width">宽度</param> 7 /// <param name="height">高度</param> 8 /// <param name="filename"></param> 9 public static void Save2Picture(int x, int y, int width, int height, string filename) 10 { 11 var pdata = new UnmanagedArray<Pixel>(width * height); 12 GL.ReadPixels(x, y, width, height, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pdata.Header); 13 var bitmap = new Bitmap(width, height); 14 int index = 0; 15 for (int j = height - 1; j >= 0; j--) 16 { 17 for (int i = 0; i < width; i++) 18 { 19 Pixel v = pdata[index++]; 20 Color c = v.ToColor(); 21 bitmap.SetPixel(i, j, c); 22 } 23 } 24 25 bitmap.Save(filename); 26 }
方法二:Marshal.Copy
方法一用到的SetPixel()速度是很慢的。
先把读到的内容写入一个byte[],然后再用Marshal.Copy()复制到Bitmap。这个方法的思路是(非托管数组->托管数组->bmpData.Scan0)
1 /// <summary> 2 /// 把OpenGL渲染的内容保存到图片文件。 3 /// </summary> 4 /// <param name="x">左下角坐标为(0, 0)</param> 5 /// <param name="y">左下角坐标为(0, 0)</param> 6 /// <param name="width">宽度</param> 7 /// <param name="height">高度</param> 8 /// <param name="filename"></param> 9 public static void Save2Picture(int x, int y, int width, int height, string filename) 10 { 11 var pdata = new UnmanagedArray<Pixel>(width * height); 12 GL.ReadPixels(x, y, width, height, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pdata.Header); 13 var format = System.Drawing.Imaging.PixelFormat.Format32bppArgb; 14 var lockMode = System.Drawing.Imaging.ImageLockMode.WriteOnly; 15 var bitmap = new Bitmap(width, height, format); 16 var bitmapRect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); 17 System.Drawing.Imaging.BitmapData bmpData = bitmap.LockBits(bitmapRect, lockMode, format); 18 { 19 int length = Math.Abs(bmpData.Stride) * bitmap.Height; 20 byte[] bitmapBytes = new byte[length]; 21 int index = 0; 22 for (int j = height - 1; j >= 0; j--) 23 { 24 for (int i = 0; i < width; i++) 25 { 26 Pixel v = pdata[index++]; 27 bitmapBytes[j * bmpData.Stride + i * 4 + 0] = v.b; 28 bitmapBytes[j * bmpData.Stride + i * 4 + 1] = v.g; 29 bitmapBytes[j * bmpData.Stride + i * 4 + 2] = v.r; 30 bitmapBytes[j * bmpData.Stride + i * 4 + 3] = v.a; 31 } 32 } 33 34 System.Runtime.InteropServices.Marshal.Copy(bitmapBytes, 0, bmpData.Scan0, length); 35 } 36 bitmap.UnlockBits(bmpData); 37 38 bitmap.Save(filename); 39 }
方法三:直接写入bmpData.Scan0
上一个方法里,通过托管数组byte[]进行过渡,是为了使用Marshal.Copy()这个method。但是我明明可以直接操作bmpData.Scan0啊,何必弄个byte[]。
1 /// <summary> 2 /// 把OpenGL渲染的内容保存到图片文件。 3 /// </summary> 4 /// <param name="x">左下角坐标为(0, 0)</param> 5 /// <param name="y">左下角坐标为(0, 0)</param> 6 /// <param name="width">宽度</param> 7 /// <param name="height">高度</param> 8 /// <param name="filename"></param> 9 public static void Save2Picture(int x, int y, int width, int height, string filename) 10 { 11 var pdata = new UnmanagedArray<Pixel>(width * height); 12 GL.ReadPixels(x, y, width, height, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, pdata.Header); 13 var format = System.Drawing.Imaging.PixelFormat.Format32bppArgb; 14 var lockMode = System.Drawing.Imaging.ImageLockMode.WriteOnly; 15 var bitmap = new Bitmap(width, height, format); 16 Rectangle bitmapRect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); 17 System.Drawing.Imaging.BitmapData bmpData = bitmap.LockBits(bitmapRect, lockMode, format); 18 unsafe 19 { 20 var array = (byte*)bmpData.Scan0.ToPointer(); 21 int index = 0; 22 for (int j = height - 1; j >= 0; j--) 23 { 24 for (int i = 0; i < width; i++) 25 { 26 Pixel v = pdata[index++]; 27 array[j * bmpData.Stride + i * 4 + 0] = v.b; 28 array[j * bmpData.Stride + i * 4 + 1] = v.g; 29 array[j * bmpData.Stride + i * 4 + 2] = v.r; 30 array[j * bmpData.Stride + i * 4 + 3] = v.a; 31 } 32 } 33 } 34 bitmap.UnlockBits(bmpData); 35 36 bitmap.Save(filename); 37 }
方法四:ReadPixels直接搞定
在上面的方法里,思路是(非托管数组->非托管数组)。
显然这个转换步骤也是多余的,直接让ReadPixels写入bmpData.Scan0的位置不就好了嘛。
1 /// <summary> 2 /// 把OpenGL渲染的内容保存到图片文件。 3 /// </summary> 4 /// <param name="x">左下角坐标为(0, 0)</param> 5 /// <param name="y">左下角坐标为(0, 0)</param> 6 /// <param name="width">宽度</param> 7 /// <param name="height">高度</param> 8 /// <param name="filename"></param> 9 public static void Save2Picture(int x, int y, int width, int height, string filename) 10 { 11 var format = System.Drawing.Imaging.PixelFormat.Format32bppArgb; 12 var lockMode = System.Drawing.Imaging.ImageLockMode.WriteOnly; 13 var bitmap = new Bitmap(width, height, format); 14 var bitmapRect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); 15 System.Drawing.Imaging.BitmapData bmpData = bitmap.LockBits(bitmapRect, lockMode, format); 16 GL.ReadPixels(x, y, width, height, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, bmpData.Scan0); 17 bitmap.UnlockBits(bmpData); 18 bitmap.RotateFlip(RotateFlipType.Rotate180FlipX); 19 20 bitmap.Save(filename); 21 }
总结
从OpenGL窗口读取出图片,是非常有用的。
原CSharpGL的其他功能(UI、3ds解析器、TTF2Bmp、CSSL等),我将逐步加入新CSharpGL。
欢迎对OpenGL有兴趣的同学关注(https://github.com/bitzhuwei/CSharpGL)
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |