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的几何意义,在上面的原理图中也有体现)。源码中我加入了非常详尽的注释,应该很容易理解。

#include <math.h>
#include <stdlib.h>
#include <GL/glut.h>
#pragma comment(lib,"glut32.lib")

/***************************************************************************//** 
* 函数名称:    DrawChess 
* 功能描述:    绘制一个围棋子。 
* 参 数:    a    >> 底部半径; 
* 参 数:    b    >> 底部距离圆心距离; 
* 参 数:    c    >> 侧面半径; 
* 参 数:    n    >> 分割粒度; 
* 返回值:     
* 其它说明:     
* 修改日期        修改人            修改内容 
* ------------------------------------------------------------------------------ 
* 2011-08-28    Cloud         创建 
*******************************************************************************/ 
// void DrawChess(double a, double b, double c, int n) 
// { 
// 	const double PI = 3.14159265358979323846; 
// 	double fRange1 = PI - atan(a / (b + c));            //侧面弧度区间 
// 	double R = sqrt(a * a + (b + c) * (b + c)) + c;        //大圆顶半径 
// 	double fRange2 = atan(a / (b + c));                    //顶部弧度区间 
// 	double vPos[3], vNormal[3];                            //顶点位置和法线方向 
// 
// 	for (int i=0; i<n; i++) 
// 	{ 
// 		for (int j=0; j<n; j++) 
// 		{ 
// 			;//底面 
// #define FILL1(n1, n2) \ 
// 			{\ 
// 				vPos[0] = (-a * (n1) / n) * cos((n2) * (2.0 * PI) / n);\ 
// 				vPos[1] = b;\ 
// 				vPos[2] = (-a * (n1) / n) * sin((n2) * (2.0 * PI) / n);\ 
// 				vNormal[0] = 0;\ 
// 				vNormal[1] = -1.0;\ 
// 				vNormal[2] = 0;    \ 
// 			}\ 
// 
// #define DRAW_TRIANGLE(x) \ 
// 			{\ 
// 			glBegin(GL_TRIANGLE_STRIP);\ 
// 			FILL##x(i, j);            glNormal3dv(vNormal); glVertex3dv(vPos);\ 
// 			FILL##x(i + 1, j);        glNormal3dv(vNormal); glVertex3dv(vPos);\ 
// 			FILL##x(i, j + 1);        glNormal3dv(vNormal); glVertex3dv(vPos);\ 
// 			FILL##x(i + 1, j + 1);    glNormal3dv(vNormal); glVertex3dv(vPos);\ 
// 			glEnd();\ 
// 			}\ 
// 
// 			DRAW_TRIANGLE(1); 
// 
// 			//侧面 
// #define FILL2(n1, n2) \ 
// 			{\ 
// 				vPos[0] = (-a - c * sin((n1) * fRange1 / n)) * cos((n2) * (2.0 * PI) / n);\ 
// 				vPos[1] = b + c - c * cos((n1) * fRange1 / n);\ 
// 				vPos[2] = (-a - c * sin((n1) * fRange1 / n)) * sin((n2) * (2.0 * PI) / n);\ 
// 				vNormal[0] = -sin((n1) * fRange1 / n) * cos((n2) * (2.0 * PI) / n);\ 
// 				vNormal[1] = -cos((n1) * fRange1 / n);\ 
// 				vNormal[2] = -sin((n1) * fRange1 / n) * sin((n2) * (2.0 * PI) / n);\ 
// 			}\ 
// 
// 			DRAW_TRIANGLE(2); 
// 
// 			//顶部 
// #define FILL3(n1, n2) \ 
// 			{\ 
// 				vPos[0] = (-R * sin(fRange2 - fRange2 * (n1) / n)) * cos((n2) * (2.0 * PI) / n);\ 
// 				vPos[1] = R * cos(fRange2 - fRange2 * (n1) / n);\ 
// 				vPos[2] = (-R * sin(fRange2 - fRange2 * (n1) / n)) * sin((n2) * (2.0 * PI) / n);\ 
// 				vNormal[0] = -sin(fRange2 - fRange2 * (n1) / n) * cos((n2) * (2.0 * PI) / n);\ 
// 				vNormal[1] = cos(fRange2 - fRange2 * (n1) / n);\ 
// 				vNormal[2] = -sin(fRange2 - fRange2 * (n1) / n) * sin((n2) * (2.0 * PI) / n);\ 
// 			}\ 
// 
// 			DRAW_TRIANGLE(3); 
// 		} 
// 	} 
// } 
/***************************************************************************//**
* 函数名称:    DrawChess
* 功能描述:    绘制一颗围棋子。
* 参    数:    a        >> 底部半径; 
* 参    数:    b        >> 底部距离圆心距离; 
* 参    数:    c        >> 侧面半径; 
* 参    数:    nSlice    >> 纵向分割的粒度;
* 参    数:    nStack    >> 环形分割的粒度;
* 返 回 值:    
* 其它说明:    
* 修改日期        修改人            修改内容
* ------------------------------------------------------------------------------
* 2011-12-05    一片云雾              创建
*******************************************************************************/
void DrawChess(GLfloat a, GLfloat b, GLfloat c, GLint nSlice, GLint nStack)
{
	const GLfloat PI = (GLfloat)(3.141592653589);                //圆周率PI
	GLfloat fYRotStep = 2.0f * PI / nStack;                        //沿着Y轴旋转的步长
	GLfloat fRange = atan(a / (b + c));                            //顶部圆弧的角度(单位为弧度)
	GLfloat R = sqrt(a * a + (b + c) * (b + c)) + c;            //大圆顶半径

	GLint nSlice1 = nSlice;                                        //底部纵向分片数量
	GLint nSlice2 = (GLint)(nSlice * (PI - fRange) * c / a);    //侧面纵向分片数量
	GLint nSlice3 = (GLint)(nSlice * R * fRange / a);            //顶部纵向分片数量

	GLfloat fStep1 = a / nSlice1;                                //顶部步长
	GLfloat fStep2 = (PI - fRange) / nSlice2;                    //侧面步长(弧度)
	GLfloat fStep3 = fRange / nSlice3;                            //顶部步长(弧度)

	GLfloat dr = -0.5f / (nSlice1 + nSlice2 + nSlice3);            //纹理半径增加的步长

	GLint i = 0, j = 0;
	for (i=0; i<nStack; i++)
	{
		GLfloat fYR = i * fYRotStep;                            //当前沿着Y轴旋转的弧度
		GLfloat fZ = -sin(fYR);                                    //Z分量比率
		GLfloat fX = cos(fYR);                                    //X分量比率
		GLfloat fZ1 = -sin(fYR + fYRotStep);                    //下一列的Z分量比率
		GLfloat fX1 = cos(fYR + fYRotStep);                        //下一列的X分量比率
		GLfloat rs = 0.5f;                                        //纹理半径的起点

		glBegin(GL_TRIANGLE_STRIP);

		//底部
		for (j=0; j<nSlice1; j++)
		{
			GLfloat r = fStep1 * j;

			glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ);
			glNormal3f(0.0f, -1.0f, 0.0f);
			glVertex3f(r * fX, b, r * fZ);

			glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1);
			glNormal3f(0.0f, -1.0f, 0.0f);
			glVertex3f(r * fX1, b, r * fZ1);

			rs += dr;
		}

		//侧面
		for (j=0; j<nSlice2; j++)
		{
			GLfloat r = a + c * sin(fStep2 * j);
			GLfloat y = b + c - c * cos(fStep2 * j);
			GLfloat nr = sin(fStep2 * j);
			GLfloat nY = -cos(fStep2 * j);

			glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ);
			glNormal3f(nr * fX, nY, nr * fZ);
			glVertex3f(r * fX, y, r * fZ);

			glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1);
			glNormal3f(nr * fX1, nY, nr * fZ1);
			glVertex3f(r * fX1, y, r * fZ1);

			rs += dr;
		}

		//顶部
		for (j=0; j<=nSlice3; j++)
		{
			GLfloat r = R * sin(fRange - j * fStep3);
			GLfloat y = R * cos(fRange - j * fStep3);
			GLfloat nr = sin(fRange - j * fStep3);
			GLfloat nY = cos(fRange - j * fStep3);

			glTexCoord2f(0.5f + rs * fX, 0.5f + rs * fZ);
			glNormal3f(nr * fX, nY, nr * fZ);
			glVertex3f(r * fX, y, r * fZ);

			glTexCoord2f(0.5f + rs * fX1, 0.5f + rs * fZ1);
			glNormal3f(nr * fX1, nY, nr * fZ1);
			glVertex3f(r * fX1, y, r * fZ1);

			rs += dr;
		}

		glEnd();
	}
}
static void init (void)
{
    glClearColor(0.0, 0.0, 0.0, 0.0);/* 显示窗口颜色为白色*/
	glShadeModel(GL_SMOOTH);
 	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
 	glEnable(GL_POLYGON_MODE);
}
void display()
{
    glClear(GL_COLOR_BUFFER_BIT);
	glTranslatef(0.0, -2.0, -6.0);
	glColor3f(1.0, 1.0, 0.0);
    DrawChess(2, 1, 0.8, 16, 32);
    glFlush();
}

void reshape(int newWidth,int newHeight)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
	gluPerspective(60, 1, 0.01, 100.0);
    glClear(GL_COLOR_BUFFER_BIT);
}

void main( int  argc, char** argv)
{
    glutInit(&argc,argv);
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(500, 500);
    glutCreateWindow("chess point");

    init();
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);

    glutMainLoop();
}

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

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

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

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

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

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

http://www.cnblogs.com/acloud/archive/2011/12/12/DrawChess.html

 

 

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值