君もバイナリアーティストになろう! ~バイナリ書き出しでメディアアートを行う~

テックポエム

君もバイナリアーティストになろう!

~バイナリ書き出しでメディアアートを行う~

本記事は、プログラミング以外の前提知識をあまり使わずに、グラフィックスプログラミングを行ってみようという試みになります。

スキルレベルとしては下記のレベル以上の方が対象です。
・自分でC#等のプログラムを書いて実行することができる。
・「変数」や「ビット」や「16進数」など、基本的な技術用語の意味が分かる。
Windowsを使用している方を対象としていますが、別のOSであっても概念は同じです。適宜読み替えてください。

プログラミング必修化

日本では、2020年からプログラミング教育の必修化が始まります。

ちょうど、2018年3月30日に、「小学校を中心としたプログラミング教育ポータル」が文部科学省・総務省・経済産業省の連携のもとオープンしました(https://miraino-manabi.jp/)。
見てみたところ、MESH(http://meshprj.com/jp/)やSCRATCH(https://scratch.mit.edu/)を使って、プログラミングを学んでいくようです。

また、先日ディズニーはプログラミング学習教材「テクノロジア魔法学校」(https://www.technologia-schoolofmagic.jp/)を発表しました。

デジタルネイティブ世代が社会に徐々に増えており、生まれたころからスマートフォンがあった、という世代もあと10年ほどすると現れます。コンピューターが扱えることは、現在ではほぼ必須のスキルとなっていますが、今後は、普段の日常会話では使わないにも関わらず、英語やアルファベットの知識が必須であるのと同じように、プログラミングの知識やスキルが必須となる時代が訪れることと思います。

プログラミングの学習

さて、プログラミングの学習に少しフォーカスを当てていきたいと思います。
プログラミングの学習というと、『画面に「Hello World」という文字を表示しましょう』という内容から始まるのがお約束となっています。

01.pngプログラミングによって、画面に「Hello, World!」を表示したもの

その後、キーボードの入力をプログラムで扱う方法を学び、「数あてゲームを作りましょう」「じゃんけんゲーム(1を入力するとグーで、2がチョキ、3がパーみたいなもの)を作りましょう」というように簡単なプログラムを作ることで、プログラミングの基本である「条件分岐」や「繰り返し」や「変数」といった概念と使い方を理解していく、というのがよくあるプログラミング学習の流れかと思います。

02.png
条件分岐を勉強する際によく作られる「数あてゲーム」

こうしていろいろなものを学び、プログラミング学習の終盤には、オセロゲームや予約管理システム、日記アプリといった、それっぽいアプリケーションを作ることになるかと思います。

業務でプログラミングを行うにあたっては、こういった学習は確かに一つの正解ではあると思います。しかし、こういったプログラミングは、どうしても「学習する」ことに重きが置かれてしまいます。作ったプログラムには学習以上の意味が無く、作った数あてゲームは、今後二度と実行されることはないでしょう。


一方、前述したMESHは、センサーを簡単に扱えるため、学習段階であっても、思いついたアイディアをすぐに実現することができ、作ったものを日々の生活に持ち込むことさえ可能です。また、SCRATCHは、処理のパーツをパズルのように繋げていくことで、アニメーションやゲームを作ることができ、「作る楽しみ」を得ることができるものとなっています。
テクノロジア魔法学校も、内容を見てみるとメディアアートやゲームといった、できあがったものが視覚的に面白いものを学んでいけるようになっているように見えます。

私はこういった「視覚的に楽しいもの」の方が作っていて楽しいため、そういう学びのアプローチがあってもいいでは、と考えております。

といっても、学習段階でメディアアートやゲームの作成を行おうとすると、前提知識がとても多く必要になるため、作るのが楽しくなる前に挫折するのは目に見えています。

難しい前提知識を使わず、視覚的に楽しいプログラミングで、さらにある程度学習も可能なものはできないかな、と考えたのが、本記事を書こうと思ったきっかけになります。

そこで「プログラムでビットマップファイル作ってみる」というアプローチに行きつきました。このアプローチであれば、ファイルの書き出しさえできれば、「画像作成」というグラフィックスプログラミングを簡単に実現することができます。

ビットマップをプログラムで作ってみよう

それでは、実際にプログラムでビットマップファイルを作っていこうと思います。

簡単なおさらいになりますが、ビットマップファイルは、グラフィックのファイル形式の一つで、拡張子が「.bmp」のものです。
このファイルには、画像のピクセルの色の情報がそのまま記録されています。
例えば下記のような画像のビットマップファイルには、「左上が黒」「右上は赤」というように、色の情報が記録されています。

03.png
縦横2ピクセルの画像の例(拡大しています)


さて、ビットマップファイルを作るにあたり、ビットマップファイルがどういったものかを知る必要があります。ビットマップファイルの内容を見るために「バイナリエディター」と呼ばれるソフトを使うことにします。
「バイナリエディター」を使うと、ファイルに実際にどんなデータが格納されているかを見ることができます。

まずペイントソフトなどを使用し、1ピクセルの赤い点を打った画像と、白い点を打った画像のビットマップファイルをそれぞれ用意します。

そしてバイナリエディターでそのビットマップファイルを見てみます。


04.png
赤い点だけの画像と白い点だけのビットマップファイルをバイナリエディターで比較したもの

上側が、赤い点を打ったビットマップファイル、下側が白い点を打ったビットマップファイルをバイナリエディターで見たものになります。コンピューターは0と1のみで情報を扱っておりますが、バイナリエディターでは、見やすいように、16進数で表示されます。

2つのファイルを見比べてみると、一番下の段だけ異なっていることが分かります。上側はFFというデータが1つですが、下側はFFが3つあるようです。

ということは、この違いが色の違いだと考えることができます。

さて、コンピューターで表示する色は、赤・緑・青の3つの組み合わせで表現するのが一般的です。この3色がそれぞれどれくらい濃いかの組み合わせで、ほぼすべての色を表現することができます。なお、英語にするとRed,Green,Blueなので、このことをそれぞれの頭文字をとってきてRGBと呼んだりします。
赤い点はRGBのRのみがある状態、白い点は、RGBのすべてがある状態です。

ビットマップファイルには、このRGBが記録されています。ビットマップファイルには色のデータが青・緑・赤の順で並ぶことになっているため、各色はバイナリのどこで表されているのか、を着色すると次のようになります。00はその色の要素が無しであり、FFというのは、逆に最大であるという意味です。


05.png
白い点だけのビットマップファイルの色情報の部分に着色したもの

今回は1ピクセルでしたが、大きなサイズの画像になっても、データの後ろに青・緑・赤のデータが連続していくだけになります。
ということは、着色した部分をプログラムで作り出すことができれば、高度なグラフィック知識がなくても、グラフィックスプログラミングが可能になりそうです。

ビットマップファイルをプログラムで作成する

色のデータがどのように入るかはわかりましたが、他のところにはどのようにデータを入れる必要があるかも知る必要があります。そこで、ビットマップファイルのフォーマットを調べるため、Wikipediaの情報を見てみます。
https://ja.wikipedia.org/wiki/Windows_bitmap#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%98%E3%83%83%E3%83%80
―Wikipedia(Windows bitmapの「ファイルヘッダ、情報ヘッダ、ビットマップデータ」)

このフォーマットの通りにバイナリをファイルに書き込んでいけば、ビットマップファイルが作れそうです。

それでは情報が揃ったので、実際にプログラミングしていきます。プログラミング言語はC#を使うことにしました。書き込むバイナリをByte[]で定義し、FileStream.Write()を使うことで、ファイルにバイナリを書き込んでいくことができます。

ビットマップファイルのフォーマットの情報から、ビットマップファイルには、色の情報のほかにも、ファイルサイズなどの情報が入っていることが分かったので、その通りにプログラミングします。
そして、バイナリエディターで見た内容と同じ内容が出力されるようにプログラミングしていきます。

using System;
using System.Linq;
using System.IO;

namespace BinaryArtist
{
    class Program
    {
        const int HEIGHT = 1;       // 横幅
        const int WIDTH = 1;        // 高さ
        const string SAVE_PATH = @"C:\BinaryArtist\test.bmp";   // ファイルを保存するパス

 //--------------------------------------------------
// バイナリ書き込み使用して、ビットマップファイルを作成する
//--------------------------------------------------
static void Main(string[] args) { // ヘッダー情報を格納する byte[] headerBuffer; // パディング(水平方向のサイズを4の倍数にするためのもの) int paddingSize = WIDTH % 4; // 画像サイズ int imageSize = (WIDTH * HEIGHT) * 3 + paddingSize * HEIGHT; // 各数値を4バイトで表現する headerBuffer = new byte[54]; byte[] sizeByte = BitConverter.GetBytes(imageSize + headerBuffer.Count()); // ファイルサイズ byte[] widthByte = BitConverter.GetBytes(WIDTH); // 幅 byte[] heightByte = BitConverter.GetBytes(HEIGHT); // 高さ byte[] imageByte = BitConverter.GetBytes(imageSize); // ビットマップデータサイズ // バイナリを書き込む headerBuffer[0] = 0x42; // 'B' headerBuffer[1] = 0x4D; // 'M' headerBuffer[2] = sizeByte[0]; // ファイルサイズ headerBuffer[3] = sizeByte[1]; // ファイルサイズ headerBuffer[4] = sizeByte[2]; // ファイルサイズ headerBuffer[5] = sizeByte[3]; // ファイルサイズ headerBuffer[10] = 0x36; // 画素データまでのオフセット headerBuffer[14] = 0x28; // ヘッダーサイズ headerBuffer[18] = widthByte[0]; // 幅 headerBuffer[19] = widthByte[1]; // 幅 headerBuffer[20] = widthByte[2]; // 幅 headerBuffer[21] = widthByte[3]; // 幅 headerBuffer[22] = heightByte[0]; // 高さ headerBuffer[23] = heightByte[1]; // 高さ headerBuffer[24] = heightByte[2]; // 高さ headerBuffer[25] = heightByte[3]; // 高さ headerBuffer[26] = 0x01; // プレーン数(1) headerBuffer[28] = 0x18; // 色ビット数(24) headerBuffer[34] = imageByte[0]; // 画像データサイズ headerBuffer[35] = imageByte[1]; // 画像データサイズ headerBuffer[36] = imageByte[2]; // 画像データサイズ headerBuffer[37] = imageByte[3]; // 画像データサイズ FileStream fs = new FileStream(SAVE_PATH, FileMode.Create, FileAccess.Write); fs.Write(headerBuffer, 0, headerBuffer.Count()); // 色データの書き込み Draw(ref fs); // ストリームを閉じる fs.Close(); } //--------------------------------------------------
// 色データをバイナリで書き込む
//--------------------------------------------------
static void Draw(ref FileStream fs) { // ビットマップデータの書き込み for (int y = 0; y < HEIGHT; y++) { byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)]; for (int x = 0; x < WIDTH; x++) { imageBuffer[x * 3 + 2] = (byte)(0xFF); // 赤 imageBuffer[x * 3 + 1] = (byte)(0x00); // 緑 imageBuffer[x * 3 + 0] = (byte)(0x00); // 青 } fs.Write(imageBuffer, 0, imageBuffer.Count()); } } } }

試しに縦横1ピクセルの真っ赤な画像を作ってみます。
出来上がったファイルをペイントで開くと、きちんと真っ赤になっていることが確認できました。

06.png
赤い点だけのビットマップファイルが作成できました

アーティストになってみる

ファイルサイズはWIDTH,HEGHTという名前でそれぞれ定数にしましたし、そこからファイルヘッダーに書き込む内容などは自動で計算するようにしてありますから、あとはDraw()でimageBufferにどんな値を入れるかを考えるだけで、いろいろな画像が作れるようになります。

大きな画像の方が見た目がよいのでWIDTHとHEIGHTを500にして、いろいろ試してみることにしました。


緑色の要素を、横位置を表すxに変えてみます。下線部が変更した部分です。

	static void Draw(ref FileStream fs)
        {
            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    imageBuffer[x * 3 + 2] = (byte)(0xFF);  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(x);  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(0x00);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

するとこんな画像が出来上がりました。

07.png
横に進むにつれて緑色の要素が濃くなっていくため、赤から黄色のグラデーションとなる

Byteは上限が255なので、256を越えるとオーバーフローにより0になります。横幅は500ですから、真ん中あたりで緑色の要素が0になったため、区切りが見えますね。

今度は変数cを用意して、xの代わりにcを使用してみます。
cはインクリメントすることで、少しずつ増えていくようにします。

	static void Draw(ref FileStream fs)
        {
            int c = 0;

            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    c++;
                    imageBuffer[x * 3 + 2] = (byte)(0xFF);    // 赤
                    imageBuffer[x * 3 + 1] = (byte)(c);       // 緑
                    imageBuffer[x * 3 + 0] = (byte)(0x00);    // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

するとこんな画像が出来上がりました。

08.png
次の行を描画する際に、変数cの値が残っているため、斜めにグラデーションがかかる結果となりました

縦方向にもグラデーションをかけるため、変数c2を作り、青要素に適用してみます。

	static void Draw(ref FileStream fs)
        {
            int c = 0;
            int c2 = 0;

            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                c2++;
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    c++;
                    imageBuffer[x * 3 + 2] = (byte)(0xFF);  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(c);  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(c2 * 2);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

09.png
縦方向にもグラデーションができました

しかし、赤要素が常に0xFFであるため、マゼンタにしかならず、青っぽさがでません。

そこで、赤要素から先ほどの変数c2を引いてみることにしました。

        static void Draw(ref FileStream fs)
        {
            int c = 0;
            int c2 = 0;

            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                c2++;
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    c++;
                    imageBuffer[x * 3 + 2] = (byte)(0xFF - c2);  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(c);  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(c2 * 2);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

すると、段ごとに赤要素が減っていき、青要素が濃くなっていくグラデーションとすることができました。

10.png
色々な値の色要素が混ざって、いろいろな色があらわれるようになりました

今度は趣向を変えて、9ピクセルごとに緑要素と青要素を0xFFにしてみます。

	static void Draw(ref FileStream fs)
        {
            int c = 0;

            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    c++;
                    imageBuffer[x * 3 + 2] = (byte)(0x00);  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(c % 9 == 0 ? 0xFF : 0x00);  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(c % 9 == 0 ? 0xFF : 0x00);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

11.png
斜めの細い線を描くことができました

絶対値をうまく使うと、オーバーフローの境目をなくせるため、きれいなグラデーションを作ることができます。

	static void Draw(ref FileStream fs)
        {
            int c = 0;

            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    c++;
                    imageBuffer[x * 3 + 2] = (byte)(Math.Abs((c % 511) - 255));  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(Math.Abs((c % 511) - 255));  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(0x00);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

12.png
「c % 511」で0~510の数値となり、これを-255することで-255~+255になります

条件式をうまく使えば、チェック模様を描くこともできます。

	static void Draw(ref FileStream fs)
        {
            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    bool b1 = (((x % 50 > 20) && (x % 50 < 40)) || ((y % 50 > 20) && (y % 50 < 40)));
                    imageBuffer[x * 3 + 2] = (byte)(b1 ? 0x00 : 0xFF);  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(b1 ? 0x00 : 0xFF);  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(0xFF);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

13.png

チェック模様が描けました


条件式を増やしていけば、複雑な模様を描いていくこともできます。

	static void Draw(ref FileStream fs)
        {
            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    bool b1 = (((x % 50 > 20) && (x % 50 < 40)) || ((y % 50 > 20) && (y % 50 < 40)));
                    bool b2 = (((x % 50 > 25) && (x % 50 < 35)) || ((y % 50 > 25) && (y % 50 < 35)));
                    imageBuffer[x * 3 + 2] = (byte)(b1 ? (b2 ? 0x88 : 0x00) : 0xFF);  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(b1 ? (b2 ? 0xBB : 0x00) : 0xFF);  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(b2 ? 0x00 : 0xFF);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

14.png

b1とb2二つの条件で色が塗り分けられます

ピクセルを操作できるという点に着目すると、かの有名なマンデルブロ集合を描くこともできます。

	static void Draw(ref FileStream fs)
        {
            // ビットマップデータの書き込み
            for (int y = 0; y < HEIGHT; y++)
            {
                byte[] imageBuffer = new byte[WIDTH * 3 + (WIDTH % 4)];
                for (int x = 0; x < WIDTH; x++)
                {
                    byte c = mandelbrot(x, y);
                    imageBuffer[x * 3 + 2] = (byte)(c);  // 赤
                    imageBuffer[x * 3 + 1] = (byte)(c);  // 緑
                    imageBuffer[x * 3 + 0] = (byte)(c);  // 青
                }
                fs.Write(imageBuffer, 0, imageBuffer.Count());
            }
        }

        static byte mandelbrot(int posX, int posY)
        {
            double px = (posX - (WIDTH / 2)) / (WIDTH / 4.0);       // 画像上の数値の範囲を-1.0~+1.0付近にする
            double py = (posY - (HEIGHT / 2)) /(HEIGHT / 4.0);      // 画像上の数値の範囲を-1.0~+1.0付近にする      
double x = 0; double y = 0; byte c = 0; // 計算結果が発散するか調査 while (c < 0xFF) { double tempX = x; double tempY = y; x = (tempX * tempX) - (tempY * tempY) + px; y = 2 * (tempX * tempY) + py; if ((x * x) + (y * y) > 5) { return c; } c++; } return c; }

15.png

マンデルブロ集合を描画することができました

組み合わせとアイディア次第でいろいろな画像を作っていくことができました。画像サイズは簡単に変えることができますので、例えば縦横5000pxという大きな画像を作ることもできます。
狙った模様が出来上がったら気持ちの良いものですし、想定通りいかなくても、偶然にも面白い画像が出来上がることもあります。この偶発性はメディアアートの特徴といえるでしょう。

まとめ

今回、バイナリをファイルに書き出し、ビットマップファイルを作成することで、グラフィックス系の難しい処理や手順、決まり事を覚えることなく、条件分岐と繰り返し処理と変数という、基本的なプログラミングの知識だけでグラフィックスプログラミングを行うことができました。また、少し手を入れるだけで、様々なグラフィックスを作ることができるため、作って終わりではなく、アイディアをもとに何度も繰り返しグラフィックスを作っていくことができます。

こういった、視覚的なアプローチからの学びも、プログラミング学習として有用だと考えております。今回のプログラムを試していただいた方には、是非、バイナリの書き出し方を色々変えて、様々なグラフィックスを作っていっていただけたらなと思います。