簡(jiǎn)述
最近一段時(shí)間由于項(xiàng)目上用到了濾鏡功能,所以一直都在學(xué)習(xí)opengles的相關(guān)知識(shí)宾巍。opengles是opengl的一個(gè)子集倒堕,是opengl針對(duì)移動(dòng)端的版本闽巩。Android手機(jī)上現(xiàn)在最多的版本應(yīng)該是opengles3.0+了框冀,不過(guò)通過(guò)前段時(shí)間的學(xué)習(xí)流椒,發(fā)現(xiàn)網(wǎng)上的教程大多數(shù)還是2.0的版本,3.0的資料不僅少明也,而且大多數(shù)只是opengles3.0的基礎(chǔ)功能宣虾。出于這個(gè)原因,嘗試寫幾篇基于OpenGLES3.0的系列博客温数。最終目標(biāo)是從零到可以自己編寫實(shí)現(xiàn)一個(gè)視頻濾鏡應(yīng)用绣硝。前半部分介紹基本的OpenGLES3.0 的基礎(chǔ)知識(shí),后半部分介紹基于OpenGL的視頻濾鏡制作撑刺。本系列博客側(cè)重用代碼說(shuō)話鹉胖,設(shè)計(jì)到的原理部分可能寫的比較粗淺,畢竟水平有限??
在這推薦兩本相關(guān)書籍:
《OPENGL ES 3.0編程指南》
《OpenGLES應(yīng)用開發(fā)實(shí)踐指南Android卷》
第一本以概念講解為主,第二本以實(shí)際應(yīng)用為主甫菠,不過(guò)第二本使用的2.0版本败许。
Hello Word
大家都知道每一門編程語(yǔ)言都有一個(gè)Hello Word,說(shuō)白了就是在控制臺(tái)輸出一個(gè)字符串淑蔚。那么OpenGL(下面OpenGL 代指OpenGLES3.0)的Hello Word應(yīng)該是什么呢。其實(shí)在OpenGL的世界里邊是沒有控制臺(tái)的愕撰,可以輸出可觀察信息的只有繪制表面也刹衫,并且OpenGL不能輸出字符,只能輸出三種基本圖元:點(diǎn)搞挣、線带迟、三角形,OpenGL繪制出來(lái)的所有東西都是由這三種基本元素組成囱桨,和Android的canvas api有些類似只不過(guò)canvas支持的圖元更多一些仓犬。對(duì)應(yīng)編程語(yǔ)言的Hello Word,咱們就繪制一個(gè)最簡(jiǎn)單的點(diǎn)舍肠。
想在Android系統(tǒng)里邊使用OpenGL首先需要一個(gè)繪制表面搀继,也就是Android里邊的一個(gè)View來(lái)承載OpenGL繪制出來(lái)的界面。Android里邊有一種專門用來(lái)處理OpenGL的View ——GLSurfaceView〈溆铮現(xiàn)在咱們就用GLSurfaceView+OpenGL來(lái)繪制一個(gè)點(diǎn)叽躯。
簡(jiǎn)單介紹一下,GLSurfaceView繼承于SurfaceView肌括,不同的是GLSurfaceView會(huì)幫你初始化一個(gè)OpenGLES的環(huán)境点骑,所以GLSurfaceView能辦到的事SurfaceView其實(shí)也能辦到的,只不過(guò)需要自己額外的初始化一個(gè)OpenGl的環(huán)境谍夭。
OK黑滴,直接上代碼
class MainActivity : AppCompatActivity() {
lateinit var rootLayout: RelativeLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rootLayout = findViewById(R.id.root)
val activityManager =getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val configurationInfo = activityManager.deviceConfigurationInfo
val supportsEs3 = configurationInfo.reqGlEsVersion >= 0x30000
val layoutParams: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
val glSurfaceView = GLSurfaceView(this)
if (supportsEs3) {
glSurfaceView.setEGLContextClientVersion(3)
}
rootLayout.addView(glSurfaceView, layoutParams)
glSurfaceView.setRenderer(PointRenderer(this))
}
}
class PointRenderer(var context: Context) : GLSurfaceView.Renderer {
var pointProgram = -1
var vertexBuffer: FloatBuffer
var avPosition = -1
private val POSITION_VERTEX = floatArrayOf(
0.0f, 0.0f, 0.0f
)
init {
vertexBuffer = ByteBuffer.allocateDirect(POSITION_VERTEX.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(POSITION_VERTEX)
vertexBuffer.position(0)
}
override fun onDrawFrame(gl: GL10?) {
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
GLES30.glUseProgram(pointProgram)
GLES30.glEnableVertexAttribArray(avPosition)
GLES30.glVertexAttribPointer(avPosition, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)
GLES30.glDrawArrays(GLES30.GL_POINTS, 0, 1)
GLES30.glDisableVertexAttribArray(avPosition)
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES30.glViewport(0, 0, width, height)
}
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
pointProgram = ShaderUtil.loadProgramFromAssets(
"vertex_point.glsl",
"frag_point.glsl",
context.resources
)
avPosition = GLES30.glGetAttribLocation(pointProgram, "av_Position")
}
}
object ShaderUtil {
fun loadShader(
shaderType: Int,
source: String?
): Int {
var shader = GLES30.glCreateShader(shaderType)
if (shader != 0) {
GLES30.glShaderSource(shader, source)
GLES30.glCompileShader(shader)
checkGLError("glCompileShader")
val compiled = IntArray(1)
GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0)
if (compiled[0] == 0) {
Log.e("ES30_ERROR", "Could not compile shader $shaderType:")
Log.e("ES30_ERROR", "ERROR: " + GLES30.glGetShaderInfoLog(shader))
GLES30.glDeleteShader(shader)
shader = 0
}
} else {
Log.e(
"ES30_ERROR", "Could not Create shader " + shaderType + ":" +
"Error:" + shader
)
}
return shader
}
fun loadProgramFromAssets(
VShaderName: String?,
FShaderName: String?,
resources: Resources
): Int {
val vertexText = loadFromAssetsFile(VShaderName, resources)
val fragmentText = loadFromAssetsFile(FShaderName, resources)
return createProgram(vertexText, fragmentText)
}
fun checkGLError(op: String) {
var error: Int
while (GLES30.glGetError().also { error = it } != GLES30.GL_NO_ERROR) {
Log.e("ES30_ERROR", "$op: glError $error")
throw RuntimeException("$op: glError $error")
}
}
fun createProgram(
vertexSource: String?,
fragmentSource: String?
): Int {
val vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource)
if (vertexShader == 0) {
return 0
}
val fragShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource)
if (fragShader == 0) {
return 0
}
var program = GLES30.glCreateProgram()
if (program != 0) {
GLES30.glAttachShader(program, vertexShader)
checkGLError("glAttachShader")
GLES30.glAttachShader(program, fragShader)
checkGLError("glAttachShader")
GLES30.glLinkProgram(program)
val linkStatus = IntArray(1)
GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0)
if (linkStatus[0] != GLES30.GL_TRUE) {
Log.e("ES30_ERROR", "ERROR: " + GLES30.glGetProgramInfoLog(program))
GLES30.glDeleteProgram(program)
program = 0
}
} else {
Log.e("ES30_ERROR", "glCreateProgram Failed: $program")
}
return program
}
fun loadFromAssetsFile(
fileName: String?,
resources: Resources
): String? {
var result: String? = null
try {
val inputStream = resources.assets.open(fileName!!)
var ch = 0
val baos = ByteArrayOutputStream()
while (inputStream.read().also { ch = it } != -1) {
baos.write(ch)
}
val buffer = baos.toByteArray()
baos.close()
inputStream.close()
result = String(buffer)
result = result.replace("\\r\\n".toRegex(), "\n")
} catch (e: Exception) {
e.printStackTrace()
}
return result
}
}
然后是著色器的代碼,頂點(diǎn)著色器:
#version 300 es
layout (location = 0) in vec4 av_Position;
void main() {
gl_Position = av_Position;
gl_PointSize = 10.0;
}
片段著色器
#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor =vec4(1.0,0.0,0.0,1.0);
}
這兩個(gè)著色器分別對(duì)應(yīng)著Render里邊加載的 "vertex_point.glsl","frag_point.glsl",這兩個(gè)資源文件紧索。好了現(xiàn)在就可以連上手機(jī)RUN一下了袁辈。效果圖如下:
這就是咱們的第一個(gè)opengles3.0(后續(xù)簡(jiǎn)稱gl)demo,內(nèi)容則是在gl3的坐標(biāo)體系的(0齐板,0吵瞻,0)處繪制一個(gè)POINT,顏色為紅色甘磨。對(duì)于Android開發(fā)的同學(xué)來(lái)說(shuō)這里邊存在一個(gè)和平時(shí)開發(fā)習(xí)慣不太一樣的地方橡羞,首先gl的坐標(biāo)默認(rèn)都是三維坐標(biāo),也就是xyz坐標(biāo)济舆,并且坐標(biāo)原點(diǎn)也不太一樣卿泽,Android的坐標(biāo)原點(diǎn)是坐上角,但是gl的坐標(biāo)體系的坐標(biāo)原點(diǎn)是屏幕中點(diǎn),并且gl系統(tǒng)中不止一套坐標(biāo)系签夭。后面的章節(jié)會(huì)給大家詳細(xì)的講解gl的坐標(biāo)系統(tǒng)齐邦。
hello world 跑完之后,咱們重新看代碼第租,梳理一遍gl的使用流程:
- MainActivity中開啟了opengles3.0的功能措拇,并且生成了一個(gè)glSurfaceView。
- 給glSurfaceView設(shè)置一個(gè)實(shí)現(xiàn)了GLSurfaceView.Renderer接口的Render對(duì)象慎宾。
- 在Render的onSurfaceCreated方法也就是gl環(huán)境創(chuàng)建完成時(shí)的回調(diào)中編譯gl的著色器程序丐吓。
- 在onSurfaceChanged回調(diào)中設(shè)置gl繪制區(qū)域在Android屏幕上的區(qū)域大小。
- 在onDrawFrame中繪制每一幀需要閑置的圖像趟据。
下邊著重介紹一下gl著色器的編譯和繪制過(guò)程券犁。
編譯著色器
那么著色器是個(gè)什么東西呢,大部分資料都會(huì)告訴你“著色器是用來(lái)實(shí)現(xiàn)圖像渲染的用來(lái)替代固定渲染管線的可編輯程序”汹碱。
WTF! 我第一次讀的時(shí)候斷句我都斷不清楚粘衬。本質(zhì)上著色器是個(gè)“可編輯程序”,作用是“用來(lái)實(shí)現(xiàn)圖像渲染”并且“用來(lái)替代固定渲染管線”咳促。好了斷句斷清楚了稚新,但是并沒有影響我看不懂這堆鬼玩意。有同感的同學(xué)請(qǐng)扣個(gè)1跪腹。
毫不夸張枷莉,以上就是我第一次接觸著色器時(shí)候內(nèi)心想法。經(jīng)過(guò)長(zhǎng)時(shí)間接觸著色器尺迂,現(xiàn)在大致上有了一些自己的理解笤妙,其實(shí)看不懂上面的話很正常,因?yàn)槟歉揪筒皇敲嫦駻ndroid工程師的解釋噪裕,更像是給一些有過(guò)計(jì)算機(jī)圖形學(xué)經(jīng)驗(yàn)的人看的蹲盘。經(jīng)過(guò)我自己的摸索,從一個(gè)Android工程師的角度來(lái)看這個(gè)著色器更像是兩個(gè)回調(diào)函數(shù)膳音,頂點(diǎn)著色器是gl在確認(rèn)繪制圖像的邊緣頂點(diǎn)位置時(shí)的一個(gè)回調(diào)函數(shù)召衔,片段著色器則可以看成gl再確認(rèn)每個(gè)像素顏色時(shí)的一個(gè)回調(diào)函數(shù)(這么說(shuō)并不準(zhǔn)確,因?yàn)間l內(nèi)部會(huì)做優(yōu)化祭陷,并不會(huì)保證每個(gè)像素都會(huì)有回調(diào)苍凛,更準(zhǔn)確的說(shuō)法是確認(rèn)每個(gè)“片段”顏色時(shí)候的回調(diào)函數(shù),所以叫片段著色器)兵志。
就像上邊demo代碼醇蝴,咱們一共繪制了一個(gè)點(diǎn),gl知道咱們只有一個(gè)頂點(diǎn)想罕,要確認(rèn)這個(gè)頂點(diǎn)位置的時(shí)候就會(huì)執(zhí)行頂點(diǎn)著色器的代碼:
#version 300 es
layout (location = 0) in vec4 av_Position;
void main() {
gl_Position = av_Position;
gl_PointSize = 10.0;
}
gl_Position是頂點(diǎn)著色器的內(nèi)置變量悠栓,av_Position是從Android環(huán)境中傳下來(lái)的參數(shù),這個(gè)頂點(diǎn)著色器其實(shí)就是把Android傳下來(lái)的位置坐標(biāo)參數(shù),賦值給了gl_Position變量惭适,從而gl知道了咱們要繪制的點(diǎn)的位置笙瑟。
確認(rèn)位置后gl還需要知道點(diǎn)的顏色,這個(gè)時(shí)候就會(huì)執(zhí)行片段著色器
#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor =vec4(1.0,0.0,0.0,1.0);
}
因?yàn)樵蹅兝L制的是一個(gè)點(diǎn)癞志,可以看成片段著色器只調(diào)用了一次往枷。fragColor則是描述該片段的輸出顏色(與定點(diǎn)著色器不同,fragColor并不是內(nèi)置變量凄杯,而是咱們自己聲明的)师溅。片段著色器中并沒有Android環(huán)境傳下來(lái)的參數(shù),而是直接將一個(gè)固定顏色紅色賦值給了fragColor變量盾舌,所以咱們會(huì)繪制出來(lái)一個(gè)紅色的點(diǎn)。
以上就是著色器的大致作用蘸鲸,后續(xù)章節(jié)還會(huì)更深入的去講著色器相關(guān)知識(shí)的妖谴,現(xiàn)在只需要對(duì)著色器有一個(gè)大致的了解就行了。
編譯著色器的流程也不復(fù)雜酌摇,主要流程就是
- glCreateShader gl生成一個(gè)空的著色器程序膝舅,需要指定類型(頂點(diǎn)著色器,片段著色器)并返回著色器索引
- glShaderSource gl載入著色器源碼窑多,需要輸入1中生成的著色器索引仍稀,以及著色器源碼
- glCompileShader gl編譯著色器,需要輸入1中生成的著色器索引
- glGetShaderiv gl檢查編譯結(jié)果(不會(huì)影響編譯結(jié)果埂息,一般用來(lái)輔助查錯(cuò))
- 重復(fù)上述1—4步驟技潘,生成另外一個(gè)著色器
- 現(xiàn)在已經(jīng)有了兩個(gè)著色器程序,需要做的就是連接兩個(gè)著色器了
- glCreateProgram 創(chuàng)建gl程序(也就是兩個(gè)著色器鏈接成功之后的完整的gl程序)千康,會(huì)返回程序索引
- glAttachShader 給7創(chuàng)建的程序添加著色器程序享幽,需要調(diào)用兩次 分別添加頂點(diǎn)著色器和片段著色器
- glLinkProgram 鏈接兩個(gè)著色器
- glGetProgramiv gl檢查鏈接結(jié)果,類似第4步
以上就是編譯著色器的整個(gè)流程拾弃,編譯完成之后我們會(huì)持有一個(gè)gl程序(Program)的索引值桩。
OPENGLES3.0的繪制
gl的繪制過(guò)程是在Render的onDrawFrame回調(diào)里邊處理的,沒回調(diào)一次代表著屏幕要刷新一幀畫面豪椿。這個(gè)直接上代碼
override fun onDrawFrame(gl: GL10?) {
//用預(yù)制的值來(lái)清空緩沖區(qū)
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
//啟用pointProgram gl程序
GLES30.glUseProgram(pointProgram)
//啟用頂點(diǎn)屬性 avPosition
GLES30.glEnableVertexAttribArray(avPosition)
//用頂點(diǎn)數(shù)組給avPosition
GLES30.glVertexAttribPointer(avPosition , 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)
//繪制圖元
GLES30.glDrawArrays(GLES30.GL_POINTS, 0, 1)
//關(guān)閉頂點(diǎn)屬性 avPosition
GLES30.glDisableVertexAttribArray(avPosition)
}
最核心的流程其實(shí)就是給gl傳遞參數(shù)奔坟,然后進(jìn)行繪制。
一個(gè)gl環(huán)境中是可以有多個(gè)gl程序的搭盾,然后使用glUseProgram 來(lái)確定當(dāng)前使用的gl程序咳秉,當(dāng)gl環(huán)境中只會(huì)使用一個(gè)gl程序時(shí),可以在onSurfaceCreated中調(diào)用一次就可以了鸯隅。
詳細(xì)說(shuō)一下傳遞參數(shù)這一塊滴某。
#version 300 es
layout (location = 0) in vec4 av_Position;
void main() {
gl_Position = av_Position;
gl_PointSize = 10.0;
}
Android環(huán)境中的int值avPosition就是頂點(diǎn)著色器av_Position變量在Android中的索引值。
GLES30.glVertexAttribPointer(avPosition , 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)
這行代碼就是給av_Position賦值的代碼。但是現(xiàn)在咱們只有一個(gè)頂點(diǎn)霎奢,也就是頂點(diǎn)著色器只會(huì)調(diào)用一次户誓。很明顯av_Position的值只有一個(gè)。假如咱們要繪制兩個(gè)頂點(diǎn)(0幕侠,0帝美,0)(0,1晤硕,0)這個(gè)時(shí)候頂點(diǎn)著色器就會(huì)調(diào)用兩次悼潭。這個(gè)時(shí)候你可能會(huì)考慮一下Android在給gl的頂點(diǎn)著色器傳遞坐標(biāo)參數(shù)的時(shí)候怎么在兩次調(diào)用著色器的時(shí)候把兩個(gè)坐標(biāo)分別傳入呢?glVertexAttribPointer api中并不會(huì)設(shè)置你的值是傳給第幾次調(diào)用的頂點(diǎn)著色器的舞箍。連著調(diào)用兩次嗎舰褪?并不是,如果是這樣那我有100個(gè)頂點(diǎn)豈不是要調(diào)用100 次api疏橄,那就太麻煩了(其實(shí)OPENGL完整版的確是有類似的api的OPENGLES作為閹割版舍去了這些低效的api)占拍。這個(gè)時(shí)候就需要了解gl中頂點(diǎn)數(shù)組的概念了,正是使用了頂點(diǎn)數(shù)組捎迫,才可以讓我們一次性的在多次頂點(diǎn)著色器的調(diào)用中晃酒,準(zhǔn)確的把不同的頂點(diǎn)參數(shù)值傳遞到每次調(diào)用的頂點(diǎn)著色器(其實(shí)頂點(diǎn)著色器就那一個(gè),只不過(guò)每次調(diào)用都會(huì)關(guān)聯(lián)一個(gè)新的頂點(diǎn)窄绒,所以更準(zhǔn)確的說(shuō)頂點(diǎn)數(shù)組的作用就是把多個(gè)頂點(diǎn)的參數(shù)正確的傳給對(duì)應(yīng)的多個(gè)頂點(diǎn)??????好繞)贝次。
可能看了上邊的入門介紹,還是有點(diǎn)云里霧里彰导,很正常蛔翅。我第一次也是嘛玩意都沒看懂。主要是因?yàn)楝F(xiàn)在很多教程都是上來(lái)就給你講功能位谋,你完全不知道這個(gè)功能是是解決什么問(wèn)題的搁宾。就像你只知道glVertexAttribPointer 是賦值api,但是不知道為什么這么設(shè)計(jì)這個(gè)api倔幼。所以在這個(gè)系列的文章中我會(huì)盡量先描述問(wèn)題盖腿,再由問(wèn)題引申出來(lái)gl的相應(yīng)方案,感覺這樣理解起來(lái)會(huì)輕松很多的损同。
好了這就是第一篇的所有內(nèi)容翩腐,又看不明白的地方不用怕,后續(xù)的文章會(huì)把坑慢慢填回來(lái)的膏燃。后續(xù)會(huì)介紹著色器的基本語(yǔ)法茂卦,以及應(yīng)用層給著色器傳值的各種方式。并且會(huì)嘗試?yán)L制更多的圖形组哩。