Created
March 17, 2021 16:52
-
-
Save tsukumijima/389508c849c84c74bdd6ead3c4c12b18 to your computer and use it in GitHub Desktop.
WebGL で動画をキャプチャして Canvas に描画する試作品(失敗作)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* WebGL で動画をキャプチャして Canvas に描画する試作品(失敗作) | |
* 元々 Safari で CanvasRenderingContext2D.drawImage() で MSE 経由の動画をキャプチャできないバグがあり、 | |
* WebGLRenderingContext.texImage2D() ならできるんじゃないかと試したときのもの | |
* macOS Safari では一応機能するが、キャプチャした瞬間映像が描画されなくなる | |
* (CSS ハックで強制的に repaint(再描画)をかける事でどうにかできなくはないが、30fps は出ない) | |
* iPadOS Safari ではキャプチャ自体が機能せず、CSS ハックも使えないので実質無意味 | |
* 参考: https://webglfundamentals.org/webgl/lessons/ja/webgl-image-processing.html | |
* 参考: https://medium.com/veltra-engineering/webgl-2d-a9c7a7d89fb8 | |
* @param {HTMLVideoElement} video | |
* @return {HTMLCanvasElement} | |
*/ | |
function CaptureVideoWebGL(video) { | |
/** | |
* 以下は強制的に repaint させる CSS ハック( bugfix クラスを video 要素に適用する) | |
* box-shadow が重要で、適用するとなぜか box-shadow をつけた( repaint がかかった)タイミングの | |
* 動画のフレームが静止画のように描画される | |
* 再描画が発生する zoom を animation で永遠にアニメーションさせることで静止画を(一応)動かしている | |
* 参考: https://stackoverflow.com/a/20982320 | |
@keyframes SafariBugfix {from { zoom: 100%; } to { zoom: 99.99999%; }} | |
.bugfix { | |
box-shadow: 0 0px; | |
animation: SafariBugfix infinite 0.0011s; | |
} | |
*/ | |
// WebGL 描画用の隠し Canvas 要素を作成 | |
// Safari ではなぜか createElement() で作成した Canvas が描画されないため | |
if (document.querySelector('#CaptureVideoWebGL') === null) { | |
const canvasElement = document.createElement('canvas'); | |
canvasElement.id = 'CaptureVideoWebGL'; | |
canvasElement.style.display = 'none'; | |
document.body.appendChild(canvasElement); | |
} | |
const canvas = document.querySelector('#CaptureVideoWebGL'); | |
// WebGL コンテキストを生成。 | |
canvas.width = video.videoWidth; | |
canvas.height = video.videoHeight; | |
const gl = canvas.getContext('webgl'); | |
if (!gl) return; | |
// バーテックスシェーダーの設定。 | |
const vertexSource = ` | |
attribute vec2 a_position; | |
attribute vec2 a_texCoord; | |
uniform vec2 u_resolution; | |
varying vec2 v_texCoord; | |
void main() { | |
// convert the rectangle from pixels to 0.0 to 1.0 | |
vec2 zeroToOne = a_position / u_resolution; | |
// convert from 0->1 to 0->2 | |
vec2 zeroToTwo = zeroToOne * 2.0; | |
// convert from 0->2 to -1->+1 (clipspace) | |
vec2 clipSpace = zeroToTwo - 1.0; | |
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); | |
// pass the texCoord to the fragment shader | |
// The GPU will interpolate this value between points. | |
v_texCoord = a_texCoord; | |
} | |
`; | |
const vertexShader = gl.createShader(gl.VERTEX_SHADER); | |
gl.shaderSource(vertexShader, vertexSource); | |
gl.compileShader(vertexShader); | |
gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS); | |
// フラグメントシェーダーの設定。 | |
const fragmentSource = ` | |
precision mediump float; | |
// our texture | |
uniform sampler2D u_image; | |
// the texCoords passed in from the vertex shader. | |
varying vec2 v_texCoord; | |
void main() { | |
gl_FragColor = texture2D(u_image, v_texCoord); | |
} | |
`; | |
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); | |
gl.shaderSource(fragmentShader, fragmentSource); | |
gl.compileShader(fragmentShader); | |
gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS); | |
// GLSL をセットアップ。 | |
const program = gl.createProgram(); | |
gl.attachShader(program, vertexShader); | |
gl.attachShader(program, fragmentShader); | |
gl.linkProgram(program); | |
gl.getProgramParameter(program, gl.LINK_STATUS); | |
gl.useProgram(program); | |
// 頂点データを移動する必要がある場所を検索します。 | |
var positionLocation = gl.getAttribLocation(program, "a_position"); | |
var texcoordLocation = gl.getAttribLocation(program, "a_texCoord"); | |
// 3つの 2D クリップスペースポイントを配置するバッファを作成します。 | |
const positionBuffer = gl.createBuffer(); | |
// それを ARRAY_BUFFER にバインドします( ARRAY_BUFFER = positionBuffer と考えてください)。 | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
// 画像と同じサイズの長方形を設定します。 | |
function setRectangle(gl, x, y, width, height) { | |
var x1 = x; | |
var x2 = x + width; | |
var y1 = y; | |
var y2 = y + height; | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ | |
x1, y1, | |
x2, y1, | |
x1, y2, | |
x1, y2, | |
x2, y1, | |
x2, y2, | |
]), gl.STATIC_DRAW); | |
} | |
setRectangle(gl, 0, 0, canvas.width, canvas.height); | |
// 長方形のテクスチャ座標を提供します。 | |
const texcoordBuffer = gl.createBuffer(); | |
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ | |
0.0, 0.0, | |
1.0, 0.0, | |
0.0, 1.0, | |
0.0, 1.0, | |
1.0, 0.0, | |
1.0, 1.0, | |
]), gl.STATIC_DRAW); | |
// テクスチャを作成します。 | |
const texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
// 任意のサイズの画像をレンダリングできるようにパラメータを設定します。 | |
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
// 画像をテクスチャにアップロードします。 | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video); | |
// lookup uniforms | |
var resolutionLocation = gl.getUniformLocation(program, "u_resolution"); | |
// クリップスペースからピクセルに変換する方法を WebGL に指示します。 | |
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |
// キャンバスをクリア。 | |
gl.clearColor(0, 0, 0, 0); | |
gl.clear(gl.COLOR_BUFFER_BIT); | |
// 私たちのプログラム(シェーダーのペア)を使用するように伝えます。 | |
gl.useProgram(program); | |
// 位置属性をオンにします。 | |
gl.enableVertexAttribArray(positionLocation); | |
// 位置バッファをバインドします。 | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
// positionBuffer(ARRAY_BUFFER)からデータを取得する方法を position 属性に指示します。 | |
var size = 2; // 2 components per iteration | |
var type = gl.FLOAT; // the data is 32bit floats | |
var normalize = false; // don't normalize the data | |
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position | |
var offset = 0; // start at the beginning of the buffer | |
gl.vertexAttribPointer(positionLocation, size, type, normalize, stride, offset); | |
// texcoord 属性をオンにします。 | |
gl.enableVertexAttribArray(texcoordLocation); | |
// texcoordバッファをバインドします。 | |
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); | |
// Tell the texcoord attribute how to get data out of texcoordBuffer (ARRAY_BUFFER) | |
var size = 2; // 2 components per iteration | |
var type = gl.FLOAT; // the data is 32bit floats | |
var normalize = false; // don't normalize the data | |
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position | |
var offset = 0; // start at the beginning of the buffer | |
gl.vertexAttribPointer(texcoordLocation, size, type, normalize, stride, offset); | |
// 解像度を設定します。 | |
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); | |
// 長方形を描きます。 | |
var primitiveType = gl.TRIANGLES; | |
var offset = 0; | |
var count = 6; | |
gl.drawArrays(primitiveType, offset, count); | |
// 使い終わったシェーダー類を削除し、メモリを解放する。 | |
// 参考: https://tech.mobilefactory.jp/entry/2019/12/17/143000 | |
gl.deleteTexture(texture); | |
gl.deleteBuffer(texcoordBuffer); | |
gl.deleteBuffer(positionBuffer); | |
gl.deleteProgram(program); | |
gl.deleteShader(fragmentShader); | |
gl.deleteShader(vertexShader); | |
delete context; | |
return canvas; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment