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<GLbyte>.allocate(capacity: MemoryLayout<GLbyte>.size * width * height * 4)
            let spriteData: UnsafeMutableRawPointer = calloc(width * height * 4, MemoryLayout<GLbyte>.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

运行结果:


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

推荐阅读更多精彩内容