2022/10/03(月) [n年前の日記]
#1 [python] PyOpenGLでSkyboxを描画
Windows10 x64 21H2 + Python 3.9.13 64bit + PyOpenGL 3.1.6 で、Skybox を描画できるかテスト。
以下のような見た目になった。マウスカーソルの位置に応じてカメラ角度が変わるようにしてみた。それっぽい感じになったような気がする。
以下のような見た目になった。マウスカーソルの位置に応じてカメラ角度が変わるようにしてみた。それっぽい感じになったような気がする。
◎ ソース。 :
ソースは以下。
_01_draw_skybox.py
使用画像は以下。512 x 512 x 6枚の画像を 2048 x 2048 x 1枚に収めている。
_montorfano_cubemap.png (2.93MB)
_alps_field_cubemap.png (3.41MB)
_bell_park_cubemap.png (3.06MB)
実行は以下。
ちなみに、画像は、Poly Haven でCC0で公開されている、正距円筒図法のHDRIを元にして作成した。ありがたや。
_Poly Haven
_正距円筒図法
_(Equirectangular projection)
作り方は以下でメモしてある。
_Skybox用のテクスチャを作成
_01_draw_skybox.py
import sys import math from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import * from PIL import Image IMG_NAME = "montorfano_cubemap.png" # IMG_NAME = "alps_field_cubemap.png" # IMG_NAME = "bell_park_cubemap.png" # SCRW, SCRH = 1600, 900 SCRW, SCRH = 512, 512 FPS = 60 BOXW = 16 scr_w, scr_h = SCRW, SCRH window = 0 mouse_x = 0 mouse_y = 0 cam_rot_x = 0.0 cam_rot_y = 0.0 cam_pos = (0, 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 draw_skybox(): 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) glDisable(GL_ALPHA_TEST) w = BOXW glPushMatrix() m = glGetDoublev(GL_MODELVIEW_MATRIX) m[3][0] = m[3][1] = m[3][2] = 0.0 glLoadMatrixd(m) va = 0.0005 # va = 0.0 pntlist = [ # x, y, z [-1, +1, -1], # 0 [+1, +1, -1], # 1 [+1, +1, +1], # 2 [-1, +1, +1], # 3 [-1, -1, -1], # 4 [+1, -1, -1], # 5 [+1, -1, +1], # 6 [-1, -1, +1], # 7 ] pos = [ [ # index, u, v [0, 0.25, 0.50 + va], [1, 0.50, 0.50 + va], [5, 0.50, 0.75 - va], [4, 0.25, 0.75 - va], ], [ [1, 0.50, 0.50 + va], [2, 0.75, 0.50 + va], [6, 0.75, 0.75 - va], [5, 0.50, 0.75 - va], ], [ [2, 0.75, 0.50 + va], [3, 1.00, 0.50 + va], [7, 1.00, 0.75 - va], [6, 0.75, 0.75 - va], ], [ [3, 0.00, 0.50 + va], [0, 0.25, 0.50 + va], [4, 0.25, 0.75 - va], [7, 0.00, 0.75 - va], ], [ [3, 0.25 + va, 0.25 + va], [2, 0.50 - va, 0.25 + va], [1, 0.50 - va, 0.50 - va], [0, 0.25 + va, 0.50 - va], ], [ [4, 0.25 + va, 0.75 + va], [5, 0.50 - va, 0.75 + va], [6, 0.50 - va, 1.00 - va], [7, 0.25 + va, 1.00 - va], ], ] for poly in pos: glBegin(GL_QUADS) for idx, u, v in poly: x, y, z = pntlist[idx] glTexCoord2f(u, v) # set u, v glVertex3f(w * x, w * y, w * z) glEnd() glPopMatrix() def draw_gl(): global cam_rot_x, cam_rot_y, cam_pos glClearColor(0.2, 0.4, 0.8, 0.0) # background color glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glLoadIdentity() # Reset The View # move camera r = 8.0 rr = r * math.cos(math.radians(cam_rot_x)) ey = r * math.sin(math.radians(cam_rot_x)) ex = rr * math.cos(math.radians(cam_rot_y + 90.0)) ez = rr * math.sin(math.radians(cam_rot_y + 90.0)) tx, ty, tz = 0.0, 0.0, 0.0 gluLookAt(ex, ey, ez, tx, ty, tz, 0, 1, 0) cam_pos = (ex, ey, ez) draw_skybox() glDisable(GL_TEXTURE_2D) glDisable(GL_ALPHA_TEST) glutSwapBuffers() def init_viewport_and_pers(width, height): global scr_w, scr_h # 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.2, 0.4, 0.8, 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 cam_rot_x, cam_rot_y # cam_rot_y += 0.25 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 mouse_move(x, y): global mouse_x, mouse_y, cam_rot_x, cam_rot_y, scr_w, scr_h dw = scr_w / 2 dh = scr_h / 2 mouse_x = (x - dw) / dh mouse_y = (y - dh) / dh cam_rot_x = 90.0 * mouse_y cam_rot_y = 180.0 * mouse_x def main(): global window glutInit(sys.argv) glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH) glutInitWindowSize(SCRW, SCRH) # glutInitWindowPosition(0, 0) window = glutCreateWindow(b"Draw Skybox") glutDisplayFunc(draw_gl) glutReshapeFunc(resize_gl) glutKeyboardFunc(key_pressed) glutPassiveMotionFunc(mouse_move) # 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()
使用画像は以下。512 x 512 x 6枚の画像を 2048 x 2048 x 1枚に収めている。
_montorfano_cubemap.png (2.93MB)
_alps_field_cubemap.png (3.41MB)
_bell_park_cubemap.png (3.06MB)
実行は以下。
python 01_draw_skybox.py
ちなみに、画像は、Poly Haven でCC0で公開されている、正距円筒図法のHDRIを元にして作成した。ありがたや。
_Poly Haven
_正距円筒図法
_(Equirectangular projection)
作り方は以下でメモしてある。
_Skybox用のテクスチャを作成
◎ 覚書。 :
Skyboxは、カメラを取り囲むような大きな箱を表示して、その箱に遠景相当のテクスチャを貼ることで実現するけれど。カメラが移動して箱との位置関係が変わってしまうとちょっと困る。
そのあたりは、箱を描画する前に設定する変換行列に対して、平行移動成分のみを 0.0 にすることで対処する。これで、カメラの角度は反映されるけど、位置関係は常時変わらない状態になる。
OpenGL (PyOpenGL) + GLUT で、マウスカーソルが移動したことを検出するには、glutPassiveMotionFunc() を使う。指定した関数には x と y の引数が渡されるけど、この x, y には、ウインドウ内におけるマウスカーソルの座標値が入ってる。
箱の境界部分に黒い線が入ってしまって悩んだけれど、テクスチャの座標指定値をほんの少し弄ってみたら消えてくれた。draw_skybox() 内の va = 0.0005 が弄ってる量。本当にこういう対処でいいのかどうか分からんけど…。ちなみに、以下が黒い線が入ってしまっている状態。
そのあたりは、箱を描画する前に設定する変換行列に対して、平行移動成分のみを 0.0 にすることで対処する。これで、カメラの角度は反映されるけど、位置関係は常時変わらない状態になる。
OpenGL (PyOpenGL) + GLUT で、マウスカーソルが移動したことを検出するには、glutPassiveMotionFunc() を使う。指定した関数には x と y の引数が渡されるけど、この x, y には、ウインドウ内におけるマウスカーソルの座標値が入ってる。
箱の境界部分に黒い線が入ってしまって悩んだけれど、テクスチャの座標指定値をほんの少し弄ってみたら消えてくれた。draw_skybox() 内の va = 0.0005 が弄ってる量。本当にこういう対処でいいのかどうか分からんけど…。ちなみに、以下が黒い線が入ってしまっている状態。
[ ツッコむ ]
以上です。