一片云雾

写博客挺浪费时间的……
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

使用OpenGL绘制漂亮的围棋子

Posted on 2011-12-12 22:27  一片云雾  阅读(7225)  评论(23编辑  收藏  举报

     作为一个围棋爱好者兼程序员,多年以来开发过很多与围棋有关的软件,诸如围棋打谱软件、棋谱管理软件、围棋棋谱下载软件、围棋网站下载软件……而其中,围棋打谱软件开发的次数最多,读书的时候就编写过一个简易的围棋打谱软件作为编程的作业。编程水平逐渐提高之后,又开发过新的围棋打谱软件。曾经买过一个Windows Mobile的手机,到处寻觅棋谱阅读和直播软件而不得,也曾经开发过Window Mobile上的围棋打谱软件。最近突然想学习3D开发(基于种种原因,我选择的是OpenGL而不是D3D),我想以后免不了我又会开发一个基于OpenGL的围棋打谱软件,因此一个3D围棋子的绘制是大概也是免不了的。

     绘制3D围棋子的过程,我觉得是很有意思的,也可以学到很多的东西。

     以下分享一下我绘制棋子的过程。

     在我以前使用GDI或者GDI+进行围棋子绘制的时候,如果使用纯粹的点、线、形状的绘制,绘制出的棋子顶多就是一个黑色或者白色的圆圈,大不了加上消锯齿,圆圈更加平滑。要达到更漂亮的棋子绘制,只能采用棋子图片辅助绘制。在进行3D棋子绘制的时候,我脑海中有过使用棋子的3D模型的念头,但瞬间被自己否决了——依赖3D模型也太没成就感了。

     由于才开始学习OpenGL不久,而以前绘制过的最复杂的3D图形也就是一个立方体,因此心中还是有些打鼓,不知道自己的数学水平能否胜任这个绘制工作。不知为何,第一感就是觉得这个工作可能需要立体几何基础等数学基础,而已经快十年没碰过数学了,几乎所有的东西都还给老师了。

     由于心中打鼓,寻觅曾经的立体几何的相关书籍无果,结果找到一本电子书——《3D数学基础图形与游戏开发》。决心在正式绘制之前进行刻苦攻读一番,读了几个小时,读到"四元数"、"欧拉角"的时候,感觉自己彻底晕菜了。于是快速浏览了剩下的内容……的目录,然后开始实战演习。这本书,阅读之后的唯一收获,就是让自己摒弃了对于绘制3D棋子的畏惧感。至少我相信,绘制一个3D的棋子,不可能比理解四元数和欧拉角更复杂。

     中国围棋子中最有代表性的是云子,云子分"单面凸"和"双面凸"两种,我比较喜欢前者,它只有一面凸起,另一面是平的。因此我下面的绘制,将绘制的是"单面凸"的围棋子。下面这个图,是几颗实际的云子放在棋盘上的照片。

     接下来对云子结构进行一下剖析:云子的实际形状,应该是顶部一个弧顶,和一个平底,平滑衔接到一起。围棋子是一个中心对称的几何体,因此一个棋子的切面,是可以完全体现这个棋子形状的。

下面的图,是一个围棋子的从侧面观察的图。本想用实际照片,奈何总是难以拍摄出理想的效果,因此下面使用的,是我绘制的一个3D棋子的侧面观察图。

     进一步分析,这个侧面图,可以用4个线条组合而成:顶部一个大圆弧,左右各一个与之相切的小圆弧,底部一条与小圆弧相切的直线。

     下面这个图应该很能说明问题(注意实线部分就是一个围棋的轮廓)。

     对于上面这个原理图,其中实线的部分是棋子轮廓,虚线部分都是辅助理解的线条。

     说起上面这个图,我实在忍不住有几句想说。由于本人的PS水平属于非常初级的水准,为完成上面这个图形,可费了不少功夫。画笔和Photo shop轮番上阵,但还是没能绘制出我在白纸上绘制的一个草图(Look,我的草图就是上面那个图)。最后被逼无奈,我写了一段程序,用GDI+绘制了3个圆圈和5条线段,然后用PS的画笔鼓捣了几下以示虚线(请原谅我拙劣的PS水平吧……)。

     再认真看一下上面这个图,顶部是一个大圆的圆弧,左右是两个小圆的圆弧,下面是一条相切的直线。就这么简单,一个棋子的轮廓就出来,而绕着中轴线转一圈,就是一个3D的围棋子了。

     图中有a、b、c三个参数,其中a是底面的半径,b是底面和大圆弧圆心之间的距离,c是侧面小圆弧的半径。通过这3个参数,可以唯一确定一个围棋子的形状和大小。其中a、b、c的比例将决定这个围棋子的形状,比如决定这个棋子是比较凸还是比较扁平。

     绘制的原理就是这么简单。

     下面研究一下实际的绘制过程。

     通过对OpenGL辅助库中的各种形状绘制的学习和研究(其实也就是把那些几何体以线条形式展现,然后放大无数倍之后仔细观察),初步有了设计方案。

     我的想法是:沿着Y轴旋转一圈,将棋子切成M片,然后再横向切上N刀,然后就可以将围棋子的整个表面,分割成M*N个矩形。然后把这些矩形逐一绘制出来,那么一个棋子就绘制出来了。

     按照这个思路,我绘制了我的第一个棋子,源码以及效果可以参见鄙人拙作"使用OpenGL绘制一颗围棋子"。当然,以我现在的目光看来,当时的代码,无论是绘制效率、代码可读性、绘制效果还是功能方面,都是有所欠缺的。但当时从无到有的突破,已经让我比较开心了。

     后来,我在阅读Richard Wright的gltDrawSphere函数时,看到里面为绘制的球设置了纹理,因此我也想为自己的棋子绘制加入纹理映射。在思考纹理映射的过程中,感觉到上面的绘制方案有所不妥:当棋子被纵向切片之后,那一片的棋子,是可以用一系列的三角形带组成,没必要用一个个独立的矩形来表示。

     顺便说一下,我之所以想重构棋子绘制算法,除了上面这个启发之外,还有一个原因,是因为我绘制的棋子,在顶部和侧面融合的时候,在某些角度观察时,我看见有一条光影(如下图)。而按照我的理解,如果两个面是相切的,应该是没有这条光影线的。我一直怀疑是代码有细微瑕疵造成的,而我对代码仔细的审查,却发现不了任何问题。我甚至把棋子的所有法线画出来,观察是否是法线方向有疏忽,也没能发现问题。期间有一个同事教了我一招判别法线方向是否平滑过渡的方法,我觉得非常有用也很有创意:将法线分量作为顶点的颜色分量,然后观察颜色是否平滑过渡

     当然,最终我把法线当作颜色信息输出后,发现赫然也有一条光影,但我将这个光影放大无数倍,研究到底误差在什么地方的时候,却发现法线是平滑过渡的,但缩小为原始尺寸看,却又分明有一条光影,难道这是我眼睛的一种错觉?

     关于这个光影线,我最终也没明白原因。我尝试过提高切片的密度,也尝试过让切片在边长上均匀,也尝试让切片在角度上均匀,似乎均没有改善。难道绝对的平滑过渡,必须曲率也不发生变化吗?

     题外话到此为止,毕竟这个不影响主题,还是回归正途。下图是我新的绘制方案的基本原理图。首先进行纵向切片,然后用三角带完成每个切片的绘制。看着下面这个图,结合后面的代码,很容易想象实际是怎么完成的。(为了突出正反面,我加入了一点光照。)

     整个棋子的绘制,源码如下(其中的参数a、b、c的几何意义,在上面的原理图中也有体现)。源码中我加入了非常详尽的注释,应该很容易理解。

View Code
  1 /***************************************************************************//**
2 * 函数名称: DrawChess
3 * 功能描述: 绘制一颗围棋子。
4 * 参 数: a >> 底部半径;
5 * 参 数: b >> 底部距离圆心距离;
6 * 参 数: c >> 侧面半径;
7 * 参 数: nSlice >> 纵向分割的粒度;
8 * 参 数: nStack >> 环形分割的粒度;
9 * 返 回 值:
10 * 其它说明:
11 * 修改日期 修改人 修改内容
12 * ------------------------------------------------------------------------------
13 * 2011-12-05 一片云雾 创建
14 *******************************************************************************/
15 void DrawChess(GLfloat a, GLfloat b, GLfloat c, GLint nSlice, GLint nStack)
16 {
17 const GLfloat PI = (GLfloat)(3.141592653589); //圆周率PI
18 GLfloat fYRotStep = 2.0f * PI / nStack; //沿着Y轴旋转的步长
19 GLfloat fRange = atan(a / (b + c)); //顶部圆弧的角度(单位为弧度)
20 GLfloat R = sqrt(a * a + (b + c) * (b + c)) + c; //大圆顶半径
21
22 GLint nSlice1 = nSlice; //底部纵向分片数量
23 GLint nSlice2 = (GLint)(nSlice * (PI - fRange) * c / a); //侧面纵向分片数量
24 GLint nSlice3 = (GLint)(nSlice * R * fRange / a); //顶部纵向分片数量
25
26 GLfloat fStep1 = a / nSlice1; //顶部步长
27 GLfloat fStep2 = (PI - fRange) / nSlice2; //侧面步长(弧度)
28 GLfloat fStep3 = fRange / nSlice3; //顶部步长(弧度)
29
30 GLfloat dr = -0.5f / (nSlice1 + nSlice2 + nSlice3); //纹理半径增加的步长
31
32 GLint i = 0, j = 0;
33 for (i=0; i<nStack; i++)
34 {
35 GLfloat fYR = i * fYRotStep; //当前沿着Y轴旋转的弧度
36 GLfloat fZ = -sin(fYR); //Z分量比率
37 GLfloat fX = cos(fYR); //X分量比率
38 GLfloat fZ1 = -sin(fYR + fYRotStep); //下一列的Z分量比率
39 GLfloat fX1 = cos(fYR + fYRotStep); //下一列的X分量比率
40 GLfloat rs = 0.5f; //纹理半径的起点
41
42 glBegin(GL_TRIANGLE_STRIP);
43
44 //底部
45 for (j=0; j<nSlice1; j++)
46 {
47 GLfloat r = fStep1 * j;
48
49 glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ);
50 glNormal3f(0.0f, -1.0f, 0.0f);
51 glVertex3f(r * fX, b, r * fZ);
52
53 glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1);
54 glNormal3f(0.0f, -1.0f, 0.0f);
55 glVertex3f(r * fX1, b, r * fZ1);
56
57 rs += dr;
58 }
59
60 //侧面
61 for (j=0; j<nSlice2; j++)
62 {
63 GLfloat r = a + c * sin(fStep2 * j);
64 GLfloat y = b + c - c * cos(fStep2 * j);
65 GLfloat nr = sin(fStep2 * j);
66 GLfloat nY = -cos(fStep2 * j);
67
68 glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ);
69 glNormal3f(nr * fX, nY, nr * fZ);
70 glVertex3f(r * fX, y, r * fZ);
71
72 glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1);
73 glNormal3f(nr * fX1, nY, nr * fZ1);
74 glVertex3f(r * fX1, y, r * fZ1);
75
76 rs += dr;
77 }
78
79 //顶部
80 for (j=0; j<=nSlice3; j++)
81 {
82 GLfloat r = R * sin(fRange - j * fStep3);
83 GLfloat y = R * cos(fRange - j * fStep3);
84 GLfloat nr = sin(fRange - j * fStep3);
85 GLfloat nY = cos(fRange - j * fStep3);
86
87 glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ);
88 glNormal3f(nr * fX, nY, nr * fZ);
89 glVertex3f(r * fX, y, r * fZ);
90
91 glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1);
92 glNormal3f(nr * fX1, nY, nr * fZ1);
93 glVertex3f(r * fX1, y, r * fZ1);
94
95 rs += dr;
96 }
97
98 glEnd();
99 }
100 }

 

     使用这个函数,我绘制了两颗棋子,加上了一点光照,效果如下。

     上面这个绘制效果图,是没有加入纹理映射的。为了让绘制功能更加强大,我又加入了纹理映射(上面的代码已经是最终代码,包括了纹理映射),毕竟gluSphere这样的几何体绘制,也是能够设置纹理映射的。而且,有了纹理映射,以后如果想实现更加独特的围棋子效果,也会更加简易。

     关于纹理的映射,我起初的设想,是将棋子顶部映射到纹理上方,棋子底部映射到纹理下方。下面是一个效果图:

     起初看见这个效果的时候,还感觉挺兴奋,觉得效果挺炫的。但后来越来越感觉不对劲,首先,纹理贴上棋子之后,纹理失真非常严重,纹理上部和中部原本是均匀的,现在上面被严重的挤压到了一起。其次,纹理的左边和右边,在棋子上转了一圈之后相遇了,形成一条边界线,如果希望平滑过渡,势必要找到左右能无缝拼接的纹理,对图片要求提高了,相应丧失了灵活性。

     后来在某天下班回家的路上,想到了目前采用的纹理映射方案:也就是将围棋顶部映射到纹理的中央,底部的中央对应纹理的四周。下图是按照新的方案,加上了纹理之后的效果图,我个人感觉比上面那个效果要好J

     以上是我绘制漂亮的围棋子的全过程。此外,真正的云子,黑棋是碧透的,也就是对着光线看,会有幽幽的绿光,煞是好看,如何绘制这种碧透的效果,我还在继续研究中……