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

На рынке сейчас используются следующие технологии:


  1. 2-х проекторная система с линейной или круговой поляризацией. Главный минус – частое прогорание поляризационных фильтров.

  2. Стерео “с самодельным” эмиттером, где для синхронизации используется 3-din разъём профессиональных видеокарт nvidia. Главный минус – видеокарту с этим разъёмом сейчас практически не достать.

  3. Nvidia 3D vision, где стандартный эмиттер “взломан” и сигнал синхронизации передаётся на другой, ведь стандартный очень слабый и не стабильный на длинном проводе. Есть производители, которые могут ставить только 301 драйвер, так как дальше NVIDIA улучшила защиту. Но, например, мы решили этот вопрос принципиально по-другому, поэтому нам не страшны эти обновления защиты.

  4. RedPoint синхронизация на основе переходника на VGA кабеле. Где в каждом не чётном кадре вверху ставится маркер в виде красной точки, что бы переходник распознал, где кадр чётный, где нет. Основной минус – это VGA со всем вытекающим качеством картинки.

  5. Различные мультипроекторные решения на основе п.1, п.3 или моно.

И экраны тоже разные:


  1. Обычный прямоугольный экран различных пропорций (как правило, по максимуму для зала, реже придерживаясь 16:9 или 4:3).

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

  3. Цилиндрический экран с примерно равным расстоянием от любой точки по одной горизонтали до проектора(-ов).

  4. Различные экраны сложных форм: сферы, неровные стены музеев и т.д.

И стала задача, чтобы рендер работал на всех системах, ОС от win XP до 10 и т.д. Причём, чаще всего, это именно старое железо и windows XP. Написать сам рендер была не проблема, я до этого разрабатывал много крутых штук для ProgDVB, в том числе и его, но тут стала проблема сведения многопроекторных систем. Ведь практически невозможно повесить 2 разных проектора, заставив их светить в одну точку. Для этого раньше приходилось использовались специальные дорогие юстировочные платформы, которые надо было накручивать в течении долгого времени где-то под потолком в неудобной позе, и, так как это механика, то от любого мало-мальски сильного хлопка двери проекторы могли снова “разъехаться”.

Да и с однопроекторными системами тоже не всё так гладко. Сами проекторы хоть и умеют геометрию подстраивать, делают это слишком ступенчато.

Поэтому была взята простая сетка с настройки ТВ канала:




Которую мышкой можно исказить примерно таким вот образом:



Чтобы экране оба проектора начинали светить соответствующими пикселами в одну точку.
Но вручную свести 2 проектора на плоском экране не сложно. Наше же ПО работает и на более сложных системах. Взять, например, 6-ти проекционную систему с цилиндрическим экраном. И так как экран цилиндрический – для каждой из 6 частей “сетки” нужно не просто линейное искажение, а гораздо более сложный алгоритм, который вручную сделать крайне тяжело и долго.

image
Шестрипроекторная система

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



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

float cc=log(color)*kj;
float4 c2=rgb*exp(cc);
return c2;

Где kj – подбираемый каждый раз параметр, для каждой конкретной проекционной системы и экрана, который зависит, прежде всего, от того, на сколько чёрный цвет у проектора реально чёрный.

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



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

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

Подготовка данных
procedure TCam_Geometry_frm.CalcPixelRegion(x,y:integer);
var
   StartP:TPoint;
  I: Integer;
  J: Integer;
  StaPo,EnPo:integer;
begin
   StartP.X := x * InternalBitmap.Width  div Image1.Width;
   StartP.Y := y * InternalBitmap.Height  div Image1.Height;
   SetLength(CheckingMask,InternalBitmap.Height);
   for I := 0 to InternalBitmap.Height - 1 do
   begin
      SetLength(CheckingMask[i],InternalBitmap.Width);
      for J := 0 to InternalBitmap.Width-1 do
      begin
         CheckingMask[i][j].IsCheckPoint := false;
         CheckingMask[i][j].IsPointChecked := false;
         CheckingMask[i][j].typ := 0;
         CheckingMask[i][j].texX := -1;
         CheckingMask[i][j].texY := -1;
      end;
   end;
   SetLength(TempFireBuf,InternalBitmap.Width * InternalBitmap.Height * 4);
   StaPo := 0;
   EnPo := 1;
   TempFireBuf[0].XPos := StartP.X;
   TempFireBuf[0].YPos := StartP.Y;
   CheckingMask[StartP.Y][StartP.X].IsPointChecked := true;
   CheckingMask[StartP.Y][StartP.X].IsCheckPoint := true;
   while StaPo <> EnPo do
   begin
      if (abs(InternalPic.GetRED(TempFireBuf[StaPo].XPos, TempFireBuf[StaPo].YPos)-
      InternalPic.GetRED(TempFireBuf[TempFireBuf[StaPo].pripos].XPos, TempFireBuf[TempFireBuf[StaPo].pripos].YPos))<SpinEdit1.Value) and
         (abs(InternalPic.GetGreen(TempFireBuf[StaPo].XPos, TempFireBuf[StaPo].YPos)-InternalPic.GetGreen(TempFireBuf[TempFireBuf[StaPo].pripos].XPos, TempFireBuf[TempFireBuf[StaPo].pripos].YPos))<SpinEdit1.Value) and
         (abs(InternalPic.GetBlue(TempFireBuf[StaPo].XPos, TempFireBuf[StaPo].YPos)-InternalPic.GetBlue(TempFireBuf[TempFireBuf[StaPo].pripos].XPos, TempFireBuf[TempFireBuf[StaPo].pripos].YPos))<SpinEdit1.Value) then
      begin
         CheckingMask[TempFireBuf[StaPo].YPos][TempFireBuf[StaPo].XPos].IsCheckPoint := true;
        if TempFireBuf[StaPo].XPos > 0 then
        begin
           if not CheckingMask[TempFireBuf[StaPo].YPos][TempFireBuf[StaPo].XPos-1].IsPointChecked then
           begin
              TempFireBuf[EnPo].XPos := TempFireBuf[StaPo].XPos-1;
              TempFireBuf[EnPo].YPos := TempFireBuf[StaPo].YPos;
              TempFireBuf[EnPo].pripos := StaPo;
              CheckingMask[TempFireBuf[EnPo].YPos][TempFireBuf[EnPo].XPos].IsPointChecked := true;
              inc(EnPo);
           end;
        end;
        if TempFireBuf[StaPo].XPos < InternalBitmap.Width - 1 then
        begin
           if not CheckingMask[TempFireBuf[StaPo].YPos][TempFireBuf[StaPo].XPos+1].IsPointChecked then
           begin
              TempFireBuf[EnPo].XPos := TempFireBuf[StaPo].XPos+1;
              TempFireBuf[EnPo].YPos := TempFireBuf[StaPo].YPos;
              TempFireBuf[EnPo].pripos := StaPo;
              CheckingMask[TempFireBuf[EnPo].YPos][TempFireBuf[EnPo].XPos].IsPointChecked := true;
              inc(EnPo);
           end;
        end;
        if TempFireBuf[StaPo].YPos > 0 then
        begin
           if not CheckingMask[TempFireBuf[StaPo].YPos-1][TempFireBuf[StaPo].XPos].IsPointChecked then
           begin
              TempFireBuf[EnPo].XPos := TempFireBuf[StaPo].XPos;
              TempFireBuf[EnPo].YPos := TempFireBuf[StaPo].YPos-1;
              TempFireBuf[EnPo].pripos := StaPo;
              CheckingMask[TempFireBuf[EnPo].YPos][TempFireBuf[EnPo].XPos].IsPointChecked := true;
              inc(EnPo);
           end;
        end;
        if (TempFireBuf[StaPo].YPos < 5) or (TempFireBuf[StaPo].YPos < 5) then
        begin
           ShowMessage('Область выделения подошла опасно к краю. Пордолжение не возможно.');
           exit;
        end;

        if TempFireBuf[StaPo].YPos < InternalBitmap.Height - 1 then
        begin
           if not CheckingMask[TempFireBuf[StaPo].YPos+1][TempFireBuf[StaPo].XPos].IsPointChecked then
           begin
              TempFireBuf[EnPo].XPos := TempFireBuf[StaPo].XPos;
              TempFireBuf[EnPo].YPos := TempFireBuf[StaPo].YPos+1;
              TempFireBuf[EnPo].pripos := StaPo;
              CheckingMask[TempFireBuf[EnPo].YPos][TempFireBuf[EnPo].XPos].IsPointChecked := true;
              inc(EnPo);
           end;
        end;
      end;
      inc(StaPo);
   end;
   SetLength(TempFireBuf,0);
end;

Затем просто этот набор пикселей превращаем в регион, в котором далее и будем искать уже линии.
procedure TCam_Geometry_frm.CreateFrame;
var
   nn:array [1..10] of integer;
   i,j,k,l,tmp:integer;
   rasts:array [1..4]of extended;
   rad:extended;
begin
   for I := 1 to 10 do
      nn[i] := GetMinY(i);
   for k := 0 to 5 do
   for I := 11 to InternalPic.PicX - 1 do
   begin
      if (nn[1] > 0) and (nn[5] > 0) and (nn[10] > 0) and (abs(nn[10]-nn[1])< 7) then
      begin
          tmp := 0;
          for l := 1 to 10 do
             tmp := tmp + nn[l];
          tmp := tmp div 10;
          while nn[5] < tmp do begin
              CheckingMask[nn[5]][i-6].IsCheckPoint := false;
              inc(nn[5]);
          end;
          while nn[5] > tmp do begin
              CheckingMask[nn[5]][i-6].IsCheckPoint := true;
              dec(nn[5]);
          end;
      end;
      for j := 2 to 10 do
         nn[j-1] := nn[j];
      nn[10] := GetMinY(i);
   end;
   for I := 1 to 10 do
      nn[i] := GetMaxY(i);
   for k := 0 to 5 do
   for I := 11 to InternalPic.PicX - 1 do
   begin
      if (nn[1] > 0) and (nn[5] > 0) and (nn[10] > 0) and (abs(nn[10]-nn[1])< 7) then
      begin
          tmp := 0;
          for l := 1 to 10 do
             tmp := tmp + nn[l];
          tmp := tmp div 10;
          while nn[5] <= tmp do begin
              CheckingMask[nn[5]][i-6].IsCheckPoint := false;
              inc(nn[5]);
          end;
          while nn[5] > tmp do begin
              CheckingMask[nn[5]][i-6].IsCheckPoint := true;
              dec(nn[5]);
          end;
      end;
      for j := 2 to 10 do
         nn[j-1] := nn[j];
      nn[10] := GetMaxY(i);
   end;
   rasts[1] := 0;rasts[2] := 0;rasts[3] := 0;rasts[4] := 0;
   Center.X := 0;Center.Y := 0;
   k := 0;
   for I := 11 to InternalPic.PicY - 1 do
      for J := 11 to InternalPic.PicX - 1 do
         if CheckingMask[i][j].IsCheckPoint then
         begin
            Center.X := Center.X + J;
            Center.Y := Center.Y + I;
            inc(k);
         end;
   Center.X := Center.X div k;
   Center.Y := Center.Y div k;
   for I := 11 to InternalPic.PicY - 1 do
      for J := 11 to InternalPic.PicX - 1 do
      begin
         if CheckingMask[i][j].IsCheckPoint then
         begin
            rad := (J-Center.X)*(J-Center.X)+(I-Center.Y)*(I-Center.Y);
            if i < Center.Y then
            begin
               if j < Center.X then
               begin
                  if (rasts[1] < rad) then
                  begin
                     rasts[1] := rad;
                     X1Y1.X := J;
                     X1Y1.Y := I;
                  end;
               end
               else
               begin
                  if (rasts[2] < rad) then
                  begin
                     rasts[2] := rad;
                     X2Y1.X := J;
                     X2Y1.Y := I;
                  end;
               end;
            end
            else
            begin
               if j < Center.X then
               begin
                  if (rasts[3] < rad) then
                  begin
                     rasts[3] := rad;
                     X1Y2.X := J;
                     X1Y2.Y := I;
                  end;
               end
               else
               begin
                  if (rasts[4] < rad) then
                  begin
                     rasts[4] := rad;
                     X2Y2.X := J;
                     X2Y2.Y := I;
                  end;
               end;
            end;
         end;
      end;

   LeftSetkaSide.IsHorisontOnScreen := false;
   LeftSetkaSide.CoordVal := 0;
   LeftSetkaSide.IsHorisontVals := false;
   LeftSetkaSide.x[1] := X1Y1.X;
   LeftSetkaSide.y[1] := X1Y1.Y;
   LeftSetkaSide.x[2] := X1Y2.X;
   LeftSetkaSide.y[2] := X1Y2.Y;

   LeftSetkaSide.y[3] := (LeftSetkaSide.y[1]+LeftSetkaSide.y[2]) / 2;
   LeftSetkaSide.x[3] := GetMinX(Round(LeftSetkaSide.y[3]));

   LeftSetkaSide.y[4] := (LeftSetkaSide.y[1] + LeftSetkaSide.y[3]) / 2;
   LeftSetkaSide.x[4] := GetMinX(Round(LeftSetkaSide.y[4]));
   LeftSetkaSide.y[5] := (LeftSetkaSide.y[2] + LeftSetkaSide.y[3]) / 2;
   LeftSetkaSide.x[5] := GetMinX(Round(LeftSetkaSide.y[5]));

   RightSetkaSide.IsHorisontOnScreen := false;
   RightSetkaSide.CoordVal := 0;
   RightSetkaSide.IsHorisontVals := false;
   RightSetkaSide.x[1] := X2Y1.X;
   RightSetkaSide.y[1] := X2Y1.Y;
   RightSetkaSide.x[2] := X2Y2.X;
   RightSetkaSide.y[2] := X2Y2.Y;

   RightSetkaSide.y[3] := (RightSetkaSide.y[1]+RightSetkaSide.y[2]) / 2;
   RightSetkaSide.x[3] := GetMaxX(Round(RightSetkaSide.y[3]));

   RightSetkaSide.y[4] := (RightSetkaSide.y[1] + RightSetkaSide.y[3]) / 2;
   RightSetkaSide.x[4] := GetMaxX(Round(RightSetkaSide.y[4]));
   RightSetkaSide.y[5] := (RightSetkaSide.y[2] + RightSetkaSide.y[3]) / 2;
   RightSetkaSide.x[5] := GetMaxX(Round(RightSetkaSide.y[5]));

   UpSetkaSide.IsHorisontOnScreen := true;
   UpSetkaSide.CoordVal := 0;
   UpSetkaSide.IsHorisontVals := false;
   UpSetkaSide.x[1] := X1Y1.X;
   UpSetkaSide.y[1] := X1Y1.Y;
   UpSetkaSide.x[2] := X2Y1.X;
   UpSetkaSide.y[2] := X2Y1.Y;

   UpSetkaSide.x[3] := (UpSetkaSide.x[1]+UpSetkaSide.x[2]) / 2;
   UpSetkaSide.y[3] := GetMinY(Round(UpSetkaSide.x[3]));

   UpSetkaSide.x[4] := (UpSetkaSide.x[1]+UpSetkaSide.x[3]) / 2;
   UpSetkaSide.y[4] := GetMinY(Round(UpSetkaSide.x[4]));
   UpSetkaSide.x[5] := (UpSetkaSide.x[2]+UpSetkaSide.x[3]) / 2;
   UpSetkaSide.y[5] := GetMinY(Round(UpSetkaSide.x[5]));

   DownSetkaSide.IsHorisontOnScreen := true;
   DownSetkaSide.CoordVal := 0;
   DownSetkaSide.IsHorisontVals := false;
   DownSetkaSide.x[1] := X1Y2.X;
   DownSetkaSide.y[1] := X1Y2.Y;
   DownSetkaSide.x[2] := X2Y2.X;
   DownSetkaSide.y[2] := X2Y2.Y;

   DownSetkaSide.x[3] := (DownSetkaSide.x[1]+DownSetkaSide.x[2]) / 2;
   DownSetkaSide.y[3] := GetMaxY(Round(DownSetkaSide.x[3]));

   DownSetkaSide.x[4] := (DownSetkaSide.x[1]+DownSetkaSide.x[3]) / 2;
   DownSetkaSide.y[4] := GetMaxY(Round(DownSetkaSide.x[4]));
   DownSetkaSide.x[5] := (DownSetkaSide.x[2]+DownSetkaSide.x[3]) / 2;
   DownSetkaSide.y[5] := GetMaxY(Round(DownSetkaSide.x[5]));
end;


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

Ну а теперь просто запустим сопоставление.

Основной алгоритм обсчёта
procedure TCam_Geometry_frm.AddLograngeKoeffs(n:integer;byX:boolean;coord:integer);
var
  I, J: integer;
  possx,possy,ccou:integer;
  srX1,srY1:extended;
  lfid:integer;
  foundPoints:arrpo;
  Center:TPoint;
  Clct,Clct2,Clct3,last:TPoint;
  dy,sry,ddy,y:extended;
//  CheAr:array of array of boolean;
begin
   possx := 0;
   possy := 0;
   ccou := 0;
   SetLength(foundPoints,0);
   for I := 0 to Length(ProjSetka[n]) - 1 do
      for J := 0 to Length(ProjSetka[n][i]) - 1 do
      begin
         if (byX and (ProjSetka[n][i][j].ProjX = coord) and IsPossHere(n,j,i,byX,20, 20,srX1,srY1))or
            ((not byX) and (ProjSetka[n][i][j].ProjY = coord) and IsPossHere(n,j,i,byX,20, 20,srX1,srY1))then
         begin
            possx := possx + j;
            possy := possy + i;
            inc(ccou);
            SetLength(foundPoints,ccou);
            foundPoints[ccou-1].X := J;
            foundPoints[ccou-1].Y := I;
         end;
      end;
   if ccou < 10 then
   begin
      possx := -3;
      exit;
   end;

   possx := possx div ccou;
   possy := possy div ccou;
   Center.X := possx; Center.Y := possy;
   lfid := length(LograngeFuncs[n]);
   SetLength(LograngeFuncs[n],length(LograngeFuncs[n])+1);
   LograngeFuncs[n][lfid].IsHorisontOnScreen := false;
   LograngeFuncs[n][lfid].CoordVal := coord;
   LograngeFuncs[n][lfid].IsHorisontVals := byX;

   i := GetMinLengthFromArr(foundPoints,Center);
    if i < 0 then
    begin
       ShowMessage('Не нашли ни одной точки для интерполяции Лагранжа!');
       exit;
    end;
   IsPossHere(n,foundPoints[i].X,foundPoints[i].Y,byX,20, 20,srX1,srY1);
   LograngeFuncs[n][lfid].x[1] := srX1;
   LograngeFuncs[n][lfid].Y[1] := srY1;

   foundPoints[i].X := -1;

   i := GetMaxLengthFromArr(foundPoints,Center);
   IsPossHere(n,foundPoints[i].X,foundPoints[i].Y,byX,20, 20,srX1,srY1);
   LograngeFuncs[n][lfid].x[5] := srX1;
   LograngeFuncs[n][lfid].Y[5] := srY1;

   foundPoints[i].X := -1;
   Clct.X := round(srX1);
   Clct.Y := round(srY1);

   i := GetMaxLengthFromArr(foundPoints,Center);
   while abs(GetAngleFrom3Points(Center,Clct,foundPoints[i])) < Pi / 2 do
   begin
      foundPoints[i].X := -1;
      i := GetMaxLengthFromArr(foundPoints,Center);
      if i < 0 then
      begin
         ShowMessage('Не нашли точки для интерполяции Лагранжа!');
         exit;
      end;
   end;
   IsPossHere(n,foundPoints[i].X,foundPoints[i].Y,byX,20, 20,srX1,srY1);
   LograngeFuncs[n][lfid].x[4] := srX1;
   LograngeFuncs[n][lfid].Y[4] := srY1;
   Clct2.X := round(srX1);
   Clct2.Y := round(srY1);
   LograngeFuncs[n][lfid].x[2] := -1;
   LograngeFuncs[n][lfid].x[3] := -1;
   while (LograngeFuncs[n][lfid].x[2] < 0) or (LograngeFuncs[n][lfid].x[3] < 0) do
   begin
      i := GetNearestFromArr(foundPoints,Center,min(GetLengthBW2P(Center,Clct),GetLengthBW2P(Center,Clct2)) div 2);
      if LograngeFuncs[n][lfid].x[2] < 0 then
      begin
         IsPossHere(n,foundPoints[i].X,foundPoints[i].Y,byX,20, 20,srX1,srY1);
         LograngeFuncs[n][lfid].x[2] := srX1;
         LograngeFuncs[n][lfid].Y[2] := srY1;
         foundPoints[i].X := -1;
         Clct3.X := round(srX1);
         Clct3.Y := round(srY1);
      end
      else
      begin
         if i < 0 then
         begin
            LograngeFuncs[n][lfid].x[3] := last.X;
            LograngeFuncs[n][lfid].Y[3] := last.Y;
         end
         else
         if abs(GetAngleFrom3Points(Center,Clct3,foundPoints[i])) > Pi / 2 then
         begin
            IsPossHere(n,foundPoints[i].X,foundPoints[i].Y,byX,20, 20,srX1,srY1);
            LograngeFuncs[n][lfid].x[3] := srX1;
            LograngeFuncs[n][lfid].Y[3] := srY1;
         end;
      end;
      if i >= 0 then
      begin
         last := foundPoints[i];
         foundPoints[i].X := -1;
      end;
   end;
   if abs(LograngeFuncs[n][lfid].x[1]-LograngeFuncs[n][lfid].x[5]) > abs(LograngeFuncs[n][lfid].y[1]-LograngeFuncs[n][lfid].y[5]) then
   begin
      LograngeFuncs[n][lfid].IsHorisontOnScreen := true;
   end
   else
      LograngeFuncs[n][lfid].IsHorisontOnScreen := false;
   if LograngeFuncs[n][lfid].IsHorisontOnScreen then
   begin
      sry := 0;
      for I := 1 to 5 do
         sry := sry + LograngeFuncs[n][lfid].y[i];
      sry := sry / 5;
      dy := 0;
      for I := 1 to 5 do
         if dy < abs(sry - LograngeFuncs[n][lfid].y[i]) then
            dy := abs(sry - LograngeFuncs[n][lfid].y[i]);
      dy := dy * 3 + 5;
      for I := 10 to 1000 do
      begin
         y := CalcPointByPolinom(n,lfid,i,-1);
         if (y > 0) and(dy < abs(sry - y)) then
         begin
            SetLength(LograngeFuncs[n],length(LograngeFuncs[n])-1);
            exit;
         end;
      end;
   end
   else
   begin
      sry := 0;
      for I := 1 to 5 do
         sry := sry + LograngeFuncs[n][lfid].x[i];
      sry := sry / 5;
      dy := 0;
      for I := 1 to 5 do
         if dy < abs(sry - LograngeFuncs[n][lfid].x[i]) then
            dy := abs(sry - LograngeFuncs[n][lfid].x[i]);
      dy := dy * 3+5;
      for I := 10 to 1000 do
      begin
         y := CalcPointByPolinom(n,lfid,-1,i);
         if (y > 0) and(dy < abs(sry - y)) then
         begin
            SetLength(LograngeFuncs[n],length(LograngeFuncs[n])-1);
            exit;
         end;
      end;
   end;
end;

Применяется вот так:
procedure TCam_Geometry_frm.sButton3Click(Sender: TObject);
var
  I, couu: Integer;
  geom_frms:array of Tcam_geomery_lines_ouput_frm;
  j,l: Integer;
  k, pos: Integer;
begin
   if not sButton1.Enabled then begin FlagStop:=true;exit;end;
   FlagStop:=false;
   SetLength(geom_frms,g_MonitorsCount);
   SetLength(ProjSetka,g_MonitorsCount);
   SetLength(LograngeFuncs,g_MonitorsCount);
   for I := 0 to g_MonitorsCount-1 do
   begin
      geom_frms[i] := Tcam_geomery_lines_ouput_frm.Create(self);
      geom_frms[i].PosX := g_MonitorsSetup[i+1].ScreenPosition.x;
      geom_frms[i].PosY := g_MonitorsSetup[i+1].ScreenPosition.y;
      Application.ProcessMessages;
      SetLength(ProjSetka[i],length(CheckingMask));
      SetLength(LograngeFuncs[i],0);
      for J := 0 to length(CheckingMask)-1 do
      begin
         SetLength(ProjSetka[i][j],length(CheckingMask[j]));
         for k := 0 to length(CheckingMask[j]) - 1 do
         begin
            ProjSetka[i][j][k].ProjX := -1;
            ProjSetka[i][j][k].ProjY:= -1;
         end;
      end;
   end;
   sButton2.Enabled := false;
   sButton1.Enabled := false;
   sButton17.Enabled := false;
   sButton4.Enabled := false;
   sButton5.Enabled := false;

   for I := 0 to g_MonitorsCount-1 do
   begin
      geom_frms[i].Show;
      geom_frms[i].SetBlack;
   end;

     for L := 0 to 40 do
     begin
        Application.ProcessMessages;
        Sleep(20);
     end;

   GetBitmapFromCam(blackBitmap);
   InitPicBuffer(blackPic,blackBitmap.Width,blackBitmap.Height);
   CopyToPic(blackBitmap,0,0,blackPic);

   for I := 0 to g_MonitorsCount-1 do
   begin
     for L := 0 to 70 do
     begin
        Application.ProcessMessages;
        Sleep(20);
     end;

      GetBitmapFromCam(blackBitmap);
      CopyToPic(blackBitmap,0,0,blackPic);

      couu := 16;
      if FlagStop then break;
      for j := 0 to couu do
      begin
         pos := j*geom_frms[i].Width div couu;
         if pos < 4 then pos := 4;
         if pos >= geom_frms[i].Width - 4 then pos := geom_frms[i].Width - 4;

         geom_frms[i].PaintLine(pos,0,pos,geom_frms[i].Height);
         for L := 0 to 70 do
         begin
            Application.ProcessMessages;
            Sleep(20);
         end;
         if not SaveProjLineCoords(i,pos,-1) then FlagStop := true;
         AddLograngeKoeffs(i,true,pos);
         pos := j*geom_frms[i].Height div couu;
         if pos < 4 then pos := 4;
         if pos >= geom_frms[i].Height - 4 then pos := geom_frms[i].Height - 4;
         geom_frms[i].PaintLine(0,pos,geom_frms[i].Width,pos);
         for L := 0 to 70 do
         begin
            Application.ProcessMessages;
            Sleep(20);
         end;
         if not SaveProjLineCoords(i,-1,pos) then FlagStop := true;
         AddLograngeKoeffs(i,false,pos);
         if FlagStop then break;
      end;
      geom_frms[i].SetBlack;
//      geom_frms[i].hide;

      SaveProjSsetka(i);
   end;
   if not FlagStop then
      SetCaptSetkaWidthToOne;
   if not FlagStop then
      CreateProjSetka;

   for I := 0 to g_MonitorsCount-1 do
   begin
      geom_frms[i].Free;
   end;
   if not FlagStop then
      SaveGeometry;
   sButton2.Enabled := true;
   sButton1.Enabled := true;
   sButton17.Enabled := true;
   sButton4.Enabled := true;
   sButton5.Enabled := true;
end;


Всё. Каждый пиксель проектора (из тех, которые возможно) сопоставлен пикселю на экране.

Теперь можно насладиться результатом.



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

Другая часть ролика, где эффект 3D минимален и можно оценить именно сведение.



И ещё пара важных замечаний:

Во-первых, обязательно вывод на каждый проектор – это свой поток, со своим кэшем кадров и синхронизацией с vsync. Иначе у вас будет всё или тормозить или рвать картинку. Особенно если проекторов под 12.

Во-вторых, если вы растягиваете картинку 4:3 предположим к 16:9, но картинка мультяшная, и пропорции предметов не очень понятны, больших проблем не будет. Но если вы растянете на цилиндрический экран, всё будет вообще не в пропорции, так как там соотношения 21:9, 27:9 и т.д. Но если показывать в пропорции правильной, то останется крутить 10-12 роликов, которые создавались именно под такой экран, а про остальные забыть.

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

Ожидая вопрос про язык программирования, интерфейс написан на Delphi, весь рендер и управление платформами – на C++.

P.S.: Если тема 5D будет интересна, могу продолжить рассказ о различных протоколах различных платформ или об адаптации готовых unity роликов виртуальной реальности для этой отрасли. Или что-нибудь ещё интересное. В общем, жду комментариев/вопросов.
Поделиться с друзьями
-->

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


  1. qrck13
    18.07.2017 19:39
    +5

    > Итак. 5D – это прежде всего кинотеатр со стерео контентом.

    5D — это в первую очередь ложь маркетологов.


    1. akadone
      18.07.2017 22:34
      -1

      5D — это название данного вида кинотеатров у нас в России. По принципу 3D — это стерео картинка, плюс одно измерение — это несколько степеней свободы платформы (2-6), плюс ещё одно — это эффекты (1-7). Что в общем-то логично, так как они на голову превосходят большинство 4D кинотеатров запада и Китая.


      1. qrck13
        19.07.2017 00:24
        +1

        Вот и я говорю, что маркетологи — такие маркетологи (читай — наглые обманщики), смешивают пространственные измерения со степенями свободы и эффектами. Так можно обычное 2D кино со звуком назвать 3D, — а что, звук это ведь дополнительный «эффект», к 2D картинке.


        1. akadone
          19.07.2017 00:48

          А существует название лучше для данного типа кинотеатров? И так, что бы все поняли о чём речь?


          1. scronheim
            19.07.2017 06:58

            Вы думаете, когда человек видит надпись 5D кинотеатр, он сразу понимает, ага, «тут несколько степеней свободы платформы (2-6), плюс ещё одно — это эффекты (1-7)». Надо обязательно сходить


          1. qrck13
            19.07.2017 07:46
            +1

            5D как минимум не корректно с научной точки зрения, т.к. D указывает на пространственную размерность. Вполне можно было бы придумать какое-нибудь красивое название и без гонок за большей (ложной) цифрой. А тут как всегда — «у конкурента цифра больше, давайте сделаем 17D!!»


            1. kharlashkin
              19.07.2017 09:44

              Извините, не сдержался.

              29D


            1. akadone
              19.07.2017 20:32

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


              1. qrck13
                19.07.2017 22:55

                Ну хотя-бы «Рынок 3D кинотеатров с дополнительными эффектами» — и то было бы честнее и понятнее.


                1. akadone
                  20.07.2017 00:34

                  Почему-то у меня от этого названия сразу ассоциация с кинотеатрами с запахом и прочей дрянью. И не как не с кинотеатром из соседнего торгового центра, на котором написано 5D. Я один такой не правильный?


                  1. qrck13
                    20.07.2017 01:44

                    > Почему-то у меня от этого названия сразу ассоциация с кинотеатрами с запахом и прочей дрянью

                    А разве вы не тем-же самым занимаетесь?


  1. maaGames
    18.07.2017 19:46

    Поддержу предыдущего комментатора про «5D», но вот сама реализация крутая. Ещё представляю, какая там жара и шум от 3-12 проекторов.) Но эффект однозначно потрясный. Я б такое купил, если бы у меня был неограниченный бюджет…


  1. arozhankov
    18.07.2017 19:47
    +1

    5D = 5 dimension = 5 измерений, величин… чего у вас нет.
    Ваш рассказ и реализации интересны, но избавьте нас, пожалуйста, от дуро-маркетинговых изречений и определений.


  1. lorc
    18.07.2017 19:48

    А вы не пробовали решать проблему пропорций с помощью Content aware fill или чего-то подобного?
    Естественно, для видео придется доработать алгоритм, но может получится очень востребованный продукт.


    1. akadone
      18.07.2017 22:42

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


  1. JekaMas
    18.07.2017 19:51

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


    1. SADKO
      19.07.2017 12:33

      О, поверьте, для аттракциона это ещё вполне приличный код…


  1. ice2heart
    19.07.2017 09:18

    А можно видео где происходит подгонка, интересно посмотреть как это в реалтайме происходит.


    1. akadone
      20.07.2017 01:34

      1. ice2heart
        20.07.2017 08:22

        Не, норм, правда я видео лучше было ускорить. Раза в 4. Становится понятно как оно работает.


  1. SADKO
    19.07.2017 12:11

    Эээ, рынок «5D» — почти мёртв, если не считать попытки отдельных товарищей продавать апгрейды для замены экрана шлемами… Лет пять назад мы шутили про то, что нафиг нужна пневмо\масло\электро платформа, можно взять и таджиков которые будут её качать, и что теперь, примерно так и происходит, человека ставят на подвешенную на цепочках дощечку, одевают шлем, и оператор аттракциона его качает :-)

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

    Что до NVIDIA то её не нужно было ломать, всё улучшение защиты, ЕМНИП это просто с некоторых ножек убрали сигналы, что-бы производителем клонов брать их было не повадно, лично для меня, моих партнёров и покупателей их продуктов это не затронуло, ибо я предпологал что это рано или поздно случиться и парсил сигнал управления очками, ибо своих-то клиентов NVIDIA по любому не кинет… Те, кто делали тупо усилители NVIDIA тоже не остались в накладе…
    По поводу длинных проводов, это вообще песня, как люди дружат с законом Ома, после этого платформы с контроллерами на куче ардуин уже не разу не удивляют :-)

    А вот что удивляет до сих пор, так это тот факт что производители решений для обновления старых аттракционов, разучив «гениальные» протоколы платформ, написав свои рендеры итд, не дерзают управлять экспанд эмитером напрямую, через com\usb\lpt и сидят все как один на NVIDIA, хотя делов-то не сложнее чем иной платформой рулить.


    1. akadone
      20.07.2017 00:58

      Не расскажите каким образом можно софтово снять статус какой ракурс показывает проектор, и сколько времени осталось до переключения с HDMI/DVI не сильно ковыряясь в бинарниках драйверов? Про call-back я вообще молчу… Предположим, что с NVIDIA Вы пошутили, они 3D поддержку и телевизорам-то сделали со скрипом, если пирамидки нет. С ATI — проще. SDK доступен. Но там просто НЕТ статуса (по крайней мере не было 3 года назад, когда я это активно ковырял, может сейчас что изменилось).
      Очки nvidia — полное барахло. Дорогие, глючные, хрупкие. Пирамидка — тем более. Даже для домашнего кинотеатра не рекомендовал бы их, если можно достать альтернативу. Мы работаем исключительно с экспандами/вольфони, и естественно ни каких dlp. Так что просто усилитель NVIDIA — штука не сильно полезная. А вот преобразователь в экспандовский сигнал — это интереснее, хотя их до сих пор не купишь так просто в магазине.


      1. SADKO
        20.07.2017 19:42

        Не подскажу ибо предполагаю что такого способа до сих пор нет, а задержки для разных устройств у NVIDIA ЕМНИП были тупо прописаны таблицей в драйверах… По DVI никакой информации о ракурсе вроде бы не передавалось в принципе, по HDMI в зависимости от версии возможность есть, но проекторы на неё плюют, ну или плевали :-)
        Тем не менее мне некогда удавалось чисто программным способом заставить DLP-Link работать с нужного ракурса, хотя в этом и не было особого смысла, и я сильно не уверен что это работало-бы с несколькими проекторами.

        Про NVIDIA я не шучу, тк был свидетелем\участником тех событий, там было несколько итераций, в начале просто проверялось наличие устройства с VID\PID и эндпоинтами, потом они стали проверять что это всё таки cypres, его можно загрузить итд. Но разработка была универсальной и с ног цыпры можно было снимать левый\правый и что-то ещё уже и не помню. На радостях таких не далёкие китайцы начали делать свои пирамиды, а NVIDIA со злорадством за этим наблюдала, и потом с очередным обновлением дров, перестала выдавать сигналы на эти ноги в пирамидах (а в свистках перестали выдавать ИК управление) :-)
        Потом вроде-бы был ещё один эпизод, когда NVIDIA стали сыпать соль в ИК на которую не реагировали оригинальные очки, за-то в очередной раз пострадали клоны очков и пирамид…
        … в процессе и до этих событий у меня было много работы по исследованию и описанию различных протоколов, бытовых и профессиональных устройств, а внутренняя жаба заставила меня делать конвертор так, что бы он работал с минимумом навесных компонентов, от внутреннего тактового генератора, и алгоритм там был более сложен, чем простое ожидание конкретного сигнала, так что и соль мне пришлась нипочём.

        Да, оригинальные пирамиды\очки были не сахар, по многим причинам, прожорливая начинка, питание от USB и закон Ома опять-же :-) Клоны были в разы лучше, один из них я даже немного по пиарил на хабре
        Но сама идея была здравая, NVIDIA первая в мире предложила «3D драйвера» из коробки, и это было прикольно.

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

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


  1. akadone
    21.07.2017 04:43

    Да-да… палмексы мы тоже продавали… Но это было совсем давно. 11-12 годы наверно… У них слишком много минусов… А NVIDIA была первой с видюхой с 3 дин, и всё. больше ни какого новаторства с их стороны не было. Они лишь догоняли и ограничивали пользователей пользуясь монополизмом. Как-то нам надо было у заказчика повесить пирамидку нвидиа на примерно 8 метрах от компьютера (радиальный экран, 3 проектора в 3DVISION объеденённых). Мы в городе скупили все самые качественные удлинители из доступных во всех компьютерных магазинах. Ни один не завелеся нормально. Точнее завелись 2 дорогих, но на 3 включениях из 5 пирамидка банально не видилась. Мы пожали плечами, и больше с этими пирамидками не связываемся.
    По вашему вопросу — если в ОС будет доступен на легальных основаниях флаг какой ракурс выводится в текущий момент на экране, на любом из доступных драйверов линейки производителя, принципов вывода и пр. — думаю, что мы очень быстро сможем предоставить коммерческий продукт (всеядный 3д плеер или что-то подобное) для подобного железа. Но пока это только мечты. ATI пытались подобное сделать, но по середине проекта их кто-то или что-то остановило. Из-за этого качественное 3Д стоит до сих пор баснословно дорого. Вот если бы не надо было покупать ерунду от мёртворожденных (NVIDIA) то для маленьких партий комплект (эмиттор + качественные очки 4 шт) обходились бы не более примерно 8тыр, а на больших партиях — и ещё бы гораздо меньше. Но это лишь мечты, и закрытые протоколы.
    В теории качественным 3д можно сейчас насладиться только в геар вр или окулус. Но это при условии, что сцена не слишком динамична, так как в противном случае вас будет ожидать разочарование: вместо качественной картинки — размытые образы того, что вы видели в последние 0.5-0.8 сек — для того, что бы вас не укачало. Так что эта ерунда тоже не даст насладиться качественной картинкой 3d картинкой с вылетами… Так что увы. Нам остаются лишь доступны всякие xD кинотеатры для наслаждения полноценным 3D.