mieki256's diary



2024/03/06(水) [n年前の日記]

#1 [python] Python + glfwで疑似3D道路の描画実験中。その4

Windows10 x64 22H2上で、Python 3.10.10 + glfw 2.7.0 + PyOpenGL 3.1.6 を使って勉強中。疑似3D道路を描画してみたい。

どうにかそれらしく描画できるようになってきた気がする。

線だけで描画 :

ひとまず、セグメント(道路データ)を、線だけで描画する感じで処理してみた。これなら基本的な処理が分かりやすくなるはず。




ソースは以下。

_06_ps3d.py
from OpenGL.GL import *
from OpenGL.GLU import *
import glfw

SCRW, SCRH = 1280, 720


class Gwk:
    """Global work class"""

    def __init__(self):
        global SCRW, SCRH

        self.running = True
        self.scrw = SCRW
        self.scrh = SCRH
        self.seg_length = 5.0
        self.view_distance = 160

        self.camera_z = 0.0
        self.spd = self.seg_length * 0.1

        self.segdata_src = [
            {"cnt": 20, "curve": 0.0, "pitch": 0.0},
            {"cnt": 10, "curve": -0.4, "pitch": 0.0},
            {"cnt": 20, "curve": 0.0, "pitch": 0.0},
            {"cnt": 5, "curve": 2.0, "pitch": 0.0},
            {"cnt": 20, "curve": 0.0, "pitch": 0.4},
            {"cnt": 5, "curve": 0.0, "pitch": 0.0},
            {"cnt": 10, "curve": -1.0, "pitch": 0.0},
            {"cnt": 15, "curve": 0.0, "pitch": -0.5},
            {"cnt": 10, "curve": 0.0, "pitch": 0.3},
            {"cnt": 20, "curve": -0.2, "pitch": 0.0},
            {"cnt": 10, "curve": 1.0, "pitch": 0.0},
            {"cnt": 5, "curve": -0.4, "pitch": 0.4},
            {"cnt": 10, "curve": 0.0, "pitch": -0.6},
            {"cnt": 5, "curve": 0.1, "pitch": 0.3},
            {"cnt": 10, "curve": 0.0, "pitch": -0.5},
            {"cnt": 20, "curve": 0.0, "pitch": 0.0},
        ]

        # count segment number
        self.seg_max = 0
        for d in self.segdata_src:
            self.seg_max += d["cnt"]

        self.seg_total_length = self.seg_length * self.seg_max

        # expand segment data
        z = 0.0
        self.segdata = []
        for i in range(len(self.segdata_src)):
            d0 = self.segdata_src[i]
            d1 = self.segdata_src[(i + 1) % len(self.segdata_src)]
            cnt = d0["cnt"]
            curve = d0["curve"]
            pitch = d0["pitch"]
            next_curve = d1["curve"]
            next_pitch = d1["pitch"]

            for j in range(cnt):
                ratio = j / cnt
                c = curve + ((next_curve - curve) * ratio)
                p = pitch + ((next_pitch - pitch) * ratio)
                self.segdata.append(
                    {
                        "z": z,
                        "curve": c,
                        "pitch": p,
                    }
                )
                z += self.seg_length


gw = Gwk()


def keyboard(window, key, scancode, action, mods):
    global gw
    if key == glfw.KEY_Q or key == glfw.KEY_ESCAPE:
        # ESC key or Q key to exit
        gw.running = False


def render():
    global gw

    # move camera
    gw.camera_z += gw.spd
    if gw.camera_z >= gw.seg_total_length:
        gw.camera_z -= gw.seg_total_length

    # get segment index
    idx = 0
    if gw.camera_z != 0.0:
        idx = int(gw.camera_z / gw.seg_length) % gw.seg_max
        if idx < 0:
            idx += gw.seg_max

    z = gw.segdata[idx]["z"]
    curve = gw.segdata[idx]["curve"]
    pitch = gw.segdata[idx]["pitch"]

    ccz = gw.camera_z % gw.seg_total_length
    camz = (ccz - z) / gw.seg_length
    xd = -camz * curve
    yd = -camz * pitch
    zd = gw.seg_length

    cx = -(xd * camz)
    cy = -(yd * camz)
    cz = z - ccz

    road_y = -10.0
    dt = []
    for k in range(gw.view_distance):
        dt.append({"x": cx, "y": (cy + road_y), "z": cz})
        cx += xd
        cy += yd
        cz += zd
        i = (idx + k) % gw.seg_max
        xd += gw.segdata[i]["curve"]
        yd += gw.segdata[i]["pitch"]

    # clear screen
    glClearColor(0, 0, 0, 1)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    glLoadIdentity()
    glTranslatef(0, 0, 0)

    # draw lines
    w = 20
    dt.reverse()
    glColor4f(1.0, 1.0, 1.0, 1.0)
    glBegin(GL_LINES)
    for d in dt:
        x, y, z = d["x"], d["y"], d["z"]
        glVertex3f(x - w, y, -z)
        glVertex3f(x + w, y, -z)
    glEnd()


def main():
    global gw

    if not glfw.init():
        raise RuntimeError("Could not initialize GLFW3")

    window = glfw.create_window(gw.scrw, gw.scrh, "lines", None, None)
    if not window:
        glfw.terminate()
        raise RuntimeError("Could not create an window")

    glfw.set_key_callback(window, keyboard)

    glfw.make_context_current(window)
    glfw.swap_interval(1)

    glViewport(0, 0, gw.scrw, gw.scrh)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(75.0, gw.scrw / gw.scrh, 2.5, -1000.0)
    glMatrixMode(GL_MODELVIEW)

    gw.camera_z = 0.0
    gw.running = True

    # main loop
    while not glfw.window_should_close(window) and gw.running:
        render()
        glfw.swap_buffers(window)
        glfw.poll_events()

    glfw.terminate()


if __name__ == "__main__":
    main()

一応、色々説明しておく。

動作には以下が必要。
  • Python 3.10.10 64bit
  • PyOpenGL 3.1.6
  • glfw 2.7.0

Python 3.x がインストールされている環境なら、pip でインストールできるのではないかな…。
python -m pip install glfw
or
pip install glfw

PyOpenGL については、以下から .whl ファイルを入手してインストールした。以下で入手できる版には、Python のバージョンと一致している freeglut*.dll も同梱されているので、別途 freeglut.dll をインストールしなくても GLUT が使える状態になる。まあ、今回は GLUT を使ってないけど…。

_Archived: Python Extension Packages for Windows - Christoph Gohlke

  • PyOpenGL-3.1.6-cp310-cp310-win_amd64.whl
  • PyOpenGL_accelerate-3.1.6-cp310-cp310-win_amd64.whl
pip install PyOpenGL-3.1.6-cp310-cp310-win_amd64.whl
pip install PyOpenGL_accelerate-3.1.6-cp310-cp310-win_amd64.whl


疑似3D道路の仕組みについて簡単に説明。いやまあ、「今の時代に疑似3D道路なんてわざわざやらんでもええやろ」と言われそうだけど、一応…。

about_pseudo3d_curve.png

一般的に、自分達が、カーブを描いた道路データを用意したいと思った際は、上図で言えば左側のような状態を考えるわけだけど。疑似3D道路は、右側のようなデータを持つことで、カーブを描いてるように見せかけてる。

左側のような道路データを持つと、カメラなり道路なりを回転させるための計算が入ってくるけれど、右側なら、横方向、もしくは縦方向に位置をずらしているだけなので、回転処理が入ってこなくて計算が簡単になる。また、昔のTVゲーム機は、ラスター単位でBG(背景画面)の表示位置を変更できたけれど、そういった、(今と比べたら)非力なハードウェアにとっても、表示位置を上下左右にずらすだけでそれらしく見えてくるこのやり方は都合が良かった。まあ、頓智ですわな…。

余談。疑似3D道路の描画実験をしていて思ったのは、まず道路データ(巷の英文解説記事では segment と呼んでることが多い)を用意することかなと…。まずは直線の道路だけでもいいからセグメントデータを用意して、それを透視変換で表示してみると、なんだか作れそうな気がしてくるというか…。後はコレをイイ感じに横や縦にずらして表示してやればええのやな、簡単やん、みたいな。まあ、実際にやってみると細かいところで「アレ? こんなはずでは」と悩む場面がちょくちょく出てくるけど…。

テクスチャを貼ったり塗りつぶしたりしてみた :

線だけで道路を描画することができたので、道路にテクスチャを貼ったり、地面を塗りつぶしたりしてみた。ここまでできれば、後は背景とビルボードを出すだけでもうちょっとそれらしくなるかなと。




ソースは以下。ちょっと長くなってしまった…。

_08_ps3d_tex.py
from OpenGL.GL import *
from OpenGL.GLU import *
import glfw
from PIL import Image
import math

SCRW, SCRH = 1280, 720


class Gwk:
    """Global work"""

    def __init__(self):
        global SCRW, SCRH

        self.running = True
        self.scrw = SCRW
        self.scrh = SCRH
        self.seg_length = 7.5
        self.view_distance = 160
        self.fovy = 70.0
        self.fovx = self.fovy * self.scrw / self.scrh
        self.znear = self.seg_length * 0.7
        self.zfar = self.seg_length * (self.view_distance + 2)

        self.camera_z = 0.0
        self.spd = self.seg_length * 0.1

        self.segdata_src = [
            {"cnt": 20, "curve": 0.0, "pitch": 0.0},
            {"cnt": 10, "curve": -0.4, "pitch": 0.0},
            {"cnt": 20, "curve": 0.0, "pitch": 0.0},
            {"cnt": 5, "curve": 2.0, "pitch": 0.0},
            {"cnt": 20, "curve": 0.0, "pitch": 0.4},
            {"cnt": 5, "curve": 0.0, "pitch": 0.0},
            {"cnt": 10, "curve": -1.0, "pitch": 0.0},
            {"cnt": 15, "curve": 0.0, "pitch": -0.5},
            {"cnt": 10, "curve": 0.0, "pitch": 0.3},
            {"cnt": 20, "curve": -0.2, "pitch": 0.0},
            {"cnt": 10, "curve": 1.0, "pitch": 0.0},
            {"cnt": 5, "curve": -0.4, "pitch": 0.4},
            {"cnt": 10, "curve": 0.0, "pitch": -0.6},
            {"cnt": 5, "curve": 0.1, "pitch": 0.3},
            {"cnt": 10, "curve": 0.0, "pitch": -0.5},
            {"cnt": 20, "curve": 0.0, "pitch": 0.0},
        ]

        # count segment number
        self.seg_max = 0
        for d in self.segdata_src:
            self.seg_max += d["cnt"]

        self.seg_total_length = self.seg_length * self.seg_max

        # expand segment data
        z = 0.0
        self.segdata = []
        for i in range(len(self.segdata_src)):
            d0 = self.segdata_src[i]
            d1 = self.segdata_src[(i + 1) % len(self.segdata_src)]
            cnt = d0["cnt"]
            curve = d0["curve"]
            pitch = d0["pitch"]
            next_curve = d1["curve"]
            next_pitch = d1["pitch"]

            for j in range(cnt):
                ratio = j / cnt
                c = curve + ((next_curve - curve) * ratio)
                p = pitch + ((next_pitch - pitch) * ratio)
                self.segdata.append(
                    {
                        "z": z,
                        "curve": c,
                        "pitch": p,
                    }
                )
                z += self.seg_length

    def load_image(self):
        im = Image.open("road.png").convert("RGBA")
        w, h = im.size
        data = im.tobytes()

        self.road_tex = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, self.road_tex)
        glPixelStorei(GL_UNPACK_ALIGNMENT, 4)
        glTexImage2D(
            GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data
        )


gw = Gwk()


def keyboard(window, key, scancode, action, mods):
    global gw
    if key == glfw.KEY_Q or key == glfw.KEY_ESCAPE:
        # ESC key or Q key to exit
        gw.running = False


def resize(window, w, h):
    if h == 0:
        return
    global gw
    gw.scrw = w
    gw.scrh = h
    glViewport(0, 0, w, h)
    gw.fovx = gw.fovy * gw.scrw / gw.scrh


def render():
    global gw

    # move camera
    gw.camera_z += gw.spd
    if gw.camera_z >= gw.seg_total_length:
        gw.camera_z -= gw.seg_total_length

    # get segment index
    idx = 0
    if gw.camera_z != 0.0:
        idx = int(gw.camera_z / gw.seg_length) % gw.seg_max
        if idx < 0:
            idx += gw.seg_max

    z = gw.segdata[idx]["z"]
    curve = gw.segdata[idx]["curve"]
    pitch = gw.segdata[idx]["pitch"]

    ccz = gw.camera_z % gw.seg_total_length
    camz = (ccz - z) / gw.seg_length
    xd = -camz * curve
    yd = -camz * pitch
    zd = gw.seg_length

    cx = -(xd * camz)
    cy = -(yd * camz)
    cz = z - ccz

    road_y = -10.0
    dt = []
    for k in range(gw.view_distance):
        i = (idx + k) % gw.seg_max
        a = i % 4
        dt.append({"x": cx, "y": (cy + road_y), "z": cz, "attr": a})
        cx += xd
        cy += yd
        cz += zd
        xd += gw.segdata[i]["curve"]
        yd += gw.segdata[i]["pitch"]
    
    # init OpenGL
    glViewport(0, 0, gw.scrw, gw.scrh)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(gw.fovy, gw.scrw / gw.scrh, gw.znear, gw.zfar)
    glMatrixMode(GL_MODELVIEW)

    # clear screen
    glClearColor(0, 0, 0, 1)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    glEnable(GL_CULL_FACE)
    glCullFace(GL_BACK)

    glLoadIdentity()
    glTranslatef(0, 0, 0)

    glEnable(GL_TEXTURE_2D)
    glBindTexture(GL_TEXTURE_2D, gw.road_tex)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)

    # draw roads
    w = 20
    tanv = math.tan(math.radians(gw.fovx) / 2.0)
    dt.reverse()

    for i in range(len(dt) - 1):

        d0 = dt[i]
        d1 = dt[i + 1]
        x0, y0, z0, a0 = d0["x"], d0["y"], d0["z"], d0["attr"]
        x1, y1, z1 = d1["x"], d1["y"], d1["z"]
        gndw0 = tanv * z0
        gndw1 = tanv * z1

        # draw ground
        glDisable(GL_TEXTURE_2D)
        if a0 % 2 == 0:
            glColor4f(0.45, 0.70, 0.25, 1)
        else:
            glColor4f(0.10, 0.68, 0.25, 1)

        glBegin(GL_QUADS)
        glVertex3f(+gndw0, y0, -z0)
        glVertex3f(-gndw0, y0, -z0)
        glVertex3f(-gndw1, y1, -z1)
        glVertex3f(+gndw1, y1, -z1)
        glEnd()

        # draw road
        glEnable(GL_TEXTURE_2D)
        glBegin(GL_QUADS)
        v0 = a0 * 0.25
        v1 = v0 + 0.25
        glColor4f(1, 1, 1, 1)
        glTexCoord2f(0.0, v0)
        glVertex3f(x0 + w, y0, -z0)
        glTexCoord2f(1.0, v0)
        glVertex3f(x0 - w, y0, -z0)
        glTexCoord2f(1.0, v1)
        glVertex3f(x1 - w, y1, -z1)
        glTexCoord2f(0.0, v1)
        glVertex3f(x1 + w, y1, -z1)
        glEnd()

    glDisable(GL_TEXTURE_2D)


def main():
    global gw

    if not glfw.init():
        raise RuntimeError("Could not initialize GLFW3")

    window = glfw.create_window(gw.scrw, gw.scrh, "lines", None, None)
    if not window:
        glfw.terminate()
        raise RuntimeError("Could not create an window")

    glfw.set_key_callback(window, keyboard)
    glfw.set_window_size_callback(window, resize)

    glfw.make_context_current(window)
    glfw.swap_interval(1)

    gw.load_image()

    gw.camera_z = 0.0
    gw.running = True

    # main loop
    while not glfw.window_should_close(window) and gw.running:
        render()
        glFlush()
        glfw.swap_buffers(window)
        glfw.poll_events()

    glfw.terminate()


if __name__ == "__main__":
    main()

使用画像は以下。256x256, 32bit(RGBA)のpng画像。コレを道路のテクスチャ画像として使ってる。

_road.png


少し説明。テクスチャ画像の読み込みには Pillow (PIL) 10.2.0 を使ってる。pip でインストールできる。
python -m pip install pillow


今回、地面を塗り潰すところでちょっと悩んでしまった。2D描画を前提とした画面なら、「画面の左端から右端まで塗り潰せ」だけで済むけれど。OpenGL のような3D空間でそういう見た目を実現するにはどうしたらいいんだろうと…。

画面の左端から右端まで覆いつくすような四角ポリゴンを置けば目的が果たせるのではないかと考えたけれど、画面端のx座標を求めるところで悩んでしまった。

x座標を求める手順としては以下。縦方向の視野角と、ウインドウの横幅、縦幅から、横方向の視野角を求めて…。描画したい位置のz値は分かってるから、tan(横方向の視野角 / 2) * z値、で、求める x座標値が得られる。

about_getx.png

縦方向の視野角は gluPerspective() を呼ぶ際に指定しているから既に分かっているし、ウインドウの横幅と縦幅はウインドウを作るための glfw.create_window() を呼ぶ際に指定してるからこれも分かっているはず。

というかこのあたり、昔やってた…。完全に忘れてた。もうダメだ自分。

_mieki256's diary - Processingを使って cube map とやらのテスト


余談。最初はフラットな見た目の画像を使って実験していたのだけど、ポリゴンの繋ぎ目のところにチラチラとテクスチャのゴミが表示されたり、境界がうっすらと見えてしまったりして、コレが気になって気になって…。ノイズだらけの画像と差し替えてみたらそれほど気にならなくなったけれど、本来はどうやって解決すべきなのだろう…?

余談 :

「せっかく OpenGL (PyOpenGL) を使ってるなら疑似3D道路なんてやらずにフツーに3D空間に道路を置いて描画すればええやん」と言われそうだけど、それは昔に Processing (Proce55ing) や three.js を使って既にやっておりまして。

_mieki256's diary - three.jsで一本道を延々と走るソレ
_mieki256's diary - Processingで一本道の道路の生成をテスト
_mieki256's diary - Processingで一本道の道路を延々走るソレを弄ってたり

なんというか、もっとレトロな雰囲気の画面を出してみたいわけですよ…。

以上です。

過去ログ表示

Prev - 2024/03 - 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
31

カテゴリで表示

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


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

Powered by hns-2.19.6, HyperNikkiSystem Project