看完这一篇,我们应该可以使用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);
}
}