2022/09/21(水) [n年前の日記]
#1 [python] PyOpenGLでビルボードが実現できそうか実験中その2
Python + PyOpenGL (OpenGL) を使って、ビルボードが実現できそうか実験しているところ。環境は、Windows10 x64 21H2 + Python 3.9.13 64bit + PyOpenGL 3.1.6。
この場合のビルボードというのは、立て看板みたいなもので、絶えずカメラのほうを向いているポリゴンというか…。それっぽいテクスチャをビルボードに貼っておくことで、一見するとそこに細かい何かが置いてあるように錯覚させることができるという…。まあ、ハードウェアスペックが低かった時代に多用されていた、頓智というか、インチキというか、そういうアレですけど…。以下の解説記事内のスクリーンショットが分かりやすい気がします。
_【Unity】ビルボードで常にカメラの方に向く木を作る - おもちゃラボ
昔のTVゲーム、「スペースハリアー」「アウトラン」「アフターバーナー」等もビルボードを活用した事例になるのだろうか。当時はポリゴンじゃなくてスプライトで実現してたと思うけど、発想は同じかなと。
この場合のビルボードというのは、立て看板みたいなもので、絶えずカメラのほうを向いているポリゴンというか…。それっぽいテクスチャをビルボードに貼っておくことで、一見するとそこに細かい何かが置いてあるように錯覚させることができるという…。まあ、ハードウェアスペックが低かった時代に多用されていた、頓智というか、インチキというか、そういうアレですけど…。以下の解説記事内のスクリーンショットが分かりやすい気がします。
_【Unity】ビルボードで常にカメラの方に向く木を作る - おもちゃラボ
昔のTVゲーム、「スペースハリアー」「アウトラン」「アフターバーナー」等もビルボードを活用した事例になるのだろうか。当時はポリゴンじゃなくてスプライトで実現してたと思うけど、発想は同じかなと。
◎ 最初に試したソース。 :
最初に書いたのは以下のソース。
_01_draw_billboard_alpha_bug.py
使用画像は以下。
_img_rgba.png
この版では、以下の解説記事を参考にして、変換行列の回転部分を単位行列にして試してみた。これは、ポリゴンをカメラに向かせる処理ではなくて、スクリーンと平行にする処理なのだとか。
_床井研究室 - ビルボード
そのあたりの処理をしているのは以下の部分。
PyOpenGL の場合、glGetDoublev(GL_MODELVIEW_MATRIX) を使うと、変換行列が二次元配列の形で取得できる。配列の並びは以下のような感じだった。
更に、glLoadMatrixd() を使えば、配列を変換行列に書き込む(変換する)ことができる。
ただ、この処理は、変換行列を書き換えてしまうので、そのままにしてしまうと後々の変換処理が軒並みおかしくなる可能性が高い。そこで、glPushMatrix() を呼んで変換行列を一時退避してから、こういった変換行列書き換え等の処理をして、処理が終わったら glPopMatrix() を呼んで変換行列の書き戻しをする、といった記述が必要になる。
さておき。実際に動かしてみたら、以下のような見た目になった。
一見すると上手く行ってるように思えたのだけど、よく見るとテクスチャの透明部分が色々おかしい。この症状は見覚えがある…。以前、three.js (WebGL) を勉強していた時にも遭遇した記憶が…。
_mieki256's diary - three.jsでビルボード
_01_draw_billboard_alpha_bug.py
import sys import math from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * from PIL import Image IMG_NAME = "img_rgba.png" SCRW, SCRH = 512, 512 FPS = 60 scr_w, scr_h = SCRW, SCRH window = 0 # Rotation angle for the Quads rotx = 0.0 roty = 0.0 texture = 0 def load_texture(): global texture # load image by using PIL im = Image.open(IMG_NAME) w, h = im.size print("Image: %d x %d, %s" % (w, h, im.mode)) if im.mode == "RGB": # RGB convert to RGBA im.putalpha(alpha=255) elif im.mode == "L" or im.mode == "P": # Grayscale, Index Color convert to RGBA im = im.convert("RGBA") raw_image = im.tobytes() ttype = GL_RGBA if im.mode == "RGB": ttype = GL_RGB print("Set GL_RGB") elif im.mode == "RGBA": ttype = GL_RGBA print("Set GL_RGBA") glBindTexture(GL_TEXTURE_2D, glGenTextures(1)) # glPixelStorei(GL_UNPACK_ALIGNMENT, 1) glPixelStorei(GL_UNPACK_ALIGNMENT, 4) # set texture glTexImage2D( GL_TEXTURE_2D, # target 0, # MIPMAP level ttype, # texture type (RGB, RGBA) w, # texture image width h, # texture image height 0, # border width ttype, # texture type (RGB, RGBA) GL_UNSIGNED_BYTE, # data is unsigne char raw_image, # texture data pointer ) glClearColor(0, 0, 0, 0) glShadeModel(GL_SMOOTH) # set texture repeat glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) # set texture filter glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE) # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL) # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) # glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND) def set_billboard_matrix(): m = glGetDoublev(GL_MODELVIEW_MATRIX) m[0][0] = m[1][1] = m[2][2] = 1.0 m[0][1] = m[0][2] = 0.0 m[1][0] = m[1][2] = 0.0 m[2][0] = m[2][1] = 0.0 glLoadMatrixd(m) def draw_gl(): global rotx, roty glClearColor(0.125, 0.25, 0.5, 0.0) # background color glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glLoadIdentity() # Reset The View # move camera r = 12.0 ex = r * math.cos(math.radians(roty + 90.0)) ey = 5.0 ez = r * math.sin(math.radians(roty + 90.0)) tx, ty, tz = 0.0, 0.0, 0.0 gluLookAt(ex, ey, ez, tx, ty, tz, 0, 1, 0) glEnable(GL_BLEND) glEnable(GL_TEXTURE_2D) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glColor4f(1.0, 1.0, 1.0, 1.0) # color # draw floor quads glPushMatrix() w = 5.0 glBegin(GL_QUADS) glTexCoord2f(0.0, 0.0) # set u, v glVertex3f(-w, 0.0, -w) # Top Left glTexCoord2f(1.0, 0.0) glVertex3f(w, 0.0, -w) # Top Right glTexCoord2f(1.0, 1.0) glVertex3f(w, 0.0, w) # Bottom Right glTexCoord2f(0.0, 1.0) glVertex3f(-w, 0.0, w) # Bottom Left glEnd() glPopMatrix() # draw object quads for i in range(0, 300, 20): x = 5.0 * math.cos(math.radians(i)) y = 0.0 z = 5.0 * math.sin(math.radians(i)) glPushMatrix() glTranslatef(x, y, z) # translate set_billboard_matrix() glBegin(GL_QUADS) glTexCoord2f(0.0, 0.0) # set u, v glVertex3f(-1.0, 2.0, 0) # Top Left glTexCoord2f(1.0, 0.0) glVertex3f(1.0, 2.0, 0.0) # Top Right glTexCoord2f(1.0, 1.0) glVertex3f(1.0, 0.0, 0.0) # Bottom Right glTexCoord2f(0.0, 1.0) glVertex3f(-1.0, 0.0, 0.0) # Bottom Left glEnd() glPopMatrix() glDisable(GL_TEXTURE_2D) glutSwapBuffers() def init_viewport_and_pers(width, height): # Prevent A Divide By Zero If The Window Is Too Small if height == 0: height = 1 scr_w, scr_h = width, height # Reset The Current Viewport And Perspective Transformation glViewport(0, 0, width, height) glMatrixMode(GL_PROJECTION) glLoadIdentity() # Reset The Projection Matrix # Calculate The Aspect Ratio Of The Window # gluPerspective(fovy, aspect, zNear, zFar ) gluPerspective(60.0, float(width) / float(height), 0.1, 100.0) glMatrixMode(GL_MODELVIEW) def InitGL(width, height): glClearColor(0.125, 0.25, 0.5, 0.0) # background color glClearDepth(1.0) # Enables Clearing Of The Depth Buffer glEnable(GL_DEPTH_TEST) # Enables Depth Testing glDepthFunc(GL_LESS) # The Type Of Depth Test To Do glShadeModel(GL_SMOOTH) # Enables Smooth Color Shading init_viewport_and_pers(width, height) def resize_gl(width, height): init_viewport_and_pers(width, height) def on_timer(value): global rotx, roty rotx = 0.0 roty = roty + 0.5 glutPostRedisplay() glutTimerFunc(int(1000 / FPS), on_timer, 0) def key_pressed(key, x, y): # If escape is pressed, kill everything. ESCAPE = b"\x1b" if key == ESCAPE or key == b'q': if glutLeaveMainLoop: glutLeaveMainLoop() else: sys.exit() def main(): global window glutInit(sys.argv) glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH) glutInitWindowSize(SCRW, SCRH) # glutInitWindowPosition(0, 0) window = glutCreateWindow(b"Draw texture") glutDisplayFunc(draw_gl) glutReshapeFunc(resize_gl) glutKeyboardFunc(key_pressed) # glutFullScreen() # glutIdleFunc(draw_gl) glutTimerFunc(int(1000 / FPS), on_timer, 0) InitGL(SCRW, SCRH) load_texture() glutMainLoop() if __name__ == "__main__": print("Hit ESC key to quit.") main()
使用画像は以下。
_img_rgba.png
この版では、以下の解説記事を参考にして、変換行列の回転部分を単位行列にして試してみた。これは、ポリゴンをカメラに向かせる処理ではなくて、スクリーンと平行にする処理なのだとか。
_床井研究室 - ビルボード
そのあたりの処理をしているのは以下の部分。
def set_billboard_matrix(): m = glGetDoublev(GL_MODELVIEW_MATRIX) m[0][0] = m[1][1] = m[2][2] = 1.0 m[0][1] = m[0][2] = 0.0 m[1][0] = m[1][2] = 0.0 m[2][0] = m[2][1] = 0.0 glLoadMatrixd(m)
PyOpenGL の場合、glGetDoublev(GL_MODELVIEW_MATRIX) を使うと、変換行列が二次元配列の形で取得できる。配列の並びは以下のような感じだった。
m[0][0] m[1][0] m[2][0] m[3][0] m[0][1] m[1][1] m[2][1] m[3][1] m[0][2] m[1][2] m[2][2] m[3][2] m[0][3] m[1][3] m[2][3] m[3][3]
更に、glLoadMatrixd() を使えば、配列を変換行列に書き込む(変換する)ことができる。
ただ、この処理は、変換行列を書き換えてしまうので、そのままにしてしまうと後々の変換処理が軒並みおかしくなる可能性が高い。そこで、glPushMatrix() を呼んで変換行列を一時退避してから、こういった変換行列書き換え等の処理をして、処理が終わったら glPopMatrix() を呼んで変換行列の書き戻しをする、といった記述が必要になる。
さておき。実際に動かしてみたら、以下のような見た目になった。
一見すると上手く行ってるように思えたのだけど、よく見るとテクスチャの透明部分が色々おかしい。この症状は見覚えがある…。以前、three.js (WebGL) を勉強していた時にも遭遇した記憶が…。
_mieki256's diary - three.jsでビルボード
◎ 修正を試みたが変化無し。 :
ググっていたら、glDepthMask() や glDepthFunc() を使うべしという話を見かけたので試してみた。
_02_draw_billboard_depth_mask.py
追記したのは以下のような行。
しかし、以下のような結果になった。ビルボードの前後関係がおかしくなってしまっている。いや、この場合、描画を指示した順番がちゃんと反映された状態で描画されるようになったと捉えるべきか…。
_02_draw_billboard_depth_mask.py
追記したのは以下のような行。
glDepthMask(GL_FALSE) glDepthFunc(GL_LEQUAL) # draw billboard ... glDepthMask(GL_TRUE) glDepthFunc(GL_LESS)
しかし、以下のような結果になった。ビルボードの前後関係がおかしくなってしまっている。いや、この場合、描画を指示した順番がちゃんと反映された状態で描画されるようになったと捉えるべきか…。
◎ ALPHA_TESTを指定して解決。 :
自分の昔の日記を眺めていたら、alphaTest なるプロパティを変更したら改善された、とメモしてあった。
_mieki256's diary - three.jsでビルボード相当を実現その2
そんなわけで、そのあたりの記述を追加してみた版が以下。
_03_draw_billboard_alpha_test.py
実際には、以下のような行を追加している。
これはどういう指定かというと…。
実行してみたところ、以下のような感じになった。
ちゃんとそれっぽく描画されている。この指定で、一応問題は解決すると分かった。
ただ、アルファチャンネルを2値化してるも同然なので…。アルファチャンネルがグラデーションを持っていても反映されないというか、縁の部分がジャギってるような見た目になりそうではあるなと…。
もっとも、画面を見た感じでは、それぞれ拡大縮小されるからジャギってるかどうかなんて正直よく分からん、という印象も受けた。これでも特に問題無さそうだよな…。
_mieki256's diary - three.jsでビルボード相当を実現その2
そんなわけで、そのあたりの記述を追加してみた版が以下。
_03_draw_billboard_alpha_test.py
実際には、以下のような行を追加している。
glAlphaFunc(GL_GREATER, 0.5) glEnable(GL_ALPHA_TEST) # draw billboard ... glDisable(GL_ALPHA_TEST)
これはどういう指定かというと…。
- glEnable(GL_ALPHA_TEST) や glDisable(GL_ALPHA_TEST) は、alpha test という処理を有効化/無効化している。
- glAlphaFunc(GL_GREATER, 0.5) は、アルファチャンネルのしきい値を設定している。
実行してみたところ、以下のような感じになった。
ちゃんとそれっぽく描画されている。この指定で、一応問題は解決すると分かった。
ただ、アルファチャンネルを2値化してるも同然なので…。アルファチャンネルがグラデーションを持っていても反映されないというか、縁の部分がジャギってるような見た目になりそうではあるなと…。
もっとも、画面を見た感じでは、それぞれ拡大縮小されるからジャギってるかどうかなんて正直よく分からん、という印象も受けた。これでも特に問題無さそうだよな…。
◎ ビルボードを奥行きでソートして描画。 :
他の方法がないかググったところ、本来こういったものは、奥から手前に向けて重ね塗りしていくのが正しい(?)やり方、という話を見かけた。
そんなわけで、ビルボードの位置に基づいて、ソートしてから描画していく方法も試してみた。
_04_draw_billboard_sort.py
そのあたりの処理をしているのは、以下の部分。ちなみに、カメラとビルボードの距離を使ってソートする方法と、変換行列内の平行移動成分を使ってソートする方法、その2つを試してみた。この程度のサンプルなら、どちらを使っても結果はさほど変わらなかった。
動作させてみたところ、以下のような見た目になった。
たしかに、描画しようとしているポリゴン群を、事前に奥行きでソートしておいて、奥のほうから手前に向かって描画していくことでも解決できそうだと分かった。
もっとも、これはまだビルボードの数が少ないから問題にならないけれど、枚数が多くなれば処理の負荷も増えてしまって、よろしくない状態になりそうではあるなと…。
そんなわけで、ビルボードの位置に基づいて、ソートしてから描画していく方法も試してみた。
_04_draw_billboard_sort.py
そのあたりの処理をしているのは、以下の部分。ちなみに、カメラとビルボードの距離を使ってソートする方法と、変換行列内の平行移動成分を使ってソートする方法、その2つを試してみた。この程度のサンプルなら、どちらを使っても結果はさほど変わらなかった。
if USE_DEPTHMASK: glDepthMask(GL_TRUE) glDepthFunc(GL_LESS) else: glDepthMask(GL_FALSE) glDepthFunc(GL_LEQUAL) # record position pos = [] for i in range(0, 300, 20): x = 5.0 * math.cos(math.radians(i)) y = 0.0 z = 5.0 * math.sin(math.radians(i)) if GET_DISTANCE: # get distance from camera to billboard dx = x - cam_pos[0] dy = y - cam_pos[1] dz = z - cam_pos[2] dist = dx * dx + dy * dy + dz * dz pos.append([x, y, z, dist]) else: # get z value glPushMatrix() glTranslatef(x, y, z) # translate m = glGetDoublev(GL_MODELVIEW_MATRIX) tx, ty, tz = m[3][0], m[3][1], m[3][2] pos.append([x, y, z, tz]) glPopMatrix() # sort billboard s_pos = sorted(pos, key=lambda x: x[3]) if GET_DISTANCE: s_pos.reverse() for p in s_pos: x, y, z, _ = p # draw billboard
動作させてみたところ、以下のような見た目になった。
たしかに、描画しようとしているポリゴン群を、事前に奥行きでソートしておいて、奥のほうから手前に向かって描画していくことでも解決できそうだと分かった。
もっとも、これはまだビルボードの数が少ないから問題にならないけれど、枚数が多くなれば処理の負荷も増えてしまって、よろしくない状態になりそうではあるなと…。
◎ 透明部分がおかしくなる理由。 :
以下のページで、何故透明部分がおかしくなるのか、図で説明されていたのでメモ。
_Alpha/Transparency Sorting, Your Z-buffer, and You - Documentation - jMonkeyEngine Hub
また、以下のページで、描画がおかしい時に確認すべきことがまとめてあった。ありがたや。GL_ALPHA_TEST についても触れられている。
_DirectXとOpenGLで描画がおかしい時に考えること - TECOの技術研究室
_Alpha/Transparency Sorting, Your Z-buffer, and You - Documentation - jMonkeyEngine Hub
また、以下のページで、描画がおかしい時に確認すべきことがまとめてあった。ありがたや。GL_ALPHA_TEST についても触れられている。
_DirectXとOpenGLで描画がおかしい時に考えること - TECOの技術研究室
[ ツッコむ ]
以上です。