Subterranean Flower

DartでWebGL入門-アニメーション編

Author
古都こと
ふよんとえんよぷよぐやま!!!

基礎編3D編では静止画の描画方法について解説した。今回はそれをアニメーションさせてみよう。

DartでWebGL入門

アニメーションの手順

だいたい以下の通りの手順になる。

  1. データを更新する
  2. 前回の描画データを削除する
  3. 新しいデータを描画する
  4. これを繰り返す

新しい要素はほとんどないのでサクッと終わらせよう。

アニメーションのために使用するメソッド

window.requestAnimationFrame

ブラウザ上で動作するタイマーではsetIntervalやsetTimeoutが有名だが、最近ではrequestAnimationFrameという便利なものがある。これは表示デバイスにあわせて最適な間隔でコールバック関数を実行してくれるというものだ。一般的なデバイスでは、1秒間あたり60回実行される。また、ウィンドウが裏に隠れてしまったときなどは、描画する負担が無駄になるので、自動的に実効頻度を落としてくれることもある。

DartにはTimerクラスがあるが、JavaScriptに変換するとsetIntervalなどに変換されて精度が落ちるので、基本的にはrequestAnimationFrameを使用することになる。

1度のリクエストで1回だけしか実行してくれないので、実際にアニメーションさせるには描画のたびに呼び出す必要がある。

void callback(double timestamp) {
  // ここに繰り返したい処理を書く。
  window.requestAnimationFrame(callback); // 毎回呼び出す必要あり。
}

window.requestAnimationFrame(callback); // 実行を開始。

RenderingContext.clear

バッファの中身をクリアするのに使用する。カラーバッファ、デプスバッファ、ステンシルバッファなどはこれでまとめて消そう。

context.clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT);

なお、カラーバッファをクリアする際は色を指定することもできる。

context.clearColor(red, green, blue, alpha);

サンプル

四角形の周りをカメラがまわるだけのアニメーションを作成しよう。カメラを動かす関係上、裏側もほしいので背面カリングはオフにしておく。

前準備は以下のようなコードになる。詳しくは3D編で解説している通り。

attribute vec3 vertexPosition;
attribute vec3 color;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

varying vec4 vColor;

void main() {
  vColor = vec4(color, 1.0);

  gl_Position = projection * view * model * vec4(vertexPosition, 1.0);
}
precision mediump float;
varying vec4 vColor;

void main() {
  gl_FragColor = vColor;
}
import 'dart:html';
import 'dart:math';
import 'dart:async';
import 'dart:web_gl';
import 'dart:typed_data';
import 'package:vector_math/vector_math.dart';

Future<List<String>> loadShaders() {
  var loadVertexShader   = HttpRequest.getString("vertex_shader.glsl");
  var loadFragmentShader = HttpRequest.getString("fragment_shader.glsl");
  return Future.wait([loadVertexShader, loadFragmentShader]);
}

void main() {
  // シェーダを読み込んでから開始する。
  loadShaders().then((shaders)=>start(shaders[0], shaders[1]));
}

void start(String vertexShaderSource, String fragmentShaderSource) {
  // 表示用キャンバスの用意。
  const WIDTH  = 512;
  const HEIGHT = 512;
  var display = new CanvasElement(width:WIDTH, height:HEIGHT);
  document.body.append(display);

  // WebGL描画コンテキスト。
  var context = display.getContext3d();
  context.viewport(0, 0, WIDTH, HEIGHT);

  /*
   * シェーダの準備。
   */
  var vertexShader = context.createShader(VERTEX_SHADER);
  context.shaderSource(vertexShader, vertexShaderSource);
  context.compileShader(vertexShader);

  var fragmentShader = context.createShader(FRAGMENT_SHADER);
  context.shaderSource(fragmentShader, fragmentShaderSource);
  context.compileShader(fragmentShader);

  if(!context.getShaderParameter(vertexShader, COMPILE_STATUS)) {
    var msg = context.getShaderInfoLog(vertexShader);
    throw new Exception("Failed to compile vertex shader: $msg");
  }

  if(!context.getShaderParameter(fragmentShader, COMPILE_STATUS)) {
    var msg = context.getShaderInfoLog(fragmentShader);
    throw new Exception("Failed to compile fragment shader: $msg");
  }

  var program = context.createProgram();
  context.attachShader(program, vertexShader);
  context.attachShader(program, fragmentShader);
  context.linkProgram(program);

  if(!context.getProgramParameter(program, LINK_STATUS)) {
    var msg = context.getProgramInfoLog(program);
    throw new Exception("Failed to link program: $msg");
  }

  context.useProgram(program);

  /*
   * デプステストなどを有効にする。
   */
  context.enable(DEPTH_TEST); // デプステスト

  /*
   * uniform変数を設定する。
   */
  // Model行列
  var model = new Matrix4.identity();

  // View行列は変化させるので、ここでは省略。
  // uniform変数のlocationだけ取得しておく。

  // Projection行列
  var fovY = 60 * PI / 180;
  var aspectRatio = WIDTH / HEIGHT;
  var projection = makePerspectiveMatrix(fovY, aspectRatio, 30, 300);

  // uniform変数を設定する。
  UniformLocation modelLocation      = context.getUniformLocation(program, "model");
  UniformLocation viewLocation       = context.getUniformLocation(program, "view");
  UniformLocation projectionLocation = context.getUniformLocation(program, "projection");
  context.uniformMatrix4fv(modelLocation, false, model.storage);
  context.uniformMatrix4fv(projectionLocation, false, projection.storage);

  /*
   * バッファの設定。
   */
  var vertexBuffer = context.createBuffer();
  var indexBuffer  = context.createBuffer();

  const POSITION_SIZE   = 3;
  const COLOR_SIZE      = 3;
  const STRIDE          = Float32List.BYTES_PER_ELEMENT * (POSITION_SIZE + COLOR_SIZE);
  const POSITION_OFFSET = 0;
  const COLOR_OFFSET    = Float32List.BYTES_PER_ELEMENT * POSITION_SIZE;

  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  var positionLocation = context.getAttribLocation(program, "vertexPosition");
  var colorLocation    = context.getAttribLocation(program, "color");
  context.enableVertexAttribArray(positionLocation);
  context.enableVertexAttribArray(colorLocation);
  context.vertexAttribPointer(positionLocation, POSITION_SIZE, FLOAT, false, STRIDE, POSITION_OFFSET);
  context.vertexAttribPointer(colorLocation, COLOR_SIZE, FLOAT, false, STRIDE, COLOR_OFFSET);

  /*
   * 描画データ。
   */
  var vertices = new Float32List.fromList(<double>[-30.0, 30.0, 0.0,  // 座標
                                                   0.0, 1.0, 0.0,     // 色
                                                   -30.0, -30.0, 0.0,
                                                   1.0, 0.0, 0.0,
                                                   30.0, 30.0, 0.0,
                                                   1.0, 0.0, 0.0,
                                                   30.0, -30.0, 0.0,
                                                   0.0, 0.0, 1.0,]);

  var indices = new Uint16List.fromList(<int>[0, 1, 2, 1, 3, 2]);

  // あらかじめデータの転送だけしておく。描画はしない。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);

  context.bindBuffer(ELEMENT_ARRAY_BUFFER, indexBuffer);
  context.bufferDataTyped(ELEMENT_ARRAY_BUFFER, indices, STATIC_DRAW);

  /*
   * ここから今回のコードを付け足していく。
   */
}

アニメーションループの用意

ループにはrequestAnimationFrameを使おう。

/*
 * ここから今回のコードを付け足していく。
 */
void animate(double timestamp) {
  // ここにループ内容を書き込んでいく
  window.requestAnimationFrame(animate); // 次のフレーム描画をリクエスト
}

window.requestAnimationFrame(animate);

ループ内容の作成

少し変化させて描画、少し変化させて描画、……と繰り返していけば動いているように見える。ここではカメラの位置を徐々に変化させている。

/*
 * ここから今回のコードを付け足していく。
 */

var radian = 0.0;
var radius = 100.0;

void animate(double timestamp) {
  // 更新
  radian += 1.0 * PI / 180;
  var cameraPosition = new Vector3(sin(radian)*radius, 100.0, cos(radian)*radius);
  var lookAtPosition = new Vector3.zero();
  var upDirection    = new Vector3(0.0, 1.0, 0.0);
  var view  = makeViewMatrix(cameraPosition, lookAtPosition, upDirection);
  context.uniformMatrix4fv(viewLocation, false, view.storage);

  // 前フレームの内容をクリア
  context.clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT);

  // 描画
  context.drawElements(TRIANGLES, indices.length, UNSIGNED_SHORT, 0);
  window.requestAnimationFrame(animate); // 次のフレーム描画をリクエスト
}

window.requestAnimationFrame(animate);

デモ

デモを開く

dart-webgl-animation

コード全体

import 'dart:html';
import 'dart:math';
import 'dart:async';
import 'dart:web_gl';
import 'dart:typed_data';
import 'package:vector_math/vector_math.dart';

Future<List<String>> loadShaders() {
  var loadVertexShader   = HttpRequest.getString("vertex_shader.glsl");
  var loadFragmentShader = HttpRequest.getString("fragment_shader.glsl");
  return Future.wait([loadVertexShader, loadFragmentShader]);
}

void main() {
  // シェーダを読み込んでから開始する。
  loadShaders().then((shaders)=>start(shaders[0], shaders[1]));
}

void start(String vertexShaderSource, String fragmentShaderSource) {
  // 表示用キャンバスの用意。
  const WIDTH  = 512;
  const HEIGHT = 512;
  var display = new CanvasElement(width:WIDTH, height:HEIGHT);
  document.body.append(display);

  // WebGL描画コンテキスト。
  var context = display.getContext3d();
  context.viewport(0, 0, WIDTH, HEIGHT);

  /*
   * シェーダの準備。
   */
  var vertexShader = context.createShader(VERTEX_SHADER);
  context.shaderSource(vertexShader, vertexShaderSource);
  context.compileShader(vertexShader);

  var fragmentShader = context.createShader(FRAGMENT_SHADER);
  context.shaderSource(fragmentShader, fragmentShaderSource);
  context.compileShader(fragmentShader);

  if(!context.getShaderParameter(vertexShader, COMPILE_STATUS)) {
    var msg = context.getShaderInfoLog(vertexShader);
    throw new Exception("Failed to compile vertex shader: $msg");
  }

  if(!context.getShaderParameter(fragmentShader, COMPILE_STATUS)) {
    var msg = context.getShaderInfoLog(fragmentShader);
    throw new Exception("Failed to compile fragment shader: $msg");
  }

  var program = context.createProgram();
  context.attachShader(program, vertexShader);
  context.attachShader(program, fragmentShader);
  context.linkProgram(program);

  if(!context.getProgramParameter(program, LINK_STATUS)) {
    var msg = context.getProgramInfoLog(program);
    throw new Exception("Failed to link program: $msg");
  }

  context.useProgram(program);

  /*
   * デプステストなどを有効にする。
   */
  context.enable(DEPTH_TEST); // デプステスト

  /*
   * uniform変数を設定する。
   */
  // Model行列
  var model = new Matrix4.identity();

  // View行列は変化させるので、ここでは省略。
  // uniform変数のlocationだけ取得しておく。

  // Projection行列
  var fovY = 60 * PI / 180;
  var aspectRatio = WIDTH / HEIGHT;
  var projection = makePerspectiveMatrix(fovY, aspectRatio, 30, 300);

  // uniform変数を設定する。
  UniformLocation modelLocation      = context.getUniformLocation(program, "model");
  UniformLocation viewLocation       = context.getUniformLocation(program, "view");
  UniformLocation projectionLocation = context.getUniformLocation(program, "projection");
  context.uniformMatrix4fv(modelLocation, false, model.storage);
  context.uniformMatrix4fv(projectionLocation, false, projection.storage);

  /*
   * バッファの設定。
   */
  var vertexBuffer = context.createBuffer();
  var indexBuffer  = context.createBuffer();

  const POSITION_SIZE   = 3;
  const COLOR_SIZE      = 3;
  const STRIDE          = Float32List.BYTES_PER_ELEMENT * (POSITION_SIZE + COLOR_SIZE);
  const POSITION_OFFSET = 0;
  const COLOR_OFFSET    = Float32List.BYTES_PER_ELEMENT * POSITION_SIZE;

  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  var positionLocation = context.getAttribLocation(program, "vertexPosition");
  var colorLocation    = context.getAttribLocation(program, "color");
  context.enableVertexAttribArray(positionLocation);
  context.enableVertexAttribArray(colorLocation);
  context.vertexAttribPointer(positionLocation, POSITION_SIZE, FLOAT, false, STRIDE, POSITION_OFFSET);
  context.vertexAttribPointer(colorLocation, COLOR_SIZE, FLOAT, false, STRIDE, COLOR_OFFSET);

  /*
   * 描画データ。
   */
  var vertices = new Float32List.fromList(<double>[-30.0, 30.0, 0.0,  // 座標
                                                   0.0, 1.0, 0.0,     // 色
                                                   -30.0, -30.0, 0.0,
                                                   1.0, 0.0, 0.0,
                                                   30.0, 30.0, 0.0,
                                                   1.0, 0.0, 0.0,
                                                   30.0, -30.0, 0.0,
                                                   0.0, 0.0, 1.0,]);

  var indices = new Uint16List.fromList(<int>[0, 1, 2, 1, 3, 2]);

  // あらかじめデータの転送だけしておく。描画はしない。
  context.bindBuffer(ARRAY_BUFFER, vertexBuffer);
  context.bufferDataTyped(ARRAY_BUFFER, vertices, STATIC_DRAW);

  context.bindBuffer(ELEMENT_ARRAY_BUFFER, indexBuffer);
  context.bufferDataTyped(ELEMENT_ARRAY_BUFFER, indices, STATIC_DRAW);

  /*
   * ここから今回のコードを付け足していく。
   */

  var radian = 0.0;
  var radius = 100.0;

  void animate(double timestamp) {
    // 更新
    radian += 1.0 * PI / 180;
    var cameraPosition = new Vector3(sin(radian)*radius, 100.0, cos(radian)*radius);
    var lookAtPosition = new Vector3.zero();
    var upDirection    = new Vector3(0.0, 1.0, 0.0);
    var view  = makeViewMatrix(cameraPosition, lookAtPosition, upDirection);
    context.uniformMatrix4fv(viewLocation, false, view.storage);

    // 前フレームの内容をクリア
    context.clear(COLOR_BUFFER_BIT | DEPTH_BUFFER_BIT);

    // 描画
    context.drawElements(TRIANGLES, indices.length, UNSIGNED_SHORT, 0);
    window.requestAnimationFrame(animate); // 次のフレーム描画をリクエスト
  }

  window.requestAnimationFrame(animate);
}