JavaScript でリアルタイム 3DCG を実現する WebGL の使い方
先日、ちょっとした思いつきで WebKit の Nightly Build をインストールし、 WebGL を試してみました。 WebGL というのは現在策定中の新しい規格で、 JavaScript を使って本格的な 3DCG を実現する API です。同じ目的を持つものとして Google の O3D がありますが、 WebGL は OpenGL ES を管理している Khronos グループを中心に Google, Mozilla, Opera, NVIDIA, AMD といった企業が参画しており、標準化という面ではリードしています。
まだ策定中の規格なので今後変化するかもしれませんが(WebGL 1.0 が正式リリースされました)、少なくとも現状の WebKit の実装については使い方がわかったので、本日はそれをご紹介します。 WebGL は Web 上の最も重要なグラフィックス API になる可能性が高いので、ぜひ今のうちにおさえておきましょう!
WebGL が利用できる環境を整える
Google Chrome 9 以降が WebGL を標準サポートしています。このページのサンプルも動作しますので、そちらをご利用ください。
WebGL はまだ策定中の規格ですので、利用するには開発版のブラウザをインストールする必要があります。ここでは、最も手軽に試せる Chromium を使うことにします(以前は Webkit を使っていましたが、 Chromium のほうが Windows/Linux もサポートしているので、書き換えました)。
Chromium をインストールするには、まずダウンロードページ(Windows 版、 Mac OS X 版、 Linux 版)から ZIP 形式のファイルをダウンロードし、適当な場所に展開してください(Windows 版はインストーラもあります)。そして、以下のようにコマンドラインオプションを付けて起動してください。
chromium --no-sandbox --enable-webgl
これで WebGL が利用できるようになります。以下のページを表示して、回転する地球と火星が表示されれば成功です。
https://cvs.khronos.org/svn/repos/registry/trunk/p...
WebGL を利用するには OpenGL 2.0 に対応した GPU が必要です。ノート PC などで採用の多い Intel 製の統合チップセットでは残念ながら動かないようなので、ご了承ください。現在は Intel 統合チップセットの Mac mini でも動作するようになっています。
HTML ファイルを用意する
環境が用意できたところで、実際に 3D シーンのレンダリングを行うページを作ります。ここでは、 「iPhone で 3D CG! OpenGL ES を使ってみよう」のときと同様に、地球を回すことにします(笑)。
WebGL に対応した WebKit なら、以下のページで実際に動作させることができます。
http://webos-goodies.googlecode.com/svn/trunk/blog...
上記の HTML も含めて、この記事で作成するファイルは以下の場所で公開しています。基本的に自由に使っていただいて構いませんが、行列演算ライブラリの CanvasMatrix.js は WebKit のソースツリーから持ってきたもので、 WebKit と同じライセンスが適用されるのでご注意ください。
http://webos-goodies.googlecode.com/svn/trunk/blog...
それでは、まずベースとなる HTML ファイルを作成しましょう。後ほどシェーダーコード用の SCRIPT タグを追加しますが、それを除くと概ね以下のようになります。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript"></script> <script src="CanvasMatrix.js" type="text/javascript"></script> <script src="webgl_sample.js" type="text/javascript"></script> </head> <body> <canvas id="screen" style="width:500px; height:500px;" width="500px" height="500px"></canvas> </body> </html>
WebGL は Canvas API の拡張として定義されているので、レンダリング対象となる CANVAS 要素を BODY タグ内に含めています。それ以外は、なんの変哲もない HTML ファイルです。
読み込んでいる JavaScript ファイルは以下のとおりです。
- jquery.min.js
- 言わずと知れた jQuary です。 Google AJAX Libraries API から読み込んでいます。
- CanvasMatrix.js
- WebKit のソースに含まれている行列演算ライブラリです。行列演算をスクラッチで書くのは面倒なので拝借しました。
- webgl_sample.js
- 以降で作っていく JavaScript コードです。
CanvasMatrix.js, webgl_sample.js は上記の HTML ファイルと同じディレクトリに置いてください。
WebGL の初期化
HTML ファイルが用意できたら、 webgl_sample.js の中身を書いていきます。まずは初期化まわりから見ていきましょう。
// グローバル変数 var gl = null; var vbuffers = null; var ibuffer = null; var texture = null; var program = null; var uniformVars = null; var count = 0; $(function() { // WebGL コンテキストの取得 var canvas = $("#screen").get(0); $.each(["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"], function(i, name) { try { gl = canvas.getContext(name); } catch(e) {} return !gl }); if(!gl) { alert("WebGL がサポートされていません。"); return; } // リソースの初期化 initVertices(); initIndices(); initTexture(); initShaders(); // 描画処理を毎秒 30 回呼び出す setInterval(redrawScene, 1000/30); });
ページの読み込みが終了した後、 CANVAS 要素の getContext メソッドで WebGL のコンテキストを取得します。通常の 2D canvas の場合は getContext への引数として "2d" を渡しますが、 WebGL では "webgl" にします。ただし、現在は規格がリリースされていないので、 "experimental-webgl" が使われています。その後の2つは古い Webkit や Firefox のためのものです。
コンテキストが取得できたら、レンダリングに必要なリソースを初期化して、最後に setInterval で定期的にレンダリングコードを呼び出すように設定し、初期化は終了です。これらの処理の詳細は以降で作成していきます。
表示するモデルデータの準備
WebGL の初期化が終わったので、次は表示するモデルデータを作成します。今回は地球を表示するので、球体のモデルデータをプログラムで生成することにしましょう。
リアルタイム 3DCG において、モデルデータは頂点データ(頂点の座標・法線・ UV など)と、それらを繋いだ三角形リスト(インデックスデータ)で構成されます(ほかにもバリエーションがありますが、ここでは省略)。 WebGL では、頂点データを VBO (Vertex Buffer Object) 、インデックスデータを IBO (Index Buffer Object) と呼ばれるオブジェクトに格納します。
「iPhone で 3D CG! OpenGL ES を使ってみよう」では、ひとつの VBO に座標・法線・ UV のすべてを格納しましたが、 C/C++ でいう構造体がない JavaScript では、この方法はとりづらいです。そこで、ここでは座標・法線・ UV を別々の VBO に格納する方法をとっています。
と、能書きはこれくらいにして、実際のコードを見ていきましょう。
頂点データの作成
頂点データを作成する initVertices 関数は以下のようになります。
function initVertices() { // 頂点データを生成 var positions = [], uvs = []; for(var i = 0 ; i <= 8 ; ++i) { var v = i / 8.0; var y = Math.cos(Math.PI * v), r = Math.sin(Math.PI * v); for(var j = 0 ; j <= 16 ; ++j) { var u = j / 16.0; positions = positions.concat( Math.cos(2 * Math.PI * u) * r, y, Math.sin(2 * Math.PI * u) * r); uvs = uvs.concat(u, v); } } // VBOを作成し、データを転送 vbuffers = $.map([positions, positions, uvs], function(data, i) { var vbuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); return vbuffer; }); gl.bindBuffer(gl.ARRAY_BUFFER, null); }
座標・ UV それぞれの配列を作成し(単位球なので、法線は座標をコピーしています)、そこにデータを格納した後、一気に VBO に転送しています。
VBO の作成・初期化処理では、まず createBuffer で VBO を作成した後、 bindBuffer でそれをコンテキストに結びつけて、 bufferData でデータを転送しています。このように、 WebGL では対象リソースをまずコンテキストに bind し、その後適切なメソッドを実行してデータを操作したりレンダリングに使用したりします。
インデックスデータの作成
次はインデックスデータの作成です。インデックスデータは、三角形の三頂点それぞれに対応する頂点のインデックスを、レンダリングする三角形の数だけ並べたものになります。
function initIndices() { // インデックスデータを生成 var indices = []; for(var j = 0 ; j < 8 ; ++j) { var base = j * 17; for(var i = 0 ; i < 16 ; ++i) { indices = indices.concat( base + i, base + i + 1, base + i + 17, base + i + 17, base + i + 1, base + i + 1 + 17); } } // IBOを作成し、データを転送 ibuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(indices), gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); // インデックスの数を保存しておく numIndices = indices.length; }
IBO の作成とデータの転送は頂点のときとほぼ同じです。レンダリング時にインデックスの数が必要になるので、グローバル変数に記憶しておきます。
テクスチャの読み込み
モデルデータに貼るテクスチャは、 Image オブジェクトに読み込んだものをテクスチャーオブジェクトに転送して利用します。したがって、 JPEG, PNG, GIF など、ブラウザで表示できる画像ファイルならなんでも表示できます。
function initTexture() { // テクスチャーオブジェクトを作成 texture = gl.createTexture(); // 画像の読み込み完了時の処理 var image = new Image(); image.onload = function() { gl.enable(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.generateMipmap(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, null); } // 画像の読み込みを開始 image.src = "earth.jpg"; }
createTexture でテクスチャーを生成した後、 Image オブジェクトを使って画像を読み込んでいます。
画像の読み込みが完了したら、テクスチャーオブジェクトをコンテキストに bind し、画像データを転送します。 generateMipmap は、テクスチャが縮小されたときの画質・レンダリング速度改善のための「ミップマップ」と呼ばれる縮小画像を生成するメソッドです。
シェーダーの作成
シェーダーというのは、 GPU 上で動作する一種のプログラムで、頂点シェーダーとフラグメントシェーダーの二種類があります。前者はひとつひとつの頂点に対して実行され、頂点座標やその他の頂点属性の計算・変換を行います。後者は三角形内の個々のピクセルに対して実行され、テクスチャや頂点カラーなどの合成を行います。
WebGL は OpenGL ES 2.0 ベースなので、必ずこのシェーダーを用意しなければなりません。そのための処理は以下のようになります。
function initShaders() { // 頂点シェーダーを作成 var vshader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vshader, $('#vshader').text()); gl.compileShader(vshader); if(!gl.getShaderParameter(vshader, gl.COMPILE_STATUS)) alert(gl.getShaderInfoLog(vshader)); // フラグメントシェーダーを作成 var fshader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fshader, $('#fshader').text()); gl.compileShader(fshader); if(!gl.getShaderParameter(fshader, gl.COMPILE_STATUS)) alert(gl.getShaderInfoLog(fshader)); // プログラムオブジェクトを作成 program = gl.createProgram(); gl.attachShader(program, vshader); gl.attachShader(program, fshader); // シェーダー内の変数を頂点属性に結びつける $.each(["position", "normal", "uv"], function(i, name) { gl.bindAttribLocation(program, i, name); }); // 頂点シェーダーとフラグメントシェーダーをリンクする gl.linkProgram(program); if(!gl.getProgramParameter(program, gl.LINK_STATUS)) alert(gl.getProgramInfoLog(program)); // シェーダーパラメータのインデックスを取得・保存 uniformVars = $.map(["mvpMatrix", "normalMatrix", "lightVec"], function(name) { return gl.getUniformLocation(program, name); }); }
ちょっと長いので、ブロックごとに説明しますね。
頂点シェーダー・フラグメントシェーダーを作成
シェーダーを作成する処理は頂点シェーダー・フラグメントシェーダーともにほとんど同じなので、一緒に説明します。手順としては、まず createShader でシェーダーを作成するのですが、このとき与える引数によって頂点シェーダーかフラグメントシェーダーかが決まります。その後 shaderSource でシェーダーのソースコードを設定し、 compileShader を実行してそれをコンパイルします。
ここではシェーダーのソースコードは HTML 内に SCRIPT タグを使って埋め込みます(後述)。もっとも、実際に設定するのは単なるテキストなので、 XHR などで別ファイルを読み込んで使うことも可能です。
シェーダーのコンパイルでエラーが起こった場合は、 "gl.getShaderi(fshader, gl.COMPILE_STATUS)" が偽になります。 getShaderInfoLog でエラーメッセージが取得できるので、なんらかの方法で出力するようにしておきましょう。
プログラムオブジェクトを作成
頂点・フラグメントシェーダーのペアを保持するプログラムオブジェクトを作成します。 createProgram を使ってプログラムオブジェクトを作成し、そこに attachShader で頂点シェーダーとフラグメントシェーダーを設定します。
こうすることで、頂点シェーダーからフラグメントシェーダーへのパラメータの受け渡しなどを適切に処理することができます。
シェーダー内の変数を頂点属性に結びつける
頂点シェーダーで頂点データを読み込むために、頂点シェーダー内の変数を頂点属性(VBO から値を読み込むための、番号で識別される器)に結びつけます。 bindAttribLocation の第二引数には頂点属性の番号、第三引数に頂点シェーダー内の変数名を指定して呼び出すことで、これを実行します。
今回の例では、頂点シェーダー内の "position" 変数に頂点属性 0 、 "normal" 変数に頂点属性 1 、 "uv" 変数に頂点属性 2 の値がそれぞれ設定されることになります。
頂点シェーダーとフラグメントシェーダーをリンクする
頂点シェーダーの出力変数の内容がフラグメントシェーダーの入力変数に適切に引き渡されるように、双方をリンクします。 linkProgram を呼び出すだけで実行されますが、シェーダーのコンパイルと同様にエラーを表示するようにした方がよいでしょう。
これによって、シェーダー内で "varying" と宣言された変数の内容が頂点シェーダーからフラグメントシェーダーへ引き継がれるようになります(実際にはそのままの値ではなく、重心座標で補間した値が引き渡されます)。
シェーダーパラメータのインデックスを取得・保存
最後に、 JavaScript からシェーダーに引き渡すパラメータについて、その設定に使用するインデックス値を取得します。
getUniformLocation の第二引数に JavaScript から設定する変数名を指定すれば、そのインデックスが変えるので、その値をグローバル関数に保存しておきます。レンダリング時には、このインデックスを指定してそれぞれの変数に値を設定するわけです。
シェーダーのソースコードを用意する
前述のとおり、シェーダーのソースコードは SCRIPT タグを使って HTML ファイルに埋め込みます。具体的には以下のようなコードを HTML の BODY タグ内に記述します。
<script id="vshader" type="x-shader/x-vertex"> #ifdef GL_ES precision highp float; #endif uniform mat4 mvpMatrix; uniform mat4 normalMatrix; uniform vec4 lightVec; attribute vec3 position; attribute vec3 normal; attribute vec2 uv; varying vec4 color; varying vec2 texCoord; void main() { vec3 n = (normalMatrix * vec4(normal, 0.0)).xyz; float light = clamp(dot(n, lightVec.xyz), 0.0, 1.0) * 0.8 + 0.2; color = vec4(light, light, light, 1.0); texCoord = uv; gl_Position = mvpMatrix * vec4(position, 1.0); } </script> <script id="fshader" type="x-shader/x-fragment"> #ifdef GL_ES precision highp float; #endif uniform sampler2D texture; varying vec4 color; varying vec2 texCoord; void main() { gl_FragColor = texture2D(texture, texCoord) * color; } </script>
SCRIPT タグの type 属性を "x-shader/x-vertex" などとしているのは、 JavaScript として解釈されないようにするためです。 "text/javascript" 以外であれば他の文字列でもかまわないと思いますが、通例としてこのようにすることになっているようです。
シェーダーの処理内容については説明するときりがないのでここでは触れません。シェーダー言語の詳細については後述の参考文献などを参照してください。ただ、変数定義の先頭にある "uniform", "attribute", "varying" といった宣言の意味を少し説明しておきます。
- uniform
- JavaScript から設定される変数です。 uniform として宣言された変数は、シェーダー内では読み込み専用の定数として扱われます(日本語おかしいな ^^;)。
- attribute
- 頂点属性から読み込んだ値が設定される変数です。シェーダー内では読み込み専用の定数として扱われます。
- varying
- 頂点シェーダーからフラグメントシェーダーに引き渡される変数です。頂点シェーダーで varying と定義された変数の値は、フラグメントシェーダーの同名の変数に自動的に設定されます(前述のとおり、重心座標による補間がかかります)。
これらの宣言を使うことで、 JavaScript や VBO からシェーダーに、もしくは頂点シェーダーからフラグメントシェーダーにデータを引き渡すことができるわけです。
画面を描画する
以上でシーンをレンダリングする準備ができました。あとは、以下の redrawScene 関数を定期的に呼び出せば地球が回転するアニメーションが表示されます。
function redrawScene() { // フレームカウントをインクリメント count += 1; // 画面をクリア gl.clearColor(0, 0, 0, 1); gl.clearDepth(1000); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // デプステストを有効にし、シェーダーを指定 gl.enable(gl.DEPTH_TEST); gl.useProgram(program); // シェーダーに渡すパラメータを計算し、設定 var lightVec = [0.5773502691896258, 0.5773502691896258, 0.5773502691896258, 0.0]; var modelMatrix = new CanvasMatrix4(); modelMatrix.rotate(count, 0, 1, 0); var mvpMatrix = new CanvasMatrix4(modelMatrix); mvpMatrix.translate(0, 0, -6); mvpMatrix.perspective(30, 500.0 / 500.0, 0.1, 1000); var normalMatrix = new CanvasMatrix4(modelMatrix); normalMatrix.invert(); normalMatrix.transpose(); $.each([mvpMatrix, normalMatrix, lightVec], function(i, value) { if(value instanceof CanvasMatrix4) gl.uniformMatrix4fv(uniformVars[i], false, value.getAsWebGLFloatArray()); else gl.uniform4fv(uniformVars[i], new Float32Array(value)); }); // VBOを頂点属性に割り当てる $.each([3, 3, 2], function(i, stride) { gl.enableVertexAttribArray(i); gl.bindBuffer(gl.ARRAY_BUFFER, vbuffers[i]); gl.vertexAttribPointer(i, stride, gl.FLOAT, false, 0, 0); }); // IBOを指定 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuffer); // テクスチャを指定 gl.enable(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); 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.drawElements(gl.TRIANGLES, numIndices, gl.UNSIGNED_SHORT, 0); // ページに反映させる gl.flush(); }
こちらの処理もブロックごとに説明します。
シェーダーに渡すパラメータを計算し、設定
最初の方は画面クリアや各種設定の初期化をしているだけなのでスキップして、シェーダーにパラメータを渡す部分を見ていきましょう。シェーダーで uniform と宣言された変数の値を設定する処理です。
最初の方は CanvasMatrix.js の行列クラスを利用して各種変換行列を算出しています。この行列クラスの使い方は CanvasMatrix.js の先頭にコメントがあるので、そちらを参照してください。
ポイントは、最後の uniformMatrix4fv や uniform4fv を呼び出している部分です。前者は行列型、後者は四次ベクトル型の変数に値を設定するメソッドです。これ以外にも変数型ごとにメソッドが用意されているので、詳細は参考文献を参照してください。
シェーダー作成時に getUniformLocation で取得したインデックスとともにこれらのメソッドを呼び出すことで、シェーダーにデータを引き渡すことができます。回転行列やライトの位置など、モデル全体をとおして変化しない値は、この方法を使って設定するとよいでしょう。
VBOを頂点属性に割り当てる
座標、法線、 UV のそれぞれの VBO を、対応する頂点属性に割り当てることで、頂点シェーダーが適切な値を読み込めるようにします。具体的には、まず enableVertexAttribArray で該当する頂点属性を有効にし、(VBO を bind した後に) vertexAttribPointer で VBO を頂点属性に割り当てます。
vertexAttribPointer の第一引数は頂点属性の番号、第二引数はひとつの頂点の要素数、第三引数は要素のデータ型です。それ以外の引数はたいてい変更することはありません。詳細はリファレンスを参照してください。
同様に、 IBO も bind して描画で使用されるようにしておきます。
テクスチャを設定
モデルに貼付けるテクスチャを有効にします。 enable でテクスチャを有効にし、 bindTexture でテクスチャをバインドすれば、それだけでテクスチャが貼られるようになります。
ただ、デフォルトでは画質などで問題があるので、 texParameteri でパラメータを調整します。 TEXTURE_(MAG|MIN)_FILTER はテクスチャの補間の設定で、サンプルではトライリニアフィルタリングを使用するように設定しています。 TEXTURE_WRAP_[ST] は UV 座標が 0 〜 1 の範囲を出たときの処理の指定で、サンプルではエッジの色を引き延ばすように設定しています。
描画
これで(やっと)描画の準備が整ったので、 drawElements でモデルを描画します。引数は先頭からプリミティブタイプ、描画するインデックスの数、インデックスのデータ型、描画を開始するインデックスのオフセットです。
ただし、 drawElements による描画はオフスクリーン・バッファに対して行われるので、それだけでは画面に反映されません。逆に言うと、シーン中のすべてのモデルを描画するまで画面には反映されないので、これによって描画のちらつきなどを押さえているわけです。
シーン中のすべてのモデルを描画したら、 flush を呼び出すことで実際のブラウザ画面に反映されます。
WebGL の将来性
以上で WebGL の基本的な使い方をご紹介しました。基本的に C/C++ 用の API をそのままポートしているので、いろいろと面倒な手続きが多くて面食らった方もおられるかもしれません。
果たしてこんな面倒な API が普及するのか、疑問に思う方も少なくないと思いますが、私はこれはとても重要な API になると考えています。確かに Web 上で 3DCG を扱う場面はそう多くありませんが、 WebGL はなにも 3DCG のためだけのものではありません。 2D のグラフィックス API として考えても、 WebGL は非常に大きな可能性を秘めているのです。
とくに重要なのが、レンダリング処理の多くをカスタマイズできる頂点・フラグメントシェーダーです。 Canvas や SVG といった API ではプリセットされた効果しか適用できないのに対して、 WebGL ならアプリケーションの必要に応じて自由にフィルターなどを設計することができます。また、 VBO にデータを格納してメソッドひとつで多数の図形を描画できるので、 JavaScript のオーバーヘッドを回避して複雑な映像表現が可能です。
こうした Better Canvas としての使い方が、 WebGL の主要な適用分野になっていくように思います。そして、 Web 上でのグラフィクス表現を飛躍的に向上させてくれるのは間違いないでしょう。皆さんも、ぜひ WebGL に触れて、その可能性を体感してみてください。
参考文献
- Learning WebGL
- WebGL についてのチュートリアルなどが公開されています。この記事を書いた後に見つけました・・・ orz
- WebGL Cheat Sheet
- WebGL のメソッド一覧です。
- OpenGL ES 2.0 Reference Page
- WebGL のベースとなっている OpenGL ES 2.0 のリファレンスです。メソッドではなくグローバル関数で実装されていたり、 "gl" というプレフィクスが付いていたりと些細な違いはありますが、ほぼ一対一で対応しているようです。
- OpenGL Shading Language
- 頂点シェーダー・フラグメントシェーダーのリファレンスがダウンロードできます。 OpenGL 用のものですが、ほとんど違いはないと思います。
- OpenGL ES 2.0 プログラミングガイド
- まだ発売されていないようですが、 OpenGL ES 2.0 の解説書の邦訳です。
- ゲームプログラミングのための3Dグラフィックス数学
- だいぶ昔の書籍ですが、リアルタイム 3DCG の基礎技術を学ぶのにお勧めです。
詳しくはこちらの記事をどうぞ!
この記事にコメントする