WebGL & Three.js

WebGL とは

WebGL とは

WebGL を触ってみよう

初期化

  <canvas id="screen" width="500px" height="500px"></canvas>
var gl     = null;
var canvas = $('#screen').get(0);
$.each(['webgl', 'experimental-webgl'], function() {
  try { gl = canvas.getContext(this); } catch(e) {}
  return !gl;
});
if(!gl) {
  alert('WebGL がサポートされていません。');
}

頂点データの生成

諸事情により、ここからは端折っていきます…

// 頂点データを生成
var positions = [];
for(var i = 0 ; i <= 8 ; ++i) {
  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);
  }
}

// VBOを作成し、データを転送
vbuffers = $.map([positions, positions], function() {
  var vbuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this), gl.STATIC_DRAW);
  return vbuffer;
});
gl.bindBuffer(gl.ARRAY_BUFFER, null);

インデックスデータの作成

// インデックスデータを生成
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;

頂点シェーダーの実装

<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;

  varying vec4 color;

  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);
    gl_Position = mvpMatrix * vec4(position, 1.0);
  }
</script>

フラグメントシェーダーの実装

<script id="fshader" type="x-shader/x-fragment">
  #ifdef GL_ES
  precision highp float;
  #endif

  varying vec4 color;

  void main() {
    gl_FragColor = color;
  }
</script>

シェーダーオブジェクトを作成

// 頂点シェーダーを作成
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"], 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);
});

描画 (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.57735, 0.57735, 0.57735, 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();

描画 (2)

$.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], 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.drawElements(gl.TRIANGLES, numIndices, gl.UNSIGNED_SHORT, 0);

// ページに反映させる
gl.flush();

実行結果

やってらんないよ…

WebGL は面倒くさい

そこで Three.js ですよ!!

Three.jsとは

WebGL Three.js を触ってみよう

先ほどと同じ事を Three.js でやると…

var SCREEN_WIDTH  = 500;
var SCREEN_HEIGHT = 500;

// シーンの初期化
var scene    = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
renderer.setSize(SCREEN_WIDTH,
                 SCREEN_HEIGHT);
renderer.setClearColorHex(0x000000, 1);
document.body.appendChild(
  renderer.domElement);

// カメラの作成
var camera = new THREE.PerspectiveCamera(
  30, SCREEN_WIDTH / SCREEN_HEIGHT,
  0.1, 1000 );
camera.position.set(0, 0, 6);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);

// ライトの作成
var light =
  new THREE.DirectionalLight(0xcccccc);
light.position.set(0.577, 0.577, 0.577);
scene.add(light);
            
var ambient =
  new THREE.AmbientLight(0x333333);
scene.add(ambient);

// モデルの作成
var geometry =
  new THREE.SphereGeometry(1, 16, 8);
var material =
  new THREE.MeshLambertMaterial(
    { color: 0xffffff ,
      ambient: 0xffffff });
var mesh =
  new THREE.Mesh(geometry, material);
scene.add(mesh);

// レンダリング
function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
};
render();
            
これだけ!

ライブラリの読み込み

基本的に、.jsファイルをひとつ読みこむだけです。

<script src="Three.js"></script>

Tree.jsは ↓ でダウンロードできます。
http://mrdoob.github.com/three.js/build/Three.js

自分でビルドするのも簡単です。

git clone https://github.com/mrdoob/three.js.git three
cd three/utils
python build.py --common --minified

--minified を外せば非圧縮バージョンになります。
デバッグに便利。

シーンの初期化

var SCREEN_WIDTH  = 500;
var SCREEN_HEIGHT = 500;

var scene    = new THREE.Scene();
var renderer = new THREE.WebGLRenderer();
renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
renderer.setClearColorHex(0x000000, 1);
document.body.appendChild(renderer.domElement);
          

カメラの作成

var camera = new THREE.PerspectiveCamera(
  30, SCREEN_WIDTH / SCREEN_HEIGHT, 0.1, 1000 );
camera.position.set(0, 0, 6);
camera.lookAt(new THREE.Vector3(0, 0, 0));
scene.add(camera);
          

ライトの作成

var light = new THREE.DirectionalLight(0xcccccc);
light.position.set(0.577, 0.577, 0.577);
scene.add(light);

var ambient = new THREE.AmbientLight(0x333333);
scene.add(ambient);

モデルの作成

var geometry = new THREE.SphereGeometry(1, 16, 8);
var material = new THREE.MeshLambertMaterial(
    { color: 0xffffff , ambient: 0xffffff });
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

レンダリング

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
};
render();

実行結果

テクスチャを貼ってみる

var material = new THREE.MeshLambertMaterial(
  { color: 0xffffff , ambient: 0xffffff,
    map: THREE.ImageUtils.loadTexture('lib/earth.jpg') });

マテリアルの初期化時にファイルを指定するだけで、
テクスチャが貼れる

実行結果

地球を回してみる

var baseTime = +new Date;
function render() {
  requestAnimationFrame(render);
  var time = (+new Date - baseTime) / 1000;
  mesh.rotation.y = 0.3 * time;
  renderer.render(scene, camera);
};
render();

描画するごとにオブジェクトの positionrotation
少しずつ変化させることでアニメーションする

実行結果

たくさん表示してみる

// モデルの作成
// ...
var meshes   = [];
var rotation = new THREE.Vector3();
for(var x = 0 ; x < 8 ; x++) {
  for(var y = 0 ; y < 8 ; y++) {
    for(var z = 0 ; z < 8 ; z++) {
      var mesh = new THREE.Mesh(geometry, material);
      mesh.position.set(x*4-14.5, y*4-14.5, -z*8-55);
      mesh.rotation = rotation;
      scene.add(mesh);
      meshes.push(mesh);
    }
  }
}

// レンダリング
var baseTime = +new Date;
function render() {
  requestAnimationFrame(render);
  var time   = (+new Date - baseTime) / 1000;
  rotation.y = 0.3 * time;
  renderer.render(scene, camera);
};
render();

実行結果

奥行き感が足りない?フォグをかけてみよう

scene.fog    = new THREE.Fog(0xffffff, 60, 130);
material.fog = true;

実行結果

影を表示する

var renderer = new THREE.WebGLRenderer();
renderer.shadowMapEnabled = true;    // ShadowMap を有効化
renderer.shadowMapBias    = 0.00385; // アーティファクトが出ないように調整

// ...

// ShadowMap が使えるのは SpotLight のみ
var light = new THREE.SpotLight(0xffffff, 1, 0, true);
light.position.set(500, 1000, 500);
scene.add(light);

// ...

var torus1 = new THREE.Mesh(torusGeometry, torusMaterial);
torus1.position.set(-200, 0, 0);
torus1.castShadow    = true; // true なら他の物体に影を落とす
torus1.receiveShadow = true; // true なら他の物体の影が落ちる
scene.add(torus1);

実行結果

モデルを読み込んでみる

// モデルを読み込む
var loader = new THREE.ColladaLoader();
loader.options.convertUpAxis = true;
loader.load('lib/monster.dae', init);

function init(collada) {
  // モデル以外の初期化処理…

  // モデルの作成
  console.log(collada);
  var mesh = collada.scene;

  // レンダリング
}

実行結果

公式サイトにはクールなデモがいっぱい

WebGL 関連ツール

Three Fab

Three.js doc

WebGL Inspector

WebGL playground

参考資料

ご清聴ありがとうございました!