"""
tkopenglモジュール
概要:
このモジュールは、OpenGL (PyOpenGLとFreeGLUT) を使用して3Dグラフィックを描画するためのユーティリティ関数とクラスを提供します。
基本的な形状の描画、テキスト表示、カメラ操作、イベント処理などをサポートしています。
関連リンク:
:doc:`tkopengl_usage`
"""
import os
import sys
import math
import numpy as np
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
[ドキュメント]
def draw_text(text, x, y, z, sx, sy, sz, R = 0.5, G = 0.5, B = 0.5):
"""
概要:
3D空間にテキストを描画します。
詳細説明:
指定された3D座標にGLUTのビットマップフォント (GLUT_BITMAP_HELVETICA_18) を使用してテキストを描画します。
テキストの色とスケールを指定できます。
:param text: str - 描画するテキスト文字列。
:param x: float - テキストのX座標。
:param y: float - テキストのY座標。
:param z: float - テキストのZ座標。
:param sx: float - テキストのX方向のスケール。
:param sy: float - テキストのY方向のスケール。
:param sz: float - テキストのZ方向のスケール。
:param R: float - テキストの赤成分 (0.0-1.0)。デフォルトは0.5。
:param G: float - テキストの緑成分 (0.0-1.0)。デフォルトは0.5。
:param B: float - テキストの青成分 (0.0-1.0)。デフォルトは0.5。
:returns: None
"""
glColor3f(R, G, B)
glRasterPos3f(x, y, z)
glPushMatrix()
# glTranslatef(0, 0, 0)
glScalef(sx, sy, sz)
for ch in text:
glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, ord(ch))
glPopMatrix()
[ドキュメント]
def draw_sphere(x, y, z, radius,
R = 1.0, G = 0.0, B = 0.0, alpha = 1.0, slices = 10, stacks = 10):
"""
概要:
3D空間に球体を描画します。
詳細説明:
指定された中心座標と半径を持つ球体を描画します。色と透明度を設定でき、
マテリアル設定を一時的に変更して描画し、元のマテリアル設定に戻します。
:param x: float - 球体の中心のX座標。
:param y: float - 球体の中心のY座標。
:param z: float - 球体の中心のZ座標。
:param radius: float - 球体の半径。
:param R: float - 球体の赤成分 (0.0-1.0)。デフォルトは1.0。
:param G: float - 球体の緑成分 (0.0-1.0)。デフォルトは0.0。
:param B: float - 球体の青成分 (0.0-1.0)。デフォルトは0.0。
:param alpha: float - 球体の透明度 (0.0-1.0)。デフォルトは1.0 (不透明)。
:param slices: int - 球体の縦方向の分割数。デフォルトは10。
:param stacks: int - 球体の横方向の分割数。デフォルトは10。
:returns: None
"""
# glColor3f(R, G, B)
current_material = glGetFloatv(GL_CURRENT_COLOR)
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, [R, G, B, alpha])
glPushMatrix()
glTranslatef(x, y, z)
glutSolidSphere(radius, slices, stacks)
glPopMatrix()
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, current_material)
[ドキュメント]
def draw_cylinder(x0, y0, z0, x1, y1, z1, radius = 0.1,
R = 0.5, G = 0.5, B = 0.5, alpha = 1.0, slices = 10, stacks = 10):
"""
概要:
3D空間に円柱を描画します。
詳細説明:
開始点 (x0, y0, z0) から終了点 (x1, y1, z1) までを結ぶ円柱を描画します。
円柱の方向は2点間のベクトルから計算され、適切に回転して配置されます。
色と透明度を設定でき、マテリアル設定を一時的に変更して描画し、元のマテリアル設定に戻します。
:param x0: float - 円柱の開始点のX座標。
:param y0: float - 円柱の開始点のY座標。
:param z0: float - 円柱の開始点のZ座標。
:param x1: float - 円柱の終了点のX座標。
:param y1: float - 円柱の終了点のY座標。
:param z1: float - 円柱の終了点のZ座標。
:param radius: float - 円柱の半径。デフォルトは0.1。
:param R: float - 円柱の赤成分 (0.0-1.0)。デフォルトは0.5。
:param G: float - 円柱の緑成分 (0.0-1.0)。デフォルトは0.5。
:param B: float - 円柱の青成分 (0.0-1.0)。デフォルトは0.5。
:param alpha: float - 円柱の透明度 (0.0-1.0)。デフォルトは1.0 (不透明)。
:param slices: int - 円柱の側面を構成する縦方向の分割数。デフォルトは10。
:param stacks: int - 円柱の側面を構成する横方向の分割数。デフォルトは10。
:returns: None
"""
# glColor3f(R, G, B)
current_material = glGetFloatv(GL_CURRENT_COLOR)
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, [R, G, B, alpha])
# Calculate direction vector
direction = np.array([x1 - x0, y1 - y0, z1 - z0])
length = np.linalg.norm(direction)
if length == 0.0: return
direction /= length
# Calculate rotation axis and angle
up = np.array([0, 0, 1])
axis = np.cross(up, direction)
angle = np.arccos(np.dot(up, direction)) * 180 / np.pi
# Draw cylinder (shaft of the arrow)
glPushMatrix()
glTranslatef(x0, y0, z0)
glRotatef(angle, axis[0], axis[1], axis[2])
gluCylinder(gluNewQuadric(), radius, radius, length, slices, stacks)
glPopMatrix()
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, current_material)
[ドキュメント]
def draw_arrow(x0, y0, z0, x1, y1, z1, radius = 0.1, arrow_h = 0.2, arrow_r = 0.2,
R = 1.0, G = 0.0, B = 0.0, alpha = 0.5, slices = 10, stacks = 10):
"""
概要:
3D空間に矢印を描画します。
詳細説明:
開始点 (x0, y0, z0) から終了点 (x1, y1, z1) までを結ぶ矢印を描画します。
矢印は円柱の軸と円錐の頭部で構成されます。方向ベクトルから回転軸と角度を計算し、
適切に配置されます。色と透明度を設定でき、マテリアル設定を一時的に変更して描画し、
元のマテリアル設定に戻します。
:param x0: float - 矢印の開始点のX座標。
:param y0: float - 矢印の開始点のY座標。
:param z0: float - 矢印の開始点のZ座標。
:param x1: float - 矢印の終了点のX座標。
:param y1: float - 矢印の終了点のY座標。
:param z1: float - 矢印の終了点のZ座標。
:param radius: float - 矢印の軸(円柱)の半径。デフォルトは0.1。
:param arrow_h: float - 矢印の頭部(円錐)の高さ。デフォルトは0.2。
:param arrow_r: float - 矢印の頭部(円錐)の底面の半径。デフォルトは0.2。
:param R: float - 矢印の赤成分 (0.0-1.0)。デフォルトは1.0。
:param G: float - 矢印の緑成分 (0.0-1.0)。デフォルトは0.0。
:param B: float - 矢印の青成分 (0.0-1.0)。デフォルトは0.0。
:param alpha: float - 矢印の透明度 (0.0-1.0)。デフォルトは0.5。
:param slices: int - 円柱および円錐の側面を構成する縦方向の分割数。デフォルトは10。
:param stacks: int - 円柱および円錐の側面を構成する横方向の分割数。デフォルトは10。
:returns: None
"""
# glColor3f(R, G, B)
current_material = glGetFloatv(GL_CURRENT_COLOR)
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, [R, G, B, alpha])
# Calculate direction vector
direction = np.array([x1 - x0, y1 - y0, z1 - z0])
length = np.linalg.norm(direction)
direction /= length
# Calculate rotation axis and angle
up = np.array([0, 0, 1])
axis = np.cross(up, direction)
angle = np.arccos(np.dot(up, direction)) * 180 / np.pi
# Draw cylinder (shaft of the arrow)
glPushMatrix()
glTranslatef(x0, y0, z0)
glRotatef(angle, axis[0], axis[1], axis[2])
gluCylinder(gluNewQuadric(), radius, radius, length - arrow_h, slices, stacks)
glPopMatrix()
# Draw cone (head of the arrow)
glPushMatrix()
glTranslatef(x1, y1, z1)
glRotatef(angle, axis[0], axis[1], axis[2])
glTranslatef(0, 0, -arrow_h)
gluCylinder(gluNewQuadric(), arrow_r, 0, arrow_h, slices, slices)
glPopMatrix()
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, current_material)
[ドキュメント]
class tkOpenGL():
"""
概要:
OpenGLウィンドウの初期化、イベント処理、描画ロジックを管理するクラス。
詳細説明:
GLUT (FreeGLUT) を使用してOpenGLコンテキストを設定し、3Dシーンを描画するための基本機能を提供します。
マウスによる視点操作(回転、拡大縮小)やキーボードによるオブジェクト操作に対応しています。
:param app: Any - アプリケーションインスタンス (未使用だが引数として存在)。
:param center: list[float] - シーンの中心座標 [cx, cy, cz]。
:param fcenter: list[float] - シーンの焦点座標 [fcx, fcy, fcz] (未使用だが引数として存在)。
:param title: bytes - ウィンドウのタイトル。デフォルトは b"Crystal Structure"。
:param window_w: int - ウィンドウの初期幅。デフォルトは800。
:param window_h: int - ウィンドウの初期高さ。デフォルトは600。
:param color_bg: list[float] - 背景色 [R, G, B, A]。デフォルトは [1.0, 1.0, 1.0, 1.0] (白)。
:param nslices: int - 描画されるオブジェクトの分割数 (スライス)。デフォルトは32。
:param nstacks: int - 描画されるオブジェクトの分割数 (スタック)。デフォルトは32。
:param debug: bool - デバッグモードの有効/無効。Trueの場合、デバッグ出力が増える。デフォルトはFalse。
"""
def __init__(self, app = None, center = [0, 0, 0], fcenter = [0.0, 0.0, 0.0],
title = b"Crystal Structure",
window_w = 800, window_h = 600, color_bg = [1.0, 1.0, 1.0, 1.0],
nslices = 32, nstacks = 32,
debug = False):
"""
概要:
tkOpenGLクラスのコンストラクタ。
詳細説明:
OpenGLウィンドウの初期設定、カメラの初期位置、およびマウス/キーボード操作のための
状態変数を初期化します。
:param app: Any - アプリケーションインスタンス (未使用だが引数として存在)。
:param center: list[float] - シーンの中心座標 [cx, cy, cz]。
:param fcenter: list[float] - シーンの焦点座標 [fcx, fcy, fcz] (未使用だが引数として存在)。
:param title: bytes - ウィンドウのタイトル。デフォルトは b"Crystal Structure"。
:param window_w: int - ウィンドウの初期幅。デフォルトは800。
:param window_h: int - ウィンドウの初期高さ。デフォルトは600。
:param color_bg: list[float] - 背景色 [R, G, B, A]。デフォルトは [1.0, 1.0, 1.0, 1.0] (白)。
:param nslices: int - 描画されるオブジェクトの分割数 (スライス)。デフォルトは32。
:param nstacks: int - 描画されるオブジェクトの分割数 (スタック)。デフォルトは32。
:param debug: bool - デバッグモードの有効/無効。Trueの場合、デバッグ出力が増える。デフォルトはFalse。
:returns: None
"""
self.center = center
self.fcenter = fcenter
self.width = window_w
self.height = window_h
self.title = title
self.debug = debug
self.print_level = 1 if debug else 0
# 状態変数
self.model_x = 0.0
self.model_y = 0.0
self.model_z = -10.0
self.mouse_down = False
self.mouse_x = 0.0
self.mouse_y = 0.0
self.angle_x = 0.0
self.angle_y = 0.0
self.angle_z = 0.0
self.scale = 1.0
self.animation_running = False
# 描画設定
self.color_bg = color_bg
[ドキュメント]
def init_canvas(self, color_bg = None, title = None):
"""
概要:
OpenGLの描画キャンバスを初期化します。
詳細説明:
GLUTウィンドウを作成し、基本的なOpenGLレンダリング設定(背景色、深度テスト、
ブレンド、シェーディングモデル、ライト、マテリアル)を初期化します。
このメソッドは、描画を開始する前に一度呼び出す必要があります。
:param color_bg: list[float], optional - 背景色 [R, G, B, A]。Noneの場合、インスタンスの`color_bg`を使用。
:param title: bytes, optional - ウィンドウのタイトル。Noneの場合、インスタンスの`title`を使用。
:returns: None
"""
if color_bg is None: color_bg = self.color_bg
if title is None: title = self.title
glutInit(sys.argv)
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
glutInitWindowSize(self.width, self.height)
self.hWindow = glutCreateWindow(title)
print("window handle: ", self.hWindow)
glClearColor(*color_bg)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable(GL_DEPTH_TEST)
# glShadeModel(GL_FLAT)
glShadeModel(GL_SMOOTH)
# ライトの設定
glEnable(GL_LIGHTING) # ライティングの有効化
glEnable(GL_LIGHT0) # ライト0の有効化
light_diffuse = [1.0, 1.0, 1.0, 1.0] # 拡散光の色
light_specular = [1.0, 1.0, 1.0, 1.0] # 鏡面反射光の色
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse)
glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular)
# マテリアルの設定
mat_ambient = [0.2, 0.2, 0.2, 1.0] # 環境光反射成分
mat_diffuse = [1.0, 1.0, 1.0, 1.0] # 拡散反射成分
mat_specular = [1.0, 1.0, 1.0, 1.0] # 鏡面反射成分
mat_shininess = [50.0] # 輝き
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient)
glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse)
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular)
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess)
self.init_projection(self.width, self.height)
# glEnable(GL_DEPTH_TEST)
[ドキュメント]
def save_image(self, filename, width, height):
"""
概要:
現在のOpenGLフレームバッファの内容を画像ファイルとして保存します。
詳細説明:
`glReadPixels` を使用して現在のウィンドウの内容をピクセルデータとして読み込み、
Pillow (PIL) ライブラリを使用して指定されたファイル名で画像として保存します。
読み込んだピクセルデータは上下反転して保存されます。
:param filename: str - 保存する画像ファイルのパス。
:param width: int - 保存する画像の幅。
:param height: int - 保存する画像の高さ。
:returns: None
"""
glPixelStorei(GL_PACK_ALIGNMENT, 1)
data = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
image = Image.frombytes("RGB", (width, height), data)
image = image.transpose(Image.FLIP_TOP_BOTTOM)
image.save(filename)
[ドキュメント]
def camera_distance(self, W, D, H, fov_y):
"""
概要:
オブジェクト全体を視野に収めるためのカメラ距離を計算します。
詳細説明:
与えられたオブジェクトの幅 (W)、奥行き (D)、高さ (H)、および垂直視野角 (fov_y) に基づいて、
オブジェクト全体がカメラの視野に収まるように必要なカメラとオブジェクト間の距離を計算します。
:param W: float - オブジェクトの幅。
:param D: float - オブジェクトの奥行き。
:param H: float - オブジェクトの高さ。
:param fov_y: float - 垂直視野角 (度数)。
:returns: float - カメラとオブジェクト間の距離。
"""
diagonal = math.sqrt(W**2 + H**2 + D**2)
fov_rad = math.radians(fov_y)
distance = diagonal / (2.0 * math.tan(fov_rad / 2.0))
return distance
[ドキュメント]
def init_projection(self, width, height):
"""
概要:
投影行列とモデルビュー行列を設定し、カメラの位置を初期化します。
詳細説明:
ウィンドウの幅と高さに基づいてビューポートを設定し、`gluPerspective` でパースペクティブ投影を定義します。
`gluLookAt` を使用してカメラの位置、注視点、上方向ベクトルを設定し、シーンの初期視点を確立します。
また、光源 (GL_LIGHT0) の位置も設定します。
:param width: int - ウィンドウの幅。
:param height: int - ウィンドウの高さ。
:returns: None
"""
fov_y = 45.0
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glViewport(0, 0, width, height)
gluPerspective(fov_y, width / height, 0.1, 100.0)
# gluOrtho2D(0, width, 0, height)
camera_distance = self.camera_distance(*(self.center * 2 + 3.0), fov_y)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glTranslatef(-self.center[0], -self.center[1], -self.center[2])
gluLookAt(*np.array([-0.5, 2.0, 0.2]) * camera_distance, # カメラの位置
0.0, 0.0, 0.0, # 注視点の位置
0.0, 0.0, 1.0) # 上方向のベクトル
glRotatef(180, 0, 0, 1)
# glRotatef(90, 1, 0, 0)
light_position = [3.0, -camera_distance, 5.0, 0.0] # ライトの位置
glLightfv(GL_LIGHT0, GL_POSITION, light_position)
# callback for mouse/keyboard
[ドキュメント]
def reshape(self, w, h):
"""
概要:
ウィンドウのリサイズ時に呼び出されるコールバック関数。
詳細説明:
ウィンドウの新しい幅 (w) と高さ (h) を受け取り、
それに応じてビューポートと投影行列を再設定し、シーンを適切に表示します。
:param w: int - 新しいウィンドウの幅。
:param h: int - 新しいウィンドウの高さ。
:returns: None
"""
self.width = w
self.height = h
self.init_projection(w, h)
[ドキュメント]
def mouse(self, button, state, x, y):
"""
概要:
マウスイベント(ボタンの押下/離上)を処理するコールバック関数。
詳細説明:
GLUT_LEFT_BUTTONが押されたときに、マウスダウンフラグをTrueに設定し、
現在のマウス座標を記録します。ボタンが離されたときはフラグをFalseに戻します。
:param button: int - 押された/離されたマウスボタン。
:param state: int - マウスボタンの状態 (GLUT_DOWN または GLUT_UP)。
:param x: int - イベント発生時のマウスのX座標。
:param y: int - イベント発生時のマウスのY座標。
:returns: None
"""
if button == GLUT_LEFT_BUTTON:
if state == GLUT_DOWN:
self.mouse_down = True
self.mouse_x = x
self.mouse_y = y
elif state == GLUT_UP:
self.mouse_down = False
[ドキュメント]
def motion(self, x, y):
"""
概要:
マウスドラッグイベントを処理するコールバック関数。
詳細説明:
マウスがドラッグされている間、現在のマウス座標と前回の座標の差に基づいて、
モデルのX軸とZ軸周りの回転角度を更新します。
更新後、シーンの再描画を要求します。
:param x: int - 現在のマウスのX座標。
:param y: int - 現在のマウスのY座標。
:returns: None
"""
if self.mouse_down:
self.angle_x -= (y - self.mouse_y)
self.angle_z += (x - self.mouse_x)
self.mouse_x = x
self.mouse_y = y
glutPostRedisplay()
if self.debug:
print(f"mouse: ({self.mouse_x}, {self.mouse_y})")
print(f"angle: ({self.angle_x}, {self.angle_y}, {self.angle_z})")
[ドキュメント]
def keyboard(self, key, x, y):
"""
概要:
キーボードイベントを処理するコールバック関数。
詳細説明:
特定のキーが押されたとき、モデルのスケールや位置 (X, Y, Z軸方向) を調整します。
たとえば、'u'/'U' で拡大、'd'/'D' で縮小、'h'/'L' でX軸移動、'j'/'k' でZ軸移動、
'<'/' >' でY軸移動を行います。
変更後、シーンの再描画を要求します。
:param key: bytes - 押されたキーのASCIIコード (bytes形式)。
:param x: int - イベント発生時のマウスのX座標。
:param y: int - イベント発生時のマウスのY座標。
:returns: None
"""
if key == b'u' or key == b'U':
self.scale += 0.1
elif key == b'd' or key == b'D':
self.scale -= 0.1
if self.scale < 0.1: self.scale = 0.1
elif key == b'h' or key == b'H':
self.model_x -= 0.1
elif key == b'l' or key == b'L':
self.model_x += 0.1
elif key == b'j' or key == b'J':
self.model_z -= 0.1
elif key == b'k' or key == b'K':
self.model_z += 0.1
elif key == b'<' or key == b',':
self.model_y -= 2.0
elif key == b'>' or key == b'.':
self.model_y += 2.0
glutPostRedisplay()
if self.debug: print(f"scale: {self.scale}")
[ドキュメント]
def key_pressed(key, x, y):
"""
概要:
キーが押されたときに呼び出されるコールバック関数。
詳細説明:
スペースキー (`b' '`) が押された場合に、アニメーションの実行状態 (`animation_running`) を
切り替えます。
※このメソッドはクラスメソッドとして実装されていますが、`self` が引数に含まれていません。
実行時にエラーが発生する可能性がありますが、既存のコードを変更できないためこのままです。
:param key: bytes - 押されたキーのASCIIコード (bytes形式)。
:param x: int - イベント発生時のマウスのX座標。
:param y: int - イベント発生時のマウスのY座標。
:returns: None
"""
if key == b' ': # スペースキーを押すとアニメーションの開始/停止を切り替える
self.animation_running = not self.animation_running
[ドキュメント]
def start_animation():
"""
概要:
アニメーションを開始します。
詳細説明:
`animation_running` フラグをTrueに設定し、アニメーションループの実行を許可します。
※このメソッドはクラスメソッドとして実装されていますが、`self` が引数に含まれていません。
実行時にエラーが発生する可能性がありますが、既存のコードを変更できないためこのままです。
:returns: None
"""
self.animation_running = True
[ドキュメント]
def stop_animation(self):
"""
概要:
アニメーションを停止します。
詳細説明:
`animation_running` フラグをFalseに設定し、アニメーションループの実行を停止します。
:returns: None
"""
self.animation_running = False
if __name__ == "__main__":
pass