見出し画像

Pythonでマイクラを作る ㉑パーリンノイズで雲を実装する

Pythonでマイクラを作る 第21回です。今回は雲の自動生成に挑戦します。本家マイクラでは、ワールドを作成するたびにランダムな地形が作成されます。まるで地球上に存在しているような美しい景観は、パーリンノイズ(Perlin Noise)によって作成されているのです。
マイクラクローンにおいて、パーリンノイズで雲を作ります。まずはパーリンノイズについて説明します。

パーリンノイズとは

perlin noise

パーリンノイズとは、CG(コンピューターグラフィック)で雲や地形などの自然物を作成するために使われるテクスチャーを作成する技法です。ランダムであり、かつ隣り会う値を近い値を取るという性質を持っているため、自然な景観を再現するのに優れています。

random

 参考に、random関数で作成した疑似乱数のグラフも書いてみました。隣り合う値は無関係のため、変化が激しく自然の景観を再現するには不向きなのがわかります。

ディレクトリ構造

# ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ fonts/
  │  ├ images/
  │  ├ music/
  │  ├ textures/
  │  ├ models/
  │  ├ src/
  │  │  ├ __init__.py
  │  │  ├ block.py  # ブロック関連
  │  │  ├ player.py  # プレイヤー関連
  │  │  ├ player_model.py  # プレイヤーモデル関連
  │  │  ├ camera.py  # カメラ関連
  │  │  ├ target.py  # ターゲットブロック関連
  │  │  ├ user_interface.py  # インターフェース関連
  │  │  ├ utils.py  # ユーティリティー
  │  │  ├ inventory.py  # インベントリ
  │  │  ├ menu.py  # メニュー関連
  │  │  ├ architecture.py  # 建築MOD
  │  │  ├ connect_to_mcpi.py  # マイクラとの連携
  │  │  ├ sound.py  # サウンド関連
  │  │  ├ noise_utils.py  # ノイズのユーティリティー  # 追加
  │  │  ├ cloud.py  # 雲 # 追加
  │  │  ├ mc.py  # 統合クラス
  │  │  
  │  ├ 21_01_perlin_noise.py  # パーリンノイズのテスト  # 追加
  │  ├ 21_02_perlin_noise.py  # パーリンノイズで雲を作る  # 追加
  │  ├ 21_03_main.py  # 統合クラスをインポートしてゲームを起動する  # 追加

今回は、6つのファイルを新しく作成します。
srcディレクトリの中に noise_utils.py と cloud.py を作成します。そして、ルートに 21_01_01_perlin_noise.py、21_01_02_perlin_noise.py、21_02_perlin_noise.py、21_03_main.py を作成します。

まずは2次元のパーリンノイズを Panda3D で表現してみましょう。

2次元のパーリンノイズ

Python の noiseモジュールでパーリンノイズが作成できます。2次元のパーリンノイズを作って、平面に並べます。ノイズの値によって濃淡をつけることでパーリンノイズがどのようなものであるかを視覚的に理解できます。

__init__.py

"""src/__init__.py"""
from .block import Block
from .player import Player
from .user_interface import UserInterface
from .inventory import Inventory
from .menu import Menu
from .architecture import Architecture
from .connect_to_mcpi import ConnectToMCPI
from .sound import Sound
from .noise_utils import make_perlin_noise  # (1)
from .mc import MC

パッケージの初期化モジュールである __init__.py に、インポート文を1行追記します。noise_utilsモジュールから、make_perlin_noise関数をインポートします((1))。

noise_utils.py

noise_utilsモジュールは、ノイズ関連の関数をまとめます。

"""src/noise_utils.py"""
from noise import pnoise2, snoise2


def make_perlin_noise(wavelength=16, size=256):
    freq = 1 / wavelength
    return [[(snoise2(x * freq, y * freq) + 1) / 2 for x in range(size)] for y in range(size)]


if __name__ == '__main__':
    print(make_perlin_noise()[:1])

noise_utils.py はノイズのユーティリティーです。
GitHub に noiseモジュールの examples がありましたので、参考にさせていただきました。make_perlin_noise関数は、2つの引数(wavelength、size)を持ち、パーリンノイズの2次元リストを返却します。
wavelength はノイズの波長を表し、大きくするとなだらかな波になります。つまり周波数(freq)が小さくなります。そして、リストのサイズは size × size になります。

[[0.5, 0.36750391125679016, 0.25917163491249084, 0.1913793385028839, 0.15964126586914062, 0.14591464400291443, 0.13329291343688965, 0.11242946982383728, 0.08301198482513428, 0.05345165729522705, 0.039404869079589844, 0.05915418267250061, 0.1216815710067749, 0.2160225808620453…

このモジュールをテストしてみます。
noise_utils.py を実行すると、パーリンノイズがコンソールに表示されます。値は 0 から 1.0 の間にある小数で、256 × 256 サイズの2次元リストに並べられています。
次に、Panda3D を使って、濃淡のあるブロックを平面に並べることで、パーリンノイズを視覚的に表現してみましょう。

21_01_01_perlin_noise.py

パーリンノイズの視覚化を行います。ノイズの値によって、ブロックに濃淡をつけて、そのブロックを平面に並べてみます。

"""21_01_01_perlin_noise.py"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from src import *


class PerlinNoise(ShowBase):
    WAVELENGTH = 16  # (1)
    SIZE = 256  # (1)

    # コンストラクタ
    def __init__(self):
        # ShowBaseを継承する
        ShowBase.__init__(self)

        # ウインドウの設定
        self.properties = WindowProperties()
        self.properties.setTitle('Perlin noise sample')
        self.properties.setSize(1200, 800)
        self.win.requestProperties(self.properties)
        self.setBackgroundColor(0, 0, 0)

        # マウス操作を禁止
        self.disableMouse()
        # カメラの設定
        self.camera.setPos(0, -PerlinNoise.SIZE * 1.1, PerlinNoise.SIZE * 1.1)  # (2)
        self.camera.lookAt(0, 0, 0)

        perlin_noise = make_perlin_noise(wavelength=PerlinNoise.WAVELENGTH, size=PerlinNoise.SIZE)  # (3)

        # ブロックを置く  # start (4)
        for i in range(PerlinNoise.SIZE):
            for j in range(PerlinNoise.SIZE):
                noise = perlin_noise[j][i]  # (5)
                cube = self.loader.loadModel('models/misc/rgbCube')
                cube.setPos(i - PerlinNoise.SIZE / 2, j - PerlinNoise.SIZE / 2, 0)
                cube.setColor(noise, noise, noise)  # (6)
                cube.reparentTo(self.render)  # end (4)


perlin_noise = PerlinNoise()
perlin_noise.run()

ParlinNoiseクラスは、パーリンノイズを視覚化するクラスです。
クラス変数は WAVELENGTH = 16、SIZE = 256 に設定しました((1))。
カメラの位置を (0, -PerlinNoise.SIZE * 1.1, PerlinNoise.SIZE * 1.1) に移動します。これで斜め上から見下ろすことができます((2))。
make_perlin_noise関数でパーリンノイズを作成し、 2次元のリスト perlin_noise に保存します((3))
(4) が、ブロックを並べて置くプログラムです。各地点のノイズの値を perlin_noise から得ることができます((5))。setColorメソッドで、ブロックの色を (noise, noise, noise) に指定して、ブロックに濃淡をつけます((6))。これで、黒(0)から白(1.0)の濃淡を持つブロックを 256 × 256 の範囲に平面に並べることができます。

perlin_noise_wavelength=16

21_01_01_perlin_noise.py を実行すると、濃淡のある平面が現れます。ランダムでありつつ、秩序も感じさせる不思議な図形が得られました。これが2次元のパーリンノイズです。白黒の山岳写真を見ているようにも思えますね。
波長(wavelength)を倍(32)にしてみましょう。どのような図形になるでしょうか。

"""21_01_02_perlin_noise.py"""

class PerlinNoise(ShowBase):
    # WAVELENGTH = 16
    WAVELENGTH = 32
    SIZE = 256
perlin_noise_wavelength=32

波長16の図形と、波長32の図形をよく見比べてください。波長32の方が荒い(大きな)波になっていることが分かります。
さらによく見ると、波長16の図形の左下4分の1 が波長32の図形と相似形であることも発見できます。つまり波長を倍にすると、2倍に引き伸ばされた図形を得ることができるのです。

次に、この図形を雲に変換します。なんと、上記のコードに条件文を一つ付け加えるだけで雲の図形を得ることができます。

パーリンノイズから雲を作る

21_01_01_perlin_noise.py をコピーして、21_02_perlin_noise.py を作成します。
修正箇所のみ示します。

21_02_perlin_noise.py

"""21_02_perlin_noise.py"""

class PerlinNoise(ShowBase):
    WAVELENGTH = 16
    SIZE = 256
    MIN_RATE = 0.8  # (1)

    # コンストラクタ
    def __init__(self):# ブロックを置く
        for i in range(PerlinNoise.SIZE):
            for j in range(PerlinNoise.SIZE):
                noise = perlin_noise[j][i]
                if noise > PerlinNoise.MIN_RATE:  # (2)
                    cube = self.loader.loadModel('models/misc/rgbCube')  # start (3)
                    cube.setPos(i - PerlinNoise.SIZE / 2, j - PerlinNoise.SIZE / 2, 0)
                    cube.setColor(noise, noise, noise)
                    cube.reparentTo(self.render)  # end (3)

クラス変数として、「MIN_RATE = 0.8」を追加します。この比率が雲を設置する条件になります。大きくすると、雲の範囲は小さくなります((1))。
ブロックを設置する、2重の for文の中に、条件式「if noise > PerlinNoise.MIN_RATE:」を追記します((2))。そして、(3) で示される4行のインデントを下げます。この修正により、noise の値が 0.8 以上のところだけ、ブロックを置くように変更できました。

perlin_noise_min_rate_0.8

21_02_perlin_noise.py を実行してください。自然に近い雲の図形を得ることができました。クラス変数 MIN_RATEの値を変更することで、雲の量を多くしたり少なくしたり調整可能です。試してみてください。

雲の実装

準備ができましたので、マイクラクローン(Pynecrafter)に雲を実装します。

__init__.py

"""src/__init__.py"""
from .block import Block
from .player import Player
from .user_interface import UserInterface
from .inventory import Inventory
from .menu import Menu
from .architecture import Architecture
from .connect_to_mcpi import ConnectToMCPI
from .sound import Sound
from .noise_utils import make_perlin_noise
from .cloud import Cloud  # (1)
from .mc import MC

__init__.py に cloudモジュールから Cloudクラスをインポートするコードを追記します((1))。

mc.py

"""src/mc.py"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from . import *


class MC(ShowBase, UserInterface, Inventory, Menu, Architecture, ConnectToMCPI, 
         Sound, Cloud):  # (1)
    def __init__(self, ground_size=128, mode='normal', cloud_range=0):  # (2)
        self.mode = mode
        self.ground_size = ground_size
        self.enable_sound_effect = False
        # ShowBaseを継承する
        ShowBase.__init__(self)
        self.font = self.loader.loadFont('fonts/PixelMplus12-Regular.ttf')
        UserInterface.__init__(self)
        Inventory.__init__(self)
        Menu.__init__(self)
        if self.mode == 'mcpi':
            ConnectToMCPI.__init__(self)
        Sound.__init__(self)
        if cloud_range:  # (3)
            Cloud.__init__(self, cloud_range)  # (3)

mc.py はゲームを起動するクラスです。クラス定義文の引数に Cloud を追加して、Cloudクラスを継承します((1))。
初期化メソッドの引数に cloud_range を追記します。引数cloud_range は雲を設置する範囲を整数で指定します。初期値は 0(つまり雲は設置しない)です((2))。
条件式「if cloud_range:」により、雲を設置するかどうか判定して、設置するときは Cloudクラスの初期化メソッドを実行します((3))。

cloud.py

"""src/cloud.py"""
from panda3d.core import *
from . import make_perlin_noise
from direct.showbase.ShowBaseGlobal import globalClock


class Cloud:
    CLOUD_WAVELENGTH = 16  # (1)
    CLOUD_MIN_RATE = 0.8 # (1)
    CLOUD_ALTITUDE = 20 # (1)
    CLOUD_HEIGHT = 2 # (1)
    CLOUD_SPEED = 3 # (1)

    def __init__(self, cloud_range):
        self.cloud_node_position = Point3(0, 0, Cloud.CLOUD_ALTITUDE) # (2)
        self.cloud_models = [] # start (3)
        for i in range(50):
            color = i * 0.02
            cube = self.loader.loadModel('models/misc/rgbCube')
            cube.setColor(color, color, color)
            cube.setScale(1.0, 1.0, Cloud.CLOUD_HEIGHT)
            self.cloud_models.append(cube)  # end (3)

        perlin_noise = make_perlin_noise(wavelength=Cloud.CLOUD_WAVELENGTH, size=cloud_range)  # (4)

        # 雲ブロックを置く
        self.cloud_node = self.render.attachNewNode(PandaNode('cloud_node')) # (5)
        for i in range(cloud_range):  # start 6)
            for j in range(cloud_range):
                noise = perlin_noise[j][i]
                if noise > Cloud.CLOUD_MIN_RATE:
                    num = int(noise / 0.02)
                    cloud_placeholder = self.cloud_node.attachNewNode(PandaNode('cloud_placeholder'))
                    cloud_placeholder.setPos(i - cloud_range / 2, j - cloud_range / 2, 0)
                    self.cloud_models[num].instanceTo(cloud_placeholder)  # end (6)


        self.taskMgr.add(self.cloud_update, 'cloud_update')  # (7)

    def cloud_update(self, task):
        dt = globalClock.getDt()  # (8)
        self.cloud_node_position.setX(self.cloud_node_position.x + Cloud.CLOUD_SPEED * dt)  # (9)
        self.cloud_node.setPos(self.cloud_node_position)  # (10)
        return task.cont

cloudモジュールに Cloudクラスを作成します。Cloudクラスは雲を実装するクラスです。
クラス変数CLOUD_WAVELENGTH、CLOUD_MIN_RATEは、雲の波長と設置する条件です。クラス変数CLOUD_ALTITUDE、CLOUD_HEIGHT、CLOUD_SPEED はそれぞれ雲を設置する高度、雲の厚み、雲を動かす速さを設定します((1))。

インスタンス変数cloud_node_position は雲をまとめるノード(cloud_node)の位置を保存します。あらかじめ、クラス変数CLOUD_ALTITUDE の大きさだけ Z方向に移動しておきます((2))。

雲のブロックは大量に設置しなければならないので、インスタンス化して描画の高速化を図ります。
インスタンス変数cloud_modelsは雲を構成するブロックを 50個保存しているリストです。これからのブロックは、黒から白の50段階のグラデーション色を指定します。ブロックの高さをクラス変数CLOUD_HEIGHT にしておきます((3))。

パーリンノイズを2次元リストとして、変数perlin_noiseに代入します((4))。

雲を動かす準備として、cloud_nodeノードを作成します。このノードに全ての雲ブロックを配置して、ノードごと同じ方向に動かすようにします((5))。

(6)の部分が、2重のfor文を使って、雲のブロックを設置するコードになります。パーリンノイズの値 noise が、0.8 以上のとき、「num = int(noise / 0.02)」により、リストself.cloud_models の何番目のモデルを使用するか決定します。cloud_placeholder をダミーのノードとして cloud_node に配置して、setPosメソッドで位置を雲ブロックを設置する場所  (i - cloud_range / 2, j - cloud_range / 2, 0) に移動します。そして、「self.cloud_models[num]」により得られた雲のブロックを instanceToメソッドで cloud_placeholder に配置します。以上でインスタンス化した雲ブロックを設置できます。
(placeholder と instanceToメソッドについては、⑯レンダリングの高速化 で詳しく説明しています。参照してください。)

雲を自動で動かすコードを記述します。
taskMgr.addクラスで、フレームごとに実行するメソッド(cloud_update)を指定します((7))。
cloud_updateメソッドは、cloud_nodeノードごと、全ての雲の位置を X方向に移動します。まず、globalClock.getDtメソッドにより、フレームが変更した時の経過時間を取得し、変数dt に代入します((8))。インスタンス変数cloud_node_position を X方向に、「クラス変数CLOUD_SPEED × 経過時間 dt」で表される移動距離、移動します(9)。最後に、setPosメソッドで、cloud_nodeノードの位置を更新します((10))。
これで Cloudクラスは完成です。

21_03_main.py

"""21_03_main.py"""
from src import MC


class Game(MC):
    def __init__(self):
        # MCを継承する
        MC.__init__(self, ground_size=256, cloud_range=256)  # (1)


game = Game()
game.run()

21_03_main.py は、ゲームを起動するモジュールです。MCクラスの初期化メソッドの引数に ground_size = 256、cloud_range = 256 を指定します。引数cloud_range を0 より大きい整数にすると、その範囲に雲が設置できます。

moving cloud

21_03_main.py を実行します。256 x 256 の範囲にパーリンノイズから生成された雲が高度 20に設置されます。雲は X方向に自動で動きます。これで今回のミッションは完了です。

次回はパーリンノイズの第2弾として、地形の自動生成に挑戦します。本家マイクラのような自然をどこまで再現できるでしょうか。お楽しみに。


前の記事
Pythonでマイクラを作る ⑳プレイヤーのモーションを実装する
次の記事
Python Panda3Dライブラリで3D地図を作成する

その他のタイトルはこちら


この記事が気に入ったらサポートをしてみませんか?