本稿は、毎日新聞の「プログラミング・はじめの一歩」とは関係のないオリジナル記事です。
目次
ドット絵とは?
「ドット絵」というものをご存知でしょうか? 下図はその例で、左は実際のサイズ(33 x 44ピクセル)で、右はそれを拡大して表示したものです。「ドット絵」はこのように、小さな点(1 x 1ピクセルのドット)に色を付けて絵を表現する技法です。
初期のビデオゲーム(「スペースインベーダー」や「ドンキーコング」など)の環境では、画面の解像度が低く、色数も限られていたので、そういった制約を克服すべく生まれた言わば苦肉の策だったのですが、制約のなくなった今でも、その独特のレトロ感には人気があり、ピクセルアートで検索すると、多くの作品が見つかります。
「ドット絵」とは、コンピュータ画面上における出力最小単位の小さなマス目に色を置き、それを並べ合わせて表現した絵であり、昔のコンピュータの限られた解像度や使用色数、データサイズなど制約の中で、出来る限りの映像表現をするために生まれ、発展した、コンピューターグラフィックスの技法のひとつである。(ニコニコ大百科「ドット絵」)
本稿では、数値のデータからドット絵を描画する方法を見ていきます。そして最終的には、データから作成したドット絵をアニメーションする方法を探ります。
数値から絵を描く仕組み
数値から絵を描く仕組み自体はそう難しくはありません。数値を入れた配列と「プログラミング はじめの一歩 JavaScript + p5.js編
2:花を1列ずつ植える」で見た「格子ブロックのひな型コード」を使います。
function setup() {
createCanvas(400, 300);
background(220);
let dataArray = [
[0, 0, 0, 0],
[0, 1, 1, 0],
[0, 0, 0, 0]
];
const rows = 3; // 横向きの行数
const columns = 4; // 縦向きの列数
const gutter = 0; // 矩形間の空き
const w = 30; // 矩形の幅
const h = 30; // 矩形の高さ
// 矩形の開始位置をずらす量 => (offsetX,offsetY)から始まる
const offsetX = 10;
const offsetY = 10;
// 矩形を格子状に並べて描く
for (let c = 0; c < columns; c++) {
for (let r = 0; r < rows; r++) {
// 配列から数値を取り出す
const num = dataArray[r][c];
// 確認用
print(c, r);
print(num);
print('----');
// 数値によって塗り色を変える
if (num === 0) {
fill(255);
}
else if (num === 1) {
fill(0);
}
// 矩形を格子状に描く
rect(offsetX + c * (gutter + w), offsetY + r * (gutter + h), w, h);
}
}
}
このコードを実行すると、キャンバスに下図左上のグリッドが描かれます。矩形は全部で12個あり、中の2つが黒く塗られます。[コンソール]には現在対象としている配列要素の位置とその値が出力されます。下図の矢印や丸で説明しているように、矩形の位置と塗り色が配列要素に対応しているのが分かります。
配列をネストした配列で行と列を作って、そこに適切な数値を入れていくことで、矩形が集まった”模様”が作成できます。行と列の数をもっと増やし、数値の種類ももっと増やして、矩形の幅と高さを1にすると、たとえば前に示したジョーカーの小さなドット絵が表現できるようになります。
しかし、4 X 3程度の小さな行列なら手作業で作成できますが、ジョーカーを表す行列はかなり複雑になることは容易に想像がつきます。たぶん途中で嫌になるでしょう。そこで便利なwEBサービスとエクセルの力を借りることにします。
画像->エクセル->CSV->loadTable()
相当複雑なドット絵でも、次の手順を取ればp5.jsのキャンバスで表すことが可能です。
- ドット絵の画像ファイルを用意する
- 「ドット絵ナニカ」を利用して、画像ファイルをエクセルファイルに変換する
- エクセルを使ってCSVファイルに変換する
- p5.jsのloadTable()関数を使って、CSVのデータを読み込む
* ドット絵の画像ファイルがあるのなら、それをloadImage()関数で読み込めばよいのでは? と思われるかも知れません。もちろんドット絵をp5.jsのImageオブジェクトとしてただ描画したいのであればそれで構いませんが、本稿の主旨はあくまでも「数値からドット絵を描く」なので、Imageオブジェクトは使用しません。
では、手順を具体的に見ていきましょう。
ドット絵の画像ファイルを用意する
使用したい画像ファイルを用意します。初めは色数の少ないシンプルなものにします。画像はインターネットでドット絵、ピクセルアート、クリップアートなどのワードで検索すると、多数見つかります。以降では前出のジョーカーの画像を使用します。
「ドット絵ナニカ」を利用して、画像ファイルをエクセルファイルに変換する
「ドット絵ナニカ」を利用すると、画像ファイルをアップロードし、エクセルのファイルに変換して、それをダウンロードすることができます。
使い方は簡単です。[画像ファイル]のボタンをクリックして、手元にある画像ファイルを指定します。その下の[ドット絵のサイズ(幅)]はひとまずデフォルトの32ビットのままにしておきます。その下の[変換]ボタンをクリックします。
すると下図に示すような結果が表示されます。0や1などの数字が並び、マス目に色が表示されています。下図の例では、たとえば2は緑色、10はピンク色を表しているようです。また右には使用された数字とそれが対応している色、その使用数が縦に並んでいます。この対応表は後で利用します。その下には幅が32ピクセルの原寸で表示されています。
下部にある[Excel download]リンクをクリックすると、エクセルのファイルがダウンロードできます。
エクセルを使ってCSVファイルに変換する
ダウンロードしたファイルをエクセルで開きます。すると、上図と同じような”絵”が表示されます。しかしこれは絵ではなく、エクセルのセルに0や1などの数値が入れられ、その数値に対応した色がセルの塗り色に設定されたエクセルの表です。ここからは少し面倒が作業になります。
セルの塗り色を調べる
セルに入れられた0や1などの数値は、そのセルの塗り色に対応しているので、セルの数値が何色かを調べる必要があります。これは、セルの書式設定で調べます。このとき役立つのが下図の対応表です。使用数が少ない色のセルを見つけるのは大変ですが、対応表の色で使用色の見当をあらかじめつけておくと、探しやすくなります。
セルの塗り色を調べるには次のようにします。数値が対応する色を調べるので、メモを用意します。
- 調べたいセルを右クリックし、表示されるコンテキストメニューから[セルの書式設定]を選ぶ
- [セルの書式設定]が表示されるので[塗りつぶし]タブをクリックする
- [サンプル]部に調べたい色が表示される。[その他の色]ボタンをクリックする
- [色の設定]が表示される。[ユーザー設定]タブのRGB値をメモする
- [キャンセル]ボタンを2回クリックして、元の画面に戻る
- これを調べたいセルで繰り返す
- メニューの[ファイル]をクリックし[エクスポート]を選択する
- 表示される[エクスポート]画面で[ファイルの種類の変更]をクリックする
- [ファイルの種類の変更]で[別のファイル形式として保存]をクリックする
- [名前を付けて保存]が表示される。[ファイルの種類]で[CSV UTF-8(コンマ区切り)(*.csv)]を選んで[保存]をクリックする
下図は数値が9で色が黄色のセルを調べているときに表示された[セルの書式設定]と[色の設定]の例です。数値の9はRGB値の(222,223,33)に対応していることが分かります。
このようにして、ジョーカーの場合なら、0から12がどのRGB値に対応しているかを調べます。
CSVファイルに変換する
エクセルのファイルをCSVファイルに変換するには、次のようにします。
loadTable()を使ってCSVのデータを読み込む
p5.jsのloadTable()関数を使うと、CSVファイルに保持したデータを容易に読み取ることができます。詳細は「11_1:表データ p5.js JavaScript」で述べています。
複雑なドット絵のCSVデータを描画する
では、変換したCSVファイルからデータを読み取り、p5.jsのキャンバスに再現するコードを見ていきましょう。前の「数値から絵を描く仕組み」ではネストした配列を使いましたが、ここでは代わりにCSデータを使っています。
// ドット絵
// ジョーカーの絵のCSVファイルからジョーカーの絵を描く
let dotEdata;
function preload() {
// ジョーカーのCSVファイルを読み込む
dotEdata = loadTable('data/joker_data.csv', 'csv', '');
}
function setup() {
createCanvas(320, 320);
background(255);
// データの行と列数の確認
print(dotEdata.getRowCount()); // 42個
print(dotEdata.getColumnCount()); // 32個
const rows = dotEdata.getRowCount(); // 行数
const columns = dotEdata.getColumnCount(); // 列数
const gutter = 0; // ドット絵なので空きはなし
const w = 2; // 矩形の幅
const h = 2; // 矩形の高さ
// 矩形の開始位置をずらす量
const offsetX = 10;
const offsetY = 10;
noStroke();
for (let c = 0; c < columns; c++) {
for (let r = 0; r < rows; r++) {
// getString()は文字列を返すのでint()関数で数値化する
const num = int(dotEdata.getString(r, c));
// CSVファイルに書かれた数値を調べ、それが対応する塗り色にする
// 個数の多いものから調べる
if (num === 0) {
fill(255, 0);
}
else if (num === 1) {
fill(0);
}
else if (num === 5) {
fill(206, 207, 206);
}
else if (num === 4) {
fill(156, 154, 156);
}
else if (num === 8) {
fill(99, 101, 99);
}
else if (num === 3) {
fill(49, 48, 49);
}
else if (num === 2) {
fill(0, 186, 0);
}
else if (num === 10) {
fill(189, 69, 115);
}
else if (num === 11) {
fill(255, 255, 115);
}
else if (num === 9) {
fill(222, 223, 33);
}
else if (num === 12) {
fill(222, 186, 156);
}
else if (num === 6) {
fill(173, 170, 173);
}
else if (num === 7) {
fill(66, 69, 66);
}
// 矩形を描画
rect(offsetX + c * (gutter + w), offsetY + r * (gutter + h), w, h);
}
}
}
数は実行結果です。
基本は「数値から絵を描く仕組み」なのですが、色数が多いのでその文if…else if文が長くなっています。ここでは変数numが参照する数値を場合分けし、前に調べたRGB値を塗り色に指定しています。
if…else if文では、条件が満たされた時点で以降のelse ifは実行されないので、条件を満たすものをできるだけ先に調べる方が効率的です。今の場合で言うと、たとえば0は487個、1は197個、7は1個です。これは「ドット絵ナニカ」で変換したときの対応表から分かります。個数は0や1、7で表される色で塗られる矩形の数を示しています。
したがって、個数が一番多い0は最初に、次に多い1はその次に、そして個数が1個と最少の7は最後に調べるのが効率的です。
しかし、今の場合のように色数がまだ少ないときは手書きによる比較で何とかなるでしょうが、もっと多くなるとひどく面倒です。この場合にもエクセルが役立ちます。
数はジョーカーの対応表の数字を写したエクセルの表です。
これをエクセルの[ユーザー設定の並び替え]機能を使って、B列の大きいもの順に並び替えます。
この結果から、if…else if文で変数numを調べる順番は、0,1,5,4,8,…7にすればよいことが分かります。
オリジナルのドット絵のデータの描画
もちろん自分で描いたドット絵のCSVデータをp5.jsのキャンバスに描画することも、同じ要領で可能です。下図は、32 x 32のサイズで犬の絵を描いた、Windows付属の[ペイント]アプリの画面と、そのCSVデータを読み込んでキャンバスに描画した結果です。
問題になるのは、ドット絵を描くテクニックだけです。[ペイント]アプリでドット絵を描く方法は、「マウスとMS付属のペイントで自己流ドット絵メイキング(1)」ページなどが参考になります。
インベーダーのアニメーション
最後に、画像ファイルを使わず(「ドット絵ナニカ」を使わず)、エクセルで直接データを2つ作成して、それらを交互に描画することでアニメーションする方法を見ていきます。
下はその実行画面です。画面のクリックでアニメーションのオン/オフが行えます。
データの作成
まず必要なのがインベーダーのデータです。これは、エクセルの表の適切なセルに数値を入れていくということです。
インベーダーは下図の8行12列のマス目を塗ることで2つ作成します。数値は0と1の2種で、0を透明の白、1を緑色に対応させます。
これに対応するエクセルの表を作成します。12列なのでL列までが埋まります。
エクセルからCSVファイルを書き出し、プログラムのdataフォルダに置きます。
プログラムの作成
上の2つのインベーダーが左右に歩くアニメーションでは、次のコードを記述しています。
// ドット絵
// インベーダーが左右に移動するアニメーション
// InvaderAクラスのインスタンス2つ
let invA_1, invA_2;
// 動いているかどうか
let isMoving = false;
// CSVデータを読み込んでインスタンスを2つ作成
function preload() {
invA_1 = new InvaderA(['data/invA_1.csv', 'data/invA_2.csv'], 0, 0);
invA_2 = new InvaderA(['data/invA_1.csv', 'data/invA_2.csv'], 50, 50);
}
function setup() {
createCanvas(400, 300);
noStroke();
// フレームレートを低くしてアニメーションの切り替えを遅くする
frameRate(2);
}
// 画面のクリックで移動をトグル
function mousePressed() {
// isMoving変数を反転
isMoving = !isMoving;
}
function draw() {
background(0);
// isMovingがtrueならアニメーションする
if (isMoving) {
invA_1.update();
invA_2.update();
}
// 描画する
invA_1.display();
invA_2.display();
}
// InvaderAクラス
class InvaderA {
// コンストラクタはCSVデータが2つ入った配列と(x,y)位置を受け取る
constructor(dataArray, x, y) {
// CSVデータを保持する配列
this.dataArray = [];
// CSVデータの読み込みには時間がかかる。1つずつ読み込む
loadTable(dataArray[0], (data1) => {
// 1つめのデータを配列に追加
this.dataArray.push(data1);
// 1つめの読み込みが終わったrあ2つめを読み来m
loadTable(dataArray[1], (data2) => {
// 2つめのデータを配列に追加
this.dataArray.push(data2);
// 描画にしようする現在のデータを配列の0番めのデータにする
this.currentData = this.dataArray[0];
});
});
this.x = x;
this.y = y;
this.speedX = 8;
this.w = 3;
this.h = 3;
this.offsetX = 10;
this.offsetY = 100;
}
// 描画する
display() {
for (let c = 0; c < this.currentData.getColumnCount(); c++) {
for (let r = 0; r < this.currentData.getRowCount(); r++) {
const num = int(this.currentData.getString(r, c));
if (num === 0) {
fill(255, 0);
}
else {
fill(83, 240, 120);
}
// this.xを加えることで移動するようになる
rect(this.offsetX + c * this.w + this.x, this.offsetY + r * this.h + this.y, this.w, this.h);
}
}
}
// 更新する
update() {
// 位置を更新
this.x += this.speedX;
// 描画に使用するCSVデータを切り替える(0,1,0,1,...)
this.currentData = this.dataArray[frameCount % 2];
// 右端と左端で反転
if (this.x > width - this.currentData.getColumnCount() * this.w - this.w || this.x < 0) {
this.speedX *= -1;
}
}
}
このプログラムでは、オブジェクト指向プログラミング(OOP)と呼ばれる手法を使って、インベーダーを作成しています。インベーダーの元になるのはInvaderAクラスです。このクラスのコンストラクタは引数として、インベーダーのCSVファイルへのパスを2つ入れた配列と、最初に描画する(x,y)座標を取ります。
コンストラクタでは、p5.jsのloadTable()関数を2つ使って、CSVファイルを1つずつ読み込んでいます。最初のloadTable()関数の2つめの引数に指定する関数で2つめのloadTable()関数を呼び出すことで、1つめのCSVファイルの読み込みが完了してから2つめのCSVファイルの読み込みを始めることができます。
アニメーションして見える論理の更新はupdate()メソッドで行っています。currentDataは今現在使用しているCSVデータで、これは足を開いているインベーダーか閉じているインベーダーのどちらかです。これをframeCount % 2を使って、呼び出されるたびに切り替えています。frameCount % 2は0,1,0,1,…を繰り返します。
display()メソッドでは、「格子ブロックのひな型コード」を使って、CSVのデータを持つp5.Tableオブジェクトから0か1を読み取って透明の白か緑の矩形を描画しています。rect()関数の(x,y)位置の引数にthis.xとthis.yを加えることで、インベーダーは左右に移動します。
またsetup()関数ではframeRate(2)を呼び出しています。これによって、通常は毎秒60回呼び出されるdraw()関数が、毎秒2回に激減します。0.5秒に1回、足を開いたインベーダーを描画し、0.5秒に1回、足を閉じたインベーダーを描画して、左右どちらかに8だけ移動させることで、レトロな動きを再現しています。