HomeArchiveFeedShelf

Godotにおける動的3D物理ロープ - パート1

静的な物理ロープをBlenderを使って作成するのは難しくありません。しかし、いくつかのシナリオでは、実行時に物理ロープを作成する必要があります。この記事では、以下の問題を解決しようとしています。

2つのPhysicsBody3Dがあり、ロープの長さ(長さは常に2つのボディ間の距離よりも長い)を考慮して、実行時に物理システムに反応するロープで接続します。

私は昨年、放棄された3Dスペースの狼人間ゲームプロジェクトでこの問題を解決しました。今、どのようにそれを行ったかを共有したいと思います。

物理ロープの基本

流体物質シミュレーションと同様に、連続した物理ロープはしばしば離散的な部分に分割されます。

各円はJointPin3Dです。セグメント(画像内の長方形)は、実際には抽象的な概念であるため描画されません(描画についてはパート2で説明します)。各ジョイントは隣接するジョイントまたは物理ボディを接続します。ジョイントを使用することで、6自由度の物理ロープを得ることができます。

ヒント:ジョイントを機能させるには、少なくとも1つのRigidBody3Dが必要です。

入力

実行時にロープを作成するために、4つの入力を受け付けます。

  1. resolution: int、セグメント数。この値が高いほど、この離散シミュレーションはより連続的に感じられます。
  2. rope_length: float、ロープの長さ。もちろん、これは2つのアンカー間の距離よりも大きくなければなりません。
  3. start_point: Node3D、ロープの開始アンカーのグローバル位置情報を提供するNode3Dで、その親ノードは物理ボディです。
  4. end_point: Node3D、上記と同様ですが、終了アンカーの情報を提供します。

概要

基本的に、私たちは2つのステップが必要です。

  1. ロープに沿ってセグメントを正しく配置します。rope_lengthresolutionからセグメントの長さを簡単に計算できます。
  2. 各セグメント間のギャップに、隣接する2つのセグメントを接続するジョイントを配置します。

ステップ2は非常に簡単ですが、ステップ1についてはもっと話す必要があります。

セグメントの位置を配置する

セグメントを作成する前に、どのようにしてその位置を知ることができるでしょうか?

まず、2つのアンカーの間に線を描き、その線に沿ってsegment_lengthjoint_lengthを考慮して均等にセグメントを配置します。ここで、segment_lengthは非常に小さく(例えば0.1)することができ、joint_length = (rope_length - resolution * segment_length) / joint_countjoint_count = resolution + 1です。

最初と最後のジョイントを開始点と終了点の位置に配置し、モジュールのユーザーがカスタマイズできるようにします。

resolutionが1または2の場合、問題ははるかに簡単です。

resolutionが1の場合、2つのジョイントを接続するセグメントは1つだけです。

resolutionが2の場合、ジョイント数は3で、そのうち2つはアンカーの位置にあり、セグメント数は2(resolutionと同じ)です。これらの3つのジョイントを均等に配置するために、左側のジョイントを中央に配置します。

resolutionが2より大きい場合、事態は興味深くなります。ロープの長さが距離よりも長いため、曲がり点で線を2つの部分に曲げる必要があります。点Tを考え、ボディAとボディBを接続する必要があると仮定します。線は2つの部分に分かれ、一方はボディAから点Tまで、もう一方は点TからボディBまでです。次に、これら2つの部分に沿ってピンジョイントを均等に配置するだけです。

次に、点Tのグローバル位置を計算する必要があります。

Tの前の部分の長さをr1、Tの後の部分の長さをr2としましょう。Tの軌跡は円であり、半径がr1r2の2つの球体によって交差します。

私たちはすでに知っています:

  1. 球体の2つの中心、これをc1c2と呼びます。
  2. 交差する円の法線、これはc1c2の間のベクトルです。
  3. r1r2、これらはほぼrope_lengthの半分であると考えることができます。もしr1r2と等しくない場合(resolutionの偶奇のため)、それは全く問題ありません。

パラメトリック方程式を使用して、この空間の円を解決できます。

ここで、は円の中心、は円の内部にある直交単位ベクトルであり、互いに直交し、法線に対して直交しています。

ここに円のコードがあります:

class_name SphereSphereCircle
extends Object

var _rng = RandomNumberGenerator.new()

# spheres
var _sphere_a_center: Vector3
var _sphere_a_radius: float

var _sphere_b_center: Vector3
var _sphere_b_radius: float

var _sphere_sphere_distance: float


# triangle
var cosA: float
var a: float
var b: float
var c: float


# circle
var center: Vector3
var radius: float
var vector_a: Vector3
var vector_b: Vector3
var normal: Vector3


func _init(sphere_a_position: Node3D, sphere_a_radius: float, \
    sphere_b_position: Node3D, sphere_b_radius: float):
    _sphere_a_center = sphere_a_position.global_transform.origin
    _sphere_a_radius = sphere_a_radius
    _sphere_b_center = sphere_b_position.global_transform.origin
    _sphere_b_radius = sphere_b_radius
    _sphere_sphere_distance = _sphere_a_center.distance_to(_sphere_b_center)

    _calc_center()
    _calc_radius()
    _calc_vectors()


func _calc_center():
    a = _sphere_b_radius
    b = _sphere_a_radius
    c = _sphere_sphere_distance
    cosA = (pow(b, 2) + pow(c, 2) - pow(a, 2)) / (2 * b * c)

    var k := b * cosA / c
    normal = _sphere_b_center - _sphere_a_center
    center = _sphere_a_center + k * normal


func _calc_radius():
    var sinA = sqrt(1 - pow(cosA, 2))
    radius = b * sinA


func _calc_vectors():
    var unit = Vector3.UP
    if not unit.cross(normal) == Vector3.ZERO:
        vector_a = unit.cross(normal).normalized()
    else:
        unit = Vector3.RIGHT
        vector_a = unit.cross(normal).normalized()

    vector_b = normal.cross(vector_a).normalized()


func get_point(theta: float) -> Vector3:
    var point := center + radius * cos(theta) * vector_a + radius * sin(theta) * vector_b
    return point

円を初期化し、 の間で theta をランダム化し、get_point(theta) を呼び出すと、点 T のグローバル位置が得られます。

点 T が得られたので、残りの作業は曲がった線に沿ってセグメントを均等に配置し、PinJoint3D でセグメントまたは物理ボディを接続するだけです。

PinJointの挿入

現在、RopeSegment で構成された segments があり、それぞれが正しいグローバル位置を持っています。次に行うのは、PinJoint3D でそれらを接続することです。

func _add_joints():
    var first_segment: RopeSegment = segments[0]
    var last_segment: RopeSegment = segments[resolution - 1]
    var first_joint = _generate_joint_between(head_anchor, first_segment)
    var last_joint = _generate_joint_between(last_segment, tail_anchor, 0)

    first_segment.add_child(first_joint)
    first_joint.global_transform.origin = start_point.global_transform.origin
    last_segment.add_child(last_joint)
    last_joint.global_transform.origin = end_point.global_transform.origin

    for i in len(segments):
        if segments.size() >= i + 2:
            var segment: RopeSegment = segments[i]
            var next_segment: RopeSegment = segments[i + 1]
            var joint = _generate_joint_between(segment, next_segment)
            segment.add_child(joint)
            var sphere_middle_point = segment.global_transform.origin + (next_segment.global_transform.origin - segment.global_transform.origin) * 0.5
            joint.global_transform.origin = sphere_middle_point

そして、_generate_joint_between() はゲームエンジンの物理が解決するための制約を設定します。

func _generate_joint_between(body_a: PhysicsBody3D, body_b: PhysicsBody3D, priority: int = 1) -> PinJoint3D:
    var joint := PinJoint3D.new()
    joint.set_node_a(body_a.get_path())
    joint.set_node_b(body_b.get_path())
    joint.set_solver_priority(priority)
    return joint

詳細については、githubリポジトリを確認してください。

次の記事では、レンダリング部分について話します。

参考

https://www.zhihu.com/question/39049685

https://blog.sina.com.cn/s/blog_6496e38e0102vi7e.html

@2023-09-06 23:51