Android OpenGLES2.0开发(五):绘制正方形和圆形

153 阅读6分钟

水到渠成,何须强求?静待花开,一切都会好的。

我们在前面的章节已经绘制了最基本的图形三角形,这一篇我们来画一个正方形和圆形。有了前面的基础,绘制正方形和圆形实际上就是手到擒来的事。

绘制正方形和圆形

来思考下正方形如何绘制?我们知道三角形是最基本的图形,那么正方形应该就是由两个三角形组成的,答案是肯定的。那么圆形如何绘制,其实就是正多边形的绘制,从原点绘制无数个三角形得到圆形。

正方形

正方形的构建比较简单,可以用两个三角形组成。当然,你也可以用很多很多三角形去合成一个正方形,只要你乐意。如下图所示,我们可以按照123组成的三角形和134组成的三角形,两个拼合成一个正方形。

未命名-3.png

1. 顶点数据定义

我们将Triangle类拷贝一份修改类名称为Square,并修改顶点数据如下:

public class Square {

	...
    // 正方形四个顶点坐标
    static float squareCoords[] = {
            -0.5f, 0.5f, 0.0f,   // top left
            -0.5f, -0.5f, 0.0f,  // bottom left
            0.5f, -0.5f, 0.0f,   // bottom right
            0.5f, 0.5f, 0.0f,    // top right
    };

    // 绘制顶点的顺序,3个数为一组
    private short drawOrder[] = {0, 1, 2, 0, 2, 3};

    private ShortBuffer mDrawIndexBuffer;
    ...

    public Square() {
        // 初始化形状坐标的顶点字节缓冲区
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 4 bytes per float)
                squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        mVertexBuffer = bb.asFloatBuffer();
        mVertexBuffer.put(squareCoords);
        mVertexBuffer.position(0);

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

2. 绘制正方形

我们只需要修改GLES20.glDrawArrays方法改为GLES20.glDrawElements通过索引绘制即可,将顶点绘制的索引数据传入

// 索引法绘制
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, mDrawIndexBuffer);

Screenshot_20241009-142215.jpg

圆形

圆形的构建,相对复杂一点,我们可以把圆形看成一个正多边形,边越多,圆越平滑。如下图所示,分别为正六边形、正八边形、正十六边形和正一百边形。

20161014185118697.jpg

以六边形为例,由012、023,034、045、056、061六个三角形,更多变形同样如此。 利用简单的数学知识,即可得到,以多边形中心建立直角坐标系,得到n变形的顶点坐标为:
private float[] createPositions(float radius, int n) {
    ArrayList<Float> data = new ArrayList<>();
    data.add(0.0f);             //设置圆心坐标
    data.add(0.0f);
    data.add(0.0f);
    float angDegSpan = 360f / n;
    for (float i = 0; i < 360 + angDegSpan; i += angDegSpan) {
        data.add((float) (radius * Math.sin(i * Math.PI / 180f)));
        data.add((float) (radius * Math.cos(i * Math.PI / 180f)));
        data.add(0.0f);
    }
    float[] f = new float[data.size()];
    for (int i = 0; i < f.length; i++) {
        f[i] = data.get(i);
    }
    return f;
}

1. 定义顶点数据

我们将Triangle类拷贝一份修改类名称为Circle,并修改顶点数据如下:

public class Circle {

    private float circleCoords[];

    public Circle() {
        circleCoords = createPositions(0.5f, 60);
        vertexCount = circleCoords.length / COORDS_PER_VERTEX;

        // 初始化形状坐标的顶点字节缓冲区
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (# of coordinate values * 4 bytes per float)
                circleCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(circleCoords);
        vertexBuffer.position(0);
    }
}

2. 绘制圆形

现在我们要绘制圆形了,但是我们发现没有定义绘制的顺序。我们要定义几十个三角形的绘制顺序工作量无疑是巨大的,有没有更方便的方式?

GLES20.glDrawArrays方法帮我们提供了多种绘制的方式,第一个参数表示绘制方式,第二个参数表示偏移量,第三个参数表示顶点个数

绘制方式如下:

int GL_POINTS       //将传入的顶点坐标作为单独的点绘制
int GL_LINES        //将传入的坐标作为单独线条绘制,ABCDEFG六个顶点,绘制AB、CD、EF三条线
int GL_LINE_STRIP   //将传入的顶点作为折线绘制,ABCD四个顶点,绘制AB、BC、CD三条线
int GL_LINE_LOOP    //将传入的顶点作为闭合折线绘制,ABCD四个顶点,绘制AB、BC、CD、DA四条线。
int GL_TRIANGLES    //将传入的顶点作为单独的三角形绘制,ABCDEF绘制ABC,DEF两个三角形
int GL_TRIANGLE_FAN    //将传入的顶点作为扇面绘制,ABCDEF绘制ABC、ACD、ADE、AEF四个三角形
int GL_TRIANGLE_STRIP   //将传入的顶点作为三角条带绘制,ABCDEF绘制ABC,BCD,CDE,DEF四个三角形

从上面的方式我们可以找到,绘制圆形我们采用GL_TRIANGLE_FAN方式就可以轻松实现

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount);

Screenshot_20241009-150309.jpg

再谈绘制

GL_TRIANGLE_STRIP

GL_TRIANGLE_STRIP的方式绘制连续的三角形,比直接用GL_TRIANGLES的方式绘制三角形少好多个顶点,效率会高很多。另外,GL_TRIANGLE_STRIP并不是只能绘制连续的三角形构成的物体,我们只需要将不需要重复绘制的点重复两次即可。比如,传入ABCDEEFFGH坐标,就会得到ABC、BCD、CDE以及FGH四个三角形

GL_TRIANGLE_FAN

扇面绘制是以第一个为零点进行绘制,通常我们绘制圆形,圆锥的锥面都会使用到,值得注意的是,最后一个点的左边应当与第二个点重合,在计算的时候,起点角度为0度,终点角度应包含360度。

看到这里我们实际上绘制正方形也同样可以使用该种方式,这样就不需要传顶点索引坐标了

顶点法和索引法

上述提到的绘制,使用的是GLES20.glDrawArrays,也就是顶点法,是根据传入的定点顺序进行绘制的。还有一个方法进行绘制GLES20.glDrawElements,称之为索引法,是根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元进行绘制。

顶点法拥有的绘制方式,索引法也都有。相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以相对顶点法减少很多重复顶点占用的空间。所以复杂图形的情况下推荐使用索引法。

复杂图形中索引法优于顶点法?我们举个例子,如要绘制一个正方体,正方体6个面,每个面两个三角形,每个三角形三个顶点(3+3)*6=36个点,刨除重复的4*6=24也需要定义24个点。而索引法只需要定义好正方体的8个顶点,然后用定义好索引即可。

最后

本章我们实现了正方形和圆形的绘制,让我们对OpenGL ES绘制图像有了更深的了解。正如我们在Android OpenGLES2.0开发(三):绘制一个三角形中提到的,绘制三角形是一切的基础,之后的文章都是在该基础上做一个微调。相信你通过这几篇文章已经对OpenGL ES初窥门径,希望在未来的篇章中我们一起登堂入室。

git地址:github.com/xiaozhi003/…