上一篇文章我们通过金字塔延伸到了正方体,然后到这篇正方体每一个面贴一张图。
先看效果图:Demo
接下来让我们开始学习OpenGL 一个重要‼️的知识点:纹理
借鉴博客:半纸渊--基础纹理
前言:之前我们说过纹理可以简单理解为图片,但是纹理不简简单单图片。
-
1. Texture 是什么?
Texture 纹理,就是一堆被精心排列过的像素;
Texture 在 OpenGL 里面有很多种类,但在 ES 版本中就两种:Texture_2D 、 Texture_CubeMap
-
Texture_2D:
就是 {x, y} 二维空间下的像素呈现,也就是说,由效果图上可知,很难做到使正方体的六个面出现不同的像素组合;图片处理一般都使用这个模式;[x 、y 属于 [0, 1] 这个范围]
-
Texture_CubeMap:
就是 { x, y, z } 三维空间下的像素呈现,也就如效果图中演示的正方体的六个面可以出现不同的像素组合;它一般是用于做环境贴图——就是制作一个环境,让 3D 模型如同置身于真实环境中【卡通环境中也行】。[x、y、z 属于 [-1, 1] 这个范围,就是与 Vertex Position 的值范围一致]
注:上面提到的所有坐标范围是指有效渲染范围,也就是说你如果提供的纹理坐标超出了这个范围也没有问题,只不过超出的部分就不渲染了;
顶点数据表示如下:
- 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使用一个纹理过滤器,我会使用下面的图像阐述每一种过滤模式:
- GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
这种方式为每个片段选择最近的纹理元素,当放大纹理时它的锯齿效果看起来相当明显,每个纹理单元都清楚地显示为一个小方块。
当我们缩小纹理时,因为没有足够的片段来绘制所有的纹理单元,许多细节将会丢失。
- GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
线性过滤使用双线插值平滑像素之间的过渡,而不是每个片段使用最近的纹理元素,OpenGL会使用四个邻接的纹理元素,并在他们之间用一个线性差值算法做差值,这个算法与前面介绍平滑着色的算法一样,之所以叫它双线性,是因为它是沿两个维度差值的,它会比近邻过滤要平滑很多,但还是会有一些锯齿显示出来,因为我们把这个纹理扩展得太多了,但是锯齿没有最近邻过滤那么明显。
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
运行结果: