目录
介绍
我在求微分方程的项目中有点偏离了方向。我真的很想展示如何计算太阳系中行星的轨迹,并对广义相对论进行校正。如果只使用牛顿引力定律,有些行星实际上不会有正确的路径,因此您需要求解一些时间步长相对较大的非线性微分方程,这对于反向欧拉积分方法来说是完美的。
无论如何,我想以3D形式展示由此产生的行星,以便您可以看到轨道。但是不同的行星有不同的颜色和构成,所以为了将它们分开,我需要一些着色或等效ImageBrushes的纹理,如果发生这种情况的话。就在那时,我偶然发现了NASA的3D资源。它们以glTF文件格式(*.glb)存储了所有3D图像,您可以下载和使用,但是如何在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包使用。但是,这只会提取有关文件如何组织、在哪里等的信息。实际数据要么在二进制部分,要么在单独的文件中。
KhoronosGroup Github帐户上有很多资源,但它们主要用于C#以外的编程语言。
提取3D模型
任何3D模型都会有一些位置数据,这些数据通常用三角形索引进行组织,并且每个三角形都有一个给出其方向的法向量。此外,像我的情况一样,也可能有一些图像具有一些纹理坐标。
描述每个对象中使用的数据是在从glTF JSON中提取的Meshes对象中给出的。由于我只想显示一颗行星,因此每个文件只包含一个网格,但一般来说,与土星及其光环一样,每个glb文件可能有许多网格。但为了简单起见并显示原则,我排除了更困难的文件类型。
每个文件都有所谓的访问器,这些访问器将指向二进制文件中存储实际信息的位置。因此,在这里,我提取每个网格(在本例中为网格)的信息。
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查看器
将所有这些信息添加到ModelVisual3D中,可以用于在Viewport3d中显示它,这是相对简单的。
// 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>
我从这个网站上偷走了一些关于简单缩放和旋转的想法。
https://www.codeproject.com/Articles/5360022/Introduction-to-glTF-for-WPF-3D