JSON モデルデータの読み込み
Graphical Web !
当テキストは Graphical Web Advent Calendar 2013 - Adventar への参加記事です。
Graphical Web という言葉は、なんとなく響きがかっこいいですね。HTML5、CSS3、そして我らが WebGL。いずれも、グラフィカルな側面を多く含む技術です。当サイトは WebGL の開発を支援するサイトですから、当然今回の企画にも WebGL にまつわる記事を寄稿させていただこうと思います。
今回のテーマは、JSON モデルデータの読み込みとレンダリングです。Advent Calendar から初めて当サイトに来られたという方もいらっしゃるでしょうから、今回はいろいろと当サイトの記事などを引用しつつ丁寧に解説してけたらと思います。
three.js と WebGL
WebGL を用いた開発シーンでは、いまや欠かすことのできない存在となった three.js ですが、私個人はこのライブラリを使うことなく現在まで過ごしてきました。
当然、当サイトでは一度も three.js について触れたことはありません。これは three.js に対する反骨精神とかライバル心とかそういうものではなく、単に自分自身ができる限りコードをスクラッチで記述することを重視するが故のことです。あらゆる言語をずっと独学でやってきた私個人としては「ライブラリを使ってサクッと生産性高くコードが書ける」ことよりも「自分自身が納得して理解したうえで動く」ことを大切にしてきました。three.js を使って簡単に実装できるものを無理にスクラッチで記述する必要はないと思いますし、なにより three.js のようなライブラリを作っている mr.doob 氏のような人は本当にすごいなぁといつも感心しっぱなしです。
three.js を使うと、モデリングソフトから出力したデータを使って、比較的簡単にモデルを表示したりモーションをつけたりすることができるみたいです。既に、今回の Advent Calender でもこのあたりのことに触れられていますね。
今回は、three.js を一切使わずに、JSON で記述したモデルデータを読み込んでレンダリングまで行ってみようと思います。いまどきそんな技術の解説にはたして需要があるのかわかりませんが、私のような物好きもいるかもしれませんし、とりあえずやってみましょう。
当テキストでは three.js こそ使いませんが、一部自作の行列演算補助用ライブラリを使っています。詳細は以下のページをご覧ください。
今回のテキストで解説するサンプルのページでは、JSON データから生成されたお馴染みのあのモデルがくるくると回ります。
前置きはこれくらいにして、早速見ていくことにしましょう。
JSON データの取得
今回のサンプルでは、JSON データを XMLHttpRequest
で取得して、そこから頂点の情報を抜き出してモデルをレンダリングします。サンプルページで読み込まれるスクリプトの冒頭は、以下のような感じになっています。
サンプルスクリプト冒頭部分
var canvas;
var gl;
var json;
window.onload = function(){
xhr('bunny.json');
}
function xhr(file){
var x = new XMLHttpRequest();
x.open('GET', file);
x.onreadystatechange = function(){
if(x.readyState == 4){
json = JSON.parse(x.responseText);
renderer();
}
}
x.send();
}
HTML 内の canvas エレメントとそこから取得する WebGL コンテキスト、そして読み込んだ JSON (bunny.json) から生成したオブジェクトの三つをグローバルな変数で管理します。まぁここでやってることは要するに JSON ファイルを取得して後述するレンダラ関数( renderer
)を呼び出すことだけですので簡単ですね。このテキストを読んでいる人に XMLHttpRequest
がわからない人はたぶんいないと思いますが、わからない人は各自調べてください。
今回利用する JSON データの中身は、ざっくりと以下のような構成になっています。
JSON データの中身
{
"vertex": 34835,
"face": 69666,
"position": [
1.4870,
0.3736,
2.2576, ...
],
"normal": [
-0.2385,
-0.8953,
-0.3764, ...
],
"index": [
0,
1,
2, ...
]
}
頂点の個数、そして面の個数に続き、頂点の位置と法線、さらに面の頂点インデックスが含まれています。今回のサンプルには含まれていませんが、当然のことながら JSON データにはテクスチャ座標などのそのほかの頂点属性も含めることができます。位置と法線、そして頂点インデックスしか含んでいない今回の JSON ファイル。それでも頂点数が 35,000 弱でファイルサイズは 2,700 KB ほどあります。巨大な JSON データを扱う場合には web workers などをうまく利用したほうがいいかもしれませんね。
はい、ということで、JSON データを読み込むことができましたので、早くもこのテキストの目的である「JSON モデルデータの読み込みとレンダリング」のうち、モデルデータの読み込みは完了してしまいました。ここまでは実に簡単ですね。
しかし、これを three.js なしでレンダリングするためには、ここから遠い遠い道のりを越えていかなくてはなりません。
レンダリングの下準備
先程のスクリプト冒頭部分から呼ばれているレンダラ関数( renderer
)の中身を見ていきます。※今回は Advent Calendar への寄稿記事ということもありますので、いつもより詳細に解説していきます
まずは canvas エレメントの取得と初期化から。
初期化処理
// canvas を取得しサイズを調整
canvas = document.getElementById('screen');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// webglコンテキストを取得
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
// シーンをレンダリングするメインシェーダの準備
var v_shader = create_shader('main_vs');
var f_shader = create_shader('main_fs');
var prg = create_program(v_shader, f_shader);
canvas エレメントを変数に取得したら、まず大きさを画面いっぱいのサイズに調整します。その後、canvas から WebGL コンテキストを取得します。
WebGL コンテキストは、WebGL のあらゆるメソッドやプロパティを一括して管理するオブジェクトです。このオブジェクトがなければ話になりませんので、最初に取得するのを忘れないようにしましょう。
WebGL コンテキストが取得できたら、続いてシーンをレンダリングするためのシェーダを生成します。上記のサンプルコードに登場する create_shader
という関数は自前の関数です。そのソースは以下。
関数 create_shader
// シェーダを生成する関数
function create_shader(id){
// シェーダを格納する変数
var shader;
// HTMLからscriptタグへの参照を取得
var scriptElement = document.getElementById(id);
// scriptタグが存在しない場合は抜ける
if(!scriptElement){return;}
// scriptタグのtype属性をチェック
switch(scriptElement.type){
// 頂点シェーダの場合
case 'x-shader/x-vertex':
shader = gl.createShader(gl.VERTEX_SHADER);
break;
// フラグメントシェーダの場合
case 'x-shader/x-fragment':
shader = gl.createShader(gl.FRAGMENT_SHADER);
break;
default :
return;
}
// 生成されたシェーダにソースを割り当てる
gl.shaderSource(shader, scriptElement.text);
// シェーダをコンパイルする
gl.compileShader(shader);
// シェーダが正しくコンパイルされたかチェック
if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
// 成功していたらシェーダを返して終了
return shader;
}else{
// 失敗していたらエラーログをアラートする
alert(gl.getShaderInfoLog(shader));
}
}
シェーダをコンパイルして返すだけの関数です。コメントが細かく書き込まれているのでちょっと文章量多いように見えますがそんなに複雑なことはしてません。
シェーダは、頂点シェーダとフラグメントシェーダの二種類があります。いずれも、この関数ひとつで生成できます。二種類のシェーダが生成できたら、それらをリンクして一組のプログラムオブジェクトにしてやります。これも、自前の関数 create_program
を使えば簡単です。そのソースは以下のようになります。
関数 create_program
// プログラムオブジェクトを生成しシェーダをリンクする関数
function create_program(vs, fs){
// プログラムオブジェクトの生成
var program = gl.createProgram();
// プログラムオブジェクトにシェーダを割り当てる
gl.attachShader(program, vs);
gl.attachShader(program, fs);
// シェーダをリンク
gl.linkProgram(program);
// シェーダのリンクが正しく行なわれたかチェック
if(gl.getProgramParameter(program, gl.LINK_STATUS)){
// 成功していたらプログラムオブジェクトを有効にする
gl.useProgram(program);
// プログラムオブジェクトを返して終了
return program;
}else{
// 失敗していたらエラーログをアラートする
alert(gl.getProgramInfoLog(program));
}
}
シェーダへの参照
プログラムオブジェクトまで生成できたら、続いてはシェーダと javascript の橋渡しを行うための準備をします。考えてみれば当然のことですがシェーダが動作するのは GPU です。だからこそ WebGL はブラウザ上で高速なレンダリングを行うことができるわけです。
WebGL の場合、GPU 側で動作するシェーダに javascript からデータを送ってやる必要があります。シェーダへデータを送るためのポインタのような役割をしてくれる各種ロケーションの取得は、一連の初期化処理時に事前に行っておきます。いざ実際にレンダリングを行う段階で、これらのロケーションを参照しつつレンダリングを行うことになります。
プログラムオブジェクトからいろいろ取得
// 頂点属性のロケーションを取得
var attLocation = new Array();
attLocation[0] = gl.getAttribLocation(prg, 'position');
attLocation[1] = gl.getAttribLocation(prg, 'normal');
// 頂点属性のサイズ
var attStride = new Array();
attStride[0] = 3;
attStride[1] = 3;
// ユニフォームロケーションを取得
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, 'mMatrix');
uniLocation[1] = gl.getUniformLocation(prg, 'mvpMatrix');
uniLocation[2] = gl.getUniformLocation(prg, 'invMatrix');
uniLocation[3] = gl.getUniformLocation(prg, 'skyDirection');
uniLocation[4] = gl.getUniformLocation(prg, 'lightDirection');
uniLocation[5] = gl.getUniformLocation(prg, 'eyePosition');
uniLocation[6] = gl.getUniformLocation(prg, 'skyColor');
uniLocation[7] = gl.getUniformLocation(prg, 'groundColor');
今回は頂点に位置情報と法線しか持たせません。いずれもひとつの頂点あたり三つのデータからなります(これは単純に XYZ です)。
uniform ロケーションについてもこのあたりで同時に取得しておきます。今回は半球ライティングを用いたライティング処理を行うため、ちょっと uniform 変数の数が多くなっています。
尚、ここで登場する uniform などの単語の意味するところがわからない場合には、以下の記事を参考にしてもらえるとわかりやすいと思います。
JSON データから頂点情報を生成
さて続いては、読み込んだ JSON データからモデルの頂点情報を抜き出し、WebGL で利用可能な状態にしてやる作業です。
WebGL では、頂点の持つ属性値やレンダリングされる際の頂点インデックスなどを専用のバッファを使って管理します。先程のシェーダなどと同様、自作の関数を使ってささっとバッファを生成してしまいましょう。
頂点情報からバッファを生成
// 頂点情報を取得
var pos = create_vbo(json.position);
var nor = create_vbo(json.normal);
var idx = create_ibo(json.index);
var indexLength = json.index.length;
var bufferList = [pos, nor];
ここで登場する create_vbo
と create_ibo
は自作の関数で、JSON から抽出した頂点データ(頂点の情報を表す配列)を使って必要なバッファオブジェクトを生成してくれます。ここで生成したバッファオブジェクトを WebGL に通知してやることで、モデルが正しく画面上にレンダリングされます。登場した二つの自作関数は以下のようなコードになっています。
関数 create_vbo, create_ibo
// VBOを生成する関数
function create_vbo(data){
// バッファオブジェクトの生成
var vbo = gl.createBuffer();
// バッファをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
// バッファにデータをセット
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
// バッファのバインドを無効化
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 生成した VBO を返して終了
return vbo;
}
// IBOを生成する関数
function create_ibo(data){
// バッファオブジェクトの生成
var ibo = gl.createBuffer();
// バッファをバインドする
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
// バッファにデータをセット
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);
// バッファのバインドを無効化
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
// 生成したIBOを返して終了
return ibo;
}
どちらもやっていることはあんまり変わりませんね。空のバッファを生成してからデータをセット、そして最終的にそのバッファオブジェクトを返しているだけです。
ここで生成したバッファオブジェクトは、レンダリングが行われる前に WebGL にバインドして紐付けてやります。この紐付け作業も、関数化しておくと手間がはぶけますので、以下のような関数を自前で用意しておきます。
頂点バッファをバインドする関数
// VBOをバインドし登録する関数
function set_attribute(vbo, attL, attS){
// 引数として受け取った配列を処理する
for(var i in vbo){
// バッファをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, vbo[i]);
// attributeLocationを有効にする
gl.enableVertexAttribArray(attL[i]);
// attributeLocationを通知し登録する
gl.vertexAttribPointer(attL[i], attS[i], gl.FLOAT, false, 0, 0);
}
}
詳細な使い方は後述しますが、この関数を使うと先に取得してあったシェーダへのポインタと頂点バッファの紐付けが行われ、正しくモデルをレンダリングすることができます。
その他の初期化処理と行列演算
続いては、3D プログラミングには欠かせない行列の扱いについて見ていきます。
行列に関する計算はちょっと複雑なので、ここばかりはさすがにライブラリを使うことを推奨します。もちろんフルスクラッチで行列までやってはいけないということではないのですが、結構大変なのであんまりオススメしません。
行列演算を行うためのライブラリにはいくつかありますが、当サイトでは先述のとおり自前の行列演算ライブラリ、minMatrixb.js を使います。
以下のコードで登場している matIV
クラスは minMatrixb.js 内で定義されているクラスです。行列に関する計算の基本的な部分は押さえてありますので、WebGL の勉強をちょっと行う程度には問題ないと思います。ライセンスも完全フリーですので適当に使ってください。
行列の生成と初期化
// 各種行列の生成と初期化
var m = new matIV();
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var tmpMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
var invMatrix = m.identity(m.create());
上記のコードで、いくつかの行列が準備されます。今回はライティングを行う関係上、逆行列なども使いますので全部で六つの行列を用意しました。
さらに、引き続き matIV
クラスのメソッドを活用しつつ、レンダリングに必要な行列演算を事前にいくつかやっておきます。
事前に行列を準備
// カメラの位置
var eyePosition = [0.0, 0.0, 20.0];
// ビュー×プロジェクション座標変換行列
m.lookAt(eyePosition, [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], vMatrix);
m.perspective(45, canvas.width / canvas.height, 0.1, 50.0, pMatrix);
m.multiply(pMatrix, vMatrix, tmpMatrix);
ビュー行列と透視射影行列を準備し、あらかじめ掛け合わせておきます。これらの計算結果は、最終的にモデル行列(ワールド変換行列)を掛け合わせてからシェーダに送ることになりますが、それはもう少し先。その前に、いくつかの設定と配列変数の準備をしておきます。これらの処理が終われば、あとはレンダリングするだけです。
いくつかの初期化処理
// 深度テストとカリングを有効にする
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.enable(gl.CULL_FACE);
// 天空の向き
var skyDirection = [0.0, 1.0, 0.0];
// ライトの向き
var lightDirection = [-0.577, 0.577, 0.577];
// 天空色
var skyColor = [0.1, 0.2, 0.5, 1.0];
// 地面色
var groundColor = [0.2, 0.2, 0.0, 1.0];
// カウンタの宣言
var count = 0;
無名関数を使ったループ処理
事前に準備するべきものは全て準備しましたので、いよいよレンダリングです。
サンプルでは javascript におけるアニメーション処理の定石である setTimeout
を使ってループ処理を行います。
ループ処理
// 恒常ループ
(function(){
// カウンタをインクリメントする
count++;
// カウンタを元にラジアンを算出
rad = (count % 360) * Math.PI / 180;
// 初期化
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 各種バッファのバインド
set_attribute(bufferList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idx);
// モデル座標変換行列を初期化
m.identity(mMatrix);
// Y軸回転 + 少しだけ手前に傾ける + 少しだけ -Y方向に移動する
m.translate(mMatrix, [0.0, -5.0, 0.0], mMatrix);
m.rotate(mMatrix, Math.PI * 0.05, [1.0, 0.0, 0.0], mMatrix);
m.rotate(mMatrix, rad, [0.0, 1.0, 0.0], mMatrix);
// 行列を全て掛け合わせる
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// ライティングのための逆行列の生成
m.inverse(mMatrix, invMatrix);
// シェーダへ情報を送る
gl.uniformMatrix4fv(uniLocation[0], false, mMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[2], false, invMatrix);
gl.uniform3fv(uniLocation[3], skyDirection);
gl.uniform3fv(uniLocation[4], lightDirection);
gl.uniform3fv(uniLocation[5], eyePosition);
gl.uniform4fv(uniLocation[6], skyColor);
gl.uniform4fv(uniLocation[7], groundColor);
// レンダリング!
gl.drawElements(gl.TRIANGLES, indexLength, gl.UNSIGNED_SHORT, 0);
// コンテキストの再描画
gl.flush();
// ループのために再帰呼び出し
setTimeout(arguments.callee, 1000 / 30);
})();
描画 FPS は約 30 になるようにしています。
ループ処理の中では、コンテキストの初期化に始まり、バッファのバインド、行列の準備、シェーダへのデータのプッシュ、そしてレンダリングという感じの流れで処理が行われます。事前に準備するものが大量にある上、ループ処理の中でも毎回これだけのことをやらなくてはならいのですからちょっと大変ですね。いくつかの処理は、ループ内に含めずに最適化することも可能です。そのあたりはいろいろ工夫してみるといいかもしれません。
シェーダのソース
さて、ここまで長々と解説してきましたが、肝心のシェーダのソースを載せてなかったですね。
WebGL では、シェーダのソースは文字列データとして用意して生成メソッドに渡します。要はメモリ上に展開された文字列データならなんでもいいのですが、HTML 内に埋め込んでおく方法を今回のサンプルでは使っています。ということで、最後に HTML ソースを掲載しておきます。
サンプルの HTML ソース
<html>
<head>
<title>WebGL sample</title>
<script src="https://wgld.org/j/minMatrixb.js" type="text/javascript"></script>
<script src="script.js" type="text/javascript"></script>
<script id="main_vs" type="x-shader/x-vertex">
attribute vec3 position;
attribute vec3 normal;
uniform mat4 mMatrix;
uniform mat4 mvpMatrix;
uniform mat4 invMatrix;
uniform vec3 skyDirection;
uniform vec3 lightDirection;
uniform vec3 eyePosition;
uniform vec4 skyColor;
uniform vec4 groundColor;
varying vec4 vColor;
void main(void){
vec3 invSky = normalize(invMatrix * vec4(skyDirection, 0.0)).xyz;
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
vec3 invEye = normalize(invMatrix * vec4(eyePosition, 0.0)).xyz;
vec3 halfLE = normalize(invLight + invEye);
float diffuse = clamp(dot(normal, invLight), 0.0, 1.0) * 0.5;
float specular = pow(clamp(dot(normal, halfLE), 0.0, 1.0), 70.0);
float hemisphere = (dot(normal, invSky) + 1.0) * 0.5;
vec4 ambient = mix(groundColor, skyColor, hemisphere);
vColor = vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0) + ambient;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
</script>
<script id="main_fs" type="x-shader/x-fragment">
precision mediump float;
varying vec4 vColor;
void main(void){
gl_FragColor = vColor;
}
</script>
<style type="text/css">
* {margin: 0px; padding: 0px;}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
このシェーダのソースがなにをやっているのかは、別途既に解説しているテキストがありますので、そちらをご覧になってみてください。ちなみに、一般に半球ライティングと呼ばれているテクニックを使ってモデルのライティングを行っています。
まとめ
さて、ここまで長々と解説してきましたが、このテキストを読むといかに three.js がすばらしいものかということがわかりますね。WebGL の冗長な部分をうまくラッピングして、効率よくコードが記述できるように工夫されていることが実感できるのではないでしょうか。今回のテキストでは、詳細はかなりはぶきながら解説してきましたが、それでもこれだけの文章量になりました。それぞれのフェーズにおける詳細な解説は、当サイトのコンテンツを最初のほうから読んでいけばどこかにあると思います。(笑)
JSON データをメモリ上に展開すること自体は JSON.parse
を使えば簡単です。その展開されたデータを、どうやって WebGL で利用できる形に加工すればいいのか、そのあたりを今回のテキストを通じて理解していただけたのなら幸いです。また、今回は XMLHttpRequest
を使って JSON データを読み込みましたが、実はもっと手軽な実装方法もあります。
結局のところ、JSON データはパースしてしまえば単なる javascript のオブジェクトにしか過ぎません。そこで、最初から変数に代入するだけの javascript ファイルを別途用意しておき、ページのロードと同時に読み込んでしまえばいいのです。例えば以下のような感じですね。
var bunny = {"vertex":34835,"face":69666,"position":[...]};
上記のように記述されたファイルを読み込めば、グローバル変数が増えてしまうことやページの読み込み遅延などの問題が起こる可能性はあるものの、簡単に JSON データを読み込むことができてしまいます。どのような方法がベストなのか、その答えは一概には言えません。ただ、やり方はいろいろあるということですね。
以下に、再度サンプルデモページへのリンクを貼っておきます。WebGL 対応ブラウザで見れば、JSON から生成されたモデルがレンダリングされる様子を見ることができるはずです。
最後に、スクリプトのソース全文を掲載しておきます。果てしない可能性を秘めた WebGL に少しでも興味を持っていただけたらと思います。
script.js 全ソース
// グローバル変数
var canvas;
var gl;
var json;
// オンロードイベント
window.onload = function(){
xhr('bunny.json');
}
// JSON ファイルの取得
function xhr(file){
var x = new XMLHttpRequest();
x.open('GET', file);
x.onreadystatechange = function(){
if(x.readyState == 4){
json = JSON.parse(x.responseText);
renderer();
}
}
x.send();
}
// レンダラ関数
function renderer(){
// canvas を取得しサイズを調整
canvas = document.getElementById('screen');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// webglコンテキストを取得
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
// シーンをレンダリングするメインシェーダの準備
var v_shader = create_shader('main_vs');
var f_shader = create_shader('main_fs');
var prg = create_program(v_shader, f_shader);
// 頂点属性のロケーションを取得
var attLocation = new Array();
attLocation[0] = gl.getAttribLocation(prg, 'position');
attLocation[1] = gl.getAttribLocation(prg, 'normal');
// 頂点属性のサイズ
var attStride = new Array();
attStride[0] = 3;
attStride[1] = 3;
// ユニフォームロケーションを取得
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, 'mMatrix');
uniLocation[1] = gl.getUniformLocation(prg, 'mvpMatrix');
uniLocation[2] = gl.getUniformLocation(prg, 'invMatrix');
uniLocation[3] = gl.getUniformLocation(prg, 'skyDirection');
uniLocation[4] = gl.getUniformLocation(prg, 'lightDirection');
uniLocation[5] = gl.getUniformLocation(prg, 'eyePosition');
uniLocation[6] = gl.getUniformLocation(prg, 'skyColor');
uniLocation[7] = gl.getUniformLocation(prg, 'groundColor');
// 頂点情報を取得
var pos = create_vbo(json.position);
var nor = create_vbo(json.normal);
var idx = create_ibo(json.index);
var indexLength = json.index.length;
var bufferList = [pos, nor];
// 各種行列の生成と初期化
var m = new matIV();
var mMatrix = m.identity(m.create());
var vMatrix = m.identity(m.create());
var pMatrix = m.identity(m.create());
var tmpMatrix = m.identity(m.create());
var mvpMatrix = m.identity(m.create());
var invMatrix = m.identity(m.create());
// カメラの位置
var eyePosition = [0.0, 0.0, 20.0];
// ビュー×プロジェクション座標変換行列
m.lookAt(eyePosition, [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], vMatrix);
m.perspective(45, canvas.width / canvas.height, 0.1, 50.0, pMatrix);
m.multiply(pMatrix, vMatrix, tmpMatrix);
// 深度テストとカリングを有効にする
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.enable(gl.CULL_FACE);
// 天空の向き
var skyDirection = [0.0, 1.0, 0.0];
// ライトの向き
var lightDirection = [-0.577, 0.577, 0.577];
// 天空色
var skyColor = [0.1, 0.2, 0.5, 1.0];
// 地面色
var groundColor = [0.2, 0.2, 0.0, 1.0];
// カウンタの宣言
var count = 0;
// 恒常ループ
(function(){
// カウンタをインクリメントする
count++;
// カウンタを元にラジアンを算出
rad = (count % 360) * Math.PI / 180;
// 初期化
gl.clearColor(0.2, 0.2, 0.2, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 各種バッファのバインド
set_attribute(bufferList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idx);
// モデル座標変換行列を初期化
m.identity(mMatrix);
// Y軸回転 + 少しだけ手前に傾ける + 少しだけ -Y方向に移動する
m.translate(mMatrix, [0.0, -5.0, 0.0], mMatrix);
m.rotate(mMatrix, Math.PI * 0.05, [1.0, 0.0, 0.0], mMatrix);
m.rotate(mMatrix, rad, [0.0, 1.0, 0.0], mMatrix);
// 行列を全て掛け合わせる
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// ライティングのための逆行列の生成
m.inverse(mMatrix, invMatrix);
// シェーダへ情報を送る
gl.uniformMatrix4fv(uniLocation[0], false, mMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[2], false, invMatrix);
gl.uniform3fv(uniLocation[3], skyDirection);
gl.uniform3fv(uniLocation[4], lightDirection);
gl.uniform3fv(uniLocation[5], eyePosition);
gl.uniform4fv(uniLocation[6], skyColor);
gl.uniform4fv(uniLocation[7], groundColor);
// レンダリング!
gl.drawElements(gl.TRIANGLES, indexLength, gl.UNSIGNED_SHORT, 0);
// コンテキストの再描画
gl.flush();
// ループのために再帰呼び出し
setTimeout(arguments.callee, 1000 / 30);
})();
// シェーダを生成する関数
function create_shader(id){
// シェーダを格納する変数
var shader;
// HTMLからscriptタグへの参照を取得
var scriptElement = document.getElementById(id);
// scriptタグが存在しない場合は抜ける
if(!scriptElement){return;}
// scriptタグのtype属性をチェック
switch(scriptElement.type){
// 頂点シェーダの場合
case 'x-shader/x-vertex':
shader = gl.createShader(gl.VERTEX_SHADER);
break;
// フラグメントシェーダの場合
case 'x-shader/x-fragment':
shader = gl.createShader(gl.FRAGMENT_SHADER);
break;
default :
return;
}
// 生成されたシェーダにソースを割り当てる
gl.shaderSource(shader, scriptElement.text);
// シェーダをコンパイルする
gl.compileShader(shader);
// シェーダが正しくコンパイルされたかチェック
if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
// 成功していたらシェーダを返して終了
return shader;
}else{
// 失敗していたらエラーログをアラートする
alert(gl.getShaderInfoLog(shader));
}
}
// プログラムオブジェクトを生成しシェーダをリンクする関数
function create_program(vs, fs){
// プログラムオブジェクトの生成
var program = gl.createProgram();
// プログラムオブジェクトにシェーダを割り当てる
gl.attachShader(program, vs);
gl.attachShader(program, fs);
// シェーダをリンク
gl.linkProgram(program);
// シェーダのリンクが正しく行なわれたかチェック
if(gl.getProgramParameter(program, gl.LINK_STATUS)){
// 成功していたらプログラムオブジェクトを有効にする
gl.useProgram(program);
// プログラムオブジェクトを返して終了
return program;
}else{
// 失敗していたらエラーログをアラートする
alert(gl.getProgramInfoLog(program));
}
}
// VBOを生成する関数
function create_vbo(data){
// バッファオブジェクトの生成
var vbo = gl.createBuffer();
// バッファをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
// バッファにデータをセット
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
// バッファのバインドを無効化
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 生成した VBO を返して終了
return vbo;
}
// VBOをバインドし登録する関数
function set_attribute(vbo, attL, attS){
// 引数として受け取った配列を処理する
for(var i in vbo){
// バッファをバインドする
gl.bindBuffer(gl.ARRAY_BUFFER, vbo[i]);
// attributeLocationを有効にする
gl.enableVertexAttribArray(attL[i]);
// attributeLocationを通知し登録する
gl.vertexAttribPointer(attL[i], attS[i], gl.FLOAT, false, 0, 0);
}
}
// IBOを生成する関数
function create_ibo(data){
// バッファオブジェクトの生成
var ibo = gl.createBuffer();
// バッファをバインドする
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
// バッファにデータをセット
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int16Array(data), gl.STATIC_DRAW);
// バッファのバインドを無効化
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
// 生成したIBOを返して終了
return ibo;
}
}