デモフレームワーク

注意:ここではある程度デモの話,C言語の基礎,WinAPIがわかるという前提で話を進めていきます。
その辺の話からよくわからない方はそういうサイトを回ってからの方が良いと思われます。
あと、VisualC++(以下VC)限定なので他の方にはあまり有効でないかもしれません。


さて、今回はいよいよデモのプログラムに迫っていきます。
前回までの小さいサイズでのWindowシステムに今回はデモの基本となるフレームワークを
追加していきたいと思います。このフレームワークは1回作っておけばあとは使いまわしがきくので
ライブラリのようにしてまとめておくと便利です。
まぁ今回は私が使っているものを用意したので、まぁこのままでも使えますし、自分で作りたい方は
参考にしつつ自分で作ってみてください。
では、ソースを見ていきたいと思います

/*
    4KB/64KB OpenGL Intro Template
    coded by kioku@Cyber K
*/

//浮動小数点使用のため必要
#ifdef __cplusplus
extern "C" { 
#endif
    int _fltused=1; 
    void _cdecl _check_commonlanguageruntime_version(){}
#ifdef __cplusplus
}
#endif

//基本的なライブラリの宣言
#pragma comment(linker,"/subsystem:windows")
#pragma comment(linker,"/NODEFAULTLIB")
//プロジェクト設定のリンカの方で設定
//#pragma comment(lib,"winmm.lib")
//#pragma comment(lib,"opengl32.lib")
//#pragma comment(lib,"glu32.lib")

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <mmsystem.h>
#include <GL/gl.h>
#include <GL/glu.h>

//================================================================================================
//ユーティリティ
#define MsgBox(str1,str2)        MessageBox(NULL,str1,str2,MB_OK)

//==========================Window関連と初期化処理===============================================
HINSTANCE kb_hInstance;
HDC  kbhDC;//デバイスコンテキスト
static DEVMODE devmode_sav;
static bool disp_change=false;

//OpenGL初期化関数
inline HGLRC __fastcall kbInit_Pixel(HWND hWnd)
{
    HGLRC hRC;
    int pixelformat;
    static PIXELFORMATDESCRIPTOR pfd = {
        sizeof(PIXELFORMATDESCRIPTOR),       //この構造体のサイズ
        1,                                   //OpenGLバージョン
        PFD_DRAW_TO_WINDOW |                 //Windowスタイル
        PFD_SUPPORT_OPENGL |                 //OpenGL
        PFD_DOUBLEBUFFER,                    //ダブルバッファ使用可能
        PFD_TYPE_RGBA,                       //RGBAカラー
        24,                                  //色数
        0, 0,                                //RGBAのビットとシフト設定
        0, 0,                                //G
        0, 0,                                //B
        0, 0,                                //A
        0,                                   //アキュムレーションバッファ
        0, 0, 0, 0,                          //RGBAアキュムレーションバッファ
        24,                                  //Zバッファ
        0,                                   //ステンシルバッファ
        0,                                   //使用しない
        PFD_MAIN_PLANE,                      //レイヤータイプ
        0,                                   //予約
        0, 0, 0                              //レイヤーマスクの設定・未使用
    };
    
    //ピクセルフォーマットの指定
    //OpenGLレンダリングコンテキストの作成
    if(((pixelformat = ChoosePixelFormat(kbhDC, &pfd)) == 0)
    || ((SetPixelFormat(kbhDC, pixelformat, &pfd) == FALSE))
    || (!(hRC=wglCreateContext(kbhDC))))    return NULL;
    return hRC;
}


inline void __fastcall kbSetViewPerspective(int x,short y, int width, int height,float zNear,float zFar)
{
    GLfloat aspect;
    glViewport(x, y, width, height);
    aspect = (GLfloat)width/(GLfloat)height;      ///アスペクト比の初期化
    glMatrixMode( GL_PROJECTION );                ///プロジェクションモードで射影
    gluPerspective( 60.0f, aspect, zNear, zFar);
    glMatrixMode( GL_MODELVIEW );                 ///ノーマルのモデルビューモードへ移行
}

#define kbSetViewport(hWnd, w, h)    kbSetViewPerspective(0, 0, w, h ,0.1f,1000.0f)

#define kbSwapBuffers()        SwapBuffers(kbhDC)

///ウインドウを作成する
inline void __fastcall kbCreateWindow(int x, int y, int width, int height, const char* title, DWORD style)
{
    //Window Create
    HWND kbhWnd;///ウインドウハンドル
    kbhWnd = CreateWindowEx(WS_EX_APPWINDOW,"BUTTON" , title ,//BUTTONクラスでウインドウ作成
            style,
            x, y,
            width,height,
            NULL, NULL, kb_hInstance, NULL);

    if (!kbhWnd) return;//作成失敗
    kbhDC = GetDC(kbhWnd);
    
    //PixelFormat初期化
    wglMakeCurrent(kbhDC,kbInit_Pixel (kbhWnd));

    //Viewportの設定
    kbSetViewport(kbhWnd,width,height);
    
    SetWindowPos(kbhWnd,(HWND)-1,0,0,width,height,SWP_SHOWWINDOW);
    ShowCursor(false);
    return;
}

//解像度変更
inline bool __fastcall kbChangeDisplaySetting(const int width,const int height)
{
    //設定
    devmode_sav.dmSize = sizeof(devmode_sav);
    devmode_sav.dmDriverExtra = 0;
    devmode_sav.dmPelsWidth = width;
    devmode_sav.dmPelsHeight = height;
    HDC hdc = GetDC(NULL);
    devmode_sav.dmBitsPerPel = GetDeviceCaps(hdc,BITSPIXEL);
    ReleaseDC(0,hdc);
    devmode_sav.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT;
    
    //変更
    if(ChangeDisplaySettings(&devmode_sav,CDS_FULLSCREEN)==DISP_CHANGE_SUCCESSFUL) return true;
    return false;
}

//フルスクリーン表示
inline void __fastcall kbGoFullScreen(const int width,const int height)
{
    if(!disp_change) disp_change=kbChangeDisplaySetting(width ,height);
}

では、解説していきます。
まず、kbWindow.hの中の中身ですが
基本的に特に変わったことはしていないと思います。
一番上の浮動小数点使用のためといコメントの下の定義は
floatやdoubleといった変数ををつかうときに必要となります。
あとは、前回までの小サイズでのWindowEXEの作り方を元に、
それに加えてOpenGLプログラムの初期化処理を加えました。
OpenGLの初期化処理に関しては、ここでは本筋ではないので
それを解説したサイトを参照してください。
DirectX使いの方は適宜読み替えて見てください。
自前ソフトウェアレンダー使いの方は・・・以下略
最後のほうは画面のフルスクリーン化です。
まぁコレも普通のフルスクリーン化の処理なので
だいたいコメントもあるし問題はないかと・・
よくわからないけど、まぁいっかぁって方はこのソースでそのままお使いください
もし、なんか明らかにおかしいところがあったり、ソースに問題があればBBSにでも
書き込んでください。それによっては解説追加も考えます。



さて、つぎはmain.cppです
/*
    4KB/64KB OpenGL Intro Template
    coded by kioku@Cyber K
*/
#include "kbWindow.h"

//=================================================================================================
//-------------------------------------------------------------------------------------------------
#define SCREEN_WIDTH    640
#define SCREEN_HEIGHT    480

#define FULLSCREEN_MODE


#define SCENE_MAX    4
DWORD SCENE[SCENE_MAX];

#define TIMESKIP_FOR_DEBUG_TIME        0
#define MUSICTIME_A                10000
#define MUSICTIME_B                10000
#define MUSICTIME_C                10000
//--------------------------------------------------------------------------------------------------
inline void __fastcall Init()
{
    //各シーンの終了時間を計算
    SCENE[0] = 0;
    SCENE[1] = MUSICTIME_A;
    SCENE[2] = SCENE[1] + MUSICTIME_B;
    SCENE[3] = SCENE[2] + MUSICTIME_C;
    
    glEnable(GL_DEPTH_TEST);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
//--------------------------------------------------------------------------------------------------

void __fastcall DrawSquare()
{
    glBegin(GL_QUADS);
        glVertex3f(-5.0f, -5.0f, 0.0f);
        glVertex3f(-5.0f, 5.0f, 0.0f);
        glVertex3f(5.0f, 5.0f, 0.0f);
        glVertex3f(5.0f, -5.0f, 0.0f);
    glEnd();
}

inline void __fastcall demoscene(short scene,float rate)
{
    switch(scene){
        case 0:{//SCENE 0
            glPushMatrix();
                glTranslatef(0.0f,0.0f,-20.0f);
                glRotatef(3600.0f*rate,0.0f,1.0f,0.0f);
                DrawSquare();
            glPopMatrix();
        }break;
        case 1:{//SCENE 1
            glPushMatrix();
                glTranslatef(0.0f,0.0f,-20.0f);
                glColor3f(0.0f,1.0f,0.0f);
                glRotatef(3600.0f*rate,0.0f,1.0f,0.0f);
                DrawSquare();
            glPopMatrix();
        }break;
        case 2:{//SCENE 2
            glPushMatrix();
                glTranslatef(0.0f,0.0f,-20.0f);
                glColor3f(1.0f,1.0f,0.0f);
                glRotatef(3600.0f*rate,0.0f,1.0f,0.0f);
                DrawSquare();
            glPopMatrix();
        }break;
    }
}

//----------------------------------------------------------------------------------------------------
inline void __fastcall OnIdle()
{
    static DWORD starttime=0;
    DWORD nowtime = timeGetTime();
    
    glClearColor(0.0f,0.0f,0.0f,0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    
    if(starttime==0) starttime = nowtime;//Start
    nowtime += TIMESKIP_FOR_DEBUG_TIME;//デバッグ用シーンスキップ
    
    //シンクロシステム
    for(short i=SCENE_MAX-2; i>=0; i--){
        if(nowtime-starttime>SCENE[i]){
            demoscene(i,(nowtime-starttime-SCENE[i])/(float)(SCENE[i+1] - SCENE[i]));
            break;
        }
    }

    if(nowtime-starttime>SCENE[SCENE_MAX-1]) ExitProcess(0);//END
    kbSwapBuffers();
}

//----------------------MAIN-----------------------------------------------------------------------
void WinMainCRTStartup()
{

#ifdef FULLSCREEN_MODE
    kbCreateWindow (0,0,SCREEN_WIDTH,SCREEN_HEIGHT,"sys64k", WS_POPUP);//フルスクリーンモード
    kbGoFullScreen (SCREEN_WIDTH,SCREEN_HEIGHT);
#else
    kbCreateWindow (0,0,SCREEN_WIDTH,SCREEN_HEIGHT,"sys64k", WS_OVERLAPPEDWINDOW);//ウインドウモード
#endif

    //初期化
    Init();
    
    // メイン メッセージ ループ
    MSG msg;
while(TRUE){
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)){
if((msg.message==WM_CHAR)&&(msg.wParam==0x1B)) break;//ESC TranslateMessage(&msg); DispatchMessage(&msg); }else{ OnIdle();//アイドル } } //後処理 ExitProcess(0);//全スレッド強制終了 return ; } int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int) { WinMainCRTStartup(); return 0; }
まぁ今回の説明のメインはこのmain.cppです。
さっきのkbWindow.hなどにウインドウ生成やOpenGLの初期化、フルスクリーンなどは
まとめてしまったので、main.cppはそれらの関数をつかって初期化の処理を行い
実際のデモシステムのコーディングを行います。
処理はOpenGLで書かれていますが、OpenGL処理の説明はしません。
それとは関係ないdemoのフレームワークの説明だけします。


まず、WinMainですが、今までWinMainCRTStartupから書かないとEXEが小さくならない
という話をしてきました。ですが今回はWinMainを書いて、その中でWinMainCRTStartup
を呼んでいます。これはなぜかというと
WinMainCRTStartupから書くと確かにサイズは小さいのですが、
デバッグが正常にできないという欠点がありました。それはVCのシステム上LIBC.libで
VCのデバッグ用の関数が組み込まれており、それを実行しないとVCがプログラムを
トレースできないという問題です。
まぁWindowをだしたり、その程度ならVCのデバッグ機能を使わずに書くことも容易ですが
デモなどの複雑な処理が増えてくるとVCのデバッグ機能が使えないのはかなり痛手です。
たしかに、4k,64kではサイズが命なんですが、別に開発し、デバッグするときのEXEまで
サイズが小さい必要はないのです。そこで、

デバッグモードのときはLIBC.libをリンクしWinMain()からはじめる
リリースモードのときはLIBC.libをリンクしないでWinMainCRTStartup()からはじめる

とすれば、開発するときはデバッグモードでVCのデバッグ機能を使いながらプログラムを作り
最後に作品にするときはリリースモードにすれば自動的に小さくなるという仕組みです。
LIBC.libをインクルードせずにコンパイルするとうまいことWinMainは無視されるので
このソースのままで自動的にWinMainCRTStartupが呼ばれます。
こうすることで、開発時にとくに4k/64kを意識することなくプログラムが組めます。
(とはいっても、無駄に大きい処理やらデータを入れてしまうとすぐに収まらなくなりますが・・)
あと、デバッグモードのときはLIBC.libがリンクされますのでC言語の標準関数が使えます。
つまり、printfデバッグなどが可能になるということです。
(ただし、デモのメインの処理に使ってしまうとあとでリリースモードにしたときに
VCから「そんな関数使えない!」と怒られてしまうので、定期的にリリースモードで
調べながら作っていくのが良いですね。)



OnIdle()関数
実際にデモを作っていくときの処理のほとんどは、このOnIdle関数の中に
書いていくことが多いです。(もちろんそこから呼び出される関数の中ですよ)

  static DWORD starttime=0;
  DWORD nowtime = timeGetTime();
  ・・・
  if(starttime==0) starttime = nowtime;//Start
  nowtime += TIMESKIP_FOR_DEBUG_TIME;//デバッグ用シーンスキップ 
  //シンクロシステム
  for(short i=SCENE_MAX-2; i>=0; i--){
      if(nowtime-starttime>SCENE[i]){
          demoscene(i,(nowtime-starttime-SCENE[i])/(float)(SCENE[i+1] - SCENE[i]));
          break;
      }
  }
  if(nowtime-starttime>SCENE[SCENE_MAX-1]) ExitProcess(0);//END
  ・・・
このOnIdle()関数の中ではDemoの時間と映像の同期を取っています
まず、一番初めの段階にstarttime=0;となっており、starttimeは0で初期化されます。
つぎにstarttime==0となっているのでstarttimeにtimeGetTime()で取得した現在の時間が
代入されます。ここでデモの始まる時間を取得しているわけです。
以後は、このstarttimeは更新されず、(nowtime - starttime)とすることで
デモが始まってからの現在の時間を取得することができます。
for()の中ではSCENE[i]に代入されている各シーンと現在のデモの再生時間と比較し
今はどのシーンを再生するべきかをif(nowtime-starttime>SCENE[i])で調べます。
再生するべきシーンが見つかればdemoscene()関数を呼び出し、
実際のデモの描画処理を行います。
この時demoscene()の第1引数はシーン番号ですが、第2引数は
(nowtime-starttime-SCENE[i])/(float)(SCENE[i+1] - SCENE[i])
となっていますが、これは現在の時間が各シーンのどれくらいの割合にあるのか
を計算しています。
つまり、
SCENE[0] SCENE[1] SCENE[2]
↑starttime             ↑nowtime
という場面では

(nowtime-starttime)-SCENE[i]で (i=0)
SCENE[0] SCENE[1] SCENE[2]
|=====|この間の時間がでます

さらに(SCENE[i+1] - SCENE[i])で (i= 0)
SCENE[0] SCENE[1] SCENE[2]
|======================| この間の時間がでます

で上の時間を下の時間で割ると
現在の時間がSCENE[1]のどのくらいの割合のところなのか
という値(rate)が出てきます(rate = 0 〜 1)
SCENE[0] SCENE[1] SCENE[2]
0----rate-------------1

この値をdemoscene関数に渡してやることで
void demoscene(i,rate)となり
たとえば、物体回転させるときの処理は
glRotatef( 3600.0f*rate, 0.0f, 1.0f, 0.0f);//Y軸を中心に回転
DrawSquare();//描画
としてやることで、3600.0f*rateで
1つのシーンの間に3600度回る(10回転)処理を
demosceneの引数に渡された割合(rate)との積を取ることで
確実にシーンの時間割合と同期したデモを作ることができます。

しかしもし、OnIdle中でで回転処理として
static float angle=0;
angle+=10;
glRotatef(angle,0.0f,1.0f,0.0f);//Y軸を中心に回転
DrawSquare();//描画
というような処理を書いてしまうと
処理速度の速いマシンでは時間当たりにこなすOnIdle関数の処理の量が多いので
angle+=10;の処理が多くなってしまい、遅いマシンと早いマシンで回転速度が違う
(遅いマシンではシーンあたり5回転、早いマシンでは15回転)なんてことになってしまいます。
これを回避して、早いマシンではFPSがあがり滑らかに見える。
遅いマシンでは描画が遅くてとびとびの画像でしかみえないが
ちゃんと時間と同期してるようにみせるには、今回のような処理が必要になります。


demoscene()関数
あとはこの関数の中に引数として渡されたシーン番号をもとに
switch文で分岐して各シーン番号の処理をcase シーン番号のところに
書いてやればOKです。
サンプルのデモはポリゴンがくるくる回ってシーンごとに色が変わるというものです。
難しい処理はしていないので、見てもらえればわかると思います。


最後に・・・ExitProcess()
デモが終了する処理としてExitProcess(0)関数を使用していますが、
これはかなり強制的にプログラムを終了させる処理で、通常は使いませんが
わざわざメモリ解放や変更した解像度を直す処理をするとかすると
結構サイズを食ってしまうので、この関数で一発強制終了します。
NT系のOSではこの関数でちゃんと自動的にメモリ開放などを行ってくれますが
9x系のOSでやるとメモリなどがしっかり解放されない可能性があります。
最近は少なくなりましたが、9x系OS向けのデモを作る場合は気をつけてください。


●おまけ(VCマクロ機能)

#ifdef FULLSCREEN_MODE
    #define FULLSCREEN_MODEが定義してある場合こちらが実行
#else
    #define FULLSCREEN_MODEが定義されてない場合こちらが実行
#endif

としておくと、デバッグするときはWindowモード
確認するときはフルスクリーンモードとかとして切り替えることができます。
(コードの変更必要なし)
#ifndef _DEBUG
としておくとリリースモードのときだけフルスクリーンというようなこともできます



●よし実行!
てことでサンプルのなかの/out/temp4k.exe
を実行してみてください。どうです?ちゃんと時間と同期して動いてますよね?
数台マシンを持ってる人は数台で実行してもらえればわかりますが、どのマシンでも同じ回転速度
同じ時間でシーンが切り替わります。
さてサイズは・・・4.5KBですね。
一緒に入っているtemp4k.batはこのexeを圧縮したものです。
これも同じように実行できるので、実行してみてください。
結果は変わりませんでしたが、サイズは1.68KBになってます。
この圧縮形式はデモパーティAssembly2004の4k部門でかなりはやった方法で
Windows標準の圧縮形式であるCAB圧縮を利用したものです。
exeをCAB形式で圧縮してバッチファイルにくみこむことで
バッチファイルを実行時にEXEを展開して実行。EXE終了後展開したファイルを消す
という方法です。この方法は64kでは使えませんが4kならたいていの大会で使える方法です。
てことで、今のデモのファイルサイズが1.68KBなんで4kならあと2.32KBつめることができます。
今回のコード量を考えたら案外4kってのも結構プログラムしなきゃいけないんだなぁと思いません?



●まとめ
これくらいをまとめておけば、あとでプログラムを使いまわすときにかなり有効です
あたらしくデモを作るときは設定を少し変えて、あとはdemoscene関数だけをかけば
すぐに作ることができます。
さて、これでデモの枠組みは出来上がりました。あとはガリガリdemoscene()関数のなかを
書いていけばデモの完成です!と、いいたいところですが、グラフィック部分はいいのですが
デモといったら音楽も忘れてはいけません。というわけで、次回のお題はソフトシンセです。


感想・質問・間違い指摘はBBSまで・・・