从渲染管线学 GLES2.0(八)----纹理

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);
}

5、参考文章

图形学底层探秘 - 纹理采样、环绕、过滤与Mipmap的那些事 - 知乎

详解Cubemap、IBL与球谐光照 - 知乎

立方体贴图 - LearnOpenGL CN

纹理 - LearnOpenGL CN

  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: gles2-gears是一个基于OpenGL ES 2.0的图形渲染示例程序。该程序主要用于演示OpenGL ES 2.0中的基本图形渲染功能和性能。它由几个齿轮组成,每个齿轮由多个三角形组成,通过旋转和平移操作,可以观察到各个齿轮之间的相互作用和渲染效果。 通过使用OpenGL ES 2.0gles2-gears可以利用硬件加速图形渲染,提供高性能的图形处理能力。它支持通过着色器程序来实现各种渲染效果,如光照、阴影和纹理等。同时,该示例程序也提供了用户交互接口,用户可以通过触摸或鼠标操作来改变齿轮的旋转速度和方向,从而观察不同的视觉效果。 gles2-gears不仅是一个OpenGL ES 2.0的示例程序,也是一个性能测试工具。通过改变齿轮数量和分辨率等参数,可以测试设备对于大规模场景和高分辨率渲染的性能表现。这对于开发者来说是非常有价值的,可以帮助他们评估设备的图形处理能力,并根据测试结果进行相应的优化。 总而言之,gles2-gears是一个基于OpenGL ES 2.0的演示和性能测试程序,可以帮助开发者习和评估设备的图形处理能力。它充分利用硬件加速,通过渲染齿轮的旋转和交互操作,展示了OpenGL ES 2.0的高性能实时图形渲染能力。 ### 回答2: gles2-gears是一个基于OpenGL ES 2.0的开源项目,它展示了使用OpenGL ES 2.0绘制的齿轮模型。它是一个典型的图形示例,用于演示OpenGL ES 2.0的功能和特性。 在gles2-gears中,通过使用OpenGL ES 2.0的着色器语言GLSL,实现了光照、纹理贴图等高级渲染技术。整个场景由齿轮模型组成,通过旋转、缩放等操作,可以观察到齿轮之间的互动效果。 gles2-gears的源代码可用于OpenGL ES 2.0编程,了解图形渲染的基本原理和技术。通过阅读和理解其代码结构和逻辑,可以了解OpenGL ES 2.0的编程模式和渲染流程。 此外,gles2-gears还可以用作性能测试工具,用于测试硬件设备的图形渲染性能。通过调整渲染分辨率、齿轮数量等参数,可以评估设备的图形处理能力,并进行性能对比。 总的来说,gles2-gears是一个教育和性能测试的工具,用于展示和验证OpenGL ES 2.0的功能和性能。无论是初者还是专业开发者,都可以使用它来习和优化图形渲染技术。 ### 回答3: gles2-gears是一个使用OpenGL ES 2.0图形库编写的一个开源项目,它展示了三个互动的齿轮,用于测试和演示OpenGL ES 2.0的性能和功能。 在它的实现过程中,作者使用了OpenGL ES 2.0的着色器语言GLSL来处理图形渲染。齿轮之间的转动是通过在每个齿轮上应用旋转变换来实现的,通过修改齿轮的旋转角度和速度,可以调整和控制齿轮之间的相对运动。此外,作者还为齿轮和整个场景设计了适当的材质、光照和阴影效果,以增强视觉效果。 这个项目最初是为了展示OpenGL ES 2.0在移动设备上的性能和功能而创建的,但它也可以在其他支持OpenGL ES 2.0的平台上运行。用户可以通过触摸或鼠标交互来控制和改变齿轮的旋转和速度,从而创建不同的视觉效果和交互体验。 值得一提的是,该项目的源代码开放,并经常作为一种教工具,帮助人们习和理解OpenGL ES 2.0的基本概念和应用技巧。许多人使用和修改这个项目,以满足不同的需求和目标。 总的来说,gles2-gears是一个展示和测试OpenGL ES 2.0在三维图形渲染中的性能和功能的开源项目。它不仅仅是一个演示程序,还可以作为习和教工具来帮助人们更好地理解和应用OpenGL ES 2.0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值