Я обожаю программирование графики! Мы все совершаем ошибки в процессе проектирования и написания кода. Иногда это ошибки логики (когда алгоритм продуман неточно или не до конца), иногда ошибки, возникающие по невнимательности, и ещё много-много вариантов. И что происходит в обычном рабочем процессе? — В списках нет необходимых записей, какие-то числа считаются неверно, вываливаются сообщения об ошибках и прочее. В программировании графики всё немного веселее, ведь часто мы получаем результат, который просто не соответствует ожидаемому. В своём небольшом проекте я решил сохранять такие “результаты” на протяжении всего процесса разработки и хотел бы поделиться ими с Вами.
Всех, кто не любит Android, Live Wallpaper, Minecraft, велосипеды, поток сознания, который слабо привязан к теме и всё около того, хочу сразу предупредить, что их может огорчить содержание этого поста, поэтому продолжайте чтение на свой страх и риск. Оставлю тут также и предупреждение для пользователей мобильного или просто небезлимитного интернета: дальше последует довольно много картинок.
Здравствуйте!
Я разработчик ПО с многолетним стажем. Нельзя сказать, что моя работа однообразна и скучна. Я решаю задачи разного уровня, иногда придумываю решения, иногда сам же и реализую их, иногда ищу уязвимости в системах, а иногда и просто рассказываю, почему монитор — это не компьютер. Пожалуй именно то, что я не занят только кодингом, вносит долю разнообразия и творчества в рабочий процесс.
Бывает также, что из-за различных проблем (бюджет или время на разработку ограничены или просто кого-то из вышестоящих петух клюнул не туда, куда было необходимо), приходится искать готовые решения для многих вещей, что превращает мою работу в детскую игру с кубиками, где требуется “класть кубик на кубик” и смотреть, как манагеры хлопают в ладоши. Пожалуй это и есть та вещь, которая превращает мою работу в рутину (хотя да, даже в моменты “игры с кубиками”, я обычно продолжаю рассказывать, что монитор — это не компьютер). Чтобы хоть как-то избежать рутины и не забыть, каково же реализовывать простые вещи, я люблю в свободное время программировать “всё подряд”. И я заметил, что обожаю именно программирование графики…
О чём речь?
Начну с начала, без особых отступлений о том, кто я и почему. Купив новый телефон (тут уже вырисовывается тенденция моих статей на хабре...), мне захотелось поставить себе “живую обоину”, т.к. уж не знаю почему, но нравится мне, когда на экране что-то шевелится. Наверное всё потому, что тогда я чувствую, что купил телефон с четырёхядерным процессором не зря. Погулял по стору, подумал, что бы я хотел видеть на экране и решил, что хочу что-то в стиле майнкрафта, но к сожалению не нашёл ничего, что бы должным образом радовало меня. Тут то я и имел неосторожность решить сделать всё сам…
Первые сомнения на этот счёт...
Следует немного отвлечься от основного рассказа и пояснить, почему у меня были сомнения на счёт идеи “а напишу — ка я себе что-то сам”. В школьные годы на компьютерах была установлена игра (как я узнал совсем недавно, игра называлась “Клад” для БК-0010), где белый человечек (за кого и следовало играть) собирал что-то (в моей памяти это были именно ключи, хотя, как выяснилось позже, это должны были быть сундуки), а чёрный человечек за что-то очень ненавидел белого и убивал его прикосновением. Не знаю почему, но мысли об игре вызывали у меня ностальгические чувства, и поэтому я решил “а напишу-ка я её сам”.
Чтобы не утомлять Вас рассказом о процессе разработки и прочих деталях (не о том моя история), просто опишу смысл: написал, работало именно так, как я запомнил, поиграл один раз, бросил, т.к. уже “наигрался” в процессе отладки.
Для тех, кому интересно, результат получился вот такой:
Внутренний голос и тут говорил мне о том, что я не стану пользоваться результатом своих трудов, т.к. слишком насмотрюсь на него за время разработки, но я, как обычно, решил, что “уж в этот раз такого точно не будет” (сила самоубеждения, ага). Забегу вперёд и скажу, что внутренний голос был прав…
А где тут программирование графики?!
Попробую вернуться к основному рассказу. Я сразу решил, что не буду использовать ни OpenGL, ни ещё чего-то, что помогло бы мне реализовать задачу — только хардкор. Тем более, меня всегда интересовало взаимодействие Java кода с нативным кодом под Android, а тут ещё и подвернулась неплохая возможность попробовать свои силы в решении этой задачи.
Сразу решил проверить, смогу ли я вообще реализовать прорисовку с достаточной частотой кадров, с постоянным вызовом нативной библиотеки. Для проверки я реализовал следующую задачу — заполнение экрана картинками с каким-то произвольным коэффициентом “затенения” (по сути просто с параметром яркости, где исходная картинка считается максимально яркой). Написал вариант на Java и C++. Прогнал оба варианта с грубым тестом подсчёта времени и увидел, что в среднем вариант на C++ работал немного быстрее, даже несмотря на то, что сам вывод готового изображения “на экран” всё-же делала Java. В качестве картинок я сразу взял одну из симпатичных, на мой взгляд, текстур для майнкрафта, результат получился примерно таким:
Поскольку я с самого начала решил, что стану всё реализовывать сам и не буду подглядывать в литературу или искать помощь в интернете, то в края моего изображения выглядели примерно так:
Очевидно, что неверно обрабатываются изображения, от которых следовало нарисовать только часть, исправляем…
Итак, задача решаема, а значит можно браться за дело и начинать искать решение и оптимальную реализацию.
Поскольку для реализации я выбрал решение с использованием JNI, то в результате разработку вёл в смешанном режиме. Основную логику я писал и проверял под Windows, генерируя сразу изображение для 10-и экранов (это и есть те широкие изображения, которые последуют далее в статье), а время от времени я проверял решение на телефоне.
“Результаты”
Итак, линия горизонта (пока случайными блоками, даже не знаю, почему сделал случайными — пишу статью и сам с себя удивляюсь):
Большие картинки кликабельны, но habrastorage немного уменьшил их размер (оригинал был 7200 х 1280)
Теперь от случайного мусора, переходим к осмысленному содержанию. Линия горизонта «осмысленно»:
Далее было необходимо создать “пещеры” (углубления в поверхности), чтобы рельеф не смотрелся так примитивно. Т.к. к тому моменту реализация была ещё сырая, то проверка алгоритма создания пещер представляла собой рисование “пещер” другой текстурой:
Следующим шагом было решено разделить всё на передний и задний план, причём задний план должен был отличаться более тёмной текстурой:
Это уже похоже на что-то, но ещё очень далеко от результата.
Поменял текстуры, решил добавить внизу источник света — лаву, но ошибся с текстурой, поэтому низ был «заполнен факелами»:
Исправляю и добавляю ещё один источник света — факелы:
Понял, что соотношение блоков переднего и заднего плана меня не устраивает и поменял коэффициенты:
Забегая вперёд, следует отметить, что это та часть, которую я менял много раз, чтобы добиться такого результата, чтобы успокоиться и больше не трогать его.
Целью добавления источников света было более адекватное освещение — освещение от источников света. Источники света были поделены на три группы:
- Освещение от неба. Самый яркий источник света, но изначально была задумана смена времени суток, а значит и освещение от неба зависит от времени.
- Освещение от лавы. Менее яркий источник света, чем небо днём, но яркость не меняется во времени.
- Освещение от факелов. Наименее яркий источник света. Яркость также постоянна.
В результате на освещение блока стали влиять два параметра — расстояние до неба и расстояние от статического источника света:
Слева распространение света от источников, а справа просто “задний план темнее, чем передний”.
Тут стала напрашиваться прозрачность, ведь нельзя оставлять факелы с белым фоном. Чтобы избежать вопросов “и в чём же проблема?”, напомню, что у меня есть массив одних циферок (пикселей) и других циферок (тоже пикселей) и все правила переноса и прорисовки необходимо было ещё написать. Хотели прозрачность — вот вам прозрачность (ещё чуть чуть прозрачнее и было бы необходимо рисовать картинку с камеры телефона, чтобы было достаточно “прозрачно”):
Исправляем…
Исправляя фон, случайно “закрасил” и пещеры землёй:
Я знаю, что может показаться, что-то из этого было сделано умышлено, но уверяю вас, что все результаты были получены случайно. По правде говоря, я даже затрудняюсь сказать, как мне это удалось, но результат такой, какой он есть. Исправляем и это…
Освещение блока за факелом вычисляется неверно (блок за факелом темнее, чем соседние блоки):
Ошибка довольно глупая, но сразу я как-то и не подумал, что факел может быть в хорошо освещённом месте и считал освещённость блока максимальным значением яркости факела. Решений могло быть по крайней мере два — исправить освещение или убрать факелы из освещённых мест. Я решил исправить освещение.
Теперь я решил, что необходимо сделать возможность указывать строку (seed), которая задаёт уникальную “карту”, а значит нужна была и своя реализация генерации случайных чисел (на самом деле не была нужна, т.к. хватило бы и обычного rand, но просился велосипед):
и
Вышло довольно “случайно”, если не сказать больше.
Следующим шагом, были добавлены дополнительные блоки (этот шаг я упускаю, уж извините, и так очень много картинок), чтобы разнообразить внешний вид рельефа и тут пришла очередь деревьев.
Про деревья я хочу отдельно сказать пару слов… К середине процесса разработки у меня уже был комментарий в коде, несколько записей на бумаге и одна пометка к скриншотам, примерно такого содержания: “i h8 3s”. И на то были причины. Деревья сразу пошли как-то сложно. Каждая мелочь, каждая правка кода обязательно сказывалась на деревьях. В целом, как бы смешно это не звучало, но самой большой занозой оказались именно деревья.
Итак, первая итерация мучений с деревьями:
Для начала было решено сделать ствол дерева и листья над стволом (пока без листьев по бокам), но даже тут я перепутал блоки и получил перевёрнутый результат.
Исправил ошибку и добавил листья по бокам, причём снова не тот блок:
В результате, после смены количества деревьев, получился вот такой результат:
Затем я решил, что алгоритм генерации линии горизонта огорчает меня и что было бы неплохо его поправить, что я и сделал. Результат был довольно предсказуемый — ничего хорошего:
Потом последовал обычный для многих разработчиков quick-fix, без особого вникания в суть проблемы (ведь я же только что писал этот код, очевидно, что я могу его исправить не задумываясь!), что, как и можно было предположить, к положительному результату это не привело:
Довольно любопытно то, что линия горизонта не совершенно ровная, а имеет небольшое искажение в начале.
В результате я поэкспериментировал с разными параметрами и после множества результатов успокоился и перестал думать о исправлениях для линии горизонта. Вот несколько результатов для линии горизонта:
После того, как я получил работающий прототип, пришло время отладить его, почистить код и заняться оптимизацией. Начал я с того, что стал избавляться от magic numbers, которых было около 10 и которые привязывали большую часть параметров к размеру экрана и блоков на момент тестов.
А деревья всё продолжали огорчать — необходимо было сделать так, чтобы они генерировались только на земле, для объёма добавил затенение на некоторые блоки листьев (что кстати тоже к тому моменту не работало так, как хотелось бы):
Стало ясно, что нужно отладить функцию прорисовки затенённый блоков, а заодно и оптимизировать в ней кое-что. Снова быстрое исправление “ошибок”, снова довольно забавный результат:
Тут меня почти моментально “осенило”, что же я сделал неправильно и новое исправление не заставило себя долго ждать:
Тут следует сразу оговориться, что обычно я не пишу код в таком стиле (т.е. в стиле “сначала пишу, потом думаю”). Но в данном проекте я находил это очень забавным. Ведь каждая моя ошибка, каждая глупость, обязательно приводила к результату, причём очень редко я мог предсказать этот результат или сразу объяснить “почему так”.
К этому моменту, текстура листьев и травы (земля с травой) была определённого зелёного цвета. Просилась реализация, которая позволяла бы менять цвет, позволяя малой кровью менять время года. Да, я отлично знаю, что это можно было легко сделать в джаве и не придумывать ничего, но спортивный интерес был слишком силён. Для этого, текстура была изменена и была написана функция для “покраски текстуры”:
Саму функцию пришлось несколько раз исправлять, т.к. она создавала эффект разнокалиберного полосатика (два изображения в середине), но не делала того, что необходимо (правое изображение). Вот несколько результатов работы алгоритма:
Время от времени, я правил те или иные методы, чтобы привести в порядок код и время от времени получал самые разные результаты. Ещё один из примеров:
Время от времени всплывали и довольно забавные ситуации из мира отладки. Например, ошибка в прорисовке градиента (очередной генератор разноцветных полосатиков):
Во время отладки этой ошибки, когда у меня уже стали заканчиваться идеи, отладчик выдал вот такое значение цвета:
В тот день отладку я оставил и решил отдохнуть, чтобы отладчик перестал рассказывать про плохое качество еды.
На этом моменте я решил начать параллельно тестировать результат и на телефоне. В телефоне порядок цветов оказался немного другим, поэтому красная и синяя составляющие цвета поменялись местами:
Кстати, я забыл лишний раз напомнить, что ненавижу деревья… Деревья ночью вели себя странно:
В это время, у телефона вообще был свой собственный взгляд на то, как следует рисовать картинку после смещения (скролинга пальцем):
и
Ладно, цвет на картинке слева такой, потому что я забыл про положение синей и красной составляющей, а вот модный эффект motion blur — это уже “спасибо” android за то, что он совершенно верно рисовал моё изображение, у которого я не подумал про альфа канал (в альфа канале к тому моменту могло быть всё что угодно).
Кстати! Давно я не показывал Вам свои деревья! Вот:
Проблем тут довольно много — и неверное освещение некоторых блоков листьев, и неверная прорисовка “прозрачных пикселей”.
Параллельно я начал работу над системой waypoint’ов или, говоря простым языком, алгоритмом поиска путей. Путь был нужен, чтобы была возможность добавить зомби и прочих персонажей и при этом не пришлось бы каждый раз просчитывать их поведение на несколько шагов вперёд (чтобы они не тупили на месте). Для себя я стал отмечать пути визуально, чтобы оценить качество алгоритма:
Более продвинутый вариант визуализации смотрелся вот так:
Кстати, зомби я в результате так и не добавил (не дошли руки), но систему вэйпойнтов отладил. Обратите внимание и на деревья на этих двух картинках. Деревья всё ещё смотрелись прекрасно…
В какой-то момент, когда я пытался исправить внешний вид деревьев, получил ещё один “положительный”:
Вот ещё несколько любопытных багов с телефона, которые напрямую связаны с прозрачностью (альфа каналом):
и
Затем были и ошибки из-за добавления дополнительных текстур (а значит и сменой индексов текстур):
Потом я “поправил” что-то в алгоритме прорисовки и получил довольно странный эффект (скорее всего напутал с размером и положением текстур):
В результате последних оптимизаций алгоритма прорисовки, я получил сразу две ошибки (снова разнокалиберные полосатики):
и
Процесс прорисовки этого чуда смотрится так (очевидно, что это самый оптимальный вариант):
Последней вещью, которую было необходимо реализовать, был эффект обьёма (грани блоков), чтобы картинка не была такой плоской и простой. Для граней необходимо было написать два алгоритма — алгоритм изменения размера картинки и алгоритм наклона изображения.
Начну с алгоритма уменьшения размера картинки:
Сверху вниз — результаты работы алгоритма. В самом низу — желаемый результат (тест проводился на большой картинке, для наглядности).
Больше отличился алгоритм поворота:
Слева наверху — начальное изображение, а справа внизу — желаемый результат, а в середине всё то, что получалось по пути к результату.
Когда алгоритмы были готовы, сделать объём уже было довольно просто. Блок составляли 3 грани (взгляд с одной стороны, псевдо-3д или так называемый 2.5d). Для красоты на грани был нанесён линейный градиент, который тоже пришлось отладить, чтобы получить желаемый результат:
Поскольку с размером граней блока я так и не смог определиться, сделал возможность изменять этот параметр:
После всех изменений, было необходимо оптимизировать процесс прорисовки, чтобы не отрисовывать ничего из того, что не нужно рисовать. Результатом этой оптимизации снова стала проблема с каналом прозрачности, которая всплыла во время тестов на телефоне. Должен признать, что полученный результат смотрелся довольно любопытно:
и
Результатом исправления этой ошибки стал не столь интересный эффект:
Итоги
В конце концов все критические ошибки были отлажены, добавил текстур и плюшек в виде эффектов. Были сделаны две версии для стора — бесплатная (очень упрощённая) и платная (со всеми плюшками).
Техническую сторону решил даже не поднимать в этой статье, т.к. и так не уверен, что кто-то дочитает её до конца. Если людям, которые осилили эту статью, будет интересно узнать о технической стороне этой истории, то напишите в комментариях или в личку, и я обязательно расскажу про сложности с которыми столкнулся, особенностями реализации и прочими деталями.
Надеюсь, что это подтолкнёт ещё кого-то на создание чего-либо своими руками, а не только используя готовые фреймворки и библиотеки.
Спасибо тем, кто хоть долистал до конца статьи!