圧縮された.X File について(独自調査)

DirectX9 までサポートされていた、モデル・アニメーションを保存するフォーマット Xファイルですが、D3DX のサポート終了とともに、読み込む方法がなくなってしまいました。
(代替フォーマットがいくらでもあり、ふつうは困らないのですが・・・・)

 今回、現行バージョン(2016年末現在)のDirectX11 で読み込んでみようというねらいで、 Xファイル読み込みのプログラムをフルスクラッチで組もうと考えました。まず第一弾として、 テキスト形式、第二弾でバイナリ形式・・・でいいかなと思ったのですが、やはり不完全は 悔しく、圧縮形式に対応しようと考え、ネットを見て圧縮されたxファイルのフォーマットを 探しました。DirectX9付属のドキュメント、MSDN、あれこれ調べましたが、詳しい情報は 結局見つかりませんでした。DirectX9のドキュメントでさえ「圧縮ストリームはサポートされない」 とのつれない言葉。
仕方ないので、判る範囲の情報から、自分であれこれ試しながら調べてみました。結果として 動くようになりました。完全に仕様を把握できているか疑問ですが、こんなフォーマットになっているようです。

ここにある数値は多分こういう意味だろうという推測とやってみるでなんとか動かしています。

正確かどうかは保証できませんが、このフォーマットを重要な業務に使うとも思えないので、 多少違っていてもいいのかなと。


【ヘッダについて】

まずはじめの16バイトは、ヘッダ(@)です。ここはMicrosoft のサイトにちゃんとフォーマット が掲載されています。+8〜+11 のバイト列が、ASCII コードで、"bzip"と なっていたら、バイナリ形式のXFile を、圧縮したものと判断できるようになっています。

【ファイルサイズ(伸長後)】

ヘッダに続く、4バイト(A)は、伸長後のXFile のサイズのようです。このサイズには
圧縮ブロックには含まれていない、ヘッダのサイズも含まれている事に注意が必要です。
つまり、ここに格納されているサイズは圧縮されているデータの圧縮前のサイズ+16です。

続いて1個から複数個のデータブロックが並びます。

【データブロック(BCDで1ブロック)について】

各ブロックの最初の2バイト(Bここでは、0x00 0x80)は、そのデータブロックを伸長した 後のデータサイズ(具体的には、0x8000 つまり32KByte)を表しています。

次の2バイト(Cここでは、0x82 0x23)は、続く圧縮されたデータ(D)のサイズを表して います。

Dのブロックは、0x43 0x4b で始まりその後一つもしくは複数のRFC1951仕様の データブロックが並びます。個数はこの時点で明確ではなく、RFC1951の仕様に したがって、最終ブロックを読むと終わりとなっています。

BCの仕組みはどちらかというと、MSZip というよりは、LZMSに近い仕様なのですが、
データブロック の 圧縮形式は、MSZip です。MSZip の仕様上 圧縮ブロックは 0x43 0x4B で始まります。



上記のデータブロックがこの後いくつか並びます。

ここにはまるポイントがあり、RFC1951 は同じデータの並びが複数回あれば、
2回目以降で1回目に戻って参照する仕様なのですが、伸長し始めからの データを参照できなければなりません。
つまりここでいう、BCD があり、次のBCDが続くとき、2回目のDから 一回目のDのデータ(両方とも伸長後ですが)を参照できなければなりません。
※自前の伸長コードを書いた時、一つ目のブロックは正しく伸長できるのに二つ目のブロックがどうしてもできず かなりハマった・・・・。

プログラムは、ひとつ一つのブロックを伸長していき、伸長したデータの合計サイズが、 先ほどの伸長後のXFile サイズに到達するか、圧縮データの末尾に到達するかのどちらかで 終了とすれば良いわけです。

 

【伸長コードサンプル】

Compression API を使用しています。そのため、このコードだとWindows7 では動作せず、Windows8 以上で動作します。

Required Header(s)Compressapi.h
Required Library(s)Cabinet.lib

///
//  @brief Decompress the specified compressed x file.
//
//  @param
//      pBufferCompressed    [in]  compressed x file data
//      dwSize               [in]  size of compressed data
//      ppBufferUncompressed [out] buffer to store uncompressed data
//      pdwUncompressedSize  [out] size of uncompressed data
//
//  @return
//      S_OK   : succeeded
//      E_FAIL : failed
//
HRESULT DecompressMSZipXFile(BYTE *pBufferCompressed, DWORD dwSize, BYTE **ppBufferUncompressed, DWORD *pdwUncompressedSize){
    HRESULT hr = E_FAIL;
    DECOMPRESSOR_HANDLE    hCompress = NULL;
    BYTE *pNewBuffer = NULL;
    BYTE *pSrc, *pDest;
    DWORD finalSize;
    DWORD compressedSize, bytesRead;
    DWORD uncompressedSize, uncompressedSum;

    //!  prepare decompressor for RFC 1951 compression
    if (0 == CreateDecompressor(COMPRESS_ALGORITHM_MSZIP|COMPRESS_RAW,NULL,&hCompress)){
        goto EXIT;
    }
    
    //! preparation
    pSrc = pBufferCompressed + 16;
    bytesRead = 16;
    uncompressedSize = 0;
    uncompressedSum = 0;
    finalSize = *(DWORD*)pSrc;    //!<  Read the uncompressed size
    pNewBuffer = new BYTE[finalSize];
    pDest = pNewBuffer;
    pSrc += sizeof(DWORD);
    bytesRead += sizeof(DWORD);
    uncompressedSum += 16;
    pDest += 16;

    //! Decompress blocks loop.
    while(bytesRead < dwSize && uncompressedSum < finalSize){
        uncompressedSize = *(WORD*)pSrc;
        pSrc += sizeof(WORD);
        compressedSize = *(WORD*)pSrc;
        pSrc += sizeof(WORD);
        if (0 == Decompress(hCompress,(VOID*)pSrc,compressedSize,pDest,uncompressedSize,0)){
            //DWORD err = GetLastError();
            SAFE_DELETE(pNewBuffer);
            goto EXIT;
        }
        bytesRead += compressedSize+4;
        pSrc  += compressedSize;
        pDest += uncompressedSize;
        uncompressedSum += uncompressedSize;
    }
    {
        //!  Clone the x file header
        DWORD *pdwSrc = (DWORD*)pBufferCompressed;
        DWORD *pdwDest = (DWORD*)pNewBuffer;
        *pdwDest++ = *pdwSrc++;    //!<  xof
        *pdwDest++ = *pdwSrc++;    //!<  vers
        //!  bin/txt
        if (0==_strnicmp((CHAR*)pdwSrc,"bzip",4)){
            *pdwDest = *(DWORD*)"bin ";
        }else if (0==_strnicmp((CHAR*)pdwSrc,"tzip",4)){
            *pdwDest = *(DWORD*)"txt ";
        }else{
            SAFE_DELETE(pNewBuffer);
            goto EXIT;
        }
        pdwDest++;
        pdwSrc++;

        *pdwDest++ = *pdwSrc++;    //!<  float length (means precision)
    }
    hr = S_OK;
EXIT:
    if (hCompress != NULL)
        CloseDecompressor(hCompress);
    *ppBufferUncompressed = pNewBuffer;
    *pdwUncompressedSize = uncompressedSum;
    return hr;
}

 

追記

旧式なフォーマットとは言っても、モデル・マテリアル・アニメーションが扱えて、 テキスト形式とバイナリ形式をほとんど同じプログラムで読み取れる 素性の良さがあり、圧縮にも対応していて。悪いフォーマットでは 無かったとは思います。

 惜しむらくはテクスチャを内包できず外部ファイルを参照しなければ ならないのに、外部ファイル参照の文字コードが多国語対応でないこと。

 今となってはここが一番時代遅れな点ですかね。

 いちおうTemplate でUNICODE は指定できるのですが、実体としては対応して いない感じです(Binaryフォーマットの、TOKEN_STRING は仕様上ASCIIのみ対応)。

 Microsoft は一時(今もですかね)すべてのデータをXML対応にする・・・てな
事を言ってましたので、XMLでないこのファイルは方針に合わなかったの かもしれず、それも影響しているかもしれません。