2022/09/09(金) [n年前の日記]
#1 [prog][windows] OpenGLを使ったWindows用スクリーンセーバをMinGWでコンパイル
Windows10 x64 21H2上で、OpenGLを使ったWindows用スクリーンセーバのサンプルを、MinGW を使ってコンパイル/ビルドしたい。
環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0) + freeglut 3.0.0-1。
MinGW-w64 (+ MSYS2) ではなくて、MinGW (+ MSYS) で作業。何度も何度も書くけれど、MinGW-w64 は libscrnsave.a (libscrnsavw.a) の中身が空っぽなのでスクリーンセーバをビルドできない。
参考にしたページは以下。OpenGL を使って、緑色の四角が円を描いて回るスクリーンセーバのソースが紹介されてる。リソースファイルやコンパイル手順までは紹介されてない。
_How to Scr: Writing an OpenGL Screensaver for Windows
_How to Scr: Sample Code for an OpenGL Screensaver
動作結果は以下。解像度が荒いけど雰囲気ぐらいは伝わるかと…。
ちなみに、このスクリーンセーバは OpenGL を使っているせいか、デスクトップキャプチャツール (OBS Studio 28.0.1) で数秒ほどキャプチャできなかった。デスクトップ画面が通常状態から OpenGL に切り替わる際にキャプチャできなくなるっぽい…。
環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0) + freeglut 3.0.0-1。
MinGW-w64 (+ MSYS2) ではなくて、MinGW (+ MSYS) で作業。何度も何度も書くけれど、MinGW-w64 は libscrnsave.a (libscrnsavw.a) の中身が空っぽなのでスクリーンセーバをビルドできない。
参考にしたページは以下。OpenGL を使って、緑色の四角が円を描いて回るスクリーンセーバのソースが紹介されてる。リソースファイルやコンパイル手順までは紹介されてない。
_How to Scr: Writing an OpenGL Screensaver for Windows
_How to Scr: Sample Code for an OpenGL Screensaver
動作結果は以下。解像度が荒いけど雰囲気ぐらいは伝わるかと…。
ちなみに、このスクリーンセーバは OpenGL を使っているせいか、デスクトップキャプチャツール (OBS Studio 28.0.1) で数秒ほどキャプチャできなかった。デスクトップ画面が通常状態から OpenGL に切り替わる際にキャプチャできなくなるっぽい…。
◎ MinGWにfreeglutを導入。 :
◎ ソース。 :
ソースファイル群とアイコンをzipにして置いておく。
_greensquare_src.zip
中身は以下。
一応、各ファイルも載せておきます。
リソースファイルは以下。ちなみに、元の参考ページではリソースファイルが掲載されてなかったので、Resource Hacker 5.1.8 を使って .scr からリソースを逆算(?)して書いてみた。
_resource.rc
リソースファイルのヘッダファイルは以下。
_resource.h
C++ソースファイルは以下。ちなみにオリジナル版はレジストリの読み書きにも対応してるのだけど、いきなりレジストリに書き込むのはちょっと怖いので、以下のソースではレジストリ関連処理( GetConfig()、WriteConfig() ) は if (0) { 〜 } でコメントアウト/無効化してある。
_greensquare.cpp
アイコンファイルは以下。中に icon.ico が入ってる。
_icon.cio.zip
_greensquare_src.zip
中身は以下。
- greensquare.cpp
- icon.ico
- Makefile
- resource.h
- resource.rc
一応、各ファイルも載せておきます。
リソースファイルは以下。ちなみに、元の参考ページではリソースファイルが掲載されてなかったので、Resource Hacker 5.1.8 を使って .scr からリソースを逆算(?)して書いてみた。
_resource.rc
/* resource.rc */ #include <windows.h> #include <scrnsave.h> #include "resource.h" #define IDC_STATIC_ICON 2300 #define IDC_STATIC_TEXT 2301 /* LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US */ /* Screensaver title */ STRINGTABLE BEGIN IDS_DESCRIPTION "Green Square Example Scr" END /* Icon */ ID_APP ICON "icon.ico" /* Dialog */ DLG_SCRNSAVECONFIGURE DIALOG DISCARDABLE 0, 0, 186, 82 STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "GreenSquare Setup" FONT 11, "Segoe UI" BEGIN DEFPUSHBUTTON "OK", IDOK, 39, 61, 50, 14 PUSHBUTTON "Cancel", IDCANCEL, 107, 61, 50, 14 AUTOCHECKBOX "Square tumbles as it revolves", IDC_TUMBLE, 25, 35, 137, 12 LTEXT "One option... that's all you get.", IDC_STATIC_TEXT, 17, 15, 150, 13 END※ この日記システムの問題で、FONT指定行の見た目がおかしくなってます。行頭の空白を削除してもらえればと。
リソースファイルのヘッダファイルは以下。
_resource.h
#ifndef _RESOURCE_H_ #define _RESOURCE_H_ #define IDC_TUMBLE 1000 #define IDC_STATIC_ICON 2300 #define IDC_STATIC_TEXT 2301 #endif
C++ソースファイルは以下。ちなみにオリジナル版はレジストリの読み書きにも対応してるのだけど、いきなりレジストリに書き込むのはちょっと怖いので、以下のソースではレジストリ関連処理( GetConfig()、WriteConfig() ) は if (0) { 〜 } でコメントアウト/無効化してある。
_greensquare.cpp
// greensquare.cpp is an open-source example screensaver by Rachel Grey, lemming@alum.mit.edu. // Paste into an IDE to compile if desired. // I haven't chosen to include the resource file, so you'd need to provide a description string /and so forth. #include <windows.h> #include <scrnsave.h> #include <GL/gl.h> #include <GL/glu.h> #include "resource.h" // get rid of these warnings: truncation from const double to float conversion from double to float // #pragma warning(disable: 4305 4244) //Define a Windows timer #define TIMER 1 #define FPS 60 // globals for size of screen static int Width, Height; static bool bTumble = true; // a global to keep track of the square's spinning static GLfloat spin = 0; ///////////////////////////////////////////////// //// INFRASTRUCTURE ENDS, SPECIFICS BEGIN /// //// /// //// In a more complex scr, I'd put all /// //// the following into other files. /// ///////////////////////////////////////////////// // Initialize OpenGL static void InitGL(HWND hWnd, HDC &hDC, HGLRC &hRC) { PIXELFORMATDESCRIPTOR pfd; ZeroMemory(&pfd, sizeof pfd); pfd.nSize = sizeof pfd; pfd.nVersion = 1; //pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL; //blaine's pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; pfd.iPixelType = PFD_TYPE_RGBA; pfd.cColorBits = 24; hDC = GetDC(hWnd); int i = ChoosePixelFormat(hDC, &pfd); SetPixelFormat(hDC, i, &pfd); hRC = wglCreateContext(hDC); wglMakeCurrent(hDC, hRC); } // Shut down OpenGL static void CloseGL(HWND hWnd, HDC hDC, HGLRC hRC) { wglMakeCurrent(NULL, NULL); wglDeleteContext(hRC); ReleaseDC(hWnd, hDC); } void SetupAnimation(int Width, int Height) { //window resizing stuff glViewport(0, 0, (GLsizei) Width, (GLsizei) Height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); if (0) { glOrtho(-300, 300, -240, 240, 25, 75); } else { float w = 300; float h = ((w * 2) * Height / Width) / 2; glOrtho(-w, w, -h, h, 25, 75); } glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(0.0, 0.0, 50.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); //camera xyz, the xyz to look at, and the up vector (+y is up) //background glClearColor(0.0, 0.0, 0.0, 0.0); //0.0s is black glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // glShadeModel(GL_FLAT); glShadeModel(GL_SMOOTH); } void OnTimer(HDC hDC) //increment and display { float xvals[] = {-30.0, -30.0, 30.0, 30.0}; float yvals[] = {-30.0, 30.0, 30.0, -30.0}; spin = spin + 1; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glPushMatrix(); glRotatef(spin, 0.0, 0.0, 1.0); glTranslatef(120, 0, 0); glRotatef(spin * ((bTumble) ? -4.0 : -1.0), 0.0, 0.0, 1.0); //draw the square (rotated to be a diamond) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glColor3f(0.1, 1.0, 0.3); // set green color glBegin(GL_POLYGON); for (int i = 0; i < 4; i++) glVertex2f(xvals[i], yvals[i]); glEnd(); glPopMatrix(); glFlush(); SwapBuffers(hDC); } void CleanupAnimation() { // didn't create any objects, so no need to clean them up } ///////// REGISTRY ACCESS FUNCTIONS /////////// void GetConfig() { if (0) { // get configuration from registry HKEY key; //DWORD lpdw; if (RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\GreenSquare", //lpctstr 0, //reserved KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) { DWORD dsize = sizeof(bTumble); DWORD dwtype = 0; RegQueryValueEx(key, "Tumble", NULL, &dwtype, (BYTE*)&bTumble, &dsize); //Finished with key RegCloseKey(key); } else //key isn't there yet--set defaults { bTumble = true; } } else { bTumble = true; } } void WriteConfig(HWND hDlg) { if (0) { HKEY key; DWORD lpdw; const char* null_str = ""; LPSTR str = const_cast<LPSTR>(null_str); if (RegCreateKeyEx(HKEY_CURRENT_USER, "Software\\GreenSquare", // lpctstr 0, // reserved str, // ptr to null-term string specifying the object type of this key REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &key, &lpdw) == ERROR_SUCCESS) { RegSetValueEx(key, "Tumble", 0, REG_DWORD, (BYTE*)&bTumble, sizeof(bTumble)); //Finished with keys RegCloseKey(key); } } } ////////////////////////////////////////////////// //// INFRASTRUCTURE -- THE THREE FUNCTIONS /// ////////////////////////////////////////////////// // Screen Saver Procedure LRESULT WINAPI ScreenSaverProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { static HDC hDC; static HGLRC hRC; static RECT rect; switch (message) { case WM_CREATE: // get window dimensions GetClientRect(hWnd, &rect); Width = rect.right; Height = rect.bottom; // get configuration from registry GetConfig(); // setup OpenGL, then animation InitGL(hWnd, hDC, hRC); SetupAnimation(Width, Height); // set timer to tick every 10 ms SetTimer(hWnd, TIMER, (int)(1000 / FPS), NULL); return 0; case WM_TIMER: OnTimer(hDC); // animate! return 0; case WM_DESTROY: KillTimer(hWnd, TIMER); CleanupAnimation(); CloseGL(hWnd, hDC, hRC); return 0; } return DefScreenSaverProc(hWnd, message, wParam, lParam); } BOOL WINAPI ScreenSaverConfigureDialog(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { //InitCommonControls(); //would need this for slider bars or other common controls HWND aCheck; switch (message) { case WM_INITDIALOG: LoadString(hMainInstance, IDS_DESCRIPTION, szAppName, 40); GetConfig(); aCheck = GetDlgItem(hDlg, IDC_TUMBLE); SendMessage(aCheck, BM_SETCHECK, (bTumble) ? BST_CHECKED : BST_UNCHECKED, 0); return TRUE; case WM_COMMAND: switch (LOWORD(wParam)) { case IDC_TUMBLE: bTumble = (IsDlgButtonChecked(hDlg, IDC_TUMBLE) == BST_CHECKED); return TRUE; //cases for other controls would go here case IDOK: WriteConfig(hDlg); // get info from controls EndDialog(hDlg, LOWORD(wParam) == IDOK); return TRUE; case IDCANCEL: EndDialog(hDlg, LOWORD(wParam) == IDOK); return TRUE; } return FALSE; } //end command switch return FALSE; } // needed for SCRNSAVE.LIB BOOL WINAPI RegisterDialogClasses(HANDLE hInst) { return TRUE; }
アイコンファイルは以下。中に icon.ico が入ってる。
_icon.cio.zip
◎ コンパイルの仕方。 :
Makefile は以下。
_Makefile
Makefile があれば、make と打つだけでコンパイル/ビルドできる。ちなみに、make clean で *.o と *.scr を削除できる。
一応説明しておくと…。
リンク時の指定については以下。
_Makefile
greensquare.scr: greensquare.o resource.o g++ greensquare.o resource.o -o greensquare.scr -static -lstdc++ -lgcc -lscrnsave -lopengl32 -lglu32 -lgdi32 -lcomctl32 -lwinmm -mwindows greensquare.o: greensquare.cpp resource.h g++ -c greensquare.cpp resource.o: resource.rc resource.h icon.ico windres resource.rc resource.o .PHONY: clean clean: rm -f *.scr rm -f *.o
Makefile があれば、make と打つだけでコンパイル/ビルドできる。ちなみに、make clean で *.o と *.scr を削除できる。
一応説明しておくと…。
- windres resource.rc resource.o で、リソースファイル resource.rc を .o に変換する。
- g++ -c greensquare.cpp で、.cpp をコンパイルして .o を生成。
- g++ greensquare.o resource.o -o greensquare.scr ... で、.oファイル群をリンクして .scr を生成。
リンク時の指定については以下。
- -static : スタティックリンクを指定。
- -lstdc++ -lgcc : C言語の標準的なライブラリをリンク。
- -lscrnsave : スクリーンセーバ用ライブラリをリンク。libscrnsave.a (libscrnsavw.a) をリンクする。
- -lopengl32 -lglu32 : OpenGLのライブラリをリンク。
- -lgdi32 : GDIライブラリをリンク。
- -lcomctl32 : よく分かってないけれど、これをつけないと「*.dll が見つからない」と言われて .scr が動作しなかった。
- -lwinmm : Windowsのタイマー関係ライブラリ、らしい。
- -mwindows : Windows用アプリケーション(GUIアプリ)であることを指定。
◎ 動作確認。 :
greensquare.scr を右クリックして動作確認。
MinGW で生成したので、この .scr は32bit版。それをWindows10 64bit上で動かしているので、C:\Windows\SysWOW64\ 以下に .scr をコピーして動作確認。スクリーンセーバとしても動いてくれた。
とりあえず、これで OpenGL を使ったスクリーンセーバを MinGW でコンパイル/ビルドできることは分かった。後は、OpenGL を使った描画処理部分を ―― OnTimer() のあたりを改造していけば、それらしいスクリーンセーバを作れそうな予感。
- 「Test」を選ぶと、フルスクリーン表示の動作確認ができる。
- 「構成」を選ぶと、スクリーンセーバ設定画面で「設定」をクリックした時の設定ダイアログが表示される。
MinGW で生成したので、この .scr は32bit版。それをWindows10 64bit上で動かしているので、C:\Windows\SysWOW64\ 以下に .scr をコピーして動作確認。スクリーンセーバとしても動いてくれた。
とりあえず、これで OpenGL を使ったスクリーンセーバを MinGW でコンパイル/ビルドできることは分かった。後は、OpenGL を使った描画処理部分を ―― OnTimer() のあたりを改造していけば、それらしいスクリーンセーバを作れそうな予感。
[ ツッコむ ]
#2 [python][windows] PyOpenGLが使うfreeglut.dllの場所を調べた
Windows10 x64 21H2 + Python 3.9.13 x64 + PyOpenGL 3.1.6 で実験をしていたのだけど、freeglut.dll がどこに置いてあるのかが気になったので少し調べてみた。
参考ページは以下。
_【Python】PyOpenGLのインストール方法を解説 | ジコログ
参考ページは以下。
_【Python】PyOpenGLのインストール方法を解説 | ジコログ
◎ 前提条件。 :
まず、自分の環境では、以下から .whl を入手してインストールしている。
_Archived: Python Extension Packages for Windows - Christoph Gohlke
インストールは以下。
この .whl には freeglut*.dll が同梱されているので、巷の解説ページで書かれているように、freeglut.dll を別途入手して C:\Windows\System32\ にコピーしなくても PyOpenGL が動いてくれる。
_Archived: Python Extension Packages for Windows - Christoph Gohlke
- PyOpenGL-3.1.6-cp39-cp39-win_amd64.whl
- PyOpenGL_accelerate-3.1.6-cp39-cp39-win_amd64.whl
インストールは以下。
pip install PyOpenGL-3.1.6-cp39-cp39-win_amd64.whl pip install PyOpenGL_accelerate-3.1.6-cp39-cp39-win_amd64.whl
この .whl には freeglut*.dll が同梱されているので、巷の解説ページで書かれているように、freeglut.dll を別途入手して C:\Windows\System32\ にコピーしなくても PyOpenGL が動いてくれる。
◎ freeglut.dllの在り処。 :
さておき。その freeglut.dll は、実際はどこにあるのか…。調べた感じでは、どうも以下のフォルダに .dll が入ってるように見えた。
以下の2つが入っていた。
ちなみに、Python 2.7.18 + PyOpenGL 3.1.0 上で確認してみたら、以下のファイルが入っていた。freeglut だけではなく GLUT も入っている。
これらの .dll は、以下のスクリプトから読み込まれている。
処理をしてる部分を抜き出してみる。
(Python3インストールフォルダ)\Lib\site-packages\OpenGL\DLLS\
以下の2つが入っていた。
- freeglut64.vc16.dll
- gle64.vc16.dll
ちなみに、Python 2.7.18 + PyOpenGL 3.1.0 上で確認してみたら、以下のファイルが入っていた。freeglut だけではなく GLUT も入っている。
- freeglut32.vc9.dll
- gle32.vc9.dll
- glut32.vc9.dll
これらの .dll は、以下のスクリプトから読み込まれている。
(Python3インストールフォルダ)\Lib\site-packages\OpenGL\platform\win32.py
処理をしてる部分を抜き出してみる。
def GLUT( self ): for possible in ('freeglut%s.%s'%(size,vc,), 'glut%s.%s'%(size,vc,)): # Prefer FreeGLUT if the user has installed it, fallback to the included # GLUT if it is installed try: return ctypesloader.loadLibrary( ctypes.windll, possible, mode = ctypes.RTLD_GLOBAL ) except WindowsError: pass return None「freeglut*.*.dll」もしくは「glut*.*.dll」が見つかったらそれらを使うようだなと…。
◎ freeglut.dllを差し替えてみる。 :
試しに、前述の処理部分を以下のように修正してみた。
これで、freeglut??.vc*.dll ではなく、freeglut.dll を読み込んでくれるのではないかと…。
以下から、freeglut 3.0.0 MSVC Package (freeglut-MSVC-3.0.0-2.mp.zip) を入手。
_freeglut Windows Development Libraries
解凍すると、bin/x64/freeglut.dll が入ってる。
以下のフォルダにコピー。
加えて、freeglut64.vc16.dll を freeglut64.vc16.dll.orig にリネームした。これで freeglut64.vc16.dll は呼び出されない状態になって、別途導入した freeglut.dll が呼び出されてる状態になるはず。
手元のサンプルスクリプトを動かしてみたところ、この状態でも動いてくれた。
しかし、上記の freeglut.dll はバージョンが 3.0.0 と古い。
MSYS2 + MinGW-w64 で入手できる freeglut.dll (libfreeglut.dll) は 3.2.2 なので、そちらも試してみた。
以下のファイルをコピーしてくる。
これを、freeglut.dll にリネームして、前述の場所にコピー。この状態でも簡単なサンプルなら動いてくれた。
ところで。手元の環境では、Python 3.9.13 のインストールフォルダ直下に freeglut.dll を置いても動いているわけで…。もし、その場所に置いても問題無く動くのであれば…。巷の解説ページは C:\Windows\System32\ 以下にコピーしてる事例がほとんどだけど、Pythonインストールフォルダに入れたほうが、まだ安全だったりしないのだろうか…。
for possible in ('freeglut%s.%s'%(size,vc,), 'glut%s.%s'%(size,vc,)): # ↓ for possible in ('freeglut', 'freeglut%s.%s' % (size,vc), 'glut%s.%s' % (size,vc)):
これで、freeglut??.vc*.dll ではなく、freeglut.dll を読み込んでくれるのではないかと…。
以下から、freeglut 3.0.0 MSVC Package (freeglut-MSVC-3.0.0-2.mp.zip) を入手。
_freeglut Windows Development Libraries
解凍すると、bin/x64/freeglut.dll が入ってる。
以下のフォルダにコピー。
(Python3インストールフォルダ)\Lib\site-packages\OpenGL\DLLS\ or (Python3インストールフォルダ)\
加えて、freeglut64.vc16.dll を freeglut64.vc16.dll.orig にリネームした。これで freeglut64.vc16.dll は呼び出されない状態になって、別途導入した freeglut.dll が呼び出されてる状態になるはず。
手元のサンプルスクリプトを動かしてみたところ、この状態でも動いてくれた。
しかし、上記の freeglut.dll はバージョンが 3.0.0 と古い。
MSYS2 + MinGW-w64 で入手できる freeglut.dll (libfreeglut.dll) は 3.2.2 なので、そちらも試してみた。
以下のファイルをコピーしてくる。
(MSYS2インストールフォルダ)\mingw64\bin\libfreeglut.dll
これを、freeglut.dll にリネームして、前述の場所にコピー。この状態でも簡単なサンプルなら動いてくれた。
ところで。手元の環境では、Python 3.9.13 のインストールフォルダ直下に freeglut.dll を置いても動いているわけで…。もし、その場所に置いても問題無く動くのであれば…。巷の解説ページは C:\Windows\System32\ 以下にコピーしてる事例がほとんどだけど、Pythonインストールフォルダに入れたほうが、まだ安全だったりしないのだろうか…。
◎ 余談。glutIdleFunc()が動かなくて悩んだ。 :
PyOpenGL関係のサンプルファイルを眺めていると、glutIdleFunc() を使っている事例を結構見かけるのだけど。手元の環境、Windows10 x64 21H2 + Python3.9.13 + PyOpenGL 3.1.6 上ではエラーが出てしまってかなり悩んだ。
やりたいことは一定の時間間隔で処理を呼び出したいだけなので、glutIdleFunc() の代わりに glutTimerFunc() を使えば目的は果たせるのだけど…。若干気になる状態なわけで。ググってみても仕様が変わった等の話を見かけないし。この症状が気になって freeglut.dll を色々差し替えていたわけで。結局動作は変わらなかったのだけど。
ふと、渡していた関数に引数があることに気づいた。glutTimerFunc() に渡す関数は引数を一つ用意するのだけど、その関数を安易に流用して、glutIdleFunc() にそのまま渡していたのが問題だった。何の引数も取らない関数を渡したらエラーが解消された。そういうオチか…トホ。
> py 03_teapot_draw_idle.py Traceback (most recent call last): File "C:\Python\Python39-64\lib\site-packages\OpenGL\GLUT\special.py", line 130, in safeCall return function( *args, **named ) TypeError: idle() missing 1 required positional argument: 'value' GLUT Idle callback <function idle at 0x000002A6438D8B80> with (),{} failed: returning None idle() missing 1 required positional argument: 'value'
やりたいことは一定の時間間隔で処理を呼び出したいだけなので、glutIdleFunc() の代わりに glutTimerFunc() を使えば目的は果たせるのだけど…。若干気になる状態なわけで。ググってみても仕様が変わった等の話を見かけないし。この症状が気になって freeglut.dll を色々差し替えていたわけで。結局動作は変わらなかったのだけど。
ふと、渡していた関数に引数があることに気づいた。glutTimerFunc() に渡す関数は引数を一つ用意するのだけど、その関数を安易に流用して、glutIdleFunc() にそのまま渡していたのが問題だった。何の引数も取らない関数を渡したらエラーが解消された。そういうオチか…トホ。
def on_timer(value): """timer callback.""" global yrot yrot += 0.5 glutPostRedisplay() # Redraw glutTimerFunc(int(1000 / FPS), on_timer, 0) def idle(): """idle callback.""" global yrot yrot += 0.5 glutPostRedisplay() # Redraw # ... if USE_IDLE: glutIdleFunc(idle) else: glutTimerFunc(int(1000 / FPS), on_timer, 0)
[ ツッコむ ]
以上、1 日分です。