1、纹理的环绕方式
纹理环绕
纹理环绕的作用主要是为了处理超出 [0, 1] 范围之外的纹理坐标,不同的纹理环绕方式对超出坐标处理的也不一样。
在 OpenGL ES 2.0 中,纹理环绕方式有 3 种
环绕方式 | 描述 |
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_REPEAT 对纹理的默认行为,重复纹理图像,也就是当纹理坐标为 [0, 1] 时可以不做处理,当纹理坐标大于 1 时只保留小数部分。
float CalcTexturePos(float pos, int mode) {
if (pos >= 0 && pos <= 1.0) {
return pos;
}
int val = (int)pos;
if (mode == GL_REPEAT) {
if (pos > 1.0) {
pos = pos - val;
} else if (pos < 0) {
pos = val - pos;
}
}
return pos;
}
GL_MIRRORED_REPEAT 和 GL_REPEAT 一样,但是每次重复图片都是镜像方式的,也就是当纹理坐标为 [0, 1] 时可以不做处理,当纹理坐标在 [1, 2] 时,和 [0, 1] 的时候是倒置的,如果是 1.9 那么其实计算出来会是 0.1,当纹理坐标为 [2, 3] 时又和 [1, 2] 是倒置的,但是这个时候就和 [0, 1] 是相同的。
float CalcTexturePos(float pos, int mode) {
if (pos >= 0 && pos <= 1.0) {
return pos;
}
int val = (int)pos;
if (mode == GL_MIRRORED_REPEAT) {
if (val % 2 == 0) {
// 这个时候和 GL_REPEAT 一样
if (pos > 1.0) {
pos = pos - val;
} else if (pos < 0) {
pos = val - pos;
}
} else {
// 镜像放置
if (pos > 1.0) {
pos = 1 - (pos - val);
} else if (pos < 0) {
pos = 1 + (pos - val);
}
}
}
return pos;
}
GL_CLAMP_TO_EDGE,纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。也就是说当纹理坐标为 [0,1] 时不做处理,大于 1 都被当作 1,小于 0 都被当作 0。
float CalcTexturePos(float pos, int mode) {
if (pos >= 0 && pos <= 1.0) {
return pos;
}
int val = (int)pos;
if (mode == GL_MIRRORED_REPEAT) {
if (pos > 1.0) {
pos = 1.0;
} else if (pos < 0.0) {
pos = 0.0;
}
}
return pos;
}
2、纹理的过滤方式
纹理过滤
在使用纹理的时候,会经常遇到把一张很大的图片放在一个小的图元里面,这个时候可能会引起失真。或者把一张小的图片放在一张大的图元里面,这个时候就会很模糊。那么过滤就是使用一些算法来对这些情况进行处理,使的纹理图像看起来更加的清晰真实清晰。
在 OpenGL ES 2.0 中,纹理过滤方式有 6 种,其中 4 种和 Mipmap 相关联。
过滤方式 | 描述 |
GL_NEAREST | 临近过滤,也是 OpenGL 的默认过滤方式,OpenGL会选择中心点最接近纹理坐标的那个像素 |
GL_LINEAR | 线性过滤,它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大 |
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
GL_NEAREST 临近过滤,选择中心点最接近纹理坐标的那个像素,比如计算出来的坐标为 100.8,那么更接近 101,那么坐标就是 101。
int u_pos = u * width - 0.5;
int v_pos = v * height - 0.5;
int addr = u_pos + v_pos * width
Color color = GetColor(addr);
GL_LINEAR 线性过滤,它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。
float fu = u * width - 0.5;
float fv = v * height - 0.5;
int iu = (int)fu;
int iv = (int)fv;
float decimal_u = fu - iu;
float decimal_v = fv - iv;
// 接下来计算 4 个地址的颜色
// 1. iu, iv
int addr = iu + iv * width;
Color color1 = GetColor(addr);
// 2. iu+1, iv
addr = (iu + 1) + iv * width;
Color color2 = GetColor(addr);
// 3. iu, iv+1
addr = iu + (iv + 1) * width;
Color color3 = GetColor(addr);
// 4. iu+1, iv+1
addr = (iu + 1) + (iv + 1) * width;
Color color4 = GetColor(addr);
// 插值计算最终颜色
Color tmp_0 = Lerp(color1, color2, f_u);
Color tmp_1 = Lerp(color3, color4, f_u);
Color result = Lerp(tmp_0, tmp_1, fv);
接下来 4 种和 Mipmap 相关的,我们放在下面纹理 Mipmap 里面介绍。
这里还需要注意的一点是,对于纹理放大(纹理小图元大)的情况,只能设置 GL_NEAREST 和 GL_LINEAR,对于纹理缩小(纹理大图元小)的情况,上面 6 种都可以设置
3、纹理的 Mipmap
Mipmap 又称为多级渐远纹理,简单来说它是一系列纹理图像,后一个纹理图像是前一个纹理图像的二分之一,就像下面这样。
Mipmap 主要解决的是纹理缩小引起的一系列问题,Mipmap 的原理是预先生成一系列以 2 为倍数缩小的纹理序列,在采样纹理时根据图形的大小自动选择相近等级的 Mipmap 进行采样。
生成 Mipmap 代码
// 生成 Mipmap
void glGenerateMipmap(GLenum target) {
// 第 0 级就是原始图像,所以从第一级开始
int texture; // 我们要生成的纹理编号
int format; // 纹理格式
/*
获取纹理编号和纹理格式,代码省略
*/
int src_width = texture_map[texture].width;
int src_height = texture_map[texture].height;
int dst_width = 0;
int dst_height = 0;
int stride = 0;
if (format == GL_RGB) {
stride = 3;
} else if (format == GL_RGBA) {
stride = 4;
} else if (format == GL_LUMINANCE || format == GL_ALPHA) {
stride = 1;
} else if (format == GL_LUMINANCE_ALPHA) {
stride = 2;
}
for (int i = 1; i < mipmap_max_level; ++i) {
if (!GetMipmapNextLevelSize(src_width, src_height, dst_width, dst_height)) {
break;
}
// 记录每层纹理的大小
texture_map[texture].texture_size_map[i].width = dst_width;
texture_map[texture].texture_size_map[i].height = dst_height;
// 计算当前 Mipmap 的大小
int size = dst_width * dst_height * stride;
context->texture_map[texture].data_map[i].resize(size);
GenMipmapData(texture, i, src_width, src_height, dst_width, dst_height);
src_width = dst_width;
src_height = dst_height;
}
}
// 判断是否存在下一层 Mipmap,以及对应大小
bool GetMipmapNextLevelSize(int src_width, int src_height, int& dst_width, int& dst_height) {
if (src_width == 1 && src_height == 1) {
return false;
}
if (src_width > 1) {
dst_width = src_width / 2;
} else {
dst_width = src_width;
}
if (src_height > 1) {
dst_height = src_height / 2;
} else {
dst_height = src_height;
}
return true;
}
// 生成数据
void GenMipmapData(char* data, int level, int src_width, int src_height, int dst_width, int dst_height) {
int comps = texture_map[texture].data_map[level].size() / (dst_width * dst_height);
int src_stride = src_width * comps;
int dst_stride = dst_width * comps;
int src_level = level - 1;
int src_index[4];
for (int i = 0; i < dst_height; ++i) {
src_index[0] = 2 * i * src_stride;
src_index[1] = src_index[0] + comps;
src_index[2] = src_index[0] + src_stride;
src_index[3] = src_index[1] + src_stride;
int dst_index = i * dst_stride;
for (int j = 0; j < dst_width; ++j) {
// std::cout << "dst_index: " << dst_index << std::endl;
for (int k = 0; k < comps; ++k) {
context->texture_map[texture].data_map[level][dst_index + j * comps+ k] =
(context->texture_map[texture].data_map[src_level][src_index[0] + k] +
context->texture_map[texture].data_map[src_level][src_index[1] + k] +
context->texture_map[texture].data_map[src_level][src_index[2] + k] +
context->texture_map[texture].data_map[src_level][src_index[3] + k]) / 4;
}
for (int k = 0; k < 4; ++k) {
src_index[k] = src_index[k] + 2 * comps;
}
}
}
}
计算合适的 Mipmap 等级
纹理一般都是用在片段着色器中,为什么叫片段着色器而不叫像素着色器,是因为正常情况下,我们都是按片段进行处理的,也就是像素块,所以我们根据相邻位置的像素块进行 Mipmap 等级的计算。
// diff_u, diff_v 代表相邻元素的 uv 差值
float CalcMipmapLevel(float diff_u, float diff_v) {
diff_u = diff_u * width;
diff_v = diff_v * height;
float delta = std::max(diff_u * diff_u, diff_v * diff_v);
float mipmap_level = 0.5 * log2(delta);
if (mipmap_level < 0) {
mipmap_level = 0;
}
return mipmap_level;
}
GL_A_MIPMAP_B
-
第一个参数 A 表示每个 Mipmap 层次里面的元素构成方式
-
第二个参数 B 表示我们应该使用什么样的方式处理 Mipmap 层
GL_NEAREST_MIPMAP_NEAREST
使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
1. 首先我们会计算出来一个 Mipmap 等级,比如这里我们计算出来是 0.8。
2. 因为是 0.8,并且对应参数是 NEAREST,那么我们选用更接近的 Mipmap 层,选用第 1 层。
3. 然后在第 1 层中计算对应 uv 坐标,计算出来之后再使用 NEAREST 临近插值计算最终的颜色。
GL_LINEAR_MIPMAP_NEAREST
使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
1. 首先我们会计算出来一个 Mipmap 等级,比如这里我们计算出来是 0.8。
2. 因为是 0.8,并且对应参数是 NEAREST,那么我们选用更接近的 Mipmap 层,选用第 1 层。
3. 然后在第 1 层中计算对应 uv 坐标,计算出来之后再使用 LINEAR 线性插值计算最终的颜色。
GL_NEAREST_MIPMAP_LINEAR
在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
1. 首先我们会计算出来一个 Mipmap 等级,比如这里我们计算出来是 0.8。
2. 因为 Mipmap 层之间使用的是线性插值,所以我们需要计算两个 Mipmap 之间的像素,所以这里我们要计算第 0 层和第 1 层对应坐标点的像素,计算的时候使用临近插值。
3. 最后在两个 Mipmap 计算出来像素使用线性插值计算最终的像素点。
GL_LINEAR_MIPMAP_LINEAR
又称三线性插值,在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
1. 首先我们会计算出来一个 Mipmap 等级,比如这里我们计算出来是 0.8。
2. 因为 Mipmap 层之间使用的是线性插值,所以我们需要计算两个 Mipmap 之间的像素,所以这里我们要计算第 0 层和第 1 层对应坐标点的像素,计算的时候使用线性插值。
3. 最后在两个 Mipmap 计算出来像素使用线性插值计算最终的像素点。
4、立方体贴图
立方体贴图就是一个包含 6 个 2D 纹理的纹理,每个 2D 纹理都组成了立方体的一个面。在片段着色器中我们使用 samplerCube 接收一个立方体贴图,并使用 textureCube 计算颜色信息。
由于我们有 6 个面,OpenGL 给我们提供了 6 个特殊的纹理目标,专门对应立方体贴图的一个面。
纹理目标 | 方位 | 其它 |
GL_TEXTURE_CUBE_MAP_POSITIVE_X | 右(right) | +X 轴 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X | 左(left) | -X 轴 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y | 上(top) | +Y 轴 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y | 下(bottom) | -Y 轴 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z | 后(back) | +Z 轴 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z | 前(front) | -Z 轴 |
这里我们主要讲一下怎么计算立方体贴图的具体的颜色,首先 textureCube 需要传入一个三维向量的坐标来计算像素,假设三维坐标值分别为 x, y, z。
1. 取 x, y, z 坐标中绝对值最大的为主轴。
2. 接下来再用正负号判断正负轴,比如 (-0.5, 0.1, 0.2) 对应 -X 轴,也就是 left 方位。
3. 根据主轴和正负轴,计算 uv 坐标,那么这里我们计算出来就是 (0.4, -0.2),计算方式如下
float mag = std::max(std::max(fabs(x), fabs(y)), fabs(z));
+X 轴
u = -z / mag;
v = -y / mag;
-X 轴
u = z / mag;
v = -y / mag;
+Y 轴
u = x / mag;
v = z / mag;
-Y 轴
u = x / mag;
v = -z / mag;
+Z 轴
u = x / mag;
v = -y / mag;
-Z 轴
u = -x / mag;
v = -y / mag;
4. 从[-1, 1]缩放到[0, 1]。(0.4, -0.2) -> (0.7, 0.4)
u = u * 0.5 + 0.5;
v = v * 0.5 + 0.5;
5. 从-X轴的贴图上以UV坐标采样
全部代码实现
Color GetCubeData(float x, float y, float z) {
float u, v;
float mag = std::max(std::max(fabs(x), fabs(y)), fabs(z));
if (mag == fabs(x)) {
index = x >= 0 ? 0 : 1;
u = z / mag;
v = -y / mag;
if (x >= 0) {
u = -z / mag;
}
} else if (mag == fabs(y)) {
index = y >= 0 ? 2 : 3;
u = x / mag;
v = -z / mag;
if (y >= 0) {
v = z / mag;
}
} else if (mag == fabs(z)) {
index = z >= 0 ? 4 : 5;
u = -x / mag;
v = -y / mag;
if (z >= 0) {
u = x / mag;
}
}
u = u * 0.5 + 0.5;
v = v * 0.5 + 0.5;
return GetData(u ,v ,index);
}