LoginSignup
0

Godot4.0 Path3DとPathFollow3Dを利用して曲線の道をメッシュで生成する

Last updated at Posted at 2023-05-09

Godot4.0のメッシュ生成では三角形の頂点を反時計周りに配置した面が表でしたが、バグだったようで、4.1rc1を使用すると時計周りに配置した面が表になりました。

目的

 Path3DとPathFollow3Dを利用して曲線の道をメッシュで生成します。
スクリーンショット (770).png

Path3D/PathFollow3D

GODOT DOCS : Beziers, curves and paths
https://docs.godotengine.org/en/latest/tutorials/math/beziers_and_curves.html#

確認用のシーンを作成する

ノード名:ノードクラス名 インスペクタ変更値
World : Node3D
 ┣━Camera3D Position : (0m, 5m, 0m) Rotation : (-90°, 0°, 0°)
 ┣━DirectionalLight3D Position : (-10m, 0m, 0m) Rotation : (-90°, 0°, 0°)
 ┣━MeshInstance3D
 ┗┳Path3D ※設定は後述
  ┗┳PathFollow3D
   ┣━left : Marker3D Position : (-0.5m, 0m, 0m)
   ┗━right : Marker3D Position : (0.5m, 0m, 0m)

 シーンは下記のようになります。(スクリプトをアタッチした後です)
スクリーンショット (756).png

※Path3Dを選択した状態で、インスペクタのPath3D/Curve/Curve3Dをクリックして詳細設定を表示します。
 Pointsを開いて「+要素を追加」ボタンを2回押下し、Pointsを下記のように設定します。
 Points[0] : Position(-2m, 0m, 1m), Out:(0m, 0m, 5m)
 Points[1] : Position( 2m, 0m, -1m), In :(0m, 0m, -5m)
スクリーンショット (757).png

 3Dビューで上面図を選択すると下記のような表示になります。
 青い細かい矢印の線がPath3Dの曲線です。
スクリーンショット (758).png

PathFollow3Dと子ノードのMarker3Dのleftとright

 Path3Dの子に配置したPathFollow3DのインスペクタにはProgress Ratioがあります。Progress Ratioを0から1まで変化すると、PathFollow3Dの位置がPATH3Dの曲線上を移動し向きも変化します。Progress Ratioを0に設定した時はPath3Dの曲線の開始点とその時の向き、Progress Ratioに1を設定したときはPath3Dの曲線の終了位置とその時の向きにPathFollow3Dノードが移動します。
 PathFollow3Dの子のleftノードとrightノードも追従して位置、向きがかわります。
 left.transform.origin.x=-0.5m、right.transform.origin.x=0.5mに設定して、Progress Ratioを0.1刻みで動かした時の位置を、leftを赤、rightを青の点で表したのが下図になります。
スクリーンショット (766).png
 Progress Ratioが0の時の赤と青の点と、Progress Ratiog0.1の時の赤と青の点を使って、四角形を作ることができます。同じように下図のように四角形をつくることができます。四角形は三角形2つで作ることができるので、ArrayMeshで作れそうです。
スクリーンショット (765).png

スクリプトを実装する

 シーン内のルートノードのWorldを右クリックして、スクリプトのアタッチを実行して、下記のように変更します。

extends Node3D

@export var m_d_path_width  : float = 0.5	# 道の幅
@export var m_d_path_devide : float = 10.0	# 道を四角形に分割する数。大きいほど道がなめらかになります。

var m_v3_path_lefts = []
var m_v3_path_rights = []

func _ready():
	create_path($Path3D/PathFollow3D)
	create_path_mesh($MeshInstance3D)

func create_path(node_pf : PathFollow3D):
	# 道幅を設定
	var node_left : Marker3D = node_pf.get_node("left")
	var node_right : Marker3D = node_pf.get_node("right")
	node_left.transform.origin.x = -m_d_path_width / 2.0
	node_right.transform.origin.x = m_d_path_width / 2.0
	
	# マーカー追加する
	for i in range(m_d_path_devide+1):
		node_pf.progress_ratio = i /m_d_path_devide 

		var l_pos = node_pf.transform * node_left.position
		m_v3_path_lefts.append(l_pos)
		var r_pos = node_pf.transform * node_right.position
		m_v3_path_rights.append(r_pos)
	

func create_path_mesh(node_mesh : MeshInstance3D):
	var vertices = PackedVector3Array()
	var normals  = PackedVector3Array()

	var pre_v3_l = Vector3.ZERO
	var pre_v3_r = Vector3.ZERO
	var f_is_first = true
	for i in range(m_v3_path_lefts.size()):
		var v3_l = m_v3_path_lefts[i]
		var v3_r = m_v3_path_rights[i]
		if f_is_first :
			f_is_first = false
		else:
			vertices.push_back(v3_l)
			vertices.push_back(v3_r)
			vertices.push_back(pre_v3_r)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)

			vertices.push_back(pre_v3_r)
			vertices.push_back(pre_v3_l)
			vertices.push_back(v3_l)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)

		pre_v3_l = v3_l
		pre_v3_r = v3_r
	
	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)
	node_mesh.mesh= tmpMesh

 実行します。
スクリーンショット (767).png

m_d_path_devide=100.0に変更するともっとなめらかになります。
スクリーンショット (768).png

ソースコード説明

@export var m_d_path_width  : float = 0.5	# 道の幅
@export var m_d_path_devide : float = 10.0	# 道を四角形に分割する数。大きいほど道がなめらかになります。

 道の幅m_d_path_widthとパスの分割数m_d_path_devideを設定します。
 パスの分割数m_d_path_devideを増やすほどカーブがなめらかになります。

var m_v3_path_lefts = []
var m_v3_path_rights = []

 Path3Dの左側の赤い点と右側の青い点の座標をVector3の配列として保持します。
 m_v3_path_lefts配列、m_v3_path_rights配列を設定した後、ArrayMeshを作成します。

func _ready():
	create_path($Path3D/PathFollow3D)
	create_path_mesh($MeshInstance3D)

 SceneTreeに追加されたときにメッシュを作成します。
 引数にノードを渡すことで汎用的に使えるようになります。

func create_path(node_pf : PathFollow3D):
	# 道幅を設定
	var node_left : Marker3D = node_pf.get_node("left")
	var node_right : Marker3D = node_pf.get_node("right")

 $Path3D/PathFollow3D/leftとrightのノードを取得します。

	node_left.transform.origin.x = -m_d_path_width / 2.0
	node_right.transform.origin.x = m_d_path_width / 2.0

 道の幅を設定しています。

	# マーカー追加する
	for i in range(m_d_path_devide+1):
		node_pf.progress_ratio = i /m_d_path_devide 

		var l_pos = node_pf.transform * node_left.position
		m_v3_path_lefts.append(l_pos)
		var r_pos = node_pf.transform * node_right.position
		m_v3_path_rights.append(r_pos)

 PathFollow3Dのprogress_ratioを0~1まで変化させるためのfor分です。
 m_d_path_devideはfloatですが、range(m_d_path_devide)は整数の配列になります。
 range(m_d_path_devide)では分割数が1個足りないのでrange(m_d_path_devide+1)と+1しています。
 (10の場合、0~9までループしますが、0と1で1個目、1と2で2個目...8と9で9個目の四角形を作ります)
 「i/m_d_path_devide」で0~1になります。

 node_left.positionはローカル座標系なので、親のPathFollow3Dの座標系に変換するために、親のPathFollow3Dのtransform(Transform3D)を使用して「node_pf.transform * node_left.position」としています。
 詳細は下記の記事を参考にしてください。

func create_path_mesh(node_mesh : MeshInstance3D):
	var vertices = PackedVector3Array()
	var normals  = PackedVector3Array()

	var pre_v3_l = Vector3.ZERO
	var pre_v3_r = Vector3.ZERO
	var f_is_first = true
	for i in range(m_v3_path_lefts.size()):
		var v3_l = m_v3_path_lefts[i]
		var v3_r = m_v3_path_rights[i]
		if f_is_first :
			f_is_first = false
		else:
			vertices.push_back(v3_l)
			vertices.push_back(v3_r)
			vertices.push_back(pre_v3_r)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)

			vertices.push_back(pre_v3_r)
			vertices.push_back(pre_v3_l)
			vertices.push_back(v3_l)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)
			normals.push_back(Vector3.UP)

		pre_v3_l = v3_l
		pre_v3_r = v3_r
	
	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)
	node_mesh.mesh= tmpMesh

 forループでひとつ前の左右位置を使って初めて四角形を作ることができるので、f_is_firstフラグをつくって0番目はスキップしています。ひとつ前のleft,right位置はpre_v3_l、pre_v3_rに格納されているので、pre_v3_l とpre_v3_rとv3_l とv3_rの4点を使って三角形を2つ作り、四角形にしています。
 vertices.push_backは時計回りになるように追加した面が、見えるようになるため、vertices.push_backする順番は注意する必要があります。
 ArrayMeshの作り方は下記を参考にしていただければと思います。

github

3Dビューで表示確認

 create_path_meshメソッドの最後に下記の1行を追加しtmpMeshを保存して、MeshInstance3Dのmeshにドラッグすると生成したメッシュを3Dビューで確認することができます。
 また上面図を選択するリストで「ワイヤーフレーム」を選択すると三角形のメッシュの形を確認することができます。

	ResourceSaver.save(tmpMesh, "res://debug_mesh_path.tres", ResourceSaver.FLAG_COMPRESS)

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

以上

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
0