MENU
Главная » Файлы » Скрипты

Scripts "Плавающие бревна".
08.05.2012, 05:43
Дано: вода, бревно
Задача: заставить бревно реалистично плавать
----------------------------------
На бревно в воде действуют две силы: сила тяготения и сила Архимеда, выталкавающая бревно наверх. Тяготение и так уже есть силами Unity, так что остаётся только смоделировать выталкивание.
Известно, что сила Архимеда действует только на погруженную в воду часть тела и численно она равна:
Сила Архимеда = (плотность воды) * (ускорение свободного падения) * (объем тела, погруженный в воду)
Плотность воды ― константа, 1000 кг/(м^3).
Ускорение свободного падения ― тоже константа. Для универсальности её можно прочитать из Physics.gravity.
Объем тела, погруженный в воду ― с этим сложнее. При большом желании его можно было бы расчитать, но во-первых, для произвольного тела это сложно, а во-вторых, заниматься этим в рилтайме в функции обработки физики нехорошо, т.к. долго. Будем прикидывать приблизительно:
Путь у нас будет не бревно, а параллепипед. Разобьём его по всём трём осям на сектора, из каждого сектора будем брать центральную точку и смотреть, находится она над водой или под водой:

Code
const int SECTORS = 3;
List<Vector3> points;

var collider = GetComponent<Collider>();
var bounds = collider.bounds;

for (int ix = 0; ix < SECTORS; ix++)
{
  for (int iy = 0; iy < SECTORS; iy++)
  {
  for (int iz = 0; iz < SECTORS; iz++)
  {
  float x = bounds.min.x + bounds.size.x / SECTORS * (0.5f+ ix);
  float y = bounds.min.y + bounds.size.y / SECTORS * (0.5f+ iy);
  float z = bounds.min.z + bounds.size.z / SECTORS * (0.5f+ iz);

  // Переводим точку из мировой в локальную систему координат
  var p = transform.InverseTransformPoint(new Vector3(x, y, z));

  points.Add(p);
  }
  }

Сила Архимеда для каждого сектора = 1000 кг/(м^3) * 9,81 м/(c^2) * (объем сектора)
Суммарная сила Архимеда = 1000 * 9,81 * (объем сектора) * (количество секторов под водой)
Если всё тело целиком находится под водой, то сила Архимеда = 1000 * 9,81 * (объем тела)
Где взять объём тела? Высчитывать по сетке объекта не будем (всё равно не каждая сетка это позволит), возьмём грубо половину объёма от bounding box:
Code
var collider = GetComponent<Collider>();
var bounds = collider.bounds;

float volume = bounds.size.x * bounds.size.y * bounds.size.z / 2;

Все сектора у нас одинаковые и нет смысла считать силу Архимеда для каждого из них, потому что она будет равна (силе Архимеда для всего тела) / (количество секторов). Поэтому расчитаем силу только один раз и заранее:
Code
const float WATER_DENSITY = 1000;

float archimedesForceMagnitude = WATER_DENSITY * Mathf.Abs(Physics.gravity.y) *volume;
Vector3 archimedesForce = new Vector3(0, archimedesForceMagnitude, 0);

Теперь все исходные данные у нас есть, можно приступать к прикладыванию силы Архимеда к объекту:
Code
void FixedUpdate()
{
  foreach (var point in points)
  {
  // Переводим точку из локальной в мировую систему координат
  var wp = transform.TransformPoint(point);

  // Функция GetWaterLevel(x,z) возвращает или просто ноль, или высоту воды в заданной позиции, если на воде есть волны.
  var waterLevel = GetWaterLevel(wp.x, wp.z);

  if (wp.y < waterLevel)
  {
  var force = archimedesForce / points.Count;
  rigidbody.AddForceAtPosition(force, wp);
  }
  }
}

Практика показала, что оно работает, но есть две проблемки:
1) Если тело падает в воду, то оно из неё выпрыгивает обратно, потом опять падает и так до бесконечности.
2) Если тело дрейфует по воде, оно заметно дёргается вверх-вниз, потому что сила Архимеда, приложенная к объекту, никогда не совпадает точно с силой тяжести, потому что изменяется дискретно. Один сектор ушёл под воду ― сила резко увеличилась, поднялся над водой ― резко уменьшилась.
Первую проблему можно решить демпфером ― будем гасить скорость тем секторам, которые оказались под водой. Будет даже реалистично. Приложим силу, пропорциональную скорости движения, но обратную по направлению:
Code
const float DAMPFER = 100;
var velocity = rigidbody.GetPointVelocity(wp);
var localDampingForce = -velocity * DAMPFER;

А чтобы убрать дискретность у силы выталкивания, добавим к ней коэффициент, который будет плавно меняться, когда центр сектора будет около поверхности воды:delta ― это половина высоты сектора.
Code
float delta;
var collider = GetComponent<Collider>();
var bounds = collider.bounds;

if (bounds.size.x < bounds.size.y)
{
  delta = bounds.size.x;
}
else
{
  delta = bounds.size.y;
}

if (bounds.size.z < delta)
{
  delta = bounds.size.z;
}
delta /= 2 * SECTORS;

Когда весь сектор под водой, коэффициент = 1,0 и на сектор будет действовать полная сила Архимеда. Когда только полсектора под водой, то коэффициент = 0,5. Если весь сектор над водой, коэффициент = 0 и сила выталкивания будет равна нулю:
Code
void FixedUpdate()
{
  foreach (var point in points)
  {
  var wp = transform.TransformPoint(point);
  var waterLevel = GetWaterLevel(wp.x, wp.z);
  if (wp.y - delta < waterLevel)
  {
  var velocity = rigidbody.GetPointVelocity(wp);
  float k = (waterLevel - wp.y) / (2 * delta) + 0.5f;
  if (k > 1)
  {
  k = 1f;
  }
  else if (k < 0)
  {
  k = 0f;
  }
  var localDampingForce = -velocity * DAMPFER;
  var localArchimedesForce = Mathf.Sqrt(k) *archimedesForce / points.Count;
   
  var force = localDampingForce + localArchimedesForce;
  rigidbody.AddForceAtPosition(force, wp);
  }
  }
}

Квадратный корень из коэффициента Mathf.Sqrt(k) можно было и не брать, но с ним показалось естественнее.

Версия 2.1

― Для удобства плотность объекта задаётся вручную; измеряется в кг/(м^3). У воды 1000, у древесины 500―700.
― Умеет работать с вогнутыми объектами.
― В целях отладки умеет рисовать gizmos с точками приложения силы выталкивания.

Code
// Buoyancy.cs
// Version 2.1
//
// http://forum.unity3d.com/threads/72974-Buoyancy-script
//
// Terms of use: do whatever you like

using System.Collections.Generic;
using UnityEngine;

public class Buoyancy : MonoBehaviour
{
// public Ocean ocean;

  public float density = 500;
  public int slicesPerAxis = 2;
  public bool isConcave = false;
  public int voxelsLimit = 16;

  private const float DAMPFER = 0.1f;
  private const float WATER_DENSITY = 1000;

  private float voxelHalfHeight;
  private Vector3 localArchimedesForce;
  private List<Vector3> voxels;
  private bool isMeshCollider;
  private List<Vector3[]> forces; // For drawing force gizmos

  /// <summary>
  /// Provides initialization.
  /// </summary>
  private void Start()
  {
  forces = new List<Vector3[]>(); // For drawing force gizmos

  // Store original rotation and position
  var originalRotation = transform.rotation;
  var originalPosition = transform.position;
  transform.rotation = Quaternion.identity;
  transform.position = Vector3.zero;

  // The object must have a collider
  if (collider == null)
  {
  gameObject.AddComponent<MeshCollider>();
  Debug.LogWarning(string.Format("[Buoyancy.cs] Object\"{0}\" had no collider. MeshCollider has been added.", name));
  }
  isMeshCollider = GetComponent<MeshCollider>() != null;

  var bounds = collider.bounds;
  if (bounds.size.x < bounds.size.y)
  {
  voxelHalfHeight = bounds.size.x;
  }
  else
  {
  voxelHalfHeight = bounds.size.y;
  }
  if (bounds.size.z < voxelHalfHeight)
  {
  voxelHalfHeight = bounds.size.z;
  }
  voxelHalfHeight /= 2 * slicesPerAxis;

  // The object must have a RidigBody
  if (rigidbody == null)
  {
  gameObject.AddComponent<Rigidbody>();
  Debug.LogWarning(string.Format("[Buoyancy.cs] Object\"{0}\" had no Rigidbody. Rigidbody has been added.", name));
  }
  rigidbody.centerOfMass = new Vector3(0, -bounds.extents.y * 0f, 0) + transform.InverseTransformPoint(bounds.center);

  voxels = SliceIntoVoxels(isMeshCollider && isConcave);

  // Restore original rotation and position
  transform.rotation = originalRotation;
  transform.position = originalPosition;

  float volume = rigidbody.mass / density;

  WeldPoints(voxels, voxelsLimit);

  float archimedesForceMagnitude = WATER_DENSITY *Mathf.Abs(Physics.gravity.y) * volume;
  localArchimedesForce = new Vector3(0, archimedesForceMagnitude, 0) / voxels.Count;

  Debug.Log(string.Format("[Buoyancy.cs] Name=\"{0}\" volume={1:0.0}, mass={2:0.0}, density={3:0.0}", name, volume, rigidbody.mass, density));
  }

  /// <summary>
  /// Slices the object into number of voxels represented by their center points.
  /// <param name="concave">Whether the object have a concave shape.</param>
  /// <returns>List of voxels represented by their center points.</returns>
  /// </summary>
  private List<Vector3> SliceIntoVoxels(bool concave)
  {
  var points = new List<Vector3>(slicesPerAxis * slicesPerAxis *slicesPerAxis);

  if (concave)
  {
  var meshCol = GetComponent<MeshCollider>();

  var convexValue = meshCol.convex;
  meshCol.convex = false;

  // Concave slicing
  var bounds = collider.bounds;
  for (int ix = 0; ix < slicesPerAxis; ix++)
  {
  for (int iy = 0; iy < slicesPerAxis; iy++)
  {
  for (int iz = 0; iz < slicesPerAxis;iz++)
  {
  float x = bounds.min.x +bounds.size.x / slicesPerAxis * (0.5f + ix);
  float y = bounds.min.y +bounds.size.y / slicesPerAxis * (0.5f + iy);
  float z = bounds.min.z +bounds.size.z / slicesPerAxis * (0.5f + iz);

  var p =transform.InverseTransformPoint(new Vector3(x, y, z));

  if(PointIsInsideMeshCollider(meshCol, p))
  {
  points.Add(p);
  }
  }
  }
  }
  if (points.Count == 0)
  {
  points.Add(bounds.center);
  }

  meshCol.convex = convexValue;
  }
  else
  {
  // Convex slicing
  var bounds = GetComponent<Collider>().bounds;
  for (int ix = 0; ix < slicesPerAxis; ix++)
  {
  for (int iy = 0; iy < slicesPerAxis; iy++)
  {
  for (int iz = 0; iz < slicesPerAxis;iz++)
  {
  float x = bounds.min.x +bounds.size.x / slicesPerAxis * (0.5f + ix);
  float y = bounds.min.y +bounds.size.y / slicesPerAxis * (0.5f + iy);
  float z = bounds.min.z +bounds.size.z / slicesPerAxis * (0.5f + iz);

  var p =transform.InverseTransformPoint(new Vector3(x, y, z));

  points.Add(p);
  }
  }
  }
  }

  return points;
  }

  /// <summary>
  /// Returns whether the point is inside the mesh collider.
  /// </summary>
  /// <param name="c">Mesh collider.</param>
  /// <param name="p">Point.</param>
  /// <returns>True - the point is inside the mesh collider. False - the point is outside of the mesh collider. </returns>
  private static bool PointIsInsideMeshCollider(Collider c, Vector3 p)
  {
  Vector3[] directions = { Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.forward, Vector3.back };

  foreach (var ray in directions)
  {
  RaycastHit hit;
  if (c.Raycast(new Ray(p - ray * 1000, ray), out hit, 1000f) == false)
  {
  return false;
  }
  }

  return true;
  }

  /// <summary>
  /// Returns two closest points in the list.
  /// </summary>
  /// <param name="list">List of points.</param>
  /// <param name="firstIndex">Index of the first point in the list. It's always less than the second index.</param>
  /// <param name="secondIndex">Index of the second point in the list. It's always greater than the first index.</param>
  private static void FindClosestPoints(IList<Vector3> list, out intfirstIndex, out int secondIndex)
  {
  float minDistance = float.MaxValue, maxDistance = float.MinValue;
  firstIndex = 0;
  secondIndex = 1;

  for (int i = 0; i < list.Count - 1; i++)
  {
  for (int j = i + 1; j < list.Count; j++)
  {
  float distance = Vector3.Distance(list[i], list[j]);
  if (distance < minDistance)
  {
  minDistance = distance;
  firstIndex = i;
  secondIndex = j;
  }
  if (distance > maxDistance)
  {
  maxDistance = distance;
  }
  }
  }
  }

  /// <summary>
  /// Welds closest points.
  /// </summary>
  /// <param name="list">List of points.</param>
  /// <param name="targetCount">Target number of points in the list.</param>
  private static void WeldPoints(IList<Vector3> list, int targetCount)
  {
  if (list.Count <= 2 || targetCount < 2)
  {
  return;
  }

  while (list.Count > targetCount)
  {
  int first, second;
  FindClosestPoints(list, out first, out second);

  var mixed = (list[first] + list[second]) * 0.5f;
  list.RemoveAt(second); // the second index is always greater that the first => removing the second item first
  list.RemoveAt(first);
  list.Add(mixed);
  }
  }

  /// <summary>
  /// Returns the water level at given location.
  /// </summary>
  /// <param name="x">x-coordinate</param>
  /// <param name="z">z-coordinate</param>
  /// <returns>Water level</returns>
  private float GetWaterLevel(float x, float z)
  {
// return ocean == null ? 0.0f : ocean.GetWaterHeightAtLocation(x, z);
  return 0.0f;
  }

  /// <summary>
  /// Calculates physics.
  /// </summary>
  private void FixedUpdate()
  {
  forces.Clear(); // For drawing force gizmos

  foreach (var point in voxels)
  {
  var wp = transform.TransformPoint(point);
  float waterLevel = GetWaterLevel(wp.x, wp.z);

  if (wp.y - voxelHalfHeight < waterLevel)
  {
  float k = (waterLevel - wp.y) / (2 *voxelHalfHeight) + 0.5f;
  if (k > 1)
  {
  k = 1f;
  }
  else if (k < 0)
  {
  k = 0f;
  }

  var velocity = rigidbody.GetPointVelocity(wp);
  var localDampingForce = -velocity * DAMPFER *rigidbody.mass;
  var force = localDampingForce + Mathf.Sqrt(k) *localArchimedesForce;
  rigidbody.AddForceAtPosition(force, wp);

  forces.Add(new[] { wp, force }); // For drawing force gizmos
  }
  }
  }

  /// <summary>
  /// Draws gizmos.
  /// </summary>
  private void OnDrawGizmos()
  {
  if (voxels == null || forces == null)
  {
  return;
  }

  const float gizmoSize = 0.05f;
  Gizmos.color = Color.yellow;

  foreach (var p in voxels)
  {
  Gizmos.DrawCube(transform.TransformPoint(p), newVector3(gizmoSize, gizmoSize, gizmoSize));
  }

  Gizmos.color = Color.cyan;

  foreach (var force in forces)
  {
  Gizmos.DrawCube(force[0], new Vector3(gizmoSize, gizmoSize, gizmoSize));
  Gizmos.DrawLine(force[0], force[0] + force[1] /rigidbody.mass);
  }
  }
}
Категория: Скрипты | Добавил: DeMaN_74
Просмотров: 5904 | Загрузок: 0 | Комментарии: 1 | Рейтинг: 5.0/2
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Хостинг от uCoz