13 мая в 21:53

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

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



Содержание


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

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


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


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


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


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

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



Как думаете, что это за фигура? 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; // чтобы спроецировать наш меш; сразу указываем меш
    }
}

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



Давайте добавим в 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; // и применяем их к мешу
}

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



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


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


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


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


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


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


8

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



16

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


24

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



Жёлтым отображены векторы нормали для одной из вершин


32

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


48

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


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


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


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


Нам нужно будет перебрать все вершины. И сдвинуть каждую вдоль её нормали на значение grayscale, полученное из карты высот, которое мы получим по текстурным координатам и умножим на константу.


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


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


По традиции создаём класс, наследуемый от 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 - 1) + 0.5), (int)(UVs[i].y * (heightMap.height - 1) + 0.5)).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 - 1) + 0.5), (int)(UVs[i].y * (heightMap.height - 1) + 0.5)).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. Балуемся с мешем."?


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

Внимание, ошибка


Перечитав статью я нашёл ошибку, связанную с позиционированием объекта и векторной алгеброй. На основной упор статьи это не влияет, но тем не менее...


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


Ошибка найдена


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


Что хотелось бы добавить


Что хотелось бы ещё добавить, так то, что есть ещё некая недомолвка относительно нормалей. Я неверно настроил нормали для моего полигона. Я указал Vector3.up, и это было бы корректно, если бы мой полигон лежал (смотрел в небо). Но мой полигон стоит и смотрит в камеру, и поэтому для него корректна нормаль (0, 0, -1), то есть направленная на камеру или Vector3.back.


Для простоты понимания на изображении ниже корректная нормаль отображена зелёным, некорректная — красным, а касательные к нормали — синим.


Как вы изначально думали насчёт того, сколько вершин у куба?
72%
(90)
8
2%
(3)
16
21%
(26)
24
4%
(5)
32
1%
(1)
48

Проголосовало 125 человек. Воздержалось 39 человек.

Интересно ли вовлечение типа «Сколько вершин у куба?», опроса в конце или «Найдите ошибку»?
53%
(33)
Все интересны
26%
(16)
Больше нравится «Сколько вершин у куба?»
5%
(3)
Больше нравится «Найдите ошибку»
16%
(10)
Ничего не нравится

Проголосовало 62 человека. Воздержалось 30 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Kit Scribe @KitScribe
карма
10,0
рейтинг 0,1
Хаброжитель
Похожие публикации
Самое читаемое Разработка

Комментарии (13)

  • +3
    Ошибка вот здесь:
    float pixelHeight = heightMap.GetPixel((int)(UVs[i].x * heightMap.width), (int)(UVs[i].y * heightMap.height)).grayscale;
    
    Текстурные координаты описывают границы пикселей, а не их центры. Нужно из ширины и высоты вычитать единицу и после умножения сдвигать все это дело на пол пикселя. Исправить можно так:
    float pixelHeight = heightMap.GetPixel((int)(UVs[i].x * (heightMap.width - 1) + 0.5), (int)(UVs[i].y * (heightMap.height - 1) + 0.5)).grayscale;
    

    • 0
      А, ну и еще вот заметил:
      Затем получать высоту пикселя, а после этого обращаться к нормали высоты и смещать высоту по вектору нормали с учётом полученной высоты пикселя и некоего множителя увеличения.
      Говорите про нормаль высоты, а обращаетесь к нормали меша.
      А еще если просто делать так: vertices[i] += (temp_mesh.normals[i] * pixelHeight) * height; то есть риск порвать меш на дублирующихся вершинах. Попробуйте на куб натянуть карту высот и посмотрите что будет. По хорошему тут либо строить копию меша с одной группой сглаживания, либо как-то заделывать дыры.
      • +1

        Обращаясь к нормали меша, мы обращаемся к нормали вершины, ведь мы проходимся по количеству вершин. Лучше наглядно, думаю :)


        Зелёным отображены нормали, синим — касательные.



        Это просто опечатка.


        Ну а в первом вы правы, да :)

        • 0

          Хотя это даже не опечатка, а скорее неточная формулировка.


          Статья исправлена

  • +1
    Вершин 8.
    Не совсем так. Если все грани жесткие, то их 24 (и то не обязательно). Юнити дублирует вертексы чтобы этого добиться. В то время как Майя, например, воспринимает такие вертексы как «виртуальные», и не показывает их.
    Если все эджи сгладить, вертексов будет 8.
    Да, они еще дублируются в местах разрыва UV-развертки.
    Так что утверждение что вершин — 32, не совсем верное.
    • 0
      Причина дублирования — на один вертекс — одна нормаль. А в местах где ребра жесткие — нормальей должно быть — две и больше. Это можно получить только дублированием вертекса. Аналогично для UV-координат.
    • 0
      Здесь мой коллега рассказывает как раз об этих нюансах, с картинками.
      • 0
        Посмотрел. Не согласен категорически. Ваш коллега дезинформирует слушателей.
        Вот он говорит:
        Движку вашему абсолютно пофигу на количество поликов, ему важно количество вертексов.
        Что неправда, практически с точностью до наоборот. Движку важно именно количество треугольников, потому что он их и растеризует. Количество вертексов влияет лишь на input assembly stage, которая как правило занимает доли процентов от всего реднера (доли процентов — это <1%). А вот количество треугольников влияет существенно, т.к. именно они и растеризуются.

        И подводит к тому, что вот это:
        Заголовок спойлера

        pендерить якобы видеокарте одинаково, потому что там и там 8 вертексов. Нет нет и еще раз нет. Дорогие именно треугольники. А в его случае он наделал еще и тонких треугольников, что значительно ухудшает ситуацию.
        Современная видеокарта растеризует все блоками (тут есть видео, можно наглядно посмотреть процесс растеризации):
        http://renderingpipeline.com/2012/03/gpu-rasterizer-pattern/
        На блок выделяется пачка тредов, и если надо закрасить всего один пиксель из этого блока, то остальные треды будут простаивать. Для тонких длинных треугольников это особо актуально, потому что они как правило не накрывают тайл целиком.
        Вот кстати еще у humus-а хороший тест:
        http://www.humus.name/index.php?page=News&ID=228
        который показывает, что даже топология влияет на производительность.

        Т.е. касательно того, что 3дмакс дублирует вертексы — он отчасти прав. Но то, что вертексы сильно влияют на производительность, а треугольники нет — он тотально не прав. И ладно бы он сам так думал, он же других учит этому… Нельзя же так.
        • 0
          Посмотрел дальше. Там он показывает скрины из humus-овской статьи… но объясняет абсолютно без понимания почему так происходит.
          Я вот подготовил картику-объяснение:
          Как я уже говорил, видеокарта рисует все блоками. На целый блок пикселей выделяется пачка потоков, и если пиксели лежат снаружи треугольника, то потоки, которым достался такой пиксель — будут либо работать вхолостую либо простаивать. На картинке закрашен блок из 8*8 пикселей. Серым отмечены потоки, которые впустую простаивали. Собственно поэтому ребра не эффективны, а не из-за того, что:
          Движок, он не понимает как ему отреднерить пиксель, который находится на эдже. Поэтому он рендерит его сначала справа эджа, потом слева, потом усредненное значение рисует.
          • 0

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

            • 0
              Кроме того, вы сравниваете филлрейт и «вес» меша (количество вертексов). И то и то влияет на производительность, но по-разному.
  • 0
    Картинка с деформированной сферой напомнила мне игру Astroneer — там в распоряжении игрока пяток планет/лун, ваш протагонист в скафандре вооружен подобием «mesh terraforming tool», планеты реально сферические (сдался при попытке прокопать насквозь, но кругосветку совершал не однократно), а так же присутствуют пещеры и надвисающие скалы (правда, весьма скудные в разнообразии и пейзажами не блещут).

    Касательно ограничения вершин в меше — ну так само собой разумеется, что нет смысла клепать один гигантский меш. Либо разбивать на «чанки», либо генерировать на лету на стороне видеокарты. Кстати, если создать стандартный terrain в юнити, включить отображение wireframe и зумиться на ландшафт — можно увидеть, как этот самый terrain переразбивается на части и с детализацией в зависимости от удаленности «чанков».
    Одно время много сил потратил на свой клон майнкрафта — и, честно сказать, всем советую. Хотя бы из соображений того, какую тонну знаний вы можете приобрести в попытке реализации всяких фич:
    генерация ландшафта по 3-х мерному массиву с данными (id блока)
    распараллеливание процессов генерации чанков в разных потоках
    возможность наиграться вдоволь с алгоритмами генерации ландшафта
    реализация алгоритма расчета освещения (особенно если совместить с написанием алгоритма Ambient Occlusion)
    • 0
      После долгой работы в экселе, рефлекторно нажал alt+Enter для переноса на новую строку, что, оказывается, незамедлительно публикует комментарий без возможности правки (прошу прощения за сумбурность вышеизложенного). Также хотел добавить к списку:
      • создание собственного редактора блоков и итемов (помогает разобраться в работе с UnityEditor функционалом, а заодно изобрести алгоритм генерации модели из текстуры, например, построить модель (например, кирки) с помощью обхода по границе прозрачности исходной текстуры), автоматически отрендерить иконки для инвентаря
      • научит оптимизации
      • подтянет знаний в области блокировок (мютексы и т.п.)


      Лично я для себя извлек много знаний и наработок для будущих проектов — как в работе с векторной математикой, так и в общих аспектах программирования.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.