Last update 1999/09/23
(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎
……ってこれはさすがに説明の必要はないですね。
ご存じない方は適当に検索してしらべるなどしてください。
簡単に言うと、レイヤ画像、アニメーションなど複数のイメージを必要とするオブジェクトを規定するフォーマットで、内部的にはPNGフォーマットが使われます。まだ正式な仕様としては定まっていないようです。
本家はこちら。日本語版を海賊版として訳されている方もいらっしゃいますね。
PNGフォーマットを自作プログラムから利用しようとする人の便宜を図って、libpngというPNGフォーマット操作用のライブラリが用意されています。以前Cマガジンで紹介されたので、ご存知の方も多いでしょう。Cライブラリですが、結構オブジェクト指向的です。
libpng最新版はこちらで手に入ります。VC++でライブラリを作成する方法については、こちらを参照のこと。というわけで、以下PNGの利用には、そうやって作ったzlib.libとlibpng.libをリンクする必要があります。
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でも同じ手法を用いています。
ここまでは前振りで、ここからが本題です。
PNGにはライセンス上の問題がないので、これを制作しているゲームの標準アニメーションフォーマットにするのは良い考えに思えます。というよりも、そうするために調査を始めたわけですが。
しかし、MNGには、現状で、大きく分けて以下のような問題点があります。
- 仕様がまだドラフト。
- 結構デカくてややこしい。
- 今後標準になる保証もない。
- サポートしているアプリケーションがほとんどない!
対応アプリケーションについてはここに載っていますが、調べてみたところ
- pngcheck
- ただのコマンドラインチェッカ(もちろんその目的では便利)。
- Animation Shop
- 専用文書ファイルが.mngという拡張子であるが、あくまで「Animation Shop アニメーション」という主張でMNGファイルとはうたっていない。調べると、なんとなくそれっぽいファイルではあるのだが、MNG仕様と細部が合わない。pngcheckでもハネられる。
- MNGeye
- スクリプトからmngファイルを生成する形式で、GUIエディット機能がない。ブラウザとしては使える。
といった状況で、フツ〜にMNGファイルを作ったり編集したりすることもできないのでした。IEやNNのようなWebブラウザでも当然見ることができません。
デザイナに「アニメーションはスクリプトを書いて作れ、フレーム数は気合で調整しろ」というのも酷なので、それらを編集することができるだけのエディタを適当に作ってみることにしました。
つ〜ことで、投げやりにMNGEditと命名。どうせその場限りなので他とかぶろうが知ったこっちゃありません。
仕様としては、
- 32bit画像(RGB+αチャンネル)しか扱わない(なんて傲慢な!)
- MNGの読み書きが可能。
- PNGはインポートして裁断してMNGにすることが可能。PNGのMNGへの追加なら一応ファイルドロップも可能。
- 順番の入れ替えとか削除はできない(これを作った目的ではそれで十分だった)
という感じで、読み込む画像ファイルの時点ですでにフォトショップなどでアルファチャンネルの設定がされていることが前提の、ナメたものになっています(まあ実際その方が作業としては楽なんですけどね)。それでもフレームのウェイトの設定には最低限必要、ということでプレビュー機能はついてたりします。
で、できたのがコレです。
PNG/MNGの調査およびライブラリのプログラミングに2日ほど、本体は1日ほどでできましたから、大体その程度のデキです。変なファイルを食わせた場合当然落ちます(えっへん)。
MNGファイルのフォーマットは前にも述べたとおり結構複雑で、とても全部なんぞ実装してられないので、勝手に以下のように決めました。
- {フレーム情報,イメージ}の並び。
- 中に入っている画像は必ず32bit画像。
といっても本物のサブセットではあるので、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; }
せっかくαチャンネルつきアニメーションファイルフォーマットが手に入ったのですから、これを複数配置して重なり具合を見てみたいものです。というわけで、またまた使い捨てプログラムを作ってみます。
とりあえずMNGBrowserと命名、これもあまりにも投げやりですが、もうどうでもいいです。
仕様はこんなカンジ。
こんな内容:
100,100,Animation1.mng 480,20,test1.mng 120,60,test2.mng 420,220,test3.mngのテキストファイルを用意して、layers.txtというファイル名(決め付け!)で同じディレクトリにおいておくと、その内容にしたがってアニメーションを表示する。ちなみに、テキストファイルの内容は{x座標,y座標,ファイル名}のリスト。
実行画面はこんなカンジになります(スクリプトは上の物とは違い、別のアニメーションをならべたものです)。
左上の座標は「描画FPS: (カーソルx座標,y座標)」です。この画面では分かりませんが、もちろん重ねあわせしてアニメーションしています。
というわけで、MNG/PNGの恩恵によって画面デザインのかなりの部分をデザイナ中心に合理化できました。やる気になればこれらを統合してその延長線上でインターフェイスとか状態遷移のエディタも作っていけると思いますが、そこまでやりだすとかなりの大事になるのでやるかどうかわかりません。
プログラムのバイナリ・ソースはこちら(整理が面倒なのでVC++のプロジェクトです)。ビルドにはVAFXとzlib・libpngが間違いなく必要ですし、それだけで足りるかどうかもよく分かりません(笑) 「何々が足りなくてビルドできない」という情報は私にも有用なので、もしうまくいかなかったらぜひ教えてください。
(C) 1999 Naoyuki Hirayama. All rights reserved.