概要
C#で3D形状、3Dモデルを簡単に表示・操作し、画像として保存するまでの良いテンプレートが見つかりませんでしたので、自分の行っている手法を紹介いたします。本記事では、3D表示にOpenGLのラッパーであるOpenTKを、画像データの保存にOpenCVのC#ラッパーのOpenCVSharpを、3D形状の読み込みにSurfaceAnalyzerを使います。完成イメージは以下のgifになります。はじめに三次元形状を読み込み、マウスで回転動作をし、画像として保存します。
なお、今回用いたソースコードはgitにまとめています。
準備
初めに、必要なライブラリのインストールを行います。
Windows Form Applicationの作成
今回は、WindowsFormApplicationとして作成していきます。
OpenTK, OpenTK.GLControlのインストール
OpenTKは、C#でOpenGLを扱うためのライブラリです。GLControlは、フォーム上にOpenGLの画面を表示するためのコントロールです。
OpenCVSharp4, OpenCVSharp4.runtime.winのインストール
OpenCVSharp4は、C#でOpenCVを使うためのラッパーです。今回は、画像を保存するために使用するため、画像保存が不要でしたらインストール不要です。System.Drawing.Bitmapでも良いのですが、取得した画像後処理をかけることを考慮して汎用性のあるこちらを用います。画像をウィンドウで表示するためには、それぞれの環境にあったruntimeもインストールする必要があります。Windowsの場合は、OpenCVSharp.runtime.winをインストールしましょう。
Surface Analyzerのインストール
Surface Analyzerは、C#でSTLデータを扱うためのライブラリです。詳細な説明は、こちらの記事をご覧ください。STLファイルを読み込むことができれば、他のライブラリでも大丈夫です。
それでは、これからコーディングを始めます。本記事では、**実際に形状を表示する"Viewerフォーム"**と、**その表示をコントロールする"Controlフォーム"**に分けてコーディングしていきます。
Viewerフォームのコーディング
Viewerフォームの作成
今回は、3D形状を表示する専用のフォームを作成し、Controlフォームから操作します。[プログラム名]>追加>フォーム(Windows Form)の順にクリックします。
ビューアの設定
下記のようにGLControlを追加し、イベントを追加します。こちらのサイトを参考にしています。GLControlはデザイナ側でも追加可能ですが、なぜかうまくいかないことが多いので自分はこちらの方法を利用しています。追加したイベントは画面上をカメラ操作するためのものです。右クリック&ドラッグ回転し、ホイール回転で回転中心からの距離を制御します。
using System;
using System.Drawing;
using System.Windows.Forms;
// openTK
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
// surfaceAnalyzer
using SurfaceAnalyzer;
namespace _3dview
{
public partial class Viewer : Form
{
#region Camera__Field
bool isCameraRotating; //カメラが回転状態かどうか
Vector2 current, previous; //現在の点、前の点
float zoom = 1.0f; //拡大度
double rotateX = 1, rotateY = 0, rotateZ = 0;//カメラの回転による移動
float theta = 0;
float phi = 0;
#endregion
public Viewer()
{
InitializeComponent();
AddglControl();
}
// glControlの追加
GLControl glControl;
private void AddglControl()
{
SuspendLayout();
int width = this.Width;
int height = this.Height;
//GLControlの初期化
glControl = new GLControl();
glControl.Name = "SHAPE";
glControl.Size = new Size(width, height);
glControl.Location = new System.Drawing.Point(0, 0);
glControl.SendToBack();
//イベントハンドラ
glControl.Load += new EventHandler(glControl_Load);
glControl.Resize += new EventHandler(glControl_Resize);
glControl.MouseDown += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseDown);
glControl.MouseMove += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseMove);
glControl.MouseUp += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseUp);
glControl.MouseWheel += new System.Windows.Forms.MouseEventHandler(this._3DView_MouseWheel);
Controls.Add(glControl);
ResumeLayout(false);
}
private void glControl_Load(object sender, EventArgs e)
{
GLControl s = (GLControl)sender;
s.MakeCurrent();
GL.ClearColor(Color4.White);
GL.Enable(EnableCap.DepthTest);
Update();
}
private void glControl_Resize(object sender, EventArgs e)
{
GL.Viewport(0, 0, glControl.Size.Width, glControl.Size.Height);
GL.MatrixMode(MatrixMode.Projection);
Matrix4 projection = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4,
(float)glControl.Size.Width / (float)glControl.Size.Height, 1.0f, 256.0f);
GL.LoadMatrix(ref projection);
Update();
}
private void _3DView_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
// 右ボタンが押された場合
if (e.Button == MouseButtons.Right)
{
isCameraRotating = true;
current = new Vector2(e.X, e.Y);
}
Update();
}
private void _3DView_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
//右ボタンが押された場合
if (e.Button == MouseButtons.Right)
{
isCameraRotating = false;
previous = Vector2.Zero;
}
Update();
}
private void _3DView_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
// カメラが回転状態の場合
if (isCameraRotating)
{
previous = current;
current = new Vector2(e.X, e.Y);
Vector2 delta = current - previous;
delta /= (float)Math.Sqrt(this.Width * this.Width + this.Height * this.Height);
float length = delta.Length;
if (length > 0.0)
{
theta += delta.X * 10;
phi += delta.Y * 10;
rotateX = Math.Cos(theta) * Math.Cos(phi);
rotateY = Math.Sin(phi);
rotateZ = Math.Sin(theta) * Math.Cos(phi);
}
Update();
}
}
private void _3DView_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
{
float delta = e.Delta;
zoom *= (float)Math.Pow(1.001, delta);
//拡大、縮小の制限
if (zoom > 4.0f)
zoom = 4.0f;
if (zoom < 0.03f)
zoom = 0.03f;
Update();
}
}
}
3D形状のレンダリングメソッドの追加
次に、3D形状の表示部を作成します。
Update()メソッドは、画面の表示の更新があるたびに呼ばれるメソッドです。ここから毎回Render()メソッドを呼び出します。
Render()メソッドは、画面の表示を変更するメソッドとなります。引数polygonは、SurfaceAnalyzerにより読み込んだ形状となります。このメソッドをControl.cs側から呼ぶことで画面表示を操作します。
DrawPolygons()メソッドは読み込んだ形状のポリゴンを一つ一つ表示します。GL.Begin()とGL.End()で囲まれた間で色、法線、頂点を与えることで画面上に表示することができます。ここでは、法線の方向に応じて色を面の描画色を指定しています。
N2TK()メソッドでは、System.NumericsとOpenTKのVector3ベクトルを変換します。
PolygonModel Polygon;
public void Update()
{
if (Polygon == null) return;
Render(Polygon);
}
public void Render(PolygonModel polygon)
{
Polygon = polygon;
// バッファのクリア
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
// カメラ設定
Vector3 vec_rotate = new Vector3((float)rotateX, (float)rotateY, (float)rotateZ);
Vector3 center = new Vector3(N2TK(Polygon.GravityPoint()));
Vector3 eye = center + vec_rotate * center.LengthFast / zoom;
Matrix4 modelView = Matrix4.LookAt(eye, center, Vector3.UnitY);
// 表示設定
GL.MatrixMode(MatrixMode.Modelview);
GL.LoadMatrix(ref modelView);
// 3D形状の表示
DrawPolygons(polygon);
// バッファの入れ替え
glControl.SwapBuffers();
}
private void DrawPolygons(PolygonModel polygon)
{
if (polygon == null) return;
//描画
GL.Begin(PrimitiveType.Triangles);
//三角形を描画
for (int l = 0; l < polygon.Faces.Count; l++)
{![Something went wrong]()
var normal = polygon.Faces[l].Normal();
GL.Color4(Math.Abs(normal.X), Math.Abs(normal.Y), Math.Abs(normal.Z), 0);
GL.Normal3(N2TK(normal));
GL.Vertex3(N2TK(polygon.Faces[l].Vertices[0].P));
GL.Vertex3(N2TK(polygon.Faces[l].Vertices[2].P));
GL.Vertex3(N2TK(polygon.Faces[l].Vertices[1].P));
}
GL.End();
}
// Numerics.Vector3をOpenTK.Vector3に変換します。
private static OpenTK.Vector3 N2TK(System.Numerics.Vector3 vec3) => new Vector3(vec3.X, vec3.Z, vec3.Y);
画像化メソッドの追加
OpenTKの表示内容を1ピクセルずつ読み取り、OpenCVSharpのMatに変換します。データを1ピクセルずつ読み込むと遅いため、メモリを直接コピーする手法を採用しています。Marshalを使用するために、usingの追加が必要です。
using System.Runtime.InteropServices;
// 画像の保存
public OpenCvSharp.Mat GetMat()
{
int width = glControl.Width;
int height = glControl.Height;
float[] floatArr = new float[width * height * 3];
OpenCvSharp.Mat ret = new OpenCvSharp.Mat(height, width, OpenCvSharp.MatType.CV_32FC3);
// dataBufferへの画像の読み込み
IntPtr dataBuffer = Marshal.AllocHGlobal(width * height * 12);
GL.ReadBuffer(ReadBufferMode.Front);
GL.ReadPixels(0, 0, width, height, PixelFormat.Bgr, PixelType.Float, dataBuffer);
// imgへの読み込み
Marshal.Copy(dataBuffer, floatArr, 0, floatArr.Length);
// opencvsharp.Matへの変換
Marshal.Copy(floatArr, 0, ret.Data, floatArr.Length);
// 破棄
Marshal.FreeHGlobal(dataBuffer);
return ret;
}
以上で、Viewerフォームのコーディングは終了です。
Controlフォームのコーディング
続いて、Controlフォームのコーディングを行います。
ボタンの配置
Controlフォーム上にボタンを追加し、それぞれ「ビューアの表示」、「形状の表示」、「保存」とします。
Viewerフォームの表示
各ボタンをダブルクリックし、イベントを追加し以下のように記述します。
初めに、「ビューアの表示」ボタンをクリックした際の動作を作成します。
using System;
using System.Windows.Forms;
namespace _3dview
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
Viewer viewer;
private void button1_Click(object sender, EventArgs e)
{
viewer = new Viewer();
viewer.Show();
}
}
}
3D形状の表示
続いて形状を読み込み、表示します。
using System;
using System.Windows.Forms;
namespace _3dview
{
public partial class Control : Form
{
public Control()
{
InitializeComponent();
}
Viewer viewer;
private void button1_Click(object sender, EventArgs e)
{
viewer = new Viewer();
viewer.Show();
}
private void button2_Click(object sender, EventArgs e)
{
// 形状の読み込み
var polygon = SurfaceAnalyzer.LoadData.LoadSTL(@"local\cube3_とんがり2.STL", true);
// 形状のレンダリング
viewer.Render(polygon);
}
private void button3_Click(object sender, EventArgs e)
{
}
}
}
実行し、「ビューアの表示」ボタンをクリックしビューアを表示します。そして「形状の表示」ボタンをクリックすると、形状が表示されます。右クリックで回転、ホイールで拡大・縮小が可能なです。
3D形状の画像化
最後に、Viewerフォームの内容を保存します。
using OpenCvSharp;
using System;
using System.Windows.Forms;
namespace _3dview
{
public partial class Control : Form
{
public Control()
{
InitializeComponent();
}
Viewer viewer;
private void button1_Click(object sender, EventArgs e)
{
viewer = new Viewer();
viewer.Show();
}
private void button2_Click(object sender, EventArgs e)
{
// 形状の読み込み
var polygon = SurfaceAnalyzer.LoadData.LoadSTL(@"local\cube3_とんがり2.STL", true);
// 形状のレンダリング
viewer.Render(polygon);
}
private void button3_Click(object sender, EventArgs e)
{
// viewerの画像の取得
using (Mat mat = viewer.GetMat())
{
// 画像の表示
Cv2.ImShow("mat", mat);
// 画像の保存
Cv2.ImWrite(@"local\mat.jpg", mat * 256);
}
}
}
}
実行[F5]し、「ビューアの表示」、「形状の表示」、「保存」の順にボタンをクリックすると画像が表示され、そのまま保存されます。OpenGLとOpenCVで座標軸の基準が異なるため、上下が反転した状態となります。
まとめ
本記事では、C#でSTLデータを読み込んで、表示・操作し、画像として保存する方法を紹介しました。C#で3D形状を扱いたいときに役に立てると幸いです。