OpenGL.ES在Android上的简单实践:12-全景(VBO-IBO)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a360940265a/article/details/79643546

OpenGL.ES在Android上的简单实践:

12-全景(VBO-IBO)

 

1、VAO?VBO?

还记得我们的之前学习的VAO吗?不清楚的同学请看这里,其实就是我们的模板代码VertexArray,那这里新蹦出来的VBO又是什么?首先我们先来认识VBO的全称——VertexBufferObject,顶点缓存对象。这和VAO——VertexArrayObject的差别就在于一个VBO是缓存的对象,另一个是现存的数组。怎么理解?这要从OpenGL的工作机理说起。

什么是OpenGL?其实这个问题很简单,它就是应用程序接口,也就是API,用于访问图形硬件中的可编程特性。OpenGL和DX相比有一个很大的特点就是跨平台的特性。换句话说,它是不依赖硬件的接口,可以运行在各种不同类型的图形硬件系统上,甚至完全是一个软件(而没有图形硬件)。

那么OpenGL是怎样做到其“跨平台”的特性呢?这是因为,OpenGL在Android上的具体实现,由Google在系统底层去完成;OpenGL在Mac/Windows上的实现,由Apple/Windows在系统底层去完成;他们都基于OpenGL的一套基准,这样就能完美的达到了跨平台的效果了。

OpenGL更是一种客户端-服务器(client-server)类型的系统。其实用到的是RPC机制(远程过程调用协议)。我们编写的程序就是一个客户端,而我们的计算机图形硬件制造商(Google/Apple/Windows)提供的OpenGL的实现就是服务器,应用层发送GL指令,通知底层硬件直达GPU,实现高效的绘制渲染。

好了,扯了那么多。这和VAO/VBO的区别有毛线关系?注意了,区别就是在VAO是放在客户端的顶点数据,而VBO是已经保存在服务端的顶点数据。这下明白了吗?

基于这点,我们分别比较VAO和VBO的模板代码,注意分析:

public class VertexArray {

    private final FloatBuffer floatBuffer;

    public VertexArray(float[] vertexData) {
        floatBuffer = ByteBuffer.allocateDirect(vertexData.length*Constants.BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vertexData);
    }

    public void setVertexAttributePointer(int attributeLocation,
                                       int componentCount, int stride, int dataOffset){
        floatBuffer.position(dataOffset);
        GLES20.glVertexAttribPointer(attributeLocation, componentCount, GLES20.GL_FLOAT,
                false, stride, floatBuffer);
        GLES20.glEnableVertexAttribArray(attributeLocation);
        floatBuffer.position(0);
    }
}

从GLES20.glVertexAttribPointer接口就可以注意到,我们每次绑定着色器程序的attributeLocation,都是指定具体的vertexData的bytebuffer,想想我们之前的曲棍球,画桌子画冰球画木槌,都是要更改相应的着色器程序并赋值顶点数据。每次都需要客户端-服务端的传递数据,性能是比较低的。

接下来,我们来编写VBO的模板代码,在VertexArray同目录下创建VertexBuffer,并添加如下代码:

public class VertexBuffer {

    private final int bufferId;

    public int getVertexBufferID() {
        return bufferId;
    }

    public VertexBuffer(float[] vertexData) {
        // 第一步,我们向OpenGL服务端申请创建缓冲区
        final int buffers[] = new int[1];
        GLES20.glGenBuffers(buffers.length, buffers, 0);
        if (buffers[0] == 0) {
            int i = GLES20.glGetError();
            throw new RuntimeException("Could not create a new vertex buffer object, glGetError : "+i);
        }
        // 保存申请返回的缓冲区标示
        bufferId = buffers[0];
        // 绑定缓冲区 为 数组缓存
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
        // 把java数据放转至到native
        FloatBuffer vertexArry = ByteBuffer.allocateDirect(vertexData.length*Constants.BYTES_PER_FLOAT)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vertexData);
        vertexArry.position(0);
        // 把native的数据绑定保存到缓存区,注意长度为字节单位。用途是为GL_STATIC_DRAW
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexArry.capacity()*Constants.BYTES_PER_FLOAT,
                vertexArry, GLES20.GL_STATIC_DRAW);
        // 告诉OpenGL 解绑缓冲区的操作。
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
    }
}

我们来分析以上的模板代码,首先一开始就是OpenGL的模板代码,类似纹理glGenTextures,我们向OpenGL服务端申请创建缓冲区,并保存其返回的缓冲区标示;接下来注意,我们glBindBuffer绑定指定的缓冲区,告诉OpenGL接下来的操作是针对bufferId的缓冲区,之后把native的顶点数据绑定保存到缓冲区对象中,最好解绑缓冲区,通知OpenGL缓冲区对象的操作已经完成了。

那么对应的设置着色器程序属性又有什么区别呢?看看如下代码:

    public void setVertexAttributePointer(int attributeLocation,
                                          int componentCount, int stride, int dataOffset){
        // 先绑定标示缓冲区,通知OpenGL要使用指定的缓冲区了。
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId);
        // 调用接口,设置着色器程序顶点属性指针
        GLES20.glVertexAttribPointer(attributeLocation, componentCount, GLES20.GL_FLOAT,
                false, stride, dataOffset);
        // 使能着色器的属性
        GLES20.glEnableVertexAttribArray(attributeLocation);
        // 告诉OpenGL 解绑缓冲区的操作。
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
    }

这和之前VAO的区别就是 调用设置着色器程序顶点属性指针方法的参数设置不同了(方法被重载),在设置着色器的时候需要进行绑定缓冲区的操作,这样OpenGL服务端才能知道操作哪组顶点缓存对象。

2、IBO——IndexBufferObject 索引缓冲区

既然顶点数据能缓存到OpenGL服务端,相对于索引(index)也是一种数据,同样的也是可以缓存到OpenGL中保存为对象。只不过在创建绑定buffers空间的时候,和顶点的数据类型不同。我们马上po上代码:

public class IndexBuffer {

    private final int indexBufferId;

    public int getIndexBufferId() {
        return indexBufferId;
    }

    public IndexBuffer(short[] indexData) {
        //allocate a buffer
        final int buffers[] = new int[1];
        GLES20.glGenBuffers(buffers.length, buffers, 0);
        if (buffers[0] == 0) {
            throw new RuntimeException("Could not create a new vertex buffer object");
        }
        indexBufferId = buffers[0];
        //bind to the buffer
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, buffers[0]);
        //Transfer data to native memory.
        ShortBuffer indexArry = ByteBuffer.allocateDirect(indexData.length*Constants.BYTES_PER_SHORT)
                .order(ByteOrder.nativeOrder())
                .asShortBuffer()
                .put(indexData);
        indexArry.position(0);
        GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexArry.capacity()*Constants.BYTES_PER_SHORT,
                indexArry, GLES20.GL_STATIC_DRAW);
        //IMPORTANT! unbind the buffer when done with it
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
    }

    public IndexBuffer(int[] indexData){
        final int buffers[] = new int[1];
        GLES20.glGenBuffers(buffers.length, buffers, 0);
        if (buffers[0] == 0) {
            throw new RuntimeException("Could not create a new vertex buffer object");
        }
        indexBufferId = buffers[0];

        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, buffers[0]);
        IntBuffer indexArry = ByteBuffer.allocateDirect(indexData.length*Constants.BYTES_PER_INT)
                .order(ByteOrder.nativeOrder())
                .asIntBuffer()
                .put(indexData);
        indexArry.position(0);
        GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexArry.capacity()*Constants.BYTES_PER_INT,
                indexArry, GLES20.GL_STATIC_DRAW);
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
    }

}

其实和VertexBuffer是一样的模板代码,区别就是在数据类型,索引使用的是 GLES20.GL_ELEMENT_ARRAY_BUFFER。我们重载构造函数,使得多种数据类型的数组能同样的创建IBO。

3、使用VBO-IBO创建全景球

全景顾名思义就是给人以三维立体感觉的实景水平360度竖直360度全方位图像,此图像最大的三个特点是:
1、全:全方位,全面的展示了360度球型范围内的所有景致;上篇文章的例子中用鼠标左键按住拖动,观看场景的各个方向;
2、景:实景,真实的场景,三维实景大多是在照片基础之上拼合得到的图像,最大限度的保留了场景的真实性;
3、360°:双360°环视的效果,虽然照片都是平面的,但是通过软件处理之后得到的360度实景,却能给人以三维立体的空间感觉,使观者犹如身在其中。

从这三点特性,我们可以知道,平面的二维图需要转变为360度的球型范围,以达到环视的效果;那么接下来,我们一起构造一个球吧。

在项目中我们创建panorama子目录,创建PanoramaRenderer,PanoramaActivity。基本的生命周期方法参照之前的模板代码。接下来,我们在objects子目录创建新类Ball.java;然后我们来分析一下怎样构建一个球坐标的顶点数据。

                                           

在数学里,球坐标系是一种利用球坐标  表示一个点 p 在三维空间的位置的三维正交坐标系。上图显示了球坐标的几何意义:原点与点 P 之间的径向距离 r ,原点到点 P 的连线与正 z-轴之间的天顶角(θ),以及原点到点 P 的连线,在 xy-平面的投影线,与正 x-轴之间的方位角(φ)。

球坐标系(r,θ,φ)与直角坐标系(x,y,z)的转换关系:

x=r · sinθ · cosφ.

y=r · sinθ · sinφ.

z=r · cosθ.

好了,理论知识有了,那么怎么利用OpenGL的机制转变成代码呢?我们前面已经知道,在OpenglES中任何形状的3D物体都是用三角形来实现了,我们的球自然不能免俗,那我们该如何用一堆三角形来完成这个球的绘制呢?回想一下我们的圆是怎么绘制的,对于圆,我们实际绘制的是一个正N边形,当N足够大时我们看起来就是一个圆了,类比一下我们的球也是一样,按照矩形的方式来看,我们绘制的球实际上是这样的,我们俗称这种矩形叫网格:

                                                

实际上,我们想要绘制这样一个个的矩形,那么我们把其中一个矩形单独拿来作分析 :

我们如何确定这个矩形的位置?我们只需要知道这个矩形的左上角的顶点坐标就可以确定该矩形的位置,这个顶点坐标显然与三个值有关,角度φ (范围为 0 到360,单位 :度) ,角度θ (范围为 0 到 180,单位:度 ),以及球的半径  r  ,r 可以是一个固定值 ,因此我们通过双重循环就可以得到所有的矩形的左上角顶点坐标了。

        final int angleSpan = 5;// 将球进行单位切分的角度,此数值越小划分矩形越多,球曲面越趋近平滑
        float radius = 1.0f;

        for (int vAngle = 0; vAngle < 180; vAngle = vAngle + angleSpan)
        {
            for (int hAngle = 0; hAngle <= 360; hAngle = hAngle + angleSpan)
            {
                // 纵向横向各到一个角度后计算对应的此点在球面上的坐标
                float x0 = (float) (radius * Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
                        .toRadians(hAngle)));
                float y0 = (float) (radius * Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
                        .toRadians(hAngle)));
                float z0 = (float) (radius * Math.cos(Math.toRadians(vAngle)));
            }
        }

得到矩形的左上角的顶点坐标后,其他三个坐标就很容易求得了。

然后我们使用刚学的索引的知识,锁定要画的那个网格矩形——两个三角形。

    private void initVertexData() {
        final int angleSpan = 5;// 将球进行单位切分的角度,此数值越小划分矩形越多,球面越趋近平滑
        final float radius = 1.0f;// 球体半径
        short offset = 0;// 索引偏移
        ArrayList<Float> vertexList = new ArrayList<>(); // 使用list存放顶点数据
        ArrayList<Short> indexList = new ArrayList<>();// 顶点索引数组
        for (int vAngle = 0; vAngle < 180; vAngle = vAngle + angleSpan)
        {
            for (int hAngle = 0; hAngle <= 360; hAngle = hAngle + angleSpan)
            {
                // 左上角 0
                float x0 = (float) (radius * Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
                        .toRadians(hAngle)));
                float y0 = (float) (radius * Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
                        .toRadians(hAngle)));
                float z0 = (float) (radius * Math.cos(Math.toRadians(vAngle)));
                vertexList.add(x0);
                vertexList.add(y0);
                vertexList.add(z0);
                // 右上角 1
                float x1 = (float) (radius * Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
                        .toRadians(hAngle + angleSpan)));
                float y1 = (float) (radius * Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
                        .toRadians(hAngle + angleSpan)));
                float z1 = (float) (radius * Math.cos(Math.toRadians(vAngle)));
                vertexList.add(x1);
                vertexList.add(y1);
                vertexList.add(z1);
                // 右下角 2
                float x2 = (float) (radius * Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
                        .cos(Math.toRadians(hAngle + angleSpan)));
                float y2 = (float) (radius * Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
                        .sin(Math.toRadians(hAngle + angleSpan)));
                float z2 = (float) (radius * Math.cos(Math.toRadians(vAngle + angleSpan)));
                vertexList.add(x2);
                vertexList.add(y2);
                vertexList.add(z2);
                // 左下角 3
                float x3 = (float) (radius * Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
                        .cos(Math.toRadians(hAngle)));
                float y3 = (float) (radius * Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
                        .sin(Math.toRadians(hAngle)));
                float z3 = (float) (radius * Math.cos(Math.toRadians(vAngle + angleSpan)));
                vertexList.add(x3);
                vertexList.add(y3);
                vertexList.add(z3);

                indexList.add((short)(offset + 0));
                indexList.add((short)(offset + 3));
                indexList.add((short)(offset + 2));
                indexList.add((short)(offset + 0));
                indexList.add((short)(offset + 2));
                indexList.add((short)(offset + 1));
                offset += 4; // 4个顶点的偏移
            }
        }
    }

好了顶点数据和对应的索引都准备好了,下一节我们就把顶点数据和索引数据转到OpenGL的对应缓冲区中,绘制一个球。

小结:这节的内容知识比较丰富,我们从OpenGL的宏观设计出发,了解了其跨平台的设计方式,和客户端服务端的运作方式。然后学习了VBO顶点缓存对象,区别开VAO-VBO;在上一节索引的基础上,学习了IBO索引缓存对象。

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/79643546