1. はじめに
本記事ではPythonでOpenGLの基本をまとめる。なぜPythonですか?
Cmake含めて、Cの環境設定はしんどいので(長年間経験したので)、Pythonを選択した。
また、PythonでやるとOS環境など無視して、どのOSでもすぐできるようにと考える。
目的はあくまでC言語の勉強ではなく、CGプログラミングを勉強すること。
もし、Cでやりたいであれば、 和歌山大学の床井浩平先生による GLUTによる『手抜き』OpenGL入門をが参考にしてください。
また、以下のgithubですべてのコードを公開する予定。
https://github.com/fermanda/Python_CG_Programming
1.1 環境
- Windows 11
- VS Code
- Python 3.8 (venv仮想環境)
1.2 セットアップ
まず、必要なパッケージをインストールする。
pip install PyOpenGL PyOpenGL_accelerate glfw
以上。
Pythonはめちゃくちゃ便利。
では、ささっと本番のCGプログラミングを開始。
その前、PyOpenGLのsyntaxは、なんとC言語とまったく一緒!
なので、床井先生の「GLUTによる『手抜き』OpenGL入門」は参考にする。
2. ウィンドウ
2.1 ウィンドウ生成
ウィンドウ作成方法はいくらでもある。しかし今回はGLUTよりGLFWを利用する。
一つの理由はGLFWの開発は進めている。
import glfw
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
def main():
# glfwを初期化する
if not glfw.init():
return
# "Hello World"のタイトルで640 × 480のウィンドウ作成する
window = glfw.create_window(640, 480, "Hello World", None, None)
if not window:
glfw.terminate()
return
# 作成したウィンドウをメインにする(レンダリングのためとか)
glfw.make_context_current(window)
# ウィンドウ開ける状態に以下のプロセスをループする。
while not glfw.window_should_close(window):
# ここからレンダリングプロセスをバッファーで行う
# レンダリングされているバッファーを当たれしいバッファーと交換する。
glfw.swap_buffers(window)
# イベント(キーなどの入力)を認識と集まる。
glfw.poll_events()
glfw.terminate()
if __name__ == "__main__":
main()
上のコードで新しい640 × 480のウィンドウを作成する。実行したら以下の結果になる。
2.2 ウィンドウの調整する
ウィンドウの背景の色も変われる。
glClearColor(R, G, B, Alpha)
を0.0から1.0の範囲で以下のように設定する。
...
while not glfw.window_should_close(window):
# ここからレンダリングプロセスをバッファーで行う
# ウインドウの色変更する
glClearColor(0.0, 0.0, 1.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
...
実行したら、以下の結果となる。
3. 2D図形
次は2D図形を描くプログラム作りましょう。
3.1 2D図形を描く
以下のコードで四角の2D図形を描けた。
import glfw
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
def main():
if not glfw.init():
return
window = glfw.create_window(640, 480, "Hello World", None, None)
if not window:
glfw.terminate()
return
glfw.make_context_current(window)
while not glfw.window_should_close(window):
# ここからレンダリングプロセスをバッファーで行う
glClear(GL_COLOR_BUFFER_BIT)
glBegin(GL_LINE_LOOP)
glVertex2d(-0.9, -0.9)
glVertex2d(0.9, -0.9)
glVertex2d(0.9, 0.9)
glVertex2d(-0.9, 0.9)
glEnd()
glFlush()
glfw.swap_buffers(window)
glfw.poll_events()
glfw.terminate()
if __name__ == "__main__":
main()
上のコードはほどんど前と一緒ですが、注目すべき部分はレンダリングのところです。
...
glBegin(GL_LINE_LOOP)
glVertex2d(-0.9, -0.9)
glVertex2d(0.9, -0.9)
glVertex2d(0.9, 0.9)
glVertex2d(-0.9, 0.9)
glEnd()
glFlush()
...
glBeginのパラメタ以下の画像で説明する。
図参考:https://tokoik.github.io/opengl/libglut.html
上のプログラムはGL_LINE_LOOP
を使って、四角を描けた。描くためにはglBegin()
で開始、glEnd()
で終了する。glFlush()
を実行したら、以前のすべてのglコマンドを実行する。
3.2 カラーを入れよう
白い薄い四角がレンダーされたが、あまり魅力じゃないので、色を付けてみよう。
vertexを描く前に色を指定する。
...
glBegin(GL_LINE_LOOP)
glColor3d(1.0, 1.0, 0.0) # ここで色を指定する
glVertex2d(-0.9, -0.9)
glVertex2d(0.9, -0.9)
glVertex2d(0.9, 0.9)
glVertex2d(-0.9, 0.9)
glEnd()
glFlush()
...
上のコードを実行したら以下の結果となる。
それはglBegin()
がまだGL_LINE_LOOP
のてめ、色変われる部分がエッジしかない。
四角全体を色を付けたりするためにはGL_POLYGON
に変更する。
GL_POLYGON
に変更と実行すると以下の結果となる。
色も、各頂点に指定できる。例えば以下のように指定しよう。
...
glBegin(GL_POLYGON)
glColor3d(1.0, 1.0, 0.0)
glVertex2d(-0.9, -0.9)
glColor3d(1.0, 0.0, 1.0)
glVertex2d(0.9, -0.9)
glColor3d(0.0, 1.0, 1.0)
glVertex2d(0.9, 0.9)
glColor3d(1.0, 1.0, 1.0)
glVertex2d(-0.9, 0.9)
glEnd()
...
上のコードを実行したら以下の結果となる。
4. プロジェクション
3D図形を2D画面に表示するためには、いくつかプロジェクション方法はあります。
しかしメインとしたはOrtographic ProjectionとPerspective Projection。
Ortographic Projectionまたはパラレルプロジェクションには2つに似た図形を遠いさを関係なく、同じサイズに見える。
Perspective Projectionには2つに似た図形を遠い方の図形は小さいに見える。
図参考:https://www.labri.fr/perso/nrougier/python-opengl/#id14
4.1 Orthographic Projection
以前に設定した四角は、各エッジに同じサイズね設定しますが、以下のような640×480の画面に表示されたのは上下と左右のエッジのサイズが違うように見える。
修正するためには今回Projectionを設定しましょう。
import glfw
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
def projection_matrix(w, h):
glViewport(0, 0, w, h)
glLoadIdentity()
glOrtho(-w/200, w/200, -h/200, h/200, -1.0, 1.0)
...
glfw.make_context_current(window)
projection_matrix(WINDOW_WIDTH, WINDOW_HEIGHT)
...
4.2 Perspective Projection
...
def projection_matrix(w, h):
glViewport(0, 0, w, h)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(35.0, w/h, 1.0, 200.0)
glMatrixMode(GL_MODELVIEW)
...
...
while not glfw.window_should_close(window):
glClearColor(0.8, 0.8, 0.8, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
glLoadIdentity()
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
...
5. 3D図形
5.1 3D図形を描く
次は3D図形を描きましょう。以前はglVertex2d()
のコマンドで、2Dポイントあるいはvertexを描けるようにしますが、3Dポイントを描くためには glVertex3d()
のコマンドで描けます。
早速にコードを書くのも難しいので、とりあずポイントの座標を想像しましょう。
以下のようにキューブの中心が(0,0,0)の座標に設定しましょう。
では、各ポイントの座標を以下のように措定できます。
VERTEXES = [
[-1, 1, -1], # A
[1, 1, -1 ], # B
[1, -1, -1], # C
[-1, -1, -1], # D
[-1, 1, 1], # E
[1, 1, 1], # F
[1, -1, 1], # G
[-1, -1, 1], # H
]
キューブでは1つのループで描けないので、今回はGL_LINES
で各ペアのポイントを線を描く。
上のキューブの画像を参考にして、以下のようにペアを措定する。
EDGES = [
[0, 1], # AB
[1, 2], # BC
[2, 3], # CD
[3, 0], # DA
[4, 5], # EF
[5, 6], # FG
[6, 7], # GH
[7, 4], # HE
[0, 4], # AE
[1, 5], # BF
[2, 6], # CG
[3, 7] # DH
]
これで3Dキューブを以下のコードで描ける。
import glfw
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
# 1.0とは大きいので、0.2に変更しましょう。
VERTEXES = [
[-0.2, 0.2, -0.2], # A
[0.2, 0.2, -0.2], # B
[0.2, -0.2, -0.2], # C
[-0.2, -0.2, -0.2], # D
[-0.2, 0.2, 0.2], # E
[0.2, 0.2, 0.2], # F
[0.2, -0.2, 0.2], # G
[-0.2, -0.2, 0.2], # H
]
EDGES = [
[0, 1], # AB
[1, 2], # BC
[2, 3], # CD
[3, 0], # DA
[4, 5], # EF
[5, 6], # FG
[6, 7], # GH
[7, 4], # HE
[0, 4], # AE
[1, 5], # BF
[2, 6], # CG
[3, 7] # DH
]
def main():
if not glfw.init():
return
window = glfw.create_window(640, 480, "Hello World", None, None)
if not window:
glfw.terminate()
return
glfw.make_context_current(window)
while not glfw.window_should_close(window):
glClear(GL_COLOR_BUFFER_BIT)
glClearColor(0.8, 0.8, 0.8, 1.0)
glBegin(GL_LINE)
glColor3d(0.0, 0.0, 0.0)
for edge in EDGES:
for vx in edge:
glVertex3dv(VERTEXES[vx])
# glVertex3d()を利用する場合はいかとなります
# glVertex3d(VERTEXES[vx][0], VERTEXES[vx][1], VERTEXES[vx][2])
glEnd()
glFlush()
glfw.swap_buffers(window)
glfw.poll_events()
glfw.terminate()
if __name__ == "__main__":
main()
上のコード実行すると、以下のように普通の四角の図形しか見えてないですね。
それは正面の視点から見えるです。今後はちゃんとまともなキューブを見えるようにするため、表示方法を学ぶ。
6. トランスフォーメーション (図形変換)
import glfw
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
lx = 0.4
VERTEXES = [
[-lx, lx, lx], # A
[lx, lx, lx], # B
[lx, -lx, lx], # C
[-lx, -lx, lx], # D
[-lx, lx, -lx], # E
[lx, lx, -lx], # F
[lx, -lx, -lx], # G
[-lx, -lx, -lx], # H
]
EDGES = [
[0, 1],
[1, 2],
[2, 3],
[3, 0],
[4, 5],
[5, 6],
[6, 7],
[7, 4],
[0, 4],
[1, 5],
[2, 6],
[3, 7]
]
def set_view(w, h):
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(35.0, w/h, 1.0, 200.0)
glMatrixMode(GL_MODELVIEW)
def main():
if not glfw.init():
return
window = glfw.create_window(WINDOW_WIDTH, WINDOW_HEIGHT, "Hello World", None, None)
if not window:
glfw.terminate()
return
glfw.make_context_current(window)
set_view(WINDOW_WIDTH, WINDOW_HEIGHT)
r = 1
while not glfw.window_should_close(window):
glClear(GL_COLOR_BUFFER_BIT)
glClearColor(0.8, 0.8, 0.8, 1.0)
glLoadIdentity()
gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
glRotated(r, 0.0, 1.0, 0.0)
r += 1
if r > 360: r=0
glBegin(GL_LINES)
glColor3d(0.0, 0.0, 0.0)
for edge in EDGES:
for vx in edge:
glVertex3dv(VERTEXES[vx])
glEnd()
glFlush()
glfw.swap_buffers(window)
glfw.poll_events()
glfw.terminate()
if __name__ == "__main__":
main()
6. 3D空間変換
7. 3Dファイルフォーマット
7.1 OBJファイルをレンダリングする
...
def load_obj(file_path):
vertices = []
faces = []
with open(file_path, 'r') as file:
for line in file:
if line.startswith('v '): # Vertex
vertices.append(list(map(float, line.strip().split()[1:])))
elif line.startswith('f '): # Face
face = [int(idx.split('/')[0]) - 1 for idx in line.strip().split()[1:]]
faces.append(face)
return np.array(vertices, dtype=np.float32), np.array(faces, dtype=np.int32)
vertices, faces = load_obj("Star/Star_01.obj")
...
...
glBegin(GL_TRIANGLES)
glColor3d(1.0, 1.0, 0.0)
for face in faces:
for vertex_idx in face:
glVertex3fv(vertices[vertex_idx])
glEnd()
...