织梦CMS - 轻松建站从此开始!

罗索实验室

当前位置: 主页 > 音视频技术 > 音视频播放 >

使用OpenGL绘制漂亮的围棋子

落鹤生 发布于 2013-12-02 21:00 点击:次 
作为一个围棋爱好者兼程序员,多年以来开发过很多与围棋有关的软件,诸如围棋打谱 软件、棋谱管理软件、围棋棋谱下载软件、围棋网站下载软件……而其中,围棋打谱软件开发的次数最多,读书的时候就编写过一个简易的围棋打谱软件作为编程的 作业。
TAG: OpenGL  围棋  

作为一个围棋爱好者兼程序员,多年以来开发过很多与围棋有关的软件,诸如围棋打谱 软件、棋谱管理软件、围棋棋谱下载软件、围棋网站下载软件……而其中,围棋打谱软件开发的次数最多,读书的时候就编写过一个简易的围棋打谱软件作为编程的 作业。编程水平逐渐提高之后,又开发过新的围棋打谱软件。曾经买过一个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的几何意义,在上面的原理图中也有体现)。源码中我加入了非常详尽的注释,应该很容易理解。

 

  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.     const GLfloat PI = (GLfloat)(3.141592653589);//圆周率PI 
  17.     GLfloat fYRotStep = 2.0f * PI / nStack;     //沿着Y轴旋转的步长 
  18.     GLfloat fRange = atan(a / (b + c));          //顶部圆弧的角度(单位为弧度) 
  19.     GLfloat R = sqrt(a * a + (b + c) * (b + c)) + c;//大圆顶半径 
  20.  
  21.     GLint nSlice1 = nSlice;                     //底部纵向分片数量 
  22.     GLint nSlice2 = (GLint)(nSlice * (PI - fRange) * c / a);//侧面纵向分片数量 
  23.     GLint nSlice3 = (GLint)(nSlice * R * fRange / a);//顶部纵向分片数量 
  24.  
  25.     GLfloat fStep1 = a / nSlice1;               //顶部步长 
  26.     GLfloat fStep2 = (PI - fRange) / nSlice2;   //侧面步长(弧度) 
  27.     GLfloat fStep3 = fRange / nSlice3;          //顶部步长(弧度) 
  28.  
  29.     GLfloat dr = -0.5f / (nSlice1 + nSlice2 + nSlice3);//纹理半径增加的步长 
  30.  
  31.     GLint i = 0, j = 0; 
  32.     for (i=0; i<nStack; i++) 
  33.     { 
  34.         GLfloat fYR = i * fYRotStep;         //当前沿着Y轴旋转的弧度 
  35.         GLfloat fZ = -sin(fYR);              //Z分量比率 
  36.         GLfloat fX = cos(fYR);               //X分量比率 
  37.         GLfloat fZ1 = -sin(fYR + fYRotStep); //下一列的Z分量比率 
  38.         GLfloat fX1 = cos(fYR + fYRotStep);  //下一列的X分量比率 
  39.         GLfloat rs = 0.5f;                   //纹理半径的起点 
  40.  
  41.         glBegin(GL_TRIANGLE_STRIP); 
  42.  
  43.         //底部 
  44.         for (j=0; j<nSlice1; j++) 
  45.         { 
  46.             GLfloat r = fStep1 * j; 
  47.  
  48.             glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ); 
  49.             glNormal3f(0.0f, -1.0f, 0.0f); 
  50.             glVertex3f(r * fX, b, r * fZ); 
  51.  
  52.             glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1); 
  53.             glNormal3f(0.0f, -1.0f, 0.0f); 
  54.             glVertex3f(r * fX1, b, r * fZ1); 
  55.  
  56.             rs += dr; 
  57.         } 
  58.  
  59.         //侧面 
  60.         for (j=0; j<nSlice2; j++) 
  61.         { 
  62.             GLfloat r = a + c * sin(fStep2 * j); 
  63.             GLfloat y = b + c - c * cos(fStep2 * j); 
  64.             GLfloat nr = sin(fStep2 * j); 
  65.             GLfloat nY = -cos(fStep2 * j); 
  66.  
  67.             glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ); 
  68.             glNormal3f(nr * fX, nY, nr * fZ); 
  69.             glVertex3f(r * fX, y, r * fZ); 
  70.  
  71.             glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1); 
  72.             glNormal3f(nr * fX1, nY, nr * fZ1); 
  73.             glVertex3f(r * fX1, y, r * fZ1); 
  74.  
  75.             rs += dr; 
  76.         } 
  77.  
  78.         //顶部 
  79.         for (j=0; j<=nSlice3; j++) 
  80.         { 
  81.             GLfloat r = R * sin(fRange - j * fStep3); 
  82.             GLfloat y = R * cos(fRange - j * fStep3); 
  83.             GLfloat nr = sin(fRange - j * fStep3); 
  84.             GLfloat nY = cos(fRange - j * fStep3); 
  85.  
  86.             glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ); 
  87.             glNormal3f(nr * fX, nY, nr * fZ); 
  88.             glVertex3f(r * fX, y, r * fZ); 
  89.  
  90.             glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1); 
  91.             glNormal3f(nr * fX1, nY, nr * fZ1); 
  92.             glVertex3f(r * fX1, y, r * fZ1); 
  93.  
  94.             rs += dr; 
  95.         } 
  96.  
  97.         glEnd(); 
  98.     } 

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

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

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

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

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

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

(acloud)
本站文章除注明转载外,均为本站原创或编译欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,同学习共成长。转载请注明:文章转载自:罗索实验室 [http://www1.rosoo.net/a/201312/16829.html]
本文出处:博客园 作者:acloud
顶一下
(3)
100%
踩一下
(0)
0%
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
栏目列表
将本文分享到微信
织梦二维码生成器
推荐内容