Processing Community Day 2021

12/11(Saturday)

自己紹介

  • 名前 : 独楽回しeddy

  • twitter : https://twitter.com/EKey2210

  • 週に1つくらいの間隔でProcessing、p5.js、glslなどを使ってお絵描きして動画投稿してます。

今回のテーマ

  • p5.js と バーテックスシェーダー
  • p5.jsでバーテックスシェーダーを使って動くものを作ってみます。

目次

  • 揺らす元の模様を作ってみる
  • 画像を貼り付ける先の平面を作る
  • 平面に画像を貼り付ける
  • 平面を揺らしてみる
  • シェーダーを使って揺らしてみる

※ 右下のカーソルで右を押すと次の章、下を押すとその章の解説へ行きます。

揺らす元の模様を作ってみる

  • createGraphics を使って作っていきます。
  • createGraphics はCanvasとは別で画像として扱う事が可能な描画領域を作り出す関数
createGraphics(width, height, [renderer]);

コード1

  • 青い背景に白で塗り潰した描画領域を作って表示するスケッチ
let grachics;

function setup(){
    // 一度だけ呼ばれる
    createCanvas(600, 600);  // 600×600 の画面を作る
    background(color("#255699"));  // 背景を塗りつぶす(色は任意)

    graphics = createGraphics(450, 300);  // 450×300 の新たな描画領域を作る
}

function draw(){
    // 何度も呼ばれる(1秒につき30 ~ 60回)
    background(37, 86, 153);

    graphics.background(255);  // 新たな白で描画領域は塗りつぶす
    
    // createGraphicsで作った描画領域は画像として扱えるので画像の描画方法と同じ
    imageMode(CENTER);  // 画像の中心を配置基準とする
    image(graphics, width/2, height/2);  // 画面の中心(横幅の半分、縦幅の半分)に配置する
}

  • 中央にある白い部分がcreateCanvasで作った描画領域。しかしこれではかなり寂しいので白い部分にもう少しだけ図形を追加してみます。

コード2

  • 描画領域に4色の図形を描いて表示するスケッチ
let grachics;

function setup(){
    // 一度だけ呼ばれる
    createCanvas(600, 600);  // 600×600 の画面を作る
    background(color("#255699"));  // 背景を塗りつぶす(色は任意)

    graphics = createGraphics(450, 300);  // 450×300 の新たな描画領域を作る

    graphics.background(255);  // 白で描画領域は塗りつぶす
    graphics.ellipseMode(CENTER); // 円を描画する際はその中心を配置基準とする
    graphics.rectMode(CENTER);  // 四角を描画する際はその中心を配置基準とする
    graphics.noStroke();  // 線は書かない

    graphics.push();  // 描画する上での設定を保存
    graphics.translate(graphics.width/2, graphics.height/2);  // 描画領域の中心を原点とする
    graphics.fill(color("#50d0d0")); // 水色
    graphics.ellipse(-120, -80, 75); // 円を描く

    graphics.fill(color("#be1e3e")); // 赤色
    graphics.rect(120, -80, 75, 75); // 四角を描く

    graphics.fill(color("#7967c3")); // 紫色
    graphics.rect(-120, 80, 75, 75); // 四角を描く

    graphics.fill(color("#ffc639")); // 黄色
    graphics.ellipse(120, 80, 75); // 円を描く
    graphics.pop(); // 描画する上での設定を元に戻す
}

function draw(){
    // 何度も呼ばれる(1秒につき30 ~ 60回)
    background(37, 86, 153);

    // createGraphicsで作った描画領域は画像として扱えるので画像の描画方法と同じ
    imageMode(CENTER);  // 画像の中心を配置基準とする
    image(graphics, width/2, height/2);  // 画面の中心(横幅の半分、縦幅の半分)に配置する
}

変更箇所

  • 描画領域に4色の図形が描画されました。これで元となる模様は完成。

画像を貼り付ける先の平面を作る

  • ここでは頂点を細かく設定された平面を作っていきます。

イメージ1

Plane1

  • 最小の平面は頂点4つから成ります。
  • [0,1,2]、[1,2,3]の順に結ぶと出来る三角形2つが組み合わさって出来ます。

イメージ2

Plane2

  • 今回は出来るだけ細かく波打つようにしたい
    -> 小さい四角形をたくさん組み合わせて作り上げるイメージ

平面を作成するスケッチ

let gWidth = 450;       // メッシュの横の長さ
let gHeight = 300;      // メッシュの縦の長さ

let len = 10;           // 小さな四角形の1辺の長さ 
let col = gWidth/len;   // 全体の横の長さをlenで割って、行の点の数を求める
let row = gHeight/len;  // 全体の縦の長さをlenで割って、列の点の数を求める

function setup() {
    createCanvas(600, 600);
    background(color("#255699"));  // 背景塗り潰し
}

function draw() {
    background(color("#255699"));  // 背景塗り潰し

    translate((width-gWidth)/2, (height-gHeight)/2);  // 平面を中央に置くために必要
    noFill();  // 塗り潰ししない
    stroke(255);  // 白い線

    for (let y = 0; y < row - 1; y++) {      // 1周毎に次の列の頂点まで見るので列数-1まで
        beginShape(TRIANGLE_STRIP);          // 順に連結した三角形 を生成する
        for (let x = 0; x < col; x++) {      // 1行の頂点全てで見ていく
            vertex(x * len, y * len);        // 頂点 x, y 
            vertex(x * len, (y + 1) * len);  // 頂点 x, y+1
        }
        endShape();                // 1列分が終わったら一旦区切る
    }
}

次のページで補足

補足

Plane3

  • beginShape(TRIANGLE_STRIP) は連結した三角形を作るための宣言
  • 0 -> 1 -> 2 -> 3 … と繋げていくことで自動で0と2、1と3も繋がってくれます。

平面を作成するスケッチ


  • 木目細やかな四角で出来た平面が出来ました!

平面に画像を貼り付ける

  • createGraphics で作った画像を平面に貼り付けます。
  • その前にuv座標について理解する必要がありますのでその説明も入れます。

uv座標について

uv

  • 画像上の点の位置を表すための座標
  • 横をu、縦をvと表し、共に最小値が0、最大値が1です。
  • 画像を貼り付ける際は、各々の頂点にuv座標を指定する必要があります。
uv
function setup(){
    createCanvas(600, 600, WEBGL);  // texture()を使うにはWEBGLモードでなければならない
    ...
    textureMode(NORMAL);  // 画像を貼る際に画像上の位置指定を0~1で行うようにする
}

function draw(){
    ...

    texture(graphics);   // 画像を貼り付けるための宣言

    beginShape();             // 頂点を結んで図形の形成することの宣言
    vertex(0,     0, 0, 0);   // 四角形の頂点の座標、uv座標
    vertex(0,   150, 0, 1);   // 四角形の頂点の座標、uv座標
    vertex(150, 150, 1, 1);   // 四角形の頂点の座標、uv座標
    vertex(150,   0, 1, 0);   // 四角形の頂点の座標、uv座標
    endShape();               // 頂点を結んで図形の形成することを終了する宣言
}

createCanvasでWEBGLモードを指定しなければtexture()を使えないので指定します。

vertex()は位置座標の後に画像の座標を指定でき、図の赤い線で示している部分を対応させてます。

平面に画像を貼り付けるスケッチ

let gWidth = 450;       // メッシュの横の長さ
let gHeight = 300;      // メッシュの縦の長さ

let len = 10;           // 小さな四角形の1辺の長さ 
let col = gWidth/len;   // 全体の横の長さをlenで割って、行の点の数を求める
let row = gHeight/len;  // 全体の縦の長さをlenで割って、列の点の数を求める

let graphics;

function setup() {
    createCanvas(600, 600, WEBGL);  // texture()を使うにはWEBGLモードでなければならない
    background(color("#255699"));  // 背景塗り潰し

    graphics = createGraphics(gWidth, gHeight);  // 平面と同じサイズで作成
    graphics.background(255);  // 新たな白で描画領域は塗りつぶす
    graphics.ellipseMode(CENTER); // 円を描画する際はその中心を配置基準とする
    graphics.rectMode(CENTER);  // 四角を描画する際はその中心を配置基準とする
    graphics.noStroke();  // 線は書かない

    graphics.push();  // 描画する上での設定を保存
    graphics.translate(graphics.width/2, graphics.height/2);  // 描画領域の中心を原点とする
    graphics.fill(color("#50d0d0")); // 水色
    graphics.ellipse(-120, -80, 75); // 円を描く

    graphics.fill(color("#be1e3e")); // 赤色
    graphics.rect(120, -80, 75, 75); // 四角を描く

    graphics.fill(color("#7967c3")); // 紫色
    graphics.rect(-120, 80, 75, 75); // 四角を描く

    graphics.fill(color("#ffc639")); // 黄色
    graphics.ellipse(120, 80, 75); // 円を描く
    graphics.pop(); // 描画する上での設定を元に戻す

    textureMode(NORMAL);  // 画像を貼る際に画像上の位置指定を0~1で行うようにする
}

function draw() {
    background(color("#255699"));  // 背景塗り潰し

    translate(-gWidth/2, -gHeight/2);  // 平面を中央に置くために必要
    noFill();  // 塗り潰ししない
    stroke(255);  // 白い線

    texture(graphics);                           // 画像を貼り付ける宣言
    for (let y = 0; y < row - 1; y++) {          // 1周毎に次の列の頂点まで見るので列数-1まで
        beginShape(TRIANGLE_STRIP);              // 順に連結した三角形 を生成する
        for (let x = 0; x < col; x++) {          // 1行の頂点全てで見ていく
            // uv座標(0~1)を求める
            let u = map(x, 0, col-1, 0, 1);      
            let v1 = map(y, 0, row-1, 0, 1);
            let v2 = map(y+1, 0, row-1, 0, 1);

            vertex(x * len, y * len, u, v1);        // 位置座標、uv座標
            vertex(x * len, (y + 1) * len, u, v2);  // 位置座標、uv座標
        }
        endShape();                              // 1列分が終わったら一旦区切る
    }
}

createCanvasでWEBGLモードを指定しなければtexture()を使えないので指定します。

貼り付ける画像を生成

描画モードをWEBGLにすることで画面中央が原点になるので位置調整

uv座標を計算し、平面を作るvertexの位置座標の後に設定

平面に画像を貼り付けるスケッチ


  • 無事平面にcreateCanvasで生成した画像を綺麗に貼りつけられました!

平面を揺らしてみる

  • 画像を貼り付けた平面に動きをつけます。
  • 今回は簡単な旗っぽい動きをつけてみます。

揺らす動き

  • 三角関数のsin波の動きをつけてみます。
  • sin(角度)の角度に頂点座標で重み付けをする。

揺らす動きをつけるスケッチ

let gWidth = 450;       // メッシュの横の長さ
let gHeight = 300;      // メッシュの縦の長さ

let len = 10;           // 小さな四角形の1辺の長さ 
let col = gWidth/len;   // 全体の横の長さをlenで割って、行の点の数を求める
let row = gHeight/len;  // 全体の縦の長さをlenで割って、列の点の数を求める

let graphics;

function setup() {
    createCanvas(600, 600, WEBGL);  // texture()を使うにはWEBGLモードでなければならない
    background(color("#255699"));  // 背景塗り潰し

    graphics = createGraphics(gWidth, gHeight);  // 平面と同じサイズで作成
    graphics.background(255);  // 新たな白で描画領域は塗りつぶす
    graphics.ellipseMode(CENTER); // 円を描画する際はその中心を配置基準とする
    graphics.rectMode(CENTER);  // 四角を描画する際はその中心を配置基準とする
    graphics.noStroke();  // 線は書かない

    graphics.push();  // 描画する上での設定を保存
    graphics.translate(graphics.width/2, graphics.height/2);  // 描画領域の中心を原点とする
    graphics.fill(color("#50d0d0")); // 水色
    graphics.ellipse(-120, -80, 75); // 円を描く

    graphics.fill(color("#be1e3e")); // 赤色
    graphics.rect(120, -80, 75, 75); // 四角を描く

    graphics.fill(color("#7967c3")); // 紫色
    graphics.rect(-120, 80, 75, 75); // 四角を描く

    graphics.fill(color("#ffc639")); // 黄色
    graphics.ellipse(120, 80, 75); // 円を描く
    graphics.pop(); // 描画する上での設定を元に戻す

    textureMode(NORMAL);  // 画像を貼る際に画像上の位置指定を0~1で行うようにする
}

function draw() {
    background(color("#255699"));  // 背景塗り潰し

    translate(-gWidth/2, -gHeight/2);  // 平面を中央に置くために必要
    noFill();  // 塗り潰ししない
    stroke(255);  // 白い線

    texture(graphics);                           // 画像を貼り付ける宣言
    for (let y = 0; y < row - 1; y++) {          // 1周毎に次の列の頂点まで見るので列数-1まで
        beginShape(TRIANGLE_STRIP);              // 順に連結した三角形 を生成する
        for (let x = 0; x < col; x++) {          // 1行の頂点全てで見ていく
            // uv座標(0~1)を求める
            let u = map(x, 0, col-1, 0, 1);      
            let v1 = map(y, 0, row-1, 0, 1);
            let v2 = map(y+1, 0, row-1, 0, 1);

            vertex(x * len, y * len + sin(frameCount*0.2+u*TWO_PI*2) * len, u, v1);        // y座標にsin関数分の値を足す(uの値で特徴づけ)
            vertex(x * len, (y + 1) * len + sin(frameCount*0.2+u*TWO_PI*2) * len, u, v2);  // y座標にsin関数分の値を足す(uの値で特徴づけ)
        }
        endShape();                              // 1列分が終わったら一旦区切る
    }
}

y座標にsin関数で計算された値を足して動きをつける。

揺らす動きをつけるスケッチ


  • 無事揺らす動きがつけられました!

シェーダーを使って揺らしてみる

  • 揺らす動きをシェーダーを使ったやり方で実現を試みます。

シェーダーとは?

  • 3次元の物体に対して陰影をつけるための計算を行うプログラムを指します。
  • glsl、Metal、HLSLなどが該当します。p5.jsではglslを使います。

シェーダーとは?

uv

  • 主にバーテックスシェーダーで頂点について、フラグメントシェーダーで色情報について処理が行われます。
  • 大まかにはバーテックスシェーダーによる座標変換、その後フラグメントシェーダーで頂点が構成する面について色情報を決定するという流れになります。

シェーダーを使う

// バーテックスシェーダーのコードをvsに文字列として入れる。
let vs = `
precision mediump float;  // どれくらいの精度で計算するかを定義

attribute vec3 aPosition;       // 頂点の位置
attribute vec2 aTexCoord;       // 画像のuv座標
uniform mat4 uProjectionMatrix; // プロジェクション変換行列(カメラに映る範囲の決定に使う)
uniform mat4 uModelViewMatrix;  // ビュー変換行列(カメラの視点の決定に使う)
uniform float time;             // 時間(p5.jsのスケッチ側から送ってもらう)

varying vec2 vTexCoord;         // 画像のuv座標(フラグメントシェーダーに送る)

void main() { 
    vec3 uPosition = aPosition + vec3(0.0, sin(time + aPosition.x * 0.02)*20.0, 0.0);  // 元々の位置座標に対して頂点のx座標で重み付けをしてsin関数の動きをつける
    vec4 positionVec4 = vec4(uPosition, 1.0);  // 位置座標をvec4型にして格納

    gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;  // 座標変換を実行(プロジェクション変換行列×ビュー変換行列×頂点座標)
    vTexCoord = aTexCoord;  // フラグメントシェーダー側に送るためのuv座標受け渡し
}
`;

// フラグメントシェーダーのコードをfsに文字列として入れる。
let fs = `
#ifdef GL_ES
precision mediump float;  // どれくらいの精度で計算するかを定義
#endif

varying vec2 vTexCoord;  // バーテックスシェーダー側から送られた画像のuv座標

uniform sampler2D tex;   // 送られた画像の情報
uniform float time;      // 時間(p5.jsのスケッチ側から送ってもらう)

void main() {

    // 送られた画像の色情報をそのまま返す
    vec2 uv = vTexCoord;
    vec4 texture = texture2D(tex, uv);
    gl_FragColor = texture;            
}
`;

let gWidth = 450;       // メッシュの横の長さ
let gHeight = 300;      // メッシュの縦の長さ

let len = 10;           // 小さな四角形の1辺の長さ 
let col = gWidth/len;   // 全体の横の長さをlenで割って、行の点の数を求める
let row = gHeight/len;  // 全体の縦の長さをlenで割って、列の点の数を求める

let graphics;
let useShader;  // シェーダー

function setup() {
    createCanvas(600, 600, WEBGL);  // texture()を使うにはWEBGLモードでなければならない
    background(color("#255699"));  // 背景塗り潰し

    useShader = createShader(vs, fs);  // シェーダーを作る

    graphics = createGraphics(gWidth, gHeight);  // 平面と同じサイズで作成
    graphics.background(255);  // 新たな白で描画領域は塗りつぶす
    graphics.ellipseMode(CENTER); // 円を描画する際はその中心を配置基準とする
    graphics.rectMode(CENTER);  // 四角を描画する際はその中心を配置基準とする
    graphics.noStroke();  // 線は書かない

    graphics.push();  // 描画する上での設定を保存
    graphics.translate(graphics.width/2, graphics.height/2);  // 描画領域の中心を原点とする
    graphics.fill(color("#50d0d0")); // 水色
    graphics.ellipse(-120, -80, 75); // 円を描く

    graphics.fill(color("#be1e3e")); // 赤色
    graphics.rect(120, -80, 75, 75); // 四角を描く

    graphics.fill(color("#7967c3")); // 紫色
    graphics.rect(-120, 80, 75, 75); // 四角を描く

    graphics.fill(color("#ffc639")); // 黄色
    graphics.ellipse(120, 80, 75); // 円を描く
    graphics.pop(); // 描画する上での設定を元に戻す

    textureMode(NORMAL);  // 画像を貼る際に画像上の位置指定を0~1で行うようにする
}

function draw() {
    background(color("#255699"));  // 背景塗り潰し

    shader(useShader);  // シェーダーの実行を宣言する

    useShader.setUniform("time", frameCount*0.1);  // 総フレーム数 × 0.01を時間としてシェーダー側に送る
    useShader.setUniform("tex", graphics);    // 作った画像(graphics)をシェーダー側に送る

    noStroke();  // メッシュの線を書かないようにする
    push();
    translate(-gWidth/2, -gHeight/2);  // 平面を中央に置くために必要
    //texture(graphics);               // この宣言も不要になる
    for (let y = 0; y < row - 1; y++) {          // 1周毎に次の列の頂点まで見るので列数-1まで
        beginShape(TRIANGLE_STRIP);              // 順に連結した三角形 を生成する
        for (let x = 0; x < col; x++) {          // 1行の頂点全てで見ていく
            // uv座標(0~1)を求める
            let u = map(x, 0, col-1, 0, 1);
            let v1 = map(y, 0, row-1, 0, 1);
            let v2 = map(y+1, 0, row-1, 0, 1);
            vertex(x * len, y * len, u, v1);    // シェーダー側で頂点を動かすのでここで動かす処理は不要になる
            vertex(x * len, (y + 1) * len, u, v2);    // シェーダー側で頂点を動かすのでここで動かす処理は不要になる
        }
        endShape();                              // 1列分が終わったら一旦区切る
    }
    pop();
}

バーテックスシェーダーのコードを文字列として定義

フラグメントシェーダーのコードを文字列として定義

定義した文字列を引数としてcreateShaderを実行し、シェーダーを実行できる状態にする。

シェーダーを実行を宣言し、適用する。

シェーダーにスケッチ側から情報を送る場合はsetUniformを使う。

シェーダー側の座標変換の前にsin関数の動きを付ける。

シェーダー側で頂点座標を動かすのでここで動かすようにする必要がなくなる。

シェーダーを使う


  • バーテックスシェーダーを使って揺れる動きを付けることができました!

まとめ

  • 描画した結果を画像として扱って動く平面に貼り付ける方法を説明しました。これが出来ると色々面白いことに応用できないかなと思います。

  • かなり簡潔にはなりましたがシェーダーを使って頂点座標を動かす方法についても解説しました。バーテックスシェーダーは自分個人としてはとっかかりに苦労したので、これで少しでも始める上での敷居が下げられる助けになればと思います。

ここまで見てくださりありがとうございました。