[# 2] Make Minecraft with Python. ~ Model drawing and player implementation ~

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

minecraft.png

Overview

This is a project to reproduce the world-famous sandbox game "Minecraft" in the programming language "Python".

** Previous article: "[# 1] Making Minecraft with Python.-Preliminary research and design-" **

** Next article: "[# 3] Make Minecraft with Python.-Improvement of player movement (concept of inertia) and collision detection-" ** **

Preface

Thank you very much for your patience. This is the second!

What to do this time

--Selection of game engine --Trial execution --Drawing a model --Player implementation

Game engine selection

I couldn't make a 3D game engine from scratch, so I searched for a 3D game engine (library) that also supports Python.

Among them, we have selected the ones that are relatively easy to use and that are likely to produce beautiful drawings.

Panda3D Engine 509ed0ebac43da9c5fa01735640f7ef6.png

It is a 3D game engine called "Panda3D". Platforms are Python and C ++.

I tried using Panda3D

I will actually move it.

▼ Installation

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

▼ Source code

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))

It was drawn like this.

image.png

Try loading the model

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)

It turned out to be something like this. It seems that you can operate it with the mouse. image.png The cube looks like this. image.png


Try drawing in 3D

Reference: 6 seconds video with gif @ panda3D

0ku48-wnqiw.gif

▼ Source code Since it will be long, only the core part will be described.

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" is one of the game engines that utilize hardware acceleration. It seems that it supports Python and .NET as well as C ++. Realization of 3D games on .NET is quite difficult, so I would like to use Ogre.NET to realize "I tried to reproduce Minecraft in C #!".

Take a look at Showcase.

This seems to be a game "X-Morph: Defense". Of course, the Ogre engine is used. That's a great graphic.

image.png

image.png

The result of various thoughts

image.png

In addition to game engines other than the above, I decided to use "Pyglet", which seems to be the most informative, considering that it is cross-platform. It does not depend on other libraries. ・ It is perfect that it supports multiple displays and multiple monitors.

I tried using Pyglet

pyxpyg.png

From here, let's actually do a simple drawing using Pyglet.

Environment

▼ Installation

pip install pyglet

▼ Installation (for PyCharm) Menu barFileSettings pygletins.png

Try to move

▼ Sample code

main.py


#Import module
from pyglet.gl import *

#Window to display
#width is the width and height is the height.
#caption is the window title and resizable is whether to allow resizing.
pyglet.window.Window(width=700, height=400, caption='Hello Pyglet!', resizable=True)
pyglet.app.run()

If it is displayed like this, it is OK. image.png

Try drawing a line

OpenGL is used in Pyglet.

▼ Sample code

main.py


#Import module
from pyglet.gl import *

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

#Window drawing event
@window.event
def on_draw():
    #Clear drawing
    window.clear()
    glBegin(GL_LINES) #Start drawing a line
    glColor3f(1, 0, 0) # R,G,B ÷ 255
    glVertex2f(0, 0) # x, y(0, 0)From
    glVertex2f(100, 100) # x, y(100, 100)Until
    glEnd() #Finish drawing the line

pyglet.app.run()

When executed, the line will be drawn like this. image.png

Commentary

In the above source code, the drawing event function of window is defined and the line is drawn using OpenGL. It seems that OpenGL recognizes the lower left corner of the screen as zero coordinates.

** 1. Declare the start of line drawing with glBegin (GL_LINES) **

** 2. Declare the color with glColor3f (1, 0, 0) ** The colors here are RGB (Red, Green, Blue). Each is usually 0 to 255, but here it is declared with Float as it says 3f, so pass the number divided by 255.f as an argument. 3f means to pass 3 numbers in float.

** 2. Declare the starting point of the line with glVertex2f (x, y) ** Pass the coordinates of the starting point of the line. 2f means to pass the coordinates of the 2 dimension with float.

** 3. Declare the end point of the line with glVertex2f (x, y) ** Pass the coordinates of the end point of the line. It's okay to pass a number larger than the window size here, but it won't be drawn off the screen.

** 4. Declare the end of line drawing with glEnd () **

pygletdem2.png

Try drawing a block model of Minecraft

Finally the main subject.

Preface

image.png It is a ** big mistake ** to try to implement everything at once in order to realize a project.

Suppose you want to implement function A, function B, and function C. If you implement these all at once and a bug or bug occurs, it is difficult to identify which function (which part) is causing the bug or bug, or it takes longer than usual to find the cause. I will do it.

The correct procedure is Implementation of function A ▶ ** Operation check ** ▶ Implementation of function B ▶ ** Operation check ** ▶ Implementation of function C is.

In the unlikely event that a problem occurs after the function C is implemented, the problem has occurred between the function B and the function C. Can be thought of as. It's more efficient.

First, let's check the operation with the minimum configuration implementation. The introduction is long.

Preparing texture

For the time being, we will use it on a trial basis, so we have prepared this texture, which is familiar to Minecraft. ▼ Texture missing16.png

Implementation

Let's actually draw using the prepared texture! The source code will be long, so I posted it on Gist.

▼ Source code Gist: main.py

When executed, it will be drawn as follows. * Normal condition. </ font>

9a6730ede366fb49dbcb1cdd1a2c17c2.png

For the time being, I was able to draw on only one side.

Commentary

I will explain while extracting parts.

world class

Defines what is called a Batch that holds the Vertex to draw.

self.batch = pyglet.graphics.Batch()

Then, define the three-dimensional coordinates that are the key. If you set it to 0 here, it will be drawn outside the screen and cannot be confirmed ** due to the location of the initial camera, so it is defined with a slight shift.

#Define 3D world coordinates
x = 0.5
y = 0
z = -2

A function is provided for loading textures. Use the native loader of pyglet. Specify the image patch for path.

#Function to load texture
def load_texture(self, path):
    texture = pyglet.image.load(path).get_texture()#Use pyglet's texture loader
    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)

Window class

Next is the window class. This class inherits from the pyglet.window.Window class.

What is "inheritance"?

In the initialization function, the instance of the World class defined earlier is initialized.

super is similar to Java.

What is "super?"

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    #World-class initialization
    self.world = World()

Then, define the function that performs the essential drawing.

def on_draw(self):
    #Clear the screen
    self.clear()
    #Since the world is 3D, set it to 3D drawing mode
    self.render_mode_3d()
    #Draw the world
    self.world.draw()

Here is the render_mode_3d () function, It is necessary to set the matrix mode when performing 3D drawing and 2D drawing. It has ** projection mode (Projection) ** and ** field of view conversion mode (Modelview) **, which defaults to field of view conversion mode. We will discuss these matrix modes later.

Note: Use glLoadIdentity () to erase (initialize) the cumulative mode conversion process. </ font>


def render_mode_3d(self):
    self.render_mode_projection()
    #Set field of view
    gluPerspective(70, self.width / self.height, 0.05, 1000)
    self.render_mode_modelview()

def render_mode_2d(self):
    self.render_mode_projection()
    #Drawing area 0 to window_width, 0 to window_height
    gluOrtho2D(0, self.width, 0, self.height)
    self.render_mode_modelview()

def render_mode_projection(self):
    glMatrixMode(GL_PROJECTION)#Projection conversion mode
    glLoadIdentity()#Eliminate the cumulative conversion process

def render_mode_modelview(self):
    glMatrixMode(GL_MODELVIEW)#Modeling conversion mode(Field conversion)
    glLoadIdentity()#Eliminate the cumulative conversion process

gluOrtho2D performs projection transformation, which is parallel projection in 2D. gluOrtho2D(left, right, top, bottom)

gl2d.png

Then, gluPerspective () sets the field of view. gluPerspective(fovY, aspect, zNear, zFar) The Y-axis viewing angle, aspect (horizontal viewing angle), Z shortest distance, and Z longest distance, respectively.

glsiya.png

Also, zNear and zFar do not change the appearance on the screen. This can be thought of as ** whether you are 1m closer to the screen or 1m closer to the screen **.

Extra edition

From here on, the content will be specialized, so if you are not interested, you can skip it.

Matrix mode

Matrix modes include ** visual transformation (GL_PROJECTION) ** and ** modeling transformation (GL_MODELVIEW) **. * To be exact, there is also GL_TEXTURE, but it is not handled in this source, so skip it. </ font>

Also, 3D ▶ 2D conversion is geometry conversion. The conversion at the time of scaling / moving is affine transformation Is called.

It's going to be super long & I think some people are interested, so I'll throw it in an external article. sorry.

"Completely understand affine transformation"

I couldn't find a Japanese article that explains in detail about geometry transformation.

Player implementation

I'd like to draw the entire block, but it's inconvenient because I can't move the viewpoint as it is, so for the time being, I'll implement the viewpoint movement just as the player. I also used "PyImGui" to display debug information.

▼ Source code Gist: main.py

▼ Like this ddiol-p2pjh.gif

Let's draw the entire block

Now that you can move the viewpoint and coordinates freely, let's draw the entire block. The source code is as follows. I'm sorry if you make a mistake in the direction. ~~ I'm scared, so I apologize in advance. ~~

▼ Source code Gist: main.py

#I'm sorry if I make a mistake
#Front
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)
#Rear
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)
#Bottom surface
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)
#Top surface
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)
#Right side
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)
#left
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)

▼ It looks like this 9cu9p-1169w.gif By the way, as you can see from the video, there are some abnormalities in the drawing. This is because * extra parts * such as the normally invisible part (rear surface) are drawn. To prevent this, set glEnable (GL_DEPTH_TEST). This is a convenient way to select / eliminate unnecessary surface drawing on the OpenGL side.

▼ After setting, it was drawn beautifully! sq6py-82p4x.gif

Finally

This time, we selected the game engine, drew the model, and implemented the player. Next time, I'd like to build a world and implement collision detection.

Thank you for watching until the end.

Recommended Posts