首先,我们看效果,如图所示,我们在屏幕上展示一个金子塔形状,同时通过设置定时器让它进行旋转。
前言
之前的文章我们已经用GLSL绘制了一张图片,其实针对GLSL来说,前面的准备工作的都是类似的,而最主要的区别在于绘制工作和着色器(Shader)代码的绘制。如果大家有兴趣,可以看看之前写的GLSL应用-- 显示图形。
准备工作
定义一些属性
#import
#import "GLSLTestView.h"
#import "GLESMath.h"
@interface GLSLTestView ()
//在iOS和tvOS上绘制OpenGL ES内容的图层,继承自CALyayer
@property(nonatomic,strong)CAEAGLLayer *myLayer;
//上下文
@property(nonatomic,strong)EAGLContext *context;
//帧缓冲区
@property(nonatomic,assign)GLuint myFrameBuffer;
//渲染缓冲区
@property(nonatomic,assign)GLuint myRenderBuffer;
//程序
@property(nonatomic,assign)GLuint myProgram;
//顶点缓冲区标志
@property(nonatomic,assign)GLuint myVertices;
旋转需要的一些属性
@implementation GLSLTestView
{
float xDegree;
float yDegree;
float zDegree;
BOOL bX;
BOOL bY;
BOOL bZ;
NSTimer* myTimer;
}
流程
之前已经介绍过,对于使用GLSL而言,基本上的绘制流程是一样的,大家可以参考GLSL应用-- 显示图形。流程图如下:
那么,我们着重来看一下着色器代码和渲染代码
着色器(Shader)
顶点着色器(Vertex Shader)
//顶点坐标
attribute vec4 position;
//每个点的颜色
attribute vec4 postionColor;
//纹理坐标
attribute vec2 textCoordinate;
//投影矩阵
uniform mat4 projectionMatrix;
//模型视图矩阵
uniform mat4 modelviewMatrix;
//颜色
varying lowp vec4 varyColor;
//纹理
varying lowp vec2 varyTextCoord;
void main()
{
//将点的颜色传入片元着色器
varyColor = postionColor;
//将纹理坐标传入片元着色器
varyTextCoord = textCoordinate;
vec4 vPos;
//矩阵相乘
vPos = projectionMatrix * modelviewMatrix * position;
gl_Position = vPos;
}
-
vPos = projectionMatrix * modelviewMatrix * position
,由于我们的图形是要进行不停的旋转的,所以要对每次旋转后的图形进行重新绘制,所以需要(投影矩阵 * 模型视图矩阵 * 点坐标),一般叉乘顺序是投影矩阵、模型视图矩阵、点坐标。 -
varying
表示桥接属性,即可由顶点着色器传入片元着色器,注意:顶点着色器和片元着色器的属性名称要一致。
片元着色器(Fragment Shader)
precision highp float;
varying lowp vec4 varyColor;
varying lowp vec2 varyTextCoord;
//纹理
uniform sampler2D textCoordMap;
//透明度
uniform float alpha;
void main(){
// gl_FragColor = varyColor;
//计算纹素
vec4 textureColor = texture2D(textCoordMap,varyTextCoord);
//获取颜色
vec4 color = varyColor;
//颜色和纹理进行混合
vec4 tempColor = mix(textureColor,color,alpha);
gl_FragColor = tempColor;
}
-
precision highp float;
这句话的意思就是默认使用高精度,在片元着色器中最好加上,否则可能出现低精度导致图形异常。 -
mixmix(textureColor,color,alpha)
指的是混合,由于一个片元上有纹理,又有颜色,所以需要进行混合,而且2个元素都必须有透明度,否则只能显示一种。 - 纹素,值得是在片元着色器中每个纹理传入后每个像素点显示的颜色。
渲染
- 清除缓存、设置视口
//清理屏幕颜色
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
//14.开启剔除操作效果
glEnable(GL_CULL_FACE);
//设置视口
CGFloat scale = [[UIScreen mainScreen] scale];
//2.设置视口
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);
- 编译着色器(Shader)及程序(Program)
//获取顶点着色器和片元着色器位置
NSString* vertextFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
NSString* fragmentFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
//清除program缓存
if (self.myProgram) {
glDeleteProgram(self.myProgram);
self.myProgram = 0;
}
//加载程序
self.myProgram = [self loadVertexShader:vertextFile andFragShader:fragmentFile];
glLinkProgram(self.myProgram);
GLint linkStatus;
glGetProgramiv(self.myProgram, GL_LINK_STATUS, &linkStatus);
if (linkStatus == GL_FALSE) {
GLchar message[256];
glGetProgramInfoLog(self.myProgram, sizeof(message), 0, &message[0]);
NSString *errorMsg = [NSString stringWithUTF8String:message];
NSLog(@"%@",errorMsg);
return;
}
//使用程序
glUseProgram(self.myProgram);
加载shader和Program
#pragma -mark loadShader
-(GLuint)loadVertexShader:(NSString *)vertext andFragShader:(NSString *)fragment{
GLuint vShader,fShader;
//创建program
GLint program = glCreateProgram();
//2.编译顶点着色程序、片元着色器程序
//参数1:编译完存储的底层地址
//参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
//参数3:文件路径
[self compileShader:&vShader withType:GL_VERTEX_SHADER andPath:vertext];
[self compileShader:&fShader withType:GL_FRAGMENT_SHADER andPath:fragment];
//把编译好的程序附着到shader(shader -> program)
glAttachShader(program, vShader);
glAttachShader(program, fShader);
//删除shader 以免占用内存
glDeleteShader(vShader);
glDeleteShader(fShader);
return program;
}
#pragma -mark compileShader
-(void)compileShader:(GLuint *)shader withType:(GLenum)type andPath:(NSString *)path{
//1.读取文件路径字符串
NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//转化成c语言字符串
const GLchar *source = [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, nil);
//把着色器代码编译成目标代码
glCompileShader(*shader);
}
- 顶点数据
//(1)顶点数组 前3顶点值(x,y,z),后3位颜色值(RGB)
GLfloat attrArr[] =
{
-0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0,0.0, //左上0
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0,0.0 , //右上1
-0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0,1.0, //左下2
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0,1.0 , //右下3
0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.5,0.5, //顶点4
};
- 前三位是顶点坐标
- 中间三位是顶点对应的颜色值,那么从一个顶点到另一个顶点颜色值会发生变化
- 后两位是纹理坐标
索引绘图
索引绘图就是我们可以按照一定的顺序将顶点连接起来而绘制出图形。
上图就是这个金字塔在坐标系中的位置,如果按照一般的绘图顺序的话,就需要在顶点数组中将顶点以此存入数组中,然后进行连接。
但是在索引绘图中,我们只需要将顶点所代表的数字进行连接即可,如图,
//(2).索引数组
GLuint indices[] =
{
0, 3, 2,
0, 1, 3,
0, 2, 4,
0, 4, 1,
2, 3, 4,
1, 4, 3,
};
使用索引绘图
//15.使用索引绘图
/*
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);
- 拷贝数据到GPU
//判断顶点数据是否为空,如果为空则创建
if (self.myVertices == 0) {
glGenBuffers(1, &(_myVertices));
}
//绑定顶点数据
glBindBuffer(GL_ARRAY_BUFFER, _myVertices);
//把顶点数据拷贝到GPU上
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
- 将
myVertices
设置成全局变量的话,就不用每次创建 -
GL_DYNAMIC_DRAW
的解释如下:
/*
GL_STATIC_DRAW
GL_STATIC_READ
GL_STATIC_COPY
GL_DYNAMIC_DRAW
GL_DYNAMIC_READ
GL_DYNAMIC_COPY
GL_STREAM_DRAW
GL_STREAM_READ
GL_STREAM_COPY
”static“表示VBO中的数据将不会被改动(一次指定多次使用),
”dynamic“表示数据将会被频繁改动(反复指定与使用),
”stream“表示每帧数据都要改变(一次指定一次使用)。
”draw“表示数据将被发送到GPU以待绘制(应用程序到GL),
”read“表示数据将被客户端程序读取(GL到应用程序),”
*/
- 顶点数据、颜色数据、纹理数据和着色器进行绑定
//(3).将顶点数据通过myPrograme中的传递到顶点着色程序的position
//1.glGetAttribLocation,用来获取vertex attribute的入口的.
//2.告诉OpenGL ES,通过glEnableVertexAttribArray,
//3.最后数据是通过glVertexAttribPointer传递过去的。
//注意:第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
GLuint position = glGetAttribLocation(self.myProgram, "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(GL_FLOAT) * 8, (GLfloat *)NULL);
//10.--------处理顶点颜色值-------
//(1).glGetAttribLocation,用来获取vertex attribute的入口的.
//注意:第二参数字符串必须和shaderv.glsl中的输入变量:positionColor保持一致
GLuint postionColor = glGetAttribLocation(self.myProgram, "postionColor");
//打开通道
glEnableVertexAttribArray(postionColor);
//设置读取方式
//参数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(postionColor, 3, GL_FLOAT, GL_FALSE, sizeof(GL_FLOAT) * 8, (float *)NULL + 3);
//纹理
GLuint textcoordColor = glGetAttribLocation(self.myProgram, "textCoordinate");
glEnableVertexAttribArray(textcoordColor);
glVertexAttribPointer(textcoordColor, 2, GL_FLOAT, GL_FALSE, sizeof(GL_FLOAT) * 8, (float *)NULL + 6);
- 绑定前需要打开通道
glEnableVertexAttribArray
-
glGetAttribLocation (GLuint program, const GLchar* name)
中的name要和着色器程序一致
- 记载纹理
调用加载纹理方法
//加载纹理
[self loadTexture:@"jay"];
加载纹理方法
-(void)loadTexture:(NSString *)textureName{
CGImageRef image = [UIImage imageNamed:textureName].CGImage;
if (image == nil) {
NSLog(@"读取图片失败");
exit(1);
}
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
//获取图片字节数
GLubyte *data = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
//创建上下文
CGContextRef context = CGBitmapContextCreate(data, width, height, 8, width * 4, CGImageGetColorSpace(image), kCGImageAlphaPremultipliedLast);
//使用默认方式绘制图片
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
//释放
CGContextRelease(context);
//绑定纹理
glBindTexture(GL_TEXTURE_2D, 0);
//9.设置纹理属性
/*
参数1:纹理维度
参数2:线性过滤、为s,t坐标设置模式
参数3:wrapMode,环绕模式
*/
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);
//载入纹理2D数据
/*
参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数2:加载的层次,一般设置为0
参数3:纹理的颜色值GL_RGBA
参数4:宽
参数5:高
参数6:border,边界宽度
参数7:format
参数8:type
参数9:纹理数据
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLfloat)width, (GLfloat)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
//释放data
free(data);
}
- 设置纹理
//设置纹理采样
glUniform1i(glGetUniformLocation(self.myProgram, "textCoordMap"), 0);
- 设置透明度
//设置透明度
glUniform1f(glGetUniformLocation(self.myProgram, "alpha"), 0.3);
- 矩阵变换
//获取projectionMatrix和modelviewMatrix
GLuint projectionMatrixSlot = glGetUniformLocation(self.myProgram, "projectionMatrix");
GLuint modelviewMatrixSlot = glGetUniformLocation(self.myProgram, "modelviewMatrix");
//创建投影矩阵
float width = self.frame.size.width;
float height = self.frame.size.height;
KSMatrix4 _projectionMatrix;
//加载单元矩阵
ksMatrixLoadIdentity(&_projectionMatrix);
//获取纵横比
float aspect = width/height;
ksPerspective(&_projectionMatrix, 30, aspect, 5.0f, 20.0f);
//(4)将投影矩阵传递到顶点着色器
/*
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
参数列表:
location:指要更改的uniform变量的位置
count:更改矩阵的个数
transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
value:执行count个元素的指针,用来更新指定uniform变量
*/
glUniformMatrix4fv(projectionMatrixSlot, 1, GL_FALSE, (GLfloat*)&_projectionMatrix.m[0][0]);
//13.创建一个4 * 4 矩阵,模型视图矩阵
KSMatrix4 _modelViewMatrix;
//(1)获取单元矩阵
ksMatrixLoadIdentity(&_modelViewMatrix);
//(2)平移,z轴平移-10
ksTranslate(&_modelViewMatrix, 0.0, 0.0, -6.0);
//(3)创建一个4 * 4 矩阵,旋转矩阵
KSMatrix4 _rotationMatrix;
//(4)初始化为单元矩阵
ksMatrixLoadIdentity(&_rotationMatrix);
//(5)旋转
ksRotate(&_rotationMatrix, xDegree, 1.0, 0.0, 0.0); //绕X轴
ksRotate(&_rotationMatrix, yDegree, 0.0, 1.0, 0.0); //绕Y轴
ksRotate(&_rotationMatrix, zDegree, 0.0, 0.0, 1.0); //绕Z轴
//(6)把变换矩阵相乘.将_modelViewMatrix矩阵与_rotationMatrix矩阵相乘,结合到模型视图
ksMatrixMultiply(&_modelViewMatrix, &_rotationMatrix, &_modelViewMatrix);
//(7)将模型视图矩阵传递到顶点着色器
/*
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
参数列表:
location:指要更改的uniform变量的位置
count:更改矩阵的个数
transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
value:执行count个元素的指针,用来更新指定uniform变量
*/
glUniformMatrix4fv(modelviewMatrixSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);
-
KSMatrix4
是一套工具类所提供的类型 -
glUniformMatrix4fv(projectionMatrixSlot, 1, GL_FALSE, (GLfloat*)&_projectionMatrix.m[0][0]);
的目的是将投影矩阵、模型视图矩阵存入着色器
//参数1:location:指要更改的uniform变量的位置
//参数2:count:更改矩阵的个数
//参数3:transpose:是否要转置矩阵,并将它作为uniform变量的值。必须为GL_FALSE
//参数4:value:执行count个元素的指针,用来更新指定uniform变量
glUniformMatrix4fv(modelviewMatrixSlot, 1, GL_FALSE, (GLfloat*)&_modelViewMatrix.m[0][0]);
- 绘制
//绘制索引数组
glDrawElements(GL_TRIANGLES, sizeof(indices) / sizeof(indices[0]), GL_UNSIGNED_INT, indices);
//绘制
[self.context presentRenderbuffer:GL_RENDERBUFFER];
- 调用定时器进行重绘
- (IBAction)xClick:(id)sender {
//开启定时器
if (!myTimer) {
myTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(reDegree) userInfo:nil repeats:YES];
}
//更新的是X还是Y
bX = !bX;
}
- (IBAction)yClick:(id)sender {
//开启定时器
if (!myTimer) {
myTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(reDegree) userInfo:nil repeats:YES];
}
//更新的是X还是Y
bY = !bY;
}
- (IBAction)zClick:(id)sender {
//开启定时器
if (!myTimer) {
myTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(reDegree) userInfo:nil repeats:YES];
}
//更新的是X还是Y
bZ = !bZ;
}
-(void)reDegree
{
//如果停止X轴旋转,X = 0则度数就停留在暂停前的度数.
//更新度数
xDegree += bX * 5;
yDegree += bY * 5;
zDegree += bZ * 5;
//重新渲染
[self renderLayer];
}
源码地址如下金字塔