更新Nehe的OpenGL中文教程源代码下载地址
http://code.google.com/p/nehe-src-cn/downloads/list
因为我租用的主机上流量过大,不得已而为之,非常抱歉
不得不转移到google code上,测试了以下速度还不错
http://code.google.com/p/nehe-src-cn/downloads/list
因为我租用的主机上流量过大,不得已而为之,非常抱歉
不得不转移到google code上,测试了以下速度还不错
linux下写简单的OpenGL程序.Mark Kilgard(glut的作者)的确是做了件有益于众人的事情.我这里使用了glut包.
代码下载
1.安装libglut3-dev包
apt-get install libglut3-dev
2.编辑源文件并编译
假设你的程序的名字叫main.c
gcc main.c -o main -lglut
一个比较完整的版本
gcc main.c -o main -I/usr/X11R6/include/ -L/usr/X11R6/lib -lX11 -lXi -lglut -lGL -lGLU
3.run
./main
不管怎么说,是跑起来了.
点击下载chm文件
在线阅读:
http://www.yakergong.com/nehe
[教程说明]
教程的英文原版由nehe制作。
课程内容由dancingwind(周炜)以及gamedev和csdn的志愿者翻译,最早的翻译应该是由CKER完成的(1~12章)。
由yaker做少量修改编译成了电子书。
注:
本教程经dancingwind授权发布于yakergong.com
dancingwind获得Nehe授权。
今天是个对我的网站有意义的日子,nehe的OpenGL教程的访问量超过了我的blog的访问量。另外一个Wtl教程的访问量也排到第三。

还是符合我的判断的,我网站的访问量很小,我也不是很在意这个东西,我想的是让我的网站提供用户最需要的内容。我在nehe的教程的“关于本教程”页面里说了,以前的blog访问量最高的一篇便是OpenGL教程的下载。那个教程是全英文版的,我自己看还好,对普通的用户来说就有点难了。我就萌生了翻译nehe的教程的想法,后来认识了dancingwind,发现他已经做好了,于是就把他的成果放到网站上了。
还有下面的地理分布图,从图里也可以大致反映出国内IT业的分布情况。

它是NVPerfKit的一个组件。
装NVPerfKit要先卸载本机的NVidia显卡驱动,NVPerfKit会替换驱动,这样它就可以在底层截取渲染操作的信息。控制面板里的NVidia developer control panel也变为可用。
今天碰到了点问题,前几天把ModelShadow(模型的阴影渲染)重写了一下,在Nehe的框架下写的。现在要转到xophiix的框架下。两边的操作基本上完全一样,但是那个阴影却不见了。
gDEBugger分了八栏:OpenGL function calls history,OpenGL State Variables,Call stack,Properities,performance graph,counter, performance Dashboard,Function Calls Statistics.今天主要用的四OpenGL function calls history和OpenGL State Variables.监视了GL_CULL_FACE,GL_CULL_FACE_MODE,GL_STENCIL_BITS这几个关系比较大的状态变量,没对比出什么区别来。
然后把OpenGL function call录制了一遍,这个功能真的很赞。录完点右边的一个按钮就可以在浏览器里看到,我对比了一下阴影渲染部分的主要调用,还是没有太大不同。

还有就是可以设置断点,在出现OpenGL错误或者接到NVidia GLExpert的报告时中断,还可以自行设置在执行某些OpenGL函数时中断,当然,如果支持条件中断就更好了。发现了一个以前的小bug,一个错误的OpenGL调用。
还可以在性能分析(profile)模式下工作,这个我并不了解,用NVPerfHUD也许更好吧,不过NVPerfHUD是为D3D程序工作的…..。可惜gDEBugger不是免费的,只能用30天。
下载NVPerfKit
这篇文章主要内容翻译自reference(1),同时也讲一下Nehe的实现方式。这里使用的是z-pass方法。
代码下载
感谢两个人的话:
DancingWind:如果我能让读者在读我的书的时候节省一分钟,那么这个班上有60个同学,我就节约了一个小时。好的教程往往可以事办功倍。
Emilmatthew:让更多人受益。
虽然我能力所限做不出好的教程,但是会尽力的。
Reference:
(1).Volume Shadows Tutorial by John Tsiombikas
(2).Nehe OpenGL tutorial:lesson 27 by Nehe
Steps:
[原理]首先,讲一下阴影体原理
我会尽我所能的讲得清楚一些,但是我仍然希望读者队模板缓存(stencil buffer),深度(depth)有基本的了解。我自己的学习过程也遇到很多困难,我所得到得经验就是要先把原理弄明白,而不是急于看别人写好的code。

这个图是很理想的,足够简单又能说明问题。黄色的点代表光源,红色的平面代表一个三角形的物体,紫色,蓝色,棕色的平面就是构成阴影体的面(实际上这三个面应该在被离光源的方向上继续延长的,现在还是继续使用它吧)。灰色的代表阴影投射到的平面。
我们一边渲染一边讲:
[首先把要绘制阴影之外的物体绘制],这也是一个原则阴影物体要最后被绘制,这样做的目的是为了获取深度信息。深度:depth也就是物体离视点的距离,深度信息通常用做遮挡判断。比如在三维空间里绘制了一前一后两个平面,就要通过深度信息来判断两个平面的前后位置,进而其中一个平面判断那些位置被遮挡了。绘制完成之后屏幕上每点的深度值都已经确定下来,然后使深度缓存不可写
比如:glDepthMask(GL_FALSE);(DX下有类似的做法)
但是允许深度测试:glEnable(GL_DEPTH_TEST);
测试函数为:glDepthFunc(GL_LEQUAL);
也就是说只有深度小于原象素的象素才会被接受。
[然后绘制阴影体]
先清空模板缓存:glClearStencil(0);
开启模板缓存测试:glEnable(GL_STENCIL_TEST);
设置测试比较函数为: glStencilFunc(GL_ALWAYS, 1, 0xffffffff);
然后设置片元通过或者未通过模板测试时如何队模板缓存中的数据进行更改: glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);也就是通过测试时增1。
然后绘制阴影体的所有朝向你的面,阴影体是这样形成的,以光源为一个顶点,物体边缘的一条边为边的三角形无限延伸所形成的四边形(理论上讲应该是一个无限的,但实际上长用一个足够长的来模拟),本例中就是紫色和蓝色的面。容易发现,这两个面上的所有片元都可以通过测试
相对应位置的模板缓存的值都会加1。
但是相应位置的深度信息不变,因为深度缓存被设置为不可写。
结果如右图所示:淡蓝色的区域模板缓存值增1。

然后重设模板测试比较函数:glStencilOp(GL_KEEP, GL_KEEP, GL_DECR);
绘制阴影体的所有背向视点的面
也就是当片于通过模板测试时,相应的模板缓存区域的值减一。接着红色区域+棕色区域的那个四边形被渲染。但是这个四边形位于红色的三角形的背后的部分无法通过深度测试,那个区域片元的位置比三角形离视点更远。

左图中棕色区域的模板缓存相应位置的值减1。经过这两次渲染,模板缓存中不为0的地方就是右边的淡蓝色区域减左边的棕色区域。
那个区域就是棕色区域下面的三角形区域,也就是阴影区。
实际上标准的做法是把场景渲染两遍的,这个下面我会翻译。现在使用的是NeHe的做法,直接给整个屏幕蒙上一个蒙板,非阴影区颜色不变,阴影区颜色加深。
glStencilFunc(GL_NOTEQUAL, 0, 0xffffffff);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
//draw a shadowing rectangle covering the entire screen
glColor4f(0.0f, 0.0f, 0.0f, 0.4f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glPushMatrix();
//将当前矩阵load indentity后,绘制任意大小的矩形都将覆盖整个屏幕
glLoadIdentity();
glBegin(GL_TRIANGLE_STRIP);
glVertex3f(-0.1f, 0.1f, -0.10f);
glVertex3f(-0.1f, -0.1f, -0.10f);
glVertex3f( 0.1f, 0.1f, -0.10f);
glVertex3f( 0.1f, -0.1f, -0.10f);
glEnd();
glPopMatrix();
这个覆盖全屏的做法感觉有点投机取巧
gluPerspective(45.0, (GLfloat)width/(GLfloat)height, 0.1, 100.0);
如果那个角度比45.0f大就无法覆盖了,我觉得正确的做法应该是改用正投影。
然后就可以恢复OpenGL状态机设置,绘制物体本身。
效果图如下:
[代码说明]
1.先说一下3ds文件的结构,它是可能包含有多个object的,读进来的模型也是包含多个object的。
// 模型信息结构体
struct t3DModel
{
UINT texture[MAX_TEXTURES];
int numOfObjects; // 模型中对象的数目
int numOfMaterials; // 模型中材质的数目
std::vector <tMaterialInfo> pMaterials;// 材质链表信息
std::vector <t3DObject> pObject; // 模型中对象链表信息
};
2.首先执行阴影的初始化工作
//载入模型
g_model.Import3DS(fileName);
// 初始化阴影
g_modelShadow.Init(g_model.GetModel(), g_lightPos);
以下是ModelShadow::Init的定义
bool ModelShadow::Init(const t3DModel &model,const float *lightPos)
{
// 获取object的数目
objNum_ = model.numOfObjects;
objInfoTable_.resize(objNum_);
for(int objIndex = 0 ; objIndex < objNum_ ; objIndex++ )
{
....
}
glClearStencil(0);
....
SetConnectivity(); // 设置三角形之间的链接关系
CalcPlane(); // 计算每个三角形的平面方程
....
}
3.初始化之后就可以渲染了
// 绘制模型,阴影
// 重设光源位置,如果光源位置不便可以忽略
g_modelShadow.ResetLightPos(g_lightPos);
// 这里获取当前的model view matrix(MVM)
g_modelShadow.GetCurMatrix();
glPushMatrix();
// 放置物体
glTranslatef(g_modelPos[0], g_modelPos[1], g_modelPos[2]);
// 这里又获取了一次model view matrix
g_model.Draw();
g_modelShadow.Draw();
glPopMatrix();
通过两次获得的矩阵的差异就可以算出中间做了那些变换,比如(glTranslatef(g_modelPos[0], g_modelPos[1], g_modelPos[2]); )
现在看一下ModelShadow::Draw的实现
void ModelShadow::Draw() const
{
glClear(GL_STENCIL_BUFFER_BIT);
GLfloat objMatrix[16];
glGetFloatv(GL_MODELVIEW_MATRIX, objMatrix);
// 求逆矩阵,这四条语句就是在求物体的局部坐标系里灯的坐标
InverseMatrix16(objMatrix);
MultiplyMatrix16(objMatrix, curMatrix_);
memcpy(lightPos_, orignLightPos_, 4 * sizeof(float));
Mtx16MultVect4(objMatrix, lightPos_);
//Cast the shadow
CastShadow();
}
再看ModelShadow::castShadow
void ModelShadow::CastShadow() const
{
SetVisibility(); // 设置每个三角形的可见性
....
// 第一趟渲染,通过模板测试的话模版值增一
glFrontFace(GL_CCW);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
DoShadowPass();
// 第二趟渲染,通过模板测试的话模版值减一
glFrontFace(GL_CW);
glStencilOp(GL_KEEP, GL_KEEP, GL_DECR);
DoShadowPass();
….
glStencilFunc(GL_NOTEQUAL, 0, 1);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
//在全屏幕混合一张图
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glColor4f(0, 0, 0, 0.3f);
….
}
DEMO下载
源文件下载
这个暑假写的学的东西,最近整理起来,刚刚学OpenGL,很多方面做得都不好。
压缩包里有介绍,先看看效果

先反省一下,以前写的确实过于简略,代码又没有设么注释。感谢EmilMatthew的话,让更多人受益 源代码正在改写,以前变量命名很随意,反正很不清晰,更提不上elegant了。另一方面也在进行优化
核心算法描述:(实际上并不算是什么算法)树主要分两部分:树枝和树干。树干使用二次曲面圆柱(gluCylinder,注意这个圆柱是可以上下半径不等的)近似,树叶就贴一片纹理。树的绘制是递归进行的,也就是说:在一段树桩上接上三段树枝所形成的东西仍然叫做树(DEMO里每一段树枝上都有三段小树枝,递归的深度是四,可以自己调整)。用代码来写的话:
void DrawTree(int depth) { DrawStem(); //绘制树桩 if (m_ndepth > 2) DrawLeaf(); // 绘制树叶 //未绘制到顶层的话继续递归绘制 MoveToStemTop(); //移动到树桩顶部(注意圆柱体是没有上下表面的,所以要用树枝挡住) DrawTree(depth + 1); //绘制第一个树枝 MoveToProperPos(); //移动到适当位置(glPushMatrix();glRotatef();glTranlatef();...) DrawTree(depth + 1); //绘制第二个树枝 MoveToProperPos(); //移动到适当位置 DrawTree(depth + 1); //绘制第三个树枝 }
绘制函数可以这样被调用:DrawTree(1); m_nDepth指的是树的递归层数,加深一层速度会慢三倍,但是可以得到更繁茂的树。 DEMO里只有四层,只是用了很大的叶子去掩盖,并且一片纹理图上就有五片叶子。不过总比一棵树上贴五片树枝纹理图,然后用billboard 好的多,很多著名游戏都是这么做的。
post:还有一点别的事情,树的顶端树枝很细,所以我并没有使用二次曲面进行模拟,而是使用了长方体。这样会稍微快一点。比较粗的树干上通常是没有叶子的,所以绘制的过程中有相关的判断,地层的树枝上不会有叶子。 我用AMD CodeAnalyst 对程序进行了分析,可惜并没有找到真正的热点。对于代码优化来讲,算法上的优化通常会更优。 在我的机器(AMD Sempron 2800+ 1.6G,512MB,6600LE 128MB)上,把绘制树的过程做成显示列表后,调用一次列表的时间大约为0.85ms ,使用近似之后可以节约大约0.04ms。目前对我来说主要问题还是效率,我希望效率可以提升一倍,反正要比导入模型更快。最近主要针对的是消除递归,树是递归定义的,希望消除递归能提高效率。主要方法还是用栈,二叉树和三叉树消除递归好像差不多。还有隐藏面消除,显卡也会完成隐藏面消除,我希望要的结果是不需要绘制的面(或者点)的信息就不要传给显卡,估计自己做可能倒不如传给显卡。还有更快的圆柱体绘制方法。还有使用更小更精巧的纹理,看WAR3的纹理图给了我一点启发,一张256X256的图片巧妙的交错存储了两种植物的纹理。30多个面,50多个顶点做出来的植物也是非常逼真,不过WAR3并不是以真实感为卖点的游戏,其实树做的就很搓,实时性可能更重要一些。如果有人对这个感兴趣的话可以在我的blog上留言。
注:递归消除部分的代码已经写完,但是效率并没有得到提高。
Recent Comments