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

2022-12-15,,,,

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个根结点的树状结构。

 0x4D4D // Main Chunk
├─ 0x0002 // M3D Version
├─ 0x3D3D // 3D Editor Chunk
│ ├─ 0x4000 // Object Block
│ │ ├─ 0x4100 // Triangular Mesh
│ │ │ ├─ 0x4110 // Vertices List
│ │ │ ├─ 0x4120 // Faces Description
│ │ │ │ ├─ 0x4130 // Faces Material
│ │ │ │ └─ 0x4150 // Smoothing Group List
│ │ │ ├─ 0x4140 // Mapping Coordinates List
│ │ │ └─ 0x4160 // Local Coordinates System
│ │ ├─ 0x4600 // Light
│ │ │ └─ 0x4610 // Spotlight
│ │ └─ 0x4700 // Camera
│ └─ 0xAFFF // Material Block
│ ├─ 0xA000 // Material Name
│ ├─ 0xA010 // Ambient Color
│ ├─ 0xA020 // Diffuse Color
│ ├─ 0xA030 // Specular Color
│ ├─ 0xA200 // Texture Map 1
│ ├─ 0xA230 // Bump Map
│ └─ 0xA220 // Reflection Map
│ │ // Sub Chunks For Each Map
│ ├─ 0xA300 // Mapping Filename
│ └─ 0xA351 // Mapping Parameters
└─ 0xB000 // Keyframer Chunk
├─ 0xB002 // Mesh Information Block
├─ 0xB007 // Spot Light Information Block
└─ 0xB008 // Frames (Start and End)
├─ 0xB010 // Object Name
├─ 0xB013 // Object Pivot Point
├─ 0xB020 // Position Track
├─ 0xB021 // Rotation Track
├─ 0xB022 // Scale Track
└─ 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方法即可实现自己的解析过程。

     public abstract class ChunkBase
{
public ChunkBase Parent;
public List<ChunkBase> Childern; public uint Length;
public uint BytesRead; public ChunkBase()
{
this.Childern = new List<ChunkBase>();
} internal virtual void Process(ParsingContext context)
{
var chunk = this;
var reader = context.reader; while (chunk.BytesRead < chunk.Length)
{
ChunkBase child = reader.ReadChunk();
child.Parent = this;
this.Childern.Add(child); child.Process(context); chunk.BytesRead += child.BytesRead;
}
}
}

数据字典

各个类型的Chunk都用一个具体的class类型表达,为了方便这些class类型与用ushort表达的的Chunk类型相互转换,我们需要2个字典。

     public static partial class ChunkBaseHelper
{ private static readonly Dictionary<Type, ushort> chunkTypeDict = new Dictionary<Type, ushort>();
private static readonly Dictionary<ushort, Type> chunkIDDict = new Dictionary<ushort, Type>(); /// <summary>
/// 开发者必须了解的东西。
/// </summary>
static ChunkBaseHelper()
{
chunkTypeDict.Add(typeof(MainChunk), 0x4D4D);
{
chunkTypeDict.Add(typeof(VersionChunk), 0x0002);
chunkTypeDict.Add(typeof(_3DEditorChunk), 0x3D3D);
{
chunkTypeDict.Add(typeof(ObjectBlockChunk), 0x4000);
{
chunkTypeDict.Add(typeof(TriangularMeshChunk), 0x4100);
{
chunkTypeDict.Add(typeof(VerticesListChunk), 0x4110);
chunkTypeDict.Add(typeof(FacesDescriptionChunk), 0x4120);
{
chunkTypeDict.Add(typeof(FacesMaterialChunk), 0x4130);
chunkTypeDict.Add(typeof(SmoothingGroupListChunk), 0x4150);
}
chunkTypeDict.Add(typeof(MappingCoordinatesListChunk), 0x4140);
chunkTypeDict.Add(typeof(LocalCoordinatesSystemChunk), 0x4160);
}
chunkTypeDict.Add(typeof(LightChunk), 0x4600);
{
chunkTypeDict.Add(typeof(SpotlightChunk), 0x4610);
}
chunkTypeDict.Add(typeof(CameraChunk), 0x4700);
}
chunkTypeDict.Add(typeof(MaterialBlockChunk), 0xAFFF);
{
chunkTypeDict.Add(typeof(MaterialNameChunk), 0xA000);
chunkTypeDict.Add(typeof(AmbientColorChunk), 0xA010);
chunkTypeDict.Add(typeof(DiffuseColorChunk), 0xA020);
chunkTypeDict.Add(typeof(SpecularColorChunk), 0xA030);
chunkTypeDict.Add(typeof(MatShininessChunk), 0xA040);
chunkTypeDict.Add(typeof(TextureMapChunk), 0xA200);
chunkTypeDict.Add(typeof(BumpMapChunk), 0xA230);
chunkTypeDict.Add(typeof(ReflectionMapChunk), 0xA220);
{
chunkTypeDict.Add(typeof(MappingFilenameChunk), 0xA300);
chunkTypeDict.Add(typeof(MappingParametersChunk), 0xA351);
}
}
}
chunkTypeDict.Add(typeof(KeyframeChunk), 0xB000);
{
chunkTypeDict.Add(typeof(MeshInformationBlockChunk), 0xB002);
chunkTypeDict.Add(typeof(SpotLightInformationBlockChunk), 0xB007);
chunkTypeDict.Add(typeof(FramesChunk), 0xB008);
{
chunkTypeDict.Add(typeof(ObjectNameChunk), 0xB010);
chunkTypeDict.Add(typeof(ObjectPivotPointChunk), 0xB013);
chunkTypeDict.Add(typeof(PositionTrackChunk), 0xB020);
chunkTypeDict.Add(typeof(RotationTrackChunk), 0xB021);
chunkTypeDict.Add(typeof(ScaleTrackChunk), 0xB022);
chunkTypeDict.Add(typeof(HierarchyPositionChunk), 0xB030);
}
}
} chunkIDDict.Add(0x4D4D, typeof(MainChunk));
{
chunkIDDict.Add(0x0002, typeof(VersionChunk));
chunkIDDict.Add(0x3D3D, typeof(_3DEditorChunk));
{
chunkIDDict.Add(0x4000, typeof(ObjectBlockChunk));
{
chunkIDDict.Add(0x4100, typeof(TriangularMeshChunk));
{
chunkIDDict.Add(0x4110, typeof(VerticesListChunk));
chunkIDDict.Add(0x4120, typeof(FacesDescriptionChunk));
{
chunkIDDict.Add(0x4130, typeof(FacesMaterialChunk));
chunkIDDict.Add(0x4150, typeof(SmoothingGroupListChunk));
}
chunkIDDict.Add(0x4140, typeof(MappingCoordinatesListChunk));
chunkIDDict.Add(0x4160, typeof(LocalCoordinatesSystemChunk));
}
chunkIDDict.Add(0x4600, typeof(LightChunk));
{
chunkIDDict.Add(0x4610, typeof(SpotlightChunk));
}
chunkIDDict.Add(0x4700, typeof(CameraChunk));
}
chunkIDDict.Add(0xAFFF, typeof(MaterialBlockChunk));
{
chunkIDDict.Add(0xA000, typeof(MaterialNameChunk));
chunkIDDict.Add(0xA010, typeof(AmbientColorChunk));
chunkIDDict.Add(0xA020, typeof(DiffuseColorChunk));
chunkIDDict.Add(0xA030, typeof(SpecularColorChunk));
chunkIDDict.Add(0xA040, typeof(MatShininessChunk));
chunkIDDict.Add(0xA200, typeof(TextureMapChunk));
chunkIDDict.Add(0xA230, typeof(BumpMapChunk));
chunkIDDict.Add(0xA220, typeof(ReflectionMapChunk));
{
chunkIDDict.Add(0xA300, typeof(MappingFilenameChunk));
chunkIDDict.Add(0xA351, typeof(MappingParametersChunk));
}
}
}
chunkIDDict.Add(0xB000, typeof(KeyframeChunk));
{
chunkIDDict.Add(0xB002, typeof(MeshInformationBlockChunk));
chunkIDDict.Add(0xB007, typeof(SpotLightInformationBlockChunk));
chunkIDDict.Add(0xB008, typeof(FramesChunk));
{
chunkIDDict.Add(0xB010, typeof(ObjectNameChunk));
chunkIDDict.Add(0xB013, typeof(ObjectPivotPointChunk));
chunkIDDict.Add(0xB020, typeof(PositionTrackChunk));
chunkIDDict.Add(0xB021, typeof(RotationTrackChunk));
chunkIDDict.Add(0xB022, typeof(ScaleTrackChunk));
chunkIDDict.Add(0xB030, typeof(HierarchyPositionChunk));
}
}
}
}
}

数据字典

未定义的Chunk

3ds文件有上千种Chunk,我们暂时不会都解析出来(也没必要全解析出来)。所以我们用一个“未定义的Chunk”类型来代表那些我们不想解析的Chunk类型。

     /// <summary>
/// 3ds文件有上千种Chunk,我们暂时不会都解析出来(也没必要全解析出来)。所以我们用一个“未定义的Chunk”类型来代表那些我们不想解析的Chunk类型。
/// </summary>
public class UndefinedChunk : ChunkBase
{
public ushort ID;
public bool IsChunk { get; private set; } public UndefinedChunk()
{
this.IsChunk = true;
} public override string ToString()
{
return string.Format("{0}(0x{1:X4}), position: {2}, length: {3}, read bytes: {4}",
this.IsChunk ? "Unknown Chunk" : "Fake Chunk", ID, Position, Length, BytesRead);
} internal override void Process(ParsingContext context)
{
var chunk = this;
var reader = context.reader;
var parent = this.Parent; uint length = this.Length - this.BytesRead; if ((parent != null))
{
var another = parent.Length - parent.BytesRead - this.BytesRead;
length = Math.Min(length, another);
} reader.BaseStream.Position += length;
chunk.BytesRead += length;
if (chunk.Length != chunk.BytesRead)
{
chunk.Length = chunk.BytesRead;
this.IsChunk = false;
}
}
}

注意:这里获取到的UndefinedChunk对象,不一定代表真的有这样一个未被解析的Chunk,它也可能是其父Chunk的一部分数据内容。所以,我们要结合这里的another值来判断到底应该继续读取多少字节,并且修补好可能出错的chunk.Length。

读出一个Chunk的扩展方法

每次获取一个Chunk对象时,都是借助BinaryReader得到Chunk类型和长度的,所以我们给它一个扩展方法,用于“读出一个Chunk”。

     public static partial class ChunkBaseHelper
{
public static ChunkBase ReadChunk(this BinaryReader reader)
{
// 2 byte ID
ushort id = reader.ReadUInt16();
// 4 byte length
uint length = reader.ReadUInt32();
// 2 + 4 = 6
uint bytesRead = ; Type type;
if (chunkIDDict.TryGetValue(id, out type))
{
object obj = Activator.CreateInstance(type);
ChunkBase result = obj as ChunkBase;
//result.ID = id;//不再需要记录ID,此对象的类型就指明了它的ID。
result.Length = length;
result.BytesRead = bytesRead;
return result;
}
else
{
return new UndefinedChunk() { ID = id, Length = length, BytesRead = bytesRead, };
}
}
}

获取Chunk类型的ushort值

得到一个Chunk对象后,可能会需要获取此对象代表的Chunk类型。

     public static partial class ChunkBaseHelper
{
public static ushort GetID(this ChunkBase chunk)
{
ushort value; if (chunk is UndefinedChunk)
{
value = (chunk as UndefinedChunk).ID;
}
else
{
Type type = chunk.GetType();
value = chunkTypeDict[type];//如果此处不存在此type的key,说明static构造函数需要添加此类型的字典信息。
} return value;
}
}

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

解析器输出:Chunk树

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

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

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

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

从Chunk树到legacy OpenGL

Dumper

已经得到了Chunk树,下面需要得到可用于OpenGL渲染的模型。这实际上是一个语义分析和生成中间代码的过程。以根结点MainChunk为例:

     public static partial class ChunkDumper
{
public static void Dump(this MainChunk chunk, out ThreeDSModel4LegacyOpenGL model)
{
model = new ThreeDSModel4LegacyOpenGL(); foreach (var item in chunk.Children)
{
if(item is VersionChunk)
{
(item as VersionChunk).Dump(model);
}
else if(item is _3DEditorChunk)
{
(item as _3DEditorChunk).Dump(model);
}
else if (item is KeyframeChunk)
{
(item as KeyframeChunk).Dump(model);
}
else if(!(item is UndefinedChunk))
{
throw new NotImplementedException(string.Format(
"not dumper implemented for {0}", item.GetType()));
}
}
}
}

我们为每个Chunk类型都编写一个Dumper,在各个Dump过程中收集需要的信息(顶点位置、UV、贴图文件名、材质、光照等),汇总到一个ThreeDSModel4LegacyOpenGL对象,这个对象就可以用来渲染图形了。

渲染

根据上文对分组索引的推测,我给出如下的渲染过程。

     public class ThreeDSModel4LegacyOpenGL
{
public List<ThreeDSMesh4LegacyOpenGL> Entities = new List<ThreeDSMesh4LegacyOpenGL>();
public Dictionary<string, ThreeDSMaterial4LegacyOpenGL> MaterialDict = new Dictionary<string, ThreeDSMaterial4LegacyOpenGL>(); public void Render()
{
foreach (ThreeDSMesh4LegacyOpenGL mesh in Entities)
{
mesh.Render(this);
}
}
}
public class ThreeDSMesh4LegacyOpenGL
{
public List<Tuple<string, ushort[]>> usingMaterialIndexesList = new List<Tuple<string, ushort[]>>();
// TODO: OO this
// fields should be private
// constructor with verts and faces
// normalize in ctor //public ThreeDSMaterial material = new ThreeDSMaterial();
//public string UsesMaterial; // The stored vertices
public Vector[] Vertexes; // The calculated normals
public Vector[] normals; // The indices of the triangles which point to vertices
public Triangle[] TriangleIndexes; // The coordinates which map the texture onto the entity
public TexCoord[] TexCoords; bool normalized = false;
public ushort[] UsesIndexes; public void Render(ThreeDSModel4LegacyOpenGL model)
{
if (TriangleIndexes == null) return; // Draw every triangle in the entity
foreach (var item in this.usingMaterialIndexesList)
{
var material = model.MaterialDict[item.Item1]; GL.Materialfv(GL.GL_FRONT_AND_BACK, GL.GL_AMBIENT, material.Ambient);
GL.Materialfv(GL.GL_FRONT_AND_BACK, GL.GL_DIFFUSE, material.Diffuse);
GL.Materialfv(GL.GL_FRONT_AND_BACK, GL.GL_SPECULAR, material.Specular);
GL.Materialf(GL.GL_FRONT_AND_BACK, GL.GL_SHININESS, material.Shininess); Texture2D[] textures = new Texture2D[] { material.GetTexture(), material.GetBumpTexture(), material.GetReflectionTexture(), };
bool drawn = false;
foreach (var texture in textures)
{
if (!(drawn && texture == null)) // 如果没有贴图,就只画一次。
{
if (texture != null)
{
GL.Enable(GL.GL_TEXTURE_2D);
texture.Bind();
} DrawTriangles(item, texture); if (texture != null)
{
texture.Unbind();
GL.Disable(GL.GL_TEXTURE_2D);
}
} drawn = true;
}
}
} private void DrawTriangles(Tuple<string, ushort[]> usingMaterialIndexes, Texture2D texture)
{
GL.Begin(GL.GL_TRIANGLES);
foreach (var usingIndex in usingMaterialIndexes.Item2)
{
Triangle tri = this.TriangleIndexes[usingIndex];
// Vertex 1
if (normalized)
{
var normal = this.normals[tri.vertex1];
GL.Normal3d(normal.X, normal.Y, normal.Z);
}
if (texture != null)
{
var texCoord = this.TexCoords[tri.vertex1];
GL.TexCoord2f(texCoord.U, texCoord.V);
}
{
var vertex = this.Vertexes[tri.vertex1];
GL.Vertex3d(vertex.X, vertex.Y, vertex.Z);
} // Vertex 2
if (normalized)
{
var normal = this.normals[tri.vertex2];
GL.Normal3d(normal.X, normal.Y, normal.Z);
}
if (texture != null)
{
var texCoord = this.TexCoords[tri.vertex2];
GL.TexCoord2f(texCoord.U, texCoord.V);
}
{
var vertex = this.Vertexes[tri.vertex2];
GL.Vertex3d(vertex.X, vertex.Y, vertex.Z);
} // Vertex 3
if (normalized)
{
var normal = this.normals[tri.vertex3];
GL.Normal3d(normal.X, normal.Y, normal.Z);
}
if (texture != null)
{
var texCoord = this.TexCoords[tri.vertex3];
GL.TexCoord2f(texCoord.U, texCoord.V);
}
{
var vertex = this.Vertexes[tri.vertex3];
GL.Vertex3d(vertex.X, vertex.Y, vertex.Z);
}
}
GL.End();
}
}

针对分组索引的渲染

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

验证分组索引的功能

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

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

这个是树皮。

这是花盆里的石头。

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

这是盆景的红叶。

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

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

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

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

从Chunk树到modern OpenGL

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

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

总结

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

CSharpGL(5)解析3DS文件并用CSharpGL渲染的相关教程结束。

《CSharpGL(5)解析3DS文件并用CSharpGL渲染.doc》

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