Pull to refresh

Я обожаю программирование графики

Reading time10 min
Views51K
cover

Я обожаю программирование графики! Мы все совершаем ошибки в процессе проектирования и написания кода. Иногда это ошибки логики (когда алгоритм продуман неточно или не до конца), иногда ошибки, возникающие по невнимательности, и ещё много-много вариантов. И что происходит в обычном рабочем процессе? — В списках нет необходимых записей, какие-то числа считаются неверно, вываливаются сообщения об ошибках и прочее. В программировании графики всё немного веселее, ведь часто мы получаем результат, который просто не соответствует ожидаемому. В своём небольшом проекте я решил сохранять такие “результаты” на протяжении всего процесса разработки и хотел бы поделиться ими с Вами.

Всех, кто не любит Android, Live Wallpaper, Minecraft, велосипеды, поток сознания, который слабо привязан к теме и всё около того, хочу сразу предупредить, что их может огорчить содержание этого поста, поэтому продолжайте чтение на свой страх и риск. Оставлю тут также и предупреждение для пользователей мобильного или просто небезлимитного интернета: дальше последует довольно много картинок.

Здравствуйте!

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

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

О чём речь?

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

Первые сомнения на этот счёт...

Следует немного отвлечься от основного рассказа и пояснить, почему у меня были сомнения на счёт идеи “а напишу — ка я себе что-то сам”. В школьные годы на компьютерах была установлена игра (как я узнал совсем недавно, игра называлась “Клад” для БК-0010), где белый человечек (за кого и следовало играть) собирал что-то (в моей памяти это были именно ключи, хотя, как выяснилось позже, это должны были быть сундуки), а чёрный человечек за что-то очень ненавидел белого и убивал его прикосновением. Не знаю почему, но мысли об игре вызывали у меня ностальгические чувства, и поэтому я решил “а напишу-ка я её сам”.

Чтобы не утомлять Вас рассказом о процессе разработки и прочих деталях (не о том моя история), просто опишу смысл: написал, работало именно так, как я запомнил, поиграл один раз, бросил, т.к. уже “наигрался” в процессе отладки.

Для тех, кому интересно, результат получился вот такой:
image

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

А где тут программирование графики?!

Попробую вернуться к основному рассказу. Я сразу решил, что не буду использовать ни OpenGL, ни ещё чего-то, что помогло бы мне реализовать задачу — только хардкор. Тем более, меня всегда интересовало взаимодействие Java кода с нативным кодом под Android, а тут ещё и подвернулась неплохая возможность попробовать свои силы в решении этой задачи.
Сразу решил проверить, смогу ли я вообще реализовать прорисовку с достаточной частотой кадров, с постоянным вызовом нативной библиотеки. Для проверки я реализовал следующую задачу — заполнение экрана картинками с каким-то произвольным коэффициентом “затенения” (по сути просто с параметром яркости, где исходная картинка считается максимально яркой). Написал вариант на Java и C++. Прогнал оба варианта с грубым тестом подсчёта времени и увидел, что в среднем вариант на C++ работал немного быстрее, даже несмотря на то, что сам вывод готового изображения “на экран” всё-же делала Java. В качестве картинок я сразу взял одну из симпатичных, на мой взгляд, текстур для майнкрафта, результат получился примерно таким:
image

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

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

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

“Результаты”


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

Большие картинки кликабельны, но habrastorage немного уменьшил их размер (оригинал был 7200 х 1280)

Теперь от случайного мусора, переходим к осмысленному содержанию. Линия горизонта «осмысленно»:


Далее было необходимо создать “пещеры” (углубления в поверхности), чтобы рельеф не смотрелся так примитивно. Т.к. к тому моменту реализация была ещё сырая, то проверка алгоритма создания пещер представляла собой рисование “пещер” другой текстурой:


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

Это уже похоже на что-то, но ещё очень далеко от результата.

Поменял текстуры, решил добавить внизу источник света — лаву, но ошибся с текстурой, поэтому низ был «заполнен факелами»:


Исправляю и добавляю ещё один источник света — факелы:


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

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

Целью добавления источников света было более адекватное освещение — освещение от источников света. Источники света были поделены на три группы:
  1. Освещение от неба. Самый яркий источник света, но изначально была задумана смена времени суток, а значит и освещение от неба зависит от времени.
  2. Освещение от лавы. Менее яркий источник света, чем небо днём, но яркость не меняется во времени.
  3. Освещение от факелов. Наименее яркий источник света. Яркость также постоянна.

В результате на освещение блока стали влиять два параметра — расстояние до неба и расстояние от статического источника света:

Слева распространение света от источников, а справа просто “задний план темнее, чем передний”.

Тут стала напрашиваться прозрачность, ведь нельзя оставлять факелы с белым фоном. Чтобы избежать вопросов “и в чём же проблема?”, напомню, что у меня есть массив одних циферок (пикселей) и других циферок (тоже пикселей) и все правила переноса и прорисовки необходимо было ещё написать. Хотели прозрачность — вот вам прозрачность (ещё чуть чуть прозрачнее и было бы необходимо рисовать картинку с камеры телефона, чтобы было достаточно “прозрачно”):

Исправляем…

Исправляя фон, случайно “закрасил” и пещеры землёй:

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

Освещение блока за факелом вычисляется неверно (блок за факелом темнее, чем соседние блоки):

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

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

и


Вышло довольно “случайно”, если не сказать больше.

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

Про деревья я хочу отдельно сказать пару слов… К середине процесса разработки у меня уже был комментарий в коде, несколько записей на бумаге и одна пометка к скриншотам, примерно такого содержания: “i h8 3s”. И на то были причины. Деревья сразу пошли как-то сложно. Каждая мелочь, каждая правка кода обязательно сказывалась на деревьях. В целом, как бы смешно это не звучало, но самой большой занозой оказались именно деревья.

Итак, первая итерация мучений с деревьями:

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

Исправил ошибку и добавил листья по бокам, причём снова не тот блок:


В результате, после смены количества деревьев, получился вот такой результат:


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


Потом последовал обычный для многих разработчиков quick-fix, без особого вникания в суть проблемы (ведь я же только что писал этот код, очевидно, что я могу его исправить не задумываясь!), что, как и можно было предположить, к положительному результату это не привело:

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

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


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

А деревья всё продолжали огорчать — необходимо было сделать так, чтобы они генерировались только на земле, для объёма добавил затенение на некоторые блоки листьев (что кстати тоже к тому моменту не работало так, как хотелось бы):


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


Тут меня почти моментально “осенило”, что же я сделал неправильно и новое исправление не заставило себя долго ждать:


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

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


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


Время от времени, я правил те или иные методы, чтобы привести в порядок код и время от времени получал самые разные результаты. Ещё один из примеров:


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


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

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

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


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


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

Ладно, цвет на картинке слева такой, потому что я забыл про положение синей и красной составляющей, а вот модный эффект motion blur — это уже “спасибо” android за то, что он совершенно верно рисовал моё изображение, у которого я не подумал про альфа канал (в альфа канале к тому моменту могло быть всё что угодно).

Кстати! Давно я не показывал Вам свои деревья! Вот:

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

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


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


Кстати, зомби я в результате так и не добавил (не дошли руки), но систему вэйпойнтов отладил. Обратите внимание и на деревья на этих двух картинках. Деревья всё ещё смотрелись прекрасно…

В какой-то момент, когда я пытался исправить внешний вид деревьев, получил ещё один “положительный”:


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

Затем были и ошибки из-за добавления дополнительных текстур (а значит и сменой индексов текстур):


Потом я “поправил” что-то в алгоритме прорисовки и получил довольно странный эффект (скорее всего напутал с размером и положением текстур):


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

Процесс прорисовки этого чуда смотрится так (очевидно, что это самый оптимальный вариант):


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

Начну с алгоритма уменьшения размера картинки:


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

Больше отличился алгоритм поворота:


Слева наверху — начальное изображение, а справа внизу — желаемый результат, а в середине всё то, что получалось по пути к результату.

Когда алгоритмы были готовы, сделать объём уже было довольно просто. Блок составляли 3 грани (взгляд с одной стороны, псевдо-3д или так называемый 2.5d). Для красоты на грани был нанесён линейный градиент, который тоже пришлось отладить, чтобы получить желаемый результат:


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


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

Результатом исправления этой ошибки стал не столь интересный эффект:


Итоги

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

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

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

Спасибо тем, кто хоть долистал до конца статьи!
Tags:
Hubs:
Total votes 112: ↑102 and ↓10+92
Comments40

Articles