前面的文章都是绘制实实在在的图形的,在OpenGL中,我们还可以使用纹理图片来渲染图形,使用图片可以让描绘出来的物体更加真实也可以让我们的开发更加简单。
资料:http://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/06%20Textures/ 。
接下来我们直接开始代码书写:
1.开始之前,我们把工具类GLESUtils优化一下,使之能直接返回我们需要的program。用了这么久,希望你自己也能封装。
修改.h
#import
#include
@interface GLESUtils : NSObject
// Create a shader object, load the shader source string, and compile the shader.
//
+(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString;
+(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath;
//直接返回program
+(GLuint)loadProgram:(NSString *)vertexShaderFilepath withFragmentShaderFilepath:(NSString *)fragmentShaderFilepath;
@end
修改.m
#import "GLESUtils.h"
@implementation GLESUtils
+(GLuint)loadShader:(GLenum)type withFilepath:(NSString *)shaderFilepath
{
NSError* error;
NSString* shaderString = [NSString stringWithContentsOfFile:shaderFilepath
encoding:NSUTF8StringEncoding
error:&error];
if (!shaderString) {
NSLog(@"Error: loading shader file: %@ %@", shaderFilepath, error.localizedDescription);
return 0;
}
return [self loadShader:type withString:shaderString];
}
+(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString
{
// Create the shader object
GLuint shader = glCreateShader(type);
if (shader == 0) {
NSLog(@"Error: failed to create shader.");
return 0;
}
// Load the shader source
const char * shaderStringUTF8 = [shaderString UTF8String];
glShaderSource(shader, 1, &shaderStringUTF8, NULL);
// Compile the shader
glCompileShader(shader);
// Check the compile status
GLint compiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );
if (infoLen > 1) {
char * infoLog = malloc(sizeof(char) * infoLen);
glGetShaderInfoLog (shader, infoLen, NULL, infoLog);
NSLog(@"Error compiling shader:\n%s\n", infoLog );
free(infoLog);
}
glDeleteShader(shader);
return 0;
}
return shader;
}
+(GLuint)loadProgram:(NSString *)vertexShaderFilepath withFragmentShaderFilepath:(NSString *)fragmentShaderFilepath
{
// Load the vertex/fragment shaders
GLuint vertexShader = [self loadShader:GL_VERTEX_SHADER
withFilepath:vertexShaderFilepath];
if (vertexShader == 0)
return 0;
GLuint fragmentShader = [self loadShader:GL_FRAGMENT_SHADER
withFilepath:fragmentShaderFilepath];
if (fragmentShader == 0) {
glDeleteShader(vertexShader);
return 0;
}
// Create the program object
GLuint programHandle = glCreateProgram();
if (programHandle == 0)
return 0;
glAttachShader(programHandle, vertexShader);
glAttachShader(programHandle, fragmentShader);
// Link the program
glLinkProgram(programHandle);
// Check the link status
Glint linked;
glGetProgramiv(programHandle, GL_LINK_STATUS, &linked);
if (!linked) {
GLint infoLen = 0;
glGetProgramiv(programHandle, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1){
char * infoLog = malloc(sizeof(char) * infoLen);
glGetProgramInfoLog(programHandle, infoLen, NULL, infoLog);
NSLog(@"Error linking program:\n%s\n", infoLog);
free(infoLog);
}
glDeleteProgram(programHandle );
return 0;
}
// Free up no longer needed shader resources
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return programHandle;
}
2.我们的项目需要返璞归真,重新写一个
1).创建项目
2).新建MyGLView(实现layerClass、initWithFrame、setupLayer、setupContext、setupRenderBuffer、setupFrameBuffer、setupProgram、render这些函数)
做完,.m应该是这样的:
#import "MyGLView.h"
#import "GLESUtils.h"
#import
@interface MyGLView ()
{
CAEAGLLayer *_eaglLayer; //OpenGL内容只会在此类layer上描绘
EAGLContext *_context; //OpenGL渲染上下文
GLuint _renderBuffer; //
GLuint _frameBuffer; //
GLuint _programHandle;
GLuint _positionSlot; //顶点槽位
GLuint _colorSlot; //颜色槽位
}
@end
@implementation MyGLView
+(Class)layerClass{
//OpenGL内容只会在此类layer上描绘
return [CAEAGLLayer class];
}
-(instancetype)initWithFrame:(CGRect)frame{
if (self==[super initWithFrame:frame]) {
[self setupLayer];
[self setupContext];
[self setupRenderBuffer];
[self setupFrameBuffer];
[self setupProgram]; //配置program
[self render];
}
return self;
}
- (void)setupLayer
{
_eaglLayer = (CAEAGLLayer*) self.layer;
// CALayer 默认是透明的,必须将它设为不透明才能让其可见,性能最好
_eaglLayer.opaque = YES;
// 设置描绘属性,在这里设置不维持渲染内容以及颜色格式为 RGBA8
_eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];
}
- (void)setupContext {
// 指定 OpenGLES 渲染API的版本,在这里我们使用OpenGLES 3.0,由于3.0兼容2.0并且功能更强,为何不用更好的呢
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES3;
_context = [[EAGLContext alloc] initWithAPI:api];
if (!_context) {
NSLog(@"Failed to initialize OpenGLES 3.0 context");
}
// 设置为当前上下文
[EAGLContext setCurrentContext:_context];
}
-(void)setupRenderBuffer{
glGenRenderbuffers(1, &_renderBuffer); //生成和绑定render buffer的API函数
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//为其分配空间
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
}
-(void)setupFrameBuffer{
glGenFramebuffers(1, &_frameBuffer); //生成和绑定frame buffer的API函数
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
//将renderbuffer跟framebuffer进行绑定
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
}
- (void)setupProgram
{
// Load shaders
//
NSString * vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader"
ofType:@"gals"];
NSString * fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader"
ofType:@"gals"];
_programHandle = [GLESUtils loadProgram:vertexShaderPath withFragmentShaderFilepath:fragmentShaderPath];
glUseProgram(_programHandle);
// Get attribute slot from program
//
_positionSlot = glGetAttribLocation(_programHandle, "vPosition");
}
-(void)render
{
//设置清屏颜色,默认是黑色,如果你的运行结果是黑色,问题就可能在这儿
glClearColor(0.3, 0.5, 0.8, 1.0);
/*
glClear指定清除的buffer
共可设置三个选项GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT
也可组合如:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
这里我们只用了color buffer,所以只需清除GL_COLOR_BUFFER_BIT
*/
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST); //添加
// Setup viewport
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
[_context presentRenderbuffer:_renderBuffer];
}
@end
3).创建顶点和片元着色器脚本文件。
由于要使用纹理,我们顶点着色器脚本VertexShader.glsl需改写为:
attribute vec4 vPosition;
attribute vec2 TexCoordIn;
varying vec2 TexCoordOut;
void main(void)
{
gl_Position = vPosition;
TexCoordOut = TexCoordIn;
}
其中TexCoordIn为传入进来的纹理坐标,TexCoordOut为要传入到片元着色器中的纹理坐标。也就是把传入进来的纹理坐标TexCoordIn传入到片元着色器以供处理。
FragmentShader.glsl改为:
uniform sampler2D ourTexture;
varying lowp vec2 TexCoordOut;
void main()
{
gl_FragColor = texture2D(ourTexture, TexCoordOut);
}
以上varying的使用就是保证TexCoordOut的唯一性,有前面的基础应该很容易理解
uniform sampler2D ourTexture; //这句代码的意义是链接的采样纹理常量
gl_FragColor = texture2D(ourTexture, TexCoordOut); //这句代码表示以纹理坐标TexCoordOut来采样ourTexture当做像素的颜色
4).使用纹理
根据我们glsl脚本,我们在项目中需要新定义两个新变量:
GLuint _texCoordSlot; //纹理坐标槽位
GLuint _ourTextureSlot; //纹理对象槽位
在MyGLView.m里
@interface MyGLView ()
{
CAEAGLLayer *_eaglLayer; //OpenGL内容只会在此类layer上描绘
EAGLContext *_context; //OpenGL渲染上下文
GLuint _renderBuffer; //
GLuint _frameBuffer; //
GLuint _programHandle;
GLuint _positionSlot; //顶点槽位
GLuint _texCoordSlot; //纹理坐标槽位
GLuint _ourTextureSlot; //纹理对象槽位
}
然后我们在设置着色器程序- (void)setupProgram()方法里获取槽位值:
- (void)setupProgram
{
// Load shaders
//
NSString * vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader"
ofType:@"gals"];
NSString * fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader"
ofType:@"gals"];
_programHandle = [GLESUtils loadProgram:vertexShaderPath withFragmentShaderFilepath:fragmentShaderPath];
glUseProgram(_programHandle);
// Get attribute slot from program
//
_positionSlot = glGetAttribLocation(_programHandle, "vPosition");
glEnableVertexAttribArray(_positionSlot);
//获取纹理相关槽位,请注意glGetAttribLocation和glGetUniformLocation的区别
_texCoordSlot = glGetAttribLocation(_programHandle, "TexCoordIn");
glEnableVertexAttribArray(_texCoordSlot);
_ourTextureSlot = glGetUniformLocation(_programHandle, "ourTexture");
}
5).纹理工具类TextureManager
到目前为止,我们嗨没有真正使用到纹理,要真正使用纹理,我们需要纹理图片和把纹理图片转成纹理对象的方法。
关于纹理图片的话,不用说啦,随便找一张:
关于把纹理图片转成纹理对象的方法,我们封装成一个专门的类TextureManager来干这种事:
新建TextureManager类继承NSObject,在.h里
#import
#import
@interface TextureManager : NSObject
/*
* 通过UIImage的方式获取纹理对象
*/
+ (GLuint)getTextureImage:(UIImage *)image;
@end
在.m中实现对应功能,代码中给出了相应解释:
#import "TextureManager.h"
#import
@implementation TextureManager
/*
* 通过UIImage的方式获取纹理对象
*/
+ (GLuint)getTextureImage:(UIImage *)image {
// 获取UIImage并转换成CGImage
CGImageRef spriteImage = image.CGImage;
if(!spriteImage) {
return 0;
}
// 获取图片的大小
GLsizei width = (GLsizei)CGImageGetWidth(spriteImage);
GLsizei height = (GLsizei)CGImageGetHeight(spriteImage);
// 分配内存,并初始化该内存空间为零, 因为一个像素有4个通道(RGBA)所以乘4
GLubyte * spriteData = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
/*
* 创建位图上下文
*/
CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,
CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
// 在上下文中绘制图片
CGContextDrawImage(spriteContext, CGRectMake(0, 0, width, height), spriteImage);
// 释放上下文
CGContextRelease(spriteContext);
// 创建纹理对象并且绑定, 纹理对象用无符号整数表示, 这个纹理对象相当于我们在C语言文件操作里面的句柄
GLuint texName;
glGenTextures(1, &texName);
glBindTexture(GL_TEXTURE_2D, texName);
//设置纹理循环模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//设置纹理过滤模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载图像数据, 并上传纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
// 解绑纹理对象(在本文这里解不解绑都一样,因为后面还是要绑定)
glBindTexture(GL_TEXTURE_2D, 0);
// 释放分配的内存空间
free(spriteData);
return texName;
}
@end
这里面值得注意的是纹理的创建过程和纹理的循环和过滤模式,对纹理循环和过滤模式不清晰请继续回到 http://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/06%20Textures/
这里也稍稍提一下多级渐远纹理,上面链接中对多级渐远纹理的解释很详细,我们在iOS端使用的时候最简单的只需要调用
glGenerateMipmap(GL_TEXTURE_2D);
然后设置纹理缩小时的过滤模式为多级渐远纹理过滤就行:
举例:
//设置纹理过滤模式
glGenerateMipmap(GL_TEXTURE_2D); //自动生成多级渐远纹理
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); //多级渐远纹理过滤模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载图像数据, 并上传纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
注意:一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理。
6).渲染纹理
图片咱有了,转纹理对象方法也有了,接下来咱们开始渲染纹理。
首先在.m里新增一个纹理对象变量
GLuint _textureID; //纹理对象
然后我们在- (void)setupProgram()方法最后获取纹理对象
//获取纹理对象
_textureID = [TextureManager getTextureImage:[UIImage imageNamed:@"timg.jpg"]];
最后就是render了,render前我们先构造纹理的坐标数据:
//4个顶点(分别表示xyz轴)
GLfloat vertices[] = {
// x y z
-0.5, -0.5, 0, //左下
0.5, -0.5, 0, //右下
-0.5, 0.5, 0, //左上
0.5, 0.5, 0, //右上
};
//4个顶点对应纹理坐标
GLfloat textureCoord[] = {
0, 0,
1, 0,
0, 1,
1, 1,
};
整个render方法如下:
-(void)render
{
//设置清屏颜色,默认是黑色,如果你的运行结果是黑色,问题就可能在这儿
glClearColor(0.3, 0.5, 0.8, 1.0);
/*
glClear指定清除的buffer
共可设置三个选项GL_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT
也可组合如:glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
这里我们只用了color buffer,所以只需清除GL_COLOR_BUFFER_BIT
*/
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST); //添加
// Setup viewport
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
//4个顶点(分别表示xyz轴)
GLfloat vertices[] = {
// x y z
-0.5, -0.5, 0, //左下
0.5, -0.5, 0, //右下
-0.5, 0.5, 0, //左上
0.5, 0.5, 0, //右上
};
//4个顶点对应纹理坐标
GLfloat textureCoord[] = {
0, 0,
1, 0,
0, 1,
1, 1,
};
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glVertexAttribPointer(_texCoordSlot, 2, GL_FLOAT, GL_FALSE, 0, textureCoord);
//使用纹理单元
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _textureID);
glUniform1i(_ourTextureSlot, 0);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
[_context presentRenderbuffer:_renderBuffer];
}
所有做完我们运行查看一下结果:
唉,这个图片怎么反了啊?
原因就是纹理坐标的原点在左下角,x,y正方向为往右往上
而我们openGL坐标系原点在屏幕中心,x正方向与纹理x正方向相同,但y正方向与纹理相反,这样的话,解决办法可以修改我们顶点数据对应的纹理坐标,也可以在VertexShader.glsl文件里把:TexCoordOut = TexCoordIn;改为TexCoordOut = vec2(TexCoordIn.x, 1.0-TexCoordIn.y);
attribute vec4 vPosition;
attribute vec2 TexCoordIn;
varying vec2 TexCoordOut;
void main(void)
{
gl_Position = vPosition;
// TexCoordOut = TexCoordIn;
TexCoordOut = vec2(TexCoordIn.x, 1.0-TexCoordIn.y);
}
这个时候图片方向就对了,我们运行一下:
所有教程代码在此 : https://github.com/qingmomo/iOS-OpenGLES-