みかづきブログ・カスタム

基本的にはちょちょいのほいです。

WebGL2を使って四角形を描き、ウェブカメラから取得した画像をテクスチャに設定する 🖼️

Three.jsPIXI.js経由で、間接的にWebGLを使ったことはあるものの、直接操作をしたことがなかったので、めちゃめちゃ基本的なところから触ってみました。

順序としては、

  • 試しに三角形を描く
  • 試しに四角形を描く
  • 四角形にテクスチャを貼る

という順序で進めていきます。

WebGLで真っ赤な三角形を描く

DEMO

三角形を描くだけでとても大変でした。

頂点シェーダーを書く

<script id="vertex-shader" type="x-shader/x-vertex">
  attribute vec3 vertexPosition;

  void main(void){
    gl_Position = vec4(vertexPosition, 1.0);
  }
</script>

頂点の座標をパスするだけのシェーダーを書きます。

フラグメントシェーダーを書く

<script id="fragment-shader" type="x-shader/x-fragment">
  precision mediump float;

  void main(void){
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
</script>

赤く塗りつぶすだけのシェーダーを描きます。

三角形を描く

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

const vertexShaderSource = document.getElementById('vertex-shader').textContent;
const vertexShader = gl.createShader(gl.VERTEX_SHADER);

gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);

const fragmentShaderSource = document.getElementById('fragment-shader').textContent;
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);

const program = gl.createProgram();

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

// Cancasの中心が(0,0)、左上が(-1, 1)、右下が(1, 1)
const vertices = [
  -.5, .5,
  -.5, -.5,
  .5, -.5
];

const vertexBuffer = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

const vertexPositionLocation = gl.getAttribLocation(program, 'vertexPosition');

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.vertexAttribPointer(vertexPositionLocation, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(vertexPositionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

gl.drawArrays(gl.TRIANGLES, 0, 3);

WebGL2で真っ赤な三角形を描く

時代に合わせて、WebGL2で書き直します。

DEMO


頂点シェーダーを書く

<script id="vertex-shader" type="x-shader/x-vertex">
  #version 300 es

  in vec2 vertexPosition;

  void main(void) {
    gl_Position = vec4(vertexPosition, 0.0, 1.0);
  }
</script>

頂点の座標をパスするだけのシェーダーを書きます。

フラグメントシェーダーを書く

<script id="fragment-shader" type="x-shader/x-fragment">
  #version 300 es
  precision mediump float;

  out vec4 fragmentColor;

  void main(void){
    fragmentColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
</script>

赤く塗りつぶすだけのシェーダーを描きます。

三角形を描く

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl2');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// #version 300 esを先頭に書く必要があるためtrimでスペースを削除する
const vertexShaderSource = document.getElementById('vertex-shader').textContent.trim();
const vertexShader = gl.createShader(gl.VERTEX_SHADER);

gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);

// #version 300 esを先頭に書く必要があるためtrimでスペースを削除する
const fragmentShaderSource = document.getElementById('fragment-shader').textContent.trim();
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);

const program = gl.createProgram();

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

// Cancasの中心が(0,0)、左上が(-1, 1)、右下が(1, 1)
const vertices = [
  -.5, .5,
  -.5, -.5,
  .5, -.5
];

const vertexBuffer = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

const vertexPositionLocation = gl.getAttribLocation(program, 'vertexPosition');

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.vertexAttribPointer(vertexPositionLocation, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(vertexPositionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

gl.drawArrays(gl.TRIANGLES, 0, 3);

WebGL2で真っ赤な四角形を描く

DEMO

2つの三角形を組み合わせて四角形を描きます。
いくつか方法があるのですが、今回はdrawElementsを使いました。
頂点シェーダー、フラグメントシェーダーは三角形のものと変わらないので、JavaScriptだけ記載します。

三角形を描く

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl2');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// #version 300 esを先頭に書く必要があるためtrimでスペースを削除する
const vertexShaderSource = document.getElementById('vertex-shader').textContent.trim();
const vertexShader = gl.createShader(gl.VERTEX_SHADER);

gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);

// #version 300 esを先頭に書く必要があるためtrimでスペースを削除する
const fragmentShaderSource = document.getElementById('fragment-shader').textContent.trim();
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);

const program = gl.createProgram();

gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);

// Cancasの中心が(0,0)、左上が(-1, 1)、右下が(1, 1)
const vertices = [
  -.5, .5, // 左上
  -.5, -.5, // 左下
  .5, -.5, // 右下
  .5, .5 // 右上
];

const vertexBuffer = gl.createBuffer();

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

const vertexPositionLocation = gl.getAttribLocation(program, 'vertexPosition');

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(vertexPositionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vertexPositionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

// 頂点を結ぶ順序を描く、左上 → 左下 → 右下・右下 → 右上 → 左上と結ぶ
const indices = [
  0, 1, 2,
  2, 3, 0
];
const indicesBuffer = gl.createBuffer();

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);

gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

WebGL2で描いた四角形にテクスチャを貼る

DEMO

jsfiddle.net

JSFiddleでデモを作る際、テクスチャに使いたい画像がクロスドメインになってしまうため、ウェブカメラの映像をテクスチャにしてみました。
iframeでエディタを埋め込むと、ブログ記事を開いた時点でカメラのパーミッションを求めるダイアログが表示されてしまうので、こちらのDEMOだけは外部リンク にしています。

頂点シェーダーを書く

<script id="vertex-shader" type="x-shader/x-vertex">
  #version 300 es

  in vec2 vertexPosition;
  in vec2 textureCoord;
  out vec2 vTextureCoord;

  void main(void) {
    vTextureCoord = textureCoord;
    gl_Position = vec4(vertexPosition, 0.0, 1.0);
  }
</script>

頂点の座標をテクスチャから取得し、テクスチャをフラグメントシェーダーに渡すようにしておきます。

フラグメントシェーダーを書く

<script id="fragment-shader" type="x-shader/x-fragment">
  #version 300 es
  precision mediump float;

  uniform sampler2D tex;

  in vec2 vTextureCoord;
  out vec4 fragmentColor;

  void main(void){
    fragmentColor = texture(tex, vTextureCoord);
  }
</script>

テクスチャの色で塗りつぶすシェーダーを書きます。

四角形を描いてテクスチャを貼る

(async () => {
  const video = document.querySelector('video');
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });

  video.onplay = () => {
    const canvas = document.querySelector('canvas');
    const gl = canvas.getContext('webgl2');

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    // #version 300 esを先頭に書く必要があるためtrimでスペースを削除する
    const vertexShaderSource = document.getElementById('vertex-shader').textContent.trim();
    const vertexShader = gl.createShader(gl.VERTEX_SHADER);

    gl.shaderSource(vertexShader, vertexShaderSource);
    gl.compileShader(vertexShader);

    // #version 300 esを先頭に書く必要があるためtrimでスペースを削除する
    const fragmentShaderSource = document.getElementById('fragment-shader').textContent.trim();
    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

    gl.shaderSource(fragmentShader, fragmentShaderSource);
    gl.compileShader(fragmentShader);

    const program = gl.createProgram();

    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    gl.useProgram(program);

    const vertices = [
      -.5, .5, // 四角形の左上
      .0, .0, // テクスチャの左上
      -.5, -.5, // 四角形の左下
      .0, 1.0, // テクスチャの左下
      .5, -.5, // 四角形の右下
      1.0, 1.0, // テクスチャの右下
      .5, .5, // 四角形の右上
      1.0, .0 // テクスチャの右上
    ];
    const windowAsspect = window.innerWidth / window.innerHeight;
    const videoAspect = video.videoWidth / video.videoHeight;
    const aspectRatio = videoAspect / windowAsspect;

    // 四角形のアスペクト比をテクスチャに合わせる
    for (let i = 0; i < vertices.length; ++i) {
      if (i % 4 === 0) {
        if (aspectRatio > 1) {
          vertices[i] = vertices[i] * aspectRatio;
        } else {
          vertices[i + 1] = vertices[i + 1] / aspectRatio;
        }
      }
    }
    const vertexBuffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    const indices = [
      0, 1, 2,
      2, 3, 0
    ];
    const indicesBuffer = gl.createBuffer();

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

    const vertexPositionLocation = gl.getAttribLocation(program, 'vertexPosition');
    const textureAttribLocation  = gl.getAttribLocation(program, 'textureCoord');

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.enableVertexAttribArray(vertexPositionLocation);
      gl.enableVertexAttribArray(textureAttribLocation);
      gl.vertexAttribPointer(vertexPositionLocation, 2, gl.FLOAT, false, 4 * Float32Array.BYTES_PER_ELEMENT, 0);
      gl.vertexAttribPointer(textureAttribLocation, 2, gl.FLOAT, false, 4 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);

    const texture = gl.createTexture();

    gl.bindTexture(gl.TEXTURE_2D, texture);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.generateMipmap(gl.TEXTURE_2D);
    gl.bindTexture(gl.TEXTURE_2D, null);

    setInterval(() => { // 60fpsで描画
      gl.bindTexture(gl.TEXTURE_2D, texture);
    	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
          gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
      gl.bindTexture(gl.TEXTURE_2D, null);
    }, 1000 / 60);
  };

  video.srcObject = stream;
})();