リクエストがあったので、「OPENTKでマウスを使った立体の自由移動 平行投影編」を作成しました。
メールでこんな相談をうけました。趣旨は以下のとおりです。
戦車のプログラムを参考にいろいろと試行錯誤しておりますが、なかなか上手くできない。
やろうとしていることは
平行投影で直方体を描画する。頂点の座標は(0,0,0)、向かいの頂点の座標を(-200,-100,-20)とする。
原点にXYZ方向に矢印を表示する
XY平面のZ座標0の位置に格子を描画する
マウスを使って移動、回転、縮小拡大をするはじめに立体(50,50,0)(-50,-50,-50)で作成して、それなりに良い感じにできたが、直方体の初期座標を(0,0,0)(-200,-100,-20)に変更したところ、回転や拡大縮小が常に原点(0,0,0)を基準に処理されてしまう・・・
まとめるとこんな感じです。戦車のプログラムとはC# OpenTKで戦車を描画するのことです。
実際に質問者様が作成されたコードも添付されていたのですが、たしかにC# OpenTKで戦車を描画するを参考にしたらしく、自作メソッド名やプロパティ名がどこかで見たようなものが散見されました。
では質問と期待に応えるべくコーディングしていきましょう。
最初にZairyoXYZという配列型のフィールド変数がありますが、これが直方体を描画するための座標です。DrawBoxメソッドを実行すれば直方体が描画されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public partial class Form1 : Form { float[,] ZairyoXYZ = new float[2, 3] { { 0, 0, 0 }, { -100, -70, -20 } }; private void DrawBox() { float x1 = ZairyoXYZ[0, 0]; float y1 = ZairyoXYZ[0, 1]; float z1 = ZairyoXYZ[0, 2]; float x2 = ZairyoXYZ[1, 0]; float y2 = ZairyoXYZ[1, 1]; float z2 = ZairyoXYZ[1, 2]; GL.Color3(Color.Yellow); GL.Begin(BeginMode.LineLoop); { GL.Vertex3(x1, y1, z1); GL.Vertex3(x1, y2, z1); GL.Vertex3(x2, y2, z1); GL.Vertex3(x2, y1, z1); } GL.End(); GL.Begin(BeginMode.LineLoop); { GL.Vertex3(x1, y1, z2); GL.Vertex3(x1, y2, z2); GL.Vertex3(x2, y2, z2); GL.Vertex3(x2, y1, z2); } GL.End(); GL.Begin(BeginMode.Lines); { GL.Vertex3(x1, y1, z1); GL.Vertex3(x1, y1, z2); GL.Vertex3(x2, y2, z1); GL.Vertex3(x2, y2, z2); GL.Vertex3(x1, y2, z1); GL.Vertex3(x1, y2, z2); GL.Vertex3(x2, y1, z1); GL.Vertex3(x2, y1, z2); } GL.End(); } } |
中心座標を(0,0,0)にすれば回転や拡大の処理が簡単にできそうですが、質問者様が作ろうとしているものの性格上、こうであったほうがいいようです。しかも拡大縮小と回転の中心はマウスがある部分にしてほしいというのです。
まずはコンストラクタのなかでイベントハンドラを追加しましょう。マウスボタンが押されたとき、離されたとき、移動しているとき、マウスホイールが回転しているときに描画状態を変更するための処理をすることになります。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); glControl.MouseDown += glControl_MouseDown; glControl.MouseMove += glControl_MouseMove; glControl.MouseWheel += glControl_MouseWheel; glControl.MouseUp += glControl_MouseUp; } } |
Projection の設定ですが、透視投影ではなく平行投影を採用したいとのことです。ゲームをつくるときは近くにあるものは大きく、遠くにあるものは小さく描画される透視投影のほうがいいのかもしれませんが、図面やデザインイラストには平行投影が適しています。平行投影だと並行に並んだ辺が、どれだけ遠くにあっても同じ長さに見えます。
まずSetProjectionメソッドを以下のように変更します。CreateOrthographicOffCenterの引数は(left, right, bottom, top)に設定した数値が一番左、右、下、上の座標になります。マウスの位置を回転や拡大処理の軸にしたいということなので、そのまま割り当ててしまいます。これでクライアント座標を取得すれば描画対象の座標と一致します。
残りの2つの引数ですが、zNear~zFarの領域だけ描画します。この幅が少ないと近すぎる部分、遠すぎる部分が描画されず切れてしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { private void SetProjection() { // ビューポートの設定 GL.Viewport(0, 0, glControl.Width, glControl.Height); // 視体積の設定 GL.MatrixMode(MatrixMode.Projection); Matrix4 proj = Matrix4.CreateOrthographicOffCenter(-(float)glControl.Width/2, (float)glControl.Width/2, -(float)glControl.Height/2, (float)glControl.Height/2, -1000, 1000); GL.LoadMatrix(ref proj); // MatrixMode を元に戻す GL.MatrixMode(MatrixMode.Modelview); } } |
以下はglControlがロードされたときにおこなわれる処理です。直方体を3倍の大きさで描画しようとしています。そのため自作メソッド SetInitScaleを呼び出しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public partial class Form1 : Form { float bigsmall = 1f; float InitBigsmall = 3f; // 初期状態の拡大率 private void glControl_Load(object sender, EventArgs e) { GL.ClearColor(glControl.BackColor); // Projection の設定 SetProjection(); // デプスバッファの使用 GL.Enable(EnableCap.DepthTest); // 視界の設定 SetInitSight(); SetInitScale(); } void SetInitScale() { bigsmall = InitBigsmall; // 原点を中心にInitBigsmall倍に拡大(縮小)する float centerX = 0; float centerY = 0; // RotateScaleInfoクラス等の定義と意味は後述 RotateScaleInfo info = new RotateScaleInfo(); info.ScaleInfo = new ScaleInfo(centerX, centerY, (LastBigsmall == 0) ? bigsmall : bigsmall / LastBigsmall); RotateScaleInfos.Insert(0, info); // LastScaleCenterX、LastScaleCenterY、LastBigsmallはフィールド変数(後述) LastScaleCenterX = centerX; LastScaleCenterY = centerY; LastBigsmall = bigsmall; glControl.Refresh(); } } |
これはglControlがリサイズされたときの処理です。
1 2 3 4 5 6 7 8 9 10 |
public partial class Form1 : Form { private void glControl_Resize(object sender, EventArgs e) { SetProjection(); // 再描画 glControl.Refresh(); } } |
次にマウスの処理を考えます。左ボタンを押した状態でドラッグすれば平行移動、右ボタンを押した状態でドラッグすれば回転移動です。水平に移動させればY軸を中心に回転し、垂直に移動させればX軸を中心に回転移動します。
まず回転処理とオブジェクトを表示させる座標がプロパティで定義されていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public partial class Form1 : Form { // 立方体のX座標 float x0 = 0; float X { get { return x0; } set { x0 = (float)Math.Round(value, 2, MidpointRounding.AwayFromZero); } } // 立方体のY座標 float y0 = 0; float Y { get { return y0; } set { y0 = (float)Math.Round(value, 2, MidpointRounding.AwayFromZero); } } // 立方体のZ座標 float z0 = 0; float Z { get { return z0; } set { z0 = (float)Math.Round(value, 2, MidpointRounding.AwayFromZero); } } // X軸を中心に回転させる角度 float rx0 = 0; float RotateX { get { return rx0; } set { if (value < 0) value += 360; rx0 = value % 360; } } // Y軸を中心に回転させる角度 float ry0 = 0; float RotateY { get { return ry0; } set { if (value < 0) value += 360; ry0 = value % 360; } } } |
マウス操作をしたときに上記のプロパティを適切に変更すればうまくいきそうです(実際にやってみるとハマりどころ満載でした)。
まずはマウスボタンが押されたら必要なデータを保存しておきます。マウスをクリックした位置、回転処理をするのであれば回転の軸になる座標を保存しておかなければなりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public partial class Form1 : Form { // マウス座標(マウスをクリックした位置の保持用) PointF _oldPoint; PointF _realPoint; // 回転処理をする軸のX座標とY座標 float AxisRotationX = 0; float AxisRotationY = 0; void glControl_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { // glControl上におけるマウスの座標を取得する Point pt = glControl.PointToClient(Control.MousePosition); // ここから回転処理をする軸のX座標とY座標を取得する AxisRotationX = (pt.X - glControl.Width / 2) - X; AxisRotationY = -(pt.Y - glControl.Height / 2) - Y; } else if (e.Button == MouseButtons.Left) { _realPoint.X = X; _realPoint.Y = Y; } else if (e.Button == MouseButtons.Middle) { ViewSettingInit(); // 中央のボタンがおされたらもとの状態に戻す glControl.Refresh(); } // マウスをクリックした位置の記録 _oldPoint.X = e.X; _oldPoint.Y = e.Y; } } |
マウスがドラッグされている間はオブジェクトを移動、回転、拡大縮小させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class Form1 : Form { void glControl_MouseMove(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { // 直方体を位置を変更する X = _realPoint.X + (e.X - _oldPoint.X); Y = _realPoint.Y - (e.Y - _oldPoint.Y); glControl.Refresh(); } else if (e.Button == MouseButtons.Right) { // 直方体を回転させる RotateX = 180.0f * ((float)(e.Y - _oldPoint.Y) / glControl.Width); RotateY = 180.0f * ((float)(e.X - _oldPoint.X) / glControl.Height); glControl.Refresh(); } } } |
次にマウスボタンが離されたときの処理を示します。マウス操作をするたびに直方体は平行移動と拡大、回転を繰り返します。しかも拡大、回転の軸になる座標は固定された値ではありません。
それらの値は記憶しておかないといけません。たんにX、Y、RotateX、RotateYのプロパティのそのときの値だけに気を取られているとうまくいきません。これに気がつかず昨日は完全にハマってしまいました。
ここでは回転の処理がおこなわれたかどうかを調べて、回転処理がおこなわれた場合はリストに保存しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public partial class Form1 : Form { List<RotateScaleInfo> RotateScaleInfos = new List<RotateScaleInfo>(); void glControl_MouseUp(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { RotateX = 180.0f * ((float)(e.Y - _oldPoint.Y) / glControl.Width); RotateY = 180.0f * ((float)(e.X - _oldPoint.X) / glControl.Height); // 回転の処理がおこなわれていないなら保存の必要はない if (RotateX == 0 && RotateY == 0) return; RotateScaleInfo info = new RotateScaleInfo(); info.RotateInfo = new RotateInfo( AxisRotationX, AxisRotationY, RotateX, RotateY ); RotateScaleInfos.Insert(0, info); AxisRotationX = 0; AxisRotationY = 0; RotateX = 0; RotateY = 0; } } } |
RotateScaleInfoクラスとRotateInfoクラスはこのようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class RotateScaleInfo { public RotateInfo RotateInfo = null; public ScaleInfo ScaleInfo = null; } public class RotateInfo { public RotateInfo(float axisRotationX, float axisRotationY, float rotateX, float rotateY) { AxisRotationX = axisRotationX; AxisRotationY = axisRotationY; RotateX = rotateX; RotateY = rotateY; } public float AxisRotationX = 0; public float AxisRotationY = 0; public float RotateX = 0; public float RotateY = 0; } public class ScaleInfo { public ScaleInfo(float scaleCenterX, float scaleCenterY, float scale) { ScaleCenterX = scaleCenterX; ScaleCenterY = scaleCenterY; Scale = scale; } public float ScaleCenterX = 0; public float ScaleCenterY = 0; public float Scale = 0; } |
次にマウスホイールを動かしたときの処理ですが、マウスがある位置を中心にして拡大縮小がおこなわれるようにしなければなりません。拡大縮小率や拡大縮小の中心の座標が変化した場合はその情報をリストに格納するのですが、拡大してさらに拡大した場合は前回の拡大率との比を格納しなければなりません。これも気がつかないとハマってしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public partial class Form1 : Form { // 最後に取得されたデータ float LastScaleCenterX = 0; float LastScaleCenterY = 0; float LastBigsmall = 0; void glControl_MouseWheel(object sender, MouseEventArgs e) { if (e.Delta > 0) bigsmall += 0.02f; else { bigsmall -= 0.02f; if (bigsmall < 0.01) bigsmall = 0.01f; } Point pt = glControl.PointToClient(Control.MousePosition); // 拡大縮小の中心点を求める float scaleCenterX = (pt.X - glControl.Width / 2) - X; float scaleCenterY = -(pt.Y - glControl.Height / 2) - Y; // 拡大縮小率や拡大縮小の中心の座標が変化した場合はリストに格納 if (LastScaleCenterX != scaleCenterX || LastScaleCenterY != scaleCenterY || LastBigsmall != bigsmall) { RotateScaleInfo info = new RotateScaleInfo(); info.ScaleInfo = new ScaleInfo(scaleCenterX, scaleCenterY, (LastBigsmall == 0) ? bigsmall : bigsmall / LastBigsmall); RotateScaleInfos.Insert(0, info); } // 最後に取得されたデータは保存しておく LastScaleCenterX = scaleCenterX; LastScaleCenterY = scaleCenterY; LastBigsmall = bigsmall; glControl.Refresh(); } } |
次に描画の処理を考えます。視点を変更するというコメントがありますが、実際にマウス操作で視点が変更されることはないので今回は呼び出す必要はありません。
DrawObjectメソッドで直方体を描画します。拡大したり回転したりというこれまでの操作情報が格納されているリストからデータを読み出して処理をおこないます。ただしいままさにマウスをドラッグして回転処理がおこなわれている場合は最初にこの処理を実行します(一番最後におこなわれた処理が最初になるため)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public partial class Form1 : Form { private void glControl_Paint(object sender, PaintEventArgs e) { GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); // 視点を変更する SetSight(); DrawObject(); glControl.SwapBuffers(); } private void DrawObject() { GL.PushMatrix(); { GL.Translate(X, Y, Z); // マウスドラッグによる回転処理がおこなわれていないときは // RotateXとRotateYは0なので何もおきない GL.Translate(AxisRotationX, AxisRotationY, 0); GL.Rotate(RotateY, 0, 1, 0); GL.Rotate(RotateX, 1, 0, 0); GL.Translate(-AxisRotationX, -AxisRotationY, 0); foreach (RotateScaleInfo rotateScaleInfo in RotateScaleInfos) { if (rotateScaleInfo.RotateInfo != null) { RotateInfo info = rotateScaleInfo.RotateInfo; GL.Translate(info.AxisRotationX, info.AxisRotationY, 0); GL.Rotate(info.RotateY, 0, 1, 0); GL.Rotate(info.RotateX, 1, 0, 0); GL.Translate(-info.AxisRotationX, -info.AxisRotationY, 0); } if (rotateScaleInfo.ScaleInfo != null) { ScaleInfo info = rotateScaleInfo.ScaleInfo; Scale(info.Scale, info.ScaleCenterX, info.ScaleCenterY); } } DrawLine1(); DrawBox(); DrawLine2(); } GL.PopMatrix(); } } |
DrawLine1メソッドとDrawLine2メソッドがありますが、これは矢印のついたXYZ座標軸と格子上の直線、直方体上に円を描画するためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
public partial class Form1 : Form { private void DrawLine1() { GL.Color3(Color.Gray); GL.Begin(BeginMode.Lines); { for (float i = -100; i <= 100; i = i + 10) { GL.Vertex3(-100, i, -0.01f); GL.Vertex3(100, i, -0.01f); } for (float i = -100; i <= 100; i = i + 10) { GL.Vertex3(i, 100, -0.01f); GL.Vertex3(i, -100, -0.01f); } } GL.End(); GL.Color3(Color.Red); GL.LineWidth(3); // XYZ軸はやや太い直線にして先端に矢印をつける GL.Begin(BeginMode.Lines); { GL.Color3(Color.Red); GL.Vertex3(0, 0, 0.01f); GL.Vertex3(30, 0, 0.01f); GL.Vertex3(30, 0, 0.01f); GL.Vertex3(27, 3, 0.01f); GL.Vertex3(30, 0, 0.01f); GL.Vertex3(27, -3, 0.01f); GL.Color3(Color.Blue); GL.Vertex3(0, 0, 0.01f); GL.Vertex3(0, 30, 0.01f); GL.Vertex3(0, 30, 0.01f); GL.Vertex3(3, 27, 0.01f); GL.Vertex3(0, 30, 0.01f); GL.Vertex3(-3, 27, 0.01f); GL.Color3(Color.Green); GL.Vertex3(0, 0, 0); GL.Vertex3(0, 0, 30); GL.Vertex3(3, 0, 27); GL.Vertex3(0, 0, 30); GL.Vertex3(0, 0, 30); GL.Vertex3(-3, 0, 27); } GL.End(); GL.LineWidth(1); } private void DrawLine2() { GL.Color3(Color.Pink); GL.Begin(BeginMode.Lines); { GL.Vertex3(-5, -5, 0); GL.Vertex3(-5, -5, 10); } GL.End(); Circle2D(2, -5, -5, 0); } } |
ミドルクリックをすると設定がクリアされ、最初に描画された状態に戻ります。そのための処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { void ViewSettingInit() { X = 0f; Y = 0f; Z = 0f; RotateX = 0f; RotateY = 0f; EyeZ = 1f; this.RotateScaleInfos.Clear(); LastBigsmall = 0; SetInitScale(); } } |
先日よりOpenTKの学習をしており、こちらのサイトのおかげで大変助かっております。
マウス操作での回転・移動などをやりたいと思い、こちらのページを参照させていただいたのですが上手く動かすことができません。
省略されている部分は”戦車”のソースから流用するものと思いますが、知識が足りなく補完できませんでした。
可能でしたらソースをいただけないでしょうか?
対応が遅れてすみませんでした。昨日は19時前に寝てしまい、今日は日曜日ということで油断していました。
こちらのミスとしてScale(float scale, float centerX, float centerY)を記事に書いていなかったこと、glControlがどこで生成されたのかを書いていなかったことがあります。
ソースはここへアップしておきました。
https://github.com/mi3w2a1/OpentkMouseTest
管理人様
返答遅くなりすみません、対応ありがとうございました。
ソースをダウンロードして確認をさせていただきます。
初めまして。最近OPENGLを利用して簡単なFEMモデル作成ツールを作ろうと思っているのですが、どちらのサイトを参考にしても描画までたどり着きません。スキルが低すぎて申し訳ないのですが、動作できているプロジェクトを一括で見せていただけませんでしょうか?
当方VisualStudio2022 win10/11で開発を行っています。メインの言語はnet.vbですが、OPENGLを操作するのにC#の方がよさげなので、こちらにシフトしようかとも考えています。
以上、よろしくお願いいたします。
管理人様
上にアップされていたソースを使ってC#での動作確認が出来、当方の考えていたvbとの連動?なんとか行きそうです。ありがとうございました。vbで直接OPENGLを操作できるようにしたかったのですが、次々と問題が発生するため今のところ諦めました。