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接口的设计思想。