OpenGL首先我們從字面意思來理解:Open Graphics Library,開放的圖形庫,圖形庫自然是處理圖形的,所以簡單來說OpenGL就是用來處理圖形的一個三方庫。
稍微技術流一點,作如下解釋:是用于渲染2D,3D矢量圖形的跨語言、跨平臺的應用程序編程接口(API)。
OpenGL在移動端的表現形式為OpenGLES,OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL三維圖形 API 的子集,針對手機、PDA和游戲主機等嵌入式設備而設計。
接下來我們從openGL在移動端的應用為入口,探一探它的奧秘。(以iOS平臺為例)
一.用openGLES繪制圖形的基本流程
1.UIView,要展示圖形,還是需要基本的承載視圖,UIView
2.layer,OpenGLES的描繪必須在CAEAGLLayer上才能顯示出來,所以我們需要重寫這個函數,修改view默認的layer返回類型,從CAEAGLLayer可以看出,它也屬于Core Animation。
+(Class)layerClass{//默認是CALayer
//OpenGL內容只會在此類layer上描繪
return [CAEAGLLayer class];
}
3.context,EAGLContext對象是管理OpenGL ES渲染上下文,若想使用OpenGL ES 進行繪制工作,則必須有一個上下文對象.
-(void)setupLayerAndContext
{
_eaglLayer = (CAEAGLLayer *)self.layer;
_eaglLayer.opaque = YES;
_eaglLayer.drawableProperties = @{
kEAGLDrawablePropertyRetainedBacking:@(NO),
kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8
};
_eaglLayer.contentsScale = screenScale;
// 指定 OpenGLES 渲染API的版本,在這里我們使用OpenGLES 3.0,由于3.0兼容2.0并且功能更強,為何不用更好的呢
//注:在iOS上,可以支持opengles3.0的最低環(huán)境是iphone5s ios7.0.
_context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];
[EAGLContext setCurrentContext:_context];
}
4.buffer,renderBuffer和frameBuffer
renderBuffer:renderbuffer對象是應用程序分配的2D圖像緩沖區(qū)。renderbuffer可以用來分配和存儲顏色、深度或模板值,也可以用作framebuffer對象中的顏色、深度或模板附件。渲染緩沖區(qū)類似于屏幕外窗口系統提供的可繪制表面,例如pbuffer。但是,渲染緩沖區(qū)不能直接用作GL紋理。
frameBuffer:framebuffer對象(通常稱為FBO)是顏色、深度和模板緩沖區(qū)連接點的集合;描述附加到FBO的顏色、深度和模板緩沖區(qū)的大小和格式等屬性的狀態(tài);以及附加到FBO的紋理和renderbuffer對象的名稱。可以將各種2D圖像附加到framebuffer對象中的顏色附著點。這些包括存儲顏色值的renderbuffer對象、二維紋理或cubemap面的mip級別,甚至三維紋理中的二維切片的mip級別。類似地,各種包含深度值的2D圖像可以附加到FBO的深度附著點。這些可以包括一個renderbuffer,一個二維紋理的mip級,或者一個存儲深度值的cubemap面。唯一可以附加到FBO模板附著點的2D圖像是一個存儲模板值的renderbuffer對象。
-(void)setupRenderBuffer{
glGenRenderbuffers(1, &_renderBuffer); //生成和綁定render buffer的API函數
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//為其分配空間
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
}
-(void)setupFrameBuffer{
glGenFramebuffers(1, &_frameBuffer); //生成和綁定frame buffer的API函數
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
//將renderbuffer跟framebuffer進行綁定
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
}
5.繪制渲染
-(void)render
{
//設置清屏顏色,默認是黑色,如果你的運行結果是黑色,問題就可能在這兒
glClearColor(0.3, 0.5, 0.8, 1.0);
/*
glClear指定清除的buffer
共可設置三個選項GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT
也可組合如:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
這里我們只用了color buffer,所以只需清除GL_COLOR_BUFFER_BIT
*/
glClear(GL_COLOR_BUFFER_BIT);
[_context presentRenderbuffer:_renderBuffer];
}
OK,到此我們就算是完成一個OpenGL ES的簡單流程了。是不是特別簡單呢?接下來我們講講坐標系統、著色器、渲染管線。
二.坐標系統
開始繪制圖形之前,我們必須先給OpenGL輸入一些頂點數據。OpenGL是一個3D圖形庫,所以我們在OpenGL中指定的所有坐標都是3D坐標(x、y和z)。OpenGL不是簡單地把所有的3D坐標變換為屏幕上的2D像素;OpenGL僅當3D坐標在3個軸(x、y和z)上都為-1.0到1.0的范圍內時才處理它。這就是標準化設備坐標,只有在這個范圍內的坐標才會最終呈現在屏幕上(在這個范圍以外的坐標都不會顯示)。
我們通常會自己設定一個坐標的范圍,之后再在頂點著色器中將這些坐標轉換為標準化設備坐標。然后將這些標準化設備坐標傳入光柵器(Rasterizer),再將他們轉換為屏幕上的二維坐標或像素。將坐標轉換為標準化設備坐標,接著再轉化為屏幕坐標的過程,這個過程涉及以下五個重要的坐標系統:
局部空間(Local Space,或者稱為物體空間(Object Space))
世界空間(World Space)
觀察空間(View Space,或者稱為視覺空間(Eye Space))
裁剪空間(Clip Space)
屏幕空間(Screen Space)
為了將坐標從一個坐標系轉換到另一個坐標系,我們需要用到幾個轉換矩陣,最重要的幾個分別是模型(Model)、觀察(View)、投影(Projection)三個矩陣,上圖也有所表示,下面我們來講解一下這三個矩陣。
投影矩陣
投影矩陣分為正交投影和透視投影,具體就不分析了,他們的區(qū)別就是:
正射投影矩陣直接將坐標映射到屏幕的二維平面內,從人的視覺效果出發(fā),將會產生不真實的結果,而透視投影遠處的頂點看起來比較小,符合人眼看物體近大遠小的效果,所以我們一般使用可能會選透視投影
在代碼里表示如下:
float aspect = self.frame.size.width/self.frame.size.height;
_projectionMatrix = GLKMatrix4MakePerspective(45.0*M_PI/180.0, aspect, 0.0001, 100);
glUniformMatrix4fv(_projectionSlot, 1, GL_FALSE, _projectionMatrix.m);
觀察矩陣(攝像機矩陣)
如下圖,我們可以直觀理解為,攝像機即我們的眼睛,眼睛可能會動,則看到的物體也可能會變化。
在代碼里表示如下:
eyeX = SXYZ * sinf(RZ) * cosf(RX);
eyeY = SXYZ * sinf(RX);
eyeZ = SXYZ * cosf(RZ) * cosf(RX);
eyeX += TX;
eyeY += TY;
eyeZ += TZ;
_camaraMatrix = GLKMatrix4MakeLookAt(eyeX, eyeY, eyeZ, TX, TY, TZ, 0, 1, 0);
glUniformMatrix4fv(_modelViewSlot, 1, GL_FALSE, _camaraMatrix.m);
模型矩陣
模型矩陣即物體相對于自身變化,如圖:
我們可以看到圖中茶壺先旋轉再平移與先平移再旋轉最終的結果是不一樣的,因為它都是基于物體本身,學過線性代數我們會知道矩陣乘法不滿足交換律。模型矩陣在代碼里表示如下:
_poiTitleModelViewMatrix = GLKMatrix4MakeTranslation(titlePoint.x, poiPlaneTop+0.005, titlePoint.y);
_poiTitleModelViewMatrix = GLKMatrix4RotateY(_poiTitleModelViewMatrix, RY);
_poiTitleModelViewMatrix = GLKMatrix4RotateX(_poiTitleModelViewMatrix, -RX);
camara_position_distance = sqrtf(pow(eyeX-titlePoint.x, 2)+pow(eyeY-(poiPlaneTop+0.005), 2)+pow(eyeZ-titlePoint.y, 2));
_poiTitleModelViewMatrix = GLKMatrix4Scale(_poiTitleModelViewMatrix, camara_position_distance, camara_position_distance, camara_position_distance);
三.著色器
頂點著色器(Vertex Shader)
在 openGL 編程中頂點著色器是必須的,頂點著色器的功能如下:
1.使用模型視圖矩陣和投影矩陣進行頂點位置變換
2.法線變換,法線工規(guī)范化
3.紋理坐標生成和變換
4.計算每個頂點的光照
5.顏色計算
總的來說就是處理頂點和顏色數據。
如下是一個自定義的Vertex.glsl:
attribute vec4 Position;
attribute vec2 TexCoordIn;
varying vec2 TexCoordOut;
uniform bool isLocate;
uniform mat4 locateModel;
uniform mat4 Projection;
uniform mat4 View;
uniform mat4 Model;
void main(){
if (isLocate) {
gl_Position = Projection * View * locateModel * Position;
}else {
gl_Position = Projection * View * Model * Position;
}
// gl_Position = Position;
TexCoordOut = vec2(TexCoordIn.x, 1.0-TexCoordIn.y);
}
片元著色器(Fragment Shader)
片元著色器就是把頂點著色器的數據處理成實際屏幕坐標上的像素顏色
片元著色器的功能如下:
1.計算顏色
2.獲取紋理值
3.往像素點中填充顏色值(紋理值/顏色值)
如下是一個自定義的Fragment.glsl:
precision mediump float;
varying mediump vec4 OutColor;
uniform bool is_side;
uniform float sideColor;
uniform bool is_sprite;
void main()
{
if (is_sprite) {
if (length(gl_PointCoord-vec2(0.5)) > 0.45) //0.5會冒出來
discard;
}else
gl_FragColor = OutColor;
gl_FragColor = vec4(gl_FragColor.r*OutColor.r,gl_FragColor.g*OutColor.g,gl_FragColor.b*OutColor.b,gl_FragColor.a*OutColor.a);
if (is_side) {
// gl_FragColor = gl_FragColor * vec4(sideColor,sideColor,sideColor,1.0);
gl_FragColor = gl_FragColor;
}
}
四.渲染管線
下圖中展示整個OpenGL ES 2.0可編程渲染管線
圖中Vertex Shader和Fragment Shader 是可編程管線;
1).Vertex Array/Buffer objects
頂點數據來源,這是渲染管線的頂點輸入,VAO VBO是頂點存儲的不同樣式,他們在繪制時的方法也不一樣。
2).Vertex Shader
頂點著色器通過矩陣變換位置、計算照明公式來生成逐頂點顏色已經生成或變換紋理坐標等基于頂點的操作。
3).Primitive Assembly
圖元裝配經過著色器處理之后的頂點在圖片裝配階段被裝配為基本圖元。OpenGL ES 支持三種基本圖元:點,線和三角形,它們是可被 OpenGL ES 渲染的。接著對裝配好的圖元進行裁剪(clip):保留完全在視錐體中的圖元,丟棄完全不在視錐體中的圖元,對一半在一半不在的圖元進行裁剪;接著再對在視錐體中的圖元進行剔除處理(cull):這個過程可編碼來決定是剔除正面,背面還是全部剔除。
4).Rasterization
光柵化。在光柵化階段,基本圖元被轉換為二維的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,顏色,紋理坐標等信息,這些值是由圖元的頂點信息進行插值計算得到的。這些片元接著被送到片元著色器中處理。這是從頂點數據到可渲染在顯示設備上的像素的質變過程。
5).Fragment Shader
片元著色器通過可編程的方式實現對每個片元的操作。在這一階段它接受光柵化處理之后的fragment,color,深度值,模版值作為輸入,片元著色器可以拋棄片元,也可以生成一個或多個顏色值作為輸出。
6).Per-Fragment Operations (逐片段操作)
它包含像素歸屬測試(Pixel Ownership Test)、裁剪測試(Scissor Test)、模板和深度測試(Stencil And Depth Test)、混合(Blending)、抖動(Dithering)這些對片段的處理。我們渲染3d圖形常會用到這些。
7).Framebuffer:這是流水線的最后一個階段,Framebuffer 中存儲這可以用于渲染到屏幕或紋理中的像素值。
五.繪制
OpenGL ES可繪制的基本圖元是點、線和三角形,如下我們分析一段繪制的代碼(代碼已經過處理):
-(void)render
{
[EAGLContext setCurrentContext:_context];
glClearColor([backColorArr[0] floatValue], [backColorArr[1] floatValue], [backColorArr[2] floatValue], 1);
// glEnable(GL_POLYGON_OFFSET_FILL); //解決z_fighting問題
// glPolygonOffset(1.0f, 1.0f);
glEnable(GL_BLEND); //允許混合
// glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
//MSAA處理
glBindFramebuffer(GL_FRAMEBUFFER, mMSAAFramebuffer);
glBindRenderbuffer(GL_RENDERBUFFER, mMSAARenderbuffer);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, self.frame.size.width*2, self.frame.size.height*2);
[self updateCubeCenterToScreenPoint]; //確定邊界
glUseProgram(_programHandle); //使用某個這色器程序
[self setupCubeProjectionAndCamara];
[self drawOutLine]; //畫輪廓
[self drawTopPoi]; //畫頂層poi 里面有多次深度測試的開啟和關閉
glDisable(GL_DEPTH_TEST); //關閉深度測試
if (naviPathArr.count) {
for (int i=0; i<naviPathArr.count; i++) {
NSArray *onePath = naviPathArr[i];
[self drawNaviRoute:onePath]; //畫路徑和箭頭
[self drawNaviArrow:onePath];
}
}
[self drawNormalOverlays]; //繪制覆蓋物
glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); //避免文字穿透
glUseProgram(_poiTitleProgramHandle);
[self setupTitleProjectionAndCamara];
if (isFirstDrawTile) {//如果第一次篩選,不是第一次的篩選都在手勢里處理了
[self setupTitlesAndFacilyties]; //篩選文字和圖例
isFirstDrawTile = NO;
}
[self drawTopPoiTexture]; //畫頂面貼圖
[self drawFacilities]; //畫圖例
[self drawPoiTitles]; //畫文字
[self drawTextureOverlays]; //畫紋理型覆蓋物
if (isLocate) {//若需要畫定位點
[self drawLocatePoint:locatePoint];
}
glUseProgram(_heatMapProgramHandle); //繪制熱力圖使用熱力圖的這色器程序
[self setupHeatMapProjectionAndCamara];
[self drawHeatMap]; //畫熱力圖
//MSAA處理
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frameBuffer);
glBindFramebuffer(GL_READ_FRAMEBUFFER, mMSAAFramebuffer);
glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, (GLenum[]){GL_DEPTH_ATTACHMENT});
glBlitFramebuffer(0, 0, _width, _height, 0, 0, _width, _height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[_context presentRenderbuffer:GL_RENDERBUFFER];//調用這句話,圖形才能渲染到屏幕上
}
六:后續(xù)
我們學習OpenGL可以懂得很多圖形學上的知識,也能擴寬我們的眼界,這門技術可能跟我們工作的專業(yè)技術有較大區(qū)別,但可以給我們不一樣的思想。如我是做iOS開發(fā)的,以前接觸的圖形上的東西就是view、layer這種,學了openGL后,會明白layer原來也是OpenGL ES的基本圖元——兩個三角形繪制而成。
在iOS12之后,OpenGL ES的api被廢棄了,蘋果還是主推他們自己研發(fā)的metal,對于OpenGL ES和metal,事實上很多api都非常相似,再學習成本不會很大。
如下兩圖是蘋果渲染繪制框架的變化(OpenGL ES -> Metal)
七:最后我們從代碼視角來看一下openGL ES繪制的整個流程
[self setupLayer];
[self setupContext];
[self setupDepthBuffer];
[self setupRenderBuffer];
[self setupFrameBuffer];
[self setupProgram]; //配置program
[self setupProjectionMatrix];
[self setupModelViewMatrix];
[self setupRenderData];
[self render];
總結:事實上大家都在復雜化OpenGL的學習,而實際上,學習OpenGL復雜的只是需要我們多了解、先了解一些圖形學知識,大量去學習OpenGL的一些理論,然后回頭邊學邊做,后面學習實際上也差不多。而這些理論知識的學習,稍微有點基礎,理解能力強點,也花不了幾天時間。
參考資料:
1.蘋果官網OpenGL ES Programming Guide:https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/Introduction/Introduction.html
2.LearnOpenGL-CN:https://learnopengl-cn.readthedocs.io/zh/latest/
3.渲染管線:https://www.cnblogs.com/edisongz/p/6918428.html