OpenTKの手始めとして、前回触れたGameWindowとGLControlという二つのプログラミングスタイルをベースとしたスケルトンを作ってみましょう。(

:メインテーマであるBCCSkeltonがそうですが、私は標準の「ドンガラ(Skelton)」を作り、それに機能を実装するスタイルのプログラミングを好んでいるということでしょう。また、両者で共通するGLクラス関係のメソッドも共有化できます。

 

繰り返しになりますが、(ウィンドウ自体にOpenTKがビルトインされた)GameWindowと、(OpenTKをウィンドウコントロールとして利用する)GLControlのメリデメはこちらを参照して下さい。↓に私が使っているGameWindowとGLControlのスケルトンを載せておきます。二つとも同じカラフルな2D三角形を描画するだけの簡単なプログラムですが、相違点と共通点があります。(なお、GLControlの場合の定期的な描画更新はユーザーがTimer等を使って実施してください。また、GLControlへのキー入力はやや処理が面倒ですので省略しています。)

 

【相違点】

GameWindowはウィンドウそのものなので、そのまま、または派生させて生成し、後はウィンドメッセージウループで呼び出される(組み込みの)割込みメソッド("Onナンチャラ")をオーバーライドして使っています。特に注意すべきは、再描画するメソッドがOnRenderFrameで、その直前に呼ばれるOnUpdateFrameで再描画前の処理を行う、ということでしょうか?

一方、GLControlは他のウィンドウコントロールと同じく、メインウィンドウ(Form)のOnLoad処理(WM_CREATE)でメインウィンドウの子として生成します。そしてこのコントロールの生成時、サイズ変更時、再描画時の割込みメソッドに処理を書いてゆきます。なお、GameWindowはGLUTの"Idle処理"と同じように特段何をしなくとも再描画処理が呼ばれますが、GLControlは意図的にWM_PAINTメッセージを出さないと再描画しないようなので、グラフィック処理時にInvalidateやRefreshメソッドを使って再描画要求を出す必要があります。

 

【共通点】

先ず、両方とも生成時にOpenGLの初期処理をOnLoadで行うところでしょうか?↓では画面消去色の設定と深度チェックを有効にしています。

次にOpenGLのお作法通り、サイズ変更時(生成時を含む)にビューポートと投影処理の設定を行います。↓では共に透視投影で処理しています。

最後は描画処理ですが、先ず画面消去、モデルビューモードで視点・視野設定を行い、GL命令等で描画を行い、ダブルバッファーの交換を行います。

 

さて、では実際のコードをご覧ください。なお、次回からは実際に

 

GameWindow版スケルトンを使ってGLUTでやってきたプリミティブ等を移植

 

してみようと思います。

 

【GameWindow_Base.cpp】

/////////////////////////////////////////////
// GameWindow_Base.cpp
// OpenTKをGameWindowで使う場合のスケルトン
// Copyrignt (c) 2024 byYsama
/////////////////////////////////////////////

using System;
using OpenTK;                        //OpenTKを使用する際に必須
using OpenTK.Graphics;                //OpenTKのグラフィックを使用する際に必須
using OpenTK.Graphics.OpenGL;        //OpenGLを使用する際に必要
using OpenTK.Input;                    //OpenTKで入力を行う際に必要(例:Keyboiard)

namespace GameWindow_Base
{
    //アプリケーションクラス
    public class App
    {
        [STAThread]
        public static void Main()
        {
            gWindow gw= new gWindow();
            gw.Run();
        }
    }

    class gWindow : GameWindow    //GameWindowクラスからユーザーウィンドウを派生させる
    {
        public gWindow() : base(640, 480, GraphicsMode.Default, "OpenTK GameWindow")    //既定引数
        {
/*  解説:マウスやキーによる割り込みの場合、次のようにして対応するメソッドを実装する。

            //Mouse割込み
            this.MouseDown += gW_MouseDown;
            this.MouseMove += gW_MouseMove;
            this.MouseUp += gW_MouseUp;
            //Key割込み
            this.KeyUp += gW_KeyUp;
            this.KeyDown += gW_KeyDown;
            this.KeyPress += gW_KeyPress;
*/

        }

        //ウィンドウの起動時に実行される
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);                                //既定処理
            GL.ClearColor(Color4.Black);                //画面消去色の設定
            GL.Enable(EnableCap.DepthTest);                //深度チェックを有効化する
        }

        //ウィンドウのサイズが変更された場合に実行される
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);                            //既定処理
            //ビューポートの設定
            GL.Viewport(0, 0, ClientRectangle.Width, ClientRectangle.Height);
            //視野空間の設定
            SetProjection();
        }

        //ウィンドウイベントが更新された場合に実行される(OnRenderFrameの前に実行される)
        protected override void OnUpdateFrame(FrameEventArgs e)
        {
            base.OnUpdateFrame(e);
            //ESCキーが押されたら終了
            if(Keyboard.GetState().IsKeyDown(Key.Escape))
            {
                Exit();
            }
        }

        //ウィンドウ描画が更新された場合に実行される
        protected override void OnRenderFrame(FrameEventArgs e)
        {
            base.OnRenderFrame(e);                        //既定処理
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            SetModelView();                                //モデルビューモードで視点を設定して原点(0, 0, 0)を見る
            //以下に描画処理を行う
            GL.Begin(PrimitiveType.Quads);
                GL.Color4(Color4.White);                            //色名で持定
                GL.Vertex3(-1.0f, 1.0f, 0.0f);
                GL.Color4(new float[] { 1.0f, 0.0f, 0.0f, 1.0f });    //配列で特定
                GL.Vertex3(-1.0f, -1.0f, 0.0f);
                GL.Color4(0.0f, 1.0f, 0.0f, 1.0f);                    //4つの引数に単精度実数型で特定
                GL.Vertex3(1.0f, -1.0f, 0.0f);
                GL.Color4((byte)0, (byte)0, (byte)255, (byte)255);  //byte型で特定
                GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.End();
            //バッファー変換
            SwapBuffers();
        }

        private void SetProjection()
        {
            //視野空間の設定
            GL.MatrixMode(MatrixMode.Projection);
            //透視投影
            Matrix4 projection = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, (float)Width / (float)Height, 0.1f, 64.0f);    //glPerspective(fov, aspect, near, far);
            GL.LoadMatrix(ref projection);
        }

        void SetModelView(float x = 0.0f, float y = 0.0f, float z = 5.0f)
        {
            GL.MatrixMode(MatrixMode.Modelview);    //モデルビューを選択
            //視点を設定して原点(0, 0, 0)を見る
            Vector3 eye = new Vector3(x, y, z);
            Matrix4 modelview = Matrix4.LookAt(eye, Vector3.Zero, Vector3.UnitY);    //gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0);
            GL.LoadMatrix(ref modelview);            //現在の変換マトリックスに設定
        }

        void SetLight(float x = 50.0f, float y = 50.0f, float z = 10.0f)
        {
            //光源の使用
            GL.Enable(EnableCap.Lighting);
            //ライト0の設定
            GL.Enable(EnableCap.Light0);
            float[] pos = new float[] {x, y, z, 0.0f };
            GL.Light(LightName.Light0, LightParameter.Position, pos);
        }
    }
}
 

【GLControl_Base.cpp】

///////////////////////////////////////////
// GLControl_Base.cpp
// OpenTKをGLControlで使う場合のスケルトン
// Copyrignt (c) 2024 byYsama
///////////////////////////////////////////

using System;
using System.Drawing;
using System.Windows.Forms;
using OpenTK;                        //OpenTKを使用する際に必須
using OpenTK.Graphics;                //OpenTKのグラフィックを使用する際に必須
using OpenTK.Graphics.OpenGL;        //OpenGLを使用する際に必要
//using OpenTK.Input;                    //OpenTKで入力を行う際に必要(例:Keyboiard)-マウスを使う場合Formsのマウスイベントと干渉するので注意

namespace GLControl_Base
{
    //アプリケーションクラス
    public class App
    {
        [STAThread]
        public static void Main()
        {
            Application.Run(new MainForm());
        }
    }

    public class MainForm : Form
    {
        private GLControl glControl;    //GLControlへのポインター

        public MainForm()
        {
            this.Text = "OpenTK_GLControl_Base";
            this.ClientSize = new Size(640, 480);
            this.MinimumSize = new Size(480, 360);
            this.Load += new EventHandler(Form_Load);
        }

        private void Form_Load(object sender, EventArgs e)
        {
            //Formのコントロールレイアウトロジック保留
            SuspendLayout();
            //GLControl関連
            glControl = new GLControl();
            glControl.Size = new Size(this.ClientSize.Width, this.ClientSize.Height);
            glControl.Location = new Point(0, 0);
            glControl.Load += new EventHandler(glControl_Load);
            glControl.Resize += new EventHandler(glControl_Resize);
            glControl.Paint += new PaintEventHandler(glControl_Paint);
/*  解説:マウスによる割り込みの場合、次のようにして対応するメソッドを実装する。

            glControl.MouseDown += glc_MouseDown;
            glControl.MouseMove += glc_MouseMove;
            glControl.MouseUp += glc_MouseUp;
*/

            Controls.Add(glControl);
            //Formのコントロールレイアウトロジック再開
            ResumeLayout();
        }
        private void glControl_Load(object sender, EventArgs e)
        {
            GL.ClearColor(glControl.BackColor);    //画面消去色の設定
            GL.Enable(EnableCap.DepthTest);        //深度バッファの使用
        }

        private void glControl_Resize(object sender, EventArgs e)
        {
            glControl.Dock = DockStyle.Fill;    //All the control's edges are docked to the all edges of its containing control and sized appropriately.
            glControl_SetProjection();
            glControl_SetModelView();            //ModelViewの設定
            //glControl_SetLight();                //ライトの設定
            glControl.Refresh();                //再描画
        }

        private void glControl_Paint(object sender, PaintEventArgs e)
        {
            //色と深度バッファーを初期化
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
            //モデルビューにして視点を設定
            glControl_SetModelView();
            //以下に描画処理を行う
            GL.Begin(PrimitiveType.Quads);
                GL.Color4(Color4.White);                            //色名で持定
                GL.Vertex3(-1.0f, 1.0f, 0.0f);
                GL.Color4(new float[] { 1.0f, 0.0f, 0.0f, 1.0f });    //配列で特定
                GL.Vertex3(-1.0f, -1.0f, 0.0f);
                GL.Color4(0.0f, 1.0f, 0.0f, 1.0f);                    //4つの引数に単精度実数型で特定
                GL.Vertex3(1.0f, -1.0f, 0.0f);
                GL.Color4((byte)0, (byte)0, (byte)255, (byte)255);  //byte型で特定
                GL.Vertex3(1.0f, 1.0f, 0.0f);
            GL.End();
            //バッファー変換
            glControl.SwapBuffers();
        }

        private void glControl_SetProjection()
        {
            //ビューポートの設定
            GL.Viewport(0, 0, glControl.Size.Width, glControl.Size.Height);
            //視野空間の設定
            GL.MatrixMode(MatrixMode.Projection);
            //透視投影-尚、glControl.AspectRatioは、(float)glControl.Width / (float)glControl.Heightと等価
            Matrix4 projection = Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, glControl.AspectRatio, 0.1f, 64.0f);
            GL.LoadMatrix(ref projection);
        }

        void glControl_SetModelView(float x = 0.0f, float y = 0.0f, float z = 5.0f)
        {
            GL.MatrixMode(MatrixMode.Modelview);    //モデルビューを選択
            //視点を設定して原点(0, 0, 0)を見る
            Vector3 eye = new Vector3(x, y, z);
            Matrix4 modelview = Matrix4.LookAt(eye, Vector3.Zero, Vector3.UnitY);    //gluLookAt(0, 0, 5, 0, 0, 0, 0, 1, 0);
            GL.LoadMatrix(ref modelview);            //現在の変換マトリックスに設定
        }

        void glControl_SetLight(float x = 50.0f, float y = 50.0f, float z = 10.0f)
        {
            //光源の使用
            GL.Enable(EnableCap.Lighting);
            //ライト0の設定
            GL.Enable(EnableCap.Light0);
            float[] pos = new float[] {x, y, z, 0.0f };
            GL.Light(LightName.Light0, LightParameter.Position, pos);
        }
    }
}

 

【OpenGL(注)】シリーズは、絵心が無く、CGに疎かった私が無謀にもOpenGLに挑戦し、

 

(1)C#用のOpenTKの導入を行って大苦戦し、学習の必要性を感じたことから、

(2)学習の必要性を感じたことから、C(++)言語用のGLUT(OpenGLユーティリティツール)をウィンドウ環境で試し

(3)次にお作法にのっとった正しい使い方で基本学習を行い、

(4)「初級」の卒業試験としてBCCSkeltonを使ってデモソフト、Glut_BCCを開発した

 

所まで来ました。

:SGI(シリコングラフィックス)の開発した「まかない」ソフトであった2D、3Dのグラフィックライブラリー(API)が1992年にオープンで公開され、以降各種言語やプラットフォーム上で使用されている。

 

そしてこれからは前回予告したように、導入したままお蔵入りしていたOpenTKを使ってみようということで「OpenTK、再び」というシリーズを開始します。

 

とはいっても、

 

まだ具体的なネタが煮詰まっていないので、現在までの私の使った印象を(シリーズを先取りして)書いてみようかと思います。

 

1.GLUTでOpenGLをウィンドウベースのプログラムで使うのはやや難がある

これはすでに書きましたが、Win32SDKベースでGLUT(実際にはfreeglutですが...)を使うと、GLUTの美点の一つである18の基本プリミティブの描画で落ちてしまいます。私には原因は分かりませんでしたが、この組み込み関数は、GLUTのglutCreateWindow関数で作成するウィンドウとそれが使うコールバック関数を予定しているので、ユーザーの作成したウィンドウでは正しく動作しないと思われます。

 

従ってウィンドウベースでGLUTを使うには、

 

(1)(実際にGlut_BCCでやったように)描画出力をGLUTウィンドウにして、制御系をユーザーウィンドウで行うか、

(2)GLUTウィンドウを作成したのち、そのハンドルを取得し、Win32APIのSetWindowLong関数を使ってGLUTウィンドウをコントロール化する

 

ことが必要になってきます。(

:Glut_BCCで、CGLUTクラスのウィンドウ作成関数、"int CGLUT::Create(char* name = NULL)"で、

    //GLのデバイスコンテキストハンドル取得
    HDC glDc = wglGetCurrentDC();
    
//ウィンドウハンドル取得
    m_hWnd = WindowFromDC(glDc);

のようにウィンドウハンドルを取得しています。従って、いつでも「コントロール化は可能」と言えます。

 

2.GLUTはオールドファッションな「C言語による手続き型プログラミング」の趣が強い

【OpenGL】3DグラフィックとGLUT」シリーズの「お作法にのっとった正しい使い方」を見ていただくと分かりますが、最終版のリリースが1998年であるGLUTなので、エントリーポイントは「main関数」ですし、C++の特徴であるクラス等は全く使われず、関数を呼び出すだけの使い方となっています。(とはいえ、そういうプログラミングスタイルと割り切ると、逆に結構使い勝手が良いとも言えますが。)

 

3.GLUTの最大の美点である基本18図形のビルトイン

そのような難点を補って余りあるのがGLUTにビルトインされた18の基本図形関数です。特にSolidとWireのTeapotは、その為にGlut_BCCを開発したいと思ったほどの優れものです。(Glut_BCCで、こいつをクルクル回しているだけで幸せになれるようです。俺って、変態?)

 

一方、OpenTKについては、

 

1.絶対的に不足するリファレンス

(これもすでに書きましたし、その為に学習にはGLUTを使ったのですが)GLUTに関しては、大学の先生等が色々と書いておられるのでweb上に豊富な情報資料があり、学習するのが楽でしたが、OpenTKの場合、ご本体以外に統一的な情報サイトが乏しく、ご本体もプログラミングの為の簡単な参照サイトがAPIだけなので、はっきり言って情報量が少ないです。メソッドやプロパティについてwebで検索するとMicrosoft LearnがOpenTKについても書いてくれていますが、これも引数や戻り値、オブジェクトの型等細かいことは書かれていません。従って私の場合、GLUTの知識経験から『GLUTのこれは、OpenTKではどう書くのかな?』的な発想で、アタリを立ててググっていました。

 

2.GLUTをやってきた人間にはやや取っ付きにくい型やメソッド

GLUTの難点はOpenTKで結構克服されており、オブジェクト指向のライブラリーになっているのですが、逆にGLUTで「変数と関数」だったものが、OpenTKでは「クラスオブジェクト、プロパティとメソッド(それも多くはStatic)」になっており、

 

似て非なる

 

プログラミングスタイルとコードに慣れる必要がありますが、如何せん、上記1(↑)がそれ阻むので、結構苦労しました。

 

3.OpenTKの二つのウィンドウスタイル(GLControlとGameWindow)

始めてC#で書かれたOpenTKのコードを見た時に「あれ?あれれっ?」と思ったのは、ウィンドウの初期化で全く異なるコードが書かれていることでした。そして色々と調べているうちに、OpenTKをコントロールとして使用するGLControlとOpenTKをビルトインしたウィンドウであるGameWindowからユーザーウィンドウを派生させる二つの使い方があることが判りました。

 

しかし、

 

これら二つの違い、メリデメについて書かれているものは見当たらず、とうとうChatGPT様にお伺いを立てたことも既に書きました

 

4.GLUTにあった18の基本図形は自分で書かなければならない

これが結構重いのですが(今も苦闘しています)、GLUTにあった

 

"ワイアーティーポット",    "ソリッドティーポット",
"ワイアー直方体",        "ソリッド直方体",
"ワイアー球体",            "ソリッド球体",
"ワイアー円錐",         "ソリッド円錐",
"ワイアー円環",            "ソリッド円環",
"ワイアー四面体",         "ソリッド四面体",
"ワイアー八面体",         "ソリッド八面体",
"ワイアー十二面体",     "ソリッド十二面体",
"ワイアー二十面体",     "ソリッド二十面体"

 

が全くビルトインされておらず、「自分でバンバン()」しなければならない、ということです。

:エースコックの石立鉄男と嵐山光三郎 によるカップ焼きそばのテレビCMで「自分でバンバンしなさい」というコピーが昔はやりました。

 

従って、

 

「しなければならない」ことは「しましょう」ということで、これを次のネタにしましょうか?

前回前々回前々々回と、やたらC++のコードばかりで退屈してしまったかと思いますので、今回はGlut_BCCの本来の開発趣旨(注)に沿って、BatchGoodでコンパイルしたGlut_BCCを実際に動かしてみましょう。

柱:私はなんでも実験、実証したがる人間なので、OpenGLの座学では飽き足らず「実際にどう見えるのか」を併行して行わないと気が済まない、というのがこのプログラムの開発趣旨でした。

 

まず起動時はこんな感じです。

 

この段階ではGLUTウィンドウはまだ作られていないので、ウィンドウコントロールは皆「無効」になっています。

 

最初に選択されているドロップダウンリストでワイアー、ソリッド各9つづつの基本図形(プリミティブ)を選択します。(既定値は有名な「ワイアーティーポット」です。)また、表示色も「色」ボタンを押して、ダイアログで選択できます。(既定値は赤色です。)選択が済んだならば「GL Window作成」ボタンを押してください。以下のような"GL Window"が現れます。

 

最初は「常時回転」にチェックが入っており(既定値)、Y軸を中心にクルクル回っています。この段階でプリミティブや色を変更してみてください。

緑色の「ドーナッツ」(円環)を選択してみましたが、矢張りティーポットに戻します。

 

先ず実験するのは「視点」です。投影面の座標はいじらないで(注)視点(eyex、eyey、eyez-初期値は0.0, 0.0, 2.0)の値を変えてみてください。因みに"2.0, 2.0, 3.0"を設定すると、こんな感じになります。(斜め右上から見下ろす感じですね。)

柱:プリミティブの中心を原点(0.0, 0.0, 0.0)に置いているので。

 

この状態で「投影」の投影方法を既定値の「透視投影(glPerspective)」から「平行投影(glOrtho)」(「並行」と誤字になっていますが...汗)に変えてみるとXYZ軸共に-1.0~1.0までの立方体を単純に投射した映像に変わります。

 

これは余り面白くないので、今度は同じ投影方法でも別の関数を使う「透視投影(glFrustum)」にし、よく見えるように視点も

"1.0, 1.0, 2.0"にしてみます。

今度は「常時回転」のチェックボタンをはずし、回転軸を変えてスライダー(トラックバー コントロール)を動かしてみてください。異なる軸で一回転するのが確認できます。(静止画では分からないので画像は省略します。)

 

再度「常時回転」にチェックを入れて、今度は「照光効果」(ライティングイフェクト)にもチェックを入れます。

今度はワイアーティーポットが金色に変わりましたね?また、よく見るとライトの当たり具合で陰影が出ていることが判ります。よりよく見るためにプリミティブをソリッドティーポットにしてみます。

 

先ずは「環境光」で。

次は「拡散光」。

最後が「鏡面光」。

 

段々と暗くなるイメージですが、実際のCGでは光もこれら3つのの光をブレンドし、更にプリミティブの表面の反射具合も「質感」をglMaterial関数を使って変化させデザインするようです。(絵心のない私には神業の様に見えますが...)

 

ということで、最初は「光と質感のブレンドも」と考えていたプログラムですが、「無理っ!」ということで(webから拝借してきた)「金色一本」とし、素人でも分かり易い「ライトの位置」の設定だけにしてみました。↓は「環境光」で既定のライト位置("50.0, 50.0, 50.0")から"-50.0, 10.0, 10.0"にしてみたものです。光の反射、陰影を楽しめます。

 

いかがだったでしょうか?

 

GLUTを使ってウィンドウベースでOpenGLの入り口に立って、「『3DCGとはどんなものかな?』と正しい疑問を持つことが出来るデモンストレーションソフトとしてはまずまずの出来ではないか?」と思ったのですが。

 

さて、

 

長々と続いてきたC++、GLUTを使ったOpenGLの学習編はここでひとまず「初級」終了ということにして、導入したまま手つかずになっていたC#、OpenTKによるOpenGLに舞い戻り、20世紀のソフトと21世紀のソフトの差を味わってみようかと思います。

 

乞御期待!

 

現在GLUTのプログラムをOpenTKように移植していて、私の勘違いから誤りを犯していたことが分かりました。

以下に該当記事とその内容と原因、修正内容を記します。ご迷惑をおかけし、申し訳ありませんでした。

 

1.該当記事

(1)【OpenGL】GLSamplerについて(2)

(2)【OpenGL】3DグラフィックとGLUT(5)

(3)【OpenGL】3DグラフィックとGLUT(6)

(4)【OpenGL】3DグラフィックとGLUT(7完)

 

2.その内容と原因

 

正三角錐の中心(重心点)に原点をおいて座標を設定しましたが、正三角錐(3D)の高さを正三角形(2D)の高さと誤認したため、Y軸座標が正しくなかったことが判りました。勘違いは、一遍の長さが1.0の正三角錐の高さを単純に"1.0"と誤信したことが原因です。

(誤)0.577f → (正)0.544f

(誤)0.289f → (正)0.272f

 

3.修正内容

以下に修正内容を載せます。

正三角形(平面-本件では底面)の高さは「二分のルート3」(sqrt(3.0) / 2 ≒ 0.866)で、中心(重心点)のZ軸座標は 1 : 2(≒0.289 : ≒0.577)となりますが、更に高さは底面の重心点Z座標「二分のルート3の2/3(≒0.577)」と「正三角錐の一辺(1.000)」とY軸座標の高さ(仮にHとします)の直角三角形を形成していることから、「Hの二乗 =1の二乗引く0.577の二乗」の平方根(長さなので正の数です)となり、「三分の2の平方根(sqrt(2.0 / 3.0))」(≒0.8165)となり、更にこの高さ(Y軸座標)の重心点は1:2で分かれるので、高さは(≒0.8167):(≒0.272 : ≒0.544)になります。

従って、上記記事の(1)~(3)については、

 

    //ポリゴン描画
    glPushMatrix();
        glRotated(theta, 0.0, 1.0, 0.0);
        theta += 0.05;
        glBegin(GL_TRIANGLES);
            //奥の面
            glColor3f(1.0f, 0.0f, 0.0f);
            glVertex3f(-0.500f, -0.272f, -0.289f);    glVertex3f(0.500f, -0.272f, -0.289f);    glVertex3f(0.000f, 0.544f, 0.000f);
            //右手前面
            glColor3f(0.0f, 1.0f, 0.0f);
            glVertex3f(0.500f, -0.272f, -0.289f);    glVertex3f(0.000f, -0.272f, 0.577f);    glVertex3f(0.000f, 0.544f, 0.000f);
            //左手前面
            glColor3f(0.0f, 0.0f, 1.0f);
            glVertex3f(0.000f, -0.272f, 0.577f);    glVertex3f(-0.500f, -0.272f, -0.289f);    glVertex3f(0.000f, 0.544f, 0.000f);
            //底面
            glColor3f(1.0f, 1.0f, 1.0f);
            glVertex3f(0.500f, -0.272f, -0.289f);    glVertex3f(0.500f, -0.272f, -0.289f);    glVertex3f(0.000f, -0.272f, 0.577f);
        glEnd();
    glPopMatrix();
 

となり、記事(4)については、

 

GLdouble g_Tvertex[4][3] = {//三角形の4頂点(上から下反時計回り4点
    {0.000f, 0.544f, 0.000f},    //A
    {0.500f, -0.272f, -0.289f},    //B
    {-0.500f, -0.272f, -0.289f},//C
    {0.000f, -0.272f, 0.577f},    //D
};
 

となります。なお、該当記事は本日日付で修正を行っております。再度子迷惑をおかけし、申し訳ございませんでした。

 

いよいよ、寄る年波を感じます。しかし、「ボケ防止」で始めたこのブログ、【OpenGL】シリーズは最後までやり通したいと思います。

 

前々回GLUTをクラス化したCGLUT.h、前回はGlut_BCCの宣言部分をカバーしましたので、今回は肝心なGlut_BCCProc.hとGlut_CBFuntion.hをザーッと解説します。(深く読むと疲れると思うので、青字当たりをザーと読み流すことをお勧めします。)

 

【Glut_BCCProc.h】

//////////////////////////////////////////
// Glut_BCCProc.h
// Copyright (c) 10/26/2021 by BCCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////

bool CMyWnd::OnInit(WPARAM wParam, LPARAM lParam) {

    //IDC_SELOBJの初期化(解説:18の基本図形選択用ドロップダウンリストです。)
    for(int i = 0; i < 18; i++)
        SendItemMsg(IDC_SELOBJ, CB_ADDSTRING, 0, (LPARAM)g_ObjName[i]);
    SendItemMsg(IDC_SELOBJ, CB_SETCURSEL, 0, (LPARAM)0);
    //IDC_SELPROJの初期化(解説:投影方法選択用ドロップダウンリストです。)
    for(int i = 0; i < 3; i++)
        SendItemMsg(IDC_SELPROJ, CB_ADDSTRING, 0, (LPARAM)g_ProjName[i]);
    SendItemMsg(IDC_SELPROJ, CB_SETCURSEL, 0, (LPARAM)0);
    //IDC_SELLIGHTの初期化(解説:照光方法選択用ドロップダウンリストです。)
    for(int i = 0; i < 3; i++)
        SendItemMsg(IDC_SELLIGHT, CB_ADDSTRING, 0, (LPARAM)g_LightName[i]);
    SendItemMsg(IDC_SELLIGHT, CB_SETCURSEL, 0, (LPARAM)0);
    //チェックボックスの初期設定(解説:「常時回転」にチェックを入れます。)
    SendItemMsg(IDC_ROTATE, BM_SETCHECK, BST_CHECKED, (LPARAM)0);
    //スライダーの範囲設定(解説:「回転角」スライダー関連です。)
    SendItemMsg(IDC_ANGLE, TBM_SETRANGE, TRUE, (LPARAM)MAKELONG(0, 360));
    //ページサイズ(マウスクリック時の移動量)設定(解説:同上)
    SendItemMsg(IDC_ANGLE, TBM_SETPAGESIZE, 0, (LPARAM)30);
    //ラインサイズ(矢印キーによる移動量)設定(解説:同上)
    SendItemMsg(IDC_ANGLE, TBM_SETLINESIZE, 0, (LPARAM)10);
    //ポジションの設定(解説:同上)
    SendItemMsg(IDC_ANGLE, TBM_SETPOS, TRUE, (LPARAM)0);
    //目盛りサイズの設定(解説:同上)
    SendItemMsg(IDC_ANGLE, TBM_SETTICFREQ, 10, 0);
    return TRUE;
}

bool CMyWnd::OnHScroll(WPARAM wParam, LPARAM lParam) {    //解説:スライダーメッセージを取得します。

    if(!g_rotate && (HWND)lParam == GetDlgItem(m_hWnd, IDC_ANGLE)) {    //常時回転の場合、何もしない
        if(LOWORD(wParam) == TB_THUMBPOSITION || LOWORD(wParam) == TB_THUMBTRACK)
            g_r = (GLdouble)HIWORD(wParam);
        else
            g_r = (GLdouble)SendItemMsg(IDC_ANGLE, TBM_GETPOS, 0, (LPARAM)0);

            //解説:スライダーからのメッセージでGlut_BCC.hにある外部変数(g_r)に回転角をセットします。
    }
    return FALSE;    //アプリケーションでこのメッセージを処理する場合は、0 を返す必要があります。(MSDN)
}

bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {

    if(MessageBox(m_hWnd, "終了しますか", "終了確認",
                    MB_YESNO | MB_ICONINFORMATION) == IDYES)
        //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    else
        //そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。
        return FALSE;
}

//ダイアログベースの場合はこれが必要
bool CMyWnd::OnDestroy(WPARAM wPram, LPARAM lParam) {

    PostQuitMessage(0);
    return TRUE;
}

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////

bool CMyWnd::OnSelected(WPARAM wParam) {

    if(HIWORD(wParam) == CBN_SELCHANGE) {    //アイテムが選択された場合
        //IDC_SELOBJの選択内容を確認し、glにセットする
        int sel = SendItemMsg(IDC_SELOBJ, CB_GETCURSEL, 0, (LPARAM)0);
        gl.m_Obj = sel / 2;
        gl.m_sw = (sel % 2 == 1);    //Solid(TRUE)か否か(WireはFALSE)
        return TRUE;
        //解説:18の基本図形はソリッドとワイアー図形の9つのセットになっており、先ず0-8で図形を判断し、奇数か偶数かでソリッドとワイアーの区別をつけています。

    }
    else
        return FALSE;
}

bool CMyWnd::OnSelCol() {    //解説:カラー選択ダイアログを呼び出し、RGB要素を外部変数で記録します。

    CMNDLG cmndlg;
    COLORREF col = cmndlg.GetColor(m_hWnd);
    if(col) {
        g_Red = (GLdouble)GetRValue(col) / 255.0;
        g_Green = (GLdouble)GetGValue(col) / 255.0;
        g_Blue = (GLdouble)GetBValue(col) / 255.0;
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnCreate() {

    //テキストボックスの状態を変更する
    for(int i = 0; i < 9; i++)
        EnableWindow(GetDlgItem(m_hWnd, IDC_EYEX + i), TRUE);
    for(int i = 0; i < 4; i++)
        EnableWindow(GetDlgItem(m_hWnd, IDC_FOV + i), TRUE);
    //コンボボックスの状態を変更する
    EnableWindow(GetDlgItem(m_hWnd, IDC_SELPROJ), TRUE);
    //ラジオボタンの状態を変更する
    EnableWindow(GetDlgItem(m_hWnd, IDC_ROTATE), TRUE);
    EnableWindow(GetDlgItem(m_hWnd, IDC_LIGHT), TRUE);
    //プッシュボタンの状態を変更する
    EnableWindow(GetDlgItem(m_hWnd, IDC_CREATE), FALSE);
    EnableWindow(GetDlgItem(m_hWnd, IDC_SETLA), TRUE);
    EnableWindow(GetDlgItem(m_hWnd, IDC_SETPROJ), TRUE);

    //解説:↑はGLUTウィンドウが作られる際に初期状態で無効なコントロールを有効にします。
    //解説:↓はGLUTでmain()関数で行うGLUTの初期化を行い、GLUTウィンドウを生成します。

    //CGLUTクラスインスタンスの初期設定
    gl.SetPos(100, 100);                    //CGLUTの初期値
    gl.SetSize(640, 480);                    //CGLUTの初期値
    gl.SetTitle("Glut Window");                //GLUTウィンドウタイトル
    //CGLUTクラスインスタンスの生成
    gl.Create();                            //引数でウィンドウタイトル設定可
     glClearColor(0.0, 0.0, 0.0, 0.0);        //黒で画面クリア
    glShadeModel(GL_SMOOTH);                //既定値の滑らかな網かけ(他の選択肢としてフラットシェーディング-GL_FLATがある)
    glEnable(GL_DEPTH_TEST);                //描画オブジェクトの陰面消去を行う為の深度処理
    gl.SetMode(GLUT_DOUBLE | GLUT_RGBA);    //CGLUTの初期値(ダブルバッファリング vs. GLUT_SINGLE)
      //CGLUTコールバック関数の設定
    gl.Idle(gl_Idle);
    gl.Display(gl_Display);
    gl.Reshape(gl_Resize);
    gl.Loop();
    return TRUE;
}

bool CMyWnd::OnSetLA() {    //解説:gluLookAt関数(gl.SetLA)を使って視点の設定を行います。

    GLdouble val[9];
    for(int i = 0; i < 9; i++)
        val[i] = GetDouble(IDC_EYEX + i);
    gl.SetLA(    val[0], val[1], val[2],        //視点のポイント            (GLdouble) eyex, eyey, eyez,
                val[3], val[4], val[5],        //クリップシーンの中心点    (GLdouble) centerx, centery, centerz,
                val[6], val[7], val[8]);    //アップベクター            (GLdouble) upx, upy, upz
    gl.Refresh();    //解説:再描画要求をします。
    return TRUE;
}

bool CMyWnd::OnSelProj(WPARAM wParam) {    //解説:投影方法の選択時にその投影方法のパラメーターを入力するコントロールを有効化させ、他を無効化させます。

    if(HIWORD(wParam) == CBN_SELCHANGE) {    //アイテムが選択された場合
        //IDC_SELPROJの選択内容を確認し、glにセットする
        gl.m_proj = SendItemMsg(IDC_SELPROJ, CB_GETCURSEL, 0, (LPARAM)0);    //投影法式変更
        SetControlState(gl.m_proj);            //投影法式に合わせたコントロールを有効化
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnSetProj() {    //解説:「投影設定」ボタンが押されると、必要なデータをコントロールから読み取り、選択された投影方法に応じてCGLUTクラスインスタンス(gl)の投影メソッドを再描画要求を出して実行します。

    switch(gl.m_proj) {
    default:
    case 0:
        //gluPerspective関数
        gl.m_fov = GetDouble(IDC_FOV);            //視野(Fiield of view)で単位は度
        gl.m_aspect = GetDouble(IDC_ASPECT);    //縦横比(幅 / 高さ)
        gl.m_near = GetDouble(IDC_NEAR);        //視点から近クリップ平面までの距離(正)
        gl.m_far = GetDouble(IDC_FAR);            //視点から遠クリップ平面までの距離(正)
        break;
    case 1:
        //glFrustum関数
        gl.m_left = GetDouble(IDC_LEFT);        //クリップ平面左座標
        gl.m_right = GetDouble(IDC_RIGHT);        //クリップ平面右座標
        gl.m_bottom = GetDouble(IDC_BOTTOM);    //クリップ平面下座標
        gl.m_top = GetDouble(IDC_TOP);            //クリップ平面上座標
        gl.m_near = GetDouble(IDC_NEAR);        //視点から近クリップ平面までの距離(正)
        gl.m_far = GetDouble(IDC_FAR);            //視点から遠クリップ平面までの距離(正)
        break;
    case 2:
        //glOrtho関数
        gl.m_left = GetDouble(IDC_LEFT);        //クリップ平面左座標
        gl.m_right = GetDouble(IDC_RIGHT);        //クリップ平面右座標
        gl.m_bottom = GetDouble(IDC_BOTTOM);    //クリップ平面下座標
        gl.m_top = GetDouble(IDC_TOP);            //クリップ平面上座標
        gl.m_front = GetDouble(IDC_BACK);        //手前クリップ平面座標
        gl.m_back = GetDouble(IDC_FRONT);        //奥クリップ平面座標
        break;
    }
    gl.Refresh();    //解説:上ではデータのセットだけ行い、投影方法の設定はこのメソッドで行う。
    return TRUE;
}

bool CMyWnd::OnRotate() {

    if(SendItemMsg(IDC_ROTATE, BM_GETCHECK, 0, (LPARAM)0) == BST_CHECKED) {    //チェックボックスの状態確認 (解説:「常時回転」のチェックボックスが付いていれば外し、回転フラグ(g_rotate)も偽にし、データ入力コントロールを有効化します。)
        SendItemMsg(IDC_ROTATE, BM_SETCHECK, BST_UNCHECKED, (LPARAM)0);
        g_rotate = FALSE;
        //テキストボックスを有効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_ENDX), TRUE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_ENDY), TRUE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_ENDZ), TRUE);
        //スライダーを有効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_ANGLE), TRUE);
    }
    else {    //解説:逆に「常時回転」のチェックが外れていれば、チェックをつけ、フラグを立ててデータ入力を不可にします。
        SendItemMsg(IDC_ROTATE, BM_SETCHECK, BST_CHECKED, (LPARAM)0);
        g_rotate = TRUE;
        //テキストボックスを無効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_ENDX), FALSE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_ENDY), FALSE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_ENDZ), FALSE);
        //スライダーを無効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_ANGLE), FALSE);
    }
    return TRUE;
}

bool CMyWnd::OnEndXChanged(WPARAM wParam) {    //解説:テキストボックスからデータを取得して、倍精度実数に変えて外部変数にセットします。

    if(!g_rotate && HIWORD(wParam) == EN_CHANGE) {    //常時回転以外で、データが変更された場合
        g_x = GetDouble(IDC_ENDX);
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnEndYChanged(WPARAM wParam) {    //解説:同上

    if(!g_rotate && HIWORD(wParam) == EN_CHANGE) {    //常時回転以外で、データが変更された場合
        g_y = GetDouble(IDC_ENDY);
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnEndZChanged(WPARAM wParam) {    //解説:同上

    if(!g_rotate && HIWORD(wParam) == EN_CHANGE) {    //常時回転以外で、データが変更された場合
        g_z = GetDouble(IDC_ENDZ);
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnLight() {    //解説:「照光効果」チェックボックスを確認し、入力を可、不可に制御します。

    if(SendItemMsg(IDC_LIGHT, BM_GETCHECK, 0, (LPARAM)0) == BST_UNCHECKED) {    //チェックボックスの状態確認
        SendItemMsg(IDC_LIGHT, BM_SETCHECK, BST_CHECKED, (LPARAM)0);
        g_light = TRUE;    //解説:「照光効果」フラグを真にします。
        //「色」ボタンを無効化する(ダイアログの上にある色選択ボタンです。)
        EnableWindow(GetDlgItem(m_hWnd, IDC_COLOR), FALSE);
        //テキストボックスを有効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_LIGHTX), TRUE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_LIGHTY), TRUE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_LIGHTZ), TRUE);
        //コンボボックスを有効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_SELLIGHT), TRUE);
        //光源を有効にする
        gl.Enable(GL_LIGHTING);
        //選択された光源(光源は8個まで設定可能)を有効にする
        gl.Enable(g_lightno[g_selected]);
        //物体の質感を有効にする設定を解除する
        gl.Disable(GL_COLOR_MATERIAL);
    }
    else {    //解説:既に有効であった場合には無効に変えます。
        SendItemMsg(IDC_LIGHT, BM_SETCHECK, BST_UNCHECKED, (LPARAM)0);
        g_light = FALSE;
        //「色」ボタンを有効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_COLOR), TRUE);
        //テキストボックスを無効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_LIGHTX), FALSE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_LIGHTY), FALSE);
        EnableWindow(GetDlgItem(m_hWnd, IDC_LIGHTZ), FALSE);
        //コンボボックスを無効化する
        EnableWindow(GetDlgItem(m_hWnd, IDC_SELLIGHT), FALSE);
        //光源を無効にする
        gl.Disable(GL_LIGHTING);
        //現在選択されている光源を無効にする
        gl.Disable(g_lightno[g_selected]);
        //物体の質感を有効にする設定
        gl.Enable(GL_COLOR_MATERIAL);
    }
    return TRUE;
}

bool CMyWnd::OnLightXChanged(WPARAM wParam) {    //解説:テキストボックスからデータを取得して、倍精度実数に変えて外部変数にセットします。

    if(g_light && HIWORD(wParam) == EN_CHANGE) {    //照光効果が有効で、データが変更された場合
        g_lightpos[0] = GetDouble(IDC_LIGHTX);
        gl_Display();    //解説:描画関数を呼び出します。
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnLightYChanged(WPARAM wParam) {    //解説:同上

    if(g_light && HIWORD(wParam) == EN_CHANGE) {    //照光効果が有効で、データが変更された場合
        g_lightpos[1] = GetDouble(IDC_LIGHTY);
        gl_Display();
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnLightZChanged(WPARAM wParam) {    //解説:同上

    if(g_light && HIWORD(wParam) == EN_CHANGE) {    //照光効果が有効で、データが変更された場合
        g_lightpos[2] = GetDouble(IDC_LIGHTZ);
        gl_Display();
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnSelLight(WPARAM wParam) {    //解説:照光方法選択ドロップダウンリストの選択処理です。

    if(HIWORD(wParam) == CBN_SELCHANGE) {    //アイテムが選択された場合
        //現在選択されている光源を無効にする
        gl.Disable(g_lightno[g_selected]);
        //IDC_SELLIGHTの選択内容を確認し、glにセットする
        g_selected = SendItemMsg(IDC_SELLIGHT, CB_GETCURSEL, 0, (LPARAM)0);    //投影法式変更
        //選択された光源を有効にする
        gl.Enable(g_lightno[g_selected]);
        gl_Display();
        return TRUE;
    }
    else
        return FALSE;
}

bool CMyWnd::OnIdok() {

    SendMsg(WM_CLOSE, 0, 0);
    return TRUE;
}

///////////////////
//ユーザー定義関数
///////////////////
//テキストボックスからGLDouble値を取得する

GLdouble CMyWnd::GetDouble(UINT id) {    //解説:テキストボックスのコントロールIDで値を返すように汎用化しました。

    char buff[MAX_PATH];
    SendItemMsg(id, WM_GETTEXT, MAX_PATH, (LPARAM)buff);
    double res = strtod(buff, NULL);    //変換に失敗した場合は0.0を返す筈
    return (GLdouble)res;
}

void CMyWnd::SetControlState(int pattern = 0) {    //解説:3つのパターンで10個のコントロールの有効、無効を変更します。このような「小技」がプログラミングの妙味ですね。

    bool tof[10] = {FALSE};
    switch(pattern) {
    case 0:
        tof[0] = tof[1] = tof[2] = tof[3] = TRUE;
        break;
    case 1:
        tof[2] = tof[3] = tof[4] = tof[5] = tof[6] = tof[7] = TRUE;
        break;
    case 2:
        tof[4] = tof[5] = tof[6] = tof[7] = tof[8] = tof[9] = TRUE;
        break;
    default:
        break;
    }
    EnableWindow(GetDlgItem(m_hWnd, IDC_FOV), tof[0]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_ASPECT), tof[1]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_NEAR), tof[2]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_FAR), tof[3]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_LEFT), tof[4]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_RIGHT), tof[5]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_BOTTOM), tof[6]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_TOP), tof[7]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_BACK), tof[8]);
    EnableWindow(GetDlgItem(m_hWnd, IDC_FRONT), tof[9]);
}

 

【Glut_CBFuntion.h】

/////////////////////
//GLUT Callback 関数
/////////////////////
////////////
//gl.Idle()
////////////

void gl_Idle() {    //解説:glutIdleFuncで設定された、GLUTのお作法にのっとったメッセージ処理がない場合のコールバック関数です。

    gl.ReDisp();        //再描画命令(解説:glPostDisplay関数を呼びます。)
}

///////////////
//gl.Display()
///////////////

void gl_Display() {    //解説:glutDisplayFuncで設定された、GLUTのお作法にのっとったモデルビューモードでのプリミティブ描画関数です。

    const static GLfloat material_ambient[4] = {0.24725, 0.1995, 0.0745, 1.0};        //質感設定(初期値-0.2 , 0.2 , 0.2 , 1)
    const static GLfloat material_diffuse[4] = {0.75164, 0.60648, 0.22648, 1.0};    //質感設定(初期値-0.8 , 0.8 , 0.8 , 1)
    const static GLfloat material_specular[4] = {0.24725, 0.1995, 0.0745, 1.0};        //質感設定(初期値-0 , 0 , 0, 1)
    const static GLfloat material_shininess = 48;
    //解説:上記は私のオリジナルではなく、WEBで見つけた「金色」表現のデータです。


    gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);    //色、深度データを初期化し、ウィンドウを消去する
    glMatrixMode(GL_MODELVIEW);        //描画コマンド設定(https://learn.microsoft.com/ja-jp/windows/win32/opengl/glmatrixmode)
    glLoadIdentity();                //モデルビューの変換行列の初期化

    if(g_light) {    //解説:「照光」効果が有効な場合の処理です。ライト0~2に異なる照光方法をセットしています。
        glLightfv(g_lightno[g_selected], GL_POSITION, g_lightpos);                        //光源の位置を設定
        glLightfv(g_lightno[g_selected], g_kind[g_selected], g_lightcolor[g_selected]);    //RGBA強度設定
    }
    else {    //解説:照光効果がある場合の処理はglColor関数を実行しても無視されるそうです。
        gl.Color(g_Red, g_Green, g_Blue, g_Alpha);    //色の設定(R, G, B, A = 1.0)
    }

    glPushMatrix();
        gl.Rotate(g_r, g_x, g_y, g_z);
        if(g_light) {    //解説:照光効果がある場合、表面の質感を設定しています。
            glMaterialfv(GL_FRONT, GL_AMBIENT, material_ambient);
            glMaterialfv(GL_FRONT, GL_SPECULAR, material_specular);
            glMaterialfv(GL_FRONT, GL_DIFFUSE, material_diffuse);
            glMaterialf(GL_FRONT, GL_SHININESS, material_shininess);
        }
        gl.DrawObject();            //3Dオブジェクトの描画(解説:18の基本図形から選択されたプリミティブを描画します。)
    glPopMatrix();
    glFlush();                        //バッファー コマンド実行と開放
    glutSwapBuffers();                //バッファを交換
    if(g_rotate) {                    //常時回転フラグ(解説:「常時回転」フラグでコントロールしています。)
        g_r += 0.1;
        if(g_r >= 360.0)
            g_r = 0.0;
    }
}

///////////////
//gl.Resize()
///////////////

void gl_Resize(int w, int h) {    //解説:glutReshapeFuncで設定されたウィンドウのサイズ変更時のコールバック関数です。

    gl.Resize(w, h);    //解説:定番のビューポートの設定や投影設定を行います。
}

 

以上でGlut_BCCのソースファイルの解説は終了します。(実に久々のBCCSkeltonプログラムでした。)次回はこのGlut_BCCシリーズの最終回として「Glut_BCCで遊んでみた!」をやってみようかと思います。

 

前回は、GLUTをクラス化して使用するCGLUT.hをやりました。今回からBCCSkeltonによるプログラミング部分になります。

 

いつものことですが(と言いつつ、最近はとんとBCCSkeltonで書いていなかったのでお分かりではないかもしれませんが)、説明が簡単な順に、

 

(1)Glut_BCC.cpp-メインのエントリーポイントがあるファイル。

(2)Glut_BCC.h-ウィンドウやダイアログのクラス定義やメンバー関数の宣言があるファイル。

をやって、最後に

(3)Glut_BCCProc.h-メンバー関数の実装を行うファイル。

(4)Glut_CBFunc.h-GLUT用のコールバック関数を記述しているファイル。

をやろうかと思います。

 

【Glut_BCC.cpp】

//////////////////////////////////////////
// Glut_BCC.cpp
//Copyright (c) 10/25/2021 by BCCSkelton
//////////////////////////////////////////

#include    "Glut_BCC.h"
#include    "Glut_CBFunc.h"    //GLUT外部コールバック関数ヘッダー
#include    "Glut_BCCProc.h"

////////////////
// WinMain関数
////////////////
i
nt WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow) {

    //2重起動防止
    if(!Glut_BCC.IsOnlyOne()) {
        //ここに2重起動時の処理を書く
        HWND hWnd = FindWindow("MainWnd", "Glut_BCC");
        if(IsIconic(hWnd))
            ShowWindow(hWnd, SW_RESTORE);
        SetForegroundWindow(hWnd);
        return 0L;
    }

    //モードレスダイアログを作成Create(hParent, DlgName, DlgProc);
    if(!Glut_BCC.Create(NULL, hInstance, "IDD_GLUT", ModelessProc))
        return 0L;

    //メッセージループに入る
    return Glut_BCC.Loop();
}
//解説:今回のプログラムは(普通のSDIウィンドウのような)モードレスダイアログ(注)で書いていますが、↑は典型的なエントリーポイントのWinMain関数です。ダイアログを作成してメッセージループに入る2行だけですが、本当はもっと長々としたプログラムが(BCCSkelton.hの中のコードで)動いています。

//注:メッセージボックスのように、呼び出したウィンドウをフリーズさせることなく、一緒に仲良く機能するダイアログ。

 

【Glut_BCC.h】

/////////////////////////////////////////////////////////////////////////////
// Glut_BCC.h
// Copyright (c) 10/26/2021 by BCCSkelton
// 参考:https://mitani.cs.tsukuba.ac.jp/lecture/old2015/cg/03/03_slides.pdf
/////////////////////////////////////////////////////////////////////////////

#include    <stdio.h>        //単精度実数を文字列化する為のsprintf関数を使う為に追加しました。
//BCCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "BCCSkelton.h"
//リソースIDのヘッダー
#include    "ResGlut_BCC.h"
//CGLクラスのヘッダー
#include    "CGLUT.h"

/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCDLGクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////

class CMyWnd : public CDLG    //解説:コメントに書かれている通り、BCCSkeltonのCDLGクラスから派生させて今回のダイアログを作ります。
{
public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(char* UName) : CDLG(UName) {}
    //メニュー項目、ダイアログコントロール関連(解説:ここで宣言している関数をProcファイルで実装します。)
    bool OnSelected(WPARAM);
    bool OnSelCol();
    bool OnCreate();
    bool OnSetLA();
    bool OnSelProj(WPARAM);
    bool OnSetProj();
    bool OnRotate();
    bool OnEndXChanged(WPARAM);
    bool OnEndYChanged(WPARAM);
    bool OnEndZChanged(WPARAM);
    bool OnLight();
    bool OnLightXChanged(WPARAM);
    bool OnLightYChanged(WPARAM);
    bool OnLightZChanged(WPARAM);
    bool OnSelLight(WPARAM);
    bool OnIdok();
    //ウィンドウメッセージ関連
    bool OnInit(WPARAM, LPARAM);
    bool OnHScroll(WPARAM, LPARAM);    //解説:普段は使わないWM_HSCROLLですが、スライダーのメッセージを受けるために使います。
    bool OnClose(WPARAM, LPARAM);
    bool OnDestroy(WPARAM, LPARAM);
    //ユーザー定義関数
    GLdouble GetDouble(UINT);    //テキストボックスからGLDouble値を取得する
    void SetControlState(int);    //解説:操作状態でコントロールを有効、無効にする必要がある為
};

////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////

CMyWnd Glut_BCC("Glut_BCC");    //ウィンドウクラスインスタンスの生成(解説:この変数宣言で実際に↑のクラスの実体を作ります。)

BEGIN_MODELESSDLGMSG(ModelessProc, Glut_BCC)    //コールバック関数名は主ウィンドウの場合ModelessProcにしている(解説:この部分はマクロでウィンドウメッセージ処理を行うコールバック関数に変わります。)
    //メニュー項目、ダイアログコントロール関連(解説:↑の関数と関連しています。)
    ON_COMMAND(Glut_BCC, IDC_SELOBJ, OnSelected(wParam))
    ON_COMMAND(Glut_BCC, IDC_COLOR, OnSelCol())
    ON_COMMAND(Glut_BCC, IDC_CREATE, OnCreate())
    ON_COMMAND(Glut_BCC, IDC_SETLA, OnSetLA())
    ON_COMMAND(Glut_BCC, IDC_SELPROJ, OnSelProj(wParam))
    ON_COMMAND(Glut_BCC, IDC_SETPROJ, OnSetProj())
    ON_COMMAND(Glut_BCC, IDC_ROTATE, OnRotate())
    ON_COMMAND(Glut_BCC, IDC_ENDX, OnEndXChanged(wParam))
    ON_COMMAND(Glut_BCC, IDC_ENDY, OnEndYChanged(wParam))
    ON_COMMAND(Glut_BCC, IDC_ENDZ, OnEndZChanged(wParam))
    ON_COMMAND(Glut_BCC, IDC_LIGHT, OnLight())
    ON_COMMAND(Glut_BCC, IDC_LIGHTX, OnLightXChanged(wParam))
    ON_COMMAND(Glut_BCC, IDC_LIGHTY, OnLightYChanged(wParam))
    ON_COMMAND(Glut_BCC, IDC_LIGHTZ, OnLightZChanged(wParam))
    ON_COMMAND(Glut_BCC, IDC_SELLIGHT, OnSelLight(wParam))
    ON_COMMAND(Glut_BCC, IDOK, OnIdok())
    //ウィンドウメッセージ関連(解説:↑の関数と関連しています。)
    //自動的にダイアログ作成時にOnInit()、終了時にOnClose()を呼びます
    ON_HSCROLL(Glut_BCC)
    ON_DESTROY(Glut_BCC)
END_DLGMSG

/////////////////////////////
//Glutクラス(CGL)外部変数
/////////////////////////////

CGLUT gl;    //解説:前回作ったCGLUTクラスの実体(インスタンス)を、外部変数として作ります。

/////////////////////////////
//GLUT描画オブジェクト名配列(解説:18の基本図形の名称を配列化しています。)
/////////////////////////////

char* g_ObjName[18] = {    "ワイアーティーポット",    "ソリッドティーポット",
                        "ワイアー直方体",        "ソリッド直方体",
                        "ワイアー球体",            "ソリッド球体",
                        "ワイアー円錐",         "ソリッド円錐",
                        "ワイアー円環",            "ソリッド円環",
                        "ワイアー四面体",         "ソリッド四面体",
                        "ワイアー八面体",         "ソリッド八面体",
                        "ワイアー十二面体",     "ソリッド十二面体",
                        "ワイアー二十面体",     "ソリッド二十面体"};

/////////////////////
//GLUT投影方法名配列(解説:投影方法を配列化しています。)
/////////////////////

char* g_ProjName[3] = {"透視投影(gluPerspective)", "透視投影(glFrustum)", "並行投影(glOrtho)"};


////////////////////
//glLight照光名配列(解説:ライトの光属性を配列化しています。)
////////////////////

char* g_LightName[3] = {"環境光(ambient)", "拡散光(diffuse)", "鏡面光(specular)"};

//外部変数(解説:実際にプログラムで使う外部変数です。Glut_BCCProc.hファイルやGlut_CBFunc.hファイルで使います。)
bool g_rotate = TRUE;                                            //常時回転フラグ
bool g_light = FALSE;                                            //Light使用フラグ
int g_prjctrls = 0;                                                //投影方式ID(0-gluPerspective、1-glFrustum、2-glOrtho)
GLdouble g_Red = 1.0, g_Green = 0, g_Blue = 0, g_Alpha = 1.0;    //描画色
GLdouble g_r = 0.0;                                                //回転角度
GLdouble g_x = 0.0, g_y = 1.0, g_z = 0.0;                        //回転軸(起点(0, 0, 0))の軸端座標

GLfloat g_lightpos[4] = {50.0, 50.0, 50.0, 1.0};                    //光源の座標(初期値(50.0, 50.0, 0.0)(解説:最初これをダイアログから変えるつもりでしたが、余りに煩瑣なので止めました。)
int g_selected = 0;                                                //ライト、光属性とRGBA強度配列用添字
const GLenum g_lightno[] = {GL_LIGHT0, GL_LIGHT1, GL_LIGHT2};    //ライト識別子(解説:本プログラムでは3つ使用)
/*
const GLenum m_lightno[] = {GL_LIGHT0, GL_LIGHT1, GL_LIGHT2,    //ライト識別子(解説:8つ迄使えるので拡張性の為に。)
                            GL_LIGHT3, GL_LIGHT4, GL_LIGHT5,
                            GL_LIGHT6, GL_LIGHT7};
*/

const GLenum g_kind[] = {GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR,};    //光属性(GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR)
/*
const GLenum m_lightparam[] = {    //光属性(GL_AMBIENT、GL_DIFFUSE、GL_SPECULAR)
                            GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR,
                            GL_POSITION, GL_SPOT_DIRECTION,
                            GL_SPOT_EXPONENT, GL_SPOT_CUTOFF,
                            GL_CONSTANT_ATTENUATIO, GL_LINEAR_ATTENUATION, GL_QUADRATIC_ATTENUATION};
*/
    //解説:これも拡張性の為に、です。今回は使っていません。
const GLfloat g_lightcolor[3][4] = {{1.0, 1.0, 1.0, 1.0},        //RGBA強度(環境光)-GL_LIGHT0
                                    {1.0, 1.0, 1.0, 1.0},        //RGBA強度(拡散光)-GL_LIGHT1
                                    {1.0, 1.0, 1.0, 1.0}};        //RGBA強度(鏡面光)-GL_LIGHT2
//解説:環境光、拡散光、鏡面光の初期値を配列にしています。

/*
const GLfloat g_lightcolor[3][4] = {{0.0, 0.0, 0.0, 0.0},        //RGBA強度(環境光)-GL_LIGHT0
                                    {1.0, 1.0, 1.0, 1.0},        //RGBA強度(拡散光)-GL_LIGHT1
                                    {1.0, 1.0, 1.0, 1.0}};        //RGBA強度(鏡面光)-GL_LIGHT2

*/
/*
const GLfloat g_lightcolor[3][4] = {{0.0, 0.0, 0.0, 1.0},        //RGBA強度(環境光)-GL_LIGHT0
                                    {0.0, 0.0, 0.0, 1.0},        //RGBA強度(拡散光)-GL_LIGHT1
                                    {0.0, 0.0, 0.0, 1.0}};        //RGBA強度(鏡面光)-GL_LIGHT2
*/
/*
const GLfloat g_lightcolor[3][4] = {{0.0, 0.0, 0.0, 1.0},        //RGBA強度(環境光)-GL_LIGHT0
                                    {0.5, 0.5, 0.5, 1.0},        //RGBA強度(拡散光)-GL_LIGHT1
                                    {0.5, 0.5, 0.5, 1.0}};        //RGBA強度(鏡面光)-GL_LIGHT2
*/
    //解説:このコメントは開発時に色々な値を試してみた際に又使うかもしれないから、と残したものです。

 

さて、ここまでで定義や宣言が終わりましたので、その内容をGlut_BCCProc.hとGlut_CBFunc.hのプログラムで実装してゆきましょう。

 

前回、「次回はGlut_BCCを作る為に必要なGLUTの機能をクラス化した"CGLUT"クラス(CGLUT.h)を解説します」などと書きましたが、実は単に関数群をクラスメンバーにした程度で「本当に必要があるの?」程度のクラスです。

 

しかし、

 

コーディングの効率とかの効率とか隠蔽化とかの問題ではなく、「私の学習(『フゥーン、そうなんだー』)」効果は結構ありました。要すればGLUTのお作法に沿ってコンストラクターでオブジェクトを作り(従ってその際のパラメーターを内部にメンバーとして保有します)、関数群はほぼそのまま(従って全部は無理なのでアプリを書くのに必要なものだけにしました)、定番処理(視点設定や視野空間の設定等)を工夫した程度です。(デストラクターなどは全く手を入れてません。→ウィンドウ廃棄(glutDestroyWindow)程度は入れるべきだったかな?)

 

以下を観ればほとんどGLUTで書くのと変わりないように見えますが、最大の利点はワイアー、ソリッド各9,全18の基本図形を好きに呼び出せるところでしょうか?

 

次回からはSkeltonWizardで作成したBCCSkeltonのプログラムを解説しましょう。

 

【CGLUT.h】

////////////////////////////////////
// CGLUT.h for BCC Skelton
// GLUT(OpenGL)クラス定義ファイル
// Copyright (c) May, 2024
//        By Ysama
////////////////////////////////////

#include    <GL\freeglut.h>                //freeglutライブラリをWindowsで使う為
#pragma comment(lib, "freeglut.lib")

class CGLUT
{
private:
    //ウィンドウ関連メンバー
    HWND m_hWnd = NULL;                                //表示ウィンドウハンドル
    int m_win_no;                                    //ウィンドウ番号
    int m_x = 100;                                    //表示ウィンドウx位置
    int m_y = 100;                                    //表示ウィンドウy位置
    int m_w = 640;                                    //表示ウィンドウ幅
    int m_h = 480;                                    //表示ウィンドウ高さ
    unsigned int m_mode = GLUT_SINGLE | GLUT_RGBA;    //表示ウィンドウモード GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE)
    char m_title[MAX_PATH] = {0};                    //表示ウィンドウタイトル
public:
    //描画オブジェクト関連
    int m_Obj = 0;                                    //描画オブジェクト番号(0 - 8)
    bool m_sw = FALSE;                                //ワイアーか、ソリッドかのフラグ(Solid - TRUE, Wire - FALSE)
    //gluLookAt関数関連
    GLdouble m_LA[9] = {0.0, 0.0, 2.0,                //視点位置(GLdouble) eyex, eyey, eyez,
                        0.0, 0.0, 0.0,                //クリップ平面の中心点(GLdouble) centerx, centery, centerz,
                        0.0, 1.0, 0.0};                //アっプベクター(GLdouble) upx, upy, upz
    //投影法選択
    int m_proj = 0;                                    //初期値はgluPerspective関数
    //gluPerspective関数関連
    GLdouble m_fov = 90.0;                            //視野(Fiield of view)で単位は度
    GLdouble m_aspect = m_w / m_h;                    //縦横比(幅 / 高さ)
    GLdouble m_near = 1.0;                            //視点から近クリップ平面までの距離(正)
    GLdouble m_far = 20.0;                            //視点から遠クリップ平面までの距離(正)
    //glFrustum関数関連
    //注意:m_nearとm_farは共用
    GLdouble m_left = -1.0;                            //クリップ平面左座標
    GLdouble m_right = 1.0;                            //クリップ平面右座標
    GLdouble m_bottom = -1.0;                        //クリップ平面下座標
    GLdouble m_top = 1.0;                            //クリップ平面上座標
    //glOrtho関数関連(立方体の左右上下前後の中心を並行投影)
    //注意:m_left、m_right、m_bottomm_topは共用

    GLdouble m_front = 1.0;                            //手前クリップ平面座標
    GLdouble m_back = -1.0;                            //奥クリップ平面座標
public:
    CGLUT();                                        //コンストラクター
    ~CGLUT();                                        //デストラクター
    HWND GethWnd() {return m_hWnd;}
    void SetPos(int x, int y) {m_x = x; m_y = y;}
    void SetSize(int w, int h) {m_w = w; m_h = h;}
    void SetTitle(char* name) {lstrcpy(m_title, name);}
    void SetMode(unsigned int mode) {m_mode = mode;}
    void Enable(GLenum cap) {glEnable(cap);}
    void Disable(GLenum cap) {glDisable(cap);}
    int Create(char*);
    void Display(void(*func)()) {glutDisplayFunc(func);} 
    void ReDisp() {glutPostRedisplay();}
    void SetLA(GLdouble, GLdouble, GLdouble,        //glLookAtと同じ
                GLdouble, GLdouble , GLdouble,        //値をm_LA配列に保存し、
                GLdouble, GLdouble, GLdouble);        //LookAt(gluLookAt)を呼ぶ
    void LookAt();
    void Color(GLint r, GLint g, GLint b, GLint a = 1) {glColor4i(r, g, b, a);}                        //オーバーロード(整数引数)
    void Color(GLfloat r, GLfloat g, GLfloat b, GLfloat a = 1.0) {glColor4f(r, g, b, a);}            //オーバーロード(単精度引数)
    void Color(GLdouble r, GLdouble g, GLdouble b, GLdouble a = 1.0) {glColor4d(r, g, b, a);}        //オーバーロード(倍精度引数)
    void Clear(GLbitfield mask) {glClear(mask);}    //GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_ACCUM_BUFFER_BIT, GL_STENCIL_BUFFER_BIT
    void ClearCol(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha) {glClearColor(red, green, blue, alpha);}
    void Transfer(GLdouble x, GLdouble y, GLdouble z) {glTranslated(x, y, z);}
    void Rotate(GLfloat angle, GLfloat x, GLfloat y, GLfloat z) {glRotatef(angle, x, y, z);}        //オーバーロード(単精度引数)
    void Rotate(GLdouble angle, GLdouble x, GLdouble y, GLdouble z) {glRotated(angle, x, y, z);}    //オーバーロード(倍精度引数)
    void Scale(GLfloat x, GLfloat y, GLfloat z) {glScalef(x, y, z);}                                //オーバーロード(単精度引数)
    void Scale(GLdouble x, GLdouble y, GLdouble z) {glScaled(x, y, z);}                                //オーバーロード(倍精度引数)
    void Reshape(void(*func)(int, int)) {glutReshapeFunc(func);}
    void Resize(int, int);
    void Refresh();                                                                                    //オーバーロード(引数なし)
    void Idle(void(*func)()) {glutIdleFunc(func);}
    void Mouse(void(*func)(int button, int state, int x, int y)) {glutMouseFunc(func);} 
    void Keyboard(void(*func)(unsigned char key, int x, int y)) {glutKeyboardFunc(func);}
    void Loop() {glutMainLoop();}
    void DrawObject();        //3Dオブジェクト描画関数
};

//コンストラクター
CGLUT::CGLUT() {

    glutInit(&__argc, __argv);            //void glutInit(int* &argc, char** argv);
    glutInitWindowPosition(m_x, m_y);    //void glutInitWindowPosition(int x, int y);
    glutInitWindowSize(m_w, m_h);        //void glutInitWindowSize(intw, int h);
    glutInitDisplayMode(m_mode);        //void glutInitDisplayMode(unsigned int mode);
}

//デストラクター
CGLUT::~CGLUT() {

}

//表示ウィンドウ生成
int CGLUT::Create(char* name = NULL) {

    m_win_no = glutCreateWindow("");    //int glutCreateWindow(char * title)    //戻り値はウィンドウ番号
    //GLのデバイスコンテキストハンドル取得
    HDC glDc = wglGetCurrentDC();
    //ウィンドウハンドル取得
    m_hWnd = WindowFromDC(glDc);
    if(!name)
        if(!m_title)
            SendMessage(m_hWnd, WM_SETTEXT, NULL, (LPARAM)__argv[0]);
        else
            SendMessage(m_hWnd, WM_SETTEXT, NULL, (LPARAM)m_title);
    else {
        lstrcpy(m_title, name);
        SendMessage(m_hWnd, WM_SETTEXT, NULL, (LPARAM)m_title);
    }
}

void CGLUT::SetLA(GLdouble eye_x, GLdouble eye_y, GLdouble eye_z,
                    GLdouble center_x, GLdouble center_y, GLdouble center_z,
                    GLdouble up_x, GLdouble up_y, GLdouble up_z) {
    m_LA[0] = eye_x; m_LA[1] = eye_y; m_LA[2] = eye_z;
    m_LA[3] = center_x; m_LA[4] = center_y; m_LA[5] = center_z;
    m_LA[6] = up_x; m_LA[7] = up_y; m_LA[8] = up_z;
    LookAt();
}

void CGLUT::LookAt() {
    gluLookAt(m_LA[0], m_LA[1], m_LA[2],
            m_LA[3], m_LA[4], m_LA[5],
            m_LA[6], m_LA[7], m_LA[8]);
}

void CGLUT::Resize(int width, int height) {

    glViewport(0, 0, width, height);    //ウインドウ全体に表示
    glMatrixMode(GL_PROJECTION);        //投影変換モードへ
    glLoadIdentity();                    //投影変換の変換行列を単位行列で初期化
    switch(m_proj) {
    case 0:    //①透視投影法1
        gluPerspective(m_fov, m_aspect, m_near, m_far);    //視野角度、縦横比、近クリップ平面までの距離、遠クリップ平面までの距離
        LookAt();
        break;
    case 1:    //②透視投影法2
        glFrustum(m_left, m_right, m_bottom, m_top, m_near, m_far);    //クリップ平面座標(left, right, bottom, top)、近クリップ平面までの距離、遠クリップ平面までの距離
        LookAt();
        break;
    case 2:    //③正投影法(遠近法を含まない投影)
        glOrtho(m_left, m_right, m_bottom, m_top, m_front, m_back);    //m_left, m_right, m_bottom, m_top, m_front, m_backで囲まれる立方体を並行投影
        break;
    }
    glMatrixMode(GL_MODELVIEW);            //視野変換・モデリング変換モードへ
    glLoadIdentity();                    //視野変換・モデリング変換の変換行列を単位行列で初期化
}

void CGLUT::Refresh() {

    Resize(m_w, m_h);
}

//3Dオブジェクト描画関数
void CGLUT::DrawObject() {

    switch(m_Obj) {
    default:    //1 - 8以外は0となる
    case 0:        //ティーポット
        if(m_sw)
            glutSolidTeapot(0.5);
        else
            glutWireTeapot(0.5);
        break;
    case 1:        //Cube(立方体)
        if(m_sw)
            glutSolidCube(0.5);
        else
            glutWireCube(0.5);
        break;
    case 2:        //Sphere(球)
        if(m_sw)
            glutSolidSphere(0.5, 20, 20);
        else
            glutWireSphere(0.5, 20, 20);
        break;
    case 3:        //Cone(円錐)
        if(m_sw)
            glutSolidCone(0.5, 0.8, 20, 20);
        else
            glutWireCone(0.5, 0.8, 20, 20);
        break;
    case 4:        //Torus (ドーナッツ)
        if(m_sw)
            glutSolidTorus(0.1, 0.4, 20, 20);
        else
            glutWireTorus(0.1, 0.4, 20, 20);
        break;
    case 5:        //Tetrahedron(4面体)
        if(m_sw)
            glutSolidTetrahedron();
        else
            glutWireTetrahedron();
        break;
    case 6:        //Octahedron(8面体)
        if(m_sw)
            glutSolidOctahedron();
        else
            glutWireOctahedron();
        break;
    case 7:        //Dodecahedron(12面体)
        if(m_sw)
            glutSolidDodecahedron();
        else
            glutWireDodecahedron();
        break;
    case 8:        //Icosahedron(20面体)
        if(m_sw)
            glutSolidIcosahedron();
        else
            glutWireIcosahedron();
        break;
    }
}
 

ひょんなことからOpenGLに手を出し、C、C++でGLUTを試した後、C#のOpenTKを導入し、矢張りOpenGLの基本をGLUTで学び直そうとしたことご高承の通りです。(「【OpenGL】3DグラフィックとGLUT」シリーズ)

 

そしてワールド、ローカルの三次元座標、ベクトル行列による座標変換、視点と視野、平行投影と透視投影を一応舐めて、立方体、正三角錐によるソリッド、ワイアー、照光、質感までやってきました。その総括として今回は本ブログの本筋であるBCCForm and BCCSkeltonとGLUTの18の基本図形を使って、「よりC++的に、よりGLUT的にデモプログラムを作ろう」というのがGlut_BCCの開発趣旨です。

 

実際にこれを開発したのは「【OpenGL】3DグラフィックとGLUT」シリーズに先立ちますが、Cベースでコンソールウィンドウを使うGLUTのオールドファッション性は嫌だったので、ウィンドウでGLUTを制御できないかと考えました。そしてその開発のプロセスは、

 

(1)GLUTのコントロールダイアログの雛形作成

(2)BCCSkeltonのよるプロトタイプの作成

解説:この段階では単にGLUTを使って動かした、というだけで、まだ「何を、どうすればよい」のかもわからない段階でした。(「X軸 、Y軸 、Z軸」とかスライダーで作っていますが、完全に的を外していましたね。)

 

(3)GLUTのクラス化

(4)コントロールダイアログの完成

解説:段々とOpenGLのことが分かってきたので、少しまともになりました。しかしこの段階でも画像が見えなかったり、視点がプリミティブの中に入ってしまったり、と色々と不具合があったのと照光や質感が未だ分かっていませんでした。

 

(5)テストランと不具合修正のループ

 

を経て、Version 1.0が出来ました。

解説:ダイアログコントロールのデータを反映するタイミングや照光、質感のデータが私にはとんと分からないのでウェブのサンプルを参考にする等、結構疲れましたね。

 

と、いうことで、

 

開発プロセスに沿って全部書くのは大変なので、Version 1.0の最終型を使って解説してゆきます。今回はリソースです。(リソースのIDファイルであるResGlut_BCC.hは省略します。)

 

【Glut_BCC.rc】

解説:リソースファイルはダイアログとアイコンだけですが、ダイアログのコントロールの数が多いので結構大変でした。但し特殊なコントロールは無く、↑のVersion 1.0のイメージと付き合わせて見てください。

 

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------

#include    "ResGlut_BCC.h"

//----------------------------------
// ダイアログ (IDD_GLUT)
//----------------------------------

IDD_GLUT DIALOG DISCARDABLE 0, 0, 294, 370
EXSTYLE WS_EX_CLIENTEDGE | WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_BORDER | WS_CAPTION | WS_SYSMENU | DS_SETFONT | DS_CENTER
CAPTION "Glut-BCC"
FONT 8, "MS 明朝"
{
 CONTROL "", IDC_SELOBJ, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 12, 11, 170, 105
 CONTROL "色", IDC_COLOR, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, 186, 11, 14, 14
 CONTROL "GL Window作成", IDC_CREATE, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON, 214, 9, 74, 18
 CONTROL "eyeX", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 45, 21, 12
 CONTROL "0.0", IDC_EYEX, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 36, 41, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "eyeY", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 78, 45, 21, 12
 CONTROL "0.0", IDC_EYEY, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 102, 41, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "eyeZ", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 144, 45, 21, 12
 CONTROL "2.0", IDC_EYEZ, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 168, 41, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "面X", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 63, 21, 12
 CONTROL "0.0", IDC_PLANEX, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 36, 59, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "面Y", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 78, 63, 21, 12
 CONTROL "0.0", IDC_PLANEY, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 102, 59, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "面Z", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 144, 63, 21, 12
 CONTROL "0.0", IDC_PLANEZ, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 168, 59, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "upvX", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 82, 21, 12
 CONTROL "0.0", IDC_UPVX, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 36, 78, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "upvY", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 78, 82, 21, 12
 CONTROL "1.0", IDC_UPVY, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 102, 78, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "upvZ", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 144, 82, 21, 12
 CONTROL "0.0", IDC_UPVZ, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 168, 78, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "視点視野設定", IDC_SETLA, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | BS_PUSHBUTTON, 214, 55, 74, 18
 CONTROL "視点視野", 0, "BUTTON", WS_CHILD | WS_VISIBLE | BS_GROUPBOX, 3, 28, 205, 70
 CONTROL "", IDC_SELPROJ, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | CBS_DROPDOWNLIST | WS_VSCROLL, 12, 112, 170, 54
 CONTROL "視野角", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 134, 32, 12
 CONTROL "90.0", IDC_FOV, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 46, 130, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "縦横比", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 96, 134, 32, 12
 CONTROL "1.3", IDC_ASPECT, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 130, 130, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "前方距離", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 152, 32, 12
 CONTROL "1.0", IDC_NEAR, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 46, 148, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "後方距離", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 96, 152, 32, 12
 CONTROL "20.0", IDC_FAR, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 130, 148, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "投影面左", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 174, 32, 12
 CONTROL "-1.0", IDC_LEFT, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 46, 170, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "投影面右", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 96, 174, 32, 12
 CONTROL "1.0", IDC_RIGHT, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 130, 170, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "投影面下", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 192, 32, 12
 CONTROL "-1.0", IDC_BOTTOM, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 46, 188, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "投影面上", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 96, 192, 32, 12
 CONTROL "1.0", IDC_TOP, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 130, 188, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "投影面奥", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 214, 32, 12
 CONTROL "-1.0", IDC_BACK, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 46, 210, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "投影面前", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 96, 214, 32, 12
 CONTROL "1.0", IDC_FRONT, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 130 210, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "投影設定", IDC_SETPROJ, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | BS_PUSHBUTTON, 214, 154, 74, 18
 CONTROL "投影", 0, "BUTTON", WS_CHILD | WS_VISIBLE | BS_GROUPBOX, 3, 102, 205, 130
 CONTROL "常時回転", IDC_ROTATE, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | BS_CHECKBOX | BS_RIGHTBUTTON, 226 244, 48, 12
 CONTROL "軸端X", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 250, 21, 12
 CONTROL "0.0", IDC_ENDX, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 36, 246, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "軸端Y", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 78, 250, 21, 12
 CONTROL "1.0", IDC_ENDY, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 102, 246, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "軸端Z", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 144, 250, 21, 12
 CONTROL "0.0", IDC_ENDZ, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 168, 246, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "", IDC_ANGLE, "MSCTLS_TRACKBAR32", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | TBS_AUTOTICKS | TBS_HORZ, 12, 274, 183, 18
 CONTROL "回転角", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY | SS_CENTER, 12, 266, 183, 9
 CONTROL "回転", 0, "BUTTON", WS_CHILD | WS_VISIBLE | BS_GROUPBOX, 3, 234, 205, 64

 CONTROL "照光効果", IDC_LIGHT, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | BS_CHECKBOX | BS_RIGHTBUTTON, 226 310, 48, 12
 CONTROL "光源X", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 316, 21, 12
 CONTROL "50.0", IDC_LIGHTX, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 36, 312, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "光源Y", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 78, 316, 21, 12
 CONTROL "50.0", IDC_LIGHTY, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 102, 312, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "光源Z", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 144, 316, 21, 12
 CONTROL "50.0", IDC_LIGHTZ, "EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | ES_AUTOHSCROLL | ES_RIGHT, 168, 312, 30, 14, WS_EX_CLIENTEDGE
 CONTROL "光反射属性", 0, "STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 12, 334, 42, 12
CONTROL "", IDC_SELLIGHT, "COMBOBOX", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_DISABLED | CBS_DROPDOWNLIST | WS_VSCROLL, 56, 330, 144, 54
 CONTROL "光源と照光", 0, "BUTTON", WS_CHILD | WS_VISIBLE | BS_GROUPBOX, 3, 300, 205, 64

 CONTROL "終了", IDOK, "BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 214, 345, 74, 18
}

//--------------------------
// イメージ(IDI_ICON)
//--------------------------

IDI_ICON    ICON    DISCARDABLE    "Icon.ico"

 

解説:アイコンはいつものことながら絵心がないので、こんなものしか作れませんでした。

 

次回はGlut_BCCを作る為に必要なGLUTの機能をクラス化した"CGLUT"クラス(CGLUT.h)を解説します。

 

現在の【OpenGL】シリーズいかがでしょうか?

プログラミングや専門知識がなくとも、「あー、こうやって画面の中で3D表示するのかぁ」という感じが伝わってくれれば御の字です。

 

所で、皆さんとCやC++でGLUTを使ってOpenGLを勉強している間に、私はOpenTKの話題を取り上げる下準備をしていますが、「これが同じOpenGL仕様に基づくライブラリーなのか?」と感じるほど、GLUTとOpenTKでは洗練度や発展度合いが違います。C#はC++の発展形(要するに"C++++"の意味です)と言われますが、多くのオブジェクトがクラス化、構造体化、プロパティ化して、よく使う関数などもメソッドとして実装されています。OpenGLなどはその活躍の場として適当なんでしょう。

 

以下は今日経験した話です。(3つとも同じように動くことを確認しました。)

 

1.面の法線の計算(1)

照光に対するプリミティブの法線を得るべく、GLUTで使った関数をベースにfloat型の変数を使って次のようにC#用に書き換えました。

 

        //面の法線の計算(1)
        float[] GetNormal(double[] vertex1, double[] vertex2, double[] vertex3)    //引数は隣接する3頂点の座標
        {
            float[] Normal = new float[3];    //戻り値用変数
            double ax, ay, az, bx, by, bz, nx, ny, nz, len; 

            //3 - 2間のベクトル(ax, ay, az)
            ax = vertex3[0] - vertex2[0];    //x
            ay = vertex3[1] - vertex2[1];    //y
            az = vertex3[2] - vertex2[2];    //z
            //1 - 2間のベクトル(bx, by, bz)
            bx = vertex1[0] - vertex2[0];    //x
            by = vertex1[1] - vertex2[1];    //y
            bz = vertex1[2] - vertex2[2];    //z
            //a、bベクトルの外積を計算
            nx = ay * bz    - az * by;
            ny = az * bx - ax * bz;
            nz = ax * by - ay * bx;
            //法線を正規化
            len = Math.Sqrt(Math.Pow(nx, 2.0) + Math.Pow(ny, 2.0) + Math.Pow(nz, 2.0));
            Normal[0] = (float)(nx / len);
            Normal[1] = (float)(ny / len);
            Normal[2] = (float)(nz / len);
            return Normal;
        }
 

2.面の法線の計算(2)

ところが、Vertex3という使いやすい(というか、OpenTKでは必須の)構造体(注)があり、こっちを使うべきじゃないか、と書き直しました。

柱:C++と異なり、C#ではクラスと構造体は一見ほとんど同じように見えます。違いはこちら。ユーザーの方が書かれた、これとか、これも参考になるでしょう。

 

        //面の法線の計算(2)
        Vector3 GetNormal(Vector3 vertex1, Vector3 vertex2, Vector3 vertex3)    //引数は隣接する3頂点の座標
        {
            Vector3 Normal = new Vector3();        //戻り値用変数
            double ax, ay, az, bx, by, bz, nx, ny, nz, len; 

            //3 - 2間のベクトル(ax, ay, az)
            ax = vertex3.X - vertex2.X;    //x
            ay = vertex3.Y - vertex2.Y;    //y
            az = vertex3.Z - vertex2.Z;    //z
            //1 - 2間のベクトル(bx, by, bz)
            bx = vertex1.X - vertex2.X;    //x
            by = vertex1.Y - vertex2.Y;    //y
            bz = vertex1.Z - vertex2.Z;    //z
            //a、bベクトルの外積を計算
            nx = ay * bz    - az * by;
            ny = az * bx - ax * bz;
            nz = ax * by - ay * bx;
            //法線を正規化
            len = Math.Sqrt(Math.Pow(nx, 2.0) + Math.Pow(ny, 2.0) + Math.Pow(nz, 2.0));
            Normal.X = (float)(nx / len);
            Normal.Y = (float)(ny / len);
            Normal.Z = (float)(nz / len);
            return Normal;
        }
 

3.面の法線の計算(3)

当たり前と言えば当たり前ですが、このVector3は当然計算に堪えるようにオブジェクト化されており、必要なメソッドも持っていました。

 

        //面の法線の計算(3)
        Vector3 GetNormal(Vector3 vec1, Vector3 vec2, Vector3 vec3)    //引数は平面上の3頂点ベクトル
        {
            return Vector3.Normalize(Vector3.Cross(vec2 - vec1, vec3 - vec1));
        }
        //Cross - 2 つのベクターのクロス積(日本では外積と呼ぶが必ずしも正確ではない)を計算します。戻り値 Vector3(https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E7%A9%8D)
        //Normalized - 指定したベクトルと方向が同じで、長さが 1 であるベクトルを返します。戻り値 Vector3(https://learn.microsoft.com/ja-jp/dotnet/api/system.numerics.vector3.normalize?view=net-8.0)

 

変数を使って25行書いたコードがコメントを入れて4行になってしまいました。

 

恐るべし、C#!

 

OpenTKの導入から始まり、お勉強のために久々にEmbarcadero C++コンパイラーGLUTfreeglutですが...)でアンマネージドプログラムサンプルを通してOpenGLを学習してきましたが、今回の照光(Lighting)と質感(Material)でひとまず初級を終了したいとおもいす。(というか、私の美術能力の限界です。)

 

前回のワイアー、ソリッドの直方体と正三角錐を交代で表示するプログラムに、今回は「ソリッド表示の際に照光効果と質感表現を確認できるようにする」というのが今回のお題です。いつものことですが、末尾に追加変更部分を彩色したプログラム(【TestGLUT004.cpp】)を掲示します。(なお、前回はソリッド表示で直接頂点データを関数に与えていましたが、ワイアーフレーム表示のデータがあるので、それを使ったループによる処理に変えています。g_Qfaceg_Tface配列を参照願います。

 

今回の照光(Lighting)を使う場合のポイントは、

 

(1)照光効果を有効にする設定(glEnable(GL_LIGHTING);)

(2)(OpenGLの使用によれば最低8つあるべき)光源を有効にする設定(glEnable(GL_LIGHT0);)

(3)glLight(~fは単精度引数型、~fvは単精度配列引数型)関数を使った光源の位置設)

(4)glLight(f | fv)関数を使った環境光(GL_AMBIENT)、拡散光(GL_DIFFUSE)、鏡面光(GL_SPECULAR)のRGB強度設定

(5)(glColorによる色指定ではなく)glNormal3(~dは倍精度引数型、~dvは倍精度配列引数型)関数による法線ベクトルの設定g_QNormalg_TNormalを参照願います。またg_TNormalについては二つのGetNormal関数も参照して下さい。)

 

です。正直に言えば、私は法線ベクトルも、どういう光の種類とRGB強度設定をすればよいのか、よく分かりませんでした。しかし回転させるプリミティブの反射がおかしいことから法線ベクトルをglColorの代わりに使わないとならないことを知り、更に照光効果を使う際はglColorによる色指定はできず、単色の濃淡で表現することも知りました。(注)

注:最初、色指定して照光効果が出ないので可也混乱して色々と調べまくった初心者でした。(汗;)

いずれにせよ、今回のプログラムでは、「ソリッド表示」時に、'l'または'L'キーを押すことで照光効果を体験することが出来るようにしました。(ワイアー表示の際には'l'または'L'キーを押しても無視するようにしました。)

 

私の無智は更に質感設定で露見します。照光効果を得るためには法線設定だけの単色表示、と思い込んでいたのですが、質感を設定することで色も表現できることを知りました。但し、そのような高度な技術は私にはなく、今回のプログラムでは'm'または'M'キーを押すことで質感が変化することが環意見できれば良しとしました。(データは既定値を使っています。)

 

いかがでしたでしょうか?私自身がOpenGLというか3Dグラフィックスの超初心者なので説明の行き届かなかった感がありますが、多くの皆さんと同じ目線でOpenGL、GLUTそして3Dグラフィックスと接することが出来たのではないでしょうか?

 

冒頭で書きましたように、これ以上進む(次はテキスチャー表示、でしょうが)のは私の能力を超えており、単に人のプログラムの紹介になってしまうこと、また当初予定した3Dグラフィックス初心者による犯しがちな過ちや悩みの共有という目的は達せられたと思われることから、「3DグラフィックとGLUT」シリーズは今回でめでたくお開きにさせていただきます。なお、次回以降は、またC#とOpenTKを深堀することを予定しています(が、明日から旅行に行くのでその道中で考えるとにしましょう。)

 

So long!

 

と書いたところで、前にBCCSkeltonで作ったGLUTのデモプログラム、Glut_BCC

の紹介をしていないことを思い出しました。GLUT学習終了記念作品ということでGLUTの特徴の一つであるソリッド、ワイアーフレーム計18の基本図形を使って、今までやってきた視点、視野(投影)、回転、照光をメインウインドウから操作するプログラムをお楽しみください。

 

【TestGLUT004.cpp】

#include <stdio.h>                //C言語ベースなのでprintf等を使う場合必要
#include <GL\freeglut.h>        //Embarcadero C++ コンパイラーの"BCC102\include\windows\sdk\GL"に入れる
#include <math.h>                //powとsqrt関数を使う為

//外部変数
int g_w, g_h;                                        //クライアント領域幅、高さ
GLdouble eyex = 0.0, eyey = 0.0, eyez = 0.9;        //視点座標
bool g_Ortho_Persp = TRUE;                            //平行投影か否かのフラグ
bool g_WorS = TRUE;                                    //Wire図形かSolid図形か
bool g_Light = FALSE;                                //ライトを使うか否かのフラグ
bool g_Material = FALSE;                            //質感を使うか否かのフラグ
bool g_Stop = FALSE;                                //中止するか否かのグフラグ
GLfloat g_Lightpos[4] = {500.0, 50.0, 50.0, 1.0};    //光源0の座標
//光源0のRGB強度(以下はGL_LIGHT0の規定値)
//0.0, 0.0, 0.0, 1.0        //RGBA強度(環境光)
//1.0, 1.0, 1.0, 1.0        //RGBA強度(拡散光)
//1.0, 1.0, 1.0, 1.0        //RGBA強度(鏡面光)

GLfloat g_LightColor[4][4] = {{0.0, 0.0, 0.0, 1.0},    //解説:光源0のRGB強度です。
                            {0.2, 0.2, 0.2, 1.0},
                            {0.5, 0.5, 0.5, 1.0},
                            {1.0, 1.0, 1.0, 1.0}
};


GLdouble g_Color[6][3] = {    //6色    //解説:使用する色を配列化しました。
    {1.0, 0.0, 0.0},        //Red
    {0.0, 1.0, 0.0},        //Green
    {0.0, 0.0, 1.0},        //Blue
    {1.0, 1.0, 0.0},        //Yellow
    {1.0, 0.0, 1.0},        //Magenda
    {0.0, 1.0, 1.0}            //Cyan
};

GLdouble g_Qvertex[8][3] = {//四角形の8頂点(奥左下から反時計回り4点、手前左下から反時計回り4点)
    {-0.5, -0.5, -0.5},        //A
    {0.5, -0.5, -0.5},        //B
    {0.5, 0.5, -0.5},        //C
    {-0.5, 0.5, -0.5},        //D
    {-0.5, -0.5, 0.5},        //E
    {0.5, -0.5, 0.5},        //F
    {0.5, 0.5, 0.5},        //G
    {-0.5, 0.5, 0.5}        //H
};

int g_Qedge[12][2] = {
    {0, 1},                    //(A-B)
    {1, 2},                    //(B-C)
    {2, 3},                    //(C-D)
    {3, 0},                    //(D-A)
    {4, 5},                    //(E-F)
    {5, 6},                    //(F-G)
    {6, 7},                    //(G-H)
    {7, 4},                    //(H-E)
    {0, 4},                    //(A-E)
    {1, 5},                    //(B-F)
    {2, 6},                    //(C-G)
    {3, 7}                     //(D-H)
};

int g_Qface[6][4] = {        //四角形6面(解説:今回は四角面描画データを配列にして使用します。)
    {0, 1, 2, 3},            //ABCD(奥面)
    {1, 5, 6, 2},            //BFGC(右面)
    {5, 4, 7, 6},            //FEHG(手前面)
    {4, 0, 3, 7},            //EADH(左面)
    {4, 5, 1, 0},            //EFBA(底面
    {3, 2, 6, 7}            
//DCGH(天面)
};

GLdouble g_QNormal[6][3] = {//立方体法線(解説:glColorの代わりに使います。)
    {0.0, 0.0, -1.0},
    {1.0, 0.0, 0.0},
    {0.0, 0.0, 1.0},
    {-1.0, 0.0, 0.0},
    {0.0, -1.0, 0.0},
    {0.0, 1.0, 0.0}
};


GLdouble g_Tvertex[4][3] = {//三角形の4頂点(上から下反時計回り4点)
    {0.000f, 0.544f, 0.000f},    //A
    {0.500f, -0.272f, -0.289f},    //B
    {-0.500f, -0.272f, -0.289f},//C
    {0.000f, -0.272f, 0.577f},    //D
};    //2024年6月4日修正


int g_Tedge[6][2] = {
    {0, 1},                    //(A-B)
    {1, 2},                    //(B-C)
    {2, 0},                    //(C-A)
    {0, 3},                    //(A-D)
    {3, 2},                    //(D-B)
    {1, 3}                    //(C-D)
};

int g_Tface[4][3] = {        //三角形の4面(解説:今回は三角面描画データを配列にしました。)
    {0, 1, 2},                //奥の面
    {0, 2, 3},                //左手前面
    {0, 3, 1},                //右手前面
    {1, 2, 3}                //底面
};

GLdouble g_TNormal[4][3] = {//三角錐法線(初期値はGetNormalの結果)
    {0.00, 0.28, -0.96},
    {-0.82, 0.34, 0.47},
    {0.82, 0.34, 0.47},
    {0.00, 1.00, 0.00}        
//解説:これは-1.00が正しい?
};

//面の法線の計算(解説:正三角錐の法線の計算を関数にしてみました。)
GLdouble* GetNormal(GLdouble* vertex1, GLdouble* vertex2, GLdouble* vertex3) {    //引数は隣接する3頂点の座標

    static GLdouble Normal[3];    //戻り値用静的変数
    GLdouble ax, ay, az, bx, by, bz, nx, ny, nz, len; 

    
//3-2間のベクトル(ax, ay, az)
    ax = vertex3[0] - vertex2[0];    //x
    ay = vertex3[1] - vertex2[1];    //y
    az = vertex3[2] - vertex2[2];    //z
    //1-2間のベクトル(bx, by, bz)
    bx = vertex1[0] - vertex2[0];    //x
    by = vertex1[1] - vertex2[1];    //y
    bz = vertex1[2] - vertex2[2];    //z
    //a、bベクトルの外積を計算
    nx = ay * bz - az * by;
    ny = az * bx - ax * bz;
    nz = ax * by - ay * bx;
    
//法線を正規化
    len = sqrt(pow(nx, 2.0) + pow(ny, 2.0) + pow(nz, 2.0));    //解説:nx, ny, nzの二乗の和の平方根です。
    Normal[0] = nx / len;
    Normal[1] = ny / len;
    Normal[2] = nz / len;
    return Normal;
}


//投影設定
void SetProjection(void) {

    //変換行列の初期化(座標変換行列に単位行列を設定)
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    //透視投射範囲の設定
    if(g_Ortho_Persp)
        glOrtho(-1.4, 1.4, -1.4, 1.4, -1.4, 1.4);                    //平行投射範囲の設定
    else {
        if(!g_h)    g_h = 1;    //高さが0の場合は1に設定
        gluPerspective(60.0, (double)g_w /(double)g_h, 1.0, 100.0);    //透視投射範囲の設定
    }
    //モデルビュー変換行列を指定
    glMatrixMode(GL_MODELVIEW);
}

void DrawAxes(void) {

    glPushMatrix();
        glBegin(GL_LINES);
            //X軸(青)
            glColor3d(0.0, 0.0, 1.0);        //青
            glVertex3d(0.0, 0.0, 0.0);        //直方体の視野空間x軸
            glVertex3d(0.95, 0.0, 0.0);
            glVertex3d(0.95, 0.0, 0.0);        //x軸の矢印
            glVertex3d(0.9, 0.03, 0.0);
            glVertex3d(0.95, 0.0, 0.0);
            glVertex3d(0.9, -0.03, 0.0);
            //Y軸(緑)
            glColor3d(0.0, 1.0, 0.0);        //緑
            glVertex3d(0.0, 0.0, 0.0);        //直方体の視野空間y軸
            glVertex3d(0.0, 0.95, 0.0);
            glVertex3d(0.0, 0.95, 0.0);        //y軸の矢印
            glVertex3d(0.03, 0.9, 0.0);
            glVertex3d(0.0, 0.95, 0.0);
            glVertex3d(-0.03, 0.9, 0.0);
            //Z軸(赤)
            glColor3d(1.0, 0.0, 0.0);        //赤
            glVertex3d(0.0, 0.0, 0.0);        //直方体の視野空間z軸
            glVertex3d(0.0, 0.0, 0.95);
            glVertex3d(0.0, 0.0, 0.95);        //z軸の矢印
            glVertex3d(0.0, 0.03, 0.9);
            glVertex3d(0.0, 0.0, 0.95);
            glVertex3d(0.0, -0.03, 0.9);
        glEnd();
    glPopMatrix();
}

void DrawPolygon() {

    static float theta = 0.0f;            //シータ値
    //ポリゴン描画
    glPushMatrix();
        glTranslated(0.5, 0.0, 0.0);
        glScaled(0.5, 0.5, 0.5);
        glRotated(theta, 0.0, 1.0, 0.0);
        theta += 0.05;
        if(g_WorS) {
            glBegin(GL_LINES);
                glColor3dv(g_Color[5]);    //シアン
                for(int i = 0; i < 6; i++) {
                    glVertex3dv(g_Tvertex[g_Tedge[i][0]]);
                    glVertex3dv(g_Tvertex[g_Tedge[i][1]]);
                }
            glEnd();
        }
        else {
            glBegin(GL_TRIANGLES);    //解説:三角形面の描画を配列データで行いました。
                for(int j = 0; j < 4; ++j) {
                    if(g_Light)
                        glNormal3dv(g_TNormal[j]);
                    else
                        glColor3dv(g_Color[j]);
                        for(int i = 0; i < 3; ++i) {
                            glVertex3dv(g_Tvertex[g_Tface[j][i]]);
                        }
                    }
            glEnd();

        }
    glPopMatrix();
    glPushMatrix();
        glTranslated(-0.5, 0.0, 0.0);
        glScaled(0.5, 0.5, 0.5);
        glRotated(theta, 0.0, 1.0, 0.0);
        glRotated(theta, 1.0, 0.0, 0.0);
        //描画処理
        if(g_WorS) {
            glBegin(GL_LINES);
                glColor3dv(g_Color[3]);    //黄
                for(int i = 0; i < 12; i++) {
                    glVertex3dv(g_Qvertex[g_Qedge[i][0]]);
                    glVertex3dv(g_Qvertex[g_Qedge[i][1]]);
                }
            glEnd();
        }
        else {
            glBegin(GL_QUADS);    //解説:四角形面の描画を配列データで行いました。
                for (int j = 0; j < 6; ++j) {
                    if(g_Light)
                        glNormal3dv(g_QNormal[j]);
                    else
                        glColor3dv(g_Color[j]);
                    for (int i = 0; i < 4; ++i) {
                        glVertex3dv(g_Qvertex[g_Qface[j][i]]);
                    }
                }
            glEnd();

        }
        theta += 0.1;
    glPopMatrix();
}

//アイドル時処理関数
void idle(void) {

    glutPostRedisplay();    //再描画関数
}

//描画関数
void display(void) {

    if(g_Stop)    return;                    //描画中止

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);    //色とZ(深度)バッファを初期化
    glLoadIdentity();                     //変換行列を初期化
    gluLookAt(    eyex, eyey, eyez,        //eyex, eyey, eyez
                0.0, 0.0, 0.0,             //targetx, targety, targetz
                0.0, 1.0, 0.0);            //アップベクター(上方向)

    if(g_Light) {                        //光源の位置設定は視点の位置を設定した後に行う必要があります。
            glLightfv(GL_LIGHT0,        //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
                    GL_POSITION,        //ライト位置を指定
                    g_Lightpos);        //光源の座標を表すGLfloatの配列のポインタ
            glLightfv(GL_LIGHT0,        //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
                    GL_AMBIENT,            //光属性(自然光)
                    g_LightColor[0]);    //RGBA強度設定
            glLightfv(GL_LIGHT0,        //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
                    GL_DIFFUSE,            //光属性(拡散光)
                    g_LightColor[3]);    //RGBA強度設定
            glLightfv(GL_LIGHT0,        //ライト識別子( GL_LIGHT1, GL_LIGHT2...)
                    GL_SPECULAR,        //光属性(鏡面光)
                    g_LightColor[3]);    //RGBA強度設定
    }    //解説:ここら辺和私もよく分からないのですが(汗;)、光源の光はこのように重ねて定義することで混合できるようです。

    const static GLfloat material_ambient[4] = {0.2, 0.2, 0.2, 1.0};    //質感設定(初期値-0.2, 0.2, 0.2, 1)
    const static GLfloat material_diffuse[4] = {0.2, 0.2, 0.2, 1.0};    //質感設定(初期値-0.2, 0.2, 0.2, 1)
    const static GLfloat material_specular[4] = {0.0, 0.0, 0.0, 1.0};    //質感設定(初期値-0 , 0 , 0, 1)
    const static GLfloat material_shininess = 48.0;
    if(g_Material) {
        glMaterialfv(GL_FRONT, GL_AMBIENT, material_ambient);
        glMaterialfv(GL_FRONT, GL_SPECULAR, material_specular);
        glMaterialfv(GL_FRONT, GL_DIFFUSE, material_diffuse);
        glMaterialf(GL_FRONT, GL_SHININESS, material_shininess);
    }    
//解説:ここでも反射の相違によるRGB強度の数値を変えることで様々な色や質感を表現できるようです、知らんけど。

    DrawAxes();                        //x, y, z軸を描画
    DrawPolygon();                    //三角錐と直方体を描画
    glFlush();                        //glutSwapBuffersで行われるので不要かも
    glutSwapBuffers();    //glutInitDisplayMode(GLUT_DOUBLE)でダブルバッファリングを利用可
}

//ウィンドウサイズ変更時関数
static void resize(int width, int height)
{
    //クライアントエリアサイズを記録
    g_w = width; g_h = height;
    //クライアントエリア全体に表示
    glViewport(0, 0, g_w, g_h);
    //投影設定
    SetProjection();
    gluLookAt(    eyex, eyey, eyez,    //eyex, eyey, eyez
                0.0, 0.0, 0.0,         //targetx, targety, targetz
                0.0, 1.0, 0.0);        //アップベクター(上方向)
}

//マウス入力処理関数
void mouse(int button, int state, int x, int y) {

    switch(button) {
    case GLUT_LEFT_BUTTON:
        //視点位置と視線方向の設定
        eyex = 1.0; eyey = 1.0; eyez = 1.0;
        g_Ortho_Persp = FALSE;
        break;
    case GLUT_MIDDLE_BUTTON:
        //変換行列の初期化(座標変換行列に単位行列を設定)
        glLoadIdentity();
        //視点位置と視線方向の設定
        eyex = 0.0; eyey = 0.0; eyez = 0.9;
        g_Ortho_Persp = TRUE;
        break;
    case GLUT_RIGHT_BUTTON:
        //視点位置と視線方向の設定
        eyex = -1.0; eyey = 1.0; eyez = 1.0;
        g_Ortho_Persp = FALSE;
        break;
    default:
        break;
    }
    //投影設定
    SetProjection();
    display();
}

//キーボード入力処理関数
void keyboard(unsigned char key, int x, int y) {

    switch(key) {
    //ESC、Qまたはqで終了
    case '\033':    //ESC
    case 'Q':
    case 'q':
        exit(0);
        break;
    case 'L':
    case 'l':
        if(g_WorS) {    
//ワイアーモードの場合はメッセージを出して戻る。
            printf("While primitives are in wire mode, you cannot get light on.\r\n");
            return;
        }
        g_Light = !g_Light;
        if(g_Light) {
            glEnable(GL_LIGHTING);            
//照光を有効化する
            glEnable(GL_LIGHT0);            //光源0を有効化する
            glDisable(GL_COLOR_MATERIAL);    //物体の質感を有効にする設定
        }
        else {
            glDisable(GL_LIGHTING);            
//照光を無効にする設定
            glDisable(GL_LIGHT0);            //光源0を無効にする設定
            glEnable(GL_COLOR_MATERIAL);    //物体の質感を有効にする設定
        }
        break;
    case 'M':
    case 'm':
        if(g_WorS || !g_Light) {            
//ワイアーフレームやライト不使用の場合強制的にオフ
            printf("You can set surface material features only in the light on.\r\n");
            g_Material = FALSE;
            return;
        }
        g_Material = !g_Material;
        break;

    case 'S':
    case 's':
        g_Stop = !g_Stop;
        break;
    case 'W':
    case 'w':
        g_WorS = !g_WorS;
        if(g_WorS) {    //ワイアーフレームの時はライトと質感は外す
            g_Light = FALSE;
            g_Material = FALSE;
            glDisable(GL_LIGHTING);            
//照光を無効にする設定
            glDisable(GL_LIGHT0);            //光源0を無効にする設定
            glEnable(GL_COLOR_MATERIAL);    //物体の質感を有効にする設定
        }
        break;
    default:
        break;
    }
}

void GetNormal() {    //解説:↑にある同名の関数(但し引数が異なるので識別される)を呼び出して、正三角錐の各面の法線を求めています。

    for(int i = 0; i < 4; i++) {
        for(int j = 0; j < 3; j++)
            g_TNormal[i][j] = (GetNormal(g_Tvertex[g_Tface[i][0]], g_Tvertex[g_Tface[i][1]], g_Tvertex[g_Tface[i][2]]))[j];
        
//printf("g_TNormal[%d] = {%.2f, %.2f, %.2f}\r\n", i, g_TNormal[i][0], g_TNormal[i][1], g_TNormal[i][2]);    //法線テスト用
    }
}


//メイン(エントリーポイント)関数
int main(int argc, char *argv[]) {

    glutInit(&argc, argv);                //GLUTの初期化
    glutInitDisplayMode(GLUT_RGBA |        //ディスプレーの初期化
                        GLUT_DOUBLE |
                        GLUT_DEPTH);
    glutInitWindowPosition(100, 100);    //ウィンドウ位置指定
    glutInitWindowSize(640, 640);        //ウィンドウサイズ指定
    //ファイル名だけを表示する
    char* fn;
    for(char* pt = argv[0]; *pt; pt++) {
        if(*pt == '\\')
            fn = ++pt;
    }
    glutCreateWindow(fn);                //タイトル付ウィンドウ生成
    glShadeModel(GL_SMOOTH);            //既定値の滑らかな網かけ
    glEnable(GL_DEPTH_TEST);            //深度テストを有効にする
    glClearColor(0.0, 0.0, 0.0, 1.0);    //画面消去色(黒)
    glutDisplayFunc(display);            //描画関数の指定
    glutReshapeFunc(resize);            //ウィンドウサイズ変更時関数の指定
    glutIdleFunc(idle);                    //アイドル時処理関数の指定
    glutMouseFunc(mouse);                //マウス入力処理関数
    glutKeyboardFunc(keyboard);            //キーボード入力処理関数
    GetNormal();    //解説:正三角錐の法線データを取得する。
    glutMainLoop();                        //メインループ関数
    return 0;
}