HRESULT Mesh::ExportToSDKMESH()

in Meshconvert/Mesh.cpp [1761:2434]


HRESULT Mesh::ExportToSDKMESH(const wchar_t* szFileName,
    size_t nMaterials, const Material* materials,
    bool force32bit,
    bool version2,
    DXGI_FORMAT normalFormat,
    DXGI_FORMAT uvFormat,
    DXGI_FORMAT colorFormat) const noexcept
{
    using namespace DXUT;

    if (!szFileName)
        return E_INVALIDARG;

    if (nMaterials > 0 && !materials)
        return E_INVALIDARG;

    if (!mnFaces || !mIndices || !mnVerts || !mPositions)
        return E_UNEXPECTED;

    if ((uint64_t(mnFaces) * 3) >= UINT32_MAX)
        return HRESULT_FROM_WIN32(ERROR_ARITHMETIC_OVERFLOW);

    // Build input layout/vertex decalaration
    static const D3D11_INPUT_ELEMENT_DESC s_elements[] =
    {
        { "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 0
        { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 1
        { "COLOR", 0, DXGI_FORMAT_B8G8R8A8_UNORM, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 2
        { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 3
        { "BINORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 4
        { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 5
        { "BLENDINDICES", 0, DXGI_FORMAT_R8G8B8A8_UINT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 6
        { "BLENDWEIGHT", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // 7
    };

    static const D3DVERTEXELEMENT9 s_decls[] =
    {
        { 0, 0, D3DDECLTYPE_FLOAT3, 0, D3DDECLUSAGE_POSITION, 0 }, // 0
        { 0, 0, D3DDECLTYPE_FLOAT3, 0, D3DDECLUSAGE_NORMAL, 0 }, // 1
        { 0, 0, D3DDECLTYPE_D3DCOLOR, 0, D3DDECLUSAGE_COLOR, 0 }, // 2
        { 0, 0, D3DDECLTYPE_FLOAT3, 0, D3DDECLUSAGE_TANGENT, 0 }, // 3
        { 0, 0, D3DDECLTYPE_FLOAT3, 0, D3DDECLUSAGE_BINORMAL, 0 }, // 4
        { 0, 0, D3DDECLTYPE_FLOAT2, 0, D3DDECLUSAGE_TEXCOORD, 0 }, // 5
        { 0, 0, D3DDECLTYPE_UBYTE4, 0, D3DDECLUSAGE_BLENDINDICES, 0 }, // 6
        { 0, 0, D3DDECLTYPE_UBYTE4N, 0, D3DDECLUSAGE_BLENDWEIGHT, 0 }, // 7
        { 0xFF, 0, D3DDECLTYPE_UNUSED, 0, 0, 0 },
    };

    static_assert((std::size(s_elements) + 1) == std::size(s_decls), "InputLayouts and Vertex Decls disagree");

    uint8_t normalType;
    size_t normalStride;
    switch (normalFormat)
    {
    case DXGI_FORMAT_R16G16B16A16_FLOAT:
        normalType = D3DDECLTYPE_FLOAT16_4; normalStride = sizeof(PackedVector::XMHALF4);
        break;

    case DXGI_FORMAT_R11G11B10_FLOAT: // Biased in GetVertexBuffer
        normalType = D3DDECLTYPE_DXGI_R11G11B10_FLOAT; normalStride = sizeof(UINT);
        break;

    default:
        normalFormat = DXGI_FORMAT_R32G32B32_FLOAT; normalType = D3DDECLTYPE_FLOAT3; normalStride = sizeof(XMFLOAT3);
        break;
    }

    uint8_t uvType;
    size_t uvStride;
    switch (uvFormat)
    {
    case DXGI_FORMAT_R16G16_FLOAT:
        uvType = D3DDECLTYPE_FLOAT16_2; uvStride = sizeof(PackedVector::XMHALF2);
        break;

    default:
        uvFormat = DXGI_FORMAT_R32G32_FLOAT; uvType = D3DDECLTYPE_FLOAT2; uvStride = sizeof(XMFLOAT2);
        break;
    }

    uint8_t colorType;
    size_t colorStride;
    switch (colorFormat)
    {
    case DXGI_FORMAT_R32G32B32A32_FLOAT:
        colorType = D3DDECLTYPE_FLOAT4; colorStride = sizeof(XMFLOAT4);
        break;

    case DXGI_FORMAT_R16G16B16A16_FLOAT:
        colorType = D3DDECLTYPE_FLOAT16_4; colorStride = sizeof(PackedVector::XMHALF4);
        break;

    case DXGI_FORMAT_R11G11B10_FLOAT:
        colorType = D3DDECLTYPE_DXGI_R11G11B10_FLOAT; colorStride = sizeof(UINT);
        break;

    case DXGI_FORMAT_R10G10B10A2_UNORM:
        colorType = D3DDECLTYPE_DXGI_R10G10B10A2_UNORM; colorStride = sizeof(UINT);
        break;

    case DXGI_FORMAT_R8G8B8A8_UNORM:
        colorType = D3DDECLTYPE_UBYTE4N; colorStride = sizeof(UINT);
        break;

    default:
        colorFormat = DXGI_FORMAT_B8G8R8A8_UNORM; colorType = D3DDECLTYPE_D3DCOLOR; colorStride = sizeof(UINT);
        break;
    }

    SDKMESH_VERTEX_BUFFER_HEADER vbHeader = {};
    vbHeader.NumVertices = mnVerts;
    vbHeader.Decl[0] = s_decls[0];

    D3D11_INPUT_ELEMENT_DESC inputLayout[MAX_VERTEX_ELEMENTS] = {};
    inputLayout[0] = s_elements[0];

    size_t nDecl = 1;
    size_t stride = sizeof(XMFLOAT3);

    if (mBlendIndices && mBlendWeights)
    {
        // BLENDWEIGHT
        vbHeader.Decl[nDecl] = s_decls[7];
        vbHeader.Decl[nDecl].Offset = static_cast<WORD>(stride);
        inputLayout[nDecl] = s_elements[7];
        ++nDecl;
        stride += sizeof(UINT);

        // BLENDINDICES
        vbHeader.Decl[nDecl] = s_decls[6];
        vbHeader.Decl[nDecl].Offset = static_cast<WORD>(stride);
        inputLayout[nDecl] = s_elements[6];
        ++nDecl;
        stride += sizeof(UINT);
    }

    if (mNormals)
    {
        vbHeader.Decl[nDecl] = s_decls[1];
        vbHeader.Decl[nDecl].Type = normalType;
        vbHeader.Decl[nDecl].Offset = static_cast<WORD>(stride);
        inputLayout[nDecl] = s_elements[1];
        inputLayout[nDecl].Format = normalFormat;
        ++nDecl;
        stride += normalStride;
    }

    if (mColors)
    {
        vbHeader.Decl[nDecl] = s_decls[2];
        vbHeader.Decl[nDecl].Type = colorType;
        vbHeader.Decl[nDecl].Offset = static_cast<WORD>(stride);
        inputLayout[nDecl] = s_elements[2];
        inputLayout[nDecl].Format = colorFormat;
        ++nDecl;
        stride += colorStride;
    }

    if (mTexCoords)
    {
        vbHeader.Decl[nDecl] = s_decls[5];
        vbHeader.Decl[nDecl].Type = uvType;
        vbHeader.Decl[nDecl].Offset = static_cast<WORD>(stride);
        inputLayout[nDecl] = s_elements[5];
        inputLayout[nDecl].Format = uvFormat;
        ++nDecl;
        stride += uvStride;
    }

    if (mTangents)
    {
        vbHeader.Decl[nDecl] = s_decls[3];
        vbHeader.Decl[nDecl].Type = normalType;
        vbHeader.Decl[nDecl].Offset = static_cast<WORD>(stride);
        inputLayout[nDecl] = s_elements[3];
        inputLayout[nDecl].Format = normalFormat;
        ++nDecl;
        stride += normalStride;
    }

    if (mBiTangents)
    {
        vbHeader.Decl[nDecl] = s_decls[4];
        vbHeader.Decl[nDecl].Type = normalType;
        vbHeader.Decl[nDecl].Offset = static_cast<WORD>(stride);
        inputLayout[nDecl] = s_elements[4];
        inputLayout[nDecl].Format = normalFormat;
        ++nDecl;
        stride += normalStride;
    }

    assert(nDecl < MAX_VERTEX_ELEMENTS);
    vbHeader.Decl[nDecl] = s_decls[std::size(s_decls) - 1];

    // Build vertex buffer
    std::unique_ptr<uint8_t> vb(new (std::nothrow) uint8_t[mnVerts * stride]);
    if (!vb)
        return E_OUTOFMEMORY;

    vbHeader.SizeBytes = uint64_t(mnVerts) * uint64_t(stride);
    vbHeader.StrideBytes = stride;

    {
        VBWriter writer;

        HRESULT hr = writer.Initialize(inputLayout, nDecl);
        if (FAILED(hr))
            return hr;

        hr = writer.AddStream(vb.get(), mnVerts, 0, stride);
        if (FAILED(hr))
            return hr;

        hr = GetVertexBuffer(writer);
        if (FAILED(hr))
            return hr;
    }

    // Build index buffer
    SDKMESH_INDEX_BUFFER_HEADER ibHeader = {};
    ibHeader.NumIndices = uint64_t(mnFaces) * 3;

    std::unique_ptr<uint16_t[]> ib16;
    if (!force32bit && Is16BitIndexBuffer())
    {
        ibHeader.SizeBytes = uint64_t(mnFaces) * 3 * sizeof(uint16_t);
        ibHeader.IndexType = IT_16BIT;

        ib16 = GetIndexBuffer16();
        if (!ib16)
            return E_OUTOFMEMORY;
    }
    else
    {
        ibHeader.SizeBytes = uint64_t(mnFaces) * 3 * sizeof(uint32_t);
        ibHeader.IndexType = IT_32BIT;
    }

    // Build materials buffer
    std::unique_ptr<SDKMESH_MATERIAL[]> mats;
    if (version2)
    {
        if (!nMaterials)
        {
            mats.reset(new (std::nothrow) SDKMESH_MATERIAL[1]);
            if (!mats)
                return E_OUTOFMEMORY;

            auto mat2 = reinterpret_cast<SDKMESH_MATERIAL_V2*>(mats.get());
            memset(mat2, 0, sizeof(SDKMESH_MATERIAL_V2));

            strcpy_s(mat2->Name, "default");
            mat2->Alpha = 1.f;
        }
        else
        {
            mats.reset(new (std::nothrow) SDKMESH_MATERIAL[nMaterials]);
            if (!mats)
                return E_OUTOFMEMORY;

            for (size_t j = 0; j < nMaterials; ++j)
            {
                auto m0 = &materials[j];
                auto m2 = reinterpret_cast<SDKMESH_MATERIAL_V2*>(&mats[j]);

                memset(m2, 0, sizeof(SDKMESH_MATERIAL_V2));

                if (!m0->name.empty())
                {
                    int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                        m0->name.c_str(), -1,
                        m2->Name, MAX_MATERIAL_NAME, nullptr, FALSE);
                    if (!result)
                    {
                        *m2->Name = 0;
                    }
                }

                m2->Alpha = m0->alpha;

                if (!m0->texture.empty())
                {
                    int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                        m0->texture.c_str(), -1,
                        m2->AlbedoTexture, MAX_TEXTURE_NAME, nullptr, FALSE);
                    if (!result)
                    {
                        *m2->AlbedoTexture = 0;
                    }
                }

                // Derive other PBR texture names from base texture
                {
                    char drive[_MAX_DRIVE] = {};
                    char dir[MAX_PATH] = {};
                    char fname[_MAX_FNAME] = {};
                    char ext[_MAX_EXT] = {};
                    _splitpath_s(m2->AlbedoTexture, drive, dir, fname, ext);

                    std::string basename = fname;
                    size_t pos = basename.find_last_of('_');
                    if (pos != std::string::npos)
                    {
                        basename = basename.substr(0, pos);
                    }

                    if (!basename.empty())
                    {
                        strcpy_s(fname, basename.c_str());
                        strcat_s(fname, "_normal");
                        _makepath_s(m2->NormalTexture, drive, dir, fname, ext);

                        strcpy_s(fname, basename.c_str());
                        strcat_s(fname, "_occlusionRoughnessMetallic");
                        _makepath_s(m2->RMATexture, drive, dir, fname, ext);

                        if (m0->emissiveColor.x > 0 || m0->emissiveColor.y > 0 || m0->emissiveColor.z > 0)
                        {
                            strcpy_s(fname, basename.c_str());
                            strcat_s(fname, "_emissive");
                            _makepath_s(m2->EmissiveTexture, drive, dir, fname, ext);
                        }
                    }
                }

                // Allow normal texture material property to override derived name
                if (!m0->normalTexture.empty())
                {
                    int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                        m0->normalTexture.c_str(), -1,
                        m2->NormalTexture, MAX_TEXTURE_NAME, nullptr, FALSE);
                    if (!result)
                    {
                        *m2->NormalTexture = 0;
                    }
                }

                // Allow emissive texture material property to override drived name
                if (!m0->emissiveTexture.empty())
                {
                    int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                        m0->emissiveTexture.c_str(), -1,
                        m2->EmissiveTexture, MAX_TEXTURE_NAME, nullptr, FALSE);
                    if (!result)
                    {
                        *m2->EmissiveTexture = 0;
                    }
                }

                // Allow RMA texture material property to override drived name
                if (!m0->rmaTexture.empty())
                {
                    int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                        m0->rmaTexture.c_str(), -1,
                        m2->RMATexture, MAX_TEXTURE_NAME, nullptr, FALSE);
                    if (!result)
                    {
                        *m2->RMATexture = 0;
                    }
                }
            }
        }
    }
    else if (!nMaterials)
    {
        mats.reset(new (std::nothrow) SDKMESH_MATERIAL[1]);
        if (!mats)
            return E_OUTOFMEMORY;

        memset(mats.get(), 0, sizeof(SDKMESH_MATERIAL));

        strcpy_s(mats[0].Name, "default");
        mats[0].Diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.f);
        mats[0].Ambient = XMFLOAT4(0.2f, 02.f, 0.2f, 1.f);
        mats[0].Power = 1.f;
    }
    else
    {
        mats.reset(new (std::nothrow) SDKMESH_MATERIAL[nMaterials]);
        if (!mats)
            return E_OUTOFMEMORY;

        for (size_t j = 0; j < nMaterials; ++j)
        {
            auto m0 = &materials[j];
            auto m = &mats[j];

            memset(m, 0, sizeof(SDKMESH_MATERIAL));

            if (!m0->name.empty())
            {
                int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                    m0->name.c_str(), -1,
                    m->Name, MAX_MATERIAL_NAME, nullptr, FALSE);
                if (!result)
                {
                    *m->Name = 0;
                }
            }

            if (!m0->texture.empty())
            {
                int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                    m0->texture.c_str(), -1,
                    m->DiffuseTexture, MAX_TEXTURE_NAME, nullptr, FALSE);
                if (!result)
                {
                    *m->DiffuseTexture = 0;
                }
            }

            if (!m0->normalTexture.empty())
            {
                int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                    m0->normalTexture.c_str(), -1,
                    m->NormalTexture, MAX_TEXTURE_NAME, nullptr, FALSE);
                if (!result)
                {
                    *m->NormalTexture = 0;
                }
            }

            if (!m0->specularTexture.empty())
            {
                int result = WideCharToMultiByte(CP_UTF8, WC_NO_BEST_FIT_CHARS,
                    m0->specularTexture.c_str(), -1,
                    m->SpecularTexture, MAX_TEXTURE_NAME, nullptr, FALSE);
                if (!result)
                {
                    *m->SpecularTexture = 0;
                }
            }

            m->Diffuse.x = m0->diffuseColor.x;
            m->Diffuse.y = m0->diffuseColor.y;
            m->Diffuse.z = m0->diffuseColor.z;
            m->Diffuse.w = m0->alpha;

            m->Ambient.x = m0->ambientColor.x;
            m->Ambient.y = m0->ambientColor.y;
            m->Ambient.z = m0->ambientColor.z;
            m->Ambient.w = 1.f;

            if (m0->specularColor.x > 0.f || m0->specularColor.y > 0.f || m0->specularColor.z > 0.f)
            {
                m->Specular.x = m0->specularColor.x;
                m->Specular.y = m0->specularColor.y;
                m->Specular.z = m0->specularColor.z;
                m->Power = (m0->specularPower <= 0.f) ? 16.f : m0->specularPower;
            }
            else
            {
                m->Power = 1.f;
            }

            m->Emissive.x = m0->emissiveColor.x;
            m->Emissive.y = m0->emissiveColor.y;
            m->Emissive.z = m0->emissiveColor.z;
        }
    }

    // Build subsets
    std::vector<SDKMESH_SUBSET> submeshes;
    std::vector<UINT> subsetArray;
    if (mAttributes)
    {
        auto subsets = ComputeSubsets(mAttributes.get(), mnFaces);

        UINT64 startIndex = 0;
        for (const auto& it : subsets)
        {
            subsetArray.push_back(static_cast<UINT>(submeshes.size()));

            SDKMESH_SUBSET s = {};
            s.MaterialID = mAttributes[it.first];
            if (s.MaterialID >= nMaterials)
                s.MaterialID = 0;

            s.PrimitiveType = PT_TRIANGLE_LIST;
            s.IndexStart = startIndex;
            s.IndexCount = uint64_t(it.second) * 3;
            s.VertexCount = mnVerts;
            submeshes.push_back(s);

            if ((startIndex + s.IndexCount) > uint64_t(mnFaces) * 3)
                return E_FAIL;

            startIndex += s.IndexCount;
        }
    }
    else
    {
        SDKMESH_SUBSET s = {};
        s.PrimitiveType = PT_TRIANGLE_LIST;
        s.IndexCount = uint64_t(mnFaces) * 3;
        s.VertexCount = mnVerts;
        subsetArray.push_back(0);
        submeshes.push_back(s);
    }

    // Create file
#if (_WIN32_WINNT >= _WIN32_WINNT_WIN8)
    ScopedHandle hFile(safe_handle(CreateFile2(szFileName,
        GENERIC_WRITE, 0, CREATE_ALWAYS, nullptr)));
#else
    ScopedHandle hFile(safe_handle(CreateFileW(szFileName,
        GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)));
#endif
    if (!hFile)
        return HRESULT_FROM_WIN32(GetLastError());

    // Write file header
    SDKMESH_HEADER header = {};
    header.Version = (version2) ? SDKMESH_FILE_VERSION_V2 : SDKMESH_FILE_VERSION;
    header.IsBigEndian = 0;

    header.NumVertexBuffers = 1;
    header.NumIndexBuffers = 1;
    header.NumMeshes = 1;
    header.NumTotalSubsets = static_cast<UINT>(submeshes.size());
    header.NumFrames = 1;
    header.NumMaterials = (nMaterials > 0) ? static_cast<UINT>(nMaterials) : 1;

    header.HeaderSize = sizeof(SDKMESH_HEADER) + sizeof(SDKMESH_VERTEX_BUFFER_HEADER) + sizeof(SDKMESH_INDEX_BUFFER_HEADER);

    size_t staticDataSize = sizeof(SDKMESH_MESH)
        + header.NumTotalSubsets * sizeof(SDKMESH_SUBSET)
        + sizeof(SDKMESH_FRAME)
        + header.NumMaterials * sizeof(SDKMESH_MATERIAL);

    header.NonBufferDataSize = uint64_t(staticDataSize) + uint64_t(subsetArray.size()) * sizeof(UINT) + sizeof(UINT);

    header.BufferDataSize = roundup4k(vbHeader.SizeBytes) + roundup4k(ibHeader.SizeBytes);

    header.VertexStreamHeadersOffset = sizeof(SDKMESH_HEADER);
    header.IndexStreamHeadersOffset = header.VertexStreamHeadersOffset + sizeof(SDKMESH_VERTEX_BUFFER_HEADER);
    header.MeshDataOffset = header.IndexStreamHeadersOffset + sizeof(SDKMESH_INDEX_BUFFER_HEADER);
    header.SubsetDataOffset = header.MeshDataOffset + sizeof(SDKMESH_MESH);
    header.FrameDataOffset = header.SubsetDataOffset + uint64_t(header.NumTotalSubsets) * sizeof(SDKMESH_SUBSET);
    header.MaterialDataOffset = header.FrameDataOffset + sizeof(SDKMESH_FRAME);

    HRESULT hr = write_file(hFile.get(), header);
    if (FAILED(hr))
        return hr;

    // Write buffer headers
    UINT64 offset = header.HeaderSize + header.NonBufferDataSize;

    vbHeader.DataOffset = offset;
    offset += roundup4k(vbHeader.SizeBytes);

    hr = write_file(hFile.get(), vbHeader);
    if (FAILED(hr))
        return hr;

    ibHeader.DataOffset = offset;
    offset += roundup4k(ibHeader.SizeBytes);

    hr = write_file(hFile.get(), ibHeader);
    if (FAILED(hr))
        return hr;

    // Write mesh headers
    assert(header.NumMeshes == 1);
    offset = header.HeaderSize + staticDataSize;

    SDKMESH_MESH meshHeader = {};
    meshHeader.NumVertexBuffers = 1;
    meshHeader.NumFrameInfluences = 1;

    {
        BoundingBox box;
        BoundingBox::CreateFromPoints(box, mnVerts, mPositions.get(), sizeof(XMFLOAT3));

        meshHeader.BoundingBoxCenter = box.Center;
        meshHeader.BoundingBoxExtents = box.Extents;
    }

    meshHeader.NumSubsets = static_cast<UINT>(submeshes.size());
    meshHeader.SubsetOffset = offset;
    offset += uint64_t(meshHeader.NumSubsets) * sizeof(UINT);
    meshHeader.FrameInfluenceOffset = offset;
    offset += sizeof(UINT);

    hr = write_file(hFile.get(), meshHeader);
    if (FAILED(hr))
        return hr;

    // Write subsets
    auto bytesToWrite = static_cast<DWORD>(sizeof(SDKMESH_SUBSET) * submeshes.size());
    DWORD bytesWritten;
    if (!WriteFile(hFile.get(), submeshes.data(), bytesToWrite, &bytesWritten, nullptr))
        return HRESULT_FROM_WIN32(GetLastError());

    if (bytesWritten != bytesToWrite)
        return E_FAIL;

    // Write frames
    SDKMESH_FRAME frame = {};
    strcpy_s(frame.Name, "root");
    frame.ParentFrame = frame.ChildFrame = frame.SiblingFrame = DWORD(-1);
    frame.AnimationDataIndex = INVALID_ANIMATION_DATA;
    XMMATRIX id = XMMatrixIdentity();
    XMStoreFloat4x4(&frame.Matrix, id);

    hr = write_file(hFile.get(), frame);
    if (FAILED(hr))
        return hr;

    // Write materials
    bytesToWrite = static_cast<DWORD>(sizeof(SDKMESH_MATERIAL) * ((nMaterials > 0) ? nMaterials : 1));
    if (!WriteFile(hFile.get(), mats.get(), bytesToWrite, &bytesWritten, nullptr))
        return HRESULT_FROM_WIN32(GetLastError());

    if (bytesWritten != bytesToWrite)
        return E_FAIL;

    // Write subset index list
    assert(meshHeader.NumSubsets == subsetArray.size());
    bytesToWrite = meshHeader.NumSubsets * sizeof(UINT);
    if (!WriteFile(hFile.get(), subsetArray.data(), bytesToWrite, &bytesWritten, nullptr))
        return HRESULT_FROM_WIN32(GetLastError());

    if (bytesWritten != bytesToWrite)
        return E_FAIL;

    // Write frame influence list
    assert(meshHeader.NumFrameInfluences == 1);
    UINT frameIndex = 0;
    hr = write_file(hFile.get(), frameIndex);
    if (FAILED(hr))
        return hr;

    // Write VB data
    bytesToWrite = static_cast<DWORD>(vbHeader.SizeBytes);
    if (!WriteFile(hFile.get(), vb.get(), bytesToWrite, &bytesWritten, nullptr))
        return HRESULT_FROM_WIN32(GetLastError());

    if (bytesWritten != bytesToWrite)
        return E_FAIL;

    bytesToWrite = static_cast<DWORD>(roundup4k(vbHeader.SizeBytes) - vbHeader.SizeBytes);
    if (bytesToWrite > 0)
    {
        assert(bytesToWrite < sizeof(g_padding));
        if (!WriteFile(hFile.get(), g_padding, bytesToWrite, &bytesWritten, nullptr))
            return HRESULT_FROM_WIN32(GetLastError());

        if (bytesWritten != bytesToWrite)
            return E_FAIL;
    }

    // Write IB data
    bytesToWrite = static_cast<DWORD>(ibHeader.SizeBytes);
    if (!WriteFile(hFile.get(), (ib16) ? static_cast<void*>(ib16.get()) : static_cast<void*>(mIndices.get()),
        bytesToWrite, &bytesWritten, nullptr))
        return HRESULT_FROM_WIN32(GetLastError());

    if (bytesWritten != bytesToWrite)
        return E_FAIL;

    bytesToWrite = static_cast<DWORD>(roundup4k(ibHeader.SizeBytes) - ibHeader.SizeBytes);
    if (bytesToWrite > 0)
    {
        assert(bytesToWrite < sizeof(g_padding));
        if (!WriteFile(hFile.get(), g_padding, bytesToWrite, &bytesWritten, nullptr))
            return HRESULT_FROM_WIN32(GetLastError());

        if (bytesWritten != bytesToWrite)
            return E_FAIL;
    }

    return S_OK;
}