Part I A Simple game of air hockey(空气曲棍球) Chapter3 Compiling Shaders and Drawing to the Screen)

Chpater 3  编译着色器并绘制到屏幕上(Compiling Shaders and Drawing to the Screen)

    这章将在上章的基础上进行学习,在这一章里首先学习如何编译、链接我们已经定义好的着色器并得到一个OpenGL着色器程序,然后我们利用这个着色器程序把我们的球台桌面绘制到屏幕上。
    现在打开AirHockey1,然后在上章的基础上继续完善我们的程序。

3.1 加载着色器(Loading Shaders)

    我们已经定义好着色器,下一步是将着色器代码加载到内存去中,所以这里我们需要编写一个方法去读取资源目录中的着色器代码。

3.1.1 从资源中加载代码(Loading Text from a Resource)

     首先创建一个java包com.airhockey.android.util,然后在该包下创建一个类TextResourceReader,并在该类中编写如下方法:

//AirHockey1/src/com/airhockey/android/util/TextResourceReader.java
public class TextResourceReader {
    public static String readTextFileFromResource(Context context, int resourceId) {
        StringBuilder body = new StringBuilder();
        try {
            InputStream inputStream = context.getResources().openRawResource(resourceId);
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String nextLine;
            while ((nextLine = bufferedReader.readLine()) != null) {
                body.append(nextLine);
                body.append('\n');
            }
        } catch (IOException e) {
            throw new RuntimeException("Could not open resource: " + resourceId, e);
        } catch (Resources.NotFoundException nfe) {
            throw new RuntimeException("Resource not found: " + resourceId, nfe);
        }

        return body.toString();
    }
}

    我们定义了方法readTextFileFromResource(),这个方法将从资源目录中加载文本。然后我们将在加载着色器代码的时候调用方法readTextFileFromResource(),并传入context以及着色器代码资源id,context用来访问资源目录下的资源。比如为了加载片元着色器代码,我们可以像这样调用这个方法:readTextFileFromResource(this.context, R.raw.simple_fragment_shader)。
    这里我们考虑了一些可能的出错情况,比如:资源可能不存在或者在读取资源的时候存在错误,在这些情况下我们捕获了这个错误,并抛出错误的原因。假如读取资源失败了,我们可以通过错误堆栈及描述找到错误的原因。

3.1.2 加载着色器代码(Reading in the Shader Code)

    现在我们将真正加载着色器代码,打开AirHockeyRender.java,在onSurfaceCreated()中glClearColor()的后面加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
String vertexShaderSource = TextResourceReader.readTextFileFromResource(context,         
    R.raw.simple_vertex_shader);
String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context,     
    R.raw.simple_fragment_shader);

    这里还需要引入context,在该类的顶部加入如下变量:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
private final Context context;

    修改AirHockeyRender的构造函数如下:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
public AirHockeyRenderer(Context context) {
    this.context = context;
}

    同时在使用AirHockeyRender的地方传入context,打开AirHockeyActivity.java 并按下面修改方法调用glSurfaceView.setRenderer() :

//AirHockey1/src/com/airhockey/android/AirHockeyActivity.java
glSurfaceView.setRenderer(new AirHockeyRenderer(this));
rendererSet = true;

    由于Activity其实就是一个Context类型变量,因此这里直接传入this。

3.1.3 加入一些Log(Keeping a Log of What's Happening)

    随着我们加入一些复杂的代码,加入log将会帮助我们知道哪里发生了什么,并容易判断哪里发生了错误。在Android里面我们可以使用内建的类Log来输出一些调试信息。
    由于我们并不想任何时候都输出log,因此我们在包com.airhockey.android.util下面添加一个类LoggerConfig 以控制是否输出log,如下代码所示:

//AirHockey1/src/com/airhockey/android/util/LoggerConfig.java
package com.airhockey.android.util;
    public class LoggerConfig {
    public static final boolean ON = true;
}

    在输出Log之前将会检查这个变量是否为true,要打开或者关闭Log,我们只需更新该变量并重新编译程序即可。

3.2 编译着色器程序(Compiling Shaders)

    现在我们已经把着色器代码加载到内存中了,下一步就是编译着色器代码。我们将创建一个辅助类用于创建着色器对象,编译着色器并把最终结果返回给调用者;一旦我们写好这样的模板代码,那么后面可以复用它了。
    现在我们创建个辅助类ShaderHelper,并添加如下代码:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
private static final String TAG = "ShaderHelper";
public static int compileVertexShader(String shaderCode) {
    return compileShader(GL_VERTEX_SHADER, shaderCode);
}
public static int compileFragmentShader(String shaderCode) {
    return compileShader(GL_FRAGMENT_SHADER, shaderCode);
}
private static int compileShader(int type, String shaderCode) {
}

    接下来,我们将一步步实现compileShader()。

3.2.1 创建着色器对象(Creating a New Shader Object)

    我们的第一步就是创建着色器对象,并检查是否创建成功,在compileShader()中加入如下代码:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
final int shaderObjectId = glCreateShader(type);
    if (shaderObjectId == 0) {
        if (LoggerConfig.ON) {
            Log.w(TAG, "Could not create new shader.");
        }
        return 0;
    }

    我们调用glCreateShader()创建了一个着色器对象,并把返回的id存储在shaderObjectId中;对于顶点着色器使用类型GL_VERTEX_SHADER,对于片元着色器使用类型GL_FRAGMENT_SHADER。
    请注意这里是如何创建并检查所创建的对象是否合法的,在OpenGL中通常是以这样的模式创建对象:
    1)我们首先使用glCreateShader()创建了一个对象,该调用会返回一个Integer类型的id。
    2)这个id代表了OpenGL对象的引用(句柄),在后面我们将用这个id来引用刚刚创建的对象。
    3)如果返回值为0,则代表创建对象失败了,这类似于我们在java代码中使用null返回值一样。
    在创建失败的时候我们将会返回0,这里为什么我们不是抛出异常而是返回0呢?其实在OpenGL内部是不会抛出任何异常的。OpenGL将会通过返回0或者通过调用glGetError()来告诉我们发生了错误;为了保持一致性,我们将会遵循这种调用规则。

3.2.2 编译着色器对象(Uploading and Compiling the Shader Source Code)

    下面的代码将会把着色器代码加载到着色器中,

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
glShaderSource(shaderObjectId, shaderCode);

    一旦我们成功创建着色器对象,我们就可以通过调用glShaderSource(shaderObjectId, shaderCode)加载着色器代码。这将告诉OpenGL载入shaderCode所指对象的代码并与shaderObjectId引用的对象相关联在一起;下一步调用glCompileShader(shaderObjectId) 编译着色器,代码如下:

glCompileShader(shaderObjectId);

    这样OpenGL就会编译刚刚载入shaderObjectId对象的代码。

3.2.3 获取编译状态(Retrieving the Compilation Status)

    添加如下代码,这样将会检查OpenGL是否已经成功编译着色器代码:

final int[] compileStatus = new int[1];
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

    这里用一个int类型长度为1的数组为参数调用glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0)函数获取编译状态,OpenGL会把结果写入compileStatus的第一个对象中。
    在Android里面常常使用这样的方式获取一个OpenGL状态值,首先创建只有一个元素的数组然后传递给相应的OpenGL函数,同时告诉OpenGL把结果存储在数组中第1个元素的位置。

3.2.4 获取着色器相关的Log(Retrieving the Shader Info Log)

    当我们获取编译状态的时候,OpenGL仅仅只是给了我们一个非真即假的值,通过这样的值我们并不知道到底发生了什么或者哪里错误了,幸运的是我们可以调用glGetShaderInfoLog(shaderObjectId)获取一些可读的信息,假如发生了什么错误,OpenGL将会把信息写到着色器的描述Log里面。
    通过下面的代码我们可以获取着色器描述信息:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
if (LoggerConfig.ON) {
    // Print the shader info log to the Android log output.
    Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
        + glGetShaderInfoLog(shaderObjectId));
}

    我们把输出信息打印到Android Log中,并把他们放到一个条件判断里面,这样的话我们可以轻易的打开或者关闭相关Log输出。

3.2.5 检查编译状态并返回着色器id(Verifying the Compilation Status and Returning the Shader Object ID)

    我们已经把着色器描述信息打印出来了,现在我们可以检查编译是否成功了,代码如下:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
if (compileStatus[0] == 0) {
    // If it failed, delete the shader object.
    glDeleteShader(shaderObjectId);
    if (LoggerConfig.ON) {
        Log.w(TAG, "Compilation of shader failed.");
    }
    return 0;
}

    我们所做的只是检查返回值,如果返回0则表示编译失败,这样的话我们也不需要这个着色器对象了,所以告诉OpenGL把刚刚创建的对象删除,并且返回0给调用者。假如编译成功,那么我们得到的就是一个合法的且可以在代码中使用的着色器。
编译着色器到处结束,现在返回着色器id给调用者,代码如下:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
return shaderObjectId;

3.2.6 在渲染器中编译着色器(Compiling the Shaders from Our Renderer Class)

    现在可以使用刚刚编写的着色器编译代码了,切换到AirHockeyRenderer.java 并在onSurfaceCreated()中加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);

    现在让我们复习下这小节里学习了些什么,首先我们创建了一个辅助类ShaderHelper,并在里面加了一个创建、编译着色器对象的方法。同时创建了LoggerConfig,它可以让我们很容易的在一个地方打开或者关闭log。
    假如你回过头来看看ShaderHelper,你会发现我们定义了如下三个方法:

    1)compileShader()
           compileShader(shaderCode)这个方法以shader 代码及shader类型为参数,对于顶点着色器类型是GL_VERTEX_SHADER ,对于片元着色器类型是GL_FRAGMENT_SHADER 。假如OpenGL能够成功编译着色器代码,则会返回着色器对象的id,否则返回0。
    2)compileVertexShader()
            compileVertexShader(shaderCode) 这个方法是一个辅助方法,它仅仅是用参数GL_VERTEX_SHADER调用compileShader()。
    3)compileFragmentShader()
            compileFragmentShader()这个方法也是一个辅助方法,它以参数GL_FRAGMENT_SHADER调用compileShader()方法。
   正如你所见,关键代码在compileShader()中,其它两个方法都是以GL_VERTEX_SHADER或者GL_FRAGMENT_SHADER调用它。

3.3 链接并创建最终OpenGL程序(Linking Shaders Together into an OpenGL Program)

    现在我们已经加载并编译了一个顶点着色器和一个片元着色器,下一步就是把他们编译成一个单独的程序。

3.3.1 理解OpenGL程序(Understanding OpenGL Programs)

    一个OpenGL程序由一个顶点着色器和一个片元着色器组成,并且顶点着色器和片元着色器通常都是一起使用。没有片元着色器,OpenGL将不知道如何组合片元而绘制出点、线、三角形;没有顶点着色器OpenGL不知道在哪里绘制出片元。
    我们知道顶点着色器会计算最终每个顶点在屏幕上的位置,我们也知道何时OpenGL把这些顶点组合成点、线、三角形,然后把他们划分成片元,最后交给片元着色器确定最终每个片元的颜色。顶点着色器和片元着色器相互配合并绘制出屏幕上的最终图像。

    尽管顶点着色器与片元着色器是一起工作的,他们也并不是必须只能与特定的顶点或者片元着色器配合工作,我们可以在多个程序中使用同一个着色器程序。
    打开ShaderHelper,并在最后加入如下代码:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
}

    正如compileShader()一样,我们也将一步一步的实现这个方法,很多代码也会与compileShader()一样。

3.3.2 创建一个新的程序并链接着色器(Creating a New Program Object and Attaching Shaders)

    我们首先调用glCreateProgram()创建一个程序并把返回的程序id保存在programObjectId中,代码如下:

final int programObjectId = glCreateProgram();
if (programObjectId == 0) {
    if (LoggerConfig.ON) {
        Log.w(TAG, "Could not create new program");
    }
    return 0;
}

    这种方法与之前创建着色器对象一致,返回的integer表示对程序对象的引用,如果创建失败了将会返回0.
    下一步是链接着色器,代码如下:

glAttachShader(programObjectId, vertexShaderId);
glAttachShader(programObjectId, fragmentShaderId);

    这里使用glAttachShader()把顶点着色器和片元着色器链接到程序programObjectId中。

 3.3.3 链接OpenGL程序(Linking the Program)

    到这里我们可以把所有的着色器链接在一起了,可以调用glLinkProgram(programObjectId)来达到目的,如下:

glLinkProgram(programObjectId);

    如何检查链接是成功还是失败呢?我们可以像编译着色器一样获取相应的状态,代码如下:

final int[] linkStatus = new int[1];
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);

    首先创建了一个数组来保存结果,然后调用glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0)把链接的结果保存到数组中。我们也会检查程序描述信息,假如出了什么错或者OpenGL有什么要输出的信息,我们都可以通过Android的log输出看到,代码如下:

if (LoggerConfig.ON) {
    // Print the program info log to the Android log output.
    Log.v(TAG, "Results of linking program:\n"
        + glGetProgramInfoLog(programObjectId));
}

3.3.4 检查程序的合法性(Verifying the Link Status and Returning the Program Object ID)

    现在我们可以检查链接状态了,如果返回 0则表示失败了,那我们就不能使用这个程序,所以我们应该删除它并返回0给调用者,代码如下:

if (linkStatus[0] == 0) {
    // If it failed, delete the program object.
    glDeleteProgram(programObjectId);
    if (LoggerConfig.ON) {
        Log.w(TAG, "Linking of program failed.");
    }
    return 0;
}

    假如链接成功,则可以使用这个程序,因此把程序id返回给调用者,代码如下:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
return programObjectId;

3.3.5 修改渲染器类(Adding the Code to Our Renderer Class)

    现在我们可以在渲染器中调用链接着色器并生存程序的代码了,在AirHockeyRenderer中加入如下变量:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
private int program;

    这个integer变量将会保存成功返回程序的id,在onSurfaceCreated()的最后加入如下代码进行着色器代码的链接:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
program = ShaderHelper.linkProgram(vertexShader, fragmentShader);

    现在你可以稍作休息,下一节我们将把所有东西组织起来形成一个OpenGL程序。

3.4 组合所有的代码(Making the Final Connections)

    我们几乎花了前面两章写了一些基本类和实现一些基本功能,我们学习了如何使用一个数组定义物体的结构,如何创建、加载及编译着色器并最后链接成一个OpenGL程序。
    现在是时候把前面所有的东西组合起来并绘制我们第一个版本的球台桌面。

3.4.1 验证OpenGL程序的合法性(Validate Our OpenGL Program Object)

    在我们使用OpenGL程序之前,我们需要验证下它是否合法。根据OpenGL ES 2.0的文档,还提供了方法让我们知道为什么当前程序不合法、无法运行。
    在ShaderHelper中添加如代码:

//AirHockey1/src/com/airhockey/android/util/ShaderHelper.java
public static boolean validateProgram(int programObjectId) {
    glValidateProgram(programObjectId);
    final int[] validateStatus = new int[1];
    glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);
    Log.v(TAG, "Results of validating program: " + validateStatus[0]
        + "\nLog:" + glGetProgramInfoLog(programObjectId));
    return validateStatus[0] != 0;
}

    我们使用glValidateProgram()来验证程序的合法性,然后以GL_VALIDATE_STATUS为参数调用函数glGetProgramiv()获取验证结果。假如OpenGL有什么描述信息那我们可以通过调用glGetProgramInfoLog()来获取并输出到控制台。
    在使用OpenGL程序之前我们都应该验证它的合法性,但是也应该仅仅只是在开发和调试阶段进行,在onSurfaceCreated()的最后加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
if (LoggerConfig.ON) {
    ShaderHelper.validateProgram(program);
}

    这部分将会在log打开的情况下调用我们之前定的验证程序,下一步是使用(enable)OpenGL程序,在onSurfaceCreated()的最后加入代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
glUseProgram(program);

    我们调用glUseProgram()告诉OpenGL使用我们的程序去绘制屏幕。

3.4.2 得到uniform类型变量的引用(Getting the Location of a Uniform)

    接下来就要获取我们定义的着色器中uniform类型变量的引用,当OpenGL把着色器链接为一个程序的时候,它会为我们在顶点着色器中定义的uniform类型的变量关联一个位置引用值,这个值是用来把数据传递给着色器的,这里我们需要u_Color的位置引用,这样我们才能在绘制的时候指定相应的颜色。
    先来看下之前定义的片元着色器:

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

    我们定义了一个uniform类型的变量u_Color,在main函数中我们把这个变量赋值给gl_FragColor。我们就是使用这个变量来设置我们需要绘制的颜色的。我们需要绘制一个球台桌面、一个中心分隔线、两个球,而这些元素都会以不同的颜色进行绘制。
    在AirHockeyRenderer.java的顶点加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
private static final String U_COLOR = "u_Color";
private int uColorLocation;

    我们定义了一个常量表示uniform变量的名字,还有变量uColorLocation用来保存uniform变量在OpenGL程序中的位置引用。在OpenGL程序成功链接后我们就可以获取uniform变量的位置引用。该位置引用变量是相互独立的,即使我们两个程序中有相同名字的uniform类型变量,这也不意味着他们共享相同的位置引用。
    在onSurfaceCreated()的最后加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
uColorLocation = glGetUniformLocation(program, U_COLOR);

    我们使用glGetUniformLocation() 来获取uniform类型变量的位置引用,并把获取的值保存在uColorLocation中,假如后面我们需要更新该uniform变量的值,将会使用uColorLocation来进行操作。

3.4.3 获取attribute类型变量的位置引用(Getting the Location of an Attribute)

    就像uiniform类型变量一样,在使用attribute类型变量之前我们也需要获取它的位置引用。我们可以让OpenGL自动给这些位置变量赋值也可以在链接程序之前调用glBindAttribLocation() 绑定相应的位置变量。后面我们将让OpenGL自动赋值,因为这样我们的代码更容易维护。

    在AirHockeyRenderer的顶部加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
private static final String A_POSITION = "a_Position";
private int aPositionLocation;

    现在我们只需要在OpenGL程序正确链接的时候加入获取attribute类型变量的位置引用,在onSurfaceCreated()的最后加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
aPositionLocation = glGetAttribLocation(program, A_POSITION);

    我们调用glGetAttribLocation()来获得attribute变量的位置引用,有了这个值,我们就可以告诉OpenGL在哪里读取相应的顶点位置数据。

3.4.4 关联顶点数据(Associating an Array of Vertex Data with an Attribute)

    下一步是告诉OpenGL到哪里读取a_Position的数据,在onSurfaceCreated()的最后加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT, false, 0,     
    vertexData);

    在这一章节的开头,我们定义了一个float类型的顶点数组, 这个数组代表了球台桌面的顶点数据。然后我们创建了个native内存vertexData,并把顶点数据copy到了该内存中。
    在告诉OpenGL从vertexData中读取数据之前,我们需要确保从数组的开始处读取数据。每一个缓冲区都有一个指针,可以通过调用position(int)来改变该指针的位置,然后OpenGL就会从指针处开始读取数据。为了确保OpenGL从vertexData的开始处读取数据,我们首先调用了position(0)。然后调用glVertexAttribPointer() 告诉OpenGL哪里可以读取到a_Position需要的顶点坐标。这是一个很重要的函数,现在我们详细看下每个参数的含义:

glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, Buffer ptr)

int index代表属性的位置引用,这里传入之前获得的值aPositionLocation
int size

代表每个属性有几个分量,比如我们前面定义的float类型顶点坐标就包含x分量及y分量,这就表示有两个分量,前面我们创建的常量POSITION_COMPONENT_COUNT就是这个含义;因此这里传入该值。

注意这里每个顶点只传入了两个分量,但是在着色器中的a_Position却是定义为有四个分量的vec4类型,在OpenGL中默认的会把前面三个分量设置为0而最后一个设置为1

int type数据类型;由于这里定义的是float类型的顶点数据,所以这里传入的是GL_FLOAT。
boolean normalized当我们使用的是int数据时才有意义;现在我们可以忽略。
int stride当我们在单个数组中存储多个属性时才有意义;由于我们现在只使用了顶点属性,所以这里只需要简单的传递0即可。
Buffer ptr告诉OpenGL哪里可以读取顶点数据,由于OpenGL
都是从当前位置开始读取数据,所以记得在读取数据之前调用vertexData.position(0)重置当前位置。

    传递不正确的参数到glVertexAttribPointer()中将会导致一些奇怪的行为并可能导致应用崩溃,这种类型的崩溃很难以追踪,因此确保传递正确的参数非常重要。
    当调用glVertexAttribPointer()后,OpenGL就知道哪里可以读取顶点坐标数据并设置相应数据给a_Position变量。

3.4.5 使用顶点数组(Enabling the Vertex Array)

    现在我们已经把属性变量关联了顶点数组,但是在开始绘制之前我们需要调用glEnableVertexAttribArray() 使能该属性。在glVertexAttribPointer()的后面加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
glEnableVertexAttribArray(aPositionLocation);

    现在OpenGL知道到哪里去读取所有的数据了。在这一部分,我们获取了uniform类型变量u_Color与attribute类型变量a_Position的位置引用值,每一个变量都有一个位置引用,OpenGL就是通过位置引用去设置相应变量值的。然后我们调用glVertexAttribPointer()告诉OpenGL它可以在vertextData中读取a_Position所需要的位置数据。

3.5 绘制到屏幕(Drawing to the Screen)

    是时候绘制所有的东西了,所以我们将绘制桌面,然后是分隔线,最后是两个小球。

3.5.1 绘制桌面(Drawing the Table)

    在glClear()的后面加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
glDrawArrays(GL_TRIANGLES, 0, 6);

    首先我们调用glUniform4f()来更新着色器程序中u_Color变量的值,不像attribute类型变量,uniform类型变量是没有默认值的,所以如果一个uniform类型的变量是用vec4来定义的话,我们需要提供所有的四大分量值。因为我们想要绘制白色的桌面,所以这里设置了RGB都为1,alpha并不那么重要,由于颜色有四个分量,所以我们也指定了它。
    指定颜色后,我们调用glDrawArrays(GLES20.GL_TRIANGLES, 0, 6)来绘制桌面,第一个参数告诉OpenGL 绘制三角形。绘制一个三角形我们需要指定三个顶点,第二个参数告诉OpenGL 从顶点数组的开始处读取坐标数据,第三个参数表示要读取6个顶点坐标。因为一个三角形有三个顶点,所以这里将会绘制两个三角形。
    现在看下在这章开始处定义的顶点,如下:

//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,
    // Line 1
    0f, 7f,
    9f, 7f,
    // Mallets
    4.5f, 2f,
    4.5f, 12f
};

    前面我们的调用glVertexAttribPointer(),参数2表示每一个顶点由2个float类型分量组成。glDrawArrays()则表示用前面6个顶点绘制三角形,因此OpenGL将会使用如下的顶点:

// 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)、and (9, 14)。

3.5.2 绘制中心分隔线(Drawing the Dividing Line)

    下一步是在桌面的中央绘制一条分隔线,在onDrawFrame()的最后加入如下代码:

glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2);

    这里把颜色设置给红色,同时告诉OpenGL从第7个点开始绘制,因为一条线有两个点,所以将会用以下顶点进行绘制:

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

3.5.3 绘制点以代替球(Drawing the Mallets as Points)

    最后的剩下的就是绘制两个小球了,这以点代替,在onDrawFrame()的最后加入如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
// Draw the first mallet blue.
glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
glDrawArrays(GL_POINTS, 8, 1);
// Draw the second mallet red.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_POINTS, 9, 1);

    我们通过以参数GL_POINTS调用glDrawArrays()来绘制点。对于第一个顶点,我们把颜色设置蓝色,绘制使用数组中位置为8的顶点坐标;对于第二个点,我们绘制成红色,同时使用数组中位置为9的顶点;所以这里使用的是如下的点进行绘制:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
// Mallets
4.5f, 2f,
4.5f, 12f

    OpenGL将会在(4.5, 2) 处绘制第一个点,同时在(4.5, 12)处绘制第二个点。

3.5.4 运行看结果(What Do We Have So Far?)

    现在是时候运行程序看看绘制出结果了,运行结果如下:

好像哪里不对?为什么我们只看到部分桌面?在回答这个问题之前,我们先把颜色修正下,在 onSurfaceCreated()中找到调用glClearColor()的地方,更新为如下代码:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

    这样的话当我们调用glClear()的时候将会把屏幕绘制成黑色而不是红色,现在是时候看看为什么我们只看到部分桌面了。

3.5.5 OpenGL是怎样将坐标映射到屏幕的?(How Does OpenGL Map Coordinates to the Screen?)

    我们现在还没有解决的一大问题是OpenGL是如何把我们传递给它的坐标映射到实际的物理屏幕坐标的呢?
    关于这个的答案相当复杂,随着我们学习的深入,我们将会学习到更多关于坐标映射的知识。现在我们只需要知道OpenGL会把屏幕上的x和y坐标都分别映射到[-1, 1],这意味着屏幕的左边对应于x轴的-1,屏幕的右边对应于x轴的+1,屏幕的底部对应于y轴的-1,屏幕的顶部对应于y轴的+1;如下示意图:

    不管屏幕的大小或者形状如何,这个规则都一样,并且我们所绘制的所有东西都必须这个范围内,否则将不可见。现在我们更新tableVerticesWithTriangles 中的顶点坐标定义为如下:

//AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java
float[] tableVerticesWithTriangles = {
    // Triangle 1
    -0.5f, -0.5f,
     0.5f, 0.5f,
    -0.5f, 0.5f,

    // Triangle 2
    -0.5f, -0.5f,
     0.5f, -0.5f,
     0.5f, 0.5f,

    // Line 1
    -0.5f, 0f,
     0.5f, 0f,

    // Mallets
       0f, -0.25f,
       0f, 0.25f
};

现在重新就行程序,应该得到如下的结果:


    现在看起来好了很多,但是我们绘制的小球哪里去了?在OpenGL中如果我们想要绘制点,则需要指定点的大小。

3.5.6 指定点的大小(Specifying the Size of Points)

    现在更新我们的着色器告诉OpenGL该绘制多大的点,在simple_vertex_shader.glsl中增加如下代码:

//AirHockey1/res/raw/simple_vertex_shader.glsl
gl_PointSize = 10.0;

    在OpenGL中我们可以通过gl_PointSize变量指定点的大小,这里指定为10,这里的10代表什么呢?当OpenGL把点划分成片元的时候,它会把以gl_Position为中心的正方形划分成片元,而这个正方形的边长就对应于gl_PointSize;这个值越大,那在屏幕绘制出的点就越大。
    现在重新运行程序,应该得到如下图的样子:


3.6 Review

    在显示出桌面之前,我们写了不少繁琐的代码, 值得庆幸的是这些代码在将来的工程都可以得到复用,现在是时候复习下这一章节里面学了些什么:
    1) 如何创建并编译着色器
    2) 还知道顶点着色器与片元着色器通常是结合在一起使用,另外还学习了如何把他们链接成OpenGL程序对象。
    3) 如何把数组里面的顶点数据与着色器里面定义的顶点变量进行关联。

    最后我们把所有的东西组合在一起,并且在屏幕上绘制出了桌面,现在是时候回顾下那些不清楚的知识点了。

3.7 Exercises

1 在桌面的中心画一个小球(点)
2 给桌面添加边框(提示:分别用两种不同的颜色画两个矩形)

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值