從零開始openGL——三、模型載入及滑鼠互動實現
前言
在上篇文章中,介紹了基本圖形的繪製。這篇部落格中將介紹模型的載入、繪製以及滑鼠互動的實現。
模型載入
模型儲存
要實現模型的讀取、繪製,我們首先需要知道模型是如何儲存在檔案中的。
通常模型是由網格組成的,且一般為三角網格。原因為:
- 其它多邊形網格可以容易地剖分為三角形
- 三點共面:保證平面性
- 可以容易地定義內外方向,進行插值等操作
可採用地資料結構包括:
- 面列表
- 儲存面中頂點的三元組(v1, v2, v3)
- 優點:方便而緊湊,可表達非流行網格
- 缺點:不能有效地支援點、面之間的鄰接關係查詢
- 鄰接矩陣
- 優點:支援頂點之間的鄰接資訊(VV)的高效查詢、支援非流行網格
- 缺點:沒有邊的顯示錶達、不支援VF(vertex to face),VE(vertex to edge),EV(edge to vertex),FE(face to edge),EF(edge to face)的快速查詢
- 半邊結構等
- 紀律所有的面、邊和頂點,包括幾何資訊、拓撲資訊、附屬屬性,流行於大部分集合建模應用
- 優點:所有查詢操作時間複雜度均為o(1),所有編輯操作時間複雜度均為o(1)
- 缺點:只能表達流行網格
- 常用半邊結構實現:CGAL(http://www.cgal.org/),Open Mesh(http://www.openmesh.org/)
在這裡,我使用的是面列表。
先定義標頭檔案
#ifndef OBJ_CLASS #define OBJ_CLASS #include <vector> #include <cmath> struct Vector3; Vector3 operator + (const Vector3& one, const Vector3& two); Vector3 operator - (const Vector3& one, const Vector3& two); Vector3 operator * (const Vector3& one, double scale); Vector3 operator / (const Vector3& one, double scale); Vector3 Cross(Vector3& one, Vector3& two); struct Vector3 { double fX; double fY; double fZ; Vector3(double x = 0.0, double y = 0.0, double z = 0.0) : fX(x), fY(y), fZ(z) {} Vector3 operator +=(const Vector3& v) { return *this = *this + v; } double Length() { return sqrt(fX * fX + fY * fY + fZ * fZ); } void Normalize()//歸一化 { double fLen = Length(); if (fLen == 0.0f) fLen = 1.0f; if (fabs(fLen) > 1e-6) { fX /= fLen; fY /= fLen; fZ /= fLen; } } }; struct Point { Vector3 pos; Vector3 normal; }; struct Face { int pts[3]; Vector3 normal; }; class CObj { public: CObj(void); ~CObj(void); std::vector<Point> m_pts; //頂點 std::vector<Face> m_faces;//面 public: bool ReadObjFile(const char* pcszFileName);//讀入模型檔案 private: void UnifyModel();//單位化模型 void ComputeFaceNormal(Face& f);//計算面的法線 }; #endif
然後是一些簡單的運算子過載以及向量計算
#include "Obj.h" #include <iostream> #include <sstream> #include <algorithm> using std::min; using std::max; Vector3 operator + (const Vector3& one, const Vector3& two) //兩個向量相加 { return Vector3(one.fX + two.fX, one.fY + two.fY, one.fZ + two.fZ); } Vector3 operator - (const Vector3& one, const Vector3& two) //兩個向量相減 { return Vector3(one.fX - two.fX, one.fY - two.fY, one.fZ - two.fZ); } Vector3 operator * (const Vector3& one, double scale) //向量與數的乘操作 { return Vector3(one.fX * scale, one.fY * scale, one.fZ * scale); } Vector3 operator / (const Vector3& one, double scale) //向量與數的除操作 { return one * (1.0 / scale); } Vector3 Cross(Vector3& one, Vector3& two) {//計算兩個向量的叉積 Vector3 vCross; vCross.fX = ((one.fY * two.fZ) - (one.fZ * two.fY)); vCross.fY = ((one.fZ * two.fX) - (one.fX * two.fZ)); vCross.fZ = ((one.fX * two.fY) - (one.fY * two.fX)); return vCross; } CObj::CObj(void) { } CObj::~CObj(void) { }
下面來講講模型的讀取等操作
模型讀取
一般在模型儲存檔案中會有這麼幾個識別符號:
- v 表示頂點位置
- vt 表示頂點紋理座標
- vn 表示頂點法向量
- f 表示一個面
開啟一看,大概是這樣的
那麼,就可以開始考慮如何讀取並將資料儲存到列表裡面了,讀檔案還是簡單的,fopen(), fgets(), feof(),剩下關鍵便是將字串轉成數字,c++中還是有現成的函式可以呼叫的,sstream標頭檔案中的istringstream。
bool CObj::ReadObjFile(const char* pcszFileName) {//讀取模型檔案 FILE* fpFile = fopen(pcszFileName, "r"); //以只讀方式開啟檔案 if (fpFile == NULL) { return false; } m_pts.clear(); m_faces.clear(); //TODO:將模型檔案中的點和麵資料分別存入m_pts和m_faces中 char strLine[1024]; Point point; Face face; std::string s1; while (!feof(fpFile)) { fgets(strLine, 1024, fpFile); if (strLine[0] == 'v') { if (strLine[1] == 'n') {//vn 我使用的檔案中沒有vn的資料,就沒有實現 } else {//v 點 std::istringstream sin(strLine); sin >> s1 >> point.pos.fX >> point.pos.fY >> point.pos.fZ; m_pts.push_back(point); } } else if (strLine[0] == 'f') {// 面 std::istringstream sin(strLine); sin >> s1 >> face.pts[0] >> face.pts[1] >> face.pts[2]; ComputeFaceNormal(face); m_faces.push_back(face); } printf("%s\n", strLine); } fclose(fpFile); UnifyModel(); //將模型歸一化 return true; }
通過上一篇文章繪製圓環和圓柱,知道了法向量是十分重要的,因此計算每個面的法向量也是不可少的
原理很簡單,叉乘即可
void CObj::ComputeFaceNormal(Face& f) {//TODO:計算面f的法向量,並儲存 f.normal = Cross(m_pts[f.pts[1]-1].pos - m_pts[f.pts[0]-1].pos, m_pts[f.pts[2]-1].pos - m_pts[f.pts[1]-1].pos); f.normal.Normalize(); }
對於模型歸一化,為何要歸一化呢?想象一下,你拿手機拍照,如果拍照物件離攝像頭很近,那在手機中展示出來的影象會是什麼樣?但是如果能不在移動相機和物件之間的距離的情況下該怎麼做?把物件等比壓縮!
void CObj::UnifyModel() {//為統一顯示不同尺寸的模型,將模型歸一化,將模型尺寸縮放到0.0-1.0之間 //原理:找出模型的邊界最大和最小值,進而找出模型的中心 //以模型的中心點為基準對模型頂點進行縮放 //TODO:新增模型歸一化程式碼 Vector3 vec_max, vec_min(1e5, 1e5, 1e5), vec; for (int i = 0; i < m_pts.size(); i++) { vec_max.fX = std::max(vec_max.fX, m_pts[i].pos.fX); vec_max.fY = std::max(vec_max.fY, m_pts[i].pos.fY); vec_max.fZ = std::max(vec_max.fZ, m_pts[i].pos.fZ); vec_min.fX = std::min(vec_min.fX, m_pts[i].pos.fX); vec_min.fY = std::min(vec_min.fY, m_pts[i].pos.fY); vec_min.fZ = std::min(vec_min.fZ, m_pts[i].pos.fZ); } vec.fX = vec_max.fX - vec_min.fX; vec.fY = vec_max.fY - vec_min.fY; vec.fZ = vec_max.fZ - vec_min.fZ; for (int i = 0; i < m_pts.size(); i++) { m_pts[i].normal = m_pts[i].pos; m_pts[i].normal.fX = (m_pts[i].normal.fX - vec_min.fX) / vec.fX - 0.5f; m_pts[i].normal.fY = (m_pts[i].normal.fY - vec_min.fY) / vec.fY - 0.5f; m_pts[i].normal.fZ = (m_pts[i].normal.fZ - vec_min.fZ) / vec.fZ - 0.5f; } //m_pts.push_back(vec); }
模型繪製
對於模型的繪製,實現起來十分容易,因為有了各個面片的資訊了。
void DrawModel(CObj &model) {//TODO: 繪製模型 for (int i = 0; i < model.m_faces.size(); i++) { glBegin(GL_TRIANGLES); glNormal3f(model.m_faces[i].normal.fX, model.m_faces[i].normal.fY, model.m_faces[i].normal.fZ); glVertex3f(model.m_pts[model.m_faces[i].pts[0] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[0] - 1].normal.fY, model.m_pts[model.m_faces[i].pts[0] - 1].normal.fZ); glVertex3f(model.m_pts[model.m_faces[i].pts[1] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[1] - 1].normal.fY, model.m_pts[model.m_faces[i].pts[1] - 1].normal.fZ); glVertex3f(model.m_pts[model.m_faces[i].pts[2] - 1].normal.fX, model.m_pts[model.m_faces[i].pts[2] - 1].normal.fY, model.m_pts[model.m_faces[i].pts[2] - 1].normal.fZ); glEnd(); } } if (g_draw_content == SHAPE_MODEL) {//繪製模型 glTranslatef(g_x_offset, g_y_offset, g_z_offset); glRotatef(g_rquad_x, 0.0f, 1.0f, 0.0f); glRotatef(g_rquad_y, 1.0f, 0.0f, 0.0f); glScalef(g_scale_size, g_scale_size, g_scale_size); DrawModel(g_obj); }
執行,載入模型!
嗯,好的,它成功出來了。
等等!為啥是頭對著我的,我怎麼調整角度?看起來有點小,我能不能把它放大點?
下面,將介紹滑鼠互動的實現。
滑鼠互動
opengl中的滑鼠互動還是比較好做的,首先需要的是在初始化的時候註冊滑鼠輸出實現回撥函式和滑鼠移動事件的回撥函式。這些在上篇文章中給的框架程式碼裡都實現了。那剩下的就是如何實現旋轉、縮放和拖動了
旋轉
首先我們要注意的是,在給出的程式碼框架裡,攝像機的lookat是這樣的
gluLookAt(0.0, 0.0, 8.0, 0, 0, 0, 0, 1.0, 0);
該函式定義一個檢視矩陣,並與當前矩陣相乘.
第一組eyex, eyey,eyez 相機在世界座標的位置;第二組centerx,centery,centerz 相機鏡頭對準的物體在世界座標的位置;第三組upx,upy,upz 相機向上的方向在世界座標中的方向。
所以,這裡攝像機是從z軸看下去的,那麼初始看到的二維平面分為為x軸和y軸。理解了這個,旋轉就很簡單了。水平拖動的時候讓模型繞y軸轉,豎直拖動的時候讓模型繞x軸轉。按下左鍵旋轉。
if (g_xform_mode == TRANSFORM_ROTATE) //旋轉 {//TODO:新增滑鼠移動控制模型旋轉引數的程式碼 g_rquad_x += (x - g_press_x) * 0.5f; g_rquad_y += (y - g_press_y) * 0.5f; g_press_x = x; g_press_y = y; }
平移
平移的實現十分簡單,計算滑鼠移動的距離即可,按下右鍵拖動
else if(g_xform_mode == TRANSFORM_TRANSLATE) //平移 {//TODO:新增滑鼠移動控制模型平移引數的程式碼 g_x_offset += (x - g_press_x) * 0.002f; g_y_offset += -(y - g_press_y) * 0.002f; g_press_x = x; g_press_y = y; }
縮放
縮放與平移相似,按下滾輪鍵滑動滑鼠
else if(g_xform_mode == TRANSFORM_SCALE) //縮放 {//TODO:新增滑鼠移動控制模型縮放參數的程式碼 g_scale_size += (x - g_press_x) * 0.01f; }
至此,我們的滑鼠互動也實現完了,下面就來試試效果
小節
這樣,模型的載入及滑鼠互動也就介紹完了,但是是不是還缺些什麼?好像這個模型跟想象當中的還是有很大區別的,表面的圖案呢??下一篇將介紹紋理貼圖和曲線繪