CSharpGL(5)解析3DS文件并用CSharpGL渲染
我曾经写过一个简单的*.3ds文件的解析器,但是只能解析最基本的顶点、索引信息,且此解析器是仿照别人的C++代码改写的,设计的也不好,不方便扩展。
现在我重新设计实现了一个*.3ds文件的解析器,它能解析的Chunk类型更多,且容易扩展。以后需要解析更多类型的Chunk时比较简单。
下载
这个3DS解析器现在不是CSharpGL的一部分,CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
本文代码可在(https://github.com/bitzhuwei/CSharpGL2)找到。
本文所用的3ds文件您可以在此(http://www.cgrealm.org/d/downpage.php?n=2&id=15764::1326768548)下载,由于文件比较大我就不上传了。
3DS文件格式
树
3ds文件是二进制的。3ds格式的基本单元叫块(chunk)。我们就是读这样一块一块的信息。目录树如下,缩进风格体现了块的父子关系。可见3ds模型文件和XML文件类似,都是只有1个根结点的树状结构。
1 0x4D4D // Main Chunk 2 ├─ 0x0002 // M3D Version 3 ├─ 0x3D3D // 3D Editor Chunk 4 │ ├─ 0x4000 // Object Block 5 │ │ ├─ 0x4100 // Triangular Mesh 6 │ │ │ ├─ 0x4110 // Vertices List 7 │ │ │ ├─ 0x4120 // Faces Description 8 │ │ │ │ ├─ 0x4130 // Faces Material 9 │ │ │ │ └─ 0x4150 // Smoothing Group List 10 │ │ │ ├─ 0x4140 // Mapping Coordinates List 11 │ │ │ └─ 0x4160 // Local Coordinates System 12 │ │ ├─ 0x4600 // Light 13 │ │ │ └─ 0x4610 // Spotlight 14 │ │ └─ 0x4700 // Camera 15 │ └─ 0xAFFF // Material Block 16 │ ├─ 0xA000 // Material Name 17 │ ├─ 0xA010 // Ambient Color 18 │ ├─ 0xA020 // Diffuse Color 19 │ ├─ 0xA030 // Specular Color 20 │ ├─ 0xA200 // Texture Map 1 21 │ ├─ 0xA230 // Bump Map 22 │ └─ 0xA220 // Reflection Map 23 │ │ // Sub Chunks For Each Map 24 │ ├─ 0xA300 // Mapping Filename 25 │ └─ 0xA351 // Mapping Parameters 26 └─ 0xB000 // Keyframer Chunk 27 ├─ 0xB002 // Mesh Information Block 28 ├─ 0xB007 // Spot Light Information Block 29 └─ 0xB008 // Frames (Start and End) 30 ├─ 0xB010 // Object Name 31 ├─ 0xB013 // Object Pivot Point 32 ├─ 0xB020 // Position Track 33 ├─ 0xB021 // Rotation Track 34 ├─ 0xB022 // Scale Track 35 └─ 0xB030 // Hierarchy Position
实际上完整的chunk列表有上千种类型,我们只需解析其中的顶点列表、面列表和纹理UV列表就行了。
以类型标识为0x4D4D的MAIN CHUNK为例,整个3ds文件的前两个byte必须是0x4D4D,否则就说明这个文件不是3ds模型文件。然后从第3到第6个byte是一个Uint32型的数值,表示整个MAIN CHUNK的长度。由于MAIN CHUNK是整个3ds文件的根结点,它的长度也即整个3ds文件的长度。
块(Chunk)的结构
每一个“chunk”的结构如下所示:
偏移量 |
长度 |
|
0 |
2 |
块标识符 |
2 |
4 |
块长: 块数据 + 子块内容 |
6 |
n |
块数据 |
6+n |
m |
S子块 |
文件内容
一个3DS文件,其中包含若干材质对象,材质对象里有材质参数和贴图文件名;还有若干子模型,每个子模型都由顶点位置、UV位置、三角形索引和分组索引构成。分组索引是这么一个东西:它由若干三角形索引的编号和一个材质对象名组成。这个分组索引似乎暗示着:渲染过程应根据分组索引描绘的顺序进行,即取出一个分组索引,绑定它指定的材质和贴图,渲染它指定的三角形,然后取出下一个分组索引继续上述渲染操作。我们将在后文进行验证。
2016-01-21
今天发现有的3ds文件是没有分组索引这个玩意的。所以要特殊处理一下。
解析器设计思路
在之前写的解析器中使用的思路是:首先根据偏移量和长度找到一个块的标识符,然后据此来判断它是什么块,遇到我们需要的块,就进一步读取,如果不需要,直接跳过这一块,读取下面的块。这没有用到面向对象的思想,只有面向过程编程。如果需要添加一个新的Chunk类型,修改起来是比较困难的。
我重新设计的解析器的思路如下:
递归读取各个块
读取一个块,然后依次读取它的各个子块。鉴于各个块之间的树状关系,这是一个递归的过程。
各个类型的块都应该继承自同一基类型ChunkBase。对于具体的Chunk类型,只需override掉Process方法即可实现自己的解析过程。
1 public abstract class ChunkBase 2 { 3 public ChunkBase Parent; 4 public List<ChunkBase> Childern; 5 6 public uint Length; 7 public uint BytesRead; 8 9 public ChunkBase() 10 { 11 this.Childern = new List<ChunkBase>(); 12 } 13 14 internal virtual void Process(ParsingContext context) 15 { 16 var chunk = this; 17 var reader = context.reader; 18 19 while (chunk.BytesRead < chunk.Length) 20 { 21 ChunkBase child = reader.ReadChunk(); 22 child.Parent = this; 23 this.Childern.Add(child); 24 25 child.Process(context); 26 27 chunk.BytesRead += child.BytesRead; 28 } 29 } 30 }
数据字典
各个类型的Chunk都用一个具体的class类型表达,为了方便这些class类型与用ushort表达的的Chunk类型相互转换,我们需要2个字典。
1 public static partial class ChunkBaseHelper 2 { 3 4 private static readonly Dictionary<Type, ushort> chunkTypeDict = new Dictionary<Type, ushort>(); 5 private static readonly Dictionary<ushort, Type> chunkIDDict = new Dictionary<ushort, Type>(); 6 7 /// <summary> 8 /// 开发者必须了解的东西。 9 /// </summary> 10 static ChunkBaseHelper() 11 { 12 chunkTypeDict.Add(typeof(MainChunk), 0x4D4D); 13 { 14 chunkTypeDict.Add(typeof(VersionChunk), 0x0002); 15 chunkTypeDict.Add(typeof(_3DEditorChunk), 0x3D3D); 16 { 17 chunkTypeDict.Add(typeof(ObjectBlockChunk), 0x4000); 18 { 19 chunkTypeDict.Add(typeof(TriangularMeshChunk), 0x4100); 20 { 21 chunkTypeDict.Add(typeof(VerticesListChunk), 0x4110); 22 chunkTypeDict.Add(typeof(FacesDescriptionChunk), 0x4120); 23 { 24 chunkTypeDict.Add(typeof(FacesMaterialChunk), 0x4130); 25 chunkTypeDict.Add(typeof(SmoothingGroupListChunk), 0x4150); 26 } 27 chunkTypeDict.Add(typeof(MappingCoordinatesListChunk), 0x4140); 28 chunkTypeDict.Add(typeof(LocalCoordinatesSystemChunk), 0x4160); 29 } 30 chunkTypeDict.Add(typeof(LightChunk), 0x4600); 31 { 32 chunkTypeDict.Add(typeof(SpotlightChunk), 0x4610); 33 } 34 chunkTypeDict.Add(typeof(CameraChunk), 0x4700); 35 } 36 chunkTypeDict.Add(typeof(MaterialBlockChunk), 0xAFFF); 37 { 38 chunkTypeDict.Add(typeof(MaterialNameChunk), 0xA000); 39 chunkTypeDict.Add(typeof(AmbientColorChunk), 0xA010); 40 chunkTypeDict.Add(typeof(DiffuseColorChunk), 0xA020); 41 chunkTypeDict.Add(typeof(SpecularColorChunk), 0xA030); 42 chunkTypeDict.Add(typeof(MatShininessChunk), 0xA040); 43 chunkTypeDict.Add(typeof(TextureMapChunk), 0xA200); 44 chunkTypeDict.Add(typeof(BumpMapChunk), 0xA230); 45 chunkTypeDict.Add(typeof(ReflectionMapChunk), 0xA220); 46 { 47 chunkTypeDict.Add(typeof(MappingFilenameChunk), 0xA300); 48 chunkTypeDict.Add(typeof(MappingParametersChunk), 0xA351); 49 } 50 } 51 } 52 chunkTypeDict.Add(typeof(KeyframeChunk), 0xB000); 53 { 54 chunkTypeDict.Add(typeof(MeshInformationBlockChunk), 0xB002); 55 chunkTypeDict.Add(typeof(SpotLightInformationBlockChunk), 0xB007); 56 chunkTypeDict.Add(typeof(FramesChunk), 0xB008); 57 { 58 chunkTypeDict.Add(typeof(ObjectNameChunk), 0xB010); 59 chunkTypeDict.Add(typeof(ObjectPivotPointChunk), 0xB013); 60 chunkTypeDict.Add(typeof(PositionTrackChunk), 0xB020); 61 chunkTypeDict.Add(typeof(RotationTrackChunk), 0xB021); 62 chunkTypeDict.Add(typeof(ScaleTrackChunk), 0xB022); 63 chunkTypeDict.Add(typeof(HierarchyPositionChunk), 0xB030); 64 } 65 } 66 } 67 68 chunkIDDict.Add(0x4D4D, typeof(MainChunk)); 69 { 70 chunkIDDict.Add(0x0002, typeof(VersionChunk)); 71 chunkIDDict.Add(0x3D3D, typeof(_3DEditorChunk)); 72 { 73 chunkIDDict.Add(0x4000, typeof(ObjectBlockChunk)); 74 { 75 chunkIDDict.Add(0x4100, typeof(TriangularMeshChunk)); 76 { 77 chunkIDDict.Add(0x4110, typeof(VerticesListChunk)); 78 chunkIDDict.Add(0x4120, typeof(FacesDescriptionChunk)); 79 { 80 chunkIDDict.Add(0x4130, typeof(FacesMaterialChunk)); 81 chunkIDDict.Add(0x4150, typeof(SmoothingGroupListChunk)); 82 } 83 chunkIDDict.Add(0x4140, typeof(MappingCoordinatesListChunk)); 84 chunkIDDict.Add(0x4160, typeof(LocalCoordinatesSystemChunk)); 85 } 86 chunkIDDict.Add(0x4600, typeof(LightChunk)); 87 { 88 chunkIDDict.Add(0x4610, typeof(SpotlightChunk)); 89 } 90 chunkIDDict.Add(0x4700, typeof(CameraChunk)); 91 } 92 chunkIDDict.Add(0xAFFF, typeof(MaterialBlockChunk)); 93 { 94 chunkIDDict.Add(0xA000, typeof(MaterialNameChunk)); 95 chunkIDDict.Add(0xA010, typeof(AmbientColorChunk)); 96 chunkIDDict.Add(0xA020, typeof(DiffuseColorChunk)); 97 chunkIDDict.Add(0xA030, typeof(SpecularColorChunk)); 98 chunkIDDict.Add(0xA040, typeof(MatShininessChunk)); 99 chunkIDDict.Add(0xA200, typeof(TextureMapChunk)); 100 chunkIDDict.Add(0xA230, typeof(BumpMapChunk)); 101 chunkIDDict.Add(0xA220, typeof(ReflectionMapChunk)); 102 { 103 chunkIDDict.Add(0xA300, typeof(MappingFilenameChunk)); 104 chunkIDDict.Add(0xA351, typeof(MappingParametersChunk)); 105 } 106 } 107 } 108 chunkIDDict.Add(0xB000, typeof(KeyframeChunk)); 109 { 110 chunkIDDict.Add(0xB002, typeof(MeshInformationBlockChunk)); 111 chunkIDDict.Add(0xB007, typeof(SpotLightInformationBlockChunk)); 112 chunkIDDict.Add(0xB008, typeof(FramesChunk)); 113 { 114 chunkIDDict.Add(0xB010, typeof(ObjectNameChunk)); 115 chunkIDDict.Add(0xB013, typeof(ObjectPivotPointChunk)); 116 chunkIDDict.Add(0xB020, typeof(PositionTrackChunk)); 117 chunkIDDict.Add(0xB021, typeof(RotationTrackChunk)); 118 chunkIDDict.Add(0xB022, typeof(ScaleTrackChunk)); 119 chunkIDDict.Add(0xB030, typeof(HierarchyPositionChunk)); 120 } 121 } 122 } 123 } 124 }
未定义的Chunk
3ds文件有上千种Chunk,我们暂时不会都解析出来(也没必要全解析出来)。所以我们用一个“未定义的Chunk”类型来代表那些我们不想解析的Chunk类型。
1 /// <summary> 2 /// 3ds文件有上千种Chunk,我们暂时不会都解析出来(也没必要全解析出来)。所以我们用一个“未定义的Chunk”类型来代表那些我们不想解析的Chunk类型。 3 /// </summary> 4 public class UndefinedChunk : ChunkBase 5 { 6 public ushort ID; 7 public bool IsChunk { get; private set; } 8 9 public UndefinedChunk() 10 { 11 this.IsChunk = true; 12 } 13 14 public override string ToString() 15 { 16 return string.Format("{0}(0x{1:X4}), position: {2}, length: {3}, read bytes: {4}", 17 this.IsChunk ? "Unknown Chunk" : "Fake Chunk", ID, Position, Length, BytesRead); 18 } 19 20 internal override void Process(ParsingContext context) 21 { 22 var chunk = this; 23 var reader = context.reader; 24 var parent = this.Parent; 25 26 uint length = this.Length - this.BytesRead; 27 28 if ((parent != null)) 29 { 30 var another = parent.Length - parent.BytesRead - this.BytesRead; 31 length = Math.Min(length, another); 32 } 33 34 reader.BaseStream.Position += length; 35 chunk.BytesRead += length; 36 if (chunk.Length != chunk.BytesRead) 37 { 38 chunk.Length = chunk.BytesRead; 39 this.IsChunk = false; 40 } 41 } 42 }
注意:这里获取到的UndefinedChunk对象,不一定代表真的有这样一个未被解析的Chunk,它也可能是其父Chunk的一部分数据内容。所以,我们要结合这里的another值来判断到底应该继续读取多少字节,并且修补好可能出错的chunk.Length。
读出一个Chunk的扩展方法
每次获取一个Chunk对象时,都是借助BinaryReader得到Chunk类型和长度的,所以我们给它一个扩展方法,用于“读出一个Chunk”。
1 public static partial class ChunkBaseHelper 2 { 3 public static ChunkBase ReadChunk(this BinaryReader reader) 4 { 5 // 2 byte ID 6 ushort id = reader.ReadUInt16(); 7 // 4 byte length 8 uint length = reader.ReadUInt32(); 9 // 2 + 4 = 6 10 uint bytesRead = 6; 11 12 Type type; 13 if (chunkIDDict.TryGetValue(id, out type)) 14 { 15 object obj = Activator.CreateInstance(type); 16 ChunkBase result = obj as ChunkBase; 17 //result.ID = id;//不再需要记录ID,此对象的类型就指明了它的ID。 18 result.Length = length; 19 result.BytesRead = bytesRead; 20 return result; 21 } 22 else 23 { 24 return new UndefinedChunk() { ID = id, Length = length, BytesRead = bytesRead, }; 25 } 26 } 27 }
获取Chunk类型的ushort值
得到一个Chunk对象后,可能会需要获取此对象代表的Chunk类型。
1 public static partial class ChunkBaseHelper 2 { 3 public static ushort GetID(this ChunkBase chunk) 4 { 5 ushort value; 6 7 if (chunk is UndefinedChunk) 8 { 9 value = (chunk as UndefinedChunk).ID; 10 } 11 else 12 { 13 Type type = chunk.GetType(); 14 value = chunkTypeDict[type];//如果此处不存在此type的key,说明static构造函数需要添加此类型的字典信息。 15 } 16 17 return value; 18 } 19 }
解析器输出:Chunk树
我们用TreeView控件来展示解析出来的Chunk树。
如果不想看那些未定义的Chunk类型,可以隐藏之。
如果需要,你可以将此Chunk树导出为文本格式:
从Chunk树到legacy OpenGL
Dumper
已经得到了Chunk树,下面需要得到可用于OpenGL渲染的模型。这实际上是一个语义分析和生成中间代码的过程。以根结点MainChunk为例:
1 public static partial class ChunkDumper 2 { 3 public static void Dump(this MainChunk chunk, out ThreeDSModel4LegacyOpenGL model) 4 { 5 model = new ThreeDSModel4LegacyOpenGL(); 6 7 foreach (var item in chunk.Children) 8 { 9 if(item is VersionChunk) 10 { 11 (item as VersionChunk).Dump(model); 12 } 13 else if(item is _3DEditorChunk) 14 { 15 (item as _3DEditorChunk).Dump(model); 16 } 17 else if (item is KeyframeChunk) 18 { 19 (item as KeyframeChunk).Dump(model); 20 } 21 else if(!(item is UndefinedChunk)) 22 { 23 throw new NotImplementedException(string.Format( 24 "not dumper implemented for {0}", item.GetType())); 25 } 26 } 27 } 28 }
我们为每个Chunk类型都编写一个Dumper,在各个Dump过程中收集需要的信息(顶点位置、UV、贴图文件名、材质、光照等),汇总到一个ThreeDSModel4LegacyOpenGL对象,这个对象就可以用来渲染图形了。
渲染
根据上文对分组索引的推测,我给出如下的渲染过程。
1 public class ThreeDSModel4LegacyOpenGL 2 { 3 public List<ThreeDSMesh4LegacyOpenGL> Entities = new List<ThreeDSMesh4LegacyOpenGL>(); 4 public Dictionary<string, ThreeDSMaterial4LegacyOpenGL> MaterialDict = new Dictionary<string, ThreeDSMaterial4LegacyOpenGL>(); 5 6 public void Render() 7 { 8 foreach (ThreeDSMesh4LegacyOpenGL mesh in Entities) 9 { 10 mesh.Render(this); 11 } 12 } 13 } 14 public class ThreeDSMesh4LegacyOpenGL 15 { 16 public List<Tuple<string, ushort[]>> usingMaterialIndexesList = new List<Tuple<string, ushort[]>>(); 17 // TODO: OO this 18 // fields should be private 19 // constructor with verts and faces 20 // normalize in ctor 21 22 //public ThreeDSMaterial material = new ThreeDSMaterial(); 23 //public string UsesMaterial; 24 25 // The stored vertices 26 public Vector[] Vertexes; 27 28 // The calculated normals 29 public Vector[] normals; 30 31 // The indices of the triangles which point to vertices 32 public Triangle[] TriangleIndexes; 33 34 // The coordinates which map the texture onto the entity 35 public TexCoord[] TexCoords; 36 37 bool normalized = false; 38 public ushort[] UsesIndexes; 39 40 public void Render(ThreeDSModel4LegacyOpenGL model) 41 { 42 if (TriangleIndexes == null) return; 43 44 // Draw every triangle in the entity 45 foreach (var item in this.usingMaterialIndexesList) 46 { 47 var material = model.MaterialDict[item.Item1]; 48 49 GL.Materialfv(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT, material.Ambient); 50 GL.Materialfv(GL.GL_FRONT_AND_BACK, GL.GL_DIFFUSE, material.Diffuse); 51 GL.Materialfv(GL.GL_FRONT_AND_BACK, GL.GL_SPECULAR, material.Specular); 52 GL.Materialf(GL.GL_FRONT_AND_BACK, GL.GL_SHININESS, material.Shininess); 53 54 Texture2D[] textures = new Texture2D[] { material.GetTexture(), material.GetBumpTexture(), material.GetReflectionTexture(), }; 55 bool drawn = false; 56 foreach (var texture in textures) 57 { 58 if (!(drawn && texture == null)) // 如果没有贴图,就只画一次。 59 { 60 if (texture != null) 61 { 62 GL.Enable(GL.GL_TEXTURE_2D); 63 texture.Bind(); 64 } 65 66 DrawTriangles(item, texture); 67 68 if (texture != null) 69 { 70 texture.Unbind(); 71 GL.Disable(GL.GL_TEXTURE_2D); 72 } 73 } 74 75 drawn = true; 76 } 77 } 78 } 79 80 private void DrawTriangles(Tuple<string, ushort[]> usingMaterialIndexes, Texture2D texture) 81 { 82 GL.Begin(GL.GL_TRIANGLES); 83 foreach (var usingIndex in usingMaterialIndexes.Item2) 84 { 85 Triangle tri = this.TriangleIndexes[usingIndex]; 86 // Vertex 1 87 if (normalized) 88 { 89 var normal = this.normals[tri.vertex1]; 90 GL.Normal3d(normal.X, normal.Y, normal.Z); 91 } 92 if (texture != null) 93 { 94 var texCoord = this.TexCoords[tri.vertex1]; 95 GL.TexCoord2f(texCoord.U, texCoord.V); 96 } 97 { 98 var vertex = this.Vertexes[tri.vertex1]; 99 GL.Vertex3d(vertex.X, vertex.Y, vertex.Z); 100 } 101 102 // Vertex 2 103 if (normalized) 104 { 105 var normal = this.normals[tri.vertex2]; 106 GL.Normal3d(normal.X, normal.Y, normal.Z); 107 } 108 if (texture != null) 109 { 110 var texCoord = this.TexCoords[tri.vertex2]; 111 GL.TexCoord2f(texCoord.U, texCoord.V); 112 } 113 { 114 var vertex = this.Vertexes[tri.vertex2]; 115 GL.Vertex3d(vertex.X, vertex.Y, vertex.Z); 116 } 117 118 // Vertex 3 119 if (normalized) 120 { 121 var normal = this.normals[tri.vertex3]; 122 GL.Normal3d(normal.X, normal.Y, normal.Z); 123 } 124 if (texture != null) 125 { 126 var texCoord = this.TexCoords[tri.vertex3]; 127 GL.TexCoord2f(texCoord.U, texCoord.V); 128 } 129 { 130 var vertex = this.Vertexes[tri.vertex3]; 131 GL.Vertex3d(vertex.X, vertex.Y, vertex.Z); 132 } 133 } 134 GL.End(); 135 } 136 }
验证分组索引的功能
上文中我们发现了分组索引的存在,根据它的内容推测了它的功能,现在来验证一下。我找到一个3ds文件,用A3dsViewer打开是这样的:
这个3ds文件附带多个贴图:
这个是树皮。
这是花盆里的石头。
这是花盆里的苔藓(某种绿色植物?)
这是盆景的红叶。
现在再用我制作的3DSViewer渲染看看:
整体上是对了,分组索引成功地将各个贴图附到了对应的三角形上。
但是花盆不应该是白的,这是某些光照没有解析的原因。
从Chunk树到modern OpenGL
有了legacy OpenGL探路,modern OpenGL的渲染就容易多了,这里暂时不详述。
总结
目前这个3ds解析器算是可用了,以后需要扩展时也很容易。如果能找到更多的3ds文件来测试,就能知道还需要解析哪些类型的Chunk了。