CSharpGL(5)解析3DS文件并用CSharpGL渲染

CSharpGL(5)解析3DS文件并用CSharpGL渲染

我曾经写过一个简单的*.3ds文件的解析器,但是只能解析最基本的顶点、索引信息,且此解析器是仿照别人的C++代码改写的,设计的也不好,不方便扩展。

现在我重新设计实现了一个*.3ds文件的解析器,它能解析的Chunk类型更多,且容易扩展。以后需要解析更多类型的Chunk时比较简单。

+BIT祝威+悄悄在此留下版了个权的信息说:

下载

这个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)下载,由于文件比较大我就不上传了。

+BIT祝威+悄悄在此留下版了个权的信息说:

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树

 

实际上完整的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文件是没有分组索引这个玩意的。所以要特殊处理一下。

 

+BIT祝威+悄悄在此留下版了个权的信息说:

解析器设计思路

之前写的解析器中使用的思路是:首先根据偏移量和长度找到一个块的标识符,然后据此来判断它是什么块,遇到我们需要的块,就进一步读取,如果不需要,直接跳过这一块,读取下面的块。这没有用到面向对象的思想,只有面向过程编程。如果需要添加一个新的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     }

 

+BIT祝威+悄悄在此留下版了个权的信息说:

解析器输出:Chunk树

我们用TreeView控件来展示解析出来的Chunk树。

如果不想看那些未定义的Chunk类型,可以隐藏之。

如果需要,你可以将此Chunk树导出为文本格式:

 

+BIT祝威+悄悄在此留下版了个权的信息说:

从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 }
针对分组索引的渲染

 

+BIT祝威+悄悄在此留下版了个权的信息说:

验证分组索引的功能

上文中我们发现了分组索引的存在,根据它的内容推测了它的功能,现在来验证一下。我找到一个3ds文件,用A3dsViewer打开是这样的:

这个3ds文件附带多个贴图:

这个是树皮。

这是花盆里的石头。

这是花盆里的苔藓(某种绿色植物?)

这是盆景的红叶。

现在再用我制作的3DSViewer渲染看看:

整体上是对了,分组索引成功地将各个贴图附到了对应的三角形上。

但是花盆不应该是白的,这是某些光照没有解析的原因。

+BIT祝威+悄悄在此留下版了个权的信息说:

从Chunk树到modern OpenGL

有了legacy OpenGL探路,modern OpenGL的渲染就容易多了,这里暂时不详述。

 

+BIT祝威+悄悄在此留下版了个权的信息说:

总结

目前这个3ds解析器算是可用了,以后需要扩展时也很容易。如果能找到更多的3ds文件来测试,就能知道还需要解析哪些类型的Chunk了。

 

posted @ 2015-09-12 01:13  BIT祝威  阅读(4359)  评论(10编辑  收藏  举报
canvas start.

canvas end.