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

Графика

Перевод Оптимизация веб-графики в 2021 году

20.06.2021 18:15:44 | Автор: admin
Изображения, используемые на веб-страницах, привлекают пользователей, пользователи довольно-таки охотно щёлкают по ним мышью. Изображения делают веб-страницы лучше во всём кроме скорости работы страниц. Изображения это огромные куски байтов, которые обычно являются теми частями сайтов, которые загружаются медленнее всего. В этом материале я собрал всё, что нужно знать в 2021 году об улучшении скорости работы веб-страниц через оптимизацию работы с изображениями.



Изображения обычно имеют большие размеры. Даже очень большие. В большинстве случаев CSS- и JavaScript-ресурсы, необходимые для обеспечения работоспособности страниц это мелочь в сравнении с тем объёмом данных, который нужно передать по сети для загрузки изображений, используемых на страницах. Медленные изображения могут повредить показателям Core Web Vitals сайта, могут оказать воздействие на SEO и потребовать дополнительных затрат на трафик. Изображения это обычно тот самый ресурс сайта, который оказывает решающее воздействие на показатель Largest Contentful Paint (LCP) и на задержки загрузки сайта. Они способны увеличить показатель Cumulative Layout Shift (CLS). Если вы не знакомы с этими показателями производительности сайтов почитайте о них в Definitive Guide to Measuring Web Performance.

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

1. Формат изображений


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

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


Изображения ленивца

Слева мы можем видеть фото нашего товарища-ленивца Сэма. Эта картинка в формате JPG занимает всего лишь 32,7 Кб. А если то же самое изображение преобразовать в формат PNG размер графического файла увеличится более чем вдвое до 90,6 Кб!

Справа находится рисунок со всё тем же Сэмом. Этот рисунок лучше всего хранить в формате PNG. Так он занимает всего 5,5 Кб. А если преобразовать его в JPG, то его размер подскочит до 11,3 Кб.

Обратите внимание: всё то, что фотографиями не является, обычно занимает меньше места, чем фотографии. Не забывайте об этом, проектируя свои веб-страницы.

Существует, конечно, ещё много графических форматов! Если у вас имеется некое векторное изображение (состоящее из всяческих линий и геометрических фигур), то вам лучше всего подойдёт формат SVG. Более новые браузеры поддерживают и более современные графические форматы вроде AVIF и WebP. Их использование для хранения подходящих изображений позволяет добиться ещё более серьёзного уменьшения размеров графических файлов.

2. Отзывчивые изображения и их пиксельные размеры


Не все посетители сайта будут просматривать его в одних и тех же условиях. У кого-то имеется огромный монитор шириной в 1600 пикселей. А кто-то смотрит сайт на планшете с шириной экрана в 900 пикселей, или на телефоне с экраном шириной в 600 пикселей. Если на сайте применяется изображение шириной в 1200 пикселей это будет означать, что при просмотре такого сайта на устройствах с небольшими экранами сетевые и другие ресурсы будут тратиться впустую, так как размер таких изображений при выводе на экран, всё равно, будет уменьшен.


Просмотр сайта на устройствах с разными экранами

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

<img src="picture-1200.jpg"srcset="picture-600.jpg  600w,picture-900.jpg  900w,picture-1200.jpg 1200w"sizes="(max-width: 900px) 100vw, 1200px"alt="my awesome picture" height="900" width="1200" />

В данном случае ширина базового изображения составляет 1200 пикселей. Оно, кроме того, является изображением, записанным в атрибут src тега и используемым по умолчанию. В srcset описаны 3 варианта изображения шириной в 600, 900 и 1200 пикселей. В sizes используются медиа-запросы CSS, позволяющие дать браузеру подсказку, касающуюся видимой области, доступной для вывода изображения. Если ширина окна меньше 900 пикселей место, где будет выведено изображение, займёт всю его ширину 100vw. В противном случае место для вывода изображения никогда не окажется шире 1200 пикселей.

Большинство инструментов для работы с изображениями, вроде Photoshop, Gimp и Paint.NET, умеют экспортировать изображения в различных размерах. Стандартные системные графические программы тоже, в определённых пределах, способны решать подобные задачи. А если надо автоматизировать обработку очень большого количества изображений возможно, есть смысл взглянуть на соответствующие инструменты командной строки вроде ImageMagick.

Скрытие изображений при просмотре сайта на мобильных устройствах


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

<img src="picture-1200.jpg"srcset="picture-600.jpg  600w,picture-900.jpg  900w,picture-1200.jpg 1200w"sizes="(max-width: 600px) 0, 600px"alt="my awesome picture" height="900" width="1200" />

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

3. Качество изображений


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


Исходное PNG-изображение с прозрачными участками имеет размер 57 Кб. Такое же изображение, но сжатое, имеет размер 15 Кб.

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

4. Встраивание изображений в веб-страницы


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

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


Изображение, встроенное в страницу

Может, выглядит это и странновато, но тут перед нами так называемый Data URL. Такие URL пользуются полной поддержкой всех браузеров. В атрибуте src сказано, что соответствующие данные надо воспринимать как PNG-изображение в кодировке base64. После описания изображения идёт набор символов, представляющих содержимое этого изображения. В данном случае это маленькая красная точка.

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

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

А вот удобный веб-инструмент для преобразования изображений в формат base64.

5. Ленивая загрузка изображений


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

Вместо того, чтобы заставлять браузер сразу загружать все изображения, можно посоветовать ему немного полениться. Ленивая загрузка изображений это такой подход к работе с изображениями, когда браузеру предлагают загружать изображения только тогда, когда они могут понадобиться пользователю. Применение ленивой загрузки изображений способно оказать огромное положительное влияние на показатель Largest Contentful Paint (LCP), так как благодаря этому браузер, при загрузке страницы, может уделить основное внимание только самым важным изображениям.

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

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

var lazyEls = [].slice.call(document.querySelectorAll("[data-src]"));var lazyObserver = new IntersectionObserver(function(entries) {entries.forEach(function(entry) {if (entry.isIntersecting) {var el = entry.target;var src = el.getAttribute("data-src");if (src) { el.setAttribute("src", src); }lazyObserver.unobserve(el);}});});lazyEls.forEach(function(el) {lazyObserver.observe(el);});

Тут, для определения того момента, когда надо загружать изображение, используется объект IntersectionObserver. Когда наступает нужный момент содержимое атрибута data-src копируется в атрибут src и изображение загружается. Тот же подход можно применить к атрибуту srcset и воспользоваться им при работе с любым количеством изображений.

Пользуются этим, переименовывая атрибут src в data-src.

<img data-src="picture-1200.jpg"loading="lazy" class="lazy" />

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

Настройка размеров области, которую займёт изображение


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

Избежать сдвига макета страницы можно, указав атрибуты height и width тега <img>.

<img data-src="picture-1200.jpg"loading="lazy" class="lazy"width="1200" height="900" />

Обратите на то, что значения атрибутов height и width это не 1200px и 900px. Это просто 1200 и 900. И работают они немного не так, как можно было бы ожидать. Размер соответствующего изображения не обязательно будет составлять 1200x900 пикселей. Этот размер будет зависеть от CSS и от размеров макета страницы. Но браузер, благодаря этим атрибутам, получит сведения о соотношении сторон изображения. В результате, узнав ширину изображения, браузер сможет правильно настроить его высоту.

То есть, например, если макет страницы имеет в ширину всего 800px, то браузер, не загружая изображение, будет знать о том, что ему надо зарезервировать 600px вертикального пространства для вывода изображения с правильным соотношением сторон.

Итоги


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

Как вы оптимизируете изображения, используемые в ваших веб-проектах?


Подробнее..

Эпоха визуального контента развитие или результат регресса пристрастия пользователей и закономерности восприятия

19.03.2021 20:17:05 | Автор: admin
Мы живём во времена, когда традиционные формы передачи информации уходят в прошлое, а центральную роль в человеческой коммуникации приобретает визуальный контент. Как уже не раз отмечали авторы Хабра, это связано с нейробиологическими закономерностями, в первую очередь, с простотой восприятия и быстрым запоминанием визуальной информации, которая обусловлена количеством нейронов КГМ, участвующих в процессе. Закономерно быстро растет и само количество информации, так, в соответствии с оценками Seagate и IDC, мировой объем информации, записанной в цифровом виде, к 2025 году достигнет 160 зеттабайт, хотя ещё в середине нулевых его оценивали в 0,16 зеттабайт. Немалая часть этого количества приходится на визуальный контент.



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

Влияние коммерческого сектора и проблема скорости покупки


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

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

Отчасти, такое мнение подтверждается маркетинговыми исследованиями поведения пользователей интернет-магазинов. Там значительная часть пользователей ограничивается информацией о 2-3-х наиболее значимых характеристиках, а всё остальное время просматривает фото на страницах товара или видеообзоры, не содержащие подробных сведений о характеристиках.

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

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

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

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

Споры об эффективности в обучении


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

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

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

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

Блоги и социальные медиа


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

Хорошо демонстрирует ситуацию сравнение динамики роста удельного веса в структуре мировых данных классических и новых социальных медиа. Например, делавший ставку на различные виды контента и обилие сервисов Facebook завоевал в конкурентной борьбе свой первый миллиард пользователей за 7 лет существования, тогда как TiK-Tok, платформа, фокусирующая пользователей на примитивном визуальном контенте, набрала тоже количество за 3 года. До этого несколько лет были связаны со стремительным ростом аудитории Instagram, также с преимущественно визуальным контентом.

В качестве заключения


Такие результаты свидетельствуют лишь о том, что визуальный контент является основным способом получения информации, а также, что наиболее предпочтительная форма коммуникации также предполагает визуальную составляющую. Более того, что пользователь скорее предпочтет не статичное, а динамичное изображение (т.е. видео или некий интерактивный формат, типа 360photo, VR-тура или 3D-обзора. Иными словами, мы стали свидетелями и участниками революции медийного потребления. Более того, как мне кажется, даже не одной за последние 10 лет.

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

Опубликован Scheme Request For Implementation 203 A Simple Drawing Language in the Style of SICP

18.09.2020 18:11:01 | Автор: admin
Функциональная геометрияФункциональная геометрия

SICP

Обложка SICPОбложка SICP

Structure and Interpretation of Computer Programs -- это один из самых известных учебников программирования в мире, на основе которого несколько десятков лет преподавался начальный курс программирования в MIT, а во многих унивеситетах, в том числе в Беркли, преподаётся до сих пор.

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

Большая часть тем, входящих в учебник, выполнимы на "стандартной" (в смысле, соответствующего последнему на текущий момент стандарту Revised^7 Report on Algorithmic Language Scheme) Scheme.

Особенные темы

Цифовой сумматор, реализуемый в качестве одного из упражений SICPЦифовой сумматор, реализуемый в качестве одного из упражений SICP

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

Принятая по-умолчанию в учебнике реализация MIT/GNU-Scheme содержит необходимые примитивы, расширяющие базовый язык так, чтобы курс становился проходим.

За многие годы, прошедшие с момента выхода SICP, некоторые реализации Scheme также реализовали многие примитивы, требуемые для прохождения курса.

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

SRFI

Логотип community processЛоготип community process

Scheme Requests for Implementation -- это community process, принятый в семействе языков Scheme. В некоторых аспектах он Java Community Process или Python Enhancement Proposals. Так или иначе, это главный инструмент обсуждения развития языкового семейства, а также главный инструмент обеспечения переносимости кода.

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

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

Функциональная геометрия

Основой главы, посвящённой графической подсистема компьютера, в SICP послужила статься Питера Хендерсона "Функциональная Геометрия". (http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.137.1503, https://eprints.soton.ac.uk/257577/1/funcgeo2.pdf)

Образец рисунка, сделанный методом функциональной геометрииОбразец рисунка, сделанный методом функциональной геометрии

Знакомым с творчеством Морица Эшера это изображение может показаться смутно знакомым.

В основе техник функциольнальной геометрии лежат две идеи.

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

  • Вторая идея в том, что нужен какой-то способ комбинировать painter'ы, получая новые painter'ы.

В КДПВ вынесена иллюстрация этого подхода.

Реализация

В предложенном SRFI не предоставленно исчерпывающей реализации метода функциональной геометрии. Однако, предложен субстрат, достаточный для того, чтобы активный студент мог сам реализовать подмножество функциональной геометрии, предложенной в SICP на любом интерпретаторе, поддерживающем SRFI-203 (результат работы автора сей заметки).

Полный текст предложения находится по ссылке:

https://srfi.schemers.org/srfi-203/srfi-203.html

Абстракт и технические детали можно найти здесь:

https://srfi.schemers.org/srfi-203/

SRFI находился на обсуждении два месяца, и за это время было предложено две реализации, для интерпретатора Chibi, и для интерпретатора Kawa.

Логотип Chibi-SchemeЛоготип Chibi-Scheme

Заключение

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

Подписка

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

  • Telegram :: http://t.me/unobvious

  • GitLab :: http://gitlab.com/lockywolf

  • Twitter :: https://twitter.com/VANikishkin

  • PayPal :: https://paypal.me/independentresearch

Подробнее..

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

29.12.2020 18:11:47 | Автор: admin

Введение

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

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

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

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

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

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

При этом, например, если максимальный размер текстуры, которую можно загрузить в видеопамять составляет 2048х2048, то, даже не самая большая из карт, например, карта ночной Земли, доступная на сайте НАСА [1], при довольно грубом разрешении около 0.75 км/пиксель уже имеет размер 54000х27000 пикселей. Таким образом, чтобы загружать такую карту как текстуру, ее нужно было бы разбить как минимум на 342 фрагмента.

Требование неупакованных данных для отображения

Одна из объективных сложностей, как раз и требующая больших объемов памяти при отображении больших объемов данных, связана с необходимостью перед отображением распаковать имеющийся файл. Та же ночная карта Земли занимает на диске около 393 Мбайт в формате JPEG, а если ее преобразовать в двумерный массив цветов пикселей RGB (т.е. по сути, в формат BMP), она займет в памяти уже 54000х27000х3=4,374 Гбайт. Это даже больше чем 232 и поэтому, например, просто адресовать такой массив в 32-х разрядной среде уже не получится, не говоря о том, что реально в 32-х разрядной среде Windows (где и требуется данное отображение) пользователям доступно только до 3 Гбайт памяти. Кстати, вероятно, поэтому даже такой развитой графический редактор как AcdSee отображает эту ночную карту Земли, но не может редактировать ее (выдает ошибку).

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

Подходящий формат картографических данных

Исторически одним из первых сжатых графических форматов был формат PCX [2], заменяющий несколько подряд идущих одноцветных пикселей на один пиксель и счетчик его повторений. В настоящее время этот формат практически не применяется, дошло до того, что его перестал отображать даже такой стандартный редактор Windows как Paint. В самом деле, фотографические изображения формат PCX сжимает очень плохо, для них разработаны специальные алгоритмы JPEG. А обычные рисунки стало проще хранить в несжатом формате BMP, поскольку рисунок в несколько мегабайт не проблема для современных компьютерных средств и сжимать такие небольшие данные уже не имеет особого смысла.

Но при этом не принимается во внимание, что еще остается целый класс специальных изображений-рисунков карты, где при наличии больших размеров и больших одноцветных площадей старый формат PCX по-прежнему очень удобен и дает большую степень сжатия. Например, уже приводимая выше ночная карта Земли в формате PCX занимает лишь около 292 Мбайт. Такое сильное сжатие даже по сравнению с форматом JPEG получилось, конечно, не только за счет алгоритма сжатия, но и за счет перехода от полного RGB-цвета (16.8 миллионов оттенков) к 256 цветам, позволяющим заменить три байта RGB каждого пикселя на один байт номера цвета в палитре формата PCX. Разумеется, ночная карта Земли - это не рисунок, а интегрированный из тысяч спутниковых фотографий ортофотоплан, т.е. тоже фотография. Но по содержащейся на ней информации, включающий лишь океан, более светлую землю и светящиеся города, она ближе к обычным картам-рисункам, не требующим для сохранения всей информации миллионов оттенков. И при этом наличие лишь 256 цветов все равно позволяет отображать даже некоторые особенности рельефа, например, ледники.

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

Использование двумерных массивов при отображении

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

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

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

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

Для ускорения обработки была использована сетка т.н. реперных точек. Один раз загруженное исходное сжатое изображение в PCX-формате просматривается и в памяти составляется двумерный массив характеристик некоторого подмножества точек, например, каждой 64-ой точки в строке. Точка в этом массиве характеризуется 4-х байтным смещением от начала сжатого изображения и байтом номера, по которому данная точка попадает внутрь группы повторяющихся пикселей. Например, если для реперной точки в массиве указан код 01 25 DE 00 0A, а по этому смещению 00DE2501 в PCX-изображении находится код CF 33, это означает, что данная реперная точка является по счету десятой (0A) в сжатой группе из 15 (CF) пикселей одинакового цвета 33.

Для того чтобы найти цвет пикселя с координатами (X, Y) в сжатом PCX-изображении, нужно сначала делением X на 64 найти ближайшую реперную точку и извлечь 5 байт информации для нее из двумерного массива реперных точек. Затем по найденному смещению встать на начало очередного элемента PCX-формата и начать распаковывать от ближайшей реперной точки до нужной. Распаковка заключается в подсчете числа пикселей. Например, если нужен цвет точки, отстоящей от рассмотренной ближайшей реперной на 3 пикселя, то он также будет равен цвету реперной точки (33), поскольку и реперная и заданная точка попадают в одну группу 15 сжатых пикселей. Для последующих точек, возможно, уже придется перемещаться к следующим элементам PCX-формата, подсчитывая их длины, пока не набежит нужный номер точки, т.е. нужная координата X. Даже в худшем случае так придется распаковать число элементов не превышающее расстояние между двумя соседними реперными точками.

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

Массив реперных точек, разумеется, тоже требует памяти. Так, для уже приводимой в пример ночной карты потребуется дополнительно 54000х27000/64х5=113906250 байт, при условии, что в качестве реперной взята каждая 64-ая точка в строке. Таким образом, общий объем, занимаемый ночной картой Земли в памяти вместе с реперными точками, составит около 406 Мбайт, что почти в 10 раз меньше, чем несжатый массив цветов RGB пикселей того же изображения.

Правда, для упрощенного 256-цветного изображения можно было бы хранить просто двумерный массив байтов номеров цветов, а не самих RGB, что само по себе дало бы трехкратное уменьшение объема по сравнению с обычным форматом BMP, но и это все равно было бы существенно больше по объему памяти (54000х27000=1458000000), чем сжатое PCX-изображение с реперными точками.

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

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

Пример изображения ночной Земли в модели иллюминатора МКС приведен ниже.

Отображение фрагмента ночной карты Земли в модели иллюминатора методом обратной трассировки лучей.Отображение фрагмента ночной карты Земли в модели иллюминатора методом обратной трассировки лучей.

На приведенном рисунке виден ярко освещенный промышленный район севера Италии, Средиземное море и север Африки на горизонте. Для наглядности данной иллюстрации выбран грубый масштаб (около 36 км на см. экрана в центре модели иллюминатора). Отображаемая в реальном времени карта в формате PCX имеет размеры 54000х27000 пикселей и полностью загружена в память.

Заключение

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

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

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

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

Литература

1. http://earthobservatory.nasa.gov/NaturalHazards/view.php?id=79765

2. ZSoft Corporation PCX Technical Reference Manual Revision4 Marietta, GA 1988

Подробнее..

Попиксельная заливка экрана в Wolfenstein 3D (FizzleFade) свежий взгляд

29.03.2021 18:20:06 | Автор: admin

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

Сразу скажу что сама идея псевдослучайной заливки экрана с наименьшими коллизиями (свести к минимуму попадания пикселей в уже нарисованные пиксели) ну как минимум потрясает, а тот факт, что для заливки таким образом области экрана 320х200 (64000 пикселей), если верить автору оригинальной статьи, ушло 131071 циклов, а они таки уйдут при 17-ти битном полиноме, т.е. более чем в два раза больше чем необходимо - удивляет и настораживает...

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

И мне стало интересно: а почему решили закрасить 64Кб видеопамяти при помощи, как по мне, избыточной 17-ти битной РСЛОС? Хм...

Итак...

Максимальный период 17-ти битной РСЛОС: 2171 = 131071 цикл

Максимальный период 16-ти битной РСЛОС: 2161 = 65535 циклов

Очевидно же, что 16-ти битной РСЛОС для закрашивания экрана в 64000 пикселей хватает с лихвой.

Надо полагать, что разработчики исходили из того, что раз уж экран 320х200, следовательно для координат по оси X, которые в пределах 1...320 (maximum 140 hex) нужно брать целых 9 бит, т.к. значение 320, минимум куда можно всунуть, так это в 9 бит, в отличии от координат по оси Y, в которой их всего 1...200 (maximum C8 hex) что свободно помещается в 8 бит, итого 9+8=17 бит РСЛОС...

А что если, к примеру, экран был бы 253х253=64009, или 254х252=64008, или 255х251=64005... (Ну да, пошутил) А хотя, вы наверное уже поняли куда я клоню... Вот возьмём значения 255 (FF hex) и 251 (FB hex), ведь все эти числа свободно помещаются в восьмибитные регистры и в перемножении дают 64005, что даже больше чем 64000 байт...

Что ж, осталось реализовать 16-ти битную РСЛОС:

Для 16-ти битной РСЛОС существует 2048 полиномов с максимальным периодом вида: Х16+...+1, в следующем коде я применил, пусть не самый короткий, но он просто первый из них: Х16532+1

Ниже я привёл код, в регистре CX которого, у нас вырабатывается некая гамма с периодом 65535, но не суть...

В старшем CH и младшем CL регистрах у нас значения которые можно использовать как координаты для нашего нестандартного экрана: CH*250+CL, т.е. Y * 250 + X.

При максимальных значениях Х=255 и Y=255 у нас получится 255*250+255=64005... А поскольку в нашей гамме не вырабатывается значение равное 0, а нам ведь нужно закрасить и нулевой адрес видеопамяти, мы смещаем всю линию назад на один пиксель командой dec di.

В следствии чего:

  • 65535 раз наносим пиксель на псевдослучайный адрес в диапазоне от 0 до 64004;

  • 1530 раз попадаем в уже нанесённые пиксели;

  • 5 раз вылетаем за пределы диапазона буфера экрана. (при желании фиксится)

Следующий код реализует псевдослучайную попиксельную заливку экрана 320х200 16-ти битной РСЛОС с максимальным периодом 65535 циклов:

        mov     ax, 13h       ; хотим видеорежим 320х200х256        int     10h           ; попросим об этом BIOS        push    0A000h        ; начало видеобуфера где-то здесь        pop     es            ; нацелим на него сегментный регистр ES        xor     cx, cx        ; вычистим место для будущей РСЛОСnext:   inc     cx            ; теперь в ней единица        shr     cx, 1         ; продвигаем РСЛОС на 1 бит вправо        jnc     skip          ; проверяем не потерялся ли младший бит        xor     cx, 8016h     ; если бит выпал, выставляем новые с инверсией по маске 1000 0000 0001 0110skip:   movzx   bx, cl        ; эм... пусть это будет координата для оси X        movzx   ax, ch        ; ну а здесь для оси Y        imul    di, ax, 0FAh  ; определим смещение перемножив Y с 251 (да, 5 пикселей вне экрана)        add     di, bx        ; добавим смещение по X        dec     di            ; все пиксели на шаг назад, дабы хоть один попал в X=0, Y=0        mov     al, 64        ; подкрасим пиксели        stosb                 ; нарисуем пиксель        loop    next          ; проверяем, не равен ли текущий РСЛОС исходному?        ret                   ; дело сделано!

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

Ниже демонстрирую более простой, быстрый и понятный код, без X и Y, просто заливаем линейную память нашего видеобуфера по закону, определённому нашей 16-ти битной РСЛОС:

        mov     ax, 13h    ; хотим видеорежим 320х200х256        int     10h        ; BIOS нам поможет в этом        push    0A000h     ; начало видеобуфера        pop     ds         ; подгоняем сегментный регистр DS под видеобуфер        mov     cl, 64     ; выбираем цвет для пикселей        mov     dx, ax     ; запоминаем исходное состояние РСЛОСnext:   shr     ax, 1      ; продвигаем РСЛОС на 1 бит вправо        jnc     noxor      ; проверяем не выпал ли младший бит        xor     ax, 8016h  ; инвертируем РСЛОС по маске: 1000 0000 0001 0110noxor:  cmp     ax, 0FA01h ; проверяем не вышел ли адрес за пределы видеобуфера        jae     skip       ; пропустим всё что не попадает в экранную область        mov     bx, ax     ; копируем AX в BX, т.к. AX не указывает на память        mov     [bx-1], cl ; сместим все адреса влево на 1, чтобы попасть в нулевой адресskip:   cmp     ax, dx     ; сравниваем текущее состояние РСЛОС с исходным        jne     next       ; повторим цикл пока текущий РСЛОС не равен исходному        ret                ; выходим из цикла, т.к. весь экран уже закрашен
Подробнее..

Recovery mode Собираем сервер для графических и CADCAM приложений для удаленной работы по RDP на базе бу CISCO UCS-C220 M3 v2

04.07.2020 20:20:46 | Автор: admin
imageПочти у каждой компании сейчас обязательно есть отдел или группа работающая в CAD/CAM
или тяжелых дизайнерских программах. Эту группу пользователей объединяет серьёзные требования к железу: много памяти 64ГБ и больше, профессиональная видеокарта, быстрый ssd, и чтобы было надежное. Зачастую компании покупают некоторым пользователям таких отделов несколько мощных ПК (или графических станций) и остальным менее мощные в зависимости от потребностей, а также финансовых возможностей компании. Зачастую это стандартный подход для решения таких задач, и он нормально работает. Но во время пандемии и удаленной работы, да и в общем такой подход неоптимален, очень избыточен и крайне неудобен в администрировании, управлении и прочих аспектах. Почему это так, и какое решение идеально удовлетворит потребности в графических станциях многих компаний? Прошу пожаловать под кат, где описано как собрать рабочее и недорогое решение, чтобы убить накормить сразу нескольких зайцев, и какие мелкие нюансы нужно учесть, чтобы успешно внедрить это решение.
В декабре прошлого года одна компания открывала новый офис для небольшого КБ и была поставлена задача организовать им всю компьютерную инфраструктуру учитывая, что ноутбуки для пользователей и пару серверов у компании уже есть. Ноутам уже было пару лет и это были в основном игровые конфигурации с 8-16ГБ ОЗУ, и в основном не справлялись с нагрузкой от CAD/CAM приложений. Пользователи должны быть мобильными, так как часто необходимо работать не из офиса. В офисе к каждому ноуту дополнительно покупается еще по монитору (так работают с графикой). При таких входных данных единственно оптимальное, но рискованное для меня решение внедрить мощный терминальный сервер с мощной профессиональной видеокартой и nvme ssd диском.

Преимущества графического терминального сервера и работы по RDP


  • На отдельных мощных ПК или графических станциях большую часть времени аппаратные ресурсы не используются даже на треть и простаивают без дела и только короткий период времени используются в 35-100% своей мощности. В основном КПД составляет 5-20 процентов.
  • Но часто аппаратная часть далеко не самая затратная составляющая, ведь базовые графические или САД/CAM лицензии на ПО часто стоят от 5000$, а если еще с расширенными опциями то и от 10 000$. Обыкновенно в сеансе RDP эти программы запускаются без проблем, но иногда необходимо дозаказывать RDP опцию, либо поискать по форумам что прописать в конфигах или реестре и как запустить в сеансе RDP такое ПО. Но проверить, что нужное нам ПО работает по RDP нужно в самом начале и сделать это просто: пробуем зайти по RDP если программа запустилась и работают все базовые программные функции, то и проблем с лицензиями скорее всего не будет. А если выдает ошибку, то перед реализацией проекта с графическим терминальным сервером, ищем удовлетворительное для нас решение проблемы.
  • Также большим плюсом есть поддержка одинаковой конфигурации и специфических настроек, компонентов и шаблонов, что часто труднореализуемо для всех ПК пользователей. Управление, администрирование и обновление ПО тоже без сучка и задоринки

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

Собираем сервер на базе бу CISCO UCS-C220 M3 v2


Изначально планировалось купить поновее и мощный сервер с 256ГБ DDR3 ecc памятью и 10GB ethernet, но сказали что нужно немного сэкономить и вписаться в бюджет на терминальный сервер 1600$. Ну ладно клиент всегда жадный прав и подбираем под эту сумму:
бу CISCO UCS-C220 M3 v2 (2 X SIX CORE 2.10GHZ E5-2620 v2) \128ГБ DDR3 ecc 625$
3.5" 3TB sas 7200 з США д 2x65$=130$
SSD M.2 2280 970 PRO, PCI-E 3.0 (x4) 512GB Samsung 200$
Видеокарта QUADRO P2200 5120MB 470$
Адаптер Ewell PCI-E 3.0 to M.2 SSD (EW239) -10$
Итого за сервер = 1435$
Планировалось брать ssd 1TB и 10GB ethernet adapter 40$, но выяснилось, что UPS к их 2 серверам не было, и пришлось немного ужаться и купить UPS PowerWalker VI 2200 RLE -350$.

Почему сервер, а не мощный ПК? Обоснование выбранной конфигурации.


Многие недальновидные админы (много раз уже сталкивался) почему то покупают мощный (зачастую игровой ПК), ставят там 2-4 диска, создают RAID 1, гордо называют это сервером и ставят его в углу офиса. Естественна вся комлектуха сборная солянка сомнительного качества. Поэтому распишу подробно почему подобрана под такой бюджет именно такая конфигурация.
  1. Надежность!!! все серверные комплектующие спроектированы и протестированы для работы более 5-10 лет. А игровые мамки от силы работают 3-5 лет и даже процент поломки во время гарантийного срока у некоторых превышает 5%. А наш сервер от супернадежного бренда CISCO, так что особых проблем не предвидится и их вероятность на порядок ниже стационарного ПК
  2. Важные компоненты типа блока питания дублируются и в идеале можно подать питание с двух разных линий и при выходе из строя одного блока сервер продолжает работать
  3. Память ECC сейчас мало кто помнит, что изначально память ECC была введена для коррекции одного бита от ошибки, возникающей в основном от воздействия космических лучей, а на объёме памяти 128ГБ ошибка может возникать несколько раз в году. На стационарном ПК мы можем наблюдать вылет программы, зависание и прочее, что некритично, но на сервере цена ошибки иногда очень высока (например неправильная запись в БД), в нашем случае при серьезном глюке надо перегрузится и иногда это стоит дневной работы нескольких человек
  4. Масштабируемость часто потребность компании в ресурсах вырастает в несколько раз за пару лет и в сервер легко добавить памяти дисков, поменять процессоры (в нашем случае шестиядерные E5-2620 на десятиядерные Xeon E5 2690 v2) на обычном ПК почти никакой масштабируемости
  5. Серверный формат U1 серверы должны стоять в серверных! и в компактных стойках, а не кочегарить(до 1КВт тепла) и шуметь в углу офиса! Как раз в новом офисе компании отдельно предоставлялось немного (3-6 юнитов) место в серверной и один юнит на наш сервер как раз был нам впритык.
  6. Удаленные: управление и консоль без этого нормальное обслуживание сервера для удаленной! работы крайне затруднительно!
  7. 128Гб ОЗУ в ТЗ было сказано 8-10 пользователей, но в реальности будет 5-6 одновременных сессий поэтому учитывая типичный в той компании максимальный расход объёма памяти 2 пользователя по 30-40ГБ=70ГБ и 4 юзера по 3-15ГБ=36ГБ, + до 10ГБ на операционку в сумме 116ГБ и 10% у нас в запасе(это все в редких случаях максимального использования. Но если будет не хватать то в любой момент можно добавить до 256ГБ
  8. Видеокарта QUADRO P2200 5120MB в среднем на пользователя в той компании в
    удаленном сеансе расход видеопамяти был от 0,3ГБ до 1,5ГБ, так что 5ГБ будет достаточно. Исходные данные были взяти с аналогичного, но менее мощного решения, на базе i5/64ГБ/Quadro P620 2ГБ, которого хватало на 3-4 пользователя
  9. SSD M.2 2280 970 PRO, PCI-E 3.0 (x4) 512GB Samsung для одновременной работы
    8-10 пользователей, необходимо именно скорости NVMe и надежность ssd Samsung. По функционалу этот диск будет использоваться для ОС и приложений
  10. 2х3TB sas объединяем в райд 1 используем для объёмных или редкоиспользуемых локальных данных пользователей, а также для бекапа системы и критическо важных локальных данных с диска nvme

Конфигурация одобрена и куплена, и вот скоро настанет момент истины!

Сборка, настройка, установка и решение проблем.


С самого начала у меня не было уверенности, что это 100% рабочее решение, так как на любом этапе, начиная со сборки заканчивая установкой, запуском и корректной работой приложений можно было застрять без возможности продолжить, поэтому про сервер я договорился, что его в течении пару дней можно будет вернуть, а другие компоненты можно использовать в альтернативном решении.
1 надуманная проблема видеокарта профессиональная, полноформатная! +пару мм, а что если не влезит? 75вт а что если pci разьем не потянет? И как нормальный теплоотвод этих 75вт сделать? Но влезла, запустилась, теплоотвод нормальный(особенно если кулеры сервера включить на обороты выше среднего. Правда когда ставил, для уверенности что бы ничего не замыкало что-то в сервере на 1мм отогнул (уже не помню что), а для лучшего теплоотвода с крышки сервера потом после окончательной настройки отодрал пленку инструкции, которая была на всю крышку и которая могла ухудшать теплоотвод через крышку.
2-е ипытание NVMe диск через переходник мог не увидится либо система туда не поставится, а если поставится, то не загрузится. Как ни странно Windows поставилась на NVMe диск, но загрузится с него не смогла, что логично так как биос(даже обновленный) ни в какую распознавать для загрузки NVMe не хотел. Не хотел костылить, но пришлось тут пришел на помощь наш любимый хабр и пост про загрузку с nvme диска на legacy системах скачал утилитку Boot Disk Utility (BDUtility.exe), создал флешку с CloverBootManager по инструкции из поста, установил флешку в биосе первой на загрузку и вот мы уже грузим загрузчик с флешки, Clover успешно увидел наш NVMe диск и через пару секунд автоматично с него загрузился! Можно было поиграться с установкой clover на наш raid 3TB диск, но было уже субота вечер, а работы оставалось еще на день, ведь до понедельника нужно было или отдавать сервер или оставлять. Загрузочную флешку оставил внутри сервера, там как раз был лишний usb.
3-я почти угроза провала. Поставил Windows 2019 standart +RD сервисы, установил главное приложение, ради которого всё затевалось, и всё чудесно работает и буквально летает. Замечательно! Еду домой и подключаюсь по RDP, приложение запускается, но ощущается серьёзный лаг, смотрю а в проге сообщение включен soft режим. Чего?! Ищу более свежие и суперпрофессиональные дрова на видеокарту, ставлю -результата ноль, более древние дрова под p1000 тоже ничего. А в это время внутренний голос всё издевается а я тебе говорил не экспериментируй со свежачком возьми p1000. А время уже давно ночь во дворе, с тяжелым сердцем ложусь спать. Воскресенье, еду в офис ставлю в сервер quadro P620 и тоже по RDP не работает MS в чем дело? Ищу по форумам 2019 server и RDP ответ нашел почти сразу. Оказывается, что так как у большинства сейчас мониторы с большим разрешением, а в большинстве серверов встроенный графический адаптер эти разрешения не поддерживает то аппаратное ускорение по умолчанию отключено через групповые политики. Цитирую инструкцию по включению:
  • Open the Edit Group Policy tool from Control Panel or use the Windows Search dialog (Windows Key + R, then type in gpedit.msc)
  • Browse to: Local Computer Policy\Computer Configuration\Administrative Templates\Windows Components\Remote Desktop Services\Remote Desktop Session Host\Remote Session Environment
  • Then enable Use the hardware default graphics adapter for all Remote Desktop Services sessions

Перезагружаемся все прекрасно по RDP работает. Меняем видеокарту на P2200 опять работает! Теперь когда мы уверены, что решение полностью рабочее приводим все настройки сервера к идеалу, вводим в домен, настраиваем доступ пользователей и прочее, ставим сервер в серверную. Тестируем всей командой пару дней всё идеально работает, на все задачи ресурсов сервера хватает с избытком, минимальный лаг возникающий в результате работы по RDP всем пользователям незаметен. Замечательно задача выполнена на 100%.

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


Так как на любом этапе внедрения графического сервера в организацию могут возникнуть подводные камни, которые могут создать ситуацию аналогичную как на картинке со сбежавшими рыбкамиimage
то на этапе планирования необходимо сделать несколько простых шагов:
  1. Целевая аудитория и задачи пользователи которые интенсивно работают с графикой и им нужно аппаратное ускорение видеокарты. Успех нашего решения основан на том, что потребности в мощности пользователей графических и CAD/CAM программ был удовлетворен с избытком более 10 лет назад, а на данный момент мы имеем запас мощности превышающий потребности в 10 и более раз. Например мощности GPU Quadro P2200 хватает с избытком на 10 пользователей, и даже при недостатке памяти видеопамяти видеокарта добирает с ОЗУ, и для обычного 3d разработчика такое небольшое падение в скорости памяти проходит незаметно. Но если в задачах пользователей есть интенсивные вычислительные задачи (рендеринг, расчеты и прочее), которые часто задействуют 100% ресурсов то наше решение не подходит, так как другие пользователи в эти периоды не смогут нормально работать. Поэтому тщательно анализируем задачи пользователей и текущую загрузку ресурсов (хотя бы приблизительно).
  2. Исходя из количества пользователей подбираем подходящий по ресурсам сервер, видеокарту и диски:
    процессоры по формуле 1 ядро на пользователя + 2,3 на ОС, все равно каждый в один момент времени не использует одного или максимум двух(при редкой загрузке модели) ядер;
    видеокарта -смотрим средний объём потребления видеопамяти и GPU на пользователя в сеансе RDP и подбираем профессиональную! видеокарту;
    аналогично поступаем с ОЗУ и дисковой подсистемой(сейчас можно даже RAID nvme недорого подобрать).
  3. Тщательно смотрим по документации к серверу (благо все брендовые сервера имеет полную документацию) соответствие по разъёмам, скоростям, питанию и поддерживаемым технологиям, а также физическим размерам, и нормам теплоотвода устанавливаемых дополнительных компонентов.
  4. Проверяем нормальную работу нашего ПО по RDP на отсутствие лицензионных ограничений и наличие необходимых лицензий. Решаем этот вопрос до первых шагов по реализации внедрения.
  5. Продумываем где будет установлен графический сервер, не забываем про UPS и наличие там высокоскоростных ethernet портов и интернет (если нужно), а также соответствие климатических требованиям сервера.
  6. Термин внедрения увеличиваем минимум до 2,5-3 недель, ведь многие даже мелкие необходимые компоненты могут ехать до двух недель, а ведь сборка и настройка проходит несколько дней только обычная загрузка сервера до ОС может быть более 5 минут.
  7. Обговариваем с руководством и поставщиками, что если вдруг на каком либо этапе проект не пойдет или пойдет не так, то можно сделать возврат или замену.
  8. Операционную систему (желательно Windows server 2019 там качественный RDP) устанавливаем вначале в Trial режиме, но ни в коем случае не evaluate (нужно потом переустанавливать с нуля). И только после успешного запуска решаем вопросы с лицензиями и активируем ОС.
  9. Также до внедрения подбираем инициативную группу для проверки работы и объясняем будущим пользователям преимущества работы с графическим сервером. Если это делать после, то увеличиваем риск рекламаций, саботажа и неаргументированных негативных отзывов.

По ощущениям работа по RDP не отличается от работы в локальном сеансе. Часто даже забываешь, что работаешь где-то по RDP ведь даже видео и иногда видеосвязь в сеансе RDP работают без ощутимых задержек, ведь сейчас у большинства подключен высокоскоростной интернет. По скорости и функционалу RDP компания Microsoft сейчас продолжает приятно удивлять и аппаратное ускорение 3D и мультимониторы все что необходимо для удаленной работы пользователям графических, 3D и CAD/CAM программ!
Так что во многих случаях установка графического сервера согласно проведенного внедрения является предпочтительней и мобильней 10 графических станций или ПК.
P.S. Как просто и безопасно подключиться через интернет по RDP, а также оптимальные настройки для RDP клиентов вы можете подсмотреть в статье "Удаленная работа в офисе. RDP, Port Knocking, Mikrotik: просто и безопасно"
Подробнее..

Из песочницы Основы компьютерной геометрии. Написание простого 3D-рендера

21.09.2020 22:05:33 | Автор: admin
Привет меня зовут Давид, а вот я собственной персоной отрендеренный своим самописным рендером:

image

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

Идея


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

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

Выбор языка изначально падал на c++ или rust, но я остановился на c# из-за простоты написания кода и широких возможностей для оптимизации. Итоговым продуктом данной статьи будет рендер, способный выдавать подобные картинки:

image

image

Все модели, использованные мной здесь, распространяются в открытом доступе, не занимайтесь пиратством и уважайте труд художников!

Математика


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

Повороты вектора. Матрица поворота


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

  • Поворот относительно начала координат
  • Поворот относительно некоторой точки

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

Давайте выведем формулы для вращения вектора в двумерном пространстве. Обозначим координаты исходного вектора {x, y}. Координаты нового вектора, повернутого на угол f, обозначим как {x y}.

image

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

image

Заметьте, что мы можем использовать формулы косинуса и синуса суммы для того, чтобы разложить значения x' и y'. Для тех, кто подзабыл я напомню эти формулы:

image

Разложив координаты повернутого вектора через них получим:

image

Здесь нетрудно заметить, что множители l * cos a и l * sin a это координаты исходного вектора: x = l * cos a, y = l * sin a. Заменим их на x и y:

image

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

image

Умножьте и проверьте что результат эквивалентен тому, что мы вывели.

Поворот в трехмерном пространстве


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

XY вращение.

При таком повороте мы вращаем вектор относительно оси OZ координатной системы. Представьте, что вектора это вертолётные лопасти, а ось OZ это мачта на которой они держаться. При XY вращении вектора будут поворачиваться относительно оси OZ, как лопасти вертолета относительно мачты.

image

Заметьте, что при таком вращении z координаты векторов не меняются, а меняются x и x координаты поэтому это и называется XY вращением.

image

Нетрудно вывести и формулы для такого вращения: z координата остается прежней, а x и y изменяются по тем же принципам, что и в 2д вращении.

image

То же в виде матрицы:

image

Для XZ и YZ вращений все аналогично:

image
image

Проекция


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

В том понимании которое мы используем здесь проекция на вектор это тоже вектор. Его координаты точка пересечения перпендикуляра опущенного из вектора a на b с вектором b.

image

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

image

Направление вектора проекции по определению совпадает с вектором b, значит проекция определяется формулой:

image

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

Теперь представим все через скалярное произведение:

image

Получаем удобную формулу для нахождения проекции:

image

Системы координат. Базисы


Многие привыкли работать в стандартной системе координат XYZ, в ней любые 2 оси будут перпендикулярны друг другу, а координатные оси можно представить в виде единичных векторов:

image

На деле же систем координат бесконечное множество, каждая из них является базисом. Базис n-мерного пространства является набором векторов {v1, v2 vn} через которые представляются все вектора этого пространства. При этом ни один вектор из базиса нельзя представить через другие его вектора. По сути каждый базис является отдельной системой координат, в которой вектора будут иметь свои, уникальные координаты.

Давайте разберем, что из себя представляет базис для двумерного пространства. Возьмём для примера всем знакомую декартову систему координат из векторов X {1, 0}, Y {0, 1}, которая является одним из базисов для двумерного пространства:

image


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

image


Теперь возьмём другой базис:

image


Через его вектора также можно представить любой 2д вектор:

image


А вот такой набор векторов не является базисом двухмерного пространства:

image


В нем два вектора {1,1} и {2,2} лежат на одной прямой. Какие бы их комбинации вы не брали получать будете только вектора, лежащие на общей прямой y = x. Для наших целей такие дефектные не пригодятся, однако, понимать разницу, я считаю, стоит. По определению все базисы объединяет одно свойство ни один из векторов базиса нельзя представить в виде суммы других векторов базиса с коэффициентами или же ни один вектор базиса не является линейной комбинацией других. Вот пример набора из 3-х векторов который так же не является базисом:

image


Через него можно выразить любой вектор двумерной плоскости, однако вектор {1, 1} в нем является лишним так как сам может быть выражен через вектора {1, 0} и {0,1} как {1,0} + {0,1}.

Вообще любой базис n-мерного пространства будет содержать ровно n векторов, для 2д это n соответственно равно 2.

Перейдем к 3д. Трехмерный базис будет содержать в себе 3 вектора:

image


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

  • 1)2 вектора не лежат на одной прямой
  • 2)3-й не лежит на плоскости образованной двумя другими.


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

image


удовлетворяет этим критериям.

Переход в другой базис


До сих пор мы записывали разложение вектора как сумму векторов базиса с коэффициентами:

image

Снова рассмотрим стандартный базис вектор {1, 3, 6} в нем можно записать так:

image

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

image


Этот базис получен из стандартного применением к нему XY вращения на 45 градусов. Возьмем вектор a в стандартной системе имеющий координаты {0 ,1, 1}

image


Через вектора нового базиса его можно разложить таким образом:

image


Если вы посчитаете эту сумму, то получите {0, 1, 1} вектор а в стандартном базисе. Исходя из этого выражения в новом базисе вектор а имеет координаты {0.7, 0.7, 1} коэффициенты разложения. Это будет виднее если взглянуть с другого ракурса:

image


Но как находить эти коэффициенты? Вообще универсальный метод это решение довольно сложной системы линейных уравнений. Однако как я сказал ранее использовать мы будем только ортогональные и нормированные базисы, а для них есть весьма читерский способ. Заключается он в нахождении проекций на вектора базиса. Давайте с его помощью найдем разложение вектора a в базисе X{0.7, 0.7, 0} Y{-0.7, 0.7, 0} Z{0, 0, 1}

image


Для начала найдем коэффициент для y. Первым шагом мы находим проекцию вектора a на вектор y (как это делать я разбирал выше):

image


Второй шаг: делим длину найденной проекции на длину вектора y, тем самым мы узнаем сколько векторов y помещается в векторе проекции это число и будет коэффициентом для y, а также y координатой вектора a в новом базисе! Для x и z повторим аналогичные операции:

image


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

image


Ну а так как мы используем только нормированные базисы и длины их векторов равны 1 отпадет необходимость делить на длину вектора в формуле перехода:

image


Раскроем x-координату через формулу проекции:

image


Заметьте, что знаменатель (x', x') и вектор x' в случае нормированного базиса так же равен 1 и их можно отбросить. Получим:

image


Мы видим, что координата x базисе выражается как скалярное произведение (a, x), координата y соответственно как (a, y), координата z (a, z). Теперь можно составить матрицу перехода к новым координатам:

image


Системы координат со смещенным центром


У всех систем координат которые мы рассмотрели выше началом координат была точка {0,0,0}. Помимо этого существуют еще системы со смещенной точкой начала координат:

image


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

Пишем геометрический движок. Создание проволочного рендера.



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

image


Полигональная графика


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

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

    abstract class Primitive    {        public Vector3[] Vertices { get; protected set; }        public int[] Indexes { get; protected set; }    }

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

   public class Cube : Primitive      {        public Cube(Vector3 center, float sideLen)        {            var d = sideLen / 2;            Vertices = new Vector3[]                {                    new Vector3(center.X - d , center.Y - d, center.Z - d) ,                    new Vector3(center.X - d , center.Y - d, center.Z) ,                    new Vector3(center.X - d , center.Y , center.Z - d) ,                    new Vector3(center.X - d , center.Y , center.Z) ,                    new Vector3(center.X + d , center.Y - d, center.Z - d) ,                    new Vector3(center.X + d , center.Y - d, center.Z) ,                    new Vector3(center.X + d , center.Y + d, center.Z - d) ,                    new Vector3(center.X + d , center.Y + d, center.Z + d) ,                };            Indexes = new int[]                {                    1,2,4 ,                    1,3,4 ,                    1,2,6 ,                    1,5,6 ,                    5,6,8 ,                    5,7,8 ,                    8,4,3 ,                    8,7,3 ,                    4,2,8 ,                    2,8,6 ,                    3,1,7 ,                    1,7,5                };        }    }int Main(){        var cube = new Cube(new Vector3(0, 0, 0), 2);}

image

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


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

image

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

При работе с объектом мы будем производить операции с его вершинами в локальной системе координат, при рендеринге будем предварительно переводить все объекты сцены в единую систему координат глобальную. Добавим системы координат в код. Для этого создадим объект класса Pivot (стержень, точка опоры) который будет представлять локальный базис объекта и его центральную точку. Перевод точки в систему координат представленную Pivot будет производиться в 2 шага:

  • 1)Представление точки относительно центра новых координат
  • 2)Разложение по векторам нового базиса

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

  • 1)Разложение по векторам глобального базиса
  • 2)Представление относительно глобального центра

Напишем класс для представления систем координат:

    public class Pivot    {        //точка центра        public Vector3 Center { get; private set; }        //вектора локального базиса - локальные координатные оси        public Vector3 XAxis { get; private set; }        public Vector3 YAxis { get; private set; }        public Vector3 ZAxis { get; private set; }        //Матрица перевода в локальные координаты        public Matrix3x3 LocalCoordsMatrix => new Matrix3x3            (                XAxis.X, YAxis.X, ZAxis.X,                XAxis.Y, YAxis.Y, ZAxis.Y,                XAxis.Z, YAxis.Z, ZAxis.Z            );        //Матрица перевода в глобальные координаты        public Matrix3x3 GlobalCoordsMatrix => new Matrix3x3            (                XAxis.X , XAxis.Y , XAxis.Z,                YAxis.X , YAxis.Y , YAxis.Z,                ZAxis.X , ZAxis.Y , ZAxis.Z            );        public Vector3 ToLocalCoords(Vector3 global)        {            //Находим позицию вектора относительно точки центра и раскладываем в локальном базисе            return LocalCoordsMatrix * (global - Center);        }        public Vector3 ToGlobalCoords(Vector3 local)        {            //В точности да наоборот - раскладываем локальный вектор в глобальном базисе и находим позицию относительно глобального центра            return (GlobalCoordsMatrix * local)  + Center;        }        public void Move(Vector3 v)        {            Center += v;        }        public void Rotate(float angle, Axis axis)        {            XAxis = XAxis.Rotate(angle, axis);            YAxis = YAxis.Rotate(angle, axis);            ZAxis = ZAxis.Rotate(angle, axis);        }    }

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

    public abstract class Primitive    {        //Локальный базис объекта        public Pivot Pivot { get; protected set; }        //Локальные вершины        public Vector3[] LocalVertices { get; protected set; }        //Глобальные вершины        public Vector3[] GlobalVertices { get; protected set; }        //Индексы вершин        public int[] Indexes { get; protected set; }        public void Move(Vector3 v)        {            Pivot.Move(v);            for (int i = 0; i < LocalVertices.Length; i++)                GlobalVertices[i] += v;        }        public void Rotate(float angle, Axis axis)        {            Pivot.Rotate(angle , axis);            for (int i = 0; i < LocalVertices.Length; i++)                GlobalVertices[i] = Pivot.ToGlobalCoords(LocalVertices[i]);        }        public void Scale(float k)        {            for (int i = 0; i < LocalVertices.Length; i++)                LocalVertices[i] *= k;            for (int i = 0; i < LocalVertices.Length; i++)                GlobalVertices[i] = Pivot.ToGlobalCoords(LocalVertices[i]);        }    }

image

Вращение и перемещение объекта с помощью локальных координат

Рисование полигонов. Камера


Основным объектом сцены будет камера с помощью нее объекты будут рисоваться на экране. Камера, как и все объекты сцены, будет иметь локальные координаты в виде объекта класса Pivot через него мы будем двигать и вращать камеру:

image

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

image

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

image


Наоборот же, зная высоту дерева он может найти высоту точки на проекционной плоскости:

image


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

image


Такая плоскость будет представлять экран. Координату проекции вершины объекта на экран будем находить в 2 этапа:

  • 1)Переводим вершину в локальные координаты камеры
  • 2)Находим проекцию точки через отношение подобных треугольников

image


Проекция будет 2-мерным вектором, ее координаты x' и y' и будут определять позицию точки на экране компьютера.

Класс камеры 1
public class Camera{    //локальные координаты камеры    public Pivot Pivot { get; private set; }    //расстояние до проекционной плоскости    public float ScreenDist { get; private set; }    public Camera(Vector3 center, float screenDist)    {        Pivot = new Pivot(center);        ScreenDist = screenDist;    }    public void Move(Vector3 v)    {        Pivot.Move(v);    }    public void Rotate(float angle, Axis axis)    {        Pivot.Rotate(angle, axis);    }    public Vector2 ScreenProection(Vector3 v)    {        var local = Pivot.ToLocalCoords(v);        //через подобные треугольники находим проекцию        var delta = ScreenDist / local.Z;        var proection = new Vector2(local.X, local.Y) * delta;        return proection;    }}


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

Отсекаем невидимые полигоны


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

image


Для отсечения невидимых вершин в open gl используются метод усекающей пирамиды. Заключается он в задании двух плоскостей ближней(near plane) и дальней(far plane). Все, что лежит между этими двумя плоскостями будет подлежать дальнейшей обработке. Я же использую упрощенный вариант с одной усекающей плоскостью z'. Все вершины, лежащие позади нее будут невидимыми.

Добавим в камеру два новых поля ширину и высоту экрана.
Теперь каждую спроецированную точку будем проверять на попадание в область экрана. Так же отсечем точки позади камеры. Если точка лежит сзади или ее проекция не попадает на экран то метод вернет точку {float.NaN, float.NaN}.

Код камеры 2
public Vector2 ScreenProection(Vector3 v){    var local = Pivot.ToLocalCoords(v);    //игнорируем точки сзади камеры    if (local.Z < ScreenDist)    {        return new Vector2(float.NaN, float.NaN);    }    //через подобные треугольники находим проекцию    var delta = ScreenDist / local.Z;    var proection = new Vector2(local.X, local.Y) * delta;    //если точка принадлежит экранной области - вернем ее    if (proection.X >= 0 && proection.X < ScreenWidth && proection.Y >= 0 && proection.Y < ScreenHeight)    {        return proection;    }    return new Vector2(float.NaN, float.NaN);}


Переводим в экранные координаты


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

image


Код камеры 3
public Vector2 ScreenProection(Vector3 v){    var local = Pivot.ToLocalCoords(v);    //игнорируем точки сзади камеры    if (local.Z < ScreenDist)    {        return new Vector2(float.NaN, float.NaN);    }    //через подобные треугольники находим проекцию    var delta = ScreenDist / local.Z;    var proection = new Vector2(local.X, local.Y) * delta;    //этот код нужен для перевода проекции в экранные координаты    var screen = proection + new Vector2(ScreenWidth / 2, -ScreenHeight / 2);    var screenCoords = new Vector2(screen.X, -screen.Y);    //если точка принадлежит экранной области - вернем ее    if (screenCoords.X >= 0 && screenCoords.X < ScreenWidth && screenCoords.Y >= 0 && screenCoords.Y < ScreenHeight)    {        return screenCoords;    }    return new Vector2(float.NaN, float.NaN);}


Корректируем размер спроецированного изображения


Если вы используете предыдущий код для того, чтобы нарисовать объект то получите что-то вроде этого:

image


Почему то все объекты рисуются очень маленькими. Для того, чтобы понять причину вспомните как мы вычисляли проекцию умножали x и y координаты на дельту отношения z' / z. Это значит, что размер объекта на экране зависит от расстояния до проекционной плоскости z'. А ведь z' мы можем задать сколь угодно маленьким значением. Значит нам нужно корректировать размер проекции в зависимости от текущего значения z'. Для этого добавим в камеру еще одно поле угол ее обзора.

image


Он нам нужен для сопоставления углового размера экрана с его шириной. Угол будет сопоставлен с шириной экрана таким образом: максимальный угол в пределах которого смотрит камера это левый или правый край экрана. Тогда максимальный угол от оси z камеры составляет o / 2. Проекция, которая попала на правый край экрана должна иметь координату x = width / 2, а на левый: x = -width / 2. Зная это выведем формулу для нахождения коэффициента растяжения проекции:

image


Код камеры 4
public float ObserveRange { get; private set; }public float Scale => ScreenWidth / (float)(2 * ScreenDist * Math.Tan(ObserveRange / 2));public Vector2 ScreenProection(Vector3 v){    var local = Pivot.ToLocalCoords(v);    //игнорируем точки сзади камеры    if (local.Z < ScreenDist)    {        return new Vector2(float.NaN, float.NaN);    }    //через подобные треугольники находим проекцию и умножаем ее на коэффициент растяжения    var delta = ScreenDist / local.Z * Scale;    var proection = new Vector2(local.X, local.Y) * delta;    //этот код нужен для перевода проекции в экранные координаты    var screen = proection + new Vector2(ScreenWidth / 2, -ScreenHeight / 2);    var screenCoords = new Vector2(screen.X, -screen.Y);    //если точка принадлежит экранной области - вернем ее    if (screenCoords.X >= 0 && screenCoords.X < ScreenWidth && screenCoords.Y >= 0 && screenCoords.Y < ScreenHeight)    {        return screenCoords;    }    return new Vector2(float.NaN, float.NaN);}


Вот такой простой код отрисовки я использовал для теста:

Код рисования объектов
public DrawObject(Primitive primitive , Camera camera){    for (int i = 0; i < primitive.Indexes.Length; i+=3)    {        var color = randomColor();        // индексы вершин полигона        var i1 = primitive.Indexes[i];        var i2 = primitive.Indexes[i+ 1];        var i3 = primitive.Indexes[i+ 2];        // вершины полигона        var v1 = primitive.GlobalVertices[i1];        var v2 = primitive.GlobalVertices[i2];        var v3 = primitive.GlobalVertices[i3];        // рисуем полигон        DrawPolygon(v1,v2,v3 , camera , color);    }}public void DrawPolygon(Vector3 v1, Vector3 v2, Vector3 v3, Camera camera , color){    //проекции вершин    var p1 = camera.ScreenProection(v1);    var p2 = camera.ScreenProection(v2);    var p3 = camera.ScreenProection(v3);    //рисуем полигон    DrawLine(p1, p2 , color);    DrawLine(p2, p3 , color);    DrawLine(p3, p2 , color);}


Давайте проверим рендер на сцене и кубов:

image


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

Результат работы рендера

image

image


Растеризация полигонов. Наводим красоту.



В прошлом разделе мы написали проволочный рендер. Теперь мы займемся его модернизацией реализуем растеризацию полигонов.

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

image


Современное искусство, полигоны сзади нарисовались поверх передних, одним словом каша. К тому же как таким образом текстурировать объекты? Да, никак. Значит нам нужно написать свой имба-растерайзер, который будет уметь в отсечение невидимых точек, текстуры и даже в шейдеры! Но для того чтобы это сделать стоит понять как вообще красить треугольники.

Алгоритм Брезенхема для рисования линии.


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

Имеется отрезок соединяющий точки {x1, y1} и {x2, y2}. Чтобы нарисовать отрезок между ними нужно закрасить все пиксели которые попадают на него. Для двух точек отрезка можно найти x-координаты пикселей в которых они лежат: нужно лишь взять целые части от координат x1 и x2. Чтобы закрасить пиксели на отрезке запускаем цикл от x1 до x2 и на каждой итерации вычисляем y координату пикселя который попадает на прямую. Вот код:

void Brezenkhem(Vector2 p1 , Vector2 p2){    int x1 = Floor(p1.X);    int x2 = Floor(p2.X);    if (x1 > x2) {Swap(x1, x2); Swap(p1 , p2);}    float d = (p2.Y - p1.Y) / (x2 - x1);    float y = p1.Y;    for (int i = x1; i <= x2; i++)    {        int pixelY = Floor(y);        FillPixel(i , pixelY);        y += d;    }}

image

Картинка из вики

Растеризация треугольника. Алгоритм заливки


Линии рисовать мы умеем, а вот с треугольниками будет чуть посложнее(не намного)! Задача рисования треугольника сводится к нескольким задачам рисования линий. Для начала разобьем треугольник на две части предварительно отсортировав точки в порядке возрастания x:

image


Заметьте теперь у нас есть две части в которых явно выражены нижняя и верхняя границы. все что осталось это залить все пиксели находящиеся между ними! Сделать это можно в 2 цикла: от x1 до x2 и от x3 до x2.

void Triangle(Vector2 v1 , Vector2 v2 , Vector2 v3){    //хардкодим BubbleSort для упорядочивания по x    if (v1.X > v2.X) { Swap(v1, v2); }    if (v2.X > v3.X) { Swap(v2, v3); }    if (v1.X > v2.X) { Swap(v1, v2); }    //узнаем на сколько увеличивается y границ при увеличении x    //избегаем деления на 0: если x1 == x2 значит эта часть треугольника - линия    var steps12 = max(v2.X - v1.X , 1);    var steps13 = max(v3.X - v1.X , 1);    var upDelta = (v2.Y - v1.Y) / steps12;    var downDelta = (v3.Y - v1.Y) / steps13;    //верхняя граница должна быть выше нижней    if (upDelta < downDelta) Swap(upDelta , downDelta);    //изначально у координаты границ равны y1    var up = v1.Y;    var down = v1.Y;    for (int i = (int)v1.X; i <= (int)v2.X; i++)    {        for (int g = (int)down; g <= (int)up; g++)        {            FillPixel(i , g);        }        up += upDelta;        down += downDelta;    }    //все то же самое для другой части треугольника    var steps32 = max(v2.X - v3.X , 1);    var steps31 = max(v1.X - v3.X , 1);    upDelta = (v2.Y - v3.Y) / steps32;    downDelta = (v1.Y - v3.Y) / steps31;    if (upDelta < downDelta) Swap(upDelta, downDelta);    up = v3.Y;    down = v3.Y;    for (int i = (int)v3.X; i >=(int)v2.X; i--)    {        for (int g = (int)down; g <= (int)up; g++)        {            FillPixel(i, g);        }        up += upDelta;        down += downDelta;    }}

Несомненно этот код можно отрефакторить и не дублировать цикл:

void Triangle(Vector2 v1 , Vector2 v2 , Vector2 v3){    if (v1.X > v2.X) { Swap(v1, v2); }    if (v2.X > v3.X) { Swap(v2, v3); }    if (v1.X > v2.X) { Swap(v1, v2); }    var steps12 = max(v2.X - v1.X , 1);    var steps13 = max(v3.X - v1.X , 1);    var steps32 = max(v2.X - v3.X , 1);    var steps31 = max(v1.X - v3.X , 1);    var upDelta = (v2.Y - v1.Y) / steps12;    var downDelta = (v3.Y - v1.Y) / steps13;    if (upDelta < downDelta) Swap(upDelta , downDelta);    TrianglePart(v1.X , v2.X , v1.Y , upDelta , downDelta);    upDelta = (v2.Y - v3.Y) / steps32;    downDelta = (v1.Y - v3.Y) / steps31;    if (upDelta < downDelta) Swap(upDelta, downDelta);    TrianglePart(v3.X, v2.X, v3.Y, upDelta, downDelta);}void TrianglePart(float x1 , float x2 , float y1  , float upDelta , float downDelta){    float up = y1, down = y1;    for (int i = (int)x1; i <= (int)x2; i++)    {        for (int g = (int)down; g <= (int)up; g++)        {            FillPixel(i , g);        }        up += upDelta; down += downDelta;    }}

Отсечение невидимых точек.


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

image


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

image


Теперь возникает вопрос как находить z-координаты точек на исходном полигоне? Это можно сделать несколькими способами. Например можно пускать луч из начала координат камеры, проходящий через точку на проекционной плоскости {x, y, z'}, и находить его пересечение с полигоном. Но искать пересечения крайне затратная операция, поэтому будем использовать другой способ. Для рисования треугольника мы интерполировали координаты его проекций, теперь, помимо этого, мы будем интерполировать также и координаты исходного полигона. Для отсечения невидимых точек будем использовать в методе растеризации состояние zbuffer-а для текущего фрейма.

Мой zbuffer будет иметь вид Vector3[] он будет содержать не только z координаты, но и интерполированные значения точек полигона(фрагменты) для каждого пикселя экрана. Это сделано в целях экономии памяти так как в дальнейшем нам все равно пригодятся эти значения для написания шейдеров! А пока что имеем следующий код для определения видимых вершин(фрагментов):

Код
public void ComputePoly(Vector3 v1, Vector3 v2, Vector3 v3 , Vector3[] zbuffer){    //находим проекцию полигона    var v1p = Camera.ScreenProection(v1);    var v2p = Camera.ScreenProection(v2);    var v3p = Camera.ScreenProection(v3);    //упорядочиваем точки по x - координате    //Заметьте, также меняем исходные точки - они должны соответствовать проекциям    if (v1p.X > v2p.X) { Swap(v1p, v2p); Swap(v1p, v2p); }    if (v2p.X > v3p.X) { Swap(v2p, v3p); Swap(v2p, v3p); }    if (v1p.X > v2p.X) { Swap(v1p, v2p); Swap(v1p, v2p); }    //считаем количество шагов для построения линии алгоритмом Брезенхема    int x12 = Math.Max((int)v2p.X - (int)v1p.X, 1);    int x13 = Math.Max((int)v3p.X - (int)v1p.X, 1);    //теперь помимо проекций будем интерполировать и исходные точки    float dy12 = (v2p.Y - v1p.Y) / x12; var dr12 = (v2 - v1) / x12;    float dy13 = (v3p.Y - v1p.Y) / x13; var dr13 = (v3 - v1) / x13;    Vector3 deltaUp, deltaDown; float deltaUpY, deltaDownY;    if (dy12 > dy13) { deltaUp = dr12; deltaDown = dr13; deltaUpY = dy12; deltaDownY = dy13;}    else { deltaUp = dr13; deltaDown = dr12; deltaUpY = dy13; deltaDownY = dy12;}    TrianglePart(v1 , deltaUp , deltaDown , x12 , 1 , v1p , deltaUpY , deltaDownY , zbuffer);    //вторую часть треугольника аналогично - думаю вы поняли}public void ComputePolyPart(Vector3 start, Vector3 deltaUp, Vector3 deltaDown,    int xSteps, int xDir, Vector2 pixelStart, float deltaUpPixel, float deltaDownPixel , Vector3[] zbuffer){    int pixelStartX = (int)pixelStart.X;    Vector3 up = start - deltaUp, down = start - deltaDown;    float pixelUp = pixelStart.Y - deltaUpPixel, pixelDown = pixelStart.Y - deltaDownPixel;    for (int i = 0; i <= xSteps; i++)    {        up += deltaUp; pixelUp += deltaUpPixel;        down += deltaDown; pixelDown += deltaDownPixel;        int steps = ((int)pixelUp - (int)pixelDown);        var delta = steps == 0 ? Vector3.Zero : (up - down) / steps;        Vector3 position = down - delta;        for (int g = 0; g <= steps; g++)        {            position += delta;            var proection = new Point(pixelStartX + i * xDir, (int)pixelDown + g);            int index = proection.Y * Width + proection.X;            //проверка на глубину            if (zbuffer[index].Z == 0 || zbuffer[index].Z > position.Z)            {                zbuffer[index] = position;            }        }    }}


image

Анимация шагов растеризатора(при перезаписи глубины в zbuffer-е пиксель выделяется красным):

Для удобства я вынес весь код в отдельный модуль Rasterizer:

Класс растеризатора
    public class Rasterizer    {        public Vertex[] ZBuffer;        public int[] VisibleIndexes;        public int VisibleCount;        public int Width;        public int Height;        public Camera Camera;        public Rasterizer(Camera camera)        {            Shaders = shaders;            Width = camera.ScreenWidth;            Height = camera.ScreenHeight;            Camera = camera;        }        public Bitmap Rasterize(IEnumerable<Primitive> primitives)        {            var buffer = new Bitmap(Width , Height);            ComputeVisibleVertices(primitives);            for (int i = 0; i < VisibleCount; i++)            {                var vec = ZBuffer[index];                var proec = Camera.ScreenProection(vec);                buffer.SetPixel(proec.X , proec.Y);            }            return buffer.Bitmap;        }        public void ComputeVisibleVertices(IEnumerable<Primitive> primitives)        {            VisibleCount = 0;            VisibleIndexes = new int[Width * Height];            ZBuffer = new Vertex[Width * Height];            foreach (var prim in primitives)            {                foreach (var poly in prim.GetPolys())                {                    MakeLocal(poly);                    ComputePoly(poly.Item1, poly.Item2, poly.Item3);                }            }        }        public void MakeLocal(Poly poly)        {            poly.Item1.Position = Camera.Pivot.ToLocalCoords(poly.Item1.Position);            poly.Item2.Position = Camera.Pivot.ToLocalCoords(poly.Item2.Position);            poly.Item3.Position = Camera.Pivot.ToLocalCoords(poly.Item3.Position);        }    }


Теперь проверим работу рендера. Для этого я использую модель Сильваны из известной RPG WOW:

image


Не очень понятно, правда? А все потому что здесь нет ни текстур ни освещения. Но вскоре мы это исправим.

Текстуры! Нормали! Освещение! Мотор!


Почему я объединил все это в один раздел? А потому что по своей сути текстуризация и расчет нормалей абсолютно идентичны и скоро вы это поймете.

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

image

Заметьте, что начало текстуры (левый нижний пиксель) в текстурных координатах имеет значение {0, 0}, конец (правый верхний пиксель) {1, 1}. Учитывайте систему координат текстуры и возможность выхода за границы картинки когда текстурная координата равна 1.

Сразу создадим класс для представления данных вершины:

  public class Vertex    {        public Vector3 Position { get; set; }        public Color Color { get; set; }        public Vector2 TextureCoord { get; set; }        public Vector3 Normal { get; set; }        public Vertex(Vector3 pos , Color color , Vector2 texCoord , Vector3 normal)        {            Position = pos;            Color = color;            TextureCoord = texCoord;            Normal = normal;        }    }

Зачем нужны нормали я объясню позже, пока что просто будем знать, что у вершин они могут быть. Теперь для текстуризации полигона нам необходимо каким-то образом сопоставить значение цвета из текстуры конкретному пикселю. Помните как мы интерполировали вершины? Здесь нужно сделать то же самое! Я не буду еще раз переписывать код растеризации, а предлагаю вам самим реализовать текстурирование в вашем рендере. Результатом должно быть корректное отображение текстур на модели. Вот, что получилось у меня:

текстурированная модель
image


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

Освещение



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

Модель Фонга


В общем случае этот метод имитирует наличие 3х составляющих освещения: фоновая(ambient), рассеянная(diffuse) и зеркальная(reflect). Сумма этих трех компонент в итоге даст имитацию физического поведения света.

image

Модель Фонга

Для расчета освещения по Фонгу нам будут нужны нормали к поверхностям, для этого я и добавил их в классе Vertex. Где же брать значения этих нормалей? Нет, ничего вычислять нам не нужно. Дело в том, что великодушные 3д редакторы часто сами считают их и предоставляют вместе с данными модели в контексте формата OBJ. Распарсив файл модели мы получаем значение нормалей для 3х вершин каждого полигона.

image

Картинка из вики

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

Фоновый свет (Ambient)


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

Рассеянный свет (Diffuse)


Когда свет падает на поверхность полигона он равномерно рассеивается. Для расчета diffuse значения на конкретном пикселе учитывается угол под которым свет падает на поверхность. Чтобы рассчитать этот угол можно применить скалярное произведение падающего луча и нормали(само собой вектора перед этим нужно нормализировать). Этот угол будет умножаться на некий коэффициент интенсивности света. Если скалярное произведение отрицательно это значит, что угол между векторами больше 90 градусов. В этом случае мы начнем рассчитывать уже не осветление, а, наоборот, затенение. Стоит избегать этого момента, сделать это можно с помощью функции max.

Код
public interface IShader    {        void ComputeShader(Vertex vertex, Camera camera);    }    public struct Light    {        public Vector3 Pos;        public float Intensivity;    }public class PhongModelShader : IShader    {        public static float DiffuseCoef = 0.1f;        public Light[] Lights { get; set; }        public PhongModelShader(params Light[] lights)        {            Lights = lights;        }        public void ComputeShader(Vertex vertex, Camera camera)        {            if (vertex.Normal.X == 0 && vertex.Normal.Y == 0 && vertex.Normal.Z == 0)            {                return;            }            var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);            foreach (var light in Lights)            {                var ldir = Vector3.Normalize(light.Pos - gPos);                var diffuseVal = Math.Max(VectorMath.Cross(ldir, vertex.Normal), 0) * light.Intensivity;                vertex.Color = Color.FromArgb(vertex.Color.A,                    (int)Math.Min(255, vertex.Color.R * diffuseVal * DiffuseCoef),                    (int)Math.Min(255, vertex.Color.G * diffuseVal * DiffuseCoef,                    (int)Math.Min(255, vertex.Color.B * diffuseVal * DiffuseCoef));            }        }    }


Давайте применим рассеянный свет и рассеем тьму:

image

Зеркальный свет (Reflect)


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

image

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

код
    public class PhongModelShader : IShader    {        public static float DiffuseCoef = 0.1f;        public static float ReflectCoef = 0.2f;        public Light[] Lights { get; set; }        public PhongModelShader(params Light[] lights)        {            Lights = lights;        }        public void ComputeShader(Vertex vertex, Camera camera)        {            if (vertex.Normal.X == 0 && vertex.Normal.Y == 0 && vertex.Normal.Z == 0)            {                return;            }            var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);            foreach (var light in Lights)            {                var ldir = Vector3.Normalize(light.Pos - gPos);                //Следующие три строчки нужны чтобы найти отраженный от поверхности луч                var proection = VectorMath.Proection(ldir, -vertex.Normal);                var d = ldir - proection;                var reflect = proection - d;                var diffuseVal = Math.Max(VectorMath.Cross(ldir, -vertex.Normal), 0) * light.Intensivity;                //луч от наблюдателя                var eye = Vector3.Normalize(-vertex.Position);                var reflectVal = Math.Max(VectorMath.Cross(reflect, eye), 0) * light.Intensivity;                var total = diffuseVal * DiffuseCoef + reflectVal * ReflectCoef;                vertex.Color = Color.FromArgb(vertex.Color.A,                    (int)Math.Min(255, vertex.Color.R * total),                    (int)Math.Min(255, vertex.Color.G * total),                    (int)Math.Min(255, vertex.Color.B * total));            }        }    }


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

image


Тени


Конечной точкой моего изложения будет реализация теней для рендера. Первая тупиковая идея которая зародилась у меня в черепушке для каждой точки проверять не лежит ли между ней и светом какой-нибудь полигон. Если лежит значит не нужно освещать пиксель. Модель Сильваны содержит 220к с лихвой полигонов. Если так для каждой точки проверять пересечение со всеми этими полигонами, то нужно сделать максимум 220000 * 1920 * 1080 * 219999 вызовов метода пересечения! За 10 минут мой компьютер смог осилить 10-у часть всех вычислений (2600 полигонов из 220000), после чего у меня случился сдвиг и я отправился на поиски нового метода.

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

Код
public class ShadowMappingShader : IShader{    public Enviroment Enviroment { get; set; }    public Rasterizer Rasterizer { get; set; }    public Camera Camera => Rasterizer.Camera;    public Pivot Pivot => Camera.Pivot;    public Vertex[] ZBuffer => Rasterizer.ZBuffer;    public float LightIntensivity { get; set; }    public ShadowMappingShader(Enviroment enviroment, Rasterizer rasterizer, float lightIntensivity)    {        Enviroment = enviroment;        LightIntensivity = lightIntensivity;        Rasterizer = rasterizer;        //я добвил события в объекты рендера, привязав к ним перерасчет карты теней        //теперь при вращении/движении камеры либо при изменение сцены шейдер будет перезаписывать глубину        Camera.OnRotate += () => UpdateDepthMap(Enviroment.Primitives);        Camera.OnMove += () => UpdateDepthMap(Enviroment.Primitives);        Enviroment.OnChange += () => UpdateDepthMap(Enviroment.Primitives);        UpdateVisible(Enviroment.Primitives);    }    public void ComputeShader(Vertex vertex, Camera camera)    {        //вычисляем глобальные координаты вершины        var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);        //дистанция до света        var lghDir = Pivot.Center - gPos;        var distance = lghDir.Length();        var local = Pivot.ToLocalCoords(gPos);        var proectToLight = Camera.ScreenProection(local).ToPoint();        if (proectToLight.X >= 0 && proectToLight.X < Camera.ScreenWidth && proectToLight.Y >= 0            && proectToLight.Y < Camera.ScreenHeight)        {            int index = proectToLight.Y * Camera.ScreenWidth + proectToLight.X;            if (ZBuffer[index] == null || ZBuffer[index].Position.Z >= local.Z)            {                vertex.Color = Color.FromArgb(vertex.Color.A,                    (int)Math.Min(255, vertex.Color.R + LightIntensivity / distance),                    (int)Math.Min(255, vertex.Color.G + LightIntensivity / distance),                    (int)Math.Min(255, vertex.Color.B + LightIntensivity / distance));            }        }        else        {            vertex.Color = Color.FromArgb(vertex.Color.A,                    (int)Math.Min(255, vertex.Color.R + (LightIntensivity / distance) / 15),                    (int)Math.Min(255, vertex.Color.G + (LightIntensivity / distance) / 15),                    (int)Math.Min(255, vertex.Color.B + (LightIntensivity / distance) / 15));        }    }    public void UpdateDepthMap(IEnumerable<Primitive> primitives)    {        Rasterizer.ComputeVisibleVertices(primitives);    }}


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

image


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

Улучшенные тени
public class ShadowMappingShader : IShader{    public Enviroment Enviroment { get; set; }    public Rasterizer Rasterizer { get; set; }    public Camera Camera => Rasterizer.Camera;    public Pivot Pivot => Camera.Pivot;    public Vertex[] ZBuffer => Rasterizer.ZBuffer;    public float LightIntensivity { get; set; }    public ShadowMappingShader(Enviroment enviroment, Rasterizer rasterizer, float lightIntensivity)    {        Enviroment = enviroment;        LightIntensivity = lightIntensivity;        Rasterizer = rasterizer;        //я добвил события в объекты рендера, привязав к ним перерасчет карты теней        //теперь при вращении/движении камеры либо при изменение сцены шейдер будет перезаписывать глубину        Camera.OnRotate += () => UpdateDepthMap(Enviroment.Primitives);        Camera.OnMove += () => UpdateDepthMap(Enviroment.Primitives);        Enviroment.OnChange += () => UpdateDepthMap(Enviroment.Primitives);        UpdateVisible(Enviroment.Primitives);    }    public void ComputeShader(Vertex vertex, Camera camera)    {        //вычисляем глобальные координаты вершины        var gPos = camera.Pivot.ToGlobalCoords(vertex.Position);        //дистанция до света        var lghDir = Pivot.Center - gPos;        var distance = lghDir.Length();        var local = Pivot.ToLocalCoords(gPos);        var proectToLight = Camera.ScreenProection(local).ToPoint();        if (proectToLight.X >= 0 && proectToLight.X < Camera.ScreenWidth && proectToLight.Y >= 0            && proectToLight.Y < Camera.ScreenHeight)        {            int index = proectToLight.Y * Camera.ScreenWidth + proectToLight.X;            var n = Vector3.Normalize(vertex.Normal);            var ld = Vector3.Normalize(lghDir);            //вычисляем сдвиг глубины            float bias = (float)Math.Max(10 * (1.0 - VectorMath.Cross(n, ld)), 0.05);            if (ZBuffer[index] == null || ZBuffer[index].Position.Z + bias >= local.Z)            {                vertex.Color = Color.FromArgb(vertex.Color.A,                    (int)Math.Min(255, vertex.Color.R + LightIntensivity / distance),                    (int)Math.Min(255, vertex.Color.G + LightIntensivity / distance),                    (int)Math.Min(255, vertex.Color.B + LightIntensivity / distance));            }        }        else        {            vertex.Color = Color.FromArgb(vertex.Color.A,                    (int)Math.Min(255, vertex.Color.R + (LightIntensivity / distance) / 15),                    (int)Math.Min(255, vertex.Color.G + (LightIntensivity / distance) / 15),                    (int)Math.Min(255, vertex.Color.B + (LightIntensivity / distance) / 15));        }    }    public void UpdateDepthMap(IEnumerable<Primitive> primitives)    {        Rasterizer.ComputeVisibleVertices(primitives);    }}

image


Бонус

Играем с нормалями


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

image


Двигаем свет


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

            float angle = (float)Math.PI / 90;            var shader = (preparer.Shaders[0] as PhongModelShader);            for (int i = 0; i < 180; i+=2)            {                shader.Lights[0] = = new Light()                    {                        Pos = shader.Lights[0].Pos.Rotate(angle , Axis.X) ,                        Intensivity = shader.Lights[0].Intensivity                    };                Draw();            }

image

Производительность


Для теста использовалась следующие конфигурации:

  • Модель Сильваны: 220к полигонов.
  • Разрешение экрана: 1920x1080.
  • Шейдеры: Phong model shader
  • Конфигурация компьютера: cpu core i7 4790, 8 gb ram

FPS рендеринга составлял 1-2 кадр/сек. Это далеко не realtime. Однако стоит все же учитывать, что вся обработка происходила без использования многопоточности, т.е. на одном ядре cpu.

Заключение


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

Краткая история 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% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Улучшение улучшенного фотореализма

14.05.2021 18:17:37 | Автор: admin
Разработчики из Intel Labs при помощи сверточной нейросети улучшают синтертические изображения, повышаеют их стабильность и реализм.

GTA V to Cityscapes


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

image

Модификации также стабильны во времени:



Озеленяем выжженную траву и холмы в Калифорнии в GTA:

image

Добавляем отражения в окна и увеличиваем эффект Френеля (например, на крыше автомобилей):

image

Восстанавливаем дороги:

image

image

image

image

Перевод GTA V в Mapillary Vistas


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

image

Удаляем далекую дымку и перестраиваем дорогу:

image

Траву делаем более объемной и зеленой:

image

image

image

image

Подробнее..

Шаблон удостоверяющей печати, когда нужно правильно и не как у всех

14.05.2021 22:04:58 | Автор: admin

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

Началось всё в 2009 году, когда вместо того, чтобы, для документов в электронном виде, отсканировать печать (как это обычно делается), я, по разным причинам (включая уже заметную на тот момент деформацию полимера действующей печати), запланировал отрисовать её заново. Но перед этим решил проверить: не была ли она сделана при помощи одного из популярных генераторов. Моё предположение оказалось верным и, через несколько минут, печать, со 100% точностью, я получил просто, выбрав подходящий шрифт и введя нужные данные в поля программы. Результат выглядел примерно так:

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

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

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

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

На следующем этапе нужно было определить минимально допустимую толщину линий и размер элементов микротеста для печати, но не найдя ничего конкретного, пришлось ориентироваться на ГОСТ Р 51511-2001, относящийся к печатям с воспроизведением государственного герба Российской Федерации, а именно на п. 6.2.3: Наличие линий толщиной 0,08+0,01 мм., и п. 6.2.1, 6.2.2: Размер элементов микротекста от 0,5 до 0,8 мм.

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

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

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

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

То же самое изображение:

Так это выглядит на самом деле (одинаковый размер печатей):

Таким образом, при одинаковых оснастках (обычно 40 мм.), оттиск по моему шаблону визуально кажется меньше, чем оттиск обычной круглой печати. Избежать этого эффекта, в моем случае, можно было заказав печать на оснастке 42-45 мм.

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

Наигравшись, я забыл о нём еще на несколько лет, до тех пор, пока, всё-таки, не захотел нормально освоить Illustrator, знакомство с которым, на тот момент, ограничивалось опытом работы над одним единственным элементом. Тогда, ожидаемо, очередные размышления на тему того, как бы сейчас выглядел шаблон, привели меня к следующей работе. В этот раз всё было по-другому: я продумывал каждый микрон, каждый элемент многократно переделывался. Иногда проходило несколько дней, прежде, чем он начинал меня полностью устраивать. Если я в чём-то сомневался, то откладывал работу, чтобы потом посмотреть на неё свежим взглядом. Примерно через год, был закончен первый вариант и меня он устраивал всем, кроме казавшейся не достаточной устойчивостью к секторному копированию (как я это назвал), когда для того, чтобы воссоздать тело печати достаточно отрисовать только небольшую её часть. Дальнейшую работу я продолжил в надежде исправить этот недостаток и, спустя какое-то время, сделать это мне удалось в полном объеме от задуманного.

Печать не чувствительна к пространственно ориентации. В середине находится читаемый двухмерный штриховой кодПечать не чувствительна к пространственно ориентации. В середине находится читаемый двухмерный штриховой код

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

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

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

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

Подробнее..

Категории

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

  • Имя: Макс
    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