OpenGL ES MVP/变换投影矩阵(8)

OpenGL ES MVP/变换投影矩阵(8)

简述

通过前面几节的学习,目前我们已经可以渲染自己想要的图像,也可以通过纹理加载图片进行渲染。接下来我们来学习一下MVP,这里的MVP不是Android应用开发里的框架MVP,而是Model,View,Projection。

首先我们先来解释一下这个是做什么的,这三者都是用来计算最终图像呈现的,举个例子,我们如果要开发一个游戏,游戏里的一个物体放在那里,物体放置的角度不同,我们看到的效果可能不一样,我们从不同的位置角度看物体,看到的也可能不一样,MVP就是在计算物体在不同的放置情况,不同的观察角度最终的成像,接下来我们一个一个介绍。

变换矩阵

在开始介绍MVP三个概念之前,我们先来介绍一下变换矩阵。

变换矩阵是MVP的基础原理,我们这里只介绍一下概念,不会深入它的原理,因为它的原理是一些数学运算,需要一些线性代数的基础。

我们对图像的变换计算,都是通过乘以一个矩阵来实现的,我们只需要知道我们将坐标乘以一个矩阵,将它映射到另一个坐标,这样就可以完成图形的变换,比如平移,缩放,翻转等等,所以我们各种变换都是通过乘以一个变换矩阵实现的,每一个模型都有一个对应的矩阵。

MVP

坐标变换:

最终坐标 = Mat(projection) * Mat(View) * Mat(model) * 原坐标

投影(Projection)

我们的图像可能是3D的,但是最终需要显示在我们的屏幕上,我们的屏幕是2D的,只能显示一个平面,所以我们需要通过计算将3D图像最终显示在2D到图像上,这便是一种投影。

投影矩阵一般有两种,一种是透视投影矩阵,另一种是正交投影矩阵。

透视投影矩阵是可以越远越小,类似现在的3D游戏,而正交投影有点类似2.5D游戏,大小不会受到远近的影响。

视口(View)

视口所处理的问题就是相机位置,比如相机向左移动,其实看到的图像就向右移动了,View的变换矩阵就是用于描述这个逻辑。

模型(Model)

这个变换矩阵描述的是物体自己的变化,比如物体自己放大,翻转,平移等等。

渲染正方体

我们以渲染一个旋转的正方体为例来介绍这几个模型。

顶点数据

我们需要渲染一个正方体,有8个顶点,正方体由6个面组成,每个面是2个三角形,就是12个三角形,我们索引缓冲区就是12个三角形,36个点。

private float[] vertexArray = new float[] {
        -0.5f, -0.5f, -0.5f,
        0.5f, -0.5f, -0.5f,
        -0.5f, 0.5f, -0.5f,
        0.5f, 0.5f, -0.5f,
        -0.5f, -0.5f, 0.5f,
        0.5f, -0.5f, 0.5f,
        -0.5f, 0.5f, 0.5f,
        0.5f, 0.5f, 0.5f,
};

private short[] indexArray = new short[] {
        0,1,2,
        1,2,3,

        0,1,4,
        1,4,5,

        2,3,7,
        2,6,7,

        4,5,6,
        5,6,7,

        0,2,4,
        2,6,4,

        1,3,5,
        3,5,7
};

着色器

顶点着色器有一个统一变量mvpMatrix,这个矩阵由model矩阵,view矩阵,projection矩阵相乘得到,我们这里只是演示就将乘法放在Cpu上处理,其实这种乘法操作放在GPU上效率会更高。

片段着色器中我们使用一个统一变量表示颜色,这里我们想要把每一个面渲染成不同颜色,因为如果是同颜色又没有边界,就看不出立体的效果。

这里我们会在每渲染两个三角形后修改统一变量来修改颜色,后面渲染的时候会看到,这个方法其实效率不是最高的,因为会多次调用DrawCall,多次DrawCall必然会增加开销,不过我们这里主要是为了演示变换矩阵的用法,所以就怎么方便怎么来了。

private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
                "uniform mat4 mvpMatrix;" +
                "void main() {" +
                "  gl_Position = mvpMatrix * vPosition;" +
                "}";

private final String fragmentShaderCode =
        "precision mediump float;" +
                "uniform vec4 outputColor;" +
                "void main() {" +
                "  gl_FragColor = outputColor;" +
                "}";

顶点缓冲区/索引缓冲区数据填充

填充流程和之前基本一样,新增了一个GLES30.glEnable(GL_DEPTH_TEST)调用,这个表示需要OpenGL ES根据Z轴来判断深度,处理覆盖关系。

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 清除颜色
    GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    GLES30.glEnable(GL_DEPTH_TEST);
    // 创建顶点缓冲区
    int[] idBuffer = new int[2];
    GLES30.glGenBuffers(2, idBuffer, 0);
    vertexBufferId = idBuffer[0];
    elementBufferId = idBuffer[1];

    // 顶点缓冲区数据填充
    FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
    vertexBuffer.put(vertexArray);
    vertexBuffer.position(0);

    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
    GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertexArray.length * 4,
            vertexBuffer,
            GLES30.GL_STATIC_DRAW
    );
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);

    ShortBuffer indexBuffer = ByteBuffer.allocateDirect(indexArray.length * 2).order(ByteOrder.nativeOrder()).asShortBuffer();
    indexBuffer.put(indexArray);
    indexBuffer.position(0);
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
    GLES30.glBufferData(
            GLES30.GL_ELEMENT_ARRAY_BUFFER,
            indexArray.length * 2,
            indexBuffer,
            GLES30.GL_STATIC_DRAW
    );
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);

    // shader
    shaderProgramId = initShaderProgram(vertexShaderCode, fragmentShaderCode);
}

初始化projection和view矩阵

setIdentityM初始化矩阵,会将矩阵所有值初始化为0,斜角线值初始化为1。

使用setLookAtM获取视口矩阵,使用perspectiveM获取透视投影矩阵,参数下面介绍了。

将两个矩阵相乘存放在vpMatrix,这里之所以没有把model矩阵也放上,是因为我们需要通过model矩阵变化实现正方体旋转,所以需要将model矩阵放在渲染的时候。

public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES30.glViewport(0, 0, width, height);
    // setLookAtM用于获取view矩阵,参数为相机位置x,y,z。模型中心位置坐标x,y,z。向上向量x,y,z
    float[] viewMatrix = new float[16];
    Matrix.setIdentityM(viewMatrix, 0);
    Matrix.setLookAtM(viewMatrix, 0, 3,3,5,0,0f,0f,0f,1f, 0f);

    float[] projectionMatrix = new float[16];
    Matrix.setIdentityM(projectionMatrix, 0);
    // 获取透视投影矩阵,参数有:视角范围45度,宽高比,透视最近距离和最远距离。
    Matrix.perspectiveM(projectionMatrix,0,45f, ((float) getWidth()) / getHeight(),0.3f,50);
    Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
}

渲染

我们这里用radiu表示渲染角度,每次渲染加2度,如果是60HZ则一秒钟会旋转120度。

通过model矩阵旋转radius度,每次渲染两个三角形换一个颜色,以实现不同面不同颜色。

乘出来最终的矩阵就是mvp矩阵,通过统一变量传递mvp矩阵。

public void onDrawFrame(GL10 gl) {
    // 清除屏幕
    GLES30.glClear(GL_DEPTH_BUFFER_BIT);
    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
    // 使能着色器程序
    GLES30.glUseProgram(shaderProgramId);

    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
    int positionLocation = GLES30.glGetAttribLocation(shaderProgramId, "vPosition");
    GLES30.glEnableVertexAttribArray(positionLocation);
    GLES30.glVertexAttribPointer(positionLocation, 3, GLES30.GL_FLOAT, false, 3 * 4, 0);

    radiu += 2;
    if (radiu >= 360) {
        radiu = radiu % 360;
    }
    Matrix.setIdentityM(modelMatrix, 0);
    // model矩阵旋转。后三个参数x,y,z为变量轴
    Matrix.rotateM(modelMatrix, 0, radiu, 1, 1, 0);
    Matrix.multiplyMM(mvpMatrix, 0, vpMatrix, 0, modelMatrix, 0);

    int matrixLocation = GLES30.glGetUniformLocation(shaderProgramId, "mvpMatrix");
    FloatBuffer mvpMatrixBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
    mvpMatrixBuffer.put(mvpMatrix);
    mvpMatrixBuffer.position(0);
    // 将mvp矩阵通过统一变量传给着色器
    GLES30.glUniformMatrix4fv(matrixLocation, 1, false, mvpMatrixBuffer);
    int outputColorLocation = GLES30.glGetUniformLocation(shaderProgramId, "outputColor");

    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
    // 调用DrawCall绘制三角形
    // 每次渲染6个顶点,两个三角形,然后就换颜色。  
    for (int i = 0; i < 6; i++) {
        GLES30.glUniform4f(outputColorLocation, colorList[i][0], colorList[i][1], colorList[i][2], colorList[i][3]);
        GLES30.glDrawElements(GLES30.GL_TRIANGLES, 6, GLES30.GL_UNSIGNED_SHORT, 6 * i * 2);
    }

    // 清除配置
    GLES30.glDisableVertexAttribArray(positionLocation);
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
    GLES30.glUseProgram(0);
}

效果

小结

我们这节通过实现一个旋转的正方体来简单的介绍了一下MVP的矩阵变换模式。这也是我们OpenGL ES最后一节了,其实OpenGL ES还有很多高级的技术,但是几乎都是跟图像数学相关的,OpenGL ES自身的核心接口能力以及介绍的差不多了。

也许在遥远的未来,我们还会写一章Vulkan的教程,在这里埋个坑。

相关推荐
程楠楠&M2 小时前
uni-app运行到 Android 真机和Android studio模拟器
android·uni-app·android studio
小雨cc5566ru4 小时前
Thinkphp/Laravel旅游景区预约系统的设计与实现
android·旅游
yuanlaile8 小时前
Windows上面搭建Flutter Android运行环境
android·flutter
服装学院的IT男8 小时前
【Android 14源码分析】WMS-窗口显示-第一步:addWindow
android
Jasonakeke11 小时前
【重学 MySQL】五十、添加数据
android·数据库·mysql
笑鸿的学习笔记11 小时前
OpenGL笔记之事件驱动设计将相机控制类和应用程序类分离
android·笔记·数码相机
INF_51212 小时前
Android Studio 占满C盘快速解决方法
android·ide·android studio
服装学院的IT男13 小时前
【Android 源码分析】Activity生命周期之onDestroy
android
篝火15 小时前
LangGPT结构化提示词编写实践
android·java·开发语言