Part I 空气曲棍球 Chapter8(8.3 Adding an Object Builder)

 8.3 构造形体(Adding an Object Builder) 

    现在开始创建我们的构建者类,在包com.airhockey.android.objects中创建类ObjectBuilder,并添加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
private static final int FLOATS_PER_VERTEX = 3;
private final float[] vertexData;
private int offset = 0;
private ObjectBuilder(int sizeInVertices) {
    vertexData = new float[sizeInVertices * FLOATS_PER_VERTEX];
}

    这段代码非常简单,定义了一个常量代表每一个顶点将会包含几个float类型数据,一个float类型的数组,一个变量记录当前数组中的读取位置;并在构造函数中根据顶点数目初始化了顶点数组。
    我们将会定义一些静态方法用以构建冰球及球棍,这些方法将会根据传递的参数调用ObjectBuilder的相关方法并把构建的数据返回给调用者。
    下面是构建者将会实现的目标:

  • 调用者可以传递不同的顶点数量,顶点越多则首先的形体越平滑。
  • 形体的顶点数据将会存储在一维float数组中,当对象构建成功后相关的顶点数据就会自动绑定到OpenGL并且调用者只需要调用一行代码就可以绘制相应形体。
  • 构建的形体将位于调用者指定的中心位置并且在x-z平面中;换句话说,形体的顶部将会垂直向上。

    现在增加如下计算圆柱所需顶点的方法:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
private static int sizeOfCircleInVertices(int numPoints) {
    return 1 + (numPoints + 1);
}

    圆柱的顶部将会使用triangle fan构建,这样的话在中心将会有一个点,圆圈将会由一个个的顶点构成,并且圆圈上的第个顶点与最后一个顶点相同,这样才能绘制一个完整的圆。
    下面是计算圆柱边缘所需顶点的方法:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
private static int sizeOfOpenCylinderInVertices(int numPoints) {
    return (numPoints + 1) * 2;
}

    圆柱边缘相当于是由一个矩形”卷“起来的,这里将会使用triangle strip进行构建,开始的两个顶点与最后的两个顶点相同。
8.3.1 构建冰球(Building a Puck with a Cylinder)
    现在我们可以创建一个静态方法来构建冰球了,增加方法createPuck()并添加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
static GeneratedData createPuck(Cylinder puck, int numPoints) {
    int size = sizeOfCircleInVertices(numPoints) + sizeOfOpenCylinderInVertices(numPoints);
    ObjectBuilder builder = new ObjectBuilder(size);
    Circle puckTop = new Circle(puck.center.translateY(puck.height / 2f), puck.radius);
    builder.appendCircle(puckTop, numPoints);
    builder.appendOpenCylinder(puck, numPoints);
    return builder.build();
}

    首先是计算一个冰球需要多少个顶点,并使用该顶点数据创建 ObjectBuilder对象;冰球由一个圆及圆柱边缘组成,因此顶点的总数就是 sizeOfCircleInVertices(numPoints) + sizeOfOpenCylinderInVertices(numPoints)。
    然后计算冰球顶部圆的位置并且调用appendCircle()创建相应对象,同时调用方法appendOpenCylinder()创建圆柱边缘部分,最后调用build()方法把产生的数据返回给调用者;由于这些方法都还没有创建,下面就来创建这些方法。
    为什么我们要把圆柱顶部圆移动puck.height / 2f大小的距离呢?看看下面的图:

Part I 空气曲棍球 Chapter8(8.3 Adding an Object Builder)


    冰球在垂直方向上的中心位于center.y处,因此把圆柱的边缘放此处刚刚好;然而由于需要把圆放在圆柱的顶部,因此上移了冰球高度的一半。
8.3.1.1 构建圆(Building a Circle with a Triangle Fan)
    下一步是使用triangle fan命令构建圆,我们将会把数据写入到vertexData中,并且将会使用offset记录写入位置,创建方法appendCircle(),并添加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
private void appendCircle(Circle circle, int numPoints) {
    // Center point of fan
    vertexData[offset++] = circle.center.x;
    vertexData[offset++] = circle.center.y;
    vertexData[offset++] = circle.center.z;
    // Fan around center point. <= is used because we want to generate
    // the point at the starting angle twice to complete the fan.
    for (int i = 0; i <= numPoints; i++) {
        float angleInRadians = ((float) i / (float) numPoints) * ((float) Math.PI * 2f);
        vertexData[offset++] = circle.center.x + circle.radius 
            * FloatMath.cos(angleInRadians);
        vertexData[offset++] = circle.center.y;
        vertexData[offset++] = circle.center.z + circle.radius 
            * FloatMath.sin(angleInRadians);
    }
}

    为了使用triangle fan命令我们首先在中心 circle.center处定义了一个顶点,然后围绕着中心顶点展开,需要注意的是第一个顶点需要重复两次;然后使用三角函数及如下图所示单位圆的概念来生存我们需要的顶点。

Part I 空气曲棍球 Chapter8(8.3 Adding an Object Builder)

    为了在一个圆的周围产生需要的顶点,我们从0到360度的范围内循环,并使用cos(angle)及sin(angle)两个三角函数分别计算相应的x轴及z轴坐标。
    另外由于我们的圆将会位于x-z平面上,所以单位圆的y坐标对应于y分量坐标值。
8.3.1.2 增加绘制命令(Adding a Draw Command for the Triangle Fan)
    我还需要告诉OpenGL如何绘制冰球的顶部,由于冰球是由两个图元组成的,triangle fan构建顶部、triangle strip构建边缘,所以我们需要一种组合这些绘制命令的方法使得我们只需要调用 puck.draw()就可以绘制相应的冰球;一种方法便是把这些绘制命令放到一个list中。
    在ObjectBuilder中添加如下代码代表绘制命令接口:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
static interface DrawCommand {
    void draw();
}

    还需要一个一实例变量保存相应的绘制命令,在vertextData后面添加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
private final List<DrawCommand> drawList = new ArrayList<DrawCommand>();

    我们现在可以增加triangle fan绘制命令了,修改appendCircle()方法并增加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
final int startVertex = offset / FLOATS_PER_VERTEX;
final int numVertices = sizeOfCircleInVertices(numPoints);

    由于我们只使用了一个数组,所以我们需要告诉OpenGL当前绘制命令的偏移量是多少,这里将相应的偏移量及顶点数量保存在startVertex及numVertices中,现在在 appendCircle()的最后添加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
drawList.add(new DrawCommand() {
    @Override
    public void draw() {
        glDrawArrays(GL_TRIANGLE_FAN, startVertex, numVertices);
    }
});

    这里我们创建一个调用glDrawArrays()方法的内部类,并且把相应的内部类对象添加到绘制命令list中去,稍后需要绘制冰球的时候我们只需运行list中的draw()函数就可以了。
8.3.1.3 构建圆柱边缘(Building a Cylinder Side with a Triangle Strip)
    下一步是使用triangle strip构建圆柱边缘,在appendCircle()的后面添加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
private void appendOpenCylinder(Cylinder cylinder, int numPoints) {
    final int startVertex = offset / FLOATS_PER_VERTEX;
    final int numVertices = sizeOfOpenCylinderInVertices(numPoints);
    final float yStart = cylinder.center.y - (cylinder.height / 2f);
    final float yEnd = cylinder.center.y + (cylinder.height / 2f);
}

    就像前面一样,这里指出了开始绘制的顶点及需要的顶点数量,同时也计算了y方向的开始及结束位置,如下图所示:

Part I 空气曲棍球 Chapter8(8.3 Adding an Object Builder)


    下面的代码将会产生triangle strip命令所需要的顶点坐标:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
for (int i = 0; i <= numPoints; i++) {
    float angleInRadians = ((float) i / (float) numPoints) * ((float) Math.PI * 2f);
    float xPosition = cylinder.center.x + cylinder.radius * FloatMath.cos(angleInRadians);
    float zPosition = cylinder.center.z + cylinder.radius * FloatMath.sin(angleInRadians);
    vertexData[offset++] = xPosition;
    vertexData[offset++] = yStart;
    vertexData[offset++] = zPosition;
    vertexData[offset++] = xPosition;
    vertexData[offset++] = yEnd;
    vertexData[offset++] = zPosition;
}

    这里使用前面的相同方法计算相应的顶点坐标,唯一不同的时候现在对于每个点需要产生两个顶点数据:一个对应于顶点而一个对应于底部;同时保证最后的两个顶点与开始的两个顶点相同以便绘制一个闭合的圆柱。
    最后添加如下代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
drawList.add(new DrawCommand() {
    @Override
    public void draw() {
        glDrawArrays(GL_TRIANGLE_STRIP, startVertex, numVertices);
    }
});

    这里使用参数GL_TRIANGLE_STRIP 告诉OpenGL使用triangle strip命令进行绘制。
8.3.2 返回数据(Returning the Generated Data)
    为了完成createPuck()方法,我们还需要定义build()方法, 我们将会使用这个方法创建GeneratedData对象以便 保存产生的数据;由于我们还没有定义该类,所以在ObjectBuilder的顶部DrawCommand的后面添加如下定义:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
static class GeneratedData {
    final float[] vertexData;
    final List<DrawCommand> drawList;
    GeneratedData(float[] vertexData, List<DrawCommand> drawList) {
        this.vertexData = vertexData;
        this.drawList = drawList;
    }
}

    该类仅仅是持有顶点数据及相应的绘制命令list,现在我们需要添加如下定义build()方法的代码:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
private GeneratedData build() {
    return new GeneratedData(vertexData, drawList);
}

    现在我们已经完成了createPuck()方法,现在我们来回忆下相应的流程:

  1. 首先调用静态方法createPuck(),该方法将会使用一个能够容纳冰球所需顶点数量的数组创建一个ObjectBuilder,同时还创建一个绘制命令list使得我们之后可以调用该list绘制冰球。
  2. 在createPuck()方法内部,我们调用appendCircle()及appendOpenCylinder() 方法分别创建冰球的顶部及边缘部分,每一个方法都会把相应数据放入vertexData中并在drawList中添加相应绘制命令。
  3. 最后我们调用build()方法返回产生的数据。

8.3.3 创建球棍(Building a Mallet with Two Cylinders)
    我们现在可以使用刚刚学习的知识创建球棍了,一个球棍可以使用两个圆柱进行构建,因此构建球棍就像是构建两个不同大小的圆柱一样;我们将会使用一种特别的方法构建球棍,如下图所示:

Part I 空气曲棍球 Chapter8(8.3 Adding an Object Builder)

    球棍的手柄高度将会是整个球棍75%的高度,底部将会是25%的高度;同时手柄是整个宽度的1/3;使用这样的定义我们将能够计算如何组合两个圆柱以便产生我们需要的球棍。
    当我们把这些定义都定下来的时候,可能画出相应的形体将会有助于理清楚的描述我们需要做的事;为了创建球棍,我们需要计算每一个圆柱顶部的y坐标及中心坐标。
    在createPuck()方法的后面添加方法createMallet(),并添加如下定义:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
static GeneratedData createMallet( Point center, float radius, float height, int numPoints) {
    int size = sizeOfCircleInVertices(numPoints) * 2 + sizeOfOpenCylinderInVertices(numPoints) * 2;
    ObjectBuilder builder = new ObjectBuilder(size);
    // First, generate the mallet base.
    float baseHeight = height * 0.25f;
    Circle baseCircle = new Circle( center.translateY(-baseHeight), radius);
    Cylinder baseCylinder = new Cylinder(baseCircle.center.translateY(-baseHeight / 2f), radius, baseHeight);
    builder.appendCircle(baseCircle, numPoints);
    builder.appendOpenCylinder(baseCylinder, numPoints);
    //...
}

    我们使用合适的大小创建了一个ObjectBuilder对象,并创建了球棍的底部,这部分的代码与cratePuck()非常相似。
    同时添加如下代码以创建球棍的手柄:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
float handleHeight = height * 0.75f;
float handleRadius = radius / 3f;
Circle handleCircle = new Circle(center.translateY(height * 0.5f), handleRadius);
Cylinder handleCylinder = new Cylinder(
    handleCircle.center.translateY(-handleHeight / 2f), handleRadius, handleHeight);
builder.appendCircle(handleCircle, numPoints);
builder.appendOpenCylinder(handleCylinder, numPoints);

    这部分代码使用不同的坐标及大小并按照前面的模式创建了球棍手柄,最后添加如下方法以完成createMallet()方法的定义:

//AirHockeyWithImprovedMallets/src/com/airhockey/android/objects/ObjectBuilder.java
return builder.build();

    这就是ObjectBuilder的定义了,我们现在可以创建冰球及球棍了,当我们需要绘制他们的时候,我们所需要做的就是绑定数据到OpenGL然后调用object.draw()方法即可。
    下一节我们将会更新定义形体的相应代码(点击进入下一节)。