Code of Poem
Unknown programmer's programming note.

pcat (2) 画像を表示する

何を作るか大体きまったので始めていきます。まずはステージを1つ作ります。

ステージというものにどのようなものをイメージしているかというと、一言でいうとマリオのステージ1-1みたいなのです。別にマリオのクローンを作ろうとしているわけではなく、大体のイメージです。

pcat stage 1-1 image

ステージをどのよう見せるかというと、上の図のように四角形の塗りつぶしだけで組み立てることも可能です。しかしあまり楽しそうではないので、画像を使って組み立てるようにします。そこで、これまで作ったものの中ではまだ画像を表示させるコードは書いてなかったので、まずはそれをやります。

画像とはなにか

コードを書く前に予備知識としていくつか調査しておきます。このセクションは適当に書いているので鵜呑みにしないでください。むしろ読む必要すらありません。

「画像」という単語はおそらくPCやインターネットの普及に伴い一部の専門家ではない人の間でも広く使われるようになった言葉です。PCに保存されている拡張子が.jpgや.pngといったファイルを開くと画像ビューアーなどと呼ばれるアプリケーションが起動して、そのファイルのデータがイメージとしてディスプレイに表示されます。Webページを表示するとHTMLなどによって指定されている.jpgや.pngといったファイルのデータがページの中に表示されます。ここで.jpgや.pngの拡張子を持つファイルは画像ファイルと呼ぶことが出来ます。画像という単語はPCやインターネットを利用する人々の会話の中では日常的に使われる単語です。例えば、「この画像いいね」とか「この画像のここ拡大してみて」です。画像ファイルという単語は「メールでこの画像ファイル送信して」などと使われます。画像ファイルとういうの長ったらしいので「メールでこの画像送信して」などといっても意味は通じるでしょう。画像とは、何らかのイメージのデータを表すものを、それがファイルであれディスプレイに表示されているものであれ、全部ひっくるめて指して用いられているものと考えることが出来ます。

このような「画像」という単語を用いると、話し手から聞き手に大体の意味が通じれば、その文脈において人間が正しい解釈をしてくれるので、日常会話や文章では大変便利な単語です。

もう少し掘り下げてみます。例えば猫の動画を撮影してcat.mp4というファイルを入手したとします。そのファイルの拡張子を変更してcat.jpgとして猫好きの人にメールで送信したとします。受信した人は画像を入手したと言えるでしょうか?そうは言えないでしょう。なぜなら、もとのファイル.mp4は動画のデータが格納されたものであり、拡張子を画像であることを示す.jpgに変えたとしても画像として取り扱うことは通常はできないからです。受信した人がメールに添付されたcat.jpgをクリックして開こうとすると、使用している環境で.jpgに紐付けられたプログラムが起動します。まずプログラムはファイルのヘッダー部分を読み込もうとするでしょう。しかし、その.jpgファイルは.mp4というファイルのフォーマットに従ったファイルヘッダーを格納していて、.jpgというファイルフォーマットのヘッダーとしては認識できません。賢いプログラムなら「拡張子が間違ってます。これは.mp4のファイルです。」といった情報をメッセージボックスで知らせてくれるかもしれません。仮にプログラムが無理やりそのファイルのヘッダー部分を.jpgファイルのヘッダーとして解釈して、ヘッダー部分以降を画像のデータとして扱ったとしてもでたらめなデータになります。そして受信した人は「画像見れないよ😾」と返信することになります。

ファイルヘッダーという用語を使いました。実は.jpgや.pngといったファイルの先頭部分、まさにヘッダー部分には、そのファイルがどういうものなのかを記録したものが含まれています。例えば画像の幅がいくつで高さがいくつで、といった情報です(正確には.jpgファイルはもう少し複雑な作りになっていて単にファイルヘッダーとは呼ばれないようです)。注目すべきところはファイルヘッダーとイメージのデータの2つに分かれているところです。ファイルヘッダーは不特定のプログラムにそのファイルのイメージデータをどのように扱えば良いかを伝達するために必要な情報です。しかし、もしそのファイルを自分のプログラムから表示させないのであり、どのように扱えばよいのかを自分のプログラムだけが知っていればよいのであれば、ヘッダー部分を省略してしまいイメージデータだけのファイルとして構成することも可能です。例えば、8ビットで1対の色を表すイメージフォーマットで、幅x高さが1x1つまり1ドットの画像ファイルを作るとします。そのファイルの中身が 0b11111111 は白の1ドットの画像で 0b00000000 は黒の1ドットの画像になります。ファイルサイズは1バイトになります。

現存する画像ファイルのフォーマットはたくさんあります。特定のプログラムでしか使われないものを含めるととてもたくさんになります。.jpgや.pngは特にインターネットを通して広く支持を得たものの例であります。たくさん存在する理由は、例えばプログラミング言語がたくさん存在していることと状況は類似しているといえるかもしれません。多くのプログラムによってサポートされることによって一極集中が進むというか、利用者が増えれば増えれば加速して普及していくことになります。ゲームにおいては特に.jpgや.pngが最適の画像フォーマットということもないですが、広く使われているというのは採用する大きな動機になります。.jpgは圧縮アルゴリズムがあまり簡単とは言えないのでやや高度なトピックになると思われます。といってもライブラリのサポートが得られるならその部分はあまり問題にはならないでしょう。.pngは透過もサポートされていて扱いやすいため採用されやすいかと思います。

もう一つだけ画像について、人間同士のやり取りで画像といったとき、それは人間が認識できるなんらかのパターンを指していっています。要はディスプレイに表示されて初めて意味を持つものです。しかし、ごく単純な画像ではない、ある程度の複雑さを持った画像は(例えばcat.jpgのような画像データ)、超能力者でもない限り、人間の手でその画像データを作成することはできません。コンピュータプログラムによる助けが必要です。猫の写真のようにデジタルカメラで作成することも可能ですが、デバイスにはおそらく画像を処理するプログラムが組み込まれているのでしょう。また、作成されたデータを直接見て、つまりビットパターンを見て猫の写真だと認識するのは、やはり超能力者でもない限り難しいです。超能力者の存在を否定することは出来ないので、そういうことができる人間がどこかにいるかもしれません。表示して人間が認識できるようにするにもコンピュータプログラムの助けが必要です。何が言いたいかというと、どんなに優れたアーティストであっても、コンピュータプログラムの介入無しでデジタルアートの作成は不可能だということです。そして、そのプログラムを作成するのはプログラマです。つまり、アーティストとプログラマは依存しあっているといういことです。これは料理人が野菜を生産する農家に目に見えなくても依存しているのと同じようなものと言えるかもしれません。

プログラムで画像データを解析しても、最終的にディスプレイに表示されなければ人間にはその画像データが表現しているものな何なのか認識することは出来ません。別の可能性を考えるとディスプレイとは限らないかもしれません。紙に印刷するためにプリンタにデータが送られるかもしれませんし、LEDを集めて作られた電光掲示板のような特殊なディスプレイでの表示に変換されるかもしれません。しかし、何らかのハードウェアへ情報を送信することによってハードウェアの状態を変化させることで、そのビットパターンから実世界での表現に置き換えることによって人間に認識されることに変わりはありません。PCのディスプレイでの表示に限っていうなら、PCに搭載されたグラフィックスカード、あるいは内臓のグラフィクスハードウェアにあるビデオメモリにビットを転送することで、接続されたディスプレイに表示されるようになります。と言いたいところですが現代のPCのグラフィックス機能はそんなに単純ではありません。直接ビデオメモリにアクセスすることは出来ません。ビデオメモリが単にディスプレイのドットと1対1で対応しているわけでもありません。ドライバを介してアクセスするか、OSにお願いするか、DirectXやOpenGLといったハードウェアにアクセスするためのグラフィックスAPIを使うかなどする必要があります。とにかく何らかの方法でPCに搭載されているビデオメモリの特定の位置に、適切なフォーマットに調整されたビットパターンを転送することで、最終的にはディスプレイの光に変換されて人間の目に入ってきて、それが画像であると認識されるわけです。

まとめておきます。

テクスチャとスプライト

くだらない考察は終わりにして、実際何をすればいいのかを検討していきます。くだらないと言いながらも先の考察で得られた実用的なものの一つに、イメージデータをなんとかしてグラフィックスハードウェアに伝達しないといけないということがあります。それをするにはどうするかということです。

  1. ビデオメモリに直接アクセスする(無理)。
  2. デバイスドライバにアクセスする。
  3. OSにお願いする。
  4. DirectXやOpenGLといったグラフィックスAPIを使う。

これらのどれも使いません。前回書いたのですが、SFMLというライブラリを使うことにしました。このライブラリを使えば恐ろしく簡単に画像を読み込んだりディスプレイに表示したりできるようになります。SFMLが内部的にどのようにして実現しているかまではわかってませんが、とにかくこう書けば動くというとこまでは分かります。完全にブラックボックス化してしまうというわけでもなく、ソースコードは公開されているので知りたければソースを読むことで知ることが出来ます。ですが、今はゲームを作ることが目的であるので詳細までは追い求めないことにします。

SFMLで画像を扱うにあたって知っておくべきことがあります。テクスチャとスプライトという用語です。SFMLのチュートリアルには次のように書かれています。

A texture is an image. But we call it "texture" because it has a very specific role: being mapped to a 2D entity.

テクスチャは画像です。しかし、2Dエンティティにマッピングされるという非常に特殊な役割があるため、これを「テクスチャ」と呼びます。

A sprite is nothing more than a textured rectangle.

スプライトは、テクスチャを持つ長方形にすぎません。

Sprites and textures (SFML / Learn / 2.5 Tutorials)

画像を表示するコードも上の引用元のリンク先を読めば良いのであまりこれから先書き続ける意義が薄れてきた気もするのですが気にしないでおきます。

SFMLではスプライトという用語は上のように定義されています。スプライトはSFMLに固有の呼び名というわけではなく広く使われています。しかし、その意味するところは安定していません。だいたいゲームに関連するグラフィックスの要素をさして使われいる点では共通しています。より広く使われているのはゲームに使えるグラフィックスの要素を貼り付けた画像という感じで使われています。複数の要素をパックにしたものはスプライトシートなどと呼ばれています。上でSFMLがテクスチャと呼んでいるものに近く混乱しやすいです。

世間でどう呼ばれていようが、これからはSFMLを使うのでSFMLの呼び方に従います。SFMLでテクスチャに対応するものはsf::Textureで、スプライトに対応するものはsf::Spriteとなっています。さらに2Dエンティティにマッピングされない単なる画像に対応するものもありsf::Imageとなっています。

テクスチャは画像ファイルのパスを指定することでそのファイルのイメージデータを保持したものを作成することが出来ます。

#include <SFML/Graphics.hpp>

int main() {
  sf::Texture texture;
  if (!texture.loadFromFile("1.png")) {
    return 1;
  }
}

このコードのソースファイル名がtexture_example.cppだとすると、コンパイルするには次のようにします。

g++ texture_example.cpp -lsfml-system -lsfml-window -lsfml-graphics -o texture_example

コンパイルに成功すれば、次のコマンドで実行できます。

./texture_example
Failed to load image "1.png". Reason: Unable to open file

エラーメッセージはSFMLが生成したものです。明示的にエラーメッセージを出力するようには記述していませんが、コンソールで実行した場合は自動で出力されるようになっているようです。ロードに失敗した場合、このエラーメッセージを確認することで何が問題なのか特定するのに役に立ちます。上では1.pngというファイルを開けないと言っています。とりあえず1.pngというファイルを用意して配置します。1.pngはさしあたって次のような画像を使うことにします。

pcatw 1.png

この画像は32x32ピクセルのPNG画像でステージの地面に使うつもりで適当に作ったものです。配置してプログラムを実行すればエラーメッセージは出力されなくなります。

sf::Texture::loadFromFileが扱える画像ファイルのフォーマットは bmp, png, tga, jpg, gif, psd, hdr, pic の8種類だそうです。sf::Image::loadFromFileに書かれています。なぜsf::Imageの方を参照しているかというと、sf::Texture::loadFromFileは、次のコードと同等だからです。

sf::Image image;
image.loadFromFile(filename);
texture.loadFromImage(image, area);

テクスチャに画像ファイルを読み込むことが出来ました。しかし、テクスチャ自体には画面に表示させるための能力がありません。

テクスチャを画面に表示させるにはスプライトにマッピングする必要があります。

#include <SFML/Graphics.hpp>
    
int main() {
  sf::Texture texture;
  if (!texture.loadFromFile("1.png")) {
    return 1;
  }
  
  sf::Sprite sprite;
  sprite.setTexture(texture);

}

sf::Spriteのコンストラクタにはテクスチャを引数とするものが用意されているので1行で書くことも出来ます。

sf::Sprite sprite{texture};

また、sf::Sprite::setTextureRectや、テクスチャともう1つ引数に取るコンストラクタの引数に矩形の範囲を指定して、サブ矩形を取り扱うことが出来ます。言い換えればテクスチャのどの部分をスプライトにマッピングするか指定することが出来ます。

sf::Sprite::setTextureRectは次のようにします。

sf::Sprite sprite;
sprite.setTexture(texture);
sprite.setTextureRect(sf::IntRect{0, 0, 16, 16});

コンストラクタで指定する場合は次のようにします。

sf::Sprite sprite{texture, sf::IntRect::{0, 0, 16, 16}};

sf::Spriteはsf::Drawableを継承しているので、sf::RenderTarget::drawでレンダーターゲットに描画することが出来ます。特に、sf::RenderWindow::drawでウィンドウに描画することが出来ます。

今回使用する画像には32x32のタイル(このように正方形パターンからなる画像のことをタイルと呼びます)が1枚含まれているだけなので、画像全体を使用します。したがって、サブ矩形は使用しません。

これまでのコードを合わせてウィンドウに画像を表示する最もシンプルと思われるコード全体を書いてみます。

ウィンドウのサイズは320x240 ((32*10)x(32*7.5))です。

#include <SFML/Graphics.hpp>

int main() {
  sf::RenderWindow window{sf::VideoMode{320, 240}, "pcat"};

  sf::Texture texture;
  if (!texture.loadFromFile("1.png")) {
    return 1;
  }

  sf::Sprite sprite{texture};

  while (window.isOpen()) {
    sf::Event event;
    while (window.pollEvent(event)) {
      if (event.type == sf::Event::Closed) {
        window.close();
      }
    }

    window.clear(sf::Color{0, 0, 255});
    window.draw(sprite);
    window.display();
  }
}

このコードをコンパイルして実行すると次のような結果が得られます。

pcat simplest sprite screenshot

画面左上に一つ表示することが出来ました。

この画像は地面のつもりで用意したものです。簡単な修正で画面下に並べて表示することができるので、それをやってみます。

sf::Sprite::setPositionで描画させる位置を指定することが出来ます。スプライトの大きさの取得はちょっと分かりづらいです。sf::Sprite::getTextureRectでマップされているテクスチャの矩形情報が取得できます。もし、スプライトがスケール(拡大縮小)されていると、スプライトの大きさとマッチしないのでsf::Sprite::getScaleで得られる値と乗算する必要があります。しかし、今はスプライトをスケーリングしないのでgetTextureRectの値をそのまま使うことが出来ます。

#include <SFML/Graphics.hpp>

int main() {
  sf::RenderWindow window{sf::VideoMode{320, 240}, "pcat"};

  sf::Texture texture;
  if (!texture.loadFromFile("1.png")) {
    return 1;
  }

  std::vector<sf::Sprite> ground(10);
  for (size_t i = 0; i < ground.size(); ++i) {
    ground[i].setTexture(texture);
    ground[i].setPosition(
        i * ground[i].getTextureRect().width,
        window.getSize().y - ground[i].getTextureRect().height);
  }

  while (window.isOpen()) {
    sf::Event event;
    while (window.pollEvent(event)) {
      if (event.type == sf::Event::Closed) {
        window.close();
      }
    }

    window.clear(sf::Color{0, 0, 255});

    for (auto &s : ground) {
      window.draw(s);
    }

    window.display();
  }
}

コンパイルして実行すると次のような画面になります。

pcatw simplest ground sprite screenshot

一応地面っぽく見えなくもないような地面ができました。

参考

今回使用したSFMLのドキュメントへのリンクを載せておきます。