在 Windows Mobile 使用 OpenGL ES 繪製3D圖形
1. 簡介
想要在 Windows Mobile 畫3維圖形,就會想到 DirectX 與 OpenGL,而想在 Windows Mobile 使用 OpenGL,則必須使用 OpenGL ES ( OpenGL for Embedded Systems ),OpenGL ES 為 OpenGL 三維圖形 API 的子集,主要針對手機、PDA 等嵌入式裝置所設計。而 OpenGL ES 可以算是 OpenGL 精簡版,捨去了 glBegin/glEnd,四邊形(GL_QUADS)、多邊形(GL_POLYGONS)等複雜圖形繪製函式。
本文介紹如何使用.NET Compact Framework wrapper for OpenGL ES 函式庫,以 C# 撰寫程式,而程式功能為畫一個旋轉的三角形,執行結果請參考以下影片。
2. OpenGL ES
2.1 初始化 EGL
要使用 OpenGL ES 於表單中,必須與要顯示的視窗做關聯,而這部份是有標準,其標準為 EGL,初始化過程如下所示
接著介紹 EGL 標準會使用 EGL 函式與流程
(1) 獲取 Display ( EGLDisplay )
Display 代表顯示器,在有些系統上可以有多個顯示器,也就會有多個 Display。獲得 Display 使用以下程式碼
egl.GetDisplay(new EGLNativeDisplayType(this));
(2) 初始化 EGL
使用以下程式碼進行初始化動作,並回傳 EGL 版本 ( major.minor )
egl.Initialize(myDisplay, out major, out minor);
(3) 設定 EGLConfig ( EGLConfig )
設定 EGLConfig 其實是指定 FrameBuffer 的參數,透過
egl.ChooseConfig(EGLDisplay myDisplay, const EGLint * attribList, EGLConfig * configs, EGLint config.Length, EGLint *numConfig)
其中 attribList 是以EGL_NONE 結束的參數群組,以 id, value 依次存放,對於個別標識性的屬性可以只有 id,沒有value,系統的總共的 Config 個數儲存在 numConfig 中。Config 有眾多的屬性,這些屬性決定 FrameBuffer 的格式和能力,以下為設定 EGLConfig 的程式碼。
EGLConfig[] configs = new EGLConfig[10];
int[] attribList = new int[]
{
egl.EGL_RED_SIZE, 5,
egl.EGL_GREEN_SIZE, 6,
egl.EGL_BLUE_SIZE, 5,
egl.EGL_DEPTH_SIZE, 16 ,
egl.EGL_SURFACE_TYPE, egl.EGL_WINDOW_BIT,
egl.EGL_STENCIL_SIZE, egl.EGL_DONT_CARE,
egl.EGL_NONE, egl.EGL_NONE
};
int numConfig;
if (!egl.ChooseConfig(myDisplay, attribList, configs, configs.Length, out numConfig) || numConfig < 1)
throw new InvalidOperationException("Unable to choose config.");
EGLConfig config = configs[0];
(4) 構造 Surface ( EGLSurface )
Surface 是一個 FrameBuffer,透過 CreateWindowSurface 創建可實際顯示的 Surface
egl.CreateWindowSurface(myDisplay, config, Handle, null);
(5) 創建 Context ( EGLContext )
OpenGL 的 pipeline 從程式的角度看就是一個狀態機,有目前的顏色、紋理、變化矩陣等狀態,這些狀態作用於程式提交的頂點坐標等圖形元素,從而形成緩衝區內的像素。在 OpenGL 中,Context 就代表這個狀態機,程式的主要工作就是像 Context 提供圖型元素、設定狀態,偶爾從 Context 裡獲取一些訊息。使用 CreateContext 來創建一個 Context
egl.CreateContext(myDisplay, config, EGLContext.None, null);
(6) 設定繪製環境 Render Context
主要將上述設定之 EGL,透過 MakeCurrent 設定繪製環境,接著就可以透過 OpenGL API 進行繪製。
egl.MakeCurrent(myDisplay, mySurface, mySurface, myContext);
2.2 進行繪製
(1) 初始化 OpenGL ES
開始繪製前,必須先做初始化,設定像背景,深度測試等參數
void InitGL()
{
gl.ShadeModel(gl.GL_SMOOTH); // 設定陰影模式為平滑
// gl.ClearColor(1.0f, 1.0f, 1.0f, 0.5f); // 白色背景
gl.ClearColor(0.0f, 0.0f, 0.0f, 0.5f); // 黑色背景
gl.ClearDepthf(1.0f); // 設置深度緩衝
gl.Enable(gl.GL_DEPTH_TEST); // 使用深度測試
gl.BlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA); // 設定混合函數,並設定源因子為GL_SRC_ALPHA,目的因子為GL_ONE_MINUS_SRC_ALPHA
gl.DepthFunc(gl.GL_LEQUAL); // 所做的深度測試為 GL_LEQUAL 深度小或相等的时候也渲染
gl.Hint(gl.GL_PERSPECTIVE_CORRECTION_HINT, gl.GL_NICEST); // 系統對測試進行修正
}
(2) OpenGL ES 繪圖部分的函式
以下程式碼為 OpenGL ES 繪圖部分的函式,並且在 OnPaint 重繪事件時調用此函式,假如將 Color 部分的程式碼註解掉,則顯示之三角型為白色
float myRotation = 0;
unsafe void DrawGLScene()
{
gl.Viewport(ClientRectangle.Left, ClientRectangle.Top, ClientRectangle.Width, ClientRectangle.Height); // 設定圖形要顯示的區域
gl.MatrixMode(gl.GL_PROJECTION); // 設定目前工作矩陣為 Projection 矩陣
gl.LoadIdentity(); // 將 Projection 矩陣初始化
gluPerspective(45, (float)ClientSize.Width / (float)ClientSize.Height, .1f, 100); // 設定圖形 gluPerspective 透視投影
float[] triangle = new float[] { 0.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f };
float[] colors = new float[] { 1.0f, 0.0f, 0.0f, 9, 0.0f, 1.0f, 0.0f, 0, 0.0f, 0.0f, 1.0f, 0 };
gl.Translatef(0.0f, 0.0f, -6.0f); // 平移函式,表示 x,y,z 偏移量
gl.Rotatef(myRotation, 0.0f, 1.0f, 0.0f); // 旋轉函式,參數x,y,z代表一個向量,指旋轉所繞的軸,參數 myRotation 表示旋轉逆時針的角度
fixed (float* trianglePointer = &triangle[0], colorPointer = &colors[0])
{
gl.EnableClientState(gl.GL_VERTEX_ARRAY); // 啟動 Client 陣列繪製模式為 GL_VERTEX_ARRAY
gl.VertexPointer(3, gl.GL_FLOAT, 0, (IntPtr)trianglePointer); // 設定指標陣列 trianglePointer
gl.EnableClientState(gl.GL_COLOR_ARRAY); // 啟動 Client 陣列繪製模式為 GL_COLOR_ARRAY
gl.ColorPointer(4, gl.GL_FLOAT, 0, (IntPtr)colorPointer); // 設定指標陣列 colorPointer
// gl.DrawArrays(gl.GL_TRIANGLES, 0, 3); // 繪製三角形
gl.DrawArrays(gl.GL_LINE_LOOP, 0, 3); // 繪製連續線段(封閉連續線段)
gl.DisableClientState(gl.GL_VERTEX_ARRAY); // 關閉 Client 陣列繪製模式 GL_VERTEX_ARRAY
gl.DisableClientState(gl.GL_COLOR_ARRAY); // 關閉 Client 陣列繪製模式 GL_COLOR_ARRAY
gl.Flush(); // 立即將目前所有的繪圖指令輸出
}
myRotation += 2f;
}
// gluPerspective 透視投影
// fovy:可視角,範圍從0到180度
// aspect: 設定投影平面寬與高的比例 (double) w/h
// near: 投影近點,表示可視區域的最近距離
// far: 投影遠點,表示可視區域的最遠距離
void gluPerspective(float fovy, float aspect, float near, float far)
{
float xmin, xmax, ymin, ymax;
ymax = near * (float)Math.Tan(fovy * 3.1415962f / 360.0);
ymin = -ymax;
xmin = ymin * aspect;
xmax = ymax * aspect;
gl.Frustumf(xmin, xmax, ymin, ymax, near, far); // 透視投影函式
}
(3) 繪圖事件
以下程式碼為覆寫 OnPaint 事件,並於事件中進行圖形繪製
// OnPaint 重繪事件
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
DrawGLScene(); // 繪圖
egl.SwapBuffers(myDisplay, mySurface);
gl.Clear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT); // 清除顏色緩衝區與深度緩衝區
Invalidate();
int tickCount = Environment.TickCount; // 取得系統啟動後經過的毫秒數
int nextFrame = (myLastFrame + 1) % myFrameTracker.Length;
// elapsed: 多久重繪 30 Frame
float elapsed = (tickCount - myFrameTracker[nextFrame]) / 1000f;
float timePerFrame = elapsed / 30;
myFps = 1 / timePerFrame;
myLastFrame = nextFrame;
myFrameTracker[nextFrame] = tickCount;
}
(4) 程式關閉時釋放資源
以下程式碼為關閉程式時,釋放 OpenGL ES 相關資源
protected override void OnClosing(CancelEventArgs e)
{
if (!egl.DestroySurface(myDisplay, mySurface))
throw new Exception("Error while destroying surface.");
if (!egl.DestroyContext(myDisplay, myContext))
throw new Exception("Error while destroying context.");
if (!egl.Terminate(myDisplay))
throw new Exception("Error while terminating display.");
base.OnClosing(e);
}
3. 程式碼
以下為完整程式碼
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using OpenGLES;
namespace TestOpenGLES
{
public partial class Form1 : Form
{
[DllImport("coredll")]
extern static IntPtr GetDC(IntPtr hwnd);
EGLDisplay myDisplay; // 創建 EGLDisplay
EGLSurface mySurface; // 創建 EGLSurface
EGLContext myContext; // 創建 EGLContext
public Form1()
{
InitializeComponent();
// start ========== EGL 設定 ==========
myDisplay = egl.GetDisplay(new EGLNativeDisplayType(this)); // 獲取 Display ( EGLDisplay )
int major, minor;
egl.Initialize(myDisplay, out major, out minor); // 初始化 EGL
EGLConfig[] configs = new EGLConfig[10]; // 設定 EGLConfig ( EGLConfig )
int[] attribList = new int[]
{
egl.EGL_RED_SIZE, 5,
egl.EGL_GREEN_SIZE, 6,
egl.EGL_BLUE_SIZE, 5,
egl.EGL_DEPTH_SIZE, 16 ,
egl.EGL_SURFACE_TYPE, egl.EGL_WINDOW_BIT,
egl.EGL_STENCIL_SIZE, egl.EGL_DONT_CARE,
egl.EGL_NONE, egl.EGL_NONE
};
int numConfig;
if (!egl.ChooseConfig(myDisplay, attribList, configs, configs.Length, out numConfig) || numConfig < 1)
throw new InvalidOperationException("Unable to choose config.");
EGLConfig config = configs[0];
mySurface = egl.CreateWindowSurface(myDisplay, config, Handle, null); // 構造 Surface ( EGLSurface )
myContext = egl.CreateContext(myDisplay, config, EGLContext.None, null); // 創建 Context ( EGLContext )
egl.MakeCurrent(myDisplay, mySurface, mySurface, myContext); // 設定繪製環境 Render Context
// end ========== EGL 初始化 ==========
gl.ClearColor(0, 0, 0, 0);
InitGL(); // 初始化 OpenGL ES
}
int[] myFrameTracker = new int[30];
int myLastFrame = 0;
float myFps = 0;
// 初始化 OpenGL ES
void InitGL()
{
gl.ShadeModel(gl.GL_SMOOTH); // 設定陰影模式為平滑
// gl.ClearColor(1.0f, 1.0f, 1.0f, 0.5f); // 白色背景
gl.ClearColor(0.0f, 0.0f, 0.0f, 0.5f); // 黑色背景
gl.ClearDepthf(1.0f); // 設置深度緩衝
gl.Enable(gl.GL_DEPTH_TEST); // 使用深度測試
gl.BlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA); // 設定混合函數,並設定源因子為GL_SRC_ALPHA,目的因子為GL_ONE_MINUS_SRC_ALPHA
gl.DepthFunc(gl.GL_LEQUAL); // 所做的深度測試為 GL_LEQUAL 深度小或相等的时候也渲染
gl.Hint(gl.GL_PERSPECTIVE_CORRECTION_HINT, gl.GL_NICEST); // 系統對測試進行修正
}
float myRotation = 0;
unsafe void DrawGLScene()
{
gl.Viewport(ClientRectangle.Left, ClientRectangle.Top, ClientRectangle.Width, ClientRectangle.Height); // 設定圖形要顯示的區域
gl.MatrixMode(gl.GL_PROJECTION); // 設定目前工作矩陣為 Projection 矩陣
gl.LoadIdentity(); // 將 Projection 矩陣初始化
gluPerspective(45, (float)ClientSize.Width / (float)ClientSize.Height, .1f, 100); // 設定圖形 gluPerspective 透視投影
float[] triangle = new float[] { 0.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f };
float[] colors = new float[] { 1.0f, 0.0f, 0.0f, 9, 0.0f, 1.0f, 0.0f, 0, 0.0f, 0.0f, 1.0f, 0 };
gl.Translatef(0.0f, 0.0f, -6.0f); // 平移函式,表示 x,y,z 偏移量
gl.Rotatef(myRotation, 0.0f, 1.0f, 0.0f); // 旋轉函式,參數x,y,z代表一個向量,指旋轉所繞的軸,參數 myRotation 表示旋轉逆時針的角度
fixed (float* trianglePointer = &triangle[0], colorPointer = &colors[0])
{
gl.EnableClientState(gl.GL_VERTEX_ARRAY); // 啟動 Client 陣列繪製模式為 GL_VERTEX_ARRAY
gl.VertexPointer(3, gl.GL_FLOAT, 0, (IntPtr)trianglePointer); // 設定指標陣列 trianglePointer
gl.EnableClientState(gl.GL_COLOR_ARRAY); // 啟動 Client 陣列繪製模式為 GL_COLOR_ARRAY
gl.ColorPointer(4, gl.GL_FLOAT, 0, (IntPtr)colorPointer); // 設定指標陣列 colorPointer
// gl.DrawArrays(gl.GL_TRIANGLES, 0, 3); // 繪製三角形
gl.DrawArrays(gl.GL_LINE_LOOP, 0, 3); // 繪製連續線段(封閉連續線段)
gl.DisableClientState(gl.GL_VERTEX_ARRAY); // 關閉 Client 陣列繪製模式 GL_VERTEX_ARRAY
gl.DisableClientState(gl.GL_COLOR_ARRAY); // 關閉 Client 陣列繪製模式 GL_COLOR_ARRAY
gl.Flush(); // 立即將目前所有的繪圖指令輸出
}
myRotation += 2f;
}
// gluPerspective 透視投影
// fovy:可視角,範圍從0到180度
// aspect: 設定投影平面寬與高的比例 (double) w/h
// near: 投影近點,表示可視區域的最近距離
// far: 投影遠點,表示可視區域的最遠距離
void gluPerspective(float fovy, float aspect, float near, float far)
{
float xmin, xmax, ymin, ymax;
ymax = near * (float)Math.Tan(fovy * 3.1415962f / 360.0);
ymin = -ymax;
xmin = ymin * aspect;
xmax = ymax * aspect;
gl.Frustumf(xmin, xmax, ymin, ymax, near, far); // 透視投影函式
}
// OnPaintBackground 重繪背景事件,不可拿掉,拿掉後重繪時會閃爍
protected override void OnPaintBackground(PaintEventArgs e)
{
}
// OnPaint 重繪事件
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
DrawGLScene(); // 繪圖
egl.SwapBuffers(myDisplay, mySurface);
gl.Clear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT); // 清除顏色緩衝區與深度緩衝區
Invalidate();
int tickCount = Environment.TickCount; // 取得系統啟動後經過的毫秒數
int nextFrame = (myLastFrame + 1) % myFrameTracker.Length;
// elapsed: 多久重繪 30 Frame
float elapsed = (tickCount - myFrameTracker[nextFrame]) / 1000f;
float timePerFrame = elapsed / 30;
myFps = 1 / timePerFrame;
myLastFrame = nextFrame;
myFrameTracker[nextFrame] = tickCount;
}
protected override void OnClosing(CancelEventArgs e)
{
if (!egl.DestroySurface(myDisplay, mySurface))
throw new Exception("Error while destroying surface.");
if (!egl.DestroyContext(myDisplay, myContext))
throw new Exception("Error while destroying context.");
if (!egl.Terminate(myDisplay))
throw new Exception("Error while terminating display.");
base.OnClosing(e);
}
// 離開
private void myExitMenuItem_Click(object sender, EventArgs e)
{
Close();
}
}
}
4. 執行結果
(1) 使用 gl.DrawArrays(gl.GL_TRIANGLES, 0, 3); // 繪製三角形
(2) 將 GL_VERTEX_ARRAY 部分註解
(3) 使用 gl.DrawArrays(gl.GL_LINE_LOOP, 0, 3); // 繪製連續線段(封閉連續線段)
(4) 將 GL_VERTEX_ARRAY 部分註解
(5) 將背景設定為白色 gl.ClearColor(1.0f, 1.0f, 1.0f, 0.5f); // 白色背景
5. 參考
.NET Compact Framework wrapper for OpenGL ES