チュートリアル WebGLスケッチの最適化

WebGLスケッチの最適化

By Dave Pagurek, Adam Ferriss

p5.jsのWebGLモードを使用すると、コンピューターのグラフィックスハードウェアにアクセスできます。これにより、低性能のモバイルデバイスでも3Dビジュアルを生成することができます。しかし、より複雑なタスクを実行する場合、良好なフレームレートを維持するのが難しいコードを書いてしまうことがあります。ここでは、スケッチのパフォーマンスを測定およびデバッグする方法と、成功するためのコード構造のヒントをいくつか紹介します。

スケッチはどの程度うまく動作していますか?

パフォーマンスが低下すると、draw関数が期待通りの頻度で実行されないことに気づくでしょう。これにより、アニメーションが遅くなったり、ぎこちなくなったりする可能性があります。パフォーマンスを向上させるための変更を行う際、状況が明らかに改善されたかどうかを確認したいと思うでしょう。アニメーションの滑らかさを感じるだけでなく、フレームレートを表示することもできます。

フレームレートは、1秒間にdraw()が呼び出される回数をカウントします。スケッチの実行速度が速いほど、この数値は高くなります—少なくとも、画面がそれ以上更新できなくなるまでは。多くのディスプレイでは、この制限は60です。スケッチが60フレーム/秒に達しなくても心配する必要はありません:30フレーム/秒でもスムーズに見えます—映画は通常24フレーム/秒で実行されています。

スケッチのフレームレートを測定するために、ページに新しい<p>要素を作成し、毎フレーム更新することができます:

let frameRateP;
function setup() {
  createCanvas(200, 200, WEBGL);
  frameRateP = createP();
}
function draw() {
  frameRateP.html(round(frameRate()));
  // ここにスケッチの残りの部分を配置します
}

この値が少し揺れるのは正常です。時には、より良い推定値を得るためにフレームレートの移動平均を表示したい場合があります。前のコードを拡張して移動平均を表示すると、次のようになります:

let frameRateP;
// フレームレートを何フレームにわたって平均化するか?
let numSamples = 30;
// すべての個別サンプルを保存する配列
let frameRateSamples = [];
// 配列の合計の実行カウント
let frameRateSum = 0;
function setup() {
  createCanvas(200, 200, WEBGL);
  frameRateP = createP();
}
function draw() {
  let newSample = frameRate();
  frameRateSamples.push(newSample);
  frameRateSum += newSample;
  if (frameRateSamples.length > numSamples) {
    frameRateSum -= frameRateSamples.shift();
  }
  frameRateP.html(
    round(frameRateSum / frameRateSamples.length)
  );
  // ここにスケッチの残りの部分を配置します
}
試してみよう!

フレームレートを低下させるには何が必要ですか?forループで多くの形状を描画してみてください。フレームレートの変化に気づくまでに、いくつの形状を描画する必要がありますか?デスクトップやラップトップではなく、スマートフォンでスケッチを見た場合、その数は異なりますか?

WebGLから最高のパフォーマンスを引き出す

WebGLスケッチのためのベストプラクティスをいくつか紹介します。これらに従うことで、パフォーマンスの良いスタートを切ることができます。

WebGLモードではp5.Graphicsよりもp5.Framebufferを優先する

2Dモードでは、何かを描画してそれを他の何かのテクスチャとして使用する必要がある場合、p5.Graphicsオブジェクトを使用します。このアプローチは、2Dモードで行われることの多くがコンピューターのCPUで行われるため、うまく機能します。WebGLモードでは、p5.Framebufferを使用する方が高速です。これはWebGLスケッチと共にコンピューターのグラフィックスハードウェアに存在し、データ転送を最小限に抑えます。

以下は、p5.Graphicsを使用するコードの例と、p5.Framebufferを使用して改善した例です:

p5.Graphicsの使用

let layer;
function setup() {
  createCanvas(200, 200, WEBGL);
  layer = createGraphics(200, 200);
  describe(
    '各面に黄色の点がある回転する赤い立方体'
  );
}
function draw() {
  let t = millis() * 0.001;
  layer.background('red');
  layer.noStroke();
  layer.fill('yellow');
  for (let i = 0; i < 30; i += 1) {
    layer.circle(
      map(noise(i*10, 0, t), 0, 1, 0, width),
      map(noise(i*10, 100, t), 0, 1, 0, height),
      20
    );
  }
 
  background(255);
  lights();
  noStroke();
  texture(layer);
  rotateX(t);
  rotateY(t);
  box(100);
}

p5.Framebufferの使用

let layer;
function setup() {
  createCanvas(200, 200, WEBGL);
  layer = createFramebuffer();
  describe(
    '各面に黄色の点がある回転する赤い立方体'
  );
}
function draw() {
  let t = millis() * 0.001;
  layer.begin();
  background('red');
  noStroke();
  fill('yellow');
  for (let i = 0; i < 30; i += 1) {
    circle(
      map(noise(i*10, 0, t), 0, 1, -width/2, width/2),
      map(noise(i*10, 100, t), 0, 1, -height/2, height/2),
      20
    );
  }
  layer.end();
 
  background(255);
  lights();
  noStroke();
  texture(layer);
  rotateX(t);
  rotateY(t);
  box(100);
}
試してみよう!

2Dモードのp5.Graphicsを使用していた場合、p5.Framebufferに切り替えることはWebGLモードを使用することを意味します。WebGLモードでも2D形状を描画できます!過去に作成した2DスケッチをWebGLモードに適応させてみてください。2Dモードではポイント(0,0)がキャンバスの左上にありますが、WebGLモードでは中央にあることを忘れないでください!

静的なp5.Graphicsp5.Imageに置き換える

時には、p5.Framebufferを使用できない場合があります—たとえば、本当に2Dモードで何かを描画する必要がある場合です。p5.Graphicsを使用しているが、そのコンテンツを毎フレーム変更しない場合は、.get()を呼び出してp5.Imageに変換することができます。これにより、p5.jsは毎フレーム新しい画像データをテクスチャに送信する必要がないことを認識し、代わりに初期テクスチャを再利用し続けることができます。

p5.Graphicsの使用

let layer;
function setup() {
  createCanvas(200, 200, WEBGL);
  layer = createGraphics(200, 200);
  layer.background('red');
  layer.noStroke();
  layer.fill('yellow');
  layer.textAlign(CENTER, CENTER);
  layer.textSize(50);
  layer.text('Hello!', width/2, height/2);

  describe(
    '各面に黄色で"Hello!"と書かれた' +
    '回転する赤い立方体'
  );
}
function draw() {
  let t = millis() * 0.001;
 
  background(255);
  lights();
  noStroke();
  texture(layer);
  rotateX(t);
  rotateY(t);
  box(100);
}

p5.Imageの使用

let layerImg;
function setup() {
  createCanvas(200, 200, WEBGL);
  let layer = createGraphics(200, 200);
  layer.background('red');
  layer.noStroke();
  layer.fill('yellow');
  layer.textAlign(CENTER, CENTER);
  layer.textSize(50);
  layer.text('Hello!', width/2, height/2);
 
  // 静的な画像として保存
  layerImg = layer.get();
 
  // 元のものはもう必要ありません
  layer.remove();

  describe(
    '各面に黄色で"Hello!"と書かれた' +
    '回転する赤い立方体'
  );
}
function draw() {
  let t = millis() * 0.001;
 
  background(255);
  lights();
  noStroke();
  texture(layerImg);
  rotateX(t);
  rotateY(t);
  box(100);
}
試してみよう!

ジェネラティブアート画像のスライドショーを作成してみてください。各スライドを作成するためにp5.Graphics.get()を使用します!

変化しない形状には p5.Geometry を使用する

beginShape()endShape() を使用すると興味深い形状を作成できますが、形状をレンダリング可能な形式に処理するのに時間がかかります。同様に、sphere()box() のような単純な部品から興味深い形状を作ることもできますが、一度に多すぎると描画が遅くなる可能性があります。

形状(または形状の一部)が時間とともに内部的に変化しない場合、たとえ 位置、スケール、向き が変化しても、buildGeometry を使用して単一の再利用可能なモデルにまとめることができます。これにより、p5.jsはフレームごとに位置を何度も再計算する必要がなくなります。

❌ 同じ形状を再描画する

function setup() {
  createCanvas(200, 200, WEBGL);
  describe('回転するコルクスクリュー');
}
function draw() {
  background(255);
  noStroke();
  lights();
  rotateY(millis() * 0.01);
 
  for (let y = -100; y <= 100; y += 0.5) {
    const angle = y * 0.1;
    const vec = createVector(50, 0)
      .rotate(angle);
    push();
    translate(vec.x, y, vec.y);
    sphere(5);
    pop();
  }
}

buildGeometry() を使用する

let shape;
function setup() {
  createCanvas(200, 200, WEBGL);
  shape = buildGeometry(function() {
    for (let y = -100; y <= 100; y += 0.5) {
      let angle = y * 0.1;
      let vec = createVector(50, 0)
        .rotate(angle);
      push();
      translate(vec.x, y, vec.y);
      sphere(5);
      pop();
    }
  });
  describe('回転するコルクスクリュー');
}
function draw() {
  background(255);
  noStroke();
  lights();
  rotateY(millis() * 0.01);
 
  model(shape);
}
試してみよう!

1匹の動物用の p5.Geometry を作成し、それを何度も描画することで、虫の群れ、鳥の群れ、魚の群れを作ってみてください!

pixels の変更よりもシェーダーを優先する

キャンバス上の個々のピクセルを見て変更することで達成できる効果は多くあります。残念ながら、スケッチ内で pixels を変更すると、JavaScriptで1つずつすべてのピクセルをループ処理する必要があります。一方、シェーダーはすべてのピクセルを一度に並列で実行できるため、速度が必要な場合の優れた代替手段となります。

以下は、pixels を使用したノイズベースのディザリングの例と、シェーダーを使用した同等のフィルターです。シェーダープログラミングの詳細については、シェーダー入門を必ずチェックしてください。

pixels を使用する

function setup() {
  createCanvas(200, 200, WEBGL);
  pixelDensity(1);
  describe('点描シェーディングを施した回転する立方体');
}
function draw() {
  background(255);
  const t = millis() * 0.001;
  push();
  rotateX(t);
  rotateY(t);
  noStroke();
  lights();
  box(100);
  pop();
 
  loadPixels();
  for (let x = 0; x < width; x += 1) {
    for (let y = 0; y < height; y += 1) {
      let idx = (y * height + x) * 4;
      let newValue;
      if (pixels[idx] + noise(x, y)*50 >= 220) {
        newValue = 255;
      } else {
        newValue = 0;
      }
      pixels[idx] = newValue;
      pixels[idx + 1] = newValue;
      pixels[idx + 2] = newValue;
    }
  }
  updatePixels();
}

✅ シェーダーを使用する

let dither;
function setup() {
  createCanvas(200, 200, WEBGL);
  dither = createFilterShader(`
precision highp float;
uniform sampler2D tex0;
varying vec2 vTexCoord;
float random(vec2 p) {
  vec3 p3  = fract(vec3(p.xyx) * .1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}
void main() {
  float sample = texture2D(tex0, vTexCoord).r;
  sample += random(vTexCoord*100.0) * 0.2;
  // sample < 0.98 の場合は 0、それ以外は 1
  float level = step(0.98, sample);
  gl_FragColor = vec4(vec3(level), 1.);
}
`);
  describe('点描シェーディングを施した回転する立方体');
}
function draw() {
  background(255);
  let t = millis() * 0.001;
  push();
  rotateX(t);
  rotateY(t);
  noStroke();
  lights();
  box(100);
  pop();
 
  filter(dither);
}
試してみよう!

ディザリングシェーダーを、各ピクセルに少量のランダムな色を追加するグレインシェーダーに適応させてみてください!

その他のヒント

これまでに挙げた構造的な変更以外にも、スケッチのレンダリング方法に影響を与えるいくつかの小さな設定を調整することで、スケッチのパフォーマンスをさらに向上させることができるかもしれません。これらの改善は、異なるブラウザ、画面、コンピューターで様々なレベルの効果を発揮する可能性があるので、様々な場所で変更を試してみてください。

ピクセル密度

一部のデバイスには高解像度の画面があります。ブラウザ上ですべてが小さくならないようにするために、これらのデバイスは低密度ディスプレイの1ピクセルごとに2x2のグリッドで4ピクセルをレンダリングします。これにより画像がくっきりしますが、描画するピクセル数が4倍になることを意味します。

これらのデバイスでスケッチが遅くなる場合は、pixelDensity(1) を追加して、キャンバスの幅と高さで指定した数のピクセルのみを描画することを検討してください。スケッチが少しぼやけて見える可能性がありますが、スケッチによっては問題ないかもしれません。

function setup() {
  createCanvas(200, 200, WEBGL);
  pixelDensity(1);
}

アンチエイリアシング

アンチエイリアシングは、形状がピクセルの一部のみと交差する場合に、ピクセルの色を滑らかにするプロセスです。これにより、ギザギザの線や目に見えるピクセルグリッドを避けることができます。しかし、高ピクセル密度ディスプレイと同様に、これはピクセルごとに追加の計算を行うことで実現されます。追加の滑らかさが必要ない場合は、アンチエイリアシングをオフにしてより速く実行することができます。

function setup() {
  createCanvas(200, 200, WEBGL);
  setAttributes({ antialias: false });
}

曲線の詳細度

model() で既製の形状を描画していない場合、一般的に三角形とエッジの数が少ないほど処理が速くなります。形状の詳細度を調整する1つの方法は curveDetail() を使用することです。与える値が大きいほど、曲線を分解する際に使用されるエッジと三角形の数が増えます。デフォルト値の20よりも小さい詳細度を試してみてください。

試してみよう!

上の曲線が滑らかに見えるために必要な詳細度のレベルはどれくらいですか?曲線をより大きく描画した場合、必要なレベルは変わりますか?小さく描画した場合はどうでしょうか?

さらなるデバッグ

スケッチの実行がまだ遅く、どの部分が原因なのかわからない場合は、ブラウザのプロファイラを使用すると便利です。これはブラウザによって異なります。詳細な手順については、Chromeの場合はこちらFirefoxの場合はこちらで詳しい情報を得ることができます。

一般的に、まずブラウザの開発者ツールを開きます。簡単な方法は、ページのどこかを右クリックして「検証」をクリックすることです。開いた新しいツールバーには、「パフォーマンス」というラベルのついたタブがあり、クリックするとプロファイラにアクセスできます。Chromeでの表示は次のようになります:

Chromeの開発者ツールのスクリーンショット。パフォーマンスタブが選択されています。ウィンドウはほとんど空白で、中央に「録画ボタンをクリックするか、Cmd+Eを押して新しい録画を開始します」というメッセージが表示されています。

Chromeの開発者ツールのスクリーンショット。パフォーマンスタブが選択されています。ウィンドウはほとんど空白で、中央に「録画ボタンをクリックするか、Cmd+Eを押して新しい録画を開始します」というメッセージが表示されています。

スケッチの実行を開始し、プロファイラの録画ボタンをクリックします。数秒後、もう一度クリックして録画を停止します。すると、次のような表示になります:

パフォーマンスタブでスケッチを録画した後のChromeの開発者ツールのスクリーンショット。2Dグラフが表示され、左からボックスが並び、各初期ボックスの下に垂直方向に徐々に小さくなるボックスが積み重なっています。

パフォーマンスタブでスケッチを録画した後のChromeの開発者ツールのスクリーンショット。2Dグラフが表示され、左からボックスが並び、各初期ボックスの下に垂直方向に徐々に小さくなるボックスが積み重なっています。

このグラフの横軸は時間を表し、どのコードが実行されているかを示しています。グラフ内のブロックが長いほど、そのコードの実行に時間がかかっていることを意味します。縦軸はネストされた関数呼び出しを示しています。スタックの上部にはdraw()関数があり、その下にはその中で呼び出される関数、さらにその下にはそれらの関数から呼び出される関数があります。マウスホイールでズームインして、1回のdraw()呼び出しがウィンドウの幅のほとんどを占めるようにすると、その直下のレベルを見てブロックの長さを確認することで、時間の使い方の内訳を見ることができます。最も長いブロックが、さらに最適化を試みるべき対象である可能性が高いです。

パフォーマンスタブでスケッチを録画した後のChromeの開発者ツールのスクリーンショット。アニメーションの1フレームにズームインしています。draw()関数がフレーム全体を占め、draw()から呼び出される関数とそれらの関数が呼び出す関数に細分化されています。

パフォーマンスタブでスケッチを録画した後のChromeの開発者ツールのスクリーンショット。アニメーションの1フレームにズームインしています。draw()関数がフレーム全体を占め、draw()から呼び出される関数とそれらの関数が呼び出す関数に細分化されています。

まとめ

これらのヒントとツールを使えば、高速に動作するWebGLプロジェクトをすぐに始められるはずです。ただし、最適化は複雑な場合もあるので、行き詰まっても心配しないでください!p5.js Discordサーバーに参加することを検討してみてください。そこでは提案を求めることができます。