2017/04/25(火) [n年前の日記]
#1 [python] pysdl2でSDL_Render云々を調べていたり
まだ pysdl2 を触っていたり。
_SDL2の使い方 - mirichiの日記 によると、「SDL_Renderer と SDL_Texture を使えば SDL2 は高速な描画ができる」という話らしいので、pysdl2 でそのあたりを使えるかどうか試したり。
_SDL2の使い方 - mirichiの日記 によると、「SDL_Renderer と SDL_Texture を使えば SDL2 は高速な描画ができる」という話らしいので、pysdl2 でそのあたりを使えるかどうか試したり。
◎ sdl2を使った版。 :
ウインドウを表示して画像を円運動させるだけのスクリプトを書いてみたり。以下のソースが参考になった。ありがたや。
_Example of render to texture with SDL2
手元のソースは、こんな感じになった。
_texture_render_test.py
_res/hello.png
長い。ウンザリしてきた。
以下で実行。
一応、分かった点について、いくつかメモ。
ウインドウを生成したら、sdl2.SDL_CreateRenderer(ウインドウ, インデックス, フラグ) を使って、SDL_Renderer を得られる。
テクスチャクラス SDL_Texture は、サーフェイスから変換できる。sdl2.SDL_CreateTextureFromSurface(SDL_Renderer, サーフェイス) を使えばいい。
画面への描画の仕方は…。テクスチャを renderer に転送することで描画するようで。
_SDL_RenderCopy
転送元領域や転送先領域は、sdl2.SDL_Rect(x, y, w, h) で用意しておく。のかな。たぶん。
_Example of render to texture with SDL2
手元のソースは、こんな感じになった。
_texture_render_test.py
_res/hello.png
u""" PySDL2のテスト. SDL_Texture と SD_Renderer を使うと高速描画できるらしいので試す。 でも、本当に速くなっているのかな…? 動作確認環境: Windows10 x64 + Python 2.7.13 32bit版 + PySDL2 0.9.5 """ import os import sdl2 import sdl2.sdlimage import ctypes import math SCREEN_SIZE = (640, 480) # SCREEN_SIZE = (1280, 720) def main(): u"""メイン処理.""" # PySDL2 の初期化 sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) # ウインドウを作成して表示。 # タイトル文字列、表示位置、ウインドウサイズを指定している window = sdl2.SDL_CreateWindow(b"render test", sdl2.SDL_WINDOWPOS_CENTERED, sdl2.SDL_WINDOWPOS_CENTERED, SCREEN_SIZE[0], SCREEN_SIZE[1], sdl2.SDL_WINDOW_SHOWN) # ウインドウから renderer を取得。ウインドウ、インデックス、フラグを渡す # インデックスが-1なら初期化 # fg = 0 # fg = sdl2.SDL_RENDERER_PRESENTVSYNC | sdl2.SDL_RENDERER_ACCELERATED fg = sdl2.SDL_RENDERER_ACCELERATED renderer = sdl2.SDL_CreateRenderer(window, -1, fg) # 画像ファイルのパスを取得 filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "res", "hello.png") # 画像をサーフェイスとして読み込み # png等の読み込みには SDL2_image を利用する surface = sdl2.sdlimage.IMG_Load(filepath.encode("utf-8")) # 画像サイズを取得しておく w = surface.contents.w h = surface.contents.h print("w,h = %d,%d" % (w, h)) # サーフェイスをテクスチャに変換 # SDL_CreateTextureFromSurface() を使えば変換できる texture = sdl2.SDL_CreateTextureFromSurface(renderer, surface) # テクスチャに変換したからサーフェイスは用済み。解放してやる sdl2.SDL_FreeSurface(surface) ang = 0 interval = sdl2.SDL_GetPerformanceFrequency() / 60 oldtime = sdl2.SDL_GetPerformanceCounter() running = True while running: # メインループ # イベントを調べる # 閉じるボタンのクリック or ESCキーが押されたらメインループを終了 event = sdl2.SDL_Event() while sdl2.SDL_PollEvent(ctypes.byref(event)) != 0: if event.type == sdl2.SDL_QUIT: # 閉じるボタンがクリックされた running = False break if event.type == sdl2.SDL_KEYDOWN: # 何かのキーが押し下げられた if event.key.keysym.sym == sdl2.SDLK_ESCAPE: # ESCキーが押された running = False break # テクスチャの表示位置を算出。円運動をさせている rad = math.radians(ang) r = SCREEN_SIZE[0] / 6 x = int(r * math.cos(rad) + (SCREEN_SIZE[0] / 2) - (w / 2)) y = int(r * math.sin(rad) + (SCREEN_SIZE[1] / 2) - (h / 2)) ang = (ang + 3) % 360 # 画面をクリア sdl2.SDL_RenderClear(renderer) # テクスチャの転送元領域を設定 src_rect = sdl2.SDL_Rect(0, 0, w, h) # テクスチャの転送先領域を設定 dst_rect = sdl2.SDL_Rect(x, y, w, h) # テクスチャをウインドウの renderer に転送 # 転送元領域に NULL を指定すれば、全体を指定できる、はずなのだが… # sdl2.SDL_RenderCopy(renderer, texture, None, dst_rect) sdl2.SDL_RenderCopy(renderer, texture, src_rect, dst_rect) # ウインドウ(renderer)の内容を更新 sdl2.SDL_RenderPresent(renderer) # ループの時間待ち while True: if sdl2.SDL_GetPerformanceCounter() - oldtime >= interval: break sdl2.SDL_Delay(0) oldtime = sdl2.SDL_GetPerformanceCounter() # メインループ終了 # テクスチャ、renderer、ウインドウの解放 sdl2.SDL_DestroyTexture(texture) sdl2.SDL_DestroyRenderer(renderer) sdl2.SDL_DestroyWindow(window) # sdl2を終了 sdl2.SDL_Quit() if __name__ == '__main__': main()
長い。ウンザリしてきた。
以下で実行。
python texture_render_test.py
一応、分かった点について、いくつかメモ。
ウインドウを生成したら、sdl2.SDL_CreateRenderer(ウインドウ, インデックス, フラグ) を使って、SDL_Renderer を得られる。
renderer = sdl2.SDL_CreateRenderer(window, -1, fg)フラグには、sdl2.SDL_RENDERER_ACCELERATED を指定しておけば、ひとまずハードウェアアクセラレーションが適用、されるのかな。たぶん。 _SDL_RendererFlags を眺めると、vsyncと同期するフラグもあるようで。
テクスチャクラス SDL_Texture は、サーフェイスから変換できる。sdl2.SDL_CreateTextureFromSurface(SDL_Renderer, サーフェイス) を使えばいい。
texture = sdl2.SDL_CreateTextureFromSurface(renderer, surface)
画面への描画の仕方は…。テクスチャを renderer に転送することで描画するようで。
sdl2.SDL_RenderCopy(転送先のSDL_renderer, 転送元テクスチャ, 転送元領域, 転送先領域)みたいな感じ。
_SDL_RenderCopy
転送元領域や転送先領域は、sdl2.SDL_Rect(x, y, w, h) で用意しておく。のかな。たぶん。
◎ sdl2.extを使った版。 :
sdl2を使った版は、やっぱり色々と記述が面倒臭いので、sdl2.ext を使って少しは行数が短くなるように書いてみたり。
_texture_render_test_ext.py
相変わらず長い。いやまあ、一応ちょっとは短くなっているのだけど。
以下で実行。
sdl2.ext版は、やっぱりスプライトを使わないと短く書けないっぽい。sdl2.ext.SpriteFactory() を使うことで、スプライトの生成ができるっぽいけど…。
スプライトの描画は、create_sprite_render_system() を呼んで、ソレ用の何かを生成して…。
ホントにコレで合ってるのか全く自信は無いけれど、一応はコレで動いてるっぽいし…合ってるんじゃないかなあ…怪しいけど。
_texture_render_test_ext.py
u""" PySDL2のテスト. SDL_Texture と SD_Renderer を使うと高速描画できるらしいので試す。 sdl2.ext を利用して、少しは行数が減るように書いてみる 動作確認環境: Windows10 x64 + Python 2.7.13 32bit版 + PySDL2 0.9.5 """ import sdl2 import sdl2.ext import math SCREEN_SIZE = (640, 480) # SCREEN_SIZE = (1280, 720) # リソース(画像等)の場所を指定 RESOURCES = sdl2.ext.Resources(__file__, "res") def main(): u"""メイン処理.""" # PySDL2 の初期化 sdl2.ext.init() # ウインドウを作成して表示。 window = sdl2.ext.Window("Hello World", size=SCREEN_SIZE) window.show() # ウインドウから renderer を取得。ウインドウ、インデックス、フラグを渡す。 # インデックスとフラグは省略可能。インデックスのデフォルト値は index=-1 # フラグのデフォルト値は flags=sdl2.SDL_RENDERER_ACCELERATED # fg = sdl2.SDL_RENDERER_PRESENTVSYNC | sdl2.SDL_RENDERER_ACCELERATED fg = sdl2.SDL_RENDERER_ACCELERATED renderer = sdl2.ext.Renderer(window, flags=fg) # テクスチャベースのスプライト、を作成できるクラス?、を生成 factory = sdl2.ext.SpriteFactory(sdl2.ext.TEXTURE, renderer=renderer) # スプライトを1つ生成。画像の読み込みも行う sprite = factory.from_image(RESOURCES.get_path("hello.png")) # スプライト描画用のrendererを生成 spriterenderer = factory.create_sprite_render_system(renderer) # テクスチャサイズを取得しておく w, h = sprite.size print("w,h = %d,%d" % (w, h)) ang = 0 interval = sdl2.SDL_GetPerformanceFrequency() / 60 oldtime = sdl2.SDL_GetPerformanceCounter() running = True while running: # メインループ # イベントを調べる # 閉じるボタンのクリック or ESCキーが押されたらメインループを終了 events = sdl2.ext.get_events() for event in events: if event.type == sdl2.SDL_QUIT: # 閉じるボタンがクリックされた running = False break if event.type == sdl2.SDL_KEYDOWN: # 何かのキーが押し下げられた if event.key.keysym.sym == sdl2.SDLK_ESCAPE: # ESCキーが押された running = False break # テクスチャの表示位置を算出。円運動をさせている rad = math.radians(ang) r = SCREEN_SIZE[0] / 6 x = int(r * math.cos(rad) + (SCREEN_SIZE[0] / 2) - (w / 2)) y = int(r * math.sin(rad) + (SCREEN_SIZE[1] / 2) - (h / 2)) ang = (ang + 3) % 360 # スプライトの表示位置を変更 sprite.position = x, y # 画面をクリア renderer.clear() # スプライトを描画 spriterenderer.render(sprite) # ループの時間待ち while True: if sdl2.SDL_GetPerformanceCounter() - oldtime >= interval: break sdl2.SDL_Delay(0) oldtime = sdl2.SDL_GetPerformanceCounter() # メインループ終了。色々なアレやソレを解放 sdl2.ext.quit() if __name__ == '__main__': main()
相変わらず長い。いやまあ、一応ちょっとは短くなっているのだけど。
以下で実行。
python texture_render_test_ext.py
sdl2.ext版は、やっぱりスプライトを使わないと短く書けないっぽい。sdl2.ext.SpriteFactory() を使うことで、スプライトの生成ができるっぽいけど…。
# テクスチャベースのスプライト、を作成できるクラス?、を生成 factory = sdl2.ext.SpriteFactory(sdl2.ext.TEXTURE, renderer=renderer) # スプライトを1つ生成。画像の読み込みも行う sprite = factory.from_image(RESOURCES.get_path("hello.png"))sdl2.ext は、スプライトの種類として、
- ソフトウェアベースのスプライト(sdl2.ext.SOFTWARE)
- テクスチャベースのスプライト(sdl2.ext.TEXTURE)
スプライトの描画は、create_sprite_render_system() を呼んで、ソレ用の何かを生成して…。
# スプライト描画用のrendererを生成 spriterenderer = factory.create_sprite_render_system(renderer)後で、.render(スプライト) を呼べばいいのかな。たぶん。
# スプライトを描画 spriterenderer.render(sprite)
ホントにコレで合ってるのか全く自信は無いけれど、一応はコレで動いてるっぽいし…合ってるんじゃないかなあ…怪しいけど。
◎ メインループの時間待ち処理。 :
メインループが60FPSで回るようにしたかったのだけど、最初は
_SDL_Delay()
だの
_SDL_GetTicks()
だのを使っていて。
_SDL:フレームレートの制御 | ジャジャガッチブログ
ただ、どうも動きがガクガクする…。
他の方法はないのかなとググっていたら、以下の記事が。
_Windows8(以降?)でSDL_delayの挙動が変わったのかしら?と言う話 - 今日の雑記
_c++ - Sleep(1) and SDL_Delay(1) takes 15 ms - Stack Overflow
Windows7 と Windows8以降では、動作が変わるとか…。そりゃ困る…。
ということで、件の記事で紹介されてる方法でループの時間待ちを書くことに。
_SDL_GetPerformanceFrequency
_SDL_GetPerformanceCounter
一応簡単に解説すると…。
_SDL:フレームレートの制御 | ジャジャガッチブログ
ただ、どうも動きがガクガクする…。
他の方法はないのかなとググっていたら、以下の記事が。
_Windows8(以降?)でSDL_delayの挙動が変わったのかしら?と言う話 - 今日の雑記
_c++ - Sleep(1) and SDL_Delay(1) takes 15 ms - Stack Overflow
Windows7 と Windows8以降では、動作が変わるとか…。そりゃ困る…。
ということで、件の記事で紹介されてる方法でループの時間待ちを書くことに。
_SDL_GetPerformanceFrequency
_SDL_GetPerformanceCounter
一応簡単に解説すると…。
- sdl2.SDL_GetPerformanceCounter() を呼べば、現在の高分解能カウンタ値を得られる。
- sdl2.SDL_GetPerformanceFrequency() は、1秒間に何回、高分解能カウンタでカウントできるかを返す。ということは、この値を60で割れば、1/60秒の間にカウントできるはずの数が得られる。(= インターバル値)
- メインループ処理を始める前に、カウンタ値を覚えておいて…。
- メインループ処理の最後のあたりで、覚えておいたカウンタ値と、現在のカウンタ値を使って、カウント数の差 = メインループの処理に何カウントかかったのかを取得する。
- もし、差が、インターバル値に達してないなら、まだ処理時間は余ってるということ。時間待ちをしないといけない。
- 差が、インターバル値に達するまで延々とループさせる。一応、ループの中で、SDL_Delay(0) を呼ぶことで、ほんの一瞬ちょっとだけ、なんとなく気持ち的にスリープさせておく。
- 差が、インターバル値に達したら、1/60秒経過したということ。現在カウンタ値を記録して、メインループをまた実行。
◎ vsyncの使い方がよく分からない。 :
SDL2 は vsync を利用できるという話を見かけたので、「vsync で合わせれば確実に60FPSで回ってくれるんじゃないのかなあ」などと思ってしまったのだけど。以下の記事を目にして考えが甘かったことに今頃気づいたり。
_python - Locking the frame rate in pygame? - Game Development Stack Exchange
「PCによってディスプレイのリフレッシュレートって違うだろ」「あんなもんを頼りにしてたら何FPSになるか予測つかんぞ」という、あまりにも当たり前な指摘。ああ、たしかに、そうだった…。PCって、ゲーム機と違って、ディスプレイのリフレッシュレートがバラバラだった…。すっかり忘れてた。
となると、vsync って何に使えるんだろ。いや、画面の書き換えが気にならないように、みたいな使い方はできるだろうけど。メインループは60FPSだけどディスプレイは50FPSとかだったら、スクロールがガクガクしたりせんのかな。どうやって辻褄を…。って今頃になってこんなので悩むとか何十年頭の中が止まってるんだよって感じですな。
さすがにこのあたり、誰か対処方法をまとめてないのかしら。昔、アプローチを3種類ぐらい紹介してた記事を見かけた記憶も…。
でも、今時は、こういうのって気にしなくて済むように、ライブラリ/ゲームエンジン側で実装済みだったりするんじゃないのか。その手の解説記事、残ってるのかな…。
_python - Locking the frame rate in pygame? - Game Development Stack Exchange
「PCによってディスプレイのリフレッシュレートって違うだろ」「あんなもんを頼りにしてたら何FPSになるか予測つかんぞ」という、あまりにも当たり前な指摘。ああ、たしかに、そうだった…。PCって、ゲーム機と違って、ディスプレイのリフレッシュレートがバラバラだった…。すっかり忘れてた。
となると、vsync って何に使えるんだろ。いや、画面の書き換えが気にならないように、みたいな使い方はできるだろうけど。メインループは60FPSだけどディスプレイは50FPSとかだったら、スクロールがガクガクしたりせんのかな。どうやって辻褄を…。って今頃になってこんなので悩むとか何十年頭の中が止まってるんだよって感じですな。
さすがにこのあたり、誰か対処方法をまとめてないのかしら。昔、アプローチを3種類ぐらい紹介してた記事を見かけた記憶も…。
でも、今時は、こういうのって気にしなくて済むように、ライブラリ/ゲームエンジン側で実装済みだったりするんじゃないのか。その手の解説記事、残ってるのかな…。
◎ とりあえずここまでかな。 :
画像描画とキー入力ができれば、あくまで見た目だけ、ではあるけれど、リアルタイムゲームっぽいソレを頑張れば作れると思うので…。とりあえず今回はここまで、かなと…。pygame より高速に描画できそうな雰囲気もあるし、これはこれで。
でも、やっぱり記述が面倒臭いなあ…。しかし、sdl2.ext は、ちょっとよく分からんし…。pysdl2 の上に被せるグッドなラッパーが出てくれば、Pythonでその手のゲームを書く行為も、ちょっとはアレになるのだろうか。
そういや、SDL2関連DLLの入手も面倒、だったのだよな…。例えば、pythonスクリプトを一つDLして実行するだけで、各ページからzipを落として展開してまとめてくれる、みたいなソレってPythonで書けないのだろうか。あるいは、関連DLLだけをまとめてインストールできる別パッケージを用意するとか…。いやまあ、そのへん、最初からpysdl2に同梱しといてよ、てな気持ちもあるけれど。
でも、やっぱり記述が面倒臭いなあ…。しかし、sdl2.ext は、ちょっとよく分からんし…。pysdl2 の上に被せるグッドなラッパーが出てくれば、Pythonでその手のゲームを書く行為も、ちょっとはアレになるのだろうか。
そういや、SDL2関連DLLの入手も面倒、だったのだよな…。例えば、pythonスクリプトを一つDLして実行するだけで、各ページからzipを落として展開してまとめてくれる、みたいなソレってPythonで書けないのだろうか。あるいは、関連DLLだけをまとめてインストールできる別パッケージを用意するとか…。いやまあ、そのへん、最初からpysdl2に同梱しといてよ、てな気持ちもあるけれど。
◎ ちなみにDXRubyで書いたら。 :
ちなみに、Ruby + DXRuby で上記のスクリプトと同じ処理を書くと…。
他の言語やライブラリで書いたら、どうなるんだろう…。Python + pyglet、Ruby + gosu、HSP、Processing、enchant.js あたりで…。このくらいなら Processing も短く書けそうですな。 *1
require 'dxruby' # 画像を読み込み img = Image.load("res/hello.png") ang = 0 # メインループ Window.loop do # ESCキーが押されたら終了 break if Input.keyPush?(K_ESCAPE) # 円運動をさせる rad = ang * Math::PI / 180.0 r = Window.width / 6 x = r * Math.cos(rad) + (Window.width / 2) - (img.width / 2) y = r * Math.sin(rad) + (Window.height / 2) - (img.height / 2) ang = (ang + 3) % 360 # ウインドウに画像を描画 Window.draw(x, y, img) end
他の言語やライブラリで書いたら、どうなるんだろう…。Python + pyglet、Ruby + gosu、HSP、Processing、enchant.js あたりで…。このくらいなら Processing も短く書けそうですな。 *1
*1: もちろん、「短ければいいってもんでもないだろう」と言われそうでもあり…。一般的に、少ない記述量で書けるってことは、その分何かがトレードオフになってるはずで…。結局そのへんケースバイケース、なのだろうと思いますが。
[ ツッコむ ]
以上です。