android平台下OpenGL ES 3.0从矩形中看矩阵和正交投影

OpenGL ES 3.0学习实践

目录

绘制矩形

新建一个矩形渲染器:

public class RectangleRenderer implements GLSurfaceView.Renderer

定义顶点着色器:

#version 300 es
- []

layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec4 aColor;
out vec4 vColor;
void main() {
     gl_Position  =  vPosition;
     gl_PointSize = 10.0;
     vColor = aColor;
}

定义片段着色器:

#version 300 es
precision mediump float;
in vec4 vColor;
out vec4 fragColor;
void main() {
     fragColor = vColor;
}

定义坐标点数据:

private float[] vertexPoints = new float[]{
            //前两个是坐标,后三个是颜色RGB
            0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
            -0.5f, -0.5f, 1.0f, 1.0f, 1.0f,
            0.5f, -0.5f, 1.0f, 1.0f, 1.0f,
            0.5f, 0.5f, 1.0f, 1.0f, 1.0f,
            -0.5f, 0.5f, 1.0f, 1.0f, 1.0f,
            -0.5f, -0.5f, 1.0f, 1.0f, 1.0f,

            0.0f, 0.25f, 0.5f, 0.5f, 0.5f,
            0.0f, -0.25f, 0.5f, 0.5f, 0.5f,
};
public RectangleRenderer() {
        //分配内存空间,每个浮点型占4字节空间
        vertexBuffer = ByteBuffer.allocateDirect(vertexPoints.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        //传入指定的坐标数据
        vertexBuffer.put(vertexPoints);
        vertexBuffer.position(0);
}

开始编译和链接着色器:

@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //设置背景颜色
        GLES30.glClearColor(0.5f, 0.5f, 0.5f, 0.5f);

        //编译
        final int vertexShaderId = ShaderUtils.compileVertexShader(vertextShader);
        final int fragmentShaderId = ShaderUtils.compileFragmentShader(fragmentShader);
        //鏈接程序片段
        mProgram = ShaderUtils.linkProgram(vertexShaderId, fragmentShaderId);
        //在OpenGLES环境中使用程序片段
        GLES30.glUseProgram(mProgram);

        aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition");
        aColorLocation = GLES30.glGetAttribLocation(mProgram, "aColor");

        vertexBuffer.position(0);
        //获取顶点数组 (POSITION_COMPONENT_COUNT = 2)
        GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, STRIDE, vertexBuffer);

        GLES30.glEnableVertexAttribArray(aPositionLocation);

        vertexBuffer.position(POSITION_COMPONENT_COUNT);
        //颜色属性分量的数量 COLOR_COMPONENT_COUNT = 3
        GLES30.glVertexAttribPointer(aColorLocation, COLOR_COMPONENT_COUNT, GLES30.GL_FLOAT, false, STRIDE, vertexBuffer);

        GLES30.glEnableVertexAttribArray(aColorLocation);
}

注意:glVertexAttribPointer这个方法的第5个参数stride,这个参数表示:

每个顶点由size指定的顶点属性分量顺序存储。stride指定顶 点索引I和(I+1),表示的顶点数据之间的位移。如果stride为0,则每个顶点的属性数据顺序存储。如果stride大于0, 则使用该值作为获取下一个索引表示的顶点数据的跨距。

//之前定义的坐标数据中,每一行是5个数据,前两个表示坐标(x,y),后三个表示颜色(r,g,b)
private static final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT;
//所以这里实际是 STRIDE = (2 + 3) x 4

开始绘制:

@Override
public void onDrawFrame(GL10 gl) {
    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);

    //绘制矩形
    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 6);

    //绘制两个点
    GLES30.glDrawArrays(GLES30.GL_POINTS, 6, 2);
}

竖屏状态下显示:

image

横屏模式下显示:

image

宽高比的问题

假设实际手机分辨率以像素为单位是720x1280,我们默认使用OpenGL占用整个显示屏。
设备在竖屏模式下,那么[-1,1]的范围对应的高有1280像素,而宽却只有720像素
图像会在x轴显得扁平,如果在横屏模式,图像会在y轴显得扁平。通过上面的例子可以看到竖屏横屏模式下就有种被拉伸的感觉。

其实在OpenGL中,我们要渲染的一切物体都要映射到x轴y轴[-1, 1]的范围内,对z轴也是一样的。这个范围内的坐标被称为归一化设备坐标,其独立于屏幕实际的尺寸或形状,但是因为它们独立于实际的屏幕尺寸,如果直接使用它们,我们就会遇到刚才的问题。

归一化设备坐标假定坐标空间是一个正方形,然而,我们实际的视口viewport可能不是一个正方形,就像我刚刚手机上显示的一样,图像在一个方向上被拉伸,在另外一个方向上被压扁。因此在一个竖屏设备上,归一化设备坐标上定义的图像看上去就是在水平方向上被压扁,在横屏模式下,同样的图像就在垂直方向上看起来是压扁的。

image

适应宽高比

这个时候我们就需要调整坐标空间,让它把屏幕的形状考虑在内,可行的一个方法是把较小的范围固定在[-1,1]内,而按屏幕尺寸的比例调整较大的范围。

举例来说,在竖屏情况下,其宽度是720,而髙度是1280,因此我们可以把宽度范围限定在[-1,1],并把高度范围调整为[-1280/720,1280/720][-1.78,1.78]。同理,在横屏模式情况下,把宽度范围设为[-1.78,1.78],而把高度范围设为[-1,1]
通过调整已有的坐标空间,最终会改变我们可用的空间,通过这个方法,不论是竖屏模式还是横屏模式,物体看起来就都一样了。

使用虚拟坐标空间

我们需要调整坐标空间,以便我们把屏幕方向考虑进来,需要停止直接在归一化设备坐标上工作,而开始在虚拟坐标空间里工作。需要找到某种可以把虚拟空间坐标转换回归一化设备坐标的方法,让OpenGL可以正确地渲染它们,这个操作叫作正交投影,不管多远 或多近,所有的物体看上去大小总是相同的。

矩阵和向量

正交投影之前,可以先来复习一下矩阵以及向量相关的知识,因为在OpenGL中大量地使用了向量和矩阵,矩阵的最重要的用途之一就是建立正交和透视投影。其原因之一是,使用矩阵做投影只涉及对一组数据按顺序执行大量的加法和乘法,这些运算在现代GPU上执行得非常快。

  • 向量

一个向量是一个有多个元索的一维数组。在OpenGL里,一个位置通常是一个四元素向量,颜色也是一样。我们使用的大多数向量一般都有四个元素。一个位置向量,它有一个x、一个y、一个z和一个w分量。

\begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix}

我们在三维空间中,x,y,z分量用的比较多

  • 矩阵

—个矩阵(Matrix)是一个有多个元素的二维数组。在OpenGL里,我们一般使用矩阵作向量投影,如正交或者透视投影,并且也用它们使物体旋转(rotation)、平移(translatum)以及缩放(scaling)。我们把矩阵与每个要变换的向最相乘即可实现这些变换。

\begin{bmatrix} {x_{x}}&{x_{y}}&{x_{z}}&{x_{w}}\\ {y_{x}}&{y_{y}}&{y_{z}}&{y_{w}}\\ {z_{x}}&{z_{y}}&{z_{z}}&{z_{w}}\\ {w_{x}}&{w_{y}}&{w_{z}}&{w_{w}}\\ \end{bmatrix}

矩阵与向量相乘

\begin{bmatrix} {x_{x}}&{x_{y}}&{x_{z}}&{x_{w}}\\ {y_{x}}&{y_{y}}&{y_{z}}&{y_{w}}\\ {z_{x}}&{z_{y}}&{z_{z}}&{z_{w}}\\ {w_{x}}&{w_{y}}&{w_{z}}&{w_{w}}\\ \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix}= \begin{bmatrix} {x_{x}x}&{x_{y}y}&{x_{z}z}&{x_{w}w}\\ {y_{x}x}&{y_{y}y}&{y_{z}z}&{y_{w}w}\\ {z_{x}x}&{z_{y}y}&{z_{z}z}&{z_{w}w}\\ {w_{x}x}&{w_{y}y}&{w_{z}z}&{w_{w}w}\\ \end{bmatrix}

对于第一行,我们让{x_{x}}{x}相乘、{x_{y}}{y}相乘、{x_{z}}{z}相乘以及{x_{w}}{w}相乘,然后把 所有四个结果加起来得到这个结果的{x}分量。
矩阵第一行的所有四个分都影响了那个结果x,第二行的所有4个分量都影响了那个结果y,以此类推。在矩阵的每一行内,第一个分量与向量的x相乘,第二个分量也与向虽的y相乘,以此类推。

  • 单位矩阵

\begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \end{bmatrix}

这个矩阵乘以任何向量都是得到与之前结果相同的向量

\begin{bmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 2 \\ 3 \\ 4 \end{bmatrix}= \begin{bmatrix} 1 \times1 + 0\times2 + 0 \times3 +0\times4\\ 0 \times1 + 1\times2 + 0 \times3 +0\times4\\ 0 \times1 + 0\times2 + 1 \times3 +0\times4\\ 0 \times1 + 0\times2 + 0 \times3 +1\times4\\ \end{bmatrix}

结果

\begin{bmatrix} 1 \\ 2 \\ 3 \\ 4 \end{bmatrix}

  • 平移矩阵

平移矩阵可以把一个物体沿着指定的方向移动

\begin{bmatrix} 0 & 0 & 0 & x_{translation} \\ 0 & 1 & 0 & y_{translation} \\ 0 & 0 & 1 & z_{translation} \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

计算过程:

\begin{bmatrix} 1 & 0 & 0 & 3\\ 0 & 1 & 0 & 3\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 2 \\ 2 \\ 0 \\ 1 \end{bmatrix}= \begin{bmatrix} 1 \times2 + 0\times2 + 0 \times0 +3\times1\\ 0 \times2 + 1\times2 + 0 \times0 +3\times1\\ 0 \times0 + 0\times0 + 1 \times0 +0\times1\\ 0 \times2 + 0\times2 + 0 \times0 +1\times1\\ \end{bmatrix}

最终的结果:

\begin{bmatrix} 5 \\ 5 \\ 0 \\ 1 \end{bmatrix}

正交投影

android中可以使用Matrix类来定义正交投影,这个类有一个称为orthoM的方法,它可以为我们生成一个正交投影。我们将使用这个投影调整坐标空间。正交投影与平移矩阵是非常相似的。

方法的参数描述:

参数 描述
float[] m 目标数组,这个数组的长度至少有16个元素,这样它才能存储正交投影矩阵
int mOffset 结果矩阵起始的偏移值
float left x轴的最小范围
float right x轴的最大范围
float bottom y轴的最小范围
float top y轴的最大范围
float near z轴的最小范围
float far z轴的最大范围

这个方法会产生下面的正交投影矩阵:

\begin{bmatrix} \cfrac{\mathbf2}{\mathbf {right-left}} & \mathbf0 & \mathbf0 & -\cfrac{\mathbf{right+left}}{\mathbf {right-left}}\\ \\ \mathbf0 & \cfrac{\mathbf2}{\mathbf {top-bottom}} & \mathbf0 & -\cfrac{\mathbf{top+bottom}}{\mathbf {top-bottom}}\\ \\ \mathbf0 & \mathbf0 & \cfrac{-\mathbf2}{\mathbf {far-near}} & -\cfrac{\mathbf{far+near}}{\mathbf {far-near}}\\ \\ \mathbf0 & \mathbf0 & \mathbf0 & \mathbf{1} \end{bmatrix}

经过上述正交投影矩阵转换之后,转换回归一矩阵

\begin{bmatrix} \cfrac{\mathbf2}{\mathbf {1.78-(-1.78)}} & \mathbf0 & \mathbf0 & -\cfrac{\mathbf{1.78+(-1.78)}}{\mathbf {1.78-(-1.78)}}\\ \\ \mathbf0 & \cfrac{\mathbf2}{\mathbf {1-(-1)}} & \mathbf0 & -\cfrac{\mathbf{1+(-1)}}{\mathbf {1-(-1)}}\\ \\ \mathbf0 & \mathbf0 & \cfrac{-\mathbf2}{\mathbf {1-(-1)}} & -\cfrac{\mathbf{1+(-1)}}{\mathbf {1-(-1)}}\\ \\ \mathbf0 & \mathbf0 & \mathbf0 & \mathbf{1} \end{bmatrix} \begin{bmatrix} 1.78 \\ 1 \\ 0 \\ 1 \end{bmatrix}=

\begin{bmatrix} \cfrac{\mathbf2}{\mathbf {1.78-(-1.78)}}\times\mathbf1.78 + \mathbf0\times\mathbf1 + \mathbf0 \times\mathbf0 + -\cfrac{\mathbf{1.78+(-1.78)}}{\mathbf {1.78-(-1.78)}}\times\mathbf0\\ \\ \mathbf0\times\mathbf1 + \cfrac{\mathbf2}{\mathbf {1-(-1)}}\times\mathbf1 +\mathbf0\times\mathbf0 + -\cfrac{\mathbf{1+(-1)}}{\mathbf {1-(-1)}}\times\mathbf1\\ \\ \mathbf0 \times\mathbf1.78+ \mathbf0\times\mathbf1 + \cfrac{-\mathbf2}{\mathbf {1-(-1)}}\times\mathbf0 + -\cfrac{\mathbf{1+(-1)}}{\mathbf {1-(-1)}}\times\mathbf1\\ \\ \mathbf0\times\mathbf1.78 + \mathbf0\times\mathbf1 + \mathbf0\times\mathbf0 + \mathbf{1}\times\mathbf1 \end{bmatrix}

输出结果

\begin{bmatrix} 1 \\ 1 \\ 0 \\ 1 \end{bmatrix}

RectangleRenderer类中定义目标数组

private final float[] mMatrix = new float[16];

修改顶点着色器

#version 300 es
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec4 aColor;
uniform mat4 u_Matrix;
out vec4 vColor;
void main() { 
     gl_Position  = u_Matrix * vPosition;
     gl_PointSize = 10.0;
     vColor = aColor;
}

修改onSurfaceCreated回调

@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        ......
        //在OpenGLES环境中使用程序片段
        GLES30.glUseProgram(mProgram);

        uMatrixLocation = GLES30.glGetUniformLocation(mProgram, "u_Matrix");

        aPositionLocation = GLES30.glGetAttribLocation(mProgram, "vPosition");
        aColorLocation = GLES30.glGetAttribLocation(mProgram, "aColor");

        ......
}

onSurfaceChanged回调中

@Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES30.glViewport(0, 0, width, height);
        final float aspectRatio = width > height ?
                (float) width / (float) height :
                (float) height / (float) width;
        if (width > height) {
            //横屏
            Matrix.orthoM(mMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f);
        } else {
            //竖屏
            Matrix.orthoM(mMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f);
        }
}

onDrawFrame回调中

@Override
    public void onDrawFrame(GL10 gl) {
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);

        GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mMatrix, 0);

        //绘制矩形
        GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 6);

        //绘制两个点
        GLES30.glDrawArrays(GLES30.GL_POINTS, 6, 2);

}

我们来观察一下最终的横竖屏状态

竖屏状态

image

横屏状态

image

左手与右手坐标系

左手坐标系

image

右手坐标系

image

项目地址:
https://github.com/byhook/opengles4android

参考:
《OpenGL ES 3.0 编程指南第2版》
《OpenGL ES应用开发实践指南Android卷》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容