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的教程,在这里埋个坑。

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95272 天前
Andorid Google 登录接入文档
android
黄林晴2 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android