计算机图形:纹理与表面细节

发布时间 2023-12-28 14:23:45作者: 明明1109

纹理技术

为什么需要纹理?

因为大多数对象表面并不光滑,单纯的光照、表面绘制技术并不能真实模拟对象,因而需要添加纹理.

如何添加纹理?

添加纹理的方式,称为添加表面细节,有以下方法:

  • 把一些小物体(如花苞、花、刺等)添加到大表面上
  • 用小的多边形区域组成表面图案
  • 将纹理数组或强度修改过程映射到一个对象表面 —— 纹理映射
  • 修改表面法向量,生成局部的凹凸效果 —— 凹凸映射
  • 修改表面法向量、切向量,显示木材等材料表面方向性纹理

纹理映射

纹理就是图案。

将纹理模式映射到对象表面上. 纹理模式可由一个矩形数组定义,也可用修改对象表面光强度值的过程定义,这种方法称为纹理映射(texture mapping)或图案映射(pattern mapping).

纹理可以是一维、二维或三维图案. 任意纹理描述称为纹理空间(texture space),用0~1.0范围的纹理坐标(texture coordinates)来表示.

纹理描述的一个成分称为纹理元(texel). 纹理元不同场景有不同含义:有时与一组颜色分量对应的纹理空间的一个位置,如RGB三角形,称为一个纹理元;有时单个纹理数组元素,如RGB颜色中的分量,也称为一个纹理元.

tips: 每个RGB包含3元素R、G、B分量,每个分量通常占用1byte.

线性纹理图案

  • 什么是一维线性纹理?

线性纹理指用一维数组连续存储多个RGB颜色分量,纹理坐标按百分比存储颜色序号.

对于一个线性图案,纹理空间用单个s坐标值表示. s=0表示第一组的RGB颜色,s=0.5表示中间的RGB颜色,s=1.0表示最后一组.

  • 应用

常用于图案条纹、围住圆柱的图案带,一条孤立线段的颜色图案.

表面纹理图案

  • 什么是二维表面纹理图案?

常用矩形颜色图案定义表面区域纹理,纹理空间用二维(s,t)坐标表示. s, t的范围0~1.0

每个纹理坐标对应一个RGB颜色. 纹理坐标(0, 0)表示原点,代表第一组颜色分量;(1.0, 1.0)代表最后一组颜色分量. 原点可在左下角,也可在左上角.

三次样条曲面或球面等对象的表面位置用uv对象空间坐标表示,投影像素位置用xy笛卡尔坐标表示.

  • 如何将纹理图案映射到对象表面,并在屏幕上显示?

2种方法:
1)将纹理映射到对象表面,再到投影平面;
2)将像素区间映射到对象表面,再将该表面区域映射到纹理空间.

将纹理图案映射到像素坐标,也称为纹理扫描(texture scanning);将像素坐标映射到纹理空间的映射称为像素次序扫描(pixel-order scanning)、逆扫描(inverse scanning)或图像次序扫描(image-order scanning).

二维纹理空间、对象空间和像空间的坐标系统关系:

img

tips: 纹理映射,就是指图中纹理-表面变换对应的映射.

从纹理空间到对象空间的仿射变换:

\[\begin{aligned} u&=u(s,t)=a_us+b_ut+c_u\\ v&=v(s,t)=a_vs+b_vt+c_v \end{aligned} \]

类似地,对象空间到像空间的变换是合并观察、投影变换.

,将一个表面纹理由纹理空间映射到圆柱体表面,并最终映射到像空间.

设纹理空间范围: \(s\in [0,1.0], t\in[0,1.0]\),圆柱坐标(对象空间):

\[u=θ, v=z, 0\le θ \le \pi/2, 0\le z \le 1 \]

在笛卡尔坐标系中,圆柱体可用参数表示:

\[x=r\cos u,y=r\sin u, z=v \]

建立了对象空间到像空间的变换. 如何建立纹理空间到对象空间的变换?
可用将纹理矩形左下角(原点)(0, 0)、右上角(1.0, 1.0)与像空间的左下角(r,0,0)、右上角(r, 1.0, 1.0)对齐.

tips: 有个隐含条件:投影平面坐标系xOy与图中xyz坐标的xOy平面重合.

对于任一点(s,t),有

\[u=s\cdot \frac{\pi}{2}, v=t \]

如何求观察-投影变换的逆变换?纹理映射的逆变换?

观察-投影变换的逆变换,即将(x,y,z)坐标表示(u,v)坐标

\[u=θ=\arctan \frac{y}{x}, v=z \]

img

体纹理图案

  • 什么是三维体纹理图案?

除线性、表面图案,还可为空间三维区域指定一组颜色. 这种纹理称为体纹理图案(volume texture patterns)或实体纹理(solid textures).

体纹理图案通过三维纹理坐标(s,t,r)指定,范围都是[0,1.0],形成一个单位立方体.

体纹理可提供内部视图,如剖切显示、切片,使得三维对象的显示中带有纹理图案,如砖块、木头等.

  • 如何将体纹理图案映射到三维块?

可将纹理空间的八个角(4个面共8个角)的坐标赋予场景的8个空间位置;
或者,将纹理空间的一个平面映射到场景中一个平面区域.

纹理缩减图案

动画等场景中,对象大小经常改变,同时,需要重新纹理映射. 当有纹理的对象缩小时,原来的纹理图案应用于较小的区域会导致纹理变形. 为了避免这种问题,可创建一系列纹理缩减图案(texture reduction patterns),在对象缩小时使用.

通常,每个缩减图案是之前的一半. 如最大时对应16x16的图案,那么可建立尺寸为8x8,4x4,2x2,1x1的另外几个图案. 这些缩减图案,常称为MIP图(MIP maps)或mip图(mip maps).

凹凸映射

纹理映射能用于模拟精致的表面细节,但对于模拟粗糙的物体表面,如橘子、草莓、葡萄干等不合适. 而凹凸映射能有效模拟.

凹凸映射(Bump Mapping)指至少两种不同的控制每个纹理元素的表面法线的方法. 核心是用扰动函数并且在光照模型计算中使用扰动法向量.

凹凸映射技术中,纹理图用来扰动每个像素的法向量,实现像素级的光照计算精度.

构造凹凸图

将扰动法向量的高分辨率信息保存在存放三维向量的二维数组中,该二维数组称为凹凸图法向量图. 如\((x, y, \bm{n})\)表示对象空间中的\((x, y)\)位置处法向量\(\bm{n}\),其中\(\bm{n}\)就是三维向量. 原始的每个向量表示三角形的任一点的插值法向量.

凹凸图中的向量(0,0,1)表示未扰动的法向量,其他任何向量表示对影响光照公式计算结果的法向量的扰动.

tips: 三角形顶点的切向量空间(局部坐标系)中,z轴//法向量⊥三角形平面,而向量(0, 0, 1)代表单位法向量.

  • 从高度图到凹凸图

通常,高度图(Height Map)更容易制作,但不适合实时计算,需要从高度图中提取的法向量,以得到凹凸图. 高度图由每个像素上的平直表面的高度组成,例如(x,y,h)表示坐标为(x,y)点对应高度为h,其中,x与s、y与t方向对应.

如何用高度图计算相邻像素间的高度差?
可计算s、t方向的切向量. 用H(i, j)表示尺寸为\(w×h\)像素的高度图在(i, j)位置的高度值,那么在该点处沿s、t方向的切向量\(\bm{S}(i, j)、\bm{t}(i, j)\)分别为:

\[\begin{aligned} \bm{S}(i,j)&=(1, 0, aH(i+1, j)-aH(i-1, j))\\ \bm{T}(i,j)&=(0, 1, aH(i, j+1)-aH(i, j-1)) \end{aligned} \]

其中,a为比例系数,可用于修改高度值范围,从而控制法向量扰动的明显程度.

为了简化描述,令\(S_z, T_z\)分别为\(\bm{S}(i, j), \bm{T}(i, j)\)的z分量

则法向量可用外积得到:

\[\begin{aligned} \bm{N}(i, j)&=\bm{S}(i, j)\times \bm{T}(i ,j)\\ &=(1, 0, S_z)\times (0, 1, T_z)\\ &=(-S_z, -T_z, 1) \end{aligned} \]

用了向量外积的坐标计算(参见解析几何笔记:向量的外积). 证明如下:
设3D正交坐标系Ⅰ\([O; \bm{e_1}, \bm{e_2}, \bm{e_3}]\)下,2个向量\(\bm{X}=(a_1,b_1,c_1), \bm{Y}=(a_2,b_2,c_2)\),则

\[\begin{aligned} \bm{X}\times \bm{Y}&=(a_1\bm{e_1}+b_1\bm{e_2}+c_1\bm{e_3})\times (a_2\bm{e_1}+b_2\bm{e_2}+c_2\bm{e_3})\\ &=\begin{vmatrix} \bm{e_1} & a_1 & a_2\\ \bm{e_2} & b_1 & b_2\\ \bm{e_3} & c_1 & c_2\\ \end{vmatrix}\\ &=\bm{e_1}(b_1c_2 - b_2c_1)-\bm{e_2}(a_1c_2 - a_2c_1)+\bm{e_3}(a_1b_2-a_2b_1) \end{aligned} \]

∵S、T方向垂直
∴可将\(\bm{S}(i, j)=(1, 0, S_z), \bm{T}(i, j)=(0, 1, T_z)\)坐标代入:

\[\bm{S}(i, j)\times \bm{T}(i ,j)=\bm{e_1}(0-S_z)-\bm{e_2}(T_z-0)+\bm{e_3}(1-0)=(-S_z, -T_z, 1) \]

即得证.

可得(i,j)处单位法向量:

\[\bm{N}(i, j)=\frac{\bm{S}(i,j)\times \bm{T}(i, j)}{|\bm{S}(i,j)\times \bm{T}(i, j)|}=\frac{(-S_z, -T_z, 1)}{\sqrt{S_z^2+T_z^2+1}} \]

其中,\(S_z=aH(i+1, j)-aH(i-1, j), T_z=aH(i, j+1)-aH(i, j-1)\).

顶点空间(待完成)

帧映射

帧映射(Frame Mapping)是一种增加表面细节的方法,凹凸映射的扩展. 可用于建立分等值曲面,模拟木纹图案、大理石等材质的条纹. 而凹凸和方向扰动,可通过查表获得.

基本思想:同时扰动:

1)扰动表面法向量\(\bm{N}\)
2)扰动\(\bm{T}、\bm{B}\).

注:\(\bm{N,T,B}\)构成一个的本地正交坐标系,表面切向量\(\bm{T}\)、双法线向量\(\bm{B}=\bm{T}\times \bm{N}\).

OpenGL函数

纹理函数扩充集,只支持RGBA颜色模型.

线纹理函数

  • 定义纹理(数组)

用颜色数组来定义线性纹理数组.

glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, nTexColors, 0, dataFormat, dataType, lineTexArray);
glEnable(GL_TEXTURE_1D);

// 原型
void glTexImage1D(GLenum  target, GLint   level, GLint   internalformat, 
    GLsizei width, GLint   border, GLint   format, 
    GLenum  type, const GLvoid  *pixels);

目标纹理target必须是GL_TEXTURE_1D,表明正在为一维对象(即线段)定义一个纹理数组. 如果不清楚系统是否支持,可用glTextImage1D + GL_PROXY_TEXTURE_1D先查询.

细节级别level为0,代表基础图像级别. 级别n代表第n个mipmap缩减图像(参见前面的纹理缩减图案).

纹理中颜色分量的数量internalformat,必须为1,2,3,4或者指定的符号常量如GL_RGB、GL_RGBA等.

纹理图像的宽度width,必须为2n+2(border),代表纹理图案中纹理颜色数量. 纹理图像的高度为1.

边框宽度border,必须为0或1.

像素数据格式format,必须为9个符号常量如GL_RED、GL_GREEN、GL_BLUE、GL_RGB、GL_RGBA等.

像素数据类型type,只能是以下符号常量GL_UNSIGNED_BYTE、GL_BYTE、GL_BITMAP、GL_UNSIGNED_SHORT、GL_SHORT、GL_UNSIGNED_INT、GL_INT 和 GL_FLOAT.

pixels是指向像素数据的指针,通常是一个数组,大小为level * width(border=0).

  • 设置纹理参数

纹理元素的区域可能与像素区域有多种映射关系,可能放大(MAG)或缩小(MIN)纹理图案以适合目标像素区域. 而glTexParameteri函数用来设置这部分参数,可简化纹理映射时的计算.

// 放大纹理图案
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// 缩小纹理图案
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

// 原型
void glTexParameteri(GLenum target, GLenum pname, GLint  param);

目标纹理target必须是GL_TEXTURE_1D(线性纹理) or GL_TEXTURE_2D(表面纹理).

纹理参数名pname可以是:
1)GL_TEXTURE_MIN_FILTER 当像素区域 > 纹理元素的区域时,将使用纹理缩小函数. 有6个纹理缩小函数,其中2个使用最近的四个纹理元素来计算纹理值,另4个使用mipmap. Mipmap用glTexImage1D/glTexImage2D定义.

2)GL_TEXTURE_MAG_FILTER 当像素区域 <= 纹理元素的区域时,将使用纹理放大函数,方式为GL_NEAREST或GL_LINEAR(对应param值).

3)GL_TEXTURE_WRAP_S 将纹理坐标的包装参数设置为GL_CLAMP或GL_REPEAT. GL_CLAMP将坐标固定到[0,1],GL_REPEAT将忽略s坐标整数部分,且用小数部分创建重复模式.

4)GL_TEXTURE_WRAP_T 将纹理坐标t的包装参数设置为GL_CLAMP或GL_REPEAT.

  • 设置当前纹理坐标

针对线性纹理,设置当前纹理坐标.

glTexCoord1*(s);

后缀码表示参数s的数据类型,支持b(字节)、s(短整数)、i(整数)、f(浮点数)、d(双精度浮点数).

s是纹理坐标,如果s是数组,则后缀码可+v. 默认值0.0. 要将线性纹理图案映射到世界坐标系的位置,可将s赋给一条线段的端点.

可在glBegin/glEnd之间调用.

  • 示例:交替使用绿色和红色的四元素线性纹理图案

线段交替使用绿色和红色.

void display()
{
    glClear(GL_COLOR_BUFFER_BIT);
    // glColor3f(0.0, 0.0, 0.0);
    glColor3f(1.0, 1.0, 1.0); // 线段用白色绘制

    GLint k;
    GLubyte texLine[16]; // 16个元素的纹理数组, 每4个元素形成一组RGBA信息

    GLfloat endPt1[3] = {20, 10, 0};
    GLfloat endPt2[3] = {400, 300, 0};

    /* 定义交替的绿色、红色的四元数线性纹理图案 */
    for (k = 0; k <= 2; k += 2) {
    texLine[4 * k] = 0;
    texLine[4 * k + 1] = 255;
    texLine[4 * k + 2] = 0;
    texLine[4 * k + 3] = 255;
    }

    for (k = 1; k <= 3; k += 2) {
    texLine[4 * k] = 255;
    texLine[4 * k + 1] = 0;
    texLine[4 * k + 2] = 0;
    texLine[4 * k + 3] = 0;
    }

    glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

    glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texLine);

    glEnable(GL_TEXTURE_1D);

    glBegin(GL_LINES); // 绘制线段
    glTexCoord1f(0.0); // 0.0 是线性纹理坐标s起点
    glVertex3fv(endPt1);
    glTexCoord1f(1.0); // 1.0 是线性纹理坐标s终点
    glVertex3fv(endPt2);
    glEnd();

    glDisable(GL_TEXTURE_1D);
    glFlush();
}

注意:线段需用白色绘制,纹理映射后仅显示纹理颜色.

表面纹理函数

  • 定义二维纹理数组

与线性纹理的区别是,必须指定二维纹理数组的宽(列数)和高(行数). 没有边界时,宽、高必须是2的幂次;有边界时,宽、高必须是2+2的幂次.

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texWidth, texHeight, 0, dataFormat, dataType, surfTexArray);

glEnable(GL_TEXTURE_2D);

第三个参数GL_RGBA表面使用RGBA颜色分量,说明该图案没有边界且不是mipmap. 因此,存储在surfTexArray的数组尺寸是 4×宽×高.

  • 设置纹理参数

类似线性纹理图案,表面纹理图案与目标像素区域有多种映射关系,可用glTexParameteri设置这部分参数.

如,下面代码使用最近颜色显示投影表面位置

// 放大纹理图案
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// 缩小纹理图案
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  • 设置纹理坐标

设置当前纹理坐标(s, t),在glBegin/glEnd之间调用,能绑定当前顶点与该纹理坐标.

注意:纹理空间中,纹理图案的左下角(0, 0)、右上角(1.0, 1.0),对应纹理数组第一组颜色和最后一组颜色.

glTexCoord2*(s, t);
  • 示例:红、绿、蓝、黄四色交替的表面纹理图案
void display()
{
    glClear(GL_COLOR_BUFFER_BIT);
    //glColor3f(0.0, 0.0, 0.0);
    glColor3f(1.0, 1.0, 1.0);

    GLubyte texArray[32][32][4];
    GLint k, j;
    GLfloat vertex1[3] = {10, 10, 0};
    GLfloat vertex2[3] = {400, 10, 0};
    GLfloat vertex3[3] = {10, 300, 0};
    GLfloat vertex4[3] = {400, 300, 0};

    // 定义红、绿、蓝、黄交替出现的纹理数组
    for (k = 0; k < 8; k++) {
    for (j = 0; j < 32; j++) {
    texArray[k][j][0] = 255;
    texArray[k][j][1] = 0;
    texArray[k][j][2] = 0;
    texArray[k][j][3] = 255;
    }
    }

    for (k = 8; k < 8*2; k++) {
    for (j = 0; j < 32; j++) {
    texArray[k][j][0] = 0;
    texArray[k][j][1] = 255;
    texArray[k][j][2] = 0;
    texArray[k][j][3] = 0;
    }
    }
    for (k = 8 * 2; k < 8 * 3; k++) {
    for (j = 0; j < 32; j++) {
    texArray[k][j][0] = 0;
    texArray[k][j][1] = 0;
    texArray[k][j][2] = 255;
    texArray[k][j][3] = 255;
    }
    }
    for (k = 8 * 3; k < 8 * 4; k++) {
    for (j = 0; j < 32; j++) {
    texArray[k][j][0] = 255;
    texArray[k][j][1] = 255;
    texArray[k][j][2] = 0;
    texArray[k][j][3] = 0;
    }
    }

    // 设置纹理参数, 指定放大、缩小函数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    // 创建纹理图案
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, texArray);
    
    glEnable(GL_TEXTURE_2D);

    // 绘制矩形并进行表面纹理映射
    glBegin(GL_QUAD_STRIP);
    glTexCoord2f(0.0, 0.0);
    glVertex3fv(vertex1);
    glTexCoord2f(1.0, 0.0);
    glVertex3fv(vertex2);
    glTexCoord2f(1.0, 1.0);
    glVertex3fv(vertex3);
    glTexCoord2f(0.0, 1.0);
    glVertex3fv(vertex4);
    glEnd();

    glDisable(GL_TEXTURE_1D);
    glFlush();
}

完整程序参见:ComputerGraphics/chapter18 | Gitee

体纹理函数

三维纹理空间函数是二维纹理函数的扩充. 函数接口与二维纹理空间函数大体一致,不过出现“2D”的地方(函数名或符号常量),需要修改为"3D".

纹理图案的颜色选项

glTextImage1D,2D,3D第三个参数internalformat,可用来指定纹理图案的每个元素的颜色分量的一般格式、数量,支持约40个符号常量.

每个纹理元素可以是一组RGBA值、RGB值、单个alpha值、单个红色强度值、单个/一对亮度值等,或者指定bit位的尺寸,如常量GL_R3_G3_B2指定1byte的RGB颜色中有3bit用于红色、3bit用于绿色、2bit用于蓝色.

参数format,指定像素数据的特殊格式. 支持11个符号常量(microsoft文档只提到9个),可以用指向颜色表的索引、单个alpha值、单个亮度值等.

参数dataType,指定内存中像素数组pixels的元素的数据类型和位尺寸,支持20个符号常量,常用有GL_BYTE、GL_INT、GL_FLOAT等.

纹理映射选项

当纹理图案映射到的对象也有自身颜色时,如何决定最终的颜色?
可以通过glTexEnvi设置,与当前对象的颜色混合,或取代对象颜色.

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, applicationMethod);

applicationMethod有4个可选操作:

1)GL_REPLACE,表示纹理颜色、亮度、光强或alpha值取代对象对应值(取决于纹理图案的元素格式internalformat).

2)GL_MODULATE(默认值),表示用纹理颜色调制当前对象的颜色值,结果依赖于纹理图案的元素格式,如alpha值调制alpha值,强度值调制强度值. 如果对象颜色是白色(对象默认颜色),则调制操作生成与取反相同的结果,依赖于如何指定纹理图案的元素.

3)GL_DECAL,将RGBA的alpha值作为透明系数,对象可看做对背景中的纹理是透明的. 如果该纹理图案仅包含RGB值而无A值,则该纹理颜色取代对象颜色;如果纹理图案仅包含alpha值,则该映射没有意义.

4)GL_BLEND,用指定的颜色blendingColor与片元进行颜色混合.

glTexEnv*(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, blendingColor);

后缀码由混合颜色的数据类型决定是i或f,如果是数组要+v.

最终颜色 = 纹理的颜色 × 常量颜色 + (1.0 - 纹理颜色) × 原来的颜色.

例如,下面代码使用黄色与对象颜色进行混合

// e.g. 黄色与对象颜色进行混合
GLfloat color[] = {1.0, 1.0, 0.0, 1.0}; // 黄色
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color);

纹理环绕

当纹理空间的坐标超出0~1.0范围时,可补充纹理空间中描述的图案:

glTexParameter*(texSpace, texWrapCoord, GL_REPEAT);

这样,仅使用纹理空间坐标值的小数部分(0~1.0)补充图案.

参数texSpace表示纹理目标,只能是GL_TEXTURE_1D/2D/3D.

参数texWrapCoord通过GL_TEXTURE_WRAP_S、GL_TEXTURE_WRAP_T、GL_TEXTURE_WRAP_R分别设置纹理空间中(s, t, r)坐标.

第三个参数为GL_CLAMP时,将纹理坐标强制到0~1.0内,如果坐标值>1.0,则赋值1.0;如果<0,则赋值0.
为GL_REPEAT(默认)时,使用0~1.0小数部分重复创建问题图案.

复制帧缓存中的纹理图案

将帧缓冲区中的像素复制到二维纹理图案中.

glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x0, y0, texWidth, texHeight, 0);

// 原型
void glCopyTexImage2D(GLenum target, GLint level, GLenum internalFormat,
   GLint x, GLint y, GLsizei width, GLsizei height,
   GLint   border
);

第二个参数level为0,表明这个图案是基础图案,而不是缩减的mipmap.

最后一个参数border为0,表明没有边框.

帧缓冲区代表屏幕的一帧数据,而要复制的帧缓冲区部分对应的矩形的左下角作为坐标原点,由参数x, y指定,width、height指定其尺寸.

如果我想复制帧缓冲区的多个部分,最后形成二维纹理图案,而不是仅来自帧缓冲区的一个矩形部分,该怎么办?

可以多次调用glCopyTexSubImage2D实现,将帧缓冲区指定矩形拷贝到要生成的二维纹理图案的指定位置. 源位置(x0, y0)(左下角)、源矩形大小texSubWidth × texSubHeight,目标位置由(xTexElement, yTexElement)确定.

glCopyTexSubImage2D(GL_TEXTURE_2D, 0, xTexElement, yTexElement, x0, y0, texSubWidth, texSubHeight);

纹理坐标数组

前面用glTexCoord设定单个纹理坐标,能不能像顶点数组一样,一次指定多个纹理坐标?

答案是可以的,使用纹理坐标数组.

  • 激活纹理坐标数组

纹理坐标数组需要先激活,再才能使用.

glEnableClientState(GL_TEXTURE_COORD_ARRAY);

关闭用glDisableClientState.

  • 指定纹理坐标数组
glTexCoordPointer(nCoords, dataType, offset, texCoordArray);

nCoords 每个数组元素的坐标数,即坐标维度,只能是1、2、3或4. 默认值4,表示以齐次坐标形式指定纹理空间,其空间位置是前3个坐标值/第4个坐标值.

dataType 每个数组元素的数据类型,支持GL_SHORT、 GL_INT、 GL_FLOAT和 GL_DOUBLE.

offset 是texCoordArray数组的位置偏移,默认值0.

texCoordArray 指向数组的第一个元素的第一个坐标的指针.

纹理图案命名

创建了多个纹理图案后,如何使用某个纹理呢?
可以对纹理图案命名(正整数),通过纹理名引用纹理.

  • 为纹理图案指定名称

调用glBindTexture可以创建命名纹理,如果指定的纹理名已创建,则该纹理名绑定到新的纹理.

例如,下面代码创建纹理名后,又绑定到新的纹理图案

// 为当前纹理图案命名
glBindTexture(GL_TEXTURE_1D, 3);
// 创建新纹理图案作为当前纹理图案
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texLine);
// 将纹理名"3"重新绑定到当前纹理图案
glBindTexture(GL_TEXTURE_1D, 3);
  • (批量)自动生成纹理名

还可以让OpenGL自动为图案生成名称,而非由调用者挑选,这样调用者不必费心知道哪些纹理名已被使用.

例如,下面代码自动生成1个纹理名并绑定到当前纹理图案

static GLuint texName;

// 生成1个新的纹理名, 保存到texName
glGenTextures(1, &texName);
// 绑定自动生成的纹理名到当前纹理图案
glBindTexture(GL_TEXTURE_2D, texName);

下面代码自动生成6个纹理名,并用其中一个绑定到当前纹理图案

static GLuint texNamesArray[6];

// 生成6个新的纹理名, 保存到数组texNamesArray
glGenTextures(6, texNamesArray);
// 绑定索引为3的纹理名到当前纹理图案
glBindTexture(GL_TEXTURE_2D, texNamesArray[3]);
  • 删除纹理名

纹理图案使用完后,可调用glDeleteTextures删除.

// nTextures 要删除的纹理名数量
// texNamesArray 存放要删除的纹理名数组
glDeleteTextures(nTextures, texNamesArray);
  • 查询纹理名是否已被使用
// texName 待查询纹理名
// 已被使用, 则返回GL_TRUE; 未被使用或者出错, 返回GL_FALSE
glIsTexture(texName);

纹理子图案

有没有一种方法引用纹理图案的一部分,而不创建新的纹理图案?
可以使用glTexSubImage2D创建子图案,从而修改原始图案的任意部分或全部.

例如,下面代码指定一组RGBA颜色值取代二维纹理的一部分,该部分没有边界、不是mipmap.

glTexSubImage2D(GL_TEXTURE_2D, 0, xTexElement, yTexElement, 
GL_RGBA, texSubWidth, texSubHeight, 0, 
dataFormat, dataType, subSurfTexArray);

xTexElement、yTexElement 用于选择原始图案中纹理元素的整数坐标位置,其中坐标(0, 0)是纹理图案的左下角.

texSubWidth、texSubHeight 决定子图案的宽、高.

subSurfTexArray 是子纹理图案的数组,由于是GL_RGBA格式元素,所以每个颜色元素4个分量,数组对应总的颜色元素个数4 × texSubWidth × texSubWidth、texSubHeight. 可通过对该数组的修改,实现对子图案的修改,最终体现为对纹理图案的修改.

其他参数同glTextImage.

纹理缩减图案函数

缩小的对象尺寸,可用函数建立一系列纹理缩减图案,称为mip图. 方法:
1)手动生成. 反复调用glTexImage创建尺寸更小的纹理图案,其中第二个参数level为前一次调用+1,即级数+1,表示尺寸缩小一半.

2)自动生成. 调用GLU函数gluBuild2DMipmaps

例如,下面代码为16x16表面纹理自动生成RGBA缩减图案:一组4个图案,缩减后尺寸分别为8x8,4x4,2x2,1x1.

gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGBA, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, surfTexArray);

也可以设定级数范围,而不是生成所有缩减图案.

gluBuild2DMipmapLevels(GL_TEXTURE_2D, GL_RGBA, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, 0, minLevel, maxLevel, surfTexArray);

其中,minLevel和maxLevel指定级数范围.

什么时候,如何使用纹理缩减图案?

对象缩小时,纹理也需要随之缩小,此时,需要对纹理进行插值以获得更平滑的图像,而glTexParameter + GL_TEXTURE_MIN_FILTER用于指定插值算法. 常见取值有:

1)GL_NEAREST 最近邻插值,选择最靠近像素中心的纹理单元的颜色值.(未使用mipmap)

2)GL_LINEAR 线性插值,选择最靠近像素中心的2x2纹理单元进行加权平均.(未使用mipmap)

3)GL_NEAREST_MIPMAP_NEAREST 选择一个尺寸最接近的mipmap,再用最靠近像素中心的纹理元素的颜色.(使用一个mipmap)

4)GL_LINEAR_MIPMAP_NEAREST 选择尺寸最接近的mipmap,再用最靠近像素中心的2x2纹理单元加权平均. (使用一个mipmap)

5)GL_NEAREST_MIPMAP_LINEAR 选择尺寸最接近的2个的mipmap,再用它们靠近像素中心的纹理元素,进行线性匀和.(使用两个mipmap)

6)GL_LINEAR_MIPMAP_LINEAR 选择尺寸最接近的2个mipmap,计算出2个纹理各自的值(2x2纹理单元加权平均),然后对2个计算结果再次进行线性匀和.(效果最好,但最慢,使用两个mipmap)

例如,下面代码让纹理子程序使用尽可能接近像素尺寸(MIPMAP_NEAREST)的缩减图案,再将该缩减图案中最靠近的纹理元素(GL_NEAREST)的颜色赋值给像素.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);

纹理边界

多个纹理或一个纹理的多个复制应用于一个对象时,可能相邻图案边界会出现走样问题. 通过纹理边界的颜色匹配,可以避免走样. 纹理边界包括2个特性:宽度,颜色.

宽度由glTexImage*创建纹理时border参数指定,颜色由glTexParameter*进行设置.

例如,下面代码为一个二维纹理图案指定边界颜色

GLfloat borderColor[4] = {1.0, 0.0, 0.0, 1.0}; // 红色

glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

其中,borderColor是四元素RGBA颜色分量. 默认边界颜色是黑色(0.0, 0.0, 0.0, 0.0).

也可以用glTexSubImage将一个相邻图案的颜色值复制到另一个图案边界,边界颜色也可以直接由glTexImage指定的纹理数组中赋值.

代理纹理

APP使用纹理时,往往需要向显卡申请大量资源. 有时不确定继续申请资源时,显卡是否能满足要求,此时可以使用代理纹理.

注意:如果用代理纹理分配资源成功,那么实际纹理不一定成功;但代理纹理失败,则实际纹理一定失败. 还需要取决于运行时情形.

如此,可以提早发现显卡是否有足够资源来处理该图案. 方法是将glTexImage函数第一个参数指定为符号常量,对于二维图案,是GL_PROXY_TEXTURE_2D;对于一维或三维,修改对应后缀为1D或3D即可.

如下面代码使用纹理代理,查询系统是否可以为二维图案指定的高度.

GLint texHeight;

glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA12, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// 查询是否能为二维图案指定的高度texHeight
glGetTeLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_RGBA12, GL_TEXTURE_HEIGHT, &texHeight);

其中,如果系统不能提供所需要的图案高度,则texHeight返回0;如果能,则返回所需要的值.

类似地,还能用符号常量GL_TEXTURE_WIDTH, _DEPTH, BORDER, BLUE_SIZE查询对应的图案参数.

齐次纹理坐标

所谓齐次坐标,指用n+1维向量表示原本n维向量. 如三维齐次坐标:(x,y,z,h),则对应三维坐标:(x/h, y/h, z/h).

纹理空间的齐次坐标,在多投影效果混合在一个显示中时很有用. 此时,纹理坐标和场景坐标的变换都一样使用4x4矩阵变换,能简化计算.

设置四维纹理空间坐标,用三维齐次坐标表示:

glTexCoord4*(s, t, r, h);

参考

[1] 伦吉尔,E.).3D游戏与计算机图形学中的数学方法(第3版)[M].清华大学出版社,2016.

[2] glteximage1d | microsoft docs

[3] gltexparameteri | microsoft docs

[4] DaveShreiner,李军,徐波.OpenGL编程指南(原书第7版)[M].机械工业出版社,2013.