Unity: як ми створювали генератор будинків для AssetStore або повернення до хрущовок



Приводом для написання цієї статті стала ця публікація про "хрущовки", в якій була піднята цікава для мене тема програмної генерації мешей Unity.

Моя стаття не призначена для тих, хто вже давно працює на Unity, малоймовірно, що тут буде щось нове для людей, знайомих з тонкощами Unity. Також для зовсім новачків тут можливі неочевидні "труднощі" і навіщо потрібно щось вигадувати, коли отже в редакторі все є. Найбільш підходящої нашою аудиторією є розробники, які вже дещо вміють робити Unity, але вони ще не вирішували задачі динамічного зміни мешей в редакторі. Також, можливо, деяким буде корисний наш досвід роботи з інспектором.

Автор не претендує на абсолютне знання предметної області, я лише хочу розповісти своїми словами вирішення деяких завдань, які з'являлися при створенні ассета.



Прочитавши цю статтю, я відразу зрозумів, що це те що потрібно для нашого проекту. Спочатку ми збиралися створювати будинки з експортованих файлів формату fbx. Звичайно, робити один в один як було описано, ми не стали, ми відштовхнулися від базової ідеї і відразу поставили собі завдання: редагування і перегляд будівель безпосередньо в редакторі Unity без перемикання в режим працює гри. Як наслідок попередньої ідеї вийшло вмістити всі налаштування будівлі в одному скрипті і, в результаті, в інспекторі об'єктів вийшло редагувати всі будова цілком в одному місці.



Невеликий відступ

Почну з самоіронії. Наша невелика команда деякий час тому взялася за розробку ігор для мобільних платформ. Почавши з амбітної задачі створення власного ігрового движка, ми приступили до роботи. За наявного власного досвіду розробки в інших сферах діяльності і читання книг про створення движків для ігор, було вирішено все писати на чистому сі(PureC). Було реалізовано кілька ключових базових підсистем свого движка: оптимізований менеджер пам'яті; графічний рендерер; система ієрархії об'єктів на основі компонентного підходу публікація на хабре) до створення складових моделей; прив'язали LUA для можливості скриптинга створюваних ігор; реалізували можливість зберігати в локальній базі даних на основі SQLite всього і вся (в тому числі і скрипти на Lua) та інше. Після досить тривалого часу реалізації всього вищеописаного і освоєння інтерфейсів, що надаються OpenGL, ми прийшли до невтішного висновку: повноцінний движок ми може бути і створимо, але дуже нескоро.

Перехід на Unity

Природно, паралельно зі створенням власного движка, ми цікавилися здобутками ігрової індустрії. Нас дуже приваблювавUnreal Engine, але мінімальний проект 30...45МБ для мобільних платформ відразу ж остудив нас, і ми вирішили пошукати інші рішення, більш-менш підійшла Shiva, після нетривалого вивчення даного движка, ми все ж вирішили пошукати ще варіанти.

Нарешті, нещодавно ми спробували свої сили у Unity. Синтаксис C# в Unity був частково освоєний, але досить важко було звикати до того, що завдяки системі складання сміття GC) не завжди треба було звільняти створені і вже не потрібні ресурси.

Обрана тематика для роботи

Перед описом подальшої роботи відразу скажу про обмеження: я почав вивчати відразу Unity 5, тобто всі приклади повинні бути працездатні в 5-й версії Unity, за попередні 4-ю, а тим більш 3-ю версію я ніяких гарантій дати не можу. З власного досвіду можу сказати, що деякі уроки/скрипти, які мені траплялися для 4-ї версії, виходило запускати на 5-ці, деякі вимагали переформатування на нову версію і успішно запускалися, а деякі так і не вдалося запустити, доводилося міняти синтаксис команд. Про зворотну сумісність версій мені нічого не відомо (невеличке доповнення: перед публікацією ассета, вказані нижче скрипти я успішно протестував на Unity 4.5.0).

Для візуального наповнення гри було вирішено наповнити навколишній фон різноманітними будівлями. Хотілося відразу в редакторі бачити зразковий зовнішній вигляд будівель. Для того, щоб мати можливість "запускати" скрипт безпосередньо в редакторі, необхідно перед класом, успадкованим від MonoBehaviour, написати рядок [ExecuteInEditMode], тобто створіть у проекті скрипт MyTest.cs і змініть його по нижченаведеному шаблоном:

using UnityEngine;

[ExecuteInEditMode]
public class MyTest : MonoBehaviour {
    //тут все як завжди, 
    //йде перерахування
    //полів і методів
}

При зміні будь-якого поля в інспекторі, буде викликатися метод сценарію (якщо він реалізований) Update(). Для того, щоб заощадити на суму зібраних для гри скриптів, можна даний метод "екранувати" парою директив #if UNITY_EDITOR… #endif, таким чином вимальовується приблизний шаблон змінюваного безпосередньо в інспекторі скрипта для компонента:

using UnityEngine;

[ExecuteInEditMode]
public class MyTest : MonoBehaviour {
    public int i=10;

#if UNITY_EDITOR
    void Update () {
        Debug.Log ("Update");
    }
#endif
}

Прикріпіть даний скрипт до якогось компоненту на сцені і змінюйте в інспекторі значення поля i, в консолі відразу ж буде відображатися "Update" (точніше кількість повідомлень "Update" буде збільшуватися), в інший час скрипт буде "очікувати" зміни публічних полів.

Необхідно відзначити, що при наявності на сцені великої кількості компонентів з цим скриптом, сцена буде помітно пригальмовувати. Щоб позбутися цього, необхідно обробник змін в інспекторі перемістити до скрипт в спеціальній папці Editor. Вміст цієї папки не буде потрапляти в готовий проект. Давайте створимо папку Editor, і в ній скрипт MyTestInspector.cs:

using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor(typeof(MyTest))]
public class MyTestInspector : Editor {

    //пишемо свій обробник інспектора 
    public override void OnInspectorGUI()
    {
        //малюємо інспектор за замовчуванням
        DrawDefaultInspector ();
        //отримуємо посилання на виділений об'єкт
        MyTest mt = target as MyTest;
        //виконуємо метод об'єкта,
        //що реагує на зміни в інспекторі
        mt.DoRefresh();
    }
}

Подивимося на змінений скрипт MyTest:

using UnityEngine;

[ExecuteInEditMode]
public class MyTest : MonoBehaviour {
    public int i=10;

    public void DoRefresh () {
        Debug.Log ("Update");
    }
}

Налаштування інспектора для більш зручної роботи

Після вивчення офіційної довідки і гугления, ми спочатку зупинилися на варіанті, який передбачав перемальовування публічних властивостей в інспекторі та реакції на їх зміну відповідними методами в скрипті, але потім було вирішено розбивати вихідні дані для редагування на взаємопов'язані структури даних, з якими легко оперувати не гублячись у приблизно 130 одночасно видимих налаштуваннях для побудови будинку. Після цього необхідність в "власному інспектора" відпала. Проте, сподіваємось в майбутньому повернутися до цього підходу при створенні і редагуванні компонентів. Якщо комусь цікаво, то можу дати посилання: тут,тут.

Деякі тонкощі створення інтерфейсу

При звичайному створення публічної змінної типу int або float, наприклад таке:

public int iterator=2;
public float dx=0.5 f;

В інспекторі вони відображаються як прості поля редагування, при частій зміні цих значень у процесі роботи моторошно набридає постійно потрапляти курсором мишки в поля, вводити осмислені цифри, мало відрізняються один від одного, і дивитися на отримані зміни, набагато зручніше інший підхід. При оголошенні полів відразу перед змінної (рядком вище) вказувати допустимий діапазон для введених значень:

[Range(-10, 10)]
public int iterator=2;
[Range(0.0 f, 1.0 f)]
public float dx=0.5 f;

Після такого доповнення перед змінними, в інспекторі досить пересувати повзунок для плавного або стрибкоподібної зміни значення.

Для того, щоб не показувати одночасно всі поля в скрипті, а у нас їх близько 130, можна вдатися до угруповання сильно пов'язані один з одним значень в один клас, і вже цей клас оголошувати публічним полем в скрипті. Для того, щоб можна було зберігати зміни полів в окремих примірниках класів і показувати поля в інспекторі, необхідно перед оголошенням класу (знову ж рядком вище) записати рядок [System.Serializable], в результаті отримуємо:

[System.Serializable]
public class LeftRightSide {
    [Range(0, 100)]
    public int leftSide=3;
    [Range(0, 100)]
    public int rightSide=20;
}

Після того, як ви оголосите даний клас публічним полем у своєму скрипті, в інспекторі з'явиться приховується/раскрываемый блок полів для редагування вмісту описаного вище класу. Можлива ієрархічну структуру різних класів один в одного і вона обмежена тільки здоровим глуздом. Такий нехитрий прийом дозволяє по-перше: групувати пов'язані один з одним дані, а по-друге: спрощує навігацію в інспекторі. Опишемо скрипт цілком:

using UnityEngine;

[ExecuteInEditMode]
public class MyTest : MonoBehaviour {
    public LeftRightSide leftRight=new LeftRightSide();

    public void DoRefresh () {
        Debug.Log ("Update");
    }
}

[System.Serializable]
public class LeftRightSide {
    [Range(0, 100)]
    public int leftSide;
    [Range(0, 100)]
    public int rightSide;
}

Змінний безпосередньо в редакторі меш

Підготовка компонентів для малювання меша. Для того, щоб мати можливість редагувати і показувати меш, можна використовувати можливості редактора, наприклад: створити порожній об'єкт на сцені, потім додати йому MeshFilter і MeshRenderer через пункти меню Component->Mesh->Mesh Filter і Component->Mesh->Mesh Renderer відповідно. Перший компонент відповідальний за "внутрішню" геометрію меша, другий пов'язаний з промальовуванням меша на екрані. Для додавання даних компонентів є й інший, більш надійний шлях. При створенні скрипта необхідно дати вказівку додавати зазначені вище два компоненти, якщо вони відсутні на тому компоненті, до якого прикріплюється скрипт. Для цього потрібно перед оголошенням класу-нащадка MonoBehaviour записати рядок [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]. Приблизна заготівля скрипта:

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
[ExecuteInEditMode]
public class MyTest : MonoBehaviour {
    //...
}

Трохи теорії (ті, хто вміє працювати з мешами у Unity, можуть пропустити цей розділ)

Щоб відобразити що-небудь в меші компонента, необхідно заповнити меш необхідними даними. Самий мінімум яких складається з набору вершин і так званих фасетів (у даного назви є синоніми, наприклад: triangles, faces, facets, масив індексів), тобто правил для зв'язування вершин один з одним. Додатково буде розглянуто можливість малювання текстур, тобто потрібно буде використовувати uv-координати. Також коротко будуть показані нормалі. Відразу обмовлюся, тут я розглядаю тільки "класичну" роботу шейдерів, без розгляду роботи системи частинок, сіткового режиму роботи тощо. Тобто буде розглядатися режим промальовування трикутниками, заснованими на вершинах і правила їх "зв'язки". Взагалі докладний розгляд даної теми, тобто мешей, шейдерів, зв'язки вершин, фасетів, нормалей, текстурних координат, верхових буферів, матриць та іншого може(і повинно) зайняти обсяг декількох книг, тому я обмежуся лише мінімальним обсягом інформації, якою має бути достатньо для побудови динамічних мешей, відображаються безпосередньо в редакторі.

Вміючи малювати трикутник, можна намалювати скільки завгодно складну фігуру, комбінуючи між собою трикутники. Наприклад, квадрата, ромба, паралелепіпеда, трапеції і взагалі чотирикутника, достатньо двох трикутників. Для малювання більш складних фігур потрібно більше трикутників, але принцип від цього не зміниться. Тобто мінімальним і достатнім примітивом для малювання всього іншого, умовимося вважати трикутник.

Коротко згадаю про шейдери: шейдери — це спеціальним чином написані і скомпільовані програми, які малюють за певними правилами трикутники. Насправді все трохи складніше, але для стислості приймемо таке трактування. Згадаю лише, що при промальовуванні чергового трикутника, шейдер нічого не знає про сусідніх трикутниках.

Пам'ять у комп'ютері лінійна, і де б не знаходився великий/складовою блок даних (тобто структура, клас, масив, тощо), його вміст знаходиться відразу один за одним, і доступ до його елементів краще організовувати теж лінійно. А так як меш являє собою складну структуру, яка може містити досить великі обсяги різних, але об'єднаних за типом конкретних даних, і управління цими даними теж правильно організувати лінійним доступом. Тобто необхідно створювати та заповнювати масиви відповідними даними, і потім ці масиви "приєднувати" до мешу.

Перерахую деякі потрібні нам типи даних для побудови мешей:

Вершини (vertices) — складаються з масиву даних типу Vector3, тобто структури, що містить у собі три поспіль йдуть даних типу float, є ні чим іншим як просторовими координатами однієї вершини по осях x, y і z.

Індекси вершин або фасети (triangles) — складаються з масиву даних типу int, але тут потрібно враховувати, що цілі числа потрібно групувати по 3 на один трикутник. Розглянемо цей момент детальніше, справа в тому, що для опису одного трикутника (тобто мінімально рисуемого примітиву) потрібно вказати 3 індексу вершини. Перші три числа задають промальовування першого трикутника, другі три — другого і т. д. Також важливо згадати порядок обходу вершин у трикутнику. Якщо ви перераховуєте вершини (тобто індекси вершин) за годинниковою стрілкою, трикутник "дивиться на вас", тобто ви його бачите, інакше трикутник не прорисуется і ви його не побачите, але якщо ви його візуально "обійдете" з протилежного боку, то він стане бачимо для вас (тобто з нового ракурсу перерахування вершин "поміняється" і за годинниковою стрілкою). Як можна здогадатися, загальна довжина всього масиву буде дорівнює кількості трикутників помноженому на три.

Нормалі (normals) — складаються з масиву даних типу Vector3, це масив "перпендикулярів" до вершин, розмірність масиву нормалей і масиву вершин однакова. Абсолютна довжина кожного вектора нормалі дорівнює одиниці, по суті формується "кут повороту" вершини. Навіщо взагалі ці нормалі потрібні? Вони потрібні для того, щоб правильно враховувати освітлення трикутника. Знаючи кути між нормаллю, променем світла і оком спостерігача, можна розрахувати освітленість. Нормалі створюються і обчислюються не для трикутників, як можна припустити, а саме для вершин окремо. Якщо нормаль задавати тільки трикутника, то шейдер не дізнається, як повинна змінюватися нормаль від трикутника до трикутника (тому, що шейдер нічого не знає про сусідні трикутники), від цього малюється фігура буде виглядати хоч і освітленій, але сильно "кутастої". Справа в тому, що шейдер при обробці кожного трикутника, рівномірно змінює деякі параметри між вершинами від однієї до іншої, в тому числі і нормалі до вершин. Завдяки цьому виходить плавне зміна освітленості навіть у межах одного трикутника. Якщо нормалі до вершин трикутника розходяться один від одного, то трикутник буде виглядати "опуклим", якщо нормалі сходяться, то відповідно трикутник буде "увігнутим", якщо нормалі паралельні, трикутник буде плоским. Сусідні трикутники будуть будуватися за цим же принципом, у них будуть збігатися відповідні вершини з нормалями, і якщо трикутники будуть під різними, але не сильно відмінними кутами, то перехід між трикутниками буде плавним, і межа між ними буде малоразличима.

UV-координати (uv) — складаються з масиву даних типу Vector2, тобто структури, що містить у собі дві змінні типу float, які є координатами x і y "всередині" текстури. Тут потрібно розповісти детальніше. Лівий нижній кут текстури відповідає uv координати (0, 0), лівий верхній — (0, 1), правий верхній — (1, 1) і правий нижній — (1, 0). Якщо ви будете брати координати в діапазоні [0...1] то у вас буде вимальовуватися частково або повністю вся текстура, залежно від поставлених значень. Можна брати і значення, що виходять за вказаний діапазон, тоді текстура буде повторюватися стільки разів, скільки ви вкажете, припустимо вибрана координата uv (2, 3.5), тоді по осі x текстура повториться 2 рази, а по осі y 3.5 рази. Для того, щоб текстура могла повторюватися, треба виставити необхідні для цього прапори. У багатьох випадках прапори виставлені за замовчуванням. Розмірність масиву uv координат така ж, як і розмірність вершинного масиву, тобто кожній вершині відповідає рисунок координата uv.

Підсумуємо вищесказане. Для створення і промальовування меша необхідно створити масиви вершин, індексів вершин, uv-координат і нормалей.

Подивіться на малюнок нижче, на ньому схематично показано розміщення вершин прямокутника відносно центру координат. Біля кутів прямокутника зазначені індекси вершин, тобто їх індекс у масиві вершин. Рекомендую при побудові будь-яких фігур створювати їх з "геометричним центром" у початку координат. Це стане в нагоді, якщо вам потрібно буде обертати і/або масштабувати вашу фігуру з передбачуваним результатом. Після створення меша ви легко зможете усунути всі його вершини в потрібному вам напрямку.



Почнемо створення меша поки тільки з вершин і індексів, змінимо зазначений вище сценарій наприклад:

Файл MyTest.cs
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
[ExecuteInEditMode]
public class MyTest : MonoBehaviour {

    //створюємо базові налаштування для банера 
    public Banner banner = new Banner(); 

    void Start () {

    }

    // функція оновлення меша 
    public void DoRefresh()
    {
        //зарезервуємо масив вершин на 3 вершини
        //для малювання одного трикутника
        //розмір масиву індексів вершин буде 3*1=3

        Vector3[] v=new Vector3[3]; //масив вершин 
        int[] f=new int[3]; //масив індексів вершин

        //зарезервуємо посилання на проміжний меш
        Mesh tmpMesh;

        //обчислюємо допоміжні значення
        float w2 = banner.bWidth / 2; //половина ширини банера
        float h2 = banner.bHeight / 2; //половина висоти банера
        //створюємо вершини , координата z дорівнює нулю
        v [0] = new Vector3 (-w2, -h2, 0); //0-я вершина
        v [1] = new Vector3 (-w2, h2, 0); //1-а вершина
        v [2] = new Vector3 (w2, h2, 0); //2-я вершина

        //перераховуємо індекси вершин
        // якщо ми дивимося на малюнок,
        //те перерахування вершин йде за годинниковою стрілкою
        f [0] = 0;
        f [1] = 1;
        f [2] = 2;

        //створюємо проміжний меш
        tmpMesh = new Mesh ();
        //прикріплюємо до мешу масиви
        tmpMesh.vertices = v; //це вершини
        tmpMesh.triangles = f; //це фасети, або масив індексів вершин
        //"приєднуємо" меш до компоненту
        GetComponent<MeshFilter> ().mesh = tmpMesh;
    }
}

[System.Serializable]
public class Banner {
    [Range(0.0 f, 1.0 f)]
    public float bWidth=0.5 f;
    [Range(0.0 f, 1.0 f)]
    public float bHeight=0.5 f;
}


Створіть на сцені "порожній," компонент (меню GameObject->Create Empty) і прикріпіть до нього скрипт, що ви повинні побачити "рожевий" трикутник, якщо трикутник не видно, пообертайте камеру навколо компонента. Спробуйте змінювати ширину банера і висоту в інспекторі, ви повинні відразу ж бачити зміни в трикутнику. Давайте зробимо прямокутник. Для цього змініть вміст методу DoRefresh () на наступне:

MyTest.cs
public void DoRefresh ()
{
    //зарезервуємо масив вершин на 4 вершини для малювання прямокутника
    //для першого трикутника знадобляться 0-я, 1-я і 2-я вершини
    //для другого знадобляться 0-я, 2-я і 3-я вершини
    //так як потрібно 2 трикутника,
    //розмір масиву індексів вершин буде 3*2=6

    Vector3[] v=new Vector3[4];
    int[] f=new int[6];

    Mesh tmpMesh;

    float w2 = banner.bWidth / 2;
    float h2 = banner.bHeight / 2;

    v [0] = new Vector3 (-w2, -h2, 0);
    v [1] = new Vector3 (-w2, h2, 0);
    v [2] = new Vector3 (w2, h2, 0);
    v [3] = new Vector3 (w2, -h2, 0); //3-я вершина

    //перераховуємо індекси вершин
    //якщо ми дивимося на малюнок,
    //те перерахування вершин йде за годинниковою стрілкою
    //1-ї трикутник
    f [0] = 0;
    f [1] = 1;
    f [2] = 2;
    //2-й трикутник
    f [3] = 0;
    f [4] = 2;
    f [5] = 3;

    tmpMesh = new Mesh ();
    tmpMesh.vertices = v;
    tmpMesh.triangles = f;
    GetComponent<MeshFilter> ().mesh = tmpMesh;
}


Після редагування скрипта і перемикання в середу Unity наш трикутник "добудується" до прямокутника. Тепер давайте змінимо колір прямокутника. Для цього необхідно змінити скрипт в 2-х місцях, в самому верху, де створено публічний клас Banner потрібно дописати рядок public Material bannerMaterial; тобто:

public Banner banner = new Banner();
//створюємо посилання на матеріал 
public Material bannerMaterial;

А також в самому кінці методу DoRefresh() допишіть рядок GetComponent<MeshRenderer> ().material = bannerMaterial; тобто:

GetComponent<MeshFilter> ().mesh = tmpMesh;
//прикріплюємо матеріал, яким буде малюватися меш 
GetComponent<MeshRenderer> ().material = bannerMaterial;

Після цього в інспекторі з'явиться змінна типу Material, якій можна призначати матеріал, і якщо ви зміните її значення, то прямокутник відразу відреагує на зміну матеріалу, "перефарбується", але він все одно буде залитий одним кольором(Unity 4.5.0 можливе зміщення текстурних координат). Це відбувається з-за того, що мешу не були надані uv координати, давайте виправимо це. Доведеться знову замінити метод DoRefresh () на наступний текст:

MyTest.cs
public void DoRefresh()
{
    Vector3[] v=new Vector3[4];
    int[] f=new int[6];

    //резервуємо масив uv координат на 4 вершини
    Vector2[] uv=new Vector2[4];

    Mesh tmpMesh;

    float w2 = banner.bWidth / 2;
    float h2 = banner.bHeight / 2;

    v [0] = new Vector3 (-w2, -h2, 0);
    v [1] = new Vector3 (-w2, h2, 0);
    v [2] = new Vector3 (w2, h2, 0);
    v [3] = new Vector3 (w2, -h2, 0);

    f [0] = 0;
    f [1] = 1;
    f [2] = 2;
    f [3] = 0;
    f [4] = 2;
    f [5] = 3;

    //наповнюємо масив масив uv координат до кожної вершини
    uv [0] = new Vector2 (0, 0); //0-я вершина, лівий нижній кут текстури
    uv [1] = new Vector2 (0, 1); //1-я вершина, верхній лівий кут текстури
    uv [2] = new Vector2 (1, 1); //2-я вершина, правий верхній кут текстури
    uv [3] = new Vector2 (1, 0); //3-я вершина, правий нижній кут текстури

    tmpMesh = new Mesh ();
    tmpMesh.vertices = v;
    tmpMesh.triangles = f;
    tmpMesh.uv = uv; //масив текстурних координат 

    GetComponent<MeshFilter> ().mesh = tmpMesh;
    GetComponent<MeshRenderer> ().material = bannerMaterial;
}


Тепер, якщо у вас до матеріалу прикріплена картинка, вона розтягнеться по всьому прямокутника. Але все одно не вистачає реалістичності. Для додання реалістичності, потрібно враховувати освітленість, а для цього необхідно створити нормалі і додати їх в меш. У нашому конкретному випадку це просто. Меш малюється в площині XOY, тобто перпендикулярний осі Z. Залишилося визначитися зі знаком z-координати нормалі. Нормалі повинні виходити з вершин у той півпростір(мається на увазі півпростір лицьової сторони трикутника), з якого видно. Відредагуємо метод DoRefresh() ще раз:

MyTest.cs
public void DoRefresh()
{
    Vector3[] v=new Vector3[4];
    int[] f=new int[6];
    Vector2[] uv=new Vector2[4];

    //резервуємо масив нормалей
    Vector3[] n = new Vector3[4];

    Mesh tmpMesh;

    float w2 = banner.bWidth / 2;
    float h2 = banner.bHeight / 2;

    v [0] = new Vector3 (-w2, -h2, 0);
    v [1] = new Vector3 (-w2, h2, 0);
    v [2] = new Vector3 (w2, h2, 0);
    v [3] = new Vector3 (w2, -h2, 0);

    f [0] = 0;
    f [1] = 1;
    f [2] = 2;
    f [3] = 0;
    f [4] = 2;
    f [5] = 3;

    uv [0] = new Vector2 (0, 0);
    uv [1] = new Vector2 (0, 1);
    uv [2] = new Vector2 (1, 1);
    uv [3] = new Vector2 (1, 0);

    //створюємо нормалі до кожної вершини, вони однакові,
    //і спрямовані в бік, протилежний напрямку осі Z
    for (int i=0; i < 4; i++) {
        n[i]=new Vector3(0, 0, -1);
    }

    tmpMesh = new Mesh ();
    tmpMesh.vertices = v;
    tmpMesh.triangles = f;
    tmpMesh.uv = uv;
    tmpMesh.normals = n; //масив нормалей 

    GetComponent<MeshFilter> ().mesh = tmpMesh;
    GetComponent<MeshRenderer> ().material = bannerMaterial;
}


Тепер, якщо змінювати інтенсивність джерела світла, напрям освітлення, можна відразу побачити результати на прямокутнику.

За сім откланяюсь, стаття отже вийшла досить великий. Прохання надсилати в лічку всі помічені помилки та неточності.

PS: Художник з мене слабкий, тому схематичний креслення вийшов не зовсім явним. Також я не можу опублікувати повний вихідний код споруди будівель, так як комерційний проект.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.