JavaScriptで書いた行列演算や3D回転のコードを、そのままの挙動でC# / .NETに移植したい、というニーズはかなり多いです。Webブラウザ上で試作したロジックを、WPF や Windows アプリ、Unity やコンソールツールに持ち込みたいとき、言語間の「配列」「数値型」「行列の向き」の違いでつまずきがちです。この記事では、JSの map/reduce スタイルの行列コードを、.NETの float[,] に丁寧に落とし込む具体的な手順と、実務でそのまま使えるサンプルコードを詳しく解説します。
JavaScriptの行列・回転コードをC#に移植するときの全体像
まず、JavaScript と C# の「考え方の差」を整理しておくと、移植作業がとてもスムーズになります。ポイントは次の4つです。
- 配列と行列の表現方法が違う(JS: ネスト配列、C#: 2次元配列やジャグ配列)
- 数値型が違う(JS:
Number=倍精度、C#:float/double) - Math系APIが違う(C# の
Mathはdouble、MathFはfloat) - 回転順序と行列の掛け方が結果を変える(X → Y → Z なのか、Z → Y → X なのか)
よくある移植のイメージを、JavaScript と C# の対応表で眺めてみましょう。
| 役割 | JavaScript側 | C# / .NET側 | ポイント |
|---|---|---|---|
| 頂点 | { x: 1, y: 2, z: 3 } | struct Vertex { float x, y, z; } | フィールド名を揃えると移植しやすい |
| 列ベクトル | Object.values(v).map(v => [v]) | float[,] v = { {x}, {y}, {z} }; | 3×1 の「列ベクトル」に統一して扱う |
| 行列 | number[][](ネスト配列) | float[,](2次元配列) | a[i][j] と a[i, j] の違いに注意 |
| 行列積 | map + reduce | for ループ(三重ループ) | 性能と読みやすさのバランスでループがおすすめ |
| 角度 | Math.sin / Math.cos | MathF.Sin / MathF.Cos | どちらもラジアン。float なら MathF で統一 |
JavaScript側の想定コードイメージ
よくある行列積と回転の例として、JavaScript側では次のようなコードが書かれていることが多いはずです。
// m1: r×k, m2: k×c
function multiplyMatrices(m1, m2) {
return m1.map((row, i) =>
m2[0].map((_, j) =>
row.reduce((acc, _, n) => acc + m1[i][n] * m2[n][j], 0)
)
);
}
// 頂点を X → Y → Z の順に回転
const vertex = { x: 1, y: 2, z: 3 };
const mRotateX = /* 3×3 の回転行列 */;
const mRotateY = /* 3×3 の回転行列 */;
const mRotateZ = /* 3×3 の回転行列 */;
let v = Object.values(vertex).map(v => [v]); // [[x],[y],[z]]
[mRotateX, mRotateY, mRotateZ].forEach(m => {
v = multiplyMatrices(m, v); // 3×3 * 3×1
});
const [x, y, z] = v.map(row => row[0]);
重要なのは、「頂点オブジェクト → 3×1の列ベクトル → 回転行列との掛け算」という流れと、回転順序(ここでは X → Y → Z) です。この構造をそのままC#に持っていけば、ほぼ確実に同じ結果が得られます。
C#への変換で押さえるべきポイント
.map()→Enumerable.Select、.reduce()→ ループ LINQである程度は雰囲気を再現できますが、数値計算は素直なforループの方が速くて明快です。- 頂点オブジェクトの列ベクトル化 JS の
Object.values(vertex).map(v => [v])は、C#ではfloat[,] v = {{x},{y},{z}}のような 3×1 の 2次元配列に対応させます。 - 角度はラジアンで統一 JS も C# も標準の三角関数はラジアン前提です。度数法で角度を管理している場合は、
deg * (MathF.PI / 180f)で変換します。 - 回転順序を必ず合わせる 回転行列は非可換なので、X→Y→Z と Z→Y→X では全く違う姿勢になります。JSの順番をそのままC#側でも再現しましょう。
2次元配列+forループで書く最小実装(おすすめパターン)
実務で一番扱いやすいのは、float[,] を使った2次元配列+三重ループの実装です。JS の multiplyMatrices を素直にC#に翻訳すると、次のようになります。
using System;
public static class Matrix3
{
// 汎用行列積 (a: r×k, b: k×c) => r×c
public static float[,] Multiply(float[,] a, float[,] b)
{
int r = a.GetLength(0), k = a.GetLength(1);
int kb = b.GetLength(0), c = b.GetLength(1);
if (k != kb) throw new ArgumentException("行列のサイズが一致しません。");
var result = new float[r, c];
for (int i = 0; i < r; i++)
for (int j = 0; j < c; j++)
{
float acc = 0f;
for (int n = 0; n < k; n++)
acc += a[i, n] * b[n, j];
result[i, j] = acc;
}
return result;
}
// 3D回転行列(ラジアン)
public static float[,] RotateX(float rad)
{
float c = MathF.Cos(rad), s = MathF.Sin(rad);
return new float[,] { { 1, 0, 0 }, { 0, c, -s }, { 0, s, c } };
}
public static float[,] RotateY(float rad)
{
float c = MathF.Cos(rad), s = MathF.Sin(rad);
return new float[,] { { c, 0, s }, { 0, 1, 0 }, { -s, 0, c } };
}
public static float[,] RotateZ(float rad)
{
float c = MathF.Cos(rad), s = MathF.Sin(rad);
return new float[,] { { c, -s, 0 }, { s, c, 0 }, { 0, 0, 1 } };
}
}
public class Example
{
public struct Vertex { public float x, y, z; }
public static void Main()
{
// 入力頂点
var vertex = new Vertex { x = 1, y = 2, z = 3 };
// 角度(ラジアン)
float angleX = 0.3f, angleY = 0.5f, angleZ = -0.2f;
// 列ベクトルに変換 [[x],[y],[z]]
float[,] v = new float[,] { { vertex.x }, { vertex.y }, { vertex.z } };
// JSと同じ順序で回転を適用:X → Y → Z
foreach (var m in new float[][,] {
Matrix3.RotateX(angleX),
Matrix3.RotateY(angleY),
Matrix3.RotateZ(angleZ)
})
{
v = Matrix3.Multiply(m, v); // 3×3 * 3×1
}
// 結果を構造体へ戻す
vertex.x = v[0, 0];
vertex.y = v[1, 0];
vertex.z = v[2, 0];
Console.WriteLine($"({vertex.x}, {vertex.y}, {vertex.z})");
}
}
最小実装のポイント解説
Matrix3.Multiply行列の「行」「列」サイズを動的に取得しているので、3×3 に限らず汎用的に使えます。kとkbを比較してサイズチェックをしている点も重要です。- 三重ループで
map+reduceを再現 JS のrow.reduce部分が、C#側ではfor (int n = 0; n < k; n++)の内側ループに対応します。 - 回転行列の定義
RotateX/Y/Zはそれぞれ 3×3 行列を返します。式は線形代数の教科書通りなので、JS側と並べて確認しやすくなっています。 - MathF を使う理由
Math.Sin/Math.Cosはdoubleを返しますが、ここではfloatで計算したいのでMathFを使っています。これにより毎回のキャストを避け、コードもスッキリします。 - 列ベクトルの形を合わせる JS の
[[x],[y],[z]]に対応して、C# ではnew float[,] { { x }, { y }, { z } }としています。この形を崩さないことで、行列の掛け順をJSと完全に一致させられます。
列ベクトルと行列の掛け順を意識する
ここで使っているスタイルは、「列ベクトル × 左から行列を掛ける」方式です。
- 頂点:3×1 の列ベクトル
- 回転行列:3×3
- 計算:
v = MZ × (MY × (MX × v))
この「向き」を JS と C# で揃えることが、結果一致への近道です。もし JS 側で右から掛けている(v * M)実装になっている場合は、C#側でも行ベクトル+右掛けに統一する必要があります。
LINQでJavaScriptのmap/reduceの雰囲気を再現する
「可読性を優先して、JS の map / reduce っぽい書き味に寄せたい」という場合は、LINQ を使って次のように書くこともできます。あくまで読みやすさ重視の選択肢です。
using System;
using System.Linq;
public static class Matrix3Linq
{
public static float[,] Multiply(float[,] a, float[,] b)
{
int r = a.GetLength(0), k = a.GetLength(1);
int kb = b.GetLength(0), c = b.GetLength(1);
if (k != kb) throw new ArgumentException("行列のサイズが一致しません。");
var result = new float[r, c];
var rows = Enumerable.Range(0, r);
var cols = Enumerable.Range(0, c);
var ks = Enumerable.Range(0, k);
foreach (var i in rows)
{
foreach (var j in cols)
{
result[i, j] = ks
.Select(n => a[i, n] * b[n, j])
.Sum();
}
}
return result;
}
}
やっていることは三重ループ版と同じですが、内側の累積処理を Select + Sum で書き換えています。JavaScript の
row.reduce((acc, _, n) => acc + m1[i][n] * m2[n][j], 0)
に対応するのが、C#版の
ks.Select(n => a[i, n] * b[n, j]).Sum();
という形です。
| 観点 | forループ版 | LINQ版 |
|---|---|---|
| 可読性 | 慣れていれば分かりやすい | JSの map/reduce に近く直感的 |
| パフォーマンス | 最速になりやすい | LINQのオーバーヘッド分遅くなりがち |
| 最適化 | JIT最適化が効きやすい | インライン化されにくく、GC負荷が増えることも |
| 用途 | ゲームループ、リアルタイム描画など | ツール、オフライン変換、学習用サンプルなど |
ゲームやリアルタイム描画など、1フレームごとに大量の頂点を回す場面では forループ版一択 に近いです。一方、「移植元のJSコードとの対応を確認しながら、アルゴリズムを理解したい」といった場面ではLINQ版が役立ちます。
System.Numericsで行列・ベクトルを自前実装しない方法
回転だけが目的なら、System.Numerics.Vector3 と Matrix4x4 を使うと、行列の自前実装すら不要になります。以下は、JS と同じように X → Y → Z の順に回転を適用する例です。
using System;
using System.Numerics;
public class ExampleNumerics
{
public static void Main()
{
var v = new Vector3(1, 2, 3);
float ax = 0.3f, ay = 0.5f, az = -0.2f;
v = Vector3.Transform(v, Matrix4x4.CreateRotationX(ax)); // X
v = Vector3.Transform(v, Matrix4x4.CreateRotationY(ay)); // Y
v = Vector3.Transform(v, Matrix4x4.CreateRotationZ(az)); // Z
Console.WriteLine(v);
}
}
System.Numerics を使うメリット
- 行列演算の実装が不要 回転行列の定義や乗算ロジックを自分で書く必要がありません。バグの入り込みやすい領域を、標準ライブラリに丸投げできます。
- SIMD最適化が効く 実行環境によっては、Vector3/Matrix4x4 の演算が SIMD で高速化されます。大量の頂点変換でも速度面で有利です。
- 将来的な拡張がしやすい 回転だけでなく、
Matrix4x4.CreateTranslationやCreateScaleなどを組み合わせれば、ワールド変換・ビュー変換なども統一的に扱えます。
JS側と結果を合わせるときの注意点
- 回転順序
CreateRotationX/Y/Zを JS と同じ順番で適用します。Vector3.Transformの呼び出し順は、そのまま回転順序に直結します。 - 座標系(左手系/右手系) three.js や WebGL など、JS 側のライブラリによっては座標系や行列の掛け順が異なります。結果が微妙に違う場合は、Z軸の正負や行列の転置を疑ってみましょう。
- 行列の掛け順
Matrix4x4は「ベクトル × 行列」か「行列 × ベクトル」かを意識する必要があります。Vector3.Transformはドキュメント通りに使えば問題ありませんが、独自にMatrix4x4を掛け合わせるときは注意が必要です。
JavaScriptとC#の配列・数値型の違いを整理する
移植時に一番ハマりやすいのが、「配列の形と数値型の違い」です。ここをきちんと整理しておくと、バグの原因を追いやすくなります。
| 観点 | JavaScript | C# | 注意点 |
|---|---|---|---|
| 数値型 | Number(倍精度浮動小数点) | float / double | float で計算すると JS より微妙に誤差が大きくなることがある |
| 三角関数 | Math.sin / Math.cos | MathF.Sin / MathF.Cos | Math は double 戻り値なので、floatを使うなら MathF 推奨 |
| 2次元配列(行列) | number[][] | float[,] | アクセスは a[i][j] vs a[i, j] |
| ジャグ配列 | 事実上同じ(長さが違ってもOK) | float[][] | 1行ごとに配列を持つ形式。2次元配列とは別物 |
| 配列サイズ取得 | arr.length | a.GetLength(0), a.GetLength(1) | 次元ごとに GetLength を使うのがポイント |
行列計算を float[,] で実装する場合は、次の2点を徹底するだけでかなり安全になります。
a[i, j]の 添字順(行、列) を一貫して守る- 行数・列数は必ず
GetLengthで取得する(マジックナンバーを書かない)
逆に、JS 側の行列が「列メジャーで格納されているのか、行メジャーなのか」があいまいな場合は、移植前にコメントを補っておくと、後からC#に持ってきやすくなります。
度数法とラジアン、回転順序の落とし穴
JSからC#に移植する際に、バグの温床になりやすいのが「角度の扱い」と「回転順序」です。
度数法(°)からラジアンへの変換
JS側で次のように「度数」で角度を持っているコードはよくあります。
const angleDeg = 45;
const angleRad = angleDeg * Math.PI / 180;
C# 側でも同様に、行列を作るときは必ずラジアンに変換します。
float angleDeg = 45f;
float angleRad = angleDeg * (MathF.PI / 180f);
var rotX = Matrix3.RotateX(angleRad);
度数法のまま MathF.Sin に渡してしまうと、見た目は少しだけ回転しているように見えても、数値としては完全に別物になります。特に3Dモデリングツールなどと連携するとき、度数・ラジアンの混在は典型的なバグ原因です。
回転順序を必ず合わせる
回転行列同士の掛け算は非可換です。
RotateX → RotateY → RotateZRotateZ → RotateY → RotateX
この2つは同じ角度を使っていても、結果の姿勢が全く違うものになります。JS側が X → Y → Z の順に回しているなら、C#でも X → Y → Z の順番を再現する必要があります。
この記事の例では、JS・C#ともに
- X軸回りに回転
- その結果をY軸回りに回転
- さらにZ軸回りに回転
という順序に統一しています。移植するときは、必ず「どの軸から回しているのか」をコメントやドキュメントで明示しましょう。
JSとC#で結果を合わせるためのチェックリスト
「見た目がなんとなく違う」「少しだけズレている」といったときは、次のチェックリストを上から順に確認してみてください。
- 角度はラジアンになっているか 度数のまま
MathF.Sin/MathF.Cosに渡していないか。 - float と double が混在していないか
Math.Sinを使ってdoubleで計算した後、雑にfloatにキャストしていないか。 - 列ベクトル vs 行ベクトルの扱い JS側では 3×1 の列ベクトルなのに、C#で 1×3 の行ベクトルとして扱っていないか。
- 回転順序が完全に一致しているか X → Y → Z と Z → Y → X 等の違いを見逃していないか。
- 座標系の違い JS側のライブラリ(three.js 等)が右手系、C#側(例えばDirectX系)が左手系、のような違いがないか。
- 行列の要素を転置していないか JSの行列定義を、そのままの順番でC#の2次元配列に写しているか(列メジャーを行メジャーと勘違いしていないか)。
簡単な検証方法(JSとC#の結果を比較する)
「ちゃんと移植できているか不安」という場合は、同じ入力に対するJSとC#の出力を比較する小さなテストコードを用意すると安心です。
検証の手順例
- JS側で適当な頂点と角度を決め、回転後の座標をコンソールに出力
- C#側で「同じ頂点」「同じ角度」「同じ順序」で回転し、結果を出力
- 小数第4位くらいまで一致するか確認(float の場合は誤差が少し大きくなることがあります)
例えば、JS側で次のようなログを出したとします。
console.log("JS:", rotatedX.toFixed(4), rotatedY.toFixed(4), rotatedZ.toFixed(4));
C#側では、同じ形式で出力して比較できます。
Console.WriteLine($"C#: {vertex.x:F4}, {vertex.y:F4}, {vertex.z:F4}");
ここで、小数第4位~5位まで一致していれば、ほぼ正しく移植できていると考えて良いでしょう。もし1桁目からズレているような場合は、前述のチェックリストを見直します。
もう一歩踏み込んだC#側の設計例
単純な移植に成功したら、次は「C#らしく扱いやすい設計」にリファクタリングしてみるのもおすすめです。例えば、頂点構造体に「回転するメソッド」を生やしてしまう方法があります。
public struct Vertex
{
public float x, y, z;
public Vertex(float x, float y, float z)
{
this.x = x; this.y = y; this.z = z;
}
public Vertex Transform(float[,] m)
{
// this を列ベクトル [[x],[y],[z]] として扱う
float[,] v = { { x }, { y }, { z } };
v = Matrix3.Multiply(m, v);
return new Vertex(v[0, 0], v[1, 0], v[2, 0]);
}
}
これを使うと、C#側のコードは次のように書けます。
var vertex = new Vertex(1, 2, 3);
float ax = 0.3f, ay = 0.5f, az = -0.2f;
var rotated = vertex
.Transform(Matrix3.RotateX(ax))
.Transform(Matrix3.RotateY(ay))
.Transform(Matrix3.RotateZ(az));
Console.WriteLine($"({rotated.x}, {rotated.y}, {rotated.z})");
JS の [mRotateX, mRotateY, mRotateZ].forEach(...) 的な「逐次適用」のイメージを保ちつつ、C#らしいオブジェクト指向スタイルに近づけることができます。
まとめ
最後に、JavaScriptの行列・回転コードを .NET / C# に移植するときのポイントを整理しておきます。
- map/reduce は C# では Select と for ループに置き換えるのが実務的 LINQ で雰囲気を再現することはできても、性能と明快さを考えると2次元配列+三重ループが無難です。
- 頂点は 3×1 の列ベクトルとして統一 JS の
[[x],[y],[z]]に合わせて、C# ではfloat[,] v = {{x},{y},{z}}にしておくと、行列の掛け順をそのまま移植できます。 - 角度は必ずラジアンにし、MathFでfloatに統一 度数法のまま三角関数に渡さないこと、
MathとMathFを混在させないことが重要です。 - 回転順序は JS と完全に揃える X → Y → Z なのか、Z → Y → X なのかで結果が大きく変わります。移植前に、元のコードの順序を必ず確認しましょう。
- System.Numerics を使えば行列実装そのものを省略できる 単に頂点を回転したいだけなら、
Vector3とMatrix4x4.CreateRotation*の組み合わせが最もシンプルです。 - JSとC#で同じ入力を与えて結果を比較する 小さな検証用コードを1つ用意しておくだけで、「本当に同じ計算になっているか」を素早く確認できます。
この記事で紹介した「2次元配列+ループの最小実装」と「System.Numerics 版」をベースにすれば、既存の JavaScript の行列・回転コードを .NET/C# にスムーズに移植できるはずです。Webで試作した3Dロジックを、デスクトップアプリやゲームエンジンに活用する足掛かりとして、ぜひ活用してみてください。

コメント