OpenGL教程 - 紋理映射 (第六課)

OpenGL教程 - 紋理映射 (第六課)
Jeff Molofee


  原 文:Lesson 6: Texture Mapping
  譯 者:CKER


  學習Texture Map紋理映射(貼圖)有很多好處。比方說您想讓一顆導彈飛過屏幕。根據前幾課的知識,我們最可行的辦法可能是很多個多邊形來構建導彈的輪廓并加上有趣的顏色。使用紋理映射,您可以使用真實的導彈圖像并讓它飛過屏幕。您覺得哪個更好看?照片還是一大堆三角形和四邊形?使用紋理映射的好處還不止是更好看,而且您的程序運行會更快。導彈貼圖可能只是一個飛過窗口的四邊形。一個由多邊形構建而來的導彈卻很可能包括成百上千的多邊形。很顯然,貼圖極大的節省了CPU時間。
  現在我們在第一課的代碼開始處增加五行新代碼。新增的第一行是#include 。它允許我們對文件進行操作,為了在后面的代碼中使用fopen(),我們增加了這一行。然后我們增加了三個新的浮點變量...xrot、 yrot、zrot。這些變量用來使立方體繞X、Y、Z軸旋轉。最后一行GLuint texture[1]為一個紋理分配存儲空間。如果您需要不止一個的紋理,應該將數字1改成您所需要的數字。


  #include                     // Windows的頭文件
  #include                      // 標準輸入/輸出庫的頭文件 (新增)
  #include                      // OpenGL32庫的頭文件
  #include                      // GLu32庫的頭文件
  #include                     // GLaux庫的頭文件


  HGLRC hRC=NULL;                       // 永久著色描述表
  HDC hDC=NULL;                        // 私有GDI設備描述表
  HWND hWnd=NULL;                       // 保存我們的窗口句柄
  HINSTANCE hInstance;                    // 保存程序的實例


  bool keys[256];                       // 用于鍵盤例程的數組
  bool active=TRUE;                      // 窗口的活動標志,缺省為TRUE
  bool fullscreen=TRUE;                    // 全屏標志缺省設定成全屏模式


  GLfloat xrot;                        // X 旋轉量 (新增)
  GLfloat yrot;                        // Y 旋轉量 (新增)
  GLfloat zrot;                        // Z 旋轉量 (新增)


  GLuint texture[1];                     // 存儲一個紋理 (新增)


  LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);    // WndProc的定義


  緊跟上面的代碼在ReSizeGLScene()之前,我們增加了下面這一段代碼。這段代碼用來加載位圖文件。如果文件不存在,返回NULL告知程序無法加載位圖。在我開始解釋這段代碼之前,關于用作紋理的圖像我想有幾點十分重要,并且您必須明白。此圖像的寬和高必須是2的n次方;寬度和高度最小必須是64象素;并且出于兼容性的原因,圖像的寬度和高度不應超過256象素。如果您的原始素材的寬度和高度不是64,128,256象素的話,使用圖像處理軟件重新改變圖像的大小。可以肯定有辦法能繞過這些限制,但現在我們只需要用標準的紋理尺寸。
  首先,我們創建一個文件句柄。句柄是個用來鑒別資源的數值,它使程序能夠訪問此資源。我們開始先將句柄設為NULL 。


  AUX_RGBImageRec *LoadBMP(char *Filename)          // 載入位圖圖象
  {
      FILE *File=NULL;                  // 文件句柄


  接下來檢查文件名是否已提供。因為LoadBMP()可以無參數調用,所以我們不得不檢查一下。您可不想什么都沒載入吧......。


      if (!Filename)                   // 確保文件名已提供。
      {
          return NULL;                 // 如果沒提供,返回 NULL
      }


  接著檢查文件是否存在。下面這一行嘗試打開文件。


      File=fopen(Filename,\\\"r\\\");              // 嘗試打開文件


  如果我們能打開文件的話,很顯然文件是存在的。使用fclose(File) 關閉文件。auxDIBImageLoad(Filename) 讀取圖象數據并將其返回。


      if (File)                      // 文件存在么?
      {
        fclose(File);                  // 關閉句柄
        return auxDIBImageLoad(Filename);        // 載入位圖并返回指針
      }


  如果我們不能打開文件,我們將返回NULL。這意味著文件無法載入。程序在后面將檢查文件是否已載入。如果沒有,我們將退出程序并彈出錯誤消息。


      return NULL;                    // 如果載入失敗,返回 NULL
  }


  下一部分代碼載入位圖(調用上面的代碼)并轉換成紋理。


  int LoadGLTextures()                  // 載入位圖(調用上面的代碼)并轉換成紋理
  {


  然后設置一個叫做Status的變量。我們使用它來跟蹤是否能夠載入位圖以及能否創建紋理。Status缺省設為 FALSE(表示沒有載入或創建任何東東)。


      int Status=FALSE;                  // Status 狀態指示器


  現在我們創建存儲位圖的圖像記錄。次記錄包含位圖的寬度、高度和數據。


      AUX_RGBImageRec *TextureImage[1];          // 創建紋理的存儲空間


  清除圖像記錄,確保其內容為空。


      memset(TextureImage,0,sizeof(void *) *1);      // 將指針設為 NULL


  現在載入位圖,并將其轉換為紋理。TextureImage[0]=LoadBMP(\\\"Data/NeHe.bmp\\\")調用LoadBMP()的代碼。載入 Data目錄下的“NeHe.bmp”位圖文件。如果一切正常,圖像數據將存放在TextureImage[0] 中,Status被設為TRUE,然后我們開始創建紋理。


      // 載入位圖,檢查有無錯誤,如果位圖沒找到則退出。
      if (TextureImage[0]=LoadBMP(\\\"Data/NeHe.bmp\\\"))
      {
          Status=TRUE;                // 將 Status 設為 TRUE


  現在使用中TextureImage[0]的數據創建紋理。第一行glGenTextures(1, &texture[0])告訴OpenGL我們想生成一個紋理名字(如果您想載入多個紋理,加大數字)。值得注意的是,開始我們使用GLuint texture[1]來創建一個紋理的存儲空間,您也許會認為第一個紋理就是存放在&texture[1]中的,但這是錯的。正確的地址應該是 &texture[0]。同樣如果使用GLuint texture[2]的話,第二個紋理存放在texture[1]中。(譯者:學C的,在這里應該沒有障礙,數組就是從零開始的呀。)第二行glBindTexture(GL_TEXTURE_2D, texture[0])告訴OpenGL將紋理名字texture[0]綁定到紋理目標上。2D紋理只有高度(在Y軸上)和寬度(在X軸上)。主函數將紋理名字指派給紋理數據。本例中我們告知OpenGL,&texture[0]處的內存已經可用。我們創建的紋理將存儲在&texture[0]的指向的內存區域。


          glGenTextures(1, &texture[0]);      // 創建紋理
          // 使用來自位圖數據生成的典型紋理
          glBindTexture(GL_TEXTURE_2D, texture[0]);


  下來我們創建真正的紋理。下面一行告訴OpenGL此紋理是一個2D紋理(GL_TEXTURE_2D)。數字零代表圖像的詳細程度,通常就由它為零去了。數字三是數據的成分數。因為圖像是由紅色數據,綠色數據,藍色數據三種組分組成。TextureImage[0]->sizeX是紋理的寬度。如果您知道寬度,您可以在這里填入,但計算機可以很容易的為您指出此值。TextureImage[0]->sizey是紋理的高度。數字零是邊框的值,一般就是零。GL_RGB告訴OpenGL圖像數據由紅、綠、藍三色數據組成。
  GL_UNSIGNED_BYTE意味著組成圖像的數據是無符號字節類型的。最后...TextureImage[0]->data告訴OpenGL紋理數據的來源。此例中指向存放在TextureImage[0]記錄中的數據。


          // 生成紋理
          glTexImage2D(GL_TEXTURE_2D, 0, 3,
              TextureImage[0]->sizeX, TextureImage[0]->sizeY,
              0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);


  下面的兩行告訴OpenGL在顯示圖像時,當它比放大得原始的紋理大(GL_TEXTURE_MAG_FILTER)或縮小得比原始得紋理小(GL_TEXTURE_MIN_FILTER)時OpenGL采用的濾波方式。通常這兩種情況下我都采用GL_LINEAR。這使得紋理從很遠處到離屏幕很近時都平滑顯示。使用GL_LINEAR需要CPU和顯卡做更多的運算。如果您的機器很慢,您也許應該采用GL_NEAREST。過濾的紋理在放大的時候,看起來斑駁的很(譯者:就是馬賽克)。您也可以結合這兩種濾波方式。在近處時使用GL_LINEAR,遠處時GL_NEAREST。


          glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); // 線形濾波
           glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); // 線形濾波
      }


  現在我們釋放前面用來存放位圖數據的內存。我們先查看位圖數據是否存放在處。如果是的話,再查看數據是否已經存儲。如果已經存儲的話,刪了它。接著再釋放TextureImage[0]圖像結構以保證所有的內存都能釋放。


      if (TextureImage[0])                // 紋理是否存在
      {
          if (TextureImage[0]->data)         // 紋理圖像是否存在
          {
              free(TextureImage[0]->data);    // 釋放紋理圖像占用的內存
          }
          free(TextureImage[0]);           // 釋放圖像結構
      }


  最后返回狀態變量。如果一切OK,變量Status的值為TRUE。否則為FALSE。


      return Status;                   // 返回 Status
  }


  我只在 InitGL 中增加很少的幾行代碼。但為了方便您查看增加了哪幾行,我這段代碼全部重貼一遍。if (!LoadGLTextures())這行代碼調用上面講的子例程載入位圖并生成紋理。如果因為任何原因LoadGLTextures()調用失敗,接著的一行返回FALSE。如果一切OK,并且紋理創建好了,我們啟用2D紋理映射。如果您忘記啟用的話,您的對象看起來永遠都是純白色,這一定不是什么好事。


  int InitGL(GLvoid)                     // 此處開始對OpenGL進行所有設置
  {
       if (!LoadGLTextures())               // 調用紋理載入子例程(新增)
      {
          return FALSE;                // 如果未能載入,返回FALSE(新增)
      }


      glEnable(GL_TEXTURE_2D);              // 啟用紋理映射(新增)
      glShadeModel(GL_SMOOTH);              // 啟用陰影平滑
      glClearColor(0.0f, 0.0f, 0.0f, 0.5f);        // 黑色背景
      glClearDepth(1.0f);                 // 設置深度緩存
      glEnable(GL_DEPTH_TEST);              // 啟用深度測試
      glDepthFunc(GL_LEQUAL);               // 所作深度測試的類型
       glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 真正精細的透視修正
      return TRUE;                    // 初始化 OK
  }


  現在我們繪制貼圖(紋理映射)過的立方體。這段代碼被狂注釋了一把,應該很好懂。開始兩行代碼 glClear() 和 glLoadIdentity() 是第一課中就有的代碼。 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 清除屏幕并設為我們在 InitGL() 中選定的顏色,本例中是黑色。深度緩存也被清除。模型觀察矩陣也使用glLoadIdentity()重置。


  int DrawGLScene(GLvoid)                   // 從這里開始進行所有的繪制
  {
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度緩存
      glLoadIdentity();                  // 重置當前的模型觀察矩陣
      glTranslatef(0.0f,0.0f,-5.0f);           // 移入屏幕 5 個單位


  下面三行使立方體繞X、Y、Z軸旋轉。旋轉多少依賴于變量 xrot, yrot 和 zrot 的值。


      glRotatef(xrot,1.0f,0.0f,0.0f);          // 繞X軸旋轉
      glRotatef(yrot,0.0f,1.0f,0.0f);          // 繞Y軸旋轉
      glRotatef(zrot,0.0f,0.0f,1.0f);          // 繞Z軸旋轉


  下一行代碼選擇我們使用的紋理。如果您在您的場景中使用多個紋理,您應該使用來 glBindTexture(GL_TEXTURE_2D, texture[ 所使用紋理對應的數字 ]) 選擇要綁定的紋理。當您想改變紋理時,應該綁定新的紋理。有一點值得指出的是,您不能在glBegin()和glEnd()之間綁定紋理,必須在glBegin()之前或 glEnd()之后綁定。注意我們在后面是如何使用glBindTexture來指定和綁定紋理的。


      glBindTexture(GL_TEXTURE_2D, texture[0]);     // 選擇紋理


  為了將紋理正確的映射到四邊形上,您必須將紋理的右上角映射到四邊形的右上角,紋理的左上角映射到四邊形的左上角,紋理的右下角映射到四邊形的右下角,紋理的左下角映射到四邊形的左下角。如果映射錯誤的話,圖像顯示時可能上下顛倒,側向一邊或者什么都不是。
  glTexCoord2f的第一個參數是X坐標。0.0f是紋理的左側。0.5f是紋理的中點,1.0f是紋理的右側。glTexCoord2f的第二個參數是Y坐標。0.0f是紋理的底部。0.5f是紋理的中點,1.0f是紋理的頂部。
  所以紋理的左上坐標是X:0.0f,Y:1.0f,四邊形的左上頂點是X:-1.0f,Y:1.0f。其余三點依此類推。
  試著玩玩glTexCoord2f的X,Y坐標參數。把1.0f改為0.5f 將只顯示紋理的左半部分,把0.0f改為0.5f將只顯示紋理的右半部分。


      glBegin(GL_QUADS);
          // 前面
          glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f, -1.0f, 1.0f); // 紋理和四邊形的左下
          glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f, -1.0f, 1.0f); // 紋理和四邊形的右下
          glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f, 1.0f);  // 紋理和四邊形的右上
          glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, 1.0f);  // 紋理和四邊形的左上


          // 后面
          glTexCoord2f(1.0f, 0.0f);glVertex3f(-1.0f, -1.0f, -1.0f); // 紋理和四邊形的右下
          glTexCoord2f(1.0f, 1.0f);glVertex3f(-1.0f, 1.0f, -1.0f); // 紋理和四邊形的右上
          glTexCoord2f(0.0f, 1.0f);glVertex3f( 1.0f, 1.0f, -1.0f); // 紋理和四邊形的左上
          glTexCoord2f(0.0f, 0.0f);glVertex3f( 1.0f, -1.0f, -1.0f); // 紋理和四邊形的左下


          // 頂面
          glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, -1.0f); // 紋理和四邊形的左上
          glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f, 1.0f, 1.0f);  // 紋理和四邊形的左下
          glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f, 1.0f, 1.0f);  // 紋理和四邊形的右下
          glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f, -1.0f); // 紋理和四邊形的右上


          // 底面
          glTexCoord2f(1.0f, 1.0f);glVertex3f(-1.0f, -1.0f, -1.0f); // 紋理和四邊形的右上
          glTexCoord2f(0.0f, 1.0f);glVertex3f( 1.0f, -1.0f, -1.0f); // 紋理和四邊形的左上
          glTexCoord2f(0.0f, 0.0f);glVertex3f( 1.0f, -1.0f, 1.0f); // 紋理和四邊形的左下
          glTexCoord2f(1.0f, 0.0f);glVertex3f(-1.0f, -1.0f, 1.0f); // 紋理和四邊形的右下


          // 右面
          glTexCoord2f(1.0f, 0.0f);glVertex3f( 1.0f, -1.0f, -1.0f); // 紋理和四邊形的右下
          glTexCoord2f(1.0f, 1.0f);glVertex3f( 1.0f, 1.0f, -1.0f); // 紋理和四邊形的右上
          glTexCoord2f(0.0f, 1.0f);glVertex3f( 1.0f, 1.0f, 1.0f);  // 紋理和四邊形的左上
          glTexCoord2f(0.0f, 0.0f);glVertex3f( 1.0f, -1.0f, 1.0f); // 紋理和四邊形的左下


          // 左面
          glTexCoord2f(0.0f, 0.0f);glVertex3f(-1.0f, -1.0f, -1.0f); // 紋理和四邊形的左下
           glTexCoord2f(1.0f, 0.0f);glVertex3f(-1.0f, -1.0f, 1.0f); // 紋理和四邊形的右下
          glTexCoord2f(1.0f, 1.0f);glVertex3f(-1.0f, 1.0f, 1.0f);  // 紋理和四邊形的右上
          glTexCoord2f(0.0f, 1.0f);glVertex3f(-1.0f, 1.0f, -1.0f); // 紋理和四邊形的左上
      glEnd();


  現在增加xrot、yrot及zrot的值。嘗試變化每次各變量的改變值來及調節立方體的旋轉速度,或改變+/-號來調節立方體的旋轉方向。


      xrot+=0.3f;                     // X 軸旋轉
      yrot+=0.2f;                     // Y 軸旋轉
      zrot+=0.4f;                     // Z 軸旋轉
      return true;                    // 繼續運行
  }


  現在您應該比較好的理解紋理映射(貼圖)了。您應該掌握了給任意四邊形表面貼上您所喜愛的圖像的技術。一旦您對2D紋理映射的理解感到自信的時候,試試給立方體的六個面貼上不同的紋理。
  當您理解紋理坐標的概念后,紋理映射并不難理解。如果您有什么意見或建議請給我寫信。如果您認為有什么不對或可以改進,請告訴我。

登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP