mieki256's diary



2022/09/19(月) [n年前の日記]

#1 [prog][windows] OpenGLを使ったWindows用スクリーンセーバをC++で書いてみた

_昨日、 C言語 + OpenGL を使ってボールが箱の中を跳ね回るプログラムを書いたけれど、せっかくだからWindows用スクリーンセーバにしてみた。

使用言語が、C言語じゃなくて C++ になってるけれど、たぶんソースはほとんどC言語の書き方になってるような気がする…。

環境は、Windows10 x64 21H2 + MinGW (g++ 9.2.0) + scrnsave.h + libscrnsave.a。

動作確認に使ったハードウェア環境は以下の2つ。どちらでも動いてくれた。
動作画面は以下。YouTubeにアップロードしてみた。

ソースその他。 :

ソースその他は、GitHub にアップロードしておいた。

_mieki256/glboundballscr: Bound ball animation screensaver win32 by using OpenGL.

Makefile もつけてあるので、MinGW が利用できる環境で make と打てばコンパイルできるのではないかと…。

実際に処理をしているソースは以下。

_glboundballscr/glboundball.cpp at main - mieki256/glboundballscr

インストール。 :

MinGW は32bit版なので、出来上がるスクリーンセーバ(.scr)も32bit版になる。
  • Windowsが64bit版、スクリーンセーバが32bit版なら、C:\Windows\SysWOW64\ 以下にコピー。
  • Windowsが64bit版、スクリーンセーバが64bit版なら、C:\Windows\System32\ 以下にコピー。
  • Windowsが32bit版なら、C:\Windows\System32\ 以下にコピー。

問題点。 :

一応スクリーンセーバにしてみたものの、何故かフレームレート(FPS、フレーム/秒)が低くなる問題に遭遇して結構ハマった。

_先日書いたGLUT(freeglut)使用版 は、ちゃんと60FPS出てるのだけど。スクリーンセーバ版を動かしたところ、どうも見た感じ、40FPS前後しか出てなくて…。

試しに目標FPS値を少しずつ上げていったところ、63FPSにして動作させた時、描画は60FPSよりちょっと低い程度になったので、その値のままにしてある。何故そうなるのかは分からないけど…。もし、動作がおかしい場合は、glboundball.cpp 内の #define FPS 63 が目標FPS値の指定なので、変更してコンパイル/動作確認してもらえればと…。

ちなみに、#define FPS_BAR 0 を 1 にしてビルドすれば、箱の一片の近くに赤い線が引かれて、FPSが大まかに分かるようになってる。
  • 箱の一辺の長さが目標FPSで、赤い線の長さが実際のFPS。
  • 例えば、赤い線の長さが、箱の一辺の0.95程度の長さなら、63 * 0.95 ≒ 60FPS が実際のFPS、ということになる。

原因を想像。 :

これは想像なのだけど、スクリーンセーバに一定の時間間隔で処理をさせるための、SetTimer() という関数の精度がよろしくないのかもしれないなと。

SetTimer() は、「xxミリ秒(msec)の間隔で WM_TIMERメッセージを送ってくれ」とOSに指示を出す関数。OSは、SetTimer() で指定されたミリ秒間隔でWM_TIMERメッセージを送ってくるので…。スクリーンセーバのプログラムは、WM_TIMERメッセージが届いたら、そのタイミングで描画処理をする作りにしておく。これで、一定の時間間隔で画面描画が行われるので、アニメーションが実現できる。

ただ、Windows NT系は、SetTimer() の精度が10ミリ秒程度だよ、という話もどこかで見かけたわけで…。 *1

60FPSで動かそうとすると、SetTimer() には 1000 / 60 ≒ 16ミリ秒(16msec)を指定することになるけれど。SetTimer() の精度が本当に10ミリ秒しかないのであれば、キッチリ16ミリ秒間隔でメッセージが送られてこなくて、20ミリ秒の間隔でメッセージが送られてきそうな気がする。それでは目標とするFPSが出ないよなと。

しかし、50FPS (1000 / 50 = 20ミリ秒)や、25FPS (1000 / 25 = 40ミリ秒) を指定しても、やっぱりフレームレートが低い…。目標の2/3ぐらいのFPSになってしまう。本当に10ミリ秒の精度なんだろうかという疑問が…。

63FPS なら、1000 / 63 ≒ 15ミリ秒になるなと…。ひょっとして、最近のWindowsは、15ミリ秒とか、あるいは5ミリ秒の精度だったりするんだろうか。

ただ、そもそも、「スクリーンセーバはバックグラウンドで動いてるプログラム群の邪魔をしない作りにするべし」という話もどこかで見かけたので…。60FPSでぬるぬる動かしたいと考えること自体がアウトなのかも。例えば10FPSぐらいでカクカクと動くのが正しいスクリーンセーバの姿、かもしれないよな、とも…。

何にせよ、これで OpenGL を使ったスクリーンセーバの作り方が少し分かってきたので、3D描画をするスクリーンセーバもC/C++で書けそうな気がしてきたなと…。

タイマーの精度について。 :

その後もググってたら、Windows7 はタイマー精度が15.6ミリ秒、という話を見かけた。

_ windows - MFCでの標準タイマーの精度をあげる方法ありますか? - スタック・オーバーフロー
Windows OSのシステムクロックは15.6ミリ秒の分解能しかもたないため(Windows 7)、記載のような事象が発生します。

_WindowsにおけるSleep(1)の精度について
_Rustのゲーム用ライブラリpistonがWindowsの上だと60fps出なかったのを直した - 名有りさんの日記
_タイマの精度 - お仕事メモ帳
この動作の原因は、Windows XP のハードウェア アブストラクションレイヤ モジュール:HAL.DLL の仕様の違いによるものであり、そのPCが使用しているHALの種類によってタイマ精度が決まります。

タイマ精度 約 10mS となるマシンは halacpi.dll が使用されており、タイマ精度 16mS となるマシンには halaacpi.dll が使用されております。

最近の機種では、割り込みコントローラに APIC (Advanced PIC) を 使用しておりまして、この場合、HAL は halaacpi.dll が使用されます。

タイマの精度 - お仕事メモ帳 より


Windows95系、NT系で違うだけ、ではないのだな…。使ってる割り込みコントローラの種類で、10ms と 15.6ms の違いが出てくると…。いやまあ、最近の機種は 15.6ms になってそうだけど。

SetTimer() に 15msec を指定するのと、16msec を指定するのでは、動作に差が出てきそう。16msec を指定すると、実際には次回のメッセージが送られてくるのが 15.6 x 2 = 31.2 msec 後になるのかも。とすると、FPSは 31〜33FPSぐらいになるのだろうか。60FPSを期待したのに40FPS前後しか出ないなーと困っていたけれど、実際は31〜33FPSだった可能性が高そう。

*1: ちなみに、Windows95系はもっと精度が荒くて、55ミリ秒程度らしい。

#2 [zatta] 日記をアップロード

2022/08/19を最後に日記をアップロードしてなかったのでアップロード。

2022/09/18() [n年前の日記]

#1 [python] PyOpenGLでボールが跳ね回るスクリプトを作成

PyOpenGL を使って、ボールが箱の中で跳ね回るスクリプトを書いてみた。

動作確認環境は以下。
動かすとこんな感じ。




以下のキーが有効。 FPS表示時の赤い線は、期待した60FPSに対して実際何FPS出ているかを、箱の一辺の長さとの比較で表示している。60FPSが出ていれば、赤い線は箱の一辺と同じ長さになるし、それより少ないFPSなら、箱の一辺より短く表示される。

ソース。 :

ソースは GitHub Gist にアップロードしてみた。このページにも埋め込めるだろうか…。どうかな…。

_01_bound_ball.py (Gist)


以降、ソースの中身について、少しだけ解説。

FPSの計測について。 :

FPSの計測は、1秒間に何回画面を書き換えたかをカウントする方法で調べた。init_count_fps()、calc_fps() を見れば、どうやっているか分かるかなと…。

時間の取得には、time.time() を使った。import time をすれば使えるようになる。time.time() を呼ぶと、その時点の時刻が、秒単位(小数点以下も含む状態)で得られるらしい。

Python 2.7.18 32bitの場合、time.time() の返り値は小数点以下3桁まで含んでいる。ミリ秒まで分かるのだろう…。
>>> import time
>>> time.time()
1663505711.946
>>> time.time()
1663505714.521

Python 3.9.13 64bitの場合、time.time() の返り値は、小数点以下7桁まで得られる模様。もしかすると、マイクロ秒まで得られる…?
>>> import time
>>> time.time()
1663505644.9368925
>>> time.time()
1663505648.3456943

まあ、Python 2.7 の場合を考えて、time.time() は少なくともミリ秒まで分かると思って使えばいいのかなと。

ちなみに、最初は時間の取得に time.perf_counter() を使っていたのだけど。time.perf_counter() は Python 3.3 で実装されたメソッドだから Python 2.7 では動かなくて、time.time() で置き換えることになった。

文字描画について。 :

OpenGL で画面に文字を描画するあたりは、GLUT (freeglut) の glutBitmapCharacter() を利用した。GLUT (freeglut) を利用できる状況じゃないと使えないけれど…。

_情報メディア実験
_glutBitmapCharacter : PyOpenGL 3.1.0 GLUT Man Pages
_~mcfletch/openglcontext/trunk : contents of tests/glutbitmapcharacter.py at revision 699
_python - pyopengl How to render text - Stack Overflow

文字を描画する際の位置決めは、最初、glRasterPos3f(x, y, z) を使ってみたのだけど、glWindowPos2f(x, y) というメソッドもあると知って、そちらを使うようにしてみた。

_glRasterPos : PyOpenGL 3.1.0 GL Man Pages
_glWindowPos : PyOpenGL 3.1.0 GL Man Pages

この glWindowPos2f() を使えば、3D空間内の位置ではなく、ウインドウ上の2D的な感覚で座標指定ができる。

ただし、注意点が2つほどある。
  • 画面左下が (0, 0) になっている。
  • OpenGL 1.4 から追加されたメソッドなので、OpenGL 1.1 等では使えない。
例えば、Windows に標準で入ってる OpenGL は 1.1 対応だそうで、そのせいか、C/C++ で書いたプログラム等で glWindowPos2f() を使おうとすると「そんなAPIは無い」と言われてしまう。

もっとも、今時のPCなら、ビデオドライバはもっと後の OpenGLバージョンにも対応してるだろうから…。もしかすると、PyOpenGLを使う時はそのあたりを上手い具合に変更してくれて、それで今回 glWindowPos2f() が使えているのかもしれない。たぶん。分からんけど。

アニメーション処理について。 :

アニメーションをするためには一定の時間間隔で処理を呼ばないといけないけれど、そのあたりは glutTimerFunc() で指定している。

_glutTimerFunc : PyOpenGL 3.1.0 GLUT Man Pages
_GLUT のイベント

巷の解説ページでは glutIdleFunc() を使う場合が多いのだけど…。glutIdleFunc() に渡す関数は引数を取らないけれど、glutTimerFunc() に渡す関数は引数を1つ取るので、その点はちょっとだけ注意。

#2 [prog] C言語とOpenGLでボールが跳ね回るプログラムを作成

_PyOpenGLを使ってボールが箱の中で跳ね回るスクリプトを書いた わけだけど、これをC言語 + OpenGL で動くように移植してみた。

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0) + freeglut 3.0.0。MSYS2 + MinGW-w64 ではなくて、MinGW + MSYS でコンパイルしている。

MinGW用の freeglut 3.0.0 バイナリ (freeglut-MinGW-3.0.0-1.mp.zip) は、以下から入手させてもらった。ありがたや。

_freeglut Windows Development Libraries

動作画面は PyOpenGL版と大体同じ。

以下のキーを受け付ける。

ソース。 :

コンパイル。 :

Makefile があるので、make と打てばコンパイルできるだろうけど…。一応、コンパイルは以下。01_gl_bound_ball.exe が生成される。
gcc 01_gl_bound_ball.c -o 01_gl_bound_ball.exe -static -D FREEGLUT_STATIC -lfreeglut_static -lopengl32 -lglu32 -lwinmm -lgdi32 -mwindows

01_gl_bound_ball.exe を実行すれば、PyOpenGL版と同じ見た目のウインドウが開くかと。

覚書。 :

FPS表示時に、一応「FPS: xx」の文字描画もしてみたけれど、glWindowPos2f() が使えなかったので、 *1 glRasterPos3f() を使うことになった。そのせいで、箱の上のほうに「FPS: xx」がくっついて動いてる表示になってしまった。ちょっとダサイ。ウインドウの特定の位置に固定して表示したかったのだけど、このあたり本来はどうやって解決すればいいのやら。

一般的に、C言語で円周率を使いたい時は、math.h をインクルードして(#include <math.h>)、M_PI を利用するらしいのだけど。試してみたら Visual Studio Code 上で「このシンボルは見当たらないよ」と言われてしまった。math.h を include する前に、「#define _USE_MATH_DEFINES」を書いておけば、その手のマクロが使えるようになるらしい。ただ、移植性は落ちるそうで…。

_定数M_PIが使えない - 需要のないページ
_プログラミングメモ日記 C++での数学定数の利用について
_M_PI ‐ 通信用語の基礎知識
_C言語(標準)にM_PIは無い - 簡潔なQ
_math.h の M_PI などは仕様外だった - GUST NOTCH? DIARY

*1: glWindowPos2f() は OpenGL 1.4 から使える。OpenGL 1.1 では使えない。

2022/09/17() [n年前の日記]

#1 [prog] OpenGL勉強中

PyOpenGLを使って、ボールが箱の中で跳ね回るスクリプトを書いているところ。それっぽい感じの見た目になってきたので、せっかくだから OpenGLを使っているWindows用スクリーンセーバとして移植を試みているのだけど。ちょっと問題が…。

PyOpenGL版と比べると、スクリーンセーバ版は、見た感じ明らかにFPSが低い。なんでだろ。

環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0)。

MinGW (gcc 9.2.0) を使うと遅くなるのだろうか。であればと、スクリーンセーバではない形で書いて動作確認。GLUT (freeglut 3.0.0) でフツーにウインドウを表示してから、同じ処理で描画。しかしコレだと PyOpenGL版と似た感じのフレームレートに見える…。ということは、スクリーンセーバ用に書くとフレームレートがおかしくなるということで…。

もしかすると、GLUT (freeglut) 使用版と、スクリーンセーバ版では、初期化時に行っている処理内容が違っていて、OpenGL のハードウェアアクセラレーションが効いていないのだろうか。スクリーンセーバ用は PIXELFORMATDESCRIPTOR を使って初期化するけれど、そこで何か指定を間違えているとか…?

それとも、OpenGL利用部分は正常に動作していて、スクリーンセーバとして動かすためのどこかしらでフレームレートが落ちているのだろうか…。

以上、3 日分です。

過去ログ表示

Prev - 2022/09 -
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project