[OpenGL]从零开始写一个Android平台下的全景视频播放器——3.2 使用OpenGL ES 2.0绘制一个球

本文介绍了如何使用OpenGL ES 2.0在Android平台上绘制一个360度全景视频播放器的球体。通过将球面划分为多个三角形并利用坐标推导,实现球体的圆润效果。文章提到了坐标系的理解、绘制原理,并给出了代码示例,展示了最终的渲染效果。

Github项目地址

为了方便没有准备好梯子的同学,我把项目在CSDN上打包下载,不过更新会慢一些

回到目录

如何用OpenGL绘制一个球呢?其实方法很多,网上一搜就能搜到一大把。

坐标系

要想知道怎么画,当然要把坐标系先搞清楚啦,上图:
这里写图片描述

这是世界坐标(看懂这张图需要一定想象力),Y轴是和重力方向相反的,X轴和水平面齐平,我们(相机)站在Z轴上,观察点是在球内的。
这里写图片描述
这是世界坐标(3D)对应的纹理坐标(2D),不过我们现在还不需要考虑这个,等第五章再来讨论纹理映射的事。

原理

绘制球的原则其实和绘制平面并没有什么区别,那么球的坐标要如何表示呢?我们知道三角形是OpenGL的基本形状,所以我们就将球面划分成许多个三角形,当我们划分的个数足够多时,整个球看起来就比较圆润了,像这样:
这里写图片描述
这里写图片描述
这里写图片描述

最后一张图是用矩形表示的终极形状(是不是很像一个地球仪),我们还需要把矩阵切分成两个三角形进行绘制。

坐标推导公式就不上了,估计也没人看,直接上代码吧。

下面是一张百度上爬来的图(坐标系标的不符合我们的要求,凑合着看吧):
这里写图片描述

题外话

在贴出球的代码之前,我们先将之前绘制平面视频的代码块提取出来,像这样:

package com.martin.ads.panoramaopengltutorial;

import android.opengl.GLES20;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import static com.martin.ads.panoramaopengltutorial.ShaderUtils.checkGlError;

public class Plain {
    private final FloatBuffer mVerticesBuffer;
    private FloatBuffer mTexCoordinateBuffer;

    private final float[] vertexData = {
            1f,-1f,0f,
            -1f,-1f,0f,
            1f,1f,0f,
            -1f,1f,0f
    };
    private final float[] textureVertexData = {
            1f,0f,
            0f,0f,
            1f,1f,
            0f,1f
    };
    public Plain() {
        mVerticesBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vertexData);
        mVerticesBuffer.position(0);

        mTexCoordinateBuffer = ByteBuffer.allocateDirect(textureVertexData.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(textureVertexData);
        mTexCoordinateBuffer.position(0);
    }

    public void uploadVerticesBuffer(int positionHandle){
        FloatBuffer vertexBuffer = getVerticesBuffer();
        if (vertexBuffer == null) return;
        vertexBuffer.position(0);

        GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
        checkGlError("glVertexAttribPointer maPosition");
        GLES20.glEnableVertexAttribArray(positionHandle);
        checkGlError("glEnableVertexAttribArray maPositionHandle");
    }

    public void uploadTexCoordinateBuffer(int textureCoordinateHandle){
        FloatBuffer textureBuffer = getTexCoordinateBuffer();
        if (textureBuffer == null) return;
        textureBuffer.position(0);

        GLES20.glVertexAttribPointer(textureCoordinateHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
        checkGlError("glVertexAttribPointer maTextureHandle");
        GLES20.glEnableVertexAttribArray(textureCoordinateHandle);
        checkGlError("glEnableVertexAttribArray maTextureHandle");
    }


    public FloatBuffer getVerticesBuffer() {
        return mVerticesBuffer;
    }

    public FloatBuffer getTexCoordinateBuffer() {
        return mTexCoordinateBuffer;
    }

    public void draw() {
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }

}

为什么要这样做呢?因为就像之前说的,绘制平面和绘制球体并没有本质的区别,所以我们完全可以抽象出一个类,只要把Plain换成Sphere,我们就完成了绘制模式的转变。

球体绘制

package com.martin.ads.panoramaopengltutorial;

import android.opengl.GLES20;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import static com.martin.ads.panoramaopengltutorial.ShaderUtils.checkGlError;

public class SphereNoTexture {

    private static final int sPositionDataSize = 3;

    private FloatBuffer mVerticesBuffer;
    private ShortBuffer indexBuffer;
    private int mNumIndices;

    public SphereNoTexture(float radius, int rings, int sectors) {
        final float PI = (float) Math.PI;
        final float PI_2 = (float) (Math.PI / 2);

        float R = 1f/(float)rings;
        float S = 1f/(float)sectors;
        short r, s;
        float x, y, z;

        int numPoint = (rings + 1) * (sectors + 1);
        float[] vertexs = new float[numPoint * 3];
        short[] indices = new short[numPoint * 6];

        //矩形的四个点
        int t = 0, v = 0;
        for(r = 0; r < rings + 1; r++) {
            for(s = 0; s < sectors + 1; s++) {
                x = (float) (Math.cos(2*PI * s * S) * Math.sin( PI * r * R ));
                y = (float) Math.sin( -PI_2 + PI * r * R );
                z = (float) (Math.sin(2*PI * s * S) * Math.sin( PI * r * R ));

                vertexs[v++] = x * radius;
                vertexs[v++] = y * radius;
                vertexs[v++] = z * radius;
            }
        }

        //球体绘制坐标索引,用于  glDrawElements
        int counter = 0;
        int sectorsPlusOne = sectors + 1;
        for(r = 0; r < rings; r++){
            for(s = 0; s < sectors; s++) {
                indices[counter++] = (short) (r * sectorsPlusOne + s);       //(a)
                indices[counter++] = (short) ((r+1) * sectorsPlusOne + (s));    //(b)
                indices[counter++] = (short) ((r) * sectorsPlusOne + (s+1));  // (c)
                indices[counter++] = (short) ((r) * sectorsPlusOne + (s+1));  // (c)
                indices[counter++] = (short) ((r+1) * sectorsPlusOne + (s));    //(b)
                indices[counter++] = (short) ((r+1) * sectorsPlusOne + (s+1));  // (d)
            }
        }

        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 4 bytes per float)
                vertexs.length * 4);
        bb.order(ByteOrder.nativeOrder());
        FloatBuffer vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(vertexs);
        vertexBuffer.position(0);

        // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 2 bytes per short)
                indices.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        indexBuffer = dlb.asShortBuffer();
        indexBuffer.put(indices);
        indexBuffer.position(0);

        mVerticesBuffer=vertexBuffer;
        mNumIndices=indices.length;
    }

    public void uploadVerticesBuffer(int positionHandle){
        FloatBuffer vertexBuffer = getVerticesBuffer();
        if (vertexBuffer == null) return;
        vertexBuffer.position(0);

        GLES20.glVertexAttribPointer(positionHandle, sPositionDataSize, GLES20.GL_FLOAT, false, 0, vertexBuffer);
        checkGlError("glVertexAttribPointer maPosition");
        GLES20.glEnableVertexAttribArray(positionHandle);
        checkGlError("glEnableVertexAttribArray maPositionHandle");
    }

    public FloatBuffer getVerticesBuffer() {
        return mVerticesBuffer;
    }

    public void draw() {
        indexBuffer.position(0);
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, mNumIndices, GLES20.GL_UNSIGNED_SHORT, indexBuffer);
    }
}

和之前绘制平面的代码对比一下,除了构造函数以外,是不是满满的套路。。

public SphereNoTexture(float radius, int rings, int sectors)

我们给构造函数传入半径,纬度切分数,经度切分数,一般sectors应该是rings的两倍(当然不是也看不出明显的区别),我们按照经纬度切分出多个矩形,然后把矩形再划分成两个小的三角形(使用下标索引的方式)

为什么还要rings + 1呢(sector类似),因为我们的最后矩形的右边界还应该和第一个矩形的左边界重合。

除了上面所说的,球体的绘制还有很多需要注意的小细节,大家可以对着代码慢慢揣摩

因为我们还没有做纹理映射,方便起见,我们将球的颜色设置为白色。
如果我们已经设置好了MVP矩阵(第五章),那么我们就能看到这样子的一个球:
这里写图片描述

呵呵,这真的是一个球,不是个圆啊!!!算了,加点噪声好了,这样明显一些:
这里写图片描述

当然,如果用棋盘纹理来表示也是棒棒哒(虽然这不是重点):
这里写图片描述

Github项目地址
回到目录

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值