您的位置:首页 > 移动开发 > Android开发

OpenGL.ES在Android上的简单实践:7-曲棍球(构建冰球木槌 上)

2018-02-08 11:42 651 查看

OpenGL.ES在Android上的简单实践:7-曲棍球(构建冰球木槌 上)

这一章的内容大体上,不涉及OpenGL.ES的内容,而且在实际商业开发中是一般不会使用到的。然而,我想说的是,作为一个专业的三维开发工程师,图形开发者,GPU研究人员(等等更多高逼格称呼的程序员)来说,掌握基础的图形构建是很有必要的。想想大千世界的物体不都是由这些简单几何体一步步构建起来的吗?可能又有老鸟会跟你说,已经有很多合适的三维库了,从最早的开源封装libgdx,再到比较高级的商业库框架Unity3D。这些库能帮助你提高生产率,但只有当你对OpenGL、三维渲染,以及底层是如何把这些东西拼在一起的有了基本的理解后才能体会到;否则这些库没有任何意义,你也许感觉像使用黑合魔法一样。举个例子,时下流行的java框架SSM使得开发效率提高N倍,但是如果都不知道java是如何工作的,就开始使用它们,是不是为时过早呢?废话不说,赶紧开始这章的内容吧。

1、简化几何物体

对于要构建一个木槌,或者一个冰球这样的事情。让我们首先在高层次上想象一下它们的形状。如下图示:


一个冰球可以被表示为一个圆柱体;木槌就有点复杂了,它被表示为两个圆柱体,其中一个在另外一个的上面。
为了弄清楚如何在OpenGL中构建这些物体。让我们想象一下怎样用纸来构建它们:首先,我们要剪出一个圆形作为圆柱的顶;然后裁剪一张恰当大小的矩形纸,并卷成一个管子;最后就是把圆放在管子上,这样就构成一个圆柱了。
那么在OpenGL世界里怎样实现以上的操作?要构建圆,我们可以使用三角形扇。我们在文章3就是用三角扇来构建长方形桌子,我们也能用一个三角扇来表示一个圆,我们只需要使用大量的三角形并按照圆形排列外面的顶点即可。那么圆柱侧面,我们可以用一个相关的概念,它被称为三角带(Triangle Strip)。与三角扇一样,三角形带让我们定义多个三角形而不用一遍又一遍地重复那些三角形中共有的点;但是它不是绕着一个圆扇形展开的,三角形带像大桥横梁一样排列,三角形彼此相邻放置,如下图所示:


正如三角形扇一样,三角形带的前三个顶点定义了第一个三角形。这之后的每个额外的顶点都定义另外一个三角形,为了使用三角形带定义这个圆柱体的侧面,我们只需要把这个带卷成一个管子,并确保最后两个顶点与最前面的两个顶点一致。

2、添加几何图形的类

要构建冰球和木槌,我们现在很清楚需要什么了:对于冰球,我们需要一个三角扇做顶和一个三角带做侧面;对于木槌,我们需要两个三角扇和两个三角带。为了更容易地构建这些物体,我们将定义一个几何图形的类,它可以容纳一些基本的图形定义,一个用来做实际构建工作的对象构建器(ObjectBuilder)。让我们以这个几何物体的类作为开始。在项目的utils目录中添加Geometry集合辅助类。添加如下代码:
public class Geometry {

// 几何点
public static class Point {
public final float x,y,z;
public Point(float x,float y,float z){
this.x = x;
this.y = y;
this.z = z;
}
public Point translateX(float value) {
return new Point(x+value,y,z);
}
public Point translateY(float value) {
return new Point(x,y+value,z);
}
public Point translateZ(float value) {
return new Point(x,y,z+value);
}
}

... ...
}
我们已经添加Geometry.Point类用来表示三维场景中的一个点,其中一组辅助函数用于把这个点沿着不同方向的轴平移。我们还需定义一个圆Geometry.Circle,添加如下代码:
public class Geometry {

... ...
// 圆形 = 中心点+半径
public static class Circle{
public final Point center;
public final float radius;

public Circle(Point center, float radius){
this.center = center;
this.radius = radius;
}

public Circle scale(float scale){
return new Circle(center, radius * scale);
}
}
... ...
}
同样一组辅助函数,用于缩放圆的半径。最后是给圆柱体一个定义Geometry.Cylinder,添加如下代码:
public class Geometry {
... ...
// 圆柱
public static class Cylinder{
public final Point center;
public final float radius;
public final float height;

public Cylinder(Point center, float radius, float height){
this.center = center;
this.radius = radius;
this.height = height;
}
}
... ...
}
圆柱体就像一个扩展的圆,中心点,一个半径和一个高度。

3、添加物体构建器

以上我们只是简单的定义几何图形的基本属性,那么接下来我们就要开始构建几何顶点了。现在我们先定义这个物体构建器应该怎样工作,列出一些基本的需求:        1、使用者可以决定这个物体应该有多少个数据点。点数越多,物体模型构建得越平滑精细。        2、物体数据顶点需要绑定到OpenGL,并且有一组绘制物体的接口。        3、物体将以使用者指定的位置为中心,并平放在x-z平面上,物体顶部指向y轴正方向。
我们同样在utils的目录下创建构建器工具类ObjectBuilder,添加如下代码:
public class ObjectBuilder {
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];
}
... ...
}
目前为止,没有什么太复杂的,我们定义了一个常量用来表示一个顶点需要多少浮点数,以及一个变量一个偏移变量offset记录数组中顶点的位置。并且我们基于传入的数组大小变量sizeInVertices,量化了这个顶点数组vertexData。我们继续添加代码:
public class ObjectBuilder {
... ...

// 构建一个圆形所需的顶点数
private static int sizeOfCircleInVertices(int numPoints){
return 1 + (numPoints + 1);
}
// 构建一个圆柱侧面所需顶点数
private static int sizeOfCylinderInVertices(int numPoints){
return (numPoints + 1) * 2;
}
}
两个计算顶点数量的方法:圆柱体顶部是一个三角扇形构造的圆,有一个圆心,围着圆的每个点就是输入的变量,并且围着圆的第一个顶点要和最后一个顶点重复才能闭合成圆;圆柱体侧面是一个卷起来的长方形,由一个三角带构成,围着顶部圆的每个点都对应着下部圆,所以要乘以2,并且前两个顶点要在最后重复一次,才能使这个长方形卷起来的管闭合;

下一步,我们开始用一个三角扇构造这个冰球顶部的圆形,并把顶点数据写进vertexData里面,offset记录偏移:
private void createCircle(Geometry.Circle circle, int numPoints){
// 圆心点
vertexData[offset++] = circle.center.x;
vertexData[offset++] = circle.center.y;
vertexData[offset++] = circle.center.z;

for(int i = 0; i<= numPoints; i++) {
float angleInRadians =
((float)i / (float)numPoints)
* ((float)Math.PI * 2f);

vertexData[offset++] = circle.center.x + circle.radius * (float)Math.cos(angleInRadians);
vertexData[offset++] = circle.center.y;
vertexData[offset++] = circle.center.z + circle.radius * (float)Math.sin(angleInRadians);
}
}
要构建一个三角扇形,我们首先在circle.center定义圆心顶点,接着,我们围绕圆心的点按扇形展开,并把第一个点绕圆周重复两次考虑在内。接下来使用三角函数和单位圆的概念生成圆周的点数据。为了生成沿着圆周的点,我们首先要一个循环,范围涵盖从0到360度。整个圆,或者0到2π弧度。然后根据上图的数学关系,确定在x-z平面的圆周上的一个点的水平x位置,我们调用cos(angle);竖直z位置,我们调用sin(angle),然后用圆半径进行缩放。


圆柱的顶部圆形顶点数据生成了,接下来就是圆柱体的侧面,我们是使用一个三角带构造这个冰球的侧面。添加如下代码:
private void createCylinder(Cylinder cylinder, int numPoints) {

final float yStart = cylinder.center.y - (cylinder.height / 2);
final float yEnd = cylinder.center.y + (cylinder.height / 2);

for( int i = 0; i <= numPoints; i++) {
float angleInRadians =
((float)i / (float)numPoints)
* ((float)Math.PI * 2f);

float xPosition = cylinder.center.x + cylinder.radius * (float)Math.cos(angleInRadians);
float zPosition = cylinder.center.z + cylinder.radius * (float)Math.sin(angleInRadians);

vertexData[offset++] = xPosition;
vertexData[offset++] = yStart;
vertexData[offset++] = zPosition;

vertexData[offset++] = xPosition;
vertexData[offset++] = yEnd;
vertexData[offset++] = zPosition;
}
}
这次我们使用了同前面生成圆周顶点一样的算法,只是这次我们为圆周上的每个顶点生成了两组,一组是圆柱体顶部,一组是圆柱体底部。圆柱cylinder的center所在的位置 等于 顶部和底部中间的位置(如下图)所以圆柱侧面顶部的y = center.y + height/2,底部的y = center.y - height /2 。请大家理解其中的关系。

现在,我们把圆柱的顶部圆以及侧面的顶点数据全部都量化好了,现在我们来创建一个静态方法来生成一个圆柱形的冰球了。添加如下代码:
static void createPuck(Cylinder puck, int numPoints) {

int size = sizeOfCircleInVertices(numPoints) +
sizeOfCylinderInVertices(numPoints);

ObjectBuilder builder = new ObjectBuilder(size);

Circle puckTop = new Circle(
puck.center.translateY(puck.height / 2),
puck.radius );

builder.createCircle(puckTop, numPoints);
builder.createCylinder(puck, numPoints);
}
到此,ObjectBuilder现在有了createCircle,createCylinder两个接口,并以这两个接口为基础创建了冰球(createPuck),已经按照基本需求,完成了第一三点。那么第二点绑定数据绘制指令要怎么办呢?
绘制指令顾名思义就是告诉OpenGL如何绘制这个冰球,因为这个冰球是由两个基本图元构造的,一个是由三角扇构造的顶部圆,一个由三角带构造的侧面,并且顶点一并放在一个数组vertexData里面,所以我们需要一种方法,它可以把这些绘画命令合并在一起,以便使用者调用puck.draw()即可。我们可以把每个绘制指令都加入到一个绘制列表(list)中。
首先我们需要创建一个用于表示绘制指令的接口。在ObjectBuilder顶部/底部加入如下代码:public class ObjectBuilder {
interface DrawCommand{
void draw();
}
... ...
private final List<DrawCommand> drawList = new ArrayList<DrawCommand>(); 
}
// 接口定义你也可以增加static,至于差异区别在哪,我就是不告诉你,你可以百度一下,增强一下基础知识。(`・ω・´)现在 我们可以为三角扇添加绘制指令了,在createCircle修正如下代码:
private void createCircle(Geometry.Circle circle, int numPoints){
final int startVertex = offset / FLOATS_PER_VERTEX;
final int numVertices = sizeOfCircleInVertices(numPoints);
// 圆心点
vertexData[offset++] = circle.center.x;
vertexData[offset++] = circle.center.y;
vertexData[offset++] = circle.center.z;

for(int i = 0; i<= numPoints; i++) {
float angleInRadians =
((float)i / (float)numPoints)
* ((float)Math.PI * 2f);

vertexData[offset++] = circle.center.x + circle.radius * (float)Math.cos(angleInRadians);
vertexData[offset++] = circle.center.y;
vertexData[offset++] = circle.center.z + circle.radius * (float)Math.sin(angleInRadians);
}
drawList.add(new DrawCommand() {
@Override
public void draw() {
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, startVertex, numVertices);
}
});
}因为我们只使用了一个数组保存所有物体的数据,我们需要告诉OpenGL每个绘画指令对应的正确的顶点偏移值,计算偏移值和长度,并把它们存储在startVertex和numVertices,然后通过调用glDrawArrays画出三角扇,最终展现出一个圆形。
好了,我们用同样的方式来修改createCylinder,告诉OpenGL如何正确的画出三角带。
private void createCylinder(Geometry.Cylinder cylinder, int numPoints) {
final int startVertex = offset / FLOATS_PER_VERTEX;
final int numVertices = sizeOfCylinderInVertices(numPoints);

final float yStart = cylinder.center.y - (cylinder.height / 2);
final float yEnd = cylinder.center.y + (cylinder.height / 2);
for( int i = 0; i <= numPoints; i++) {
float angleInRadians =
((float)i / (float)numPoints)
* ((float)Math.PI * 2f);

float xPosition = cylinder.center.x + cylinder.radius * (float)Math.cos(angleInRadians);
float zPosition = cylinder.center.z + cylinder.radius * (float)Math.sin(angleInRadians);

vertexData[offset++] = xPosition;
vertexData[offset++] = yStart;
vertexData[offset++] = zPosition;

vertexData[offset++] = xPosition;
vertexData[offset++] = yEnd;
vertexData[offset++] = zPosition;
}
drawList.add(new DrawCommand() {
@Override
public void draw() {
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, startVertex, numVertices);
}
});
}OK!我们现在就差最后一步,就是在createPuck返回数据和绘制指令给调用者使用了。我们得定义一个holder类,以便我们可以用单个对象返回顶点数据和绘制指令列表。我们还是在ObjectBuilder的顶部/底部添加这个holder类,代码如下:
static class GeneratedData{
final List<DrawCommand> drawCommandlist;
final float[] vertexData;
GeneratedData(float[] data, List<DrawCommand> drawList){
this.drawCommandlist = drawList;
this.vertexData = data;
}
}最终我们修改createPuck函数,代码如下:
static GeneratedData createPuck(Geometry.Cylinder puck, int numPoints) {
int size = sizeOfCircleInVertices(numPoints) +
sizeOfCylinderInVertices(numPoints);

ObjectBuilder builder = new ObjectBuilder(size);

Geometry.Circle puckTop = new Geometry.Circle(
puck.center.translateY(puck.height / 2),
puck.radius );

builder.createCircle(puckTop, numPoints);
builder.createCylinder(puck, numPoints);

return builder.build();
}

private GeneratedData build() {
        return new GeneratedData(vertexData, drawList);
    }


这次文章代码较多,需要分开篇幅介绍。让我们花费点时间复习一下现有的内容知识:
1、首先,我们从ObjectBuilder的外部调用静态方法createPuck,这个方法创建一个新的ObjectBuilder对象,它用合适大小的数组保存这个冰球的所有数据,它也创建了一个绘制列表,以便后续绘制这个冰球。
2、在createPuck内部,我们调用createCircle、createCylinder分别生成冰球的顶部和侧面。每个方法都给vertexData添加了数据,并给drawList添加了绘制指令。
3、最终,我们调用build生成包装类返回给调用者进行操作。
我们现在已经学习到了怎样画出一个简易冰球(矮扁的圆柱体),那么能不能在这基础上画出简易的木槌呢?(矮扁的圆柱底+苗条的圆柱)     模型出来了,我们又怎样的画到OpenGL的世界坐标上呢?我们下篇文章再见。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: