我们上一篇中介绍了2D图形的绘制,那么今天来看一下3D图形的绘制。为了使3D效果更加明显,我们增加了旋转功能,因此就需要用到矩阵记录变化。除此之外,还添加了颜色与纹理的混合,并且改变了绘图方式:索引绘图
前言
OpenGL提供了一些绘图函数
。
到目前为止我们使用的glDrawArrays
绘图函数属于顺序绘制
。这意味着顶点缓冲区从指定的偏移量开始被扫描,每X(点为1,直线为2等)个顶点构成一个图元。这样使用起来非常方便,缺点是当多个图元共用一个顶点时,这个顶点必须在顶点缓冲区中出现多次。也就是说,这些顶点没有共享的概念。
而索引绘制
的函数glDrawElements
则提供这种共享机制。我们除了一个顶点缓存区外,还有一个索引缓存区用来存放顶点的索引值。索引缓存区的扫描和顶点缓存区类似,以每X个索引对应的顶点构成一个基本图元。共享机制在提高内存使用效率上非常重要,因为计算机中的绝大多数图形对象都是三角形网格构成的,这些三角形有很多都是共用顶点。
例如: 如果我们要绘制一个金字塔。
- 顺序绘制,需要传入18个顶点的信息
- 索引绘制,只需要传入5个顶点的信息
一、效果图
二、流程图
三、GLSL绘制3D与2D的主要区别
- 多了投影矩阵、模型视图矩阵
- 多了索引数组,并改变了绘制方式
- 多了一组颜色数据,为了做混合效果
- 因为是3D可旋转,所以要开启正背面剔除或者深度测试
四、代码部分
1、顶点着色器
//特意加的注释,真是项目中,切记不要写中文,避免不必要的错误
attribute vec4 position;//顶点数据
attribute vec4 positionColor;//顶点颜色
attribute vec2 textCoor;//纹理数据
uniform mat4 projectionMatrix;//投影矩阵
uniform mat4 modelViewMatrix;//模型视图矩阵
varying lowp vec2 vTextCoor;//传递给 片元着色器的 顶点颜色
varying lowp vec4 varyColor;//传递给 片元着色器的 纹理数据
void main()
{
vTextCoor = textCoor;
varyColor = positionColor;
vec4 vPos;
vPos = projectionMatrix * modelViewMatrix * position;
gl_Position = vPos;
}
2、片元着色器
//特意加的注释,真是项目中,切记不要写中文,避免不必要的错误
precision highp float;![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d7d6df3f338a45bb931d655c45fc6390~tplv-k3u1fbpfcp-zoom-1.image)//默认高精度,一定要写
varying lowp vec2 vTextCoor; //纹理坐标
varying lowp vec4 varyColor; //顶点颜色数据
uniform sampler2D colorMap; //纹理数据
void main()
{
//拿到每个像素的纹素
vec4 weakMask = texture2D(colorMap, vTextCoor);
//拿到颜色数据
vec4 mask = varyColor;
float alpha = 0.3;
//要想进行混合,需要半透明才可以,所以调整他们各自的透明度
vec4 tempColor = mask * (1.0 - alpha) + weakMask * alpha;
gl_FragColor = tempColor;
}
3、无区别部分
前面的步骤代码基本都一样,(只是声明多了点东西)
为了方便看出区别,从第6步分开了,不一样的代码放到第5部分
#import "MyView.h"
#import
//导入工具类
#import "GLESMath.h"
#import "GLESUtils.h"
@interface MyView()
{
//这里的东西是用来控制旋转操作的参数
float xDegree;
float yDegree;
float zDegree;
BOOL bX;
BOOL bY;
BOOL bZ;
NSTimer* myTimer;
BOOL isHybrid;
}
//EAGL提供的绘制表面
@property (nonatomic,strong) CAEAGLLayer *myEaglLayer;
//上下文
@property (nonatomic,strong) EAGLContext *myContext;
//渲染缓冲区
@property (nonatomic,assign) GLuint myColorRenderBuffer;
//帧缓冲区
@property (nonatomic,assign) GLuint myColorFrameBuffer;
//程序对象的id
@property (nonatomic,assign) GLuint myPrograme;
//顶点缓冲区id
@property (nonatomic , assign) GLuint myVertices;
@end
@implementation MyView
- (void)layoutSubviews
{
isHybrid = NO;
//1、设置图层
[self setUpLayer];
//2、设置上下文
[self setUpContext];
//3、清空缓冲区
[self deleteBuffers];
//4、设置renderBuffer
[self setUpRenderBuffer];
//5、设置frameBuffer
[self setUpframeBuffer];
//6、开始绘制
[self renderDraw];
//7、添加底部按钮,控制旋转、切换
[self setUpBottomButtons];
}
#pragma mark - 1、设置图层
-(void)setUpLayer{
//1、创建图层
self.myEaglLayer = (CAEAGLLayer *)self.layer;
self.myEaglLayer.opaque = YES;
//2、设置scale
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
//3、设置描述属性
self.myEaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
@false,kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,
nil];
}
+(Class)layerClass{
return [CAEAGLLayer class];
}
#pragma mark - 2、设置上下文
-(void)setUpContext{
//初始化上下文
EAGLContext *context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
//判断
if (!context) {
NSLog(@"上下文创建失败");
return;
}
//设置当前上下文
[EAGLContext setCurrentContext:context];
self.myContext = context;
}
#pragma mark - 3、清空缓冲区
-(void)deleteBuffers{
//清空渲染缓冲区
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
//清空帧缓冲区
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
#pragma mark - 4、设置renderBuffer
-(void)setUpRenderBuffer{
//定义一个缓冲区id
GLuint rBuffer;
//申请一个缓冲区
glGenRenderbuffers(1, &rBuffer);
self.myColorRenderBuffer = rBuffer;
//绑定渲染缓冲区
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
//把layer的存储 绑定到 渲染缓冲区。此处也把context与layer绑定在一起
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEaglLayer];
}
#pragma mark - 5、设置frameBuffer
-(void)setUpframeBuffer{
GLuint *fBuffer;
glGenFramebuffers(1, &fBuffer);
self.myColorFrameBuffer = fBuffer;
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
//把renderBuffer和frameBuffer绑定在一起.把renderBuffer绑定到GL_COLOR_ATTACHMENT0上
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
#pragma mark - 6、开始绘制
-(void)renderDraw{
//1、设置背景色&清空颜色缓冲区
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
//2、设置视口
CGFloat 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);
//3、读取2个着色器地址
NSString *vertexFile = [[NSBundle mainBundle] pathForResource:@"shader" ofType:@"vsh"];
NSString *fragmentFile = [[NSBundle mainBundle] pathForResource:@"shader" ofType:@"fsh"];
//4、编译加载shader,并附着到程序上,拿到程序id
//**注意** 有可能一个项目中有多个程序。严谨一点 先判断
if (self.myPrograme) {
glDeleteProgram(self.myPrograme);
self.myPrograme = 0;
}
self.myPrograme = [self loadShaderWithVertexFile:vertexFile andFragmentFile:fragmentFile];
//5、连接link
glLinkProgram(self.myPrograme);
//6、检验link
GLint linkStatus;
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
//定义一个c语言字符串接受失败信息,不知道信息有多大,尽量写大一点,这里写了512
GLchar message[512];
glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
//转成oc字符串并打印信息
NSString *messageString = [NSString stringWithUTF8String:message];
NSLog(@"link失败信息:%@",messageString);
return;
}
//7、使用program
glUseProgram(self.myPrograme);
5、有区别的部分
与上面部分 无缝衔接的看
//========================================================================================================
//**注意**| 这里开始就与之前加载2D图片有所区别了 |**注意**
//========================================================================================================
//8.创建顶点数组 & 索引数组
//(1)顶点数组 前3顶点值(x,y,z),中间3位颜色值(RGB),后面2位是纹理(s,t)
GLfloat attrArr[] =
{
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 1.0f,//左上
0.5f, 0.5f, 0.0f, 0.0f, 0.5f, 0.0f, 1.0f, 1.0f,//右上
-0.5f, -0.5f, 0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f,//左下
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.0f,//右下
0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.5f, 0.5f,//顶点
};
//(2).索引数组
GLuint indices[] =
{
0, 3, 2,
0, 1, 3,
0, 2, 4,
0, 4, 1,
2, 3, 4,
1, 4, 3,
};
//9、从数组copy到缓冲区
//1)因为用了定时器会不断刷新,先判断缓冲区是否为空,为空再申请id
if (self.myVertices == 0) {
glGenBuffers(1, &_myVertices);
}
//2)绑定到顶点缓冲区
glBindBuffer(GL_ARRAY_BUFFER, _myVertices);
//3)从内存copy到显存
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
//10、打开通道,传递顶点数据
GLint position = glGetAttribLocation(self.myPrograme, "position");
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*8, (float *)NULL + 0);
//11、打开通道,传递颜色数据
GLint positionColor = glGetAttribLocation(self.myPrograme, "positionColor");
glEnableVertexAttribArray(positionColor);
glVertexAttribPointer(positionColor, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*8, (float *)NULL + 3);
//开关控制是否混合纹理和颜色
if (isHybrid == YES) {
//12、打开通道,传递纹理数据
GLint textCoor = glGetAttribLocation(self.myPrograme, "textCoor");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*8, (float *)NULL + 6);
//13、加载纹理
[self setUpTexture:@"mark.jpeg"];
//14、设置纹理采样器 sampler2D 整个OpenGL中能有16个纹理id,默认0是第一个
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);
}
//15、先找到program中投影矩阵、视图模型矩阵的地址。如果找不到返回-1 表示没有找到这2个对象
GLuint projectionMatrixSlot = glGetUniformLocation(self.myPrograme, "projectionMatrix");
GLuint modelViewMatrixSlot = glGetUniformLocation(self.myPrograme, "modelViewMatrix");
//16、投影矩阵 :决定了是正投影还是透视投影
//1)创建一个4*4的投影矩阵
KSMatrix4 _projectionMatrix;
//2)获取单元矩阵
ksMatrixLoadIdentity(&_projectionMatrix);
//3)计算纵横比
float width = self.frame.size.width;
float height = self.frame.size.height;
float aspect = width / height;
//4)获取投影矩阵
ksPerspective(&_projectionMatrix, 30.0, aspect, 5.0f, 30.0f);
//5)把投影矩阵传递到顶点着色器
/*
参数列表:
location:指要更改的uniform变量的位置
count:更改矩阵的个数
transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
value:执行count个元素的指针,用来更新指定uniform变量
*/
glUniformMatrix4fv(projectionMatrixSlot, 1, GL_FALSE, (GLfloat *)&_projectionMatrix.m[0][0]);
//17、视图模型矩阵 : 决定了 金字塔是怎么移动旋转的
//1)创建一个4*4的矩阵
KSMatrix4 _modelViewMatrix;
//2)获取单元矩阵
ksMatrixLoadIdentity(&_modelViewMatrix);
//3)平移,z轴移动-10 相当于OpenGL中setUpRC()中,设置物体位置一样
ksTranslate(&_modelViewMatrix, 0.0, 0.0, -10.0);
//4)创建一个旋转矩阵 这里相当于OpenGL中RenderSence()中,记录模型变化的地方一样
KSMatrix4 _rotationMatrix;
//5)加载一个单元矩阵
ksMatrixLoadIdentity(&_rotationMatrix);
//6)旋转
ksRotate(&_rotationMatrix, xDegree, 1.0, 0.0, 0.0);
ksRotate(&_rotationMatrix, yDegree, 0.0, 1.0, 0.0);
ksRotate(&_rotationMatrix, zDegree, 0.0, 0.0, 1.0);
//7)把最终的 旋转矩阵 和 模型视图矩阵 相乘,结果放到模型视图矩阵中
ksMatrixMultiply(&_modelViewMatrix, &_rotationMatrix, &_modelViewMatrix);
//8)把模型视图矩阵传递到顶点着色器中
glUniformMatrix4fv(modelViewMatrixSlot, 1, GL_FALSE, &_modelViewMatrix.m[0][0]);
//18、开启正背面剔除
glEnable(GL_CULL_FACE);
//18、索引绘图
/*
void glDrawElements(GLenum mode,GLsizei count,GLenum type,const GLvoid * indices);
参数列表:
mode:要呈现的画图的模型
GL_POINTS
GL_LINES
GL_LINE_LOOP
GL_LINE_STRIP
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
count:绘图个数
type:类型
GL_BYTE
GL_UNSIGNED_BYTE
GL_SHORT
GL_UNSIGNED_SHORT
GL_INT
GL_UNSIGNED_INT
indices:绘制索引数组
*/
glDrawElements(GL_TRIANGLES, sizeof(indices) / sizeof(indices[0]), GL_UNSIGNED_INT, indices);
//19、渲染到屏幕上
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}
#pragma mark -第6步需要的 封装方法
#pragma mark - 编译加载着色器shader,并且与程序附着,拿到最后的程序id
-(GLuint)loadShaderWithVertexFile:(NSString *)vertexFile andFragmentFile:(NSString *)fragmentFile
{
//定义着色器对象
GLuint verShader,fragShader;
//创建程序对象
GLuint program = glCreateProgram();
//编译着色器
[self compileShader:&verShader type:GL_VERTEX_SHADER filePath:vertexFile];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER filePath:fragmentFile];
//拿到了着色器对象,附着到程序上
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
//附着完成,拿到了程序id,着色器就可以释放了
glDeleteShader(verShader);
glDeleteShader(fragShader);
return program;
}
#pragma mark - 编译着色器
-(void)compileShader:(GLuint *)shader type:(GLenum)type filePath:(NSString *)filePath{
//1 读取着色器的路径,转换成c语言字符串
NSString *pathString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
const GLchar *source = (GLchar *)[pathString UTF8String];
//2、根据类型创建着色器
*shader = glCreateShader(type);
//3、把着色器字符串的源码,放到着色器对象里面
glShaderSource(*shader, 1, &source, NULL);
//4、编译(把我们写的着色器源代码 编译成 目标代码,也就是shader就完成了)
glCompileShader(*shader);
}
#pragma mark - 加载纹理,解压图片 === 这里也是图片解压缩的原理
-(GLuint)setUpTexture:(NSString *)imageName{
//1、纹理解压缩
CGImageRef spriImage = [UIImage imageNamed:imageName].CGImage;
//2、判断图片有没有拿到
if (!spriImage) {
NSLog(@"图片没有拿到");
exit(1);
}
//3、创建一个上下文
/*
CGBitmapContextCreate
参数1:data,指向要渲染的绘制图像的内存地址
参数2:width,bitmap的宽度,单位为像素
参数3:height,bitmap的高度,单位为像素
参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
参数6:colorSpace,bitmap上使用的颜色空间 kCGImageAlphaPremultipliedLast:RGBA
*/
//1)拿到图片的宽高
size_t width = CGImageGetWidth(spriImage);
size_t height = CGImageGetHeight(spriImage);
//2)拿到图片的大小,也就是纹理数据
GLubyte *spriData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
//3)创建上下文
CGContextRef spriContext = CGBitmapContextCreate(spriData, width, height, 8, width*4, CGImageGetColorSpace(spriImage), kCGImageAlphaPremultipliedLast);
//4、将图片绘制出来
//1)拿到坐标
CGRect rect = CGRectMake(0, 0, width, height);
//2)使用默认方式绘制
CGContextDrawImage(spriContext, rect, spriImage);
//3)翻转策略
CGContextTranslateCTM(spriContext, rect.origin.x, rect.origin.y);
CGContextTranslateCTM(spriContext, 0, rect.size.height);
CGContextScaleCTM(spriContext, 1.0, -1.0);
CGContextTranslateCTM(spriContext, -rect.origin.x, -rect.origin.y);
CGContextDrawImage(spriContext, rect, spriImage);
//5、绘制完成,释放上下文
CGContextRelease(spriContext);
//6、绑定纹理id(如果只有一个纹理,直接使用0就行了)
glBindTexture(GL_TEXTURE_2D, 0);
//7、设置纹理的缩放和环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
//8、载入2D纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (float)width, (float)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriData);
//9、释放纹理数据
free(spriData);
return 0;
}
#pragma mark - 7、添加底部按钮,控制旋转、切换
-(void)setUpBottomButtons{
NSArray *array = @[@"x",@"y",@"z",@"混合"];
#define AppViewW 50
#define AppViewH 50
#define KColCount 4 //每行的个数
//每个Button的起始位置
#define KStartX 30
#define KStartY self.frame.size.height - 80
//两个Button之间的间距
#define SpaceX 30
#define SpaceY 0
for (int i=0; i