Three.jsやPIXI.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でデモを作る際、テクスチャに使いたい画像がクロスドメインになってしまうため、ウェブカメラの映像をテクスチャにしてみました。
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; })();