はじめに
GODOT4.0でGDScriptから自由な形の三角形を作りたくなったのでArrayMeshについて調査しました。
Materialと衝突領域も設定したいと思います。
やりたいこと
・GDScriptで3D空間の3点を指定して三角形を作る。
・Materialを設定する
・衝突領域を設定する
GITHUB
本記事を最後まで実行したプロジェクトです。
事前準備:クリックしたところの3次元座標をprintする
ArrayMeshを確認する事前準備として、地面を作成して。地面にタッチ(クリック)したところの3D空間座標をprintするまで実装します。
新規シーンを作成して必要なノードを追加
Godotで新規プロジェクトを作成して、下記のようにノードを構成します。
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)
「マウスでタッチ操作をエミュレート」を有効する
プロジェクトメニューのプロジェクト設定の「入力デバイス/マウスでタッチ操作をエミュレート」をオンにします。
タッチしたところの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座標に値が設定されています。
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
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.
法線を追加する
面が暗いのは法線を指定していないためです。
法線とは面に対して垂直なベクトル(下図の緑の矢印)で、ライティング処理に使用されます。
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番目)に法線配列を設定します。
(試しに)頂点ごとに法線の向きを変える
法線はライティング処理に使用されるのを確認します。
先ほど追加した「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点の中間は自動で計算しているのが、頂点があるように見えますね。
本来は球を疑似した三角形メッシュでもライティングで球に見えるようにするためのものだと思います。
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に設定します。
Materialファイルを作成してArrayMeshに適用する
Materialをコードからすべて設定するのは大変なので、Materialリソースファイルを作成します。
ファイルシステムの「res」を右クリックして、新規からリソースを実行します。
StandardMaterial3Dを選択します。
ファイル名を「triangle_material.tres」に変更して保存します。
作成されました。
作成した「triangle_material.tres」をダブルクリックして、インスペクタのBaseMaterial3Dのalbedoを開いて、Colorを赤に変更します。
スクリプトから「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)
Collisionを設定する
Meshから衝突形状を生成して設定することができます。
シーンのルートノードのWorldを右クリックして子ノードを追加からArea3Dを選択します。
Area3Dを右クリックして子ノードを追加からCollisionShape3Dを追加します。
衝突形状を見やすいようにCollisionShape3DのインスペクタのNode3D/Transform/Positionのyを1.0mにしておきます。
create_triangle_array_mesh関数の最後に以下を追加します。
create_trimesh_shapeメソッドはMesh形状からConcavePolygonShape3Dを生成します。
# MeshからCollision形状(ConcavePolygonShape3D)生成
$Area3D/CollisionShape3D.shape = tmpMesh.create_trimesh_shape()
目視で確認できるように、デバッグメニューから「コリジョン形状を表示」をチェックします。
実行します。
3点をクリックして赤い三角形を表示すると、その1m上に衝突形状が表示されているのがわかります。
以上。