揭秘OpenGL红宝书中棋盘生成程序

OpenGL红宝书中在内存中生成图像时,多次提到了一个生成黑白棋盘的例子。相关代码如下:

#define checkImageWidth 64
#define checkImageHeight 64
GLubyte checkImage[checkImageWidth][checkImageHeight][3];

......

void makeCheckImage(void)
{
    int i, j, c;
    
    for (i = 0; i < checkImageHeight; i++) {
        for (j = 0; j < checkImageWidth; j++) {
            c = ((((i & 0x8) == 0) ^ ((j & 0x8)) == 0)) * 255;
            checkImage[i][j][0] = (GLubyte) c;
            checkImage[i][j][1] = (GLubyte) c;
            checkImage[i][j][2] = (GLubyte) c;
        }
    }
}

......

void display()
{
    ......
    glDrawPixels(checkImageWidth, checkImageHeight, GL_RGB, GL_UNSIGNED_BYTE, checkImage);
    ......
}

checkImage的类型为GLubyte,对应于unsigned char,是8位的无符号整数,使用1字节来表示颜色的RGB值。

R: {1 1 1 1 1 1 1 1}

G: {0 0 0 0 0 0 0 0}

B: {1 1 1 1 1 1 1 1}

数组在内存中二进制的表示:

            第1列                                                第2列                  第64列

第1行 {1 1 1 1 1 1 1 1} {1 1 1 1 1 1 1 1} {1 1 1 1 1 1 1 1} {1 1 1 1 1 1 1 1} …… {0 0 0 0 0 0 0 0}

第2行 {0 0 0 0 0 0 0 0} {0 0 0 0 0 0 0 0} {0 0 0 0 0 0 0 0} {0 0 0 0 0 0 0 0} …… {1 1 1 1 1 1 1 1}

第3行 {1 1 1 1 1 1 1 1} {1 1 1 1 1 1 1 1} {1 1 1 1 1 1 1 1} {1 1 1 1 1 1 1 1} …… {0 0 0 0 0 0 0 0}


……


第64行 ……


下面看数组间的规律。


checkImage[0][0][0] = 0, checkImage[0][0][1] = 0, checkImage[0][0][2] = 0

checkImage[0][1][0] = 0, checkImage[0][1][1] = 0, checkImage[0][1][2] = 0

checkImage[0][2][0] = 0, checkImage[0][2][1] = 0, checkImage[0][2][2] = 0

checkImage[0][3][0] = 0, checkImage[0][3][1] = 0, checkImage[0][3][2] = 0

checkImage[0][4][0] = 0, checkImage[0][4][1] = 0, checkImage[0][4][2] = 0

checkImage[0][5][0] = 0, checkImage[0][5][1] = 0, checkImage[0][5][2] = 0

checkImage[0][6][0] = 0, checkImage[0][6][1] = 0, checkImage[0][6][2] = 0

checkImage[0][7][0] = 0, checkImage[0][7][1] = 0, checkImage[0][7][2] = 0


checkImage[0][8][0] = FF, checkImage[0][8][1] = FF, checkImage[0][8][2] = FF

checkImage[0][9][0] = FF, checkImage[0][9][1] = FF, checkImage[0][9][2] = FF

checkImage[0][10][0] = FF, checkImage[0][10][1] = FF, checkImage[0][10][2] = FF

checkImage[0][11][0] = FF, checkImage[0][11][1] = FF, checkImage[0][11][2] = FF

checkImage[0][12][0] = FF, checkImage[0][12][1] = FF, checkImage[0][12][2] = FF

checkImage[0][13][0] = FF, checkImage[0][13][1] = FF, checkImage[0][13][2] = FF

checkImage[0][14][0] = FF, checkImage[0][14][1] = FF, checkImage[0][14][2] = FF

checkImage[0][15][0] = FF, checkImage[0][15][1] = FF, checkImage[0][15][2] = FF


checkImage[0][16][0] = 0, checkImage[0][16][1] = 0, checkImage[0][16][2] = 0

checkImage[0][17][0] = 0, checkImage[0][17][1] = 0, checkImage[0][17][2] = 0

checkImage[0][18][0] = 0, checkImage[0][18][1] = 0, checkImage[0][18][2] = 0

checkImage[0][19][0] = 0, checkImage[0][19][1] = 0, checkImage[0][19][2] = 0

checkImage[0][20][0] = 0, checkImage[0][20][1] = 0, checkImage[0][20][2] = 0

checkImage[0][21][0] = 0, checkImage[0][21][1] = 0, checkImage[0][21][2] = 0

checkImage[0][22][0] = 0, checkImage[0][22][1] = 0, checkImage[0][22][2] = 0

checkImage[0][23][0] = 0, checkImage[0][23][1] = 0, checkImage[0][23][2] = 0


……


checkImage是一个3维数组,第1维表示行,第2维表示列,第3维表示像素颜色的RGB值。一个FF为8位,因此第3维的3个FF可表示24位的真彩。


从上可看出,每隔8列,则在checkImage的值则在0与FF之间切换,即在黑白之间切换。即,对于第0行,0至7列为黑,8至15列为白,16至23列为黑。由于checkImageWidth值为64,因此可产生 64 / 8 = 8 的黑白格。如果需要修改颜色,则将第3维的数值改为相应的值即可。


为何要每隔8列才切换黑白?每隔1列也可以,但在高分辨率的显示器下面非常不明显。因此这个8列,实际上控制着黑白格的宽度。


同理,每隔8行,则在黑白间切换。这就产生了错落有致的正方形黑白格。


需要注意的是,glDrawPixels函数虽然依次读取数据数值,但却是从最下面的1行开始绘制的。即checkImage[0][x][x]对于于最下面第1行,checkImage[1][x][x]对于于最下面第2行。因此,下面的代码可将左下角的单元格颜色值设为红色。


   for (i =0; i <8; i++) {

       for (j =0; j <8; j++) {

           checkImage[i][j][0] = (GLubyte)255;

           checkImage[i][j][1] = (GLubyte)0;

           checkImage[i][j][2] = (GLubyte)0;

        }

    }


现在的问题是,代码是如何自动产生颜色的黑白值的?


0x8的二进制是1000,表达式 i & 0x8 只有在i值的高位为1时才能得到1000,否则全为0000。 因此表达式 (i & 0x8) == 0 只在i值的高位为1时才返回false,否则为true。上面的说法是严格从语义的角度来进行的解释,但不好理解,没有太多实际意义。而实际上,对位与操作符“&”,更好的理解是,位与操作符“&”经常用于通过选择特定位为0的屏蔽因子而屏蔽特定的位,从而留下只关心的位。例如,对于例中所选择的屏蔽因子0x8,二进制为1000,


  1100      (i)

& 1000      (屏蔽因子)

----

  1000      (低3位被屏蔽掉,只剩最高位)


因为低3位为000,因此,无论i为何值,位与后的结果其低3位必为0值,而最高位则可保留原值。因此,通过与0x8位与,我们将i值的低3位全部屏蔽了,而只关心i值的千分位。这样,表达式


(i & 0x8) == 0


的意思是,i的千分位是否为0?


同样,表达式(j & 0x8) == 0 的意思是,j的千分位是否为0?


位或操作符“|”通常用于将特定位打开。如,下面将最低位打开。


  0100      (i)

| 0001      (打开因子)

----

  0101      (最低位必然为1,从而打开最低位)


位异或操作符“^”在操作数不同时返回1,在操作数相同时返回0。通常用于反转特定的位。


   0101      (i)

|  0011      (反转因子)

----

   0110      (将最低2位反转)


回到我们的例子,对于


c = ((((i &0x8) ==0) ^ ((j &0x8)) ==0)) *255;


设 i & 0x8 == 0 的值为a,则表达式变为


c = ((a ^ ((j &0x8)) ==0)) *255;


设 ((j & 0x8)) 的值为b,则表达式变为


c = ((a ^ b ==0)) *255;


因为 == 优先于 ^,因此将先算 b == 0,设 b == 0 的值为d,则表达式变为


c = ((a ^ d)) *255;


很明显,例子代码中的书写格式并不规范,一是 ((j & 0x8)) 的括号重复; 二是a ^ b ==0 让读者去猜^与==到底哪个优先,在编译器中也检测出该问题而发出警告; 三是((a ^ c)) *255的括号再次重复。综上,例子代码的公式可改为:


c = ( ( (i &0x8) ==0 ) ^ ( (j &0x8) ==0) ) *255;


含义比较清楚了,i的千分位是否为0?j的千分位是否为0?将它们异或后再乘以255。最后结果是,只有1真1假时,才得到255即白色,否则为黑色。也即当i或j中有且仅有一个的千分位为1时才会产生白色。


不同字长的最高位为1的值分别有:


二进制            字长   十六进制  十进制

1000               4       0x8       8

10000000           8       0x80      128

100000000000       12      0x800     2048

1000000000000000   16      0x8000    32768

……


而在字长为4、最高位为1时,可能的值如下:


二进制 十六进制 十进制

1000      0x8      8

1001      0x9      9

1010      0xA      10

1011      0xB      11

1100      0xC      12

1101      0xD      13

1110      0xE      14

1111      0xF      15


  1xxx

& 1000

————

  1000


因此,当字长为4,行i或列j中有且仅有一个存在8到15的值域时才会产生白色。因此就有第0行中8至15列的白色方格:


checkImage[0][8][0]  = FF, checkImage[0][8][1] =  FF, checkImage[0][8][2] =  FF

checkImage[0][9][0]  = FF, checkImage[0][9][1] =  FF, checkImage[0][9][2] =  FF

checkImage[0][10][0] = FF, checkImage[0][10][1] = FF, checkImage[0][10][2] = FF

checkImage[0][11][0] = FF, checkImage[0][11][1] = FF, checkImage[0][11][2] = FF

checkImage[0][12][0] = FF, checkImage[0][12][1] = FF, checkImage[0][12][2] = FF

checkImage[0][13][0] = FF, checkImage[0][13][1] = FF, checkImage[0][13][2] = FF

checkImage[0][14][0] = FF, checkImage[0][14][1] = FF, checkImage[0][14][2] = FF

checkImage[0][15][0] = FF, checkImage[0][15][1] = FF, checkImage[0][15][2] = FF


16的二进制是10000,是第一个字长超过4的数。(准确地说,0xF只能表示0-15的数,16-255的数必须用0xFF的范围来表示。)对于它,


  00010000     (16)

& 00001000    (屏蔽因子0x8)

------

  00000000


虽然最高位为1,但经0x8屏蔽后,结果为0,false。


来看24,这是字长超过4,且二进制的千分位为1的第一个数。


  00011000

& 00001000

------

  00001000

  

结果为true,白色。因此有


checkImage[0][24][0] = FF, checkImage[0][24][1] = FF, checkImage[0][24][2] = FF

checkImage[0][25][0] = FF, checkImage[0][25][1] = FF, checkImage[0][25][2] = FF

checkImage[0][26][0] = FF, checkImage[0][26][1] = FF, checkImage[0][26][2] = FF

checkImage[0][27][0] = FF, checkImage[0][27][1] = FF, checkImage[0][27][2] = FF

checkImage[0][28][0] = FF, checkImage[0][28][1] = FF, checkImage[0][28][2] = FF

checkImage[0][29][0] = FF, checkImage[0][29][1] = FF, checkImage[0][29][2] = FF

checkImage[0][30][0] = FF, checkImage[0][30][1] = FF, checkImage[0][30][2] = FF

checkImage[0][31][0] = FF, checkImage[0][31][1] = FF, checkImage[0][31][2] = FF


上面的规律是,不管最高位是否为1,只要二进制的千分位为1,就可在和0x8位与后其值为真。


在所有小于64的数中,符合二进制的千分位为1这个条件的数有:


8  - 15

24 - 31

40 - 47

56 - 63


揭秘完毕,总结规律:


位与运算不仅可以屏蔽特定位,巧用位与表达式可以在特定数值间来回翻转。


例如:如果需要连续的4个数且每隔4位翻转一次,则可用下面的代码:


   for (int i = 0; i < 30; i++) {

       if ((i &0x4) == 0) {

           printf("%d ", i);

        }

    }


结果为:


0 1 2 3  8 9 10 11  16 17 18 19  24 25 26 27


多试几个数,以期获得规律的灵感。


下面将checkImageWidth及checkImageHeight均改为256之后,将屏蔽因子改为0x16后的图像:

揭秘OpenGL红宝书中棋盘生成程序_第1张图片

0x32的图像:

揭秘OpenGL红宝书中棋盘生成程序_第2张图片

实在是没力去思考为什么了。



你可能感兴趣的:(揭秘OpenGL红宝书中棋盘生成程序)