用计算机程序制作三维立体画
摘 要 该文介绍了三维立体画的原理和制作方法,并给出了用 C语言编写的源程序。借
助于 ,读者可以自己设计和欣赏各式各样的三维立体画。目前,市面上正在流行
各式各样的立体画,其特点是从外表来看与一般的图案很相似,但是双眼紧盯着注视片刻
后,一恍惚之间眼前便出现了画中画——立体像。笔者第一次看到这种画便被发明者的创
意所倾倒。利用众所皆知的双眼视差原理,竟能在一张平面纸上制造出如此奇幻。但是立
体画本身除了其发明者的灵感和画面创作者的别出心裁之外,其原理上并无神秘之处。用
计算机程序来实现它,可说是易如反掌。笔者用一个晚上时间,便在微机上用 BASIC语言实
现了简单形体——平面圆饼的立体画。当然,要使该程序具有完善的功能,提高其制作速
度,还是应该用编译语言(如 C语言)来编写。本文中给出的源程序借助于 Windows中
的.BMP图形文件,可使大家自己制作任意形态的立体画。一、立体画的原理看过立体电影
的人都知道,当人的双眼分别接收不同视角拍摄的图像时便会产生立体感。这是由于人眼
长期观察的习惯造成的。和立体电影原理相同的立体摄影风景照片也很早就已出现。图 1
中给出了这种立体照片的示意图。左、右照片分别是人的双眼角度上观察一棱锥体时左右
眼看到的图像(图 2)。左眼看到的是棱锥的顶端向右错动了一些的图像,右眼的看到则是棱
锥的顶端向左错动了一些的图像。如果用一张硬卡片隔开两张照片(如图 3),@
@;图 1@@@@;图 2@@@@;图 3双眼分别看两张画,会
看到一个立体的棱锥体。这种立体照片的观察方法在测绘学中也早已采用。但是,目前的
三维立体画在形式上与这些很不相同。它是怎样在同一张画面上呈现立体的呢?首先,分析
一下人们是怎样从这些立体画中看出“立体形体”的。从前面所说的可以知道,人眼要得
到立体感,双眼必须有视差,即双眼看到的图像应该有差异。人们在看立体画时,都有“恍
惚”一下的过程。在这过程中,双眼的视中心发生了错动(如图 4)。这样@@;
图 4左眼看到的是画面的“偏左像”,右眼看到的是画面的“偏右像”。@@只要“偏左
像”和“偏右像”的内容相当于图 1的左、右照片,双眼就会感到立体形体。那么,能否把
图 1的左、右照片分别当做“偏左像”和“偏右像”,简单重叠来得到立体画呢?显然不
行。能够合成立体画的“偏左像”和“偏右像”是要满足一定条件的。如果图 5中表现的
棱锥体的表面上有图案的话,@@;图 5像素 a和像素 a'应该具有相同的颜色,
因为它们是从不同视角观察的@@同一个实体点。像素 b和像素 b'、像素 c和像素 c'的情
况与此相同。把两幅画分别当作“偏左图”和“偏右图”,部分重叠成为同一画面时,在新
的画面上这种关系仍应该表现为 a=a',b=b',c=c'(如图 6)。但这时应该注意到,在这张合
成@@;图 6画面上,点 a'既是“偏右图”上的点 a',又是“偏左图”上的点
b。而@@一张画面上相同坐标点的像素只可能是一种颜色,因此,产生了新的像素关系 a'
=b。另外,点 a既是“偏左图”上的点 a,又是“偏右图”上的点 c',所以,a=c'。以此类
推,点 b'和点 c也有类似的情况。因此出现了新的关系表示式,...'=c=c'=a=a'=b=b'
=...。这就构成了立体画面上像素必须要满足的条件:“等颜色像素链”。立体画上的所
有点都从属于某一条“等颜色像素链”。这就是所有立体画图案都呈现出某种程度上的水
平周期性的原因。因此,对于任意立体形状,只要构造出相应的这种“等像素链”,并按其
规律充填图案即可得到立体画。但是正如前面所述,由于这种“等像素链”条件的约束,人
们虽然可以随意构造出各种形体的立体画,但其立体形体的表面图案是不能完全随人意愿
的。二、制作立体画的计算机程序由于人的双眼的水平性,以上的“等像素链”只按水平
方向分布,与垂直方向无关。因此,在程序中,各个像素行的处理过程是相互独立的。制作
立体画的程序主结构图如图 7。@@;图 7 制作立体画的程序主结构图在以上
结构图中,关键是如何建立“等@@像素链”。具体的处理如下。对于立体形体上的每一个
点,首先求出该点在“偏左图”和“偏右图”上的坐标。以图 1中的棱锥顶点为例,实际上
其 X坐标是在中心点,但由于双眼的位置并不在其正上方,顶点在“偏左图”上向右位移,
在“偏右图”上向左位移,而且其位移值的大小显然与其高度有关,即该点坐标越高位移值
就越大。a,b,c等点也都有这些位移。在求出一个点在“偏左图”和“偏右图”上的坐标
后,再算出在合成图(如图 6)上的对应坐标,以建立“等像素”关系,如 a=a'。当立体形体
的一个水平剖面上的全部点经过以上处理后,合成图的各条“等像素链”关系也就自然形
成了。另外,由于有可能出现高点遮盖低点的情况,“等像素链”的构造应该从低点到高点
逐层进行,高点的“等像素”关系将替代低点的“等像素”关系。这也是程序主结构图中
“首先,对于没有任何形体存在的背景平面构造‘等像素链’”的原因。下面给出了根据
以上结构图用 C语言编写的源程序。程序中,每一个坐标点对应一个结构型数据,它包含
“前像素”、“后像素”两个指针。“前像素”指针指向该坐标点作为“偏右图”上的一
点,在“偏左图”所对应的点的坐标。“后像素”指针指向该坐标点作为“偏左图”上的
一点,在“偏右图”所对应的点的坐标。程序中,“立体形体水平剖面的高低坐标数据”、
“原始图案素材”和输出的“立体画”的文件格式都是采用了 的 Pbrush产生
的 BMP图形文件格式。图幅大小要求都是 640×400,用 16种颜色方式。其中,立体形体上各
点的高低坐标用图形文件中的颜色值表示,因此该图形文件的图形与带颜色的等高线图安
全相同。通常情况下,在 16色的 BMP文件中颜色值从小到大的顺序为:黑色、暗红色、暗
绿色、暗黄色、暗蓝色、暗紫色、暗青色、暗灰色、灰色、明红色、明绿色、明黄色、明
蓝色、明紫色、明青色、白色。本程序采用最简单的“图案充填”方案,即各条“链”上
的像素点皆采用该“链”上的第一个像素的颜色。程序中的常数 EYE-SPACE表示“偏左
图”和“偏右图”之间的偏差,BO-DOT是表明“链”的首或尾的指针标志。该程序寄生在
Windows 中的 Pbrush软件上。借助于它来构筑立体形体(即立体形体水平剖面高低坐
标数据文件图 8),设计原始图案(图 9)。程序运行后,逐行输入并处理以上两个文件中的图
形,然后输出立体画结果文件(图 10)。最后,用 Pbrush来观赏立体画 。
当然,要设计出令人赏心悦目的立体画,必须在立体形体和图案素材的选择和搭配上做到天
衣无缝,独具匠心。@@;图 8@@@@;图 9@@@@;图 10
程序清单@@/*--from --to ----*/include<>#define
COMPRESSION 0#define SIZE-OF-BITMAPFILEHEADER 14#define SIZE-OF-
BITMAPINFOHEADER 40#define SIZE-OF-RGBQUAD 4#define PIXEL-DATE-OFFSET
14+40+4*16/*SIZE-OF-BITMAPFILEHEADER+SIZE-OF-BITMAPINFOHEADER+BITS-PER-
PIXEL*NUM-COLOR*/#define NUM-COLOR 16#define NUM-LINE 400#define WIDTH
640#define BITS-PER-PIXEL 4#define PIXEL-PER-BYTE 2 /*8/BITS-PER-
PIXEL*/#define BYTE-PER-LINE 320 /*((WIDTH*BITS-PER-PIXEL-1)/32+1)*4 */#define
NO-DOT WIDTH+1#define EYE-SPACE 128struct{unsignde char color ;unsigned int
pri-x ;unsigned int nxt-x ;}dot[WIDTH];main(){struct
tagBITMAPFILEHEADER{unsigned char bfType1,bfType2; /* always equal
to'BM'*/unsigned long int bfSize; /*size of file */unsigned int
bfReserved1,bfReserved2; /* set to zero */unsiged long int bfoffits; /*byte
offset from BITMAPFILEHEADER to bitmap pixeldata in the file
*/}BITMAPFILEHEADER;struct tagBITMAPINFOHEADER{unsigned long int biSize,/*
size of BITMAPINFOHEADER */biWidth;/* width in pixels biHEight;/* hEIght in
pixels */unsigned int biPlanes, /* always 1 */biBitCount; /* color bits per
pixel must be 1,4,8 or 24 */unsigned long int biCompression, /*BI-RGB,BI-RLE 8
or 4*/biSizeImage, /*total bytes in image */biXPelsPerMeter,/* 0,or opt,h res.
*/biYPelsPerMeter,/* 0,or opt,h res. */biClrUsed, /* normally 0,can set a
lower no. colors than biBitCount */biClrImportant; /* normally 0
*/}BITMAPINFOHEADER;struct tagRGBQUAD{unsigned char rgbBlue, /* blue
intensity,0-255 */rgbGreen, /* green intensity,0-255 */rgbRed, /* red
intensity,0-255 */rgbReserved; /* reserved,set to Zero */}RGBQUAD[NUM-
COLOR];char *fn-layer="";char *fn-org="";char *fn-
result="";FILE *flayer,*fOrigin, *fResult;unsigned char tmp-
byte1,tmp-byte2;unsigned int line,i-byte,i-pixel,x;unsigned int layer;int
left-x,right-x;tmp-x;unsigned long int cur-offset;unsigned char h[WIDTH],org-
color[WIDTH];puts("---WINTRICK---");puts("---by Li Jisong ---");if(
(fLayer=fopen(fn-layer,"rb") )!=NULL) {fread(&BITMAPFILEHEADER,SIZE-OF-
BITMAPFILEHADER,1,fLayer);fread(&BITMAPINFOHEADER,SIZE-OF-
BITMAPINFOHEADER,1,fLayer);if( =='B' &&
=='M'&& ==WIDTH &&
==NUM-LINE&& ==BITS-PER-
PIXEL&& ==COMPRESSION)fread(RGBQUAD,SIZE-OF-
RGBQUAD,NUM-COLOR,fLayer);else{fclose(fLayer);printf("File %s is not fit for
this program!\n",fn-layer);getch();exit(1);}}else{
printf("File %s does not exist!\n",fn-layer);getch();exit(2);}if(
(fOrigin=fopen(fn-org,"rb"))!=NULL) {fread(& BITMAPFILEHEADER,SIZE-OF-
BITMAPFILEHEADER,1,fOrigin);fread(& BITMAPINFOHEADER,SIZE-OF-
BITMAPINFOHEADER,1,fOrigin);if( =='B' &&
=='M'&& ==WIDTH &&
==NUM-LINE&& ==BITS-PER-
PIXEL&& ==COMPRESSION)fread(RGBQUAD,SIZE-OF-
RGBQUAD,NUM-COLOR,fOrigin);else {fclose(fOrigin);printf("File %s is not fit
for this program!\n",fn-org);getch();exit(3);}}else {printf("File %s does not
exist!\n",fn-org);getch();exit(4);}if( (fResult=fopen(fn-
result,"wb"))!=NULL){fwrite(&BITMAPFILEHEADER,SIZE-OF-
BITMAPFILEHEADER,1,fResult);fwrite(& BITMAPINFOHIADER,SIZE-OF-
BITMAPINFOHEADER,1,fResult);fwrite(RGBQUAD,SIZE-OF-RGBQUAD,NUM-
COLOR,fResult);}else {printf("File %s open error!\n",fn-
result);getch();exit(5);}for(line=0;line<NUM-
LINE;line++){printf("line=%d\n",line);cur-offset=(unsigned long int)PIXEL-
DATA-OFFSET+(unsigned long int)BYTE-PER-LINE*line;fseek (fLayer,cur-
offset,SEEK-SET);fseek (fOrigin,cur-offset,SEEK-SET);for(i-byte=0;i-byte<BYTE-
PER-LINE;i-byte++) {fread(&tmp-byte1,1,1,fLayer);fread(&tmp-
byte2,1,1,fOrigin);for(i-pixel=0;i-pixel<PIXEL-PER-BYTE;i-pixel++) {x=i-
byte*PIXEL-PER-BYTE+i-pixel;if(x<WIDTH) {h[x]=(unsigned char) (tmp-
byte1<<(BITS-PER-PIXEL*i-pixel)) /((unsigned char)0x80>>(BITS-PER-PIXEL-
1));org-color[x]=(unsigned char) (tmp-byte2<<(BITS-PER-PIXEL*i-
pixel))/((unsigned char)0x80>>(BITS-PER-PIXEL-1));}}}for(x=0;x<WIDTH;x++)
{dot[x].color=0;dot[x].nxt-x=NO-DOT;dot[x].pri-x=NO-DOT;if((x+EYE-
SPACE)<WIDTH) dot[x].nxt-x=x+EYE-SPACE;if((signed)x-EYE-SPACE)>=0)dot[x].pri-
x=x-EYE-SPACE;}for(layer=1;layer<NUM-COLOR;layer++)for(x=0;x<WIDTH;x++) {left-
x=x-EYE-SPACE/2+(layer/2);right-x=x+EYE-SPACE/2-
((layer+1)/2);if((h[x]==layer)&&(left-x>=0)&&(rignt-x<WIDTH)) {if (dot[left-
x].nxt-x!=NO-DOT) dot[dot[left-x].nxt-x].pri-x=NO-DOT;dot[left-x].nxt-x=right-
x;if (dot[right-x].pri-x!=NO-DOT) dot[dot[right-x].nxt-x].nxt-x=NO-
DOT;dot[right-x].pri-x=left-x;}}for(x=0;x<WIDTH;x++) {if(dot[x].pri-x==NO-DOT)
{dot[x].color=org-color[x];tmp-x=x;while( dot[tmp-x].nxt-x!=NO-DOT) {tmp-
x=dot[tmp-x].nxt-x;dot[tmp-x].color=org-color[x];}}}fseek(fResult,cur-
offset,SEEK-SET);for(i-byte=0;i-byte<BYTE-PER-LINE;i-byte++) {tmp-
bytel=0;for(i-pixel=0;i-pixel<PIXEL-PER-BYTE;i-pixel++) {x=i-byte*PIXEL-PER-
BYTE+i-pixel;if(x<WIDTH)tmp-byte1=(unsigned cha
参考文献孙志辉、王萃寒、王茜.实用 Windows 详解.北京:电子工业出版社,1994.