-- [S]tudying in [U]nique [M]ethod to [M]aster c[E]lshade [R]endering [S]kills (误?) --
查看文章 |
[Qt OpenGL教程] Cel-Shading
2007年06月10日 23:12
本篇教程的来源应该说相当复杂,简单的说程序的框架主要基于齐亮的Qt OpenGL系列教程,实现的是NeHe OpenGL教程第37课的Qt4的迁移。(齐亮的Qt OpenGL系列与NeHe OpenGL的对应关系请参见齐亮主页的说明——可惜的是齐亮的教程只进行到第18课。而最原始的作者应该是Sami "MENTAL" Hamlaoui,他把源代码贡献给了NeHe并编写了教程,同时程序里到的模型也是他的爱好使然……Quake2 ?) 关于使用Qt编写OpenGL程序,您首先可以简单看一下《C++ GUI Programming with Qt 4》第8章的“Graphics with OpenGL”部分,或者直接参考齐亮的Qt OpenGL系列(齐亮使用的是Qt3)。坦率的说,自己的迁移工作并不是十分到位,所以如果您发现有什么错误或者有任何改进的建议的话,请来信让我知道,E-mail:linshier@gmail.com。 NeHeWidget类 (由nehewidget.h展开) 原作者开始就定义一堆结构结构来存储数据,具体的说明就不翻译,只是要提醒一下,矩阵的存储这里使用的是一个包含十六个浮点数的1维数组,其原因主要是取决于OpenGL的存储方式,否则使用时是会发生错位的情况,具体细节您如果不很了解的话可以参考一下OpenGL的相关文档。 typedef struct tagMATRIX { float Data[16]; } MATRIX; typedef struct tagVECTOR { float X, Y, Z; } VECTOR; typedef struct tagVERTEX { VECTOR Nor; VECTOR Pos; } VERTEX; typedef struct tagPOLYGON { VERTEX Verts[3]; } POLYGON; 从Qt3升级到Qt4时,一些函数的使用方法发生了变化,再次提醒一下 class NeHeWidget : public QGLWidget { Q_OBJECT public: NeHeWidget( QWidget* parent = 0, bool fs = false ); ~NeHeWidget(); protected: void initializeGL(); void paintGL(); void resizeGL( int width, int height ); void keyPressEvent( QKeyEvent *e ); void loadGLTextures(); protected: bool fullscreen; private: bool readMesh(); 使用这个函数来读取模型数据 private: bool outlineDraw; bool outlineSmooth; 这里是两个线框显示效果的标志 float outlineWidth; 这个变量用来存储线框的宽度 VECTOR lightAngle; 这里并不使用OpenGL提供的光线,自定义的灯光的基本参数就是光线的方向 bool lightRotate; 是否旋转灯光的标志 float modelAngle; bool modelRotate; 标识模型的旋转角度和是否进入旋转状态 POLYGON *polyData; int polyNum; 模型多边形面的数据 GLuint shaderTexture[1]; 用来存储我们的卡通材质 }; (由nehewidget.cpp展开) 因为我们在程序中要使用到sqrtf这个函数,所以math.h头文件是必须的。 #include "nehewidget.h" #include <QKeyEvent> #include <math.h> #include <QFile> #include <QTextStream> #include <QMessageBox> inline float dotProduct( VECTOR &V1, VECTOR &V2 ) { return V1.X * V2.X + V1.Y * V2.Y + V1.Z * V2.Z; } inline float magnitude( VECTOR &V ) { return sqrtf (V.X * V.X + V.Y * V.Y + V.Z * V.Z); } void normalize( VECTOR &V ) { float M = magnitude (V); if ( M != 0.0f ){ V.X /= M; V.Y /= M; V.Z /= M; } } void rotateVector( MATRIX &M, VECTOR &V, VECTOR &D ) { D.X = (M.Data[0] * V.X) + (M.Data[4] * V.Y) + (M.Data[8] * V.Z); D.Y = (M.Data[1] * V.X) + (M.Data[5] * V.Y) + (M.Data[9] * V.Z); D.Z = (M.Data[2] * V.X) + (M.Data[6] * V.Y) + (M.Data[10] * V.Z); } 一些基本的三维运算函数,大家如果记不大清的话,可以找回《高代》书复习复习。 float outlineColor[3] = { 0.0f, 0.0f, 0.0f }; 在这里可以设置线框的颜色 NeHeWidget::NeHeWidget(QWidget* parent, bool fs) : QGLWidget(parent) , polyNum(0) { xRot = yRot = zRot = 0.0; zoom = -5.0; xSpeed = ySpeed = 0.0; filter = 0; light = false; fogFilter = 0; fullscreen = fs; setGeometry( 0, 0, 640, 480 ); if (fullscreen) showFullScreen(); outlineDraw = true; outlineSmooth = false; outlineWidth = 3.0f; lightRotate = false; modelAngle = 0.0f; modelRotate = false; polyData = NULL; polyNum = 0; lightAngle.X = 0.0f; lightAngle.Y = 0.0f; lightAngle.Z = 1.0f; normalize( lightAngle ); 初始化一些环境参数 readMesh(); 读取模型数据 } NeHeWidget::~NeHeWidget() { } void NeHeWidget::initializeGL() { loadGLTextures(); glShadeModel( GL_SMOOTH ); // Enables Smooth Color Shading glDisable( GL_LINE_SMOOTH ); // Initially Disable Line Smoothing glClearColor( 0.5, 0.5, 0.5, 1.0 ); glClearDepth( 1.0 ); glEnable( GL_DEPTH_TEST ); glDepthFunc( GL_LEQUAL ); glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST ); glEnable( GL_CULL_FACE ); // Enable OpenGL Face Culling glDisable( GL_LIGHTING ); // Disable OpenGL Lighting } 模型数据的读取也没有没有过多需要解释的地方,只是如果您想我一样想用Qt的文件接口来读取数据的话,记得将QDataStream字节序设成LittleEndian bool NeHeWidget::readMesh () { QFile file( "Data\\model.txt" ); if ( !file.open( QIODevice::ReadOnly ) ) { QMessageBox::warning( this, tr( "Error" ), tr("Cannot read file %1:\n%2." ) .arg( file.fileName() ) .arg( file.errorString() ) ); return false; } QDataStream in( &file ); in.setByteOrder(QDataStream::LittleEndian); in >> polyNum; polyData = new POLYGON[polyNum]; for( int i = 0; i < polyNum; i++ ){ //polyNum for ( int j = 0; j < 3; j++ ){ in >> polyData[i].Verts[j].Nor.X >> polyData[i].Verts[j].Nor.Y >> polyData[i].Verts[j].Nor.Z; in >> polyData[i].Verts[j].Pos.X >> polyData[i].Verts[j].Pos.Y >> polyData[i].Verts[j].Pos.Z; } } file.close(); return true; } 注意区分一下,模型数据文件和材质数据文件,也许是方便我们设置调试材质,原作者将其存储为文本文件 void NeHeWidget::loadGLTextures() { QString line; float shaderData[32][3]; QFile file( "Data\\Shader.txt" ); if ( !file.open( QIODevice::ReadOnly ) ) { QMessageBox::warning( this, tr( "Error" ), tr("Cannot read file %1:\n%2." ) .arg( file.fileName() ) .arg( file.errorString() ) ); return; } QTextStream in( &file ); for ( int i = 0; i < 32; i++ ){ if (in.atEnd()) break; line = in.readLine(); shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = line.toFloat(); } file.close(); glGenTextures( 1, &shaderTexture[0] ); glBindTexture( GL_TEXTURE_1D, shaderTexture[0] glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); glTexParameteri( GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); glTexImage1D( GL_TEXTURE_1D, 0, GL_RGB, 32, 0, GL_RGB , GL_FLOAT, shaderData ); } void NeHeWidget::paintGL() { float TmpShade; MATRIX TmpMatrix; VECTOR TmpVector, TmpNormal; 先定义了一些临时变量 glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); glLoadIdentity(); anti-alaising检查,大家应该很容易理解。 if ( outlineSmooth ){ glHint( GL_LINE_SMOOTH_HINT, GL_NICEST ); glEnable( GL_LINE_SMOOTH ); }else glDisable( GL_LINE_SMOOTH ); 为了更好的看到载入的模型,我们将摄象机向后移动2个单位,之后以一定角度旋转模型,这样做就和我们平时用到的三维软件的旋转效果一致了,如果我们用相反的顺序设置的话,模型将绕摄象机旋转,也许会让您有不适应的感觉。 glTranslatef( 0.0f, 0.0f, -2.0f ); glRotatef( modelAngle, 0.0f, 1.0f, 0.0f ); 对于需要自行处理渲染函数的我们而言,真正工作的第一步就是当前的MODELVIEW矩阵从OpenGL中取出并存储在TmpMatrix里。 glGetFloatv( GL_MODELVIEW_MATRIX, TmpMatrix.Data ); // Get The Generated Matrix 原作者使用magic来形容执行以下若干行代码后的效果。首先我们启用一维纹理,然后启用着色纹理。这被OpenGL用来当作一个look-up表格。关于模型的颜色的设定,原作者推荐并选择了白色,原因是它亮度高并且描影法比其它颜色好。同时对于黑色,我这里也强烈建议您不要使用:) // Cel-Shading Code glEnable( GL_TEXTURE_1D ); glBindTexture( GL_TEXTURE_1D, shaderTexture[0] ); 下面一段代码涉及到矩阵变换的内容,原作者做了一番唠叨,不过我觉得没有多大必要,如需要,我觉得您可以复习一下图形学的相关章节会更清楚一些:)核心就是要来自行处理光线渲染的功能,将顶点法线(经过矩阵变换后的)与光线方向点积得到TmpShade glColor3f( 1.0f, 1.0f, 1.0f ); glBegin( GL_TRIANGLES ); for (int i = 0; i < polyNum; i++ ){ for (int j = 0; j < 3; j++){ TmpNormal.X = polyData[i].Verts[j].Nor.X; TmpNormal.Y = polyData[i].Verts[j].Nor.Y; TmpNormal.Z = polyData[i].Verts[j].Nor.Z; rotateVector( TmpMatrix, TmpNormal, TmpVector ); normalize( TmpVector ); TmpShade = dotProduct( TmpVector, lightAngle ); TmpShade需要约束在0——1的范围 if ( TmpShade < 0.0f ) TmpShade = 0.0f; 之后TmpShade相当于index,到look-up表中(我们前面设置的一维纹理中),查找得到对应的材质值。或者,您可以理解为在顶点方向与光线方向夹角与一维纹理之间建立起一种线性的映射关系。然后,我们很自然的把这个点绘制好就行了,就像我们平时做的那样。 glTexCoord1f( TmpShade ); glVertex3fv( &polyData[i].Verts[j].Pos.X ); } } glEnd(); glDisable (GL_TEXTURE_1D); 首先程序启用了OpenGL的混合效果,虽然它可以让画面看起来更漂亮一些,但需要说明的是,这里混合模式的设置并不是必须的。实际上,描绘轮廓的基本思路是,需要更改OpenGL的绘制模式,使得背面的ploygon使用线框绘制,而剔除掉正面的ploygon,同时保证深度测试的类型为小于或等于就可以了。 if ( outlineDraw ){ glEnable( GL_BLEND ); glBlendFunc( GL_SRC_ALPHA ,GL_ONE_MINUS_SRC_ALPHA ); glPolygonMode( GL_BACK, GL_LINE ); glLineWidth( outlineWidth ); glCullFace (GL_FRONT); glDepthFunc (GL_LEQUAL); glColor3fv (&outlineColor[0]); glBegin (GL_TRIANGLES); for ( int i = 0; i < polyNum; i++ ){ for ( int j = 0; j < 3; j++ ){ glVertex3fv (&polyData[i].Verts[j].Pos.X); } } glEnd (); glDepthFunc (GL_LESS); glCullFace (GL_BACK); glPolygonMode (GL_BACK, GL_FILL); glDisable (GL_BLEND); } } void NeHeWidget::resizeGL( int width, int height ) { if (height == 0){ height = 1; } glViewport( 0, 0, (GLint)width, (GLint)height ); glMatrixMode( GL_PROJECTION ); glLoadIdentity(); gluPerspective( 45.0, (GLfloat)width/(GLfloat)height, 0.1, 100.0 ); glMatrixMode( GL_MODELVIEW ); glLoadIdentity(); } 对场景的控制: <SPACE> = 按住旋转场景 1 = 轮廓绘制开关 2 = anti-aliasing开关 <UP> =增加线宽 <DOWN> = 减小线宽 void NeHeWidget::keyPressEvent( QKeyEvent *e ) { switch ( e->key() ){ case Qt::Key_Space: modelAngle += 2.0f; updateGL(); break; case Qt::Key_1: outlineDraw = !outlineDraw; updateGL(); break; case Qt::Key_2: outlineSmooth = !outlineSmooth; updateGL(); break; case Qt::Key_Up: outlineWidth++; updateGL(); break; case Qt::Key_Down: outlineWidth--; updateGL(); break; case Qt::Key_F2: fullscreen = !fullscreen; if ( fullscreen ){ showFullScreen(); }else { showNormal(); setGeometry( 0, 0, 640, 480 ); } update(); break; case Qt::Key_Escape: close(); } } 好了,所有有关OpenGL的工作都完成了,现在只需要添加一个main.cpp就大功告成了^__^ #include <qapplication.h> #include <qmessagebox.h> #include "nehewidget.h" int main( int argc, char *argv[] ) { bool fs = false; QApplication a(argc,argv); switch( QMessageBox::information( 0, "Start FullScreen?", "Would You Like To Run In Fullscreen Mode?", QMessageBox::Yes, QMessageBox::No | QMessageBox::Default ) ){ case QMessageBox::Yes: fs = true; break; case QMessageBox::No: fs = false; break; } NeHeWidget w(0, fs); w.show(); return a.exec(); } `````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````` Sami Hamlaoui (MENTAL) 在原文的最后总结道: 现在,您可以看到Cel-Shading并非难事。当然,技术可以更加精进。XIII就是一个很好的例子http://www.nvidia.com/object/game_xiii.html,在XIII中您会认为自己沉浸在一个卡通世界里。如果您想在卡通透视技术里达到更深层次,你可以浏览《Real-time Rendering》这本书“Non-Photorealistic Rendering”这一章的内容。如果您更喜欢在WEB上读论文,在这里可以发现一大堆的链接:http://www.red3d.com/cwr/npr/ |


