iOS视觉-- (05) OpenGL ES+GLSL实现正方体贴6张图解析

上一篇文章我们通过金字塔延伸到了正方体,然后到这篇正方体每一个面贴一张图。
先看效果图:Demo

效果图

接下来让我们开始学习OpenGL 一个重要‼️的知识点:纹理
借鉴博客:半纸渊--基础纹理
前言:之前我们说过纹理可以简单理解为图片,但是纹理不简简单单图片。

  • 1. Texture 是什么?

Texture 纹理,就是一堆被精心排列过的像素;
Texture 在 OpenGL 里面有很多种类,但在 ES 版本中就两种:Texture_2D 、 Texture_CubeMap

  • Texture_2D:

就是 {x, y} 二维空间下的像素呈现,也就是说,由效果图上可知,很难做到使正方体的六个面出现不同的像素组合;图片处理一般都使用这个模式;[x 、y 属于 [0, 1] 这个范围]


2D纹理坐标
  • Texture_CubeMap:

就是 { x, y, z } 三维空间下的像素呈现,也就如效果图中演示的正方体的六个面可以出现不同的像素组合;它一般是用于做环境贴图——就是制作一个环境,让 3D 模型如同置身于真实环境中【卡通环境中也行】。[x、y、z 属于 [-1, 1] 这个范围,就是与 Vertex Position 的值范围一致]


3D纹理坐标

注:上面提到的所有坐标范围是指有效渲染范围,也就是说你如果提供的纹理坐标超出了这个范围也没有问题,只不过超出的部分就不渲染了;

顶点数据表示如下:

  • Texture_2D:
 //------------- 正方体 -------------
        let attrArr: [GLfloat] = [
            // 顶点:(x, y, z)      颜色:(r, g, b)      纹理: (s, t)
            // 前面
            -0.5, 0.5, 0.5,        1.0, 1.0, 1.0,       0.0, 0.0, // 前左上 0
            -0.5, -0.5, 0.5,       1.0, 1.0, 1.0,       0.0, 1.0, // 前左下 1
            0.5, -0.5, 0.5,        1.0, 1.0, 1.0,       1.0, 1.0, // 前右下 2
            0.5, 0.5, 0.5,         1.0, 1.0, 1.0,       1.0, 0.0, // 前右上 3
...
]
  • Texture_CubeMap:
        let attrArr: [GLfloat] = [
            // 顶点:(x, y, z)      颜色:(r, g, b)      纹理: (s, t, p)
            // 前面
            -1.0, 1.0, 1.0,        1.0, 0.0, 0.0,       -1.0, 1.0, 1.0, // 前左上 0
            -1.0, -1.0, 1.0,       0.0, 1.0, 0.0,       -1.0, -1.0, 1.0, // 前左下 1
            1.0, -1.0, 1.0,        0.0, 0.0, 1.0,       1.0, -1.0, 1.0, // 前右下 2
            1.0, 1.0, 1.0,         1.0, 1.0, 1.0,       1.0, 1.0, 1.0, // 前右上 3
...
]
⚠️ps: CubeMap 里面的纹理坐标和顶点数据是一样的
  • 加载CubeMap纹理

CubeMap共有6个面,然后分别设置
GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515
GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516
GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518
GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A

代码如下:注意⚠️:这里是cubeMap 6张图片的宽高要一致

    //7.1 设置立方体纹理
    func setupCubeTexture() {
        //7.绑定纹理到默认的纹理ID(这里只有一张图片,故而相当于默认于片元着色器里面的us2d_texture)
        glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
        
        for i in 0..<6 {
            let spriteImage: CGImage = UIImage(named: "timg-\(i+1)")!.cgImage!

            //2.读取图片的大小:宽和高 注意⚠️:这里是cubeMap 6张图片的宽高要一致
            let width = 512//spriteImage.width
            let height = 512//spriteImage.height
            
            //3.获取图片字节数: 宽x高x4(RGBA)
    //        let spriteData: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size * width * height * 4)
            let spriteData: UnsafeMutableRawPointer = calloc(width * height * 4, MemoryLayout.size)
            
           
            //4.创建上下文
            /*
             参数1:data,指向要渲染的绘制图像的内存地址
             参数2:width,bitmap的宽度,单位为像素
             参数3:height,bitmap的高度,单位为像素
             参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
             参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
             参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
             let colorSpace = CGColorSpaceCreateDeviceRGB()
             */
            let spriteContext: CGContext = CGContext(data: spriteData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: spriteImage.colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
            
            //5.在CGContextRef上绘图
            let rect = CGRect(x: 0, y: 0, width: width, height: height)
            spriteContext.draw(spriteImage, in: rect)
            
            //载入纹理2D数据 就是加载纹理像素到 GPU 的方法
            /*
             参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
             参数2:加载的层次,一般设置为0
             参数3:纹理的颜色值GL_RGBA
             参数4:宽
             参数5:高
             参数6:border,边界宽度
             参数7:format
             参数8:type
             参数9:纹理数据
             */
            glTexImage2D(GLenum(GL_TEXTURE_CUBE_MAP_POSITIVE_X + Int32(i)), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), spriteData)
            
            //释放spriteData
            free(spriteData)
        }
        
        

        //设置纹理属性
        /*
         参数1:纹理维度
         参数2:线性过滤、为s,t坐标设置模式
         参数3:wrapMode,环绕模式
         */
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
        
        
        //绑定纹理
        /*
         参数1:纹理维度
         参数2:纹理ID,因为只有一个纹理,给0就可以了。
         */
        glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
    }

因为我们的设置的纹理坐标由两位:[s, t] --> [s, t, p],所以着色器中的纹理坐标要做相应的改变,还有渲染那里读取数据的时候也做相应改变。

  • 顶点着色器代码:
//纹理坐标vec2 --> vec3
attribute vec4 position;
attribute vec4 positionColor; //顶点颜色
attribute vec3 textCoordinate; //纹理坐标
uniform mat4 projectionMatrix; //投影矩阵
uniform mat4 modelViewMatrix;  //模型视图矩阵

varying lowp vec4 varyColor; //顶点颜色
varying lowp vec3 varyTextCoord; //传递给片元着色器纹理坐标

void main()
{
    varyColor = positionColor;
    varyTextCoord = textCoordinate;
    
    vec4 vPos;
    vPos = projectionMatrix * modelViewMatrix * position;
    gl_Position = vPos;
}

  • 片元着色器代码:
//纹理坐标vec2 --> vec3
varying lowp vec4 varyColor; //顶点颜色
varying lowp vec3 varyTextCoord; //顶点着色器传递过来的纹理坐标

//uniform sampler2D colorMap; //纹理
uniform samplerCube us2d_texture;

void main()
{
    gl_FragColor = textureCube(us2d_texture, varyTextCoord) * varyColor;
}

渲染代码:

步长和纹理坐标做相应的调整即可,就不贴了

到此正方体贴图工作就完成了。详细请查看源码


但是从借鉴的博客半纸渊--基础纹理,看到他能实现下图像魔方的效果,既然都做到多面贴图了,所以也想试试看。

效果图

通过查看他的源码,这种实现方式也是:glTexImage2D,只不过最后一个参数数据是个颜色数组

  • 加载纹理的代码就变成这样:
    //7.2 设置立方体像素纹理
    func setupCubePixelsTexture() {
        //7.绑定纹理到默认的纹理ID(这里只有一张图片,故而相当于默认于片元着色器里面的us2d_texture)
        glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
        
        for i in 0..<6 {
            glTexImage2D(GLenum(GL_TEXTURE_CUBE_MAP_POSITIVE_X + Int32(i)), 0, GL_RGBA, 2, 2, 0, GLenum(GL_RGBA), GLenum(GL_FLOAT), texCubemapPixelDatas[i])
        }
        
        

        //设置纹理属性
        /*
         参数1:纹理维度
         参数2:线性过滤、为s,t坐标设置模式
         参数3:wrapMode,环绕模式
         */
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_CUBE_MAP), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
        
        
        //绑定纹理
        /*
         参数1:纹理维度
         参数2:纹理ID,因为只有一个纹理,给0就可以了。
         */
        glBindTexture(GLenum(GL_TEXTURE_CUBE_MAP), 0)
    }

运行结果图

这并不是我们想要的效果,而且连方格都没有显示出来,虽然中间看似有分割线。但是离效果图还是天差地别的。怎么回事呢?对比了一下发现在过滤方式不同:GL_LINEAR 和 GL_NEAREST

摘抄自:mChenys -- 六、OpenGL ES纹理的使用
当纹理大小要被扩大或者缩小的时候,我们需要使用纹理过滤明确说明会发生什么,当我们在渲染表面上绘制一个纹理时,那个纹理的纹理元素可能无法精确地映射到OpenGL生成的片段上,有2种情况:缩小或者放大。
当我们尽力把几个纹理元素挤进一个片段时,缩小就会发生了,当把一个纹理元素扩展到许多片段时,放大就发生了。
针对每一种情况,我们都可以配置OpenGL使用一个纹理过滤器,我会使用下面的图像阐述每一种过滤模式:

图1

  • GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
    图2

    这种方式为每个片段选择最近的纹理元素,当放大纹理时它的锯齿效果看起来相当明显,每个纹理单元都清楚地显示为一个小方块。
    图3

    当我们缩小纹理时,因为没有足够的片段来绘制所有的纹理单元,许多细节将会丢失。
    图4
  • GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
    图5

    线性过滤使用双线插值平滑像素之间的过渡,而不是每个片段使用最近的纹理元素,OpenGL会使用四个邻接的纹理元素,并在他们之间用一个线性差值算法做差值,这个算法与前面介绍平滑着色的算法一样,之所以叫它双线性,是因为它是沿两个维度差值的,它会比近邻过滤要平滑很多,但还是会有一些锯齿显示出来,因为我们把这个纹理扩展得太多了,但是锯齿没有最近邻过滤那么明显。
    图6

    PS:纹理放大时使用线性过滤(GL_LINEAR),缩小时用邻近过滤(GL_NEAREST)

然后我们修改过滤方式为:邻近过滤(GL_NEAREST)
效果如下:

邻近过滤效果图

这里看到已经差不多了和他的一样了。但是总觉得怪怪的。就是每一个面都会有一块是黑色的,而对照的却不是这样的。然后再去仔细看了看。后面发现原来还是在这个方法上出现了问题:

glTexImage2D(GLenum(GL_TEXTURE_CUBE_MAP_POSITIVE_X + Int32(i)), 0, GL_RGBA, 2, 2, 0, GLenum(GL_RGBA), GLenum(GL_FLOAT), texCubemapPixelDatas[i])
//载入纹理2D数据 就是加载纹理像素到 GPU 的方法
参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
参数2:加载的层次,一般设置为0
参数3:纹理的颜色值GL_RGBA
参数4:宽
参数5:高
参数6:border,边界宽度
参数7:format
参数8:type
参数9:纹理数据
这里的(参数3:纹理的颜色值,参数7:format)我们传的是GL_RGBA,而数组里面只有(r, g, b)并没有a,所以出现取值有问题吧。改成 GL_RGB

运行结果:


运行结果

你可能感兴趣的:(iOS视觉-- (05) OpenGL ES+GLSL实现正方体贴6张图解析)