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

OpenGL.ES在Android上的简单实践:8-曲棍球(构建冰球木槌 下 & 模型视图投影矩阵)

2018-02-24 15:30 696 查看

OpenGL.ES在Android上的简单实践:8-曲棍球(构建圆柱体木槌)

1、创建木槌顶点数据

本篇文章继续第7篇文章之后的内容,我们通过ObjectBuilder.cratePuck创建冰球了(偏平圆柱体),这次我们在之前的基础上创建简易木槌(矮扁的圆柱底部+苗条的圆柱摇杆)如下图



手柄的高度占整体高度的75%,而基部的高度占整体高度25%。我们也可以看出手柄的宽度占整体宽度的三分之一。有了这些定义,我们就能计算在哪里放置组成木槌的这两个圆柱体了。
在createPuck之后加入名为“createMallet”的方法,我们以下面的代码开始:
static GeneratedData createMallet(Geometry.Point center, float radius, float height, int numPoints) {
int size = sizeOfCircleInVertices(numPoints) * 2
+ sizeOfCylinderInVertices(numPoints) * 2;
ObjectBuilder builder = new ObjectBuilder(size);

// 底座部分
float baseHeight = height * 0.25f;
Geometry.Circle baseCircle = new Geometry.Circle(
center.translateY(-baseHeight),
radius
);
Geometry.Cylinder baseCylinder = new Geometry.Cylinder(
baseCircle.center.translateY(-baseHeight / 2f),
radius,
baseHeight
);
builder.createCircle(baseCircle, numPoints);
builder.createCylinder(baseCylinder, numPoints);
// 上半部分
float handleHeight = height * 0.75f;
float handleRadius = radius / 3f;
Geometry.Circle handleCircle = new Geometry.Circle(
center.translateY(height * 0.5f),
handleRadius
);
Geometry.Cylinder handleCylinder = new Geometry.Cylinder(
handleCircle.center.translateY(-handleHeight / 2f),
handleRadius,
handleHeight
);
builder.createCircle(handleCircle, numPoints);
builder.createCylinder(handleCylinder, numPoints);

return builder.build();
}我们遵循上篇的思路一样的步骤,但是使用了不同大小。我们由函数传入的物体主中心点,来推断上部分和下部分圆柱体的圆形中心,再顺藤摸瓜推断圆柱侧面的位置。
这就是我们的ObjectBuilder类的全部了!我们现在能生成冰球和木槌了;当我们要绘制他们时,只需要把顶点数据绑定到OpenGL,并调用object.draw()就可以了。
既然我们有了一个物体构建器,就不用再把木槌画成点了,我们更新Mallet类。
public class Mallet {
private static final int POSITION_COMPONENT_COUNT = 3;

private final VertexArray vertexArray;
private List<ObjectBuilder.DrawCommand> drawList;

private final float raduis;
private final float height;

public Mallet(float radius, float height, int numPointsAroundMallet){
ObjectBuilder.GeneratedData mallet = ObjectBuilder.createMallet(
new Geometry.Point(0f, 0f, 0f),
radius, height, numPointsAroundMallet);
this.raduis = radius;
this.height = height;

vertexArray = new VertexArray(mallet.vertexData);
drawList = mallet.drawCommandlist;
}

public void bindData(ColorShaderProgram shaderProgram){
vertexArray.setVertexAttributePointer(
shaderProgram.aPositionLocation,
POSITION_COMPONENT_COUNT, 0, 0
);
}

public void draw(){
for (ObjectBuilder.DrawCommand command : drawList) {
command.draw();
}
}
}参见以上的代码,改动不少,但都有迹可循。bindData遵循的模式与Table一样:它把顶点数据绑定到着色器程序定义的属性上。第二个onDraw方法只是遍历ObjectBuilder.createMallet创建的绘制列表,去掉了之前给着色器的颜色分量赋值的部分代码
按照上面的模式,我们在Table、Mallet同目录创建冰球Puck,并添加如下代码:
public class Puck {
private static final int POSITION_COMPONENT_COUNT = 3;

public final float radius, height;

private final VertexArray vertexArray;
private final List<ObjectBuilder.DrawCommand> drawList;

public Puck(float radius, float height, int numPointsAroundPuck) {
ObjectBuilder.GeneratedData puck = ObjectBuilder.createPuck(
new Geometry.Cylinder(new Geometry.Point(0f, 0f, 0f), radius, height),
numPointsAroundPuck
);

vertexArray = new VertexArray(puck.vertexData);
drawList = puck.drawCommandlist;

this.radius = radius;
this.height = height;
}

public void bindData(ColorShaderProgram shaderProgram) {
vertexArray.setVertexAttributePointer(
shaderProgram.aPositionLocation,
POSITION_COMPONENT_COUNT,
0, 0
);
}

public void draw() {
for(ObjectBuilder.DrawCommand command : drawList) {
command.draw();
}
}
}这些代码遵循着一样的模式,统一规范好管理。

2、更新着色器

下一步我们就要去更新着色器,因为我们的木槌已经出来模型顶点了,我们不再使用顶点的位置代表木槌,因此我们不得不把颜色作为一个uniform传递进去。我们更新ColorShaderProgram如下代码: protected static final String U_COLOR = "u_Color";
public final int uColorLocation;

public ColorShaderProgram(Context context) {
        uColorLocation = GLES20.glGetUniformLocation(programId, U_COLOR);
    }

    public void setUniforms(float[] matrix, float r, float g, float b) {
        GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
        GLES20.glUniform4f(uColorLocation, r, g, b, 1f);
    }我们还重载了setUniforms方法,把颜色值参数一同的传递到片段着色器里面去,我们继续更新ColorShaderProgram对应的顶点着色器simple_vertex_shader.glsl 和 片段着色器simple_fragment_shader.glsl // 更新前的 simple_vertex_shader.glsl
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;

void main()
{
    v_Color = a_Color;
    gl_Position = u_Matrix * a_Position;
    gl_PointSize = 20.0;
}

// 更新后的 simple_vertex_shader.glsl
uniform mat4 u_Matrix;
attribute vec4 a_Position;

void main()
{
gl_Position = u_Matrix * a_Position;
}
// 更新前的 simple_fragment_shader.glsl
precision mediump float;
varying vec4 v_Color;

void main()
{
    gl_FragColor = v_Color;
}

// 更新后的 simple_fragment_shader.glsl
precision mediump float;
uniform vec4 u_Color;

void main()
{
gl_FragColor = u_Color;
}
我们前后对比一下着色器代码
        1、顶点着色器更新前后的对比,去掉了color的属性,改用到片段着色器里面赋值。从意义上来说就是:颜色值不再是顶点数据的一部分,不再根据顶点去定义。
        2、片段着色器更新前后的对比,color的属性不再是从顶点着色器传递过来,而是直接在着色器程序外部赋值。颜色值和顶点数据分离成两个独立的变量。

3、把模型呈现到世界坐标上

本次文章最复杂的内容已经完成了。我们学习了简单的几何形状构造冰球和木槌,并更新了着色器用以反映这些变化。所有剩下的就是把这些变化集成到HockeyRenderer2上,我们更新代码如下:public class HockeyRenderer2 implements GLSurfaceView.Renderer {

private Puck puck;

@Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
... ...
        table = new Table();
        mallet = new Mallet(0.08f, 0.15f, 32);
        puck = new Puck(0.06f, 0.02f, 32);
        ... ...
    }

@Override
    public void onDrawFrame(GL10 gl10) {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        textureShaderProgram.userProgram();
        textureShaderProgram.setUniforms(uMatrix, textureId);
        table.bindData(textureShaderProgram);
        table.draw();

        colorShaderProgram.userProgram();
        colorShaderProgram.setUniforms(uMatrix, 1f, 0f, 0f);
        mallet.bindData(colorShaderProgram);
        mallet.draw();

        colorShaderProgram.userProgram();
        colorShaderProgram.setUniforms(uMatrix, 0f, 1f, 0f);
        puck.bindData(colorShaderProgram);
        puck.draw();
    }
}我们新增定义冰球Puck,并在onSurfaceCreated回调接口创建Table Puck Mallet三个模型的顶点数据。(其实这个三个模型的代码都没用OpenGL相关接口,我想说什么大家自己想想)随后我们在onDrawFrame接口把三个模型绘制正确的绘制出来。其实mallet和puck都用了同一组shaderProgram,在绘制puck的时候没必要再一次userProgram(),我们只是更改了不同的颜色值。以上代码就绪,我们看看运行效果。



我们现在冰球和木槌(其实还有桌子)其实都是安放在一个坐标系上(世界坐标系)的中心原点处,我们可以通过之前的modelMatrix来修改位置,不过这里我想引入一些新的概念。

4、认识模型视图投影矩阵,以及正确的层次结构

想想我们整个项目,我们先是添加一个正交投影矩阵调整宽高比。之后添加了一个矩阵modelMatrix用于操作整个场景,用于调整桌子的角度,以达到一个三维效果。其实这里的modelMatrix起到作用就是视图矩阵。视图矩阵实际只是模型矩阵的扩展,只是视图矩阵作用于整个场景(包括场景在内的模型)模型矩阵只作用于某个指定的物体。
我们可以这样理解,模型矩阵(model)针对的是Table/Mallet/Puck等一系列物体对象,一般情况下模型矩阵都是物体对象的私有变量,每个实体对象各自维护自己的模型矩阵,相当于我们玩吃鸡/WOW/Dota的时候,你用鼠标键盘操纵的那个角色。而视图矩阵(view)是针对整个场景,简单来说就是OpenGL的世界坐标,这个视图矩阵就是新引入的摄像头概念,相当于我们玩吃鸡/WOW/Dota的时候,你用鼠标拖动的视野镜头。废了那么多口舌希望大家能明白,我们花点时间复习总结一下,把一个物体放到屏幕上的三个主要的矩阵类型:
· 模型矩阵(Model)
模型矩阵是用来把物体放在世界空间坐标系的。比如,我们可能有冰球模型和木槌模型,他们初始化的中心点都在(0,0,0)。没有模型矩阵,这些模型就会卡在那里:如果我们想要移动它们,就不得不自己更新每个模型的每个顶点。如果不想这样做,我们可以使用一个模型矩阵,把那些顶点与这个矩阵相乘来变换它们。
· 视图矩阵(View)

视图矩阵是出于同模型矩阵一样的原因被使用的,但是它平等地影响场景中的每个物体,因为它影响所有的东西,它在功能上等同于一个你手持着的相机,来回移动相机,你会从不同的视角看见不同的东西。
· 投影矩阵(Projection)

这个矩阵帮助创建三维的幻象,通常只有当屏幕变换方位时,它才会变化。

好了,扯了那么久也就说了一半,为什么说只有一半?MVP三个矩阵全出来了,那究竟怎么用?按照之前的套路,无非就是把这几个矩阵相乘起来嘛,那么问题来了。谁先乘,谁乘谁? 怎么理解?

说方法之前,先问大家,这三个矩阵概念出现的先后顺序是怎样的?投影,视图,模型,是这样对吧?
· 我们用投影矩阵解决横竖屏切换的时候变形的问题,其中原理是OpenGL的透视除法,此时的顶点我们标记为vertex(clip),之后交给OpenGL归一化处理和视口变换得出窗口坐标vertex(final);
· 视图矩阵用来以正确的角度观察桌子,以呈现3D效果,此时的顶点我们标记为vertex(eye);
· 这次我们再引入模型矩阵,用以针对各个物体对象的操作,此时的顶点我们标记为vertex(model);
· 模型呈现在屏幕上的还需要加载到OpenGL的世界坐标上,这样的转换不需要什么矩阵,但却是一个不可缺少的过程,此时的顶点我们标记为vertex(world);
以上四点究竟连接成一个完整的过程:
vertex(clip)=ProjectionMatrix * vertex(eye);【需要投影变换的是 从摄像机角度观察的物体】
vertex(clip)=ProjectionMatrix * ViewMatrix * vertex(world);【需要投影变换的是 从摄像机角度观察的 OpenGL世界中的物体】

vertex(clip)=ProjectionMatrix * ViewMatrix * ModelMatrix * vertex(model)【需要投影变换的是 从摄像机角度观察的 OpenGL世界中的 经过(玩家)操作过后的 物体】

最后 vertex(final)=OpenGL自处理 * vertex(clip);

这样描述之后 明白了吗?其中更为专业的数学原理变换请参考这里

5、集成所有的变化

让我们继续给HockeyRenderer2添加一个视图矩阵(摄像机),同时,我们还需要为桌子、木槌和冰球增加自己的模型矩阵用于操作变换。public class HockeyRenderer2 implements GLSurfaceView.Renderer {

    private final float[] projectionMatrix = new float[16];
    private final float[] viewMatrix = new float[16];
// private final float[] modelMatrix = new float[16];
public HockeyRenderer2(Context context) {
        this.context = context;
        Matrix.setIdentityM(modelMatrix,0);
        Matrix.setIdentityM(viewMatrix,0);
    }
}我们在顶部新增一个viewMatrix视图矩阵,并在构造函数初始化为单位矩阵,删除modelMatrix定义及其相关代码。 @Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0,width,height);

MatrixHelper.perspectiveM(projectionMatrix, 45, (float)width/(float)height, 1f, 100f);
Matrix.setLookAtM(viewMatrix, 0,
0f,1.2f, 2.2f,// eye
0f,0f,0f, // center
0f,1f,0f); // up
}更新onSurfaceChanged代码,我们先设置视口,并建立投影矩阵,接着就是新内容:调用android.opengl.Matrix库当中的setLookAtM创建一个特殊类型的视图矩阵。参数解释如下:
· float rm:这是目标数组,我们填入viewMatrix。这个矩阵的长度应该至少容纳16个元素,以便它能存储视图矩阵。
· int mOffset:我们填0,setLookAtM会把结果从目标数组偏移mOffset个单位之后才开始存储结果数据。
· float eyeX,eyeY,eyeZ:这是摄像机所在的位置。场景中的所有东西看起来都像是从这个点观察他们一样。
· float centerX,centerY,centerZ:这是摄像机所要看的地方;这个位置出现在整个场景的中心。
· float upX,upY,upZ:之前两组坐标还不能正确的描述你所想看到的画面,那么这组坐标就是你的摄像机头顶指向的方向向量。

我们调用setLookAtM时,把摄像机(eye)设为(0, 1.2, 2.2),这意味着摄像机的位置在x-z平面上方1.2个单位,并向后2.2个单位。换句话说,场景中的所有东西都出现在你下面1.2个单位 前面2.2个单位的地方。把中心(center)设为(0, 0, 0),意味着你将向下看你前面的原点,并把指向(up)设为(1, 0, 0),意味着摄像机的头顶方向笔直向上。如果设为(-1, 0, 0),就像倒立着,头顶指向下。 这样我们就设置好了视图矩阵。

接下来我们更新物体对象Table,Mallet和Puck。在对象内部各自建立模型矩阵,并在构造函数内部初始化为单位矩阵:class org.zzrblog.blogapp.objects.Table / Mallet / Puck

public class Table {

public float[] modelMatrix = new float[16];

public Table(){
        vertexArray = new VertexArray(VERTEX_DATA);
        Matrix.setIdentityM(modelMatrix,0);
    }
... ...
}好了,现在三个矩阵都准备就绪了,我们看看上面的分析:

vertex(clip)=ProjectionMatrix * ViewMatrix * ModelMatrix * vertex(model)其中前两项是单一实例的,后面是两项是根据不同物体使用不同的变量。我们先处理前两项矩阵相乘,我们回到HockeyRenderer2,新增定义viewProjectionMatrix,并构造函数初始化为单位矩阵。private final float[] viewProjectionMatrix = new float[16];
... ...
Matrix.setIdentityM(viewProjectionMatrix,0);接着在onSurfaceChanged回调最后,即初始化投影矩阵 和 视图矩阵之后,把两矩阵相乘,结果保存到viewProjectionMatrixMatrix.multiplyMM(viewProjectionMatrix,0, projectionMatrix,0, viewMatrix,0); // 矩阵相乘 注意左右顺序接下就是关键操作,各位乘客请注意,超速拐弯了。
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
GLES20.glViewport(0,0,width,height);

MatrixHelper.perspectiveM(projectionMatrix, 45, (float)width/(float)height, 1f, 100f);
Matrix.setLookAtM(viewMatrix, 0,
0f, 1.2f, 2.2f,
0f, 0f, 0f,
0f, 1f, 0f);

Matrix.multiplyMM(viewProjectionMatrix,0,  projectionMatrix,0, viewMatrix,0);

Matrix.rotateM(table.modelMatrix,0, -90f, 1f,0f,0f);
Matrix.translateM(mallet.modelMatrix,0, 0f, mallet.height, 0.5f);
Matrix.translateM(puck.modelMatrix,0, 0f, puck.height, 0f );
}

@Override
public void onDrawFrame(GL10 gl10) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, table.modelMatrix,0);
textureShaderProgram.userProgram();
textureShaderProgram.setUniforms(modelViewProjectionMatrix, textureId);
table.bindData(textureShaderProgram);
table.draw();

Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, mallet.modelMatrix,0);
colorShaderProgram.userProgram();
colorShaderProgram.setUniforms(modelViewProjectionMatrix, 1f, 0f, 0f);
mallet.bindData(colorShaderProgram);
mallet.draw();

Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, puck.modelMatrix,0);
colorShaderProgram.userProgram();
colorShaderProgram.setUniforms(modelViewProjectionMatrix, 0f, 1f, 0f);
puck.bindData(colorShaderProgram);
puck.draw();
}
我们认真分析这段模板代码:
1、onSurfaceChanged 初始化投影矩阵 和 视图矩阵之后,把两矩阵相乘,结果保存viewProjectionMatrix;然后我们操作桌子的模型矩阵,因为这个桌子原来是以 x 和 y 坐标定义的,因此要使它平放在x-z的平面上,因此我们绕x轴向后旋转90度。我们亦不需要把桌子平移一定的距离,让它保持在位置(0, 0, 0),并且视图矩阵已经想办法使桌子对我们可见了。
2、木槌和冰球已经被定义好,并被水平放在x-z平面上,因此我们不需要旋转它们。我们要根据传递进来的参数平移它们,将它们放在桌子上方正确的位置上。onDrawFrame与之前的相比,我们每次setUniforms的时候都更新最新的MVP三大矩阵,接着会被传递給着色器程序当中。
3、为什么我们要每次onDrawFrame都重新把 模型矩阵 与 投影视图矩阵 相乘?其实因为正常情况下,玩家都在时时刻刻和操作对象交互,模型矩阵基本时时刻刻都在改变。所以我们每帧回调都更新

运行程序,如果一切无什么大错误,那它看起来应该是如下图所示。结合文章7的内容,基本认识了物体的顶点构造和使用。我们还认识了三大矩阵以及它们三者的层次结构。



作为练习,大家可以尝试在第一个木槌的对面再增加多一个木槌。ヾ(◍°∇°◍)ノ゙
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息