p5.js WebGL入門 4 テクスチャ

本稿は「18.4: Texture – WebGL and p5.js Tutorial」YouTube動画を元にしています。

テクスチャマッピング

テクスチャとは絵柄、図柄といった意味で、3Dジオメトリの表面に好きな画像をテクスチャとして割り当てることができます。

下図はBlenderで平面を作成し、それにJPGイメージをテクスチャとして設定しているところです。

これをBlenderでレンダリングすると、下図のように、平面にJPGイメージが貼り付けられた結果が得られます。

ここではざっと、次のようなことが行われています。まず、JPGイメージは縦ピクセル x 横ピクセル個のピクセルで構成されています。

各ピクセルはRGB(A)値を持っています。Blenderはテクスチャに指定された画像を調べて、テクスチャが割り当てられた平面に対応させます。

具体的に言うと、画像サイズが100 x 100で、平面も100 x 100の場合、画像の(0,0)にあるピクセルを、平面の(0,0,0)に対応させ、(0,100)にあるピクセルを(0,100,0)に対応させ、これを画像の全位置で繰り返します。すると、画像全体を平面が存在する3Dの世界に持ち込むことができます。

テクスチャのこの対応付けはテクスチャマッピングと呼ばれます。

とは言え、見ている人(ビューワー)に対して、直立、正対している平面なら、単純な1対1対応でマッピングすればよいわけですが、角度がつくと、事態は複雑になります。下図の表面A、B、Cには同じテクスチャが描画されますが、ビューワーから見た各表面の形はことなるので、テクスチャも歪めて描く必要があります。

p5.js WEBGLモードでは、こういった細かな作業は全部内部で処理されるので、心配は無用です。

イメージのテクスチャマッピング

p5.js WEBGLモードでのテクスチャマッピングは、細かなことを考えず、texture()関数を使うだけで、いたって簡単に行えます。

次のコードでは、立方体にテクスチャを適用しているので、立方体を回転させると、テクスチャの画像を貼り付けた箱が回転しているように見えます。

let angle = 0;
// テクスチャ用変数
let tex;
function preload() {
  // 画像ファイルをイメージとして読み込む
  tex = loadImage('assets/texImage.jpg');
}

function setup() {
  createCanvas(400, 300, WEBGL);
  noStroke();
}

function draw() {
  background(100);

  rotateX(angle);
  rotateY(angle + 0.3);
  rotateZ(angle + 1.2);
  // texture()関数に、テクスチャとして使用するイメージを指定する
  texture(tex);

  box(100);

  angle += 0.03;
}
texture()

ジオメトリにテクスチャを割り当てる。
シンタックス
texture(tex)
パラメータ
tex p5.Image|p5.MediaElement|p5.Graphics: テクスチャとしてレンダリングする2次元グラフィックス

球体にうまく対応できる画像ファイルが用意できると、sphere()で表現できる範囲が広がります。

let angle = 0;
// テクスチャ用変数
let tex;
let earth, ball;

function preload() {
    // 画像ファイルをイメージとして読み込む
    earth = loadImage('assets/opengl.png');
    ball = loadImage('assets/ball.png');
}

function setup() {
    createCanvas(400, 300, WEBGL);
    angleMode(DEGREES);
    noStroke();
    tex = earth;

    const radio = createRadio();
    radio.option('地球');
    radio.option('ボール');
    radio.value('地球');
    radio.position(10, 10);
    radio.changed(() => {
        const val = radio.value();
        if (val === '地球') {
            tex = earth;
        }
        else if (val === 'ボール') {
            tex = ball;
        }
    })
}

function draw() {
    background(200);
    // ライティング
    ambientLight(100);
    directionalLight(255, 255, 255, 1, 1, 0);
    pointLight(255, 255, 255, -50, -50, 150);
    // 自転と地軸の傾き
    rotateY(angle + 0.3);
    rotateZ(-23.4);
    // テクスチャを指定
    texture(tex);
    sphere(100);
    angle += 0.3;
}

左上のラジオボタンのクリックで、球体に適用するテクスチャが変更できます。

オフスクリーンキャンバスのテクスチャマッピング

texture()関数のパラメータには、p5.Graphicsもあります。これはテクスチャとしてp5.Graphicsが利用できるということです。p5.Graphicsは、キャンバス要素としてWebページに追加しない、画面外のキャンバス(オフスクリーン)キャンバスです。オフスクリーンキャンバスについては「1:構造(Structure)」で述べています。

let angle = 0;
// オフスクリーンキャンバス
let offScreenCanvas;

function setup() {
    createCanvas(400, 300, WEBGL);
    angleMode(DEGREES);
    // オフスクリーンキャンバスを作成
    offScreenCanvas = createGraphics(200, 200);
    // オフスクリーンキャンバスを作成の背景色と塗り色を設定
    offScreenCanvas.background('#006600');
    offScreenCanvas.fill('#FF6600');
}

function draw() {
    background(100);

    rotateX(angle);
    rotateY(angle + 0.3);
    rotateZ(angle + 0.5);

    // オフスクリーンキャンバスに、マウス位置を中心とする直径20の円を描く
    offScreenCanvas.ellipse(mouseX, mouseY, 20);

    // テクスチャとしてオフスクリーンキャンバスを適用
    texture(offScreenCanvas);
    box(100);
    angle += 1;
}

マウスでキャンバスの左上をなぞると、立方体のテクスチャがリアルタイムで変化します。

ビデオのテクスチャマッピング

texture()関数のパラメータには、p5.MediaElementも含まれています。p5.MediaElementは、createVideo()やcreateAudio()、createCapture()関数で作成されるHTML要素です。

createVideo()とcreateCapture()からは<video>要素が、createAudio()からは<audio>要素が作成されます。テクスチャとして使用できるのは<video>要素です。

ビデオの映像は動いているように見えますが、これは目の錯覚で、実際には毎秒60枚の静止画像が、パラパラ漫画のように、連続して描画されています。したがってこの高速で変化するビデオの1枚1枚の静止画像をとらえることができれば、それをテクスチャとして使用でき、その結果、ジオメトリの表面でビデオが再生されているように見えるというわけです。

createVideo()

createVideo()関数は、録画の終わったビデオファイルの再生に利用できます。ただし利用するのは<video>要素が再生するビデオ各フレームの静止画像であり、<video>要素自体は表示しません。

let angle = 0;
// ビデオ
let vid;
let isUserOK = false;
let customFont;

function preload() {
    vid = createVideo('assets/lego_starWars.mp4');
    // ビデオのサウンドを再生しない場合にはmutedをtrueにする
    vid.elt.muted = true;
    // またはvid.volume(0);
    vid.elt.currentTime = 1;
    vid.hide();

    // オープンタイプフォントを読み込む
    customFont = loadFont('assets/NotoSerifJP-ExtraLight.otf');
}

function setup() {
    createCanvas(400, 300, WEBGL);
    angleMode(DEGREES);
    noStroke();

    // フォントの塗り色
    fill('yellow');
    // このプログラムで使用するフォントを指定
    textFont(customFont);
    // フォントのサイズとアライメント
    textSize(30);
    textAlign(CENTER);
}

function draw() {
    background(100);
    // ユーザーが自分からビデオ再生を行った場合は
    if (isUserOK) {
        rotateX(angle);
        rotateY(angle + 0.3);
        rotateZ(angle + 0.5);

        texture(vid);
        box(100);
        angle += 1;
    }
    else {
        text('再生するには\n画面をクリック', 10, 10);
    }
}

// ビデオ再生はユーザーの積極的なインタラクションによって開始する
function mousePressed() {
    // video.play()は非同期動作(すぐに再生が始まるわけではない)
    const playPromise = vid.elt.play();
    if (playPromise !== undefined) {
        playPromise.then(() => {
                print('再生開始');
                isUserOK = true;
            })
            .catch((error) => {
                print('エラー発生!');
            })
    }
}

画面のクリックで、ビデオのテクスチャマッピングが実行されます。

ここではcreateVideo()関数を使って<video>要素を作成し、その後hide()メソッドで<video>要素を隠しています。プログラムでは、隠した<video>要素の1フレーム1フレームを読み取って、それをテクスチャとして立方体に適用しています。

しかし、上記コードには、前のイメージのテクスチャマッピングにはなかった、テクスチャマッピングとは直接関係のない、ビデオ再生に関するコードが含まれています。それは、ブラウザのビデオやオーディオの再生には、ユーザーによる積極的なアクションが必要だとする、ブラウザの制約によるものです。言い換えると、開発者はビデオやサウンドを勝手に再生してはいけない、ユーザーが再生ボタンをクリックするなどして、自分からアクションを起こす手順を取る必要がある、という制約です。

ビデオをsetup()関数内で直接再生しようとすると、Chromeブラウザの場合コンソール画面に、Uncaught (in promise) DOMException: play() failed because the user didn’t interact with the document first(ユーザーが初めにドキュメントとやりとりしなかったため、play()は失敗した)という警告が表示されます。上記コードでは、これを回避するために、ページのマウスプレスで呼び出されるmousePressed()関数内で、再生を開始しています(参考ページ「DOMException: The play() request was interrupted」)。

// ビデオ再生はユーザーの積極的なインタラクションによって開始する
function mousePressed() {
    // video.play()は非同期動作(すぐに再生が始まるわけではない)
    // play()はPromiseを返す
    const playPromise = vid.elt.play();
    // 再生が始まったら、変数isUserOKをtrueにする => draw()で実際の描画を実行する
    if (playPromise !== undefined) {
        playPromise.then(() => {
                print('再生開始');
                isUserOK = true;
            })
            .catch((error) => {
                print('エラー発生!');
            })
    }
}

そして、ユーザーにマウスクリックをうながすために、キャンバスにテキストを表示しています(WEBGLモードでテキストを描画するには、カスタムフォントを読み込んでそれを使用する必要があります)。

またWebGL: INVALID_VALUE: tex(Sub)Image2D: video visible size is empty(無効な値、ビデオの表示サイズが空)という警告が表示される場合があります。これは、draw()で最初のフレームが描画されるとき、ビデオの最初のフレームにテクスチャにすべきイメージがない、ことによる警告だと思われます。draw()が次のフレームに進むと立方体にビデオのテクスチャは描画されます。上記コードでは、これを回避するために、ビデオを開始フレームを強制的に1にしています(vid.elt.currentTime = 1;)。

なお、突然サウンドが再生されると、多くのユーザーはびっくりするので、クリックによってサウンドも再生されることをユーザーに前もって知らせる工夫をするか、上記コードのようにミュートしておくのが無難でしょう。

createCapture()

createCapture()関数は、コンピュータに接続したカメラからの映像の再生に利用できます。createCapture()が作成する<video>の各フレームはテクスチャマッピングに使用できます。

let angle = 0;
//
let cam;
let isUserOK = false;
let customFont;

function preload() {
    // オープンタイプフォントを読み込む
    customFont = loadFont('assets/NotoSerifJP-ExtraLight.otf');
}

function setup() {
    createCanvas(400, 300, WEBGL);
    angleMode(DEGREES);
    noStroke();

    fill('yellow');
    textFont(customFont);
    textSize(30);
    textAlign(CENTER);
}

function draw() {
    background(100);
    // ユーザーが自分からビデオ再生を行った場合は
    if (isUserOK) {
        rotateX(angle);
        rotateY(angle + 0.3);
        rotateZ(angle + 0.5);

        texture(cam);
        box(100);
        angle += 1;
    }
    else {
        text('実行するには\n画面をクリック', 10, 10);
    }
}

// ビデオ再生はユーザーの積極的なインタラクションによって開始する
function mousePressed() {
    cam = createCapture(VIDEO);
    cam.size(320, 240)
    cam.hide();
    isUserOK = true;
}

p5.jsのビデオに関する扱いは「12_2:ビデオの操作 Creative Coding p5.js」で述べています。

UV座標

beginShape()とendShape()関数の間でvertex()関数を使って作成するカスタムシェイプにテクスチャを適用するには、UV座標をvertex()に渡す必要があります。これについては「p5.js WebGL入門 8 3D カスタムシェイプ」で述べています。

またカスタムシェイプにテクスチャを適用するときに使用するtextureMode()関数と、UV座標の操作でテクスチャの繰り返しを制御するtextureWrap()関数も「p5.js WebGL入門 8 3D カスタムシェイプ」で述べています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA