OpenGL ES之纹理渲染

一、前言

OpenGL是Khronos Group开发维护的一个规范,它主要为我们定义了用来操作图形和图片的一些列函数API,需要注意的是OpenGL本身并不是API。

GPU的硬件开发商则需要提供满足OpenGL规范的实现,这些实现通常被称之为“驱动”,他们负责OpenGL定义的API命令翻译为GPU指令。

OpenGL ES((OpenGL for Embedded Systems) )是OpenGL三维图形的API的子集,针对手机、PDA和游戏主机等嵌入式设备儿设计。

二、基础概念

image

渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被分为上面几个阶段,每个阶段会把前一个阶段的输出作为输入。

注意蓝色部分是我们可以注入自定义的着色器部分

  • 1、顶点着色器:它把单独的顶点作为输入,主要目的是把3D坐标转换为另一种3D坐标
  • 2、图元装配:将顶点着色器输出的所有顶点作为输入,并所有的点装配为指定图元形状
  • 3、几何着色器:把接收到的图元形式的一系列点的集合作为输入,它可以通过产生新顶点构造出新的图元来生车给你其他形状
  • 4、光栅化阶段:把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段,在片段着色器运行之前会执行裁切,超出视图外的所有像素都将被裁切,以提升效率
  • 5、片段着色器:计算一个像素的最终颜色
  • 6、测试和混合:这个阶段检测片段对应的深度值,来判断这个像素在其他物体的前面还是后面,决定是否丢弃。这个阶段也会检查alpha值并对物体进行混合

1、帧缓存

用来接收GPU渲染出来的2D图像像素数据的缓冲区叫做帧缓存。帧缓存可以同时存在多个,并通过OpenGL ES让GPU把渲染结果存储到任意数量的帧缓存中。

程序和操作系统会把渲染结果保存到后帧缓存在内的其他帧缓存中。当渲染后的后帧缓存包含一个完整的图像时,前帧缓存和后帧缓存会瞬间切换。

image

2、OpenGL 坐标系

OpenGL ES总是开始于一个矩形的笛卡尔坐标系,这意味着任意两个轴之间的角度都是90度。空间中的每个位置称之为顶点,每个顶点通过其X、Y、Z轴上的位置被定义。

OpenGL ES坐标都是浮点数储存的,并且没有单位。

image

3、纹理

纹理是用来保存图像的颜色元素值的OpenGL ES缓存。

纹素:当用图像初始化一个纹理缓存后,这个图像的每个像素变成了纹理的一个纹素,与像素类似,纹素保存颜色数据。像素通常表示计算机屏幕上的一个世纪的颜色点;而纹素存在于一个虚拟的没有尺寸的坐标系中。

4、 纹理坐标系

纹理坐标系为一个命名为S和T的2D轴。在一个纹理中无论有多少纹素,纹理的尺寸永远在S轴上从0.0到1.0,T轴上从0.0到1.0.

image

5、片元

  • 点阵化(rasterizing):转换几何图形数据为帧缓存中的颜色像素的渲染步骤叫做点阵化,每个颜色像素叫做片元。

  • 映射(mapping):指定怎么对齐纹理和顶点,以便让GPU知道每个片元颜色由哪些纹素决定。每个顶点还会给出U和V的坐标值,每个U坐标会映射顶点到视窗中的最终位置到纹理中的沿着S轴的一个位置,V坐标映射到T轴。

image
  • 取样(sampling):GPU根据计算出来的每个片元的U、V位置从绑定的纹理中选择纹素。

6、为缓存提供数据

  • 1、 生成:glGenBuffers() - 请求OpenGL ES为图形处理器控制的缓存生成一个独一无二的标识

  • 2、 绑定:glBindBuffer() - 告诉OpenGL ES接下来的运算使用哪个缓存

  • 3、 缓存数据:glBufferData()、glBufferSubData() - 让OpenGL ES为当前的缓存分配并初始化足够的连续内存。(通常是从CPU控制的内存复制数据到分配的内存)

  • 4、 启用或者禁止:glEnableVertexAttribArray() | glDisableVertexAttribArray() - 告诉OpenGL ES接下来渲染中是否使用缓存中的数据

  • 5、 设置指针:glVertextAttribPoint() - 告诉OpenGL ES在缓存中的数据的类型和所有需要访问的数据的内存偏移值

  • 6、 绘图:glDrawArrays() | glDrawElements() - 告诉OpenGL ES使用当前绑定并且用缓存中的数据渲染整个场景或者部分场景

  • 7、 删除:glDeleteBuffers() - 告诉OpenGL ES删除以前生成的缓存并释放相关资源

7、绘制序列

image

三、使用GLKit渲染图片

1、定义结构体并初始化顶点和纹理坐标

typedef struct {
    GLKVector3 positionCoord;   // 顶点坐标(x, y, z)
    GLKVector2 textureCoord;    // 纹理坐标(U, V)
} SenceVertex;

const SenceVertex vertices[] = {
    {{-0.5, -0.5, 0.0}, {0.0, 0.0}},  // 左下角
    {{ 0.5, -0.5, 0.0}, {1.0, 0.0}},  // 右下角
    {{-0.5,  0.5, 0.0}, {0.0, 1.0}},  // 左上角
    {{ 0.5,  0.5, 0.0}, {1.0, 1.0}}   // 右上角
};

2、初始化GLKView并设置上下文

self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.context];
    
self.glkView = [[GLKView alloc] initWithFrame:self.view.bounds context:self.context];
self.glkView.delegate = self;
[self.view addSubview:self.glkView];

3、使用GLKTextureLoader加载图片纹理

NSString *imageFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"leaves.png"];
UIImage *image = [UIImage imageWithContentsOfFile:imageFilePath];
    
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:image.CGImage options:nil error:NULL];

4、生成绑定顶点缓存

glGenBuffers(1, &_vertextBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _vertextBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(SenceVertex) * 4, vertices, GL_STATIC_DRAW);

4、实现GLKView代理的渲染回掉

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    [self.baseEffect prepareToDraw];
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 顶点
    glEnableVertexAttribArray(GLKVertexAttribPosition); // 启用顶点缓存数据
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));    // 设置顶点数据指针
    // 纹理
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);    // 启用纹理缓存数据
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));    // 设置纹理数据指针
    // 绘制
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

四、使用GLSL渲染图片

image

1、新建顶点着色器和片元着色器

// 顶点着色器
attribute vec4 Position;
attribute vec2 InputTextureCoordinate;

varying vec2 TextureCoordinate;

void main (void) {
    gl_Position = Position;
    TextureCoordinate = InputTextureCoordinate;
}

// 片元着色器
// 1. 指定默认精度
precision mediump float;
uniform sampler2D InputImageTexture;
// 
varying vec2 TextureCoordinate;

void main() {
    gl_FragColor = texture2D(InputImageTexture, TextureCoordinate);
}
  • attribute:只能存在于顶点着色器,一般用来表示顶点数据,如:顶点坐标、法线、纹理坐标、顶点颜色等
  • varying:顶点着色器和片元着色器传递数据用
  • uniform:外部程序传递给着色器的变量,就像是C语言的敞亮,它不能被shader程序修改(只能用,不能改)
  • gl_Position和gl_FragColor:内建变量,前一个是向顶点着色器输出向量,后一个是向片段着色器输出向量
  • texture2D:根据纹理坐标获取纹素

2、编译顶点着色器和片元着色器

* -loadShader;
* -compileShader:type:
* -linkProgram:
* validateProgram:
image
NSString *filePath = [[NSBundle mainBundle] pathForResource:shaderName
                                                     ofType:(type == GL_VERTEX_SHADER ? @"vsh" : @"fsh")];
    
NSError *error = nil;
NSString *shaderString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&error];
if (error) {
    NSAssert(NO, @"文件读取失败");
}
    
GLuint shader = glCreateShader(type);
    
const char *shaderStringUTF8 = [shaderString UTF8String];
GLint shaderStringLength = (GLint)[shaderString length];
glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);
    
glCompileShader(shader);
    
GLint compileRes;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileRes);
if (compileRes == GL_FALSE) {
    GLchar message[256];
    glGetShaderInfoLog(shader, sizeof(message), 0, &message[0]);
    NSAssert(NO, @"着色器编译错误");
}

3、程序编译、链接并验证

GLint program = glCreateProgram();
// 链接两个着色器
glAttachShader(program, vertextShader);
glAttachShader(program, fragmentShader);
    
// 链接程序
glLinkProgram(program);
    
GLint linkRes;
glGetProgramiv(program, GL_LINK_STATUS, &linkRes);
if (linkRes == GL_FALSE) {
    GLchar message[256];
    glGetProgramInfoLog(program, sizeof(message), 0, &message[0]);
    NSAssert(NO, @"程序编译错误");
}

4、获取着色器参数

GLint positionAttribute = glGetAttribLocation(program, "Position"); // 顶点坐标
GLint textureCoordinateAttribute = glGetAttribLocation(program, "InputTextureCoordinate");  // 纹理坐标
GLint textureUniform = glGetUniformLocation(program, "InputImageTexture");  // 输入纹理

5、创建、绑定帧缓存和渲染缓存

if (_frameBuffer != 0) {
    glDeleteFramebuffers(1, &_frameBuffer);
    _frameBuffer = 0;
}
    
glGenFramebuffers(1, &_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);

if (_renderBuffer != 0) {
    glDeleteRenderbuffers(1, &_renderBuffer);
    _renderBuffer = 0;
}
    
glGenRenderbuffers(1, &_renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);

// 绑定RenderBuffer
 glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _renderBuffer);
 [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
 GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
 
 if (status != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"绑定失败");
 }  

6、加载纹理

CGImageRef imageRef = image.CGImage;
GLint width = (GLint)CGImageGetWidth(imageRef);
GLint height = (GLint)CGImageGetHeight(imageRef);
CGRect rect = CGRectMake(0, 0, width, height);
    
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
void *imageData = malloc(width * height * 4);
CGContextRef contextRef = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpaceRef, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    
CGColorSpaceRelease(colorSpaceRef);
//  因为Core Graphics是以原点在左上角同时Y轴向下增大的形式来实现iOS图片保存的
//  OpenGL ES的纹理坐标 原点在左下角,同时Y轴向下增大
CGContextTranslateCTM(contextRef, 0, height);
CGContextScaleCTM(contextRef, 1.0, -1.0);
CGContextDrawImage(contextRef, rect, imageRef);
    
GLuint textureID;
glGenBuffers(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// level: 指定MIP贴图的初始细节级别
// internalFormat: 指定纹理缓存内每个纹素需要保存的信息数量,对于iOS设备来说,纹素信息要么是GL_RGB,要么是GL_RGBA
// format: 指定初始化缓存所使用的图像数据的每个像素所要保存的信息,这个参数总是internalFormat参数相同
/* type: 指定缓存中的纹素所使用的编码类型,
 * GL_UNSIGNED_BYTE、GL_UNSIGNED_SHORT_5_6_5、GL_UNSIGNED_SHORT_4_4_4_4、GL_UNSIGNED_SHORT_5_5_5_1
 * 使用GL_UNSIGNED_BYTE会提供最佳色彩
*/
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
/* GL_TEXTURE_MIN_FILTER: 在多个纹素对应一个片元的星矿下,使用怎么样的形式取样颜色
 * GL_LINEAR: 混色纹素颜色,例如:黑白交替,取样的混合纹素为灰色
 * GL_NEAREST: 取样其中一个,因此最终片元颜色可能为白色或者黑色
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
/*
 * 当U、V坐标小于0、或者大于1时,有两种选择
 * 1. GL_CLAMP_TO_EDGE: 取纹理边缘的纹素
 * 2. GL_REPEAT: 循环
 */
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
glBindTexture(GL_TEXTURE_2D, 0);
    
CGContextRelease(contextRef);
free(imageData);

7、将纹理传给着色器程序并绘制

// 将纹理ID传给着色器程序
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texure);
glUniform1i(textureUniform, 0);
    
// 顶点buffer
glGenBuffers(1, &_vertextBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _vertextBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(SenceVertex) * 4, self.vertices, GL_STATIC_DRAW);
    
glClear(GL_COLOR_BUFFER_BIT);
    
// 顶点
glEnableVertexAttribArray(positionAttribute);
glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));
// 纹理
glEnableVertexAttribArray(textureCoordinateAttribute);
glVertexAttribPointer(textureCoordinateAttribute, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

[self.context presentRenderbuffer:_renderBuffer];

源码

请到GitHub查看完整代码.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258