最初に
この記事は、2012年に発売されたNintendo 3DSで発売された「とびだせどうぶつの森」のために書いた記事です。Switch版の「あつまれどうぶつの森」のマイデザインに関しては、新たに記事書きましたので興味ある方は以下参照ください。
Processing Advent Calendar 2012 参加します
Processing Advent Calendar 2012というProcessingに関した記事を毎日持ち回りで投稿する企画に、発起人の@reona396さんからお誘いを受け参加しています。
ちなみにProcessingは何かというとプログラミング言語なのですが、よいのはまずマルチプラットフォームで、いろんなOSで動くことと、手軽に何かつくれる、特に絵が簡単にかけるのが素晴らしいです。他のプログラミング言語だと、普通絵を描くのに色々約束事があるのですが、Processingだとほんと一行で絵がかけてしまいます。あとは、Arduinoという最近脚光を浴びている安価なイタリア製マイコンの開発言語のベースになっていたり、Kinectを自在に操ることができたりと拡張性も大きいです。
そんなわけで、自分みたいにプログラムの正しさとか、保守性とか興味無くてツールとして使いたいという人にはうってつけの言語ではないかなと思います。
Processingで「どうぶつの森のマイデザイン補助ツール」を作る
そんなProcessingを使ってこんなことができるよという例として、今回はどうぶつの森のマイデザイン補助ツールを作ってみたいと思います。こんなことまでできるんだぜ!凄いぜProcessing!
もともとはtwitter上でとある方に、昔つくった「Processingで写真や画像をドット絵に変換するソフト」に関して質問受けた会話の流れで、このツールを実はどうぶつの森のマイデザインの際の補助ツールとして使っていると聞いたのがきっかけです。
@karaage0703 実はとびだせ どうぶつの森のマイデザインというものでドット絵を書く機会がありまして、画像をドット変換するツールを探してたのですが、どれもしっくり来るものがなく悩んでいたところからあげさんのブログを見つけたという流れでした。
— kupa (@kupa_mentos) 2012年11月14日
そんな使い方があるなんて想定もしていなかったので、びっくりしました。なんでもとびだせどうぶつの森のマイデザインという機能では、32×32のドットに159色のパレットから15色を選んでドット絵をつくるとのこと。その方は、どうも私のソフトで絵や写真からドット絵にした後、減色処理を別のソフトで行ってから、その絵を参考にドット絵を書くという使い方をしているようでした。
需要あると知ってしまったら、もうちょい便利なものにしてみようと思うのが人情、どうぶつの森専用にカスタマイズして32×32のドット絵生成した後、どうぶつの森のパレットの色に対応する159色まで減色させた後、さらに同時使用可能色数である15色まで減色して、どのパレットの色を使えばよいかわかる表も並べて表示するようなソフトを作ってみました。
もちろんこのためにとびだせどうぶつの森をダウンロード購入したぜ!たぬきちの馬鹿野郎!
マイデザイン補助ツール結果
言葉で説明するとわかりにくいので実例を示します。
右側の数字は、どのドットがどのパレット色に対応しているかを示しています。色と数字の関係は「とびだせどうぶつの森 マイデザインのパレット 全カラーコード公開」を参照して下さい。とびだせどうぶつの森を持っている人はこれの意味がわかるはず!きっと!
ドット絵変換のしくみ
ドット絵にするところは、「Processingで写真や画像をドット絵に変換するソフトを作ってみました」でつくったソフトをそのまま使用。画像を32×32に分割した後に、それぞれの分割されたエリアの色情報(RGB値)を平均化しているだけです。
その後は減色処理。こんなんきっと探せばうまいやり方沢山でてくるだろと思っていたのですが、予想外に奥が深い分野みたいでなかなか簡単にできる方法が無かったです。私がとった方法は、まずドット絵の全てのドットに対して、マイデザインのパレットの一番近い色に変換してやります。変換に関しては、一番ポピュラな色距離を使った方法をとっています。具体的には、ドット絵のあるドットの色情報を(Rx,Gx,Bx)、どうぶつの森のマイデザインのパレット159色を(R1,G1,B1)〜(R159,G159,B159)と定義したとき、それぞれのパレットの色とドットの色距離は (Rx-R1)^2+(Gx-G1)^2+(Bx-B1)^2〜(Rx-R159)^2+(Gx-G159)^2+(Bx-B159)^2で表すことができて。これが一番小さいものを一番近い色と見なすという考え方です。
159色から15色の減色に関しては、「159色の中で一番使ってない色を探す→その色と最も近い色を探す→一番使ってない色をその色と最も近い色と同じ色にしてやる」というプロセスを色が15色になるまで繰り返すことで実現しました。
分かりやすく途中経過を示すとこんな感じです。
次にカラーパレット159色に適用。この時点で実は50色程度になっています。何色になるかは元の絵によって異なると思います。
ドット絵の減色の効果って意外に面白いものですね。
今後の予定
実はとびだせどうぶつの森、このマイデザインをQRコードに書き出す機能があります。QRコードが解析できれば、いちいち手動で打ち込んだりせず、絵や写真からドット絵のデータをQRコードで書き出し、Nintendo 3DSのカメラで読み取りという素晴らしい機能が実現できるのですが時間切れでした(あさみさんと仲良くなれるまで機能が使えないため)。QRコード調べたら結構面白かったので、色々試してみたいなと。
iPhoneアプリで、写真撮影したらどうぶつの森用のQRコード書き出してるアプリ出せば結構うれるんじゃないかなと思うので、アイディアをここに置いておきますね。好きにアイディア使ってよいので、完成したらそっと私に送って下さい。
「マイデザイン補助ツール」使い方
上皮質剥がれ子さんが、本ツールの使い方を詳細に説明して下さっています。「画像や写真からマイデザイン」(とびだせ どうぶつの森日記。)。わかりやすいので、使い方が分からないよという人は参照して下さい。自分でツール作っておいて実は一度も試していなかったので、ほんとに使える人がいて一安心しています(笑)。正直Windowsで動くのかも自信無かったので一安心。
※12/12/17 他にもツール作っている人がいたのでリンクしておきます。結構QRコード実現している人沢山いるんですね。
『とび森のマイデザインツールについて』(N氏のメモ帳)
以下はソースコードです。
ソースコード
ソースコード公開します。プログラム自体は汚いですが、Processingをダウンロードしてコピペするだけで誰でも自分な好きな絵や写真から(時間をかければ)マイデザインつくることができるはずなので試してみるのがよいと思います。ちなみにProcessingのバージョンは1.5.1に対応していますので気をつけて下さい(2.0系では動きません)。ちょっと変えれば動くのですがサボってます。興味ある人は改造してみて下さい。
使い方は、コピペして実行するとダイアログが開くので、好きな画像ファイルを選んで下さい。自動的に結果が表示されます。「p」を押すとプログラムと同じフォルダに結果の絵が「screenshot.jpg」というファイル名で保存されます。
※12/12/17 パレットナンバが1つずれていたのを修正しました
※12/12/23 画面が大きすぎる場合は final int scale = 24; の数字を小さくしてみて下さい
PImage img0; PImage writeImg; //Window Size final int size_x = 32; final int size_y = 32; final int dotframe = 1; final int scale = 24; final int n_pallet = 159; final int max_color = 15; final int font_size = int(scale/2); float [] pallet_r = { 255, 255, 239, 255, 255, 189, 206, 156, 82, 255, 255, 222, 255, 255, 206, 189, 189, 140, 222, 255, 222, 255, 255, 189, 222, 189, 99, 255, 255, 255, 255, 255, 222, 189, 156, 140, 255, 239, 206, 189, 206, 156, 140, 82, 49, 255, 255, 222, 255, 255, 140, 189, 140, 82, 222, 206, 115, 173, 156, 115, 82, 49, 33, 255, 255, 222, 255, 255, 206, 156, 140, 82, 222, 189, 99, 156, 99, 82, 66, 33, 33, 189, 140, 49, 49, 0, 49, 0, 16, 0, 156, 99, 33, 66, 0, 82, 33, 16, 0, 222, 206, 140, 173, 140, 173, 99, 82, 49, 189, 115, 49, 99, 16, 66, 33, 0, 0, 173, 82, 0, 82, 0, 66, 0, 0, 0, 206, 173, 49, 82, 0, 115, 0, 0, 0, 173, 115, 99, 0, 33, 82, 0, 0, 33, 255, 239, 222, 206, 189, 173, 156, 140, 115, 99, 82, 66, 49, 33, 0 }; float [] pallet_g = { 239, 154, 85, 101, 0, 69, 0, 0, 32, 186, 117, 48, 85, 0, 101, 69, 0, 32, 207, 207, 101, 170, 101, 138, 69, 69, 48, 239, 223, 207, 186, 170, 138, 101, 85, 69, 207, 138, 101, 138, 0, 101, 0, 0, 0, 186, 154, 32, 85, 0, 85, 0, 0, 0, 186, 170, 69, 117, 48, 48, 32, 16, 16, 255, 255, 223, 255, 223, 170, 154, 117, 85, 186, 154, 48, 85, 0, 69, 0, 0, 16, 186, 154, 48, 85, 0, 48, 0, 16, 0, 239, 207, 101, 170, 138, 117, 85, 48, 32, 255, 255, 170, 223, 255, 186, 186, 154, 101, 223, 207, 85, 154, 117, 117, 69, 32, 16, 255, 255, 138, 186, 207, 154, 101, 69, 32, 255, 239, 207, 239, 255, 170, 170, 138, 69, 255, 255, 223, 255, 223, 186, 186, 138, 69, 255, 239, 223, 207, 186, 170, 154, 138, 117, 101, 85, 69, 48, 32, 0 }; float [] pallet_b = { 255, 173, 156, 173, 99, 115, 82, 49, 49, 206, 115, 16, 66, 0, 99, 66, 0, 33, 189, 99, 33, 33, 0, 82, 0, 0, 16, 222, 206, 173, 140, 140, 99, 66, 49, 33, 255, 255, 222, 206, 255, 156, 173, 115, 66, 255, 255, 189, 239, 206, 115, 156, 99, 66, 156, 115, 49, 66, 0, 33, 0, 0, 0, 206, 115, 33, 0, 0, 0, 0, 0, 0, 255, 239, 206, 255, 255, 140, 156, 99, 49, 255, 255, 173, 239, 255, 140, 173, 99, 33, 189, 115, 16, 49, 49, 82, 0, 33, 16, 189, 140, 82, 140, 0, 156, 0, 0, 0, 255, 255, 156, 255, 255, 173, 115, 115, 66, 255, 255, 189, 206, 255, 173, 140, 82, 49, 239, 222, 173, 189, 206, 173, 156, 115, 49, 173, 115, 66, 0, 33, 82, 0, 0, 33, 255, 239, 222, 206, 189, 173, 156, 140, 115, 99, 82, 66, 49, 33, 0 }; void setup() { size(size_x*scale*2+dotframe, size_y*scale); println("select photo."); String imgPath = selectInput(); img0 = loadImage(imgPath); textSize(font_size); background(0,0,0); writeImg = createGraphics(size_x*scale, size_y*scale, P2D); } int CountColor(int[] pallet, int n) { int cont = 0; int []pallet_cnt = new int[n]; for (int i=0; i < n; i++) { pallet_cnt[i] = 0; for (int j= 0; j < size_x*size_y; j++) { if (pallet[j] == i) { pallet_cnt[i]++; } } } for (int i=0; i < n; i++) { if (pallet_cnt[i] > 0) { cont++; } } return cont; } void draw() { img0.loadPixels(); // Making Image----- float[] img_r = new float[img0.width*img0.height]; float[] img_g = new float[img0.width*img0.height]; float[] img_b = new float[img0.width*img0.height]; float[] img16_r = new float[size_x*size_y]; float[] img16_g = new float[size_x*size_y]; float[] img16_b = new float[size_x*size_y]; int[] img16_pallet = new int[size_x*size_y]; for (int i = 0; i < img0.width*img0.height; i++) { color tmp_color = img0.pixels[i]; img_r[i] = red(tmp_color); img_g[i] = green(tmp_color); img_b[i] = blue(tmp_color); } int div_x = int(img0.width / size_x); int div_y = int(img0.height / size_y); float tmp_r; float tmp_g; float tmp_b; // make dot image for (int i = 0; i < size_x*size_y; i++) { tmp_r = 0; tmp_g = 0; tmp_b = 0; int tmp_y = int(i / size_x); int tmp_x = i - size_x*tmp_y; for (int j = 0; j < div_x; j++) { for (int k = 0; k < div_y; k++) { tmp_r += img_r[tmp_x*div_x + tmp_y*img0.width*div_y+ j + k*img0.width]; tmp_g += img_g[tmp_x*div_x + tmp_y*img0.width*div_y+ j + k*img0.width]; tmp_b += img_b[tmp_x*div_x + tmp_y*img0.width*div_y+ j + k*img0.width]; } } tmp_r = tmp_r/(div_x*div_y); tmp_g = tmp_g/(div_x*div_y); tmp_b = tmp_b/(div_x*div_y); img16_r[i] = tmp_r; img16_g[i] = tmp_g; img16_b[i] = tmp_b; } writeImg.loadPixels(); // matching pallet for (int i = 0; i < size_x*size_y; i++) { float []tmp_color_distance = new float[n_pallet]; for (int j= 0; j < n_pallet; j++) { tmp_color_distance[j] = (img16_r[i] - pallet_r[j])*(img16_r[i] - pallet_r[j]) + (img16_g[i] - pallet_g[j])*(img16_g[i] - pallet_g[j]) + (img16_b[i] - pallet_b[j])*(img16_b[i] - pallet_b[j]); } float tmp_color_min = tmp_color_distance[0]; img16_pallet[i] = 0; for (int j= 1; j < n_pallet; j++) { if (tmp_color_min > tmp_color_distance[j]) { tmp_color_min = tmp_color_distance[j]; img16_pallet[i] = j; } } } // reduce pallet while (CountColor (img16_pallet, n_pallet) > max_color) { int []pallet_cnt = new int[n_pallet]; for (int i=0; i < n_pallet; i++) { pallet_cnt[i] = 0; for (int j= 0; j < size_x*size_y; j++) { if (img16_pallet[j] == i) { pallet_cnt[i]++; } } } int min_colornumb = 0; int first_cnt=0; int min_tmp = 0; while (pallet_cnt[first_cnt] == 0) { first_cnt++; } int min_colorcnt = pallet_cnt[first_cnt]; for (int i=first_cnt; i < n_pallet; i++) { if (min_colorcnt > pallet_cnt[i] && pallet_cnt[i] > 0) { min_tmp = 1; min_colorcnt = pallet_cnt[i]; min_colornumb = i; } } float min_colordist = 0; int neardist_colornumb = 0; if (min_tmp == 0) { min_colornumb = first_cnt; int tmp_cnt = first_cnt+1; while (pallet_cnt[tmp_cnt] == 0) { tmp_cnt++; } min_colordist = (pallet_r[min_colornumb]-pallet_r[tmp_cnt])*(pallet_r[min_colornumb]-pallet_r[tmp_cnt]) + (pallet_g[min_colornumb]-pallet_g[tmp_cnt])*(pallet_g[min_colornumb]-pallet_g[tmp_cnt]) + (pallet_b[min_colornumb]-pallet_b[tmp_cnt])*(pallet_b[min_colornumb]-pallet_b[tmp_cnt]); neardist_colornumb = tmp_cnt; } else { min_colordist = (pallet_r[min_colornumb]-pallet_r[first_cnt])*(pallet_r[min_colornumb]-pallet_r[first_cnt]) + (pallet_g[min_colornumb]-pallet_g[first_cnt])*(pallet_g[min_colornumb]-pallet_g[first_cnt]) + (pallet_b[min_colornumb]-pallet_b[first_cnt])*(pallet_b[min_colornumb]-pallet_b[first_cnt]); neardist_colornumb = first_cnt; } for (int i=first_cnt; i < n_pallet; i++) { if (pallet_cnt[i] > 0 && i != min_colornumb) { float tmp_dist = (pallet_r[min_colornumb]-pallet_r[i])*(pallet_r[min_colornumb]-pallet_r[i]) + (pallet_g[min_colornumb]-pallet_g[i])*(pallet_g[min_colornumb]-pallet_g[i]) + (pallet_b[min_colornumb]-pallet_b[i])*(pallet_b[min_colornumb]-pallet_b[i]); if (min_colordist > tmp_dist) { min_colordist = tmp_dist; neardist_colornumb = i; } } } for (int i=0; i < size_x*size_y; i++) { if (img16_pallet[i] == min_colornumb) { img16_pallet[i] = neardist_colornumb; } } } // pallet update for (int i = 0; i < size_x*size_y; i++) { img16_r[i] = pallet_r[img16_pallet[i]]; img16_g[i] = pallet_g[img16_pallet[i]]; img16_b[i] = pallet_b[img16_pallet[i]]; } // scaling dot image for (int i = 0; i < size_x*size_y; i++) { int tmp_y = int(i / size_x); int tmp_x = i - size_x*tmp_y; for (int j = 0; j < scale; j++) { for (int k = 0; k < scale; k++) { writeImg.pixels[tmp_y*size_x*scale*scale+tmp_x*scale+j*size_x*scale+k] = color(img16_r[i], img16_g[i], img16_b[i]); } } } writeImg.updatePixels(); image(writeImg, 0, 0, size_x*scale, size_y*scale); // draw pallet number for (int x = 0; x < size_x; x++) { for (int y = 0; y < size_y; y++) { String tx = str(img16_pallet[y*size_x+x]+1); fill(255,255,255); textAlign(CENTER); text(tx, size_x*scale+(x+0.5)*scale, (y+0.5)*scale); } } // draw frame for (int x = 0; x < size_x*2+1; x++) { stroke(255); strokeWeight(dotframe); line(x*scale, 0, x*scale, size_y*scale); } for (int y = 0; y < size_y+1; y++) { stroke(255); strokeWeight(dotframe); line(0, y*scale-dotframe, size_x*scale*2+1, y*scale-dotframe); } } void keyPressed() { // save image if (key == 'p' || key == 'P') { save("screenshot.jpg"); println("screen saved."); } // exit if (key == ' ') { exit(); } }
関連記事
変更履歴
- 2020/03/21 あつまれどうぶつの森に関して追記