|
第37课
|
|
|
卡通映射:
什么是卡通了,一个轮廓加上少量的几种颜色。使用一维纹理映射,你也可以实现这种效果。 |
|
|
|
看到人们仍然e-mail我请求在文章中使用我方才在GameDev.net上写的源代码,还看到文章的第二版(在那每一个API附带源码)不是在中途完成之前连贯的结束。我已经把这篇指南一并出租给了NeHe(这实际上是写文章的最初意图)因此你们所有的OpenGL领袖可以玩转它。对模型的选择表示抱歉,但是我最近一直在玩Quake
2。
注释:这篇文章的源代码可以在这里找到:
http://www.gamedev.net/reference/programming/features/celshading.
这篇指南实际上并不解释原理,仅仅解释代码。在上面的连接中可以发现为什么它能工作。现在不断地大声抱怨STOP E-MAILING ME REQUESTS
FOR SOURCE CODE!!!!
首先,我们需要包含一些额外的头文件。第一个(math.h)我们可以使用sqrtf (square root)函数,第二个用来访问文件。
| |
#include <math.h>
#include <stdio.h>
|
现在我们将定义一些结构体来帮助我们存贮我们的数据(保存好几百浮点数组)。第一个是tagMATRIX结构体。如果你仔细地看,你将看到我们正象包含一个十六个浮点数的1维数组~一个2维4×4数族一样存储那个矩阵。这下至OpenGL存储它的矩阵的方式。如果我们使用4x4数组,这些值将发生错误的顺序。
| |
typedef struct tagMATRIX
{
float Data[16];
}
MATRIX;
|
第二是向量的类。 仅存储X,Y和Z的值 |
|
typedef struct tagVECTOR
{
float X, Y, Z;
}
VECTOR;
|
第三,我们持有顶点的结构。每一个顶点仅需要它的法线和位置(没有纹理的现行纵坐标)信息。它们必须以这样的次序被存放,否则当它停止装载文件的事件将发生严重的错误(我发现艰难的情形:(教我分块出租我的代码。)。
| |
typedef struct tagVERTEX
{
VECTOR Nor;
VECTOR Pos;
}
VERTEX;
|
最后是多边形的结构。我知道这是存储顶点的愚蠢的方法,要不是它完美工作的简单的缘故。通常我愿意使用一个顶点数组,一个多边形数组,和包括一个在多边形中的3个顶点的指数,但这比较容易显示你想干什么。 |
|
typedef struct tagPOLYGON
{
VERTEX Verts[3];
}
POLYGON;
|
优美简单的材料也在这里了。为每一个变量的一个解释考虑那个注释。 |
|
bool outlineDraw = true;
bool outlineSmooth = false;
float outlineColor[3] = { 0.0f, 0.0f, 0.0f };
float outlineWidth = 3.0f;
VECTOR lightAngle;
bool lightRotate = false;
float modelAngle = 0.0f;
bool modelRotate = false;
POLYGON *polyData = NULL;
int polyNum = 0;
GLuint shaderTexture[1];
|
这是得到的再简单不过的模型文件格式。 最初的少量字节存储在场景中的多边形的编号,文件的其余是tagPOLYGON结构体的一个数组。正因如此,数据在没有任何需要去分类到详细的顺序的情况下被读出。 |
|
BOOL ReadMesh ()
{
FILE *In = fopen ("Data\\model.txt", "rb");
if (!In)
return FALSE;
fread (&polyNum, sizeof (int), 1, In);
polyData = new POLYGON [polyNum];
fread (&polyData[0], sizeof (POLYGON) * polyNum, 1, In);
fclose (In);
return TRUE;
}
|
一些基本的数学函数而已。DotProduct计算2个向量或平面之间的角,Magnitude函数计算向量的长度,Normalize函数缩放向量到一个单位长度。
| |
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);
}
|
引擎的第一个主要的函数…… 初始化,按所说的精确地做。我已经砍掉了在注释中不再需要的代码段。 |
|
BOOL Initialize (GL_Window* window, Keys* keys)
{
|
这3个变量用来装载着色文件。在文本文件中为了单一的线段线段包含了空间,虽然shaderData存储了真实的着色值。你可能奇怪为什么我们的96个值被32个代替了。好了,我们需要转换greyscale
值为RGB以便OpenGL能使用它们。我们仍然可以以greyscale存储这些值,但向上负载纹理时我们至于R,G和B成分仅仅使用同一值。 |
|
char Line[255];
float shaderData[32][3];
g_window = window;
g_keys = keys;
FILE *In = NULL;
|
当绘制线条时,我们想要确保很平滑。初值被关闭,但是按“2”键,它可以被toggled on/off。 |
|
glShadeModel (GL_SMOOTH);
glDisable (GL_LINE_SMOOTH);
glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glClearColor (0.7f, 0.7f, 0.7f, 0.0f);
glClearDepth (1.0f);
glEnable (GL_DEPTH_TEST);
glDepthFunc (GL_LESS);
glShadeModel (GL_SMOOTH);
glDisable (GL_LINE_SMOOTH);
glEnable (GL_CULL_FACE);
|
我们使 OpenGL灯光不可用因为我们自己做所以的灯光计算。 |
|
glDisable (GL_LIGHTING);
|
这里是我们装载阴影文件的地方。它简单地以32个浮点值ASCII码存放(为了轻松修改),每一个在separate线上。 |
|
In = fopen ("Data\\shader.txt", "r");
if (In)
{
for (i = 0; i < 32; i++)
{
if (feof (In))
break;
fgets (Line, 255, In);
|
这里我们转换 greyscale 值为 RGB, 正象上面所描述的。 |
|
shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = atof (Line);
}
fclose (In);
}
else
return FALSE;
|
现在我们向上装载这个纹理。同样它清楚地规定,不要使用任何一种过滤在纹理上否则它看起来奇怪,至少可以这样说。GL_TEXTURE_1D被使用因为它是值的一维数组。 |
|
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);
|
现在调整灯光方向。我已经使得它向下指向Z轴正方向,这意味着它将正面碰撞模型 |
|
lightAngle.X = 0.0f;
lightAngle.Y = 0.0f;
lightAngle.Z = 1.0f;
Normalize (lightAngle);
|
读取Mesh文件,并返回 |
|
return ReadMesh ();
}
|
与上面的函数相对应…… 卸载,删除由Initalize 和 ReadMesh 创建的纹理和多边形数据。 |
|
void Deinitialize (void)
{
glDeleteTextures (1, &shaderTexture[0]);
delete [] polyData;
}
|
主要的演示循环。所有这些用来处理输入和更新角度。控制如下:
<SPACE> =锁定旋转
1 = 锁定轮廓绘制
2 = 锁定轮廓 anti-aliasing
<UP> =增加线宽
<DOWN> = 减小线宽 | |
void Update (DWORD milliseconds)
{
if (g_keys->keyDown [' '] == TRUE)
{
modelRotate = !modelRotate;
g_keys->keyDown [' '] = FALSE;
}
if (g_keys->keyDown ['1'] == TRUE)
{
outlineDraw = !outlineDraw;
g_keys->keyDown ['1'] = FALSE;
}
if (g_keys->keyDown ['2'] == TRUE)
{
outlineSmooth = !outlineSmooth;
g_keys->keyDown ['2'] = FALSE;
}
if (g_keys->keyDown [VK_UP] == TRUE)
{
outlineWidth++;
g_keys->keyDown [VK_UP] = FALSE;
}
if (g_keys->keyDown [VK_DOWN] == TRUE)
{
outlineWidth--;
g_keys->keyDown [VK_DOWN] = FALSE;
}
if (modelRotate)
modelAngle += (float) (milliseconds) / 10.0f;
}
|
你一直在等待的函数。Draw 函数做每一件事情——计算阴影的值,着色网孔,着色轮廓,等等,这是它作的。 |
|
void Draw (void)
{
|
TmpShade用来存储当前顶点的色度值。所有顶点数据同时被计算,意味着我们只需使用我们能继续使用的单个的变量。
TmpMatrix, TmpVector 和 TmpNormal同样被用来计算顶点数据,TmpMatrix在函数开始时被调整一次并一直保持到Draw函数被再次调用。TmpVector
和 TmpNormal则相反,当另一个顶点被处理时改变。
| |
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个单元,之后以一定角度旋转模型。注:由于我们首先移动摄象机,这个模型将在现场旋转。如果我们以另一种方法做,模型将绕摄象机旋转。
我们之后从OpenGL中取最新创建的矩阵并把它存储在 TmpMatrix。
| |
glTranslatef (0.0f, 0.0f, -2.0f);
glRotatef (modelAngle, 0.0f, 1.0f, 0.0f);
glGetFloatv (GL_MODELVIEW_MATRIX, TmpMatrix.Data);
|
戏法开始了。首先我们启用一维纹理,然后启用着色纹理。这被OpenGL用来当作一个look-up表格。我们之后调整模型的颜色(白色)我选择白色是因为它亮度高并且描影法比其它颜色好。我建议你不要使用黑色:)
| |
glEnable (GL_TEXTURE_1D);
glBindTexture (GL_TEXTURE_1D, shaderTexture[0]);
glColor3f (1.0f, 1.0f, 1.0f);
|
现在我们开始绘制那些三角形。尽管我们看到在数组中的每一个多边形,然后旋转它的每一个顶点。第一步是拷贝法线信息到一个临时的结构中。因此我们能旋转法线,但仍然保留原来保存的值(没有精确降级)。
| |
glBegin (GL_TRIANGLES);
for (i = 0; i < polyNum; i++)
{
for (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;
|
第二,我们通过初期从OpenGL中攫取的矩阵来旋转这个法线。我们之后规格化因此它并不全部变为螺旋形。 |
|
RotateVector (TmpMatrix, TmpNormal, TmpVector);
Normalize (TmpVector);
|
第三,我们获得那个旋转的法线的点积灯光方向(称为lightAngle,因为我忘了从我的旧的light类中改变它)。我们之后约束这个值在0——1的范围。(从-1到+1)
|
|
TmpShade = DotProduct (TmpVector, lightAngle);
if (TmpShade < 0.0f)
TmpShade = 0.0f;
|
第四,对于OpenGL我们象忽略纹理坐标一样忽略这个值。阴影纹理与一个查找表一样来表现(色度值正成为指数),这是(我认为)为什么1D纹理被创造主要原因。对于OpenGL我们之后忽略这个顶点位置,并不断重复,重复。至此我认为你已经抓到了概念。 |
|
glTexCoord1f (TmpShade);
glVertex3fv (&polyData[i].Verts[j].Pos.X);
}
}
glEnd ();
glDisable (GL_TEXTURE_1D);
|
现在我们转移到轮廓之上。一个轮廓能以“它的相邻的边,一边为可见,另一边为不可见”定义。在OpenGL中,这是深度测试被规定小于或等于(GL_LEQUAL)当前值的地方,并且就在那时所有前面的面被精选。我们同样也要混合线条,以使它看起来不错:)
那么,我们使混合可用并规定混合模式。我们告诉OpenGL与着色线条一样着色backfacing多边形,并且规定这些线条的宽度。我们精选所有前面多边形,并规定测试深度小于或等于当前的Z值。在这个线条的的颜色被规定后,我们从头到尾循环每一个多边形,绘制它的顶点。我们仅需忽略顶点位置,而不是法线或着色值因为我们需要的仅仅是轮廓。
| |
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 (i = 0; i < polyNum; i++)
{
for (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);
}
}
|
你现在看到Cel-Shading并非那样难。当然技术可以提高非常多。一个好的例子是游戏XIII
http://www.nvidia.com/object/game_xiii.html,它使你认为你在一个卡通世界里。如果你想在卡通透视技术里达到更深层次,你可以浏览这本书实时透视这一章“Non-Photorealistic
Rendering”。如果你更喜欢在WEB上读论文,在这里可以发现一大堆联接列表:http://www.red3d.com/cwr/npr/
|
版权与使用声明:
我是个对学习和生活充满激情的普通男孩,在网络上我以DancingWind为昵称,我的联系方式是[email protected],如果你有任何问题,都可以联系我。
引子
网络是一个共享的资源,但我在自己的学习生涯中浪费大量的时间去搜索可用的资料,在现实生活中花费了大量的金钱和时间在书店中寻找资料,于是我给自己起了个昵称DancingWind,其意义是想风一样从各个知识的站点中吸取成长的养料。在飘荡了多年之后,我决定把自己收集的资料整理为一个统一的资源库。
版权声明
所有DancingWind发表的内容,大多都来自共享的资源,所以我没有资格把它们据为己有,或声称自己为这些资源作出了一点贡献。故任何人都可以复制,修改,重新发表,甚至以自己的名义发表,我都不会追究,但你在做以上事情的时候必须保证内容的完整性,给后来的人一个完整的教程。最后,任何人不能以这些资料的任何部分,谋取任何形式的报酬。
发展计划
在国外,很多资料都是很多人花费几年的时间慢慢积累起来的。如果任何人有兴趣与别人共享你的知识,我很欢迎你与我联系,但你必须同意我上面的声明。
感谢
感谢我的母亲一直以来对我的支持和在生活上的照顾。
感谢我深爱的女友田芹,一直以来默默的在精神上和生活中对我的支持,她甚至把买衣服的钱都用来给我买书了,她真的是我见过的最好的女孩,希望我能带给她幸福。
源码 RAR格式 |
| <
第36课 |
第38课
> |