В настоящее время интерес к софтверным движкам, как из игр Quake, DOOM или Duke Nukem 3D практически нулевой. Однако, эти движки имели своё очарование и мне, например, очень нравится графика именно таких вот движков с нереалистичными текстурами на стенах. Конечно, такие текстуры можно накладывать без фильтрации и в OpenGL, получая такой же уровень графики, но всё же, написать собственный софтверный движок было весьма интересно. Движок уровня Quake я написать в своё время не смог, так как не удалось создать редактор 3D карты – я просто не представлял, как вообще рисовать в 3D карту. Да и с большой вероятность текстурирование в произвольном случае в моём исполнении (без ассемблера) было бы очень медленно. Но движок уровня DOOM мне покорился. Основу такого движка я написал в 2002 году, пользуясь книжкой Шикина и Борескова “Компьютерная графика. Полигональные модели”. На базе того движка используя графику из Doom я написал некое подобие игры под MS-DOS на Watcom C. Несколько лет назад я решил вынуть из той игры код движка и переработать его под мои текущие знания языка Си++ и представления о том, как стоило бы устроить этот движок. Ну и заодно перенести этот движок под Windows и дополнить наклонами головы, как в Blood или Duke Nukem. О том, что в результате получилось, я и написал в этой статье.

Итак, в этой статье я расскажу, как написать движок уровня Duke Nukem 3D, однако, без наклонных поверхностей, которые есть в этой игре. То есть, до движка Build моё творение не дотянуло. Кроме того, представленный движок будет использовать не BSP (как DOOM), а метод порталов. Изначальный мой движок использовал как раз BSP, но, как оказалось, метод порталов работает быстрее. Про BSP же я тоже расскажу и приведу сам движок его использующий.

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


Вид лабиринта из редактора карты

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



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


Разбиение карты

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

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


Экранный портал

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


В этом уравнении портал задаётся прямой с координатами (x1;y1)-(x2;y2), а положение игрока соответствует координатам (x,y). Если в результате расчета значение P окажется в некотором диапазоне (я принял от -10 до +10), то можно считать, что (x,y) находится очень близко к прямой, на которой лежит портал.

Общим в обоих вариантах движков является метод рисования вертикальных и горизонтальных поверхностей. Так как движок у нас типа DOOM, то никаких произвольно ориентированных поверхностей у нас не будет – только вертикальные и горизонтальные. Как их выводить? Допустим, с помощью порталов или BSP-дерева мы получим упорядоченный от игрока в бесконечность набор сегментов и линий раздела. Чтобы находящиеся вдали сегменты не затёрли уже выведенные ближние к игроку, мы будем использовать линии горизонта. Для каждого столбца экрана (размер окна по X зададим макросом WINDOW_WIDTH) мы зададим две координаты – верхнюю и нижнюю. Итого, нам нужен массив TopLine[WINDOW_WIDTH] и массив BottomLine[WINDOW_WIDTH]. Перед выводом сцены нужно инициализировать эти массивы так:

 for(n=0;n<WindowWidth;n++)
 {
  //инициализировали линии горизонта
  TopLine[n]=0;
  BottomLine[n]=WindowHeight-1;
 }

Где WindowHeight – высота окна, а WindowWidth-ширина окна.

Если для какого-либо столбца экрана x при выводе окажется, что TopLine[x]>BottomLine[x], то данный столбец полностью заполнен, и выводить его не требуется.

Сегмент или линия раздела (которая выводится два раза – как верхняя и как нижняя) при выводе всегда отсекаются по линиям горизонта. Текстурируется только та часть, которая находится между линиями горизонта. В промежуток между верхней точкой выводимой линии или сегмента и TopLine[x] выводится текстура потолка, а между BottomLine[x] и нижней точкой текстура пола (разумеется, для верхней линии раздела пол не рисуется, как и для нижней не рисуется потолок).

При выводе сегмента мы должны установить после вывода столбца TopLine[x]>BottomLine[x], так как сегмент закрывает собой всё, что за ним находится.


Вывод сегмента

При выводе верхней линии раздела, TopLine[x] переносится в нижнюю точку линии раздела. При выводе нижней линии раздела, BottomLine[x] переносится в верхнюю точку линии раздела.
При построении сцены используется перспективная проекция в соответствии с формулой:



Здесь Z – координата глубины (от игрока), а X и Y – положение объекта относительно игрока.



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



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

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

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

Чтобы верно залить пол или потолок после вывода сегмента/линии раздела нам потребуется такая штука, как таблица VisualPlanes. Дело в том, что стены мы выводим вертикально, а пол/потолок горизонтально. Следовательно, нам нужно сначала вывести стену, а затем уже пол или потолок. Вот для этого и нужна эта таблица. Что она собой представляет?

  //параметры текстурирования полов и потолков
  struct SVisualPlanes
  {
   long MinX;//минимальная координата X области
   long MaxX;//максимальная координата X области
   long TopY[WINDOW_WIDTH];//верхняя координата
   long BottomY[WINDOW_WIDTH];//нижняя координата
  };

Такая вот структура описывает всего лишь фрагмент пола (или потолка) от MinX до MaxX с заданной внутри него верхней и нижней координатой столбцов. Выглядит просто и логично, да? Не так просто будет понять, как из этого набора вертикальных столбцов перейти к горизонтальным линиям. Таких таблиц нужно две – для пола и для потолка. В DOOM эти таблицы полов/потолков собираются и объединяются друг с другом (и этим обусловлено ограничение на их количество в одной сцене). Я так не стал делать и использую одни и те же две таблицы пола и потолка, а вывожу пол и потолок сразу после вывода стены. Точки в эти таблицы добавляются вот по каким критериям:

//----------------------------------------------------------------------------------------------------
//заполнение линии буфера текстур пола
//----------------------------------------------------------------------------------------------------
void CEngine_Base::DrawFloorLine(long x,long y1,long y2,SVisualPlanes &sVisualPlanes_Bottom)
{
 if (y2<WindowYCenterWithOffset) return;
 if (y1>=WindowHeight) return;
 if (y1<WindowYCenterWithOffset) y1=WindowYCenterWithOffset;
 if (y2>=WindowHeight) y2=WindowHeight-1;
 if (x>sVisualPlanes_Bottom.MaxX) sVisualPlanes_Bottom.MaxX=x;
 if (x<sVisualPlanes_Bottom.MinX) sVisualPlanes_Bottom.MinX=x;
 if (y1<sVisualPlanes_Bottom.TopY[x]) sVisualPlanes_Bottom.TopY[x]=y1;
 if (y2>sVisualPlanes_Bottom.BottomY[x]) sVisualPlanes_Bottom.BottomY[x]=y2;
}
//----------------------------------------------------------------------------------------------------
//заполнение линии буфера текстур потолка
//----------------------------------------------------------------------------------------------------
void CEngine_Base::DrawFlowLine(long x,long y1,long y2,SVisualPlanes &sVisualPlanes_Top)
{ 
 if (y2<0) return;
 if (y1>WindowYCenterWithOffset) return;
 if (y1<0) y1=0;
 if (y2>WindowYCenterWithOffset) y2=WindowYCenterWithOffset;
 if (x>sVisualPlanes_Top.MaxX) sVisualPlanes_Top.MaxX=x;
 if (x<sVisualPlanes_Top.MinX) sVisualPlanes_Top.MinX=x;
 if (y1<sVisualPlanes_Top.TopY[x]) sVisualPlanes_Top.TopY[x]=y1;
 if (y2>sVisualPlanes_Top.BottomY[x]) sVisualPlanes_Top.BottomY[x]=y2;
}

Здесь x – координата столбца, y1-верхняя точка столбца, y2-нижняя точка столбца, WindowYCenterWithOffset-координата центра экрана (наш движок позволит наклонять голову и тем самым нам вместо истинного центра экрана нужен будет смещённый центр – об этом ниже).

Инициализируются эти таблицы перед каждым выводом стены так:

SVisualPlanes sVisualPlanes_Top;
 SVisualPlanes sVisualPlanes_Bottom;

 sVisualPlanes_Top.MinX=WINDOW_WIDTH-1;
 sVisualPlanes_Top.MaxX=0;
 sVisualPlanes_Bottom.MinX=WINDOW_WIDTH-1;
 sVisualPlanes_Bottom.MaxX=0;
 for(n=0;n<WINDOW_WIDTH;n++)
 {
  sVisualPlanes_Top.TopY[n]=WINDOW_HEIGHT-1;
  sVisualPlanes_Top.BottomY[n]=0;

  sVisualPlanes_Bottom.TopY[n]=WINDOW_HEIGHT-1;
  sVisualPlanes_Bottom.BottomY[n]=0;
 }

А вот рисование по таким таблицам выполняется следующим кодом:

//----------------------------------------------------------------------------------------------------
//рисование потолков
//----------------------------------------------------------------------------------------------------
void CEngine_Base::DrawFlow(long sector_index,const SVisualPlanes &sVisualPlanes_Top)
{
 //параметры сектора
 long level=vector_CISectorPtr[sector_index]->GetUp();
 long texture=vector_CISectorPtr[sector_index]->GetCTextureFollow_Up_Ptr()->GetCurrentTexture().TextureIndex;
 long bright=vector_CISectorPtr[sector_index]->GetLighting();
 long z=static_cast<long>((level-PlayerZ)*(WindowYCenter));

 long x1;
 long x2;
 long x;
 long y;
 x1=sVisualPlanes_Top.MinX;
 x2=sVisualPlanes_Top.MaxX;
 if (x2<x1) return;
 long y_top=sVisualPlanes_Top.TopY[x1];
 long y_bottom=sVisualPlanes_Top.BottomY[x1];
 for(y=y_top;y<=y_bottom;y++) X_Table[y]=x1;

 for(x=x1;x<=x2;x++)
 {
  long zd;
  long y1=sVisualPlanes_Top.TopY[x];
  long y2=sVisualPlanes_Top.BottomY[x];
  if (y2<y1) continue;//при возможных пропусках точек на границах (а они есть), алгоритм развалится
  //если верхняя линия поднимается
  while(y1<y_top) 
  {
   y_top--;
   X_Table[y_top]=x;
  }
  //если нижняя линия опускается
  while(y2>y_bottom) 
  {
   y_bottom++;
   X_Table[y_bottom]=x;
  }

  //если верхняя линия опускается
  zd=(WindowYCenterWithOffset-y_top)+1;
  while(y_top<y1)
  {   
   long dist=z/zd;
   long scale=dist/zd;
   DrawTextureLine(dist,scale,bright,texture,y_top,X_Table[y_top],x-1);   
   y_top++;
   zd--;
  }

  //если нижняя линия поднимается
  zd=(WindowYCenterWithOffset-y_bottom)+1;
  while(y_bottom>y2)
  {   
   long dist=z/zd;
   long scale=dist/zd;
   DrawTextureLine(dist,scale,bright,texture,y_bottom,X_Table[y_bottom],x-1);
   y_bottom--;
   zd++;
  }
 } 
 //заливаем промежуток между top и bottom
 long zd=(WindowYCenterWithOffset-y_top)+1;
 for(y=y_top;y<=y_bottom;y++,zd--)
 {  
  long dist=z/zd;
  long scale=dist/zd;
  DrawTextureLine(dist,scale,bright,texture,y,X_Table[y],x2);
 }
}
//----------------------------------------------------------------------------------------------------
//рисование полов
//----------------------------------------------------------------------------------------------------
void CEngine_Base::DrawFloor(long sector_index,const SVisualPlanes &sVisualPlanes_Bottom)
{ 
 //параметры сектора
 long level=vector_CISectorPtr[sector_index]->GetDown();
 long texture=vector_CISectorPtr[sector_index]->GetCTextureFollow_Down_Ptr()->GetCurrentTexture().TextureIndex;
 long bright=vector_CISectorPtr[sector_index]->GetLighting();
 long z=static_cast<long>((PlayerZ-level)*(WindowYCenter));

 long x1;
 long x2;
 long x;
 long y;
 x1=sVisualPlanes_Bottom.MinX;
 x2=sVisualPlanes_Bottom.MaxX;
 if (x2<x1) return;
 long y_top=sVisualPlanes_Bottom.TopY[x1];
 long y_bottom=sVisualPlanes_Bottom.BottomY[x1];
 for(y=y_top;y<=y_bottom;y++) X_Table[y]=x1;
 for(x=x1;x<=x2;x++)
 {
  long zd;
  long y1=sVisualPlanes_Bottom.TopY[x];
  long y2=sVisualPlanes_Bottom.BottomY[x];
  if (y2<y1) continue;//при возможных пропусках точек на границах (а они есть), алгоритм развалится

  //если верхняя линия поднимается
  while(y1<y_top) 
  {
   y_top--;
   X_Table[y_top]=x;
  }
  //если нижняя линия опускается
  while(y2>y_bottom) 
  {
   y_bottom++;
   X_Table[y_bottom]=x;
  }

  //если верхняя линия опускается
  zd=(y_top-WindowYCenterWithOffset)+1;
  while(y_top<y1)
  {   
   long dist=z/zd;
   long scale=dist/zd;
   DrawTextureLine(dist,scale,bright,texture,y_top,X_Table[y_top],x-1);   
   y_top++;
   zd++;
  }

  //если нижняя линия поднимается
  zd=(y_bottom-WindowYCenterWithOffset)+1;
  while(y_bottom>y2)
  {   
   long dist=z/zd;
   long scale=dist/zd;
   DrawTextureLine(dist,scale,bright,texture,y_bottom,X_Table[y_bottom],x-1);   
   y_bottom--;
   zd--;
  }
 }
 //заливаем промежуток между top и bottom
 long zd=(y_top-WindowYCenterWithOffset)+1;
 for(y=y_top;y<=y_bottom;y++,zd++)
 {  
  long dist=z/zd;
  long scale=dist/zd;
  DrawTextureLine(dist,scale,bright,texture,y,X_Table[y],x2);
 }
}

Здесь DrawTextureLine рисует одну горизонтальную линию текстуры пола или потолка. При выводе используется буфер начальных координат X для каждой координаты Y: long X_Table[WINDOW_HEIGHT].

Сначала в этот буфер мы записываем MinX для первого столбца. Идея тут в том, что при проходе по столбцам мы отслеживаем, как ведёт себя координата Y. Если для левой границы при выводе пола верхняя координата уменьшается, а нижняя увеличивается, мы отмечаем этот факт в буфере начальных координат по X для данных координат Y (X_Table[y]). Но если изменение Y пойдёт в обратную сторону, значит, мы перешли к “закрыванию” линии и перед закрытием линию нужно текстурировать. Этот фокус работает потому, что мы используем выпуклые сектора. Кстати, при выводе сцены с помощью BSP-дерева, следует помнить, что выводится стена целиком, при этом её части могут быть перекрыты другими стенами (при выводе в этих местах линии горизонта установлены в TopLine[x]>BottomLine[x]), а это значит, что при таких разрывах требуется запускать текстурирование полов и потолков и начинать формировать таблицу VisualPlanes заново. В методе порталов таких проблем нет, там стена всегда отсекается по порталу.


Текстурирование с помощью VisualPlanes

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

Также в движке есть освещение. Ну тут совсем всё просто – чем дальше от игрока (координата Z), тем темнее. И для стен и для полов/потолков реализация такого затемнения сложности не представляет.

Ну вот, вроде как и всё.

В архиве лежит сам движок и редактор карт (пока до конца недоделанный – например, нельзя изменить стартовую позицию игрока). Не пугайтесь, если в исходниках увидите многочисленные сдвиги (>> и <<) – местами я использовал вычисления с фиксированной точкой.

В движке управление – курсор, мышка, ctrl-присесть, F1-F3-смена класса движков (экранный портал, геометрический портал, BSP-дерево), F5-сохранить координаты игрока, F9-восстановить координаты игрока.
Поделиться с друзьями
-->

Комментарии (34)


  1. cyberonix
    01.06.2017 14:56

    подскажи, как можно положить плоскость по оси z. Я вращаю плоскость https://www.youtube.com/watch?v=gnfVa_zwK-E по часовой и против.
    Как раз пробую освоить геометрию и 3d в частности.


  1. da-nie
    01.06.2017 14:58

    Что значит «положить»?


  1. cyberonix
    01.06.2017 15:07

    если мы смотрим на плоскость и она от нас как бы легла в глубину по оси z и при этом угол между x и y 90 градусов.


  1. da-nie
    01.06.2017 20:13

    Так положить можно просто умножив вектор координат точек плоскости на матрицу вращения вокруг X на угол 90 градусов.


  1. ustaspolansky
    02.06.2017 06:08

    Прошу прощение за нубство (Скачал посмотрел исходники), отрисовка через GDI?
    Ещё в начале пути освоения и не совсем понял где сама отрисовка в исходниках.


    1. da-nie
      02.06.2017 06:09

      Нет, через Direct Draw. В cvideo задаётся адрес видеопамяти и размер строки, которые получены от Direct Draw при захвате вторичной плоскости.


      1. ustaspolansky
        02.06.2017 10:58

        Спасибо! Будем изучать, благо всё очень аккуратно написано.


        1. da-nie
          02.06.2017 11:27
          +1

          Аккуратно-то аккуратно, но в устаревшей манере — я в современном Си++ плаваю, как топор (всякие лямбды и прочее для меня тёмный лес).
          Там в Direct Draw просто получается адрес видеопамяти, который передаётся в класс видео CVideo. Дальше интерфейсный класс CIEngine определяет все функции вывода графики и загрузки данных. От этого класса унаследован класс CEngine_Base, в котором определены многие функции и добавлены защищённые функции, общие для все трёх вариантов движков (текстурирование линий, например). А уже от этого класса унаследованы сами движки. Все эти классы первым делом у CVideo запрашивают параметры видеоэкрана, а рисуют уже как в MS-DOS — записью в видеопамять.


          1. XProger
            02.06.2017 18:56

            Кстати, чтобы совсем отвязаться от DirectDraw в пользу GDI можно попробовать SetDIBitsToDevice, также выделяешь кусок памяти, растеризуешь и в нужный момент блитишь на поверхность окна.


            1. da-nie
              02.06.2017 20:40

              Э… А зачем нужно от Direct Draw отвязываться? К нему наоборот привязываться надо исходя из соображений быстродействия.


              1. XProger
                02.06.2017 20:41

                Э… портировать на Linux например, да и нет у него нынче никакого быстродействия.


                1. da-nie
                  02.06.2017 21:20

                  Ну, портировать-то как раз просто. Нужно просто в cVideo задать адрес буфера, выделенного с помощью malloc или new. А после рисования вывести этот буфер чем угодно. А клавиатура в движке привязана классом CKeyboard. То есть, клавиатура там виртуальная.
                  Кстати, я под QNX предыдущую версию портировал.

                  #include "cmain.h"
                  
                  CMain cMain;
                  extern CVideo cVideo;
                  
                  //-Конструктор класса--------------------------------------------------------
                  CMain::CMain()
                  {
                   VideoBufferPtr=new unsigned long[WINDOW_WIDTH*WINDOW_HEIGHT];
                   cVideo.SetVideoPointer(VideoBufferPtr,WINDOW_WIDTH); 
                  }
                  //-Деструктор класса---------------------------------------------------------
                  CMain::~CMain()
                  { 
                   delete[](VideoBufferPtr);
                  }
                  //-Замещённые функции предка-----------------------------------------------
                  //-Новые функции класса------------------------------------------------------
                  
                  //----------------------------------------------------------------------------------------------------
                  //открытие окна
                  //----------------------------------------------------------------------------------------------------
                  bool CMain::OpenWindow(void)
                  {
                   PgSetDrawBufferSize(65535);
                   cControl.Init(); 
                   return(true);
                  }
                  //----------------------------------------------------------------------------------------------------
                  //закрытие окна
                  //----------------------------------------------------------------------------------------------------
                  bool CMain::CloseWindow(void)
                  {
                   cControl.Close();	
                   return(true);	
                  }
                  //-Функции обработки сообщений класса----------------------------------------
                  //----------------------------------------------------------------------------------------------------
                  //нажали или отпустили кнопку мыши
                  //----------------------------------------------------------------------------------------------------
                  void CMain::OnActivate_MouseButton(long x,long y,bool left,bool right,bool center)
                  {
                   OnPaint();
                  }
                  //----------------------------------------------------------------------------------------------------
                  //перерисовать картинку
                  //----------------------------------------------------------------------------------------------------
                  void CMain::OnPaint(void)
                  {
                   PhDim_t ImageSize;
                   int ImageBPL=WINDOW_WIDTH*sizeof(unsigned long);
                   ImageSize.w=WINDOW_WIDTH;
                   ImageSize.h=WINDOW_HEIGHT;
                   PhPoint_t pos;
                   pos.x=0;
                   pos.y=0;
                   PgDrawImagev(VideoBufferPtr,Pg_IMAGE_DIRECT_8888,&pos,&ImageSize,ImageBPL,0);  
                  }
                  
                  //----------------------------------------------------------------------------------------------------
                  //обработка таймера
                  //----------------------------------------------------------------------------------------------------
                  void CMain::OnActivate_Timer(void)
                  { 
                   cVideo.SetVideoPointer(VideoBufferPtr,WINDOW_WIDTH);
                   cControl.Processing();	
                   OnPaint();
                  }
                  
                  //-Новые функции класса------------------------------------------------------
                  
                  //-Прочее--------------------------------------------------------------------
                  
                  


                  /* Y o u r   D e s c r i p t i o n                       */
                  /*                            AppBuilder Photon Code Lib */
                  /*                                         Version 2.03  */
                  
                  /* Standard headers */
                  #include <stdio.h>
                  #include <stdlib.h>
                  #include <unistd.h>
                  #include <string.h>
                  #include <photon/PkKeyDef.h>
                  #include <Ph.h>
                  #include <Pt.h>
                  #include <Ap.h>
                  
                  /* Local headers */
                  #include "ablibs.h"
                  #include "abimport.h"
                  #include "proto.h"
                  #include "ckbrd.h"
                  
                  extern CKeyboard cKeyboard;
                  
                  int OnActivate_Raw(PtWidget_t *widget, ApInfo_t *apinfo, PtCallbackInfo_t *cbinfo )
                  {
                   if (cbinfo->event->type==Ph_EV_KEY)
                   {
                    PhKeyEvent_t *kev=(PhKeyEvent_t *)PhGetData(cbinfo->event);
                    long scan=kev->key_scan;
                    if (kev->key_flags&Pk_KF_Scan_Valid) 
                    {
                     if (kev->key_flags&Pk_KF_Key_Down) cKeyboard.SetKeyState(scan,true);
                                                  else  cKeyboard.SetKeyState(scan&0x7f,false);
                     //printf("Scan:%i\r\n",scan);                                
                    }
                   }
                   return(Pt_CONTINUE);
                  }
                  


  1. XProger
    02.06.2017 20:47
    +1

    Ещё вариант портального софт-рендера на C


    1. da-nie
      02.06.2017 21:23

      Прикольно. :) Правда, а это точно софтверный рендер — вот то, что с освещением? Вроде как карта освещенённости используется, но её в софверном варианте довольно дорого выводить.


  1. viiri
    03.06.2017 07:18

    Пожалуйста, укажите лицензию на код и, если совсем не затруднит, создайте репозиторий, например на GitHub или Bitbucket. Спасибо!


  1. da-nie
    03.06.2017 07:23

    Лицензию на код? Я в них не разбираюсь. Как называется лицензия, если вы можете делать с кодом абсолютно всё, что захотите? Free?
    А на Github я не могу выложить — я никогда не работал с такими системами разработки и понятия не имею, как там вообще осуществляется работа. Я попытался понять, нифига не понял, что там вообще нужно делать. Дело в том, что я в команде никогда не работал (хоть я и пишу ПО по работе в НИИ, но я самоучка); у нас таких систем не применяется и опыта работы с ними у меня полный ноль.


    1. SerVB
      03.06.2017 18:50
      +1

      Вроде бы MIT License самая разрешающая. https://ru.m.wikipedia.org/wiki/Лицензия_MIT


      1. da-nie
        04.06.2017 19:09

        Спасибо. :) Ну, значит, MIT. :)

        Правда, не знаю, кому эти исходники пригодиться вообще могут. Мне тут на одном форуме сказали, фигурально, что я фигнёй страдаю с софтверными движками и обычным OpenGL. И что давно вместо «forward rendering» используют Deferred Rendering и надо брать готовый движок типа Ogre3d и не изобретать велосипед. :) И я в общем, почти согласен с первым, но вот со вторым не соглашусь — зачем мне Ogre3D, если я игру делать всё равно не буду. :)


        1. SerVB
          05.06.2017 12:04

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

          По этой теме точно не следует волноваться! Сообществу, имхо, чем больше кодов выложено, тем лучше ;)


  1. da-nie
    03.06.2017 07:32

    Кстати, я совсем забыл — в этом движке используется пирамида фильтрации текстур. Ну тут совсем просто — определяется, во сколько раз текстура сжата и выбирается нужная текстура и всё. Функция FindTexture как раз этим и занимается. Также в движке есть вывод спрайта — он просто закомментирован (функция PutSprite). В качестве спрайта используется текстура стены (он тестовый). Но для его вывода нужно раскомментировать в функциях текстурирования в cEngine_Base.cpp заполнение Z-буфера. Раньше я как раз я Z-буфером спрайты и выводил. Но это медленно, поэтому я отключил спрайты и как будет желание и время, подумаю, как обойтись без него.


  1. Idot
    04.06.2017 10:22

    da-nie нельзя ли, пожалуйста, продублировать скачивание файлов, чтобы качать их НЕ с яндекса?
    (до сих пор пытаюсь скачать пример и постоянно обламываюсь)


    1. da-nie
      04.06.2017 10:43

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


      1. Idot
        04.06.2017 15:41

        Спасибо! Качается!


        1. da-nie
          04.06.2017 19:05

          Кое-что расскажу о том, как это работает. В файлах исходников геометрический портал в cengine_gportal.cpp. Там вот что делается.
          1) Задаём исходный портал, совпадающий с областью поля зрения игрока.
          2) Задаём прямую отсечения — это не более, чем прямая, перпендикулярная вектору зрения игрока. Она нужна, чтобы выбросить из рассмотрения всё, что за спиной игрока. Дело в том, что использованная математика требует, чтобы всё происходило перед игроком.
          3) Назначаем текущим сектором, сектор, где находится игрок.
          4) Выводим стены текущего сектора
          5) Рекурсивно бегаем через все порталы сектора, модифицируя портал на ходу и меняя текущий сектор (как зашли в сектор -выводим все его стены). При этом блокируем портал при заходе в него и разблокирует после выхода (через один портал можно смотреть по-разному, в зависимости от того, как мы в него пришли).

          Вот и всё.А вот картинка, как обрезаются порталы и стены.



          В OpenGL обрезать нужно только порталы — стены сами обрежутся OpenGL с помощью Z-буфера.

          Вроде всё. :)


        1. da-nie
          04.06.2017 21:27

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


        1. da-nie
          05.06.2017 13:54

          Попробовал конвертировать карту типа Wolf-3D. Получилось плохо и вот почему: если каждую клетку назначить сектором с порталами в 4 стороны (или меньше — со стороны блоков-стен нужно поставить стену, а не портал), то при рекурсивном обходе порталов в некоторых зонах начинаются тормоза (вплоть до FPS=0). Фишка в том, что раз сектора мелкие, обход через все порталы всех остальных порталов и секторов вырождается в здоровенную рекурсию. То есть, вот так втупую лабиринт из Wolf-3D с порталами не подружить. Нужно как-то объединять сектора в один большой сектор, не создавая порталов.


        1. da-nie
          05.06.2017 14:29

          Всё, понял в чём дело. В функции конвертации карты из Wold-3D создавался двойной набор порталов. Вот оно и дико тормозило. Исправил — и всё заработало на ура. :) Не такая уж там и большая рекурсия, даже без объединения квадратов.


          1. da-nie
            05.06.2017 14:37

            Конвертировал вот такой вот функцией в редакторе 3DEngineEditor. Карта задаётся в текстовом файле:'#'-стена.

            //---------------------------------------------------------------------------
            //импорт карты из текстового файла
            //---------------------------------------------------------------------------
            void CDocument_Map::ImportMapFromTXT(FILE *file,CDialog_SettingsWall *cDialog_SettingsWall_Ptr,CDialog_SettingsSector *cDialog_SettingsSector_Ptr)
            {
             long n;
             DeleteAll();
             //считываем текстовый файл в плоский массив (0-следующая строка)
             vector<unsigned char> vector_data;
             bool element=false;
             long x=0;
             long y=0;
             long max_x=0;
             while(1)
             {
              unsigned char b;
              if (fread(&b,sizeof(char),1,file)<=0) break;//файл закончился
              if (b<static_cast<unsigned char>(' '))
              {
               if (element==true)//если в строке что-то было считано, делаем перевод строки 
               {
                y++;
                vector_data.push_back(0);
               }
               element=false;
               if (x>max_x) max_x=x;
               x=0;//строка начинается заново
               continue;
              }
              element=true;
              x++;
              vector_data.push_back(b);
             }
             long max_y=y;
             //разворачиваем считанную карту в двумерный массив [max_y][max_x] и заполняем его точками
             vector< vector<unsigned char> > vector_map(max_y,vector<unsigned char>(max_x,'.'));
             x=0;
             y=0;
             long size=vector_data.size();
             for(n=0;n<size;n++)
             {
              if (vector_data[n]==0 || n==size-1)
              {
               y++;
               x=0;
               continue;
              }
              if (vector_data[n]==static_cast<unsigned char>('#')) vector_map[max_y-y-1][x]=static_cast<unsigned char>('#');//непустое поле
              x++;
             }
             vector_data.clear();
             //создаём сектора и сегменты по считанной карте
             CSector cSector_Create;//создаваемый сектор
             cSector_Create.vector_SSectorPoint.clear();
             cSector_Create.Select=false;
             cSector_Create.sSector_State=cDialog_SettingsSector_Ptr->GetState();
             CWall cWall_Create;//создаваемая стена
             cWall_Create.Frontier=false;
             cWall_Create.Select=false;
             cWall_Create.sWall_State=cDialog_SettingsWall_Ptr->GetState();
             for(y=0;y<max_y;y++)
             {
              for(x=0;x<max_x;x++)
              {
               unsigned char b=vector_map[y][x];
               if (b==static_cast<unsigned char>('.'))//сектор пустой
               {
                //создаём пустой сектор
                cSector_Create.vector_SSectorPoint.clear();
                //добавляем точки четырёхугольника, описывающего сектор
                SSectorPoint sSectorPoint[4];
                sSectorPoint[0].X=(x+1)*100;
                sSectorPoint[0].Y=(y+1)*100;
                sSectorPoint[1].X=(x+1)*100;
                sSectorPoint[1].Y=(y+0)*100;
                sSectorPoint[2].X=(x+0)*100;
                sSectorPoint[2].Y=(y+0)*100;
                sSectorPoint[3].X=(x+0)*100;
                sSectorPoint[3].Y=(y+1)*100;
            
                cSector_Create.vector_SSectorPoint.push_back(sSectorPoint[0]);
                cSector_Create.vector_SSectorPoint.push_back(sSectorPoint[1]);
                cSector_Create.vector_SSectorPoint.push_back(sSectorPoint[2]);
                cSector_Create.vector_SSectorPoint.push_back(sSectorPoint[3]);
                //добавляем сектор в список секторов
                vector_CSector.push_back(cSector_Create);
            	//если вокруг сектора есть стектора-стены, то добавляем стену, иначе добавляем линию раздела
            	for(n=0;n<4;n++)
            	{
                 long next_n=(n+1)%4;
                 cWall_Create.X1=sSectorPoint[n].X;
                 cWall_Create.Y1=sSectorPoint[n].Y;
                 cWall_Create.X2=sSectorPoint[next_n].X;
                 cWall_Create.Y2=sSectorPoint[next_n].Y;
                 cWall_Create.Frontier=true;//стена - линия раздела
            	 if (n==0)//правая стена
            	 {
                  if (x<max_x-1)
            	  {
                   if (vector_map[y][x+1]!=static_cast<unsigned char>('.')) cWall_Create.Frontier=false;//сектор справа - стена
            	  }
            	  else cWall_Create.Frontier=false;
            	 }
            
            	 if (n==1)//нижняя стена
            	 {
                  if (y>0)
            	  {
                   if (vector_map[y-1][x]!=static_cast<unsigned char>('.')) cWall_Create.Frontier=false;//сектор снизу - стена
                                                                       else continue;//не ставим портал, так как он уже есть с этой стороны
            	  }
            	  else cWall_Create.Frontier=false;
            	 }
            
            	 if (n==2)//левая стена
            	 {
                  if (x>0)
            	  {
                   if (vector_map[y][x-1]!=static_cast<unsigned char>('.')) cWall_Create.Frontier=false;//сектор слева - стена
            	                                                       else continue;//не ставим портал, так как он уже есть с этой стороны
            	  }
            	  else cWall_Create.Frontier=false;
            	 }
            
            	 if (n==3)//верхняя стена
            	 {
                  if (y<max_y-1)
            	  {
                   if (vector_map[y+1][x]!=static_cast<unsigned char>('.')) cWall_Create.Frontier=false;//сектор сверху - стена
            	  }
            	  else cWall_Create.Frontier=false;
            	 }
            	 //добавляем сегмент
                 vector_CWall.push_back(cWall_Create);
            	}
               }
              }
             }
            }
            


            1. Idot
              05.06.2017 14:49

              Круто! Впечатляет!


              1. da-nie
                05.06.2017 16:32

                Так это ж самая очевидная схема работы с порталами. :) Я сперва именно геометрический портал и сделал. А потом, много позже, сообразил, что можно по экрану отсекать в случае, когда стены не могут наклоняться влево-вправо. В общем, для Wolf-3D под OpenGL геометрический портал просто идеален. Только надо при выводе стен не выводить одну и ту же стенку дважды (делать отметку о том, что стена уже выведена) и выводить не фрагмент, видимый через портал, а всю стену сразу. Так же и с персонажами и прочими объектами — выводить только из тех секторов-квадратов, которые были выведены через портал и заодно можно проверить, что объекты вообще видны в портал (описать прямоугольник вида сверху вокруг объектов и сравнивать его попадание в портал).


  1. Wild_ButcheR
    07.06.2017 09:22

    Движок легендарный, спору нет. Я даже не во все игры успел на нем погонять.


    1. da-nie
      07.06.2017 11:28

      Это который от Сильвермана — Build. Это легендарный 100%. Shadow Warrion, Blood, Duke Nukem 3D.

      Но к этой статье приложен не он, а самодельный, гораздо менее мощный.
      И вот что меня удивляет. 20 лет, как вышел Duke 3D. Армия программистов. А движков самодельных (хоть с OpenGL, Direct 3D, хоть сотфверных) сделано очень мало. Как так-то? Вообще странно, столько программистов, а весь инет не завален их интересными программами (например, графическими). То ли они пишут только на работе, а дома нет, то ли ещё есть какая причина, но при таком количестве программистов их программ должно быть сильно дофига.


      1. msdos9
        07.06.2017 12:12

        Можно также спросить, где Бахи, Бетховены, Пушкины, Толстые и всякие другие Моцарты…


        1. da-nie
          07.06.2017 12:23

          А на этот вопрос какой ответ? :)