p5.js備忘録(3)WebGL

OpenGL(2D/3Dグラフィック用API)やOpenGL ES(組込機器用のOpenGL)をもとにWeb上で利用できるように作られたAPIがWebGLです

備考:WebGL2.0はWebGL 1.0に機能を追加した技術
OpenGL ES2.0⇨WebGL 1.0
OpenGL ES3.0⇨WebGL 2.0

WebGLの機能をカプセル化する多くのフレームワークがあり、THREE.js が最も有名ですがまずは初心者に優しいp5.jsから😅

p5.jsは2Dグラフィックスを主に扱うライブラリですが、3Dグラフィックスも実現することができます

目次
  1. WebGLの実装で知っておきたいこと
  2. WebGLコンテキスト
  3. 3Dジオメトリ
  4. カスタムShapes
  5. Camera
  6. テクスチャ
  7. TEXT
  8. Lights
  9. Material
  10. createGraphics関数

WebGLの実装で知っておきたいこと

3D表現をする場合はとくに処理負荷が高くなります

WebGLで描かれるのは頂点データによって形成された三角形の集まりです

WebGLのプログラムはJavaScriptで記述するコードGPUで実行するシェーダー(GLSLという言語)で構成されます
*シェーダー(頂点シェーダーとフラグメントシェーダー)をWebGLに渡してコンパイル
JavaScriptはCPU(シングルスレッド)・GLSLはGPU(同時に並列)で処理
*CPUは一人で順番に複雑な作業をする・GPUは数千人で同時に単純作業をするイメージ

バッファはGPUにアップロードするバイナリデータの配列です
テクスチャはシェーダーでランダムにアクセスできるデータの配列です

ブラウザのJavascriptエンジンに多くのものが依存しているため、パフォーマンスは使用するブラウザによって違いが出ます
大量のオブジェクトの処理はかなりの負荷になり、ブラウザがクラッシュする可能性があります

WebGLコンテキスト

const canvas = document.querySelector("#glCanvas");
const gl = canvas.getContext('webgl');
const gl = canvas.getContext('webgl2');

p5.jsでWEBGLのコンテキストを有効にすることで「2D と 3D 」の両方で描画できるようになります

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

WEBGLモードでは、3 番目の次元「Z(画面から手前に向かう軸)」があります
0,0,0 (x,y,z) ポイントはキャンバスの中央です
*原点を左上隅に戻す場合translate(-width/2,-height/2,0);

3Dジオメトリ

3Dジオメトリ
*引数は位置ではなくサイズのみです
位置を変更するにはtranslate(x,y,z);
*detailは細分割数で曲線と線をどの程度滑らかに描画するかを指定できます
detailの数値が大きいほど滑らかな曲線が作成されますが負荷がかかるので通常はデフォルトで十分

  • box([width], [height], [depth], [detailX], [detailY]):幅、高さ、深さのボックスを描画します
  • plane([width], [height], [detailX], [detailY]):幅と高さで平面を描く
  • sphere([radius], [detailX], [detailY]):与えられた半径でを描きます
  • ellipsoid(([radiusx], [radiusy], [radiusz], [detailX], [detailY]):与えられた半径で楕円体を描く
  • cone([radius], [height], [detailX], [detailY], [cap]):指定された半径と高さの円錐を描画します
    *capはBooleanで円錐の底面を描画するかどうか
  • cylinder([radius], [height], [detailX], [detailY], [bottomCap], [topCap]):指定された半径と高さで円柱を描画します
  • torus([radius], [tubeRadius], [detailX], [detailY])):与えられた半径とチューブ半径でドーナツ型を描く

引数なしでジオメトリ(sphere・box・cone・cylinder・torus・plane・ellipsoid)を描画

ジオメトリを描画

boxをY軸を起点に回転
*クリックでスタートとストップを制御

<div id="canvas1"></div>
<script>
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas1');
  noLoop();
}
function draw(){
  background(200);
  rotateY(frameCount * 0.01);
  box(50);
}
const el1 = document.getElementById('canvas1');
   el1.addEventListener('click', ()=>{
    isLooping()? noLoop():loop()
  })
</script>

カスタムShapes

WEBGLモードでも2D Primitives(line()ellipse()など)で2Dの描画もできます
point(), line(), triangle() quad()はZ軸を引数にとることができます

*WEBGLではvertex()は x・y・z の 3つの引数を取ります

vertexでZ軸が有効になるので頂点の位置を加味した座標を配置
クリックでをX軸を起点に回転のスタートとストップを制御

<div id="canvas5"></div>
<script>
function setup(){
 createCanvas(200, 200, WEBGL).parent('canvas5');
 noLoop();
}
function draw(){
 background(0);
 stroke(255);
 noFill();
 rotateX(frameCount * 0.01);
 beginShape();
  vertex(-50, -50, -50);
  vertex( 50, -50, -50);
  vertex(0, 0, 50);

  vertex( 50, -50, -50);
  vertex( 50, 50, -50);
  vertex(0, 0, 50);

  vertex( 50, 50, -50);
  vertex(-50, 50, -50);
  vertex(0, 0, 50);

  vertex(-50,  50, -50);
  vertex(-50, -50, -50);
  vertex(0, 0, 50);
endShape();
}
const el5 = document.getElementById('canvas5');
   el5.addEventListener('click', ()=>{
   isLooping()? noLoop():loop()
 })
</script>

Camera

実際の見え方は移動距離(translate)ではなくカメラに大きく依存します
カメラが移動することでオブジェクトが移動しているように見える
*要するにカメラが視点

camera(x,y,z)
現在のカメラの位置を設定
シーンには複数のカメラを含めることができますが、一度にアクティブにできるカメラは 1 つだけです

camera(x,y,z)
//デフォルトは視野角60度の遠近法で下記を設定するのと同じ
camera(0, 0, (height/2) / tan(PI/6), 0, 0, 0, 0, 1, 0)
  • x: カメラの位置x座標(オプション)
  • y : カメラの位置y座標(オプション)
  • z :カメラの位置z座標(オプション)
  • centerX :カメラが指している場所x座標(オプション)
  • centerY :カメラが指している場所y座標(オプション)
  • centerZ : カメラが指している場所z座標 (オプション)
  • upX :カメラ上部のx座標(オプション)
  • upY:カメラ上部のy座標(オプション)
  • upZ:カメラ上部のz座標(オプション)

*up:被写体の回転

orbitControl()メソッドでマウスを使用してカメラを配置・ズーム・パンができます

デフォルトのカメラオプションをオーバーライドするには
perspective() または ortho() を呼び出します

  • perspective(fovy, aspect, near, far)
    遠近法を設定します
    遠近法はz軸で観察者に近いオブジェクトは遠くにあるオブジェクトよりも大きく表示されます
    引数が指定されていない場合
    perspective(PI/3, width/height, eyeZ/10, eyeZ*10)
    *fovy:視野(角度)
    *near:オブジェクトが表示されるために必要なカメラからの最小距離
    *far:オブジェクトがカメラから離れることのできる最大距離
  • orthoortho(left, right, bottom, top, near, far)
    正投影を設定します
    正投影は同じ寸法のオブジェクトは、z軸で離れていても同じサイズに見えます
    引数が指定されていない場合
    ortho(-width/2, width/2, -height/2, height/2)

<div id="canvas2"></div>
<script>
function setup(){
  createCanvas(200, 200, WEBGL).parent('canvas2');
  perspective(PI/3, width/height, 0, 500);
  noLoop();
}
function draw(){
  background(200);
  rotateX(0.2);
  rotateY(0.2);
  push();
  translate(0, 0, sin(frameCount / 30) * 100);
  box(50);
  pop();
}
</script>
<div id="canvas3"></div>
<script>
function setup(){
  createCanvas(200, 200, WEBGL).parent('canvas3');
  ortho(-width/2, width/2, -height/2, height/2, 0, 500);
  noLoop();
}
function draw(){
  background(200);
  rotateX(0.2);
  rotateY(0.2);
  push();
  translate(0, 0, sin(frameCount / 30) * 100;
  box(50);
  pop();
}
</script>

テクスチャ

texture()
WEBGLモードでのみ使用可
後続の形状をレンダリングするテクスチャを設定します
サポートされているテクスチャは画像・ビデオ・オフスクリーンレンダリング
*オフスクリーンレンダリングはプログラムが自分自身でレンダリングした画像をテクスチャに利用する

preload() メソッド内でテクスチャリング用の画像やビデオファイルをロードするのがベスト

<div id="canvas6"></div>
<script>
let img;
function preload() {
  img = loadImage('https://koro-koro.com/wp-content/uploads/2022/05/2.png');
}
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas6');
}
function draw() {
  background(0);
  rotateZ(frameCount * 0.01);
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.01);
  texture(img);
  box(width / 3);
}
</script>

beginShape() グラフィックにテクスチャを適用するにはUV座標を渡す必要があり、textureMode(NORMAL) を使用します

UV座標
3DCGモデルにテクスチャをマッピングするとき貼り付ける位置や方向大きさなどを指定するために使う座標(横方向の軸がU、縦方向の軸がV)
UV値は縦横ともに全て「0.0~1.0」の範囲

textureMode() :テクスチャマッピングの座標空間を設定
IMAGE :デフォルトで、画像の実際の座標を参照
NORMAL :0から1の範囲の値の正規化された空間を指します

<div id="canvas7"></div>
<script>
let img;
function preload() {
  img = loadImage('https://koro-koro.com/wp-content/uploads/2022/05/2.png');
}
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas7');
  textureMode(NORMAL);
}
function draw() {
  background(0);
  rotateY(frameCount * 0.01);
  texture(img)
  beginShape();
  vertex(-50, -50, 0, 0);
  vertex(50, -50, 1, 0);
  vertex(50, 50, 1, 1);
  vertex(-50, 50, 0, 1);
  endShape();
}
</script>

TEXT

2つの方法

テキストをオフスクリーンレンダラーに描画してからテクスチャとして使用
createGraphics(w, h, [renderer])はRendererオブジェクトを作成して返します
*w, h,で幅と高さをピクセル単位で定義
setup() で一度だけテキストをレンダリングしたテクスチャを draw() 関数で再利用できる場合パフォーマンスが向上
*テキストの忠実度はtext() ほど高くない

<div id="canvas8"></div>
<script>
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas8');
   pg = createGraphics(200, 200);
   pg.textSize(width / 4);
   pg.background(255);
   pg.text('HELLO', 15, 115);
}
function draw() {
  background(0);  
  rotateX(frameCount * 0.01);
  texture(pg);
  noStroke();
  plane(180);
}
</script>

text()メソッドのWEBGLバージョンを使用する場合
preload()関数内でloadFont() でロードされたopentype/truetypeフォントのみを使用できます
ファイルをアクセスできる場所に配置するか、CORS互換のwebl URLを使用する必要があります
stroke() は現在サポートされていない
*レンダリングされたテキストの忠実度が高い

<div id="canvas9"></div>
<script>
let myFont;
function preload() {
  const url = "https://fonts.gstatic.com/ea/notosansjapanese/v6/NotoSansJP-Bold.otf";
  myFont = loadFont(url);
}
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas9');;
  textFont(myFont);
  textSize(width / 4);
  textAlign(CENTER, CENTER);
}
function draw() {
  background(50);
  rotateX(frameCount * 0.01);
  text('HELLO', 0, 0);
}
</script>

Lights

ライトはdraw() 内で呼び出されループ で永続性を維持する必要があります
setup() 内に配置すると最初のループでのみ効果があります

3種類のライト関数
draw()内で使用する必要があります

  • ambientLight()
    最も単純で後で描画されるオブジェクトに均等な(全方向)光を作成します
    引数にはp5.Colorまたはr・g・b・aの数値を取ります
  • directionalLight()
    指定された色と方向でライトを作成します
    方向は「-1から1」で指定
    左から:(1,0,0)・右から:(-1,0,0)
    上から:(0,1,0)・下から:(0,-1,0)
    奥から:(0,0,1)・手前から:(0,0,-1)
    特定の起点がないためジオメトリに近づけたり遠ざけたりすることはできません
    最大5つのディレクショナルライトをアクティブにできます
  • pointLight(r,g,b,x,y,z)
    指定されたライトの色と位置でライトを作成します
    1点から全方向に放射
    特定の起点から光るためオブジェクトから遠い場合と近い場合で異なる効果があります
    最大 5 つのポイントライトをアクティブにできます

ambientLight(255):白のライト
ambientLight(0) : ブラックライト(ライトなし)
ambientMaterial()でマテリアルの反射する色を設定します
*ambientMaterialの詳細はMaterial参照

<div id="canvas10"></div>
<script>
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas10');
  background(0)
  ambientLight(255); // white light
  ambientMaterial(255, 127, 80);
  noStroke()
  sphere()
}
</script>
<div id="canvas11"></div>
<script>
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas11');
}
function draw(){
  background(0);
  let dirX = (mouseX / width - 0.5) * 2;
  let dirY = (mouseY / height - 0.5) * 2;
  directionalLight(255, 127, 80, -dirX, -dirY, -1);
  noStroke();
  sphere();
} 
</script>
<div id="canvas12"></div>
<script>
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas12');
}
function draw(){
  background(0);
  let locX = mouseX - width / 2;
  let locY = mouseY - height / 2;
  pointLight(255, 127, 80, locX, locY, 50);
  noStroke();
  sphere();
} 
</script>

Material

ジオメトリの表面にあたるライトのカラーをどのように反射するか

normalMaterial()
パラメータを取りません
ジオメトリの法線ベクトルをRGBカラーに自動的にマップします
ライトの影響を受けません
X軸に面した面は赤・Y軸に面した面は緑・Z軸に面した面は青

<div id="canvas13"></div>
<script>
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas13'); 
}
function draw() {
  background(0);
  rotateX(frameCount * 0.02);
  rotateZ(frameCount * 0.02);
  normalMaterial();
  box();
}
</script>

ambientMaterial()
オブジェクトがあらゆるライトの下で反射する色を設定します
全体の色はそれに先行するライト関数の影響を受けます
ambientMaterial(255, 255, 0)黄色で
ライトが白色 (255、255、255) を発する場合オブジェクトはライトの赤と緑の成分を反射するため黄色
ライトが赤色 (255, 0, 0) を発する場合オブジェクトはライトの赤色成分を反射するため赤色
ライトが青色 (0, 0, 255) を発する場合反射できる光の成分がないためオブジェクトは黒
ライトのRGB値が「0」の部分は反射しない

<div id="canvas14"></div>
<script>
function setup(){
  createCanvas(200, 200, WEBGL).parent('canvas14'); 
  noStroke();
}
function draw(){
  background(0);
  directionalLight(255, 0, 0, 1, 0, -1);
  ambientMaterial(255, 255, 0);
  rotateX(c14.frameCount * 0.02);
  rotateZ(c14.frameCount * 0.02);
  box();
}
</script>

specularMaterial()
最もリアルで光沢があります
光沢はshininess()関数で制御できます
shininess()のデフォルトおよび最小値は1です
オブジェクトが反射する色はすべてのタイプのライトで光源の色を反映します

<div id="canvas15"></div>
<script>
function setup() {
  createCanvas(200, 200, WEBGL).parent('canvas15'); 
  background(255);
  pointLight(255, 255, 255, 250, 100, 50);
  specularMaterial(250);
  shininess(50);
  noStroke();
  sphere();
}
</script>

createGraphics関数

オフスクリーンのバッファを作成するために使用され、通常のキャンバスとは別にグラフィックを描画し、それを後でメインのキャンバスに合成することができます

基本的な使い方

function setup() {
  createCanvas(400, 400);
  pg = createGraphics(200, 200);
}

function draw() {
  background(200);

  // pg (オフスクリーンバッファ) に描画
  pg.background(100);
  pg.fill(255, 0, 0);
  pg.ellipse(pg.width / 2, pg.height / 2, 50, 50);

  // メインキャンバスにオフスクリーンバッファを描画
  image(pg, 100, 100);
}

createGraphics() を使って作成したグラフィックは、テクスチャとして他のオブジェクトに適用することもできます

let pg;
let angle = 0;

function setup() {
  createCanvas(400, 400, WEBGL);
  pg = createGraphics(200, 200);
}

function draw() {
  background(200);

  // オフスクリーンバッファに描画
  pg.background(50);
  pg.fill(255);
  pg.textSize(32);
  pg.textAlign(CENTER, CENTER);
  pg.text('Hello, p5.js!', pg.width / 2, pg.height / 2);

  // テクスチャとして適用
  texture(pg);
  rotateY(angle);
  box(100);
  angle += 0.03;
}