学习OpenGL ES之基本纹理

本系列所有文章目录

获取示例代码


纹理通常来说就是一张图片,我们为每一个顶点指定纹理坐标,然后就可以在Shader中获取相应的纹理像素点颜色了。

纹理坐标

首先解释一下什么是纹理坐标。把一张图的左上角定为0,0点,长宽都定义为1,剩余四个点的坐标就会如下图所示。这样就构成了纹理坐标系统。

一般使用uv来表示纹理坐标,uv是一个二维向量(u,v),u和v的取值从0到1。我在代码中为每个顶点数据增加了2个GLFloat来表示uv的值。下面是X轴上平面的的代码。

- (void)drawXPlanes {
    static GLfloat triangleData[] = {
// X轴0.5处的平面
      0.5,  -0.5,    0.5f, 1,  0,  0, 0, 0,
      0.5,  -0.5f,  -0.5f, 1,  0,  0, 0, 1,
      0.5,  0.5f,   -0.5f, 1,  0,  0, 1, 1,
      0.5,  0.5,    -0.5f, 1,  0,  0, 1, 1,
      0.5,  0.5f,    0.5f, 1,  0,  0, 1, 0,
      0.5,  -0.5f,   0.5f, 1,  0,  0, 0, 0,
// X轴-0.5处的平面
      -0.5,  -0.5,    0.5f, -1,  0,  0, 0, 0,
      -0.5,  -0.5f,  -0.5f, -1,  0,  0, 0, 1,
      -0.5,  0.5f,   -0.5f, -1,  0,  0, 1, 1,
      -0.5,  0.5,    -0.5f, -1,  0,  0, 1, 1,
      -0.5,  0.5f,    0.5f, -1,  0,  0, 1, 0,
      -0.5,  -0.5f,   0.5f, -1,  0,  0, 0, 0,
    };
    [self bindAttribs:triangleData];
    glDrawArrays(GL_TRIANGLES, 0, 12);
}

我们分析一下X轴0.5处的平面的顶点数据。

      0.5,  -0.5,    0.5, 1,  0,  0, 0, 0,
      0.5,  -0.5,  -0.5, 1,  0,  0, 0, 1,
      0.5,  0.5,   -0.5, 1,  0,  0, 1, 1,
      0.5,  0.5,    -0.5, 1,  0,  0, 1, 1,
      0.5,  0.5,    0.5, 1,  0,  0, 1, 0,
      0.5,  -0.5,   0.5, 1,  0,  0, 0, 0,

第一个三角形uv和顶点对应关系如下。
0.5, -0.5, 0.5点对应的uv0, 0
0.5, -0.5, -0.5点对应的uv0, 1
0.5, 0.5, -0.5点对应的uv1, 1

第二个三角形uv和顶点对应关系如下。
0.5, 0.5, -0.5点对应的uv1, 1
0.5, 0.5, 0.5点对应的uv1, 0
0.5, -0.5, 0.5点对应的uv0, 0

这两个三角形的uv分别对应纹理的两个三角部分,合在一起刚好是完整的纹理。

在3D建模中,这种顶点和uv的映射关系是要通过建模工具去完成的,只有为每个顶点配置了合适的uv,才能让贴图按照你想要的方式显示出来。

然后增加绑定uv属性的代码。

- (void)bindAttribs:(GLfloat *)triangleData {
    // 启用Shader中的两个属性
    // attribute vec4 position;
    // attribute vec4 color;
    GLuint positionAttribLocation = glGetAttribLocation(self.shaderProgram, "position");
    glEnableVertexAttribArray(positionAttribLocation);
    GLuint colorAttribLocation = glGetAttribLocation(self.shaderProgram, "normal");
    glEnableVertexAttribArray(colorAttribLocation);
    GLuint uvAttribLocation = glGetAttribLocation(self.shaderProgram, "uv");
    glEnableVertexAttribArray(uvAttribLocation);
    
    // 为shader中的position和color赋值
    // glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
    // indx: 上面Get到的Location
    // size: 有几个类型为type的数据,比如位置有x,y,z三个GLfloat元素,值就为3
    // type: 一般就是数组里元素数据的类型
    // normalized: 暂时用不上
    // stride: 每一个点包含几个byte,本例中就是6个GLfloat,x,y,z,r,g,b
    // ptr: 数据开始的指针,位置就是从头开始,颜色则跳过3个GLFloat的大小
    glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData);
    glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData + 3 * sizeof(GLfloat));
    glVertexAttribPointer(uvAttribLocation, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData + 6 * sizeof(GLfloat));
}

将顶点数据最后两个GLFloat绑定到Shader的uv属性上。

生成纹理

我们有了坐标,那么纹理数据怎么获取呢?GLKit提供了非常便捷的方式为我们生成纹理。

- (void)genTexture {
    NSString *textureFile = [[NSBundle mainBundle] pathForResource:@"texture" ofType:@"jpg"];
    NSError *error;
    self.diffuseTexture = [GLKTextureLoader textureWithContentsOfFile:textureFile options:nil error:&error];
}

diffuseTexture是GLKTextureInfo类型的,它的属性name将会被用来和OpenGL系统进行交互。

@property (strong, nonatomic) GLKTextureInfo *diffuseTexture;

绑定和使用纹理

有了纹理,接下来就要把它传递给Shader,前面我们已经把每个顶点的纹理坐标传递给了Vertex Shader。在Vertex Shader中新增了属性attribute vec2 uv;,以及varying vec2 fragUV;。Vertex Shader做的事情就是把uv直接传递给Fragment Shader,让它去处理。

attribute vec4 position;
attribute vec3 normal;
attribute vec2 uv;

uniform float elapsedTime;
uniform mat4 projectionMatrix;
uniform mat4 cameraMatrix;
uniform mat4 modelMatrix;

varying vec3 fragNormal;
varying vec2 fragUV;

void main(void) {
    mat4 mvp = projectionMatrix * cameraMatrix * modelMatrix;
    fragNormal = normal;
    fragUV = uv;
    gl_Position = mvp * position;
}

Fragment Shader中增加了uniform sampler2D diffuseMap;sampler2D是纹理的参数类型。然后将diffuseMap在纹理坐标fragUV上的像素颜色作为基本色vec4 materialColor = texture2D(diffuseMap, fragUV);texture2D函数用来采样纹理在某个uv坐标下的颜色,返回值类型是vec4

precision highp float;

varying vec3 fragNormal;
varying vec2 fragUV;

uniform float elapsedTime;
uniform vec3 lightDirection;
uniform mat4 normalMatrix;
uniform sampler2D diffuseMap;

void main(void) {
    vec3 normalizedLightDirection = normalize(-lightDirection);
    vec3 transformedNormal = normalize((normalMatrix * vec4(fragNormal, 1.0)).xyz);
    
    float diffuseStrength = dot(normalizedLightDirection, transformedNormal);
    diffuseStrength = clamp(diffuseStrength, 0.0, 1.0);
    vec3 diffuse = vec3(diffuseStrength);
    
    vec3 ambient = vec3(0.3);
    
    vec4 finalLightStrength = vec4(ambient + diffuse, 1.0);

    vec4 materialColor = texture2D(diffuseMap, fragUV);
    
    gl_FragColor = finalLightStrength * materialColor;
}

回到OC代码。将我们生成的纹理绑定到uniform diffuseMap上。

  // 绑定纹理
    GLuint diffuseMapUniformLocation = glGetUniformLocation(self.shaderProgram, "diffuseMap");
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, self.diffuseTexture.name);
    glUniform1i(diffuseMapUniformLocation, 0);

绑定纹理的流程是:

  1. 激活纹理的某个通道glActiveTexture(GL_TEXTURE0);,OpenGL ES中最多可以激活8个通道。通道0是默认激活的,所以本例中这一句也可以不写。
  2. 绑定生成的纹理到GL_TEXTURE_2DglBindTexture(GL_TEXTURE_2D, self.diffuseTexture.name);,注意这里是绑定到GL_TEXTURE_2D而不是GL_TEXTURE0
  3. 将0传递给uniform diffuseMap,如果激活的是GL_TEXTURE1就传递1,以此类推。

到此,纹理的基本使用方法就介绍完了,效果如下。

补充:使用OpenGL函数生成纹理

除了使用GLKit生成纹理之外,还可以直接使用OpenGL生成纹理。

  1. 首先将图片的数据以RGBA的形式导出。
  2. 使用glGenTextures生成纹理,这里生成的纹理就相当于上面说到的self.diffuseTexture.name
  3. 使用glBindTexture绑定纹理到GL_TEXTURE_2D
  4. 使用glTexImage2D写图片数据,我们的图片数据已经统一导出成RGBA格式了,所以颜色格式参数使用GL_RGBA。每个颜色组件参数使用GL_UNSIGNED_BYTE,就是说R,G,B,A每个数据各占一个字节的大小。
  5. 使用glTexParameteri设置采样方式和重复方式,每个方式具体的效果大家可以自行修改例子观察一下。重复方式主要用于uv超出0到1的场景。
  6. glBindTexture(GL_TEXTURE_2D, 0);是为了清空GL_TEXTURE_2D绑定的数据,可以把GL_TEXTURE_2D理解为一个工作台,你处理完了你的事情需要把工作台清理干净。
- (void)genTextureWithGLCommands {
    UIImage *img = [UIImage imageNamed:@"texture.jpg"];
    // 将图片数据以RGBA的格式导出到textureData中
    CGImageRef imageRef = [img CGImage];
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    
    GLubyte *textureData = (GLubyte *)malloc(width * height * 4);
    
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * width;
    NSUInteger bitsPerComponent = 8;
    
    CGContextRef context = CGBitmapContextCreate(textureData, width, height,
                                                 bitsPerComponent, bytesPerRow, colorSpace,
                                                 kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    CGContextRelease(context);
    
    // 生成纹理
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, textureData);
    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);
    glBindTexture(GL_TEXTURE_2D, 0);
    
    self.diffuseTextureWithGLCommands = texture;
}

注意,作为纹理的图片的尺寸最好是2的n次方,比如1024,512。一方面提高性能,另一方面不是所有的3D图形处理系统都支持非2的n次方尺寸的纹理。

纹理坐标系的补充

本文的纹理坐标系是使用GLKTextureLoader加载纹理得出的默认坐标系,如果你想要在竖直方向翻转坐标系,可以使用可选项GLKTextureLoaderOriginBottomLeft,将它设置为YES生成纹理。这样(0,0)点就是在左下角,(1,1) 点在右上角。代码如下 。

self.diffuseTexture = [GLKTextureLoader textureWithContentsOfFile:textureFile options:@{GLKTextureLoaderOriginBottomLeft: @(YES)} error:&error];

这里感谢@史前图腾同学的提醒。

关于genTextureWithGLCommands方法生成出来的纹理坐标系

如果你使用gl系列方法自己生成纹理,默认纹理坐标应该是(0,0)点在左下角,(1,1) 点在右上角,但是CGContextDrawImage方法会把图片上下颠倒,所以genTextureWithGLCommands生成出来的纹理坐标系恰好和GLKTextureLoader生成出来的保持了一致。

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

推荐阅读更多精彩内容