生成 3D 制造格式包

本指南介绍 3D 制造格式 (3MF) 文件类型的结构,以及如何使用 Windows.Graphics.Printing3D API 来创建和操作它。

重要的 API

什么是 3D 制造格式?

3MF 是一组约定,用于使用 XML 描述用于制造 (3D 打印) 的 3D 模型的外观和结构。 它定义了一组与 3D 制造设备 (必需和可选) 及其关系的部件。 遵循 3MF 的数据集可以另存为扩展名为 .3mf 的文件。

Windows.Graphics.Printing3D 命名空间中的 Printing3D3MFPackage 类类似于单个 .3mf 文件,而其他类映射到 .3mf 文件中的特定 XML 元素。 本指南介绍如何以编程方式创建和设置 3MF 文档的每个main部分、如何使用 3MF 材料扩展,以及如何转换 Printing3D3MFPackage 对象并将其另存为 .3mf 文件。 有关 3MF 的标准或 3MF Materials Extension 的详细信息,请参阅 3MF 规范

3MF 结构中的核心类

Printing3D3MFPackage 类表示完整的 3MF 文档,并且 3MF 文档的核心是其模型部分,由 Printing3DModel 类表示。 有关 3D 模型的大多数信息是通过设置 Printing3DModel 类的属性及其基础类的属性来存储的。

var localPackage = new Printing3D3MFPackage();
var model = new Printing3DModel();
// specify scaling units for model data
model.Unit = Printing3DModelUnit.Millimeter;

元数据

3MF 文档的模型部分可以将元数据以存储在 Metadata 属性中的字符串的键/值对的形式保存。 有预定义的元数据,但自定义对可以作为扩展 (3MF 规范) 中更详细地描述的一部分添加。 包的接收方 (3D 制造设备) 决定是否以及如何处理元数据,但最好在 3MF 包中包含尽可能多的信息。

model.Metadata.Add("Title", "Cube");
model.Metadata.Add("Designer", "John Smith");
model.Metadata.Add("CreationDate", "1/1/2016");

网格数据

在本指南中,网格是由一组顶点 (构造的三维几何体,尽管它不必显示为单个实心) 。 网格部件由 Printing3DMesh 类表示。 有效的网格对象必须包含有关所有顶点的位置以及存在于某些顶点集之间的所有三角形面的信息。

以下方法将顶点添加到网格中,然后为它们提供 3D 空间中的位置。

private async Task GetVerticesAsync(Printing3DMesh mesh) {
    Printing3DBufferDescription description;

    description.Format = Printing3DBufferFormat.Printing3DDouble;

    // have 3 xyz values
    description.Stride = 3;

    // have 8 vertices in all in this mesh
    mesh.CreateVertexPositions(sizeof(double) * 3 * 8);
    mesh.VertexPositionsDescription = description;

    // set the locations (in 3D coordinate space) of each vertex
    using (var stream = mesh.GetVertexPositions().AsStream()) {
        double[] vertices =
        {
            0, 0, 0,
            10, 0, 0,
            0, 10, 0,
            10, 10, 0,
            0, 0, 10,
            10, 0, 10,
            0, 10, 10,
            10, 10, 10,
        };

        // convert vertex data to a byte array
        byte[] vertexData = vertices.SelectMany(v => BitConverter.GetBytes(v)).ToArray();

        // write the locations to each vertex
        await stream.WriteAsync(vertexData, 0, vertexData.Length);
    }
    // update vertex count: 8 vertices in the cube
    mesh.VertexCount = 8;
}

下一个方法定义要跨这些顶点绘制的所有三角形:

private static async Task SetTriangleIndicesAsync(Printing3DMesh mesh) {

    Printing3DBufferDescription description;

    description.Format = Printing3DBufferFormat.Printing3DUInt;
    // 3 vertex indices
    description.Stride = 3;
    // 12 triangles in all in the cube
    mesh.IndexCount = 12;

    mesh.TriangleIndicesDescription = description;

    // allocate space for 12 triangles
    mesh.CreateTriangleIndices(sizeof(UInt32) * 3 * 12);

    // get a datastream of the triangle indices (should be blank at this point)
    var stream2 = mesh.GetTriangleIndices().AsStream();
    {
        // define a set of triangle indices: each row is one triangle. The values in each row
        // correspond to the index of the vertex. 
        UInt32[] indices =
        {
            1, 0, 2,
            1, 2, 3,
            0, 1, 5,
            0, 5, 4,
            1, 3, 7,
            1, 7, 5,
            2, 7, 3,
            2, 6, 7,
            0, 6, 2,
            0, 4, 6,
            6, 5, 7,
            4, 5, 6,
        };
        // convert index data to byte array
        var vertexData = indices.SelectMany(v => BitConverter.GetBytes(v)).ToArray();
        var len = vertexData.Length;
        // write index data to the triangle indices stream
        await stream2.WriteAsync(vertexData, 0, vertexData.Length);
    }

}

注意

所有三角形都必须以逆时针方向的顺序(当从网格对象外部查看三角形时)定义其索引,以便使它们的面法线矢量指向外部。

当 Printing3DMesh 对象包含顶点和三角形的有效集时,应将其添加到模型的 Meshes 属性中。 包中的所有 Printing3DMesh 对象都必须存储在 Printing3DModel 类的 Meshes 属性下,如下所示。

// add the mesh to the model
model.Meshes.Add(mesh);

创建材料

3D 模型可以保留多个材料的数据。 此约定旨在充分利用可在单个打印作业中使用多个材料的 3D 制造设备。 还有多 种类型的 材料组,每个材料组都能够支持许多不同的单独材料。

每个材料组必须具有唯一的引用 ID 号,并且该组中的每个材料还必须具有唯一的 ID。 然后,模型中的不同网格对象可以引用材料。

此外,每个网格上的单个三角形可以指定不同的材料,甚至可以在一个三角形中表示不同的材料,每个三角形顶点分配有不同的材料,并且将表面材料计算为它们之间的梯度。

首先,我们将演示如何在各自的材料组中创建不同类型的材料,并将其存储为模型对象上的资源。 然后,我们将为单个网格和单个三角形分配不同的材料。

基本材料

默认材料类型为基本材料,该类型具有颜色材料值(如下所述)和旨在指定要使用的材料类型的名称属性。

// add material group
// all material indices need to start from 1: 0 is a reserved id
// create new base materialgroup with id = 1
var baseMaterialGroup = new Printing3DBaseMaterialGroup(1);

// create color objects
// 'A' should be 255 if alpha = 100%
var darkBlue = Windows.UI.Color.FromArgb(255, 20, 20, 90);
var orange = Windows.UI.Color.FromArgb(255, 250, 120, 45);
var teal = Windows.UI.Color.FromArgb(255, 1, 250, 200);

// create new ColorMaterials, assigning color objects
var colrMat = new Printing3DColorMaterial();
colrMat.Color = darkBlue;

var colrMat2 = new Printing3DColorMaterial();
colrMat2.Color = orange;

var colrMat3 = new Printing3DColorMaterial();
colrMat3.Color = teal;

// setup new materials using the ColorMaterial objects
// set desired material type in the Name property
var baseMaterial = new Printing3DBaseMaterial {
    Name = Printing3DBaseMaterial.Pla,
    Color = colrMat
};

var baseMaterial2 = new Printing3DBaseMaterial {
    Name = Printing3DBaseMaterial.Abs,
    Color = colrMat2
};

// add base materials to the basematerialgroup

// material group index 0
baseMaterialGroup.Bases.Add(baseMaterial);
// material group index 1
baseMaterialGroup.Bases.Add(baseMaterial2);

// add material group to the basegroups property of the model
model.Material.BaseGroups.Add(baseMaterialGroup);

注意

 3D 制造设备将确定哪些可用物理材料映射到存储在 3MF 中的哪些虚拟材料元素。 材料映射不一定是 1:1。 如果 3D 打印机仅使用一种材料,它将在该材料中打印整个模型,而不考虑为哪些对象或人脸分配了不同的材料。

颜色材料

颜色材料类似于基本材料,但它们不包含名称。 因此,它们不会提供有关计算机应使用哪些材料类型的说明。 它们仅保留颜色数据,并允许计算机选择材料类型 (计算机可能会提示用户选择) 。 在下面的示例中 colrMat ,前一方法中的对象单独使用。

// add ColorMaterials to the Color Material Group (with id 2)
var colorGroup = new Printing3DColorMaterialGroup(2);

// add the previous ColorMaterial objects to this ColorMaterialGroup
colorGroup.Colors.Add(colrMat);
colorGroup.Colors.Add(colrMat2);
colorGroup.Colors.Add(colrMat3);

// add colorGroup to the ColorGroups property on the model
model.Material.ColorGroups.Add(colorGroup);

复合材料

复合材料 指示制造设备使用不同 基材料的均匀混合物。 每个复合材料组都必须仅引用一个要从中抽取成分的基本材料组。 此外,此组中要提供 的基本材料 必须列在 材料索引 列表中,每个 复合材料 在指定比率时将引用该列表 (每个 复合材料基材料) 的比例。

// CompositeGroups
// create new composite material group with id = 3
var compositeGroup = new Printing3DCompositeMaterialGroup(3);

// indices point to base materials in BaseMaterialGroup with id =1
compositeGroup.MaterialIndices.Add(0);
compositeGroup.MaterialIndices.Add(1);

// create new composite materials
var compMat = new Printing3DCompositeMaterial();
// fraction adds to 1.0
compMat.Values.Add(0.2); // .2 of first base material in BaseMaterialGroup 1
compMat.Values.Add(0.8); // .8 of second base material in BaseMaterialGroup 1

var compMat2 = new Printing3DCompositeMaterial();
// fraction adds to 1.0
compMat2.Values.Add(0.5);
compMat2.Values.Add(0.5);

var compMat3 = new Printing3DCompositeMaterial();
// fraction adds to 1.0
compMat3.Values.Add(0.8);
compMat3.Values.Add(0.2);

var compMat4 = new Printing3DCompositeMaterial();
// fraction adds to 1.0
compMat4.Values.Add(0.4);
compMat4.Values.Add(0.6);

// add composites to group
compositeGroup.Composites.Add(compMat);
compositeGroup.Composites.Add(compMat2);
compositeGroup.Composites.Add(compMat3);
compositeGroup.Composites.Add(compMat4);

// add group to model
model.Material.CompositeGroups.Add(compositeGroup);

纹理坐标材料

3MF 支持使用 2D 图形为 3D 模型的图面上色。 通过此方式,该模型可以在每个三角形面上传达更多的颜色数据(与每个三角形顶点只有一个颜色值相反)。 与 颜色材料一样,纹理坐标材料仅传达颜色数据。 若要使用 2D 纹理,必须先声明纹理资源。

注意

纹理数据属于 3MF 程序包本身,而不属于程序包内的模型部件。

// texture resource setup
Printing3DTextureResource texResource = new Printing3DTextureResource();
// name conveys the path within the 3MF document
texResource.Name = "/3D/Texture/msLogo.png";

// in this case, we reference texture data in the sample appx, convert it to 
// an IRandomAccessStream, and assign it as the TextureData
Uri texUri = new Uri("ms-appx:///Assets/msLogo.png");
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(texUri);
IRandomAccessStreamWithContentType iRandomAccessStreamWithContentType = await file.OpenReadAsync();
texResource.TextureData = iRandomAccessStreamWithContentType;
// add this testure resource to the 3MF Package
localPackage.Textures.Add(texResource);

// assign this texture resource to a Printing3DModelTexture
var modelTexture = new Printing3DModelTexture();
modelTexture.TextureResource = texResource;

接下来,我们必须填写 Texture3Coord 材料。 其中每一个都引用一个纹理资源,并在图像上指定一个特定的点(在 UV 坐标中)。

// texture2Coord Group
// create new Texture2CoordMaterialGroup with id = 4
var tex2CoordGroup = new Printing3DTexture2CoordMaterialGroup(4);

// create texture materials:
// set up four tex2coordmaterial objects with four (u,v) pairs, 
// mapping to each corner of the image:

var tex2CoordMaterial = new Printing3DTexture2CoordMaterial();
tex2CoordMaterial.U = 0.0;
tex2CoordMaterial.V = 1.0;
tex2CoordGroup.Texture2Coords.Add(tex2CoordMaterial);

var tex2CoordMaterial2 = new Printing3DTexture2CoordMaterial();
tex2CoordMaterial2.U = 1.0;
tex2CoordMaterial2.V = 1.0;
tex2CoordGroup.Texture2Coords.Add(tex2CoordMaterial2);

var tex2CoordMaterial3 = new Printing3DTexture2CoordMaterial();
tex2CoordMaterial3.U = 0.0;
tex2CoordMaterial3.V = 0.0;
tex2CoordGroup.Texture2Coords.Add(tex2CoordMaterial3);

var tex2CoordMaterial4 = new Printing3DTexture2CoordMaterial();
tex2CoordMaterial4.U = 1.0;
tex2CoordMaterial4.V = 0.0;
tex2CoordGroup.Texture2Coords.Add(tex2CoordMaterial4);

// add our Printing3DModelTexture to the Texture property of the group
tex2CoordGroup.Texture = modelTexture;

// add metadata about the texture so that u,v values can be used
model.Metadata.Add("tex4", "/3D/Texture/msLogo.png");
// add group to groups on the model's material
model.Material.Texture2CoordGroups.Add(tex2CoordGroup);

将材料映射到面

为了指定将哪些材料映射到每个三角形上的哪些顶点,需要对模型的网格对象执行更多工作, (如果模型包含多个网格,则它们必须分别) 分配其材料。 如上所述,材料按顶点、按三角形分配。 以下示例演示如何输入和解释此信息。

private static async Task SetMaterialIndicesAsync(Printing3DMesh mesh) {
    // declare a description of the material indices
    Printing3DBufferDescription description;
    description.Format = Printing3DBufferFormat.Printing3DUInt;
    // 4 indices for material description per triangle
    description.Stride = 4;
    // 12 triangles total
    mesh.IndexCount = 12;
    mesh.TriangleMaterialIndicesDescription = description;

    // create space for storing this data
    mesh.CreateTriangleMaterialIndices(sizeof(UInt32) * 4 * 12);

    {
        // each row is a triangle face (in the order they were created)
        // first column is the id of the material group, last 3 columns show which material id (within that group)
        // maps to each triangle vertex (in the order they were listed when creating triangles)
        UInt32[] indices =
        {
            // base materials:
            // in  the BaseMaterialGroup (id=1), the BaseMaterial with id=0 will be applied to these triangle vertices
            1, 0, 0, 0, 
            1, 0, 0, 0,
            // color materials:
            // in the ColorMaterialGroup (id=2), the ColorMaterials with these ids will be applied to these triangle vertices
            2, 1, 1, 1,
            2, 1, 1, 1,
            2, 0, 0, 0,
            2, 0, 0, 0,
            2, 0, 1, 2,
            2, 1, 0, 2,
            // composite materials:
            // in the CompositeMaterialGroup (id=3), the CompositeMaterial with id=0 will be applied to these triangles
            3,0,0,0,
            3,0,0,0,
            // texture materials:
            // in the Texture2CoordMaterialGroup (id=4), each texture coordinate is mapped to the appropriate vertex on these
            // two adjacent triangle faces, so that the square face they create displays the original rectangular image
            4, 0, 3, 1,
            4, 2, 3, 0,
        };

        // get the current (unassigned) vertex data as a stream and write our new 'indices' data to it.
        var stream = mesh.GetTriangleMaterialIndices().AsStream();
        var vertexData = indices.SelectMany(v => BitConverter.GetBytes(v)).ToArray();
        var len = vertexData.Length;
        await stream.WriteAsync(vertexData, 0, vertexData.Length);
    }
}

组件和版本

组件结构允许用户在可打印的 3D 模型中放置多个网格对象。 Printing3DComponent 对象包含单个网格和对其他组件的引用列表。 这实际上是 Printing3DComponentWithMatrix 对象的列表。 每个 Printing3DComponentWithMatrix 对象都包含 一个 Printing3DComponent 和一个应用于 Printing3DComponent 的网格和包含组件的转换矩阵。

例如,汽车的模型可能由承载车身网格的“车身”Printing3DComponent 组成。 然后,“Body”组件可能包含对四个不同的 Printing3DComponentWithMatrix 对象的引用,这些对象都引用相同的 Printing3DComponent ,而“Wheel”网格可能包含四个不同的转换矩阵, (将车轮映射到汽车车身) 上的四个不同位置。 在此方案中,“车身”网格和“车轮”网格各自只需存储一次,即使最终产品总共会展示五个网格。

所有 Printing3DComponent 对象都必须直接在模型的 Components 属性中引用。 要在打印作业中使用的单个特定组件存储在 Build 属性中。

// create new component
Printing3DComponent component = new Printing3DComponent();

// assign mesh to the component's mesh
component.Mesh = mesh;

// add component to the model's list of all used components
// a model can have references to multiple components
model.Components.Add(component);

// create the transform matrix
var componentWithMatrix = new Printing3DComponentWithMatrix();
// assign component to this componentwithmatrix
componentWithMatrix.Component = component;

// create an identity matrix
var identityMatrix = Matrix4x4.Identity;

// use the identity matrix as the transform matrix (no transformation)
componentWithMatrix.Matrix = identityMatrix;

// add component to the build property.
model.Build.Components.Add(componentWithMatrix);

保存包

现在我们具有包含已定义材料和组件的模型,我们可以将其保存到程序包。

// save the model to the package:
await localPackage.SaveModelToPackageAsync(model);
// get the model stream
var modelStream = localPackage.ModelPart;

// fix any textures in the model file
localPackage.ModelPart = await FixTextureContentType(modelStream);

以下函数可确保正确指定纹理。

/// <summary>
/// Ensure textures are saved correctly.
/// </summary>
/// <param name="modelStream">3dmodel.model data</param>
/// <returns></returns>
private async Task<IRandomAccessStream> FixTextureContentType(IRandomAccessStream modelStream) {
    XDocument xmldoc = XDocument.Load(modelStream.AsStreamForRead());

    var outputStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
    var writer = new Windows.Storage.Streams.DataWriter(outputStream);
    writer.UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.Utf8;
    writer.ByteOrder = Windows.Storage.Streams.ByteOrder.LittleEndian;
    writer.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

    var text = xmldoc.ToString();
    // ensure that content type is set correctly
    // texture content can be either png or jpg
    var replacedText = text.Replace("contenttype=\"\"", "contenttype=\"image/png\"");
    writer.WriteString(replacedText);

    await writer.StoreAsync();
    await writer.FlushAsync();
    writer.DetachStream();
    return outputStream;
}

我们可以从此处在应用内启动打印作业(请参阅从应用进行 3D 打印),或者将此 Printing3D3MFPackage 另存为 .3mf 文件。

以下方法选取已完成的 Printing3D3MFPackage 并将其数据保存到.3mf 文件。

private async void SaveTo3mf(Printing3D3MFPackage localPackage) {

    // prompt the user to choose a location to save the file to
    FileSavePicker savePicker = new FileSavePicker();
    savePicker.DefaultFileExtension = ".3mf";
    savePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
    savePicker.FileTypeChoices.Add("3MF File", new[] { ".3mf" });
    var storageFile = await savePicker.PickSaveFileAsync();
    if (storageFile == null) {
        return;
    }

    // save the 3MF Package to an IRandomAccessStream
    using (var stream = await localPackage.SaveAsync()) {
        // go to the beginning of the stream
        stream.Seek(0);

        // read from the file stream and write to a buffer
        using (var dataReader = new DataReader(stream)) {
            await dataReader.LoadAsync((uint)stream.Size);
            var buffer = dataReader.ReadBuffer((uint)stream.Size);

            // write from the buffer to the storagefile specified
            await FileIO.WriteBufferAsync(storageFile, buffer);
        }
    }
}

从应用进行 3D 打印
3D 打印 UWP 示例