今天来探究一下OpenGL ES如何编写顶点着色器和片元着色器的代码,以及如何编译、链接和使用。
一、编写顶点着色器和片元着色器代码
第一步:创建Xcode工程,并创建shaderv.vsh和shaderf.fsh文件
选择File->New->File->Empty
将文件重命名为.vsh后缀的文件,此文件的.vsh后缀名不是给xcode标记为什么文件,而是告诉开发者,此文件是顶点着色器文件。
同样的,生成.fsh后缀的片元着色器文件
第二步:编写顶点着色器代码和片元着色器代码
顶点着色器代码
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = textCoordinate;
gl_Position = position;
}
- attribute 是修饰类型关键字,只能在顶点着色器代码中使用,用来传递数据; vec4 表示四维向量数据类型;position表示变量名称,用来传递顶点坐标。
- vec2 表示二维向量数据类型,textCoordinate表示变量名称,用来传递纹理坐标。纹理坐标不会在顶点着色器中使用,这里只是通过顶点着色器桥接给片元着色器。
- varying 是修饰类型关键字,修饰的变量要从顶点着色器传递到片元着色器;lowp也是修饰类型关键字,表示修饰的vec2使用低精度;varyTextCoord表示要传递片元着色器的纹理坐标
- varyTextCoord = textCoordinate;表示把外面传进来的纹理坐标传递到片元着色器去。
- gl_Position = position;表示将外面传进来的顶点坐标赋值给内置变量gl_Position,得到顶点坐标的处理结果。
片元着色器代码
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
gl_FragColor = texture2D(colorMap, varyTextCoord);
}
- precision 修饰类型关键字,highp 修改高精度,用他们来修饰float表示这个文件的float数据使用高精度。
- varying lowp vec2 varyTextCoord;用来接收顶点着色器传递过来的纹理坐标,这里的定义必须和顶点着色器中的定义完全一样,不然数据就传递不过来。
- uniform修改类型关键字,smampler2D 修饰的colorMap表示传递进来的纹理数据。
- gl_FragColor 是片元着色器的内置变量,接收处理后的片元数据。texture2D函数将纹理数据映射到纹理坐标上。
二、编译、链接和使用着色器代码
首先编写加载着色器代码
-(GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
//1.定义2个零时着色器对象
GLuint verShader, fragShader;
//创建program
GLint program = glCreateProgram();
//2.编译顶点着色程序、片元着色器程序
//参数1:编译完存储的底层地址
//参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
//参数3:文件路径
[self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
//3.创建最终的程序
glAttachShader(program, verShader);
glAttachShader(program, fragShader);
//4.释放不需要的shader
glDeleteShader(verShader);
glDeleteShader(fragShader);
return program;
}
编译着色器的代码
//编译shader
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
//1.读取文件路径字符串
NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar *)[content UTF8String];
//2.创建一个shader(根据type类型)
*shader = glCreateShader(type);
//3.将着色器源码附加到着色器对象上。
//参数1:shader,要编译的着色器对象 *shader
//参数2:numOfStrings,传递的源码字符串数量 1个
//参数3:strings,着色器程序的源码(真正的着色器程序源码)
//参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
glShaderSource(*shader, 1, &source,NULL);
//4.把着色器源代码编译成目标代码
glCompileShader(*shader);
}
顶点着色器代码和片元着色器代码不像.m文件编写的代码一样,它其实是字符串,我们要想使用它,要进行编译。
编译着色器代码
-(void)comileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
//读取文件路径
NSString *content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar *source = (GLchar *)[content UTF8String];
//创建着色器对象
*shader = glCreateShader(type);
//将着色器代码附着到着色器对象上
/*
//参数1:shader,要编译的着色器对象 *shader
//参数2:numOfStrings,传递的源码字符串数量 1个
//参数3:strings,着色器程序的源码(真正的着色器程序源码)
//参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
*/
glShaderSource(*shader, 1, &source, NULL);
//编译着色器代码
glCompileShader(*shader);
}
我们已经写好了着色器代码的编译程序,那我们如何使用呢?有以下几个步骤,这几个步骤基础是确定的,不会变化太多
- 1.设置图层
- 2.设置上下文
- 3.清空缓冲区
- 4.设置RenderBuffer
- 5.设置FrameBuffer
- 6.开始使用
下面我们一步一步的实现
导入头文件
#import <OpenGLES/ES2/gl.h>
定义属性
//在iOS和tvOS上绘制OpenGL ES内容的图层,继承与CALayer
@property (nonatomic, strong) CAEAGLLayer *myEagLayer;
//上下文
@property (nonatomic, strong) EAGLContext *myContext;
@property (nonatomic, assign) GLuint myColorRenderBuffer;
@property (nonatomic, assign) GLuint myColorFrameBuffer;
@property (nonatomic, assign) GLuint myPrograme;
1.设置图层
//MARK: 1.设置图层
-(void)setupLayer{
// 重写layerClass,将CCView返回的图层从CALayer替换成CAEAGLLayer
self.myEagLayer = (CAEAGLLayer *)self.layer;
//设置scale
[self setContentScaleFactor:[UIScreen mainScreen].scale];
//设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
}
注意,必须重写如下方法,否则self.layer无法强转成CAEAGLLayer
+(Class)layerClass{
return [CAEAGLLayer class];
}
2.设置上下文
-(void)setupContext{
//创建上下文
EAGLContext *context = [[EAGLContext alloc] initWithAPI:(kEAGLRenderingAPIOpenGLES2)];
if (!context) {
NSLog(@"Create context failed!");
return;
}
//设置上下文
BOOL result = [EAGLContext setCurrentContext:context];
if (result == NO) {
NSLog(@"setCurrentContext failed!");
return;
}
self.myContext = context;
}
3.清空缓存区
-(void)deleteRenderAndFrameBuffer{
/*
buffer分为frame buffer 和 render buffer2个大类。
其中frame buffer 相当于render buffer的管理者。
frame buffer object即称FBO。
render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。
*/
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
glDeleteFramebuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;
}
4.设置RenderBuffer
-(void)setupRenderBuffer{
//定义一个bufferID
GLuint buffer;
//申请一个bufferID
glGenRenderbuffers(1, &buffer);
self.myColorRenderBuffer = buffer;
//将bufferID绑定到renderBuffer
glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
//调用上下文,将图层绑定到RenderBuffer
[self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
RenderBuffer必须要通过上下文与FrameBuffer进行绑定,由FrameBuffer来管理
5.设置FrameBuffer
-(void)setupFrameBuffer{
//定义一个bufferID
GLuint buffer;
//申请一个bufferID;
glGenFramebuffers(1, &buffer);
self.myColorFrameBuffer = buffer;
//将bufferID绑定到FrameBuffer
glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
//将RendBuffer绑定到GL_COLOR_ATTACHMENT0上,并使用FrameBuffer来管理
/*
生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
*/
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}
6.开始绘制
首先实现从图片上加载纹理
-(GLuint)setupTexture:(NSString *)fileName{
//将UIImage转换成CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
//判断图片是否获取成功
if (spriteImage == nil) {
NSLog(@"Failed to load image %@",fileName);
exit(1);
}
//读取图片信息
//宽高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
//字节数,宽X高X4
GLubyte *spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
//创建上下文
/*
参数1:data,指向要渲染的绘制图像的内存地址
参数2:width,bitmap的宽度,单位为像素
参数3:height,bitmap的高度,单位为像素
参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
参数6:colorSpace,bitmap上使用的颜色空间
kCGImageAlphaPremultipliedLast:RGBA
*/
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
//在CGContextRef上将图片绘制出来
CGRect rect = CGRectMake(0, 0, width, height);
//使用默认的方式绘制
CGContextDrawImage(spriteContext, rect, spriteImage);
//释放上下文
CGContextRelease(spriteContext);
//绑定纹理到默认Id,默认打为ID为0的纹理
glBindTexture(GL_TEXTURE_2D, 0);
//设置纹理参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//GL_LINEAR线性过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);//邻近过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
//载入纹理数据
float fw = width,fh = height;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
//释放
free(spriteData);
//这里返回0,表示返回默认打开的纹理Id
return 0;
}
-(void)renderLayer{
//清空颜色
glClearColor(0.3f, 0.45f, 0.5f, 1.0f);
//清空颜色缓冲区
glClear(GL_COLOR_BUFFER_BIT);
//设置视口大小
CGFloat scale = [UIScreen mainScreen].scale;
//UIscreen获得的是逻辑大小 openGL的作用范围是像素 所以要乘缩放因子
glViewport(self.frame.origin.x*scale, self.frame.origin.y*scale, self.frame.size.width*scale, self.frame.size.height*scale);
//读取顶点着色器和片元着色器程序
NSString *vertFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
//加载着色器
self.myPrograme = [self loadShaders:vertFile withFrag:fragFile];
//链接着色器代码
glLinkProgram(self.myPrograme);
//获取链接结果
GLint linkStatus;
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
//获取错误信息,并打印
GLchar message[512];
glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
NSString *msgString = [NSString stringWithUTF8String:message];
NSLog(@"Programe Link Error:%@",msgString);
return;
}
NSLog(@"Program link Success!");
//使用program
glUseProgram(self.myPrograme);
//设置顶点坐标和纹理坐标
GLfloat attrArr[] = {
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
0.5f, 0.5f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
};
//将顶点数据和纹理数据copy到缓冲区
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
//将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
//把顶点数据从CPU内存复制到GPU上
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
//8.将顶点数据通过myPrograme中的传递到顶点着色程序的position
//1.glGetAttribLocation,用来获取vertex attribute的入口的.
//2.告诉OpenGL ES,通过glEnableVertexAttribArray,
//3.最后数据是通过glVertexAttribPointer传递过去的。
//将顶点坐标通过myPrograme传递到顶点着色器的position上
GLuint position = glGetAttribLocation(self.myPrograme, "position");
//打开读取数据的通道
glEnableVertexAttribArray(position);
//设置读取的格式
/**
//参数1:index,顶点数据的索引
//参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
//参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
//参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
//参数5:stride,连续顶点属性之间的偏移量,默认为0;
//参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
*/
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
//设置纹理数据
//(1).glGetAttribLocation,用来获取vertex attribute的入口的.
//注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
GLuint textCoord = glGetAttribLocation(self.myPrograme, "textCoordinate");
//置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(textCoord);
//(3).设置读取方式
//参数1:index,顶点数据的索引
//参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
//参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
//参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
//参数5:stride,连续顶点属性之间的偏移 量,默认为0;
//参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
glVertexAttribPointer(textCoord, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL+3);
//加载纹理
[self setupTexture:@"kunkun"];
//设置纹理采样器
glUniform1i(self.myPrograme, "colorMap");
// 绘制
glDrawArrays(GL_TRIANGLES, 0, 6);
//从渲染缓冲区显示到屏幕上
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];
}
最后重写layoutSubViews
-(void)layoutSubviews{
//1.设置图层
[self setupLayer];
//2.设置上下文
[self setupContext];
//3.清空缓冲区
[self deleteRenderAndFrameBuffer];
//4.设置RenderBuffer
[self setupRenderBuffer];
//5.设置FrameBuffer
[self setupFrameBuffer];
//6.开始使用
[self renderLayer];
}
这样我们就成功编译、链接并使用了顶点着色器和片元着色器的代码来加载一张图片,效果如下:
可是图片居然是倒过来的,下面我们来解决纹理倒置的问题
解决纹理倒置的几个方法
将顶点坐标翻转180度
1.修改顶点着色器,传一个旋转矩阵
attribute vec4 position;
attribute vec2 textCoordinate;
uniform mat4 rotateMatrix;
varying lowp vec2 varyTextCoord;
void main(){
varyTextCoord = textCoordinate;
vec4 vPos = position;
vPos = vPos * rotateMatrix;
gl_Position = vPos;
}
2.实现旋转矩阵,并将其传递给顶点着色器
/*
注意,想要获取shader里面的变量,这里记得要在glLinkProgram后面,后面,后面!
*/
-(void)rotateTextureImage{
//思路:将顶点旋转180度
//获取旋转的弧度
float radius = 180.0 * M_PI / 180.0;
//得到对应的sin和cos
float s = sin(radius);
float c = cos(radius);
/*
OpenGL ES是列向量,如:(x,
y,
z,
a)
要想使得它乘以一个矩阵后x,y,变化,而z,a不变,根据矩阵的乘法原则,则这个矩阵要符合如下结构
{ i,j,0,0
w,v,0,0
0,0,1,0
0,0,0,1
}
*/
GLfloat zRotation[16] = {
c,-s,0,0,
s,c,0,0,
0,0,1,0,
0,0,0,1
};
//拿到着色器传递旋转矩阵的通道,"rotateMatrix"就是shaderv.vsh文件里面的“uniform mat rotateMatrix”
GLuint rotate = glGetUniformLocation(self.myPrograme, "rotateMatrix");
//设置旋转矩阵
/*
location : 对于shader 中的ID
count : 个数
transpose : 转置
value : 指针
*/
glUniformMatrix4fv(rotate, 1, GL_FALSE, zRotation);
}
3.在绘制之前调用上面方法
-(void)renderLayer{
...
[self rotateTextureImage];
//绘制
glDrawArrays(GL_TRIANGLES, 0, 6);
...
}
这种方式虽然能解决问题,但是每次渲染时都要对所有的顶点进行一次旋转,增加了大量的计算,而且修改顶点着色器的代码,顶点着色器的代码应该逻辑越简单越好,计算越少越好!
获取纹理数据时将纹理进行旋转,得到正纹理
在绘制之前,先平移,再翻转,再平移
-(GLuint)setupTexture:(NSString *)fileName{
...
//解决纹理倒置
CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
CGContextTranslateCTM(spriteContext, 0, rect.size.height);
CGContextScaleCTM(spriteContext, 1.0, -1.0);
CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
//使用默认的方式绘制
CGContextDrawImage(spriteContext, rect, spriteImage);
...
}
以上四个步骤对应如下图:
先将纹理平移至要显示的位置,再沿y轴方向平移图片的高度,再等比例缩放,缩放时1代表等比例,-1翻转,最后再平称到显示的位置上。
在片元点着色器中,将纹理坐标进行翻转
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main()
{
//gl_FragColor = texture2D(colorMap, varyTextCoord);
gl_FragColor = texture2D(colorMap, vec2(varyTextCoord.x,1.0-varyTextCoord.y));
}
在顶点着色器中,将纹理坐标进行翻转
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main()
{
//varyTextCoord = textCoordinate;
varyTextCoord = vec2(textCoordinate.x,1.0-textCoordinate.y);
gl_Position = position;
}
绘制的时候,对纹理坐标进行翻转
//6.开始绘制
-(void)renderLayer{
...
//6.设置顶点、纹理坐标
//前3个是顶点坐标,后2个是纹理坐标
// GLfloat attrArr[] =
// {
// 0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
// -0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
// -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
//
// 0.5f, 0.5f, -1.0f, 1.0f, 1.0f,
// -0.5f, 0.5f, -1.0f, 0.0f, 1.0f,
// 0.5f, -0.5f, -1.0f, 1.0f, 0.0f,
// };
GLfloat attrArr[] =
{
0.5f, -0.5f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, -1.0f, 0.0f, 1.0f,
0.5f, 0.5f, -1.0f, 1.0f, 0.0f,
-0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -1.0f, 1.0f, 1.0f,
};
...
}
代码下载地址:链接: pan.baidu.com/s/15LqW8KZw… 密码: w4so