OpenGL ES 3.0从画圆中了解坐标系统

以前学习了绘制点、线、三角形,都很完美的展现出来了,因此有点小膨胀,想画一个圆形证实本身OpenGL已经入门了。java

画一个圆形其实和画一个三角形没有太大区别,由于一个圆形也就是由无数个相同顶点的三角形组成的,三角形个数趋向于无限大的时候,整个图案也就越趋向于圆。顶点数据就不能手写了,能够靠代码生成。git

private float[] createPositions() {
    // 绘制的半径
    float radius = 0.8f;
    ArrayList<Float> data = new ArrayList<>();
    data.add(0.0f); //设置圆心坐标
    data.add(0.0f);
    data.add(0.0f);
    float angDegSpan = 360f / 360; // 分红360份
    for (float i = 0; i < 360 + angDegSpan; i += angDegSpan) {
        data.add((float) (radius * Math.sin(i * Math.PI / 180f)));
        data.add((float) (radius * Math.cos(i * Math.PI / 180f)));
        data.add(0.0f);
    }
    float[] f = new float[data.size()];
    for (int i = 0; i < f.length; i++) {
        f[i] = data.get(i);
    }
    return f;
}
复制代码

把圆分红了 360 份。圆形的顶点数据也分为了三部分了,以原心做为咱们的中心点,中间的 360 个点用来绘制三角形,最后一个点使得咱们的图形闭合。github

GLES30.glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, 362);
复制代码

当信心满满地运行后,现实却打了脸:数组

圆形变成了一个椭圆。。。 为何呢?app

OpenGL但愿在每次顶点着色器运行后,咱们可见的全部顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每一个顶点的xyz坐标都应该在**-1.01.0**之间,超出这个坐标范围的顶点都将不可见。学习

而在上面的例子中,假设实际手机分辨率以像素为单位是720x1280,咱们默认使用OpenGL占用整个显示屏。 设备在竖屏模式下,那么[-1,1]的范围对应的高有1280像素,而宽却只有720像素。正由于标准化设备坐标假定坐标空间是一个正方形,而实际的设备屏幕不是正方形,由于宽高之间的比例,使得绘制结果与预料结果不一致。spa

如何解决这个问题?在OpenGL中使用了正交投影的方式解决这个问题。3d

在学习正交投影前先学习下OpenGL的坐标系统。code

坐标系统

OpenGL中咱们一般会本身设定一个坐标的范围,以后再在顶点着色器中将这些坐标变换为标准化设备坐标。而后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。orm

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程一般是分步进行的,也就是相似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标以前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优势在于,在这些特定的坐标系统中,一些操做或运算更加方便和容易,这一点很快就会变得很明显。对咱们来讲比较重要的总共有5个不一样的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

这就是一个顶点在最终被转化为片断以前须要经历的全部不一样状态。

为了将坐标从一个坐标系变换到另外一个坐标系,咱们须要用到几个变换矩阵,最重要的几个分别是模型(Model)观察(View)投影(Projection)三个矩阵。咱们的顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在以后会变为世界坐标(World Coordinate)观察坐标(View Coordinate)裁剪坐标(Clip Coordinate),并最后以**屏幕坐标(Screen Coordinate)**的形式结束。下面的这张图展现了整个流程以及各个变换过程作了什么:

首先了解下OpenGL是一个右手坐标系,简单来讲,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你。坐标系画起来以下:

局部空间

局部空间坐标是 OpenGL 绘制坐标的起点,接下来全部的转换操做都是在局部空间坐标基础上进行的。

局部空间坐标就是咱们本身定义的起始坐标点,是相对于原点 (0,0,0)(0,0,0) 的。

此时所在的空间就是局部空间,也就是说咱们在局部空间里面定义物体的起始坐标。

世界空间

若是咱们将咱们全部的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并非咱们想要的结果。咱们想为每个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。若是你但愿将物体分散在世界上摆放(特别是很是真实的那样),这就是你但愿物体变换到的空间。物体的坐标将会从局部变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的。

模型矩阵是一种变换矩阵,它能经过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你能够将它想像为变换一个房子,你须要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,而后在y轴上往左旋转一点以搭配附近的房子。

观察空间

观察空间常常被人们称之OpenGL的摄像机(Camera)(因此有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。所以观察空间就是从摄像机的视角所观察到的空间。而这一般是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一块儿的变换一般存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。

从平常生活的经验中能够很容易地了解到,随着摄像机位置、姿态的不一样,就算是对同一 个场景进行拍摄,获得的画面也是迥然不一样的。 所以摄像机的位置、姿态在 OpenGL ES 3.0 应用程序的开发中就显得很是重要,因此先介绍一下摄像机的设置方法。

摄像机的设置须要给出 3 方面的信息,包括摄像机的位置、观察的方向以及 up 方向,具体状况如图所示。

  • 摄像机的位置很容易理解,用其在 3D 空间中的坐标来表示。
  • 摄像机观察的方向能够理解为摄像机镜头的指向,用一个观察目标点来表示(经过摄像机位置与观察目标点能够肯定一个向量,此向量即表明了摄像机观察的方向)。
  • 摄像机的 up 方向能够理解为摄像机顶端的指向,用一个向量来表示。

经过摄像机拍摄场景与人眼观察现实世界很相似,所以,经过人眼对现实世界观察的切身感觉能够帮助读者理解摄像机的各个参数。

裁剪空间

在一个顶点着色器运行的最后,OpenGL指望全部的坐标都能落在一个特定的范围内,且任何在这个范围以外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,因此剩下的坐标就将变为屏幕上可见的片断。这也就是裁剪空间(Clip Space)名字的由来。

由于将全部可见的坐标都指定在-1.0到1.0的范围内不是很直观,因此咱们会指定本身的坐标集(Coordinate Set)并将它变换回标准化设备坐标系,就像OpenGL指望的那样。

为了将顶点坐标从观察变换到裁剪空间,咱们须要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,好比在每一个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。全部在范围外的坐标不会被映射到在-1.0到1.0的范围之间,因此会被裁剪掉。在上面这个投影矩阵所指定的范围内,坐标(1250, 500, 750)将是不可见的,这是因为它的x坐标超出了范围,它被转化为一个大于1.0的标准化设备坐标,因此被裁剪掉了。

由投影矩阵建立的观察箱(Viewing Box)被称为平截头体(Frustum),每一个出如今平截头体范围内的坐标都会最终出如今用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(并且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),由于使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。

一旦全部顶点被变换到裁剪空间,最终的操做——透视除法(Perspective Division)将会执行,在这个过程当中咱们将位置向量的x,y,z份量分别除以向量的齐次w份量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每个顶点着色器运行的最后被自动执行。

在这一阶段以后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片断。

将观察坐标变换为裁剪坐标的投影矩阵能够为两种不一样的形式,每种形式都定义了不一样的平截头体。咱们能够选择建立一个正交投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)

正交投影

OpenGL ES 3.0 中,根据应用程序中提供的投影矩阵,管线会肯定一个可视空间区域,称为视景体。视景体是由 6 个平面肯定的,这 6 个平面分别为:上平面(up)、下平面(down)、左平面(left)、右平面(right)、远平面(far)、近平面(near)。

场景中处于视景体内的物体会被投影到近平面上(视景体外面的物体将被裁剪掉),而后再将近平面上投影出的内容映射到屏幕上的视口中。

因为正交投影是平行投影的一种,其投影线(物体的顶点与近平面上投影点的连线)是平行的。故其视景体为长方体,投影到近平面上的图形不会产生真实世界中“近大远小”的效果,下图更清楚地说明了这个问题。

透视投影

现实世界中人眼观察物体时会有“近大远小”的效果,咱们看一条无限长的高速公路或铁路时尤为明显,正以下面图片显示的那样:

因为透视,这两条线在很远的地方看起来会相交。所以,要想开发出更加真实的场景,仅使用正交投影是远远不够的,这时能够采用透视投影。透视投影的投影线是不平行的,他们相交于视点。经过透视投影,能够产生现实世界中“近大远小”的效果,大部分 3D 游戏采用的都是透视投影。

透视投影中,视景体为锥台形区域,如图所示。

从上图中能够看出,透视投影的投影线互不平行,都相交于视点。所以,一样尺寸的物体,近处的投影出来大,远处的投影出来小,从而产生了现实世界中“近大远小”的效果。下图更清楚地说明了这个问题。

把它们都组合到一块儿

咱们为上述的每个步骤都建立了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据如下过程被变换到裁剪坐标:

注意矩阵运算的顺序是相反的(记住咱们须要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

实现画圆

上面大概了解了下OpenGL的坐标系统,里面涉及的知识实在太多,后面慢慢了解。咱们先使用正交投影完成一个完美的圆的绘制。为了解决图像拉伸问题,就是要保证近平面的宽高比和视口的宽高比一致,并且是以较短的那一边做为 1 的标准,让图像保持居中。

OpenGL中经过调用 Matrix 类的 orthoM 方法完成对正交投影的设置,其基本代码以下。

orthoM(float[] m,                            //存储生成矩阵元素的float[]类型数组
       int mOffset,                          //填充起始偏移量
       float left,float right,               //near面的left、right
       float bottom,float top,               //near面的bottom、top
       float near,float far)                 //near面、far面与视点的距离
复制代码
  • orthoM 方法的功能为根据接收的 6 个正交投影相关参数产生正交投影矩阵,并将矩阵的元素填充到指定的数组中。
  • 参数 left、right 为近平面左右侧边对应的 x 坐标,top、bottom 为近平面上下侧边对应的 y坐标,分别用来肯定左平面、右平面、上平面、下平面的位置。参数 near、far 分别为视景体近平面与远平面距视点的距离。

近平面的坐标原点位于中心,向右为 X 轴正方向,向上为 Y 轴正方向,因此咱们的 left、bottom 要为负数,而 right、top 要为正数。同时,近平面和远平面的距离都是指相对于视点的距离,因此 near、far 要为正数,并且 far>near。

能够在 GLSurfaceView 的 surfaceChanged 里面来设定正交投影矩阵。

float aspectRatio = width > height ? (float) width / (float) height : (float) height / (float) width;
if (width > height) {
    Matrix.orthoM(mMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, 0f, 10f);
} else {
    Matrix.orthoM(mMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, 0f, 10f);
}
复制代码

这样就把近平面和视口的宽高比设置为一致的了,解决了以前图像被拉伸的问题。

完整代码请看Github:OpenGLES-Learning : CircleRenderer

相关文章
相关标签/搜索