歡迎來到另一個激動人心的課程,這課的代碼是Banu Cosmin所寫,當然教程還是我自己寫的。在這課里,我將教你創建真正的反射,基于物理的。
由于它將用到蒙板緩存,所以需要耗費一些資源。當然隨著顯卡和CPU的發展,這些都不是問題了,好了讓我們開始吧!
下面我們設置光源的參數
static GLfloat LightAmb[] = {0.7f, 0.7f, 0.7f, 1.0f}; // 環境光
static GLfloat LightDif[] = {1.0f, 1.0f, 1.0f, 1.0f}; // 漫射光
static GLfloat LightPos[] = {4.0f, 4.0f, 6.0f, 1.0f}; // 燈光的位置
下面用二次幾何體創建一個球,并設置紋理
GLUquadricObj *q; // 使用二次幾何體創建球
GLfloat xrot = 0.0f; // X方向的旋轉角度
GLfloat yrot = 0.0f; // Y方向的旋轉角的
GLfloat xrotspeed = 0.0f; // X方向的旋轉速度
GLfloat yrotspeed = 0.0f; // Y方向的旋轉速度
GLfloat zoom = -7.0f; // 移入屏幕7個單位
GLfloat height = 2.0f; // 球離開地板的高度
GLuint texture[3]; // 使用三個紋理
ReSizeGLScene() 和LoadBMP() 沒有變化
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
AUX_RGBImageRec *LoadBMP(char *Filename)
下面的代碼載入紋理
int LoadGLTextures() // 載入*.bmp文件,并轉化為紋理
{
int Status=FALSE;
AUX_RGBImageRec *TextureImage[3]; // 創建三個圖象
memset(TextureImage,0,sizeof(void *)*3);
if ((TextureImage[0]=LoadBMP("Data/EnvWall.bmp")) && // 載入地板圖像
(TextureImage[1]=LoadBMP("Data/Ball.bmp")) && // 載入球圖像
(TextureImage[2]=LoadBMP("Data/EnvRoll.bmp"))) // 載入強的圖像
{
Status=TRUE;
glGenTextures(3, &texture[0]); // 創建紋理
for (int loop=0; loop<3; loop++) // 循環設置三個紋理參數
{
glBindTexture(GL_TEXTURE_2D, texture[loop]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX, TextureImage[loop]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}
for (loop=0; loop<3; loop++)
{
if (TextureImage[loop])
{
if (TextureImage[loop]->data)
{
free(TextureImage[loop]->data);
}
free(TextureImage[loop]);
}
}
}
return Status; // 成功返回
}
一個新的函數glClearStencil被加入到初始化代碼中,它用來設置清空操作后蒙板緩存中的值。其他的操作保持不變。
int InitGL(GLvoid) // 初始化OpenGL
{
if (!LoadGLTextures()) // 載入紋理
{
return FALSE;
}
glShadeModel(GL_SMOOTH);
glClearColor(0.2f, 0.5f, 1.0f, 1.0f);
glClearDepth(1.0f);
glClearStencil(0); // 設置蒙板值
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glEnable(GL_TEXTURE_2D); // 使用2D紋理
下面的代碼用來啟用光照
glLightfv(GL_LIGHT0, GL_AMBIENT, LightAmb);
glLightfv(GL_LIGHT0, GL_DIFFUSE, LightDif);
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);
glEnable(GL_LIGHT0);
glEnable(GL_LIGHTING);
下面的代碼使用二次幾何體創建一個球體,在前面的教程中都已經詳纖,這里不再重復。
q = gluNewQuadric(); // 創建一個二次幾何體
gluQuadricNormals(q, GL_SMOOTH); // 使用平滑法線
gluQuadricTexture(q, GL_TRUE); // 使用紋理
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); // 設置球紋理映射
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
return TRUE; // 初始化完成,成功返回
}
下面的代碼繪制我們的球
void DrawObject() // 繪制我們的球
{
glColor3f(1.0f, 1.0f, 1.0f); // 設置為白色
glBindTexture(GL_TEXTURE_2D, texture[1]); // 設置為球的紋理
gluSphere(q, 0.35f, 32, 16); // 繪制球
繪制完一個白色的球后,我們使用環境貼圖來繪制另一個球,把這兩個球按alpha混合起來。
glBindTexture(GL_TEXTURE_2D, texture[2]); // 設置為環境紋理
glColor4f(1.0f, 1.0f, 1.0f, 0.4f); // 使用alpha為40%的白色
glEnable(GL_BLEND); // 啟用混合
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // 把原顏色的40%與目標顏色混合
glEnable(GL_TEXTURE_GEN_S); // 使用球映射
glEnable(GL_TEXTURE_GEN_T);
gluSphere(q, 0.35f, 32, 16); // 繪制球體,并混合
glDisable(GL_TEXTURE_GEN_S); // 讓OpenGL回到默認的屬性
glDisable(GL_TEXTURE_GEN_T);
glDisable(GL_BLEND);
}
繪制地板
void DrawFloor()
{
glBindTexture(GL_TEXTURE_2D, texture[0]); // 選擇地板紋理,地板由一個長方形組成
glBegin(GL_QUADS);
glNormal3f(0.0, 1.0, 0.0);
glTexCoord2f(0.0f, 1.0f); // 左下
glVertex3f(-2.0, 0.0, 2.0);
glTexCoord2f(0.0f, 0.0f); // 左上
glVertex3f(-2.0, 0.0,-2.0);
glTexCoord2f(1.0f, 0.0f); // 右上
glVertex3f( 2.0, 0.0,-2.0);
glTexCoord2f(1.0f, 1.0f); // 右下
glVertex3f( 2.0, 0.0, 2.0);
glEnd();
}
現在到了我們繪制函數的地方,我們將把所有的模型結合起來創建一個反射的場景。
向往常一樣先把各個緩存清空,接著定義我們的剪切平面,它用來剪切我們的圖像。這個平面的方程為equ[]={0,-1,0,0},向你所看到的它的法線是指向-y軸的,這告訴我們你只能看到y軸坐標小于0的像素,如果你啟用剪切功能的話。
關于剪切平面,我們在后面會做更多的討論。繼續吧:)
int DrawGLScene(GLvoid)
{
// 清除緩存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// 設置剪切平面
double eqr[] = {0.0f,-1.0f, 0.0f, 0.0f};
下面我們把地面向下平移0.6個單位,因為我們的眼睛在y=0的平面,如果不平移的話,那么看上去平面就會變為一條線,為了看起來更真實,我們平移了它。
glLoadIdentity();
glTranslatef(0.0f, -0.6f, zoom); // 平移和縮放地面
下面我們設置了顏色掩碼,在默認情況下所有的顏色都可以寫入,即在函數glColorMask中,所有的參數都被設為GL_TRUE,如果設為零表示這部分顏色不可寫入。現在我們不希望在屏幕上繪制任何東西,所以把參數設為0。
glColorMask(0,0,0,0);
下面來設置蒙板緩存和蒙板測試。
首先我們啟用蒙板測試,這樣就可以修改蒙板緩存中的值。
下面我們來解釋蒙板測試函數的含義:
當你使用glEnable(GL_STENCIL_TEST)啟用蒙板測試之后,蒙板函數用于確定一個顏色片段是應該丟棄還是保留(被繪制)。蒙板緩存區中的值與參考值ref進行比較,比較標準是func所指定的比較函數。參考值和蒙板緩存區的值都可以與掩碼進行為AND操作。蒙板測試的結果還導致蒙板緩存區根據glStencilOp函數所指定的行為進行修改。
func的參數值如下:
常量 含義
GL_NEVER 從不通過蒙板測試
GL_ALWAYS 總是通過蒙板測試
GL_LESS 只有參考值<(蒙板緩存區的值&mask)時才通過
GL_LEQUAL 只有參考值<=(蒙板緩存區的值&mask)時才通過
GL_EQUAL 只有參考值=(蒙板緩存區的值&mask)時才通過
GL_GEQUAL 只有參考值>=(蒙板緩存區的值&mask)時才通過
GL_GREATER 只有參考值>(蒙板緩存區的值&mask)時才通過
GL_NOTEQUAL 只有參考值!=(蒙板緩存區的值&mask)時才通過
接下來我們解釋glStencilOp函數,它用來根據比較結果修改蒙板緩存區中的值,它的函數原形為:
void glStencilOp(GLenum sfail, GLenum zfail, GLenum zpass),各個參數的含義如下:
sfail
當蒙板測試失敗時所執行的操作
zfail
當蒙板測試通過,深度測試失敗時所執行的操作
zpass
當蒙板測試通過,深度測試通過時所執行的操作
具體的操作包括以下幾種
常量 描述
GL_KEEP 保持當前的蒙板緩存區值
GL_ZERO 把當前的蒙板緩存區值設為0
GL_REPLACE 用glStencilFunc函數所指定的參考值替換蒙板參數值
GL_INCR 增加當前的蒙板緩存區值,但限制在允許的范圍內
GL_DECR 減少當前的蒙板緩存區值,但限制在允許的范圍內
GL_INVERT 將當前的蒙板緩存區值進行逐位的翻轉
當完成了以上操作后我們繪制一個地面,當然現在你什么也看不到,它只是把覆蓋地面的蒙板緩存區中的相應位置設為1。
glEnable(GL_STENCIL_TEST); // 啟用蒙板緩存
glStencilFunc(GL_ALWAYS, 1, 1); // 設置蒙板測試總是通過,參考值設為1,掩碼值也設為1
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // 設置當深度測試不通過時,保留蒙板中的值不變。如果通過則使用參考值替換蒙板值
glDisable(GL_DEPTH_TEST); // 禁用深度測試
DrawFloor(); // 繪制地面
我們現在已經在蒙板緩存區中建立了地面的蒙板了,這是繪制影子的關鍵,如果想知道為什么,接著向后看吧:)
下面我們啟用深度測試和繪制顏色,并相應設置蒙板測試和函數的值,這種設置可以使我們在屏幕上繪制而不改變蒙板緩存區的值。
glEnable(GL_DEPTH_TEST); //啟用深度測試
glColorMask(1,1,1,1); // 可以繪制顏色
glStencilFunc(GL_EQUAL, 1, 1); //下面的設置指定當我們繪制時,不改變蒙板緩存區的值
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
下面的代碼設置并啟用剪切平面,使得只能在地面的下方繪制
glEnable(GL_CLIP_PLANE0); // 使用剪切平面
glClipPlane(GL_CLIP_PLANE0, eqr); // 設置剪切平面為地面,并設置它的法線為向下
glPushMatrix(); // 保存當前的矩陣
glScalef(1.0f, -1.0f, 1.0f); // 沿Y軸反轉
由于上面已經啟用了蒙板緩存,則你只能在蒙板中值為1的地方繪制,反射的實質就是在反射屏幕的對應位置在繪制一個物體,并把它放置在反射平面中。下面的代碼完成這個功能
glLightfv(GL_LIGHT0, GL_POSITION, LightPos); // 設置燈光0
glTranslatef(0.0f, height, 0.0f);
glRotatef(xrot, 1.0f, 0.0f, 0.0f);
glRotatef(yrot, 0.0f, 1.0f, 0.0f);
DrawObject(); // 繪制反射的球
glPopMatrix(); // 彈出保存的矩陣
glDisable(GL_CLIP_PLANE0); // 禁用剪切平面
glDisable(GL_STENCIL_TEST); // 關閉蒙板
下面的代碼繪制地面,并把地面顏色和反射的球顏色混合,使其看起來像反射的效果。
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);
glEnable(GL_BLEND); // 啟用混合
glDisable(GL_LIGHTING); // 關閉光照
glColor4f(1.0f, 1.0f, 1.0f, 0.8f); // 設置顏色為白色
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // 設置混合系數
DrawFloor(); // 繪制地面
下面的代碼在距地面高為height的地方繪制一個真正的球
glEnable(GL_LIGHTING); // 使用光照
glDisable(GL_BLEND); // 禁用混合
glTranslatef(0.0f, height, 0.0f); // 移動高位height的位置
glRotatef(xrot, 1.0f, 0.0f, 0.0f); // 設置球旋轉的角度
glRotatef(yrot, 0.0f, 1.0f, 0.0f);
DrawObject(); // 繪制球
下面的代碼用來處理鍵盤控制等常規操作
xrot += xrotspeed; // 更新X軸旋轉速度
yrot += yrotspeed; // 更新Y軸旋轉速度
glFlush(); // 強制OpenGL執行所有命令
return TRUE; // 成功返回
}
下面的代碼處理鍵盤控制,上下左右控制球的旋轉。PageUp/Pagedown控制球的上下。A,Z控制球離你的遠近。
void ProcessKeyboard()
{
if (keys[VK_RIGHT]) yrotspeed += 0.08f;
if (keys[VK_LEFT]) yrotspeed -= 0.08f;
if (keys[VK_DOWN]) xrotspeed += 0.08f;
if (keys[VK_UP]) xrotspeed -= 0.08f;
if (keys['A']) zoom +=0.05f;
if (keys['Z']) zoom -=0.05f;
if (keys[VK_PRIOR]) height +=0.03f;
if (keys[VK_NEXT]) height -=0.03f;
}
KillGLWindow() 函數沒有任何改變
GLvoid KillGLWindow(GLvoid)
CreateGLWindow()函數基本沒有改變,只是添加了以行啟用蒙板緩存
static PIXELFORMATDESCRIPTOR pfd=
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW |
PFD_SUPPORT_OPENGL |
PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
bits,
0, 0, 0, 0, 0, 0,
0,
0,
0,
0, 0, 0, 0,
16,
下面就是在這個函數中唯一改變的地方,記得把0變為1,它啟用蒙板緩存。
1, // 使用蒙板緩存
0,
PFD_MAIN_PLANE,
0,
0, 0, 0
};
WinMain()函數基本沒有變化,只是加上以行鍵盤控制的處理函數
ProcessKeyboard(); // 處理按鍵相應
我真的希望你能喜歡這個教程,我清楚地知道我想做的每一件事,以及如何一步一步實現我心中想創建的效果。但把它表達出來又是另一回事,當你坐下來并實際的去向那些從來沒聽到過蒙板緩存的人解釋這一切時,你就會清楚了。好了,如果你有什么不清楚的,或者有更好的建議,請讓我知道,我想些最好的教程,你的反饋很重要!