WPF 3D glTF 入门






4.83/5 (6投票s)
读取 glTF 文件并显示简单 3D 图像的基础指南
引言
我在解决微分方程的项目上稍微分了神。我真的很想展示如何计算太阳系中行星的轨迹,并对广义相对论进行修正。如果只使用牛顿万有引力定律,一些行星实际上不会有正确的路径,因此你需要用相对较大的时间步长来求解一些非线性微分方程,这非常适合向后欧拉积分方法。
总之,我想用 3D 展示结果行星,这样你就可以看到轨道。但是不同的行星有不同的颜色和组成,所以为了区分它们,我需要一些颜色或等效的纹理的 ImageBrushes
(图像画刷)。 就在那时,我偶然发现了 美国宇航局的 3D 资源。它们将所有 3D 图像存储为 glTF 文件格式 (*.glb),你可以下载并使用,但是如何在 WPF 3D 中显示这些文件以及这些文件到底是什么?
我只打算用这个工具来获取一些非常简单的 3D 形状,你必须重写代码才能使其以一般方式工作。
背景
glTF 代表 graphics language Transmission Format(图形语言传输格式),似乎是存储和发送 3D 图形组件时每个人都在实施的新标准。当前版本的规范由 Khronos Group 维护,该组织还在其 Github 账户上发布了大量开发人员材料。
描述新标准 2.0 版本的交换格式的文档 在这里。 可以在 以下快速指南 pdf 文档 中查看该格式的快速指南。
该文档类型实际上很简单,摘自快速指南
因此,加载 *.glb 的代码实际上非常简单
// Load all byte arrays from the Binary file glTF version 2
using (var stream = File.Open(filename, FileMode.Open))
{
using (var reader = new BinaryReader(stream, Encoding.UTF8, false))
{
// Reading the initial data that determines the file type
Magic = reader.ReadUInt32();
Version = reader.ReadUInt32();
TotalFileLength = reader.ReadUInt32();
// Read the JSON data
JsonChuckLength = reader.ReadUInt32();
UInt32 chunckType = reader.ReadUInt32();
// Should be equal to JSON_hex 0x4E4F534A;
string hexValue = chunckType.ToString("X");
JSON_data = reader.ReadBytes((int)JsonChuckLength);
// Read the binary data
BinChuckLength = reader.ReadUInt32();
UInt32 chunckType2 = reader.ReadUInt32();
// Should be equal to BIN_hex 0x004E4942;
string hexValue2 = chunckType2.ToString("X");
BIN_data = reader.ReadBytes((int)BinChuckLength);
}
}
我们现在提取了 JSON 数据和二进制数据,分别位于两个不同的数组中。 然而,已经有一个工具用于从 JSON 中提取所有信息,该工具在 github 和 NuGet 包中均可用。 但是,这只会提取关于文件如何组织的信息,什么在哪里等等。 实际数据要么在二进制部分,要么在单独的文件中。
KhronosGroup Github 帐户上有很多资源,但它们主要用于 C# 以外的编程语言。
提取 3D 模型
任何 3D 模型都将有一些位置数据,这些数据通常使用三角形索引进行组织,并且这些三角形中的每一个都将有一个给出其方向的法线向量。 此外,也可能像我的情况一样,有带有某些纹理坐标的图像。
在从 glTF JSON 中提取的 Meshes
对象中描述了每个对象中使用的数据。 由于我只想显示一个行星,因此每个文件只包含一个网格,但一般来说,就像土星及其光环一样,每个 glb 文件可能包含多个网格。 但是为了简单起见并展示原理,我排除了更难的文件类型。
每个文件都将有所谓的 Accessors(访问器),这些访问器将指向二进制文件中存储实际信息的位置。 因此,在这里,我提取了每个网格(或在这种情况下是一个网格)的信息。
for (int i = 0; i < glTFFile.Accessors.Count(); i++)
{
Accessor CurrentAccessor = glTFFile.Accessors[i];
// Read the byte positions and offsets for each accessors
var BufferViewIndex = CurrentAccessor.BufferView;
BufferView BufferView = glTFFile.BufferViews[(int)BufferViewIndex];
var Offset = BufferView.ByteOffset;
var Length = BufferView.ByteLength;
// Check which type of accessor it is
string type = "";
if (AttrebutesIndex.ContainsKey(i))
type = AttrebutesIndex[i];
if (type == "POSITION")
{
// Used to scale all planets to +/- 1
float[] ScalingFactorForVariables = new float[3];
if (CurrentAccessor.Max == null)
ScalingFactorForVariables = new float[3] { 1.0f, 1.0f, 1.0f };
else
ScalingFactorForVariables = CurrentAccessor.Max;
// Upscaling factor
float UpscalingFactor = 1.5f;
Point3DCollection PointsPosisions = new Point3DCollection();
for (int n = Offset; n < Offset + Length; n += 4)
{
float x = BitConverter.ToSingle(BIN_data, n) /
ScalingFactorForVariables[0] * UpscalingFactor;
n += 4;
float y = BitConverter.ToSingle(BIN_data, n) /
ScalingFactorForVariables[1] * UpscalingFactor;
n += 4;
float z = BitConverter.ToSingle(BIN_data, n) /
ScalingFactorForVariables[2] * UpscalingFactor;
PointsPosisions.Add(new Point3D(x, y, z));
}
MaterialPoints = PointsPosisions;
}
else if (type == "NORMAL")
{
Vector3DCollection NormalsForPosisions = new Vector3DCollection();
for (int n = Offset; n < Offset + Length; n += 4)
{
float x = BitConverter.ToSingle(BIN_data, n);
n += 4;
float y = BitConverter.ToSingle(BIN_data, n);
n += 4;
float z = BitConverter.ToSingle(BIN_data, n);
NormalsForPosisions.Add(new Vector3D(x, y, z));
}
NormalPoints = NormalsForPosisions;
}
else if (type.Contains("TEXCOORD"))
{
// Assuming texture positions
PointCollection vec2 = new PointCollection();
for (int n = Offset; n < Offset + Length; n += 4)
{
double x = (double)BitConverter.ToSingle(BIN_data, n);
n += 4;
double y = (double)BitConverter.ToSingle(BIN_data, n);
vec2.Add(new Point(x, y));
}
TexturePoints = vec2;
}
else
{
if (CurrentAccessor.ComponentType == Accessor.ComponentTypeEnum.UNSIGNED_SHORT)
{
for (int n = Offset; n < Offset + Length; n += 2)
{
UInt16 TriangleItem = BitConverter.ToUInt16(BIN_data, n);
Indecies.Add((Int32)TriangleItem);
}
}
}
}
如果您有纹理坐标,还将有可以从二进制部分或单独文件中加载的图像。
foreach (glTFLoader.Schema.Image item in glTFFile.Images)
{
//var ImageType = item.MimeType;
int BufferViewIndex = (int)item.BufferView;
BufferView BufferView = glTFFile.BufferViews[BufferViewIndex];
var Offset = BufferView.ByteOffset;
var Length = BufferView.ByteLength;
// Copy the relevant data from binary part
byte[] ImageBytes = new byte[Length];
Array.Copy(BIN_data, Offset, ImageBytes, 0, Length);
// Convert to image
MemoryStream ms = new MemoryStream(ImageBytes);
BitmapImage Img = new BitmapImage();
Img.BeginInit();
Img.StreamSource = ms;
Img.EndInit();
Images.Add(Img);
}
生成 WPF 3D glTF 查看器
将所有这些信息添加到可用于在 Viewport3d
中显示的 ModelVisual3D
相对简单。
// Construct the WPF 3D ViewPort model
Model3D = new MeshGeometry3D();
Model3D.TriangleIndices = Indecies;
Model3D.Positions = MaterialPoints;
Model3D.Normals = NormalPoints;
Model3D.TextureCoordinates = TexturePoints;
// Geometry model
GeoModel3D.Geometry = Model3D;
GeoModel3D.Material = new DiffuseMaterial() { Brush = new ImageBrush(Images[0]) };
// ModelVisual3D for showing this component
Visualisation.Content = GeoModel3D;
Viewport3D
非常简单,我所需要的只是定位相机并给场景添加一些光照。 所有行星都位于原点 (0,0,0) 。
<Viewport3D Name="viewport3D1" Width="400" Height="400">
<Viewport3D.Camera>
<PerspectiveCamera x:Name="camMain"
Position="6 5 4" LookDirection="-6 -5 -4">
</PerspectiveCamera>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight x:Name="dirLightMain" Direction="-1,-1,-1">
</DirectionalLight>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
我从 这个网站 上窃取了一些关于简单缩放和旋转的想法。
历史
- 2023 年 5 月 2 日:初始版本