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();
}
}
}