OpenGL.ES在Android上的简单实践:13-全景(画个球)

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

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

13-全景(画个球)

1、画个球

继续上一节的操作,我们已经通过两次for循环遍历了一个球的所有网格矩形的顶点,并用List存储起来。并且用另外的一个List存储所要画的三角形顶点的索引值。接下来,我们就要开始使用VBO-IBO画出网格矩形,最终画出整个球体出来。

    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)
            {
                ... ...
            }
        }

        numElements = indexList.size();// 记录有多少个索引点
        // 创建 顶点数据缓存对象
        float[] data_vertex = new float[vertexList.size()];
        for (int i = 0; i < vertexList.size(); i++) {
            data_vertex[i] = vertexList.get(i);
        }
        vertexBuffer = new VertexBuffer(data_vertex);
        // 创建 索引缓存对象
        short[] data_index = new short[indexList.size()];
        for (int i = 0; i < indexList.size(); i++) {
            data_index[i] = indexList.get(i);
        }
        indexBuffer = new IndexBuffer(data_index);
    }

数据准备好了,下一步我们准备着色器程序,这个球的着色器比较简单,我们只需要把顶点数据画出来,至于颜色值,我们这次没有附加到顶点数据里面,那就直接指定就好了。下面直接贴上着色器的vertex_shader和fragment_shader的代码:

uniform mat4 u_Matrix;      //最终的变换矩阵
attribute vec4 a_Position;  //顶点位置

void main()
{
    gl_Position = u_Matrix * a_Position;
}
precision mediump float;

void main()
{
    vec4 color = vec4(1.0, 0.2, 0.8, 0);
    // 我们这次直接在片段着色器内部指定颜色。
    gl_FragColor = color;
}

这组着色器比正方体的更为简单,不难理解。(以后会安排关于shader的进阶学习,(^-^)V)

然后我们直接构建着色器程序,并获取其中的属性变量,贴上模板代码:

public class BallShaderProgram extends ShaderProgram {

    protected static final String U_MATRIX = "u_Matrix";
    public final int uMatrixLocation;

    protected static final String A_POSITION = "a_Position";
    public final int aPositionLocation;

    public BallShaderProgram(Context context ) {
        super(context, R.raw.ball_vs, R.raw.ball_fs);

        uMatrixLocation = GLES20.glGetUniformLocation(programId, U_MATRIX);
        aPositionLocation = GLES20.glGetAttribLocation(programId, A_POSITION);
    }

    public void setUniforms(float[] matrix){
        GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
    }
}

最后,我们再准备一个方法,用于把顶点数据VBO设置到着色器程序属性的使能接口。超简单,如下:

    private void setAttributeStatus() {
        vertexBuffer.setVertexAttributePointer(
                ballShaderProgram.aPositionLocation,
                POSITION_COORDIANTE_COMPONENT_COUNT,
                0, 0 );
    }

好了,我们看看现在球体类Ball.java的整体代码结构:

public class Ball {
    private static final int POSITION_COORDIANTE_COMPONENT_COUNT = 3; // 每个顶点的坐标数 x y z

    private Context context;
    IndexBuffer indexBuffer; // 顶点数据缓存对象
    VertexBuffer vertexBuffer; // 索引缓存对象
    BallShaderProgram ballShaderProgram; // 着色器程序
    float[] modelMatrix = new float[16]; // 模型矩阵

    public Ball(Context context){
        this.context = context;
        Matrix.setIdentityM(modelMatrix,0);
        initVertexData();
        buildProgram();
        // setAttributeStatus();
    }

    private void initVertexData() {
        ... ...
        // 节省篇幅,具体内容参照以上和前一节文章。
    }

    private void buildProgram() {
        ballShaderProgram = new BallShaderProgram(context);
        ballShaderProgram.userProgram();
    }

    private void setAttributeStatus() {
        vertexBuffer.setVertexAttributePointer(
                ballShaderProgram.aPositionLocation,
                POSITION_COORDIANTE_COMPONENT_COUNT,
                0, 0 );
    }
}

2、还是画个球。

球体类ball已经准备差不多了,我们回到PanoramaRenderer,创建一个ball,并添加一系列的模板代码:

public class PanoramaRenderer implements GLSurfaceView.Renderer{

    private final Context context;
    private final float[] modelViewProjectionMatrix = new float[16];
    private final float[] viewProjectionMatrix = new float[16];
    private final float[] projectionMatrix = new float[16];
    private final float[] viewMatrix = new float[16];

    public PanoramaRenderer(Context context) {
        this.context = context;
        Matrix.setIdentityM(projectionMatrix,0);
        Matrix.setIdentityM(viewMatrix,0);
        Matrix.setIdentityM(viewProjectionMatrix,0);
        Matrix.setIdentityM(modelViewProjectionMatrix,0);
    }

    Ball ball;

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        ball = new Ball(context);
    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        GLES20.glViewport(0,0,width,height);
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);
        // 打开深度测试
        MatrixHelper.perspectiveM(projectionMatrix, 45, (float)width/(float)height, 1f, 100f);
        Matrix.setLookAtM(viewMatrix, 0,
                0f, 0f, 3f,
                0f, 0f, 0f,
                0f, 1f, 0f);
        Matrix.multiplyMM(viewProjectionMatrix,0,  projectionMatrix,0, viewMatrix,0);
    }

    @Override
    public void onDrawFrame(GL10 gl10) {
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        // ball->draw
    }
}

到这里,我们就差最后一步draw了。那究竟要怎么draw呢?我们想想之前的正方体的代码。每次draw之前都实时更新mvp矩阵,我们现在ball的着色器程序封装在里面了,不单独暴露在外,所以我们draw的接口要稍微改写一下,需要接受mvp矩阵并更新到着色器里面。代码如下:

public void draw(float[] modelViewProjectionMatrix) {
        ballShaderProgram.userProgram();
        setAttributeStatus();
        // 将最终变换矩阵写入
        ballShaderProgram.setUniforms(modelViewProjectionMatrix);

        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.getIndexBufferId());
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, numElements, GLES20.GL_UNSIGNED_SHORT, 0);
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
    }

更新着色器程序的矩阵后,我们就要激活索引缓存对象IBO,并用一个int值numElements记录索引元素的数目,然后调用GLES20.glDrawElements接口绘制索引值;注意第三个参数GL_UNSIGNED_SHORT,因为我们的索引值类型是short;最后绘制成功后解绑缓存对象。

现在,让我们启动程序,打开PanoramaActivity,看看ball是什么效果?

3、增加纹理

以上程序运行后,画面呈现一个圆形,我们根本看不出这是一个完整的球。我们可以在这个圆形上贴上一张全景图,最典型的全景图就是世界地图或者从其实视觉网站复制一张下来,如下:

那么,我们就在以上的着色器程序的基础上添加纹理吧。纹理相关的知识我们说得不多,基础的知识参考这里。我们这此机会好好复习一下纹理的知识。

首先,添加纹理到OpenGL并显示出来,着色器程序需要两个关键,一个是纹理单元,另外一个就是纹理坐标了。我们更新顶点着色器和片段着色器。

uniform mat4 u_Matrix;      //最终的变换矩阵
attribute vec4 a_Position;  //顶点位置

attribute vec2 a_TextureCoordinates; //纹理坐标
varying vec2 v_TextureCoordinates; //传递給片段着色器

void main()
{
    gl_Position = u_Matrix * a_Position;
    v_TextureCoordinates = a_TextureCoordinates;
}
precision mediump float;

uniform sampler2D u_TextureUnit; // 纹理采样器
varying vec2 v_TextureCoordinates; // 纹理坐标

void main()
{
    // vec4 color = vec4(1.0, 0.2, 0.8, 0);
    // gl_FragColor = color;
    gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates); // 纹理采样
}

对应的,我们需要更新着色器程序BallShaderProgram

    ... ...
    protected static final String A_TEXTURE_COORDINATES = "a_TextureCoordinates";
    public final int aTextureCoordinatesLocation;

    protected static final String U_TEXTURE_UNIT = "u_TextureUnit";
    public final int uTextureUnitLocation;

    public BallShaderProgram(Context context ) {
        super(context, R.raw.ball_vs, R.raw.ball_fs);
        ... ...
        aTextureCoordinatesLocation=GLES20.glGetAttribLocation(programId, A_TEXTURE_COORDINATES);
        uTextureUnitLocation = GLES20.glGetUniformLocation(programId, U_TEXTURE_UNIT);
    }

接下来,我们在Ball类中添加纹理的初始化模板代码,就一行代码,so easy:

    private int textureId;
    public Ball(Context context){
        ... ...
        initVertexData();
        initTexture();
        buildProgram();
    }
    private void initTexture() {
        textureId = TextureHelper.loadTexture(context, R.mipmap.world);
    }

同样更新着色器程序BallShaderProgram赋值接口setUniforms,增加纹理单元的赋值

    public void setUniforms(float[] matrix,int textureId){
        GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
        // 激活纹理单元0
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        // 绑定纹理对象ID
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
        // 告诉shaderProgram sampler2D纹理采集器 使用纹理单元0的纹理对象。
        GLES20.glUniform1i(uTextureUnitLocation, 0);
    }

好了,我们打通了着色器程序纹理单元这一道了,剩下纹理坐标。那么问题来了,纹理坐标怎么计算?我们参考球体坐标,其实一个三维圆降到二维,就是一个长方矩形,我们很好的就能对应到全景图的纹理坐标。我们还是沿用以前的方法,把坐标数据和纹理数据放在一起,当作一组完整的顶点数据,保存统一缓存对象中。我们修改ball的initVertexData代码:

public class Ball {
    private static final int POSITION_COORDIANTE_COMPONENT_COUNT = 3; // 每个顶点的坐标数 x y z
    private static final int TEXTURE_COORDIANTE_COMPONENT_COUNT = 2; // 每个顶点的坐标数 s t
    private static final int STRIDE = (POSITION_COORDIANTE_COMPONENT_COUNT
            + TEXTURE_COORDIANTE_COMPONENT_COUNT)
            * Constants.BYTES_PER_FLOAT;

    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)
            {
                // st纹理坐标
                float s0 = hAngle / 360.0f; //左上角 s
                float t0 = vAngle / 180.0f; //左上角 t
                float s1 = (hAngle + angleSpan)/360.0f; //右下角s
                float t1 = (vAngle + angleSpan)/180.0f; //右下角t
                // 左上角 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);
                vertexList.add(s0);
                vertexList.add(t0);
                // 右上角 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);
                vertexList.add(s1);
                vertexList.add(t0);
                // 右下角 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);
                vertexList.add(s1);
                vertexList.add(t1);
                // 左下角 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);
                vertexList.add(s0);
                vertexList.add(t1);

                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个顶点的偏移
            }
        }
        numElements = indexList.size();// 记录有多少个索引点

        float[] data_vertex = new float[vertexList.size()];
        for (int i = 0; i < vertexList.size(); i++) {
            data_vertex[i] = vertexList.get(i);
        }
        vertexBuffer = new VertexBuffer(data_vertex);

        short[] data_index = new short[indexList.size()];
        for (int i = 0; i < indexList.size(); i++) {
            data_index[i] = indexList.get(i);
        }
        indexBuffer = new IndexBuffer(data_index);
    }

}

我们利用球坐标的两个弧角度,在双for循环中求出同一位置下的纹理和位置坐标,并添加到list中。随后我们就要更新setAttributeStatus方法,使得设置着色器程序正确,更新的代码如下:

    private void setAttributeStatus() {
        // 每一组完整的顶点数据间隔STRIDE个字节,请注意
        vertexBuffer.setVertexAttributePointer(
                ballShaderProgram.aPositionLocation,
                POSITION_COORDIANTE_COMPONENT_COUNT,
                STRIDE, 0 ); 
        // 每一组完整的顶点数据由  x y z s t 排列组成,所以纹理数据要先偏移正确的字节数,才能载入。
        vertexBuffer.setVertexAttributePointer(
                ballShaderProgram.aTextureCoordinatesLocation,
                TEXTURE_COORDIANTE_COMPONENT_COUNT,
                STRIDE,
                POSITION_COORDIANTE_COMPONENT_COUNT * Constants.BYTES_PER_FLOAT);
    }

此时运行项目,看看效果是否如下:

                                                       

没毛病是吧?额,还真有点毛病,还记得我们设置的观察矩阵(相机)的位置参数是多少吗?不记得我贴上来:

        Matrix.setLookAtM(viewMatrix, 0,
                0f, 0f, 4f,// x,y,z
                0f, 0f, 0f,
                0f, 1f, 0f);

明明是在z轴上,根据OpenGL在Android上的世界坐标,z轴就是垂直屏幕xy的平面,平视球体的中心,也就是我们看到的应该是地球赤道啊,为啥我们现在看到的是北极圈?哈,毛病是在我们上篇文章百度回来的球体坐标,看清楚它的xyz坐标方向和我们的OpenGL在Android上的世界坐标方向,原来是不一样的!我们要把它调整回正确的方向。球体坐标的xyz对应AndroidOpenGL的zxy。请大家注意。

还有一点:不知道有没同学疑问,正常的OpenGL纹理坐标是st是至下而上,至左向右的延伸。那么对应我们球体的双for循环, 左上角0 不应该对应的是 s0t1?右上角1 不应该对应的是 s1t1?这个和Android的图片原点坐标有关,详细请参考之前的文章

猜你喜欢

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