概要
本記事はCluster,Inc. Advent Calendar 2017の6日目の記事です。
Unityちゃん付属のSpringBoneが微妙に使いにくかったため、使いやすくなるようにガッツリと作り直してみました。
左がUnityちゃん付属の物理エンジンで、右が今回作ったものです。
従来のものとの違い
改善された点
- 重力の影響を受けるようになった
- 1クラスで完結するようになり、セットアップが簡単になった
- ボーンの軸方向を意識せずに済むようになった
劣化した点
- 少し重くなった
- 細かい調整ができなくなった
使い方
物理演算したいGameObjectに Pbd Spring Bone
をアタッチし、揺れモノの起点となるボーンを指定するだけで動きます。
仕組み
基本的な考え方は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