Part I A Simple game of air hockey(空气曲棍球)-Chapter2 Defining Vertices and Shaders

   上一章节简单介绍了OpenGL ES 2.0开发的简单入门知识,实际上你学没有真正进入OpenGL ES 2.0的世界,从这一章开始将会一步步的带着你开始你的OpenGL ES 2.0实战之旅,这里最终会实现一个“空气曲棍球”的实例,学习完这一阶段的教程,你将了解如何绘制常规的几何图形,如三角形、四边形、正方体、球体,了解什么是顶点着色器,了解什么是片元着色器,了解如何进行纹理贴图。下面正式进入主题。

Chapter 2 定义顶点(Vertices)与着色器(Shaders) (Defining Vertices and Shaders)

    这一章节将会介绍一个简单的曲棍球游戏,在我们实现这个游戏的同时,将会学习一些重要的OpenGL ES 2.0的形体绘制技巧。
    我们首先学习如何由点开始构建形体,然后学习如何利用着色器来绘制形体,着色器是一种告诉OpenGL如何绘制形体的小程序。由于点、线、三角形这些几何形状(图元)都是由顶点组合并由着色器进行绘制而成的,所以这两个概念非常重要。
2.1 Why Air Hockey?
  这里简单介绍下这里要实现的效果,首先是一张球台,这里暂时以简单矩形代替,然后中间有一条分隔线,两边是两个球棍,矩形的中心位置是一个球(暂时也点代替)。这些就是我们要绘制的元素。当这一系列教程完结后将会绘制如下更真实的场景。


2.2 准备工作(Don’t Start from Scratch)
 1 把上一章的工程复制一份,并命名为AHockey1
 2 把FirstOpenGLProjectActivity.java 更名为AirHockeyActivity, FirstOpenGLProjectRenderer.java更名为AirHockeyRenderer.java。
 3 在res/values/strings.xml中把app_name的值改为Air Hockey
 4 包名com.firstopenglproject.android’更改为‘com.airhockey.android’
 5 打开AndroidManifest.xml 把包名更改为 com.airhockey.android,同时一并 修改 android:name 为‘com.airhockey.android.AirHockeyActivity’

2.3 定义球台(桌面)的结构(Defining the Structure of Our Air Hockey Table)
    在我们绘制桌面之前我们需要告诉OpenGL如何进行绘制;在绘制的第一步就是定义一种让OpenGL能够理解的几何结构。在OpenGL中,所有的几何结构都开始于顶点(Vertex)。
 1)关于顶点
  顶点就是代表几何体的一角的一个点,它可以关联多种属性。最重要的就是关于这个点位于几何坐标空间中的哪个位置。
 2)使用点定义桌面
    这里将使用一个矩形来代替桌面,因为矩形有四顶点,我们将会使用4个vertices,由于它是2维的,所以每一维都会有一个坐标代表它的位置。
 假如我们用纸把它画出来,我们可能画出如下所示的矩形桌面:

 3)定义顶点
   我们将会用一个float类型的数组来保存我们的顶点,由于是2维的,每一个顶点将会由两个float类型的数值来表示,一个表示x方向的位置,另一个表示y方向的位置。
   这里定义一个常量表示每个顶点的维数,把如下变量定义在AirHockeyRender里面。

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
private static final int POSITION_COMPONENT_COUNT = 2;

    添加如下的构造函数:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
public AirHockeyRenderer() {
    float[] tableVertices = {
        0f, 0f,
        0f, 14f,
        9f, 14f,
        9f, 0f
    };
}

     这里把顶点定义在一个顺序数组里面,后面将会引用这个数组来表示顶点位置。这里只保存了顶点的位置,当然在需要的时候也会按照这种方式来保存与顶点相应的颜色。
4) 点、线、三角形
    在OpenGL中我们只提供了绘制点、线、三角形的方法,那我们怎样绘制一个矩形呢?
  三角形是一种最基本的图形,在实现生活中到处都是,比如那些结构化的铁桥,因为它的基本图元就是稳定的三角形。三边连接三个顶点,假如我们拿掉一个顶点,则它变成一条线,再拿掉一个点,则会得到一个点。
    点和线可以用来实现一些特定的效果,但是只有三角形才可以用来实现一些复杂的几何物体及纹理这样的场景。在OpenGL中通过组合顶点并告诉OpenGL如何把这些顶点连接起来就可以实现绘制三角形。所有我们期望绘制的几何物体都需要由点、线、三角形来组成,假如我们需要绘制一个拱桥,则需要足够多的点来进行绘制相应的曲线。
   因此在不能直接绘制矩形的情况下我们如何去绘制它呢?其实我们可以想象成是由两个三角形组成的,如下图所示:

     现在修改代码,表示我们将使用两个三角形而不是使用四边形进行绘制,如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
float[] tableVerticesWithTriangles = {
    // Triangle 1
    0f, 0f,
    9f, 14f,
    0f, 14f,
    // Triangle 2
    0f, 0f,
    9f, 0f,
    9f, 14f
};

    我们的数组现在包含了两个三角形的顶点,第一个三角形与顶点(0, 0), (9, 14), (0, 14)关联,第二个三角形与第一个三角形中的两个顶点相同并与(0, 0), (9, 0), (9, 14)关联。
    当我们想用OpenGL进行绘制的时候,我们要从点、线、三角形的角度去考虑如何构建我们的几何形体。
tips:三角形的顶点环绕顺序(The Winding Order of a Triangle)
    你可能已经注意到,我们在定义顶点的时候是按照逆时针方向进行顶点顺序定义的,这个叫做环绕方向。如果我们能一致性的在任何地方使用一种环绕方向,那将会得到一定的性能提升,因为我们可以使用这个环绕方向来指定一个物体的的前面和后面,这样的话让OpenGL不要去绘制我们看不见的那一面(后面back of any object)。
5)添加中心分隔线及两个球棍(Adding the Center Line and Two Mallets)

    到这里顶点的定义基本上完成,不过需要定义中心分隔线以及两个球棍,这里定义的图形如下所示:

    我们使用一条线代替中间线,用点代替球棍,在之前顶点数组中增加如下顶点:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
// Line 1
0f, 7f,
9f, 7f,
// Mallets
4.5f, 2f,
4.5f, 12f

2.4 Making the Data Accessible to OpenGL(为OpenGL环境设置数据)

    现在我们已经定义了顶点,但是要让OpenGL能绘制这些点的话还差最后一步。原因是我们的java代码运行环境与OpenGL代码(shader)的运行环境并不是使用同一种语言,这里有两个概念我们必须要理解清楚:
    1)当我们在模拟器或者真机中运行我们的代码的时候,它并不是直接运行于机器的硬件环境中。而是运行于我们熟知的Dalvik虚拟机环境中,在这个环境中运行的代码不能直接访问硬件(native environment)除非通过特殊的API。
    2)Dalvik虚拟机也使用GC机制,这意味着当它发现有一个变量、对象或者一段内存不再使用的时候,GC会回收相关内存以便重用这段内存,它也可能会重新整理内存以便能更高效的执行内存分配操作。
    硬件运行环境并不像虚拟机的运行机制,它也不会期望有一段内存可以回收或者自动释放。
 Android就是这样设计的,所以开发们在开发应用的时候不需要关心特定的CPU构架或者机器构架,也不需要去关心一些低层的内存管理机制。这种运行方式通常工作得很好,但是当我们要与一些低层的库交互的时候,我们需要遵循相关库的原则,比如OpenGL,作为一个低层的库OpenGL直接运行于硬件上,这里没有虚拟机、没有GC、没有内存紧缩操作。
1 调用本地代码(Calling Native Code from Java)

    Dalvik虚拟机正好体现了Android的强大之处,但是假如我们的代码位于虚拟机中,那怎样与OpenGL交互呢?这里有两种方法,第一种方法是使用JNI,这种方法已经由Android SDK为我们集成好了。当我们调用位于包android.opengl.GLES20中的方法时,SDK就是使用JNI在幕后调用底层系统库的。
2 把java内存拷贝到native内存(Copying Memory from Java’s Memory Heap to the Native Memory Heap)
    第二种方法是改变内存分配的方式。我们可以使用Java中的一些特殊函数,这些函数将会分配一些native内存且把java层数据复制到native内存中。这些native内存对于native环境来说是可访问的,且它位于GC的管辖范围外。
我们将会向下图一样传输数据,在Render开头处构造函数前添加如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
private static final int BYTES_PER_FLOAT = 4;
private final FloatBuffer vertexData;

    我们添加了一个常量BYTES_PER_FLOAT及一个FloatBuffer。在java中float是32位,也就是常量所定义的4个字节,后面有很多地方将会访问到这个常量。FloatBuffer将用于存储一些位于native内存中的数据。
现在构造函数中加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
vertexData = ByteBuffer
    .allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
    .order(ByteOrder.nativeOrder())
    .asFloatBuffer();
vertexData.put(tableVerticesWithTriangles);

    对这段稍作解释下,首先用 ByteBuffer.allocateDirect()分配了一段不受GC控制的native内存,我们需要传递一个表示需要分配多少字节的参数,因为我们的顶点数据是存储在一个float数组中,而每一个float占用4个字节,所以我们传递了参数tableVerticesWithTriangles.length * BYTES_PER_FLOAT。
    下一行告诉ByteBuffer应该把字节数据按照native order来组织存储。当我们存储一些跨越多个字节的数值时,比如一个32位的Integer,字节数据可以按照最重要的到次重要的排序或者按照相反顺序排序,就像你把一个数值从左到右写和从右向左写一样。知道低层以什么样的顺序组织字节数据对于我们来说并不是多么重要,重要是的我们要与平台使用的字节顺序一致,我们通过调用order(ByteOrder.nativeOrder())来做到这一点。
    最后,我们并不直接处理字节数据,而是与通过调用asFloatBuffer() 得到的字节数组FloatBuffer进行交互,然后调用vertexData.put(tableVerticesWithTriangles)把数据从Dalvik内存拷贝到native内存。当持有这段内存的进程销毁的时候这段内存将会得到释放,所以通常情况下我们并不需要担心这种情况。假如你写出了一些一直创建ByteBuffer的代码,你可能就需要去看下有堆分裂及内存管理方面的技术了。
    这里用简单的几行代码就能把数据从Dalvik内存拷贝到OpenGL中去,在继续后面的学习之前了解下这个工作方式是很重要的。就像各个国家的文化不同一样,当我们进入到native代码中的时候也需要了解这种变化。

2.5 OpenGL管道化(OpenGL Pipeline)

    我们现在已经介绍了球台面的结构,而且已经把数据拷贝到了OpenGL能访问到的native内存中。在能够绘制桌面之前我们需要通过OpenGL管道化(如下图大致流程)来传送这些数据,而这个任务就交给了被称为子程序的shader,shader将会告诉GPU如何使用我们给的数据来绘制图形。OpenGL中有两种类型的shader,在使用OpenGL绘制的任何程序中我们都需要定义这两种类型的shader(也称为着色器)。


 1> 顶点着色器(Vertex shader)会针对每一个顶点都会跑一次且决定最终绘制位置,一旦最终位置知道,OpenGL将会把可见的顶点集合组合成点、线和三角形。
 2> 片元着色器(ragment shader)将最终决定每个顶点的颜色,片元可以理解为一个单色的小矩形块,可以类比于电脑屏幕中的一个像素。一旦最终颜色已经分配,OpenGL将会把它们写入到一个叫做帧缓存(FrameBuffer)的内存块中,然后Android就会把这个帧缓存显示到屏幕上去。
Tips:我们为什么要用着色器?
    在还没有采用着色器编写渲染效果之前,OpenGL使用一些固定的函数来实现一些有限的效果,比如场景中有几个光源、需要添加多少烟雾。这些固定API很容易使用,但是却不容易扩展,你仅仅能够使用这些API实现相应有限的效果。假如你想要实现实现如卡通渲染之类的一些自定义效果,很抱歉,你是做不到的。
    OpenGL的工作者意识到随着硬件的提高OpenGL API也需要演进并很好的利用硬件。在OpenGL ES 2.0中,他们 增加了可编程着色器,同时为了保持简洁,他们把所有之前的固定渲染API移除了,因此我们必须使用着色器编程。
   我们现在可以利用着色器控制每一个顶点如何绘制到屏幕上去,同时也可以控制每个点、线、三角形的片元如何得到绘制,这就为我们绘制任务东西打下了一个基础,我们现在可以做基于像素的光照和其它细腻的效果,比如cartoon-cel shading。只要我们能用着色器语言把我们想像的自定义效果表达出来,那我们就可以把它绘制出来。

1 创建我们的第一个顶点着色器(Creating Our First Vertex Shader)
    我们创建一个接收在java代码中定义的顶点坐标的着色器,按如下步骤进行:
     1)在res目录下创建一个raw目录
     2) 在raw目录中创建一个名为simple_vertex_shader.glsl的文件。

    我们将在simple_vertex_shader.glsl里面写着色器代码,如下代码所示:

//AirHockey1/res/raw/simple_vertex_shader.glsl
attribute vec4 a_Position;
void main() {
    gl_Position = a_Position;
}

    这个着色器是使用GLSL(OpenGL's shading language)编写的,这种着色器语言有着与C语言相似的语法结构。更多请参考GLSL快速概览或者详细指南
    这个着色器将会针对我们定义的每个顶点调用一次,当它调用的时候就会接收一个a_Position属性vec4类型的参数。
一个vec4类型的向量包含了四个部分,在位置向量这种情况下,我们可以把这四个部分就是x、y、z及w坐标,x、y、z对应于3D坐标,w则是一个特殊的坐标我们将在后面介绍。假如没有定义,OpenGL的默认行为是把前三个部分设置为0,最后一个为1。
还记得之前我们提过的一个顶点有多种属性吗?比如颜色、位置,这里的attribute关键字正好体现了如何把这些属性赋值给着色器。
    然后是main函数,也就是着色器的函数入口点,这里仅仅是把我们定义的顶点position拷贝到内建变量gl_Position中去,着色器必须对gl_Position赋值,OpenGL将会把gl_Position作为我们定义的顶点的最终位置,然后把顶点集合组成点、线、三角形。

2 创建我们的第一个片元着色器(Creating Our First Fragment Shader)
    现在我们已经创建了一个能决定最终顶点位置的顶点着色器,但还需要再创建一个决定最终顶点颜色的片元着色器;在创建片元着色器之前,我们先来学习什么是片元着色器、如何创建片元着色器。

1) 光栅化艺术
    我们的手机屏幕由数千到数百万像素点组成的,每一个像素都可以显示数百万种颜色中颜色中的一种。然而这其实只是对我们眼睛的视觉上的一种欺骗而已,大部分屏幕并不能真正的创建数百万种颜色,因为每个像素都很小且由黄绿蓝三中颜色组成,这样我们的眼睛就会把这三种颜色混合在一起,于是就产生了各种各样的颜色。当把把足够多的像素显示在一起的时候我们就可以显示一页的文字或者展示一幅蒙拉丽莎的相片。
    OpenGL通过一个叫做光栅化的过程可以把点、线、三角形分解成片元(fragment),OpenGL就是通过这样的过程把一幅图像映射到手机屏幕上的像素点的。这些片元就类似于手机屏幕上的像素点,且每个片元也是由一种颜色(包括RGBA)组成的,片元着色器中的颜色就是由RGBA组成的,在下一节我们将会介绍这个颜色模型。
    在下图中展示了OpenGL如何把一条线光栅化成片元(fragment),显示系统通常会把片元直接映射到屏幕像素,因此一个片元就对应到一个屏幕像素。然而情况并不总是这样,一个超高分辨率屏幕有可能用一个大的片元(big fragment,即一个片元对应到多个像素点)以便减少GPU的工作。

2)创建片元着色器
    片元着色器的主要目的是告诉GPU最终将会渲染什么颜色,OpenGL将对图元(点、线、三角形)的每个片元(fragment)调用一次片元着色器子程序,因此如果一个三角形映射到10000个片元,那么片元着色器将会被调用10000次。
现在我们将在/res/raw/simple_fragment_shader中创如下建片元着色器:

//AirHockey1/res/raw/simple_fragment_shader.glsl
precision mediump float;
uniform vec4 u_Color;
void main() {
    gl_FragColor = u_Color;
}

数据精度修饰符
    第一行代码为片元着色器指定了所有float类型数据的默认精度,这就像在Java代码中选择float或者double类型一样。
我们可以选择lowp、mediump、highp类型的精度,分别对应于低精度、中精度和高精度。然而高精度片元着色器仅仅在某些OpenGL实现上才支持。
    那为什么在顶点着色器中没有这个精度选择过程呢?顶点着色器也可以改变它的默认精度,但是因为对于顶点来说顶点的精度很重要,因此OpenGL设计者把顶点着色器的精度默认设置为高精度了。
    正如你所想象,高精度意味着更精确,但是它是以性能的损失为代价的。对于我们这里的片元着色器,为了达到最大兼容性而选择了中精度,同时也是速度和体验上(speed and quality)的一个折中。

片元着色器的颜色
    这里的片元着色器与之前我们定义的顶点着色器很相似,这次我们传递了一个uniform类型的u_Color变量,它并不会像顶点着色器一样每个顶点都会设置一次,uniform类型的变量的值会为所有顶点保持同一值直到你改变它为此。就像顶点着色器一样,颜色u_Color也是由4个部分组成的一个向量,分别对应于RGBA。
    片元着色器的入口也是main函数,它把uiform类型的变量gl_FragColor,每个片元着色器都必须提供gl_FragColor的值,OpenGL将会用这个值作为当前片元着色器的最终颜色。

2.6 OpenGL使用的颜色模型(The OpenGL Color Model)

    OpenGL使用的颜色模型是由RGB三种颜色组成的加成色模型,很多颜色都可以由这三种颜色按照不同的比例混合得到。比如红色和绿色得到黄色,红色和绿色得到洋红,蓝色和绿色得到青色,如果把三种颜色混合则得到白色,就像下图显示的一样:

    这个颜色模型也许与你在学校里面学的减成色模型不一样,在减成色模型里面,蓝色和黄色得到绿色,把多种颜色混合在一起会得到一个暗的颜色。造成这种现象的主要原因是这种成色模型不发射光(does not emit light)而是吸收光,我们使用越多的颜色进行混合,那光吸收得到越多,得到的颜色就越暗。
    RGB这种加成色的颜色模型遵循光照的一些特性,当两种不同颜色混合在一起的时候,我们得到不是一个更暗的颜色,而是一个更亮的颜色。当大雨过后,我们看到的天空的中的彩虹,它就是由可见光谱中的所有不同颜色组成的,这些颜色混合可以得到白色。
1 把颜色映射显示到屏幕上(Mapping Colors to the Display)
    在OpenGL里面,所有的颜色都有一种线性关系:一个0.5倍的红色应与0.25倍红色之间的亮度关系应该是2倍,同理,1倍红色与0.5倍红色之间也是2倍的亮度关系。这种基色在OpenGL中被规范化到区间[0,1],0代表没有这种颜色,1代表这种颜色的全色显示。
    这种颜色模型也我们的电脑或者手机屏幕都有很好的映射关系(然而,非线性屏幕中我们将了解到这种映射并非一对一)。这些显示屏幕几乎都是使用RGB三色模型,0代表没有该颜色通道,1代表该颜色通道完全显示。在OpenGL中使用这种颜色模型几乎可以把我们能看到的所有颜色都渲染出来。
    在(第四章节,Adding Color and Shader)我们将会学习到更多关于颜色的使用。

2.7 A Review
    我们基本上将这一章都放在了学习定义顶点和渲染器上,让我们复习下主要学习到了些什么:
    首先学习了如何定义顶点数组以及如何把这些顶点数组拷贝到OpenGL能访问到的native内存去中。然后写了一个顶点着色器以及一个片元着色器,这里我们知道渲染器只是跑在GPU上的一种程序。
    在下一章节中,我们将在这一章节的基础上继续我们的学习,当学习完下一章节后我们就可以把桌面画出来了,并且能做更多的练习;首先将学习如何把着色器代码加载到内存中然后编译它,因为顶点着色器和片元着色器都是放在一起共同工作的,所以我们还要学习如何把他们链接到一起以便OpenGL能够执行它。
    一旦我们编译并把着色器代码链接到一起,我们就可以把所有的东西组合到一起,然后就可以用OpenGL把我们第一个版本的球台桌面画到屏幕上去了(点击进入下一章)。
 

最后附上工程代码(点击下载

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值