HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う

第16回 3次元空間で座標を回す

この記事を読むのに必要な時間:およそ 7 分

遠近法の投影(透視投影)

コンピュータグラフィックス(CG)の3次元座標空間の描画は,最終的に2次元平面のスクリーンに対して行われる。したがって,3次元空間で計算した座標に遠近法の効果を加えたうえで,スクリーンに投影するという処理になる。この処理を英語で"perspective projection"と呼び,⁠遠近法投影」とか透視投影と訳される。

遠近法では,奥行き(z座標)が遠いものほど小さく,また距離も縮めて表現する(デザインの世界では「パース」といわれる⁠⁠。そして,かぎりなく遠ざかるにつれ,ある1点に向かって小さくなり,やがて見えなくなる。この点を「消失点」と呼ぶ図4⁠。

図4 遠ざかるオブジェクトは消失点に向けて小さくなる

図4 遠ざかるオブジェクトは消失点に向けて小さくなる

また,カメラのレンズでは「焦点距離」ということばが使われる。たとえば,広角レンズは焦点距離が短く,同じ距離でも広い範囲が撮影できる。他方,焦点距離の長い望遠レンズは,写せる範囲は狭くなるものの,遠くのものを近寄せる。それだけでなく,画角は遠近感にも影響を及ぼす。

ふたつのものの奥行きの距離は,レンズが広角になり,焦点距離が短くなるほど,離れて見えるようになる。これを,遠近感の「誇張効果」という。逆に,焦点距離の長い望遠レンズでは,肉眼よりも距離が近づいて見える図5⁠。一眼レフカメラでは,撮影する範囲や距離だけでなく,このような遠近感の効果も考えてレンズを選ぶ※2⁠。

図5 焦点距離による表現の違い
短い焦点距離

図5 短い焦点距離


長い焦点距離

図5 長い焦点距離

焦点距離は,3次元座標空間における奥行きとなるz軸座標の視点から,投影面(スクリーン)までの距離を表す。焦点距離が長いほど,z位置の離れた(奥の)オブジェクトは,投影像がスクリーンに相対的に大きく表示される。また,視野角は視点から投影面全体を見た角度だ図6⁠。したがって,焦点距離は,視野角と連動する。焦点距離を操作すれば,視野角もそれに応じて変わることになる。

図6 z軸における焦点距離と視野角

図6 z軸における焦点距離と視野角

実際のオブジェクトと投影像をそれぞれ底辺とし,ともに視点が頂点となる相似なふたつの三角形から,比率がつぎのように求められる。

投影像の大きさ/オブジェクトの大きさ = 焦点距離 / (焦点距離 + z位置)
投影像の大きさ = オブジェクトの大きさ×焦点距離 / (焦点距離 + z位置)

したがって,3次元空間のz座標値に応じてxy座標値を2次元平面に透視投影する比率はつぎのとおりだ。予め焦点距離を定めたうえで,この投影比率をxy座標に乗じれば,透視投影された座標が定まる。

透視投影比率 = 焦点距離 / (焦点距離 + z位置)
※2
レンズの焦点距離による遠近感の効果の違いについては,デジタル一眼レフカメラ "α":アルファ背景と遠近感が参考になるだろう。

3次元空間座標を透視投影する

前掲コード1に,透視投影するための新たな関数(getProjetedPoint())を加えよう。引数は焦点距離と3次元座標のオブジェクトのふたつだ。投影された2次元座標をPointオブジェクトで返す。

焦点距離は変数(focalLength)に定めた。透視投影する関数(getProjetedPoint())は,アニメーションのリスナー関数(rotate())から,各3次元座標(point)を回転するごとに呼び出す。気をつけなければならないのは,透視投影された座標は2次元平面に描くための値で,3次元座標とは別ということだ。そのため投影後のPointオブジェクトを入れる配列を別に変数(points2D)として定めた。

透視投影の関数(getProjetedPoint())は,引数に受取った3次元座標のオブジェクト(_point3D)から,前項で示した式の比率(w)にもとづいてxy座標を求める。そして,座標値を新たなPointオブジェクト(point2D)に定めて返している。戻された値は,アニメーションの関数(rotate())が投影座標の配列(points2D)にすべて納めたうえで,線描の関数(draw())に渡されて透視投影した星形が描かれることになる。

var points2D = [];
var focalLength = 300;

function rotate(eventObject) {
  var count = points.length;
  points2D.length = 0;
  matrix.identity().rotate(angle);
  for (var i = 0; i < count; i++) {
    var point = points[i];
    matrix.transformPoint(point.x, point.z, _point);
    point.x = _point.x;
    point.z = _point.y;
    points2D[i] = getProjetedPoint(focalLength, point);
  }
  // draw(points);
  draw(points2D);
}

function getProjetedPoint(focalLength, _point3D) {
  var point2D = new createjs.Point();
  var w = focalLength / (focalLength + _point3D.z);
  point2D.x = _point3D.x * w;
  point2D.y = _point3D.y * w;
  return point2D;
}

なお,アニメーションのリスナー関数(rotate())で投影座標の配列(points2D)Array.lengthプロパティを0にしているのは,エレメントが入っていない空っぽの状態にするためだ。もっとも今回のお題では,エレメント数が決まっていて上書きされるため,必ずしも空にしなくても構わない。ただ,このように配列を初期化しておけば,後でエレメント数が変わっても大丈夫だ。

前掲コード1にこれらの遠近法の計算を加えたのが,つぎのコード2だ。これで,水平に回る星のアニメーションに遠近感が与えられる(図7⁠⁠。水平の伸び縮みではなく,回っているという感じがするだろう。

図7 遠近法が投影された回る星のアニメーション

図7 遠近法が投影された回る星のアニメーション 図7 遠近法が投影された回る星のアニメーション

コード2 星形の3次元座標に遠近法を加えてy軸で回すアニメーション

var stage;
var drawGraphics;
var points;
var angle = Math.PI / 36;
var matrix = new createjs.Matrix2D();
var stageCenterX;
var _point = new createjs.Point();
var points2D = [];
var focalLength = 300;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageCenterX = canvasElement.width / 2;
  drawGraphics = createGraphics(stageCenterX, canvasElement.height / 2);
  points = createStarPoints(5, 65, 25);
  draw(points);
  createjs.Ticker.addEventListener("tick", rotate);
  stage.addEventListener("stagemousemove", setAngle);
}
function setAngle(eventObject) {
  var mouseX = eventObject.stageX;
  angle = (mouseX - stageCenterX) * 1 / 300;
}
function rotate(eventObject) {
  var count = points.length;
  points2D.length = 0;
  matrix.identity().rotate(angle);
  for (var i = 0; i < count; i++) {
    var point = points[i];
    matrix.transformPoint(point.x, point.z, _point);
    point.x = _point.x;
    point.z = _point.y;
    points2D[i] = getProjetedPoint(focalLength, point);
  }
  draw(points2D);
}
function createGraphics(x, y) {
  var drawShape = new createjs.Shape();
  drawShape.x = x;
  drawShape.y = y;
  stage.addChild(drawShape);
  return drawShape.graphics;
}
function draw(points) {
  var count = points.length;
  var point = points[count - 1];
  drawGraphics.clear()
  .beginStroke("mediumblue")
  .setStrokeStyle(3)
  .moveTo(point.x, point.y);
  for (var i = 0; i < count; i++) {
    point = points[i];
    drawGraphics.lineTo(point.x, point.y);
  }
  stage.update();
}
function createStarPoints(numVertices, longRadius, shortRadius) {
  var starPoints = [];
  var angle = Math.PI;
  var theta = angle / numVertices;
  angle /= -2;
  for (var i = 0; i < numVertices; i++) {
    starPoints.push(newPoint3D(longRadius * Math.cos(angle), longRadius * Math.sin(angle), 0));
    angle += theta;
    starPoints.push(newPoint3D(shortRadius * Math.cos(angle), shortRadius * Math.sin(angle), 0));
    angle += theta;
  }
  return starPoints;
}
function getProjetedPoint(focalLength, _point3D) {
  var point2D = new createjs.Point();
  var w = focalLength / (focalLength + _point3D.z);
  point2D.x = _point3D.x * w;
  point2D.y = _point3D.y * w;
  return point2D;
}
function newPoint3D(x, y, z) {
  var point3D = {x:x, y:y, z:z};
  return point3D;
}

前掲コード2で焦点距離(focalLength)の値をどう決めたらよいか,疑問に思われた読者もおられよう。結論から申し上げれば,実際に試して各自の判断で決める。ただ,その際ステージの大きさを基準に考えるとよい。

コード2の値(300)はステージ幅(240)より気持ち大きめだ。もっと焦点距離が長くなれば,遠近感は弱まる図8左⁠⁠。あまり短くすると,遠近感の強い歪んだ表現になってくる図8右⁠⁠。

図8 焦点距離による遠近感の違い
焦点距離500

図8 焦点距離500


焦点距離100

図8 焦点距離100

今回のお題の表現は,これででき上がりだ。jsdo.itにもサンプルコードを掲げた。次回は,スクリプトの組立てに手を入れて仕上げたい。具体的には,ごく簡単なクラスを定義してみる。

著者プロフィール

野中文雄(のなかふみお)

ソフトウェアトレーナー,テクニカルライター,オーサリングエンジニア。上智大学法学部卒,慶応義塾大学大学院経営管理研究科修士課程修了(MBA)。独立系パソコン販売会社で,総務・人事,企画,外資系企業担当営業などに携わる。その後,マルチメディアコンテンツ制作会社に転職。ソフトウェアトレーニング,コンテンツ制作などの業務を担当する。2001年11月に独立。Web制作者に向けた情報発信プロジェクトF-siteにも参加する。株式会社ロクナナ取締役(非常勤)。

URLhttp://www.FumioNonaka.com/

著書