GogotのArrayMeshで落とし穴を作る

# 目的
　Godot4.0のArrayMeshを作って、落とし穴を作ります。

　前々回の下記記事でArrayMeshからCollisionShape3Dを設定する方法を書きましたが、本当に動的に作成したCollisionShape3Dが使えるのか確認したいと思います。

https://qiita.com/FootInGlow/items/54a1cccc8e12f44ba707

# やりたいこと
　地面にカーソルで操作できる穴を作ります。
　地面の上にはRigidBody3Dの球や長方形を置いておいて、穴が下を通ると落ちることを確認します。
　![落ちる.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/8ea5dc98-e53b-99d7-d4a6-5f2d415cad12.png)

# 新規プロジェクト作成してノードを追加する
　Godotで新規プロジェクトを作成して、下記のようにノードを構成します。
　またHoleOnGroundを右クリックして「スクリプトをアタッチ」してください。
![スクリーンショット (662).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/b8799613-f7a1-d063-2389-5b800b932d16.png)

　obj1～obj4は球を配置、obj5のみ長方形を配置しています。

| ノード名:ノードクラス名 |インスペクタ変更値|
|:-|:-|
|World : Node3D||
|　┝━Camera3D|Position : (0m, 4m, 7m)  Rotation : (-45°, 0°, 0°)
|　┝━DirectionalLight3D|Position : (-10m, 0m, 0m)  Rotation : (-10°, 45°, 0°) shadow:オン|
|　┝━HoleOnGround : StaticBody3D||
|　┃　　┝━MeshInstance3D　|設定不要(GDScriptから設定する）|
|　┃　　┗━CollisionShape3D|設定不要(GDScriptから設定する）|
|　┗━Objects : Node3D||
|     ┝━obj1 : RigidBody3D|Position : (-2m, 4m, 0m)|
|     ┃　　┝━MeshInstance3D|新規SphereMesh3D Radius=0.2m, Height:0.4m|
|     ┃　　┗━CollisionShape3D|新規SphereShape3D Radius=0.2m|
|     ┝━obj2 : RigidBody3D|Position : (0m, 4m, 3m)|
|     ┃　　┝━MeshInstance3D|同上|
|     ┃　　┗━CollisionShape3D|同上|
|     ┝━obj3 : RigidBody3D|Position : (0m, 4m, -2m)|
|     ┃　　┝━MeshInstance3D|同上|
|     ┃　　┗━CollisionShape3D|同上|
|     ┝━obj4 : RigidBody3D|Position : (2m, 4m, 0m)|
|     ┃　　┝━MeshInstance3D|同上|
|     ┃　　┗━CollisionShape3D|同上|
|     ┗━obj5 : RigidBody3D|Position : (-2m, 4m, -2m)|
|      　　┝━MeshInstance3D|新規BoxMesh3D x:0.5m, y:1m, z:0.5m|
|      　　┗━CollisionShape3D|新規BoxShape3D x:0.5m, y:1m, z:0.5m|

![スクリーンショット (663).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/55759eea-c1e8-74d0-33ac-a486f8547ce8.png)

# GDScriptを作成する
 下記のように実装しました。

```GDScript
extends StaticBody3D

#
# カーソルキー操作でhole（落とし穴）を動かす
#
const SPEED = 3.0

# 地面の大きさ
var m_d_ground_w_m : float = 20.0
var m_d_ground_d_m : float = 20.0

# 落とし穴
var m_v3_hole_pos = Vector3(0, 0, 0)
var m_d_hole_x_m : float = 1.0
var m_d_hole_z_m : float = 1.0

func _physics_process(delta):
	var velocity = Vector3.ZERO
	var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if direction:
		velocity.x = direction.x
		velocity.z = direction.z

	# holeの位置を移動
	m_v3_hole_pos += velocity.normalized() * SPEED * delta
	set_hole_and_ground_array_mesh()
	
# 三角形２つで四角形を作る
# l:Left, t:Top, r:Right, b:bottom
func add_square(vertices, normals, x_l, z_t, x_r, z_b):
	vertices.push_back(Vector3(x_l, 0, z_t))
	vertices.push_back(Vector3(x_r, 0, z_t))
	vertices.push_back(Vector3(x_r, 0, z_b))
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)

	vertices.push_back(Vector3(x_r, 0, z_b))
	vertices.push_back(Vector3(x_l, 0, z_b))
	vertices.push_back(Vector3(x_l, 0, z_t))
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)
	

# 落とし穴ArrayMeshを作成する
func set_hole_and_ground_array_mesh():

	var vertices = PackedVector3Array()
	var normals  = PackedVector3Array()
	# 地面のXZ平面上の左上、右下の位置を設定
	var ground_LT = Vector3(-m_d_ground_w_m/2.0 , 0, -m_d_ground_d_m/2.0)
	var ground_RB = Vector3( m_d_ground_w_m/2.0 , 0,  m_d_ground_d_m/2.0)
	# holeのXZ平面上の左上、右下の位置を設定
	var hole_size = Vector3( m_d_hole_x_m , 0,  m_d_hole_z_m)
	var hole_LT = m_v3_hole_pos - hole_size/2.0
	var hole_RB = m_v3_hole_pos + hole_size/2.0

	# 3×３グリッドの上３ブロック
	add_square(vertices, normals, ground_LT.x, ground_LT.z ,ground_RB.x, hole_LT.z)
	# 3×３グリッド、左（holeの左）
	add_square(vertices, normals, ground_LT.x, hole_LT.z ,hole_LT.x, hole_RB.z)
	# 3×３グリッド、右（holeの右）
	add_square(vertices, normals, hole_RB.x, hole_LT.z ,ground_RB.x, hole_RB.z)
	# 3×３グリッドの下３ブロック
	add_square(vertices, normals, ground_LT.x, hole_RB.z ,ground_RB.x, ground_RB.z)
	
	var arrays = Array()
	arrays.resize(ArrayMesh.ARRAY_MAX)
	arrays[ArrayMesh.ARRAY_VERTEX] = vertices
	arrays[ArrayMesh.ARRAY_NORMAL] = normals

	var tmpMesh = ArrayMesh.new()
	tmpMesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
	$MeshInstance3D.mesh = tmpMesh
	
	# MeshからCollision形状(ConcavePolygonShape3D)生成
	$CollisionShape3D.shape = tmpMesh.create_trimesh_shape()
```

# 実行します。
　4つの球の真ん中にあるのが、落とし穴です。カーソルキーで動かすことができます。
![スクリーンショット (665).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/757b3e21-c9d7-73ea-bf33-995583b97a8d.png)

　地面と落とし穴の色が似ていて見にくいですが、穴が下に来ると球や長方形が落ちます。
　RigitBody3Dが物理エンジンで制御されるので、リアルに落ちていきます。
　![スクリーンショット (674).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/f1c49e9f-a4ec-1eac-ea36-86878c9fff81.png)

# スクリプト説明
　スクリプトを説明します。


```GDScript
extends StaticBody3D

#
# カーソルキー操作でhole（落とし穴）を動かす
#
const SPEED = 3.0
```
 SPEEDはカーソルキーによる落とし穴の移動速度を定義しています。

```GDScript
# 地面の大きさ
var m_d_ground_w_m : float = 20.0
var m_d_ground_d_m : float = 20.0
```
　地面は原点を中心に配置するので、縦横±10mの正方形になります。

```GDScript
# 落とし穴
var m_v3_hole_pos = Vector3(0, 0, 0)
var m_d_hole_x_m : float = 1.0
var m_d_hole_z_m : float = 1.0
```
　落とし穴をし初期位置と大きさを定義します。m_v3_hole_posは落とし穴の正方形の中心です。

```GDScript
func _physics_process(delta):
	var velocity = Vector3.ZERO
	var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
	if direction:
		velocity.x = direction.x
		velocity.z = direction.z

	# holeの位置を移動
	m_v3_hole_pos += velocity.normalized() * SPEED * delta
	set_hole_and_ground_array_mesh()
```
　CharacterBody3DのGDScriptテンプレートからカーソルキー上下左右の処理を流用しました。
　最後にm_v3_hole_posにx軸とz軸の移動量を足して、set_hole_and_ground_array_mesh()を呼び出して落とし穴メッシュを作り直しています。

```GDScript
# 三角形２つで四角形を作る
# l:Left, t:Top, r:Right, b:bottom
func add_square(vertices, normals, x_l, z_t, x_r, z_b):
	vertices.push_back(Vector3(x_l, 0, z_t))
	vertices.push_back(Vector3(x_r, 0, z_t))
	vertices.push_back(Vector3(x_r, 0, z_b))
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)

	vertices.push_back(Vector3(x_r, 0, z_b))
	vertices.push_back(Vector3(x_l, 0, z_b))
	vertices.push_back(Vector3(x_l, 0, z_t))
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.UP)
```
　4角形を作るメソッドです。
　引数に四角形の左上(x,z)=(x_l,z_y)と右下(x,z)=(x_r, z_b)を指定すると、三角形２つ作って、長方形を作成します。
　時計回りに頂点をpush_backします。
　また今回生成する平面の向きは上（Y軸プラス方向）なので法線はVector3.UPを指定します。
```GDScript
# 落とし穴ArrayMeshを作成する
func set_hole_and_ground_array_mesh():
```
　ArrayMeshとCollisionShape3Dを作成するメソッドです。
```GDScript

	var vertices = PackedVector3Array()
	var normals  = PackedVector3Array()
```
　PackedVector3Arrayを作ります。
```GDScript
	# 地面のXZ平面上の左上、右下の位置を設定
	var ground_LT = Vector3(-m_d_ground_w_m/2.0 , 0, -m_d_ground_d_m/2.0)
	var ground_RB = Vector3( m_d_ground_w_m/2.0 , 0,  m_d_ground_d_m/2.0)

	# holeのXZ平面上の左上、右下の位置を設定
	var hole_size = Vector3( m_d_hole_x_m , 0,  m_d_hole_z_m)
	var hole_LT = m_v3_hole_pos - hole_size/2.0
	var hole_RB = m_v3_hole_pos + hole_size/2.0
```
　地面と落とし穴の左上、右下の座標を求めています。下図が参考になると思います。
（本当はZ軸は手前を向いているので矢印が逆ですが、今回のケースでは大丈夫だと思います）
![スクリーンショット (682).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/30907009-e808-d39c-93d8-6f0ce78fb75d.png)


```GDScript

	# 3×３グリッドの上３ブロック
	add_square(vertices, normals, ground_LT.x, ground_LT.z ,ground_RB.x, hole_LT.z)
	# 3×３グリッド、左（holeの左）
	add_square(vertices, normals, ground_LT.x, hole_LT.z ,hole_LT.x, hole_RB.z)
	# 3×３グリッド、右（holeの右）
	add_square(vertices, normals, hole_RB.x, hole_LT.z ,ground_RB.x, hole_RB.z)
	# 3×３グリッドの下３ブロック
	add_square(vertices, normals, ground_LT.x, hole_RB.z ,ground_RB.x, ground_RB.z)
```
　下図のように3×３ブロックに分けて、左の図のように、一番上の3ブロック、下の3ブロックと右の図のように中央(落とし穴）の左、中央の右と、全部で４つの長方形に分けて、四角形のメッシュを作成します。
![スクリーンショット (678).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691050/19736ff1-72c7-51cf-aa1d-f6b1885d45e5.png)


```GDScript
	var arrays = Array()
	arrays.resize(ArrayMesh.ARRAY_MAX)
	arrays[ArrayMesh.ARRAY_VERTEX] = vertices
	arrays[ArrayMesh.ARRAY_NORMAL] = normals

	var tmpMesh = ArrayMesh.new()
	tmpMesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
	$MeshInstance3D.mesh = tmpMesh
	
```
　ArrayMeshを生成して、MeshInstance3Dに設定します。
```GDScript
	# MeshからCollision形状(ConcavePolygonShape3D)生成
	$CollisionShape3D.shape = tmpMesh.create_trimesh_shape()
```
　ArrayMeshからCollision形状(ConcavePolygonShape3D)を生成してCollisionShape3Dに設定しています。

以上