看到人們?nèi)匀籩-mail我請求在文章中使用我方才在GameDev.net上寫的源代碼,還看到文章的第二版(在那每一個API附帶源碼)不是在中途完成之前連貫的結(jié)束。我已經(jīng)把這篇指南一并出租給了NeHe(這實(shí)際上是寫文章的最初意圖)因此你們所有的OpenGL領(lǐng)袖可以玩轉(zhuǎn)它。對模型的選擇表示抱歉,但是我最近一直在玩Quake 2。
注釋:這篇文章的源代碼可以在這里找到:
http://www.gamedev.net/reference/programming/features/celshading.
這篇指南實(shí)際上并不解釋原理,僅僅解釋代碼。在上面的連接中可以發(fā)現(xiàn)為什么它能工作。現(xiàn)在不斷地大聲抱怨STOP E-MAILING ME REQUESTS FOR SOURCE CODE!!!!
首先,我們需要包含一些額外的頭文件。第一個(math.h)我們可以使用sqrtf (square root)函數(shù),第二個用來訪問文件。
#include <math.h>
#include <stdio.h>
現(xiàn)在我們將定義一些結(jié)構(gòu)體來幫助我們存貯我們的數(shù)據(jù)(保存好幾百浮點(diǎn)數(shù)組)。第一個是tagMATRIX結(jié)構(gòu)體。如果你仔細(xì)地看,你將看到我們正象包含一個十六個浮點(diǎn)數(shù)的1維數(shù)組~一個2維4×4數(shù)族一樣存儲那個矩陣。這下至OpenGL存儲它的矩陣的方式。如果我們使用4x4數(shù)組,這些值將發(fā)生錯誤的順序。
typedef struct tagMATRIX // 保存OpenGL矩陣的結(jié)構(gòu)體
{
float Data[16]; // 由于OpenGL的矩陣的格式我們使用[16
}
MATRIX;
第二是向量的類。 僅存儲X,Y和Z的值
typedef struct tagVECTOR // 存儲一個單精度向量的結(jié)構(gòu)體
{
float X, Y, Z; // 向量的分量
}
VECTOR;
第三,我們持有頂點(diǎn)的結(jié)構(gòu)。每一個頂點(diǎn)僅需要它的法線和位置(沒有紋理的現(xiàn)行縱坐標(biāo))信息。它們必須以這樣的次序被存放,否則當(dāng)它停止裝載文件的事件將發(fā)生嚴(yán)重的錯誤(我發(fā)現(xiàn)艱難的情形:(教我分塊出租我的代碼。)。
typedef struct tagVERTEX // 存放單一頂點(diǎn)的結(jié)構(gòu)
{
VECTOR Nor; // 頂點(diǎn)法線
VECTOR Pos; // 頂點(diǎn)位置
}
VERTEX;
最后是多邊形的結(jié)構(gòu)。我知道這是存儲頂點(diǎn)的愚蠢的方法,要不是它完美工作的簡單的緣故。通常我愿意使用一個頂點(diǎn)數(shù)組,一個多邊形數(shù)組,和包括一個在多邊形中的3個頂點(diǎn)的指數(shù),但這比較容易顯示你想干什么。
typedef struct tagPOLYGON // 存儲單一多邊形的結(jié)構(gòu)
{
VERTEX Verts[3]; // 3個頂點(diǎn)結(jié)構(gòu)數(shù)組
}
POLYGON;
優(yōu)美簡單的材料也在這里了。為每一個變量的一個解釋考慮那個注釋。
bool outlineDraw = true; // 繪制輪廓的標(biāo)記
bool outlineSmooth = false; // Anti-Alias 線段的標(biāo)記
float outlineColor[3] = { 0.0f, 0.0f, 0.0f }; // 線段的顏色
float outlineWidth = 3.0f; // 線段的寬度
VECTOR lightAngle; // 燈光的方向
bool lightRotate = false; // 是否我們旋轉(zhuǎn)燈光的標(biāo)記
float modelAngle = 0.0f; // 模型的Y軸角度
bool modelRotate = false; // 旋轉(zhuǎn)模型的標(biāo)記
POLYGON *polyData = NULL; // 多邊形數(shù)據(jù)
int polyNum = 0; // 多邊形的編號
GLuint shaderTexture[1]; // 存儲紋理ID
這是得到的再簡單不過的模型文件格式。 最初的少量字節(jié)存儲在場景中的多邊形的編號,文件的其余是tagPOLYGON結(jié)構(gòu)體的一個數(shù)組。正因如此,數(shù)據(jù)在沒有任何需要去分類到詳細(xì)的順序的情況下被讀出。
BOOL ReadMesh () // 讀“model.txt” 文件
{
FILE *In = fopen ("Data\\model.txt", "rb"); // 打開文件
if (!In)
return FALSE; // 如果文件沒有打開返回 FALSE
fread (&polyNum, sizeof (int), 1, In); // 讀文件頭,多邊形的個數(shù)
polyData = new POLYGON [polyNum]; // 分配內(nèi)存
fread (&polyData[0], sizeof (POLYGON) * polyNum, 1, In);// 把所有多邊形的數(shù)據(jù)讀入
fclose (In); // 關(guān)閉文件
return TRUE; // 工作完成
}
一些基本的數(shù)學(xué)函數(shù)而已。DotProduct計(jì)算2個向量或平面之間的角,Magnitude函數(shù)計(jì)算向量的長度,Normalize函數(shù)縮放向量到一個單位長度。
inline float DotProduct (VECTOR &V1, VECTOR &V2) //計(jì)算兩個向量之間的角度
{
return V1.X * V2.X + V1.Y * V2.Y + V1.Z * V2.Z;
}
inline float Magnitude (VECTOR &V) // 計(jì)算向量的長度
{
return sqrtf (V.X * V.X + V.Y * V.Y + V.Z * V.Z);
}
void Normalize (VECTOR &V) // 創(chuàng)建一個單位長度的向量
{
float M = Magnitude (V);
if (M != 0.0f) // 確保我們沒有被0隔開
{
V.X /= M;
V.Y /= M;
V.Z /= M;
}
}
這個函數(shù)利用給定的矩陣旋轉(zhuǎn)一個向量。請注意它僅旋轉(zhuǎn)這個向量——與向量的位置相比它算不了什么。它用來當(dāng)旋轉(zhuǎn)法線確保當(dāng)我們在計(jì)算燈光時(shí)它們停留在正確的方向上。
void RotateVector (MATRIX &M, VECTOR &V, VECTOR &D) // 利用提供的矩陣旋轉(zhuǎn)一個向量
{
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);
}
引擎的第一個主要的函數(shù)…… 初始化,按所說的精確地做。我已經(jīng)砍掉了在注釋中不再需要的代碼段。
// 一些GL 初始代碼和用戶初始化從這里開始
BOOL Initialize (GL_Window* window, Keys* keys)
{
這3個變量用來裝載著色文件。在文本文件中為了單一的線段線段包含了空間,雖然shaderData存儲了真實(shí)的著色值。你可能奇怪為什么我們的96個值被32個代替了。好了,我們需要轉(zhuǎn)換greyscale 值為RGB以便OpenGL能使用它們。我們?nèi)匀豢梢砸詆reyscale存儲這些值,但向上負(fù)載紋理時(shí)我們至于R,G和B成分僅僅使用同一值。
char Line[255]; // 255個字符的存儲量
float shaderData[32][3]; // 96個著色值的存儲量
g_window = window; g_keys = keys;
FILE *In = NULL; // 文件指針
當(dāng)繪制線條時(shí),我們想要確保很平滑。初值被關(guān)閉,但是按“2”鍵,它可以被toggled on/off。
glShadeModel (GL_SMOOTH); // 使用色彩陰影平滑
glDisable (GL_LINE_SMOOTH); // 線條平滑初始化不可用
glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 提高計(jì)算精度
glClearColor (0.7f, 0.7f, 0.7f, 0.0f); // 設(shè)置為灰色背景
glClearDepth (1.0f); // 設(shè)置深度緩存值
glEnable (GL_DEPTH_TEST); // 啟用深度測試
glDepthFunc (GL_LESS); // 設(shè)置深度比較函數(shù)
glShadeModel (GL_SMOOTH); // 啟用反走樣
glDisable (GL_LINE_SMOOTH);
glEnable (GL_CULL_FACE); // 啟用剔除多邊形功能
我們使 OpenGL燈光不可用因?yàn)槲覀冏约鹤鏊缘臒艄庥?jì)算。
glDisable (GL_LIGHTING); // 使 OpenGL 燈光不可用
這里是我們裝載陰影文件的地方。它簡單地以32個浮點(diǎn)值A(chǔ)SCII碼存放(為了輕松修改),每一個在separate線上。
In = fopen ("Data\\shader.txt", "r"); // 打開陰影文件
if (In) // 檢查文件是否打開
{
for (i = 0; i < 32; i++) // 循環(huán)32次
{
if (feof (In)) // 檢查文件是否結(jié)束
break;
fgets (Line, 255, In); // 獲得當(dāng)前線條
這里我們轉(zhuǎn)換 greyscale 值為 RGB, 正象上面所描述的。
// 從頭到尾復(fù)制這個值
shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = atof (Line);
}
fclose (In); // 關(guān)閉文件
}
else
return FALSE;
現(xiàn)在我們向上裝載這個紋理。同樣它清楚地規(guī)定,不要使用任何一種過濾在紋理上否則它看起來奇怪,至少可以這樣說。GL_TEXTURE_1D被使用因?yàn)樗侵档囊痪S數(shù)組。
glGenTextures (1, &shaderTexture[0]); // 獲得一個自由的紋理ID
glBindTexture (GL_TEXTURE_1D, shaderTexture[0]); // 綁定這個紋理。 從現(xiàn)在開始它變?yōu)橐痪S
// 使用鄰近點(diǎn)過濾
glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri (GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// 設(shè)置紋理
glTexImage1D (GL_TEXTURE_1D, 0, GL_RGB, 32, 0, GL_RGB , GL_FLOAT, shaderData);
現(xiàn)在調(diào)整燈光方向。我已經(jīng)使得它向下指向Z軸正方向,這意味著它將正面碰撞模型
lightAngle.X = 0.0f;
lightAngle.Y = 0.0f;
lightAngle.Z = 1.0f;
Normalize (lightAngle);
讀取Mesh文件,并返回
return ReadMesh (); // 讀取Mesh文件,并返回
}
與上面的函數(shù)相對應(yīng)…… 卸載,刪除由Initalize 和 ReadMesh 創(chuàng)建的紋理和多邊形數(shù)據(jù)。
void Deinitialize (void)
{
glDeleteTextures (1, &shaderTexture[0]); // 刪除陰影紋理
delete [] polyData; // 刪除多邊形數(shù)據(jù)
}
主要的演示循環(huán)。所有這些用來處理輸入和更新角度。控制如下:
<SPACE> =鎖定旋轉(zhuǎn)
1 = 鎖定輪廓繪制
2 = 鎖定輪廓 anti-aliasing
<UP> =增加線寬
<DOWN> = 減小線寬
void Update (DWORD milliseconds) // 這里執(zhí)行動作更新
{
if (g_keys->keyDown [' '] == TRUE) // 空格是否被按下
{
modelRotate = !modelRotate; // 鎖定模型旋轉(zhuǎn)開/關(guān)
g_keys->keyDown [' '] = FALSE;
}
if (g_keys->keyDown ['1'] == TRUE) // 1是否被按下
{
outlineDraw = !outlineDraw; // 切換是否繪制輪廓線
g_keys->keyDown ['1'] = FALSE;
}
if (g_keys->keyDown ['2'] == TRUE) // 2是否被按下
{
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) // 是否旋轉(zhuǎn)
modelAngle += (float) (milliseconds) / 10.0f; // 更新旋轉(zhuǎn)角度
}
你一直在等待的函數(shù)。Draw 函數(shù)做每一件事情——計(jì)算陰影的值,著色網(wǎng)孔,著色輪廓,等等,這是它作的。
void Draw (void)
{
TmpShade用來存儲當(dāng)前頂點(diǎn)的色度值。所有頂點(diǎn)數(shù)據(jù)同時(shí)被計(jì)算,意味著我們只需使用我們能繼續(xù)使用的單個的變量。
TmpMatrix, TmpVector 和 TmpNormal同樣被用來計(jì)算頂點(diǎn)數(shù)據(jù),TmpMatrix在函數(shù)開始時(shí)被調(diào)整一次并一直保持到Draw函數(shù)被再次調(diào)用。TmpVector 和 TmpNormal則相反,當(dāng)另一個頂點(diǎn)被處理時(shí)改變。
float TmpShade; // 臨時(shí)色度值
MATRIX TmpMatrix; // 臨時(shí) MATRIX 結(jié)構(gòu)體
VECTOR TmpVector, TmpNormal; // 臨時(shí) VECTOR結(jié)構(gòu)體
清除緩沖區(qū)矩陣數(shù)據(jù)
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除緩沖區(qū)
glLoadIdentity (); // 重置矩陣
首先檢查我們是否想擁有平滑的輪廓。如果是,我們就打開anti-alaising 。否則把它關(guān)閉。簡單!
if (outlineSmooth) // 檢查我們是否想要 Anti-Aliased 線條
{
glHint (GL_LINE_SMOOTH_HINT, GL_NICEST); // 啟用它們
glEnable (GL_LINE_SMOOTH);
}
else // 否則不啟用
glDisable (GL_LINE_SMOOTH);
然后我們設(shè)置視口。我們反向移動攝象機(jī)2個單元,之后以一定角度旋轉(zhuǎn)模型。注:由于我們首先移動攝象機(jī),這個模型將在現(xiàn)場旋轉(zhuǎn)。如果我們以另一種方法做,模型將繞攝象機(jī)旋轉(zhuǎn)。
我們之后從OpenGL中取最新創(chuàng)建的矩陣并把它存儲在 TmpMatrix。
glTranslatef (0.0f, 0.0f, -2.0f); // 移入屏幕兩個單位
glRotatef (modelAngle, 0.0f, 1.0f, 0.0f); // 繞Y軸旋轉(zhuǎn)這個模型
glGetFloatv (GL_MODELVIEW_MATRIX, TmpMatrix.Data); // 獲得產(chǎn)生的矩陣
戲法開始了。首先我們啟用一維紋理,然后啟用著色紋理。這被OpenGL用來當(dāng)作一個look-up表格。我們之后調(diào)整模型的顏色(白色)我選擇白色是因?yàn)樗炼雀卟⑶颐栌胺ū绕渌伾谩N医ㄗh你不要使用黑色:)
// 卡通渲染代碼
glEnable (GL_TEXTURE_1D); // 啟用一維紋理
glBindTexture (GL_TEXTURE_1D, shaderTexture[0]); // 鎖定我們的紋理
glColor3f (1.0f, 1.0f, 1.0f); // 調(diào)整模型的顏色
現(xiàn)在我們開始繪制那些三角形。盡管我們看到在數(shù)組中的每一個多邊形,然后旋轉(zhuǎn)它的每一個頂點(diǎn)。第一步是拷貝法線信息到一個臨時(shí)的結(jié)構(gòu)中。因此我們能旋轉(zhuǎn)法線,但仍然保留原來保存的值(沒有精確降級)。
glBegin (GL_TRIANGLES); // 告訴 OpenGL 我們即將繪制三角形
for (i = 0; i < polyNum; i++) // 從頭到尾循環(huán)每一個多邊形
{
for (j = 0; j < 3; j++) // 從頭到尾循環(huán)每一個頂點(diǎn)
{
TmpNormal.X = polyData[i].Verts[j].Nor.X; // 用當(dāng)前頂點(diǎn)的法線值填充TmpNormal結(jié)構(gòu)
TmpNormal.Y = polyData[i].Verts[j].Nor.Y;
TmpNormal.Z = polyData[i].Verts[j].Nor.Z;
第二,我們通過初期從OpenGL中攫取的矩陣來旋轉(zhuǎn)這個法線。我們之后規(guī)格化因此它并不全部變?yōu)槁菪巍?nbsp;
// 通過矩陣旋轉(zhuǎn)
RotateVector (TmpMatrix, TmpNormal, TmpVector);
Normalize (TmpVector); // 規(guī)格化這個新法線
第三,我們獲得那個旋轉(zhuǎn)的法線的點(diǎn)積燈光方向(稱為lightAngle,因?yàn)槲彝藦奈业呐f的light類中改變它)。我們之后約束這個值在0——1的范圍。(從-1到+1)
// 計(jì)算色度值
TmpShade = DotProduct (TmpVector, lightAngle);
if (TmpShade < 0.0f)
TmpShade = 0.0f; // 如果負(fù)值約束這個值到0
第四,對于OpenGL我們象忽略紋理坐標(biāo)一樣忽略這個值。陰影紋理與一個查找表一樣來表現(xiàn)(色度值正成為指數(shù)),這是(我認(rèn)為)為什么1D紋理被創(chuàng)造主要原因。對于OpenGL我們之后忽略這個頂點(diǎn)位置,并不斷重復(fù),重復(fù)。至此我認(rèn)為你已經(jīng)抓到了概念。
glTexCoord1f (TmpShade); // 規(guī)定紋理的縱坐標(biāo)當(dāng)作這個色度值
// 送頂點(diǎn)
glVertex3fv (&polyData[i].Verts[j].Pos.X);
}
}
glEnd (); // 告訴OpenGL 完成繪制
glDisable (GL_TEXTURE_1D); // 1D 紋理不可用
現(xiàn)在我們轉(zhuǎn)移到輪廓之上。一個輪廓能以“它的相鄰的邊,一邊為可見,另一邊為不可見”定義。在OpenGL中,這是深度測試被規(guī)定小于或等于(GL_LEQUAL)當(dāng)前值的地方,并且就在那時(shí)所有前面的面被精選。我們同樣也要混合線條,以使它看起來不錯:)
那么,我們使混合可用并規(guī)定混合模式。我們告訴OpenGL與著色線條一樣著色backfacing多邊形,并且規(guī)定這些線條的寬度。我們精選所有前面多邊形,并規(guī)定測試深度小于或等于當(dāng)前的Z值。在這個線條的的顏色被規(guī)定后,我們從頭到尾循環(huán)每一個多邊形,繪制它的頂點(diǎn)。我們僅需忽略頂點(diǎn)位置,而不是法線或著色值因?yàn)槲覀冃枰膬H僅是輪廓。
// 輪廓代碼
if (outlineDraw) // 檢查看是否我們需要繪制輪廓
{
glEnable (GL_BLEND); // 使混合可用
// 調(diào)整混合模式
glBlendFunc (GL_SRC_ALPHA ,GL_ONE_MINUS_SRC_ALPHA);
glPolygonMode (GL_BACK, GL_LINE); // 繪制輪廓線
glLineWidth (outlineWidth); // 調(diào)整線寬
glCullFace (GL_FRONT); // 剔出前面的多邊形
glDepthFunc (GL_LEQUAL); // 改變深度模式
glColor3fv (&outlineColor[0]); // 規(guī)定輪廓顏色
glBegin (GL_TRIANGLES); // 告訴OpenGL我們想要繪制什么
for (i = 0; i < polyNum; i++) // 從頭到尾循環(huán)每一個多邊形
{
for (j = 0; j < 3; j++) // 從頭到尾循環(huán)每一個頂點(diǎn)
{
// 送頂點(diǎn)
glVertex3fv (&polyData[i].Verts[j].Pos.X);
}
}
glEnd (); // 告訴 OpenGL我們已經(jīng)完成
這樣以后,我們就把它規(guī)定為以前的狀態(tài),然后退出
glDepthFunc (GL_LESS); // 重置深度測試模式
glCullFace (GL_BACK); // 重置剔出背面多邊形
glPolygonMode (GL_BACK, GL_FILL); // 重置背面多邊形繪制方式
glDisable (GL_BLEND); // 混合不可用
}
}
你現(xiàn)在看到Cel-Shading并非那樣難。當(dāng)然技術(shù)可以提高非常多。一個好的例子是游戲XIII http://www.nvidia.com/object/game_xiii.html,它使你認(rèn)為你在一個卡通世界里。如果你想在卡通透視技術(shù)里達(dá)到更深層次,你可以瀏覽這本書實(shí)時(shí)透視這一章“Non-Photorealistic Rendering”。如果你更喜歡在WEB上讀論文,在這里可以發(fā)現(xiàn)一大堆聯(lián)接列表:http://www.red3d.com/cwr/npr/