原理で3DCG つまづくポイント

※ 日記 「月別2015/08.html」 から抜粋、再編集

h=x*(s/z), v=y*(s/z) という中学校数学の文字式を使えば3次元座標 x, y, z を2次元座標 h, v に変換して簡単に3DCGプログラミングを実現できます。その式によって描かれる3DCGは普通の3DCGモデリングソフトと同じ構図となるので習得しない手はありません。しかし、簡単とはいえ、実はつまづくポイントがいくつかあるので説明したいと思います。(4つ説明します)

「この図が表示できない!」と嘆くところから話は始まります。ページの最後にはこの図が表示できるようになります。


test.js

元のモデルはこれです。Shade3Dで作成したものです。(下図)

Shade3DなどのモデリングソフトからJavaScriptのデータを作成する手順はこちらを参照してください。(モデリングソフトがFBXを出力できることと、私が作ったツールが必要です。)

3Dモデル「FBX形式 → JavaScript等」変換ツール


それではいってみよう。


つまづくポイント1 左上に描かれてしまう 

以下のプログラムは、単純に中学校数学の文字式 h=x*(s/z), v=y*(s/z) を使って3DCGを3DCGを描くプログラムです。式の結果の h, v をそのままCANVASの描画関数に渡しています。

このファイル: _res/step-001.html

<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<title>3DCG</title>
<script>
var tens = new Array();
var mens = new Array();
</script>
<!--3Dモデル読込-->
<script src="test.js"></script>
<script>

var s = 50;
var canvas;


//プログラムのメインルーチン
function onloadx() {
var canvasEL = document.getElementById( "canvasELID" );
canvas = canvasEL.getContext( '2d' );

draw();
}


function draw() {

//白で画面クリア
canvas.fillStyle = "white";
canvas.fillRect( 0, 0, 640, 480 );


//モデルの各面(ポリゴン)ごとに描く
for( var j = 0; j < mens.length; j++ ) {
var thisMenKouseiTens = mens[ j ];
canvas.beginPath();
	
//CANVASのパスを使って描く

//その面を構成する各頂点を順にたどる。
for( var i = 0; i < thisMenKouseiTens.length; i++ ) {

//その頂点の x, y, z を取り出す。
var tenNumber = thisMenKouseiTens[ i ];
var x = tens[ tenNumber ][ 0 ];
var y = tens[ tenNumber ][ 1 ];
var z = tens[ tenNumber ][ 2 ];

//中学校数学の文字式を使えば
//3次元座標 x, y, z を2次元座標 h, v に変換できる。
var h=x*(s/z);
var v=y*(s/z);

//パスで描く。最初の頂点はmoveTo()、続く頂点はlineTo()。
if( i == 0 ) canvas.moveTo( h, v ); else canvas.lineTo( h, v );
}

canvas.closePath();

//線を引く。ワイヤーフレームとなる。
canvas.strokeStyle = "green";
canvas.stroke();
}
}

</script>
</head>
<body onload="onloadx();">
<canvas id="canvasELID" width="640" height="480" style="border:solid 1px black;"></canvas>
</body>
</html>

 新しいウィンドウで実行  [小WINで表示]


式の計算結果の h, v をそのまま加工せずに描くと左図のようになります。

あれっできん!

左上に小さく何か描かれています。

いくつか問題がありますが、まずはどうして左上に描かれてしまうのか考えてみよう。


数学の式をグラフにするとき、原点 0, 0 を中心に線を書きますよね?

プログラムでは画面の左上が 0, 0 の原点となるので、何もしないと左上を中心に描いてしまいます。

これは今回に限らず、ほかの数学関数の sin(), cos() 関数などでも同じです。

そういうときは計算結果に画面のサイズの半分を足して描くものを画面の中央に持ってくるようにします。

画面サイズが 640 x 480 だとすると、

h = h + 320、v = v + 240

とします。

その変更をしたのが下のプログラムです。

※マーカーで塗った部分が前のプログラムと比べて変更した部分です。


比較ファイル: _res/step-001.html
このファイル: _res/step-002.html

<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<title>3DCG</title>
<script>
var tens = new Array();
var mens = new Array();
</script>
<!--3Dモデル読込-->
<script src="test.js"></script>
<script>

var s = 50;
var canvas;


//プログラムのメインルーチン
function onloadx() {
var canvasEL = document.getElementById( "canvasELID" );
canvas = canvasEL.getContext( '2d' );

draw();
}


function draw() {

//白で画面クリア
canvas.fillStyle = "white";
canvas.fillRect( 0, 0, 640, 480 );


//モデルの各面(ポリゴン)ごとに描く
for( var j = 0; j < mens.length; j++ ) {
var thisMenKouseiTens = mens[ j ];
canvas.beginPath();
	
//CANVASのパスを使って描く

//その面を構成する各頂点を順にたどる。
for( var i = 0; i < thisMenKouseiTens.length; i++ ) {

//その頂点の x, y, z を取り出す。
var tenNumber = thisMenKouseiTens[ i ];
var x = tens[ tenNumber ][ 0 ];
var y = tens[ tenNumber ][ 1 ];
var z = tens[ tenNumber ][ 2 ];

//中学校数学の文字式を使えば
//3次元座標 x, y, z を2次元座標 h, v に変換できる。
var h=x*(s/z);
var v=y*(s/z);

//画面のサイズの半分を足して描くものを画面の中央に持ってくる
h = h + 320;
v = v + 240;

//パスで描く。最初の頂点はmoveTo()、続く頂点はlineTo()。
if( i == 0 ) canvas.moveTo( h, v ); else canvas.lineTo( h, v );
}

canvas.closePath();

//線を引く。ワイヤーフレームとなる。
canvas.strokeStyle = "green";
canvas.stroke();
}
}

</script>
</head>
<body onload="onloadx();">
<canvas id="canvasELID" width="640" height="480" style="border:solid 1px black;"></canvas>
</body>
</html>

 新しいウィンドウで実行  [小WINで表示]

すると左図のようになります。

中央に持ってこれましたが、それとは別の話でちょっとおかしな図になっています。

んん~できん! 

単純に h=x*(s/z), v=y*(s/z) の式を使っただけではダメだというのがわかります。

何がいけないのでしょうか?

 つまづくポイント2 物体の中心が目の位置に重なっている

作成した3Dモデルは x, y, z の点の集まりです。その各点は物体の中心を原点 0, 0, 0 としたときの値です。


▲車は車の中心を原点(図の赤球)にして作成している

ところで、3D空間において「Z座標が0」というのは、画面のどの位置になるでしょうか?

3DCGの画面というのは人間の目で見た様子を描いたものなので、人間の目をもとにして考えてみましょう。

あなたの目にとって、Z座標(おくゆき)が0というのはどの位置でしょうか?

答えは「目の位置」です。

Z座標の単位が仮にcmだとして、「Z座標が10cm」の場所は、目から前方に10cm離れた場所ということです。「Z座標が0」ならそれは目と同じ位置です。

今回のプログラムでは h=x*(s/z), v=y*(s/z) の式で描いたときに「物体が目からどれだけ離れているか」という情報を考慮していないので、物体は「Z座標が0」の状態になっており、目の位置に物体の中心が重なるように描かれてしまっています。(下図)


▲正六面体を原点に描いたときのようす

目と物体が重なっているのは問題ですが、さらに問題なのは、この図の目よりも後ろにある4つの赤い頂点です。

この赤い頂点は目の後ろにあるので目には見えないはずで画面にも本来表示されないはずです。しかしプログラムは正直に計算して答えを出して描いてしまいます。

目が原点ならば、目の前のすべての物体は目を基準に考えて前方にあるから正(プラス)の方向に奥行き(Z座標)があると言えます。それに対し、目の後ろの物体は負(マイナス)の方向に奥行きがあると言えます。

難しいかもしれませんが、

たとえば、2つの点があって、その x 座標と y 座標はともに 100 で、z 座標だけが 100 と -100 で異なっているとき、

h=x*(s/z), v=y*(s/z) (※sは50とする)

この式で2つの点の画面上の座標 h, v を計算してみると、

Z座標が 100 のときは、h = 100 * ( 50 / 100 ) = 50、v = 100 * ( 50 / 100 ) = 50

Z座標が -100 のときは、h = 100 * ( 50 / -100 ) = -50、v = 100 * ( 50 / -100 ) = -50

で、画面座標は h, v ともに±が反転します。

これをわかりやすく図解すると、変換前の3次元の x, y, z の時点では下図ような正しい状態ですが、



変換の式を使って2次元の h, v にすると下図のように原点の後方の頂点は位置がおかしくなります。



横から見るとこんな感じにねじれていると言っていいでしょう。

後方  前方

プログラムでは上記の模式図とは違い、さらに画角(s)が関係して目に近い部分が大きくなっています。(ペットの鼻デカ写真のようにレンズに近い部分(車のバンパー?)だけ強調されます)

それが、プログラムの結果である下図ということです。一部がクロスしているように見えます。車輪も前車輪と後車輪がそれぞれ上下逆になっています。車の半分がひっくり返っているんです。

(余談ですが、目の構造や、カメラの構造に詳しい方は知っていると思いますが、風景がレンズを通過するとき、さかさまになって、目の網膜やフィルムに投影されるので、あながちおかしな図というわけではないかもしれません)

おかしな図になった原因はこれでわかってもらえたと思いますが、これを直すにはどうすればいいのでしょうか?


物体に位置情報を付けて、3次元空間の中で物体を x, y, z 自由な場所に移動できるようにして、Z座標を目から遠ざけるように設定します。

※マーカーで塗った部分が前のプログラムから変更した部分です。

比較ファイル: _res/step-002.html
このファイル: _res/step-003.html

<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<title>3DCG</title>
<script>
var tens = new Array();
var mens = new Array();
</script>
<!--3Dモデル読込-->
<script src="test.js"></script>
<script>

//物体に位置情報を付け、Z座標を目から遠ざける
var modelPos = new Object();
modelPos.x = 0;
modelPos.y = 0;
modelPos.z = 500;

var s = 50;
var canvas;


//プログラムのメインルーチン
function onloadx() {
var canvasEL = document.getElementById( "canvasELID" );
canvas = canvasEL.getContext( '2d' );

draw();
}


function draw() {

//白で画面クリア
canvas.fillStyle = "white";
canvas.fillRect( 0, 0, 640, 480 );


//モデルの各面(ポリゴン)ごとに描く
for( var j = 0; j < mens.length; j++ ) {
var thisMenKouseiTens = mens[ j ];
canvas.beginPath();
	
//CANVASのパスを使って描く

//その面を構成する各頂点を順にたどる。
for( var i = 0; i < thisMenKouseiTens.length; i++ ) {

//その頂点の x, y, z を取り出す。
var tenNumber = thisMenKouseiTens[ i ];
var x = tens[ tenNumber ][ 0 ];
var y = tens[ tenNumber ][ 1 ];
var z = tens[ tenNumber ][ 2 ];

//位置情報を適用
x = x + modelPos.x;
y = y + modelPos.y;
z = z + modelPos.z;

//中学校数学の文字式を使えば
//3次元座標 x, y, z を2次元座標 h, v に変換できる。
var h=x*(s/z);
var v=y*(s/z);

//画面のサイズの半分を足して描くものを画面の中央に持ってくる
h = h + 320;
v = v + 240;

//パスで描く。最初の頂点はmoveTo()、続く頂点はlineTo()。
if( i == 0 ) canvas.moveTo( h, v ); else canvas.lineTo( h, v );
}

canvas.closePath();

//線を引く。ワイヤーフレームとなる。
canvas.strokeStyle = "green";
canvas.stroke();
}
}

</script>
</head>
<body onload="onloadx();">
<canvas id="canvasELID" width="640" height="480" style="border:solid 1px black;"></canvas>
</body>
</html>

 新しいウィンドウで実行  [小WINで表示]

すると今度は物体が小さくなってしまいました。

あれっできん!

図が小さいので拡大する必要があります。

つまづくポイント3 物体を拡大したい


見えるものを拡大するには4つの方法が考えられます。

ひとつひとつ試していきましょう。


物体を拡大したい 1: 物体そのものを大きくする方法



比較プログラムを見る。

結果は左図です。

これは車のバンパー(?)が長方形に大きく強調され、ペットの鼻デカ写真の状態になっています。

拡大したいだけなのに、歪んでしまっては困ります。この方法はここでは使えません。

ちなみに、一部がこういうふうに


とがって変な形に見えている理由は、shade3DがFBXテキストを出力した際に、


側面をこのように分割したからです。


物体を拡大したい 2: 物体を視点に近づけて大きく見せる方法



比較プログラムを見る。

結果は左図です。

これもペットの鼻デカ写真になっています。この方法はここでは使えません。

ちなみに1の方法と、この2の方法は、変更した箇所は違っていても、処理としては同一なので結果の画像は同じになっています。

これは、遠くの1m四方の正方形と、近くの10cm四方の正方形は距離は違っていても、見える大きさとしては同一になりうる、という現実にもある現象です。


物体を拡大したい 3: 望遠レンズとして s の値を大きくする方法





比較プログラムを見る。

結果は左図です。

車がさかさまになっているようですが、拡大の結果としては良好です。

ちなみに、「s、s」と専門用語のように言っていますが、s の変数名の由来は、Wikipediaの3DCGの原理のページで s と仮に呼ばれていたのを私が日常的に使っているだけです。s とは「スクリーン座標」(screen)の s です。この値は「焦点距離」(レンズサイズ)でもあるため、shouten で偶然にも s で一致しています。


物体を拡大したい 4: 画面を引き延ばすように拡大する方法





比較プログラムを見る。

結果は左図です。

こちらも車がさかさまですが、拡大の結果としては良好です。

前の 3 の方法は s を大きくして望遠レンズをシミュレートしたものでした。

しかし今回のプログラムは望遠レンズは想定しないで、肉眼で見た様子としてプログラムしたいので、この4の方法を使うことにしましょう。

ちなみに3と4の方法も、変更した箇所は違っていても、処理としては同一で結果の画像は同じになっています。

これは中学校数学の文字式 h=x*(s/z), v=y*(s/z) をよく見るとわかります。s を2倍することと、式の右辺全体を2倍することは数学としては同じことをやっていますよね?

余談ですが、そう考えると、実際のカメラでレンズを変えることと、現像時に拡大率を変えることは、同じことをやっているのかもしれません。

では次にさかさまはどうしてさかさまになっているのか考えてみましょう。


つまづくポイント4 さかさまになっている

3Dモデルを作成(モデリング)したときは、上下であるY軸が上に行くほど増える、という座標だったのに対し(下図左)、プログラムの画面では画面の左上が原点の 0, 0 で、Y軸(V軸)は下に行くほど増えるようになっています(下図右)。

▲3Dモデルを作成(モデリング)したとき
緑色の軸がY軸。上に行くほど増える。
▲プログラムの画面
Y軸(V軸)は下に行くほど増える。

この2つのY軸の方向が逆になっているので、そのまま描くとさかさまになってしまうわけです。

ではどうすればこのさかさまを直せるのかというと、結果の v を -v にするだけで直ります。

なぜ - を付けるだけで、図形が逆になるのかというと、それは過去のドキュメントを参照してください。リンク(移動)

比較ファイル: _res/step-004_4.html
このファイル: _res/step-005.html

<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
<title>3DCG</title>
<script>
var tens = new Array();
var mens = new Array();
</script>
<!--3Dモデル読込-->
<script src="test.js"></script>
<script>

//物体に位置情報を付け、Z座標を目から遠ざける
var modelPos = new Object();
modelPos.x = 0;
modelPos.y = 0;
modelPos.z = 500;

var s = 50;
var canvas;


//プログラムのメインルーチン
function onloadx() {
var canvasEL = document.getElementById( "canvasELID" );
canvas = canvasEL.getContext( '2d' );

draw();
}


function draw() {

//白で画面クリア
canvas.fillStyle = "white";
canvas.fillRect( 0, 0, 640, 480 );


//モデルの各面(ポリゴン)ごとに描く
for( var j = 0; j < mens.length; j++ ) {
var thisMenKouseiTens = mens[ j ];
canvas.beginPath();
	
//CANVASのパスを使って描く

//その面を構成する各頂点を順にたどる。
for( var i = 0; i < thisMenKouseiTens.length; i++ ) {

//その頂点の x, y, z を取り出す。
var tenNumber = thisMenKouseiTens[ i ];
var x = tens[ tenNumber ][ 0 ];
var y = tens[ tenNumber ][ 1 ];
var z = tens[ tenNumber ][ 2 ];

//位置情報を適用
x = x + modelPos.x;
y = y + modelPos.y;
z = z + modelPos.z;

//中学校数学の文字式を使えば
//3次元座標 x, y, z を2次元座標 h, v に変換できる。
var h=x*(s/z);
var v=y*(s/z);

//画面を引き延ばす
h = h * 17.6;
v = v * 17.6;

//さかさまを直す
v = -v;

//画面のサイズの半分を足して描くものを画面の中央に持ってくる
h = h + 320;
v = v + 240;

//パスで描く。最初の頂点はmoveTo()、続く頂点はlineTo()。
if( i == 0 ) canvas.moveTo( h, v ); else canvas.lineTo( h, v );
}

canvas.closePath();

//線を引く。ワイヤーフレームとなる。
canvas.strokeStyle = "green";
canvas.stroke();
}
}

</script>
</head>
<body onload="onloadx();">
<canvas id="canvasELID" width="640" height="480" style="border:solid 1px black;"></canvas>
</body>
</html>

 新しいウィンドウで実行  [小WINで表示]

結果は左図です。

以上でようやく中学校数学の文字式 h=x*(s/z), v=y*(s/z) で3DCGが描けました。

やればできるんだよ 

最後に全体的な注意点です。

3DCGでは座標の加工という作業がわりと多くあって、加工のタイミング(計算のどの時点でその加工を行うか)を間違えるとエラーは出てないけど意図した結果にならなくて、そういう直すのが難しいバグを出しがちです。

今回の加工順。

  1. 小さい画面を大きくするため引き延ばす。
  2. さかさまを直すために -v にする。
  3. 中央に描くために h, v に画面の半分を足す。

この1と2はどちらも h, v に対する掛け算なので、数学の式としては順序が入れ替わっても問題ありません(2x3x4と2x4x3は同じです)が、3は1や2よりも先にやってしまうと、h, v が大きくなって画面外に描かれ、白紙という結果になることが多いです。

まとめ

画面が思ったようにならないときは、以下のような点を修正します。

以上でおわりです。お疲れさまでした。


ページの上端へ (もくじ開く)