Help us understand the problem. What is going on with this article?

PBDベースで揺れモノ用の物理エンジンを実装した話

More than 1 year has passed since last update.

概要

本記事はCluster,Inc. Advent Calendar 2017の6日目の記事です。

Unityちゃん付属のSpringBoneが微妙に使いにくかったため、使いやすくなるようにガッツリと作り直してみました。
左がUnityちゃん付属の物理エンジンで、右が今回作ったものです。
capture.gif

従来のものとの違い

改善された点

  • 重力の影響を受けるようになった
  • 1クラスで完結するようになり、セットアップが簡単になった
  • ボーンの軸方向を意識せずに済むようになった

劣化した点

  • 少し重くなった
  • 細かい調整ができなくなった

使い方

物理演算したいGameObjectに Pbd Spring Boneをアタッチし、揺れモノの起点となるボーンを指定するだけで動きます。
SnapCrab_NoName_2017-12-6_19-38-40_No-00.png

仕組み

基本的な考え方はPosition Based Dynamicsと呼ばれるものです。

GameObjectそれぞれの各点を質点と見なし、現在の位置/1フレーム前の位置を保持しています。

readonly Dictionary<GameObject, Vector3> defaultLocalPositions = new Dictionary<GameObject, Vector3>();
readonly Dictionary<GameObject, Quaternion> defaultLocalRotations = new Dictionary<GameObject, Quaternion>();

前回の位置と現在の位置から速度を求め、重力と減衰、形を維持しようとする力を加えます。

var velocity = (positions[current] - prevPositions[current]) / prevDeltaTime;
var positionForce =
    (parent.transform.TransformPoint(defaultLocalPositions[current]) - positions[current]) * spring;
velocity += positionForce * Time.deltaTime;
velocity += Vector3.down * gravity * Time.deltaTime;
velocity *= damp;

求めた速度を元に位置を更新します。

prevPositions[current] = positions[current];
positions[current] = positions[current] + velocity * Time.deltaTime;

質点がコライダーにめり込んでいた場合、めり込まない位置まで移動させます。

foreach (var sphereCollider in colliders)
{
    var colliderScale = Mathf.Max(
        Mathf.Abs(sphereCollider.transform.lossyScale.x),
        Mathf.Abs(sphereCollider.transform.lossyScale.y),
        Mathf.Abs(sphereCollider.transform.lossyScale.z));
    var colliderPos = sphereCollider.transform.TransformPoint(sphereCollider.center);

    if ((positions[current] - colliderPos).magnitude > pointSize + colliderScale * sphereCollider.radius)
    {
        continue;
    }

    positions[current] = colliderPos +
                            (positions[current] - colliderPos).normalized *
                            (pointSize + colliderScale * sphereCollider.radius);
}

ボーンの長さが一定になるよう、位置を補正してやります。

positions[current] = positions[parent] + (positions[current] - positions[parent]).normalized *
                        defaultLocalPositions[current].magnitude;

求めた位置を元に、親ボーンの角度を計算します。

parent.transform.rotation =
    Quaternion.FromToRotation(parent.transform.TransformDirection(defaultLocalPositions[current]),
        positions[current] - positions[parent]) * parent.transform.rotation;

自分自身のメソッドを再帰呼び出しして、子要素についても同様のことを繰り返します。

foreach (var child in current.Children())
{
    UpdateRecursive(child, current);
}

コード

/**
MIT Lisence

Copyright (c) 2017 Cluster,Inc.

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

using System.Collections.Generic;
using Unity.Linq;
using UnityEngine;

public class PbdSpringBone : MonoBehaviour
{
    readonly Dictionary<GameObject, Vector3> defaultLocalPositions = new Dictionary<GameObject, Vector3>();
    readonly Dictionary<GameObject, Vector3> prevPositions = new Dictionary<GameObject, Vector3>();
    readonly Dictionary<GameObject, Vector3> positions = new Dictionary<GameObject, Vector3>();

    float prevDeltaTime;

    [SerializeField] float spring = 100;
    [SerializeField] float damp = 0.85f;
    [SerializeField] float gravity = 9.8f;
    [SerializeField] float pointSize = 0.02f;
    [SerializeField] SphereCollider[] colliders;
    [SerializeField] GameObject[] rootBones;

    public float Spring
    {
        get { return spring; }
        set { spring = value; }
    }

    public float Damp
    {
        get { return damp; }
        set { damp = value; }
    }

    public float Gravity
    {
        get { return gravity; }
        set { gravity = value; }
    }

    public SphereCollider[] Colliders
    {
        get { return colliders; }
        set { colliders = value; }
    }

    public GameObject[] RootBones
    {
        get { return rootBones; }
        set { rootBones = value; }
    }

    void Start()
    {
        foreach (var child in rootBones)
        {
            InitializeRecursive(child);
        }
        prevDeltaTime = Time.deltaTime;
    }

    void LateUpdate()
    {
        foreach (var child in rootBones)
        {
            UpdateRecursive(child);
        }
        prevDeltaTime = Time.deltaTime;
    }

    void OnDrawGizmos()
    {
        if (!Application.isPlaying)
        {
            return;
        }
        foreach (var child in rootBones)
        {
            if (child == null)
            {
                continue;
            }
            DrawGizmosRecursive(child);
        }
        foreach (var sphereCollider in colliders)
        {
            if (sphereCollider == null)
            {
                continue;
            }
            Gizmos.color = Color.blue;
            var scale = Mathf.Max(
                Mathf.Abs(sphereCollider.transform.lossyScale.x),
                Mathf.Abs(sphereCollider.transform.lossyScale.y),
                Mathf.Abs(sphereCollider.transform.lossyScale.z));
            var pos = sphereCollider.transform.TransformPoint(sphereCollider.center);
            Gizmos.DrawWireSphere(pos, scale * sphereCollider.radius);
        }
    }

    void InitializeRecursive(GameObject current)
    {
        positions[current] = current.transform.position;
        defaultLocalPositions[current] = current.transform.localPosition;
        prevPositions[current] = current.transform.position;

        foreach (var child in current.Children())
        {
            InitializeRecursive(child);
        }
    }

    void UpdateRecursive(GameObject current, GameObject parent = null)
    {
        if (parent != null)
        {
            var velocity = (positions[current] - prevPositions[current]) / prevDeltaTime;
            var positionForce =
                (parent.transform.TransformPoint(defaultLocalPositions[current]) - positions[current]) * spring;
            velocity += positionForce * Time.deltaTime;
            velocity += Vector3.down * gravity * Time.deltaTime;
            velocity *= damp;

            prevPositions[current] = positions[current];
            positions[current] = positions[current] + velocity * Time.deltaTime;

            foreach (var sphereCollider in colliders)
            {
                var colliderScale = Mathf.Max(
                    Mathf.Abs(sphereCollider.transform.lossyScale.x),
                    Mathf.Abs(sphereCollider.transform.lossyScale.y),
                    Mathf.Abs(sphereCollider.transform.lossyScale.z));
                var colliderPos = sphereCollider.transform.TransformPoint(sphereCollider.center);

                if ((positions[current] - colliderPos).magnitude > pointSize + colliderScale * sphereCollider.radius)
                {
                    continue;
                }

                positions[current] = colliderPos +
                                        (positions[current] - colliderPos).normalized *
                                        (pointSize + colliderScale * sphereCollider.radius);
            }

            positions[current] = positions[parent] + (positions[current] - positions[parent]).normalized *
                                    defaultLocalPositions[current].magnitude;
        }
        else
        {
            prevPositions[current] = positions[current];
            positions[current] = current.transform.position;
        }

        if (parent != null)
        {
            parent.transform.rotation =
                Quaternion.FromToRotation(parent.transform.TransformDirection(defaultLocalPositions[current]),
                    positions[current] - positions[parent]) * parent.transform.rotation;

        }

        foreach (var child in current.Children())
        {
            UpdateRecursive(child, current);
        }
    }

    void DrawGizmosRecursive(GameObject current, GameObject parent = null)
    {
        Gizmos.color = parent != null ? Color.yellow : Color.red;
        Gizmos.DrawWireSphere(positions[current], pointSize);

        if (parent != null)
        {
            Gizmos.DrawLine(positions[current], positions[parent]);
        }

        foreach (var child in current.Children())
        {
            DrawGizmosRecursive(child, current);
        }
    }
}

ライセンス

© Unity Technologies Japan/UCL

virtualcast
VRシステム(バーチャルキャスト)の開発、運営、企画
https://virtualcast.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした