Android平台下OpenGL初步

Android OpenGL ES 开发教程 从入门到精通 http://blog.csdn.net/zhoudailiang/article/details/50176143

http://blog.csdn.net/jason0539/article/details/9164885

1、GLSurfaceView

 

GLSurfaceView是Android应用程序中实现OpenGl画图的重要组成部分。GLSurfaceView中封装了一个Surface。而android平台下关于图像的现实,差不多都是由Surface来实现的。

2、Renderer

 

有了GLSurfaceView之后,就相当于我们有了画图的纸。现在我们所需要做的就是如何在这张纸上画图。所以我们需要一支笔。
Renderer是GLSurfaceView的内部静态接口,它就相当于在此GLSurfaceView上作画的笔。我们通过实现这个接口来“作画”。最后通过GLSurfaceView的setRenderer(GLSurfaceView.Renderer renderer)方法,就可以将纸笔关联起来了。


实现Renderer需要实现它的三个接口:onSurfaceCreated(GL10 gl, EGLConfig config)、 onSurfaceChanged(GL10 gl, int width, int height)、onDrawFrame(GL10 gl)。下面就这三个接口的具体意义做个简单的介绍。

2.1、onSurfaceCreated

此方法看名字就知道它是在Surface创建的时候被调用的。因此我们可以在这个函数的实现中做一些初始化的工作。例如取出文房四宝、铺好画布、调好颜料之类的。它的函数原
型如下:
public abstract void onSurfaceCreated (GL10 gl, EGLConfig config)
第二个参数在文档中没有关于它的任何public方法和域。因此我们可以不用管它。
第一个参数非常重要。如果说Renderer是画笔的话,那么这个gl参数,就可以说是我们的手了。如何操作这支画笔,都是它说了算!所以我们绝大部分时候都是通过这个叫做gl的手来指挥Renderer画图的。

2.2 onSurfaceChanged

当GLSurfaceView大小改变时,对应的Surface大小也会改变。值得注意的是,在Surface刚创建的时候,它的size其实是0,也就是说在画第一次图之前它也会被调用一次的。(而且对于很多时候,Surface的大小是不会改变的,那么此函数就只在创建之初被调用一次而已)

原型如下:
public abstract void onSurfaceChanged (GL10 gl, int width, int height)
同样的,画图的手是必需的。
另外值得注意的是,它告诉了我们这张纸有多高多宽。这点很重要。因为在onSurfaceCreated的时候我们是不知道纸的宽高的,所以有一些和长宽相关的初始化工作还得在此函数中来做。

2.3 onDrawFrame

好了,我们的初始化工作做得差不多了,那么现在就是该真刀真枪画画的时候了!此函数就是真正给你画画用的。每调用一次就画一幅图。可能的疑问是这个函数什么时候会被调
用呢?最开始的时候肯定是会被调用的。以后会有两种模式供你选择:

  1. RENDERMODE_CONTINUOUSLY
  2. RENDERMODE_WHEN_DIRTY

第一种模式(RENDERMODE_CONTINUOUSLY):
连续不断的刷,画完一幅图马上又画下一幅。这种模式很明显是用来画动画的;


第二种模式(RENDERMODE_WHEN_DIRTY):
只有在需要重画的时候才画下一幅。这种模式就比较节约CPU和GPU一些,适合用来画不经常需要刷新的情况。多说一句,系统如何知道需要重画了呢?当然是你要告诉它……
调用GLSurfaceView的requestRender ()方法,告诉它,你脏了。


这两种模式在什么地方设置呢? GLSurfaceView的setRenderMode(int renderMode)方法。可以供你设置你需要的刷新模式。


还是来看看这个函数的原型吧: public abstract void onDrawFrame (GL10 gl) 很简单,只有手。

 

3、 Android下OpenGL绘图基本流程:

 

我们从画一个三角形开始说起:

3.1 MyRender

经过前面的介绍,我们应该知道现在需要做的事,就是写好Renderer的三个接口方法。
我们需要重新写一个类实现它,然后重写这三个方法。
class MyRender implements GLSurfaceView.Renderer
OK,笔已经拿好了。“铺好纸”是非常关键的一步。虽然我们说GLSurfaceView就是我们作图的纸,但是“铺”好这张纸,却也非常的重要。

 

下面我们重点讲下,这纸该怎么铺。OpenGL这张纸可不是一般的纸啊。最起码,它是三维的。然而实际的显示屏幕却是一个平面。所以这纸还不好“铺”。


首先,不一定整张纸都用来作画吧,Surface不一定全部都用上(当然,一般情况下我们是全部用上的)。那么我们需要计划好,哪部分区域用来作画。
gl.glViewport(0, 0, width, height);
根据这里的参数width和height,我们可以知道这个宽高需要在onSurfaceChanged里得知。


然后,这一步很关键。如何在平面上画三维坐标的点或图形呢?OpenGL有一个坐标系,如下图:


我们需要将这个坐标系和我们的GLSurfaceView里的Surface做一个映射关系。
glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
gl.glFrustumf(-400, 400, -240, 240, 0.3f, 100);

 

glMatrixMode(GL10.GL_PROJECTION); 是说我们现在改变的是坐标系与Surface的映射关系(投影矩阵)。

下一句 gl.glLoadIdentity(); 是将以前的改变都清掉(之前对投影矩阵的任何改变)。

glFrustumf (float left, float right, float bottom, float top, float zNear, float zFar) 这个函数非常Powerful。它实现了Surface和坐标系之间的映射关系。它是以透视投影的方式来进行映射的。


透视投影的意思见下图:

 


映射说明:
1、 前面一个矩形表示的是我们平面作图的范围。即Surface的作图范围。它有四条边,
我们将它们暂时命名edgeLeft、edgeRight、edgeTop、edgeBottom。
2、 glFrustumf 参数中的left、right、bottom、top指的是作图范围的四条edge在OpenGL x = -400的位置。同理top、right、bottom的值表示的是edgeRight、edgeTop、edgeBottom这几条边在坐标系中的位置。
3、上面第二点定出了作图范围的x和y的范围。那么对于3D的OpenGL这张纸来说,我们还需要定出z的范围。首先,要想象一下,相机或者眼睛在坐标系的哪个位置?
默认的眼睛的位置在OpenGL坐标的原点处(0,0,0)。视线方向是平行于Z轴往里看。
near表示的是眼睛到作图平面的距离(绝对值哦!),far表示眼睛到最远可见处的平面范围。于是,默认情况下z的作图范围就是在-near到-far的位置。
4、好了,我们已经确定好x、y、z方向的作图范围了。回过头来,就可以发现,这张“立体”的纸,是一个方椎体切去头部的平截头体。我们所画的物体坐标落在这个区域范围内的部分将可以被我们看到(即在屏幕里画出来)。OK,至此,我们把纸终于铺好了。

 

glMatrixMode函数的选项(参数)有后面三种:GL_PROJECTION,GL_MODELVIEW和GL_TEXTURE;

  • GL_PROJECTION,是投影的意思,就是要对投影相关进行操作,也就是把物体投影到一个平面上,就像我们照相一样,把3维物体投到2维的平面上。这样,接下来的语句可以是跟透视相关的函数,比如glFrustum()或gluPerspective();
  • GL_MODELVIEW,是对模型视景的操作,接下来的语句描绘一个以模型为基础的适应,这样来设置参数,接下来用到的就是像gluLookAt()这样的函数;
  • GL_TEXTURE,就是对纹理相关进行操作;

顺便说下,OpenGL里面的操作,很多是基于对矩阵的操作的,比如位移,旋转,缩放,所以,
这里其实说的规范一点就是glMatrixMode是用来指定哪一个矩阵是当前矩阵,而它的参数代表要操作的目标:

  • GL_PROJECTION是对投影矩阵操作;
  • GL_MODELVIEW是对模型视景矩阵操作;
  • GL_TEXTURE是对纹理矩阵进行随后的操作;

 

 

3.2 画图之前先构图


Android 的 OpenGL 作图,不同于一般的作图,这点我们不得不感慨。它将数据和画完全分开来。例如,我们要画一个三角形。很显然,三角形有三个点。我们在画图之前首先要构图,比如每个点在哪个地方。我们将这些数据放在一个一个数组缓冲区中,放好这些数据之后,再统一一起画出来。

下面,主要讲下,如何将顶点数据和颜色数据放入符合 Android OpenGL 的数组缓冲区中。

 

首先我们要明白的是,OpenGL 是一个非常底层的画图接口,它所使用的缓冲区存储结构是和我们的 Java 程序中不相同的。Java 是大端字节序(BigEdian),而 OpenGL 所需要的数据是小端字节序(LittleEdian)。所以,我们在将 Java 的缓冲区转化为 OpenGL 可用的缓冲区时需要作一些工作。


byte 数据缓冲区

不管我们的数据是整型的还是浮点型的,为了完成 BigEdian 到 LittleEdian 的转换,我们都首先需要一个 ByteBuffer。我们通过它来得到一个可以改变字节序的缓冲区。

ByteBuffer mBuffer = ByteBuffer.allocateDirect(pointCount*dimension*4);
mBuffer.order(ByteOrder.nativeOrder());

注意,我们应该用“allocateDirect”来分配内存空间,因为这样才可以 order 排序。最后我们就可以通过:
resultBuffer = mBuffer.asFloatBuffer();
resultBuffer = mBuffer.asIntBuffer();
将字节缓冲区转化为整型或者浮点型缓冲区了。

 

 

3.3 终于画图了!


有了前面所有的准备工作之后,有一个好消息可以告诉你,我们终于可以画图了!而且,有了前面这么多工作,真正画图的工作其实比较简单。


3.3.1、清理好你的纸

前面我们说过了,在画图之前,一定要把纸给清理干净喽:

gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

另外,之前我们在映射坐标系的时候,用了glMatrixMode(GL10.GL_PROJECTION);来指定改变的是投影矩阵。那么现在要画图了,所以我们需要指定改变的是“视图矩阵”:

gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();

 

3.3.2、启用数组

我们的前面说过,画图的数据都放在数组缓冲区里,最后再一起传过来作画。那么我们首先要告诉 OpenGL,我们需要用到哪些数组。例如我们需要顶点数组和颜色数组:

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);

 

3.3.3、指定数组数据

我们前面已经构造好了我们的数据缓冲区,floatBuffer(或 IntBuffer)。现在我们只需要将这个数据缓冲区和对应的功能绑定起来就好了:

gl.glVertexPointer(3, GL10.GL_FLOAT, 0, VertexBuffer);
gl.glColorPointer(4, GL10.GL_FLOAT, 0, colorBuffer);

这两句话分别绑定了顶点数据数组和颜色数据数组。其中第一个参数表示的是每个点有几个坐标。例如顶点,有 x、y、z值,所以是 3;而颜色是 r、g、b、a 值,所以是 4。

 

3.3.4、画图!

gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

第一个参数指明了画图的类型——三角形(android 似乎只支持画三角形、点、线,不支持画多边形)。后面两个参数指明,从哪个顶点开始画,画多少个顶点。

 

OK!至此,我们的第一个三角形就画出来了,来看看效果吧。

 

OpenGL ES之glFrustumf设置投影视角详解 http://blog.csdn.net/wangyuchun_799/article/details/7841429

-(void)setClipping
{
float aspectRatio;
const float zNear = 0.1; //1
const float zFar = 1000; //2
const float fieldOfView = 60.0; //3
GLfloat size;
CGRect frame = [[UIScreen mainScreen] bounds]; //4

aspectRatio=(float)frame.size.width/(float)frame.size.height; //5

 

//Set the OpenGL projection matrix.

glMatrixMode(GL_PROJECTION); //6
glLoadIdentity();
size = zNear * tanf(GLKMathDegreesToRadians (fieldOfView) / 2.0); //7
glFrustumf(-size, size, -size /aspectRatio, size /aspectRatio, zNear, zFar); //8
glViewport(0, 0, frame.size.width, frame.size.height); //9
//Make the OpenGL ModelView matrix the default.
glMatrixMode(GL_MODELVIEW); //10
}

glFrustumf中文API文档参考OpenGL ES之glFrustum函数

        第1、2行指定了近裁面和远裁面的距离。这两个值的意思是,任何远于1000或近于0.1的对象都将被过滤掉。你可能会问一千什么?就是一千!单位看你自己设想。你可以把它想象成光年,或者英尺,都无所谓。不信的话你自己随便设置一下试试。

        第3行设定视角为60度。

        第4行获取屏幕的尺寸大小。

        第5行根据屏幕的尺寸计算最终屏幕的纵横比例。它的高度和宽度的值决定了相对高度的视域(FOV),如果将其翻转的话,将变成相对于宽度的视域。如果我们要设置一个60度视域,就像一个宽角度镜头,那么它将基于窗口的高度而非宽度。当渲染到一个非正方形的屏幕上时尤为重要。

        由于glfrustumf影响的是投影矩阵,所以我们需要确认将当前矩阵从模型视图矩阵设置成投影矩阵。第6行就是要做这个滴。

第7行计算锥形视角的左右上下的限制值。你可以把它想象成3D空间中的虚拟窗口。原点在屏幕中央,所以x轴和y轴的值都是从-size到+size。这就是为什么会有GLKMathDegreesToRadians (fieldOfView) / 2.0将窗口分为两部分——视角的角度是从-30度到+30度的。乘以zNear就可以计算出近剪裁面在坐标轴各个方向上的大小。这就是正切函数的作用了,眼睛在z轴上,到原点的距离是zNear,视域被z轴分为上下两部分各为30度,所以就可以明白size就是近剪裁面在x和y轴上的长度。

    第8行将计算的左右上下以及近剪裁面和远剪裁面的值传进glFrustumf函数。这里下边和上边的值都除以了aspectRatio(屏幕宽高比),而左右边没有,这是因为调用glLoadIdentity函数标准化投影矩阵的时候将所有的顶点数据都标准化到了-1~1的范围内,屏幕宽度和高度实际大小不一样,但都被标准化成了1。所以如果左右值和上下值一样的话得到的就是一个宽度比较大而高度比较小的长方形,而不是预期的正方形,所以左右值不变,而上下值要除以宽高比。

        第9行用来设置视口,一般为屏幕的大小。不过你可以根据需要来设置坐标和宽度、高度。

        第10行为将当前的矩阵从投影矩阵设置为模型视图矩阵。

      

        由于设置投影视角的方法基本上是固定的,只不过需要调整一下近剪裁面和远剪裁面以及视角的大小,所以可以将setClipping函数进一步封装为:

-(void)setClippingWithNear:(const float)zNear far:(const float)zFar angle:(const float) fieldOfView
{

float aspectRatio;
GLfloat size;
CGRect frame = [[UIScreen mainScreen] bounds]; //4

aspectRatio=(float)frame.size.width/(float)frame.size.height; //5

 

//Set the OpenGL projection matrix.

glMatrixMode(GL_PROJECTION); //6
glLoadIdentity();
size = zNear * tanf(GLKMathDegreesToRadians (fieldOfView) / 2.0); //7
glFrustumf(-size, size, -size /aspectRatio, size /aspectRatio, zNear, zFar); //8
glViewport(0, 0, frame.size.width, frame.size.height); //9
//Make the OpenGL ModelView matrix the default.
glMatrixMode(GL_MODELVIEW); //10

}

 初识openGL---openGL学习笔记(一) http://blog.csdn.net/a358333644/article/details/50732170

openGL:

openGL是用C语言实现的,而我们作为安卓开发者,严格来讲,我们使用的是es,也就是openGL es,原因无非就像数据库一样,安卓上使用的数据库永远不可能是MySQL之类的,因为终端讲究轻量。

接下来我们来创建第一个openGL项目:

首先我们来了解一下GLSurfaceView这个类,这个类相当于是在屏幕上的一个模板,如下图。

所以,创建一个类继承GLSurfaceView之后,我们就可以new出GLSurfaceView类的对象,调用GLSurfaceView类的对象的setRenderer()方法,就可以对GLSurfaceView进行渲染,而我们的主要工作是,如何进行渲染。

class MyGLSurfaceView extends GLSurfaceView{

    public MyGLSurfaceView(Context context) {
        super(context);
    }

    public MyGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    }

 

 

那么就要自定义一个渲染器:new一个类实现Renderer,需重写三个方法:onSurfaceCreated,onSurfaceChanged和onDrawFrame。

 

//自定义渲染器
class MyRenderer implements GLSurfaceView.Renderer{
    //表层创建时调用
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //设置清屏色
        gl.glClearColor(0, 0, 0, 1);
        //启用顶点缓冲区
        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);

    }
    //当表层size改变时调用
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

        //设置视口,输出画面的区域。
        gl.glViewport(0,0,width,height);
        //宽高比
        float ratio = (float)width/(float)height;
        //矩阵模式,投影矩阵,openGL基于状态机。
        gl.glMatrixMode( GL10.GL_PROJECTION );
        //加载单位矩阵
        gl.glLoadIdentity();
        //设置平截头体
        gl.glFrustumf(-1f,1f,-ratio,ratio,3,7); //-1f,1f为平截头体左右大小比,-ratio,ratio为平截头体底面和顶面,3,7为近平面和原平面距离

    }
    //绘图调用
    @Override
    public void onDrawFrame(GL10 gl) {
        //清除颜色缓冲区。
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

        //模型视图矩阵。
        gl.glMatrixMode( GL10.GL_MODELVIEW );
        //加载单位矩阵。
        gl.glLoadIdentity();
        //0,0,5:放置眼球的坐标。
        //0,0,0:眼球观察的中心点坐标。
        //0,1,0:指定眼球向上的向量。
        GLU.gluLookAt(gl,0,0,5,0,0,0,0,1,0);

        /**
         * 画三角形
         */

        //三角形顶点坐标
        float [] coords = {
                0f,0.5f,0f,
                -0.5f,-0.5f,0f,
                0.5f,-0.5f,0f
        };
        //分配字节缓存空间,存放顶点坐标数据。
        ByteBuffer ibb = ByteBuffer.allocateDirect(coords.length * 4);
        //设置顺序(本地顺序)。
        ibb.order(ByteOrder.nativeOrder());
        //放置顶点数组。
        FloatBuffer fbb = ibb.asFloatBuffer();
        fbb.put(coords);
        //定位指针位置,从该位置开始读取顶点数据。
        ibb.position(0);

        //设置绘图颜色:红色。
        gl.glColor4f(1f,0,0,1f);
        //3→三维点,使用三个坐标表示一个点。
        //每个点的数据类型。
        //0→跨度。
        //ibb指定缓冲区。
        gl.glVertexPointer(3,GL10.GL_FLOAT,0,ibb);
        //画数组
        gl.glDrawArrays(GL10.GL_TRIANGLES,0,3);


    }
}

显而易见,onSurfaceCreated和onSurfaceChanged都是在做初始化工作,关键的是onDrawFrame方法:

 

需要注意的是,onSurfaceChanged中设置的平截头体,所谓平截头体就是一个类似棱锥的区域,如下图:

 


 

最左边的眼睛通过中间墙上的孔,可以看得见与孔相同比例的空间,那么墙上的孔到墙后景物中间的四棱锥,就是一个平截头体。墙上的孔叫近平面,后方景物叫远平面。因为光是直射的,所以,我们只要指定好近平面的比例,以及图中两段红线的比例,那么就能确定远平面,继而确定平截头体。

 

确定了平截头体之后,就要关注onDrawFrame方法,类似于拍照的过程,我确定了要拍什么之后,还是要确定拍照时候要确定相机的位置,相机的朝向,相机的反正。这个三个属性就对应了程序中的GLU.gluLookAt()方法。详细参数见代码注释。

 

通过这几步之后,还是不能拍照,因为我们还有一些参数没有设置,比如绘图颜色。上例程序中是画了一个三角形,那么还需指定三角形的三点坐标等等,具体细节代码中注释的很详细,不再赘述。

posted @ 2016-11-30 14:25  mydddfly  阅读(5034)  评论(0编辑  收藏  举报