CSharpGL(9)解析OBJ文件并用CSharpGL渲染

2022-12-15,,,,

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文件里可能有多个模型,每个模型都是由(若干顶点属性信息+若干面信息)这样的顺序描述的。

解析器设计思路

代码并不复杂。

     public class ObjFile
{
private List<ObjModel> models = new List<ObjModel>(); public List<ObjModel> Models
{
get { return models; }
//set { models = value; }
} public static ObjFile Load(string filename)
{
ObjFile file = new ObjFile(); LoadModels(filename, file);
GenNormals(file);
OrganizeModels(file); return file;
} private static void OrganizeModels(ObjFile file)
{
List<ObjModel> models = new List<ObjModel>();
foreach (var model in file.models)
{
var newModel = OrganizeModels(model);
models.Add(newModel);
} file.models.Clear();
file.models.AddRange(models);
} private static ObjModel OrganizeModels(ObjModel model)
{
ObjModel result = new ObjModel();
result.positionList = model.positionList; result.normalList.AddRange(model.normalList); bool hasUV = model.uvList.Count > ;
if (hasUV)
{
result.uvList.AddRange(model.uvList);
} for (int i = ; i < model.innerFaceList.Count; i++)
{
var face = model.innerFaceList[i];
var tuple = new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position);
result.faceList.Add(tuple);
if (face.vertex0.normal > )
result.normalList[face.vertex0.position - ] = model.normalList[face.vertex0.normal - ];
if (face.vertex1.normal > )
result.normalList[face.vertex1.position - ] = model.normalList[face.vertex1.normal - ];
if (face.vertex2.normal > )
result.normalList[face.vertex2.position - ] = model.normalList[face.vertex2.normal - ]; if (hasUV)
{
if (face.vertex0.uv > )
result.uvList[face.vertex0.position - ] = model.uvList[face.vertex0.uv - ];
if (face.vertex1.uv > )
result.uvList[face.vertex1.position - ] = model.uvList[face.vertex1.uv - ];
if (face.vertex2.uv > )
result.uvList[face.vertex2.position - ] = model.uvList[face.vertex2.uv - ];
} result.faceList.Add(new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position));
//result.faceList[i] = new Tuple<int, int, int>(face.vertex0.position, face.vertex1.position, face.vertex2.position);
} //model.innerFaceList.Clear(); return result;
} private static void GenNormals(ObjFile file)
{
foreach (var model in file.models)
{
GenNormals(model);
}
} private static void GenNormals(ObjModel model)
{
if (model.normalList.Count > ) { return; } var faceNormals = new vec3[model.innerFaceList.Count];
model.normalList.AddRange(new vec3[model.positionList.Count]); for (int i = ; i < model.innerFaceList.Count; i++)
{
var face = model.innerFaceList[i];
vec3 vertex0 = model.positionList[face.vertex0.position - ];
vec3 vertex1 = model.positionList[face.vertex1.position - ];
vec3 vertex2 = model.positionList[face.vertex2.position - ];
vec3 v1 = vertex0 - vertex2;
vec3 v2 = vertex2 - vertex1;
faceNormals[i] = v1.cross(v2);
} for (int i = ; i < model.positionList.Count; i++)
{
vec3 sum = new vec3();
int shared = ;
for (int j = ; j < model.innerFaceList.Count; j++)
{
var face = model.innerFaceList[j];
if (face.vertex0.position - == i || face.vertex1.position - == i || face.vertex2.position - == i)
{
sum = sum + faceNormals[i];
shared++;
}
}
if (shared > )
{
sum = sum / shared;
sum.Normalize();
}
model.normalList[i] = sum;
} } private static void LoadModels(string filename, ObjFile file)
{
using (var sr = new StreamReader(filename))
{
var model = new ObjModel(); while (!sr.EndOfStream)
{
string line = sr.ReadLine();
string[] parts = line.Split(separator, StringSplitOptions.RemoveEmptyEntries);
if (parts[] == ("v"))
{
if (model.innerFaceList.Count > )
{
file.models.Add(model);
model = new ObjModel();
} vec3 position = new vec3(float.Parse(parts[]), float.Parse(parts[]), float.Parse(parts[]));
model.positionList.Add(position);
}
else if (parts[] == ("vt"))
{
vec2 uv = new vec2(float.Parse(parts[]), float.Parse(parts[]));
model.uvList.Add(uv);
}
else if (parts[] == ("vn"))
{
vec3 normal = new vec3(float.Parse(parts[]), float.Parse(parts[]), float.Parse(parts[]));
model.normalList.Add(normal);
}
else if (parts[] == ("f"))
{
Triangle triangle = ParseFace(parts);
model.innerFaceList.Add(triangle);
}
} file.models.Add(model);
}
} private static Triangle ParseFace(string[] parts)
{
Triangle result = new Triangle();
if (parts[].Contains("//"))
{
for (int i = ; i < ; i++)
{
string[] indexes = parts[i].Split('/');
int position = int.Parse(indexes[]); int normal = int.Parse(indexes[]);
result[i - ] = new VertexInfo() { position = position, normal = normal, uv = - };
}
}
else if (parts[].Contains("/"))
{
int components = parts[].Split('/').Length;
if (components == )
{
for (int i = ; i < ; i++)
{
string[] indexes = parts[i].Split('/');
int position = int.Parse(indexes[]); int uv = int.Parse(indexes[]);
result[i - ] = new VertexInfo() { position = position, normal = -, uv = uv };
}
}
else if (components == )
{
for (int i = ; i < ; i++)
{
string[] indexes = parts[i].Split('/');
int position = int.Parse(indexes[]); int uv = int.Parse(indexes[]); int normal = int.Parse(indexes[]);
result[i - ] = new VertexInfo() { position = position, normal = normal, uv = uv, };
}
}
}
else
{
for (int i = ; i < ; i++)
{
int position = int.Parse(parts[i]);
result[i - ] = new VertexInfo() { position = position, normal = -, uv = -, };
}
} return result;
} static readonly char[] separator = new char[] { ' ' };
static readonly char[] separator1 = new char[] { '/' };
} class VertexInfo
{
public int position;
public int normal;
public int uv;
}
class Triangle
{
public VertexInfo vertex0;
public VertexInfo vertex1;
public VertexInfo vertex2; public VertexInfo this[int index]
{
set
{
if (index == )
{
this.vertex0 = value;
}
else if (index == )
{
this.vertex1 = value;
}
else if (index == )
{
this.vertex2 = value;
}
else
{
throw new ArgumentException();
}
}
}
}

Parser

用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 }

Adapter

渲染

剩下的就简单了,把其他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 }

ObjModelElement

结果如图所示。我用normal值来表示颜色,就成了这个样子。

总结

本篇介绍了一个OBJ文件解析器、渲染器和IModel接口的设计思想。

CSharpGL(9)解析OBJ文件并用CSharpGL渲染的相关教程结束。

《CSharpGL(9)解析OBJ文件并用CSharpGL渲染.doc》

下载本文的Word格式文档,以方便收藏与打印。