Before we go
在高性能graphics領(lǐng)域蚜枢,特別是3D graphics領(lǐng)域,OpenGL無疑是目前的最佳選擇针饥,雖然祟偷,現(xiàn)在有很多集成度高的三方的庫或者SDK,但是學(xué)習(xí)一下OpenGL仍然是非常有好處的打厘,你可以了解基本的computer graphics的概念,這會讓你在使用它們的時候更加的從容贺辰。
OpenGL是一個跨平臺的高性能3D渲染API户盯,OpenGL ES是它的嵌入式平臺版本嵌施。
我們即將踏上學(xué)習(xí)OpenGL ES 2.0之旅,主要針對于Android平臺莽鸭,會有一系列文章來分享學(xué)習(xí)OpenGL ES的總結(jié)吗伤。
主要編程語言將使用Kotlin,對于Kotlin還不熟悉的同學(xué)可以先看前面的介紹和實例來快速的熟悉一下硫眨。
Android上面的OpenGL ES一共有三個版本足淆,1.0,2.0以及現(xiàn)在的3.x(3.1, 3.2)礁阁,其中1.0是舊式的API巧号,與桌面版本的OpenGL非常接近,但是卻不太好用姥闭。從2.0開始丹鸿,API有較大變化,具體的渲染相關(guān)使用專門的著色語言來表達
矩陣的處理放到一個單獨的類Matrix中棚品,這樣解耦后靠欢,學(xué)習(xí)起來和理解起來相對容易,API也不會依賴于具體的對象铜跑,直接使用static式的GLES20或者GLES30就好了门怪。3.0是向后兼容的,它完全兼容2.0锅纺。所以掷空,從2.0開始學(xué)習(xí),是一個
比較好的選擇伞广,而且2.0被Android 2.3以后的SDK支持拣帽,應(yīng)該說目前所有的設(shè)備API上面都是支持OpenGL ES 2.0的(當(dāng)然,具體的支持情況還看硬件GPU)嚼锄。
為了方便减拭,在此系列文章中,OpenGL区丑,或者OpenGL ES或者GL拧粪,都是指OpenGL ES 2.0。
關(guān)于平臺沧侥,雖然我們是基于Android平臺來學(xué)習(xí)可霎,但是OpenGL是跨平臺的,所有平臺的GL的API(OpenGL, ES宴杀,或者WebGL癣朗,或者水果平臺)長的都類似,方法名字旺罢,以及參數(shù)都差不多旷余。雖然不可以直接使用绢记,但是當(dāng)作參考都沒有問題。
開發(fā)環(huán)境搭建
首先是Android app的開發(fā)環(huán)境搭建正卧,這個不多說了蠢熄,大家自行Google。SDK版本最好高一點炉旷,至少要是5.0 (API 20)以上吧签孔。
其次是Kotlin語言的支持,如是是Android Studio 3.0以上的版本窘行,自帶支持饥追,不用折騰。否則可以參考官方網(wǎng)站的指導(dǎo)抽高。
涉及到SDK相關(guān)的東西就是Activity判耕,我們是有頁面顯示的,所以必須要有一個Activity翘骂,這個都懂得壁熄。主要是widget就是android.oepngl.GLSurfaceView,
以及android.opengl.GLSurfaceView.Renderer碳竟。GLSurfaceView是Android平臺專門用于OpenGL繪制的組件草丧,我們只需要創(chuàng)建一個
實例,然后做一些基本的配置就好了莹桅,每個例子的配置都是很類似昌执。重點就是要實現(xiàn)一個GLSurfaceView.Renderer,這個是OpenGL開發(fā)的重點诈泼。
Step by step guide
首先懂拾,新建一個Android app項目,注意帶上Kotlin支持铐达,默認是鉤上的岖赋。名字隨意,比如叫EffectiveGL瓮孙。
然后唐断,在項目新建一個空白Activity,不用鉤選backward compat和創(chuàng)建layout杭抠,因為我們只用一個GLSurfaceView脸甘,用不著layout文件,另外偏灿,我們是用Kotlin丹诀,Kotlin是用Anko來用代碼寫布局。
再有,在Activity里面忿墅,創(chuàng)建一個GLSurfaceView對象扁藕,然后當(dāng)作Activity的布局。
最后疚脐,實現(xiàn)一個Renderer接口,塞給GLSurfaceView邢疙,并對其做簡單的配置棍弄。
最終,一個準(zhǔn)備好開發(fā)OpenGL的基本代碼是這樣子的疟游,這些基礎(chǔ)的準(zhǔn)備工作呼畸,后面的示例中會略掉。
class HelloPoints : Activity() {
private lateinit var glSurfaceView: GLSurfaceView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = "Play with Points"
glSurfaceView = GLSurfaceView(this)
setContentView(glSurfaceView)
glSurfaceView.setEGLContextClientVersion(2)
glSurfaceView.setRenderer(PointsRender)
glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
}
override fun onResume() {
super.onResume()
glSurfaceView.onResume()
}
override fun onPause() {
super.onPause()
glSurfaceView.onPause()
}
companion object PointsRender : GLSurfaceView.Renderer {
override fun onDrawFrame(p0: GL10?) {
}
override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {
}
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
}
}
}
基礎(chǔ)概念理解
有一些基礎(chǔ)中的基礎(chǔ)的概念需要理解一下,才能開始碼代碼。剛接觸這么多概念首量,可能還沒有理解它們旁振,沒有關(guān)系,先建立一個大概印象刁品,隨著學(xué)習(xí)的深入,就慢慢理解它們了。
GL context
GL API的調(diào)用蹦漠,雖然都是static形式的,沒有限制车海,在哪里都能直接call笛园,但是實際上它是有一個上下文環(huán)境的,叫GL context(目前階段先這么叫著吧侍芝,不是太嚴(yán)謹(jǐn)哈)研铆。這有點聽不懂,用人話說州叠,
就是所有的GL API的調(diào)用都要在GLSurfaceView.Renderer的三個方法里面來call棵红,就是方法的調(diào)用棧必須從這幾個方法開始。在其他地方call是沒有效果的:
onSurfaceCreated
onSurfaceChanged
onDrawFrame
GL的坐標(biāo)系
OpenGL的坐標(biāo)系是所謂的右手坐標(biāo)系留量。
首先它是三維的笛卡爾坐標(biāo)系:原點在屏幕正中窄赋,x軸從屏幕左向右,最左是-1楼熄,最右是1忆绰;y軸從屏幕下向上,最下是-1可岂,最上是1错敢;z軸從屏幕里面向外,最里面是-1,最外面是1稚茅。
shader
GL ES 2.0與1.0版本最大的區(qū)別在于纸淮,把渲染相關(guān)的操作用一個專門的叫作著色語言的程序來表達,全名叫作OpenGL ES Shading language亚享,它是一個編程語言咽块,與C語言非常類似,能夠直接操作矩陣和向量欺税,運行在GPU之上
專門用于圖形渲染侈沪。它又分為兩種,一個叫做頂點著色器(vertex shader)晚凿,另一個叫做片元著色器(fragment shader)亭罪。前者用來指定幾何形狀的頂點;后者用于指定每個頂點的著色歼秽。
每個GL程序必須要有一個vertex shader和一個fragment shader应役,且它們是相互對應(yīng)的。(相互對應(yīng)燥筷,意思是vertex shader必須要有一個fragment shader箩祥,反之亦然,但并不一定是一一對應(yīng))荆责。當(dāng)然滥比,也是可以復(fù)用的,
比如同一個vertex shader做院,可能會多個fragment shader來表達不同的著色方案盲泛。
坐標(biāo)值和顏色值
坐標(biāo)正常的取值范圍都是-1到1,且是float類型键耕。
顏色值是0到1寺滚,也是float類型,0是空(無的意思屈雄,比如黑色村视,或者全透明),1是有(全的意思酒奶,比如白色蚁孔,或者不透明),有些API是使用0~255惋嚎,這時就需要轉(zhuǎn)換一下杠氢。
其實呢,寫成超過此范圍的值也是可以的另伍,比如坐標(biāo)傳2鼻百,或者顏色寫成5,OpenGL會處理成為它的合理的取值之內(nèi),用clamp的方式温艇,超過的會被砍掉因悲,如傳5,相當(dāng)于傳1勺爱。
好了晃琳,準(zhǔn)備工作差不多了,我們來擼代碼吧琐鲁。
年輕人的第一個OpenGL程序
我們的目標(biāo)是畫一個紅色的點蝎土,就是這個樣子的:
注意: 鑒于方便理解,我們暫時只做一些2D的渲染绣否,也不調(diào)整view port,因為這會涉及比較復(fù)雜的Model View Projection矩陣的設(shè)置挡毅。
最終的代碼就是這個樣子的蒜撮,重點看一下Renderer的實現(xiàn),后面詳細講解:
const val TAG = "HelloPoints"
class HelloPoints : Activity() {
private lateinit var glSurfaceView: GLSurfaceView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = "Play with Points"
glSurfaceView = GLSurfaceView(this)
setContentView(glSurfaceView)
glSurfaceView.setEGLContextClientVersion(2)
glSurfaceView.setRenderer(PointsRender)
glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
}
override fun onResume() {
super.onResume()
glSurfaceView.onResume()
}
override fun onPause() {
super.onPause()
glSurfaceView.onPause()
}
companion object PointsRender : GLSurfaceView.Renderer {
private const val VERTEX_SHADER =
"void main() {\n" +
"gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n" +
"gl_PointSize = 20.0;\n" +
"}\n"
private const val FRAGMENT_SHADER =
"void main() {\n" +
"gl_FragColor = vec4(1., 0., 0.0, 1.0);\n" +
"}\n"
private var mGLProgram: Int = -1
override fun onDrawFrame(p0: GL10?) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
GLES20.glUseProgram(mGLProgram)
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1)
}
override fun onSurfaceChanged(p0: GL10?, p1: Int, p2: Int) {
GLES20.glViewport(0, 0, p1, p2)
}
override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 1f)
val vsh = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
GLES20.glShaderSource(vsh, VERTEX_SHADER)
GLES20.glCompileShader(vsh)
val fsh = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER)
GLES20.glShaderSource(fsh, FRAGMENT_SHADER)
GLES20.glCompileShader(fsh)
mGLProgram = GLES20.glCreateProgram()
GLES20.glAttachShader(mGLProgram, vsh)
GLES20.glAttachShader(mGLProgram, fsh)
GLES20.glLinkProgram(mGLProgram)
GLES20.glValidateProgram(mGLProgram)
val status = IntArray(1)
GLES20.glGetProgramiv(mGLProgram, GLES20.GL_VALIDATE_STATUS, status, 0)
Log.d(TAG, "validate shader program: " + GLES20.glGetProgramInfoLog(mGLProgram))
}
}
}
示例代碼講解
基礎(chǔ)設(shè)施
先來看一下Activity的onCreate/onResume和onPause這三個方法跪呈。先是在onCreate里面創(chuàng)建一個GLSurfaceView實例段磨,設(shè)置為content view,因為我們要使用OpenGL ES 2.0耗绿,所以要setEGLContextClientVersion(2)苹支。然后,再
設(shè)置一個Renderer實例误阻,渲染模式(render mode)分為兩種债蜜,一個是GLSurfaceView主動刷新(continuously),不停的回調(diào)Renderer的onDrawFrame究反,另外一種叫做被動刷新(when dirty)寻定,就是當(dāng)請求刷新時才調(diào)一次onDrawFrame。
這里我們用continuously的方式精耐。
至于onResume/onPause狼速,API要求是要調(diào)用一下GLSurfaceView的onResume和onPause,照做就好卦停,對于我們的示例來說向胡,其實調(diào)與不調(diào)看不出區(qū)別。這只是影響離開Activity頁面時的性能惊完,我們學(xué)習(xí)初期僵芹,可以不予關(guān)注。
Renderer之onSurfaceCreated
這個是最先被回調(diào)到的方法专执,告訴你系統(tǒng)層面淮捆,已經(jīng)ready了,你可以開始做你的事情了。一般我們會在此方法里面做一些初始化工作攀痊,比如編譯鏈接shader程序桐腌,初始化buffer等。我們一行一行的來分析:
GLES20.glClearColor(0f, 0f, 0f, 1f) // 參數(shù)順序 r, g, b, a
這句是告訴OpenGL苟径,給我把背景案站,或者叫作畫布,畫成黑色棘街,不透明蟆盐。比較繞人的說法是用參數(shù)指定的(r, g, b, a)這個顏色來初始化顏色緩沖區(qū)(color buffer)。目前就理解成為畫面背景色就可以了遭殉。
接下來的這一坨是編譯和鏈接shader程序:
val vsh = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER)
創(chuàng)建一個vertex shader程序石挂,返回的是它的句柄,此返回值會用在后續(xù)操作的參數(shù)险污,所以痹愚,要用變量記錄下來。
GLES20.glShaderSource(vsh, VERTEX_SHADER) // 告訴OpenGL蛔糯,這一坨字串里面是vertex shader的源碼拯腮。
GLES20.glCompileShader(vsh) // 編譯vertex shader
接下來的三行,是編譯fragment shader蚁飒,跟vertex shader是一樣的动壤。
然后是創(chuàng)建shader program并把shader鏈到上頭去。同樣的淮逻,先創(chuàng)建一個shader program句柄琼懊,后面要用,所以要記錄一下弦蹂,因為要在此方法外使用program句柄肩碟,所以要用全局變量來記錄。
mGLProgram = GLES20.glCreateProgram() // 創(chuàng)建shader program句柄
GLES20.glAttachShader(mGLProgram, vsh) // 把vertex shader添加到program
GLES20.glAttachShader(mGLProgram, fsh) // 把fragment shader添加到program
GLES20.glLinkProgram(mGLProgram) // 做鏈接凸椿,可以理解為把兩種shader進行融合削祈,做好投入使用的最后準(zhǔn)備工作
到此,其實shader program的準(zhǔn)備工作已經(jīng)做完了脑漫,但是如果shader編譯或者鏈接過程出錯了怎么辦呢髓抑?能不能提早發(fā)現(xiàn)呢?當(dāng)然优幸,有辦法檢查一下吨拍,就是用接下來的這幾句:
GLES20.glValidateProgram(mGLProgram) // 讓OpenGL來驗證一下我們的shader program,并獲取驗證的狀態(tài)
val status = IntArray(1)
GLES20.glGetProgramiv(mGLProgram, GLES20.GL_VALIDATE_STATUS, status, 0) // 獲取驗證的狀態(tài)
Log.d(TAG, "validate shader program: " + GLES20.glGetProgramInfoLog(mGLProgram))
如果有語法錯誤网杆,編譯錯誤羹饰,或者狀態(tài)出錯伊滋,這一步是能夠檢查出來的。如果一切正常队秩,則取出來的status[0]為0笑旺。
Renderer之onSurfaceChanged
此回調(diào),會在surface發(fā)生改變時馍资,通常是size發(fā)生變化筒主。這里我們改變一下視角。
GLES20.glViewport(0, 0, p1, p2) // 參數(shù)是left, top, width, height
就是要指定OpenGL的可視區(qū)域(view port)鸟蟹,(0, 0)是左上角乌妙,然后是width和height。
我們目前只學(xué)習(xí)2D繪制建钥,所以藤韵,先不管三維視角的處理。
Renderer之onDrawFrame
這個是最重要的方法熊经,沒有之一荠察。前面兩個,只會在surface created時調(diào)一次奈搜。而此方法是用來繪制每幀的,所以每次刷新都會被調(diào)一次盯荤,所有的繪制都發(fā)生在這里馋吗。
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) // 清除顏色緩沖區(qū),因為我們要開始新一幀的繪制了秋秤,所以先清理宏粤,以免有臟數(shù)據(jù)。
GLES20.glUseProgram(mGLProgram) // 告訴OpenGL灼卢,使用我們在onSurfaceCreated里面準(zhǔn)備好了的shader program來渲染
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1) // 開始渲染绍哎,發(fā)送渲染點的指令, 第二個參數(shù)是offset鞋真,第三個參數(shù)是點的個數(shù)崇堰。目前只有一個點,所以是1涩咖。
vertex shader
private const val VERTEX_SHADER =
"void main() {\n" +
"gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n" +
"gl_PointSize = 20.0;\n" +
"}\n"
shader語言跟C語言很像海诲,它有一個主函數(shù),也叫void main(){}檩互。
gl_Position是一個內(nèi)置變量特幔,用于指定頂點,它是一個點闸昨,三維空間的點蚯斯,所以用一個四維向量來賦值薄风。vec4是四維向量的類型,vec4()是它的構(gòu)造方法拍嵌。等等遭赂,三維空間,不是(x, y, z)三個嗎撰茎?咋用vec4呢嵌牺?
四維是叫做齊次坐標(biāo),它的幾何意義仍是三維龄糊,先了解這么多逆粹,記得對于2D的話,第四位永遠傳1.0就可以了炫惩。這里僻弹,是指定原點(0, 0, 0)作為頂點,就是說想在原點位置畫一個點他嚷。gl_PointSize是另外一個內(nèi)置變量蹋绽,用于指定點的大小。
這個shader就是想在原點畫一個尺寸為20的點筋蓖。
fragment shader
private const val FRAGMENT_SHADER =
"void main() {\n" +
"gl_FragColor = vec4(1., 0., 0.0, 1.0);\n" +
"}\n"
gl_FragColor是fragment shader的內(nèi)置變量卸耘,用于指定當(dāng)前頂點的顏色,四個分量(r, g, b, a)粘咖。這里是想指定為紅色蚣抗,不透明。
Fun time
更改一些參數(shù)瓮下,看看會發(fā)生什么:
- 改變onSurfaceCreated中的glClearColor的顏色值
- 改變gl_Position
- 改變gl_PointSize
- 改變gl_FragColor
One more thing
此系列教程會共存在同一個Android app項目里面翰铡,所以我們會隨著代碼的增加而進行一系列的重構(gòu),但是這與我們的主題OpenGL無關(guān)讽坏,如果是單純學(xué)習(xí)OpenGL锭魔,可以略過此節(jié)。
因為路呜,每個教程會講解不同的點迷捧,對Activity可能有不同的需求,所以胀葱,一個教程對應(yīng)著一個Activity党涕,這樣就需要一個列表來作為路由目錄頁面:
class HomeActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = "Learn OpenGL ES Effectively"
verticalLayout {
textView("Welcome to the world of OpenGL ES") {
gravity = Gravity.CENTER
}.onClick { startActivity<HelloPoints>() }
}
}
}
參考資料
-
《WebGL Programming Guide》
WebGL跟OpenGL ES 2.0相差無幾,可以直接參考巡社。這本書最大好處是講解比較清晰膛堤,層次遞進,代碼完整晌该,非常適合初學(xué)者上手肥荔。 -
《OpenGL? ES 2.0 Programming Guide》
這本書比較啰嗦和枯燥绿渣,它更接近于規(guī)范,非常詳盡嚴(yán)謹(jǐn)?shù)闹v述燕耿,但是講解過少中符,示例也少。所以誉帅,它更適合于有一定基礎(chǔ)淀散,想要更深入的全面的理解某一概念時看,不適合入門蚜锨。
所以档插,這兩本書加起來看效果最佳,先入門亚再,理解基本概念郭膛,然后再通過后者全面理解,鞏固加強氛悬。
原文鏈接:http://toughcoder.net/blog/2018/07/31/introduction-to-opengl-es-2-dot-0/
微信公眾號:稀有猿訴