分享
這系列收集OpenGL ES的應(yīng)用。
這篇介紹的3D魔方(原文地址),重點(diǎn)是魔方的旋轉(zhuǎn)與點(diǎn)擊的判斷。
效果展示

概念準(zhǔn)備
拾取
把地形的位置坐標(biāo)編碼到片元的顏色分量中,用戶觸摸時,檢查特定的像素的顏色分量以確定觸摸到的地形的位置。用戶看不到用于拾取的渲染,因?yàn)橛糜谑叭〉南袼仡伾秩揪彺娌粫@示到屏幕上,而是渲染到一個OpenGL ES的幀緩存對象(FBO)中。
- 1、基于顏色拾取
把位置信息編碼進(jìn)顏色分量,使用 glReadPixels() 讀取。
把渲染值從FBO讀取到CPU控制的內(nèi)存需要花費(fèi)時間執(zhí)行耗時的同步操作。
拾取在每秒中可能發(fā)生多次,會影響渲染。
- 2、幾何拾取
設(shè)想一個光線從平截體近平面上一個觸摸位置頭投射向這個位置對應(yīng)的遠(yuǎn)平面的點(diǎn)。被這個光線穿過的離視點(diǎn)最近的對象就是要拾取的對象。
不需要讀取FBO的渲染值,通過觸摸的視口坐標(biāo)和平截體,可形成光線。
核心思路
魔方直接渲染到屏幕,拾取的時候再渲染一次到FBO,通過拾取結(jié)果決定是旋轉(zhuǎn)某一列還是旋轉(zhuǎn)整個魔方。
具體解析
1、坐標(biāo)系與旋轉(zhuǎn)
#define ROTATE_NONE -1
#define ROTATE_ALL 0
#define ROTATE_X_CLOCKWISE 1
#define ROTATE_X_ANTICLOCKWISE 2
#define ROTATE_Y_CLOCKWISE 3
#define ROTATE_Y_ANTICLOCKWISE 4
#define ROTATE_Z_CLOCKWISE 5
#define ROTATE_Z_ANTICLOCKWISE 6
ROTATE_NONE 為未旋轉(zhuǎn)
ROTATE_ALL 為旋轉(zhuǎn)整個魔方
ROTATE_X_CLOCKWISE 為繞X軸順時針
ROTATE_X_ANTICLOCKWISE 為繞X軸逆時針
ROTATE_Y_CLOCKWISE 為繞Y軸順時針
ROTATE_Y_ANTICLOCKWISE 為Y軸逆時針
ROTATE_Z_CLOCKWISE 為繞Z軸順時針
ROTATE_Z_ANTICLOCKWISE 為繞Z軸逆時針
魔方的坐標(biāo)系如下:

2、attribute屬性、uniform變量的統(tǒng)一管理
YHCOpenGLProgram是對GLProgram的封裝,可以設(shè)置頂點(diǎn)、片元著色器,設(shè)置attribute屬性、uniform變量。
流程大致分三步
//1、初始化,并設(shè)置屬性
_program = [[YHCOpenGLProgram alloc] init];
[_program setVertexShader:@"Shader"];
[_program setFragmentShader:@"Shader"];
[_program addAttributeLocation:@"ATTRIBUTE_VERTEX" forAttribute:@"position"];
[_program compileAndLink];
attributes[ATTRIBUTE_VERTEX] = [_program getAttributeIDForIndex:@"ATTRIBUTE_VERTEX"];
//2、在鏈接之前 綁定 attribute (attribute location 綁定 必須在鏈接之前)
for (NSString *key in _attributes) {
YHCShaderAttribute *thisAttribute = [_attributes objectForKey:key];
glBindAttribLocation(_programId, thisAttribute.attributeId, [thisAttribute.attributeName UTF8String]);
}
//3、鏈接成功后,從 OpenGL 獲取uniform locations (該操作應(yīng)該在link之后進(jìn)行)
for (NSString *key in _uniforms) {
YHCShaderUniform *thisUniform = [_uniforms objectForKey:key];
[thisUniform setUniformLocation:glGetUniformLocation(_programId, [thisUniform.uniformName UTF8String])];
}
3、邏輯設(shè)計
沒有深入了解,參考原文,還有代碼里的GameLogic類。
4、文字顯示
加載一張含有多個文字的圖片,通過在上面選定區(qū)域來顯示文字(無法顯示中文)。

思考1:是否存在替代的做法?
5、旋轉(zhuǎn)部分魔方的動畫實(shí)現(xiàn)
不斷增大_sliceRotateAngle,當(dāng)_sliceRotateAngle>=90°之后,設(shè)置為_rotationState為ROTATE_NONE,并設(shè)置_currentSlice數(shù)組為-1,完成一次旋轉(zhuǎn)。
if(_rotationState == ROTATE_X_CLOCKWISE || _rotationState == ROTATE_Y_CLOCKWISE || _rotationState == ROTATE_Z_CLOCKWISE){
_sliceRotateAngle += rotationAngle;
}else if(_rotationState == ROTATE_X_ANTICLOCKWISE || _rotationState == ROTATE_Y_ANTICLOCKWISE || _rotationState == ROTATE_Z_ANTICLOCKWISE){
_sliceRotateAngle += -rotationAngle;
}else{
_sliceRotateAngle = 0;
}
6、旋轉(zhuǎn)整個魔方
監(jiān)聽touchesMove:withEvent:方法,通過locationWithUITouch:View得出點(diǎn)擊位置的Point,和touchesBegan開始記錄的_lastTouchPosition相比,得出繞X、Y軸旋轉(zhuǎn)的角度大小,直接對整個魔方的旋轉(zhuǎn)矩陣進(jìn)行操作。
當(dāng)初始點(diǎn)擊處不在魔方時,旋轉(zhuǎn)整個魔方。根據(jù)點(diǎn)擊初始點(diǎn)的x、y移動的距離,來決定饒Y、X軸的角度,注意是相反的。
- (void)rotateCubeAroundX:(float)x andY:(float)y
{
GLfloat totalXRotation = x * M_PI / 180.0f;
GLfloat totalYRotation = y * M_PI / 180.0f;
if (_rotationState == ROTATE_ALL) {
[MatrixTools applyRotation:_rotationMatrix x:totalXRotation y:totalYRotation z:0.0];
}
}
7、旋轉(zhuǎn)部分魔方判斷
獲取點(diǎn)擊的位置,重繪魔方到FBO,獲取點(diǎn)擊位置對應(yīng)的顏色,確定rotationState。
if (_isSelectMode) {
glVertexAttribPointer(ATTRIBUTE_COLOR, 4, GL_UNSIGNED_BYTE, 1, 0, cubes[i]._colors);
glEnableVertexAttribArray(ATTRIBUTE_COLOR); // 如果是選擇模式,用顏色
}else{
glVertexAttribPointer(ATTRIBUTE_TEXTURE_COORD, 2, GL_FLOAT, 0, 0, cubes[i]._textureCoords);
glEnableVertexAttribArray(ATTRIBUTE_TEXTURE_COORD); // 如果不上選擇模式,使用紋理坐標(biāo)
}
glReadPixels(point1.x,viewport[3]-point1.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixelColor);
思考2:為何不直接讀取屏幕的顏色?
總結(jié)
魔方的邏輯較復(fù)雜,著重了解魔方的顯示、旋轉(zhuǎn),點(diǎn)擊的拾取與判斷。
代碼地址在這里。
思考
1、替代的做法:文字直接添加到UILabel,UILabel繪制成紋理,再加載到OpenGL ES。
2、如果添加的是紋理,顏色變量無法攜帶位置信息。