Русский
Русский
English
Статистика
Реклама

Blender

PVS-Studio, Blender цикл заметок о пользе регулярного использования статического анализа

03.03.2021 20:06:54 | Автор: admin

PVS-Studio мониторит код Blender
В статьях мы регулярно повторяем важную мысль: статический анализатор должен использоваться регулярно. В этом случае многие ошибки выявляются на самом раннем этапе, а их исправление максимально дёшево. Однако теория это одно, но намного лучше подкреплять слова практическими примерами. Рассмотрим несколько свежих ошибок, появившихся в новом коде проекта Blender.


Недавно мы настроили регулярную проверку проекта Blender, о чём мой коллега рассказал в статье "Just for fun: команда PVS-Studio придумала мониторить качество некоторых открытых проектов". В дальнейшем планируем начать мониторить ещё некоторые интересные проекты.


Сразу скажу, что мы не ставим перед собой задачу найти как можно больше ошибок. Целью является периодическое написание небольших заметок (таких как эта), в которых мы будем на практике показывать достоинства регулярного анализа кода. Другими словами, мы иногда будем описывать некоторые интересные ошибки в новом коде, найденные при очередном ночном запуске PVS-Studio, и тем самым популяризировать правильное использование методологии статического анализа кода.


Итак, давайте посмотрим, что найдено в свежем коде проекта Blender.


Фрагмент первый: double-checked locking


typedef struct bNodeTree {  ....  struct NodeTreeUIStorage *ui_storage;} bNodeTree;static void ui_storage_ensure(bNodeTree &ntree){  /* As an optimization, only acquire a lock if the UI storage doesn't exist,   * because it only needs to be allocated once for every node tree. */  if (ntree.ui_storage == nullptr) {    std::lock_guard<std::mutex> lock(global_ui_storage_mutex);    /* Check again-- another thread may have allocated the storage       while this one waited. */    if (ntree.ui_storage == nullptr) {      ntree.ui_storage = new NodeTreeUIStorage();    }  }}

Предупреждение PVS-Studio. V1036: Potentially unsafe double-checked locking. node_ui_storage.cc 46


Перед нами неправильная реализация блокировки с двойной проверкой. Для пояснения проблемы процитирую фрагмент статьи "C++ and the Perils of Double-Checked Locking", написанной Scott Meyers и Andrei Alexandrescu ещё в 2004 году. Как видите, проблема давно известна, но это не защищает разработчиков от того, чтобы наступать на одни и те же грабли. Хорошо, что анализатор PVS-Studio помогает выявлять подобные проблемы :). Итак, фрагмент из статьи:


Consider again the line that initializes pInstance: pInstance = newSingleton;

This statement causes three things to happen:

Step 1: Allocate memory to hold a Singleton object.

Step 2: Construct a Singleton object in the allocated memory.

Step 3: Make pInstance point to the allocated memory.

Of critical importance is the observation that compilers are not constrainedto perform these steps in this order! In particular, compilers are sometimes allowed to swap steps 2 and 3. Why they might want to do that is a question we'll address in a moment. For now, let's focus on what happens if they do.

Consider the following code, where we've expanded pInstance's initialization line into the three constituent tasks we mentioned above and where we've merged steps 1 (memory allocation) and 3 (pInstance assignment) into a single statement that precedes step 2 (Singleton construction). The idea is not that a human would write this code. Rather, it's that a compiler might generate code equivalent to this in response to the conventional DCLP source code (shown earlier) that a human would write.

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


Подобные ошибки очень коварны! Они могут очень редко проявлять себя. Программа вроде как работает, проходит все тесты и так далее. Но время от времени она неожиданно падает у пользователей, и понять причину может быть крайне проблематично. Воспроизвести такую ошибку может быть очень сложно. Другим словами, исправление этой ошибки по жалобе от пользователей может обойтись в 1000 раз дороже, чем правка кода, сделанная по результату анализа кода с помощью PVS-Studio или аналогичного инструмента.


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


Фрагмент второй: realloc


static void icon_merge_context_register_icon(struct IconMergeContext *context,                                             const char *file_name,                                             struct IconHead *icon_head){  context->read_icons = realloc(context->read_icons,    sizeof(struct IconInfo) * (context->num_read_icons + 1));  struct IconInfo *icon_info = &context->read_icons[context->num_read_icons];  icon_info->head = *icon_head;  icon_info->file_name = strdup(path_basename(file_name));  context->num_read_icons++;}

Анализатор PVS-Studio выдаёт здесь два предупреждения, и это правильно. Здесь действительно допущено сразу две ошибки различного плана.


Первая: V701: realloc() possible leak: when realloc() fails in allocating memory, original pointer 'context->read_icons' is lost. Consider assigning realloc() to a temporary pointer. datatoc_icon.c 252


Если память не удастся выделить, функция realloc вернёт значение NULL. Нулевой указатель будет записан в переменную context->read_icons, а её предыдущее значение будет потеряно. Раз предыдущее значение указателя потеряно, то и невозможно освободить ранее выделенный блок памяти, на который ссылался этот указатель. Произойдёт утечка памяти.


Вторая: V522: There might be dereferencing of a potential null pointer 'context->read_icons'. Check lines: 255, 252. datatoc_icon.c


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


Интереснее другое. На самом-то деле падение может и не произойти. Запись производится вовсе не по нулевому указателю, а куда-то дальше. Теоретически возможна ситуация, когда этот адрес уже не находится в странице памяти, защищенной от записи, и никакого падения не будет. Будут испорчены какие-то случайные данные в памяти, и программа продолжит своё выполнение. Последствия работы с испорченными данными непредсказуемы. Подробнее всё это разобрано в статье "Почему важно проверять, что вернула функция malloc".


Фрагмент третий: разыменование указателя для проверки


static int node_link_invoke(bContext *C, wmOperator *op, const wmEvent *event){  ....  bNodeLinkDrag *nldrag = node_link_init(bmain, snode, cursor, detach);  nldrag->last_picked_multi_input_socket_link = NULL;  if (nldrag) {    op->customdata = nldrag;  ....}

Предупреждение PVS-Studio: V595: The 'nldrag' pointer was utilized before it was verified against nullptr. Check lines: 1037, 1039. node_relationships.c


Один из самых частых паттернов ошибок (proof). В начале указатель nldrag разыменовывается. Но из следующего условного оператора становится видно, что на самом деле этот указатель может быть нулевым.


Всё просто и понятно. Но, согласитесь, намного лучше поправить такую ошибку сразу, ещё на этапе написания кода, а не после того, когда на неё наткнётся тестировщик или пользователь.


Кстати, нашлась ещё одна такая-же ошибка, но описывать её неинтересно. Приведу только сообщение: V595: The 'seq' pointer was utilized before it was verified against nullptr. Check lines: 373, 385. strip_add.c


Заключение


Используйте статические анализаторы кода регулярно. От этого выиграют как разработчики, так и пользователи. Вы можете скачать и попробовать PVS-Studio здесь. Спасибо за внимание.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio, Blender: Series of Notes on Advantages of Regular Static Analysis of Code.

Подробнее..

Как я написал браузерный 3D FPS шутер на Three.js, Vue и Blender

07.05.2021 02:13:27 | Автор: admin
Стартовый экран игрыСтартовый экран игры

Мотивация

На пути каждого коммерческого разработчика (не только кодеров, но, знаю, у дизайнеров, например, также) рано или поздно встречаются топкие-болотистые участки, унылые мрачные места, блуждая по которым можно вообще забрести в мертвую пустыню профессионального выгорания и/или даже к психотерапевту на прием за таблетками. Работодатели-бизнес очевидно задействует ваши наиболее развитые скилы, выжимая по максимуму, стек большинства вакансий оккупирован одними и теми же энтерпрайз-инструментами, кажется, не для всех случаев самыми удачными, удобными и интересными, и вы понимаете что вам придется именно усугублять разгребать тонну такого легаси Часто отношения в команде складываются для вас не лучшим образом, и вы не получаете настоящего понимания и отдачи, драйва от коллег Умение тащить себя по-мюнхаузеновски за волосы, снова влюбляться в технологии, увлекаться чем-то новым [вообще и/или для себя, может быть смежной областью], имхо, не просто является важным качеством профессионала, но, на самом деле, помогает разработчику выжить в капитализме, оставаясь не только внешне востребованным, конкурентоспособным с наступающей на пятки молодежи, но, прежде всего, давая энергию и движение изнутри. Иногда приходится слышать что-нибудь вроде: а вот мой бывший говорил, что если бы можно было не кодить, он бы не кодил!. Да и нынешняя молодежь осознала что в сегодняшней ситуации честно и нормально зарабатывать можно только в айти, и уже стоят толпою на пороге HR-отдела... Не знаю, мне нравилось кодить с детства, а кодить хочется что-нибудь если не полезное, то хотя бы интересное. Короче, я далеко не геймер, но в моей жизни было несколько коротких периодов когда я позорно загамывал. Да само увлечение компьютерами в детстве началось, конечно же, с игр. Я помню как в девяностые в город завезли Спектрумы. Есть тогда было часто практически нечего, но отец все-таки взял последние деньги из заначки, пошел, отстоял невиданно огромную очередь и приобрел нам с братом нашу первую чудо-машину. Мы подключали его через шнур с разъемами СГ-5 к черно-белому телевизору Рекорд, картинка тряслась и моргала, игры нужно было терпеливо загружать в оперативную память со старенького кассетного магнитофона [до сих пор слышу ядовитые звуки загрузки], часто переживая неудачи... Несмотря на то что ранние программисты и дизайнеры умудрялись помещать с помощью своего кода в 48 килобайт оперативной памяти целые миры с потрясающим геймплеем, мне быстро надоело играть и я увлекся программированием на Бейсике)), рисовал спрайтовую графику (и векторная трехмерная тогда тоже уже была, мы даже купили сложную книжку), писал простую музыку в редакторе... Так вот, некоторое время назад мне опять все надоело, была пандемийная зима и на велике не покататься, рок-группа не репетировала Я почитал форумы и установил себе несколько более-менее свежих популярных игр, сделанных на Unity или Unreal Engine, очевидно. Мне нравятся РПГ-открытые миры-выживалки, вот это все... После работы я стал каждый вечер погружаться в виртуальные миры и рубиться-качаться, но хватило меня ненадолго. Игры все похожи по механикам, однообразный геймплей размазан по небольшому сюжету на кучу похожих заданий с бесконечными боями Но, самое смешное, это реально безбожно лагает в важных механиках. Лагают коммерческие продукты которые продают за деньги А любой баг, имхо, это сильное разочарование он мгновенно выносит из виртуальной среды, цифровой сказки в реальный мир Конечно, отличная графика, очень круто нарисовано. Но, утрируя, я понял что все эти поделки на энтерпрайзных движках, по сути даже не кодят. Их собирают менеджеры и дизайнеры, просто играясь с цветом кубиков, но сами кубики, при этом практически не меняются... Вообщем, когда стало совсем скучно, я подумал что а я ведь тоже так могу, да прямо в браузере на богомерзком непредназначенным для экономии памяти серьезного программирования джаваскрипте. Решил наконец полностью соответствовать тому что все время с умным видом повторяю сыну: уметь делать игры, намного интереснее чем в них играть. Одним словом, я задался целью написать свой кастомный браузерный FPS-шутер на открытых технологиях.

Итак, на данный момент, первый результат по этой долгоиграющей таски на самого себя можно тестить: http://robot-game.ru/

Стек и архитектура

Вполне может быть, что я не вкурсе чего-то (ммм на ум приходит что-нибудь вроде quakejs и WebAssembly), но, с основной технологией было, походу, особо без вариантов. Библиотека Three.js давно привлекала мое внимание. Кроме того, в реальной коммерческой практике, несколько раз, но уже приходилось сталкиваться с заказами на разработку с ее использованием. На ней я сделал собственно саму игру.

Очевидно, что нужно что-то вокруг для простого интерфейса пользователя: шкал, текстовых сообщений, инструкций, контролов настроек, вот этого всего. Я решил поленился, не усложнять себе жизнь и использовать любимый фреймворк Vue 2, хотя, надо было, конечно, писать на свежем, похожем по дизайну и еще более прогрессивном по сути молниеносном Svelte. Но так как хорошенько разобраться предстояло, прежде всего, с Three, думаю, это было правильное решение. Хорошо знакомый и предсказуемый, лаконичный, изящный, удобный и эффективный Vue, позволил практически не тратить время на внешний пользовательский интерфейс.

Когда-то давно я работал дизайнером на винде и достаточно бойко рисовал 2D в Иллюстраторе, но навыков 3D у меня никаких не было. А вот в процессе создания шутера пришлось пойти, скачать и установить одним кликом на свой нынешний Linux Blender. Я быстро научился рисовать с помощью примитивов мир, отдельные объекты, и даже научился делать UV-развертки на них. Но! В целях простоты, скорости работы и оптимизации объема ассетов в моей нынешней реализации не используются текстурные развертки. Я просто подгружаю чистые легковесные бинарные glTF: .glb-файлы и натягиваю на них всего несколько вариантов нескольких текстур уже в джаваскрипте. Это приводит к тому что текстуры на объектах искажаются в разных плоскостях, но на основном бетоне для стен, смотрится даже прикольно, такой разный, рваный ритм. Кроме того, сейчас персонажи не анимируются пока не было времени изучить скелетную анимацию. Одной из основных целей написания этой статьи является желание найти (по знакомым не получилось) специалиста который поможет довести проект до красоты (очень хочется) и согласится добавить совсем немного анимаций на мои .glb (об условиях договоримся). Тогда враги, будут погружаться в виде glTF со встраиванием: .gltf-файлов со встроенными текстурами и анимациями. Сейчас уже есть два вида врагов: ползающие-прыгающие наземные дроны-пауки и их летающая версия. Первых нужно научить шевелить лапками при движении и подбирать их в прыжке, а вторым добавить вращение лопастей.

Модель дрона-паука в BlenderМодель дрона-паука в Blender

Для того чтобы игру нельзя было тупо-легко прочитить через браузерное хранилище я добавил простенький бэкенд на Express с облачной MongoDB. Он хранит в базе данные о прогрессе пользователя по токену, который на фронте записывается в хранилище. Хотелось сделать не просто FPS-шутер, а привнести в геймплей элементы РПГ. Например, в нынешней реализации мир делиться на пять больших уровней-локаций между которыми можно перемещаться через перезагрузку. При желании локации можно быстро дорисовывать из уже имеющихся и добавлять в игру, указывая только двери входа и выхода, стартовую и конечную координату, хорошее направление камеры для них (при переходе живого персонажа через дверь текущее направление сохраняется-переносится). На каждом уровне есть только одна формальная цель найти и подобрать пропуск к двери на следующий уровень. Пропуски не теряются при проигрыше на локации (только при выборе перехода на стартовый уровень после выигрыша на последнем пятом). А вот враги и полезные предметы цветы и бутылки при переходе между локациями, проигрыше или перезагрузке страницы пока выставляются заново согласно основной glb-модели одновременно и схеме, и визуальной клетке локации об этом дальше. И тут вот первое важное про архитектуру: мой фронтенд это совсем примитивное SPA. Vue, например, ни для чего не нужен роутер. Вероятно, я получу негативную реакцию некоторых продвинутых читателей, после того, как сообщу что потратил кучу времени для того чтобы попробовать организовать перезагрузку-очистку сцены внутри системы и пока с самым провальным результатом. Вот к такой спорной мысли я пришел в процессе своих экспериментов: самый эффективный, простой, даже, в этой ситуации, правильный и при этом, конечно же, топорный подход, это нативный форс-релоад после того как мы сохраняем или обнуляем данные пользователя на бэкенде:

window.location.reload(true);

А потом просто дадада считываем их обратно )) и строим всю сцену заново, с чистого листа, так сказать. Тут, конечно, можно было бы улучшить прокидывать пользователя через хранилище вместо того чтобы ожидать разрешения запроса, но это не критично, в данном случае. Небольшое количество оптимизированных текстур (меньше полтора мегабайта сейчас), сильно компрессированного аудио (MP3, понятно: 44100Гц 16 бит, но с сильным сжатием 128 кбит/с меньше полтора мегабайта все вместе сейчас), основная модель-локация весящая около 100Кб и модели отдельных объектов каждая еще меньше... Я добился того что переход между локациями полная перезагрузка мира занимает вполне приемлемое время, судя по записи перфомансов примерно две с чем-то, три секунды. И это, кажется, меньше чем во всех шовных открытых мирах от энтерпрайза которые я видел. Продвинуто бесшовный я тоже один нашел и поиграл, но он лагал хуже всех, и когда сюжет наконец двинулся с мертвой точки вдруг перестали работать сейвы; тут я уже забил

Все использующиеся в игре текстурыВсе использующиеся в игре текстурыПерфомансПерфоманс

Хочется сразу сказать что техлиды и сеньоры с менторским тоном и заоблачной экспертизой в микробенчмаркинге в комментариях только приветствуются. Это же вообще самое забавное и интересное на Хабре когда лиды с сеньорами начинают рубиться в комментариях за стоимость операций в джаваскрипте и то, чей микробенчмаркинг заоблачнее! Остается только надеятся на то, что когда вы будете размазывать мой форсрелоад как дешёвое и сердитое средство изменения сцены вы обязательно продемонстрируете ваши работающие примеры в которых сцена Three с большим количеством разнообразных объектов на ней очищается и заново инициализируется через свои внутренние методы (например, без перезагрузки текстур и прочих ассетов, аудио). Я же не говорю что это невозможно, это очевидно дорого. Намного дороже чем просто сделать форсрелоад. Понятно что хороший проект это прежде всего кодовая база которая может и должна легко развиваться. Но невозможно прикрутить все фичи сразу, а использование дешевого релоада сейчас никак не блокирует добавление более сложного функционала в будущем. Да и кроме дешевизны более простой подход и идеологически привлекателен. Я убежден что хороший код это простой и понятный код, хороший подход простой подход, точно так же как и интерфейс который они предоставляют. Простое решение лучше сложного, особенно если мы только начинаем строить что-то.

Для того чтобы избежать лишних сложностей в моей реализации сцена практически неизменна. Она разворачивается, запускается и дальше функционирует в некотором постоянном виде [порождая и уничтожая только выстрелы и взрывы] пока не происходит переход в другую локацию (или проигрыш на этой). Конкретнее: cейчас я нигде кроме удаления не подлежащих внешнему учету выстрелов и взрывов не использую scene.remove(object.mesh) например при сборе героем полезных предметов, делая вместо этого:

// встроенное свойство на Object3D в Threeobject.mesh.visible = false;// кастомный флаг кастомного массива объектовobject.isPicked = true;

Поэтому мы, например, можем даже использовать свойство id: number mesh`ей вместо uuid: string для учета и идентификации объектов. Так как все подлежащие учету объекты всегда остаются на сцене мы можем быть уверены что Three не поменяет айдишники, сдвинув нумерацию под коробкой при удалении элемента (но если вы хотите все-таки удалять что-то такое просто опирайтесь на uuid при работе с этим).

Я нигде и не на чем не использую .dispose(), так как мне просто нечего удалять. В документации библиотеки сказано что лучший момент и повод для этого переход между уровнями, когда можно и нужно, например удалить ненужные текстуры. Как вы видите выше, текстур у нас совсем немного и они все всегда нужны.

Посмотрим на структуру проекта:

. /public // статические ресурсы   /audio // аудио     ...   /images // изображения     /favicons // дополнительные фавиконки для браузеров       ...     /modals // картинки для информационных панелей       /level1 // для уровня 1         ...       ...     /models       /Levels         /level0 // модель-схема Песочницы (скрытый уровень 0 - тестовая арена)           Scene.glb         ...       /Objects          Element.glb          ...     /textures        texture1.jpg        ...   favicon.ico // основная фавиконка 16 на 16   index.html // статичный индекс   manifest.json // файл манифеста   start.jpg // картинка для репозитория ) /src   /assets // ассеты сорцов     optical.png // у меня один такой )))   /components // компоненты, миксины и модули     /Layout // компоненты и миксины UI-обертки над игрой       Component1.vue // копонент 1       mixin1.js // миксин 1       ...     /Three // сама игра        /Modules // готовые полезные модули из библиотеки          ...        /Scene           /Enemies // модули врагов             Enemy1.js             ...           /Weapon // модули оружия             Explosions.js // взрывы             HeroWeapon.js // оружие персонажа             Shots.js // выстрелы врагов           /World // модули различных элементов мира             Element1.js             ...           Atmosphere.js // модуль с общими для всех уровней объектами (общий свет, небо, звук ветра) и проверками взаимодействия между другими модулями           AudioBus.js // аудио-шина           Enemies.js // модуль всех врагов           EventsBus.js // шина событий           Hero.js // модуль персонажа           Scene.vue // основной компонент игры           World.js // мир   /store // хранилище Vuex     ...   /styles // стилевая база препроцессора SCSS     ...   /utils // набор утилитарных js-модулей для различных функциональностей     api.js // интерфейс для связи с бэкендом     constants.js // вся конфигурация игры и тексты-переводы     i18n.js // конфигурация переводчика     screen-helper.js // модуль "экранный помощник"     storage.js // модуль для взаимодействия с браузерным хранилищем     utilities.js // набор полезных функций-атомов   App.vue // "главный" компонент   main.js // эндпоинт сорцов Vue ... // все остальное на верхнем уровне проекта, как обычно: конфиги, gitignore, README.md и прочее

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

Сейчас игра в спокойном состоянии когда потревоженных врагов нет или совсем мало, на компьютере с поддержкой GPU выдает практически коммерческие 60FPS в Google Chrome (ну или Yandex Bro). В Firefox игра запускается, но показатель производительности не менее чем в 2-3 раза ниже. А когда начинается мясо, появляется много потревоженных врагов, выстрелов и взрывов в Лисе процесс начинает лагать и может вообще повиснуть. Моя экспертиза в микробенчмаркинге сейчас пока не позволяет с умным видом рассуждать о причинах этой разницы. Будем считать что дело в более слабой поддержке WebGL и вычислительных способностях, что-то такое))...

Легенда

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

Земля, далекое будущее. Люди давным-давно перебили друг-друга в ядерных войнах, выясняя кто правый, кто левый, кто белый, а кто красный и прочее. На не затронутых бомбардировками атоллах в Тихом Океане размножилось несколько рас человекоподобных роботов. Например, более человекоподобные, имитирующие органику, двуполость и личные отношения Собутыльники, которые перерабатывают животных и растительность в жизненную силу и спецэффекты. Внутри них, по тонким крепким трубкам, течет специальный сброженный органический микс, схожий с человеческим вином, приводя их в движение. Или более машиноподбные однополые Кибер-Танцоры, проповедующие медитативный Дзинь-Нойз. На почве гендерных и религиозных разногласий между культурами, конечно же, понеслась жестокая война.

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

Робот-Собутыльник приходит в себя на полу пыточной камеры тюрьмы Однополых... Баки пусты... Его мучители, видимо, решили что он уже не жилец, и оставили подыхать... На стене висит портрет легендарного Последнего Президента идеологического предтечи и кумира Танцоров человека, когда-то развязавшего последнюю в истории человечества войну

Аллегории прозрачные, конечно. Но сама рамочная идея про конфликт однополых и двуполых машин в будущем, имхо, замечательная. Так и будет, точно, все предпосылки уже налицо. ))

В игре пока нет самих главных врагов, только их дроны (нужен специалист по скелетной анимации отзовись!), но уже присутствуют специфические объекты и отдельная механика позволяющая глубже раскрыть генезис Собутыльников, историю их борьбы против демократической диктатуры Танцоров. Это информационные панели которые включаются когда герой находится поблизости и важно в одном помещении с ними. Ниже об этом будет подробнее, когда будем разбирать постройку мира и взаимодействие модулей.

ДашбордДашборд

Если подойти к панели и нажать E открывается модаль с исторической справкой:

Рассказ о будущем внутриРассказ о будущем внутри

Это задел для дальнейшего развития в сторону РПГ. Например на таких дашбордах несложно организовать торговый ларек, в котором можно было бы менять поверженный и подобранный металлолом на полезные цветы и бутылки.

Геймплей

В моей игре любой выстрел сталкиваясь с препятствием вызывает взрыв который уронит и врагов и героя на величину зависящую от расстояния. Поражение врага вызывает взрыв повышенной мощности следует быть осторожным в ближнем бою.

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

Цветы и бутылкиЦветы и бутылки

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

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

Уровни сложностиУровни сложности

Если я найду скелетного аниматора и смогу довести до ума то что уже есть и потом продолжить развивать проект дальше, можно будет подумать о следующих фичах:

  • Разное оружие. Виномет потеряет прицел, но станет работать как пулемет-автомат не нужно будет все время нажимать на левую кнопку мыши. С прицелом будет снайперская винтовка стреляющая стальными снарядами (которые, например, можно делать из собранных разрушенных врагов тут бы пригодился уже упомянутый торговый ларек на дашбордах).

  • Еще один вид бутылок с крепышом часовые мины: установил быстро отбегаешь. Будут полезны для разрушения Танков с огромным здоровьем или крупных скоплений любых врагов.

  • Новые типы врагов. Танки медленные, но очень живучие и с убойным выстрелом. Стационарные дроны-пушки умеющие стрелять не в горизонтальной плоскости навесом как делают дроны сейчас, а под разными углами и двойными зарядами. Рядовые бойцы Танцоры Роботы-Курицы мой барабанщик почему-то их именно так видит. В идеале они высаживаются как спезназ, приземляясь на челноке в центр третьей локации когда герой на нее заходит. В пятой локации может появиться босс: Робот-Блогер Финальный с ракетницей

  • Трубочных и двуполых Собутыльников нарисовать сложно, но в идеале было бы рассадить их по камерам Централа четвертой локации.

  • Можно добавить 2D-карту с врагами (внизу и по центру экрана)

Планов полно, но без скелетной анимации они бессмысленны, конечно

Но хватит лирики, перейдем к техническим решениям и собственно коду.

Конфигурация

Особенный кайф от написания кастомной игры в том, что после того как вы доставили новые фичи или любые изменения в код вам просто необходимо расслабиться и их честно искренне протестировать. Ручками. Сделать несколько каток, по любому. Тесты тут никак и ничем не помогут, даже, убежден, наоборот будут мешать прогрессу, особенно если вы не работаете по заранее известному плану, а постоянно экспериментируете. Браузерная игра на джаваскрипт это в принципе превосходный пример того, когда статическая типизация или разработка через тестирование будут только мешать добиться действительно качественного результата. (А на чем тут необходимо проверять типы, господа сеньоры? Я до сих пор в замешательстве от React c CSS Modules и просто Flow, а не TS даже в котором авторы маниакально проверяли что каждый, еще и передаваемый по цепочке компонент, класс модулей для оформления !!! это string А тут что будем маниакально типизировать, вектора?). И даже сам Роберт Мартин в Идеальном программисте делает несколько пассажей на тему бессмысленности TDD, когда говорит о рисках при разработке GUI. В моей игре можно сказать что и нет практически ничего кроме тонны двумерного и трехмерного GUI, ну и логики для него. Любая ошибка либо вызовет исключение, либо неправильное поведение во вьюхе и геймплее, которое может быть очень быстро обнаружено с помощью визуальной проверки, но очень сомнительно что вообще способно быть покрыто тестом.

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

Все настройки настройки и значения влияющие на геймплей и дизайн (константа DESIGN), а также весь текстовый контент-переводы у меня сосредоточены в constants.js.

Контрол

На сайте библиотеки Three представлено большое количество полезных примеров с демо-стендами, самых разных реализаций, функциональностей которые стоит изучить и по возможности к месту использовать. Я отталкивался в своих исследованиях, прежде всего, вот от этого примера. Это правильный, мягкий инерционный контрол от первого лица который математически обсчитывает столкновения с клеткой-миром gld-моделью с помощью октодерева. Проверять столкновения можно для капсулы (для героя или врагов) или обычных сферы Sphere и луча Ray от Three. Этого в принципе достаточно для чтобы сделать FPS-игру: сделать так чтобы герой и враги не сталкивались с миром и между собой, выстрелы взрывались при попадании в другие объекты и тд.

Для того чтобы понимать что происходит когда вы нажимаете кнопку Играть, игра запускается и курсор мыши пропадает вы должны знать о браузерной фиче Pointer_Lock_API. Мы добавляем такой контрол вместе со всей стандартной кухней Three в инициализации основного компонента-сцены, ищите:

// Controls// In First Person...

Но! Тут нюанс браузеры обязательно оставляют путь для панического отступления пользователю и резервируют клавишу Esc для того чтобы пользователь всегда мог разлочить указатель. Это касается нашего UI/UX в игре необходима клавиша P ставящая мир на паузу. Когда указатель залочен то бишь запущен игровой процесс нажатие на Esc, как уже сказано вызовет паузу. Но если мы попытаемся добавить обработку отпускания по 27ому коду даже только для режима паузы, все равно очень быстро увидим в консоли:

ОшибкаОшибка

Поэтому: забудьте про Esc. Пауза по клавише P. Есть еще одно ограничение и проблема связанная с созданием хорошего FPS-контрола: оружие. Я так понял что в энтерпрайзных реализациях руки-оружие это отдельный независимый план наложенный поверх мира. С Three, насколько я понимаю, сделать так не получится. Поэтому мой пока единственный в арсенале грозный виномет с оптическим прицелом это объект сцены который приделан к контролу. Я копирую вектор направления камеры на него. Но около зенита и надира в результате его начинает штормить он не может однозначно определить позицию. При взгляде совсем под ноги я его просто скрываю, а вот стрелять наверх нужно. Что делать с этим небольшим и не особо заметным багом я пока не придумал.

Оптический прицел винометаОптический прицел винометаВыстрел вверхВыстрел вверх

Пытаясь сделать скоростной задорный шутер на Three мы можем сразу забыть о тенях или дополнительных источниках освещения, особенно движущихся. Да, я пытался запилить качающиеся на ветру лампы для особенного мрачняка и криповости, движущиеся тени от них. Нет никак нельзя даже статичные точечные источники света сильно просаживают производительность (а нам еще врагов гонять). По поводу света я пришел к простому компромиссу: чтобы картинка не выглядела совсем сухо и скучно приделать мощный фонарик к контролу, герою. Фонарик можно выключать клавиша T.

Далее я просто пройдусь по основным модулям давая небольшие комментарии в интересных моментах.

Сцена

Основной компонент Scene.vue предоставляет:

  • всю стандартную кухню Three: Renderer, Scene и ее туман, Camera и Audio listener в ней, Controls

  • набор утилитарных переменных для использования в анимационных циклах низовых модулей

  • переменные для хранения коллекций примитивных дополнительных объектов превдоmesh`ей по которым работает кастинг

  • в том числе и через используемые миксины все необходимые ему самому или его низовым модулям геттеры и экшены стора Vuex

  • обрабатывает большинство (кроме тех, что удобно ловить в логике героя) событий клавиатуры, мыши и так далее

  • инициализирует Аудиошину, Шину Событий и Мир

  • анимирует Шину Событий, Героя и Мир

  • в наблюдателях значений важных геттеров добавляет игровой логики

Весь код тут простой, прямо очевидный, практически не требующий дополнительных пояснений. Что-то мы будем рассматривать дальше, например что за такие превдоmesh`и для кастинга. Но стоит только остановить внимание на такой простой базовой сущности как переменные. Дело в том что существует еще один действительно важный аспект который позволит нам вытянуть эту задачу с шутером это тут можно начинать смеяться экономия памяти в джаваскрипте (господа техлиды-сеньоры?). Да не надо ни при каких обстоятельствах создавать переменные в анимационных циклах и проверках низовых модулей, нужно использовать только те что уже есть в основном компоненте (ну или модуле), созданы заранее. Контекст основного компонента можно передавать в публичные методы низовых модулей-функций.

Стандартный модуль героя, врагов, предмета или специфического объекта вроде двери или информационной панели в общем виде выглядит так:

import * as Three from 'three';import { DESIGN } from '@/utils/constants';function Module() {  let variable; // локальная переменная - когда очень удобна или необходима при инициализации или во всей логике    // ...  // Инициализация  this.init = (    scope,    texture1,    material1,    // ...  ) => {    // variable = ...    // ...  };  // Функция анимационного цикла для этого модуля - опционально (предметы, например, не нужно анимировать)  this.animate = (scope) => {    // А вот тут и в остальной логике стараемся использовать уже только переменные Scene.vue:    scope.moduleObjectsSore.filter(object => object.mode === DESIGN.ENEMIES.mode.active).forEach((object) => {      // scope.number = ...      // scope.direction = new Three.Vector3(...);      // variable = ... - так, конечно, тоже можно, главное не let variableNew;      // ...    });  };}export default Module;

Стор

Хранилище Vuex поделено на 3 простых модуля. layout.js отвечает за основные параметры игрового процесса: паузы-геймоверы и тд, взаимодействует с API-бекенда. В hero.js большое количество полей и их геттеров, но всего два экшена/мутации. Этот модуль позволяет в максимально унифицированной форме распространять изменения значений отдельных параметров, шкал, флагов на герое с помощью setScale или может пакетно установить эти значения через setUser.

Третий модуль совсем примитивный preloader.js и целиком состоит из однотипных boolean-полей с false по дефолту. Пока его поле isGameLoaded единственное в состоянии модуля с геттером с false не получает true при запуске или перезагрузке приложения пользователь будет видеть лоадер. Каждое из остальных полей обозначает подгрузку определенного ассета: текстуры, модели, аудио или постройку определенного типа объектов.

Если нам нужно подгрузить, например, текстуру песка:

import * as Three from 'three';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  this.init = (    scope,    // ...  ) => {    const sandTexture = new Three.TextureLoader().load(      './images/textures/sand.jpg',      () => {        scope.render(); // нужно вызвать рендер если объекты использующию эту текстуру заметны "на первом экране"          loaderDispatchHelper(scope.$store, 'isSandLoaded');      },    );  };}export default Module;
// В @/utils/utilities.js:export const loaderDispatchHelper = (store, field) => {  store.dispatch('preloader/preloadOrBuilt', field).then(() => {    store.dispatch('preloader/isAllLoadedAndBuilt');  }).catch((error) => { console.log(error); });};

Когда отправка сообщения о том что элемент подгружен разрешается функция-помощник из набора атомов-утилит отправляет экшен проверяющий все ли готово?.

Согласен что решение по прелоадера не идеальное с точки зрения UI в том смысле что мы не демонстрируем общий прогресс по загрузке. Но на данном этапе это не кажется критически важным, особенно в свете озвученной выше концепции и даже факта быстрой перезагрузки локаций.

Аудиошина

Одна только необходимость воздействовать сразу на все звучащие аудио, например, при переходе в режим паузы или при включении машины времени диктует требование формирование общего микшера в системе аудиошины. Кроме того, такой подход максимально удобно унифицирует синтаксис однообразных похожих вызовов и избавляет от необходимости следить за очередностью подгрузки аудио на объекты (когда на одном объекте может звучать несколько) с помощью LoadingManager`ов.

Аудио бывают:

1) Звучащие на контроле-герое и PositionalAudio на объектах

2) Луп или сэмпл

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

В Hero удобно записывать аудио в переменную чтобы можно было просто работать [в обход шины] с ними в специфической логике:

// В @/components/Three/Scene/Hero.js:import * as Three from "three";import {  DESIGN,  // ...} from '@/utils/constants';import {  loaderDispatchHelper,  // ...} from '@/utils/utilities';function Hero() {  const audioLoader = new Three.AudioLoader();  let steps;  let speed;  // ...  this.init = (    scope,    // ...  ) => {    audioLoader.load('./audio/steps.mp3', (buffer) => {      steps = scope.audio.addAudioToHero(scope, buffer, 'steps', DESIGN.VOLUME.hero.step, false);      loaderDispatchHelper(scope.$store, 'isStepsLoaded');    });  };  this.setHidden = (scope, isHidden) => {    if (isHidden) {      // ...      steps.setPlaybackRate(0.5);    } else {      // ...      steps.setPlaybackRate(1);    }  };  this.setRun = (scope, isRun) => {    if (isRun && scope.keyStates['KeyW']) {      steps.setVolume(DESIGN.VOLUME.hero.run);      steps.setPlaybackRate(2);    } else {      steps.setVolume(DESIGN.VOLUME.hero.step);      steps.setPlaybackRate(1);    }  };  // ...  this.animate = (scope) => {    if (scope.playerOnFloor) {      if (!scope.isPause) {        // ...        // Steps sound        if (steps) {          if (scope.keyStates['KeyW']            || scope.keyStates['KeyS']            || scope.keyStates['KeyA']            || scope.keyStates['KeyD']) {            if (!steps.isPlaying) {              speed = scope.isHidden ? 0.5 : scope.isRun ? 2 : 1;              steps.setPlaybackRate(speed);              steps.play();            }          }        }      } else {        if (steps && steps.isPlaying) steps.pause();        // ...      }    }  };}export default Module;

Казалось бы шаги нужно делать лупом? Ан нет, не получится. И, например, вешать еще один последний шаг на окончание движения не позволит само устройство инерционного контрола собирающего много быстрых событий. С ним даже не особо ясно когда это вообще происходит когда позиция героя перестаёт изменяться нам уже не надо. Найденное мною решение простое и хорошо работает, дает нужный эффект. Сэмпл шага это ровно два шага. Если появляется событие клавиатуры запускаем аудио. И оно всегда отыгрывает до конца. Если персонаж переходит на бег или начинает ползти просто меняем скорость.

Тут надо упомянуть только об одном обнаруженном мною нюансе. Если аудио не луп с ним возникают проблемы при постановке и снятии с паузы. При следующих запусках отыгранная в предыдущий раз часть трека съедается пока он вообще не перестает звучать. Фиксит это данная строчка в функции добавления аудио на шину:

if (!isLoop) audio.onEnded = () => audio.stop();

Имейте ввиду!

import * as Three from "three";import { DESIGN, OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Module() {  const audioLoader = new Three.AudioLoader();  // ...  let material = null;  const geometry = new Three.SphereBufferGeometry(0.5, 8, 8);  let explosion;  let explosionClone;  let boom;  this.init = (    scope,    fireMaterial,    // ...  ) => {    // Звук наземных врагов - загружаем в инициализации на объекты через шину    audioLoader.load('./audio/mechanism.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isMechanismLoaded');      scope.array = scope.enemies.filter(enemy => enemy.name !== OBJECTS.DRONES.name);      scope.audio.addAudioToObjects(scope, scope.array, buffer, 'mesh', 'mechanism', DESIGN.VOLUME.mechanism, true);     });    // Звук взрыва - то есть - "добавляемой и уничтожаемой" сущности - загружаем и записываем в переменную    material = fireMaterial;    explosion = new Three.Mesh(geometry, material);    audioLoader.load('./audio/explosion.mp3', (buffer) => {      loaderDispatchHelper(scope.$store, 'isExplosionLoaded');      boom = buffer;    });  };  // ...  // ... где-то в логике врагов:  this.moduleFunction = (scope, enemy) => {    scope.audio.startObjectSound(enemy.id, 'mechanism');    // ...    scope.audio.stopObjectSound(enemy.id, 'mechanism');    // ...  };  // При добавлении взрыва на шину взрывов:  this.addExplosionToBus = (    scope,    // ...  ) => {    explosionClone = explosion.clone();    // ..    scope.audio.playAudioOnObject(scope, explosionClone, boom, 'boom', DESIGN.VOLUME.explosion);    // ..  };}export default Module;

Попробуйте подобрать и принять зеленый цветок запускающий машину времени в игре, классно? ))

Шина событий и сообщения

Очевидно что кроме аудиомикшера игре необходима еще одна одна шина: делей для событий модуль который будет задерживать отправку изменений в контексте игрового времени. Например, если было показано сообщение, а потом система поставлена на паузу мы не можем использовать нигде обычные таймауты только обновляемые в анимационном цикле часы Clock Three. Модуль хранит актуальные записи о связанных событиях и в нужный момент вызывает переданный коллбэк.

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

Мир

Модель первой локацииМодель первой локации

В инициализации модуля мира по порядку:

  1. Загружаются все переиспользуемые в остальных модулях текстуры и создаются все такие материалы и геометрии.

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

  3. На примитивы которые должны стать видимыми элементами, строительными блоками мира накладываются текстуры. Места-пустышки обозначающие места рождения врагов или нахождения полезных предметов после учета удаляются.

  4. Двери переносятся в отдельную группу из основной модели. Из этих двух групп очищенной сцены и все дверей создаются стартовые октодеревья.

  5. Инициализируются все остальные модули.

Я разбираю один файл glb и как совершенно необходимый такой игре редактор уровней и как готовую модель для построения стартовых октодеревьев мира, и, отдельно дверей в нем, и как почти готовую не текстурированную основу самого примитивного визуального мира. Различать примитивы можно с помощью специфических маркеров в их наименовании. Это не самое надежное соглашение, оно чревато ошибками, но они легко обнаруживаются визуально при ручном тестировании. Изменения можно вносить очень быстро. Тут уже все зависит от вашей фантазии и выдуманного с помощью нее дизайна и геймплея, ну и количества времени которые вы можете на это потратить. Например, я использую маркер Mandatory если хочу чтобы цветок или бутылка были обязательными, если его нет постройка зависит от рандома. Или для механики включения-выключения информационных панелей собирается отдельный массив с их комнатами параллелепипедами определяющими объем в котором панель реагирует на персонажа. Для геометрии такого объекта следует сделать при инициализации:

room.geometry.computeBoundingBox();

room.visible = false;

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

// В @/components/Three/Scene/World/Screens.js:this.isHeroInRoomWithScreen = (scope, screen) => {scope.box.copy(screen.room.geometry.boundingBox).applyMatrix4(screen.room.matrixWorld); if (scope.box.containsPoint(scope.camera.position)) return true;return false;};

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

Псевдообъект-помощник для двериПсевдообъект-помощник для двериДверь не закрываетсяДверь не закрывается

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

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

Кастинг

Вот мы и добрались до самого интересного: кастинг и столкновения. Как сделать так, чтобы предметы можно было собирать, а герой и враги не сталкивались с миром и друг-другом. Для обоих механик я использую дополнительные невидимые примитивы, псевдоmesh`и. Они инициализируются и записываются в абстрактные объекты которыми оперирует система вместе с основным видимым и всеми необходимыми им флагами-свойствами. Для движущихся врагов еще записывается коллайдер Sphere. Псевдомеши идут на кастинг (предметы) или построение и обновление октодеревьев (враги). А коллайдер для проверки столкновения с ними.

Псевдообъекты-помощники для предметовПсевдообъекты-помощники для предметов

Геометрия и материал готовиться в мире перед инициализацией всех вещей и надежнее сделать материал двусторонними так кастинг будет работать даже если герой оказался внутри псевдообъекта:

// В @/components/Three/Scene/World.js:const pseudoGeometry = new Three.SphereBufferGeometry(DESIGN.HERO.HEIGHT / 2,  4, 4); const pseudoMaterial = new Three.MeshStandardMaterial({ color: DESIGN.COLORS.white, side: Three.DoubleSide,});new Bottles().init(scope, pseudoGeometry, pseudoMaterial);

В модуле конкретной вещи:

// В @/components/Three/Scene/World/Thing.js:import * as Three from 'three';import { GLTFLoader } from '@/components/Three/Modules/Utils/GLTFLoader';import { OBJECTS } from '@/utils/constants';import { loaderDispatchHelper } from '@/utils/utilities';function Thing() {  let thingClone;  let thingGroup;  let thingPseudo;  let thingPseudoClone;  this.init = (    scope,    pseudoGeometry,    pseudoMaterial,  ) => {    thingPseudo = new Three.Mesh(pseudoGeometry, pseudoMaterial);    new GLTFLoader().load(      './images/models/Objects/Thing.glb',      (thing) => {        loaderDispatchHelper(scope.$store, 'isThingLoaded'); // загружена модель        for (let i = 0; i < OBJECTS.THINGS[scope.l].data.length; i++) {          // eslint-disable-next-line no-loop-func          thing.scene.traverse((child) => {            // ... - тут "покраска" материалами частей вещи          });          // Клонируем объект и псевдо          thingClone = thing.scene.clone();          thingPseudoClone = thingPseudo.clone();          // Псевдо нужно дать правильное имя чтобы мы могли различать его при кастинге          thingPseudoClone.name = OBJECTS.THINGS.name;          thingPseudoClone.position.y += 1.5; // корректируем немного позицию по высоте          thingPseudoClone.visible = false; // выключаем рендер          thingPseudoClone.updateMatrix(); // обновляем          thingPseudoClone.matrixAutoUpdate = false; // запрещаем автообновление          // Делаем из обхекта и псевдо удобную группу          thingGroup = new Three.Group();          thingGroup.add(thingClone);          thingGroup.add(thingPseudoClone);          // Выставляем координаты из собранных из модели уровня данных          thingGroup.position.set(            OBJECTS.THINGS[scope.l].data[i].x,            OBJECTS.THINGS[scope.l].data[i].y,            OBJECTS.THINGS[scope.l].data[i].z,          );          // Записываем в "рабочие объеты" - по ним будем кастить и прочее          scope.things.push({            id: thingPseudoClone.id,            group: thingGroup,          });          scope.objects.push(thingPseudoClone);          scope.scene.add(thingGroup); // добавляем на сцену        }        loaderDispatchHelper(scope.$store, 'isThingsBuilt'); // построено      },    );  };}export default Thing;

Теперь мы можем тыкать направленным вперед лучом из героя в анимационном цикле Hero.js:

// В @/components/Three/Scene/Hero.js:import { DESIGN, OBJECTS } from '@/utils/constants';function Hero() {  // ...  this.animate = (scope) => {    // ...    // Raycasting    // Forward ray    scope.direction = scope.camera.getWorldDirection(scope.direction);    scope.raycaster.set(scope.camera.getWorldPosition(scope.position), scope.direction);    scope.intersections = scope.raycaster.intersectObjects(scope.objects);    scope.onForward = scope.intersections.length > 0 ? scope.intersections[0].distance < DESIGN.HERO.CAST : false;    if (scope.onForward) {      scope.object = scope.intersections[0].object;      // Кастим предмет THINGS      if (scope.object.name.includes(OBJECTS.THINGS.name)) {        // ...      }    }    // ...  };}export default Hero;

Кастинг очень полезен и для усовершенствования ИИ врагов. С помощью него возможно проверять имеет ли смысл, есть ли возможность двигаться-прыгать вперед, лететь вниз, делать выстрел. В утилитах:

// В @/utils/utilities.js:// let arrowHelper;const fixNot = (value) => { if (!value) return Number.MAX_SAFE_INTEGER; return value;};export const isEnemyCanMoveForward = (scope, enemy) => { scope.ray = new Three.Ray(enemy.collider.center, enemy.mesh.getWorldDirection(scope.direction).normalize()); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); scope.resultEnemies = scope.octreeEnemies.rayIntersect(scope.ray); // arrowHelper = new Three.ArrowHelper(scope.direction, enemy.collider.center, 6, 0xffffff); // scope.scene.add(arrowHelper); if (scope.result || scope.resultDoors || scope.resultEnemies) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance), fixNot(scope.resultEnemies.distance));   return scope.number > 6; } return true;};

Для наглядной визуальной отладки подобных механик очень полезен объект Three ArrowHelper. Если мы включим его добавление на сцену в функции выше:

Отладка с включенными стрелочными помощникамиОтладка с включенными стрелочными помощниками

С помощью подобной простой функции можно добиться того что враги не реагируют на героя через стены строим луч от центра коллайдера к камере и сравниваем дистанцию от камеры до обнаруженного столкновения:

// В @/utils/utilities.js:export const isToHeroRayIntersectWorld = (scope, collider) => { scope.direction.subVectors(collider.center, scope.camera.position).negate().normalize(); scope.ray = new Three.Ray(collider.center, scope.direction); scope.result = scope.octree.rayIntersect(scope.ray); scope.resultDoors = scope.octreeDoors.rayIntersect(scope.ray); if (scope.result || scope.resultDoors) {   scope.number = Math.min(fixNot(scope.result.distance), fixNot(scope.resultDoors.distance));   scope.dictance = scope.camera.position.distanceTo(collider.center);   return scope.number < scope.dictance; } return false;};

Враги

Кроме инициализации самих низовых модулей врагов, модуль Enemies.js содержит и всю общую логику обслуживающую жизненный цикл врагов. Сейчас у меня четыре этапа-режима жизни у врагов:

// В @/utils/constatnts.js:export const DESIGN = {  DIFFICULTY: {    civil: 'civil',    anarchist: 'anarchist',    communist: 'communist',  },  ENEMIES: {    mode: {      idle: 'idle',      active: 'active',      dies: 'dies',      dead: 'dead',    },    spider: {      // ...      decision: {        enjoy: 60,        rotate: 25,        shot: {          civil: 40,          anarchist: 30,          communist: 25,        },        jump: 50,        speed: 20,        bend: 30,      },    },    drone: {      // ...      decision: {        enjoy: 50,        rotate: 25,        shot: {          civil: 50,          anarchist: 40,          communist: 30,        },        fly: 40,        speed: 20,        bend: 25,      },    },  },  // ...};
// В @/components/Three/Scene/Enemies.js:import { DESIGN } from '@/utils/constants';import {  randomInteger,  isEnemyCanShot,  // ...} from "@/utils/utilities";function Enemies() {  // ...  const idle = (scope, enemy) => {    // ...  };  const active = (scope, enemy) => {    // ...    // Где-то в логике агрессивного режима: решение на выстрел (если отдыхает)    scope.decision = randomInteger(1, DESIGN.ENEMIES[enemy.name].decision.shot[scope.difficulty]) === 1;    if (scope.decision) {      if (isEnemyCanShot(scope, enemy)) {        scope.boolean = enemy.name === OBJECTS.DRONES.name;        scope.world.shots.addShotToBus(scope, enemy.mesh.position, scope.direction, scope.boolean);        scope.audio.replayObjectSound(enemy.id, 'shot');      }    }  };  const gravity = (scope, enemy) => {    // ...  };  this.animate = (scope) => {    scope.enemies.filter(enemy => enemy.mode !== DESIGN.ENEMIES.mode.dead).forEach((enemy) => {      switch (enemy.mode) {        case DESIGN.ENEMIES.mode.idle:          idle(scope, enemy);          break;        case DESIGN.ENEMIES.mode.active:          active(scope, enemy);          break;        case DESIGN.ENEMIES.mode.dies:          gravity(scope, enemy);          break;      }    });  };}export default Enemies;

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

Но! Самое важное на что нужно обратить внимание: в idle спокойном режиме полноценно двигается некоторое случайное время только один выбранный случайным образом враг. Остальные поворачиваются на месте + может и должна быть запущена анимация. Такая оптимизация позволяет действительно полноценно разгрузить систему.

Столкновения

Октодеревом в данном тексте обозначается максимально упрощенная модель 3D-пространства которое занимает некоторая группа объектов с минимально необходимым и достаточным для обсчета количеством граней, рёбер и вершин.

Такие октодеревья, как вы уже наверняка поняли из примера полезной функции из конца раздела про кастинг помогают нам грамотно обсчитывать кастинг лучей или столкновения коллайдеров героя/врагов с миром и другими объектами. В случае если персонаж движется и мы обнаруживаем столкновение октодерева с коллайдером капсулой героя или сферой врага вектору ускорения этого объекта добавляется вектор выталкивающий обратно центр его коллайдера (и все остальное, соответственно).

В текущей реализации используются три октодерева: мир: 1) пол, бетонные блоки, трубы, стекла, а также 2) двери и 3) враги. Каждый из врагов обсчитывает свои столкновения с персональным октодеревом врагов собранным без него.

Мы можем и должны обновлять октодеревья. Если дверь была открыта-закрыта или враг передвинулся нам нужно пересобрать соответствующее октодерево. При открытии закрытии двери лучше всего сделать это два раза: когда дверь открылась или закрылась настолько что герой уже может или наоборот больше не может пройти через нее и когда процесс открытия/закрытия завершен.

В моей реализации на объекты врагов инициализируется по две псевдо-коробки. Это не оптимально, но сделано в целях балансировки визуальной составляющих двух механик: 1) столкновение героя с врагами 2) столкновения врагов с миром и между собой. Визуальное тестирование показало что для реализации первой механики нужна коробка намного более меньшего размера чем для второй.

Точность обсчета столкновений с октодеревьями напрямую зависит от качества оптимизации анимационного цикла и ее непосредственного результата фактической производительности системы. Если скорость объекта высокая, но система начинает захлебываться, то есть фрейм, дельта анимационного цикла удлиняется обсчет с октодеревом не успеет компенсировать или даже вообще зарегистрировать столкновение и пользователь может отхватить глюк: герой или враг может быть вытолкнут через стену-стекло. Но не будем о грустном. )

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

// В @/utils/constatnts.js:export const DESIGN = {  OCTREE_UPDATE_TIMEOUT: 0.5,  // ...};
// В @/utils/utilities.js:// Обновить персональное октодерево врагов для одного врагаimport * as Three from "three";import { Octree } from "../components/Three/Modules/Math/Octree";export const updateEnemiesPersonalOctree = (scope, id) => {  scope.group = new Three.Group();  scope.enemies.filter(obj => obj.id !== id).forEach((enemy) => {    scope.group.add(enemy.pseudoLarge);  });  scope.octreeEnemies = new Octree();  scope.octreeEnemies.fromGraphNode(scope.group);  scope.scene.add(scope.group);};
// Столкновения враговconst enemyCollitions = (scope, enemy) => {  // Столкновения c миром - полом, стенами, стеклами и трубами  scope.result = scope.octree.sphereIntersect(enemy.collider);  enemy.isOnFloor = false;  if (scope.result) {    enemy.isOnFloor = scope.result.normal.y > 0;    // На полу?    if (!enemy.isOnFloor) {      enemy.velocity.addScaledVector(scope.result.normal, -scope.result.normal.dot(enemy.velocity));    } else {      // Подбитый враг становится совсем мертвым после падения на пол и тд      // ...    }    enemy.collider.translate(scope.result.normal.multiplyScalar(scope.result.depth));  }  // Столкновения c дверями  scope.resultDoors = scope.octreeDoors.sphereIntersect(enemy.collider);  if (scope.resultDoors) {    enemy.collider.translate(scope.resultDoors.normal.multiplyScalar(scope.resultDoors.depth));  }  // Делаем октодерево из всех врагов без этого, если давно не делали  if (scope.enemies.length > 1    && !enemy.updateClock.running) {    if (!enemy.updateClock.running) enemy.updateClock.start();    updateEnemiesPersonalOctree(scope, enemy.id);    scope.resultEnemies = scope.octreeEnemies.sphereIntersect(enemy.collider);    if (scope.resultEnemies) {      result = scope.resultEnemies.normal.multiplyScalar(scope.resultEnemies.depth);      result.y = 0;      enemy.collider.translate(result);    }  }  if (enemy.updateClock.running) {    enemy.updateTime += enemy.updateClock.getDelta();    if (enemy.updateTime > DESIGN.OCTREE_UPDATE_TIMEOUT && enemy.updateClock.running) {      enemy.updateClock.stop();      enemy.updateTime = 0;    }  }};

Своя атмосфера

Модуль с романтическим названием Atmosphere.js отвечает за элементы мира которые одинаковы для всех локаций: свет, небо, и в дальнейшем взаимодействие между другими модулями в анимационном цикле работает посредником.

Если вывалится за стену и забежать за край небаЕсли вывалится за стену и забежать за край неба

Я долго придумывал и придумал как сделать прикольное небо, облака: это огромная вывернутая по одной из осей и вращающаяся текстурированная сфера.

При некотором изменении позиции героя (сейчас это 10 метров) модуль проверяет всех живых врагов и панели и меняет их режимы в зависимости от актуальной ситуации. Сейчас дроны не видят героя через толстые пуленепробиваемые стекла. Для того чтобы это изменить нужно выделить стекла в отдельное октодерево, как и двери.

Пуленепробиваемые стеклаПуленепробиваемые стекла

Да, это вам не React c TS и тестами в финтех и банки!

Выводы которые я могу сделать на основе практики создания браузерной FPS на Three:

  • Мы не можем использовать тени и множество источников света

  • Мы должны экономить память в анимационном цикле и использовать в нем только готовые переменные

  • Мы должны и во всех остальных возможных аспектах максимально тщательно оптимизировать анимационный цикл, кастинг и обсчет столкновений в нем в контексте геймплея, так, чтобы сохранить драйв, но избежать падения производительности

  • Статическая типизация и юнит-тесты ничем не могут помочь в данном эксперименте

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

Подробнее..

Blender для (геофизического) моделирования и визуализации

14.08.2020 12:15:32 | Автор: admin

Недавно мы обсудили, как дополнить ранее построенную в ParaView геологическую модель вулкана Тамбора, Индонезия с помощью симуляции потоков дыма, воды, лавы, в MantaFlow и визуализировать результаты в ParaView: Гидродинамическое моделирование (CFD) на рельефе с помощью MantaFlow и визуализация результатов в ParaView Сегодня мы посмотрим, что получится, если использовать "настоящий" софт для работы с трехмерной компьютерной графикой Blender. В последние его версии встроен тот же самый "движок" физически корректной гидродинамической симуляции MantaFlow, это многое упрощает. Как обычно, инструкции по преобразованию данных, исходные данные и проект Blender смотрите в моем GitHub репозитории: ParaView-Blender.


Tambora Volcano Plume Simulation


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


Введение


В предыдущей статье изложено подробно, почему так важна физически корректная симуляция геологических процессов в дополнение к статичным геологическим моделям. Но стоит ли тратить время на достаточно сложную программу Blender и перенос данных в нее, тем более, что у нас уже есть готовые модели в ParaView? Лучше всего, разумеется, взять и попробовать но это трудоемко, так что должны быть какие-то веские причины потратить на это время. В связи с этим, уместно вспомнить, как примерно 20 лет назад на кафедре в университете мы сделали некоторые симуляции лабораторных работ по физике в проприетарном Autodesk 3ds Max. Само собой, полученные результаты были далеки от профессионально сделанных моделей из комплекта примеров 3ds Max, ведь YouTube с современным изобилием обучающих роликов тогда не существовал. И все же возможность многократно повторить симуляцию, при этом ускорить ее или замедлить, одновременно увидеть несколько разных представлений происходящих процессов и другие возможности позволяли по-новому взглянуть и понять физические явления. Да, на реальном оборудовании можно научиться работать с оборудованием, но именно компьютерная симуляция лучше для изучения физических процессов. Возвращаясь к геофизическим моделям, это тем более верно, поскольку происходящее в недрах (вулкана) нам в принципе недоступно для прямого наблюдения. Также, сегодня нам доступны Open Source программные продукты, в том числе Blender, и более того, со встроенным физически корректным симулятором MantaFlow, которым я и так давно пользуюсь. А еще, в Blender теперь есть новый "движок" рендеринга Eevee на порядок быстрее предыдущего (Cycles). Кажется, пора пробовать!


Сразу скажу, что Blender я увидел несколько дней назад и вообще эта тема мне мало знакома. Так что, фактически, мы будем сравнивать результаты визуализации в ParaView, которым я ежедневно пользуюсь лет так пять уже, и результаты в Blender, с которым я знаком примерно пять дней. При этом, стабильная версия Blender не делает то, что мне нужно (и регулярно крэшится), а тестовая весьма нестабильна (ладно, крэшится, так еще при попытке обновиться на новый билд через пару дней вообще все симуляции перестали работать). Погуглив и пообщавшись с опытными пользователями, причина стала понятна мало кто работает со столь сложными исходными данными, а симуляция наливания вина из бутылки в винный бокал работает отлично :) И все же, мне удалось сделать задуманное, а баги, надеюсь, исправят со временем (пытаюсь слать баг-репорты, в ответ мне рекомендуют обходные пути тоже неплохо).


Подготовка данных для Blender


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


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


Визуализация в Blender


Все подробности можно посмотреть в приведенном в репозитории файле проекта Blender отмечу лишь, что мне показалось очень удобной система задания облета камеры вокруг сцены. В ParaView это, мягко говоря, совсем не так очевидно. Анимации из 180 кадров хватило для создания плавного поворота на 90 градусов, цвет фона выбран черным для упрощения визуализации модели и пламени (серый фон мне не понравился, а на белом плохо видно).


Поскольку более высокая детальность симуляции требует и значительно больших вычислительных ресурсов, исходная модель магматических камер упрощена так, чтобы с ней можно было работать при разрешении области симуляции 60 ячеек по одной координате. Да, половинный и удвоенный масштаб я тоже проверял, чтобы убедиться в устойчивости результатов. Вертикальный масштаб рельефа удвоен для наглядности, а сам рельеф сглажен (непосредственно в MantaFlow с этим нет проблем, а вот в Blender требуется гладкая поверхность для моделирования потоков на ней).


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



Заключение


Полученная симуляция оказывается достаточно познавательна, на мой взгляд, если сравнить исходную геологическую модель с анимированной магмой, заполнение магматических камер происходит в согласии с не анимированными магматическими каналами модели (показаны красно-коричневыми на модели, см. первый кадр видео). То есть даже небольшого выбранного разрешения для симуляции оказывается достаточно, чтобы показать вполне согласованную динамическую модель. Кроме того, если продолжать симуляцию (в проекте до 250 кадров настроено, но на видео показаны только 180 кадров), обнаруживается очень интересный эффект, который на самом деле может иметь место при извержении магма на видео поднимается до поверхности и опускается вниз, а далее (не показано) она снова поднимается и снова опускается! И это при том, что положение источника магмы и параметры симуляции не меняются. Разумеется, этот же эффект сохраняется и при удвоении разрешения симуляции если нужна большая детализация. С геологами я подробно еще не обсуждал результаты, вероятно, они еще что интересное заметят.


Из курьезов за время карантина у меня "сдохла" батарея в макбуке 15" и я благополучно пересел на старенький макбук 13", которой вполне мне хватает для ежедневного консольного программирования на C, Python, SQL и запуска ssh на амазоновские инстансы EC2 для вычислений. В общем, в сервис я так и не добрался. Так вот, для комфортной работы в Blender этого явно мало и размер экрана и вычислительная мощность недостаточны. При этом, я использовал только рендеринг на движке Eevee, потому что при включении Cycles рендеринг даже одного кадра становится чрезвычайно длительным процессом. Зато теперь я могу уверенно сказать, что на любом более-менее приличном современном ноутбуке можно отлично работать в Blender.

Подробнее..

Геология XXI века от реальности к виртуальности

15.03.2021 12:08:42 | Автор: admin

Ранее в статьях мы уже обсудили доступные данные (результаты наземных и спутниковых гравитационных и магнитных измерений, ортофото и космические снимки, цифровые модели рельефа), теоретические подходы и методы обработки (интерферометрия, построение обратных геофизических моделей), обработку данных в ParaView (выделение изоповерхностей) и Blender (высококачественная визуализация и анимация подготовленных в ParaView данных) и даже посмотрели Python Jupyter notebook с вычислениями и визуализацией моделей (включая выделение изоповерхностей средствами библиотеки VTK). Осталось построенные геотермальные изоповерхности конвертировать в формат модели дополненной реальности и получить геотермальную модель в дополненной реальности(AR). Эта модель может быть легко просмотрена прямо из браузера на iOS/iPadOS или из загруженного файла на MacOS (поддержка AR добавлена два-три года назад, на старых устройствах для этого потребуется обновиться). Увы, стандартов много и удастся ли открыть модель на Windows или Android, я не знаю (напишите в комментариях, если можно добавить поддержку нужного стандарта каким-то софтом). Как всегда, исходная модель доступна на GitHub в репозитории ParaView-Blender в виде исходных STL/PLY файлов и проекта Blender.



AR Модель геотермального резервуара Лахендонг, полуостров Минахаса, Северный Сулавеси, Индонезия Замеры температуры по скважинам обозначены цветными дисками синим 0-150C (далеко от резервуара), белым 150-250C (переходная область вблизи от резервуара), красным 250-350C (внутри геотермального резервуара).


Вместо введения, или зачем все это нужно


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


К примеру, мы не можем пробурить скважину до центра Земли и собрать образцы, поэтому приходится придумывать другие способы исследования скажем, регистрировать волны мощных землетрясений и прочих геологических событий в разных местах планеты и сопоставлять, пытаясь определить, прошла ли волна напрямую (по хорде) через толщу земли или отклонилась, как волна замедлилась и так далее. В результате, когда была пробурена Кольская сверхглубокая скважина, вся наша устоявшаяся геологическая картина оказалась ошибочной неведомым образом температуры в глубине вдвое выше ожидаемых (к потоку тепла из глубинных слоев еще столько же добавляется в результате нагрева от радиоктивного распада), породы не плотные, а очень трещиноватые и заполненные водой (притом, на поверхности начали бурить сплошной и прочный Балтийский щит возрастом миллиарды лет), отсутствует ожидаемый переход между гранитами и базальтами на глубине около 7км (этот переход отлично виден на сейсмограммах и не было сомнений в его существовании), При этом, образцы пород (керн) удавалось получить в виде грязи или пыли, в зависимости от влагонасыщенности пород (то есть даже простое, на первый взгляд, измерение плотности пород задача весьма непростая). Почему я говорю именно про эту скважину? Потому, что это проект мирового масштаба и призванный (в свое время) показать всю силу советской науки, то есть выполнялся на "высшем уровне", в то время как результаты коммерческого бурения очень далеки от него по качеству сбора образцов и их анализа, и все подробности и неточности сбора и обработки данных не регистрируются.


Про фальсификации это отдельная увлекательная история. Помнится, я очень удивился, когда узнал о существовании геологов-детективов, которые ищут полезные ископаемые в архивах. В частности, в США по закону необходимо сдавать геологические отчеты в правительственный архив, и государство очень не любит там фальсификаций, хотя просто ошибки, конечно, простительны. Так вот, не редки случаи, когда при бурении, скажем, на нефть, вопреки всем ожиданиям, скважина оказывается сухой. В этом случае отчет сдается в архив и геологи выбирают место для новой скважины. Но далеко не всегда такая скважина действительно сухая! Если геолог или буровики допустили ошибку и пробили нефтеносный горизонт, они могут отчитаться о пустой скважине и избежать обвинений и ответственности за потерю скважины. В таком случае, скважина попадает в общедоступную базу данных как сухая и рядом обычно скважины никто не бурит. А вот в сданных в архив геологических отчетах можно найти правду или ее часть и восстановить истинную ситуацию, а потенциал погубленной скважины может оказаться превосходным. Такие находки потом продаются нефтедобытчикам нужно лишь пробурить новую скважину с практически гарантированным результатом, притом, что земля с пустой скважиной стоит дешевле, чем с действующими или поблизости от них.


Еще есть совпадения и ошибки интерпретации. Поскольку геологоразведка состоит из многих этапов (и никогда не заканчивается), зачастую интерпретацию данных каждого этапа используют на последующих без пересмотра и анализа всех собранных данных совместно. Рассмотрим на примере нефтяного участка в России, уже зарегистрированного с отличным ресурсным потенциалом (извлекаемыми запасами нефти). Дело было примерно так: разведка, как полагается, началась весной, пара пробуренных скважин оказались рабочими и показали отличное давление нефтяного пласта. Геологи проанализировали данные сейсмики и бурения и нарисовали модель мощной нефтяной линзы, посчитали извлекаемый объем (много!), с этими результатами зарегистрировали месторождение и участок был продан компании, занимающейся добычей нефти. Но вскоре что-то пошло не так и давление в скважинах упало. Не беда, главное, это отличное новое месторождение. У скважин попробовали менять режим работы, возможно, попробовали еще немного пробурить и так далее и на следующую весну они снова фонтанировали нефтью. Все бы хорошо, но через пару месяцев давление снова упало Проверка всех данных показала, что найденный "нефтяной пласт" не был проанализирован и оказался водным (что, кстати, подсказывал рельеф местности и водотоки), а нефть (более легкая, чем вода) находилась лишь в тонкой прослойке, и мало того, хорошее давление в пласте создавалось лишь весной за счет грунтовых вод (достаточно сравнить подъем поверхности по данным спутниковой интерферометрии, совпадающий с периодами высокого давления в скважинах). Почему не пробурили саму потенциальную нефтяную линзу для проверки? Все просто ее центр находится за пределами данного участка и принадлежит другому владельцу, так что там бурить не было возможности. Что ж, бывает, именно поэтому многие компании, занимающиеся разведкой полезных ископаемых, не занимаются их промышленной добычей.


Хотел еще про золото на примерах рассказать, но как-то много получается, так что как-нибудь в другой раз. Вкратце для поиска золота (и не только для него) может потребоваться восстанавливать древний рельеф (палеорельеф) и водотоки, изучать пути переноса и тогда, может быть, удастся построить карту расположения как россыпного, так и коренного (рудного) золота. Первое намного легче найти из названия понятно, что оно как бы рассыпано на поверхности или под ней, образуется при разрушении (выветривании, размытии) выходов коренных месторождений на поверхность и постепенно вымываются оттуда (древние водотоки, помните) и накапливаются где-то в низинах (вот почему важен палеорельеф). Второе найти намного сложнее, так как выходы на поверхность могут быть точечными и неведомо где расположенными вдали от россыпного золота, зато и золота там больше. Для тех, кто не хочет рисковать, но хочет манить есть довольно беспроигрышный вариант с повторной добычей на выбранных в прошлые века россыпных месторождениях, так как с современной техникой там еще можно добыть золота а в свободное время погулять вокруг, присмотреться, можно и коренное найти (примерно этим там геолог и занимается в свободное от текущих задач время, конечно, ходит не просто так, но походить придется). При этом приходится сталкиваться с так называемыми ураганными пробами (специалистам по данным известны как outliers), которые правдивы технически (в самом деле, в отдельной низинке может лежать горсть самородков) но совершенно не информативны в целом (вся территория гарантированно не будет покрыта слоем самородков!). А еще геолог, как правило, вовсе не стремится делать бесполезную работу и будет брать пробы там, где, по его мнению, есть надежда найти золото и игнорировать всю ту территорию, которую считает бесперспективной таким образом, подавляющая часть территории окажется вовсе не проанализированной, так что выборка получается очень смещенной.


Наконец, очень много допускается просто ошибок при конвертировании данных из разных систем координат, при копировании между разными документами и, особенно, при оцифровке бумажных отчетов (в том числе потому, что там часто просто не удается разобрать написанного, а перенести надо всю информацию, хоть как-нибудь и без отметок о достоверности полученного пересказа). Классическая проблема нулевые координаты скважин (0,0), географически это дно океана около Африки и совпадающие ширина и долгота (зная координаты участка, легко определить, что именно напутано, вопрос лишь, где взять недостающую координату).


Таким образом, из вышесказаного становится ясно, что никаким данным априори доверять нельзя, а нужно все проверять и сопоставлять, а при получении геологического результата снова перепроверять те данные, которые к нему привели. Именно поэтому нейросети и прочие недетерминированные методы применяются, в основном, для прикидки как правило, это далеко не лучшая идея засунуть в "черный ящик" мусорные данные ("мусор на входе мусор на выходе"). Спасают во всем этом калейдоскопе методы анализа и очистки данных (data science) и качественная визуализация, которой никогда не бывает много -на ней геолог видит намного больше, чем специалист по данным, который эту визуализацию и сделал. Вряд ли нам удастся выбрать идеальный ракурс для статичной модели с точки зрения геолога (притом, как вы уже догадались, три геолога попросят три разных ракурса). Возможности дополненой или виртуальной реальности позволяют легко и быстро масштабировать и перемещать модель, просматривать с любый ракурсов и делать скриншоты для их сверки с данными получается виртуальное геологическое исследование. Все это в общем-то понятно и очевидно, а действительно актуальным стало лишь с появлением широко распространенных стандартов, поддерживаемых во множестве смартфонов, которые сегодня есть почти у всех.


Технические подробности


Как говорится, все развивается по спирали, и с технологией виртуальной реальности с моделями формата VRML я столкнулся полтора десятка лет назад. Увы, это было практически бесполезно, поскольку ресурсов обычного десктопного компьютера не хватало на комфортную работу с моделью, а через несколько минут вынужденно медленного просмотра приходилось перезапускать программу просмотра, поскольку она выходила за пределы доступной оперативной памяти и дальнейшая работа становилась невозможной. Это уж не говоря о необходимости ставить какой-то сомнительный софт для просмотра таких моделей, причем программ просмотрщиков было много и они были несовместимы между собой Сегодня многое изменилось каждый более-менее современный MacOS компьютер и iOS/iPadOS устройство имеют поддержку просмотра моделей дополненной реальности, причем это очень удобно делать именно с мобильного устройства, которое при этом даже не грееется на ощупь и позволяет очень плавно и со всех сторон осматривать модель и с ней взаимодействовать (масштабировать, перемещать, вращать). Также возможно добавлять в модели различные триггеры событий и ссылки на веб-адреса, делать анимации и так далее. Все это и послужило причиной, почему я, после обновения на MacOS Catalina (мне и с предыдущей хорошо было, так что обычно я жду года после выхода, прежде чем обновляться на новую, уже стабильную систему) решил попробовать сделать такую модель в дополненной реальности. Чтобы упростить себе задачу, начал со статической модели, вдобавок, которая у меня уже готова в виде проекта Blender.


Apple предлагает набор средств разработки AR Creation Tools, из которых мне пока потребовался только консольный Python модуль USDZ Tools, а рекомендуемый Reality Composer потребовал установки среды разработки XCode (внимание: сразу после инсталляции занимает 30ГБ места на диске) и еще не пригодился. Отдельно устанавливаемый Reality Convertor умеет шуметь вентилятором и делает то же самое, что и Python модуль, а еще в нем можно красивый скриншот модели сделать (смотрите картинку в начале статьи).


Смотрите на GitHub модель и инструкции для переноса данных из ParaView в Blender и в AR модель (или сделанных с помощью библиотеки VTK в Jupyter notebook, как описано в моей предыдущей статье): ParaView-Blender В поддиректории "export" кратко приведены подробности переноса и подготовки данных. Сами исходные данные модели и проект Blender доступны в поддиректории "Minahasa". Там же доступны "сырые" данные в виде скриптов Google Earth Engine (GEE), GeoJSON, TIFF, NetCDF файлов обратной геофизической модели для исходного проекта ParaView. Вот как выглядит нужный нам резервуар:



Рендеринг из проекта Blender на GitHub для AR упростим проект


Формат экспорта gITF 2.0 позволяет разом перенести множество объектов из Blender, команда конвертации приведена на гитхабе по ссылке выше. При этом, необходимые текстуры требуется предварительно подготавливать и сохранять в отдельные файлы с помощью их "прожига" (Bake). Для переноса в формат модели AR пришлось обойтись диффузным шейдером и "прожигом" (Bake) только диффузного света согласно цветам узлов модели как показано ниже:



Проект Blender с готовой для экспорта в AR моделью за основу взят проект из репозитория ParaView-Blender


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


Заключение


Получилось много текста, и даже без формул, что для меня несколько странно. В связи с приближением полевого сезона, коллега геолог занят золотыми месторождениями в Сибири и мой текст еще не проверял, поэтому возможны ошибки в геологических терминах, буду благодарен читателям за сообщения об этом. Стоит ли продолжать тематику и о чем рассказать? Мне кажется, мы уже кратко рассмотрели полный цикл работы от подготовки данных и теории до программирования и визуализации моделей, теперь можно о чем-то подробнее поговорить, если будет интерес. Может быть, вы хотите видеть больше готовых моделей? Часть наших моделей уже доступны на YouTube канале, возможно, дальше будем параллельно выкладывать AR модели. Конечно, рабочих участков здесь нет, все эти модели сделаны для апробации технологии и территорий хоть это и трудоемко, но стоит того, на наш взгляд. Подробнее и чаще мы публикуемся на LinkedIn для открытого обсуждения там можно модель по любой территории обсудить с геологами, работавшими в данной местности, попросить у сообщества геологические карты или другие данные и так далее.

Подробнее..

Недельный геймдев 21 6 июня, 2021

09.06.2021 00:09:23 | Автор: admin

Из новостей на неделе: вышел Unity 2021.2a19 с обновлением пайплайна работы с ассетами, исходники Периметра на Гитхаб выложили, вышел Blender 2.93 LTS, AMD FidelityFX Super Resolution появится в первых играх уже 22 июня, в Steam появились совместные наборы, прогресс по GDScript в Godot по пути к 4.0, вышли Howler 2022, KeyShot 10.2 и новый пакет Arm Mobile Studio для Unity.

Из интересностей: исследование того, как Nanite работает изнутри, советы по оптимизации работы с Substance, анимированный мост в Unreal Engine чисто в шейдере, интересные примеры VFX из недавних фильмов.

Обновления/релизы/новости

Вышел Unity 2021.2a19

Из крутого, как по мне, появилась поддержка параллельного импорта моделей и текстур (включить в Project Settings -> Editor -> Refresh).

И появилось окно импорта ассетов: позволяет посмотреть дату импорта, предыдущие версии ассетов, список зависимостей, импортеры, используемые для импорта артефакта, длительность импорта и многое другое.

Исходники Периметра на Гитхаб выложили

Можно собрать, как минимум, на XP и Win7.

AMD FidelityFX Super Resolution появится в первых играх уже 22 июня

  • На картах AMD прирост до 60%.

  • 4 пресета качества.

  • А так как оно не привязано к AMD, то будет работать и на картах Nvidia. На GTX 1060 даёт +41%

Вышел Blender 2.93 LTS

Который знаменует собой завершение более чем двадцатилетней истории разработки. А скоро появится и 3.0.

  • Новый воркспейс и редактор спредшитов для геометрических нод.

  • Новые способы использовать маски на моделях во время лепки.

  • Более быстрый и качественный рендеринг в Cycles и Eevee.

  • Крупное обновления набора инструментов Grease Pencil для 2D-анимации.

  • Обновились многие тулсеты.

Подробнее на сайте.

В Steam появилась новая функция под названием совместные наборы

С её помощью разработчики и издатели могут создавать наборы из нескольких игр в Steam совместно с другими разработчиками и издателями.

Подробнее в доках.

Вышел Verge3D 3.7 для Blender

Добавили технику глобального освещения для значительного улучшения реализма сцен, clipping planes, чтобы заглянуть внутрь сложных объектов, интегрировали инструменты для разработки мобильных и настольных приложений, добавили батчинг геометрии в рантайме.

Кроме того, включили в это обновление 3 новых демки.

Прогресс по GDScript в Godot по пути к 4.0

Ещё многое предстоит сделать, но и в этом обновлении достаточно интересных новшеств.

  • Типизированные массивы.

  • Лямбда-выражение.

  • Статические методы для встроенных типов.

  • Оптимизации. В частности, ведётся работа над уменьшением ветвлений. Вводится временный стек, чтобы убрать переинициализацию вариантов. Это поможет с типами, которые требуют аллокаций, например, Transform.

  • Покрытие тестами.

В библиотеку Chaos Cosmos для V-Ray было добавлено 100 новых ассетов

Идеально подходящих для интерьера: столы, стулья, лампы и посуда. В дополнение библиотека теперь включает несколько человеческих персонажей от AXYZ Design, одного из ведущих поставщиков отсканированных фотографий людей в 3D.

Unity хотят переосмыслить Unite

Поэтому в этому году конференции не будет. Ждём 2022.

Вышел Howler 2022

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

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

SpookyGhost вышел в опенсорс по MIT лицензии

Кроссплатформенный инструмент для процедурной анимации, созданный с использованием игрового движка nCine 2D на C++ и ImGui для UI, можно посмотреть на Гитхабе.

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

Вышел KeyShot 10.2

Добавили инструмент Mesh Simplification для уменьшения числа треугольников моделей, предназначенный для упрощения их повторного использования в AR приложениях, таких как KeyVR KeyShot.

Библиотека материалов KeyShot также была обновлена: теперь пользователи могут выбирать семь различных 3D-моделей для предварительного просмотра материалов в виде миниатюр.

Вышел новый пакет Arm Mobile Studio для Unity

Новый инструмент для анализа производительности мобильных устройств.

Интересные статьи/видео

Nanite: взгляд изнутри

Эмилио Лопес, Senior Graphics Engineer из Playground Games, поделился своим взглядом на Nanite и попытался разобрать то, как система работает. При исследовании во многом полагался на RenderDoc.

Запись стрима на канале Unreal Ungine по работе с Nanite в UE5 и про саму технологию

Оптимизация работы с Substance

Виктор Андреенков поделился советами, как сделать Substance Painter менее требовательным по железу без снижения качества работы.

Анимированный мост в Unreal Engine чисто в шейдере

Т. к. с мостом не нужно взаимодействовать, то и skeletal mesh не нужен, поэтому шейдер в таком случае показался автору разумным решением.

Создание мифических существ и VFX дыма с огнём

Члены команды REALTIME рассказали 80lv о создании мифических существ для тв-сериалов и обсудили процесс создания реалистичных визуальных эффектов дыма и огня.

Разное

Про создание VFX в Армии мертвецов Зака Снайдера

Использовалось сочетание наземных и воздушных снимков LIDAR и дронов.

Примеры VFX в Майор Гром: Чумной Доктор

Подробнее..

Недельный геймдев 22 13 июня, 2021

16.06.2021 00:20:01 | Автор: admin

Из новостей на этой неделе: стала доступна превью версия Unreal Engine 4.27 с включёнными в движок Bink Video и Bink Audio, Unity выпустили новый стартовый пак, вышла новая версия движка Diligent Engine 2.5, Activision выпустили расширение для Windows для просмотра USD-файлов, разработчики Cascadeur получили дополнительные 1.5 миллиона долларов на развитие продукта, PolyHertz выпустил новый скрипт UnChamfer Pro для 3ds Max, Khronos запускают программу сертификации 3D-просмотровщиков.

Из интересностей: подробный доклад от Insomniac про работу со светом в Marvels Spider-Man, занятная механика для VR игры, полезный доклад от Риотов про то, как они балансят и нерфят персонажей.

Обновления/релизы/новости

Стала доступна превью версия Unreal Engine 4.27

Из ключевого:

  • Oodle и Bink теперь встроены в движок.

  • Улучшения по части Open XR.

  • Path Tracer теперь в бетке.

  • Куча улучшений GPU Lightmass: запекания, теней, поддержки mGPU.

  • Обновление Niagara: версионность модулей, новый дебагер, куча улучшений UX/UI.

  • Оптимизировали рендеринг на мобильных платформах. Ключевые направления оптимизаций: Distance Field Shadows, Fast Approximate Anti-Aliasing (FXAA), Temporal Anti-Aliasing (TAA).

  • Изменения по части Datasmith, особенно в Datasmith Exporter Plugin для ArchiCAD.

  • Огромное число изменения инструмента для синематиков и виртуального продакшена.

Bink Video и Bink Audio теперь доступны в Unreal Engine бесплатно

Epic Games выпустили последние изменения по интеграции технологии RAD Game Tools (которые приобрела в начале этого года) в Unreal Engine. Доступно в Unreal Engine 4.27 и Unreal Engine 5.

Bink Video и Bink Audio кроссплатформенные видео и аудио кодеки с упором на производительность.

Разработчики Cascadeur получили дополнительные 1.5 миллиона долларов от Nekki

Спустя 2 месяца после выхода в ранний доступ этот инструмент для анимаций насчитывает уже 80000 пользователей. Команда, тем времени, из 25 разработчиков должна вырасти до 30. Компания планирует достичь двух важных этапов к полноценному релизу в 2022:

  1. Улучшить по максимуму уникальные инструменты Deep Physics с поддержкой AI.

  2. Функциональные возможности Cascadeur должны быть расширены дополнительными стандартными инструментами.

Unity выпустили новый стартовый пак

Набор содержит бесплатные и легковесные базовые контроллеры персонажей от первого и третьего лица для последней версии Unity 2020 LTS и более поздних версий с использованием Cinemachine и Input System.

Старые версии Unity также могут работать с паком, но, вероятно, нужно будет что-то дотюнить.

МФТИ и Gaijin Entertainment запускают магистерскую программу (4 семестра) по программированию игр

Заявку нужно подать до 30 июня. Обучение очное.

Вышла новая версия движка Diligent Engine 2.5

В этой версии:

  • Трассировка лучей теперь включена на Metal.

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

  • Несколько контекстов позволяют выполнять асинхронные вычисления и параллельный рендеринг.

  • Запросы на RT позволяют бросать лучи из обычных шейдеров (пиксельные, вычислительные и т. д.).

Activision выпустили расширение с открытым исходным кодом для Windows для просмотра USD

Проект доступен на GitHub под лицензией Apache 2.0 и позволяет пользователям Windows взаимодействовать с USD файлами (всё более широко используемым отраслевым форматом, созданным Pixar) прямо в Windows Explorer.

Первую версию браузера ассетов добавят в Blender 3.0

Пользователи давно ждут нечто подобное.

Epic Games поделились кратким руководством для тех, кто хочет познакомиться с Unreal Engine 5

В 16-страничной pdf-ке разобраны основные вопросы и приведены полезные ссылки. Ничего того, что есть в онлайн документации, но зато всё кратко и сжато. Как раз подойдёт для ознакомления.

PolyHertz выпустил новый скрипт UnChamfer Pro для 3ds Max

Предназначенный для быстрого, простого и точного удаления или масштабирования фасок меша.

BuildBox решили поменять прайс на свой движок после негодования клиентов

Они не будут брать комиссию с ревенью, если вы не используете их специальное решение для монетизации.

Ранее компания изменила прайс, помомо подписки захотели процент процент с ревенью: 30%, если у вас план Plus, 10%, если у вас PRO план. Вовремя одумались.

Faceware Technologies запустили Faceware Studio PLE, новую бесплатную версию Faceware Studio, программы для мокапа в реальном времени

Лицензия позволяет художникам бесплатно использовать Faceware Studio в течение шести месяцев для некоммерческой разработки игр, визуальных эффектов.

Вышла бетка Kinetix Advanced

Облачный инструмент позволяет сгенерировать 3d-анимацию из .MP4 видео.

Можно попробовать бесплатно, преобразовав 60-секундное видео. За 15 евро в месяц можно конвертировать до 3 минут видео и иметь доступ ко всем инструментам, которые предлагает Kinetix, а за 120 евро в месяц 30 минут видео.

Khronos запускают программу сертификации 3D-просмотровщиков

Консистентное отображение на различных платформах повышает доверие потребителей. CGTrader и Sketchfab уже подписались.

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

Интересные статьи/видео

4 полезных совета по работе с Substance Painter для новичков

  1. Маски ваши лучшие друзья.

  2. Будьте рассказчиком. Недостаточно просто навесить текстурки, важно понимать, как эти объекты будут смотреться рядом с другими.

  3. Нужно понимать/осознавать как свет и игровые движки влияют на ваши текстуры.

  4. Создайте свою собственную библиотеку материалов.

На этой неделе Epic Games обсудили новую систему глобального освещения Lumen из Unreal Engine 5

Специалисты рассказали, что это такое, как включить, и что технология из себя предоставляет.

Полезный доклад от Риотов про то, как они балансят и нерфят персонажей

Не смотрите на оценки под видео, контент норм. Там приведены реальные данные и метрики и, что самое главное, докладчик рассказывает, как они принимают решения на основе этих данных.

Подробный доклад от Insomniac про работу со светом в Marvels Spider-Man

Разное

Разбор VFX из фильма Поколение Вояджер (2021)

Подробнее..

Лицевые анимации из двумерных видео

29.11.2020 04:09:36 | Автор: admin
КликбейтКликбейт

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

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

Введение

Дело в том, что существующие способы лицевой анимации требуют дополнительных программных, аппаратных или визуальных средств решения. А эти решения, в свою очередь, стоят денег. Давайте посмотрим на существующие решения и решим, что должно быть в нашем решении:

По моему маленьку расследованию, в настоящее время чуществуют следующие методы:

  • Создание маркеров на основе захвата движения по точкам или отметинам на лице исполнителя

    • Методы, использующие камеры, размещенные вокруг объекта и точки, нанесённые на лицо актёра или естественные отметины на коже для точного определения положения лица в пространстве

  • Безмаркерные методы с использованием различных типов камер

    • Методы, использующие различные способы создания карты глубин, такие, как камеры Kinect или Intel Real Sense

  • Звуко-управляемые методы

    • Методы, использующие скрытые марковские модели или нейронные сети для преобразования звуковых параметров в поток управляющих параметров для модели лица.

  • Анимация по ключевым кадрам

    • Методы, использующие классический способ анимации, при котором различные части модели лица смещаются вручную

Таким образом, проанализировав существующие методы создания лицевой анимации, мы можем определить необходимый список параметров алгоритма:

  1. Автоматизированность

  2. Отсутствие необходимости в использовании дорогостоящего оборудования

  3. Относительная точность передачи положения лица в пространства

  4. Отсутствие необходимости использования дополнительных маркеров

Выборка

Чтобы собрать что-то дельное, необходимо составить математическую модель того, что мы собираемся делать. Ну или хотя бы понять, как вообще мы будем определять качество итогового результата? Поэтому

Благо лиц в интернете достаточно. Так что в качестве тестовых данных я выбрал четырёх популярных персонажей. Важное замечание: все видео сохранены в 2х различных форматах, имеют различные условия освещения.

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

Так выглядят три последовательных кадра и параметры, которые мы оцениваем на них. Данные собираются автоматически в процессе построения опорных точек. Основные параметры, которые я собрал включают в себя:

  • Rele - расстояние между левым и правым глазом, показывающее искажение верхней части лица

  • Ren - расстояние между правым глазом и носом, показывающее искажение правой скулы

  • Len - расстояние между левым глазом и носом, показывающее искажение левой скулы

  • Chin 1-8 - набор расстояний, состоящих из расстояний между левой и правой частью лица, показывающий общее искажение лица

Поговорим о том, что эти параметры собой представляют.

Параметр rele

Пусть (x,y,z) координаты центра левого и правого глаза вычисляются последовательно для каждого кадра по следующим формулам:

x = \frac{\sum_{i=m}^{n}{a_{i_0}+a} }{n+1}\\y = \frac{\sum_{i=m}^{n}{a_{i_1}+a} }{n+1}\\ z = \frac{\sum_{i=m}^{n}{a_{i_2}+a} }{n+1}

Список значений rele хранит в себе длину вектора AB, где A-координата центра левого глаза, B-координата центра правого глаза соответственно, а равно, расстояние между центрами левого и правого глаза. Пусть каждое значение списка rele вычисляется по следующей формуле для каждого кадра:

rele_i = \sqrt{(x_{left}-x_{right})^2+(y_{left}-y_{right})^2+(z_{left}-z_{right})^2}

Тогда значение rele для всех кадров, пропорционально их количеству будем вычислять по следующей формуле, где - количество кадров анимации:

rele = \frac{ \sum_{i=m}^{n}{\frac{rele_i}{rele_0}} }{a}

Значение rele показывает среднее изменение длины вектора AB - расстояния между центрами правого и левого глаза. Из чего можем провести следующую зависимость:

Чем меньше значение функции rele для видеофайла, тем меньше изменялись основные пропорции лица, а равно, тем (предположительно) лучше качество конечной анимации

Параметр len

Пусть (x,y,z) координаты центра левого глаза вычисляются последовательно для каждого кадра по следующим формулам:

x = \frac{\sum_{i=m}^{n}{a_{i_0}+a} }{n+1}\\y = \frac{\sum_{i=m}^{n}{a_{i_1}+a} }{n+1}\\ z = \frac{\sum_{i=m}^{n}{a_{i_2}+a} }{n+1}

Список значений len хранит в себе длину вектораAB, где A-координата центра левого глаза, B-координата конца носа, а равно, расстояние между центром левого глаза и концом носа. Пусть каждое значение списка len вычисляется по следующей формуле для каждого кадра:

len_i = \sqrt{(x_{left}-x_{nose})^2+(y_{left}-y_{nose})^2+(z_{left}-z_{nose})^2}

Тогда значение len для всех кадров, пропорционально их количеству будем вычислять по следующей формуле, где - количество кадров анимации:

len = \frac{ \sum_{i=m}^{n}{\frac{len_i}{len_0}} }{a}

Значение len показывает среднее изменение длины вектора AB - расстояния между центром левого глаза и носом. Из чего можем провести следующую зависимость:

Чем меньше значение функции len для видеофайла, тем меньше изменялось расстояние между левым глазом и носом, а равно один из основных параметров лица, тем (предположительно) лучше качество конечной анимации

Параметр ren будем вычислять по тем же формулам, внеся соответсвующие изменения.

Список параметров chin0-chin7

Список параметров chin0 - chin7 показывает изменение длины вектора AB между каждыми двумя точками, относящимися к нижней дуги лица (точки 0-16).

Список значений chini хранит в себе длины векторов AB, где A-точка лежащая на левой полудуге, B-точка лежащая на правой полудуге соответственно, а равно, расстояние левой и правой стороной лица в точке i. Пусть каждое значение списка chini вычисляется по следующей формуле для каждого кадра:

chin_i = \sqrt{(x_{chin-left}-x_{chin-right})^2+(y_{chin-left}-y_{chin-right})^2+(z_{chin-left}-z_{chin-right})^2}

Тогда значение chin_i будет вычисляться для каждой точки нижней дуги лица (кроме вершины) по следующей формуле, где - количество кадров анимации:

chin = \frac{ \sum_{i=m}^{n}{\frac{chin_i}{n}} }{a}

Значение chin показывает среднее изменение длины вектора AB - расстояния между двумя точками на левой и правой стороне нижней дуги лица. Из чего можем провести следующую зависимость:

Чем меньше значение функции chin{0-7} для видеофайла, тем меньше изменялось расстояние между двумя половинами лица, а равно один из основных параметров лица, тем (предположительно) лучше качество конечной анимации

Все эти метрики позволяют относительно точно отследить качество итоговой анимации не только по итогу, но ещё и покадрово как-то вот так:

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

Этапы формаирования косточек и анимацииЭтапы формаирования косточек и анимации

Результаты работы скрипта

Кошмар разКошмар раз
Кошмар дваКошмар два
Кошмар триКошмар триНормальная табличка сюда не влезла, наслаждайтесь 10/10 шакаловНормальная табличка сюда не влезла, наслаждайтесь 10/10 шакалов

Давайте подробнее рассмотрим "Кошмар три"

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

Ну и вот итог всего пути, который мы прошли. От точек в пространстве, до сносной анимации:

Подводим итоги

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

Да и исполнение занимает значительное количество времени, что в сравнении с некоторыми из способов недопустимо

Подробнее..

ORM textures, а стоит ли оно того?

15.06.2020 00:20:27 | Автор: admin
Недавно я читала статью и увидела описание процесса создания интересной текстурной карты, о которой пойдет речь далее



Вступление


Что же такое ORM? Сокращенно это Occlusion, Roughness, Metallic необычное сочетание трех разных PBR карт запакованных в одной текстуре.

Пример создания такой текстуры я буду показывать в Substance Painter, но также ее можно делать и в Photoshop (для меня это более затратный по времени способ тогда, как Substance Painter делает ее автоматически, главное только правильно настроить конфигурацию). Для рендера я буду использовать Blender 2.8.

Проблема


Для настройки текстур на объекте, работая в Blender, как и в любом другом редакторе, очень часто приходится добавлять примерно пять основных текстурных карт (BaseColor, Normal, AO, Metallic, Roughness), а то и больше. Этот процесс занимает хоть и не большое, но все-таки драгоценное время художника. Поэтому в данной статье я и хочу разобрать такую тему, как объединение карт в ORM и выяснить, какое она дает преимущество перед традиционными текстурами и дает ли его вообще.

Разбор


Ambient Occlusion, Roughness, Metallic это три черно-белые (Grayscale) карты. Это значит, что их цвет лежит только в одном канале. Для справки, в текстурировании мы можем использовать текстуры, состоящие из 3-4 каналов (RGB, RGB+A).

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



С теорией разобрались. Но перед тем как показывать настройку проектов в Substance Painter и Blender, я бы хотела привести немного измерений рендеринга одного и того же 3D объекта с использованием разных движков, попеременно подключая два разных материала (один с тремя отдельными картами, другой с использованием ORM).



Для рендеринга я использовала такие движки как Eevee, Cycles (CPU и GPU) и Octane Render (все настройки стандартные, sampling равен 128).

(Сразу хочу сказать, что в Octane я не профи, и, если вы заметите у меня неточности в настройке материалов, буду только рада, если вы на них укажите).



Теперь выбор остается за вами. Как видно результаты рендера очень близкие. Хочу напомнить, что эта статья написана в первую очередь лишь для того, что бы показать, как работать с ORM, а не про оценку времени рендеринга на разных движках.

Настройка текстур


Первоначально создадим свою конфигурацию в Substance Painter и добавим в нее три ячейки для текстур, первую и вторую RGB+A (три связанных канала и один несвязанный), а третью R+G+B (три несвязанных канала). Теперь переносим в ячейки уже имеющиеся карты. В итоге должна получиться такая картина



При экспорте я использовала добавление альфа канала и Dilation + Transparent для того, чтобы текстура запекалась только на месте UV шеллов и нигде больше. На рисунке видно какие в итоге получились карты.

Теперь разберемся с настройкой текстур в Blender. Думаю, что таким же образом можно настраивать ноды и в других редакторах, таких как Maya или Cinema4D.
(Сейчас будет совсем туториал, поэтому я уберу его из основного текста дабы не нагружать статью большим числом картинок).

Настройка
Итак начнем с обычной настройки материала в Blender.



В Eevee и Cycles ноды с картами подключаются к своим ячейкам на главной ноде (PrincipledBSDF), которая их комбинирует. Единственное, что может вызвать вопрос, это, что я делаю с Normal Map? Такими действиями я инвертирую зеленый канал, чтобы карта из типа DirectX стала OpenGL. Да, можно и сразу выгрузить в OpenGL, но мне нравится более универсальный вариант.

Во втором случае AO, Metallic и Roughness я заменяю на ORM и добавляю ноду Separate RGB, чтобы распаковать каналы и достать карты по отдельности. Далее по той же схеме подключаю их к главной ноде.



Теперь разберемся с Octane. Ноды в этом движке отличаются, но имеют похожий смысл.



Вроде ничего сложного, теперь с ORM.



В случае с ORM разделение каналов работает по другой схеме и не так просто, как в Eevee и Cycles. Добавляя RGB Spectrum Tex (красный, зеленый или синий) и ORM в Multiply Tex, я вытягиваю из ORM только один канал, в зависимости от цвета RGB Spectrum Tex. Cosine Mix Tex преобразует этот канал в оттенки серого и отправляет в главную ноду (Universal Material).


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



Это попарное сравнение всех рендеров мечей. Как видно Octane несколько отличается, так как имеет другие стандартные настройки, и настройки освещения в том числе. Но повторю еще раз, я не профессионал. На настройку материалов в Eevee и Cycles я потратила меньше времени, чем обычно, в Octane наоборот больше, так что использовать ORM или нет решать вам, а у меня всё.

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

Буду рада критике и пожеланиям!
Подробнее..

Установка и настройка Armory 3D

25.11.2020 14:11:26 | Автор: admin

Итак, в прошлом посте я рассказал вам что что такое Armory Engine. Сейчас расскажу вам как установить движок и сделать свой первый тестовый уровень (в следующем уроке). Перед тем как начать, вам потребуются навыки работы с Blender это ваш основной инструмент. Если их нет, что ж, в сети достаточно уроков. От себя лично, могу порекомендовать курсы от Артема Слаквы (ни разу не реклама). Ладно, начнем.

Что нам потребуется:

  • Стабильная версия Blender (автор рекомендует версию 2.8, поскольку 2.9 работает с движком нестабильно)
  • Аддон к Blender на официальном сайте (движок поставляется в виде аддона, если вам не жалко, задонатьте автору)

Установка и настройка


Установка проста до безобразия скачайте и распакуйте архив движка. Установите как аддон в установленный Blender. Edit > Preferences -> Add-ons -> armory.py.

Armory.py находится в распакованом архиве. Далее, включите добавленный аддон галочкой в Render: Armory в Preferences: Add-ons.

Теперь нужно проверить правильность установки движка. В вкладке Render проверяем путь SDK Path. Он должен соответствовать тому где у вас лежит SDK. Например D:\ArmorySDK2011\ArmorySDK если же нет, то щелкнув там же значок папки, выберете тот путь где у вас распакован движок.


Сохраняем .blend файл и нажимаем кнопку F5 (play) расположенную на панели Properties > Render > Armory Player. Распространенные проблемы с установкой можно посмотреть здесь.

Движок уже включает в себя Haxe и Kha поэтому ставить отдельно их не надо.

Редактор кода


Armory автоматически установит правильный редактор кода. Это работает так: движок сканирует переменные установленного IDE, и если например, у вас установлен Visual Studio то по умолчанию будет VS. Если нет, то будет использоваться переменная среды консольного редактора кода.

Автор рекомендует Kode Studio


"haxe.executable": "ArmorySDK/Kha/Tools/haxe/haxe-linux64","kha.khaPath": "ArmorySDK/Kha","krom.kromPath": "ArmorySDK/Krom"

Но редактор можно использовать любой: Sublime, Atom, Notepad++ etc. Только укажите в Code Editor Executable путь к своему редактору.

В следующем уроке будет создание первого тестового уровня.
Подробнее..

Основы Armory. Traits

07.12.2020 14:17:59 | Автор: admin

Traits, как вы уже успели догадаться из прошлых постов - это система скриптов в Armory. Если вы работали с движком Source то наверно помните такие розовые кубики - там они именовались Entities. Так и у нас, суть одна и та же. В нашем случае используемые traits можно посмотреть только в Outliner, переключившившись на режим просмотра Orphan Data в Collections. Это крайне неудобно, потому что нет визуального отображения в скриптов в сцене.

Типы traits

Traits делятся на несколько видов:

  • Haxe - написание скриптов с нуля на Haxe.

  • Wasm - см. WebAssembly.

  • UI - работа пользовательским интерфейсом.

  • Bundled - готовые / связанные сценарии Haxe.

  • Nodes - создание скриптов в Logic Editor (как в blueprints UE4).

Все они могут и (будут!) использоваться в вашем проекте.

Fake User

Если вы написали или составили в Logic Editor traits и не привязали к объекту - при экспорте игры она не сохранится. Чтобы не допустить пропажу ценных часов потраченных на составление хитроумного скрипта, в Blender есть одноименная функция. Нажмите значок щита рядом с вашей trait и она будет экспортирована. Функции или ноды требующих имени, должны быть экспортированы.

Traits Events

Trait могут запускать ивенты с жизненным циклом:

  • Trait.notifyOnAdd() - к объекту добавлен trait.

  • Trait.notifyOnInit() - объект добавлен в сцену.

  • Trait.notifyOnRemove() - объект удаляется со сцены.

  • Trait.notifyOnUpdate() - обновляет логику игры.

  • Trait.notifyOnRender() - обновляет рендеринг.

  • Trait.notifyOnRender2D() - обновляет рендер 2D.

Если сцена построена асинхронно т.е если в ней присутствуют не все объекты, то onInit может добавлять их. Если в сцене у вас есть trait которая зависит от других объектов, то используйте Scene.active.notifyOnInit() - этот ивент вызывается когда сцена полностью построена и в ней присутствуют все необходимые объекты.

Свойства traits

Скриптам можно задавать параметры и свойства прямо из Blender. Перед переменной в исходном коде нужно вставить метаданную @prop. Переменная var поддерживается только с ключевым словом. Final не будет работать, потому что свойства trait устанавливаются через Haxe Reflection API, после сборки игры.

Поддерживаются следующие типы данных:

Пример исходного кода:

Код
 package arm;  // See below ("Object data types")import iron.object.CameraObject; import iron.math.Vec2;import iron.math.Vec3;import iron.math.Vec4; class MyTrait extends iron.Trait {// Primitive data types@propvar intValue: Int = 40; // Type annotation possible, but not required@propvar floatValue = 3.14;@propvar stringValue = "Hello world!";@propvar booleanValue = true; // Object data types@propvar objValue: iron.object.Object; // Needs type annotation to be recognized@propvar camObjValue: CameraObject; // Type can be imported (see above)...@propvar lightObjValue: iron.object.LightObject; // .. or not, both will work@propvar meshObjValue: iron.object.MeshObject;@propvar speakerObjValue: iron.object.SpeakerObject; // Vector data types@propvar vector2DValue: Vec2 = new Vec2(0.2, 0.5); // Initialization possible...@propvar vector3DValue: Vec3; //... but not required@propvar vector4DValue = new Vec4(1, 2, 3, 4); // Not visible in Blender, `@prop` is missingvar notVisibleValue = 0.0; // ...}

Вот что дает этот исходник:

Предупреждения и ошибки

Движок будет предупреждать о неверных @prop:

  • Каждый @prop должен объявлять следующую переменную

  • Должна быть поддержка свойств

  • Синтаксис свойств должен быть правильным

  • Не лучшим решением будет делать статические свойства, потому что несколько объектов могут записать одно и то же свойство, что может привести к ошибкам.

Упорядочивание

По дефолту все traits создаются и хранятся в папке sources/ arm. Сваливать все в одну кучу - не самое умное решение, вам же потом поддерживать проект и искать битый скрипт среди сотен других - то еще занятие. Сохранять лучше в отдельных папках и подпапках для каждой сцены.

Можно упорядочить иерархически, для этого используем Haxe package syntax при наименовании какой нибудь traits. Например создадим trait с именем general.BoxBehavior - она сохранится в подкаталоге Sources/arm/general с именем BoxBehavior.hx

Вложенные подпапки можно создавать бесконечно, добавляя имя traits через точку: general.terrain.TerrainCollider создаст файл с именем TerrainCollider.hx по пути Sources/arm/ general/terrain.

Если вдруг понадобилось присвоить объекту другую характеристику, то нажмите на Class под списком свойств и выберите в выпадающем списке необходимое имя.

Подробнее..

Компьютерное зрение в промышленной дефектоскопии Часть 2 Генерируем стремные трубы чтобы порадовать нейронку

18.02.2021 18:15:13 | Автор: admin


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


  • как сделали такой генератор на основе Blender и Python,
  • какие типы масок для задач компьютерного зрения вообще можно получить в Blender.

Заметка от партнера IT-центра МАИ и организатора магистерской программы VR/AR & AI компании PHYGITALISM.


Введение


Напомним, что генератор фотореалистичных 3D моделей промышленных труб и их дефектов нужен был для того, чтобы обогатить набор обучающих данных для нейросетевых алгоритмов детекции дефектов на изображениях (дефекты труб на ТЭЦ, снятые при помощи беспилотников). Беспилотники и автоматическая детекция дефектов на практике должна минимизировать время и расходы на проведение сервисного обслуживания станции.


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


Если данные сгенерированы корректно, то генератор позволяет:


  • сократить объем необходимых реальных фотографий труб,
  • получить данные, которые в реальности встречаются крайне редко и сделать набор данных более сбалансированным,
  • получить большее число признаков (в данном примере, помимо изображения дефекта, мы получаем его 3D модель).

Поскольку генератор позволяет получать 3D объекты, он способен стать источником новых данных не только для алгоритмов классического компьютерного зрения (CV), но и для целого ряда задач геометрического глубокого обучения (3D ML, GDL).


Применение 3D ML подходов может дать преимущество при решении задач дефектоскопии, так как пространственные сканеры / камеры глубины (RGB-D, Lidar и пр.) позволяют находить менее очевидные человеческому глазу дефекты и реконструировать изучаемые объекты (например, вздутие трубы не всегда можно обнаружить, не потрогав трубу руками или чувствительным щупом).


Часть 1: Реализация генератора данных



Рис.1 Примеры данных, сгенерированных в проекте. Слева направо: меш труб, отрендеренные изображения с текстурами, битовые маски дефектов (авторазметка), ограничивающие прямоугольники дефектов (авторазметка).


Вся работа по созданию искусственного набора данных была осуществлена в Blender, с использованием скриптов на языке Python. Исключение составила лишь программа преобразования растровой разметки в формат Yolo, написанная на языке Rust.


Минутка текстовых шуток для IT-шников:
В нашем проекте Python генерировал змеевики, а Rust получал разметку ржавчины.


Рис.2 Рабочее окно Blender с плагином генератора труб.


На начальных этапах работы тестировалось построение сцены (и трубы и сами дефекты) средствами полигонального моделирования. При таком подходе к генерации данных встает вопрос о том, как внести разнообразия в процесс генерации и о том, как удобнее создавать разметку, ведь отмечать поврежденные участки в меше группами вершин не самый удобный способ. Из этих соображений было принято решение объединить создание дефектов и разметки с помощью шейдеров.



Рис. 3 Пример сцены, из которой рендерились наборы изображений с разметкой поперечных трещин.


Задача создания генератора синтетических данных была разделена на следующие этапы:


  • Настройка цифрового двойника камеры БПЛА с накамерным светом (рендеринг итоговых изображений должен позволять добиться реалистичности в синтетических данных).
  • Создание инструмента для быстрого моделирования базовой геометрии труб.
  • Настройка процедурных материалов для различных поверхностей труб (металлический блеск, ржавчина и пр.).
  • Настройка процедурных материалов для различных дефектов труб (различные трещины, дыры, изгибы и пр.).
  • Настройка процедурной анимации позиции камеры, освещения и материалов для создания разнообразных изображений из одной сцены.
  • Настройка масок дефектов (битовые маски и ограничивающие прямоугольники для разных классов дефектов).
  • Рендер итоговых сцен.
  • Приведение разметки дефектов к форматам YOLO и MS COCO.

Настройка цифрового двойника камеры БПЛА с накамерным светом



Рис.4 Камера с осветителем на сцене в Blender.


Объект с самосветящимся материалом, имитирующий кольцевой осветитель, и всенаправленная лампа назначены дочерними объектами камеры. Размер сенсора, угол обзора, относительное отверстие объектива и разрешение получаемого изображения настроены в соответствии с реальными характеристиками камеры DJI Mavic 2 Zoom.


Инструмент для быстрого моделирования базовой геометрии труб



Рис.5 Сцена, наполненная змеевиками труб, полученными с помощью скрипта из этого раздела.


В первую очередь нужно отметить, что вся геометрия труб строилась по NURBs без прямой конвертации в полигональные модели (Про использование NURBS в 3D моделировании на хабре писали в этой заметке). Все дефекты, в том числе, геометрические создавались посредством материалов на этапе рендера.


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


Код для процедурной генерации труб:
import bpyimport timefrom math import sin, cos, pi, radians# Генерация змеевиков в плоскости внутри прямоугольника со сторонами# sizeX, sizeYdef create_flat_curve(sizeX, sizeY):    points = []    for y in range(sizeY):        for x in range(sizeX):            if y % 2 == 0:                point = [x, y]            else:                point = [sizeX - x - 1, y]            points.append(point)    curve_name = "Pipe_Flat_" + str(time.time_ns())    curveData = bpy.data.curves.new(curve_name, type='CURVE')    curveData.dimensions = '3D'    curveData.resolution_u = 2    polyline = curveData.splines.new('NURBS')    polyline.points.add(len(points)-1)    for i, point in enumerate(points):        x,y = point        polyline.points[i].co = (x, y, 0, 1)    curveData.bevel_depth = 0.4    #polyline.use_endpoint_u = True    curveOB = bpy.data.objects.new(curve_name, curveData)    bpy.context.collection.objects.link(curveOB)# Генерация змеевиков в пространствеdef create_cyl_curve(radius, angle, height, density, horizontal):    phi = radians(angle)    steps = int(density * phi / (pi*2))     print("Steps:", steps)    points = []    if horizontal:        for z in range(height):            for step in range(steps):                if z % 2 == 0:                    x = radius * cos(step * phi / steps)                    y = radius * sin(step * phi / steps)                    else:                    x = radius * cos(phi - (step+1) * phi / steps)                    y = radius * sin(phi - (step+1) * phi / steps)                  point = [x, y, z]                points.append(point)    if not horizontal:        for step in range(steps):            for z in range(height):                x = radius * cos(step * phi / steps)                y = radius * sin(step * phi / steps)                    if step % 2 == 0:                    point = [x, y, height-z-1]                else:                    point = [x, y, z]                points.append(point)    print("Points:", len(points))    curve_name = "Pipe_Cylinder_" + str(time.time_ns())    curveData = bpy.data.curves.new(curve_name, type='CURVE')    curveData.dimensions = '3D'    curveData.resolution_u = 2    polyline = curveData.splines.new('NURBS')    polyline.points.add(len(points)-1)    for i, point in enumerate(points):        x,y,z = point        polyline.points[i].co = (x, y, z, 1)    curveData.bevel_depth = 0.3    #polyline.use_endpoint_u = True    curveOB = bpy.data.objects.new(curve_name, curveData)    bpy.context.collection.objects.link(curveOB)

Настройка материалов поверхностей труб


От использования готовых наборов текстур из изображений пришлось отказаться по нескольким причинам:


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

Запекание сгенерированных текстур, например из Substance Painter, было исключено чтобы не множить инструменты и сущности (такая вот бритва Оккама у нас вышла).



Рис.6 Группа нод (назовем ее супернодой) для настройки материалов, объединенные в одну большую ноду.


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


Примечание:

Здесь и далее для конструирования шейдеров используется интерфейс с нодами, поскольку разработчик данного решения CG художник, и этот подход был для него предпочтительным =)



Рис. 7 Содержание суперноды из рис.6: материалы внутри группы собирались преимущественно из шумов и градиентов.


Настройка материалов дефектов труб



Рис.8 Пример сгенерированных труб с дефектами коррозии (слева) и цветами побежалости (справа).


Такие дефекты, как коррозия и цвета побежалости, создавались непосредственно из шума Перлина, градиентов и смещения (Displacement) геометрии модели по нормали к поверхности (само смещение производилась в шейдере, при этом геометрия неподвижна).



Рис.9 Создание дефекта Разрыв трубы в Blender.


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



Рис.10 Создание дефекта Выход трубы из ряда в Blender.


Для таких дефектов, как выход трубы из ряда и разрыв, использовалось векторное смещение по выбранной оси в системе координат объекта. Создание такого чисто геометрического результата средствами шейдеров обусловлено удобством вывода данных для разметки как значения материала (примеры таких разметок смотри во второй части заметки).


Анимация камеры, освещение и материалы


Для получения набора разнообразных изображений из одной сцены мы анимировали позицию и поворот камеры, яркость источников света и параметры материалов в одном ключе с использованием анимационного модификатора Noise с заданными пороговыми значениями. Таким образом, можно было не беспокоиться о количестве кадров последующего рендера, ведь сколько бы их не оказалось, каждый был уникальным безо всяких закономерностей.



Рис.11 Применение шума на анимационных кривых позиции, поворота камеры, интенсивности и позиции источника света для процедурной съемки сцены.


Настройка масок дефектов


Для вывода черно-белых масок разметки дефектов использовался канал Arbitrary Output Value (AOV), в ноду которого подавался коэффициент смешивания базового материала и материала дефекта. Иногда использовалась бинарная математическая нода Greater Than (на выходе 0, если входное значение меньше порогового, иначе 1).


Рендер


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



Рис.12 Директории с сохраненными изображениями (слева) и разметкой (справа).


Приведение разметки к формату YOLO


Формат разметки YOLO предполагает обозначение участков изображения ограничивающими прямоугольниками. Текстовый файл должен содержать нормированные координаты центров ограничивающих прямоугольников и их габариты. Для получения такого вида разметки была написана программа, рекурсивно проходящая по соседним пикселям маски, значения которых отличны от нуля, и сохраняющая минимальные и максимальные координаты связанных пикселей, после чего абсолютные координаты вершин прямоугольников нормализовались. Выбор языка Rust для написания этой программы был обусловлен скоростью выполнения и возможностью с лёгкостью реализовать одновременную обработку нескольких изображений на разных потоках процессора. Ниже приведен код на Python для поиска группы пикселей изображения, относящейся к одному дефекту.



Рис.13 Сгенерированные трубы с дефектом трещины (слева) и соответствующая маска для данного изображения (справа).


Код с поиском масок дефектов на Python (предполагается что на входе имеется изображение как на рис.13 справа):
import bpyimport colorsysimage = bpy.data.images["two_cubes.png"]sizeX = 64sizeY = 64image.scale(sizeX, sizeY)pixels = image.pixelssize = [sizeX, sizeY]print(len(pixels))grid = [[ [0] for y in range(size[1])] for x in range(size[0])] print("LEN:", len(grid))print(len(pixels)/4, " == ", sizeX*sizeY)def rgb_to_hex(rgb):    hex_string = ""    for c in rgb:        hex_string += str(hex(max(min(int(c * 255 + 0.5), 255), 0)))[2::]     return hex_string.upper()def search_neighbours(grid, x, y, color, l,b,r,t):    grid[x][y] = 0    #print("FROM", x, y)    if x < l:        l = x    if x > r:        r = x    if y < b:        b = y    if y > t:        t = y    if x < size[0]-1:            if grid[x+1][y] == color:        #print("RIGHT")            l,b,r,t = search_neighbours(grid, x+1, y, color, l, b, r, t)    if y < size[1]:        if grid[x][y+1] == color:        #print("UP")            l,b,r,t = search_neighbours(grid, x, y+1, color, l, b, r, t)    if x > 0:        if grid[x-1][y] == color:        #print("LEFT")            l,b,r,t = search_neighbours(grid, x-1, y, color, l, b, r, t)    if y > 0:        if grid[x][y-1] == color:        #print("DOWN")            l,b,r,t = search_neighbours(grid, x, y-1, color, l, b, r, t)    return (l,b,r,t)  for i in range(0, int(len(pixels) / 4)):    if pixels[i*4] > 0:#sum(pixels[i*4:i*4+3]) > 0:        x = (i) % size[1]        y = int(i / size[1])        #color = pixels[i*4:i*4+2]        #hex_col = rgb_to_hex(pixels[i*4:i*4+3])        grid[x][y] = 1              #print(x,y)print("GRID FINISHED")        color = 1islands = []for y in range(size[1]):    for x in range(size[0]):        if grid[x][y] == color:            "LOOKING FOR NEW ISLAND"            print(search_neighbours(grid, x, y, color, x, y, x, y))

Часть 2: Создание масок и разметки в Blender



Рис.14 Исходная тестовая сцена в Blender.


В этой части мы постараемся больше показывать, нежели рассказывать. На основе одной тестовой сцены в Blender (рис. 14) покажем, какие возможно сгенерировать полезные для задач компьютерного зрения изображения из трехмерной сцены.


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


1.Комбинированные Рендер-пассы


Рис. 15 Combined Pass: итоговый рендер сцены с учетом всех компонент.


2. Глубина сцены


Рис.16 Depth Pass: карта глубины для данной сцены.


Канал глубины позволяет передать информацию о позициях пикселей в пространстве через их удалённость от камеры. Используя пару изображений Combined+Depth можно тренировать нейросети, восстанавливающие трёхмерные сцены из изображений.


3. Карты нормалей


Рис. 17 Normal Pass: карта нормалей сцены.


Канал нормалей снабжает изображение информацией о нормалях поверхностей, что дает возможность не только менять освещение при постобработке, но и понимать форму объектов (хотя информации об их расположении в пространстве относительно друг друга отсутствует).


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


4. Диффузная составляющая


Рис. 18 Diffuse Color Pass: рассеивающая составляющая материалов (например высокое значение у зеркальных металлических поверхностей и низкое значение у шероховатых диэлектриков).


5. Бликовая составляющая


Рис. 19 Glossy Color Pass: отражающая способность материала (блики).


6. Имитирующая составляющая


Рис. 20 Emission Pass: самосвечение материалов.


7. Суммарная интенсивность света


Рис. 21 Ambient Occlusion Pass: суммарная интенсивность света в каждой точке.


8. Теневая составляющая


Рис. 22 Shadow Pass: тени (для каждой точки пространства просчитываются относительно источников света на сцене).


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


Маски можно создавать на индивидуальные объекты и их группы, материалы, а также на произвольные параметры материалов.


9. Маска индивидуальных объектов (instance segmentation)


Рис. 23 Cryptomatte Object Pass: разметка различных объектов случайным цветом.


10. Разметка объектов по классам материалов


Рис. 24 Cryptomatte Material Pass: разметка различных материалов случайными цветами.


В пассах Cryptomatte всем объектам и материалам присваиваются уникальные цвета.


Допустим, мы хотим создать две маски: на одной будут отмечены все обезьянки, на другой геометрические примитивы. Всем объектам нужно назначить Object ID (он же Pass Index), для обезьянок это будет 1, для примитивов 2, 0 останется для пола. Для удобства объекты разных классов можно распределить по коллекциям и написать скрипт, который присваивает всем объектам коллекции свой Object ID.



Чтобы получить необходимую маску, нужно использовать ноду ID Mask в композиторе.
Также в композиторе можно настроить одновременный вывод пассов и масок в отдельные файлы (см. рисунок ниже).



11. Разметка объектов по категориям (semantic segmentation)


Рис. 25 monkeys mask: маскирование объектов класса обезьяна.



Рис. 26 primitives mask: маскирование объектов класса геометрические примитивы.


Если мы хотим отметить каждый интересующий нас объект по отдельности, им нужно присвоить свои уникальные Object ID.


12. AOV - Arbitrary Output Value

Blender позволяет выводить в изображение любые параметры материалов. Для этого в настройках пассов нужно создать слой AOV, в который будут сохраняться значения из шейдеров в виде RGB или числа с плавающей точкой. В соответствующем материале нужно создать ноду Output/AOV и присвоить ей имя слоя.


Разберём этот материал:

Рис. 27 Combined Pass, умноженный на маску материала.



Здесь текстура шума подана на параметр Scale шейдера подповерхностного рассеивания (см. рис. выше). Допустим, мы хотим получить маску на те области поверхности, в которых параметр Scale больше 1.8.


В результате получим маску:


Проделаем теперь подобное с другим материалом и выделим красные области, подав в AOV фактор смешивания синего и красного цветов:


AOV даёт более широкие возможности для разметки, этот подход можно использовать для обозначения областей объектов, подверженных смещению (Displacement). На объектах на изображении ниже использовалось смещение по нормали поверхности для имитации повреждений:


Нужно отметить, что на этих объектах разные материалы, но для каждого из них включен вывод значения смещения в один канал AOV, значения при этом складываются.



Отдельным примером может служить использование AOV для разметки повреждённых областей объектов, на которых основной материал заменяется на прозрачный. На этой обезьянке применено трёхмерное смещение (Vector Displacement), то есть каждый участок, подверженный такому эффекту смещается не по нормали к исходной поверхности, а по трём осям согласно значениям из цвета, подаваемого на вход (R,G и B соответствуют X, Y и Z).





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




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


Заключение



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


В будущем мы постараемся рассказать и про другие наши эксперименты связанные с 3D ML вобще и с Blender в частности, а пока можете подписаться на наш канал в Telegram 3D ML, где мы рассказываем несколько раз в неделю о новостях и достижениях в этой науке)

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru