Last update 1999/09/23

MNG/PNGを使ってみる

(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎


PNGとは

……ってこれはさすがに説明の必要はないですね。

ご存じない方は適当に検索してしらべるなどしてください。

MNGとは

簡単に言うと、レイヤ画像、アニメーションなど複数のイメージを必要とするオブジェクトを規定するフォーマットで、内部的にはPNGフォーマットが使われます。まだ正式な仕様としては定まっていないようです。

本家はこちら日本語版を海賊版として訳されている方もいらっしゃいますね。

PNGを自作プログラムで利用する

PNGフォーマットを自作プログラムから利用しようとする人の便宜を図って、libpngというPNGフォーマット操作用のライブラリが用意されています。以前Cマガジンで紹介されたので、ご存知の方も多いでしょう。Cライブラリですが、結構オブジェクト指向的です。

libpng最新版はこちらで手に入ります。VC++でライブラリを作成する方法については、こちらを参照のこと。というわけで、以下PNGの利用には、そうやって作ったzlib.libとlibpng.libをリンクする必要があります。

PNGイメージを読み書き

PNGイメージの読み書きは、libpngライブラリの適当な関数を呼び出すだけなので、方法さえ分かっていればとても簡単です。

というわけで、我がvafximg::ImageにPNGフォーマットのファイルを読み込む関数をサクッとこしらえてみました。

読み込みはこんな調子。

void PNGRead(png_structp png_ptr,png_bytep data,png_size_t length);

template <class Image>
bool LoadImageFromPNGStream(std::istream& is,Image& image)
{
    // PNG読み込み開始
    png_structp png_ptr;
    png_infop   info_ptr;
    {
        // png_struct
        png_ptr=png_create_read_struct(
            PNG_LIBPNG_VER_STRING,
            NULL,
            NULL,
            NULL);
        if(png_ptr==NULL){
            return false;
        }

        // png_info
        info_ptr=png_create_info_struct(png_ptr);
        if(info_ptr==NULL){
            png_destroy_read_struct(&png_ptr,NULL,NULL);
            return false;
        }

        png_set_read_fn(png_ptr,&is,PNGRead);
    }

    // 画像情報の取得
    png_uint_32 width,height;
    int bit_depth,color_type,interlace_type;
    int compression_type,filter_type;
    {
        png_read_info(png_ptr,info_ptr);
        png_get_IHDR(png_ptr,info_ptr,&width,&height,&bit_depth,&color_type,
            &interlace_type,&compression_type,&filter_type);
    }

    image.SetExtent(st2d::Extent(width,height));

    std::vector<png_bytep> row_pointers;
    {
        for(int y=0;y<image.GetHeight();y++){
            row_pointers.push_back((png_bytep)image.GetAddress(st2d::Point(0,y)));
        }
    }

    png_set_bgr(png_ptr);
    png_read_image(png_ptr,row_pointers.begin());

    png_read_end(png_ptr,info_ptr);

    png_destroy_read_struct(&png_ptr,&info_ptr,(png_infopp)NULL);

    return true;
}

でもって書き出しはこんな調子です。

void PNGWrite(png_structp png_ptr,png_bytep data,png_size_t length);
void PNGFlush(png_structp png_ptr);

template <class Image>
bool SaveImageToPNGStream(std::ostream& os,const Image& image)
{
    // pngバージョン
    png_structp png_ptr;
    png_infop   info_ptr;
    {
        // png_struct
        png_ptr=png_create_write_struct(
            PNG_LIBPNG_VER_STRING,
            NULL,
            NULL,
            NULL);
        if(png_ptr==NULL){
            return false;
        }

        // png_info
        info_ptr=png_create_info_struct(png_ptr);
        if(info_ptr==NULL){
            png_destroy_read_struct(&png_ptr,NULL,NULL);
            return false;
        }

        png_set_write_fn(png_ptr,&os,PNGWrite,PNGFlush);
    }

    // 画像情報の取得
    png_uint_32 width   =image.GetWidth();
    png_uint_32 height  =image.GetHeight();
    int bit_depth       =8;
    int color_type      =PNG_COLOR_TYPE_RGB_ALPHA;
    int interlace_type  =PNG_INTERLACE_NONE;
    int compression_type=PNG_COMPRESSION_TYPE_BASE;
    int filter_type     =PNG_FILTER_TYPE_BASE;
    {
        png_set_IHDR(png_ptr,info_ptr,width,height,bit_depth,color_type,
            interlace_type,compression_type,filter_type);
        png_write_info(png_ptr,info_ptr);
    }

    std::vector<png_bytep> row_pointers;
    {
        for(int y=0;y<image.GetHeight();y++){
            row_pointers.push_back((png_bytep)image.GetAddress(st2d::Point(0,y)));
        }
    }

    png_set_bgr(png_ptr);
    png_write_image(png_ptr,row_pointers.begin());

    png_write_end(png_ptr,info_ptr);
    png_destroy_write_struct(&png_ptr,&info_ptr);

    return true;
}

これらを見て分かるように、入出力が関数ポインタによるコールバックによって仮想化されている点が、libpngのイイトコロです。ソース中の黄色い部分を見ると、入出力関数へのポインタと同時にファイルストリームへのポインタを与えていることが分かりますね。これは何もlibpngがC++のiostreamをサポートしてるわけじゃなくて、コールバック関数に渡すためのユーザ引数としてvoid*型のポインタを受け取っているだけなのです。

ですから、コールバック関数では、次のようにして要求された作業を遂行します。

void PNGRead(png_structp png_ptr,png_bytep data,png_size_t length)
{
    std::istream* is=(std::istream*)png_get_io_ptr(png_ptr);
    is->read((char*)data,length);
}

void PNGWrite(png_structp png_ptr,png_bytep data,png_size_t length)
{
    std::ostream* os=(std::ostream*)png_get_io_ptr(png_ptr);
    os->write((const char*)data,length);
}

void PNGFlush(png_structp png_ptr)
{
    std::ostream* os=(std::ostream*)png_get_io_ptr(png_ptr);
    os->flush();
}

png_get_io_ptrによって、png_set_read_fun/png_set_write_funで与えた引数をpng_ptrで示されるコンテキストオブジェクトから取得できるので、それをストリームへのポインタにキャストして仕事をまっとうしているわけです。意味は分かりますよね。Vismでも同じ手法を用いています。

MNGアニメーションについて

ここまでは前振りで、ここからが本題です。

PNGにはライセンス上の問題がないので、これを制作しているゲームの標準アニメーションフォーマットにするのは良い考えに思えます。というよりも、そうするために調査を始めたわけですが。

しかし、MNGには、現状で、大きく分けて以下のような問題点があります。

対応アプリケーションについてはここに載っていますが、調べてみたところ

pngcheck
ただのコマンドラインチェッカ(もちろんその目的では便利)。
Animation Shop
専用文書ファイルが.mngという拡張子であるが、あくまで「Animation Shop アニメーション」という主張でMNGファイルとはうたっていない。調べると、なんとなくそれっぽいファイルではあるのだが、MNG仕様と細部が合わない。pngcheckでもハネられる。
MNGeye
スクリプトからmngファイルを生成する形式で、GUIエディット機能がない。ブラウザとしては使える。

といった状況で、フツ〜にMNGファイルを作ったり編集したりすることもできないのでした。IEやNNのようなWebブラウザでも当然見ることができません。

デザイナに「アニメーションはスクリプトを書いて作れ、フレーム数は気合で調整しろ」というのも酷なので、それらを編集することができるだけのエディタを適当に作ってみることにしました。

MNGエディタ

つ〜ことで、投げやりにMNGEditと命名。どうせその場限りなので他とかぶろうが知ったこっちゃありません。

仕様としては、

という感じで、読み込む画像ファイルの時点ですでにフォトショップなどでアルファチャンネルの設定がされていることが前提の、ナメたものになっています(まあ実際その方が作業としては楽なんですけどね)。それでもフレームのウェイトの設定には最低限必要、ということでプレビュー機能はついてたりします。

で、できたのがコレです。

MNGEdit.gif (13353 バイト)

PNG/MNGの調査およびライブラリのプログラミングに2日ほど、本体は1日ほどでできましたから、大体その程度のデキです。変なファイルを食わせた場合当然落ちます(えっへん)。

MNGファイルのフォーマットは前にも述べたとおり結構複雑で、とても全部なんぞ実装してられないので、勝手に以下のように決めました。

といっても本物のサブセットではあるので、MNGeyeやpngcheckでは正当性があるものとして扱われます。プレビューではαチャンネルをサポートしていませんが、ちゃんと出力ファイルには書き出されます。もし今後まともなアニメーションエディタが出てきたら、そいつに食わせることは可能でしょう。

まあこれを見てる皆さんはこんなものはどうでもいいでしょうから、プログラムの説明に入ります。

アニメーションデータ構造

MNG仕様を見れば分かりますが、これはアニメーションというよりはグラフィックスメタファイルのフォーマットです。別に私はそんなものがほしいわけではないので、内部データ構造は以下のようにすることにしました。今後気が向いたらフルサポートもあるかもしれません。

/*============================================================================
 *
 * class animation
 *
 * 画像アニメーションクラス
 *
 *==========================================================================*/

template <int Bits>
struct animation_frame {
    vafximg::Image<Bits>    Image;
    unsigned int            Delay;
};

template <int Bits>
class animation {
public:
    typedef vafximg::Pixel<Bits>        PixelType;
    typedef PixelType::Reference        PixelReference; 
    typedef PixelType::Pointer          PixelPointer;
    typedef PixelType::ConstReference   ConstPixelReference;
    typedef PixelType::ConstPointer     ConstPixelPointer;

public:
    animation(void)
    {
        vIndex=0;
        vDelayed=0;
    }
    animation(const animation<Bits>& r)
    {
        vIndex=0;
        vDelayed=0;
        operator=(r);
    }
    ~animation(void){}

    const animation<Bits>& operator=(const animation<Bits>& r)
    {
        vFrames=r.vFrames;
        return *this;
    }

    int GetWidth(void)const {return vFrames[vIndex].Image.GetWidth();}
    int GetHeight(void)const{return vFrames[vIndex].Image.GetHeight();}
    int GetPitch(void)const {return vFrames[vIndex].Image.GetPitch();}
    st2d::Extent GetExtent(void)const{return st2d::Extent(GetWidth(),GetHeight());}
    st2d::Rect GetRect(void)const{return st2d::Rect(st2d::Point0,GetExtent());}
    byte* GetBuffer(void)
    {
        return vFrames[vIndex].Image.GetBuffer();
    }
    const byte* GetBuffer(void)const
    {
        return vFrames[vIndex].Image.GetBuffer();
    }
    int GetBufferSize(void)
    {
        return vFrames[vIndex].Image.GetBufferSize();
    }
    PixelPointer GetAddress(const st2d::Point& aPoint)
    {
        return vFrames[vIndex].Image.GetAddress(aPoint);
    }
    ConstPixelPointer GetAddress(const st2d::Point& aPoint)const
    {
        assert(GetRect().Contains(aPoint));
        return vFrames[vIndex].Image.GetAddress();
    }
    void SetPixel(const st2d::Point& aPoint,PixelType aPixel)
    {
        vFrames[vIndex].Image.SetPixel(aPoint,aPixel);
    }
    ConstPixelReference GetPixel(const st2d::Point& aPoint)const
    {
        return vFrames[vIndex].Image.GetPixel(aPoint);
    }

    // 独自インターフェイス
    void Rewind(void)
    {
        vIndex=0;
        vDelayed=0;
    }
    void NextFrame(void)
    {
        vDelayed=0;
        vIndex++;
        if(vFrames.size()<=vIndex){
            vIndex=0;
        }
    }
    void Step(void)
    {
        vDelayed++;

        if(vFrames[vIndex].Delay<vDelayed){
            NextFrame();
        }
    }
    int GetActiveIndex(void){return vIndex;}
    std::vector<animation_frame<Bits> >& RefFrames(void){return vFrames;}
    const std::vector<animation_frame<Bits> >& RefFrames(void)const{return vFrames;}

protected:
    void SetWidth(int n)    {vWidth=n;}
    void SetHeight(int n)   {vHeight=n;}
    void SetPitch(int n)    {vPitch=n;}

private:
    std::vector<animation_frame<Bits> >     vFrames;
    int                                     vIndex;
    int                                     vDelayed;

};

vafximg::Imageをご覧になったことなら分かると思いますが、それと同じインターフェイスを持っているので、画像の転送などでは透過的に扱うことができます。

作ってる途中で、これとは別にAnimationView(←→Model)みたいなクラスを作って、イメージとの透過インターフェイスや現在Indexの概念はそちらに移すべきだと思ったのですが、もう面倒になってしまったので気が向くまでやりません。

でもってロード、セーブはそれぞれvafxmnga::LoadAnimationFromMNGStream/vafxmnga::SaveAnimationToMNGStreamを用います。これらはどうしても長くなるので、VAFX最新版のVAFXMNGA.hpp/VAFXMNGA.cppをご覧になってください。

これらを使うと、以下のようになります。これはPNGファイルを読み込んで適当に裁断してMNGファイルとして出力する使い捨てコマンドラインプログラムのソースです。そんなに難しいことはないと思います。

#include "stdafx.h"
#include <VAFXImg.hpp>
#include <VAFXPNG.hpp>
#include <VAFXMNGA.hpp>
#include <VAFXWUty.hpp>
#include <fstream>

int main(int argc, char* argv[])
{
    std::ifstream ifs("test.png",std::ios::binary);

    vafximg::Image<32> image;
    vafxpng::LoadImageFromPNGStream(ifs,image);

    int w=64;
    int h=64;

    {for(int y=0;y*h<image.GetHeight();y++){
        vafxmnga::Animation<32> anim;
        for(int x=0;x*w<image.GetWidth();x++){
            vafxmnga::AnimationFrame<32> af;
            af.Image.SetExtent(st2d::Extent(w,h));
            vafximg::CopyRect(
                af.Image    ,st2d::Point0,
                image       ,st2d::Rect(x*w,y*h,(x+1)*w,(y+1)*h),
                vafximg::CopyPixel<vafximg::Pixel<32> >());
            af.Delay=10;
            anim.RefFrames().push_back(af);
        }
        char buffer[256];
        sprintf(buffer,"test%03d.mng",y);
        std::ofstream os(buffer,std::ios::binary);
        vafxmnga::SaveAnimationToMNGStream(os,anim);
    }}

    return 0;
}

MNGブラウザ

せっかくαチャンネルつきアニメーションファイルフォーマットが手に入ったのですから、これを複数配置して重なり具合を見てみたいものです。というわけで、またまた使い捨てプログラムを作ってみます。

とりあえずMNGBrowserと命名、これもあまりにも投げやりですが、もうどうでもいいです。

仕様はこんなカンジ。

こんな内容:

100,100,Animation1.mng
480,20,test1.mng
120,60,test2.mng
420,220,test3.mng

のテキストファイルを用意して、layers.txtというファイル名(決め付け!)で同じディレクトリにおいておくと、その内容にしたがってアニメーションを表示する。ちなみに、テキストファイルの内容は{x座標,y座標,ファイル名}のリスト。

実行画面はこんなカンジになります(スクリプトは上の物とは違い、別のアニメーションをならべたものです)。

MNGBrowser.gif (4582 バイト)

左上の座標は「描画FPS: (カーソルx座標,y座標)」です。この画面では分かりませんが、もちろん重ねあわせしてアニメーションしています。

というわけで、MNG/PNGの恩恵によって画面デザインのかなりの部分をデザイナ中心に合理化できました。やる気になればこれらを統合してその延長線上でインターフェイスとか状態遷移のエディタも作っていけると思いますが、そこまでやりだすとかなりの大事になるのでやるかどうかわかりません。

 

プログラムのバイナリ・ソースはこちら(整理が面倒なのでVC++のプロジェクトです)。ビルドにはVAFXとzlib・libpngが間違いなく必要ですし、それだけで足りるかどうかもよく分かりません(笑) 「何々が足りなくてビルドできない」という情報は私にも有用なので、もしうまくいかなかったらぜひ教えてください。


(C) 1999 Naoyuki Hirayama. All rights reserved.