2024/04/22(月) [n年前の日記]
#1 [python] PyOpenGLでモデルデータを描画したい
Python + PyOpenGL + glfw で、モデルデータを描画したい。とりあえず、wavefront obj形式のモデルデータを読み込んで描画できるだけでも助かる。
環境は Windows10 x64 22H2 + Python 3.10.10 64bit。PyOpenGL 3.1.6 + PyOpenGL-accelerate 3.1.6 + glfw 2.7.0。
環境は Windows10 x64 22H2 + Python 3.10.10 64bit。PyOpenGL 3.1.6 + PyOpenGL-accelerate 3.1.6 + glfw 2.7.0。
◎ ライブラリを探す :
wavefront obj形式を解析する処理を自分で書いてもいいのだけど、大人気のプログラミング言語、Python のことだから、絶対に誰かが既にそういう処理を書いてライブラリにしているはず。PyPI で検索してみた。
_PyPI - The Python Package Index
色々見かけたけど、PyWavefront とやらはどうだろう。
_PyWavefront - PyPI
_pywavefront/PyWavefront: Python library for importing Wavefront .obj files
_【Pythonライブラリ】「pywavefront」のサンプルコード | YuNi-Wiki
pip でインストール。
PyWavefront 1.3.3 がインストールされた。
_PyPI - The Python Package Index
色々見かけたけど、PyWavefront とやらはどうだろう。
_PyWavefront - PyPI
_pywavefront/PyWavefront: Python library for importing Wavefront .obj files
_【Pythonライブラリ】「pywavefront」のサンプルコード | YuNi-Wiki
pip でインストール。
pip install pywavefront
PyWavefront 1.3.3 がインストールされた。
◎ PyWavefrontの使い方 :
とりあえず、以下のように書けば、.obj と .mtl を読み込んでくれるらしい。
ただ、変数objに入った、このクラスが、どんな情報を持っているのかが分からない…。とりあえず、それらしい情報を print() で出力してみよう…。
テストに使ったモデルデータは以下。
_cube01.obj
_cube01.mtl
_suzanne01.obj
_suzanne01.mtl
_car.obj
_car.mtl
car.obj、car.mtl は、以下のデータを利用させてもらった。ライセンスがCC0なので、改変して利用してもOK。自由に使える。ありがたや。ほんの少しだけ修正して、ポリゴン数を微妙に減らして使ってみた。
_Car Kit | OpenGameArt.org
_Car Kit - Kenney
テストに使った Pythonスクリプトは以下。
_dump_obj.py
出力結果は以下。
_dump_obj_output.txt
どうやら、.mesh_list が持ってる情報を使えば、描画ができそうだなと…。
import pywavefront obj = pywavefront.Wavefront("hoge.obj")
ただ、変数objに入った、このクラスが、どんな情報を持っているのかが分からない…。とりあえず、それらしい情報を print() で出力してみよう…。
テストに使ったモデルデータは以下。
_cube01.obj
_cube01.mtl
_suzanne01.obj
_suzanne01.mtl
_car.obj
_car.mtl
car.obj、car.mtl は、以下のデータを利用させてもらった。ライセンスがCC0なので、改変して利用してもOK。自由に使える。ありがたや。ほんの少しだけ修正して、ポリゴン数を微妙に減らして使ってみた。
_Car Kit | OpenGameArt.org
_Car Kit - Kenney
テストに使った Pythonスクリプトは以下。
_dump_obj.py
import pywavefront infile = "cube01.obj" # infile = "suzanne01.obj" def dump_material(mat): print() print(f".name = {mat.name}") print(f".vertex_format = {mat.vertex_format}") print(f".vertices = {mat.vertices}") print(f".diffuse = {mat.diffuse}") print(f".ambient = {mat.ambient}") print(f".specular = {mat.specular}") print(f".texture = {mat.texture}") def main(): obj = pywavefront.Wavefront(infile) # obj.parse() print(f"file_name = {obj.file_name}") print(f"mrllibs = {obj.mtllibs}") print(f"vertices = {obj.vertices}") print(f"\nmaterials = {obj.materials}") # for matname, mat in obj.materials.items(): # dump_material(mat) print(f"\nmeshes = {obj.meshes}") # for meshname, mesh in obj.meshes.items(): # print() # print(f"Mesh name = {meshname}") # print(f"name = {mesh.name}") # print(f"faces = {mesh.faces}") # print(f"materials = {mesh.materials}") # print("\n# mesh.materials") # for mat in mesh.materials: # dump_material(mat) print(f"\nmesh_List = {obj.mesh_list}") for mesh in obj.mesh_list: print(f".name = {mesh.name}") print(f".faces = {mesh.faces}") print(f".materials = {mesh.materials}") for mat in mesh.materials: dump_material(mat) if __name__ == "__main__": main()
python dump_obj.py
出力結果は以下。
_dump_obj_output.txt
どうやら、.mesh_list が持ってる情報を使えば、描画ができそうだなと…。
- .mesh_list という配列の中に、各mesh が入っている。
- 各meshは、マテリアル(.materials) を持っている。
- そのマテリアルの中に、頂点配列(.vertices)も含まれている。
- 頂点配列は、テクスチャUV情報、法線情報、頂点座標値が、インターリーブされた形で入ってる。
- どんなインターリーブになっているかは、.vertex_format に入っている。
◎ インターリーブについて :
OpenGLのインターリーブについては、以下のページが参考になるかなと…。
_GPU本来の性能を引き出すWebGL頂点データ作成法 #WebGL - Qiita
_wgld.org | WebGL: インターリーブ配列 VBO |
要は、頂点座標、法線情報、テクスチャUV情報などを、頂点1つ毎にチマチマと並べている頂点配列の格納形式、ということでいいのだろうか。ただ、上記のページの解説によると、昔のGPUではインターリーブをすると高速化できたけど、今のGPUでは遅くなるそうで…。
そのインターリーブとやらは OpenGL 1.1 でも使えるのだろうかと気になったけど、Microsoftのページでは、「OpenGL 1.1以降で使える」と書いてあるようだなと…。
_glInterleavedArrays 関数 (Gl.h) - Win32 apps | Microsoft Learn
_GPU本来の性能を引き出すWebGL頂点データ作成法 #WebGL - Qiita
_wgld.org | WebGL: インターリーブ配列 VBO |
要は、頂点座標、法線情報、テクスチャUV情報などを、頂点1つ毎にチマチマと並べている頂点配列の格納形式、ということでいいのだろうか。ただ、上記のページの解説によると、昔のGPUではインターリーブをすると高速化できたけど、今のGPUでは遅くなるそうで…。
そのインターリーブとやらは OpenGL 1.1 でも使えるのだろうかと気になったけど、Microsoftのページでは、「OpenGL 1.1以降で使える」と書いてあるようだなと…。
_glInterleavedArrays 関数 (Gl.h) - Win32 apps | Microsoft Learn
注意 : glInterleavedArrays 関数は、OpenGL バージョン 1.1 以降でのみ使用できます。glInterleavedArrays 関数 (Gl.h) - Win32 apps | Microsoft Learn より
◎ PyWavefrontを利用して描画してみる :
PyOpenGL 3.1.6 + glfw 2.7.0 + PyWavefront 1.3.3 を利用して、wavefront obj形式のモデルデータを読み込んで描画してみる。
以下のような見た目になった。
_draw_opengl_glfw.py
render() の中で OpenGL関係の描画をしているから、そこを見れば大体分かりそうだけど。肝は以下だろうか…。
PyWavefront を使って読み込むと、マテリアル毎に、そのマテリアル情報を使って描画したい頂点配列をまとめてくれるので…。
インターリーブのフォーマットについては、今回のテストモデルデータを使う限り、GL_T2F_N3F_V3F を固定で指定しちゃって問題無さそうだったけど。本来は、.vertex_format から指定すべき値を決めないといけない気がする。
ちなみに、GL_T2F_N3F_V3F は、テクスチャUVがfloatで2個分(=T2F)、法線情報がfloatで3個分(=N3F)、頂点座標がfloatで3個分(=V3F)、の順番で入っていると示す値、だと思う。
さておき。描画されたモデルの色が、なんだかおかしい気がする…。blender上で表示されたソレとは随分違う色になってしまっていて…。OpenGL側でマテリアルの指定をするあたりで何か間違えてしまっている気がする。
以下のような見た目になった。
_draw_opengl_glfw.py
import glfw from OpenGL.GL import * from OpenGL.GLUT import * from OpenGL.GLU import * import pywavefront model_kind = 2 modeldata = [ {"file": "./cube01.obj", "scale": 2.0}, {"file": "./suzanne01.obj", "scale": 5.0}, {"file": "./car.obj", "scale": 7.0}, ] SCRW, SCRH = 1280, 720 WDWTITLE = "Draw wavefront obj" FOV = 50.0 light_pos = [1.0, 1.0, 1.0, 0.0] light_ambient = [0.2, 0.2, 0.2, 1.0] light_diffuse = [0.9, 0.9, 0.9, 1.0] light_specular = [0.5, 0.5, 0.5, 1.0] winw, winh = SCRW, SCRH ang = 0.0 obj = None def init_animation(infile): global ang, obj ang = 0.0 obj = pywavefront.Wavefront(infile) def render(): global ang, obj ang += 45.0 / 60.0 # init OpenGL glViewport(0, 0, winw, winh) glMatrixMode(GL_PROJECTION) glLoadIdentity() gluPerspective(FOV, float(winw) / float(winh), 1.0, 1000.0) # clear screen glClearDepth(1.0) glClearColor(0, 0, 0, 1) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glMatrixMode(GL_MODELVIEW) glLoadIdentity() glDepthFunc(GL_LESS) glEnable(GL_DEPTH_TEST) glEnable(GL_BLEND) glEnable(GL_NORMALIZE) glEnable(GL_CULL_FACE) glFrontFace(GL_CCW) # glCullFace(GL_FRONT) glCullFace(GL_BACK) # set material glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE) glEnable(GL_COLOR_MATERIAL) # set lighting glLightfv(GL_LIGHT0, GL_POSITION, light_pos) glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient) glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse) glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular) glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) # obj move and rotate glTranslatef(0.0, 0.0, -20.0) scale = modeldata[model_kind]["scale"] glScalef(scale, scale, scale) glRotatef(20.0, 1, 0, 0) # glRotatef(ang * 0.5, 1, 0, 0) glRotatef(ang, 0, 1, 0) # draw obj for mesh in obj.mesh_list: for mat in mesh.materials: r, g, b, a = mat.diffuse glColor4f(r, g, b, a) gl_floats = (GLfloat * len(mat.vertices))(*mat.vertices) count = len(mat.vertices) / mat.vertex_size glInterleavedArrays(GL_T2F_N3F_V3F, 0, gl_floats) glDrawArrays(GL_TRIANGLES, 0, int(count)) def key_callback(window, key, scancode, action, mods): if action == glfw.PRESS: if key == glfw.KEY_ESCAPE or key == glfw.KEY_Q: glfw.set_window_should_close(window, True) def resize(window, w, h): if h == 0: return set_view(w, h) def set_view(w, h): global winw, winh winw, winh = w, h glViewport(0, 0, w, h) def main(): if not glfw.init(): raise RuntimeError("Could not initialize GLFW3") return window = glfw.create_window(SCRW, SCRH, WDWTITLE, None, None) if not window: glfw.terminate() raise RuntimeError("Could not create an window") return glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 1) glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 1) glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) glfw.window_hint(glfw.DEPTH_BITS, 24) glfw.set_key_callback(window, key_callback) glfw.set_window_size_callback(window, resize) glfw.make_context_current(window) glfw.swap_interval(1) set_view(SCRW, SCRH) # get csv filepath infile = modeldata[model_kind]["file"] if len(sys.argv) != 2 else sys.argv[1] init_animation(infile) # main loop while not glfw.window_should_close(window): render() glfw.swap_buffers(window) glfw.poll_events() glfw.destroy_window(window) glfw.terminate() if __name__ == "__main__": main()
render() の中で OpenGL関係の描画をしているから、そこを見れば大体分かりそうだけど。肝は以下だろうか…。
import pywavefront # ... obj = pywavefront.Wavefront(infile) # ... # draw obj for mesh in obj.mesh_list: for mat in mesh.materials: r, g, b, a = mat.diffuse glColor4f(r, g, b, a) gl_floats = (GLfloat * len(mat.vertices))(*mat.vertices) count = len(mat.vertices) / mat.vertex_size glInterleavedArrays(GL_T2F_N3F_V3F, 0, gl_floats) glDrawArrays(GL_TRIANGLES, 0, int(count))
PyWavefront を使って読み込むと、マテリアル毎に、そのマテリアル情報を使って描画したい頂点配列をまとめてくれるので…。
- マテリアル単位でループ処理する。
- マテリアル情報を元にしながら色の設定を行う。
- glInterleavedArrays() でインターリーブのフォーマットを指定。
- glDrawArrays() で頂点配列の描画を指示。
インターリーブのフォーマットについては、今回のテストモデルデータを使う限り、GL_T2F_N3F_V3F を固定で指定しちゃって問題無さそうだったけど。本来は、.vertex_format から指定すべき値を決めないといけない気がする。
ちなみに、GL_T2F_N3F_V3F は、テクスチャUVがfloatで2個分(=T2F)、法線情報がfloatで3個分(=N3F)、頂点座標がfloatで3個分(=V3F)、の順番で入っていると示す値、だと思う。
さておき。描画されたモデルの色が、なんだかおかしい気がする…。blender上で表示されたソレとは随分違う色になってしまっていて…。OpenGL側でマテリアルの指定をするあたりで何か間違えてしまっている気がする。
[ ツッコむ ]
以上です。