2021年12月18日
プログラミング
GLSLを使用した透過動画の実装方法(半透明要素がある場合)
はじめに
この記事は新しい技術にチャレンジし続けるpalanのアドベントカレンダーDay18の記事です!
昨日は「ラズパイでオリジナルPCをつくろう!〜メリーゴーランド編〜」についての記事でした。
今回は下記リンクのデモページのように、
半透明要素がある透過動画を実装する方法について紹介していきます!
https://webvr-lab.eishis.com/namecard/transparent-video-with-glsl/1224/index.html
透過動画について
Webでは動画ファイルのアルファ値は考慮されず、
透明になる想定で制作した部分が真っ黒に表示されてしまいます。
これはブラウザの仕様なのでしょうがないんですよね…。
じゃあ、どうすれば良いのかといいますと、
動画とは別に実装が必要となります。
実装内容は下記の2種類あり、12月4日に前者を紹介したので本日は後者を紹介します!
・動画ファイル内に半透明要素がない場合
・動画ファイル内に半透明要素がある場合
半透明要素がある場合に準備するものは下記の2点です。
・色情報と透過情報を分けた動画
・色情報と透過情報を組み合わせる処理
色情報と透過情報を分けた動画
下記のように、上半分に色情報を下半分に透過情報を分けて配置している動画です。
・上半分: くり抜きたい要素そのまま配置
・下半分: くり抜きたい要素を白で塗りつぶして配置し、任意の透過度に設定
くり抜きたい要素にも透過度を設定すると背景の黒色も混ざった色になるので、
必ず上半分のくり抜きたい要素はそのまま配置してください!
また、「canvas自体に透過度設定すれば良いのでは?」という声もありますが、
動画内に複数要素がありそれぞれに異なる透過度を設定したい場合は、
こちら作り方が一番理想の見え方になると思います!
今回は下半分の透過は50%に設定しています!
色情報と透過情報を組み合わせる処理
先ほどの色情報と透過情報を分けた動画の情報を組み合わせて、
半透明の要素を表現する処理のことを指します。
考えうる実装方法は下記の2種類です。
・A: canvas で処理
パフォーマンスは悪く、実装は比較的易しい
・B: GLSL で処理
パフォーマンスが良く、実装は比較的難しい
弊社では透過動画を使ったxRの案件を数多くいただいており、
パフォーマンスを考慮することの方が多いので、
Bについてさらに詳しく解説して行きます!
パフォーマンスについて
先ほど挙げた実装方法2種類のパフォーマンスが良い悪いの違いは何か。
それは、適切な処理領域で実装できているかということです。
適切な処理領域を選択しないと処理落ちが発生し、
コマ落ちのように見えてしまうことがあります。
デバイスの処理領域は下記の2種類あり、それぞれ向き不向きがあります。
・CPU: 重いタスクを順番に処理することに向いている
・GPU: 軽いタスクを並列に処理することに向いている
今回のクロマキー処理(緑をプログラムで取得し透明に差し替える)は、
軽いタスクなのですがピクセル毎に発生し、
とにかく大量なので GPU で行う方が適切ということになります。
実装について
じゃあ、クロマキー処理を GPU で実装したい時は何をすれば良いのか。
そこで登場するのが WebGL と GLSL という技術です。
・WebGL: JavaScript と GPU と繋ぐ役割を持つ API
・GLSL: GPU での処理を記述できる言語
※もっと適切な説明はあるのですが、さらにややこしくなるので簡潔に…笑
詳しいことは doxas さんが運営する wgld.org を参照することをオススメします。
この2つの技術を使って実装をしていきます!
WebGL
難しそうなコードがいっぱい並んでいますね…。
ただ、どんなプロジェクトでも同じようなコードになるくらいのお決まりのコードなので、
覚える必要は全くありません。
export default class WebGLController {
gl: WebGLRenderingContext | null;
constructor() {
this.gl = null;
}
// MEMO: WebGLコンテキストを初期化する関数
initWebGLContext(canvas: HTMLCanvasElement) {
this.gl = canvas.getContext("webgl");
}
// MEMO: WebGLコンテキストを返す関数
getWebGLContext() {
return this.gl;
}
// MEMO: テクスチャを初期化する関数
initTexture() {
const texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
const level = 0;
const internalFormat = this.gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = this.gl.RGBA;
const srcType = this.gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 0, 255]);
this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, true);
this.gl.texImage2D(this.gl.TEXTURE_2D, level, internalFormat, width, height, border, srcFormat, srcType, pixel);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
return texture;
}
// MEMO: テクスチャを更新する関数
updateTexture(texture: WebGLTexture, video: HTMLVideoElement) {
const level = 0;
const internalFormat = this.gl.RGBA;
const srcFormat = this.gl.RGBA;
const srcType = this.gl.UNSIGNED_BYTE;
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
this.gl.texImage2D(this.gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, video);
}
// -----
// シェーダとは...?
// 3Dオブジェクトをディスプレイに描画するためのプログラム
// -----
// MEMO: シェーダを生成する関数
createShader(type: "vertex" | "fragment", source: string) {
let shader;
switch (type) {
case "vertex":
shader = this.gl.createShader(this.gl.VERTEX_SHADER);
break;
case "fragment":
shader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
break;
default:
return;
}
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
return shader;
} else {
alert(this.gl.getShaderInfoLog(shader));
console.log(this.gl.getShaderInfoLog(shader));
}
}
// -----
// プログラムオブジェクトとは...?
// vertexシェーダとfragmentシェーダを紐付けたり
// JavaScriptからGLSLへ変数を渡す役割をもつオブジェクトのこと
// ------
// MEMO: プログラムオブジェクトを生成しシェーダをリンクする関数
createProgram(vertexShader: string, fragmentShader: string) {
const program = this.gl.createProgram();
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
this.gl.useProgram(program);
return program;
} else {
return null;
}
}
// -----
// VBOとは...?
// 頂点データの格納オブジェクトのこと
// ------
// MEMO: VBOを生成する関数
createVbo(vboArray: number[]) {
const vbo = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vbo);
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(vboArray), this.gl.STATIC_DRAW);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null);
return vbo;
}
// -----
// IBOとは...?
// 頂点データの肥大化を抑えるため、
// 頂点データを使い回すインデックスデータの格納オブジェクトのこと
// ------
// MEMO: IBOを生成する関数
createIbo(iboArray: number[]) {
const ibo = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, ibo);
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Int16Array(iboArray), this.gl.STATIC_DRAW);
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, null);
return ibo;
}
}
※ 下記からも閲覧可能です。
https://github.com/palan-inc/transparent-video-with-glsl/blob/main/src/ts/modules/WebGLController.ts
GLSL
ここが一番重要なコードです!
動画の上半分から色情報を取得し、
動画の下半分から透過情報を取得し、
色情報と透過情報を組み合わせて出力しています。
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
uniform sampler2D map;
void main(){
vec2 uv = gl_FragCoord.xy/resolution.xy;
// MEMO: 動画の上半分から色情報を取得
vec2 uvTop = vec2(uv.x, uv.y / 2.0 + 0.5);
vec4 colorTop = texture2D(map, uvTop);
// MEMO: 動画の下半分から透過情報を取得
vec2 uvBottom = vec2(uv.x, uv.y / 2.0);
vec4 alphaBottom = texture2D( map, uvBottom );
// MEMO: 色情報と透過情報を組み合わせて出力
gl_FragColor = vec4( colorTop.r, colorTop.g, colorTop.b, alphaBottom.b);
}
※ 下記からも閲覧可能です。
https://github.com/palan-inc/transparent-video-with-glsl/blob/main/src/ts/script-1224.ts
以上の実装で完成です!
下記リンクのデモページで実装結果を確認することができます!
https://webvr-lab.eishis.com/namecard/transparent-video-with-glsl/1224/index.html
まとめ
本日は後者を紹介しました!
・動画ファイル内に半透明要素がない場合
・動画ファイル内に半透明要素がある場合
bageleeを運営しているpalanでは透過動画を使ったxRの案件を数多くいただいております。
ご興味のある方はぜひお問い合わせください!
参考
https://wgld.org/d/glsl/g001.html
https://wgld.org/d/webgl/w080.html
https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Animating_textures_in_WebGL
JavaScriptのお仕事に関するご相談
Bageleeの運営会社、palanではJavaScriptに関するお仕事のご相談を無料で承っております。
zoomなどのオンラインミーティング、お電話、貴社への訪問、いずれも可能です。
ぜひお気軽にご相談ください。
この記事は
参考になりましたか?
0
0
関連記事
2018年7月6日
【js】jQueryでできるハンバーガーメニューの作成
2018年6月18日
プログレスバーを簡単に実装できるprogressbar.js
2018年5月8日
画像に簡単にアニメーションをつけるcurtain.js
2018年2月14日
【js】jQueryでできるアコーディオンメニューの作成
2017年9月4日
【JS】スクロールした時にCSSanimationを発動させる「scrollMonitor」
簡単に自分で作れるWebAR
「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。
palanARへpalanでは一緒に働く仲間を募集しています
正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。
運営メンバー
Eishi Saito 総務
SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。
sasakki デザイナー
アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。
やまかわたかし デザイナー
フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。
Sayaka Osanai デザイナー
Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。
はらた エンジニア
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
こぼり ともろう エンジニア
サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。
ささい エンジニア
フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。
Damien
WebAR/VRの企画・開発をやっています。森に住んでいます。
ゲスト bagelee
かっきー
まりな
suzuki
miyagi
ogawa
雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。
いわもと
デザイナーをしています。 好きな食べ物はラーメンです。
taishi kobari
フロントエンドの開発を主に担当してます。Blitz.js好きです。
kubota shogo
サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!
nishi tomoya
aihara
グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。
nagao
SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。
Kainuma
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
sugimoto
asama
ando
iwasawa ayane
oshimo
異業界からやってきたデザイナー。 palanARのUIをメインに担当してます。 これからたくさん吸収していきます!