Статьи

Написать 3D Soft Engine с нуля: Часть 4

В предыдущем уроке, часть 3 , мы загрузили файл JSON, в котором наши сетки были сериализованы из Blender. До сих пор наша функция рендеринга рисовала сетки только с помощью простого каркасного рендеринга. Теперь мы посмотрим, как заполнить треугольники с помощью алгоритма растеризации . Затем мы увидим, как работать с Z-буфером, чтобы лица, живущие сзади, не рисовались сверху на передних гранях.

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

растеризации

Существует множество различных типов алгоритмов растеризации. Я даже знаю кого-то в моей команде, кто создал собственный запатентованный алгоритм растеризации для известного производителя GPU. Также благодаря ему я теперь знаю, что такое Бустрофедон , и с тех пор он действительно изменил мою жизнь. ?

Если быть более серьезным, мы собираемся реализовать в этом руководстве простой, но эффективный алгоритм растеризации. Поскольку мы работаем на CPU с нашим программным 3D-движком, мы должны уделить много внимания этой части. Действительно, это будет стоить нам много ресурсов процессора. Сегодня, конечно, эта тяжелая часть выполняется непосредственно графическими процессорами.

Давайте начнем с упражнения. Возьмите лист бумаги и начните рисовать все типы треугольников, о которых вы только могли подумать. Идея состоит в том, чтобы найти общий способ рисования треугольников любого типа .

Если мы сортируем три вершины каждого треугольника по координатам Y, чтобы всегда иметь P1, за которым следует P2, а затем P3, у нас будет только 2 возможных случая:

образ

Затем вы видите, что у нас есть 2 случая: P2 справа от P1P3 или P2 слева от P1P3 . В нашем случае, так как мы хотим всегда рисовать наши линии слева направо от sx до ex, у нас будет первый условный IF для обработки этих двух случаев.

Кроме того, мы будем рисовать слева направо, двигаясь вниз от P1.Y к P3.Y, следуя красной линии, нарисованной на левой стороне фигуры. Но нам нужно изменить нашу логику, достигнув P2.Y, так как наклон будет изменяться в обоих случаях. Вот почему у нас есть 2 шага в процессе сканирования линии . Двигаясь вниз от P1.Y к P2.Y, а затем от P2.Y к P3.Y , наш конечный пункт назначения.

Вся логика, необходимая для понимания того, как построить наш алгоритм, описана в Википедии: http://en.wikipedia.org/wiki/Slope . Это действительно какая-то базовая математика.

Чтобы иметь возможность сортировать случаи между случаем 1 и случаем 2, вам просто нужно вычислить обратные наклоны следующим образом:

dP1P2 = P2.X — P1.X / P2.Y — P1.Y и dP1P3 = P3.X — P1.X / P3.Y — P1.Y

Если dP1P2> dP1P3, то мы в первом случае с P2 справа, в противном случае, если dP1P2> dP1P2, мы во втором случае с P2 слева.

Теперь, когда у нас есть основная логика нашего алгоритма, нам нужно знать, как вычислять X в каждой строке между SX (начало X) и EX (конец X) на моем рисунке. Поэтому нам нужно сначала вычислить SX & EX. Поскольку мы знаем значение Y и наклон P1P3 & P1P2, мы можем легко найти интересующие нас SX & EX.

Давайте возьмем шаг 1 случая 1 в качестве примера. Первым шагом является вычисление нашего градиента с текущим значением Y в нашем цикле. Он скажет нам, на каком этапе мы находимся в обработке строки сканирования между P1.Y и P2.Y на шаге 1.

градиент = текущий Y — P1.Y / P2.Y — P1.Y

Поскольку X и Y линейно связаны, мы можем интерполировать SX на основе этого градиента, используя P1.X и P3.X, и интерполировать EX, используя P1.X и P2.X.

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

Если это все еще недостаточно ясно, вот другие интересные статьи для чтения, посвященные также растеризации:

3D-рендеринг программного обеспечения — Часть I
Растеризация треугольника
Алгоритмы растеризации программного обеспечения для заполнения треугольников

Теперь, когда мы описали наш алгоритм. Давайте теперь поработаем над кодом. Начните с удаления drawLine и drawBline из класса устройств. Затем замените ваши существующие функции / методы следующими:

  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  общедоступный проект Vector3 ( Vector3ordin , Matrix TransMat) 
     // преобразование координат 
     var point = Vector3 .TransformCoordinate (ordinate, TransMat); 
     // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
     var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f; 
     var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f; 
     return ( новый Vector3 (x, y, point.Z)); 
  // DrawPoint вызывает PutPixel, но выполняет операцию отсечения до 
  публичный void DrawPoint ( точка Vector2, цвет Color4 ) 
     // Отсечение того, что видно на экране 
     if (point.X> = 0 && point.Y> = 0 && point.X <bmp.PixelWidth && point.Y <bmp.PixelHeight) 
     { 
         // Рисуем точку 
         PutPixel (( int ) point.X, ( int ) point.Y, color); 
     } 
 

  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  общественный проект (координаты: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector3 { 
     // преобразование координат 
     var point = BABYLON.Vector3.TransformCoordinates (координировать, transMat); 
     // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно снова преобразовать их так, чтобы x: 0, y: 0 слева вверху. 
     var x = point.x * this .workingWidth + this .workingWidth / 2.0; 
     var y = -point.y * this .workingHeight + this .workingHeight / 2.0; 
     return ( новый BABYLON.Vector3 (x, y, point.z)); 
  // drawPoint вызывает putPixel, но выполняет операцию отсечения до 
  public drawPoint (точка: BABYLON.Vector2, цвет: BABYLON.Color4): void { 
     // Отсечение того, что видно на экране 
     if (point.x> = 0 && point.y> = 0 && point.x < this .workingWidth && point.y < this .workingHeight) { 
         // Рисуем желтую точку 
         this .putPixel (point.x, point.y, color); 
     } 
 

Мы просто готовим материал для второй части этого урока. Теперь вот самая важная часть. Вот логика, которая будет рисовать треугольники на основе предыдущих объяснений.

  // Зажимаем значения, чтобы держать их между 0 и 1 
  зажим с плавающей точкой ( значение с плавающей точкой, минимум с плавающей точкой = 0, максимум с плавающей точкой = 1) 
     вернуть Math .Max (min, Math .Min (значение, max)); 
  // интерполируем значение между 2 вершинами  
  / мин начальная точка, макс конечная точка 
  / и градиент% между 2 точками 
  Float Interpolate ( плавание мин, плавание макс, градиент плавания ) 
     возврат min + (max - min) * Зажим (градиент); 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  void ProcessScanLine ( int , Vector3 pa, Vector3 pb, Vector3 pc, Vector3 pd, Color4 color) 
     // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между 
// если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 вар градиент1 = pa.Y! = pb.Y? (y - pa.Y) / (pb.Y - pa.Y): 1; вар градиент2 = стр.Y! = pd.Y? (y - стр. Y) / (pd.Y - pc.Y): 1; int sx = ( int ) интерполировать (pa.X, pb.X, градиент1); int ex = ( int ) интерполировать (pc.X, pd.X, градиент2); // рисуем линию слева (sx) вправо (ex) для ( var x = sx; x <ex; x ++) { DrawPoint ( новый Vector2 (x, y), цвет); } публичный void DrawTriangle ( Vector3 p1, Vector3 p2, Vector3 p3, Color4 color) // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) // затем p2 между p1 и p3 если (p1.Y> p2.Y) { var temp = p2; p2 = p1; р1 = темп; } если (p2.Y> p3.Y) { var temp = p2; р2 = р3; р3 = температура; } если (p1.Y> p2.Y) { var temp = p2; p2 = p1; р1 = темп; } // обратные склоны поплавок dP1P2, dP1P3; // http://en.wikipedia.org/wiki/Slope // Вычисление обратных уклонов если (p2.Y - p1.Y> 0) dP1P2 = (p2.X - p1.X) / (p2.Y - p1.Y); еще dP1P2 = 0; если (p3.Y - p1.Y> 0) dP1P3 = (p3.X - p1.X) / (p3.Y - p1.Y); еще dP1P3 = 0; // Первый случай, когда треугольники такие: // P1 // - // - // - - // - - // - - P2 // - - // - - // - // P3 если (dP1P2> dP1P3) { для ( var y = ( int ) p1.Y; y <= ( int ) p3.Y; y ++) { если (у <p2.Y) { ProcessScanLine (y, p1, p3, p1, p2, color); } еще { ProcessScanLine (y, p1, p3, p2, p3, color); } } } // Первый случай, когда треугольники такие: // P1 // - // - // - - // - - // P2 - - // - - // - - // - // P3 еще { для ( var y = ( int ) p1.Y; y <= ( int ) p3.Y; y ++) { если (у <p2.Y) { ProcessScanLine (y, p1, p2, p1, p3, color); } еще { ProcessScanLine (y, p2, p3, p1, p3, color); } } }

  // Зажимаем значения, чтобы держать их между 0 и 1 
  открытый зажим (значение: число , мин: число = 0, макс: число = 1): число { 
     вернуть Math.max (мин, Math.min (значение, макс)); 
  // интерполируем значение между 2 вершинами  
  / мин начальная точка, макс конечная точка 
  / и градиент% между 2 точками 
  публичная интерполяция (min: число , max: число , градиент: число ) { 
     вернуть min + (max - min) * этот .clamp (градиент); 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  public processScanLine (y: номер , pa: BABYLON.Vector3, pb: BABYLON.Vector3, 
pc: BABYLON.Vector3, pd: BABYLON.Vector3, цвет: BABYLON.Color4): void { // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как // начальный X (sx) и конечный X (ex) для рисования между
// если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 var градиент1 = pa.y! = pb.y? (y - pa.y) / (pb.y - pa.y): 1; var градиент2 = pc.y! = pd.y? (y - pc.y) / (pd.y - pc.y): 1; var sx = this .interpolate (pa.x, pb.x, градиент1) >> 0; var ex = this .interpolate (pc.x, pd.x, градиент2) >> 0; // рисуем линию слева (sx) вправо (ex) для ( var x = sx; x <ex; x ++) { this .drawPoint ( новый BABYLON.Vector2 (x, y), цвет); } public drawTriangle (p1: BABYLON.Vector3, p2: BABYLON.Vector3,
p3: BABYLON.Vector3, цвет: BABYLON.Color4): void { // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) // затем p2 между p1 и p3 if (p1.y> p2.y) { var temp = p2; p2 = p1; р1 = темп; } if (p2.y> p3.y) { var temp = p2; р2 = р3; р3 = температура; } if (p1.y> p2.y) { var temp = p2; p2 = p1; р1 = темп; } // обратные склоны var dP1P2: число ; var dP1P3: число ; // http://en.wikipedia.org/wiki/Slope // Вычисление уклонов if (p2.y - p1.y> 0) dP1P2 = (p2.x - p1.x) / (p2.y - p1.y); еще dP1P2 = 0; если (p3.y - p1.y> 0) dP1P3 = (p3.x - p1.x) / (p3.y - p1.y); еще dP1P3 = 0; // Первый случай, когда треугольники такие: // P1 // - // - // - - // - - // - - P2 // - - // - - // - // P3 если (dP1P2> dP1P3) { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { if (y <p2.y) { this .processScanLine (y, p1, p3, p1, p2, color); } еще { это .processScanLine (y, p1, p3, p2, p3, color); } } } // Первый случай, когда треугольники такие: // P1 // - // - // - - // - - // P2 - - // - - // - - // - // P3 еще { for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { if (y <p2.y) { this .processScanLine (y, p1, p2, p1, p3, color); } еще { это .processScanLine (y, p2, p3, p1, p3, color); } } }

  // Зажимаем значения, чтобы держать их между 0 и 1 
  Device.prototype.clamp = function (value, min, max) { 
     if ( typeof min === "undefined" ) {min = 0;  } 
     if ( typeof max === "undefined" ) {max = 1;  } 
     вернуть Math.max (мин, Math.min (значение, макс)); 
  ; 
  // интерполируем значение между 2 вершинами  
  / мин начальная точка, макс конечная точка 
  / и градиент% между 2 точками 
  Device.prototype.interpolate = функция (мин, макс, градиент) { 
     вернуть min + (max - min) * этот .clamp (градиент); 
  ; 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  Device.prototype.processScanLine = function (y, pa, pb, pc, pd, color) { 
     // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между     
     // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 
     var градиент1 = pa.y! = pb.y?  (y - pa.y) / (pb.y - pa.y): 1; 
     var градиент2 = pc.y! = pd.y?  (y - pc.y) / (pd.y - pc.y): 1; 
      var sx = this .interpolate (pa.x, pb.x, градиент1) >> 0; 
     var ex = this .interpolate (pc.x, pd.x, градиент2) >> 0; 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) { 
         this .drawPoint ( новый BABYLON.Vector2 (x, y), цвет); 
     } 
  ; 
  Device.prototype.drawTriangle = function (p1, p2, p3, color) { 
     // Сортировка точек, чтобы всегда иметь этот порядок на экране p1, p2 & p3 
     // с p1 всегда вверх (таким образом, Y имеет наименьший возможный, чтобы быть около верхнего экрана) 
     // затем p2 между p1 и p3 
     if (p1.y> p2.y) { 
         var temp = p2; 
         p2 = p1; 
         р1 = темп; 
     } 
     if (p2.y> p3.y) { 
         var temp = p2; 
         р2 = р3; 
         р3 = температура; 
     } 
     if (p1.y> p2.y) { 
         var temp = p2; 
         p2 = p1; 
         р1 = темп; 
     } 
      // обратные склоны 
     var dP1P2;  var dP1P3; 
      // http://en.wikipedia.org/wiki/Slope 
     // Вычисление уклонов 
     if (p2.y - p1.y> 0) { 
         dP1P2 = (p2.x - p1.x) / (p2.y - p1.y); 
     } еще { 
         dP1P2 = 0; 
     } 
      if (p3.y - p1.y> 0) { 
         dP1P3 = (p3.x - p1.x) / (p3.y - p1.y); 
     } еще { 
         dP1P3 = 0; 
     } 
      // Первый случай, когда треугольники такие: 
     // P1 
     // - 
     // -  
     // - - 
     // - - 
     // - - P2 
     // - - 
     // - - 
     // - 
     // P3 
     если (dP1P2> dP1P3) { 
         for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { 
             if (y <p2.y) { 
                 this .processScanLine (y, p1, p3, p1, p2, color); 
             } еще { 
                 это .processScanLine (y, p1, p3, p2, p3, color); 
             } 
         } 
     } 
     // Первый случай, когда треугольники такие: 
     // P1 
     // - 
     // -  
     // - - 
     // - - 
     // P2 - -  
     // - - 
     // - - 
     // - 
     // P3 
     еще { 
         for ( var y = p1.y >> 0; y <= p3.y >> 0; y ++) { 
             if (y <p2.y) { 
                 this .processScanLine (y, p1, p2, p1, p3, color); 
             } еще { 
                 это .processScanLine (y, p2, p3, p1, p3, color); 
             } 
         } 
     } 
  ; 

В коде вы видите, как мы обрабатываем 2 типа треугольников для заполнения, а также 2 шага в процессе линии сканирования.

Наконец, вам нужно обновить функцию рендеринга, чтобы она вызывала drawTriangle вместо 3 вызовов drawLine / drawBline . Мы также используем уровень серого, чтобы нарисовать каждый треугольник. В противном случае, если мы нарисуем каждый из них одним и тем же цветом, мы не сможем реально увидеть, что происходит. В следующем уроке мы увидим, как правильно обращаться со светом.

  var faceIndex = 0; 
  foreach ( переменное лицо в сетке. Лица) 
     var vertexA = mesh.Vertices [face.A]; 
     var vertexB = mesh.Vertices [face.B]; 
     var vertexC = mesh.Vertices [face.C]; 
      var pixelA = Project (vertexA, transformMatrix); 
     var pixelB = Project (vertexB, transformMatrix); 
     var pixelC = Project (vertexC, transformMatrix); 
      var color = 0.25f + (faceIndex% mesh.Faces.Length) * 0.75f ​​/ mesh.Faces.Length; 
     DrawTriangle (pixelA, pixelB, pixelC, новый Color4 (цвет, цвет, цвет, 1)); 
     faceIndex ++; 
 

И у вас должен быть этот первый результат:

Что там происходит не так? У вас, вероятно, есть ощущение, что вы можете смотреть через сетку. Это потому, что мы рисуем все треугольники, не «пряча» треугольники, живущие сзади.

Z-буфер или как использовать буфер глубины

Затем нам нужно проверить значение Z текущего пикселя и сравнить его с буфером перед его прорисовкой . Если Z текущего пикселя для рисования ниже, чем предыдущий пиксель, который был нарисован здесь, мы можем переопределить его. Действительно, это будет означать, что текущая грань, которую мы рисуем, находится перед ранее нарисованной гранью. Однако, если Z текущего пикселя для рисования больше, чем предыдущий пиксель, нарисованный здесь, мы можем отменить операцию рисования.

Затем нам нужно сохранить историю этих Z-индексов на пиксель на экране . Чтобы сделать это, объявите новый массив с плавающей точкой, назвал его deepBuffer . Его размер будет равен количеству пикселей на экране (ширина * высота). Этот буфер глубины должен быть инициализирован во время каждой операции clear () с очень высоким значением Z по умолчанию.

В функции / методе putPixel нам просто нужно проверить индекс Z пикселя по сравнению с тем, который был сохранен в буфере глубины. Более того, часть нашей предыдущей логики возвращала Vector2 для логического рисования на экране. Мы собираемся изменить его на Vector3, чтобы выдвинуть значения Z вершин, поскольку теперь нам нужна эта информация, чтобы правильно рисовать грани.

Наконец, так же, как мы интерполировали значение X между каждой стороной треугольников, нам нужно также интерполировать значения Z, используя тот же алгоритм для каждого пикселя.

В заключение вот код, который необходимо обновить в вашем объекте Device:

  закрытый байт [] backBuffer; 
  private readonly float [] deepBuffer; 
  private WriteableBitmap bmp; 
  private readonly int renderWidth; 
  private readonly int renderHeight; 
  Публичное устройство ( WriteableBitmap BMP) 
     это .bmp = bmp; 
     renderWidth = bmp.PixelWidth; 
     renderHeight = bmp.PixelHeight; 
      // размер заднего буфера равен количеству пикселей для рисования 
     // на экране (ширина * высота) * 4 (значения R, G, B и Alpha).  
     backBuffer = новый байт [bmp.PixelWidth * bmp.PixelHeight * 4]; 
     deepBuffer = new float [bmp.PixelWidth * bmp.PixelHeight]; 
  // Этот метод вызывается для очистки заднего буфера с определенным цветом 
  public void Clear ( байт r, байт g, байт b, байт a) { 
     // Очистка обратного буфера 
     for ( var index = 0; index <backBuffer.Length; index + = 4) 
     { 
         // BGRA используется Windows вместо RGBA в HTML5 
         backBuffer [index] = b; 
         backBuffer [index + 1] = g; 
         backBuffer [index + 2] = r; 
         backBuffer [index + 3] = a; 
     } 
      // Очистка буфера глубины 
     for ( var index = 0; index <deepBuffer.Length; index ++) 
     { 
         deepBuffer [index] = float .MaxValue; 
     } 
  // Вызывается поставить пиксель на экран с определенными координатами X, Y 
  public void PutPixel ( int x, int y, float z, Color4 color) 
     // Поскольку у нас есть 1-D массив для нашего заднего буфера 
     // нам нужно знать эквивалентную ячейку в 1-D 
     // в 2D координатах на экране 
     var index = (x + y * renderWidth); 
     var index4 = index * 4; 
      if (deepBuffer [index] <z) 
     { 
         возврат ;  // Сброс 
     } 
      deepBuffer [index] = z; 
      backBuffer [index4] = ( byte ) (color.Blue * 255); 
     backBuffer [index4 + 1] = ( байт ) (color.Green * 255); 
     backBuffer [index4 + 2] = ( байт ) (color.Red * 255); 
     backBuffer [index4 + 3] = ( байт ) (color.Alpha * 255); 
  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  общедоступный проект Vector3 ( Vector3ordin , Matrix TransMat) 
     // преобразование координат 
     var point = Vector3 .TransformCoordinate (ordinate, TransMat); 
     // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно преобразовать их снова, чтобы в верхнем левом углу были значения x: 0, y: 0. 
     var x = point.X * bmp.PixelWidth + bmp.PixelWidth / 2.0f; 
     var y = -point.Y * bmp.PixelHeight + bmp.PixelHeight / 2.0f; 
     return ( новый Vector3 (x, y, point.Z)); 
  // DrawPoint вызывает PutPixel, но выполняет операцию отсечения до 
  общественная пустота DrawPoint ( точка Vector3, цвет Color4 ) 
     // Отсечение того, что видно на экране 
     if (point.X> = 0 && point.Y> = 0 && point.X <bmp.PixelWidth && point.Y <bmp.PixelHeight) 
     { 
         // Рисуем точку 
         PutPixel (( int ) point.X, ( int ) point.Y, point.Z, color); 
     } 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  void ProcessScanLine ( int , Vector3 pa, Vector3 pb, Vector3 pc, Vector3 pd, Color4 color) 
     // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между 
     // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 
     вар градиент1 = pa.Y! = pb.Y?  (y - pa.Y) / (pb.Y - pa.Y): 1; 
     вар градиент2 = стр.Y! = pd.Y?  (y - стр. Y) / (pd.Y - pc.Y): 1; 
      int sx = ( int ) интерполировать (pa.X, pb.X, градиент1); 
     int ex = ( int ) интерполировать (pc.X, pd.X, градиент2); 
      // начинаем Z и заканчиваем Z 
     float z1 = интерполяция (pa.Z, pb.Z, градиент1); 
     float z2 = интерполяция (pc.Z, pd.Z, градиент2); 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) 
     { 
         градиент плавания = (x - sx) / ( плавание ) (ex - sx); 
          var z = интерполировать (z1, z2, градиент); 
         DrawPoint ( новый Vector3 (x, y, z), цвет); 
     } 
 

  // размер заднего буфера равен количеству пикселей для рисования 
  / на экране (ширина * высота) * 4 (значения R, G, B и Alpha).  
  приватный буфер: ImageData; 
  частная рабочая Canvas: HTMLCanvasElement; 
  приватный рабочийContext: CanvasRenderingContext2D; 
  частная рабочаяШирина: число ; 
  частный рабочийВысота: номер ; 
  // равно backbuffer.data 
  приватные backbufferdata; 
  приватный глубинный буфер: число []; 
  конструктор (canvas: HTMLCanvasElement) { 
     this .workingCanvas = canvas; 
     this .workingWidth = canvas.width; 
     this .workingHeight = canvas.height; 
     this .workingContext = this .workingCanvas.getContext ( "2d" ); 
     this .depthbuffer = new Array ( this .workingWidth * this .workingHeight); 
  // Эта функция вызывается для очистки заднего буфера с определенным цветом 
  public clear (): void { 
     // Очистка с черным цветом по умолчанию 
     this .workingContext.clearRect (0, 0, this .workingWidth, this .workingHeight); 
     // после очистки с черными пикселями мы возвращаем связанные данные изображения в  
     // очищаем задний буфер 
     this .backbuffer = this .workingContext.getImageData (0, 0, this .workingWidth, this .workingHeight); 
      // Очистка буфера глубины 
     for ( var i = 0; i < this .depthbuffer.length; i ++) { 
         // Максимально возможное значение  
         this .depthbuffer [i] = 10000000; 
     } 
  // Вызывается поставить пиксель на экран с определенными координатами X, Y 
  public putPixel (x: число , y: число , z: число , цвет: BABYLON.Color4): void { 
     this .backbufferdata = this .backbuffer.data; 
     // Поскольку у нас есть 1-D массив для нашего заднего буфера 
     // нам нужно знать эквивалентный индекс ячейки в 1-D 
     // на 2D координатах экрана 
     var index: number = ((x >> 0) + (y >> 0) * this .workingWidth); 
     var index4: число = индекс * 4; 
      if ( this .depthbuffer [index] <z) { 
         возврат ;  // Сброс 
     } 
      this .depthbuffer [index] = z; 
      // Цветовое пространство RGBA используется холстом HTML5  
     this .backbufferdata [index4] = color.r * 255; 
     это .backbufferdata [index4 + 1] = color.g * 255; 
     this .backbufferdata [index4 + 2] = color.b * 255; 
     this .backbufferdata [index4 + 3] = color.a * 255; 
  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  общественный проект (координаты: BABYLON.Vector3, transMat: BABYLON.Matrix): BABYLON.Vector3 { 
     // преобразование координат 
     var point = BABYLON.Vector3.TransformCoordinates (координировать, transMat); 
     // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно снова преобразовать их так, чтобы x: 0, y: 0 слева вверху. 
     var x = point.x * this .workingWidth + this .workingWidth / 2.0; 
     var y = -point.y * this .workingHeight + this .workingHeight / 2.0; 
     return ( новый BABYLON.Vector3 (x, y, point.z)); 
  // drawPoint вызывает putPixel, но выполняет операцию отсечения до 
  public drawPoint (точка: BABYLON.Vector3, цвет: BABYLON.Color4): void { 
     // Отсечение того, что видно на экране 
     if (point.x> = 0 && point.y> = 0 && point.x < this .workingWidth && point.y < this .workingHeight) { 
         // Рисуем желтую точку 
         this .putPixel (point.x, point.y, point.z, color); 
     } 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  public processScanLine (y: номер , pa: BABYLON.Vector3, pb: BABYLON.Vector3, pc: BABYLON.Vector3, pd: BABYLON.Vector3, цвет: BABYLON.Color4): void { 
     // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между 
     // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 
     var градиент1 = pa.y! = pb.y?  (y - pa.y) / (pb.y - pa.y): 1; 
     var градиент2 = pc.y! = pd.y?  (y - pc.y) / (pd.y - pc.y): 1; 
      var sx = this .interpolate (pa.x, pb.x, градиент1) >> 0; 
     var ex = this .interpolate (pc.x, pd.x, градиент2) >> 0; 
      // начинаем Z и заканчиваем Z 
     var z1: number = this .interpolate (pa.z, pb.z, градиент1); 
     var z2: number = this .interpolate (pc.z, pd.z, градиент2); 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) { 
         градиент var : число = (x - sx) / (ex - sx);  // нормализация вкуса 
          var z = this .interpolate (z1, z2, Градиент); 
          this .drawPoint ( новый BABYLON.Vector3 (x, y, z), цвет); 
     } 
 

  Функция Device (canvas) { 
     this .workingCanvas = canvas; 
     this .workingWidth = canvas.width; 
     this .workingHeight = canvas.height; 
     this .workingContext = this .workingCanvas.getContext ( "2d" ); 
     this .depthbuffer = new Array ( this .workingWidth * this .workingHeight); 
  // Эта функция вызывается для очистки заднего буфера с определенным цветом 
  Device.prototype.clear = function () { 
     // Очистка с черным цветом по умолчанию 
     this .workingContext.clearRect (0, 0, this .workingWidth, this .workingHeight); 
     // после очистки с черными пикселями мы возвращаем связанные данные изображения в  
     // очищаем задний буфер 
     this .backbuffer = this .workingContext.getImageData (0, 0, this .workingWidth, this .workingHeight); 
      // Очистка буфера глубины 
     for ( var i = 0; i < this .depthbuffer.length; i ++) { 
         // Максимально возможное значение  
         this .depthbuffer [i] = 10000000; 
     } 
  ; 
  // Вызывается поставить пиксель на экран с определенными координатами X, Y 
  Device.prototype.putPixel = function (x, y, z, color) { 
     this .backbufferdata = this .backbuffer.data; 
     // Поскольку у нас есть 1-D массив для нашего заднего буфера 
     // нам нужно знать эквивалентный индекс ячейки в 1-D 
     // на 2D координатах экрана 
     var index = ((x >> 0) + (y >> 0) * this .workingWidth); 
     var index4 = index * 4; 
      if ( this .depthbuffer [index] <z) { 
         возврат ;  // Сброс 
     } 
      this .depthbuffer [index] = z; 
      // Цветовое пространство RGBA используется холстом HTML5  
     this .backbufferdata [index4] = color.r * 255; 
     это .backbufferdata [index4 + 1] = color.g * 255; 
     this .backbufferdata [index4 + 2] = color.b * 255; 
     this .backbufferdata [index4 + 3] = color.a * 255; 
  ; 
  // Проект берет несколько трехмерных координат и преобразует их 
  / в 2D координатах с использованием матрицы преобразования 
  Device.prototype.project = function (ordin, TransMat) { 
     // преобразование координат 
     var point = BABYLON.Vector3.TransformCoordinates (координировать, transMat); 
     // Преобразованные координаты будут основаны на системе координат 
     // начиная с центра экрана.  Но рисование на экране обычно начинается 
     // сверху слева.  Затем нам нужно снова преобразовать их так, чтобы x: 0, y: 0 слева вверху. 
     var x = point.x * this .workingWidth + this .workingWidth / 2.0; 
     var y = -point.y * this .workingHeight + this .workingHeight / 2.0; 
     return ( новый BABYLON.Vector3 (x, y, point.z)); 
  ; 
  // drawPoint вызывает putPixel, но выполняет операцию отсечения до 
  Device.prototype.drawPoint = function (point, color) { 
     // Отсечение того, что видно на экране 
     if (point.x> = 0 && point.y> = 0 && point.x < this .workingWidth && point.y < this .workingHeight) { 
         // Рисуем точку 
         this .putPixel (point.x, point.y, point.z, color); 
     } 
  ; 
  // рисуем линию между 2 точками слева направо 
  / papb -> pcpd 
  / pa, pb, pc, pd должны быть отсортированы до 
  Device.prototype.processScanLine = function (y, pa, pb, pc, pd, color) { 
     // Благодаря текущему Y мы можем вычислить градиент для вычисления других значений, таких как 
     // начальный X (sx) и конечный X (ex) для рисования между 
     // если pa.Y == pb.Y или pc.Y == pd.Y, градиент равен 1 
     var градиент1 = pa.y! = pb.y?  (y - pa.y) / (pb.y - pa.y): 1; 
     var градиент2 = pc.y! = pd.y?  (y - pc.y) / (pd.y - pc.y): 1; 
      var sx = this .interpolate (pa.x, pb.x, градиент1) >> 0; 
     var ex = this .interpolate (pc.x, pd.x, градиент2) >> 0; 
      // начинаем Z и заканчиваем Z 
     var z1 = this .interpolate (pa.z, pb.z, градиент1); 
     var z2 = this .interpolate (pc.z, pd.z, градиент2); 
      // рисуем линию слева (sx) вправо (ex)  
     для ( var x = sx; x <ex; x ++) { 
         градиент var = (x - sx) / (ex - sx); 
         var z = this .interpolate (z1, z2, Градиент); 
         this .drawPoint ( новый BABYLON.Vector3 (x, y, z), цвет); 
     } 
  ; 

Используя этот новый код, вы должны получить тот же тип рендеринга, что и встроенный в самом начале этой статьи iframe.

Как обычно, вы можете скачать решения, содержащие исходный код:

C # : SoftEngineCSharpPart4.zip

TypeScript : SoftEngineTSPart4.zip

JavaScript : SoftEngineJSPart4.zip или просто щелкните правой кнопкой мыши -> просмотреть источник в первом встроенном фрейме

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

образ

Но перед этим у меня есть дополнительный бонусный урок по оптимизации и параллелизму, объясняющий, как улучшить текущий алгоритм благодаря Parallel.For в C # и почему у нас не может быть такой же оптимизации в JavaScript. Ищите это завтра.

Первоначально опубликовано: http://blogs.msdn.com/b/davrous/archive/2013/06/21/tutorial-part-4-learning-how-to-write-a-3d-software-engine-in-c -ts-or-js-rasterization-amp-z-buffering.aspx . Перепечатано здесь с разрешения автора.