iOS平台OpenGL ES 2.0纹理渲染与坐标矫正实践
本文探讨在iOS环境下利用OpenGL ES 2.0实现图像纹理渲染,并解决常见的图像倒置问题。
核心渲染架构
渲染管线涉及帧缓冲对象(FBO)与渲染缓冲对象(RBO)的协同工作。FBO作为管理者,负责维护color buffer、depth buffer、stencil buffer等附件。
自定义渲染视图实现
创建继承自UIView的渲染容器,通过重写+layerClass方法将底层图层替换为CAEAGLLayer:
// RenderContainer.h
#import <UIKit/UIKit.h>
@interface RenderContainer : UIView
@end
// RenderContainer.m
#import "RenderContainer.h"
#import <OpenGLES/ES2/gl.h>
@interface RenderContainer ()
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, assign) GLuint colorRenderBuffer;
@property (nonatomic, assign) GLuint frameBuffer;
@property (nonatomic, assign) GLuint shaderProgram;
@end
@implementation RenderContainer
+ (Class)layerClass {
return [CAEAGLLayer class];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self configureLayer];
[self initializeContext];
[self setupBuffers];
[self executeRender];
}
- (void)configureLayer {
self.glLayer = (CAEAGLLayer *)self.layer;
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
self.glLayer.opaque = YES;
self.glLayer.drawableProperties = @{
kEAGLDrawablePropertyRetainedBacking: @NO,
kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
};
}
- (void)initializeContext {
EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!ctx || ![EAGLContext setCurrentContext:ctx]) {
NSLog(@"Context initialization failed");
return;
}
self.glContext = ctx;
}
- (void)setupBuffers {
// 清理旧缓冲
glDeleteRenderbuffers(1, &_colorRenderBuffer);
glDeleteFramebuffers(1, &_frameBuffer);
// 创建渲染缓冲
GLuint rb;
glGenRenderbuffers(1, &rb);
self.colorRenderBuffer = rb;
glBindRenderbuffer(GL_RENDERBUFFER, rb);
[self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
// 创建帧缓冲并绑定
GLuint fb;
glGenFramebuffers(1, &fb);
self.frameBuffer = fb;
glBindFramebuffer(GL_FRAMEBUFFER, fb);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER, rb);
}
@end
着色器程序构建
- (GLuint)buildProgramWithVertex:(NSString *)vertPath fragment:(NSString *)fragPath {
GLuint vertShader, fragShader;
[self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertPath];
[self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragPath];
GLuint prog = glCreateProgram();
glAttachShader(prog, vertShader);
glAttachShader(prog, fragShader);
glDeleteShader(vertShader);
glDeleteShader(fragShader);
return prog;
}
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)path {
NSString *source = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding error:nil];
const GLchar *srcPtr = (GLchar *)[source UTF8String];
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &srcPtr, NULL);
glCompileShader(*shader);
}
顶点数据与纹理映射
- (void)executeRender {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
CGFloat scale = [[UIScreen mainScreen] scale];
glViewport(0, 0, self.bounds.size.width * scale,
self.bounds.size.height * scale);
// 顶点坐标与纹理坐标交错存储:x,y,z,u,v
GLfloat vertexData[] = {
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 左下
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // 左上
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // 右下
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // 左上
0.5f, 0.5f, 0.0f, 1.0f, 0.0f // 右上
};
self.shaderProgram = [self buildProgramWithVertex:@"vertex.vsh"
fragment:@"fragment.fsh"];
glLinkProgram(self.shaderProgram);
glUseProgram(self.shaderProgram);
// 上传顶点数据
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
// 配置顶点属性
GLint posLoc = glGetAttribLocation(self.shaderProgram, "aPosition");
glEnableVertexAttribArray(posLoc);
glVertexAttribPointer(posLoc, 3, GL_FLOAT, GL_FALSE,
5 * sizeof(GLfloat), (void *)0);
// 配置纹理坐标属性
GLint texLoc = glGetAttribLocation(self.shaderProgram, "aTexCoord");
glEnableVertexAttribArray(texLoc);
glVertexAttribPointer(texLoc, 2, GL_FLOAT, GL_FALSE,
5 * sizeof(GLfloat), (void *)(3 * sizeof(GLfloat)));
[self loadTexture:@"sample"];
// 可选:应用旋转矩阵
GLint matrixLoc = glGetUniformLocation(self.shaderProgram, "uTransform");
GLfloat rotation[16] = {
1, 0, 0, 0,
0, -1, 0, 0, // Y轴翻转
0, 0, 1, 0,
0, 0, 0, 1
};
glUniformMatrix4fv(matrixLoc, 1, GL_FALSE, rotation);
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
纹理加载与图像解码
- (void)loadTexture:(NSString *)imageName {
CGImageRef image = [UIImage imageNamed:imageName].CGImage;
if (!image) return;
size_t w = CGImageGetWidth(image);
size_t h = CGImageGetHeight(image);
GLubyte *data = (GLubyte *)calloc(w * h * 4, sizeof(GLubyte));
CGContextRef ctx = CGBitmapContextCreate(data, w, h, 8, w * 4,
CGImageGetColorSpace(image),
kCGImageAlphaPremultipliedLast);
// 方案3:上下文变换实现翻转
CGContextTranslateCTM(ctx, 0, h);
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), image);
CGContextRelease(ctx);
glBindTexture(GL_TEXTURE_2D, 0);
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);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)w, (GLsizei)h,
0, GL_RGBA, GL_UNSIGNED_BYTE, data);
free(data);
}
着色器源码
顶点着色器(vertex.vsh):
attribute vec4 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uTransform;
varying vec2 vTexCoord;
void main() {
vTexCoord = aTexCoord;
gl_Position = aPosition * uTransform;
}
片元着色器(fragment.fsh):
varying lowp vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
// 方案4:片元着色器中翻转Y坐标
// vec2 coord = vec2(vTexCoord.x, 1.0 - vTexCoord.y);
gl_FragColor = texture2D(uTexture, vTexCoord);
}
图像倒置问题的五种解决方案
| 方案 | 实现位置 | 核心操作 |
|---|---|---|
| 1 | 顶点数据 | 调整纹理坐标V值(1.0↔0.0互换) |
| 2 | 顶点着色器 | vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y); |
| 3 | 图像解码 | CGContextScaleCTM(ctx, 1.0, -1.0) |
| 4 | 片元着色器 | 采样时计算1.0 - vTexCoord.y |
| 5 | 变换矩阵 | 构造Y轴翻转的4×4矩阵传入uniform |
方案2在顶点阶段处理效率最优(顶点数远小于像素数),方案3在数据源处解决最为彻底。