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

Работа с 3d-графикой

Как я написал браузерный 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 скиньте, пожалуйста ему ссылку на статью?

Подробнее..

Перевод Vulkan. Руководство разработчика. Непрограммируемые стадии конвейера

26.04.2021 18:04:48 | Автор: admin
Я работаю переводчиком в компании CG Tribe в Ижевске и здесь публикую переводы Vulkan Tutorial (оригинал vulkan-tutorial.com) на русский язык.

Сегодня я хочу представить перевод новой главы раздела, посвященного графическому конвейеру (Graphics pipeline basics), которая называется Fixed functions.

Содержание
1. Вступление

2. Краткий обзор

3. Настройка окружения

4. Рисуем треугольник

  1. Подготовка к работе
  2. Отображение на экране
  3. Графический конвейер (pipeline)
  4. Отрисовка
  5. Повторное создание цепочки показа

5. Буферы вершин

  1. Описание
  2. Создание буфера вершин
  3. Staging буфер
  4. Буфер индексов

6. Uniform-буферы

  1. Дескриптор layout и буфера
  2. Дескриптор пула и sets

7. Текстурирование

  1. Изображения
  2. Image view и image sampler
  3. Комбинированный image sampler

8. Буфер глубины

9. Загрузка моделей

10. Создание мип-карт

11. Multisampling

FAQ

Политика конфиденциальности


Непрограммируемые стадии конвейера



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

Входные данные вершин


Структура VkPipelineVertexInputStateCreateInfo описывает формат данных вершин, которые передаются в вершинный шейдер. Есть два типа описаний:

  • Описание атрибутов: тип данных, передаваемый в вершинный шейдер, привязка к буферу данных и смещение в нем
  • Привязка (binding): расстояние между элементами данных и то, каким образом связаны данные и выводимая геометрия (повершинная привязка или per-instance) (см. Geometry instancing)

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

VkPipelineVertexInputStateCreateInfo vertexInputInfo{};vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;vertexInputInfo.vertexBindingDescriptionCount = 0;vertexInputInfo.pVertexBindingDescriptions = nullptr; // OptionalvertexInputInfo.vertexAttributeDescriptionCount = 0;vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional

Члены pVertexBindingDescriptions и pVertexAttributeDescriptions указывают на массив структур, которые описывают вышеупомянутые данные для загрузки атрибутов вершин. Добавьте эту структуру в функцию createGraphicsPipeline сразу после shaderStages.

Input assembler


Структура VkPipelineInputAssemblyStateCreateInfo описывает 2 вещи: какая геометрия образуется из вершин и разрешен ли рестарт геометрии для таких геометрий, как line strip и triangle strip. Геометрия указывается в поле topology и может иметь следующие значения:

  • VK_PRIMITIVE_TOPOLOGY_POINT_LIST: геометрия отрисовывается в виде отдельных точек, каждая вершина отдельная точка
  • VK_PRIMITIVE_TOPOLOGY_LINE_LIST: геометрия отрисовывается в виде набора отрезков, каждая пара вершин образует отдельный отрезок
  • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: геометрия отрисовывается в виде непрерывной ломаной, каждая последующая вершина добавляет к ломаной один отрезок
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: геометрия отрисовывается как набор треугольников, причем каждые 3 вершины образуют независимый треугольник
  • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: геометрия отрисовывается как набор связанных треугольников, причем две последние вершины предыдущего треугольника используются в качестве двух первых вершин для следующего треугольника

Обычно вершины загружаются последовательно в том порядке, в котором вы их расположите в вершинном буфере. Однако с помощью индексного буфера вы можете изменить порядок загрузки. Это позволяет выполнить оптимизацию, например, повторно использовать вершины. Если в поле primitiveRestartEnable задать значение VK_TRUE, можно прервать отрезки и треугольники с топологией VK_PRIMITIVE_TOPOLOGY_LINE_STRIP и VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP и начать рисовать новые примитивы, используя специальный индекс 0xFFFF или 0xFFFFFFFF.

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

VkPipelineInputAssemblyStateCreateInfo inputAssembly{};inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;inputAssembly.primitiveRestartEnable = VK_FALSE;

Вьюпорт и scissors


Вьюпорт описывает область фреймбуфера, в которую рендерятся выходные данные. Почти всегда для вьюпорта задаются координаты от (0, 0) до (width, height).

VkViewport viewport{};viewport.x = 0.0f;viewport.y = 0.0f;viewport.width = (float) swapChainExtent.width;viewport.height = (float) swapChainExtent.height;viewport.minDepth = 0.0f;viewport.maxDepth = 1.0f;

Помните, что размер swap chain и images может отличаться от значений WIDTH и HEIGHT окна. Позже images из swap chain будут использоваться в качестве фреймбуферов, поэтому мы должны использовать именно их размер.

minDepth и maxDepth определяют диапазон значений глубины для фреймбуфера. Эти значения должны находиться в диапазоне [0,0f, 1,0f], при этом minDepth может быть больше maxDepth. Используйте стандартные значения 0.0f и 1.0f, если не собираетесь делать ничего необычного.

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



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

VkRect2D scissor{};scissor.offset = {0, 0};scissor.extent = swapChainExtent;

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

VkPipelineViewportStateCreateInfo viewportState{};viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;viewportState.viewportCount = 1;viewportState.pViewports = &viewport;viewportState.scissorCount = 1;viewportState.pScissors = &scissor;

Растеризатор


Растеризатор преобразует геометрию, полученную из вершинного шейдера, во множество фрагментов. Здесь также выполняется тест глубины, face culling, scissor тест и настраивается способ заполнения полигонов фрагментами: заполнение всего полигона, либо только ребра полигонов (каркасный рендеринг). Все это настраивается в структуре VkPipelineRasterizationStateCreateInfo.

VkPipelineRasterizationStateCreateInfo rasterizer{};rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;rasterizer.depthClampEnable = VK_FALSE;

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

rasterizer.rasterizerDiscardEnable = VK_FALSE;

Если для rasterizerDiscardEnable задать VK_TRUE, стадия растеризации отключается и выходные данные не передаются во фреймбуфер.

rasterizer.polygonMode = VK_POLYGON_MODE_FILL;

polygonMode определяет, каким образом генерируются фрагменты. Доступны следующие режимы:

  • VK_POLYGON_MODE_FILL: полигоны полностью заполняются фрагментами
  • VK_POLYGON_MODE_LINE: ребра полигонов преобразуются в отрезки
  • VK_POLYGON_MODE_POINT: вершины полигонов рисуются в виде точек

Для использования этих режимов, за исключением VK_POLYGON_MODE_FILL, нужно включить соответствующую опцию GPU.

rasterizer.lineWidth = 1.0f;

В поле lineWidth задается толщина отрезков. Максимальная поддерживаемая ширина отрезка зависит от вашего оборудования, а для отрезков толще 1,0f требуется включить опцию GPU wideLines.

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;

Параметр cullMode определяет тип отсечения (face culling). Вы можете совсем отключить отсечение, либо включить отсечение лицевых и/или нелицевых граней. Переменная frontFace определяет порядок обхода вершин (по часовой стрелке или против) для определения лицевых граней.

rasterizer.depthBiasEnable = VK_FALSE;rasterizer.depthBiasConstantFactor = 0.0f; // Optionalrasterizer.depthBiasClamp = 0.0f; // Optionalrasterizer.depthBiasSlopeFactor = 0.0f; // Optional

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

Мультисэмплинг


Структура VkPipelineMultisampleStateCreateInfo настраивает мультисэмплинг один из способов сглаживания (anti-aliasing). Он работает главным образом на краях, комбинируя цвета разных полигонов, которые растеризуются в одни и те же пиксели. Это позволяет избавиться от наиболее заметных артефактов. Основное преимущество мультисэмплинга в том, что фрагментный шейдер в большинстве случаев выполняется только один раз на пиксель, что гораздо лучше, например, чем рендеринг в большем разрешении с последующим уменьшением размеров. Чтобы использовать мультисэмплинг, необходимо включить соответствующую опцию GPU.

VkPipelineMultisampleStateCreateInfo multisampling{};multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;multisampling.sampleShadingEnable = VK_FALSE;multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;multisampling.minSampleShading = 1.0f; // Optionalmultisampling.pSampleMask = nullptr; // Optionalmultisampling.alphaToCoverageEnable = VK_FALSE; // Optionalmultisampling.alphaToOneEnable = VK_FALSE; // Optional

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

Тест глубины и тест трафарета


При использовании буфера глубины и/или трафаретного буфера нужно настроить их с помощью VkPipelineDepthStencilStateCreateInfo. У нас пока нет в этом необходимости, поэтому мы просто передадим nullptr вместо указателя на эту структуру. Мы вернемся к этому в главе, посвященной буферу глубины.

Смешивание цветов


Цвет, возвращаемый фрагментным шейдером, нужно объединить с цветом, уже находящимся во фреймбуфере. Этот процесс называется смешиванием цветов, и есть два способа его сделать:

  • Смешать старое и новое значение, чтобы получить выходной цвет
  • Объединить старое и новое значение с помощью побитовой операции

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

VkPipelineColorBlendAttachmentState colorBlendAttachment{};colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;colorBlendAttachment.blendEnable = VK_FALSE;colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // OptionalcolorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // OptionalcolorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // OptionalcolorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // OptionalcolorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // OptionalcolorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional

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

if (blendEnable) {    finalColor.rgb = (srcColorBlendFactor * newColor.rgb) <colorBlendOp> (dstColorBlendFactor * oldColor.rgb);    finalColor.a = (srcAlphaBlendFactor * newColor.a) <alphaBlendOp> (dstAlphaBlendFactor * oldColor.a);} else {    finalColor = newColor;}finalColor = finalColor & colorWriteMask;

Если для blendEnable установлено VK_FALSE, цвет из фрагментного шейдера передается без изменений. Если установлено VK_TRUE, для вычисления нового цвета используются две операции смешивания. Конечный цвет фильтруется с помощью colorWriteMask для определения, в какие каналы выходного изображения идет запись.

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

finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;finalColor.a = newAlpha.a;

Это может быть настроено с помощью следующих параметров:

colorBlendAttachment.blendEnable = VK_TRUE;colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;

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

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

VkPipelineColorBlendStateCreateInfo colorBlending{};colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;colorBlending.logicOpEnable = VK_FALSE;colorBlending.logicOp = VK_LOGIC_OP_COPY; // OptionalcolorBlending.attachmentCount = 1;colorBlending.pAttachments = &colorBlendAttachment;colorBlending.blendConstants[0] = 0.0f; // OptionalcolorBlending.blendConstants[1] = 0.0f; // OptionalcolorBlending.blendConstants[2] = 0.0f; // OptionalcolorBlending.blendConstants[3] = 0.0f; // Optional

Если вы хотите использовать второй способ смешивания (побитовая операция), установите VK_TRUE для logicOpEnable. После этого вы сможете указать побитовую операцию в поле logicOp. Обратите внимание, что первый способ автоматически становится недоступным, как если бы в каждом подключенном фреймбуфере для blendEnable было установлено VK_FALSE! Обратите внимание, colorWriteMask используется и для побитовых операций, чтобы определить, содержимое каких каналов будут изменено. Вы можете отключить оба режима, как это сделали мы, в этом случае цвета фрагментов будут записаны во фреймбуфер без изменений.

Динамическое состояние


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

VkDynamicState dynamicStates[] = {    VK_DYNAMIC_STATE_VIEWPORT,    VK_DYNAMIC_STATE_LINE_WIDTH};VkPipelineDynamicStateCreateInfo dynamicState{};dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;dynamicState.dynamicStateCount = 2;dynamicState.pDynamicStates = dynamicStates;

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

Layout конвейера


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

Эти uniform-переменные необходимо указать во время создания конвейера с помощью объекта VkPipelineLayout. Несмотря на то, что мы пока не будем использовать эти переменные, нам все равно нужно создать пустой layout конвейера.

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

VkPipelineLayout pipelineLayout;

Затем создадим объект в функции createGraphicsPipeline:

VkPipelineLayoutCreateInfo pipelineLayoutInfo{};pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;pipelineLayoutInfo.setLayoutCount = 0; // OptionalpipelineLayoutInfo.pSetLayouts = nullptr; // OptionalpipelineLayoutInfo.pushConstantRangeCount = 0; // OptionalpipelineLayoutInfo.pPushConstantRanges = nullptr; // Optionalif (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {    throw std::runtime_error("failed to create pipeline layout!");}

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

void cleanup() {    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);    ...}

Заключение


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

Для создания графического конвейера осталось создать последний объект проход рендера.
Подробнее..

3D teeth instance segmentation. В темноте, но не один

23.05.2021 14:09:40 | Автор: admin

3D сегментация зубов от поиска данных до конечного результата. Почти.

Дисклеймер

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

Об авторе

Добрый - всем, зовут Андрей(27). Постараюсь коротко. Почему программирование? По образованию - бакалавр электромеханик, профессию знаю. Отработал 2 года на должности инженера-энергетика в буровой компании вполне успешно, вместо повышения написал заявление - сгорел, да не по мне оказалось это всё. Нравится создавать, находить решения сложных задач, с ПК в обнимку с сознательных лет. Выбор очевиден. Вначале (полгода назад), всерьёз думал записаться на курсы от Я или подобные. Начитался отзывов, поговорил с участниками и понял что с получением информацией проблем нет. Так нашел сайт, там получил базу по Python и с ним уже начал свой путь (сейчас там постепенно изучаю всё, что связано с ML). Сразу заинтересовало машинное обучение, CV в частности. Придумал себе задачу и вот здесь (по мне, так отличный способ учиться).

1. Введение

В результате нескольких неудачных попыток, пришел к решению использовать 2 легковесные модели для получения желаемого результата. 1-ая сегментирует все зубы как [1, 0] категорию, а вторая делит их на категории[0, 8]. Но начнем по порядку.

2. Поиск и подготовка данных

Потратив не один вечер на поиск данных для работы, пришел в выводу что в свободном доступе челюсть в хорошем качестве и формате (*.stl, *.nrrd и т.д.) не получится. Лучшее, что мне попалось - это тестовый образец головы пациента после хирургической операции на челюсти в программе 3D Slicer.

Очевидно, мне не нужна голова целиком, поэтому обрезал исходник в той же программе до размера 163*112*120рх (в данном посте {x*y*z = ш-г-в} и 1рх - 0,5мм), оставив только зубы и сопутствующие челюстно-лицевые части.

Уже больше похоже на то что нужно, дальше - интереснее. Теперь нужно создать маски всех необходимых нам объектов. Для тех, кто уже работал с этим - "autothreshold" не то чтобы совсем не работает, просто лишнего много, думаю, исправление заняло бы столько же времени, сколько и разметка вручную(через маски).

- Пиксели(срезы слева)? - Вспоминаем размер изображения- Пиксели(срезы слева)? - Вспоминаем размер изображения

Размечал часов 12~14. И да, тот факт что я не сразу разметил каждый зуб как категорию стоил мне еще порядка 4 часов. В итоге у нас есть данные, с которыми у же можно работать.

Конечный вариант маски. Smooth 0.5. (сглаживание в обучении не использовалось)Конечный вариант маски. Smooth 0.5. (сглаживание в обучении не использовалось)

Должен добавить, даже на мой (без опыта) взгляд, этих данных очень мало для обучения и последующей полноценной работы нейронной сети. На данном этапе, единственное что пришло в голову, повернуть имеющиеся данные N-раз и соединить, random-crop использовать не стал.

Код подготовки данных
import nrrdimport torchimport torchvision.transforms as tfclass DataBuilder:    def __init__(self,                 data_path,                 list_of_categories,                 num_of_chunks: int = 0,                 augmentation_coeff: int = 0,                 num_of_classes: int = 0,                 normalise: bool = False,                 fit: bool = True,                 data_format: int = 0,                 save_data: bool = False                 ):        self.data_path = data_path        self.number_of_chunks = num_of_chunks        self.augmentation_coeff = augmentation_coeff        self.list_of_cats = list_of_categories        self.num_of_cls = num_of_classes        self.normalise = normalise        self.fit = fit        self.data_format = data_format        self.save_data = save_data    def forward(self):        data = self.get_data()        data = self.fit_data(data) if self.fit else data        data = self.pre_normalize(data) if self.normalise else data        data = self.data_augmentation(data, self.augmentation_coeff) if self.augmentation_coeff != 0 else data        data = self.new_chunks(data, self.number_of_chunks) if self.number_of_chunks != 0 else data        data = self.category_splitter(data, self.num_of_cls, self.list_of_cats) if self.num_of_cls != 0 else data        torch.save(data, self.data_path[-14:]+'.pt') if self.save_data else None        return torch.unsqueeze(data, 1)    def get_data(self):        if self.data_format == 0:            return torch.from_numpy(nrrd.read(self.data_path)[0])        elif self.data_format == 1:            return torch.load(self.data_path).cpu()        elif self.data_format == 2:            return torch.unsqueeze(self.data_path, 0).cpu()        else:            print('Available types are: "nrrd", "tensor" or "self.tensor(w/o load)"')    @staticmethod    def fit_data(some_data):        data = torch.movedim(some_data, (1, 0), (0, -1))        data_add_x = torch.nn.ZeroPad2d((5, 0, 0, 0))        data = data_add_x(data)        data = torch.movedim(data, -1, 0)        data_add_z = torch.nn.ZeroPad2d((0, 0, 8, 0))        return data_add_z(data)    @staticmethod    def pre_normalize(some_data):        min_d, max_d = torch.min(some_data), torch.max(some_data)        return (some_data - min_d) / (max_d - min_d)    @staticmethod    def data_augmentation(some_data, aug_n):        torch.manual_seed(17)        tr_data = []        for e in range(aug_n):            transform = tf.RandomRotation(degrees=(20*e, 20*e))            for image in some_data:                image = torch.unsqueeze(image, 0)                image = transform(image)                tr_data.append(image)        return tr_data    def new_chunks(self, some_data, n_ch):        data = torch.stack(some_data, 0) if self.augmentation_coeff != 0 else some_data        data = torch.squeeze(data, 1)        chunks = torch.chunk(data, n_ch, 0)        return torch.stack(chunks)    @staticmethod    def category_splitter(some_data, alpha, list_of_categories):        data, _ = torch.squeeze(some_data, 1).to(torch.int64), alpha        for i in list_of_categories:            data = torch.where(data < i, _, data)            _ += 1        return data - alpha

Имейте ввиду что это финальная версия кода подготовки данных для 3D U-net. Форвард:

  • Загружаем дату (в зависимости от типа).

  • Добавляем 0 по краям чтобы подогнать размер до 168*120*120 (вместо исходных 163*112*120). *пригодится дальше.

  • Нормализуем входящие данные в 0...1 (исходные ~-2000...16000).

  • Поворачиваем N-раз и соединяем.

  • Полученные данные режем на равные части чтобы забить память видеокарты по максимуму (в моем случае это 1, 1, 72, 120, 120).

  • Эта часть распределяет по категориям 28 имеющихся зубов и фон для облегчения обучения моделей (см. Введение):

    • одну категорию для 1-ой;

    • на 9 категорий (8+фон) для 2-ой.

Dataloader стандартный
import torch.utils.data as tudclass ToothDataset(tud.Dataset):    def __init__(self, images, masks):        self.images = images        self.masks = masks    def __len__(self): return len(self.images)    def __getitem__(self, index):        if self.masks is not None:            return self.images[index, :, :, :, :],\                    self.masks[index, :, :, :, :]        else:            return self.images[index, :, :, :, :]def get_loaders(images, masks,                batch_size: int = 1,                num_workers: int = 1,                pin_memory: bool = True):    train_ds = ToothDataset(images=images,                            masks=masks)    data_loader = tud.DataLoader(train_ds,                                 batch_size=batch_size,                                 shuffle=False,                                 num_workers=num_workers,                                 pin_memory=pin_memory)    return data_loader

На выходе имеем следующее:

Semantic

Instance

Predictions

Data

(27*, 1, 56*, 120,120)[0...1]

(27*, 1, 56*, 120,120) [0, 1]

(1, 1, 168, 120, 120)[0...1]

Masks

(27*, 1, 56*, 120,120)[0, 1]

(27*, 1, 56*, 120,120)[0, 8]

-

*эти размеры менялись, в зависимости от эксперимента, подробности - дальше.

3. Выбор и настройка моделей обучения

Цель работы - обучение. Поэтому взял наиболее простую и понятную для себя модель нейросети архитектуры U-Net. Код не выкладываю, можно посмотреть тут.

2D U-Net2D U-Net

Подробно рассказывать не буду, информации в достатке в сети. Метод оптимизации - Adam, функция расчета потерь Dice-loss(implement), спусков/подъемов 4, фильтры [64, 128, 256, 512] (знаю, много, об этом - позже). Обучал в среднем 60-80 epochs на эксперимент. Transfer learning не использовал.

model.summary()
model = UNet(dim=2, in_channels=1, out_channels=1, n_blocks=4, start_filters=64).to(device)print(summary(model, (1, 168, 120)))"""----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv2d-1         [-1, 64, 168, 120]             640              ReLU-2         [-1, 64, 168, 120]               0       BatchNorm2d-3         [-1, 64, 168, 120]             128            Conv2d-4         [-1, 64, 168, 120]          36,928              ReLU-5         [-1, 64, 168, 120]               0       BatchNorm2d-6         [-1, 64, 168, 120]             128         MaxPool2d-7           [-1, 64, 84, 60]               0         DownBlock-8  [[-1, 64, 84, 60], [-1, 64, 168, 120]]  0            Conv2d-9          [-1, 128, 84, 60]          73,856             ReLU-10          [-1, 128, 84, 60]               0      BatchNorm2d-11          [-1, 128, 84, 60]             256           Conv2d-12          [-1, 128, 84, 60]         147,584             ReLU-13          [-1, 128, 84, 60]               0      BatchNorm2d-14          [-1, 128, 84, 60]             256        MaxPool2d-15          [-1, 128, 42, 30]               0        DownBlock-16  [[-1, 128, 42, 30], [-1, 128, 84, 60]]  0           Conv2d-17          [-1, 256, 42, 30]         295,168             ReLU-18          [-1, 256, 42, 30]               0      BatchNorm2d-19          [-1, 256, 42, 30]             512           Conv2d-20          [-1, 256, 42, 30]         590,080             ReLU-21          [-1, 256, 42, 30]               0      BatchNorm2d-22          [-1, 256, 42, 30]             512        MaxPool2d-23          [-1, 256, 21, 15]               0        DownBlock-24  [[-1, 256, 21, 15], [-1, 256, 42, 30]]  0           Conv2d-25          [-1, 512, 21, 15]       1,180,160             ReLU-26          [-1, 512, 21, 15]               0      BatchNorm2d-27          [-1, 512, 21, 15]           1,024           Conv2d-28          [-1, 512, 21, 15]       2,359,808             ReLU-29          [-1, 512, 21, 15]               0      BatchNorm2d-30          [-1, 512, 21, 15]           1,024        DownBlock-31  [[-1, 512, 21, 15], [-1, 512, 21, 15]]  0  ConvTranspose2d-32          [-1, 256, 42, 30]         524,544             ReLU-33          [-1, 256, 42, 30]               0      BatchNorm2d-34          [-1, 256, 42, 30]             512      Concatenate-35          [-1, 512, 42, 30]               0           Conv2d-36          [-1, 256, 42, 30]       1,179,904             ReLU-37          [-1, 256, 42, 30]               0      BatchNorm2d-38          [-1, 256, 42, 30]             512           Conv2d-39          [-1, 256, 42, 30]         590,080             ReLU-40          [-1, 256, 42, 30]               0      BatchNorm2d-41          [-1, 256, 42, 30]             512          UpBlock-42          [-1, 256, 42, 30]               0  ConvTranspose2d-43          [-1, 128, 84, 60]         131,200             ReLU-44          [-1, 128, 84, 60]               0      BatchNorm2d-45          [-1, 128, 84, 60]             256      Concatenate-46          [-1, 256, 84, 60]               0           Conv2d-47          [-1, 128, 84, 60]         295,040             ReLU-48          [-1, 128, 84, 60]               0      BatchNorm2d-49          [-1, 128, 84, 60]             256           Conv2d-50          [-1, 128, 84, 60]         147,584             ReLU-51          [-1, 128, 84, 60]               0      BatchNorm2d-52          [-1, 128, 84, 60]             256          UpBlock-53          [-1, 128, 84, 60]               0  ConvTranspose2d-54         [-1, 64, 168, 120]          32,832             ReLU-55         [-1, 64, 168, 120]               0      BatchNorm2d-56         [-1, 64, 168, 120]             128      Concatenate-57        [-1, 128, 168, 120]               0           Conv2d-58         [-1, 64, 168, 120]          73,792             ReLU-59         [-1, 64, 168, 120]               0      BatchNorm2d-60         [-1, 64, 168, 120]             128           Conv2d-61         [-1, 64, 168, 120]          36,928             ReLU-62         [-1, 64, 168, 120]               0      BatchNorm2d-63         [-1, 64, 168, 120]             128          UpBlock-64         [-1, 64, 168, 120]               0           Conv2d-65          [-1, 1, 168, 120]              65================================================================Total params: 7,702,721Trainable params: 7,702,721Non-trainable params: 0----------------------------------------------------------------Input size (MB): 0.08Forward/backward pass size (MB): 7434.08Params size (MB): 29.38Estimated Total Size (MB): 7463.54"""
Эксп.12D U-Net, подача изображений покадрово, плоскость [x, z]Эксп.12D U-Net, подача изображений покадрово, плоскость [x, z]

Определенно, это - зубы. Только кроме зубов есть много всего, нам ненужного. Подробнее о трансформации numpy - *.stl в Главе 6. Посмотрим ещё раз на фактический размер и качество изображений, которые попадают на вход нейросети:

Слева на право:1. Не видно[x, y]. 2. Немного лучше[x, z]. 3.Ещё лучше[y, z]Слева на право:1. Не видно[x, y]. 2. Немного лучше[x, z]. 3.Ещё лучше[y, z]

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

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

Эксп.2Каскад 2-ух 2D U-Net, подача изображений покадрово, плоскость [y, z]Эксп.2Каскад 2-ух 2D U-Net, подача изображений покадрово, плоскость [y, z]

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

Эксп.3Каскад 2-ух 2D U-Net, подача изображений покадрово плоскость [y, z]с увеличением времени обучения на 50%Эксп.3Каскад 2-ух 2D U-Net, подача изображений покадрово плоскость [y, z]с увеличением времени обучения на 50%

Ввиду последних событий было принято решение о переходе на 3D архитектуру нейронной сети. Переподготовил входные данные, а именно разделил на части размером (24*, 120, 120). Почему так? - изначально большая модель обучения (~22млн. параметров). Моя видеокарта(1063gtx) не могла физически вместить больше.

24*

Это размер глубины. Был подобран так чтобы:

  • количество данных(1512, 120, 120) делится нацело на это число - получается 63;

  • в свою очередь получившийся batch size (24, 120, 120) - максимум, вмещающийся в память видеокарты с текущими параметрами сети;

  • само это число (24) делилось на количество спусков/подъемов так же нацело (имеется в виду соответствие выражению 24/2/2/2=3 и 3*2*2*2=24, где количество делений/умножений на 2 соответствует количеству спусков/подъемов минус 1);

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

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=1, n_blocks=4, start_filters=64).to(device)print(summary(model, (1, 24, 120, 120)))"""  ----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1     [-1, 64, 24, 120, 120]             1,792              ReLU-2     [-1, 64, 24, 120, 120]                 0       BatchNorm3d-3     [-1, 64, 24, 120, 120]               128            Conv3d-4     [-1, 64, 24, 120, 120]           110,656              ReLU-5     [-1, 64, 24, 120, 120]                 0       BatchNorm3d-6     [-1, 64, 24, 120, 120]               128         MaxPool3d-7        [-1, 64, 12, 60, 60]                0         DownBlock-8  [[-1, 64, 12, 60, 60], [-1, 64, 24, 120, 120]]               0            Conv3d-9       [-1, 128, 12, 60, 60]          221,312             ReLU-10       [-1, 128, 12, 60, 60]                0      BatchNorm3d-11       [-1, 128, 12, 60, 60]              256           Conv3d-12       [-1, 128, 12, 60, 60]          442,496             ReLU-13       [-1, 128, 12, 60, 60]                0      BatchNorm3d-14       [-1, 128, 12, 60, 60]              256        MaxPool3d-15       [-1, 128, 6, 30, 30]                 0        DownBlock-16  [[-1, 128, 6, 30, 30], [-1, 128, 12, 60, 60]]               0           Conv3d-17       [-1, 256, 6, 30, 30]           884,992             ReLU-18       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-19       [-1, 256, 6, 30, 30]               512           Conv3d-20       [-1, 256, 6, 30, 30]         1,769,728             ReLU-21       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-22       [-1, 256, 6, 30, 30]               512        MaxPool3d-23       [-1, 256, 3, 15, 15]                 0        DownBlock-24  [[-1, 256, 3, 15, 15], [-1, 256, 6, 30, 30]]               0           Conv3d-25       [-1, 512, 3, 15, 15]         3,539,456             ReLU-26       [-1, 512, 3, 15, 15]                 0      BatchNorm3d-27       [-1, 512, 3, 15, 15]             1,024           Conv3d-28       [-1, 512, 3, 15, 15]         7,078,400             ReLU-29       [-1, 512, 3, 15, 15]                 0      BatchNorm3d-30       [-1, 512, 3, 15, 15]             1,024        DownBlock-31  [[-1, 512, 3, 15, 15], [-1, 512, 3, 15, 15]]               0  ConvTranspose3d-32       [-1, 256, 6, 30, 30]         1,048,832             ReLU-33       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-34       [-1, 256, 6, 30, 30]               512      Concatenate-35       [-1, 512, 6, 30, 30]                 0           Conv3d-36       [-1, 256, 6, 30, 30]         3,539,200             ReLU-37       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-38       [-1, 256, 6, 30, 30]               512           Conv3d-39       [-1, 256, 6, 30, 30]         1,769,728             ReLU-40       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-41       [-1, 256, 6, 30, 30]               512          UpBlock-42       [-1, 256, 6, 30, 30]                 0  ConvTranspose3d-43       [-1, 128, 12, 60, 60]          262,272             ReLU-44       [-1, 128, 12, 60, 60]                0      BatchNorm3d-45       [-1, 128, 12, 60, 60]              256      Concatenate-46       [-1, 256, 12, 60, 60]                0           Conv3d-47       [-1, 128, 12, 60, 60]          884,864             ReLU-48       [-1, 128, 12, 60, 60]                0      BatchNorm3d-49       [-1, 128, 12, 60, 60]              256           Conv3d-50       [-1, 128, 12, 60, 60]          442,496             ReLU-51       [-1, 128, 12, 60, 60]                0      BatchNorm3d-52       [-1, 128, 12, 60, 60]              256          UpBlock-53       [-1, 128, 12, 60, 60]                0  ConvTranspose3d-54       [-1, 64, 24, 120, 120]          65,600             ReLU-55       [-1, 64, 24, 120, 120]               0      BatchNorm3d-56       [-1, 64, 24, 120, 120]             128      Concatenate-57      [-1, 128, 24, 120, 120]               0           Conv3d-58       [-1, 64, 24, 120, 120]         221,248             ReLU-59       [-1, 64, 24, 120, 120]               0      BatchNorm3d-60       [-1, 64, 24, 120, 120]             128           Conv3d-61       [-1, 64, 24, 120, 120]         110,656             ReLU-62       [-1, 64, 24, 120, 120]               0      BatchNorm3d-63       [-1, 64, 24, 120, 120]             128          UpBlock-64       [-1, 64, 24, 120, 120]               0           Conv3d-65        [-1, 1, 24, 120, 120]              65================================================================Total params: 22,400,321Trainable params: 22,400,321Non-trainable params: 0----------------------------------------------------------------Input size (MB): 0.61Forward/backward pass size (MB): 15974.12Params size (MB): 85.45Estimated Total Size (MB): 16060.18----------------------------------------------------------------"""
Эксп.43D U-Net, подача объемом, плоскость [y, z],время*0,38Эксп.43D U-Net, подача объемом, плоскость [y, z],время*0,38

С учетом сокращенного на ~60% времени обучения(25 epochs) результат меня устроил, продолжаем.

Эксп.53D U-Net, подача объемом, плоскость [y, z], 65 epochs ~ 1,5 часа Эксп.53D U-Net, подача объемом, плоскость [y, z], 65 epochs ~ 1,5 часа

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

Эксп.63D U-Net, подача объемом, плоскость [x, z], 105 epochs ~ 2,1 часа Эксп.63D U-Net, подача объемом, плоскость [x, z], 105 epochs ~ 2,1 часа

"Научный" перебор параметров в течении недели принес результат. Уменьшил количество параметров сети до ~400к (от первоначальных ~22м) путем уменьшения фильтра [18, 32, 64, 128] и спуска/подъема до 3. Изменил метод оптимизации на RSMProp. Уменьшение количества параметров нейросети позволило увеличить объем входных данных в три раза (1, 1, 72*, 120, 120). Посмотрим результат?

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=1, n_blocks=3, start_filters=18).to(device)print(summary(model, (1, 1, 72, 120, 120)))"""----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1     [-1, 18, 72, 120, 120]             504              ReLU-2     [-1, 18, 72, 120, 120]               0       BatchNorm3d-3     [-1, 18, 72, 120, 120]              36            Conv3d-4     [-1, 18, 72, 120, 120]           8,766              ReLU-5     [-1, 18, 72, 120, 120]               0       BatchNorm3d-6     [-1, 18, 72, 120, 120]              36         MaxPool3d-7       [-1, 18, 36, 60, 60]               0         DownBlock-8  [[-1, 18, 36, 60, 60], [-1, 18, 24, 120, 120]]               0            Conv3d-9       [-1, 36, 36, 60, 60]          17,532             ReLU-10       [-1, 36, 36, 60, 60]               0      BatchNorm3d-11       [-1, 36, 36, 60, 60]              72           Conv3d-12       [-1, 36, 36, 60, 60]          35,028             ReLU-13       [-1, 36, 36, 60, 60]               0      BatchNorm3d-14       [-1, 36, 36, 60, 60]              72        MaxPool3d-15        [-1, 36, 18, 30, 30]              0        DownBlock-16  [[-1, 36, 18, 30, 30], [-1, 36, 36, 60, 60]]               0           Conv3d-17        [-1, 72, 18, 30, 30]         70,056             ReLU-18        [-1, 72, 18, 30, 30]              0      BatchNorm3d-19        [-1, 72, 18, 30, 30]            144           Conv3d-20        [-1, 72, 18, 30, 30]        140,040             ReLU-21        [-1, 72, 18, 30, 30]              0      BatchNorm3d-22        [-1, 72, 18, 30, 30]            144        DownBlock-23  [[-1, 72, 18, 30, 30], [-1, 72, 18, 30, 30]]               0  ConvTranspose3d-24       [-1, 36, 36, 60, 60]          20,772             ReLU-25       [-1, 36, 36, 60, 60]               0      BatchNorm3d-26       [-1, 36, 36, 60, 60]              72      Concatenate-27       [-1, 72, 36, 60, 60]               0           Conv3d-28       [-1, 36, 36, 60, 60]          70,020             ReLU-29       [-1, 36, 36, 60, 60]               0      BatchNorm3d-30       [-1, 36, 36, 60, 60]              72           Conv3d-31       [-1, 36, 36, 60, 60]          35,028             ReLU-32       [-1, 36, 36, 60, 60]               0      BatchNorm3d-33       [-1, 36, 36, 60, 60]              72          UpBlock-34       [-1, 36, 36, 60, 60]               0  ConvTranspose3d-35     [-1, 18, 72, 120, 120]           5,202             ReLU-36     [-1, 18, 72, 120, 120]               0      BatchNorm3d-37     [-1, 18, 72, 120, 120]              36      Concatenate-38     [-1, 36, 72, 120, 120]               0           Conv3d-39     [-1, 18, 72, 120, 120]          17,514             ReLU-40     [-1, 18, 72, 120, 120]               0      BatchNorm3d-41     [-1, 18, 72, 120, 120]              36           Conv3d-42     [-1, 18, 72, 120, 120]           8,766             ReLU-43     [-1, 18, 72, 120, 120]               0      BatchNorm3d-44     [-1, 18, 72, 120, 120]              36          UpBlock-45     [-1, 18, 72, 120, 120]               0           Conv3d-46      [-1, 1, 72, 120, 120]              19================================================================Total params: 430,075Trainable params: 430,075Non-trainable params: 0----------------------------------------------------------------Input size (MB): 1.32Forward/backward pass size (MB): 5744.38Params size (MB): 1.64Estimated Total Size (MB): 5747.34----------------------------------------------------------------"""
72*

Некоторые из вас подумают, исходные данные (168, 120, 120), а часть (72, 120, 120). Назревает вопрос, как делить. Всё просто, во 2 главе мы увеличивали размер наших данных и затем делили их на части, соответствующие объему памяти видеокарты. Я увеличил данные в 9 раз (1512, 120, 120) т.е. повернул на 9 различных углов относительно одной оси, а затем разделил на 21(batch size) часть по (72, 120, 120). Так же 72 соответствует всем условиям, описанным в 24*(выше).

Эксп.73D U-Net, подача объемом, плоскость [x, z],Маска (слева) и готовая сегментация (справа),оптимизированные параметры сети,время обучения(65 epochs) ~ 14мин.Эксп.73D U-Net, подача объемом, плоскость [x, z],Маска (слева) и готовая сегментация (справа),оптимизированные параметры сети,время обучения(65 epochs) ~ 14мин.

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

О размере подаваемых данных

Первоначальная идея при переходе на 3D архитектуру была в том чтобы делить данные не слайсами (как в данном посте) (1512, 120, 120) --> 21*(1, 72, 120, 120), а кубиками ~х*(30, 30, 30) или около того (результат этой попытки не был сохранен оп понятным причинам). Опытным путем понял 2 вещи: чем большими порциями ты подаешь 3-х мерные объекты, тем лучше результат(для моего конкретного случая); и нужно больше изучать теорию того, с чем работаешь.

О времени обучения и размере модели

Параметры сети подобраны так, что обучение 1 epochs на моей "старушке" занимает ~13сек, а размер конечной модели не превышает 2мб (прошлая>80мб). Время рабочего цикла примерно равно 1 epochs. Однако стоит понимать, это обучение и работа на данных достаточно маленького размера.

Для разделения на категории пришлось немного повозиться с функцией расчета ошибки и визуализацией данных. Первоначально поставил себе задачу разделить на 8 категорий + фон. О loss function и визуализации поговорим подробнее.

Код training loop
import torchfrom tqdm import tqdmfrom _loss_f import LossFunctionclass TrainFunction:    def __init__(self,                 data_loader,                 device_for_training,                 model_name,                 model_name_pretrained,                 model,                 optimizer,                 scale,                 learning_rate: int = 1e-2,                 num_epochs: int = 1,                 transfer_learning: bool = False,                 binary_loss_f: bool = True                 ):        self.data_loader = data_loader        self.device = device_for_training        self.model_name_pretrained = model_name_pretrained        self.semantic_binary = binary_loss_f        self.num_epochs = num_epochs        self.model_name = model_name        self.transfer = transfer_learning        self.optimizer = optimizer        self.learning_rate = learning_rate        self.model = model        self.scale = scale    def forward(self):        print('Running on the:', torch.cuda.get_device_name(self.device))        self.model.load_state_dict(torch.load(self.model_name_pretrained)) if self.transfer else None        optimizer = self.optimizer(self.model.parameters(), lr=self.learning_rate)        for epoch in range(self.num_epochs):            self.train_loop(self.data_loader, self.model, optimizer, self.scale, epoch)            torch.save(self.model.state_dict(), 'models/' + self.model_name+str(epoch+1)                       + '_epoch.pth') if (epoch + 1) % 10 == 0 else None    def train_loop(self, loader, model, optimizer, scales, i):        loop, epoch_loss = tqdm(loader), 0        loop.set_description('Epoch %i' % (self.num_epochs - i))        for batch_idx, (data, targets) in enumerate(loop):            data, targets = data.to(device=self.device, dtype=torch.float), \                            targets.to(device=self.device, dtype=torch.long)            optimizer.zero_grad()            *тут секрет*            with torch.cuda.amp.autocast():                predictions = model(data)                loss = LossFunction(predictions, targets,                                    device_for_training=self.device,                                    semantic_binary=self.semantic_binary                                    ).forward()            scales.scale(loss).backward()            scales.step(optimizer)            scales.update()            epoch_loss += (1 - loss.item())*100            loop.set_postfix(loss=loss.item())        print('Epoch-acc', round(epoch_loss / (batch_idx+1), 2))

4. Функция расчета ошибки

Мне в целом понравилось как проявляет себя Dice-loss в сегментации, только 'проблема' в том что он работает с форматом данных [0, 1]. Однако, если предварительно разделить данные на категории (а так же привести к формату [0, 1]), и пропускать пары (имеется ввиду "предсказание" и "маска" только одной категории) в стандартную Dice-loss функцию, то это может сработать.

Код categorical_dice_loss
import torchclass LossFunction:    def __init__(self,                 prediction,                 target,                 device_for_training,                 semantic_binary: bool = True,                 ):        self.prediction = prediction        self.device = device_for_training        self.target = target        self.semantic_binary = semantic_binary    def forward(self):        if self.semantic_binary:            return self.dice_loss(self.prediction, self.target)        return self.categorical_dice_loss(self.prediction, self.target)    @staticmethod    def dice_loss(predictions, targets, alpha=1e-5):        intersection = 2. * (predictions * targets).sum()        denomination = (torch.square(predictions) + torch.square(targets)).sum()        dice_loss = 1 - torch.mean((intersection + alpha) / (denomination + alpha))        return dice_loss    def categorical_dice_loss(self, prediction, target):        pr, tr = self.prepare_for_multiclass_loss_f(prediction, target)        target_categories, losses = torch.unique(tr).tolist(), 0        for num_category in target_categories:            categorical_target = torch.where(tr == num_category, 1, 0)            categorical_prediction = pr[num_category][:][:][:]            losses += self.dice_loss(categorical_prediction, categorical_target).to(self.device)        return losses / len(target_categories)    @staticmethod    def prepare_for_multiclass_loss_f(prediction, target):        prediction_prepared = torch.squeeze(prediction, 0)        target_prepared = torch.squeeze(target, 0)        target_prepared = torch.squeeze(target_prepared, 0)        return prediction_prepared, target_prepared

Тут просто, но всё равно объясню "categorical_dice_loss":

  • подготовка данных (убираем ненужные в данном расчете измерения);

  • получения списка категорий, которые содержит каждый batch масок;

  • для каждой категории берем "прогноз" и "маску" соответствующих категорий, приводим значения к формату [0, 1] и пропускаем через стандартную Dice-loss;

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

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

5. Визуализация данных

Так и хочется добавить "..как отдельный вид искусства". Начну с того что прочитать *.nrrd оказалось самым простым.

Код
import nrrd# читает в numpyread = nrrd.read(data_path) data, meta_data = read[0], read[1]print(data.shape, np.max(data), np.min(data), meta_data, sep="\n")(163, 112, 120)14982-2254  OrderedDict([('type', 'short'), ('dimension', 3), ('space', 'left-posterior-superior'), ('sizes', array([163, 112, 120])), ('space directions', array([[-0.5,  0. ,  0. ],       [ 0. , -0.5,  0. ],       [ 0. ,  0. ,  0.5]])), ('kinds', ['domain', 'domain', 'domain']), ('endian', 'little'), ('encoding', 'gzip'), ('space origin', array([131.57200623,  80.7661972 ,  32.29940033]))])

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

Неправильный путь

Иными словами, чтобы сделать куб нам необходимо 8 вершин и 12 треугольных поверхностей. В этом и состояла первая идея (до применения специальных библиотек) - заменить все пиксели (числа в 3-х мерной матрице) на такие кубики. Код я не сохранил, но смысл прост, рисуем куб на месте "пикселя" со сдвигом -1 по трем направлениям, потом следующий и т.д.

Выглядит это так же бредово, как и звучитВыглядит это так же бредово, как и звучит

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

from skimage.measure import marching_cubesimport nrrdimport numpy as npfrom stl import meshpath = 'some_path.nrrd'data = nrrd.read(path)[0]def three_d_creator(some_data):    vertices, faces, volume, _ = marching_cubes(some_data)    cube = mesh.Mesh(np.full(faces.shape[0], volume.shape[0], dtype=mesh.Mesh.dtype))    for i, f in enumerate(faces):        for j in range(3):            cube.vectors[i][j] = vertices[f[j]]    cube.save('name.stl')    return cubestl = three_d_creator(datas)

Пользовался этим способом, но иногда файлы "ломались" в процессе сохранения и не открывались. А на те, которые открывались, ругался встроенный в Win 10 3D Builder и постоянно пытался там что-то исправить. Так же еще придется "прикрутить" к коду модуль для просмотра 3D объектов без их сохранения. Решение "из коробки" дальше.

На момент написания статью пользуюсь v3do. Коротко, быстро, удобно и можно сразу осмотреть модель.

Код перевода npy в stl и вывода объекта на дисплей
from vedo import Volume, show, writeprediction = 'some_data_path.npy'def show_save(data, save=False):    data_multiclass = Volume(data, c='Set2', alpha=(0.1, 1), alphaUnit=0.87, mode=1)    data_multiclass.addScalarBar3D(nlabels=9)    show([(data_multiclass, "Multiclass teeth segmentation prediction")], bg='black', N=1, axes=1).close()    write(data_multiclass.isosurface(), 'some_name_.stl') if save else None    show_save(prediction, save=True)

Названия функций говорят сами за себя.

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

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=9, n_blocks=3, start_filters=9).to(device)print(summary(model, (1, 168*, 120, 120)))    """----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1      [-1, 9, 168, 120, 120]            252              ReLU-2      [-1, 9, 168, 120, 120]              0       BatchNorm3d-3      [-1, 9, 168, 120, 120]             18            Conv3d-4      [-1, 9, 168, 120, 120]          2,196              ReLU-5      [-1, 9, 168, 120, 120]              0       BatchNorm3d-6      [-1, 9, 168, 120, 120]             18         MaxPool3d-7        [-1, 9, 84, 60, 60]               0         DownBlock-8  [[-1, 9, 84, 60, 60], [-1, 9, 168, 120, 120]]               0            Conv3d-9       [-1, 18, 84, 60, 60]           4,392             ReLU-10       [-1, 18, 84, 60, 60]               0      BatchNorm3d-11       [-1, 18, 84, 60, 60]              36           Conv3d-12       [-1, 18, 84, 60, 60]           8,766             ReLU-13       [-1, 18, 84, 60, 60]               0      BatchNorm3d-14       [-1, 18, 84, 60, 60]              36        MaxPool3d-15       [-1, 18, 42, 30, 30]               0        DownBlock-16  [[-1, 18, 18, 42, 30], [-1, 18, 84, 60, 60]]               0           Conv3d-17       [-1, 36, 42, 30, 30]          17,532             ReLU-18       [-1, 36, 42, 30, 30]               0      BatchNorm3d-19       [-1, 36, 42, 30, 30]              72           Conv3d-20       [-1, 36, 42, 30, 30]          35,028             ReLU-21       [-1, 36, 42, 30, 30]               0      BatchNorm3d-22       [-1, 36, 42, 30, 30]              72        DownBlock-23  [[-1, 36, 42, 30, 30], [-1, 36, 42, 30, 30]]               0  ConvTranspose3d-24       [-1, 18, 84, 60, 60]           5,202             ReLU-25       [-1, 18, 84, 60, 60]               0      BatchNorm3d-26       [-1, 18, 84, 60, 60]              36      Concatenate-27       [-1, 36, 84, 60, 60]               0           Conv3d-28       [-1, 18, 84, 60, 60]          17,514             ReLU-29       [-1, 18, 84, 60, 60]               0      BatchNorm3d-30       [-1, 18, 84, 60, 60]              36           Conv3d-31       [-1, 18, 84, 60, 60]           8,766             ReLU-32       [-1, 18, 84, 60, 60]               0      BatchNorm3d-33       [-1, 18, 84, 60, 60]              36          UpBlock-34       [-1, 18, 84, 60, 60]               0  ConvTranspose3d-35      [-1, 9, 168, 120, 120]          1,305             ReLU-36      [-1, 9, 168, 120, 120]              0      BatchNorm3d-37      [-1, 9, 168, 120, 120]             18      Concatenate-38     [-1, 18, 168, 120, 120]              0           Conv3d-39      [-1, 9, 168, 120, 120]          4,383             ReLU-40      [-1, 9, 168, 120, 120]              0      BatchNorm3d-41      [-1, 9, 168, 120, 120]             18           Conv3d-42      [-1, 9, 168, 120, 120]          2,196             ReLU-43      [-1, 9, 168, 120, 120]              0      BatchNorm3d-44      [-1, 9, 168, 120, 120]             18          UpBlock-45      [-1, 9, 168, 120, 120]              0           Conv3d-46      [-1, 9, 168, 120, 120]             90================================================================Total params: 108,036Trainable params: 108,036Non-trainable params: 0----------------------------------------------------------------Input size (MB): 3.96Forward/backward pass size (MB): 12170.30Params size (MB): 0.41Estimated Total Size (MB): 12174.66----------------------------------------------------------------    """

*Ввиду ещё большего уменьшения параметров сети(фильтр[9, 18, 36, 72]), удалось уместить объект в память видеокарты целиком - 9*(168, 120, 120)

6. After words

Думал, что закончил, а оказалось - только начал. Тут еще есть над чем поработать. Мне, в целом, 2 этап не нравится, хоть он и работает. Зачем заново переопределять каждый пиксель, когда мне нужен целый регион? А если, образно, есть 28 разделенных регионов, зачем мне пытаться определить их все, не проще ли определить один зуб и завязать это всё на "условный" ориентированный/неориентированный граф? Или вместо U-net использовать GCNN и вместо Pytorch - Pytorch3D? Пятна, думаю, можно убрать с помощью выравнивания данных внутри bounding box(ведь один зуб может принадлежать только 1 категории). Но, возможно, это вопросы для следующей публикации.

Прототип (набросок)
Тот самый "условный граф"
Пример неориентированного графа на 28 категорий с "разделителями"Пример неориентированного графа на 28 категорий с "разделителями"

Отдельное спасибо моей жене - Алёне, за особую поддержку во время этого "погружения в темноту".

Благодарю всех за внимание. Конструктивная критика и предложения, как исправлений, так и новых проектов - приветствуются.

Подробнее..

Перевод Vulkan. Руководство разработчика. Проходы рендера (Render passes)

04.06.2021 18:05:38 | Автор: admin
Меня зовут Александра, я работаю в IT-компании CG Tribe в Ижевске и занимаюсь переводом Vulkan Tutorial на русский язык (ссылка на источник vulkan-tutorial.com).

Сегодня хочу поделиться переводом заключительных глав раздела, посвященного графическому конвейеру (Graphics pipeline basics), Render passes и Conclusion.

Содержание
1. Вступление

2. Краткий обзор

3. Настройка окружения

4. Рисуем треугольник

  1. Подготовка к работе
  2. Отображение на экране
  3. Графический конвейер (pipeline)
  4. Отрисовка
  5. Повторное создание цепочки показа

5. Буферы вершин

  1. Описание
  2. Создание буфера вершин
  3. Staging буфер
  4. Буфер индексов

6. Uniform-буферы

  1. Дескриптор layout и буфера
  2. Дескриптор пула и sets

7. Текстурирование

  1. Изображения
  2. Image view и image sampler
  3. Комбинированный image sampler

8. Буфер глубины

9. Загрузка моделей

10. Создание мип-карт

11. Multisampling

FAQ

Политика конфиденциальности


Проходы рендера




Подготовка


Прежде чем завершить создание графического конвейера нужно сообщить Vulkan, какие буферы (attachments) будут использоваться во время рендеринга. Необходимо указать, сколько будет буферов цвета, буферов глубины и сэмплов для каждого буфера. Также нужно указать, как должно обрабатываться содержимое буферов во время рендеринга. Вся эта информация обернута в объект прохода рендера (render pass), для которого мы создадим новую функцию createRenderPass. Вызовем эту функцию из initVulkan перед createGraphicsPipeline.

void initVulkan() {    createInstance();    setupDebugMessenger();    createSurface();    pickPhysicalDevice();    createLogicalDevice();    createSwapChain();    createImageViews();    createRenderPass();    createGraphicsPipeline();}...void createRenderPass() {}


Настройка буферов (attachments)


Мы используем только один цветовой буфер, представленный одним из images в swap chain.

void createRenderPass() {    VkAttachmentDescription colorAttachment{};    colorAttachment.format = swapChainImageFormat;    colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;}

Формат цветового буфера (поле format) должен соответствовать формату image из swap chain, и поскольку мы пока не задействуем мультисэмплинг, нам понадобится только 1 сэмпл.

colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;

loadOp и storeOp указывают, что делать с данными буфера перед рендерингом и после него. Для loadOp возможны следующие значения:

  • VK_ATTACHMENT_LOAD_OP_LOAD: буфер будет содержать те данные, которые были помещены в него до этого прохода (например, во время предыдущего прохода)
  • VK_ATTACHMENT_LOAD_OP_CLEAR: буфер очищается в начале прохода рендера
  • VK_ATTACHMENT_LOAD_OP_DONT_CARE: содержимое буфера не определено; для нас оно не имеет значения

Мы будем использовать VK_ATTACHMENT_LOAD_OP_CLEAR, чтобы заполнить фреймбуфер черным цветом перед отрисовкой нового фрейма.

Для storeOp возможны только два значения:

  • VK_ATTACHMENT_STORE_OP_STORE: содержимое буфера сохраняется в память для дальнейшего использования
  • VK_ATTACHMENT_STORE_OP_DONT_CARE: после рендеринга буфер больше не используется, и его содержимое не имеет значения

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

colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;

loadOp и storeOp применяются к буферам цвета и глубины. Для буфера трафарета используются поля stencilLoadOp/stencilStoreOp. Мы не используем буфер трафарета, поэтому результаты загрузки и сохранения нас не интересуют.

colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

Текстуры и фреймбуферы в Vulkan это объекты VkImage с определенным форматом пикселей, однако layout пикселей в памяти может меняться в зависимости от того, что вы хотите сделать с image.

Вот некоторые из наиболее распространенных layout-ов:

  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: images используются в качестве цветового буфера
  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: images используются для показа на экране
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: image принимает данные во время операций копирования

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

В initialLayout указывается layout, в котором будет image перед началом прохода рендера. В finalLayout указывается layout, в который image будет автоматически переведен после завершения прохода рендера. Значение VK_IMAGE_LAYOUT_UNDEFINED в поле initialLayout обозначает, что нас не интересует предыдущий layout, в котором был image. Использование этого значения не гарантирует сохранение содержимого image, но это и не важно, поскольку мы все равно очистим его. После рендеринга нам нужно вывести наш image на экран, поэтому в поле finalLayout укажем VK_IMAGE_LAYOUT_PRESENT_SRC_KHR.

Подпроходы (subpasses)


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

Каждый подпроход ссылается на один или несколько attachment-ов. Эти отсылки представляют собой структуры VkAttachmentReference:

VkAttachmentReference colorAttachmentRef{};colorAttachmentRef.attachment = 0;colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

В поле attachment указывается порядковый номер буфера в массиве, на который ссылается подпроход. Наш массив состоит только из одного буфера VkAttachmentDescription, его индекс равен 0. В поле layout мы указываем layout буфера во время подпрохода, ссылающегося на этот буфер. Vulkan автоматически переведет буфер в этот layout, когда начнется подпроход. Мы используем attachment в качестве буфера цвета, и layout VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL обеспечит нам самую высокую производительность.

Подпроход описывается с помощью структуры VkSubpassDescription:

VkSubpassDescription subpass{};subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;

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

subpass.colorAttachmentCount = 1;subpass.pColorAttachments = &colorAttachmentRef;

Директива layout(location = 0) out vec4 outColor ссылается именно на порядковый номер буфера в массиве subpass.pColorAttachments.

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

  • pInputAttachments: буферы, содержимое которых читается из шейдера
  • pResolveAttachments: буферы, которые используются для цветовых буферов с мультисэмплингом
  • pDepthStencilAttachment: буферы глубины и трафарета
  • pPreserveAttachments: буферы, которые не используются в текущем подпроходе, но данные которых должны быть сохранены


Проход рендера (render pass)


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

VkRenderPass renderPass;VkPipelineLayout pipelineLayout;

Теперь создадим объект прохода рендера. Для этого заполним структуру VkRenderPassCreateInfo массивом буферов и подпроходами рендера. Обратите внимание, объекты VkAttachmentReference используют индексы из этого массива (прим. переводчика: видимо, имеется в виду массив renderPassInfo.pAttachments).

VkRenderPassCreateInfo renderPassInfo{};renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;renderPassInfo.attachmentCount = 1;renderPassInfo.pAttachments = &colorAttachment;renderPassInfo.subpassCount = 1;renderPassInfo.pSubpasses = &subpass;if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {    throw std::runtime_error("failed to create render pass!");}

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

void cleanup() {    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);    vkDestroyRenderPass(device, renderPass, nullptr);    ...}

Мы проделали большую работу, и осталось лишь собрать все воедино, чтобы наконец-то создать графический конвейер!

C++ code / Vertex shader / Fragment shader


Заключение


Теперь мы можем объединить все структуры и объекты, чтобы создать графический конвейер!
Давайте вспомним, какие объекты у нас уже есть:

  • Шейдеры: шейдерные модули, определяющие функционал программируемых стадий конвейера
  • Непрограммируемые стадии: структуры, описывающие работу конвейера на непрограммируемых стадиях, таких как input assembler, растеризатор, вьюпорт и функция смешивания цветов
  • Layout конвейера: описание uniform-переменных и push-констант, которые используются конвейером и которые могут обновляться динамически
  • Проход рендера (render pass): описания буферов (attachments), в которые будет производиться рендер

Все эти объекты полностью определяют функционал графического конвейера. С ними можно начать заполнение структуры VkGraphicsPipelineCreateInfo. Сделаем это в конце функции createGraphicsPipeline, но перед вызовами vkDestroyShaderModule, поскольку шейдерные модули будут использоваться во время создания конвейера.

VkGraphicsPipelineCreateInfo pipelineInfo{};pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;pipelineInfo.stageCount = 2;pipelineInfo.pStages = shaderStages;

Начнем с указателя на массив структур VkPipelineShaderStageCreateInfo.

pipelineInfo.pVertexInputState = &vertexInputInfo;pipelineInfo.pInputAssemblyState = &inputAssembly;pipelineInfo.pViewportState = &viewportState;pipelineInfo.pRasterizationState = &rasterizer;pipelineInfo.pMultisampleState = &multisampling;pipelineInfo.pDepthStencilState = nullptr; // OptionalpipelineInfo.pColorBlendState = &colorBlending;pipelineInfo.pDynamicState = nullptr; // Optional

Затем заполним указатели на все структуры, описывающие непрограммируемые стадии конвейера.

pipelineInfo.layout = pipelineLayout;

После этого укажем layout конвейера, который является дескриптором Vulkan, а не указателем на структуру.

pipelineInfo.renderPass = renderPass;pipelineInfo.subpass = 0;

В конце сделаем ссылку на проход (render pass) и номер подпрохода (subpass), который используется в создаваемом пайплайне. Во время рендера можно использовать и другие объекты прохода, но они должны быть совместимы с нашим renderPass. Требования к совместимости вы можете найти здесь, однако в руководстве мы будем использовать только один проход.

pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // OptionalpipelineInfo.basePipelineIndex = -1; // Optional

Остались два параметра basePipelineHandle и basePipelineIndex. Vulkan позволяет создать производный графический конвейер из существующего конвейера. Суть в том, что создание производного конвейера не требует больших затрат, поскольку большинство функций берется из родительского конвейера. Также переключение между дочерними конвейерами одного родителя осуществляется намного быстрее. В поле basePipelineHandle вы можете указать дескриптор существующего конвейера, либо сделать отсылку к другому конвейеру, который будет создан по индексу, в поле basePipelineIndex. У нас только один конвейер, поэтому укажем VK_NULL_HANDLE и невалидный порядковый номер. Эти значения используются только в том случае, если в VkGraphicsPipelineCreateInfo в поле flags указано VK_PIPELINE_CREATE_DERIVATIVE_BIT.

Прежде чем завершить создание конвейера создадим член класса для хранения объекта VkPipeline:

VkPipeline graphicsPipeline;

И наконец создадим графический конвейер:

if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {    throw std::runtime_error("failed to create graphics pipeline!");}

Функция vkCreateGraphicsPipelines содержит больше параметров, чем обычная функция создания объектов в Vulkan. За один вызов она позволяет создать несколько объектов VkPipeline из массива структур VkGraphicsPipelineCreateInfo.

Параметр VkPipelineCache необязательный, поэтому мы передаем VK_NULL_HANDLE. Кэш конвейера может использоваться для хранения и повторного использования данных, связанных с созданием конвейера. Он совместно используется множеством вызовов vkCreateGraphicsPipelines и даже может быть сохранен на диск для переиспользования при последующих запусках программы. Впоследствии это может значительно ускорить процесс создания конвейера. Мы еще вернемся к этой теме в главе, посвященной кэшу конвейера.

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

void cleanup() {    vkDestroyPipeline(device, graphicsPipeline, nullptr);    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);    ...}

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

C++ code / Vertex shader / Fragment shader
Подробнее..

Управление данными из нескольких CAD-систем в единой среде разработки

20.04.2021 18:08:31 | Автор: admin

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

Роли Collaborative Designer в платформе 3DEXPERIENCE позволяют управлять проектными данными и документацией в облачной среде. Проекты можно хранить в глобальной сети, использовать повторно и безопасно управлять ими. Данные при таком способе работы нельзя случайно потерять или удалить.

Платформа 3DEXPERIENCE не только поддерживает работу внутренней команды конструкторов, но также открывает возможности защищенного доступа к данным для ключевых клиентов, поставщиков и руководителей проектов. Роль Collaborative Designer позволяет всем, независимо от того, в какой системе проектирования они работают, вносить вклад в единую структуру изделий, устраняя необходимость в независимом управлении ее фрагментами.

Блок-схема процесса корректировки данных, созданных в Creo Parametric.

Новые роли Collaborative Designer

Благодаря введению новых ролей подключаться к платформе 3DEXPERIENCE могут еще больше CAD-пользователей. Компании, в которых используются Creo Parametric от PTC, Inventor от Autodesk или Solid Edge от Siemens, получили возможность управлять своими проектными данными и предоставлять их в общий доступ в облачной платформе 3DEXPERIENCE. Роли добавлены к уже существующим для SOLIDWORKS, CATIA V5, AutoCAD и DraftSight.

Новые роли Collaborative Designer обеспечивают прямую интеграцию изнутри CAD-системы, обеспечивая беспрепятственное взаимодействие с платформой 3DEXPERIENCE. Как только данные попадают в платформу, они становятся доступны пользователям всех решений 3DEXPERIENCE для проектирования, моделирования, производства и управления. Роли это инструмент для реализации следующих целей:

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

  • общий доступ к моделям и чертежам через браузерное приложение;

  • разработка комплексного 3D-определения изделия в различных CAD-системах.

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

Люди и данные: работа в связке

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

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

Централизованное размещение проектов

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

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

Единый источник достоверной информации

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

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

Используя Collaborative Designer, участники проекта безопасно управляют данными CAD и организовывают общий доступ к ним. Доступ возможен из любого места, где есть подключение к Интернету. Все заинтересованные стороны мгновенно получают актуальную информацию и могут в любой момент видеть текущий статус изделия и его компонентов.

Дополнительная информация

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

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

Подробнее..

Перевод Скрываем воду внутри лодки

23.04.2021 10:10:48 | Автор: admin
При разработке игр про лодки, да и любых других игр с обширными водными поверхностями, существует проблема сокрытия поверхности воды, когда на ней что-то плавает. Я расскажу о решении, используемом в моей игре Sail Forth на движке Unity, но эта методика применима для любого другого движка.


Та самая проблема. Тащите ведро!

Так как в большинстве игр вода это просто большая плоскость, логично, что плавающие на ней объекты будут пересекаться с её поверхностью!

Как же нам устранить эту проблему? Мне известны две основные методики: одна основана на деформировании меша воды вокруг корпуса судна, вторая заключается в маскировании поверхности воды внутри судна. Я знаю, как использовать вторую методику, поэтому мы реализуем её.

Решение состоит из трёх компонентов:

  • Создание меша маски для каждого судна
  • Написание шейдера для меша маски
  • Изменение шейдера воды для использования маски

Меш маски


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


Меш маски воды

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

Шейдер маски


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

Shader "Custom/WaterMask"{  SubShader   {    Pass     {      // Render the mask after regular geometry, but before masked geometry and      // transparent things.      // You may need to adjust the queue value depending on your setup      Tags {"RenderType"="Opaque" "Queue"="Geometry+10" "IgnoreProjector"="True" }      // Don't draw in the RGBA channels; just the depth buffer      // This is important for making our mask mesh invisible      ColorMask 0      // We write to the depth buffer which will hide the water below our mask mesh      ZWrite On      // We don't want anything to draw in front of our mask,       // as it would allow the water to then be drawn on top of us      ZTest Off    }  }}

Шейдер для меша маски воды

Пока таким будет весь шейдер для маски воды! Краткое описание: он выполняет рендеринг после всей непрозрачной (opaque) геометрии (например, лодки) и до воды, и не записывает никаких цветов, зато выполняет запись глубин. Последнее означает, что мы не сможем его увидеть, но он перекрывает объекты за собой. Он не перекрывает саму лодку, потому что отрисовывается после лодки.


Включение и отключение меша маски

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

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

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


Ужасно

Что здесь происходит? Лодка качается на волнах, и иногда часть волн поднимается над верхней частью лодки. Это означает, что вода ближе к камере, чем маска, поэтому проходит Z-тест и рендерится.

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

Стенсил-буфер


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

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

Shader "Custom/WaterMask"{  SubShader   {    Pass     {      // Render the mask after regular geometry, but before masked geometry and      // transparent things.      // You may need to adjust the queue value depending on your setup      Tags {"RenderType"="Opaque" "Queue"="Geometry+10" "IgnoreProjector"="True" }      // Don't draw in the RGBA channels; just the depth buffer      // This is important for making our mask mesh invisible      ColorMask 0      // Writing to z depth isn't necessarily required, but might hide       // any extra effects your water has like caustics when the boat interior is below the water surface      ZWrite On      // We don't want anything to draw in front of our mask,       // as it would allow the water to then be drawn on top of us      ZTest Off      // The real meat of the solution.       // Ref - The value this stencil operation is in reference to. I arbitrarily picked '1'.      // Comp - The comparison method for deciding whether to draw a pixel.       //        For drawing the mask, we always want it to render regardless of the       //        stencil state, so I chose 'always'      // Pass - What to do with the stencil state after drawing a pixel. I chose 'replace',       //        which means that whatever was in the stencil buffer will be replaced with '1'       //        where our mask is drawn.      Stencil       {        Ref 1        Comp always        Pass replace      }    }  }}

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

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

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

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

Pass replace при отрисовке пикселя мы должны заменять текущее значение стенсила нашим значением, то есть 1.

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

Теперь нам нужно использовать эту информацию стенсила в шейдере воды.

Маскирование в шейдере воды


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

Pass{  ZWrite On  // Mask the water using the stencil buffer  Stencil   {    Ref 1    Comp notequal    Pass keep  }  CGPROGRAM  #pragma vertex waterVert  #pragma fragment waterFrag  ENDCG}

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

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

Поэтому мы делаем параметр Comp равным notequal, то есть сравнение будет выполняться на неравенство. Если значение стенсила не равно 1, то тест стенсила оказывается пройденным.

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


Вода, вода повсюду, но ни капли в нашей лодке!

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

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

Ещё одна проблема, похожая на ситуацию с потоплением, рассмотрена в этой замечательной статье: https://simonschreibt.de/gat/black-flag-waterplane/. Высокая волна может встать между лодкой и камерой, которая в таком случае неправильно создаст стенсил-буфер, что приведёт к некрасивому артефакту. Конкретно в вашей игре такая ситуация может и не возникнуть, или наоборот, будет очень заметной.


Упс

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

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

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


Артефакт устранён
Подробнее..

Молекулярная биология. Houdini. NVIDIA 3080. Коронавирус vs иммуноглобулины

01.05.2021 14:19:19 | Автор: admin

Ссылка на наш ролик

Это мой второй текст на Хабре. Он плавно вырос из первой статьи Молекулярная биология и Houdini летом двадцатого.

Мы закончили наш новый (второй) ролик 12 апреля 2021 года, в День космонавтики. Дата получилась случайной я очень хотел закончить работу в понедельник. Но это оказалось идеальное совпадение.

Поехали!

Вот приблизительный диалог, который состоялся у нас с Валерией (молекулярный биолог) на старте проекта:

А давайте сделаем английскую озвучку к нашему ролику про иммуноглобулин?

А давайте.

И заменим ротавирус на коронавирус. Они же очень похожи.

Да.

Часть сцен нам даже не нужно будет переделывать.

Да.

И перейдём с CPU-рендера на GPU. Откажемся от Blender в пользу DaVinci Resolve. Тайминг у нас останется тот же одна минута. Звук и вирус. Думаю, за пару месяцев мы всё закончим.

Ага, наверное.

Мы делали ролик 5 (пять) месяцев. Я мог работать по 2 часа в день с 12 до 2 ночи и не каждый день. Длительность ролика увеличилась в 3.5 раза. Увеличение сложности проекта (по сравнению с первым нашим роликом) моим внутренним ОщущаторомСложности оценивается, как шестикратная.

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

За разных людей у нас был я.

Какие это фрагменты? Сториборд. Переход с Mantra (внутренний CPU-рендер Houdini) на RedShift (GPU-рендер, приобретается отдельно). Решение задачек средствами Houdini (обожаю эту часть). Рендер. Цветовое решение. Озвучка. Музыка и звуки. Монтаж в Resolve (к счастью, есть free-версия).

Я разобью повествование на разрозненные части. Хотя это, на самом деле, конечно, суп где всё влияет на всё. И мы постоянно перемещались от одного к другому, постепенно меняясь.

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

Раскадровка (сториборд)

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

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

Сториборд. Вирус прикрепляется к клеточному рецептору АСЕ2.Сториборд. Вирус прикрепляется к клеточному рецептору АСЕ2.Сториборд. Серые круги клетки. Чёрные вирусы. Белые иммуноглобулины или антитела.Сториборд. Серые круги клетки. Чёрные вирусы. Белые иммуноглобулины или антитела.

Работа

Я за лёгкость. За юмор. За улыбку. Поэтому мне очень симпатичны Фейнман, Сапольски и Северинов. Одновременно с этим я понимаю, что я не могу с хохотом заниматься молекулярной биологией потому что я некомпетентен в этом (в молекулярной биологии). Для этого я делегирую ответственность в принятии решений молекулярному биологу. Я также не компетентен в английском. Тогда появляется переводчик. И каждый из них принимает решение в своей зоне ответственности. Я лишь собираю из этого всего пазл. Не вмешиваясь. В моей вселенной командная работа движется в сторону, где роль руководителя стремится к 0.

Мы хотим делать и научно достоверные и красивые работы. Чтобы школьнику, студентке, взрослому было интересно. Поэтому за научную достоверность у нас молекулярный биолог Валерия Архипова. И большинство частей вируса и клеток это всамделишные атомарные запчасти, полученные с сайта www.rcsb.org.

Мы пошли по пути создания релиза целого ролика и постепенного (слой за слоем) его улучшения. Всего было сделано 5 релизов и 6 финальный. Первый имел длину полторы минуты. Последний три с половиной.

Вот, как выглядит одна и та же сцена в разных релизах:

Одна сцена в разных релизах.Одна сцена в разных релизах.

Первый релиз сделан на демоверсии RedShift. Это видно по ватермаркам. Увидев достоинства от перехода на GPU-рендер, я купил лицензию.

Но постойте! А как, почему длительность ролика увеличилась с одной минуты до трёх с половиной?! Был же сториборд. Где все ключевые сцены утверждены.

Я неслучайно написал, что в сториборде (моём) нет музыки, озвучки и эстетики. И когда я собрал модель вируса, да ещё и на атомарном уровне, то мне захотелось всё это показать поближе, порассматривать.

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

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

RedShift

Считать картинки можно на процессоре, как это делал я в первом ролике. А можно на видеокарте. По тестам разница в скорости 5-10 раз не в пользу процессора. Какие очевидные плюсы и минусы от перехода на GPU-рендер? Ну, думал я, из очевидных плюсов, во-первых, скорость. Во-вторых, скорость. Из минусов? Память. У видеокарты её сильно меньше, чем общей памяти. Но ведь у меня очень простые сцены! Даже текстур нет.

И я попробовал.

На моей новёхонькой 3080 в демоверсии RedShift кадр в 2к с драфтовыми настройками считался секунд 5. Это было удивительно! Mantra, славящаяся своей неторопливостью, справляется секунд за 30 (на 16 ядрах, между прочим). Конечно, я тут же перешёл на этот рендер! И с самого начала проекта второй ролик делался уже в RedShift.

Почему Redshift? Выбор был между Octane, Vray и Arnold. От Vray я отказался сразу. Я работал с этим рендером в 3dsmax и мне не нравится обилие настроек и хаотичность результата. Arnold в тестах показывал результаты сравнимые с Мантрой и меня отпугнул факт, что этот рендер выкупила компания Autodesk. Octane показался мне очень сырым. RedShift самый часто используемый рендер в моушн графике. По нему много туторов и развито сообщество. Наконец, сама компания Maxon, которая приобрела RedShift, мне очень симпатична.

Я, к сожалению, не вёл дневник в процессе работы, как это делал, например, Фёдор Овчинников, когда создавал книжные магазины, а потом придумал сделать Додо-пиццу. Поэтому могу вспомнить только ретроспективно.

У RedShift свои материалы, свет. И свои алгоритмы работы с instance и частицами. Со всеми граблями и айсбергами я знакомился в процессе работы.

Модель вируса или мембраны это, по сути, множество атомов. Или шариков. Есть два способа эти шарики получить. Буквально нарисовать сферу нужного диаметра и порендерить её. Или работать с частицами. Тогда на экране мы видим множество точек, а вот на рендере вместо них появляются сферы. Оп!

В случае работы с частицами мы получаем массу преимуществ. Вьюпорт не тормозит. Не нужно следить за детальностью сферы ближний и дальний план отрабатываются на рендере. Объект ничего не весит. Он буквально состоит из координат точек и всё. А значит подготовка к рендеру занимает меньше времени. Жмём рендер!

RedShift. Шейдер одинаковый. Слева частицы. Справа сферы. WTF?RedShift. Шейдер одинаковый. Слева частицы. Справа сферы. WTF?

Эээ но подождите! Почему они разных цветов?! И что это за чёрные швы в местах, где сферы пересекаются? Для швов я нашёл решение. Они убираются за счёт операции булеан. Оп! Через две минуты boolean посчитался и чёрные швы исчезли. Можно рендерить! Секундочку! Но ведь весь рендер занимает 5 секунд. А булеан 2 минуты. Да. Но ведь работает. Нет. Спасибо, но нет.

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

Очень часто в сцене используются одинаковые части. Скажем, шипы у вируса. Это же, по сути, один шип, который рандомно развёрнут и расставлен по сферической мембране. И если мы так сделаем, то в этом случае мы получаем экономию по памяти на рендере и шустрый вьюпорт. Для этого у нас есть instance-объекты. Правильно? Логично?

Я тоже так думал. И у меня ушёл ещё один месяц на злость, смирение, слёзы и принятие другой реальности.

В RedShift (у меня) инстансные объекты из частиц/сфер рендерятся вот так:

RedShift. Слева без инстанс объектов. Справа с instance-объектами (сферы или частицы).RedShift. Слева без инстанс объектов. Справа с instance-объектами (сферы или частицы).

Видите? Свет. Слева он ровный и заполняющий. А справа он у каждого объекта свой и рандомно повёрнут. Хотя условия освещения в сцене одинаковые. Я не смог решить эту проблему. Кроме как отказаться от instance-объектов и упаковки. А учитывая, что атомов в сцене миллионы, то от сфер пришлось отказаться в пользу частиц. Памяти видеокарты (10Гб) не хватало. К слову, оперативной (64Гб) при таком подходе не хватало тоже.

NVIDIA 3080 и RedShift. В умелых руках и 10Гб мало. Не instance-геометрия крашит рендер.NVIDIA 3080 и RedShift. В умелых руках и 10Гб мало. Не instance-геометрия крашит рендер.

Уже после проекта мы обсудили эту особенность со Стасом Рыхликовым и он предположил, что дело в нежности объектов, которые мы копируем. О чём речь? Нужно убрать все ноды трансформаций и все другие ноды и тогда, возможно, всё будет хорошо.

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

При рендере на видеокарте так не получится. Рендер крашится. Или замедляется. Что, если подумать, логично. Но неприятно, да.

У меня не было цветового решения объектов. Я выкрашивал вирус в цвет окисленной меди (бирюзовый) и смотрел. Красиво? Красиво. А остальные объекты в какой цвет красить будем? И нужна цветовая гармония, настроение. Я остановился вот на такой палитре.

Цветовая палитра ролика.Цветовая палитра ролика.

И я нашёл решение, которое мне очень нравится. В жизни объекты, которые мы видим в ролике, настолько малы. что они меньше длины световой волны. Поэтому они не окрашиваются в цвета. Они чёрно-белые. Исключение составляет клетка. Она достаточно большая и уже может окрашиваться. Фотографии микроскопии, которые мы видим, раскрашены вручную. Я люблю чёрно-белую фотографию. Она даёт пространство воображению. В том числе и поэтому почти все рендеры в ролике чёрно-белые. Раскрашивал я их на монтаже. И такой подход, конечно, даёт огромное пространство для цветового решения объектов.

Ниже финальные рендеры из проекта. В оригинале рендеры в 2к. Для уменьшения трафика размер картинок уменьшен до 1к.

NVIDIA 3080. Redshift. Финальный рендер 2к. Атомарная модель коронавируса. Время рендера до 3 минут.NVIDIA 3080. Redshift. Финальный рендер 2к. Атомарная модель коронавируса. Время рендера до 3 минут.NVIDIA 3080. Redshift. Финальный рендер 2к. Атомарная модель коронавируса. Время рендера до 4х минут.NVIDIA 3080. Redshift. Финальный рендер 2к. Атомарная модель коронавируса. Время рендера до 4х минут.NVIDIA 3080. Redshift. Финальный рендер 2к. Эпительная клетка облепленная экземплярами коронавируса(порядка.150 000). Время рендера до 2 минут.NVIDIA 3080. Redshift. Финальный рендер 2к. Эпительная клетка облепленная экземплярами коронавируса(порядка.150 000). Время рендера до 2 минут.NVIDIA 3080. Redshift. Финальный рендеры 2к. SSS + DOF + Motion Blur. 1-4 минуты на кадр. Виньетирование добавлено в постпродакшене.NVIDIA 3080. Redshift. Финальный рендеры 2к. SSS + DOF + Motion Blur. 1-4 минуты на кадр. Виньетирование добавлено в постпродакшене.

Я оценил мощь 3080 и RedShift, когда нужны были тесты, драфты, финальные рендеры и потом куча перерендеров. Дело не только в скорости. Фактически, финального качества ролик считался те же 7 дней. Только это уже был ролик не в одну минуту, а в три с половиной. Такая скорость позволяет перманентно находится внутри проекта. Не выпадать из него на время рендера. И это очень и очень ускоряет работу. Я сэкономил огромное количество времени на том, что тесты считались по 5 секунд кадр, а не 30. Кажется и то и то отлично. Только в первом случае для секвенции в 200 кадров (менее 10 секунд) я получал результат через 20 минут. А во втором почти через 2 часа. И в этом важнейшее отличие между процессором и видеокартой.

И не забываем о масштабируемости. У меня одна видеокарта. Если их будет больше, то скорость рендера падает линейно. А при подключении NVLink память у видеокарт (не более двух) становится общей это очень классный бонус!

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

Houdini

Я люблю Houdini. И люблю решать задачки. И технические задачи, в отличие от задач эстетических, могут решаться участниками одинаково. Скажем решение, уравнения вида x = 5 2 будет иметь одинаковый вид для любого участника процесса решения. Школьница, студент, мама и аспирант решат его одинаково. Более того, решение будет понятно и принято всеми.

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

Поэтому решение технических задач мне нравится. А пиксельхантинг нет.

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

Клеточная мембрана. Вот как она выглядит на картинке из Википедии.

Википедия. Клеточная мембрана.Википедия. Клеточная мембрана.

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

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

Эндоцитоз. Способ проникновения вируса в клетку.Эндоцитоз. Способ проникновения вируса в клетку.

Давайте представим, что мы шариком тычем в тонкий лист картона. Картон, как не особо эластичная поверхность, порвётся. Но клетка не рвётся. Значит она эластичная. Хорошо. Заменим лист картона на что-то тягучее. Жвачку. Или слайм. Теперь наш шарик прекрасно входит и ничего не рвётся. Но погодите-ка. Ведь жвачка и слайм тянутся и в месте натяжения толщина вещества истончается. И, во-вторых, не происходит окутывания шарика. Эээ значит мембрана по свойству не плотное вещество, но и не тянется. И тут я словил инсайт. Мембрана жидкость! Именно этим объясняется её одинаковая плотность и не растяжимость. И теперь, если это жидкость, значит всегда и везде расстояние между фосполипидами одинаково. Жидкость не сжимаема.

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

К этому прибавляется проблема свойственная 3d-анимаций фликинг или мерцание кадров. Это явление возникает, если у нас низкие настройки сглаживания рендера или меняется численность объектов. На статичной картинке этого эффекта, конечно, не видно.

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

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

Озвучка

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

Поэтому в нашем втором ролике к работе со звуком я приступил задолго до финальной версии. Мы заранее приняли решение, что озвучка будет на английском. Рендеры считались в 960х540 в драфтовом качестве. И я держал в голове возможность отрезать или наращивать секвенции.

Я сделал четвертую версию ролика. Мы написали текст на английском для озвучки. Затем пригласили в команду переводчика - Александру.

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

Когда женщина-робот озвучила текст я сел за монтаж. Видео и звук. О нет! Эти ребята совсем не хотели дружить. Даже мои возможности примирить их ускорением или замедлением секвенций не особо помогали. В озвученных предложениях появились многозначительные паузы или предложения наступали друг другу на ноги. Нужно всё перемонтировать. Я очень расстроился.

Пришёл ответ по стоимости участия живого диктора. 27000 рублей. Таких финансовых возможностей у меня не было. Я ещё немножко огорчился. Потом ещё. Пока увы совсем ничего не осталось.

Было решено оставить голос робота-женщины. И я приступил к новой версии монтажа.

DaVinchi Resolve

Монтаж и звук для первого ролика сделан в Blender. После этого опыта я возжелал сменить коня. А какие есть варианты? Чтобы под Windows. При этом композитинг и монтаж это разный софт.

  1. After Effects + Premiere Pro.

  2. Sony Vegas + ?

  3. Davinci Resolve + Fusion.

  4. Nuke + ?

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

DaVinci Resolve. Вот, как выглядит четвёртая версия ролика. Проект мы закончили на шестой.DaVinci Resolve. Вот, как выглядит четвёртая версия ролика. Проект мы закончили на шестой.

Resolve прекрасно работает с exr. Очень быстро рендерит секвенции на таймлайне.

Вот пример раскрашенного рендера. Помните, я в части про RedShift рассказывал, что почти все рендеры чёрно-белые?

DaVinci Resolve. Коронавирус. Раскрашенный чёрно-белый рендер.DaVinci Resolve. Коронавирус. Раскрашенный чёрно-белый рендер.DaVinci Resolve. Коронавирус и поверхность клетки. Вирус прикрепляется к АСЕ2 рецептору.DaVinci Resolve. Коронавирус и поверхность клетки. Вирус прикрепляется к АСЕ2 рецептору.DaVinci Resolve. Эндоцитоз. Коронавирус попадает в клетку.DaVinci Resolve. Эндоцитоз. Коронавирус попадает в клетку.DaVinci Resolve. Иммуноглобулин IgG.DaVinci Resolve. Иммуноглобулин IgG.DaVinci Resolve. Иммуноглобулин IgG.DaVinci Resolve. Иммуноглобулин IgG.

Что дальше?

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

Я устал. Но это приятная усталость завершения. Без разрешения себе и другим ошибаться я устал бы ещё больше. И я вижу, что столь сложные и длительные (по таймингу) проекты требует большего участия, чем пара часов в день.

Я хотел добавить в название статьи фрактальность. Причём же здесь фрактальность? Ролик же про коронавирус и иммуноглобулин. Ну да. Но сколько можно?

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

И вот это бесконечное погружение внутрь. Фрактальность. На следующем уровне абстракции понадобится подключение динамики Houdini. Чтобы фосфолипиды учитывали положение соседей и не протыкали друг друга. Погрузившись ещё глубже нам понадобится сторонний софт, занимающийся молекулярной динамикой: GROMACS, CHARMM, NAMD и другие. Копнув еще глубже, мы увидим, что атомы это, оказывается, совсем не шарики.

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

P.S.

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

Пока я работал над статьёй закончился конкурс на Биомолекула. Мы участвовали с нашим первым роликом. И выиграл приз годовую подписку на OctaneRender.

Подробнее..

Работа с 3D моделями в Python с использованием библиотеки OpenMesh

02.05.2021 18:15:06 | Автор: admin

Есть очень удобная и мощная библиотека, которая значительно упрощает работу с полигональными 3D моделями или мешами под названием OpenMesh. Она предоставляет широкий набор операций для работы с 3D моделями и имеет версию в Python. В этой статье я покажу как работать с 3D моделями используя обертку OpenMesh для Python. Кому интересно, прошу под кат.

Начало работы с OpenMesh

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

Для начала установим пакет с помощью pip:

pip install openmesh

Создадим новый Python скрипт и импортируем модуль openmesh

import openmesh as omimport numpy as np

Создадим объект, представляющий 3D меш

mesh = om.TriMesh()

Добавим несколько вершин

# add a a couple of vertices to the meshvh0 = mesh.add_vertex([0, 1, 0])vh1 = mesh.add_vertex([1, 0, 0])vh2 = mesh.add_vertex([2, 1, 0])vh3 = mesh.add_vertex([0,-1, 0])vh4 = mesh.add_vertex([2,-1, 0])

и несколько полигонов

fh0 = mesh.add_face(vh0, vh1, vh2)fh1 = mesh.add_face(vh1, vh3, vh4)fh2 = mesh.add_face(vh0, vh3, vh1)

В OpenMesh вершина представлена объектом VertexHandle. Объекты VertexHandle передаются во многие методы библиотеки OpenMesh в качестве параметра.

Есть также альтернативный способ через питоновский список вершин

vh_list = [vh2, vh1, vh4]fh3 = mesh.add_face(vh_list)

Стоит отметить, что OpenMesh также вводит специальный тип элемента в модели под названием Half-edge. Я не буду его рассматривать в данной статье. Подробнее о нем можно почитать здесь.

Манипуляция с отображением текстуры и координатами вершин

Я не буду раскрывать тему отображения текстуры (texture mapping) полигонов. Читатель может прочитать детальное объяснение этой темы здесь.

Получим координаты текстуры (UV texture coordinates) вершины:

tc = mesh.texcoord2D(vh)

tc представляет собой tuple. Значения координат u и v можно получить по индексу 0 и 1 соответственно.

Изменим координаты текстуры

uv_coords = [0.5, 0.2]mesh.set_texcoord2D(vh, uv_coords)

Здесь vh - объект типа VertexHandle.

Получим точку с координатами вершины

point = mesh.point(vh)

point представляет собой tuple. Координаты точки можно получить по индексу 0 и 1

x, y = tc[0], tc[1]

Можно получить все точки вершин модели

point_array = mesh.points()

и использовать их для сдвига модели вдоль оси (например X)

point_array += np.array([1, 0, 0])

Важные замечания по работе с OpenMesh

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

При сохранение меша в файл OpenMesh по умолчанию не сохраняет координаты текстур для вершин (vt строки) в файле obj. Чтобы решить эту проблему нужно передать параметр vertex_tex_coord в метод write_mesh (источник):

om.write_mesh(test_out.obj, mesh, vertex_tex_coord=True)

Также OpenMesh не сохраняет файл материалов mtl в файле obj. Для сохранения информации о материале используйте параметр face_color при чтении файла obj

mesh = openmesh.read_trimesh('test.obj', vertex_tex_coord=True, face_color=True)

и записи в файл

openmesh.write_mesh('test_out.obj', mesh, vertex_tex_coord=True, face_color=True)

То же касается и нормалей. Чтение модели obj с нормалями

mesh = openmesh.read_trimesh('test.obj', vertex_normal=True)

и записи в файл

openmesh.write_mesh('test_out.obj', mesh, vertex_normal=True)

Здесь важно использовать одинаковые параметры и при чтении и при записи. Например, если мы хотим получить и сохранить информации о материале нужно использовать параметр face_color в обоих методах read_trimesh и write_mesh.

При работе с OpenMesh я сделал интересное наблюдение: порядок индексов координат текстур вершин (индексы строк vt) меняется. Например для такой строки в исходном файле obj

f 1/1 2/2 3/3

Соответствующая строка в выходном файле obj может выглядеть примерно так

f 1/3 2/1 3/2

Итерации и циклы

Итерации над вершинами в меше

for vh in mesh.vertices():    print(vh.idx())

Цикл for возвращает объекты vh типа VertexHandle. idx() возвращает индекс вершины.

Итерации над полигонами и гранями

# iterate over all edgesfor eh in mesh.edges():    print eh.idx()# iterate over all facesfor fh in mesh.faces():    print fh.idx()

Аналогично итератору над вершинами цикл for возвращает объекты fh типа FaceHandle.

Итерация над всеми half-edge в меше

for heh in mesh.halfedges():    print heh.idx()

Над вершинами соседними с заданной

for vh_n in mesh.vv(vh):    print(vh_n.idx())

Над гранями выходящими из заданной вершины

for eh in mesh.ve(vh1):    print eh.idx()

Над полигонами, смежными с заданной вершиной

for fh in mesh.vf(vh1):    print fh.idx()

Все то же самое можно проделать и с полигоном

# iterate over the face's verticesfor vh in mesh.fv(fh0):    print vh.idx()   # iterate over the face's halfedgesfor heh in mesh.fh(fh0):    print heh.idx()# iterate over the face's edgesfor eh in mesh.fe(fh0):    print eh.idx()    # iterate over all edge-neighboring facesfor fh in mesh.ff(fh0):    print fh.idx()

Это все. Не так сложно, не правда ли. Удачи вам в работе с 3D мешами с использованием OpenMesh и до новых встреч.

Подробнее..

Парящие Острова настраиваем стилизованные шейдера с помощью HDRP в Unity

08.05.2021 22:04:46 | Автор: admin

Автор статьи Maciej Hernik и главный редактор портала 80.lv Kirill Tokarev любезно позволили нам сделать этот перевод.

Maciej Hernik обсудил с нами детали его стилизованной сцены Парящие Острова: шейдеры для травы, деревьев и воды, Volume Overrides, текстурирование асcетов и многое другое.

Вступление

Всем привет! Меня зовут Maciej Hernik и я самоучка, художник по окружению (Level Artist) из Польшы. Сколько я себя помню, я всегда обожал игры. Мое знакомство с видеоиграми произошло, когда я увидел несколько на PS1. В то время я был ребенком, у меня была мечта, что однажды я буду делать свои собственные игры, хотя я еще и понятия не имел как их делать. Я стал старше, узнал что такое 3D арт и нашел это очень интересным и увлекательным занятием. Моя страсть к игровому арту дала мне возможность получить свою первую работу и в этот момент я понял, что хочу зарабатывать на жизнь созданием игрового арта и самих игр.

Парящие Острова: идея

В начале, эта работа предполагалась, как бессрочный проект вроде площадки для экспериментов с HDRP в Unity, это бы помогло мне чуть больше ознакомиться с инструментарием HDRP. Однако, когда я сделал первые итерации шейдеров растительности и показал их своим друзьям, они вдохновили меня сделать красочную сцену с использованием этих шейдеров. Тогда то я и решил сделать эту художественную работу в стиле волшебной сказки, вдохновленный игрой The Legend of Zelda. И в этой статье я хочу разобрать некоторые компоненты из этой работы.

Начало проекта

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

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

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

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

Создание травы

Шейдер травы был одним из первых созданных для этого проекта. Однако он прошел через множество итераций, чтобы достичь наиболее подходящего эффекта. Я выбрал более процедурный подход, так как посчитал, что это будет наиболее сложный и интересный путь для создания травы. Для создания шейдера я использовал Shader Graph в Unity версии 2020.1.0f1.

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

  • Primary color + Shadow color (Главный цвет + Цвет тени)

  • Additional color (Дополнительный цвет)

  • Ground color ( Цвет земли)

  • Highlights (Блики)

Primary color и Shadow color разделены на две части. Одна часть это просто цвет травы, а вторая часть отвечает за нанесение поверх другого цвета. Это достигается за счет проекции текстуры на траву из вида сверху в мировых координатах (World Position). Additional color использовался, чтобы достичь большего разнообразия травы, потому что я чувствовал, что двух цветов будет недостаточно, особенно если смотреть сверху. Ground color нужен, чтобы достичь деликатной, почти незаметной имитации окружающего затенения (Ambient Occlusion) снизу травы, чтобы немного разбить элементы. В конечном итоге, я уменьшил его, потому что он создавал слишком сильный контраст между пучками травы и это не подходило для стилизации, по-моему мнению. Именно поэтому, этот эффект окружающего затенения (Ambient Occlusion) от Ground color, очень слабо использован в этой сцене. Последний слой создает блики (Highlights) в верхней части травы, зависящие от скорости перемещения текстуры маски.

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

Деревья

Я создал отдельный шейдер для деревьев и другой растительности, так как он немного отличается от шейдера травы. В этот раз использовался освещаемый шейдер как основу (Lit Master Shader). Благодаря этому я очень легко мог получить информацию от направления света, а также в HDRP освещаемый шейдер (Lit Master Shader) дает возможность управлять полупрозрачностью (Translucency) объекта.

Модель дерева состоит из простых плоскостей (Planes), но с измененными нормалями, чтобы достичь более мягкий вид и получить лучший контроль над распределением света по дереву. Я добился этого в Blender благодаря модификатору передачи данных (Data Transfer Modifier), который позволил мне перенести нормали c другого меша на плоскости (Planes) из которых состоит дерево.

Деревья не имеют диффузную текстуру (Diffuse). Вместо этого, в шейдере я использовал информацию о направлении света. Благодаря этому я смог регулировать полупрозрачность (Translucency) дерева, чтобы решить - как много света будет проходить сквозь него. Я так же использовал информацию о направлении света, чтобы менять цвет в освещенной части и в затененной части дерева. Эти функции были ключем для меня, и помогли достичь стилизованного вида растений.

Я также реализовал возможность регулировать цвет в зависимости от высоты объекта, по локальному значению Y. Это позволило мне создать симпатичный градиент сверху вниз, что также помогло сделать более мягкий переход от земли к дереву. В итоге, я решил добавить деликатное окружающее затенение (Ambient Occlusion) между листьями, чтобы немного разделить их.

Эффекты воды

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

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

  • Вода с шейдером воды

  • Частицы пены и пузырьков

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

Я добавил немного частиц для симуляции пены и пузырьков, используя систему частиц Unity c кастомным мешем в виде сферы. Чтобы добиться финального вида, я поиграл с такими настройками как эмиссия, форма системы частиц и размер частиц в течение их времени жизни (Size Over Lifetime).

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

Ассеты не растительного происхождения

Аcсеты, такие как камни, мост и Монолит я сделал стандартным образом - смоделировал их в Blender, заскульптил высокополигональные модели (High Poly) в ZBrush, и запек их на низкополигональные меши (Low Poly) в Substance Painter.

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

Фокус в том, чтобы использовать Blur Slope фильтр на слое Baked Lighting в Substance Painter, который я кладу поверх всех остальных слоев. Это позволило мне достичь стилизованного неоднородного эффекта.

Очень важно знать, что слой Baked Lighting добавляет тени в текстуру, основываясь на освещении именно от окружения в самом Substance Painter. Из-за этого, я выбрал окружение, которое не такое направленное с точки зрения освещения.

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

Настройка Volume

Последняя часть проекта, о которой я хотел поговорить, это компонент Volume в Unity HDRP, он позволил мне отрегулировать освещение и пост-обработку (Post-Processing). В Volume я добавил несколько Overrides, что помогло мне достичь разных эффектов.

В виде Overrides были добавлены Visual Environment, HDRI Sky и Indirect Lighting Controller, это обеспечило мне немного окружающего освещения (Ambient Light) от неба. При использовании Indirect Lighting Controller я мог регулировать интенсивность окружающего освещения (Ambient Light), что было довольно удобно для меня.

Еще одна полезная опция, которую я действительно люблю использовать внутри Unity HDRP, это туман (Fog) и особенно объемный туман (Volumetric Fog). Я обнаружил, что лучший способ использовать его - это настроить пару компонентов Density Volume в сцене. Density Volume - это компонент, который позволяет вам вручную регулировать область, где будет отображаться туман и на сколько сильным он будет в этой области.

Я также добавил некоторые эффекты пост-обработки (Post-Processing) в Volume. Потому что мне казалось, что сцена была слишком темной и нужно было добавить больше свечения (Bloom), чтобы добиться сказочного вида, к которому я стремился. В итоге, сцена получила больше синих оттенков, особенно в тенях и я остался доволен конечным результатом.

Заключение

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

Если у вас есть какие-то вопросы об этой сцене или о творчестве в целом, то не стесняйтесь писать мне наArtStation.

Спасибо вам за прочтение! Пока!
Maciej Hernik, Level Artist

С оригинальной статьей можно ознакомиться на 80.lvздесь.

Перевод подготовлен при поддержке проектаAlmost There.

Подробнее..

Перевод Парящие Острова настраиваем стилизованные шейдера с помощью HDRP в Unity

09.05.2021 10:20:54 | Автор: admin

Автор статьи Maciej Hernik и главный редактор портала 80.lv Kirill Tokarev любезно позволили нам сделать этот перевод.

Maciej Hernik обсудил с нами детали его стилизованной сцены Парящие Острова: шейдеры для травы, деревьев и воды, Volume Overrides, текстурирование асcетов и многое другое.

Вступление

Всем привет! Меня зовут Maciej Hernik и я самоучка, художник по окружению (Level Artist) из Польши. Сколько я себя помню, я всегда обожал игры. Мое знакомство с видеоиграми произошло, когда я увидел несколько на PS1. В то время я был ребенком, у меня была мечта, что однажды я буду делать свои собственные игры, хотя я еще и понятия не имел как их делать. Я стал старше, узнал что такое 3D арт и нашел это очень интересным и увлекательным занятием. Моя страсть к игровому арту дала мне возможность получить свою первую работу и в этот момент я понял, что хочу зарабатывать на жизнь созданием игрового арта и самих игр.

Парящие Острова: идея

В начале, эта работа предполагалась, как бессрочный проект вроде площадки для экспериментов с HDRP в Unity, это бы помогло мне чуть больше ознакомиться с инструментарием HDRP. Однако, когда я сделал первые итерации шейдеров растительности и показал их своим друзьям, они вдохновили меня сделать красочную сцену с использованием этих шейдеров. Тогда то я и решил сделать эту художественную работу в стиле волшебной сказки, вдохновленный игрой The Legend of Zelda. И в этой статье я хочу разобрать некоторые компоненты из этой работы.

Начало проекта

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

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

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

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

Создание травы

Шейдер травы был одним из первых созданных для этого проекта. Однако он прошел через множество итераций, чтобы достичь наиболее подходящего эффекта. Я выбрал более процедурный подход, так как посчитал, что это будет наиболее сложный и интересный путь для создания травы. Для создания шейдера я использовал Shader Graph в Unity версии 2020.1.0f1.

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

  • Primary color + Shadow color (Главный цвет + Цвет тени)

  • Additional color (Дополнительный цвет)

  • Ground color ( Цвет земли)

  • Highlights (Блики)

Primary color и Shadow color разделены на две части. Одна часть это просто цвет травы, а вторая часть отвечает за нанесение поверх другого цвета. Это достигается за счет проекции текстуры на траву из вида сверху в мировых координатах (World Position). Additional color использовался, чтобы достичь большего разнообразия травы, потому что я чувствовал, что двух цветов будет недостаточно, особенно если смотреть сверху. Ground color нужен, чтобы достичь деликатной, почти незаметной имитации окружающего затенения (Ambient Occlusion) снизу травы, чтобы немного разбить элементы. В конечном итоге, я уменьшил его, потому что он создавал слишком сильный контраст между пучками травы и это не подходило для стилизации, по-моему мнению. Именно поэтому, этот эффект окружающего затенения (Ambient Occlusion) от Ground color, очень слабо использован в этой сцене. Последний слой создает блики (Highlights) в верхней части травы, зависящие от скорости перемещения текстуры маски.

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

Деревья

Я создал отдельный шейдер для деревьев и другой растительности, так как он немного отличается от шейдера травы. В этот раз использовался освещаемый шейдер как основу (Lit Master Shader). Благодаря этому я очень легко мог получить информацию от направления света, а также в HDRP освещаемый шейдер (Lit Master Shader) дает возможность управлять полупрозрачностью (Translucency) объекта.

Модель дерева состоит из простых плоскостей (Planes), но с измененными нормалями, чтобы достичь более мягкий вид и получить лучший контроль над распределением света по дереву. Я добился этого в Blender благодаря модификатору передачи данных (Data Transfer Modifier), который позволил мне перенести нормали c другого меша на плоскости (Planes) из которых состоит дерево.

Деревья не имеют диффузную текстуру (Diffuse). Вместо этого, в шейдере я использовал информацию о направлении света. Благодаря этому я смог регулировать полупрозрачность (Translucency) дерева, чтобы решить - как много света будет проходить сквозь него. Я так же использовал информацию о направлении света, чтобы менять цвет в освещенной части и в затененной части дерева. Эти функции были ключем для меня, и помогли достичь стилизованного вида растений.

Я также реализовал возможность регулировать цвет в зависимости от высоты объекта, по локальному значению Y. Это позволило мне создать симпатичный градиент сверху вниз, что также помогло сделать более мягкий переход от земли к дереву. В итоге, я решил добавить деликатное окружающее затенение (Ambient Occlusion) между листьями, чтобы немного разделить их.

Эффекты воды

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

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

  • Вода с шейдером воды

  • Частицы пены и пузырьков

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

Я добавил немного частиц для симуляции пены и пузырьков, используя систему частиц Unity c кастомным мешем в виде сферы. Чтобы добиться финального вида, я поиграл с такими настройками как эмиссия, форма системы частиц и размер частиц в течение их времени жизни (Size Over Lifetime).

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

Ассеты не растительного происхождения

Аcсеты, такие как камни, мост и Монолит я сделал стандартным образом - смоделировал их в Blender, заскульптил высокополигональные модели (High Poly) в ZBrush, и запек их на низкополигональные меши (Low Poly) в Substance Painter.

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

Фокус в том, чтобы использовать Blur Slope фильтр на слое Baked Lighting в Substance Painter, который я кладу поверх всех остальных слоев. Это позволило мне достичь стилизованного неоднородного эффекта.

Очень важно знать, что слой Baked Lighting добавляет тени в текстуру, основываясь на освещении именно от окружения в самом Substance Painter. Из-за этого, я выбрал окружение, которое не такое направленное с точки зрения освещения.

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

Настройка Volume

Последняя часть проекта, о которой я хотел поговорить, это компонент Volume в Unity HDRP, он позволил мне отрегулировать освещение и пост-обработку (Post-Processing). В Volume я добавил несколько Overrides, что помогло мне достичь разных эффектов.

В виде Overrides были добавлены Visual Environment, HDRI Sky и Indirect Lighting Controller, это обеспечило мне немного окружающего освещения (Ambient Light) от неба. При использовании Indirect Lighting Controller я мог регулировать интенсивность окружающего освещения (Ambient Light), что было довольно удобно для меня.

Еще одна полезная опция, которую я действительно люблю использовать внутри Unity HDRP, это туман (Fog) и особенно объемный туман (Volumetric Fog). Я обнаружил, что лучший способ использовать его - это настроить пару компонентов Density Volume в сцене. Density Volume - это компонент, который позволяет вам вручную регулировать область, где будет отображаться туман и на сколько сильным он будет в этой области.

Я также добавил некоторые эффекты пост-обработки (Post-Processing) в Volume. Потому что мне казалось, что сцена была слишком темной и нужно было добавить больше свечения (Bloom), чтобы добиться сказочного вида, к которому я стремился. В итоге, сцена получила больше синих оттенков, особенно в тенях и я остался доволен конечным результатом.

Заключение

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

Если у вас есть какие-то вопросы об этой сцене или о творчестве в целом, то не стесняйтесь писать мне наArtStation.

Спасибо вам за прочтение! Пока!
Maciej Hernik, Level Artist

С оригинальной статьей можно ознакомиться на 80.lvздесь.

Перевод подготовлен при поддержке проектаAlmost There.

Подробнее..

Термический анализ в SOLIDWORKS Simulation на примере микрочипа

14.05.2021 14:06:19 | Автор: admin

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

Введение

В качестве модели взята сборка микрочипа, которая состоит из теплоотвода (снизу) и собственно чипа (сверху) рис. 1.

Добавив модуль Simulation в интерфейс SOLIDWORKS, создаем Новое исследование и выбираем Термический анализ. У нас загрузилось дерево исследования, в котором мы можем задавать настройки для проведения анализа (рис. 2).

рис.2рис.2

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

Задание материала

Первое, что нам необходимо сделать, это задать материал. Щелкаем правой кнопкой мыши по одной из деталей и нажимаем Применить/редактировать материал. В нашем примере выберем для теплоотвода алюминий, а именно Сплав 1060. Материалом для чипа пусть будет оцинкованная сталь. Потребуется указать теплопроводность такие обязательные параметры выделяются красным цветом в открывающейся таблице (рис. 3). Скопируем оцинкованную сталь в папку Настроенный пользователем материал и добавим материалу теплопроводность: 50.

рис.3рис.3

Задание граничных условий

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

рис.4рис.4

Следующим шагом зададим тепловую мощность микрочипа. Щелкнем правой кнопкой мыши по кнопке Термические нагрузки и перейдем в настройки тепловой мощности. Выберем в дереве сборки весь элемент Чип и укажем 15 ватт (рис. 5). Тепло будет выделяться из этого элемента.

Далее задаем набор контактов. Для этого щелкаем правой кнопкой мыши по кнопке Соединения, выбираем тип контакта Тепловое сопротивление и указываем грани, где чип и теплоотвод соприкасаются. Устанавливаем тепловое сопротивление равным 2,857е-6 К/Вт.

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

Задаем коэффициент конвективной теплоотдачи: 200 Вт/м2К. Этот коэффициент характеризует интенсивность теплообмена между поверхностью тела и окружающей средой. Указываем массовую температуру окружающей среды, то есть температуру, которая окружает нашу модель. Для этого параметра установим 300 К (рис. 6).

рис.6рис.6

То же самое сделаем и для чипа. Выбираем внешние грани чипа, задаем коэффициент конвективной теплоотдачи равным 90 Вт/м2К, а массовую температуру окружающей среды, как и в предыдущем случае, 300 К.

Результаты

Запустим исследование (рис. 7). По умолчанию сетка будет построена автоматически.

рис.7рис.7

Исследование завершено, можно ознакомиться с распределением температуры. Для этого выберем параметр Ограничение сечения по плоскости справа (рис. 8).

рис.8рис.8

Теперь мы видим, как температура распространяется от чипа по теплоотводу (рис. 9).

рис.9рис.9

Задание переходного процесса

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

рис.10рис.10

Щелкнув по исследованию правой кнопкой мыши, зайдем в его свойства (рис. 11).

рис.11рис.11

Изменим тип решения на Переходный процесс. Укажем общее время (например, 100 секунд) и установим пятисекундный временной интервал (рис. 12).

рис.12рис.12

Теперь для выполнения нестационарного термического исследования требуется использовать начальную температуру. Выбираем температуру в Термических нагрузках и задаем начальную температуру для всех тел: 22C (рис. 13).

рис.13рис.13

Запускаем решение. Получив результат, можем посмотреть распределение температуры и ее значение в выбранный момент времени (рис. 14).

рис.14рис.14

Вывод

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

Автор: Максим Салимов, технический специалист по SOLIDWORKS, ГК CSoft. email: salimov.maksim@csoft.ru

Подробнее..

Эволюция методов mesh denoising от простых фильтров до 3D глубокого обучения

14.05.2021 20:06:19 | Автор: admin

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

Зачем нужен mesh denoising?

С помощью технологии трехмерного сканирования можно получить 3D-модель реального объекта. Но знаете ли вы, что почти всегда такие объекты содержат шумы и неточности? В Twin3d мы сканируем людей (и не только) и с помощью фотограмметрии получаем 3D-модели, которые дальше необходимо обрабатывать в зависимости от конечной цели их использования. Естественно, от шумов надо избавляться, чтобы применять виртуальную модель человека в кино/играх/рекламе. Нужно много чего еще делать, но об этом мы поговорим потом.

Сканирование объектов и получение их 3D-моделей используется не только для создания виртуальных людей. Это популярно в reverse engineering для получения CAD-деталей без чертежей, где нужна большая точность реконструкции и шумы недопустимы. Также как людей и CAD-детали, можно сканировать реальные объекты одежду, обувь, аксессуары, что сейчас очень распространено в связи с созданием виртуальных примерочных. В таких случаях тоже хочется сделать вещь визуально идеальной, поэтому отсканированный объект необходимо обрабатывать.

Процесс устранения шума с 3D-моделей, полученных после сканирования, получил название mesh denoising. Иногда можно встретить слово smoothing, что означает просто сглаживание. 3D-моделлеры пользуются профессиональным ПО для решения данной задачи, но при этом они тратят достаточно много времени, чтобы убрать все неровности и шероховатости поверхности вручную. А как это делается без вмешательства 3D-моделлера? С помощью методов, которые мы рассмотрим далее.

С чего все начиналось

Когда-то были фильтры Просто сглаживающие фильтры, которые берут координаты вершин меша и усредняют по соседним вершинам (Laplacian smoothing, Taubin smoothing).

Laplacian smoothingLaplacian smoothing

В 2003 году появляется Bilateral mesh denoising расширение билатерального фильтра (который использовался для сглаживания шума на 2D картинках) на трехмерные полигональные сетки. Суть остается та же усредняются координаты вершин, но уже немного умнее: используются при этом как координаты вершин, так и нормали вершин. Еще через 7 лет придумали применять такой билатеральный фильтр не к вершинам, а к нормалям граней (Bilateral normal filtering for mesh denoising), что значительно увеличило качество сглаживания.

Итеративный процесс вычисления новой нормали с помощью Bilateral Normal Filtering заключается в следующем:

n_i^k=\Lambda(\sum_{f_j\in N_i}A_jW_s(||c_i-c_j||)W_r(||n_i-n_j||)n_j),

где N_i набор соседних граней для грани f_i , n_j нормаль грани f_j , A_j площадь грани f_j , c_j центроид грани f_j (точка пересечения медиан треугольника), W(x)=exp(-x^2/(2\sigma^2)) гауссиана, \Lambda оператор нормализации.

Билатеральный фильтр является средним взвешенным с весом, состоящим из нескольких частей. W_s(x) для грани f_i определяет значимость грани f_j в терминах удаленности друг от друга чем больше расстояние, тем меньше вес. Аналогично с W_r(x) , только на вес влияет не расстояние между гранями, а разница между векторами нормалей грани. Также учитывается значение площади грани A_j

В 2015 году улучшают подход с билатеральными фильтрами с появлением Guided Mesh Normal Filtering, где используется направляющая нормаль для сглаживания.

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

Наряду с фильтрами развивались подходы mesh denoising, основанные на оптимизации. Так, например, в работе Mesh Denoising via L0 minimization авторы максимизируют плоские поверхности меша и постепенно устраняют шум, не сглаживая при этом углы. Данный подход хорош в случаях, когда все отсканированные объекты CAD-модели с правильными геометрическими формами.

Первые попытки обучения алгоритмов для mesh denoising

Настоящим прорывом в 2016 году стала работа Mesh Denoising via Cascaded Normal Regression, в которой впервые использовались данные для обучения алгоритма. Прежде всего, авторы создали соответствующий датасет из noisy и ground truth (GT) 3D-моделей с разным шумом (доступен по ссылке проекта).

Датасет состоит из синтетических данных (Synthetic) и полученных с помощью различных сканеров (Kinect v1, Kinect v2, Kinect Fusion). В Synthetic в качестве noisy моделей используются модели с искусственно сгенерированным шумом (используется небольшое случайное смещение координат). Отсканированные данные уже содержат шум, зависящий от параметров сканеров и используемых в них технологий. Для получения GT моделей для реальных сканов использовался сканер Artec Spider c порядком точности, превышающим Microsoft Kinect.

Примеры из датасетаПримеры из датасета

Основная идея работы для каждой пары граней noisy и ground truth мешей создать пару дескриптор и GT нормаль. Дескриптор одной грани это нормаль грани после применения билатеральных фильтров с разными параметрами. Далее такие дескрипторы необходимо разделить на отдельные кластеры, которые определяют принадлежность к той или иной геометрической форме. Для отдельного кластера обучается отдельная регрессия, после которой на выходе получается обновленная нормаль грани. Весь описанный процесс повторяется несколько раз, каскадами.

Cascaded Normal Regression PipelineCascaded Normal Regression Pipeline

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

После этого появилась работа NormalNet: Learning based Guided Normal Filtering for Mesh Denoising. Основная идея обучать сверточную нейронную сеть для определения направляющей нормали, после применять Guided Mesh Normal Filtering. Для определения направляющей нормали происходит вокселизация локальной структуры каждой грани меша (voxel объемный пиксель), чтобы привести его к упорядоченному представлению и иметь возможность использовать CNN. Общий пайплайн работы представлен на картинке ниже.

NormalNet pipelineNormalNet pipeline

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

Архитектура CNN в NormalNetАрхитектура CNN в NormalNet

Таким образом, итеративно получая с помощью вокселизации и CNN направляющие нормали, а после применяя Guided Normal Filtering, авторы строят процесс устранения шума. Грубо говоря, в данном подходе происходит очередное улучшение качества сглаживающего фильтра Guided Normal Filtering.

Наконец, от использования билатеральных фильтров решили отказаться в работе DNF-Net: a Deep Normal Filtering Network for Mesh Denoising и предоставили полное управление процессом фильтрации шума нейронным сетям. Это первая работа, которая делает end-to-end процесс фильтрации без ручного составления признаков. С помощью отдельных логических частей нейронной сетки на основе ResNet авторы формируют карты признаков для всего меша с шумом и отдельно для шума, а после их обработки получают обновленные нормали граней.

Архитектура DNF-NetАрхитектура DNF-Net

На вход DNF-Net принимает патчи граней: нормали и индексы соседних граней. С помощью блока multi-scale feature embedding unit составляется карта признаков для каждого патча. В этом блоке анализируется локальная геометрическая структура меша на разных масштабах. На трех уровнях (см. картинку ниже) составляются три разные локальные карты признаков , которые учитывают разное количество соседних граней (по возрастанию). После их конкатенации и прогона через полносвязные слои нейронной сети получается глобальная карта признаков F для каждой грани меша.

Multi-scale Feature Embedding UnitMulti-scale Feature Embedding Unit

Впоследствии с помощью residual learning unit аналогичным образом извлекается карта признаков для шума. С помощью KNN (K-Nearest Neighbors) происходит поиск k похожих граней в представлении созданных признаков. Из продублированной k раз исходной карты признаков вычитаются карты признаков похожих граней. Аналогично используются полносвязные слои нейронной сетки формирования шумной карты признаков.

Residual learning unitResidual learning unit

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

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

Deep learning и свертки на графах

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

Так, в работе Mesh Denoising with Facet Graph Convolutions был предложен еще один end-to-end pipeline для устранения шума с помощью представления мешей как графов, только вместо натурального представления графа полигональной сетки (вершины, связанные с вершинами) используется другое грани, связанные с гранями. Основная идея сгенерировать граф и запустить на нем сверточную нейронную сеть.

Facet Graph Convolution pipelineFacet Graph Convolution pipeline

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

Архитектура Graph Convolution NetworkАрхитектура Graph Convolution NetworkСвертка графаСвертка графа

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

Заключение

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

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

В Twin3d мы занимаемся разными задачами, и mesh denoising входит в их число. В будущих статьях будем рассказывать уже о своих разработках, не пропустите ;)

Подробнее..

Перевод Это норма 5 различные способы создания карт нормалей

18.05.2021 10:20:20 | Автор: admin
image


Существует множество способов создания и применения карт нормалей. Давайте рассмотрим известные мне способы их изготовления.

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

Предыдущие части:

Это норма: что такое карты нормалей и как они работают

Это норма 2: как запекаются карты нормалей

Это норма 3: типы карт нормалей

Это норма 4: решение проблем с картами нормалей

Совершенно гладкие lowpoly


При таком способе все нормали низкополигональной модели усредняются: все рёбра считаются плавными или имеют одну группу сглаживания (smoothing group).

Это означает, что всю работу должна выполнять карта нормалей. Lowpoly-модель обычно сильно отличается от highpoly-модели, пока не будут наложены карты нормалей.

image

Основное преимущество этого способа он очень прост для художников: не нужно заниматься разделением UV-островов, настраивать плавность модели или модифицировать нормали lowpoly. Достаточно создать lowpoly и highpoly, сгладить lowpoly и нажать на кнопку Запечь.

Однако такой подход имеет некоторые недостатки:

  • Важно правильно настроить касательное пространство (tangent space). Так как мы сильно зависим от карты нормалей, малейшие изменения в её настройке будут при таком способе особенно заметны. В целом, нужно сделать так, чтобы в качестве касательного пространства программы 3d-моделирования, запекания и рендеринга использовали Mikkts, и чтобы все модели экспортировались и импортировались соответствующим образом.
  • Так как карта нормалей должна сильно сгибать нормали, карта нормалей, скорее всего, будет более разноцветной и иметь множество градиентов. То есть размер карты нормалей будет чуть больше, и с большей вероятностью будет содержать артефакты сжатия. Чтобы компенсировать это, при использовании данного метода обычно увеличивают разрешение карты нормалей или уменьшают степень её сжатия. Для игр на PC это не сильно снижает производительность, но для мобильных игр с большим количеством карт нормалей это может быть заметно.
  • Если убрать карту нормалей модели, то обычно из-за этого она выглядит плохо (особенно если имеет множество острых рёбер).
  • Добавление деталей карт нормалей текстурам (например, в Photoshop) может быть затруднено.
  • Нормали lowpoly с большей вероятностью будут сгибаться и не совпадать с поверхностью highpoly, и выше вероятность появления искажённых деталей.

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

Модели с острыми рёбрами (с настроенными smoothing groups/hard edges)


При такой методике мы разделяем нормали некоторых вершин, задавая группы сглаживания или выбирая резкие рёбра. Некоторые люди любят настраивать остроту рёбер при помощи угла (обычно 30, 45 и 60), но я предпочитаю задавать их вручную (параллельно думая о том, где будут находиться швы UV-развёртки).

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

image

Недостатки:

  • Обычно этот метод требует чуть больше работы, чем первый.
  • Необходимо разделять UV в местах острых рёбер, чтобы избежать видимых линий вдоль этих рёбер, что может усложнить работу с некоторыми UV-островами (например, при ручном рисовании части модели, имеющей разделённые UV, могут появиться нежелательные швы). К счастью, в таких программах, как Substance Painter, Mari и 3D Coat, есть решения этой проблемы.
  • Задание острых рёбер и разделение UV немного увеличивает количество вершин, однако обычно незначительно.

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

На плоских поверхностях мы можем использовать острые рёбра для привязки нормалей lowpoly к твердотельной (hard surface) модели. Если поверхности lowpoly и highpoly полностью совпадают (т.е. они обе плоские), то карта нормалей в этой области будет плоской, благодаря чему можно без проблем добавлять и удалять детали карты нормалей на текстуре.

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

Работа с настраиваемыми нормалями (и использование midpoly)


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

Важно правильно экспортировать, запекать и импортировать модели с настраиваемыми нормалями. Большинство 3D-движков понимает модели с настраиваемыми нормалями, однако по умолчанию эта опция обычно выключена.

image

Этот метод породил новый подход к моделированию под названием midpoly. Это скорее метод моделирования, а не текстурирования. В целом его смысл таков, что вместо генерирования деталей при помощи карт нормалей мы генерируем их, напрямую модифицируя нормали lowpoly.

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

Сначала мы добавляем на ребро фаску (bevel), расположенную в месте перехода. Затем мы модифицируем нормали получившихся вершин так, чтобы они смотрели в сторону основных граней. При этом нормали lowpoly будут направлены к более крупным граням, а переход направлений нормалей будет ограничен ребром с фаской.

image

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

Вот сравнение трёх описанных в статье методов:

image

Другие методики и эксперименты


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

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

    Я нашёл три способа реализации этой методики:
    • Использование специальной программы, которая приблизительно преобразует цветовую информацию в ориентацию нормалей, например, такой.
    • Создание версий спрайта в градациях серого, освещённого с разных сторон, и их использование для создания карты нормалей. Как мы знаем из второй части моей серии статей, карту нормалей можно считать сочетанием трёх изображений: освещённая слева модель хранится в красном канале, освещённая снизу/сверху в зелёном канале, а освещённая спереди в синем канале. То есть мы можем отрисовать спрайт, освещённый снизу/сверху, слева и спереди, и скомбинировать карту нормалей, используя эти изображения как её каналы, например, как в этой программе.
    • Создание карты высот и применение программы, преобразующей её информацию в карту нормалей: похоже на предыдущую методику, но гораздо быстрее, потому что нам нужно создать только одну дополнительную текстуру вместо трёх.
  • Рисованные вручную карты нормалей. Специально для тех, кто играет в Dark Souls и думает: слишком просто, надо пройти её вслепую. Такие методики обычно слишком медленны для применения в продакшене, и чаще всего лучше просто скульптить из плоскости Zbrush и экспортировать в карту нормалей. Вот некоторые из способов:
    • Создание карты высот и использование специальной программы для преобразования её информации в карту нормалей (как и в примере с пиксель-артом). Вероятно, самый быстрый и более удобный способ создания карты нормалей по сравнению с рисованием вручную.
    • По референсу сферы: при помощи сферы с картой нормали в качестве палитры мы можем сопоставить угол поверхности с соответствующим цветом на сфере.
    • Использование специальных кистей: В Krita есть специальная кисть, преобразующая наклон карандаша в информацию карт нормалей.
  • Композитинг карт нормалей при помощи текстур: используя библиотеку текстур карт нормалей, мы можем комбинировать их, чтобы добавлять детали в карты нормалей. Эти детали можно комбинировать вместе. Не забывайте, что смешивание карт нормалей не такой простой процесс, как можно подумать.
  • Фильтры изображений: некоторые программы могут аппроксимировать информацию карт нормалей на основе карт diffuse/albedo/basecolor. Обычно они неточны, но могут подойти для фоновых или мелких моделей.
  • Matcaps: маткапы (Matcaps) это упрощённые шейдеры, определяющие цвет пикселей на экране на основании направлений нормалей геометрии. Можно применить маткап к любой геометрии и отрендерить изображение, чтобы быстро сгенерировать текстуры карт нормалей. Это легко сделать в Zbrush для генерации повторяющихся текстур.
  • Карты нормалей деталей: в некоторых случаях можно дополнять сгенерированную из highpoly карту нормалей новыми деталями (например, можно скомбинировать карту нормалей скульпта головы с текстурами пор кожи), и загружать эту дополнительную карту нормалей только на определённом расстоянии, чтобы снизить потребление памяти, сохраняя при этом детализацию вблизи.
Подробнее..

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

21.05.2021 16:09:34 | Автор: admin
В фильмах или роликах с YouTube мы наблюдаем происходящее из одной точки, нам не доступны перемещение по сцене или смещение угла зрения. Но, кажется, ситуация меняется. Так, исследователи из Политехнического университета Вирджинии и Facebook разработали новый алгоритм обработки видео. Благодаря ему, можно произвольно изменять угол просмотра уже готового видеопотока. Что примечательно алгоритм использует кадры, которые получены при съемке на одну камеру, совмещение нескольких видеопотоков с разных камер не требуется.

В основе нового алгоритма нейросеть NeRF (Neural Radiance Fields for Unconstrained). Эта появившаяся в прошлом году сеть умеет превращать фотографии в объемную анимацию. Однако для достижения эффекта перемещения в видео проект пришлось существенно доработать.

Что именно умеет NeRF?



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


Сама по себе нейросеть умеет создавать 3D-изображение под разными углами из множества снимков. Также она может вычленять 2D-модели. Эти изображения переводят из объемных в плоскостные путем попиксельного переноса. Как именно?

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

Видеопоток с эффектом


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


Что касается статичной модели, то она устроена по тому же принципу, что и NeRF. Есть только одно отличие из кадра сразу удалили все движущиеся объекты.


С динамической моделью все намного интереснее. Для ее обработки не хватало кадров. Тогда нейросеть научили предсказывать кадры для объемного потока. Точнее кадры к каждому конкретному моменту времени t. Эти моменты условно назвали t-1 и t+1. Суть 3D-потока сводится к оптическому потоку, только в этом случае его строят для объемных объектов.

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

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

Подробнее..

Российские BIM-технологии проектирование генерального плана в Model Studio CS

26.05.2021 14:12:47 | Автор: admin

Мы продолжаем серию статей, посвященных технологиям информационного моделирования в промышленном и гражданском строительстве. Ранее в материале Российские BIM-технологии: комплексное проектирование на базе Model Studio CS мы рассказали, как комплекс Model Studio CS реализует на основе единой программной линейки концепцию среды общих данных, информационного моделирования и комплексного проектирования. Следующие статьи мы посвятим автоматизированным рабочим местам (АРМ) для специалистов различного профиля.

Начнем с Model Studio CS Генплан решения, предлагающего все необходимое для эффективной работы инженера-генпланиста. Программный комплекс построен по модульному принципу. Модуль Model Studio CS содержит общие основные команды. Инструменты для работы с объектами собраны в модуле CADLib Проект. Работа с поверхностями осуществляется средствами модуля Генплан, а для решения прикладных задач предназначен модуль Гео.

Model Studio CS Генплан: основные инструменты

Model Studio CS Генплан это система комплексного трехмерного проектирования объектов промышленного и гражданского назначения, обеспечивающая быстрое и удобное создание существующих и проектных поверхностей, размещение на плане зданий и сооружений, объектов благоустройства, а также выпуск проектной/рабочей документации. Продукт адресован специалистам отделов изысканий и генплана.

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

Технология совместной работы с единой базой Model Studio CS

Как и все продукты комплексной линейки трехмерного проектирования Model Studio CS, программный комплекс Model Studio CS Генплан позволяет работать на платформе nanoCAD Plus 21.0, nanoCAD Plus 11.1 или AutoCAD 2017-2022.

О решениях, положенных в основу коллективной работы, подробно рассказано в статье Российские BIM-технологии: комплексное проектирование на базе Model Studio CS, поэтому здесь лишь вкратце напомню основные моменты.

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

Коллективный доступ к комплексной BIM-модели и управлению инженерными данными информационной модели, структурирование, хранение, визуализация информационных моделей, их проверка на предмет коллизий осуществляются в среде общих данных CADLib Модель и Архив (рис.1).

В самом начале работы проектировщики, работающие в Model Studio CS, подключаются к базе проекта из специализированных приложений с помощью технологии CADLib Проект. Это позволяет осуществлять доступ к актуальным настройкам проекта и 3D-моделям, а также быстро публиковать изменения в общую базу данных.

Рис. 1. Отображение поверхностей в среде CADLib Модель иАрхивРис. 1. Отображение поверхностей в среде CADLib Модель иАрхив

База оборудования, изделий и материалов

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

Рис. 2. База данных оборудования, изделий и материаловРис. 2. База данных оборудования, изделий и материалов

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

Инструменты построения модели

Model Studio CS Генплан состоит из нескольких модулей:

  • Model Studio CS содержит общие основные команды;

  • CADLib Проект включает средства работы с объектами проекта;

  • модуль Генплан предоставляет инструменты для работы споверхностями;

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

Цифровая модель рельефа

Одной из важнейших задач при проектировании генплана является создание трехмерной цифровой модели рельефа. Такая модель формируется в виде 3D-граней и строится с использованием отметок и структурных линий. Model Studio CS Генплан позволяет быстро и просто построить модели как существующего, так и проектного рельефа.

В качестве исходных данных для проектирования (рис.3) могут использоваться:

  • отсканированные чертежи, по которым можно выполнять сколку существующего рельефа и ситуации;

  • облако точек, полученное в результате лазерного сканирования;

  • текстовый файл с точками, имеющими координаты XYZ;

  • поверхность, созданная в виде 3D-граней в любом другом программном обеспечении;

  • плоский чертеж формата *.dwg, на котором есть отметки в виде текста и маркера. Программа считывает высотное положение отметок из текста.

Рис. 3. Исходные данные для проектированияРис. 3. Исходные данные для проектированияРис. 4. Цифровая модель рельефаРис. 4. Цифровая модель рельефа

Для редактирования модели (рис.4) используются такие операции, как переброс ребер, изменение отметки, перемещение узла, вставка и удаление точек. По созданной поверхности создаются и подписываются горизонтали (рис.5).

Рис. 5. Горизонтали по существующей поверхностиРис. 5. Горизонтали по существующей поверхности

Для создания проектной поверхности и расчета вертикальной планировки (рис. 6) в Model Studio CS Генплан предложены удобные инструменты. На плане расставляются опорные точки и уклоноуказатели между ними. Точки динамически связаны с уклоноуказателями. При изменении уклона, расстояния или проектной отметки выбранные параметры пересчитываются. Выполняется расчет точек по заданному уклону и расстоянию и расчет откосов с выходом на заданную поверхность.

Рис. 6. Расчет вертикальной планировки площадкиРис. 6. Расчет вертикальной планировки площадки

Горизонтальная планировка

Посадку на генплан зданий и сооружений (рис. 7) можно осуществить несколькими способами:

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

  • отобразить проектируемые здания, созданные специалистами смежных разделов, из базы данных проекта в CADLib Модель и Архив;

  • применить специальную команду;

  • использовать возможности платформы.

Рис. 7. Расположение зданий и сооружений на генпланеРис. 7. Расположение зданий и сооружений на генплане

На генплане расставляются элементы благоустройства и озеленения (рис.8). Все элементы (рис.9) хранятся во встроенной библиотеке стандартных компонентов, открытой для пополнения пользователем.

Рис. 8. База данных оборудования, изделий и материалов в части благоустройстваРис. 8. База данных оборудования, изделий и материалов в части благоустройстваРис. 9. Пример благоустройства и озеленения площадкиРис. 9. Пример благоустройства и озеленения площадки

Выпуск документации

Математическое ядро Model Studio CS формирует чертежи (рис.10) на основе трехмерной модели. Программа генерирует планы, виды и разрезы, в автоматическом режиме проставляя отметки, выноски, позиционные обозначения и размеры.

Процесс получения чертежа прост, понятен каждому проектировщику и сводится к выполнению несложных действий:

  • определить линию разреза, а также его глубину и высоту, то есть установить границы вида на модели;

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

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

Рис. 10. Получаемые чертежиРис. 10. Получаемые чертежи

По всем объектам, размещенным в модели на генплане, можно автоматически получить ведомости и спецификации (рис.11), соответствующие требованиям ГОСТ. Предусмотрена возможность самостоятельной настройки шаблонов документов.

Рис. 11. Пример получаемых ведомостей и спецификацийРис. 11. Пример получаемых ведомостей и спецификаций

По расставленным зданиям и сооружениям автоматически формируется экспликация (рис.12).

Рис. 12. Пример экспликации зданий и сооруженийРис. 12. Пример экспликации зданий и сооружений

Работа с геоданными

Model Studio CS Генплан позволяет интегрировать геологическую модель (рис. 13) например, созданную в программе CREDO с трехмерной моделью Model Studio CS и использовать данные этой модели для генерации продольного профиля, а также для расчета земляных работ по геологическим слоям.

Рис. 13. Пример геологической моделиРис. 13. Пример геологической модели

Заключение

Model Studio CS Генплан это новый перспективный продукт, эффективный и простой в использовании, значительно расширяющий возможности платформ nanoCAD/AutoCAD, делающий работу инженера более комфортной и эффективной.

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

Ольга Белкина,
эксперт по решениям генплана
отдела комплексной автоматизации
в строительстве
ГК CSoft
E-mail:
belkina@csoft.ru

Подробнее..

Перевод Как работает рендеринг 3D-игр сглаживание с помощью SSAA, MSAA, FXAA, TAA и других методик

28.05.2021 10:14:13 | Автор: admin
image

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

Часть 1: обработка вершин

Часть 2: растеризация и трассировка лучей

Часть 3: текстурирование и фильтрация текстур

Часть 4: освещение и тени

Кто виноват


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

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

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


Разрешения 10 x 7 пикселей не совсем хватает для отображения этого треугольника без искажений

Некоторые сигналы меняются не в пространстве, а во времени, и здесь мы тоже получаем искажения при сэмплировании с заданными интервалами. Например, для преобразования аналоговой аудиодорожки в цифровую требуется измерять уровень звука через определённые доли секунды (допустим, в случае CD audio это каждые 0,02 миллисекунды).

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

В мире кинематографа это приводит к странно выглядящим движениям, например, колёса автомобиля как будто вращаются в обратном направлении. Это проявляется и в 3D-графике, когда частота кадров рендеринга сцены недостаточна для полной передачи движения объектов и персонажей, из-за чего края выглядят размытыми или зазубренными; последний дефект усугубляется пространственным искажением.

Хотя методики, используемые для решения этих проблем, имеют общее название anti-aliasing (AA), способы их решения в фильмах совершенно отличаются от способов в 3D-игре. В последних применяется множество методик, имеющих всевозможные названия. Но прежде чем мы приступим к общему обзору самых популярных алгоритмов, давайте поговорим о разрешении кадра и частоте кадров.

Зачем? Потому, что если бы и то, и другое было чрезвычайно велико, то искажение не представляло бы проблемы. Если взять старый бенчмарк, например 3DMark03, и запустить его на современной системе, то мы сможем сфокусироваться исключительно на пространственных искажениях.


Показанное выше изображение, взятое из первого теста Wings of Fury, сделано с разрешением 1280 x 720 пикселей. Четырнадцать лет назад, когда лучшими графическими картами были ATI Radeon 9800 XT и Nvidia GeForce FX 5900 Ultra, самые большие мониторы имели разрешение примерно 1600 x 1200, поэтому разрешение теста можно считать примерно низким или средним разрешением (сродни современному 1080p).


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

В этом следует винить относительно низкую степень сэмплирования, поэтому логичнее всего будет её увеличить; давайте теперь рассмотрим ту же сцену в 4K (3840 x 2160 пикселей).


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

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

В случае такой старой программы, как 3DMark03, на современном PC переход от 1280 x 768 к 3840 x 2160 снизил среднюю частоту кадров с 1670 до 1274 FPS, то есть увеличение количества пикселей на 740% снизило производительность всего на 24%. Однако в более новых программах ситуация будет иной.

Это легко продемонстрировать, запустив современный 3DMark при различных разрешениях; на графике ниже показана средняя частота кадров первого теста графики в бенчмарке Time Spy.


Переход с 720p на 4K это повышение разрешения на 800%, но частота кадров упала на 81%. Хотя игры могут и не демонстрировать этот паттерн в точности, но современные AAA-игры не очень от него отличаются. Это говорит нам, что если мы хотим максимально снизить влияние искажений, нам нужен способ получше, чем повышение разрешения растра чем ниже частота кадров, тем хуже становятся временные искажения.

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

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

Supersampling anti-aliasing (SSAA)


Этот способ, часто называемый также full scene anti-aliasing самый старый и простой из всех. Он заключается в рендеринге сцены с повышенным разрешением, с последующим сэмплированием и смешением результата в меньшее количество пикселей. Например, монитор может иметь максимальное разрешение 1920 x 1080, но игра рендерится с разрешением 3840 x 2160, а затем картинка масштабируется и передаётся на экран.

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

Разумеется, мощности современных GPU позволяют использовать более сложные алгоритмы сэмплирования и смешения. Но давайте вкратце рассмотрим, как это работает. На изображении ниже показано действие классического 4x SSAA. 4x означает, что вычислением арифметического среднего смешивается вместе 4 сэмпла (также называемых taps) для получения окончательного цвета. Для этого растровое разрешение увеличивается по каждой из осей в два раза.


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

Описанный выше паттерн и способ смешения называется box filter, однако существует его популярное улучшение с использованием повёрнутой сетки позиций сэмплов (RGSS). Проблема SSAA заключается в том, что все эти дополнительные пиксели необходимо обрабатывать, и как мы видели из тестов 3DMark, повышение разрешения растра запросто может сильно снизить частоту кадров.

По большей части суперсэмплирование можно заменить более эффективными алгоритмами, однако оно обрело новую жизнь в качестве параметра драйверов графических карт AMD и Nvidia. AMD называет свою технологию Virtual Super Resolution (VSR), а Nvidia Dynamic Super Resolution (DSR).

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

Multisample anti-aliasing (MSAA)


Этот способ возник как результат работы исследовательских лабораторий Silicon Graphics в начале 90-х. По сути, это SSAA, но применённая только там, где это на самом деле нужно. Ну, на самом деле, внутри технологии есть не только это, но такое объяснение поможет вам понять, как работает алгоритм.

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

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


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

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


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

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

Затем, как и в случае с SSAA, мы сэмплируем и смешиваем детализированный буфер, приводя его к требуемому разрешению, и всё! Мы получили кадр без искажений. С точки зрения производительности, мы выполняли пиксельные шейдеры только для относительно малого количества точек, но для этого нам понадобилось создать и хранить пару буферов высокого разрешения.

Поэтому для multisample anti-aliasing требуется много видеопамяти и широкий канал передачи данных (плюс возможность быстрого считывания/записи в z-буферы), зато эта методика не требует большой вычислительной нагрузки на шейдеры. Давайте воспользуемся старым примером кода AMD, чтобы посмотреть, как она выглядит и чем отличается от SSAA.


Код запускает сцену с простыми текстурами и освещением, но со множеством геометрии, поэтому искажения на её рёбрах бросаются в глаза намного сильнее. В верхнем левом углу есть любопытная информация для рендеринга каждого кадра в среднем требуется 0,18 миллисекунд, а для смешивания в окончательный результат всего 0,02 мс. Буфер цвета (тот, который мы видим) занимает 7,4 МБ, как и буфер глубин.

Также можно приблизить отдельные области картинки, чтобы увидеть всю ужасность искажений. Не забывайте, что мы могли бы отрендерить всё это в более высоком разрешении, но это просто увеличит время рендеринга. Но если мы применим к сцене 4x SSAA, то именно это и произойдёт.


Обратите внимание, что в представленном выше изображении время рендеринга увеличилось до 0,4 мс (рост на 122%), а время смешивания (под названием Resolve) удвоилось. Кроме того, размер буферов цвета и глубин увеличился в четыре раза. Такова цена использования SSAA, и хотя любой современный GPU с лёгкостью справится с этой сценой, в новых 3D-играх ситуация окажется ужасной.

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


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

Даже искажения на рёбрах примитивов кажутся меньшими, однако это больше связано с использованным паттерном сэмплирования, а не с самим принципом работы MSAA. На самом деле, если посмотреть на текстуру стены в увеличенной области, становится очевидным один недостаток multisample AA.

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

Что же делать, если методики supersampling и multisampling anti-aliasing неидеальны?

Fast approximate anti-aliasing (FXAA)


В 2009 году Nvidia представила другой способ улучшения изломанных краёв фигур в 3D-сцене. SSAA пользуется исключительно грубой силой, MSAA аппаратными функциями и трюками в коде. FXAA спроектирована так, чтобы выполняться исключительно через шейдеры. После появления этой методики её несколько раз совершенствовали и сегодня она активно применяется в играх.

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

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


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

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

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

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

Во-вторых, она сглаживает все рёбра, а не только периметры фигур. Например, текстуры с прозрачностью (часто используемые для дыма, мусора и листвы) будут сглажены, на что MSAA не способна.


Без AA (слева) и FXAA (справа) обратите внимание, что деревья и элероны крыла выглядят намного плавнее

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

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

Существуют и другие полноэкранные алгоритмы поиска рёбер. Источником вдохновения для разработчиков FXAA стало созданная Intel Morphological anti-aliasing (MLAA); эта методика была усовершенствована разработчиком игр Crytek и испанским Университетом Сарагосы, которые дали ей название Enhanced Sub-pixel MLAA (сокращённо SMAA).

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

Temporal anti-aliasing (TAA)


Пока мы рассматривали только методики для устранения визуального влияния пространственных искажений. Для борьбы с временными искажениями (temporal aliasing), возникающими из-за того, что 3D-игры генерируют дискретные сэмплы непрерывного движения, чаще всего используется следующий алгоритм.

Мы начинаем с рендеринга кадра и его отображения; однако также мы сохраняем значения пикселей в блок памяти, называемый буфером истории (history buffer). Затем рендерер переходит к следующему кадру последовательности и обрабатывает его, но перед его отображением сэмплирует буфер истории, а результаты сэмплирования смешиваются с текущим кадром. Затем в буфер истории сохраняется этот результат, скопированный для создания окончательного изображения, а готовый буфер помечается как готовый для отображения на мониторе.


Общая схема временного сглаживания.

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

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

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


Самый популярный алгоритм TAA.

Временное сглаживание (Temporal AA) может создавать избыточное размытие, а также проблему под названием ghosting, при которой края движущихся объектов выглядят размазанными, а не смягчёнными.

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

Кроме использования значений скоростей, большинство реализаций алгоритма TAA выполняют ещё один процесс проверки сэмплов истории; это не позволяет использовать значения из предыдущих кадров, не относящиеся к текущему кадру (например, они могут оказаться скрытыми за сдвинувшимся объектом). В этой методике обычно используется ограничивающий параллелепипед, выровненный по координатным осям (axis-aligned bounding box, AABB), в котором по осям отложена хроматичность буфера истории; она отсекает все пиксели, имеющие цвет за пределами этих границ.

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


Без AA (слева) и TAA (справа) обратите внимание на размытие деталей на крыле

Для разработчиков кодировать всё это значительно сложнее, чем добавить в игру SSAA или MSAA. Но современные GPU способны быстро вычислять все необходимые шейдеры; в то время как алгоритмы supersampling и multisampling для каждого кадра требуют множества сэмплов, TAA, по сути, распределяет эти сэмплы на несколько кадров. Это означает, что в играх, не сильно ограниченных объёмом вычисляемых шейдеров, TAA можно реализовать ценой относительно малого снижения производительности.

Кроме того, TAA хорошо работает с отложенным освещением и может использоваться с аналогами FXAA и SMAA, что приводит к ещё большему улучшению графики. К сожалению, эта методика подвержена чрезмерному размытию и возникновению артефактов мерцания вдоль рёбер с высокой контрастностью. Однако вычислительная мощь GPU, похоже, ещё долго не выйдет на плато, и подобные техники можно совершенствовать, используя более сложные методы определения способа сэмплирования и проверки буфера истории.

И это ещё не всё!


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

Например, когда Nvidia выпустила серию графических карт GeForce 9, то объявила и о создании модифицированной версии MSAA под названием Multi-Frame Sampled Anti-aliasing (MFAA). По сути, GPU изменяет паттерн сэмплирования с каждым последующим кадром, то есть на кадр приходится меньшее количество создаваемых и смешиваемых сэмплов.


Среднее от нескольких кадров и создаваемый эффект практически такие же, как при обычном MSAA, но с меньшим снижением производительности. К сожалению, этот алгоритм можно реализовать в играх, разрабатываемых под руководством Nvidia, и он недоступен во всех проектах. Однако он всё равно существует и его можно включить в панели управления драйвером GeForce.

Позже этот разработчик GPU вложил значительные ресурсы в разработку алгоритма сглаживания с использованием искусственного интеллекта под названием Deep Learning Super Sampling (DLSS), впервые появившегося в 2018 году вместе с выпуском чипов Turing.

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


Хотя основное преимущество DLSS заключается в повышении производительности (например, рендеринг выполняется при 1080p, но нейросеть повышает разрешение до 1440p), система, по сути, применяет AA, потому что её целевыми данными является изображение.

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

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

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

Советы и трюки SOLIDWORKS

02.06.2021 10:16:01 | Автор: admin

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

В этой заметке мы ответим на некоторые наиболее актуальные вопросы. Минимум воды, максимум пользы. Итак, начинаем наш краткий ликбез.

1. Как установить существующую библиотеку материалов

Файлы с расширением .sldmat содержат сведения о механических и физических свойствах материалов. Если вы скачали библиотеку с сайта i-tools.info, следующие 5 шагов помогут вам ее установить. Для добавления библиотеки необходимо открыть любую деталь в SOLIDWORKS:

1. В дереве конструирования FeatureManager нажимаем правой кнопкой мыши на Материал.

2. Выбираем пункт Редактировать материал.

3. В левом поле открывшегося окна кликаем в любом месте правой кнопкой мыши и выбираем Открыть библиотеку.

4. Выбираем директорию, в которой находится файл .sldmat, либо копируем его в папку с пользовательскими материалами SOLIDWORKS. Уточнить папку, выбранную по умолчанию, можно в разделе Настройки пользователя Месторасположение файлов Отобразить папки для Базы данных материалов.

5. Выбираем файл с расширением .sldmat и нажимаем кнопку Открыть.

Библиотека установлена! Если она не отображается в окне, необходимо закрыть и вновь открыть окно редактирования материала.

2. Можно ли работать на любом компьютере с установленным SOLIDWORKS, используя лишь свою лицензию?

ДА! Это называется онлайн-лицензирование SOLIDWORKS Online Licensing. Вам потребуются лишь компьютер с доступом в интернет и SOLIDWORKS выше версии 2018 года.

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

Можно сказать, это лицензия SOLIDWORKS, которая находится в облаке.

3. В чем отличие SOLIDWORKS Simulation Standard и пакета Simulation Standard, входящего в SOLIDWORKS CAD Premium?

a) В SOLIDWORKS CAD Premium нельзя строить диаграмму усталости, усталостные напряжения и получать количество циклов до разрушений.

b) В SOLIDWORKS Simulation Standard доступен анализ тенденций, то есть построение зависимостей в результатах различных повторов статического исследования. Например, меняя нагрузку, можно отслеживать напряжение, перемещение и т.д.

4. Как показать основные плоскости компонентов в сборке?

Для этого нужно включить Просмотр плоскостей:

А затем выбрать значок Скрыть / Показать основные плоскости:

5. Как выбирать спрятанные грани, не применяя функцию Скрыть деталь?

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

6. Как посмотреть на деталь из сборки, не открывая деталь отдельно?

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

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

Хотите узнать больше? Подписывайтесь на наш YouTube-канал и изучайте SOLIDWORKS самостоятельно. Нужно обучение с профессионалами? Переходите по ссылке и выбирайте курс.

Автор: Максим Салимов, технический специалист ГК CSoft, solidworks@csoft.ru

Подробнее..

Краткая история 3D в видео-играх

03.06.2021 10:20:31 | Автор: admin

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

Я большой фанат видеоигр, работаю в 3D уже 15 лет, но ни разу не встречал последовательно написанной истории развития 3D-графики в гейм-индустрии и решил написать ее сам. Копнув в историю, я нашел много забавных вещей: например, что первую 3D-игру создали, пользуясь служебным положением, ученые NASA на лучших компьютерах своего времени; как пришли и ушли аркадные автоматы, как эллипсоидный движок Ecstatica позволял делать идеально круглые ягодицы персонажей 94 году и многое другое.

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

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

Пролог. Лаборатории NASA


Забавный факт, что первыми создателями и геймерами в 3D-видеоигры были программисты и ученые NASA.


Maze War шутер, где игроки перемещаются по лабиринту, другие участники игры представлены на экране в виде глазных яблок

Первой трехмерной игрой был Maze War (1973) многопользовательский шутер, где игроки в виде глазных яблок перемещаются по лабиринту и убивают друг-друга. Ее создали на лучших компьютерах того времени Imlac PDS-1 ценой в 8 тысяч долларов (4 недорогих автомобиля) в свободное время два программиста, работавших в Исследовательском центре Эймса NASA. Движения игроков были дискретными а камера могла поворачиваться ровно на 90 градусов по горизонтальной оси.


Imlac PDS-1. 16 bit. 8 16 Kb RAM с магнитным сердечником

А в 1975 году появилась Spasim трехмерный многопользовательский космический симулятор на 32 игрока.


Spasim в нее также играли с помощью оборудования ценой в несколько миллионов долларов

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

Система состояла из сотни терминалов расположенных в учебных заведениях и была построена вокруг суперкомпьютера CDC Cyber 73, ценой в несколько миллионов долларов. При этом игра работала со скоростью 1 кадр в секунду (1 fps). Создать игру удалось благодаря использованию системы передового программного языка TUTOR.



Компьютерный зал CDC Cyber 170, 1986. 25 Mhz и 8 Mb RAM

Затем 3D игры перестали быть эксклюзивом секретных лабораторий NASA и началась эпоха коммерческих и доступных широкой аудитории трехмерных видеоигр.

Предтечи


1980 Wirframe-каркасы / векторные контуры


Пока понятие домашний компьютер лишь зарождалось, а видеоускорители не существовали, передовые игровые технологии разрабатывались для аркадных автоматов. Выход трехмерной игры стал возможным благодаря процессору MOS Technology M6502 (1.512 Mhz), и использованию сопроцессора Math Box. Их производительность позволила рендерить простейший вид трехмерной графики wireframe.



Wireframe. Отображение только ребер, грани же остаются прозрачными

В 1980 году на аркадном автомате выходит игра Battlezone от Atari. От первого лица игрок управляет танком и перемещаясь по трехмерному полю боя стреляет в другие танки.

Благодаря новизне игрового процесса и трехмерной графике игра долго была популярна и позже в 1983 году была портирована на Atari 2600, а позже и на другие домашние игровые консоли, а также получила ремейк в 1998 году. Игра выглядела как цифровой интернет мир будущего в фантастике XX века (сейчас такой визуальный стиль называют ретро-вейв).

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


1983 Закрашенные полигоны


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



Закрашенные полигоны. Полигоны залиты цветом и применяется плоская модель теней

Первой такой игрой стала игра в жанре shootem up I, Robot от Atari. Цель игры пройти 126 уровней, перекрасив красные квадраты в синий цвет, уничтожив щит и глаз Старшего брата. После выпуска игры I, Robot получила негативные отзывы критиков и не окупила затрат на разработку. Было произведено примерно 7501500 автоматов, некоторые из которых сохранились до сих пор. В настоящее время игровые автоматы для этой игры являются редким предметом коллекционирования, а игра получила запоздалое признание за инновационную трехмерную графику.

В аркадном автомате для I, Robot, использовался 8-разрядный процессор Motorola 6809, мощностью 1,5 Mhz.


1985 Масштабируемые спрайты




Масштабируемые спрайты. 2D спрайт увеличивается или уменьшался в зависимости от удаления объекта от камеры

Масштабируемые спрайты использовались на аркадном автомате в игре Space Harrier (1985) от SEGA динамичноом 3D шутере Shoot 'em up от третьего лица в сюрреалистичном мире, наполненном яркими цветами.

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

Чтобы создать ощущение 3D-глубины масштаб спрайта увеличивался или уменьшался в зависимости от удаления объекта от камеры. Хотя это было и не в полной мере 3D, это была уже 16-битная картинка требующая хорошей производительности. В сердце автомата было установлено два 32-битных процессора Motorola 680x0 мощностью в 10 Mhz, а за звук отвечал Yamaha YM2203 (4 Mhz).

Как и предыдущие игры со временем Space Harrier была портирована на домашние игровые системы Sega 32X, Sega Saturn, Sharp X68000, а позже и поставлялась в составе Shenmue для Dreamcast и Xbox.


Space Harrier (1985) сияет своей детализацией на фоне 3D игр того времени

1994 Великий год в становлении видеоигровой 3D графики


1994 был годом, когда домашние игровые системы стали достаточно мощными, чтобы двигать прогресс игрового 3D. Видеоигровая графика начинает свое шествие навстречу фотореализму.

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

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



Затенение по Гуро. Сглаживание цветовых переходов между гранями полигонов

Это позволило объектам без жестких граней реалистично реагировать на свет. Star Wars: Tie Fighter (1994) была первой игрой где использовался данный метод. Она вышла на домашнем компьютере под управлением MS-DOS 4.0 и требовала процессор intel i386 и частотой 12-40 Mhz и 2 Mb RAM.


Star Wars: Tie Fighter (1994)

В этом же году выходит первое поколение консолей, способных рендерить 3D: Nintendo 64 и Playstation.

Nintendo 64 центральный процессор NEC VR4300 (93, 75 Mhz), вспомогательный процессор для обработки графики и звука Reality Co-Processor (RCP) частотой в 62,5 Mhz, разделенный внутри на два основных компонента Reality Drawing Processor (RDP) и Reality Signal Processor (RSP). Друг с другом компоненты обмениваются данными через 128-разрядную шину с пропускной способностью в 1,0 ГБ/с. RSP это 8-разрядный целочисленный векторный процессор на основе MIPS R4000. Он программируется микрокодом, что позволяет значительно изменять функциональность процессора, если потребуется, для различных видов работ, точности и загрузки. RSP выполняет геометрические преобразования, обрезку, вычисления связанные с освещением, обработку треугольников, и обладает производительностью примерно в 100 000 полигонов в секунду.

Playstation центральный процессор MIPS R3000A-совместимый (R3051) 32-разрядный RISC-микропроцессор, работающий на частоте 33,8688 Mhz, ОЗУ 2 Мб + видео ОЗУ 1 Мб + аудио ОЗУ 512 Кб. Что позволяло получить реальную производительность: 360 000 полигонов в секунду/ 180 000 текстурированных и освещенных полигонов в секунду.

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



Существовала виртуальная 3D сетка-таблица, к ячейкам которой привязывались полигоны. Поэтому создавалось ощущение подергивания поверхностей.

Тем не менее консоли пятого поколения подняли планку графики в домашних видеоиграх и запустил тренд на использование 3D вышли Need For Speed, Tekken, Super Mario 64.

Все еще 1994. Ecstatica эллипсоидный движок


В то время, как все практиковали ставшей сегодня традиционным метод полигонального 3D, Эндрю Спенсер пишет движок в котором все состоит из эллипсоидов. Так появляется survival horror игра Ecstatica.

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



Эллипсоидный движок. Экзотический подход к созданию 3D из сфер, а не полигонов.

Ecstatica работала на MS-DOS и требовала процессора intel pentium (60 Mhz).



Зрелость. Эпоха шейдеров и видеоускорителей


Производительность устройств росла и развитие технологий рендера продолжилось в нескольких направлениях:

  • Увеличение количества полигонов и разрешения текстур: в 1998 году в Tomb Raider III использовались текстуры 64x64 px, а в 2016 в Uncharted 4 4096x4096 px.
    Если в 2001 году у Мастера Чифа в Halo модель имели 2000 полигонов, то в 2017 в Mass Effect Andromeda около 60000 полигонов. Но не только количество полигонов и разрешение текстур влияет на финальную картинку.


Разница в разрешении текстур в 1998 и 2016 годах. Справа видно что для одного шейдера стали использоваться несколько разных карт.

  • Из кино пришли пост-эффекты (эффекты которые накладываются на изображение уже поверх отрендеренного кадра): такие как наложение бликов-флееров, виньетки, инверсии, tone mapping, color grading и прочих эффектов. Также из кино приходит рендер по слоям/каналам с сохранением кадров с информацией о глубине, движении, тенях в буфере. Отличная статья о том как это устроено в GTA V: http://personeltest.ru/aways/habr.com/ru/company/ua-hosting/blog/271931/

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


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



Шейдер 2000 г. (простая текстура цвета), и шейдер 2006 г. (детали на камне реагирующие на угол падения света, поддельные отражения на сферах, мягкие тени)

Далее несколько лет совершенствований уже существующих технологий: Появляются альтернативы Normal Map в виде Parallax map, которые делают объемные элементы текстур еще более реалистичными, совершенствуется screen space reflection, появляется AO, Physically Based Rendering, и т.д. А затем случается следующий эволюционный шаг рейтрейсинг.



Рейтрейсинг. Метод рендера при котором просчитывается настоящее поведение света и отражений.

Трассировка лучей (Ray tracing; рейтрейсинг) в компьютерных играх это решение для создания реалистичного освещения, отражений и теней, обеспечивающее более высокий уровень реализма по сравнению с традиционными способами рендеринга. Nvidia Turing, и ATI 6000 стали первыми GPU, позволяющими проводить трассировку лучей в реальном времени. В этом им помогают нейросети и искусственный интеллект, т.к. чистой производительности, к сожалению, все еще не хватает чтобы рендер происходил в достаточном разрешении.

Вскоре выходит демо Unreal Engine 5 запущенное на Playstation 5 и демонстрирует что в реальном времени можно просчитывать не только свет, тени, но и proxy-геометрию, это такой уровень детализации который используется в кино (полигоны настолько маленькие и их настолько много что они выглядят как шум). Раньше же как правило сначала создавалась высокополигональная модель, затем часть деталей сохранялась только в текстурах а количество полигонов максимально сокращалось на благо оптимизации.

Чуть больше чем за 40 лет от голой полигональной сетки видеоигровая индустрия пришла к настоящему видеореализму.


Демо Unreal Engine 5.

Особенно впечатляет когда включается полигональная сетка. Сравните её с сеткой в Battlezone 1980

Эпилог


Что нового ожидать в видеоигровой графике? Индустрия созрела и, к сожалению, технологии потеряли в своей загадочности. Игровая графика в течение последних 20 лет гналась за кинографикой. Постепенно в рендер в реальном времени приходили технологии из кино. Рендер кадра занимал часы теперь занимает 1/60 секунды. Видеоигровые технологии догнали кинотехнологии и в чем-то можно сказать перегнали. Хорошо известен пример сериала Мандалорец, где использовался видеоигровой движок Unreal 4. Круг замкнулся.

Технологии стали доступны дома теперь каждый может делать видеоигры и спецэффекты Голливудского уровня. И не это ли фантастика.





Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Ошибку Rockstar может совершить каждый (и я тоже)

12.06.2021 16:15:59 | Автор: admin

Несколько месяцев назад в новостях всплыла потрясающая статья [переводы на Хабре: один и второй] о Grand Theft Auto Online.

Советую прочитать статью целиком, но если вкратце, GTA Online имела внезапно квадратичную производительность при парсинге большого JSON-блоба (из-за многократных вызовов strlen); после устранения этой ошибки время загрузки уменьшилось почти на 70%.

Это вызвало оживлённые дискуссии: в этом виноват C? Или, возможно, "web shit"? Или капитализм и его стимулы?

Однако все были солидарны в одном: они бы ни за что не написали подобной глупости.

(Вы уже чувствуете, что надвигается?)


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


Благодаря продуманному коду она открывает 97-мегабайтный двоичный файл STL на Macbook Pro 2013 года всего за 165 миллисекунд. Это потрясающая скорость.

Из соображений совместимости я написал небольшой парсер и для ASCII STL.

ASCII STL это формат обычного текста с плохой спецификацией, который выглядит вот так:

solid cube_corner          facet normal 0.0 -1.0 0.0            outer loop              vertex 0.0 0.0 0.0              vertex 1.0 0.0 0.0              vertex 0.0 0.0 1.0            endloop          endfacet          facet normal 0.0 0.0 -1.0            outer loop              vertex 0.0 0.0 0.0              vertex 0.0 1.0 0.0              vertex 1.0 0.0 0.0            endloop          endfacet          ...endsolid

Я написал чрезвычайно надёжный парсер, добавив в комментарий такое описание:

/*  Самый либеральный парсер ASCII STL: игнорирует всё, кроме *  слова 'vertex', а затем одно за другим считывает три значения float. */

Загрузка ASCII STL всегда казалась немного медленной, но я предполагал, что причина этого в неэффективном текстовом формате.

(Тучи сгущаются.)



За несколько дней произошло несколько событий:


Вот логи загрузки 1,5-мегабайтного ASCII STL метками времени (в секундах):

[erizo] (0.000000) main.c:10      | Startup![erizo] (0.162895) window.c:91    | Created window[erizo] (0.162900) window.c:95    | Made context current[erizo] (0.168715) window.c:103   | Initialized GLEW[erizo] (0.178329) window.c:91    | Created window[erizo] (0.178333) window.c:95    | Made context current[erizo] (1.818734) loader.c:109   | Parsed ASCII STL[erizo] (1.819471) loader.c:227   | Workers have deduplicated vertices[erizo] (1.819480) loader.c:237   | Got 5146 vertices (7982 triangles)[erizo] (1.819530) loader.c:240   | Waiting for buffer...[erizo] (1.819624) loader.c:326   | Allocated buffer[erizo] (1.819691) loader.c:253   | Sent buffers to worker threads[erizo] (1.819883) loader.c:258   | Joined worker threads[erizo] (1.819887) loader.c:279   | Loader thread done[erizo] (1.821291) instance.c:32  | Showed window

С момента запуска до отображения окна прошло больше 1,8 секунды!

Посмотрев на парсер ASCII свежим взглядом, я увидел, что причина очевидна:

    /*  The most liberal ASCII STL parser:  Ignore everything except     *  the word 'vertex', then read three floats after each one. */    const char VERTEX_STR[] = "vertex ";    while (1) {        data = strstr(data, VERTEX_STR);        if (!data) {            break;        }        /* Skip to the first character after 'vertex' */        data += strlen(VERTEX_STR);        for (unsigned i=0; i < 3; ++i) {            SKIP_WHILE(isspace);            float f;            const int r = sscanf(data, "%f", &f);            ABORT_IF(r == 0 || r == EOF, "Failed to parse float");            if (buf_size == buf_count) {                buf_size *= 2;                buffer = (float*)realloc(buffer, buf_size * sizeof(float));            }            buffer[buf_count++] = f;            SKIP_WHILE(!isspace);        }    }

Можно заметить, что в коде есть sscanf, считывающая одно значение float из начала потока данных и каждый раз проверяющая длину всей строки.

Да, я совершил ту же ошибку, что и программисты, работавшие над GTA Online: написал внезапно квадратичный парсер!

Замена вызова sscanf на вызов strtof снизила время загрузки почти в 10 раз: с 1,8 секунды до 199 миллисекунд.

[erizo] (0.000000) main.c:10      | Startup![erizo] (0.178082) window.c:91    | Created window[erizo] (0.178086) window.c:95    | Made context current[erizo] (0.184226) window.c:103   | Initialized GLEW[erizo] (0.194469) window.c:91    | Created window[erizo] (0.194472) window.c:95    | Made context current[erizo] (0.196126) loader.c:109   | Parsed ASCII STL[erizo] (0.196866) loader.c:227   | Workers have deduplicated vertices[erizo] (0.196871) loader.c:237   | Got 5146 vertices (7982 triangles)[erizo] (0.196921) loader.c:240   | Waiting for buffer...[erizo] (0.197013) loader.c:326   | Allocated buffer[erizo] (0.197082) loader.c:253   | Sent buffers to worker threads[erizo] (0.197303) loader.c:258   | Joined worker threads[erizo] (0.197306) loader.c:279   | Loader thread done[erizo] (0.199328) instance.c:32  | Showed window



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

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

(Очевидно, мораль истории такова: не используйте sscanf для многократного парсинга одиночных токенов из начала строки; уверен, у вас всё будет нормально, если вы просто избежите этого.)



На правах рекламы


VDSina предлагает мощные и недорогие VPS с посуточной оплатой. Интернет-канал для каждого сервера 500 Мегабит, защита от DDoS-атак включена в тариф, возможность установить Windows, Linux или вообще ОС со своего образа, а ещё очень удобная панель управления серверами собственной разработки. Обязательно попробуйте!

Подробнее..

Российские BIM-технологии проектирование архитектурно-строительной части в Model Studio CS

16.06.2021 16:13:28 | Автор: admin

Введение

Проектирование сложных общественных игражданских зданий исооружений невозможно без надежных исовременных средств автоматизации проектирования. Одним из таких инструментов, чья эффективность уже доказана на практике, стала линейка продуктов Model Studio CS. Созданная для российской инженерной школы, она включает в себя лучшие мировые достижения вобласти информационных технологий иСАПР, учитывает российскую технологию проектирования изарубежный опыт, предлагает русскоязычную среду проектирования ибазы данных материалов и изделий.

Model Studio CS современная и мощная российская программная система, обеспечивающая все необходимое для комплексного параллельного трехмерного информационного проектирования.

Продолжая знакомить читателей с материалами, представленными ГК CSoft на вебинаре Унифицированные АРМ на базе Model Studio CS и nanoCAD, который состоялся 20октября 2020г., предлагаем вашему вниманию обзор АРМ Строителя (АР, КМ, КЖ).

В основу АРМ Строителя положен Model Studio CS Строительные решения эффективный и простой в использовании программный продукт для быстрого и удобного создания цифровой трехмерной модели объектов промышленного и гражданского назначения по разделам АР, АС, КМ и КЖ. Несомненным его плюсом является мультиплатформенность: в качестве графической платформы может использоваться и nanoCAD, стремительно набирающий популярность в нашей стране, и AutoCAD версий 2017-2022.

Технология совместной работы с единой базой Model Studio CS

О решениях, на которых базируется коллективная работа, подробно рассказано в статье Российские BIM-технологии: комплексное проектирование на базе Model Studio CS, поэтому здесь ограничимся кратким упоминанием основных моментов.

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

Коллективный доступ к комплексной BIM-модели и управлению инженерными данными информационной модели, структурирование, хранение, визуализация информационных моделей, их проверка на предмет коллизий осуществляются в среде общих данных CADLib Модель и Архив.

Информационная модель в CADLib Модель и АрхивИнформационная модель в CADLib Модель и Архив

В самом начале работы проектировщики, использующие Model Studio CS, подключаются к базе проекта из специализированных приложений с помощью технологии CADLib Проект. Это позволяет осуществлять доступ к актуальным настройкам проекта и 3D-моделям, а также быстро публиковать изменения в общую базу данных.

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

Для выполнения прочностного анализа конструкции предусмотрена возможность прямой, без использования промежуточных форматов, передачи 3D-модели здания и данных по нему из Model Studio CS Строительные решения в расчетные комплексы ЛИРА-САПР, ЛИРА-СОФТ и SCAD Office. В случае ЛИРА-СОФТ обеспечена двусторонняя связь: модель можно не только передать для расчетов, но и получить обратно.

Экспорт модели железобетонного каркаса в расчетные комплексыЭкспорт модели железобетонного каркаса в расчетные комплексы

Кроме того, Model Studio CS Строительные решения обеспечивает выпуск проектной и рабочей документации в соответствии с требованиями ГОСТ, включая автоматический расчет объемов работ. Недавно в программе реализована интеграция с системой АВС для разработки сметной и ресурсной документации.

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

Работа с базой данных

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

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

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

Model Studio CS Строительные решения предоставляет все необходимое для использования базы данных: средства поиска (простого или с предварительно заданными условиями), инструменты работы с предопределенными выборками, классификаторами. Предусмотрена возможность без вставки в чертеж просмотреть, как выглядит объект, и получить полную информацию о нем: марку, размеры, название завода-изготовителя, материал, вес, состав и другие данные, необходимые для принятия оптимального решения.

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

Ознакомившись с базой данных, сделаем краткий обзор технологий проектирования в Model Studio CS Строительные решения. Прежде всего остановимся на технологии проектирования разделов АР и АС.

Model Studio CS Строительные решения: основные инструменты

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

Размещение ограждающих конструкцийРазмещение ограждающих конструкций

Теперь о технологии проектирования раздела КМ, позволяющей работать с базой профилей металлопроката и сталей по ГОСТ, СТО и ТУ. Отметим, что при изменении нормативных документов база данных пополняется и актуализируется. Обеспечена возможность использовать библиотеку типовых параметрических узлов, а также разрабатывать пользовательские параметрические узловые соединения и сохранять их как библиотечные элементы.

Формирование трехмерной информационной модели по разделу КМФормирование трехмерной информационной модели по разделу КМ

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

Отдельная тема технология проектирования кабельных эстакад. База данных содержит огромное количество параметрических объектов (фундаменты, стойки, балки кабельной эстакады). Процесс построения кровли эстакады автоматизирован. В новых версиях Model Studio CS база данных пополнилась узлами сопряжения кровли эстакады: крестообразными и примыкающими соединениями, а также угловым соединением, в котором можно редактировать угол наклона примыкающих частей кровли, а подрезка будет осуществляться автоматически. При работе со стойками и фундаментами поддерживается расстановка объектов с привязкой к рельефу местности.

Кабельная эстакадаКабельная эстакада

Теперь рассмотрим технологию проектирования раздела КЖ. Model Studio CS Строительные решения предоставляет проектировщикам широкие возможности для работы со сборными и монолитными железобетонными конструкциями из базы данных. Например, существует команда, позволяющая производить детальное армирование монолитных конструкций с учетом защитного слоя бетона. В программе заложены такие объекты, как рабочая арматура, сварные арматурные сетки по ГОСТ, арматурные изделия хомуты, шпильки, скобы. После создания конструкции из набора отдельных элементов можно заняться сборкой и маркировкой элементов армирования для последующего сохранения готового изделия в базу данных. По этим арматурным изделиям можно выводить табличные документы в виде ведомости расхода стали, а также групповую спецификацию.

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

Пример армирования столбчатого фундаментаПример армирования столбчатого фундамента

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

Выпуск документации

После формирования модели в Model Studio CS Строительные решения мы можем приступить к выпуску проектной документации. Планы, разрезы и сечения здесь формируются автоматически. Также автоматически по заранее определенным правилам оформляется графика (с возможностью проставить выноски, отметки уровня, оси). Автоматизирован и процесс получения табличной документации в различных форматах (nanoCAD, AutoCAD, MS Word, MS Excel и др.). Пользователь может настроить собственные правила оформления чертежей и спецификаций.

Автоматическая генерация чертежей в Model Studio CS Строительные решенияАвтоматическая генерация чертежей в Model Studio CS Строительные решения

Помимо отчетной документации в виде таблиц, есть возможность сформировать ведомость объемов работ. А если объектам назначены сметные свойства, вы можете с помощью специального инструмента экспортировать данные модели в систему ABC Смета и получить отчетные документы в виде смет.

Формирование ведомости объемов работФормирование ведомости объемов работ

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

Учет рельефа местностиУчет рельефа местности

Заключение

Model Studio CS Строительные решения является гармоничной составляющей комплексной системы проектирования единственной на платформе nanoCAD/AutoCAD, работающей с учетом национальных стандартов и традиций проектирования.

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

Приятным сюрпризом для новых пользователей станет тот факт, что в комплект поставки Model Studio CS Строительные решения входит обширная русскоязычная документация в виде руководства пользователя, учебного пособия, тестовых примеров для получения тех или иных видов спецификаций и чертежей. Кроме того, срок обучения этой системе составляет всего 3-4 дня.

Александр Белкин,
заместитель руководителя
отдела комплексной автоматизации
в строительстве
ГК CSoft
E-mail:
belkin@csoft.ru

Подробнее..

Категории

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

  • Имя: Макс
    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