[Windows Mobile]OpenGL ES 繪制3D圖形

  • 36974
  • 0
  • 2013-04-15

在 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,初始化過程如下所示

image

接著介紹 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);  // 繪製三角形

image

 

(2) 將 GL_VERTEX_ARRAY 部分註解

image

 

(3) 使用 gl.DrawArrays(gl.GL_LINE_LOOP, 0, 3);  // 繪製連續線段(封閉連續線段)

image

 

(4) 將 GL_VERTEX_ARRAY 部分註解

image

 

(5) 將背景設定為白色 gl.ClearColor(1.0f, 1.0f, 1.0f, 0.5f);  // 白色背景

image

 

 

5. 參考

.NET Compact Framework wrapper for OpenGL ES

OpenGL ES系列之基本-0:了解OpenGL ES社區

OpenGL ES系列之基本-1:初始化EGL

OpenGL 基礎圖形編程 - 總目錄