ブラザーSDGsストーリー リニューアル WebGL舞台裏
こんにちは、AQUARING かに です。
今回は今年2月にサイトリニューアルを担当した Brother様 の「ブラザーSDGsストーリー」サイトの WebGL の舞台裏をご紹介します。
リニューアルの経緯
リニューアル前のサイトでも同じキャラクターを用いた表現をしていましたが、旧サイトではアニメーション付きの glTF モデルを WebGL でリアルタイムで再生する形で実装していました。
幅広いデバイスでのキャラクターのクオリティ担保やローディング軽量化などを考慮した結果、今回のリニューアルでは一部仕様を変更してトップページのキャラクターをプリレンダリング動画に変更しました。
といっても video タグだけでは動画の背景透過ができないため、素材側を工夫して WebGL で描画することで実現しています。
今回はその動画素材側の工夫について紹介します。
3DCG のキャラクター動画は前回に引き続き maremonさん(ソアズロックさん)に制作していただきました。ありがとうございました!
Node.jsで動画素材の画像処理を自動化
まず、WebGLで動画素材をアルファマスクさせるために RGB チャンネルとアルファチャンネルを左右に連結させた動画素材を maremon さんに制作していただきました。
編集前素材
左:RGB
右:アルファ
の動画素材です。
この動画を GLSL 内で左右分割し、右半分をアルファチャンネルとして出力することで動画のアルファマスクができます。
このままだと表示した際にキャラクターの背中側に落ちる影がなく不自然に見えるため、素材側にドロップシャドウ用のチャンネルも追加します。
動画素材側にドロップシャドウを追加したいのですが、CG制作側にそこまでお任せしてしまうとシャドウの半径などの調整でリテイクが何度も発生しそうなことが予測できたため、AQUARING側でコントロールしやすいように元動画からシャドウ付き動画を自動生成する仕組みをNode.js環境で構築しました。
前提知識
動画から連番画像の エンコード/デコードには FFmpeg、
画像の分割/連結やドロップシャドウ生成には ImageMagick、
Node.jsからのコマンド実行には child_process.exec() を Promise を返すようにした execAsync() を自作して await できるようにしました。
const { exec } = require('child_process');
const execAsync = (command) => {
return new Promise((resolve, reject) => {
exec(command, (err, stdout, stderr) => {
if (err) return reject(err, stderr);
resolve(stdout);
});
});
};
手順
① 動画(mp4) から 連番画像(png)に変換
// ffmpeg -i ./src/XXX.mp4 -vcodec png ./src/XXX_%03d.png
//
// ./src/XXX.mp4 (Input:mp4)
// -> ./src/XXX_000.png (Output:png)
// -> ./src/XXX_001.png (Output:png)
// -> ./src/XXX_002.png (Output:png) ...
const videoToSequence = async () => {
const command = `ffmpeg -i ./src/${ params.MOVIE_FILENAME }.mp4 -vcodec png ./src/${ params.MOVIE_FILENAME }_%03d.png`;
await execAsync(command).catch(err => { throw err; });
};
※ jpeg だと劣化が重なってしまうため png に変換しています。
② 左画像(RGB)と右画像(A)に分割
Node.js からの ImageMagick の実行には npm パッケージの imagemagick を使用しています。
const imagemagick = require('imagemagick');
const convertAsync = (args) => {
return new Promise((resolve, reject) => {
imagemagick.convert(args, (err, stdout) => {
if (err) return reject(err);
resolve(stdout);
});
});
};
こちらも実行時に await できるように Promise を返す convertAsync() を自作しました。
convert -crop 50%x100% で入力画像を左右に分割します。
// convert -crop 50%x100% ./src/XXX_000.png ./tmp/XXX_000-%01d.png
//
// ./src/XXX_000.png (Input:RGB+Alpha)
// -> ./tmp/XXX_000-0.png (Output:RGB)
// -> ./tmp/XXX_000-1.png (Output:Alpha)
await convertAsync([
'-crop',
'50%x100%',
`./src/${ basename }.png`,
`./tmp/${ basename }-%01d.png`,
]).catch(err => { throw err; });
③ ドロップシャドウ画像を生成
アルファチャネル画像にブラーを適用した画像を保存しておきます。
// convert ./tmp/XXX_000-1.png -blur 0x30 ./tmp/XXX_000-2.png
//
// ./tmp/XXX_000-1.png (Input:Alpha)
// -> ./tmp/XXX_000-2.png (Output:DropShadow)
await convertAsync([
`./tmp/${ basename }-1.png`,
'-blur',
`0x${ params.shadowBlurRadius }`,
`./tmp/${ basename }-2.png`,
]).catch(err => { throw err; });
④ RGB + Alpha + Shadow の3枚をヨコ並びで結合
convert +append で画像をヨコ方向に連結します。
// convert +append ./tmp/XXX_000-0.png ./tmp/XXX_000-1.png ./tmp/XXX_000-2.png ./dist/XXX_000.png
//
// ./tmp/XXX_000-0.png (Input:RGB)
// ./tmp/XXX_000-1.png (Input:Alpha)
// ./tmp/XXX_000-2.png (Input:DropShadow)
// -> ./dist/XXX_000.png (Output:RGB+Alpha+DropShadow)
await convertAsync([
'+append',
`./tmp/${ basename }-0.png`,// RGB
`./tmp/${ basename }-1.png`,// Alpha
`./tmp/${ basename }-2.png`,// Shadow
`./dist/${ basename }.png`
]).catch(err => { throw err; });
⑤ 編集後のフレーム画像から動画(mp4)にエンコード
// ffmpeg -y -r 30 -i ./dist/XXX_%03d.png -vcodec libx264 -pix_fmt yuv420p -r 29.97 ./dist_video/XXX-shadow.mp4
//
// ./dist/XXX_000.png (Input:png)
// ./dist/XXX_001.png (Input:png)
// ./dist/XXX_002.png (Input:png) ...
// -> ./dist_video/XXX-shadow.mp4 (Output:mp4)
const sequenceToVideo = async () => {
const command = `ffmpeg -y -r 30 -i ./dist/${ params.MOVIE_FILENAME }_%03d.png -vcodec libx264 -pix_fmt yuv420p -r 29.97 ./dist_video/${ params.MOVIE_FILENAME }-shadow${ params.shadowBlurRadius }.mp4`;
const stdout = await execAsync(command).catch(err => { throw err; });
};
今回はこの一連のタスクのスクリプト実行を npm scripts に割り当てて、
$ yarn convert-video "filename"
で発火できるようにしました。
編集後素材
元動画の右側にドロップシャドウ用のチャンネルを追加した動画素材です。
左:RGB
中:アルファ
右:ドロップシャドウ
この動画素材をWebGLのテクスチャとして読み込み、GLSLでUVをX方向に3分割して各チャンネルを取得しコンポジットすることでドロップシャドウ付きのアルファマスクが表示できます。
ドロップシャドウの色/透明度/オフセットの値はJavaScriptからGLSLにuniform変数として渡すことで柔軟に調整できるようにしました。
p5.jsを使ったWebGLプリレンダリングツール
上記のドロップシャドウつき動画素材をWebGL(GLSL)でコンポジットすることでキャラクターを表示しているトップページのほかに、各カテゴリトップページ下部の他カテゴリへの導線部でもキャラクター動画を使用しています。
ですが、このブロックではキャラクターの背景がトップページのように動的でなくカテゴリカラーのベタ塗りのため、背景色・影・アルファマスクしたキャラクターをすべて合成した状態の動画素材をvideoタグで再生しています。
実はこの動画も自動生成ツールを作成して書き出しています。
この動画を作成している時点ではすでにトップページのWebGLコンポジット用に書かれたシェーダーがあったので、それをうまく流用してここでは p5.js を使ったWebGLプリレンダリングツールを作成しました。
(普段 p5.js を DailyCoding で使っていますが、ツール制作にも役立つなんて素敵…!)
前半で紹介したシャドウ付き動画生成の過程で出力される、RGB + アルファ + シャドウ の連番 png をディレクトリに格納して html を開くと、GLSLで背景色・シャドウ・アルファマスクをコンポジットして全フレーム分を自動でダウンロードします。
全フレームダウンロード後にコンソールに ffmpeg コマンドが出力されるようにしたため、コマンドをコピペして実行するだけですぐに mp4 にエンコードできるようになっています。(エンコードまでブラウザ側に任せても良いのですが、コントロールしやすさのため連番書き出しにしました。)
元素材ではアニメーションの関係でキャラクターが画角の中心にいないため、デバッグモードでガイドの表示と矢印キーでのキャラクターの位置修正ができるようにもしました。
WebGL総本山に取り上げていただきました
同時期に公開したブラザーブランドストーリーの解説がメインの記事になっていますが、SDGsストーリーのキャラクター部分をプリレンダリング動画で実装している意図を完全に言い当てられていてさすがです!
さいごに
AQUARINGでは素材制作や編集をデザイナーが担うことが多いのですが、今回のように単純な素材編集であればエンジニア側で巻き取ってシステム構築をして自動化することで効率よくプロジェクトを回せますし、素材編集時の人為的ミスの削減にもつながるのでいいことだらけですね。
この内容を社内のフロントエンドユニットで共有したところ、FFmpeg やImageMagick の存在を知らなかったというメンバーもいたので、これからもっと職種を越えた仕事の仕方ができるエンジニアが増えていくといいなとおもいます。
この記事が気に入ったらサポートをしてみませんか?