mieki256's diary



2017/04/25(火) [n年前の日記]

#1 [python] pysdl2でSDL_Render云々を調べていたり

まだ pysdl2 を触っていたり。

_SDL2の使い方 - mirichiの日記 によると、「SDL_Renderer と SDL_Texture を使えば SDL2 は高速な描画ができる」という話らしいので、pysdl2 でそのあたりを使えるかどうか試したり。

sdl2を使った版。 :

ウインドウを表示して画像を円運動させるだけのスクリプトを書いてみたり。以下のソースが参考になった。ありがたや。

_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
texture_render_test_ss.png

一応、分かった点について、いくつかメモ。

ウインドウを生成したら、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
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
texture_render_test_ext_ss.png


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)
この2種類が用意されてるようで。名前からして、おそらく前者は遅いんじゃないかな、と…。

スプライトの描画は、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

一応簡単に解説すると…。
  • 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種類ぐらい紹介してた記事を見かけた記憶も…。

でも、今時は、こういうのって気にしなくて済むように、ライブラリ/ゲームエンジン側で実装済みだったりするんじゃないのか。その手の解説記事、残ってるのかな…。

とりあえずここまでかな。 :

画像描画とキー入力ができれば、あくまで見た目だけ、ではあるけれど、リアルタイムゲームっぽいソレを頑張れば作れると思うので…。とりあえず今回はここまで、かなと…。pygame より高速に描画できそうな雰囲気もあるし、これはこれで。

でも、やっぱり記述が面倒臭いなあ…。しかし、sdl2.ext は、ちょっとよく分からんし…。pysdl2 の上に被せるグッドなラッパーが出てくれば、Pythonでその手のゲームを書く行為も、ちょっとはアレになるのだろうか。

そういや、SDL2関連DLLの入手も面倒、だったのだよな…。例えば、pythonスクリプトを一つDLして実行するだけで、各ページからzipを落として展開してまとめてくれる、みたいなソレってPythonで書けないのだろうか。あるいは、関連DLLだけをまとめてインストールできる別パッケージを用意するとか…。いやまあ、そのへん、最初からpysdl2に同梱しといてよ、てな気持ちもあるけれど。

ちなみにDXRubyで書いたら。 :

ちなみに、Ruby + DXRuby で上記のスクリプトと同じ処理を書くと…。
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: もちろん、「短ければいいってもんでもないだろう」と言われそうでもあり…。一般的に、少ない記述量で書けるってことは、その分何かがトレードオフになってるはずで…。結局そのへんケースバイケース、なのだろうと思いますが。

以上です。

過去ログ表示

Prev - 2017/04 - Next
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