2024/12/18(水) [n年前の日記]
#1 [godot] Godot EngineでRigidBody3Dが何に当たったか調べたい。その2
_昨日
に続き、Windows10 x64 22H2 + Godot Engine 4.3 64bit でゴルフゲームっぽいものが作れないものかなと試してる。
昨日の実験で、StaticBody3D を複数用意して、それぞれにグループを設定してやれば、どのグループと当たっているか判別することは可能と分かった。
ただ、そのためには、blender上でマテリアル別にモデルデータを作って、複数のファイルとしてエクスポートして、その複数のファイルを Godot Engine にインポートして、それぞれにグループを指定する作業が必要になってしまう。
1つのモデルデータの中から、衝突したポリゴンを特定して、そのポリゴンのマテリアル情報を取得することができれば、複数のモデルデータを云々という作業を回避できるのではないか。そんなことができるのかどうか実験してみた。
結論を先に書くと、一応できる。できるのだけど、ちょっと問題がある。それについては後述。
昨日の実験で、StaticBody3D を複数用意して、それぞれにグループを設定してやれば、どのグループと当たっているか判別することは可能と分かった。
ただ、そのためには、blender上でマテリアル別にモデルデータを作って、複数のファイルとしてエクスポートして、その複数のファイルを Godot Engine にインポートして、それぞれにグループを指定する作業が必要になってしまう。
1つのモデルデータの中から、衝突したポリゴンを特定して、そのポリゴンのマテリアル情報を取得することができれば、複数のモデルデータを云々という作業を回避できるのではないか。そんなことができるのかどうか実験してみた。
結論を先に書くと、一応できる。できるのだけど、ちょっと問題がある。それについては後述。
◎ 実験結果 :
以下が結果のスクリーンショット動画。ボールが落ちている地面(ポリゴン)のマテリアル名を取得することができている。
◎ 構成 :
Godotエディタ上の見た目は以下。
ノード構成は以下にした。
ボール担当の RigidBody3D の設定については、以下のような感じ。
RigidBody3D のシグナルについては、body_entered(body:Node) を、RigidBody3D にアタッチしたスクリプトに「接続」(connect)した。
この body_entered というシグナルは、*Body3D と衝突した時に発生する。そして、接続先の関数が ―― 上図で言えば緑色のアイコンがついている _on_body_entered() が呼ばれる。その関数の中で *Body3D と当たった瞬間の処理を書けばいい。
ノード構成は以下にした。
- RigidBody3D がボール担当のノード。今回は見た目を分かりやすくするために、球ではなく箱を表示してる。アタリ範囲は球のまま。
- StaticBody3D が地面を担当するノード。子ノードとして、見た目のモデルデータを担当する MeshInstance3D と、アタリ範囲を担当する CollisionShape3D を持っている。
- RayCast3D は、ボールの中心座標から真下に光線を伸ばして何かと当たっているか調べるためのノード。見た目を分かりやすくするために MeshInstance3D も子ノードとして持っているけれど、本来は無くていい。
- Label3D は、衝突結果をウインドウ上にテキスト表示するために配置した。
ボール担当の RigidBody3D の設定については、以下のような感じ。
- Can Sleep を無効にして、スリープしないようにしている。
- Continuous CD を有効にして、継続的に衝突判定するようにしている。
- Contact Monitor を有効にして、衝突状態を常時調べるようにしている。
- Max Contacts Reported を10にして、10個まで衝突した結果を取れるようにしている。0のままだと衝突結果を1つも得られない。
- 一応、Mass を変更して、重量を指定した。
- ちなみにプロジェクト設定で、1秒あたりの物理ティック数を 60 から 120 に変更してる。そうしないと地面をすり抜けてしまったので…。Continuous CD を有効にしただけではダメだった…。
RigidBody3D のシグナルについては、body_entered(body:Node) を、RigidBody3D にアタッチしたスクリプトに「接続」(connect)した。
この body_entered というシグナルは、*Body3D と衝突した時に発生する。そして、接続先の関数が ―― 上図で言えば緑色のアイコンがついている _on_body_entered() が呼ばれる。その関数の中で *Body3D と当たった瞬間の処理を書けばいい。
◎ スクリプト :
RigidBody3D にアタッチしたスクリプトは以下。
_rigid_body_3d.gd
_rigid_body_3d.gd
extends RigidBody3D @export var raycast3d: RayCast3D @export var camera3d: Camera3D @export var label3d: Label3D @export var camera_offset: Vector3 = Vector3(0, 2, 4) @export var move_speed = 100.0 var hit_face_index = -1 var hit_surface_index = -1 var hit_surface_name = " " var init_pos: Vector3 func _ready(): init_pos = global_position func _process(delta): # Reset position by Home key if Input.is_action_just_pressed("ui_home"): global_position = init_pos linear_velocity *= 0 # Jump by Space key if Input.is_action_just_pressed("ui_accept"): apply_central_impulse(Vector3(0, 0.25, 0)) # Move by Left, Right, Up, Down key var d = Vector3.ZERO d.x = Input.get_axis("ui_left", "ui_right") d.z = Input.get_axis("ui_up", "ui_down") apply_central_force(d.normalized() * move_speed * delta) # Set Camera3D, RayCast3D, Label3D position raycast3d.global_position = global_position camera3d.global_position = global_position + camera_offset label3d.global_position = global_position + Vector3(0, 1.2, 0) label3d.text = "Hit face[%d]\n[%d] %s" % [hit_face_index, hit_surface_index, hit_surface_name] func _on_body_entered(body): var starttime = Time.get_ticks_msec() print("call _on_body_entered()") print(body) if not is_instance_of(body, StaticBody3D): print("Not StaticBody3D") return if not body.is_in_group("ground"): print("Not ground group") return # Check collision position of the RayCast3D and StaticBody3D var col_pos = Vector3.ZERO raycast3d.global_position = global_position raycast3d.force_raycast_update() if raycast3d.is_colliding(): var c = raycast3d.get_collider() if c is StaticBody3D: col_pos = raycast3d.get_collision_point() print("RayCast3D hit StaticBody3D, Pos: ", col_pos) else: print("Not RayCast3D hit StaticBody3D") else: print("Not hit RayCast3D") if col_pos == Vector3.ZERO: return hit_face_index = -1 hit_surface_index = -1 hit_surface_name = " " var staticbody3d: StaticBody3D = body var meshinstance3d = staticbody3d.get_node("MeshInstance3D") print(meshinstance3d) var mesh: Mesh = meshinstance3d.mesh print(mesh) var mesh_face_count = len(mesh.get_faces()) print("Mesh face count : %d" % mesh_face_count) var surface_count = mesh.get_surface_count() # surface is blender material print("Surface count : %d" % surface_count) var fcnt = 0 for surface_index in range(surface_count): var mdt = MeshDataTool.new() mdt.create_from_surface(mesh, surface_index) # get materal name var material = mesh.surface_get_material(surface_index) var matname = material.resource_name var face_count = mdt.get_face_count() print("[%d %s] face count %d" % [surface_index, matname, face_count]) fcnt += face_count for face_index in range(face_count): # get vertext index var i0 = mdt.get_face_vertex(face_index, 0) var i1 = mdt.get_face_vertex(face_index, 1) var i2 = mdt.get_face_vertex(face_index, 2) # get vertex position var p0 = mdt.get_vertex(i0) var p1 = mdt.get_vertex(i1) var p2 = mdt.get_vertex(i2) var cp = Vector2(col_pos.x, col_pos.z) var a = Vector2(p0.x, p0.z) var b = Vector2(p1.x, p1.z) var c = Vector2(p2.x, p2.z) if is_point_in_triangle(cp, a, b, c): hit_face_index = face_index hit_surface_index = surface_index hit_surface_name = matname print("Hit face index : %d" % hit_face_index) if hit_face_index >= 0: print("Hit face index : %d, surface index, name : %d, %s" % [hit_face_index, hit_surface_index, hit_surface_name]) var endtime = Time.get_ticks_msec() print("%d msec, face count : %d" % [(endtime - starttime), fcnt]) func cross2d(a: Vector2, b: Vector2): return a.x * b.y - a.y * b.x func is_point_in_triangle(point: Vector2, a: Vector2, b: Vector2, c: Vector2): var an: Vector2 = a - point var bn: Vector2 = b - point var cn: Vector2 = c - point var orientation: bool = cross2d(an, bn) > 0 if ((cross2d(bn,cn) > 0) != orientation): return false return (cross2d(cn,an) > 0) == orientation
◎ 少し解説 :
_process(delta) の中ではボールの位置を動かすための処理しか書いてないので無視していい。今回の実験の肝は、_on_body_entered(body) 関数の中。
_on_body_entered(body) は、RigidBody3D が *Body3D と衝突した瞬間に呼ばれる。ずっと呼ばれ続けるわけではなくて、衝突した瞬間のみ ―― ボールが落下してきて地面と触れた時だけ呼ばれる。ボールがずっと地面の上に居る時は呼ばれない。前述の動画内で、マテリアルを取得できてない場面が多々あるけど、それは地面と衝突した瞬間しか検出できてないから。
_on_body_entered(body) の引数には、衝突した相手が入ってる。今回は、地面を担当する StaticBody3D が入ってくるはず。
やっていることをざっくり説明すると…。
RayCast3D が、見た目を担当している MeshInstance3D に対しても利用できたら話は早いのだけど…。どうも調べた感じでは、RayCast3D はアタリ範囲を持つ CollisionShape3D の類としか衝突判定してくれないようで…。
しかし、CollisionShape3D が持ってるメッシュデータはアタリ判定処理に特化しちゃっていて、マテリアル等の見た目に関する情報はごっそり削除したデータになっているはずだから、CollisionShape3D が持っているメッシュデータを頼りにして、どのポリゴンがどのマテリアルを使っているか調べることはおそらくできない…。このへん何か誤解してたり、裏技がある可能性もありそうだけど…。
となると、見た目を担当している MeshInstance3D(の Mesh)を対象にして、衝突したポリゴンの特定をしないといけない。ここが面倒臭い。
そんな時に使えるのが MeshDataTool なる機能。
表示用メッシュデータは、表示しやすい状態に並べたデータを持っているようで、そんなデータを相手にして調べていくのはどうも面倒臭いっぽい。これを、各面に対して調べやすいように並べ替えたデータにしてくれるのが MeshDataTool、らしい。変換作業で処理時間がかかりそうではあるけど…。
その MeshDataTool だけど、どうやらサーフェイス(Surface, blender で言うところのマテリアル情報)毎に作って処理してやらないといけないっぽい。だから、サーフェイス数(マテリアル種類数)だけループ処理をすることになる。
MeshDataTool が作れたら、そのサーフェイスを使っている/割り当てられている面の枚数も分かる。その面の枚数で更にループ処理をして…。各面の頂点インデックス番号を取得して、そこから頂点座標を取得して、その三角形の中に衝突座標が入ってるかどうかをチェックしていくことになる。
今回、座標が三角形の中に内包されてるかどうかは外積で調べてる。以下のやり取りで紹介されてたコードを利用させてもらった。元々は Godot Engine のソース内のコードらしい。であれば Godot Engine に、点と三角形の内包判定機能があっても良さそうだけど…。
_Calculate if bool is inside tri with shader : r/godot
_on_body_entered(body) は、RigidBody3D が *Body3D と衝突した瞬間に呼ばれる。ずっと呼ばれ続けるわけではなくて、衝突した瞬間のみ ―― ボールが落下してきて地面と触れた時だけ呼ばれる。ボールがずっと地面の上に居る時は呼ばれない。前述の動画内で、マテリアルを取得できてない場面が多々あるけど、それは地面と衝突した瞬間しか検出できてないから。
_on_body_entered(body) の引数には、衝突した相手が入ってる。今回は、地面を担当する StaticBody3D が入ってくるはず。
やっていることをざっくり説明すると…。
- RayCast3D を使って、ボールの真下にレイ(= 光線)を伸ばして、地面と衝突している座標を取得する。
- 見た目を担当している MeshInstance3D、が持っている Mesh (三角形ポリゴンで構成されてるモデルデータ) に対して、MeshDataToolというものを利用して、各面(フェイス、ポリゴン)を調べやすい状態にする。
- x-z平面上で、衝突座標 (x, z) を内包する三角ポリゴンがどれなのか、総当たりで調べる。該当するポリゴンが見つかったら、サーフェイス情報(blenderで言うところのマテリアル情報)を確定する。
RayCast3D が、見た目を担当している MeshInstance3D に対しても利用できたら話は早いのだけど…。どうも調べた感じでは、RayCast3D はアタリ範囲を持つ CollisionShape3D の類としか衝突判定してくれないようで…。
しかし、CollisionShape3D が持ってるメッシュデータはアタリ判定処理に特化しちゃっていて、マテリアル等の見た目に関する情報はごっそり削除したデータになっているはずだから、CollisionShape3D が持っているメッシュデータを頼りにして、どのポリゴンがどのマテリアルを使っているか調べることはおそらくできない…。このへん何か誤解してたり、裏技がある可能性もありそうだけど…。
となると、見た目を担当している MeshInstance3D(の Mesh)を対象にして、衝突したポリゴンの特定をしないといけない。ここが面倒臭い。
そんな時に使えるのが MeshDataTool なる機能。
表示用メッシュデータは、表示しやすい状態に並べたデータを持っているようで、そんなデータを相手にして調べていくのはどうも面倒臭いっぽい。これを、各面に対して調べやすいように並べ替えたデータにしてくれるのが MeshDataTool、らしい。変換作業で処理時間がかかりそうではあるけど…。
その MeshDataTool だけど、どうやらサーフェイス(Surface, blender で言うところのマテリアル情報)毎に作って処理してやらないといけないっぽい。だから、サーフェイス数(マテリアル種類数)だけループ処理をすることになる。
MeshDataTool が作れたら、そのサーフェイスを使っている/割り当てられている面の枚数も分かる。その面の枚数で更にループ処理をして…。各面の頂点インデックス番号を取得して、そこから頂点座標を取得して、その三角形の中に衝突座標が入ってるかどうかをチェックしていくことになる。
今回、座標が三角形の中に内包されてるかどうかは外積で調べてる。以下のやり取りで紹介されてたコードを利用させてもらった。元々は Godot Engine のソース内のコードらしい。であれば Godot Engine に、点と三角形の内包判定機能があっても良さそうだけど…。
_Calculate if bool is inside tri with shader : r/godot
◎ 問題点 :
一応目的は果たせたけど問題が…。処理が遅い…。予想はしてたけど…。
時間を測ってみたら、今回の判定処理だけで5〜7ミリ秒かかってた。ポリゴン枚数は355枚。ローポリだからこんなもんで済んでるけれど、枚数が2倍3倍10倍になっていったら処理時間も比例して増えて、あっさり1フレーム = 16.7ミリ秒をオーバーしてしまいそう。
動作確認で使ってるPCのCPUは Ryzen 5 5600X (6C 12T、3.7 - 4.6GHz)。もっと非力なCPUで動かしたらどうなることか…。
表示用モデルデータのポリゴンを総当たりで調べてるから、そりゃ遅いはずで…。
ただ、今回は実験だから、全ポリゴン枚数をループでチェックしてるけど。本来、途中で衝突ポリゴンが見つかったらループから抜けてしまうことで多少は改善されるはず。しかしそれでも最悪の場合、全枚数をチェックすることになるはずで…。
時間を測ってみたら、今回の判定処理だけで5〜7ミリ秒かかってた。ポリゴン枚数は355枚。ローポリだからこんなもんで済んでるけれど、枚数が2倍3倍10倍になっていったら処理時間も比例して増えて、あっさり1フレーム = 16.7ミリ秒をオーバーしてしまいそう。
動作確認で使ってるPCのCPUは Ryzen 5 5600X (6C 12T、3.7 - 4.6GHz)。もっと非力なCPUで動かしたらどうなることか…。
表示用モデルデータのポリゴンを総当たりで調べてるから、そりゃ遅いはずで…。
ただ、今回は実験だから、全ポリゴン枚数をループでチェックしてるけど。本来、途中で衝突ポリゴンが見つかったらループから抜けてしまうことで多少は改善されるはず。しかしそれでも最悪の場合、全枚数をチェックすることになるはずで…。
◎ 課題 :
マテリアル種類別(サーフェイス種類別)でループ処理をして、全ポリゴンの頂点座標を取得することができてきるのだから…。似たような処理をして、1つの表示用モデルデータから、マテリアル別の StaticBody3D + CollisionShape3D をゲームプログラムの初期化処理内で生成してしまえば、昨日行ったような形で衝突判定ができそうな気もする。そうすれば、判定処理が少しは速くなるのではないか…。
問題は、どうやったらその CollisionShape3D 用の Mesh を作れるのか、そこが分からないという…。
今回の CollisionShape3D には、ConcavePolygonShape3D が設定されていた。つまり、Mesh から ConcavePolygonShape3D を作成する方法が分かれば…。
ConvexPolygonShape3D というものもあるらしいけれど、それは凸多面体、中身があることになっている形状を扱う時に使うらしい。
ConcavePolygonShape3D は中身が無い形状とのことで…。そこにある面をすり抜けたら、もう戻ってくることができない形状、ということかな…。
問題は、どうやったらその CollisionShape3D 用の Mesh を作れるのか、そこが分からないという…。
今回の CollisionShape3D には、ConcavePolygonShape3D が設定されていた。つまり、Mesh から ConcavePolygonShape3D を作成する方法が分かれば…。
ConvexPolygonShape3D というものもあるらしいけれど、それは凸多面体、中身があることになっている形状を扱う時に使うらしい。
ConcavePolygonShape3D は中身が無い形状とのことで…。そこにある面をすり抜けたら、もう戻ってくることができない形状、ということかな…。
[ ツッコむ ]
以上、1 日分です。