2021年12月4日

プログラミング

GLSLを使用した透過動画の実装方法(半透明要素がない場合)

目次

  1. はじめに
  2. 透過動画について
  3. パフォーマンスについて
  4. 実装について
  5. まとめ

はじめに

この記事は新しい技術にチャレンジし続けるpalanのアドベントカレンダーDay4の記事です!

昨日は「AR開発ツール: Spark AR StudioとLens Studio」についての記事でした。

AR開発ツール: Spark AR StudioとLens Studio

今回は下記リンクのデモページのように、
透過動画を実装する方法について紹介していきます!
https://webvr-lab.eishis.com/namecard/transparent-video-with-glsl/1204/index.html

透過動画について

Webでは動画ファイルのアルファ値は考慮されず、
透明になる想定で制作した部分が真っ黒に表示されてしまいます。

これはブラウザの仕様なのでしょうがないんですよね…。

じゃあ、どうすれば良いのかといいますと、
動画とは別に実装が必要となります。

実装内容は下記の2種類あり、本日は前者を紹介し12月18日に後者を紹介します!

・動画ファイル内に半透明要素がない場合
・動画ファイル内に半透明要素がある場合

半透明要素がない場合に準備するものは下記の2点です。

クロマキー動画
クロマキー処理

クロマキー動画

下記のように透明にしたい部分を緑や青などで塗りつぶした動画のことを指します。

今回は緑で塗りつぶした動画を制作しました!

クロマキー処理

先ほどのクロマキー動画の緑をプログラムで取得し透明に差し替える処理のことを指します。

考えうる実装方法は下記の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 vec2 resolution;
uniform sampler2D map;
const vec3 chromaKeyColor = vec3(0.0, 1.0, 0.0);
const float threshold = 0.785;
void main(void) {
vec2 uv = gl_FragCoord.xy / resolution.xy;
vec3 textureColor = texture2D(map, uv).rgb;
float difference = length(chromaKeyColor - textureColor.rgb);
gl_FragColor = difference < threshold ? vec4(0.0, 0.0, 0.0, 0.0) : vec4(textureColor, 1.0);
}

※ 下記からも閲覧可能です。
https://github.com/palan-inc/transparent-video-with-glsl/blob/main/src/ts/script-1204.ts

以上の実装で完成です!

下記リンクのデモページで実装結果を確認することができます!
https://webvr-lab.eishis.com/namecard/transparent-video-with-glsl/1204/index.html

まとめ

本日は前者を紹介しました!
12月18日には後者を紹介するのでお楽しみに!

・動画ファイル内に半透明要素がない場合
・動画ファイル内に半透明要素がある場合

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

AUTHOR

sasai

ささい エンジニア

フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。

アプリでもっと便利に!気になる記事をチェック!

記事のお気に入り登録やランキングが表示される昨日に対応!毎日の情報収集や調べ物にもっと身近なメディアになりました。

簡単に自分で作れるWebAR

「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。

palanARへ
palanar

palanはWebARの開発を
行っています

弊社では企画からサービスの公開終了まで一緒に関わらせていただきます。 企画からシステム開発、3DCG、デザインまで一貫して承ります。

webar_waterpark

palanでは一緒に働く仲間を募集しています

正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。

話を聞いてみたい

運営メンバー

eishis

Eishi Saito 総務

SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。

sasakki デザイナー

アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。

yamakawa

やまかわたかし デザイナー

フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。

Sayaka Osanai デザイナー

Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。

はらた

はらた エンジニア

サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています

kobori

こぼり ともろう エンジニア

サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。

sasai

ささい エンジニア

フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。

damien

Damien

WebAR/VRの企画・開発をやっています。森に住んでいます。

ゲスト bagelee

ゲスト bagelee

かっきー

かっきー

まりな

まりな

suzuki

suzuki

miyagi

ogawa

ogawa

雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。

いわもと

いわもと

デザイナーをしています。 好きな食べ物はラーメンです。

kobari

taishi kobari

フロントエンドの開発を主に担当してます。Blitz.js好きです。

shogokubota

kubota shogo

サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!

nishi tomoya

aihara

aihara

グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。

nagao

SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。

kainuma

Kainuma

サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています

sugimoto

sugimoto

asama

ando

CONTACT PAGE TOP