本案例的主要目的是理解GLSL中如何使用索引繪圖。最終效果如下:

GLSL使用索引繪圖效果圖
什么是索引繪圖
一個圖形有許多頂點,如本案例最終效果中的金字塔,一共有5個面6個三角形組成。由這6個三角形根據圖元裝配繪制金字塔效果。在繪制過程中,共使用了18個頂點,因為圖元裝配方式使得有些頂點是進行了復用,而實際肉眼可見頂點個數只有5個。如下圖所示。

image.png
索引繪制技巧就是將圖形中肉眼可見的頂點,通過索引的方式表示頂點之間的連接。
在上圖中,各三角形的頂點構成方式如下:
- 左面的三角形由頂點{0,2,4}構成。
- 右面的三角形由頂點{1,4,3}構成。
- 前面的三角形由頂點{2,3,4}構成。
- 后面的三角形由頂點{0,4,1}構成。
- 底部由兩個三角形組成,兩個三角形分別是由頂點{0,3,2}和頂點{0,1,3}構成。
使用GLSL索引繪圖的思路如下:

GLSL索引繪圖的思路
準備工作
- 新建一個iOS工程,并將新工程中的view的繼承關系,修改成繼承于自定義View。
- 在圖形進行旋轉時需要用到矩陣相乘的操作,本案例中引入一個數學庫,完成矩陣相乘等操作。
自定義著色器文件
自定義著色器文件的內容如下:

自定義著色器
頂點著色器
- 由于本案例中點擊按鈕將實現旋轉,這將涉及矩陣變換,因此需要在頂點著色器中定義兩個uniform變量:1,投影矩陣;2,模型視圖矩陣。
- 在計算頂點最終坐標時,需要將投影矩陣模型視圖矩陣源頂點坐標,再將最終結果值賦值給內建變量gl_Position。
- 注意:在頂點數據與矩陣相乘時,需要注意相乘的順序。因為在OpenGL ES中是以列向量為主,position是列向量,即41矩陣。而投影矩陣、模型視圖矩陣都是44矩陣,因此需要按投影矩陣模型視圖矩陣position的順序。
attribute vec4 position;
attribute vec4 positionColor;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
varying lowp vec4 varyColor;
void main(){
varyColor = positionColor;
gl_Position = projectionMatrix * modelViewMatrix * position;
}
片元著色器
- 本案例中不涉及紋理操作,因此只需要在片元著色器中將頂點顏色直接賦值給內建變量gl_FragColor即可。
- 如果頂點顏色少于頂點個數,則會對缺少顏色的頂點進行顏色插值,產生一個類似顏色漸變的效果。
- 如果不想引入插值,則需要指定整個面的顏色而不是頂點顏色。
varying lowp vec4 varyColor;
void main(){
gl_FragColor = varyColor;
}
重寫layoutSubviews函數
在layoutSubviews函數中,主要完成以下工作:

image.png
- 設置繪制圖層
創(chuàng)建繪制內容的載體。
-(void)setupLayer{
self.myEagLayer = (CAEAGLLayer*)self.layer;
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
self.myEagLayer.opaque = YES;
self.myEagLayer.drawableProperties = @{
kEAGLDrawablePropertyRetainedBacking:@false,
kEAGLDrawablePropertyColorFormat:kEAGLColorFormatRGBA8
};
}
+ (Class)layerClass{
return [CAEAGLLayer class];
}
- 設置上下文
用于記錄繪制過程中的一些狀態(tài)信息。
-(void)setupContext{
self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.myContext];
}
- 清空緩沖區(qū)
避免之前緩沖區(qū)的數據對本次繪制造成影響。
-(void)deletBuffer{
glDeleteRenderbuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
glDeleteFramebuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
- 設置頂點緩沖區(qū)
設置一下用于存儲頂點數據或顏色數據等的緩沖區(qū)。
-(void)setupRenderBuffer{
glGenRenderbuffers(1, &_myColorRenderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
- 設置幀緩沖區(qū)
設置管理頂點緩沖區(qū)的幀緩沖區(qū)。
-(void)setupFrameBuffer{
glGenFramebuffers(1, &_myColorFrameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
- 根據著色器文件創(chuàng)建program
6.1 讀取著色器文件;
6.2 創(chuàng)建頂點著色器和片元著色器;
6.3 分別著色器文件將內容加載到頂點著器和片元著色器中;
6.4 編譯著色器;
6.5 創(chuàng)建program;
6.6 將著色器鏈附著到program上;
6.7 刪除已經附著過的著色器;
以上是一個編譯著色器文件并附著至program中的流程,在OpenGL ES案例03 - 使用GLSL完成紋理圖片加載中也有描述過該流程,詳細的代碼可參見本案例的Demo。
-(void)setupProgram{
NSString* vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString* fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
self.myProgram = [self loadShader:vertFile frag:fragFile];
glLinkProgram(self.myProgram);
glUseProgram(self.myProgram);
}
- 設置頂點數據
這部分主要是將頂點數據從CPU傳遞給GPU,并打開頂點屬性通道。
-(void)setupVertexData{
//(1)頂點數組 前3頂點值(x,y,z),后3位顏色值(RGB)
GLfloat attrArr[] = {
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, //左上0
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, //右上1
-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, //左下2
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, //右下3
0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, //頂點4
};
GLuint bufferID;
glGenBuffers(1, &bufferID);
glBindBuffer(GL_ARRAY_BUFFER, bufferID);
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
GLuint position = glGetAttribLocation(self.myProgram, "position");
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), NULL);
GLuint positionColor = glGetAttribLocation(self.myProgram, "positionColor");
glEnableVertexAttribArray(positionColor);
glVertexAttribPointer(positionColor, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLfloat*)NULL+3);
}
- 設置投影矩陣
本案例中繪制的是一個立體圖形,為了更逼近真實效果,需要設置一個投影矩陣。
8.1 創(chuàng)建一個4*4的矩陣;
8.2 通過ksMatrixLoadIdentity函數,將投影矩陣初始化為一個單元矩陣;
8.3 通過ksPerspective函數,設置透視投影;
8.4 通過glUniformMatrix4fv函數,將投影矩陣傳遞給頂點著色器;
-(void)setupProjectionData{
GLuint projectionMatrixSlot = glGetUniformLocation(self.myProgram, "projectionMatrix");
KSMatrix4 _projectionMatrix;
ksMatrixLoadIdentity(&_projectionMatrix);
float aspect = self.frame.size.width/self.frame.size.height;
ksPerspective(&_projectionMatrix, 30, aspect, 5.0, 20.0);
ksTranslate(&_projectionMatrix, 0, 0, -10.0);
glUniformMatrix4fv(projectionMatrixSlot, 1, GL_FALSE, &_projectionMatrix.m[0][0]);
}
- 設置模型視圖矩陣
初始化一個模型視圖矩陣,后續(xù)可將旋轉、平移等操作與該矩陣進行相乘。
9.1 定義一個4*4的模型視圖矩陣;
9.2 通過ksMatrixLoadIdentity函數,將模型視圖矩陣初始化為一個單元矩陣;
9.3 通過glUniformMatrix4fv函數,將模型視圖矩陣傳遞給頂點著色器;
- (void)setupModelViewData{
GLuint modelViewMatrixSlot = glGetUniformLocation(self.myProgram, "modelViewMatrix");
KSMatrix4 _modelViewMatrix;
ksMatrixLoadIdentity(&_modelViewMatrix);
glUniformMatrix4fv(modelViewMatrixSlot, 1, GL_FALSE, &_modelViewMatrix.m[0][0]);
}
- 開始繪制
至此,基本的操作已經完成了,之前的案例中使用的默認繪圖方式,這里通過使用glDrawElements函數來完成索引繪圖。
-(void)render{
glClearColor(0, 0.0, 0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
GLfloat scale = [[UIScreen mainScreen] scale];
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);
glEnable(GL_CULL_FACE);
//(2).索引數組
GLuint indices[] = {
0, 3, 2,
0, 1, 3,
0, 2, 4,
0, 4, 1,
2, 3, 4,
1, 4, 3,
};
/*
GLenum mode:繪圖的模型
GL_POINTS
GL_LINES
GL_LINE_LOOP
GL_LINE_STRIP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
GLsizei count:繪圖所需要索引的個數
GLenum type:索引數組中數據類型
const GLvoid* indices:索引數組
*/
glDrawElements(GL_TRIANGLES, sizeof(indices)/sizeof(indices[0]), GL_UNSIGNED_INT, indices);
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}
至此,已經完成了整個圖形的繪制。接下來就是完成點擊按鈕后可以執(zhí)行旋轉的效果了。
實現按鈕點擊后圖形旋轉效果
為了讓圖形能沿著x、y、z軸旋轉,我們定義三個按鈕,分別命名為x、y、z,當點擊不同按鈕時,沿著不同的軸進行旋轉,點擊一次為啟動旋轉,再次點擊為停止旋轉。
重點:此部分的重點為如何發(fā)生旋轉。
- 定義一個4*4的模型視圖矩陣;
- 通過ksMatrixLoadIdentity函數將模型視圖矩陣初始化為一個單元矩陣;
- 定義一個4*4的旋轉矩陣;
- 通過ksMatrixLoadIdentity函數,將定義的旋轉矩陣也初始化為一個單元矩陣;
- 通過ksRotate函數,實現沿某個軸發(fā)生旋轉;
- 通過ksMatrixMultiply函數,將旋轉矩陣與模型視圖矩陣相乘,并將結果放入模型視圖矩陣中;
- 通過glUniformMatrix4fv函數將最終模型視圖矩陣傳遞給頂點著色器;
- 重新繪制;
-(void)reDegree{
xDegree += bX*5;
yDegree += bY*5;
zDegree += bZ*5;
GLuint modelViewMatrixSlot = glGetUniformLocation(self.myProgram, "modelViewMatrix");
KSMatrix4 _modelViewMatrix;
ksMatrixLoadIdentity(&_modelViewMatrix);
KSMatrix4 _rotationMatrix;
ksMatrixLoadIdentity(&_rotationMatrix);
ksRotate(&_rotationMatrix, xDegree, 1, 0, 0);
ksRotate(&_rotationMatrix, yDegree, 0, 1, 0);
ksRotate(&_rotationMatrix, zDegree, 0, 0, 1);
ksMatrixMultiply(&_modelViewMatrix, &_rotationMatrix, &_modelViewMatrix);
glUniformMatrix4fv(modelViewMatrixSlot, 1, GL_FALSE, &_modelViewMatrix.m[0][0]);
[self render];
}
完整代碼見:GLSL三角形變換Demo地址