mieki256's diary



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ゲーム、「スペースハリアー」「アウトラン」「アフターバーナー」等もビルボードを活用した事例になるのだろうか。当時はポリゴンじゃなくてスプライトで実現してたと思うけど、発想は同じかなと。

最初に試したソース。 :

最初に書いたのは以下のソース。

_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

追記したのは以下のような行。

    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

実際には、以下のような行を追加している。
    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) は、アルファチャンネルのしきい値を設定している。
この場合、アルファチャンネルの値が 0.5 より大きければそのドットは描画されるし、0.5より小さければそのドットは無視される。描画対象にするのか、しないのか、つまりは二値化している模様。

実行してみたところ、以下のような感じになった。




ちゃんとそれっぽく描画されている。この指定で、一応問題は解決すると分かった。

ただ、アルファチャンネルを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の技術研究室

以上、1 日分です。

過去ログ表示

Prev - 2022/09 - 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