Страница 1 из 4

Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 04:20
alexz
Изображение

Дано: вода, бревно

Задача: заставить бревно реалистично плавать

---------------------------------------------------------------------------------------------------------------------------------------
На бревно в воде действуют две силы: сила тяготения и сила Архимеда, выталкавающая бревно наверх. Тяготение и так уже есть силами Unity, так что остаётся только смоделировать выталкивание.

Известно, что сила Архимеда действует только на погруженную в воду часть тела и численно она равна:

Сила Архимеда = (плотность воды) * (ускорение свободного падения) * (объем тела, погруженный в воду)

Плотность воды ― константа, 1000 кг/(м^3).
Ускорение свободного падения ― тоже константа. Для универсальности её можно прочитать из Physics.gravity.
Объем тела, погруженный в воду ― с этим сложнее. При большом желании его можно было бы расчитать, но во-первых, для произвольного тела это сложно, а во-вторых, заниматься этим в рилтайме в функции обработки физики нехорошо, т.к. долго. Будем прикидывать приблизительно:

Путь у нас будет не бревно, а параллепипед. Разобьём его по всём трём осям на сектора, из каждого сектора будем брать центральную точку и смотреть, находится она над водой или под водой:

Изображение

Синтаксис:
Используется csharp
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:

Синтаксис:
Используется csharp
var collider = GetComponent<Collider>();
var bounds = collider.bounds;

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


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

Синтаксис:
Используется csharp
const float WATER_DENSITY = 1000;

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


Теперь все исходные данные у нас есть, можно приступать к прикладыванию силы Архимеда к объекту:
Синтаксис:
Используется csharp
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) Если тело дрейфует по воде, оно заметно дёргается вверх-вниз, потому что сила Архимеда, приложенная к объекту, никогда не совпадает точно с силой тяжести, потому что изменяется дискретно. Один сектор ушёл под воду ― сила резко увеличилась, поднялся над водой ― резко уменьшилась.

Первую проблему можно решить демпфером ― будем гасить скорость тем секторам, которые оказались под водой. Будет даже реалистично. Приложим силу, пропорциональную скорости движения, но обратную по направлению:

Синтаксис:
Используется csharp
const float DAMPFER = 100;

var velocity = rigidbody.GetPointVelocity(wp);
var localDampingForce = -velocity * DAMPFER;
 


А чтобы убрать дискретность у силы выталкивания, добавим к ней коэффициент, который будет плавно меняться, когда центр сектора будет около поверхности воды:
Изображение

delta ― это половина высоты сектора.

Синтаксис:
Используется csharp
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 и сила выталкивания будет равна нулю:

Синтаксис:
Используется csharp
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) можно было и не брать, но с ним показалось естественнее.

---------------------------------------------------------------------------------------------------------------------------------------
F ― полный экран
левая кнопка мыши ― бросить бревно
R ― вкл/выкл отражения
С ― «срубить» дерево
левый Shift ― ускорение времени

HTML код для вашего блога :
Код: Выделить всё
<script language='javascript' type="text/javascript"> document.write("<iframe marginheight='0' src='http://unity3d.ru/distribution/player.php?url=http://dl.dropbox.com/u/8203557/island4.unity3d&w=600&h=400&t=false&preview=1' height='"+(400+30)+"' width='600' frameborder='0' scrolling='no'></iframe>"); </script>


---------------------------------------------------------------------------------------------------------------------------------------
Версия 2.1

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

Изображение Изображение Изображение

Синтаксис:
Используется csharp
// Buoyancy.cs
// by Alex Zhdankin
// 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 int firstIndex, 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), new Vector3(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);
                }
        }
}

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 10:30
DbIMok
молодец, что не поленился все расписать, оформить и т.д.
(3A4OT)
чисто по скриншоту ориентируясь, я бы сказал, что бревна слишком сильно притоплены

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 12:26
Neodrop
(3A4OT)

Отличная работа. Спасибо за открытость \m/

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 12:59
sp00n
(3A4OT) Спасибо огромное!

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 18:02
alexz
DbIMok писал(а):чисто по скриншоту ориентируясь, я бы сказал, что бревна слишком сильно притоплены

Да, у них большая плотность: 800...1000 кг/(м^3). Если ходить по таким брёвнам, они под тяжестью игрока уходят под воду.

Вот брёвна с плотностью 600..700, примерно такая она у древесины и есть.
Изображение

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 18:53
depsemt
(3A4OT)
Автор, ты очень молодец!!!!))

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 21:08
ScorpionMax
Здравствуйте. Я Скопировал скрипт, но на версии 3.1.0.4F не работает, скрипт не виден. Что делать, возможно это из-за того что я создал не в шарпе а в обычном текстовике и вставил скопированный скрипт?

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 21:20
alexz
ScorpionMax писал(а):Здравствуйте. Я Скопировал скрипт, но на версии 3.1.0.4F не работает, скрипт не виден. Что делать, возможно это из-за того что я создал не в шарпе а в обычном текстовике и вставил скопированный скрипт?

Получившийся файл должен называться Buoyancy.cs
Не Buoyancy.txt и не Buoyancy.cs.txt

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 21:53
WebWolf
alexz писал(а):
ScorpionMax писал(а):Здравствуйте. Я Скопировал скрипт, но на версии 3.1.0.4F не работает, скрипт не виден. Что делать, возможно это из-за того что я создал не в шарпе а в обычном текстовике и вставил скопированный скрипт?

Получившийся файл должен называться Buoyancy.cs
Не Buoyancy.txt и не Buoyancy.cs.txt

А можно исходник сцены? :-) (popcorn1)

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 05 янв 2011, 22:00
alexz
150 Мб
Нормально или сделать простенький текстовый проект?

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 06 янв 2011, 00:38
ScorpionMax
alexz писал(а):
ScorpionMax писал(а):Здравствуйте. Я Скопировал скрипт, но на версии 3.1.0.4F не работает, скрипт не виден. Что делать, возможно это из-за того что я создал не в шарпе а в обычном текстовике и вставил скопированный скрипт?

Получившийся файл должен называться Buoyancy.cs
Не Buoyancy.txt и не Buoyancy.cs.txt


У меня все правильно, я умею это делать, и работать с расширениями файлов.
Вот собственно...
Изображение

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 06 янв 2011, 01:01
alexz
А в каком смысле «скрипт не виден»? Его нельзя повесить на объект или можно, но он не работает? Судя по тому, что скрипт успешно компилируется, по крайней мере, его должно быть видно.

Чтобы скрипт делал что-то полезное, у объекта должен быть какой-нибудь коллайдер, Rigidbody и должна быть указана какая-нибудь правдоподобная масса. Для куба размером 1м лучший результат получается, если масса от нескольких сотен килограмм до тонны. Если больше, куб утонет.

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 06 янв 2011, 01:17
ScorpionMax
alexz писал(а):А в каком смысле «скрипт не виден»? Его нельзя повесить на объект или можно, но он не работает? Судя по тому, что скрипт успешно компилируется, по крайней мере, его должно быть видно.

Чтобы скрипт делал что-то полезное, у объекта должен быть какой-нибудь коллайдер, Rigidbody и должна быть указана какая-нибудь правдоподобная масса. Для куба размером 1м лучший результат получается, если масса от нескольких сотен килограмм до тонны. Если больше, куб утонет.


Я все сделал, теперь скрипт виден, назначается обьекту, но он исчезает при касание с терайном и водой...

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 06 янв 2011, 01:35
alexz
ScorpionMax писал(а):Я все сделал, теперь скрипт виден, назначается обьекту, но он исчезает при касание с терайном и водой...

Какие у объекта размеры, масса?

---
Немного обновил скрипт в первом сообщении:
1) Теперь он сам добавляет коллайдер и rigidbody, если их нет, и предупреждает об этом в логах.
2) Для гашения скорости движущихся объектов сделал поправку на их массу. Иначе для тяжёлых объектов, типа брёвен, всё работало хорошо, а маленькие легкие при попадании в воду останавливались чуть ли ни мгновенно, как будто это кисель, а не вода.

Re: Buoyancy - плавучие объекты

СообщениеДобавлено: 06 янв 2011, 12:55
ScorpionMax
Все стандартое, как из деки Bootcamp, имею в виду размеры, масса 1 кг...
Сейчас скрипт новый погляжу...