JavaScriptの行列・回転コードをC#に完全移植する実践ガイド|.NETで3D回転行列を正しく扱う方法

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 + reducefor ループ(三重ループ)性能と読みやすさのバランスでループがおすすめ
角度Math.sin / Math.cosMathF.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 =&gt; a[i, n] * b[n, j])
                    .Sum();
            }
        }

        return result;
    }
}

やっていることは三重ループ版と同じですが、内側の累積処理を Select + Sum で書き換えています。JavaScript の


row.reduce((acc, _, n) =&gt; acc + m1[i][n] * m2[n][j], 0)

に対応するのが、C#版の


ks.Select(n =&gt; 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#の配列・数値型の違いを整理する

移植時に一番ハマりやすいのが、「配列の形と数値型の違い」です。ここをきちんと整理しておくと、バグの原因を追いやすくなります。

観点JavaScriptC#注意点
数値型Number(倍精度浮動小数点)float / doublefloat で計算すると JS より微妙に誤差が大きくなることがある
三角関数Math.sin / Math.cosMathF.Sin / MathF.CosMath は double 戻り値なので、floatを使うなら MathF 推奨
2次元配列(行列)number[][]float[,]アクセスは a[i][j] vs a[i, j]
ジャグ配列事実上同じ(長さが違ってもOK)float[][]1行ごとに配列を持つ形式。2次元配列とは別物
配列サイズ取得arr.lengtha.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 → RotateZ
  • RotateZ → 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#の出力を比較する小さなテストコードを用意すると安心です。

検証の手順例

  1. JS側で適当な頂点と角度を決め、回転後の座標をコンソールに出力
  2. C#側で「同じ頂点」「同じ角度」「同じ順序」で回転し、結果を出力
  3. 小数第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ロジックを、デスクトップアプリやゲームエンジンに活用する足掛かりとして、ぜひ活用してみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次