Skip to content

Instantly share code, notes, and snippets.

@tsukumijima
Created March 17, 2021 16:52
Show Gist options
  • Save tsukumijima/389508c849c84c74bdd6ead3c4c12b18 to your computer and use it in GitHub Desktop.
Save tsukumijima/389508c849c84c74bdd6ead3c4c12b18 to your computer and use it in GitHub Desktop.
WebGL で動画をキャプチャして Canvas に描画する試作品(失敗作)
/**
* 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