Director 偶而上來逛逛的過客
註冊時間: 2013-11-04 文章: 13
381.66 果凍幣
|
發表於: 2013-12-5, AM 7:42 星期四 文章主題: GLSL&GLUT 從環境設定開始的基礎教學(05) - skybox(cubemap的小應用) |
|
|
引言回覆: |
※2015/7/5補充:
其實已經過很久了,GLee似乎已經沒再繼續更新,我在寫了這篇教學不久後也改用了GLEW取代Glee,它們的功能及函式名稱其實是幾乎一樣的,安裝方式同其他函式庫
所以只需要將glee.h及glee.lib改為glew.h及glew.lib就好,這是下載的地方,抓Binaries那個http://glew.sourceforge.net/。
lib用 lib\Release\Win32 裡面的。
|
趁最近還空閒,我盡可能快的把這些紀錄做完
在一個3D遊戲裡面,除非你想做的是夜晚,不然skybox一般來說都是必須的
同樣的場景有沒有加入skybox都會產生截然不同的效果
而延續上一次的程式,這就是我們今天要做的。
事實上,製作skybox並不困難,我是指在實作上,然而它運用的技術並不是那麼好理解的,所以我比較想著重在討論cubemap這種貼圖技術。
那麼就先對cubemap做點前情提要,如有錯誤煩請指證
引言回覆: |
cubemap,其實我沒有找到我覺得比較合適的中文翻譯,請允許我到結束都這樣稱呼它。
所謂cubemap,在我查閱相關資料的過程,大概覺得維基的解釋最為好懂(可參照http://en.wikipedia.org/wiki/Cube_mapping)。
簡單來說,cubemap大家可以想像成你或者一個物體被一個正方體給包住,那麼無論你看向哪個方向,你勢必都會看到這個正方體任何一面,到這裡應該沒什麼問題。
那麼,把你的視線當成是一個向量,我們看的到東西是因為光線反射入我們的眼睛,cubemap需要的就是這個反射進眼睛的向量,它利用這個反射向量來計算要回傳哪一個像素顏色。
cubemap的歷史可以參考維基那篇。
它的實際運用其實有很多,skybox算是最尋常且算簡單的一環。
其他我還知道且用過的應該就是鏡面反射的實作,像是這兩隻猴子。
左邊是折射的實作,右邊則是反射。
反射及折射,現在大多都是利用cubemap來達成的,簡單來說3D遊戲中所謂的反射就是把周圍的場景畫面當作材質,貼在要反射畫面的物體上面。
我們只要根據攝影機的位置以及物體vertex的位置,來計算反射的向量,就能取到該vertex對應的該有的材質顏色,把這些拼湊起來,最後就會形成像是反射那樣的效果。
我自己見過一些遊戲會在建置完後對每一個反射物體進行cubemap的材質取景,以供在遊戲中實作反射時使用,但假如要進行即時的反射,就必須仰賴FBO(frame buffer object)的實作,這個我們會在下一章提到。
那麼,這個章節並沒有要做到反射,只算是使用cubemap的暖身而已吧
前情提要就先到這了!
|
這次的程式碼並不複雜,但具備基本的觀念來做實作是很重要的,所以花了點時間介紹
接下來就正式進入這次的Shader實作。
從這個章節之後,會需要愈來愈多的不同功能的Shader,所以在開頭我們要先把之前第一章的那些置入Shader的功能包裝起來,既然我們寫的是C++,那自然就是包裝成class囉!
其實並沒有甚麼特別的,就只是把之前寫的loadFile、loadShader、initShader放進去而已。
步驟我就不贅述了,算是基礎的C++實作
代碼: | // shader.h
#ifndef SHADER_H
#define SHADER_H
#include "allheader.h"
class shader{
GLuint vs, fs, program;
void loadFile( const char* filename, std::string &string );
GLuint loadShader( std::string &source, GLenum type, const char* filename ) ;
public:
shader( const char* vname, const char* fname );
~shader();
void useShader();
void delShader();
GLuint getProgramID();
};
#endif |
代碼: | // shader.cpp
#include "shader.h"
shader::shader( const char* vname, const char* fname )
{
std::string tmp;
vs = loadShader( tmp, GL_VERTEX_SHADER, vname ); // 編譯shader並且把id傳回vs
tmp = "";
fs = loadShader( tmp, GL_FRAGMENT_SHADER, fname );
program = glCreateProgram(); // 創建一個program
glAttachShader( program, vs ); // 把vertex shader跟program連結上
glAttachShader( program, fs ); // 把fragment shader跟program連結上
glLinkProgram( program ); // 根據被連結上的shader, link出各種processor
}
shader::~shader()
{
glDetachShader( program, vs );
glDetachShader( program, fs );
glDeleteShader( vs );
glDeleteShader( fs );
glDeleteProgram( program );
}
void shader::loadFile( const char* filename, std::string &string )
{
std::ifstream fp(filename);
if( !fp.is_open() ){
std::cout << "Open <" << filename << "> error." << std::endl;
return;
}
char temp[300];
while( !fp.eof() ){
fp.getline( temp, 300 );
string += temp;
string += '\n';
}
fp.close();
}
GLuint shader::loadShader( std::string &source, GLenum type, const char* filename )
{
loadFile( filename, source ); // 把程式碼讀進source
GLuint ShaderID;
ShaderID = glCreateShader( type ); // 告訴OpenGL我們要創的是哪種shader
const char* csource = source.c_str(); // 把std::string結構轉換成const char*
glShaderSource( ShaderID, 1, &csource, NULL ); // 把程式碼放進去剛剛創建的shader object中
glCompileShader( ShaderID ); // 編譯shader
char error[1000] = "";
glGetShaderInfoLog( ShaderID, 1000, NULL, error ); // 這是編譯過程的訊息, 錯誤什麼的把他丟到error裡面
std::cout << "File: <" << filename << "> Complie status: \n" << error << std::endl; // 然後輸出出來
return ShaderID;
}
void shader::useShader()
{
glUseProgram( program );
}
void shader::delShader()
{
// 值得特別提的是這個
// 當我們使用完shader, 要換成別種來進行渲染的時候
// 一般就是直接使用id為0的那個program來做類似初始化的動作
glUseProgram( 0 );
}
GLuint shader::getProgramID()
{
return program;
} |
那麼我們就開始skybox的實作吧!
一樣簡單敘述步驟:
Step1: 讀取材質並設置cubemap
Step2: 建立一個立方體構造
Step3: 依照攝影機位置之於立方體形成的向量來取像素
引言回覆: | Step1
在讀取材質之前,首先你需要一個包含正方體六面的天空材質。
這個網址可以找到漂亮的高清skybox材質,http://www.93i.de/products/media/skybox-texture-set-1。
※我範例用的也是這個網頁取得的材質,如網頁不會下載可抓這個https://www.dropbox.com/s/23xu1z4jk1zyfma/skybox.rar
OK! 那麼有了材質之後就要把它給讀進記憶體裡才能夠使用,於是我們來寫一個loadCubemap的函式。
※範例程式碼會用上載入材質那章使用的ilut,當然利用其它圖檔處理的函式庫修改也是沒問題的
※若要安裝請洽這篇http://www.gamelife.idv.tw/viewtopic.php?t=2772
一般的skybox texture為了方便都會直接把圖檔命名為top、bottom、front、back、right、left這樣,你們可以自由地依自己喜好命名它們。
代碼: | GLuint loadCubemap( std::string* filename, bool isBmp )
{
GLuint cubemapID;
glGenTextures( 1, &cubemapID );
// glBindTexture的第一個參數要改成GL_TEXTURE_CUBE_MAP
glBindTexture( GL_TEXTURE_CUBE_MAP, cubemapID );
for( int i=0 ; i<6 ; i++ ){
unsigned int tmpID;
int w = 0, h = 0;
tmpID = ilutGLLoadImage( (wchar_t*)filename[i].c_str() );
// 這是在ilut中取得圖像資料的方式
ILubyte* tmpData = ilGetData();
w = ilGetInteger( IL_IMAGE_WIDTH );
h = ilGetInteger( IL_IMAGE_HEIGHT );
// 使用ilutGLLoadImage讀取bmp檔後, 若直接拿來送交glTexImage2D設置cubemap
// 會產生RBG變成BGR的問題, 所以要另外寫個函式來處理
if( isBmp )
bitmapBGRtoRGB( tmpData, w, h );
// #define GL_TEXTURE_CUBE_MAP_POSITIVE_X 0x8515
// #define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516
// #define GL_TEXTURE_CUBE_MAP_POSITIVE_Y 0x8517
// #define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518
// #define GL_TEXTURE_CUBE_MAP_POSITIVE_Z 0x8519
// #define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A
// 這是在GLee.h中定義的cubemap參數, 依序各個之間只相差1
// 所以利用這點來簡化修改glTexImage2D第一項參數值的步驟
glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X+i, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, tmpData );
// 基本的材質性質設定
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
}
glBindTexture( GL_TEXTURE_CUBE_MAP, 0 );
return cubemapID;
} |
這個設計方式是把檔名存在6個std::string結構裡,然後傳入做處理,大部分的內容應該都在註解介紹完了。
glTexImage2D設置第一個參數的那個點子是來自我在第1章提到的那篇教程,從裡面偷學的,覺得很好就一直留下了
中間要注意的是bmp的檔案被ilutGLLoadImage讀入後,再用ilGetData取回img資料時,會取得bmp的原檔案資料,即原本的BGR排列的資料,所以要針對bmp檔進行BGR轉RBG的動作,才能夠傳入glTexImage2D做設置。
這是個簡易的轉換函式。
代碼: | void bitmapBGRtoRGB( unsigned char* imgData, int w, int h )
{
unsigned char tmp;
for( int i=0, j=0 ; i < w*h ; i++, j+=3 )
{
tmp = imgData[j];
imgData[j] = imgData[j+2];
imgData[j+2] = tmp;
}
} |
那麼當這些都安置好後,就能夠來取資料夾中的skybox材質了。
代碼: |
std::string filename[6];
bool isBmp = false;
for( int i=0 ; i<6 ; i++ )
filename[i] = "skybox\\";
filename[0] += "right";
filename[1] += "left";
filename[2] += "bottom";
filename[3] += "top";
filename[4] += "front";
filename[5] += "back";
for( int i=0 ; i<6 ; i++ ){
std::string tmp = ".bmp";
filename[i] += tmp;
if( tmp == ".bmp" )
isBmp = true;
}
skyboxTex = loadCubemap( filename, isBmp ); |
|
如此一來,我們就成功載入了一套cubemap的貼圖了!
再來就是建造一個立方體以供我們取向量。
※這裡要強調的是,我們並非要把材質貼到立方體上,而是根據攝影機位置(上一章有提到攝影機位置之於view matrix永遠都在0, 0, 0),計算出面對的地方該取的材質像素。
引言回覆: | Step2
建立一個立方體並不難,用GL_QUADS繪製六個面即可。
然而繪製的大小,依據各位目前用來設定view matrix的方式會有分岐。
若是使用glRotate以及glTranslate(如我放上的camera class),則可以利用一點小技巧,就是把繪製skybox的部分放在glRotate以及glTranslate之間,這樣就永遠不必擔心有一天會衝出skybox,因為glTranslate只會影響到後面繪製的圖形,這樣就畫一個很小的正方形包住就好。
而若是使用gluLookAt的朋友,因為它設置view matrix無論旋轉還是位移都只在這行函式完成,所以繪製skybox的時候就要確認它包裹住你的移動範圍,不然當你的攝影機出了立方體的話就看不見天空了!
那以下是利用camera class繪製skybox的部分
代碼: | // 在camera class中, Control的功能只包含glRotate, 所以放在繪製skybox之前
// UpdateCamera負責glTranslate, 會產生位移, 所以在繪製完skybox之後才呼叫
MainCamera.Control( stateKeyboard, CAM_NO_MOUSE_INPUT );
glPushMatrix();
// 載入圖檔的時候沒注意到全方位都翻轉了
// 可自行重新命名來更動對應的位置
// 我這邊就直接用glRotatef反轉了OAO
glRotatef( 180, 1.0, 0.0, 0.0 );
// 繪製skybox之前要先關閉深度探測
// 因為我們寫的shader是直接把圖畫在鏡頭上(像是2D投影那樣)
// 若是開深度探測則會擋住你之後畫的所有東西
glDisable( GL_DEPTH_TEST );
// 啟用shader
skyboxShader->useShader();
// 與之前載入材質相同, 先active一個材質unit
glActiveTexture( GL_TEXTURE0 );
// 然後再把剛剛載入的skybox texture ID綁在上面
glBindTexture( GL_TEXTURE_CUBE_MAP, skyboxTex );
// 然後送入我們寫的skybox shader, 裡面對應的uniform叫做cubeMap
glUniform1i( glGetUniformLocation( skyboxShader->getProgramID(), "cubeMap" ), 0 );
// 繪製一個立方體, 不必設置glTexCoord
glBegin( GL_QUADS );
glVertex3f( 1.0, 1.0, 1.0 );
glVertex3f( 1.0, -1.0, 1.0 );
glVertex3f( 1.0, -1.0, -1.0 );
glVertex3f( 1.0, 1.0, -1.0 );
glVertex3f( -1.0, 1.0, 1.0 );
glVertex3f( -1.0, 1.0, -1.0 );
glVertex3f( -1.0, -1.0, -1.0 );
glVertex3f( -1.0, -1.0, 1.0 );
glVertex3f( 1.0, 1.0, 1.0 );
glVertex3f( -1.0, 1.0, 1.0 );
glVertex3f( -1.0, -1.0, 1.0 );
glVertex3f( 1.0, -1.0, 1.0 );
glVertex3f( 1.0, -1.0, -1.0 );
glVertex3f( -1.0, -1.0, -1.0 );
glVertex3f( -1.0, 1.0, -1.0 );
glVertex3f( 1.0, 1.0, -1.0 );
glVertex3f( 1.0, 1.0, 1.0 );
glVertex3f( 1.0, 1.0, -1.0 );
glVertex3f( -1.0, 1.0, -1.0 );
glVertex3f( -1.0, 1.0, 1.0 );
glVertex3f( -1.0, -1.0, 1.0 );
glVertex3f( -1.0, -1.0, -1.0 );
glVertex3f( 1.0, -1.0, -1.0 );
glVertex3f( 1.0, -1.0, 1.0 );
glEnd();
skyboxShader->delShader();
glEnable( GL_DEPTH_TEST );
glPopMatrix();
// 結束後清理frame buffer的深度bit
glClear( GL_DEPTH_BUFFER_BIT );
MainCamera.UpdateCamera(); |
若是使用gluLookAt的話,在畫立方體之前設個glScale放大個幾百倍就沒問題了(直接改glVertex的值也可以啦,無論如何要記得保持立方體的形狀就好)。
那接下來在Step3我們就會利用這個正方體來取我們要的材質。
|
這次比較特別,到了最後才講Shader,因為使用OpenGL包裝好的函式,基本上不太複雜。
引言回覆: | Step3
首先我們要先利用被我們包裝好的shader class來建構shader。
代碼: | shader* skyboxShader; |
然後在初始化的函式中引入我們的shader檔
代碼: | skyboxShader = new shader( "skyboxShader.vs", "skyboxShader.frag" ); |
最後記得在我們程式結束時釋放它。
OK! 這就是使用shader class的方式。
那我們來看這次的Shader吧!
代碼: | // skyboxShader.vs
varying vec3 vertixVector;
void main()
{
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
vertixVector = gl_Vertex.xyz;
} |
代碼: | // skyboxShader.frag
varying vec3 vertixVector;
uniform samplerCube cubeMap;
void main()
{
gl_FragColor = textureCube( cubeMap, vertixVector );
} |
事實上這次的並不困難,因為最重要的部分textureCube已經寫好了。
有看前情提要的話,簡單來說textureCube就是把vertixVector作為反射向量,來回推那個vertex該是什麼顏色。
那麼在skyboxShader.vs中我們要怎麼取得那個向量呢?
很簡單,模擬光反射入我們眼睛的向量,就是物體到攝影機的向量,而這個向量相當於物體的點座標減去攝影機的座標,然而,假如各位還記得攝影機的位置永遠在(0, 0, 0)的話,那這行應該就沒什麼問題了。
代碼: | vertixVector = gl_Vertex.xyz; |
那麼這次Shader只有四行,應該這樣就可以了。
|
今次的GLSL利用cubemap實作skybox的章節大概就到這邊。
這學期有五個專題要做,所以進度有點慢,說是紀錄但是已經跟不上我現在正在做的實作了
不過拿來當作回顧也是不錯的
最後留下這一章節的範例程式碼,skybox的材質連結在上面,因為上傳大小問題就不包含了,而再來的code會變多份,所以就不再直接貼上改用論壇附件
那麼下次再見
Happy coding!
描述: |
|
下載 |
檔名: |
ch5.rar |
附件大小: |
105.33 KB |
下載次數: |
共 482 次 |
|
|