Panda3D オープンワールドゲーム教材

【概要】Panda3Dゲームエンジンを使用した3D地形生成とキャラクター操作の学習教材である。OpenSimplexノイズによる手続き的地形生成(あらかじめ作った地形データを読み込むのではなく、計算で地形を作り出す方式)、GeoMipTerrainによるLOD制御、三人称カメラの実装方法を、動作するプログラムを通じて理解できる。

資料:[PDF], [パワーポイント]

オープンワールドゲームの実行画面

【本教材を読む前に】次の3点を前提として把握しておくとよい。

【目次】

  1. プログラム利用ガイド
  2. 前準備:環境構築とプログラムの実行方法
  3. プログラムコードの説明
  4. 演習:3次元CGをパラメータ調整で体験する

プログラム利用ガイド

1. このプログラムの利用シーン

本プログラムは、Panda3Dゲームエンジンを使用した3D地形生成とキャラクター操作の学習教材である。手続き的地形生成、LOD(Level of Detail:詳細度。視点から遠いものほど粗く描画して処理を軽くする仕組み)制御、三人称カメラ(プレイヤーを後方から映すカメラ)の実装方法を、動作するプログラムを通じて理解できる。

2. 主な機能

3. 基本的な使い方

  1. 起動

    コマンドプロンプトでpythonコマンドを実行する。地形生成には数秒かかる場合がある。

  2. 移動操作

    WASDキーでプレイヤーを前後左右に移動させる。移動方向はカメラの向きを基準とする。

  3. カメラ操作

    左マウスボタンを押しながらドラッグすると、カメラが回転する。マウスホイールで視点距離を調整できる。

  4. 終了

    ESCキーを押すとプログラムが終了する。

4. 便利な機能

前準備:環境構築とプログラムの実行方法

事前準備:Pythonのインストール

本教材はWindowsのパソコンで動作することを前提とする。Pythonが動作する環境が必要であり、未導入の場合は事前にインストールしておく。Panda3Dおよび使用ライブラリはバージョンによって対応するPythonのバージョンが決まっているため、Pythonは比較的新しい安定版(3.10〜3.13系など)を導入しておくとよい。GPU搭載機・CPUのみの機のいずれでも動作する(影や地形描画はGPUがあるほど快適だが、影が出ない場合の対処は冒頭の「本教材を読む前に」を参照)。

必要なライブラリのインストール

本教材の演習では、3次元コンピュータグラフィックスのためのライブラリ panda3d、ノイズ生成ライブラリ opensimplex、数値計算ライブラリ numpy を使う。インストールは1回だけ行えばよい。

手順:コマンドプロンプトを起動する(Windowsキー → 「cmd」と入力 → Enter)。起動したコマンドプロンプトに次を入力して実行する。

pip install --no-user panda3d numpy opensimplex

opensimplex はインストール時に numpy も自動的に導入するが、上記のように明示しておくと分かりやすい。)

プログラムの実行

本ページのコードを実行する方法は2通りある。どちらでも動作する。

方法A:Visual Studio Codeで実行する。Visual Studio Codeがインストール・設定済みであれば、本ページのコードを編集画面にコピー&ペーストし、そのまま実行する。

方法B:ファイルに保存して実行する。メモ帳などのテキストエディタを開き、本ページのコードを貼り付ける。a.pyのようなファイル名で保存する。コマンドプロンプトでそのファイルがあるフォルダに移動し、次を入力して実行する。

python a.py

プログラムコードの説明

1. 概要

本プログラムは、Panda3Dゲームエンジンを使用した三人称視点のオープンワールドゲーム教材である。実行時にOpenSimplexノイズでハイトマップ(地形の高さを格子状に並べた2次元配列)を動的に生成し、粒子ベースの侵食シミュレーションを適用した後、GeoMipTerrainで地形をレンダリング(3次元データから画面に描画する処理)する。プレイヤーはWASDキーとマウス操作で地形上を移動できる。ノイズ生成と配列処理にはNumPyを用いており、グリッド全体をまとめて計算するため、1画素ずつ計算するよりはるかに短く・高速なコードになっている。

2. 主要技術

OpenSimplexノイズ(ライブラリ利用)

手続き的地形生成には、なめらかに変化するノイズ(雑音状の値)が必要である。ノイズには、Ken Perlinが1985年に発表したPerlinノイズ[1]を起源とする一連の手法があり、本教材ではその後継であるOpenSimplexノイズを opensimplex ライブラリ経由で利用する。OpenSimplexはPerlinノイズに見られがちな格子状のムラ(アーティファクト)が出にくく、地形生成に向く。ライブラリの noise2array 関数は、x座標とy座標の配列を渡すとグリッド全体のノイズ値を一度に返すため、コードを簡潔に保てる。本プログラムでは、基本ノイズ、リッジノイズ(尾根状の形状を生成する変形ノイズ)、マスクノイズ(地形タイプを区分するためのノイズ)を組み合わせて複合地形を生成する。

GeoMipTerrain

Willem H. de Boerが2000年に発表したGeoMipMapping(Geometrical MipMapping)アルゴリズムに基づくPanda3Dの地形レンダリングクラスである[2]。地形をブロック単位に分割し、視点からの距離に応じて各ブロックのLOD(詳細度)を変化させる。視点移動時に全体を再生成する必要がなく、変更のあるブロックのみを更新する。

3. 技術的特徴

4. 実装の特色

教材としての利用を想定し、以下の機能を備える。

5. 参考文献

  1. Perlin, K. (1985). An Image Synthesizer. Proceedings of the 12th Annual Conference on Computer Graphics and Interactive Techniques (SIGGRAPH '85), 287-296. https://doi.org/10.1145/325334.325247
  2. de Boer, W. H. (2000). Fast Terrain Rendering Using Geometrical MipMapping. https://www.flipcode.com/archives/article_geomipmaps.pdf
  3. Mei, X., Decaudin, P., & Hu, B.-G. (2007). Fast Hydraulic Erosion Simulation and Visualization on GPU. Proceedings of the 15th Pacific Conference on Computer Graphics and Applications, 47-56. https://doi.org/10.1109/PG.2007.15
"""
Panda3D オープンワールドゲーム教材
- 実行時にOpenSimplexノイズ + NumPyでハイトマップを動的生成
- GeoMipTerrainによるリアルな地形
- 三人称カメラ
- WASD + マウスによる簡易移動
"""

from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from panda3d.core import (
    GeoMipTerrain, PNMImage, Texture,
    Vec3, DirectionalLight, AmbientLight,
    WindowProperties, LColor, ClockObject
)
import math
import sys
import random

import numpy as np
import opensimplex


# ---- ノイズ生成のヘルパー(すべてNumPy配列に対して計算する) ----

def noise_grid(seed, xs, ys):
    """指定シードで、座標配列 xs, ys から (len(ys), len(xs)) のノイズ配列を生成。
    返り値は -1〜1 の範囲。"""
    opensimplex.seed(seed)
    return opensimplex.noise2array(xs, ys)


def octave_grid(seed, coords, scale, octaves=6, persistence=0.5):
    """オクターブノイズ(複数周波数の合成)を配列で計算。返り値は おおむね -1〜1。"""
    total = np.zeros((coords.size, coords.size))
    frequency = 1.0
    amplitude = 1.0
    max_value = 0.0
    for _ in range(octaves):
        xs = coords * scale * frequency
        total += noise_grid(seed, xs, xs) * amplitude
        max_value += amplitude
        amplitude *= persistence
        frequency *= 2.0
    return total / max_value


def ridge_grid(seed, coords, scale, octaves=6, persistence=0.5):
    """リッジノイズ(尾根・谷を生成する変形ノイズ)を配列で計算。返り値は 0〜1。"""
    total = np.zeros((coords.size, coords.size))
    frequency = 1.0
    amplitude = 1.0
    max_value = 0.0
    for _ in range(octaves):
        xs = coords * scale * frequency
        n = noise_grid(seed, xs, xs)
        n = 1.0 - np.abs(n)   # 尾根の形状
        n = n * n             # より鋭い尾根に
        total += n * amplitude
        max_value += amplitude
        amplitude *= persistence
        frequency *= 2.0
    return total / max_value


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

        # グローバルクロック(フレーム間の経過時間 dt を取得するために使用)
        self.clock = ClockObject.getGlobalClock()

        # ウィンドウ設定
        self.set_window_title("Open World - Panda3D教材")

        # 地形パラメータ
        self.terrain_size = 513     # 2^n + 1 である必要がある
        self.terrain_scale = 2.5    # 水平方向の拡大率(ワールド座標 = ハイトマップ座標 × この値)
        self.height_scale = 80.0    # 高低差の倍率

        # ノイズのシード値(演習3で変更する)
        self.seed_base = 42      # 基本地形
        self.seed_ridge = 123    # 山脈用
        self.seed_detail = 789   # 細部用
        self.seed_mask = 456     # 平地/山地マスク用

        # 侵食シミュレーションの反復回数(演習4で変更する)
        self.erosion_iterations = 5000

        # プレイヤー設定(マップ中央付近に配置)
        # マップの一辺の長さ = (terrain_size - 1) * terrain_scale なので、その半分が中央
        center = (self.terrain_size - 1) * self.terrain_scale / 2.0
        self.player_pos = Vec3(center, center, 0)
        self.player_heading = 0
        self.move_speed = 50.0      # 広いマップ用に速度を上げている(ワールド座標での速度)

        # カメラ設定
        self.camera_distance = 30.0  # 広いマップ用に距離を拡大
        self.camera_height = 15.0
        self.camera_heading = 0      # カメラの水平角度
        self.camera_pitch = 30       # カメラの垂直角度
        self.mouse_dragging = False
        self.last_mouse_x = 0
        self.last_mouse_y = 0

        # 移動状態
        self.key_map = {
            "forward": False, "backward": False,
            "left": False, "right": False
        }

        # 初期化
        self.setup_terrain()
        self.setup_player()
        self.setup_camera()
        self.setup_lighting()
        self.setup_controls()
        self.setup_sky()

        # メインループ
        self.taskMgr.add(self.update, "update")

    def set_window_title(self, title):
        """ウィンドウタイトル設定"""
        props = WindowProperties()
        props.setTitle(title)
        self.win.requestProperties(props)

    def generate_heightmap(self):
        """複合ノイズ+侵食シミュレーションでハイトマップを動的生成(NumPy配列で計算)"""
        size = self.terrain_size

        # 0, 1, 2, ... という格子座標(ハイトマップ座標)
        coords = np.arange(size, dtype=np.float64)

        # 各種ノイズをグリッド全体でまとめて生成(0〜1 に正規化)
        mask = octave_grid(self.seed_mask, coords, 0.005, octaves=3, persistence=0.5)
        mask = (mask + 1) / 2
        mask = np.power(mask, 0.8)  # コントラスト調整

        base = octave_grid(self.seed_base, coords, 0.015, octaves=6, persistence=0.5)
        base = (base + 1) / 2

        ridge = ridge_grid(self.seed_ridge, coords, 0.012, octaves=5, persistence=0.45)  # すでに0〜1

        detail = octave_grid(self.seed_detail, coords, 0.05, octaves=3, persistence=0.4)
        detail = (detail + 1) / 2

        # マスクに基づいて3つの地形タイプを合成
        # mask < 0.35: 平地優勢 / 0.35〜0.55: 丘陵(遷移)/ mask >= 0.55: 山岳優勢
        flat = np.power(base * 0.3 + detail * 0.1, 2.0)              # 平地(低く平坦)
        hill = base * 0.5 + ridge * 0.3 + detail * 0.1              # 丘陵
        mountain = np.power(base * 0.3 + ridge * 0.5 + detail * 0.15, 1.2) * 0.6 + 0.4  # 山岳

        # 丘陵は平地と丘陵地形を遷移パラメータ t で混ぜる
        t = np.clip((mask - 0.35) / 0.2, 0.0, 1.0)
        hill_blended = flat * (1 - t) + hill * t

        # マスク値に応じて平地 / 丘陵 / 山岳を選択
        heightdata = np.where(mask < 0.35, flat,
                              np.where(mask < 0.55, hill_blended, mountain))

        # 侵食シミュレーション
        heightdata = self.apply_erosion(heightdata, size, self.erosion_iterations)

        # 0〜1に正規化
        min_h, max_h = heightdata.min(), heightdata.max()
        range_h = (max_h - min_h) if max_h > min_h else 1.0
        heightdata = (heightdata - min_h) / range_h

        # PNMImageに書き込む
        img = PNMImage(size, size)
        for y in range(size):
            for x in range(size):
                h = float(heightdata[y][x])
                img.setXel(x, y, h, h, h)  # setXel は 0.0〜1.0 の浮動小数で指定

        # テクスチャ生成用に保持
        self.heightdata = heightdata
        return img

    def apply_erosion(self, heightdata, size, iterations):
        """簡易粒子ベース侵食シミュレーション(4近傍の最急降下モデル)"""
        erosion_rate, deposition_rate, evaporation, min_slope = 0.3, 0.3, 0.02, 0.01

        for _ in range(iterations):
            x, y = random.uniform(1, size - 2), random.uniform(1, size - 2)
            sediment, water = 0.0, 1.0

            for _ in range(50):  # 雨粒が流れ落ちる(最大50ステップ)
                ix, iy = int(x), int(y)
                if ix < 1 or ix >= size - 1 or iy < 1 or iy >= size - 1:
                    break

                h = heightdata[iy][ix]
                neighbors = [(ix-1, iy, heightdata[iy][ix-1]), (ix+1, iy, heightdata[iy][ix+1]),
                             (ix, iy-1, heightdata[iy-1][ix]), (ix, iy+1, heightdata[iy+1][ix])]
                lowest = min(neighbors, key=lambda n: n[2])
                slope = h - lowest[2]

                if slope < min_slope:
                    heightdata[iy][ix] += sediment * deposition_rate
                    break

                capacity = slope * water
                if sediment > capacity:
                    deposit = (sediment - capacity) * deposition_rate
                    heightdata[iy][ix] += deposit
                    sediment -= deposit
                else:
                    erode = min((capacity - sediment) * erosion_rate, h * 0.1)
                    heightdata[iy][ix] -= erode
                    sediment += erode

                x, y = float(lowest[0]), float(lowest[1])
                water *= (1 - evaporation)
                if water < 0.01:
                    break

        return heightdata

    def setup_terrain(self):
        """地形の生成と設定"""
        # ハイトマップ生成
        heightmap = self.generate_heightmap()

        # GeoMipTerrain作成
        self.terrain = GeoMipTerrain("terrain")
        self.terrain.setHeightfield(heightmap)

        # 地形設定
        self.terrain.setBlockSize(32)
        self.terrain.setNear(40)
        self.terrain.setFar(200)
        self.terrain.setFocalPoint(self.camera)

        # 地形生成
        self.terrain.generate()

        # シーンに追加
        self.terrain_root = self.terrain.getRoot()
        self.terrain_root.reparentTo(self.render)
        self.terrain_root.setScale(self.terrain_scale, self.terrain_scale, self.height_scale)

        # テクスチャ生成(高さに応じた色分け)
        self.apply_terrain_texture()

        # 定期的にLOD更新
        self.taskMgr.add(self.update_terrain, "update_terrain")

    def apply_terrain_texture(self):
        """高さに応じた地形テクスチャを生成(self.heightdata を利用)"""
        size = self.terrain_size
        h = self.heightdata  # 0〜1 のNumPy配列

        # 高さしきい値で4色に色分け(演習6で変更する)
        # RGBそれぞれの配列を作り、しきい値ごとに色を上書きする
        r = np.full((size, size), 0.9)   # 既定は山頂(白)
        g = np.full((size, size), 0.9)
        b = np.full((size, size), 0.95)

        # 低地: 緑(草原)
        m = h < 0.3
        r[m], g[m], b[m] = 0.2, 0.6, 0.2
        # 中腹: 黄緑
        m = (h >= 0.3) & (h < 0.5)
        r[m], g[m], b[m] = 0.4, 0.5, 0.2
        # 高地: 茶色(岩)
        m = (h >= 0.5) & (h < 0.7)
        r[m], g[m], b[m] = 0.5, 0.4, 0.3
        # 山頂(h >= 0.7)は既定値のまま

        tex_img = PNMImage(size, size)
        for y in range(size):
            for x in range(size):
                tex_img.setXel(x, y, float(r[y][x]), float(g[y][x]), float(b[y][x]))

        tex = Texture()
        tex.load(tex_img)  # PNMImage を Texture に読み込む
        self.terrain_root.setTexture(tex)

    def update_terrain(self, task):
        """地形LODの更新"""
        self.terrain.update()
        return Task.cont

    def setup_player(self):
        """プレイヤー(シンプルな球体)の作成"""
        # プレイヤーノード
        self.player = self.render.attachNewNode("player")

        # 可視化用の球体(Panda3D同梱モデルを使用)
        self.player_model = self.loader.loadModel("models/misc/sphere")
        self.player_model.setScale(1.0)
        self.player_model.setColor(1, 0.5, 0, 1)  # オレンジ
        self.player_model.reparentTo(self.player)

        # 初期位置設定
        start_height = self.get_terrain_height(self.player_pos.x, self.player_pos.y)
        self.player_pos.z = start_height + 2.0
        self.player.setPos(self.player_pos)

    def setup_camera(self):
        """三人称追従カメラの設定"""
        # デフォルトのマウス操作(Panda3D標準のカメラ操作)を無効化
        self.disableMouse()

    def setup_lighting(self):
        """ライティング設定(環境光+指向性光源+シャドウ)"""
        # 環境光の設定(全方向から均一に照らす光)
        ambient = AmbientLight('ambient')
        ambient.setColor(LColor(0.3, 0.3, 0.35, 1))  # やや暗めの環境光
        ambient_np = self.render.attachNewNode(ambient)
        self.render.setLight(ambient_np)

        # 指向性光源(太陽光のような平行光)
        sun = DirectionalLight('sun')
        sun.setColor(LColor(0.9, 0.85, 0.7, 1))  # 暖色系の太陽光

        # シャドウマッピングの設定
        sun.setShadowCaster(True, 2048, 2048)  # 影を有効化(2048x2048解像度)
        sun.getLens().setFilmSize(200, 200)     # 影の範囲(広いマップ用)
        sun.getLens().setNearFar(1, 400)        # 影の描画範囲

        sun_np = self.render.attachNewNode(sun)
        sun_np.setHpr(45, -60, 0)  # 光の方向を設定(HPR: Heading, Pitch, Roll)
        self.render.setLight(sun_np)

        # シェーダ自動生成を有効化(シャドウマッピングの描画に必要)
        # 影が出ない・画面が暗い環境では、次の1行をコメントアウトすると影なしで動作する
        self.render.setShaderAuto()

        self.sun_np = sun_np  # 後で参照できるよう保持

    def setup_sky(self):
        """空の背景色設定"""
        self.setBackgroundColor(0.5, 0.7, 1.0, 1)  # 水色

    def setup_controls(self):
        """キー・マウス操作の設定"""
        # キーバインド
        self.accept("w", self.set_key, ["forward", True])
        self.accept("w-up", self.set_key, ["forward", False])
        self.accept("s", self.set_key, ["backward", True])
        self.accept("s-up", self.set_key, ["backward", False])
        self.accept("a", self.set_key, ["left", True])
        self.accept("a-up", self.set_key, ["left", False])
        self.accept("d", self.set_key, ["right", True])
        self.accept("d-up", self.set_key, ["right", False])

        # マウスドラッグでカメラ回転
        self.accept("mouse1", self.start_drag)
        self.accept("mouse1-up", self.stop_drag)

        # マウスホイールでズーム
        self.accept("wheel_up", self.zoom_in)
        self.accept("wheel_down", self.zoom_out)

        # 終了
        self.accept("escape", sys.exit)

    def start_drag(self):
        """マウスドラッグ開始"""
        self.mouse_dragging = True
        if self.mouseWatcherNode.hasMouse():
            self.last_mouse_x = self.mouseWatcherNode.getMouseX()
            self.last_mouse_y = self.mouseWatcherNode.getMouseY()

    def stop_drag(self):
        """マウスドラッグ終了"""
        self.mouse_dragging = False

    def zoom_in(self):
        """ズームイン"""
        self.camera_distance = max(10, self.camera_distance - 3)

    def zoom_out(self):
        """ズームアウト"""
        self.camera_distance = min(100, self.camera_distance + 3)

    def set_key(self, key, value):
        """キー状態の更新"""
        self.key_map[key] = value

    def get_terrain_height(self, x, y):
        """指定座標の地形高さを取得"""
        # ワールド座標を地形(ハイトマップ)座標に変換
        tx = x / self.terrain_scale
        ty = y / self.terrain_scale

        # 範囲チェック
        if 0 <= tx < self.terrain_size - 1 and 0 <= ty < self.terrain_size - 1:
            elevation = self.terrain.getElevation(tx, ty)
            return elevation * self.height_scale
        return 0

    def update(self, task):
        """メインループ"""
        dt = self.clock.getDt()  # 前フレームからの経過時間(秒)

        # マウスドラッグでカメラ回転
        self.handle_mouse_drag()

        # キー入力で移動
        self.handle_movement(dt)

        # カメラをプレイヤーに追従
        self.update_camera()

        return Task.cont

    def handle_mouse_drag(self):
        """マウスドラッグ処理"""
        if self.mouse_dragging and self.mouseWatcherNode.hasMouse():
            mx = self.mouseWatcherNode.getMouseX()
            my = self.mouseWatcherNode.getMouseY()

            # マウス移動量を計算
            dx = mx - self.last_mouse_x
            dy = my - self.last_mouse_y

            # カメラ角度を更新
            self.camera_heading -= dx * 100
            self.camera_pitch += dy * 50

            # ピッチ制限(5〜80度)
            self.camera_pitch = max(5, min(80, self.camera_pitch))

            self.last_mouse_x = mx
            self.last_mouse_y = my

    def handle_movement(self, dt):
        """移動処理(カメラの向きを基準に移動)"""
        # 移動ベクトル計算
        move_vec = Vec3(0, 0, 0)

        if self.key_map["forward"]:
            move_vec.y += 1
        if self.key_map["backward"]:
            move_vec.y -= 1
        if self.key_map["left"]:
            move_vec.x -= 1
        if self.key_map["right"]:
            move_vec.x += 1

        if move_vec.length() > 0:
            move_vec.normalize()
            move_vec *= self.move_speed * dt

            # カメラの向きに応じて移動方向を回転
            heading_rad = math.radians(self.camera_heading)
            cos_h = math.cos(heading_rad)
            sin_h = math.sin(heading_rad)

            new_x = move_vec.x * cos_h - move_vec.y * sin_h
            new_y = move_vec.x * sin_h + move_vec.y * cos_h

            self.player_pos.x += new_x
            self.player_pos.y += new_y

            # プレイヤーの向きをカメラの向きに合わせる
            self.player_heading = self.camera_heading

        # 地形に沿って高さを調整
        terrain_height = self.get_terrain_height(self.player_pos.x, self.player_pos.y)
        self.player_pos.z = terrain_height + 2.0

        # プレイヤー位置更新
        self.player.setPos(self.player_pos)
        self.player.setH(self.player_heading)

    def update_camera(self):
        """カメラをプレイヤーに追従させる"""
        # 球面座標でカメラ位置を計算
        heading_rad = math.radians(self.camera_heading)
        pitch_rad = math.radians(self.camera_pitch)

        # カメラ位置(プレイヤーを中心とした球面上)
        cam_x = self.player_pos.x - self.camera_distance * math.sin(heading_rad) * math.cos(pitch_rad)
        cam_y = self.player_pos.y - self.camera_distance * math.cos(heading_rad) * math.cos(pitch_rad)
        cam_z = self.player_pos.z + self.camera_distance * math.sin(pitch_rad)

        # カメラ位置設定
        self.camera.setPos(cam_x, cam_y, cam_z)

        # プレイヤーを見る
        self.camera.lookAt(self.player_pos + Vec3(0, 0, 2))


# メイン実行
if __name__ == "__main__":
    print("=== Open World Game ===")
    print("操作方法:")
    print("  WASD: プレイヤー移動")
    print("  左ドラッグ: カメラ回転")
    print("  ホイール: ズームイン/アウト")
    print("  ESC: 終了")
    print("地形を生成中...")

    game = OpenWorldGame()
    game.run()

演習:3次元CGをパラメータ調整で体験する

本章では、プログラムのパラメータ(数値設定)を変えながら3次元コンピュータグラフィックスの表現を体験する。各演習は《手順》《ヒント》《考察ポイント》で構成する。コードを変更したら保存し、再実行して画面の変化を観察すること。

演習1:プログラムを動かして3D空間を体感する

《手順》

  1. プログラムを実行する。
  2. WASDキーで地形を歩き回る。
  3. 左マウスボタンを押しながらドラッグし、視点を回転させる。
  4. マウスホイールでズームイン・ズームアウトを行う。
  5. 低地(緑)、中腹(黄緑)、高地(茶)、山頂(白)の各エリアまで移動する。

《ヒント》

《考察ポイント》

演習2:地形の起伏を変える

《手順》

  1. コード中の self.height_scale = 80.0(地形の高低差の倍率)を 40.0 に変更し、実行する。
  2. 続いて 120.0160.0 に変更し、それぞれ実行する。
  3. 各設定で同じ場所まで移動し、見た目を比較する。

《ヒント》

《考察ポイント》

演習3:地形の形を変える(ノイズのシード値)

シード値とは乱数発生の初期値のことであり、同じシード値からは毎回まったく同じノイズが得られる。

《手順》

  1. __init__ 内の self.seed_base = 42 の数値を別の整数(例:71002024)に変更し、実行する。
  2. 同様に self.seed_ridgeself.seed_detailself.seed_mask のシード値も変えて試す。
  3. 気に入った地形が現れたシード値を記録する。

《ヒント》

《考察ポイント》

演習4:侵食の効果を観察する

《手順》

  1. __init__ 内の self.erosion_iterations = 5000(侵食シミュレーションの反復回数)を 0 に変更し、実行する(侵食なし)。
  2. 続いて 20001000020000 に変更し、それぞれ実行する。
  3. 谷や尾根の見え方を比較する。地形生成にかかる時間も観察する。

《ヒント》

《考察ポイント》

演習5:LODの効きを確認する

《手順》

  1. self.terrain.setNear(40)self.terrain.setFar(200)(setNearはこの距離以下で最高詳細度、setFarはこの距離以上で最低詳細度となる、距離のしきい値)を、setNear(10)setFar(50) に変更し、実行する。
  2. 遠くの地形を観察し、ポリゴン(メッシュを構成する三角形などの面)の粗さを確認する。
  3. 続いて setNear(100)setFar(500) に変更し、実行する。
  4. マウスホイールでズームアウトし、全景を見比べる。

《ヒント》

《考察ポイント》

演習6:色分けの境界を変える

《手順》

  1. apply_terrain_texture 内の高さしきい値(0.30.50.7)を変更する。
  2. 例:雪のエリアを広げるには山頂判定(h >= 0.7)のしきい値を 0.55 に下げる。雪を減らすには 0.85 に上げる(高地判定の上側のしきい値もあわせて調整する)。
  3. 各エリアの面積比がどう変わるかを観察する。
  4. 余裕があれば、色(r, g, b:赤・緑・青の各成分を0.0〜1.0で指定)も変えて独自のカラースキーム(配色)を作る。

《ヒント》

《考察ポイント》

演習7:光と影の演出を変える

《手順》

  1. setup_lighting 内の sun_np.setHpr(45, -60, 0) の第2引数(光源のピッチ。値が負で絶対値が大きいほど光が地面に対して上から差す)を -20(夕方寄り)や -80(ほぼ真上)に変更する。
  2. 太陽光の色 LColor(0.9, 0.85, 0.7, 1)(r, g, b, アルファ値)を、夕焼け風(LColor(1.0, 0.5, 0.3, 1))や月光風(LColor(0.5, 0.6, 0.8, 1))に変える。
  3. 背景色 setBackgroundColor(0.5, 0.7, 1.0, 1) も時間帯に合わせて変える。

《ヒント》

《考察ポイント》

演習8:自分だけの地形を作る(総合)

《手順》

  1. 演習2〜7で得た知見をもとに、テーマ(例:「雪山」「砂漠」「夕焼けの草原」)を一つ決める。
  2. height_scale、ノイズのシード値、侵食回数、色分け、ライティングを組み合わせて調整する。
  3. 完成したパラメータをスクリーンショットとともに記録する。
  4. 変更したパラメータの一覧と、その意図を簡潔にまとめる。

《ヒント》

《考察ポイント》