文章出處

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     }
struct Pixel

為了使用非托管數組,還需要用到 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


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()