(1)
我们要做的是,根据原始纹理T0创建一系列的纹理(通常使用平均滤波):T1、T2…Tn,其中每个纹理的大小都是前一个纹理的1/4,即长度和宽度减半,如图12.40所示。
要根据前一个mip纹理计算当前纹理中纹素的值,可以使用平均滤波器,即在RGB空间中,计算纹素(x,y)、(x+1,y)、(x+1,y+1)和(x,y+1)的平均值,然后将结果写入到当前纹理中,如图12.41所示。
![]() |
(点击查看大图)图12.40 根据原始纹理创建Mip纹理链 |
![]() |
(点击查看大图)图12.41 用于创建mip纹理的平均滤波器 |
创建Mip纹理链时需要遵守一些规则。首先,所有纹理都必须是方形的,且边长为2的幂。这样,可以在两个轴上按相同的比例缩小,Mip纹理链末尾的最后一个纹理总是1×1的。另一个约定是,原始纹理为mip等级0,然后依次为mip等级1、2、3、4…n。例如,如果原始纹理的大小为256×256,则mip链中各个纹理的大小如表12.4所示。
表12.4 原始纹理为256×256时,各个Mip纹理的大小
Mip纹理 | 纹理大小 | Mip纹理 | 纹理大小 |
T0 | 256×256 | T5 | 8×8 |
T1 | 128×128 | T6 | 4×4 |
T2 | 64×64 | T7 | 2×2 |
T3 | 32×32 | T8 | 1×1 |
T4 | 16×16 |
|
|
除原始纹理外,mip等级数为log2T0的边长。
在这个例子中,为log2256 = 8。要计算总纹理数,只需将上述结果加1。因此计算总纹理数的公式为:log2T0的边长+1。在这个例子中,总纹理数为9。
(2)
这里纹理要占用多少内存呢?图12.42是实际的mip纹理,其中每个纹理的大小都是前一个纹理的1/4。由于每次都将纹理缩小到原来的1/4,因此占用的内存很少。下面计算在前一个例子中,纹理需要占用多少内存。
![]() |
图12.42 实际的mip纹理 |
初始纹理的大小为256×256,需要占用2562个字。Mip纹理链需要的内存量如下:
![]() |
要计算x为原始纹理所需内存量的多少倍,将其除以2562:
![]() |
换句话说,加上mip纹理时,每个纹理需要的内存量只增加33%,代价很小,收益却很高。
知道如何创建mip纹理以及它们占用的内存量后,需要拿出一种将其加入到引擎中的方法。作者苦思闷想,考虑过再次修改物体/多边形数据结构,最后终于柳暗花明:只需将纹理指针指向mip纹理链而不是单个纹理即可。然而,在每个多边形中设置一个Mipmapping标记,并在渲染多边形时,将纹理指针视为执行位图数组的指针,而不是单个位图的指针。这样,便可以使用原来的数据结构。请看图12.43,以理解这里讨论的方法。
![]() |
图12.43 支持mip纹理链的纹理指针数组 |
需要对纹理进行Mipmapping时,原来的texture指针指向0级别mip纹理。据此分两步创建mip纹理链:首先,分配一个位图指针数组,并将原来的texture指针指向该数组;然后为每个mip纹理分配内存、生成mip纹理,并将位图指针数组中的每个元素指向相应mip纹理的位图。看起来令人迷惑,其实并非如此。
这种方案存在的唯一问题是,我们强行将物体/多边形的位图图像指针指向了一个位图指针数组。因此,必须非常小心,避免不知道纹理已被mipmap化的函数将0级纹理视为普通纹理;而知道纹理已被mipmap化的函数必须从mip纹理链中选择正确的纹理。
(3)
int Generate_Mipmaps(BITMAP_IMAGE_PTR source, // 原始纹理 BITMAP_IMAGE_PTR *mipmaps, // 指向mip纹理数组的指针 float gamma) // gamma修正因子
{
// 这个函数创建一个mip纹理链
// 调用该函数时,mipmap指向原始纹理
// 该函数退出时,mipmap指向一个指针数组,其中包含指向各个mip级纹理的指针
// 另外,该函数返回mip等级数,如果发生错误,则返回-1
// 最后一个参数gamma用于提高mip纹理的亮度,因为平均滤波器会降低亮度
// 1.01通常是不错的选择
// 该参数大于1.0时,将提高亮度;小于1.0时将降低亮度;为1.0时没有影响 BITMAP_IMAGE_PTR *tmipmaps; // 局部变量,指向指针数组的指针 // 第一步:计算mip等级数
int num_mip_levels = logbase2ofx[source->width] + 1; // 为指针数组分配内存
tmipmaps = (BITMAP_IMAGE_PTR *)malloc(num_mip_levels * sizeof(BITMAP_IMAGE_PTR) ); // 将元素0指向原始纹理
tmipmaps[0] = source; // 设置宽度和高度(它们相同)
int mip_width = source->width;
int mip_height = source->height; // 使用平均滤波器生成各个mip纹理
for (int mip_level = 1; mip_level < num_mip_levels; mip_level++) { // 计算下一个mip纹理的大小 mip_widthmip_width = mip_width / 2; mip_heightmip_height = mip_height / 2; // 为位图对象分配内存 tmipmaps[mip_level] = (BITMAP_IMAGE_PTR)malloc(sizeof(BITMAP_IMAGE) ); // 创建用于存储mip纹理的位图 Create_Bitmap(tmipmaps[mip_level],0,0, mip_width, mip_height, 16); // 让位图可用于渲染 SET_BIT(tmipmaps[mip_level]->attr, BITMAP_ATTR_LOADED); // 遍历前一个mip纹理,使用平均滤波器创建当前mip纹理 for (int x = 0; x < tmipmaps[mip_level]->width; x++) { for (int y = 0; y < tmipmaps[mip_level]->height; y++) { // 需要计算4个纹素的平均值,这些纹素在前一个mip纹理中的位置如下: // (x*2, y*2)、(x*2+1, y*2)、(x*2,y*2+1)、(x*2+1,y*2+1) // 然后将计算结果写入到当前mip纹理的(x, y)处 float r0, g0, b0, // 4个样本纹素的R、G、B分量 r1, g1, b1, r2, g2, b2, r3, g3, b3; int r_avg, g_avg, b_avg; // 用于存储平均值 USHORT *src_buffer = (USHORT *)tmipmaps[mip_level-1]->buffer, *dest_buffer = (USHORT *)tmipmaps[mip_level]->buffer; // 提取每个纹素的R、G、B值 _RGB565FROM16BIT( src_buffer[(x*2+0) + (y*2+0)*mip_width*2] , &r0, &g0, &b0); _RGB565FROM16BIT( src_buffer[(x*2+1) + (y*2+0)*mip_width*2] , &r1, &g1, &b1); _RGB565FROM16BIT( src_buffer[(x*2+0) + (y*2+1)*mip_width*2] , &r2, &g2, &b2); _RGB565FROM16BIT( src_buffer[(x*2+1) + (y*2+1)*mip_width*2] , &r3, &g3, &b3); // 计算平均值,并考虑gamma参数 r_avg = (int)(0.5f + gamma*(r0+r1+r2+r3)/4); g_avg = (int)(0.5f + gamma*(g0+g1+g2+g3)/4); b_avg = (int)(0.5f + gamma*(b0+b1+b2+b3)/4); // 根据5.6.5格式,对R、G、B值进行截取 if (r_avg > 31) r_avg = 31; if (g_avg > 63) g_avg = 63; if (b_avg > 31) b_avg = 31; // 写入数据 dest_buffer[x + y*mip_width] = _RGB16BIT565(r_avg,g_avg,b_avg); } // end for y } // end for x } // end for mip_level // 让mipmaps指向指针数组
*mipmaps = (BITMAP_IMAGE_PTR)tmipmaps;
// 成功返回
return(num_mip_levels); } // end Generate_Mipmaps
(4)
该函数接受三个参数,它们有些棘手。第一个参数是位图指针source,可以是指向任何有效BITMAP_IMAGE对象的指针,切记它是一个指针。第二个参数要复杂些:
- BITMAP_IMAGE_PTR *mipmaps;
这是一个指向BITMAP_IMAGE指针的指针。假设物体(或多边形)有一个texture指针,它指向一个BITMAP_IMAGE。现在的问题是,当mip纹理函数生成所有的mip纹理时,我们要将该指针指向mip纹理链本身,为此,需要知道该指针的地址,以便能够修改它,因此需要一个** BITMAP_IMAGE。这就是需要一个指向指针的指针作为参数的原因。我们将让该参数指向mip纹理指针数组。
这个函数提供了这样的灵活性:可以将同一个对象作为参数source和mipmap。对于source参数,将其设置为指向该对象的指针;对于参数mipmaps,需要将其设置为指向该对象的指针的地址,这样函数才能对其进行修改。假设您这样调用该函数:
- Generate_Mipmaps(obj->texture, (BITMAP_IMAGE_PTR *)&obj->texture,1.01);
其中obj的类型为OBJECT4DV2_PTR,它有一个texture字段,该字段是一个指向BITMAP_IMAGE的指针(BITMAP_IMAGE_PTR)。仔细查看上述调用可以发现:我们将该指针作为第一个参数,同时将其地址作为第二个参数,因此对第二个参数进行修改时,将修改obj->texture指向的实际值,从而丢失它指向的原始数据。但事实上,并不会丢失任何数据,我们将把obj->texture指向的原始位图用作mip纹理链中的第一个纹理。在删除mip纹理链时,我们重新将该指针指向原始位图。图12.44说明了整个处理过程。
![]() |
(点击查看大图)图12.44 让obj.texture指向mip纹理链和重新指向T0 |
回到mip纹理生成函数--它创建mip纹理链:为纹理指针数组分配内存;然后计算下一个mip等级的大小,为该mip等级分配内存,在RGB空间使用平均滤波器来创建该mip等级。这一过程不断重复,直到mip等级的大小为1×1为止,然后函数结束。该函数的最后一个参数(默认值为1.01)用于设置gamma值。使用平均过滤器来不断缩小图像时,其亮度会逐渐降低;因此通常使用gamma因子来提高每个mip等级的亮度,确保所有mip等级的亮度一致。为说明这一点,作者编写了一个演示程序,名为DEMOII12_10.CPP|H。用户可以选择不同的纹理图,该程序将动态地创建mip纹理,并显示它们,如图12.45所示。另外,用户还可以修改gamma值,以查看其影响。该程序的控制方式如下:
![]() |
图12.45 mip纹理实时生成程序的屏幕截图 |