Help us understand the problem. What is going on with this article?

【#2】PythonでMinecraftを作る。~モデル描画とプレイヤー実装~

Part of pictures, articles, images on this page are copyrighted by Mojang AB.

minecraft.png

概要

世界的に超有名なサンドボックスゲーム「Minecraft」をプログラミング言語「Python」で再現するプロジェクトです。

前回の記事: 「【#1】PythonでMinecraftを作る。~下調べと設計~」

次の記事: 「【#3】PythonでMinecraftを作る。~プレイヤーの動きの改善(慣性の概念)と衝突判定~」

前置き

大変おまたせしました。
第2弾です!

今回やること

  • ゲームエンジンの選定
  • お試し実行
  • モデルの描画
  • プレイヤーの実装

ゲームエンジンの選定

流石に3Dゲームエンジンをイチから作るわけにもいかないので、Pythonにも対応している3Dゲームエンジン(ライブラリ)を探しました。

その中でも比較的利用しやすく、綺麗な描画ができそうなものを選定しました。

Panda3D Engine

509ed0ebac43da9c5fa01735640f7ef6.png

「Panda3D」という3Dゲームエンジンです。
プラットフォームはPythonとC++。

Panda3D使ってみた

実際に動かしてみます。

▼インストール

pip install --pre --extra-index-url https://archive.panda3d.org/ panda3d

▼ソースコード

main.py
from Renderer import engine

def main():
    _render = engine.Renderer()
    _render.run()

if __name__ == "__main__":
    main()
engine.py
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
from pandac.PandaModules import WindowProperties

class Renderer(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        props = WindowProperties()
        props.setTitle('PyCraft')
        props.setSize(1280, 720)
        self.win.requestProperties(props)

        OnscreenText(text="PyCraft ScreenText",
                     parent=None, align=TextNode.ARight,
                     fg=(1, 1, 1, 1), pos=(-0.1, 0.1), scale=.08,
                     shadow=(0, 0, 0, 0.5))

こんな感じに描画されました。

image.png

モデルを読み込んでみる

engine.py
from direct.showbase.ShowBase import ShowBase
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
from pandac.PandaModules import WindowProperties
from direct.showbase.Loader import Loader

class Renderer(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        props = WindowProperties()
        props.setTitle('PyCraft')
        props.setSize(1280, 720)
        self.win.requestProperties(props)

        OnscreenText(text="PyCraft ScreenText",
                     parent=None, align=TextNode.ARight,
                     fg=(1, 1, 1, 1), pos=(-0.1, 0.1), scale=.08,
                     shadow=(0, 0, 0, 0.5))

        self.scene = self.loader.loadModel("models/environment")
        self.scene.reparentTo(self.render)
        self.scene.setScale(1, 1, 1)
        self.scene.setPos(0, 0, 0)

        self.cube = self.loader.loadModel("models/misc/rgbCube")
        self.cube.reparentTo(self.render)
        self.cube.setScale(1, 1, 1)
        self.cube.setPos(0, 20, 0)

こんな感じになりました。
マウスでくりくり操作できるみたいです。
image.png
キューブはこんな感じ。
image.png


3D描画してみる

参考: gifで6秒動画 @panda3D

0ku48-wnqiw.gif

▼ソースコード
長くなってしまうので、コア部分のみ記載します。

engine.py
        self.world = BulletWorld()
        self.world.setGravity(Vec3(0, 0, -9.81))
        self.worldPath = self.render.attachNewNode("")

        debugNode = BulletDebugNode()
        nodePath = self.worldPath.attachNewNode(debugNode)
        nodePath.show()
        self.world.setDebugNode(debugNode)

        bodyGround = BulletRigidBodyNode()
        bodyGround.addShape(BulletBoxShape((3, 3, 0.001)))
        nodePath = self.worldPath.attachNewNode(bodyGround)
        nodePath.setPos(0, 0, -2)
        nodePath.setHpr(0, 12, 0)
        self.world.attachRigidBody(bodyGround)

        self.boxes = []
        for i in range(30):
            bodyBox = BulletRigidBodyNode()
            bodyBox.setMass(1.0)
            bodyBox.addShape(BulletBoxShape((0.5, 0.5, 0.5)))
            nodePath = self.worldPath.attachNewNode(bodyBox)
            nodePath.setPos(0, 0, 2 + i * 2)
            self.boxes.append(nodePath)
            self.world.attachRigidBody(bodyBox)

Ogre3D

「Ogre3D」 はハードウェアアクセラレーションを活用したゲームエンジンのひとつです。
C++はもちろん、Pythonや.NETにも対応している様子。
.NET上での3Dゲームの実現はかなり厳しいので、「C#でマインクラフト再現してみた!」の実現にはOgre.NETを使いたい。

ショーケースを見てみる。

こちらは、「X-Morph: Defense」とうゲームらしい。
もちろん、Ogreエンジンが使われています。素晴らしいグラフィックですね。

image.png

image.png

色々考えた結果

image.png

上記以外のゲームエンジンも加えて、クロスプラットフォームである点も考慮に入れ、一番情報の多そうな「Pyglet」を使用することにしました。
他ライブラリに依存しないこと・マルチディスプレイやマルチモニタに対応している点も申し分無いです。

Pyglet使ってみた

pyxpyg.png

ここからは、実際にPygletを使用して簡単な描画を行ってみます。

環境構築

▼インストール

pip install pyglet

▼インストール(PyCharmの場合)
メニューバーファイル設定
pygletins.png

動かしてみる

▼サンプルコード

main.py
#モジュールをインポート
from pyglet.gl import *

#表示させるウィンドウ
#widthは横幅、heightは縦幅です。
#captionはウィンドウタイトル、resizableはサイズ変更を許可するか、です。
pyglet.window.Window(width=700, height=400, caption='Hello Pyglet!', resizable=True)
pyglet.app.run()

こんな感じに表示されればOKです。
image.png

線を描画してみる

PygletではOpenGLが使われています。

▼サンプルコード

main.py
#モジュールをインポート
from pyglet.gl import *

#ウィンドウ
window = pyglet.window.Window(width=700, height=400, caption='Hello Pyglet!', resizable=True)

#ウィンドウの描画イベント
@window.event
def on_draw():
    #描画をクリア
    window.clear()
    glBegin(GL_LINES) # 線の描画を開始
    glColor3f(1, 0, 0) # R,G,B ÷ 255
    glVertex2f(0, 0) # x, y(0, 0)から
    glVertex2f(100, 100) # x, y(100, 100)まで
    glEnd() #線の描画を終わる

pyglet.app.run()

実行すると、このような感じに線が描画されます。
image.png

解説

上記ソースコードではwindowの描画イベント関数を定義し、OpenGLを活用して線を描画しています。
OpenGLでは画面左下がゼロ座標と認識されるみたいですね。

1. glBegin(GL_LINES)で線の描画の開始を宣言

2. glColor3f(1, 0, 0)で色を宣言
ここでいう色はRGB(Red,Green,Blue)です。
それぞれ通常0~255ですが、ここでは3fとある通りFloatで宣言するので、255.fで割った数を引数として渡します。
3f3つの数値をfloatで渡すという意味です。

2. glVertex2f(x, y)で線の開始地点を宣言
線の開始地点の座標を渡します。
2f2次元の座標をfloatで渡す、という意味です。

3. glVertex2f(x, y)で線の終了地点を宣言
線の終了地点の座標を渡します。
ここではウィンドウサイズ以上の数値を渡しても問題ありませんが、画面外には描画されません。

4. glEnd()で線の描画の終了を宣言

pygletdem2.png

Minecraftのブロックモデルを描画してみる

やっと本題です。

前置き

image.png
プロジェクトを実現するにあたって、最初から全てを一気に実装しようとするのは大きな間違いです。

機能A機能B機能Cを実装したいとします。
これらをいっぺんに実装してしまい、バグや不具合が発生した場合、どの機能(のどの部分)がバグや不具合を引き起こしているのか特定が困難であったり、通常よりも原因の発見までに時間を要してしまいます。

正しい手順は、
機能Aの実装 ▶ 動作確認 ▶ 機能Bの実装 ▶ 動作確認 ▶ 機能Cの実装
です。

万が一機能Cの実装後に不具合が発生した場合、機能Bから機能Cの間で不具合が発生しているんだな。
と考えることができます。より効率的です。
※それぞれの機能の互換性による不具合を除く

初めは、最小構成での実装で動作確認を行いましょう。
前置きが長い。

テクスチャの用意

とりあえず試験的に運用するので、Minecraftでもお馴染みのこのテクスチャを用意しました。
▼テクスチャ
missing16.png

いざ実装

用意したテクスチャを使用して実際に描画してみます!
ソースコードは長くなるので、Gistに掲載しました。

▼ソースコード
Gist: main.py

実行すると、以下のように描画されます。
※正常な状態です。

9a6730ede366fb49dbcb1cdd1a2c17c2.png

とりあえず、1面だけではありますが描画できました。

解説

部分部分抜き出しながら解説します。

ワールドクラス

描画するVertexを保持するBatchと呼ばれるものを定義します。

self.batch = pyglet.graphics.Batch()

そして、肝となる3次元座標を定義します。
ここでは0としてしまうと、初期カメラの場所との兼ね合い上画面外に描画されてしまい確認することができませんので、微妙にずらして定義しています。

#3次元ワールド座標を定義
x = 0.5
y = 0
z = -2

テクスチャのロードは関数を用意。
pygletのネイティブローダーを使います。
pathにはイメージのパッチを指定して下さい。

#テクスチャをロードする関数
def load_texture(self, path):
    texture = pyglet.image.load(path).get_texture()#pygletのテクスチャローダーを使います
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
    return pyglet.graphics.TextureGroup(texture)

ウィンドウクラス

お次はウィンドウクラスです。
このクラスは、pyglet.window.Windowクラスを継承しています。

「継承」とは?

初期化関数では先程定義したWorldクラスのインスタンスの初期化を行っています。

superはJavaと似てますね。

「superとは?」

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    #ワールドクラスの初期化
    self.world = World()

そして、肝心の描画を行う関数を定義。

def on_draw(self):
    #スクリーンをクリア
    self.clear()
    #ワールドは3次元なので、3D描画モードに設定
    self.render_mode_3d()
    #ワールドを描画
    self.world.draw()

ここでrender_mode_3d()関数ですが、
3D描画を行う際と2D描画を行う際にはマトリクスモードの設定が必要です。
これには投影変換モード(Projection)視野変換モード(Modelview)があり、デフォルトでは視野変換モードになっています。
これらのマトリクスモードについては、後ほど解説します。

注: モード変換処理の累積の消去(初期化)はglLoadIdentity()を使用します。


def render_mode_3d(self):
    self.render_mode_projection()
    #視野を設定
    gluPerspective(70, self.width / self.height, 0.05, 1000)
    self.render_mode_modelview()

def render_mode_2d(self):
    self.render_mode_projection()
    #描画領域 0からwindow_width、 0からwindow_height
    gluOrtho2D(0, self.width, 0, self.height)
    self.render_mode_modelview()

def render_mode_projection(self):
    glMatrixMode(GL_PROJECTION)#投影変換モード
    glLoadIdentity()#変換処理の累積を消去

def render_mode_modelview(self):
    glMatrixMode(GL_MODELVIEW)#モデリング変換モード(視野変換)
    glLoadIdentity()#変換処理の累積を消去

gluOrtho2Dでは、2Dにおける並行投射である投射変換を行います。
gluOrtho2D(left, right, top, bottom)

gl2d.png

そして、gluPerspective()では視野の設定を行います。
gluPerspective(fovY, aspect, zNear, zFar)
それぞれY軸の視野角、アスペクト(横方向の視野角)、Zの最短距離、Zの最長距離です。

glsiya.png

また、zNearzFarはスクリーン上の見た目に変化を及ぼしません。
これは、自分がスクリーンに1m近付こうが、スクリーンを1m近付けようが同じことであることと同様に考えることができます。

番外編

ここからは専門的な内容になりますので、興味の無い方は飛ばして頂いて大丈夫です。

マトリクスモード

マトリクスモードには、視界変換(GL_PROJECTION)モデリング変換(GL_MODELVIEW)があります。
※正確には、GL_TEXTUREもありますが当ソースでは扱っていないので飛ばします。

また、3次元▶2次元の変換のことをジオメトリ変換
拡大縮小・移動の際の変換のことをアフィン変換
と呼びます。

超長くなりそう & 興味のある人は一部だと思うので、外部記事に投げます。
ごめんなさい。

「完全に理解するアフィン変換」

ジオメトリ変換については詳しく解説している日本語の記事を見つけられませんでした。

プレイヤーの実装

ブロックを全面描画したいところですが、このままでは視点移動ができず不便なので、とりあえずプレイヤーとは名ばかりの視点移動君を実装します。
また、デバッグ情報の表示には「PyImGui」を使いました。

▼ソースコード
Gist: main.py

▼こんな感じ
ddiol-p2pjh.gif

ブロックを全面描画してみよう

ようやく視点移動と座標移動を自由に行えるようになったところで、ブロックを全面描画してみましょう。
ソースコードは以下の通りです。
方角を間違えていましたらごめんなさい。怖いので予め謝っておきます。

▼ソースコード
Gist: main.py

#方向間違えてたらごめんなさい
#前面
self.batch.add(4, GL_QUADS, self.loaded_texture, ('v3f', (x, y, z, x, y, z+1, x, y+1, z+1, x, y+1, z,)), texture_coordinates)
#後方
self.batch.add(4, GL_QUADS, self.loaded_texture, ('v3f', (x+1, y, z+1, x+1, y, z, x+1, y+1, z, x+1, y+1, z+1,)), texture_coordinates)
#下面
self.batch.add(4, GL_QUADS, self.loaded_texture, ('v3f', (x, y, z, x+1, y, z, x+1, y, z+1, x, y, z+1,)), texture_coordinates)
#上面
self.batch.add(4, GL_QUADS, self.loaded_texture, ('v3f', (x, y+1, z+1, x+1, y+1, z+1, x+1, y+1, z, x, y+1, z,)), texture_coordinates)
#右側
self.batch.add(4, GL_QUADS, self.loaded_texture, ('v3f', (x+1, y, z, x, y, z, x, y+1, z, x+1, y+1, z,)), texture_coordinates)
#左側
self.batch.add(4, GL_QUADS, self.loaded_texture, ('v3f', (x, y, z+1, x+1, y, z+1, x+1, y+1, z+1, x, y+1, z+1,)), texture_coordinates)

▼こんな感じになりました
9cu9p-1169w.gif
さて、動画からわかります通り、一部描画に異常がみられます。
これは、通常見えていない部分(後面)等の余計な部分が描画されていることが原因です。
これを防ぐために、glEnable(GL_DEPTH_TEST)を設定します。
これは、OpenGL側で勝手にいらない面の描画を選定/排除してくれる便利なものです。

▼設定後は綺麗に描画されました!
sq6py-82p4x.gif

最後に

今回は、ゲームエンジンの選定からモデル描画、プレイヤーの実装を行いました。
次回はワールドの構築と当たり判定の実装を行えればいいかなと思ってます。

今回も最後までご覧頂きありがとうございました。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした