Chapter 27

点の描画・ピクセルの扱い方

miku
miku
2021.11.12に更新

point()

function setup() {
  createCanvas(windowWidth, windowHeight);

  stroke(240);
  point(100, 200);
}

point(x, y) で 座標 (x, y) にあるピクセルに対して stroke() で指定した色を塗る。

点のランダム描画

function setup() {
  createCanvas(windowWidth, windowHeight);
  stroke(240);
}

function draw() {
  for (let i = 0; i < 20; i++) {
    const x = random(width);
    const y = random(height);
    point(x, y);
  }
}

毎フレーム、ランダムな場所に点を打つ作例。1フレーム1ピクセルだと描画の反映が遅いので、ループ文で1フレームに数十回点を打つようにしている。

point()を扱う上での注意点

point() は円や矩形を描く関数と同じで、点を打つとそのまま画面に描画を行う。ピクセルごとの描画なので、画面全体に点を描くという場面も存在する。

for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    point(x, y);
  }
}

そのような場合、上記のように書けるが、これはかなり遅い処理だ。点を画面に描画という処理を画面サイズのピクセル分だけ行うので、数秒の処理時間がかかってもおかしくない。なので、draw() などで毎フレーム実行するなどという場合は確実に遅延が出る。

for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    // ここでピクセルの更新を行う
  }
}

// ここで画面に表示する

理想としてはループ内で (x, y) 座標に特定の色を塗るという情報を保持するが、まだ画面には描画を行わず、ループを抜け終わったあとで、1回にまとめて画面に描画をするという方法を取りたい。ここで set() という関数を利用する。

set()

function setup() {
  createCanvas(windowWidth, windowHeight);

  const c = color(0, 0, 255);

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
     // pixels配列に書き込む
      set(x, y, c);
    }
  }

 // pixels配列の内容を画面に反映する
  updatePixels();
}

set(x, y, 色) で指定した座標に色を書き込む。ただし、書き込み先は pixels という配列になる。

この配列に色を書き込んでも画面には反映されない。反映を行うには updatePixels() を呼び出す。この関数を呼び出すと pixels の内容をもとに画面に描画が行われる。

つまりループの中で set() を利用して pixels にピクセルの色変更情報を入れておき、ループを抜けたあとに updatePixels() を呼び出して画面に反映すればいいわけだ。

pixels配列を直接触る

set() に変えるだけでかなり速くはなるのだが、 set() は汎用的な関数なので、実際には pixels を直接触ると更に速くなる。

Canvasのピクセルには赤・緑・青・透明度の4つの情報があり、pixels では各ピクセルごとの4つの情報を1つ1つセットする必要がある。具体的には下記のとおりだ。

インデックス ピクセル
pixels[0] 1つ目のピクセル
pixels[1] 1つ目のピクセル
pixels[2] 1つ目のピクセル
pixels[3] 1つ目のピクセル 透明度
pixels[4] 2つ目のピクセル
pixels[5] 2つ目のピクセル
pixels[6] 2つ目のピクセル
pixels[7] 2つ目のピクセル 透明度

各ピクセルの赤・緑・青・透明度の順番で値をセットしていく。1つ目のピクセルは (0, 0)、2つ目のピクセルは (0, 1)...のように左から右、上から下にアクセスしていく。

各ピクセルごとに4つの情報があり、先頭が赤色なのだから、i 番目のピクセルの赤情報にアクセスしたい場合は pixels[i * 4] にアクセスすればいい。(i0 から始まる)

緑は赤の次の位置にあるのだから pixels[i * 4 + 1]
青はその次の pixels[i * 4 + 2]
透明度はその次の pixels[i * 4 + 3]

となる。

ただし、初期状態の pixels の中身は空配列となっており、loadPixels() という関数を呼び出すと画面上のピクセルの内容が pixels にセットされる。set() を呼び出したときにエラーが出ないのは、内部で loadPIxels() が実行されているからである。

function setup() {
  createCanvas(windowWidth, windowHeight);

  loadPixels();

  for (let i = 0; i < pixels.length; i += 4) {
    pixels[i] = 0; // 赤
    pixels[i + 1] = 0; // 緑
    pixels[i + 2] = 255; // 青
    pixels[i + 3] = 255; // 透明度
  }

  updatePixels();
}

pixelDensity

最近は、Retinaディスプレイなどの採用により、スマートフォンの解像度がPCと遜色ないレベルまで上がってきた。しかし、物理的な画面サイズ自体は小さいままなので、ブラウザでサイトを開いたときに、レスポンシブ対応などでPC扱いされて、そのままPCの表示が出てきてしまうと困る。なので、CSSやJavaScriptで参照できる画面サイズは本来のサイズより1/2や1/3のような小さいものになっている。前者のようなもとの解像度のことを物理解像度あるいはデバイスピクセル、後者のような解像度のことをCSS解像度あるいはCSSピクセルと呼ぶ。

今まで使用してきた windowWidthwindowHeight はCSSピクセルでの横幅・縦幅が返ってくるし、circle()point() に指定するような (x, y) はCSSピクセルでの指定になる。

しかし、pixels はデバイスピクセル扱いなので、それを考慮した指定を行う必要がある。

まず、最初に調べなければならないのが、デバイスピクセルに対してCSSピクセルは何分の1になっているかということである。

pixelDensity();

p5.jsでは pixelDensity() という関数で調べることができる。たとえば 3 が返ってきたらならば、デバイスピクセルとCSSピクセルの比率は 3 : 1 ということになる。1つのピクセルが実際には横3倍、縦3倍の9ピクセルで表現されていると考えてもいい。

先程も説明したが pixels はデバイスピクセルの扱いなので、配列の長さは width * height * 4 ではなく、width * height * pixelDensity() * pixelDensity() * 4 ということになる。

pixelDensity() の2乗分だけ pixels のサイズが増えるので、もし画面全体をピクセル単位で走査するようなコードだと、同じように pixelDensity() の2乗の時間がかかることになる。

pixelDensity(1);

pixelDensity(val); を呼び出すと、pixels のサイズが width * height * val * val * 4 になる。1 を設定すると実質 pixelDensity() の考慮をしなくて済むので計算が楽になる上に配列のサイズもかなり小さくなる。

ただし、デメリットもあり、たとえば本来 pixelDensity()3 のものを 1 に下げると、若干ぼやけた表示になる可能性がある。それに加え、circle()point() のような図形を描く関数も pixelDensity() を考慮した描画となっているので、こちらも滑らかさが変わってしまう可能性がある。基本的に pixelDensity() を下げることを考慮するのは pixels を直接触るときだけにしたほうがいい。

本書では以降、画面全体に対する pixels の走査を行う場合は処理時間を考慮して pixelDensity()1 を設定する方針でいく。

ピクセルの読み込み

今まではピクセルの書き込み方法を解説したが、次は読み込みについて扱う。

get(0, 0);

get(x, y) でCanvasの (x, y) にあるピクセルを [赤, 緑, 青, 透明度] の形で返す。set() と同じでCSSピクセルでの指定なので、普通に座標を指定すればいい。注意点としては pixels からの参照ではないので、画面全体のピクセルを取得するようなコードだと遅いということだ。その場合は loadPixels()pixels に反映した後、pixels の内容を直接参照したほうがいい。

円の定義で点だけで円を描画

function setup() {
  createCanvas(windowWidth, windowHeight);
  pixelDensity(1);

  background("#f06d06");
  loadPixels();

  const c = color(0);
  const r = 100;

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const d = dist(x, y, width / 2, height / 2);
      if (d <= r) {
        setPixel(x, y, c);
      }
    }
  }

  updatePixels();
}

function setPixel(x, y, c) {
  const i = (y * width + x) * 4;
  pixels[i + 0] = c[0];
  pixels[i + 1] = c[1];
  pixels[i + 2] = c[2];
}

画面中央からの距離が r 以内のピクセルにだけ色を塗る作例。