using System; using System.Drawing; using System.Windows.Forms; namespace _3dDraw { /// <summary> /// 3次元座標を2次元座標に変換するクラス /// </summary> public class CoordinateConverter { // 視点を表す角度プロパティ(単位は度: Degree) // X軸周りの回転角度 (xθ) public double AngleX { get; set; } = 0.0; // Y軸周りの回転角度 (yθ) public double AngleY { get; set; } = 0.0; // Z軸周りの回転角度 (zθ) public double AngleZ { get; set; } = 0.0; /// <summary> /// カメラから投影面までの距離(透視投影用)。 /// 0以下の値を設定すると、奥行きを無視した平行投影(正投影)になります。 /// </summary> public double CameraDistance { get; set; } = 1000.0; /// <summary> /// X, Y, Z軸の表示/非表示を切り替えるフラグ。 /// [0]:X軸, [1]:Y軸, [2]:Z軸。デフォルトは全てtrue。 /// </summary> public bool[] AxisDraw { get; set; } = new bool[] { true, true, true }; /// <summary> /// 描画する軸の長さ(原点からプラス・マイナス方向それぞれへの長さ) /// </summary> public double AxisLength { get; set; } = 500.0; /// <summary> /// 描画対象の3次元座標データの配列 (複数の線分データを保持) /// </summary> public double[][,] Points3D { get; set; } /// <summary> /// 描画先の PictureBox インスタンス /// </summary> public PictureBox TargetPictureBox { get; set; } /// <summary> /// プロパティに設定された Points3D のうち、0番目のポイントデータを2次元座標に変換して返します。 /// </summary> /// <returns>0番目のポイントデータの2次元座標配列</returns> public double[,] Convert() { if (Points3D != null && Points3D.Length > 0 && Points3D[0] != null) { return Convert(Points3D[0]); } return null; } /// <summary> /// n × 3 の3次元座標配列を、n × 2 の2次元座標配列に変換します。 /// </summary> /// <param name="points3D">n行3列の2次元配列 (x, y, z)</param> /// <returns>n行2列の2次元配列 (x, y)</returns> public double[,] Convert(double[,] points3D) { // 入力配列の行数(n)を取得 int n = points3D.GetLength(0); // 列数が3であることを確認 if (points3D.GetLength(1) != 3) { throw new ArgumentException("入力配列は n × 3 のサイズである必要があります。"); } // 戻り値用の n × 2 配列を初期化 double[,] points2D = new double[n, 2]; // 角度(度)をラジアンに変換 double radX = AngleX * Math.PI / 180.0; double radY = AngleY * Math.PI / 180.0; double radZ = AngleZ * Math.PI / 180.0; // 回転計算用のサイン・コサインを事前計算 double cx = Math.Cos(radX); double sx = Math.Sin(radX); double cy = Math.Cos(radY); double sy = Math.Sin(radY); double cz = Math.Cos(radZ); double sz = Math.Sin(radZ); for (int i = 0; i < n; i++) { double x = points3D[i, 0]; double y = points3D[i, 1]; double z = points3D[i, 2]; // 1. Z軸周りの回転 double x1 = x * cz - y * sz; double y1 = x * sz + y * cz; double z1 = z; // 2. Y軸周りの回転 double x2 = x1 * cy + z1 * sy; double y2 = y1; double z2 = -x1 * sy + z1 * cy; // 3. X軸周りの回転 double x3 = x2; double y3 = y2 * cx - z2 * sx; double z3 = y2 * sx + z2 * cx; double projX = x3; double projY = y3; // 透視投影(遠近法)の適用 if (CameraDistance > 0) { // カメラの距離とZ座標からスケール(縮尺)を計算 // Zがプラス(奥)に行くほどスケールが小さくなる double scale = CameraDistance / (CameraDistance - z3); projX *= scale; projY *= scale; } // 結果を配列に格納 points2D[i, 0] = projX; points2D[i, 1] = projY; } return points2D; } /// <summary> /// 全てのポイントデータを同じ色と太さで描画します。 /// </summary> public void Draw(Color color, float thickness) { Draw(new Color[] { color }, new float[] { thickness }); } /// <summary> /// 設定された複数の Points3D を2次元座標に変換し、TargetPictureBox にそれぞれ別の線で描画します。 /// </summary> /// <param name="colors">それぞれのポイントデータに対応する線の色の配列</param> /// <param name="thicknesses">それぞれのポイントデータに対応する線の太さの配列</param> public void Draw(Color[] colors, float[] thicknesses) { if (TargetPictureBox == null) { throw new InvalidOperationException("TargetPictureBox が設定されていません。"); } if (Points3D == null || Points3D.Length == 0) { return; // データがない場合は何もしない } // PictureBox の Image を準備 Bitmap bmp = TargetPictureBox.Image as Bitmap; if (bmp == null || bmp.Width != TargetPictureBox.Width || bmp.Height != TargetPictureBox.Height) { bmp = new Bitmap(TargetPictureBox.Width, TargetPictureBox.Height); } // PictureBoxの中心を原点(0,0)とするためのオフセット float offsetX = TargetPictureBox.Width / 2.0f; float offsetY = TargetPictureBox.Height / 2.0f; using (Graphics g = Graphics.FromImage(bmp)) { // 背景をクリア g.Clear(Color.White); g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // --- 軸の描画 --- if (AxisDraw != null && AxisDraw.Length >= 3) { // 薄い赤、薄い緑、薄い青 (半透明のARGBで指定し、背景に馴染むようにします) Color[] axisColors = new Color[] { Color.FromArgb(128, 255, 0, 0), // 薄い赤 (X軸) Color.FromArgb(128, 0, 255, 0), // 薄い緑 (Y軸) Color.FromArgb(128, 0, 0, 255) // 薄い青 (Z軸) }; // 原点を通る各軸の直線データ (マイナス方向からプラス方向へ) double[][,] axisPoints = new double[][,] { new double[,] { { -AxisLength, 0, 0 }, { AxisLength, 0, 0 } }, // X軸 new double[,] { { 0, -AxisLength, 0 }, { 0, AxisLength, 0 } }, // Y軸 new double[,] { { 0, 0, -AxisLength }, { 0, 0, AxisLength } } // Z軸 }; for (int i = 0; i < 3; i++) { if (AxisDraw[i]) { // 軸の3次元座標を2次元に変換 double[,] p2D = Convert(axisPoints[i]); PointF[] drawPts = new PointF[2]; for (int j = 0; j < 2; j++) { drawPts[j] = new PointF((float)p2D[j, 0] + offsetX, -(float)p2D[j, 1] + offsetY); } // 軸を描画 (少し細めの 1.5f に設定) using (Pen axisPen = new Pen(axisColors[i], 1.5f)) { g.DrawLine(axisPen, drawPts[0], drawPts[1]); } } } } // 各ポイントデータ(線分のまとまり)ごとに描画処理を行う for (int dataIndex = 0; dataIndex < Points3D.Length; dataIndex++) { double[,] currentPoints3D = Points3D[dataIndex]; if (currentPoints3D == null || currentPoints3D.GetLength(0) == 0) continue; // 1. 3次元座標を2次元座標に変換 (既存のメソッドを利用) double[,] points2D = Convert(currentPoints3D); int n = points2D.GetLength(0); // 2. 描画用の PointF 配列を作成 PointF[] drawPoints = new PointF[n]; for (int i = 0; i < n; i++) { // Y軸は画面では下方向がプラスになるため反転 (-y) させます drawPoints[i] = new PointF( (float)points2D[i, 0] + offsetX, -(float)points2D[i, 1] + offsetY ); } // 色と太さを配列から取得 // (指定された要素数が足りない場合は、最後の要素を再利用するか黒/1.0fにする安全設計) Color color = (colors != null && colors.Length > 0) ? colors[Math.Min(dataIndex, colors.Length - 1)] : Color.Black; float thickness = (thicknesses != null && thicknesses.Length > 0) ? thicknesses[Math.Min(dataIndex, thicknesses.Length - 1)] : 1.0f; // 3. 描画 using (Pen pen = new Pen(color, thickness)) { if (n > 1) { // 順番に線でつなぐ g.DrawLines(pen, drawPoints); } else if (n == 1) { // 1点のみの場合は点を描画 using (Brush brush = new SolidBrush(color)) { g.FillEllipse(brush, drawPoints[0].X - thickness / 2, drawPoints[0].Y - thickness / 2, thickness, thickness); } } } } } // 結果を反映 TargetPictureBox.Image = bmp; TargetPictureBox.Invalidate(); } } }