Panda3D オープンワールドゲーム教材
【概要】Panda3Dゲームエンジンを使用した3D地形生成とキャラクター操作の学習教材である。Perlinノイズによる手続き的地形生成、GeoMipTerrainによるLOD制御、三人称カメラの実装方法を、動作するプログラムを通じて理解できる。
【目次】
プログラム利用ガイド
1. このプログラムの利用シーン
Panda3Dゲームエンジンを使用した3D地形生成とキャラクター操作の学習教材である。手続き的地形生成、LOD(詳細度)制御、三人称カメラの実装方法を、動作するプログラムを通じて理解できる。
2. 主な機能
- 動的地形生成:起動時にPerlinノイズと侵食シミュレーションで地形を生成する。
- 三人称カメラ:プレイヤーを追従するカメラで、マウス操作により視点を回転できる。
- 地形追従移動:プレイヤーは地形の起伏に沿って移動し、高度が自動的に調整される。
- LOD地形描画:視点からの距離に応じて地形の詳細度が変化し、描画負荷を抑制する。
3. 基本的な使い方
- 起動:
ターミナルでpythonコマンドを実行する。地形生成には数秒かかる場合がある。
- 移動操作:
WASDキーでプレイヤーを前後左右に移動させる。移動方向はカメラの向きを基準とする。
- カメラ操作:
左マウスボタンを押しながらドラッグすると、カメラが回転する。マウスホイールで視点距離を調整できる。
- 終了:
ESCキーを押すとプログラムが終了する。
4. 便利な機能
- カメラピッチ制限:カメラの垂直角度は5度から80度の範囲に制限されており、地面にめり込んだり真上を向いたりすることを防ぐ。
- ズーム範囲:カメラ距離は10から100の範囲で調整でき、ホイール1回で3ずつ変化する。
- 地形の色分け:高度に応じて地形の色が変化し、低地(緑)、中腹(黄緑)、高地(茶)、山頂(白)を視覚的に区別できる。
- 影の描画:太陽光による影が描画される。
Python開発環境,ライブラリ類
ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。
Python 3.12 のインストール
インストール済みの場合は実行不要。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。
REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent --accept-source-agreements --accept-package-agreements
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
【関連する外部ページ】
Python の公式ページ: https://www.python.org/
AI エディタ Windsurf のインストール
Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。
管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。
winget install --scope machine --id Codeium.Windsurf -e --silent --accept-source-agreements --accept-package-agreements
【関連する外部ページ】
Windsurf の公式ページ: https://windsurf.com/
必要なライブラリのインストール
コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する:
pip install panda3d
プログラムコードの説明
1. 概要
このプログラムは、Panda3Dゲームエンジンを使用した三人称視点のオープンワールドゲーム教材である。実行時にPerlinノイズでハイトマップを動的に生成し、粒子ベースの侵食シミュレーションを適用した後、GeoMipTerrainで地形をレンダリングする。プレイヤーはWASDキーとマウス操作で地形上を移動できる。
2. 主要技術
Perlinノイズ
Ken Perlinが1985年に発表した手続き的ノイズ生成アルゴリズム[1]。格子点に割り当てた勾配ベクトルと、格子点から入力座標への方向ベクトルの内積を計算し、補間することで連続的なノイズ値を生成する。本プログラムでは、基本ノイズ、リッジノイズ(尾根状の形状を生成する変形ノイズ)、マスクノイズを組み合わせて複合地形を生成している。
GeoMipTerrain
Willem H. de Boerが2000年に発表したGeoMipMapping(Geometrical MipMapping)アルゴリズム[2]に基づくPanda3Dの地形レンダリングクラス。地形をブロック単位に分割し、視点からの距離に応じて各ブロックの詳細度(LOD: Level of Detail)を変化させる。視点移動時に全体を再生成する必要がなく、変更のあるブロックのみを更新する。
3. 技術的特徴
- 複合ノイズ生成
地形マスクを用いて平地、丘陵、山岳エリアを区分し、それぞれに適したノイズ合成比率を適用する。基本ノイズ(オクターブノイズ)、リッジノイズ、詳細ノイズを組み合わせることで、単一のノイズ関数では表現できない多様な地形形状を実現している。
- 粒子ベース侵食シミュレーション
ランダムな位置に仮想的な雨粒を配置し、勾配に沿って流下させながら侵食と堆積を計算する手法[3]。各粒子は運搬容量を持ち、勾配が急な箇所では地形を削り、緩やかな箇所では堆積物を放出する。この処理により、尾根や谷といった水流による地形特徴が追加される。
- 距離ベースLOD制御
GeoMipTerrainのsetNear/setFarメソッドにより、焦点(カメラ位置)からの距離に応じたLODしきい値を設定している。近距離では詳細なメッシュを、遠距離では簡略化されたメッシュをレンダリングすることで、視覚品質と処理負荷のバランスを取る。
- 高さベーステクスチャ生成
ハイトマップの高度値に応じて色を割り当てる方式で地形テクスチャを生成する。低地は緑(草原)、中腹は黄緑、高地は茶色(岩)、山頂は白(雪)として着色している。
4. 実装の特色
教材としての利用を想定し、以下の機能を備える。
- 三人称追従カメラ(球面座標系で位置を計算)
- マウスドラッグによるカメラ回転とホイールによるズーム
- カメラの向きを基準とした移動方向の計算
- シャドウマッピングによる影の描画
- プレイヤーの高さを地形に追従させる処理
5. 参考文献
- 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
- de Boer, W. H. (2000). Fast Terrain Rendering Using Geometrical MipMapping. https://www.flipcode.com/archives/article_geomipmaps.pdf
- 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 オープンワールドゲーム教材
- 実行時にPerlinノイズでハイトマップを動的生成
- GeoMipTerrainによるリアルな地形
- 三人称カメラ
- WASD + マウスによる簡易移動
"""
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from panda3d.core import (
GeoMipTerrain, PNMImage, Filename, TextureStage, Texture,
Vec3, Point3, CardMaker, NodePath, DirectionalLight, AmbientLight,
WindowProperties, LColor
)
import math
import sys
import random
class PerlinNoise:
"""純粋Python実装のPerlinノイズ"""
def __init__(self, seed=0):
random.seed(seed)
self.perm = list(range(256))
random.shuffle(self.perm)
self.perm += self.perm # 512要素に拡張
def fade(self, t):
return t * t * t * (t * (t * 6 - 15) + 10)
def lerp(self, a, b, t):
return a + t * (b - a)
def grad(self, hash_val, x, y):
h = hash_val & 3
if h == 0:
return x + y
elif h == 1:
return -x + y
elif h == 2:
return x - y
else:
return -x - y
def noise2d(self, x, y):
"""2Dパーリンノイズ(-1〜1の範囲)"""
xi = int(x) & 255
yi = int(y) & 255
xf = x - int(x)
yf = y - int(y)
u = self.fade(xf)
v = self.fade(yf)
aa = self.perm[self.perm[xi] + yi]
ab = self.perm[self.perm[xi] + yi + 1]
ba = self.perm[self.perm[xi + 1] + yi]
bb = self.perm[self.perm[xi + 1] + yi + 1]
x1 = self.lerp(self.grad(aa, xf, yf), self.grad(ba, xf - 1, yf), u)
x2 = self.lerp(self.grad(ab, xf, yf - 1), self.grad(bb, xf - 1, yf - 1), u)
return self.lerp(x1, x2, v)
def octave_noise(self, x, y, octaves=6, persistence=0.5):
"""オクターブノイズ(複数周波数の合成)"""
total = 0
frequency = 1
amplitude = 1
max_value = 0
for _ in range(octaves):
total += self.noise2d(x * frequency, y * frequency) * amplitude
max_value += amplitude
amplitude *= persistence
frequency *= 2
return total / max_value
def ridge_noise(self, x, y, octaves=6, persistence=0.5):
"""リッジノイズ(尾根・谷を生成する変形ノイズ)"""
total = 0
frequency = 1
amplitude = 1
max_value = 0
for _ in range(octaves):
# 絶対値を取り、反転することで尾根状の形状を生成
n = self.noise2d(x * frequency, y * frequency)
n = 1.0 - abs(n) # 尾根の形状
n = n * n # より鋭い尾根に
total += n * amplitude
max_value += amplitude
amplitude *= persistence
frequency *= 2
return total / max_value
class OpenWorldGame(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# ウィンドウ設定
self.set_window_title("Open World - Panda3D教材")
# 地形パラメータ
self.terrain_size = 513 # 2^n + 1 である必要がある(257→513で面積約4倍)
self.terrain_scale = 2.5 # スケールも拡大(合計で面積約10倍)
self.height_scale = 80.0 # 高低差も拡大
# プレイヤー設定
self.player_pos = Vec3(640, 640, 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):
"""複合ノイズ+侵食シミュレーションでハイトマップを動的生成"""
size = self.terrain_size
# 複数シードのPerlinノイズ
perlin_base = PerlinNoise(seed=42) # 基本地形
perlin_ridge = PerlinNoise(seed=123) # 山脈用
perlin_detail = PerlinNoise(seed=789) # 細部用
perlin_mask = PerlinNoise(seed=456) # 平地/山地マスク用
# 高さデータを2次元配列で保持(侵食処理用)
heightdata = [[0.0] * size for _ in range(size)]
# ステップ1: 複合ノイズ生成
for y in range(size):
for x in range(size):
# 大規模な地形マスク(平地と山岳地帯を分ける)
mask = perlin_mask.octave_noise(x * 0.005, y * 0.005, octaves=3, persistence=0.5)
mask = (mask + 1) / 2 # 0〜1に正規化
mask = pow(mask, 0.8) # コントラスト調整
# 基本地形(なだらかな起伏)
base = perlin_base.octave_noise(x * 0.015, y * 0.015, octaves=6, persistence=0.5)
base = (base + 1) / 2
# リッジノイズ(山脈・尾根)
ridge = perlin_ridge.ridge_noise(x * 0.012, y * 0.012, octaves=5, persistence=0.45)
# 細部ノイズ(小さな起伏)
detail = perlin_detail.octave_noise(x * 0.05, y * 0.05, octaves=3, persistence=0.4)
detail = (detail + 1) / 2
# マスクに基づいて合成
# mask < 0.4: 平地優勢
# mask > 0.6: 山岳優勢
if mask < 0.35:
# 平地エリア(低くて平坦)
combined = base * 0.3 + detail * 0.1
combined = pow(combined, 2.0) # より平坦に
elif mask < 0.55:
# 丘陵エリア(中程度の起伏)
t = (mask - 0.35) / 0.2 # 0〜1の遷移
flat_part = base * 0.3 + detail * 0.1
hill_part = base * 0.5 + ridge * 0.3 + detail * 0.1
combined = flat_part * (1 - t) + hill_part * t
else:
# 山岳エリア(高くて急峻)
combined = base * 0.3 + ridge * 0.5 + detail * 0.15
combined = pow(combined, 1.2) # より急峻に
combined = combined * 0.6 + 0.4 # 最低高度を上げる
heightdata[y][x] = combined
# ステップ2: 簡易侵食シミュレーション(粒子ベース)
heightdata = self.apply_erosion(heightdata, size, iterations=5000)
# 正規化して画像に変換
heightdata = self.normalize_heightdata(heightdata, size)
img = PNMImage(size, size)
for y in range(size):
for x in range(size):
h = heightdata[y][x]
img.setXel(x, y, h, h, h)
return img
def normalize_heightdata(self, heightdata, size):
"""高さデータを0〜1に正規化"""
min_h = min(min(row) for row in heightdata)
max_h = max(max(row) for row in heightdata)
range_h = max_h - min_h if max_h > min_h else 1.0
for y in range(size):
for x in range(size):
heightdata[y][x] = (heightdata[y][x] - min_h) / range_h
return heightdata
def apply_erosion(self, heightdata, size, iterations=5000):
"""簡易粒子ベース侵食シミュレーション"""
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(heightmap)
# 定期的にLOD更新
self.taskMgr.add(self.update_terrain, "update_terrain")
def apply_terrain_texture(self, heightmap):
"""高さに応じた地形テクスチャを生成"""
tex_img = PNMImage(self.terrain_size, self.terrain_size)
for y in range(self.terrain_size):
for x in range(self.terrain_size):
height = heightmap.getGray(x, y)
# 高さに応じて色を変える
if height < 0.3:
# 低地: 緑(草原)
r, g, b = 0.2, 0.6, 0.2
elif height < 0.5:
# 中腹: 黄緑
r, g, b = 0.4, 0.5, 0.2
elif height < 0.7:
# 高地: 茶色(岩)
r, g, b = 0.5, 0.4, 0.3
else:
# 山頂: 白(雪)
r, g, b = 0.9, 0.9, 0.95
tex_img.setXel(x, y, r, g, b)
tex = Texture()
tex.load(tex_img)
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")
# 可視化用の球体
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):
"""三人称追従カメラの設定"""
# デフォルトマウス操作を無効化
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) # 光の方向を設定
sun_np.setPos(640, 640, 150) # 光源位置(シャドウ計算用)
self.render.setLight(sun_np)
# シャドウを有効化
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 = globalClock.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
# ピッチ制限(0〜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()
実験・研究スキルの基礎:Windowsで学ぶ実験
1. 実験・研究のスキル構成要素
実験や研究を行うには、以下の5つの構成要素を理解する必要がある。
1.1 実験用データ
このプログラムではパラメータ値が実験用データである。地形生成に関わる数値(ノイズのシード、スケール、侵食パラメータなど)を変更することで、異なる結果を得る。
1.2 実験計画
何を明らかにするために実験を行うのかを定める。
計画例:
- ノイズのシード値が地形形状に与える影響を確認する
- 侵食シミュレーションの反復回数が地形の自然さに与える影響を確認する
- height_scaleパラメータが地形の起伏に与える影響を確認する
- LOD距離設定が描画品質と処理負荷に与える影響を確認する
- 山岳地帯と平地の比率を制御するマスク閾値を見つける
1.3 プログラム
実験を実施するためのツールである。このプログラムはPanda3DのGeoMipTerrainクラスとPerlinノイズアルゴリズムを使用している。
- プログラムの機能を理解して活用することが基本である
- 基本となるプログラムを出発点として、将来、さまざまな機能を自分で追加することができる
1.4 プログラムの機能
このプログラムは複数のパラメータで地形生成を制御する。
主要な入力パラメータ:
- terrain_size:地形の解像度(頂点数)
- height_scale:地形の高低差の倍率
- 侵食パラメータ:erosion_rate、deposition_rate、iterations
- ノイズパラメータ:シード値、オクターブ数、persistence
出力情報:
- 3D地形の描画結果
- 地形の色分け(高度に応じた草原、岩、雪の表現)
- 影の描画による即時の視覚的確認
パラメータ変更の方法:
- ソースコード内の数値を変更し、プログラムを再実行する
1.5 検証(結果の確認と考察)
プログラムの実行結果を観察し、パラメータの影響を考察する。
基本認識:
- パラメータを変えると結果が変わる。その変化を観察することが実験である
- 「良い結果」「悪い結果」は目的によって異なる
観察のポイント:
- 地形の起伏はどう変化するか
- 山脈や谷の形成パターンは自然に見えるか
- 侵食による細部の変化は視認できるか
- 処理時間はどの程度変化するか
- 描画のフレームレートは維持されているか
2. 間違いの原因と対処方法
2.1 プログラムのミス(人為的エラー)
プログラムがエラーで停止する
- 原因:構文エラー、Panda3Dがインストールされていない
- 対処方法:エラーメッセージを確認し、提供されたコードと比較する
ウィンドウが表示されない
- 原因:Panda3Dの初期化に失敗している、またはグラフィックドライバの問題
- 対処方法:Panda3Dのインストールを確認し、必要に応じてグラフィックドライバを更新する
地形生成に時間がかかる
- 原因:terrain_sizeが大きい、または侵食シミュレーションの反復回数が多い
- 対処方法:これは正常な動作である。生成が完了するまで待つ
2.2 期待と異なる結果が出る場合
地形が平坦すぎる
- 原因:height_scaleが小さすぎる、またはノイズのpersistenceが低すぎる
- 対処方法:height_scaleを80から160程度に増加させて観察する
地形が険しすぎる
- 原因:height_scaleが大きすぎる、またはリッジノイズの比率が高すぎる
- 対処方法:height_scaleを40程度まで下げて確認する
侵食の効果が見えない
- 原因:侵食シミュレーションの反復回数が少なすぎる
- 対処方法:iterationsを5000から20000程度に増加させる。処理時間も増加する点に注意する
フレームレートが低下する
- 原因:terrain_sizeが大きすぎる、またはLOD設定が不適切
- 対処方法:setNear/setFarの値を調整し、遠距離の詳細度を下げる
3. 実験レポートのサンプル
侵食シミュレーションが地形の自然さに与える影響
実験目的:
侵食シミュレーションの反復回数が地形の自然さに与える影響を確認し、処理時間とのバランスが取れる設定を見つける。
実験計画:
他のパラメータを固定し、侵食シミュレーションのiterations値を変化させて地形を比較する。
実験方法:
プログラムのiterations値を変更して実行し、以下の基準で評価する。
- 谷の形成:水流による谷が視認できるか
- 尾根の鮮明さ:山の尾根が自然に形成されているか
- 処理時間:地形生成にかかる時間
実験結果:
| iterations | 処理時間 | 谷の形成 | 尾根の鮮明さ | 総合評価 |
|---|---|---|---|---|
| xxxx | x秒 | x | x | x |
| xxxx | x秒 | x | x | x |
| xxxx | x秒 | x | x | x |
| xxxx | x秒 | x | x | x |
考察:
- (例文)iterations=xxxxでは侵食の効果がほとんど見られず、ノイズそのままの人工的な地形となった
- (例文)iterations=xxxxでは谷の形成が始まり、水の流れを想起させる地形パターンが現れた
- (例文)iterations=xxxxでは谷と尾根が明確になり、自然な山岳地形に近づいた。処理時間は許容範囲内であった
- (例文)iterations=xxxx以上では視覚的な改善が小さくなり、処理時間の増加に見合わなくなった
結論:
(例文)本実験においては、iterations=xxxxが処理時間と地形の自然さのバランスが取れた設定であった。即時生成が必要な場合はxxxxを、品質を優先する場合はxxxxが適切である。用途に応じてパラメータを調整する必要性が確認できた。