Создание пакета трехмерного производственного формата

В этом руководстве описывается структура типа файла 3D Manufacturing Format (3MF) и способ использования API Windows.Graphics.Printing3D для его создания и управления им.

Важные API

Что такое трехмерный производственный формат?

3MF — это набор соглашений об использовании XML для описания внешнего вида и структуры трехмерных моделей для производства (трехмерной печати). Он определяет набор деталей (обязательных и необязательных) и их связи с трехмерной производственной устройством. Набор данных, соответствующий 3MF, можно сохранить в виде файла с расширением .3MF.

Класс Printing3D3MFPackage в пространстве имен Windows.Graphics.Printing3D аналогиен одному 3MF-файлу, в то время как другие классы сопоставляются с конкретными XML-элементами в файле .3MF. В этом руководстве описывается, как можно программно создать и задать каждую из main частей документа 3MF, как можно использовать расширение материалов 3MF и как объект Printing3D3MFPackage можно преобразовать и сохранить в виде 3MF-файла. Подробнее о стандартах 3MF и расширении 3MF Materials см. в разделе 3MF Specification.

Основные классы в структуре 3MF

Класс Printing3D3MFPackage представляет полный документ 3MF. В основе документа 3MF лежит часть модели, представленная классом Printing3DModel. Большая часть сведений о трехмерной модели хранится путем задания свойств класса 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. Допустимый объект сетки должен содержать сведения о расположении всех вершин, а также всех треугольников, существующих между определенными наборами вершин.

Следующий метод добавляет вершины в сетку, а затем предоставляет им расположения в трехмерном пространстве.

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 в пакете должны храниться в свойстве Meshes класса Printing3DModel , как показано ниже.

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

Создание материалов

Трехмерная модель может содержать данные для нескольких материалов. Это соглашение позволяет воспользоваться устройствами трехмерной печати, которые могут использовать несколько материалов в одном задании печати. Существует также несколько типов групп материалов, каждая из которых может поддерживать ряд различных отдельных материалов.

Каждая группа материалов должна иметь уникальный идентификатор ссылки, а каждый материал в этой группе также должен иметь уникальный идентификатор. Затем различные объекты сетки в модели могут ссылаться на материалы.

Кроме того, отдельные треугольники на каждой сетке могут указывать разные материалы, и различные материалы могут быть представлены даже в одном треугольнике, при этом каждой вершине треугольника назначен отдельный материал, а материал поверхности вычисляется как градиент между ними.

Сначала мы покажем, как создавать различные типы материалов в соответствующих группах материалов и хранить их в качестве ресурсов в объекте модели. Затем мы назначим различные материалы отдельным сеткам и отдельным треугольникам.

Базовые материалы

Тип материалов по умолчанию — это базовый материал, у которого есть значение Цветной материал (описано ниже) и атрибут имени, указывающий тип используемого материала.

// 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);

Примечание

 Устройство трехмерной печати определит, какие доступные физические материалы соответствуют виртуальным материалам в 3MF. Сопоставление материалов не обязательно должно быть 1:1. Если трехмерный принтер использует только один материал, он будет печатать всю модель в этом материале, независимо от того, какие объекты или лица были назначены различным материалам.

Цветовые материалы

Цветные материалы аналогичны базовым, но у них нет имени. Таким образом они не предоставляют никаких инструкции о типе материала, который будет использовать устройство. Они содержат только данные цвета и позволяют компьютеру выбрать тип материала (компьютер может предложить пользователю выбрать). В следующем примере 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 поддерживает использование двумерных изображений для отображения цвета на поверхности трехмерных моделей. Таким образом модель может передавать гораздо больше данных цветов для каждой грани треугольника (вместо одного значения цвета для каждой вершины из треугольников). Как и цветные материалы, материалы координат текстуры передают только цветовые данные. Чтобы использовать двухd-текстуру, сначала необходимо объявить ресурс текстуры.

Примечание

Данные текстуры относятся к самому пакету 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);
    }
}

Компоненты и сборка

Структура компонента позволяет пользователю разместить несколько объектов сетки в печатаемой трехмерной модели. Объект Printing3DComponent содержит одну сетку и список ссылок на другие компоненты. Фактически это список объектов Printing3DComponentWithMatrix. Каждый объект Printing3DComponentWithMatrix содержит объект Printing3DComponent и матрицу преобразования, которая применяется к сетке и компонентам Объекта Printing3DComponent.

Например, модель автомобиля может состоять из кузова Printing3DComponent, который содержит сетку кузова автомобиля. Компонент Body может содержать ссылки на четыре разных объекта Printing3DComponentWithMatrix , которые ссылаются на один и тот же Объект Printing3DComponent , а сетка Wheel может содержать четыре разных матрицы преобразования (сопоставление колес с четырьмя разными положениями на теле автомобиля). В этом сценарии сетку Body и сетку 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;
}

Затем мы можем запустить задание печати в приложении (см. раздел Трехмерная печать из приложения) или сохранить пакет 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);
        }
    }
}

Трехмерная печать из приложения
Пример трехмерной печати UWP