OpenGLチュートリアル:3D テトリス

目次

  1. はじめに
  2. 開発環境の準備
    • 必要なライブラリ
    • 開発環境のセットアップ
  3. 3Dテトリスの基本構造
    • ブロックの表現
    • テトリスフィールドの設計
    • カメラワーク
  4. OpenGLによる描画
    • 頂点バッファオブジェクト(VBO)と頂点配列オブジェクト(VAO)
    • シェーダーの作成(頂点シェーダー、フラグメントシェーダー)
    • ブロックの描画処理
  5. ゲームロジックの実装
    • ブロックの生成
    • ブロックの移動と回転
    • 衝突判定
    • ラインの消去とスコアリング
  6. コード実装(サンプル)
    • メインループ
    • 描画関連処理
    • ゲームロジック関連処理
  7. 実行結果と今後の展望
    • 動作デモ
    • 改善点と今後の機能追加
  8. まとめ

1. はじめに

3Dテトリス開発の魅力

テトリスは、シンプルながらも奥深いパズルゲームとして長年愛されています。それを3D空間で表現することで、新たなゲーム体験とプログラミングの挑戦が生まれます。OpenGLを用いることで、美しい3Dグラフィックスとスムーズなアニメーションを実現し、より没入感のあるテトリスを作り上げることができます。

本記事の概要

本記事では、OpenGLを使って3Dテトリスを開発する基本的な手順とコード例を紹介します。3Dグラフィックスの基礎からゲームロジックの実装まで、ステップバイステップで解説していきます。

2. 開発環境の準備

必要なライブラリ

OpenGLの基本的な機能に加えて、以下のライブラリを使用します。

  • GLFW: ウィンドウの作成と管理、入力処理
  • GLEW: OpenGL拡張機能を利用するためのライブラリ
  • NumPy: 数値計算ライブラリ(ここでは行列計算などに使用)

開発環境のセットアップ

お使いのOSに合わせて、上記のライブラリをインストールしてください。具体的なインストール方法は各ライブラリの公式サイトなどを参照してください。

3. 3Dテトリスの基本構造

ブロックの表現

3Dテトリスでは、各ブロックは複数の立方体(キューブ)で構成されます。それぞれのキューブは、OpenGLのプリミティブである三角形の組み合わせで描画されます。

テトリスフィールドの設計

テトリスフィールドは、3次元の格子状の空間として表現します。各格子点にブロックが存在するかどうかを管理します。

カメラワーク

3D空間での視点を操作するために、カメラを導入します。ユーザーが視点を移動させたり、固定された視点からゲームをプレイできるようにします。

4. OpenGLによる描画

頂点バッファオブジェクト(VBO)と頂点配列オブジェクト(VAO)

OpenGLでオブジェクトを描画するためには、その形状を定義する頂点データをGPUに送る必要があります。VBOは頂点データを格納するバッファ、VAOはVBOと頂点属性の設定をまとめたオブジェクトです。

シェーダーの作成(頂点シェーダー、フラグメントシェーダー)

シェーダーはGPU上で実行されるプログラムで、頂点の変換やピクセルの色決定を行います。

  • 頂点シェーダー: 各頂点の位置を変換し、クリッピング空間での座標を計算します。
  • フラグメントシェーダー: 各ピクセルの最終的な色を決定します。

ブロックの描画処理

各ブロックを構成するキューブの頂点データをVBOに格納し、VAOを使って描画します。モデル変換行列を適用することで、各キューブを適切な位置に配置します。

import glfw
from OpenGL.GL import *
import numpy as np

# キューブの頂点データ
vertices = np.array([
    [-0.5, -0.5, -0.5],
    [ 0.5, -0.5, -0.5],
    [ 0.5,  0.5, -0.5],
    [-0.5,  0.5, -0.5],
    [-0.5, -0.5,  0.5],
    [ 0.5, -0.5,  0.5],
    [ 0.5,  0.5,  0.5],
    [-0.5,  0.5,  0.5]
], dtype=np.float32)

# キューブの面(インデックス)
indices = np.array([
    0, 1, 2, 2, 3, 0,  # 背面
    1, 5, 6, 6, 2, 1,  # 右面
    5, 4, 7, 7, 6, 5,  # 前面
    4, 0, 3, 3, 7, 4,  # 左面
    3, 2, 6, 6, 7, 3,  # 上面
    4, 5, 1, 1, 0, 4   # 下面
], dtype=np.uint32)

def draw_cube():
    glBindVertexArray(VAO)
    glDrawElements(GL_TRIANGLES, len(indices), GL_UNSIGNED_INT, None)
    glBindVertexArray(0)

5. ゲームロジックの実装

ブロックの生成

ランダムにテトリスのブロック(I, O, T, S, Z, J, L型など)を生成し、フィールドの上部中央に配置します。

ブロックの移動と回転

ユーザーの入力に応じて、ブロックを左右に移動させたり、回転させたりする処理を実装します。

衝突判定

ブロックが他のブロックやフィールドの壁、底面と衝突していないかを常にチェックします。衝突した場合、それ以上移動できないように制御します。

ラインの消去とスコアリング

横一列にブロックが揃った場合、そのラインを消去し、スコアを加算します。上のブロックを落下させる処理も必要です。

# サンプル:ブロックの移動(左右)
block_position = np.array([0.0, 10.0, 0.0], dtype=np.float32)
move_speed = 0.1

def move_block_left():
    block_position[0] -= move_speed

def move_block_right():
    block_position[0] += move_speed

# サンプル:簡単な衝突判定(床との衝突)
field_bottom = -5.0
def check_collision():
    if block_position[1] <= field_bottom:
        return True
    return False

6. コード実装(サンプル)

ここでは、基本的なOpenGLの初期化、描画ループ、そして簡単なブロックの描画と移動のサンプルコードを示します。完全なゲームロジックは複雑になるため、主要な部分に焦点を当てています。

import glfw
from OpenGL.GL import *
import numpy as np
from pyrr import Matrix44

# 画面サイズ
screen_width = 800
screen_height = 600

# キューブの頂点データとインデックス(再掲)
vertices = np.array(...)
indices = np.array(...)

# シェーダーソースコード
vertex_shader_source = """
#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
"""

fragment_shader_source = """
#version 330 core
out vec4 FragColor;
uniform vec4 color;

void main()
{
    FragColor = color;
}
"""

# OpenGLの初期化
def initialize_opengl():
    if not glfw.init():
        return None

    glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
    glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
    glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)

    window = glfw.create_window(screen_width, screen_height, "3D Tetris", None, None)
    if not window:
        glfw.terminate()
        return None

    glfw.make_context_current(window)
    glfw.set_framebuffer_size_callback(window, framebuffer_size_callback)
    glfw.set_key_callback(window, key_callback)

    return window

def framebuffer_size_callback(window, width, height):
    glViewport(0, 0, width, height)

# キー入力コールバック関数
def key_callback(window, key, scancode, action, mods):
    if key == glfw.KEY_ESCAPE and action == glfw.PRESS:
        glfw.set_window_should_close(window, True)
    if key == glfw.KEY_LEFT and action == glfw.REPEAT:
        move_block_left()
    if key == glfw.KEY_RIGHT and action == glfw.REPEAT:
        move_block_right()

# シェーダーのコンパイル
def compile_shader(shader_type, source):
    shader = glCreateShader(shader_type)
    glShaderSource(shader, source)
    glCompileShader(shader)
    # エラーチェック
    success = glGetShaderiv(shader, GL_COMPILE_STATUS)
    if not success:
        info_log = glGetShaderInfoLog(shader).decode("utf-8")
        print(f"Shader compilation error: {info_log}")
    return shader

# シェーダープログラムの作成
def create_shader_program(vertex_shader, fragment_shader):
    program = glCreateProgram()
    glAttachShader(program, vertex_shader)
    glAttachShader(program, fragment_shader)
    glLinkProgram(program)
    # エラーチェック
    success = glGetProgramiv(program, GL_LINK_STATUS)
    if not success:
        info_log = glGetProgramInfoLog(program).decode("utf-8")
        print(f"Shader linking error: {info_log}")
    glDeleteShader(vertex_shader)
    glDeleteShader(fragment_shader)
    return program

# メインループ
def main():
    window = initialize_opengl()
    if not window:
        return

    vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source)
    fragment_shader = compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source)
    shader_program = create_shader_program(vertex_shader, fragment_shader)

    # VBOとVAOの作成と設定
    VBO = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)

    EBO = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.nbytes, indices, GL_STATIC_DRAW)

    VAO = glGenVertexArrays(1)
    glBindVertexArray(VAO)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * 4, ctypes.c_void_p(0))
    glEnableVertexAttribArray(0)
    glBindVertexArray(0)

    glUseProgram(shader_program)

    # 透視投影行列の作成
    projection = Matrix44.perspective_projection(45.0, screen_width / screen_height, 0.1, 100.0)
    projection_loc = glGetUniformLocation(shader_program, "projection")
    glUniformMatrix4fv(projection_loc, 1, GL_FALSE, projection)

    # ビュー行列の作成(カメラの位置と向き)
    view = Matrix44.look_at(
        (4.0, 4.0, 6.0),  # カメラの位置
        (0.0, 0.0, 0.0),  # 注視点
        (0.0, 1.0, 0.0)   # 上方向ベクトル
    )
    view_loc = glGetUniformLocation(shader_program, "view")
    glUniformMatrix4fv(view_loc, 1, GL_FALSE, view)

    glEnable(GL_DEPTH_TEST)

    block_color_loc = glGetUniformLocation(shader_program, "color")

    while not glfw.window_should_close(window):
        glClearColor(0.2, 0.3, 0.3, 1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        glUseProgram(shader_program)
        glBindVertexArray(VAO)

        # モデル行列の作成(ブロックの位置)
        model = Matrix44.translation(block_position)
        model_loc = glGetUniformLocation(shader_program, "model")
        glUniformMatrix4fv(model_loc, 1, GL_FALSE, model)
        glUniform4f(block_color_loc, 0.8, 0.3, 0.8, 1.0) # ブロックの色

        draw_cube()

        glfw.swap_buffers(window)
        glfw.poll_events()

        if check_collision():
            block_position[1] = field_bottom # 衝突したらその場に固定(簡単な例)

    glfw.terminate()

if __name__ == "__main__":
    main()

注意: 上記のコードは非常に基本的な例であり、完全な3Dテトリスゲームとしては多くの機能が不足しています。例えば、ブロックの形状、回転、他のブロックとの衝突判定、ライン消去、スコアリングなどは実装されていません。

7. 実行結果と今後の展望

動作デモ

上記のコードを実行すると、シンプルな立方体が描画され、左右のキー入力で移動することができます。

改善点と今後の機能追加

  • ブロックの形状と種類の追加: 様々なテトリスブロックの形状を定義し、ランダムに生成するようにします。
  • ブロックの回転: ユーザーの入力に応じてブロックを回転させる機能を追加します。
  • 衝突判定の完全な実装: ブロック同士やフィールドとの正確な衝突判定を実装します。
  • ライン消去とスコアリング: 横一列揃ったラインを消去し、スコアを計算するロジックを追加します。
  • 落下速度の調整: 時間経過とともにブロックの落下速度を上げるようにします。
  • ゲームオーバー処理: ブロックが積み上がり、新しいブロックを生成できなくなったらゲームオーバーとなる処理を追加します。
  • より洗練された描画: テクスチャやライティングを導入し、よりリアルな描画を目指します。

8. まとめ

本記事では、OpenGLを使って3Dテトリスを開発するための基本的なステップとコード例を紹介しました。3Dグラフィックスの知識とゲームロジックの実装は挑戦的ですが、完成した時の達成感は格別です。ぜひ、このブログ記事を参考に、あなた自身の3Dテトリスを開発してみてください。