DMM.comラボエンジニアブログ

DMM.comラボのエンジニアブログです。DMM.comを支える技術について書いています。

DMM insideに引っ越しました。 移転先はこちら -> https://inside.dmm.com/

WebGLを使ってブラウザ上で3Dモデルを描画した話

皆様、はじめまして!DMM.com Labo システム本部 事業サービス開発部の久野です。この度、社内の勉強会のLTで話した内容をまとめてみました!

内容はWebGLというJavaScriptのAPIを使って、ブラウザ上で3Dモデルをアニメーション付きで描画するために何を行ったのか、です。

▼実際に動作するデモです。
テクスチャを大量に読み込まなければならないので表示まで時間がかかるかもしれません。

▲ブラウザによっては動作しない可能性がありますがデモプログラムなので悪しからず。

WebGLとはなんぞや?

WebGLとはKronos Groupが管理するOpenGL ES 2.0をブラウザ上のJavaScriptから扱えるようにしたAPIです。GPU(グラフィックカードまたはグラフィックボード)の機能を駆使して高度な3D描画を高速に行うことが出来、昨今のPC、スマートフォン等の主要なブラウザでこのAPIはサポートされています。今やHTML5の標準APIと言っても過言ではない機能です。

f:id:dmmlabotech:20160624083132p:plain

プラグイン無しにWebGLはブラウザ上で動くこともあり最近ではUnityやAnimation CC(旧 Flash Professional)等のツールがWebGLに対応した形式でのデータ出力に対応しています。

出来ること

WebGLで出来る事としては主にGPUの操作になります。

  • GPUのメモリ(ビデオメモリまたはVRAM)の確保
  • メインメモリからGPU上へデータの転送
  • シェーダプログラム(GPUクラスタ上で動作するプログラム)のコンパイルとリンク
  • 描画のコントロール

出来ないこと

WebGLで出来ない事としては・・・

  • 幾何学を取り扱うための数学的な処理
    (ベクトル、行列、四元数といった数学ユーティリティが無い!)
  • 3Dモデルデータの読み込み

WebGLはGPUへのアクセスを抽象化しただけの簡素なAPIなので3Dの描画を支援するためのユーティリティ等は一切含まれていません。

それは幾何学を取り扱うためのベクトルや行列、四元数といった数学的なユーティリティ(プログラマブルシェーダには用意されていますがJavaScriptからは使用不可)にくわえ、3Dモデルデータを読み込む機能やアニメーションを処理するユーティリティなども当然ありません・・・

そのような理由もありWebGLを取り扱う際は汎用的なライブラリを導入するか自前で実装するしかないのです。

フルスクラッチで3Dモデル描画を実装してみた

f:id:dmmlabotech:20160629135833p:plain

Unityやthree.jsといった3D描画をブラウザ上で比較的に簡単かつお手軽に実装出来るツールやフレームワーク、ライブラリが登場しています。しかし3Dモデルの描画という仕組みを深く知るため、ここはあえてフルスクラッチでアニメーション付き3Dモデルの描画を実装に挑戦しました。

フルスクラッチでの実装を行うにあたり何が必要かと考えた所、大きく分けて3つの実装が必要になるだろうと考えました。

  1. アニメーション付き3Dモデルデータの準備
  2. JavaScriptで3Dモデルデータの読み込み処理の実装
  3. WebGLでの描画処理の実装

アニメーション付き3Dモデルデータの準備

やはり質の高い3Dモデルデータが無いと、たとえ描画プログラムに凝ってもパッとしないものが出来上がる可能性が高いと考え、まずは完成物の見た目を良くするため質の高い3Dモデルデータを用意する事にしました。

しかし、ここで大きな問題が・・・

アニメーション付き3Dモデルの形式をいくつか調査してみて候補として上げたものとしてAutodesk社が策定したFBX形式やKronos Groupが管理するのCOLLADA形式がありました。

しかし、これらの仕様は複雑で解析処理を自前で実装する事が難しく、さらに私が今回の3Dモデルの描画実装で使用しないであろうデータの仕様が多く含まれていました。three.js等を使用すれば簡単に読み込み処理を実装し不要なデータは無視するという実装は出来たでしょうが、どうしてもフルスクラッチで実装してみたかったので、これらの形式の使用は見送ました。

さらにデータに含まれる情報のレベルを落とした形式の候補として、Microsoft Direct X 9系まで採用されていた比較的に構造や仕様が単純なX-Fileと呼ばれる形式を使用しようと思ったのですが、定義されているデータの仕様が単純な割には読み込み側の解析処理が非常に面倒なのでこちらも見送りに・・・

いきなり大きな壁にぶつかってしまいました。

Blenderとの出会い

f:id:dmmlabotech:20160624102848p:plain

大きな壁にぶつかって途方に暮れながら何か良い方法はないかと3DCGソフトの情報を探していたところBlenderという無料でありながらプロフェッショナルでも採用例があるほど多機能な3DCGソフトを見つけました!しかも、これがPythonを使ってプラグインを実装出来るという優れもの!

これを使用してプラグインを組むことにしました!

複雑な3Dのデータを私がプログラム上で読み書きしやすいような形式の3Dモデルのデータとして出力するプラグインを作れば、難解な形式(FBX形式やCOLLADA形式)の読み込み処理を実装しなくて済むので、幾分か楽なのではないかと思い、Blenderを使用してデータ出力するプラグインを作成する事にしました。

3Dモデルデータの用意

私は3Dプログラミングと3Dモデル作成を一人で作成出来るような人では無いので、何処からか3Dモデルデータを調達してくる必要がありました。このプログラムを試しで実装している間は無料でダウンロード出来て個人利用に利用が限られる3Dモデルを適当に選んで使用していたのですがプレゼン資料等の公的な場に持参することを考慮するためにライセンスに問題がない3Dモデルデータが必要になりました。

そこで色々と探してみたところDeNA様が配布しているハッカドールのMMD用モデルデータ、これがCCライセンスであり、ライセンスの内容的に問題がなさそうなので、そちらを使わせて頂く事にしました。

f:id:dmmlabotech:20160624143347p:plain

手順としてはMMD用の3DモデルをプラグインをBlenderに導入してインポートしました。

(;´Д`).oO(DMMのブログだし艦これのモデルを使いたかったけど個人作成のMMDモデルはライセンスが・・・)

BlenderのAPIの学習

Blenderのプラグインは調べた限りではPythonかC/C++で書くことが出来ます。3Dモデルのエクスポート程度であればPythonで十分対応出来そうなので公式のAPIドキュメントを黙々と読み、PythonのAPIを把握していきました。

f:id:dmmlabotech:20160624105801p:plain

Blenderには素晴らしい機能があり、Pythonのスクリプトコンソールが組み込まれていて3DモデルをBlenderに読み込ませた状態で対話的にAPIを叩きモデルデータのステータスを変化させることが出来ます。さらにBlenderはUIやオブジェクトにカーソルを合わせることにより、そのUIやオブジェクトに対して、どのようなコマンドを打てばステータスを操作出来るかをポップアップで表示してくれるのでプログラマにとってはとても親切な設計だと思いました!

f:id:dmmlabotech:20160624105110p:plain

エクスポータの実装

一通りBlenderのAPIについて学び終えたのでBlenderのプラグインの作成ガイドを読み、手順に従ってBlender上で実装されているエディタを使用して3Dモデルをバイナリのデータにエクスポートするためのプラグインを作成していきました。

しかしBlender上で実装されているエディタは、eclipse等のよくあるIDEほど性能は良くありませんでした。デバッガが無いのでPythonのコンソール上で実装した処理を部分的に実行し、正しく動作しているかを埋め込んだログで確認していくようなデバッグの方法しかなく大変で、その上、日本語でコメント書けなかったりとプラグインの開発には苦戦を強いられました。

f:id:dmmlabotech:20160624111231p:plain

そんな苦労を乗り越え、完成したプラグインを使用して3Dモデルのバイナリデータを無事、出力しました。

JavaScriptでデータの読み込みの実装

3Dモデルのバイナリデータは用意できたので次は作成したデータをプラグインで実装したエクスポート処理の逆順で3Dモデルデータを解析しインスタンス化していきます。やはり3Dモデルデータのフォーマットを自分で決めながら実装を作成したので読み込み処理の実装は比較的スムーズに時間をかけずに作成することが出来ました。

読み込み手順としてXMLHttpRequestクラスを使い3Dモデルデータを読み込んでからArrayBufferクラスとDataViewクラスを使用しバイナリレベルでのデータ解析の処理を実装していきました。

この間で一つ苦労したのが文字列の取り扱い方法です。Blenderからデータを出力するときに文字列はUTF-8にエンコードするような処理を実装していたのでUTF-8のバイナリデータをJavaScriptのString型にデコードする処理を実装しました。

/**
 * Decoding string from byte array of the UTF-8 codes.
 *
 * @memberof xpl.StringUtils
 * @function decodeUTF8
 * @param {Array} ary - The byte array of the UTF-8 codes.
 * @returns {String} The decoded string.
 */
ns.StringUtils.decodeUTF8 = function (ary) {
    let str = "";
    for (let i = 0; i < ary.length;) {
        let code = 0xff & ary[i];
        let remain = 0;

        if (code < 0x80) {
            // 7bit.
            remain = 0;
        } else if (code < 0xdf) {
            // 11bit.
            code = (0x1f & code) << 6;
            remain = 1;
        } else if (code < 0xef) {
            // 16bit.
            code = (0xf & code) << 12;
            remain = 2;
        } else if (code < 0xf7) {
            // 21bit.
            code = (0x7 & code) << 18;
            remain = 3;
        } else if (code < 0xfb) {
            // 26bit.
            code = (0x3 & code) << 24;
            remain = 4;
        } else if (code < 0xfd) {
            // 31bit.
            code = (0x1 & code) << 30;
            remain = 5;
        }
        i++;
        while (0 < remain && i < ary.length) {
            let c = ary[i];
            if (c < 0x80) {
                break;
            }
            code |= (0x3f & c) << (6 * --remain);
            i++;
        }
        if (code < 0xd7ff || (0xe000 <= code && code <= 0xffff)) {
            // a code of UTF-16.
            str += String.fromCharCode(code);
        } else {
            // two codes of UTF-16.
            str += String.fromCharCode(
                0xd800 | (((code - 0x10000) >> 10) & 0x3ff),
                0xdc00 | (code & 0x3ff));
        }
    }
    return str;
};

WebGLで描画処理の実装

ここから数学との地獄の戦いです。

冒頭にも書きましたがWebGLには幾何学操作を行うための数学的なユーティリティの実装が皆無なのでフルスクラッチでこれらを実装しなくてはなりません。更にこれらを実装した後は作成した数学ユーティリティを駆使してスキニングやIK(逆運動学)を実装していかなくてはならず技術的な課題が数多くありました。

ベクトル(Vector)、行列(Matrix)、四元数(Quaternion)の実装

WebGLで3D描画を実装する上で必須と思われる三種の神器、ベクトル、行列、四元数を順々に実装していきました。ベクトルと行列は高校等で学習されている方も多いのではないでしょうか?

ベクトル(Vector)とは

ベクトルは複数の値で構成されていて向きと大きさを表すための定義で頂点座標や法線、テクスチャ座標、色といった情報を収納したり空間上のベクトルの長さの算出や2つのベクトルの角度や回転軸の算出等の色々な処理に使用します。

主に実装したもの

  • 内積、外積
  • ノルムの算出、正規化
行列(Matrix)とは

行列は行と列の矩形状に数でを並べた数学上の道具です。今回は3D空間上でのスクリーンの位置や3Dモデルの位置等を操作に使用します。

主に実装したもの

  • 行列同士の掛け算
  • 行列式の算出、逆行列
  • 回転行列、拡大行列、平行移動等の基本的な行列生成
  • ビュー行列、パース行列等の表示に関わる行列生成
四元数(Quaternion)とは

四元数とは、その定義は難しく1つの実数に3つの虚数(Xの2乗が0以下の数)で構成された4次元ベクトルの一種であり、複素数を拡張した数です。行列で処理するには色々と問題のある回転の問題を解くために使用します。

主に実装したもの

  • ノルムの算出、正規化
  • 四元数同士の掛け算
  • 四元数とベクトルの掛け算
  • 球面線形補完
  • 四元数から行列への変換

スキニングの実装

スキニングとはロボットのように明確な関節がなく人間の様な柔らかい皮膚をもち関節の境界が曖昧なモデルの関節の動きによる皮膚の変形を表現するための手法です。

人型のモデルの腕を例に上げます。

肘を起点に、肘から肩の間にある頂点と、肘から手首の間にある頂点があります。肘から肩の間にある頂点は肘から肩に伸びるボーンの回転に影響を大きく受けます。肘から手首の間にある頂点は肘から手首に伸びるボーンの回転に影響を大きく受けます。しかし肘の周りの頂点はどうでしょうか?

その境界はあやふやで、その付近の皮膚は伸びたり縮んだりと、どちらのボーンの位置にも影響される領域があることに気づきます。これを表現するためにスキニングと呼ばれる手法を実装する必要があります。

f:id:dmmlabotech:20160624205840p:plain

実装としては、これらのモデルの頂点がどのボーンにどれだけ影響されて座標を変えるのかといった情報をスキンウェイトという値に従って、元のモデル情報を描画時に変形させて実現します。これは数千、数万と定義されている頂点に対してJavaScriptで実装すると重くなるのでGPUで動作するシェーダプログラムの方で実装してしまいます。

逆運動学(Inverse Kinematics)の実装

ボーンの運動は通常、親ボーンから子ボーンに向かって再帰的に動作が伝達されて可動しますが今回使用しているBlenderを初め殆どの3DCGソフトはIK(Inverse Kinematics : 逆運動学)なるものが実装されていて3Dモデルにアニメーションを実装するにあたり重要な機能となっています。

IKの概要としては通常は親ボーンから子ボーンに向かって再帰的に回転等を伝達させて子ボーンの位置が確定してのですがIKに関しては、この手順が逆で子ボーンが配置されるべき位置が既にあって、それに合わせ子ボーンから親ボーンに向かってボーンの回転等を逆算するようなアルゴリズムとなっています。

例えば、腕のように「肩 ⇒ 肘 ⇒ 手首 ⇒ 手」と動作が伝達されていく動作が通常のボーンの動きなのですがIKに関しては手の位置が既に決まっている状態で「手 ⇒ 手首 ⇒ 肘 ⇒ 肩」と子ボーンから親ボーンへと動作を逆算していくイメージになります。

これは歩きのモーションや自伝車のペダルを漕ぐようなモーションの様な足をのせる位置が決まっているアニメーションの作成に効力を発揮します。なぜなら通常であれば対象のボーンが目標位置に移動するように親ボーンから子ボーンの回転を調整しなくてはなりませんがIKを使用すれば、足の位置さえ決めれば回転を自動的に計算してくれるような機能だからです。

f:id:dmmlabotech:20160627100934p:plain

今回はこのIKのアルゴリズムとしてCCD法(Cyclic-Coordinate-Descent Inverse Kinematics)という方法で実装を試みました。

このCCD法というのは大雑把に説明すると対象のボーンの末端が目標位置に近づくようにボーンを回転し、それを子ボーンから親ボーンに向かって順々に対象のボーンが目標位置に近づくように処理していきます。そして、この処理は1回だけでは結果が収束しないので数十回から多い時は数百回、反復させ結果を収束させていきます。

このCCD法ですが2次元上だと簡単なアルゴリズムに見えますが3次元で実装すると回転軸がZ軸の1つからX軸, Y軸, Z軸の3つに増え考慮すべきことが爆発的に増え急激に難易度が跳ね上がる上に、しっかり最適化しないと60FPS出ているアニメーションを数FPSにまで落とす程のほどの重い処理なので今回の実装の中で最も苦労しました。

描画処理の作成

色々な技術的試練を乗り越えた後に半分力尽きている状態だったので、簡素な影をつけただけの単純なシェーダをとりあえず書き、完成!

f:id:dmmlabotech:20160624151522p:plain

やはりフルスクラッチは挫折しそうになるほど大変でした。要求される知識の広さ、ソースコードの量の多さ、それらの知識の収集と実装は土日の手隙な時間を利用して作ったとはいえ着手から完成まで数ヶ月を要しました・・・

Unityやthree.jsのような簡単に3Dを表示出来るツールやフレームワーク、ライブラリの類は偉大だなと思います。

(;´Д`).oO(あと超汚いソースコードでよければ、GitHubで公開しています。)

最後に

WebGLを使用してフルスクラッチでアニメーション付き3Dモデルを描画した話はいかがだったでしょうか?

WebGLを含め3Dプログラミングはゲーム系でなく業務系のシステムでは採用例が日本では、まだまだ少なくニッチな分野だと言わざる得ません。

しかし、こちらのWebGL 総本山という日本語のページの中では最も情報量の多いWebGL系の情報サイトを見てみると業務系のシステムの採用例が散見され、ゲームを嗜んでいるユーザ以外にも3D関連の技術は身近になりつつあると私は考えています。

将来、3Dを使用した商品プロモーション等のコンテンツやUIにWebGL関連の技術がどんどん採用され今までにないUXをユーザに提供出来るような世の中になれば3Dプログラミングがある程度出来る私やWebGLが好きなエンジニアにとっては面白いかなと思っています。

この記事を読んでWebGLに少しでも興味を持って頂けたのであれば幸いです。

そして最後まで読んで頂いき、ありがとうございました!

WebGLに関連するのお勧め

WebGL 総本山

WebGL 開発支援サイト wgld.org