電腦遊戲製作開發設計論壇 首頁 電腦遊戲製作開發設計論壇
任何可以在PC上跑的遊戲都可以討論,主要以遊戲之製作開發為主軸,希望讓台灣的遊戲人有個討論、交流、教學、經驗傳承的園地
 
 常見問題常見問題   搜尋搜尋   會員列表會員列表   會員群組會員群組   會員註冊會員註冊 
 個人資料個人資料   登入檢查您的私人訊息登入檢查您的私人訊息   登入登入 

Google
GLSL&GLUT 從環境設定開始的基礎教學(05) - skybox(cubemap的小應用)

 
發表新主題   回覆主題    電腦遊戲製作開發設計論壇 首頁 -> 遊戲程式高級班:DirectX、OpenGL及各種圖型函式庫
上一篇主題 :: 下一篇主題  
發表人 內容
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 裡面的。



 趁最近還空閒,我盡可能快的把這些紀錄做完 Very Happy

 在一個3D遊戲裡面,除非你想做的是夜晚,不然skybox一般來說都是必須的 Smile

 同樣的場景有沒有加入skybox都會產生截然不同的效果



 而延續上一次的程式,這就是我們今天要做的。


 事實上,製作skybox並不困難,我是指在實作上,然而它運用的技術並不是那麼好理解的,所以我比較想著重在討論cubemap這種貼圖技術。

 那麼就先對cubemap做點前情提要,如有錯誤煩請指證 Very Happy
引言回覆:

 cubemap,其實我沒有找到我覺得比較合適的中文翻譯,請允許我到結束都這樣稱呼它。

 所謂cubemap,在我查閱相關資料的過程,大概覺得維基的解釋最為好懂(可參照http://en.wikipedia.org/wiki/Cube_mapping)。

 簡單來說,cubemap大家可以想像成你或者一個物體被一個正方體給包住,那麼無論你看向哪個方向,你勢必都會看到這個正方體任何一面,到這裡應該沒什麼問題。

 那麼,把你的視線當成是一個向量,我們看的到東西是因為光線反射入我們的眼睛,cubemap需要的就是這個反射進眼睛的向量,它利用這個反射向量來計算要回傳哪一個像素顏色。

 cubemap的歷史可以參考維基那篇。

 它的實際運用其實有很多,skybox算是最尋常且算簡單的一環。

 其他我還知道且用過的應該就是鏡面反射的實作,像是這兩隻猴子。

 左邊是折射的實作,右邊則是反射。

 反射及折射,現在大多都是利用cubemap來達成的,簡單來說3D遊戲中所謂的反射就是把周圍的場景畫面當作材質,貼在要反射畫面的物體上面。

 我們只要根據攝影機的位置以及物體vertex的位置,來計算反射的向量,就能取到該vertex對應的該有的材質顏色,把這些拼湊起來,最後就會形成像是反射那樣的效果。

 我自己見過一些遊戲會在建置完後對每一個反射物體進行cubemap的材質取景,以供在遊戲中實作反射時使用,但假如要進行即時的反射,就必須仰賴FBO(frame buffer object)的實作,這個我們會在下一章提到。

 那麼,這個章節並沒有要做到反射,只算是使用cubemap的暖身而已吧 Very Happy

 前情提要就先到這了!



 這次的程式碼並不複雜,但具備基本的觀念來做實作是很重要的,所以花了點時間介紹 Very Happy

 接下來就正式進入這次的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章提到的那篇教程,從裡面偷學的,覺得很好就一直留下了 Razz

 中間要注意的是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" );

 最後記得在我們程式結束時釋放它。
代碼:
   delete skyboxShader;

 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的章節大概就到這邊。

 這學期有五個專題要做,所以進度有點慢,說是紀錄但是已經跟不上我現在正在做的實作了 Confused

 不過拿來當作回顧也是不錯的 Very Happy

 最後留下這一章節的範例程式碼,skybox的材質連結在上面,因為上傳大小問題就不包含了,而再來的code會變多份,所以就不再直接貼上改用論壇附件 Smile

 那麼下次再見 Very Happy

 Happy coding!



ch5.rar
 描述:
本章節範例程式碼

下載
 檔名:  ch5.rar
 附件大小:  105.33 KB
 下載次數:  共 187 次

回頂端
檢視會員個人資料 發送私人訊息 發送電子郵件 參觀發表人的個人網站
從之前的文章開始顯示:   
發表新主題   回覆主題    電腦遊戲製作開發設計論壇 首頁 -> 遊戲程式高級班:DirectX、OpenGL及各種圖型函式庫 所有的時間均為 台灣時間 (GMT + 8 小時)
1頁(共1頁)

 
前往:  
無法 在這個版面發表文章
無法 在這個版面回覆文章
無法 在這個版面編輯文章
無法 在這個版面刪除文章
無法 在這個版面進行投票
可以 在這個版面附加檔案
可以 在這個版面下載檔案


Powered by phpBB © 2001, 2005 phpBB Group
正體中文語系由 phpbb-tw 維護製作