1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GodotのArrayMeshで三角形を作ってMaterialと衝突領域を設定する

Last updated at Posted at 2023-04-07

はじめに

 GODOT4.0でGDScriptから自由な形の三角形を作りたくなったのでArrayMeshについて調査しました。
 Materialと衝突領域も設定したいと思います。

やりたいこと

 ・GDScriptで3D空間の3点を指定して三角形を作る。
 ・Materialを設定する
 ・衝突領域を設定する

GITHUB

 本記事を最後まで実行したプロジェクトです。

事前準備:クリックしたところの3次元座標をprintする

 ArrayMeshを確認する事前準備として、地面を作成して。地面にタッチ(クリック)したところの3D空間座標をprintするまで実装します。

新規シーンを作成して必要なノードを追加

 Godotで新規プロジェクトを作成して、下記のようにノードを構成します。
スクリーンショット (600).png
World : Node3D
 ┝━Camera3D
 ┝━DirectionalLight3D
 ┝━StaticBody3D →地面にする
 ┃  ┝━MeshInstance3D 
 ┃  ┗━CollisionShape3D
 ┝━ArrayMeshTriangle : MeshInstance3D →三角形の描画用
 ┗━ArrayMeshLineStrip : MeshInstance3D →クリックした点を結ぶ線の描画用

  • Camera3D
    地面を見下ろすように設定します。
    Node3D/Transform/Position : (x,y,z)=(0m, 4m, 7m)
    Node3D/Transform/Rotation : (x,y,z)=(-45°, 0°, 0°)
  • DirectionalLight3D
    左斜め前方に影が伸びるように設定します。
    Light3D/Light/Shadow/Enable : オン
    Node3D/Transform/Position : (x,y,z)=(-10m, 0m, 0m) ※DirectionalLight3Dのpositionはどこでもよい
    Node3D/Transform/Rotation : (x,y,z)=(-10°, 45°, 0°)
  • StaticBody3D
    XZ平面に10m×10mの地面を作成。y=0が地面になるようにします。
    Node3D/Transform/Position : (x,y,z)=(0m, -0.5m, 0m)
  • StaticBody3D/MeshInstance3D
    MeshInstance3D/Mesh : 新規BoxMesh
    MeshInstance3D/Mesh/Size : (x,y,z)=(10m, 1m, 10m)
  • StaticBody3D/CollisionShape3D
    CollisionShape3D/Shape : 新規BoxShape3D
    CollisionShape3D/Shape/Size : (x,y,z)=(10m, 1m, 10m)

スクリーンショット (603).png

「マウスでタッチ操作をエミュレート」を有効する

 プロジェクトメニューのプロジェクト設定の「入力デバイス/マウスでタッチ操作をエミュレート」をオンにします。
スクリーンショット (604).png

タッチしたところの3D座標を表示するスクリプトを実装

 ルートノードのWorldを右クリックして、「スクリプトにアタッチ」して下記のようにスクリプトを修正します。
 仕組みなど知りたい場合はこの記事を参考にしてください。

extends Node3D

@export var m_ray_length_m = 1000		# [m]十分な長さにする
	
var m_touch_pos = Vector2.ZERO
var m_f_is_screen_touch = false
var m_f_pre_is_screen_touch = false

func _input(event):
	if ( event is InputEventScreenDrag ) or ( event is InputEventScreenTouch ):
		m_touch_pos = event.position
		# スマホタッチ中フラグ
		m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )
		
func _physics_process(delta):
	# 前周期はタッチしていなくて、今周期でタッチした場合を検出したい
	if m_f_is_screen_touch and !m_f_pre_is_screen_touch:
		var camera = get_viewport().get_camera_3d()

		# カメラを利用して3D空間のカメラ位置とタッチしたピクセルに対応する方向の1000m先の位置を計算する
		var from3d = camera.project_ray_origin(m_touch_pos)
		var to3d = from3d + camera.project_ray_normal(m_touch_pos) * m_ray_length_m

		# 3D ray physics queryの作成
		var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
		query.collide_with_areas = true		# Area3Dを検知できるようにする
		
		# Godotの3Dの物理とコリジョンを保存しているspaceという情報を使用してオブジェクト検出
		var space_state = get_world_3d().direct_space_state
		var result = space_state.intersect_ray(query)
		if result:
			print("result.position:", result.position)

	# 今週機のタッチ/タッチしていない状態を保存する
	m_f_pre_is_screen_touch = m_f_is_screen_touch	

 実行します。
 地面をタッチ(クリック)すると、タッチ(クリック)した点の3D空間座標を出力します。
 地面表面がy=0になるようにStaticBody3Dを配置したので、y=0で、xとz座標に値が設定されています。
スクリーンショット (606).png

ArrayMeshでクリックした点を線でつなぐ

 三角形を描画する前に、クリックしたところを見える化します。
 せっかくなのでArrayMeshのLINESTRIPを使用して描画します。

 Vector3Dの配列をArrayMeshインスタンスにセットして、シーンの内のArrayMeshLineStrip(MeshInstance3D)に設定します。

 スクリプト全体を示し、そのあとに修正点について説明します。

extends Node3D

@export var m_ray_length_m = 1000		# [m]十分な長さにする
	
var m_touch_pos = Vector2.ZERO
var m_f_is_screen_touch = false
var m_f_pre_is_screen_touch = false

func _input(event):
	if ( event is InputEventScreenDrag ) or ( event is InputEventScreenTouch ):
		m_touch_pos = event.position
		# スマホタッチ中フラグ
		m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )
		
func _physics_process(delta):
	# 前周期はタッチしていなくて、今周期でタッチした場合を検出したい
	if m_f_is_screen_touch and !m_f_pre_is_screen_touch:
		var camera = get_viewport().get_camera_3d()

		# カメラを利用して3D空間のカメラ位置とタッチしたピクセルに対応する方向の1000m先の位置を計算する
		var from3d = camera.project_ray_origin(m_touch_pos)
		var to3d = from3d + camera.project_ray_normal(m_touch_pos) * m_ray_length_m

		# 3D ray physics queryの作成
		var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
		query.collide_with_areas = true		# Area3Dを検知できるようにする
		
		# Godotの3Dの物理とコリジョンを保存しているspaceという情報を使用してオブジェクト検出
		var space_state = get_world_3d().direct_space_state
		var result = space_state.intersect_ray(query)
		if result:
			print("result.position:", result.position)
			add_vertex_position(result.position)

	# 今週機のタッチ/タッチしていない状態を保存する
	m_f_pre_is_screen_touch = m_f_is_screen_touch	

@onready var m_vertex_list = Array()
func add_vertex_position(v3_pos):
	# Line Strip
	m_vertex_list.append(v3_pos)
	create_linestrip_array_mesh(m_vertex_list)

func create_linestrip_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
	
	for v3 in vertex_list:
		vertices.push_back(v3)

	var arrays = Array()
	arrays.resize(ArrayMesh.ARRAY_MAX)
	arrays[ArrayMesh.ARRAY_VERTEX] = vertices
	var tmpMesh = ArrayMesh.new()
	tmpMesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINE_STRIP, arrays)
	$ArrayMeshLineStrip.mesh = tmpMesh
  • _physics_processの修正
    タッチした点の3D空間座標のprintの次の行に「add_vertex_position(result.position)」を追加しました。

  • @onready var m_vertex_list = Array()
     タッチした点の3D空間座標をすべて記憶するための配列を用意します。
     @onreadyアノテーションを使用しているので、本シーンがアクティブになったタイミングで設定されます。

  • add_vertex_positionメソッド追加
    タッチした点の3D空間座標(Vector3)をm_vertex_list配列に追加して、create_linestrip_array_meshメソッドを呼び出します。

func add_vertex_position(v3_pos):
	# Line Strip
	m_vertex_list.append(v3_pos)
	create_linestrip_array_mesh(m_vertex_list)
  • func create_linestrip_array_mesh(vertex_list):
     ArrayMeshに渡す3D空間の点を頂点(Vertex)と呼びます。OpenGLと同じです。
     頂点配列は PackedVector3Arrayを使用します。
     「Packed」とついているのでGPUに転送するための条件を保証したメモリ領域を保証するのだと思います(先頭アドレスが~byteの倍数、連続した領域、隙間がないなど)。
     3D空間座標のVector3配列をPackedVector3Array()にコピーします。
	var vertices = PackedVector3Array()
	
	for v3 in vertex_list:
		vertices.push_back(v3)

 ArrayMesh.ARRAY_MAX(13)の配列を用意して、ArrayMesh.ARRAY_VERTEX番目(0番目)に頂点配列を設定します。
 1~12には必要に応じて法線情報(後述)、テクスチャ情報など設定しますが今は設定しません。

	var arrays = Array()
	arrays.resize(ArrayMesh.ARRAY_MAX)
	arrays[ArrayMesh.ARRAY_VERTEX] = vertices

 ArrayMeshインスタンスを生成して、add_surface_from_arraysメソッドで頂点配列を元にMeshを生成します。

	var tmpMesh = ArrayMesh.new()
	tmpMesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINE_STRIP, arrays)

 生成したMeshをシーンのMeshInstance3Dに設定します。

	$ArrayMeshLineStrip.mesh = tmpMesh

 実行します。
 クリックするごとに線が描画されます。
スクリーンショット (607).png

3点クリックしたところで三角形を描画する

 3点クリックするたびにArrayMeshで三角形を描画します。
 スクリプト全体を示し、そのあとに修正点について説明します。

extends Node3D

@export var m_ray_length_m = 1000		# [m]十分な長さにする
	
var m_touch_pos = Vector2.ZERO
var m_f_is_screen_touch = false
var m_f_pre_is_screen_touch = false

func _input(event):
	if ( event is InputEventScreenDrag ) or ( event is InputEventScreenTouch ):
		m_touch_pos = event.position
		# スマホタッチ中フラグ
		m_f_is_screen_touch = event.is_pressed() or ( event is InputEventScreenDrag )
		
func _physics_process(delta):
	# 前周期はタッチしていなくて、今周期でタッチした場合を検出したい
	if m_f_is_screen_touch and !m_f_pre_is_screen_touch:
		var camera = get_viewport().get_camera_3d()

		# カメラを利用して3D空間のカメラ位置とタッチしたピクセルに対応する方向の1000m先の位置を計算する
		var from3d = camera.project_ray_origin(m_touch_pos)
		var to3d = from3d + camera.project_ray_normal(m_touch_pos) * m_ray_length_m

		# 3D ray physics queryの作成
		var query = PhysicsRayQueryParameters3D.create(from3d, to3d)
		query.collide_with_areas = true		# Area3Dを検知できるようにする
		
		# Godotの3Dの物理とコリジョンを保存しているspaceという情報を使用してオブジェクト検出
		var space_state = get_world_3d().direct_space_state
		var result = space_state.intersect_ray(query)
		if result:
			print("result.position:", result.position)
			add_vertex_position(result.position)

	# 今週機のタッチ/タッチしていない状態を保存する
	m_f_pre_is_screen_touch = m_f_is_screen_touch	

@onready var m_vertex_list = Array()
@onready var m_triangle_vertex_list = Array()

func add_vertex_position(v3_pos):
	# Line Strip
	m_vertex_list.append(v3_pos)
	create_linestrip_array_mesh(m_vertex_list)

	# Triangle
	m_triangle_vertex_list.append(v3_pos)
	if m_triangle_vertex_list.size() >= 3:
		create_triangle_array_mesh(m_triangle_vertex_list)
		m_triangle_vertex_list.clear()

func create_linestrip_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
	
	for v3 in vertex_list:
		vertices.push_back(v3)

	var arrays = Array()
	arrays.resize(ArrayMesh.ARRAY_MAX)
	arrays[ArrayMesh.ARRAY_VERTEX] = vertices
	var tmpMesh = ArrayMesh.new()
	tmpMesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINE_STRIP, arrays)
	$ArrayMeshLineStrip.mesh = tmpMesh

func create_triangle_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
		
	for v3 in vertex_list:
		vertices.push_back(v3)

	var arrays = Array()
	arrays.resize(ArrayMesh.ARRAY_MAX)
	arrays[ArrayMesh.ARRAY_VERTEX] = vertices
	var tmpMesh = ArrayMesh.new()
	tmpMesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
	$ArrayMeshTriangle.mesh = tmpMesh
  • @onready var m_triangle_vertex_list = Array()
     三角形の3点を保存するための配列を宣言します。

  • func add_vertex_position(v3_pos):
    タッチした点の3D空間座標をm_triangle_vertex_listに追加します。
     3点(以上)になった場合はcreate_triangle_array_meshメソッドに3点の配列を引数として呼び出します。
     呼び出した後はm_triangle_vertex_listをクリアします。

	# Triangle
	m_triangle_vertex_list.append(v3_pos)
	if m_triangle_vertex_list.size() >= 3:
		create_triangle_array_mesh(m_triangle_vertex_list)
		m_triangle_vertex_list.clear()
  • func create_triangle_array_mesh(vertex_list):
    LINEStripの処理とほとんど同じですが、add_surface_from_arrays()メソッドの第1引数をMesh.PRIMITIVE_TRIANGLESに変更しました。
func create_triangle_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
		
	for v3 in vertex_list:
		vertices.push_back(v3)

	var arrays = Array()
	arrays.resize(ArrayMesh.ARRAY_MAX)
	arrays[ArrayMesh.ARRAY_VERTEX] = vertices
	var tmpMesh = ArrayMesh.new()
	tmpMesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
	$ArrayMeshTriangle.mesh = tmpMesh

 実行します。
 3点は画面を見て時計回りにクリックしてください。

 三角形メッシュの場合頂点を格納する順番はとても重要です。3点を順番にたどったときに時計回りに見える面を表面、反対側を裏面と判断します。裏面は特に指定がなければ描画されません。本記事では実装しませんが3点を時計回りに並べなおす必要がありそうです。

https://docs.godotengine.org/en/latest/classes/class_arraymesh.html
Note: Note: Godot uses clockwise winding order for front faces of triangle primitive modes.

 3点毎に三角形が表示されるようになりました。
スクリーンショット (633).png

法線を追加する

 面が暗いのは法線を指定していないためです。
 法線とは面に対して垂直なベクトル(下図の緑の矢印)で、ライティング処理に使用されます。
スクリーンショット (609).png

func create_triangle_array_mesh(vertex_list)に法線を設定する処理を追加する

今回追加している面は上方向(Y軸のプラス方向)を向いています。
create_triangle_array_mesh()に法線として上方向(Vector3.UP=(0,1,0)を設定する処理を追加しました。

func create_triangle_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
	var normals = PackedVector3Array()
	
	for v3 in vertex_list:
		v3.y = 0.1
		vertices.push_back(v3)
		normals.push_back(Vector3.UP)
		
	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)
	$ArrayMeshTriangle.mesh = tmpMesh
  • var normals = PackedVector3Array()
     法線はPackedVector3Array配列を使用します。

  • normals.push_back(Vector3.UP)
     上方向のベクトルを法線として登録します。
     描画した三角形が見えるように、高さy=0.1mを設定します。

	for v3 in vertex_list:
		v3.y = 0.1
		vertices.push_back(v3)
		normals.push_back(Vector3.UP)
  • arrays[ArrayMesh.ARRAY_NORMAL] = normals
     配列のArrayMesh.ARRAY_NORMAL番目(1番目)に法線配列を設定します。

 実行します。地面の上面と同じ色になりました。
 
スクリーンショット (610).png

(試しに)頂点ごとに法線の向きを変える

 法線はライティング処理に使用されるのを確認します。

 先ほど追加した「normals.push_back(Vector3.UP)」を削除して、下記のように3点分の法線として、Vector3.UP、Vector3.BACK、Vector3.DOWNを追加してみます。

func create_triangle_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
	var normals = PackedVector3Array()
	
	for v3 in vertex_list:
		v3.y = 0.1
		vertices.push_back(v3)

	normals.push_back(Vector3.UP)
	normals.push_back(Vector3.BACK)
	normals.push_back(Vector3.DOWN)

	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)
	$ArrayMeshTriangle.mesh = tmpMesh

 実行します。
 描画しているのは3点の平面の三角形ですが、立体的に見えるようになりました。
 1クリック目は三角形の右側の点で、法線はVector3.UPを指定しているので、地面の上面と同じ色(同じライティング)になります。
 2クリック目は三角形の下側の点で、法線はVector3.BACK(手前側)を指定しているので、地面の手前と明るい色(同じライティング)になります。
 1クリック目は三角形の左上の点で、法線はVector3.DOWN(地面の裏側)を指定しているので、真っ暗です。

 3点の中間は自動で計算しているのが、頂点があるように見えますね。
 本来は球を疑似した三角形メッシュでもライティングで球に見えるようにするためのものだと思います。

スクリーンショット (615).png

Materialを設定して緑色にする

 Materialを追加して、緑色に変更します。
 法線はVector3.UPに戻しています。

func create_triangle_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
	var normals = PackedVector3Array()
	
	for v3 in vertex_list:
		v3.y = 0.1
		vertices.push_back(v3)
		normals.push_back(Vector3.UP)

	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)
	$ArrayMeshTriangle.mesh = tmpMesh

	# Materialを設定
	var material = StandardMaterial3D.new()
	material.albedo_color = Color(0, 1, 0)
	$ArrayMeshTriangle.mesh.surface_set_material(0, material)

 最後に追加した3行でMaterialを作成し、albedo_colorに緑をにして、Meshに設定します。

スクリーンショット (617).png

Materialファイルを作成してArrayMeshに適用する

 Materialをコードからすべて設定するのは大変なので、Materialリソースファイルを作成します。

 ファイルシステムの「res」を右クリックして、新規からリソースを実行します。
スクリーンショット (623).png

 StandardMaterial3Dを選択します。
スクリーンショット (619).png
 ファイル名を「triangle_material.tres」に変更して保存します。
スクリーンショット (620).png
 作成されました。
スクリーンショット (624).png

 作成した「triangle_material.tres」をダブルクリックして、インスペクタのBaseMaterial3Dのalbedoを開いて、Colorを赤に変更します。
スクリーンショット (625).png

 スクリプトから「triangle_material.tres」を読み込んでArrayMeshに設定します。

var m_material = preload("res://triangle_material.tres")

func create_triangle_array_mesh(vertex_list):
	var vertices = PackedVector3Array()
	var normals = PackedVector3Array()
	
	for v3 in vertex_list:
		v3.y = 0.1
		vertices.push_back(v3)
		normals.push_back(Vector3.UP)

	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)
	$ArrayMeshTriangle.mesh = tmpMesh

	# Materialを設定
	$ArrayMeshTriangle.mesh.surface_set_material(0, m_material)

 実行します。赤くなりました。
スクリーンショット (627).png

Collisionを設定する

 Meshから衝突形状を生成して設定することができます。

 シーンのルートノードのWorldを右クリックして子ノードを追加からArea3Dを選択します。
 Area3Dを右クリックして子ノードを追加からCollisionShape3Dを追加します。

 衝突形状を見やすいようにCollisionShape3DのインスペクタのNode3D/Transform/Positionのyを1.0mにしておきます。

 スクリーンショット (628).png

 create_triangle_array_mesh関数の最後に以下を追加します。
 create_trimesh_shapeメソッドはMesh形状からConcavePolygonShape3Dを生成します。

	# MeshからCollision形状(ConcavePolygonShape3D)生成
	$Area3D/CollisionShape3D.shape = tmpMesh.create_trimesh_shape()

 目視で確認できるように、デバッグメニューから「コリジョン形状を表示」をチェックします。
 スクリーンショット (631).png

 実行します。
 3点をクリックして赤い三角形を表示すると、その1m上に衝突形状が表示されているのがわかります。
スクリーンショット (632).png

 以上。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?