学习OpenGL ES之基本纹理

作者: handyTOOL | 来源:发表于2017-05-08 16:52 被阅读587次

    本系列所有文章目录

    获取示例代码


    纹理通常来说就是一张图片,我们为每一个顶点指定纹理坐标,然后就可以在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生成出来的保持了一致。

    相关文章

      网友评论

      • 苹果API搬运工:iOS中的纹理原始坐标应该也是左下角为(0,0),右上角为(1,1)吧,只是默认情况下(即option为nil时)为了和UIView的屏幕坐标保持一致, GLKTextureLoader对纹理的y轴进行了反转.
        也可以在option中设置不反转
        NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft:@(YES)};
        self.diffuseTexture = [GLKTextureLoader textureWithContentsOfFile:texturePath options:options error:&error];
        不反转的好处是:立方体平面中,uv坐标可以直接和顶点坐标对应起来,直接把-0.5变成0, 0.5变成1就可以了. 比如x=0.5处的平面,只要替换y,z坐标就行了:
        0.5, -0.5, 0.5点对应的uv是0, 1,
        0.5, -0.5, -0.5点对应的uv是0, 0,
        0.5, 0.5, -0.5点对应的uv是1, 0。

        记得刚开始学习OpenGL的时候,看到这么一大堆坐标就崩溃了,而且不同资料上讲得坐标系还不一致,让我困惑了好久...
        苹果API搬运工:@handyTOOL 加油,虽然我在进阶篇已经有点力不从心了,还是期待你的高级篇
        handyTOOL:多谢你的提醒,我整理了一下,对于纹理坐标系做了一些补充,写在了文末:smile:
        handyTOOL:@史前图腾 嗯,一般加载3D模型的时候,UV都是在3D编辑器中编辑好的,所以你的纹理是否需要翻转应该是已经确定的事情。之前练习加载FBX模型的时候,好像纹理都是需要Y轴翻转的。

      本文标题:学习OpenGL ES之基本纹理

      本文链接:https://www.haomeiwen.com/subject/xfgmtxtx.html