基礎編、3D編では静止画の描画方法について解説した。今回はそれをアニメーションさせてみよう。
DartでWebGL入門
アニメーションの手順
だいたい以下の通りの手順になる。
- データを更新する
- 前回の描画データを削除する
- 新しいデータを描画する
- これを繰り返す
新しい要素はほとんどないのでサクッと終わらせよう。
アニメーションのために使用するメソッド
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);
デモ
コード全体
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);
}