CSharpGL(9)解析OBJ文件并用CSharpGL渲染
2016-08-13
由于CSharpGL一直在更新,現在這個教程已經不適用最新的代碼了。CSharpGL源碼中包含10多個獨立的Demo,更適合入門參考。
為了盡可能提升渲染效率,CSharpGL是面向Shader的,因此稍有難度。
最近研究shader,需要一些典型的模型來顯示效果。我自己做了幾個。
但是都不如這個茶壺更典型。
我搜羅半天,找到幾個用*.obj格式存儲的茶壺模型,于是不得不寫個OBJ格式文件的解析器來讀取和渲染這個茶壺了。
下載
這個OBJ解析器是CSharpGL的一部分,CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL)
OBJ文件格式
OBJ文件格式是非常簡單的。這種文件以純文本的形式存儲了模型的頂點、法線和紋理坐標和材質使用信息。OBJ文件的每一行,都有極其相似的格式。在OBJ文件中,每行的格式如下:
前綴 參數1 參數2 參數3 ...
其中,前綴標識了這一行所存儲的信息類型。參數則是具體的數據。OBJ文件常見的的前綴有
v 表示本行指定一個頂點。 前綴后跟著3個單精度浮點數,分別表示該定點的X、Y、Z坐標值
vt 表示本行指定一個紋理坐標。此前綴后跟著兩個單精度浮點數。分別表示此紋理坐標的U、V值
vn 表示本行指定一個法線向量。此前綴后跟著3個單精度浮點數,分別表示該法向量的X、Y、Z坐標值
f 表示本行指定一個表面(Face)。一個表面實際上就是一個三角形圖元。此前綴行的參數格式后面將詳細介紹。
usemtl 此前綴后只跟著一個參數。該參數指定了從此行之后到下一個以usemtl開頭的行之間的所有表面所使用的材質名稱。該材質可以在此OBJ文件所附屬的MTL文件中找到具體信息。
mtllib 此前綴后只跟著一個參數。該參數指定了此OBJ文件所使用的材質庫文件(*.mtl)的文件路徑
現在,我們再來看一下OBJ文件的結構。在一個OBJ文件中,首先有一些以v、vt或vn前綴開頭的行指定了所有的頂點、紋理坐標、法線的坐標。然后再由一些以f開頭的行指定每一個三角形所對應的頂點、紋理坐標和法線的索引。在頂點、紋理坐標和法線的索引之間,使用符號"/"隔開的。一個f行可以以下面幾種格式出現:
f 1 2 3 這樣的行表示以第1、2、3號頂點組成一個三角形。
f 1/3 2/5 3/4 這樣的行表示以第1、2、3號頂點組成一個三角形,其中第一個頂點的紋理坐標的索引值為3,第二個頂點的紋理坐標的索引值為5,第三個頂點的紋理坐標的索引值為4。
f 1/3/4 2/5/6 3/4/2 這樣的行表示以第1、2、3號頂點組成一個三角形,其中第一個頂點的紋理坐標的索引值為3,其法線的索引值是4;第二個頂點的紋理坐標的索引值為5,其法線的索引值是6;第三個頂點的紋理坐標的索引值為6,其法線的索引值是2。
f 1//4 2//6 3//2這樣的行表示以第1、2、3號頂點組成一個三角形,且忽略紋理坐標。其中第一個頂點的法線的索引值是4;第二個頂點的法線的索引值是6;第三個頂點的法線的索引值是2。
值得注意的是文件中的索引值是以1作為起點的,這一點與C語言中以0作為起點有很大的不同。在渲染的時候應注意將從文件中讀取的坐標值減去1。
另外,一個OBJ文件里可能有多個模型,每個模型都是由(若干頂點屬性信息+若干面信息)這樣的順序描述的。
解析器設計思路
代碼并不復雜。
1 public class ObjFile 2 { 3 private List<ObjModel> models = new List<ObjModel>(); 4 5 public List<ObjModel> Models 6 { 7 get { return models; } 8 //set { models = value; } 9 } 10 11 public static ObjFile Load(string filename) 12 { 13 ObjFile file = new ObjFile(); 14 15 LoadModels(filename, file); 16 GenNormals(file); 17 OrganizeModels(file); 18 19 return file; 20 } 21 22 private static void OrganizeModels(ObjFile file) 23 { 24 List<ObjModel> models = new List<ObjModel>(); 25 foreach (var model in file.models) 26 { 27 var newModel = OrganizeModels(model); 28 models.Add(newModel); 29 } 30 31 file.models.Clear(); 32 file.models.AddRange(models); 33 } 34 35 private static ObjModel OrganizeModels(ObjModel model) 36 { 37 ObjModel result = new ObjModel(); 38 result.positionList = model.positionList; 39 40 result.normalList.AddRange(model.normalList); 41 42 bool hasUV = model.uvList.Count > 0; 43 if (hasUV) 44 { 45 result.uvList.AddRange(model.uvList); 46 } 47 48 for (int i = 0; i < model.innerFaceList.Count; i++) 49 { 50 var face = model.innerFaceList[i]; 51 var tuple = new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position); 52 result.faceList.Add(tuple); 53 if (face.vertex0.normal > 0) 54 result.normalList[face.vertex0.position - 1] = model.normalList[face.vertex0.normal - 1]; 55 if (face.vertex1.normal > 0) 56 result.normalList[face.vertex1.position - 1] = model.normalList[face.vertex1.normal - 1]; 57 if (face.vertex2.normal > 0) 58 result.normalList[face.vertex2.position - 1] = model.normalList[face.vertex2.normal - 1]; 59 60 if (hasUV) 61 { 62 if (face.vertex0.uv > 0) 63 result.uvList[face.vertex0.position - 1] = model.uvList[face.vertex0.uv - 1]; 64 if (face.vertex1.uv > 0) 65 result.uvList[face.vertex1.position - 1] = model.uvList[face.vertex1.uv - 1]; 66 if (face.vertex2.uv > 0) 67 result.uvList[face.vertex2.position - 1] = model.uvList[face.vertex2.uv - 1]; 68 } 69 70 result.faceList.Add(new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position)); 71 //result.faceList[i] = new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position); 72 } 73 74 //model.innerFaceList.Clear(); 75 76 return result; 77 } 78 79 private static void GenNormals(ObjFile file) 80 { 81 foreach (var model in file.models) 82 { 83 GenNormals(model); 84 } 85 } 86 87 private static void GenNormals(ObjModel model) 88 { 89 if (model.normalList.Count > 0) { return; } 90 91 var faceNormals = new vec3[model.innerFaceList.Count]; 92 model.normalList.AddRange(new vec3[model.positionList.Count]); 93 94 for (int i = 0; i < model.innerFaceList.Count; i++) 95 { 96 var face = model.innerFaceList[i]; 97 vec3 vertex0 = model.positionList[face.vertex0.position - 1]; 98 vec3 vertex1 = model.positionList[face.vertex1.position - 1]; 99 vec3 vertex2 = model.positionList[face.vertex2.position - 1]; 100 vec3 v1 = vertex0 - vertex2; 101 vec3 v2 = vertex2 - vertex1; 102 faceNormals[i] = v1.cross(v2); 103 } 104 105 for (int i = 0; i < model.positionList.Count; i++) 106 { 107 vec3 sum = new vec3(); 108 int shared = 0; 109 for (int j = 0; j < model.innerFaceList.Count; j++) 110 { 111 var face = model.innerFaceList[j]; 112 if (face.vertex0.position - 1 == i || face.vertex1.position - 1 == i || face.vertex2.position - 1 == i) 113 { 114 sum = sum + faceNormals[i]; 115 shared++; 116 } 117 } 118 if (shared > 0) 119 { 120 sum = sum / shared; 121 sum.Normalize(); 122 } 123 model.normalList[i] = sum; 124 } 125 126 } 127 128 private static void LoadModels(string filename, ObjFile file) 129 { 130 using (var sr = new StreamReader(filename)) 131 { 132 var model = new ObjModel(); 133 134 while (!sr.EndOfStream) 135 { 136 string line = sr.ReadLine(); 137 string[] parts = line.Split(separator, StringSplitOptions.RemoveEmptyEntries); 138 if (parts[0] == ("v")) 139 { 140 if (model.innerFaceList.Count > 0) 141 { 142 file.models.Add(model); 143 model = new ObjModel(); 144 } 145 146 vec3 position = new vec3(float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3])); 147 model.positionList.Add(position); 148 } 149 else if (parts[0] == ("vt")) 150 { 151 vec2 uv = new vec2(float.Parse(parts[1]), float.Parse(parts[2])); 152 model.uvList.Add(uv); 153 } 154 else if (parts[0] == ("vn")) 155 { 156 vec3 normal = new vec3(float.Parse(parts[1]), float.Parse(parts[2]), float.Parse(parts[3])); 157 model.normalList.Add(normal); 158 } 159 else if (parts[0] == ("f")) 160 { 161 Triangle triangle = ParseFace(parts); 162 model.innerFaceList.Add(triangle); 163 } 164 } 165 166 file.models.Add(model); 167 } 168 } 169 170 private static Triangle ParseFace(string[] parts) 171 { 172 Triangle result = new Triangle(); 173 if (parts[1].Contains("//")) 174 { 175 for (int i = 1; i < 4; i++) 176 { 177 string[] indexes = parts[i].Split('/'); 178 int position = int.Parse(indexes[0]); int normal = int.Parse(indexes[1]); 179 result[i - 1] = new VertexInfo() { position = position, normal = normal, uv = -1 }; 180 } 181 } 182 else if (parts[1].Contains("/")) 183 { 184 int components = parts[1].Split('/').Length; 185 if (components == 2) 186 { 187 for (int i = 1; i < 4; i++) 188 { 189 string[] indexes = parts[i].Split('/'); 190 int position = int.Parse(indexes[0]); int uv = int.Parse(indexes[1]); 191 result[i - 1] = new VertexInfo() { position = position, normal = -1, uv = uv }; 192 } 193 } 194 else if (components == 3) 195 { 196 for (int i = 1; i < 4; i++) 197 { 198 string[] indexes = parts[i].Split('/'); 199 int position = int.Parse(indexes[0]); int uv = int.Parse(indexes[1]); int normal = int.Parse(indexes[2]); 200 result[i - 1] = new VertexInfo() { position = position, normal = normal, uv = uv, }; 201 } 202 } 203 } 204 else 205 { 206 for (int i = 1; i < 4; i++) 207 { 208 int position = int.Parse(parts[i]); 209 result[i - 1] = new VertexInfo() { position = position, normal = -1, uv = -1, }; 210 } 211 } 212 213 return result; 214 } 215 216 static readonly char[] separator = new char[] { ' ' }; 217 static readonly char[] separator1 = new char[] { '/' }; 218 } 219 220 class VertexInfo 221 { 222 public int position; 223 public int normal; 224 public int uv; 225 } 226 class Triangle 227 { 228 public VertexInfo vertex0; 229 public VertexInfo vertex1; 230 public VertexInfo vertex2; 231 232 public VertexInfo this[int index] 233 { 234 set 235 { 236 if (index == 0) 237 { 238 this.vertex0 = value; 239 } 240 else if (index == 1) 241 { 242 this.vertex1 = value; 243 } 244 else if (index == 2) 245 { 246 this.vertex2 = value; 247 } 248 else 249 { 250 throw new ArgumentException(); 251 } 252 } 253 } 254 }
用CSharpGL渲染OBJ模型文件
IModel接口
我發現一個shader可以渲染多個模型,一個模型也可以用多種shader來渲染。為了保證這種多對多關系,CSharpGL創建了一個IModel接口,用于將模型數據轉換為OpenGL需要的Vertex Buffer Object。
public interface IModel { BufferRenderer GetPositionBufferRenderer(string varNameInShader); BufferRenderer GetColorBufferRenderer(string varNameInShader); BufferRenderer GetNormalBufferRenderer(string varNameInShader); BufferRenderer GetIndexes(); } |
從模型到VBO
為了保證Obj解析器項目的純凈,我們不直接讓ObjModel實現IModel接口,而是另建一個Adapter類(可能不是這個名字,原諒我沒有細學設計模式)。
1 class ObjModelAdpater : IModel
2 {
3 private ObjModel model;
4 public ObjModelAdpater(ObjModel model)
5 {
6 this.model = model;
7 }
8
9
10 CSharpGL.Objects.VertexBuffers.BufferRenderer IModel.GetPositionBufferRenderer(string varNameInShader)
11 {
12 using (var buffer = new ObjModelPositionBuffer(varNameInShader))
13 {
14 buffer.Alloc(model.positionList.Count);
15 unsafe
16 {
17 vec3* array = (vec3*)buffer.FirstElement();
18 for (int i = 0; i < model.positionList.Count; i++)
19 {
20 array[i] = model.positionList[i];
21 }
22 }
23
24 return buffer.GetRenderer();
25 }
26
27 }
28
29 CSharpGL.Objects.VertexBuffers.BufferRenderer IModel.GetColorBufferRenderer(string varNameInShader)
30 {
31 if (model.uvList.Count == 0) { return null; }
32
33 using (var buffer = new ObjModelColorBuffer(varNameInShader))
34 {
35 buffer.Alloc(model.uvList.Count);
36 unsafe
37 {
38 vec2* array = (vec2*)buffer.FirstElement();
39 for (int i = 0; i < model.uvList.Count; i++)
40 {
41 array[i] = model.uvList[i];
42 }
43 }
44
45 return buffer.GetRenderer();
46 }
47
48 }
49
50 CSharpGL.Objects.VertexBuffers.BufferRenderer IModel.GetNormalBufferRenderer(string varNameInShader)
51 {
52 using (var buffer = new ObjModelNormalBuffer(varNameInShader))
53 {
54 buffer.Alloc(model.normalList.Count);
55 unsafe
56 {
57 vec3* array = (vec3*)buffer.FirstElement();
58 for (int i = 0; i < model.normalList.Count; i++)
59 {
60 array[i] = model.normalList[i];
61 }
62 }
63
64 return buffer.GetRenderer();
65 }
66
67 }
68
69 CSharpGL.Objects.VertexBuffers.BufferRenderer IModel.GetIndexes()
70 {
71 using (var buffer = new IndexBuffer<uint>(DrawMode.Triangles, IndexElementType.UnsignedInt, BufferUsage.StaticDraw))
72 {
73 buffer.Alloc(model.faceList.Count * 3);
74 unsafe
75 {
76 uint* array = (uint*)buffer.FirstElement();
77 for (int i = 0; i < model.faceList.Count; i++)
78 {
79 array[i * 3 + 0] = (uint)(model.faceList[i].Item1 - 1);
80 array[i * 3 + 1] = (uint)(model.faceList[i].Item2 - 1);
81 array[i * 3 + 2] = (uint)(model.faceList[i].Item3 - 1);
82 }
83 }
84
85 return buffer.GetRenderer();
86 }
87 }
88 }
89
90
91 class ObjModelPositionBuffer : PropertyBuffer<vec3>
92 {
93 public ObjModelPositionBuffer(string varNameInShader)
94 : base(varNameInShader, 3, GL.GL_FLOAT, BufferUsage.StaticDraw)
95 {
96
97 }
98 }
99
100 class ObjModelColorBuffer : PropertyBuffer<vec2>
101 {
102 public ObjModelColorBuffer(string varNameInShader)
103 : base(varNameInShader, 3, GL.GL_FLOAT, BufferUsage.StaticDraw)
104 {
105
106 }
107 }
108
109 class ObjModelNormalBuffer : PropertyBuffer<vec3>
110 {
111 public ObjModelNormalBuffer(string varNameInShader)
112 : base(varNameInShader, 3, GL.GL_FLOAT, BufferUsage.StaticDraw)
113 {
114
115 }
116 }
渲染
剩下的就簡單了,把其他Element的框架抄來就差不多了。
1 class ObjModelElement : SceneElementBase
2 {
3 ShaderProgram shaderProgram;
4
5 #region VAO/VBO renderers
6
7 VertexArrayObject vertexArrayObject;
8
9 const string strin_Position = "in_Position";
10 BufferRenderer positionBufferRenderer;
11
12 //const string strin_Color = "in_Color";
13 //BufferRenderer colorBufferRenderer;
14
15 const string strin_Normal = "in_Normal";
16 BufferRenderer normalBufferRenderer;
17
18 BufferRenderer indexBufferRenderer;
19
20 #endregion
21
22 #region uniforms
23
24
25 const string strmodelMatrix = "modelMatrix";
26 public mat4 modelMatrix;
27
28 const string strviewMatrix = "viewMatrix";
29 public mat4 viewMatrix;
30
31 const string strprojectionMatrix = "projectionMatrix";
32 public mat4 projectionMatrix;
33
34 #endregion
35
36
37 public PolygonModes polygonMode = PolygonModes.Filled;
38
39 private int indexCount;
40
41 private ObjModelAdpater objModelAdapter;
42
43 public ObjModelElement(ObjModel objModel)
44 {
45 this.objModelAdapter = new ObjModelAdpater(objModel);
46 }
47
48 protected void InitializeShader(out ShaderProgram shaderProgram)
49 {
50 var vertexShaderSource = ManifestResourceLoader.LoadTextFile(@"ObjModelElement.vert");
51 var fragmentShaderSource = ManifestResourceLoader.LoadTextFile(@"ObjModelElement.frag");
52
53 shaderProgram = new ShaderProgram();
54 shaderProgram.Create(vertexShaderSource, fragmentShaderSource, null);
55
56 }
57
58 protected void InitializeVAO()
59 {
60 IModel model = this.objModelAdapter;
61
62 this.positionBufferRenderer = model.GetPositionBufferRenderer(strin_Position);
63 //this.colorBufferRenderer = model.GetColorBufferRenderer(strin_Color);
64 this.normalBufferRenderer = model.GetNormalBufferRenderer(strin_Normal);
65 this.indexBufferRenderer = model.GetIndexes();
66
67 IndexBufferRenderer renderer = this.indexBufferRenderer as IndexBufferRenderer;
68 if (renderer != null)
69 {
70 this.indexCount = renderer.ElementCount;
71 }
72 }
73
74 protected override void DoInitialize()
75 {
76 InitializeShader(out shaderProgram);
77
78 InitializeVAO();
79 }
80
81 protected override void DoRender(RenderEventArgs e)
82 {
83 if (this.vertexArrayObject == null)
84 {
85 var vao = new VertexArrayObject(
86 this.positionBufferRenderer,
87 //this.colorBufferRenderer,
88 this.normalBufferRenderer,
89 this.indexBufferRenderer);
90 vao.Create(e, this.shaderProgram);
91
92 this.vertexArrayObject = vao;
93 }
94
95 ShaderProgram program = this.shaderProgram;
96 // 綁定shader
97 program.Bind();
98
99 program.SetUniformMatrix4(strprojectionMatrix, projectionMatrix.to_array());
100 program.SetUniformMatrix4(strviewMatrix, viewMatrix.to_array());
101 program.SetUniformMatrix4(strmodelMatrix, modelMatrix.to_array());
102
103 int[] originalPolygonMode = new int[1];
104 GL.GetInteger(GetTarget.PolygonMode, originalPolygonMode);
105
106 GL.PolygonMode(PolygonModeFaces.FrontAndBack, this.polygonMode);
107 this.vertexArrayObject.Render(e, this.shaderProgram);
108 GL.PolygonMode(PolygonModeFaces.FrontAndBack, (PolygonModes)(originalPolygonMode[0]));
109
110 // 解綁shader
111 program.Unbind();
112 }
113
114
115
116 protected override void CleanUnmanagedRes()
117 {
118 if (this.vertexArrayObject != null)
119 {
120 this.vertexArrayObject.Dispose();
121 }
122
123 base.CleanUnmanagedRes();
124 }
125
126 public void DecreaseVertexCount()
127 {
128 IndexBufferRenderer renderer = this.indexBufferRenderer as IndexBufferRenderer;
129 if (renderer != null)
130 {
131 if (renderer.ElementCount > 0)
132 renderer.ElementCount--;
133 }
134 }
135
136 public void IncreaseVertexCount()
137 {
138 IndexBufferRenderer renderer = this.indexBufferRenderer as IndexBufferRenderer;
139 if (renderer != null)
140 {
141 if (renderer.ElementCount < this.indexCount)
142 renderer.ElementCount++;
143 }
144 }
145
146
147 }
結果如圖所示。我用normal值來表示顏色,就成了這個樣子。
總結
本篇介紹了一個OBJ文件解析器、渲染器和IModel接口的設計思想。
文章列表
留言列表