在上一篇中,渲染图片的时候,我们使用了 2 个三角形来绘制一个矩形,并且留下思考:能否用更少的顶点来渲染一个矩形
2 个三角形渲染一个矩形
GLfloat vertices[] = {
// 第一个三角形
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f, // 左上角
// 第二个三角形
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
观察顶点数组,我们可以发现,有两个顶点坐标是重复的,那么我们可以通过索引数组使用 4 个顶点来绘制一个矩形
索引绘制
索引数组是顶点数组的索引,把 squareVertexData 数组看成 4 个顶点,每个顶点会有 3 个 GLfloat 数据,索引从 0 开始。
索引缓冲对象(EBO)的工作方式正是这样的。和顶点缓冲对象一样,EBO 也是一个缓冲,它专门存储索引,OpenGL ES调用这些顶点的索引来决定该绘制哪个顶点。
具体操作
顶点定义
GLfloat vertices[] = {
0.5f, 0.5f, 0.0f, // 右上角
0.5f, -0.5f, 0.0f, // 右下角
-0.5f, -0.5f, 0.0f, // 左下角
-0.5f, 0.5f, 0.0f // 左上角
};
GLubyte indices[] = { // 注意索引从0开始!
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};
调用glDrawElements()渲染
// 6表示有6个索引数据,可以使用sizeof(indices)/sizeof(GLubyte)来确定
// glDrawElements 函数的原型为:glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices);
// 第一个参数 mode 为描绘图元的模式,其有效值为:GL_POINTS, GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP, GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN。
// 第二个参数 count 为顶点索引的个数也就是,type 是指顶点索引的数据类型,因为索引始终是正值,索引这里必须是无符号型的非浮点类型,因此只能是 GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, GL_UNSIGNED_INT 之一,为了减少内存的消耗,尽量使用最小规格的类型如 GL_UNSIGNED_BYTE。
// 第三个参数 indices 是存放顶点索引的数组。(indices 是 index 的复数形式,3D 里面很多单词的复数都挺特别的。)
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, indices);
渲染正方体
通过 GLKit 渲染一个正方体,并使其旋转
效果如下:

思路

使用 GLKit 进行图形变换、纹理贴图加载、深度测试,用GLKBaseEffect来管理纹理贴图和矩阵操作
具体实现
渲染正方体
之前,我们已经知道如何渲染一个面的矩形了,现在只需要将添加其余 6 个面的顶点坐标和纹理坐标,获取顶点数据
顶点缓存
将顶点数据 copy 到显存中,并开启顶点属性变量
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL);
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 3);
设置变换矩阵
设置着色器的投影矩阵和模型视图矩阵
CGSize size = self.view.bounds.size;
float aspect = fabs(size.width / size.height);
// 投影矩阵
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(90.0), aspect, 0.1f, 100.f);
projectionMatrix = GLKMatrix4Scale(projectionMatrix, 1.0f, 1.0f, 1.0f);
self.mEffect.transform.projectionMatrix = projectionMatrix;
// 平移矩阵
GLKMatrix4 modelViewMatrix = GLKMatrix4Translate(GLKMatrix4Identity, 0.0f, 0.0f, -2.0f);
self.mEffect.transform.modelviewMatrix = modelViewMatrix;
GLKMatrix4MakePerspective是透视投影变换
GLKMatrix4Translate是平移变换
- (void)update {
GLKMatrix4 modelViewMatrix = GLKMatrix4Translate(GLKMatrix4Identity, 0.0f, 0.0f, -2.0f);
// modelViewMatrix = GLKMatrix4Rotate(modelViewMatrix, self.mDegreeX, 1.0, 1.0, 1.0);
modelViewMatrix = GLKMatrix4RotateX(modelViewMatrix, self.mDegreeX);
modelViewMatrix = GLKMatrix4RotateY(modelViewMatrix, self.mDegreeX);
modelViewMatrix = GLKMatrix4RotateZ(modelViewMatrix, self.mDegreeX);
self.mEffect.transform.modelviewMatrix = modelViewMatrix;
}
在场景变换函数里面,更新着色器的模型视图矩阵
GLKMatrix4RotateX是绕 X 轴旋转
深度测试
在配置上下文时,需要设置深度测试的格式view.drawableDepthFormat = GLKViewDrawableDepthFormat24;,同时开启深度测试glEnable(GL_DEPTH_TEST);,
在渲染场景的回调中,需要增加清除深度测试的缓冲区GL_DEPTH_BUFFER_BIT
通过 Core Animation 实现正方体旋转
CALayer 有一个用来做 3D 变换的属性:transform,类型是CATransform3D
CATransform3D是一个矩阵,和 OpenGL ES 中的矩阵一样,都是 4*4 的矩阵。
例:旋转子视图-45度:
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, -(45.0f/180.0f*M_PI), 0.0f, 1.0f, 0.0f);
layerView.layer.transform = transform;
具体实现
为正方体创建一个固态的 3D 对象,这里用独立的 UIImageView 来加载纹理
- (void)addCubeWithCATransform3D:(CATransform3D)transform {
NSString *filePath = [[NSBundle mainBundle]pathForResource:@"cover" ofType:@"jpg"];
UIImageView *face = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
face.image = [UIImage imageWithContentsOfFile:filePath];
[self.container addSubview:face];
CGSize containerSize = self.container.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
face.layer.transform = transform;
}
平移矩阵
通过CATransform3DMakeTranslation和CATransform3DRotate实现 6 个面的矩阵
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DMakeTranslation(0, 0, 100);
[self addCubeWithCATransform3D:transform];
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addCubeWithCATransform3D:transform];
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addCubeWithCATransform3D:transform];
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addCubeWithCATransform3D:transform];
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addCubeWithCATransform3D:transform];
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addCubeWithCATransform3D:transform];
旋转矩阵
设置定时器,执行事件:更新sublayerTransform的旋转矩阵
static CGFloat angle = 1.0f;
double delayInSeconds = 0.1;
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC, 0.0);
dispatch_source_set_event_handler(timer, ^{
CATransform3D transform3d = self.container.layer.sublayerTransform;
transform3d = CATransform3DRotate(transform3d, angle/180.0f*M_PI, 1.0f, 1.0f, 1.0f);
self.container.layer.sublayerTransform = transform3d;
});
dispatch_resume(timer);
在这里我们需要使用sublayerTransform而不是直接把superLayer进行旋转
尽管 Core Animation 图层存在于 3D 空间内,但他们并不都是存在同一个 3D 空间。
每个图层的 3D 场景其实是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的 3D 场景,但当你倾斜这个图层,你会发现实际上这个 3D 场景仅仅是被绘制在图层的表面