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

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回调最后,即初始化投影矩阵 和 视图矩阵之后,把两矩阵相乘,结果保存到viewProjectionMatrix

Matrix.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();
    }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的内容,基本认识了物体的顶点构造和使用。我们还认识了三大矩阵以及它们三者的层次结构。

作为练习,大家可以尝试在第一个木槌的对面再增加多一个木槌。ヾ(◍°∇°◍)ノ゙

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值