c# winform用sharpGL(OpenGl)解析讀取3D模型obj
- 2019 年 11 月 2 日
- 筆記
原文作者:aircraft
原文鏈接:https://www.cnblogs.com/DOMLX/p/11783026.html
自己寫了個簡單的類讀取解析obj模型,使用導入類,然後new個對象,在讀取obj模型,然後調用顯示列表顯示就可以了。至於其他什麼旋轉移動的你們自己加起來應該很容易的,因為我有看過c#下別人寫的obj模型解析的代碼項目,加了很多東西,我都找不到自己要用的代碼在哪裡,而我只需要讀取解析obj模型這塊代碼而已,氣的我自己寫了個類自己解析,所以我怕我代碼寫多了,
你們反而看起來不好理解hhhhhh
在c++下用OpenGL解析的話可以看我其他博客
運行環境:vs2017,需要配置的庫為:sharpGL
一.讀取3D模型
在3d圖形處理中,一個模型(model)通常由一個或者多個Mesh(網格)組成,一個Mesh是可繪製的獨立實體。例如複雜的人物模型,可以分別劃分為頭部,四肢,服飾,武器等各個部分來建模,這些Mesh組合在一起最終形成人物模型。
Mesh由頂點、邊、面Faces組成的,它包含繪製所需的數據,例如頂點位置、紋理坐標、法向量,材質屬性等內容,它是OpenGL用來繪製的最小實體。Mesh的概念示意如下圖所示(來自:What is a mesh in OpenGL?):
Mesh
Mesh可以包含多個Face,一個Face是Mesh中一個可繪製的基本圖元,例如三角形,多邊形,點。要想模型更加逼真,一般需要增加更多圖元使Mesh更加精細,當然這也會受到硬件處理能力的限制,例如PC遊戲的處理能力要強於移動設備。由於多邊形都可以劃分為三角形,而三角形是圖形處理器中都支持的基本圖元,因此使用得較多的就是三角形網格來建模。例如下面的圖(來自:What is a mesh in OpenGL?)表達了使用越來越複雜的Mesh建模一隻兔子的過程:
Mesh2
隨着增加三角形個數,兔子模型變得越來越真實。
讀取3d模型有很多種方法,但是最常用的無非就是調用別人寫好的庫,比如(openmesh),其次呢就是自己讀取解析3d模型文件裏面的一個個坐標數據,什麼v vf vn之類的。
那麼現在就是講一下第二種方法,就是直接解析讀取3d模型文件,提取裏面我們所需的數據。下面是一個obj的模型文件,我們可以使用記事本打開看看裏面是什麼:
# Blender3D v249 OBJ File: untitled.blend # www.blender3d.org mtllib cube.mtl v 1.000000 -1.000000 -1.000000 v 1.000000 -1.000000 1.000000 v -1.000000 -1.000000 1.000000 v -1.000000 -1.000000 -1.000000 v 1.000000 1.000000 -1.000000 v 0.999999 1.000000 1.000001 v -1.000000 1.000000 1.000000 v -1.000000 1.000000 -1.000000 vt 0.748573 0.750412 vt 0.749279 0.501284 vt 0.999110 0.501077 vt 0.999455 0.750380 vt 0.250471 0.500702 vt 0.249682 0.749677 vt 0.001085 0.750380 vt 0.001517 0.499994 vt 0.499422 0.500239 vt 0.500149 0.750166 vt 0.748355 0.998230 vt 0.500193 0.998728 vt 0.498993 0.250415 vt 0.748953 0.250920 vn 0.000000 0.000000 -1.000000 vn -1.000000 -0.000000 -0.000000 vn -0.000000 -0.000000 1.000000 vn -0.000001 0.000000 1.000000 vn 1.000000 -0.000000 0.000000 vn 1.000000 0.000000 0.000001 vn 0.000000 1.000000 -0.000000 vn -0.000000 -1.000000 0.000000 usemtl Material_ray.png s off f 5/1/1 1/2/1 4/3/1 f 5/1/1 4/3/1 8/4/1 f 3/5/2 7/6/2 8/7/2 f 3/5/2 8/7/2 4/8/2 f 2/9/3 6/10/3 3/5/3 f 6/10/4 7/6/4 3/5/4 f 1/2/5 5/1/5 2/9/5 f 5/1/6 6/10/6 2/9/6 f 5/1/7 8/11/7 6/10/7 f 8/11/7 7/12/7 6/10/7 f 1/2/8 2/9/8 3/13/8 f 1/2/8 3/13/8 4/14/8
對這個文本格式做一個簡要說明:
- 以#開始的行為注釋行
- usemtl和mtllib表示的材質相關數據,解析材質數據稍微繁瑣,本節我們只是為了說明加載模型的原理,不做討論。
- o 引入一個新的object
- v 表示頂點位置
- vt 表示頂點紋理坐標
- vn 表示頂點法向量
- f 表示一個面,面使用1/2/8這樣格式,表示頂點位置/紋理坐標/法向量的索引,這裡索引的是前面用v,vt,vn定義的數據 注意這裡Obj的索引是從1開始的,而不是0
那麼我們只要拿到這些數據,按照opengl的繪製的規則,不就可以把他們都繪製出來了嗎?
讀取數據obj的類myReadobj.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Collections; using SharpGL; namespace WindowsFormsApp5 { class myReadObj { public myReadObj() { } public class POINT3 { public double X; public double Y; public double Z; }; public class WenLi { public double TU; public double TV; }; public class FaXiangLiang { public double NX; public double NY; public double NZ; }; public class Mian { public int[] V = new int[3]; public int[] T = new int[3]; public int[] N = new int[3]; }; public class Model { public List<POINT3> V = new List<POINT3>();//V:代表頂點。格式為V X Y Z,V後面的X Y Z表示三個頂點坐標。浮點型 public List<WenLi> VT = new List<WenLi>();//表示紋理坐標。格式為VT TU TV。浮點型 public List<FaXiangLiang> VN = new List<FaXiangLiang>();//VN:法向量。每個三角形的三個頂點都要指定一個法向量。格式為VN NX NY NZ。浮點型 public List<Mian> F = new List<Mian>();//F:面。面後面跟着的整型值分別是屬於這個面的頂點、紋理坐標、法向量的索引。 //面的格式為:f Vertex1/Texture1/Normal1 Vertex2/Texture2/Normal2 Vertex3/Texture3/Normal3 } public Model mesh = new Model(); public float movX; public float movY; public float movZ; public float xRotate; public float yRotate; public float x; public float y; //放縮參數 public static float scale; //顯示列表 public uint showFaceList; public int YU = 1; public void loadFile(String fileName) { // Mian[] f; //POINT3[] v; //FaXiangLiang[] vn; //WenLi[] vt; StreamReader objReader = new StreamReader(fileName); ArrayList al = new ArrayList(); string texLineTem = ""; while (objReader.Peek() != -1) { texLineTem = objReader.ReadLine(); if (texLineTem.Length < 2) continue; if (texLineTem.IndexOf("v") == 0) { if (texLineTem.IndexOf("t") == 1)//vt 0.581151 0.979929 紋理 { string[] tempArray = texLineTem.Split(' '); WenLi vt = new WenLi(); vt.TU = double.Parse(tempArray[1]); vt.TV = double.Parse(tempArray[2]); mesh.VT.Add(vt); } else if (texLineTem.IndexOf("n") == 1)//vn 0.637005 -0.0421857 0.769705 法向量 { string[] tempArray = texLineTem.Split(new char[] { '/', ' ' }, System.StringSplitOptions.RemoveEmptyEntries); FaXiangLiang vn = new FaXiangLiang(); vn.NX = double.Parse(tempArray[1]); vn.NY = double.Parse(tempArray[2]); if (tempArray[3] == "\") { texLineTem = objReader.ReadLine(); vn.NZ = double.Parse(texLineTem); } else vn.NZ = double.Parse(tempArray[3]); mesh.VN.Add(vn); } else {//v -53.0413 158.84 -135.806 點 string[] tempArray = texLineTem.Split(' '); POINT3 v = new POINT3(); v.X = double.Parse(tempArray[1]); v.Y = double.Parse(tempArray[2]); v.Z = double.Parse(tempArray[3]); mesh.V.Add(v); } } else if (texLineTem.IndexOf("f") == 0) { //f 2443//2656 2442//2656 2444//2656 面 string[] tempArray = texLineTem.Split(new char[] { '/', ' ' }, System.StringSplitOptions.RemoveEmptyEntries); Mian f = new Mian(); int i = 0; int k = 1; while (i < 3) { if (mesh.V.Count() != 0) { f.V[i] = int.Parse(tempArray[k]) - 1; k++; } if (mesh.VT.Count() != 0) { f.T[i] = int.Parse(tempArray[k]) - 1; k++; } if (mesh.VN.Count() != 0) { f.N[i] = int.Parse(tempArray[k]) - 1; k++; } i++; } mesh.F.Add(f); } } } public uint createListFace(ref SharpGL.OpenGL gl) { gl.NewList(showFaceList, OpenGL.GL_COMPILE); if(mesh.V.Count() == 0) return 119; for (int i = 0; i < mesh.F.Count(); i++) { gl.Begin(OpenGL.GL_TRIANGLES); // 繪製三角形 if (mesh.VT.Count() != 0) gl.TexCoord(mesh.VT[mesh.F[i].T[0]].TU, mesh.VT[mesh.F[i].T[0]].TV); //紋理 if (mesh.VN.Count() != 0) gl.Normal(mesh.VN[mesh.F[i].N[0]].NX, mesh.VN[mesh.F[i].N[0]].NY, mesh.VN[mesh.F[i].N[0]].NZ);//法向量 gl.Vertex(mesh.V[mesh.F[i].V[0]].X / YU, mesh.V[mesh.F[i].V[0]].Y / YU, mesh.V[mesh.F[i].V[0]].Z / YU); // 上頂點 if (mesh.VT.Count() != 0) gl.TexCoord(mesh.VT[mesh.F[i].T[1]].TU, mesh.VT[mesh.F[i].T[1]].TV); //紋理 if (mesh.VN.Count() != 0) gl.Normal(mesh.VN[mesh.F[i].N[1]].NX, mesh.VN[mesh.F[i].N[1]].NY, mesh.VN[mesh.F[i].N[1]].NZ);//法向量 gl.Vertex(mesh.V[mesh.F[i].V[1]].X / YU, mesh.V[mesh.F[i].V[1]].Y / YU, mesh.V[mesh.F[i].V[1]].Z / YU); // 左下 if (mesh.VT.Count() != 0) gl.TexCoord(mesh.VT[mesh.F[i].T[2]].TU, mesh.VT[mesh.F[i].T[2]].TV); //紋理 if (mesh.VN.Count() != 0) gl.Normal(mesh.VN[mesh.F[i].N[2]].NX, mesh.VN[mesh.F[i].N[2]].NY, mesh.VN[mesh.F[i].N[2]].NZ);//法向量 gl.Vertex(mesh.V[mesh.F[i].V[2]].X / YU, mesh.V[mesh.F[i].V[2]].Y / YU, mesh.V[mesh.F[i].V[2]].Z / YU); // 右下 gl.End(); // 三角形繪製結束 } gl.EndList(); return showFaceList; } } }
自己拿來用的話改一下這句 為你們的命名空間名字就行了 namespace WindowsFormsApp5
因為昨天才開始學c#,所以還是不太懂得c#的一些語法,寫法風格也偏向我經常寫的c++,大家將就着看吧。。。。。等我多學幾天,學點c#的代碼規範再重新改吧。。。
調用方法也很簡單,在winform下的話,在openglControl控件的draw事件中加下面的代碼:
第一步:New一個對象
第二步:讀取自己路徑下的obj模型文件
第三步:調用顯示列表繪製圖案
private void openGLControl1_OpenGLDraw(object sender, SharpGL.RenderEventArgs args) { // 創建一個GL對象 SharpGL.OpenGL gl = this.openGLControl1.OpenGL; gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT); // 清空屏幕 gl.LoadIdentity(); // 重置 gl.Translate(0.0f, 0.0f, -6.0f); // 設置坐標,距離屏幕距離為6 gl.Rotate(0, 1.0f, 0.0f, 0.0f); // 繞X軸旋轉 gl.Rotate(y, 0.0f, 1.0f, 0.0f); // 繞Y軸旋轉 gl.Rotate(0, 0.0f, 0.0f, 1.0f); // 繞Z軸旋轉 gl.Scale(0.003, 0.003, 0.003); myReadObj obj = new myReadObj(); obj.loadFile("bunny.obj"); obj.createListFace(ref gl); gl.CallList(obj.showFaceList); }
對了如果用opengl讀取模型解析3d模型後看起來像個2d的樣子,無非就是你的光照問題,或者模型文件裏面沒有頂點法線vt存在。。這時候可以藉助一些3d模型操作軟件 導入重新生成保存。
運行結果:
項目代碼百度雲鏈接關注下面公眾號,添加小編微信,發送文章標題加源碼獲取。。。。。
若有興趣交流分享技術,可關注本人公眾號,裏面會不定期的分享各種編程教程,和共享源碼,諸如研究分享關於c/c++,python,前端,後端,opencv,halcon,opengl,機器學習深度學習之類有關於基礎編程,圖像處理和機器視覺開發的知識