看完這一篇,我們應該可以使用OpenGL繪制如下圖的場景了。該場景是一個旋轉的三菱錐 矩陣,下面是旋轉到不同方位的截圖:
我整整花了一個星期的時間來研究SWT中的OpenGL,遇到的第一個困難是找不到傳說中的 GL類和GLU類,最後,通過搜索引擎終於找到了,原來使用Eclipse進行OpenGL開發,還需要 另外下載OpenGL插件,如下圖:
這裡有OpenGL的類庫,還有一個示例,把類庫下載下來,解壓,放到Eclipse的Plugin目 錄下,然後在我們的項目中添加依賴項,就可以看到我們需要使用的類了,如下圖:
我們需要對OpenGL編程的一些基本概念有點了解,在OpenGL中,3D場景不是直接繪制到操 作系統的窗口上的,而是有一個稱為著色描述表(Rendering Context)的東西,我們這裡簡 稱它為context,OpenGL的繪圖命令都是在當前context上進行繪制,然後再把它渲染到操作 系統的設備描述表(Device Context)上,這裡,我們可以簡單的理解成把它渲染到窗口控 件上(其實也可以渲染到全屏幕)。
在Windows中使用OpenGL編程比較麻煩,因為我們需要設置一個叫做象素格式的東西,大 家只要看看下面的這段C代碼,就知道我為什麼說它麻煩了:
static PIXELFORMATDESCRIPTOR pfd= //pfd 告訴窗口我們所希望的東 東
{
sizeof(PIXELFORMATDESCRIPTOR), //上訴格式描述符的大小
1, // 版本號
PFD_DRAW_TO_WINDOW | // 格式必須支持窗口
PFD_SUPPORT_OPENGL | // 格式必須支持 OpenGL
PFD_DOUBLEBUFFER, // 必須支持雙緩沖
PFD_TYPE_RGBA, // 申請 RGBA 格式
bits, // 選定色彩深度
0, 0, 0, 0, 0, 0, // 忽略的色彩位
0, // 無Alpha緩存
0, // 忽略Shift Bit
0, // 無聚集緩存
0, 0, 0, 0, // 忽略聚集位
16, // 16位 Z-緩存 (深度緩存 )
0, // 無模板緩存
0, // 無輔助緩存
PFD_MAIN_PLANE, // 主繪圖層
0, // 保留
0, 0, 0 // 忽略層遮罩
};
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd))) // Windows 找到相應的 象素格式了嗎?
{
KillGLWindow(); // 重置顯示區
MessageBox(NULL,"Can't Find A Suitable PixelFormat.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
if(!SetPixelFormat(hDC,PixelFormat,&pfd)) // 能夠設置象素格式麼?
{
KillGLWindow(); // 重置顯示區
MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
if (!(hRC=wglCreateContext(hDC))) // 能否取得著色描述表?
{
KillGLWindow(); // 重置顯示區
MessageBox(NULL,"Can't Create A GL Rendering Context.",
"ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE; // 返回 FALSE
}
在SWT中,我們開發OpenGL應用就要簡單得多,這全部要歸功於org.eclipse.swt.opengl 包下面的GLCanvas類和GLData類,使用GLCanvas類可以直接創建一個用於OpenGL渲染的控件 ,至於設置象素格式這樣復雜的問題,它已經幫我們解決了,不信你看GLCanvas類的構造函 數的實現。
GLCanvas類中的幾個方法代表了我一開始提到的OpenGL的幾個基本概念,setCurrent()方 法就是為了把該控件的context設置為OpenGL的當前著色描述表,然後使用GL和GLU類中的方 法在當前context上進行繪圖,繪制完圖形以後,再使用GLCanvas類的swapBuffers()方法交 換緩沖區,也就是把context中的3D場景渲染到控件上。
寫到這裡,大家肯定認為一切問題都應該迎刃而解了,然而,我卻碰到了另外一個困難, 這個困難就是SWT的OpenGL表現怪異,怎麼個怪異呢?請看下面視圖類的代碼:public void createPartControl(Composite parent) {
// TODO 自動生成方法存根
GLData data = new GLData();
data.depthSize = 1;
data.doubleBuffer = true;
GLCanvas canvas = new GLCanvas(parent, SWT.NO_BACKGROUND, data);
//設置該canvas的context為OpenGL的當前context
if(!canvas.isCurrent()){
canvas.setCurrent();
}
//這裡可以進行OpenGL繪圖
//交換緩存,將圖形渲染到控件上
canvas.swapBuffers();
}
按道理,我們應該可以得到一個經典的3D的黑色場景,但是,我得到的卻是這樣的效果:
相當的郁悶啊,就是這個問題困擾了我至少一個星期。我把官方網站上的示例看了有看, 就是找不到問題的關鍵所在。直到最後,我用了另外一個線程,每100ms都調用 canvas.swapBuffers()把場景渲染一遍問題才解決。由此可見,之所以回出現上面的問題, 主要是因為我們渲染的場景很快會被操作系統的其他繪圖操作所覆蓋,只有不斷的渲染我們 才能看到連續的3D圖形。
我是這樣實現讓3D場景連續渲染的:
public void createPartControl(Composite parent) {
// TODO 自動生成方法存根
GLData data = new GLData();
data.depthSize = 1;
data.doubleBuffer = true;
GLCanvas canvas = new GLCanvas(parent, SWT.NO_BACKGROUND, data);
//將繪圖代碼轉移到定時器中
Refresher rf = new Refresher(canvas);
rf.run();
}
Refresher類的代碼如下:
class Refresher implements Runnable {
public static final int DELAY = 100;
private GLCanvas canvas;
public Refresher(GLCanvas canvas) {
this.canvas = canvas;
}
public void run() {
if (this.canvas != null && !this.canvas.isDisposed()) {
if(!canvas.isCurrent()){
canvas.setCurrent();
}
//這裡添加OpenGL繪圖代碼
canvas.swapBuffers();
this.canvas.getDisplay().timerExec(DELAY, this);
}
}
}
問題解決,得到的效果圖如下:
OK,下面的任務就是完完全全的使用OpenGL的繪圖功能了,不管你的OpenGL教材使用的是 什麼操作系統什麼編程語言,你都能很簡單的把它的概念拿到這裡來使用。
使用OpenGL的第一件事,就是要設置投影矩陣、透視圖和觀察者矩陣,如果你不知道為什 麼要這麼做,請查看OpenGL的基礎教材,在這裡,照搬就行了。為了讓我們的控件在每次改 變大小的時候都能夠做這些設置,我們使用事件監聽器,如下:
public void createPartControl(Composite parent) {
// TODO 自動生成方法存根
GLData data = new GLData();
data.depthSize = 1;
data.doubleBuffer = true;
canvas = new GLCanvas(parent, SWT.NO_BACKGROUND, data);
canvas.addControlListener(new ControlAdapter() {
public void controlResized(ControlEvent e) {
Rectangle rect = canvas.getClientArea();
GL.glViewport(0, 0, rect.width, rect.height);
//選擇投影矩陣
GL.glMatrixMode(GL.GL_PROJECTION);
//重置投影矩陣
GL.glLoadIdentity();
//設置窗口比例和透視圖
GLU.gluPerspective(45.0f, (float) rect.width / (float) rect.height, 0.1f, 100.0f);
//選擇模型觀察矩陣
GL.glMatrixMode(GL.GL_MODELVIEW);
//重置模型觀察矩陣
GL.glLoadIdentity();
//黑色背景
GL.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
//設置深度緩存
GL.glClearDepth(1.0f);
//啟動深度測試
GL.glEnable(GL.GL_DEPTH_TEST);
//選擇深度測試類型
GL.glDepthFunc(GL.GL_LESS);
//啟用陰影平滑
GL.glShadeModel(GL.GL_SMOOTH);
//精細修正透視圖
GL.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
//清除屏幕和深度緩存
GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
//重置當前的模型觀察矩陣
GL.glLoadIdentity();
}
});
canvas.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
dispose();
}
});
調用glLoadIdentity()之後,實際上將當前點移到了屏幕中心,X坐標軸從左至右,Y坐標 軸從下至上,Z坐標軸從裡至外。OpenGL屏幕中心的坐標值是X和Y軸上的0.0f點。中心左面的 坐標值是負值,右面是正值。移向屏幕頂端是正值,移向屏幕底端是負值。移入屏幕深處是 負值,移出屏幕則是正值。
glTranslatef(x, y, z)是將當前點沿著X,Y和Z軸移動,當我們繪圖的時候,不是相對於 屏幕中間,而是相對於當前點。
glBegin(GL.GL_TRIANGLES)的意思是開始繪制三角形,glEnd()告訴OpenGL三角形已經創 建好了。通常當我們需要畫3個頂點時,可以使用GL_TRIANGLES。在絕大多數的顯卡上,繪制 三角形是相當快速的。如果要畫四個頂點,使用GL_QUADS的話會更方便。但據我所知,絕大 多數的顯卡都使用三角形來為對象著色。最後,如果想要畫更多的頂點時,可以使用 GL_POLYGON。
glVertex(x,y,z)用來設置頂點,如果繪制三角形,這些頂點需要三個一組,如果繪制四 邊形,則是四個為一組。如果我們要為頂點著色,就需要glColor3f(r,g,b)方法,記住,每 次設置以後,這個顏色就是當前顏色,直到再次調用該方法重新設置為止。
最後需要介紹的是glRotatef(Angle,Xvector,Yvector,Zvector)方法,該方法負責讓對象 圍繞指定的軸旋轉,Angle參數指轉動的角度,注意是浮點數哦。
下面是我的視圖類的全部代碼,我把3D繪圖的任務全部放到了另外一個線程中,並且定義 了一個遞歸方法public void drawPyramid(float x, float y, float z, int n)用來繪制三 菱錐矩陣。如下:
package cn.blogjava.youxia.views;
import org.eclipse.opengl.GL;
import org.eclipse.opengl.GLU;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.opengl.GLData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.swt.opengl.GLCanvas;
import org.eclipse.swt.SWT;
public class OpenGLView extends ViewPart {
GLCanvas canvas;
@Override
public void createPartControl(Composite parent) {
// TODO 自動生成方法存根
GLData data = new GLData();
data.depthSize = 1;
data.doubleBuffer = true;
canvas = new GLCanvas(parent, SWT.NO_BACKGROUND, data);
canvas.addControlListener(new ControlAdapter() {
public void controlResized(ControlEvent e) {
Rectangle rect = canvas.getClientArea();
GL.glViewport(0, 0, rect.width, rect.height);
//選擇投影矩陣
GL.glMatrixMode(GL.GL_PROJECTION);
//重置投影矩陣
GL.glLoadIdentity();
//設置窗口比例和透視圖
GLU.gluPerspective(45.0f, (float) rect.width / (float) rect.height, 0.1f, 100.0f);
//選擇模型觀察矩陣
GL.glMatrixMode(GL.GL_MODELVIEW);
//重置模型觀察矩陣
GL.glLoadIdentity();
//黑色背景
GL.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
//設置深度緩存
GL.glClearDepth(1.0f);
//啟動深度測試
GL.glEnable(GL.GL_DEPTH_TEST);
//選擇深度測試類型
GL.glDepthFunc(GL.GL_LESS);
//啟用陰影平滑
GL.glShadeModel(GL.GL_SMOOTH);
//精細修正透視圖
GL.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
//清除屏幕和深度緩存
GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
//重置當前的模型觀察矩陣
GL.glLoadIdentity();
}
});
canvas.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
dispose();
}
});
/**//*
*/
Refresher rf = new Refresher(canvas);
rf.run();
}
@Override
public void setFocus() {
// TODO 自動生成方法存根
}
}
class Refresher implements Runnable {
public static final int DELAY = 100;
private GLCanvas canvas;
private float rotate = 0.0f;
public Refresher(GLCanvas canvas) {
this.canvas = canvas;
}
public void run() {
if (this.canvas != null && !this.canvas.isDisposed()) {
if(!canvas.isCurrent()){
canvas.setCurrent();
}
//這裡添加OpenGL繪圖代碼
GL.glLoadIdentity();
GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
GL.glTranslatef(0, 4.5f, -11);
//圍繞y軸轉起來
rotate += 0.5;
GL.glRotatef(rotate, 0, 1.0f, 0);
//調用遞歸函數,繪制三菱錐矩陣
drawPyramid(0,0,0,4);
canvas.swapBuffers();
this.canvas.getDisplay().timerExec(DELAY, this);
}
}
public void drawPyramid(float x, float y, float z, int n){
if(n == 0)return;
//畫一個三菱錐
GL.glBegin(GL.GL_TRIANGLES);
//畫背面
GL.glColor3f(1.0f,0.0f,0.0f);
GL.glVertex3f( x, y, z);
GL.glColor3f(0.0f,1.0f,0.0f);
GL.glVertex3f(x+1.0f,y-1.63f,z-0.57f);
GL.glColor3f(0.0f,0.0f,1.0f);
GL.glVertex3f( x-1.0f,y-1.63f,z-0.57f);
//畫底面
GL.glColor3f(1.0f,0.0f,0.0f);
GL.glVertex3f( x,y-1.63f,z+1.15f);
GL.glColor3f(0.0f,1.0f,0.0f);
GL.glVertex3f(x-1.0f,y-1.63f,z-0.57f);
GL.glColor3f(0.0f,0.0f,1.0f);
GL.glVertex3f( x+1.0f,y-1.63f,z-0.57f);
//畫左側面
GL.glColor3f(1.0f,0.0f,0.0f);
GL.glVertex3f( x,y,z);
GL.glColor3f(0.0f,1.0f,0.0f);
GL.glVertex3f(x-1.0f,y-1.63f,z-0.57f);
GL.glColor3f(0.0f,0.0f,1.0f);
GL.glVertex3f( x,y-1.63f,z+1.15f);
//畫右側面
GL.glColor3f(1.0f,0.0f,0.0f);
GL.glVertex3f( x,y,z);
GL.glColor3f(0.0f,1.0f,0.0f);
GL.glVertex3f(x,y-1.63f,z+1.15f);
GL.glColor3f(0.0f,0.0f,1.0f);
GL.glVertex3f( x+1.0f,y-1.63f,z-0.57f);
GL.glEnd();
//遞歸調用,畫多個三菱錐
drawPyramid(x,y-1.63f,z+1.15f,n-1);
drawPyramid(x-1.0f,y-1.63f,z-0.57f,n-1);
drawPyramid(x+1.0f,y-1.63f,z-0.57f,n-1);
}
}