Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот

в 18:53, , рубрики: C#, MESH, unity3d, unity3d уроки, разработка игр

В первой части мы рассмотрели как сгенерировать меш, используя карту высот. За продолжение проголосовало 50 человек, поэтому думаю, что пришло время рассмотреть способы деформации меша с помощью той же карты высот. Всех заинтересованных прошу под кат.

Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот - 1

Здесь нам уже понадобится более глубокое понимание меша, поэтому...

Рассмотрим меш более подробно

Из прошлой статьи мы узнали какими атрибутами владеет меш и для чего они нужны, но поверхностно. Сейчас давайте проясним более подробно для чего они нужны и за что отвечают.

Чтобы не бегать туда-сюда, приведём список того, чем владеет меш:

  • vertices — вершины
  • triangles — треугольники
  • normals — нормали
  • uv — текстурные координаты
  • tangents — касательные

Касательные нам не нужны для того, чтобы построить меш, так что их трогать не будем. Для деформации по карте высот нам будут нужны UV координаты и нормали. Ну и чтобы разобраться со всем этим, давайте попробуем понять самую простую фигуру.

Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот - 2

Как думаете, что это за фигура? Plane? Это не так. Plane имеет гораздо больше треугольников, вершин, uv координат и т.д. Чтобы убедиться в этом, вы можете создать Plane и включить отображение типа Wireframe. Так что же это? Можно сказать, это полигон.

У него есть:

  • 4 вершины — vertices {0, 1, 2, 3}
  • 4 вершины — edges
  • 4 нормали — normals {Vector3(0, 1, 0) x 4} (хотя могут быть и другие)
  • 4 вектора координат текстуры {Vector2(0, 0), Vector2(1, 0), Vector2(0, 1), Vector2(1, 1)} (порядок задаёте вы)
  • 2 треугольника — triangles {(0, 2, 3), (3, 1, 0)} (также порядок за вами)
  • 1 лицевая сторона — face
  • 1 поверхность — surface (обычно и по-хорошему surface.Length = face.Length)

Переведём на язык Unity количество этого всего

  • mesh.triangles.Length = 6
  • mesh.vertices.Length = 4
  • mesh.normals.Length = 4
  • mesh.uv.Length = 4

А теперь попробуем создать это в Unity с помощью кода. Создадим 4 сферы и разместим их в родителя с названием "vertices". Напишем скрипт TestPoly, который повесим на объект "vertices" или куда угодно.

TestPoly.cs

using UnityEngine;

public class TestPoly : MonoBehaviour
{
   // наши вершины
    public GameObject v0;
    public GameObject v1;
    public GameObject v2;
    public GameObject v3;
    public Material mat; // материал, чтобы объект не был розовым
    GameObject poly;
    Mesh mesh;

    void Start()
    {
        poly = new GameObject("poly");
        mesh = new Mesh();

        Vector3[] vertices = new Vector3[] {
        // задаём позиции вершин
                    v0.transform.position,
                    v1.transform.position,
                    v2.transform.position,
                    v3.transform.position,
                };

        Vector3[] normals = new Vector3[] {
            // все помнят, что нормаль всегда перпендикулярна вершине?
            new Vector3(0, 1, 0),
            new Vector3(0, 1, 0),
            new Vector3(0, 1, 0),
            new Vector3(0, 1, 0),
        };

        Vector2[] UVs = new Vector2[] {
                     // U  V
            new Vector2(0, 0),
            new Vector2(1, 0),
            new Vector2(0, 1),
            new Vector2(1, 1),
            // Аналог X и Y, но названы U и V, чтобы не путаться
        };

        int[] triangles = new int[] {
            0, 2, 3, // первый треугольник
            3, 1, 0, // второй треугольник
        };

        mesh.vertices = vertices;
        mesh.normals = normals;
        mesh.uv = UVs;
        mesh.triangles = triangles;

        poly.AddComponent<MeshRenderer>().material = mat; // чтобы отобразить наш меш; сразу вешаем материал
        poly.AddComponent<MeshFilter>().sharedMesh = mesh; // чтобы спроецировать наш меш; сразу указываем меш
    }
}

Отлично, мы видим наш меш.

Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот - 3

Давайте добавим в Update возможность его изменять по вершинам, чтобы было немного интереснее.

Update в TestPoly.cs

void Update()
{
   // чтобы не заморачиваться с Raycast'ом
   // изменения будем производить из сцены

    Vector3[] vertices = new Vector3[] {
                // получаем новые позиции наших вершин
                v0.transform.position,
                v1.transform.position,
                v2.transform.position,
                v3.transform.position,
            };

    mesh.vertices = vertices; // и применяем их к мешу
}

Меш деформируется, как и планировалось. И отображение остаётся неизменным.

Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот - 4

Если вы решили повращать меш и заметили отставание, то вы делаете это неправильно. Чтобы избежать разрыва при перевороте, нужно вращать вершины. То есть объект "verticies". Ведь скрипт устанавливает вершины по этим точкам.

Любой меш состоит грубо говоря из таких полигонов. По личному опыту знаю, что лучше всего понимание приходит на личном взаимодействии, так что попробуйте поизменять массив UVs, triangles и normals и понаблюдайте за результатом. Если вы готовы, давайте двигаться дальше.

Небольшой интерактив

Наверняка вы уже подустали, так что почему бы не отвлечься? Как насчёт небольшой игры? :) Кстати, это поможет понять нормали. Раз, два, три — начало игры!

Как вы думаете, сколько вершин (vertices) имеет куб?

Только подумайте прежде чем открывать спойлеры :)

8

Почти верно! Вы выбрали количество Edge-вершин. 4 снизу и 4 сверху.

Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот - 5

16

Нет, к сожалению, это не так. Попробуйте создать куб в Unity и посмотрите на него со всех сторон.

24

Да, всё верно. Куб имеет 24 вершины.
Всего у куба 8 Edge-вершин и на каждую из вершин приходится по 3 нормали.
8 * 3 = 24

Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот - 6
Жёлтым отображены векторы нормали для одной из вершин

32

Нет, вы ошиблись в расчётах. Логика верная, но подумайте ещё. Создание куба в Unity может вам помочь.

48

Нет, это явно перебор...

Теория деформации с помощью карты высот

В теории хотелось бы, чтобы для того, чтобы произвести деформацию меша с помощью карты высот нам нужно было бы пройтись по карте высот, опять же получить grayscale и изменить высоту вершины меша относительно полученного значения grayscale, помноженного на какой-нибудь коэффициент увеличения.

Но на практике… Как мы можем помнить, карта высот содержит в себе максимум 65025 вершин, минимум — сколько угодно. Что, если деформируемый меш содержит в себе не такое же количество вершин? Тут-то нам и помогут UV координаты и нормали.

Нам нужно будет проходиться по каждой высоте меша, обращаться к UV координатам и проецировать их на карту высот. Затем получать высоту пикселя, а после этого обращаться к нормали высоты и смещать высоту по вектору нормали с учётом полученной высоты пикселя и некоего множителя увеличения.

Если ничего непонятно, перечитайте вдумчиво. Если опять ничего непонятно, давайте напишем код. Логика иногда помогает.

Пишем расширение

По традиции создаём класс, наследуемый от ParentEditor. Его можно найти в предыдущей статье. Наполним класс переменными и сделаем базовый OnGUI.

MeshDeformator

using UnityEngine;
using UnityEditor;

public class MeshDeformator : ParentEditor
{
    public GameObject sourceGameObject;
    public string mname = "Enter name: ";
    public Texture2D heightMap;
    public float height = 35f; // множитель смещения

    [MenuItem("Tools/Mesh Deformator")]
    static void Init()
    {
        MeshDeformator md = (MeshDeformator)GetWindow(typeof(MeshDeformator));
        md.Show();
    }

    void OnGUI()
    {
        sourceGameObject = (GameObject)EditorGUILayout.ObjectField("Game Object to deform", sourceGameObject, typeof(GameObject), true);
        mname = EditorGUILayout.TextField("Mesh name", mname);
        heightMap = (Texture2D)EditorGUILayout.ObjectField("Height Map", heightMap, typeof(Texture2D), false);
        height = EditorGUILayout.Slider("Height scale", height, -100, 100);
    }
}

Добавим в класс метод DeformMesh

DeformMesh

    public void DeformMesh()
    {
        Mesh temp_mesh = new Mesh();
        // копируем меш с помощью класса в ParentEditor
        temp_mesh = CopyMesh(sourceGameObject.GetComponent<MeshFilter>().sharedMesh);
        GameObject temp_go = new GameObject(sourceGameObject.name + ".clone");
        temp_go.transform.position = sourceGameObject.transform.position;

        // для удобства скопируем вершины и UV координаты
        Vector3[] vertices = temp_mesh.vertices;
        Vector2[] UVs = temp_mesh.uv;

        for (int i = 0; i < vertices.Length; i++)
        {
             // получаем высоту пикселя относительно количества вершин
            float pixelHeight = heightMap.GetPixel((int)(UVs[i].x * heightMap.width), (int)(UVs[i].y * heightMap.height)).grayscale;
            // сдвигаем высоту по вектору нормали
            vertices[i] += (temp_mesh.normals[i] * pixelHeight) * height;
        }

        temp_mesh.vertices = vertices;     // присваиваем вершины
        temp_mesh.RecalculateNormals(); // уже использовали
        temp_mesh.RecalculateBounds();  // обновляем bounds

        temp_go.AddComponent<MeshFilter>().sharedMesh = temp_mesh;
        temp_go.AddComponent<MeshRenderer>().material = sourceGameObject.GetComponent<MeshRenderer>().sharedMaterial;
        sourceGameObject = temp_go;
    }

Вы могли увидеть, что мы использовали два метода: RecalculateNormals() и RecalculateBounds(). Первый мы уже использовали, а вот RecalculateBounds() ещё нет. Что же он делает? Грубо говоря, он делает перерасчёт mesh.bounds (границ меша). То есть он обновляет данные о нашем каркасе.

Давайте объединим всё в один скрипт и добавим возможность сохранить наш меш и получившийся объект в префаб.

MeshDeformator.cs

using UnityEngine;
using UnityEditor;

public class MeshDeformator : ParentEditor
{
    public GameObject sourceGameObject;
    public string mname = "Enter mesh name: ";
    public Texture2D heightMap;
    public float height = 35f;

    [MenuItem("Tools/Mesh Deformator")]
    static void Init()
    {
        MeshDeformator md = (MeshDeformator)GetWindow(typeof(MeshDeformator));
        md.Show();
    }

    void OnGUI()
    {
        sourceGameObject = (GameObject)EditorGUILayout.ObjectField("Game Object to deform", sourceGameObject, typeof(GameObject), true);
        mname = EditorGUILayout.TextField("Mesh name", mname);
        heightMap = (Texture2D)EditorGUILayout.ObjectField("Height Map", heightMap, typeof(Texture2D), false);
        height = EditorGUILayout.Slider("Height scale", height, -100, 100);

        if (GUILayout.Button("Deform Mesh", GUILayout.Height(20)))
        {
            DeformMesh();
        }

        if (GUILayout.Button("Save", GUILayout.Height(20)))
        {
            CreatePaths(); // метод ParentEditor
            Mesh updated_mesh = sourceGameObject.GetComponent<MeshFilter>().sharedMesh;
            AssetDatabase.CreateAsset(updated_mesh, "Assets/MeshTools/Meshes/Updated/" + mname + ".asset");
            PrefabUtility.CreatePrefab("Assets/MeshTools/Prefabs/Updated/" + mname + ".prefab", sourceGameObject);
             // сохраняем меш и префаб в Assets/MeshTools/Updated/Meshes/mname
             // Assets/MeshTools/Updated/Prefabs/mname соответственно
        }
    }

    public void DeformMesh()
    {
        Mesh temp_mesh = new Mesh();
        temp_mesh = CopyMesh(sourceGameObject.GetComponent<MeshFilter>().sharedMesh);
        GameObject temp_go = new GameObject(mname + ".clone");
        temp_go.transform.position = sourceGameObject.transform.position;

        Vector3[] vertices = temp_mesh.vertices;
        Vector2[] UVs = temp_mesh.uv;

        for (int i = 0; i < vertices.Length; i++)
        {
            float pixelHeight = heightMap.GetPixel((int)(UVs[i].x * heightMap.width), (int)(UVs[i].y * heightMap.height)).grayscale;
            vertices[i] += (temp_mesh.normals[i] * pixelHeight) * height;
        }

        temp_mesh.vertices = vertices;
        temp_mesh.RecalculateNormals();
        temp_mesh.RecalculateBounds();

        temp_go.AddComponent<MeshFilter>().sharedMesh = temp_mesh;
        temp_go.AddComponent<MeshRenderer>().material = sourceGameObject.GetComponent<MeshRenderer>().sharedMaterial;
        sourceGameObject = temp_go;
    }
}

Тестируем

Я взял высокополигональную сферу, количество вершин: 1324. И отдеформировал её с разными Height scale факторами.

На изображении ниже вы можете видеть:

  • Стандартная сфера со Scale = (10, 10, 10)
  • Недеформированная сфера
  • Слегка деформированная сфера (Height scale = 1.5)
  • Очень деформированная сфера (Height scale = 10)

Unity3D. Балуемся с мешем. Часть 2 — Деформация меша с помощью карты высот - 7

Что дальше?

Что нас с вами ждёт дальше в курсе статей "Unity3D. Балуемся с мешем."?

  • Если статьи встретят хорошо, то мы научимся деформировать меш, основываясь на коллизии. А потом если всё пойдёт по плану, то, возможно, напишем, своё расширение по типу ZBrash.
  • Если же нет — это будет последняя стать.

Автор: KitScribe

Источник

Поделиться