Как я стал D-шником

С тех пор, как у меня появился компьютер, меня всегда интересовало то, как он работает. Надо сказать, что, хотя по профессии я художник, я всю жизнь интересуюсь механизмами и электроникой. Больше всего, конечно, меня привлекает создание и обработка изображений при помощи вычислительной техники. Но знакомства с одними только графическими редакторами мне оказалось мало – гораздо интереснее изучить и понять, как, собственно, все эти фотошопы и 3ds-max’ы устроены. Ну и, конечно, игры – куда же без них? Мне кажется, что компьютерные игры можно считать «восьмым искусством» после кино. Эволюция четко прослеживается: сначала были статические картины, затем появились «движущиеся» – кинематограф и мультипликация; логично допустить, что следующее поколение произведений искусства должно быть интерактивным: зритель сам должен принимать участие в раскрывающемся перед ним действии. Художники XXI века – это создатели игр…
Моим первым языком программирования был BASIC. Если быть точным, его надмножество Blitz BASIC – это, в сущности, была целая среда для создания простых приложений и игр. Тогда меня поразил сам факт того, что программа для компьютера – это не что иное, как набор инструкций, человекочитаемый текст на специальном языке! Впрочем, человекочитаемость иных текстов на BASIC можно поставить под сомнение – особенно помня слова Эдсгера Дейкстры: «Студентов, ранее изучавших Бейсик, практически невозможно обучить хорошему программированию. Как потенциальные программисты они подверглись необратимой умственной деградации…» И, тем не менее, эта ни с чем не сравнимая магия переменных, циклов, функций – равно как и желтых букв на синем фоне – сделала свое дело. Не говоря уже о трехмерной графике на основе DirectX 7, которая была встроена в язык – стоит ли говорить, насколько поражала воображение возможность вот так, запросто, безо всяких специальных знаний нарисовать на экране вращающийся кубик?..
До кубиков, правда, дело дошло нескоро – ведь необходимо было изучить азы. В этом большую помощь оказали школьные уроки информатики: помимо непосредственно азов (устройство компьютера, двоичная система, теория алгоритмов и т.д.), на них изучался всеми любимый и тепло вспоминаемый Pascal. Конечно, работать в DOS-режиме Windows 95 было еще тем удовольствием, но именно тогда началось мое знакомство со всем семейством паскалеподобных языков, что не могло не сказаться на будущих предпочтениях.
Однако Паскаль, при всех его достоинствах, не слишком хорошо подходил для моей главной цели – создания 2D-игр (трехмерная графика Blitz BASIC мне тогда была еще не по зубам). И тут мне посчастливилось наткнуться на Game Maker – специализированную среду для быстрой разработки игр с собственным редактором уровней, объектной системой, графическим редактором и встроенным скриптовым языком GML. Это была некая смесь Pascal и C++: можно было либо использовать фигурные скобки, либо begin/end. 
GML завоевал мое внимание на долгие пять лет. За это время я изучил Game Maker, что называется, вдоль и поперек, написал множество разнообразных 2D-демок (большинство из которых, увы, не сохранилось) и несколько полноценных игр – вы, кстати, можете ознакомиться с некоторыми тогдашними творениями на странице “Игры и демки”. Меня разочаровывало только одно: отсутствие в программе приличных инструментов для рисования трехмерной графики. Был простенький 3D-режим на основе Direct3D – очень медленный, без доступа к программируемому конвейеру и другим современным «наворотам». Хотя, справедливости ради стоит отметить, что и на нем можно было делать неплохие вещи.

К этому времени появились специализированные 3D-движки, специально написанные для GM. В их числе был враппер популярной Delphi-библиотеки GLScene – Xtreme3D, поддерживавший практически все возможности оной: большое количество поддерживаемых форматов моделей и текстур, анимация, различные спецэффекты вроде динамической воды и системы частиц, менеджер управления ресурсами, встроенная проверка столкновений и даже физика на основе движка ODE. Я заинтересовался, изучил этот движок и даже открыл по нему сайт – http://xtreme3d.narod.ru(впрочем, после переезда на Ucoz он переживает не лучшие времена).

Так совпало, что именно в это время я познакомился с движением СПО (свободного программного обеспечения), и это навсегда изменило мое отношение к компьютерам. В какой-то момент моими привычными инструментами стали исключительно свободные программы – GIMP, Blender, OpenOffice.org и т.д. Я понял, что свобода изучения, изменения и распространения программ важнее их качества – хотя это, на первый взгляд, кажется абсурдным. Так утверждает Ричард Столлман – великий человек, основатель проекта GNU, автор лицензии GPL и создатель таких программ, как компилятор GCC и текстовый редактор Emacs. Над его словами часто иронизируют – но в итоге он всегда оказывается прав.

Закрытость и несвободность ПО – это зло, которое в равной степени вредит и его пользователям, и разработчикам. Пользователям – потому что они никогда не могут быть уверены, что закрытая программа не следит за ними, не ворует их информацию. А разработчикам – потому что закрытую программу труднее поддерживать, улучшать и исправлять: ведь этим занимаются только работники фирмы, владеющей программой. В результате ошибки в ПО исправляются годами – а то и вовсе не исправляются (и это не шутка). А если бы исходный код был публично доступен, любой квалифицированный программист сразу исправил бы найденный баг, отправил бы разработчикам патч, предложил бы рекомендации к улучшению тех или иных конструкций. И таких помощников были бы сотни – взгляните на крупные открытые проекты, развиваемые сообществом. Наконец, если фирма, занимающаяся разработкой программы, обанкротится, то сообщество ее пользователей, не имея на руках исходного кода, оказывается асболютно беспомощным – но зачастую люди продолжают использовать такие «мертвые» программы годами и десятилетиями, если им нет альтернативы. Такая ситуация, к примеру, наблюдается в нашей промышленности и оборонной технике. Вот такие абсурды порождает собственническое отношение к коду. 
Осознав все это, я пришел к выводу, что проприетарная Xtreme3D (к тому же, по всей видимости, заброшенная автором) – это тупиковый путь. В разработке ПО нужно пользоваться только свободными инструментами – и я начал изучать C++, выбрав этот язык как наиболее доступный из всех компилируемых. Моей первой IDE под Windows стала Dev-C++ – среда, основанная на инструментарии GCC/MinGW. Язык сразу поразил меня своей красотой и выразительностью – до этого я не имел никакого понятия о ООП и составных типах данных. Впечатлившись этим богатством и относительной простотой его использования (Dev-C++ позволяла устанавливать пакеты расширения – можно было найти пакеты для работы с OpenGL, DirectX, Irrlicht и т.д.), я загорелся идеей написать свой собственный 3D-движок для Game Maker. 

Дело шло неплохо – пока я в один прекрасный день не установил Linux. Это прямого отношения к моему программерскому хобби не имело, но в силу обстоятельств оказало на него сильное влияние. Несколько лет я большую часть времени проводил в Linux – и, естественно, писал под него программки, изучал линуксовые инструменты для разработчиков. К Game Maker уже практически не прикасался. В какой-то момент я понял, что нет больше смысла привязывать себя к Windows – и с тех пор мой движок существует как отдельный самодостаточный проект. Правда, он до сих пор не принял устоявшейся формы. Нет ни релизов, ни версий, ни постоянного репозитория, ни даже конкретного названия – это просто движок. Периодически я выпускаю какие-то демки и мини-игры, основанные на нем. Но окончательной версии, готовой к использованию сторонними лицами, пока нет.

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

Мое представление о D изначально было весьма смутным: мне казалось, что это какое-то расширение С++ – наподобие того, как сам C++ является расширением C. Ознакомившись со статьей на Википедии, я почувствовал, что это именно то, что мне нужно: компиляция в машинный код, как у C++, и автоматическое управление памятью, как в Java (но надо сказать, что с Java мне на тот момент еще не приходилось работать, поэтому сравнивал я, естественно, с C++). И это не считая многочисленных полезных мелочей, которые D унаследовал от своих многочисленных предшественников – C#, Python, Haskell и др. Обрадовало то, что компилятор D компактен и легок в установке: распаковал архив в любой каталог и работай. В одном архиве – версии для Windows, Linux и FreeBSD, плюс подробная документация по языку и стандартной библиотеке.

Я пишу на D уже несколько лет и очень доволен языком. С каждым релизом он становится все лучше. Не берусь рекомендовать его тем, кто вынужден тянуть вагон legacy-кода на C++ (все-таки, полной бинарной совместимости с C++ у D нет), а также тем, кто такого багажа не имеет, но планирует на полном серьезе трудоустраиваться куда-нибудь в качестве программиста (там тоже будет legacy). Но тот, кто свободен от этого балласта – хакер, любитель, инди-разработчик – обязательно оценит язык по достоинству. Даже если у вас есть некоторое количество кода на «плюсах», без которого вы не можете жить, ничто не мешает потратить пару вечеров и портировать его на D. Сам я так и сделал – некоторые компоненты моей библиотеки dlib были портированы с C++.

dlib начиналась как библиотека линейной алгебры (манипуляции над векторами, матрицами, кватернионами и т.д.) – такие пишет для себя каждый игровой программист. Алгебра сейчас составляет пакет dlib.math. Потом были добавлены средства вычислительной геометрии (dlib.geometry) – игровому движку требуются функции обнаружения пересечений фигур, пространственные измерения и т.д. Вслед за ней – пакет для хранения и обработки изображений (dlib.image), который родился в результате изучения мной различных алгоритмов фильтрации. Эта часть dlib еще далека от завершения – к примеру, полностью поддерживается только формат PNG, не реализованы многие другие функции. Однако dlib.image уже вполне годится для использования в играх. Чем будет dlib в будущем? Вероятнее всего, универсальной вычислительной библиотекой: в перспективе я планирую добавить поддержку обработки и кодирования аудио и видео. dlib можно будет применять как backend для построения игровых движков, симуляторов, аудио- и видеопроигрывателей, программ-конвертеров, различных графических редакторов, инструментов монтажа и композитинга.

Улучшения в dmech

Ведется работа по улучшению физического движка dmech:

  • производительность повышена на 50%;
  • повышена стабильность симуляции;
  • улучшена система проверки столкновений – добавлен генератор contact manifold’ов;
  • реализовано двухосевое трение в солвере контактов.

Демка для Linux (1,84 МБ)
Исходники (0,3 МБ)

Периодически будут появляться новые демки – следите за изменениями в публичной папке на DropBox.

В скором времени изменения будут доступны на GitHub.

Выбор объектов мышью

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

Vector3f clickVector(
    Camera camera, 
    int x, 
    int y, 
    int scrw, 
    int scrh, 
    float yfov)
{
    Vector3f camPos = camera.getPosition();
    Vector3f camDir = camera.getDirectionVector();

    float aspect = cast(float)scrw / cast(float)scrh;

    float xfov = fovXfromY(yfov, aspect);

    float tfov1 = tan(yfov * PI / 360.0f); 
    float tfov2 = tan(xfov * PI / 360.0f);

    Vector3f camUp = camera.getUpVector() * tfov1;
    Vector3f camRight = camera.getRightVector() * tfov2;

    float width  = 1.0f - 2.0f * cast(float)(x) / cast(float)(scrw);
    float height = 1.0f - 2.0f * cast(float)(y) / cast(float)(scrh);

    float mx = camDir.x + camUp.x * height + camRight.x * width;
    float my = camDir.y + camUp.y * height + camRight.y * width;
    float mz = camDir.z + camUp.z * height + camRight.z * width;

    return Vector3f(mx, my, mz);
}

T fovXfromY(T) (T yfov, T aspectRatio) 
{
    yfov = degtorad(yfov);
    T xfov = 2.0 * atan(tan(yfov * 0.5) * aspectRatio);
    return radtodeg(xfov);
}

При нажатии кнопки мыши делаем следующее:

Vector3f v = -clickVector(
    camera, mouseX, mouseY, 
    windowWidth, windowHeight, 60);

v.normalize();

Vector3f rayStart = camera.getPosition;
Vector3f rayEnd = camera.getPosition + v * 1000.0f;
Ray r = Ray(rayStart, rayEnd);

Vector3f isecPoint;
float nearestDistance = float.max;
int pickIndex = -1;

foreach(i, obj; sceneObjects)
{
    auto bs = obj.boundingSphere;
    if (r.intersectSphere(bs, isecPoint))
    {
        float d = distance(isecPoint, rayStart);
        if (d < nearestDistance)
        {
            nearestDistance = d;
            pickIndex = i;
        }
    }
}

if (pickIndex >= 0)
{
    writefln("You clicked sphere %s!", pickIndex);
}

Физический движок

Не писал уже целый месяц, приношу за это извинения читателям этого блога =) Месяц ушел на создание каркаса физического движка для Atrium. Для решения контактов используется метод Sequential Impulses, поддерживается трение. На данный момент реализованы два типа геометрических объектов – сфера и бесконечная плоскость. Интегрирование пока производится методом Эйлера, планируется поддержка метода Рунге-Кутты.

В представленной демке симулируется система из 6 шаров и плоскости. Красным шаром можно управлять с клавиатуры (клавиши-стрелки). Зеленый шар имеет повышенную массу, большой шар – пониженную.

Сборка для Linux:
physics-testbuild1-linux-glibc28-x86.tar.bz2

Эффект затенения

«Честные» динамические тени в аркадных 3D-играх не всегда бывают уместны. Как правило, разработчики ограничиваются статическими предрассчитанными тенями от неподвижных объектов и простым темным кружочком на земле под персонажем – дешево и сердито =)  Я решил дополнить этот нехитрый метод одной простой, но важной деталью: изменение яркости персонажа в зависимости от его местоположения – в тени или на свету. 

Цвет для яркости будет считываться из карты освещения, в которой «запечены» все статические тени на карте.

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

// Проверяем факт пересечения
IntrStatus istatus = character.downRay.intersectTriangle(tri);
if (istatus.hit)
{
  // Извлекаем точку пересечения
  Vector3f ipt = istatus.intersectionPoint;  
  
  if (ipt.y > currentFloorHeight)
  {
    currentFloorHeight = ipt.y;
    
    // Берем материал полигона
    Material mat = materialByIndex[tri.matIndex];
    
    // Если нет карты освещения (текстура 1), то ничего не делаем
    if (mat.textures[1])
    {
      // Берем изображение карты освещения
      Image lightmap = imageByIndex[mat.textures[1].imgIndex];
      
      // Находим текстурные координаты точки пересечения
      Vector2f tc = triObjSpaceToTexSpace(tri.vertices, tri.texCoords2, ipt);
      
      // Конвертируем текстурные координаты в дискретные
      uint imgX = cast(uint)(tc.x * lightmap.width - 0.5f);
      uint imgY = cast(uint)(tc.y * lightmap.height - 0.5f);
      
      // Считываем цвет пикселя c карты освещения
      Color lumel = lightmap[imgX, imgY];
      
      // Применяем полученный цвет к материалу персонажа
      chMaterial.ambientColor = lumel;
      chMaterial.diffuseColor = lumel;
      chMaterial.specularColor = lumel;
    }
  }
}

Осталось определить функцию triObjSpaceToTexSpace. Она будет использовать барицентрические координаты:

Vector2f triObjSpaceToTexSpace(
  Vector3f[3] vertices, 
  Vector2f[3] texCoords, 
  Vector3f point)
{ 
  Vector3f v0 = vertices[2] - vertices[0];
  Vector3f v1 = vertices[1] - vertices[0];
  Vector3f v2 = point - vertices[0];

  float dot00 = dot(v0, v0);
  float dot01 = dot(v0, v1);
  float dot02 = dot(v0, v2);
  float dot11 = dot(v1, v1);
  float dot12 = dot(v1, v2);

  float invDenom = 1.0f / (dot00 * dot11 - dot01 * dot01);
  float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
  float v = (dot00 * dot12 - dot01 * dot02) * invDenom;
      
  Vector2f t2 = texCoords[1] - texCoords[0];
  Vector2f t1 = texCoords[2] - texCoords[0];

  return texCoords[0] + t1 * u + t2 * v;
}