锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

OpenGL ES 2.0 for Android教程(三):编译着色器并绘制到屏幕

时间:2023-02-17 00:00:00 线条连接器绿色矩形连接器2方型连接器连接器3040连接器白色

OpenGL ES 2 第三章:编译着色器并绘制屏幕

文章传送门

OpenGL ES 2.0 for Android教程(一)

OpenGL ES 2.0 for Android教程(二)

OpenGL ES 2.0 for Android教程(四)

OpenGL ES 2.0 for Android教程(五)

OpenGL ES 2.0 for Android教程(六)

OpenGL ES 2.0 for Android教程(七)

OpenGL ES 2.0 for Android教程(八)

OpenGL ES 2.0 for Android教程(九)

本章将继续我们在上一章开始的工作。作为本章的游戏计划,我们将首先加载和编译我们定义的着色器,然后链接到一个OpenGL在程序中,我们使用这个着色器程序在屏幕上绘制我们的曲棍球桌。

加载着色器

现在我们已经为着色器编写了代码,下一步是将它们加载到内存中。做到这一点,我们首先需要编写一种从资源文件夹中读取代码的方法。

从raw在资源中加载字符串

创建util包,并创建Utils.kt,输入以下代码:

fun Context.readStringFromRaw(@RawRes resId: Int): String { 
             return runCatching { 
                 val builder = StringBuilder()         val reader = BufferedReader(InputStreamReader(resources.openRawResource(resId)))         var nextLine: String? = reader.readLine()         while (nextLine != null) { 
                     builder.append(nextLine).append("\n")             nextLine = reader.readLine()         }         reader.close()         builder.toString()     }.onFailure { 
                 when(it) { 
                     is IOException -> { 
                         throw RuntimeException("Could not open resource: $resId", it)
            }
            is Resources.NotFoundException -> { 
        
                throw RuntimeException("Resource not found: $resId", it)
            }
            else -> { 
        }
        }
    }.getOrThrow()
}

读取着色器代码

我们现在将添加调用,以实际读取着色器代码。切换到AirHockeyRenderer。在onSurfaceCreated()中调用glClearColor()后添加以下代码:

val vertexShaderCode = context.readStringFromRaw(R.raw.simple_vertex_shader)
val fragmentShaderCode = context.readStringFromRaw(R.raw.simple_fragment_shader)

同时在AirHockeyRenderer构造函数中添加context的传入。

class AirHockeyRenderer(private val context: Context): GLSurfaceView.Renderer { 
        
    // ...
}

日志辅助

我们可能需要观察日志来辅助我们观察OpenGL ES的运行流程,以下给出一个简单的日志类:

import android.util.Log
object LogU { 
        
    const val TAG = "LogU"
    var on = true

    fun v(tag: String = TAG, throwable: Throwable): LogU { 
        
        if (on) { 
        
            Log.v(tag, Log.getStackTraceString(throwable))
        }
        return this
    }

    fun v(tag: String = TAG, message: Any?): LogU { 
        
        if (on) { 
        
            Log.v(tag, message.toString())
        }
        return this
    }

    fun d(tag: String = TAG, throwable: Throwable): LogU { 
        
        if (on) { 
        
            Log.d(tag, Log.getStackTraceString(throwable))
        }
        return this
    }

    fun d(tag: String = TAG, message: Any?): LogU { 
        
        if (on) { 
        
            Log.d(tag, message.toString())
        }
        return this
    }

    fun i(tag: String = TAG, throwable: Throwable): LogU { 
        
        if (on) { 
        
            Log.i(tag, Log.getStackTraceString(throwable))
        }
        return this
    }

    fun i(tag: String = TAG, message: Any?): LogU { 
        
        if (on) { 
        
            Log.i(tag, message.toString())
        }
        return this
    }

    fun w(tag: String = TAG, throwable: Throwable):LogU { 
        
        if (on) { 
        
            Log.w(tag, Log.getStackTraceString(throwable))
        }
        return this
    }

    fun w(tag: String = TAG, message: Any?):LogU { 
        
        if (on) { 
        
            Log.w(tag, message.toString())
        }
        return this
    }

    fun e(tag: String = TAG, throwable: Throwable): LogU { 
        
        if (on) { 
        
            Log.e(tag, Log.getStackTraceString(throwable))
        }
        return this
    }

    fun e(tag: String = TAG, message: Any?): LogU { 
        
        if (on) { 
        
            Log.e(tag, message.toString())
        }
        return this
    }

    fun set(b: Boolean): LogU { 
        
        on = b
        return this
    }
}

编译着色器

现在我们已经从文件中读取了着色器源代码,下一步是编译每个着色器。我们将创建一个新的Helper类,该类将创建一个新的OpenGL着色器对象,编译着色器代码,并返回该着色器代码的着色器对象。一旦我们有了这个样板代码,我们将能够在未来的项目中重用它。首先,创建一个新类ShaderHelper,并在该类中添加以下代码:

object ShaderHelper { 
        
    private const val tag = "ShaderHelper"
    fun compileVertexShader(shaderCode: String) = compileShader(GL_VERTEX_SHADER, shaderCode)

    fun compileFragmentShader(shaderCode: String) = compileShader(GL_FRAGMENT_SHADER, shaderCode)

    fun compileShader(type: Int, shaderCode: String): Int { 
        
        
    }
}

GL_VERTEX_SHADER等常量位于android.opengl.GLES20包内。

我们将使用它作为着色器辅助对象的基础。在下一节中,我们将逐步构建compileShader()

创建新的着色器对象

我们应该做的第一件事是创建一个新的着色器对象,并检查创建是否成功。将以下代码添加到compileShader()

fun compileShader(type: ShaderType, code: String): Int { 
        
    val shaderObjectId = glCreateShader(type)
    if (type == 0) { 
        
        LogU.w(tag = tag, message = "Could not create new shader")
        return 0
    }
}

我们通过调用GLES20中的静态方法glCreateShader()创建一个新的着色器对象,并将该对象的ID存储在shaderObjectId中。

如何创建对象并检查它是否有效,这种模板代码在OpenGL中随处可见:

  1. 我们首先使用glCreateShader()之类的调用创建一个对象。此调用将返回一个整数。
  2. 这个整数是对OpenGL对象的引用。将来每当我们想要引用这个对象时,我们都需要将相同的整数传递回OpenGL。
  3. 返回值0表示对象创建失败,类似于Java代码中的返回值null

如果对象创建失败,我们将向调用代码返回0。为什么我们要返回0而不是抛出异常?OpenGL实际上不会在内部抛出任何异常。而是以返回值为0的方式告诉我们发生了错误,可以通过调用glGetError()询问OpenGL是否有任何API调用导致了错误。我们打算与OpenGL的行为保持一致。

上传和编译着色器源代码

让我们添加以下代码,将着色器源代码上传到着色器对象中:

glShaderSource(shaderObjectId, shaderCode)

创建了一个有效的着色器对象之后,我们调用glShaderSource(shaderObjectId,shaderCode)来上传源代码。此方法告诉OpenGL读入字符串shaderCode中定义的源代码,并将其与shaderObjectId引用的着色器对象相关联。然后,我们可以调用glCompileShader(shaderObjectId)来编译着色器。

glCompileShader(shaderObjectId);

这会告诉OpenGL编译之前上传到shaderObjectId的源代码。

检索编译状态

让我们添加以下代码来检查OpenGL着色器是否编译成功:

val compileStatus: IntArray = IntArray(1)
glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0)

为了检查编译成功或失败,我们首先使用Kotlin创建一个在Java层看来长度为1的新int数组,并将其称为compileStatus。然后我们调用glGetShaderiv(shaderObjectId,GLES20.GL_COMPILE_STATUS,compileStatus,0)。这会告诉OpenGL读取与shaderObjectId关联的编译状态,并将其写入compileStatus的第0个元素。

这是Android上OpenGL的另一种常见模式。为了检索值,我们通常使用长度为1的数组,并将该数组传递到OpenGL调用中。在同一个调用中,我们告诉OpenGL将结果存储在数组的第一个元素中。

检索着色器信息日志

当我们获取编译状态时,OpenGL会给我们一个简单的成功或失败的答案,但无法得知哪里出了问题。我们可以通过调用glGetShaderInfoLog(shaderObjectId)获得一条可读的消息。如果OpenGL对我们的着色器有什么有趣的说法,它会将消息存储在着色器的信息日志中。

让我们添加以下代码以获取着色器信息日志:

LogU.v(tag = tag, message ="Results of compiling source:" +
                "\n$shaderCode\n:${ 
          glGetShaderInfoLog(shaderObjectId)}")

验证编译状态并返回着色器对象ID

现在我们已经记录了着色器信息日志,我们可以检查编译是否成功。

我们需要做的就是检查在检索编译状态的步骤中返回的值是否为0。如果为0,则编译失败。在这种情况下,我们不再需要着色器对象,所以我们告诉OpenGL删除它,并返回0。如果编译成功,那么我们的着色器对象是有效的,我们可以在代码中使用它,因此可以返回新的着色器对象ID:

if (compileStatus[0] == 0) { 
        
    // 编译失败,删除着色器
    glDeleteShader(shaderObjectId)
    LogU.w(tag, "Compilation of shader failed.")
    return 0
}
return shaderObjectId

从ShaderHelper编译着色器

现在是充分利用我们刚刚创建的代码的时候了。切换到AirHockeyRenderer.kt并将以下代码添加到onSurfaceCreated()

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { 
        
    // 设置ClearColor
    glClearColor(0F, 0F, 0F, 0F)
    val vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode)
    val fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode)
}

让我们回顾一下我们在前面中所做的工作。首先,我们创建了一个新类ShaderHelper,并添加了一个方法来创建和编译一个新的着色器对象。我们还创建了LogU,这是一个帮助我们打印日志的类。

compileShader()现在看起来就像这样:

object ShaderHelper { 
        
    fun compileShader(type: Int, shaderCode: String): Int { 
        
        // 创建着色器
        val shaderObjectId = glCreateShader(type)
        if (type == 0) { 
        
            LogU.d(tag = tag, message = "Could not create new shader")
            return 0
        }
        // 传输并编译代码
        glShaderSource(shaderObjectId, shaderCode)
        glCompileShader(shaderObjectId)

        // 获得代码编译结果
        val compileStatus = IntArray(1)
        glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0)
        LogU.v(tag = tag, message ="Results of compiling source:" +
                "\n$shaderCode\n:${ 
          glGetShaderInfoLog(shaderObjectId)}")
        if (compileStatus[0] == 0) { 
        
            // 编译失败,删除着色器
            glDeleteShader(shaderObjectId)
            LogU.w(tag, "Compilation of shader failed.")
            return 0
        }
        return shaderObjectId
    }
}

将着色器链接到OpenGL程序中

现在我们已经加载并编译了顶点着色器和片元着色器,下一步是将它们绑定到单个程序中(bind them together into a single program)。

理解OpenGL程序

可以将一个顶点着色器和一个片元着色器简单链接到一个对象中而构成一个OpenGL程序,但顶点着色器和片元着色器缺一不可。如果没有片元着色器,OpenGL将不知道如何绘制组成每个点、线和三角形的片元;如果没有顶点着色器,OpenGL就不知道在哪里绘制这些片元。

我们知道顶点着色器计算屏幕上每个顶点的最终位置。我们还知道,当OpenGL将这些顶点分组(groups)为点、线和三角形并将它们分解(breaks)为片元时,它会向片元着色器询问每个片元的最终颜色。顶点着色器和片元着色器协同工作,在屏幕上生成最终图像。

尽管顶点着色器和片元着色器总是一起使用,但它们不一定要保持单一配对(monogamous):我们可以在多个程序中使用同一个着色器。

让我们打开ShaderHelper,并在类的末尾添加以下代码:

fun linkProgram(vertexShaderId: Int, fragmentShaderId: Int) { 
        
        
}

正如我们对compileShader()所做的那样,我们将逐步构建这个方法。大部分代码在概念上与compileShader()类似。

创建新程序对象并附加着色器

我们要做的第一件事是通过调用glCreateProgram()创建一个新的程序对象,并将该对象的ID存储在programObjectId中。让我们添加以下代码:

val programObjectId = glCreateProgram()
if (programObjectId == 0) { 
        
    LogU.w(tag, "Could not create new program")
    return 0  
}

语义与我们之前创建新着色器对象时相同:返回的整数是我们对程序对象的引用,如果对象创建失败,我们将得到0的返回值。

下一步是附加(attach)着色器,我们继续在glCreateProgram()后面添加代码:

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

使用glAttachShader(),我们把顶点着色器和片元着色器附加到程序对象。

链接程序

我们现在准备好加入我们的着色器,我们将通过调用glLinkProgram(programObjectId)来实现这一点。

要检查链接是否失败或成功,我们将按照编译着色器时的相同步骤进行操作。我们首先创建一个新的int数组来保存结果。然后我们调用glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0)将结果存储在这个数组中。我们还将检查程序信息日志,出现问题时,或者OpenGL对我们的程序有什么有趣的说法,都能在Android的日志输出中看到。

glLinkProgram(programObjectId)
val linkStatus = IntArray(1)
glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0)
LogU.v(tag, "Results of linking program:\n${ 
          glGetProgramInfoLog(programObjectId)}")

验证链接状态并返回程序对象ID

我们现在需要检查链接状态:如果是0,这意味着链接失败,我们不能使用这个程序对象,所以我们应该删除它并将0返回给调用代码。如果链接成功,直接返回程序对象id:

if (linkStatus[0] == 0) { 
        
    glDeleteProgram(programObjectId)
    LogU.w(tag, "Linking of program failed.")
    return 0
}
return programObjectId

将代码添加到我们的Renderer类中

链接之前得到的两个着色器,我们需要添加一个类变量来缓存程序id:

class AirHockeyRenderer{ 
              
    private var programId = 0

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { 
        
        // ...
        programId = ShaderHelper.linkProgram(vertexShader, fragmentShader)
    }
}

在下一节中,我们将开始进行最终连接,并将数据链接到OpenGL。

最后的连接

我们在上两章的大部分时间里为我们的应用奠定了一个基本的基础:我们学会了如何使用属性数组定义一个对象的结构,并且我们还学会了如何创建着色器、加载和编译它们,并将它们链接到一个OpenGL程序中。

现在是时候在这个基础上建立最后的连接了。在接下来的几个步骤中,我们将把这些代码组织在一起,然后准备在屏幕上绘制我们的空气曲棍球桌的第一个版本。

验证我们的OpenGL程序

在我们开始使用OpenGL程序之前,我们应该先验证它,看看该程序在当前OpenGL状态下是否有效。根据OpenGL ES 2.0文档,它为OpenGL提供了一种方法,让我们知道为什么当前程序可能效率低下、无法运行等等。

让我们向ShaderHelper添加以下方法:

fun validateProgram(programObjectId: Int): Boolean { 
        
    glValidateProgram(programObjectId)
    val validateStatus = IntArray(1)
    glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0)
    LogU.v(tag, "Results of validating program: " + validateStatus[0]
            + "\nLog:" + glGetProgramInfoLog(programObjectId))
    return validateStatus[0] != 0
}

我们调用glValidateProgram()来验证程序,传入GL_VALIDATE_STATUS 作为参数,通过调用glGetProgramiv()来检查结果。我们还通过调用glGetProgramInfoLog()打印日志,如果OpenGL有什么有趣的话要说,它将出现在程序日志中。

我们应该在开始使用程序之前对其进行验证,也只应该在开发和调试应用程序时进行验证。因此,我们在Utils.kt添加一个判断是否为debug版本的方法,然后将一些代码添加到onSurfaceCreated()的末尾:

// Utils.kt
/** * 判断是否为debug版本 */
private fun Context.isDebugVersion(): Boolean =
    runCatching { 
        
        (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
    }.getOrDefault(false)

// 追加在onSurfaceCreated()的代码
if (context.isDebugVersion()) { 
        
    ShaderHelper.validateProgram(programId)
}

接下来我们应该做的是启用我们一直努力创建的OpenGL程序。将以下内容添加到onSurfaceCreated()的末尾:

glUseProgram(programId)

我们调用glUseProgram()告诉OpenGL,在屏幕上绘制某些东西时应当使用这里定义的程序。

获取Uniform的位置

下一步是获取我们之前在着色器中定义的uniform变量的位置。当OpenGL将我们的着色器链接到一个程序中时,它实际上会将顶点着色器中定义的每个uniform与一个位置编号相关联(it will actually associate each uniform defined in the vertex shader with a location number. )。这些位置编号用于将数据发送到着色器,我们需要u_Color 的位置,以便在绘制时设置颜色。

让我们快速回顾一下片元着色器的代码:

precision mediump float;

uniform vec4 u_Color;

void main() {
    gl_FragColor = u_Color;
}

在我们的着色器中,我们定义了一个名为u_Coloruniform变量,并在main()中将该uniform变量指定给gl_FragColor。我们将用这个uniform变量来设定我们所画的东西的颜色。我们要用不同的颜色来画一张桌子,一条中心分界线,两个木槌。

让我们在AirHockeyRenderer添加一些代码:

// 未列出完整代码
class AirHockeyRenderer(): GLSurfaceView.Renderer { 
        
    // ...
    /** * 缓存u_Color变量的位置 */
    private var uColorLocation = 0
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { 
        
        // ...
        uColorLocation = glGetUniformLocation(programId, U_COLOR) // 获取位置
    }    
    companion object { 
        
        private const val POSITION_COMPONENT_COUNT = 2
        private const val BYTES_PER_FLOAT = 4
        private const val U_COLOR = "u_Color"
    }
}

首先,我们需要为uniform变量的名称创建了一个常量,并创建一个变量来保存其在OpenGL程序对象中的位置。uniform变量的位置无法事先指定,所以程序成功链接后,我们才需要去查询位置。uniform的位置对于程序对象唯一:即使我们在两个不同的程序中包含相同的uniform名称,它们也不会共享相同的位置。

然后我们调用glGetUniformLocation()来获取uniform的位置,并将该位置存储在uColorLocation中,以便于使用它来更新uniform的值。

获取属性的位置

uniform一样,我们也需要在使用属性(attribute)之前获得属性的位置。我们可以让OpenGL自动给这些属性分配位置编号,或者在将着色器链接到一起之前,我们可以通过调用glBindAttribLocation()自行分配这些编号。目前我们让OpenGL自动分配属性位置,因为它使我们的代码更易于管理。

现在,我们只需要添加一些代码,以便在着色器链接在一起后获得属性位置。添加的代码的处理逻辑与uniform变量类似,我们调用glGetAttriblLocation()来获取属性的位置。有了这个位置,我们就可以告诉OpenGL在哪里找到这个属性的数据。

// 未列出完整代码
class AirHockeyRenderer(): GLSurfaceView.Renderer { 
        
    // ...
    /** * 缓存a_Position的位置 */
    private var aPositionLocation = 0
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { 
        
        // ...
        aPositionLocation = glGetAttribLocation(programId, A_POSITION)
    }    
    companion object { 
        
        private const val POSITION_COMPONENT_COUNT = 2
        private const val BYTES_PER_FLOAT = 4
        private const val U_COLOR  

相关文章