Skip to content

Basic‐Tutorials‐Japanese

NAS6mixfoolv edited this page Jul 5, 2026 · 8 revisions

プロジェクトテンプレートと基本チュートリアル

目次に戻る

コンソールでNAS6LIBの基本操作をテストできるシンプルなページとファイルセットです。
Three.jsやX3DOMなどの3Dレンダリングライブラリとの最小限の統合サンプルも含まれています。
コメントを有効にし、テストコードを追加してご利用ください。
TestPage ZipFile (External Link)
testpage000 DEMO (External Link)
test3JS000 DEMO (External Link)
testX3DOM000 DEMO (External Link)


インストールとセットアップガイド

NAS6LIBは他の既存ライブラリに依存しません。
NAS6LIBはほぼスタンドアロンで使用できるように設計されていますが、JQueryプロパティアクセス、X3DOM、ThreeJSの
高度なレンダリング機能と併用すると、より便利に使用できます。

最小チュートリアル に示されているように、NAS6LIB の各モジュールの *.js ファイルは、
script タグを使用して外部ファイルを読み込むことで使用できます。


最小チュートリアル

プロジェクトテンプレートを表示するには、少し上にスクロールしてください。

  • クイックスタート

  • 1. ライブラリを読み込む

必要な JavaScript ファイルを HTML に含めます。
特定のユースケースに応じて、vector.js、matrix.js、quaternion.js、planet.js などの他のモジュールを追加します 。

HTML

<script src="https://nas6mixfoolv.github.io/NAS6LIB/javascripts/nas6lib/vector.js"></script>
  • 2.最小サンプル (2D ベクトル加算)
    まずは簡単な JavaScript の例をご覧ください。

JavaScript

var v1 = new N6LVector([1, 2]);
var v2 = new N6LVector([3, 4]);
var v3 = v1.Add(v2);
console.log(v3.x); // 結果: [4, 6]

行列とベクトル(基本演算)

目次に戻る

* N6LMatrix と N6LVector の基本

ライブラリの読み込み

HTML

<script src="https://nas6mixfoolv.github.io/NAS6LIB/javascripts/nas6lib/vector.js"></script>
<script src="https://nas6mixfoolv.github.io/NAS6LIB/javascripts/nas6lib/matrix.js"></script>
<script src="https://nas6mixfoolv.github.io/NAS6LIB/javascripts/nas6lib/quaternion.js"></script>

* 作成、初期化

目次に戻る

要素の順序はw-x-y-z-...

JavaScript

//4次ベクトル
var veca = new N6LVector(4);
//同次4次ベクトル
var vecb = new N6LVector(4, true);
//3次ベクトル
var vecc = new N6LVector(new Array(1, 2, 3));
//x軸単位同次4次ベクトル
var vecd = new N6LVector(new Array(1, 1, 0, 0), true);
var vece = new N6LVector([1, 1, 0, 0], true);
var vecf = new N6LVector(4, true).UnitVec(1);
//ゼロホモ4次ベクトル
var vecg = new N6LVector(new Array(1, 0, 0, 0), true);
var vech = new N6LVector([1, 0, 0, 0], true);
var veci = new N6LVector(4, true).ZeroVec();
//ディープコピー
var vecf = new N6LVector(veci);

//4行4列
var mata = new N6LMatrix(4);
//4行8列
var matb = new N6LMatrix(4, 8);
//4行4列の単位行列
var matc = new N6LMatrix(new Array(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1), 4, 4);
var matd = new N6LMatrix(new Array(
new N6LVector(new Array(1, 0, 0, 0)),
new N6LVector(new Array(0, 1, 0, 0)),

new N6LVector(new Array(0, 0, 1, 0)),

new N6LVector(new Array(0, 0, 0, 1)) ));
var mate = new N6LMatrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] );
//ディープコピー
var matf = new N6LMatrix(mate);
/*
注:4次以上は同次座標とみなされます。
4次以上の従来の座標を作成したい場合は、N6LMatrix.SetHomo(false) とします**
宣言の構築を続行してください。
*/

* 加算、減算、乗算(ベクトルと行列、行列と行列)、除算(便宜上)

目次に戻る

JavaScript

//ベクトル
/*
bHomo=true の場合、w 要素はスキップされます。
*/

//[1, 2] + [3, 4] = [4, 6]
var v1 = new N6LVector([1, 2]);
var v2 = new N6LVector([3, 4]);
var v3 = v1.Add(v2);
console.log(v3.x); // 結果: [4, 6]
//[1, 2] + 3 = [4, 5]
var v4 = v1.Add(3);
console.log(v4.x); // 結果: [4, 5]

//[3, 4] - [1, 2] = [2, 2]
var v5 = v2.Sub(v1);
console.log(v5.x); // 結果: [2, 2]
//[3, 4] - 2 = [1, 2]
var v6 = v2.Sub(2);
console.log(v6.x); // 結果: [1, 2]

//[1, 2] * [3, 4] = 11
var n1 = v1.Mul(v2);
console.log(n1); // 結果: 11
//[1, 2] * [[1, 2], [3, 4]] = [7, 10]
var m1 = new N6LMatrix([[1, 2], [3, 4]]);
var v7 = v1.Mul(m1);
console.log(v7.x); // 結果: [7, 10]
//[1, 2] * 2 = [2, 4]
var v8 = v1.Mul(2);
console.log(v8.x); // 結果: [2, 4]

//[3, 4] / [1, 2] = 5
var n2 = v2.Div(v1);
console.log(n2); // 結果: 5
//[3, 4] / [[1, 2], [3, 4]] = [4.333..., 2.5]
var v9 = v2.Div(m1);
console.log(v9.x); // 結果: [4.333..., 2.5]
//[3, 4] / 2 = [1.5, 2]
var v10 = v2.Div(2);
console.log(v10.x); // 結果: [1.5, 2]
//行列

//[[1, 2], [3, 4]] + [[2, 4], [6, 8]] = [[3, 6], [9, 12]]
var m2 = new N6LMatrix([[2, 4], [6, 8]]);
var m3 = m1.Add(m2);
console.log(m3.x); // 結果: [[3, 6], [9, 12]]
//[[1, 2], [3, 4]] + 2 = [[3, 4], [5, 6]]
var m4 = m1.Add(2);
console.log(m4.x); // 結果: [[3, 4], [5, 6]]

//[[2, 4], [6, 8]] - [[1, 2], [3, 4]] = [[1, 2], [3, 4]]
var m5 = m2.Sub(m1);
console.log(m5.x); // 結果: [[1, 2], [3, 4]]
//[[2, 4], [6, 8]] - 1 = [[1, 3], [5, 7]]
var m6 = m2.Sub(1);
console.log(m6.x); // 結果: [[1, 3], [5, 7]]

//[[1, 2], [3, 4]] * [[2, 4], [6, 8]] = [[14, 20], [30, 44]]
var m7 = m1.Mul(m2);
コンソール.ログ(m7.x); // 結果: [[14, 20], [30, 44]]
//[[1, 2], [3, 4]] * [1, 2] = [5, 11]
var v11 = m1.Mul(v1);
コンソール.ログ(v11.x); // 結果: [5, 11]
//[[1, 2], [3, 4]] * 2 = [[2, 4], [6, 8]]
var m8 = m1.Mul(2);
console.log(m8.x); // 結果: [[2, 4], [6, 8]]

//[[2, 4], [6, 8]] / [[1, 2], [3, 4]] = [[3.333..., 2], [8.666..., 5]]
var m9 = m2.Div(m1);
console.log(m9.x); // 結果: [[3.333..., 2], [8.666..., 5]]
//[[2, 4], [6, 8]] / [1, 2] = [4, 10]
var v12 = m2.Div(v1);
console.log(v12.x); // 結果: [4, 10]
//[[2, 4], [6, 8]] / 2 = [[1, 2], [3, 4]]
var m10 = m2.Div(2);

console.log(m10.x); // 結果: [[1, 2], [3, 4]]

* 正規化、絶対値

目次に戻る

JavaScript

//ベクター

//[3, 4].Abs() = 5
var v1 = new N6LVector([3, 4]);
var n1 = v1.Abs();
console.log(n1); // 結果: 5
//[3, 4].Normal() = [3/5=0.6, 4/5=0.8]
var v2 = v1.NormalVec();
console.log(v2.x); // 結果: [3/5=0.6, 4/5=0.8]
//[3, 4].Normal([6, 8]) = [3/5=0.6, 4/5=0.8]
var v3 = new N6LVector([6, 8]);
var v4 = v1.NormalVec(v3);
console.log(v4.x); // 結果: [3/5=0.6, 4/5=0.8]

//行列

//[[2, 0], [0, 2]].Normal() = [[1, 0], [0, 1]]
var m1 = new N6LMatrix([[2, 0], [0, 2]]);
var m2 = m1.NormalMat();
console.log(m2.x); // 結果: [[1, 0], [0, 1]]

* 内積、外積 (ベクトル)

目次に戻る

JavaScript

//ベクトル

//[3, 4].Dot([5, 6]) = 3*5+4*6=39
var v1 = new N6LVector([3, 4]);
var v2 = new N6LVector([5, 6]);
var n1 = v1.Dot(v2);
console.log(n1); // 結果: 3*5+4*6=39
//[3, 4].Cross([5, 6]) = 3*6-4*5=-2
var n2 = v1.Cross(v2);
console.log(n2); // 結果: 3*6-4*5=-2

* 転置、逆行列 (Matrix)

目次に戻る

JavaScript

//Matrix

//[[1, 0, 0, 0], [2, 0, 0, 1], [3, 0, 1, 0], [4, -1, 0, 0]].Transpose() = [[1, 2, 3, 4], [0, 0, 0, -1], [0, 0, 1, 0], [0, 1, 0, 0]]
//[[1, 0, 0, 0], [2, 0, 0, 1], [3, 0, 1, 0], [4, -1, 0, 0]].Inverse() = [[1, 0, 0, 0], [2, 0, 0, -1], [3, 0, 1, 0], [4, 1, 0, 0]]
var m1 = new N6LMatrix([[1, 0, 0, 0], [2, 0, 0, 1], [3, 0, 1, 0], [4, -1, 0, 0]]);
var m2 = m1.TransposedMat();
console.log(m2.x); // 結果: [[1, 2, 3, 4], [0, 0, 0, -1], [0, 0, 1, 0], [0, 1, 0, 0]]
var dt = [];
var m3 = m1.InverseMat(dt);
コンソールログ(m3.x); // 結果: [[1, 0, 0, 0], [2, 0, 0, -1], [3, 0, 1, 0], [4, 1, 0, 0]]

//[[0, 0, 1], [0, 1, 0], [-1, 0, 0]].Transpose() = [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
//[[0, 0, 1], [0, 1, 0], [-1, 0, 0]].Inverse() = [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
var m4 = new N6LMatrix([[0, 0, 1], [0, 1, 0], [-1, 0, 0]]);
var m5 = m4.TransposedMat();
console.log(m5.x); // 結果: [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
var m6 = m4.InverseMat(dt);
console.log(m6.x); // 結果: [[0, 0, -1], [0, 1, 0], [1, 0, 0]]

* 座標変換の基本

目次に戻る

JavaScript

//行列

//[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]].Translate([1, 2, 3, 4]) = [[1, 0, 0, 0], [2, 1, 0, 0], [3, 0, 1, 0], [4, 0, 0, 1]]
var m1 = new N6LMatrix(4).UnitMat();
var v1 = new N6LVector([1, 2, 3, 4], true);
var m2 = m1.TranslatedMat(v1);
console.log(m2.x); // 結果: [[1, 0, 0, 0], [2, 1, 0, 0], [3, 0, 1, 0], [4, 0, 0, 1]]

* 回転、平行移動、スケール行列の作成と適用

目次に戻る

JavaScript

//行列

var m1 = new N6LMatrix(4).UnitMat();
var v2 = new N6LVector([Math.PI / 2.0, 0, 1, 0], true);
var v3 = new N6LVector([1, 2, 2, 2],true);
var v4 = new N6LVector([1, 3, 4, 5],true);
var m3 = m1.AffineMat(v3, v2, v4);
console.log(m3.x); // 結果: [[1, 0, 0, 0], [3, 0, 0, 2], [4, 0, 2, 0], [5, -2, 0, 0]]
var v5 = new N6LVector([1, 0, 1, 0], true);
var m4 = m3.RotAxis(v5, -Math.PI / 2.0);
console.log(m4.x); // 結果: [[1, 0, 0, 0], [3, 2, 0, 0], [4, 0, 2, 0], [5, 0, 0, 2]]

* ローカル座標とワールド座標の変換

目次に戻る

JavaScript

//ローカル座標とワールド座標

//行列
var um = new N6LMatrix(4).UnitMat();
var ay = new N6LVector(4, true).UnitVec(2);
var lm1 = um.RotAxis(ay, Math.PI / 2.0);
var tr = new N6LVector([1, 0, 0, 5], true);
var lm2 = lm1.TranslatedMat(tr);
console.log(lm2.x); // 結果: Local[[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [5, -1, 0, 0]]
var lm3 = lm2.TranslatedMat(tr.Mul(-1));
var lm4 = lm3.RotAxis(ay, -Math.PI / 2.0);
console.log(lm4.x); // 結果: World[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]

回転行列の行優先と列優先の配置について

目次に戻る

NAS6LIB は行優先の行列配置を使用しますが、多くのレンダラーは列優先の行列配置を使用します。
また、左オペランドとなるベクトルまたは行列(lh)、右オペランドとなるベクトルまたは
行列(rh)と乗算される点にも注目してください。これは、C# マネージコードにおける演算子
オーバーロードの経験と、左オペランドのクラスエンティティからメンバー関数を呼び出す
というプログラミング規則に基づいています。

注:
NAS6LIB では、
N6LVector.Mul(N6LVector または N6LMatrix など = rh)
N6LMatrix.Mul(N6LVector または N6LMatrix など = rh) となり、
lh は格納されている行ベクトルをそのまま使用しますが、
rh は転置後の列ベクトルを使い、
それらのベクトルの乗算を実行します。

それぞれの計算用途の違いについて説明します。

回転行列には、行優先と列優先の配置があります。

簡単に説明すると、
行優先とは各ローカル軸が行順に配置されることを意味し、
列優先とは各ローカル軸が列順に配置されることを意味します。

そして数学的には、ベクトルと行列の乗算は次のように決定されます。

(ベクトルまたは行列の行の各要素) x (ベクトルまたは行列の列の各要素)

これには、ワールドシステムとビューシステムの議論も含まれます。

数学的な定義により、ワールドシステムは無変換であり、ビューシステムは
ワールドシステムの逆行列、つまり最適化された転置行列です。
もっと簡単に言うと、ワールドシステムとビューシステムは逆の関係にあり、
互いに正対しています。机の上に自分の方を向いて人形を置いた場合、
自分がビューシステム座標系にいるなら、人形の座標系はワールド座標系となり、
図形の座標系がビューシステムならば、自分がワールドシステム座標系となります。
ローカル軸の符号関係は逆転することがあります。これを事前に確認しておかないと、
逆の話をしてしまうことになります。
ここでは、一般的なビューの視点とは逆の、ワールドの視点から説明します。

Mが正則行列のとき、$M^{-1}$が存在します。

$M^\top (M^{-1})^\top = (M^{-1} M)^\top = I^\top = I$
$(M^{-1})^\top M^\top = (M M^{-1})^\top = I^\top = I$

言い換えれば、

$M ((M^\top)^{-1})^\top = ((M^\top)^{-1} M^\top)^\top = I^\top = I$
$((M^\top)^{-1})^\top M = (M^\top (M^\top)^{-1})^\top = I^\top = I$

したがって、$M^{-1} = ((M^\top)^{-1})^\top$ となり、
$M^{-1} = M^\top$ ならば、$M^{-1} = M^\top$

特に、回転行列などの直交行列の場合、逆行列は転置行列 $M^{-1} = M^\top$ に等しくなります。
この特性は、変換の計算負荷を軽減する上で非常に重要です。

ここで、レンダラーが実行する主な計算は最終画面に表示される計算であるため、
(転置された)ビューシステムを扱うのが便利です。

(ベクトルまたは行列の行の各要素)と(ベクトルまたは行列の列の各要素)の定義に戻ると、

行列 M が左オペランド、つまり Mv や MN などの演算の場合、
M は(ベクトルまたは行列の行の各要素)であり、それを何かの列要素で乗算するため、行優先の配置が便利です。

つまり、Mに着目してその配置を考えると、
行優先配置:$Mv$
列優先配置:$vM^\top$
これは世界から見た話です。

これを前提として、
$M^{-1} = M^\top$
$M^\top N^\top = (N M)^\top$

レンダラーがこれではなく列優先を採用する理由は、前述のように、
最終画面に表示される計算であるため、(転置された)ビューシステムだからです。
これがビューマトリックスが語る物語です。
行優先配置:$M^\top v^\top = (v M)^\top = v' M'$
列優先配置:$v M = (M v) = M'^\top v'^\top$
これは誤解しやすい概念なので、以下の点をよく考えてください。

レンダラーは最終的に画面に表示される計算を行うため、
ビュー行列を主に扱うレンダラーは
$M' = M^{-1} = M^\top$ を計算で
主に扱うのが便利ですが、ワールド行列を主に扱う場合は逆の関係が便利です。

簡単に言うと、レンダラーの主な機能はビューを計算することです。
w 行列なので、ワールド行列の視点から見ると、レンダラーの計算は転置された逆行列になります。

3D グラフィックスにおいて、しばしば混乱を招く重要な「逆関係」を以下に示します。

  1. ビュー行列とワールド行列:ビュー行列はワールド行列の逆行列であり、
    直交行列(回転行列など)の場合は転置行列に相当します。
  • 1. の意味:ビューシステムとワールドシステムは、逆の「対面」関係にあります。
    机の上に、自分の方を向いて人物を置いたと想像してください。
    そのシーンをビューシステムの視点(自分の目)から描写する場合、人物の座標系はワールド座標系です。
    逆に、ワールドフレーム視点(デスク固有の座標系)からシーンを定義すると、
    ワールドフレーム視点が視点フレームになり、目の座標系がワールドフレームになります。
    現在検討しているシステムによっては、ローカル軸の符号が逆に見える場合があります。
    まずどちらの視点(ワールドまたはビュー)について議論しているのかを確認することが重要です。
    そうしないと、理解が全く逆になる可能性があります。
  1. 左手座標系と右手座標系:これらの座標系は、特にZ軸の方向に関して、逆の関係にあります。
  2. 乗算と転置の順序:ベクトルまたは行列を別の行列に乗算する順序は、
    規則(行優先と列優先、乗算前と乗算後など)に応じて、転置関係になることがよくあります。

NAS6LIB の実装に関する注意:
NAS6LIB では、次のような演算において、
N6LVector.Mul(N6LVector or N6LMatrix etc = rh) や
N6LMatrix.Mul(N6LVector or N6LMatrix etc = rh) が実行されます。
左辺のオペランド (lh、メソッドを呼び出すオブジェクト) は、格納されている行ベクトルを直接使用します。
右辺のオペランド (rh) は、乗算の前に内部的に転置され、列ベクトルが取得されます。
そして、乗算はこれらのベクトル間のドット積として実行されます。

これらの点を考慮して、簡略化した概念を以下に示します。

順方向変換: $v'(out) = ROTA \{ M * v(in) \}$
↑ 開発者/ライブラリ利用者の観点からの説明は、主にワールド行列に焦点を当てています。
逆変換: $v(in) = ROTA^{-1} \{ M^\top * v'(out) \}$
↑ レンダラーの観点からの説明は、主にビュー行列に焦点を当てています。

順変換: $v'(out) = ROTB \{ v(in) * M^\top \}$
逆変換: $v(in) = ROTB^{-1} \{ v'(out) * M \}$

ROTAとROTBの違いは、引数入力とメイン行列の乗算が右から(後乗算)行われるか、
左から(前乗算)行われるかです。順変換と逆変換の違いは、出力が入力から計算されるか(順変換)、
それとも出力から入力が推測されるか(逆変換)です。

これらのうち、ビュー行列ベースの逆変換はレンダラーの観点からは便利であり、
ワールド行列ベースの順変換はライブラリユーザーの観点からは便利です。
そのため、ThreeJSとX3DOMの両方でカメラやオブジェクトのワールド行列を取得できるにもかかわらず、
レンダラーが提供する行列乗算などの操作用APIは、多くの場合、
ワールドではなくビューを主に対象として設計されています。そのため、逆行列(つまり転置)を計算し、
必要に応じて再度転置する方が効率的であることがよくあります。
この理解は、プログラマブルシェーダーを作成する際に非常に役立ちます。

レンダラーが列優先の行列配置を採用する理由は、

逆変換: v(in) = ROTA^-1{ M^T v'(out) } です。

これは画面上にビューとして描画される逆行列、つまり転置行列です。
したがって、配置が列優先であれば、最終的には行優先となり、
2次元配列には有利になります。

さらに、この議論をさらに深めていくと、必然的に、
外積の方向がなぜ左手系か右手系かに決定されたのかという疑問に至ります。

  • 物理学はすべて右手系なのに、なぜ左手系が存在するのでしょうか?

グラフの横軸は自然に左から右、縦軸は自然に下から上に向かうため、
Z軸を平面から飛び出させるには、右手系にする必要があります。
しかし、表示面の縦軸、つまり文字の流れは当然上から下なので、
これは左手系のシステムです。また、文字を表現する媒体が彫り物から筆記へと移行するにつれ、
文字の流れの順序も右から左、そして左から右へと変化しました。
つまり、軸の方向は、一般的には、表現媒体の変化とその方向の必要性によって決まるのです。
言い換えれば、グラフ上では右手系ですが、文章の流れとしては左手系で表現するのが便利です。
また、右手系と左手系を区別する簡単な方法は、X軸の方向を左から右、Y軸の方向を下から上と固定することです。
右手系とは、Z軸の方向が後ろから前になる系、左手系とは、Z軸の方向が前から後ろになる系です。

注:
3DCGでは、自分が話しているのが世界行列なのか視点行列なのかをまず確認する必要があります。そうでないと、
認識の矛盾、つまり逆の意味になってしまう可能性があります。


* 同次座標と bHomo フラグ

目次に戻る

注:
これは、w、x、y、z、… の順序で定義される同次座標の説明です。順序は異なりますが、機能は同じです。
NAS6LIB はレンダラーではないため、右手系や左手系は存在せず、主にレンダラーの座標系に従います。
ただし、ベクトルの外積は右手系で定義されており、レンダラーの座標系に従っている限り、大きな問題は発生しません。

このセクションでは、N6L が同次座標をどのように扱うか、そしてライブラリ内での特別な動作を指示する
bHomo フラグの意味について説明します。

  • 座標系の想定
    N6L は基本的に DirectX の規約に基づいているため、行優先の左手座標系を想定しています。

右手座標系を採用している他のライブラリや API とやり取りする場合は、入出力時に転置や Z 軸反転(-1 による乗算)
などの変換が必要になる場合があることに注意することが重要です。

ただし、N6L 内でのみ計算を実行している限り、内部規約を一貫して遵守していれば、これらの外部変換は厳密には必要ありません。

  • N6L の行列レイアウト

同次変換行列は通常、次のように表現されます。

$$M = \begin{pmatrix} ROT & T \\\ 0 & 1 \end{pmatrix}$$

ここで、ROT は回転成分、T は平行移動成分です。N6L は内部表現として、機能的には同等ですが、
若干異なる行優先レイアウトを採用しています。この配置によって計算上の問題は発生しません。

N6L の期待される行列レイアウト(行優先):

$$M = \begin{pmatrix} 1 & 0 & 0 & 0 \\\ T_0 & ROT_{00} & ROT_{01} & ROT_{02} \\\ T_1 & ROT_{10} & ROT_{11} & ROT_{12} \\\ T_2 & ROT_{20} & ROT_{21} & ROT_{22} \end{pmatrix}$$

Mij は回転変換成分です。
Tk は並進成分です。
行 N0 は w 成分の行です。
行 N1 は回転行列の x 軸成分の行です(ローカル座標系)。
行 N2 は回転行列の y 軸成分の行です(ローカル座標系)。
N3行目は、回転行列(ローカル座標系)のZ軸成分の行です。

注:
ベクトル×行列の計算は列優先の計算です。
行列×ベクトルの計算は行優先の計算です。
NAS6LIBで使用される行優先の計算形式の利点は、
ローカル回転変換の各軸を行として直接抽出できることです。
この場合、移動成分は列に配置されますが、
それらを同次ゼロベクトルで乗算することで、
同様に直接抽出できます。
この場合、式の行優先表記は主にN6LMatrix.Mul(N6LVector)ですが、
状況に応じて列優先表記のN6LVector.Mul(N6LMatrix)として記述することもできます。

同次座標系の主な利点
同次座標系と4x4行列を使用することで、様々な3Dグラフィックス変換を統一された
線形代数演算として扱うことができます。その利点は計り知れません。

  • 統一された変換表現:

  • 平行移動、回転、拡大縮小、せん断、さらには透視投影といった多様な変換を、
    単一の4x4行列の乗算で表現できます。これにより、複雑な変換チェーン
    (例: オブジェクトの回転 → 平行移動 → カメラビューの変換)が単純な行列積に簡素化され、
    コードが大幅に効率化されます。同次座標系がなければ、異なる変換タイプごとに
    異なる計算方法が必要になり、コードがはるかに複雑になります。

  • 効率的な逆行列計算(特に回転):
    4x4同次行列から3x3の回転部分を抽出し、その直交行列の特性を利用して、
    転置するだけで逆行列を求めることができます。この最適化により、
    計算コストの高い一般逆行列計算(ガウス法など)を回避できます。
    ssian elimination(SS消去法)と呼ばれる手法で表現され、
    リアルタイムグラフィックスのパフォーマンスに大きく貢献します。

  • 透視投影表現:
    同次座標のw成分は、透視投影(奥行き知覚)を表現するために不可欠です。

物体の距離が変化するとw成分も変化し、最終的な3Dから2Dへの投影において正しい透視投影を実現します。

  • 点とベクトルの区別(変換特性):
    同次座標では、点(位置)は通常(x, y, z, 1)
    、方向ベクトルは(x, y, z, 0)
    として表現されます。この区別により、変換行列を適用した際の挙動が自動的に決定されます。

  • 点は平行移動の影響を受けます。
    方向ベクトルは平行移動の影響を受けません(回転と拡大縮小のみ)。
    この特性も、単一の行列演算によって自動的に実現されます。

  • bHomoフラグ:N6Lの魔法のスイッチ

bHomoフラグはN6L内で特別なスイッチとして機能し、trueに設定すると独自の動作が可能になります。

bHomoがtrueの場合、N6Lは特定の操作を実行します。
算術演算、転置、その他の変換において、N6Lは3x3 ROT成分(w成分は省略)を抽出し、
この3x3部分行列に対して演算を実行し、その後w成分を再結合します。

この動作は、同次座標計算の本質的な特性を活用しており、より直感的で合理的な変換記述方法を可能にします。

重要事項: 変換チェーンの最後でbHomoフラグをfalseにしなければならない場合があります
(例:特定の演算のために非同次3D座標に戻す場合)。
bHomo フラグを適切に切り替えたり管理したりし忘れたりすると、予期せぬ表示エラーやバグが発生する可能性があります。
bHomo が、行列の想定される使用例に応じて true または false に設定されていることを常に確認してください。
N6LXXX.SetHomo(rh) で bHomo フラグを設定し、N6LXXX.ToHomo() で w 要素を追加し、N6LXXX.ToNormal() で
w 要素を削除するなどすることで、bHomo フラグを比較的安全に操作できます。

  • N6L における bHomo フラグの動作

bHomo フラグの動作は複雑ですが、導入された理由を説明すると、
例えば、N6LVector.Abs() の実装は次のとおりです。

//絶対値の平方//絶対値の平方
SquareAbs() {
  var sum = 0.0;
  var i = 0;
  var l = new N6LVector(this);
  if(l.bHomo) {
    i = 1;
    l = l.Homogeneous();
  }
  for(; i < l.x.length; i++) sum += l.x[i] * l.x[i];
  return sum;
};

//絶対値//絶対値
Abs() {
  return Math.sqrt(this.SquareAbs());
};

つまり、bHomoフラグはw要素をスキップするかどうかを決定し、この条件が存在する場合、
同じN6LVectorを同次ベクトルと非同次ベクトルのどちらとして扱い、同次ベクトルの場合はw要素を絶対値に含めません。
この方法は、w要素を自動で判定できるという利点があります。
しかし、この方法にもいくつか問題点があります。 N6LMatrixでは、データはN6LVector配列として
N6LMatrix.x要素に格納されますが、このデータ格納では、N6LVector配列のベクトル要素のbHomoが
デフォルトでfalseである必要があります(通常の行列計算の動作)。
したがって、行列の行を取得するには、次の2つの方法があります。

var m = new N6LMatrix(4).UnitMat();
var v1 = m.x[1]; // ローカルx軸のbHomo=falseを取得
var v2 = m.GetRow(1); // ローカルx軸のbHomo=trueを取得

取得したN6LVectorのbHomoフラグが実装要件で期待されるとおりに設定されていない場合、w要素の処理時にバグが発生する可能性があります。
私の経験上、m.x[1]を直接取得する癖があり、bHomo=false のせいで意図せず誤動作が発生するケースがありました。
このようなケースの解決策は、行列からベクトルを取得した後、明示的に SetHomo(true or false) を呼び出すことです。


NAS6LIBにおける同次座標の特別な配置が計算上問題ないことの証明

目次に戻る

3x3の回転行列を格納するだけでは、並進成分を考慮することは困難であり、ほぼ不可能です。
そこで、この解決策として、追加の次元を含む同次座標を導入します。これにより、回転成分と並進成分を分離できるため、
扱いやすくなります。一般的な数学的慣習では、同次座標成分(w要素)は最後に追加されますが、NAS6LIBでは先頭に追加します。
これにより、座標が同次かどうかに応じてw要素をスキップする計算が容易になります。
常にダミーのw要素を追加してxyz座標のインデックスを固定することも可能ですが、
NAS6LIBではこれを採用しませんでした。例えば、2次元のベクトルや行列の計算でも、
ダミーのw要素の存在は混乱を招くため、スキップを制御するために bHomo フラグを使用することにしました。

P = [Px, Py, Pz, (Pw = 1)]

P' = MP + T

一般的な数学表記では、次のように表します。

$$M = \begin{pmatrix} M_{00} & M_{01} & M_{02} & (M_{03} = T_0) \\\ M_{10} & M_{11} & M_{12} & (M_{13} = T_1) \\\ M_{20} & M_{21} & M_{22} & (M_{23} = T_2) \\\ 0 & 0 & 0 & 1 \end{pmatrix}$$

P'の場合、

$P'x = M_{00}Px + M_{01}Py + M_{02}Pz + T_0Pw$
$P'y = M_{10}Px + M_{11}Py + M_{12}Pz + T_1Pw$
$P'z = M_{20}Px + M_{21}Py + M_{22}Pz + T_2Pw$
$P'w = M_{30}Px + M_{31}Py + M_{32}Pz + T_3Pw = 1$

Q = [(Qw = 1), Qx, Qy, Qz]

Q' = NQ + U

$$N = \begin{pmatrix} 1 & 0 & 0 & 0 \\\ (N_{10} = U_1) & N_{11} & N_{12} & N_{13} \\\ (N_{20} = U_2) & N_{21} & N_{22} & N_{23} \\\ (N_{30} = U_3) & N_{31} & N_{32} & N_{33} \end{pmatrix}$$

回転成分 3x3 と並進成分の対応は

Nab = M(i+1)(j+1)
Ul = T(k+1)

この場合、Q' は

$Q'w = U_0Qw + N_{01}Qx + N_{02}Qy + N_{03}Qz = 1$
$Q'x = U_1Qw + N_{11}Qx + N_{12}Qy + N_{13}Qz$
$Q'y = U_2Qw + N_{21}Qx + N_{22}Qy + N_{23}Qz$
$Q'z = U_3Qw + N_{31}Qx + N_{32}Qy + N_{33}Qz$

つまり、P' と Q' は結局同じ意味を持つので、w 成分を先頭に置いても計算に大きな問題はありません。

また、同次座標では、Pw 成分と Mw 成分が全体のスケールであり、全体を分割することを同次座標の正規化と呼びます。


N6L における型識別による抽象化

目次に戻る

C# のマネージドコードの経験に基づき、N6L は二項演算子において呼び出し元クラスを
lh(左辺)、2 つの項目を rh(右辺)とみなし、lh、rh などの型識別から同じ
Mul [乗算] の動作を自動的に決定し、型に応じて複数の意味を持たせます。

型識別の実装方法は非常にシンプルです。N6LXXX.typename = "N6LXXX"
型名の文字列は、コンストラクタの typename プロパティに直接格納されます。
これにより、typename プロパティの存在を確認し、文字列を比較するだけで型識別が可能になります。

このようにして得られた型識別を用いて、例えば、N6LVector.NormalVec() [正規化] を次のように実現できます。

//convenience//便宜上
Div(rh) {
  var ret = 0.0;
  if(rh && rh.typename == "N6LVector"){
    // rh が N6LVector の場合の処理 // 省略
  }
  else if(rh && rh.typename == "N6LMatrix"){
    // rh が N6LMatrix の場合の処理 // 省略
  }
  else if(typeof(rh) == "number") {
    // rh が実数の場合の処理
    ret = new N6LVector(this);
    var i = 0;
    var l = new N6LVector(this);
    var r = rh;
    if(l.bHomo) {
      i = 1;
      l = l.Homogeneous();
    }
    if(rh == 0.0) return l;
    for(; i < l.x.length; i++) {
      ret.x[i] = l.x[i] / r;
    }
    return ret;
  }
  return ret;
};

//絶対値の二乗
SquareAbs() {
  var sum = 0.0;
  var i = 0;
  var l = new N6LVector(this);
  if(l.bHomo) {
    i = 1;
    l = l.Homogeneous();
  }
  for(; i < l.x.length; i++) sum += l.x[i] * l.x[i];
  return sum;
};

//絶対値
Abs() {
  return Math.sqrt(this.SquareAbs());
};

上記のように実装すると、正規化は数学的に次のように記述できます。

//normalize//Normalize
NormalVec(a) {
  if(a != undefined) {
    var ab = a.Sub(this);
    return ab.NormalVec();
  }
  if(!this.Abs()) return new N6LVector(this);
  return this.Div(this.Abs());
};

正規化呼び出しにベクトル引数が与えられた場合、ベクトル lh - rh の正規化が返されます。
引数がない場合、正規化は最終的に解決され、明確な式、つまり v / v.abs として記述されます。
ベクトルの除算は数学的には定義されていませんが、私たちはそれほど厳密さを気にせず、
実装の便宜上これを採用しています。SquareAbsは元々Absのみで実装されていましたが、
物理演算を行うにつれて絶対値の2乗を直接求める機会が増え、
余分な時間のかかるMath.sqrtを省略できるようになりました。
また、Absの動作はbHomoフラグによって分岐し、同次ベクトルの場合はw要素がスキップされて
合計に含まれないため、直感的な表記にも役立ちます。
このように、表現する表記が直感的であればあるほど、型識別とフラグによる
動作の抽象化が効果的になり、コードの可読性が向上します。


* クォータニオン (N6LQuaternion)

目次に戻る

* クォータニオンの基礎

* 回転表現としてのクォータニオンの利点 (ジンバルロックの回避)

回転のための回転行列オブジェクトをZYZ軸の周りで角度φ、θ、ψで回転させる回転法は、オイラー角回転式として知られています。

しかし、この方法はジンバルロックと呼ばれる問題を引き起こすことがよくあります。

ジンバルロックは、オイラー角が通常、固定されたグローバル軸の周りの回転を定義するために発生します。
(場合によっては、グローバル軸と中間軸を組み合わせた連続的な回転を定義することもありますが、これも同様の問題を引き起こす可能性があります。)
これらの軸のうち2つが一直線になると(例えば、1つの軸の周りで90度回転した後、別の軸が3つ目の軸と同一線上になるなど)、
回転の自由度が1つ失われます。これにより、一直線になった軸の周りで異なる方向にさらに回転することができなくなり、
予期しない、または制御不能な回転につながります。

ジンバルロックを回避するには、この軸の配置の問題に悩まされない回転表現が必要です。
四元数(またはロドリゲスの回転公式のような軸-角度表現)は、任意の軸の周りの回転を表すことで解決策を提供します。
四元数は回転軸とその軸の周りの角度を直接符号化するため、軸の崩壊の問題を本質的に回避し、ジンバルロックを防止します。

四元数を同等のロドリゲスの回転公式と比較すると、四元数の大きな利点は計算効率にあります。
四元数を使用することで、ロドリゲスの公式では通常回転行列に変換する際に必要となる2つの三角関数計算(正弦と余弦)を省略できます。
その結果、コンピュータがこれらの三角関数を近似するために使用する、時間のかかるマクローリン級数展開も不要になります。
この複雑な計算を直接回避できるからこそ、四元数ベースの回転は計算速度と数値安定性に優れていることが知られています。


* 作成、初期化

目次に戻る

JavaScript

var quta = new N6LQuaternion(1, 0, 0, 0);
var qutb = new N6LQuaternion(1, new Array(0, 0, 0));
var qutc = new N6LQuaternion(new Array(1, 0, 0, 0));
var qutd = new N6LQuaternion(new N6LVector([1, 0, 0, 0]));
var quute = new N6LQuaternion(1, new N6LVector([1, 2, 3, 4], true));
var qutf = new N6LQuaternion([1, 0, 0, 0]);
var qutg = new N6LQuaternion(quta); // ディープコピー

* 乗算、正規化

目次に戻る

JavaScript

//クォータニオン

var q1 = new N6LQuaternion([1, 0, 0, 1]);
var q2 = new N6LQuaternion([1, 0, 1, 0]);
var q3 = q1.Mul(q2);
console.log(q3.q.x); // 結果: [0.5, -0.5, 0.5, 0.5]
var q4 = q3.NormalQuat();
console.log(q4.q.x); // 結果: [0.5, -0.5, 0.5, 0.5]

* 行列の相互変換 (N6LQuaternion、N6LVector、N6LMatrix の連携)

目次に戻る

JavaScript

//Vector←→Matrix←→Quaternion

var v1 = new N6LVector([Math.PI / 2.0, 0, 1, 0], true);
var m1 = v1.Matrix();
var q1 = m1.Quaternion();
var m2 = q1.Matrix();
var v2 = m2.Vector();
console.log(v2.x); // 結果: [Math.PI / 2.0 = 1.571, 0, 1, 0]

* アニメーションSlerp(球面線形補間)などへの応用

目次に戻る

JavaScript

var q1 = new N6LQuaternion([1, 0, 0, 1]);
var q2 = new N6LQuaternion([1, 0, 1, 0]);
var q3 = q1.Lerp(q2, 0.5);
console.log(q3.q.x); // 結果: [0.816, 0, 0.408, 0.408]
var q4 = q1.Lerp(q2, 1);
console.log(q4.q.x); // 結果: [0.707, 0, 0.707, 0]
var q5 = q1.Slerp(q2, 0.5);
console.log(q5.q.x); // 結果: [0.816, 0, 0.408, 0.408]
var q6 = q1.Slerp(q2, 1);
console.log(q6.q.x); // 結果: [0.707, 0, 0.707, 0]
var q7 = q1.Slerp2(q2, 0.5);
console.log(q7.q.x); // 結果: [0.816, 0, 0.408, 0.408]
var q8 = q1.Slerp2(q2, 1);
console.log(q8.q.x); // 結果: [0.707, 0, 0.707, 0]

* クォータニオン球面補間表示

目次に戻る

Normal/Logarithmic Quaternion Lerp/Slerpについて視覚化したWEBアプリの紹介です

クォータニオン球面補間表示 [Wiki 外のリンク]
gif of qtlerp

このWebアプリケーションは、3Dグラフィックスや物理シミュレーションで用いられる
クォータニオン(四元数)の補間の仕組みを、3次元空間で視覚的に比較・学習するためのツールです。

抽象的で理解が難しいクォータニオンの補間経路の違いを、
ユーザーが設定した軸と角度に基づいてリアルタイムで描画します。

  • アプリケーションの機能 ユーザーは、始点(Src)と終点(Dest)の回転軸と回転角度を自由に設定できます。
    アプリは、その設定された回転を達成するための3種類の補間経路を同時に計算し、
    色分けして球体上に表示します。

    1. 補間アルゴリズムの比較
      以下の3種類の補間経路を描画し、その違いを視覚的に比較できます。
補間方法 経路の色 特徴 数学的経路
SLERP (球面線形補間) シアン(青緑) 4次元の単位球上で最短距離(大円弧)を一定の角速度で移動します。 Shortest arc
LERP (線形補間) ホワイト(白) 4次元空間で直線をたどり、その結果を球面上に射影します。経路は球の中心に向かってわずかに凹みます。 Straight line in 4D
LnQuat LERP (対数空間線形補間) ピンク クォータニオンを対数空間に変換し、そこで線形補間(LSLERP)を行います。SLERPと同じ最短経路(大円弧)をたどるのが理想です。 Logarithmic arc
    1. 特殊なケースの検証
      このシミュレーターは、クォータニオンの理解を深める上で重要な以下の特殊な挙動を検証できます。

回転軸が一致する場合: 始点と終点の軸が同じでも、角度差に応じた補間が正しく行われること
を確認できます。(LnQuat LERPのデバッグ済み)

最短経路の選択: SLERP/LERPが意図的に遠回り(最短ではない経路)を選択する設計になっている場合、
LnQuat LERPのみが最短経路を示す現象を確認できます。

視覚的な差: LERPの線が他の二つよりわずかに球の中心に凹む様子を、入力パラメータを変えて確認できます。

  • 利用対象
    3Dゲームエンジンやグラフィックスを学ぶ学生
    クォータニオンベースのモーションや物理を実装する開発者
    クォータニオン補間の原理を視覚的に理解したいすべての人

* カメラと投影

目次に戻る

このセクションでは、3Dグラフィックスにおけるカメラと投影の基本概念について説明します。
これらは、3Dシーンを2Dスクリーンにレンダリングするために不可欠です。
N6LMatrixライブラリとN6LVectorライブラリが提供する関連機能に焦点を当てます。

シンプルな透視投影テスト

シンプルな透視投影テスト デモと解説 (外部リンク)

Z軸の符号規則に関する注意: このセクションでは、投影式におけるZ軸の正負の方向は、
特定の標準的な規則に従うものではありません。
提供されている frustum 関数を用いて、透視投影の核となる数学的概念を説明することに重点を置いています。

このセクションでは、Three.js や X3DOM などの外部 3D ライブラリに依存せず、
HTML Canvas と独自の数学的計算のみを使用して 3D ワイヤーフレームをレンダリングする基本的なデモの概要を示します。
これは透視投影の仕組みを最初から学ぶ基本的な例です。

コアコンセプトと公式
このデモは、2つの主要な変換とその応用を中心に展開します。

  • 回転公式(ロドリゲスの回転公式に相当):
    このrot関数は、3D点pを任意の軸aを中心に角度thだけ回転させます。

このカスタム実装は、回転の数学的原理を直接反映しています。

function rot(a, th, p) {
var c = Math.cos(th), s = Math.sin(th);
戻り値 [
(c + a[0] * a[0] * (1 - c)) * p[0] + (a[0] * a[1] * (1 - c) - a[2] * s) * p[1] + (a[0] * a[2] * (1 - c) + a[1] * s) * p[2],
(a[1] * a[0] * (1 - c) + a[2] * s) * p[0] + (c + a[1] * a[1] * (1 - c)) * p[1] + (a[1] * a[2] * (1 - c) - a[0] * s) * p[2],
(a[2] * a[0] * (1 - c) - a[1] * s) * p[0] + (a[2] * a[1] * (1 - c) + a[0] * s) * p[1] + (c + a[2] * a[2] * (1 - c)) * p[2]
];
}
  • 近平面透視投影の公式:
    frustum関数は、近平面への単純な透視投影を実装します。

3次元点P = (Px, Py, Pz)は、画面上の2次元点P' = (P'x, P'y)に投影されます。

使用される公式は次のとおりです。

P' = [ (n / Pz) * Px, (n / Pz) * Py ]

ここで、nは近平面までの距離です。

function frustum(n, p) {
return [(n / p[2]) * p[0], (n / p[2]) * p[1]];
}

アニメーションループの構造
このデモでは、N6LTimerMan によって管理されるアニメーションループ (GLoop) を使用します。

  • エントリポイントとメインループの設定:
    enter3 関数はシーンを初期化し、メインアニメーションループを 50 ミリ秒ごとに実行するように設定します。
function enter3() {
init(); // 3D ポイントと時間を初期化
TMan.add(); // マネージャーにタイマーを追加
TMan.timer[0].setalerm(function() { GLoop(0); }, 50); // メインループのアラームを設定
return true;
}

メインループ (GLoop):

各フレームで、GLoop 関数は 3D ポイントに対して一連の変換を実行します。

1. 複合回転: ポイントは、Z 軸、Y 軸、X 軸を中心に、異なる速度で順次回転します。
2.Z軸移動: カメラからの距離を調整するために、ポイントはZ軸に沿って移動します(例: rp[i][2] += 40)。
3.透視投影: 変換された各3Dポイントは、frustum関数を使用して2Dの近接平面に投影されます。
4.ワイヤーフレーム描画: 投影された2Dポイントは接続されてワイヤーフレームを形成し、HTML Canvasに描画されます。

function GLoop(id) {
  // ... (時間増分と変数宣言)
  // 回転、移動、透視変換
  for (i = 0; i < p.length; i++) rp[i] = rot([0, 0, 1], 1 * time, p[i]); // Z軸回転
  for (i = 0; i < rp.length; i++) rp[i] = rot([0, 1, 0], 2 * time, rp[i]); // Y軸回転
  for (i = 0; i < rp.length; i++) rp[i] = rot([1, 0, 0], 3 * time, rp[i]); // X軸回転
  for (i = 0; i < rp.length; i++) rp[i][2] += 40; // Z軸移動
  for (i = 0; i < rp.length; i++) pp[i] = frustum(1, rp[i]); // 透視投影
  // ... (Canvas 上のワイヤーフレーム描画)
}

スクリーン座標 (x,y) から 3D 座標 (X,Y,Z) への変換 (ピッキング)

このページでは、2D スクリーン座標を 3D ワールド座標に変換する基本原理についても説明します。
この変換は「ピッキング」または「レイキャスティング」と呼ばれることもあります。

透視投影の公式は、次の通りです。
x = (n / Z) * X
y = (n / Z) * Y

逆変換は次のように導かれます。
X = (Z / n) * x
Y = (Z / n) * y

これらから、Zは次のように表せます。
Z = (n / x) * X
Z = (n / y) * Y

これらを組み合わせると、投影面上のXとYの関係は次のようになります。
(n / x) * X = (n / y) * Y
X = (x / y) * Y または Y = (y / x) * X

核となる考え方は次のとおりです。

  1. 3DオブジェクトのZ座標を仮定します(例えば、その深度のXY平面でn = 1の場合、Z = -5)。
  2. 対応するX座標とY座標を計算します。X = (Z / n) * x = 5x Y = (Z / n) * y = 5y
  3. 基本的に、選択プロセスは既知のZ座標(例えば、近接平面Z=n)から開始し、
    外側に向かって(Z座標を増やしながら)反復し、3D座標(X、Y、Z)と2Dスクリーン座標(x、y)
    の間の導出関係を満たす3Dオブジェクトが見つかるまで続けます。この「光線」に沿って
    最初に遭遇したオブジェクトが「選択」されます。

* ビューマトリックス

目次に戻る

ビューマトリックスは、3Dシーンにおけるビューアーの配置に不可欠です。
ビューマトリックスは、オブジェクトをワールド座標系からビュー空間(基本的にはカメラの視点)に変換します。


* LookAt 行列の構築

カメラを 3D 空間内の特定の点に向けるために、N6LMatrix は専用の LookAtMat メソッドを提供します。

  • N6LMatrix.LookAtMat(eye, lookat, up):
    このメソッドは、ワールド空間におけるカメラの位置 (eye) と
    ターゲット点 (lookat) に基づいてビュー行列を作成します。
    これは f です。視線とカメラの上方向(上)の座標を計算します。
    これは、現実世界のカメラを被写体に正確に向けるのと同じだと考えてください。

  • N6LMatrix.LookAtMat2(rh):
    これは LookAtMat のオーバーロード版です。N6LMatrix インスタンス自体
    (おそらくカメラの現在のポーズを表す)を目の位置として使用し、rh を視線ターゲットとして使用してビュー行列を計算します。
    これは、既存のカメラの視線を調整するのに便利です。

* カメラの移動と回転

カメラの動きの制御は、主に並進(移動)と回転(向きの変更)を伴います。
N6LMatrix ライブラリは、これを実現するための強力なツールを提供しています。

  • N6LMatrix.MoveMat(outmat, outv, d, pyr, v, a, vmin, vmax):
    この包括的なメソッドは、カメラの移動と回転の両方を 1 回の呼び出しで処理します。
    outmat[0]とoutv[0]は、移動後の行列と速度を返す出力パラメータです。
    d: 並進の変位ベクトルを表します。
    pyr: カメラのピッチ、ヨー、ロールを定義する4次元ベクトルです。
    v: 現在の速度です。
    a: 加速度です。
    vmin、vmax: 速度の制限値(987654321.0は制限なしを意味します)。
    このメソッドは、加速、減速、速度制限など、物理法則に基づいたカメラモーションの統合シミュレーションを可能にします。

  • N6LMatrix.TranslatedMat():
    より単純な並進移動の場合、このメソッドは基本的な並進行列を生成します。

  • N6LMatrix.RotAxis() (およびその他の回転関連メソッド):
    これらのメソッドは、カメラの向きを変更するために使用される回転行列を生成します。

  • N6LMatrix.InverseMat(dt, sw):
    このメソッドは、逆行列を計算し、その行列式も提供します。カメラのワールド行列からビュー行列を導出したり、
    複雑な変換を元に戻したりするのに非常に便利です。sw パラメータは、逆行列計算アルゴリズムを柔軟に選択できるようにします。

これらの機能を組み合わせることで、インタラクティブなカメラコントロールから 3D シーンの自動カメラパスまで、あらゆる機能を実装できます。


* 投影行列

オブジェクトはビュー空間に変換された後、最終的に投影行列を使用して 2D スクリーンに投影されます。
投影には、主に透視投影と正投影の 2 つの種類があります。

* 透視投影の設定

目次に戻る

透視投影は、人間の目が奥行きを認識する方法をシミュレートします。遠くのオブジェクトは小さく見え、近くのオブジェクトは大きく見えます。
これは、奥行きとスケール感のあるリアルな3Dシーンを作成するために不可欠です。

  • N6LMatrix.FrustumMat(left, right, top, bottom, near, far):
    このメソッドは、透視投影行列を構築します。そのパラメータは、3D世界の可視領域を表す、切頂ピラミッドである視錐台を定義します。

left, right, top, bottom: 視錐台の左、右、上、下のクリッピング面の座標。
near, far: 近距離と遠距離のクリッピング面までの距離。これらのパラメータは、視野とアスペクト比を総合的に決定し、
3D空間のどの部分が画面に投影されるかを定義します。

  • N6LVector.FromLogAxis(base, range, x) / N6LVector.ToLogAxis(base, range, x):
    これらのメソッドは、対数軸と法線軸間の変換を容易にします。これは、非常に近いオブジェクトと非常に遠いオブジェクトの
    両方を含むシーンで深度バッファの精度を向上させ、Zファイティングの問題を軽減するのに特に役立ちます。

  • N6LVector.FrustumInfVec(base, range, v) / N6LVector.InvFrustumInfVec(base, range, v, z):
    これらのメソッドは、無限遠透視投影に使用されます。無限遠透視投影は、遠方クリッピング面が概念的に
    無限遠にある特殊な形式で、深度範囲の最適化によく使用されます。

* 正投影の設定

正投影は、透視投影のないビューを提供します。オブジェクトはカメラからの距離に関係なく元のサイズを維持するため、
CADアプリケーション、2Dゲーム、または一貫したスケールが重要な技術図面に最適です。

  • N6LMatrix.OrthoMat(left, right, top, bottom, near, far):
    このメソッドは、正投影行列を構築します。そのパラメータは、可視ボリュームを表す長方形の境界ボックスを定義します。
    left, right, top, bottom: 投影ボックスの左、右、上、下の境界の座標。
    near, far: 近クリッピング面と遠クリッピング面までの距離。透視投影とは異なり、
    この定義されたボックス内のすべてのオブジェクトは、奥行きに関係なく、実際のサイズで投影されます。

* N6LTimerMan による時間管理

メインスレッドの構築

目次に戻る

HTML

<script src="https://nas6mixfoolv.github.io/NAS6LIB/javascripts/nas6lib/timer.js"></script>

JavaScript

  
  // 通常、DOMContentLoaded を待つ必要はありませんN6LTimerMan をインスタンス化しますが、
  // 他の DOM 要素に依存する処理がある場合は、DOMContentLoaded 内で実行します。
  var TMan = new N6LTimerMan(); // NAS6LIB/javascripts/nas6lib/timer.js に定義されているものと想定します。
  var GLoopID = -1; // メインスレッドの ID
  
  //N6L 初期化処理
  function initialiseN6L() {
    // N6L 初期化の主な処理をここに記述します。
    // 例えば、N6LMatrix を使用した計算の初期化、Canvas の取得、描画コンテキストの準備などです。
    console.log("DOM が完全にロードされました。N6L を初期化しています...");

    GLoopID = TMan.add(); // タイマーマネージャに新しいタイマーを追加します。
    GLoop(GLoopID); // メインスレッドを初回実行して開始します。
  }

  //メインスレッド
  function GLoop(id){
    // メインスレッドの処理をここに記述します。

    // 最後に、スレッドを続行するためにメインスレッドをリセットします。ただし、終了条件でリセットしない場合は、スレッドは停止します。
    TMan.timer[id].setalerm(function() { GLoop(id); }, 50); // 50ミリ秒後にメインスレッドをリセットします。
  }

  // DOMContentLoaded イベントリスナーを追加します。
  // window.document.addEventListener を使用するのが標準です。
  document.addEventListener('DOMContentLoaded', initializeN6L);

  // 代わりに、window.addEventListener('load', initializeN6L); を使用することもできます。
  // DOMContentLoaded の方が高速で、DOM 操作に適しています。

メインスレッドを100回呼び出し、その後自動的に終了させるコード例:

JavaScript

// N6LTimerMan のインスタンス化に DOMContentLoaded を待つ必要は通常ありませんが、
// 他の DOM 要素に依存する処理がある場合は、DOMContentLoaded 内で実行します。
var TMan = new N6LTimerMan(); // NAS6LIB/javascripts/nas6lib/timer.js に定義されているものと想定
var GLoopID = -1; // メインスレッドの ID

// カウンタ変数を追加
var loopCount = 0;
const MAX_LOOP_COUNT = 100; // 終了回数を定義

// N6L 初期化処理
function initialiseN6L() {
  // N6L 初期化のためのメイン処理をここに記述します。
  // 例えば、N6LMatrix を用いた演算の初期化、Canvas の取得、描画コンテキストの準備など。
  console.log("DOM が完全にロードされました。N6L を初期化しています...");

  GLoopID = TMan.add(); // タイマーマネージャに新しいタイマーを追加します。
  GLoop(GLoopID); // メインスレッドを初回実行して開始します。
}

//メインスレッド
function GLoop(id){
  // メインスレッドの処理をここに記述します。
  console.log("ループ回数: " + loopCount); // 現在の回数を表示します(デバッグ用)

  // 回数をインクリメントします。
  loopCount++;

  // 終了条件をチェックします。
  if (loopCount <= MAX_LOOP_COUNT) {
  // 最後に、メインスレッドをリセットしてスレッドを続行します。ただし、終了条件でリセットしない場合は、スレッドは停止します。
  TMan.timer[id].setalerm(function() { GLoop(id); }, 50); // 50ミリ秒後にメインスレッドをリセットします。
  } else {
    console.log("メインループは " + MAX_LOOP_COUNT + " 回の反復処理後に終了しました。");
    // これはオプションですが、厳密にしたい場合は停止するように指定してください。
    // TMan.timer[id].stop(); // タイマーを停止し、ループを終了します。
  }
}

// DOMContentLoaded イベントリスナーを追加します。
// window.document.addEventListener を使用するのが標準です。
document.addEventListener('DOMContentLoaded', initializeN6L);

// 代わりに、window.addEventListener('load', initializeN6L); を使用することもできます。
// DOMContentLoaded の方が高速で、DOM 操作に適しています。

N6LTimerMan を使えば、JavaScript でマルチスレッドを簡単に実装できます

目次に戻る
N6LTimerMan.htm を使用した JavaScript マルチスレッドテスト DEMO
N6LTimerMan.zip を使用した JavaScript マルチスレッドテスト ZIP

./mttest.htm(スクリプト部分)

window.addEventListener("DOMContentLoaded", init);
var TMan = new N6LTimerMan(); // タイマーマネージャー
var pos = [ new N6LVector(4, true), new N6LVector(4, true), new N6LVector(4, true)];
var th = [0, 0, 0, 0];
var dt = [50, 100, 150, 500];
var spd = 5.0;
var cnt = 0;
var div = 72;
var sph3;
var TManIDs = [];

function init() {
  const width = 500;
  const height = 250;

  const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector("#cnv0")
});
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(width, height);
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    45,
    width / height,
    1,
    10000
  );
  camera.position.set(0, 0, 20);

  // 球体を作成
  const sph0geometry = new THREE.SphereGeometry(1, 128, 128);
  // マテリアルの色を設定
  const sph0material = new THREE.MeshBasicMaterial({ color: '#ff0000'});
  // メッシュを作成
  const sph0 = new THREE.Mesh(sph0geometry, sph0material);
  sph0.position.set(0, 6, 0);
  pos[0] = new N6LVector([1, 0, 6, 0], true);
  // 球体を作成
  const sph1geometry = new THREE.SphereGeometry(1, 128, 128);
  // マテリアルの色を設定します
  const sph1material = new THREE.MeshBasicMaterial({ color: '#00ff00'});
  // メッシュを作成します
  const sph1 = new THREE.Mesh(sph1geometry, sph1material);
  sph1.position.set(0, 2, 0);
  pos[1] = new N6LVector([1, 0, 2, 0], true);
  // 球体を作成します
  const sph2geometry = new THREE.SphereGeometry(1, 128, 128);
  // マテリアルの色を設定します
  const sph2material = new THREE.MeshBasicMaterial({ color: '#0000ff'});
  // メッシュを作成
  const sph2 = new THREE.Mesh(sph2geometry, sph2material);
  sph2.position.set(0, -2, 0);
  pos[2] = new N6LVector([1, 0, -2, 0], true);
  // 球体を作成
  const sph3geometry = new THREE.SphereGeometry(1, 128, 128);
  // マテリアルの色を設定
  const sph3material = new THREE.MeshBasicMaterial({ color: '#808080'});
  // メッシュを作成
  sph3 = new THREE.Mesh(sph3geometry, sph3material);
  sph3.position.set(0, -6, 0);
  pos[3] = new N6LVector([1, 0, -6, 0], true);
  scene.add(sph0);
  scene.add(sph1);
  scene.add(sph2);
  scene.add(sph3);
  // ディレクショナルライト光源
  const light = new THREE.DirectionalLight(0xffffff);
  light.position.set(1, 1, 1);
  // シーンに追加
  scene.add(light);

  // タイマーを作成
  TManIDs = [TMan.add(), TMan.add(), TMan.add(), TMan.add(), TMan.add()];
  // 最初の実行
  Loop0(TManIDs[0]);
  Loop1(TManIDs[1]);
  Loop2(TManIDs[2]);
  Loop3(TManIDs[3]);
  RDLoop(TManIDs[4]);

  function RDLoop(id) {
    // レンダリング
    renderer.render(scene, camera);
    TMan.timer[id].setalerm(function() { RDLoop(id); }, 50);
  }

  function Loop0(id) {
    th[id] += spd * Math.PI / 180.0;
    sph0.position.set(5 * Math.sin(th[id]) + pos[id].x[1], 6, 0);

    TMan.timer[id].setalerm(function() { Loop0(id); }, dt[id]);
  }

  function Loop1(id) {
    th[id] += spd * Math.PI / 180.0;
    sph1.position.set(5 * Math.sin(th[id]) + pos[id].x[1], 2, 0); TMan.timer[id].setalerm(function() { Loop1(id); }, dt[id]);
  }

  function Loop2(id) {
    th[id] += spd * Math.PI / 180.0;
    sph2.position.set(5 * Math.sin(th[id]) + pos[id].x[1], -2, 0);

    TMan.timer[id].setalerm(function() { Loop2(id); }, dt[id]);
  }

  function Loop3(id) {
    th[id] += spd * Math.PI / 180.0;

    var col1 = new N6LHsv(0, [255, 255, 0, 0]);
    var col2 = new N6LHsv(0, [255, 255, 0, 0]);
    var col = col1.HsvGrd(div, cnt, col2.ahsv, 1);
    var str = col.Str();
    cnt++;

    sph3.material.color.set(str);
    sph3.position.set(5 * Math.sin(th[id]) + pos[id].x[1], -6, 0);

    var c = (Math.cos(th[id]) + 1.0) / 2.0;
    dt[id] = 50 + c * 450; // 50~500[ms]の可変タイマー

    TMan.timer[id].setalerm(function() { Loop3(id); }, dt[id]);
  }

}
var TMan = new N6LTimerMan(); // タイマーマネージャー
var pos = [ new N6LVector(4, true), new N6LVector(4, true), new N6LVector(4, true), new N6LVector(4, true)];
var th = [0, 0, 0, 0];
var dt = [50, 100, 150, 500];
var spd = 5.0;

...

var TManIDs = [];

キー変数 (TMan、pos、th、dt、spd)
タイマーマネージャーとボールの位置 (pos)、角度 (th)、速度 (spd)、マルチスレッド間隔 (dt)
タイマーマネージャー内の各タイマーのIDの配列 宣言済み

var cnt = 0;
var div = 72;
var sph3;

ボールの色を変更するために使用します
init() で3Dシーンを作成します

//タイマーを作成します
TManIDs = [TMan.add(), TMan.add(), TMan.add(), TMan.add(), TMan.add()];

5つのタイマーを作成します

//最初の実行
Loop0(TManIDs[0]);
Loop1(TManIDs[1]);
Loop2(TManIDs[2]);
Loop3(TManIDs[3]);
RDLoop(TManIDs[4]);

それぞれの最初の実行
以前はIDマジックナンバーを手動で入力していましたが、Geminiでこのようにするように警告されました><

function RDLoop(id) {

// レンダリング
renderer.render(scene, camera);
TMan.timer[id].setalerm(function() { RDLoop(id); }, 50);
}

これはレンダリングスレッドです。 Loop0()、Loop1()、Loop2()、Loop3() は、各ボールの動きを表すスレッドです。
Loop0() を見てみましょう。

function Loop0(id) {
  th[id] += spd * Math.PI / 180.0;
  sph0.position.set(5 * Math.sin(th[id]) + pos[id].x[1], 6, 0);

  TMan.timer[id].setalerm(function() { Loop0(id); }, dt[id]);
}

th[id] += spd * Math.PI / 180.0;
th[id] に角度 spd 度が加算されます。
sph0.position.set(5 * Math.sin(th[id]) + pos[id].x[1], 6, 0);
角度から計算された位置が球に適用されます。
TMan.timer[id].setalerm(function() { Loop0(id); }, dt[id]);
Loop0(id) は dt[id] 間隔で再度呼び出されます。
var dt = [50, 100, 150, 500];
はこのように宣言されているため、
Loop0 は 50ms ごとに、Loop1 は 100ms ごとに、Loop2 は 150ms ごとに呼び出されます。

function Loop3(id) {
  th[id] += spd * Math.PI / 180.0;

  var col1 = new N6LHsv(0, [255, 255, 0, 0]);
  var col2 = new N6LHsv(0, [255, 255, 0, 0]);
  var col = col1.HsvGrd(div, cnt, col2.ahsv, 1);
  var str = col.Str();
  cnt++;

  sph3.material.color.set(str);
  sph3.position.set(5 * Math.sin(th[id]) + pos[id].x[1], -6, 0);

  var c = (Math.cos(th[id]) + 1.0) / 2.0;
  dt[id] = 50 + c * 450; // 50~500[ms]の可変タイマー

  TMan.timer[id].setalerm(function() { Loop3(id); }, dt[id]);
}

Loop3
col1(ARGB:FFFF0000,AHSV:100,0,100,100) から col2(ARGB:FFFF0000,AHSV:100,0,100,100) まで
RGB色相環の周りに、div(72)個のセグメントに分割されたHSVグラデーションを作成します。
色は sph3.material.color.set(str); で適用されます。
var c = (Math.cos(th[id]) + 1.0) / 2.0;
dt[id] = 50 + c * 450; //50~500[ms] 可変タイマー
コードが追加されているので、50~500[ms] の可変タイマーになります。
そのため、N6LTimerMan を使うと簡単にマルチスレッド化を実現できます。
N6LTimerMan コアではソフトウェアタイマーが 1 つしか実行されないことに注意してください。
N6LTimerMan が管理するタイマーは、リソースを可能な限り圧迫しないように各コアに分散されます。
そのため、N6LTimer.add() を何度も呼び出してもあまり影響はありません。問題ありません。
もちろん、間隔が短いタイマーはシステムに負荷をかけます。

N6LTimerMan のコアとなる動作は、N6LTimerMan で最も高速なソフトウェアタイマーの1つを使用して、
コアタイマーチェックスレッドを繰り返し実行することです。

時間を測定し、各タイマーの setalerm() で設定された時間が経過すると、
各タイマーに登録されている関数を呼び出します。

これだけです。

N6LTimerMan.changeinterval(INT); // タイマーチェック間隔の変更
コアタイマーチェック間隔は、


目次に戻る
NAS6LIB リポジトリに戻る [Wiki 外のリンク]

Clone this wiki locally