当前位置: 首页 > news >正文

【openGLES】纹理

文章目录

  • 纹理基础
    • 2D纹理
      • 🧭 纹理坐标(UV坐标)
      • ⚙️ 在OpenGL ES中使用2D纹理
    • 立方图纹理
      • 创建立方图纹理
      • 主要应用场景
      • 注意事项
    • 3D纹理
      • 🧱 核心工作原理
      • ⚙️ 创建与使用3D纹理(OpenGL ES示例)
      • 🎯 主要应用场景
      • ⚠️ 挑战与局限性
      • 💡 优化策略
    • 2D纹理数组
      • 🧱 核心工作原理
      • ⚙️ 创建与使用流程(OpenGL ES)
      • 🚀 性能优势
      • ⚠️ 重要注意事项
    • 设置纹理过滤方式
      • 🎯 详细参数说明
    • MipMap贴图
      • 🧠 核心概念与原理
      • 🎯 主要作用与优势
      • ⚖️ 代价与缺点
      • 🛠️ 在 OpenGL/OpenGL ES 中的使用
    • 纹理格式
    • 在着色器中使用纹理
      • 🖼️ 一、纹理的准备与设置 (CPU 端)
      • ⚙️ 二、在着色器中采样纹理 (GPU 端)
        • 1. 顶点着色器(Vertex Shader)
        • 2. 片元着色器(Fragment Shader)
      • 📱 三、特殊纹理类型:采样外部纹理 (Android)
      • 🚀 四、其他纹理采样函数
    • 如何使用多个纹理
      • 🧠 核心概念:纹理单元
      • 📝 使用多个纹理的步骤
      • ⚠️ 重要注意事项
    • OES纹理和普通纹理的区别
    • 压缩纹理
      • 🧠 核心原理与价值
      • 📊 常见纹理压缩格式
      • ⚙️ 工作流程:压缩与使用
      • 🎨 视觉质量与性能权衡
      • 📱 平台兼容性实践
    • 从颜色缓冲区复制纹理数据
      • 指定数据源
      • 🧭 工作原理与参数选择
      • ⚠️ 重要注意事项
      • 复制数据
    • 采样器对象
      • 🧰 核心工作原理
      • ⚙️ 创建与管理采样器对象
      • 🎯 采样器对象的使用场景与优势
      • 📝 使用示例
      • ⚠️ 注意事项
    • 不可变纹理
      • 🧱 核心特点
      • ⚙️ 如何创建不可变纹理
      • 🆚 不可变纹理 vs 可变纹理
      • 💡 使用场景与优势
      • ⚠️ 注意事项
    • 像素解包缓冲区对象
      • 🧠 核心工作原理
      • 🚀 性能优势
      • 📝 使用提示

纹理基础

2D纹理

2D纹理是计算机图形学中最基础且应用最广泛的纹理类型,它本质上是一张二维图像(如.jpg、.png文件),通过特定的映射方式“贴”到3D模型的表面,从而为模型赋予丰富的表面细节(如颜色、图案、粗糙度等),而无需增加模型的几何复杂度。

为了让你能快速掌握核心概念,我先把2D纹理的关键特性总结在下面的表格里:

特性维度说明
本质一个存储图像数据的二维数组,每个元素称为“纹素”(Texel)
纹理坐标通常用 (s, t)(u, v) 表示,范围一般为 [0.0, 1.0],原点 (0,0) 通常位于纹理图像的左下角(注意:有些图像库的坐标系原点在左上角,可能需要翻转)
主要作用为3D模型表面提供颜色、法线、高光等细节信息,大幅提升视觉真实感,同时避免模型过于复杂
核心优势细节表现:用纹理替代几何细节;资源复用:一张纹理可用于多个模型;动态变化:通过修改纹理或坐标实现动画
常见应用模型漫反射贴图、法线贴图、高光贴图、UI元素、渲染目标等

🧭 纹理坐标(UV坐标)

纹理坐标,通常称为 UV 坐标,是连接3D模型顶点与2D纹理图像的桥梁。它定义了模型表面的点与纹理像素(纹素)的对应关系:

  • 坐标系:采用二维坐标 (u, v)(或 (s, t))。
  • 范围:通常归一化到 [0.0, 1.0] 范围内。坐标 (0.0, 0.0) 通常对应纹理图像的左下角(1.0, 1.0) 对应右上角
  • 插值:UV坐标在顶点着色器中定义,经过光栅化后,在片元着色器中插值得到每个片元具体的纹理坐标,用于采样。

当纹理坐标超出 [0, 1] 范围时,其行为由纹理环绕模式(Wrapping Mode) 决定,常见模式有:

  • GL_REPEAT重复纹理图像(默认行为)。
  • GL_CLAMP_TO_EDGE:将坐标钳制在 [0,1] 范围,边缘纹素被拉伸。
  • GL_MIRRORED_REPEAT镜像重复纹理图像。

⚙️ 在OpenGL ES中使用2D纹理

在OpenGL ES中创建和使用一个2D纹理通常遵循以下步骤:

  1. 创建纹理对象
    使用 glGenTextures 生成一个纹理对象(获取纹理ID)。

    GLuint textureID;
    glGenTextures(1, &textureID); // 生成1个纹理,ID存入textureID
    
  2. 绑定纹理对象
    使用 glBindTexture 将纹理绑定到目标 GL_TEXTURE_2D,后续操作都针对此绑定纹理。

    glBindTexture(GL_TEXTURE_2D, textureID);
    
  3. 设置纹理参数
    使用 glTexParameteri 设置纹理的过滤方式环绕方式

    // 设置缩小和放大过滤方式(如线性过滤)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 设置S和T方向的环绕方式(如重复)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    
  4. 加载图像数据到纹理
    使用 glTexImage2D 将CPU内存中的图像数据上传到GPU的纹理对象中。

    glTexImage2D(GL_TEXTURE_2D,      // 纹理目标0,                  // mipmap级别(0为基础级别)GL_RGBA,            // 纹理在GPU中的内部格式(如GL_RGBA)width,              // 纹理宽度(像素)height,             // 纹理高度(像素)0,                  // 边框(必须为0)GL_RGBA,            // 源像素数据格式GL_UNSIGNED_BYTE,   // 源像素数据类型imageData           // 指向图像数据的指针
    );
    

    更现代的做法是使用不可变纹理(OpenGL ES 3.0+),先通过 glTexStorage2D 分配固定存储,再用 glTexSubImage2D 填充数据。

  5. 生成Mipmap(可选)
    为纹理生成一系列逐渐缩小的多级渐远纹理,有助于提升缩小渲染时的视觉质量和性能。

    glGenerateMipmap(GL_TEXTURE_2D);
    
  6. 在着色器中使用纹理

    • 顶点着色器:接收顶点属性传来的纹理坐标,并传递给片元着色器。
      in vec2 aTexCoord; // 输入的纹理坐标
      out vec2 vTexCoord; // 输出给片元着色器
      void main() {... // 处理位置vTexCoord = aTexCoord;
      }
      
    • 片元着色器:使用采样器(Sampler) uniform sampler2Dtexture 函数,根据插值后的纹理坐标对纹理进行采样,获取颜色值。
      uniform sampler2D uTexture; // 纹理采样器
      in vec2 vTexCoord;          // 插值后的纹理坐标
      out vec4 fragColor;         // 输出的片元颜色void main() {fragColor = texture(uTexture, vTexCoord); // 纹理采样// 也可混合其他属性,如:fragColor = texture(uTexture, vTexCoord) * vec4(vColor, 1.0);
      }
      
    • 应用程序中:在渲染前,需要激活纹理单元并将纹理绑定到该单元,然后将纹理单元的编号传递给着色器中的采样器。
      glActiveTexture(GL_TEXTURE0); // 激活纹理单元0
      glBindTexture(GL_TEXTURE_2D, textureID); // 将纹理绑定到当前激活的单元
      GLint loc = glGetUniformLocation(shaderProgram, "uTexture");
      glUniform1i(loc, 0); // 告诉着色器采样器使用纹理单元0
      

立方图纹理

立方图纹理(Cube Map)是OpenGL ES中一种重要的3D纹理映射技术,它由6个独立的2D纹理面组成,分别对应立方体的六个面(右、左、上、下、后、前)。与2D纹理使用二维坐标采样不同,立方图纹理使用三维方向向量进行采样,该向量从立方体中心指向外部,用于确定采样点位于立方体的哪个面上以及具体的纹理位置(纹素)。

创建立方图纹理

创建立方图纹理的过程与2D纹理类似,但需要为立方体的六个面分别指定图像数据。

GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);

立方图纹理有六个面,每个面都需要通过glTexImage2D函数加载图像数据。OpenGL ES提供了六个特定的纹理目标来对应每个面:

  • GL_TEXTURE_CUBE_MAP_POSITIVE_X(右)
  • GL_TEXTURE_CUBE_MAP_NEGATIVE_X(左)
  • GL_TEXTURE_CUBE_MAP_POSITIVE_Y(上)
  • GL_TEXTURE_CUBE_MAP_NEGATIVE_Y(下)
  • GL_TEXTURE_CUBE_MAP_POSITIVE_Z(后)
  • GL_TEXTURE_CUBE_MAP_NEGATIVE_Z(前)
    你可以使用一个循环,通过递增纹理目标枚举值来为每个面加载图像
for (unsigned int i = 0; i < 6; i++)
{// 假设 images[i] 已经加载了六个面的图像数据glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA, images[i].width, images[i].height, 0, GL_RGBA, GL_UNSIGNED_BYTE, images[i].data);
}

为立方图纹理设置环绕方式和过滤模式。对于环绕方式,通常将S、T、R坐标都设置GL_CLAMP_TO_EDGE,以避免在立方体边缘采样时出现接缝

glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

片段着色器示例

precision mediump float;
varying vec3 vTexCoord;
uniform samplerCube uSkybox;void main() {gl_FragColor = textureCube(uSkybox, vTexCoord); // OpenGL ES 2.0// 在OpenGL ES 3.0及更高版本中,也可以使用:// gl_FragColor = texture(uSkybox, vTexCoord);
}

主要应用场景

  • 天空盒:创建一个无限大的环境背景,将观察者包围在一个场景中,是立方图纹理最经典的应用。绘制天空盒时,通常将其深度设置为最大值(例如,将顶点着色器中的gl_Position的z分量设置为w分量,这样透视除法后深度值为1.0),并确保其不会被场景中的其他物体遮挡。同时,为了确保观察者移动时天空盒不会相对移动,通常需要去除视图矩阵中的平移部分,只保留旋转。
  • 反射:模拟光滑表面(如金属、镜子)反射周围环境的效果。反射向量 R 可以通过入射向量 I(从相机到片段的位置向量)和表面法线 N 计算得到:R = reflect(I, N)。然后用 R 作为方向向量对立方图纹理进行采样。
  • 折射:模拟光线通过透明介质(如玻璃、水)时发生的弯曲现象。折射向量可以通过斯涅尔定律计算,OpenGL ES提供了refract函数来方便计算。然后用计算出的折射向量对立方图纹理进行采样。

注意事项

  • ​Android纹理坐标系​:需要注意的是,Android平台的Bitmap库通常以左上角为原点,而OpenGL ES纹理坐标默认以左下角为原点。直接加载Bitmap到立方图纹理可能导致图像上下颠倒。解决方法包括在加载前垂直翻转Bitmap,或在着色器中对纹理坐标进行调整(例如,将t坐标转换为1.0 - t)。

3D纹理

3D纹理(3D Texture),也称为体积纹理(Volume Texture),是计算机图形学中一种在三维空间中定义纹理数据的技术。与2D纹理(只有宽和高)不同,3D纹理增加了深度(Depth)维度,形成了一个真正的三维体素(Voxel)网格。每个体素都存储了颜色、密度或其他材质信息

特性3D纹理2D纹理
维度3维 (宽度, 高度, 深度)2维 (宽度, 高度)
基本单元体素 (Voxel)纹素 (Texel)
数据组织三维体素网格二维像素网格
纹理坐标(s, t, r)r 是深度坐标,在 [0.0, 1.0] 内连续采样(s, t)
采样器sampler3Dsampler2D
内存占用非常高 (尺寸的三次方增长)相对较低
主要应用体积渲染、医学成像、复杂材质模拟模型表面贴图、UI元素、渲染目标

🧱 核心工作原理

3D纹理的本质是一个三维数组,其每个元素(体素)包含了该空间位置上的属性值(如RGBA颜色、密度、法线等)。在渲染时,通过三维纹理坐标 (s, t, r) 对纹理进行采样:

  • s, t, r 三个分量通常都在 [0.0, 1.0] 范围内。
  • 采样时,GPU会在三个维度上进行插值计算(如三线性过滤),以获取平滑的过渡效果。

⚙️ 创建与使用3D纹理(OpenGL ES示例)

在OpenGL ES 3.0及更高版本中,创建和使用3D纹理的基本步骤如下:

  1. 创建纹理对象并绑定

    GLuint texture3D;
    glGenTextures(1, &texture3D);
    glBindTexture(GL_TEXTURE_3D, texture3D); // 注意目标是GL_TEXTURE_3D
    
  2. 分配存储空间并填充数据(使用不可变存储推荐):

    // 使用glTexStorage3D分配不可变存储
    glTexStorage3D(GL_TEXTURE_3D, levels, GL_RGBA8, width, height, depth);
    // 或使用glTexImage3D分配存储并上传数据
    glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, width, height, depth, 0, GL_RGBA, GL_UNSIGNED_BYTE, volumeData);
    

    其中 width, height, depth 定义了3D纹理的尺寸,volumeData 是指向三维体积数据的指针。

  3. 设置纹理参数

    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); // 注意R轴的环绕方式
    
  4. 在着色器中采样
    在GLSL着色器中,使用 sampler3D 声明采样器,并通过 texture() 函数进行采样。

    #version 300 es
    uniform sampler3D u_VolumeTexture;
    in vec3 v_VolumeTexCoord; // 三维纹理坐标
    out vec4 fragColor;void main() {fragColor = texture(u_VolumeTexture, v_VolumeTexCoord);
    }
    

🎯 主要应用场景

  1. 体积渲染 (Volume Rendering)
    这是3D纹理最经典的应用。通过采样3D纹理来渲染半透明的体积介质,如:

    • 云、烟、火:通过3D纹理定义其密度和颜色,通过光线步进(Ray Marching)算法进行渲染。
    • 医学成像(CT、MRI):将扫描数据(如DICOM)转换为3D纹理,可视化人体器官、骨骼等内部结构。
  2. 复杂材质模拟
    模拟木材、大理石、奶酪等具有内部结构的材质。当物体被切开时,其截面也能显示出连续的内部纹理,这是2D纹理无法实现的。

  3. 三维噪声
    使用3D噪声纹理(如Perlin Noise)来实时生成各种特效,如地形高度图、云层图案、流体模拟等。由于噪声是3D的,所以效果在空间中无缝连续。

⚠️ 挑战与局限性

  1. 巨大的内存占用
    3D纹理的内存消耗是 O(n³)。一个普通的 256x256x256、RGBA8格式的3D纹理,其未压缩大小约为 64MB (256*256*256*4 Bytes)。这对于移动设备的显存是巨大压力。

  2. 性能开销
    采样3D纹理比采样2D纹理需要更多的计算资源,尤其是在进行三线性过滤(Trilinear Filtering)时,需要采样并混合8个体素(2D纹理的双线性过滤只需4个纹素)。

  3. 压缩至关重要
    由于内存占用巨大,纹理压缩是使用3D纹理的必备技术。专为3D纹理设计的压缩格式(如基于ASTC的3D压缩)可以显著减少内存占用和带宽需求。

  4. 硬件支持
    3D纹理需要OpenGL ES 3.0及以上版本的支持。在移动设备上,虽然多数现代设备都支持,但仍需在代码中检查其可用性。

💡 优化策略

  1. 使用压缩纹理
    尽可能使用压缩格式(如ETC2、ASTC)的3D纹理,或专为体积数据设计的压缩格式,以大幅减少内存占用。

  2. 稀疏纹理(Sparse Texture)
    OpenGL ES 3.1引入了稀疏纹理(一种高级特性),它允许你只加载3D纹理中当前需要的部分(Tile),而不是整个体积数据,这对于处理超大的3D纹理非常有效。

  3. 降低分辨率
    在视觉效果和性能之间权衡,适当降低3D纹理的深度分辨率(有时甚至宽度和高度),因为深度维度对内存的影响最大。

  4. 替代方案
    有时可以用2D纹理数组(2D Texture Array) 来模拟3D纹理。它由多个2D切片组成,可以通过索引访问,但在R轴方向不支持插值。适用于那些在深度方向上离散的数据(如动画帧、地层切片)。

2D纹理数组

2D纹理数组是OpenGL ES 3.0引入的一种强大纹理类型,它允许你将多个尺寸和格式相同的2D纹理以数组的形式组织和管理,并通过一个采样器在着色器中进行访问。这极大地简化了多纹理操作,并提升了性能。

特性2D纹理数组 (Texture2D Array)普通2D纹理3D纹理
数据组织多个2D纹理的有序集合(像一本书,每页一张独立图片)单一的2D图像一个连续的3D体积数据(像一块豆腐)
纹理坐标(s, t, layer)layer是数组索引(整数部分起作用)(s, t)(s, t, r)r是深度坐标(在[0,1]内连续插值)
采样器类型sampler2DArraysampler2Dsampler3D
主要用途地形材质、纹理动画帧、角色多皮肤模型表面贴图、UI体积渲染(云、雾)、医学数据
插值方式在s和t方向插值,layer索引通常取整(或按 nearest 过滤)在s和t方向插值在s、t和r方向都进行插值
内存占用较高(存储多个纹理)较低非常高(尺寸三次方增长)

🧱 核心工作原理

你可以把2D纹理数组想象成一本

  • 这本书的每一页(Slice) 都是一张独立的、尺寸和格式完全相同的2D图片。
  • 采样时,你需要提供三个坐标:(s, t) 用来定位到某一页上的某个特定纹素,而 layer(或称为r坐标)则用来决定翻到哪一页。通常,layer坐标的整数部分直接作为数组索引,因此它不像真正的3D纹理那样在“层”之间进行插值。

这种结构使得它非常适合管理一系列相关的图像,例如:

  • 地形系统中不同的地表材质(草地、泥土、沙石、雪地)。
  • 纹理动画的每一帧序列。
  • 角色不同的皮肤或外观变体

⚙️ 创建与使用流程(OpenGL ES)

在OpenGL ES 3.0+中,创建和使用2D纹理数组通常遵循以下步骤:

  1. 创建纹理对象并绑定到正确目标

    GLuint textureArrayID;
    glGenTextures(1, &textureArrayID);
    // 注意:目标是 GL_TEXTURE_2D_ARRAY
    glBindTexture(GL_TEXTURE_2D_ARRAY, textureArrayID);
    
  2. 分配存储空间(使用不可变存储推荐)
    使用 glTexStorage3D 为纹理数组一次性分配所有层级的存储空间,这是高效且现代的做法。

    // 为纹理数组分配空间:2层,每层尺寸 512x512,格式为 RGBA8
    glTexStorage3D(GL_TEXTURE_2D_ARRAY,1,              // Mipmap 层级数 (此处为1,无多级渐远)GL_RGBA8,       // 内部格式512, 512,       // 每层纹理的宽度和高度2);             // 数组的层数(即纹理个数)
    
  3. 为每一层填充图像数据
    使用 glTexSubImage3D 为数组中的每一层(每个纹理)上传图像数据。

    // 为第0层(数组索引从0开始)填充数据
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY,0,              // Mipmap 级别0, 0, 0,        // 目标层内的偏移量 (xoffset, yoffset, zoffset)512, 512, 1,    // 当前层数据的宽、高、深度(深度为1,指一层)GL_RGBA, GL_UNSIGNED_BYTE,imageData0);    // 指向第0张图片数据的指针// 为第1层填充数据
    glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, 512, 512, 1, GL_RGBA, GL_UNSIGNED_BYTE, imageData1);
    
  4. 设置纹理参数
    参数设置方式与2D纹理类似,但注意环绕模式也适用于R方向(层方向,尽管通常设置为 GL_CLAMP_TO_EDGE)。

    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); // 处理层索引超出范围的情况
    
  5. 在着色器中使用
    在着色器中,你需要使用 sampler2DArray 类型的采样器统一变量,并提供 vec3 类型的纹理坐标,其中第三个分量(通常命名为 rlayer)是数组索引
    顶点着色器

    #version 300 es
    layout(location = 0) in vec4 aPosition;
    layout(location = 1) in vec3 aTexCoord; // 注意是 vec3,包含层索引
    out vec3 vTexCoord;
    void main() {gl_Position = aPosition;vTexCoord = aTexCoord;
    }
    

    片元着色器

    #version 300 es
    precision mediump float;
    precision mediump sampler2DArray; // 为 sampler2DArray 声明精度
    in vec3 vTexCoord;
    uniform sampler2DArray uTextureArray; // 采样器类型是 sampler2DArray
    out vec4 fragColor;
    void main() {// 使用三层纹理坐标进行采样// vTexCoord.z (或 .r) 是数组索引,决定使用哪一层纹理fragColor = texture(uTextureArray, vTexCoord);
    }
    

🚀 性能优势

使用2D纹理数组的核心优势在于性能优化

  • 减少API调用与绑定操作:传统方式中,若要使用N张纹理,需要调用多次 glBindTextureglActiveTexture,并设置多个采样器统一变量。使用纹理数组后,只需一次绑定,极大减少了CPU开销。
  • 简化着色器管理:着色器中只需声明一个 sampler2DArray 统一变量,而不是多个 sampler2D。这使得着色器代码更简洁,更易于管理,并且支持动态数量的纹理,提高了灵活性。
  • 提升缓存效率:由于数组中的纹理在内存中是连续或按特定规则排列的,GPU采样器在访问相邻层或相同位置的纹素时可能拥有更高的缓存命中率。

⚠️ 重要注意事项

  1. 同质要求:纹理数组中的所有纹理必须具有完全相同的尺寸(宽度、高度)、格式和Mipmap层级数量。这是使用纹理数组的一个基本限制。
  2. 层索引:在着色器中采样时,纹理坐标的第三个分量(层索引)通常是浮点数,但GPU会将其转换为整数(具体转换方式取决于过滤模式,如 GL_NEAREST 或线性过滤下的特殊处理)来选取具体的层。确保该值在有效的层范围内,或通过环绕模式处理越界情况。
  3. 兼容性:2D纹理数组是 OpenGL ES 3.0 及更高版本的特性。在移动端开发时,务必检查目标设备的支持情况。
  4. 并非真正的3D纹理:务必理解2D纹理数组与3D纹理的本质区别。2D纹理数组是层的集合,每层是独立的2D图像;而3D纹理是一个连续的体积数据块,在三个维度上都会进行插值,用于表现连续变化的体积信息,如烟雾、云层或医学CT数据。

设置纹理过滤方式

glTexParameteri 是 OpenGL 和 OpenGL ES 中用于配置纹理参数的重要函数。

参数名含义与作用常用值示例
target指定要操作的纹理目标GL_TEXTURE_2D(操作2D纹理)GL_TEXTURE_CUBE_MAP(立方体贴图)GL_TEXTURE_EXTERNAL_OES(Android特有)
pname指定要设置的纹理参数名称GL_TEXTURE_MIN_FILTER(缩小过滤)GL_TEXTURE_MAG_FILTER(放大过滤)GL_TEXTURE_WRAP_S(S轴环绕方式)
parampname 参数指定的纹理属性设置的具体值GL_LINEAR(线性过滤)GL_REPEAT(重复环绕)GL_CLAMP_TO_EDGE(钳位到边缘)

🎯 详细参数说明

  1. target(纹理目标)
    这个参数指定了你当前绑定的是哪种类型的纹理,后续的参数设置将应用于这个目标。常见的值包括:

    • GL_TEXTURE_2D:这是最常用的,表示操作的是一个普通的2D纹理 。
    • GL_TEXTURE_CUBE_MAP:表示操作的是一个立方体贴图(Cubemap),用于天空盒、环境反射等。
    • 在 Android 平台上,还可能遇到 GLES11Ext.GL_TEXTURE_EXTERNAL_OES,这是一种特殊的纹理类型,通常用于预览相机或视频 。
  2. pname(参数名称)
    这个参数告诉 OpenGL 你想要设置纹理的哪一项属性。主要可以分为以下几类:

    • 过滤方式 (Filtering)
      • GL_TEXTURE_MIN_FILTER:当纹理被缩小(纹理像素小于屏幕像素)时采用的过滤算法。因为需要处理多个纹素如何合并成一个像素的情况,所以选项较多,包括使用 Mipmap 的选项 。
      • GL_TEXTURE_MAG_FILTER:当纹理被放大(纹理像素大于屏幕像素)时采用的过滤算法。通常只有 GL_NEARESTGL_LINEAR
    • 环绕方式 (Wrapping)
      • GL_TEXTURE_WRAP_S:定义纹理坐标在 S 轴(水平方向,类似 X 轴) 上超出 [0, 1] 范围时的行为 。
      • GL_TEXTURE_WRAP_T:定义纹理坐标在 T 轴(垂直方向,类似 Y 轴) 上超出 [0, 1] 范围时的行为 。
    • 其他参数
      • 例如 GL_TEXTURE_BORDER_COLOR(设置边框颜色)等,这些可能在某些高级用法或特定扩展中使用。
  3. param(参数值)
    这个参数为 pname 所指定的属性设置具体的值。其可选值取决于你要设置的参数名称:

    • 对于过滤参数 (GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER)
      • GL_NEAREST邻近过滤):取纹理坐标最接近的那个纹素的颜色。速度快,但可能产生锯齿状的块状效果 。
      • GL_LINEAR线性过滤):取纹理坐标附近几个纹素的加权平均值。效果更平滑,但计算量稍大 。
      • 仅用于 GL_TEXTURE_MIN_FILTER 的 Mipmap 选项
        • GL_NEAREST_MIPMAP_NEAREST
        • GL_LINEAR_MIPMAP_NEAREST
        • GL_NEAREST_MIPMAP_LINEAR
        • GL_LINEAR_MIPMAP_LINEAR(又称三线性过滤,效果最平滑)
    • 对于环绕参数 (GL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_T)
      • GL_REPEAT重复纹理。这是默认行为,纹理在表面上平铺 。
      • GL_CLAMP_TO_EDGE:将纹理坐标钳位到边缘,即超出范围的部分使用边缘像素的颜色进行延伸。常用于避免纹理在边缘处出现不希望的重复(如地板纹理)。
      • GL_MIRRORED_REPEAT镜像重复纹理,每次重复都会进行镜像翻转 。

MipMap贴图

Mipmap(多级渐远纹理)是一项在计算机图形学中广泛使用的纹理映射技术,主要用于提升渲染效率和视觉效果。

🧠 核心概念与原理

Mipmap 的核心思想是为同一张纹理图像预生成一系列分辨率逐级减半的副本,形成一个像金字塔一样的结构(称为纹理金字塔)。这些副本共同构成了一套“多级渐远纹理”。

  • 层级结构:假设原始纹理(Level 0)的大小是 1024x1024 像素,那么其 Mipmap 链通常包括:
    • Level 0: 1024x1024 (原始尺寸)
    • Level 1: 512x512
    • Level 2: 256x256
    • … 以此类推,直到 1x1 像素。
  • LOD(Level of Detail):在渲染时,GPU 会根据像素在屏幕上的覆盖面积与纹理纹素的比例,自动计算并选择一个合适的 Mipmap 级别(这个过程称为 LOD 选择)。物体离相机越远,屏幕投影面积越小,就会选择越高(分辨率越低)的 Mipmap 级别

🎯 主要作用与优势

使用 Mipmap 技术主要能带来以下几方面的好处:

  • 有效减少纹理闪烁和摩尔纹:当物体离相机很远时,一个屏幕像素可能对应纹理上的一大片区域(多个纹素)。如果不使用 Mipmap,直接对高分辨率纹理进行采样,由于采样不足,会导致颜色剧烈变化,产生令人不适的闪烁(Flickering)摩尔纹(Moire Pattern)。Mipmap 通过使用预先经过滤波处理的下采样图像,使得远处物体的纹理过渡更平滑,显著减少了这些视觉瑕疵。
  • 提升渲染性能:当需要渲染的物体较小时,直接使用其高分辨率的原始纹理不仅浪费显存带宽(需要读取大量无用数据),也对缓存不友好。Mipmap 允许 GPU 直接读取尺寸更小、更合适的纹理级别,减少了数据访问量,从而提高了采样效率和渲染速度。
  • 改善缓存命中率:较小的纹理更容易完全放入 GPU 的高速缓存中。当纹理数据连续被访问时(纹理坐标变化不大),使用合适级别的 Mipmap 可以显著提高缓存命中率,避免频繁从显存中读取数据,进一步提升了性能。

⚖️ 代价与缺点

当然,使用 Mipmap 也需要付出一定的代价:

  • 增加内存占用:存储整个 Mipmap 链需要额外的显存空间。理论上,其总内存占用量大约是原始纹理的 1/3(无穷级数求和:1 + 1/4 + 1/16 + 1/64 + … ≈ 4/3)。
  • 可能造成轻微模糊:对于非正方形或各向异性的表面(如锐利倾斜的平面),标准的 Mipmap 选择机制有时会使纹理看起来比实际情况更模糊一些。这通常可以通过各向异性过滤(Anisotropic Filtering) 技术来显著改善。

🛠️ 在 OpenGL/OpenGL ES 中的使用

在 OpenGL 和 OpenGL ES 中,使用 Mipmap 通常涉及以下步骤:

  1. 生成 Mipmap 链
    • 自动生成(最常见):在指定了 Level 0 的基础纹理数据后,调用 glGenerateMipmap(GL_TEXTURE_2D),驱动会自动为你生成所有更高级别的 Mipmap。
    • 手动指定:你也可以为每个级别调用 glTexImage2D,手动提供每一级的图像数据。
  2. 设置过滤参数这是关键的一步,通过 glTexParameteri 来设置缩小过滤器(GL_TEXTURE_MIN_FILTER),以启用 Mipmap 采样

纹理格式

OpenGL ES 中的纹理格式定义了纹理图像数据在内存和显存中的组织方式,直接影响渲染效果、内存占用和性能。

格式类别常见格式示例数据范围与用途特点与性能
未确定大小格式GL_RGB, GL_RGBA, GL_LUMINANCE由实现决定具体存储位数,兼容性好方便但控制力弱,可能因设备而异
确定大小格式GL_RGB8, GL_RGBA8明确指定每个通道的位数,控制精确渲染一致性好,推荐在新项目中使用
浮点纹理格式GL_RGBA16F, GL_R11F_G11F_B10F存储HDR高动态范围数据,范围远超[0,1]需要更多显存和带宽,适合高精度计算和HDR渲染
sRGB纹理格式GL_SRGB8, GL_SRGB8_ALPHA8存储伽马校正后的颜色数据,用于色彩正确的渲染自动进行伽马转换,保证颜色在不同设备上显示一致
压缩纹理格式ETC1, ETC2, ASTC显著减少纹理内存占用和带宽移动平台主流,需设备支持,不可直接读写像素
深度/模板格式GL_DEPTH_COMPONENT16, GL_DEPTH24_STENCIL8存储深度和模板信息,用于阴影、遮挡等效果专用于深度和模板测试,不存储颜色信息

🧠 理解纹理格式的关键参数

在OpenGL ES中,通过 glTexImage2D 等函数指定纹理格式时,会遇到几个关键参数:

  • internalformat:指定纹理数据在GPU内存(显存)中的存储格式。它定义了纹理的内部组织方式,如颜色分量、位数和数据类型。这就是上表所列的各种格式。
  • format:指定提供的源像素数据(在CPU内存中) 的格式。例如 GL_RGB, GL_RGBA
  • type:指定源像素数据的数据类型,如 GL_UNSIGNED_BYTE(每个通道8位无符号整数),GL_FLOAT(浮点数),GL_UNSIGNED_SHORT_5_6_5(RGB565压缩格式)等。

internalformatformattype 参数的组合必须是有效的,无效组合会导致运行时错误。通常建议保持 internalformatformat 一致或兼容,以减少驱动程序在纹理上传时进行格式转换的开销。

⚙️ 选择纹理格式的考量因素

为你的应用选择合适的纹理格式时,需要考虑以下几点:

  1. 视觉需求 vs 性能
    • 高质量、高精度:需要透明通道或高精度颜色时,选择 GL_RGBA8 或浮点格式(如 GL_RGBA16F)。但这会消耗更多内存和带宽。
    • 性能优先、无透明:如果不需要透明通道,GL_RGB8GL_RGBA8 少25%的内存占用。压缩纹理格式能极大节省内存和带宽,是移动设备上的首选。
  2. 平台兼容性
    • 较旧的GPU可能不支持较新的压缩格式(如ASTC)或浮点纹理。需要查询扩展支持或使用备选格式。
    • GL_LUMINANCEGL_ALPHA 等格式在OpenGL ES 3.0+中已被标记为已弃用,建议使用 GL_REDGL_RG 等替代。
  3. 色彩空间
    • 对于大多数颜色纹理,使用线性空间的格式(如 GL_RGB8)。
    • 如果纹理图像是在sRGB色彩空间下制作或存储的(如JPEG纹理),并且你希望OpenGL ES在采样时自动转换回线性空间,则应使用sRGB格式(如 GL_SRGB8),以保证颜色渲染的正确性。
  4. 特殊用途
    • HDR渲染:使用浮点纹理格式(GL_RGBA16F, GL_R11F_G11F_B10F)。
    • 延迟渲染G-Buffer:使用多种格式存储位置、法线、颜色等不同信息(如 GL_RGB10_A2, GL_RGBA16F)。
    • 深度测试:使用深度纹理格式(GL_DEPTH_COMPONENT16/24/32F)。

在着色器中使用纹理

🖼️ 一、纹理的准备与设置 (CPU 端)

在着色器使用纹理之前,需要在应用程序(CPU端)中完成纹理的创建、配置和数据上传。

  1. 生成纹理对象并设置参数
    使用 glGenTextures 生成纹理对象,glBindTexture 绑定它,然后通过 glTexParameteri 设置纹理的过滤方式(如 GL_LINEAR)和环绕方式(如 GL_REPEAT)。这决定了纹理在放大、缩小或坐标超出 [0, 1] 范围时的行为。

    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);// 设置纹理参数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);// 加载图像数据到纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
    glGenerateMipmap(GL_TEXTURE_2D); // 可选:生成Mipmap
    
  2. 将纹理绑定到纹理单元并传递单元编号
    OpenGL ES 通过纹理单元(Texture Unit) 管理多个纹理。你需要激活一个纹理单元(如 GL_TEXTURE0),将纹理绑定上去,然后将该纹理单元的编号(如 0)传递给着色器中的采样器统一变量。

    // 在渲染循环中
    glActiveTexture(GL_TEXTURE0); // 激活纹理单元0
    glBindTexture(GL_TEXTURE_2D, textureID); // 将纹理绑定到当前激活的纹理单元(GL_TEXTURE0)// 获取着色器中采样器统一变量的位置,并设置其值为纹理单元的编号(0对应GL_TEXTURE0)
    GLint texUniformLoc = glGetUniformLocation(shaderProgram, "uTexture");
    glUniform1i(texUniformLoc, 0);
    

⚙️ 二、在着色器中采样纹理 (GPU 端)

1. 顶点着色器(Vertex Shader)

顶点着色器的任务是接收顶点的纹理坐标(通常由应用程序传入),并将其插值后传递给片元着色器。

#version 300 es
layout(location = 0) in vec4 aPosition; // 顶点位置
layout(location = 1) in vec2 aTexCoord; // 顶点纹理坐标out vec2 vTexCoord; // 输出到片元着色器的插值后的纹理坐标void main() {gl_Position = aPosition;vTexCoord = aTexCoord; // 直接传递纹理坐标,或者在这里进行变换
}
2. 片元着色器(Fragment Shader)

片元着色器是纹理采样的核心。它通过 uniform sampler2D 声明一个纹理采样器,接收从顶点着色器插值得到的纹理坐标,并使用 texture 函数进行采样,最终输出颜色。

#version 300 es
precision mediump float; // 设置浮点数精度in vec2 vTexCoord; // 从顶点着色器传入的插值后的纹理坐标uniform sampler2D uTexture; // 纹理采样器统一变量out vec4 fragColor; // 输出的片元颜色void main() {// 使用texture函数采样纹理,返回RGBA颜色fragColor = texture(uTexture, vTexCoord);
}

📱 三、特殊纹理类型:采样外部纹理 (Android)

在 Android 平台上处理摄像头预览或视频解码帧时,常使用 GL_TEXTURE_EXTERNAL_OES 类型的纹理,这需要在着色器中启用特殊扩展并使用 samplerExternalOES 采样器。

// 片元着色器
#version 300 es
#extension GL_OES_EGL_image_external : require // 必须启用扩展
precision mediump float;in vec2 vTexCoord;
uniform samplerExternalOES uTexture; // 采样器类型不同out vec4 fragColor;void main() {// 注意:采样外部纹理有时使用 texture2D 函数fragColor = texture2D(uTexture, vTexCoord);
}

🚀 四、其他纹理采样函数

除了基本的 texture 函数,GLSL 还提供了其他函数用于更精细的控制:

  • textureLod(sampler2D sampler, vec2 coord, float lod):允许显式指定采样的细节级别(LOD),常用于一些高级效果或自定义的Mipmap选择。
  • texelFetch(sampler2D sampler, ivec2 P, int lod):使用整数像素坐标直接获取纹素,不进行任何过滤。适用于需要精确像素操作或屏幕后处理等场景。

如何使用多个纹理

在 OpenGL ES 中,glActiveTexture 是一个非常重要的函数,它主要用于管理多个纹理单元(Texture Unit),确保在渲染时能够正确访问和使用多个纹理。

🧠 核心概念:纹理单元

OpenGL ES 通过纹理单元来管理多个纹理。你可以把纹理单元想象成 GPU 上预留的多个纹理插槽。每个纹理单元可以绑定一个纹理对象(Texture Object),而着色器中的采样器(Sampler)需要通过指定纹理单元的编号来知道该从哪个“插槽”里读取纹理数据。

默认情况下,OpenGL ES 使用纹理单元 GL_TEXTURE0。如果你只使用一张纹理,不调用 glActiveTexture 可能也不会出问题(因为默认就在 GL_TEXTURE0 上操作)。但一旦需要使用多张纹理,就必须通过 glActiveTexture激活和切换当前的纹理单元,以便对不同的纹理进行操作。

📝 使用多个纹理的步骤

当需要在 OpenGL ES 中使用多个纹理时(例如同时使用漫反射贴图、高光贴图、法线贴图等),你需要遵循以下步骤:

  1. 生成纹理对象:为每个纹理生成唯一的 ID。

    GLuint diffuseTex, specularTex;
    glGenTextures(1, &diffuseTex);
    glGenTextures(1, &specularTex);
    
  2. 激活纹理单元并绑定纹理:使用 glActiveTexture 选择要操作的纹理单元,然后用 glBindTexture 将纹理对象绑定到该单元的目标上(如 GL_TEXTURE_2D)。

    // 激活纹理单元0并绑定漫反射纹理
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, diffuseTex);
    // 设置纹理参数和加载图像数据...
    glTexImage2D(GL_TEXTURE_2D, ...);// 激活纹理单元1并绑定高光纹理
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, specularTex);
    // 设置纹理参数和加载图像数据...
    glTexImage2D(GL_TEXTURE_2D, ...);
    
  3. 告知着色器纹理单元编号:在着色器中,采样器统一变量(uniform sampler2D)需要知道它应该从哪个纹理单元采样。你需要将纹理单元的索引号(0, 1, 2…)传递给着色器。

    // 假设你的着色器程序中有两个采样器:uDiffuseMap 和 uSpecularMap
    GLint diffuseLoc = glGetUniformLocation(shaderProgram, "uDiffuseMap");
    GLint specularLoc = glGetUniformLocation(shaderProgram, "uSpecularMap");// 使用 glUniform1i 设置采样器对应的纹理单元索引
    glUniform1i(diffuseLoc, 0); // 0 表示对应 GL_TEXTURE0
    glUniform1i(specularLoc, 1); // 1 表示对应 GL_TEXTURE1
    

    在着色器中,你会这样声明:

    uniform sampler2D uDiffuseMap;  // 将使用纹理单元0
    uniform sampler2D uSpecularMap; // 将使用纹理单元1
    
  4. 在着色器中采样多个纹理:在片元着色器中,你可以使用多个纹理采样器,并根据纹理坐标从不同的纹理中获取颜色信息,然后进行混合或其他计算。

    varying vec2 vTexCoord;
    uniform sampler2D uDiffuseMap;
    uniform sampler2D uSpecularMap;void main() {vec4 diffuseColor = texture2D(uDiffuseMap, vTexCoord);vec4 specularColor = texture2D(uSpecularMap, vTexCoord);// 例如:将漫反射颜色和高光颜色相加gl_FragColor = diffuseColor + specularColor;
    }
    

⚠️ 重要注意事项

  • 纹理单元的数量:不同的 GPU 和设备支持的最大纹理单元数量是有限的。你可以通过 glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxUnits) 来查询你的设备支持的最大数量。通常现代设备都支持至少 8 个或更多,但为了兼容性,特别是在移动设备上,最好不要过度使用。
  • 绑定状态:纹理单元的绑定状态是 OpenGL ES 上下文状态的一部分。当你绑定一个纹理到某个纹理单元后,它会一直保持绑定状态,直到你再次绑定另一个纹理或解绑。这允许你预先设置好所有纹理,在渲染时只需切换着色器程序即可,无需重新绑定纹理,从而提升效率。
  • 性能考虑:虽然使用多个纹理可以带来丰富的视觉效果,但过多的纹理采样和复杂的混合计算会增加片元着色器的负担,可能影响渲染性能。需要根据目标设备的性能进行权衡和优化。

OES纹理和普通纹理的区别

OES纹理(GL_TEXTURE_EXTERNAL_OES)是OpenGL ES中一种特殊的纹理类型,主要用于处理Android设备上由外部生产者(如摄像头、视频解码器)生成的流数据。它与普通的2D纹理(GL_TEXTURE_2D)在设计目标和用法上有一些关键区别。为了让你能快速把握核心差异,我用一个表格来汇总它们的对比:

特性维度OES纹理 (GL_TEXTURE_EXTERNAL_OES)普通2D纹理 (GL_TEXTURE_2D)
来源与数据外部生产者(如Camera、MediaCodec、SurfaceTexture)通过BufferQueue提供应用程序自行定义和加载(如图片文件、内存数据)
尺寸与格式不可更改,由数据源决定,通常是非2的幂次方(NPOT)可自由指定,通常建议为2的幂次方(POT)
采样器类型samplerExternalOES (需扩展声明 #extension GL_OES_EGL_image_external : require)sampler2D
纹理坐标可能使用特殊的变换矩阵(通过SurfaceTexture.getTransformMatrix()获取)标准的(s, t)坐标,范围[0.0, 1.0]
Mipmapping不支持(无多级渐远纹理)支持,可自动或手动生成Mipmap链
纹理函数部分设备上可能需要使用 texture2D 而非 texture 函数进行采样使用标准的 texture 函数
性能优化流式数据设计,避免内存拷贝,直接从BufferQueue消费数据,延迟低数据需从CPU内存上传至GPU显存,可能成为性能瓶颈
主要应用场景摄像头预览、视频播放、实时特效等需要处理连续视频流的场合静态贴图、UI元素、游戏纹理等预先准备好的图像资源
💡 转换:从OES纹理到普通2D纹理

有时算法或特效库可能只接受普通的GL_TEXTURE_2D纹理。这时,一个常见的做法是离屏渲染

  1. 创建一个帧缓冲区对象(FBO) 并绑定它。
  2. 将一张普通的2D纹理附加为FBO的颜色附件。
  3. 将OES纹理绑定到一个纹理单元,并设置好合适的着色器(包括坐标变换)。
  4. 绘制一个全屏四边形,让片段着色器对OES纹理进行采样。
  5. 采样结果便会渲染到附加的2D纹理上。

压缩纹理

纹理压缩是计算机图形学中的一项关键技术,它通过减少纹理数据的内存占用和带宽需求来提升渲染效率,对于移动设备、游戏和实时图形应用至关重要。下面我将为你梳理纹理压缩的核心要点。

🧠 核心原理与价值

纹理压缩是一种专为图形渲染设计的图像压缩技术。它与JPEG、PNG等通用图像压缩不同,核心区别在于:

  • 目标不同:纹理压缩旨在让GPU能直接、高效地访问压缩后的数据,无需完全解压到内存,从而节省显存和内存带宽。而JPEG/PNG等格式需要先由CPU完全解压成RGB等未压缩格式,才能上传给GPU,这会增加内存占用和数据传输量。
  • 随机访问能力:GPU在渲染时对纹理的访问是高度随机的。纹理压缩格式(如S3TC/DXT、ETC、ASTC)通常采用基于固定大小块(Block)的压缩方式(例如4x4像素为一个块),每个块可以独立解码。这使得GPU可以快速定位和解码所需的特定纹素(texel),而不需要解码整个纹理。通用压缩格式则不支持这种高效的随机访问。

主要价值

  • 大幅降低内存占用和带宽消耗:这是最直接的好处,压缩后的纹理有时只有原始未压缩格式的1/6甚至更小。
  • 提升渲染性能:减少了数据吞吐量,降低了功耗和发热,尤其有利于移动设备。
  • 支持更复杂精美的场景:在相同的内存预算下,可以使用更多或更高分辨率的纹理。

📊 常见纹理压缩格式

不同的硬件平台有不同的“原生”支持格式,了解它们有助于为你的应用选择最合适的格式。

格式系列主要特点与子格式常见平台支持压缩率/像素位深 (常见)
ETC / ETC2ETC1:OpenGL ES 2.0标准一部分,仅支持RGB,无透明通道。所有Android设备支持。ETC2:OpenGL ES 3.0+标配,支持RGB和RGBA(含透明通道),压缩质量优于ETC1。Android 主流支持ETC1 RGB: 4bpp; ETC2 RGBA: 8bpp
ASTC非常灵活,支持多种块大小(从4x4到12x12),从而提供广泛的压缩率(从8bpp到低于1bpp)。支持1-4通道(R, RG, RGB, RGBA)和LDR/HDR。需要 OpenGL ES 3.2+ 或扩展,现代移动GPU支持可变 (e.g., 4x4块: 8bpp; 8x8块: 2bpp)
S3TC (DXTC / BC)桌面GPU传统标准。DXT1: 压缩RGB(1位透明或无情调);DXT5: 压缩RGBA(高质量透明)。Windows/桌面 主流,部分移动芯片(如NVIDIA Tegra)支持DXT1: 4bpp; DXT5: 8bpp
PVRTCPowerVR GPU(常见于旧款iOS设备)专用格式。有2bpp和4bpp两种模式。iOS设备(PowerVR架构GPU)2bpp, 4bpp
ATC (ATITC)Qualcomm Adreno GPU专用格式。搭载Qualcomm Adreno GPU的Android设备类似DXT, 4bpp/8bpp

⚙️ 工作流程:压缩与使用

在OpenGL ES中使用压缩纹理通常遵循以下步骤:

  1. 获取压缩纹理数据

    • 使用离线工具(如ARM Mali Texture Compression Tool、PVRTexTool、ASTC Encoder等)将源图像(PNG、JPG等)预先压缩成特定的压缩格式(.ktx, .dds等容器格式或原始数据)。
    • 或在支持的情况下,在运行时通过glTexImage2D指定特定的internalformat(如GL_COMPRESSED_RGBA8_ETC2_EAC),由驱动实时压缩(开销较大,不推荐)。
  2. 上传至GPU
    使用 glCompressedTexImage2D 函数直接上传压缩后的数据,而非使用 glTexImage2D

    // 以ETC2 RGBA为例
    glCompressedTexImage2D(GL_TEXTURE_2D,        // 目标0,                    // Mipmap级别GL_COMPRESSED_RGBA8_ETC2_EAC, // 内部格式,指定了压缩格式width, height,        // 纹理尺寸0,                    // 边框,必须为0dataSize,             // 压缩数据的总大小(字节)compressedData       // 指向压缩纹理数据的指针
    );
    
  3. 采样与渲染
    在着色器中,压缩纹理的采样方式与未压缩纹理完全一样。GPU硬件会在采样时自动实时解码。

    uniform sampler2D uCompressedTexture;
    in vec2 vTexCoord;
    out vec4 fragColor;
    void main() {fragColor = texture(uCompressedTexture, vTexCoord);
    }
    

🎨 视觉质量与性能权衡

纹理压缩是有损压缩。压缩率越高,通常图像质量损失的风险也越大。这种损失可能表现为:

  • 块状伪影(Blocking Artifacts):在低比特率格式中,4x4块的边界可能变得可见。
  • 颜色 banding:渐变区域可能出现色阶断裂。
  • 细节模糊:高频细节可能丢失。

📱 平台兼容性实践

  1. 查询设备支持
    在运行时,可以通过查询扩展字符串来检测设备支持的压缩格式。
    // 获取设备支持的所有扩展
    const char* extensions = (const char*)glGetString(GL_EXTENSIONS);
    // 检查是否支持ASTC压缩格式
    if (strstr(extensions, "GL_KHR_texture_compression_astc_ldr") != nullptr) {// 设备支持ASTC格式
    }
    // 同样可以检查 "GL_OES_compressed_ETC2_RGBA8_texture" 等
    
  2. Android多格式备选
    为纹理提供多种压缩格式的版本(如ASTC、ETC2),在运行时根据设备支持情况选择加载最合适的一种。
  3. 在AndroidManifest.xml中声明
    如果你的应用必须使用某种特定的纹理压缩格式,可以在清单文件中声明,以便应用商店(如Google Play)进行过滤,避免应用安装在不支持的设备上。
    <!-- 声明支持ASTC压缩格式 -->
    <supports-gl-texture android:name="GL_KHR_texture_compression_astc_ldr" />
    <!-- 声明需要OpenGL ES 3.2,它包含了对ETC2和ASTC的强制要求 -->
    <uses-feature android:glEsVersion="0x00030002" android:required="true"/>
    

从颜色缓冲区复制纹理数据

指定数据源

glReadBuffer 是 OpenGL 和 OpenGL ES 中一个用于指定后续像素读取操作源的函数。它告诉 OpenGL 接下来像 glReadPixelsglCopyTexImage2D 这样的命令应该从哪个颜色缓冲区(color buffer)读取数据。

下面是关于 glReadBuffer 的核心信息:

特性描述
主要功能指定后续像素读取命令(如 glReadPixels, glCopyTexImage2D)的源颜色缓冲区。
常用参数 (mode)GL_FRONT, GL_BACK, GL_COLOR_ATTACHMENTi (i为附件编号) 等,具体取决于当前绑定的帧缓冲区类型。
错误情况传入不支持的 mode 值、指定不存在的缓冲区或在 glBegin/glEnd 配对中调用都会生成错误。
相关信息查询使用 glGet 并传入参数 GL_READ_BUFFER 可以获取当前设置的读取缓冲区。

🧭 工作原理与参数选择

glReadBuffer 的具体行为高度依赖于当前绑定的帧缓冲区(Framebuffer)类型

  1. 当绑定的是默认帧缓冲区(即屏幕缓冲区)时

    • 单缓冲(Single-buffered)配置下,通常只能使用 GL_FRONT(前缓冲区)或 GL_BACK(后缓冲区,如果存在的话)。
    • 双缓冲(Double-buffered)配置下,通常使用 GL_BACK 来从后缓冲区(当前正在渲染的目标)读取,以避免干扰正在显示的前缓冲区。
    • 也支持一些更具体的缓冲区,如 GL_FRONT_LEFT, GL_BACK_RIGHT 等,但这取决于系统是否支持立体渲染(Stereo)以及具体的缓冲区配置。
  2. 当绑定的是自定义的帧缓冲区对象(FBO)时

    • 参数通常为 GL_COLOR_ATTACHMENTi,其中 i 是颜色附件的索引(例如 GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1)。这允许你指定从 FBO 的哪个颜色附件读取数据。
    • 也可以设置为 GL_NONE,表示不从任何颜色缓冲区读取。

默认行为

  • 在单缓冲配置中,默认模式通常是 GL_FRONT
  • 在双缓冲配置中,默认模式通常是 GL_BACK

⚠️ 重要注意事项

  • OpenGL ES 的特别说明:在 OpenGL ES 3.0 及以上版本中,glReadBuffer 的使用更加明确。
    • 当绑定的是默认帧缓冲区时,mode 只能是 GL_BACKGL_NONE
    • 当绑定的是FBO时,mode 必须是 GL_COLOR_ATTACHMENTiGL_NONE
    • 许多在桌面 OpenGL 中可用的特定缓冲区(如 GL_FRONT_LEFT)在 OpenGL ES 中不可用

复制数据

函数名称主要作用典型应用场景
glReadPixels()从帧缓冲区读取像素数据到客户端内存(CPU)截图、像素颜色采样、像素数据分析
glCopyTexImage2D()从帧缓冲区复制像素数据并定义整个2D纹理创建基于屏幕内容的完整纹理
glCopyTexSubImage2D()从帧缓冲区复制像素数据以更新部分2D纹理更新现有纹理的特定区域
glCopyTexSubImage3D()从帧缓冲区复制像素数据以更新部分3D纹理更新3D纹理或纹理数组的特定切片或区域

采样器对象

在 OpenGL ES(特别是从 3.0 版本开始)和 WebGL2 中,采样器对象(Sampler Objects) 是一项重要特性,它允许你将纹理的采样参数(如过滤和环绕方式)与纹理对象本身分离开来。这提供了更大的灵活性和效率。

特性维度传统方式 (OpenGL ES 2.0)采样器对象 (OpenGL ES 3.0+)
管理方式采样状态(过滤、环绕等)直接绑定在纹理对象上采样状态独立于纹理对象,存储在专门的采样器对象中
灵活性低。一张纹理只能有一套采样参数。想用不同方式采样同一纹理数据需创建多个纹理对象高。一个采样器对象可被多个纹理对象共享,一个纹理对象也可与不同采样器对象搭配使用
内存效率较低。相同纹理数据因采样参数不同需多次存储较高。纹理数据只需存储一份,通过搭配不同采样器对象实现多种采样方式
API 函数前缀glTexParameter*glSamplerParameter*

🧰 核心工作原理

采样器对象的核心思想是解耦

  • 纹理对象(Texture Object):主要负责存储纹理数据(纹素),例如图像的颜色、深度等信息。
  • 采样器对象(Sampler Object):主要负责定义如何对纹理数据进行采样,包括过滤方式、环绕方式等参数。

通过这种分离,你可以在渲染时动态地将任何一个纹理对象与任何一个采样器对象组合使用。这意味着同一份纹理数据,可以在不同的绘制调用中,通过绑定不同的采样器对象来采用不同的过滤方式或环绕方式进行采样,而无需创建多份纹理数据副本。

⚙️ 创建与管理采样器对象

  1. 创建采样器对象
    使用 glGenSamplers 函数来创建一个或多个采样器对象。

    GLuint samplerId;
    glGenSamplers(1, &samplerId); // 创建采样器对象
    
  2. 设置采样器参数
    使用 glSamplerParameteri(或 glSamplerParameterf, glSamplerParameteriv 等)函数来设置采样器对象的参数。这些参数与使用 glTexParameter* 为纹理对象设置的参数类型完全相同,例如:

    • GL_TEXTURE_MIN_FILTER (缩小过滤)
    • GL_TEXTURE_MAG_FILTER (放大过滤)
    • GL_TEXTURE_WRAP_S (S方向环绕方式)
    • GL_TEXTURE_WRAP_T (T方向环绕方式)
    • GL_TEXTURE_WRAP_R (R方向环绕方式,用于3D纹理)
    • GL_TEXTURE_COMPARE_MODE (深度比较模式)
    • GL_TEXTURE_COMPARE_FUNC (深度比较函数)
    // 设置采样器的缩小过滤器为三线性过滤
    glSamplerParameteri(samplerId, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    // 设置放大过滤器为线性过滤
    glSamplerParameteri(samplerId, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 设置S和T方向的环绕方式为Clamp to Edge
    glSamplerParameteri(samplerId, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glSamplerParameteri(samplerId, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
  3. 绑定采样器对象到纹理单元
    使用 glBindSampler 函数将采样器对象绑定到某个纹理单元(Texture Unit)。注意,是绑定到纹理单元,而不是直接绑定到纹理对象。

    // 将采样器对象绑定到纹理单元0
    glBindSampler(0, samplerId);
    

    绑定后,所有绑定到该纹理单元的纹理对象,在采样时都会使用这个采样器对象所定义的参数。

  4. 删除采样器对象
    当采样器对象不再需要时,使用 glDeleteSamplers 函数删除它们以释放资源。

    glDeleteSamplers(1, &samplerId);
    

🎯 采样器对象的使用场景与优势

  1. 为同一纹理数据应用不同的采样设置
    这是采样器对象最直接的优势。例如,你有一张高精度的地板纹理:

    • 在近距离渲染时,你希望使用 GL_LINEAR_MIPMAP_LINEAR 过滤以获得平滑效果。
    • 在远距离或作为天空盒渲染时,你希望使用 GL_NEAREST 以避免在边缘产生不必要的模糊。
      传统方式需要两份纹理数据,而现在只需要一份纹理对象,搭配两个不同的采样器对象即可。
  2. 提升代码清晰度和可维护性
    将采样状态集中管理在采样器对象中,而不是散落在各个纹理的设置代码里,使得状态管理更加模块化和清晰。

  3. 共享采样设置
    一个定义好的采样器对象(例如一个通用的“线性Clamp”采样器)可以被多个不同的纹理对象共享使用,避免了为每个纹理重复设置相同的参数。

📝 使用示例

假设你有一个纹理对象 textureId,并创建了两个采样器对象:一个用于平滑过滤 (smoothSampler),一个用于邻近过滤 (nearestSampler)。

// 创建和设置平滑采样器
GLuint smoothSampler;
glGenSamplers(1, &smoothSampler);
glSamplerParameteri(smoothSampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glSamplerParameteri(smoothSampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);// 创建和设置邻近采样器
GLuint nearestSampler;
glGenSamplers(1, &nearestSampler);
glSamplerParameteri(nearestSampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glSamplerParameteri(nearestSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);// ... 在渲染循环中 ...// 场景1:需要平滑采样时
glActiveTexture(GL_TEXTURE0); // 激活纹理单元0
glBindTexture(GL_TEXTURE_2D, textureId); // 将纹理绑定到单元0
glBindSampler(0, smoothSampler); // 将平滑采样器绑定到单元0
// ... 绘制操作 ...// 场景2:需要邻近采样时(可能是下一帧或绘制另一个物体)
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureId); // 纹理还是那个纹理
glBindSampler(0, nearestSampler); // 但采样器换了!
// ... 绘制操作 ...

在着色器中,你不需要做任何更改,依然使用 sampler2D 等采样器统一变量,并通过 texture 函数进行采样。OpenGL 会自动使用当前绑定到该纹理单元的纹理对象的数据采样器对象的设置来完成采样。

⚠️ 注意事项

  • 优先级:当为一个纹理单元同时绑定了一个纹理对象和一个采样器对象时,采样器对象的参数会覆盖掉纹理对象自身设置的采样参数。如果绑定采样器对象为 0(glBindSampler(unit, 0)),则会回退到使用纹理对象自身的参数。
  • 兼容性:采样器对象是 OpenGL ES 3.0 及更高版本 的核心功能。在 OpenGL ES 2.0 中不支持。WebGL 中,采样器对象是 WebGL2 的特性。
  • 性能:在驱动层面,采样器对象可能允许硬件更高效地处理采样状态的变化,因为采样状态现在被封装在独立的对象中,可能更容易被缓存和优化。

不可变纹理

不可变纹理(Immutable-Format Texture)是 OpenGL ES 3.0 及更高版本中引入的一项重要特性,它通过改变纹理的创建和管理方式来提升性能和可靠性。简单来说,一旦你创建了一个不可变纹理,它的数据格式(如 GL_RGBA8)和尺寸(宽、高、深度)就固定了,不能再被改变。你仍然可以更新纹理的内容(例如使用 glTexSubImage2D),但不能改变它的“骨架”。

🧱 核心特点

  1. 格式和尺寸不可变:通过 glTexStorage* 系列函数(如 glTexStorage2D)创建不可变纹理时,你需要一次性指定纹理的所有级别(mipmap levels)、内部格式(internalformat)和尺寸。之后,这些属性就无法再更改。
  2. 内容可更新:虽然纹理的“骨架”固定了,但其“血肉”——也就是纹理数据本身——仍然可以通过 glTexSubImage*DglCopyTexSubImage*D 或渲染到纹理(FBO)等方式进行更新和修改。
  3. 提升性能:因为驱动在纹理创建时就能预先确定纹理的所有信息(格式、尺寸、mipmap层级),所以它能提前完成所有必要的内存分配和一致性检查。在渲染时,驱动程序就可以跳过这些运行时检查,从而减少开销,提升渲染性能。这对于基于分块渲染(Tile-Based Rendering)的移动GPU尤其有益。
  4. 更强的错误检查:由于格式和尺寸不可变,任何试图改变它们的操作(如调用 glTexImage2D)都会产生错误,这有助于开发者更早地发现代码中的问题。

⚙️ 如何创建不可变纹理

创建不可变纹理通常使用 glTexStorage* 系列函数。以最常用的 glTexStorage2D 为例:

// 1. 生成并绑定纹理对象
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);// 2. 创建不可变存储空间!这是关键步骤。
//    此处指定了:目标、mipmap层级数(此处为1)、内部格式(GLenum)、宽度、高度。
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 512, 512);// 3. (可选)随后可以向纹理填充数据,例如使用 glTexSubImage2D
//    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, imageData);// 4. 设置纹理参数(过滤、环绕方式等)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

主要的 glTexStorage* 函数包括:

  • glTexStorage1D: 用于 1D 纹理。
  • glTexStorage2D: 用于 2D 纹理和立方体贴图纹理。
  • glTexStorage3D: 用于 3D 纹理和 2D 纹理数组。

🆚 不可变纹理 vs 可变纹理

特性不可变纹理可变纹理 (传统方式)
创建方式glTexStorage*glTexImage*
格式/尺寸创建时固定,不可更改随时可通过 glTexImage* 重新指定甚至重建
性能通常更高,驱动可预先优化较低,驱动需在渲染时进行检查和状态管理
安全性更高,避免运行时意外更改格式或尺寸较低,易出现不一致的错误
典型用途大部分固定格式的纹理(漫反射、法线贴图等)需要动态改变尺寸或格式的特殊场景(如动态生成)

💡 使用场景与优势

  • 性能关键的应用:对于游戏、交互式可视化等对性能要求较高的应用,使用不可变纹理可以减少驱动开销,提升帧率。
  • 代码健壮性:强制开发者提前规划纹理资源,避免运行时意外修改纹理格式或尺寸导致的错误。
  • 移动设备:在内存和算力有限的移动设备上,不可变纹理的性能优势更为明显。

⚠️ 注意事项

  • OpenGL ES 版本:不可变纹理是 OpenGL ES 3.0 的核心功能。在 OpenGL ES 2.0 中,你需要通过扩展(如 GL_EXT_texture_storage)来使用它,但支持情况和性能可能有所不同。
  • 无法重定义:一旦创建,就无法用 glTexImage*D 来重新定义不可变纹理的格式或尺寸。如果必须改变,你需要删除旧纹理并创建一个新的。
  • 内容更新:记得使用 glTexSubImage*D 来更新不可变纹理的内容,使用 glTexImage*D 会导致错误。

像素解包缓冲区对象

像素解包缓冲区对象(Pixel Unpack Buffer Object,简称 PBO 的一种)是 OpenGL 中一种用于高效处理像素数据上传至 GPU 的机制。它允许你将像素数据(如纹理图像)先存入一个缓冲区对象,然后让 OpenGL 通过直接内存访问(DMA) 直接从该缓冲区将数据上传到纹理等目标,从而减少 CPU 参与,提升数据上传效率。

操作类型传统方式 (无 PBO)使用像素解包缓冲区 (PBO)
数据上传至纹理glTexImage*D 直接从 CPU 内存拷贝数据glTexImage*D 从 PBO (GPU 内存) 读取数据
性能关键同步操作,CPU 需等待数据上传完成异步 DMA 传输,CPU 提交后立即返回,GPU 负责后续传输
数据流向应用程序内存 → (CPU 参与拷贝) → OpenGL 纹理对象应用程序内存 → PBO → (DMA, 无需CPU) → OpenGL 纹理对象

🧠 核心工作原理

像素解包缓冲区对象是 OpenGL 缓冲区对象的一种特殊绑定形式。其核心在于通过 GL_PIXEL_UNPACK_BUFFER 目标,将一块显存区域定义为像素数据上传的中间暂存区

  1. 创建与绑定
    使用 glGenBuffers() 创建缓冲区对象,然后通过 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, id) 将其绑定为像素解包缓冲区。

    GLuint pboId;
    glGenBuffers(1, &pboId);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboId);
    
  2. 数据填充与分配
    使用 glBufferData() 为缓冲区分配空间并填充数据。你可以传入像素数据指针,也可传 NULL 先分配空间稍后映射填充。

    // 分配空间并上传数据
    glBufferData(GL_PIXEL_UNPACK_BUFFER, dataSize, pixelData, GL_STREAM_DRAW);
    // 或仅分配空间,稍后映射填充
    glBufferData(GL_PIXEL_UNPACK_BUFFER, dataSize, NULL, GL_STREAM_DRAW);
    
  3. 上传至纹理
    当像素解包缓冲区绑定后,调用如 glTexImage2DglTexSubImage2D 等函数时,不再从 CPU 内存指针读取数据,而是从当前绑定的 PBO 中读取。此时这些函数的数据指针参数通常被解释为缓冲区中的偏移量(byte offset)。传递 0 表示从缓冲区起始位置读取。

    // 绑定你的纹理...
    glBindTexture(GL_TEXTURE_2D, textureId);
    // 确保 PBO 已绑定
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboId);
    // 从 PBO 的偏移量 0 处开始上传数据到纹理
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); // 注意最后一个参数是偏移量,不是内存指针
    
  4. 解绑
    操作完成后,通过 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) 解绑 PBO。

🚀 性能优势

像素解包缓冲区对象通过以下方式提升性能:

  • 减少 CPU 开销:数据从应用程序内存到 PBO 的填充通常仍需要 CPU,但从 PBO 到纹理的上传由 GPU DMA 控制器完成,解放了 CPU
  • 异步操作glTexImage2D 等函数在 PBO 绑定后可能立即返回,无需等待数据传输完成,实现了异步操作。
  • 避免管线停滞:传统方式中,glTexImage2D 可能导致 CPU 等待 GPU 空闲,而 PBO 有助于减少这种等待。

📝 使用提示

  • 用法提示 (Usage Hints)glBufferData 的 usage 参数(如 GL_STREAM_DRAWGL_STATIC_DRAW)向 OpenGL 提示数据的预期使用模式,帮助驱动优化。
  • 内存映射:你可以使用 glMapBufferRange 映射 PBO 到客户端内存直接写入数据,然后用 glUnmapBuffer 解映射。
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboId);
    GLvoid* pboPtr = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
    if(pboPtr) {// 直接向 pboPtr 写入数据...memcpy(pboPtr, imageData, dataSize);glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }
    
  • 双 PBO 乒乓操作:对于流式纹理更新(如视频播放),可以使用两个 PBO 进行乒乓操作,在一个 PBO 用于纹理上传时,CPU 填充另一个 PBO,从而重叠 CPU 和 GPU 操作。

文章转载自:

http://O2N7uq1h.ttdxn.cn
http://WJGDBM4h.ttdxn.cn
http://jo3V4fJ3.ttdxn.cn
http://j8BmDMfB.ttdxn.cn
http://GFPh4HB0.ttdxn.cn
http://mmlDF41W.ttdxn.cn
http://Eug4rtVb.ttdxn.cn
http://HBPLEbmM.ttdxn.cn
http://H9YAvnw2.ttdxn.cn
http://ksG7C4mm.ttdxn.cn
http://T2tgoQ3e.ttdxn.cn
http://BRcBq3qM.ttdxn.cn
http://c8lbRUcT.ttdxn.cn
http://acxDzqdZ.ttdxn.cn
http://y4cVEb5q.ttdxn.cn
http://caAeKeKv.ttdxn.cn
http://Ea1GO7W8.ttdxn.cn
http://Uba8bDHT.ttdxn.cn
http://O0AuJFce.ttdxn.cn
http://7EbuI55A.ttdxn.cn
http://m7cnYdBO.ttdxn.cn
http://au4HttHS.ttdxn.cn
http://PkbTAore.ttdxn.cn
http://fGGWjpn2.ttdxn.cn
http://iVKflsPv.ttdxn.cn
http://ELGmzAAj.ttdxn.cn
http://qrnoVYow.ttdxn.cn
http://2SU1S3Ni.ttdxn.cn
http://8Z0RRmEq.ttdxn.cn
http://qi7n7kVx.ttdxn.cn
http://www.dtcms.com/a/378395.html

相关文章:

  • 什么是OCSP装订(OCSP Stapling)?它如何加速SSL握手?
  • 微硕WINSOK MOS管WSF3089,赋能汽车转向系统安全升级
  • Matplotlib 动画显示进阶:交互式控制、3D 动画与未来趋势
  • 立体校正原理
  • CAD球体密堆积_圆柱体试件3D插件 球体颗粒在圆柱容器内的堆积建模
  • 西门子 S7-200 PLC SMART 模拟量指令库(Scale)添加与实战使用指南
  • 后端Web实战-Spring原理
  • 计算机网络---内网穿透
  • QTDay1 图形化界面
  • Flutter 中的 Isolate
  • 将容器连接到默认桥接网络
  • 探索AI工具宝库:Awesome AI Tools - 让AI成为你的超级助手
  • UEC++学习(十八)使用TAutoConsoleVariable<T> / FAutoConsoleCommand自定义控制台变量/命令
  • 2.9Vue创建项目(组件)的补充
  • MasterGo蒙版
  • 一次.dockerignore设置错误导致的docker build排查
  • 第六节,探索 ​​CSS 的高级特性、复杂布局技巧、性能优化以及与现代前端工作流的整合​​
  • Flink on YARN 实战问题排查指南(精华版)
  • Java全栈学习笔记34
  • 进程控制(1)
  • 操作系统进程管理——同步与互斥的基本概念
  • 灰色关联分析笔记
  • CAD文件坐标系未知?用Bigemap Pro自动计算中央子午线,准确定位!
  • 项目管理核心八项(软件篇)
  • 创新驱动:医养照护与管理实训室建设方案构建
  • C++ 之 cli窗口交互程序测试DLL
  • openEuler系统远程管理方案:cpolar实现安全高效运维
  • Spring常用注解介绍
  • 《秋鳞小故事——编译器》
  • 【前端Vue】如何优雅地在vue中引入ace-editor编辑器